feat(@schematics/angular): add production by default optional migration

With this change we add an optional migration to update Angular CLI workspace configurations to 'production' mode by default.

To run this migration use the below commands
```
ng update @angular/cli
ng update @angular/cli --migrate-only production-by-default
```
This commit is contained in:
Alan Agius 2021-03-23 08:26:21 +01:00 committed by Charles
parent decb05b2fe
commit c7e126609f
4 changed files with 448 additions and 1 deletions

View File

@ -144,6 +144,11 @@
"version": "12.0.0-next.4", "version": "12.0.0-next.4",
"factory": "./update-8/#updateLazyModulePaths", "factory": "./update-8/#updateLazyModulePaths",
"description": "Lazy loading syntax migration. Update lazy loading string syntax to use dynamic imports." "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."
} }
} }
} }

View File

@ -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<string, JsonValue | undefined> = {};
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;
}

View File

@ -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);
});
});

View File

@ -76,7 +76,7 @@ export interface ServerBuilderOptions {
tsConfig: string; tsConfig: string;
main: string; main: string;
fileReplacements?: FileReplacements[]; fileReplacements?: FileReplacements[];
optimization?: { optimization?: boolean | {
scripts?: boolean; scripts?: boolean;
styles?: boolean; styles?: boolean;
}; };