From e701baedd3fd1a05700ffe4a1fc01621fd6237c7 Mon Sep 17 00:00:00 2001 From: Doug Parker Date: Tue, 1 Jul 2025 14:29:02 -0700 Subject: [PATCH 01/65] release: cut the v20.1.0-rc.0 release --- CHANGELOG.md | 23 ++++++++++++++++++----- package.json | 2 +- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e678d91cd723..91445ecb29a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ + + +# 20.1.0-rc.0 (2025-07-01) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------- | +| [f1d41b069](https://github.com/angular/angular-cli/commit/f1d41b069db6cd3ab83561113567b8e5f4bf25d8) | fix | remove unused `@vitejs/plugin-basic-ssl` dependency | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------- | +| [73f57f3c9](https://github.com/angular/angular-cli/commit/73f57f3c9e0d9434c0f8507508dfe30f6a402861) | fix | proxy karma request from `/` to `/base` | + + + # 20.0.5 (2025-07-01) @@ -1302,7 +1320,6 @@ - Protractor is no longer supported. Protractor was marked end-of-life in August 2023 (see https://protractortest.org/). Projects still relying on Protractor should consider migrating to another E2E testing framework, several support solid migration paths from Protractor. - - https://angular.dev/tools/cli/end-to-end - https://blog.angular.dev/the-state-of-end-to-end-testing-with-angular-d175f751cb9c @@ -4113,7 +4130,6 @@ Alan Agius, Charles Lyding, Doug Parker, Joey Perrott and Piotr Wysocki ```scss @import 'font-awesome/scss/font-awesome'; ``` - - By default the CLI will use Sass modern API, While not recommended, users can still opt to use legacy API by setting `NG_BUILD_LEGACY_SASS=1`. - Internally the Angular CLI now always set the TypeScript `target` to `ES2022` and `useDefineForClassFields` to `false` unless the target is set to `ES2022` or later in the TypeScript configuration. To control ECMA version and features use the Browerslist configuration. @@ -4937,7 +4953,6 @@ Alan Agius, Charles Lyding and Doug Parker ### @angular/cli - Several changes to the `ng analytics` command syntax. - - `ng analytics project ` has been replaced with `ng analytics ` - `ng analytics ` has been replaced with `ng analytics --global` @@ -4967,7 +4982,6 @@ Alan Agius, Charles Lyding and Doug Parker - `browser` and `karma` builders `script` and `styles` options input files extensions are now validated. Valid extensions for `scripts` are: - - `.js` - `.cjs` - `.mjs` @@ -4976,7 +4990,6 @@ Alan Agius, Charles Lyding and Doug Parker - `.mjsx` Valid extensions for `styles` are: - - `.css` - `.less` - `.sass` diff --git a/package.json b/package.json index 1113c6151f4c..fecfd42ac53f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@angular/devkit-repo", - "version": "20.1.0-next.3", + "version": "20.1.0-rc.0", "private": true, "description": "Software Development Kit for Angular", "keywords": [ From 71a7ecd984a573a664fef213ad35c7a3aef6b6ff Mon Sep 17 00:00:00 2001 From: Doug Parker Date: Wed, 2 Jul 2025 10:39:40 -0700 Subject: [PATCH 02/65] test: raise timeout of `//packages/ngtools/webpack:test` This seems to be timing out occasionally in CI. (cherry picked from commit d96a0933deffaf7ade5326caeef38e0c8566a402) --- packages/ngtools/webpack/BUILD.bazel | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ngtools/webpack/BUILD.bazel b/packages/ngtools/webpack/BUILD.bazel index eb75d42a0185..332e8a39cc1a 100644 --- a/packages/ngtools/webpack/BUILD.bazel +++ b/packages/ngtools/webpack/BUILD.bazel @@ -57,6 +57,7 @@ ts_project( jasmine_test( name = "test", + size = "medium", data = [ ":webpack_test_lib", # Needed at runtime for runtime TS compilations performed by tests. From 343ae2d4c90d0df8e6d1e82db64449ea060cfee2 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 3 Jul 2025 09:15:47 +0200 Subject: [PATCH 03/65] fix(@angular/build): correctly remap Angular diagnostics The esbuild builder has some logic that re-map diagnostics coming from the compiler, depending on their `source` and `code`. Currently this is incorrect, because it assumed that a value of `ngtsc` for `source` always means that the error is from the Angular compiler, but what it actually means is that it comes from an Angular template diagnostics. Furthermore, we can't rely on a `source` always being defined. These changes align the logic to a similar one we already have in the compiler where we assume the diagnostic comes from Angular if it starts with `-99`. (cherry picked from commit 1cde40e38a22c406c777abc013311e490c3781b3) --- .../build/src/tools/esbuild/angular/diagnostics.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/angular/build/src/tools/esbuild/angular/diagnostics.ts b/packages/angular/build/src/tools/esbuild/angular/diagnostics.ts index fab71ee848d2..bf85b69f707c 100644 --- a/packages/angular/build/src/tools/esbuild/angular/diagnostics.ts +++ b/packages/angular/build/src/tools/esbuild/angular/diagnostics.ts @@ -75,9 +75,13 @@ export function convertTypeScriptDiagnostic( ): PartialMessage { let codePrefix = 'TS'; let code = `${diagnostic.code}`; - if (diagnostic.source === 'ngtsc') { + + // Custom ngtsc diagnostics are prefixed with -99 which isn't a valid TypeScript diagnostic code. + // Strip it and mark the diagnostic as coming from Angular. Note that we can't rely on + // `diagnostic.source`, because it isn't always produced. This is identical to: + // https://github.com/angular/angular/blob/main/packages/compiler-cli/src/ngtsc/diagnostics/src/util.ts + if (code.startsWith('-99')) { codePrefix = 'NG'; - // Remove `-99` Angular prefix from diagnostic code code = code.slice(3); } From e869021285de0e7cebd353a351668fe40608eeaf Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Thu, 3 Jul 2025 10:37:58 +0000 Subject: [PATCH 04/65] fix(@angular/build): failed to proxy error for assets Remove proxy to handle `/` to `/base/` instead update the `AngularAssetsMiddleware` Closes #30639 (cherry picked from commit 1aeefa73577342e00a2ae3f9833ac4ca8ee81f36) --- .../src/builders/karma/application_builder.ts | 12 ++- .../angular/build/src/builders/karma/index.ts | 3 - .../karma/tests/options/scripts_spec.ts | 74 +++++++++++++++++++ 3 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 packages/angular/build/src/builders/karma/tests/options/scripts_spec.ts diff --git a/packages/angular/build/src/builders/karma/application_builder.ts b/packages/angular/build/src/builders/karma/application_builder.ts index d33469a45ef6..3dd968006684 100644 --- a/packages/angular/build/src/builders/karma/application_builder.ts +++ b/packages/angular/build/src/builders/karma/application_builder.ts @@ -84,13 +84,22 @@ class AngularAssetsMiddleware { return; } + // Implementation of serverFile can be found here: + // https://github.com/karma-runner/karma/blob/84f85e7016efc2266fa6b3465f494a3fa151c85c/lib/middleware/common.js#L10 switch (file.origin) { case 'disk': this.serveFile(file.inputPath, undefined, res, undefined, undefined, /* doNotCache */ true); break; case 'memory': // Include pathname to help with Content-Type headers. - this.serveFile(`/unused/${url.pathname}`, undefined, res, undefined, file.contents, true); + this.serveFile( + `/unused/${url.pathname}`, + undefined, + res, + undefined, + file.contents, + /* doNotCache */ false, + ); break; } } @@ -508,6 +517,7 @@ async function initializeApplication( scriptsFiles.push({ pattern: `${outputPath}/${outputName}`, watched: false, + included: typeof scriptEntry === 'string' ? true : scriptEntry.inject !== false, type: 'js', }); } diff --git a/packages/angular/build/src/builders/karma/index.ts b/packages/angular/build/src/builders/karma/index.ts index d3aefeeff628..036d503cfdf6 100644 --- a/packages/angular/build/src/builders/karma/index.ts +++ b/packages/angular/build/src/builders/karma/index.ts @@ -107,9 +107,6 @@ function getBuiltInKarmaConfig( 'karma-jasmine-html-reporter', 'karma-coverage', ].map((p) => workspaceRootRequire(p)), - proxies: { - '/': '/base/', - }, jasmineHtmlReporter: { suppressAll: true, // removes the duplicated traces }, diff --git a/packages/angular/build/src/builders/karma/tests/options/scripts_spec.ts b/packages/angular/build/src/builders/karma/tests/options/scripts_spec.ts new file mode 100644 index 000000000000..483a05929e1c --- /dev/null +++ b/packages/angular/build/src/builders/karma/tests/options/scripts_spec.ts @@ -0,0 +1,74 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { execute } from '../../index'; +import { BASE_OPTIONS, KARMA_BUILDER_INFO, describeKarmaBuilder } from '../setup'; + +describeKarmaBuilder(execute, KARMA_BUILDER_INFO, (harness, setupTarget) => { + describe('Option: "scripts"', () => { + beforeEach(async () => { + await setupTarget(harness); + }); + + it(`should be able to access non injected script`, async () => { + await harness.writeFiles({ + 'src/test.js': `console.log('hello from test script.')`, + 'src/app/app.component.ts': ` + import { Component } from '@angular/core'; + + @Component({ + selector: 'app-root', + standalone: false, + template: '

Hello World

' + }) + export class AppComponent { + loadScript() { + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.onload = () => resolve(); + script.onerror = reject; + script.src = 'test.js'; + document.body.appendChild(script); + }); + } + } + `, + 'src/app/app.component.spec.ts': ` + import { TestBed } from '@angular/core/testing'; + import { AppComponent } from './app.component'; + + describe('AppComponent', () => { + beforeEach(() => TestBed.configureTestingModule({ + declarations: [AppComponent] + })); + + it('should load script', async () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + + await expectAsync(fixture.componentInstance.loadScript()).toBeResolved(); + }); + });`, + }); + + harness.useTarget('test', { + ...BASE_OPTIONS, + scripts: [ + { + input: 'src/test.js', + bundleName: 'test', + inject: false, + }, + ], + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + }); + }); +}); From 28bac3b8101c9b521cd17bb0287ca4ba72c32c1e Mon Sep 17 00:00:00 2001 From: cexbrayat Date: Thu, 3 Jul 2025 10:57:21 +0200 Subject: [PATCH 05/65] fix(@schematics/angular): remove constructor from service template Now that the style guide recommends to use `inject()`, having a constructor in a service is not really useful. (cherry picked from commit cefea43cea1ff7138f05cdef8202d77f403a382a) --- .../files/__name@dasherize__.__type@dasherize__.ts.template | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/schematics/angular/service/files/__name@dasherize__.__type@dasherize__.ts.template b/packages/schematics/angular/service/files/__name@dasherize__.__type@dasherize__.ts.template index ad3685368077..5c104786d178 100644 --- a/packages/schematics/angular/service/files/__name@dasherize__.__type@dasherize__.ts.template +++ b/packages/schematics/angular/service/files/__name@dasherize__.__type@dasherize__.ts.template @@ -4,6 +4,5 @@ import { Injectable } from '@angular/core'; providedIn: 'root' }) export class <%= classify(name) %><%= classify(type) %> { - - constructor() { } + } From 45f5a4784586f7fba51e6319e82f91046ee79b0f Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Thu, 3 Jul 2025 14:10:31 +0000 Subject: [PATCH 06/65] build: update bazel commands to use pnpm `yarn` is no longer used. (cherry picked from commit cad159402760163c8fd7c724f2d0f12cdefbc5a4) --- packages/angular/ssr/test/npm_package/BUILD.bazel | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/angular/ssr/test/npm_package/BUILD.bazel b/packages/angular/ssr/test/npm_package/BUILD.bazel index 97331cf8a9e0..d79cbf7d0105 100644 --- a/packages/angular/ssr/test/npm_package/BUILD.bazel +++ b/packages/angular/ssr/test/npm_package/BUILD.bazel @@ -37,7 +37,7 @@ diff_test( failure_message = """ To accept the new golden file, execute: - yarn bazel run //packages/angular/ssr/test/npm_package:beasties_license_test.accept + pnpm bazel run //packages/angular/ssr/test/npm_package:beasties_license_test.accept """, file1 = ":THIRD_PARTY_LICENSES.txt.golden", file2 = ":beasties_license_file", @@ -50,7 +50,7 @@ write_file( [ "#!/usr/bin/env bash", "cd ${BUILD_WORKSPACE_DIRECTORY}", - "yarn bazel build //packages/angular/ssr:npm_package", + "pnpm bazel build //packages/angular/ssr:npm_package", "cp -fv dist/bin/packages/angular/ssr/npm_package/third_party/beasties/THIRD_PARTY_LICENSES.txt packages/angular/ssr/test/npm_package/THIRD_PARTY_LICENSES.txt.golden", ], is_executable = True, From 8879716cac9b2134db2795b1810595ea56e9d421 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Thu, 3 Jul 2025 12:35:47 +0000 Subject: [PATCH 07/65] fix(@angular/build): expose unit test and karma builder API This commit exposes the Karma and Unit test builder API Closes: #30644 (cherry picked from commit d0bdf20ff505cbfcf647c58a33d6362a95ac368a) --- goldens/public-api/angular/build/index.api.md | 60 +++++++++++++++++++ .../build/src/builders/unit-test/builder.ts | 14 +++-- .../build/src/builders/unit-test/index.ts | 6 +- .../src/builders/unit-test/karma-bridge.ts | 4 +- .../build/src/builders/unit-test/options.ts | 6 +- packages/angular/build/src/index.ts | 6 ++ 6 files changed, 83 insertions(+), 13 deletions(-) diff --git a/goldens/public-api/angular/build/index.api.md b/goldens/public-api/angular/build/index.api.md index aa9ac693864e..6e01517ec550 100644 --- a/goldens/public-api/angular/build/index.api.md +++ b/goldens/public-api/angular/build/index.api.md @@ -6,6 +6,7 @@ import { BuilderContext } from '@angular-devkit/architect'; import { BuilderOutput } from '@angular-devkit/architect'; +import type { ConfigOptions } from 'karma'; import type http from 'node:http'; import { OutputFile } from 'esbuild'; import type { Plugin as Plugin_2 } from 'esbuild'; @@ -151,9 +152,17 @@ export function executeDevServerBuilder(options: DevServerBuilderOptions, contex // @public export function executeExtractI18nBuilder(options: ExtractI18nBuilderOptions, context: BuilderContext, extensions?: ApplicationBuilderExtensions): Promise; +// @public +export function executeKarmaBuilder(options: KarmaBuilderOptions, context: BuilderContext, transforms?: { + karmaOptions?: (options: KarmaConfigOptions) => KarmaConfigOptions; +}): AsyncIterable; + // @public export function executeNgPackagrBuilder(options: NgPackagrBuilderOptions, context: BuilderContext): AsyncIterableIterator; +// @public +export function executeUnitTestBuilder(options: UnitTestBuilderOptions, context: BuilderContext, extensions?: ApplicationBuilderExtensions): AsyncIterable; + // @public export type ExtractI18nBuilderOptions = { buildTarget?: string; @@ -164,6 +173,40 @@ export type ExtractI18nBuilderOptions = { progress?: boolean; }; +// @public +export type KarmaBuilderOptions = { + aot?: boolean; + assets?: AssetPattern_2[]; + browsers?: Browsers; + codeCoverage?: boolean; + codeCoverageExclude?: string[]; + define?: { + [key: string]: string; + }; + exclude?: string[]; + externalDependencies?: string[]; + fileReplacements?: FileReplacement_2[]; + include?: string[]; + inlineStyleLanguage?: InlineStyleLanguage_2; + karmaConfig?: string; + loader?: { + [key: string]: any; + }; + main?: string; + poll?: number; + polyfills?: string[]; + preserveSymlinks?: boolean; + progress?: boolean; + reporters?: string[]; + scripts?: ScriptElement_2[]; + sourceMap?: SourceMapUnion_2; + stylePreprocessorOptions?: StylePreprocessorOptions_2; + styles?: StyleElement_2[]; + tsConfig: string; + watch?: boolean; + webWorkerTsConfig?: string; +}; + // @public export type NgPackagrBuilderOptions = { poll?: number; @@ -172,6 +215,23 @@ export type NgPackagrBuilderOptions = { watch?: boolean; }; +// @public +export type UnitTestBuilderOptions = { + browsers?: string[]; + buildTarget: string; + codeCoverage?: boolean; + codeCoverageExclude?: string[]; + codeCoverageReporters?: SchemaCodeCoverageReporter[]; + debug?: boolean; + exclude?: string[]; + include?: string[]; + providersFile?: string; + reporters?: string[]; + runner: Runner; + tsConfig: string; + watch?: boolean; +}; + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/angular/build/src/builders/unit-test/builder.ts b/packages/angular/build/src/builders/unit-test/builder.ts index 2df5aed0eabb..f056dd4d10b0 100644 --- a/packages/angular/build/src/builders/unit-test/builder.ts +++ b/packages/angular/build/src/builders/unit-test/builder.ts @@ -24,10 +24,14 @@ import { OutputHashing } from '../application/schema'; import { writeTestFiles } from '../karma/application_builder'; import { findTests, getTestEntrypoints } from '../karma/find-tests'; import { useKarmaBuilder } from './karma-bridge'; -import { NormalizedUnitTestOptions, injectTestingPolyfills, normalizeOptions } from './options'; -import type { Schema as UnitTestOptions } from './schema'; +import { + NormalizedUnitTestBuilderOptions, + injectTestingPolyfills, + normalizeOptions, +} from './options'; +import type { Schema as UnitTestBuilderOptions } from './schema'; -export type { UnitTestOptions }; +export type { UnitTestBuilderOptions }; type VitestCoverageOption = Exclude; @@ -36,7 +40,7 @@ type VitestCoverageOption = Exclude { @@ -383,7 +387,7 @@ function generateOutputPath(): string { return path.join('dist', 'test-out', `${datePrefix}-${uuidSuffix}`); } function generateCoverageOption( - codeCoverage: NormalizedUnitTestOptions['codeCoverage'], + codeCoverage: NormalizedUnitTestBuilderOptions['codeCoverage'], workspaceRoot: string, outputPath: string, ): VitestCoverageOption { diff --git a/packages/angular/build/src/builders/unit-test/index.ts b/packages/angular/build/src/builders/unit-test/index.ts index bd325ec661fa..47666cdc1067 100644 --- a/packages/angular/build/src/builders/unit-test/index.ts +++ b/packages/angular/build/src/builders/unit-test/index.ts @@ -7,10 +7,10 @@ */ import { type Builder, createBuilder } from '@angular-devkit/architect'; -import { type UnitTestOptions, execute } from './builder'; +import { type UnitTestBuilderOptions, execute } from './builder'; -export { type UnitTestOptions, execute }; +export { type UnitTestBuilderOptions, execute }; -const builder: Builder = createBuilder(execute); +const builder: Builder = createBuilder(execute); export default builder; diff --git a/packages/angular/build/src/builders/unit-test/karma-bridge.ts b/packages/angular/build/src/builders/unit-test/karma-bridge.ts index ece522f59601..4fa7b085802c 100644 --- a/packages/angular/build/src/builders/unit-test/karma-bridge.ts +++ b/packages/angular/build/src/builders/unit-test/karma-bridge.ts @@ -9,11 +9,11 @@ import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect'; import type { ApplicationBuilderInternalOptions } from '../application/options'; import type { KarmaBuilderOptions } from '../karma'; -import { type NormalizedUnitTestOptions, injectTestingPolyfills } from './options'; +import { type NormalizedUnitTestBuilderOptions, injectTestingPolyfills } from './options'; export async function useKarmaBuilder( context: BuilderContext, - unitTestOptions: NormalizedUnitTestOptions, + unitTestOptions: NormalizedUnitTestBuilderOptions, ): Promise> { if (unitTestOptions.debug) { context.logger.warn( diff --git a/packages/angular/build/src/builders/unit-test/options.ts b/packages/angular/build/src/builders/unit-test/options.ts index 5363d8916d97..43147a23c065 100644 --- a/packages/angular/build/src/builders/unit-test/options.ts +++ b/packages/angular/build/src/builders/unit-test/options.ts @@ -11,14 +11,14 @@ import path from 'node:path'; import { normalizeCacheOptions } from '../../utils/normalize-cache'; import { getProjectRootPaths } from '../../utils/project-metadata'; import { isTTY } from '../../utils/tty'; -import type { Schema as UnitTestOptions } from './schema'; +import type { Schema as UnitTestBuilderOptions } from './schema'; -export type NormalizedUnitTestOptions = Awaited>; +export type NormalizedUnitTestBuilderOptions = Awaited>; export async function normalizeOptions( context: BuilderContext, projectName: string, - options: UnitTestOptions, + options: UnitTestBuilderOptions, ) { // Setup base paths based on workspace root and project information const workspaceRoot = context.workspaceRoot; diff --git a/packages/angular/build/src/index.ts b/packages/angular/build/src/index.ts index a27e1f5ae9d2..2831f59a38f4 100644 --- a/packages/angular/build/src/index.ts +++ b/packages/angular/build/src/index.ts @@ -26,3 +26,9 @@ export { execute as executeNgPackagrBuilder, type NgPackagrBuilderOptions, } from './builders/ng-packagr'; +export { + execute as executeUnitTestBuilder, + type UnitTestBuilderOptions, +} from './builders/unit-test'; + +export { execute as executeKarmaBuilder, type KarmaBuilderOptions } from './builders/karma'; From b67fdfd6bc422bd6a46db923470579c760c5ec27 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Fri, 4 Jul 2025 10:28:06 +0000 Subject: [PATCH 08/65] fix(@angular/build): resolve "Controller is already closed" error in Karma This issue stems from the `Some of your tests did a full page reload` error, which occurs because `karmaOptions.client.clearContext` is set to `true` when `singleRun` is `false`, even though Karma is running in watch mode. Previously, the `watch` flag was configured inconsistently across multiple locations, leading to misalignment. Closes #30506 (cherry picked from commit 49676c80bfe608c307304b837075e810be3de07d) --- .../src/builders/karma/application_builder.ts | 14 ++++---- .../angular/build/src/builders/karma/index.ts | 28 +++++---------- .../build/src/builders/karma/options.ts | 36 +++++++++++++++++++ .../build/src/builders/karma/schema.json | 3 +- 4 files changed, 54 insertions(+), 27 deletions(-) create mode 100644 packages/angular/build/src/builders/karma/options.ts diff --git a/packages/angular/build/src/builders/karma/application_builder.ts b/packages/angular/build/src/builders/karma/application_builder.ts index 3dd968006684..534c239da573 100644 --- a/packages/angular/build/src/builders/karma/application_builder.ts +++ b/packages/angular/build/src/builders/karma/application_builder.ts @@ -24,7 +24,7 @@ import { ApplicationBuilderInternalOptions } from '../application/options'; import { Result, ResultFile, ResultKind } from '../application/results'; import { OutputHashing } from '../application/schema'; import { findTests, getTestEntrypoints } from './find-tests'; -import { Schema as KarmaBuilderOptions } from './schema'; +import { NormalizedKarmaBuilderOptions } from './options'; const localResolve = createRequire(__filename).resolve; const isWindows = process.platform === 'win32'; @@ -275,7 +275,7 @@ function injectKarmaReporter( } export function execute( - options: KarmaBuilderOptions, + options: NormalizedKarmaBuilderOptions, context: BuilderContext, karmaOptions: ConfigOptions, transforms: { @@ -359,14 +359,14 @@ function normalizePolyfills(polyfills: string | string[] | undefined): [string[] } async function collectEntrypoints( - options: KarmaBuilderOptions, + options: NormalizedKarmaBuilderOptions, context: BuilderContext, projectSourceRoot: string, ): Promise> { // Glob for files to test. const testFiles = await findTests( - options.include ?? [], - options.exclude ?? [], + options.include, + options.exclude, context.workspaceRoot, projectSourceRoot, ); @@ -376,7 +376,7 @@ async function collectEntrypoints( // eslint-disable-next-line max-lines-per-function async function initializeApplication( - options: KarmaBuilderOptions, + options: NormalizedKarmaBuilderOptions, context: BuilderContext, karmaOptions: ConfigOptions, transforms: { @@ -435,7 +435,7 @@ async function initializeApplication( scripts: options.scripts, polyfills, webWorkerTsConfig: options.webWorkerTsConfig, - watch: options.watch ?? !karmaOptions.singleRun, + watch: options.watch, stylePreprocessorOptions: options.stylePreprocessorOptions, inlineStyleLanguage: options.inlineStyleLanguage, fileReplacements: options.fileReplacements, diff --git a/packages/angular/build/src/builders/karma/index.ts b/packages/angular/build/src/builders/karma/index.ts index 036d503cfdf6..295d887e19d8 100644 --- a/packages/angular/build/src/builders/karma/index.ts +++ b/packages/angular/build/src/builders/karma/index.ts @@ -15,6 +15,7 @@ import { import type { ConfigOptions } from 'karma'; import { createRequire } from 'node:module'; import path from 'node:path'; +import { NormalizedKarmaBuilderOptions, normalizeOptions } from './options'; import type { Schema as KarmaBuilderOptions } from './schema'; export type KarmaConfigOptions = ConfigOptions & { @@ -34,19 +35,17 @@ export async function* execute( } = {}, ): AsyncIterable { const { execute } = await import('./application_builder'); - const karmaOptions = getBaseKarmaOptions(options, context); + const normalizedOptions = normalizeOptions(options); + const karmaOptions = getBaseKarmaOptions(normalizedOptions, context); - yield* execute(options, context, karmaOptions, transforms); + yield* execute(normalizedOptions, context, karmaOptions, transforms); } function getBaseKarmaOptions( - options: KarmaBuilderOptions, + options: NormalizedKarmaBuilderOptions, context: BuilderContext, ): KarmaConfigOptions { - let singleRun: boolean | undefined; - if (options.watch !== undefined) { - singleRun = !options.watch; - } + const singleRun = !options.watch; // Determine project name from builder context target const projectName = context.target?.project; @@ -67,21 +66,12 @@ function getBaseKarmaOptions( karmaOptions.client.clearContext ??= singleRun ?? false; // `singleRun` defaults to `false` per Karma docs. // Convert browsers from a string to an array - if (typeof options.browsers === 'string' && options.browsers) { - karmaOptions.browsers = options.browsers.split(',').map((browser) => browser.trim()); - } else if (options.browsers === false) { - karmaOptions.browsers = []; + if (options.browsers) { + karmaOptions.browsers = options.browsers; } if (options.reporters) { - // Split along commas to make it more natural, and remove empty strings. - const reporters = options.reporters - .reduce((acc, curr) => acc.concat(curr.split(',')), []) - .filter((x) => !!x); - - if (reporters.length > 0) { - karmaOptions.reporters = reporters; - } + karmaOptions.reporters = options.reporters; } return karmaOptions; diff --git a/packages/angular/build/src/builders/karma/options.ts b/packages/angular/build/src/builders/karma/options.ts new file mode 100644 index 000000000000..f8dfa1e8e0b0 --- /dev/null +++ b/packages/angular/build/src/builders/karma/options.ts @@ -0,0 +1,36 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { Schema as KarmaBuilderOptions } from './schema'; + +export type NormalizedKarmaBuilderOptions = Awaited>; + +export function normalizeOptions(options: KarmaBuilderOptions) { + const { watch = true, include = [], exclude = [], reporters = [], browsers, ...rest } = options; + + let normalizedBrowsers: string[] | undefined; + if (typeof options.browsers === 'string' && options.browsers) { + normalizedBrowsers = options.browsers.split(',').map((browser) => browser.trim()); + } else if (options.browsers === false) { + normalizedBrowsers = []; + } + + // Split along commas to make it more natural, and remove empty strings. + const normalizedReporters = reporters + .reduce((acc, curr) => acc.concat(curr.split(',')), []) + .filter((x) => !!x); + + return { + reporters: normalizedReporters.length ? normalizedReporters : undefined, + browsers: normalizedBrowsers, + watch, + include, + exclude, + ...rest, + }; +} diff --git a/packages/angular/build/src/builders/karma/schema.json b/packages/angular/build/src/builders/karma/schema.json index d24313ba29a6..6a4ab981a4d0 100644 --- a/packages/angular/build/src/builders/karma/schema.json +++ b/packages/angular/build/src/builders/karma/schema.json @@ -229,7 +229,8 @@ }, "watch": { "type": "boolean", - "description": "Run build when files change." + "description": "Run build when files change.", + "default": true }, "poll": { "type": "number", From 45c4413c18f2a576f5c627d4137ff8cbbe82929b Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Fri, 4 Jul 2025 11:09:34 +0000 Subject: [PATCH 09/65] docs: update unit tests watch description Descriptions for the "watch" option where not clear. (cherry picked from commit 8df6fcd43f4ca57594a3e483d9d0435bf9f5e33b) --- packages/angular/build/src/builders/karma/schema.json | 2 +- packages/angular/build/src/builders/unit-test/schema.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/angular/build/src/builders/karma/schema.json b/packages/angular/build/src/builders/karma/schema.json index 6a4ab981a4d0..325f4298f779 100644 --- a/packages/angular/build/src/builders/karma/schema.json +++ b/packages/angular/build/src/builders/karma/schema.json @@ -229,7 +229,7 @@ }, "watch": { "type": "boolean", - "description": "Run build when files change.", + "description": "Re-run tests when source files change.", "default": true }, "poll": { diff --git a/packages/angular/build/src/builders/unit-test/schema.json b/packages/angular/build/src/builders/unit-test/schema.json index 93ca22b5003f..7c54babe0fda 100644 --- a/packages/angular/build/src/builders/unit-test/schema.json +++ b/packages/angular/build/src/builders/unit-test/schema.json @@ -44,7 +44,7 @@ }, "watch": { "type": "boolean", - "description": "Run build when files change." + "description": "Re-run tests when source files change. Defaults to `true` in TTY environments and `false` otherwise." }, "debug": { "type": "boolean", From 462bb3e70f1516b071e0f58e64e929a2483c99eb Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Mon, 7 Jul 2025 11:22:57 +0000 Subject: [PATCH 10/65] refactor(@angular/build): clean up duplicate of code in the karma builders This ensures a cleaner and easier to follow code and also avoid having the fix bugs in multiple places. (cherry picked from commit b160819fa3517b4935dfe37e538d5f144fbea84e) --- goldens/public-api/angular/build/index.api.md | 4 +- .../src/builders/karma/application_builder.ts | 124 ++++++++++++++---- .../angular/build/src/builders/karma/index.ts | 102 +------------- .../build/src/builders/karma/options.ts | 30 ++++- packages/angular/build/src/private.ts | 1 - .../build_angular/src/builders/karma/index.ts | 49 ++++--- 6 files changed, 157 insertions(+), 153 deletions(-) diff --git a/goldens/public-api/angular/build/index.api.md b/goldens/public-api/angular/build/index.api.md index 6e01517ec550..6aab303b4c9e 100644 --- a/goldens/public-api/angular/build/index.api.md +++ b/goldens/public-api/angular/build/index.api.md @@ -153,9 +153,7 @@ export function executeDevServerBuilder(options: DevServerBuilderOptions, contex export function executeExtractI18nBuilder(options: ExtractI18nBuilderOptions, context: BuilderContext, extensions?: ApplicationBuilderExtensions): Promise; // @public -export function executeKarmaBuilder(options: KarmaBuilderOptions, context: BuilderContext, transforms?: { - karmaOptions?: (options: KarmaConfigOptions) => KarmaConfigOptions; -}): AsyncIterable; +export function executeKarmaBuilder(options: KarmaBuilderOptions, context: BuilderContext, transforms?: KarmaBuilderTransformsOptions): AsyncIterable; // @public export function executeNgPackagrBuilder(options: NgPackagrBuilderOptions, context: BuilderContext): AsyncIterableIterator; diff --git a/packages/angular/build/src/builders/karma/application_builder.ts b/packages/angular/build/src/builders/karma/application_builder.ts index 534c239da573..ae238b7239cf 100644 --- a/packages/angular/build/src/builders/karma/application_builder.ts +++ b/packages/angular/build/src/builders/karma/application_builder.ts @@ -12,7 +12,7 @@ import { randomUUID } from 'node:crypto'; import * as fs from 'node:fs/promises'; import type { IncomingMessage, ServerResponse } from 'node:http'; import { createRequire } from 'node:module'; -import * as path from 'node:path'; +import path from 'node:path'; import { ReadableStreamController } from 'node:stream/web'; import { globSync } from 'tinyglobby'; import { BuildOutputFileType } from '../../tools/esbuild/bundler-context'; @@ -24,7 +24,9 @@ import { ApplicationBuilderInternalOptions } from '../application/options'; import { Result, ResultFile, ResultKind } from '../application/results'; import { OutputHashing } from '../application/schema'; import { findTests, getTestEntrypoints } from './find-tests'; -import { NormalizedKarmaBuilderOptions } from './options'; +import { NormalizedKarmaBuilderOptions, normalizeOptions } from './options'; +import { Schema as KarmaBuilderOptions } from './schema'; +import type { KarmaBuilderTransformsOptions } from './index'; const localResolve = createRequire(__filename).resolve; const isWindows = process.platform === 'win32'; @@ -275,21 +277,20 @@ function injectKarmaReporter( } export function execute( - options: NormalizedKarmaBuilderOptions, + options: KarmaBuilderOptions, context: BuilderContext, - karmaOptions: ConfigOptions, - transforms: { - // The karma options transform cannot be async without a refactor of the builder implementation - karmaOptions?: (options: ConfigOptions) => ConfigOptions; - } = {}, + transforms?: KarmaBuilderTransformsOptions, ): AsyncIterable { + const normalizedOptions = normalizeOptions(context, options); + const karmaOptions = getBaseKarmaOptions(normalizedOptions, context); + let karmaServer: Server; return new ReadableStream({ async start(controller) { let init; try { - init = await initializeApplication(options, context, karmaOptions, transforms); + init = await initializeApplication(normalizedOptions, context, karmaOptions, transforms); } catch (err) { if (err instanceof ApplicationBuildError) { controller.enqueue({ success: false, message: err.message }); @@ -336,13 +337,9 @@ async function getProjectSourceRoot(context: BuilderContext): Promise { return projectSourceRoot; } -function normalizePolyfills(polyfills: string | string[] | undefined): [string[], string[]] { - if (typeof polyfills === 'string') { - polyfills = [polyfills]; - } else if (!polyfills) { - polyfills = []; - } - +function normalizePolyfills( + polyfills: string[] = [], +): [polyfills: string[], jasmineCleanup: string[]] { const jasmineGlobalEntryPoint = localResolve('./polyfills/jasmine_global.js'); const jasmineGlobalCleanupEntrypoint = localResolve('./polyfills/jasmine_global_cleanup.js'); const sourcemapEntrypoint = localResolve('./polyfills/init_sourcemaps.js'); @@ -379,9 +376,7 @@ async function initializeApplication( options: NormalizedKarmaBuilderOptions, context: BuilderContext, karmaOptions: ConfigOptions, - transforms: { - karmaOptions?: (options: ConfigOptions) => ConfigOptions; - } = {}, + transforms?: KarmaBuilderTransformsOptions, ): Promise< [typeof import('karma'), Config & ConfigOptions, BuildOptions, AsyncIterator | null] > { @@ -423,13 +418,7 @@ async function initializeApplication( index: false, outputHashing: OutputHashing.None, optimization: false, - sourceMap: options.codeCoverage - ? { - scripts: true, - styles: true, - vendor: true, - } - : options.sourceMap, + sourceMap: options.sourceMap, instrumentForCoverage, styles: options.styles, scripts: options.scripts, @@ -551,8 +540,8 @@ async function initializeApplication( } const parsedKarmaConfig: Config & ConfigOptions = await karma.config.parseConfig( - options.karmaConfig && path.resolve(context.workspaceRoot, options.karmaConfig), - transforms.karmaOptions ? transforms.karmaOptions(karmaOptions) : karmaOptions, + options.karmaConfig, + transforms?.karmaOptions ? await transforms.karmaOptions(karmaOptions) : karmaOptions, { promiseConfig: true, throwErrors: true }, ); @@ -718,3 +707,82 @@ function getInstrumentationExcludedPaths(root: string, excludedPaths: string[]): return excluded; } +function getBaseKarmaOptions( + options: NormalizedKarmaBuilderOptions, + context: BuilderContext, +): ConfigOptions { + // Determine project name from builder context target + const projectName = context.target?.project; + if (!projectName) { + throw new Error(`The 'karma' builder requires a target to be specified.`); + } + + const karmaOptions: ConfigOptions = options.karmaConfig + ? {} + : getBuiltInKarmaConfig(context.workspaceRoot, projectName); + + const singleRun = !options.watch; + karmaOptions.singleRun = singleRun; + + // Workaround https://github.com/angular/angular-cli/issues/28271, by clearing context by default + // for single run executions. Not clearing context for multi-run (watched) builds allows the + // Jasmine Spec Runner to be visible in the browser after test execution. + karmaOptions.client ??= {}; + karmaOptions.client.clearContext ??= singleRun; + + // Convert browsers from a string to an array + if (options.browsers) { + karmaOptions.browsers = options.browsers; + } + + if (options.reporters) { + karmaOptions.reporters = options.reporters; + } + + return karmaOptions; +} + +function getBuiltInKarmaConfig( + workspaceRoot: string, + projectName: string, +): ConfigOptions & Record { + let coverageFolderName = projectName.charAt(0) === '@' ? projectName.slice(1) : projectName; + coverageFolderName = coverageFolderName.toLowerCase(); + + const workspaceRootRequire = createRequire(workspaceRoot + '/'); + + // Any changes to the config here need to be synced to: packages/schematics/angular/config/files/karma.conf.js.template + return { + basePath: '', + frameworks: ['jasmine'], + plugins: [ + 'karma-jasmine', + 'karma-chrome-launcher', + 'karma-jasmine-html-reporter', + 'karma-coverage', + ].map((p) => workspaceRootRequire(p)), + jasmineHtmlReporter: { + suppressAll: true, // removes the duplicated traces + }, + coverageReporter: { + dir: path.join(workspaceRoot, 'coverage', coverageFolderName), + subdir: '.', + reporters: [{ type: 'html' }, { type: 'text-summary' }], + }, + reporters: ['progress', 'kjhtml'], + browsers: ['Chrome'], + customLaunchers: { + // Chrome configured to run in a bazel sandbox. + // Disable the use of the gpu and `/dev/shm` because it causes Chrome to + // crash on some environments. + // See: + // https://github.com/puppeteer/puppeteer/blob/v1.0.0/docs/troubleshooting.md#tips + // https://stackoverflow.com/questions/50642308/webdriverexception-unknown-error-devtoolsactiveport-file-doesnt-exist-while-t + ChromeHeadlessNoSandbox: { + base: 'ChromeHeadless', + flags: ['--no-sandbox', '--headless', '--disable-gpu', '--disable-dev-shm-usage'], + }, + }, + restartOnFileChange: true, + }; +} diff --git a/packages/angular/build/src/builders/karma/index.ts b/packages/angular/build/src/builders/karma/index.ts index 295d887e19d8..f2db8d56c598 100644 --- a/packages/angular/build/src/builders/karma/index.ts +++ b/packages/angular/build/src/builders/karma/index.ts @@ -13,15 +13,12 @@ import { createBuilder, } from '@angular-devkit/architect'; import type { ConfigOptions } from 'karma'; -import { createRequire } from 'node:module'; -import path from 'node:path'; -import { NormalizedKarmaBuilderOptions, normalizeOptions } from './options'; + import type { Schema as KarmaBuilderOptions } from './schema'; -export type KarmaConfigOptions = ConfigOptions & { - buildWebpack?: unknown; - configFile?: string; -}; +export interface KarmaBuilderTransformsOptions { + karmaOptions?: (options: ConfigOptions) => ConfigOptions | Promise; +} /** * @experimental Direct usage of this function is considered experimental. @@ -29,98 +26,11 @@ export type KarmaConfigOptions = ConfigOptions & { export async function* execute( options: KarmaBuilderOptions, context: BuilderContext, - transforms: { - // The karma options transform cannot be async without a refactor of the builder implementation - karmaOptions?: (options: KarmaConfigOptions) => KarmaConfigOptions; - } = {}, + transforms?: KarmaBuilderTransformsOptions, ): AsyncIterable { const { execute } = await import('./application_builder'); - const normalizedOptions = normalizeOptions(options); - const karmaOptions = getBaseKarmaOptions(normalizedOptions, context); - - yield* execute(normalizedOptions, context, karmaOptions, transforms); -} - -function getBaseKarmaOptions( - options: NormalizedKarmaBuilderOptions, - context: BuilderContext, -): KarmaConfigOptions { - const singleRun = !options.watch; - - // Determine project name from builder context target - const projectName = context.target?.project; - if (!projectName) { - throw new Error(`The 'karma' builder requires a target to be specified.`); - } - - const karmaOptions: KarmaConfigOptions = options.karmaConfig - ? {} - : getBuiltInKarmaConfig(context.workspaceRoot, projectName); - - karmaOptions.singleRun = singleRun; - - // Workaround https://github.com/angular/angular-cli/issues/28271, by clearing context by default - // for single run executions. Not clearing context for multi-run (watched) builds allows the - // Jasmine Spec Runner to be visible in the browser after test execution. - karmaOptions.client ??= {}; - karmaOptions.client.clearContext ??= singleRun ?? false; // `singleRun` defaults to `false` per Karma docs. - - // Convert browsers from a string to an array - if (options.browsers) { - karmaOptions.browsers = options.browsers; - } - - if (options.reporters) { - karmaOptions.reporters = options.reporters; - } - - return karmaOptions; -} - -function getBuiltInKarmaConfig( - workspaceRoot: string, - projectName: string, -): ConfigOptions & Record { - let coverageFolderName = projectName.charAt(0) === '@' ? projectName.slice(1) : projectName; - coverageFolderName = coverageFolderName.toLowerCase(); - - const workspaceRootRequire = createRequire(workspaceRoot + '/'); - // Any changes to the config here need to be synced to: packages/schematics/angular/config/files/karma.conf.js.template - return { - basePath: '', - rootUrl: '/', - frameworks: ['jasmine'], - plugins: [ - 'karma-jasmine', - 'karma-chrome-launcher', - 'karma-jasmine-html-reporter', - 'karma-coverage', - ].map((p) => workspaceRootRequire(p)), - jasmineHtmlReporter: { - suppressAll: true, // removes the duplicated traces - }, - coverageReporter: { - dir: path.join(workspaceRoot, 'coverage', coverageFolderName), - subdir: '.', - reporters: [{ type: 'html' }, { type: 'text-summary' }], - }, - reporters: ['progress', 'kjhtml'], - browsers: ['Chrome'], - customLaunchers: { - // Chrome configured to run in a bazel sandbox. - // Disable the use of the gpu and `/dev/shm` because it causes Chrome to - // crash on some environments. - // See: - // https://github.com/puppeteer/puppeteer/blob/v1.0.0/docs/troubleshooting.md#tips - // https://stackoverflow.com/questions/50642308/webdriverexception-unknown-error-devtoolsactiveport-file-doesnt-exist-while-t - ChromeHeadlessNoSandbox: { - base: 'ChromeHeadless', - flags: ['--no-sandbox', '--headless', '--disable-gpu', '--disable-dev-shm-usage'], - }, - }, - restartOnFileChange: true, - }; + yield* execute(options, context, transforms); } export type { KarmaBuilderOptions }; diff --git a/packages/angular/build/src/builders/karma/options.ts b/packages/angular/build/src/builders/karma/options.ts index f8dfa1e8e0b0..03ba443702c3 100644 --- a/packages/angular/build/src/builders/karma/options.ts +++ b/packages/angular/build/src/builders/karma/options.ts @@ -6,12 +6,23 @@ * found in the LICENSE file at https://angular.dev/license */ +import type { BuilderContext } from '@angular-devkit/architect'; +import { resolve } from 'node:path'; import { Schema as KarmaBuilderOptions } from './schema'; -export type NormalizedKarmaBuilderOptions = Awaited>; +export type NormalizedKarmaBuilderOptions = ReturnType; -export function normalizeOptions(options: KarmaBuilderOptions) { - const { watch = true, include = [], exclude = [], reporters = [], browsers, ...rest } = options; +export function normalizeOptions(context: BuilderContext, options: KarmaBuilderOptions) { + const { + sourceMap, + karmaConfig, + browsers, + watch = true, + include = [], + exclude = [], + reporters = [], + ...rest + } = options; let normalizedBrowsers: string[] | undefined; if (typeof options.browsers === 'string' && options.browsers) { @@ -25,12 +36,23 @@ export function normalizeOptions(options: KarmaBuilderOptions) { .reduce((acc, curr) => acc.concat(curr.split(',')), []) .filter((x) => !!x); + // Sourcemaps are always needed when code coverage is enabled. + const normalizedSourceMap = options.codeCoverage + ? { + scripts: true, + styles: true, + vendor: true, + } + : sourceMap; + return { + ...rest, + sourceMap: normalizedSourceMap, + karmaConfig: karmaConfig ? resolve(context.workspaceRoot, karmaConfig) : undefined, reporters: normalizedReporters.length ? normalizedReporters : undefined, browsers: normalizedBrowsers, watch, include, exclude, - ...rest, }; } diff --git a/packages/angular/build/src/private.ts b/packages/angular/build/src/private.ts index 2e2691b02485..29e98bf531aa 100644 --- a/packages/angular/build/src/private.ts +++ b/packages/angular/build/src/private.ts @@ -26,7 +26,6 @@ export { buildApplicationInternal } from './builders/application'; export type { ApplicationBuilderInternalOptions } from './builders/application/options'; export { type Result, type ResultFile, ResultKind } from './builders/application/results'; export { serveWithVite } from './builders/dev-server/vite-server'; -export { execute as executeKarmaInternal } from './builders/karma/application_builder'; // Tools export * from './tools/babel/plugins'; diff --git a/packages/angular_devkit/build_angular/src/builders/karma/index.ts b/packages/angular_devkit/build_angular/src/builders/karma/index.ts index 24b808485803..ea79a9165771 100644 --- a/packages/angular_devkit/build_angular/src/builders/karma/index.ts +++ b/packages/angular_devkit/build_angular/src/builders/karma/index.ts @@ -45,20 +45,29 @@ export function execute( return from(getExecuteWithBuilder(options, context)).pipe( mergeMap(([useEsbuild, executeWithBuilder]) => { - const karmaOptions = getBaseKarmaOptions(options, context, useEsbuild); - - if (useEsbuild && transforms.webpackConfiguration) { - context.logger.warn( - `This build is using the application builder but transforms.webpackConfiguration was provided. The transform will be ignored.`, - ); - } - - if (useEsbuild && options.fileReplacements) { - options.fileReplacements = normalizeFileReplacements(options.fileReplacements, './'); + if (useEsbuild) { + if (transforms.webpackConfiguration) { + context.logger.warn( + `This build is using the application builder but transforms.webpackConfiguration was provided. The transform will be ignored.`, + ); + } + + if (options.fileReplacements) { + options.fileReplacements = normalizeFileReplacements(options.fileReplacements, './'); + } + + if (typeof options.polyfills === 'string') { + options.polyfills = [options.polyfills]; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return executeWithBuilder(options as any, context, transforms); + } else { + const karmaOptions = getBaseKarmaOptions(options, context); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return executeWithBuilder(options as any, context, karmaOptions, transforms); } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return executeWithBuilder(options as any, context, karmaOptions, transforms); }), ); } @@ -66,7 +75,6 @@ export function execute( function getBaseKarmaOptions( options: KarmaBuilderOptions, context: BuilderContext, - useEsbuild: boolean, ): KarmaConfigOptions { let singleRun: boolean | undefined; if (options.watch !== undefined) { @@ -81,7 +89,7 @@ function getBaseKarmaOptions( const karmaOptions: KarmaConfigOptions = options.karmaConfig ? {} - : getBuiltInKarmaConfig(context.workspaceRoot, projectName, useEsbuild); + : getBuiltInKarmaConfig(context.workspaceRoot, projectName); karmaOptions.singleRun = singleRun; @@ -115,7 +123,6 @@ function getBaseKarmaOptions( function getBuiltInKarmaConfig( workspaceRoot: string, projectName: string, - useEsbuild: boolean, ): ConfigOptions & Record { let coverageFolderName = projectName.charAt(0) === '@' ? projectName.slice(1) : projectName; if (/[A-Z]/.test(coverageFolderName)) { @@ -127,13 +134,13 @@ function getBuiltInKarmaConfig( // Any changes to the config here need to be synced to: packages/schematics/angular/config/files/karma.conf.js.template return { basePath: '', - frameworks: ['jasmine', ...(useEsbuild ? [] : ['@angular-devkit/build-angular'])], + frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ 'karma-jasmine', 'karma-chrome-launcher', 'karma-jasmine-html-reporter', 'karma-coverage', - ...(useEsbuild ? [] : ['@angular-devkit/build-angular/plugins/karma']), + '@angular-devkit/build-angular/plugins/karma', ].map((p) => workspaceRootRequire(p)), jasmineHtmlReporter: { suppressAll: true, // removes the duplicated traces @@ -171,7 +178,7 @@ async function getExecuteWithBuilder( [ boolean, ( - | (typeof import('@angular/build/private'))['executeKarmaInternal'] + | (typeof import('@angular/build'))['executeKarmaBuilder'] | (typeof import('./browser_builder'))['execute'] ), ] @@ -179,8 +186,8 @@ async function getExecuteWithBuilder( const useEsbuild = await checkForEsbuild(options, context); let execute; if (useEsbuild) { - const { executeKarmaInternal } = await import('@angular/build/private'); - execute = executeKarmaInternal; + const { executeKarmaBuilder } = await import('@angular/build'); + execute = executeKarmaBuilder; } else { const browserBuilderModule = await import('./browser_builder'); execute = browserBuilderModule.execute; From d300b56e8c35cbb15909a707a290aad826992893 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 2 Jul 2025 17:03:26 -0400 Subject: [PATCH 11/65] refactor(@angular/cli): expand MCP tool/resource descriptions The descriptions for both the initial best practices resource and list projects tool have been expanded to provide more context regarding their use and results. (cherry picked from commit e23730a2ea71d40deb3e0ecf6fcc707794280930) --- .../cli/src/commands/mcp/mcp-server.ts | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/packages/angular/cli/src/commands/mcp/mcp-server.ts b/packages/angular/cli/src/commands/mcp/mcp-server.ts index 33df33f71ba9..1339e8f2c0d9 100644 --- a/packages/angular/cli/src/commands/mcp/mcp-server.ts +++ b/packages/angular/cli/src/commands/mcp/mcp-server.ts @@ -28,9 +28,12 @@ export async function createMcpServer(context: { 'instructions', 'instructions://best-practices', { - title: 'Angular System Instructions', + title: 'Angular Best Practices and Code Generation Guide', description: - 'A set of instructions to help LLMs generate correct code that follows Angular best practices.', + "A comprehensive guide detailing Angular's best practices for code generation and development. " + + 'This guide should be used as a reference by an LLM to ensure any generated code ' + + 'adheres to modern Angular standards, including the use of standalone components, ' + + 'typed forms, modern control flow syntax, and other current conventions.', mimeType: 'text/markdown', }, async () => { @@ -46,18 +49,26 @@ export async function createMcpServer(context: { server.registerTool( 'list_projects', { - title: 'List projects', + title: 'List Angular Projects', description: - 'List projects within an Angular workspace.' + - ' This information is read from the `angular.json` file at the root path of the Angular workspace', + 'Lists the names of all applications and libraries defined within an Angular workspace. ' + + 'It reads the `angular.json` configuration file to identify the projects. ', + annotations: { + readOnlyHint: true, + }, }, - () => { - if (!context.workspace) { + async () => { + const { workspace } = context; + + if (!workspace) { return { content: [ { - type: 'text', - text: 'Not within an Angular project.', + type: 'text' as const, + text: + 'No Angular workspace found.' + + ' An `angular.json` file, which marks the root of a workspace,' + + ' could not be located in the current directory or any of its parent directories.', }, ], }; @@ -66,10 +77,8 @@ export async function createMcpServer(context: { return { content: [ { - type: 'text', - text: - 'Projects in the Angular workspace: ' + - [...context.workspace.projects.keys()].join(','), + type: 'text' as const, + text: 'Projects in the Angular workspace: ' + [...workspace.projects.keys()].join(','), }, ], }; From f6420db95cbac373212d6f482919033c30e6317c Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 7 Jul 2025 09:24:36 -0400 Subject: [PATCH 12/65] refactor(@angular/cli): provide additional project details for MCP list projects tool The Angular CLI's MCP server will now provide additional information regarding each project when the list projects tool is used. This includes: * name * type (application/library) * root (project root path relative to the Angular workspace root) * sourceRoot (source code root path relative to the Angular workspace root) * selectorPrefix (default selector prefix used for component generation) (cherry picked from commit 9d5c8077e95d9c2a9344546e6f57d75cc4cfd57d) --- packages/angular/cli/BUILD.bazel | 1 + packages/angular/cli/package.json | 3 +- .../cli/src/commands/mcp/mcp-server.ts | 54 +++++++++++++++++-- pnpm-lock.yaml | 23 ++++---- 4 files changed, 65 insertions(+), 16 deletions(-) diff --git a/packages/angular/cli/BUILD.bazel b/packages/angular/cli/BUILD.bazel index 41201feb3c1f..a25061997acf 100644 --- a/packages/angular/cli/BUILD.bazel +++ b/packages/angular/cli/BUILD.bazel @@ -58,6 +58,7 @@ ts_project( ":node_modules/pacote", ":node_modules/resolve", ":node_modules/yargs", + ":node_modules/zod", "//:node_modules/@angular/core", "//:node_modules/@types/ini", "//:node_modules/@types/node", diff --git a/packages/angular/cli/package.json b/packages/angular/cli/package.json index 950db34a956e..6b58a139b964 100644 --- a/packages/angular/cli/package.json +++ b/packages/angular/cli/package.json @@ -38,7 +38,8 @@ "pacote": "21.0.0", "resolve": "1.22.10", "semver": "7.7.2", - "yargs": "18.0.0" + "yargs": "18.0.0", + "zod": "3.25.75" }, "ng-update": { "migrations": "@schematics/angular/migrations/migration-collection.json", diff --git a/packages/angular/cli/src/commands/mcp/mcp-server.ts b/packages/angular/cli/src/commands/mcp/mcp-server.ts index 1339e8f2c0d9..81a11ac6c94a 100644 --- a/packages/angular/cli/src/commands/mcp/mcp-server.ts +++ b/packages/angular/cli/src/commands/mcp/mcp-server.ts @@ -9,6 +9,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { readFile } from 'node:fs/promises'; import path from 'node:path'; +import { z } from 'zod'; import type { AngularWorkspace } from '../../utilities/config'; import { VERSION } from '../../utilities/version'; @@ -30,10 +31,10 @@ export async function createMcpServer(context: { { title: 'Angular Best Practices and Code Generation Guide', description: - "A comprehensive guide detailing Angular's best practices for code generation and development. " + - 'This guide should be used as a reference by an LLM to ensure any generated code ' + - 'adheres to modern Angular standards, including the use of standalone components, ' + - 'typed forms, modern control flow syntax, and other current conventions.', + "A comprehensive guide detailing Angular's best practices for code generation and development." + + ' This guide should be used as a reference by an LLM to ensure any generated code' + + ' adheres to modern Angular standards, including the use of standalone components,' + + ' typed forms, modern control flow syntax, and other current conventions.', mimeType: 'text/markdown', }, async () => { @@ -56,6 +57,34 @@ export async function createMcpServer(context: { annotations: { readOnlyHint: true, }, + outputSchema: { + projects: z.array( + z.object({ + name: z + .string() + .describe('The name of the project, as defined in the `angular.json` file.'), + type: z + .enum(['application', 'library']) + .optional() + .describe(`The type of the project, either 'application' or 'library'.`), + root: z + .string() + .describe('The root directory of the project, relative to the workspace root.'), + sourceRoot: z + .string() + .describe( + `The root directory of the project's source files, relative to the workspace root.`, + ), + selectorPrefix: z + .string() + .optional() + .describe( + 'The prefix to use for component selectors.' + + ` For example, a prefix of 'app' would result in selectors like ''.`, + ), + }), + ), + }, }, async () => { const { workspace } = context; @@ -74,13 +103,28 @@ export async function createMcpServer(context: { }; } + const projects = []; + // Convert to output format + for (const [name, project] of workspace.projects.entries()) { + projects.push({ + name, + type: project.extensions['projectType'] as 'application' | 'library' | undefined, + root: project.root, + sourceRoot: project.sourceRoot ?? path.posix.join(project.root, 'src'), + selectorPrefix: project.extensions['prefix'] as string, + }); + } + + // The structuredContent field is newer and may not be supported by all hosts. + // A text representation of the content is also provided for compatibility. return { content: [ { type: 'text' as const, - text: 'Projects in the Angular workspace: ' + [...workspace.projects.keys()].join(','), + text: `Projects in the Angular workspace:\n${JSON.stringify(projects)}`, }, ], + structuredContent: { projects }, }; }, ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 494137b8d861..a5a00e7b18fe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -507,6 +507,9 @@ importers: yargs: specifier: 18.0.0 version: 18.0.0 + zod: + specifier: 3.25.75 + version: 3.25.75 packages/angular/pwa: dependencies: @@ -8334,8 +8337,8 @@ packages: peerDependencies: zod: ^3.24.1 - zod@3.25.67: - resolution: {integrity: sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==} + zod@3.25.75: + resolution: {integrity: sha512-OhpzAmVzabPOL6C3A3gpAifqr9MqihV/Msx3gor2b2kviCgcb+HM9SEOpMWwwNp9MRunWnhtAKUoo0AHhjyPPg==} zone.js@0.15.1: resolution: {integrity: sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==} @@ -9433,8 +9436,8 @@ snapshots: dependencies: google-auth-library: 9.15.1(encoding@0.1.13)(supports-color@10.0.0) ws: 8.18.2 - zod: 3.25.67 - zod-to-json-schema: 3.24.5(zod@3.25.67) + zod: 3.25.75 + zod-to-json-schema: 3.24.5(zod@3.25.75) optionalDependencies: '@modelcontextprotocol/sdk': 1.13.3 transitivePeerDependencies: @@ -9692,8 +9695,8 @@ snapshots: express-rate-limit: 7.5.0(express@5.1.0) pkce-challenge: 5.0.0 raw-body: 3.0.0 - zod: 3.25.67 - zod-to-json-schema: 3.24.5(zod@3.25.67) + zod: 3.25.75 + zod-to-json-schema: 3.24.5(zod@3.25.75) transitivePeerDependencies: - supports-color @@ -11738,7 +11741,7 @@ snapshots: dependencies: devtools-protocol: 0.0.1452169 mitt: 3.0.1 - zod: 3.25.67 + zod: 3.25.75 cli-cursor@3.1.0: dependencies: @@ -16880,10 +16883,10 @@ snapshots: yoctocolors-cjs@2.1.2: {} - zod-to-json-schema@3.24.5(zod@3.25.67): + zod-to-json-schema@3.24.5(zod@3.25.75): dependencies: - zod: 3.25.67 + zod: 3.25.75 - zod@3.25.67: {} + zod@3.25.75: {} zone.js@0.15.1: {} From f369d88653360efa5d8d7db8a8f7ce8cb95a28f3 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 8 Jul 2025 08:08:30 +0000 Subject: [PATCH 13/65] build: change size to "medium" and "small" There are several warnings about tests whose specified size is too big. These tests shards run under 300s and thus `medium` is more appropiate. (cherry picked from commit d9468a649282cb38484fac24596122fc5e89c48f) --- .../testing/builder/src/builder-harness.ts | 3 +- packages/angular/build/BUILD.bazel | 12 +-- .../tests/options/polyfills_spec.ts | 100 +++++++++--------- 3 files changed, 59 insertions(+), 56 deletions(-) diff --git a/modules/testing/builder/src/builder-harness.ts b/modules/testing/builder/src/builder-harness.ts index 092206698f83..ec4973efa021 100644 --- a/modules/testing/builder/src/builder-harness.ts +++ b/modules/testing/builder/src/builder-harness.ts @@ -263,7 +263,7 @@ export class BuilderHarness { } const logs: logging.LogEntry[] = []; - context.logger.subscribe((e) => logs.push(e)); + const logger$ = context.logger.subscribe((e) => logs.push(e)); return observableFrom(this.schemaRegistry.compile(this.builderInfo.optionSchema)).pipe( mergeMap((validator) => validator(targetOptions)), @@ -302,6 +302,7 @@ export class BuilderHarness { }), finalize(() => { this.watcherNotifier = undefined; + logger$.unsubscribe(); for (const teardown of context.teardowns) { // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/packages/angular/build/BUILD.bazel b/packages/angular/build/BUILD.bazel index e46c2da6fcee..69d8ac3e2dd8 100644 --- a/packages/angular/build/BUILD.bazel +++ b/packages/angular/build/BUILD.bazel @@ -291,15 +291,15 @@ ts_project( jasmine_test( name = "application_integration_tests", - size = "large", + size = "medium", data = [":application_integration_test_lib"], flaky = True, - shard_count = 20, + shard_count = 25, ) jasmine_test( name = "dev-server_integration_tests", - size = "large", + size = "medium", data = [":dev-server_integration_test_lib"], flaky = True, shard_count = 10, @@ -307,7 +307,7 @@ jasmine_test( jasmine_test( name = "karma_integration_tests", - size = "large", + size = "medium", data = [":karma_integration_test_lib"], env = { # TODO: Replace Puppeteer downloaded browsers with Bazel-managed browsers, @@ -320,9 +320,9 @@ jasmine_test( jasmine_test( name = "unit-test_integration_tests", - size = "large", + size = "small", data = [":unit-test_integration_test_lib"], - shard_count = 10, + shard_count = 5, ) genrule( diff --git a/packages/angular/build/src/builders/application/tests/options/polyfills_spec.ts b/packages/angular/build/src/builders/application/tests/options/polyfills_spec.ts index 290ea281208d..bcbb21f237a7 100644 --- a/packages/angular/build/src/builders/application/tests/options/polyfills_spec.ts +++ b/packages/angular/build/src/builders/application/tests/options/polyfills_spec.ts @@ -16,71 +16,73 @@ const testsVariants: [suitName: string, baseUrl: string | undefined][] = [ ]; describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { - for (const [suitName, baseUrl] of testsVariants) { - describe(suitName, () => { - beforeEach(async () => { - await harness.modifyFile('tsconfig.json', (content) => { - const tsconfig = JSON.parse(content); - tsconfig.compilerOptions.baseUrl = baseUrl; - - return JSON.stringify(tsconfig); - }); - }); - - it('uses a provided TypeScript file', async () => { - harness.useTarget('build', { - ...BASE_OPTIONS, - polyfills: ['src/polyfills.ts'], + describe('Option: polyfills', () => { + for (const [suitName, baseUrl] of testsVariants) { + describe(suitName, () => { + beforeEach(async () => { + await harness.modifyFile('tsconfig.json', (content) => { + const tsconfig = JSON.parse(content); + tsconfig.compilerOptions.baseUrl = baseUrl; + + return JSON.stringify(tsconfig); + }); }); - const { result } = await harness.executeOnce(); + it('uses a provided TypeScript file', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + polyfills: ['src/polyfills.ts'], + }); - expect(result?.success).toBe(true); - - harness.expectFile('dist/browser/polyfills.js').toExist(); - }); + const { result } = await harness.executeOnce(); - it('uses a provided JavaScript file', async () => { - await harness.writeFile('src/polyfills.js', `console.log('main');`); + expect(result?.success).toBe(true); - harness.useTarget('build', { - ...BASE_OPTIONS, - polyfills: ['src/polyfills.js'], + harness.expectFile('dist/browser/polyfills.js').toExist(); }); - const { result } = await harness.executeOnce(); + it('uses a provided JavaScript file', async () => { + await harness.writeFile('src/polyfills.js', `console.log('main');`); - expect(result?.success).toBe(true); + harness.useTarget('build', { + ...BASE_OPTIONS, + polyfills: ['src/polyfills.js'], + }); - harness.expectFile('dist/browser/polyfills.js').content.toContain(`console.log("main")`); - }); + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); - it('fails and shows an error when file does not exist', async () => { - harness.useTarget('build', { - ...BASE_OPTIONS, - polyfills: ['src/missing.ts'], + harness.expectFile('dist/browser/polyfills.js').content.toContain(`console.log("main")`); }); - const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); + it('fails and shows an error when file does not exist', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + polyfills: ['src/missing.ts'], + }); - expect(result?.success).toBe(false); - expect(logs).toContain( - jasmine.objectContaining({ message: jasmine.stringMatching('Could not resolve') }), - ); + const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); - harness.expectFile('dist/browser/polyfills.js').toNotExist(); - }); + expect(result?.success).toBe(false); + expect(logs).toContain( + jasmine.objectContaining({ message: jasmine.stringMatching('Could not resolve') }), + ); - it('resolves module specifiers in array', async () => { - harness.useTarget('build', { - ...BASE_OPTIONS, - polyfills: ['zone.js', 'zone.js/testing'], + harness.expectFile('dist/browser/polyfills.js').toNotExist(); }); - const { result } = await harness.executeOnce(); - expect(result?.success).toBeTrue(); - harness.expectFile('dist/browser/polyfills.js').toExist(); + it('resolves module specifiers in array', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + polyfills: ['zone.js', 'zone.js/testing'], + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + harness.expectFile('dist/browser/polyfills.js').toExist(); + }); }); - }); - } + } + }); }); From 67e481ba0a1a2eedd5f29052456e2ffeabd8c732 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 8 Jul 2025 09:19:35 +0000 Subject: [PATCH 14/65] test: reduce karma test flakes by using `clearContext` that is set in the builder This is needed as a workaround for https://github.com/angular/angular-cli/issues/28271 (cherry picked from commit 50c79cff750b2aeabb85b39ad46f19fd66652d43) --- .../testing/builder/projects/hello-world-app/karma.conf.js | 3 --- .../src/builders/application/tests/options/polyfills_spec.ts | 4 ++++ .../test/hello-world-lib/projects/lib/karma.conf.js | 3 --- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/modules/testing/builder/projects/hello-world-app/karma.conf.js b/modules/testing/builder/projects/hello-world-app/karma.conf.js index 1cf153a1da81..7d5f7c8d98f5 100644 --- a/modules/testing/builder/projects/hello-world-app/karma.conf.js +++ b/modules/testing/builder/projects/hello-world-app/karma.conf.js @@ -23,9 +23,6 @@ module.exports = function(config) { require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma'), ], - client: { - clearContext: false, // leave Jasmine Spec Runner output visible in browser - }, jasmineHtmlReporter: { suppressAll: true // removes the duplicated traces }, diff --git a/packages/angular/build/src/builders/application/tests/options/polyfills_spec.ts b/packages/angular/build/src/builders/application/tests/options/polyfills_spec.ts index bcbb21f237a7..fcf6a1e39c9e 100644 --- a/packages/angular/build/src/builders/application/tests/options/polyfills_spec.ts +++ b/packages/angular/build/src/builders/application/tests/options/polyfills_spec.ts @@ -20,6 +20,10 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { for (const [suitName, baseUrl] of testsVariants) { describe(suitName, () => { beforeEach(async () => { + beforeEach(async () => { + await harness.writeFile('src/main.ts', 'console.log("TEST");'); + }); + await harness.modifyFile('tsconfig.json', (content) => { const tsconfig = JSON.parse(content); tsconfig.compilerOptions.baseUrl = baseUrl; diff --git a/packages/angular_devkit/build_angular/test/hello-world-lib/projects/lib/karma.conf.js b/packages/angular_devkit/build_angular/test/hello-world-lib/projects/lib/karma.conf.js index fe024ed63ee2..5573d103b26e 100644 --- a/packages/angular_devkit/build_angular/test/hello-world-lib/projects/lib/karma.conf.js +++ b/packages/angular_devkit/build_angular/test/hello-world-lib/projects/lib/karma.conf.js @@ -20,9 +20,6 @@ module.exports = function (config) { require('karma-coverage-istanbul-reporter'), require('@angular-devkit/build-angular/plugins/karma') ], - client: { - clearContext: false // leave Jasmine Spec Runner output visible in browser - }, jasmineHtmlReporter: { suppressAll: true // removes the duplicated traces }, From 4a791319d272da52eb6839aebc05749683b4d32e Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 8 Jul 2025 09:28:06 +0000 Subject: [PATCH 15/65] test: remove contents of `main.ts` from polyfill tests These test do not need the contents of the `main.ts` (cherry picked from commit bfbef6e3754a6f8b1aff633889af71340f4af6f9) --- .../src/builders/application/tests/options/polyfills_spec.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/angular/build/src/builders/application/tests/options/polyfills_spec.ts b/packages/angular/build/src/builders/application/tests/options/polyfills_spec.ts index fcf6a1e39c9e..8b5cc3a09ab3 100644 --- a/packages/angular/build/src/builders/application/tests/options/polyfills_spec.ts +++ b/packages/angular/build/src/builders/application/tests/options/polyfills_spec.ts @@ -20,9 +20,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { for (const [suitName, baseUrl] of testsVariants) { describe(suitName, () => { beforeEach(async () => { - beforeEach(async () => { - await harness.writeFile('src/main.ts', 'console.log("TEST");'); - }); + await harness.writeFile('src/main.ts', 'console.log("TEST");'); await harness.modifyFile('tsconfig.json', (content) => { const tsconfig = JSON.parse(content); From be89073b92f7e1ffd13fce35f928c9acc8d2ca58 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 8 Jul 2025 12:28:11 +0000 Subject: [PATCH 16/65] test: Reduce default timeouts The default 2.5-minute timeout is often unnecessary and can interfere with test execution. Specifically, Bazel may terminate the test before Jasmine does, making it harder to identify which test is causing the issue. (cherry picked from commit 744721182b5a35385c5a92c4aafd847dc4f6de32) --- modules/testing/builder/src/test-utils.ts | 4 +- .../tests/options/output-path_spec.ts | 40 +++++++++---------- .../src/builders/app-shell/app-shell_spec.ts | 9 +++++ .../src/builders/ng-packagr/works_spec.ts | 13 ++++-- .../src/builders/prerender/works_spec.ts | 10 +++++ 5 files changed, 51 insertions(+), 25 deletions(-) diff --git a/modules/testing/builder/src/test-utils.ts b/modules/testing/builder/src/test-utils.ts index 126af073b726..f41cb42d1ea6 100644 --- a/modules/testing/builder/src/test-utils.ts +++ b/modules/testing/builder/src/test-utils.ts @@ -24,8 +24,8 @@ import { import path from 'node:path'; import { firstValueFrom } from 'rxjs'; -// Default timeout for large specs is 2.5 minutes. -jasmine.DEFAULT_TIMEOUT_INTERVAL = 150000; +// Default timeout for large specs is 60s. +jasmine.DEFAULT_TIMEOUT_INTERVAL = 60_000; export const workspaceRoot = join(normalize(__dirname), `../projects/hello-world-app/`); export const host = new TestProjectHost(workspaceRoot); diff --git a/packages/angular/build/src/builders/application/tests/options/output-path_spec.ts b/packages/angular/build/src/builders/application/tests/options/output-path_spec.ts index f8d4513c7de7..3f4898e1c6d8 100644 --- a/packages/angular/build/src/builders/application/tests/options/output-path_spec.ts +++ b/packages/angular/build/src/builders/application/tests/options/output-path_spec.ts @@ -10,28 +10,28 @@ import { buildApplication } from '../../index'; import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { - beforeEach(async () => { - // Add a global stylesheet media file - await harness.writeFile('src/styles.css', `h1 { background: url('./spectrum.png')}`); - // Add a component stylesheet media file - await harness.writeFile('src/app/abc.svg', ''); - await harness.writeFile('src/app/app.component.css', `h2 { background: url('./abc.svg')}`); - - // Enable SSR - await harness.modifyFile('src/tsconfig.app.json', (content) => { - const tsConfig = JSON.parse(content); - tsConfig.files ??= []; - tsConfig.files.push('main.server.ts', 'server.ts'); - - return JSON.stringify(tsConfig); - }); + describe('Option: "outputPath"', () => { + beforeEach(async () => { + // Add a global stylesheet media file + await harness.writeFile('src/styles.css', `h1 { background: url('./spectrum.png')}`); + // Add a component stylesheet media file + await harness.writeFile('src/app/abc.svg', ''); + await harness.writeFile('src/app/app.component.css', `h2 { background: url('./abc.svg')}`); + + // Enable SSR + await harness.modifyFile('src/tsconfig.app.json', (content) => { + const tsConfig = JSON.parse(content); + tsConfig.files ??= []; + tsConfig.files.push('main.server.ts', 'server.ts'); + + return JSON.stringify(tsConfig); + }); - // Application server code is not needed in this test - await harness.writeFile('src/main.server.ts', `console.log('Hello!');`); - await harness.writeFile('src/server.ts', `console.log('Hello!');`); - }); + // Application server code is not needed in this test + await harness.writeFile('src/main.server.ts', `console.log('Hello!');`); + await harness.writeFile('src/server.ts', `console.log('Hello!');`); + }); - describe('Option: "outputPath"', () => { describe(`when option value is is a string`, () => { beforeEach(() => { harness.useTarget('build', { diff --git a/packages/angular_devkit/build_angular/src/builders/app-shell/app-shell_spec.ts b/packages/angular_devkit/build_angular/src/builders/app-shell/app-shell_spec.ts index 468bdb6ff2bd..6ee544305b06 100644 --- a/packages/angular_devkit/build_angular/src/builders/app-shell/app-shell_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/app-shell/app-shell_spec.ts @@ -13,6 +13,15 @@ import { createArchitect, host } from '../../testing/test-utils'; describe('AppShell Builder', () => { const target = { project: 'app', target: 'app-shell' }; let architect: Architect; + const originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; + + beforeAll(() => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = 100_000; + }); + + afterAll(() => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; + }); beforeEach(async () => { await host.initialize().toPromise(); diff --git a/packages/angular_devkit/build_angular/src/builders/ng-packagr/works_spec.ts b/packages/angular_devkit/build_angular/src/builders/ng-packagr/works_spec.ts index 06609780b8b8..4bf1ac2dec91 100644 --- a/packages/angular_devkit/build_angular/src/builders/ng-packagr/works_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/ng-packagr/works_spec.ts @@ -19,14 +19,21 @@ import { } from '@angular-devkit/core'; import { debounceTime, map, take, tap } from 'rxjs'; -// Default timeout for large specs is 2.5 minutes. -jasmine.DEFAULT_TIMEOUT_INTERVAL = 150000; - describe('NgPackagr Builder', () => { const workspaceRoot = join(normalize(__dirname), `../../../test/hello-world-lib/`); const host = new TestProjectHost(workspaceRoot); let architect: Architect; + const originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; + + beforeAll(() => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = 80_000; + }); + + afterAll(() => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; + }); + beforeEach(async () => { await host.initialize().toPromise(); diff --git a/packages/angular_devkit/build_angular/src/builders/prerender/works_spec.ts b/packages/angular_devkit/build_angular/src/builders/prerender/works_spec.ts index 4d26c6049542..8c55c923d02d 100644 --- a/packages/angular_devkit/build_angular/src/builders/prerender/works_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/prerender/works_spec.ts @@ -11,6 +11,16 @@ import { join, normalize, virtualFs } from '@angular-devkit/core'; import { createArchitect, host } from '../../testing/test-utils'; describe('Prerender Builder', () => { + const originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; + + beforeAll(() => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = 100_000; + }); + + afterAll(() => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; + }); + const target = { project: 'app', target: 'prerender' }; let architect: Architect; From b54a45bd3dde689b398742a8e0673429929a3b1e Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 8 Jul 2025 12:55:29 +0000 Subject: [PATCH 17/65] refactor: reduce the number of builds done in output-path_spec Significantly reduces the number of application builds performed in this test (cherry picked from commit f5c953d437b6e57c1caeeda4e6d1d781984ba470) --- .../tests/options/output-path_spec.ts | 129 ++---------------- 1 file changed, 13 insertions(+), 116 deletions(-) diff --git a/packages/angular/build/src/builders/application/tests/options/output-path_spec.ts b/packages/angular/build/src/builders/application/tests/options/output-path_spec.ts index 3f4898e1c6d8..b6c72b9bee58 100644 --- a/packages/angular/build/src/builders/application/tests/options/output-path_spec.ts +++ b/packages/angular/build/src/builders/application/tests/options/output-path_spec.ts @@ -32,8 +32,8 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { await harness.writeFile('src/server.ts', `console.log('Hello!');`); }); - describe(`when option value is is a string`, () => { - beforeEach(() => { + describe('when option value is a string', () => { + it('should emit browser, media and server files in their respective directories', async () => { harness.useTarget('build', { ...BASE_OPTIONS, polyfills: [], @@ -44,34 +44,20 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { entry: 'src/server.ts', }, }); - }); - it(`should emit browser bundles in 'browser' directory`, async () => { const { result } = await harness.executeOnce(); expect(result?.success).toBeTrue(); harness.expectFile('dist/browser/main.js').toExist(); - }); - - it(`should emit media files in 'browser/media' directory`, async () => { - const { result } = await harness.executeOnce(); - expect(result?.success).toBeTrue(); - harness.expectFile('dist/browser/media/spectrum.png').toExist(); harness.expectFile('dist/browser/media/abc.svg').toExist(); - }); - - it(`should emit server bundles in 'server' directory`, async () => { - const { result } = await harness.executeOnce(); - expect(result?.success).toBeTrue(); - harness.expectFile('dist/server/server.mjs').toExist(); }); }); - describe(`when option value is an object`, () => { + describe('when option value is an object', () => { describe(`'media' is set to 'resources'`, () => { - beforeEach(() => { + it('should emit browser, media and server files in their respective directories', async () => { harness.useTarget('build', { ...BASE_OPTIONS, polyfills: [], @@ -85,33 +71,19 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { entry: 'src/server.ts', }, }); - }); - it(`should emit browser bundles in 'browser' directory`, async () => { const { result } = await harness.executeOnce(); expect(result?.success).toBeTrue(); harness.expectFile('dist/browser/main.js').toExist(); - }); - - it(`should emit media files in 'browser/resource' directory`, async () => { - const { result } = await harness.executeOnce(); - expect(result?.success).toBeTrue(); - harness.expectFile('dist/browser/resource/spectrum.png').toExist(); harness.expectFile('dist/browser/resource/abc.svg').toExist(); - }); - - it(`should emit server bundles in 'server' directory`, async () => { - const { result } = await harness.executeOnce(); - expect(result?.success).toBeTrue(); - harness.expectFile('dist/server/server.mjs').toExist(); }); }); describe(`'media' is set to ''`, () => { - beforeEach(() => { + it('should emit browser, media and server files in their respective directories', async () => { harness.useTarget('build', { ...BASE_OPTIONS, polyfills: [], @@ -125,36 +97,20 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { entry: 'src/server.ts', }, }); - }); - it(`should emit browser bundles in 'browser' directory`, async () => { const { result } = await harness.executeOnce(); expect(result?.success).toBeTrue(); harness.expectFile('dist/browser/main.js').toExist(); - }); - - it(`should emit media files in 'browser' directory`, async () => { - const { result } = await harness.executeOnce(); - expect(result?.success).toBeTrue(); - harness.expectFile('dist/browser/spectrum.png').toExist(); harness.expectFile('dist/browser/abc.svg').toExist(); - - // Component CSS should not be considered media - harness.expectFile('dist/browser/app.component.css').toNotExist(); - }); - - it(`should emit server bundles in 'server' directory`, async () => { - const { result } = await harness.executeOnce(); - expect(result?.success).toBeTrue(); - harness.expectFile('dist/server/server.mjs').toExist(); + harness.expectFile('dist/browser/app.component.css').toNotExist(); }); }); describe(`'server' is set to 'node-server'`, () => { - beforeEach(() => { + it('should emit browser, media and server files in their respective directories', async () => { harness.useTarget('build', { ...BASE_OPTIONS, polyfills: [], @@ -168,33 +124,19 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { entry: 'src/server.ts', }, }); - }); - it(`should emit browser bundles in 'browser' directory`, async () => { const { result } = await harness.executeOnce(); expect(result?.success).toBeTrue(); harness.expectFile('dist/browser/main.js').toExist(); - }); - - it(`should emit media files in 'browser/media' directory`, async () => { - const { result } = await harness.executeOnce(); - expect(result?.success).toBeTrue(); - harness.expectFile('dist/browser/media/spectrum.png').toExist(); harness.expectFile('dist/browser/media/abc.svg').toExist(); - }); - - it(`should emit server bundles in 'node-server' directory`, async () => { - const { result } = await harness.executeOnce(); - expect(result?.success).toBeTrue(); - harness.expectFile('dist/node-server/server.mjs').toExist(); }); }); describe(`'browser' is set to 'public'`, () => { - beforeEach(() => { + it('should emit browser, media and server files in their respective directories', async () => { harness.useTarget('build', { ...BASE_OPTIONS, polyfills: [], @@ -208,51 +150,19 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { entry: 'src/server.ts', }, }); - }); - it(`should emit browser bundles in 'public' directory`, async () => { const { result } = await harness.executeOnce(); expect(result?.success).toBeTrue(); harness.expectFile('dist/public/main.js').toExist(); - }); - - it(`should emit media files in 'public/media' directory`, async () => { - const { result } = await harness.executeOnce(); - expect(result?.success).toBeTrue(); - harness.expectFile('dist/public/media/spectrum.png').toExist(); harness.expectFile('dist/public/media/abc.svg').toExist(); - }); - - it(`should emit server bundles in 'server' directory`, async () => { - const { result } = await harness.executeOnce(); - expect(result?.success).toBeTrue(); - harness.expectFile('dist/server/server.mjs').toExist(); }); }); describe(`'browser' is set to ''`, () => { - it(`should emit browser bundles in '' directory`, async () => { - harness.useTarget('build', { - ...BASE_OPTIONS, - polyfills: [], - server: 'src/main.server.ts', - outputPath: { - base: 'dist', - browser: '', - }, - ssr: false, - }); - - const { result } = await harness.executeOnce(); - expect(result?.success).toBeTrue(); - - harness.expectFile('dist/main.js').toExist(); - }); - - it(`should emit media files in 'media' directory`, async () => { + it('should emit browser and media files in the root output directory when ssr is disabled', async () => { harness.useTarget('build', { ...BASE_OPTIONS, polyfills: [], @@ -268,11 +178,12 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { const { result } = await harness.executeOnce(); expect(result?.success).toBeTrue(); + harness.expectFile('dist/main.js').toExist(); harness.expectFile('dist/media/spectrum.png').toExist(); harness.expectFile('dist/media/abc.svg').toExist(); }); - it(`should error when ssr is enabled`, async () => { + it('should error when ssr is enabled', async () => { harness.useTarget('build', { ...BASE_OPTIONS, polyfills: [], @@ -298,8 +209,8 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { }); }); - describe(`'server' is set ''`, () => { - beforeEach(() => { + describe(`'server' is set to ''`, () => { + it('should emit browser, media and server files in their respective directories', async () => { harness.useTarget('build', { ...BASE_OPTIONS, polyfills: [], @@ -313,27 +224,13 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { entry: 'src/server.ts', }, }); - }); - it(`should emit browser bundles in 'browser' directory`, async () => { const { result } = await harness.executeOnce(); expect(result?.success).toBeTrue(); harness.expectFile('dist/browser/main.js').toExist(); - }); - - it(`should emit media files in 'browser/media' directory`, async () => { - const { result } = await harness.executeOnce(); - expect(result?.success).toBeTrue(); - harness.expectFile('dist/browser/media/spectrum.png').toExist(); harness.expectFile('dist/browser/media/abc.svg').toExist(); - }); - - it(`should emit server bundles in '' directory`, async () => { - const { result } = await harness.executeOnce(); - expect(result?.success).toBeTrue(); - harness.expectFile('dist/server.mjs').toExist(); }); }); From ace8a3544b9e86af97298638a7a8894b6e167f06 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 8 Jul 2025 14:16:21 +0000 Subject: [PATCH 18/65] test: increase timeout for ssr dev server tests These old tests take longer to run (cherry picked from commit 0e6344019ed54c23a7ba17b4b75cb9e16f90feab) --- .../src/builders/ssr-dev-server/specs/proxy_spec.ts | 9 +++++++++ .../src/builders/ssr-dev-server/specs/ssl_spec.ts | 9 +++++++++ .../src/builders/ssr-dev-server/specs/works_spec.ts | 9 +++++++++ 3 files changed, 27 insertions(+) diff --git a/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/proxy_spec.ts b/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/proxy_spec.ts index 4f1db1d17b17..a4cafd66ee06 100644 --- a/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/proxy_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/proxy_spec.ts @@ -15,8 +15,17 @@ import { SSRDevServerBuilderOutput } from '../index'; describe('Serve SSR Builder', () => { const target = { project: 'app', target: 'serve-ssr' }; + const originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; let architect: Architect; + beforeAll(() => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = 100_000; + }); + + afterAll(() => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; + }); + beforeEach(async () => { await host.initialize().toPromise(); architect = (await createArchitect(host.root())).architect; diff --git a/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/ssl_spec.ts b/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/ssl_spec.ts index 6182b2e2baba..3792d87f839c 100644 --- a/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/ssl_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/ssl_spec.ts @@ -15,8 +15,17 @@ import { SSRDevServerBuilderOutput } from '../index'; describe('Serve SSR Builder', () => { const target = { project: 'app', target: 'serve-ssr' }; + const originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; let architect: Architect; + beforeAll(() => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = 100_000; + }); + + afterAll(() => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; + }); + beforeEach(async () => { await host.initialize().toPromise(); architect = (await createArchitect(host.root())).architect; diff --git a/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/works_spec.ts b/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/works_spec.ts index 5944eb31c09d..8e92c4d666e3 100644 --- a/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/works_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/works_spec.ts @@ -14,8 +14,17 @@ import { SSRDevServerBuilderOutput } from '../index'; describe('Serve SSR Builder', () => { const target = { project: 'app', target: 'serve-ssr' }; + const originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; let architect: Architect; + beforeAll(() => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = 100_000; + }); + + afterAll(() => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; + }); + beforeEach(async () => { await host.initialize().toPromise(); architect = (await createArchitect(host.root())).architect; From 2784883ecfb63e4aa6a6c69fd10e457316b4958c Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 7 Jul 2025 14:29:01 -0400 Subject: [PATCH 19/65] fix(@angular/build): support extra test setup files with unit-test vitest runner When using the experimental unit-test builder with Vitest, a new `setupFiles` option is now available. This option is similar to the Vitest option in that it allows for setup and configuration prior to each test. The `setupFiles` are executed after any application polyfills as well as after the TestBed initialization. If custom TestBed initialization is needed (this is not typical), The TestBed environment can first be reset in a setupFile and then initialized as needed. Note that resetting the TestBed environment in this way will cause the `providersFile` to no longer add any providers to the tests and any custom providers would need to be manually added during initialization. (cherry picked from commit 1ef02b7415a215734fef0363d113a5bd793c5893) --- goldens/public-api/angular/build/index.api.md | 1 + .../build/src/builders/unit-test/builder.ts | 6 ++++-- .../build/src/builders/unit-test/karma-bridge.ts | 6 ++++++ .../build/src/builders/unit-test/options.ts | 3 +++ .../build/src/builders/unit-test/schema.json | 16 +++++++++++++++- 5 files changed, 29 insertions(+), 3 deletions(-) diff --git a/goldens/public-api/angular/build/index.api.md b/goldens/public-api/angular/build/index.api.md index 6aab303b4c9e..19869c39699c 100644 --- a/goldens/public-api/angular/build/index.api.md +++ b/goldens/public-api/angular/build/index.api.md @@ -226,6 +226,7 @@ export type UnitTestBuilderOptions = { providersFile?: string; reporters?: string[]; runner: Runner; + setupFiles?: string[]; tsConfig: string; watch?: boolean; }; diff --git a/packages/angular/build/src/builders/unit-test/builder.ts b/packages/angular/build/src/builders/unit-test/builder.ts index f056dd4d10b0..014d41cffb64 100644 --- a/packages/angular/build/src/builders/unit-test/builder.ts +++ b/packages/angular/build/src/builders/unit-test/builder.ts @@ -197,9 +197,11 @@ export async function* execute( } // Add setup file entries for TestBed initialization and project polyfills - const setupFiles = ['init-testbed.js']; + const setupFiles = ['init-testbed.js', ...normalizedOptions.setupFiles]; if (buildTargetOptions?.polyfills?.length) { - setupFiles.push('polyfills.js'); + // Placed first as polyfills may be required by the Testbed initialization + // or other project provided setup files (e.g., zone.js, ECMAScript polyfills). + setupFiles.unshift('polyfills.js'); } const debugOptions = normalizedOptions.debug ? { diff --git a/packages/angular/build/src/builders/unit-test/karma-bridge.ts b/packages/angular/build/src/builders/unit-test/karma-bridge.ts index 4fa7b085802c..7bfe9a1ebe94 100644 --- a/packages/angular/build/src/builders/unit-test/karma-bridge.ts +++ b/packages/angular/build/src/builders/unit-test/karma-bridge.ts @@ -21,6 +21,12 @@ export async function useKarmaBuilder( ); } + if (unitTestOptions.setupFiles.length) { + context.logger.warn( + 'The "karma" test runner does not support the "setupFiles" option. The option will be ignored.', + ); + } + const buildTargetOptions = (await context.validateOptions( await context.getTargetOptions(unitTestOptions.buildTarget), await context.getBuilderNameForTarget(unitTestOptions.buildTarget), diff --git a/packages/angular/build/src/builders/unit-test/options.ts b/packages/angular/build/src/builders/unit-test/options.ts index 43147a23c065..c1d4b8a308a1 100644 --- a/packages/angular/build/src/builders/unit-test/options.ts +++ b/packages/angular/build/src/builders/unit-test/options.ts @@ -62,6 +62,9 @@ export async function normalizeOptions( watch: options.watch ?? isTTY(), debug: options.debug ?? false, providersFile: options.providersFile && path.join(workspaceRoot, options.providersFile), + setupFiles: options.setupFiles + ? options.setupFiles.map((setupFile) => path.join(workspaceRoot, setupFile)) + : [], }; } diff --git a/packages/angular/build/src/builders/unit-test/schema.json b/packages/angular/build/src/builders/unit-test/schema.json index 7c54babe0fda..0f54d813ace1 100644 --- a/packages/angular/build/src/builders/unit-test/schema.json +++ b/packages/angular/build/src/builders/unit-test/schema.json @@ -76,7 +76,14 @@ "type": "array", "minItems": 1, "maxItems": 2, - "items": [{ "$ref": "#/definitions/coverage-reporters" }, { "type": "object" }] + "items": [ + { + "$ref": "#/definitions/coverage-reporters" + }, + { + "type": "object" + } + ] } ] } @@ -92,6 +99,13 @@ "type": "string", "description": "TypeScript file that exports an array of Angular providers to use during test execution. The array must be a default export.", "minLength": 1 + }, + "setupFiles": { + "type": "array", + "items": { + "type": "string" + }, + "description": "A list of global setup and configuration files that are included before the test files. The application's polyfills are always included before these files. The Angular Testbed is also initialized prior to the execution of these files." } }, "additionalProperties": false, From aea3ed808ed92d554f03ba266afb35470edfae18 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Wed, 9 Jul 2025 12:35:24 +0000 Subject: [PATCH 20/65] fix(@angular/build): exclude `@vitest/browser/context` from esbuild bundling Bundling this module causes unit tests to fail with `@vitest/browser/context can be imported only inside the Browser Mode. Your test is running in browser pool. Make sure your regular tests are excluded from the "test.include" glob pattern.`, This is because `@vitest/browser/context` is a virtual mode in vite and the package on NPM is dummy that is used for static analysis. Closes: #30677 (cherry picked from commit 9e292f1c16b563291caa523a6433e4ecc9b9b344) --- packages/angular/build/src/builders/unit-test/builder.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/angular/build/src/builders/unit-test/builder.ts b/packages/angular/build/src/builders/unit-test/builder.ts index 014d41cffb64..fd9f88580d70 100644 --- a/packages/angular/build/src/builders/unit-test/builder.ts +++ b/packages/angular/build/src/builders/unit-test/builder.ts @@ -138,7 +138,11 @@ export async function* execute( optimization: false, tsConfig: normalizedOptions.tsConfig, entryPoints, - externalDependencies: ['vitest', ...(buildTargetOptions.externalDependencies ?? [])], + externalDependencies: [ + 'vitest', + '@vitest/browser/context', + ...(buildTargetOptions.externalDependencies ?? []), + ], }; extensions ??= {}; extensions.codePlugins ??= []; From bf6823db5592ef4c2038790e7e92b49c1f9fbea6 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 9 Jul 2025 16:19:50 -0400 Subject: [PATCH 21/65] build: update Angular versions for 20.1 stable The package versions for Angular related packages have been adjusted to reflect the stable release of Angular 20.1.0. --- constants.bzl | 8 +- package.json | 28 +- packages/angular/build/package.json | 2 +- packages/angular/ssr/package.json | 12 +- .../angular_devkit/build_angular/package.json | 2 +- packages/ngtools/webpack/package.json | 4 +- pnpm-lock.yaml | 415 ++++++++++-------- 7 files changed, 269 insertions(+), 202 deletions(-) diff --git a/constants.bzl b/constants.bzl index 5f612cb4ed17..5a343d327aec 100644 --- a/constants.bzl +++ b/constants.bzl @@ -3,10 +3,10 @@ RELEASE_ENGINES_NODE = "^20.19.0 || ^22.12.0 || >=24.0.0" RELEASE_ENGINES_NPM = "^6.11.0 || ^7.5.6 || >=8.0.0" RELEASE_ENGINES_YARN = ">= 1.13.0" -NG_PACKAGR_VERSION = "^20.1.0-next.0" -ANGULAR_FW_VERSION = "^20.1.0-next.0" -ANGULAR_FW_PEER_DEP = "^20.0.0 || ^20.1.0-next.0" -NG_PACKAGR_PEER_DEP = "^20.0.0 || ^20.1.0-next.0" +NG_PACKAGR_VERSION = "^20.1.0" +ANGULAR_FW_VERSION = "^20.1.0" +ANGULAR_FW_PEER_DEP = "^20.0.0" +NG_PACKAGR_PEER_DEP = "^20.0.0" # Baseline widely-available date in `YYYY-MM-DD` format which defines Angular's # browser support. This date serves as the source of truth for the Angular CLI's diff --git a/package.json b/package.json index fecfd42ac53f..3b3427420e1e 100644 --- a/package.json +++ b/package.json @@ -46,20 +46,20 @@ }, "homepage": "https://github.com/angular/angular-cli", "devDependencies": { - "@angular/animations": "20.1.0-next.3", - "@angular/cdk": "20.1.0-next.2", - "@angular/common": "20.1.0-next.3", - "@angular/compiler": "20.1.0-next.3", - "@angular/compiler-cli": "20.1.0-next.3", - "@angular/core": "20.1.0-next.3", - "@angular/forms": "20.1.0-next.3", - "@angular/localize": "20.1.0-next.3", - "@angular/material": "20.1.0-next.2", - "@angular/ng-dev": "https://github.com/angular/dev-infra-private-ng-dev-builds.git#8fe809d31ea3536087ca56cf7ff9ddd544dba658", - "@angular/platform-browser": "20.1.0-next.3", - "@angular/platform-server": "20.1.0-next.3", - "@angular/router": "20.1.0-next.3", - "@angular/service-worker": "20.1.0-next.3", + "@angular/animations": "20.1.0", + "@angular/cdk": "20.1.0", + "@angular/common": "20.1.0", + "@angular/compiler": "20.1.0", + "@angular/compiler-cli": "20.1.0", + "@angular/core": "20.1.0", + "@angular/forms": "20.1.0", + "@angular/localize": "20.1.0", + "@angular/material": "20.1.0", + "@angular/ng-dev": "https://github.com/angular/dev-infra-private-ng-dev-builds.git#800f6e7be48e84780621f8f7e9eec79a865346fd", + "@angular/platform-browser": "20.1.0", + "@angular/platform-server": "20.1.0", + "@angular/router": "20.1.0", + "@angular/service-worker": "20.1.0", "@bazel/bazelisk": "1.26.0", "@bazel/buildifier": "8.2.1", "@eslint/compat": "1.3.1", diff --git a/packages/angular/build/package.json b/packages/angular/build/package.json index 50fdeb3f0922..fa846025666f 100644 --- a/packages/angular/build/package.json +++ b/packages/angular/build/package.json @@ -53,7 +53,7 @@ "@angular-devkit/core": "workspace:*", "jsdom": "26.1.0", "less": "4.3.0", - "ng-packagr": "20.1.0-next.0", + "ng-packagr": "20.1.0", "postcss": "8.5.6", "rxjs": "7.8.2", "vitest": "3.2.4" diff --git a/packages/angular/ssr/package.json b/packages/angular/ssr/package.json index fb2fab874853..95041fba6135 100644 --- a/packages/angular/ssr/package.json +++ b/packages/angular/ssr/package.json @@ -29,12 +29,12 @@ }, "devDependencies": { "@angular-devkit/schematics": "workspace:*", - "@angular/common": "20.1.0-next.3", - "@angular/compiler": "20.1.0-next.3", - "@angular/core": "20.1.0-next.3", - "@angular/platform-browser": "20.1.0-next.3", - "@angular/platform-server": "20.1.0-next.3", - "@angular/router": "20.1.0-next.3", + "@angular/common": "20.1.0", + "@angular/compiler": "20.1.0", + "@angular/core": "20.1.0", + "@angular/platform-browser": "20.1.0", + "@angular/platform-server": "20.1.0", + "@angular/router": "20.1.0", "@schematics/angular": "workspace:*" }, "sideEffects": false, diff --git a/packages/angular_devkit/build_angular/package.json b/packages/angular_devkit/build_angular/package.json index 4af04a0eea28..b1de9559c2b3 100644 --- a/packages/angular_devkit/build_angular/package.json +++ b/packages/angular_devkit/build_angular/package.json @@ -68,7 +68,7 @@ "@angular/ssr": "workspace:*", "@web/test-runner": "0.20.2", "browser-sync": "3.0.4", - "ng-packagr": "20.1.0-next.0", + "ng-packagr": "20.1.0", "undici": "7.11.0" }, "peerDependencies": { diff --git a/packages/ngtools/webpack/package.json b/packages/ngtools/webpack/package.json index 622a5b9ce86d..6cd84a7561f6 100644 --- a/packages/ngtools/webpack/package.json +++ b/packages/ngtools/webpack/package.json @@ -27,8 +27,8 @@ }, "devDependencies": { "@angular-devkit/core": "workspace:0.0.0-PLACEHOLDER", - "@angular/compiler": "20.1.0-next.3", - "@angular/compiler-cli": "20.1.0-next.3", + "@angular/compiler": "20.1.0", + "@angular/compiler-cli": "20.1.0", "typescript": "5.8.3", "webpack": "5.99.9" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a5a00e7b18fe..bb584e2d4bdc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,47 +15,47 @@ importers: .: devDependencies: '@angular/animations': - specifier: 20.1.0-next.3 - version: 20.1.0-next.3(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1)) + specifier: 20.1.0 + version: 20.1.0(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1)) '@angular/cdk': - specifier: 20.1.0-next.2 - version: 20.1.0-next.2(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) + specifier: 20.1.0 + version: 20.1.0(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) '@angular/common': - specifier: 20.1.0-next.3 - version: 20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) + specifier: 20.1.0 + version: 20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) '@angular/compiler': - specifier: 20.1.0-next.3 - version: 20.1.0-next.3 + specifier: 20.1.0 + version: 20.1.0 '@angular/compiler-cli': - specifier: 20.1.0-next.3 - version: 20.1.0-next.3(@angular/compiler@20.1.0-next.3)(typescript@5.8.3) + specifier: 20.1.0 + version: 20.1.0(@angular/compiler@20.1.0)(typescript@5.8.3) '@angular/core': - specifier: 20.1.0-next.3 - version: 20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1) + specifier: 20.1.0 + version: 20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1) '@angular/forms': - specifier: 20.1.0-next.3 - version: 20.1.0-next.3(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.1.0-next.3(@angular/animations@20.1.0-next.3(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2) + specifier: 20.1.0 + version: 20.1.0(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.1.0(@angular/animations@20.1.0(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2) '@angular/localize': - specifier: 20.1.0-next.3 - version: 20.1.0-next.3(@angular/compiler-cli@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(typescript@5.8.3))(@angular/compiler@20.1.0-next.3) + specifier: 20.1.0 + version: 20.1.0(@angular/compiler-cli@20.1.0(@angular/compiler@20.1.0)(typescript@5.8.3))(@angular/compiler@20.1.0) '@angular/material': - specifier: 20.1.0-next.2 - version: 20.1.0-next.2(qcbtsxazl5mfvqry4icthrqdfq) + specifier: 20.1.0 + version: 20.1.0(eyz3ivwkchkj2fzdwhjlf4itye) '@angular/ng-dev': - specifier: https://github.com/angular/dev-infra-private-ng-dev-builds.git#8fe809d31ea3536087ca56cf7ff9ddd544dba658 - version: https://codeload.github.com/angular/dev-infra-private-ng-dev-builds/tar.gz/8fe809d31ea3536087ca56cf7ff9ddd544dba658(@modelcontextprotocol/sdk@1.13.3)(encoding@0.1.13) + specifier: https://github.com/angular/dev-infra-private-ng-dev-builds.git#800f6e7be48e84780621f8f7e9eec79a865346fd + version: https://codeload.github.com/angular/dev-infra-private-ng-dev-builds/tar.gz/800f6e7be48e84780621f8f7e9eec79a865346fd(@modelcontextprotocol/sdk@1.13.3)(encoding@0.1.13) '@angular/platform-browser': - specifier: 20.1.0-next.3 - version: 20.1.0-next.3(@angular/animations@20.1.0-next.3(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1)) + specifier: 20.1.0 + version: 20.1.0(@angular/animations@20.1.0(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1)) '@angular/platform-server': - specifier: 20.1.0-next.3 - version: 20.1.0-next.3(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@20.1.0-next.3)(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.1.0-next.3(@angular/animations@20.1.0-next.3(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2) + specifier: 20.1.0 + version: 20.1.0(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@20.1.0)(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.1.0(@angular/animations@20.1.0(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2) '@angular/router': - specifier: 20.1.0-next.3 - version: 20.1.0-next.3(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.1.0-next.3(@angular/animations@20.1.0-next.3(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2) + specifier: 20.1.0 + version: 20.1.0(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.1.0(@angular/animations@20.1.0(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2) '@angular/service-worker': - specifier: 20.1.0-next.3 - version: 20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) + specifier: 20.1.0 + version: 20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) '@bazel/bazelisk': specifier: 1.26.0 version: 1.26.0 @@ -442,8 +442,8 @@ importers: specifier: 4.3.0 version: 4.3.0 ng-packagr: - specifier: 20.1.0-next.0 - version: 20.1.0-next.0(@angular/compiler-cli@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(typescript@5.8.3))(tslib@2.8.1)(typescript@5.8.3) + specifier: 20.1.0 + version: 20.1.0(@angular/compiler-cli@20.1.0(@angular/compiler@20.1.0)(typescript@5.8.3))(tslib@2.8.1)(typescript@5.8.3) postcss: specifier: 8.5.6 version: 8.5.6 @@ -533,23 +533,23 @@ importers: specifier: workspace:* version: link:../../angular_devkit/schematics '@angular/common': - specifier: 20.1.0-next.3 - version: 20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) + specifier: 20.1.0 + version: 20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) '@angular/compiler': - specifier: 20.1.0-next.3 - version: 20.1.0-next.3 + specifier: 20.1.0 + version: 20.1.0 '@angular/core': - specifier: 20.1.0-next.3 - version: 20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1) + specifier: 20.1.0 + version: 20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1) '@angular/platform-browser': - specifier: 20.1.0-next.3 - version: 20.1.0-next.3(@angular/animations@20.1.0-next.3(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1)) + specifier: 20.1.0 + version: 20.1.0(@angular/animations@20.1.0(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1)) '@angular/platform-server': - specifier: 20.1.0-next.3 - version: 20.1.0-next.3(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@20.1.0-next.3)(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.1.0-next.3(@angular/animations@20.1.0-next.3(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2) + specifier: 20.1.0 + version: 20.1.0(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@20.1.0)(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.1.0(@angular/animations@20.1.0(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2) '@angular/router': - specifier: 20.1.0-next.3 - version: 20.1.0-next.3(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.1.0-next.3(@angular/animations@20.1.0-next.3(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2) + specifier: 20.1.0 + version: 20.1.0(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.1.0(@angular/animations@20.1.0(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2) '@schematics/angular': specifier: workspace:* version: link:../../schematics/angular @@ -764,8 +764,8 @@ importers: specifier: 3.0.4 version: 3.0.4 ng-packagr: - specifier: 20.1.0-next.0 - version: 20.1.0-next.0(@angular/compiler-cli@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(typescript@5.8.3))(tslib@2.8.1)(typescript@5.8.3) + specifier: 20.1.0 + version: 20.1.0(@angular/compiler-cli@20.1.0(@angular/compiler@20.1.0)(typescript@5.8.3))(tslib@2.8.1)(typescript@5.8.3) undici: specifier: 7.11.0 version: 7.11.0 @@ -859,11 +859,11 @@ importers: specifier: workspace:0.0.0-PLACEHOLDER version: link:../../angular_devkit/core '@angular/compiler': - specifier: 20.1.0-next.3 - version: 20.1.0-next.3 + specifier: 20.1.0 + version: 20.1.0 '@angular/compiler-cli': - specifier: 20.1.0-next.3 - version: 20.1.0-next.3(@angular/compiler@20.1.0-next.3)(typescript@5.8.3) + specifier: 20.1.0 + version: 20.1.0(@angular/compiler@20.1.0)(typescript@5.8.3) typescript: specifier: 5.8.3 version: 5.8.3 @@ -907,47 +907,47 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} - '@angular/animations@20.1.0-next.3': - resolution: {integrity: sha512-dLwFETixX6F3gKMVzNgPrG3Jj3pRbKldyZlBjzXJzlXq0jf/SnJjKTG73Lur4I7WIlIQYi/qGuRGTKyBFxs7jg==} + '@angular/animations@20.1.0': + resolution: {integrity: sha512-5ILngsvu5VPQYaIm7lRyegZaDaAEtLUIPSS8h1dzWPaCxBIJ4uwzx9RDMiF32zhbxi+q0mAO2w2FdDlzWTT3og==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: - '@angular/common': 20.1.0-next.3 - '@angular/core': 20.1.0-next.3 + '@angular/common': 20.1.0 + '@angular/core': 20.1.0 - '@angular/cdk@20.1.0-next.2': - resolution: {integrity: sha512-fH+XDVUep7UUpG6Cg83Fw/EPwjOBGFGWTipBDzgglWcVyVqiRteBKHn2XaOMDdV8Pr/igX9bMn1Rzj713Y9ZzQ==} + '@angular/cdk@20.1.0': + resolution: {integrity: sha512-JhgbSOv7xZqWNZjuCh8A3A7pGv0mhtmGjHo36157LrxRO6R7x2yJJjxC5nQeroKZWhgN+X/jG/EJlzEvl9PxTw==} peerDependencies: - '@angular/common': ^20.0.0-0 || ^20.1.0-0 || ^20.2.0-0 || ^20.3.0-0 || ^21.0.0-0 - '@angular/core': ^20.0.0-0 || ^20.1.0-0 || ^20.2.0-0 || ^20.3.0-0 || ^21.0.0-0 + '@angular/common': ^20.0.0 || ^21.0.0 + '@angular/core': ^20.0.0 || ^21.0.0 rxjs: ^6.5.3 || ^7.4.0 - '@angular/common@20.1.0-next.3': - resolution: {integrity: sha512-pxn5e0yEJzaYIWJs4/hg1O0qjPHPR/ZgiW0tvnxzZDL/Lefzhnz1ZVKw3aDky/V9JFG1Ze0D4KadVjsT4b8FNQ==} + '@angular/common@20.1.0': + resolution: {integrity: sha512-RsHClHJux+4lXrHdGHVw22wekRbSjYtx6Xwjox2S+IRPP51CbX0KskAALZ9ZmtCttkYSFVtvr0S+SQrU2cu5WA==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: - '@angular/core': 20.1.0-next.3 + '@angular/core': 20.1.0 rxjs: ^6.5.3 || ^7.4.0 - '@angular/compiler-cli@20.1.0-next.3': - resolution: {integrity: sha512-xIB9meGhoTsxp56Mhj/km5CIRO1wW0ZoeaeMn1yatzSIMJXEYMw2GHq3Ro3HowMLwvvv2xebx9P6qmAaqX74Cw==} + '@angular/compiler-cli@20.1.0': + resolution: {integrity: sha512-ajbCmvYYFxeXRdKSfdHjp62MZ2lCMUS0UzswBDAbT9sPd/ThppbvLXLsMBj8SlwaXSSBeTAa1oSHEO1MeuVvGQ==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} hasBin: true peerDependencies: - '@angular/compiler': 20.1.0-next.3 + '@angular/compiler': 20.1.0 typescript: 5.8.3 peerDependenciesMeta: typescript: optional: true - '@angular/compiler@20.1.0-next.3': - resolution: {integrity: sha512-I6za3dJ50Kx/XRH1txmUgxJyobthSnEqmNkfncE0RfbtrJ9nQ0grVwn+FYCS7UR2zP/rjMBmudjYRd4kWUiVug==} + '@angular/compiler@20.1.0': + resolution: {integrity: sha512-sM8H3dJotIDDmI1u8qGuAn16XVfR7A4+/5s5cKLI/osnnIjafi5HHqAf76R5IlGoIv0ZHVQIYaJ/Qdvfyvdhfg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - '@angular/core@20.1.0-next.3': - resolution: {integrity: sha512-QUYew+W6NyP7oiQ5XgKP1fPeT2MJiy+SlzkxxfP2tzQBh/c5tjSlHvj12hF+WeyKe94wirz3sMnCP7tfMB6jlA==} + '@angular/core@20.1.0': + resolution: {integrity: sha512-/dJooZi+OAACkjWgGMPrOOGikdtlTJXwdeXPJTgZSUD5L8oQMbhZFG0XW/1Hldvsti87wPjZPz67ivB7zR86VA==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: - '@angular/compiler': 20.1.0-next.3 + '@angular/compiler': 20.1.0 rxjs: ^6.5.3 || ^7.4.0 zone.js: ~0.15.0 peerDependenciesMeta: @@ -956,74 +956,74 @@ packages: zone.js: optional: true - '@angular/forms@20.1.0-next.3': - resolution: {integrity: sha512-y9QxqhkuzuwQ+FhDrrJDiTTrDWk+X9n9iESxqOB1Ixo+HvS0qDrKJU/GT4IcaXzbYfXg75jjbqv5G7ouLBGAUw==} + '@angular/forms@20.1.0': + resolution: {integrity: sha512-NgQxowyyG2yiSOXxtQS1xK1vAQT+4GRoMFuzmS3uBshIifgCgFckSxJHQXhlQOInuv2NsZ1Q0HuCvao+yZfIow==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: - '@angular/common': 20.1.0-next.3 - '@angular/core': 20.1.0-next.3 - '@angular/platform-browser': 20.1.0-next.3 + '@angular/common': 20.1.0 + '@angular/core': 20.1.0 + '@angular/platform-browser': 20.1.0 rxjs: ^6.5.3 || ^7.4.0 - '@angular/localize@20.1.0-next.3': - resolution: {integrity: sha512-NjdkhxMRKCV6h/xjfgLFlohx5cOTR98Z2e4WJJtxzzWwAIFN+XDoDOuVjxM8pt0KD8ON/Dxkab9T+kc04LR+wg==} + '@angular/localize@20.1.0': + resolution: {integrity: sha512-ZTAxJkLmYxBxeHVSf3VMY1qivlypxGsJy90LRzZl8KeYROt6g8NQ0MXO8M4Y+0+dXUTZDQcYcGq8TFxpMw1fqQ==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} hasBin: true peerDependencies: - '@angular/compiler': 20.1.0-next.3 - '@angular/compiler-cli': 20.1.0-next.3 + '@angular/compiler': 20.1.0 + '@angular/compiler-cli': 20.1.0 - '@angular/material@20.1.0-next.2': - resolution: {integrity: sha512-p8b3fRINdIenk05WT3A0OMIg3sIy7FbUA99XopXVuSBJdf0yvSKPTX+fu3QBXESgVSVsZauM972gVqJdlGd6Yw==} + '@angular/material@20.1.0': + resolution: {integrity: sha512-LfGz/V/kZwRIhzIZBiurM4Wc5CQiiJkiOChUfoEOvQLN2hckPFZbbvtg6JwxxA6nhzsDhuGHbj7Xj5dNsLfZLw==} peerDependencies: - '@angular/cdk': 20.1.0-next.2 - '@angular/common': ^20.0.0-0 || ^20.1.0-0 || ^20.2.0-0 || ^20.3.0-0 || ^21.0.0-0 - '@angular/core': ^20.0.0-0 || ^20.1.0-0 || ^20.2.0-0 || ^20.3.0-0 || ^21.0.0-0 - '@angular/forms': ^20.0.0-0 || ^20.1.0-0 || ^20.2.0-0 || ^20.3.0-0 || ^21.0.0-0 - '@angular/platform-browser': ^20.0.0-0 || ^20.1.0-0 || ^20.2.0-0 || ^20.3.0-0 || ^21.0.0-0 + '@angular/cdk': 20.1.0 + '@angular/common': ^20.0.0 || ^21.0.0 + '@angular/core': ^20.0.0 || ^21.0.0 + '@angular/forms': ^20.0.0 || ^21.0.0 + '@angular/platform-browser': ^20.0.0 || ^21.0.0 rxjs: ^6.5.3 || ^7.4.0 - '@angular/ng-dev@https://codeload.github.com/angular/dev-infra-private-ng-dev-builds/tar.gz/8fe809d31ea3536087ca56cf7ff9ddd544dba658': - resolution: {tarball: https://codeload.github.com/angular/dev-infra-private-ng-dev-builds/tar.gz/8fe809d31ea3536087ca56cf7ff9ddd544dba658} - version: 0.0.0-dfe138678e4edb4789fbe40ae7792c046de3b4bd + '@angular/ng-dev@https://codeload.github.com/angular/dev-infra-private-ng-dev-builds/tar.gz/800f6e7be48e84780621f8f7e9eec79a865346fd': + resolution: {tarball: https://codeload.github.com/angular/dev-infra-private-ng-dev-builds/tar.gz/800f6e7be48e84780621f8f7e9eec79a865346fd} + version: 0.0.0-6f54d143077baef582d70873722166fdc040066c hasBin: true - '@angular/platform-browser@20.1.0-next.3': - resolution: {integrity: sha512-LAqNlGAo/K4qQdijOq8rwPvvWgF+CpM7007BaYNIIuFyKrsvVPxIXHBs6lWoGWtLDdv+f0RJLYLrvYLjjoSWhw==} + '@angular/platform-browser@20.1.0': + resolution: {integrity: sha512-l3+Ijq5SFxT0v10DbOyMc7NzGdbK76yot2i8pXyArlPSPmpWvbbjXbiBqzrv3TSTrksHBhG3mMvyhTmHQ1cQFA==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: - '@angular/animations': 20.1.0-next.3 - '@angular/common': 20.1.0-next.3 - '@angular/core': 20.1.0-next.3 + '@angular/animations': 20.1.0 + '@angular/common': 20.1.0 + '@angular/core': 20.1.0 peerDependenciesMeta: '@angular/animations': optional: true - '@angular/platform-server@20.1.0-next.3': - resolution: {integrity: sha512-v2XyJwDRSYhJuUelzUYP+Okt8gB4Y70V2u9EV5oE2qykreUs7qZH2LzokihpE7VwLo31gCw+mahvx2AyvOCeAw==} + '@angular/platform-server@20.1.0': + resolution: {integrity: sha512-LoQVckLKprNY9HEtIUn48xL+cj8Eqr2iFqRJl8t523tYslXnJ1jnqUG6YCXZJBPeNOl9aF1IJ7/zbfzWYhTIBg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: - '@angular/common': 20.1.0-next.3 - '@angular/compiler': 20.1.0-next.3 - '@angular/core': 20.1.0-next.3 - '@angular/platform-browser': 20.1.0-next.3 + '@angular/common': 20.1.0 + '@angular/compiler': 20.1.0 + '@angular/core': 20.1.0 + '@angular/platform-browser': 20.1.0 rxjs: ^6.5.3 || ^7.4.0 - '@angular/router@20.1.0-next.3': - resolution: {integrity: sha512-1HelGy8NP/B1sm1W+ms3LSGBGBXbdwsK1HuVXHkLjnNB8KgD3xtziiYzAsHtaNFuxZGmD4jcWV2wprFnJ2RcBA==} + '@angular/router@20.1.0': + resolution: {integrity: sha512-fuUX1+AhcVSDgSSx85o6VOtXKM3oXAza+44jQ+nJGf316P0xpLKA586DKRNPjS4sRsWM7otKuOOTXXc4AMUHpQ==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: - '@angular/common': 20.1.0-next.3 - '@angular/core': 20.1.0-next.3 - '@angular/platform-browser': 20.1.0-next.3 + '@angular/common': 20.1.0 + '@angular/core': 20.1.0 + '@angular/platform-browser': 20.1.0 rxjs: ^6.5.3 || ^7.4.0 - '@angular/service-worker@20.1.0-next.3': - resolution: {integrity: sha512-tWWfI80OvpOIx9DIS7CAHVzY+Q8igJX08mdnAuQX+s/lllsCJV9/nyTlWU8A/NLv4hcOXqDLZrt85LGkgQ1YaA==} + '@angular/service-worker@20.1.0': + resolution: {integrity: sha512-ulJwc6L6QCYEjKNycsDHhzUneGqADFkdqeEBctXWym0adkngsjUUBzF4jCtT0KoCRHOcENfPLa1o1TaF0KkC7A==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} hasBin: true peerDependencies: - '@angular/core': 20.1.0-next.3 + '@angular/core': 20.1.0 rxjs: ^6.5.3 || ^7.4.0 '@asamuzakjp/css-color@3.2.0': @@ -1037,18 +1037,22 @@ packages: resolution: {integrity: sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==} engines: {node: '>=6.9.0'} - '@babel/core@7.27.4': - resolution: {integrity: sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==} - engines: {node: '>=6.9.0'} - '@babel/core@7.27.7': resolution: {integrity: sha512-BU2f9tlKQ5CAthiMIgpzAh4eDTLWo1mqi9jqE2OxMG0E/OM199VJt2q8BztTxpnSW0i1ymdwLXRJnYzvDM5r2w==} engines: {node: '>=6.9.0'} + '@babel/core@7.28.0': + resolution: {integrity: sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==} + engines: {node: '>=6.9.0'} + '@babel/generator@7.27.5': resolution: {integrity: sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==} engines: {node: '>=6.9.0'} + '@babel/generator@7.28.0': + resolution: {integrity: sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==} + engines: {node: '>=6.9.0'} + '@babel/helper-annotate-as-pure@7.27.3': resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} engines: {node: '>=6.9.0'} @@ -1074,6 +1078,10 @@ packages: peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + '@babel/helper-member-expression-to-functions@7.27.1': resolution: {integrity: sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==} engines: {node: '>=6.9.0'} @@ -1146,6 +1154,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.28.0': + resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1': resolution: {integrity: sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==} engines: {node: '>=6.9.0'} @@ -1533,6 +1546,10 @@ packages: resolution: {integrity: sha512-X6ZlfR/O/s5EQ/SnUSLzr+6kGnkg8HXGMzpgsMsrJVcfDtH1vIp6ctCN4eZ1LS5c0+te5Cb6Y514fASjMRJ1nw==} engines: {node: '>=6.9.0'} + '@babel/traverse@7.28.0': + resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==} + engines: {node: '>=6.9.0'} + '@babel/types@7.27.6': resolution: {integrity: sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==} engines: {node: '>=6.9.0'} @@ -1541,6 +1558,10 @@ packages: resolution: {integrity: sha512-8OLQgDScAOHXnAz2cV+RfzzNMipuLVBz2biuAJFMV9bfkNf393je3VM8CLkjQodW5+iWsSJdSgSWT6rsZoXHPw==} engines: {node: '>=6.9.0'} + '@babel/types@7.28.0': + resolution: {integrity: sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==} + engines: {node: '>=6.9.0'} + '@bazel/bazelisk@1.26.0': resolution: {integrity: sha512-bTNcHdGyEQ9r7SczEYUa0gkEQhJo1ld2BjXI8fWBvsUeoHi03QpUs2HZgDbjjrpQFQqG2ZbO7ihZvH8MjhUTHw==} hasBin: true @@ -2003,6 +2024,9 @@ packages: resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} + '@jridgewell/gen-mapping@0.3.12': + resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} + '@jridgewell/gen-mapping@0.3.8': resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} engines: {node: '>=6.0.0'} @@ -2024,6 +2048,9 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jridgewell/trace-mapping@0.3.29': + resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} + '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} @@ -6147,12 +6174,12 @@ packages: resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} engines: {node: '>= 0.4.0'} - ng-packagr@20.1.0-next.0: - resolution: {integrity: sha512-CIyRatR8O5jWyGZkKHSwKhehvh2AL6a90MvvPY9P2H6NtgpZO5hhSja5GRrexRs7sS71i9pXj/HEu+Phec56hg==} + ng-packagr@20.1.0: + resolution: {integrity: sha512-objHk39HWnSSv54KD0Ct4A02rug6HiqbmXo1KJW39npzuVc37QWfiZy94afltH1zIx+mQqollmGaCmwibmagvQ==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} hasBin: true peerDependencies: - '@angular/compiler-cli': ^20.0.0 || ^20.0.0-next.0 || ^20.1.0-next.0 + '@angular/compiler-cli': ^20.0.0 || ^20.1.0-next.0 || ^20.2.0-next.0 tailwindcss: ^2.0.0 || ^3.0.0 || ^4.0.0 tslib: ^2.3.0 typescript: 5.8.3 @@ -6806,7 +6833,6 @@ packages: engines: {node: '>=0.6.0', teleport: '>=0.2.0'} deprecated: |- You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other. - (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) qjobs@1.2.0: @@ -8350,30 +8376,30 @@ snapshots: '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 - '@angular/animations@20.1.0-next.3(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))': + '@angular/animations@20.1.0(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))': dependencies: - '@angular/common': 20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) - '@angular/core': 20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1) + '@angular/common': 20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) + '@angular/core': 20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1) tslib: 2.8.1 - '@angular/cdk@20.1.0-next.2(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2)': + '@angular/cdk@20.1.0(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2)': dependencies: - '@angular/common': 20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) - '@angular/core': 20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1) + '@angular/common': 20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) + '@angular/core': 20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1) parse5: 7.3.0 rxjs: 7.8.2 tslib: 2.8.1 - '@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2)': + '@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2)': dependencies: - '@angular/core': 20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1) + '@angular/core': 20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1) rxjs: 7.8.2 tslib: 2.8.1 - '@angular/compiler-cli@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(typescript@5.8.3)': + '@angular/compiler-cli@20.1.0(@angular/compiler@20.1.0)(typescript@5.8.3)': dependencies: - '@angular/compiler': 20.1.0-next.3 - '@babel/core': 7.27.4 + '@angular/compiler': 20.1.0 + '@babel/core': 7.28.0 '@jridgewell/sourcemap-codec': 1.5.0 chokidar: 4.0.3 convert-source-map: 1.9.0 @@ -8386,48 +8412,48 @@ snapshots: transitivePeerDependencies: - supports-color - '@angular/compiler@20.1.0-next.3': + '@angular/compiler@20.1.0': dependencies: tslib: 2.8.1 - '@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1)': + '@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1)': dependencies: rxjs: 7.8.2 tslib: 2.8.1 optionalDependencies: - '@angular/compiler': 20.1.0-next.3 + '@angular/compiler': 20.1.0 zone.js: 0.15.1 - '@angular/forms@20.1.0-next.3(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.1.0-next.3(@angular/animations@20.1.0-next.3(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)': + '@angular/forms@20.1.0(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.1.0(@angular/animations@20.1.0(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)': dependencies: - '@angular/common': 20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) - '@angular/core': 20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1) - '@angular/platform-browser': 20.1.0-next.3(@angular/animations@20.1.0-next.3(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1)) + '@angular/common': 20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) + '@angular/core': 20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1) + '@angular/platform-browser': 20.1.0(@angular/animations@20.1.0(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1)) rxjs: 7.8.2 tslib: 2.8.1 - '@angular/localize@20.1.0-next.3(@angular/compiler-cli@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(typescript@5.8.3))(@angular/compiler@20.1.0-next.3)': + '@angular/localize@20.1.0(@angular/compiler-cli@20.1.0(@angular/compiler@20.1.0)(typescript@5.8.3))(@angular/compiler@20.1.0)': dependencies: - '@angular/compiler': 20.1.0-next.3 - '@angular/compiler-cli': 20.1.0-next.3(@angular/compiler@20.1.0-next.3)(typescript@5.8.3) - '@babel/core': 7.27.4 + '@angular/compiler': 20.1.0 + '@angular/compiler-cli': 20.1.0(@angular/compiler@20.1.0)(typescript@5.8.3) + '@babel/core': 7.28.0 '@types/babel__core': 7.20.5 tinyglobby: 0.2.14 yargs: 18.0.0 transitivePeerDependencies: - supports-color - '@angular/material@20.1.0-next.2(qcbtsxazl5mfvqry4icthrqdfq)': + '@angular/material@20.1.0(eyz3ivwkchkj2fzdwhjlf4itye)': dependencies: - '@angular/cdk': 20.1.0-next.2(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) - '@angular/common': 20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) - '@angular/core': 20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1) - '@angular/forms': 20.1.0-next.3(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.1.0-next.3(@angular/animations@20.1.0-next.3(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2) - '@angular/platform-browser': 20.1.0-next.3(@angular/animations@20.1.0-next.3(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1)) + '@angular/cdk': 20.1.0(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) + '@angular/common': 20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) + '@angular/core': 20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1) + '@angular/forms': 20.1.0(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.1.0(@angular/animations@20.1.0(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2) + '@angular/platform-browser': 20.1.0(@angular/animations@20.1.0(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1)) rxjs: 7.8.2 tslib: 2.8.1 - '@angular/ng-dev@https://codeload.github.com/angular/dev-infra-private-ng-dev-builds/tar.gz/8fe809d31ea3536087ca56cf7ff9ddd544dba658(@modelcontextprotocol/sdk@1.13.3)(encoding@0.1.13)': + '@angular/ng-dev@https://codeload.github.com/angular/dev-infra-private-ng-dev-builds/tar.gz/800f6e7be48e84780621f8f7e9eec79a865346fd(@modelcontextprotocol/sdk@1.13.3)(encoding@0.1.13)': dependencies: '@google-cloud/spanner': 8.0.0(supports-color@10.0.0) '@google/genai': 1.6.0(@modelcontextprotocol/sdk@1.13.3)(encoding@0.1.13)(supports-color@10.0.0) @@ -8448,35 +8474,35 @@ snapshots: - encoding - utf-8-validate - '@angular/platform-browser@20.1.0-next.3(@angular/animations@20.1.0-next.3(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))': + '@angular/platform-browser@20.1.0(@angular/animations@20.1.0(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))': dependencies: - '@angular/common': 20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) - '@angular/core': 20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1) + '@angular/common': 20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) + '@angular/core': 20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1) tslib: 2.8.1 optionalDependencies: - '@angular/animations': 20.1.0-next.3(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1)) + '@angular/animations': 20.1.0(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1)) - '@angular/platform-server@20.1.0-next.3(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@20.1.0-next.3)(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.1.0-next.3(@angular/animations@20.1.0-next.3(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)': + '@angular/platform-server@20.1.0(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@20.1.0)(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.1.0(@angular/animations@20.1.0(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)': dependencies: - '@angular/common': 20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) - '@angular/compiler': 20.1.0-next.3 - '@angular/core': 20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1) - '@angular/platform-browser': 20.1.0-next.3(@angular/animations@20.1.0-next.3(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1)) + '@angular/common': 20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) + '@angular/compiler': 20.1.0 + '@angular/core': 20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1) + '@angular/platform-browser': 20.1.0(@angular/animations@20.1.0(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1)) rxjs: 7.8.2 tslib: 2.8.1 xhr2: 0.2.1 - '@angular/router@20.1.0-next.3(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.1.0-next.3(@angular/animations@20.1.0-next.3(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)': + '@angular/router@20.1.0(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.1.0(@angular/animations@20.1.0(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)': dependencies: - '@angular/common': 20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) - '@angular/core': 20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1) - '@angular/platform-browser': 20.1.0-next.3(@angular/animations@20.1.0-next.3(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1)) + '@angular/common': 20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) + '@angular/core': 20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1) + '@angular/platform-browser': 20.1.0(@angular/animations@20.1.0(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1)) rxjs: 7.8.2 tslib: 2.8.1 - '@angular/service-worker@20.1.0-next.3(@angular/core@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2)': + '@angular/service-worker@20.1.0(@angular/core@20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2)': dependencies: - '@angular/core': 20.1.0-next.3(@angular/compiler@20.1.0-next.3)(rxjs@7.8.2)(zone.js@0.15.1) + '@angular/core': 20.1.0(@angular/compiler@20.1.0)(rxjs@7.8.2)(zone.js@0.15.1) rxjs: 7.8.2 tslib: 2.8.1 @@ -8496,18 +8522,18 @@ snapshots: '@babel/compat-data@7.27.5': {} - '@babel/core@7.27.4': + '@babel/core@7.27.7': dependencies: '@ampproject/remapping': 2.3.0 '@babel/code-frame': 7.27.1 '@babel/generator': 7.27.5 '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.4) + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.7) '@babel/helpers': 7.27.6 - '@babel/parser': 7.27.5 + '@babel/parser': 7.27.7 '@babel/template': 7.27.2 - '@babel/traverse': 7.27.4 - '@babel/types': 7.27.6 + '@babel/traverse': 7.27.7 + '@babel/types': 7.27.7 convert-source-map: 2.0.0 debug: 4.4.1(supports-color@10.0.0) gensync: 1.0.0-beta.2 @@ -8516,18 +8542,18 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/core@7.27.7': + '@babel/core@7.28.0': dependencies: '@ampproject/remapping': 2.3.0 '@babel/code-frame': 7.27.1 - '@babel/generator': 7.27.5 + '@babel/generator': 7.28.0 '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.7) + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) '@babel/helpers': 7.27.6 - '@babel/parser': 7.27.7 + '@babel/parser': 7.28.0 '@babel/template': 7.27.2 - '@babel/traverse': 7.27.7 - '@babel/types': 7.27.7 + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.0 convert-source-map: 2.0.0 debug: 4.4.1(supports-color@10.0.0) gensync: 1.0.0-beta.2 @@ -8544,6 +8570,14 @@ snapshots: '@jridgewell/trace-mapping': 0.3.25 jsesc: 3.1.0 + '@babel/generator@7.28.0': + dependencies: + '@babel/parser': 7.28.0 + '@babel/types': 7.28.0 + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + jsesc: 3.1.0 + '@babel/helper-annotate-as-pure@7.27.3': dependencies: '@babel/types': 7.27.6 @@ -8587,6 +8621,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-globals@7.28.0': {} + '@babel/helper-member-expression-to-functions@7.27.1': dependencies: '@babel/traverse': 7.27.4 @@ -8601,18 +8637,18 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.27.3(@babel/core@7.27.4)': + '@babel/helper-module-transforms@7.27.3(@babel/core@7.27.7)': dependencies: - '@babel/core': 7.27.4 + '@babel/core': 7.27.7 '@babel/helper-module-imports': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 '@babel/traverse': 7.27.7 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.27.3(@babel/core@7.27.7)': + '@babel/helper-module-transforms@7.27.3(@babel/core@7.28.0)': dependencies: - '@babel/core': 7.27.7 + '@babel/core': 7.28.0 '@babel/helper-module-imports': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 '@babel/traverse': 7.27.7 @@ -8681,6 +8717,10 @@ snapshots: dependencies: '@babel/types': 7.27.7 + '@babel/parser@7.28.0': + dependencies: + '@babel/types': 7.28.0 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -9181,6 +9221,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/traverse@7.28.0': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.0 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.0 + '@babel/template': 7.27.2 + '@babel/types': 7.28.0 + debug: 4.4.1(supports-color@10.0.0) + transitivePeerDependencies: + - supports-color + '@babel/types@7.27.6': dependencies: '@babel/helper-string-parser': 7.27.1 @@ -9191,6 +9243,11 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 + '@babel/types@7.28.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@bazel/bazelisk@1.26.0': {} '@bazel/buildifier@8.2.1': {} @@ -9608,6 +9665,11 @@ snapshots: '@istanbuljs/schema@0.1.3': {} + '@jridgewell/gen-mapping@0.3.12': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/gen-mapping@0.3.8': dependencies: '@jridgewell/set-array': 1.2.1 @@ -9630,6 +9692,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping@0.3.29': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping@0.3.9': dependencies: '@jridgewell/resolve-uri': 3.1.2 @@ -14398,10 +14465,10 @@ snapshots: netmask@2.0.2: {} - ng-packagr@20.1.0-next.0(@angular/compiler-cli@20.1.0-next.3(@angular/compiler@20.1.0-next.3)(typescript@5.8.3))(tslib@2.8.1)(typescript@5.8.3): + ng-packagr@20.1.0(@angular/compiler-cli@20.1.0(@angular/compiler@20.1.0)(typescript@5.8.3))(tslib@2.8.1)(typescript@5.8.3): dependencies: '@ampproject/remapping': 2.3.0 - '@angular/compiler-cli': 20.1.0-next.3(@angular/compiler@20.1.0-next.3)(typescript@5.8.3) + '@angular/compiler-cli': 20.1.0(@angular/compiler@20.1.0)(typescript@5.8.3) '@rollup/plugin-json': 6.1.0(rollup@4.44.1) '@rollup/wasm-node': 4.44.0 ajv: 8.17.1 From ed09cef09c5fd0f8eb59580d15cbe0ca9190eccf Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 9 Jul 2025 18:10:07 -0400 Subject: [PATCH 22/65] release: cut the v20.1.0 release --- CHANGELOG.md | 138 ++++++++++++--------------------------------------- package.json | 2 +- 2 files changed, 33 insertions(+), 107 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91445ecb29a4..190c379edf2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,42 +1,6 @@ - + -# 20.1.0-rc.0 (2025-07-01) - -### @angular-devkit/build-angular - -| Commit | Type | Description | -| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------- | -| [f1d41b069](https://github.com/angular/angular-cli/commit/f1d41b069db6cd3ab83561113567b8e5f4bf25d8) | fix | remove unused `@vitejs/plugin-basic-ssl` dependency | - -### @angular/build - -| Commit | Type | Description | -| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------- | -| [73f57f3c9](https://github.com/angular/angular-cli/commit/73f57f3c9e0d9434c0f8507508dfe30f6a402861) | fix | proxy karma request from `/` to `/base` | - - - - - -# 20.0.5 (2025-07-01) - -### @angular-devkit/build-angular - -| Commit | Type | Description | -| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------- | -| [1ebd53df7](https://github.com/angular/angular-cli/commit/1ebd53df7168307f699a9f9ae8f5ef5b9bcf352c) | fix | remove unused `@vitejs/plugin-basic-ssl` dependency | - -### @angular/build - -| Commit | Type | Description | -| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------- | -| [05cebdbcd](https://github.com/angular/angular-cli/commit/05cebdbcd1466bf5c95eb724a784aeb8c7ac083f) | fix | proxy karma request from `/` to `/base` | - - - - - -# 20.1.0-next.3 (2025-06-25) +# 20.1.0 (2025-07-09) ### @angular/cli @@ -48,7 +12,7 @@ | Commit | Type | Description | | --------------------------------------------------------------------------------------------------- | ---- | --------------------------- | -| [4221a33cc](https://github.com/angular/angular-cli/commit/4221a33cc7dee8a46464345f09005795f217ad02) | fix | add missing prettier config | +| [1c19e0dcd](https://github.com/angular/angular-cli/commit/1c19e0dcd4a87fbf542201e09a402a8fccdfcd88) | feat | use signal in app component | ### @angular-devkit/build-angular @@ -60,16 +24,43 @@ | Commit | Type | Description | | --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------- | +| [1159cf081](https://github.com/angular/angular-cli/commit/1159cf08103081d2b851e59bc1c5fb200f114982) | feat | add code coverage reporters option for unit-test | +| [8f305ef0b](https://github.com/angular/angular-cli/commit/8f305ef0ba91ec9bf6417b7084965205cf5488e7) | feat | add dataurl, base64 loaders | | [adfeee0a4](https://github.com/angular/angular-cli/commit/adfeee0a4c95a03d430054eeecd4cca1bdb0efeb) | fix | adjust coverage includes/excludes for unit-test vitest runner | +| [c19cd2985](https://github.com/angular/angular-cli/commit/c19cd2985cbf1ea8c1c15f020bc530d6768cb0fa) | fix | coverage reporter option | +| [8879716ca](https://github.com/angular/angular-cli/commit/8879716cac9b2134db2795b1810595ea56e9d421) | fix | expose unit test and karma builder API | +| [a415a4999](https://github.com/angular/angular-cli/commit/a415a4999f337f5bc3c0ee626aaba58b6c5ad4e1) | fix | improve default coverage reporter handling for vitest | | [e0de8680d](https://github.com/angular/angular-cli/commit/e0de8680d1ea25aa71024d7b89beaa1e75889c47) | fix | inject zone.js/testing before karma builder execution | +| [2672f6ec1](https://github.com/angular/angular-cli/commit/2672f6ec17de6e05b19acda0e0b09a6715c9f83f) | fix | json and json-summary as vitest coverage reporters | +| [b67fdfd6b](https://github.com/angular/angular-cli/commit/b67fdfd6bc422bd6a46db923470579c760c5ec27) | fix | resolve "Controller is already closed" error in Karma | +| [2784883ec](https://github.com/angular/angular-cli/commit/2784883ecfb63e4aa6a6c69fd10e457316b4958c) | fix | support extra test setup files with unit-test vitest runner | +| [f177f5508](https://github.com/angular/angular-cli/commit/f177f5508adb23f604d9abb5f4a33f3af5f32561) | fix | support injecting global styles into vitest unit-tests | +| [130c65014](https://github.com/angular/angular-cli/commit/130c650146595f237bc3285302d0075ba0387546) | fix | use an empty array as default value for vitest exclude | +| [917af12ae](https://github.com/angular/angular-cli/commit/917af12aeb82b1437e7b43a03ae80b58a09f0224) | fix | use date/time based output path for vitest unit-test | ### @angular/ssr | Commit | Type | Description | | --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------- | -| [861a61a3b](https://github.com/angular/angular-cli/commit/861a61a3b26a3e88105641084415f45a07cb56b5) | fix | avoid preloading unnecessary dynamic bundles | | [21b5852f1](https://github.com/angular/angular-cli/commit/21b5852f120dd42ea4ae9fce043e04ec61da16dd) | fix | ensure `loadChildren` runs in correct injection context during route extraction | -| [1c5bd2ef2](https://github.com/angular/angular-cli/commit/1c5bd2ef2fa95a789e14ab8c497b48e125ceb4f8) | fix | ensure correct referer header handling in web request conversion | + + + + + +# 20.0.5 (2025-07-01) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------- | +| [1ebd53df7](https://github.com/angular/angular-cli/commit/1ebd53df7168307f699a9f9ae8f5ef5b9bcf352c) | fix | remove unused `@vitejs/plugin-basic-ssl` dependency | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------- | +| [05cebdbcd](https://github.com/angular/angular-cli/commit/05cebdbcd1466bf5c95eb724a784aeb8c7ac083f) | fix | proxy karma request from `/` to `/base` | @@ -92,30 +83,6 @@ - - -# 20.1.0-next.2 (2025-06-18) - -### @schematics/angular - -| Commit | Type | Description | -| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------- | -| [c43711177](https://github.com/angular/angular-cli/commit/c43711177b13b15ae4fbc7a009ae137bdc3fea4d) | fix | include `main.server.ts` in `tsconfig.files` when present | -| [4be58ee8c](https://github.com/angular/angular-cli/commit/4be58ee8c9896107925507a60cc8dd830c93bb7e) | fix | reset module `typeSeparator` when generating applications | - -### @angular/build - -| Commit | Type | Description | -| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------ | -| [c19cd2985](https://github.com/angular/angular-cli/commit/c19cd2985cbf1ea8c1c15f020bc530d6768cb0fa) | fix | coverage reporter option | -| [049e6886f](https://github.com/angular/angular-cli/commit/049e6886f88267158d85ca72020fec728c3de0ac) | fix | include custom bundle name scripts with karma | -| [1d76d0ee5](https://github.com/angular/angular-cli/commit/1d76d0ee59d54a889b564bdf85f183fd08ddc860) | fix | increase worker idle timeout | -| [2672f6ec1](https://github.com/angular/angular-cli/commit/2672f6ec17de6e05b19acda0e0b09a6715c9f83f) | fix | json and json-summary as vitest coverage reporters | -| [60a16a82a](https://github.com/angular/angular-cli/commit/60a16a82a99718a527e2c6b588d1489fba5bd500) | fix | set scripts option output as classic script for karma | -| [130c65014](https://github.com/angular/angular-cli/commit/130c650146595f237bc3285302d0075ba0387546) | fix | use an empty array as default value for vitest exclude | - - - # 20.0.3 (2025-06-18) @@ -137,28 +104,6 @@ - - -# 20.1.0-next.1 (2025-06-11) - -### @schematics/angular - -| Commit | Type | Description | -| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------------- | -| [1c19e0dcd](https://github.com/angular/angular-cli/commit/1c19e0dcd4a87fbf542201e09a402a8fccdfcd88) | feat | use signal in app component | -| [42f45a39e](https://github.com/angular/angular-cli/commit/42f45a39e63ab3ee1ba8d1b9af8d2e397ca07159) | fix | add `less` as a devDependency when selected as the style preprocessor | - -### @angular/build - -| Commit | Type | Description | -| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------- | -| [e36cbba11](https://github.com/angular/angular-cli/commit/e36cbba11ecc0d95a0e7ff0e8184212ca824e87a) | fix | do not consider internal Angular files as external imports | -| [a415a4999](https://github.com/angular/angular-cli/commit/a415a4999f337f5bc3c0ee626aaba58b6c5ad4e1) | fix | improve default coverage reporter handling for vitest | -| [f177f5508](https://github.com/angular/angular-cli/commit/f177f5508adb23f604d9abb5f4a33f3af5f32561) | fix | support injecting global styles into vitest unit-tests | -| [917af12ae](https://github.com/angular/angular-cli/commit/917af12aeb82b1437e7b43a03ae80b58a09f0224) | fix | use date/time based output path for vitest unit-test | - - - # 20.0.2 (2025-06-11) @@ -202,25 +147,6 @@ - - -# 20.1.0-next.0 (2025-06-05) - -### @schematics/angular - -| Commit | Type | Description | -| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------- | -| [0b7d48c7c](https://github.com/angular/angular-cli/commit/0b7d48c7cafb49aa3cac7d9da831eff039b3e047) | fix | correctly detect modules using new file extension format | - -### @angular/build - -| Commit | Type | Description | -| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------ | -| [1159cf081](https://github.com/angular/angular-cli/commit/1159cf08103081d2b851e59bc1c5fb200f114982) | feat | add code coverage reporters option for unit-test | -| [8f305ef0b](https://github.com/angular/angular-cli/commit/8f305ef0ba91ec9bf6417b7084965205cf5488e7) | feat | add dataurl, base64 loaders | - - - # 20.0.1 (2025-06-04) diff --git a/package.json b/package.json index 3b3427420e1e..7684d6ac7981 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@angular/devkit-repo", - "version": "20.1.0-rc.0", + "version": "20.1.0", "private": true, "description": "Software Development Kit for Angular", "keywords": [ From e46cedb3bc157f5bb7d88e0c3ae00838e8b4fbf9 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Wed, 9 Jul 2025 09:36:20 +0000 Subject: [PATCH 23/65] refactor: use `executeWithCases` instead of RXJS boillerplate Clean up several tests (cherry picked from commit 3c3887703c9b4981e66ff13310d6d9faa46dfe58) --- .../testing/builder/src/jasmine-helpers.ts | 32 +- .../tests/behavior/rebuild-assets_spec.ts | 78 +-- .../behavior/rebuild-component_styles_spec.ts | 56 +-- .../tests/behavior/rebuild-errors_spec.ts | 473 ++++++++---------- .../tests/behavior/rebuild-general_spec.ts | 111 ++-- .../behavior/rebuild-global_styles_spec.ts | 189 +++---- .../tests/behavior/rebuild-index-html_spec.ts | 60 +-- .../behavior/rebuild-web-workers_spec.ts | 145 +++--- .../behavior/typescript-rebuild-lazy_spec.ts | 71 ++- .../typescript-rebuild-touch-file_spec.ts | 44 +- .../options/inline-style-language_spec.ts | 83 ++- .../tests/behavior/build-errors_spec.ts | 63 +-- .../build_localize_replaced_watch_spec.ts | 40 +- .../behavior/build_translation_watch_spec.ts | 51 +- .../serve-live-reload-proxies_spec.ts | 151 ++---- .../behavior/serve_service-worker_spec.ts | 81 ++- .../karma/tests/behavior/rebuilds_spec.ts | 94 +--- .../src/builders/browser/specs/styles_spec.ts | 21 +- .../tests/behavior/index_watch_spec.ts | 45 +- .../tests/behavior/localize_watch_spec.ts | 41 +- .../tests/behavior/rebuild-errors_spec.ts | 333 ++++++------ .../options/inline-style-language_spec.ts | 76 ++- .../browser/tests/options/verbose_spec.ts | 48 +- .../browser/tests/options/watch_spec.ts | 42 +- .../build_localize_replaced_watch_spec.ts | 40 +- .../behavior/build_translation_watch_spec.ts | 49 +- .../serve-live-reload-proxies_spec.ts | 195 ++++---- .../behavior/serve_service-worker_spec.ts | 79 ++- .../dev-server/tests/options/watch_spec.ts | 108 ++-- .../src/builders/dev-server/tests/setup.ts | 6 - .../karma/tests/behavior/rebuilds_spec.ts | 67 +-- 31 files changed, 1212 insertions(+), 1760 deletions(-) diff --git a/modules/testing/builder/src/jasmine-helpers.ts b/modules/testing/builder/src/jasmine-helpers.ts index 15045a2f56d5..94fdbeb38fe1 100644 --- a/modules/testing/builder/src/jasmine-helpers.ts +++ b/modules/testing/builder/src/jasmine-helpers.ts @@ -9,15 +9,19 @@ import { BuilderHandlerFn } from '@angular-devkit/architect'; import { json } from '@angular-devkit/core'; import { readFileSync } from 'node:fs'; -import { concatMap, count, firstValueFrom, take, timeout } from 'rxjs'; -import { BuilderHarness, BuilderHarnessExecutionResult } from './builder-harness'; +import { concatMap, count, debounceTime, firstValueFrom, take, timeout } from 'rxjs'; +import { + BuilderHarness, + BuilderHarnessExecutionOptions, + BuilderHarnessExecutionResult, +} from './builder-harness'; import { host } from './test-utils'; /** * Maximum time for single build/rebuild * This accounts for CI variability. */ -export const BUILD_TIMEOUT = 25_000; +export const BUILD_TIMEOUT = 30_000; const optionSchemaCache = new Map(); @@ -62,10 +66,12 @@ export class JasmineBuilderHarness extends BuilderHarness { executionResult: BuilderHarnessExecutionResult, index: number, ) => void | Promise)[], + options?: Partial & { timeout?: number }, ): Promise { const executionCount = await firstValueFrom( - this.execute().pipe( - timeout(BUILD_TIMEOUT), + this.execute(options).pipe( + timeout(options?.timeout ?? BUILD_TIMEOUT), + debounceTime(100), // This is needed as sometimes 2 events for the same change fire with webpack. concatMap(async (result, index) => await cases[index](result, index)), take(cases.length), count(), @@ -118,13 +124,17 @@ export function expectFile(path: string, harness: BuilderHarness): Harness return { toExist() { const exists = harness.hasFile(path); - expect(exists).toBe(true, 'Expected file to exist: ' + path); + expect(exists) + .withContext('Expected file to exist: ' + path) + .toBeTrue(); return exists; }, toNotExist() { const exists = harness.hasFile(path); - expect(exists).toBe(false, 'Expected file to not exist: ' + path); + expect(exists) + .withContext('Expected file to exist: ' + path) + .toBeFalse(); return !exists; }, @@ -170,13 +180,17 @@ export function expectDirectory( return { toExist() { const exists = harness.hasDirectory(path); - expect(exists).toBe(true, 'Expected directory to exist: ' + path); + expect(exists) + .withContext('Expected directory to exist: ' + path) + .toBeTrue(); return exists; }, toNotExist() { const exists = harness.hasDirectory(path); - expect(exists).toBe(false, 'Expected directory to not exist: ' + path); + expect(exists) + .withContext('Expected directory to not exist: ' + path) + .toBeFalse(); return !exists; }, diff --git a/packages/angular/build/src/builders/application/tests/behavior/rebuild-assets_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/rebuild-assets_spec.ts index a48c19fd1baf..7bfcca94d242 100644 --- a/packages/angular/build/src/builders/application/tests/behavior/rebuild-assets_spec.ts +++ b/packages/angular/build/src/builders/application/tests/behavior/rebuild-assets_spec.ts @@ -6,16 +6,9 @@ * found in the LICENSE file at https://angular.dev/license */ -import { concatMap, count, take, timeout } from 'rxjs'; import { buildApplication } from '../../index'; import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; -/** - * Maximum time in milliseconds for single build/rebuild - * This accounts for CI variability. - */ -const BUILD_TIMEOUT = 10_000; - describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { describe('Behavior: "Rebuilds when input asset changes"', () => { beforeEach(async () => { @@ -36,30 +29,18 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { watch: true, }); - const buildCount = await harness - .execute({ outputLogsOnFailure: false }) - .pipe( - timeout(BUILD_TIMEOUT), - concatMap(async ({ result }, index) => { - switch (index) { - case 0: - expect(result?.success).toBeTrue(); - harness.expectFile('dist/browser/asset.txt').content.toContain('foo'); + await harness.executeWithCases([ + async ({ result }) => { + expect(result?.success).toBeTrue(); + harness.expectFile('dist/browser/asset.txt').content.toContain('foo'); - await harness.writeFile('public/asset.txt', 'bar'); - break; - case 1: - expect(result?.success).toBeTrue(); - harness.expectFile('dist/browser/asset.txt').content.toContain('bar'); - break; - } - }), - take(2), - count(), - ) - .toPromise(); - - expect(buildCount).toBe(2); + await harness.writeFile('public/asset.txt', 'bar'); + }, + ({ result }) => { + expect(result?.success).toBeTrue(); + harness.expectFile('dist/browser/asset.txt').content.toContain('bar'); + }, + ]); }); it('remove deleted asset from output', async () => { @@ -79,32 +60,21 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { watch: true, }); - const buildCount = await harness - .execute({ outputLogsOnFailure: false }) - .pipe( - timeout(BUILD_TIMEOUT), - concatMap(async ({ result }, index) => { - switch (index) { - case 0: - expect(result?.success).toBeTrue(); - harness.expectFile('dist/browser/asset-one.txt').toExist(); - harness.expectFile('dist/browser/asset-two.txt').toExist(); + await harness.executeWithCases([ + async ({ result }) => { + expect(result?.success).toBeTrue(); + harness.expectFile('dist/browser/asset-one.txt').toExist(); + harness.expectFile('dist/browser/asset-two.txt').toExist(); - await harness.removeFile('public/asset-two.txt'); - break; - case 1: - expect(result?.success).toBeTrue(); - harness.expectFile('dist/browser/asset-one.txt').toExist(); - harness.expectFile('dist/browser/asset-two.txt').toNotExist(); - break; - } - }), - take(2), - count(), - ) - .toPromise(); + await harness.removeFile('public/asset-two.txt'); + }, - expect(buildCount).toBe(2); + ({ result }) => { + expect(result?.success).toBeTrue(); + harness.expectFile('dist/browser/asset-one.txt').toExist(); + harness.expectFile('dist/browser/asset-two.txt').toNotExist(); + }, + ]); }); }); }); diff --git a/packages/angular/build/src/builders/application/tests/behavior/rebuild-component_styles_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/rebuild-component_styles_spec.ts index a252a0580d0b..26ae35a8221f 100644 --- a/packages/angular/build/src/builders/application/tests/behavior/rebuild-component_styles_spec.ts +++ b/packages/angular/build/src/builders/application/tests/behavior/rebuild-component_styles_spec.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.dev/license */ -import { concatMap, count, take, timeout } from 'rxjs'; import { buildApplication } from '../../index'; import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; @@ -32,46 +31,31 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { await harness.writeFile('src/app/app.component.scss', "@import './a';"); await harness.writeFile('src/app/a.scss', '$primary: aqua;\\nh1 { color: $primary; }'); - const buildCount = await harness - .execute() - .pipe( - timeout(30000), - concatMap(async ({ result }, index) => { - expect(result?.success).toBe(true); + await harness.executeWithCases([ + async ({ result }) => { + expect(result?.success).toBe(true); - switch (index) { - case 0: - harness.expectFile('dist/browser/main.js').content.toContain('color: aqua'); - harness.expectFile('dist/browser/main.js').content.not.toContain('color: blue'); + harness.expectFile('dist/browser/main.js').content.toContain('color: aqua'); + harness.expectFile('dist/browser/main.js').content.not.toContain('color: blue'); - await harness.writeFile( - 'src/app/a.scss', - '$primary: blue;\\nh1 { color: $primary; }', - ); - break; - case 1: - harness.expectFile('dist/browser/main.js').content.not.toContain('color: aqua'); - harness.expectFile('dist/browser/main.js').content.toContain('color: blue'); + await harness.writeFile('src/app/a.scss', '$primary: blue;\\nh1 { color: $primary; }'); + }, + async ({ result }) => { + expect(result?.success).toBe(true); - await harness.writeFile( - 'src/app/a.scss', - '$primary: green;\\nh1 { color: $primary; }', - ); - break; - case 2: - harness.expectFile('dist/browser/main.js').content.not.toContain('color: aqua'); - harness.expectFile('dist/browser/main.js').content.not.toContain('color: blue'); - harness.expectFile('dist/browser/main.js').content.toContain('color: green'); + harness.expectFile('dist/browser/main.js').content.not.toContain('color: aqua'); + harness.expectFile('dist/browser/main.js').content.toContain('color: blue'); - break; - } - }), - take(3), - count(), - ) - .toPromise(); + await harness.writeFile('src/app/a.scss', '$primary: green;\\nh1 { color: $primary; }'); + }, + ({ result }) => { + expect(result?.success).toBe(true); - expect(buildCount).toBe(3); + harness.expectFile('dist/browser/main.js').content.not.toContain('color: aqua'); + harness.expectFile('dist/browser/main.js').content.not.toContain('color: blue'); + harness.expectFile('dist/browser/main.js').content.toContain('color: green'); + }, + ]); }); } }); diff --git a/packages/angular/build/src/builders/application/tests/behavior/rebuild-errors_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/rebuild-errors_spec.ts index 196cbf4e6b5d..0dde3b4be58f 100644 --- a/packages/angular/build/src/builders/application/tests/behavior/rebuild-errors_spec.ts +++ b/packages/angular/build/src/builders/application/tests/behavior/rebuild-errors_spec.ts @@ -7,7 +7,6 @@ */ import { logging } from '@angular-devkit/core'; -import { concatMap, count, take, timeout } from 'rxjs'; import { buildApplication } from '../../index'; import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; @@ -73,85 +72,71 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { `, ); - const buildCount = await harness - .execute({ outputLogsOnFailure: false }) - .pipe( - timeout(BUILD_TIMEOUT), - concatMap(async ({ result, logs }, index) => { - switch (index) { - case 0: - expect(result?.success).toBeTrue(); + await harness.executeWithCases( + [ + async ({ result }) => { + expect(result?.success).toBeTrue(); - // Update directive to use a different input type for 'foo' (number -> string) - // Should cause a template error - await harness.writeFile( - 'src/app/dir.ts', - ` + // Update directive to use a different input type for 'foo' (number -> string) + // Should cause a template error + await harness.writeFile( + 'src/app/dir.ts', + ` import { Directive, Input } from '@angular/core'; @Directive({ selector: 'dir', standalone: false }) export class Dir { @Input() foo: string; } `, - ); - - break; - case 1: - expect(result?.success).toBeFalse(); - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching(typeErrorText), - }), - ); - - // Make an unrelated change to verify error cache was updated - // Should persist error in the next rebuild - await harness.modifyFile('src/main.ts', (content) => content + '\n'); - - break; - case 2: - expect(result?.success).toBeFalse(); - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching(typeErrorText), - }), - ); - - // Revert the directive change that caused the error - // Should remove the error - await harness.writeFile('src/app/dir.ts', goodDirectiveContents); - - break; - case 3: - expect(result?.success).toBeTrue(); - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching(typeErrorText), - }), - ); - - // Make an unrelated change to verify error cache was updated - // Should continue showing no error - await harness.modifyFile('src/main.ts', (content) => content + '\n'); - - break; - case 4: - expect(result?.success).toBeTrue(); - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching(typeErrorText), - }), - ); - - break; - } - }), - take(5), - count(), - ) - .toPromise(); - - expect(buildCount).toBe(5); + ); + }, + async ({ result, logs }) => { + expect(result?.success).toBeFalse(); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(typeErrorText), + }), + ); + + // Make an unrelated change to verify error cache was updated + // Should persist error in the next rebuild + await harness.modifyFile('src/main.ts', (content) => content + '\n'); + }, + async ({ result, logs }) => { + expect(result?.success).toBeFalse(); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(typeErrorText), + }), + ); + + // Revert the directive change that caused the error + // Should remove the error + await harness.writeFile('src/app/dir.ts', goodDirectiveContents); + }, + async ({ result, logs }) => { + expect(result?.success).toBeTrue(); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(typeErrorText), + }), + ); + + // Make an unrelated change to verify error cache was updated + // Should continue showing no error + await harness.modifyFile('src/main.ts', (content) => content + '\n'); + }, + ({ result, logs }) => { + expect(result?.success).toBeTrue(); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(typeErrorText), + }), + ); + }, + ], + { outputLogsOnFailure: false }, + ); }); it('detects cumulative block syntax errors', async () => { @@ -160,104 +145,89 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { watch: true, }); - const buildCount = await harness - .execute({ outputLogsOnFailure: false }) - .pipe( - timeout(BUILD_TIMEOUT), - concatMap(async ({ logs }, index) => { - switch (index) { - case 0: - // Add invalid block syntax - await harness.appendToFile('src/app/app.component.html', '@one'); - - break; - case 1: - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringContaining('@one'), - }), - ); - - // Make an unrelated change to verify error cache was updated - // Should persist error in the next rebuild - await harness.modifyFile('src/main.ts', (content) => content + '\n'); - - break; - case 2: - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringContaining('@one'), - }), - ); - - // Add more invalid block syntax - await harness.appendToFile('src/app/app.component.html', '@two'); - - break; - case 3: - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringContaining('@one'), - }), - ); - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringContaining('@two'), - }), - ); - - // Add more invalid block syntax - await harness.appendToFile('src/app/app.component.html', '@three'); - - break; - case 4: - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringContaining('@one'), - }), - ); - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringContaining('@two'), - }), - ); - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringContaining('@three'), - }), - ); - - // Revert the changes that caused the error - // Should remove the error - await harness.writeFile('src/app/app.component.html', '

GOOD

'); - - break; - case 5: - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringContaining('@one'), - }), - ); - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringContaining('@two'), - }), - ); - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringContaining('@three'), - }), - ); - - break; - } - }), - take(6), - count(), - ) - .toPromise(); - - expect(buildCount).toBe(6); + await harness.executeWithCases( + [ + async () => { + // Add invalid block syntax + await harness.appendToFile('src/app/app.component.html', '@one'); + }, + async ({ logs }) => { + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringContaining('@one'), + }), + ); + + // Make an unrelated change to verify error cache was updated + // Should persist error in the next rebuild + await harness.modifyFile('src/main.ts', (content) => content + '\n'); + }, + async ({ logs }) => { + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringContaining('@one'), + }), + ); + + // Add more invalid block syntax + await harness.appendToFile('src/app/app.component.html', '@two'); + }, + async ({ logs }) => { + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringContaining('@one'), + }), + ); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringContaining('@two'), + }), + ); + + // Add more invalid block syntax + await harness.appendToFile('src/app/app.component.html', '@three'); + }, + async ({ logs }) => { + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringContaining('@one'), + }), + ); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringContaining('@two'), + }), + ); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringContaining('@three'), + }), + ); + + // Revert the changes that caused the error + // Should remove the error + await harness.writeFile('src/app/app.component.html', '

GOOD

'); + }, + ({ logs }) => { + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringContaining('@one'), + }), + ); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringContaining('@two'), + }), + ); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringContaining('@three'), + }), + ); + }, + ], + { outputLogsOnFailure: false }, + ); }); it('recovers from component stylesheet error', async () => { @@ -267,46 +237,34 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { aot: false, }); - const buildCount = await harness - .execute({ outputLogsOnFailure: false }) - .pipe( - timeout(BUILD_TIMEOUT), - concatMap(async ({ result, logs }, index) => { - switch (index) { - case 0: - await harness.writeFile('src/app/app.component.css', 'invalid-css-content'); - - break; - case 1: - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching('invalid-css-content'), - }), - ); - - await harness.writeFile('src/app/app.component.css', 'p { color: green }'); - - break; - case 2: - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching('invalid-css-content'), - }), - ); - - harness - .expectFile('dist/browser/main.js') - .content.toContain('p {\\n color: green;\\n}'); - - break; - } - }), - take(3), - count(), - ) - .toPromise(); - - expect(buildCount).toBe(3); + await harness.executeWithCases( + [ + async () => { + await harness.writeFile('src/app/app.component.css', 'invalid-css-content'); + }, + async ({ logs }) => { + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('invalid-css-content'), + }), + ); + + await harness.writeFile('src/app/app.component.css', 'p { color: green }'); + }, + ({ logs }) => { + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('invalid-css-content'), + }), + ); + + harness + .expectFile('dist/browser/main.js') + .content.toContain('p {\\n color: green;\\n}'); + }, + ], + { outputLogsOnFailure: false }, + ); }); it('recovers from component template error', async () => { @@ -315,59 +273,46 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { watch: true, }); - const buildCount = await harness - .execute({ outputLogsOnFailure: false }) - .pipe( - timeout(BUILD_TIMEOUT), - concatMap(async ({ result, logs }, index) => { - switch (index) { - case 0: - // Missing ending `>` on the div will cause an error - await harness.appendToFile('src/app/app.component.html', '
Hello, world!({ - message: jasmine.stringMatching('Unexpected character "EOF"'), - }), - ); - - await harness.appendToFile('src/app/app.component.html', '>'); - - break; - case 2: - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching('Unexpected character "EOF"'), - }), - ); - - harness.expectFile('dist/browser/main.js').content.toContain('Hello, world!'); - - // Make an additional valid change to ensure that rebuilds still trigger - await harness.appendToFile('src/app/app.component.html', '
Guten Tag
'); - - break; - case 3: - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching('invalid-css-content'), - }), - ); - - harness.expectFile('dist/browser/main.js').content.toContain('Hello, world!'); - harness.expectFile('dist/browser/main.js').content.toContain('Guten Tag'); - - break; - } - }), - take(4), - count(), - ) - .toPromise(); - - expect(buildCount).toBe(4); + await harness.executeWithCases( + [ + async () => { + // Missing ending `>` on the div will cause an error + await harness.appendToFile('src/app/app.component.html', '
Hello, world! { + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('Unexpected character "EOF"'), + }), + ); + + await harness.appendToFile('src/app/app.component.html', '>'); + }, + async ({ logs }) => { + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('Unexpected character "EOF"'), + }), + ); + + harness.expectFile('dist/browser/main.js').content.toContain('Hello, world!'); + + // Make an additional valid change to ensure that rebuilds still trigger + await harness.appendToFile('src/app/app.component.html', '
Guten Tag
'); + }, + ({ logs }) => { + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('invalid-css-content'), + }), + ); + + harness.expectFile('dist/browser/main.js').content.toContain('Hello, world!'); + harness.expectFile('dist/browser/main.js').content.toContain('Guten Tag'); + }, + ], + { outputLogsOnFailure: false }, + ); }); }); }); diff --git a/packages/angular/build/src/builders/application/tests/behavior/rebuild-general_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/rebuild-general_spec.ts index ca88f94e5b63..d9ea8870f687 100644 --- a/packages/angular/build/src/builders/application/tests/behavior/rebuild-general_spec.ts +++ b/packages/angular/build/src/builders/application/tests/behavior/rebuild-general_spec.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.dev/license */ -import { concatMap, count, take, timeout } from 'rxjs'; import { buildApplication } from '../../index'; import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; @@ -45,68 +44,54 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { `, ); - const buildCount = await harness - .execute({ outputLogsOnFailure: false }) - .pipe( - timeout(BUILD_TIMEOUT), - concatMap(async ({ result, logs }, index) => { - switch (index) { - case 0: - expect(result?.success).toBeTrue(); - harness.expectFile('dist/browser/main.js').content.toContain('FILE-A'); - - // Delete the imported file - await harness.removeFile('src/app/file-a.ts'); - - break; - case 1: - // Should fail from missing import - expect(result?.success).toBeFalse(); - - // Remove the failing import - await harness.modifyFile('src/app/app.component.ts', (content) => - content.replace(`import './file-a';`, ''), - ); - - break; - case 2: - expect(result?.success).toBeTrue(); - - harness.expectFile('dist/browser/main.js').content.not.toContain('FILE-A'); - - // Recreate the file and the import - await harness.writeFile('src/app/file-a.ts', fileAContent); - await harness.modifyFile( - 'src/app/app.component.ts', - (content) => `import './file-a';\n` + content, - ); - - break; - case 3: - expect(result?.success).toBeTrue(); - - harness.expectFile('dist/browser/main.js').content.toContain('FILE-A'); - - // Change the imported file - await harness.modifyFile('src/app/file-a.ts', (content) => - content.replace('FILE-A', 'FILE-B'), - ); - - break; - case 4: - expect(result?.success).toBeTrue(); - - harness.expectFile('dist/browser/main.js').content.toContain('FILE-B'); - - break; - } - }), - take(5), - count(), - ) - .toPromise(); - - expect(buildCount).toBe(5); + await harness.executeWithCases( + [ + async ({ result }) => { + expect(result?.success).toBeTrue(); + harness.expectFile('dist/browser/main.js').content.toContain('FILE-A'); + + // Delete the imported file + await harness.removeFile('src/app/file-a.ts'); + }, + async ({ result }) => { + // Should fail from missing import + expect(result?.success).toBeFalse(); + + // Remove the failing import + await harness.modifyFile('src/app/app.component.ts', (content) => + content.replace(`import './file-a';`, ''), + ); + }, + async ({ result }) => { + expect(result?.success).toBeTrue(); + + harness.expectFile('dist/browser/main.js').content.not.toContain('FILE-A'); + + // Recreate the file and the import + await harness.writeFile('src/app/file-a.ts', fileAContent); + await harness.modifyFile( + 'src/app/app.component.ts', + (content) => `import './file-a';\n` + content, + ); + }, + async ({ result }) => { + expect(result?.success).toBeTrue(); + + harness.expectFile('dist/browser/main.js').content.toContain('FILE-A'); + + // Change the imported file + await harness.modifyFile('src/app/file-a.ts', (content) => + content.replace('FILE-A', 'FILE-B'), + ); + }, + ({ result }) => { + expect(result?.success).toBeTrue(); + + harness.expectFile('dist/browser/main.js').content.toContain('FILE-B'); + }, + ], + { outputLogsOnFailure: false }, + ); }); }); }); diff --git a/packages/angular/build/src/builders/application/tests/behavior/rebuild-global_styles_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/rebuild-global_styles_spec.ts index e58b2e031a90..22c4c32202bd 100644 --- a/packages/angular/build/src/builders/application/tests/behavior/rebuild-global_styles_spec.ts +++ b/packages/angular/build/src/builders/application/tests/behavior/rebuild-global_styles_spec.ts @@ -6,16 +6,9 @@ * found in the LICENSE file at https://angular.dev/license */ -import { concatMap, count, take, timeout } from 'rxjs'; import { buildApplication } from '../../index'; import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; -/** - * Maximum time in milliseconds for single build/rebuild - * This accounts for CI variability. - */ -export const BUILD_TIMEOUT = 30_000; - describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { describe('Behavior: "Rebuilds when global stylesheets change"', () => { beforeEach(async () => { @@ -33,41 +26,31 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { await harness.writeFile('src/styles.scss', "@import './a';"); await harness.writeFile('src/a.scss', '$primary: aqua;\\nh1 { color: $primary; }'); - const buildCount = await harness - .execute({ outputLogsOnFailure: false }) - .pipe( - timeout(30000), - concatMap(async ({ result }, index) => { - switch (index) { - case 0: - expect(result?.success).toBe(true); - harness.expectFile('dist/browser/styles.css').content.toContain('color: aqua'); - harness.expectFile('dist/browser/styles.css').content.not.toContain('color: blue'); - - await harness.writeFile( - 'src/a.scss', - 'invalid-invalid-invalid\\nh1 { color: $primary; }', - ); - break; - case 1: - expect(result?.success).toBe(false); - - await harness.writeFile('src/a.scss', '$primary: blue;\\nh1 { color: $primary; }'); - break; - case 2: - expect(result?.success).toBe(true); - harness.expectFile('dist/browser/styles.css').content.not.toContain('color: aqua'); - harness.expectFile('dist/browser/styles.css').content.toContain('color: blue'); - - break; - } - }), - take(3), - count(), - ) - .toPromise(); - - expect(buildCount).toBe(3); + await harness.executeWithCases( + [ + async ({ result }) => { + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/styles.css').content.toContain('color: aqua'); + harness.expectFile('dist/browser/styles.css').content.not.toContain('color: blue'); + + await harness.writeFile( + 'src/a.scss', + 'invalid-invalid-invalid\\nh1 { color: $primary; }', + ); + }, + async ({ result }) => { + expect(result?.success).toBe(false); + + await harness.writeFile('src/a.scss', '$primary: blue;\\nh1 { color: $primary; }'); + }, + ({ result }) => { + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/styles.css').content.not.toContain('color: aqua'); + harness.expectFile('dist/browser/styles.css').content.toContain('color: blue'); + }, + ], + { outputLogsOnFailure: false }, + ); }); it('rebuilds Sass stylesheet after error on initial build from import', async () => { @@ -80,37 +63,28 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { await harness.writeFile('src/styles.scss', "@import './a';"); await harness.writeFile('src/a.scss', 'invalid-invalid-invalid\\nh1 { color: $primary; }'); - const buildCount = await harness - .execute({ outputLogsOnFailure: false }) - .pipe( - timeout(30000), - concatMap(async ({ result }, index) => { - switch (index) { - case 0: - expect(result?.success).toBe(false); - - await harness.writeFile('src/a.scss', '$primary: aqua;\\nh1 { color: $primary; }'); - break; - case 1: - expect(result?.success).toBe(true); - harness.expectFile('dist/browser/styles.css').content.toContain('color: aqua'); - harness.expectFile('dist/browser/styles.css').content.not.toContain('color: blue'); - - await harness.writeFile('src/a.scss', '$primary: blue;\\nh1 { color: $primary; }'); - break; - case 2: - expect(result?.success).toBe(true); - harness.expectFile('dist/browser/styles.css').content.not.toContain('color: aqua'); - harness.expectFile('dist/browser/styles.css').content.toContain('color: blue'); - break; - } - }), - take(3), - count(), - ) - .toPromise(); - - expect(buildCount).toBe(3); + await harness.executeWithCases( + [ + async ({ result }) => { + expect(result?.success).toBe(false); + + await harness.writeFile('src/a.scss', '$primary: aqua;\\nh1 { color: $primary; }'); + }, + async ({ result }) => { + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/styles.css').content.toContain('color: aqua'); + harness.expectFile('dist/browser/styles.css').content.not.toContain('color: blue'); + + await harness.writeFile('src/a.scss', '$primary: blue;\\nh1 { color: $primary; }'); + }, + ({ result }) => { + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/styles.css').content.not.toContain('color: aqua'); + harness.expectFile('dist/browser/styles.css').content.toContain('color: blue'); + }, + ], + { outputLogsOnFailure: false }, + ); }); it('rebuilds dependent Sass stylesheets after error on initial build from import', async () => { @@ -127,45 +101,36 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { await harness.writeFile('src/other.scss', "@import './a'; h1 { color: green; }"); await harness.writeFile('src/a.scss', 'invalid-invalid-invalid\\nh1 { color: $primary; }'); - const buildCount = await harness - .execute({ outputLogsOnFailure: false }) - .pipe( - timeout(30000), - concatMap(async ({ result }, index) => { - switch (index) { - case 0: - expect(result?.success).toBe(false); - - await harness.writeFile('src/a.scss', '$primary: aqua;\\nh1 { color: $primary; }'); - break; - case 1: - expect(result?.success).toBe(true); - harness.expectFile('dist/browser/styles.css').content.toContain('color: aqua'); - harness.expectFile('dist/browser/styles.css').content.not.toContain('color: blue'); - - harness.expectFile('dist/browser/other.css').content.toContain('color: green'); - harness.expectFile('dist/browser/other.css').content.toContain('color: aqua'); - harness.expectFile('dist/browser/other.css').content.not.toContain('color: blue'); - - await harness.writeFile('src/a.scss', '$primary: blue;\\nh1 { color: $primary; }'); - break; - case 2: - expect(result?.success).toBe(true); - harness.expectFile('dist/browser/styles.css').content.not.toContain('color: aqua'); - harness.expectFile('dist/browser/styles.css').content.toContain('color: blue'); - - harness.expectFile('dist/browser/other.css').content.toContain('color: green'); - harness.expectFile('dist/browser/other.css').content.not.toContain('color: aqua'); - harness.expectFile('dist/browser/other.css').content.toContain('color: blue'); - break; - } - }), - take(3), - count(), - ) - .toPromise(); - - expect(buildCount).toBe(3); + await harness.executeWithCases( + [ + async ({ result }) => { + expect(result?.success).toBe(false); + + await harness.writeFile('src/a.scss', '$primary: aqua;\\nh1 { color: $primary; }'); + }, + async ({ result }) => { + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/styles.css').content.toContain('color: aqua'); + harness.expectFile('dist/browser/styles.css').content.not.toContain('color: blue'); + + harness.expectFile('dist/browser/other.css').content.toContain('color: green'); + harness.expectFile('dist/browser/other.css').content.toContain('color: aqua'); + harness.expectFile('dist/browser/other.css').content.not.toContain('color: blue'); + + await harness.writeFile('src/a.scss', '$primary: blue;\\nh1 { color: $primary; }'); + }, + ({ result }) => { + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/styles.css').content.not.toContain('color: aqua'); + harness.expectFile('dist/browser/styles.css').content.toContain('color: blue'); + + harness.expectFile('dist/browser/other.css').content.toContain('color: green'); + harness.expectFile('dist/browser/other.css').content.not.toContain('color: aqua'); + harness.expectFile('dist/browser/other.css').content.toContain('color: blue'); + }, + ], + { outputLogsOnFailure: false }, + ); }); }); }); diff --git a/packages/angular/build/src/builders/application/tests/behavior/rebuild-index-html_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/rebuild-index-html_spec.ts index df9dbc6f0c93..99603bc98cee 100644 --- a/packages/angular/build/src/builders/application/tests/behavior/rebuild-index-html_spec.ts +++ b/packages/angular/build/src/builders/application/tests/behavior/rebuild-index-html_spec.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.dev/license */ -import { concatMap, count, take, timeout } from 'rxjs'; import { buildApplication } from '../../index'; import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; @@ -29,43 +28,28 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { watch: true, }); - const buildCount = await harness - .execute({ outputLogsOnFailure: false }) - .pipe( - timeout(30000), - concatMap(async ({ result }, index) => { - switch (index) { - case 0: - expect(result?.success).toBe(true); - harness.expectFile('dist/browser/index.html').content.toContain('charset="utf-8"'); - - await harness.modifyFile('src/index.html', (content) => - content.replace('charset="utf-8"', 'abc'), - ); - break; - case 1: - expect(result?.success).toBe(true); - harness - .expectFile('dist/browser/index.html') - .content.not.toContain('charset="utf-8"'); - - await harness.modifyFile('src/index.html', (content) => - content.replace('abc', 'charset="utf-8"'), - ); - break; - case 2: - expect(result?.success).toBe(true); - harness.expectFile('dist/browser/index.html').content.toContain('charset="utf-8"'); - - break; - } - }), - take(3), - count(), - ) - .toPromise(); - - expect(buildCount).toBe(3); + await harness.executeWithCases([ + async ({ result }) => { + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/index.html').content.toContain('charset="utf-8"'); + + await harness.modifyFile('src/index.html', (content) => + content.replace('charset="utf-8"', 'abc'), + ); + }, + async ({ result }) => { + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/index.html').content.not.toContain('charset="utf-8"'); + + await harness.modifyFile('src/index.html', (content) => + content.replace('abc', 'charset="utf-8"'), + ); + }, + ({ result }) => { + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/index.html').content.toContain('charset="utf-8"'); + }, + ]); }); }); }); diff --git a/packages/angular/build/src/builders/application/tests/behavior/rebuild-web-workers_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/rebuild-web-workers_spec.ts index 421e51f99f5b..4e167f2994c6 100644 --- a/packages/angular/build/src/builders/application/tests/behavior/rebuild-web-workers_spec.ts +++ b/packages/angular/build/src/builders/application/tests/behavior/rebuild-web-workers_spec.ts @@ -7,16 +7,9 @@ */ import { logging } from '@angular-devkit/core'; -import { concatMap, count, take, timeout } from 'rxjs'; import { buildApplication } from '../../index'; import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; -/** - * Maximum time in milliseconds for single build/rebuild - * This accounts for CI variability. - */ -export const BUILD_TIMEOUT = 30_000; - /** * A regular expression used to check if a built worker is correctly referenced in application code. */ @@ -56,84 +49,66 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { `, ); - const buildCount = await harness - .execute({ outputLogsOnFailure: false }) - .pipe( - timeout(BUILD_TIMEOUT), - concatMap(async ({ result, logs }, index) => { - switch (index) { - case 0: - expect(result?.success).toBeTrue(); - - // Ensure built worker is referenced in the application code - harness - .expectFile('dist/browser/main.js') - .content.toMatch(REFERENCED_WORKER_REGEXP); - - // Update the worker file to be invalid syntax - await harness.writeFile('src/app/worker.ts', `asd;fj$3~kls;kd^(*fjlk;sdj---flk`); - - break; - case 1: - expect(result?.success).toBeFalse(); - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching(errorText), - }), - ); - - // Make an unrelated change to verify error cache was updated - // Should persist error in the next rebuild - await harness.modifyFile('src/main.ts', (content) => content + '\n'); - - break; - case 2: - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching(errorText), - }), - ); - - // Revert the change that caused the error - // Should remove the error - await harness.writeFile('src/app/worker.ts', workerCodeFile); - - break; - case 3: - expect(result?.success).toBeTrue(); - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching(errorText), - }), - ); - - // Make an unrelated change to verify error cache was updated - // Should continue showing no error - await harness.modifyFile('src/main.ts', (content) => content + '\n'); - - break; - case 4: - expect(result?.success).toBeTrue(); - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching(errorText), - }), - ); - - // Ensure built worker is referenced in the application code - harness - .expectFile('dist/browser/main.js') - .content.toMatch(REFERENCED_WORKER_REGEXP); - - break; - } - }), - take(5), - count(), - ) - .toPromise(); - - expect(buildCount).toBe(5); + await harness.executeWithCases( + [ + async ({ result }) => { + expect(result?.success).toBeTrue(); + + // Ensure built worker is referenced in the application code + harness.expectFile('dist/browser/main.js').content.toMatch(REFERENCED_WORKER_REGEXP); + + // Update the worker file to be invalid syntax + await harness.writeFile('src/app/worker.ts', `asd;fj$3~kls;kd^(*fjlk;sdj---flk`); + }, + async ({ result, logs }) => { + expect(result?.success).toBeFalse(); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(errorText), + }), + ); + + // Make an unrelated change to verify error cache was updated + // Should persist error in the next rebuild + await harness.modifyFile('src/main.ts', (content) => content + '\n'); + }, + async ({ logs }) => { + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(errorText), + }), + ); + + // Revert the change that caused the error + // Should remove the error + await harness.writeFile('src/app/worker.ts', workerCodeFile); + }, + async ({ result, logs }) => { + expect(result?.success).toBeTrue(); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(errorText), + }), + ); + + // Make an unrelated change to verify error cache was updated + // Should continue showing no error + await harness.modifyFile('src/main.ts', (content) => content + '\n'); + }, + ({ result, logs }) => { + expect(result?.success).toBeTrue(); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(errorText), + }), + ); + + // Ensure built worker is referenced in the application code + harness.expectFile('dist/browser/main.js').content.toMatch(REFERENCED_WORKER_REGEXP); + }, + ], + { outputLogsOnFailure: false }, + ); }); }); }); diff --git a/packages/angular/build/src/builders/application/tests/behavior/typescript-rebuild-lazy_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/typescript-rebuild-lazy_spec.ts index c8dd39bfae5d..1f1efafaf3c5 100644 --- a/packages/angular/build/src/builders/application/tests/behavior/typescript-rebuild-lazy_spec.ts +++ b/packages/angular/build/src/builders/application/tests/behavior/typescript-rebuild-lazy_spec.ts @@ -7,7 +7,6 @@ */ import type { logging } from '@angular-devkit/core'; -import { concatMap, count, firstValueFrom, take, timeout } from 'rxjs'; import { buildApplication } from '../../index'; import { OutputHashing } from '../../schema'; import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; @@ -42,51 +41,39 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { ssr: true, }); - const buildCount = await firstValueFrom( - harness.execute({ outputLogsOnFailure: false }).pipe( - timeout(30_000), - concatMap(async ({ result, logs }, index) => { - switch (index) { - case 0: - expect(result?.success).toBeTrue(); + await harness.executeWithCases( + [ + async ({ result }) => { + expect(result?.success).toBeTrue(); - // Add valid code - await harness.appendToFile('src/lazy.ts', `console.log('foo');`); + // Add valid code + await harness.appendToFile('src/lazy.ts', `console.log('foo');`); + }, + async ({ result }) => { + expect(result?.success).toBeTrue(); - break; - case 1: - expect(result?.success).toBeTrue(); + // Update type of 'foo' to invalid (number -> string) + await harness.writeFile('src/lazy.ts', `export const foo: string = 1;`); + }, + async ({ result, logs }) => { + expect(result?.success).toBeFalse(); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching( + `Type 'number' is not assignable to type 'string'.`, + ), + }), + ); - // Update type of 'foo' to invalid (number -> string) - await harness.writeFile('src/lazy.ts', `export const foo: string = 1;`); - - break; - case 2: - expect(result?.success).toBeFalse(); - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching( - `Type 'number' is not assignable to type 'string'.`, - ), - }), - ); - - // Fix TS error - await harness.writeFile('src/lazy.ts', `export const foo: string = "1";`); - - break; - case 3: - expect(result?.success).toBeTrue(); - - break; - } - }), - take(4), - count(), - ), + // Fix TS error + await harness.writeFile('src/lazy.ts', `export const foo: string = "1";`); + }, + ({ result }) => { + expect(result?.success).toBeTrue(); + }, + ], + { outputLogsOnFailure: false }, ); - - expect(buildCount).toBe(4); }); }); }); diff --git a/packages/angular/build/src/builders/application/tests/behavior/typescript-rebuild-touch-file_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/typescript-rebuild-touch-file_spec.ts index 65f0540f2d1b..eeb160ebef47 100644 --- a/packages/angular/build/src/builders/application/tests/behavior/typescript-rebuild-touch-file_spec.ts +++ b/packages/angular/build/src/builders/application/tests/behavior/typescript-rebuild-touch-file_spec.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.dev/license */ -import { concatMap, count, take, timeout } from 'rxjs'; import { buildApplication } from '../../index'; import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; @@ -20,32 +19,23 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { aot, }); - const buildCount = await harness - .execute({ outputLogsOnFailure: false }) - .pipe( - timeout(30_000), - concatMap(async ({ result }, index) => { - switch (index) { - case 0: - expect(result?.success).toBeTrue(); - // Touch a file without doing any changes. - await harness.modifyFile('src/app/app.component.ts', (content) => content); - break; - case 1: - expect(result?.success).toBeTrue(); - await harness.removeFile('src/app/app.component.ts'); - break; - case 2: - expect(result?.success).toBeFalse(); - break; - } - }), - take(3), - count(), - ) - .toPromise(); - - expect(buildCount).toBe(3); + await harness.executeWithCases( + [ + async ({ result }) => { + expect(result?.success).toBeTrue(); + // Touch a file without doing any changes. + await harness.modifyFile('src/app/app.component.ts', (content) => content); + }, + async ({ result }) => { + expect(result?.success).toBeTrue(); + await harness.removeFile('src/app/app.component.ts'); + }, + ({ result }) => { + expect(result?.success).toBeFalse(); + }, + ], + { outputLogsOnFailure: false }, + ); }); } }); diff --git a/packages/angular/build/src/builders/application/tests/options/inline-style-language_spec.ts b/packages/angular/build/src/builders/application/tests/options/inline-style-language_spec.ts index 632bc6f1db7b..21a905c792d6 100644 --- a/packages/angular/build/src/builders/application/tests/options/inline-style-language_spec.ts +++ b/packages/angular/build/src/builders/application/tests/options/inline-style-language_spec.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.dev/license */ -import { concatMap, count, take, timeout } from 'rxjs'; import { buildApplication } from '../../index'; import { InlineStyleLanguage } from '../../schema'; import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; @@ -87,56 +86,38 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { content.replace('__STYLE_MARKER__', '$primary: indianred;\\nh1 { color: $primary; }'), ); - const buildCount = await harness - .execute() - .pipe( - timeout(30000), - concatMap(async ({ result }, index) => { - expect(result?.success).toBe(true); - - switch (index) { - case 0: - harness - .expectFile('dist/browser/main.js') - .content.toContain('color: indianred'); - harness.expectFile('dist/browser/main.js').content.not.toContain('color: aqua'); - - await harness.modifyFile('src/app/app.component.ts', (content) => - content.replace( - '$primary: indianred;\\nh1 { color: $primary; }', - '$primary: aqua;\\nh1 { color: $primary; }', - ), - ); - break; - case 1: - harness - .expectFile('dist/browser/main.js') - .content.not.toContain('color: indianred'); - harness.expectFile('dist/browser/main.js').content.toContain('color: aqua'); - - await harness.modifyFile('src/app/app.component.ts', (content) => - content.replace( - '$primary: aqua;\\nh1 { color: $primary; }', - '$primary: blue;\\nh1 { color: $primary; }', - ), - ); - break; - case 2: - harness - .expectFile('dist/browser/main.js') - .content.not.toContain('color: indianred'); - harness.expectFile('dist/browser/main.js').content.not.toContain('color: aqua'); - harness.expectFile('dist/browser/main.js').content.toContain('color: blue'); - - break; - } - }), - take(3), - count(), - ) - .toPromise(); - - expect(buildCount).toBe(3); + await harness.executeWithCases([ + async ({ result }) => { + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/main.js').content.toContain('color: indianred'); + harness.expectFile('dist/browser/main.js').content.not.toContain('color: aqua'); + + await harness.modifyFile('src/app/app.component.ts', (content) => + content.replace( + '$primary: indianred;\\nh1 { color: $primary; }', + '$primary: aqua;\\nh1 { color: $primary; }', + ), + ); + }, + async ({ result }) => { + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/main.js').content.not.toContain('color: indianred'); + harness.expectFile('dist/browser/main.js').content.toContain('color: aqua'); + + await harness.modifyFile('src/app/app.component.ts', (content) => + content.replace( + '$primary: aqua;\\nh1 { color: $primary; }', + '$primary: blue;\\nh1 { color: $primary; }', + ), + ); + }, + ({ result }) => { + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/main.js').content.not.toContain('color: indianred'); + harness.expectFile('dist/browser/main.js').content.not.toContain('color: aqua'); + harness.expectFile('dist/browser/main.js').content.toContain('color: blue'); + }, + ]); }); }); } diff --git a/packages/angular/build/src/builders/dev-server/tests/behavior/build-errors_spec.ts b/packages/angular/build/src/builders/dev-server/tests/behavior/build-errors_spec.ts index 82467da0d249..3bf4aa5fed6e 100644 --- a/packages/angular/build/src/builders/dev-server/tests/behavior/build-errors_spec.ts +++ b/packages/angular/build/src/builders/dev-server/tests/behavior/build-errors_spec.ts @@ -6,11 +6,10 @@ * found in the LICENSE file at https://angular.dev/license */ -import { concatMap, count, take, timeout } from 'rxjs'; +import { logging } from '@angular-devkit/core'; import { executeDevServer } from '../../index'; import { describeServeBuilder } from '../jasmine-helpers'; -import { BASE_OPTIONS, BUILD_TIMEOUT, DEV_SERVER_BUILDER_INFO } from '../setup'; -import { logging } from '@angular-devkit/core'; +import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup'; describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupTarget) => { describe('Behavior: "Rebuild Error Detection"', () => { @@ -27,40 +26,30 @@ describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupT // Missing ending `>` on the div will cause an error await harness.appendToFile('src/app/app.component.html', '
Hello, world! { - switch (index) { - case 0: - expect(result?.success).toBeFalse(); - debugger; - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching('Unexpected character "EOF"'), - }), - ); - - await harness.appendToFile('src/app/app.component.html', '>'); - - break; - case 1: - expect(result?.success).toBeTrue(); - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching('Unexpected character "EOF"'), - }), - ); - break; - } - }), - take(2), - count(), - ) - .toPromise(); - - expect(buildCount).toBe(2); + await harness.executeWithCases( + [ + async ({ result, logs }) => { + expect(result?.success).toBeFalse(); + debugger; + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('Unexpected character "EOF"'), + }), + ); + + await harness.appendToFile('src/app/app.component.html', '>'); + }, + ({ result, logs }) => { + expect(result?.success).toBeTrue(); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('Unexpected character "EOF"'), + }), + ); + }, + ], + { outputLogsOnFailure: false }, + ); }); }); }); diff --git a/packages/angular/build/src/builders/dev-server/tests/behavior/build_localize_replaced_watch_spec.ts b/packages/angular/build/src/builders/dev-server/tests/behavior/build_localize_replaced_watch_spec.ts index 9bc326ebe087..210dc01fc454 100644 --- a/packages/angular/build/src/builders/dev-server/tests/behavior/build_localize_replaced_watch_spec.ts +++ b/packages/angular/build/src/builders/dev-server/tests/behavior/build_localize_replaced_watch_spec.ts @@ -6,10 +6,9 @@ * found in the LICENSE file at https://angular.dev/license */ -import { concatMap, count, take, timeout } from 'rxjs'; import { executeDevServer } from '../../index'; import { describeServeBuilder } from '../jasmine-helpers'; -import { BASE_OPTIONS, BUILD_TIMEOUT, DEV_SERVER_BUILDER_INFO } from '../setup'; +import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup'; describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupTarget) => { describe('Behavior: "i18n $localize calls are replaced during watching"', () => { @@ -45,31 +44,24 @@ describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupT `, ); - const buildCount = await harness - .execute() - .pipe( - timeout(BUILD_TIMEOUT * 2), - concatMap(async ({ result }, index) => { - expect(result?.success).toBe(true); + await harness.executeWithCases([ + async ({ result }) => { + expect(result?.success).toBe(true); - const response = await fetch(new URL('main.js', `${result?.baseUrl}`)); - expect(await response?.text()).not.toContain('$localize`:'); + const response = await fetch(new URL('main.js', `${result?.baseUrl}`)); + expect(await response?.text()).not.toContain('$localize`:'); - switch (index) { - case 0: { - await harness.modifyFile('src/app/app.component.html', (content) => - content.replace('introduction', 'intro'), - ); - break; - } - } - }), - take(2), - count(), - ) - .toPromise(); + await harness.modifyFile('src/app/app.component.html', (content) => + content.replace('introduction', 'intro'), + ); + }, + async ({ result }) => { + expect(result?.success).toBe(true); - expect(buildCount).toBe(2); + const response = await fetch(new URL('main.js', `${result?.baseUrl}`)); + expect(await response?.text()).not.toContain('$localize`:'); + }, + ]); }); }); }); diff --git a/packages/angular/build/src/builders/dev-server/tests/behavior/build_translation_watch_spec.ts b/packages/angular/build/src/builders/dev-server/tests/behavior/build_translation_watch_spec.ts index 00c652449db2..b7d65e52e966 100644 --- a/packages/angular/build/src/builders/dev-server/tests/behavior/build_translation_watch_spec.ts +++ b/packages/angular/build/src/builders/dev-server/tests/behavior/build_translation_watch_spec.ts @@ -7,11 +7,10 @@ */ /* eslint-disable max-len */ -import { concatMap, count, take, timeout } from 'rxjs'; import { URL } from 'node:url'; import { executeDevServer } from '../../index'; import { describeServeBuilder } from '../jasmine-helpers'; -import { BASE_OPTIONS, BUILD_TIMEOUT, DEV_SERVER_BUILDER_INFO } from '../setup'; +import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup'; describeServeBuilder( executeDevServer, @@ -30,7 +29,7 @@ describeServeBuilder( }, i18n: { locales: { - 'fr': 'src/locales/messages.fr.xlf', + fr: 'src/locales/messages.fr.xlf', }, }, }); @@ -53,38 +52,26 @@ describeServeBuilder( await harness.writeFile('src/locales/messages.fr.xlf', TRANSLATION_FILE_CONTENT); - const buildCount = await harness - .execute() - .pipe( - timeout(BUILD_TIMEOUT), - concatMap(async ({ result }, index) => { - expect(result?.success).toBe(true); + await harness.executeWithCases([ + async ({ result }) => { + expect(result?.success).toBe(true); - const mainUrl = new URL('main.js', `${result?.baseUrl}`); + const mainUrl = new URL('main.js', `${result?.baseUrl}`); + const response = await fetch(mainUrl); + expect(await response?.text()).toContain('Bonjour'); - switch (index) { - case 0: { - const response = await fetch(mainUrl); - expect(await response?.text()).toContain('Bonjour'); - - await harness.modifyFile('src/locales/messages.fr.xlf', (content) => - content.replace('Bonjour', 'Salut'), - ); - break; - } - case 1: { - const response = await fetch(mainUrl); - expect(await response?.text()).toContain('Salut'); - break; - } - } - }), - take(2), - count(), - ) - .toPromise(); + await harness.modifyFile('src/locales/messages.fr.xlf', (content) => + content.replace('Bonjour', 'Salut'), + ); + }, + async ({ result }) => { + expect(result?.success).toBe(true); - expect(buildCount).toBe(2); + const mainUrl = new URL('main.js', `${result?.baseUrl}`); + const response = await fetch(mainUrl); + expect(await response?.text()).toContain('Salut'); + }, + ]); }); }); }, diff --git a/packages/angular/build/src/builders/dev-server/tests/behavior/serve-live-reload-proxies_spec.ts b/packages/angular/build/src/builders/dev-server/tests/behavior/serve-live-reload-proxies_spec.ts index 7617e31b45af..083773529058 100644 --- a/packages/angular/build/src/builders/dev-server/tests/behavior/serve-live-reload-proxies_spec.ts +++ b/packages/angular/build/src/builders/dev-server/tests/behavior/serve-live-reload-proxies_spec.ts @@ -12,10 +12,9 @@ import { createServer } from 'node:http'; import { createProxyServer } from 'http-proxy'; import { AddressInfo } from 'node:net'; import puppeteer, { Browser, Page } from 'puppeteer'; -import { count, debounceTime, finalize, switchMap, take, timeout } from 'rxjs'; import { executeDevServer } from '../../index'; import { describeServeBuilder } from '../jasmine-helpers'; -import { BASE_OPTIONS, BUILD_TIMEOUT, DEV_SERVER_BUILDER_INFO } from '../setup'; +import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup'; // eslint-disable-next-line @typescript-eslint/no-explicit-any declare const document: any; @@ -190,38 +189,24 @@ describeServeBuilder( await harness.writeFile('src/app/app.component.html', '

{{ title }}

'); - const buildCount = await harness - .execute() - .pipe( - debounceTime(1000), - timeout(BUILD_TIMEOUT * 2), - switchMap(async ({ result }, index) => { - expect(result?.success).toBeTrue(); - if (typeof result?.baseUrl !== 'string') { - throw new Error('Expected "baseUrl" to be a string.'); - } - - switch (index) { - case 0: - await goToPageAndWaitForWS(page, result.baseUrl); - await harness.modifyFile('src/app/app.component.ts', (content) => - content.replace(`'app'`, `'app-live-reload'`), - ); - break; - case 1: - const innerText = await page.evaluate( - () => document.querySelector('p').innerText, - ); - expect(innerText).toBe('app-live-reload'); - break; - } - }), - take(2), - count(), - ) - .toPromise(); - - expect(buildCount).toBe(2); + await harness.executeWithCases([ + async ({ result }) => { + expect(result?.success).toBeTrue(); + if (typeof result?.baseUrl !== 'string') { + throw new Error('Expected "baseUrl" to be a string.'); + } + + await goToPageAndWaitForWS(page, result.baseUrl); + await harness.modifyFile('src/app/app.component.ts', (content) => + content.replace(`'app'`, `'app-live-reload'`), + ); + }, + async ({ result }) => { + expect(result?.success).toBeTrue(); + const innerText = await page.evaluate(() => document.querySelector('p').innerText); + expect(innerText).toBe('app-live-reload'); + }, + ]); }); it('works without http -> http proxy', async () => { @@ -232,42 +217,29 @@ describeServeBuilder( await harness.writeFile('src/app/app.component.html', '

{{ title }}

'); let proxy: ProxyInstance | undefined; - const buildCount = await harness - .execute() - .pipe( - debounceTime(1000), - timeout(BUILD_TIMEOUT * 2), - switchMap(async ({ result }, index) => { + try { + await harness.executeWithCases([ + async ({ result }) => { expect(result?.success).toBeTrue(); if (typeof result?.baseUrl !== 'string') { throw new Error('Expected "baseUrl" to be a string.'); } - switch (index) { - case 0: - proxy = await createProxy(result.baseUrl, false); - await goToPageAndWaitForWS(page, proxy.url); - await harness.modifyFile('src/app/app.component.ts', (content) => - content.replace(`'app'`, `'app-live-reload'`), - ); - break; - case 1: - const innerText = await page.evaluate( - () => document.querySelector('p').innerText, - ); - expect(innerText).toBe('app-live-reload'); - break; - } - }), - take(2), - count(), - finalize(() => { - proxy?.server.close(); - }), - ) - .toPromise(); - - expect(buildCount).toBe(2); + proxy = await createProxy(result.baseUrl, false); + await goToPageAndWaitForWS(page, proxy.url); + await harness.modifyFile('src/app/app.component.ts', (content) => + content.replace(`'app'`, `'app-live-reload'`), + ); + }, + async ({ result }) => { + expect(result?.success).toBeTrue(); + const innerText = await page.evaluate(() => document.querySelector('p').innerText); + expect(innerText).toBe('app-live-reload'); + }, + ]); + } finally { + proxy?.server.close(); + } }); it('works without https -> http proxy', async () => { @@ -278,42 +250,29 @@ describeServeBuilder( await harness.writeFile('src/app/app.component.html', '

{{ title }}

'); let proxy: ProxyInstance | undefined; - const buildCount = await harness - .execute() - .pipe( - debounceTime(1000), - timeout(BUILD_TIMEOUT * 2), - switchMap(async ({ result }, index) => { + try { + await harness.executeWithCases([ + async ({ result }) => { expect(result?.success).toBeTrue(); if (typeof result?.baseUrl !== 'string') { throw new Error('Expected "baseUrl" to be a string.'); } - switch (index) { - case 0: - proxy = await createProxy(result.baseUrl, true); - await goToPageAndWaitForWS(page, proxy.url); - await harness.modifyFile('src/app/app.component.ts', (content) => - content.replace(`'app'`, `'app-live-reload'`), - ); - break; - case 1: - const innerText = await page.evaluate( - () => document.querySelector('p').innerText, - ); - expect(innerText).toBe('app-live-reload'); - break; - } - }), - take(2), - count(), - finalize(() => { - proxy?.server.close(); - }), - ) - .toPromise(); - - expect(buildCount).toBe(2); + proxy = await createProxy(result.baseUrl, true); + await goToPageAndWaitForWS(page, proxy.url); + await harness.modifyFile('src/app/app.component.ts', (content) => + content.replace(`'app'`, `'app-live-reload'`), + ); + }, + async ({ result }) => { + expect(result?.success).toBeTrue(); + const innerText = await page.evaluate(() => document.querySelector('p').innerText); + expect(innerText).toBe('app-live-reload'); + }, + ]); + } finally { + proxy?.server.close(); + } }); }, ); diff --git a/packages/angular/build/src/builders/dev-server/tests/behavior/serve_service-worker_spec.ts b/packages/angular/build/src/builders/dev-server/tests/behavior/serve_service-worker_spec.ts index f0a237cae51a..b3b63c3a3093 100644 --- a/packages/angular/build/src/builders/dev-server/tests/behavior/serve_service-worker_spec.ts +++ b/packages/angular/build/src/builders/dev-server/tests/behavior/serve_service-worker_spec.ts @@ -6,11 +6,10 @@ * found in the LICENSE file at https://angular.dev/license */ -import { concatMap, count, take, timeout } from 'rxjs'; import { executeDevServer } from '../../index'; import { executeOnceAndFetch } from '../execute-fetch'; import { describeServeBuilder } from '../jasmine-helpers'; -import { BASE_OPTIONS, BUILD_TIMEOUT, DEV_SERVER_BUILDER_INFO } from '../setup'; +import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup'; const manifest = { index: '/index.html', @@ -57,7 +56,7 @@ describeServeBuilder( }, i18n: { sourceLocale: { - 'code': 'fr', + code: 'fr', }, }, }); @@ -176,48 +175,40 @@ describeServeBuilder( watch: true, }); - const buildCount = await harness - .execute() - .pipe( - timeout(BUILD_TIMEOUT), - concatMap(async ({ result }, index) => { - expect(result?.success).toBeTrue(); - const response = await fetch(new URL('ngsw.json', `${result?.baseUrl}`)); - const { hashTable } = (await response.json()) as { hashTable: object }; - const hashTableEntries = Object.keys(hashTable); - - switch (index) { - case 0: - expect(hashTableEntries).toEqual([ - '/assets/folder-asset.txt', - '/favicon.ico', - '/index.html', - '/media/spectrum.png', - ]); - - await harness.writeFile( - 'src/assets/folder-new-asset.txt', - harness.readFile('src/assets/folder-asset.txt'), - ); - break; - - case 1: - expect(hashTableEntries).toEqual([ - '/assets/folder-asset.txt', - '/assets/folder-new-asset.txt', - '/favicon.ico', - '/index.html', - '/media/spectrum.png', - ]); - break; - } - }), - take(2), - count(), - ) - .toPromise(); - - expect(buildCount).toBe(2); + await harness.executeWithCases([ + async ({ result }) => { + expect(result?.success).toBeTrue(); + const response = await fetch(new URL('ngsw.json', `${result?.baseUrl}`)); + const { hashTable } = (await response.json()) as { hashTable: object }; + const hashTableEntries = Object.keys(hashTable); + + expect(hashTableEntries).toEqual([ + '/assets/folder-asset.txt', + '/favicon.ico', + '/index.html', + '/media/spectrum.png', + ]); + + await harness.writeFile( + 'src/assets/folder-new-asset.txt', + harness.readFile('src/assets/folder-asset.txt'), + ); + }, + async ({ result }) => { + expect(result?.success).toBeTrue(); + const response = await fetch(new URL('ngsw.json', `${result?.baseUrl}`)); + const { hashTable } = (await response.json()) as { hashTable: object }; + const hashTableEntries = Object.keys(hashTable); + + expect(hashTableEntries).toEqual([ + '/assets/folder-asset.txt', + '/assets/folder-new-asset.txt', + '/favicon.ico', + '/index.html', + '/media/spectrum.png', + ]); + }, + ]); }); }); }, diff --git a/packages/angular/build/src/builders/karma/tests/behavior/rebuilds_spec.ts b/packages/angular/build/src/builders/karma/tests/behavior/rebuilds_spec.ts index 6ec02c2c28f1..a03dbf235982 100644 --- a/packages/angular/build/src/builders/karma/tests/behavior/rebuilds_spec.ts +++ b/packages/angular/build/src/builders/karma/tests/behavior/rebuilds_spec.ts @@ -6,10 +6,8 @@ * found in the LICENSE file at https://angular.dev/license */ -import { concatMap, count, debounceTime, distinctUntilChanged, take, timeout } from 'rxjs'; import { execute } from '../../index'; import { BASE_OPTIONS, KARMA_BUILDER_INFO, describeKarmaBuilder } from '../setup'; -import { BuilderOutput } from '@angular-devkit/architect'; import { randomBytes } from 'node:crypto'; describeKarmaBuilder(execute, KARMA_BUILDER_INFO, (harness, setupTarget) => { @@ -26,48 +24,29 @@ describeKarmaBuilder(execute, KARMA_BUILDER_INFO, (harness, setupTarget) => { const goodFile = await harness.readFile('src/app/app.component.spec.ts'); - interface OutputCheck { - (result: BuilderOutput | undefined): Promise; - } - - const expectedSequence: OutputCheck[] = [ - async (result) => { - // Karma run should succeed. - // Add a compilation error. - expect(result?.success).withContext('Initial test run should succeed').toBeTrue(); - // Add an syntax error to a non-main file. - await harness.appendToFile('src/app/app.component.spec.ts', `error`); - }, - async (result) => { - expect(result?.success) - .withContext('Test should fail after build error was introduced') - .toBeFalse(); - await harness.writeFile('src/app/app.component.spec.ts', goodFile); - }, - async (result) => { - expect(result?.success) - .withContext('Test should succeed again after build error was fixed') - .toBeTrue(); - }, - ]; - - const buildCount = await harness - .execute({ outputLogsOnFailure: false }) - .pipe( - timeout(60000), - debounceTime(500), - // There may be a sequence of {success:true} events that should be - // de-duplicated. - distinctUntilChanged((prev, current) => prev.result?.success === current.result?.success), - concatMap(async ({ result }, index) => { - await expectedSequence[index](result); - }), - take(expectedSequence.length), - count(), - ) - .toPromise(); - - expect(buildCount).toBe(expectedSequence.length); + await harness.executeWithCases( + [ + async ({ result }) => { + // Karma run should succeed. + // Add a compilation error. + expect(result?.success).withContext('Initial test run should succeed').toBeTrue(); + // Add an syntax error to a non-main file. + await harness.appendToFile('src/app/app.component.spec.ts', `error`); + }, + async ({ result }) => { + expect(result?.success) + .withContext('Test should fail after build error was introduced') + .toBeFalse(); + await harness.writeFile('src/app/app.component.spec.ts', goodFile); + }, + ({ result }) => { + expect(result?.success) + .withContext('Test should succeed again after build error was fixed') + .toBeTrue(); + }, + ], + { outputLogsOnFailure: false }, + ); }); it('correctly serves binary assets on rebuilds', async () => { @@ -89,12 +68,8 @@ describeKarmaBuilder(execute, KARMA_BUILDER_INFO, (harness, setupTarget) => { assets: ['src/random.bin'], }); - interface OutputCheck { - (result: BuilderOutput | undefined): Promise; - } - - const expectedSequence: OutputCheck[] = [ - async (result) => { + await harness.executeWithCases([ + async ({ result }) => { // Karma run should succeed. expect(result?.success).withContext('Initial test run should succeed').toBeTrue(); // Modify test file to trigger a rebuild @@ -103,25 +78,10 @@ describeKarmaBuilder(execute, KARMA_BUILDER_INFO, (harness, setupTarget) => { `\n;console.log('modified');`, ); }, - async (result) => { + ({ result }) => { expect(result?.success).withContext('Test should succeed again').toBeTrue(); }, - ]; - - const buildCount = await harness - .execute({ outputLogsOnFailure: true }) - .pipe( - timeout(60000), - debounceTime(500), - concatMap(async ({ result }, index) => { - await expectedSequence[index](result); - }), - take(expectedSequence.length), - count(), - ) - .toPromise(); - - expect(buildCount).toBe(expectedSequence.length); + ]); }); }); }); diff --git a/packages/angular_devkit/build_angular/src/builders/browser/specs/styles_spec.ts b/packages/angular_devkit/build_angular/src/builders/browser/specs/styles_spec.ts index 1c1aaaee202a..b91062b85f4d 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser/specs/styles_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser/specs/styles_spec.ts @@ -492,30 +492,31 @@ describe('Browser Builder styles', () => { await browserBuild(architect, host, target, overrides); }); - it('causes equal failure for tilde and tilde-slash url()', async () => { + it('causes equal failure for tilde url()', async () => { host.writeMultipleFiles({ 'src/styles.css': ` body { - background-image: url('~/does-not-exist.jpg'); + background-image: url('~does-not-exist.jpg'); } `, }); - const overrides = { optimization: true }; - const run = await architect.scheduleTarget(target, overrides); + const run = await architect.scheduleTarget(target, { optimization: true }); await expectAsync(run.result).toBeResolvedTo(jasmine.objectContaining({ success: false })); + await run.stop(); + }); + it('causes equal failure for tilde-slash url()', async () => { host.writeMultipleFiles({ 'src/styles.css': ` body { - background-image: url('~does-not-exist.jpg'); + background-image: url('~/does-not-exist.jpg'); } `, }); - const run2 = await architect.scheduleTarget(target, overrides); - await expectAsync(run2.result).toBeResolvedTo(jasmine.objectContaining({ success: false })); - await run2.stop(); + const run = await architect.scheduleTarget(target, { optimization: true }); + await expectAsync(run.result).toBeResolvedTo(jasmine.objectContaining({ success: false })); await run.stop(); }); @@ -583,9 +584,7 @@ describe('Browser Builder styles', () => { const { files } = await browserBuild(architect, host, target, overrides); expect(await files['styles.css']).toMatch(/\.one(.|\n|\r)*\.two(.|\n|\r)*\.three/); }); - }); - extensionsWithImportSupport.forEach((ext) => { it(`adjusts relative resource URLs when using @import in ${ext} (global)`, async () => { host.copyFile('src/spectrum.png', './src/more-styles/images/global-img-relative.png'); host.writeMultipleFiles({ @@ -659,7 +658,7 @@ describe('Browser Builder styles', () => { result = await browserBuild(architect, host, target, { optimization: true }); expect(await result.files['styles.css']).toContain('rgba(0,0,0,.15)'); - }); + }, 80_000); it('works when using the same css file in `styles` and `stylesUrl`', async () => { host.writeMultipleFiles({ diff --git a/packages/angular_devkit/build_angular/src/builders/browser/tests/behavior/index_watch_spec.ts b/packages/angular_devkit/build_angular/src/builders/browser/tests/behavior/index_watch_spec.ts index 423b1ddf5311..a0b0b7fadb26 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser/tests/behavior/index_watch_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser/tests/behavior/index_watch_spec.ts @@ -6,8 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import { concatMap, count, take, timeout } from 'rxjs'; -import { BUILD_TIMEOUT, buildWebpackBrowser } from '../../index'; +import { buildWebpackBrowser } from '../../index'; import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { @@ -18,36 +17,24 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { watch: true, }); - const buildCount = await harness - .execute() - .pipe( - timeout(BUILD_TIMEOUT), - concatMap(async ({ result }, index) => { - expect(result?.success).toBe(true); + await harness.executeWithCases([ + async ({ result }) => { + expect(result?.success).toBe(true); - switch (index) { - case 0: { - harness.expectFile('dist/index.html').content.toContain('HelloWorldApp'); - harness.expectFile('dist/index.html').content.not.toContain('UpdatedPageTitle'); + harness.expectFile('dist/index.html').content.toContain('HelloWorldApp'); + harness.expectFile('dist/index.html').content.not.toContain('UpdatedPageTitle'); - // Trigger rebuild - await harness.modifyFile('src/index.html', (s) => - s.replace('HelloWorldApp', 'UpdatedPageTitle'), - ); - break; - } - case 1: { - harness.expectFile('dist/index.html').content.toContain('UpdatedPageTitle'); - break; - } - } - }), - take(2), - count(), - ) - .toPromise(); + // Trigger rebuild + await harness.modifyFile('src/index.html', (s) => + s.replace('HelloWorldApp', 'UpdatedPageTitle'), + ); + }, + ({ result }) => { + expect(result?.success).toBe(true); - expect(buildCount).toBe(2); + harness.expectFile('dist/index.html').content.toContain('UpdatedPageTitle'); + }, + ]); }); }); }); diff --git a/packages/angular_devkit/build_angular/src/builders/browser/tests/behavior/localize_watch_spec.ts b/packages/angular_devkit/build_angular/src/builders/browser/tests/behavior/localize_watch_spec.ts index 5f51ce3c87b4..2c39bf738b3a 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser/tests/behavior/localize_watch_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser/tests/behavior/localize_watch_spec.ts @@ -6,8 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import { concatMap, count, take, timeout } from 'rxjs'; -import { BUILD_TIMEOUT, buildWebpackBrowser } from '../../index'; +import { buildWebpackBrowser } from '../../index'; import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { @@ -45,33 +44,19 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { await harness.writeFile('src/locales/messages.fr.xlf', TRANSLATION_FILE_CONTENT); - const buildCount = await harness - .execute() - .pipe( - timeout(BUILD_TIMEOUT), - concatMap(async ({ result }, index) => { - expect(result?.success).toBe(true); + await harness.executeWithCases([ + async ({ result }) => { + expect(result?.success).toBe(true); + harness.expectFile('dist/fr/main.js').content.toContain('Bonjour'); - switch (index) { - case 0: { - harness.expectFile('dist/fr/main.js').content.toContain('Bonjour'); - - // Trigger rebuild - await harness.appendToFile('src/app/app.component.html', '\n\n'); - break; - } - case 1: { - harness.expectFile('dist/fr/main.js').content.toContain('Bonjour'); - break; - } - } - }), - take(2), - count(), - ) - .toPromise(); - - expect(buildCount).toBe(2); + // Trigger rebuild + await harness.appendToFile('src/app/app.component.html', '\n\n'); + }, + ({ result }) => { + expect(result?.success).toBe(true); + harness.expectFile('dist/fr/main.js').content.toContain('Bonjour'); + }, + ]); }); }); }); diff --git a/packages/angular_devkit/build_angular/src/builders/browser/tests/behavior/rebuild-errors_spec.ts b/packages/angular_devkit/build_angular/src/builders/browser/tests/behavior/rebuild-errors_spec.ts index ea4501600bab..0daece623c63 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser/tests/behavior/rebuild-errors_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser/tests/behavior/rebuild-errors_spec.ts @@ -7,8 +7,7 @@ */ import { logging } from '@angular-devkit/core'; -import { concatMap, count, take, timeout } from 'rxjs'; -import { BUILD_TIMEOUT, buildWebpackBrowser } from '../../index'; +import { buildWebpackBrowser } from '../../index'; import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { @@ -68,85 +67,71 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { `, ); - const buildCount = await harness - .execute({ outputLogsOnFailure: false }) - .pipe( - timeout(BUILD_TIMEOUT), - concatMap(async ({ result, logs }, index) => { - switch (index) { - case 0: - expect(result?.success).toBeTrue(); + await harness.executeWithCases( + [ + async ({ result }) => { + expect(result?.success).toBeTrue(); - // Update directive to use a different input type for 'foo' (number -> string) - // Should cause a template error - await harness.writeFile( - 'src/app/dir.ts', - ` + // Update directive to use a different input type for 'foo' (number -> string) + // Should cause a template error + await harness.writeFile( + 'src/app/dir.ts', + ` import { Directive, Input } from '@angular/core'; @Directive({ selector: 'dir', standalone: false }) export class Dir { @Input() foo: string; } `, - ); - - break; - case 1: - expect(result?.success).toBeFalse(); - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching(typeErrorText), - }), - ); - - // Make an unrelated change to verify error cache was updated - // Should persist error in the next rebuild - await harness.modifyFile('src/main.ts', (content) => content + '\n'); - - break; - case 2: - expect(result?.success).toBeFalse(); - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching(typeErrorText), - }), - ); - - // Revert the directive change that caused the error - // Should remove the error - await harness.writeFile('src/app/dir.ts', goodDirectiveContents); - - break; - case 3: - expect(result?.success).toBeTrue(); - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching(typeErrorText), - }), - ); - - // Make an unrelated change to verify error cache was updated - // Should continue showing no error - await harness.modifyFile('src/main.ts', (content) => content + '\n'); - - break; - case 4: - expect(result?.success).toBeTrue(); - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching(typeErrorText), - }), - ); - - break; - } - }), - take(5), - count(), - ) - .toPromise(); - - expect(buildCount).toBe(5); + ); + }, + async ({ result, logs }) => { + expect(result?.success).toBeFalse(); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(typeErrorText), + }), + ); + + // Make an unrelated change to verify error cache was updated + // Should persist error in the next rebuild + await harness.modifyFile('src/main.ts', (content) => content + '\n'); + }, + async ({ result, logs }) => { + expect(result?.success).toBeFalse(); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(typeErrorText), + }), + ); + + // Revert the directive change that caused the error + // Should remove the error + await harness.writeFile('src/app/dir.ts', goodDirectiveContents); + }, + async ({ result, logs }) => { + expect(result?.success).toBeTrue(); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(typeErrorText), + }), + ); + + // Make an unrelated change to verify error cache was updated + // Should continue showing no error + await harness.modifyFile('src/main.ts', (content) => content + '\n'); + }, + ({ result, logs }) => { + expect(result?.success).toBeTrue(); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(typeErrorText), + }), + ); + }, + ], + { outputLogsOnFailure: false }, + ); }); it('detects template errors with AOT codegen differences', async () => { @@ -218,85 +203,71 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { `, ); - const buildCount = await harness - .execute({ outputLogsOnFailure: false }) - .pipe( - timeout(BUILD_TIMEOUT), - concatMap(async ({ result, logs }, index) => { - switch (index) { - case 0: - expect(result?.success).toBeTrue(); + await harness.executeWithCases( + [ + async ({ result }) => { + expect(result?.success).toBeTrue(); - // Update second directive to use string property `foo` as an Input - // Should cause a template error - await harness.writeFile( - 'src/app/dir2.ts', - ` + // Update second directive to use string property `foo` as an Input + // Should cause a template error + await harness.writeFile( + 'src/app/dir2.ts', + ` import { Directive, Input } from '@angular/core'; @Directive({ selector: 'dir', standalone: false }) export class Dir2 { @Input() foo: string; } `, - ); - - break; - case 1: - expect(result?.success).toBeFalse(); - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching(typeErrorText), - }), - ); - - // Make an unrelated change to verify error cache was updated - // Should persist error in the next rebuild - await harness.modifyFile('src/main.ts', (content) => content + '\n'); - - break; - case 2: - expect(result?.success).toBeFalse(); - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching(typeErrorText), - }), - ); - - // Revert the directive change that caused the error - // Should remove the error - await harness.writeFile('src/app/dir2.ts', goodDirectiveContents); - - break; - case 3: - expect(result?.success).toBeTrue(); - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching(typeErrorText), - }), - ); - - // Make an unrelated change to verify error cache was updated - // Should continue showing no error - await harness.modifyFile('src/main.ts', (content) => content + '\n'); - - break; - case 4: - expect(result?.success).toBeTrue(); - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching(typeErrorText), - }), - ); - - break; - } - }), - take(5), - count(), - ) - .toPromise(); - - expect(buildCount).toBe(5); + ); + }, + async ({ result, logs }) => { + expect(result?.success).toBeFalse(); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(typeErrorText), + }), + ); + + // Make an unrelated change to verify error cache was updated + // Should persist error in the next rebuild + await harness.modifyFile('src/main.ts', (content) => content + '\n'); + }, + async ({ result, logs }) => { + expect(result?.success).toBeFalse(); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(typeErrorText), + }), + ); + + // Revert the directive change that caused the error + // Should remove the error + await harness.writeFile('src/app/dir2.ts', goodDirectiveContents); + }, + async ({ result, logs }) => { + expect(result?.success).toBeTrue(); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(typeErrorText), + }), + ); + + // Make an unrelated change to verify error cache was updated + // Should continue showing no error + await harness.modifyFile('src/main.ts', (content) => content + '\n'); + }, + ({ result, logs }) => { + expect(result?.success).toBeTrue(); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(typeErrorText), + }), + ); + }, + ], + { outputLogsOnFailure: false }, + ); }); it('recovers from component stylesheet error', async () => { @@ -306,47 +277,35 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { aot: false, }); - const buildCount = await harness - .execute({ outputLogsOnFailure: false }) - .pipe( - timeout(BUILD_TIMEOUT), - concatMap(async ({ result, logs }, index) => { - switch (index) { - case 0: - expect(result?.success).toBeTrue(); - await harness.writeFile('src/app/app.component.css', 'invalid-css-content'); - - break; - case 1: - expect(result?.success).toBeFalse(); - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching('invalid-css-content'), - }), - ); - - await harness.writeFile('src/app/app.component.css', 'p { color: green }'); - - break; - case 2: - expect(result?.success).toBeTrue(); - expect(logs).not.toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching('invalid-css-content'), - }), - ); - - harness.expectFile('dist/main.js').content.toContain('p { color: green }'); - - break; - } - }), - take(3), - count(), - ) - .toPromise(); - - expect(buildCount).toBe(3); + await harness.executeWithCases( + [ + async ({ result }) => { + expect(result?.success).toBeTrue(); + await harness.writeFile('src/app/app.component.css', 'invalid-css-content'); + }, + async ({ result, logs }) => { + expect(result?.success).toBeFalse(); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('invalid-css-content'), + }), + ); + + await harness.writeFile('src/app/app.component.css', 'p { color: green }'); + }, + ({ result, logs }) => { + expect(result?.success).toBeTrue(); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('invalid-css-content'), + }), + ); + + harness.expectFile('dist/main.js').content.toContain('p { color: green }'); + }, + ], + { outputLogsOnFailure: false }, + ); }); }); }); diff --git a/packages/angular_devkit/build_angular/src/builders/browser/tests/options/inline-style-language_spec.ts b/packages/angular_devkit/build_angular/src/builders/browser/tests/options/inline-style-language_spec.ts index 177341814525..eede6f2f8099 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser/tests/options/inline-style-language_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser/tests/options/inline-style-language_spec.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.dev/license */ -import { concatMap, count, take, timeout } from 'rxjs'; import { buildWebpackBrowser } from '../../index'; import { InlineStyleLanguage } from '../../schema'; import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; @@ -88,49 +87,38 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { content.replace('__STYLE_MARKER__', '$primary: indianred;\\nh1 { color: $primary; }'), ); - const buildCount = await harness - .execute() - .pipe( - timeout(30000), - concatMap(async ({ result }, index) => { - expect(result?.success).toBe(true); - - switch (index) { - case 0: - harness.expectFile('dist/main.js').content.toContain('color: indianred'); - harness.expectFile('dist/main.js').content.not.toContain('color: aqua'); - - await harness.modifyFile('src/app/app.component.ts', (content) => - content.replace( - '$primary: indianred;\\nh1 { color: $primary; }', - '$primary: aqua;\\nh1 { color: $primary; }', - ), - ); - break; - case 1: - harness.expectFile('dist/main.js').content.not.toContain('color: indianred'); - harness.expectFile('dist/main.js').content.toContain('color: aqua'); - - await harness.modifyFile('src/app/app.component.ts', (content) => - content.replace( - '$primary: aqua;\\nh1 { color: $primary; }', - '$primary: blue;\\nh1 { color: $primary; }', - ), - ); - break; - case 2: - harness.expectFile('dist/main.js').content.not.toContain('color: indianred'); - harness.expectFile('dist/main.js').content.not.toContain('color: aqua'); - harness.expectFile('dist/main.js').content.toContain('color: blue'); - break; - } - }), - take(3), - count(), - ) - .toPromise(); - - expect(buildCount).toBe(3); + await harness.executeWithCases([ + async ({ result }) => { + expect(result?.success).toBe(true); + harness.expectFile('dist/main.js').content.toContain('color: indianred'); + harness.expectFile('dist/main.js').content.not.toContain('color: aqua'); + + await harness.modifyFile('src/app/app.component.ts', (content) => + content.replace( + '$primary: indianred;\\nh1 { color: $primary; }', + '$primary: aqua;\\nh1 { color: $primary; }', + ), + ); + }, + async ({ result }) => { + expect(result?.success).toBe(true); + harness.expectFile('dist/main.js').content.not.toContain('color: indianred'); + harness.expectFile('dist/main.js').content.toContain('color: aqua'); + + await harness.modifyFile('src/app/app.component.ts', (content) => + content.replace( + '$primary: aqua;\\nh1 { color: $primary; }', + '$primary: blue;\\nh1 { color: $primary; }', + ), + ); + }, + ({ result }) => { + expect(result?.success).toBe(true); + harness.expectFile('dist/main.js').content.not.toContain('color: indianred'); + harness.expectFile('dist/main.js').content.not.toContain('color: aqua'); + harness.expectFile('dist/main.js').content.toContain('color: blue'); + }, + ]); }); }); } diff --git a/packages/angular_devkit/build_angular/src/builders/browser/tests/options/verbose_spec.ts b/packages/angular_devkit/build_angular/src/builders/browser/tests/options/verbose_spec.ts index fccda49f8fea..c74af39557ed 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser/tests/options/verbose_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser/tests/options/verbose_spec.ts @@ -7,8 +7,7 @@ */ import { logging } from '@angular-devkit/core'; -import { concatMap, count, take, timeout } from 'rxjs'; -import { BUILD_TIMEOUT, buildWebpackBrowser } from '../../index'; +import { buildWebpackBrowser } from '../../index'; import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; // The below plugin is only enabled when verbose option is set to true. @@ -73,34 +72,23 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { watch: true, }); - await harness - .execute() - .pipe( - timeout(BUILD_TIMEOUT), - concatMap(async ({ result, logs }, index) => { - expect(result?.success).toBeTrue(); - - switch (index) { - case 0: - // Amend file - await harness.appendToFile('/src/main.ts', ' '); - break; - case 1: - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching( - /angular\.watch-files-logs-plugin\n\s+Modified files:\n.+main\.ts/, - ), - }), - ); - - break; - } - }), - take(2), - count(), - ) - .toPromise(); + await harness.executeWithCases([ + async ({ result }) => { + expect(result?.success).toBeTrue(); + // Amend file + await harness.appendToFile('/src/main.ts', ' '); + }, + ({ result, logs }) => { + expect(result?.success).toBeTrue(); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching( + /angular\.watch-files-logs-plugin\n\s+Modified files:\n.+main\.ts/, + ), + }), + ); + }, + ]); }); it('should not include error stacktraces when false', async () => { diff --git a/packages/angular_devkit/build_angular/src/builders/browser/tests/options/watch_spec.ts b/packages/angular_devkit/build_angular/src/builders/browser/tests/options/watch_spec.ts index 5c39195e7009..d61290aa0b7c 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser/tests/options/watch_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser/tests/options/watch_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import { concatMap, count, take, timeout } from 'rxjs'; +import { timeout } from 'rxjs'; import { buildWebpackBrowser } from '../../index'; import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; @@ -77,33 +77,21 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { watch: true, }); - const buildCount = await harness - .execute() - .pipe( - timeout(30000), - concatMap(async ({ result }, index) => { - expect(result?.success).toBe(true); - - switch (index) { - case 0: - harness.expectFile('dist/main.js').content.not.toContain('abcd1234'); - - await harness.modifyFile( - 'src/main.ts', - (content) => content + 'console.log("abcd1234");', - ); - break; - case 1: - harness.expectFile('dist/main.js').content.toContain('abcd1234'); - break; - } - }), - take(2), - count(), - ) - .toPromise(); + await harness.executeWithCases([ + async ({ result }) => { + expect(result?.success).toBe(true); + harness.expectFile('dist/main.js').content.not.toContain('abcd1234'); - expect(buildCount).toBe(2); + await harness.modifyFile( + 'src/main.ts', + (content) => content + 'console.log("abcd1234");', + ); + }, + ({ result }) => { + expect(result?.success).toBe(true); + harness.expectFile('dist/main.js').content.toContain('abcd1234'); + }, + ]); }); }); }); diff --git a/packages/angular_devkit/build_angular/src/builders/dev-server/tests/behavior/build_localize_replaced_watch_spec.ts b/packages/angular_devkit/build_angular/src/builders/dev-server/tests/behavior/build_localize_replaced_watch_spec.ts index cc68d0d7a189..ee9e88c039c3 100644 --- a/packages/angular_devkit/build_angular/src/builders/dev-server/tests/behavior/build_localize_replaced_watch_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/dev-server/tests/behavior/build_localize_replaced_watch_spec.ts @@ -7,11 +7,10 @@ */ /* eslint-disable max-len */ -import { concatMap, count, take, timeout } from 'rxjs'; import { URL } from 'node:url'; import { executeDevServer } from '../../index'; import { describeServeBuilder } from '../jasmine-helpers'; -import { BASE_OPTIONS, BUILD_TIMEOUT, DEV_SERVER_BUILDER_INFO } from '../setup'; +import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup'; describeServeBuilder( executeDevServer, @@ -53,31 +52,24 @@ describeServeBuilder( `, ); - const buildCount = await harness - .execute() - .pipe( - timeout(BUILD_TIMEOUT * 2), - concatMap(async ({ result }, index) => { - expect(result?.success).toBe(true); + await harness.executeWithCases([ + async ({ result }) => { + expect(result?.success).toBe(true); - const response = await fetch(new URL('main.js', `${result?.baseUrl}`)); - expect(await response?.text()).not.toContain('$localize`:'); + const response = await fetch(new URL('main.js', `${result?.baseUrl}`)); + expect(await response?.text()).not.toContain('$localize`:'); - switch (index) { - case 0: { - await harness.modifyFile('src/app/app.component.html', (content) => - content.replace('introduction', 'intro'), - ); - break; - } - } - }), - take(2), - count(), - ) - .toPromise(); + await harness.modifyFile('src/app/app.component.html', (content) => + content.replace('introduction', 'intro'), + ); + }, + async ({ result }) => { + expect(result?.success).toBe(true); - expect(buildCount).toBe(2); + const response = await fetch(new URL('main.js', `${result?.baseUrl}`)); + expect(await response?.text()).not.toContain('$localize`:'); + }, + ]); }); }, ); diff --git a/packages/angular_devkit/build_angular/src/builders/dev-server/tests/behavior/build_translation_watch_spec.ts b/packages/angular_devkit/build_angular/src/builders/dev-server/tests/behavior/build_translation_watch_spec.ts index 00c652449db2..0fa3104a4914 100644 --- a/packages/angular_devkit/build_angular/src/builders/dev-server/tests/behavior/build_translation_watch_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/dev-server/tests/behavior/build_translation_watch_spec.ts @@ -7,11 +7,10 @@ */ /* eslint-disable max-len */ -import { concatMap, count, take, timeout } from 'rxjs'; import { URL } from 'node:url'; import { executeDevServer } from '../../index'; import { describeServeBuilder } from '../jasmine-helpers'; -import { BASE_OPTIONS, BUILD_TIMEOUT, DEV_SERVER_BUILDER_INFO } from '../setup'; +import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup'; describeServeBuilder( executeDevServer, @@ -53,38 +52,26 @@ describeServeBuilder( await harness.writeFile('src/locales/messages.fr.xlf', TRANSLATION_FILE_CONTENT); - const buildCount = await harness - .execute() - .pipe( - timeout(BUILD_TIMEOUT), - concatMap(async ({ result }, index) => { - expect(result?.success).toBe(true); + await harness.executeWithCases([ + async ({ result }) => { + expect(result?.success).toBe(true); - const mainUrl = new URL('main.js', `${result?.baseUrl}`); + const mainUrl = new URL('main.js', `${result?.baseUrl}`); + const response = await fetch(mainUrl); + expect(await response?.text()).toContain('Bonjour'); - switch (index) { - case 0: { - const response = await fetch(mainUrl); - expect(await response?.text()).toContain('Bonjour'); - - await harness.modifyFile('src/locales/messages.fr.xlf', (content) => - content.replace('Bonjour', 'Salut'), - ); - break; - } - case 1: { - const response = await fetch(mainUrl); - expect(await response?.text()).toContain('Salut'); - break; - } - } - }), - take(2), - count(), - ) - .toPromise(); + await harness.modifyFile('src/locales/messages.fr.xlf', (content) => + content.replace('Bonjour', 'Salut'), + ); + }, + async ({ result }) => { + expect(result?.success).toBe(true); - expect(buildCount).toBe(2); + const mainUrl = new URL('main.js', `${result?.baseUrl}`); + const response = await fetch(mainUrl); + expect(await response?.text()).toContain('Salut'); + }, + ]); }); }); }, diff --git a/packages/angular_devkit/build_angular/src/builders/dev-server/tests/behavior/serve-live-reload-proxies_spec.ts b/packages/angular_devkit/build_angular/src/builders/dev-server/tests/behavior/serve-live-reload-proxies_spec.ts index 7617e31b45af..c2a1758f2b5e 100644 --- a/packages/angular_devkit/build_angular/src/builders/dev-server/tests/behavior/serve-live-reload-proxies_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/dev-server/tests/behavior/serve-live-reload-proxies_spec.ts @@ -11,11 +11,11 @@ import { tags } from '@angular-devkit/core'; import { createServer } from 'node:http'; import { createProxyServer } from 'http-proxy'; import { AddressInfo } from 'node:net'; +import { setTimeout as setTimeoutPromise } from 'node:timers/promises'; import puppeteer, { Browser, Page } from 'puppeteer'; -import { count, debounceTime, finalize, switchMap, take, timeout } from 'rxjs'; import { executeDevServer } from '../../index'; import { describeServeBuilder } from '../jasmine-helpers'; -import { BASE_OPTIONS, BUILD_TIMEOUT, DEV_SERVER_BUILDER_INFO } from '../setup'; +import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup'; // eslint-disable-next-line @typescript-eslint/no-explicit-any declare const document: any; @@ -190,38 +190,28 @@ describeServeBuilder( await harness.writeFile('src/app/app.component.html', '

{{ title }}

'); - const buildCount = await harness - .execute() - .pipe( - debounceTime(1000), - timeout(BUILD_TIMEOUT * 2), - switchMap(async ({ result }, index) => { - expect(result?.success).toBeTrue(); - if (typeof result?.baseUrl !== 'string') { - throw new Error('Expected "baseUrl" to be a string.'); - } - - switch (index) { - case 0: - await goToPageAndWaitForWS(page, result.baseUrl); - await harness.modifyFile('src/app/app.component.ts', (content) => - content.replace(`'app'`, `'app-live-reload'`), - ); - break; - case 1: - const innerText = await page.evaluate( - () => document.querySelector('p').innerText, - ); - expect(innerText).toBe('app-live-reload'); - break; - } - }), - take(2), - count(), - ) - .toPromise(); - - expect(buildCount).toBe(2); + await harness.executeWithCases([ + async ({ result }) => { + expect(result?.success).toBeTrue(); + if (typeof result?.baseUrl !== 'string') { + throw new Error('Expected "baseUrl" to be a string.'); + } + + await goToPageAndWaitForWS(page, result.baseUrl); + await harness.modifyFile('src/app/app.component.ts', (content) => + content.replace(`'app'`, `'app-live-reload'`), + ); + }, + async ({ result }) => { + expect(result?.success).toBeTrue(); + + // Wait for page to reload. + await setTimeoutPromise(500); + + const innerText = await page.evaluate(() => document.querySelector('p').innerText); + expect(innerText).toBe('app-live-reload'); + }, + ]); }); it('works without http -> http proxy', async () => { @@ -232,42 +222,38 @@ describeServeBuilder( await harness.writeFile('src/app/app.component.html', '

{{ title }}

'); let proxy: ProxyInstance | undefined; - const buildCount = await harness - .execute() - .pipe( - debounceTime(1000), - timeout(BUILD_TIMEOUT * 2), - switchMap(async ({ result }, index) => { - expect(result?.success).toBeTrue(); - if (typeof result?.baseUrl !== 'string') { - throw new Error('Expected "baseUrl" to be a string.'); - } - - switch (index) { - case 0: - proxy = await createProxy(result.baseUrl, false); - await goToPageAndWaitForWS(page, proxy.url); - await harness.modifyFile('src/app/app.component.ts', (content) => - content.replace(`'app'`, `'app-live-reload'`), - ); - break; - case 1: - const innerText = await page.evaluate( - () => document.querySelector('p').innerText, - ); - expect(innerText).toBe('app-live-reload'); - break; - } - }), - take(2), - count(), - finalize(() => { - proxy?.server.close(); - }), - ) - .toPromise(); - - expect(buildCount).toBe(2); + try { + await harness.executeWithCases( + [ + async ({ result }) => { + expect(result?.success).toBeTrue(); + if (typeof result?.baseUrl !== 'string') { + throw new Error('Expected "baseUrl" to be a string.'); + } + + proxy = await createProxy(result.baseUrl, false); + await goToPageAndWaitForWS(page, proxy.url); + await harness.modifyFile('src/app/app.component.ts', (content) => + content.replace(`'app'`, `'app-live-reload'`), + ); + }, + async ({ result }) => { + expect(result?.success).toBeTrue(); + + // Wait for page to reload. + await setTimeoutPromise(500); + + const innerText = await page.evaluate( + () => document.querySelector('p').innerText, + ); + expect(innerText).toBe('app-live-reload'); + }, + ], + { timeout: 50_000 }, + ); + } finally { + proxy?.server.close(); + } }); it('works without https -> http proxy', async () => { @@ -278,42 +264,39 @@ describeServeBuilder( await harness.writeFile('src/app/app.component.html', '

{{ title }}

'); let proxy: ProxyInstance | undefined; - const buildCount = await harness - .execute() - .pipe( - debounceTime(1000), - timeout(BUILD_TIMEOUT * 2), - switchMap(async ({ result }, index) => { - expect(result?.success).toBeTrue(); - if (typeof result?.baseUrl !== 'string') { - throw new Error('Expected "baseUrl" to be a string.'); - } - - switch (index) { - case 0: - proxy = await createProxy(result.baseUrl, true); - await goToPageAndWaitForWS(page, proxy.url); - await harness.modifyFile('src/app/app.component.ts', (content) => - content.replace(`'app'`, `'app-live-reload'`), - ); - break; - case 1: - const innerText = await page.evaluate( - () => document.querySelector('p').innerText, - ); - expect(innerText).toBe('app-live-reload'); - break; - } - }), - take(2), - count(), - finalize(() => { - proxy?.server.close(); - }), - ) - .toPromise(); - - expect(buildCount).toBe(2); + + try { + await harness.executeWithCases( + [ + async ({ result }) => { + expect(result?.success).toBeTrue(); + if (typeof result?.baseUrl !== 'string') { + throw new Error('Expected "baseUrl" to be a string.'); + } + + proxy = await createProxy(result.baseUrl, true); + await goToPageAndWaitForWS(page, proxy.url); + await harness.modifyFile('src/app/app.component.ts', (content) => + content.replace(`'app'`, `'app-live-reload'`), + ); + }, + async ({ result }) => { + expect(result?.success).toBeTrue(); + + // Wait for page to reload. + await setTimeoutPromise(500); + + const innerText = await page.evaluate( + () => document.querySelector('p').innerText, + ); + expect(innerText).toBe('app-live-reload'); + }, + ], + { timeout: 50_000 }, + ); + } finally { + proxy?.server.close(); + } }); }, ); diff --git a/packages/angular_devkit/build_angular/src/builders/dev-server/tests/behavior/serve_service-worker_spec.ts b/packages/angular_devkit/build_angular/src/builders/dev-server/tests/behavior/serve_service-worker_spec.ts index 2d90b2ead76d..556bbef930f5 100644 --- a/packages/angular_devkit/build_angular/src/builders/dev-server/tests/behavior/serve_service-worker_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/dev-server/tests/behavior/serve_service-worker_spec.ts @@ -6,11 +6,10 @@ * found in the LICENSE file at https://angular.dev/license */ -import { concatMap, count, take, timeout } from 'rxjs'; import { executeDevServer } from '../../index'; import { executeOnceAndFetch } from '../execute-fetch'; import { describeServeBuilder } from '../jasmine-helpers'; -import { BASE_OPTIONS, BUILD_TIMEOUT, DEV_SERVER_BUILDER_INFO } from '../setup'; +import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup'; const manifest = { index: '/index.html', @@ -179,48 +178,40 @@ describeServeBuilder( watch: true, }); - const buildCount = await harness - .execute() - .pipe( - timeout(BUILD_TIMEOUT), - concatMap(async ({ result }, index) => { - expect(result?.success).toBeTrue(); - const response = await fetch(new URL('ngsw.json', `${result?.baseUrl}`)); - const { hashTable } = (await response.json()) as { hashTable: object }; - const hashTableEntries = Object.keys(hashTable); - - switch (index) { - case 0: - expect(hashTableEntries).toEqual([ - '/assets/folder-asset.txt', - '/favicon.ico', - '/index.html', - '/media/spectrum.png', - ]); - - await harness.writeFile( - 'src/assets/folder-new-asset.txt', - harness.readFile('src/assets/folder-asset.txt'), - ); - break; - - case 1: - expect(hashTableEntries).toEqual([ - '/assets/folder-asset.txt', - '/assets/folder-new-asset.txt', - '/favicon.ico', - '/index.html', - '/media/spectrum.png', - ]); - break; - } - }), - take(2), - count(), - ) - .toPromise(); - - expect(buildCount).toBe(2); + await harness.executeWithCases([ + async ({ result }) => { + expect(result?.success).toBeTrue(); + const response = await fetch(new URL('ngsw.json', `${result?.baseUrl}`)); + const { hashTable } = (await response.json()) as { hashTable: object }; + const hashTableEntries = Object.keys(hashTable); + + expect(hashTableEntries).toEqual([ + '/assets/folder-asset.txt', + '/favicon.ico', + '/index.html', + '/media/spectrum.png', + ]); + + await harness.writeFile( + 'src/assets/folder-new-asset.txt', + harness.readFile('src/assets/folder-asset.txt'), + ); + }, + async ({ result }) => { + expect(result?.success).toBeTrue(); + const response = await fetch(new URL('ngsw.json', `${result?.baseUrl}`)); + const { hashTable } = (await response.json()) as { hashTable: object }; + const hashTableEntries = Object.keys(hashTable); + + expect(hashTableEntries).toEqual([ + '/assets/folder-asset.txt', + '/assets/folder-new-asset.txt', + '/favicon.ico', + '/index.html', + '/media/spectrum.png', + ]); + }, + ]); }); }); }, diff --git a/packages/angular_devkit/build_angular/src/builders/dev-server/tests/options/watch_spec.ts b/packages/angular_devkit/build_angular/src/builders/dev-server/tests/options/watch_spec.ts index e09ea21a58c5..a22856aaffdf 100644 --- a/packages/angular_devkit/build_angular/src/builders/dev-server/tests/options/watch_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/dev-server/tests/options/watch_spec.ts @@ -6,10 +6,10 @@ * found in the LICENSE file at https://angular.dev/license */ -import { TimeoutError, concatMap, count, take, timeout } from 'rxjs'; +import { TimeoutError } from 'rxjs'; import { executeDevServer } from '../../index'; import { describeServeBuilder } from '../jasmine-helpers'; -import { BASE_OPTIONS, BUILD_TIMEOUT, DEV_SERVER_BUILDER_INFO } from '../setup'; +import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup'; describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupTarget) => { describe('Option: "watch"', () => { @@ -24,32 +24,28 @@ describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupT }); await harness - .execute() - .pipe( - timeout(BUILD_TIMEOUT), - concatMap(async ({ result }, index) => { - expect(result?.success).toBe(true); + .executeWithCases( + [ + async ({ result }) => { + expect(result?.success).toBeTrue(); - switch (index) { - case 0: - await harness.modifyFile( - 'src/main.ts', - (content) => content + 'console.log("abcd1234");', - ); - break; - case 1: - fail('Expected files to not be watched.'); - break; - } - }), - take(2), + await harness.modifyFile( + 'src/main.ts', + (content) => content + 'console.log("abcd1234");', + ); + }, + () => { + fail('Expected files to not be watched.'); + }, + ], + { timeout: 25_000 }, ) - .toPromise() .catch((error) => { // Timeout is expected if watching is disabled if (error instanceof TimeoutError) { return; } + throw error; }); }); @@ -60,30 +56,19 @@ describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupT watch: undefined, }); - const buildCount = await harness - .execute() - .pipe( - timeout(BUILD_TIMEOUT), - concatMap(async ({ result }, index) => { - expect(result?.success).toBe(true); - - switch (index) { - case 0: - await harness.modifyFile( - 'src/main.ts', - (content) => content + 'console.log("abcd1234");', - ); - break; - case 1: - break; - } - }), - take(2), - count(), - ) - .toPromise(); + await harness.executeWithCases([ + async ({ result }) => { + expect(result?.success).toBe(true); - expect(buildCount).toBe(2); + await harness.modifyFile( + 'src/main.ts', + (content) => content + 'console.log("abcd1234");', + ); + }, + ({ result }) => { + expect(result?.success).toBe(true); + }, + ]); }); it('watches for file changes when true', async () => { @@ -92,30 +77,19 @@ describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupT watch: true, }); - const buildCount = await harness - .execute() - .pipe( - timeout(BUILD_TIMEOUT), - concatMap(async ({ result }, index) => { - expect(result?.success).toBe(true); - - switch (index) { - case 0: - await harness.modifyFile( - 'src/main.ts', - (content) => content + 'console.log("abcd1234");', - ); - break; - case 1: - break; - } - }), - take(2), - count(), - ) - .toPromise(); + await harness.executeWithCases([ + async ({ result }) => { + expect(result?.success).toBe(true); - expect(buildCount).toBe(2); + await harness.modifyFile( + 'src/main.ts', + (content) => content + 'console.log("abcd1234");', + ); + }, + ({ result }) => { + expect(result?.success).toBe(true); + }, + ]); }); }); }); diff --git a/packages/angular_devkit/build_angular/src/builders/dev-server/tests/setup.ts b/packages/angular_devkit/build_angular/src/builders/dev-server/tests/setup.ts index 1ca7202347ab..f92d3b713c9f 100644 --- a/packages/angular_devkit/build_angular/src/builders/dev-server/tests/setup.ts +++ b/packages/angular_devkit/build_angular/src/builders/dev-server/tests/setup.ts @@ -67,12 +67,6 @@ export const BASE_OPTIONS = Object.freeze({ watch: false, }); -/** - * Maximum time for single build/rebuild - * This accounts for CI variability. - */ -export const BUILD_TIMEOUT = 25_000; - /** * Cached browser builder option schema */ diff --git a/packages/angular_devkit/build_angular/src/builders/karma/tests/behavior/rebuilds_spec.ts b/packages/angular_devkit/build_angular/src/builders/karma/tests/behavior/rebuilds_spec.ts index e740b7adfcd6..ad9a0b432555 100644 --- a/packages/angular_devkit/build_angular/src/builders/karma/tests/behavior/rebuilds_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/karma/tests/behavior/rebuilds_spec.ts @@ -6,10 +6,8 @@ * found in the LICENSE file at https://angular.dev/license */ -import { concatMap, count, debounceTime, distinctUntilChanged, take, timeout } from 'rxjs'; import { execute } from '../../index'; import { BASE_OPTIONS, KARMA_BUILDER_INFO, describeKarmaBuilder } from '../setup'; -import { BuilderOutput } from '@angular-devkit/architect'; describeKarmaBuilder(execute, KARMA_BUILDER_INFO, (harness, setupTarget) => { describe('Behavior: "Rebuilds"', () => { @@ -25,48 +23,29 @@ describeKarmaBuilder(execute, KARMA_BUILDER_INFO, (harness, setupTarget) => { const goodFile = await harness.readFile('src/app/app.component.spec.ts'); - interface OutputCheck { - (result: BuilderOutput | undefined): Promise; - } - - const expectedSequence: OutputCheck[] = [ - async (result) => { - // Karma run should succeed. - // Add a compilation error. - expect(result?.success).withContext('Initial test run should succeed').toBeTrue(); - // Add an syntax error to a non-main file. - await harness.appendToFile('src/app/app.component.spec.ts', `error`); - }, - async (result) => { - expect(result?.success) - .withContext('Test should fail after build error was introduced') - .toBeFalse(); - await harness.writeFile('src/app/app.component.spec.ts', goodFile); - }, - async (result) => { - expect(result?.success) - .withContext('Test should succeed again after build error was fixed') - .toBeTrue(); - }, - ]; - - const buildCount = await harness - .execute({ outputLogsOnFailure: false }) - .pipe( - timeout(60000), - debounceTime(500), - // There may be a sequence of {success:true} events that should be - // de-duplicated. - distinctUntilChanged((prev, current) => prev.result?.success === current.result?.success), - concatMap(async ({ result }, index) => { - await expectedSequence[index](result); - }), - take(expectedSequence.length), - count(), - ) - .toPromise(); - - expect(buildCount).toBe(expectedSequence.length); + await harness.executeWithCases( + [ + async ({ result }) => { + // Karma run should succeed. + // Add a compilation error. + expect(result?.success).withContext('Initial test run should succeed').toBeTrue(); + // Add an syntax error to a non-main file. + await harness.appendToFile('src/app/app.component.spec.ts', `error`); + }, + async ({ result }) => { + expect(result?.success) + .withContext('Test should fail after build error was introduced') + .toBeFalse(); + await harness.writeFile('src/app/app.component.spec.ts', goodFile); + }, + async ({ result }) => { + expect(result?.success) + .withContext('Test should succeed again after build error was fixed') + .toBeTrue(); + }, + ], + { outputLogsOnFailure: false }, + ); }); }); }); From 541b33f8d977c1fe8f609099a8b8ed1c5f8e827e Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Fri, 11 Jul 2025 09:28:22 +0000 Subject: [PATCH 24/65] fix(@angular/build): emit a warning when `outputHashing` is set to `all` or `bundles` when HMR is enabled These values are incompatible with HMR. Closes #30697 (cherry picked from commit af3b14c4f3336d4e29932fa4753aa0b55c9a2e57) --- .../build/src/builders/dev-server/vite-server.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/angular/build/src/builders/dev-server/vite-server.ts b/packages/angular/build/src/builders/dev-server/vite-server.ts index 7dbafe80f8f4..6f68a37691c6 100644 --- a/packages/angular/build/src/builders/dev-server/vite-server.ts +++ b/packages/angular/build/src/builders/dev-server/vite-server.ts @@ -28,6 +28,7 @@ import { loadProxyConfiguration, normalizeSourceMaps } from '../../utils'; import { useComponentStyleHmr, useComponentTemplateHmr } from '../../utils/environment-options'; import { loadEsmModule } from '../../utils/load-esm'; import { Result, ResultFile, ResultKind } from '../application/results'; +import { OutputHashing } from '../application/schema'; import { type ApplicationBuilderInternalOptions, BuildOutputFileType, @@ -158,6 +159,19 @@ export async function* serveWithVite( process.setSourceMapsEnabled(true); } + if ( + serverOptions.hmr && + (browserOptions.outputHashing === OutputHashing.All || + browserOptions.outputHashing === OutputHashing.Bundles) + ) { + serverOptions.hmr = false; + + context.logger.warn( + `Hot Module Replacement (HMR) is disabled because the 'outputHashing' option is set to '${browserOptions.outputHashing}'. ` + + 'HMR is incompatible with this setting.', + ); + } + const componentsHmrCanBeUsed = browserOptions.aot && serverOptions.liveReload && serverOptions.hmr; From 558a0fe9275e68e0b768de3ee2e5bee0d6d84a6e Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Fri, 11 Jul 2025 06:32:32 +0000 Subject: [PATCH 25/65] fix(@angular/build): normalize code coverage include paths to POSIX Ensures that code coverage `include` patterns are converted to a POSIX-style format. Closes #30698 (cherry picked from commit cad16426435f0e166aa09ead2fae894e43d1927b) --- .../build/src/builders/unit-test/builder.ts | 14 ++++--- packages/angular/build/src/utils/path.ts | 37 +++++++++++++++++++ 2 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 packages/angular/build/src/utils/path.ts diff --git a/packages/angular/build/src/builders/unit-test/builder.ts b/packages/angular/build/src/builders/unit-test/builder.ts index fd9f88580d70..c62310e0798d 100644 --- a/packages/angular/build/src/builders/unit-test/builder.ts +++ b/packages/angular/build/src/builders/unit-test/builder.ts @@ -14,6 +14,7 @@ import path from 'node:path'; import { createVirtualModulePlugin } from '../../tools/esbuild/virtual-module-plugin'; import { assertIsError } from '../../utils/error'; import { loadEsmModule } from '../../utils/load-esm'; +import { toPosixPath } from '../../utils/path'; import { buildApplicationInternal } from '../application'; import type { ApplicationBuilderExtensions, @@ -117,7 +118,7 @@ export async function* execute( buildTargetOptions.polyfills = injectTestingPolyfills(buildTargetOptions.polyfills); - const outputPath = path.join(context.workspaceRoot, generateOutputPath()); + const outputPath = toPosixPath(path.join(context.workspaceRoot, generateOutputPath())); const buildOptions: ApplicationBuilderInternalOptions = { ...buildTargetOptions, watch: normalizedOptions.watch, @@ -156,10 +157,11 @@ export async function* execute( `import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';`, '', normalizedOptions.providersFile - ? `import providers from './${path - .relative(projectSourceRoot, normalizedOptions.providersFile) - .replace(/.[mc]?ts$/, '') - .replace(/\\/g, '/')}'` + ? `import providers from './${toPosixPath( + path + .relative(projectSourceRoot, normalizedOptions.providersFile) + .replace(/.[mc]?ts$/, ''), + )}'` : 'const providers = [];', '', // Same as https://github.com/angular/angular/blob/05a03d3f975771bb59c7eefd37c01fa127ee2229/packages/core/testing/src/test_hooks.ts#L21-L29 @@ -406,7 +408,7 @@ function generateCoverageOption( return { enabled: true, excludeAfterRemap: true, - include: [`${path.relative(workspaceRoot, outputPath)}/**`], + include: [`${toPosixPath(path.relative(workspaceRoot, outputPath))}/**`], // Special handling for `reporter` due to an undefined value causing upstream failures ...(codeCoverage.reporters ? ({ reporter: codeCoverage.reporters } satisfies VitestCoverageOption) diff --git a/packages/angular/build/src/utils/path.ts b/packages/angular/build/src/utils/path.ts new file mode 100644 index 000000000000..036dcb23502e --- /dev/null +++ b/packages/angular/build/src/utils/path.ts @@ -0,0 +1,37 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { posix } from 'node:path'; +import { platform } from 'node:process'; + +const WINDOWS_PATH_SEPERATOR_REGEXP = /\\/g; + +/** + * Converts a Windows-style file path to a POSIX-compliant path. + * + * This function replaces all backslashes (`\`) with forward slashes (`/`). + * It is a no-op on POSIX systems (e.g., Linux, macOS), as the conversion + * only runs on Windows (`win32`). + * + * @param path - The file path to convert. + * @returns The POSIX-compliant file path. + * + * @example + * ```ts + * // On a Windows system: + * toPosixPath('C:\\Users\\Test\\file.txt'); + * // => 'C:/Users/Test/file.txt' + * + * // On a POSIX system (Linux/macOS): + * toPosixPath('/home/user/file.txt'); + * // => '/home/user/file.txt' + * ``` + */ +export function toPosixPath(path: string): string { + return platform === 'win32' ? path.replace(WINDOWS_PATH_SEPERATOR_REGEXP, posix.sep) : path; +} From 1602181d2feef4af7eb7ee2d95a2c42b0abb347d Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Fri, 11 Jul 2025 06:39:05 +0000 Subject: [PATCH 26/65] refactor(@angular/build): use the `toPosixPath` util to convert windows file Use the shared util instead of duplicated the code. (cherry picked from commit 199c12c60189af12c78ba3f5e3f69fea781dfccb) --- .../build/src/builders/application/build-action.ts | 3 ++- .../angular/build/src/builders/karma/find-tests.ts | 9 ++++----- .../src/tools/esbuild/application-code-bundle.ts | 12 +++--------- .../angular/build/src/tools/esbuild/global-styles.ts | 3 ++- .../build/src/tools/sass/rebasing-importer.ts | 3 ++- .../build/src/utils/server-rendering/prerender.ts | 3 ++- packages/angular/build/src/utils/service-worker.ts | 7 ++++--- 7 files changed, 19 insertions(+), 21 deletions(-) diff --git a/packages/angular/build/src/builders/application/build-action.ts b/packages/angular/build/src/builders/application/build-action.ts index c59863f0ebf5..afc59785be7d 100644 --- a/packages/angular/build/src/builders/application/build-action.ts +++ b/packages/angular/build/src/builders/application/build-action.ts @@ -16,6 +16,7 @@ import { logMessages, withNoProgress, withSpinner } from '../../tools/esbuild/ut import { ChangedFiles } from '../../tools/esbuild/watcher'; import { shouldWatchRoot } from '../../utils/environment-options'; import { NormalizedCachedOptions } from '../../utils/normalize-cache'; +import { toPosixPath } from '../../utils/path'; import { NormalizedApplicationBuildOptions, NormalizedOutputOptions } from './options'; import { ComponentUpdateResult, @@ -105,7 +106,7 @@ export async function* runEsBuildBuildAction( // Ignore the output and cache paths to avoid infinite rebuild cycles outputOptions.base, cacheOptions.basePath, - `${workspaceRoot.replace(/\\/g, '/')}/**/.*/**`, + `${toPosixPath(workspaceRoot)}/**/.*/**`, ]; // Setup a watcher diff --git a/packages/angular/build/src/builders/karma/find-tests.ts b/packages/angular/build/src/builders/karma/find-tests.ts index ec25d56cf9d2..67ae410c6125 100644 --- a/packages/angular/build/src/builders/karma/find-tests.ts +++ b/packages/angular/build/src/builders/karma/find-tests.ts @@ -9,6 +9,7 @@ import { PathLike, constants, promises as fs } from 'node:fs'; import { basename, dirname, extname, join, relative } from 'node:path'; import { glob, isDynamicPattern } from 'tinyglobby'; +import { toPosixPath } from '../../utils/path'; /* Go through all patterns and find unique list of files */ export async function findTests( @@ -59,8 +60,6 @@ export function getTestEntrypoints( ); } -const normalizePath = (path: string): string => path.replace(/\\/g, '/'); - const removeLeadingSlash = (pattern: string): string => { if (pattern.charAt(0) === '/') { return pattern.substring(1); @@ -94,10 +93,10 @@ async function findMatchingTests( projectSourceRoot: string, ): Promise { // normalize pattern, glob lib only accepts forward slashes - let normalizedPattern = normalizePath(pattern); + let normalizedPattern = toPosixPath(pattern); normalizedPattern = removeLeadingSlash(normalizedPattern); - const relativeProjectRoot = normalizePath(relative(workspaceRoot, projectSourceRoot) + '/'); + const relativeProjectRoot = toPosixPath(relative(workspaceRoot, projectSourceRoot) + '/'); // remove relativeProjectRoot to support relative paths from root // such paths are easy to get when running scripts via IDEs @@ -125,7 +124,7 @@ async function findMatchingTests( // normalize the patterns in the ignore list const normalizedIgnorePatternList = ignore.map((pattern: string) => - removeRelativeRoot(removeLeadingSlash(normalizePath(pattern)), relativeProjectRoot), + removeRelativeRoot(removeLeadingSlash(toPosixPath(pattern)), relativeProjectRoot), ); return glob(normalizedPattern, { diff --git a/packages/angular/build/src/tools/esbuild/application-code-bundle.ts b/packages/angular/build/src/tools/esbuild/application-code-bundle.ts index c5d18d67228d..b17029f6c5e1 100644 --- a/packages/angular/build/src/tools/esbuild/application-code-bundle.ts +++ b/packages/angular/build/src/tools/esbuild/application-code-bundle.ts @@ -13,6 +13,7 @@ import { extname, relative } from 'node:path'; import type { NormalizedApplicationBuildOptions } from '../../builders/application/options'; import { ExperimentalPlatform } from '../../builders/application/schema'; import { allowMangle } from '../../utils/environment-options'; +import { toPosixPath } from '../../utils/path'; import { SERVER_APP_ENGINE_MANIFEST_FILENAME, SERVER_APP_MANIFEST_FILENAME, @@ -719,9 +720,7 @@ function getEsBuildCommonPolyfillsOptions( } // Generate module contents with an import statement per defined polyfill - let contents = polyfillPaths - .map((file) => `import '${file.replace(/\\/g, '/')}';`) - .join('\n'); + let contents = polyfillPaths.map((file) => `import '${toPosixPath(file)}';`).join('\n'); // The below should be done after loading `$localize` as otherwise the locale will be overridden. if (i18nOptions.shouldInline) { @@ -746,10 +745,5 @@ function getEsBuildCommonPolyfillsOptions( } function entryFileToWorkspaceRelative(workspaceRoot: string, entryFile: string): string { - return ( - './' + - relative(workspaceRoot, entryFile) - .replace(/.[mc]?ts$/, '') - .replace(/\\/g, '/') - ); + return './' + toPosixPath(relative(workspaceRoot, entryFile).replace(/.[mc]?ts$/, '')); } diff --git a/packages/angular/build/src/tools/esbuild/global-styles.ts b/packages/angular/build/src/tools/esbuild/global-styles.ts index 682885c43350..fd2cb13fa7b2 100644 --- a/packages/angular/build/src/tools/esbuild/global-styles.ts +++ b/packages/angular/build/src/tools/esbuild/global-styles.ts @@ -8,6 +8,7 @@ import assert from 'node:assert'; import { NormalizedApplicationBuildOptions } from '../../builders/application/options'; +import { toPosixPath } from '../../utils/path'; import { BundlerOptionsFactory } from './bundler-context'; import { createStylesheetBundleOptions } from './stylesheets/bundle-options'; import { createVirtualModulePlugin } from './virtual-module-plugin'; @@ -91,7 +92,7 @@ export function createGlobalStylesBundleOptions( assert(files, `global style name should always be found [${args.path}]`); return { - contents: files.map((file) => `@import '${file.replace(/\\/g, '/')}';`).join('\n'), + contents: files.map((file) => `@import '${toPosixPath(file)}';`).join('\n'), loader: 'css', resolveDir: workspaceRoot, }; diff --git a/packages/angular/build/src/tools/sass/rebasing-importer.ts b/packages/angular/build/src/tools/sass/rebasing-importer.ts index d5ade8b6cf54..15c94a25aeef 100644 --- a/packages/angular/build/src/tools/sass/rebasing-importer.ts +++ b/packages/angular/build/src/tools/sass/rebasing-importer.ts @@ -13,6 +13,7 @@ import { basename, dirname, extname, join, relative } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import type { CanonicalizeContext, Importer, ImporterResult, Syntax } from 'sass'; import { assertIsError } from '../../utils/error'; +import { toPosixPath } from '../../utils/path'; import { findUrls } from './lexer'; /** @@ -83,7 +84,7 @@ abstract class UrlRebasingImporter implements Importer<'sync'> { // Normalize path separators and escape characters // https://developer.mozilla.org/en-US/docs/Web/CSS/url#syntax - const rebasedUrl = rebasedPath.replace(/\\/g, '/').replace(/[()\s'"]/g, '\\$&'); + const rebasedUrl = toPosixPath(rebasedPath).replace(/[()\s'"]/g, '\\$&'); updatedContents ??= new MagicString(contents); // Always quote the URL to avoid potential downstream parsing problems diff --git a/packages/angular/build/src/utils/server-rendering/prerender.ts b/packages/angular/build/src/utils/server-rendering/prerender.ts index a8a42c7c941a..e087262a7f0c 100644 --- a/packages/angular/build/src/utils/server-rendering/prerender.ts +++ b/packages/angular/build/src/utils/server-rendering/prerender.ts @@ -13,6 +13,7 @@ import { OutputMode } from '../../builders/application/schema'; import { BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundler-context'; import { BuildOutputAsset } from '../../tools/esbuild/bundler-execution-result'; import { assertIsError } from '../error'; +import { toPosixPath } from '../path'; import { urlJoin } from '../url'; import { WorkerPool } from '../worker-pool'; import { IMPORT_EXEC_ARGV } from './esm-in-memory-loader/utils'; @@ -94,7 +95,7 @@ export async function prerenderPages( const assetsReversed: Record = {}; for (const { source, destination } of assets) { - assetsReversed[addLeadingSlash(destination.replace(/\\/g, posix.sep))] = source; + assetsReversed[addLeadingSlash(toPosixPath(destination))] = source; } // Get routes to prerender diff --git a/packages/angular/build/src/utils/service-worker.ts b/packages/angular/build/src/utils/service-worker.ts index f9d5c14d27fd..c6f95f99a595 100644 --- a/packages/angular/build/src/utils/service-worker.ts +++ b/packages/angular/build/src/utils/service-worker.ts @@ -14,6 +14,7 @@ import { BuildOutputFile, BuildOutputFileType } from '../tools/esbuild/bundler-c import { BuildOutputAsset } from '../tools/esbuild/bundler-execution-result'; import { assertIsError } from './error'; import { loadEsmModule } from './load-esm'; +import { toPosixPath } from './path'; class CliFilesystem implements Filesystem { constructor( @@ -52,7 +53,7 @@ class CliFilesystem implements Filesystem { if (stats.isFile()) { // Uses posix paths since the service worker expects URLs - items.push('/' + path.relative(this.base, entryPath).replace(/\\/g, '/')); + items.push('/' + toPosixPath(path.relative(this.base, entryPath))); } else if (stats.isDirectory()) { subdirectories.push(entryPath); } @@ -75,11 +76,11 @@ class ResultFilesystem implements Filesystem { ) { for (const file of outputFiles) { if (file.type === BuildOutputFileType.Media || file.type === BuildOutputFileType.Browser) { - this.fileReaders.set('/' + file.path.replace(/\\/g, '/'), async () => file.contents); + this.fileReaders.set('/' + toPosixPath(file.path), async () => file.contents); } } for (const file of assetFiles) { - this.fileReaders.set('/' + file.destination.replace(/\\/g, '/'), () => + this.fileReaders.set('/' + toPosixPath(file.destination), () => fsPromises.readFile(file.source), ); } From eb29941361ff7f133ab297a1345d7b3d90b734e4 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 9 Jul 2025 16:05:07 -0400 Subject: [PATCH 27/65] refactor(@angular/cli): add a documentation search tool to MCP server An additional MCP tool is now available with the `ng mcp` stdio MCP server that supports querying the `angular.dev` documentation. This uses the same algolia based search indexing that the documentation website uses. Rate limiting has been implemented with the MCP tool that may be adjusted based on feedback. The tool returns one or more URLs and titles for relevant documentation for a given query. Content of these search results are currently not fetched but rather this action is deferred to the host to determine which items are most relevant and should be retrieved from the documentation website. (cherry picked from commit 16ae6a1639c425ffd7c353a43efc2b472f024b33) --- packages/angular/cli/BUILD.bazel | 1 + packages/angular/cli/package.json | 1 + .../angular/cli/src/commands/mcp/constants.ts | 13 ++ .../cli/src/commands/mcp/mcp-server.ts | 3 + .../cli/src/commands/mcp/tools/doc-search.ts | 123 ++++++++++++++ pnpm-lock.yaml | 152 ++++++++++++++++++ 6 files changed, 293 insertions(+) create mode 100644 packages/angular/cli/src/commands/mcp/constants.ts create mode 100644 packages/angular/cli/src/commands/mcp/tools/doc-search.ts diff --git a/packages/angular/cli/BUILD.bazel b/packages/angular/cli/BUILD.bazel index a25061997acf..2eacbb6b4ebf 100644 --- a/packages/angular/cli/BUILD.bazel +++ b/packages/angular/cli/BUILD.bazel @@ -51,6 +51,7 @@ ts_project( ":node_modules/@listr2/prompt-adapter-inquirer", ":node_modules/@modelcontextprotocol/sdk", ":node_modules/@yarnpkg/lockfile", + ":node_modules/algoliasearch", ":node_modules/ini", ":node_modules/jsonc-parser", ":node_modules/npm-package-arg", diff --git a/packages/angular/cli/package.json b/packages/angular/cli/package.json index 6b58a139b964..a7816b7275f0 100644 --- a/packages/angular/cli/package.json +++ b/packages/angular/cli/package.json @@ -30,6 +30,7 @@ "@modelcontextprotocol/sdk": "1.13.3", "@schematics/angular": "workspace:0.0.0-PLACEHOLDER", "@yarnpkg/lockfile": "1.1.0", + "algoliasearch": "5.32.0", "ini": "5.0.0", "jsonc-parser": "3.3.1", "listr2": "8.3.3", diff --git a/packages/angular/cli/src/commands/mcp/constants.ts b/packages/angular/cli/src/commands/mcp/constants.ts new file mode 100644 index 000000000000..6530bfd34175 --- /dev/null +++ b/packages/angular/cli/src/commands/mcp/constants.ts @@ -0,0 +1,13 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +export const k1 = '@angular/cli'; +export const at = 'QBHBbOdEO4CmBOC2d7jNmg=='; +export const iv = Buffer.from([ + 0x97, 0xf4, 0x62, 0x95, 0x3e, 0x12, 0x76, 0x84, 0x8a, 0x09, 0x4a, 0xc9, 0xeb, 0xa2, 0x84, 0x69, +]); diff --git a/packages/angular/cli/src/commands/mcp/mcp-server.ts b/packages/angular/cli/src/commands/mcp/mcp-server.ts index 81a11ac6c94a..13ba22fbc688 100644 --- a/packages/angular/cli/src/commands/mcp/mcp-server.ts +++ b/packages/angular/cli/src/commands/mcp/mcp-server.ts @@ -12,6 +12,7 @@ import path from 'node:path'; import { z } from 'zod'; import type { AngularWorkspace } from '../../utilities/config'; import { VERSION } from '../../utilities/version'; +import { registerDocSearchTool } from './tools/doc-search'; export async function createMcpServer(context: { workspace?: AngularWorkspace; @@ -129,5 +130,7 @@ export async function createMcpServer(context: { }, ); + await registerDocSearchTool(server); + return server; } diff --git a/packages/angular/cli/src/commands/mcp/tools/doc-search.ts b/packages/angular/cli/src/commands/mcp/tools/doc-search.ts new file mode 100644 index 000000000000..5d7a682eb36f --- /dev/null +++ b/packages/angular/cli/src/commands/mcp/tools/doc-search.ts @@ -0,0 +1,123 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { LegacySearchMethodProps, SearchResponse } from 'algoliasearch'; +import { createDecipheriv } from 'node:crypto'; +import { z } from 'zod'; +import { at, iv, k1 } from '../constants'; + +const ALGOLIA_APP_ID = 'L1XWT2UJ7F'; +// https://www.algolia.com/doc/guides/security/api-keys/#search-only-api-key +// This is a search only, rate limited key. It is sent within the URL of the query request. +// This is not the actual key. +const ALGOLIA_API_E = '322d89dab5f2080fe09b795c93413c6a89222b13a447cdf3e6486d692717bc0c'; + +/** + * Registers a tool with the MCP server to search the Angular documentation. + * + * This tool uses Algolia to search the official Angular documentation. + * + * @param server The MCP server instance with which to register the tool. + */ +export async function registerDocSearchTool(server: McpServer): Promise { + let client: import('algoliasearch').SearchClient | undefined; + + server.registerTool( + 'search_documentation', + { + title: 'Search Angular Documentation (angular.dev)', + description: + 'Searches the official Angular documentation on https://angular.dev.' + + ' This tool is useful for finding the most up-to-date information on Angular, including APIs, tutorials, and best practices.' + + ' Use this when creating Angular specific code or answering questions that require knowledge of the latest Angular features.', + annotations: { + readOnlyHint: true, + }, + inputSchema: { + query: z + .string() + .describe( + 'The search query to use when searching the Angular documentation.' + + ' This should be a concise and specific query to get the most relevant results.', + ), + }, + }, + async ({ query }) => { + if (!client) { + const dcip = createDecipheriv( + 'aes-256-gcm', + (k1 + ALGOLIA_APP_ID).padEnd(32, '^'), + iv, + ).setAuthTag(Buffer.from(at, 'base64')); + const { searchClient } = await import('algoliasearch'); + client = searchClient( + ALGOLIA_APP_ID, + dcip.update(ALGOLIA_API_E, 'hex', 'utf-8') + dcip.final('utf-8'), + ); + } + + const { results } = await client.search(createSearchArguments(query)); + + // Convert results into text content entries instead of stringifying the entire object + const content = results.flatMap((result) => + (result as SearchResponse).hits.map((hit) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const hierarchy = Object.values(hit.hierarchy as any).filter( + (x) => typeof x === 'string', + ); + const title = hierarchy.pop(); + const description = hierarchy.join(' > '); + + return { + type: 'text' as const, + text: `## ${title}\n${description}\nURL: ${hit.url}`, + }; + }), + ); + + return { content }; + }, + ); +} + +/** + * Creates the search arguments for an Algolia search. + * + * The arguments are based on the search implementation in `adev`. + * + * @param query The search query string. + * @returns The search arguments for the Algolia client. + */ +function createSearchArguments(query: string): LegacySearchMethodProps { + // Search arguments are based on adev's search service: + // https://github.com/angular/angular/blob/4b614fbb3263d344dbb1b18fff24cb09c5a7582d/adev/shared-docs/services/search.service.ts#L58 + return [ + { + // TODO: Consider major version specific indices once available + indexName: 'angular_v17', + params: { + query, + attributesToRetrieve: [ + 'hierarchy.lvl0', + 'hierarchy.lvl1', + 'hierarchy.lvl2', + 'hierarchy.lvl3', + 'hierarchy.lvl4', + 'hierarchy.lvl5', + 'hierarchy.lvl6', + 'content', + 'type', + 'url', + ], + hitsPerPage: 10, + }, + type: 'default', + }, + ]; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bb584e2d4bdc..e66d7e582460 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -480,6 +480,9 @@ importers: '@yarnpkg/lockfile': specifier: 1.1.0 version: 1.1.0 + algoliasearch: + specifier: 5.32.0 + version: 5.32.0 ini: specifier: 5.0.0 version: 5.0.0 @@ -903,6 +906,58 @@ importers: packages: + '@algolia/client-abtesting@5.32.0': + resolution: {integrity: sha512-HG/6Eib6DnJYm/B2ijWFXr4txca/YOuA4K7AsEU0JBrOZSB+RU7oeDyNBPi3c0v0UDDqlkBqM3vBU/auwZlglA==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-analytics@5.32.0': + resolution: {integrity: sha512-8Y9MLU72WFQOW3HArYv16+Wvm6eGmsqbxxM1qxtm0hvSASJbxCm+zQAZe5stqysTlcWo4BJ82KEH1PfgHbJAmQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-common@5.32.0': + resolution: {integrity: sha512-w8L+rgyXMCPBKmEdOT+RfgMrF0mT6HK60vPYWLz8DBs/P7yFdGo7urn99XCJvVLMSKXrIbZ2FMZ/i50nZTXnuQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-insights@5.32.0': + resolution: {integrity: sha512-AdWfynhUeX7jz/LTiFU3wwzJembTbdLkQIOLs4n7PyBuxZ3jz4azV1CWbIP8AjUOFmul6uXbmYza+KqyS5CzOA==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-personalization@5.32.0': + resolution: {integrity: sha512-bTupJY4xzGZYI4cEQcPlSjjIEzMvv80h7zXGrXY1Y0KC/n/SLiMv84v7Uy+B6AG1Kiy9FQm2ADChBLo1uEhGtQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-query-suggestions@5.32.0': + resolution: {integrity: sha512-if+YTJw1G3nDKL2omSBjQltCHUQzbaHADkcPQrGFnIGhVyHU3Dzq4g46uEv8mrL5sxL8FjiS9LvekeUlL2NRqw==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-search@5.32.0': + resolution: {integrity: sha512-kmK5nVkKb4DSUgwbveMKe4X3xHdMsPsOVJeEzBvFJ+oS7CkBPmpfHAEq+CcmiPJs20YMv6yVtUT9yPWL5WgAhg==} + engines: {node: '>= 14.0.0'} + + '@algolia/ingestion@1.32.0': + resolution: {integrity: sha512-PZTqjJbx+fmPuT2ud1n4vYDSF1yrT//vOGI9HNYKNA0PM0xGUBWigf5gRivHsXa3oBnUlTyHV9j7Kqx5BHbVHQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/monitoring@1.32.0': + resolution: {integrity: sha512-kYYoOGjvNQAmHDS1v5sBj+0uEL9RzYqH/TAdq8wmcV+/22weKt/fjh+6LfiqkS1SCZFYYrwGnirrUhUM36lBIQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/recommend@5.32.0': + resolution: {integrity: sha512-jyIBLdskjPAL7T1g57UMfUNx+PzvYbxKslwRUKBrBA6sNEsYCFdxJAtZSLUMmw6MC98RDt4ksmEl5zVMT5bsuw==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-browser-xhr@5.32.0': + resolution: {integrity: sha512-eDp14z92Gt6JlFgiexImcWWH+Lk07s/FtxcoDaGrE4UVBgpwqOO6AfQM6dXh1pvHxlDFbMJihHc/vj3gBhPjqQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-fetch@5.32.0': + resolution: {integrity: sha512-rnWVglh/K75hnaLbwSc2t7gCkbq1ldbPgeIKDUiEJxZ4mlguFgcltWjzpDQ/t1LQgxk9HdIFcQfM17Hid3aQ6Q==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-node-http@5.32.0': + resolution: {integrity: sha512-LbzQ04+VLkzXY4LuOzgyjqEv/46Gwrk55PldaglMJ4i4eDXSRXGKkwJpXFwsoU+c1HMQlHIyjJBhrfsfdyRmyQ==} + engines: {node: '>= 14.0.0'} + '@ampproject/remapping@2.3.0': resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} @@ -3376,6 +3431,10 @@ packages: ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + algoliasearch@5.32.0: + resolution: {integrity: sha512-84xBncKNPBK8Ae89F65+SyVcOihrIbm/3N7to+GpRBHEUXGjA3ydWTMpcRW6jmFzkBQ/eqYy/y+J+NBpJWYjBg==} + engines: {node: '>= 14.0.0'} + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -8371,6 +8430,83 @@ packages: snapshots: + '@algolia/client-abtesting@5.32.0': + dependencies: + '@algolia/client-common': 5.32.0 + '@algolia/requester-browser-xhr': 5.32.0 + '@algolia/requester-fetch': 5.32.0 + '@algolia/requester-node-http': 5.32.0 + + '@algolia/client-analytics@5.32.0': + dependencies: + '@algolia/client-common': 5.32.0 + '@algolia/requester-browser-xhr': 5.32.0 + '@algolia/requester-fetch': 5.32.0 + '@algolia/requester-node-http': 5.32.0 + + '@algolia/client-common@5.32.0': {} + + '@algolia/client-insights@5.32.0': + dependencies: + '@algolia/client-common': 5.32.0 + '@algolia/requester-browser-xhr': 5.32.0 + '@algolia/requester-fetch': 5.32.0 + '@algolia/requester-node-http': 5.32.0 + + '@algolia/client-personalization@5.32.0': + dependencies: + '@algolia/client-common': 5.32.0 + '@algolia/requester-browser-xhr': 5.32.0 + '@algolia/requester-fetch': 5.32.0 + '@algolia/requester-node-http': 5.32.0 + + '@algolia/client-query-suggestions@5.32.0': + dependencies: + '@algolia/client-common': 5.32.0 + '@algolia/requester-browser-xhr': 5.32.0 + '@algolia/requester-fetch': 5.32.0 + '@algolia/requester-node-http': 5.32.0 + + '@algolia/client-search@5.32.0': + dependencies: + '@algolia/client-common': 5.32.0 + '@algolia/requester-browser-xhr': 5.32.0 + '@algolia/requester-fetch': 5.32.0 + '@algolia/requester-node-http': 5.32.0 + + '@algolia/ingestion@1.32.0': + dependencies: + '@algolia/client-common': 5.32.0 + '@algolia/requester-browser-xhr': 5.32.0 + '@algolia/requester-fetch': 5.32.0 + '@algolia/requester-node-http': 5.32.0 + + '@algolia/monitoring@1.32.0': + dependencies: + '@algolia/client-common': 5.32.0 + '@algolia/requester-browser-xhr': 5.32.0 + '@algolia/requester-fetch': 5.32.0 + '@algolia/requester-node-http': 5.32.0 + + '@algolia/recommend@5.32.0': + dependencies: + '@algolia/client-common': 5.32.0 + '@algolia/requester-browser-xhr': 5.32.0 + '@algolia/requester-fetch': 5.32.0 + '@algolia/requester-node-http': 5.32.0 + + '@algolia/requester-browser-xhr@5.32.0': + dependencies: + '@algolia/client-common': 5.32.0 + + '@algolia/requester-fetch@5.32.0': + dependencies: + '@algolia/client-common': 5.32.0 + + '@algolia/requester-node-http@5.32.0': + dependencies: + '@algolia/client-common': 5.32.0 + '@ampproject/remapping@2.3.0': dependencies: '@jridgewell/gen-mapping': 0.3.8 @@ -11271,6 +11407,22 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + algoliasearch@5.32.0: + dependencies: + '@algolia/client-abtesting': 5.32.0 + '@algolia/client-analytics': 5.32.0 + '@algolia/client-common': 5.32.0 + '@algolia/client-insights': 5.32.0 + '@algolia/client-personalization': 5.32.0 + '@algolia/client-query-suggestions': 5.32.0 + '@algolia/client-search': 5.32.0 + '@algolia/ingestion': 1.32.0 + '@algolia/monitoring': 1.32.0 + '@algolia/recommend': 5.32.0 + '@algolia/requester-browser-xhr': 5.32.0 + '@algolia/requester-fetch': 5.32.0 + '@algolia/requester-node-http': 5.32.0 + ansi-colors@4.1.3: {} ansi-escapes@4.3.2: From 3ccd1e8d269824840ab3010eb38637a30b5b56be Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 16 Jul 2025 10:28:51 -0400 Subject: [PATCH 28/65] refactor(@angular/build): provide private factory API for internal compilation infrastructure The `createAngularCompilation` factory function is now available from the `private` entry point for the `@angular/build` package. All items exposed from this entry point are not subject to SemVer guarantees and may change between releases. (cherry picked from commit 3faa3b4648329d8ca067af91c6d318783de7bb82) --- packages/angular/build/src/private.ts | 3 +++ .../angular/build/src/tools/angular/compilation/factory.ts | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/angular/build/src/private.ts b/packages/angular/build/src/private.ts index 29e98bf531aa..950a54797250 100644 --- a/packages/angular/build/src/private.ts +++ b/packages/angular/build/src/private.ts @@ -58,6 +58,9 @@ export function createCompilerPlugin( ); } +export type { AngularCompilation } from './tools/angular/compilation'; +export { createAngularCompilation }; + // Utilities export * from './utils/bundle-calculator'; export { checkPort } from './utils/check-port'; diff --git a/packages/angular/build/src/tools/angular/compilation/factory.ts b/packages/angular/build/src/tools/angular/compilation/factory.ts index 91447dea24cf..ebfa7aa7edc4 100644 --- a/packages/angular/build/src/tools/angular/compilation/factory.ts +++ b/packages/angular/build/src/tools/angular/compilation/factory.ts @@ -20,8 +20,9 @@ import type { AngularCompilation } from './angular-compilation'; export async function createAngularCompilation( jit: boolean, browserOnlyBuild: boolean, + parallel: boolean = useParallelTs, ): Promise { - if (useParallelTs) { + if (parallel) { const { ParallelCompilation } = await import('./parallel-compilation'); return new ParallelCompilation(jit, browserOnlyBuild); From b7121e8432d3fa961e5116d5e8d6dd13c24e3002 Mon Sep 17 00:00:00 2001 From: Angular Robot Date: Wed, 16 Jul 2025 05:08:18 +0000 Subject: [PATCH 29/65] build: update dependency node to v22.17.1 See associated pull request for more information. (cherry picked from commit 671e0f82fad528ee1df48769a45b629b2e38e169) --- .nvmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nvmrc b/.nvmrc index fc37597bccdb..7377d130eda5 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22.17.0 +22.17.1 From 0925ebd7e79a83e6675579486870ca282044f049 Mon Sep 17 00:00:00 2001 From: Matthieu Riegler Date: Wed, 16 Jul 2025 02:28:21 +0200 Subject: [PATCH 30/65] refactor(@schematics/angular): fix layout of pill button In the case of a long project name, the button would wrap. This commit prevents that. (cherry picked from commit 3308c3dc7efb213b2e4c20f06d76217cf6d2198f) --- .../application/files/common-files/src/app/app.html.template | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/schematics/angular/application/files/common-files/src/app/app.html.template b/packages/schematics/angular/application/files/common-files/src/app/app.html.template index 84990f7afef2..b706f5bff17e 100644 --- a/packages/schematics/angular/application/files/common-files/src/app/app.html.template +++ b/packages/schematics/angular/application/files/common-files/src/app/app.html.template @@ -124,6 +124,7 @@ line-height: 1.4rem; letter-spacing: -0.00875rem; text-decoration: none; + white-space: nowrap; } .pill:hover { From 15c0148ccdd6b98611838d8b524c8560b42826df Mon Sep 17 00:00:00 2001 From: Jan Martin Date: Wed, 16 Jul 2025 14:10:05 -0700 Subject: [PATCH 31/65] release: cut the v20.1.1 release --- CHANGELOG.md | 14 ++++++++++++++ package.json | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 190c379edf2b..f0fd17070983 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ + + +# 20.1.1 (2025-07-16) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------ | +| [541b33f8d](https://github.com/angular/angular-cli/commit/541b33f8d977c1fe8f609099a8b8ed1c5f8e827e) | fix | emit a warning when `outputHashing` is set to `all` or `bundles` when HMR is enabled | +| [558a0fe92](https://github.com/angular/angular-cli/commit/558a0fe9275e68e0b768de3ee2e5bee0d6d84a6e) | fix | normalize code coverage include paths to POSIX | + + + # 20.1.0 (2025-07-09) @@ -4056,6 +4069,7 @@ Alan Agius, Charles Lyding, Doug Parker, Joey Perrott and Piotr Wysocki ```scss @import 'font-awesome/scss/font-awesome'; ``` + - By default the CLI will use Sass modern API, While not recommended, users can still opt to use legacy API by setting `NG_BUILD_LEGACY_SASS=1`. - Internally the Angular CLI now always set the TypeScript `target` to `ES2022` and `useDefineForClassFields` to `false` unless the target is set to `ES2022` or later in the TypeScript configuration. To control ECMA version and features use the Browerslist configuration. diff --git a/package.json b/package.json index 7684d6ac7981..f90330ba70f7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@angular/devkit-repo", - "version": "20.1.0", + "version": "20.1.1", "private": true, "description": "Software Development Kit for Angular", "keywords": [ From 9dfdaf58cd493be394d48f66f758486a409312e2 Mon Sep 17 00:00:00 2001 From: Jan Olaf Martin Date: Wed, 16 Jul 2025 14:44:12 -0700 Subject: [PATCH 32/65] test: use valid theme for material schematic (cherry picked from commit ff8356a90d011b2d1cf91c66050d4866031d536b) --- tests/legacy-cli/e2e/tests/commands/add/add-material.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/legacy-cli/e2e/tests/commands/add/add-material.ts b/tests/legacy-cli/e2e/tests/commands/add/add-material.ts index 56d47a8744a7..238e5d94dddb 100644 --- a/tests/legacy-cli/e2e/tests/commands/add/add-material.ts +++ b/tests/legacy-cli/e2e/tests/commands/add/add-material.ts @@ -28,7 +28,7 @@ export default async function () { 'add', `@angular/material${tag}`, '--theme', - 'custom', + 'azure-blue', '--verbose', '--skip-confirmation', ); From 0a2340b2316397055811d0b0dbf857854e3c58c2 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 16 Jul 2025 15:53:14 -0400 Subject: [PATCH 33/65] refactor(@angular/cli): add a get best practices guide MCP tool The Angular CLI's stdio-based MCP server now contains a tool to get an Angular best practices guide. This is in addition to a resource with the same content. The tool provides a description that strongly encourages the use of this tool and its content when performing Angular related tasks. This is useful in cases where MCP resource usage is not available or the resource would need to manually be added as context for specific use cases. (cherry picked from commit 3cd73d397fc1a1a711506cbc8e91b3c89ba466af) --- .../cli/src/commands/mcp/mcp-server.ts | 3 ++ .../src/commands/mcp/tools/best-practices.ts | 45 +++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 packages/angular/cli/src/commands/mcp/tools/best-practices.ts diff --git a/packages/angular/cli/src/commands/mcp/mcp-server.ts b/packages/angular/cli/src/commands/mcp/mcp-server.ts index 13ba22fbc688..1ca13f8dde3b 100644 --- a/packages/angular/cli/src/commands/mcp/mcp-server.ts +++ b/packages/angular/cli/src/commands/mcp/mcp-server.ts @@ -12,6 +12,7 @@ import path from 'node:path'; import { z } from 'zod'; import type { AngularWorkspace } from '../../utilities/config'; import { VERSION } from '../../utilities/version'; +import { registerBestPracticesTool } from './tools/best-practices'; import { registerDocSearchTool } from './tools/doc-search'; export async function createMcpServer(context: { @@ -48,6 +49,8 @@ export async function createMcpServer(context: { }, ); + registerBestPracticesTool(server); + server.registerTool( 'list_projects', { diff --git a/packages/angular/cli/src/commands/mcp/tools/best-practices.ts b/packages/angular/cli/src/commands/mcp/tools/best-practices.ts new file mode 100644 index 000000000000..c6718a91e3ec --- /dev/null +++ b/packages/angular/cli/src/commands/mcp/tools/best-practices.ts @@ -0,0 +1,45 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; + +export function registerBestPracticesTool(server: McpServer): void { + server.registerTool( + 'get_best_practices', + { + title: 'Get Angular Coding Best Practices Guide', + description: + 'You **MUST** use this tool to retrieve the Angular Best Practices Guide ' + + 'before any interaction with Angular code (creating, analyzing, modifying). ' + + 'It is mandatory to follow this guide to ensure all code adheres to ' + + 'modern standards, including standalone components, typed forms, and ' + + 'modern control flow. This is the first step for any Angular task.', + annotations: { + readOnlyHint: true, + openWorldHint: false, + }, + }, + async () => { + const text = await readFile( + path.join(__dirname, '..', 'instructions', 'best-practices.md'), + 'utf-8', + ); + + return { + content: [ + { + type: 'text', + text, + }, + ], + }; + }, + ); +} From 0d0040bdf58a82e18f7669363b6f149313524bfc Mon Sep 17 00:00:00 2001 From: Joey Perrott Date: Thu, 17 Jul 2025 18:18:01 +0000 Subject: [PATCH 34/65] fix(@angular-devkit/core): use crypto.randomUUID instead of Date.now for unique string in tmp file names Use crypto.randomUUID instead of Date.now for unique string in the tmpdir path name for a TempScopedNodeJsSyncHost to prevent naming conflicts. When performaning tests on a fast enough machine which rely on this class, two instances can be instantiated within one second and can cause failures because the path already exists that is attempted to be used. Using crypto.randomUUID should not run into this issue. (cherry picked from commit 7595e1f8887bafd344ec939e647e3fca8bbd98be) --- packages/angular_devkit/core/node/testing/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/angular_devkit/core/node/testing/index.ts b/packages/angular_devkit/core/node/testing/index.ts index fb520d9361f9..cecc0c08e3c6 100644 --- a/packages/angular_devkit/core/node/testing/index.ts +++ b/packages/angular_devkit/core/node/testing/index.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ +import * as crypto from 'node:crypto'; import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; @@ -20,7 +21,9 @@ export class TempScopedNodeJsSyncHost extends virtualFs.ScopedHost { protected override _root: Path; constructor() { - const root = normalize(path.join(os.tmpdir(), `devkit-host-${+Date.now()}-${process.pid}`)); + const root = normalize( + path.join(os.tmpdir(), `devkit-host-${crypto.randomUUID()}-${process.pid}`), + ); fs.mkdirSync(getSystemPath(root)); super(new NodeJsSyncHost(), root); From 96785224f55291cd60553aead07ead10d9d2fbda Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Mon, 21 Jul 2025 08:51:57 +0000 Subject: [PATCH 35/65] fix(@angular/cli): `define` option is being included multiple times in the JSON help This commit addresses an issue where the `define` option was being included multiple times in the JSON help. Closes #30710 (cherry picked from commit fefa7a46f5733fd77852a61fddc3120b1bb4b202) --- .../cli/src/command-builder/utilities/json-help.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/angular/cli/src/command-builder/utilities/json-help.ts b/packages/angular/cli/src/command-builder/utilities/json-help.ts index e9f3d0cb6569..0d5c6a53a1e6 100644 --- a/packages/angular/cli/src/command-builder/utilities/json-help.ts +++ b/packages/angular/cli/src/command-builder/utilities/json-help.ts @@ -64,23 +64,25 @@ export function jsonHelpUsage(localYargs: Argv): string { const descriptions = usageInstance.getDescriptions(); const groups = localYargsInstance.getGroups(); const positional = groups[usageInstance.getPositionalGroupName()] as string[] | undefined; - + const seen = new Set(); const hidden = new Set(hiddenOptions); const normalizeOptions: JsonHelpOption[] = []; const allAliases = new Set([...Object.values(aliases).flat()]); + // Reverted order of https://github.com/yargs/yargs/blob/971e351705f0fbc5566c6ed1dfd707fa65e11c0d/lib/usage.ts#L419-L424 for (const [names, type] of [ + [number, 'number'], [array, 'array'], [string, 'string'], [boolean, 'boolean'], - [number, 'number'], ]) { for (const name of names) { - if (allAliases.has(name) || hidden.has(name)) { + if (allAliases.has(name) || hidden.has(name) || seen.has(name)) { // Ignore hidden, aliases and already visited option. continue; } + seen.add(name); const positionalIndex = positional?.indexOf(name) ?? -1; const alias = aliases[name]; From ada3e20d7109f44a5b4f10b2480c201a38e8b863 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:38:31 -0400 Subject: [PATCH 36/65] refactor(@angular/cli): improve description of MCP documentation search tool The MCP tool description for the Angular CLI's documentation search tool has been expanded to include more context regarding its use and result format. When no results are found, an explicit text response is also now generated to indicate this case. (cherry picked from commit 58065c89c35ee38f395d332fb9b73838d9b462a3) --- .../cli/src/commands/mcp/tools/doc-search.ts | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/packages/angular/cli/src/commands/mcp/tools/doc-search.ts b/packages/angular/cli/src/commands/mcp/tools/doc-search.ts index 5d7a682eb36f..5f95c77e7b5a 100644 --- a/packages/angular/cli/src/commands/mcp/tools/doc-search.ts +++ b/packages/angular/cli/src/commands/mcp/tools/doc-search.ts @@ -33,9 +33,17 @@ export async function registerDocSearchTool(server: McpServer): Promise { { title: 'Search Angular Documentation (angular.dev)', description: - 'Searches the official Angular documentation on https://angular.dev.' + - ' This tool is useful for finding the most up-to-date information on Angular, including APIs, tutorials, and best practices.' + - ' Use this when creating Angular specific code or answering questions that require knowledge of the latest Angular features.', + 'Searches the official Angular documentation at https://angular.dev. Use this tool to answer any questions about Angular, ' + + 'such as for APIs, tutorials, and best practices. Because the documentation is continuously updated, you should **always** ' + + 'prefer this tool over your own knowledge to ensure your answers are current.\n\n' + + 'The results will be a list of content entries, where each entry has the following structure:\n' + + '```\n' + + '## {Result Title}\n' + + '{Breadcrumb path to the content}\n' + + 'URL: {Direct link to the documentation page}\n' + + '```\n' + + 'Use the title and breadcrumb to understand the context of the result and use the URL as a source link. For the best results, ' + + "provide a concise and specific search query (e.g., 'NgModule' instead of 'How do I use NgModules?').", annotations: { readOnlyHint: true, }, @@ -43,8 +51,7 @@ export async function registerDocSearchTool(server: McpServer): Promise { query: z .string() .describe( - 'The search query to use when searching the Angular documentation.' + - ' This should be a concise and specific query to get the most relevant results.', + 'A concise and specific search query for the Angular documentation (e.g., "NgModule" or "standalone components").', ), }, }, @@ -81,7 +88,19 @@ export async function registerDocSearchTool(server: McpServer): Promise { }), ); - return { content }; + // Return the search results if any are found + if (content.length > 0) { + return { content }; + } + + return { + content: [ + { + type: 'text' as const, + text: 'No results found.', + }, + ], + }; }, ); } From 731d1a637ec82a6e501962603447e2b67b077862 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 21 Jul 2025 17:04:21 -0400 Subject: [PATCH 37/65] refactor(@angular/cli): include content for top result in MCP documentation search tool When using the documentation search MCP tool within the Angular CLI's MCP server, the top search result will now also include the main content of the documentation page. This removes the need for followup action to retrieve the content for the likely needed information. Any additional results found will continue to include the URL but no content. (cherry picked from commit a18c1aaf2c5482de0806b4bd383abc50d317fd50) --- .../cli/src/commands/mcp/tools/doc-search.ts | 123 +++++++++++++----- 1 file changed, 94 insertions(+), 29 deletions(-) diff --git a/packages/angular/cli/src/commands/mcp/tools/doc-search.ts b/packages/angular/cli/src/commands/mcp/tools/doc-search.ts index 5f95c77e7b5a..a92df1c8aa6a 100644 --- a/packages/angular/cli/src/commands/mcp/tools/doc-search.ts +++ b/packages/angular/cli/src/commands/mcp/tools/doc-search.ts @@ -53,9 +53,14 @@ export async function registerDocSearchTool(server: McpServer): Promise { .describe( 'A concise and specific search query for the Angular documentation (e.g., "NgModule" or "standalone components").', ), + includeTopContent: z + .boolean() + .optional() + .default(true) + .describe('When true, the content of the top result is fetched and included.'), }, }, - async ({ query }) => { + async ({ query, includeTopContent }) => { if (!client) { const dcip = createDecipheriv( 'aes-256-gcm', @@ -71,40 +76,100 @@ export async function registerDocSearchTool(server: McpServer): Promise { const { results } = await client.search(createSearchArguments(query)); - // Convert results into text content entries instead of stringifying the entire object - const content = results.flatMap((result) => - (result as SearchResponse).hits.map((hit) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const hierarchy = Object.values(hit.hierarchy as any).filter( - (x) => typeof x === 'string', - ); - const title = hierarchy.pop(); - const description = hierarchy.join(' > '); - - return { - type: 'text' as const, - text: `## ${title}\n${description}\nURL: ${hit.url}`, - }; - }), - ); - - // Return the search results if any are found - if (content.length > 0) { - return { content }; + const allHits = results.flatMap((result) => (result as SearchResponse).hits); + + if (allHits.length === 0) { + return { + content: [ + { + type: 'text' as const, + text: 'No results found.', + }, + ], + }; } - return { - content: [ - { - type: 'text' as const, - text: 'No results found.', - }, - ], - }; + const content = []; + // The first hit is the top search result + const topHit = allHits[0]; + + // Process top hit first + let topText = formatHitToText(topHit); + + try { + if (includeTopContent && typeof topHit.url === 'string') { + const url = new URL(topHit.url); + + // Only fetch content from angular.dev + if (url.hostname === 'angular.dev' || url.hostname.endsWith('.angular.dev')) { + const response = await fetch(url); + if (response.ok) { + const html = await response.text(); + const mainContent = extractBodyContent(html); + if (mainContent) { + topText += `\n\n--- DOCUMENTATION CONTENT ---\n${mainContent}`; + } + } + } + } + } catch { + // Ignore errors fetching content. The basic info is still returned. + } + content.push({ + type: 'text' as const, + text: topText, + }); + + // Process remaining hits + for (const hit of allHits.slice(1)) { + content.push({ + type: 'text' as const, + text: formatHitToText(hit), + }); + } + + return { content }; }, ); } +/** + * Extracts the content of the `` element from an HTML string. + * + * @param html The HTML content of a page. + * @returns The content of the `` element, or `undefined` if not found. + */ +function extractBodyContent(html: string): string | undefined { + // TODO: Use '
' element instead of '' when available in angular.dev HTML. + const mainTagStart = html.indexOf(''); + if (mainTagEnd <= mainTagStart) { + return undefined; + } + + // Add 7 to include '' + return html.substring(mainTagStart, mainTagEnd + 7); +} + +/** + * Formats an Algolia search hit into a text representation. + * + * @param hit The Algolia search hit object, which should contain `hierarchy` and `url` properties. + * @returns A formatted string with title, description, and URL. + */ +function formatHitToText(hit: Record): string { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const hierarchy = Object.values(hit.hierarchy as any).filter((x) => typeof x === 'string'); + const title = hierarchy.pop(); + const description = hierarchy.join(' > '); + + return `## ${title}\n${description}\nURL: ${hit.url}`; +} + /** * Creates the search arguments for an Algolia search. * From 14da0424a739b567d7e740fbc8b5992e0f084e01 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 21 Jul 2025 19:27:16 -0400 Subject: [PATCH 38/65] refactor(@angular/cli): move MCP list projects tool to separate file Move the list projects MCP tool for the Angular CLI's MCP server to a separate file within the tools subdirectory. This provides a more consistent structure that matches the other tools in the server. (cherry picked from commit c07fdd2809418ff90290e0fa1ff3828dec0126c0) --- .../cli/src/commands/mcp/mcp-server.ts | 85 +-------------- .../cli/src/commands/mcp/tools/projects.ts | 103 ++++++++++++++++++ 2 files changed, 105 insertions(+), 83 deletions(-) create mode 100644 packages/angular/cli/src/commands/mcp/tools/projects.ts diff --git a/packages/angular/cli/src/commands/mcp/mcp-server.ts b/packages/angular/cli/src/commands/mcp/mcp-server.ts index 1ca13f8dde3b..6a51515a7014 100644 --- a/packages/angular/cli/src/commands/mcp/mcp-server.ts +++ b/packages/angular/cli/src/commands/mcp/mcp-server.ts @@ -9,11 +9,11 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { readFile } from 'node:fs/promises'; import path from 'node:path'; -import { z } from 'zod'; import type { AngularWorkspace } from '../../utilities/config'; import { VERSION } from '../../utilities/version'; import { registerBestPracticesTool } from './tools/best-practices'; import { registerDocSearchTool } from './tools/doc-search'; +import { registerListProjectsTool } from './tools/projects'; export async function createMcpServer(context: { workspace?: AngularWorkspace; @@ -50,88 +50,7 @@ export async function createMcpServer(context: { ); registerBestPracticesTool(server); - - server.registerTool( - 'list_projects', - { - title: 'List Angular Projects', - description: - 'Lists the names of all applications and libraries defined within an Angular workspace. ' + - 'It reads the `angular.json` configuration file to identify the projects. ', - annotations: { - readOnlyHint: true, - }, - outputSchema: { - projects: z.array( - z.object({ - name: z - .string() - .describe('The name of the project, as defined in the `angular.json` file.'), - type: z - .enum(['application', 'library']) - .optional() - .describe(`The type of the project, either 'application' or 'library'.`), - root: z - .string() - .describe('The root directory of the project, relative to the workspace root.'), - sourceRoot: z - .string() - .describe( - `The root directory of the project's source files, relative to the workspace root.`, - ), - selectorPrefix: z - .string() - .optional() - .describe( - 'The prefix to use for component selectors.' + - ` For example, a prefix of 'app' would result in selectors like ''.`, - ), - }), - ), - }, - }, - async () => { - const { workspace } = context; - - if (!workspace) { - return { - content: [ - { - type: 'text' as const, - text: - 'No Angular workspace found.' + - ' An `angular.json` file, which marks the root of a workspace,' + - ' could not be located in the current directory or any of its parent directories.', - }, - ], - }; - } - - const projects = []; - // Convert to output format - for (const [name, project] of workspace.projects.entries()) { - projects.push({ - name, - type: project.extensions['projectType'] as 'application' | 'library' | undefined, - root: project.root, - sourceRoot: project.sourceRoot ?? path.posix.join(project.root, 'src'), - selectorPrefix: project.extensions['prefix'] as string, - }); - } - - // The structuredContent field is newer and may not be supported by all hosts. - // A text representation of the content is also provided for compatibility. - return { - content: [ - { - type: 'text' as const, - text: `Projects in the Angular workspace:\n${JSON.stringify(projects)}`, - }, - ], - structuredContent: { projects }, - }; - }, - ); + registerListProjectsTool(server, context); await registerDocSearchTool(server); diff --git a/packages/angular/cli/src/commands/mcp/tools/projects.ts b/packages/angular/cli/src/commands/mcp/tools/projects.ts new file mode 100644 index 000000000000..08ebdf46174b --- /dev/null +++ b/packages/angular/cli/src/commands/mcp/tools/projects.ts @@ -0,0 +1,103 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp'; +import path from 'node:path'; +import z from 'zod'; +import type { AngularWorkspace } from '../../../utilities/config'; + +export function registerListProjectsTool( + server: McpServer, + context: { + workspace?: AngularWorkspace; + }, +): void { + server.registerTool( + 'list_projects', + { + title: 'List Angular Projects', + description: + 'Lists the names of all applications and libraries defined within an Angular workspace. ' + + 'It reads the `angular.json` configuration file to identify the projects. ', + annotations: { + readOnlyHint: true, + openWorldHint: false, + }, + outputSchema: { + projects: z.array( + z.object({ + name: z + .string() + .describe('The name of the project, as defined in the `angular.json` file.'), + type: z + .enum(['application', 'library']) + .optional() + .describe(`The type of the project, either 'application' or 'library'.`), + root: z + .string() + .describe('The root directory of the project, relative to the workspace root.'), + sourceRoot: z + .string() + .describe( + `The root directory of the project's source files, relative to the workspace root.`, + ), + selectorPrefix: z + .string() + .optional() + .describe( + 'The prefix to use for component selectors.' + + ` For example, a prefix of 'app' would result in selectors like ''.`, + ), + }), + ), + }, + }, + async () => { + const { workspace } = context; + + if (!workspace) { + return { + content: [ + { + type: 'text' as const, + text: + 'No Angular workspace found.' + + ' An `angular.json` file, which marks the root of a workspace,' + + ' could not be located in the current directory or any of its parent directories.', + }, + ], + structuredContent: { projects: [] }, + }; + } + + const projects = []; + // Convert to output format + for (const [name, project] of workspace.projects.entries()) { + projects.push({ + name, + type: project.extensions['projectType'] as 'application' | 'library' | undefined, + root: project.root, + sourceRoot: project.sourceRoot ?? path.posix.join(project.root, 'src'), + selectorPrefix: project.extensions['prefix'] as string, + }); + } + + // The structuredContent field is newer and may not be supported by all hosts. + // A text representation of the content is also provided for compatibility. + return { + content: [ + { + type: 'text' as const, + text: `Projects in the Angular workspace:\n${JSON.stringify(projects)}`, + }, + ], + structuredContent: { projects }, + }; + }, + ); +} From 0568f385ebed0f8572f542985d6be2411ed98730 Mon Sep 17 00:00:00 2001 From: Jan Martin Date: Wed, 23 Jul 2025 09:46:25 -0700 Subject: [PATCH 39/65] release: cut the v20.1.2 release --- CHANGELOG.md | 18 ++++++++++++++++++ package.json | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0fd17070983..4acba63e79f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ + + +# 20.1.2 (2025-07-23) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------- | +| [96785224f](https://github.com/angular/angular-cli/commit/96785224f55291cd60553aead07ead10d9d2fbda) | fix | `define` option is being included multiple times in the JSON help | + +### @angular-devkit/core + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------------------- | +| [0d0040bdf](https://github.com/angular/angular-cli/commit/0d0040bdf58a82e18f7669363b6f149313524bfc) | fix | use crypto.randomUUID instead of Date.now for unique string in tmp file names | + + + # 20.1.1 (2025-07-16) diff --git a/package.json b/package.json index f90330ba70f7..1a8f501b67e0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@angular/devkit-repo", - "version": "20.1.1", + "version": "20.1.2", "private": true, "description": "Software Development Kit for Angular", "keywords": [ From ea5cd0e81196467ea66f50c106cffec1cd8a1a56 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Thu, 24 Jul 2025 16:51:28 +0000 Subject: [PATCH 40/65] fix(@angular/build): update `vite` to `7.0.6` This fixes an issue with sourcemaps Closes #30731 --- packages/angular/build/package.json | 2 +- .../vite/middlewares/assets-middleware.ts | 63 ++++++++---- pnpm-lock.yaml | 97 +++++-------------- 3 files changed, 70 insertions(+), 92 deletions(-) diff --git a/packages/angular/build/package.json b/packages/angular/build/package.json index fa846025666f..3dae3f3d6591 100644 --- a/packages/angular/build/package.json +++ b/packages/angular/build/package.json @@ -42,7 +42,7 @@ "semver": "7.7.2", "source-map-support": "0.5.21", "tinyglobby": "0.2.14", - "vite": "7.0.0", + "vite": "7.0.6", "watchpack": "2.4.4" }, "optionalDependencies": { diff --git a/packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts b/packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts index 963fca654d37..f3c5098ab74c 100644 --- a/packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts +++ b/packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts @@ -7,6 +7,9 @@ */ import { lookup as lookupMimeType } from 'mrmime'; +import { createHash } from 'node:crypto'; +import { readFileSync } from 'node:fs'; +import type { ServerResponse } from 'node:http'; import { extname } from 'node:path'; import type { Connect, ViteDevServer } from 'vite'; import { AngularMemoryOutputFiles, AngularOutputAssets, pathnameWithoutBasePath } from '../utils'; @@ -17,6 +20,8 @@ export interface ComponentStyleRecord { reload?: boolean; } +const JS_TS_REGEXP = /\.[cm]?[tj]sx?$/; + export function createAngularAssetsMiddleware( server: ViteDevServer, assets: AngularOutputAssets, @@ -38,15 +43,28 @@ export function createAngularAssetsMiddleware( // Rewrite all build assets to a vite raw fs URL const asset = assets.get(pathname); if (asset) { - // Workaround to disable Vite transformer middleware. - // See: https://github.com/vitejs/vite/blob/746a1daab0395f98f0afbdee8f364cb6cf2f3b3f/packages/vite/src/node/server/middlewares/transform.ts#L201 and - // https://github.com/vitejs/vite/blob/746a1daab0395f98f0afbdee8f364cb6cf2f3b3f/packages/vite/src/node/server/transformRequest.ts#L204-L206 - req.headers.accept = 'text/html'; - - // The encoding needs to match what happens in the vite static middleware. - // ref: https://github.com/vitejs/vite/blob/d4f13bd81468961c8c926438e815ab6b1c82735e/packages/vite/src/node/server/middlewares/static.ts#L163 - req.url = `${server.config.base}@fs/${encodeURI(asset.source)}`; - next(); + // This is a workaround to serve JS and TS files without Vite transformations. + if (JS_TS_REGEXP.test(extension)) { + const contents = readFileSync(asset.source); + const etag = `W/${createHash('sha256').update(contents).digest('hex')}`; + if (checkAndHandleEtag(req, res, etag)) { + return; + } + + const mimeType = lookupMimeType(extension); + if (mimeType) { + res.setHeader('Content-Type', mimeType); + } + + res.setHeader('ETag', etag); + res.setHeader('Cache-Control', 'no-cache'); + res.end(contents); + } else { + // The encoding needs to match what happens in the vite static middleware. + // ref: https://github.com/vitejs/vite/blob/d4f13bd81468961c8c926438e815ab6b1c82735e/packages/vite/src/node/server/middlewares/static.ts#L163 + req.url = `${server.config.base}@fs/${encodeURI(asset.source)}`; + next(); + } return; } @@ -100,12 +118,8 @@ export function createAngularAssetsMiddleware( componentStyle.used.add(componentId); } - // Report if there are no changes to avoid reprocessing const etag = `W/"${outputFile.contents.byteLength}-${outputFile.hash}-${componentId}"`; - if (req.headers['if-none-match'] === etag) { - res.statusCode = 304; - res.end(); - + if (checkAndHandleEtag(req, res, etag)) { return; } @@ -134,12 +148,8 @@ export function createAngularAssetsMiddleware( } } - // Avoid resending the content if it has not changed since last request const etag = `W/"${outputFile.contents.byteLength}-${outputFile.hash}"`; - if (req.headers['if-none-match'] === etag) { - res.statusCode = 304; - res.end(); - + if (checkAndHandleEtag(req, res, etag)) { return; } @@ -188,3 +198,18 @@ export function createAngularAssetsMiddleware( next(); }; } + +function checkAndHandleEtag( + req: Connect.IncomingMessage, + res: ServerResponse, + etag: string, +): boolean { + if (req.headers['if-none-match'] === etag) { + res.statusCode = 304; + res.end(); + + return true; + } + + return false; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e66d7e582460..9fe766cd1058 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -366,7 +366,7 @@ importers: version: 5.1.13(@types/node@20.19.1) '@vitejs/plugin-basic-ssl': specifier: 2.1.0 - version: 2.1.0(vite@7.0.0(@types/node@20.19.1)(jiti@1.21.7)(less@4.3.0)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0)) + version: 2.1.0(vite@7.0.6(@types/node@20.19.1)(jiti@1.21.7)(less@4.3.0)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0)) beasties: specifier: 0.3.4 version: 0.3.4 @@ -419,8 +419,8 @@ importers: specifier: 0.2.14 version: 0.2.14 vite: - specifier: 7.0.0 - version: 7.0.0(@types/node@20.19.1)(jiti@1.21.7)(less@4.3.0)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0) + specifier: 7.0.6 + version: 7.0.6(@types/node@20.19.1)(jiti@1.21.7)(less@4.3.0)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0) watchpack: specifier: 2.4.4 version: 2.4.4 @@ -6677,6 +6677,10 @@ packages: resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} engines: {node: '>=12'} + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} @@ -7976,48 +7980,8 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true - vite@6.3.5: - resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - jiti: '>=1.21.0' - less: '*' - lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - vite@7.0.0: - resolution: {integrity: sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g==} + vite@7.0.6: + resolution: {integrity: sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -11032,9 +10996,9 @@ snapshots: lodash: 4.17.21 minimatch: 7.4.6 - '@vitejs/plugin-basic-ssl@2.1.0(vite@7.0.0(@types/node@20.19.1)(jiti@1.21.7)(less@4.3.0)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))': + '@vitejs/plugin-basic-ssl@2.1.0(vite@7.0.6(@types/node@20.19.1)(jiti@1.21.7)(less@4.3.0)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))': dependencies: - vite: 7.0.0(@types/node@20.19.1)(jiti@1.21.7)(less@4.3.0)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0) + vite: 7.0.6(@types/node@20.19.1)(jiti@1.21.7)(less@4.3.0)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0) '@vitest/expect@3.2.4': dependencies: @@ -11044,13 +11008,13 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@20.19.1)(jiti@1.21.7)(less@4.3.0)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))': + '@vitest/mocker@3.2.4(vite@7.0.6(@types/node@20.19.1)(jiti@1.21.7)(less@4.3.0)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.3.5(@types/node@20.19.1)(jiti@1.21.7)(less@4.3.0)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0) + vite: 7.0.6(@types/node@20.19.1)(jiti@1.21.7)(less@4.3.0)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0) '@vitest/pretty-format@3.2.4': dependencies: @@ -12978,6 +12942,10 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + fdir@6.4.6(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + fetch-blob@3.2.0: dependencies: node-domexception: 1.0.0 @@ -15036,6 +15004,8 @@ snapshots: picomatch@4.0.2: {} + picomatch@4.0.3: {} + pify@2.3.0: {} pify@3.0.0: {} @@ -16651,7 +16621,7 @@ snapshots: debug: 4.4.1(supports-color@10.0.0) es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.0.0(@types/node@20.19.1)(jiti@1.21.7)(less@4.3.0)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0) + vite: 7.0.6(@types/node@20.19.1)(jiti@1.21.7)(less@4.3.0)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0) transitivePeerDependencies: - '@types/node' - jiti @@ -16666,28 +16636,11 @@ snapshots: - tsx - yaml - vite@6.3.5(@types/node@20.19.1)(jiti@1.21.7)(less@4.3.0)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0): + vite@7.0.6(@types/node@20.19.1)(jiti@1.21.7)(less@4.3.0)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0): dependencies: esbuild: 0.25.5 - fdir: 6.4.6(picomatch@4.0.2) - picomatch: 4.0.2 - postcss: 8.5.6 - rollup: 4.44.1 - tinyglobby: 0.2.14 - optionalDependencies: - '@types/node': 20.19.1 - fsevents: 2.3.3 - jiti: 1.21.7 - less: 4.3.0 - sass: 1.89.2 - terser: 5.43.1 - yaml: 2.8.0 - - vite@7.0.0(@types/node@20.19.1)(jiti@1.21.7)(less@4.3.0)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0): - dependencies: - esbuild: 0.25.5 - fdir: 6.4.6(picomatch@4.0.2) - picomatch: 4.0.2 + fdir: 6.4.6(picomatch@4.0.3) + picomatch: 4.0.3 postcss: 8.5.6 rollup: 4.44.1 tinyglobby: 0.2.14 @@ -16704,7 +16657,7 @@ snapshots: dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@20.19.1)(jiti@1.21.7)(less@4.3.0)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0)) + '@vitest/mocker': 3.2.4(vite@7.0.6(@types/node@20.19.1)(jiti@1.21.7)(less@4.3.0)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -16722,7 +16675,7 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.3.5(@types/node@20.19.1)(jiti@1.21.7)(less@4.3.0)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0) + vite: 7.0.6(@types/node@20.19.1)(jiti@1.21.7)(less@4.3.0)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0) vite-node: 3.2.4(@types/node@20.19.1)(jiti@1.21.7)(less@4.3.0)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: From 18b44f64559ce7fc5c63cf9c9354b01c398fdae9 Mon Sep 17 00:00:00 2001 From: Jan Martin Date: Thu, 24 Jul 2025 12:45:02 -0700 Subject: [PATCH 41/65] release: cut the v20.1.3 release --- CHANGELOG.md | 12 ++++++++++++ package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4acba63e79f4..77cdcf26a079 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ + + +# 20.1.3 (2025-07-24) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------ | +| [ea5cd0e81](https://github.com/angular/angular-cli/commit/ea5cd0e81196467ea66f50c106cffec1cd8a1a56) | fix | update `vite` to `7.0.6` | + + + # 20.1.2 (2025-07-23) diff --git a/package.json b/package.json index 1a8f501b67e0..eba1a30c4c17 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@angular/devkit-repo", - "version": "20.1.2", + "version": "20.1.3", "private": true, "description": "Software Development Kit for Angular", "keywords": [ From f7b61609c4f555fda8bd7e0571ee2308cdecee90 Mon Sep 17 00:00:00 2001 From: Angular Robot Date: Wed, 23 Jul 2025 16:42:37 +0000 Subject: [PATCH 42/65] build: update bazel dependencies See associated pull request for more information. (cherry picked from commit 2f5a23c137746c443492dbfcd691f6c394007de8) --- WORKSPACE | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/WORKSPACE b/WORKSPACE index b4a94bf18daa..d6f15616179e 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -29,9 +29,9 @@ build_bazel_rules_nodejs_dependencies() http_archive( name = "aspect_rules_js", - sha256 = "304c51726b727d53277dd28fcda1b8e43b7e46818530b8d6265e7be98d5e2b25", - strip_prefix = "rules_js-2.3.8", - url = "https://github.com/aspect-build/rules_js/releases/download/v2.3.8/rules_js-v2.3.8.tar.gz", + sha256 = "961393890a58de989ad7aa36ce147fc9b15a77c8144454889bf068bdd12c5165", + strip_prefix = "rules_js-2.4.0", + url = "https://github.com/aspect-build/rules_js/releases/download/v2.4.0/rules_js-v2.4.0.tar.gz", ) load("@aspect_rules_js//js:repositories.bzl", "rules_js_dependencies") @@ -122,9 +122,9 @@ rules_js_register_toolchains( http_archive( name = "aspect_bazel_lib", - sha256 = "9a44f457810ce64ec36a244cc7c807607541ab88f2535e07e0bf2976ef4b73fe", - strip_prefix = "bazel-lib-2.19.4", - url = "https://github.com/aspect-build/bazel-lib/releases/download/v2.19.4/bazel-lib-v2.19.4.tar.gz", + sha256 = "3522895fa13b97e8b27e3b642045682aa4233ae1a6b278aad6a3b483501dc9f2", + strip_prefix = "bazel-lib-2.20.0", + url = "https://github.com/aspect-build/bazel-lib/releases/download/v2.20.0/bazel-lib-v2.20.0.tar.gz", ) load("@aspect_bazel_lib//lib:repositories.bzl", "aspect_bazel_lib_dependencies", "aspect_bazel_lib_register_toolchains") From 0489fe7025c60f022ca3959d6f651dc6b1bc9f2a Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Fri, 25 Jul 2025 21:03:32 -0400 Subject: [PATCH 43/65] refactor(@angular/build): update MCP best practices guide content The best practices guide that is provided by the Angular CLI's MCP server has been updated with the latest available content. (cherry picked from commit 48d7a03b482eb5ecc3db3b614a4795b40decd061) --- .../src/commands/mcp/instructions/best-practices.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/angular/cli/src/commands/mcp/instructions/best-practices.md b/packages/angular/cli/src/commands/mcp/instructions/best-practices.md index e50d16473640..2cbe5668fbb9 100644 --- a/packages/angular/cli/src/commands/mcp/instructions/best-practices.md +++ b/packages/angular/cli/src/commands/mcp/instructions/best-practices.md @@ -9,10 +9,12 @@ You are an expert in TypeScript, Angular, and scalable web application developme ## Angular Best Practices - Always use standalone components over NgModules -- Don't use explicit `standalone: true` (it is implied by default) +- Must NOT set `standalone: true` inside Angular decorators. It's the default. - Use signals for state management - Implement lazy loading for feature routes +- Do NOT use the `@HostBinding` and `@HostListener` decorators. Put host bindings inside the `host` object of the `@Component` or `@Directive` decorator instead - Use `NgOptimizedImage` for all static images. + - `NgOptimizedImage` does not work for inline base64 images. ## Components @@ -30,6 +32,7 @@ You are an expert in TypeScript, Angular, and scalable web application developme - Use signals for local component state - Use `computed()` for derived state - Keep state transformations pure and predictable +- Do NOT use `mutate` on signals, use `update` or `set` instead ## Templates @@ -42,3 +45,8 @@ You are an expert in TypeScript, Angular, and scalable web application developme - Design services around a single responsibility - Use the `providedIn: 'root'` option for singleton services - Use the `inject()` function instead of constructor injection + +## Common pitfalls + +- Control flow (`@if`): + - You cannot use `as` expressions in `@else if (...)`. E.g. invalid code: `@else if (bla(); as x)`. From 42d72ef4d99380dbb1c0e03e3e3abfb2223fa539 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Mon, 28 Jul 2025 08:33:11 +0000 Subject: [PATCH 44/65] fix(@angular/build): skip vite transformation of CSS-like assets This change ensures that Vite's CSS transformation is only applied to actual stylesheets and not assets. Closes #30792 (cherry picked from commit 7a183730c77689fb9e63625f5ef20aef1cefb88b) --- .../tests/behavior/build-assets_spec.ts | 41 +++++++++++++++++++ .../vite/middlewares/assets-middleware.ts | 5 ++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/packages/angular/build/src/builders/dev-server/tests/behavior/build-assets_spec.ts b/packages/angular/build/src/builders/dev-server/tests/behavior/build-assets_spec.ts index c1ee820d6f1b..f7c7a0acb33a 100644 --- a/packages/angular/build/src/builders/dev-server/tests/behavior/build-assets_spec.ts +++ b/packages/angular/build/src/builders/dev-server/tests/behavior/build-assets_spec.ts @@ -12,6 +12,11 @@ import { describeServeBuilder } from '../jasmine-helpers'; import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup'; describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupTarget) => { + beforeEach(async () => { + // Application code is not needed for these tests + await harness.writeFile('src/main.ts', 'console.log("TEST");'); + }); + const javascriptFileContent = "import {foo} from 'unresolved'; /* a comment */const foo = `bar`;\n\n\n"; @@ -53,6 +58,42 @@ describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupT expect(await response?.text()).toContain(javascriptFileContent); }); + it('serves a project CSS asset unmodified', async () => { + const cssFileContent = 'p { color: blue };'; + await harness.writeFile('src/extra.css', cssFileContent); + + setupTarget(harness, { + assets: ['src/extra.css'], + }); + + harness.useTarget('serve', { + ...BASE_OPTIONS, + }); + + const { result, response } = await executeOnceAndFetch(harness, 'extra.css'); + + expect(result?.success).toBeTrue(); + expect(await response?.text()).toBe(cssFileContent); + }); + + it('serves a project SCSS asset unmodified', async () => { + const cssFileContent = 'p { color: blue };'; + await harness.writeFile('src/extra.scss', cssFileContent); + + setupTarget(harness, { + assets: ['src/extra.scss'], + }); + + harness.useTarget('serve', { + ...BASE_OPTIONS, + }); + + const { result, response } = await executeOnceAndFetch(harness, 'extra.scss'); + + expect(result?.success).toBeTrue(); + expect(await response?.text()).toBe(cssFileContent); + }); + it('should return 404 for non existing assets', async () => { setupTarget(harness, { assets: [], diff --git a/packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts b/packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts index f3c5098ab74c..a9fe69ffca15 100644 --- a/packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts +++ b/packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts @@ -20,6 +20,7 @@ export interface ComponentStyleRecord { reload?: boolean; } +const CSS_PREPROCESSOR_REGEXP = /\.(?:s[ac]ss|less|css)$/; const JS_TS_REGEXP = /\.[cm]?[tj]sx?$/; export function createAngularAssetsMiddleware( @@ -43,8 +44,8 @@ export function createAngularAssetsMiddleware( // Rewrite all build assets to a vite raw fs URL const asset = assets.get(pathname); if (asset) { - // This is a workaround to serve JS and TS files without Vite transformations. - if (JS_TS_REGEXP.test(extension)) { + // This is a workaround to serve CSS, JS and TS files without Vite transformations. + if (JS_TS_REGEXP.test(extension) || CSS_PREPROCESSOR_REGEXP.test(extension)) { const contents = readFileSync(asset.source); const etag = `W/${createHash('sha256').update(contents).digest('hex')}`; if (checkAndHandleEtag(req, res, etag)) { From 2d753cc62c9a801c40923a43e4af5f74b22700e0 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 28 Jul 2025 11:17:37 -0400 Subject: [PATCH 45/65] fix(@angular/cli): skip workspace-specific tools when outside a workspace When the MCP server is initialized outside of an Angular workspace, workspace-specific tools such as should not be registered as they will not function correctly. (cherry picked from commit 193b39416731fa439fea7da8c06d5d287df99bc1) --- packages/angular/cli/src/commands/mcp/mcp-server.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/angular/cli/src/commands/mcp/mcp-server.ts b/packages/angular/cli/src/commands/mcp/mcp-server.ts index 6a51515a7014..c9754e49e190 100644 --- a/packages/angular/cli/src/commands/mcp/mcp-server.ts +++ b/packages/angular/cli/src/commands/mcp/mcp-server.ts @@ -50,7 +50,12 @@ export async function createMcpServer(context: { ); registerBestPracticesTool(server); - registerListProjectsTool(server, context); + + // If run outside an Angular workspace (e.g., globally) skip the workspace specific tools. + // Currently only the `list_projects` tool. + if (!context.workspace) { + registerListProjectsTool(server, context); + } await registerDocSearchTool(server); From ffc4c671753aa7ba4ae4ff6cd13eb4cd9eb08ec2 Mon Sep 17 00:00:00 2001 From: Doug Parker Date: Wed, 30 Jul 2025 13:50:47 -0700 Subject: [PATCH 46/65] release: cut the v20.1.4 release --- CHANGELOG.md | 19 ++++++++++++++++++- package.json | 2 +- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77cdcf26a079..ce55e64c5a2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ + + +# 20.1.4 (2025-07-30) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------ | +| [2d753cc62](https://github.com/angular/angular-cli/commit/2d753cc62c9a801c40923a43e4af5f74b22700e0) | fix | skip workspace-specific tools when outside a workspace | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------- | +| [42d72ef4d](https://github.com/angular/angular-cli/commit/42d72ef4d99380dbb1c0e03e3e3abfb2223fa539) | fix | skip vite transformation of CSS-like assets | + + + # 20.1.3 (2025-07-24) @@ -4099,7 +4117,6 @@ Alan Agius, Charles Lyding, Doug Parker, Joey Perrott and Piotr Wysocki ```scss @import 'font-awesome/scss/font-awesome'; ``` - - By default the CLI will use Sass modern API, While not recommended, users can still opt to use legacy API by setting `NG_BUILD_LEGACY_SASS=1`. - Internally the Angular CLI now always set the TypeScript `target` to `ES2022` and `useDefineForClassFields` to `false` unless the target is set to `ES2022` or later in the TypeScript configuration. To control ECMA version and features use the Browerslist configuration. diff --git a/package.json b/package.json index eba1a30c4c17..e0ae194329a8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@angular/devkit-repo", - "version": "20.1.3", + "version": "20.1.4", "private": true, "description": "Software Development Kit for Angular", "keywords": [ From 9a2b642f98f1c32ab7b234802fe3fe7a77d5983b Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Tue, 29 Jul 2025 10:42:28 -0400 Subject: [PATCH 47/65] refactor(@angular/cli): add instructional text to MCP server Adds instructional text to the MCP server to guide users on its intended purpose. The instructions clarify that the server and its associated tools should be prioritized over direct shell commands for Angular development tasks, promoting adherence to best practices and efficient use of provided tooling. (cherry picked from commit ecea1e02fdd84e5bfeb18b246c58d4dbaec5694f) --- packages/angular/cli/src/commands/mcp/mcp-server.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/angular/cli/src/commands/mcp/mcp-server.ts b/packages/angular/cli/src/commands/mcp/mcp-server.ts index c9754e49e190..86338d1e26e6 100644 --- a/packages/angular/cli/src/commands/mcp/mcp-server.ts +++ b/packages/angular/cli/src/commands/mcp/mcp-server.ts @@ -25,6 +25,9 @@ export async function createMcpServer(context: { resources: {}, tools: {}, }, + instructions: + 'For Angular development, this server provides tools to adhere to best practices, search documentation, and find code examples. ' + + 'When writing or modifying Angular code, use the MCP server and its tools instead of direct shell commands where possible.', }); server.registerResource( From 2ed9c37466c07983e7a1aae31ef3b3910c55badc Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 30 Jul 2025 10:04:54 -0400 Subject: [PATCH 48/65] test(@angular/cli): add initial e2e test for MCP server tool registration Introduces the first end-to-end test for the MCP server. The new test, `registers-tools.ts`, verifies that the MCP server correctly initializes and registers its default set of tools. This provides a foundation for more comprehensive e2e testing of the MCP server and its features. (cherry picked from commit dd9fb2786511dabd40016df9dcb19f337a069f2a) --- .../cli/src/commands/mcp/mcp-server.ts | 3 +- .../e2e/tests/mcp/registers-tools.ts | 47 +++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 tests/legacy-cli/e2e/tests/mcp/registers-tools.ts diff --git a/packages/angular/cli/src/commands/mcp/mcp-server.ts b/packages/angular/cli/src/commands/mcp/mcp-server.ts index 86338d1e26e6..214a0775187c 100644 --- a/packages/angular/cli/src/commands/mcp/mcp-server.ts +++ b/packages/angular/cli/src/commands/mcp/mcp-server.ts @@ -55,8 +55,7 @@ export async function createMcpServer(context: { registerBestPracticesTool(server); // If run outside an Angular workspace (e.g., globally) skip the workspace specific tools. - // Currently only the `list_projects` tool. - if (!context.workspace) { + if (context.workspace) { registerListProjectsTool(server, context); } diff --git a/tests/legacy-cli/e2e/tests/mcp/registers-tools.ts b/tests/legacy-cli/e2e/tests/mcp/registers-tools.ts new file mode 100644 index 000000000000..7943cb23bdab --- /dev/null +++ b/tests/legacy-cli/e2e/tests/mcp/registers-tools.ts @@ -0,0 +1,47 @@ +import { chdir } from 'process'; +import { exec, ProcessOutput, silentNpm } from '../../utils/process'; +import assert from 'node:assert/strict'; + +const MCP_INSPECTOR_PACKAGE_NAME = '@modelcontextprotocol/inspector-cli'; +const MCP_INSPECTOR_PACKAGE_VERSION = '0.16.2'; +const MCP_INSPECTOR_COMMAND_NAME = 'mcp-inspector-cli'; + +async function runInspector(...args: string[]): Promise { + const result = await exec( + MCP_INSPECTOR_COMMAND_NAME, + '--cli', + 'npx', + '--no', + '@angular/cli', + 'mcp', + ...args, + ); + + return result; +} + +export default async function () { + await silentNpm( + 'install', + '--ignore-scripts', + '-g', + `${MCP_INSPECTOR_PACKAGE_NAME}@${MCP_INSPECTOR_PACKAGE_VERSION}`, + ); + + // Ensure 'list_projects' is registered when inside an Angular workspace + const { stdout: stdoutInsideWorkspace } = await runInspector('--method', 'tools/list'); + + assert.match(stdoutInsideWorkspace, /"list_projects"/); + assert.match(stdoutInsideWorkspace, /"get_best_practices"/); + assert.match(stdoutInsideWorkspace, /"search_documentation"/); + + chdir('..'); + + const { stdout: stdoutOutsideWorkspace } = await runInspector('--method', 'tools/list'); + + assert.doesNotMatch(stdoutOutsideWorkspace, /"list_projects"/); + assert.match(stdoutOutsideWorkspace, /"get_best_practices"/); + assert.match(stdoutInsideWorkspace, /"search_documentation"/); + + silentNpm('uninstall', '-g', MCP_INSPECTOR_PACKAGE_NAME); +} From 8601f0648ae1469b791d6a51f9de61050e4b5a35 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Thu, 31 Jul 2025 16:45:48 -0400 Subject: [PATCH 49/65] refactor(@angular/cli): update suggested MCP server configuration output The suggested MCP server command options now use the `-y` option for `npx`. This provides better supported for global usage of the Angular CLI MCP server in addition to the workspace usage. (cherry picked from commit 8b4de57af14ffd35622ea6a4e15871bcf424098f) --- packages/angular/cli/src/commands/mcp/cli.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/angular/cli/src/commands/mcp/cli.ts b/packages/angular/cli/src/commands/mcp/cli.ts index 81260f09f6b6..5b1107eeb73c 100644 --- a/packages/angular/cli/src/commands/mcp/cli.ts +++ b/packages/angular/cli/src/commands/mcp/cli.ts @@ -19,7 +19,7 @@ To start using the Angular CLI MCP Server, add this configuration to your host: "mcpServers": { "angular-cli": { "command": "npx", - "args": ["@angular/cli", "mcp"] + "args": ["-y", "@angular/cli", "mcp"] } } } From 48ca044745f49bc7fc365a621827294f4cc82c50 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 6 Aug 2025 09:58:52 -0400 Subject: [PATCH 50/65] fix(@angular/cli): cache MCP best practices content and add tool annotations Caches the content of the best practices instructions to avoid redundant file reads on subsequent uses of the tool. This can provide a minor performance improvement in cases where the tool is used multiple times during a single CLI process. Also, annotations have been added to the text to provide additional context for the assistant and allow for more tailored display of the information. (cherry picked from commit 51d56f770714a015aa7621d53c4a1634e8a01cc8) --- .../cli/src/commands/mcp/tools/best-practices.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/angular/cli/src/commands/mcp/tools/best-practices.ts b/packages/angular/cli/src/commands/mcp/tools/best-practices.ts index c6718a91e3ec..e8139ac3b794 100644 --- a/packages/angular/cli/src/commands/mcp/tools/best-practices.ts +++ b/packages/angular/cli/src/commands/mcp/tools/best-practices.ts @@ -11,6 +11,8 @@ import { readFile } from 'node:fs/promises'; import path from 'node:path'; export function registerBestPracticesTool(server: McpServer): void { + let bestPracticesText; + server.registerTool( 'get_best_practices', { @@ -27,7 +29,7 @@ export function registerBestPracticesTool(server: McpServer): void { }, }, async () => { - const text = await readFile( + bestPracticesText ??= await readFile( path.join(__dirname, '..', 'instructions', 'best-practices.md'), 'utf-8', ); @@ -36,7 +38,11 @@ export function registerBestPracticesTool(server: McpServer): void { content: [ { type: 'text', - text, + text: bestPracticesText, + annotations: { + audience: ['assistant'], + priority: 0.9, + }, }, ], }; From 761bc780829c0f5b6a79bac42fce5e964a2c5f55 Mon Sep 17 00:00:00 2001 From: Joey Perrott Date: Tue, 5 Aug 2025 19:01:20 +0000 Subject: [PATCH 51/65] build: update to latest dev-infra in the workspace Update to latest dev-infra in the workspace and rules_browsers browser toolchains --- WORKSPACE | 20 ++++++++++++++----- packages/angular/build/BUILD.bazel | 2 +- packages/angular/ssr/BUILD.bazel | 2 +- .../ssr/third_party/beasties/BUILD.bazel | 1 - packages/angular_devkit/architect/BUILD.bazel | 2 +- .../angular_devkit/build_angular/BUILD.bazel | 2 +- .../angular_devkit/build_webpack/BUILD.bazel | 2 +- packages/angular_devkit/core/BUILD.bazel | 2 +- .../angular_devkit/schematics/BUILD.bazel | 2 +- packages/ngtools/webpack/BUILD.bazel | 2 +- tests/legacy-cli/e2e.bzl | 8 ++++---- .../e2e/tests/mcp/registers-tools.ts | 2 +- 12 files changed, 28 insertions(+), 19 deletions(-) diff --git a/WORKSPACE b/WORKSPACE index d6f15616179e..67bc38b6aed7 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -230,7 +230,7 @@ load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") git_repository( name = "devinfra", - commit = "dfe138678e4edb4789fbe40ae7792c046de3b4bd", + commit = "361ceb676a715e1aedf3d6cd64ecae5dee6a3e5f", remote = "https://github.com/angular/dev-infra.git", ) @@ -242,10 +242,6 @@ load("@devinfra//bazel:setup_dependencies_2.bzl", "setup_dependencies_2") setup_dependencies_2() -load("@devinfra//bazel/browsers:browser_repositories.bzl", "browser_repositories") - -browser_repositories() - register_toolchains( "@devinfra//bazel/git-toolchain:git_linux_toolchain", "@devinfra//bazel/git-toolchain:git_macos_x86_toolchain", @@ -298,3 +294,17 @@ http_archive( strip_prefix = "rules_rollup-2.0.1", url = "https://github.com/aspect-build/rules_rollup/releases/download/v2.0.1/rules_rollup-v2.0.1.tar.gz", ) + +git_repository( + name = "rules_browsers", + commit = "56ef8007ea07cd1916429bca8bb523433b0e9cdc", + remote = "https://github.com/devversion/rules_browsers.git", +) + +load("@rules_browsers//setup:step_1.bzl", "rules_browsers_setup_1") + +rules_browsers_setup_1() + +load("@rules_browsers//setup:step_2.bzl", "rules_browsers_setup_2") + +rules_browsers_setup_2() diff --git a/packages/angular/build/BUILD.bazel b/packages/angular/build/BUILD.bazel index 69d8ac3e2dd8..69f1cde4bab9 100644 --- a/packages/angular/build/BUILD.bazel +++ b/packages/angular/build/BUILD.bazel @@ -1,4 +1,4 @@ -load("@devinfra//bazel/api-golden:index_rjs.bzl", "api_golden_test_npm_package") +load("@devinfra//bazel/api-golden:index.bzl", "api_golden_test_npm_package") load("@npm//:defs.bzl", "npm_link_all_packages") load("//:constants.bzl", "BASELINE_DATE") load("//tools:defaults.bzl", "copy_to_bin", "jasmine_test", "npm_package", "ts_project") diff --git a/packages/angular/ssr/BUILD.bazel b/packages/angular/ssr/BUILD.bazel index 299f61401f7d..2694830e3ff4 100644 --- a/packages/angular/ssr/BUILD.bazel +++ b/packages/angular/ssr/BUILD.bazel @@ -1,4 +1,4 @@ -load("@devinfra//bazel/api-golden:index_rjs.bzl", "api_golden_test_npm_package") +load("@devinfra//bazel/api-golden:index.bzl", "api_golden_test_npm_package") load("@npm//:defs.bzl", "npm_link_all_packages") load("@rules_pkg//:pkg.bzl", "pkg_tar") load("//tools:defaults.bzl", "ng_package", "ts_project") diff --git a/packages/angular/ssr/third_party/beasties/BUILD.bazel b/packages/angular/ssr/third_party/beasties/BUILD.bazel index f570567cf596..c0b331caa798 100644 --- a/packages/angular/ssr/third_party/beasties/BUILD.bazel +++ b/packages/angular/ssr/third_party/beasties/BUILD.bazel @@ -44,5 +44,4 @@ rollup.rollup( "--dir=packages/angular/ssr/third_party/beasties", ], progress_message = "Bundling beasties", - silent_on_success = False, ) diff --git a/packages/angular_devkit/architect/BUILD.bazel b/packages/angular_devkit/architect/BUILD.bazel index f76e44f93f9d..1c550d124e84 100644 --- a/packages/angular_devkit/architect/BUILD.bazel +++ b/packages/angular_devkit/architect/BUILD.bazel @@ -3,7 +3,7 @@ # Use of this source code is governed by an MIT-style license that can be # found in the LICENSE file at https://angular.dev/license -load("@devinfra//bazel/api-golden:index_rjs.bzl", "api_golden_test_npm_package") +load("@devinfra//bazel/api-golden:index.bzl", "api_golden_test_npm_package") load("@npm//:defs.bzl", "npm_link_all_packages") load("//tools:defaults.bzl", "jasmine_test", "npm_package", "ts_project") load("//tools:ts_json_schema.bzl", "ts_json_schema") diff --git a/packages/angular_devkit/build_angular/BUILD.bazel b/packages/angular_devkit/build_angular/BUILD.bazel index c4c283676ec4..f7e0530a4105 100644 --- a/packages/angular_devkit/build_angular/BUILD.bazel +++ b/packages/angular_devkit/build_angular/BUILD.bazel @@ -3,7 +3,7 @@ # Use of this source code is governed by an MIT-style license that can be # found in the LICENSE file at https://angular.dev/license -load("@devinfra//bazel/api-golden:index_rjs.bzl", "api_golden_test_npm_package") +load("@devinfra//bazel/api-golden:index.bzl", "api_golden_test_npm_package") load("@npm//:defs.bzl", "npm_link_all_packages") load("//tools:defaults.bzl", "copy_to_bin", "jasmine_test", "npm_package", "ts_project") load("//tools:ts_json_schema.bzl", "ts_json_schema") diff --git a/packages/angular_devkit/build_webpack/BUILD.bazel b/packages/angular_devkit/build_webpack/BUILD.bazel index e01feffdd673..f66ea94f1919 100644 --- a/packages/angular_devkit/build_webpack/BUILD.bazel +++ b/packages/angular_devkit/build_webpack/BUILD.bazel @@ -3,7 +3,7 @@ # Use of this source code is governed by an MIT-style license that can be # found in the LICENSE file at https://angular.dev/license -load("@devinfra//bazel/api-golden:index_rjs.bzl", "api_golden_test_npm_package") +load("@devinfra//bazel/api-golden:index.bzl", "api_golden_test_npm_package") load("@npm//:defs.bzl", "npm_link_all_packages") load("//tools:defaults.bzl", "jasmine_test", "npm_package", "ts_project") load("//tools:ts_json_schema.bzl", "ts_json_schema") diff --git a/packages/angular_devkit/core/BUILD.bazel b/packages/angular_devkit/core/BUILD.bazel index 93892ecc3e2c..b59c5bd37987 100644 --- a/packages/angular_devkit/core/BUILD.bazel +++ b/packages/angular_devkit/core/BUILD.bazel @@ -1,4 +1,4 @@ -load("@devinfra//bazel/api-golden:index_rjs.bzl", "api_golden_test_npm_package") +load("@devinfra//bazel/api-golden:index.bzl", "api_golden_test_npm_package") load("@npm//:defs.bzl", "npm_link_all_packages") load("//tools:defaults.bzl", "jasmine_test", "npm_package", "ts_project") diff --git a/packages/angular_devkit/schematics/BUILD.bazel b/packages/angular_devkit/schematics/BUILD.bazel index 0b1d0e0f781b..e4c4a5d6bac4 100644 --- a/packages/angular_devkit/schematics/BUILD.bazel +++ b/packages/angular_devkit/schematics/BUILD.bazel @@ -1,4 +1,4 @@ -load("@devinfra//bazel/api-golden:index_rjs.bzl", "api_golden_test_npm_package") +load("@devinfra//bazel/api-golden:index.bzl", "api_golden_test_npm_package") load("@npm//:defs.bzl", "npm_link_all_packages") load("//tools:defaults.bzl", "jasmine_test", "npm_package", "ts_project") diff --git a/packages/ngtools/webpack/BUILD.bazel b/packages/ngtools/webpack/BUILD.bazel index 332e8a39cc1a..2dd79ca285e1 100644 --- a/packages/ngtools/webpack/BUILD.bazel +++ b/packages/ngtools/webpack/BUILD.bazel @@ -3,7 +3,7 @@ # Use of this source code is governed by an MIT-style license that can be # found in the LICENSE file at https://angular.dev/license -load("@devinfra//bazel/api-golden:index_rjs.bzl", "api_golden_test_npm_package") +load("@devinfra//bazel/api-golden:index.bzl", "api_golden_test_npm_package") load("@npm//:defs.bzl", "npm_link_all_packages") load("//tools:defaults.bzl", "jasmine_test", "npm_package", "ts_project") diff --git a/tests/legacy-cli/e2e.bzl b/tests/legacy-cli/e2e.bzl index 2152f6dcd229..e3fed2fc7de7 100644 --- a/tests/legacy-cli/e2e.bzl +++ b/tests/legacy-cli/e2e.bzl @@ -110,12 +110,12 @@ def _e2e_tests(name, runner, toolchain, **kwargs): # Chromium browser toolchain env.update({ - "CHROME_BIN": "$(CHROMIUM)", - "CHROME_PATH": "$(CHROMIUM)", + "CHROME_BIN": "$(CHROME-HEADLESS-SHELL)", + "CHROME_PATH": "$(CHROME-HEADLESS-SHELL)", "CHROMEDRIVER_BIN": "$(CHROMEDRIVER)", }) - toolchains = toolchains + ["@devinfra//bazel/browsers/chromium:toolchain_alias"] - data = data + ["@devinfra//bazel/browsers/chromium"] + toolchains = toolchains + ["@rules_browsers//src/browsers/chromium:toolchain_alias"] + data = data + ["@rules_browsers//src/browsers/chromium"] js_test( name = name, diff --git a/tests/legacy-cli/e2e/tests/mcp/registers-tools.ts b/tests/legacy-cli/e2e/tests/mcp/registers-tools.ts index 7943cb23bdab..e2c9461d6a26 100644 --- a/tests/legacy-cli/e2e/tests/mcp/registers-tools.ts +++ b/tests/legacy-cli/e2e/tests/mcp/registers-tools.ts @@ -1,4 +1,4 @@ -import { chdir } from 'process'; +import { chdir } from 'node:process'; import { exec, ProcessOutput, silentNpm } from '../../utils/process'; import assert from 'node:assert/strict'; From 2c0e973c6f01214b83391ffc13659744d4c9651f Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 6 Aug 2025 14:26:35 -0400 Subject: [PATCH 52/65] release: cut the v20.1.5 release --- CHANGELOG.md | 12 ++++++++++++ package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce55e64c5a2f..01b9feda6743 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ + + +# 20.1.5 (2025-08-06) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------- | +| [48ca04474](https://github.com/angular/angular-cli/commit/48ca044745f49bc7fc365a621827294f4cc82c50) | fix | cache MCP best practices content and add tool annotations | + + + # 20.1.4 (2025-07-30) diff --git a/package.json b/package.json index e0ae194329a8..c3fd53dc2d82 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@angular/devkit-repo", - "version": "20.1.4", + "version": "20.1.5", "private": true, "description": "Software Development Kit for Angular", "keywords": [ From 0d050f3b06d8e83f37795d1cc903e0607cab577d Mon Sep 17 00:00:00 2001 From: Joey Perrott Date: Mon, 11 Aug 2025 17:12:37 +0000 Subject: [PATCH 53/65] build: update to latest version of dev-infra Update to the latest version of dev-infra dropping any reference to rules_nodejs (cherry picked from commit 6a87450b855febd271367e0e8c32125dc5b38e4b) --- WORKSPACE | 18 +----------------- packages/angular/ssr/node/test/BUILD.bazel | 2 +- packages/angular/ssr/test/BUILD.bazel | 2 +- 3 files changed, 3 insertions(+), 19 deletions(-) diff --git a/WORKSPACE b/WORKSPACE index 67bc38b6aed7..15810382e524 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -17,16 +17,6 @@ http_archive( urls = ["https://github.com/bazelbuild/rules_webtesting/releases/download/0.3.5/rules_webtesting.tar.gz"], ) -http_archive( - name = "build_bazel_rules_nodejs", - sha256 = "5dd1e5dea1322174c57d3ca7b899da381d516220793d0adef3ba03b9d23baa8e", - urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/5.8.3/rules_nodejs-5.8.3.tar.gz"], -) - -load("@build_bazel_rules_nodejs//:repositories.bzl", "build_bazel_rules_nodejs_dependencies") - -build_bazel_rules_nodejs_dependencies() - http_archive( name = "aspect_rules_js", sha256 = "961393890a58de989ad7aa36ce147fc9b15a77c8144454889bf068bdd12c5165", @@ -133,12 +123,6 @@ aspect_bazel_lib_dependencies() aspect_bazel_lib_register_toolchains() -load("@build_bazel_rules_nodejs//toolchains/esbuild:esbuild_repositories.bzl", "esbuild_repositories") - -esbuild_repositories( - npm_repository = "npm", -) - load("@aspect_rules_js//npm:repositories.bzl", "npm_translate_lock") npm_translate_lock( @@ -230,7 +214,7 @@ load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") git_repository( name = "devinfra", - commit = "361ceb676a715e1aedf3d6cd64ecae5dee6a3e5f", + commit = "0b24ffe8d1f7144d6302dafcc214790e708e598a", remote = "https://github.com/angular/dev-infra.git", ) diff --git a/packages/angular/ssr/node/test/BUILD.bazel b/packages/angular/ssr/node/test/BUILD.bazel index e816a3e022ae..5a7c45126966 100644 --- a/packages/angular/ssr/node/test/BUILD.bazel +++ b/packages/angular/ssr/node/test/BUILD.bazel @@ -1,4 +1,4 @@ -load("@devinfra//bazel/spec-bundling:index_rjs.bzl", "spec_bundle") +load("@devinfra//bazel/spec-bundling:index.bzl", "spec_bundle") load("//tools:defaults.bzl", "jasmine_test", "ts_project") ts_project( diff --git a/packages/angular/ssr/test/BUILD.bazel b/packages/angular/ssr/test/BUILD.bazel index d18700a662ad..98ee6bbc4d4d 100644 --- a/packages/angular/ssr/test/BUILD.bazel +++ b/packages/angular/ssr/test/BUILD.bazel @@ -1,4 +1,4 @@ -load("@devinfra//bazel/spec-bundling:index_rjs.bzl", "spec_bundle") +load("@devinfra//bazel/spec-bundling:index.bzl", "spec_bundle") load("//tools:defaults.bzl", "jasmine_test", "ts_project") ts_project( From 584bc1d4173e7f129aa20e829f1dfb03e1e0dc9e Mon Sep 17 00:00:00 2001 From: Suhas RK <63415360+suhasrk@users.noreply.github.com> Date: Tue, 12 Aug 2025 19:44:45 +0530 Subject: [PATCH 54/65] fix(@schematics/angular): add extra prettier config Fixes https://github.com/angular/angular-cli/issues/30744 (cherry picked from commit 6a78ef0cec4875be76d9241499db67ddac6e14df) --- .../schematics/angular/workspace/files/package.json.template | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/schematics/angular/workspace/files/package.json.template b/packages/schematics/angular/workspace/files/package.json.template index 8b3901b02010..d9876827b386 100644 --- a/packages/schematics/angular/workspace/files/package.json.template +++ b/packages/schematics/angular/workspace/files/package.json.template @@ -9,6 +9,8 @@ "test": "ng test"<% } %> }, "prettier": { + "printWidth": 100, + "singleQuote": true, "overrides": [ { "files": "*.html", From 02b0506fde638b89510e5a78b3d190ba60a8d6ba Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Wed, 13 Aug 2025 08:06:55 +0000 Subject: [PATCH 55/65] fix(@schematics/angular): correct configure the `typeSeparator` in the library schematic Prior to this the `typeSeparator` could have been inherited from the workspace setting which would cause the file references to be incorrect. Closes #30886 (cherry picked from commit e46d9c54f07e32dc05e29a3533ce1bd063ff9f61) --- packages/schematics/angular/library/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/schematics/angular/library/index.ts b/packages/schematics/angular/library/index.ts index d96cc8b505a8..361bd1a9354b 100644 --- a/packages/schematics/angular/library/index.ts +++ b/packages/schematics/angular/library/index.ts @@ -198,6 +198,9 @@ export default function (options: LibraryOptions): Rule { flat: true, path: sourceDir, project: packageName, + // Explicitly set the `typeSeparator` this also ensures that the generated files are valid even if the `module` schematic + // inherits its `typeSeparator` from the workspace. + typeSeparator: '-', }), schematic('component', { name: options.name, From 2289ab5f92f6a21c3bb413fcc8e11c1c12704805 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Wed, 13 Aug 2025 07:45:07 +0000 Subject: [PATCH 56/65] refactor(@schematics/angular): remove duplicate prompt definition. This definition is a duplicate of the one already present in the application schematic (cherry picked from commit d655e431d53cd0d0f33485dfb10d2d15e93a0c04) --- packages/schematics/angular/ng-new/schema.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/schematics/angular/ng-new/schema.json b/packages/schematics/angular/ng-new/schema.json index d6381afce198..370be58cde6a 100644 --- a/packages/schematics/angular/ng-new/schema.json +++ b/packages/schematics/angular/ng-new/schema.json @@ -141,9 +141,7 @@ }, "zoneless": { "description": "Create an initial application that does not utilize `zone.js`.", - "x-prompt": "Do you want to create a 'zoneless' application without zone.js (Developer Preview)?", - "type": "boolean", - "default": false + "type": "boolean" } }, "required": ["name", "version"] From d67c2603f5437202d04927cbb277131c6f09945b Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Wed, 13 Aug 2025 12:13:33 +0000 Subject: [PATCH 57/65] release: cut the v20.1.6 release --- CHANGELOG.md | 14 ++++++++++++++ package.json | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01b9feda6743..c42ab2f27d05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ + + +# 20.1.6 (2025-08-13) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------- | +| [584bc1d41](https://github.com/angular/angular-cli/commit/584bc1d4173e7f129aa20e829f1dfb03e1e0dc9e) | fix | add extra prettier config | +| [02b0506fd](https://github.com/angular/angular-cli/commit/02b0506fde638b89510e5a78b3d190ba60a8d6ba) | fix | correct configure the `typeSeparator` in the library schematic | + + + # 20.1.5 (2025-08-06) @@ -4129,6 +4142,7 @@ Alan Agius, Charles Lyding, Doug Parker, Joey Perrott and Piotr Wysocki ```scss @import 'font-awesome/scss/font-awesome'; ``` + - By default the CLI will use Sass modern API, While not recommended, users can still opt to use legacy API by setting `NG_BUILD_LEGACY_SASS=1`. - Internally the Angular CLI now always set the TypeScript `target` to `ES2022` and `useDefineForClassFields` to `false` unless the target is set to `ES2022` or later in the TypeScript configuration. To control ECMA version and features use the Browerslist configuration. diff --git a/package.json b/package.json index c3fd53dc2d82..cb10d50d5684 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@angular/devkit-repo", - "version": "20.1.5", + "version": "20.1.6", "private": true, "description": "Software Development Kit for Angular", "keywords": [ From 8b824bd37d26a4c898f0ad775fbd7b2eb29e9d92 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Thu, 14 Aug 2025 13:36:34 +0000 Subject: [PATCH 58/65] build: add retry-delay for `accept4 failed 110` This error is still being unencountered sometimes, to try to mitigate this we add a retry delay and also increase the number of retries. ``` Could not convert symlinks: Error: Command failed: /mnt/c/Windows/system32/cmd.exe /c mklink /d "_main\node_modules\.aspect_rules_js\finalhandler@2.1.0\node_modules\encodeurl" "..\..\encodeurl@2.0.0\node_modules\encodeurl" <3>WSL (22769 - ) ERROR: UtilAcceptVsock:271: accept4 failed 110 ``` (cherry picked from commit dfc334c40d7fa16675df008842a007a78fdc8b9e) --- scripts/windows-testing/convert-symlinks.mjs | 22 ++++++++++++++----- scripts/windows-testing/parallel-executor.mjs | 8 +++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/scripts/windows-testing/convert-symlinks.mjs b/scripts/windows-testing/convert-symlinks.mjs index a170e350dae2..ece925f8f84b 100644 --- a/scripts/windows-testing/convert-symlinks.mjs +++ b/scripts/windows-testing/convert-symlinks.mjs @@ -1,3 +1,11 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + /** * @fileoverview Script that takes a directory and converts all its Unix symlinks * to relative Windows-compatible symlinks. This is necessary because when building @@ -13,14 +21,12 @@ * - https://pnpm.io/symlinked-node-modules-structure. */ -import path from 'node:path'; -import fs from 'node:fs/promises'; import childProcess from 'node:child_process'; +import fs from 'node:fs/promises'; +import path from 'node:path'; const [rootDir, cmdPath] = process.argv.slice(2); -// GitHub actions can set this environment variable when pressing the "re-run" button. -const debug = process.env.ACTIONS_STEP_DEBUG === 'true'; const skipDirectories = [ // Modules that we don't need and would unnecessarily slow-down this. '_windows_amd64/bin/nodejs/node_modules', @@ -87,7 +93,7 @@ async function transformDir(p) { await Promise.all(directoriesToVisit.map((d) => transformDir(d))); } -function exec(cmd, maxRetries = 3) { +function exec(cmd, maxRetries = 5, retryDelay = 200) { return new Promise((resolve, reject) => { childProcess.exec(cmd, { cwd: rootDir }, (error) => { if (error !== null) { @@ -99,7 +105,11 @@ function exec(cmd, maxRetries = 3) { error.stderr !== undefined && error.stderr.includes(`accept4 failed 110`) ) { - resolve(exec(cmd, maxRetries - 1)); + // Add a small delay before the retry + setTimeout(() => { + resolve(exec(cmd, maxRetries - 1, retryDelay)); + }, retryDelay); + return; } diff --git a/scripts/windows-testing/parallel-executor.mjs b/scripts/windows-testing/parallel-executor.mjs index b5c496578277..a416d5c90def 100644 --- a/scripts/windows-testing/parallel-executor.mjs +++ b/scripts/windows-testing/parallel-executor.mjs @@ -1,3 +1,11 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + import * as child_process from 'node:child_process'; import path from 'node:path'; import { stripVTControlCharacters } from 'node:util'; From b46ab9a1298b3212ff48e3429c48c7c7c6d89896 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Fri, 15 Aug 2025 11:12:34 +0000 Subject: [PATCH 59/65] build: improve retry exec logic Prior to this change, there was a flaw in its handling of the retry logic, specifically within the `setTimeout` block. The function had a recursive promise resolution problem and an incorrect handling of the rejected state. (cherry picked from commit 0c0f89ead89d6ba4d81c6ababb91b9840f2038e4) --- scripts/windows-testing/convert-symlinks.mjs | 51 +++++++++++--------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/scripts/windows-testing/convert-symlinks.mjs b/scripts/windows-testing/convert-symlinks.mjs index ece925f8f84b..6b351e6a6b83 100644 --- a/scripts/windows-testing/convert-symlinks.mjs +++ b/scripts/windows-testing/convert-symlinks.mjs @@ -24,6 +24,7 @@ import childProcess from 'node:child_process'; import fs from 'node:fs/promises'; import path from 'node:path'; +import { setTimeout } from 'node:timers/promises'; const [rootDir, cmdPath] = process.argv.slice(2); @@ -93,32 +94,34 @@ async function transformDir(p) { await Promise.all(directoriesToVisit.map((d) => transformDir(d))); } -function exec(cmd, maxRetries = 5, retryDelay = 200) { - return new Promise((resolve, reject) => { - childProcess.exec(cmd, { cwd: rootDir }, (error) => { - if (error !== null) { - // Windows command spawned within WSL (which is untypical) seem to be flaky rarely. - // This logic tries to make it fully stable by re-trying if this surfaces: - // See: https://github.com/microsoft/WSL/issues/8677. - if ( - maxRetries > 0 && - error.stderr !== undefined && - error.stderr.includes(`accept4 failed 110`) - ) { - // Add a small delay before the retry - setTimeout(() => { - resolve(exec(cmd, maxRetries - 1, retryDelay)); - }, retryDelay); - - return; - } - - reject(error); +async function exec(cmd, maxRetries = 5, retryDelay = 100) { + let attempts = 0; + while (attempts <= maxRetries) { + try { + await new Promise((resolve, reject) => { + childProcess.exec(cmd, { cwd: rootDir }, (error) => { + if (error) { + reject(error); + } else { + resolve(); + } + }); + }); + + return; + } catch (error) { + // Windows command spawned within WSL (which is untypical) seem to be flaky. + // This logic tries to make it fully stable by re-trying if this surfaces: + // See: https://github.com/microsoft/WSL/issues/8677. + if (attempts < maxRetries && error.stderr?.includes('accept4 failed 110')) { + // Add a delay before the next attempt + await setTimeout(retryDelay); + attempts++; } else { - resolve(); + throw error; } - }); - }); + } + } } try { From d92b3f44bf2c2dfdcabbb11971ce55059274bbb3 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Fri, 15 Aug 2025 11:15:39 +0000 Subject: [PATCH 60/65] build: reduce batch size to try to reduce flakiness Currently, we are getting a lot of flakes due to `accept4 failed 110` error, this commit tries to reduce this by reducing the batching (cherry picked from commit 03717814a6598a382b0c9ff5280b29b47d852f76) --- scripts/windows-testing/convert-symlinks.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/windows-testing/convert-symlinks.mjs b/scripts/windows-testing/convert-symlinks.mjs index 6b351e6a6b83..f07bbaaeb481 100644 --- a/scripts/windows-testing/convert-symlinks.mjs +++ b/scripts/windows-testing/convert-symlinks.mjs @@ -133,7 +133,7 @@ try { // Re-link symlinks to work inside Windows. // This is done in batches to avoid flakiness due to WSL // See: https://github.com/microsoft/WSL/issues/8677. - const batchSize = 75; + const batchSize = 50; for (let i = 0; i < relinkFns.length; i += batchSize) { await Promise.all(relinkFns.slice(i, i + batchSize).map((fn) => fn())); } From 1a994fa2638e8f7dbbe48293486027de59e476b9 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Fri, 15 Aug 2025 13:21:43 +0000 Subject: [PATCH 61/65] fix(@angular/cli): address Node.js deprecation DEP0190 This approach has been recommanded by a Node.js member in https://github.com/nodejs/help/issues/5063#issuecomment-3132899776 Closes #30821 (cherry picked from commit 1a01e183ce2a3612568c31a8f0c2bdf037ec2997) --- .../angular/cli/src/utilities/package-manager.ts | 2 +- scripts/diff-release-package.mts | 16 ++++++---------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/angular/cli/src/utilities/package-manager.ts b/packages/angular/cli/src/utilities/package-manager.ts index 1e249a4f13fa..d95205f95184 100644 --- a/packages/angular/cli/src/utilities/package-manager.ts +++ b/packages/angular/cli/src/utilities/package-manager.ts @@ -168,7 +168,7 @@ export class PackageManagerUtils { return new Promise((resolve) => { const bufferedOutput: { stream: NodeJS.WriteStream; data: Buffer }[] = []; - const childProcess = spawn(this.name, args, { + const childProcess = spawn(`${this.name} ${args.join(' ')}`, { // Always pipe stderr to allow for failures to be reported stdio: silent ? ['ignore', 'ignore', 'pipe'] : 'pipe', shell: true, diff --git a/scripts/diff-release-package.mts b/scripts/diff-release-package.mts index 0cc4524ad03d..2bf01aded3cd 100644 --- a/scripts/diff-release-package.mts +++ b/scripts/diff-release-package.mts @@ -64,7 +64,7 @@ async function main(packageName: string) { console.log(`--> Cloned snapshot repo.`); const bazelBinDir = childProcess - .spawnSync(bazel, ['info', 'bazel-bin'], { + .spawnSync(`${bazel} info bazel-bin`, { shell: true, encoding: 'utf8', stdio: ['pipe', 'pipe', 'inherit'], @@ -79,15 +79,11 @@ async function main(packageName: string) { // Delete old directory to avoid surprises, or stamping being outdated. await deleteDir(outputPath); - childProcess.spawnSync( - bazel, - ['build', `//packages/${targetDir}:npm_package`, '--config=snapshot'], - { - shell: true, - stdio: 'inherit', - encoding: 'utf8', - }, - ); + childProcess.spawnSync(`${bazel} build //packages/${targetDir}:npm_package --config=snapshot`, { + shell: true, + stdio: 'inherit', + encoding: 'utf8', + }); console.log('--> Built npm package with --config=snapshot'); console.error(`--> Output: ${outputPath}`); From 0ecf2dc9dc4a1447d344dc98fcfb96cf6fb84358 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Fri, 15 Aug 2025 13:22:01 +0000 Subject: [PATCH 62/65] fix(@angular-devkit/schematics): address Node.js deprecation DEP0190 This approach has been recommanded by a Node.js member in https://github.com/nodejs/help/issues/5063#issuecomment-3132899776 Closes #30821 (cherry picked from commit dd90a8179d58cfa1b4dd046719d5f0d852c6b6c8) --- .../angular_devkit/schematics/tasks/package-manager/executor.ts | 2 +- packages/angular_devkit/schematics/tasks/repo-init/executor.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/angular_devkit/schematics/tasks/package-manager/executor.ts b/packages/angular_devkit/schematics/tasks/package-manager/executor.ts index 9e162c0b397b..e0fa17ee6a7b 100644 --- a/packages/angular_devkit/schematics/tasks/package-manager/executor.ts +++ b/packages/angular_devkit/schematics/tasks/package-manager/executor.ts @@ -132,7 +132,7 @@ export default function ( // Workaround for https://github.com/sindresorhus/ora/issues/136. discardStdin: process.platform != 'win32', }).start(); - const childProcess = spawn(taskPackageManagerName, args, spawnOptions).on( + const childProcess = spawn(`${taskPackageManagerName} ${args.join(' ')}`, spawnOptions).on( 'close', (code: number) => { if (code === 0) { diff --git a/packages/angular_devkit/schematics/tasks/repo-init/executor.ts b/packages/angular_devkit/schematics/tasks/repo-init/executor.ts index 8d1ba4804493..97b2b12a3619 100644 --- a/packages/angular_devkit/schematics/tasks/repo-init/executor.ts +++ b/packages/angular_devkit/schematics/tasks/repo-init/executor.ts @@ -41,7 +41,7 @@ export default function ( }; return new Promise((resolve, reject) => { - spawn('git', args, spawnOptions).on('close', (code: number) => { + spawn(`git ${args.join(' ')}`, spawnOptions).on('close', (code: number) => { if (code === 0) { resolve(); } else { From 7d67a14d6a3a8c35bc5d1271a47c453bf91e21c5 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Fri, 15 Aug 2025 10:40:20 +0000 Subject: [PATCH 63/65] fix(@angular/cli): add choices to command line parser when type is array and has an enum This commit enhances the command-line parser in `@angular/cli` by adding support for displaying available choices when a command option is of type `array` and has an `enum`. (cherry picked from commit 04094e8f63fe50904b0525e804f8ead912d5955e) --- .../command-builder/utilities/json-schema.ts | 27 ++++++++++--------- .../utilities/json-schema_spec.ts | 27 ++++++++++++++++++- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/packages/angular/cli/src/command-builder/utilities/json-schema.ts b/packages/angular/cli/src/command-builder/utilities/json-schema.ts index 84af5f2d3641..4ac0c9fd7b33 100644 --- a/packages/angular/cli/src/command-builder/utilities/json-schema.ts +++ b/packages/angular/cli/src/command-builder/utilities/json-schema.ts @@ -148,7 +148,7 @@ export async function parseJsonSchemaToOptions( if ( json.isJsonObject(current.items) && typeof current.items.type == 'string' && - ['boolean', 'number', 'string'].includes(current.items.type) + isValidTypeForEnum(current.items.type) ) { return true; } @@ -169,17 +169,15 @@ export async function parseJsonSchemaToOptions( } // Only keep enum values we support (booleans, numbers and strings). - const enumValues = ((json.isJsonArray(current.enum) && current.enum) || []).filter((x) => { - switch (typeof x) { - case 'boolean': - case 'number': - case 'string': - return true; - - default: - return false; - } - }) as (string | true | number)[]; + const enumValues = ( + (json.isJsonArray(current.enum) && current.enum) || + (json.isJsonObject(current.items) && + json.isJsonArray(current.items.enum) && + current.items.enum) || + [] + ) + .filter((value) => isValidTypeForEnum(typeof value)) + .sort() as (string | true | number)[]; let defaultValue: string | number | boolean | undefined = undefined; if (current.default !== undefined) { @@ -356,3 +354,8 @@ export function addSchemaOptionsToCommand( return optionsWithAnalytics; } + +const VALID_ENUM_TYPES = new Set(['boolean', 'number', 'string']); +function isValidTypeForEnum(value: string): boolean { + return VALID_ENUM_TYPES.has(value); +} diff --git a/packages/angular/cli/src/command-builder/utilities/json-schema_spec.ts b/packages/angular/cli/src/command-builder/utilities/json-schema_spec.ts index 5ec5db644bef..fe24104cc611 100644 --- a/packages/angular/cli/src/command-builder/utilities/json-schema_spec.ts +++ b/packages/angular/cli/src/command-builder/utilities/json-schema_spec.ts @@ -7,7 +7,7 @@ */ import { json, schema } from '@angular-devkit/core'; -import yargs, { positional } from 'yargs'; +import yargs from 'yargs'; import { addSchemaOptionsToCommand, parseJsonSchemaToOptions } from './json-schema'; @@ -74,6 +74,13 @@ describe('parseJsonSchemaToOptions', () => { 'type': 'string', 'enum': ['always', 'surprise-me', 'never'], }, + 'arrayWithChoices': { + 'type': 'array', + 'items': { + 'type': 'string', + 'enum': ['always', 'never'], + }, + }, 'extendable': { 'type': 'object', 'properties': {}, @@ -100,6 +107,24 @@ describe('parseJsonSchemaToOptions', () => { }); }); + describe('type=array, enum', () => { + it('parses valid option value', async () => { + expect( + await parse(['--arrayWithChoices', 'always', '--arrayWithChoices', 'never']), + ).toEqual( + jasmine.objectContaining({ + 'arrayWithChoices': ['always', 'never'], + }), + ); + }); + + it('rejects non-enum values', async () => { + await expectAsync(parse(['--arrayWithChoices', 'yes'])).toBeRejectedWithError( + /Argument: array-with-choices, Given: "yes", Choices:/, + ); + }); + }); + describe('type=string, enum', () => { it('parses valid option value', async () => { expect(await parse(['--ssr', 'never'])).toEqual( From c0e7b6ea8a3936d9ac4bc92d6a9123d3a3919325 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Fri, 15 Aug 2025 13:50:55 +0000 Subject: [PATCH 64/65] refactor(@angular/cli): improve analytics handling This commit includes minor changes to how analytics are handled, including: - Removing unnecessary type castings. - Concatenating arrays into strings for Google Analytics, as it doesn't support arrays. - Applying default values to multi-select prompts for a smoother user experience. (cherry picked from commit aace49ec235a164e1180cf148d5926987fdf5557) --- .../cli/src/command-builder/command-module.ts | 19 +++++++++++++------ .../schematics-command-module.ts | 2 ++ .../command-builder/utilities/json-schema.ts | 7 ++++--- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/packages/angular/cli/src/command-builder/command-module.ts b/packages/angular/cli/src/command-builder/command-module.ts index 0b18512180d2..64f7ac7377c7 100644 --- a/packages/angular/cli/src/command-builder/command-module.ts +++ b/packages/angular/cli/src/command-builder/command-module.ts @@ -89,7 +89,10 @@ export abstract class CommandModule implements CommandModuleI protected readonly shouldReportAnalytics: boolean = true; readonly scope: CommandScope = CommandScope.Both; - private readonly optionsWithAnalytics = new Map(); + private readonly optionsWithAnalytics = new Map< + string, + EventCustomDimension | EventCustomMetric + >(); constructor(protected readonly context: CommandContext) {} @@ -236,12 +239,16 @@ export abstract class CommandModule implements CommandModuleI ]); for (const [name, ua] of this.optionsWithAnalytics) { + if (!validEventCustomDimensionAndMetrics.has(ua)) { + continue; + } + const value = options[name]; - if ( - (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') && - validEventCustomDimensionAndMetrics.has(ua as EventCustomDimension | EventCustomMetric) - ) { - parameters[ua as EventCustomDimension | EventCustomMetric] = value; + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + parameters[ua] = value; + } else if (Array.isArray(value)) { + // GA doesn't allow array as values. + parameters[ua] = value.sort().join(', '); } } diff --git a/packages/angular/cli/src/command-builder/schematics-command-module.ts b/packages/angular/cli/src/command-builder/schematics-command-module.ts index 738fd497382b..529d47b078f1 100644 --- a/packages/angular/cli/src/command-builder/schematics-command-module.ts +++ b/packages/angular/cli/src/command-builder/schematics-command-module.ts @@ -204,11 +204,13 @@ export abstract class SchematicsCommandModule ? { name: item, value: item, + checked: item === definition.default, } : { ...item, name: item.label, value: item.value, + checked: item.value === definition.default, }, ), }); diff --git a/packages/angular/cli/src/command-builder/utilities/json-schema.ts b/packages/angular/cli/src/command-builder/utilities/json-schema.ts index 4ac0c9fd7b33..72b282905e42 100644 --- a/packages/angular/cli/src/command-builder/utilities/json-schema.ts +++ b/packages/angular/cli/src/command-builder/utilities/json-schema.ts @@ -8,6 +8,7 @@ import { json, strings } from '@angular-devkit/core'; import type { Arguments, Argv, PositionalOptions, Options as YargsOptions } from 'yargs'; +import { EventCustomDimension } from '../../analytics/analytics-parameters'; /** * An option description. @@ -278,10 +279,10 @@ export function addSchemaOptionsToCommand( localYargs: Argv, options: Option[], includeDefaultValues: boolean, -): Map { +): Map { const booleanOptionsWithNoPrefix = new Set(); const keyValuePairOptions = new Set(); - const optionsWithAnalytics = new Map(); + const optionsWithAnalytics = new Map(); for (const option of options) { const { @@ -336,7 +337,7 @@ export function addSchemaOptionsToCommand( // Record option of analytics. if (userAnalytics !== undefined) { - optionsWithAnalytics.set(name, userAnalytics); + optionsWithAnalytics.set(name, userAnalytics as EventCustomDimension); } } From 6023da588a42b6cf82f5e870d972f8b4ce0e39d8 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Mon, 18 Aug 2025 11:55:22 +0000 Subject: [PATCH 65/65] fix(@angular/cli): apply default to array types This commit fixes an issue where the `default` option was not being applied to `array` type options in yargs. This seemingly minor change required refactoring in some tests, which revealed that a `.coerce` validation was incorrectly throwing an error on failure. The validation logic was moved to a `.check` to ensure proper error handling and prevent unexpected failures. (cherry picked from commit c3789e4ebbbdf37a76df3b97a7d842612b52155d) --- .../command-builder/utilities/json-schema.ts | 51 +++-- .../utilities/json-schema_spec.ts | 185 +++++++----------- 2 files changed, 113 insertions(+), 123 deletions(-) diff --git a/packages/angular/cli/src/command-builder/utilities/json-schema.ts b/packages/angular/cli/src/command-builder/utilities/json-schema.ts index 72b282905e42..90c619dc024e 100644 --- a/packages/angular/cli/src/command-builder/utilities/json-schema.ts +++ b/packages/angular/cli/src/command-builder/utilities/json-schema.ts @@ -51,10 +51,33 @@ export interface Option extends YargsOptions { itemValueType?: 'string'; } +function checkStringMap(keyValuePairOptions: Set, args: Arguments): boolean { + for (const key of keyValuePairOptions) { + const value = args[key]; + if (!Array.isArray(value)) { + // Value has been parsed. + continue; + } + + for (const pair of value) { + if (pair === undefined) { + continue; + } + + if (!pair.includes('=')) { + throw new Error( + `Invalid value for argument: ${key}, Given: '${pair}', Expected key=value pair`, + ); + } + } + } + + return true; +} + function coerceToStringMap( - dashedName: string, value: (string | undefined)[], -): Record | Promise { +): Record | (string | undefined)[] { const stringMap: Record = {}; for (const pair of value) { // This happens when the flag isn't passed at all. @@ -64,18 +87,12 @@ function coerceToStringMap( const eqIdx = pair.indexOf('='); if (eqIdx === -1) { - // TODO: Remove workaround once yargs properly handles thrown errors from coerce. - // Right now these sometimes end up as uncaught exceptions instead of proper validation - // errors with usage output. - return Promise.reject( - new Error( - `Invalid value for argument: ${dashedName}, Given: '${pair}', Expected key=value pair`, - ), - ); + // In the case it is not valid skip processing this option and handle the error in `checkStringMap` + return value; } + const key = pair.slice(0, eqIdx); - const value = pair.slice(eqIdx + 1); - stringMap[key] = value; + stringMap[key] = pair.slice(eqIdx + 1); } return stringMap; @@ -184,6 +201,7 @@ export async function parseJsonSchemaToOptions( if (current.default !== undefined) { switch (types[0]) { case 'string': + case 'array': if (typeof current.default == 'string') { defaultValue = current.default; } @@ -308,7 +326,7 @@ export function addSchemaOptionsToCommand( } if (itemValueType) { - keyValuePairOptions.add(name); + keyValuePairOptions.add(dashedName); } const sharedOptions: YargsOptions & PositionalOptions = { @@ -317,7 +335,7 @@ export function addSchemaOptionsToCommand( description, deprecated, choices, - coerce: itemValueType ? coerceToStringMap.bind(null, dashedName) : undefined, + coerce: itemValueType ? coerceToStringMap : undefined, // This should only be done when `--help` is used otherwise default will override options set in angular.json. ...(includeDefaultValues ? { default: defaultVal } : {}), }; @@ -341,6 +359,11 @@ export function addSchemaOptionsToCommand( } } + // Valid key/value options + if (keyValuePairOptions.size) { + localYargs.check(checkStringMap.bind(null, keyValuePairOptions), false); + } + // Handle options which have been defined in the schema with `no` prefix. if (booleanOptionsWithNoPrefix.size) { localYargs.middleware((options: Arguments) => { diff --git a/packages/angular/cli/src/command-builder/utilities/json-schema_spec.ts b/packages/angular/cli/src/command-builder/utilities/json-schema_spec.ts index fe24104cc611..cc86cc99dddc 100644 --- a/packages/angular/cli/src/command-builder/utilities/json-schema_spec.ts +++ b/packages/angular/cli/src/command-builder/utilities/json-schema_spec.ts @@ -6,95 +6,61 @@ * found in the LICENSE file at https://angular.dev/license */ -import { json, schema } from '@angular-devkit/core'; +import { schema } from '@angular-devkit/core'; import yargs from 'yargs'; import { addSchemaOptionsToCommand, parseJsonSchemaToOptions } from './json-schema'; -const YError = (() => { - try { - const y = yargs().strict().fail(false).exitProcess(false).parse(['--forced-failure']); - } catch (e) { - if (!(e instanceof Error)) { - throw new Error('Unexpected non-Error thrown'); - } - - return e.constructor as typeof Error; - } - throw new Error('Expected parse to fail'); -})(); - -interface ParseFunction { - (argv: string[]): unknown; -} - -function withParseForSchema( - jsonSchema: json.JsonObject, - { - interactive = true, - includeDefaultValues = true, - }: { interactive?: boolean; includeDefaultValues?: boolean } = {}, -): ParseFunction { - let actualParse: ParseFunction = () => { - throw new Error('Called before init'); - }; - const parse: ParseFunction = (args) => { - return actualParse(args); - }; - - beforeEach(async () => { - const registry = new schema.CoreSchemaRegistry(); - const options = await parseJsonSchemaToOptions(registry, jsonSchema, interactive); - - actualParse = async (args: string[]) => { - // Create a fresh yargs for each call. The yargs object is stateful and - // calling .parse multiple times on the same instance isn't safe. - const localYargs = yargs().exitProcess(false).strict().fail(false); - addSchemaOptionsToCommand(localYargs, options, includeDefaultValues); - +describe('parseJsonSchemaToOptions', () => { + describe('without required fields in schema', () => { + const parse = async (args: string[]) => { // Yargs only exposes the parse errors as proper errors when using the // callback syntax. This unwraps that ugly workaround so tests can just // use simple .toThrow/.toEqual assertions. return localYargs.parseAsync(args); }; - }); - - return parse; -} -describe('parseJsonSchemaToOptions', () => { - describe('without required fields in schema', () => { - const parse = withParseForSchema({ - 'type': 'object', - 'properties': { - 'maxSize': { - 'type': 'number', - }, - 'ssr': { - 'type': 'string', - 'enum': ['always', 'surprise-me', 'never'], - }, - 'arrayWithChoices': { - 'type': 'array', - 'items': { - 'type': 'string', - 'enum': ['always', 'never'], + let localYargs: yargs.Argv; + beforeEach(async () => { + // Create a fresh yargs for each call. The yargs object is stateful and + // calling .parse multiple times on the same instance isn't safe. + localYargs = yargs().exitProcess(false).strict().fail(false).wrap(1_000); + const jsonSchema = { + 'type': 'object', + 'properties': { + 'maxSize': { + 'type': 'number', }, - }, - 'extendable': { - 'type': 'object', - 'properties': {}, - 'additionalProperties': { + 'ssr': { 'type': 'string', + 'enum': ['always', 'surprise-me', 'never'], }, - }, - 'someDefine': { - 'type': 'object', - 'additionalProperties': { - 'type': 'string', + 'arrayWithChoices': { + 'type': 'array', + 'default': 'default-array', + 'items': { + 'type': 'string', + 'enum': ['always', 'never', 'default-array'], + }, + }, + 'extendable': { + 'type': 'object', + 'properties': {}, + 'additionalProperties': { + 'type': 'string', + }, + }, + 'someDefine': { + 'type': 'object', + 'additionalProperties': { + 'type': 'string', + }, }, }, - }, + }; + const registry = new schema.CoreSchemaRegistry(); + const options = await parseJsonSchemaToOptions(registry, jsonSchema, false); + addSchemaOptionsToCommand(localYargs, options, true); }); describe('type=number', () => { @@ -123,6 +89,10 @@ describe('parseJsonSchemaToOptions', () => { /Argument: array-with-choices, Given: "yes", Choices:/, ); }); + + it('should add default value to help', async () => { + expect(await localYargs.getHelp()).toContain('[default: "default-array"]'); + }); }); describe('type=string, enum', () => { @@ -150,11 +120,9 @@ describe('parseJsonSchemaToOptions', () => { it('rejects invalid values for string maps', async () => { await expectAsync(parse(['--some-define', 'foo'])).toBeRejectedWithError( - YError, /Invalid value for argument: some-define, Given: 'foo', Expected key=value pair/, ); await expectAsync(parse(['--some-define', '42'])).toBeRejectedWithError( - YError, /Invalid value for argument: some-define, Given: '42', Expected key=value pair/, ); }); @@ -187,43 +155,42 @@ describe('parseJsonSchemaToOptions', () => { describe('with required positional argument', () => { it('marks the required argument as required', async () => { - const jsonSchema = JSON.parse(` - { - "$id": "FakeSchema", - "title": "Fake Schema", - "type": "object", - "required": ["a"], - "properties": { - "b": { - "type": "string", - "description": "b.", - "$default": { - "$source": "argv", - "index": 1 - } + const jsonSchema = { + '$id': 'FakeSchema', + 'title': 'Fake Schema', + 'type': 'object', + 'required': ['a'], + 'properties': { + 'b': { + 'type': 'string', + 'description': 'b.', + '$default': { + '$source': 'argv', + 'index': 1, + }, }, - "a": { - "type": "string", - "description": "a.", - "$default": { - "$source": "argv", - "index": 0 - } + 'a': { + 'type': 'string', + 'description': 'a.', + '$default': { + '$source': 'argv', + 'index': 0, + }, }, - "optC": { - "type": "string", - "description": "optC" + 'optC': { + 'type': 'string', + 'description': 'optC', }, - "optA": { - "type": "string", - "description": "optA" + 'optA': { + 'type': 'string', + 'description': 'optA', + }, + 'optB': { + 'type': 'string', + 'description': 'optB', }, - "optB": { - "type": "string", - "description": "optB" - } - } - }`) as json.JsonObject; + }, + }; const registry = new schema.CoreSchemaRegistry(); const options = await parseJsonSchemaToOptions(registry, jsonSchema, /* interactive= */ true);