Skip to content

fix: align host bindings, .d.ts injection and SSR/linker with ngtsc (#686–#696) - #697

Merged
Brooooooklyn merged 22 commits into
mainfrom
fix/issues-686-696
Nov 10, 2026
Merged

Brooooooklyn merged 22 commits into
mainfrom
fix/issues-686-696

Conversation

@Brooooooklyn

@Brooooooklyn Brooooooklyn commented Nov 9, 2026 •

Copy link
Copy Markdown
Member

Fixes the issues we filed while reviewing #682, #683 and #685, each checked against ngtsc 22.2 (and 19.2 / 20.1 / 20.2 where a version gate applies).

Fixes #686
Fixes #687
Fixes #688
Fixes #689
Fixes #690
Fixes #691
Fixes #692
Fixes #693
Fixes #694
Fixes #695
Fixes #696

What changed

host bindings (partial + full + linker)
  #686  partial host maps: every key quoted          {"role": …}  like toOptionalLiteralMap
  #687  "__proto__" host keys dropped                 like ngtsc
  #690  @HostBinding value: this.x / this['my-prop']  getSafePropertyAccessString
  #688  [@trigger] ordered like UPDATE_HOST_ORDERING
  #689  [animate.*] / (animate.*) / animate.* host → ɵɵanimateEnter/Leave
          < 20.2          → plain DOM property / hostAttrs (no new instruction)
          20.2.0–20.3.0   → + features: [ɵɵAnimationsFeature()]  for animate.leave
          ≥ 20.3.1        → no feature

.d.ts  (Rust dts_inject + vite dts plugin)
  #692  members name classes as typeof iN.X (own import), ɵmod lists from the module
  #693  declaration map shifted with the splices (identity map in transform)
  #691  each .d.ts gets only its own module's members
          transform hook (per module) ─┐
          generateBundle (assets)  ────┼─► match by declaration map, else by one
          writeBundle (on disk)    ────┘   (prefix, root) layout per dir; never by class name
  #694  vite-plugin-dts (writes in writeBundle) and tsdown .d.cts pass covered

vite
  #695  build: excluded deps are linked; dev: only that env's optimizeDeps.exclude is skipped
  #696  SSR manifest embeds the built client index.html
          captured client page → client outDir on disk → source page (warns) → minimal page

Known limits

Not in this PR (filed as follow-ups)

Tests

  • cargo test (all features and default): all pass
  • conformance: 1264/1264
  • vitest: all pass; Playwright ssr-manifest: 8/8
  • The issue repros were re-run on real builds: ngtsc / the Angular linker for host output, rolldown + rolldown-plugin-dts, tsdown (esm + cjs), vite + vite-plugin-dts and @rollup/plugin-typescript for .d.ts, with consumer tsc (skipLibCheck: false), and Vite 8 --app for SSR.

🤖 Generated with Claude Code

Brooooooklyn and others added 21 commits November 9, 2026 18:56
…l for SSR

- linker: only the dev server skips `optimizeDeps.exclude`, using each
  environment's own list. Builds link every package, like the Angular CLI,
  so server bundles no longer keep ngDeclare calls from
  @angular/platform-server.
- ssr manifest: share the plugin across environments and embed the client's
  built index.html (captured in generateBundle, else read from the client
  outDir) as index.server.html, so SSR pages load the hashed bundles instead
  of /src/main.ts. Warn when falling back to a stale or source page.

Fixes #695
Fixes #696

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Class references in `ɵmod` lists and `hostDirectives` of the `.d.ts`
now follow ngtsc's `toR3Reference` (ForceNewImport): a class of this
file keeps its name, any other is its export through a namespace import
(`typeof i1.Dep`, `typeof i1.default`).

- The `.d.ts` has its own namespace registry, like the second
  ImportManager of `IvyDeclarationDtsTransform`, so its `iN` are numbered
  by first use there and the JS gains no import.
- `ɵmod` lists come from the flattened `ModuleLists` (spreads, `const`
  lists, `ns.X`, `ModuleWithProviders<T>`), the same read as the partial
  declaration.
- `generate_component_dts`, `generate_directive_dts` and
  `generate_ng_module_dts` take the precomputed type references.

A same-file class that is not exported stays bare, like ngc. A barrel
specifier is kept as written.

Fixes #692

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Split inject_dts_declarations into plan_splices + merge/apply (same
output) and add:
- inject_dts_declarations_with_map: shifts an existing map's mappings;
  original tokens keep their mapping, injected text is unmapped, as in
  ngtsc's afterDeclarations emit.
- inject_dts_declarations_with_identity_map: a map from the new code back
  to the original, for a bundler transform hook.

Expose both over napi and as injectDtsDeclarationsWithMap /
injectDtsDeclarationsWithIdentityMap in vite-plugin/utils/dts.ts.

Fixes #693

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…indings

Host property names now go through one shared parse_host_property_name
(component, directive and linker paths), following Angular's
parsePropertyBinding:

- `@x` is a LegacyAnimation binding named `x` (the `@` is stripped and
  added back by reify). It becomes a DomProperty, so it sorts with the
  other DOM properties and adjacent triggers chain, as in ngtsc.
- `animate.*` is an Animation binding. The host convert_animations pass
  turns it into an Animation create op, so it emits
  ɵɵanimateEnter/Leave(function <Fn>_HostBindings_<name>_cb() {...}) in
  the create block with no host vars. Host naming, host reify and
  temporaries now handle that op.
- Host ingest keeps the animation kind when stripping `attr.`, so
  `[@attr.foo]` stays the trigger `@foo` (Angular ingest.ts).

Temporaries in an `animate.*` callback are now declared inside it, which
also fixes the undeclared `_tmp` in template `[animate.*]` callbacks.

Fixes #688
Fixes #689

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… ngtsc

- Quote every key of the partial host attributes / listeners / properties,
  like ngtsc's toOptionalLiteralMap.
- Drop host listener / property bindings named exactly __proto__ in
  merge_host_bindings (full and partial), and skip __proto__ keys in the
  linker's host attributes / listeners / properties, like ngtsc and
  Angular's linker, which assign into a plain object.
- Read a @HostBinding member through `this`
  (getSafePropertyAccessString('this', name)): `this.m`, `this['my-prop']`.

Fixes #686
Fixes #687
Fixes #690

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Like ngtsc (DtsTransformRegistry keys members by source file and class
node), each declaration file now only gets the members of its own
source module, and its declaration map stays in step.

- Bundled declarations (rolldown-plugin-dts, tsdown): a new `transform`
  hook (order 'pre') adds members to each `.d.ts` module before the
  bundler merges and renames classes, and returns an identity map so
  the bundler chains the dts map. It waits for the source with
  `this.load` when needed. `generateBundle` no longer touches chunks
  and never flattens by class name.
- `.d.ts` assets (emitFile): matched to one source by tsc's rootDir
  layout (`matchDtsSource`), and their map (asset, inline base64 or
  `<file>.map`) is shifted.
- Files a generator writes itself (vite-plugin-dts): a `writeBundle`
  pass (order 'post', serialized) scans `output.dir` and Vite's
  `build.outDir`, skips node_modules, dot-dirs and dirs at or above the
  root, matches files by the same path rule and updates sibling maps.
- tsdown `.d.cts`: `tsdownConfig` wraps `inputOptions` so the cjs
  declaration pass gets a fresh, collect-only angular() instance.
- README: list what is covered and what is not.

Fixes #691
Fixes #692
Fixes #693
Fixes #694

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	crates/oxc_angular_compiler/tests/linker_test.rs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… embedded

Follow-up to #696 (review findings on the SSR manifest plugin):

- `vite build --app --watch` builds the client and server environments at
  the same time, so the server could embed the previous round's client
  index.html (a deleted hashed script) without a warning, and round 1 told
  users to "List `client` first" even when it already was. The client now
  drops its captured page at each watch round's buildStart, the server warns
  that in watch mode the SSR page may come from the previous client round or
  the source page, and the "List `client` first" advice is only given when
  `client` is listed after the server environment.
- The fallback warning fired for every server build, even when no module
  uses AngularAppEngine. It is now emitted once per build, from the first
  transform that injects the manifest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two review findings for #689:

- No angularVersion gate: host `[animate.*]` emitted
  ɵɵanimateEnter/Leave for Angular < 20.2, which @angular/core of that
  version does not export. Host ingest now keeps `animate.*` a plain
  property binding when the version is known and older than 20.2, so
  directive, component and linker output is ɵɵdomProperty /
  ɵɵhostProperty("animate.enter", ...) with hostVars, as ngtsc 20.1.8 /
  19.2.15 emit it. Unknown versions keep the new behaviour.

- The linker put static `animate.enter` / `animate.leave` host
  attributes into hostAttrs. They now go to the host binding job as
  static attributes and become ɵɵanimateEnter/Leave("value") in rf&1,
  after the listeners, matching Angular's babel linker output. Before
  v20.2 they stay in hostAttrs, like Angular's linker of that version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rt names

Review fixes for #691, #692 and #693.

- #691/#693 "Bundled .d.ts now loses the JSDoc of the first class in a file
  with no imports" and "The new import goes between the first statement's
  leading JSDoc and the class": in a .d.ts with no import, the namespace
  imports now go before the first statement's comments, as in ngtsc
  (import, /** doc */, class). `/// <reference>` directives and a file
  header followed by a blank line (TS's detached comments) stay on top.
  rolldown-plugin-dts and tsdown (esm and cjs) now keep the class docs.
- #693 "shift_mappings is O(splices x file size)": find each splice's
  line and column in one pass, move each segment by binary search, and
  merge the 1-field break segments while writing each line. A 2 MB .d.ts
  with 11000 classes went from 91 s to under 5 s in a debug test run.
- #692 "Imports named by a string give typeof i1.SDir": `"s-dir" as SDir`
  now gives `typeof i1["s-dir"]` (ngc writes `typeof i1.s-dir`, which does
  not parse); `"Plain" as P` gives `i1.Plain`, as in ngc. The injection
  pass imports the namespace such a type query names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review fixes for #691 and #694 (vite dts plugin).

- Disk pass and generateBundle assets: a .d.ts with a declaration map is
  matched to the source the map names, and left alone when that source is
  not in the graph. Without a map, one layout (output prefix + source
  root) is voted per output dir, and a file matches only when its path is
  exactly that layout for a graph source. Fixes "Disk/asset path match
  gives a non-graph .d.ts another module's Ivy members", "Disk pass suffix
  match gives a file outside the bundle another module's Ivy members"
  (test doubles such as src/testing/button.ts stay plain), and "No members
  at all when the graph's common dir is above the generator's root" /
  "vite-plugin-dts with entryRoot set" (entryRoot 'src' + ../shared).
  A tie goes to the shorter prefix, then to the root holding more graph
  sources (tsc's common dir); a full tie matches nothing.
- Warn once per dir when none of the files a generator wrote can be
  matched.
- Index sources by base name and match a whole dir at once ("matchDtsSource
  is O(files x sources)"): 2000 assets now take ~15 ms instead of ~1.5 s.
- tsdown: an object inputOptions is merged with tsdown's own mergeConfig
  (loaded from the project), so non-cjsDts passes get exactly tsdown's
  plugin order and array rules (0.23: plugins [defaults, user]; <= 0.22:
  arrays replaced). If tsdown/config can't be loaded, the object is left
  alone. Fixes "tsdown inputOptions wrapper changes how tsdown merges an
  object-form inputOptions".
- tsdown < 0.21.9 has no plugin hooks: the disk pass now does nothing
  there, and the README says the .d.cts needs tsdown >= 0.21.9. Fixes
  "README says tsdown cjs .d.cts is covered, but that needs tsdown >= 0.22"
  (the real floor is 0.21.9).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Angular 20.2.0 to 20.3.0 adds features: [ɵɵAnimationsFeature()] when a
host attribute, property or listener is animate.leave
(hasAnimationHostBinding in addFeatures). In those versions only that
feature sets up the registry ɵɵanimateLeave writes to, so without it the
directive throws at creation. Full compile and the linker now add it in
that window; 20.3.1 dropped both and gets nothing.

Also, before 20.2 a static animate.* host attribute stays in hostAttrs in
full compile too, as the linker already did and as ngtsc does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… ngc

The detached-header rule kept a comment group followed by a blank line
above the new import. ngc edits the declaration AST, so its import is
always first, and the rule could split a JSDoc from its class when a
blank line followed the JSDoc. Only /// <reference> and /// <amd-…>
directives stay on top now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- matchDtsSources: when layouts tie, a file matches only when every tied
  layout gives it the same source. The 'root holding more sources'
  tie-break could give a testing/ stub the members of its src/lib/ twin.
- A declaration map source reached through a symlink (macOS /tmp) is
  compared by real path, so it still finds its graph module.
- The no-match warning no longer says to turn on declarationMap when the
  files already have maps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Brooooooklyn

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-09T13:06:04.322444Z b91ef14 Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9d6fa01cff

ℹ️ 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".

A .d.ts member like typeof i1["s-dir"] was not seen as a use of i1,
because only "i1." was looked for, so the namespace import was left
out when no other member used that module. Look for "i1[" too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Brooooooklyn

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

Reviewed commit: b91ef14de9

ℹ️ 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".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment