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