Skip to content

refactor: send hotswap UI refresh through a JS definition - #26979

Open
mcollovati wants to merge 1 commit into
mainfrom
refactor/hotswap-refresh-js-definition
Open

mcollovati wants to merge 1 commit into
mainfrom
refactor/hotswap-refresh-js-definition

Conversation

@mcollovati

Copy link
Copy Markdown
Collaborator

refactor · vaadin-dev-server · dev mode apps with push that hotswap a class

Background — JS definitions. An interface annotated with @JsDefinition declares JavaScript that the build puts into the frontend bundle, and executeJs(Class) calls it by a hash instead of sending JavaScript text. The client then never has to build a function from a string, so the call works under a content security policy without unsafe-eval.

After a hotswap, the request that tells the browser to refresh a UI with push was the last JavaScript string sent with executeJs in Flow. It now uses a JS definition, like every other call, so it no longer needs eval in the browser.

Risks:

  • ⚠️ Public API: adds the nested interface Hotswapper.RefreshUiJs, public because every JS definition is, and documented as internal.
  • ✅ No breaking change, no behaviour change, no security, memory, serialization, threading, performance or migration impact.

Context. The definition lives in vaadin-dev-server, not flow-server, so production bundles do not carry it. The prebuilt dev bundle in the platform repo depends on vaadin-dev-server, so it includes the definition and dev mode apps do not rebundle.

  • Added Hotswapper.RefreshUiJs, which fires the vaadin-refresh-ui window event with fullRefresh as a parameter
    • The dev tools already listen for that event and refresh the UI while keeping scroll positions, so no frontend code changed
  • Replaced the String.format + executeJs(String) call in Hotswapper with executeJs(RefreshUiJs.class).refresh(fullRefresh)
  • Changed HotswapperTest.RefreshTestingUI to record the flag from RefreshUiJs instead of matching the JavaScript text

@mcollovati

Copy link
Copy Markdown
Collaborator Author

Type of change

  • Refactor

How to test

No integration test view covers the push hotswap path. In a dev mode app with @Push and a hotswap agent (JBR + HotswapAgent, or the dev loop):

  1. Open a view and scroll the page down.
  2. Change a component class used by the view and hotswap it.
  3. The view refreshes without a full page reload, the scroll position is kept, and the browser console shows no error.
Test coverage
  • HotswapperTest (65 tests) passes; the refresh cases assert the route-only and full-chain flag through RefreshUiJs.
  • The test stubs the definition, so JsDefinitionProxy.checkDefinition and TaskGenerateJsDefinitions were run by hand on the interface: it is accepted, and the generated bundle entry is window.Vaadin.Flow.jsDefinitions["3e8b0e97…"] = async function ($0) { window.dispatchEvent(new CustomEvent('vaadin-refresh-ui', { detail: { fullRefresh: $0 } })) }.
  • Not tested in a running app.

@mcollovati
mcollovati marked this pull request as ready for review November 11, 2026 16:03
@mcollovati mcollovati added the csp label Nov 11, 2026

@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 2 comments.

Finding
⚠️ Spring apps that set vaadin.allowed-packages never scan RefreshUiJs, so the hotswap refresh breaks for them.
🧪 No test checks the event name or payload sent to the browser anymore.

* For internal use only. May be renamed or removed in a future release.
*/
@JsDefinition
public interface RefreshUiJs extends Serializable {

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.

⚠️ Spring apps that set vaadin.allowed-packages never scan RefreshUiJs, so the hotswap refresh breaks for them.

When allowed packages are set, Spring dev mode scans only the app packages, the custom list and DEFAULT_SCAN_ONLY in VaadinServletContextInitializer. That list was widened on purpose to cover the framework's JS definitions (see its comment), but it has no entry for com.vaadin.base.devserver. For these apps:

  • The generated definitions file leaves out RefreshUiJs, so the push refresh after a hotswap has no function to run in the browser. Before this PR it worked, because it was sent as a string.
  • The generated content no longer matches the hash of the prebuilt dev bundle, which includes the definition. That forces a rebundle, which goes against the PR's claim that dev mode apps do not rebundle.

To fix it, add the package (or Hotswapper's package) to DEFAULT_SCAN_ONLY. The guard test onStartup_allowedPackagesSet_devModeScansEveryFrameworkJsDefinition only looks for definitions under com.vaadin.flow, which is why it misses this one. Widen it as well.

Hotswapper.java:824 · correctness

}).when(pageSpy).executeJs(anyString(), any(Object[].class));
// Intercept the request to the browser to check whether a UI
// refresh has been asked for, and for which part of the chain.
Hotswapper.RefreshUiJs refreshUiJs = fullRefresh -> refreshRouteChainRequested = fullRefresh;

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.

🧪 No test checks the event name or payload sent to the browser anymore.

The old test matched the JavaScript text against vaadin-refresh-ui and the detail.fullRefresh shape. The new stub replaces the whole definition with a lambda. A typo in the @JsExpression event name or detail key would now pass every test, and the dev tools would just stop refreshing. A small check on the expression, or a test against the generated snippet, would bring that coverage back.

HotswapperTest.java:1680 · tests

@vaadin-bot

Copy link
Copy Markdown
Collaborator

@mcollovati this "chore:"/"test:"/"refactor:" PR has been approved automatically, but auto merge has not been enabled for it. You can enable the auto merge by yourself, or ask another member for another review. [Message is sent from bot]

@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

 1 691 files  ±0   1 781 suites  ±0   2h 3m 23s ⏱️ - 15m 42s
13 479 tests ±0  13 407 ✅ ±0  72 💤 ±0  0 ❌ ±0 
14 113 runs  ±0  14 041 ✅ ±0  72 💤 ±0  0 ❌ ±0 

Results for commit 2b2f51a. ± Comparison against base commit 468d0aa.

This branch has not been deployed

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants