[CL-954] Upgrade to Angular 21 (#19725)

* prune desktop packages

* Fix @napi-rs/cli version mismatch in desktop napi workspace

Aligns package.json declaration with the locked version (3.2.0) to
resolve npm workspace inconsistency that was blocking ng update.

* update Storybook to v10

* update Angular to v21

* override jest in ng builder

* Add jest-environment-jsdom as explicit dependency

Previously installed as a side effect of a jest@29 override; removing
that override caused it to disappear from node_modules.

* Add .claude/worktrees/ to .gitignore

* Restore @napi-rs/cli to 3.5.1 to match main

* Pin jest-environment-jsdom to 29.7.0 and add to renovate config

* Override jest-environment-jsdom to 29.7.0 in build-angular context

* Add isolatedModules to libs/subscription tsconfig.spec.json to fix Angular 21 module resolution

* Change moduleResolution to bundler for Angular 21 subpath export compatibility

* Add isolatedModules to Angular libs with old spec tsconfig pattern

* Disable emitDecoratorMetadata in spec tsconfigs with isolatedModules

* Fix HostListener event parameter types for Angular 21 compiler strictness

* Revert accidental change to access-selector spec

* Remove accidentally generated desktop package-lock.json

* Fix type-only imports/exports caught by Rolldown in Storybook v10/Vite v8

* fix vault-wrapper type error from Angular 21 stricter generic inference

ngComponentOutlet accepts Type<unknown>; annotate computed() explicitly
since VaultComponent is generic and VaultOrigComponent is not, preventing
TypeScript from inferring a compatible union constructor type.

* Fix kitchen-sink interaction tests for Storybook v10

Replace fire-and-forget navigateTo + synchronous getByRole with
navigateAndWaitFor<T>, which sets the hash and retries the ready
callback via waitFor. Storybook v10 starts play functions before
Angular's initial router navigation completes, so synchronous DOM
queries after navigation were failing intermittently.

* Provide ZoneJS change detection scheduler for Storybook stories

Angular 21 no longer sets up the ZoneJS change detection scheduler by
default in bootstrapApplication. Storybook's Angular renderer uses
bootstrapApplication internally and does not add provideZoneChangeDetection
automatically, so Default CD components relying on zone.js to trigger
re-renders after async operations were not updating before Chromatic
snapshots.

* Wait for dialog/side nav to render before Chromatic snapshot

After userEvent.click the dialog and side nav open asynchronously.
Without an explicit waitFor, Chromatic captures the snapshot before the
resulting UI state is present.

* Fix kitchen-sink waitFor: re-query side nav button, use querySelector for dialog

- openSideNav: re-query the toggle button inside waitFor to avoid reading
  a stale DOM reference after Angular re-renders the element post-click
- SimpleDialogOpen / VirtualScrollBlockingDialog: replace getByRole("dialog")
  with querySelector("cdk-dialog-container") to avoid testing-library's
  visibility check failing on a momentarily inaccessible overlay element

* Revert kitchen-sink stories to main

* Bump Angular, Storybook, and ng-select to latest patch versions

* Trigger pre-commit hooks on merge

* Regenerate package-lock.json with --force to fix npm ci sync
This commit is contained in:
Will Martin 2026-05-18 11:55:47 -04:00 committed by GitHub
parent 7caeab7de5
commit 69c937e592
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 7933 additions and 13554 deletions

View File

@ -183,6 +183,7 @@
"itertools",
"jest",
"jest-diff",
"jest-environment-jsdom",
"jest-junit",
"jest-mock-extended",
"jest-preset-angular",
@ -515,6 +516,7 @@
"interprocess",
"jest",
"jest-diff",
"jest-environment-jsdom",
"jest-junit",
"jest-mock-extended",
"jest-preset-angular",

1
.gitignore vendored
View File

@ -1,6 +1,7 @@
# General
.DS_Store
Thumbs.db
.claude/worktrees/
# IDEs and editors
.idea/

View File

@ -1,9 +1,13 @@
// This file has been automatically migrated to valid ESM format by Storybook.
import { createRequire } from "node:module";
import { dirname, join } from "path";
import { StorybookConfig } from "@storybook/angular";
import remarkGfm from "remark-gfm";
import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin";
const require = createRequire(import.meta.url);
const config: StorybookConfig = {
stories: [
"../libs/auth/src/**/*.mdx",

View File

@ -1,6 +1,7 @@
import { provideZoneChangeDetection } from "@angular/core";
import { setCompodocJson } from "@storybook/addon-docs/angular";
import { withThemeByClassName } from "@storybook/addon-themes";
import { componentWrapperDecorator } from "@storybook/angular";
import { applicationConfig, componentWrapperDecorator } from "@storybook/angular";
import type { Preview } from "@storybook/angular";
import docJson from "../documentation.json";
@ -17,6 +18,9 @@ const wrapperDecorator = componentWrapperDecorator((story) => {
const preview: Preview = {
decorators: [
applicationConfig({
providers: [provideZoneChangeDetection()],
}),
withThemeByClassName({
themes: {
light: "theme_light",

View File

@ -2,11 +2,12 @@ import createEmotion from "@emotion/css/create-instance";
import { html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators.js";
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
import type { Theme } from "@bitwarden/common/platform/enums";
import { ThemeTypes } from "@bitwarden/common/platform/enums";
import { EventSecurity } from "../../../utils/event-security";
import { OptionSelectionButton } from "../buttons/option-selection-button";
import { Option } from "../common-types";
import type { Option } from "../common-types";
import { optionItemIconWidth } from "./option-item";
import { OptionItems, optionsMenuItemMaxWidth } from "./option-items";

View File

@ -1,4 +1,4 @@
import { enableProdMode } from "@angular/core";
import { enableProdMode, provideZoneChangeDetection } from "@angular/core";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import { PopupSizeService } from "../platform/popup/layout/popup-size.service";
@ -20,7 +20,9 @@ if (process.env.ENV === "production") {
}
function init() {
void platformBrowserDynamic().bootstrapModule(AppModule);
void platformBrowserDynamic().bootstrapModule(AppModule, {
applicationProviders: [provideZoneChangeDetection()],
});
}
init();

View File

@ -1,6 +1,6 @@
import "core-js/proposals/explicit-resource-management";
import { enableProdMode } from "@angular/core";
import { enableProdMode, provideZoneChangeDetection } from "@angular/core";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
// eslint-disable-next-line @typescript-eslint/no-require-imports
@ -14,7 +14,9 @@ if (!ipc.platform.isDev) {
enableProdMode();
}
void platformBrowserDynamic().bootstrapModule(AppModule);
void platformBrowserDynamic().bootstrapModule(AppModule, {
applicationProviders: [provideZoneChangeDetection()],
});
// Disable drag and drop to prevent malicious links from executing in the context of the app
document.addEventListener("dragover", (event) => event.preventDefault());

View File

@ -1,5 +1,5 @@
import { CommonModule } from "@angular/common";
import { Component, computed, inject } from "@angular/core";
import { Component, computed, inject, Type } from "@angular/core";
import { toSignal } from "@angular/core/rxjs-interop";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
@ -22,7 +22,7 @@ export class VaultWrapperComponent {
this.configService.getFeatureFlag$(FeatureFlag.DesktopUiMigrationMilestone3),
);
protected readonly componentToRender = computed(() =>
protected readonly componentToRender = computed<Type<unknown>>(() =>
this.useMilestone3() ? VaultComponent : VaultOrigComponent,
);
}

View File

@ -1,4 +1,4 @@
import { enableProdMode } from "@angular/core";
import { enableProdMode, provideZoneChangeDetection } from "@angular/core";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import { AppModule } from "./app/app.module";
@ -7,4 +7,6 @@ if (process.env.NODE_ENV === "production") {
enableProdMode();
}
void platformBrowserDynamic().bootstrapModule(AppModule);
void platformBrowserDynamic().bootstrapModule(AppModule, {
applicationProviders: [provideZoneChangeDetection()],
});

View File

@ -1,4 +1,4 @@
import { enableProdMode } from "@angular/core";
import { enableProdMode, provideZoneChangeDetection } from "@angular/core";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import { AppModule } from "./app/app.module";
@ -7,4 +7,6 @@ if (process.env.NODE_ENV === "production") {
enableProdMode();
}
void platformBrowserDynamic().bootstrapModule(AppModule);
void platformBrowserDynamic().bootstrapModule(AppModule, {
applicationProviders: [provideZoneChangeDetection()],
});

View File

@ -4,7 +4,9 @@
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"moduleResolution": "node10",
"types": ["jest", "node"]
"types": ["jest", "node"],
"isolatedModules": true,
"emitDecoratorMetadata": false
},
"include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"]
}

View File

@ -96,8 +96,10 @@ export class BitFormFieldComponent implements AfterContentChecked {
*/
protected readonly defaultContentIsFocused = signal(false);
@HostListener("focusin", ["$event.target"])
onFocusIn(target: HTMLElement) {
this.defaultContentIsFocused.set(target.matches("[data-default-content] *:focus-visible"));
onFocusIn(target: EventTarget) {
this.defaultContentIsFocused.set(
(target as HTMLElement).matches("[data-default-content] *:focus-visible"),
);
}
@HostListener("focusout")
onFocusOut() {

View File

@ -24,8 +24,8 @@ export class ItemComponent {
*/
protected readonly focusVisibleWithin = signal(false);
@HostListener("focusin", ["$event.target"])
onFocusIn(target: HTMLElement) {
this.focusVisibleWithin.set(target.matches("[data-fvw-target]:focus-visible"));
onFocusIn(target: EventTarget) {
this.focusVisibleWithin.set((target as HTMLElement).matches("[data-fvw-target]:focus-visible"));
}
@HostListener("focusout")
onFocusOut() {

View File

@ -282,7 +282,7 @@ export class LayoutComponent {
* @see https://github.com/angular/components/issues/10247#issuecomment-384060265
**/
private readonly skipLink = viewChild.required<LinkComponent>("skipLink");
handleKeydown(ev: KeyboardEvent) {
handleKeydown(ev: Event) {
if (isNothingFocused()) {
ev.preventDefault();
this.skipLink().el.nativeElement.focus();

View File

@ -1,7 +1,8 @@
import { provideZoneChangeDetection } from "@angular/core";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import { AppModule } from "./app/app.module";
platformBrowserDynamic()
.bootstrapModule(AppModule)
.bootstrapModule(AppModule, { applicationProviders: [provideZoneChangeDetection()] })
.catch((err) => console.error(err)); // eslint-disable-line

View File

@ -121,8 +121,8 @@ export class NavItemComponent extends NavBaseComponent {
: "",
);
protected onFocusIn(target: HTMLElement) {
this.focusVisibleWithin.set(target.matches("[data-fvw]:focus-visible"));
protected onFocusIn(target: EventTarget) {
this.focusVisibleWithin.set((target as HTMLElement).matches("[data-fvw]:focus-visible"));
}
protected onFocusOut() {

View File

@ -84,8 +84,8 @@ export class TabLinkComponent implements FocusableOption, AfterViewInit {
/** Roving tabindex value — parent nav-bar sets one link to 0 and the rest to -1. */
readonly tabIndex = signal(-1);
@HostListener("keydown", ["$event"]) onKeyDown(event: KeyboardEvent) {
if (event.code === "Space") {
@HostListener("keydown", ["$event"]) onKeyDown(event: Event) {
if ((event as KeyboardEvent).code === "Space") {
this.tabItem().click();
}
}

View File

@ -4,7 +4,9 @@
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"moduleResolution": "node10",
"types": ["jest", "node"]
"types": ["jest", "node"],
"isolatedModules": true,
"emitDecoratorMetadata": false
},
"include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"]
}

View File

@ -3,4 +3,4 @@ export { LogLevel } from "./log-level";
export { ConsoleLogService } from "./console-log.service";
export { FlightRecorder } from "./flight-recorder";
export { buildFlightRecorderCsvExport } from "./flight-recorder-export";
export { FlightRecorderEvent } from "@bitwarden/sdk-internal";
export type { FlightRecorderEvent } from "@bitwarden/sdk-internal";

View File

@ -1,7 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "commonjs"
"module": "commonjs",
"moduleResolution": "node"
},
"files": [],
"include": [],

View File

@ -4,7 +4,9 @@
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"moduleResolution": "node10",
"types": ["jest", "node"]
"types": ["jest", "node"],
"isolatedModules": true,
"emitDecoratorMetadata": false
},
"include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"]
}

View File

@ -4,7 +4,9 @@
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"moduleResolution": "node10",
"types": ["jest", "node"]
"types": ["jest", "node"],
"isolatedModules": true,
"emitDecoratorMetadata": false
},
"include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"]
}

21341
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -46,10 +46,10 @@
"libs/**/*"
],
"devDependencies": {
"@angular-devkit/build-angular": "20.3.12",
"@angular-eslint/schematics": "20.7.0",
"@angular/cli": "20.3.12",
"@angular/compiler-cli": "20.3.18",
"@angular-devkit/build-angular": "21.2.9",
"@angular-eslint/schematics": "21.3.1",
"@angular/cli": "21.2.9",
"@angular/compiler-cli": "21.2.11",
"@babel/core": "7.29.0",
"@babel/preset-env": "7.29.2",
"@compodoc/compodoc": "1.2.1",
@ -57,20 +57,20 @@
"@electron/rebuild": "4.0.3",
"@eslint/compat": "2.0.5",
"@lit-labs/signals": "0.2.0",
"@ngtools/webpack": "20.3.12",
"@ngtools/webpack": "21.2.9",
"@storybook/addon-a11y": "10.3.6",
"@storybook/addon-designs": "11.1.3",
"@storybook/addon-docs": "10.3.6",
"@storybook/addon-links": "10.3.6",
"@storybook/addon-themes": "10.3.6",
"@storybook/angular": "10.3.6",
"@storybook/test-runner": "0.24.3",
"@storybook/web-components-vite": "10.3.6",
"@nx/devkit": "22.6.5",
"@nx/eslint": "22.6.5",
"@nx/jest": "22.6.5",
"@nx/js": "22.6.5",
"@nx/webpack": "22.6.5",
"@storybook/addon-a11y": "9.1.16",
"@storybook/addon-designs": "9.0.0-next.3",
"@storybook/addon-docs": "9.1.16",
"@storybook/addon-links": "9.1.16",
"@storybook/addon-themes": "9.1.16",
"@storybook/angular": "9.1.16",
"@storybook/test-runner": "0.22.0",
"@storybook/web-components-vite": "9.1.16",
"@tailwindcss/container-queries": "0.1.1",
"@types/chrome": "0.1.28",
"@types/firefox-webext-browser": "143.0.0",
@ -118,7 +118,7 @@
"eslint-plugin-jest": "29.15.2",
"eslint-plugin-rxjs": "5.0.3",
"eslint-plugin-rxjs-angular": "2.0.1",
"eslint-plugin-storybook": "9.1.20",
"eslint-plugin-storybook": "10.3.6",
"eslint-plugin-tailwindcss": "3.18.3",
"fantasticon": "4.1.0",
"html-loader": "5.1.0",
@ -127,6 +127,7 @@
"husky": "9.1.7",
"jest": "30.3.0",
"jest-diff": "30.3.0",
"jest-environment-jsdom": "29.7.0",
"jest-junit": "17.0.0",
"jest-mock-extended": "4.0.1",
"jest-preset-angular": "16.1.4",
@ -142,9 +143,9 @@
"process": "0.11.10",
"remark-gfm": "4.0.1",
"rimraf": "6.1.2",
"storybook": "10.3.6",
"sass": "1.99.0",
"sass-loader": "16.0.7",
"storybook": "9.1.20",
"style-loader": "4.0.0",
"svgo": "4.0.1",
"tailwindcss": "3.4.18",
@ -164,15 +165,15 @@
"webpack-node-externals": "3.0.0"
},
"dependencies": {
"@angular/animations": "20.3.18",
"@angular/cdk": "20.2.14",
"@angular/common": "20.3.18",
"@angular/compiler": "20.3.18",
"@angular/core": "20.3.18",
"@angular/forms": "20.3.18",
"@angular/platform-browser": "20.3.18",
"@angular/platform-browser-dynamic": "20.3.18",
"@angular/router": "20.3.18",
"@angular/animations": "21.2.11",
"@angular/cdk": "21.2.9",
"@angular/common": "21.2.11",
"@angular/compiler": "21.2.11",
"@angular/core": "21.2.11",
"@angular/forms": "21.2.11",
"@angular/platform-browser": "21.2.11",
"@angular/platform-browser-dynamic": "21.2.11",
"@angular/router": "21.2.11",
"@bitwarden/commercial-sdk-internal": "0.2.0-main.757",
"@bitwarden/desktop-napi": "file:apps/desktop/desktop_native/napi",
"@bitwarden/sdk-internal": "0.2.0-main.757",
@ -182,7 +183,7 @@
"@koa/router": "15.4.0",
"@microsoft/signalr": "10.0.0",
"@microsoft/signalr-protocol-msgpack": "10.0.0",
"@ng-select/ng-select": "20.7.0",
"@ng-select/ng-select": "21.8.2",
"big-integer": "1.6.52",
"braintree-web-drop-in": "1.46.0",
"buffer": "6.0.3",
@ -234,6 +235,9 @@
"react": "18.3.1",
"react-dom": "18.3.1",
"@types/react": "18.3.27",
"@angular-devkit/build-angular": {
"jest-environment-jsdom": "29.7.0"
},
"tmp": ">=0.2.4"
},
"engines": {

View File

@ -2,7 +2,7 @@
"compilerOptions": {
"strict": false,
"pretty": true,
"moduleResolution": "node",
"moduleResolution": "bundler",
"noImplicitAny": true,
"target": "ES2016",
"module": "ES2020",