Skip to content

fix: support @Service and ChangeDetectionStrategy.Eager (Angular v22)#360

Merged
brandonroberts merged 3 commits into
mainfrom
fix/linker-ngdeclare-service
Jun 17, 2026
Merged

fix: support @Service and ChangeDetectionStrategy.Eager (Angular v22)#360
brandonroberts merged 3 commits into
mainfrom
fix/linker-ngdeclare-service

Conversation

@brandonroberts

Copy link
Copy Markdown
Collaborator

Summary

Two Angular v22 compatibility gaps, surfaced while running a real v22 app (the Angular Components docs site) through @oxc-angular/vite. Both caused silent runtime failures; both are now fixed and backward compatible with Angular 17–21.

1. @Service decorator (Angular v22)

@Service was unhandled at three layers, so any @Service class fell back to the JIT compiler at runtime ("service needs the JIT compiler, '@angular/compiler' is not available"):

  • LinkerɵɵngDeclareService wasn't recognized, so partially-compiled libraries using @Service (e.g. BrowserXhr in @angular/common) stayed partial. Added a link_service handler emitting ɵɵdefineService (factory delegates to ɵfac unless an explicit factory is declared; autoProvided only when false), mirroring compileService.
  • Compiler — the has_angular_decorator pre-check omitted Service, so a @Service-only file skipped AOT entirely and the decorator was downleveled to a runtime _decorate() call. Added Service to the pre-check.
  • Vite plugin — the quick decorator filter in vite-plugin/index.ts omitted @Service, skipping transformAngularFile for @Service-only files. Added it.

Backward compatible: service compilation is gated on supports_service_decorator() (v22+), and the @angular/core import check ignores unrelated @Service decorators.

2. ChangeDetectionStrategy.Eager (Angular v22)

v22 reworked the enum: OnPush = 0 is now the default strategy, Eager = 1 is new, Default = 1 is a deprecated alias, and the runtime flipped to onPush = changeDetection !== Eager. The compiler modeled only the pre-v22 {Default, OnPush} pair and dropped Eager, so changeDetection: ChangeDetectionStrategy.Eager was omitted from ɵɵdefineComponent — the v22 runtime saw undefined, computed onPush = true, and an eager component silently behaved as OnPush (async updates never re-rendered).

The fix is version-gated so it doesn't regress 17–21:

  • ComponentMetadata.change_detection is now Option (distinguishes "unspecified" from explicit).
  • The emitter writes the numeric value only when it differs from the value the runtime assumes for an omitted field, keyed on AngularVersion::change_detection_default_is_on_push():
    • v22+: omit OnPush, emit 1 for Eager.
    • < v22: omit Default, emit 0 for an explicit OnPush.
    • Unknown version assumes latest.
  • Partial declarations emit the symbolic member whenever specified (matching Angular's partial compiler); the linker resolves OnPush/Eager0/1 against the consuming app's version.

Test plan

  • cargo test — full suite green (1052 lib + integration/partial/linker/parity/service; 0 failures)
  • cargo fmt --all -- --check — clean
  • cargo run -p oxc_angular_conformance1264/1264 (100%), snapshot unchanged (git diff --exit-code passes)
  • NAPI pnpm test — 200 passed
  • Manual end-to-end — rebuilt the binding and verified against the v22 docs app: BrowserXhr/@Service classes compile to ɵɵdefineService (no JIT), and a changeDetection: Eager component renders correctly (onPush=false, all items render)

New regression coverage (runs in CI via cargo test):

  • Service: linker (ɵɵngDeclareService → ɵɵdefineService) + compiler (@Service-only file AOT-compiles)
  • ChangeDetection version matrix: v22 Eager→1 / OnPush omitted / Default→1; v21 OnPush→0 / Default omitted; unspecified omitted on both; partial-emit parity vs upstream; linker Eager→1

🤖 Generated with Claude Code

brandonroberts and others added 2 commits June 15, 2026 20:47
Angular v22's new @service decorator was unhandled at three layers, so any
@service class fell back to the JIT compiler at runtime ("service needs the
JIT compiler, '@angular/compiler' is not available"):

- Linker: ɵɵngDeclareService was not recognized, so partially-compiled
  libraries using @service (e.g. BrowserXhr in @angular/common) were left as
  partial declarations. Add a link_service handler that emits ɵɵdefineService
  (factory delegates to ɵfac unless an explicit factory is declared;
  autoProvided emitted only when false), mirroring compileService.
- Compiler: the has_angular_decorator pre-check omitted Service, so a file
  whose only Angular decorator was @service skipped AOT compilation entirely
  and the decorator was downleveled to a runtime _decorate() call. Add Service
  to the pre-check.
- Vite plugin: the quick decorator filter in vite-plugin/index.ts omitted
  @service, skipping transformAngularFile for @Service-only files. Add it.

Regression tests cover the linker (ɵɵngDeclareService -> ɵɵdefineService for
default / explicit factory / autoProvided:false) and the compiler (a
@Service-only file emits ɵfac + ɵɵdefineService, not a runtime decorator).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Angular reworked ChangeDetectionStrategy in v22: OnPush = 0 became the default
strategy, Eager = 1 was added, and Default = 1 is a deprecated alias for Eager.
The runtime onPush flag flipped accordingly:
  - >= v22: onPush = changeDetection !== Eager  (default OnPush)
  - <  v22: onPush = changeDetection === OnPush  (default Default/Eager)

The compiler modeled only the pre-v22 {Default, OnPush} pair and dropped
`Eager`, so `changeDetection: ChangeDetectionStrategy.Eager` was lost from
ɵɵdefineComponent — the v22 runtime saw `undefined`, computed `onPush = true`,
and an eager component silently behaved as OnPush (async updates not rendered).

Fix without regressing 17-21:
  - Model the enum as {OnPush(0), Eager(1)} with Default parsed as the Eager
    alias, and make ComponentMetadata.change_detection an Option so "not
    specified" is distinct from an explicit strategy.
  - Emit the numeric value only when it differs from the value the runtime
    assumes for an omitted field, gated on the target version
    (AngularVersion::change_detection_default_is_on_push): v22+ omits OnPush and
    emits 1 for Eager; < v22 omits Default and emits 0 for an explicit OnPush.
    Unknown version assumes latest.
  - Partial declarations emit the symbolic member whenever specified (matching
    Angular's partial compiler); the linker resolves OnPush/Eager to 0/1.

Adds version-matrixed regression tests (v22 Eager->1, OnPush omitted; v21
OnPush->0, Default omitted; unspecified omitted on both).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@brandonroberts brandonroberts force-pushed the fix/linker-ngdeclare-service branch from 380444a to 2893a3f Compare June 16, 2026 01:49
@brandonroberts brandonroberts enabled auto-merge (squash) June 16, 2026 02:04
@brandonroberts

Copy link
Copy Markdown
Collaborator Author

@codex[agent] review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2893a3fdd4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/oxc_angular_compiler/src/component/hoist.rs
Comment thread crates/oxc_angular_compiler/src/component/decorator.rs Outdated
Make `@Service` hoisting import-aware: the TDZ hoist filter matched the
decorator by name only, so a third-party `@Service` class (a common name
in non-Angular DI frameworks) would have its referenced top-level
declarations reordered above the class, changing that class's runtime
evaluation order. The filter now verifies the `@Service` import resolves
to `@angular/core` (identifier and namespace forms) before hoisting; the
other decorator names keep their long-standing name-only behavior, where
over-triggering only hoists a TDZ-safe declaration earlier.

Preserve `ChangeDetectionStrategy.Default` as distinct from `Eager`.
`Default` (the pre-v22 spelling) and `Eager` share the numeric value 1,
but a partial declaration targeting a pre-v22 Angular must emit the exact
member the author wrote — emitting `Eager` there would reference a member
that does not exist. Add a distinct `Default` enum variant carried through
metadata extraction, partial symbolic emit, and the napi round-trip; full
AOT emit still collapses both to numeric 1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@brandonroberts brandonroberts merged commit f69ea3e into main Jun 17, 2026
9 checks passed
@brandonroberts brandonroberts deleted the fix/linker-ngdeclare-service branch June 17, 2026 08:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants