Skip to content

fix!: clarify @JsModule, @CssImport, @JavaScript, @StyleSheet semantics#24207

Draft
Artur- wants to merge 5 commits intomainfrom
feature/clarify-javascript-jsmodule-stylesheet
Draft

fix!: clarify @JsModule, @CssImport, @JavaScript, @StyleSheet semantics#24207
Artur- wants to merge 5 commits intomainfrom
feature/clarify-javascript-jsmodule-stylesheet

Conversation

@Artur-
Copy link
Copy Markdown
Member

@Artur- Artur- commented Apr 27, 2026

Establishes a clean model:

  • @JsModule / @CssImport: build-time bundle sources, fed into Vite. App source: src/main/frontend/. Addon source: META-INF/frontend/ (with META-INF/resources/frontend/ kept for compatibility but deprecated).
  • @StyleSheet / @JavaScript: runtime <link>/<script> elements served by the servlet container from META-INF/resources/. Relative URLs always resolve against the context root, regardless of servlet mapping. @JavaScript has a type attribute (Type.SCRIPT default, or Type.MODULE for <script type="module">), so CDN-hosted and hand-authored ES modules can be loaded at runtime through annotations as well.

Incompatible user-facing changes

  1. @StyleSheet("foo.css") on a Component, app on custom servlet mapping (e.g. /app/*): now resolves to <contextroot>/foo.css instead of 404'ing under /app/foo.css. Bug fix; nothing to do. If you actually wanted servlet-mapping-relative loading, write @StyleSheet("base://foo.css").

Compatible changes

  1. @StyleSheet("../foo.css") (any .. path): now logs a WARN and is silently dropped at component level too (AppShell already did this). Remove the traversal — put the file at a non-traversing path under META-INF/resources/ and reference it directly.
  2. Multiple variants of the same @StyleSheet file ("foo.css" + "./foo.css" + "context://foo.css"): now deduplicated to one <link> tag.
  3. @JavaScript("./foo.js") or @JavaScript("foo.js") (bare relative, default type=Type.SCRIPT): build-time WARN "uses the deprecated bundled interpretation. Prepend context:// for a runtime <script> tag, set type=Type.MODULE for a runtime <script type="module"> tag, or migrate to @JsModule for bundling." Still bundles for now. Pick one:
    • @JsModule("./foo.js") if you wanted Vite to bundle it (file stays in src/main/frontend/ or addon META-INF/frontend/);
    • @JavaScript("context://foo.js") for a plain runtime <script> (move file to src/main/resources/META-INF/resources/foo.js or addon META-INF/resources/foo.js);
    • @JavaScript(value="foo.js", type=Type.MODULE) for a runtime <script type="module"> (same file location as the previous option).
  4. @JsModule("https://cdn.example.com/foo.js") (or any http://, https://, //, context://, base://, /abs): build-time WARN "is a runtime URL. @JsModule is for build-time bundle sources only; use @JavaScript(value="...", type=Type.MODULE) for a runtime <script type="module"> tag." Still loaded at runtime as <script type="module"> (no longer also bundled). Switch to @JavaScript(value="https://cdn.example.com/foo.js", type=Type.MODULE) to preserve the module semantics, or to plain @JavaScript("https://cdn.example.com/foo.js") if you actually wanted a classic <script> tag.
  5. New @JavaScript.type() attribute with values Type.SCRIPT (default, current behavior) and Type.MODULE (new, renders <script type="module">). Source-compatible: existing @JavaScript annotations keep their previous behavior.
  6. Add-on JAR with files under META-INF/resources/frontend/: once-per-jar build-time WARN "Addon '' contains frontend sources under META-INF/resources/frontend/. This location is deprecated; migrate them to META-INF/frontend/ (bundle sources for @JsModule/@CssImport) or to META-INF/resources/ (runtime resources for @StyleSheet/@JavaScript)." Still works. Split:
    • bundle-source files (referenced by @JsModule/@CssImport) → move to META-INF/frontend/.
    • runtime files (referenced by @StyleSheet/@JavaScript) → move to META-INF/resources/ (drop the frontend/ segment, adjust annotation values accordingly).

References

#22888, #23326, #16780 and the v25 web-component-path / CSS-loading forum threads.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 27, 2026

Test Results

 1 395 files  + 1   1 395 suites  +1   1h 15m 33s ⏱️ +11s
10 081 tests +13  10 011 ✅ +13  70 💤 ±0  0 ❌ ±0 
10 556 runs  +13  10 477 ✅ +13  79 💤 ±0  0 ❌ ±0 

Results for commit 84ca840. ± Comparison against base commit f4c473a.

♻️ This comment has been updated with latest results.

Artur- added 4 commits May 1, 2026 06:55
AppShellRegistry.resolveStyleSheetHref expanded context://-prefixed
@Stylesheet values server-side using request.getContextPath() + "/",
producing absolute server paths like <link href="/foo/styles.css">
that get baked into index.html. This breaks behind reverse proxies
that don't preserve the servlet container's context path in the
public URL: the server emits /foo/styles.css but the browser fetches
it from the public host where /foo/ doesn't exist.

Use service.getContextRootRelativePath(request) instead — the same
servlet-relative path (./, ../, etc.) that the bootstrap callback
populates into CONTEXT_ROOT_URL for the UIDL path. The resulting
href is resolved by the browser against <base>, which Vaadin sets
from the actual request URL (honoring X-Forwarded-* headers).

This brings AppShell-level @Stylesheet resolution in line with the
component-level UIDL path, which already used the relative form via
the client-side URIResolver.

Test fixtures updated to reflect the new servlet-relative hrefs.
AppShellRegistryAuraAutoLoadTest had a Mockito mock that returned
null for getContextRootRelativePath; it now stubs "./".

Related to #24218.
@Stylesheet currently produces broken <link> elements when the Vaadin
servlet is mapped at a non-root path (vaadin.urlMapping=/ui/* etc.):
the browser resolves the bare relative href against <base> (which
points to the servlet mapping path), so a file at
META-INF/resources/styles.css is fetched as /ui/styles.css and 404s.
Users had to know to write @Stylesheet("context://styles.css")
explicitly to step out of the servlet path.

Frame the right behavior into the framework instead:

- New FrontendDependencyUrlResolver.resolveToContextRoot extracts the
  prefix-handling rules into one place: http(s)://, //, context://,
  base:// pass through unchanged; "/" is treated as an absolute server
  path; "./" leads strip to a context-root-relative value; everything
  else gets a context:// prefix prepended. Path traversals are
  rejected.
- UIInternals.addComponentDependencies normalizes @Stylesheet values
  through the resolver before storing them on the dependency list, so
  component-level @Stylesheet now renders correctly under any servlet
  mapping. The same normalization keys ActiveStyleSheetTracker so
  spelling variants of the same file (foo.css, ./foo.css,
  context://foo.css) deduplicate to a single <link>.
- AppShellRegistry.resolveStyleSheetHref delegates to the same
  resolver, replacing the inline rule set. The trailing
  BootstrapUriResolver call continues to expand context:// using the
  servlet-relative path produced by getContextRootRelativePath, so
  AppShell-level resolution stays consistent with the UIDL path.

Test fixtures updated for the canonical context://-prefixed URLs in
the dependency list. UidlWriterTest also registers inline test
resources at the leading-slash path (/inline.css) to match the
servlet container lookup that resolveResource produces for a
context:// value.

Fixes #22888.
Lets a @javascript annotation render as a <script type="module"> tag
instead of a classic <script>, so hand-authored or CDN-hosted ES
modules can be loaded at runtime through annotations without going
through Vite. For build-time bundled ES modules @jsmodule remains the
right tool.

The new Type enum has values SCRIPT (default, current behavior) and
MODULE. With type=MODULE:
- UIInternals.addExternalDependencies dispatches the value to
  Page.addJsModule(resolved) instead of Page.addJavaScript. The value
  is normalized via FrontendDependencyUrlResolver.resolveToContextRoot,
  so bare relatives become context://<value> and are served as
  static resources by the servlet container under any servlet mapping.
- FrontendClassVisitor.JSAnnotationVisitor reads the type enum via a
  new visitEnum override and skips MODULE-typed values from the
  bundle imports collection. The type attribute does not exist on
  @jsmodule, so visitEnum is a no-op for it.

Existing @javascript usages keep their behavior: bare relative values
default to type=SCRIPT and continue to bundle (legacy interpretation),
external URLs continue to render as runtime <script> tags.

LoadMode is ignored for type=MODULE because Page.addJsModule does not
take a LoadMode parameter; documented on @JavaScript.loadMode.
@Artur- Artur- force-pushed the feature/clarify-javascript-jsmodule-stylesheet branch from bb8ba7c to 365f4b2 Compare May 1, 2026 14:45
Establishes a clean four-quadrant model:
- @jsmodule / @CssImport: build-time bundle sources, fed into Vite. App
  source: src/main/frontend/. Addon source: META-INF/frontend/ (with
  META-INF/resources/frontend/ kept for compatibility but deprecated).
- @Stylesheet / @javascript: runtime <link>/<script> elements served by
  the servlet container from META-INF/resources/. Relative URLs always
  resolve against the context root, regardless of servlet mapping.

Concrete changes:

- New FrontendDependencyUrlResolver with resolveToContextRoot and
  isRuntimeDependencyUrl. Single source of truth for the prefix table:
  http(s)://, //, context://, base://, /abs are runtime; bare relatives
  and ./foo are normalized to context://foo.
- AppShellRegistry.resolveStyleSheetHref delegates to the resolver.
- UIInternals.addComponentDependencies eagerly resolves @Stylesheet
  values via the same resolver, so component-level @Stylesheet now
  bypasses the Vaadin servlet mapping the same way AppShell-level
  @Stylesheet already did. Fixes the issue where
  @Stylesheet("foo.css") returned 404 when the Vaadin servlet was
  mapped at a non-root path.
- @javascript values with a runtime prefix (context://, base://, /abs,
  http(s)://, //) are now served as runtime <script> elements rather
  than bundled. Bare-relative @javascript values still bundle for
  backwards compatibility but emit a one-time consolidated build-time
  warning per (class, value).
- @jsmodule with a runtime URL is similarly deprecated. The scanner
  filters such values out of the bundle imports and tracks them on
  ClassInfo.deprecatedRuntimeModules. FrontendDependencies emits a
  consolidated build-time warning recommending migration to
  @javascript(value=..., type=Type.MODULE) for runtime
  <script type="module"> loading. The runtime path in
  UIInternals.addExternalDependencies continues to load external
  @jsmodule URLs as runtime modules so existing apps keep working;
  the previous double-load (bundle + runtime) is gone.
- TaskCopyFrontendFiles emits a once-per-jar/dir warning when an addon
  uses META-INF/resources/frontend/, recommending the split layout.
- Javadoc rewrites for all four annotations describe the bundle-source
  vs runtime distinction, source locations, and the URL prefix table.
- Page.addStyleSheet/addJavaScript Javadoc cross-references the
  annotation prefix tables.

References #22888, #23326, #16780 and
the v25 web-component-path / CSS-loading forum threads.
@Artur- Artur- force-pushed the feature/clarify-javascript-jsmodule-stylesheet branch from 365f4b2 to 84ca840 Compare May 2, 2026 05:31
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented May 2, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant