Skip to content

chore(deps): update dependency vite to v8.0.0 - #125

Merged
renovate[bot] merged 1 commit into
mainfrom
renovate/vite-8.x
Mar 15, 2026
Merged

renovate[bot] merged 1 commit into
mainfrom
renovate/vite-8.x

Conversation

@renovate

@renovate renovate Bot commented Mar 15, 2026

Copy link
Copy Markdown
Contributor

This PR contains the following updates:

Package Change Age Confidence
vite (source) 8.0.0-beta.18 → 8.0.0 age confidence

Release Notes

vitejs/vite (vite)

v8.0.0

Compare Source

Features
Bug Fixes

Configuration

📅 Schedule: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 Automerge: Enabled.

♻ Rebasing: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@renovate
renovate Bot merged commit 06c6573 into main Mar 15, 2026
2 checks passed
@renovate
renovate Bot deleted the renovate/vite-8.x branch March 15, 2026 05:33
Brooooooklyn pushed a commit that referenced this pull request Sep 23, 2026
…ith Angular's own tests (#491)

* test(i18n): add failing tests for i18n attribute and ICU messages

Expected output is taken from Angular 22.1.5 ngc.

- i18n-<attr> messages (plain, interpolated, and alongside an i18n
  element body) currently lose their text: goog.getMsg("") and
  $localize`:@@id:`.
- An element whose only child is an ICU currently emits an extra empty
  sub-message referenced as {"icu": "�i18n_N�"}, splits the
  ICU's inner {{count}} into a $localize substitution, and emits no
  i18nPostprocess call, so the runtime fails to parse the ICU.
- An ICU next to other content currently gets an empty sub-message
  instead of the ICU text.

* fix(i18n): use the attribute value as the text of i18n-<attr> messages

Messages for i18n-<attr> were built from the i18n-<attr> metadata alone
(meaning, description, @@id), so their text was always empty:
goog.getMsg("") and $localize`:@@close:`. At runtime the attribute
rendered as an empty string in the source locale.

Angular's I18nMetaVisitor builds these messages with
_generateI18nMessage([attr], meta) whenever the attribute has a value.
Do the same through I18nMessageFactory, and keep the message's text and
interpolation placeholder nodes. Ingest now reads interpolation
placeholder names from those nodes, matching Angular's
Object.keys(message.placeholders), so an interpolated attribute gets
its INTERPOLATION param instead of "".

* test(i18n): add failing test for tag placeholders inside an ICU

Angular 22.1.5 maps each tag placeholder inside an ICU (START_BOLD_TEXT
and so on) back to its markup through ɵɵi18nPostprocess.

* fix(i18n): emit ICU messages and their post-processing like Angular

ICU messages were built as an empty, id-less message wrapping a single
ICU placeholder, and ingest never linked an ICU to its message. As a
result every ICU became an empty sub-message (i18n_N = $localize``),
referenced from the parent as the string "�i18n_N�"; the ICU's
inner {{count}} was split into a $localize substitution with value "";
and no ɵɵi18nPostprocess call was emitted. At runtime @angular/core
threw "Unable to parse ICU expression".

This ports Angular's behaviour:

- I18nMetaVisitor builds an ICU's message from [expansion], and an ICU
  that is the only child of an i18n element reuses that element's
  message (currentMessage || meta). ingestIcu passes icu.i18n to the
  IcuStart op, so create_i18n_contexts treats such an ICU as the message
  itself rather than a sub-message.
- Placeholders inside an ICU serialize as literal {NAME} ICU text
  (IcuSerializerVisitor), not {$name} / $localize substitutions.
- r3 ICU placeholders are keyed by the message's placeholder names
  (INTERPOLATION, START_BOLD_TEXT, ...) and include element markup, as
  in r3_template_transform's visitExpansion.
- Each message gets its context's post-processing params plus its ICU
  placeholder literals (extractI18nMessages), with formatIcuPlaceholder
  joining strings and values without a ${} wrapper.
- collectMessage emits `i18n_N = ɵɵi18nPostprocess(i18n_N, {...})` after
  the Closure/$localize branch, and passes a single ICU sub-message by
  its variable ({ "icu": i18n_0 }), as addSubMessageParams does.

* test(i18n): add failing test for two ICUs in one message

Angular 22.1.5 names the placeholders ICU and ICU_1 and passes each its
own sub-message. oxc names both ICU, so the second ICU renders as "".

* fix(i18n): give each ICU in a message its own placeholder name

Every ICU placeholder was hardcoded to "ICU". With two ICUs in one
message, both sub-messages mapped to the same placeholder, so the root
message passed "�I18N_EXP_ICU�" for ICU and "" for ICU_1, and the
second ICU rendered as an empty string.

Angular names ICU placeholders with getPlaceholderName('ICU', ...),
giving ICU, ICU_1, ... when it builds the enclosing message. Take the
names from that message, which the factory already builds, matched by
each ICU's start offset.

* test(i18n): add failing tests from Angular's i18n compliance scenarios

Found by compiling Angular 22.1.5's r3_view_compiler_i18n compliance
cases with ngc and oxc and comparing messages and rendered output.
Expected values are ngc's output.

- $localize placeholder names lose their underscores (STARTBOLDTEXT
  instead of START_BOLD_TEXT), which changes the message id.
- i18n-<attr> on an empty attribute creates an empty message.
- Custom placeholder names (// i18n(ph="PH")) are not used for the
  param, so the interpolation renders as "".
- Whitespace next to an ICU is dropped from the message.
- Elements with structural directives get duplicated tag values and a
  spurious i18nPostprocess.
- @if/@else inside i18n gets duplicated and missing block values.

* fix(i18n): keep original placeholder names in $localize messages

Messages were stored with camelCase placeholders ({$startBoldText}) for
goog.getMsg, and $localize recovered the name by upper-casing it, which
drops the underscores: :STARTBOLDTEXT: instead of :START_BOLD_TEXT:.
Placeholder names are part of the id $localize computes, so every
message containing an element, block or custom placeholder got a
different id from the one ng extract-i18n produces, and its translation
was never applied.

Angular serializes the message separately for each target:
LocalizeSerializerVisitor uses formatI18nPlaceholderName(name, false)
and GetMsgSerializerVisitor the camelCase form. Store the original
names ({$START_BOLD_TEXT}), use them as-is for $localize and param
lookup, and convert to camelCase only for goog.getMsg.

* fix(i18n): only create i18n-<attr> messages for attributes with a value

i18n-title on title="" produced an empty translatable message. Angular's
I18nMetaVisitor only attaches a message to a plain attribute of that
name when it has a value (`meta !== undefined && attr.value`), so an
empty attribute, or a bound [title], stays a regular attribute/binding.

* fix(i18n): honour custom placeholder names for interpolations

`{{ name // i18n(ph="PH") }}` produced a message with {$ph} but a param
named INTERPOLATION, so goog.getMsg got {"interpolation": ...} and
$localize substituted "" for PH. The placeholder names used for the
binding came from a separate code path that always used INTERPOLATION.

Use the custom name there too, as Angular's i18n parser does with
extractPlaceholderName.

* fix(i18n): keep whitespace-only text in i18n messages

The i18n message factory dropped whitespace-only text nodes, so
`{count, plural, ...} <b>x</b>` became "{$icu}{$startBoldText}..." and
whitespace in preserveWhitespaces templates was lost from messages.

Angular's _I18nVisitor.visitText keeps every text node; retainEmptyTokens
only concerns zero-length tokens in interpolated text. Whitespace that
should not be translated is removed earlier by the WhitespaceVisitor,
which keeps whitespace next to ICUs.

* fix(i18n): resolve element and block placeholders once per view, in order

resolve_i18n_element_placeholders walked the view tree from the root and
then processed every child view again on its own. Placeholders in child
views were recorded twice, the second time without their structural
directive, giving values like "[�*2:1��#1:1�|�#1:1�]" and a
spurious i18nPostprocess. @else/@else if branches (ConditionalBranch
ops) were not handled, so their block placeholders had no value, and
child views were resolved after their parent's ops rather than at their
position.

Port Angular's resolvePlaceholdersForView: only walk from the root,
handle ConditionalCreate and ConditionalBranchCreate like templates, and
record template start, recurse, then record template close in op order.
recordTemplateClose also uses the enclosing block's subTemplateIndex,
not null.

* fix(i18n): take block placeholder names from the i18n message

Block placeholders were named from a counter shared by every block, so
the @else in `@if (a) {...} @else {...}` became START_BLOCK_ELSE_1 in the
params while the message said START_BLOCK_ELSE, and its value was lost.

Angular names block placeholders with the message's placeholder
registry, which numbers per name and reuses a name for an identical
block. Take the names from the message the factory already builds,
matched by each block's start offset, as is done for ICUs.

* test(i18n): add failing tests for the remaining compliance divergences

Second batch from comparing oxc with Angular 22.1.5 ngc on the
r3_view_compiler_i18n compliance cases (expected values are ngc's):

- colons in meaning/description are not escaped in the $localize raw
  metadata block, so the text renders the metadata
- ICUs outside an i18n block get no message and render nothing
- nested ICU VAR placeholders are numbered parent first
- interpolated attributes on elements inside an ICU are not mapped
- spaces around ICU keywords leave VAR_SELECT unresolved (runtime error)
- void elements add an empty {$} placeholder
- nested tag placeholders are numbered parent first
- <ng-content> tag placeholders get no value
- custom placeholder names with spaces (ph = "who") are ignored
- interpolations lose their value in an i18n element with a structural
  directive, or next to an element with an interpolated i18n attribute
- elements inheriting a namespace are not named with it

* fix(i18n): serialize void elements as a single placeholder

<img> inside an i18n block produced "{$TAG_IMG}{$}": the message
serializer always wrote a close placeholder, empty for void elements,
which added an unnamed placeholder with no value. Angular's
serializers write only the start placeholder when isVoid.

* fix(i18n): accept whitespace in custom placeholder name comments

`{{ name // i18n(ph = "who") }}` was named INTERPOLATION because the
custom name was only recognised as the exact text i18n(ph=. Port
Angular's _CUSTOM_PH_EXP, which allows whitespace between the comment,
i18n, (, ph, = and the quoted name.

* fix(i18n): escape $localize metadata blocks in the raw strings

$localize finds the end of a message's metadata block by the first `:`
not escaped in the template's raw string. oxc emitted identical cooked
and raw strings with no escapes, so a meaning or description containing
`:` ended the block early: i18n="meaning:A|descA@@ida" rendered
"A|descA@@ida:Content A" and lost its custom id. A message starting with
`:` got a backslash in its cooked text instead.

Port Angular's createCookedRawString: cooked strings are unescaped, and
raw strings escape backslashes, colons inside the metadata block, a
leading colon, backticks and `${`. LocalizedStringExpr carries the raw
parts for the emitter.

* fix(i18n): name nested tag placeholders children first

In `<span title="x">a<span>inner</span></span>` the outer span was
START_TAG_SPAN and the inner START_TAG_SPAN_1; Angular's
_I18nVisitor._visitElementLike visits children before naming the
element, so the inner span is START_TAG_SPAN and the outer
START_TAG_SPAN_1. The message id and the param mapping both depend on
this.

Visit children first in the message factory, and take the tag names
used for element placeholders from that message (matched by the
element's start offset) instead of a separate registry that could
disagree with it.

* fix(i18n): take nested ICU VAR names from the ICU's message

For nested ICUs the message (built by the factory, which follows
Angular) numbers inner ICUs first: `{VAR_SELECT_2, select, male
{{VAR_SELECT, ...}} female {{VAR_SELECT_1, ...}} ...}`. The r3 ICU vars
were named separately, parent first, so VAR_SELECT_2 was mapped to the
first nested ICU's expression and the outer ICU switched on the wrong
value at runtime.

Name each ICU var from the ICU's message placeholders, matched by the
switch expression's start offset.

* fix(i18n): trim ICU var names when spaces surround the ICU type

`{count, select , ...}` has the type "select ", so the message's VAR
placeholder is "VAR_SELECT ". oxc used that name for the ICU's var, and
the post-processing key became VAR_SELECT_, which left the ICU's
{VAR_SELECT , ...} unresolved: "Unable to parse ICU expression" at
runtime. Angular's r3 visitExpansion trims VAR keys (key.trim()).

* fix(i18n): bind interpolations in element markup inside ICUs

In `{x, select, other {<span title="{{a}}-{{a}}">foo</span>}}` the
START_TAG_SPAN post-processing value was the literal markup, so the
rendered title was "{a}-{a}". Angular's r3 visitExpansion passes each
non-VAR placeholder through _visitTextWithInterpolation, making the
markup bound text whose expressions become placeholders:
<span title="�1�-�2�">.

* fix(i18n): give <ng-content> inside i18n blocks its placeholder values

<ng-content> in an i18n block appeared in the message as
START_TAG_NG_CONTENT/CLOSE_TAG_NG_CONTENT, but the r3 Content node had
no i18n metadata and ingest never set the projection's placeholder, so
both params were "" and the projected content was dropped from the
translated message. Angular's ingestContent passes content.i18n (a tag
placeholder) to createProjectionOp.

* fix(i18n): keep placeholder names for text that is only an interpolation

A text node inside an i18n block that is only an interpolation, like
`<b>{{ name }}</b>` or `{{ name }}<h1 ...>`, got a bare placeholder as
its i18n metadata instead of a container. Ingest reads interpolation
placeholder names from a container only, so the expression got no
placeholder, INTERPOLATION had no value, and the text rendered empty.

Angular's _visitTextWithInterpolation returns a Container whenever the
text has an interpolation; do the same.

* fix(i18n): compile ICUs outside i18n blocks

`<div>{gender, select, male {...} other {...}}</div>` without an i18n
attribute rendered nothing: the ICU was dropped when not inside an i18n
element. Angular's I18nMetaVisitor gives every expansion a message, and
the wrapI18nIcus phase wraps an ICU that is not in an i18n block in its
own i18nStart/i18nEnd, which oxc already implements.

* test(i18n): add failing test for SVG element placeholder names

Angular 22.1.5 names SVG elements in an i18n message with their
namespace (START_TAG__SVG_SVG, START_TAG__SVG_CIRCLE), which is part of
the message id. oxc uses START_TAG_SVG.

* fix(i18n): name element placeholders with their namespace

Angular's HTML parser gives elements their full namespaced name (an
<svg> is :svg:svg, a <circle> inside it :svg:circle, a <span> inside
<xhtml:div> :xhtml:span), and i18n placeholder names derive from it:
START_TAG__SVG_SVG. oxc's HTML AST keeps the raw name, so any SVG icon
inside an i18n message was START_TAG_SVG, giving the message a
different id from ng extract-i18n.

Port _getElementFullName (explicit prefix, else the tag's implicit
namespace, else the parent's unless it prevents inheritance) into the
i18n message factory, tracking the parent element's full name, and
pass the i18n root's full name from html_to_r3.

* test(i18n): add failing test for whitespace around a root-level ICU

Angular 22.1.5 compiles a template that is only an ICU surrounded by
whitespace to a single i18n instruction; oxc keeps the whitespace as
text nodes.

* fix(i18n): drop whitespace around ICUs at the template root

A template that is only `{age, select, ...}` with surrounding newlines
compiled to text(" "), i18n, text("\n"), so the rendered ICU had
extra whitespace. Angular's parseTemplate runs the whitespace visitor
over root nodes with visitAll rather than visitAllWithSiblings, so
root-level text has no sibling context and whitespace next to a
root-level ICU is removed (it is kept inside elements).

* test(i18n): add failing test for &ngsp; in i18n messages

Angular 22.1.5 keeps &ngsp; as U+E500 in the message text, which is
part of the message id; oxc replaces it with a space.

* fix(i18n): build text messages from tokens like Angular

`a&ngsp;b` in an i18n block became the message "a b"; Angular's is
"a\uE500b", so the message ids differed. Angular's _I18nVisitor builds
text with more than one token from the tokens, where whitespace
processing has only rewritten plain text tokens and an entity token
keeps its decoded value. Do the same, and return a Container for any
text with an interpolation, as _visitTextWithInterpolation does.

* refactor(i18n): address clippy lints in the i18n fixes

* docs: note that legacy i18n message IDs are not supported

oxc emits no legacy message IDs (enableI18nLegacyMessageIdFormat) and
has no i18nNormalizeLineEndingsInICUs option, so translations keyed
only by legacy IDs are not applied. Remove the i18nNormalizeLineEndingsInIcus
option from the root README, which does not exist.

* fix(compare): compare static definitions emitted as class fields

compareFullFileSemantically only compared `ClassName.ɵcmp = ...`
assignments, but both Angular and Oxc now emit `static ɵcmp = ...`
class fields, so no ɵcmp/ɵfac/ɵdir/ɵpipe/ɵmod was compared in
full-file mode. The template function, consts (where every i18n
message lives), decls and vars were never checked: with the compiler
from before the recent i18n fixes, which emitted goog.getMsg("") where
Angular emits goog.getMsg("Close"), all i18n fixtures still matched.
With this change 20 of the 28 fail against that compiler.

- Extract `static ɵ*` class fields from the AST.
- Compare field values by AST, ignoring differences that do not
  change behaviour: formatting and string escapes, arrow vs function
  for consts, $localize tagged template vs __makeTemplateObject call
  (cooked and raw strings are still compared), pure: true in
  ɵɵdefinePipe, and Closure-only i18n metadata Oxc does not emit
  (goog.getMsg variable names, original_code).
- Add fixtures/known-differences.ts for the 40 fixtures that this
  surfaces with real, non-i18n differences (query chaining, factory
  form, NgModule imports, host attribute order, style shimming, ...).
  Listed fixtures report as known differences; a listed fixture that
  matches fails so the entry gets removed.

* test(i18n): add failing tests found by the compare tool's i18n fixtures

Expected values are Angular 22.1.5 ngc output (legacy message ids off,
as in the compare tool):

- ICU placeholders in $localize carry their sub-message's id
  (:ICU@@911278603808503436:); oxc writes :ICU:.
- i18n text expressions are applied after advancing to the i18n
  block's last slot; oxc skips the advance() when an element with i18n
  attributes comes after the text.
- an interpolated i18n attribute on an explicit <ng-template> uses the
  bindings marker (3); oxc uses the i18n marker (6).

* fix(i18n): use the bindings marker for i18n attributes on ng-template

An interpolated i18n attribute on an explicit <ng-template> was
extracted with the i18n attribute marker (6). Angular's attribute
extraction uses the i18n marker only when the binding's templateKind is
null, i.e. not on a template, so it emits the bindings marker (3).
oxc's PropertyOp has no templateKind; treat bindings that target a
Template op as having one.

* fix: advance to i18nAttributes and conditional branch slots

generate_advance only mapped some slot-consuming ops to their slots.
Update ops targeting an i18nAttributes op (such as i18n text
expressions, which assign_i18n_slot_dependencies moves to the i18n
block's last slot consumer) or a conditional branch got no advance(),
so they ran with the previous element selected. Angular's
generateAdvance maps every op with the consumes-slot trait.

* fix(i18n): name ICU sub-messages on their $localize placeholders

When a message has no legacy ids, Angular's serializeI18nTemplatePart
writes the id of an ICU placeholder's sub-message into the placeholder
metadata: `${i18n_0}:ICU@@911278603808503436:`. oxc emits no legacy ids,
which is Angular's enableI18nLegacyMessageIdFormat: false output, but
wrote plain `:ICU:`.

Compute each ICU sub-message's id as Angular does,
computeMsgId(message.messageString, meaning), with a port of
serializeMessage (the messageString serializer, which differs from the
code-generation strings), and carry it through the message metadata to
the $localize placeholder. Message metadata is now built by one
I18nMessageMetadata::from_message instead of six copies.

The three tests that asserted `:ICU:` took their expected values from
ngc with legacy ids on; they now use ngc's legacy-off output, matching
oxc's legacy-less messages.

* fix(compare): keep whitespace in static fields for the semantic check

Class static fields were whitespace-collapsed before the AST
comparison, which also collapsed newlines inside Angular's multi-line
$localize template literals, so messages with newlines looked
different from Oxc's (which escapes them). Compare the raw field text:
the AST comparison already ignores formatting.

Known differences: remove edge-cases/unicode-special-escapes, which
was an artifact of the collapsing, and add the fixtures whose
remaining differences are selector whitespace in shimmed styles and
the pipe factory form (used by the i18n fixtures added next).

* test(compare): add i18n fixtures from Angular's own i18n tests

Generated by the new `pnpm generate:i18n-fixtures [angular-dir]`
(defaults to the Angular submodule) from Angular 22.1.5:

- every r3_view_compiler_i18n compliance case (99 input files), one
  fixture file per directory
- every template in the runtime i18n acceptance spec (129)

plus 34 hand-written scenarios checked while fixing oxc's i18n output
(entities, &ngsp;, @let, @switch, *ngFor, listeners, pipes in ICUs,
namespaces, custom placeholder names, ...).

NgModule declarations are made standalone so that fixtures compare
i18n output rather than NgModule scoping, which Oxc does not resolve.
Skipped, with reasons: external-template cases, three acceptance
templates that are intentionally invalid or need a spec-local
directive, and ngNonBindable inside i18n (Angular's compiler crashes).

Against the compiler from before the recent i18n fixes, 211 of the 262
active i18n fixtures fail; all pass now.

* docs(compare): document known differences and the i18n fixture generator

* refactor(i18n): address clippy lints in the i18n fixes

* refactor(i18n): tidy the placeholder-name plumbing

- Remove serialize_i18n_nodes and its helpers: the i18n message string
  now comes from the message factory, leaving them unused.
- Group the placeholder names taken from a message into
  MessagePlaceholderNames (icus/blocks/tags) and IcuPlaceholderNames
  (vars/interpolations) instead of five separate maps on the
  transformer.
- Merge create_message_in back into create_message with the
  parent_element parameter, rather than keeping two entry points.

* test(i18n): add failing test for literal placeholder-like text

Angular keeps `{$notAPlaceholder}` in an i18n message as text; oxc
treats it as a placeholder, dropping the text from $localize and
case-folding it in goog.getMsg.

* fix(i18n): keep text that looks like a placeholder marker

`&#123;$notAPlaceholder&#125;` in an i18n message was treated as a
placeholder: $localize replaced it with an empty substitution and
goog.getMsg case-folded it, so the text was lost and the message id
was wrong. The message string cannot tell `{$NAME}` markers from text
that looks like one.

Escape `{$` and `{\` in message text when serializing the message
string, and unescape when generating goog.getMsg and $localize. The
serializer used for message ids is Angular's and stays unescaped.

* fix(i18n): clear the sole-ICU message after visiting an element's children

The i18n message an element shares with its only ICU child is consumed
by that child. Clear it afterwards so it cannot be picked up by a later
ICU if the child is ever not visited.

* refactor(compare): type the static field comparison like the rest of the file

Use NormAstNode and unknown instead of any in the new AST comparison
and class field extraction.

* test(compare): regenerate i18n fixtures from the Angular submodule

The fixtures were generated from a 22.1.5 checkout; regenerate from the
repository's submodule (22.0.0) so that `pnpm generate:i18n-fixtures`
with no arguments reproduces them. Only the version header differs: the
i18n test cases are identical in both.

* perf(i18n): avoid allocating a full element name per element

element_full_name allocated a String for every element in the template,
including the common case of an un-namespaced tag in a non-i18n template
where the name is returned unchanged. Returning a Cow borrows the name
unless a namespace prefix has to be prepended.

* test(compare): scope known differences to the fields they document

A fixture listed in known-differences.ts had every difference forgiven, so
a new regression in one of those 51 fixtures was reported as the known
difference instead of a failure. Each entry now records the static fields
that differ, and a difference in any other field of the same fixture fails
as usual.

* test(compare): stop recording an i18n fixture flag nothing reads

The generator copied angularCompilerOptions.i18nUseExternalIds from
Angular's test cases into the fixtures, but neither compiler wrapper
passes it on, so both sides compile with external IDs either way. Drop
it rather than imply coverage of local message IDs that does not exist.

* docs: correct the option names in the README examples

TransformOptions has crossFileElision and i18NUseExternalIds (N-API
capitalises the N), not enableCrossFileElision or i18nUseExternalIds, and
has no useDomOnlyMode. The plugin options block listed an interface name
and options the plugin does not have, and omitted ssrEntry,
angularVersion and templateTransform.

* test(compare): refresh the known differences for Angular 22.1.7

The recorded differences were captured against Angular 21.2.6. Fourteen
of them now match and are removed; the change-detection and incremental
hydration differences that 22.1.7 exposes are recorded with the fields
they affect.

* test(compare): drop a known-difference reason with no remaining users

* chore: drop the handoff note that was committed by mistake
Brooooooklyn added a commit that referenced this pull request Oct 22, 2026
…ith ngtsc (#598)

* perf(vite): skip the resource URL parse when no URL is spelled

transform() called extractComponentUrls for every file with an Angular
decorator, a second full Rust parse of the file. Most files (services,
pipes, inline-template components) never spell `templateUrl` or
`styleUrl`, so they are now compiled with empty resources directly.

The compiler still reads keys the way the parser does, so a component can
name a resource through an escaped key (`styleUrls`) or a computed
key folded from a same-file constant. On that path the compiler reports
COMPONENT_RESOURCE_NOT_FOUND, so any error on the fast path reruns the
file through resolveResources. Without URLs resolveResources returned the
same empty resources and touched no state, so output is unchanged.

JIT mode now skips resolveResources entirely: the Rust JIT transform
never reads resources (it emits `angular:jit:*` imports that jitPlugin
loads) and registers no components for HMR, so the reads and the owner
bookkeeping were unused.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(vite): pass preserveWhitespaces to the HMR template compile

The @ng/component endpoint compiled the new template without the
decorator's `preserveWhitespaces`, so compileForHmrSync always stripped
whitespace. For a component with `preserveWhitespaces: true` the update
module then built a different view than the full transform (missing
padding and inter-element text nodes), and text shifted after every HMR.

The endpoint already has the class's extracted metadata; pass its
`preserveWhitespaces` like `encapsulation`. The other template options
compileForHmrSync reads (i18nUseExternalIds, advancedOptimizations,
legacyOptionalChaining) are not set by the full transform either, so both
use the same defaults.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* perf(vite): preprocess an edited stylesheet once per HMR cycle

One stylesheet edit went through preprocessCSS up to three times:
refreshStyleDeps in handleHotUpdate, the @ng/component endpoint, and the
next transform of the invalidated component.

Add one compileStyle() helper that all three use. It keeps the output
per stylesheet, tagged with the source text it was built from, so a
file that changed again in between is rebuilt (the endpoint still reads
from disk every time). Entries only live for one HMR cycle:
handleHotUpdate clears the map on entry, and a generation counter keeps
a compile from an earlier cycle from storing into the new one. The
output can depend on files outside the reported deps (Tailwind scans
templates), so a later template edit still rebuilds the styles the
endpoint serves, as before. Builds keep no entries; resourceCache
already serves their repeat reads.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore(vite): remove the unused styles plugin

`@oxc-angular/vite-styles` only transformed module ids carrying an
`ngcomp` query, and nothing creates such ids: neither the plugin, the
binding, nor the compiler emit `?ngcomp=`. HMR styles are encapsulated
inside compileForHmrSync. Drop the plugin and its encapsulateStyle
import; the binding export itself stays.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(i18n): port parseI18nMeta 1:1 so i18n metadata is trimmed once

Angular's `parseI18nMeta` trims the whole `i18n` / `i18n-*` value once and
then splits it on `@@` and `|` without trimming the parts. We trimmed the
meaning and description separately, so `i18n="  m | d  "` gave meaning `m`
and description `d` where ngtsc gives `m ` and ` d`. These strings are
emitted in `$localize` (`:m | d:`) and JSDoc and feed the message id, so
the output differed from ngtsc.

Add `parse_i18n_meta`, a direct port: trim with `String.prototype.trim`
semantics, find `|` in the whole value (so a `|` after `@@` leaves the
description empty, as in Angular), and use it both for the message factory
input and for the r3 i18n metadata, replacing a second ad-hoc parser.

Pinned by unit tests, an integration test with ngtsc 22.2.2 output, and the
`i18n-meta-untrimmed-parts` compare fixture.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(template): always run the interpolation parser, as Angular does

`has_interpolation` pre-scanned text and attribute values and only ran the
interpolation parser when the first `{{` came before the first `}}`. Angular's
BindingParser always runs `parseInterpolation` and keeps the value static when
it finds no expression, so values such as `title="}} {{a}}"` or
`<p>&#125;&#125; {{a}}</p>` stayed static here but are bound by ngtsc
(`ɵɵinterpolate1("}} ", ctx.a)`).

Remove the pre-scan for text nodes, attribute values and ICU placeholders.
Attribute parsing is split from building the bound attribute so the
`i18n-*` metadata is only taken when the value really binds. The i18n
container of bound text is now built only for text that binds.

Also report Angular's `splitInterpolation` error "Blank expressions are not
allowed in interpolated strings at column N in [input]" for `{{ }}`, in both
the plain and the entity-token paths; ngtsc reports it at this point too.

Pinned by unit tests and three `interpolation-after-closing-braces-*`
compare fixtures checked against ngtsc 22.2.2.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* perf(template): share the i18n source file and stop copying parser input

- `HtmlToR3Transform` built a new `ParseSourceFile` (a full copy of the
  template text) for every i18n element, ICU and translated attribute.
  Build it once per template on first use and share the `Arc`, which also
  shares its cached line table. Templates without i18n never build it.
- `Parser::new` copied every expression twice (`strip_comments` returned a
  `String`, then it was copied into the arena). The stripped source is a
  prefix of the input, so borrow the slice instead.
- `Parser::parse_interpolation` now returns early when the text has no
  `{{`. It runs on every text node and attribute value now that the
  pre-scan is gone, and this avoids copying static text into the arena.

No output change: cargo tests and the compare fixtures are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* perf(transform): compile the HMR template copy only when HMR is on

compile_component_full compiled every component template a second time
through compile_template_to_js to fill `template_js`, but only the HMR
`templateUpdates` map reads that string. Make it an Option and run the
second compile only when `options.hmr` is set. With HMR on, the
templateUpdates keys and values are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(factory): give classes with no constructor and no superclass empty deps

A @Pipe, @NgModule or @Injectable class without a constructor was always
compiled to the inherited-factory form
(`ɵɵgetInheritedFactory(X)(t || X)`), even when it extends nothing.
ngtsc's getConstructorDependencies only returns null when the class has a
base class; otherwise it treats the class as having an implicit no-arg
constructor, so the factory is `new (t || X)()`.

The three extract_constructor_deps helpers now return empty deps when
the class has no `extends` clause, matching the directive and component
paths, which already did this.

Removes the now-passing FACTORY entries from the e2e known differences
and adds fixtures for a pipe and an injectable that extend a base class
without a constructor (inherited factory on both sides).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* perf(transform): build one oxc Semantic per file in the AOT transform

transform_angular_file built three oxc Semantic models of the same
parsed program: one inside ImportElisionAnalyzer::analyze, one inside
the initializer-API validation, and one for the TDZ hoist. The program
is never mutated after parsing (all changes are text edits applied to
the source), so one model built right after the parse serves all three.

- ImportElisionAnalyzer::analyze_with_semantic and
  validation::initializer_api_usage_errors_with_semantic take the
  caller's model; the old entry points stay as wrappers that build
  their own (used by the JIT path and unit tests).
- The validation reads the AST node store, so the shared model is
  built with nodes only when that rule will run
  (initializer_api_usage_needs_semantic, same check as before).
- The hoist keeps its cheap decorated-class gate but no longer builds
  a model of its own.

Output is unchanged: cargo snapshots and e2e compare fixtures match.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(transform): pin HMR templateUpdates content

With HMR on, each component's templateUpdates entry must equal the
template compiled on its own by compile_template_to_js; without HMR
the map is empty. Guards the HMR-only second template compile.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(linker): annotate linked define calls with /*@__PURE__*/

Angular's babel linker emits every `ɵɵdefine*` call it builds
(defineInjectable, defineService, defineInjector, defineNgModule,
definePipe, defineDirective, defineComponent) with a leading
`/* @__PURE__ */` comment, because the compiler marks those invocations
pure. The OXC linker left them bare, so bundlers could not drop unused
linked classes from library code.

Add the annotation to each define call, in the `/*@__PURE__*/ ` form
the OXC emitter already uses. Factories and `ɵsetClassMetadata` stay
unannotated, and in the NgModule `registerNgModuleType` IIFE only the
inner define call is pure, matching the real linker output
(@angular/compiler-cli 22.2.3 linker/babel).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(output): emit $localize as a native tagged template

ngtsc emits a localized message as a native tagged template for ES2015+
targets (and Angular's own AbstractEmitterVisitor does the same):

  $localize `:meaning\:x|desc@@id:Hello ${"�0�"}:INTERPOLATION:`

Oxc printed the ES5 downlevel form instead, a call through an inline
__makeTemplateObject polyfill with the cooked and raw strings as array
literals. Vite never targets ES5 here, so emit the tagged template and
drop the polyfill.

Only the raw strings are printed; the engine derives the cooked strings
from them. The raw strings already carry Angular's createCookedRawString
escapes (backslashes doubled, backticks and `${` escaped, colons escaped
in the metadata block), and they are printed verbatim like the
TypeScript printer prints rawText, line terminators included. A stored
lone surrogate, which UTF-8 output cannot hold, is printed as its
\uXXXX escape so the cooked string still holds that code unit.

The i18n integration tests now assert the native form, and a compare
fixture pins backticks, `${`, backslashes, colons and line terminators
against ngtsc.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(component): emit consts with initializers as an arrow function

When a template's consts need prepare statements (the i18n Closure /
$localize branches), Angular's compiler wraps them in
`o.arrowFn([], [...statements, return consts])`, which ngtsc prints as

  consts: () => { let i18n_0; ...; return [...]; }

Oxc emitted an anonymous `function () { ... }`. Build an arrow function
with a block body instead, in the component definition and in the HMR
and linker template outputs, which build the same consts expression.
Without initializers consts stays a plain array, as in Angular.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(injectable): use expression-body arrow factories like ngtsc

ngtsc's compileInjectable builds two factory wrappers with o.arrowFn and
an expression body:

- useFactory without deps:   factory: () => makeIt()
- useClass without deps:     factory: __ngFactoryType__ => Other.ɵfac(__ngFactoryType__)

Oxc emitted `function () { return makeIt(); }` for the first. For
useClass it emitted `Other.ɵfac` directly, or, for a forwardRef,
`function (t) { return i0.resolveForwardRef(Other).ɵfac(t); }`.

ngtsc delegates directly (`Type.ɵfac`) only when the useClass is the
same node as the decorated class, which never happens for a written
useClass, and full compilation passes resolveForwardRefs: false, so a
forwardRef(() => Other) is unwrapped and gets the same arrow as a plain
useClass: Other. The arrow also reads the class lazily, so a useClass
declared later in the file no longer hits the TDZ when ɵprov is built.

The compare fixture object-members-injectable-use-factory now matches
ngtsc's ɵprov; its known difference keeps only the unrelated ɵfac entry.
New fixtures pin useFactory, useClass and useClass with forwardRef.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(compare): make the ngtsc fixture comparison honest

The fixture compare reported 100% while hiding real differences:

- compareFullFileSemantically rewrote the whole source text before
  comparing (replaceAll(',}'), (',]'), (' }'), regex-based @Inject,
  nullish, numeric and object-literal rewrites) and accepted static
  fields whose whitespace-squashed text matched, so whitespace changes
  inside CSS and template strings counted as matches.
- extractClasses skipped exported classes, so class members (for example
  signal fields) were never compared; top-level constants were never
  compared either, and their name mapping stripped whitespace inside
  strings.
- A fixture listed in known-differences.ts only failed on undocumented
  static fields, imports and metadata calls; class, function, export
  and parse-error differences were ignored, and class-metadata fixtures
  dropped most diff kinds before the check.
- Factory fixtures compared Oxc's output with itself and passed.

Now every part is compared on the oxc-parser AST (formatting, quotes,
trailing commas, parentheses and plain template literals do not matter;
string contents do), Oxc's _cN constants are renamed to Angular's by AST
value, exported classes and functions are unwrapped, classes are compared
without their static ɵ fields (compared on their own), and top-level
constants are compared. The ɵcmp input/output sub-diffs parsed with
regexes are dropped; the ɵcmp diff itself is still reported. Unused
compareJsSemantically and normalize-template-literals.ts are removed.

The runner checks every reported difference of a listed fixture against
its entry (new classes/functions/exports lists) and fails on any listed
difference that no longer occurs. Factory fixtures are reported as
skipped (or oxc-error).

Fixtures can set ngtscMode: 'full' to compile the reference in full
mode instead of experimental-local. The change-detection and
custom-decorator fixtures use it: local mode keeps changeDetection
symbolic and leaves a class with a non-Angular decorator uncompiled,
which a real build does not. In full mode their changeDetection,
ɵfac and setClassMetadata match.

Newly exposed differences are listed in known-differences.ts with tags:
CSS_WS (29 fixtures), DEBUG_NAME (43). The full-mode fixtures now carry
DOM_ONLY (14) and COMPONENT_DEPS (3) instead of the local-mode
CHANGE_DETECTION_V22 / CUSTOM_DECORATOR reasons.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(linker): parse host styleAttribute like the template pipeline

The linker split `host.styleAttribute` on every `;` and the first `:`,
so a value such as `background: url('a;b')` was cut inside the quotes
and produced broken `hostAttrs`. Values were also wrapped in quotes
without escaping, so a `"` inside a value broke the emitted string.

Angular's linker feeds the attribute through the template pipeline's
`parse_extracted_styles` parser, which ignores `;` and `:` inside quotes
and parentheses and hyphenates property names. Reuse the existing port
of that parser (`parse_style_string`, now reachable from the linker)
and escape keys and values with the emitter's string escaper. The test
pins Angular 22.2.3's babel linker output for the same declaration.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(defer): port Angular's OnTriggerParser for @defer triggers

Replace the find/rfind/split(',') string scanning of `on`, `prefetch on`
and `hydrate on` triggers with a port of Angular's OnTriggerParser
(r3_deferred_triggers.ts), built on the expression lexer's tokens.

What changes:
- Trigger lists are split on top-level comma tokens only; commas in
  strings, arrays, object literals and calls stay inside the argument.
- Stray tokens are reported like Angular ("Unexpected token", "Unexpected
  end of expression"), e.g. `on timer(1s) idle`, `on hover(a) foo`.
- Error messages are Angular's verbatim (`Duplicate "idle" trigger is not
  allowed`, `Could not parse time value of trigger "timer"`, `Unrecognized
  trigger`, the viewport option errors incl. the dynamic node class name,
  ...) and carry Angular's spans instead of the whole block span.
- Trigger source/name spans follow Angular.
- `when` conditions and `viewport` options now report expression parse
  errors, like Angular's BindingParser.
- `viewport({trigger: this.a})` is rejected like in Angular.
- Keyword patterns (`^on\s`, `^when\s`, ...) account for the trailing
  whitespace the HTML lexer trims, so `@defer (on)` / `@defer (when)` are
  "Unrecognized trigger" while `@defer (on )` stays accepted.

`@defer (when )` keeps adding no trigger: Angular builds an empty `when`
trigger there that ngtsc then fails to compile.

tests/fixtures/defer_triggers_ngtsc.json snapshots @angular/compiler
22.2.2 parseTemplate results (errors with spans, triggers) for 99
templates, including the Angular spec's trigger error cases.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(security): sanitize static and i18n attributes like ngtsc

Static attributes were ingested with SecurityContext::None, so a constant
like `<iframe srcdoc="...">` or `<script src="...">` was emitted as a plain
string. ngtsc wraps these in ɵɵtrustConstantHtml / ɵɵtrustConstantResourceUrl.

- ingest: compute the security context of static attributes from the
  element tag (with its :svg:/:math: prefix), ng-template, or ng-content,
  as Angular's ingest does.
- const_collection: when an extracted attribute has a trusted value fn,
  emit it as a tagged template (i0.ɵɵtrustConstantX`...`).
- resolve_sanitizers: do not set trusted value fns on host attributes;
  upstream skips this for host jobs.
- Port resolveI18nAttrSanitizers. It wraps translated attribute values
  (e.g. `<a href="..." i18n-href>`) in the matching sanitizer or in
  ɵɵvalidateAttribute. It runs right after collectI18nConsts, as upstream
  does. It is limited to the Angular versions that have upstream commit
  61a97f22e8 (19.2.23+, 20.3.22+, 21.2.15+, 22+), the same gate as
  normalizeTagName.

Tests: integration tests check the exact output of ngtsc 22.2.2 for the
security_sensitive_constant_attributes compliance case, templates, host
attributes, i18n attributes, and the version gate. A new e2e compare
fixture file covers the same cases.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* perf(pipeline): skip needless clones and walks in template phases

These changes do not change any output (all compare fixtures still match).

- generate_arrow_functions: record on the job whether any arrow function
  was hoisted, and return early from collect_arrow_functions when none was.
- emit: move view and pool statements into the output instead of deep
  cloning them.
- any_cast: replace `$any(x)` with `x` by moving the argument out instead
  of cloning it.
- resolve_dollar_event: walk each listener once. The same walk rewrites
  `$event` and records whether the listener reads it. This drops the
  separate "contains $event" IR walk.
- i18n_const_collection: set i18n attribute consts in a single pass over
  the views, not one pass per message.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(ng_module): patch the scope on with a JIT-only ɵɵsetNgModuleScope like ngtsc

In full compilation ngtsc uses R3SelectorScopeMode.SideEffect: ɵmod keeps
only `type` (and `bootstrap`), and declarations/imports/exports go to a
tree-shakeable `(function () { (typeof ngJitMode === "undefined" ||
ngJitMode) && ɵɵsetNgModuleScope(M, {...}); })()` after the class. Oxc
inlined the scope into ɵɵdefineNgModule, which keeps every declaration
and import alive in production bundles and differs from ngtsc's ɵmod.

- to_r3_metadata now uses SideEffect.
- The guard is built with the shared `guarded_expression` (real
  `typeof`, `===`) instead of a `typeof ngJitMode` identifier and `==`.
- The IIFE callee is parenthesized; the side-effect path was never
  emitted before, and without the parens it printed `function() {...}();`,
  which does not parse.
- Forward-declared lists use `() => [...]` like ngtsc's refsToArray,
  not `function () { return [...]; }`.
- A list Oxc cannot resolve to class names in one file (a const array,
  a spread, a `forRoot()` call) used to be dropped or cut down to the
  names it could find. It now reaches ɵɵsetNgModuleScope as written,
  like ngtsc's local compilation; the runtime flattens it, resolves
  forward refs and unwraps ModuleWithProviders. Plain lists of class
  names (and forwardRef(() => X)) still emit the resolved array, so the
  output matches ngtsc exactly there. Empty lists are skipped.

Partial compilation is unchanged (it never emits the side effect).

Removes the NG_MODULE_IMPORTS known differences and adds fixtures for
a module whose declarations/imports use const arrays and spreads, and
for a module that extends a base class without a constructor.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(styles): gate the legacy ShadowCss polyfill passes on the Angular version

Angular v22.0.0-rc.0 removed the polyfill-next-selector, polyfill-rule
and polyfill-unscoped-rule rewrites and the ::shadow / ::content
replacement from ShadowCss (22.0.0-next.12 still has them; checked
against the published @angular/compiler packages). The shim always ran
them, so on v22 these blocks were rewritten instead of being scoped like
any other rule.

Angular 19-21 are still supported, so the passes stay and run only when
the configured version predates v22.0.0-rc.0
(AngularVersion::uses_shadow_css_polyfills). None keeps meaning
"latest", like the other version gates.

The version is threaded through:
- shim_css_text_for_version / encapsulate_style_for_version (new);
  shim_css_text / encapsulate_style keep their signatures (latest).
- finalize_component_style takes the version; the component compiler,
  HMR compile and the linker pass theirs.
- NAPI encapsulateStyle gets an optional third angularVersion argument
  (backward compatible) and the vite styles plugin passes its
  angularVersion option.

e2e compare: shadow-css-polyfill-next-selector and
shadow-css-polyfill-unscoped-rule now match ngtsc 22 and leave
known-differences; the polyfill-rule reason is updated (ngtsc 22.2
strips a marker in the rule body as part of CSS nesting support).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(styles): align the CSS shim scanners with Angular v22 shadow_css.ts

Each change was checked against @angular/compiler 22.2.2's
encapsulateStyle with the same input.

- Trim the result before restoring comments, like _scopeCssText. A
  leading newline or space used to ship in every component style.
- Keep the text between a rule's `}` and the next `}`. It was collected
  as a selector and dropped on an at-rule close (`{ ... } }` became
  `{ ... }}`), or carried into the next selector on a stray `}`
  (`@media x { .a {} b } c {}` scoped `b c` as one selector). It is now
  written out unscoped, in place, as Angular leaves it.
- Skip the character after a backslash in the string scanners of the
  :host-context and selector-scoping passes. An escaped quote ended the
  string early and the rest of the stylesheet was read as one string,
  so later rules were left unscoped.
- Rewrite :host-context() at any depth. It only ran at the top level,
  so inside @media/@supports/@layer/... it was left as a literal
  `[_ngcontent-%COMP%]:host-context(.x)`. Selectors are still processed
  one at a time (text since the previous `{`/`}`), so rules are not
  combined with each other.
- Scope only the bodies of Angular's scopedAtRuleIdentifiers (@media,
  @supports, @document, @layer, @container, @scope, @starting-style),
  strip @font-face/@page as before, and copy every other at-rule body
  unchanged. Declaration-only bodies (@property, @counter-style,
  @view-transition, @position-try...) were read as selectors and split
  off their at-rule (`@property --x {} syntax: ...`), and
  @font-feature-values lost its nested block.

e2e compare: new styles fixtures pin each fix; component-with-pipes and
shadow-css-ng-deep-basic now match and leave known-differences.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* perf(cross-file): skip evaluating top-level class and function exports

A top-level `export class` or `export function` always evaluates to a
reference, which `StaticValue` can't hold, so `eval_export` parsed the
whole file again only to answer `None`. That happens for every imported
component class in `imports: [...]` and for each class/function in a
`{ ...ns }` spread.

The export map now marks those entries (`ExportInfo::never_static`) and
`export_value_at` returns `None` for them without evaluating. Only
declarations in the program body are marked: a namespace member shares
its entry with the top-level name, which still gets evaluated. A
top-level variable or enum of the same name (a duplicate TypeScript
rejects, but which the evaluator resolves first) clears the mark, so
the answer is the same as before in every case.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(linker,optimizer): honour the sourcemap option

`OptimizeOptions.sourcemap` was accepted but ignored: `optimize` always
returned `map: None`. The linker had no option at all, and its Vite
plugin returned `map: null` from `transform`, which tells Vite the code
did not move even though linked declarations replace whole calls and
change line counts. Stack traces and devtools into linked library code
pointed at the wrong lines.

- optimizer: when `sourcemap` is set, apply edits with the existing
  `apply_edits_with_sourcemap` helper (already used by the component
  transform) and return its map.
- linker: add `LinkOptions.sourcemap` (Rust and NAPI, default false) and
  do the same.
- vite plugin: add a `sourcemap` option to `angularLinkerPlugin`, forward
  it to the binding from both the transform hook and the dependency
  optimizer load hook (which now returns `{ code, map }` when a map
  exists), and pass `angular({ sourceMap })` through to it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* perf(cross-file): parse each analyzed file once per analyzer

`analyze_file` parsed a file to build its export map, then
`eval_export_uncached` parsed it again for every distinct export it
evaluated, and `standalone_class_selector` once more per selector
lookup, each time also cloning the kept source text.

Files are now parsed once into a `ParsedFile` that owns its arena
(boxed `Allocator` holding the source text), the `Program`, and the
`StringConsts` the evaluator reads, with the import resolver already
attached. `analyze_file` keeps that parse when `values_enabled`; files
it didn't keep are parsed on first use, as the old disk fallback did.
The parsed files replace the `sources` map and are dropped with
`clear_cache`.

The program and consts borrow the arena, so they are stored with a
`'static` lifetime and only lent out through `ParsedFile::with`, whose
closure is generic over the lifetime so nothing borrowed escapes. The
attached resolver holds a `Weak` handle to the analyzer state, so the
cache doesn't form an `Rc` cycle. A fresh `Evaluator` is still built
per export (its fuel and memo stay per evaluation), so the output is
unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* perf(optimizer): build edit source maps in linear time

`generate_sourcemap_from_edits` rescanned the source and the output from
byte 0 for every unchanged segment to find its line and column, and
re-summed the edit shifts from the first edit each time. That is
quadratic in file size, which did not matter for single component files
but does for the multi-megabyte FESM bundles the linker and optimizer
now map.

Segments are visited in ascending source order, so keep a running
prefix sum of edit shifts and a forward-only line/column cursor per
string (it restarts from the start if asked for an earlier offset).
The generated maps are byte-identical.

Linking then optimizing the 200 @angular/* fesm2022 files in
node_modules with sourcemaps on (release build):
  link      0.224s -> 0.077s  (0.055s without maps)
  optimize  0.358s -> 0.078s  (0.038s without maps)
with the same hash over every generated map before and after.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(ng_module): always run ɵɵsetNgModuleScope, without the ngJitMode guard

The previous commit moved the NgModule scope out of ɵɵdefineNgModule into
`(typeof ngJitMode === "undefined" || ngJitMode) && ɵɵsetNgModuleScope(...)`,
like ngtsc. ngtsc can let production builds drop that call because it lists
every component's dependencies statically. Oxc compiles one file at a time
and resolves dependencies at runtime with ɵɵgetComponentDepsFactory, which
reads the scope from ɵmod (and only ɵɵsetNgModuleScope registers the module
with the deps tracker). The Vite plugin defines ngJitMode as false in
production, so with the guard:

- a standalone component that imports an Oxc-compiled NgModule lost that
  module's exported directives and pipes (it had them before, when the
  scope was inline in ɵmod), and
- a list given as a const array or spread never reached the runtime.

The call is now always made. ɵmod keeps only `type` (and `bootstrap`) and
the scope still goes through ɵɵsetNgModuleScope, so raw lists are flattened
by the runtime. As a side effect a component declared in an NgModule now
finds its owner module in production too; before this branch it had no
dependencies in any mode.

Cost: an NgModule that nothing uses is no longer tree-shaken, because the
IIFE is a side effect. The scope was not tree-shakeable before this branch
either (it was inline in ɵmod).

The e2e compare harness only compares the class body, so the guard does
not show up there; the exact output is pinned by the Rust tests, and a new
vitest test runs the compiled code against @angular/core with
ngJitMode = false.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(styles): do not start an at-rule on an @ inside a value or header

An unquoted `@` in the last declaration of a rule (`url(a@2x.png)`,
`url(#m@x)`, `image-set(...)`), common in minified CSS, opened an
at-rule header in scope_selectors. The header swallowed the rule's
closing `}` and ran up to the next `{`; since it was not a scoped
at-rule, the unscoped-body path consumed the `}` and never cleared
in_declaration, so every later rule was copied unscoped and leaked
out of the component. An `@` inside an at-rule header
(`@supports (background: url(a@2x.png))`) also restarted the header,
so the @supports body was left unscoped.

Only start an at-rule when neither a declaration block nor an at-rule
header is open. Nested at-rules inside a rule body are copied
unchanged, as before and as in Angular. Verified against
@angular/compiler 22.2.2 encapsulateStyle.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(compare): compare top-level statements, export targets and BigInt classes

Review follow-up for the honest ngtsc compare:

- Top-level statements that no other part compared (setClassDebugInfo,
  setNgModuleScope, export default expressions, ...) were never looked
  at, so fixtures listed only for their ɵfac/ɵmod hid a missing
  ɵɵsetNgModuleScope call. They are now extracted in order, labelled
  `callee(firstArg)` and compared on the AST as a new statement diff
  kind that KnownDifference can list (`statements`). Their order,
  including the setClassMetadata calls, is compared too: a statement
  Oxc emits ahead of one Angular emits first is reported as
  `order <label>`. A setClassDebugInfo call that differs only in
  filePath is reported as `<label> filePath`.
- ngtsc now compiles component and full-transform fixtures under the
  project root. The `/fixtures/...` path lay outside it, so Angular
  left filePath and lineNumber out of setClassDebugInfo.
- compareExports reported an export pointing at another local binding
  or module as a match; it is now a `different` export diff.
- normalizeAst threw on BigInt literals, turning any class or constant
  with `10n` into an undocumentable parse error.

The setClassDebugInfo filePath (Oxc emits the path it is given, Angular
the project-relative path) and order (before setClassMetadata, Angular
emits it after) differ in 1034 fixtures. They are listed once in
GLOBAL_KNOWN_DIFFERENCES (DEBUG_INFO_PATH, DEBUG_INFO_ORDER); a fixture
with only these differences is reported as a known difference, not a
match. Newly listed per fixture: NG_MODULE_SCOPE on 3 NgModule
fixtures, DEBUG_INFO_LINE on edge-cases/unicode-special-escapes
(lineNumber ignores \r, U+2028 and U+2029 line breaks).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(linker): annotate the declaration IIFE around define calls as pure

When a linked component or directive needs template or host binding
declarations, the linker wraps them and the define call in an IIFE. The
inner define call was annotated, but the IIFE call itself was not, so
bundlers kept unused linked classes alive. Angular's babel linker hoists
those declarations instead and leaves `/* @__PURE__ */ i0.ɵɵdefine*(...)`
as the static field value. The IIFE only declares functions and pooled
constants before returning the pure call, so it is pure too.

The registerNgModuleType IIFE keeps no annotation, as it has a side
effect.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(vite): drive linker source maps from sourceMap.vendor

The linker only processes node_modules files, but angular() passed it
the scripts setting, so `sourceMap: { vendor: false }` (and the default)
still produced maps for every linked package. Follow Angular CLI's
normalizeSourceMaps: vendor maps are on only when the object form sets
`vendor: true`, and only together with scripts.

The sourcemap flag also changes what the dependency optimizer load hook
returns, and Vite keys its deps cache on optimizer plugin names, not on
their options. Add a `+map` suffix to the plugin name so toggling the
option re-links prebundled deps.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(compare): reconcile known differences after merging wave 1

Drop entries that wave 1 fixed (ng-deep and polyfill-next-selector
whitespace, the injectable useFactory wrapper, the leftover factory
entries) and document the unguarded ɵɵsetNgModuleScope call, which Oxc
keeps on purpose because it resolves component dependencies at runtime.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(transform): order class metadata before debug info and module scope

ngtsc's compileResults pushes ɵsetClassMetadata, then ɵsetClassDebugInfo,
then the HMR initializer; Oxc emitted the debug info first. For NgModules,
ngtsc's insertMetadataStatement unshifts ɵsetClassMetadata in front of the
ɵmod statements, so it comes before the ɵɵsetNgModuleScope IIFE; Oxc
appended it after.

ɵsetClassDebugInfo's lineNumber now counts line breaks like TypeScript's
computeLineStarts (\r\n, \r, \n, U+2028, U+2029) instead of only \n.

Drops the DEBUG_INFO_ORDER global entry, the NG_MODULE_SCOPE_ORDER
entries and the DEBUG_INFO_LINE entry from the compare known differences.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(vite): load every resource the compiler needs, link stylesheets included

What:
- `templateUrl`, `template` and `styleUrl(s)` are now read with the partial
  evaluator (after the old same-file folding), like ngtsc's
  `parseTemplateDeclaration` / `extractStyleUrlsFromExpression`: spreads in
  `styleUrls`, `'a' + B`, a const array, and - with `resolveImportedValues` -
  a const imported from another file.
- New `extract_component_resource_urls` in the crate builds the same consts
  and cross-file resolver as the transform (shared helpers), so the URLs it
  returns are exactly the keys the transform looks up. The NAPI
  `extractComponentUrls(source, filename, options?)` takes the transform's
  `TransformOptions` and uses it.
- `TransformResult.missingLinkUrls` lists template
  `<link rel="stylesheet" href>`s that were not in the resolved styles.
  The plugin reads and preprocesses them through the same code path as
  `styleUrls` (direct styles for cache, watcher and HMR), transforms once
  more, and only hrefs that still cannot be read stay
  COMPONENT_RESOURCE_NOT_FOUND. A missing link no longer sends a fast-path
  file through the URL parse.
- `compileForHmrSync` returns `templateStyleUrls`; the `@ng/component`
  endpoint reads them like `styleUrls` and compiles again, so HMR updates
  keep link styles.

Why:
The plugin extracted URLs without the cross-file resolver, so an imported
`templateUrl` const was silently skipped, and template stylesheet links were
never loaded, so every such component failed with
COMPONENT_RESOURCE_NOT_FOUND. ngtsc (22.2.3, checked on a real program)
resolves both: links relative to the component file, compiled after the
decorator's `styleUrls`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(defer): emit the idle trigger timeout

`@defer (on idle(100ms))`, `prefetch on idle(...)` and `hydrate on
idle(...)` parsed the timeout but ingest dropped it, so Oxc emitted
`ɵɵdeferOnIdle()` where ngtsc 22 emits `ɵɵdeferOnIdle(100)`. Ingest now
passes R3IdleDeferredTrigger.timeout through the DeferOn op and reify adds
it as the argument, like Angular's ingestDeferTriggers + reify.

The timeout is a v22 feature: before that Angular's createIdleTrigger
rejects any parameter with `"idle" trigger cannot have parameters`, so the
trigger parser now does the same when an older Angular version is set.

Adds the defer/defer-on-idle-timeout compare fixture.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(host): align host bindings and hostDirectives with ngtsc

Host attributes, properties and listeners now follow ngtsc's rules, and
hostDirectives is read and emitted the way Angular does it.

What changed:
- hostAttrs order: HostBindingInput.attributes is an insertion-ordered
  IndexMap (it was an FxHashMap, so the order was hash order). Every
  host kind is also sorted like JS object keys: array-index keys
  ('2', '[10]') first, ascending, then the rest as written.
- A static `class`/`style` in `host:` is added after the other
  attributes only when it is non-empty, as in createHostBindingsFunction.
- One shared host merge (directive/metadata.rs merge_host_bindings) for
  @Directive and @Component: `host:` entries go in first and each
  @HostBinding/@HostListener replaces the entry with the same key where
  it was (ngtsc's extractHostBindings). Before, members were appended,
  so a key bound in both places produced two listeners/bindings.
- hostDirectives is read through the Evaluator (evaluate_host_directives),
  shared by both decorators: constant arrays, spreads, constant
  `directive`/mapping values and @angular/core forwardRef all resolve.
  Mappings follow parseMappingStringArray (keyed by field, last alias
  wins, only the first two `:` parts count).
- One createHostDirectivesFeatureArg port for directives and components:
  an entry is the bare class unless `inputs` or `outputs` is written
  (even empty), and a forward reference wraps the whole array in
  `function () { return [...] }`. Directives used to always emit
  `{ directive }` and wrap each forward ref in `forwardRef()`.
  R3HostDirectiveMetadata and HostDirectiveMetadata gain
  `has_mapping_keys` to carry the "key was written" fact.

Why: these were documented differences against ngtsc (hostAttrs order,
HostDirectivesFeature `{ directive }` shape) plus duplicate host
listeners/bindings when `host:` and member decorators used one key.

Tests: new compare fixtures (host-bindings/host-order-and-merge,
host-directives/host-directives-evaluated) checked against ngtsc 22.2,
more attributes in host-spread-and-constant, Rust unit and integration
tests. The HOST_ATTRS_ORDER and empty-mappings known-differences
entries are removed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(vite): read only URL fields for resources; register HMR link styles

What:
- `extract_component_resource_urls` no longer runs the whole
  `extract_component_metadata`. A new
  `extract_component_resource_urls_of` finds the `@Component` config and
  evaluates only `templateUrl` / `styleUrl` / `styleUrls`, with the same
  helpers the metadata uses (`component_config`, `read_style_urls`), so the
  URLs stay the transform's keys. The cross-file resolver is lazy, so it
  now reads another file only when a URL names an imported binding.
- `ComponentResourceUrls.value_dependencies` lists the files read for
  that (Rust only), which pins the behaviour in a test.
- The `@ng/component` endpoint registers the template's stylesheet links
  as direct styles of the component (owner maps, watcher, preprocessor
  deps), like the transform's dependency loop.

Why:
- With `resolveImportedValues` (always on in the plugin) an imported
  `host`, `styles`, inputs or ctor token made the extractor read and parse
  that file, then the transform read it again: ~400-500ms per call on a
  large meta.ts, now ~0.1ms.
- A template-only HMR edit that added a `<link rel=stylesheet>` compiled
  the CSS in, but later edits to that CSS sent no component update until
  a full reload. Links a later edit removes stay registered until the
  next transform; an edit to one only sends an extra update.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(styles): port Angular's static style parser 1:1

parse_style_string split a static `style` attribute differently from
Angular's parse() (parse_extracted_styles.ts, shared with Domino's
style_parser.js). It is now a direct port, quirks included:

- declarations with an empty value are kept (`a:;` gives `a` = "")
- a `;` before any `name:` does not start a new property, so `x;a:b`
  gives `x;a` and `a:b;;c:d` gives `;c`
- the escaped-quote check reads the quote itself, so a matching quote
  always closes, even after a backslash
- names and values are trimmed like String.prototype.trim, and hyphenate
  lowercases like toLowerCase

The linker's host styleAttribute uses the same parser, as Angular's host
binding job does.

Adds three bindings/style-attr-* compare fixtures (all three mismatched
before) and unit tests checked against Angular 22.2's parse().

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(partial): report template errors in partial compilation

Partial mode returned the ɵɵngDeclareComponent declaration before the
template was parsed, so a broken template compiled silently and failed
only later in the linker. ngtsc parses the template in every mode: its
errors are reported and compilePartial emits nothing for the class.

The partial branch now runs after the HTML parse and R3 transform, so
their errors are reported the same way as in full mode. The declaration
still carries the template text unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(ng-module): add the module's exports to ɵinj imports

ngtsc adds an NgModule's exports to its injector imports, so the
providers of an exported NgModule are registered even when the module
does not import it. Oxc used only the `imports` expression.

Oxc compiles one file at a time, so it follows ngtsc's local compilation
(ng_module handler `analyze`): the raw `imports` and then the raw
`exports` go into ɵinj imports, an array literal entry by entry and any
other expression as is. A full ngtsc build keeps only exported NgModules
and filters imports by what they resolve to, which needs other files; the
runtime ignores entries that are neither NgModules nor standalone
components. A non-array `imports: SHARED` is now emitted as
`imports: [SHARED]`, as ngtsc does.

Adds the injector/injector-with-exports compare fixture (with the usual
NG_MODULE_SCOPE entry) and a Rust test.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(component): compile DOM-only templates for standalone components without imports

A full ngtsc build compiles the template of a standalone component without
directive dependencies in TemplateCompilationMode.DomOnly (ɵɵdomElementStart,
ɵɵdomListener, ɵɵdomProperty, ɵɵdomTemplate, ...). Oxc always used Full mode,
like ngtsc's local mode, which cannot see what `imports` holds. But a file
alone can prove the scope empty, so Oxc now uses DomOnly when all of these
hold:

- the component is standalone (explicit or the v19+ default);
- `imports` and `deferredImports` are each absent or `[]`, and the decorator
  object has no spread or accessor that could add them (new
  `ComponentMetadata::imports_provably_empty`);
- its own selector (or `ng-component` when it has none) matches nothing in
  its template, `@defer` blocks included: a standalone scope always holds the
  component itself (StandaloneComponentScopeReader), and a selectorless
  node counts as a match;
- not `jit`, and Angular >= 20.1.0-next.3, the first release with DomOnly
  (checked against the published @angular/compiler builds).

Identifiers, spreads, forwardRef, pipe-only or any other imports keep Full
mode. Schemas and hostDirectives do not matter, as in ngtsc. @defer and i18n
need nothing special: reify already mirrors Angular for both. The HMR update
module still compiles in Full mode, which works for every template.

Compare harness: ngtsc's local mode never emits DomOnly, so a fixture without
an explicit `ngtscMode` now takes its reference from a full build when every
component in the file is standalone and has no imports, and from local mode
otherwise. 629 fixtures that broke under local mode match a full build. The
four forwardRef fixtures mix both kinds, so they pin `ngtscMode: 'full'` and
document COMPONENT_DEPS for the importing component. The 12 DOM_ONLY entries
that now match are removed, expectedFeatures that named the old instructions
now name the DOM-only ones, and templates/dom-only.fixture.ts covers bindings,
control flow, @defer, i18n, `imports: []`, hostDirectives and the
self-selector cases (a recursive component keeps Full mode; its missing
self-`dependencies` entry is documented as SELF_DEPENDENCY).

Rust tests that pinned Full mode for standalone components now pin DomOnly,
with new tests for the not-provable cases, own-selector matching and the
version gate. Tests ported from NgModule-based compliance cases compile
their components as `standalone: false`, as upstream does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(styles): port ShadowCss selector scoping to match Angular's whitespace

Oxc collapsed the whitespace inside shimmed selectors, where Angular keeps
what its regexes produce: `_applySelectorScope` writes every combinator part
as `${scopedPart} ${separator} `, so `.dark :host` becomes
`.dark   [_nghost]` and `a > b` becomes `a[c]    > b[c]`. 31 compare
fixtures carried this difference.

The selector layer of encapsulation.rs is now a direct port of
shadow_css.ts instead of ad-hoc string handling:

- `_insertPolyfillHostInCssText`, `_convertColonHost` and
  `_convertColonHostContext` run in Angular's order and emit the
  `-shadowcsshost` markers, matched case-insensitively like the `gim`
  regexes. Host-context permutations come out in Angular's order.
- `_scopeSelector`, `_applySelectorScope`, `_scopeSelectorPart`,
  `_applySimpleSelectorScope` and `SafeSelector` keep the
  `_shouldScopeIndicator` state, the `_selectorSplitRe` list split (so a
  newline after a comma is kept), the separator regex with its
  parenthesis lookahead, and strings escaped as in `escapeInStrings`.
- `_stripScopingSelectors` replaces the old strip helper for `@font-face`
  and `@page` bodies.

Angular 22.2.0-next.2 added CSS nesting support to ShadowCss. Gated on
that version (latest when no version is given): a lone `&` and rules
without a block stay unscoped, and a rule body with no nested rule (or under
a `::ng-deep` selector) has its host marker and deep combinators stripped.
Nested rules are still not scoped.

Two deliberate differences stay: comment placeholders are not scoped as
selector text, and a newline or tab between compound selectors is still a
descendant combinator (#175); Angular leaves the left side unscoped there.

A differential run of 6100 generated and spec inputs against
@angular/compiler 22.2.2, 22.2.0-next.1/next.2 and 22.1.8 now matches
except for those two cases and nested rules. The fixed known-differences
entries (CSS_WS, the selector list newline, the :host-context order and
the 22.2 rule body strip) are removed, and new fixtures and Rust tests pin
the exact output.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* refactor(vite): decide HMR vs reload with a Rust fingerprint, drop the JS lexer

What:
- New `component_hmr_fingerprint` in the crate (NAPI: sync
  `componentHmrFingerprint(source, filename, classNames)`): one oxc parse,
  then a hash of the whole source with the inside of each literal
  `template` / `styles` value of the named components left out. Classes are
  the top-level ones the transform compiles; the decorator is Angular's
  `@Component` found by its `@angular/core` import (same lookup as the
  transform). A source with parse errors leaves nothing out.
- `TransformResult.hmrFingerprint` (only with `hmr: true`) gives the same
  value from the transform's own parse, for exactly the classes it compiled,
  so the transform hook needs no extra call. It replaces
  `componentDecorators`, which only existed so the JS strip could find the
  decorator spelling.
- The vite plugin caches the fingerprint instead of the stripped source and
  compares it in `handleHotUpdate` and in the `@ng/component` "compiled from
  this source" check. `utils/decorator-fields.ts` (769-line JS tokenizer) and
  its test are deleted; the cases are ported to
  `tests/hmr_fingerprint_test.rs`, plus a vitest file against the binding.

Why:
The JS lexer re-tokenized TypeScript by hand and had known holes: regex
literals (a `/` or quote inside one broke the walk, so every edit reloaded),
and two `@Component`s on one class (it paired the class with the second
one, which the compiler does not take, so edits to the compiled template
reloaded while edits to the dead one hot-updated). The AST has neither
problem. The HMR decision is otherwise the same: same fields (`template`,
`styles`), same "single literal only" rule, same set of classes, and
`templateUrl` / `styleUrl(s)` still reload (changing them changes which
files the component owns, which only a fresh transform records).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(ng-module): restore #[test] on test_jit_true_ng_module_skips_aot

The class-metadata ordering tests added in c84b68e were inserted between
the existing #[test] attribute and test_jit_true_ng_module_skips_aot, so
the new test took the attribute and the jit:true NgModule test stopped
running (rustc reported it as dead code). Put the attribute back so the
jit:true NgModule path is covered again.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(napi): keep TransformResult.componentDecorators as a deprecated field

What:
- Restore `component_decorators` on the crate's `TransformResult` and
  `componentDecorators` on the NAPI `TransformResult`, filled at the same
  spot as before (one map insert per component compiled with `hmr: true`),
  next to the new `hmrFingerprint`. The doc comment marks it deprecated
  (`@deprecated` in index.d.ts).
- Restore the Rust test that pins it (`hmr_reports_component_decorators_as_written`)
  and add a vitest check against the binding.

Why:
The previous commit dropped the field when the vite plugin stopped reading
it, but it has been part of the published binding since v0.0.40 (tags
v0.0.40 to v0.0.42). An outside caller reading
`result.componentDecorators[id]` would silently get `undefined`. The task
only asked to add the fingerprint, so keep the old field and leave its
removal to a later, announced release.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(styles): keep host markers together when splitting on a newline

The Oxc-only rule (#175) that treats a newline, tab, CR or form feed as a
descendant combinator could cut a part that Angular scopes as one. A later
`:host` is folded into the first `:host(...)` match as a bare
`-shadowcsshost`, and Angular only turns it into the host attribute while it
shares a part with a `-shadowcsshost-no-combinator` marker. A part whose
only marker is inside a pseudo-function also gets the content attribute in
front. Splitting there leaked the internal marker
(`:host(.h)\n:host` -> `.h[h]   -shadowcsshost`), emitted an empty `:not()`
that makes the browser drop the rule, or scoped the host as content.

The newline split is now skipped inside an Angular part that holds a bare
host marker or a no-combinator marker inside parens, so those parts come
out exactly as @angular/compiler 22.2.2 emits them. The #175 behaviour is
kept everywhere else.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(hmr): compile component updates with the full transform's options

What:
- New `compile_component_hmr_update` in the crate (NAPI:
  `compileComponentHmrUpdate` / `compileComponentHmrUpdateSync`). It takes
  the transform's `TransformOptions` and `ResolvedResources` plus the class
  name, parses the file once, extracts that class's metadata with the same
  consts (imported values included with `resolveImportedValues`), resolves
  the template, `styleUrls` and template `<link>` styles the same way, and
  runs `compile_component_full` for it. The update's `template`, `consts`,
  `decls`, `vars`, `ngContentSelectors` and `styles` are read from the ɵcmp
  that compile builds, so they are the transform's text. Exceptions:
  constants start at `_c0`, `@angular/core` is `i0` (the module binds it),
  and a `@defer` block of a class with `deferredImports` keeps a null
  dependency function (the resolver's dynamic imports would resolve against
  the update module's URL).
- Resources it was not given come back as `missingTemplateUrl` /
  `missingStyleUrls`. Styles are `null` (keep the live ones) when a
  stylesheet is missing and nothing was read, when `styles` cannot be
  evaluated, or when the source's HMR fingerprint differs from the one the
  component was compiled from (`sourceFingerprint`).
- `compile_component_full` gets an `hmr_update` flag (skips the second
  template compile and collects the update fields) and takes the import
  elision analyzer as an `Option`, since only implicit `@defer` needs it.
- The `@ng/component` endpoint calls it instead of
  `extractComponentMetadataSync` + JS style merging + `compileForHmrSync`.
  It builds the options with the same `transformOptionsFor` as `transform`,
  reads the URLs the compiler names (templates through `templateTransform`,
  styles through the cycle-shared `compileStyle`), and remembers per
  component which URLs the last update read, so a repeat edit compiles
  once.
- `compileForHmrSync` / `compileForHmr` stay exported, marked
  `@deprecated` in their docs.

Why:
The endpoint parsed the file again with an extractor that did not resolve
imported values, then compiled the template alone with a handful of options:
no file path (i18n Closure message names fell back to `MSG_APP_*`, template
errors said "line N of the template" instead of `file:line:col`), and
imported `styles` came out unknown so the update dropped them. Two
hmr-hot-update tests pinned that limitation; they now expect the imported
styles the transform compiled.

Tests: Rust `hmr_component_update_test` and vitest
`hmr-component-update.test.ts` check the update's template, consts, styles
and error locations against `transformAngularFile` (whitespace, i18n,
error-location, resources, defer cases); a new hmr-hot-update case checks the
endpoint's invalidate message names `./app.component.html:2:1`. Compare
harness: 0 mismatched.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* perf(vite-plugin): cut repeat binding calls on the error and first-HMR paths

The no-URL fast path reran extractComponentUrls and a second
transformAngularFile for any error beyond the missing template links, so
a template syntax error cost two parses and two compiles. It now reruns
only when there are more COMPONENT_RESOURCE_NOT_FOUND errors than missing
links. Diagnostics have no code field, so the code is matched in the
message, where the compiler always writes "(COMPONENT_RESOURCE_NOT_FOUND)".

The first HMR update of a component called compileComponentHmrUpdate up
to three times (learn templateUrl/styleUrls, then template <link>s),
because hmrResourceUrls was only filled by earlier endpoint calls. In
watch mode the transform now seeds it for each compiled file@Class with
the templates and stylesheets it read (template links included). It
holds a list of template URLs now, since the transform knows the URLs per
file, not per class.

Tests count binding calls for both cases.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(styles): skip backslash escapes outside strings when scoping selectors

A backslash-escaped quote in a selector (`.x\'y\'`, Tailwind's
`.font-\[\'Inter\'\]`) opened a string in `scope_selectors`. Since the
in-string escape handling from b3464bb, that string never closed, so the
rule and every later one were emitted unscoped, and `:host` leaked as
`-shadowcsshost-no-combinator`.

Outside strings, a backslash now copies itself and the next character,
like Angular's `escapeInStrings`/`escapeBlocks`, so `\'`, `\"`, `\{`,
`\}` and `\@` open no string, block or at-rule. An escaped brace still
ends the selector text, as `_ruleRe` reads no brace into a selector
(`.a\{ .b {}` only scopes ` .b`). The other string scanners in the file
already skip escapes through `string_mask`.

`closes_enclosing_paren` scanned to the end of the selector for every
comma and combinator, quadratic in the selector length. It is replaced by
a table built in one right-to-left pass per selector, with the same
results (a unit test checks it against the old scan on every short
string over `(`, `)`, `a`). A 250k-char selector list now scopes in about
0.1s in a debug build instead of 3.6s.

Checked against @angular/compiler 22.2.2: all new test expectations match
its encapsulateStyle, and 36k reviewer fuzz cases plus 40k generated
ones show no case that matched before and differs now.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(styles): end an at-rule header at an escaped brace

Angular's _ruleRe never reads a brace into an at-rule header, so in
'@media x\{ .a{} } .b{}' the header text stays plain and '.a' is scoped
as its own rule. The escape handling from the previous commit kept the
header open until the next real '{', which left '.a' unscoped.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(vite): use forward-slash module ids in the fast-path tests

The helpers passed raw join() paths as the transform id and the
@ng/component id, while hotUpdate normalized ctx.file. On Windows the
keys then differed (C:\ vs C:/), the .ts edit was taken for a plain
module and full-reloaded, and the update module came back empty. Vite
always hands the plugin forward-slash ids, so the helpers now do too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* perf(elision): keep only export maps when analyzing files

analyze_file kept every analyzed file's AST and consts until the analyzer
was dropped, so a wide export * barrel held all of them in memory. Keep
only the export map and parse lazily for files that are evaluated or whose
selector is read.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(styles): keep the body of an unterminated at-rule

An unscoped at-rule (@keyframes, @property, @font-face, @page) lost its
whole body when the stylesheet ended before the closing brace. Flush the
collected body at end of input, as ngtsc does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.

0 participants