feat: support Angular 20 (#1220)

Adds Angular 20 support.

This also remove the custom jest setup as it was not compatible with
Angular 20 and uses builtin support for vitest in Angular 20.
For this, Angular has been removed from the jest setup and the pipeline
runs it separately now.

Fixes #1206

---------

Signed-off-by: Lukas Reining <lukas.reining@codecentric.de>
This commit is contained in:
Lukas Reining 2025-07-25 18:48:38 +02:00 committed by GitHub
parent 07af3a9eda
commit aa232a9d6a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 5720 additions and 8515 deletions

View File

@ -37,8 +37,11 @@ jobs:
- name: Lint
run: npm run lint
- name: Test
run: npm run test
- name: Test Jest Projects
run: npm run test:jest
- name: Test Angular SDK
run: npm run test:angular
codecov-and-docs:
runs-on: ubuntu-latest

View File

@ -190,26 +190,6 @@ export default {
],
},
},
{
displayName: 'angular',
testEnvironment: 'jsdom',
preset: 'jest-preset-angular',
testMatch: ['<rootDir>/packages/angular/projects/angular-sdk/src/**/*.spec.{ts,tsx}'],
setupFilesAfterEnv: ['<rootDir>/packages/angular/setup-jest.ts'],
moduleNameMapper: {
'@openfeature/core': '<rootDir>/packages/shared/src',
'@openfeature/web-sdk': '<rootDir>/packages/web/src',
},
transform: {
'^.+\\.(ts|js|html|svg)$': [
'jest-preset-angular',
{
tsconfig: '<rootDir>/packages/angular/tsconfig.json',
isolatedModules: true,
},
],
},
}
],
// Use this configuration option to add custom reporters to Jest

14002
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,9 @@
"private": true,
"description": "OpenFeature SDK for JavaScript",
"scripts": {
"test": "jest --selectProjects=shared --selectProjects=server --selectProjects=web --selectProjects=react --selectProjects=angular --selectProjects=nest --silent",
"test": "npm run test:jest && npm run test:angular",
"test:jest": "jest --selectProjects=shared --selectProjects=server --selectProjects=web --selectProjects=react --selectProjects=nest --silent",
"test:angular": "npm run test:coverage --workspace=packages/angular",
"e2e-server": "git submodule update --init --recursive && shx cp test-harness/features/evaluation.feature packages/server/e2e/features && jest --selectProjects=server-e2e --verbose",
"e2e-web": "git submodule update --init --recursive && shx cp test-harness/features/evaluation.feature packages/web/e2e/features && jest --selectProjects=web-e2e --verbose",
"e2e": "npm run e2e-server && npm run e2e-web",
@ -54,8 +56,6 @@
"jest-environment-jsdom": "^29.7.0",
"jest-environment-node": "^29.7.0",
"jest-junit": "^16.0.0",
"jest-preset-angular": "^14.2.4",
"ng-packagr": "^18.2.1",
"prettier": "^3.2.5",
"react": "^18.2.0",
"rollup": "^4.0.0",
@ -69,9 +69,6 @@
"typescript": "^4.7.4",
"uuid": "^11.0.0"
},
"overrides": {
"typescript": "^4.7.4"
},
"workspaces": [
"packages/shared",
"packages/server",

View File

@ -10,7 +10,7 @@
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"builder": "@angular/build:ng-packagr",
"options": {
"project": "projects/angular-sdk/ng-package.json"
},
@ -32,6 +32,15 @@
"projects/angular-sdk/**/*.html"
]
}
},
"test": {
"builder": "@angular/build:unit-test",
"options": {
"tsConfig": "projects/angular-sdk/tsconfig.spec.json",
"providersFile": "projects/angular-sdk/src/test-provider.ts",
"runner": "vitest",
"buildTarget": "::development"
}
}
}
}

View File

@ -7,37 +7,42 @@
"lint": "ng lint",
"lint:fix": "ng lint --fix",
"watch": "ng build --watch --configuration development",
"test": "jest",
"test": "ng test",
"test:coverage": "ng test --no-watch --code-coverage",
"build": "ng build && npm run postbuild",
"postbuild": "shx cp ./../../LICENSE ./dist/angular/LICENSE",
"publish-if-not-exists": "cp $NPM_CONFIG_USERCONFIG .npmrc && if [ \"$(npm --prefix dist/angular run current-published-version -s)\" = \"$(npm --prefix dist/angular run current-version -s)\" ]; then echo 'already published, skipping'; else cd dist/angular && npm publish --access public; fi"
},
"private": true,
"devDependencies": {
"@angular-devkit/build-angular": "^19.0.0",
"@angular-eslint/builder": "19.3.0",
"@angular-eslint/eslint-plugin": "19.3.0",
"@angular-eslint/eslint-plugin-template": "19.3.0",
"@angular-eslint/schematics": "19.3.0",
"@angular-eslint/template-parser": "19.3.0",
"@angular/animations": "^19.0.0",
"@angular/cli": "^19.0.0",
"@angular/common": "^19.0.0",
"@angular/compiler": "^19.0.0",
"@angular/compiler-cli": "^19.0.0",
"@angular/core": "^19.0.0",
"@angular/forms": "^19.0.0",
"@angular/platform-browser": "^19.0.0",
"@angular/platform-browser-dynamic": "^19.0.0",
"@angular/router": "^19.0.0",
"@angular-eslint/builder": "^20.1.1",
"@angular-eslint/eslint-plugin": "^20.1.1",
"@angular-eslint/eslint-plugin-template": "^20.1.1",
"@angular-eslint/schematics": "^20.1.1",
"@angular-eslint/template-parser": "^20.1.1",
"@angular/animations": "^20.1.1",
"@angular/build": "^20.1.1",
"@angular/cli": "^20.1.1",
"@angular/common": "^20.1.1",
"@angular/compiler": "^20.1.1",
"@angular/compiler-cli": "^20.1.1",
"@angular/core": "^20.1.1",
"@angular/forms": "^20.1.1",
"@angular/platform-browser": "^20.1.1",
"@angular/platform-browser-dynamic": "^20.1.1",
"@angular/router": "^20.1.1",
"@typescript-eslint/eslint-plugin": "7.18.0",
"@typescript-eslint/parser": "7.18.0",
"@vitest/browser": "^3.2.4",
"@vitest/coverage-v8": "^3.2.4",
"eslint": "^8.57.0",
"jest-preset-angular": "^14.2.4",
"ng-packagr": "^19.0.0",
"jsdom": "^26.1.0",
"ng-packagr": "^20.1.0",
"playwright": "^1.53.2",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"typescript": "^5.5.4",
"typescript": "^5.8.3",
"vitest": "^3.2.4",
"zone.js": "~0.15.0"
}
}

View File

@ -0,0 +1,18 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For Angular's browser support policy, please see:
# https://angular.dev/reference/versions#browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
Chrome >= 107
ChromeAndroid >= 107
Edge >= 107
Firefox >= 104
FirefoxAndroid >= 104
Safari >= 16
iOS >= 16

View File

@ -17,18 +17,18 @@
"prepack": "shx cp ./../../../../LICENSE ./LICENSE"
},
"peerDependencies": {
"@angular/common": "^16.2.12 || ^17.3.0 || ^18.0.0 || ^19.0.0",
"@angular/core": "^16.2.12 || ^17.3.0 || ^18.0.0 || ^19.0.0",
"@angular/common": "^16.2.12 || ^17.3.0 || ^18.0.0 || ^19.0.0 || ^20.0.0",
"@angular/core": "^16.2.12 || ^17.3.0 || ^18.0.0 || ^19.0.0 || ^20.0.0",
"@openfeature/web-sdk": "^1.4.1"
},
"dependencies": {
"tslib": "^2.3.0"
},
"devDependencies": {
"@openfeature/core": "*",
"@openfeature/web-sdk": "*",
"@angular/common": "^19.0.0",
"@angular/core": "^19.0.0"
"@openfeature/core": "^1.8.1",
"@openfeature/web-sdk": "^1.5.0",
"@angular/common": "^20.1.2",
"@angular/core": "^20.1.2"
},
"sideEffects": false,
"keywords": [

View File

@ -253,6 +253,7 @@ describe('FeatureFlagDirective', () => {
await expectRenderedText(fixture, 'case-2', 'Flag On');
await updateFlagValue(provider, false);
fixture.detectChanges(); // Ensure change detection after flag update
await expectRenderedText(fixture, 'case-2', 'Flag Off');
});
});
@ -393,7 +394,9 @@ describe('FeatureFlagDirective', () => {
await waitForClientReady(client);
await expectRenderedText(fixture, 'case-6', 'Flag On');
fixture.componentInstance.specialFlagKey = 'new-test-flag';
fixture.componentRef.setInput('specialFlagKey', 'new-test-flag');
await fixture.whenStable();
await expectRenderedText(fixture, 'case-6', 'Flag Off');
});
@ -445,7 +448,9 @@ describe('FeatureFlagDirective', () => {
);
await OpenFeature.setProviderAndWait(newDomain, newProvider);
fixture.componentInstance.domain = newDomain;
fixture.componentRef.setInput('domain', newDomain);
await fixture.whenStable();
await expectRenderedText(fixture, 'case-6', 'Flag Off');
});
});
@ -560,8 +565,7 @@ async function createTestingModule(config?: {
],
}).createComponent(TestComponent);
fixture.componentInstance.domain = domain;
fixture.detectChanges();
fixture.componentRef.setInput('domain', domain);
await fixture.whenStable();
const client = OpenFeature.getClient(domain);

View File

@ -8,6 +8,7 @@ import {
OnInit,
TemplateRef,
ViewContainerRef,
inject,
} from '@angular/core';
import {
Client,
@ -35,6 +36,9 @@ class FeatureFlagDirectiveContext<T extends FlagValue> {
selector: '[featureFlag]',
})
export abstract class FeatureFlagDirective<T extends FlagValue> implements OnInit, OnDestroy, OnChanges {
protected _changeDetectorRef: ChangeDetectorRef;
protected _viewContainerRef: ViewContainerRef;
protected _featureFlagDefault: T;
protected _featureFlagDomain: string | undefined;
@ -64,13 +68,7 @@ export abstract class FeatureFlagDirective<T extends FlagValue> implements OnIni
protected _reconcilingTemplateRef: TemplateRef<FeatureFlagDirectiveContext<T>> | null;
protected _reconcilingViewRef: EmbeddedViewRef<unknown> | null;
protected constructor(
protected _changeDetectorRef: ChangeDetectorRef,
protected _viewContainerRef: ViewContainerRef,
templateRef: TemplateRef<FeatureFlagDirectiveContext<T>>,
) {
this._thenTemplateRef = templateRef;
}
protected constructor() {}
set featureFlagDomain(domain: string | undefined) {
/**
@ -232,6 +230,10 @@ export abstract class FeatureFlagDirective<T extends FlagValue> implements OnIni
selector: '[booleanFeatureFlag]',
})
export class BooleanFeatureFlagDirective extends FeatureFlagDirective<boolean> implements OnChanges {
override _changeDetectorRef = inject(ChangeDetectorRef);
override _viewContainerRef = inject(ViewContainerRef);
override _thenTemplateRef = inject<TemplateRef<FeatureFlagDirectiveContext<boolean>>>(TemplateRef);
/**
* The key of the boolean feature flag.
*/
@ -242,12 +244,8 @@ export class BooleanFeatureFlagDirective extends FeatureFlagDirective<boolean> i
*/
@Input({ required: true }) booleanFeatureFlagDefault: boolean;
constructor(
_changeDetectorRef: ChangeDetectorRef,
_viewContainerRef: ViewContainerRef,
templateRef: TemplateRef<FeatureFlagDirectiveContext<boolean>>,
) {
super(_changeDetectorRef, _viewContainerRef, templateRef);
constructor() {
super();
}
override ngOnChanges() {
@ -347,6 +345,10 @@ export class BooleanFeatureFlagDirective extends FeatureFlagDirective<boolean> i
selector: '[numberFeatureFlag]',
})
export class NumberFeatureFlagDirective extends FeatureFlagDirective<number> implements OnChanges {
override _changeDetectorRef = inject(ChangeDetectorRef);
override _viewContainerRef = inject(ViewContainerRef);
override _thenTemplateRef = inject<TemplateRef<FeatureFlagDirectiveContext<number>>>(TemplateRef);
/**
* The key of the number feature flag.
*/
@ -362,12 +364,8 @@ export class NumberFeatureFlagDirective extends FeatureFlagDirective<number> imp
*/
@Input({ required: false }) numberFeatureFlagValue?: number;
constructor(
_changeDetectorRef: ChangeDetectorRef,
_viewContainerRef: ViewContainerRef,
templateRef: TemplateRef<FeatureFlagDirectiveContext<number>>,
) {
super(_changeDetectorRef, _viewContainerRef, templateRef);
constructor() {
super();
}
override ngOnChanges() {
@ -467,6 +465,10 @@ export class NumberFeatureFlagDirective extends FeatureFlagDirective<number> imp
selector: '[stringFeatureFlag]',
})
export class StringFeatureFlagDirective extends FeatureFlagDirective<string> implements OnChanges {
override _changeDetectorRef = inject(ChangeDetectorRef);
override _viewContainerRef = inject(ViewContainerRef);
override _thenTemplateRef = inject<TemplateRef<FeatureFlagDirectiveContext<string>>>(TemplateRef);
/**
* The key of the string feature flag.
*/
@ -482,12 +484,8 @@ export class StringFeatureFlagDirective extends FeatureFlagDirective<string> imp
*/
@Input({ required: false }) stringFeatureFlagValue?: string;
constructor(
_changeDetectorRef: ChangeDetectorRef,
_viewContainerRef: ViewContainerRef,
templateRef: TemplateRef<FeatureFlagDirectiveContext<string>>,
) {
super(_changeDetectorRef, _viewContainerRef, templateRef);
constructor() {
super();
}
override ngOnChanges() {
@ -587,6 +585,10 @@ export class StringFeatureFlagDirective extends FeatureFlagDirective<string> imp
selector: '[objectFeatureFlag]',
})
export class ObjectFeatureFlagDirective<T extends JsonValue> extends FeatureFlagDirective<T> implements OnChanges {
override _changeDetectorRef = inject(ChangeDetectorRef);
override _viewContainerRef = inject(ViewContainerRef);
override _thenTemplateRef = inject<TemplateRef<FeatureFlagDirectiveContext<T>>>(TemplateRef);
/**
* The key of the object feature flag.
*/
@ -602,12 +604,8 @@ export class ObjectFeatureFlagDirective<T extends JsonValue> extends FeatureFlag
*/
@Input({ required: false }) objectFeatureFlagValue?: T;
constructor(
_changeDetectorRef: ChangeDetectorRef,
_viewContainerRef: ViewContainerRef,
templateRef: TemplateRef<FeatureFlagDirectiveContext<T>>,
) {
super(_changeDetectorRef, _viewContainerRef, templateRef);
constructor() {
super();
}
override ngOnChanges() {

View File

@ -0,0 +1,3 @@
import { provideZonelessChangeDetection } from '@angular/core';
export default [provideZonelessChangeDetection()];

View File

@ -3,15 +3,20 @@
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jest",
"vitest/globals",
"node"
],
"paths": {
"angular": [
"./dist/angular"
]
},
"esModuleInterop": true,
"emitDecoratorMetadata": true
},
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts",
"setup-jest.ts"
"src/test-provider.ts"
]
}

View File

@ -1,5 +0,0 @@
import 'jest-preset-angular/setup-jest';
import { TestBed } from '@angular/core/testing';
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());

View File

@ -23,8 +23,8 @@
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
"target": "ES2015",
"module": "ES2015",
"target": "ES2022",
"module": "ES2022",
"useDefineForClassFields": false,
"strictNullChecks": false,
"lib": [

View File

@ -3,13 +3,14 @@
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jest"
"vitest/globals",
"node"
],
"esModuleInterop": true,
"emitDecoratorMetadata": true
},
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
"projects/angular-sdk/src/**/*.spec.ts",
"projects/angular-sdk/src/**/*.d.ts"
]
}

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
},
},
});

View File

@ -18,3 +18,4 @@ export abstract class OpenFeatureError extends Error {
this.cause = options?.cause;
}
}