diff --git a/.nvmrc b/.nvmrc index fc37597bccdb..7377d130eda5 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22.17.0 +22.17.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index e678d91cd723..c42ab2f27d05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,24 +1,92 @@ - + -# 20.0.5 (2025-07-01) +# 20.1.6 (2025-08-13) -### @angular-devkit/build-angular +### @schematics/angular -| Commit | Type | Description | -| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------- | -| [1ebd53df7](https://github.com/angular/angular-cli/commit/1ebd53df7168307f699a9f9ae8f5ef5b9bcf352c) | fix | remove unused `@vitejs/plugin-basic-ssl` dependency | +| 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) + +### @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) + +### @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 | -| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------- | -| [05cebdbcd](https://github.com/angular/angular-cli/commit/05cebdbcd1466bf5c95eb724a784aeb8c7ac083f) | fix | proxy karma request from `/` to `/base` | +| 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) + +### @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) + +### @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) + +### @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-next.3 (2025-06-25) +# 20.1.0 (2025-07-09) ### @angular/cli @@ -30,7 +98,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 @@ -42,16 +110,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` | @@ -74,30 +169,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) @@ -119,28 +190,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) @@ -184,25 +233,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) @@ -1302,7 +1332,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 @@ -4937,7 +4966,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 +4995,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 +5003,6 @@ Alan Agius, Charles Lyding and Doug Parker - `.mjsx` Valid extensions for `styles` are: - - `.css` - `.less` - `.sass` diff --git a/WORKSPACE b/WORKSPACE index b4a94bf18daa..15810382e524 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -17,21 +17,11 @@ 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 = "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 +112,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") @@ -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 = "dfe138678e4edb4789fbe40ae7792c046de3b4bd", + commit = "0b24ffe8d1f7144d6302dafcc214790e708e598a", remote = "https://github.com/angular/dev-infra.git", ) @@ -242,10 +226,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 +278,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/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/goldens/public-api/angular/build/index.api.md b/goldens/public-api/angular/build/index.api.md index aa9ac693864e..19869c39699c 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,15 @@ 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?: KarmaBuilderTransformsOptions): 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 +171,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 +213,24 @@ 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; + setupFiles?: string[]; + tsConfig: string; + watch?: boolean; +}; + // (No @packageDocumentation comment for this package) ``` 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/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/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/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/package.json b/package.json index 1113c6151f4c..cb10d50d5684 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@angular/devkit-repo", - "version": "20.1.0-next.3", + "version": "20.1.6", "private": true, "description": "Software Development Kit for Angular", "keywords": [ @@ -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/BUILD.bazel b/packages/angular/build/BUILD.bazel index e46c2da6fcee..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") @@ -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/package.json b/packages/angular/build/package.json index 50fdeb3f0922..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": { @@ -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/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/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/application/tests/options/output-path_spec.ts b/packages/angular/build/src/builders/application/tests/options/output-path_spec.ts index f8d4513c7de7..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 @@ -10,30 +10,30 @@ 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'); + 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); + }); - 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(() => { + 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(); }); }); 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..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 @@ -16,71 +16,75 @@ 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.writeFile('src/main.ts', 'console.log("TEST");'); + + 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(); + }); }); - }); - } + } + }); }); 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/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/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; diff --git a/packages/angular/build/src/builders/karma/application_builder.ts b/packages/angular/build/src/builders/karma/application_builder.ts index d33469a45ef6..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, normalizeOptions } from './options'; import { Schema as KarmaBuilderOptions } from './schema'; +import type { KarmaBuilderTransformsOptions } from './index'; const localResolve = createRequire(__filename).resolve; const isWindows = process.platform === 'win32'; @@ -84,13 +86,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; } } @@ -268,19 +279,18 @@ function injectKarmaReporter( export function execute( 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 }); @@ -327,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'); @@ -350,14 +356,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, ); @@ -367,12 +373,10 @@ async function collectEntrypoints( // eslint-disable-next-line max-lines-per-function async function initializeApplication( - options: KarmaBuilderOptions, + options: NormalizedKarmaBuilderOptions, context: BuilderContext, karmaOptions: ConfigOptions, - transforms: { - karmaOptions?: (options: ConfigOptions) => ConfigOptions; - } = {}, + transforms?: KarmaBuilderTransformsOptions, ): Promise< [typeof import('karma'), Config & ConfigOptions, BuildOptions, AsyncIterator | null] > { @@ -414,19 +418,13 @@ 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, polyfills, webWorkerTsConfig: options.webWorkerTsConfig, - watch: options.watch ?? !karmaOptions.singleRun, + watch: options.watch, stylePreprocessorOptions: options.stylePreprocessorOptions, inlineStyleLanguage: options.inlineStyleLanguage, fileReplacements: options.fileReplacements, @@ -508,6 +506,7 @@ async function initializeApplication( scriptsFiles.push({ pattern: `${outputPath}/${outputName}`, watched: false, + included: typeof scriptEntry === 'string' ? true : scriptEntry.inject !== false, type: 'js', }); } @@ -541,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 }, ); @@ -708,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/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/builders/karma/index.ts b/packages/angular/build/src/builders/karma/index.ts index d3aefeeff628..f2db8d56c598 100644 --- a/packages/angular/build/src/builders/karma/index.ts +++ b/packages/angular/build/src/builders/karma/index.ts @@ -13,14 +13,12 @@ import { createBuilder, } from '@angular-devkit/architect'; import type { ConfigOptions } from 'karma'; -import { createRequire } from 'node:module'; -import path from 'node:path'; + 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. @@ -28,112 +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 karmaOptions = getBaseKarmaOptions(options, context); - - yield* execute(options, context, karmaOptions, transforms); -} - -function getBaseKarmaOptions( - options: KarmaBuilderOptions, - context: BuilderContext, -): KarmaConfigOptions { - let singleRun: boolean | undefined; - if (options.watch !== undefined) { - 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 (typeof options.browsers === 'string' && options.browsers) { - karmaOptions.browsers = options.browsers.split(',').map((browser) => browser.trim()); - } else if (options.browsers === false) { - karmaOptions.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; - } - } - - 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)), - proxies: { - '/': '/base/', - }, - 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 new file mode 100644 index 000000000000..03ba443702c3 --- /dev/null +++ b/packages/angular/build/src/builders/karma/options.ts @@ -0,0 +1,58 @@ +/** + * @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 { BuilderContext } from '@angular-devkit/architect'; +import { resolve } from 'node:path'; +import { Schema as KarmaBuilderOptions } from './schema'; + +export type NormalizedKarmaBuilderOptions = ReturnType; + +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) { + 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); + + // 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, + }; +} diff --git a/packages/angular/build/src/builders/karma/schema.json b/packages/angular/build/src/builders/karma/schema.json index d24313ba29a6..325f4298f779 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": "Re-run tests when source files change.", + "default": true }, "poll": { "type": "number", 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/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(); + }); + }); +}); diff --git a/packages/angular/build/src/builders/unit-test/builder.ts b/packages/angular/build/src/builders/unit-test/builder.ts index 2df5aed0eabb..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, @@ -24,10 +25,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 +41,7 @@ type VitestCoverageOption = Exclude { @@ -113,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, @@ -134,7 +139,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 ??= []; @@ -148,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 @@ -193,9 +203,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 ? { @@ -383,7 +395,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 { @@ -396,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/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..7bfe9a1ebe94 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( @@ -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 5363d8916d97..c1d4b8a308a1 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; @@ -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 93ca22b5003f..0f54d813ace1 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", @@ -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, 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'; diff --git a/packages/angular/build/src/private.ts b/packages/angular/build/src/private.ts index 2e2691b02485..950a54797250 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'; @@ -59,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); 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); } 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/tools/vite/middlewares/assets-middleware.ts b/packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts index 963fca654d37..a9fe69ffca15 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,9 @@ export interface ComponentStyleRecord { reload?: boolean; } +const CSS_PREPROCESSOR_REGEXP = /\.(?:s[ac]ss|less|css)$/; +const JS_TS_REGEXP = /\.[cm]?[tj]sx?$/; + export function createAngularAssetsMiddleware( server: ViteDevServer, assets: AngularOutputAssets, @@ -38,15 +44,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 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)) { + 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 +119,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 +149,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 +199,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/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; +} 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), ); } diff --git a/packages/angular/cli/BUILD.bazel b/packages/angular/cli/BUILD.bazel index 41201feb3c1f..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", @@ -58,6 +59,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..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", @@ -38,7 +39,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/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-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]; 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..90c619dc024e 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. @@ -50,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. @@ -63,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; @@ -148,7 +166,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,22 +187,21 @@ 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) { switch (types[0]) { case 'string': + case 'array': if (typeof current.default == 'string') { defaultValue = current.default; } @@ -280,10 +297,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 { @@ -309,7 +326,7 @@ export function addSchemaOptionsToCommand( } if (itemValueType) { - keyValuePairOptions.add(name); + keyValuePairOptions.add(dashedName); } const sharedOptions: YargsOptions & PositionalOptions = { @@ -318,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 } : {}), }; @@ -338,10 +355,15 @@ export function addSchemaOptionsToCommand( // Record option of analytics. if (userAnalytics !== undefined) { - optionsWithAnalytics.set(name, userAnalytics); + optionsWithAnalytics.set(name, userAnalytics as EventCustomDimension); } } + // 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) => { @@ -356,3 +378,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..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,88 +6,61 @@ * found in the LICENSE file at https://angular.dev/license */ -import { json, schema } from '@angular-devkit/core'; -import yargs, { positional } from 'yargs'; +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'], - }, - 'extendable': { - 'type': 'object', - 'properties': {}, - 'additionalProperties': { - 'type': 'string', + 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', }, - }, - 'someDefine': { - 'type': 'object', - 'additionalProperties': { + 'ssr': { 'type': 'string', + 'enum': ['always', 'surprise-me', 'never'], + }, + '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', () => { @@ -100,6 +73,28 @@ 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:/, + ); + }); + + it('should add default value to help', async () => { + expect(await localYargs.getHelp()).toContain('[default: "default-array"]'); + }); + }); + describe('type=string, enum', () => { it('parses valid option value', async () => { expect(await parse(['--ssr', 'never'])).toEqual( @@ -125,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/, ); }); @@ -162,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); 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"] } } } 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/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)`. diff --git a/packages/angular/cli/src/commands/mcp/mcp-server.ts b/packages/angular/cli/src/commands/mcp/mcp-server.ts index 33df33f71ba9..214a0775187c 100644 --- a/packages/angular/cli/src/commands/mcp/mcp-server.ts +++ b/packages/angular/cli/src/commands/mcp/mcp-server.ts @@ -11,6 +11,9 @@ import { readFile } from 'node:fs/promises'; import path from 'node:path'; 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; @@ -22,15 +25,21 @@ 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( '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 () => { @@ -43,38 +52,14 @@ export async function createMcpServer(context: { }, ); - server.registerTool( - 'list_projects', - { - title: 'List 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', - }, - () => { - if (!context.workspace) { - return { - content: [ - { - type: 'text', - text: 'Not within an Angular project.', - }, - ], - }; - } + registerBestPracticesTool(server); - return { - content: [ - { - type: 'text', - text: - 'Projects in the Angular workspace: ' + - [...context.workspace.projects.keys()].join(','), - }, - ], - }; - }, - ); + // If run outside an Angular workspace (e.g., globally) skip the workspace specific tools. + if (context.workspace) { + registerListProjectsTool(server, context); + } + + await registerDocSearchTool(server); return server; } 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..e8139ac3b794 --- /dev/null +++ b/packages/angular/cli/src/commands/mcp/tools/best-practices.ts @@ -0,0 +1,51 @@ +/** + * @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 { + let bestPracticesText; + + 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 () => { + bestPracticesText ??= await readFile( + path.join(__dirname, '..', 'instructions', 'best-practices.md'), + 'utf-8', + ); + + return { + content: [ + { + type: 'text', + text: bestPracticesText, + annotations: { + audience: ['assistant'], + priority: 0.9, + }, + }, + ], + }; + }, + ); +} 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..a92df1c8aa6a --- /dev/null +++ b/packages/angular/cli/src/commands/mcp/tools/doc-search.ts @@ -0,0 +1,207 @@ +/** + * @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 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, + }, + inputSchema: { + query: z + .string() + .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, includeTopContent }) => { + 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)); + + const allHits = results.flatMap((result) => (result as SearchResponse).hits); + + if (allHits.length === 0) { + 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. + * + * 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/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 }, + }; + }, + ); +} 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/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/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/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/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( 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, 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_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/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/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/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; 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 }, + ); }); }); }); 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; 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; 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 }, 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/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); 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/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 { diff --git a/packages/ngtools/webpack/BUILD.bazel b/packages/ngtools/webpack/BUILD.bazel index eb75d42a0185..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") @@ -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. 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/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 { 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, 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"] 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() { } + } 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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 494137b8d861..9fe766cd1058 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 @@ -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 @@ -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 @@ -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 @@ -507,6 +510,9 @@ importers: yargs: specifier: 18.0.0 version: 18.0.0 + zod: + specifier: 3.25.75 + version: 3.25.75 packages/angular/pwa: dependencies: @@ -530,23 +536,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 @@ -761,8 +767,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 @@ -856,11 +862,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 @@ -900,51 +906,103 @@ 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'} - '@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: @@ -953,74 +1011,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': @@ -1034,18 +1092,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'} @@ -1071,6 +1133,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'} @@ -1143,6 +1209,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'} @@ -1530,6 +1601,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'} @@ -1538,6 +1613,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 @@ -2000,6 +2079,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'} @@ -2021,6 +2103,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==} @@ -3346,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'} @@ -6144,12 +6233,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 @@ -6588,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'} @@ -6803,7 +6896,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: @@ -7888,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: @@ -8334,43 +8386,120 @@ 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==} 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 '@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 @@ -8383,48 +8512,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) @@ -8445,35 +8574,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 @@ -8493,18 +8622,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 @@ -8513,18 +8642,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 @@ -8541,6 +8670,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 @@ -8584,6 +8721,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 @@ -8598,18 +8737,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 @@ -8678,6 +8817,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 @@ -9178,6 +9321,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 @@ -9188,6 +9343,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': {} @@ -9433,8 +9593,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: @@ -9605,6 +9765,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 @@ -9627,6 +9792,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 @@ -9692,8 +9862,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 @@ -10826,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: @@ -10838,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: @@ -11201,6 +11371,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: @@ -11738,7 +11924,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: @@ -12756,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 @@ -14395,10 +14585,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 @@ -14814,6 +15004,8 @@ snapshots: picomatch@4.0.2: {} + picomatch@4.0.3: {} + pify@2.3.0: {} pify@3.0.0: {} @@ -16429,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 @@ -16444,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 @@ -16482,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 @@ -16500,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: @@ -16880,10 +17055,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: {} 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}`); diff --git a/scripts/windows-testing/convert-symlinks.mjs b/scripts/windows-testing/convert-symlinks.mjs index a170e350dae2..f07bbaaeb481 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,13 @@ * - 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'; +import { setTimeout } from 'node:timers/promises'; 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,28 +94,34 @@ async function transformDir(p) { await Promise.all(directoriesToVisit.map((d) => transformDir(d))); } -function exec(cmd, maxRetries = 3) { - 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`) - ) { - resolve(exec(cmd, maxRetries - 1)); - 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 { @@ -120,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())); } 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'; 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/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', ); 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..e2c9461d6a26 --- /dev/null +++ b/tests/legacy-cli/e2e/tests/mcp/registers-tools.ts @@ -0,0 +1,47 @@ +import { chdir } from 'node: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); +}