Skip to content

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

Merged
Brooooooklyn merged 5 commits into
voidzero-dev:mainfrom
ashley-hunter:t3code/fix-hmr-scoped-path-names
Oct 2, 2026
Merged

Brooooooklyn merged 5 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.

@Brooooooklyn
Brooooooklyn force-pushed the t3code/fix-hmr-scoped-path-names branch from fdaf345 to a0e3076 Compare October 1, 2026 16:42
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.
@Brooooooklyn
Brooooooklyn force-pushed the t3code/fix-hmr-scoped-path-names branch from a0e3076 to 97f0827 Compare October 2, 2026 03:30
ashley-hunter and others added 2 commits October 2, 2026 11:38
…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.
Two latent id-encoding bugs in the /@ng/component endpoint, found while
checking the `@`-split fix:

- `URLSearchParams.get('c')` already decodes the percent-encoded id, so
  the extra `decodeURIComponent` ran a second pass: a path with a
  literal `%` either turned `%2F` back into `/` (map lookup miss, empty
  module) or threw `URIError` before the try block. Use the `get` result
  verbatim.
- The `angular:invalidate` error fallback sent the decoded id, but the
  client compares `d.id` against the ENCODED id literal baked into its
  initializer, so the comparison never matched and the reload never
  fired. Send `encodeURIComponent(componentId)`.

Also drop the unused `include_full_metadata` field from
`HmrUpdateModuleOptions`; nothing reads it.

Adds a regression test for a component under a `100%` directory.
@Brooooooklyn
Brooooooklyn force-pushed the t3code/fix-hmr-scoped-path-names branch from 97f0827 to cbbd1d1 Compare October 2, 2026 03:38
@Brooooooklyn
Brooooooklyn merged commit c8893dd into voidzero-dev:main Oct 2, 2026
10 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.

2 participants