Permalink
Browse files

feat(build): allow output hashing to be configured (#3885)

1 parent 888beb7 commit b82fe414df41e00e5cd6b6e524eaa16ec90410c8 @clydin clydin committed with hansl Jan 10, 2017
@@ -26,6 +26,8 @@ ng build
`--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
@@ -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.
@@ -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: 'define the output filename cache-busting hashing mode'
+ }
],
run: function (commandOptions: BuildOptions) {
@@ -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=[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=[name]${hashFormat.file}.[ext]&limit=10000`
+ },
+ { test: /\.(eot|svg)$/, loader: `file-loader?name=[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),
@@ -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 { };
};
@@ -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')
}),
@@ -109,3 +109,21 @@ export function extraEntryParser(
return extraEntry;
});
}
+
+export interface HashFormat {
+ chunk: string;
+ extract: string;
+ file: string;
+}
+
+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'];
+}
@@ -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
@@ -33,7 +33,8 @@ export default Task.extend({
runTaskOptions.vendorChunk,
runTaskOptions.verbose,
runTaskOptions.progress,
- deployUrl
+ deployUrl,
+ runTaskOptions.outputHashing
).config;
const webpackCompiler: any = webpack(config);
@@ -34,7 +34,8 @@ export default <any>Task.extend({
runTaskOptions.vendorChunk,
runTaskOptions.verbose,
runTaskOptions.progress,
- deployUrl
+ deployUrl,
+ runTaskOptions.outputHashing
).config;
const webpackCompiler: any = webpack(config);
@@ -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/));
+}

0 comments on commit b82fe41

Please sign in to comment.