Skip to content

Commit

Permalink
add tailwindcss to thymeleaf template module
Browse files Browse the repository at this point in the history
See #4339
  • Loading branch information
renanfranca committed Dec 14, 2023
1 parent 3b7cd82 commit 4796c10
Show file tree
Hide file tree
Showing 14 changed files with 287 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,8 @@ public ThymeleafTemplateModuleApplicationService() {
public JHipsterModule buildThymeleafTemplateModule(JHipsterModuleProperties properties) {
return thymeleafTemplateFactory.buildModule(properties);
}

public JHipsterModule buildThymeleafTemplateTailwindcssModule(JHipsterModuleProperties properties) {
return thymeleafTemplateFactory.buildTailwindcssModule(properties);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package tech.jhipster.lite.generator.server.springboot.thymeleaf.template.domain;

import static tech.jhipster.lite.module.domain.JHipsterModule.*;
import static tech.jhipster.lite.module.domain.packagejson.VersionSource.*;

import tech.jhipster.lite.generator.client.common.domain.ClientsModulesFactory;
import tech.jhipster.lite.module.domain.JHipsterModule;
import tech.jhipster.lite.module.domain.file.JHipsterDestination;
import tech.jhipster.lite.module.domain.file.JHipsterSource;
Expand All @@ -10,22 +12,92 @@

public class ThymeleafTemplateModuleFactory {

private static final JHipsterSource SOURCE = from("server/springboot/thymeleaf/template/src/main/resources");
private static final JHipsterSource SOURCE = from("server/springboot/thymeleaf/template");
private static final JHipsterSource DOCUMENTATION_SOURCE = SOURCE.append("documentation");
private static final JHipsterSource RESOURCES_SOURCE = SOURCE.append("src/main/resources");
private static final String TEMPLATES = "templates";
private static final String TEMPLATES_LAYOUT = "templates/layout";
private static final String STATIC_CSS = "static/css";

private static final JHipsterDestination DESTINATION = to("src/main/resources");
private static final JHipsterDestination ROOT_DESTINATION = to(".");

private static final String POSTCSS_CONFIG_JS = "postcss.config.js";
private static final String TAILWIND_CONFIG_JS = "tailwind.config.js";

private static final String TAILWINDCSS_REQUIRE = " ,require('tailwindcss')";
private static final String TAILWINDCSS_SETUP =
"""
@tailwind base;
@tailwind components;
@tailwind utilities;
""";

public JHipsterModule buildModule(JHipsterModuleProperties properties) {
Assert.notNull("properties", properties);

//@formatter:off
return moduleBuilder(properties)
return ClientsModulesFactory.clientModuleBuilder(properties)
.documentation(documentationTitle("Thymeleaf"), DOCUMENTATION_SOURCE.template("thymeleaf.md"))
.packageJson()
.addDevDependency(packageName("@babel/cli"), COMMON)
.addDevDependency(packageName("autoprefixer"), COMMON)
.addDevDependency(packageName("browser-sync"), COMMON)
.addDevDependency(packageName("cssnano"), COMMON)
.addDevDependency(packageName("mkdirp"), COMMON)
.addDevDependency(packageName("npm-run-all"), COMMON)
.addDevDependency(packageName("onchange"), COMMON)
.addDevDependency(packageName("path-exists-cli"), COMMON)
.addDevDependency(packageName("postcss"), COMMON)
.addDevDependency(packageName("postcss-cli"), COMMON)
.addDevDependency(packageName("recursive-copy-cli"), COMMON)
.addScript(scriptKey("build"), scriptCommand("npm-run-all --parallel build:*"))
.addScript(scriptKey("build:html"), scriptCommand("recursive-copy \\\\\"src/main/resources/templates\\\\\" target/classes/templates -w"))
.addScript(scriptKey("build:css"), scriptCommand("mkdirp target/classes/static/css && postcss src/main/resources/static/css/*.css -d target/classes/static/css"))
.addScript(scriptKey("build:js"), scriptCommand("path-exists src/main/resources/static/js && (mkdirp target/classes/static/js && babel src/main/resources/static/js/ --out-dir target/classes/static/js/) || echo \\\\\"No 'src/main/resources/static/js' directory found.\\\\\""))
.addScript(scriptKey("build:svg"), scriptCommand("path-exists src/main/resources/static/svg && recursive-copy \\\\\"src/main/resources/static/svg\\\\\" target/classes/static/svg -w -f \\\\\"**/*.svg\\\\\" || echo \\\\\"No 'src/main/resources/static/svg' directory found.\\\\\""))
.addScript(scriptKey("build-prod"), scriptCommand("NODE_ENV='production' npm-run-all --parallel build-prod:*"))
.addScript(scriptKey("build-prod:html"), scriptCommand("npm run build:html"))
.addScript(scriptKey("build-prod:css"), scriptCommand("npm run build:css"))
.addScript(scriptKey("build-prod:js"), scriptCommand("path-exists src/main/resources/static/js && (mkdirp target/classes/static/js && babel src/main/resources/static/js/ --minified --out-dir target/classes/static/js/) || echo \\\\\"No 'src/main/resources/static/js' directory found.\\\\\""))
.addScript(scriptKey("build-prod:svg"), scriptCommand("npm run build:svg"))
.addScript(scriptKey("watch"), scriptCommand("npm-run-all --parallel watch:*"))
.addScript(scriptKey("watch:html"), scriptCommand("onchange \\\\\"src/main/resources/templates/**/*.html\\\\\" -- npm run build:html"))
.addScript(scriptKey("watch:css"), scriptCommand("onchange \\\\\"src/main/resources/static/css/**/*.css\\\\\" -- npm run build:css"))
.addScript(scriptKey("watch:js"), scriptCommand("onchange \\\\\"src/main/resources/static/js/**/*.js\\\\\" -- npm run build:js"))
.addScript(scriptKey("watch:svg"), scriptCommand("onchange \\\\\"src/main/resources/static/svg/**/*.svg\\\\\" -- npm run build:svg"))
.addScript(scriptKey("watch:serve"), scriptCommand("browser-sync start --proxy localhost:8080 --files \\\\\"target/classes/templates\\\\\" \\\\\"target/classes/static\\\\\""))
.and()
.files()
.add(RESOURCES_SOURCE.append(TEMPLATES).template("index.html"), toSrcMainResources().append(TEMPLATES).append("index.html"))
.add(RESOURCES_SOURCE.append(TEMPLATES_LAYOUT).template("main.html"), toSrcMainResources().append(TEMPLATES_LAYOUT).append("main.html"))
.add(RESOURCES_SOURCE.append(STATIC_CSS).template("application.css"), DESTINATION.append(STATIC_CSS).append("application.css"))
.add(SOURCE.template(POSTCSS_CONFIG_JS), ROOT_DESTINATION.append(POSTCSS_CONFIG_JS))
.and()
.build();
//@formatter:on
}

public JHipsterModule buildTailwindcssModule(JHipsterModuleProperties properties) {
Assert.notNull("properties", properties);

//@formatter:off
return ClientsModulesFactory.clientModuleBuilder(properties)
.packageJson()
.addDevDependency(packageName("tailwindcss"), COMMON)
.addScript(scriptKey("watch:html"), scriptCommand("onchange \\\\\\\"src/main/resources/templates/**/*.html\\\\\\\" -- npm-run-all --serial build:css build:HTML"))
.addScript(scriptKey("watch:serve"), scriptCommand("browser-sync start --no-inject-changes --proxy localhost:8080 --files \\\\\\\"target/classes/templates\\\\\\\" \\\\\\\"target/classes/static\\\\\\\""))
.and()
.mandatoryReplacements()
.in(path(POSTCSS_CONFIG_JS))
.add(lineBeforeText("// jhipster-needle-thymeleaf-postcss-plugins"), TAILWINDCSS_REQUIRE)
.and()
.in(path("src/main/resources/static/css/application.css"))
.add(lineBeforeText("/* jhipster-needle-thymeleaf-css */"), TAILWINDCSS_SETUP)
.and()
.and()
.files()
.add(SOURCE.append(TEMPLATES).template("index.html"), toSrcMainResources().append(TEMPLATES).append("index.html"))
.add(SOURCE.append(TEMPLATES_LAYOUT).template("main.html"), toSrcMainResources().append(TEMPLATES_LAYOUT).append("main.html"))
.add(SOURCE.append(STATIC_CSS).template("application.css"), DESTINATION.append(STATIC_CSS).append("application.css"))
.add(SOURCE.template(TAILWIND_CONFIG_JS), ROOT_DESTINATION.append(TAILWIND_CONFIG_JS))
.and()
.build();
//@formatter:on
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,18 @@ public JHipsterModuleResource thymeleafTemplateModule(ThymeleafTemplateModuleApp
.tags("server", "spring", "spring-boot", "thymeleaf")
.factory(thymeleafTemplate::buildThymeleafTemplateModule);
}

@Bean
public JHipsterModuleResource thymeleafTemplateTailwindcssModule(ThymeleafTemplateModuleApplicationService thymeleafTemplate) {
return JHipsterModuleResource
.builder()
.slug(THYMELEAF_TEMPLATE_TAILWINDCSS)
.propertiesDefinition(
JHipsterModulePropertiesDefinition.builder().addBasePackage().addProjectBaseName().addConfigurationFormat().build()
)
.apiDoc("Thymeleaf", "Add tailwindcss to the thymeleaf template")
.organization(JHipsterModuleOrganization.builder().addDependency(THYMELEAF_TEMPLATE).build())
.tags("server", "spring", "spring-boot", "thymeleaf", "tailwindcss")
.factory(thymeleafTemplate::buildThymeleafTemplateTailwindcssModule);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ public enum JHLiteModuleSlug implements JHipsterModuleSlugFactory {
SPRING_BOOT_MVC_EMPTY("spring-boot-mvc-empty"),
SPRING_BOOT_THYMELEAF("spring-boot-thymeleaf"),
THYMELEAF_TEMPLATE("thymeleaf-template"),
THYMELEAF_TEMPLATE_TAILWINDCSS("thymeleaf-template-tailwindcss"),
SPRING_BOOT_TOMCAT("spring-boot-tomcat"),
SPRING_BOOT_UNDERTOW("spring-boot-undertow"),
SPRING_BOOT_WEBFLUX_EMPTY("spring-boot-webflux-empty"),
Expand Down
12 changes: 12 additions & 0 deletions src/main/resources/generator/dependencies/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@
"description": "JHipster Lite : used for Dependencies",
"license": "Apache-2.0",
"devDependencies": {
"@babel/cli": "^7.23.4",
"@playwright/test": "1.40.1",
"@prettier/plugin-xml": "3.2.2",
"@types/jest": "29.5.11",
"@types/node": "20.9.5",
"@typescript-eslint/eslint-plugin": "6.14.0",
"@typescript-eslint/parser": "6.14.0",
"autoprefixer": "^10.4.16",
"browser-sync": "^2.29.3",
"cssnano": "^6.0.1",
"cypress": "13.6.1",
"eslint": "8.55.0",
"eslint-import-resolver-typescript": "3.6.1",
Expand All @@ -20,11 +24,19 @@
"jasmine-core": "5.1.1",
"jest": "29.7.0",
"lint-staged": "15.2.0",
"mkdirp": "^3.0.1",
"node": "20.10.0",
"npm": "10.2.5",
"npm-run-all": "^4.1.5",
"onchange": "^7.1.0",
"path-exists-cli": "^2.0.0",
"postcss": "^8.4.32",
"postcss-cli": "^11.0.0",
"prettier": "3.1.1",
"prettier-plugin-java": "2.5.0",
"prettier-plugin-packagejson": "2.4.7",
"recursive-copy-cli": "^1.0.20",
"tailwindcss": "^3.3.6",
"ts-jest": "29.1.1",
"typescript": "5.3.3"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Getting Started

### Reference Documentation

* [Thymeleaf](https://www.thymeleaf.org)

### Guides
The following guides illustrate how to use some features concretely:

* [Handling Form Submission](https://spring.io/guides/gs/handling-form-submission/)
* [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/)
* [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/)
* [Building REST services with Spring](https://spring.io/guides/tutorials/rest/)

# Live reload setup

This project uses NPM and spring-boot-devtools module to have live reloading.

Use the following steps to get it working:

1. Run the Spring Boot application with the `default` profile: `./mvnw`
2. From a terminal, run `npm run build && npm run watch` (You can also run `npm run --silent build && npm run --silent watch` if you want less output in the terminal)
3. Your default browser will open at http://localhost:3000

You should now be able to change any HTML or CSS and have the browser reload upon saving the file.

NOTE: If you use a separate authentication server (e.g. social logins, or Keycloak) then after login,
you might get redirected to http://localhost:8080 as opposed to http://localhost:3000.
Be sure to set the port back to `3000` in your browser to have live reload.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const postcssConfig = {
plugins: [
require('autoprefixer'),
// jhipster-needle-thymeleaf-postcss-plugins
],
};

// If we are in production mode, then add cssnano
if (process.env.NODE_ENV === 'production') {
postcssConfig.plugins.push(
require('cssnano')({
// use the safe preset so that it doesn't
// mutate or remove code from our css
preset: 'default',
}),
);
}
module.exports = postcssConfig;
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<!-- jhipster-needle-thymeleaf-css -->
/* jhipster-needle-thymeleaf-css */
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/main/resources/templates/**/*.html'],
theme: {
extend: {},
},
plugins: [],
}
14 changes: 13 additions & 1 deletion src/test/features/server/springboot/springboot-thymeleaf.feature
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,21 @@ Feature: Spring Boot Thymeleaf module

Scenario: Should apply Thymeleaf Template module
When I apply modules to default project
| init |
| maven-java |
| spring-boot |
| spring-boot-thymeleaf |
| thymeleaf-template |
| thymeleaf-template |
Then I should have files in "src/main/resources/templates"
| index.html |

Scenario: Should apply Thymeleaf Template Tailwindcss module
When I apply modules to default project
| init |
| maven-java |
| spring-boot |
| spring-boot-thymeleaf |
| thymeleaf-template |
| thymeleaf-template-tailwindcss |
Then I should have files in ""
| tailwind.config.js |
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,99 @@ class ThymeleafTemplateModuleFactoryTest {
void shouldCreateThymeleafTemplateModule() {
JHipsterModuleProperties properties = JHipsterModulesFixture
.propertiesBuilder(TestFileUtils.tmpDirForTest())
.projectBaseName("jhipster")
.projectBaseName("jhiTest")
.basePackage("com.jhipster.test")
.build();

JHipsterModule module = factory.buildModule(properties);

assertThatModuleWithFiles(module)
//@formatter:off
assertThatModuleWithFiles(module, packageJsonFile())
.hasFiles("documentation/thymeleaf.md")
.hasFile("package.json")
.containing(nodeDependency("@babel/cli"))
.containing(nodeDependency("autoprefixer"))
.containing(nodeDependency("browser-sync"))
.containing(nodeDependency("cssnano"))
.containing(nodeDependency("mkdirp"))
.containing(nodeDependency("npm-run-all"))
.containing(nodeDependency("onchange"))
.containing(nodeDependency("path-exists-cli"))
.containing(nodeDependency("postcss"))
.containing(nodeDependency("postcss-cli"))
.containing(nodeDependency("recursive-copy-cli"))
.containing("\"build\": \"npm-run-all --parallel build:*\"")
.containing("\"build:html\": \"recursive-copy \\\"src/main/resources/templates\\\" target/classes/templates -w\"")
.containing(
"\"build:css\": \"mkdirp target/classes/static/css && postcss src/main/resources/static/css/*.css -d target/classes/static/css\""
)
.containing(
"\"build:js\": \"path-exists src/main/resources/static/js && (mkdirp target/classes/static/js && babel src/main/resources/static/js/ --out-dir target/classes/static/js/) || echo \\\"No 'src/main/resources/static/js' directory found.\\\"\""
)
.containing(
"\"build:svg\": \"path-exists src/main/resources/static/svg && recursive-copy \\\"src/main/resources/static/svg\\\" target/classes/static/svg -w -f \\\"**/*.svg\\\" || echo \\\"No 'src/main/resources/static/svg' directory found.\\\"\""
)
.containing("\"build-prod\": \"NODE_ENV='production' npm-run-all --parallel build-prod:*\"")
.containing("\"build-prod:html\": \"npm run build:html\"")
.containing("\"build-prod:css\": \"npm run build:css\"")
.containing(
"\"build-prod:js\": \"path-exists src/main/resources/static/js && (mkdirp target/classes/static/js && babel src/main/resources/static/js/ --minified --out-dir target/classes/static/js/) || echo \\\"No 'src/main/resources/static/js' directory found.\\\"\""
)
.containing("\"build-prod:svg\": \"npm run build:svg\"")
.containing("\"watch\": \"npm-run-all --parallel watch:*\"")
.containing("\"watch:html\": \"onchange \\\"src/main/resources/templates/**/*.html\\\" -- npm run build:html\"")
.containing("\"watch:css\": \"onchange \\\"src/main/resources/static/css/**/*.css\\\" -- npm run build:css\"")
.containing("\"watch:js\": \"onchange \\\"src/main/resources/static/js/**/*.js\\\" -- npm run build:js\"")
.containing("\"watch:svg\": \"onchange \\\"src/main/resources/static/svg/**/*.svg\\\" -- npm run build:svg\"")
.containing(
"\"watch:serve\": \"browser-sync start --proxy localhost:8080 --files \\\"target/classes/templates\\\" \\\"target/classes/static\\\"\""
)
.and()
.hasFiles("src/main/resources/templates/index.html")
.hasFiles("src/main/resources/templates/layout/main.html")
.hasFiles("src/main/resources/static/css/application.css");
.hasFiles("src/main/resources/static/css/application.css")
.hasFiles("postcss.config.js");
//@formatter:on
}

@Test
void shouldCreateTailwindcssModule() {
JHipsterModuleProperties properties = JHipsterModulesFixture
.propertiesBuilder(TestFileUtils.tmpDirForTest())
.projectBaseName("jhipster")
.build();

JHipsterModule module = factory.buildTailwindcssModule(properties);

//@formatter:off
assertThatModuleWithFiles(
module,
packageJsonFile(),
new ModuleFile("src/test/resources/projects/thymeleaf/postcss.config.js.template", "postcss.config.js"),
new ModuleFile("src/test/resources/projects/thymeleaf/application.css.template", "src/main/resources/static/css/application.css")
)
.hasFile("package.json")
.containing(nodeDependency("tailwindcss"))
.containing(
"\"watch:html\": \"onchange \\\"src/main/resources/templates/**/*.html\\\" -- npm-run-all --serial build:css build:HTML\""
)
.containing(
"\"watch:serve\": \"browser-sync start --no-inject-changes --proxy localhost:8080 --files \\\"target/classes/templates\\\" \\\"target/classes/static\\\"\""
)
.and()
.hasFile("postcss.config.js")
.containing(",require('tailwindcss')")
.and()
.hasFile("src/main/resources/static/css/application.css")
.containing(
"""
@tailwind base;
@tailwind components;
@tailwind utilities;
"""
)
.and()
.hasFiles("tailwind.config.js");
//@formatter:on
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/* jhipster-needle-thymeleaf-css */
19 changes: 19 additions & 0 deletions src/test/resources/projects/thymeleaf/postcss.config.js.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const postcssConfig = {
plugins: [
require('autoprefixer'),
// jhipster-needle-thymeleaf-postcss-plugins
],
};

// If we are in production mode, then add cssnano
if (process.env.NODE_ENV === 'production') {
postcssConfig.plugins.push(
require('cssnano')({
// use the safe preset so that it doesn't
// mutate or remove code from our css
preset: 'default',
}),
);
}

module.exports = postcssConfig;
1 change: 1 addition & 0 deletions tests-ci/generate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,7 @@ elif [[ $application == 'thymeleafapp' ]]; then
applyModules \
"spring-boot-thymeleaf" \
"thymeleaf-template" \
"thymeleaf-template-tailwindcss" \
"webjars-locator" \
"htmx-webjars"

Expand Down

0 comments on commit 4796c10

Please sign in to comment.