From 1de6d71edd899465a01c65790f6fb04159acc821 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Fri, 26 Feb 2021 10:42:55 +0100 Subject: [PATCH] feat(@schematics/angular): production builds by default With this change we do several changes to the `angular.json` configuration for `build` , `server` and `app-shell` targets so that these are `production` by default. - build, server and app-shell targets are configured to run production by default. - We add a new configuration named `development` to run the mentioned builder targets in development. Ex: `ng build --configuration development`. - When adding `universal` or `app-shell`, we generate the full set of configurations as per the `buiid` target. Previously, we only generated the `production` configuration. - We added a helper script in `package.json` to run build in watch mode. `npm run watch` which is a shortcut for `ng build --watch --configuration development` --- packages/angular/pwa/pwa/index_spec.ts | 2 +- .../schematics/angular/app-shell/index.ts | 56 ++++++++++++----- .../angular/app-shell/index_spec.ts | 4 +- .../schematics/angular/application/index.ts | 16 +++-- packages/schematics/angular/e2e/index.ts | 5 +- packages/schematics/angular/e2e/index_spec.ts | 6 +- packages/schematics/angular/library/index.ts | 5 +- .../schematics/angular/library/index_spec.ts | 14 ++--- .../migrations/update-9/update-i18n_spec.ts | 2 + .../update-9/update-workspace-config_spec.ts | 4 ++ .../angular/service-worker/index.ts | 17 ++--- .../angular/service-worker/index_spec.ts | 28 +++------ .../angular/service-worker/schema.json | 2 +- .../schematics/angular/universal/index.ts | 62 ++++++++++--------- .../angular/universal/index_spec.ts | 10 +-- .../workspace/files/package.json.template | 3 +- 16 files changed, 128 insertions(+), 108 deletions(-) diff --git a/packages/angular/pwa/pwa/index_spec.ts b/packages/angular/pwa/pwa/index_spec.ts index b3a54fffcbcc..ea8dd9f5e461 100644 --- a/packages/angular/pwa/pwa/index_spec.ts +++ b/packages/angular/pwa/pwa/index_spec.ts @@ -52,7 +52,7 @@ describe('PWA Schematic', () => { schematicRunner.runSchematicAsync('ng-add', defaultOptions, appTree).toPromise().then(tree => { const configText = tree.readContent('/angular.json'); const config = JSON.parse(configText); - const swFlag = config.projects.bar.architect.build.configurations.production.serviceWorker; + const swFlag = config.projects.bar.architect.build.options.serviceWorker; expect(swFlag).toEqual(true); done(); }, done.fail); diff --git a/packages/schematics/angular/app-shell/index.ts b/packages/schematics/angular/app-shell/index.ts index 5afa57a707ad..8a1fc629af56 100644 --- a/packages/schematics/angular/app-shell/index.ts +++ b/packages/schematics/angular/app-shell/index.ts @@ -155,7 +155,7 @@ function addUniversalTarget(options: AppShellOptions): Rule { } function addAppShellConfigToWorkspace(options: AppShellOptions): Rule { - return () => { + return (host, context) => { if (!options.route) { throw new SchematicsException(`Route is not defined`); } @@ -166,20 +166,44 @@ function addAppShellConfigToWorkspace(options: AppShellOptions): Rule { return; } + // Validation of targets is handled already in the main function. + // Duplicate keys means that we have configurations in both server and build builders. + const serverConfigKeys = project.targets.get('server')?.configurations ?? {}; + const buildConfigKeys = project.targets.get('build')?.configurations ?? {}; + + const configurationNames = Object.keys({ + ...serverConfigKeys, + ...buildConfigKeys, + }); + + const configurations: Record = {}; + for (const key of configurationNames) { + if (!serverConfigKeys[key]) { + context.logger.warn(`Skipped adding "${key}" configuration to "app-shell" target as it's missing from "server" target.`); + + continue; + } + + if (!buildConfigKeys[key]) { + context.logger.warn(`Skipped adding "${key}" configuration to "app-shell" target as it's missing from "build" target.`); + + continue; + } + + configurations[key] = { + browserTarget: `${options.clientProject}:build:${key}`, + serverTarget: `${options.clientProject}:server:${key}`, + }; + } + project.targets.add({ name: 'app-shell', builder: Builders.AppShell, + defaultConfiguration: configurations['production'] ? 'production' : undefined, options: { - browserTarget: `${options.clientProject}:build`, - serverTarget: `${options.clientProject}:server`, route: options.route, }, - configurations: { - production: { - browserTarget: `${options.clientProject}:build:production`, - serverTarget: `${options.clientProject}:server:production`, - }, - }, + configurations, }); }); }; @@ -242,9 +266,9 @@ function addServerRoutes(options: AppShellOptions): Rule { if (!isImported(moduleSource, 'Routes', '@angular/router')) { const recorder = host.beginUpdate(modulePath); const routesChange = insertImport(moduleSource, - modulePath, - 'Routes', - '@angular/router'); + modulePath, + 'Routes', + '@angular/router'); if (routesChange) { applyToUpdateRecorder(recorder, [routesChange]); } @@ -263,16 +287,16 @@ function addServerRoutes(options: AppShellOptions): Rule { if (!isImported(moduleSource, 'RouterModule', '@angular/router')) { const recorder = host.beginUpdate(modulePath); const routerModuleChange = insertImport(moduleSource, - modulePath, - 'RouterModule', - '@angular/router'); + modulePath, + 'RouterModule', + '@angular/router'); if (routerModuleChange) { applyToUpdateRecorder(recorder, [routerModuleChange]); } const metadataChange = addSymbolToNgModuleMetadata( - moduleSource, modulePath, 'imports', 'RouterModule.forRoot(routes)'); + moduleSource, modulePath, 'imports', 'RouterModule.forRoot(routes)'); if (metadataChange) { applyToUpdateRecorder(recorder, metadataChange); } diff --git a/packages/schematics/angular/app-shell/index_spec.ts b/packages/schematics/angular/app-shell/index_spec.ts index 16c7d3e756ec..25227f0ddb68 100644 --- a/packages/schematics/angular/app-shell/index_spec.ts +++ b/packages/schematics/angular/app-shell/index_spec.ts @@ -68,9 +68,9 @@ describe('App Shell Schematic', () => { const content = tree.readContent(filePath); const workspace = JSON.parse(content); const target = workspace.projects.bar.architect['app-shell']; - expect(target.options.browserTarget).toEqual('bar:build'); - expect(target.options.serverTarget).toEqual('bar:server'); expect(target.options.route).toEqual('shell'); + expect(target.configurations.development.browserTarget).toEqual('bar:build:development'); + expect(target.configurations.development.serverTarget).toEqual('bar:server:development'); expect(target.configurations.production.browserTarget).toEqual('bar:build:production'); expect(target.configurations.production.serverTarget).toEqual('bar:server:production'); }); diff --git a/packages/schematics/angular/application/index.ts b/packages/schematics/angular/application/index.ts index 93053a0e6086..952dc63ae01e 100644 --- a/packages/schematics/angular/application/index.ts +++ b/packages/schematics/angular/application/index.ts @@ -172,6 +172,7 @@ function addAppToWorkspaceFile(options: ApplicationOptions, appDir: string): Rul targets: { build: { builder: Builders.Browser, + defaultConfiguration: 'production', options: { outputPath: `dist/${options.name}`, index: `${sourceRoot}/index.html`, @@ -190,30 +191,35 @@ function addAppToWorkspaceFile(options: ApplicationOptions, appDir: string): Rul }, configurations: { production: { + budgets, fileReplacements: [{ replace: `${sourceRoot}/environments/environment.ts`, with: `${sourceRoot}/environments/environment.prod.ts`, }], + buildOptimizer: true, optimization: true, outputHashing: 'all', sourceMap: false, namedChunks: false, extractLicenses: true, vendorChunk: false, - buildOptimizer: true, - budgets, + }, + development: { + vendorChunk: true, }, }, }, serve: { builder: Builders.DevServer, - options: { - browserTarget: `${options.name}:build`, - }, + defaultConfiguration: 'development', + options: {}, configurations: { production: { browserTarget: `${options.name}:build:production`, }, + development: { + browserTarget: `${options.name}:build:development`, + }, }, }, 'extract-i18n': { diff --git a/packages/schematics/angular/e2e/index.ts b/packages/schematics/angular/e2e/index.ts index 2e6f56bb78e5..fc7aaf2092c7 100644 --- a/packages/schematics/angular/e2e/index.ts +++ b/packages/schematics/angular/e2e/index.ts @@ -48,14 +48,17 @@ export default function (options: E2eOptions): Rule { project.targets.add({ name: 'e2e', builder: Builders.Protractor, + defaultConfiguration: 'development', options: { protractorConfig: `${root}/protractor.conf.js`, - devServerTarget: `${options.relatedAppName}:serve`, }, configurations: { production: { devServerTarget: `${options.relatedAppName}:serve:production`, }, + development: { + devServerTarget: `${options.relatedAppName}:serve:development`, + }, }, }); diff --git a/packages/schematics/angular/e2e/index_spec.ts b/packages/schematics/angular/e2e/index_spec.ts index fb63f66adea4..d82b82c2af05 100644 --- a/packages/schematics/angular/e2e/index_spec.ts +++ b/packages/schematics/angular/e2e/index_spec.ts @@ -94,9 +94,9 @@ describe('Application Schematic', () => { const tree = await schematicRunner.runSchematicAsync('e2e', defaultOptions, applicationTree) .toPromise(); const workspace = JSON.parse(tree.readContent('/angular.json')); - const e2eOptions = workspace.projects.foo.architect.e2e.options; - expect(e2eOptions.protractorConfig).toEqual('projects/foo/e2e/protractor.conf.js'); - expect(e2eOptions.devServerTarget).toEqual('foo:serve'); + const { options, configurations } = workspace.projects.foo.architect.e2e; + expect(options.protractorConfig).toEqual('projects/foo/e2e/protractor.conf.js'); + expect(configurations.development.devServerTarget).toEqual('foo:serve:development'); }); }); diff --git a/packages/schematics/angular/library/index.ts b/packages/schematics/angular/library/index.ts index 6e1bc5ebabe4..318de1f1de21 100644 --- a/packages/schematics/angular/library/index.ts +++ b/packages/schematics/angular/library/index.ts @@ -95,14 +95,17 @@ function addLibToWorkspaceFile( targets: { build: { builder: Builders.NgPackagr, + defaultConfiguration: 'production', options: { - tsConfig: `${projectRoot}/tsconfig.lib.json`, project: `${projectRoot}/ng-package.json`, }, configurations: { production: { tsConfig: `${projectRoot}/tsconfig.lib.prod.json`, }, + development: { + tsConfig: `${projectRoot}/tsconfig.lib.json`, + }, }, }, test: { diff --git a/packages/schematics/angular/library/index_spec.ts b/packages/schematics/angular/library/index_spec.ts index f3a847fe4a20..016a2291b78d 100644 --- a/packages/schematics/angular/library/index_spec.ts +++ b/packages/schematics/angular/library/index_spec.ts @@ -309,22 +309,22 @@ describe('Library Schematic', () => { const config = getJsonFileContent(tree, '/angular.json'); const project = config.projects.foo; expect(project.root).toEqual('foo'); - const buildOpt = project.architect.build.options; - expect(buildOpt.project).toEqual('foo/ng-package.json'); - expect(buildOpt.tsConfig).toEqual('foo/tsconfig.lib.json'); + const { options, configurations } = project.architect.build; + expect(options.project).toEqual('foo/ng-package.json'); + expect(configurations.production.tsConfig).toEqual('foo/tsconfig.lib.prod.json'); - const appTsConfig = getJsonFileContent(tree, '/foo/tsconfig.lib.json'); - expect(appTsConfig.extends).toEqual('../tsconfig.json'); + const libTsConfig = getJsonFileContent(tree, '/foo/tsconfig.lib.json'); + expect(libTsConfig.extends).toEqual('../tsconfig.json'); const specTsConfig = getJsonFileContent(tree, '/foo/tsconfig.spec.json'); expect(specTsConfig.extends).toEqual('../tsconfig.json'); }); - it(`should add 'production' configuration`, async () => { + it(`should add 'development' configuration`, async () => { const tree = await schematicRunner.runSchematicAsync('library', defaultOptions, workspaceTree) .toPromise(); const workspace = JSON.parse(tree.readContent('/angular.json')); - expect(workspace.projects.foo.architect.build.configurations.production).toBeDefined(); + expect(workspace.projects.foo.architect.build.configurations.development).toBeDefined(); }); it(`should add 'ng-packagr' builder`, async () => { diff --git a/packages/schematics/angular/migrations/update-9/update-i18n_spec.ts b/packages/schematics/angular/migrations/update-9/update-i18n_spec.ts index af3464533fb2..f90e754c696d 100644 --- a/packages/schematics/angular/migrations/update-9/update-i18n_spec.ts +++ b/packages/schematics/angular/migrations/update-9/update-i18n_spec.ts @@ -49,6 +49,8 @@ describe('Migration to version 9', () => { tree, ) .toPromise(); + + tree.overwrite('angular.json', tree.readContent('angular.json').replace(/development/g, 'production')); }); describe('i18n configuration', () => { diff --git a/packages/schematics/angular/migrations/update-9/update-workspace-config_spec.ts b/packages/schematics/angular/migrations/update-9/update-workspace-config_spec.ts index 9523041460df..58e6101ce49c 100644 --- a/packages/schematics/angular/migrations/update-9/update-workspace-config_spec.ts +++ b/packages/schematics/angular/migrations/update-9/update-workspace-config_spec.ts @@ -93,6 +93,8 @@ describe('Migration to version 9', () => { ); tree.overwrite('tsconfig.app.json', tsConfig); + + tree.overwrite('angular.json', tree.readContent('angular.json').replace(/development/g, 'production')); }); describe('scripts and style options', () => { @@ -277,6 +279,8 @@ describe('Migration to version 9', () => { tree, ) .toPromise(); + + tree.overwrite('angular.json', tree.readContent('angular.json').replace(/development/g, 'production')); }); it('should add optimization option when not defined', async () => { diff --git a/packages/schematics/angular/service-worker/index.ts b/packages/schematics/angular/service-worker/index.ts index 169e789c4476..650d21206676 100644 --- a/packages/schematics/angular/service-worker/index.ts +++ b/packages/schematics/angular/service-worker/index.ts @@ -130,21 +130,12 @@ export default function (options: ServiceWorkerOptions): Rule { if (!buildTarget) { throw targetBuildNotFoundError(); } - const buildOptions = - (buildTarget.options || {}) as unknown as BrowserBuilderOptions; - let buildConfiguration; - if (options.configuration && buildTarget.configurations) { - buildConfiguration = - buildTarget.configurations[options.configuration] as unknown as BrowserBuilderOptions | undefined; - } - - const config = buildConfiguration || buildOptions; + const buildOptions = (buildTarget.options || {}) as unknown as BrowserBuilderOptions; const root = project.root; + buildOptions.serviceWorker = true; + buildOptions.ngswConfigPath = join(normalize(root), 'ngsw-config.json'); - config.serviceWorker = true; - config.ngswConfigPath = join(normalize(root), 'ngsw-config.json'); - - let { resourcesOutputPath = '' } = config; + let { resourcesOutputPath = '' } = buildOptions; if (resourcesOutputPath) { resourcesOutputPath = normalize(`/${resourcesOutputPath}`); } diff --git a/packages/schematics/angular/service-worker/index_spec.ts b/packages/schematics/angular/service-worker/index_spec.ts index 501c943eb5a7..2b4a5768d3c9 100644 --- a/packages/schematics/angular/service-worker/index_spec.ts +++ b/packages/schematics/angular/service-worker/index_spec.ts @@ -19,7 +19,7 @@ describe('Service Worker Schematic', () => { const defaultOptions: ServiceWorkerOptions = { project: 'bar', target: 'build', - configuration: 'production', + configuration: '', }; let appTree: UnitTestTree; @@ -45,25 +45,13 @@ describe('Service Worker Schematic', () => { .toPromise(); }); - it('should update the production configuration', async () => { + it('should add `serviceWorker` option to build target', async () => { const tree = await schematicRunner.runSchematicAsync('service-worker', defaultOptions, appTree) .toPromise(); const configText = tree.readContent('/angular.json'); - const config = JSON.parse(configText); - const swFlag = config.projects.bar.architect - .build.configurations.production.serviceWorker; - expect(swFlag).toEqual(true); - }); + const buildConfig = JSON.parse(configText).projects.bar.architect.build; - it('should update the target options if no configuration is set', async () => { - const options = { ...defaultOptions, configuration: '' }; - const tree = await schematicRunner.runSchematicAsync('service-worker', options, appTree) - .toPromise(); - const configText = tree.readContent('/angular.json'); - const config = JSON.parse(configText); - const swFlag = config.projects.bar.architect - .build.options.serviceWorker; - expect(swFlag).toEqual(true); + expect(buildConfig.options.serviceWorker).toBeTrue(); }); it('should add the necessary dependency', async () => { @@ -162,8 +150,7 @@ describe('Service Worker Schematic', () => { expect(tree.exists(path)).toEqual(true); const { projects } = JSON.parse(tree.readContent('/angular.json')); - expect(projects.bar.architect.build.configurations.production.ngswConfigPath) - .toBe('projects/bar/ngsw-config.json'); + expect(projects.bar.architect.build.options.ngswConfigPath).toBe('projects/bar/ngsw-config.json'); }); it('should add $schema in ngsw-config.json with correct relative path', async () => { @@ -214,7 +201,7 @@ describe('Service Worker Schematic', () => { it('should add resourcesOutputPath to root assets when specified', async () => { const config = JSON.parse(appTree.readContent('/angular.json')); - config.projects.bar.architect.build.configurations.production.resourcesOutputPath = 'outDir'; + config.projects.bar.architect.build.options.resourcesOutputPath = 'outDir'; appTree.overwrite('/angular.json', JSON.stringify(config)); const tree = await schematicRunner.runSchematicAsync('service-worker', defaultOptions, appTree) .toPromise(); @@ -243,7 +230,6 @@ describe('Service Worker Schematic', () => { expect(tree.exists('/ngsw-config.json')).toBe(true); const { projects } = JSON.parse(tree.readContent('/angular.json')); - expect(projects.foo.architect.build.configurations.production.ngswConfigPath) - .toBe('ngsw-config.json'); + expect(projects.foo.architect.build.options.ngswConfigPath).toBe('ngsw-config.json'); }); }); diff --git a/packages/schematics/angular/service-worker/schema.json b/packages/schematics/angular/service-worker/schema.json index 93dfea349318..dfe6328473a6 100644 --- a/packages/schematics/angular/service-worker/schema.json +++ b/packages/schematics/angular/service-worker/schema.json @@ -20,7 +20,7 @@ "configuration": { "type": "string", "description": "The configuration to apply service worker to.", - "default": "production" + "x-deprecated": "No longer has an effect." } }, "required": [ diff --git a/packages/schematics/angular/universal/index.ts b/packages/schematics/angular/universal/index.ts index 67d6be6f7be8..7e66da5e0485 100644 --- a/packages/schematics/angular/universal/index.ts +++ b/packages/schematics/angular/universal/index.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ import { + JsonValue, Path, basename, join, @@ -35,62 +36,65 @@ import { findBootstrapModuleCall, findBootstrapModulePath } from '../utility/ng- import { relativePathToWorkspaceRoot } from '../utility/paths'; import { targetBuildNotFoundError } from '../utility/project-targets'; import { getWorkspace, updateWorkspace } from '../utility/workspace'; -import { BrowserBuilderOptions, Builders, OutputHashing } from '../utility/workspace-models'; +import { BrowserBuilderOptions, Builders } from '../utility/workspace-models'; import { Schema as UniversalOptions } from './schema'; function updateConfigFile(options: UniversalOptions, tsConfigDirectory: Path): Rule { return updateWorkspace(workspace => { const clientProject = workspace.projects.get(options.clientProject); - if (clientProject) { - const buildTarget = clientProject.targets.get('build'); - let fileReplacements; - if (buildTarget && buildTarget.configurations && buildTarget.configurations.production) { - fileReplacements = buildTarget.configurations.production.fileReplacements; - } - - if (buildTarget && buildTarget.options) { - buildTarget.options.outputPath = `dist/${options.clientProject}/browser`; - } + if (clientProject) { // In case the browser builder hashes the assets // we need to add this setting to the server builder // as otherwise when assets it will be requested twice. // One for the server which will be unhashed, and other on the client which will be hashed. - let outputHashing: OutputHashing | undefined; - if (buildTarget && buildTarget.configurations && buildTarget.configurations.production) { - switch (buildTarget.configurations.production.outputHashing as OutputHashing) { - case 'all': - case 'media': - outputHashing = 'media'; - break; + const getServerOptions = (options: Record = {}): {} => { + return { + outputHashing: options?.outputHashing === 'all' ? 'media' : options?.outputHashing, + fileReplacements: options?.fileReplacements, + optimization: options?.optimization === undefined ? undefined : !!options?.optimization, + sourceMap: options?.sourceMap, + localization: options?.localization, + stylePreprocessorOptions: options?.stylePreprocessorOptions, + resourcesOutputPath: options?.resourcesOutputPath, + deployUrl: options?.deployUrl, + i18nMissingTranslation: options?.i18nMissingTranslation, + preserveSymlinks: options?.preserveSymlinks, + extractLicenses: options?.extractLicenses, + }; + }; + + const buildTarget = clientProject.targets.get('build'); + if (buildTarget?.options) { + buildTarget.options.outputPath = `dist/${options.clientProject}/browser`; + } + + const buildConfigurations = buildTarget?.configurations; + const configurations: Record = {}; + if (buildConfigurations) { + for (const [key, options] of Object.entries(buildConfigurations)) { + configurations[key] = getServerOptions(options); } } const mainPath = options.main as string; const serverTsConfig = join(tsConfigDirectory, 'tsconfig.server.json'); - clientProject.targets.add({ name: 'server', builder: Builders.Server, + defaultConfiguration: 'production', options: { outputPath: `dist/${options.clientProject}/server`, main: join(normalize(clientProject.root), 'src', mainPath.endsWith('.ts') ? mainPath : mainPath + '.ts'), tsConfig: serverTsConfig, + ...(buildTarget?.options ? getServerOptions(buildTarget?.options) : {}), }, - configurations: { - production: { - outputHashing, - fileReplacements, - sourceMap: false, - optimization: true, - }, - }, + configurations, }); const lintTarget = clientProject.targets.get('lint'); if (lintTarget && lintTarget.options && Array.isArray(lintTarget.options.tsConfig)) { - lintTarget.options.tsConfig = - lintTarget.options.tsConfig.concat(serverTsConfig); + lintTarget.options.tsConfig = lintTarget.options.tsConfig.concat(serverTsConfig); } } }); diff --git a/packages/schematics/angular/universal/index_spec.ts b/packages/schematics/angular/universal/index_spec.ts index 47793f2e69e0..2320448bf423 100644 --- a/packages/schematics/angular/universal/index_spec.ts +++ b/packages/schematics/angular/universal/index_spec.ts @@ -156,13 +156,9 @@ describe('Universal Schematic', () => { expect(opts.main).toEqual('projects/bar/src/main.server.ts'); expect(opts.tsConfig).toEqual('projects/bar/tsconfig.server.json'); const configurations = targets.server.configurations; - expect(configurations.production).toBeDefined(); - expect(configurations.production.fileReplacements).toBeDefined(); - expect(configurations.production.outputHashing).toBe('media'); - const fileReplacements = targets.server.configurations.production.fileReplacements; - expect(fileReplacements.length).toEqual(1); - expect(fileReplacements[0].replace).toEqual('projects/bar/src/environments/environment.ts'); - expect(fileReplacements[0].with).toEqual('projects/bar/src/environments/environment.prod.ts'); + expect(configurations.production.fileReplacements.length).toEqual(1); + expect(configurations.production.fileReplacements[0].replace).toEqual('projects/bar/src/environments/environment.ts'); + expect(configurations.production.fileReplacements[0].with).toEqual('projects/bar/src/environments/environment.prod.ts'); }); it('should update workspace with a build target outputPath', async () => { diff --git a/packages/schematics/angular/workspace/files/package.json.template b/packages/schematics/angular/workspace/files/package.json.template index 66219ead04f7..d5ddb223adc0 100644 --- a/packages/schematics/angular/workspace/files/package.json.template +++ b/packages/schematics/angular/workspace/files/package.json.template @@ -4,7 +4,8 @@ "scripts": { "ng": "ng", "start": "ng serve", - "build": "ng build"<% if (!minimal) { %>, + "build": "ng build", + "watch": "ng build --watch --configuration development"<% if (!minimal) { %>, "test": "ng test", "lint": "ng lint"<% } %> },