Skip to content

fix(linker): resolve changeDetection default from the declaration version - #429

Merged
Brooooooklyn merged 2 commits into
voidzero-dev:mainfrom
ashley-hunter:fix/linker-change-detection-default
Aug 10, 2026
Merged

fix(linker): resolve changeDetection default from the declaration version#429
Brooooooklyn merged 2 commits into
voidzero-dev:mainfrom
ashley-hunter:fix/linker-change-detection-default

Conversation

@ashley-hunter

@ashley-hunter ashley-hunter commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

The linker emitted changeDetection only when the partial declaration carried the field, with no default for the omitted case.

Which strategy an omitted field means depends on the version the library was compiled against. Angular v22 made OnPush the default, so anything older meant Eager. The TS linker resolves that explicitly:

// partial_component_linker_1.ts
const hasOnPushByDefault = major >= 22 || version === PLACEHOLDER_VERSION;
...
changeDetection: metaObj.has('changeDetection')
  ? parseChangeDetectionStrategy(metaObj.getValue('changeDetection'))
  : hasOnPushByDefault ? ChangeDetectionStrategy.OnPush : ChangeDetectionStrategy.Eager,

Impact

In v22 OnPush = 0, Eager = 1, and Default = 1 is a deprecated alias for Eager. The runtime derives the flag from:

// packages/core/src/render3/definition.ts
onPush: componentDefinition.changeDetection !== ChangeDetectionStrategy.Eager,

So an absent field evaluates undefined !== 1 and reads as OnPush. A pre-v22 component that never declared a strategy was therefore silently switched from Eager to OnPush, and stops re-rendering on anything that is not a signal or input change. Nothing is logged, so it surfaces only as stale UI.

Scanning a real node_modules: 154 ɵɵngDeclareComponent declarations, 139 of them pre-v22, and 6 of those omit the field. Most libraries do declare a strategy, so the blast radius is narrow, but the ones that do not are unlucky:

Component Selector Compiled with
AgGridAngular ag-grid-angular v17.3.12
HighchartsChartComponent highcharts-chart v16.2.12
MarkdownComponent markdown v21.2.5
_MatDialogContainerBase (none) v16.1.1
NgpTooltipTextContentComponent (none) v21.2.14
DynamicViewComponent dynamic-view v18.0.4

A grid or chart component quietly becoming OnPush is a rough one to track down.

Also in this change (reverted after review)

This originally stopped emitting changeDetection: 0 for an explicit OnPush, to match the meta.changeDetection !== OnPush guard in compileComponentFromMetadata. That was wrong and has been reverted in cdd3353.

That compiler ships with the runtime it targets. This linker does not know the consumer's version: angularLinkerPlugin() takes no options and linkCode calls linkAngularPackage(code, id), while the plugin's angularVersion option documents support back to v19. Angular 20 and 21 compute onPush = changeDetection === OnPush, so an absent field reads as Default there and the component would have silently lost OnPush. Emitting 0 is correct on both: pre-v22 0 === 0, v22 0 !== 1.

So this PR is now just the version-gated default. 1 is likewise correct on both runtimes (pre-v22 1 === 0 is false, v22 1 !== 1 is false, both Eager/Default).

One existing test asserted the changeDetection: 0 emit; it is unchanged, and now carries a comment explaining why the emit has to stay so a future parity cleanup trips over it.

Tests

Three tests covering the version gate: pre-v22 without the field must emit changeDetection: 1, v22+ must not emit, and 0.0.0-PLACEHOLDER must not emit. Only the first is red before the fix; the other two are guards for behaviour that must not change.

The version gate reuses the shape of the existing get_default_standalone_value helper, which already does the same semver-plus-placeholder resolution for standalone.

Verification

Every CI step run locally on macOS:

Step Result
cargo check --all-features pass
cargo test pass
cargo fmt --all -- --check pass
cargo run -p oxc_angular_conformance 1264/1264 (100%), snapshot byte-identical
pnpm build-dev + build:ts pass
pnpm test 200/200
pnpm check pass
pnpm test:e2e 34/34
compare --fixtures 100%

Also rebuilt the napi binding and confirmed ag-grid-angular, highcharts-angular and ng-primitives now emit changeDetection: 1.

Re-ran the full suite after the revert; all of the above still pass.

Note

This was found by diffing linked output against @angular/compiler-cli's linker rather than by any existing test, and the same exercise found #428. compare --fixtures stays at 100% throughout, because that harness covers source to AOT compilation and has no ngDeclare fixtures, so the linker currently has no differential coverage against the official linker. A partial-declaration category there would likely have caught both, but that is well outside the scope of this change.

…ersion

The linker emitted `changeDetection` only when the partial declaration
carried the field, with no default for the omitted case. Which strategy
an omitted field means depends on the version the library was compiled
against: v22 made OnPush the default, so anything older meant `Eager`.
The TS linker resolves that with
`hasOnPushByDefault = major >= 22 || version === PLACEHOLDER_VERSION`
in `partial_component_linker_1.ts`.

Because the runtime derives `onPush = changeDetection !== Eager`, an
absent field reads as OnPush. A pre-v22 component that never declared a
strategy was therefore silently switched from Eager to OnPush, and stops
re-rendering on anything that is not a signal or input change. Nothing is
logged, so it surfaces only as stale UI.

Six components in a typical `node_modules` hit this, including
`AgGridAngular` (v17.3.12), `HighchartsChartComponent` (v16.2.12),
`MarkdownComponent` (v21.2.5) and `_MatDialogContainerBase` (v16.1.1).

Also stops emitting `changeDetection: 0` for an explicit OnPush. OnPush
is the emit-time default and the TS compiler leaves it out
(`meta.changeDetection !== OnPush` in `compileComponentFromMetadata`);
the runtime infers it from the absent field. This is byte-identical in
behaviour, and it clears the last `changeDetection` divergence when
diffing linked output against the official linker across a real
`node_modules`.
@Brooooooklyn

Copy link
Copy Markdown
Member

@codex 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: 90c1a1984e

ℹ️ 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/linker/mod.rs Outdated
Reverts the parity tweak that dropped the field for OnPush, per review.

The TS compiler omits OnPush, but it ships with the runtime it targets.
This linker does not know the consumer's version: `angularLinkerPlugin()`
takes no options and `linkCode` calls `linkAngularPackage(code, id)`, and
the plugin's `angularVersion` option documents support back to v19.

Angular 20 and 21 compute `onPush = changeDetection === OnPush`, so an
absent field reads as Default there and the component would silently lose
OnPush. `0` is correct on both: pre-v22 `0 === 0`, and v22 `0 !== 1`.

The version-gated default for an omitted field is unaffected, since `1`
is likewise correct on both.
@Brooooooklyn

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🚀

Reviewed commit: cdd3353804

ℹ️ 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".

@Brooooooklyn
Brooooooklyn merged commit 32a97c5 into voidzero-dev:main Aug 10, 2026
10 checks passed
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