diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/models/build-options.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/build-options.ts index 649d287ad144..422e37cfb291 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/models/build-options.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/build-options.ts @@ -16,9 +16,10 @@ import { CurrentFileReplacement, ExtraEntryPoint, } from '../../browser/schema'; +import { NormalizedOptimization } from '../../utils/index'; export interface BuildOptions { - optimization: boolean; + optimization: NormalizedOptimization; environment?: string; outputPath: string; resourcesOutputPath?: string; diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/browser.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/browser.ts index 0fc80f090891..3eae2eaf5d63 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/browser.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/browser.ts @@ -20,8 +20,12 @@ export function getBrowserConfig(wco: WebpackConfigOptions) { const extraPlugins = []; let isEval = false; + const { styles: stylesOptimization, scripts: scriptsOptimization } = buildOptions.optimization; // See https://webpack.js.org/configuration/devtool/ for sourcemap types. - if (buildOptions.sourceMap && buildOptions.evalSourceMap && !buildOptions.optimization) { + if (buildOptions.sourceMap && + buildOptions.evalSourceMap && + !stylesOptimization && + !scriptsOptimization) { // Produce eval sourcemaps for development with serve, which are faster. isEval = true; } diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts index 147f770912ea..d12f7208dff4 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts @@ -33,6 +33,7 @@ export const buildOptimizerLoader: string = g['_DevKitIsLocal'] // tslint:disable-next-line:no-big-function export function getCommonConfig(wco: WebpackConfigOptions) { const { root, projectRoot, buildOptions } = wco; + const { styles: stylesOptimization, scripts: scriptsOptimization } = buildOptions.optimization; const nodeModules = findUp('node_modules', projectRoot); if (!nodeModules) { @@ -204,37 +205,61 @@ export function getCommonConfig(wco: WebpackConfigOptions) { alias = rxPaths(nodeModules); } catch { } - const terserOptions = { - ecma: wco.supportES2015 ? 6 : 5, - warnings: !!buildOptions.verbose, - safari10: true, - output: { - ascii_only: true, - comments: false, - webkit: true, - }, + const extraMinimizers = []; + if (stylesOptimization) { + extraMinimizers.push( + new CleanCssWebpackPlugin({ + sourceMap: buildOptions.stylesSourceMap, + // component styles retain their original file name + test: (file) => /\.(?:css|scss|sass|less|styl)$/.test(file), + }), + ); + } - // On server, we don't want to compress anything. We still set the ngDevMode = false for it - // to remove dev code. - compress: (buildOptions.platform == 'server' ? { - global_defs: { - ngDevMode: false, - }, - } : { - pure_getters: buildOptions.buildOptimizer, - // PURE comments work best with 3 passes. - // See https://github.com/webpack/webpack/issues/2899#issuecomment-317425926. - passes: buildOptions.buildOptimizer ? 3 : 1, - global_defs: { - ngDevMode: false, + if (scriptsOptimization) { + const terserOptions = { + ecma: wco.supportES2015 ? 6 : 5, + warnings: !!buildOptions.verbose, + safari10: true, + output: { + ascii_only: true, + comments: false, + webkit: true, }, - }), - // We also want to avoid mangling on server. - ...(buildOptions.platform == 'server' ? { mangle: false } : {}), - }; + + // On server, we don't want to compress anything. We still set the ngDevMode = false for it + // to remove dev code. + compress: (buildOptions.platform == 'server' ? { + global_defs: { + ngDevMode: false, + }, + } : { + pure_getters: buildOptions.buildOptimizer, + // PURE comments work best with 3 passes. + // See https://github.com/webpack/webpack/issues/2899#issuecomment-317425926. + passes: buildOptions.buildOptimizer ? 3 : 1, + global_defs: { + ngDevMode: false, + }, + }), + // We also want to avoid mangling on server. + ...(buildOptions.platform == 'server' ? { mangle: false } : {}), + }; + + extraMinimizers.push( + new TerserPlugin({ + sourceMap: buildOptions.scriptsSourceMap, + parallel: true, + cache: true, + terserOptions, + }), + ); + } return { - mode: buildOptions.optimization ? 'production' : 'development', + mode: scriptsOptimization || stylesOptimization + ? 'production' + : 'development', devtool: false, resolve: { extensions: ['.ts', '.tsx', '.mjs', '.js'], @@ -296,17 +321,7 @@ export function getCommonConfig(wco: WebpackConfigOptions) { new HashedModuleIdsPlugin(), // TODO: check with Mike what this feature needs. new BundleBudgetPlugin({ budgets: buildOptions.budgets }), - new CleanCssWebpackPlugin({ - sourceMap: buildOptions.stylesSourceMap, - // component styles retain their original file name - test: (file) => /\.(?:css|scss|sass|less|styl)$/.test(file), - }), - new TerserPlugin({ - sourceMap: buildOptions.scriptsSourceMap, - parallel: true, - cache: true, - terserOptions, - }), + ...extraMinimizers, ], }, plugins: extraPlugins, diff --git a/packages/angular_devkit/build_angular/src/browser/index.ts b/packages/angular_devkit/build_angular/src/browser/index.ts index 1f3b3fa468f9..f2b9b09c7af1 100644 --- a/packages/angular_devkit/build_angular/src/browser/index.ts +++ b/packages/angular_devkit/build_angular/src/browser/index.ts @@ -35,10 +35,12 @@ import { statsWarningsToString, } from '../angular-cli-files/utilities/stats'; import { + NormalizedOptimization, NormalizedSourceMaps, defaultProgress, normalizeAssetPatterns, normalizeFileReplacements, + normalizeOptimization, normalizeSourceMaps, } from '../utils'; import { @@ -56,10 +58,14 @@ const webpackMerge = require('webpack-merge'); // BrowserBuildSchema and BrowserBuilder.buildWebpackConfig. // It would really help if it happens during architect.validateBuilderOptions, or similar. export interface NormalizedBrowserBuilderSchema extends - Pick>, + Pick< + BrowserBuilderSchema, + Exclude + >, NormalizedSourceMaps { assets: AssetPatternObject[]; fileReplacements: CurrentFileReplacement[]; + optimization: NormalizedOptimization; } export class BrowserBuilder implements Builder { @@ -92,13 +98,18 @@ export class BrowserBuilder implements Builder { options = { ...options, ...normalizedOptions, + // tslint:disable-next-line:no-any + optimization: normalizeOptimization(options.optimization) as any, }; }), concatMap(() => { let webpackConfig; try { webpackConfig = this.buildWebpackConfig(root, projectRoot, host, - options as NormalizedBrowserBuilderSchema); + // todo replace with unknown + // we need to find a clear way to create this options + // tslint:disable-next-line:no-any + options as any as NormalizedBrowserBuilderSchema); } catch (e) { return throwError(e); } diff --git a/packages/angular_devkit/build_angular/src/browser/schema.d.ts b/packages/angular_devkit/build_angular/src/browser/schema.d.ts index ba27d8ab0418..c9c9d02158b8 100644 --- a/packages/angular_devkit/build_angular/src/browser/schema.d.ts +++ b/packages/angular_devkit/build_angular/src/browser/schema.d.ts @@ -44,7 +44,7 @@ export interface BrowserBuilderSchema { /** * Enables optimization of the build output. */ - optimization: boolean; + optimization: OptimizationOptions; /** * Replace files with other files in the build. @@ -237,6 +237,15 @@ export interface BrowserBuilderSchema { profile: boolean; } +export type OptimizationOptions = boolean | OptimizationObject; + +export interface OptimizationObject { + /** Enables optimization of the scripts output. */ + scripts?: boolean; + /** Enables optimization of the styles output. */ + styles?: boolean; +} + export type SourceMapOptions = boolean | SourceMapObject; export interface SourceMapObject { diff --git a/packages/angular_devkit/build_angular/src/browser/schema.json b/packages/angular_devkit/build_angular/src/browser/schema.json index 13c58553d0a4..913c0660c81b 100644 --- a/packages/angular_devkit/build_angular/src/browser/schema.json +++ b/packages/angular_devkit/build_angular/src/browser/schema.json @@ -55,9 +55,29 @@ "additionalProperties": false }, "optimization": { - "type": "boolean", - "description": "When true, uses optimization for the app build.", - "default": false + "description": "Enables optimization of the build output.", + "default": false, + "oneOf": [ + { + "type": "object", + "properties": { + "scripts": { + "type": "boolean", + "description": "Enables optimization of the scripts output.", + "default": true + }, + "styles": { + "type": "boolean", + "description": "Enables optimization of the styles output.", + "default": true + } + }, + "additionalProperties": false + }, + { + "type": "boolean" + } + ] }, "fileReplacements": { "description": "Replace files with other files in the build.", diff --git a/packages/angular_devkit/build_angular/src/dev-server/index.ts b/packages/angular_devkit/build_angular/src/dev-server/index.ts index f72fe3caa3be..394b2ed1babb 100644 --- a/packages/angular_devkit/build_angular/src/dev-server/index.ts +++ b/packages/angular_devkit/build_angular/src/dev-server/index.ts @@ -24,7 +24,12 @@ import * as WebpackDevServer from 'webpack-dev-server'; import { checkPort } from '../angular-cli-files/utilities/check-port'; import { BrowserBuilder, NormalizedBrowserBuilderSchema, getBrowserLoggingCb } from '../browser/'; import { BrowserBuilderSchema } from '../browser/schema'; -import { normalizeAssetPatterns, normalizeFileReplacements, normalizeSourceMaps } from '../utils'; +import { + normalizeAssetPatterns, + normalizeFileReplacements, + normalizeOptimization, + normalizeSourceMaps, +} from '../utils'; const opn = require('opn'); @@ -86,15 +91,26 @@ export class DevServerBuilder implements Builder { browserOptions = { ...browserOptions, ...normalizedOptions, + // tslint:disable-next-line:no-any + optimization: normalizeOptimization(options.optimization) as any, }; }), concatMap(() => { const webpackConfig = this.buildWebpackConfig( - root, projectRoot, host, browserOptions as NormalizedBrowserBuilderSchema); + // todo should be unknown + // tslint:disable-next-line:no-any + root, projectRoot, host, browserOptions as any as NormalizedBrowserBuilderSchema, + ); let webpackDevServerConfig: WebpackDevServer.Configuration; try { - webpackDevServerConfig = this._buildServerConfig(root, options, browserOptions); + webpackDevServerConfig = this._buildServerConfig( + root, + options, + // todo should be unknown + // tslint:disable-next-line:no-any + browserOptions as any as NormalizedBrowserBuilderSchema, + ); } catch (err) { return throwError(err); } @@ -179,11 +195,11 @@ export class DevServerBuilder implements Builder { root: Path, projectRoot: Path, host: virtualFs.Host, - browserOptions: BrowserBuilderSchema, + browserOptions: NormalizedBrowserBuilderSchema, ) { const browserBuilder = new BrowserBuilder(this.context); const webpackConfig = browserBuilder.buildWebpackConfig( - root, projectRoot, host, browserOptions as NormalizedBrowserBuilderSchema); + root, projectRoot, host, browserOptions); return webpackConfig; } @@ -191,7 +207,7 @@ export class DevServerBuilder implements Builder { private _buildServerConfig( root: Path, options: DevServerBuilderOptions, - browserOptions: BrowserBuilderSchema, + browserOptions: NormalizedBrowserBuilderSchema, ) { const systemRoot = getSystemPath(root); if (options.disableHostCheck) { @@ -203,6 +219,7 @@ export class DevServerBuilder implements Builder { } const servePath = this._buildServePath(options, browserOptions); + const { styles, scripts } = browserOptions.optimization; const config: WebpackDevServer.Configuration = { host: options.host, @@ -214,13 +231,13 @@ export class DevServerBuilder implements Builder { htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'], } as WebpackDevServer.HistoryApiFallbackConfig, stats: false, - compress: browserOptions.optimization, + compress: styles || scripts, watchOptions: { poll: browserOptions.poll, }, https: options.ssl, overlay: { - errors: !browserOptions.optimization, + errors: !(styles || scripts), warnings: false, }, public: options.publicHost, @@ -332,7 +349,10 @@ export class DevServerBuilder implements Builder { config.proxy = proxyConfig; } - private _buildServePath(options: DevServerBuilderOptions, browserOptions: BrowserBuilderSchema) { + private _buildServePath( + options: DevServerBuilderOptions, + browserOptions: NormalizedBrowserBuilderSchema, + ) { let servePath = options.servePath; if (!servePath && servePath !== '') { const defaultServePath = diff --git a/packages/angular_devkit/build_angular/src/dev-server/schema.json b/packages/angular_devkit/build_angular/src/dev-server/schema.json index c69da96418a2..d4e59a1ea066 100644 --- a/packages/angular_devkit/build_angular/src/dev-server/schema.json +++ b/packages/angular_devkit/build_angular/src/dev-server/schema.json @@ -84,8 +84,29 @@ "default": true }, "optimization": { - "type": "boolean", - "description": "Enables optimization of the build output." + "description": "Enables optimization of the build output.", + "default": false, + "oneOf": [ + { + "type": "object", + "properties": { + "scripts": { + "type": "boolean", + "description": "Enables optimization of the scripts output.", + "default": true + }, + "styles": { + "type": "boolean", + "description": "Enables optimization of the styles output.", + "default": true + } + }, + "additionalProperties": false + }, + { + "type": "boolean" + } + ] }, "aot": { "type": "boolean", diff --git a/packages/angular_devkit/build_angular/src/server/index.ts b/packages/angular_devkit/build_angular/src/server/index.ts index fdd2f7094dd4..8bcbfa7c1e1f 100644 --- a/packages/angular_devkit/build_angular/src/server/index.ts +++ b/packages/angular_devkit/build_angular/src/server/index.ts @@ -30,7 +30,12 @@ import { import { readTsconfig } from '../angular-cli-files/utilities/read-tsconfig'; import { requireProjectModule } from '../angular-cli-files/utilities/require-project-module'; import { getBrowserLoggingCb } from '../browser'; -import { defaultProgress, normalizeFileReplacements, normalizeSourceMaps } from '../utils'; +import { + defaultProgress, + normalizeFileReplacements, + normalizeOptimization, + normalizeSourceMaps, +} from '../utils'; import { BuildWebpackServerSchema } from './schema'; const webpackMerge = require('webpack-merge'); @@ -62,6 +67,8 @@ export class ServerBuilder implements Builder { options = { ...options, ...normalizedOptions, + // tslint:disable-next-line:no-any + optimization: normalizeOptimization(options.optimization || {}) as any, }; }), concatMap(() => { diff --git a/packages/angular_devkit/build_angular/src/server/schema.d.ts b/packages/angular_devkit/build_angular/src/server/schema.d.ts index bb00e4fd2fea..383c7c8f36b2 100644 --- a/packages/angular_devkit/build_angular/src/server/schema.d.ts +++ b/packages/angular_devkit/build_angular/src/server/schema.d.ts @@ -6,7 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ -import { BrowserBuilderSchema, FileReplacement, SourceMapOptions } from '../browser/schema'; +import { + BrowserBuilderSchema, + FileReplacement, + OptimizationObject, + SourceMapOptions, +} from '../browser/schema'; export interface BuildWebpackServerSchema { /** @@ -72,7 +77,7 @@ export interface BuildWebpackServerSchema { /** * Enables optimization of the build output. */ - optimization?: boolean; + optimization?: OptimizationObject; /** * Log progress to the console while building. */ diff --git a/packages/angular_devkit/build_angular/src/server/schema.json b/packages/angular_devkit/build_angular/src/server/schema.json index 3cd83727fd61..8a502c583754 100644 --- a/packages/angular_devkit/build_angular/src/server/schema.json +++ b/packages/angular_devkit/build_angular/src/server/schema.json @@ -28,9 +28,29 @@ "additionalProperties": false }, "optimization": { - "type": "boolean", "description": "Enables optimization of the build output.", - "default": false + "default": false, + "oneOf": [ + { + "type": "object", + "properties": { + "scripts": { + "type": "boolean", + "description": "Enables optimization of the scripts output.", + "default": true + }, + "styles": { + "type": "boolean", + "description": "Enables optimization of the styles output.", + "default": true + } + }, + "additionalProperties": false + }, + { + "type": "boolean" + } + ] }, "fileReplacements": { "description": "Replace files with other files in the build.", diff --git a/packages/angular_devkit/build_angular/src/utils/index.ts b/packages/angular_devkit/build_angular/src/utils/index.ts index e4b77f57dcbe..190ca556a3ae 100644 --- a/packages/angular_devkit/build_angular/src/utils/index.ts +++ b/packages/angular_devkit/build_angular/src/utils/index.ts @@ -11,3 +11,4 @@ export * from './run-module-as-observable-fork'; export * from './normalize-file-replacements'; export * from './normalize-asset-patterns'; export * from './normalize-source-maps'; +export * from './normalize-optimization'; diff --git a/packages/angular_devkit/build_angular/src/utils/normalize-optimization.ts b/packages/angular_devkit/build_angular/src/utils/normalize-optimization.ts new file mode 100644 index 000000000000..36aa1e00420a --- /dev/null +++ b/packages/angular_devkit/build_angular/src/utils/normalize-optimization.ts @@ -0,0 +1,24 @@ +/** + * @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 { OptimizationOptions, SourceMapOptions } from '../browser/schema'; + +export interface NormalizedOptimization { + scripts: boolean; + styles: boolean; +} + +export function normalizeOptimization(optimization: OptimizationOptions): NormalizedOptimization { + const scripts = !!(typeof optimization === 'object' ? optimization.scripts : optimization); + const styles = !!(typeof optimization === 'object' ? optimization.styles : optimization); + + return { + scripts, + styles, + }; +} diff --git a/packages/angular_devkit/build_angular/test/browser/optimization-level_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/optimization-level_spec_large.ts index 932ae0928baa..61d0cfff1ecc 100644 --- a/packages/angular_devkit/build_angular/test/browser/optimization-level_spec_large.ts +++ b/packages/angular_devkit/build_angular/test/browser/optimization-level_spec_large.ts @@ -9,6 +9,7 @@ import { DefaultTimeout, runTargetSpec } from '@angular-devkit/architect/testing'; import { join, normalize, virtualFs } from '@angular-devkit/core'; import { tap } from 'rxjs/operators'; +import { BrowserBuilderSchema } from '../../src/browser/schema'; import { browserTargetSpec, host } from '../utils'; @@ -46,4 +47,72 @@ describe('Browser Builder optimization level', () => { }), ).toPromise().then(done, done.fail); }); + + it('supports styles only optimizations', (done) => { + const overrides: Partial = { + optimization: { + styles: true, + scripts: false, + }, + aot: true, + extractCss: true, + styles: ['src/styles.css'], + }; + + host.appendToFile('src/main.ts', '/** js comment should not be dropped */'); + host.appendToFile('src/app/app.component.css', 'div { color: white }'); + host.writeMultipleFiles({ + 'src/styles.css': `div { color: white }`, + }); + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const scriptContent = virtualFs.fileBufferToString( + host.scopedSync().read(join(outputPath, 'main.js')), + ); + expect(scriptContent).toContain('js comment should not be dropped'); + expect(scriptContent).toContain('color:#fff'); + + const styleContent = virtualFs.fileBufferToString( + host.scopedSync().read(join(outputPath, 'styles.css')), + ); + expect(styleContent).toContain('color:#fff'); + }), + ).toPromise().then(done, done.fail); + }); + + it('supports scripts only optimizations', (done) => { + const overrides: Partial = { + optimization: { + styles: false, + scripts: true, + }, + aot: true, + extractCss: true, + styles: ['src/styles.css'], + }; + + host.appendToFile('src/main.ts', '/** js comment should be dropped */'); + host.appendToFile('src/app/app.component.css', 'div { color: white }'); + host.writeMultipleFiles({ + 'src/styles.css': `div { color: white }`, + }); + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const scriptContent = virtualFs.fileBufferToString( + host.scopedSync().read(join(outputPath, 'main.js')), + ); + expect(scriptContent).not.toContain('js comment should be dropped'); + expect(scriptContent).toContain('color: white'); + + const styleContent = virtualFs.fileBufferToString( + host.scopedSync().read(join(outputPath, 'styles.css')), + ); + expect(styleContent).toContain('color: white'); + }), + ).toPromise().then(done, done.fail); + }); });