From 755f3a07f5fe485c1ed8c0c6060d6d5c799c085c Mon Sep 17 00:00:00 2001 From: aparziale Date: Tue, 15 Oct 2024 13:09:08 +0200 Subject: [PATCH] feat(@schematics/angular): add option to setup new workspace or application as zoneless mode --- .../src/app/app.component.spec.ts.template | 3 +- .../src/app/app.module.ts.template | 4 +- .../files/module-files/src/main.ts.template | 3 +- .../src/app/app.component.spec.ts.template | 2 + .../src/app/app.config.ts.template | 6 +- .../schematics/angular/application/index.ts | 4 +- .../angular/application/index_spec.ts | 72 +++++++++++++++++++ .../angular/application/schema.json | 5 ++ packages/schematics/angular/ng-new/index.ts | 1 + .../schematics/angular/ng-new/schema.json | 5 ++ .../angular/service-worker/index_spec.ts | 2 +- .../application/application-zoneless.ts | 21 ++++++ 12 files changed, 118 insertions(+), 10 deletions(-) create mode 100644 tests/legacy-cli/e2e/tests/generate/application/application-zoneless.ts diff --git a/packages/schematics/angular/application/files/module-files/src/app/app.component.spec.ts.template b/packages/schematics/angular/application/files/module-files/src/app/app.component.spec.ts.template index c567ade0de2c..22fc41727c65 100644 --- a/packages/schematics/angular/application/files/module-files/src/app/app.component.spec.ts.template +++ b/packages/schematics/angular/application/files/module-files/src/app/app.component.spec.ts.template @@ -1,4 +1,4 @@ -import { TestBed } from '@angular/core/testing';<% if (routing) { %> +<% if(experimentalZoneless) { %> import { provideExperimentalZonelessChangeDetection } from '@angular/core'; <% } %>import { TestBed } from '@angular/core/testing';<% if (routing) { %> import { RouterModule } from '@angular/router';<% } %> import { AppComponent } from './app.component'; @@ -11,6 +11,7 @@ describe('AppComponent', () => { declarations: [ AppComponent ], + <% if(experimentalZoneless) { %>providers: [provideExperimentalZonelessChangeDetection()]<% } %> }).compileComponents(); }); diff --git a/packages/schematics/angular/application/files/module-files/src/app/app.module.ts.template b/packages/schematics/angular/application/files/module-files/src/app/app.module.ts.template index 88ee10b8a0e8..4279b6581c35 100644 --- a/packages/schematics/angular/application/files/module-files/src/app/app.module.ts.template +++ b/packages/schematics/angular/application/files/module-files/src/app/app.module.ts.template @@ -1,4 +1,4 @@ -import { NgModule } from '@angular/core'; +import { NgModule<% if(experimentalZoneless) { %>, provideExperimentalZonelessChangeDetection<% } %> } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; <% if (routing) { %> import { AppRoutingModule } from './app-routing.module';<% } %> @@ -12,7 +12,7 @@ import { AppComponent } from './app.component'; BrowserModule<% if (routing) { %>, AppRoutingModule<% } %> ], - providers: [], + providers: [<% if (experimentalZoneless) { %>provideExperimentalZonelessChangeDetection()<% } %>], bootstrap: [AppComponent] }) export class AppModule { } diff --git a/packages/schematics/angular/application/files/module-files/src/main.ts.template b/packages/schematics/angular/application/files/module-files/src/main.ts.template index ac03d58c6153..a4e16b837f8a 100644 --- a/packages/schematics/angular/application/files/module-files/src/main.ts.template +++ b/packages/schematics/angular/application/files/module-files/src/main.ts.template @@ -1,10 +1,9 @@ <% if(!!viewEncapsulation) { %>import { ViewEncapsulation } from '@angular/core'; <% }%>import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; - import { AppModule } from './app/app.module'; platformBrowserDynamic().bootstrapModule(AppModule, { - ngZoneEventCoalescing: true<% if(!!viewEncapsulation) { %>, + <% if(!experimentalZoneless) { %>ngZoneEventCoalescing: true,<% } %><% if(!!viewEncapsulation) { %> defaultEncapsulation: ViewEncapsulation.<%= viewEncapsulation %><% } %> }) .catch(err => console.error(err)); diff --git a/packages/schematics/angular/application/files/standalone-files/src/app/app.component.spec.ts.template b/packages/schematics/angular/application/files/standalone-files/src/app/app.component.spec.ts.template index fad53115332e..b428b8e81cba 100644 --- a/packages/schematics/angular/application/files/standalone-files/src/app/app.component.spec.ts.template +++ b/packages/schematics/angular/application/files/standalone-files/src/app/app.component.spec.ts.template @@ -1,3 +1,4 @@ +<% if(experimentalZoneless) { %> import { provideExperimentalZonelessChangeDetection } from '@angular/core'; <% } %> import { TestBed } from '@angular/core/testing'; import { AppComponent } from './app.component'; @@ -5,6 +6,7 @@ describe('AppComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [AppComponent], + <% if(experimentalZoneless) { %>providers: [provideExperimentalZonelessChangeDetection()]<% } %> }).compileComponents(); }); diff --git a/packages/schematics/angular/application/files/standalone-files/src/app/app.config.ts.template b/packages/schematics/angular/application/files/standalone-files/src/app/app.config.ts.template index b3d5dd14acd7..26c02f5e489a 100644 --- a/packages/schematics/angular/application/files/standalone-files/src/app/app.config.ts.template +++ b/packages/schematics/angular/application/files/standalone-files/src/app/app.config.ts.template @@ -1,8 +1,10 @@ -import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';<% if (routing) { %> +import { ApplicationConfig, <% if(!experimentalZoneless) { %>provideZoneChangeDetection<% } else { %>provideExperimentalZonelessChangeDetection<% } %> } from '@angular/core';<% if (routing) { %> import { provideRouter } from '@angular/router'; import { routes } from './app.routes';<% } %> export const appConfig: ApplicationConfig = { - providers: [provideZoneChangeDetection({ eventCoalescing: true })<% if (routing) { %>, provideRouter(routes)<% } %>] + providers: [ + <% if(experimentalZoneless) { %>provideExperimentalZonelessChangeDetection()<% } else { %>provideZoneChangeDetection({ eventCoalescing: true })<% } %><% if (routing) {%>, provideRouter(routes)<% } %> + ] }; diff --git a/packages/schematics/angular/application/index.ts b/packages/schematics/angular/application/index.ts index 2d3f4f620ab8..3bd591f8eeba 100644 --- a/packages/schematics/angular/application/index.ts +++ b/packages/schematics/angular/application/index.ts @@ -239,7 +239,7 @@ function addAppToWorkspaceFile( outputPath: `dist/${folderName}`, index: `${sourceRoot}/index.html`, browser: `${sourceRoot}/main.ts`, - polyfills: ['zone.js'], + polyfills: options.experimentalZoneless ? [] : ['zone.js'], tsConfig: `${projectRoot}tsconfig.app.json`, inlineStyleLanguage, assets: [{ 'glob': '**/*', 'input': `${projectRoot}public` }], @@ -279,7 +279,7 @@ function addAppToWorkspaceFile( : { builder: Builders.Karma, options: { - polyfills: ['zone.js', 'zone.js/testing'], + polyfills: options.experimentalZoneless ? [] : ['zone.js', 'zone.js/testing'], tsConfig: `${projectRoot}tsconfig.spec.json`, inlineStyleLanguage, assets: [{ 'glob': '**/*', 'input': `${projectRoot}public` }], diff --git a/packages/schematics/angular/application/index_spec.ts b/packages/schematics/angular/application/index_spec.ts index 0bbfc60f5c43..fd287a8ffc8f 100644 --- a/packages/schematics/angular/application/index_spec.ts +++ b/packages/schematics/angular/application/index_spec.ts @@ -698,5 +698,77 @@ describe('Application Schematic', () => { }), ); }); + + it('should add provideExperimentalZonelessChangeDetection() in app.module.ts when experimentalZoneless is true', async () => { + const tree = await schematicRunner.runSchematic( + 'application', + { + ...defaultOptions, + experimentalZoneless: true, + standalone: false, + }, + workspaceTree, + ); + const path = '/projects/foo/src/app/app.module.ts'; + const fileContent = tree.readContent(path); + expect(fileContent).toContain('provideExperimentalZonelessChangeDetection()'); + }); + + it('should not add provideExperimentalZonelessChangeDetection() in app.module.ts when experimentalZoneless is false', async () => { + const tree = await schematicRunner.runSchematic( + 'application', + { + ...defaultOptions, + experimentalZoneless: false, + standalone: false, + }, + workspaceTree, + ); + const path = '/projects/foo/src/app/app.module.ts'; + const fileContent = tree.readContent(path); + expect(fileContent).not.toContain('provideExperimentalZonelessChangeDetection()'); + }); + + it('should add provideExperimentalZonelessChangeDetection() when experimentalZoneless is true', async () => { + const tree = await schematicRunner.runSchematic( + 'application', + { + ...defaultOptions, + experimentalZoneless: true, + }, + workspaceTree, + ); + const path = '/projects/foo/src/app/app.config.ts'; + const fileContent = tree.readContent(path); + expect(fileContent).toContain('provideExperimentalZonelessChangeDetection()'); + }); + + it('should not add provideExperimentalZonelessChangeDetection() when experimentalZoneless is false', async () => { + const tree = await schematicRunner.runSchematic( + 'application', + { + ...defaultOptions, + experimentalZoneless: false, + }, + workspaceTree, + ); + const path = '/projects/foo/src/app/app.config.ts'; + const fileContent = tree.readContent(path); + expect(fileContent).not.toContain('provideExperimentalZonelessChangeDetection()'); + }); + + it('should not add provideZoneChangeDetection when experimentalZoneless is true', async () => { + const tree = await schematicRunner.runSchematic( + 'application', + { + ...defaultOptions, + experimentalZoneless: true, + }, + workspaceTree, + ); + const path = '/projects/foo/src/app/app.config.ts'; + const fileContent = tree.readContent(path); + expect(fileContent).not.toContain('provideZoneChangeDetection'); + }); }); }); diff --git a/packages/schematics/angular/application/schema.json b/packages/schematics/angular/application/schema.json index 579cc03878cd..fe617a5a2824 100644 --- a/packages/schematics/angular/application/schema.json +++ b/packages/schematics/angular/application/schema.json @@ -117,6 +117,11 @@ "type": "boolean", "default": false, "x-user-analytics": "ep.ng_ssr" + }, + "experimentalZoneless": { + "description": "Create an application that does not utilize zone.js.", + "type": "boolean", + "default": false } }, "required": ["name"] diff --git a/packages/schematics/angular/ng-new/index.ts b/packages/schematics/angular/ng-new/index.ts index 3bb9da1a71ee..f5e37ccccf40 100644 --- a/packages/schematics/angular/ng-new/index.ts +++ b/packages/schematics/angular/ng-new/index.ts @@ -57,6 +57,7 @@ export default function (options: NgNewOptions): Rule { minimal: options.minimal, standalone: options.standalone, ssr: options.ssr, + experimentalZoneless: options.experimentalZoneless, }; return chain([ diff --git a/packages/schematics/angular/ng-new/schema.json b/packages/schematics/angular/ng-new/schema.json index c9634a14c893..44d64cdcdb8d 100644 --- a/packages/schematics/angular/ng-new/schema.json +++ b/packages/schematics/angular/ng-new/schema.json @@ -138,6 +138,11 @@ "description": "Creates an application with Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering) enabled.", "type": "boolean", "x-user-analytics": "ep.ng_ssr" + }, + "experimentalZoneless": { + "description": "Create an application that does not utilize zone.js.", + "type": "boolean", + "default": false } }, "required": ["name", "version"] diff --git a/packages/schematics/angular/service-worker/index_spec.ts b/packages/schematics/angular/service-worker/index_spec.ts index 650f5a9597f6..87f3a9aafc46 100644 --- a/packages/schematics/angular/service-worker/index_spec.ts +++ b/packages/schematics/angular/service-worker/index_spec.ts @@ -131,7 +131,7 @@ describe('Service Worker Schematic', () => { provideServiceWorker('ngsw-worker.js', { enabled: !isDevMode(), registrationStrategy: 'registerWhenStable:30000' - }) + }) `); }); diff --git a/tests/legacy-cli/e2e/tests/generate/application/application-zoneless.ts b/tests/legacy-cli/e2e/tests/generate/application/application-zoneless.ts new file mode 100644 index 000000000000..8f92eac3e7cf --- /dev/null +++ b/tests/legacy-cli/e2e/tests/generate/application/application-zoneless.ts @@ -0,0 +1,21 @@ +import { ng } from '../../../utils/process'; +import { useCIChrome } from '../../../utils/project'; + +export default async function () { + await ng('generate', 'app', 'standalone', '--experimental-zoneless', '--standalone'); + await useCIChrome('standalone', 'projects/standalone'); + await ng('test', 'standalone', '--watch=false'); + await ng('build', 'standalone'); + + await ng( + 'generate', + 'app', + 'ngmodules', + '--experimental-zoneless', + '--no-standalone', + '--skip-install', + ); + await useCIChrome('ngmodules', 'projects/ngmodules'); + await ng('test', 'ngmodules', '--watch=false'); + await ng('build', 'ngmodules'); +}