Skip to content

feat(hmr): component template & style hot module replacement (#145) - #187

Merged
lukekania merged 3 commits into
milestone/v0.11.0-builder-parityfrom
feat/hmr-compiler-codegen
Jun 9, 2026
Merged

feat(hmr): component template & style hot module replacement (#145)#187
lukekania merged 3 commits into
milestone/v0.11.0-builder-parityfrom
feat/hmr-compiler-codegen

Conversation

@lukekania

Copy link
Copy Markdown
Owner

Lights up Angular-parity template/style HMR end to end (#145), building on the config plumbing (#185) and dev-server endpoint + runtime bus (#186). Stacked on #186.

template-compiler

  • New hmr module: encode_uri_component (JS encodeURIComponent semantics), per-component id encodeURIComponent("relpath@Class"), the initializer IIFE, and the update-module codegen. The update module reuses the existing Ivy definition verbatim via the linker's var-alias technique (var ɵɵX = i0.ɵɵX), with @angular/core arriving as the namespaces argument and template deps as positional params; it reassigns only ɵcmp (template/style-only path).
  • CompileOptions.hmr and CompiledFile.hmr carry the artifacts; the source rewrite adds import * as i0 so the appended initializer can reach ɵɵreplaceMetadata.

cli

  • Thread hmr through the build; aggregate per-component update modules and resource→component maps into the build result; cache the artifacts in the incremental module cache.
  • serve: classify each rebuild — global stylesheet → css-update, external component template/style → component-update(s), .ts/inline/unknown → full reload — and inject the import.meta.hot prelude into every chunk (eager main.js and lazy chunk-*.js).

Verified end-to-end (Angular 21 app)

  • Editing a component template (.html) → swapped in place via the /@ng/component update module and ɵɵreplaceMetadata; no reload, state preserved. The served module reflected the live template edit.
  • Editing a component external style (.scss) → component-update.
  • Editing a global stylesheet → css-update <link> swap.
  • Editing a component class (.ts) → full reload.

The bundler correctly bound import * as i0 to the synthesized core namespace (var i0 = __ns_angular_core_...), and the initializer / update-module references resolved after bundling.

Definition of done (#145)

  • .css edit replaces styles in-place without reload ✅
  • .html template re-renders the affected component without reload ✅
  • .ts edit falls back to full reload ✅
  • hmr: true enables the above; hmr: false (default) keeps full-reload behavior ✅

Follow-ups (deferred)

  • Inline-template / inline-style (.ts) HMR detection via def diffing (currently reloads).
  • Multiple components per file (currently HMRs the first component; pipeline is single-component today).

Targets the milestone branch via the stack; retarget to it once #186 merges.

lukekania added 3 commits June 8, 2026 17:00
First slice of Hot Module Replacement: read the `hmr` flag and hot-swap
global stylesheets in place without a full page reload.

- project-resolver: parse `architect.serve.options.hmr` (base + active
  serve configuration override) into `ResolvedAngularProject.hmr`;
  defaults to false.
- cli: add `--hmr` / `--no-hmr` to `serve` (CLI wins over angular.json,
  else inherit, else false). When HMR is on and a rebuild touches only
  global stylesheet entries, emit a CSS-update instead of a reload.
- dev-server: new `DevServerEvent::CssUpdate { timestamp }` →
  `event: css-update` SSE frame; the injected client swaps the
  `styles.css` <link> with a cache-busted href, preserving page state.

Component template/style HMR and the `/@ng/component` update endpoint
land in follow-up slices on the milestone branch.
Runtime scaffolding for component-level HMR, ahead of the compiler
codegen that populates it.

- dev-server: new `DevServerEvent::ComponentUpdate { id, timestamp }` →
  `event: angular:component-update` SSE frame; a shared `ComponentUpdates`
  registry (id → update-module source) on `DevServerConfig`, served at
  `GET /@ng/component?c=<id>` (text/javascript, empty 200 for unknown ids,
  400 when `c` is absent) — mirrors @angular/build's component middleware.
- client: the injected script publishes a `window.__ngcHmr` bus and
  dispatches `angular:component-update` to registered handlers; new
  `HMR_RUNTIME_PRELUDE` binds `import.meta.hot` to that bus.
- serve: share the registry with the dev server and prepend the runtime
  prelude to `main.js` on each HMR build so per-component initializers can
  resolve `import.meta.hot`.

The registry stays empty and component edits still trigger a full reload
until the template-compiler emits update modules (next slice).
Lights up Angular-parity template/style HMR end to end, building on the
config plumbing, dev-server endpoint, and runtime bus from the previous
slices.

template-compiler:
- New `hmr` module: `encode_uri_component` (JS semantics), per-component
  id `encodeURIComponent("relpath@Class")`, the initializer IIFE, and the
  update-module codegen. The update module reuses the existing Ivy def
  verbatim via the linker's var-alias technique (`var XX = i0.XX`), with
  `@angular/core` arriving as the `namespaces` arg and template deps as
  positional params; it reassigns only `ɵcmp` (template/style-only path).
- `CompileOptions.hmr` and `CompiledFile.hmr` carry the artifacts;
  `rewrite_source` adds `import * as i0` so the appended initializer can
  reach `ɵɵreplaceMetadata`.

cli:
- Thread `hmr` through `run_build_with_options`; aggregate per-component
  update modules and resource→component maps into `BuildResult`; cache the
  artifacts in the incremental `CachedModule`.
- serve: classify each rebuild — global stylesheet → css-update, external
  component template/style → component-update(s), `.ts`/inline/unknown →
  full reload — and inject the `import.meta.hot` prelude into every chunk
  (eager `main.js` and lazy `chunk-*.js`).

Verified against an Angular 21 app: editing a component `.html`/`.scss`
swaps it in place via the `/@ng/component` update module and
`ɵɵreplaceMetadata`; global styles swap the `<link>`; `.ts` reloads.
@lukekania
lukekania changed the base branch from feat/hmr-component-endpoint to milestone/v0.11.0-builder-parity June 9, 2026 14:09
@lukekania
lukekania merged commit f4e31ca into milestone/v0.11.0-builder-parity Jun 9, 2026
6 checks passed
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.

1 participant