Which @angular/* package(s) are the source of the bug?
compiler
Is this a regression?
Yes
Description
A library compiled with Angular < 22 in partial compilation mode (ɵɵngDeclareComponent({ version: "20.3.x", ... })) whose component omits changeDetection is handled differently by the two consumers of that declaration in an Angular 22 application:
- AOT (partial linker) keeps it on
Default/Eager: packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_component_linker_1.ts computes hasOnPushByDefault = major >= 22 || version === PLACEHOLDER_VERSION and only then falls back to OnPush. That is correct: a component written for v20 was always checked and may assign state inside subscriptions or callbacks without markForCheck().
- JIT (
ɵɵngDeclareComponent → ɵcompilerFacade.compileComponentDeclaration) ignores the declared version: packages/compiler/src/jit_compiler_facade.ts, convertDeclareComponentFacadeToMetadata, uses changeDetection: decl.changeDetection ?? ChangeDetectionStrategy.OnPush. The same v20 component becomes OnPush and silently stops re-rendering.
JIT is what Storybook (@storybook/angular), Jest (jest-preset-angular), Vitest without the AOT plugin, TestBed and ng serve --aot=false use, so a library that works in the built application breaks in every one of those. We hit it with Kendo UI for Angular 25 (compiled with Angular 20.3): its internal TreeViewGroupComponent assigns nodes inside a subscription and rendered an empty tree in Storybook, while the AOT build was fine.
This is the declare-path counterpart of #70262. That issue is about the JIT decorator path (@Component metadata → Eager) diverging from AOT (OnPush) for the app's own components. Here the JIT declare path (library partial declarations) diverges from the linker in the opposite direction: it applies the v22 default to declarations that predate v22. The two paths disagree with AOT in different ways, so fixing #70262 alone does not fix this.
Expected: the JIT declare path applies the same rule as the linker, i.e. OnPush only when decl.version is >= 22 (or the placeholder version), Eager otherwise.
Workaround we use in Storybook/Vitest (must run before any library declaration is evaluated):
import '@angular/compiler';
import { ChangeDetectionStrategy } from '@angular/core';
const facade = (globalThis as any).ng?.ɵcompilerFacade;
const original = facade.compileComponentDeclaration.bind(facade);
facade.compileComponentDeclaration = (env: unknown, url: string, decl: any) => {
const major = Number.parseInt(String(decl.version ?? '0').split('.')[0], 10);
const patched =
decl.changeDetection == null && major > 0 && major < 22
? { ...decl, changeDetection: ChangeDetectionStrategy.Eager }
: decl;
return original(env, url, patched);
};
Please provide a link to a minimal reproduction of the bug
https://github.com/diegohoyos/angular-jit-onpush-repro
Please provide the exception or error you saw
No exception. The component compiled through ɵɵngDeclareComponent gets ɵcmp.onPush === true and its view is never refreshed after the subscription assigns new state.
Please provide the environment you discovered this bug in (run ng version)
Angular CLI : 22.2.0
Angular : 22.2.0
Node.js : 22.23.2
Package Manager : npm 12.0.2
Operating System : win32 x64
┌───────────────────────────┬───────────────────┬───────────────────┐
│ Package │ Installed Version │ Requested Version │
├───────────────────────────┼───────────────────┼───────────────────┤
│ @angular/build │ 22.2.0 │ 22.2.0 │
│ @angular/cli │ 22.2.0 │ 22.2.0 │
│ @angular/common │ 22.2.0 │ 22.2.0 │
│ @angular/compiler │ 22.2.0 │ 22.2.0 │
│ @angular/compiler-cli │ 22.2.0 │ 22.2.0 │
│ @angular/core │ 22.2.0 │ 22.2.0 │
│ @angular/platform-browser │ 22.2.0 │ 22.2.0 │
│ rxjs │ 7.8.2 │ 7.8.2 │
│ typescript │ 6.0.3 │ 6.0.3 │
│ zone.js │ 0.16.3 │ 0.16.3 │
└───────────────────────────┴───────────────────┴───────────────────┘
The library (legacy-lib) was compiled with @angular/compiler-cli 20.3.33 / typescript 5.8.3, compilationMode: "partial".
Anything else?
packages/compiler-cli/linker/.../partial_component_linker_1.ts: const hasOnPushByDefault = major >= 22 || version === PLACEHOLDER_VERSION; then changeDetection: metaObj.has('changeDetection') ? ... : hasOnPushByDefault ? OnPush : Eager.
packages/compiler/src/jit_compiler_facade.ts, convertDeclareComponentFacadeToMetadata: changeDetection: decl.changeDetection ?? ChangeDetectionStrategy.OnPush with no look at decl.version.
- Reproduces on 22.0.x through 22.2.0.
Which @angular/* package(s) are the source of the bug?
compiler
Is this a regression?
Yes
Description
A library compiled with Angular < 22 in partial compilation mode (
ɵɵngDeclareComponent({ version: "20.3.x", ... })) whose component omitschangeDetectionis handled differently by the two consumers of that declaration in an Angular 22 application:Default/Eager:packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_component_linker_1.tscomputeshasOnPushByDefault = major >= 22 || version === PLACEHOLDER_VERSIONand only then falls back toOnPush. That is correct: a component written for v20 was always checked and may assign state inside subscriptions or callbacks withoutmarkForCheck().ɵɵngDeclareComponent→ɵcompilerFacade.compileComponentDeclaration) ignores the declared version:packages/compiler/src/jit_compiler_facade.ts,convertDeclareComponentFacadeToMetadata, useschangeDetection: decl.changeDetection ?? ChangeDetectionStrategy.OnPush. The same v20 component becomesOnPushand silently stops re-rendering.JIT is what Storybook (
@storybook/angular), Jest (jest-preset-angular), Vitest without the AOT plugin,TestBedandng serve --aot=falseuse, so a library that works in the built application breaks in every one of those. We hit it with Kendo UI for Angular 25 (compiled with Angular 20.3): its internalTreeViewGroupComponentassigns nodes inside a subscription and rendered an empty tree in Storybook, while the AOT build was fine.This is the declare-path counterpart of #70262. That issue is about the JIT decorator path (
@Componentmetadata →Eager) diverging from AOT (OnPush) for the app's own components. Here the JIT declare path (library partial declarations) diverges from the linker in the opposite direction: it applies the v22 default to declarations that predate v22. The two paths disagree with AOT in different ways, so fixing #70262 alone does not fix this.Expected: the JIT declare path applies the same rule as the linker, i.e.
OnPushonly whendecl.versionis >= 22 (or the placeholder version),Eagerotherwise.Workaround we use in Storybook/Vitest (must run before any library declaration is evaluated):
Please provide a link to a minimal reproduction of the bug
https://github.com/diegohoyos/angular-jit-onpush-repro
Please provide the exception or error you saw
Please provide the environment you discovered this bug in (run
ng version)Anything else?
packages/compiler-cli/linker/.../partial_component_linker_1.ts:const hasOnPushByDefault = major >= 22 || version === PLACEHOLDER_VERSION;thenchangeDetection: metaObj.has('changeDetection') ? ... : hasOnPushByDefault ? OnPush : Eager.packages/compiler/src/jit_compiler_facade.ts,convertDeclareComponentFacadeToMetadata:changeDetection: decl.changeDetection ?? ChangeDetectionStrategy.OnPushwith no look atdecl.version.