From 8cea8ffdc28908b325d8a56f06ff3d626efdeb8d Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Sun, 10 Dec 2017 21:25:36 -0500 Subject: [PATCH] refactor(@angular/cli): add webpack integrated scripts plugin --- package-lock.json | 99 ++++--------- package.json | 3 +- .../cli/models/webpack-configs/common.ts | 24 +-- .../cli/models/webpack-configs/utils.ts | 4 +- packages/@angular/cli/package.json | 2 +- .../insert-concat-assets-webpack-plugin.ts | 52 ------- .../cli/plugins/scripts-webpack-plugin.ts | 140 ++++++++++++++++++ packages/@angular/cli/plugins/webpack.ts | 2 +- packages/@angular/cli/tasks/eject.ts | 19 +-- .../cli/utilities/package-chunk-sort.ts | 4 + 10 files changed, 186 insertions(+), 163 deletions(-) delete mode 100644 packages/@angular/cli/plugins/insert-concat-assets-webpack-plugin.ts create mode 100644 packages/@angular/cli/plugins/scripts-webpack-plugin.ts diff --git a/package-lock.json b/package-lock.json index 3517925304..184bce270c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -174,6 +174,16 @@ "integrity": "sha1-WJKKYh0BTOarWcWpxBBx9zKLDKk=", "dev": true }, + "@types/loader-utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@types/loader-utils/-/loader-utils-1.1.0.tgz", + "integrity": "sha512-VR4oHG6TzhpemxtBDf0BD8xlOiPo2B6zcFEA2Jjmgf1RqSrHLAiteIksV3YvpVn0Pd4HxV1B3LQ6Mf2pGTyZ7g==", + "dev": true, + "requires": { + "@types/node": "6.0.88", + "@types/webpack": "3.0.11" + } + }, "@types/lodash": { "version": "4.14.74", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.74.tgz", @@ -239,6 +249,12 @@ "@types/mime": "2.0.0" } }, + "@types/source-list-map": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", + "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", + "dev": true + }, "@types/source-map": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/@types/source-map/-/source-map-0.5.1.tgz", @@ -271,6 +287,17 @@ "@types/uglify-js": "2.6.29" } }, + "@types/webpack-sources": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-0.1.3.tgz", + "integrity": "sha512-yS052yVjjyIjwcUqIEe2+JxbWsw27OM8UFb1fLUGacGYtqMRwgAx2qk41VTE/nPMjw/xfD0JiHPD0Q99dlrInA==", + "dev": true, + "requires": { + "@types/node": "6.0.88", + "@types/source-list-map": "0.1.2", + "@types/source-map": "0.5.1" + } + }, "JSONStream": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.1.tgz", @@ -1030,11 +1057,6 @@ } } }, - "charenc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" - }, "chokidar": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", @@ -1753,11 +1775,6 @@ "which": "1.3.0" } }, - "crypt": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" - }, "cryptiles": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", @@ -5333,16 +5350,6 @@ "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz", "integrity": "sha1-3oGf282E3M2PrlnGrreWFbnSZqw=" }, - "md5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz", - "integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=", - "requires": { - "charenc": "0.0.2", - "crypt": "0.0.2", - "is-buffer": "1.1.5" - } - }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -9254,58 +9261,6 @@ } } }, - "webpack-concat-plugin": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/webpack-concat-plugin/-/webpack-concat-plugin-1.4.2.tgz", - "integrity": "sha512-HdV2xOq4twtL2ThR9NSCCQ888v1JBMpJfm3k2mA1I5LkS2+/6rv8q/bb9yTSaR0fVaMtANZi4Wkz0xc33MAt6w==", - "requires": { - "md5": "2.2.1", - "uglify-js": "2.8.29" - }, - "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.7", - "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=", - "requires": { - "camelcase": "1.2.1", - "cliui": "2.1.0", - "decamelize": "1.2.0", - "window-size": "0.1.0" - } - } - } - }, "webpack-core": { "version": "0.6.9", "resolved": "https://registry.npmjs.org/webpack-core/-/webpack-core-0.6.9.tgz", diff --git a/package.json b/package.json index bf71255ab1..4590dc2c60 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,6 @@ "uglifyjs-webpack-plugin": "~1.1.2", "url-loader": "^0.6.2", "webpack": "~3.10.0", - "webpack-concat-plugin": "^1.4.2", "webpack-dev-middleware": "~1.12.0", "webpack-dev-server": "~2.9.3", "webpack-merge": "^4.1.0", @@ -116,6 +115,7 @@ "@types/glob": "^5.0.29", "@types/jasmine": "2.5.45", "@types/lodash": "~4.14.50", + "@types/loader-utils": "^1.1.0", "@types/minimist": "^1.2.0", "@types/mock-fs": "^3.6.30", "@types/node": "^6.0.84", @@ -123,6 +123,7 @@ "@types/semver": "^5.3.30", "@types/source-map": "^0.5.0", "@types/webpack": "^3.0.5", + "@types/webpack-sources": "^0.1.3", "conventional-changelog": "1.1.0", "dtsgenerator": "^0.9.1", "eslint": "^3.11.0", diff --git a/packages/@angular/cli/models/webpack-configs/common.ts b/packages/@angular/cli/models/webpack-configs/common.ts index 3b8751292a..263d428e61 100644 --- a/packages/@angular/cli/models/webpack-configs/common.ts +++ b/packages/@angular/cli/models/webpack-configs/common.ts @@ -2,13 +2,12 @@ import * as webpack from 'webpack'; import * as path from 'path'; import * as CopyWebpackPlugin from 'copy-webpack-plugin'; 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 { requireProjectModule } from '../../utilities/require-project-module'; import { WebpackConfigOptions } from '../webpack-config'; +import { ScriptsWebpackPlugin } from '../../plugins/scripts-webpack-plugin'; -const ConcatPlugin = require('webpack-concat-plugin'); const ProgressPlugin = require('webpack/lib/ProgressPlugin'); const CircularDependencyPlugin = require('circular-dependency-plugin'); const SilentError = require('silent-error'); @@ -65,23 +64,16 @@ export function getCommonConfig(wco: WebpackConfigOptions) { // Add a new asset for each entry. globalScriptsByEntry.forEach((script) => { - const hash = hashFormat.chunk !== '' && !script.lazy ? '.[hash]' : ''; - extraPlugins.push(new ConcatPlugin({ - uglify: buildOptions.target === 'production' ? { sourceMapIncludeSources: true } : false, - sourceMap: buildOptions.sourcemaps, + // Lazy scripts don't get a hash, otherwise they can't be loaded by name. + const hash = script.lazy ? '' : hashFormat.script; + extraPlugins.push(new ScriptsWebpackPlugin({ name: script.entry, - // Lazy scripts don't get a hash, otherwise they can't be loaded by name. - fileName: `[name]${script.lazy ? '' : hash}.bundle.js`, - filesToConcat: script.paths + sourceMap: buildOptions.sourcemaps, + filename: `${script.entry}${hash}.bundle.js`, + scripts: script.paths, + basePath: projectRoot, })); }); - - // Insert all the assets created by ConcatPlugin in the right place in index.html. - extraPlugins.push(new InsertConcatAssetsWebpackPlugin( - globalScriptsByEntry - .filter((el) => !el.lazy) - .map((el) => el.entry) - )); } // process asset entries diff --git a/packages/@angular/cli/models/webpack-configs/utils.ts b/packages/@angular/cli/models/webpack-configs/utils.ts index 8679dc5a81..99a6b7b072 100644 --- a/packages/@angular/cli/models/webpack-configs/utils.ts +++ b/packages/@angular/cli/models/webpack-configs/utils.ts @@ -81,8 +81,8 @@ export function getOutputHashFormat(option: string, length = 20): HashFormat { const hashFormats: { [option: string]: HashFormat } = { none: { chunk: '', extract: '', file: '' , script: '' }, media: { chunk: '', extract: '', file: `.[hash:${length}]`, script: '' }, - bundles: { chunk: `.[chunkhash:${length}]`, extract: `.[contenthash:${length}]`, file: '' , script: '.[hash]' }, - all: { chunk: `.[chunkhash:${length}]`, extract: `.[contenthash:${length}]`, file: `.[hash:${length}]`, script: '.[hash]' }, + bundles: { chunk: `.[chunkhash:${length}]`, extract: `.[contenthash:${length}]`, file: '' , script: `.[hash:${length}]` }, + all: { chunk: `.[chunkhash:${length}]`, extract: `.[contenthash:${length}]`, file: `.[hash:${length}]`, script: `.[hash:${length}]` }, }; /* tslint:enable:max-line-length */ return hashFormats[option] || hashFormats['none']; diff --git a/packages/@angular/cli/package.json b/packages/@angular/cli/package.json index 4cdb5b7b40..468daef52f 100644 --- a/packages/@angular/cli/package.json +++ b/packages/@angular/cli/package.json @@ -53,6 +53,7 @@ "less-loader": "^4.0.5", "license-webpack-plugin": "^1.0.0", "lodash": "^4.11.1", + "loader-utils": "1.1.0", "memory-fs": "^0.4.1", "minimatch": "^3.0.4", "node-modules-path": "^1.0.0", @@ -77,7 +78,6 @@ "uglifyjs-webpack-plugin": "~1.1.2", "url-loader": "^0.6.2", "webpack": "~3.10.0", - "webpack-concat-plugin": "^1.4.2", "webpack-dev-middleware": "~1.12.0", "webpack-dev-server": "~2.9.3", "webpack-merge": "^4.1.0", diff --git a/packages/@angular/cli/plugins/insert-concat-assets-webpack-plugin.ts b/packages/@angular/cli/plugins/insert-concat-assets-webpack-plugin.ts deleted file mode 100644 index 039c0afafd..0000000000 --- a/packages/@angular/cli/plugins/insert-concat-assets-webpack-plugin.ts +++ /dev/null @@ -1,52 +0,0 @@ -// Add assets from `ConcatPlugin` to index.html. - - -export class InsertConcatAssetsWebpackPlugin { - // Priority list of where to insert asset. - private insertAfter = [ - /polyfills(\.[0-9a-f]{20})?\.bundle\.js/, - /inline(\.[0-9a-f]{20})?\.bundle\.js/, - ]; - - constructor(private entryNames: string[]) { } - - apply(compiler: any): void { - compiler.plugin('compilation', (compilation: any) => { - compilation.plugin('html-webpack-plugin-before-html-generation', - (htmlPluginData: any, callback: any) => { - - const fileNames = this.entryNames.map((entryName) => { - const fileName = htmlPluginData.assets.webpackConcat - && htmlPluginData.assets.webpackConcat[entryName]; - - if (!fileName) { - // Something went wrong and the asset was not correctly added. - throw new Error(`Cannot find file for ${entryName} script.`); - } - - if (htmlPluginData.assets.publicPath) { - if (htmlPluginData.assets.publicPath.endsWith('/')) { - return htmlPluginData.assets.publicPath + fileName; - } - return htmlPluginData.assets.publicPath + '/' + fileName; - } - return fileName; - }); - - let insertAt = 0; - - // TODO: try to figure out if there are duplicate bundle names when adding and throw - for (let el of this.insertAfter) { - const jsIdx = htmlPluginData.assets.js.findIndex((js: string) => js.match(el)); - if (jsIdx !== -1) { - insertAt = jsIdx + 1; - break; - } - } - - htmlPluginData.assets.js.splice(insertAt, 0, ...fileNames); - callback(null, htmlPluginData); - }); - }); - } -} diff --git a/packages/@angular/cli/plugins/scripts-webpack-plugin.ts b/packages/@angular/cli/plugins/scripts-webpack-plugin.ts new file mode 100644 index 0000000000..32d8a2785e --- /dev/null +++ b/packages/@angular/cli/plugins/scripts-webpack-plugin.ts @@ -0,0 +1,140 @@ +/** + * @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 { Compiler, loader } from 'webpack'; +import { CachedSource, ConcatSource, OriginalSource, RawSource, Source } from 'webpack-sources'; +import { interpolateName } from 'loader-utils'; +import * as path from 'path'; + +const Chunk = require('webpack/lib/Chunk'); + +export interface ScriptsWebpackPluginOptions { + name: string; + sourceMap: boolean; + scripts: string[]; + filename: string; + basePath: string; +} + +interface ScriptOutput { + filename: string; + source: CachedSource; +} + +export class ScriptsWebpackPlugin { + private _lastBuildTime?: number; + private _cachedOutput?: ScriptOutput; + + constructor(private options: Partial = {}) {} + + shouldSkip(compilation: any, scripts: string[]): boolean { + if (this._lastBuildTime == undefined) { + this._lastBuildTime = Date.now(); + return false; + } + + for (let i = 0; i < scripts.length; i++) { + const scriptTime = compilation.fileTimestamps[scripts[i]]; + if (!scriptTime || scriptTime > this._lastBuildTime) { + this._lastBuildTime = Date.now(); + return false; + } + } + + return true; + } + + private _insertOutput(compilation: any, { filename, source }: ScriptOutput, cached = false) { + const chunk = new Chunk(); + chunk.rendered = !cached; + chunk.id = this.options.name; + chunk.ids = [chunk.id]; + chunk.name = this.options.name; + chunk.isInitial = () => true; + chunk.files.push(filename); + + compilation.chunks.push(chunk); + compilation.assets[filename] = source; + } + + apply(compiler: Compiler): void { + if (!this.options.scripts || this.options.scripts.length === 0) { + return; + } + + const scripts = this.options.scripts + .filter(script => !!script) + .map(script => path.resolve(this.options.basePath || '', script)); + + compiler.plugin('this-compilation', (compilation: any) => { + compilation.plugin('additional-assets', (callback: (err?: Error) => void) => { + if (this.shouldSkip(compilation, scripts)) { + if (this._cachedOutput) { + this._insertOutput(compilation, this._cachedOutput, true); + } + compilation.fileDependencies.push(...scripts); + + callback(); + + return; + } + + const sourceGetters = scripts.map(fullPath => { + return new Promise((resolve, reject) => { + compilation.inputFileSystem.readFile(fullPath, (err: Error, data: Buffer) => { + if (err) { + reject(err); + return; + } + + const content = data.toString(); + + let source; + if (this.options.sourceMap) { + // TODO: Look for source map file (for '.min' scripts, etc.) + + let adjustedPath = fullPath; + if (this.options.basePath) { + adjustedPath = path.relative(this.options.basePath, fullPath); + } + source = new OriginalSource(content, adjustedPath); + } else { + source = new RawSource(content); + } + + resolve(source); + }); + }); + }); + + Promise.all(sourceGetters) + .then(sources => { + const concatSource = new ConcatSource(); + sources.forEach(source => { + concatSource.add(source); + concatSource.add('\n'); + }); + + const combinedSource = new CachedSource(concatSource); + const filename = interpolateName( + { resourcePath: 'scripts.js' } as loader.LoaderContext, + this.options.filename, + { content: combinedSource.source() }, + ); + + const output = { filename, source: combinedSource }; + this._insertOutput(compilation, output); + this._cachedOutput = output; + compilation.fileDependencies.push(...scripts); + + callback(); + }) + .catch((err: Error) => callback(err)); + }); + }); + } +} diff --git a/packages/@angular/cli/plugins/webpack.ts b/packages/@angular/cli/plugins/webpack.ts index a51f71eff8..e261571f7c 100644 --- a/packages/@angular/cli/plugins/webpack.ts +++ b/packages/@angular/cli/plugins/webpack.ts @@ -1,6 +1,6 @@ // Exports the webpack plugins we use internally. export { BaseHrefWebpackPlugin } from '../lib/base-href-webpack/base-href-webpack-plugin'; export { GlobCopyWebpackPlugin, GlobCopyWebpackPluginOptions } from './glob-copy-webpack-plugin'; -export { InsertConcatAssetsWebpackPlugin } from './insert-concat-assets-webpack-plugin'; export { NamedLazyChunksWebpackPlugin } from './named-lazy-chunks-webpack-plugin'; +export { ScriptsWebpackPlugin, ScriptsWebpackPluginOptions } from './scripts-webpack-plugin'; export { SuppressExtractedTextChunksWebpackPlugin } from './suppress-entry-chunks-webpack-plugin'; diff --git a/packages/@angular/cli/tasks/eject.ts b/packages/@angular/cli/tasks/eject.ts index c644fa12c3..432e3f7640 100644 --- a/packages/@angular/cli/tasks/eject.ts +++ b/packages/@angular/cli/tasks/eject.ts @@ -26,7 +26,6 @@ const HtmlWebpackPlugin = require('html-webpack-plugin'); const SubresourceIntegrityPlugin = require('webpack-subresource-integrity'); 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'); @@ -159,18 +158,6 @@ class JsonWebpackSerializer { return plugin.options; } - private _concatPlugin(plugin: any) { - const options = plugin.settings; - if (!options || !options.filesToConcat) { - return options; - } - - const filesToConcat = options.filesToConcat - .map((file: string) => path.relative(process.cwd(), file)); - - return { ...options, filesToConcat }; - } - private _uglifyjsPlugin(plugin: any) { return plugin.options; } @@ -204,6 +191,7 @@ class JsonWebpackSerializer { break; case angularCliPlugins.BaseHrefWebpackPlugin: case angularCliPlugins.NamedLazyChunksWebpackPlugin: + case angularCliPlugins.ScriptsWebpackPlugin: case angularCliPlugins.SuppressExtractedTextChunksWebpackPlugin: this._addImport('@angular/cli/plugins/webpack', plugin.constructor.name); break; @@ -249,10 +237,6 @@ class JsonWebpackSerializer { args = this._licenseWebpackPlugin(plugin); this._addImport('license-webpack-plugin', 'LicenseWebpackPlugin'); break; - case ConcatPlugin: - args = this._concatPlugin(plugin); - this.variableImports['webpack-concat-plugin'] = 'ConcatPlugin'; - break; case UglifyJSPlugin: args = this._uglifyjsPlugin(plugin); this.variableImports['uglifyjs-webpack-plugin'] = 'UglifyJsPlugin'; @@ -594,7 +578,6 @@ export default Task.extend({ 'stylus-loader', 'url-loader', 'circular-dependency-plugin', - 'webpack-concat-plugin', 'copy-webpack-plugin', 'uglifyjs-webpack-plugin', ].forEach((packageName: string) => { diff --git a/packages/@angular/cli/utilities/package-chunk-sort.ts b/packages/@angular/cli/utilities/package-chunk-sort.ts index a7841aeb54..67322ff201 100644 --- a/packages/@angular/cli/utilities/package-chunk-sort.ts +++ b/packages/@angular/cli/utilities/package-chunk-sort.ts @@ -15,6 +15,10 @@ export function packageChunkSort(appConfig: any) { extraEntryParser(appConfig.styles, './', 'styles').forEach(pushExtraEntries); } + if (appConfig.scripts) { + extraEntryParser(appConfig.scripts, './', 'scripts').forEach(pushExtraEntries); + } + entryPoints.push(...['vendor', 'main']); function sort(left: any, right: any) {