Skip to content

Commit 95038c5

Browse files
authored
fix: tailwind styles and add-on css import (#23303) (#23363)
Collect jar-resource and StyleSheet css as imports into tailwind.css Now tailwind knows to go through said files and where they are located. Fixes #23082 Fixes #23218
1 parent 1b6a376 commit 95038c5

File tree

5 files changed

+439
-16
lines changed

5 files changed

+439
-16
lines changed

flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskGenerateTailwindCss.java

Lines changed: 210 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,21 @@
1818
import java.io.File;
1919
import java.io.IOException;
2020
import java.io.InputStream;
21+
import java.nio.file.Path;
22+
import java.util.Collection;
23+
import java.util.HashSet;
24+
import java.util.List;
25+
import java.util.Set;
26+
import java.util.stream.Collectors;
2127

28+
import org.slf4j.Logger;
29+
import org.slf4j.LoggerFactory;
30+
31+
import com.vaadin.flow.component.dependency.StyleSheet;
2232
import com.vaadin.flow.internal.StringUtil;
33+
import com.vaadin.flow.server.frontend.scanner.ClassFinder;
34+
import com.vaadin.flow.server.frontend.scanner.CssData;
35+
import com.vaadin.flow.server.frontend.scanner.FrontendDependenciesScanner;
2336

2437
import static com.vaadin.flow.server.frontend.FrontendUtils.TAILWIND_CSS;
2538

@@ -34,8 +47,13 @@
3447
public class TaskGenerateTailwindCss extends AbstractTaskClientGenerator {
3548

3649
private static final String RELATIVE_SOURCE_PATH_MARKER = "#relativeSourcePath#";
50+
private static final String CSS_IMPORT_MARKER = "/* #cssImport# */";
51+
private static final String STYLES_CSS = "styles.css";
52+
private static final Logger log = LoggerFactory
53+
.getLogger(TaskGenerateTailwindCss.class);
3754

38-
private String relativeSourcePath;
55+
private final String relativeSourcePath;
56+
private final String themeImportReplacement;
3957

4058
private final File tailwindCss;
4159

@@ -46,13 +64,16 @@ public class TaskGenerateTailwindCss extends AbstractTaskClientGenerator {
4664
* the task options
4765
*/
4866
TaskGenerateTailwindCss(Options options) {
67+
4968
tailwindCss = new File(options.getFrontendGeneratedFolder(),
5069
TAILWIND_CSS);
5170
relativeSourcePath = options.getFrontendGeneratedFolder().toPath()
5271
.relativize(options.getNpmFolder().toPath().resolve("src"))
53-
.toString();
54-
// Use forward slash as a separator
55-
relativeSourcePath = relativeSourcePath.replace(File.separator, "/");
72+
.toString().replace(File.separator, "/");
73+
74+
// Import theme CSS files and @CssImport files to enable @apply
75+
// directive processing
76+
themeImportReplacement = buildCssImports(options);
5677
}
5778

5879
@Override
@@ -62,6 +83,8 @@ protected String getFileContent() throws IOException {
6283
var template = StringUtil.toUTF8String(indexStream);
6384
template = template.replace(RELATIVE_SOURCE_PATH_MARKER,
6485
relativeSourcePath);
86+
template = template.replace(CSS_IMPORT_MARKER,
87+
themeImportReplacement);
6588
return template;
6689
}
6790
}
@@ -75,4 +98,187 @@ protected File getGeneratedFile() {
7598
protected boolean shouldGenerate() {
7699
return true;
77100
}
101+
102+
private String buildCssImports(Options options) {
103+
StringBuilder imports = new StringBuilder();
104+
105+
addThemeImportIfAvailable(options, imports);
106+
107+
addCssImports(options, imports);
108+
109+
return imports.toString();
110+
}
111+
112+
/**
113+
* Add import to theme style.css if a legacy style theme is used.
114+
*
115+
* @param options
116+
* the task options
117+
* @param imports
118+
* the imports string builder
119+
*/
120+
private static void addThemeImportIfAvailable(Options options,
121+
StringBuilder imports) {
122+
123+
FrontendDependenciesScanner frontendDependenciesScanner = options
124+
.getFrontendDependenciesScanner();
125+
String themeName = "";
126+
if (frontendDependenciesScanner != null
127+
&& frontendDependenciesScanner.getThemeDefinition() != null) {
128+
themeName = frontendDependenciesScanner.getThemeDefinition()
129+
.getName();
130+
}
131+
132+
// Import theme's styles.css if theme exists
133+
if (themeName != null && !themeName.isEmpty()) {
134+
String themePath = "themes/" + themeName + "/" + STYLES_CSS;
135+
Path frontendGeneratedFolder = options.getFrontendGeneratedFolder()
136+
.toPath();
137+
138+
// Try frontend directory first
139+
File stylesCss = new File(options.getFrontendDirectory(),
140+
themePath);
141+
if (stylesCss.exists()) {
142+
String relativePath = frontendGeneratedFolder
143+
.relativize(stylesCss.toPath()).toString()
144+
.replace(File.separator, "/");
145+
imports.append("@import '").append(relativePath).append("';\n");
146+
} else if (options.getJarFrontendResourcesFolder() != null) {
147+
// Try JAR resources folder
148+
stylesCss = new File(options.getJarFrontendResourcesFolder(),
149+
themePath);
150+
if (stylesCss.exists()) {
151+
String relativePath = frontendGeneratedFolder
152+
.relativize(stylesCss.toPath()).toString()
153+
.replace(File.separator, "/");
154+
imports.append("@import './").append(relativePath)
155+
.append("';\n");
156+
}
157+
}
158+
}
159+
}
160+
161+
/**
162+
* Add all found CssImport and StyleSheet with found files into imports.
163+
*
164+
* @param options
165+
* the task options
166+
* @param imports
167+
* the imports string builder
168+
*/
169+
private void addCssImports(Options options, StringBuilder imports) {
170+
171+
Collection<String> cssImports = options.getFrontendDependenciesScanner()
172+
.getCss().values().stream().flatMap(List::stream)
173+
.map(CssData::getValue).collect(Collectors.toList());
174+
cssImports.addAll(collectStyleSheetAnnotations(options));
175+
176+
// Import all @CssImport CSS files that exist in the frontend directory
177+
for (String cssPath : cssImports) {
178+
if (cssPath != null && !cssPath.isEmpty()) {
179+
String cssFile = resolveCssFile(options, cssPath);
180+
if (cssFile != null) {
181+
imports.append("@import '").append(cssFile).append("';\n");
182+
}
183+
}
184+
}
185+
}
186+
187+
private String resolveCssFile(Options options, String cssPath) {
188+
// Handle Frontend/ alias
189+
if (cssPath.startsWith("Frontend/")) {
190+
cssPath = cssPath.substring("Frontend/".length());
191+
}
192+
// Handle ./ prefix
193+
if (cssPath.startsWith("./")) {
194+
cssPath = cssPath.substring(2);
195+
}
196+
197+
Path frontendGeneratedFolder = options.getFrontendGeneratedFolder()
198+
.toPath();
199+
200+
// Try frontend directory first
201+
File cssFile = new File(options.getFrontendDirectory(), cssPath);
202+
if (cssFile.exists()) {
203+
String relativePath = frontendGeneratedFolder
204+
.relativize(cssFile.toPath()).toString()
205+
.replace(File.separator, "/");
206+
return "./" + relativePath;
207+
}
208+
// Try jar resources folder
209+
if (options.getJarFrontendResourcesFolder() != null) {
210+
cssFile = new File(options.getJarFrontendResourcesFolder(),
211+
cssPath);
212+
if (cssFile.exists()) {
213+
String relativePath = frontendGeneratedFolder
214+
.relativize(cssFile.toPath()).toString()
215+
.replace(File.separator, "/");
216+
return "./" + relativePath;
217+
}
218+
}
219+
// Try resources directory
220+
File resourcesFolder = options.getNpmFolder().toPath()
221+
.resolve("src/main/resources/META-INF/resources/").toFile();
222+
223+
if (resourcesFolder.exists()) {
224+
cssFile = new File(resourcesFolder, cssPath);
225+
if (cssFile.exists()) {
226+
return options.getFrontendGeneratedFolder().toPath()
227+
.relativize(cssFile.toPath()).toString()
228+
.replace(File.separator, "/");
229+
}
230+
}
231+
232+
return null;
233+
}
234+
235+
/**
236+
* Scans the classpath for @StyleSheet annotations and collects the
237+
* referenced CSS file paths.
238+
*
239+
* @return set of CSS file paths referenced by @StyleSheet annotations
240+
*/
241+
private Set<String> collectStyleSheetAnnotations(Options options) {
242+
Set<String> cssPaths = new HashSet<>();
243+
ClassFinder classFinder = options.getClassFinder();
244+
245+
if (classFinder == null) {
246+
log.debug("ClassFinder not available, skipping scan");
247+
return cssPaths;
248+
}
249+
250+
try {
251+
for (Class<?> clazz : classFinder
252+
.getAnnotatedClasses(StyleSheet.class)) {
253+
for (StyleSheet annotation : clazz
254+
.getAnnotationsByType(StyleSheet.class)) {
255+
String value = annotation.value();
256+
if (isLocalStylesheet(value)) {
257+
cssPaths.add(value);
258+
}
259+
}
260+
}
261+
} catch (Exception e) {
262+
log.warn("Error scanning for @StyleSheet annotations", e);
263+
}
264+
265+
return cssPaths;
266+
}
267+
268+
/**
269+
* Checks if the stylesheet path is a local file (not an external URL).
270+
*
271+
* @param path
272+
* the stylesheet path from the annotation
273+
* @return true if it's a local file path
274+
*/
275+
private boolean isLocalStylesheet(String path) {
276+
if (path == null || path.isBlank()) {
277+
return false;
278+
}
279+
String lower = path.toLowerCase();
280+
// External URLs are ignored
281+
return !lower.startsWith("http://") && !lower.startsWith("https://");
282+
}
283+
78284
}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
@import 'tailwindcss/theme.css';
22
@import 'tailwindcss/utilities.css';
33

4-
@source "#relativeSourcePath#";
4+
/* #cssImport# */
5+
6+
@source '#relativeSourcePath#';

0 commit comments

Comments
 (0)