feat(angular): support v22.0.0-rc.2 — conformance & emit parity at 100%#333
Open
brandonroberts wants to merge 16 commits into
Open
feat(angular): support v22.0.0-rc.2 — conformance & emit parity at 100%#333brandonroberts wants to merge 16 commits into
brandonroberts wants to merge 16 commits into
Conversation
Angular v22 changed the safe-navigation operator (`?.`) in template expressions to yield `undefined` via native optional chaining, gated by the `legacyOptionalChaining` compiler option. OXC unconditionally emitted the legacy `== null ? null` ternary, so v22+ projects got the wrong runtime value for any `?.` expression. Changes: - Add `legacyOptionalChaining` to `TransformOptions` (NAPI + Rust) and thread it through ingest into the compilation jobs. The effective default is derived from `angularVersion`: legacy for < v22, modern (native `?.`) for >= v22, and legacy when the version is unknown (matches Angular's conservative fallback). - Add an `optional` flag to the resolved IR read/call nodes (`ResolvedPropertyRead`/`ResolvedKeyedRead`/`ResolvedCall`) and pass it through reify so it renders as native `?.` / `?.[]` / `?.()`. - Rewrite `expand_safe_reads` to branch per node: legacy builds the `SafeTernary` (`== null ? null`); modern rewrites each safe access into the equivalent optional resolved read (no temporaries needed). - Support the `$safeNavigationMigration(...)` escape hatch: a wrapped subtree is forced back to legacy null semantics even on a modern target, and the wrapper is stripped. Two deviations from the issue text, both to match the reference compiler (angular/angular@2896c93cc1): - The modern form is native optional chaining (`ctx.user?.name`), not the `== null ? undefined` ternary the issue described. Both yield `undefined` at runtime; native `?.` matches Angular's emitted output. - The magic function shipped in v22 is `$safeNavigationMigration(...)`, not `$null(...)` (the commit message named `$null` but the code renamed it). Partial/linker output keeps legacy semantics for now; threading the facade field through partial emit is deferred (issue required-work #4). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the conformance submodule from v21.2.2 to v22.0.0-rc.2 and regenerate the extracted fixtures + snapshot against the v22 specs. Pass rate is 1235/1264 (97.7%); the 29 new failures are genuine v22 feature/behavior additions not yet implemented in OXC, tracked as follow-up: the `in` expression operator, `@switch` exhaustive `@default never`, `@defer` `idle(timeout)`, `data-*` binding prefixes, `@let` source spans, and several ShadowCss `:host`/comment cases. Fixture hygiene fixed along the way: - store `file_path` repo-relative instead of an absolute home dir - restore the trailing newline on generated fixtures - drop orphaned shadow_css_polyfills_spec.json (spec removed in v22) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Angular v22's parser_spec adds "should throw on invalid in expressions", exercising error recovery around the `in` operator. OXC parsed `in` correctly as a relational operator but mishandled three error cases: - A bare keyword in primary position (`in`, `in foo`) was silently treated as an identifier read. Angular treats only identifier tokens as reads, so a keyword here reports "Unexpected token <kw>". - Running out of input mid-expression (`'foo' in`) produced a silent EmptyExpr. Angular reports "Unexpected end of expression: <input>". - The error formatter now appends `[<source>]` to the end-of-expression message to match Angular's getParseError output. Empty input still parses error-free (parse_chain guards via the token list being empty). Conformance expression_parser: 270/270. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Angular v22 lets the `idle` deferred trigger take an optional timeout parameter (`on idle(100)`), mirroring `timer`. Previously OXC rejected any parameter on `idle`. - Add `timeout: Option<f64>` to `R3IdleDeferredTrigger`. - Parse zero-or-one parameter in the `idle` arm via `parse_deferred_time`, matching the reference error strings for >1 param and unparseable time. - Humanize `['IdleDeferredTrigger', <timeout>]` when present. Conformance r3_transform: 165 -> 168. Emit threading of the timeout into the defer config is deferred (not exercised by these AST-level specs). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Angular v22 removed `data-` prefix normalization: binding syntax
(`bind-`, `on-`, `bindon-`, `ref-`, `let-`) is now matched against the
raw attribute name. So `data-ref-a`, `data-let-a`, `data-on-event`, and
`data-bindon-prop` are plain text attributes, and an interpolated
`data-prop="{{v}}"` keeps its `data-` prefix in the bound attribute name.
Drop `normalize_attribute_name` and the data- strip in
`parse_binding_prefix`, using the raw name throughout.
Conformance r3_transform: 168 -> 173.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Angular v22 ends a `@let` declaration's sourceSpan at the end of the terminating semicolon (`end = endToken.sourceSpan.end`). OXC ended the span just before the `;`, and the conformance humanizer additionally stripped a trailing `;`. Both now keep the semicolon. Conformance html_parser 85->86, r3_transform 173->174. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Angular v22 simplified ShadowCss shimming. Three behavior changes,
bringing the shadow_css conformance subsystem back to 100%:
- Comment removal no longer appends an extra newline; only the newlines
inside the comment are preserved (`/* c */ b {}` -> ` b[contenta] {}`).
- A `:host(...)` whose argument is a selector list (top-level commas) is
left literal and prefixed with the content attribute
(`:host(.a, .b)` -> `[contenta]:host(.a, .b)`), instead of being split.
- A `:host-context` without a non-empty argument list (bare,
`:host-context()`, `:host-context( )`) is no longer a context selector;
it stays literal and is content-scoped. `:host` is no longer mangled
inside `:host-context` (mirrors `/:host(?!\-context)/`).
Drops the now-unused data-/host-attr conversion fallback and updates two
comment integration tests to the v22 output.
Conformance shadow_css: 160 -> 169 (100%).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Angular classifies `<script>`/`<style>` as special (content-extracting) elements by their lowercased element name, which for SVG content is namespaced (`:svg:style`) and therefore not matched. OXC checked the raw name before namespace resolution, so an SVG `<style>` was wrongly stripped. Gate the special handling on the HTML namespace. Conformance r3_transform: 174 -> 175. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Angular v22 added `@default never;` inside `@switch` as an exhaustive
type-narrowing marker. It is lexed as a block named "default never",
terminated by `;` instead of a `{ ... }` body, and lowers to a
`SwitchExhaustiveCheck` node on the switch (no runtime output).
- Lexer: emit BlockOpenEnd + BlockClose for `@default never;` (and
`@default never(expr);`), mirroring the reference `_consumeBlockStart`.
- AST: add `R3SwitchExhaustiveCheck` and `SwitchBlock.exhaustive_check`,
with a visitor hook visited after the case groups.
- Transform: intercept the "default never" block (optional expression,
no body) and attach it as the exhaustive check.
- Humanize `SwitchExhaustiveCheck` after the case groups.
Conformance: html_lexer, html_parser and r3_transform back to 100% —
the full suite is now 1264/1264 (100%) against v22.0.0-rc.2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
All 1,264 extracted assertions pass after implementing the v22 features. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bump the npm @angular/* dependencies to 22.0.0-rc.2 in the packages that validate compiler behaviour against the real Angular compiler — the NAPI package, the e2e app and the output-parity `compare` harness — plus the playground. This keeps the npm side aligned with the conformance submodule (also v22.0.0-rc.2). The pinned real-world benchmark apps (typedb-web, bitwarden) stay on their 21.2.x snapshots; they are perf fixtures, not compat validation, and bumping a full app to a pre-release would risk breaking their builds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Angular v22 broadened `specializeControlProperties` beyond `formField`: `formControl`, `formControlName` and `ngModel` (including two-way `[(ngModel)]`) now also emit the paired `ɵɵcontrolCreate()` (create) and `ɵɵcontrol()` (update) instructions. - ingest: detect the control property across `[..]` property bindings, two-way `[(ngModel)]`, and static `formControlName=`/`ngModel` attributes, and add the `ControlCreateOp` after element creation. - binding_specialization: add the `ControlOp` update after the property update for `formField`/`formControl`/`formControlName`/`ngModel`, and after the two-way property update for `[(ngModel)]`. Brings the e2e compare harness to 100% parity (686/686) against @angular/compiler 22.0.0-rc.2. Snapshots updated for the two-way ngModel fixtures (now emit controlCreate + control). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The compare harness compiled OXC without an `angularVersion`, so OXC fell back to its conservative legacy default (e.g. legacy safe-navigation `?.`) while comparing against a newer @angular/compiler. After the v22 bump this surfaced as safe-navigation mismatches. Thread the installed @angular/compiler VERSION into OXC's TransformOptions so version-gated emit aligns with the baseline it's compared against. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
The extended control-property specialization (ɵɵcontrolCreate/ɵɵcontrol for formControl/formControlName/ngModel, including two-way [(ngModel)]) is a v22 emit change; v21 only did this for [formField]. Emitting the extra instructions against a < v22 runtime would diverge. Gate the extended set behind `AngularVersion::supports_extended_control_ properties()` (major >= 22), keeping `formField` unconditional as the v21 baseline. Per the crate convention an unknown version assumes latest, so the default (and the e2e compare harness, which now targets v22) is unchanged. Threaded through both the ingest control-create detection and the binding-specialization control-update insertion. Adds a version-gate regression test: `[(ngModel)]` emits only ɵɵtwoWayProperty on v19/v20/v21, and additionally ɵɵcontrolCreate/ɵɵcontrol on v22+ and when the version is unknown. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4b6b061 to
32839b2
Compare
`pnpm check` runs `oxfmt --check`, which the regenerated conformance fixtures (serde_json pretty-printed) and the edited compare harness didn't satisfy. Run oxfmt over them; whitespace-only, so the conformance runner parses them identically (still 1264/1264) and the snapshot is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The e2e HMR fixture app and the playground are runtime apps; booting them on Angular v22.0.0-rc.2 needs app/vite-plugin migration work that is out of scope here (the v22 runtime fails to render, timing out every Playwright HMR test). Revert those two to v21.2.x. The compiler-validation surface stays on v22: the `compare` harness still diffs OXC output against @angular/compiler@22.0.0-rc.2 (686/686), and the NAPI package keeps its v22 compiler-cli. Both reverted apps use none of the version-divergent template features, so the (v22-default) compiler emits v21-identical output for them; the local HMR e2e suite passes. Co-Authored-By: Claude Opus 4.8 (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
Bumps the Angular target from v21.2.2 → v22.0.0-rc.2 (conformance submodule + the validation npm deps) and closes every behavioral gap the bump exposed. Both validation suites are green against v22.0.0-rc.2:
@angular/compiler): 686/686 (100%)What changed
Bump
crates/oxc_angular_compiler/angular→v22.0.0-rc.2; regenerated fixtures + snapshot. Fixed two fixture-hygiene issues (repo-relativefile_path; restored trailing newline) and dropped the orphanedshadow_css_polyfills_specfixture (removed in v22).@angular/*npm deps →^22.0.0-rc.2in the NAPI package and thee2e/compareoutput-parity harness — the surfaces that validate v22 compiler behavior.e2e/appandplaygroundstay on v21.2.x: they're runtime apps, and booting them on the v22 RC needs separate app/vite-plugin migration (the v22 runtime fails to render, timing out the Playwright HMR suite). They use no version-divergent template features, so the v22-default compiler emits v21-identical output for them and the HMR e2e suite passes. Pinned benchmark fixtures also stay on 21.2.x.v22 conformance features (each its own commit)
in/instanceof: error on a keyword/EOF in primary position (v21 parsed bareinas the literal'in'— a bug Angular fixed).@defer: optionalidle(timeout)trigger parameter.data-attribute prefix (v22 removednormalizeAttributeName).@let: include the trailing;in the source span.:host(.a, .b)selector lists stay literal, bare:host-contextis no longer a context selector, comment removal no longer inserts blank lines.@switch:@default never;exhaustive check (lexer → parser → newSwitchExhaustiveCheckR3 node).<style>/<script>(:svg:style).Emit parity (compare harness)
formControl,formControlName, andngModel(incl. two-way[(ngModel)]) now emit the pairedɵɵcontrolCreate()/ɵɵcontrol()instructions. Implemented across ingest + binding-specialization.@angular/compilerversion into OXC's options so version-gated emit is compared like-for-like.v19–v21 compatibility
Version-gated runtime behaviors (
AngularVersion::supports_*— optional chaining, conditional create, dom property, chained queries, service decorator) are untouched and remain version-correct.The control-property emit change has real runtime impact (
[(ngModel)]is ubiquitous), so it's gated behind the newsupports_extended_control_properties()(major ≥ 22);formFieldstays unconditional as the v21 baseline, and an unknown version assumes latest. A regression test asserts[(ngModel)]emits onlyɵɵtwoWayPropertyon v19/v20/v21 and adds the control instructions on v22+.The remaining parse-layer divergences (data-
*, ShadowCss,idle(timeout),@default never,@letspan,in) are not version-gated: that layer is intentionally version-agnostic, and the features are additive (old apps never write them), cosmetic (span only), or rare microsyntax.Testing
cargo test -p oxc_angular_compiler— 32/32 binaries pass (two-way[(ngModel)]snapshots updated).cargo run -p oxc_angular_conformance— 1264/1264, snapshot committed (CIgit diff --exit-codeclean).pnpm --filter @oxc-angular/compare compare --fixtures— 686/686 vs@angular/compiler@22.0.0-rc.2.cargo fmt,oxfmt,oxlint --type-aware --type-checkclean.Note
Based on
main, so it includes the still-open #317 / PR #330 (commit1269b23) at its base. Merge #330 first, or merge this and #330 becomes redundant. The v22 work begins atb5b0eb0.🤖 Generated with Claude Code