diff --git a/packages/angular/cli/lib/config/schema.json b/packages/angular/cli/lib/config/schema.json index 8a3117b98f1d..1b7b03daa269 100644 --- a/packages/angular/cli/lib/config/schema.json +++ b/packages/angular/cli/lib/config/schema.json @@ -1024,6 +1024,7 @@ "allScript", "any", "anyScript", + "anyComponentStyle", "bundle", "initial" ] diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/bundle-budget.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/bundle-budget.ts index 4ee676886846..911e4c55d852 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/bundle-budget.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/bundle-budget.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ import { Compiler, compilation } from 'webpack'; -import { Budget } from '../../browser/schema'; +import { Budget, Type } from '../../browser/schema'; import { Size, calculateBytes, calculateSizes } from '../utilities/bundle-calculator'; import { formatSize } from '../utilities/stats'; @@ -30,33 +30,28 @@ export class BundleBudgetPlugin { apply(compiler: Compiler): void { const { budgets } = this.options; - compiler.hooks.afterEmit.tap('BundleBudgetPlugin', (compilation: compilation.Compilation) => { - if (!budgets || budgets.length === 0) { - return; - } - budgets.map(budget => { - const thresholds = this.calculate(budget); - - return { - budget, - thresholds, - sizes: calculateSizes(budget, compilation), - }; - }) - .forEach(budgetCheck => { - budgetCheck.sizes.forEach(size => { - this.checkMaximum(budgetCheck.thresholds.maximumWarning, size, compilation.warnings); - this.checkMaximum(budgetCheck.thresholds.maximumError, size, compilation.errors); - this.checkMinimum(budgetCheck.thresholds.minimumWarning, size, compilation.warnings); - this.checkMinimum(budgetCheck.thresholds.minimumError, size, compilation.errors); - this.checkMinimum(budgetCheck.thresholds.warningLow, size, compilation.warnings); - this.checkMaximum(budgetCheck.thresholds.warningHigh, size, compilation.warnings); - this.checkMinimum(budgetCheck.thresholds.errorLow, size, compilation.errors); - this.checkMaximum(budgetCheck.thresholds.errorHigh, size, compilation.errors); - }); + if (!budgets || budgets.length === 0) { + return; + } - }); + compiler.hooks.compilation.tap('BundleBudgetPlugin', (compilation: compilation.Compilation) => { + compilation.hooks.afterOptimizeChunkAssets.tap('BundleBudgetPlugin', () => { + // In AOT compilations component styles get processed in child compilations. + // tslint:disable-next-line: no-any + const parentCompilation = (compilation.compiler as any).parentCompilation; + if (!parentCompilation) { + return; + } + + const filteredBudgets = budgets.filter(budget => budget.type === Type.AnyComponentStyle); + this.runChecks(filteredBudgets, compilation); + }); + }); + + compiler.hooks.afterEmit.tap('BundleBudgetPlugin', (compilation: compilation.Compilation) => { + const filteredBudgets = budgets.filter(budget => budget.type !== Type.AnyComponentStyle); + this.runChecks(filteredBudgets, compilation); }); } @@ -116,4 +111,25 @@ export class BundleBudgetPlugin { return thresholds; } + + private runChecks(budgets: Budget[], compilation: compilation.Compilation) { + budgets + .map(budget => ({ + budget, + thresholds: this.calculate(budget), + sizes: calculateSizes(budget, compilation), + })) + .forEach(budgetCheck => { + budgetCheck.sizes.forEach(size => { + this.checkMaximum(budgetCheck.thresholds.maximumWarning, size, compilation.warnings); + this.checkMaximum(budgetCheck.thresholds.maximumError, size, compilation.errors); + this.checkMinimum(budgetCheck.thresholds.minimumWarning, size, compilation.warnings); + this.checkMinimum(budgetCheck.thresholds.minimumError, size, compilation.errors); + this.checkMinimum(budgetCheck.thresholds.warningLow, size, compilation.warnings); + this.checkMaximum(budgetCheck.thresholds.warningHigh, size, compilation.warnings); + this.checkMinimum(budgetCheck.thresholds.errorLow, size, compilation.errors); + this.checkMaximum(budgetCheck.thresholds.errorHigh, size, compilation.errors); + }); + }); + } } diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/bundle-calculator.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/bundle-calculator.ts index 8af29c8e1208..8ef3cb93884f 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/bundle-calculator.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/bundle-calculator.ts @@ -25,9 +25,11 @@ export function calculateSizes(budget: Budget, compilation: Compilation): Size[] allScript: AllScriptCalculator, any: AnyCalculator, anyScript: AnyScriptCalculator, + anyComponentStyle: AnyComponentStyleCalculator, bundle: BundleCalculator, initial: InitialCalculator, }; + const ctor = calculatorMap[budget.type]; const calculator = new ctor(budget, compilation); @@ -101,6 +103,20 @@ class AllCalculator extends Calculator { } } +/** + * Any components styles + */ +class AnyComponentStyleCalculator extends Calculator { + calculate() { + return Object.keys(this.compilation.assets) + .filter(key => key.endsWith('.css')) + .map(key => ({ + size: this.compilation.assets[key].size(), + label: key, + })); + } +} + /** * Any script, individually. */ diff --git a/packages/angular_devkit/build_angular/src/browser/schema.json b/packages/angular_devkit/build_angular/src/browser/schema.json index 42c8ef8045cc..dbc930292512 100644 --- a/packages/angular_devkit/build_angular/src/browser/schema.json +++ b/packages/angular_devkit/build_angular/src/browser/schema.json @@ -482,6 +482,7 @@ "allScript", "any", "anyScript", + "anyComponentStyle", "bundle", "initial" ] diff --git a/packages/angular_devkit/build_angular/test/browser/aot_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/aot_spec_large.ts index 785713cac442..4d0a141a5c77 100644 --- a/packages/angular_devkit/build_angular/test/browser/aot_spec_large.ts +++ b/packages/angular_devkit/build_angular/test/browser/aot_spec_large.ts @@ -7,7 +7,7 @@ */ import { Architect } from '@angular-devkit/architect'; -import { join, normalize, virtualFs } from '@angular-devkit/core'; +import { join, logging, normalize, virtualFs } from '@angular-devkit/core'; import { BrowserBuilderOutput } from '../../src/browser'; import { createArchitect, host, ivyEnabled } from '../utils'; @@ -39,4 +39,28 @@ describe('Browser Builder AOT', () => { await run.stop(); }); + + it('shows warnings for component styles', async () => { + const overrides = { + aot: true, + optimization: true, + }; + + host.writeMultipleFiles({ + 'src/app/app.component.css': ` + .foo { color: white; padding: 1px; }; + .buz { color: white; padding: 2px; }; + `, + }); + + const logger = new logging.Logger(''); + const logs: string[] = []; + logger.subscribe(e => logs.push(e.message)); + + const run = await architect.scheduleTarget(targetSpec, overrides, { logger }); + const output = await run.result; + expect(output.success).toBe(true); + expect(logs.join()).toContain('WARNING in Invalid selector'); + await run.stop(); + }); }); diff --git a/packages/angular_devkit/build_angular/test/browser/bundle-budgets_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/bundle-budgets_spec_large.ts index 37e150210ecb..2ce1a5691e41 100644 --- a/packages/angular_devkit/build_angular/test/browser/bundle-budgets_spec_large.ts +++ b/packages/angular_devkit/build_angular/test/browser/bundle-budgets_spec_large.ts @@ -64,6 +64,68 @@ describe('Browser Builder bundle budgets', () => { await run.stop(); }); + it(`shows warnings for large component css when using 'anyComponentStyle' when AOT`, async () => { + const overrides = { + aot: true, + optimization: true, + budgets: [{ type: 'anyComponentStyle', maximumWarning: '1b' }], + }; + + const cssContent = ` + .foo { color: white; padding: 1px; } + .buz { color: white; padding: 2px; } + .bar { color: white; padding: 3px; } + `; + + host.writeMultipleFiles({ + 'src/app/app.component.css': cssContent, + 'src/assets/foo.css': cssContent, + 'src/styles.css': cssContent, + }); + + const logger = new logging.Logger(''); + const logs: string[] = []; + logger.subscribe(e => logs.push(e.message)); + + const run = await architect.scheduleTarget(targetSpec, overrides, { logger }); + const output = await run.result; + expect(output.success).toBe(true); + expect(logs.length).toBe(2); + expect(logs.join()).toMatch(/WARNING.+app\.component\.css/); + await run.stop(); + }); + + it(`shows error for large component css when using 'anyComponentStyle' when AOT`, async () => { + const overrides = { + aot: true, + optimization: true, + budgets: [{ type: 'anyComponentStyle', maximumError: '1b' }], + }; + + const cssContent = ` + .foo { color: white; padding: 1px; } + .buz { color: white; padding: 2px; } + .bar { color: white; padding: 3px; } + `; + + host.writeMultipleFiles({ + 'src/app/app.component.css': cssContent, + 'src/assets/foo.css': cssContent, + 'src/styles.css': cssContent, + }); + + const logger = new logging.Logger(''); + const logs: string[] = []; + logger.subscribe(e => logs.push(e.message)); + + const run = await architect.scheduleTarget(targetSpec, overrides, { logger }); + const output = await run.result; + expect(output.success).toBe(false); + expect(logs.length).toBe(2); + expect(logs.join()).toMatch(/ERROR.+app\.component\.css/); + await run.stop(); + }); + describe(`should ignore '.map' files`, () => { it(`when 'bundle' budget`, async () => { const overrides = { diff --git a/packages/ngtools/webpack/src/resource_loader.ts b/packages/ngtools/webpack/src/resource_loader.ts index 75d9a68db70d..8dc72d93f202 100644 --- a/packages/ngtools/webpack/src/resource_loader.ts +++ b/packages/ngtools/webpack/src/resource_loader.ts @@ -100,10 +100,19 @@ export class WebpackResourceLoader { return new Promise((resolve, reject) => { childCompiler.compile((err: Error, childCompilation: any) => { // Resolve / reject the promise - if (childCompilation && childCompilation.errors && childCompilation.errors.length) { - const errorDetails = childCompilation.errors.map(function (error: any) { - return error.message + (error.error ? ':\n' + error.error : ''); - }).join('\n'); + const { warnings, errors } = childCompilation; + + if (warnings && warnings.length) { + this._parentCompilation.warnings.push(...warnings); + } + + if (errors && errors.length) { + this._parentCompilation.errors.push(...errors); + + const errorDetails = errors + .map((error: any) => error.message + (error.error ? ':\n' + error.error : '')) + .join('\n'); + reject(new Error('Child compilation failed:\n' + errorDetails)); } else if (err) { reject(err); diff --git a/packages/schematics/angular/application/index.ts b/packages/schematics/angular/application/index.ts index f7ff777a4b77..8a80af4bead8 100644 --- a/packages/schematics/angular/application/index.ts +++ b/packages/schematics/angular/application/index.ts @@ -223,6 +223,11 @@ function addAppToWorkspaceFile(options: ApplicationOptions, appDir: string): Rul type: 'initial', maximumWarning: '2mb', maximumError: '5mb', + }, + { + type: 'anyComponentStyle', + maximumWarning: '6kb', + maximumError: '10kb', }], }, }, diff --git a/packages/schematics/angular/migrations/update-9/update-workspace-config.ts b/packages/schematics/angular/migrations/update-9/update-workspace-config.ts index 3dc9f637ce53..ca8310724223 100644 --- a/packages/schematics/angular/migrations/update-9/update-workspace-config.ts +++ b/packages/schematics/angular/migrations/update-9/update-workspace-config.ts @@ -12,11 +12,17 @@ import { } from '@angular-devkit/core'; import { Rule, Tree, UpdateRecorder } from '@angular-devkit/schematics'; import { + appendValueInAstArray, findPropertyInAstObject, insertPropertyInAstObjectInOrder, removePropertyInAstObject, } from '../../utility/json-utils'; +export const ANY_COMPONENT_STYLE_BUDGET = { + type: 'anyComponentStyle', + maximumWarning: '6kb', +}; + export function UpdateWorkspaceConfig(): Rule { return (tree: Tree) => { let workspaceConfigPath = 'angular.json'; @@ -59,8 +65,9 @@ export function UpdateWorkspaceConfig(): Rule { const builder = findPropertyInAstObject(buildTarget, 'builder'); // Projects who's build builder is not build-angular:browser if (builder && builder.kind === 'string' && builder.value === '@angular-devkit/build-angular:browser') { - updateOption('styles', recorder, buildTarget); - updateOption('scripts', recorder, buildTarget); + updateStyleOrScriptOption('styles', recorder, buildTarget); + updateStyleOrScriptOption('scripts', recorder, buildTarget); + addAnyComponentStyleBudget(recorder, buildTarget); } } @@ -69,8 +76,8 @@ export function UpdateWorkspaceConfig(): Rule { const builder = findPropertyInAstObject(testTarget, 'builder'); // Projects who's build builder is not build-angular:browser if (builder && builder.kind === 'string' && builder.value === '@angular-devkit/build-angular:karma') { - updateOption('styles', recorder, testTarget); - updateOption('scripts', recorder, testTarget); + updateStyleOrScriptOption('styles', recorder, testTarget); + updateStyleOrScriptOption('scripts', recorder, testTarget); } } } @@ -84,19 +91,21 @@ export function UpdateWorkspaceConfig(): Rule { /** * Helper to retreive all the options in various configurations */ -function getAllOptions(builderConfig: JsonAstObject): JsonAstObject[] { +function getAllOptions(builderConfig: JsonAstObject, configurationsOnly = false): JsonAstObject[] { const options = []; const configurations = findPropertyInAstObject(builderConfig, 'configurations'); if (configurations && configurations.kind === 'object') { 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[]; } -function updateOption(property: 'scripts' | 'styles', recorder: UpdateRecorder, builderConfig: JsonAstObject) { +function updateStyleOrScriptOption(property: 'scripts' | 'styles', recorder: UpdateRecorder, builderConfig: JsonAstObject) { const options = getAllOptions(builderConfig); 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); + } + } +} 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 64865e83c83b..dd83dbdeb1f2 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 @@ -8,9 +8,19 @@ import { EmptyTree } from '@angular-devkit/schematics'; 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) { - return JSON.parse(tree.readContent('/angular.json')); +// tslint:disable-next-line: no-any +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 = [ @@ -69,60 +79,95 @@ describe('Migration to version 9', () => { .toPromise(); }); - it('should update scripts in build target', () => { - let config = readWorkspaceConfig(tree); - let build = config.projects['migration-test'].architect.build; - build.options.scripts = scriptsWithLazy; - build.configurations.production.scripts = scriptsWithLazy; - - tree.overwrite(workspacePath, JSON.stringify(config)); - const tree2 = schematicRunner.runSchematic('migration-09', {}, tree.branch()); - config = readWorkspaceConfig(tree2); - build = config.projects['migration-test'].architect.build; - expect(build.options.scripts).toEqual(scriptsExpectWithLazy); - expect(build.configurations.production.scripts).toEqual(scriptsExpectWithLazy); - }); - - it('should update styles in build target', () => { - let config = readWorkspaceConfig(tree); - let build = config.projects['migration-test'].architect.build; - build.options.styles = stylesWithLazy; - build.configurations.production.styles = stylesWithLazy; - - tree.overwrite(workspacePath, JSON.stringify(config)); - const tree2 = schematicRunner.runSchematic('migration-09', {}, tree.branch()); - config = readWorkspaceConfig(tree2); - 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', () => { - let config = readWorkspaceConfig(tree); - let test = config.projects['migration-test'].architect.test; - test.options.scripts = scriptsWithLazy; - test.configurations = { production: { scripts: scriptsWithLazy } }; - - tree.overwrite(workspacePath, JSON.stringify(config)); - const tree2 = schematicRunner.runSchematic('migration-09', {}, tree.branch()); - config = readWorkspaceConfig(tree2); - test = config.projects['migration-test'].architect.test; - expect(test.options.scripts).toEqual(scriptsExpectWithLazy); - expect(test.configurations.production.scripts).toEqual(scriptsExpectWithLazy); + describe('scripts and style options', () => { + it('should update scripts in build target', () => { + let config = getWorkspaceTargets(tree); + config.build.options.scripts = scriptsWithLazy; + config.build.configurations.production.scripts = scriptsWithLazy; + + updateWorkspaceTargets(tree, config); + const tree2 = schematicRunner.runSchematic('migration-09', {}, tree.branch()); + config = getWorkspaceTargets(tree2).build; + expect(config.options.scripts).toEqual(scriptsExpectWithLazy); + expect(config.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 test target', () => { - let config = readWorkspaceConfig(tree); - let test = config.projects['migration-test'].architect.test; - test.options.styles = stylesWithLazy; - test.configurations = { production: { styles: stylesWithLazy } }; - - tree.overwrite(workspacePath, JSON.stringify(config)); - const tree2 = schematicRunner.runSchematic('migration-09', {}, tree.branch()); - config = readWorkspaceConfig(tree2); - test = config.projects['migration-test'].architect.test; - expect(test.options.styles).toEqual(stylesExpectWithLazy); - expect(test.configurations.production.styles).toEqual(stylesExpectWithLazy); + describe('anyComponentStyle bundle budget', () => { + it('should not append budget when already exists', () => { + const defaultBudget = [ + { type: 'initial', maximumWarning: '2mb', maximumError: '5mb' }, + { type: 'anyComponentStyle', maximumWarning: '10kb', maximumError: '50kb' }, + ]; + + let config = getWorkspaceTargets(tree); + config.build.configurations.production.budgets = defaultBudget; + updateWorkspaceTargets(tree, config); + + const tree2 = schematicRunner.runSchematic('migration-09', {}, tree.branch()); + config = getWorkspaceTargets(tree2).build; + expect(config.configurations.production.budgets).toEqual(defaultBudget); + }); + + it('should append budget in build target', () => { + const defaultBudget = [{ type: 'initial', maximumWarning: '2mb', maximumError: '5mb' }]; + let config = getWorkspaceTargets(tree); + config.build.configurations.production.budgets = defaultBudget; + updateWorkspaceTargets(tree, config); + + const tree2 = schematicRunner.runSchematic('migration-09', {}, tree.branch()); + config = getWorkspaceTargets(tree2).build; + expect(config.configurations.production.budgets).toEqual([ + ...defaultBudget, + ANY_COMPONENT_STYLE_BUDGET, + ]); + }); + + it('should add budget in build target', () => { + let config = getWorkspaceTargets(tree); + config.build.configurations.production.budgets = undefined; + updateWorkspaceTargets(tree, config); + + const tree2 = schematicRunner.runSchematic('migration-09', {}, tree.branch()); + config = getWorkspaceTargets(tree2).build; + expect(config.configurations.production.budgets).toEqual([ANY_COMPONENT_STYLE_BUDGET]); + }); }); }); }); diff --git a/packages/schematics/angular/utility/config.ts b/packages/schematics/angular/utility/config.ts index 2d3b0c8c6cd0..5b72c4d4f5ed 100644 --- a/packages/schematics/angular/utility/config.ts +++ b/packages/schematics/angular/utility/config.ts @@ -130,7 +130,7 @@ export interface AppConfig { /** * The type of budget */ - type?: ('bundle' | 'initial' | 'allScript' | 'all' | 'anyScript' | 'any'); + type?: ('bundle' | 'initial' | 'allScript' | 'all' | 'anyScript' | 'any' | 'anyComponentStyle'); /** * The name of the bundle */ diff --git a/packages/schematics/angular/utility/json-utils.ts b/packages/schematics/angular/utility/json-utils.ts index f34c303f1717..72cb4937514f 100644 --- a/packages/schematics/angular/utility/json-utils.ts +++ b/packages/schematics/angular/utility/json-utils.ts @@ -31,7 +31,7 @@ export function appendPropertyInAstObject( recorder.insertRight(commaIndex, ','); index = end.offset; } - const content = JSON.stringify(value, null, indent).replace(/\n/g, indentStr); + const content = _stringifyContent(value, indentStr); recorder.insertRight( index, (node.properties.length === 0 && indent ? '\n' : '') @@ -84,7 +84,7 @@ export function insertPropertyInAstObjectInOrder( const insertIndex = insertAfterProp === null ? node.start.offset + 1 : insertAfterProp.end.offset + 1; - const content = JSON.stringify(value, null, indent).replace(/\n/g, indentStr); + const content = _stringifyContent(value, indentStr); recorder.insertRight( insertIndex, indentStr @@ -168,7 +168,7 @@ export function appendValueInAstArray( index, (node.elements.length === 0 && indent ? '\n' : '') + ' '.repeat(indent) - + JSON.stringify(value, null, indent).replace(/\n/g, indentStr) + + _stringifyContent(value, indentStr) + indentStr.slice(0, -indent), ); } @@ -191,3 +191,24 @@ export function findPropertyInAstObject( function _buildIndent(count: number): string { 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); +} diff --git a/packages/schematics/angular/utility/workspace-models.ts b/packages/schematics/angular/utility/workspace-models.ts index 9c8eebdcf78f..4c83b5f18f18 100644 --- a/packages/schematics/angular/utility/workspace-models.ts +++ b/packages/schematics/angular/utility/workspace-models.ts @@ -38,8 +38,8 @@ export interface BrowserBuilderBaseOptions { index?: string; polyfills: string; assets?: (object|string)[]; - styles?: string[]; - scripts?: string[]; + styles?: (object|string)[]; + scripts?: (object|string)[]; sourceMap?: boolean; }