Skip to content

Commit

Permalink
Merge pull request #8358 from renanfranca/4339-add-tailwindcss-to-thy…
Browse files Browse the repository at this point in the history
…meleaf-template

add tailwindcss to thymeleaf template module
  • Loading branch information
DanielFran authored Jan 8, 2024
2 parents 4848c46 + 05a9412 commit 35e53fb
Show file tree
Hide file tree
Showing 17 changed files with 480 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ public JHipsterModule buildThymeleafTemplateModule(JHipsterModuleProperties prop
return thymeleafTemplateFactory.buildModule(properties);
}

public JHipsterModule buildThymeleafTemplateTailwindcssModule(JHipsterModuleProperties properties) {
return thymeleafTemplateFactory.buildTailwindcssModule(properties);
}

public JHipsterModule buildThymeleafHtmxWebjarsModule(JHipsterModuleProperties properties) {
return thymeleafTemplateFactory.buildHtmxWebjarsModule(properties);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,36 +1,153 @@
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 java.util.regex.Pattern;
import tech.jhipster.lite.generator.client.common.domain.ClientsModulesFactory;
import tech.jhipster.lite.module.domain.JHipsterModule;
import tech.jhipster.lite.module.domain.JHipsterProjectFilePath;
import tech.jhipster.lite.module.domain.file.JHipsterDestination;
import tech.jhipster.lite.module.domain.file.JHipsterSource;
import tech.jhipster.lite.module.domain.properties.JHipsterModuleProperties;
import tech.jhipster.lite.module.domain.replacement.ElementReplacer;
import tech.jhipster.lite.module.domain.replacement.RegexReplacer;
import tech.jhipster.lite.shared.error.domain.Assert;

public class ThymeleafTemplateModuleFactory {

private static final String PROPERTIES = "properties";
private static final JHipsterSource SOURCE = from("server/springboot/thymeleaf/template/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 String STATIC_IMAGES = "static/images";
private static final String MAIN_HTML = "main.html";
private static final String POSTCSS_CONFIG_JS = "postcss.config.js";
private static final String TAILWIND_CONFIG_JS = "tailwind.config.js";

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 JHipsterProjectFilePath MAIN_RESOURCES_PATH = path("src/main/resources");
private static final JHipsterDestination DESTINATION = to(MAIN_RESOURCES_PATH.get());
private static final JHipsterDestination ROOT_DESTINATION = to(".");

private static final String MAIN_SCRIPT_NEEDLE = "<!-- jhipster-needle-thymeleaf-main-script -->";
private static final String THYMELEAF_POSTCSS_PLUGINS_NEEDLE = "// jhipster-needle-thymeleaf-postcss-plugins";
private static final String THYMELEAF_CSS_NEEDLE = "/* jhipster-needle-thymeleaf-css */";
private static final Pattern WELCOME_THYMELEAF_MESSAGE_PATTERN = Pattern.compile(
"<div>Welcome to your Spring Boot with Thymeleaf project!</div>"
);
private static final ElementReplacer EXISTING_WELCOME_THYMELEAF_MESSAGE_NEEDLE = new RegexReplacer(
(contentBeforeReplacement, replacement) -> WELCOME_THYMELEAF_MESSAGE_PATTERN.matcher(contentBeforeReplacement).find(),
WELCOME_THYMELEAF_MESSAGE_PATTERN
);

private static final String TAILWINDCSS_REQUIRE = " ,require('tailwindcss')";
private static final String TAILWINDCSS_SETUP =
"""
/*! @import */
@tailwind base;
@tailwind components;
@tailwind utilities;
""";
private static final String TAILWINDCSS_WELCOME_THYMELEAF_MESSAGE =
"""
<main class="flex flex-col min-h-screen w-full justify-center">
<section
class="flex flex-col w-full py-16 md:py-24 border-2 border-dashed border-green-500"
>
<div
class="flex flex-col w-full max-w-7xl mx-auto px-4 md:px-8 xl:px-20 gap-8"
>
<div class="flex justify-center items-center gap-2">
<img
class="w-36 h-36"
th:src="@{/images/ThymeleafLogo.png}"
alt="Thymeleaf Logo"
/>
<h1 class="text-6xl font-bold">Thymeleaf</h1>
</div>
<div class="flex justify-center">
<div class="text-lg">
Welcome to your Spring Boot with Thymeleaf project!
</div>
</div>
</div>
</section>
</main>
""";

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(THYMELEAF_POSTCSS_PLUGINS_NEEDLE), TAILWINDCSS_REQUIRE)
.and()
.in(path("src/main/resources/static/css/application.css"))
.add(lineBeforeText(THYMELEAF_CSS_NEEDLE), TAILWINDCSS_SETUP)
.and()
.in(path("src/main/resources/templates/index.html"))
.add(EXISTING_WELCOME_THYMELEAF_MESSAGE_NEEDLE, TAILWINDCSS_WELCOME_THYMELEAF_MESSAGE)
.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))
.add(RESOURCES_SOURCE.append(STATIC_IMAGES).file("ThymeleafLogo.png"), DESTINATION.append(STATIC_IMAGES).append("ThymeleafLogo.png"))
.and()
.build();
//@formatter:on
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class ThymeleafTemplateModuleConfiguration {
private static final String TAG_SPRING = "spring";
private static final String TAG_BOOT = "spring-boot";
private static final String TAG_THYMELEAF = "thymeleaf";
private static final String TAG_TAILWINDCSS = "tailwindcss";
private static final String TAG_WEBJAR = "webjar";

@Bean
Expand All @@ -33,6 +34,20 @@ public JHipsterModuleResource thymeleafTemplateModule(ThymeleafTemplateModuleApp
.factory(thymeleafTemplate::buildThymeleafTemplateModule);
}

@Bean
public JHipsterModuleResource thymeleafTemplateTailwindcssModule(ThymeleafTemplateModuleApplicationService thymeleafTemplate) {
return JHipsterModuleResource
.builder()
.slug(THYMELEAF_TEMPLATE_TAILWINDCSS)
.propertiesDefinition(
JHipsterModulePropertiesDefinition.builder().addBasePackage().addProjectBaseName().addConfigurationFormat().build()
)
.apiDoc(capitalize(TAG_THYMELEAF), "Add tailwindcss to the thymeleaf template")
.organization(JHipsterModuleOrganization.builder().addDependency(THYMELEAF_TEMPLATE).build())
.tags(TAG_SERVER, TAG_SPRING, TAG_BOOT, TAG_THYMELEAF, TAG_TAILWINDCSS)
.factory(thymeleafTemplate::buildThymeleafTemplateTailwindcssModule);
}

@Bean
public JHipsterModuleResource thymeleafTemplateHtmxWebjarsModule(ThymeleafTemplateModuleApplicationService thymeleafTemplate) {
return JHipsterModuleResource
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ public enum JHLiteModuleSlug implements JHipsterModuleSlugFactory {
THYMELEAF_TEMPLATE("thymeleaf-template"),
THYMELEAF_TEMPLATE_HTMX_WEBJAR("thymeleaf-template-htmx-webjars"),
THYMELEAF_TEMPLATE_ALPINEJS_WEBJAR("thymeleaf-template-alpinejs-webjars"),
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.18.0",
"@typescript-eslint/parser": "6.18.0",
"autoprefixer": "10.4.16",
"browser-sync": "2.29.3",
"cssnano": "6.0.1",
"cypress": "13.6.2",
"eslint": "8.56.0",
"eslint-import-resolver-typescript": "3.6.1",
Expand All @@ -20,12 +24,20 @@
"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-gherkin": "2.2.1",
"prettier-plugin-java": "2.5.0",
"prettier-plugin-packagejson": "2.4.9",
"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 */
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/main}">
<head>
<title>Thymeleaf project</title>
</head>
<body>
<div layout:fragment="content">
<div>Welcome to your Spring Boot with Thymeleaf project!</div>
<!-- jhipster-needle-thymeleaf-index-content -->
</div>
<th:block layout:fragment="script-content">
<!-- Add additional scripts there that are only needed for this page (Application wide scripts should be added in layout/main.html) --->
<!-- jhipster-needle-thymeleaf-index-script -->
</th:block>
</body>
<html
lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/main}"
>
<head>
<title>Thymeleaf project</title>
</head>
<body>
<div layout:fragment="content">
<div>Welcome to your Spring Boot with Thymeleaf project!</div>
<!-- jhipster-needle-thymeleaf-index-content -->
</div>
<th:block layout:fragment="script-content">
<!-- Add additional scripts there that are only needed for this page (Application wide scripts should be added in layout/main.html) --->
<!-- jhipster-needle-thymeleaf-index-script -->
</th:block>
</body>
</html>
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: [],
}
13 changes: 13 additions & 0 deletions src/test/features/server/springboot/springboot-thymeleaf.feature
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,28 @@ 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 |
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 |

Scenario: Should apply Thymeleaf Template htmx, alpinejs webjars
When I apply modules to default project
| init |
| maven-java |
| spring-boot |
| spring-boot-thymeleaf |
Expand Down
Loading

0 comments on commit 35e53fb

Please sign in to comment.