Skip to content

JIT ɵɵngDeclareComponent defaults pre-v22 library components to OnPush; the AOT linker keeps them on Default (hasOnPushByDefault version rule is missing in convertDeclareComponentFacadeToMetadata) #71107

Description

@diegohoyos

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);
};
Image

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: compilerIssues related to `ngc`, Angular's template compilergemini-triagedLabel noting that an issue has been triaged by gemini

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions