Skip to content

fix: close older ngtsc parity gaps found while reviewing #660 - #662

Merged
Brooooooklyn merged 37 commits into
perf/shared-const-poolfrom
fix/older-ngtsc-gaps
Oct 30, 2026
Merged

Brooooooklyn merged 37 commits into
perf/shared-const-poolfrom
fix/older-ngtsc-gaps

Conversation

@Brooooooklyn

@Brooooooklyn Brooooooklyn commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

Summary

Stacked on #660. Fixes the older ngtsc parity bugs found while reviewing #660. Each one was reproduced against ngtsc / compiler-cli 22.2.3 (or the Angular 21.2 source for the pre-v22 cases) before it was fixed. Each fix has its own commit and a test that fails without it.

(main) <- #660 perf/shared-const-pool <- #662 fix/older-ngtsc-gaps

Fixes

commit bug before after (= ngtsc)
fix(emitter) useClass: makeClass(), deps: [DEP] new makeClass()(dep) throws new (makeClass())(dep)
fix(injectable) optional-chain factory / useClass Maybe?.make(), Maybe?.cls.ɵfac(t) return undefined (Maybe?.make)(), (Maybe?.cls).ɵfac(t)
fix(injectable) useClass: X, deps: [] X.ɵfac(t) new (X)(); partial keeps deps: []
fix(transform) decorated class inside a function, block, arrow or IIFE left as __decorate(...), needs JIT compiled; consts hoisted after the imports (also a const it shares with an earlier class); its deferrable imports count with the file's other components
fix(queries) @ViewChild('x, ') ["x"] ["x",""] (JS trim rules)
fix(linker) linker query predicates inline arrays pooled _cN consts
fix(linker) linker with angularVersion < 22 no attrs attrs: _cN, as Angular 21's linker
fix(i18n) i18n_N / MSG_..._N names restart at 0 per class counted across the file
fix(emit) order inside one template _c0, A_Defer_1_DepsFn, _forTrack0 A_Defer_1_DepsFn, _c0, _forTrack0 (pooled order)
fix(compile-template) compileTemplate with host bindings template consts first host consts first
fix(partial) @defer in partial mode static import kept, eager dependencies deferBlockDependencies + ɵɵngDeclareClassMetadataAsync, import removed; our linker links it
fix(defer) imports: [ChildA, ...SHARED] (or a nested array / ns.X entry) ChildA stays eager ChildA lazy-loaded
fix(hmr) edit a nested component's inline template full reload component update
fix(debug-info) setClassDebugInfo of a class without export decorator's line class keyword's line
fix(transform) decorated class in a namespace left as __decorate(...) compiled, no metadata calls after it, .d.ts member under NS.X
fix(linker) useClass: Maybe?.cls / a ? B : C in the linker Maybe?.cls.ɵfac(t) (Maybe?.cls).ɵfac(t)
fix(injectable) useFactory: f, deps: [] () => f(), partial drops deps conditional factory; partial keeps deps: []
fix(service) @Service in partial mode full ɵɵdefineService ɵɵngDeclareService + ɵɵngDeclareFactory; our linker links it
fix(defer) same-file eager @defer dependency no DepsFn const A_Defer_1_DepsFn = () => [Dep]
fix(defer) imports: IMPORTS (same-file const, let, alias, as const) no per-block DepsFn per-block DepsFns, shared across classes
fix(defer) @defer block using a class from [OtherB] / ns.X left out of the DepsFn listed
fix(partial) partial dependencies with [OtherB], ns.X, ...SHARED entries dropped listed, deduped
fix(defer) pipe used only inside @defer eager lazy-loaded
fix(linker) query calls in linked code one call each chained, ɵɵqueryAdvance(n)
fix(linker) no angularVersion ɵɵproperty ɵɵdomProperty (DomOnly), as Angular 22's linker
fix(linker) i18n closure names MSG_APP_... named after the linked file
fix(linker) constants of several declarations in one file one pool per declaration one pool per scope: names counted across the file, equal constants shared (a @defer DepsFn that reads inner bindings stays local)
fix(i18n) closure names with external ids MSG_<suffix>N MSG_EXTERNAL_<digest>$$<suffix>N
fix(reify) _forTrack vs arrowFn in one template arrowFn first _forTrack first
fix(transform) where pool declarations go before each class all after the imports, in pool order (top-level declaration order now matches ngtsc in all 63 harness files)

Not changed

  • A generated _c0 shadowed by an enclosing function make(_c0): ngtsc emits the same. The PR thread is left open for a decision.
  • getComponentDepsFactory(T, [...]) for an eager imports list we can't resolve without type info: by design, as in single-file builds.
  • A nested class whose @defer block uses a sibling local class: ngtsc hoists that DepsFn outside the sibling's scope, which breaks at runtime; ours keeps getComponentDepsFactory.

Checks

🤖 Generated with Claude Code

Brooooooklyn and others added 12 commits October 29, 2026 15:07
A useClass with deps is instantiated as `new useClass(...deps)`, but the
emitter printed the class expression bare. For `useClass: makeClass()`
that gave `new makeClass()(dep)`, which constructs `makeClass` itself and
throws "Class constructors cannot be invoked without 'new'"; for
`useClass: Maybe?.cls` it gave `new Maybe?.cls(dep)`, a SyntaxError.

ngtsc's `ts.factory.createNewExpression` runs the callee through
`parenthesizeExpressionOfNew`, which wraps a callee whose left spine holds
a call, an optional chain or a non-left-hand-side expression:
`new (makeClass())(i0.ɵɵinject(DEP))`. Port that rule for Instantiate.

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

A factory written as an optional chain (`useFactory: Maybe?.make`, or
`@Service({ factory: Maybe?.make })`, with or without deps) was called
as `Maybe?.make()`, which joins the chain and silently returns
undefined when `Maybe` is nullish. A `useClass: Maybe?.cls` without
deps had the same problem: `Maybe?.cls.ɵfac(__ngFactoryType__)`.

ngtsc calls the user factory through `ts.factory.createCallExpression`,
whose `parenthesizeLeftSideOfAccess` wraps an optional chain:
`(Maybe?.make)()`, which throws instead. Property access does the same,
`(Maybe?.cls).ɵfac(__ngFactoryType__)`. Wrap such expressions the same
way in the useFactory wrapper, the delegated factory, the service
factory and the useClass delegation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`useClass: X, deps: []` was read as having no deps, so full output
delegated to `X.ɵfac` and partial output dropped `deps: []`. A plain
class (`useClass: Holder.cls()`) has no `ɵfac`, so it threw
"Holder.cls(...).ɵfac is not a function" in both modes.

ngtsc only checks whether the `deps` key is present: full output builds
`new (Holder.cls())()` in the conditional factory, and partial output
keeps `useClass: Holder.cls(), deps: []`. Track a missing `deps` key as
`None` for useClass and build the delegated factory for an empty list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A string predicate like `@ViewChild('x, ')` is split on `,` into its refs.
The file transform dropped the empty parts (`["x"]`), and the linker did not
split at all: it passed `["y, z"]` through as one ref. Both trimmed with
Rust's white space set, which strips U+0085 and keeps U+FEFF.

Angular's `getQueryPredicate` does `selector.split(',').map(t => t.trim())`
for every string of the predicate, keeping empty parts: ngtsc 22.2.3 gives
`["x", ""]`, and Angular's linker splits a partial `["y, z"]` into
`["y", "z"]`. `String.prototype.trim` strips U+FEFF and keeps U+0085
(`["x"]` for `'x'`, `["y\u0085"]` for `'y\u0085'`). Do the same in
both paths, for decorator and signal queries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The linker emitted string query predicates inline, as
`ɵɵviewQuery(["x"], 5)`, and started the host bindings' pool at `_c0`.

Angular's linker compiles a declaration with `compileComponentFromMetadata` /
`compileDirectiveFromMetadata` and one `ConstantPool`: `getQueryPredicate`
pools each string predicate with `getConstLiteral(..., true)`, content queries
first, then view queries, before the host bindings' constants and the
template's, which reuse equal literals (`["*"]` for `ngContentSelectors`).
A predicate with a lone surrogate (`["\uD800"]`) is split and pooled like any
other. Pool them the same way, in that order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The linker never emitted `attrs`, whatever `angularVersion` was. The file
transform does since 1a577e6, for versions below 22.0.0-next.4.

Angular 21's linker runs `compileComponentFromMetadata`, which pools the
first selector's attributes after `baseDirectiveFields` (the query predicates
and the host bindings' constants) and before the template, and sets `attrs`
after `features`: a `span[bitBadge]` component linked with 21.2.25 gives
`const _c0 = ["bitBadge", ""]; ... attrs: _c0, ngContentSelectors: _c1`.
Do the same when `angularVersion` asks for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each component numbered its i18n_N and MSG_ names from its own index, so
the second component of a file started again at i18n_0 and MSG_..._0,
and the Closure names had an extra underscore after the file suffix
(MSG_TEST_TS__0).

ngtsc claims both names with pool.uniqueName, so they run on across the
file: i18n_ counts on its own, the external Closure name counts the file
suffix (MSG_EXTERNAL_<id>$$TEST_TS_0), and the other one counts
MSG_<suffix> (MSG_TEST_TS_0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A template declared all its pooled values first and then the pool's
statements, so a DepsFn pooled before a pure function was declared
after it: _c0, A_Defer_1_DepsFn, _forTrack0.

ngtsc's ConstantPool.statements is one list in insertion order, which
gives A_Defer_1_DepsFn, _c0, _forTrack0. The pool now records how many
statements came before each value, and emit interleaves them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
compileTemplate (compile_template_to_js_with_options) compiled the
template first and the host bindings after, so the template's constants
took the first _c names and a host constant equal to a template one
reused the template's. The file transform and the linker already do
host first.

ngtsc's compileComponentFromMetadata compiles the host bindings
(baseDirectiveFields) before the template with one pool: the host
bindings' constants come first, and the template reuses equal ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Partial compilation returned before the `@defer` step, so an `imports`
entry used only inside `@defer` kept its static import and an eager
`dependencies` entry, and the declaration had no
`deferBlockDependencies`.

ngtsc's `compilePartial` resolves the blocks as its full compilation
does: the entry leaves `dependencies`, its static import is removed,
`ɵɵngDeclareComponent` lists one dependency function per block in
`deferBlockDependencies` (null for a block without dependencies), and
`ɵɵngDeclareClassMetadataAsync` takes the lazy class as a
`resolveMetadata` parameter. The `@defer` step now runs for both modes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An `imports` entry that doesn't name a class, a spread
(`imports: [ChildA, ...SHARED]`), a nested array (`[ChildA, [OtherB]]`)
or a namespace member (`[ChildA, ns.ChildB]`), made the `@defer` step
give up on the whole component, so ChildA, used only inside `@defer`,
stayed an eager dependency and kept its static import, in that
component and in any other of the file that defers it.

ngtsc's `registerDeferrableCandidate` only skips such an entry, which is
not an identifier: ChildA is still lazy-loaded, and the classes the
entry brings in stay eager, even when one of them is the deferred
class. These entries are now skipped the same way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The transform only walked top-level statements, so a @component,
@directive, @pipe, @Injectable or @NgModule class declared in a function
body, block, arrow or IIFE kept its `__decorate` call and needed the JIT
compiler at runtime.

ngtsc compiles these classes like top-level ones: the definitions go on
the class, `setClassMetadata` / `setClassDebugInfo` / the HMR initializer
right after it in the same scope, and the constant pool and child template
functions at the file top level, after the imports, shared with the file's
other classes (the function may run before its own statement).
A class declared in another class's members compiles before that class.
Now the transform does the same, keys class definitions by span (two
functions may declare classes of one name), keeps nested classes out of
the .d.ts, and finds them for import elision, resource URLs and HMR
updates. A nested class whose metadata or constructor parameter types
read a binding of the enclosing function keeps its decorators: the
evaluator and import map only see top-level bindings, and would
otherwise miss a local const or use the binding a local one shadows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Brooooooklyn
Brooooooklyn added this pull request to stack #663 October 29, 2026 07:10
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Codex Review Summary

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

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-29T10:41:04.244585Z 9191f1d 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.

@Brooooooklyn Brooooooklyn changed the title fix/older ngtsc gaps fix: close older ngtsc parity gaps found while reviewing #660 Oct 29, 2026
@Brooooooklyn

Copy link
Copy Markdown
Member Author

@codex review

@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: 85da8a6256

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

Brooooooklyn and others added 2 commits October 29, 2026 15:18
… class

A nested class can reuse a constant an earlier top-level class pooled, which
was declared before that earlier class. A function declared later in the file
can run before that statement, so the nested class read the constant in its
TDZ (ReferenceError). Declare the constants of every class compiled before
the last nested one after the imports too, where ngtsc declares them all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The count of other components' deferrable `imports` entries walked only
top-level classes. A nested component that defers an import counted the
top-level ones and removed the static import, while the top-level component
missed the nested mark and kept an eager reference: ReferenceError when the
module loads. Two nested components deferring one import both stayed eager.

Walk every class the transform compiles, as ngc's DeferredSymbolTracker sees
every component of the file. A nested class that keeps its decorators (it
reads an enclosing binding) keeps its `imports` entry and is skipped.

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 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: 06b1fb0003

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

HMR finds a component by {path}@{name}. Two classes named Tp in different
functions shared that id, so both listeners took one update, and the update
compiler always picked the first Tp: changing the second component's template
could install the first's on both. ngtsc builds the same id for both.

Compile a class whose name another class of the file declares without HMR
(no initializer, no template update), and give it no HMR update.

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 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: 713bf03a71

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

The guard that keeps a nested class's decorators when its metadata reads an
enclosing binding visited only the arguments of a field initializer call. With
`function make(input) { class C { value = input(0); } }` the callee resolved
to the imported `input` by name, so C got a signal input ngtsc doesn't see.
Visit the callee too.

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 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: f4d0a67980

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

Brooooooklyn and others added 3 commits October 29, 2026 17:59
The linker built the useClass factory as the text `{useClass}.ɵfac(...)`.
A conditional useClass then read `flag ? A : B.ɵfac(...)`, and an optional
chain read `Maybe?.cls.ɵfac(...)`, pulling .ɵfac into the chain.

Angular's linker builds this as an AST, so TS's parenthesizeLeftSideOfAccess
wraps any receiver that is not a plain left-hand side or is an optional
chain: `(flag ? A : B).ɵfac(...)`, `(Maybe?.cls).ɵfac(...)`. The other
string-built templates already wrap their operands (`new (X)(...)`,
`(f)(...)`).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`useFactory: make, deps: []` was read as having no deps, so full output
wrapped it as `() => make()` and partial output dropped `deps: []`.

ngtsc only checks whether the `deps` key is present, as for useClass: full
output builds the delegated conditional factory, and partial output keeps
`useFactory: make, deps: []`, which the linkers turn into the same
conditional factory. Track a missing `deps` key as `None` for useFactory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In partial mode a @service class got a full ɵɵdefineService and a plain
ɵfac function, so a library build shipped already-linked code.

ngtsc's ServiceDecoratorHandler.compilePartial emits
ɵɵngDeclareFactory({..., deps: [], target: ɵɵFactoryTarget.Service}) and
ɵɵngDeclareService({minVersion: "22.0.0", ..., type, autoProvided?: false,
factory?}), keeping the user factory as written. Emit the same; the linker
already links ɵɵngDeclareService.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brooooooklyn and others added 16 commits October 29, 2026 17:59
The HMR fingerprint only looked at top-level classes, so the inline
template and styles of a component declared in a function stayed in it.
An edit to them changed the fingerprint and the vite plugin reloaded the
page, though the transform compiles that component with HMR (as ngtsc
does) and an update would do.

Walk every class the transform compiles, nested ones too, and leave out
a name two classes declare, which compiles without HMR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
setClassDebugInfo's lineNumber came from the class span start. A class
with no `export` (and every nested class) has its decorators in that
span, so the line was the decorator's: 3 for `@Component(...)\nclass Foo`
on lines 3-4.

ngtsc takes the line of `clazz.name.pos`, the end of the `class`
keyword. Find that keyword after the last decorator, past modifiers and
comments, and take its line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A decorated class in a `namespace` body was left as is: the class walk
skipped module blocks.

ngtsc compiles it like a class nested in a function (definitions on the
class, constants and child templates at the file top), but only adds
statements after a class in a block or the file, so a namespace member
gets no class metadata, debug info, NgModule scope or HMR code. It is in
the .d.ts, inside its namespace: its declaration is named `NS.App`, so
the injection finds that class and not a same-named one elsewhere. As it
gets no HMR code, it does not make a top-level class of its name
ambiguous for HMR, and its template stays in the HMR fingerprint. A
`declare` namespace or module is still left alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The linker emitted one statement per query creation call
(ɵɵviewQuery(_c0, 5); ɵɵviewQuery(_c1, 5);) and one ɵɵqueryAdvance() per
signal query. Angular's linker compiles the queries with
createViewQueriesFunction / createContentQueriesFunction, which chain the
calls of one instruction (ɵɵviewQuery(_c0, 5)(_c1, 5)) and collapse
consecutive advances into one (ɵɵqueryAdvance(3)). Do the same, with the
file transform's version gate for the chaining: runtimes before v21.0.4
return nothing from the query instructions.

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

The linker always compiled templates in Full mode, so a standalone
component with no directive dependencies got ɵɵelementStart and
ɵɵproperty where Angular 22's linker emits ɵɵdomElementStart and
ɵɵdomProperty. The version was not the cause: an explicit 22.2.2 gave the
same output. Angular's linker sets hasDirectiveDependencies to
!isStandalone, a directive, component or NgModule dependency, or a @defer
block with a dependency function, and compileComponentFromMetadata picks
DomOnly without them. Do the same, for Angular 20.1 and later (and an
unset version), like the file transform.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The linker compiled templates without a context file path and with
external ids, so its Closure message names were MSG_APP_0 (or
MSG_EXTERNAL_<id>$$APP_1 for a custom id). Angular's linker sets
relativeContextFilePath to the linked file's path and i18nUseExternalIds
to false, which gives MSG__X_LIB_A_COMPONENT_MJS_0 for
/x/lib/a.component.mjs. Pass the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The linker compiled each partial declaration with its own constant pool
and kept the constants in an IIFE per declaration, so the _cN, i18n_N and
MSG_ numbers restarted for each declaration of a file and equal constants
(query predicates, pure functions, track functions, long styles) were
declared again.

Angular's FileLinker gives the declarations whose ngImport is bound in
the same module, function declaration or function expression one
EmitScope with one ConstantPool, whose statements the babel plugin puts
once at the top of that scope: before the first statement that is not an
import, or at the start of the function body. Any other binding (an arrow
function, a method, a global) gets a LocalEmitScope, which keeps the
constants in an IIFE. The main template function goes inline in the
definition. A long style is inline on its first use and shared from its
second; a first use in an earlier declaration stays inline, since that
declaration has been emitted already.

Do the same: find the binding of ngImport with oxc_semantic, carry the
pool of its scope from one declaration to the next, insert the scope's
statements once, inline the template function, pool the styles after
the template, and name the host binding function with the pool, as
Angular's addNamesToView does (A_HostBindings1 for a second class A).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since the linker shares one constant pool per scope, a @defer dependency
function went to the top of the scope binding ngImport. When it used a
binding of a scope inside it, such as a class declared in the function
that declares the component, it threw a ReferenceError there.

Angular 22.2.3's linker moves it to the top all the same, so its output
breaks too. Keep such a declaration in a local IIFE with its own pool, as
Angular's LocalEmitScope does and as the linker did before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without `resolveImportedValues`, a `@defer` block that uses a component
declared in the same file got no dependency function, and `ɵɵdefer` had
no resolver argument. ngtsc's full compilation emits one per block
(`const A_Defer_1_DepsFn = () => [Dep]`) and drops a class used only
inside `@defer` from `dependencies`. The selector of a class of the file
needs no other file, so it is now read without cross-file analysis too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With `imports: IMPORTS` and `const IMPORTS = [Dep, Dep2]` in the same
file, no `@defer` block got its own dependency function. ngtsc resolves
the `const` and lists the classes each block uses in a per-block
`DepsFn`, in full and partial mode. It reads each class through the
identifier in the `const`'s array, so components naming the same array
share one function. Like its partial evaluator, the array is read
through `let`, `as const` and a variable naming another one. The classes
stay eager, since only entries of an `imports` array literal can be
lazy-loaded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A `@defer` block's dependency function left out the classes that a
nested array (`[OtherB]`), a spread of a same-file `const` array or a
namespace member (`ns.ChildB`) brings in. ngtsc flattens and resolves
`imports` into its scope, so the block's `DepsFn` lists them, eagerly
(`[ChildA, OtherB]`), and lists a class two entries name once: for
`[ChildA, ns.ChildA, ns.ChildB]` it gives `[ChildA, i1.ChildB]`. The
namespace member is emitted as written (`ns.ChildB`) rather than through
a new import.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The partial declaration's `dependencies` was built only from the plain
identifiers of an `imports` array literal, so a nested array
(`[OtherB]`), a namespace member (`ns.ChildB`), a spread of a same-file
`const` array, a `forwardRef(() => X)` entry and a non-literal
`imports: IMPORTS` were dropped, with or without `@defer`. ngtsc
flattens and resolves `imports` before it lists `dependencies`. They are
now listed once per class: `Cmp` and `ns.Cmp` are one class when they
read one export of one module, or when cross-file analysis finds one
declaration, and a class `@defer` lazy-loads is left out however else
`imports` names it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A standalone pipe in `imports` used only inside `@defer` stayed eager,
in full and partial mode: `@defer` matched the template against
selectors only. ngtsc's binder records the pipes each block uses, so the
pipe is listed in the block's `DepsFn` as `import("./p").then(m => m.P)`,
its static import is removed and the class metadata is set
asynchronously. Pipes are now matched by their name, listed after the
directives, and a `when` condition counts toward the enclosing template.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Template i18n messages only had a custom @@id, so with i18nUseExternalIds a
message without one was named MSG_<suffix>N. ngtsc gives every message an id
in its first I18nMetaVisitor pass (_setMessageId: the custom id, else
decimalDigest of the message before whitespace removal; a sole ICU takes its
element's id) and names it MSG_EXTERNAL_<id>$$<suffix>N, ICU sub-messages
and i18n-* attributes included. The $localize meta block keeps only custom
ids, as in ngtsc. The linker now compiles with i18nUseExternalIds false, as
Angular's linker does, so its names keep the MSG_<suffix>N form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
All arrow functions of a view were hoisted before any create operation
was reified, so an arrowFn of a binding was pooled before the _forTrack
of a @for in the same view. ngtsc's reify hoists each create operation's
expressions just before reifying it (pooling _forTrack with its
repeaterCreate), and the update operations' arrow functions after all
create operations, which declares _forTrack0 before arrowFn0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each class's constants and child view functions were declared just before
that class (only files with a nested class put them after the imports), so
a file's pool was split among its classes, and a long style that a later
class shared was declared with the earlier class's part. ngtsc declares all
of its constant pool's statements after the imports, before the first
class, in pool order, and pools a shared style while the later class
compiles. Every class's declarations now go there, joined into one edit in
compile order, with a shared style declared with the class that shared it.
When a statement comes before the last import (ngtsc moves the imports up),
the declarations go before that statement. The TDZ hoisting of top-level
bindings (#287) is unchanged.

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. Hooray!

Reviewed commit: 8880a1034f

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

Brooooooklyn and others added 2 commits October 29, 2026 18:36
When a statement comes before the last import, the pool goes before that
statement. For `@Other() export class Plain {}` the statement's span starts
at `export`, so the pool landed between the kept decorator and its class:
`@Other() const _forTrack0 = ...`, a syntax error for esbuild and oxc.
Start at the class's first decorator when it comes first.

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

`A.C` dropped the `A` of `namespace A.B` and then looked for `C` like a
bare name, which also searches nested namespaces, so it matched `A.B.C`:
that class got both sets of members (duplicate identifiers) and `A.C` none.
Look a dotted name up segment by segment, the last naming a class declared
directly in the innermost namespace, and try each namespace of a name.

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: Something went wrong. Try again later by commenting “@codex review”.

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

@Brooooooklyn

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Nice work!

Reviewed commit: 9191f1d601

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

@Brooooooklyn
Brooooooklyn merged commit 9f5cca1 into main Oct 30, 2026
10 checks passed
@Brooooooklyn
Brooooooklyn deleted the fix/older-ngtsc-gaps branch October 30, 2026 05:49
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