Skip to content

docs: add a hotswap guidelines chapter on regenerating frontend files - #25806

Open
totally-not-ai[bot] wants to merge 6 commits into
mainfrom
docs/hotswap-generated-frontend-files
Open

totally-not-ai[bot] wants to merge 6 commits into
mainfrom
docs/hotswap-generated-frontend-files

Conversation

@totally-not-ai

@totally-not-ai totally-not-ai Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a guidelines chapter that explains how development-mode hotswap keeps generated frontend files (like layouts.json and the Hilla endpoint clients) in sync with the Java classes they come from. It writes down the rules we already follow in the code, so the next hotswapper is written the same way.

What changed

Documentation only — no code, no behavior change.

  • New guidelines/frontend-hotswap.md, listed in guidelines/overview.md.
  • States the scope up front: only the frontend side. The server-side half of hotswap (route registries, stylesheet links, reflection caches, translations) is handled by other VaadinHotswapper implementations and is out of scope.
  • Main rule: a hotswapper re-runs the real generator at the narrowest granularity it offers, and never builds the file content itself.
  • For Flow tasks: expose one narrow public static entry point that shares the private logic with execute(), build a minimal Options from VaadinService (with a null ClassFinder), and unwrap ExecutionFailedException. Uses TaskGenerateReactFiles.writeLayouts and RouteRegistryHotswapper as the worked example, including how the layout-set union gates the rewrite.
  • Explains why writes must go through writeIfChanged (skips no-op writes, writes atomically so Vite never sees a half-written file), and notes that the file tracking half of GeneratedFilesSupport does nothing on the hotswap path.
  • Covers how to get the browser to react, weakest mechanism first: rely on the Vite watcher, then triggerUpdate, then HMR events.
  • Covers Hilla as the second shape of the same pattern: Flow owns only the EndpointGeneratorTaskFactory SPI, the implementation re-runs the whole pipeline via EndpointCodeGenerator.update, and gates on cheap checks first.
  • Describes the case that regeneration cannot fix — @JsModule, @JavaScript, @CssImport, @NpmPackage, @Theme — where only a restart rebuilds the bundle, and how the opt-in vaadin-dev dev loop detects it.
  • Ends with how to test a hotswapper: use a real temp project folder and assert on the file, in both directions.

@github-actions

github-actions Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Test Results

 1 454 files   -  1   1 538 suites   - 1   1h 31m 29s ⏱️ + 1m 38s
11 926 tests  - 13  11 858 ✅  - 13  68 💤 ±0  0 ❌ ±0 
12 244 runs   - 13  12 176 ✅  - 13  68 💤 ±0  0 ❌ ±0 

Results for commit d644415. ± Comparison against base commit 487fb8c.

♻️ This comment has been updated with latest results.

@Artur-

Artur- commented Sep 18, 2026

Copy link
Copy Markdown
Member

@claude review if these are actually followed in the project

@claude

claude Bot commented Sep 18, 2026

Copy link
Copy Markdown

Claude finished @Artur-'s task in 2m 20s —— View job


Review: are these practices actually followed in the project?

I checked every concrete claim in guidelines/hotswap.md against the current code. Almost everything is accurate and matches the implementation. I found one description that doesn't quite match the code, plus a couple of minor simplifications worth noting.

  • Read the new chapter and extract the checkable claims
  • Verify Flow-task claims
  • Verify browser-notification claims
  • Verify Hilla endpoint claims
  • Verify dev-loop restart claim
  • Verify testing claim
  • Post findings

✅ Verified accurate

Claim in the doc Code
TaskGenerateReactFiles.writeLayouts(Options, Collection<Class<?>>) is public static; both execute() and writeLayouts go through private writeLayoutsJson TaskGenerateReactFiles.java:223 (static), :177 (execute), :227 (static → private), :239 (private) ✓
writeLayouts unwraps ExecutionFailedException into a runtime exception TaskGenerateReactFiles.java:228-236
RouteRegistryHotswapper.onClassesChange builds a minimal Options with null ClassFinder + .withFrontendDirectory(...) and calls writeLayouts RouteRegistryHotswapper.java:93-99 — matches the doc snippet almost verbatim ✓
AbstractFileGeneratorFallibleCommand.writeIfChangedFileIOUtils.writeIfChanged AbstractFileGeneratorFallibleCommand.java:56/75GeneratedFilesSupport.java:57/94FileIOUtils.java:470/488 ✓ (one extra hop through GeneratedFilesSupport, see note below)
writeIfChanged writes atomically (temp file in same dir, then move) FileIOUtils.java:507-534 (Files.createTempFile + ATOMIC_MOVE, non-atomic fallback) ✓
HotswapEvent.triggerUpdate(UIUpdateStrategy), REFRESH/RELOAD, RELOAD has priority and can't be downgraded HotswapEvent.java:70-74, javadoc :63-65
HotswapEvent.sendHmrEvent(String, JsonNode) and updateClientResource(String, String) HotswapEvent.java:153, :127
DefaultTranslationsHotswapper sends a translations-update HMR event DefaultTranslationsHotswapper.java:50
StyleSheetHotswapper pushes CSS via updateClientResource StyleSheetHotswapper.java:333/355/397
NodeTasks.addEndpointServicesTasks returns early unless FrontendBuildUtils.isHillaUsed(...), resolves EndpointGeneratorTaskFactory via Lookup, endpoint task added only when options.getFrontendGeneratedFolder() is set NodeTasks.java:347-364
TaskGenerateOpenAPI / TaskGenerateEndpoint are marker FallibleCommand subinterfaces TaskGenerateOpenAPI.java:25, TaskGenerateEndpoint.java:25
GeneratedFilesSupport.track(File); every command gets it via FallibleCommand.setGeneratedFileSupport; TaskRemoveOldFrontendGeneratedFiles deletes untracked files GeneratedFilesSupport.java:122, FallibleCommand.java:51, TaskRemoveOldFrontendGeneratedFiles.java:40-41,87
DevLoopRedefiner.frontendDependencies(Class) fingerprints @JsModule, @JavaScript, @CssImport, @NpmPackage, @Theme, and a redefine escalates when the fingerprint moves DevLoopRedefiner.java:1345-1387, escalation at :294
Test against a real temp project folder, asserting both write and no-write directions; see RouteRegistryHotswapperTest RouteRegistryHotswapperTest.java:217-304 (Files.createTempDirectory("temp-project"), asserts on generated/layouts.json, covers written + left-alone) ✓

The Hilla-side symbols the doc names (com.vaadin.hilla.Hotswapper, EndpointCodeGenerator.update(String...)) live in the separate hilla repo, so I couldn't verify them here — but the Flow-side SPI they hang off of is exactly as described.


⚠️ One description that doesn't match the code

RouteRegistryHotswapper gating (hotswap.md:86-88) — the doc says:

RouteRegistryHotswapper collects the registry's layouts before and after applying the class changes and only calls writeLayouts when that set differs.

That's not what the code does. It collects layouts before and after into the same HashSet (a union, not a diff), then gates on whether any changed class is a member of that union:

Set<Class<?>> layouts = new HashSet<>(((AbstractRouteRegistry) appRegistry).getLayouts());
RouteUtil.updateRouteRegistry(appRegistry, addedClasses, modifiedClasses, removedClasses);
layouts.addAll(((AbstractRouteRegistry) appRegistry).getLayouts());

if (configuration.isReactEnabled()
        && Stream.of(addedClasses, modifiedClasses, removedClasses)
                .flatMap(Set::stream).anyMatch(layouts::contains)) {
    ... writeLayouts(...);
}

(RouteRegistryHotswapper.java:79-100)

So the real gate is "a changed/added/removed class is, or just stopped being, a @Layout" (plus a React-enabled check the doc omits) — never a before-vs-after set comparison. The union of before+after exists precisely so a class that lost its @Layout annotation still matches. The doc's "only when that set differs" phrasing describes a diff that isn't there and would give a reader the wrong mental model when writing their own gate. Worth rewording to something like "only when one of the changed classes is (or was) a layout".

Fix this →


Minor notes (not errors)

  • hotswap.md:74-75 says AbstractFileGeneratorFallibleCommand.writeIfChanged "delegates to FileIOUtils.writeIfChanged". There's an intermediate hop through GeneratedFilesSupport.writeIfChanged (which also does the track() bookkeeping) before reaching FileIOUtils. The Hilla section (:144-146) correctly attributes writeIfChanged to GeneratedFilesSupport, so the two passages are slightly inconsistent about which class "owns" it. Harmless, but a one-line tweak would make them agree.
  • The example snippet at :42-47 uses appRegistry.getLayouts() while the real call casts (((AbstractRouteRegistry) appRegistry).getLayouts()). Fine as a doc simplification.

Bottom line: the guidelines faithfully reflect the implementation. The only substantive correction is the RouteRegistryHotswapper gating description — it's a diff-vs-union/membership mismatch that I'd recommend rewording before merge.
· branch docs/hotswap-generated-frontend-files

Comment thread guidelines/hotswap.md Outdated
@@ -0,0 +1,187 @@
# Hotswap

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We maybe rename this to hotswapping frontend files or similar, as there is also the other part of hotswapping of Java changes such as new routes etc. Otherwise we should make sure it covers the Java only part also

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good point — renamed. The chapter is now guidelines/frontend-hotswap.md, titled "Hotswapping frontend files", and it opens with an explicit scope note saying the purely server-side half (route registries on a @Route change, @StyleSheet link add/remove, reflection caches, translations) is handled by other VaadinHotswapper implementations and is not covered here. The note also points out that both halves plug into the same interface and that one hotswapper often does both, RouteRegistryHotswapper being the example. That keeps the door open for a server-side chapter later without this one implying it already covers that ground.

@Artur- if you'd rather have one chapter covering both halves instead, say so and I'll fold the server-side part in here rather than scoping it out.

and the client reaches them through a chunk keyed by class name. Updating the
imports file alone leaves the chunk wrong, so the bundle has to be rebuilt.

`DevLoopRedefiner.frontendDependencies(Class)` fingerprints exactly these

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

devloop is something opt-in that most do not use today - here it sounds like it is always used

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. That section now leads with the general case: nothing in the runtime hotswap path can repair a @JsModule/@Theme-style change, so what fixes it is an application restart that lets the startup tasks regenerate the imports and rebuild the bundle — in most projects today whatever restart the developer's setup already does. The dev loop is then introduced as opt-in and only active when the daemon launched the application, as the thing that detects the case and escalates automatically rather than leaving it to be noticed. The advice to extend DevLoopRedefiner.frontendDependencies when a new annotation joins that group is unchanged.

Rename to frontend-hotswap.md and state the scope up front: the
server-side half of hotswap - route registries, stylesheet links,
reflection caches, translations - is handled by other VaadinHotswapper
implementations and is not covered here.

Also describe the restart that repairs a frontend-dependency annotation
change as the general case, since the vaadin-dev dev loop is opt-in and
only active when the daemon launched the application.
@sonarqubecloud

Copy link
Copy Markdown

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