Skip to content

Commit d7d946a

Browse files
committed
feat(@schematics/angular): Applications are zoneless by default
This change updates applications to omit the ZoneJS dependency by default. This change also includes the addition of `provideZoneChangeDetection` in the `initTestEnvironment` when ZoneJS is detected in the configuration (either on window or in the polyfills).
1 parent 907eabd commit d7d946a

File tree

24 files changed

+93
-93
lines changed

24 files changed

+93
-93
lines changed

packages/angular/build/src/builders/karma/application_builder.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,14 +190,18 @@ async function runEsbuild(
190190
context: BuilderContext,
191191
projectSourceRoot: string,
192192
): Promise<[Result & { kind: ResultKind.Full }, AsyncIterator<Result> | null]> {
193+
const usesZoneJS = buildOptions.polyfills?.includes('zone.js');
193194
const virtualTestBedInit = createVirtualModulePlugin({
194195
namespace: 'angular:test-bed-init',
195196
loadContent: async () => {
196197
const contents: string[] = [
197198
// Initialize the Angular testing environment
199+
`import { NgModule${usesZoneJS ? ', provideZoneChangeDetection' : ''} } from '@angular/core';`,
198200
`import { getTestBed } from '@angular/core/testing';`,
199201
`import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';`,
200-
`getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting(), {`,
202+
`@NgModule({ providers: [${usesZoneJS ? 'provideZoneChangeDetection(), ' : ''}], })`,
203+
`export class TestModule {}`,
204+
`getTestBed().initTestEnvironment([BrowserTestingModule, TestModule], platformBrowserTesting(), {`,
201205
` errorOnUnknownElements: true,`,
202206
` errorOnUnknownProperties: true,`,
203207
`});`,

packages/angular/build/src/builders/unit-test/runners/vitest/build-options.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ import { RunnerOptions } from '../api';
1717
function createTestBedInitVirtualFile(
1818
providersFile: string | undefined,
1919
projectSourceRoot: string,
20+
polyfills: string[] = [],
2021
): string {
22+
const usesZoneJS = polyfills.includes('zone.js');
2123
let providersImport = 'const providers = [];';
2224
if (providersFile) {
2325
const relativePath = path.relative(projectSourceRoot, providersFile);
@@ -28,15 +30,15 @@ function createTestBedInitVirtualFile(
2830

2931
return `
3032
// Initialize the Angular testing environment
31-
import { NgModule } from '@angular/core';
33+
import { NgModule${usesZoneJS ? ', provideZoneChangeDetection' : ''} } from '@angular/core';
3234
import { getTestBed, ɵgetCleanupHook as getCleanupHook } from '@angular/core/testing';
3335
import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';
3436
${providersImport}
3537
// Same as https://github.com/angular/angular/blob/05a03d3f975771bb59c7eefd37c01fa127ee2229/packages/core/testing/srcs/test_hooks.ts#L21-L29
3638
beforeEach(getCleanupHook(false));
3739
afterEach(getCleanupHook(true));
3840
@NgModule({
39-
providers,
41+
providers: [${usesZoneJS ? 'provideZoneChangeDetection(), ' : ''}...providers],
4042
})
4143
export class TestModule {}
4244
getTestBed().initTestEnvironment([BrowserTestingModule, TestModule], platformBrowserTesting(), {
@@ -113,7 +115,11 @@ export async function getVitestBuildOptions(
113115

114116
buildOptions.polyfills = injectTestingPolyfills(buildOptions.polyfills);
115117

116-
const testBedInitContents = createTestBedInitVirtualFile(providersFile, projectSourceRoot);
118+
const testBedInitContents = createTestBedInitVirtualFile(
119+
providersFile,
120+
projectSourceRoot,
121+
buildOptions.polyfills,
122+
);
117123

118124
return {
119125
buildOptions,

packages/angular_devkit/build_angular/src/builders/jest/init-test-bed.mjs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,16 @@
99
// TODO(dgp1130): These imports likely don't resolve in stricter package environments like `pnpm`, since they are resolved relative to
1010
// `@angular-devkit/build-angular` rather than the user's workspace. Should look into virtual modules to support those use cases.
1111

12+
import { provideZoneChangeDetection, NgModule } from '@angular/core';
1213
import { getTestBed } from '@angular/core/testing';
1314
import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';
1415

15-
getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting(), {
16+
@NgModule({
17+
providers: [typeof window.Zone !== 'undefined' ? provideZoneChangeDetection() : []],
18+
})
19+
class TestModule {}
20+
21+
getTestBed().initTestEnvironment([BrowserTestingModule, TestModule], platformBrowserTesting(), {
1622
errorOnUnknownElements: true,
1723
errorOnUnknownProperties: true,
1824
});

packages/angular_devkit/build_angular/src/builders/karma/browser_builder.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,14 +149,18 @@ async function initializeBrowser(
149149
return [karma, (await webpackConfigurationTransformer?.(config)) ?? config];
150150
}
151151

152-
function getBuiltInMainFile(): string {
152+
function getBuiltInMainFile(includeZoneProvider = false): string {
153153
const content = Buffer.from(
154154
`
155+
import { provideZoneChangeDetection, ɵcompileNgModuleDefs as compileNgModuleDefs } from '@angular/core';
155156
import { getTestBed } from '@angular/core/testing';
156157
import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';
157158
159+
export class TestModule {}
160+
compileNgModuleDefs(TestModule, {providers: [${includeZoneProvider ? 'provideZoneChangeDetection()' : ''}]});
161+
158162
// Initialize the Angular testing environment.
159-
getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting(), {
163+
getTestBed().initTestEnvironment([BrowserTestingModule, TestModule], platformBrowserTesting(), {
160164
errorOnUnknownElements: true,
161165
errorOnUnknownProperties: true
162166
});

packages/angular_devkit/build_angular/src/builders/web-test-runner/jasmine_runner.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
import { NgModule } from '@angular/core';
910
import { getTestBed } from '@angular/core/testing';
1011
import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';
1112
import {
@@ -63,8 +64,13 @@ export async function runJasmineTests(jasmineEnv) {
6364
// eslint-disable-next-line no-undef
6465
jasmine.DEFAULT_TIMEOUT_INTERVAL = config.defaultTimeoutInterval;
6566

67+
@NgModule({
68+
providers: [typeof window.Zone !== 'undefined' ? provideZoneChangeDetection() : []],
69+
})
70+
class TestModule {}
71+
6672
// Initialize `TestBed` automatically for users. This assumes we already evaluated `zone.js/testing`.
67-
getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting(), {
73+
getTestBed().initTestEnvironment([BrowserTestingModule, TestModule], platformBrowserTesting(), {
6874
errorOnUnknownElements: true,
6975
errorOnUnknownProperties: true,
7076
});

packages/schematics/angular/application/files/module-files/src/app/app-module.ts.template

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { NgModule, provideBrowserGlobalErrorListeners<% if(zoneless) { %>, provideZonelessChangeDetection<% } %> } from '@angular/core';
1+
import { NgModule, provideBrowserGlobalErrorListeners } from '@angular/core';
22
import { BrowserModule } from '@angular/platform-browser';
33
<% if (routing) { %>
44
import { AppRoutingModule } from './app-routing-module';<% } %>
@@ -13,8 +13,7 @@ import { App } from './app';
1313
AppRoutingModule<% } %>
1414
],
1515
providers: [
16-
provideBrowserGlobalErrorListeners()<% if (zoneless) { %>,
17-
provideZonelessChangeDetection()<% } %>
16+
provideBrowserGlobalErrorListeners()
1817
],
1918
bootstrap: [App]
2019
})

packages/schematics/angular/application/files/module-files/src/app/app__suffix__.spec.ts.template

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
<% if(zoneless) { %>import { provideZonelessChangeDetection } from '@angular/core';
2-
<% } %>import { TestBed } from '@angular/core/testing';<% if (routing) { %>
1+
import { TestBed } from '@angular/core/testing';<% if (routing) { %>
32
import { RouterModule } from '@angular/router';<% } %>
43
import { App } from './app';
54

@@ -11,8 +10,7 @@ describe('App', () => {
1110
],<% } %>
1211
declarations: [
1312
App
14-
],<% if(zoneless) { %>
15-
providers: [provideZonelessChangeDetection()]<% } %>
13+
],
1614
}).compileComponents();
1715
});
1816

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { ApplicationConfig, provideBrowserGlobalErrorListeners, <% if(!zoneless) { %>provideZoneChangeDetection<% } else { %>provideZonelessChangeDetection<% } %> } from '@angular/core';<% if (routing) { %>
1+
import { ApplicationConfig, provideBrowserGlobalErrorListeners<% if(!zoneless) { %>, provideZoneChangeDetection<% } %> } from '@angular/core';<% if (routing) { %>
22
import { provideRouter } from '@angular/router';
33

44
import { routes } from './app.routes';<% } %>
55

66
export const appConfig: ApplicationConfig = {
77
providers: [
8-
provideBrowserGlobalErrorListeners(),
9-
<% if(zoneless) { %>provideZonelessChangeDetection()<% } else { %>provideZoneChangeDetection({ eventCoalescing: true })<% } %>,
8+
provideBrowserGlobalErrorListeners(),<% if(!zoneless) { %>
9+
provideZoneChangeDetection({ eventCoalescing: true }),<% } %>
1010
<% if (routing) {%>provideRouter(routes)<% } %>
1111
]
1212
};

packages/schematics/angular/application/files/standalone-files/src/app/app__suffix__.spec.ts.template

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
<% if(zoneless) { %>import { provideZonelessChangeDetection } from '@angular/core';
2-
<% } %>import { TestBed } from '@angular/core/testing';
1+
import { TestBed } from '@angular/core/testing';
32
import { App } from './app';
43

54
describe('App', () => {
65
beforeEach(async () => {
76
await TestBed.configureTestingModule({
8-
imports: [App],<% if(zoneless) { %>
9-
providers: [provideZonelessChangeDetection()]<% } %>
7+
imports: [App],
108
}).compileComponents();
119
});
1210

packages/schematics/angular/application/index_spec.ts

Lines changed: 11 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ describe('Application Schematic', () => {
296296
expect(pkg.devDependencies['less']).toEqual(latestVersions['less']);
297297
});
298298

299-
it('should include zone.js if "zoneless" option is not present', async () => {
299+
it('should _not_ include zone.js if "zoneless" option is not present', async () => {
300300
const tree = await schematicRunner.runSchematic(
301301
'application',
302302
{
@@ -307,7 +307,7 @@ describe('Application Schematic', () => {
307307
);
308308

309309
const pkg = JSON.parse(tree.readContent('/package.json'));
310-
expect(pkg.dependencies['zone.js']).toEqual(latestVersions['zone.js']);
310+
expect(pkg.dependencies['zone.js']).toBeUndefined();
311311
});
312312

313313
it('should not include zone.js if "zoneless" option is true', async () => {
@@ -389,7 +389,7 @@ describe('Application Schematic', () => {
389389
expect(buildOpt.index).toBeUndefined();
390390
expect(buildOpt.browser).toEqual('src/main.ts');
391391
expect(buildOpt.assets).toEqual([{ 'glob': '**/*', 'input': 'public' }]);
392-
expect(buildOpt.polyfills).toEqual(['zone.js']);
392+
expect(buildOpt.polyfills).toEqual(undefined);
393393
expect(buildOpt.tsConfig).toEqual('tsconfig.app.json');
394394

395395
const testOpt = prj.architect.test.options;
@@ -478,7 +478,7 @@ describe('Application Schematic', () => {
478478
expect(project.root).toEqual('foo');
479479
const buildOpt = project.architect.build.options;
480480
expect(buildOpt.browser).toEqual('foo/src/main.ts');
481-
expect(buildOpt.polyfills).toEqual(['zone.js']);
481+
expect(buildOpt.polyfills).toEqual(undefined);
482482
expect(buildOpt.tsConfig).toEqual('foo/tsconfig.app.json');
483483
expect(buildOpt.assets).toEqual([{ 'glob': '**/*', 'input': 'foo/public' }]);
484484

@@ -650,8 +650,8 @@ describe('Application Schematic', () => {
650650
expect(moduleFiles.length).toEqual(0);
651651
});
652652

653-
it('should enable zone event coalescing by default', async () => {
654-
const options = { ...defaultOptions, standalone: true };
653+
it('should enable zone event coalescing by default for zone.js apps', async () => {
654+
const options = { ...defaultOptions, standalone: true, zoneless: false };
655655

656656
const tree = await schematicRunner.runSchematic('application', options, workspaceTree);
657657
const appConfig = tree.readContent('/projects/foo/src/app/app.config.ts');
@@ -692,12 +692,13 @@ describe('Application Schematic', () => {
692692
});
693693

694694
describe('standalone=false', () => {
695-
it('should add the ngZoneEventCoalescing option by default', async () => {
695+
it('should add the ngZoneEventCoalescing option by default with zone.js apps', async () => {
696696
const tree = await schematicRunner.runSchematic(
697697
'application',
698698
{
699699
...defaultOptions,
700700
standalone: false,
701+
zoneless: false,
701702
},
702703
workspaceTree,
703704
);
@@ -800,7 +801,7 @@ describe('Application Schematic', () => {
800801
);
801802
});
802803

803-
it('should add provideZonelessChangeDetection() in app-module.ts when zoneless is true', async () => {
804+
it('should not add provideZonelessChangeDetection() in app-module.ts when zoneless is true', async () => {
804805
const tree = await schematicRunner.runSchematic(
805806
'application',
806807
{
@@ -812,53 +813,10 @@ describe('Application Schematic', () => {
812813
);
813814
const path = '/projects/foo/src/app/app-module.ts';
814815
const fileContent = tree.readContent(path);
815-
expect(fileContent).toContain('provideZonelessChangeDetection()');
816-
});
817-
818-
it('should not add provideZonelessChangeDetection() in app-module.ts when zoneless is false', async () => {
819-
const tree = await schematicRunner.runSchematic(
820-
'application',
821-
{
822-
...defaultOptions,
823-
zoneless: false,
824-
standalone: false,
825-
},
826-
workspaceTree,
827-
);
828-
const path = '/projects/foo/src/app/app-module.ts';
829-
const fileContent = tree.readContent(path);
830-
expect(fileContent).not.toContain('provideZonelessChangeDetection()');
831-
});
832-
833-
it('should add provideZonelessChangeDetection() when zoneless is true', async () => {
834-
const tree = await schematicRunner.runSchematic(
835-
'application',
836-
{
837-
...defaultOptions,
838-
zoneless: true,
839-
},
840-
workspaceTree,
841-
);
842-
const path = '/projects/foo/src/app/app.config.ts';
843-
const fileContent = tree.readContent(path);
844-
expect(fileContent).toContain('provideZonelessChangeDetection()');
845-
});
846-
847-
it('should not add provideZonelessChangeDetection() when zoneless is false', async () => {
848-
const tree = await schematicRunner.runSchematic(
849-
'application',
850-
{
851-
...defaultOptions,
852-
zoneless: false,
853-
},
854-
workspaceTree,
855-
);
856-
const path = '/projects/foo/src/app/app.config.ts';
857-
const fileContent = tree.readContent(path);
858816
expect(fileContent).not.toContain('provideZonelessChangeDetection()');
859817
});
860818

861-
it('should not add provideZoneChangeDetection when zoneless is true', async () => {
819+
it('should not add any change detection provider when zoneless is true', async () => {
862820
const tree = await schematicRunner.runSchematic(
863821
'application',
864822
{
@@ -869,7 +827,7 @@ describe('Application Schematic', () => {
869827
);
870828
const path = '/projects/foo/src/app/app.config.ts';
871829
const fileContent = tree.readContent(path);
872-
expect(fileContent).not.toContain('provideZoneChangeDetection');
830+
expect(fileContent).not.toMatch(/provideZone(less)?ChangeDetection/gi);
873831
});
874832
});
875833

0 commit comments

Comments
 (0)