feat(@schematics/angular): add migration to add anyComponentStyle bundle budget

This commit is contained in:
Alan Agius 2019-07-23 19:58:40 +02:00 committed by vikerman
parent f32c9dde1a
commit 19b120947d
5 changed files with 175 additions and 61 deletions

View File

@ -12,11 +12,17 @@ import {
} from '@angular-devkit/core'; } from '@angular-devkit/core';
import { Rule, Tree, UpdateRecorder } from '@angular-devkit/schematics'; import { Rule, Tree, UpdateRecorder } from '@angular-devkit/schematics';
import { import {
appendValueInAstArray,
findPropertyInAstObject, findPropertyInAstObject,
insertPropertyInAstObjectInOrder, insertPropertyInAstObjectInOrder,
removePropertyInAstObject, removePropertyInAstObject,
} from '../../utility/json-utils'; } from '../../utility/json-utils';
export const ANY_COMPONENT_STYLE_BUDGET = {
type: 'anyComponentStyle',
maximumWarning: '6kb',
};
export function UpdateWorkspaceConfig(): Rule { export function UpdateWorkspaceConfig(): Rule {
return (tree: Tree) => { return (tree: Tree) => {
let workspaceConfigPath = 'angular.json'; let workspaceConfigPath = 'angular.json';
@ -59,8 +65,9 @@ export function UpdateWorkspaceConfig(): Rule {
const builder = findPropertyInAstObject(buildTarget, 'builder'); const builder = findPropertyInAstObject(buildTarget, 'builder');
// Projects who's build builder is not build-angular:browser // Projects who's build builder is not build-angular:browser
if (builder && builder.kind === 'string' && builder.value === '@angular-devkit/build-angular:browser') { if (builder && builder.kind === 'string' && builder.value === '@angular-devkit/build-angular:browser') {
updateOption('styles', recorder, buildTarget); updateStyleOrScriptOption('styles', recorder, buildTarget);
updateOption('scripts', recorder, buildTarget); updateStyleOrScriptOption('scripts', recorder, buildTarget);
addAnyComponentStyleBudget(recorder, buildTarget);
} }
} }
@ -69,8 +76,8 @@ export function UpdateWorkspaceConfig(): Rule {
const builder = findPropertyInAstObject(testTarget, 'builder'); const builder = findPropertyInAstObject(testTarget, 'builder');
// Projects who's build builder is not build-angular:browser // Projects who's build builder is not build-angular:browser
if (builder && builder.kind === 'string' && builder.value === '@angular-devkit/build-angular:karma') { if (builder && builder.kind === 'string' && builder.value === '@angular-devkit/build-angular:karma') {
updateOption('styles', recorder, testTarget); updateStyleOrScriptOption('styles', recorder, testTarget);
updateOption('scripts', recorder, testTarget); updateStyleOrScriptOption('scripts', recorder, testTarget);
} }
} }
} }
@ -84,19 +91,21 @@ export function UpdateWorkspaceConfig(): Rule {
/** /**
* Helper to retreive all the options in various configurations * Helper to retreive all the options in various configurations
*/ */
function getAllOptions(builderConfig: JsonAstObject): JsonAstObject[] { function getAllOptions(builderConfig: JsonAstObject, configurationsOnly = false): JsonAstObject[] {
const options = []; const options = [];
const configurations = findPropertyInAstObject(builderConfig, 'configurations'); const configurations = findPropertyInAstObject(builderConfig, 'configurations');
if (configurations && configurations.kind === 'object') { if (configurations && configurations.kind === 'object') {
options.push(...configurations.properties.map(x => x.value)); options.push(...configurations.properties.map(x => x.value));
} }
options.push(findPropertyInAstObject(builderConfig, 'options')); if (!configurationsOnly) {
options.push(findPropertyInAstObject(builderConfig, 'options'));
}
return options.filter(o => o && o.kind === 'object') as JsonAstObject[]; return options.filter(o => o && o.kind === 'object') as JsonAstObject[];
} }
function updateOption(property: 'scripts' | 'styles', recorder: UpdateRecorder, builderConfig: JsonAstObject) { function updateStyleOrScriptOption(property: 'scripts' | 'styles', recorder: UpdateRecorder, builderConfig: JsonAstObject) {
const options = getAllOptions(builderConfig); const options = getAllOptions(builderConfig);
for (const option of options) { for (const option of options) {
@ -121,3 +130,42 @@ function updateOption(property: 'scripts' | 'styles', recorder: UpdateRecorder,
} }
} }
} }
function addAnyComponentStyleBudget(recorder: UpdateRecorder, builderConfig: JsonAstObject) {
const options = getAllOptions(builderConfig, true);
for (const option of options) {
const aotOption = findPropertyInAstObject(option, 'aot');
if (!aotOption || aotOption.kind !== 'true') {
// AnyComponentStyle only works for AOT
continue;
}
const budgetOption = findPropertyInAstObject(option, 'budgets');
if (!budgetOption) {
// add
insertPropertyInAstObjectInOrder(recorder, option, 'budgets', [ANY_COMPONENT_STYLE_BUDGET], 14);
continue;
}
if (budgetOption.kind !== 'array') {
continue;
}
// if 'anyComponentStyle' budget already exists don't add.
const hasAnyComponentStyle = budgetOption.elements.some(node => {
if (!node || node.kind !== 'object') {
// skip non complex objects
return false;
}
const budget = findPropertyInAstObject(node, 'type');
return !!budget && budget.kind === 'string' && budget.value === 'anyComponentStyle';
});
if (!hasAnyComponentStyle) {
appendValueInAstArray(recorder, budgetOption, ANY_COMPONENT_STYLE_BUDGET, 16);
}
}
}

View File

@ -8,9 +8,19 @@
import { EmptyTree } from '@angular-devkit/schematics'; import { EmptyTree } from '@angular-devkit/schematics';
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
import { WorkspaceTargets } from '../../utility/workspace-models';
import { ANY_COMPONENT_STYLE_BUDGET } from './update-workspace-config';
function readWorkspaceConfig(tree: UnitTestTree) { // tslint:disable-next-line: no-any
return JSON.parse(tree.readContent('/angular.json')); function getWorkspaceTargets(tree: UnitTestTree): any {
return JSON.parse(tree.readContent(workspacePath))
.projects['migration-test'].architect;
}
function updateWorkspaceTargets(tree: UnitTestTree, workspaceTargets: WorkspaceTargets) {
const config = JSON.parse(tree.readContent(workspacePath));
config.projects['migration-test'].architect = workspaceTargets;
tree.overwrite(workspacePath, JSON.stringify(config, undefined, 2));
} }
const scriptsWithLazy = [ const scriptsWithLazy = [
@ -69,60 +79,95 @@ describe('Migration to version 9', () => {
.toPromise(); .toPromise();
}); });
it('should update scripts in build target', () => { describe('scripts and style options', () => {
let config = readWorkspaceConfig(tree); it('should update scripts in build target', () => {
let build = config.projects['migration-test'].architect.build; let config = getWorkspaceTargets(tree);
build.options.scripts = scriptsWithLazy; config.build.options.scripts = scriptsWithLazy;
build.configurations.production.scripts = scriptsWithLazy; config.build.configurations.production.scripts = scriptsWithLazy;
tree.overwrite(workspacePath, JSON.stringify(config)); updateWorkspaceTargets(tree, config);
const tree2 = schematicRunner.runSchematic('migration-09', {}, tree.branch()); const tree2 = schematicRunner.runSchematic('migration-09', {}, tree.branch());
config = readWorkspaceConfig(tree2); config = getWorkspaceTargets(tree2).build;
build = config.projects['migration-test'].architect.build; expect(config.options.scripts).toEqual(scriptsExpectWithLazy);
expect(build.options.scripts).toEqual(scriptsExpectWithLazy); expect(config.configurations.production.scripts).toEqual(scriptsExpectWithLazy);
expect(build.configurations.production.scripts).toEqual(scriptsExpectWithLazy); });
it('should update styles in build target', () => {
let config = getWorkspaceTargets(tree);
config.build.options.styles = stylesWithLazy;
config.build.configurations.production.styles = stylesWithLazy;
updateWorkspaceTargets(tree, config);
const tree2 = schematicRunner.runSchematic('migration-09', {}, tree.branch());
config = getWorkspaceTargets(tree2).build;
expect(config.options.styles).toEqual(stylesExpectWithLazy);
expect(config.configurations.production.styles).toEqual(stylesExpectWithLazy);
});
it('should update scripts in test target', () => {
let config = getWorkspaceTargets(tree);
config.test.options.scripts = scriptsWithLazy;
config.test.configurations = { production: { scripts: scriptsWithLazy } };
updateWorkspaceTargets(tree, config);
const tree2 = schematicRunner.runSchematic('migration-09', {}, tree.branch());
config = getWorkspaceTargets(tree2).test;
expect(config.options.scripts).toEqual(scriptsExpectWithLazy);
expect(config.configurations.production.scripts).toEqual(scriptsExpectWithLazy);
});
it('should update styles in test target', () => {
let config = getWorkspaceTargets(tree);
config.test.options.styles = stylesWithLazy;
config.test.configurations = { production: { styles: stylesWithLazy } };
updateWorkspaceTargets(tree, config);
const tree2 = schematicRunner.runSchematic('migration-09', {}, tree.branch());
config = getWorkspaceTargets(tree2).test;
expect(config.options.styles).toEqual(stylesExpectWithLazy);
expect(config.configurations.production.styles).toEqual(stylesExpectWithLazy);
});
}); });
it('should update styles in build target', () => { describe('anyComponentStyle bundle budget', () => {
let config = readWorkspaceConfig(tree); it('should not append budget when already exists', () => {
let build = config.projects['migration-test'].architect.build; const defaultBudget = [
build.options.styles = stylesWithLazy; { type: 'initial', maximumWarning: '2mb', maximumError: '5mb' },
build.configurations.production.styles = stylesWithLazy; { type: 'anyComponentStyle', maximumWarning: '10kb', maximumError: '50kb' },
];
tree.overwrite(workspacePath, JSON.stringify(config)); let config = getWorkspaceTargets(tree);
const tree2 = schematicRunner.runSchematic('migration-09', {}, tree.branch()); config.build.configurations.production.budgets = defaultBudget;
config = readWorkspaceConfig(tree2); updateWorkspaceTargets(tree, config);
build = config.projects['migration-test'].architect.build;
expect(build.options.styles).toEqual(stylesExpectWithLazy);
expect(build.configurations.production.styles).toEqual(stylesExpectWithLazy);
});
it('should update scripts in test target', () => { const tree2 = schematicRunner.runSchematic('migration-09', {}, tree.branch());
let config = readWorkspaceConfig(tree); config = getWorkspaceTargets(tree2).build;
let test = config.projects['migration-test'].architect.test; expect(config.configurations.production.budgets).toEqual(defaultBudget);
test.options.scripts = scriptsWithLazy; });
test.configurations = { production: { scripts: scriptsWithLazy } };
tree.overwrite(workspacePath, JSON.stringify(config)); it('should append budget in build target', () => {
const tree2 = schematicRunner.runSchematic('migration-09', {}, tree.branch()); const defaultBudget = [{ type: 'initial', maximumWarning: '2mb', maximumError: '5mb' }];
config = readWorkspaceConfig(tree2); let config = getWorkspaceTargets(tree);
test = config.projects['migration-test'].architect.test; config.build.configurations.production.budgets = defaultBudget;
expect(test.options.scripts).toEqual(scriptsExpectWithLazy); updateWorkspaceTargets(tree, config);
expect(test.configurations.production.scripts).toEqual(scriptsExpectWithLazy);
});
it('should update styles in test target', () => { const tree2 = schematicRunner.runSchematic('migration-09', {}, tree.branch());
let config = readWorkspaceConfig(tree); config = getWorkspaceTargets(tree2).build;
let test = config.projects['migration-test'].architect.test; expect(config.configurations.production.budgets).toEqual([
test.options.styles = stylesWithLazy; ...defaultBudget,
test.configurations = { production: { styles: stylesWithLazy } }; ANY_COMPONENT_STYLE_BUDGET,
]);
});
tree.overwrite(workspacePath, JSON.stringify(config)); it('should add budget in build target', () => {
const tree2 = schematicRunner.runSchematic('migration-09', {}, tree.branch()); let config = getWorkspaceTargets(tree);
config = readWorkspaceConfig(tree2); config.build.configurations.production.budgets = undefined;
test = config.projects['migration-test'].architect.test; updateWorkspaceTargets(tree, config);
expect(test.options.styles).toEqual(stylesExpectWithLazy);
expect(test.configurations.production.styles).toEqual(stylesExpectWithLazy); const tree2 = schematicRunner.runSchematic('migration-09', {}, tree.branch());
config = getWorkspaceTargets(tree2).build;
expect(config.configurations.production.budgets).toEqual([ANY_COMPONENT_STYLE_BUDGET]);
});
}); });
}); });
}); });

View File

@ -130,7 +130,7 @@ export interface AppConfig {
/** /**
* The type of budget * The type of budget
*/ */
type?: ('bundle' | 'initial' | 'allScript' | 'all' | 'anyScript' | 'any'); type?: ('bundle' | 'initial' | 'allScript' | 'all' | 'anyScript' | 'any' | 'anyComponentStyle');
/** /**
* The name of the bundle * The name of the bundle
*/ */

View File

@ -31,7 +31,7 @@ export function appendPropertyInAstObject(
recorder.insertRight(commaIndex, ','); recorder.insertRight(commaIndex, ',');
index = end.offset; index = end.offset;
} }
const content = JSON.stringify(value, null, indent).replace(/\n/g, indentStr); const content = _stringifyContent(value, indentStr);
recorder.insertRight( recorder.insertRight(
index, index,
(node.properties.length === 0 && indent ? '\n' : '') (node.properties.length === 0 && indent ? '\n' : '')
@ -84,7 +84,7 @@ export function insertPropertyInAstObjectInOrder(
const insertIndex = insertAfterProp === null const insertIndex = insertAfterProp === null
? node.start.offset + 1 ? node.start.offset + 1
: insertAfterProp.end.offset + 1; : insertAfterProp.end.offset + 1;
const content = JSON.stringify(value, null, indent).replace(/\n/g, indentStr); const content = _stringifyContent(value, indentStr);
recorder.insertRight( recorder.insertRight(
insertIndex, insertIndex,
indentStr indentStr
@ -168,7 +168,7 @@ export function appendValueInAstArray(
index, index,
(node.elements.length === 0 && indent ? '\n' : '') (node.elements.length === 0 && indent ? '\n' : '')
+ ' '.repeat(indent) + ' '.repeat(indent)
+ JSON.stringify(value, null, indent).replace(/\n/g, indentStr) + _stringifyContent(value, indentStr)
+ indentStr.slice(0, -indent), + indentStr.slice(0, -indent),
); );
} }
@ -191,3 +191,24 @@ export function findPropertyInAstObject(
function _buildIndent(count: number): string { function _buildIndent(count: number): string {
return count ? '\n' + ' '.repeat(count) : ''; return count ? '\n' + ' '.repeat(count) : '';
} }
function _stringifyContent(value: JsonValue, indentStr: string): string {
// TODO: Add snapshot tests
// The 'space' value is 2, because we want to add 2 additional
// indents from the 'key' node.
// If we use the indent provided we will have double indents:
// "budgets": [
// {
// "type": "initial",
// "maximumWarning": "2mb",
// "maximumError": "5mb"
// },
// {
// "type": "anyComponentStyle",
// 'maximumWarning": "5kb"
// }
// ]
return JSON.stringify(value, null, 2).replace(/\n/g, indentStr);
}

View File

@ -38,8 +38,8 @@ export interface BrowserBuilderBaseOptions {
index?: string; index?: string;
polyfills: string; polyfills: string;
assets?: (object|string)[]; assets?: (object|string)[];
styles?: string[]; styles?: (object|string)[];
scripts?: string[]; scripts?: (object|string)[];
sourceMap?: boolean; sourceMap?: boolean;
} }