@@ -59,8 +59,34 @@ public class CssBundler {
5959 private static final String MAYBE_LAYER_OR_MEDIA_QUERY = "(" + LAYER + "|"
6060 + MEDIA_QUERY + ")" ;
6161
62+ // Selects how url(...) references are rewritten when inlining @import
63+ // statements. The right choice depends on how the bundled CSS is later
64+ // delivered to the browser:
65+ //
66+ // THEMES — used for application themes; url() targets are rewritten to
67+ // absolute "VAADIN/themes/<theme>/..." paths because themes are
68+ // served from a known fixed location.
69+ //
70+ // STATIC_RESOURCES — used in dev mode for @StyleSheet files served from
71+ // public roots (META-INF/resources, webapp, ...). The dev-tools
72+ // live-reload client (vaadin-dev-tools.ts onUpdate) injects the
73+ // bundled content into an inline <style> tag and removes the
74+ // original <link>. An inline <style> has no URL of its own, so the
75+ // browser resolves any relative url() against the *page URL*, which
76+ // would point to the wrong folder. To stay correct we rewrite to
77+ // absolute paths rooted at the servlet context (e.g.
78+ // "/myapp/relurl-test/images/dot.svg").
79+ //
80+ // STATIC_RESOURCES_RELATIVE — used in prod by TaskProcessStylesheetCss.
81+ // The bundled CSS is written back in-place under META-INF/resources
82+ // and served via the original <link href>, so the browser still
83+ // fetches it from the entry file's URL. Relative url() therefore
84+ // resolves against the entry's folder and we just need to express
85+ // each url() relative to that folder. We can't use STATIC_RESOURCES
86+ // here because the deployment context path is not known at build
87+ // time.
6288 private enum BundleFor {
63- THEMES , STATIC_RESOURCES
89+ THEMES , STATIC_RESOURCES , STATIC_RESOURCES_RELATIVE
6490 }
6591
6692 /**
@@ -167,6 +193,36 @@ public static String inlineImportsForPublicResources(File baseFolder,
167193 BundleFor .STATIC_RESOURCES , contextPath , null );
168194 }
169195
196+ /**
197+ * Inlines imports for CSS files located under public static resources (e.g.
198+ * META-INF/resources) at build time, rewriting relative {@code url(...)}
199+ * references so they are expressed relative to the entry CSS file's folder.
200+ * <p>
201+ * Unlike {@link #inlineImportsForPublicResources(File, File, String)}, this
202+ * variant does not prepend a servlet context path. The resulting URLs are
203+ * purely relative, which is required when the CSS is processed at build
204+ * time and the deployment context path is not yet known.
205+ *
206+ * @param baseFolder
207+ * folder of the entry CSS file; inlined {@code url(...)}
208+ * references are rewritten to paths relative to this folder.
209+ * @param cssFile
210+ * the CSS file to process.
211+ * @param nodeModulesFolder
212+ * the node_modules folder for resolving npm package imports, or
213+ * {@code null} if node_modules resolution is not needed.
214+ * @return the processed stylesheet content, with inlined imports and
215+ * rewritten URLs.
216+ * @throws IOException
217+ * if filesystem resources cannot be read.
218+ */
219+ public static String inlineImportsForStaticResourcesRelative (
220+ File baseFolder , File cssFile , File nodeModulesFolder )
221+ throws IOException {
222+ return inlineImports (baseFolder , cssFile , new HashSet <>(),
223+ BundleFor .STATIC_RESOURCES_RELATIVE , null , nodeModulesFolder );
224+ }
225+
170226 /**
171227 * Internal implementation that can optionally skip URL rewriting.
172228 *
@@ -247,6 +303,9 @@ private static String inlineImports(File baseFolder, File cssFile,
247303 } else if (bundleFor == BundleFor .STATIC_RESOURCES ) {
248304 content = rewriteCssUrlsForStaticResources (baseFolder , cssFile ,
249305 contextPath , content );
306+ } else if (bundleFor == BundleFor .STATIC_RESOURCES_RELATIVE ) {
307+ content = rewriteCssUrlsForStaticResourcesRelative (baseFolder ,
308+ cssFile , content );
250309 }
251310 content = StringUtil .removeComments (content , true );
252311 List <String > unhandledImports = new ArrayList <>();
@@ -307,48 +366,76 @@ private static String inlineImports(File baseFolder, File cssFile,
307366
308367 private static String rewriteCssUrlsForStaticResources (File baseFolder ,
309368 File cssFile , String contextPath , String content ) {
310- // Public resources: rebase URLs from the current cssFile to the
311- // entry stylesheet base folder
369+ // Bundled CSS is delivered as inline <style> in dev mode, so url()s
370+ // need to be absolute paths rooted at the servlet context.
371+ return rewriteCssUrls (baseFolder , cssFile , content ,
372+ target -> rebaseToContextPath (baseFolder , contextPath , target ));
373+ }
374+
375+ private static String rewriteCssUrlsForStaticResourcesRelative (
376+ File baseFolder , File cssFile , String content ) {
377+ // Bundled CSS is written back to disk and served from the entry
378+ // file's URL in prod mode, so url()s can be expressed relative to
379+ // that entry folder.
380+ Path baseNormalized = baseFolder .toPath ().normalize ().toAbsolutePath ();
381+ return rewriteCssUrls (baseFolder , cssFile , content ,
382+ target -> baseNormalized .relativize (target ).toString ()
383+ .replace ('\\' , '/' ));
384+ }
385+
386+ /**
387+ * Common url() rewriting pipeline used by both static-resource bundling
388+ * strategies. Walks every {@code url(...)} in {@code content}, skips ones
389+ * that should be left untouched (empty, absolute, protocol-prefixed,
390+ * unresolvable, outside {@code baseFolder}, or pointing at a missing file),
391+ * and lets the caller decide how to format the kept ones via
392+ * {@code targetToUrl}.
393+ */
394+ private static String rewriteCssUrls (File baseFolder , File cssFile ,
395+ String content ,
396+ java .util .function .Function <Path , String > targetToUrl ) {
397+ Path baseNormalized = baseFolder .toPath ().normalize ().toAbsolutePath ();
312398 Matcher urlMatcher = URL_PATTERN .matcher (content );
313- content = urlMatcher .replaceAll (result -> {
399+ return urlMatcher .replaceAll (result -> {
314400 String url = getNonNullGroup (result , 2 , 3 , 4 );
315401 if (url == null || url .trim ().endsWith (".css" )) {
316- // CSS imports handled separately below
402+ // @import-style url()s are handled by import inlining, not
403+ // here.
317404 return Matcher .quoteReplacement (urlMatcher .group ());
318405 }
319406 String sanitized = sanitizeUrl (url );
320407 if (sanitized == null ) {
321408 return Matcher .quoteReplacement (urlMatcher .group ());
322409 }
323410 String trimmed = sanitized .trim ();
324- // Only handle relative URLs (no protocol, no leading slash, not
325- // data URIs)
326- // Treat only known protocols as absolute to avoid false
327- // positives like "my:file.css"
328- if (trimmed .startsWith ("/" )
411+ // Only handle relative URLs: not empty, no leading slash, no
412+ // known protocol prefix. Match only known protocols as absolute
413+ // to avoid false positives like "my:file.css".
414+ if (trimmed .isEmpty () || trimmed .startsWith ("/" )
329415 || PROTOCOL_PATTER_FOR_URLS .matcher (trimmed ).matches ()) {
330416 return Matcher .quoteReplacement (urlMatcher .group ());
331417 }
332418 try {
333419 Path target = cssFile .getParentFile ().toPath ().resolve (trimmed )
334- .normalize ();
335- // Only rewrite when we can safely confirm the target (in
336- // url()) file exists
337- if (Files .exists (target )) {
338- // For inline <style> tags, make URLs absolute to the
339- // application context
340- String rebased = rebaseToContextPath (baseFolder ,
341- contextPath , target );
342- return Matcher .quoteReplacement ("url('" + rebased + "')" );
420+ .normalize ().toAbsolutePath ();
421+ if (!target .startsWith (baseNormalized )) {
422+ // Target is outside the entry's base folder (e.g. an
423+ // npm-package CSS referencing a sibling file). Don't
424+ // invent a path for it.
425+ return Matcher .quoteReplacement (urlMatcher .group ());
343426 }
427+ if (!Files .exists (target )) {
428+ // Don't rewrite something we can't confirm.
429+ return Matcher .quoteReplacement (urlMatcher .group ());
430+ }
431+ return Matcher .quoteReplacement (
432+ "url('" + targetToUrl .apply (target ) + "')" );
344433 } catch (Exception e ) {
345- // On any resolution issue, keep the original
346434 getLogger ().debug ("Unable to resolve url: {}" ,
347435 urlMatcher .group ());
348436 }
349437 return Matcher .quoteReplacement (urlMatcher .group ());
350438 });
351- return content ;
352439 }
353440
354441 private static String rewriteCssUrlsForThemes (File baseFolder , File cssFile ,
0 commit comments