From 89dad28c40b4987a159b4fceeb0e2339208e29c2 Mon Sep 17 00:00:00 2001
From: Alan Agius <17563226+alan-agius4@users.noreply.github.com>
Date: Wed, 24 Sep 2025 09:29:11 +0000
Subject: [PATCH 1/2] test: change E2E to zoneless
The e2e tests reflect the new default behaviour.
---
.../src/builders/prerender/index.ts | 5 +++-
.../e2e/initialize/500-create-project.ts | 2 +-
.../e2e/tests/basic/scripts-array.ts | 2 --
.../build/app-shell/app-shell-ngmodule.ts | 10 +------
.../e2e/tests/build/jit-ngmodule.ts | 9 +------
.../build/prerender/http-requests-assets.ts | 17 ++++++------
.../express-engine-ngmodule.ts | 9 +------
...er-routes-output-mode-static-http-calls.ts | 8 +++---
...s-output-mode-static-i18n_APP_BASE_HREF.ts | 5 ++--
tests/legacy-cli/e2e/tests/build/sourcemap.ts | 4 +--
tests/legacy-cli/e2e/tests/build/wasm-esm.ts | 12 +--------
.../tests/commands/builder-project-by-cwd.ts | 4 +--
.../project-cannot-be-determined-by-cwd.ts | 4 +--
.../serve/ssr-http-requests-assets.ts | 26 +++++++------------
.../generate/application/application-basic.ts | 10 -------
.../application-no-zoneless-ng-module.ts | 16 ++++++++++++
.../application-no-zoneless-standalone.ts | 16 ++++++++++++
...s.ts => application-zoneless-ng-module.ts} | 10 +++----
.../application-zoneless-standalone.ts | 12 +++++++++
.../legacy-cli/e2e/tests/jest/no-zoneless.ts | 21 +++++++++++++++
.../e2e/tests/misc/multiple-targets.ts | 2 +-
.../e2e/tests/vite/ssr-error-stack.ts | 17 +++---------
tests/legacy-cli/e2e/utils/jest.ts | 1 -
23 files changed, 114 insertions(+), 108 deletions(-)
delete mode 100644 tests/legacy-cli/e2e/tests/generate/application/application-basic.ts
create mode 100644 tests/legacy-cli/e2e/tests/generate/application/application-no-zoneless-ng-module.ts
create mode 100644 tests/legacy-cli/e2e/tests/generate/application/application-no-zoneless-standalone.ts
rename tests/legacy-cli/e2e/tests/generate/application/{application-zoneless.ts => application-zoneless-ng-module.ts} (53%)
create mode 100644 tests/legacy-cli/e2e/tests/generate/application/application-zoneless-standalone.ts
create mode 100644 tests/legacy-cli/e2e/tests/jest/no-zoneless.ts
diff --git a/packages/angular_devkit/build_angular/src/builders/prerender/index.ts b/packages/angular_devkit/build_angular/src/builders/prerender/index.ts
index b56aa9df4b26..4fcbd8f1fec3 100644
--- a/packages/angular_devkit/build_angular/src/builders/prerender/index.ts
+++ b/packages/angular_devkit/build_angular/src/builders/prerender/index.ts
@@ -168,7 +168,10 @@ async function _renderUniversal(
browserOptions.optimization,
);
- const zonePackage = require.resolve('zone.js', { paths: [context.workspaceRoot] });
+ let zonePackage: string | undefined;
+ try {
+ zonePackage = require.resolve('zone.js/node', { paths: [context.workspaceRoot] });
+ } catch {}
const { baseOutputPath = '' } = serverResult;
const worker = new Piscina({
diff --git a/tests/legacy-cli/e2e/initialize/500-create-project.ts b/tests/legacy-cli/e2e/initialize/500-create-project.ts
index dfc48be927a7..837f54efbcde 100644
--- a/tests/legacy-cli/e2e/initialize/500-create-project.ts
+++ b/tests/legacy-cli/e2e/initialize/500-create-project.ts
@@ -20,7 +20,7 @@ export default async function () {
// Ensure local test registry is used when outside a project
await setNPMConfigRegistry(true);
- await ng('new', 'test-project', '--skip-install', '--no-zoneless');
+ await ng('new', 'test-project', '--skip-install');
await expectFileToExist(join(process.cwd(), 'test-project'));
process.chdir('./test-project');
diff --git a/tests/legacy-cli/e2e/tests/basic/scripts-array.ts b/tests/legacy-cli/e2e/tests/basic/scripts-array.ts
index 09bca1c04fb9..0721b120da2b 100644
--- a/tests/legacy-cli/e2e/tests/basic/scripts-array.ts
+++ b/tests/legacy-cli/e2e/tests/basic/scripts-array.ts
@@ -58,7 +58,6 @@ export default async function () {
await expectFileToMatch(
'dist/test-project/browser/index.html',
[
- '',
'',
'',
'',
@@ -69,7 +68,6 @@ export default async function () {
'dist/test-project/browser/index.html',
[
'',
- '',
'',
'',
'',
diff --git a/tests/legacy-cli/e2e/tests/build/app-shell/app-shell-ngmodule.ts b/tests/legacy-cli/e2e/tests/build/app-shell/app-shell-ngmodule.ts
index 1175f083dcfe..b1b6cfab499d 100644
--- a/tests/legacy-cli/e2e/tests/build/app-shell/app-shell-ngmodule.ts
+++ b/tests/legacy-cli/e2e/tests/build/app-shell/app-shell-ngmodule.ts
@@ -7,15 +7,7 @@ import { updateJsonFile } from '../../../utils/project';
const snapshots = require('../../../ng-snapshot/package.json');
export default async function () {
- await ng(
- 'generate',
- 'app',
- 'test-project-two',
- '--routing',
- '--no-standalone',
- '--skip-install',
- '--no-zoneless',
- );
+ await ng('generate', 'app', 'test-project-two', '--routing', '--no-standalone', '--skip-install');
await ng('generate', 'app-shell', '--project', 'test-project-two');
const isSnapshotBuild = getGlobalVariable('argv')['ng-snapshots'];
diff --git a/tests/legacy-cli/e2e/tests/build/jit-ngmodule.ts b/tests/legacy-cli/e2e/tests/build/jit-ngmodule.ts
index ac784e5c341d..910f8993a16d 100644
--- a/tests/legacy-cli/e2e/tests/build/jit-ngmodule.ts
+++ b/tests/legacy-cli/e2e/tests/build/jit-ngmodule.ts
@@ -3,14 +3,7 @@ import { ng } from '../../utils/process';
import { updateJsonFile, useCIChrome, useCIDefaults } from '../../utils/project';
export default async function () {
- await ng(
- 'generate',
- 'app',
- 'test-project-two',
- '--no-standalone',
- '--skip-install',
- '--no-zoneless',
- );
+ await ng('generate', 'app', 'test-project-two', '--no-standalone', '--skip-install');
await ng('generate', 'private-e2e', '--related-app-name=test-project-two');
// Setup testing to use CI Chrome.
diff --git a/tests/legacy-cli/e2e/tests/build/prerender/http-requests-assets.ts b/tests/legacy-cli/e2e/tests/build/prerender/http-requests-assets.ts
index bb57edcc88a0..dc238941dc81 100644
--- a/tests/legacy-cli/e2e/tests/build/prerender/http-requests-assets.ts
+++ b/tests/legacy-cli/e2e/tests/build/prerender/http-requests-assets.ts
@@ -1,7 +1,7 @@
import { ng } from '../../../utils/process';
import { getGlobalVariable } from '../../../utils/env';
-import { expectFileToMatch, rimraf, writeMultipleFiles } from '../../../utils/fs';
-import { installWorkspacePackages } from '../../../utils/packages';
+import { expectFileToMatch, writeMultipleFiles } from '../../../utils/fs';
+import { installWorkspacePackages, uninstallPackage } from '../../../utils/packages';
import { useSha } from '../../../utils/project';
export default async function () {
@@ -11,16 +11,15 @@ export default async function () {
return;
}
- // Forcibly remove in case another test doesn't clean itself up.
- await rimraf('node_modules/@angular/ssr');
- await ng('add', '@angular/ssr', '--skip-confirmation');
+ await uninstallPackage('@angular/ssr');
+ await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install');
await useSha();
await installWorkspacePackages();
await writeMultipleFiles({
// Add http client and route
'src/app/app.config.ts': `
- import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
+ import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import {Home} from './home/home';
@@ -35,7 +34,6 @@ export default async function () {
}]),
provideClientHydration(),
provideHttpClient(withFetch()),
- provideZoneChangeDetection({ eventCoalescing: true }),
],
};
`,
@@ -46,7 +44,7 @@ export default async function () {
// Update component to do an HTTP call to asset.
'src/app/app.ts': `
- import { Component, inject } from '@angular/core';
+ import { ChangeDetectorRef, Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';
import { HttpClient } from '@angular/common/http';
@@ -64,15 +62,18 @@ export default async function () {
export class App {
data: any;
dataWithSpace: any;
+ private readonly cdr: ChangeDetectorRef = inject(ChangeDetectorRef);
constructor() {
const http = inject(HttpClient);
http.get('/media.json').subscribe((d) => {
this.data = d;
+ this.cdr.markForCheck();
});
http.get('/media%20with-space.json').subscribe((d) => {
this.dataWithSpace = d;
+ this.cdr.markForCheck();
});
}
}
diff --git a/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-ngmodule.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-ngmodule.ts
index f8b45482535e..f05d2182bbd2 100644
--- a/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-ngmodule.ts
+++ b/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-ngmodule.ts
@@ -15,14 +15,7 @@ export default async function () {
// forcibly remove in case another test doesn't clean itself up
await rimraf('node_modules/@angular/ssr');
- await ng(
- 'generate',
- 'app',
- 'test-project-two',
- '--no-standalone',
- '--skip-install',
- '--no-zoneless',
- );
+ await ng('generate', 'app', 'test-project-two', '--no-standalone', '--skip-install');
await ng('generate', 'private-e2e', '--related-app-name=test-project-two');
// Setup testing to use CI Chrome.
diff --git a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-static-http-calls.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-static-http-calls.ts
index d1da216605bc..9b37d3218b3c 100644
--- a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-static-http-calls.ts
+++ b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-static-http-calls.ts
@@ -22,7 +22,7 @@ export default async function () {
'public/media.json': JSON.stringify({ dataFromAssets: true }),
// Update component to do an HTTP call to asset and API.
'src/app/app.ts': `
- import { Component, inject } from '@angular/core';
+ import { ChangeDetectorRef, Component, inject } from '@angular/core';
import { JsonPipe } from '@angular/common';
import { RouterOutlet } from '@angular/router';
import { HttpClient } from '@angular/common/http';
@@ -40,23 +40,26 @@ export default async function () {
export class App {
assetsData: any;
apiData: any;
+ private readonly cdr: ChangeDetectorRef = inject(ChangeDetectorRef);
constructor() {
const http = inject(HttpClient);
http.get('/media.json').toPromise().then((d) => {
this.assetsData = d;
+ this.cdr.markForCheck();
});
http.get('/api').toPromise().then((d) => {
this.apiData = d;
+ this.cdr.markForCheck();
});
}
}
`,
// Add http client and route
'src/app/app.config.ts': `
- import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
+ import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { Home } from './home/home';
@@ -71,7 +74,6 @@ export default async function () {
}]),
provideClientHydration(),
provideHttpClient(withFetch()),
- provideZoneChangeDetection({ eventCoalescing: true }),
],
};
`,
diff --git a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-static-i18n_APP_BASE_HREF.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-static-i18n_APP_BASE_HREF.ts
index 51f5e3990bae..245375101946 100644
--- a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-static-i18n_APP_BASE_HREF.ts
+++ b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-static-i18n_APP_BASE_HREF.ts
@@ -1,7 +1,7 @@
import { join } from 'node:path';
import { existsSync } from 'node:fs';
import assert from 'node:assert';
-import { expectFileNotToExist, expectFileToMatch, writeFile } from '../../../utils/fs';
+import { expectFileToMatch, writeFile } from '../../../utils/fs';
import { ng, noSilentNg, silentNg } from '../../../utils/process';
import { installWorkspacePackages, uninstallPackage } from '../../../utils/packages';
import { useSha } from '../../../utils/project';
@@ -66,7 +66,7 @@ export default async function () {
await writeFile(
'src/app/app.config.ts',
`
- import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
+ import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
@@ -75,7 +75,6 @@ export default async function () {
export const appConfig: ApplicationConfig = {
providers: [
- provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideClientHydration(),
{
diff --git a/tests/legacy-cli/e2e/tests/build/sourcemap.ts b/tests/legacy-cli/e2e/tests/build/sourcemap.ts
index cd3d6c32ab0c..2e153e637f30 100644
--- a/tests/legacy-cli/e2e/tests/build/sourcemap.ts
+++ b/tests/legacy-cli/e2e/tests/build/sourcemap.ts
@@ -12,10 +12,10 @@ export default async function () {
await ng('build', '--output-hashing=bundles', '--source-map', '--configuration=development');
await ng('build', '--output-hashing=none', '--source-map');
- await testForSourceMaps(useWebpackBuilder ? 3 : 2);
+ await testForSourceMaps(useWebpackBuilder ? 2 : 1);
await ng('build', '--output-hashing=none', '--source-map', '--configuration=development');
- await testForSourceMaps(useWebpackBuilder ? 4 : 2);
+ await testForSourceMaps(useWebpackBuilder ? 3 : 1);
}
async function testForSourceMaps(expectedNumberOfFiles: number): Promise {
diff --git a/tests/legacy-cli/e2e/tests/build/wasm-esm.ts b/tests/legacy-cli/e2e/tests/build/wasm-esm.ts
index 43d3708c2c36..8a9735172f1b 100644
--- a/tests/legacy-cli/e2e/tests/build/wasm-esm.ts
+++ b/tests/legacy-cli/e2e/tests/build/wasm-esm.ts
@@ -62,16 +62,6 @@ export default async function () {
// of a JIT production build.
json.projects['test-project'].architect.build.options.polyfills = [];
});
- await replaceInFile(
- 'src/app/app.config.ts',
- 'provideZoneChangeDetection',
- 'provideZonelessChangeDetection',
- );
- await replaceInFile(
- 'src/app/app.config.ts',
- 'provideZoneChangeDetection({ eventCoalescing: true })',
- 'provideZonelessChangeDetection()',
- );
await ng('build');
@@ -94,7 +84,7 @@ export default async function () {
await ng('e2e');
// Setup prerendering and build to test Node.js functionality
- await ng('add', '@angular/ssr', '--skip-confirmation');
+ await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install');
await useSha();
await installWorkspacePackages();
diff --git a/tests/legacy-cli/e2e/tests/commands/builder-project-by-cwd.ts b/tests/legacy-cli/e2e/tests/commands/builder-project-by-cwd.ts
index 519274db5458..6033f4542391 100644
--- a/tests/legacy-cli/e2e/tests/commands/builder-project-by-cwd.ts
+++ b/tests/legacy-cli/e2e/tests/commands/builder-project-by-cwd.ts
@@ -3,8 +3,8 @@ import { expectFileToExist } from '../../utils/fs';
import { ng } from '../../utils/process';
export default async function () {
- await ng('generate', 'app', 'second-app', '--skip-install', '--no-zoneless');
- await ng('generate', 'app', 'third-app', '--skip-install', '--no-zoneless');
+ await ng('generate', 'app', 'second-app', '--skip-install');
+ await ng('generate', 'app', 'third-app', '--skip-install');
const startCwd = process.cwd();
try {
diff --git a/tests/legacy-cli/e2e/tests/commands/project-cannot-be-determined-by-cwd.ts b/tests/legacy-cli/e2e/tests/commands/project-cannot-be-determined-by-cwd.ts
index 4d78ade44d85..b4b572bfa3ee 100644
--- a/tests/legacy-cli/e2e/tests/commands/project-cannot-be-determined-by-cwd.ts
+++ b/tests/legacy-cli/e2e/tests/commands/project-cannot-be-determined-by-cwd.ts
@@ -14,8 +14,8 @@ export default async function () {
delete workspaceJson.projects['test-project'];
});
- await ng('generate', 'app', 'second-app', '--skip-install', '--no-zoneless');
- await ng('generate', 'app', 'third-app', '--skip-install', '--no-zoneless');
+ await ng('generate', 'app', 'second-app', '--skip-install');
+ await ng('generate', 'app', 'third-app', '--skip-install');
const startCwd = process.cwd();
diff --git a/tests/legacy-cli/e2e/tests/commands/serve/ssr-http-requests-assets.ts b/tests/legacy-cli/e2e/tests/commands/serve/ssr-http-requests-assets.ts
index c7cfc16a42fa..6ebb9c20022f 100644
--- a/tests/legacy-cli/e2e/tests/commands/serve/ssr-http-requests-assets.ts
+++ b/tests/legacy-cli/e2e/tests/commands/serve/ssr-http-requests-assets.ts
@@ -1,30 +1,20 @@
import assert from 'node:assert';
import { killAllProcesses, ng } from '../../../utils/process';
-import { rimraf, writeMultipleFiles } from '../../../utils/fs';
-import { installWorkspacePackages } from '../../../utils/packages';
+import { writeMultipleFiles } from '../../../utils/fs';
+import { installWorkspacePackages, uninstallPackage } from '../../../utils/packages';
import { ngServe, useSha } from '../../../utils/project';
-import { getGlobalVariable } from '../../../utils/env';
export default async function () {
- const useWebpackBuilder = !getGlobalVariable('argv')['esbuild'];
-
- // Forcibly remove in case another test doesn't clean itself up.
- await rimraf('node_modules/@angular/ssr');
- if (useWebpackBuilder) {
- // `--server-routing` not supported in `browser` builder.
- await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install');
- } else {
- await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install');
- }
-
+ await uninstallPackage('@angular/ssr');
+ await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install');
await useSha();
await installWorkspacePackages();
await writeMultipleFiles({
// Add http client and route
'src/app/app.config.ts': `
- import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
+ import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { Home } from './home/home';
@@ -39,7 +29,6 @@ export default async function () {
}]),
provideClientHydration(),
provideHttpClient(withFetch()),
- provideZoneChangeDetection({ eventCoalescing: true }),
],
};
`,
@@ -47,7 +36,7 @@ export default async function () {
'public/media.json': JSON.stringify({ dataFromAssets: true }),
// Update component to do an HTTP call to asset.
'src/app/app.ts': `
- import { Component, inject } from '@angular/core';
+ import { ChangeDetectorRef, Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';
import { HttpClient } from '@angular/common/http';
@@ -63,10 +52,13 @@ export default async function () {
})
export class App {
data: any;
+ private readonly cdr: ChangeDetectorRef = inject(ChangeDetectorRef);
+
constructor() {
const http = inject(HttpClient);
http.get('/media.json').toPromise().then((d) => {
this.data = d;
+ this.cdr.markForCheck();
});
}
}
diff --git a/tests/legacy-cli/e2e/tests/generate/application/application-basic.ts b/tests/legacy-cli/e2e/tests/generate/application/application-basic.ts
deleted file mode 100644
index bff84ac1e37b..000000000000
--- a/tests/legacy-cli/e2e/tests/generate/application/application-basic.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { expectFileToMatch } from '../../../utils/fs';
-import { ng } from '../../../utils/process';
-import { useCIChrome } from '../../../utils/project';
-
-export default function () {
- return ng('generate', 'application', 'app2', '--no-zoneless')
- .then(() => expectFileToMatch('angular.json', /\"app2\":/))
- .then(() => useCIChrome('app2', 'projects/app2'))
- .then(() => ng('test', 'app2', '--watch=false'));
-}
diff --git a/tests/legacy-cli/e2e/tests/generate/application/application-no-zoneless-ng-module.ts b/tests/legacy-cli/e2e/tests/generate/application/application-no-zoneless-ng-module.ts
new file mode 100644
index 000000000000..b475ed03b3a7
--- /dev/null
+++ b/tests/legacy-cli/e2e/tests/generate/application/application-no-zoneless-ng-module.ts
@@ -0,0 +1,16 @@
+import { installWorkspacePackages, uninstallPackage } from '../../../utils/packages';
+import { ng } from '../../../utils/process';
+import { useCIChrome, useSha } from '../../../utils/project';
+
+export default async function () {
+ try {
+ await ng('generate', 'app', 'ngmodules', '--no-standalone', '--skip-install', '--no-zoneless');
+ await useSha();
+ await installWorkspacePackages();
+ await useCIChrome('ngmodules', 'projects/ngmodules');
+ await ng('test', 'ngmodules', '--watch=false');
+ await ng('build', 'ngmodules');
+ } finally {
+ await uninstallPackage('zone.js');
+ }
+}
diff --git a/tests/legacy-cli/e2e/tests/generate/application/application-no-zoneless-standalone.ts b/tests/legacy-cli/e2e/tests/generate/application/application-no-zoneless-standalone.ts
new file mode 100644
index 000000000000..065957f9fe11
--- /dev/null
+++ b/tests/legacy-cli/e2e/tests/generate/application/application-no-zoneless-standalone.ts
@@ -0,0 +1,16 @@
+import { installWorkspacePackages, uninstallPackage } from '../../../utils/packages';
+import { ng } from '../../../utils/process';
+import { useCIChrome, useSha } from '../../../utils/project';
+
+export default async function () {
+ try {
+ await ng('generate', 'app', 'standalone', '--standalone', '--skip-install', '--no-zoneless');
+ await useSha();
+ await installWorkspacePackages();
+ await useCIChrome('standalone', 'projects/standalone');
+ await ng('test', 'standalone', '--watch=false');
+ await ng('build', 'standalone');
+ } finally {
+ await uninstallPackage('zone.js');
+ }
+}
diff --git a/tests/legacy-cli/e2e/tests/generate/application/application-zoneless.ts b/tests/legacy-cli/e2e/tests/generate/application/application-zoneless-ng-module.ts
similarity index 53%
rename from tests/legacy-cli/e2e/tests/generate/application/application-zoneless.ts
rename to tests/legacy-cli/e2e/tests/generate/application/application-zoneless-ng-module.ts
index 721ce8fe3599..5ad207a2fa76 100644
--- a/tests/legacy-cli/e2e/tests/generate/application/application-zoneless.ts
+++ b/tests/legacy-cli/e2e/tests/generate/application/application-zoneless-ng-module.ts
@@ -1,13 +1,11 @@
+import { installWorkspacePackages } from '../../../utils/packages';
import { ng } from '../../../utils/process';
-import { useCIChrome } from '../../../utils/project';
+import { useCIChrome, useSha } from '../../../utils/project';
export default async function () {
- await ng('generate', 'app', 'standalone', '--standalone');
- await useCIChrome('standalone', 'projects/standalone');
- await ng('test', 'standalone', '--watch=false');
- await ng('build', 'standalone');
-
await ng('generate', 'app', 'ngmodules', '--no-standalone', '--skip-install');
+ await useSha();
+ await installWorkspacePackages();
await useCIChrome('ngmodules', 'projects/ngmodules');
await ng('test', 'ngmodules', '--watch=false');
await ng('build', 'ngmodules');
diff --git a/tests/legacy-cli/e2e/tests/generate/application/application-zoneless-standalone.ts b/tests/legacy-cli/e2e/tests/generate/application/application-zoneless-standalone.ts
new file mode 100644
index 000000000000..b203ed4a28d0
--- /dev/null
+++ b/tests/legacy-cli/e2e/tests/generate/application/application-zoneless-standalone.ts
@@ -0,0 +1,12 @@
+import { installWorkspacePackages } from '../../../utils/packages';
+import { ng } from '../../../utils/process';
+import { useCIChrome, useSha } from '../../../utils/project';
+
+export default async function () {
+ await ng('generate', 'app', 'standalone', '--standalone', '--skip-install');
+ await useSha();
+ await installWorkspacePackages();
+ await useCIChrome('standalone', 'projects/standalone');
+ await ng('test', 'standalone', '--watch=false');
+ await ng('build', 'standalone');
+}
diff --git a/tests/legacy-cli/e2e/tests/jest/no-zoneless.ts b/tests/legacy-cli/e2e/tests/jest/no-zoneless.ts
new file mode 100644
index 000000000000..411378839b7f
--- /dev/null
+++ b/tests/legacy-cli/e2e/tests/jest/no-zoneless.ts
@@ -0,0 +1,21 @@
+import { applyJestBuilder } from '../../utils/jest';
+import { installPackage, uninstallPackage } from '../../utils/packages';
+import { ng } from '../../utils/process';
+
+export default async function (): Promise {
+ await applyJestBuilder({
+ tsConfig: 'tsconfig.spec.json',
+ polyfills: ['zone.js', 'zone.js/testing'],
+ });
+
+ try {
+ await installPackage('zone.js');
+ const { stderr } = await ng('test');
+
+ if (!stderr.includes('Jest builder is currently EXPERIMENTAL')) {
+ throw new Error(`No experimental notice in stderr.\nSTDERR:\n\n${stderr}`);
+ }
+ } finally {
+ await uninstallPackage('zone.js');
+ }
+}
diff --git a/tests/legacy-cli/e2e/tests/misc/multiple-targets.ts b/tests/legacy-cli/e2e/tests/misc/multiple-targets.ts
index 512ca748db50..a99a37d54b06 100644
--- a/tests/legacy-cli/e2e/tests/misc/multiple-targets.ts
+++ b/tests/legacy-cli/e2e/tests/misc/multiple-targets.ts
@@ -2,7 +2,7 @@ import { expectFileToExist } from '../../utils/fs';
import { ng } from '../../utils/process';
export default async function () {
- await ng('generate', 'app', 'secondary-app', '--no-zoneless');
+ await ng('generate', 'app', 'secondary-app');
await ng('build', 'secondary-app', '--configuration=development');
await expectFileToExist('dist/secondary-app/browser/index.html');
await expectFileToExist('dist/secondary-app/browser/main.js');
diff --git a/tests/legacy-cli/e2e/tests/vite/ssr-error-stack.ts b/tests/legacy-cli/e2e/tests/vite/ssr-error-stack.ts
index a0d7c87bed13..8fce78b9e7ad 100644
--- a/tests/legacy-cli/e2e/tests/vite/ssr-error-stack.ts
+++ b/tests/legacy-cli/e2e/tests/vite/ssr-error-stack.ts
@@ -1,22 +1,13 @@
import { doesNotMatch, match } from 'node:assert';
import { ng } from '../../utils/process';
-import { appendToFile, rimraf } from '../../utils/fs';
+import { appendToFile } from '../../utils/fs';
import { ngServe, useSha } from '../../utils/project';
-import { installWorkspacePackages } from '../../utils/packages';
-import { getGlobalVariable } from '../../utils/env';
+import { installWorkspacePackages, uninstallPackage } from '../../utils/packages';
export default async function () {
- const useWebpackBuilder = !getGlobalVariable('argv')['esbuild'];
-
// Forcibly remove in case another test doesn't clean itself up.
- await rimraf('node_modules/@angular/ssr');
- if (useWebpackBuilder) {
- // `--server-routing` not supported in `browser` builder.
- await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install');
- } else {
- await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install');
- }
-
+ await uninstallPackage('@angular/ssr');
+ await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install');
await useSha();
await installWorkspacePackages();
diff --git a/tests/legacy-cli/e2e/utils/jest.ts b/tests/legacy-cli/e2e/utils/jest.ts
index 904cc6f903d6..6431a93eb64f 100644
--- a/tests/legacy-cli/e2e/utils/jest.ts
+++ b/tests/legacy-cli/e2e/utils/jest.ts
@@ -5,7 +5,6 @@ import { updateJsonFile } from './project';
export async function applyJestBuilder(
options: {} = {
tsConfig: 'tsconfig.spec.json',
- polyfills: ['zone.js', 'zone.js/testing'],
},
): Promise {
await silentNpm('install', 'jest@29.5.0', 'jest-environment-jsdom@29.5.0', '--save-dev');
From 8866386dddcf3fdba718689bc6dd823348a83754 Mon Sep 17 00:00:00 2001
From: Alan Agius <17563226+alan-agius4@users.noreply.github.com>
Date: Wed, 24 Sep 2025 12:39:11 +0000
Subject: [PATCH 2/2] fix(@angular-devkit/build-angular): make zone.js optional
in server and app-shell builders
This commit refactors the server and Jest builders to make `zone.js` an optional dependency, preventing errors in projects that do not include it.
For server builds, `zone.js/node` is now only imported if `zone.js` is detected in the project's dependencies.
---
.../src/builders/app-shell/index.ts | 6 +++-
.../src/builders/app-shell/render-worker.ts | 7 ++--
.../build_angular/src/builders/jest/index.ts | 2 +-
.../src/builders/jest/init-test-bed.mjs | 2 +-
.../src/builders/prerender/index.ts | 7 +++-
.../src/builders/prerender/render-worker.ts | 8 +++--
.../prerender/routes-extractor-worker.ts | 8 +++--
.../src/builders/server/index.ts | 32 ++++++++-----------
.../server/platform-server-exports-loader.ts | 12 +++++--
.../ssr-dev-server/specs/proxy_spec.ts | 2 --
.../builders/ssr-dev-server/specs/ssl_spec.ts | 2 --
.../ssr-dev-server/specs/works_spec.ts | 2 --
.../angular/application/index_spec.ts | 4 +--
.../files/server-builder/server.ts.template | 2 --
tests/legacy-cli/e2e/utils/jest.ts | 1 +
15 files changed, 53 insertions(+), 44 deletions(-)
diff --git a/packages/angular_devkit/build_angular/src/builders/app-shell/index.ts b/packages/angular_devkit/build_angular/src/builders/app-shell/index.ts
index 1cdcd213c055..abdaf31f3a98 100644
--- a/packages/angular_devkit/build_angular/src/builders/app-shell/index.ts
+++ b/packages/angular_devkit/build_angular/src/builders/app-shell/index.ts
@@ -43,7 +43,11 @@ async function _renderUniversal(
// Locate zone.js to load in the render worker
const root = context.workspaceRoot;
- const zonePackage = require.resolve('zone.js', { paths: [root] });
+ let zonePackage: string | undefined;
+
+ try {
+ zonePackage = require.resolve('zone.js', { paths: [root] });
+ } catch {}
const projectName = context.target && context.target.project;
if (!projectName) {
diff --git a/packages/angular_devkit/build_angular/src/builders/app-shell/render-worker.ts b/packages/angular_devkit/build_angular/src/builders/app-shell/render-worker.ts
index 21a39698b5a0..90af7e01b001 100644
--- a/packages/angular_devkit/build_angular/src/builders/app-shell/render-worker.ts
+++ b/packages/angular_devkit/build_angular/src/builders/app-shell/render-worker.ts
@@ -17,7 +17,7 @@ import { workerData } from 'node:worker_threads';
* This is passed as workerData when setting up the worker via the `piscina` package.
*/
const { zonePackage } = workerData as {
- zonePackage: string;
+ zonePackage: string | undefined;
};
interface ServerBundleExports {
@@ -136,8 +136,9 @@ function isBootstrapFn(
* @returns A promise resolving to the render function of the worker.
*/
async function initialize() {
- // Setup Zone.js
- await import(zonePackage);
+ if (zonePackage) {
+ await import(zonePackage);
+ }
// Return the render function for use
return render;
diff --git a/packages/angular_devkit/build_angular/src/builders/jest/index.ts b/packages/angular_devkit/build_angular/src/builders/jest/index.ts
index c972dfa41a46..00450a59b6d7 100644
--- a/packages/angular_devkit/build_angular/src/builders/jest/index.ts
+++ b/packages/angular_devkit/build_angular/src/builders/jest/index.ts
@@ -133,7 +133,7 @@ export default createBuilder(
// the environment for fake async to work correctly.
// Third, we initialize `TestBed`. This is dependent on fake async being set up correctly beforehand.
`--setupFilesAfterEnv="/jest-global.mjs"`,
- ...(options.polyfills ? [`--setupFilesAfterEnv="/polyfills.mjs"`] : []),
+ ...(options.polyfills?.length ? [`--setupFilesAfterEnv="/polyfills.mjs"`] : []),
`--setupFilesAfterEnv="/init-test-bed.mjs"`,
// Don't run any infrastructure files as tests, they are manually loaded where needed.
diff --git a/packages/angular_devkit/build_angular/src/builders/jest/init-test-bed.mjs b/packages/angular_devkit/build_angular/src/builders/jest/init-test-bed.mjs
index dc511242ee52..2a9913b70363 100644
--- a/packages/angular_devkit/build_angular/src/builders/jest/init-test-bed.mjs
+++ b/packages/angular_devkit/build_angular/src/builders/jest/init-test-bed.mjs
@@ -9,7 +9,7 @@
// TODO(dgp1130): These imports likely don't resolve in stricter package environments like `pnpm`, since they are resolved relative to
// `@angular-devkit/build-angular` rather than the user's workspace. Should look into virtual modules to support those use cases.
-import { provideZoneChangeDetection, NgModule } from '@angular/core';
+import { NgModule, provideZoneChangeDetection } from '@angular/core';
import { getTestBed } from '@angular/core/testing';
import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';
diff --git a/packages/angular_devkit/build_angular/src/builders/prerender/index.ts b/packages/angular_devkit/build_angular/src/builders/prerender/index.ts
index 4fcbd8f1fec3..9ebbd4402b94 100644
--- a/packages/angular_devkit/build_angular/src/builders/prerender/index.ts
+++ b/packages/angular_devkit/build_angular/src/builders/prerender/index.ts
@@ -57,6 +57,11 @@ async function getRoutes(
}
}
+ let zonePackage: string | undefined;
+ try {
+ zonePackage = require.resolve('zone.js/node', { paths: [workspaceRoot] });
+ } catch {}
+
if (discoverRoutes) {
const renderWorker = new Piscina({
filename: require.resolve('./routes-extractor-worker'),
@@ -65,7 +70,7 @@ async function getRoutes(
indexFile,
outputPath,
serverBundlePath,
- zonePackage: require.resolve('zone.js', { paths: [workspaceRoot] }),
+ zonePackage,
} as RoutesExtractorWorkerData,
recordTiming: false,
});
diff --git a/packages/angular_devkit/build_angular/src/builders/prerender/render-worker.ts b/packages/angular_devkit/build_angular/src/builders/prerender/render-worker.ts
index e651b6f84344..da478318ebc7 100644
--- a/packages/angular_devkit/build_angular/src/builders/prerender/render-worker.ts
+++ b/packages/angular_devkit/build_angular/src/builders/prerender/render-worker.ts
@@ -51,7 +51,7 @@ interface ServerBundleExports {
* This is passed as workerData when setting up the worker via the `piscina` package.
*/
const { zonePackage } = workerData as {
- zonePackage: string;
+ zonePackage: string | undefined;
};
/**
@@ -163,8 +163,10 @@ function isBootstrapFn(
* @returns A promise resolving to the render function of the worker.
*/
async function initialize() {
- // Setup Zone.js
- await import(zonePackage);
+ if (zonePackage) {
+ // Setup Zone.js
+ await import(zonePackage);
+ }
// Return the render function for use
return render;
diff --git a/packages/angular_devkit/build_angular/src/builders/prerender/routes-extractor-worker.ts b/packages/angular_devkit/build_angular/src/builders/prerender/routes-extractor-worker.ts
index ef324ba1dea6..0d5220d5f0e9 100644
--- a/packages/angular_devkit/build_angular/src/builders/prerender/routes-extractor-worker.ts
+++ b/packages/angular_devkit/build_angular/src/builders/prerender/routes-extractor-worker.ts
@@ -15,7 +15,7 @@ import * as path from 'node:path';
import { workerData } from 'node:worker_threads';
export interface RoutesExtractorWorkerData {
- zonePackage: string;
+ zonePackage: string | undefined;
indexFile: string;
outputPath: string;
serverBundlePath: string;
@@ -74,8 +74,10 @@ async function extract(): Promise {
* @returns A promise resolving to the extract function of the worker.
*/
async function initialize() {
- // Setup Zone.js
- await import(zonePackage);
+ if (zonePackage) {
+ // Setup Zone.js
+ await import(zonePackage);
+ }
return extract;
}
diff --git a/packages/angular_devkit/build_angular/src/builders/server/index.ts b/packages/angular_devkit/build_angular/src/builders/server/index.ts
index 3bdab8a55977..5a246178d10c 100644
--- a/packages/angular_devkit/build_angular/src/builders/server/index.ts
+++ b/packages/angular_devkit/build_angular/src/builders/server/index.ts
@@ -198,7 +198,6 @@ async function initialize(
const originalOutputPath = options.outputPath;
// Assets are processed directly by the builder except when watching
const adjustedOptions = options.watch ? options : { ...options, assets: [] };
-
const { config, projectRoot, projectSourceRoot, i18n } =
await generateI18nBrowserWebpackConfigFromContext(
{
@@ -271,23 +270,18 @@ function getPlatformServerExportsConfig(wco: BrowserWebpackConfigOptions): Parti
// Add `@angular/platform-server` exports.
// This is needed so that DI tokens can be referenced and set at runtime outside of the bundle.
- // Only add `@angular/platform-server` exports when it is installed.
- // In some cases this builder is used when `@angular/platform-server` is not installed.
- // Example: when using `@nguniversal/common/clover` which does not need `@angular/platform-server`.
-
- return isPackageInstalled(wco.root, '@angular/platform-server')
- ? {
- module: {
- rules: [
- {
- loader: require.resolve('./platform-server-exports-loader'),
- include: [path.resolve(wco.root, wco.buildOptions.main)],
- options: {
- angularSSRInstalled: isPackageInstalled(wco.root, '@angular/ssr'),
- },
- },
- ],
+ return {
+ module: {
+ rules: [
+ {
+ loader: require.resolve('./platform-server-exports-loader'),
+ include: [path.resolve(wco.root, wco.buildOptions.main)],
+ options: {
+ angularSSRInstalled: isPackageInstalled(wco.root, '@angular/ssr'),
+ isZoneJsInstalled: isPackageInstalled(wco.root, 'zone.js'),
+ },
},
- }
- : {};
+ ],
+ },
+ };
}
diff --git a/packages/angular_devkit/build_angular/src/builders/server/platform-server-exports-loader.ts b/packages/angular_devkit/build_angular/src/builders/server/platform-server-exports-loader.ts
index 41ec7feacf43..efaa2c87f533 100644
--- a/packages/angular_devkit/build_angular/src/builders/server/platform-server-exports-loader.ts
+++ b/packages/angular_devkit/build_angular/src/builders/server/platform-server-exports-loader.ts
@@ -12,11 +12,14 @@
* @see https://github.com/webpack/webpack/issues/15936.
*/
export default function (
- this: import('webpack').LoaderContext<{ angularSSRInstalled: boolean }>,
+ this: import('webpack').LoaderContext<{
+ angularSSRInstalled: boolean;
+ isZoneJsInstalled: boolean;
+ }>,
content: string,
map: Parameters[1],
) {
- const { angularSSRInstalled } = this.getOptions();
+ const { angularSSRInstalled, isZoneJsInstalled } = this.getOptions();
let source = `${content}
@@ -30,6 +33,11 @@ export default function (
`;
}
+ if (isZoneJsInstalled) {
+ source = `import 'zone.js/node';
+ ${source}`;
+ }
+
this.callback(null, source, map);
return;
diff --git a/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/proxy_spec.ts b/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/proxy_spec.ts
index a4cafd66ee06..cbde961e59e4 100644
--- a/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/proxy_spec.ts
+++ b/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/proxy_spec.ts
@@ -32,8 +32,6 @@ describe('Serve SSR Builder', () => {
host.writeMultipleFiles({
'src/main.server.ts': `
- import 'zone.js/node';
-
import { CommonEngine } from '@angular/ssr/node';
import * as express from 'express';
import { resolve, join } from 'node:path';
diff --git a/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/ssl_spec.ts b/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/ssl_spec.ts
index 3792d87f839c..a67cf0b3c5b5 100644
--- a/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/ssl_spec.ts
+++ b/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/ssl_spec.ts
@@ -32,8 +32,6 @@ describe('Serve SSR Builder', () => {
host.writeMultipleFiles({
'src/main.server.ts': `
- import 'zone.js/node';
-
import { CommonEngine } from '@angular/ssr/node';
import * as express from 'express';
import { resolve, join } from 'node:path';
diff --git a/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/works_spec.ts b/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/works_spec.ts
index 8e92c4d666e3..64c56024f089 100644
--- a/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/works_spec.ts
+++ b/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/works_spec.ts
@@ -31,8 +31,6 @@ describe('Serve SSR Builder', () => {
host.writeMultipleFiles({
'src/main.server.ts': `
- import 'zone.js/node';
-
import { CommonEngine } from '@angular/ssr/node';
import * as express from 'express';
import { resolve, join } from 'node:path';
diff --git a/packages/schematics/angular/application/index_spec.ts b/packages/schematics/angular/application/index_spec.ts
index 45aa584c7d8e..ba0033f8f119 100644
--- a/packages/schematics/angular/application/index_spec.ts
+++ b/packages/schematics/angular/application/index_spec.ts
@@ -389,7 +389,7 @@ describe('Application Schematic', () => {
expect(buildOpt.index).toBeUndefined();
expect(buildOpt.browser).toEqual('src/main.ts');
expect(buildOpt.assets).toEqual([{ 'glob': '**/*', 'input': 'public' }]);
- expect(buildOpt.polyfills).toEqual(undefined);
+ expect(buildOpt.polyfills).toBeUndefined();
expect(buildOpt.tsConfig).toEqual('tsconfig.app.json');
const testOpt = prj.architect.test.options;
@@ -478,7 +478,7 @@ describe('Application Schematic', () => {
expect(project.root).toEqual('foo');
const buildOpt = project.architect.build.options;
expect(buildOpt.browser).toEqual('foo/src/main.ts');
- expect(buildOpt.polyfills).toEqual(undefined);
+ expect(buildOpt.polyfills).toBeUndefined();
expect(buildOpt.tsConfig).toEqual('foo/tsconfig.app.json');
expect(buildOpt.assets).toEqual([{ 'glob': '**/*', 'input': 'foo/public' }]);
diff --git a/packages/schematics/angular/ssr/files/server-builder/server.ts.template b/packages/schematics/angular/ssr/files/server-builder/server.ts.template
index 7567fa65a81d..7327c26532ea 100644
--- a/packages/schematics/angular/ssr/files/server-builder/server.ts.template
+++ b/packages/schematics/angular/ssr/files/server-builder/server.ts.template
@@ -1,5 +1,3 @@
-import 'zone.js/node';
-
import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine } from '@angular/ssr/node';
import express from 'express';
diff --git a/tests/legacy-cli/e2e/utils/jest.ts b/tests/legacy-cli/e2e/utils/jest.ts
index 6431a93eb64f..ed7fe7c04aa1 100644
--- a/tests/legacy-cli/e2e/utils/jest.ts
+++ b/tests/legacy-cli/e2e/utils/jest.ts
@@ -4,6 +4,7 @@ import { updateJsonFile } from './project';
/** Updates the `test` builder in the current workspace to use Jest with the given options. */
export async function applyJestBuilder(
options: {} = {
+ polyfills: [],
tsConfig: 'tsconfig.spec.json',
},
): Promise {