diff --git a/docs/documentation/build.md b/docs/documentation/build.md index 4ae7f45898e9..2a3678bd904d 100644 --- a/docs/documentation/build.md +++ b/docs/documentation/build.md @@ -332,3 +332,13 @@ Note: service worker support is experimental and subject to change. Show circular dependency warnings on builds.

+ +
+ build-optimizer +

+ --build-optimizer (aliases: -bo) +

+

+ (Experimental) Enables @angular-devkit/build-optimizer optimizations when using `--aot`. +

+
diff --git a/package.json b/package.json index f90128dd3698..5df01f21c664 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ }, "homepage": "https://github.com/angular/angular-cli", "dependencies": { + "@angular-devkit/build-optimizer": "0.0.3", "autoprefixer": "^6.5.3", "chalk": "^1.1.3", "circular-dependency-plugin": "^3.0.0", diff --git a/packages/@angular/cli/commands/build.ts b/packages/@angular/cli/commands/build.ts index fb609ae4a322..2138b1aa028d 100644 --- a/packages/@angular/cli/commands/build.ts +++ b/packages/@angular/cli/commands/build.ts @@ -48,7 +48,6 @@ export const baseBuildCommandOptions: any = [ { name: 'vendor-chunk', type: Boolean, - default: true, aliases: ['vc'], description: 'Use a separate bundle containing only vendor libraries.' }, @@ -159,6 +158,14 @@ export const baseBuildCommandOptions: any = [ aliases: ['scd'], description: 'Show circular dependency warnings on builds.', default: buildConfigDefaults['showCircularDependencies'] + }, + { + name: 'build-optimizer', + type: Boolean, + default: false, + aliases: ['bo'], + description: '(Experimental) Enables @angular-devkit/build-optimizer ' + + 'optimizations when using `--aot`.' } ]; @@ -185,6 +192,11 @@ const BuildCommand = Command.extend({ // Check angular version. Version.assertAngularVersionIs2_3_1OrHigher(this.project.root); + // Default vendor chunk to false when build optimizer is on. + if (commandOptions.vendorChunk === undefined) { + commandOptions.vendorChunk = !commandOptions.buildOptimizer; + } + const BuildTask = require('../tasks/build').default; const buildTask = new BuildTask({ diff --git a/packages/@angular/cli/commands/eject.ts b/packages/@angular/cli/commands/eject.ts index f4e625f8d2ce..4983d691785f 100644 --- a/packages/@angular/cli/commands/eject.ts +++ b/packages/@angular/cli/commands/eject.ts @@ -32,6 +32,12 @@ const EjectCommand = Command.extend({ availableOptions: baseEjectCommandOptions, run: function (commandOptions: EjectTaskOptions) { + + // Default vendor chunk to false when build optimizer is on. + if (commandOptions.vendorChunk === undefined) { + commandOptions.vendorChunk = !commandOptions.buildOptimizer; + } + const EjectTask = require('../tasks/eject').default; const ejectTask = new EjectTask({ project: this.project, diff --git a/packages/@angular/cli/commands/serve.ts b/packages/@angular/cli/commands/serve.ts index ee4194f8a571..85fa09caba03 100644 --- a/packages/@angular/cli/commands/serve.ts +++ b/packages/@angular/cli/commands/serve.ts @@ -121,6 +121,12 @@ const ServeCommand = Command.extend({ const ServeTask = require('../tasks/serve').default; Version.assertAngularVersionIs2_3_1OrHigher(this.project.root); + + // Default vendor chunk to false when build optimizer is on. + if (commandOptions.vendorChunk === undefined) { + commandOptions.vendorChunk = !commandOptions.buildOptimizer; + } + return checkPort(commandOptions.port, commandOptions.host, defaultPort) .then(port => { commandOptions.port = port; diff --git a/packages/@angular/cli/models/build-options.ts b/packages/@angular/cli/models/build-options.ts index 55be02530677..e8e2265eddce 100644 --- a/packages/@angular/cli/models/build-options.ts +++ b/packages/@angular/cli/models/build-options.ts @@ -22,4 +22,5 @@ export interface BuildOptions { preserveSymlinks?: boolean; extractLicenses?: boolean; showCircularDependencies?: boolean; + buildOptimizer?: boolean; } diff --git a/packages/@angular/cli/models/webpack-config.ts b/packages/@angular/cli/models/webpack-config.ts index e12daf8842b9..e3a1b2061d30 100644 --- a/packages/@angular/cli/models/webpack-config.ts +++ b/packages/@angular/cli/models/webpack-config.ts @@ -70,6 +70,11 @@ export class NgCliWebpackConfig { if (buildOptions.target !== 'development' && buildOptions.target !== 'production') { throw new Error("Invalid build target. Only 'development' and 'production' are available."); } + + if (buildOptions.buildOptimizer + && !(buildOptions.aot || buildOptions.target === 'production')) { + throw new Error('The `--build-optimizer` option cannot be used without `--aot`.'); + } } // Fill in defaults for build targets diff --git a/packages/@angular/cli/models/webpack-configs/common.ts b/packages/@angular/cli/models/webpack-configs/common.ts index 1c5580c950ed..4a23ab39d311 100644 --- a/packages/@angular/cli/models/webpack-configs/common.ts +++ b/packages/@angular/cli/models/webpack-configs/common.ts @@ -19,6 +19,7 @@ const CircularDependencyPlugin = require('circular-dependency-plugin'); * require('json-loader') * require('url-loader') * require('file-loader') + * require('@angular-devkit/build-optimizer') */ export function getCommonConfig(wco: WebpackConfigOptions) { @@ -71,6 +72,16 @@ export function getCommonConfig(wco: WebpackConfigOptions) { })); } + if (buildOptions.buildOptimizer) { + extraRules.push({ + test: /\.js$/, + use: [{ + loader: '@angular-devkit/build-optimizer/webpack-loader', + options: { sourceMap: buildOptions.sourcemaps } + }] + }); + } + return { resolve: { extensions: ['.ts', '.js'], @@ -107,7 +118,7 @@ export function getCommonConfig(wco: WebpackConfigOptions) { node: { fs: 'empty', // `global` should be kept true, removing it resulted in a - // massive size increase with NGO on AIO. + // massive size increase with Build Optimizer on AIO. global: true, crypto: 'empty', tls: 'empty', diff --git a/packages/@angular/cli/models/webpack-configs/production.ts b/packages/@angular/cli/models/webpack-configs/production.ts index c86975ac2ab5..4e574f7428d2 100644 --- a/packages/@angular/cli/models/webpack-configs/production.ts +++ b/packages/@angular/cli/models/webpack-configs/production.ts @@ -3,6 +3,7 @@ import * as webpack from 'webpack'; import * as fs from 'fs'; import * as semver from 'semver'; import { stripIndent } from 'common-tags'; +import { PurifyPlugin } from '@angular-devkit/build-optimizer'; import { StaticAssetPlugin } from '../../plugins/static-asset'; import { GlobCopyWebpackPlugin } from '../../plugins/glob-copy-webpack-plugin'; import { WebpackConfigOptions } from '../webpack-config'; @@ -91,9 +92,17 @@ export const getProdConfig = function (wco: WebpackConfigOptions) { })); } + const uglifyCompressOptions: any = { screw_ie8: true, warnings: buildOptions.verbose }; + + if (buildOptions.buildOptimizer) { + // This plugin must be before webpack.optimize.UglifyJsPlugin. + extraPlugins.push(new PurifyPlugin()); + uglifyCompressOptions.pure_getters = true; + } + return { entry: entryPoints, - plugins: [ + plugins: extraPlugins.concat([ new webpack.EnvironmentPlugin({ 'NODE_ENV': 'production' }), @@ -101,10 +110,10 @@ export const getProdConfig = function (wco: WebpackConfigOptions) { new webpack.optimize.ModuleConcatenationPlugin(), new webpack.optimize.UglifyJsPlugin({ mangle: { screw_ie8: true }, - compress: { screw_ie8: true, warnings: buildOptions.verbose }, + compress: uglifyCompressOptions, sourceMap: buildOptions.sourcemaps, comments: false }) - ].concat(extraPlugins) + ]) }; }; diff --git a/packages/@angular/cli/models/webpack-configs/typescript.ts b/packages/@angular/cli/models/webpack-configs/typescript.ts index 51a6cb77fd18..ba58d224ba64 100644 --- a/packages/@angular/cli/models/webpack-configs/typescript.ts +++ b/packages/@angular/cli/models/webpack-configs/typescript.ts @@ -74,7 +74,6 @@ function _createAotPlugin(wco: WebpackConfigOptions, options: any) { }, options)); } - export const getNonAotConfig = function(wco: WebpackConfigOptions) { const { appConfig, projectRoot } = wco; const tsConfigPath = path.resolve(projectRoot, appConfig.root, appConfig.tsconfig); @@ -86,7 +85,7 @@ export const getNonAotConfig = function(wco: WebpackConfigOptions) { }; export const getAotConfig = function(wco: WebpackConfigOptions) { - const { projectRoot, appConfig } = wco; + const { projectRoot, buildOptions, appConfig } = wco; const tsConfigPath = path.resolve(projectRoot, appConfig.root, appConfig.tsconfig); const testTsConfigPath = path.resolve(projectRoot, appConfig.root, appConfig.testTsconfig); @@ -99,8 +98,16 @@ export const getAotConfig = function(wco: WebpackConfigOptions) { pluginOptions.exclude = exclude; } + let boLoader: any = []; + if (buildOptions.buildOptimizer) { + boLoader = [{ + loader: '@angular-devkit/build-optimizer/webpack-loader', + options: { sourceMap: buildOptions.sourcemaps } + }]; + } + return { - module: { rules: [{ test: /\.ts$/, loader: webpackLoader }] }, + module: { rules: [{ test: /\.ts$/, use: [...boLoader, webpackLoader] }] }, plugins: [ _createAotPlugin(wco, pluginOptions) ] }; }; diff --git a/packages/@angular/cli/package.json b/packages/@angular/cli/package.json index 2c4483305e93..9e83474cea4e 100644 --- a/packages/@angular/cli/package.json +++ b/packages/@angular/cli/package.json @@ -27,6 +27,7 @@ }, "homepage": "https://github.com/angular/angular-cli", "dependencies": { + "@angular-devkit/build-optimizer": "0.0.3", "@ngtools/json-schema": "1.1.0", "@ngtools/webpack": "1.6.0-beta.1", "autoprefixer": "^6.5.3", diff --git a/tests/e2e/tests/build/build-optimizer.ts b/tests/e2e/tests/build/build-optimizer.ts new file mode 100644 index 000000000000..a1bc94329962 --- /dev/null +++ b/tests/e2e/tests/build/build-optimizer.ts @@ -0,0 +1,10 @@ +import { ng } from '../../utils/process'; +import { expectFileToMatch, expectFileToExist } from '../../utils/fs'; +import { expectToFail } from '../../utils/utils'; + + +export default function () { + return ng('build', '--aot', '--bo') + .then(() => expectToFail(() => expectFileToExist('dist/vendor.js'))) + .then(() => expectToFail(() => expectFileToMatch('dist/main.js', /\.decorators =/))); +} diff --git a/yarn.lock b/yarn.lock index 2842334bdda9..9527cafadd1f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,15 @@ # yarn lockfile v1 +"@angular-devkit/build-optimizer@0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-optimizer/-/build-optimizer-0.0.3.tgz#092bdf732b79a779ce540f9bb99d6590dd971204" + dependencies: + loader-utils "^1.1.0" + magic-string "^0.19.1" + source-map "^0.5.6" + typescript "^2.3.3" + "@angular/compiler-cli@^4.0.0": version "4.2.4" resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-4.2.4.tgz#cce941a28362fc1c042ab85890fcaab1e233dd57" @@ -3143,7 +3152,7 @@ macaddress@^0.2.8: version "0.2.8" resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12" -magic-string@^0.19.0: +magic-string@^0.19.0, magic-string@^0.19.1: version "0.19.1" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.19.1.tgz#14d768013caf2ec8fdea16a49af82fc377e75201" dependencies: @@ -5242,7 +5251,7 @@ typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" -typescript@~2.3.1: +typescript@^2.3.3, typescript@~2.3.1: version "2.3.4" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.3.4.tgz#3d38321828231e434f287514959c37a82b629f42"