Repository navigation
fix(constant-pool): share long strings and literals across a file like ngtsc - #660
Conversation
…e ngtsc
Port Angular's `ConstantPool.getConstLiteral` string rule
(packages/compiler/src/constant_pool.ts: `getConstLiteral`,
`FixupExpression`, `POOL_INCLUSION_LENGTH_THRESHOLD_FOR_STRINGS`,
`isLongStringLiteral`). A string of 50+ UTF-16 units stays inline on its
first use; its second use makes one shared `const`, and the first use is
patched to read it. Component styles now go through it, as in
`compileComponentFromMetadata` (render3/view/compiler.ts:
`constantPool.getConstLiteral(literal(style))`).
ngtsc compiles a file with one pool. Oxc keeps one pool per class, so the
file transform hands the pooled literals from one component's pool to the
next. Two components with the same long style now share `_c0`, and two
`@ViewChild('x')` queries share one `["x"]`. When a later component shares
a style an earlier one has inline, the earlier ɵcmp is re-emitted with the
constant and the constant is declared before that class. A component that
fails to compile hands no literals on, like it leaves the `_c` index.
Also stop counting component-specific names (`Cmp_Template`, ...) in the
`_c` index handed to the next class, which shifted its constants. Only
the shared `_forTrack` and `arrowFn` functions are numbered across the
file (ngtsc's `uniqueName` keeps one count per name), so a class named
`_A` no longer shifts the next class's `_c` or `Template` names.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: aa10c40a5d
ℹ️ 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".
…don't collide A literal's pool key joined its strings unquoted, so `["a", "b,str:c,str:d"]` and `["a", "b", "c", "d"]` had the same key. With literals now shared across the classes of a file, the second component read the first one's `attrs` constant. Strings are now quoted and escaped in the key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@codex review |
|
Codex Review: Didn't find any major issues. Already looking forward to the next diff. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
# Conflicts: # crates/oxc_angular_compiler/src/pipeline/constant_pool.rs
… the file #657 gave each directive its own query pool, which started empty and was never handed on, so `["x"]` was declared again for every directive and for the components after it. The directive pool now takes the file's pooled literals and passes them on, like a component's, as ngtsc has one pool per file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fec390efbe
ℹ️ 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".
…lass `$A` and `_A` (or `A_` and `A$`) sanitize to the same prefix, so two classes of a file could both declare `_A_Conditional_0_Template` at the top level, a SyntaxError. ngtsc's one pool per file claims names across classes and numbers the second one `_A_Conditional_0_Template1`. The pooled literals handed from class to class (now `SharedPool`) carry the class-specific names each pool claimed, so the next pool numbers its own the same way. `_forTrack` and `arrowFn` keep the file offset. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@codex review |
|
Codex Review: Didn't find any major issues. Keep them coming! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
ngtsc compiles a file with one ConstantPool. Three gaps remained: - `_forTrack`/`arrowFn` were numbered from the file's `_c` index. Angular's `uniqueName` counts each base name on its own and `freshName` counts `_c`, so the offset is gone and every claimed name is handed on. - Equal shared functions (`getSharedFunctionReference`: track, arrow and `@defer` deps fns) and pure functions (`getSharedConstant`) were declared again by each class. The shared pool now carries them, so a later class reuses the const the earlier class declared. A deps fn reads an imported class through the identifier in the component's own `imports` (a `WrappedNodeExpr` from `LocalIdentifierStrategy`), and `isEquivalent` compares nodes, so such a function is shared within one component only. A wrapped node is now compared by name and span and emitted as written. - A component compiled its template before its host bindings. Angular's `compileComponentFromMetadata` runs `baseDirectiveFields` (queries, then `createHostBindingsFunction`) first, then the selector attrs and the template, so host constants take the lower `_c` indices and are declared first. The HMR update now carries the host constants the template reads. The linker does the same: host bindings first, with one pool, so the template reuses their pure and arrow functions. Ported from @angular/compiler 22.2.2 `ConstantPool` (constant_pool.ts), `WrappedNodeExpr.isEquivalent` (output_ast.ts) and `compileComponentFromMetadata` / `baseDirectiveFields` (render3/view/compiler.ts), and compiler-cli 22.2.3 `LocalIdentifierStrategy` / `Reference.getIdentityIn` (ngtsc/imports). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An import used only inside `@defer` blocks by two components of the same file stayed eager: a class was deferrable only when its `imports` entry was its single value reference, so a second component listing it kept the static import. Port ngtsc's file-wide rule instead. `DeferredSymbolTracker` counts each specifier's references and `markAsDeferrableCandidate` removes the `imports` entries of components that use the class only inside `@defer` (`registerDeferrableCandidate` in `resolveDeferBlocks`); `canDefer` allows a declaration when no specifier has references left. Another component's marks are found the first time a decision needs them, by parsing its template. A class two entries name is listed once (`seenDeps` in `resolveAllDeferredDependencies`). Angular sources: compiler-cli ngtsc/imports/src/deferred_symbol_tracker.ts and ngtsc/annotations/component/src/handler.ts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A block-body arrow factory the converter keeps as raw source (e.g. one
that declares a `const`) was called bare inside the `@Service` /
`@Injectable` factory wrapper: `return () => {...}();` and
`factory:() =>() => {...}()`, a SyntaxError. An async arrow kept its
syntax but called its own body instead of the factory.
ngtsc builds `meta.useFactory.callFn([])` over a WrappedNodeExpr, and
`ts.factory.createCallExpression` runs the callee through
`parenthesizeLeftSideOfAccess`, which wraps anything that is not a
left-hand-side expression. Port that rule for `RawSource` callees in the
emitter: arrows (and other non-LHS source) get parens; names, member
access, function and class expressions stay bare, like ngtsc.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ngtsc 22 has no `attrs` on ɵcmp: angular/angular@d99ab0e040 ("fix(compiler): stop generating unused field") removed it from compileComponentFromMetadata in v22.0.0-next.4, and no runtime reads it. We still pooled the first selector's attributes, which also took a `_cN` index and shifted every later constant. Pool and emit it only when angularVersion is below 22.0.0-next.4 (19.x to 21.x still emit it), and pool it after the query predicates, as v21.2's compileComponentFromMetadata does after baseDirectiveFields. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With an import every component of the file defers now loaded lazily, two classes whose blocks load the same thing build the same DepsFn, and the file-wide pool hands the second one the first's name. ngtsc 22.2.3 emits the same. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v21.2's compileComponentFromMetadata pools attrs after baseDirectiveFields, which compiles the host bindings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a9aecad3a2
ℹ️ 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".
Two bindings like [k, 'a","b'] and [k, 'a', 'b'] built the same key, so the second reused the first's pure function and got a two-entry array. With the pool now shared across classes this also reached from one component to the next. Quote strings with escaping, as expression_to_key already does. ngtsc's GenericKeyFn doesn't escape either and emits the same wrong sharing; this only changes output for such strings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@codex review |
|
Codex Review: Didn't find any major issues. You're on a roll. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
* fix(emitter): parenthesize a call or optional chain used as a new callee
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>
* fix(injectable): parenthesize an optional-chain factory before calling 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>
* fix(injectable): keep an empty deps array on a useClass provider
`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>
* fix(queries): keep empty parts of a string query predicate
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>
* fix(linker): pool string query predicates as constants
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>
* fix(linker): emit the selector attrs field before Angular v22
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>
* fix(i18n): count message names across the file with the pool
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>
* fix(emit): declare pool values and shared functions in pooled order
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>
* fix(compile-template): compile host bindings before the template
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>
* fix(partial): lazy-load @defer dependencies in partial declarations
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>
* fix(defer): lazy-load an imports entry next to a non-class entry
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>
* fix(transform): compile decorated classes nested in functions and blocks
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>
* fix(transform): hoist constants a nested class shares with an earlier 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>
* fix(defer): count nested components' deferrable imports across the file
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>
* fix(hmr): compile same-named nested components without HMR
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>
* fix(transform): keep a nested class calling a shadowing initializer API
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>
* fix(linker): parenthesize a non-plain useClass before .ɵfac
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>
* fix(injectable): keep an empty deps array on a useFactory provider
`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>
* fix(service): emit partial declarations for @service
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>
* fix(hmr): read nested components' template and styles in the fingerprint
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>
* fix(debug-info): take the line of the class keyword, not a decorator
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>
* fix(transform): compile decorated classes in a namespace
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>
* fix(linker): chain consecutive query creation calls
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>
* fix(linker): compile standalone templates without directive deps in DomOnly 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>
* fix(linker): name i18n closure messages after the linked file
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>
* fix(linker): share one constant pool per scope like Angular's linker
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>
* fix(linker): keep defer deps that use inner bindings local
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>
* fix(defer): give same-file @defer dependencies their own resolver
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>
* fix(defer): read @defer dependencies through a const imports array
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>
* fix(defer): list classes of non-identifier imports entries in deps fns
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>
* fix(partial): list classes of every imports entry in dependencies
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>
* fix(defer): lazy-load a pipe used only inside @defer
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>
* fix(i18n): name closure messages after their computed message id
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>
* fix(reify): pool a template's track function before its arrow functions
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>
* fix(transform): declare every class's pool after the imports
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>
* fix(transform): put the pool before the decorators a class keeps
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>
* fix(dts): match a dotted class name only through its whole namespace 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>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Summary
Part of #623 (the shared inline styles row, plus shared query predicates across classes).
ngtsc compiles a whole file with one constant pool. Oxc has one pool per class. This PR ports Angular's
ConstantPool.getConstLiteralstring rule (packages/compiler/src/constant_pool.ts:getConstLiteral,FixupExpression,POOL_INCLUSION_LENGTH_THRESHOLD_FOR_STRINGS,isLongStringLiteral). It also passes the pooled literals from one class's pool to the next.const, and the first use is changed to read that const.compileComponentFromMetadata(constantPool.getConstLiteral(literal(style)))._c0. Two@ViewChild('x')queries share one["x"], including the query predicates of a@Directive(pooled since fix(compiler): align five small emit shapes with ngtsc (#623) #657)._forTrackandarrowFnnames are numbered across the file. Names tied to one class (Cmp_Template,_A_Conditional_0_Template, ...) no longer push up the next class's_cindex, even when the class name starts with_. The names each class claims are handed on too, so a later class whose name sanitizes the same ($Aafter_A,A$afterA_) gets_A_Conditional_0_Template1, as in ngtsc, instead of a second declaration with the same name._cindex.Before / after
Two components, same long style:
Class
_A(with@if), then classB(with@ViewChild('x')), both using the same long style:Class
Bfails (an i18n meaning that contains*/), then classChas two queries:Older bugs fixed here (found while reviewing the merge)
These were on
mainbefore this PR. Each one is checked against ngtsc 22.2.3.8bcb1e0).@Injectable/@ServiceuseFactory: () => { ... }was emitted as() => {...}(). The emitter now wraps a raw arrow in parentheses when it is called.3d8c45d). The rule was "the import is used once in the file". It now follows ngc's file-wideDeferredSymbolTracker: the import goes when every use in the file is animportsentry of a component whose@deferblocks use it.4cdce8e):baseDirectiveFieldsruns first), also in HMR updates and the linker;_forTrackand lazy DepsFns are declared once and reused by later classes;_forTrackhas its own count instead of starting at the file's_cindex.6bb4adb): strings are escaped, so[k, 'a","b']and[k, 'a', 'b']no longer share one function. (ngtsc'sGenericKeyFnhas the same collision; output only changes for such strings.)attrsfield (1a577e6). Angular 22 dropped it (angular/angular@d99ab0e040). It is now emitted only whenangularVersionis below 22.0.0-next.4, pooled after the host consts as in v21.2.ngtsc parity checked (compiler-cli 22.2.3)
@ViewChildpredicate across classes._Afollowed byB, and a class named_Afollowed by_B:_cnames and Template names now match.Checks
cargo test --all-features: 3674 passed, 0 failedbuild-test+ vitest: 401/401 passednode --checkclean🤖 Generated with Claude Code