FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

fix(vite): stop requesting a full reload on component template HMR by Brooooooklyn · Pull Request #444 · voidzero-dev/oxc-angular-compiler · GitHub

fix(vite): stop requesting a full reload on component template HMR - #444

Merged
Brooooooklyn merged 4 commits into
mainfrom
fix/443-template-hmr-full-reload
Aug 24, 2026
Merged

fix(vite): stop requesting a full reload on component template HMR#444
Brooooooklyn merged 4 commits into
mainfrom
fix/443-template-hmr-full-reload

Conversation

Brooooooklyn commented Aug 23, 2026
edited
Loading

Copy link
Copy Markdown
Member

Fixes #443.

The bug

Editing a component templateUrl .html puts a full-reload on the HMR socket, on top of the angular:component-update that already hot-swapped the template.

edit  src/app/app.html
        │
        ▼
plugin handleHotUpdate
   ├─ ws.send  angular:component-update      ← the real HMR, works
   └─ return ctx.modules  ==  []             ← the template is watched, never a module
        │
        ▼
vite 8   updateModules
   if (!modules.length && file.endsWith('.html') && env === 'client')
        └─ hot.send({ type: 'full-reload', path: … })

Whether the browser obeys depends on the payload path. Vite's client only path-matches when the path ends in .html; a * path skips that test and reloads at once.

setup payload reloads?
plain SPA at / /src/app/app.html no
deep route, or base: '/app/' /src/app/app.html no
server.middlewareMode: true "*" yes
template also in the module graph "*" yes
.css styleUrl edit none sent no

Reproduced at the reporter's versions — @oxc-angular/vite@0.0.35, vite@8.2.1, @angular/*@21.2.21. Console order in the failing case:

[SPY] angular:component-update {…}          ← DOM updates
[SPY] vite:beforeFullReload {"path":"*"}    ← ~2 ms later
[vite] connecting...                        ← new document

Even where the browser ignores the payload, Vite prints page reload src/app/app.html and clears the terminal on every save.

The fix

Register each template with addWatchFile, so it enters the module graph as a js module with the component .ts as its importer, and return it as its own HMR boundary. Both of Vite's .html reload branches are then unreachable.

The browser never imported the template, so Vite's client finds no hotModulesMap entry and the resulting js-update is a no-op there. The DOM change still comes entirely from angular:component-update. Verified: moduleEvalCount: 0, no console errors, and the browser never requests the .html.

Templates only. An import edge on a style makes Vite propagate the change up to the component .ts and re-execute it, which defines a duplicate class (NG0912) and leaves angular:component-update patching a class that is no longer mounted. Styles then go stale from the second edit onward — this showed up as three red e2e tests while developing. Styles never needed the edge: Vite only force-reloads on .html.

Two other candidates were measured and rejected, because both look correct on the first edit and serve a stale template from the second onward:

candidate fixes middlewareMode? second edit
return [] no — path is hardcoded * there ok
addWatchFile alone yes stale
return the component .ts module yes stale
addWatchFile + self-accepting yes ok

Why no test caught it

The e2e sentinel tests assert the browser did not act on a reload. That stayed true while the server kept asking for one. setupEventListeners was meant to cover the gap, but an injected <script type="module"> has no import.meta.hot, so it never registered a listener — and no test ever read its output.

Added HmrDetector.captureWirePayloads, which wraps WebSocket before page load and persists payloads in sessionStorage so a full-reload is still observable after the reload it caused. Two new tests:

  • a template edit sends no full-reload payload (fails on main with ["connected","full-reload","custom","full-reload"])
  • index.html still sends one — it is not hot-swappable and must keep reloading

Verification

  • 213 unit tests pass, 36 e2e tests pass, oxfmt --check clean.
  • middlewareMode repro at the reporter's versions: navs: 0, sentinel survives, second edit renders MARKER_V2_EDITED, moduleEvalCount: 0.
  • Regressions re-checked in middlewareMode: index.html still reloads, component .css still hot-swaps, a global stylesheet still flows through Vite's CSS pipeline (Plugin swallows HMR updates that it doesn't handle #185), plain .ts still full-reloads.

Review round (Codex)

Four P2 findings, each verified empirically before acting. Two were real regressions from this PR and are fixed; two are declined with measurements.

# finding verdict action
1 shared templateUrl only updates one owner pre-existing; stated mechanism refuted; this PR improves it #445
2 ?raw variant marked self-accepting confirmed, introduced here fixed in b52bc85
3 graph edge added when liveReload: false confirmed, introduced here fixed in 07ed3ec
4 self-accept flag survives template pruning confirmed but unobservable; suggested fix is a regression declined

On #4: clearing the flag does not restore Vite's droppable .html reload — the node is type: 'js', so propagation dead-ends and Vite sends path="*", an unconditional reload in both modes. That is the same regression class #3 just removed. Full measurements are in the review threads.

Known limits

  • A templateUrl shared by two components still hot-updates only one of them — pre-existing, tracked in A templateUrl shared by two components only hot-updates one of them #445.
  • At liveReload: false, a reload serves the old template, because every resourceCache.delete sits inside handleHotUpdate past its early return. Pre-existing on main; a product call, not settled here.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FUvBViRkyuDvqUMkuT5XnH


Note

Medium Risk
Touches Vite HMR module-graph and self-accept behavior; a mistake can cause full reloads, stale templates, or duplicate component classes (NG0912). Scoped to templates with unit and e2e coverage.

Overview
Stops Vite from putting a full-reload on the HMR socket when a component templateUrl .html is edited, which stacked on top of angular:component-update and always reloaded in middlewareMode (path *) — #443.

The plugin now addWatchFiles templates (HMR on only, never styles) so they exist as js modules in the graph, then marks the template’s own node self-accepting so the update does not re-execute the component .ts. ?raw and other postfixed variants stay un-marked; index.html still full-reloads.

E2E now records HMR websocket payloads (not just DOM sentinels) and asserts a template edit sends no full-reload while index.html still does.

Reviewed by Cursor Bugbot for commit 07ed3ec. Bugbot is set up for automated code reviews on this repo. Configure here.

)

Editing a component `templateUrl` `.html` put a `full-reload` on the HMR
socket on top of the `angular:component-update` that already hot-swapped
the template.

`handleHotUpdate` returned `ctx.modules` for the template, and that array
is empty: the file was watched but never a module. Vite full-reloads any
changed `.html` whose module list is empty or holds no `js` module.

Vite's client usually drops that payload, because an `.html` path only
reloads when it matches `location.pathname`. That guard is gone when the
path is `*`, which is what Vite sends in `middlewareMode` — so the page
reloaded right after the update was applied. Reproduced at the reporter's
versions (0.0.35 / vite 8.2.1 / angular 21.2.21): `middlewareMode`
reloads, plain SPA does not, `.css` never does. Either way the terminal
printed `page reload …` and cleared the screen on every save.

Register each template with `addWatchFile`, so it enters the module graph
as a `js` module with the component `.ts` as its importer, and return it
as its own HMR boundary. The browser never imported the template, so the
resulting `js-update` is a client no-op; the DOM change still comes from
`angular:component-update`.

Templates only. An import edge on a style makes Vite propagate the change
up to the component `.ts` and re-execute it, which defines a duplicate
class (NG0912) and leaves `angular:component-update` patching a class that
is no longer mounted — styles then go stale from the second edit onward.
Styles never needed it: Vite only force-reloads on `.html`.

The e2e sentinel tests could not catch this. They assert the browser did
not act on a reload, which stayed true while the server kept asking for
one. Added `captureWirePayloads`, which records the HMR socket across
navigations, and two tests: a template edit sends no `full-reload`, and
`index.html` still does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUvBViRkyuDvqUMkuT5XnH

cursor Bot commented Aug 23, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_6491c4da-d714-4760-8ecf-0f808478dec6)

chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ccaa2dd7e1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

`{ id, _clientModule: { isSelfAccepting: false } }` is not assignable to
`Partial<ModuleNode>` — `_clientModule` is an `EnvironmentModuleNode`.
CI's Lint step failed on it, before the e2e suite ran.

Extract `createMockTemplateModule`, which casts once and hands back the
client node so the assertion stays readable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUvBViRkyuDvqUMkuT5XnH

cursor Bot commented Aug 23, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_e4ee3073-b717-4d85-9270-aa76cbbb3763)

chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 045049e3b9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Vite sets `mod.file` to `cleanUrl(mod.id)`, so a template that application
code also imports as `./tpl.html?raw` is filed under the same path and lands
in `ctx.modules` next to the node `addWatchFile` created. Matching on `file`
marked that browser-imported variant as a boundary too, and Vite then stopped
propagation at a module with no `import.meta.hot.accept` handler — its
importers kept the stale raw string.

Match on `id` instead. An exact match means same file, no postfix, so it
covers `#fragment` as well as `?query`.

Measured on an Angular app whose non-component module imports the same file
as `./app.html?raw`:

  before   raw value after a template edit: MARKER_V0  (stale)
  after    raw value after a template edit: MARKER_V1_EDITED

#443 stays fixed — no `full-reload` in SPA or middlewareMode when nothing
imports the template. With a `?raw` importer present the page reloads again,
which is Vite's native semantics for a chain with no accept boundary, and is
what keeps the raw importer correct.

Reported by Codex review on #444.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUvBViRkyuDvqUMkuT5XnH

cursor Bot commented Aug 23, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_f22e4b49-4658-4a4d-b1b0-8fc58e66f071)

chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b52bc850e5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

With `liveReload: false`, `handleHotUpdate` returns on its first line, so
the graph edge has no consumer inside the plugin. It still fed Vite's
default propagation, which found no accepting boundary and issued a full
reload — turning a payload the client used to drop into an unconditional
one with path `*`.

Measured on an Angular app at `liveReload: false`, editing a template:

  build   mode   payload                                    navs
  main    SPA    full-reload path=/src/app/app.html          0
  PR      SPA    full-reload path=*                          1   <- regression
  gated   SPA    full-reload path=/src/app/app.html          0
  main    MW     full-reload path=*                          1
  gated   MW     full-reload path=*                          1

The gate restores main's payloads exactly, and `liveReload: true` is
unaffected — no `full-reload` in either mode, template still self-accepting.

Also collapse the duplicated plugin lookup in the test file into
`getAngularPlugin(options)` rather than add two more copies.

Reported by Codex review on #444.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUvBViRkyuDvqUMkuT5XnH

cursor Bot commented Aug 23, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_2dd1fb91-c7d4-4420-aaa0-bc7b6138f735)

arnoudb commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Nice! I tried to fix it myself but failed 😅. I can confirm this branch solves the issue i expereinced.
Amazing how fast this has been picked up. Thnx @Brooooooklyn!

Brooooooklyn merged commit b86ec45 into main Aug 24, 2026
11 checks passed
Brooooooklyn deleted the fix/443-template-hmr-full-reload branch August 24, 2026 01:19
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HMR always causes a full page reload in vite.

2 participants


Back | FazBrowse Home | New Git URL