From dea04b1d5707a42819df18f9316536cb830d26fc Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Wed, 6 Sep 2017 15:12:07 +0100 Subject: [PATCH] feat(@angular/cli): support ES2015 target --- package-lock.json | 105 +++++++++++------- package.json | 1 + .../cli/models/webpack-configs/common.ts | 12 ++ .../cli/models/webpack-configs/production.ts | 56 ++++++++-- packages/@angular/cli/package.json | 1 + packages/@angular/cli/tasks/eject.ts | 10 ++ .../@angular/cli/utilities/read-tsconfig.ts | 10 ++ tests/e2e/tests/build/script-target.ts | 23 ++++ 8 files changed, 164 insertions(+), 54 deletions(-) create mode 100644 packages/@angular/cli/utilities/read-tsconfig.ts create mode 100644 tests/e2e/tests/build/script-target.ts diff --git a/package-lock.json b/package-lock.json index a7ce48cd5c33..343656eeebfb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7505,54 +7505,22 @@ "optional": true }, "uglifyjs-webpack-plugin": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz", - "integrity": "sha1-uVH0q7a9YX5m9j64kUmOORdj4wk=", + "version": "1.0.0-beta.1", + "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.0.0-beta.1.tgz", + "integrity": "sha1-WVwU2Lhb7dcIq3pugRiU++DmmJA=", "requires": { "source-map": "0.5.6", - "uglify-js": "2.8.29", + "uglify-es": "3.0.28", "webpack-sources": "1.0.1" }, "dependencies": { - "camelcase": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" - }, - "cliui": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", - "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", - "requires": { - "center-align": "0.1.3", - "right-align": "0.1.3", - "wordwrap": "0.0.2" - } - }, - "uglify-js": { - "version": "2.8.29", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", - "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", - "requires": { - "source-map": "0.5.6", - "uglify-to-browserify": "1.0.2", - "yargs": "3.10.0" - } - }, - "wordwrap": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=" - }, - "yargs": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", - "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "uglify-es": { + "version": "3.0.28", + "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.0.28.tgz", + "integrity": "sha512-xw1hJsSp361OO0Sq0XvNyTI2wfQ4eKNljfSYyeYX/dz9lKEDj+DK+A8CzB0NmoCwWX1MnEx9f16HlkKXyG65CQ==", "requires": { - "camelcase": "1.2.1", - "cliui": "2.1.0", - "decamelize": "1.2.0", - "window-size": "0.1.0" + "commander": "2.11.0", + "source-map": "0.5.6" } } } @@ -7877,11 +7845,64 @@ "has-flag": "2.0.0" } }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "requires": { + "source-map": "0.5.6", + "uglify-to-browserify": "1.0.2", + "yargs": "3.10.0" + }, + "dependencies": { + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "requires": { + "center-align": "0.1.3", + "right-align": "0.1.3", + "wordwrap": "0.0.2" + } + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "requires": { + "camelcase": "1.2.1", + "cliui": "2.1.0", + "decamelize": "1.2.0", + "window-size": "0.1.0" + } + } + } + }, + "uglifyjs-webpack-plugin": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz", + "integrity": "sha1-uVH0q7a9YX5m9j64kUmOORdj4wk=", + "requires": { + "source-map": "0.5.6", + "uglify-js": "2.8.29", + "webpack-sources": "1.0.1" + } + }, "which-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" }, + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=" + }, "yargs": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-8.0.2.tgz", diff --git a/package.json b/package.json index eab60eb72d6a..1b71eca82a16 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "stylus": "^0.54.5", "stylus-loader": "^3.0.1", "typescript": "~2.4.2", + "uglifyjs-webpack-plugin": "1.0.0-beta.1", "url-loader": "^0.5.7", "webpack": "~3.5.5", "webpack-concat-plugin": "1.4.0", diff --git a/packages/@angular/cli/models/webpack-configs/common.ts b/packages/@angular/cli/models/webpack-configs/common.ts index 82afcaab7cff..e8806ddb1f50 100644 --- a/packages/@angular/cli/models/webpack-configs/common.ts +++ b/packages/@angular/cli/models/webpack-configs/common.ts @@ -1,11 +1,13 @@ import * as webpack from 'webpack'; import * as path from 'path'; import * as CopyWebpackPlugin from 'copy-webpack-plugin'; +import * as ts from 'typescript'; import { NamedLazyChunksWebpackPlugin } from '../../plugins/named-lazy-chunks-webpack-plugin'; import { InsertConcatAssetsWebpackPlugin } from '../../plugins/insert-concat-assets-webpack-plugin'; import { extraEntryParser, getOutputHashFormat, AssetPattern } from './utils'; import { isDirectory } from '../../utilities/is-directory'; import { WebpackConfigOptions } from '../webpack-config'; +import { readTsconfig } from '../../utilities/read-tsconfig'; const ConcatPlugin = require('webpack-concat-plugin'); const ProgressPlugin = require('webpack/lib/ProgressPlugin'); @@ -149,10 +151,20 @@ export function getCommonConfig(wco: WebpackConfigOptions) { extraPlugins.push(new NamedLazyChunksWebpackPlugin()); } + // Read the tsconfig to determine if we should prefer ES2015 modules. + const tsconfigPath = path.resolve(projectRoot, appConfig.root, appConfig.tsconfig); + const tsConfig = readTsconfig(tsconfigPath); + const supportES2015 = tsConfig.options.target !== ts.ScriptTarget.ES3 + && tsConfig.options.target !== ts.ScriptTarget.ES5; + return { resolve: { extensions: ['.ts', '.js'], modules: ['node_modules', nodeModules], + mainFields: [ + ...(supportES2015 ? ['es2015'] : []), + 'browser', 'module', 'main' + ], symlinks: !buildOptions.preserveSymlinks }, resolveLoader: { diff --git a/packages/@angular/cli/models/webpack-configs/production.ts b/packages/@angular/cli/models/webpack-configs/production.ts index dc81100d2085..23ccfc360e24 100644 --- a/packages/@angular/cli/models/webpack-configs/production.ts +++ b/packages/@angular/cli/models/webpack-configs/production.ts @@ -2,19 +2,23 @@ import * as path from 'path'; import * as webpack from 'webpack'; import * as fs from 'fs'; import * as semver from 'semver'; +import * as ts from 'typescript'; import { stripIndent } from 'common-tags'; import { LicenseWebpackPlugin } from 'license-webpack-plugin'; 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'; +import { readTsconfig } from '../../utilities/read-tsconfig'; + +const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); export const getProdConfig = function (wco: WebpackConfigOptions) { const { projectRoot, buildOptions, appConfig } = wco; let extraPlugins: any[] = []; - let entryPoints: {[key: string]: string[]} = {}; + let entryPoints: { [key: string]: string[] } = {}; if (appConfig.serviceWorker) { const nodeModules = path.resolve(projectRoot, 'node_modules'); @@ -66,7 +70,7 @@ export const getProdConfig = function (wco: WebpackConfigOptions) { extraPlugins.push(new GlobCopyWebpackPlugin({ patterns: [ 'ngsw-manifest.json', - {glob: 'ngsw-manifest.json', input: path.resolve(projectRoot, appConfig.root), output: ''} + { glob: 'ngsw-manifest.json', input: path.resolve(projectRoot, appConfig.root), output: '' } ], globOptions: { cwd: projectRoot, @@ -99,7 +103,7 @@ export const getProdConfig = function (wco: WebpackConfigOptions) { })); } - const uglifyCompressOptions: any = { screw_ie8: true, warnings: buildOptions.verbose }; + const uglifyCompressOptions: any = {}; if (buildOptions.buildOptimizer) { // This plugin must be before webpack.optimize.UglifyJsPlugin. @@ -110,21 +114,49 @@ export const getProdConfig = function (wco: WebpackConfigOptions) { uglifyCompressOptions.passes = 3; } + // Read the tsconfig to determine if we should apply ES6 uglify. + const tsconfigPath = path.resolve(projectRoot, appConfig.root, appConfig.tsconfig); + const tsConfig = readTsconfig(tsconfigPath); + const supportES2015 = tsConfig.options.target !== ts.ScriptTarget.ES3 + && tsConfig.options.target !== ts.ScriptTarget.ES5; + + if (supportES2015) { + extraPlugins.push(new UglifyJSPlugin({ + sourceMap: buildOptions.sourcemaps, + uglifyOptions: { + ecma: 6, + warnings: buildOptions.verbose, + ie8: false, + mangle: true, + compress: uglifyCompressOptions, + output: { + ascii_only: true, + comments: false + }, + } + })); + } else { + uglifyCompressOptions.screw_ie8 = true; + uglifyCompressOptions.warnings = buildOptions.verbose; + extraPlugins.push(new webpack.optimize.UglifyJsPlugin({ + mangle: { screw_ie8: true }, + compress: uglifyCompressOptions, + output: { ascii_only: true }, + sourceMap: buildOptions.sourcemaps, + comments: false + })); + + } + return { entry: entryPoints, - plugins: extraPlugins.concat([ + plugins: [ new webpack.EnvironmentPlugin({ 'NODE_ENV': 'production' }), new webpack.HashedModuleIdsPlugin(), new webpack.optimize.ModuleConcatenationPlugin(), - new webpack.optimize.UglifyJsPlugin({ - mangle: { screw_ie8: true }, - compress: uglifyCompressOptions, - output: { ascii_only: true }, - sourceMap: buildOptions.sourcemaps, - comments: false - }) - ]) + ...extraPlugins + ] }; }; diff --git a/packages/@angular/cli/package.json b/packages/@angular/cli/package.json index 18a0b5c26dff..ea35a0bc451b 100644 --- a/packages/@angular/cli/package.json +++ b/packages/@angular/cli/package.json @@ -76,6 +76,7 @@ "stylus": "^0.54.5", "stylus-loader": "^3.0.1", "typescript": ">=2.0.0 <2.6.0", + "uglifyjs-webpack-plugin": "1.0.0-beta.1", "url-loader": "^0.5.7", "webpack": "~3.5.5", "webpack-concat-plugin": "1.4.0", diff --git a/packages/@angular/cli/tasks/eject.ts b/packages/@angular/cli/tasks/eject.ts index 242404503843..a910d961a139 100644 --- a/packages/@angular/cli/tasks/eject.ts +++ b/packages/@angular/cli/tasks/eject.ts @@ -24,6 +24,7 @@ const HtmlWebpackPlugin = require('html-webpack-plugin'); const SilentError = require('silent-error'); const CircularDependencyPlugin = require('circular-dependency-plugin'); const ConcatPlugin = require('webpack-concat-plugin'); +const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); const Task = require('../ember-cli/lib/models/task'); const ProgressPlugin = require('webpack/lib/ProgressPlugin'); @@ -158,6 +159,10 @@ class JsonWebpackSerializer { return plugin.settings; } + private _uglifyjsPlugin(plugin: any) { + return plugin.options; + } + private _pluginsReplacer(plugins: any[]) { return plugins.map(plugin => { let args = plugin.options || undefined; @@ -229,6 +234,10 @@ class JsonWebpackSerializer { args = this._concatPlugin(plugin); this.variableImports['webpack-concat-plugin'] = 'ConcatPlugin'; break; + case UglifyJSPlugin: + args = this._uglifyjsPlugin(plugin); + this.variableImports['uglifyjs-webpack-plugin'] = 'UglifyJsPlugin'; + break; default: if (plugin.constructor.name == 'AngularServiceWorkerPlugin') { this._addImport('@angular/service-worker/build/webpack', plugin.constructor.name); @@ -546,6 +555,7 @@ export default Task.extend({ 'circular-dependency-plugin', 'webpack-concat-plugin', 'copy-webpack-plugin', + 'uglifyjs-webpack-plugin', ].forEach((packageName: string) => { packageJson['devDependencies'][packageName] = ourPackageJson['dependencies'][packageName]; }); diff --git a/packages/@angular/cli/utilities/read-tsconfig.ts b/packages/@angular/cli/utilities/read-tsconfig.ts new file mode 100644 index 000000000000..ec6db2883576 --- /dev/null +++ b/packages/@angular/cli/utilities/read-tsconfig.ts @@ -0,0 +1,10 @@ +import * as path from 'path'; +import * as ts from 'typescript'; + +export function readTsconfig(tsconfigPath: string) { + const configResult = ts.readConfigFile(tsconfigPath, ts.sys.readFile); + const tsConfig = ts.parseJsonConfigFileContent(configResult.config, ts.sys, + path.dirname(tsconfigPath), undefined, tsconfigPath); + return tsConfig; +} + diff --git a/tests/e2e/tests/build/script-target.ts b/tests/e2e/tests/build/script-target.ts new file mode 100644 index 000000000000..9f354ae5e421 --- /dev/null +++ b/tests/e2e/tests/build/script-target.ts @@ -0,0 +1,23 @@ +import { appendToFile, expectFileToMatch } from '../../utils/fs'; +import { ng } from '../../utils/process'; +import { expectToFail } from '../../utils/utils'; +import { updateJsonFile } from '../../utils/project'; + + +export default function () { + return Promise.resolve() + // Force import a known ES6 module and build with prod. + // ES6 modules will cause UglifyJS to fail on a ES5 compilation target (default). + .then(() => appendToFile('src/main.ts', ` + import * as es6module from '@angular/core/@angular/core'; + console.log(es6module); + `)) + .then(() => expectToFail(() => ng('build', '--prod'))) + .then(() => updateJsonFile('tsconfig.json', configJson => { + const compilerOptions = configJson['compilerOptions']; + compilerOptions['target'] = 'es2015'; + })) + .then(() => ng('build', '--prod', '--output-hashing=none')) + // Check class constructors are present in the vendor output. + .then(() => expectFileToMatch('dist/vendor.bundle.js', /class \w{constructor\(\){/)); +}