diff --git a/packages/schematics/angular/migrations/migration-collection.json b/packages/schematics/angular/migrations/migration-collection.json index 8f9fe5ac3b..c0efd4bef8 100644 --- a/packages/schematics/angular/migrations/migration-collection.json +++ b/packages/schematics/angular/migrations/migration-collection.json @@ -144,6 +144,11 @@ "version": "12.0.0-next.4", "factory": "./update-8/#updateLazyModulePaths", "description": "Lazy loading syntax migration. Update lazy loading string syntax to use dynamic imports." + }, + "production-by-default": { + "version": "9999.0.0", + "factory": "./update-12/production-default-config", + "description": "Optional migration to update Angular CLI workspace configurations to 'production' mode by default." } } } diff --git a/packages/schematics/angular/migrations/update-12/production-default-config.ts b/packages/schematics/angular/migrations/update-12/production-default-config.ts new file mode 100644 index 0000000000..272972b1cd --- /dev/null +++ b/packages/schematics/angular/migrations/update-12/production-default-config.ts @@ -0,0 +1,132 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { JsonValue, logging, tags, workspaces } from '@angular-devkit/core'; +import { Rule } from '@angular-devkit/schematics'; +import { allTargetOptions, allWorkspaceTargets, updateWorkspace } from '../../utility/workspace'; +import { Builders } from '../../utility/workspace-models'; + +export default function (): Rule { + return async (_host, context) => updateWorkspace(workspace => { + for (const [name, target] of allWorkspaceTargets(workspace)) { + let defaultConfiguration: string | undefined; + + // Only interested in 1st party builders + switch (target.builder) { + case Builders.AppShell: + case Builders.Browser: + case Builders.Server: + case Builders.NgPackagr: + defaultConfiguration = 'production'; + break; + case Builders.DevServer: + case Builders.Protractor: + case '@nguniversal/builders:ssr-dev-server': + defaultConfiguration = 'development'; + break; + case Builders.TsLint: + case Builders.ExtractI18n: + case Builders.Karma: + // Nothing to update + break; + default: + context.logger.warn(tags.stripIndents`Cannot update "${name}" target configuration as it's using "${target.builder}" + which is a third-party builder. This target configuration will require manual review.`); + + continue; + } + + if (!defaultConfiguration) { + continue; + } + + updateTarget(name, target, context.logger, defaultConfiguration); + } + }); +} + +function getArchitectTargetWithConfig(currentTarget: string, overrideConfig?: string): string { + const [project, target, config = 'development'] = currentTarget.split(':'); + + return `${project}:${target}:${overrideConfig || config}`; +} + +function updateTarget( + targetName: string, + target: workspaces.TargetDefinition, + logger: logging.LoggerApi, + defaultConfiguration: string, +): void { + if (!target.configurations) { + target.configurations = {}; + } + + if (target.configurations?.development) { + logger.info(tags.stripIndents`Skipping updating "${targetName}" target configuration as a "development" configuration is already defined.`); + + return; + } + + if (!target.configurations?.production) { + logger.info(tags.stripIndents`Skipping updating "${targetName}" target configuration as a "production" configuration is not defined.`); + + return; + } + + const developmentOptions: Record = {}; + let serverTarget = true; + let browserTarget = true; + let devServerTarget = true; + + for (const [, options] of allTargetOptions(target)) { + if (typeof options.serverTarget === 'string') { + options.serverTarget = getArchitectTargetWithConfig(options.serverTarget); + if (!developmentOptions.serverTarget) { + developmentOptions.serverTarget = getArchitectTargetWithConfig(options.serverTarget, 'development'); + } + } else { + serverTarget = false; + } + + if (typeof options.browserTarget === 'string') { + options.browserTarget = getArchitectTargetWithConfig(options.browserTarget); + if (!developmentOptions.browserTarget) { + developmentOptions.browserTarget = getArchitectTargetWithConfig(options.browserTarget, 'development'); + } + } else { + browserTarget = false; + } + + if (typeof options.devServerTarget === 'string') { + options.devServerTarget = getArchitectTargetWithConfig(options.devServerTarget); + if (!developmentOptions.devServerTarget) { + developmentOptions.devServerTarget = getArchitectTargetWithConfig(options.devServerTarget, 'development'); + } + } else { + devServerTarget = false; + } + } + + // If all configurastions have a target defined delete the one in options. + if (target.options) { + if (serverTarget) { + delete target.options.serverTarget; + } + + if (browserTarget) { + delete target.options.browserTarget; + } + + if (devServerTarget) { + delete target.options.devServerTarget; + } + } + + target.defaultConfiguration = defaultConfiguration; + target.configurations.development = developmentOptions; +} diff --git a/packages/schematics/angular/migrations/update-12/production-default-config_spec.ts b/packages/schematics/angular/migrations/update-12/production-default-config_spec.ts new file mode 100644 index 0000000000..45076c7d1c --- /dev/null +++ b/packages/schematics/angular/migrations/update-12/production-default-config_spec.ts @@ -0,0 +1,310 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { JsonObject } from '@angular-devkit/core'; +import { EmptyTree } from '@angular-devkit/schematics'; +import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; +import { Builders, ProjectType, WorkspaceSchema } from '../../utility/workspace-models'; + +function getArchitect(tree: UnitTestTree): JsonObject { + return JSON.parse(tree.readContent('/angular.json')).projects.app.architect; +} + +function createWorkSpaceConfig(tree: UnitTestTree) { + const angularConfig: WorkspaceSchema = { + version: 1, + projects: { + app: { + root: '', + sourceRoot: 'src', + projectType: ProjectType.Application, + prefix: 'app', + architect: { + browser: { + builder: Builders.Browser, + options: { + outputPath: 'dist/integration-project', + index: 'src/index.html', + main: 'src/main.ts', + polyfills: 'src/polyfills.ts', + tsConfig: 'tsconfig.app.json', + aot: true, + sourceMap: true, + assets: [ + 'src/favicon.ico', + 'src/assets', + ], + styles: [ + 'src/styles.css', + ], + scripts: [], + }, + configurations: { + production: { + deployUrl: 'http://cdn.com', + fileReplacements: [{ + replace: 'src/environments/environment.ts', + with: 'src/environments/environment.prod.ts', + }], + optimization: true, + outputHashing: 'all', + sourceMap: false, + namedChunks: false, + extractLicenses: true, + vendorChunk: false, + buildOptimizer: true, + watch: true, + budgets: [{ + type: 'initial', + maximumWarning: '2mb', + maximumError: '5mb', + }], + }, + optimization_sm: { + sourceMap: true, + optimization: true, + namedChunks: false, + vendorChunk: true, + buildOptimizer: true, + }, + }, + }, + ng_packagr: { + builder: Builders.NgPackagr, + options: { + watch: true, + tsConfig: 'projects/lib/tsconfig.lib.json', + }, + configurations: { + production: { + watch: false, + tsConfig: 'projects/lib/tsconfig.lib.prod.json', + }, + }, + }, + dev_server: { + builder: Builders.DevServer, + options: { + browserTarget: 'app:build', + watch: false, + }, + configurations: { + production: { + browserTarget: 'app:build:production', + }, + optimization_sm: { + browserTarget: 'app:build:optimization_sm', + }, + }, + }, + app_shell: { + builder: Builders.AppShell, + options: { + browserTarget: 'app:build', + serverTarget: 'app:server', + }, + configurations: { + optimization_sm: { + browserTarget: 'app:build:optimization_sm', + serverTarget: 'app:server:optimization_sm', + }, + production: { + browserTarget: 'app:build:production', + serverTarget: 'app:server:optimization_sm', + }, + }, + }, + server: { + builder: Builders.Server, + options: { + outputPath: 'dist/server', + main: 'server.ts', + tsConfig: 'tsconfig.server.json', + optimization: false, + sourceMap: true, + }, + configurations: { + optimization_sm: { + sourceMap: true, + optimization: true, + }, + production: { + fileReplacements: [ + { + replace: 'src/environments/environment.ts', + with: 'src/environments/environment.prod.ts', + }, + ], + sourceMap: false, + optimization: true, + }, + }, + }, + }, + }, + }, + }; + + tree.create('/angular.json', JSON.stringify(angularConfig, undefined, 2)); +} + +const schematicName = 'production-by-default'; +describe(`Migration to update 'angular.json' configurations to production by default. ${schematicName}`, () => { + const schematicRunner = new SchematicTestRunner( + 'migrations', + require.resolve('../migration-collection.json'), + ); + + let tree: UnitTestTree; + beforeEach(() => { + tree = new UnitTestTree(new EmptyTree()); + createWorkSpaceConfig(tree); + }); + + it('update browser builder configurations', async () => { + const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise(); + const { browser } = getArchitect(newTree); + const output = { + builder: '@angular-devkit/build-angular:browser', + options: { + outputPath: 'dist/integration-project', + index: 'src/index.html', + main: 'src/main.ts', + polyfills: 'src/polyfills.ts', + tsConfig: 'tsconfig.app.json', + aot: true, + sourceMap: true, + assets: ['src/favicon.ico', 'src/assets'], + styles: ['src/styles.css'], + scripts: [], + }, + configurations: { + production: { + deployUrl: 'http://cdn.com', + optimization: true, + outputHashing: 'all', + sourceMap: false, + namedChunks: false, + extractLicenses: true, + vendorChunk: false, + buildOptimizer: true, + watch: true, + fileReplacements: [{ + replace: 'src/environments/environment.ts', + with: 'src/environments/environment.prod.ts', + }], + budgets: [{ + type: 'initial', + maximumWarning: '2mb', + maximumError: '5mb', + }], + }, + optimization_sm: { + sourceMap: true, + optimization: true, + namedChunks: false, + vendorChunk: true, + buildOptimizer: true, + }, + development: {}, + }, + defaultConfiguration: 'production', + }; + + expect(browser).toEqual(output); + }); + + it('update ng-packagr builder configurations', async () => { + const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise(); + const { ng_packagr } = getArchitect(newTree); + const output = { + builder: '@angular-devkit/build-angular:ng-packagr', + options: { watch: true, tsConfig: 'projects/lib/tsconfig.lib.json' }, + configurations: { + production: { watch: false, tsConfig: 'projects/lib/tsconfig.lib.prod.json' }, + development: {}, + }, + defaultConfiguration: 'production', + }; + + expect(ng_packagr).toEqual(output); + }); + + it('update dev-server builder configurations', async () => { + const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise(); + const { dev_server } = getArchitect(newTree); + const output = { + builder: '@angular-devkit/build-angular:dev-server', + options: { watch: false }, + configurations: { + production: { browserTarget: 'app:build:production' }, + optimization_sm: { browserTarget: 'app:build:optimization_sm' }, + development: { browserTarget: 'app:build:development' }, + }, + defaultConfiguration: 'development', + }; + + expect(dev_server).toEqual(output); + }); + + it('update server builder configurations', async () => { + const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise(); + const { server } = getArchitect(newTree); + const output = { + builder: '@angular-devkit/build-angular:server', + options: { + outputPath: 'dist/server', + main: 'server.ts', + tsConfig: 'tsconfig.server.json', + optimization: false, + sourceMap: true, + }, + configurations: { + optimization_sm: { sourceMap: true, optimization: true }, + production: { + fileReplacements: [{ + replace: 'src/environments/environment.ts', + with: 'src/environments/environment.prod.ts', + }], + sourceMap: false, + optimization: true, + }, + development: {}, + }, + defaultConfiguration: 'production', + }; + + expect(server).toEqual(output); + }); + + it('update app-shell builder configurations', async () => { + const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise(); + const { app_shell } = getArchitect(newTree); + + const output = { + builder: '@angular-devkit/build-angular:app-shell', + options: {}, + configurations: { + optimization_sm: { + browserTarget: 'app:build:optimization_sm', + serverTarget: 'app:server:optimization_sm', + }, + production: { + browserTarget: 'app:build:production', + serverTarget: 'app:server:optimization_sm', + }, + development: { + serverTarget: 'app:server:development', + browserTarget: 'app:build:development', + }, + }, + defaultConfiguration: 'production', + }; + + expect(app_shell).toEqual(output); + }); +}); diff --git a/packages/schematics/angular/utility/workspace-models.ts b/packages/schematics/angular/utility/workspace-models.ts index 248d4ab7a4..1113946fe1 100644 --- a/packages/schematics/angular/utility/workspace-models.ts +++ b/packages/schematics/angular/utility/workspace-models.ts @@ -76,7 +76,7 @@ export interface ServerBuilderOptions { tsConfig: string; main: string; fileReplacements?: FileReplacements[]; - optimization?: { + optimization?: boolean | { scripts?: boolean; styles?: boolean; };