Skip to content

fix(hmr): split component ids on the last @ - #492

Open
ashley-hunter wants to merge 4 commits into
voidzero-dev:mainfrom
ashley-hunter:t3code/fix-hmr-scoped-path-names
Open

ashley-hunter wants to merge 4 commits into
voidzero-dev:mainfrom
ashley-hunter:t3code/fix-hmr-scoped-path-names

Conversation

@ashley-hunter

@ashley-hunter ashley-hunter commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator

Fixes HMR for components whose file path contains @, such as scoped packages compiled from source (node_modules/@scope/...) or monorepo directories like packages/@company/app/....

The bug

A component id is {path}@{ClassName}, and the class name was read back out as the text after the first @. When the path itself contains @, that produced an update function named after the path:

compileForHmrSync('<div></div>', 'Foo', 'node_modules/@scope/pkg/src/a.ts', null, {}).hmrModule
// export default function scope/pkg/src/a.ts_UpdateMetadata(scope/pkg/src/a.ts, ɵɵnamespaces) {

which is a syntax error. The vite plugin had the same bug in two places (the @ng/component endpoint and the per-file class-name registration). There it showed up as an empty HMR response every time, with no error.

The fix

A class name is a JS identifier and cannot contain @, so the last @ is the separator.

  • generate_hmr_update_module_from_js: rsplit_once('@'), keeping the Component fallback when there is no @.
  • compileForHmrSync: uses the class name it is already given instead of re-parsing the id. This matches Angular, which names the function from meta.className and never parses ids (@angular/build looks update modules up by the whole id).
  • vite plugin: indexOf('@')lastIndexOf('@') at both sites.

parseComponentId already used rfind.

Also: the endpoint served nothing on Windows

While fixing the CI run on Windows this turned up a separate bug in the same endpoint. componentsByFile and componentMetadataCache are keyed by the transform id, which Vite gives with forward slashes on every platform (C:/Users/...). The endpoint looked them up by path.resolve(fileId), which on Windows is a backslash path, so it:

  • returned an empty module for every component, so template HMR did nothing, and
  • never treated an empty style list as definitive.

The existing tests passed join() paths (backslashes on Windows), which match path.resolve, so they never saw it. The endpoint now looks the maps up by fileId verbatim and keeps resolvedId for the disk reads only. A new test sends a forward-slash id; it failed on Windows CI before the fix (113f612) with only that test red.

Tests

Each test was seen failing before its fix:

  • Rust: ids with one and two @ in the path, and the no-@ fallback.
  • compileForHmrSync / generateHmrModule / parseComponentId with scoped paths; the generated module is checked to parse.
  • Plugin: the endpoint serves the module for a component under packages/@company/..., and pruning a removed class under an @ path leaves its sibling's pending update intact.
  • transformAngularFile with hmr: true on a scoped path emits a parseable initializer containing the full encoded id.

A component id is `{path}@{ClassName}` and the path itself can contain `@`
(`node_modules/@scope/...`, `packages/@company/...`). Taking the text after
the first `@` produced update functions named after the path, e.g.
`function scope/pkg/src/a.ts_UpdateMetadata(...)`, which is a syntax error,
and made the vite plugin serve an empty HMR module for such components.

A class name is a JS identifier and cannot contain `@`, so the last `@` is
the separator. `compileForHmrSync` now uses the class name it is given
instead of re-parsing the id, matching Angular, which never parses ids.
The endpoint resolves the file path with `path.resolve`, which yields
backslashes on Windows, so the tests must pass the same native path the
other plugin tests use rather than a forward-slash one.
…lved path

`componentsByFile` and `componentMetadataCache` are keyed by the transform
id, which Vite gives with forward slashes on every platform. The endpoint
looked them up by `path.resolve(fileId)`, which on Windows is a backslash
path, so it served an empty module for every component and never treated
an empty style list as definitive. Keep the resolved path for the disk
reads only.
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