Skip to content

Commit b82fe41

Browse files
clydinhansl
authored andcommitted
feat(build): allow output hashing to be configured (angular#3885)
1 parent 888beb7 commit b82fe41

File tree

11 files changed

+115
-30
lines changed

11 files changed

+115
-30
lines changed

docs/documentation/build.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ ng build
2626

2727
`--output-path` (`-o`) path where output will be placed
2828

29+
`--output-hashing` define the output filename cache-busting hashing mode
30+
2931
`--watch` (`-w`) flag to run builds when files change
3032

3133
`--surpress-sizes` flag to suppress sizes from build output

packages/angular-cli/commands/build.run.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@ export default function buildRun(commandOptions: BuildOptions) {
1313
}
1414
}
1515

16+
if (!commandOptions.outputHashing) {
17+
if (commandOptions.target === 'development') {
18+
commandOptions.outputHashing = 'none';
19+
}
20+
if (commandOptions.target === 'production') {
21+
commandOptions.outputHashing = 'all';
22+
}
23+
}
24+
1625
const project = this.project;
1726

1827
// Check angular version.

packages/angular-cli/commands/build.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export interface BuildOptions {
1717
i18nFormat?: string;
1818
locale?: string;
1919
deployUrl?: string;
20+
outputHashing?: string;
2021
}
2122

2223
const BuildCommand = Command.extend({
@@ -45,7 +46,13 @@ const BuildCommand = Command.extend({
4546
{ name: 'i18n-file', type: String, default: null },
4647
{ name: 'i18n-format', type: String, default: null },
4748
{ name: 'locale', type: String, default: null },
48-
{ name: 'deploy-url', type: String, default: null, aliases: ['d'] }
49+
{ name: 'deploy-url', type: String, default: null, aliases: ['d'] },
50+
{
51+
name: 'output-hashing',
52+
type: String,
53+
values: ['none', 'all', 'media', 'bundles'],
54+
description: 'define the output filename cache-busting hashing mode'
55+
}
4956
],
5057

5158
run: function (commandOptions: BuildOptions) {

packages/angular-cli/models/webpack-build-common.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import { GlobCopyWebpackPlugin } from '../plugins/glob-copy-webpack-plugin';
44
import { SuppressEntryChunksWebpackPlugin } from '../plugins/suppress-entry-chunks-webpack-plugin';
55
import { packageChunkSort } from '../utilities/package-chunk-sort';
66
import { BaseHrefWebpackPlugin } from '@angular-cli/base-href-webpack';
7-
import { extraEntryParser, makeCssLoaders } from './webpack-build-utils';
7+
import { extraEntryParser, makeCssLoaders, getOutputHashFormat } from './webpack-build-utils';
88

99
const autoprefixer = require('autoprefixer');
1010
const ProgressPlugin = require('webpack/lib/ProgressPlugin');
1111
const HtmlWebpackPlugin = require('html-webpack-plugin');
12+
const ExtractTextPlugin = require('extract-text-webpack-plugin');
1213
const SilentError = require('silent-error');
1314

1415
/**
@@ -31,7 +32,8 @@ export function getWebpackCommonConfig(
3132
sourcemap: boolean,
3233
vendorChunk: boolean,
3334
verbose: boolean,
34-
progress: boolean
35+
progress: boolean,
36+
outputHashing: string,
3537
) {
3638

3739
const appRoot = path.resolve(projectRoot, appConfig.root);
@@ -46,6 +48,9 @@ export function getWebpackCommonConfig(
4648
main: [appMain]
4749
};
4850

51+
// determine hashing format
52+
const hashFormat = getOutputHashFormat(outputHashing);
53+
4954
// process global scripts
5055
if (appConfig.scripts && appConfig.scripts.length > 0) {
5156
const globalScripts = extraEntryParser(appConfig.scripts, appRoot, 'scripts');
@@ -143,21 +148,31 @@ export function getWebpackCommonConfig(
143148
entry: entryPoints,
144149
output: {
145150
path: path.resolve(projectRoot, appConfig.outDir),
146-
publicPath: appConfig.deployUrl
151+
publicPath: appConfig.deployUrl,
152+
filename: `[name]${hashFormat.chunk}.bundle.js`,
153+
sourceMapFilename: `[name]${hashFormat.chunk}.bundle.map`,
154+
chunkFilename: `[id]${hashFormat.chunk}.chunk.js`
147155
},
148156
module: {
149157
rules: [
150158
{ enforce: 'pre', test: /\.js$/, loader: 'source-map-loader', exclude: [nodeModules] },
151159

152160
{ test: /\.json$/, loader: 'json-loader' },
153-
{ test: /\.(jpg|png|gif)$/, loader: 'url-loader?limit=10000' },
161+
{
162+
test: /\.(jpg|png|gif)$/,
163+
loader: `url-loader?name=[name]${hashFormat.file}.[ext]&limit=10000`
164+
},
154165
{ test: /\.html$/, loader: 'raw-loader' },
155166

156-
{ test: /\.(otf|ttf|woff|woff2)$/, loader: 'url-loader?limit=10000' },
157-
{ test: /\.(eot|svg)$/, loader: 'file-loader' }
167+
{
168+
test: /\.(otf|ttf|woff|woff2)$/,
169+
loader: `url-loader?name=[name]${hashFormat.file}.[ext]&limit=10000`
170+
},
171+
{ test: /\.(eot|svg)$/, loader: `file-loader?name=[name]${hashFormat.file}.[ext]` }
158172
].concat(extraRules)
159173
},
160174
plugins: [
175+
new ExtractTextPlugin(`[name]${hashFormat.extract}.bundle.css`),
161176
new HtmlWebpackPlugin({
162177
template: path.resolve(appRoot, appConfig.index),
163178
filename: path.resolve(appConfig.outDir, appConfig.index),
Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,3 @@
1-
const ExtractTextPlugin = require('extract-text-webpack-plugin');
2-
31
export const getWebpackDevConfigPartial = function(projectRoot: string, appConfig: any) {
4-
return {
5-
output: {
6-
filename: '[name].bundle.js',
7-
sourceMapFilename: '[name].bundle.map',
8-
chunkFilename: '[id].chunk.js'
9-
},
10-
plugins: [
11-
new ExtractTextPlugin({filename: '[name].bundle.css'})
12-
]
13-
};
2+
return { };
143
};

packages/angular-cli/models/webpack-build-production.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import * as path from 'path';
22
import * as webpack from 'webpack';
3-
const ExtractTextPlugin = require('extract-text-webpack-plugin');
43
import {CompressionPlugin} from '../lib/webpack/compression-plugin';
54
const autoprefixer = require('autoprefixer');
65
const postcssDiscardComments = require('postcss-discard-comments');
@@ -22,13 +21,7 @@ export const getWebpackProdConfigPartial = function(projectRoot: string,
2221
const appRoot = path.resolve(projectRoot, appConfig.root);
2322

2423
return {
25-
output: {
26-
filename: '[name].[chunkhash].bundle.js',
27-
sourceMapFilename: '[name].[chunkhash].bundle.map',
28-
chunkFilename: '[id].[chunkhash].chunk.js'
29-
},
3024
plugins: [
31-
new ExtractTextPlugin('[name].[chunkhash].bundle.css'),
3225
new webpack.DefinePlugin({
3326
'process.env.NODE_ENV': JSON.stringify('production')
3427
}),

packages/angular-cli/models/webpack-build-utils.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,21 @@ export function extraEntryParser(
109109
return extraEntry;
110110
});
111111
}
112+
113+
export interface HashFormat {
114+
chunk: string;
115+
extract: string;
116+
file: string;
117+
}
118+
119+
export function getOutputHashFormat(option: string, length = 20): HashFormat {
120+
/* tslint:disable:max-line-length */
121+
const hashFormats: { [option: string]: HashFormat } = {
122+
none: { chunk: '', extract: '', file: '' },
123+
media: { chunk: '', extract: '', file: `.[hash:${length}]` },
124+
bundles: { chunk: `.[chunkhash:${length}]`, extract: `.[contenthash:${length}]`, file: '' },
125+
all: { chunk: `.[chunkhash:${length}]`, extract: `.[contenthash:${length}]`, file: `.[hash:${length}]` },
126+
};
127+
/* tslint:enable:max-line-length */
128+
return hashFormats[option] || hashFormats['none'];
129+
}

packages/angular-cli/models/webpack-config.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ export class NgCliWebpackConfig {
3232
vendorChunk = false,
3333
verbose = false,
3434
progress = true,
35-
deployUrl?: string
35+
deployUrl?: string,
36+
outputHashing?: string
3637
) {
3738
const config: CliConfig = CliConfig.fromProject();
3839
const appConfig = config.config.apps[0];
@@ -48,7 +49,8 @@ export class NgCliWebpackConfig {
4849
sourcemap,
4950
vendorChunk,
5051
verbose,
51-
progress
52+
progress,
53+
outputHashing
5254
);
5355
let targetConfigPartial = this.getTargetConfig(
5456
this.ngCliProject.root, appConfig, sourcemap, verbose

packages/angular-cli/tasks/build-webpack-watch.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ export default Task.extend({
3333
runTaskOptions.vendorChunk,
3434
runTaskOptions.verbose,
3535
runTaskOptions.progress,
36-
deployUrl
36+
deployUrl,
37+
runTaskOptions.outputHashing
3738
).config;
3839
const webpackCompiler: any = webpack(config);
3940

packages/angular-cli/tasks/build-webpack.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ export default <any>Task.extend({
3434
runTaskOptions.vendorChunk,
3535
runTaskOptions.verbose,
3636
runTaskOptions.progress,
37-
deployUrl
37+
deployUrl,
38+
runTaskOptions.outputHashing
3839
).config;
3940

4041
const webpackCompiler: any = webpack(config);
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import {stripIndents} from 'common-tags';
2+
import * as fs from 'fs';
3+
import {ng} from '../../utils/process';
4+
import { writeMultipleFiles, expectFileToMatch } from '../../utils/fs';
5+
6+
function verifyMedia(css: RegExp, content: RegExp) {
7+
return new Promise((resolve, reject) => {
8+
const [fileName] = fs.readdirSync('./dist').filter(name => name.match(css));
9+
if (!fileName) {
10+
reject(new Error(`File ${fileName} was expected to exist but not found...`));
11+
}
12+
resolve(fileName);
13+
})
14+
.then(fileName => expectFileToMatch(`dist/${fileName}`, content));
15+
}
16+
17+
export default function() {
18+
return Promise.resolve()
19+
.then(() => writeMultipleFiles({
20+
'src/styles.css': stripIndents`
21+
body { background-image: url("image.svg"); }
22+
`,
23+
'src/image.svg': 'I would like to be an image someday.'
24+
}))
25+
.then(() => ng('build', '--dev', '--output-hashing=all'))
26+
.then(() => expectFileToMatch('dist/index.html', /inline\.[0-9a-f]{20}\.bundle\.js/))
27+
.then(() => expectFileToMatch('dist/index.html', /main\.[0-9a-f]{20}\.bundle\.js/))
28+
.then(() => expectFileToMatch('dist/index.html', /styles\.[0-9a-f]{20}\.bundle\.(css|js)/))
29+
.then(() => verifyMedia(/styles\.[0-9a-f]{20}\.bundle\.(css|js)/, /image\.[0-9a-f]{20}\.svg/))
30+
31+
.then(() => ng('build', '--prod', '--output-hashing=none'))
32+
.then(() => expectFileToMatch('dist/index.html', /inline\.bundle\.js/))
33+
.then(() => expectFileToMatch('dist/index.html', /main\.bundle\.js/))
34+
.then(() => expectFileToMatch('dist/index.html', /styles\.bundle\.(css|js)/))
35+
.then(() => verifyMedia(/styles\.bundle\.(css|js)/, /image\.svg/))
36+
37+
.then(() => ng('build', '--dev', '--output-hashing=media'))
38+
.then(() => expectFileToMatch('dist/index.html', /inline\.bundle\.js/))
39+
.then(() => expectFileToMatch('dist/index.html', /main\.bundle\.js/))
40+
.then(() => expectFileToMatch('dist/index.html', /styles\.bundle\.(css|js)/))
41+
.then(() => verifyMedia(/styles\.bundle\.(css|js)/, /image\.[0-9a-f]{20}\.svg/))
42+
43+
.then(() => ng('build', '--dev', '--output-hashing=bundles'))
44+
.then(() => expectFileToMatch('dist/index.html', /inline\.[0-9a-f]{20}\.bundle\.js/))
45+
.then(() => expectFileToMatch('dist/index.html', /main\.[0-9a-f]{20}\.bundle\.js/))
46+
.then(() => expectFileToMatch('dist/index.html', /styles\.[0-9a-f]{20}\.bundle\.(css|js)/))
47+
.then(() => verifyMedia(/styles\.[0-9a-f]{20}\.bundle\.(css|js)/, /image\.svg/));
48+
}

0 commit comments

Comments
 (0)