From 47820b8996c0881fc56d817975986f47a8ce687f Mon Sep 17 00:00:00 2001 From: Charles Lyding Date: Fri, 6 Jan 2017 14:00:47 -0500 Subject: [PATCH 1/4] feat: allow output hashing to be configured --- packages/angular-cli/commands/build.run.ts | 9 +++++++ packages/angular-cli/commands/build.ts | 9 ++++++- .../models/webpack-build-common.ts | 27 ++++++++++++++----- .../models/webpack-build-development.ts | 13 +-------- .../models/webpack-build-production.ts | 7 ----- .../angular-cli/models/webpack-build-utils.ts | 15 +++++++++++ packages/angular-cli/models/webpack-config.ts | 6 +++-- .../angular-cli/tasks/build-webpack-watch.ts | 3 ++- packages/angular-cli/tasks/build-webpack.ts | 3 ++- 9 files changed, 62 insertions(+), 30 deletions(-) diff --git a/packages/angular-cli/commands/build.run.ts b/packages/angular-cli/commands/build.run.ts index f3bbf0214321..a0d29f432e23 100644 --- a/packages/angular-cli/commands/build.run.ts +++ b/packages/angular-cli/commands/build.run.ts @@ -13,6 +13,15 @@ export default function buildRun(commandOptions: BuildOptions) { } } + if (!commandOptions.outputHashing) { + if (commandOptions.target === 'development') { + commandOptions.outputHashing = 'none'; + } + if (commandOptions.target === 'production') { + commandOptions.outputHashing = 'all'; + } + } + const project = this.project; // Check angular version. diff --git a/packages/angular-cli/commands/build.ts b/packages/angular-cli/commands/build.ts index 00f1d82f80c4..bee7430c6fb6 100644 --- a/packages/angular-cli/commands/build.ts +++ b/packages/angular-cli/commands/build.ts @@ -17,6 +17,7 @@ export interface BuildOptions { i18nFormat?: string; locale?: string; deployUrl?: string; + outputHashing?: string; } const BuildCommand = Command.extend({ @@ -45,7 +46,13 @@ const BuildCommand = Command.extend({ { name: 'i18n-file', type: String, default: null }, { name: 'i18n-format', type: String, default: null }, { name: 'locale', type: String, default: null }, - { name: 'deploy-url', type: String, default: null, aliases: ['d'] } + { name: 'deploy-url', type: String, default: null, aliases: ['d'] }, + { + name: 'output-hashing', + type: String, + values: ['none', 'all', 'media', 'bundles'], + description: 'Sets the hashing mode for output filenames' + } ], run: function (commandOptions: BuildOptions) { diff --git a/packages/angular-cli/models/webpack-build-common.ts b/packages/angular-cli/models/webpack-build-common.ts index 6130f887fbc0..b3892d33c821 100644 --- a/packages/angular-cli/models/webpack-build-common.ts +++ b/packages/angular-cli/models/webpack-build-common.ts @@ -4,11 +4,12 @@ import { GlobCopyWebpackPlugin } from '../plugins/glob-copy-webpack-plugin'; import { SuppressEntryChunksWebpackPlugin } from '../plugins/suppress-entry-chunks-webpack-plugin'; import { packageChunkSort } from '../utilities/package-chunk-sort'; import { BaseHrefWebpackPlugin } from '@angular-cli/base-href-webpack'; -import { extraEntryParser, makeCssLoaders } from './webpack-build-utils'; +import { extraEntryParser, makeCssLoaders, getOutputHashFormat } from './webpack-build-utils'; const autoprefixer = require('autoprefixer'); const ProgressPlugin = require('webpack/lib/ProgressPlugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); +const ExtractTextPlugin = require('extract-text-webpack-plugin'); const SilentError = require('silent-error'); /** @@ -31,7 +32,8 @@ export function getWebpackCommonConfig( sourcemap: boolean, vendorChunk: boolean, verbose: boolean, - progress: boolean + progress: boolean, + outputHashing: string, ) { const appRoot = path.resolve(projectRoot, appConfig.root); @@ -46,6 +48,9 @@ export function getWebpackCommonConfig( main: [appMain] }; + // determine hashing format + const hashFormat = getOutputHashFormat(outputHashing); + // process global scripts if (appConfig.scripts && appConfig.scripts.length > 0) { const globalScripts = extraEntryParser(appConfig.scripts, appRoot, 'scripts'); @@ -143,21 +148,31 @@ export function getWebpackCommonConfig( entry: entryPoints, output: { path: path.resolve(projectRoot, appConfig.outDir), - publicPath: appConfig.deployUrl + publicPath: appConfig.deployUrl, + filename: `[name]${hashFormat.chunk}.bundle.js`, + sourceMapFilename: `[name]${hashFormat.chunk}.bundle.map`, + chunkFilename: `[id]${hashFormat.chunk}.chunk.js` }, module: { rules: [ { enforce: 'pre', test: /\.js$/, loader: 'source-map-loader', exclude: [nodeModules] }, { test: /\.json$/, loader: 'json-loader' }, - { test: /\.(jpg|png|gif)$/, loader: 'url-loader?limit=10000' }, + { + test: /\.(jpg|png|gif)$/, + loader: `url-loader?name=${hashFormat.file}.[ext]&limit=10000` + }, { test: /\.html$/, loader: 'raw-loader' }, - { test: /\.(otf|ttf|woff|woff2)$/, loader: 'url-loader?limit=10000' }, - { test: /\.(eot|svg)$/, loader: 'file-loader' } + { + test: /\.(otf|ttf|woff|woff2)$/, + loader: `url-loader?name=${hashFormat.file}.[ext]&limit=10000` + }, + { test: /\.(eot|svg)$/, loader: `file-loader?name=${hashFormat.file}.[ext]` } ].concat(extraRules) }, plugins: [ + new ExtractTextPlugin(`[name]${hashFormat.extract}.bundle.css`), new HtmlWebpackPlugin({ template: path.resolve(appRoot, appConfig.index), filename: path.resolve(appConfig.outDir, appConfig.index), diff --git a/packages/angular-cli/models/webpack-build-development.ts b/packages/angular-cli/models/webpack-build-development.ts index 21ed45f0e9ca..19b6f1281e30 100644 --- a/packages/angular-cli/models/webpack-build-development.ts +++ b/packages/angular-cli/models/webpack-build-development.ts @@ -1,14 +1,3 @@ -const ExtractTextPlugin = require('extract-text-webpack-plugin'); - export const getWebpackDevConfigPartial = function(projectRoot: string, appConfig: any) { - return { - output: { - filename: '[name].bundle.js', - sourceMapFilename: '[name].bundle.map', - chunkFilename: '[id].chunk.js' - }, - plugins: [ - new ExtractTextPlugin({filename: '[name].bundle.css'}) - ] - }; + return { }; }; diff --git a/packages/angular-cli/models/webpack-build-production.ts b/packages/angular-cli/models/webpack-build-production.ts index 1ec187c372b8..ffb198cf9d41 100644 --- a/packages/angular-cli/models/webpack-build-production.ts +++ b/packages/angular-cli/models/webpack-build-production.ts @@ -1,6 +1,5 @@ import * as path from 'path'; import * as webpack from 'webpack'; -const ExtractTextPlugin = require('extract-text-webpack-plugin'); import {CompressionPlugin} from '../lib/webpack/compression-plugin'; const autoprefixer = require('autoprefixer'); const postcssDiscardComments = require('postcss-discard-comments'); @@ -22,13 +21,7 @@ export const getWebpackProdConfigPartial = function(projectRoot: string, const appRoot = path.resolve(projectRoot, appConfig.root); return { - output: { - filename: '[name].[chunkhash].bundle.js', - sourceMapFilename: '[name].[chunkhash].bundle.map', - chunkFilename: '[id].[chunkhash].chunk.js' - }, plugins: [ - new ExtractTextPlugin('[name].[chunkhash].bundle.css'), new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('production') }), diff --git a/packages/angular-cli/models/webpack-build-utils.ts b/packages/angular-cli/models/webpack-build-utils.ts index ca12eacbdba3..82729d62ca68 100644 --- a/packages/angular-cli/models/webpack-build-utils.ts +++ b/packages/angular-cli/models/webpack-build-utils.ts @@ -109,3 +109,18 @@ export function extraEntryParser( return extraEntry; }); } + +const hashFormats: { [option: string]: any } = { + 'none': { chunk: '', extract: '', file: '[name]' }, + 'media': { chunk: '', extract: '', file: '[hash:20]' }, + 'bundles': { chunk: '.[chunkhash]', extract: '.[contenthash:20]', file: '[name]' }, + 'all': { chunk: '.[chunkhash]', extract: '.[contenthash:20]', file: '[hash:20]' }, +}; + +export function getOutputHashFormat(hashOption: string): any { + let format = hashFormats['none']; + if (hashOption in hashFormats) { + format = hashFormats[hashOption]; + } + return format; +} diff --git a/packages/angular-cli/models/webpack-config.ts b/packages/angular-cli/models/webpack-config.ts index 61878c3d0bcf..cc9e942b212a 100644 --- a/packages/angular-cli/models/webpack-config.ts +++ b/packages/angular-cli/models/webpack-config.ts @@ -32,7 +32,8 @@ export class NgCliWebpackConfig { vendorChunk = false, verbose = false, progress = true, - deployUrl?: string + deployUrl?: string, + outputHashing?: string ) { const config: CliConfig = CliConfig.fromProject(); const appConfig = config.config.apps[0]; @@ -48,7 +49,8 @@ export class NgCliWebpackConfig { sourcemap, vendorChunk, verbose, - progress + progress, + outputHashing ); let targetConfigPartial = this.getTargetConfig( this.ngCliProject.root, appConfig, sourcemap, verbose diff --git a/packages/angular-cli/tasks/build-webpack-watch.ts b/packages/angular-cli/tasks/build-webpack-watch.ts index 1681ae3add6a..4e07802bce61 100644 --- a/packages/angular-cli/tasks/build-webpack-watch.ts +++ b/packages/angular-cli/tasks/build-webpack-watch.ts @@ -33,7 +33,8 @@ export default Task.extend({ runTaskOptions.vendorChunk, runTaskOptions.verbose, runTaskOptions.progress, - deployUrl + deployUrl, + runTaskOptions.outputHashing ).config; const webpackCompiler: any = webpack(config); diff --git a/packages/angular-cli/tasks/build-webpack.ts b/packages/angular-cli/tasks/build-webpack.ts index 0fce473ef348..78b770099c6b 100644 --- a/packages/angular-cli/tasks/build-webpack.ts +++ b/packages/angular-cli/tasks/build-webpack.ts @@ -34,7 +34,8 @@ export default Task.extend({ runTaskOptions.vendorChunk, runTaskOptions.verbose, runTaskOptions.progress, - deployUrl + deployUrl, + runTaskOptions.outputHashing ).config; const webpackCompiler: any = webpack(config); From e8bb9613bc6e7213522265b637b3a2f65f1f72c6 Mon Sep 17 00:00:00 2001 From: Charles Lyding Date: Sat, 7 Jan 2017 12:58:27 -0500 Subject: [PATCH 2/4] cleanup and address comments --- .../models/webpack-build-common.ts | 6 ++--- .../angular-cli/models/webpack-build-utils.ts | 27 ++++++++++--------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/packages/angular-cli/models/webpack-build-common.ts b/packages/angular-cli/models/webpack-build-common.ts index b3892d33c821..b7493d287c89 100644 --- a/packages/angular-cli/models/webpack-build-common.ts +++ b/packages/angular-cli/models/webpack-build-common.ts @@ -160,15 +160,15 @@ export function getWebpackCommonConfig( { test: /\.json$/, loader: 'json-loader' }, { test: /\.(jpg|png|gif)$/, - loader: `url-loader?name=${hashFormat.file}.[ext]&limit=10000` + loader: `url-loader?name=[name]${hashFormat.file}.[ext]&limit=10000` }, { test: /\.html$/, loader: 'raw-loader' }, { test: /\.(otf|ttf|woff|woff2)$/, - loader: `url-loader?name=${hashFormat.file}.[ext]&limit=10000` + loader: `url-loader?name=[name]${hashFormat.file}.[ext]&limit=10000` }, - { test: /\.(eot|svg)$/, loader: `file-loader?name=${hashFormat.file}.[ext]` } + { test: /\.(eot|svg)$/, loader: `file-loader?name=[name]${hashFormat.file}.[ext]` } ].concat(extraRules) }, plugins: [ diff --git a/packages/angular-cli/models/webpack-build-utils.ts b/packages/angular-cli/models/webpack-build-utils.ts index 82729d62ca68..8ecd7787c17f 100644 --- a/packages/angular-cli/models/webpack-build-utils.ts +++ b/packages/angular-cli/models/webpack-build-utils.ts @@ -110,17 +110,20 @@ export function extraEntryParser( }); } -const hashFormats: { [option: string]: any } = { - 'none': { chunk: '', extract: '', file: '[name]' }, - 'media': { chunk: '', extract: '', file: '[hash:20]' }, - 'bundles': { chunk: '.[chunkhash]', extract: '.[contenthash:20]', file: '[name]' }, - 'all': { chunk: '.[chunkhash]', extract: '.[contenthash:20]', file: '[hash:20]' }, -}; +export interface HashFormat { + chunk: string; + extract: string; + file: string; +} -export function getOutputHashFormat(hashOption: string): any { - let format = hashFormats['none']; - if (hashOption in hashFormats) { - format = hashFormats[hashOption]; - } - return format; +export function getOutputHashFormat(option: string, length = 20): HashFormat { + /* tslint:disable:max-line-length */ + const hashFormats: { [option: string]: HashFormat } = { + none: { chunk: '', extract: '', file: '' }, + media: { chunk: '', extract: '', file: `.[hash:${length}]` }, + bundles: { chunk: `.[chunkhash:${length}]`, extract: `.[contenthash:${length}]`, file: '' }, + all: { chunk: `.[chunkhash:${length}]`, extract: `.[contenthash:${length}]`, file: `.[hash:${length}]` }, + }; + /* tslint:enable:max-line-length */ + return hashFormats[option] || hashFormats['none']; } From 06908080017e98650d0c53b31d6ab6d8ac07fd2c Mon Sep 17 00:00:00 2001 From: Charles Lyding Date: Tue, 10 Jan 2017 13:49:38 -0500 Subject: [PATCH 3/4] add tests --- tests/e2e/tests/build/output-hashing.ts | 48 +++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tests/e2e/tests/build/output-hashing.ts diff --git a/tests/e2e/tests/build/output-hashing.ts b/tests/e2e/tests/build/output-hashing.ts new file mode 100644 index 000000000000..a6c539292839 --- /dev/null +++ b/tests/e2e/tests/build/output-hashing.ts @@ -0,0 +1,48 @@ +import {stripIndents} from 'common-tags'; +import * as fs from 'fs'; +import {ng} from '../../utils/process'; +import { writeMultipleFiles, expectFileToMatch } from '../../utils/fs'; + +function verifyMedia(css: RegExp, content: RegExp) { + return new Promise((resolve, reject) => { + const [fileName] = fs.readdirSync('./dist').filter(name => name.match(css)); + if (!fileName) { + reject(new Error(`File ${fileName} was expected to exist but not found...`)); + } + resolve(fileName); + }) + .then(fileName => expectFileToMatch(`dist/${fileName}`, content)); +} + +export default function() { + return Promise.resolve() + .then(() => writeMultipleFiles({ + 'src/styles.css': stripIndents` + body { background-image: url("image.svg"); } + `, + 'src/image.svg': 'I would like to be an image someday.' + })) + .then(() => ng('build', '--dev', '--output-hashing=all')) + .then(() => expectFileToMatch('dist/index.html', /inline\.[0-9a-f]{20}\.bundle\.js/)) + .then(() => expectFileToMatch('dist/index.html', /main\.[0-9a-f]{20}\.bundle\.js/)) + .then(() => expectFileToMatch('dist/index.html', /styles\.[0-9a-f]{20}\.bundle\.(css|js)/)) + .then(() => verifyMedia(/styles\.[0-9a-f]{20}\.bundle\.(css|js)/, /image\.[0-9a-f]{20}\.svg/)) + + .then(() => ng('build', '--prod', '--output-hashing=none')) + .then(() => expectFileToMatch('dist/index.html', /inline\.bundle\.js/)) + .then(() => expectFileToMatch('dist/index.html', /main\.bundle\.js/)) + .then(() => expectFileToMatch('dist/index.html', /styles\.bundle\.(css|js)/)) + .then(() => verifyMedia(/styles\.bundle\.(css|js)/, /image\.svg/)) + + .then(() => ng('build', '--dev', '--output-hashing=media')) + .then(() => expectFileToMatch('dist/index.html', /inline\.bundle\.js/)) + .then(() => expectFileToMatch('dist/index.html', /main\.bundle\.js/)) + .then(() => expectFileToMatch('dist/index.html', /styles\.bundle\.(css|js)/)) + .then(() => verifyMedia(/styles\.bundle\.(css|js)/, /image\.[0-9a-f]{20}\.svg/)) + + .then(() => ng('build', '--dev', '--output-hashing=bundles')) + .then(() => expectFileToMatch('dist/index.html', /inline\.[0-9a-f]{20}\.bundle\.js/)) + .then(() => expectFileToMatch('dist/index.html', /main\.[0-9a-f]{20}\.bundle\.js/)) + .then(() => expectFileToMatch('dist/index.html', /styles\.[0-9a-f]{20}\.bundle\.(css|js)/)) + .then(() => verifyMedia(/styles\.[0-9a-f]{20}\.bundle\.(css|js)/, /image\.svg/)); +} From 90d3e7e3f8ea71445581c58d56bcc0217458da57 Mon Sep 17 00:00:00 2001 From: Charles Lyding Date: Tue, 10 Jan 2017 15:42:52 -0500 Subject: [PATCH 4/4] add docs --- docs/documentation/build.md | 2 ++ packages/angular-cli/commands/build.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/documentation/build.md b/docs/documentation/build.md index 2ea2c32850e5..3723e145fc82 100644 --- a/docs/documentation/build.md +++ b/docs/documentation/build.md @@ -10,6 +10,8 @@ `--output-path` (`-o`) path where output will be placed +`--output-hashing` define the output filename cache-busting hashing mode + `--watch` (`-w`) flag to run builds when files change `--surpress-sizes` flag to suppress sizes from build output diff --git a/packages/angular-cli/commands/build.ts b/packages/angular-cli/commands/build.ts index bee7430c6fb6..245ac7d849dc 100644 --- a/packages/angular-cli/commands/build.ts +++ b/packages/angular-cli/commands/build.ts @@ -51,7 +51,7 @@ const BuildCommand = Command.extend({ name: 'output-hashing', type: String, values: ['none', 'all', 'media', 'bundles'], - description: 'Sets the hashing mode for output filenames' + description: 'define the output filename cache-busting hashing mode' } ],