Skip to content

feat: serve @JavaScript with a runtime prefix as a runtime <script> - #24653

Merged
mcollovati merged 32 commits into
mainfrom
javascript-is-runtime
Oct 22, 2026
Merged

mcollovati merged 32 commits into
mainfrom
javascript-is-runtime

Conversation

@Artur-

@Artur- Artur- commented Jun 16, 2026 •

Copy link
Copy Markdown
Member

@javascript values with a runtime prefix (context://, base://, /abs, http(s)://, //) are now served as runtime <script> elements rather than bundled, matching how the corresponding @Stylesheet values are resolved.

  • New FrontendDependencyUrlResolver.isRuntimeDependencyUrl recognises the runtime prefix table (http(s)://, //, context://, base://, /abs) and is the single source of truth for distinguishing runtime URLs from bundler import specifiers.
  • UIInternals treats such @javascript values as runtime dependencies via isRuntimeJavaScript, alongside type=MODULE values.
  • Both dependency scanners (FrontendClassVisitor for production and FullDependenciesScanner for dev mode) filter runtime-prefixed @JavaScript values out of the bundle imports.
  • Bare-relative @JavaScript values are still bundled for backwards compatibility, but FrontendDependencies logs a single build-time warning that lists every such usage. The warning recommends @JsModule for bundling, or moving the file to a static location and using a runtime prefix. The @JavaScript javadoc documents this interpretation as deprecated.

Behavior change: before this change, @JavaScript("/foo.js") pointing at frontend/foo.js was bundled, because the bundler resolves a leading slash against the frontend folder. It now renders <script src="/foo.js"> resolved against the context root, which returns 404 unless the file is also served as a static resource. No build-time warning covers this form, since a leading slash is now a valid runtime URL and the scanner cannot tell the two uses apart. To keep bundling such a file, switch to @JsModule("./foo.js"). This needs a release note.

Production-mode fix: developmentOnly = true is now honoured for dependencies that are loaded at runtime instead of bundled. Before, a runtime @JavaScript value (runtime prefix or Type.MODULE) or an external @JsModule URL such as @JsModule(value = "https://…/dev-helper.js", developmentOnly = true) was also loaded in production. The @JsModule case and the prefixed @JavaScript cases other than a leading / were already affected before this PR.

Hotswap: a runtime @JavaScript value is added as a <script> element, which cannot be removed from the page. When such a value is changed or removed on a component in use, the new JavaScriptHotswapper reloads the affected UIs instead of refreshing them, so the old script is not left running next to the new one. Adding a value still only refreshes. The hotswapper also handles a change on a superclass or a @Uses target of a component in use: it reads the previous values from the cached metadata of the component in use and clears that cache, so new pages get the new value too. This only applies to components shown in an open UI during the hotswap; others keep their cached metadata until a restart, as before. A runtime @JavaScript change on the app shell reloads all UIs, since those scripts are rendered into the initial page. A change to a bundled @JavaScript value, or a value moved between bundled and runtime, cannot be applied without a restart (the bundle is built at startup), so the hotswapper logs a warning saying so, for a changed class whether or not it is in use. To share the runtime check, FrontendDependencyUrlResolver.isRuntimeJavaScript(JavaScript) is added and ComponentMetaData.DependencyInfo.getJavaScripts() is made public (both internal-use classes).

App shell: runtime @JavaScript values on the AppShellConfigurator were kept out of the bundle by the scanners but never added to the page, since the app shell is not a component (for context:// and http(s):// values this was already the case before this PR). AppShellRegistry now adds them to the index page as <script> elements (type="module" for Type.MODULE, defer for LoadMode.LAZY), resolved the same way as on components, and skips developmentOnly values in production. LoadMode.INLINE is rejected on the app shell with a hint to use @Inline.

API Changes

// com.vaadin.flow.server.FrontendDependencyUrlResolver (internal use)
// Added
public static boolean isRuntimeDependencyUrl(String value)
public static boolean isRuntimeJavaScript(JavaScript javaScript)
public static String resolveRuntimeJavaScript(String value) // shared by UIInternals and AppShellRegistry

// com.vaadin.flow.component.internal.ComponentMetaData.DependencyInfo
// Changed
- List<JavaScript> getJavaScripts()
+ public List<JavaScript> getJavaScripts() // visibility raised from package-private

// com.vaadin.base.devserver.hotswap.impl.JavaScriptHotswapper (internal use)
// Added
public class JavaScriptHotswapper implements VaadinHotswapper
public JavaScriptHotswapper()
public void onClassesChange(HotswapClassEvent event)
public void onClassesChange(HotswapClassSessionEvent event)
public void onHotswapComplete(HotswapCompleteEvent event)

Artur- added 2 commits June 16, 2026 13:53
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 @JavaScript.Type enum has values SCRIPT (default, current
behavior) and MODULE. The annotation gains a type() attribute that
selects between them.

To make @javascript the unified entry point on the programmatic side
as well, this commit also:
- adds a new Page.addJavaScript(String url, LoadMode loadMode,
  JavaScript.Type type) overload that handles both classic <script>
  and <script type="module"> tags, with full LoadMode support for
  both;
- delegates the existing addJavaScript(String, LoadMode) and
  addJavaScript(String) overloads to the new method with
  Type.SCRIPT;
- deprecates Page.addJsModule(String) — recommend
  addJavaScript(url, loadMode, Type.MODULE) instead. The deprecated
  method keeps working for backwards compatibility.

UIInternals.addExternalDependencies routes both @javascript runtime
values and external @jsmodule values through the new addJavaScript
overload. @javascript values pass js.loadMode() and js.type()
straight through, so type=MODULE supports LAZY and INLINE load modes
just like type=SCRIPT.

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.
@javascript values with a runtime prefix (context://, base://, /abs,
http(s)://, //) are now served as runtime <script> elements rather than
bundled, matching how the corresponding @Stylesheet values are resolved.

- New FrontendDependencyUrlResolver.isRuntimeDependencyUrl recognises the
  runtime prefix table (http(s)://, //, context://, base://, /abs) and is
  the single source of truth for distinguishing runtime URLs from bundler
  import specifiers.
- UIInternals treats such @javascript values as runtime dependencies via
  isRuntimeJavaScript, alongside type=MODULE values.
- The scanner (FrontendClassVisitor) filters runtime-prefixed @javascript
  values out of the bundle imports.
- Bare-relative @javascript values still bundle for backwards
  compatibility but FrontendDependencies emits a one-time consolidated
  build-time warning per (class, value) recommending migration.
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
0.0% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@github-actions

github-actions Bot commented Jun 16, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

 1 668 files  + 1   1 754 suites  +1   2h 2m 46s ⏱️ - 5m 29s
12 979 tests +39  12 910 ✅ +39  69 💤 ±0  0 ❌ ±0 
13 613 runs  +39  13 544 ✅ +39  69 💤 ±0  0 ❌ ±0 

Results for commit 7e4f494. ± Comparison against base commit a59da97.

♻️ This comment has been updated with latest results.

@Artur-

Artur- commented Oct 15, 2026

Copy link
Copy Markdown
Member Author

Merge main.

totally-not-ai Bot and others added 2 commits October 15, 2026 06:32
Resolve conflicts with the squash-merged #24239 (type attribute for
@javascript) by taking main's version and re-applying this branch's
runtime-prefix change on top.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The merge commit picked up copyright header rewrites from the
formatter hook in files unrelated to this change. Restore them to
main's version and update the resolveRuntimeJavaScript Javadoc to
match the values that now reach it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Memory footprint

No change compared with the base commit a59da97.

@github-actions github-actions Bot added +0.0.1 and removed +0.1.0 labels Oct 15, 2026
totally-not-ai Bot and others added 2 commits October 15, 2026 06:56
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Removes the duplicated prefix check, which also lower-cased without
Locale.ROOT.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@totally-not-ai

Copy link
Copy Markdown
Contributor

Fixed the java:S1449 issue: URL prefix checks now lower-case with Locale.ROOT, so they don't depend on the default locale (e.g. the Turkish dotless i). resolveToContextRoot now reuses isRuntimeDependencyUrl instead of its own copy of the same prefix check, which had the same locale problem.

@Artur-

Artur- commented Oct 15, 2026

Copy link
Copy Markdown
Member Author

merge main

@totally-not-ai

Copy link
Copy Markdown
Contributor

@Artur- Merged latest main into the branch (no conflicts); flow-server compiles and the resolver tests pass.

@Artur-
Artur- marked this pull request as ready for review October 16, 2026 08:14
@github-actions

Copy link
Copy Markdown
Contributor

Runtime-prefixed @JavaScript values now take the same path out of the bundle as @StyleSheet values

flowchart LR
    subgraph Before
        direction TB
        B1["JavaScript annotation ((foo.js/redacted) -->|"build-time scan"| B2["FrontendClassVisitor.JSAnnotationVisitor: not type=MODULE, so bundle it"]
        B1 -->|"render time"| B3["UIInternals.isRuntimeJavaScript(): not MODULE, not UrlUtil.isExternal() - not runtime"]
        B2 --> B4["classInfo.scripts (bundle import)"]
        B3 --> B5["no runtime script tag emitted"]
    end
    subgraph After
        direction TB
        A1["JavaScript annotation ((foo.js/redacted) -->|"build-time scan"| A2["FrontendDependencyUrlResolver.isRuntimeDependencyUrl() true - skip bundle"]:::changed
        A1 -->|"render time"| A3["FrontendDependencyUrlResolver.isRuntimeDependencyUrl() true - is runtime"]:::changed
        A2 --> A4["excluded from classInfo.scripts"]
        A3 --> A5["UIInternals.addRuntimeDependencies() emits script tag"]:::changed
    end
    Before ~~~ After
    classDef changed stroke:#c9a227,stroke-width:3px
Loading

Before this PR, the build-time scanner (FrontendClassVisitor.JSAnnotationVisitor) only excluded type=MODULE values from the bundle, so a runtime-prefixed value like (foo.js/redacted) still went into classInfo.scripts, while UIInternals.isRuntimeJavaScript()only recognizedtype=MODULEorUrlUtil.isExternal()(plainhttp(s)://///) as runtime, so no (script)tag was ever emitted for it. The newFrontendDependencyUrlResolver.isRuntimeDependencyUrl()` predicate is now the single check both sides use, so the same value is consistently excluded from the bundle and served as a runtime script.

Diagram Bot draws the mechanism this pull request touches; it does not review the change. Verify it against the diff.

Generated by Diagram Bot for #24653 · agent · 82.8 AIC · ⊞ 9.2K · ◷

@vaadin-review-bot vaadin-review-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the changes — left 1 comment and collected 2 findings below that could not be attached to the diff.

Finding
⚠️ Only the bytecode scanner excludes runtime-prefixed @javascript values; the full scanner, which is the default in dev mode, still bundles them
📝 The @javascript javadoc still documents bundled bare-relative values as normal behavior while the build now calls that usage deprecated
📝 The deprecation warning's first two suggestions do not work for files in the frontend directory

⚠️ Only the bytecode scanner excludes runtime-prefixed @javascript values; the full scanner, which is the default in dev mode, still bundles them
FullDependenciesScanner.collectScripts got the Type.MODULE exclusion but not the new runtime-prefix exclusion. It is the scanner selected whenever optimizeBundle is false, which is the default for dev mode (DevModeInitializer) and for BuildFrontendUtil's dev-bundle path.

context:// and base:// survive this because AbstractUpdateImports.resolveModules filters those prefixes later, but a leading-slash value does not: @JavaScript("/absolute.js") still ends up as import '/absolute.js'; in the generated imports while also being added as a runtime <script>. So the feature behaves one way in a production build and another way in dev.

The module-exclusion equivalent has a test in both scanners (FullDependenciesScannerTest.getScripts_javaScriptWithTypeModule_excludedFromBundleImports); the runtime-prefix exclusion only has one in FrontendDependenciesTest.
FullDependenciesScanner.java:395 · correctness

📝 The @javascript javadoc still documents bundled bare-relative values as normal behavior while the build now calls that usage deprecated
Two mismatches introduced by this change:

  • The class javadoc says "Other paths used in the value() method are considered as relative to frontend directory and they are bundled". Leading-slash and base:// values are no longer bundled.
  • Nothing in the annotation says the bundled interpretation is deprecated, and neither @JavaScript nor value() carries @Deprecated, yet every build now prints "uses the deprecated bundled interpretation" for the usage the javadoc presents as the normal one.
    JavaScript.java:55 · documentation

}
for (String value : classInfo.scripts) {
if (warnedJavaScriptKeys.add(className + ':' + value)) {
log().warn(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 The deprecation warning's first two suggestions do not work for files in the frontend directory

Bare-relative @JavaScript values are documented as relative to the frontend directory. Both context:// and Type.MODULE resolve against the servlet context root, where such a file is not served, so following either suggestion gives a 404 unless the file is also moved out of frontend/. Only the @JsModule suggestion works as written.

The warning also fires for classes from third-party add-ons, which the application developer cannot change and cannot silence.

FrontendDependencies.java:247 · documentation

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vaadin-review-bot Good catch. I reworded the warning. Its first suggestion now says to replace @JavaScript with @JsModule using the same value, which works as-is for files in frontend/. For loading at runtime, it now says the file must first be moved out of frontend/ to a statically served location (e.g. src/main/resources/META-INF/resources), and only then be referenced with context:// or type = Type.MODULE. Since add-on annotations can't be changed by the app developer, the warning is now logged once per scan with all usages listed, instead of once per annotation. It also notes that usages in add-ons need to be updated by the add-on maintainer.

totally-not-ai Bot and others added 2 commits October 16, 2026 08:34
The full dependencies scanner, used in dev mode, still bundled
@javascript values with a leading slash. Exclude runtime-prefixed
values there as well so dev and production builds agree.

Log the deprecation warning once per scan listing all usages, with
suggestions that work for files located in the frontend directory,
and document the deprecated bundled interpretation in the javadoc.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Verify that all bundled @javascript usages end up in a single warning,
that no warning is logged when only runtime values are used, and that
@jsmodule values with a runtime prefix are still bundled by the full
scanner. Make the warning wording apply to npm package values too and
stop promising a build warning in the javadoc, as dev mode does not
emit it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@totally-not-ai

Copy link
Copy Markdown
Contributor

@vaadin-review-bot Addressed the two findings that weren't attached to the diff:

  • Full scanner: FullDependenciesScanner.collectScripts now skips runtime-prefixed @JavaScript values in the same way as the bytecode scanner. This means @JavaScript("/absolute.js") is no longer emitted as an import in dev mode. The FullDependenciesScannerTest sample component now includes context://, base:// and leading-slash values, and the test checks that only the bare relative value is bundled.
  • Javadoc: the class and value() javadoc now list the runtime prefixes, including leading / and base://, and state that these are never bundled. They also say that the bundled interpretation of bare relative values is deprecated and point to @JsModule instead. I didn't add @Deprecated to value() because only one interpretation of its values is deprecated, not the attribute itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@totally-not-ai

Copy link
Copy Markdown
Contributor

Replaced the lambda with JavaScript::value in UIInternals (S1612). I left the S1066 finding at UIInternals.java:2231 (setChildModeless) as it is: that nested if is existing code this PR doesn't touch, so changing it here would add unrelated changes to the diff.

Artur- and others added 3 commits October 19, 2026 09:46
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
totally-not-ai Bot and others added 3 commits October 20, 2026 15:37
The build drops every value with a scheme (UrlUtil.isExternal) from the
bundle, so a @javascript value such as ftp://... that has none of the
runtime prefixes was neither bundled nor loaded. Treat such values as
runtime values again, as before, both in the scanners and in UIInternals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Also check that such values are not reported as missing from the
production bundle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fixtures used a frontend:// pseudo-scheme, which the bundler drops
like any other URL with a scheme and which is now treated as a runtime
value by the scanner as well. Use plain relative values so the tests
keep exercising bundled script collection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mcollovati

Copy link
Copy Markdown
Collaborator

It looks like there's an issue with hotswap: if I change the value for a @Javascript annotation that now is served as <script>, the browser will load the new JavaScript and also keep the old one.
IIRC, a loaded JavaScript cannot be removed from the page, but I wonder if the hotswapper should reload the page instead of doing a refresh in this case.

totally-not-ai Bot and others added 2 commits October 20, 2026 17:08
A @javascript value that is loaded at runtime is added as a script
element, which cannot be removed from the page again. Refreshing the UI
after the value was changed or removed kept the old script and added the
new one next to it. Reload the UIs using the component instead.

The check for runtime @javascript values moves to
FrontendDependencyUrlResolver so that UIInternals and the hotswapper
share it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Read the scripts after the redefinition through the component metadata
as well, so scripts of @uses targets do not look removed and trigger a
reload on every redefinition. Add tests for closing UIs, Type.MODULE
values, clearing the recorded state and isRuntimeJavaScript.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@totally-not-ai

Copy link
Copy Markdown
Contributor

@mcollovati Good catch. A script that has run cannot be removed from the page, so a refresh can only add the new <script> next to the old one. I added a JavaScriptHotswapper. Before the metadata cache is cleared, it records the runtime @JavaScript values (value and type) of each redefined component. If any of them was removed or changed, it requests UIUpdateStrategy.RELOAD for every UI where the component is in use. Only adding a value keeps the refresh, because the new script is simply added. Bundled @JavaScript values are not affected. To share the runtime check with UIInternals, it moved to FrontendDependencyUrlResolver.isRuntimeJavaScript. JavaScriptHotswapperTest covers changing the value, changing the type, adding a value, a bundled value, and a component that is not in use.

}
// Logged once per scan rather than per usage, as annotations coming
// from add-ons cannot be changed by the application developer
log().warn("Found @JavaScript values that are bundled. "

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JavaScript on the AppShellConfigurator is only seen by the scanners. AppShellRegistry reads @StyleSheet, @Meta, @Inline and others from the app shell, but not @JavaScript, and UIInternals.addRuntimeDependencies only runs for components. So once the scanners drop a runtime value from the bundle, nothing adds it at runtime.

For http:// and context:// values on the app shell this was already the case before this PR. With this PR, two things make it worse:

  • @JavaScript("/x.js") on the app shell joins that group, so it is no longer loaded at all.
  • This warning lists app-shell usages too (- org.example.AppShell: "./x.js") and tells the developer to move the file to a static location and use context://. On the app shell that leaves the script never loaded, with no error.

I checked this with an app at 894880dc5c and on 25.3.4, in development mode, production mode and hotdeploy. The app shell's "./x.js" and @JsModule values load on both versions. Its context:// and http:// values never load on either version. With this PR, its / and Type.MODULE values never load either. Nothing is logged in any of these cases.

Possible fixes:

  • Make AppShellRegistry add runtime @JavaScript values to the page, the same way it handles @StyleSheet (resolved through FrontendDependencyUrlResolver, and skipping developmentOnly in production).
  • Or, as a minimum, give app-shell usages a different hint (@JsModule, or a component), and warn at build time when a runtime value is found on the app shell.

@JavaScript placed directly on a WebComponentExporter probably has the same gap, but I have not checked that.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mcollovati Thanks, fixed in 25842f5 with the first option. AppShellRegistry now adds the app shell's runtime @JavaScript values (runtime prefix, other schemes, Type.MODULE) to the index page as <script> elements. They are resolved through FrontendDependencyUrlResolver the same way as on components; UIInternals and the registry now share the new resolveRuntimeJavaScript. Type.MODULE gives type="module", LoadMode.LAZY gives defer, and developmentOnly values are skipped in production. LoadMode.INLINE throws on the app shell and points to @Inline. With this in place, the warning's context:// advice also works for app-shell usages, so I left its wording as is. Tests in VaadinAppShellInitializerTest cover the order, attributes, de-duplication, production filtering and the INLINE error. A follow-up commit, 208cfdb, also makes the hotswapper reload all UIs when a runtime @JavaScript on the app shell changes, because those scripts are rendered into the initial page. I haven't looked at @JavaScript on a WebComponentExporter yet.

// hotswappers have run, so it still holds the values from before the
// redefinition
for (Class<? extends Component> type : componentClasses(event)) {
previousScripts.put(type.getName(),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "before" value is read through ComponentUtil.getDependencies(service, type). That only gives the old values when the changed class itself has a cached ComponentMetaData. Two common cases don't have one:

  • A parent class. abstract class Base has @JavaScript("context://a.js"), and the view is View extends Base. Change or remove the annotation on Base and hotswap. Only Base is in the changed classes. It was never instantiated, so it has no cache entry, and getDependencies computes "before" from the class that is already redefined. Before and after are then equal, so there is no reload.
  • A @Uses target. View has @Uses(Helper.class), and Helper has a runtime @JavaScript. Change it on Helper. Helper usually has no cache entry either, and isComponentInUse(ui, Helper.class) is false when no Helper instance is in the tree. So there is no reload.

I checked both cases with the dev loop (apply --no-restart), on 25.3.4 and with this PR. In both, the new value never loads until a restart, not even in a newly loaded page, because the view's cached metadata still holds the old value. That stale cache is older than this PR. What this PR adds is a hotswapper that handles the view's own class (I checked it: the page is reloaded and only the new script runs), but not a parent class or a @Uses target, where it sees no change at all.

The tests load a new copy of the class under the same name rather than redefining it in place, so they don't run into this.

A possible fix: on a change, also clear the cached metadata of the classes that extend or @Uses a changed class, and compare their before and after values. That also fixes the stale script in a new page.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mcollovati Fixed in 5d4a063 and 208cfdb. The session handler now walks the components in use in each UI. For each one whose class, a superclass, or a (transitive) @Uses target is a changed class, it compares before and after. For a component whose own class didn't change, "before" comes from its cached metadata, which still holds the old values. That cache entry is then cleared, so a newly loaded page gets the new value too. Every component class in use is checked, not just up to the first one that needs a reload. The new tests redefine the class in place through the ByteBuddy agent, not as a copy under the same name, so they cover the parent-class and @Uses-target cases you describe. They also check that the component's dependencies return the new value afterwards. One case is still open: if no UI has the dependent component open during the hotswap, its stale cache entry stays until restart. That's the older cache behaviour, and there is no API to find the dependent classes in the cache without a UI.

Set<RuntimeScript> previous = previousScripts.get(type.getName());
// Read the same way as the previous values, so that @Uses
// dependencies are on both sides
if (previous == null || runtimeScripts(new ComponentMetaData(type)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only runtime values are compared here, so a hotswap that moves a value between bundled and runtime is not handled. The bundle (or the generated imports, with hotdeploy) only changes at startup:

  • @JavaScript("./x.js") → @JavaScript("context://x.js"): the old set is empty, so containsAll is true and there is no reload. The route refresh adds <script src="context://x.js">, and the bundle still runs x.js, so the file runs twice.
  • @JavaScript("context://x.js") → @JavaScript("./x.js"): this triggers a reload, but x.js is not in the bundle that was built at startup, so the file is not loaded at all.

I checked both cases with the dev loop (apply --no-restart), on 25.3.4 and with this PR. The first case runs the file twice, both on the page that stays open and on a freshly loaded page. The second case loads nothing. Neither is new: 25.3.4 behaves the same for context:// and http:// values. But with this PR more values are loaded at runtime, and the new JavaScriptHotswapper is the natural place to catch both cases. In the first case it does not reload the page today, so nothing tells the developer that a restart is needed.

Since this cannot work without a restart, could the hotswapper detect a value that moved between bundled and runtime (or any change to a bundled @JavaScript), and log that a restart is needed? JsDefinitionHotswapper.warnAboutMissingDefinitions does the same for JavaScript definitions. Then the developer at least knows why the script runs twice or not at all.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mcollovati Done in 5d4a063 and 208cfdb. The hotswapper now records all @JavaScript values, not only runtime ones. When the bundled set of a changed class (whether or not it is in use) or of a component in use changes, it logs a warning that a restart is needed (once per class per hotswap). That covers a changed bundled value and a value moved between bundled and runtime in either direction. In the ./x.js → context://x.js case nothing is reloaded, but the warning explains why the script runs twice. The reverse case still reloads for the removed runtime value and also logs the warning. Tests cover a changed bundled value and both directions of the move.

@mcollovati

Copy link
Copy Markdown
Collaborator

Dev loop restarts the app for runtime @JavaScript changes (follow-up suggestion)

With this PR, a runtime @JavaScript value (a runtime prefix, another scheme, or Type.MODULE) does not need a restart after a hotswap. UIInternals reads the annotation again when the view is attached. For the view's own class, the new JavaScriptHotswapper also reloads the page when a value is removed.

The dev loop still restarts the app for every @JavaScript change. DevLoopRedefiner.frontendDependencies (vaadin-dev-server, line 1440) records every value as script:<value>, without checking whether it is bundled. When it changes, TransactionEngine.blockedReason (flow-devloop-daemon, line 1666) escalates to a restart: frontend imports changed (…): @JsModule and friends are read at startup (dev bundle rebuild).

I checked this with an app and the dev loop:

  • A plain apply restarts the app for a change from context://js/a.js to http://cdn/js/b.js.
  • apply --no-restart applies the same change as a real hotswap. With this PR the open page is reloaded and only the new script runs; nothing else is needed.

So for runtime values the restart only costs time. I suggest a follow-up issue after this PR is merged, so that it can use FrontendDependencyUrlResolver.isRuntimeJavaScript from here:

  • In frontendDependencies, leave out the @JavaScript values that isRuntimeJavaScript accepts, the same way @StyleSheet is already left out. Nothing on the daemon side has to change.
  • Keep the restart for bundled values. A value that moves between bundled and runtime (for example ./x.js → context://x.js) still causes a restart, because the set of bundled values changes. That is needed, because the bundle only changes at startup: without a restart, such a file runs twice or not at all.
  • External @JsModule values are loaded at runtime too (UrlUtil.isExternal), so they can be handled the same way.

totally-not-ai Bot and others added 3 commits October 22, 2026 06:58
The scanner keeps runtime @javascript values out of the bundle, but
the app shell is not a component, so UIInternals never adds them to
the page and they were not loaded at all. AppShellRegistry now adds
them to the index page as script elements, skipping developmentOnly
values in production. LoadMode.INLINE is rejected there, as @inline
covers that case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pper

A change to a runtime @javascript on a superclass or a @uses target of
a component in use was not noticed: the changed class usually has no
cached metadata, so the previous values could not be read, while the
component in use kept its cached metadata with the old values, also
for new pages. The hotswapper now walks the components in use, reads
the previous values of those that depend on a changed class from their
cached metadata, and clears that cache.

Bundled values only change at startup, so a change to a bundled value,
or a value moved between bundled and runtime, now logs that a restart
is needed instead of silently running the script twice or not at all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Check every component class in use instead of stopping at the first
one that needs a reload, so the stale metadata of each is cleared.
Warn about changed bundled values of a changed class also when it is
not in use, and reload all UIs when a runtime @javascript value on the
app shell changes, as those are rendered into the initial page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@totally-not-ai

Copy link
Copy Markdown
Contributor

@mcollovati Agreed on the dev loop suggestion. Leaving out the values that FrontendDependencyUrlResolver.isRuntimeJavaScript accepts (and external @JsModule URLs) in DevLoopRedefiner.frontendDependencies would avoid the restart, and changes to bundled values would still restart. I've kept it out of this PR to keep the scope down. It fits a follow-up once this is merged and isRuntimeJavaScript is on main.

totally-not-ai Bot and others added 2 commits October 22, 2026 07:52
With the coverage profile, JaCoCo adds a method to the test classes
when they are loaded, so redefining them from their class files fails
with "attempted to delete a method". Redefine copies defined from the
class files instead; they have no code source, so JaCoCo leaves them
alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mcollovati
mcollovati previously approved these changes Oct 22, 2026
Move adding a single app shell script into its own method, make the
operator precedence explicit, and tidy two tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@totally-not-ai

Copy link
Copy Markdown
Contributor

Fixed all four Sonar findings in the latest commit. AppShellRegistry.addJavaScripts now passes each runtime value to a new addJavaScript method, so the loop has no break or continue, and the developmentOnly condition now has explicit parentheses. In the tests, JavaScriptHotswapperTest.tearDown no longer declares throws Exception, and the INLINE test gets the registry before the assertThrows lambda. Behaviour is unchanged, and the affected tests pass.

@sonarqubecloud

Copy link
Copy Markdown

@mcollovati
mcollovati added this pull request to the merge queue Oct 22, 2026
Merged via the queue into main with commit b0935b8 Oct 22, 2026
53 checks passed
@mcollovati
mcollovati deleted the javascript-is-runtime branch October 22, 2026 10:01
vaadin-bot added a commit to vaadin/docs that referenced this pull request Oct 22, 2026
Describe that a @javascript value with a runtime prefix (context://,
base://, a leading /, http(s)://, //, or another scheme) is loaded as
a runtime script instead of being bundled, and that bundling a bare
relative @javascript value is deprecated in favor of @jsmodule. Updates
articles/flow/advanced/loading-resources.adoc.

Documents vaadin/flow#24653 (`7e4f4942cd59453d143191947639dc56a7d936f0`).
@github-actions

Copy link
Copy Markdown
Contributor

Pull request created: #6463

Generated by Documentation Bot · agent · 117.8 AIC · ⊞ 11.7K

@github-actions

Copy link
Copy Markdown
Contributor

Documentation Bot: Draft documentation pull request for this change: vaadin/docs#6463

Files updated:

  • articles/flow/advanced/loading-resources.adoc

It was written from the state of this pull request as you see it now. Please review it and mark it ready for review.

Generated by Documentation Bot for #24653 · agent · 117.8 AIC · ⊞ 11.7K · ◷

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.

3 participants