1818import java .io .File ;
1919import java .io .IOException ;
2020import 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 ;
2232import 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
2437import static com .vaadin .flow .server .frontend .FrontendUtils .TAILWIND_CSS ;
2538
3447public 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}
0 commit comments