fix: support @Service and ChangeDetectionStrategy.Eager (Angular v22)#360
Merged
Conversation
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>
380444a to
2893a3f
Compare
Collaborator
Author
|
@codex[agent] review |
There was a problem hiding this comment.
💡 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".
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>
Brooooooklyn
approved these changes
Jun 17, 2026
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
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.
@Servicedecorator (Angular v22)@Servicewas unhandled at three layers, so any@Serviceclass fell back to the JIT compiler at runtime ("service needs the JIT compiler, '@angular/compiler' is not available"):ɵɵngDeclareServicewasn't recognized, so partially-compiled libraries using@Service(e.g.BrowserXhrin@angular/common) stayed partial. Added alink_servicehandler emittingɵɵdefineService(factory delegates toɵfacunless an explicitfactoryis declared;autoProvidedonly whenfalse), mirroringcompileService.has_angular_decoratorpre-check omittedService, so a@Service-only file skipped AOT entirely and the decorator was downleveled to a runtime_decorate()call. AddedServiceto the pre-check.vite-plugin/index.tsomitted@Service, skippingtransformAngularFilefor@Service-only files. Added it.Backward compatible: service compilation is gated on
supports_service_decorator()(v22+), and the@angular/coreimport check ignores unrelated@Servicedecorators.2.
ChangeDetectionStrategy.Eager(Angular v22)v22 reworked the enum:
OnPush = 0is now the default strategy,Eager = 1is new,Default = 1is a deprecated alias, and the runtime flipped toonPush = changeDetection !== Eager. The compiler modeled only the pre-v22{Default, OnPush}pair and droppedEager, sochangeDetection: ChangeDetectionStrategy.Eagerwas omitted fromɵɵdefineComponent— the v22 runtime sawundefined, computedonPush = 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_detectionis nowOption(distinguishes "unspecified" from explicit).AngularVersion::change_detection_default_is_on_push():OnPush, emit1forEager.Default, emit0for an explicitOnPush.OnPush/Eager→0/1against 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— cleancargo run -p oxc_angular_conformance— 1264/1264 (100%), snapshot unchanged (git diff --exit-codepasses)pnpm test— 200 passedBrowserXhr/@Serviceclasses compile toɵɵdefineService(no JIT), and achangeDetection: Eagercomponent renders correctly (onPush=false, all items render)New regression coverage (runs in CI via
cargo test):ɵɵngDeclareService → ɵɵdefineService) + compiler (@Service-only file AOT-compiles)Eager→1/OnPushomitted /Default→1; v21OnPush→0/Defaultomitted; unspecified omitted on both; partial-emit parity vs upstream; linkerEager→1🤖 Generated with Claude Code