Repository navigation
fix(styles): treat newlines/tabs as CSS descendant combinators in encapsulation - #175
Merged
Brooooooklyn merged 1 commit intoMar 27, 2026
Conversation
…apsulation split_by_combinators() only recognized ' ' (space) as a whitespace descendant combinator, missing '\n', '\t', and '\r'. When a comment placeholder (%COMMENT%) was followed by a newline, the placeholder and selector merged into one part, causing scope_simple_selector to bail out and skip scoping the first rule entirely. This surfaced when SCSS @import pulled in files with block comments — Sass hoists the comment before the first rule, and the newline after it triggered the bug. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>}} {{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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Issue where if a style had a comment before it, the style was not encapsulated.
split_by_combinators()only recognized' '(space) as a whitespace descendant combinator, missing\n,\t, and\r%COMMENT%) was followed by a newline, the placeholder and selector merged into one part, causingscope_simple_selectorto bail out and skip scoping the first rule@importpulled in files with block comments — Sass hoists the comment before the first rule, and the newline after it triggered the bugTest plan
:hostwith newline combinator