Skip to content

Commit 342867e

Browse files
committedJun 3, 2018
feat: Use webpack 4 entries api to extract asset information
BREAKING CHANGES: Pass the entry point names to the custom sort function instead of chunk objects. Removed the alter`htmlWebpackPluginAlterChunks` hook. Changed the structure of the `assets` argument for all hooks.
1 parent 37db086 commit 342867e

File tree

4 files changed

+193
-253
lines changed

4 files changed

+193
-253
lines changed
 

‎README.md

-4
Original file line numberDiff line numberDiff line change
@@ -289,10 +289,6 @@ plugins: [
289289

290290
To allow other [plugins](https://github.com/webpack/docs/wiki/plugins) to alter the HTML this plugin executes the following events:
291291

292-
#### `SyncWaterfallHook`
293-
294-
* `htmlWebpackPluginAlterChunks`
295-
296292
#### `AsyncSeriesWaterfallHook`
297293

298294
* `htmlWebpackPluginBeforeHtmlGeneration`

‎index.js

+167-195
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class HtmlWebpackPlugin {
3636
/**
3737
* @type {HtmlWebpackPluginOptions}
3838
*/
39-
this.options = _.extend({
39+
this.options = Object.assign({
4040
template: path.join(__dirname, 'default_index.ejs'),
4141
templateContent: undefined,
4242
templateParameters: templateParametersGenerator,
@@ -125,138 +125,119 @@ class HtmlWebpackPlugin {
125125
* @param {WebpackCompilation} compilation
126126
* @param {() => void} callback
127127
*/
128-
(compilation, callback) => {
129-
const applyPluginsAsyncWaterfall = self.applyPluginsAsyncWaterfall(compilation);
130-
// Get chunks info as json
131-
// Note: we're excluding stuff that we don't need to improve toJson serialization speed.
132-
const chunkOnlyConfig = {
133-
assets: false,
134-
cached: false,
135-
children: false,
136-
chunks: true,
137-
chunkModules: false,
138-
chunkOrigins: false,
139-
errorDetails: false,
140-
hash: false,
141-
modules: false,
142-
reasons: false,
143-
source: false,
144-
timings: false,
145-
version: false
146-
};
147-
const allChunks = compilation.getStats().toJson(chunkOnlyConfig).chunks;
148-
// Filter chunks (options.chunks and options.excludeCHunks)
149-
let chunks = self.filterChunks(allChunks, self.options.chunks, self.options.excludeChunks);
150-
// Sort chunks
151-
chunks = self.sortChunks(chunks, self.options.chunksSortMode, compilation);
152-
// Let plugins alter the chunks and the chunk sorting
153-
chunks = compilation.hooks.htmlWebpackPluginAlterChunks.call(chunks, { plugin: self });
154-
// Get assets
155-
const assets = self.htmlWebpackPluginAssets(compilation, chunks);
156-
// If this is a hot update compilation, move on!
157-
// This solves a problem where an `index.html` file is generated for hot-update js files
158-
// It only happens in Webpack 2, where hot updates are emitted separately before the full bundle
159-
if (self.isHotUpdateCompilation(assets)) {
160-
return callback();
161-
}
128+
(compilation, callback) => {
129+
const applyPluginsAsyncWaterfall = self.applyPluginsAsyncWaterfall(compilation);
130+
// Get all entry point names for this html file
131+
const entryNames = Array.from(compilation.entrypoints.keys());
132+
const filteredEntryNames = self.filterChunks(entryNames, self.options.chunks, self.options.excludeChunks);
133+
const sortedEntryNames = self.sortEntryChunks(filteredEntryNames, this.options.chunksSortMode, compilation);
134+
// Turn the entry point names into file paths
135+
const assets = self.htmlWebpackPluginAssets(compilation, sortedEntryNames);
136+
137+
// If this is a hot update compilation, move on!
138+
// This solves a problem where an `index.html` file is generated for hot-update js files
139+
// It only happens in Webpack 2, where hot updates are emitted separately before the full bundle
140+
if (self.isHotUpdateCompilation(assets)) {
141+
return callback();
142+
}
162143

163-
// If the template and the assets did not change we don't have to emit the html
164-
const assetJson = JSON.stringify(self.getAssetFiles(assets));
165-
if (isCompilationCached && self.options.cache && assetJson === self.assetJson) {
166-
return callback();
167-
} else {
168-
self.assetJson = assetJson;
169-
}
144+
// If the template and the assets did not change we don't have to emit the html
145+
const assetJson = JSON.stringify(self.getAssetFiles(assets));
146+
if (isCompilationCached && self.options.cache && assetJson === self.assetJson) {
147+
return callback();
148+
} else {
149+
self.assetJson = assetJson;
150+
}
170151

171-
Promise.resolve()
152+
Promise.resolve()
172153
// Favicon
173-
.then(() => {
174-
if (self.options.favicon) {
175-
return self.addFileToAssets(self.options.favicon, compilation)
176-
.then(faviconBasename => {
177-
let publicPath = compilation.mainTemplate.getPublicPath({hash: compilation.hash}) || '';
178-
if (publicPath && publicPath.substr(-1) !== '/') {
179-
publicPath += '/';
180-
}
181-
assets.favicon = publicPath + faviconBasename;
182-
});
183-
}
184-
})
154+
.then(() => {
155+
if (self.options.favicon) {
156+
return self.addFileToAssets(self.options.favicon, compilation)
157+
.then(faviconBasename => {
158+
let publicPath = compilation.mainTemplate.getPublicPath({hash: compilation.hash}) || '';
159+
if (publicPath && publicPath.substr(-1) !== '/') {
160+
publicPath += '/';
161+
}
162+
assets.favicon = publicPath + faviconBasename;
163+
});
164+
}
165+
})
185166
// Wait for the compilation to finish
186-
.then(() => compilationPromise)
187-
.then(compiledTemplate => {
167+
.then(() => compilationPromise)
168+
.then(compiledTemplate => {
188169
// Allow to use a custom function / string instead
189-
if (self.options.templateContent !== undefined) {
190-
return self.options.templateContent;
191-
}
192-
// Once everything is compiled evaluate the html factory
193-
// and replace it with its content
194-
return self.evaluateCompilationResult(compilation, compiledTemplate);
195-
})
170+
if (self.options.templateContent !== undefined) {
171+
return self.options.templateContent;
172+
}
173+
// Once everything is compiled evaluate the html factory
174+
// and replace it with its content
175+
return self.evaluateCompilationResult(compilation, compiledTemplate);
176+
})
196177
// Allow plugins to make changes to the assets before invoking the template
197178
// This only makes sense to use if `inject` is `false`
198-
.then(compilationResult => applyPluginsAsyncWaterfall('htmlWebpackPluginBeforeHtmlGeneration', false, {
199-
assets: assets,
200-
outputName: self.childCompilationOutputName,
201-
plugin: self
202-
})
203-
.then(() => compilationResult))
179+
.then(compilationResult => applyPluginsAsyncWaterfall('htmlWebpackPluginBeforeHtmlGeneration', false, {
180+
assets: assets,
181+
outputName: self.childCompilationOutputName,
182+
plugin: self
183+
})
184+
.then(() => compilationResult))
204185
// Execute the template
205-
.then(compilationResult => typeof compilationResult !== 'function'
206-
? compilationResult
207-
: self.executeTemplate(compilationResult, chunks, assets, compilation))
186+
.then(compilationResult => typeof compilationResult !== 'function'
187+
? compilationResult
188+
: self.executeTemplate(compilationResult, assets, compilation))
208189
// Allow plugins to change the html before assets are injected
209-
.then(html => {
210-
const pluginArgs = {html: html, assets: assets, plugin: self, outputName: self.childCompilationOutputName};
211-
return applyPluginsAsyncWaterfall('htmlWebpackPluginBeforeHtmlProcessing', true, pluginArgs);
212-
})
213-
.then(result => {
214-
const html = result.html;
215-
const assets = result.assets;
216-
// Prepare script and link tags
217-
const assetTags = self.generateHtmlTagObjects(assets);
218-
const pluginArgs = {head: assetTags.head, body: assetTags.body, plugin: self, chunks: chunks, outputName: self.childCompilationOutputName};
219-
// Allow plugins to change the assetTag definitions
220-
return applyPluginsAsyncWaterfall('htmlWebpackPluginAlterAssetTags', true, pluginArgs)
221-
.then(result => self.postProcessHtml(html, assets, { body: result.body, head: result.head })
222-
.then(html => _.extend(result, {html: html, assets: assets})));
223-
})
190+
.then(html => {
191+
const pluginArgs = {html: html, assets: assets, plugin: self, outputName: self.childCompilationOutputName};
192+
return applyPluginsAsyncWaterfall('htmlWebpackPluginBeforeHtmlProcessing', true, pluginArgs);
193+
})
194+
.then(result => {
195+
const html = result.html;
196+
const assets = result.assets;
197+
// Prepare script and link tags
198+
const assetTags = self.generateHtmlTagObjects(assets);
199+
const pluginArgs = {head: assetTags.head, body: assetTags.body, plugin: self, outputName: self.childCompilationOutputName};
200+
// Allow plugins to change the assetTag definitions
201+
return applyPluginsAsyncWaterfall('htmlWebpackPluginAlterAssetTags', true, pluginArgs)
202+
.then(result => self.postProcessHtml(html, assets, { body: result.body, head: result.head })
203+
.then(html => _.extend(result, {html: html, assets: assets})));
204+
})
224205
// Allow plugins to change the html after assets are injected
225-
.then(result => {
226-
const html = result.html;
227-
const assets = result.assets;
228-
const pluginArgs = {html: html, assets: assets, plugin: self, outputName: self.childCompilationOutputName};
229-
return applyPluginsAsyncWaterfall('htmlWebpackPluginAfterHtmlProcessing', true, pluginArgs)
230-
.then(result => result.html);
231-
})
232-
.catch(err => {
206+
.then(result => {
207+
const html = result.html;
208+
const assets = result.assets;
209+
const pluginArgs = {html: html, assets: assets, plugin: self, outputName: self.childCompilationOutputName};
210+
return applyPluginsAsyncWaterfall('htmlWebpackPluginAfterHtmlProcessing', true, pluginArgs)
211+
.then(result => result.html);
212+
})
213+
.catch(err => {
233214
// In case anything went wrong the promise is resolved
234215
// with the error message and an error is logged
235-
compilation.errors.push(prettyError(err, compiler.context).toString());
236-
// Prevent caching
237-
self.hash = null;
238-
return self.options.showErrors ? prettyError(err, compiler.context).toHtml() : 'ERROR';
239-
})
240-
.then(html => {
216+
compilation.errors.push(prettyError(err, compiler.context).toString());
217+
// Prevent caching
218+
self.hash = null;
219+
return self.options.showErrors ? prettyError(err, compiler.context).toHtml() : 'ERROR';
220+
})
221+
.then(html => {
241222
// Replace the compilation result with the evaluated html code
242-
compilation.assets[self.childCompilationOutputName] = {
243-
source: () => html,
244-
size: () => html.length
245-
};
246-
})
247-
.then(() => applyPluginsAsyncWaterfall('htmlWebpackPluginAfterEmit', false, {
248-
html: compilation.assets[self.childCompilationOutputName],
249-
outputName: self.childCompilationOutputName,
250-
plugin: self
251-
}).catch(err => {
252-
console.error(err);
253-
return null;
254-
}).then(() => null))
223+
compilation.assets[self.childCompilationOutputName] = {
224+
source: () => html,
225+
size: () => html.length
226+
};
227+
})
228+
.then(() => applyPluginsAsyncWaterfall('htmlWebpackPluginAfterEmit', false, {
229+
html: compilation.assets[self.childCompilationOutputName],
230+
outputName: self.childCompilationOutputName,
231+
plugin: self
232+
}).catch(err => {
233+
console.error(err);
234+
return null;
235+
}).then(() => null))
255236
// Let webpack continue with it
256-
.then(() => {
257-
callback();
258-
});
259-
});
237+
.then(() => {
238+
callback();
239+
});
240+
});
260241
}
261242

262243
/**
@@ -309,7 +290,7 @@ class HtmlWebpackPlugin {
309290
*
310291
* @returns Promise<string>
311292
*/
312-
executeTemplate (templateFunction, chunks, assets, compilation) {
293+
executeTemplate (templateFunction, assets, compilation) {
313294
// Template processing
314295
const templateParams = this.getTemplateParameters(compilation, assets);
315296
let html = '';
@@ -363,35 +344,38 @@ class HtmlWebpackPlugin {
363344
fsStatAsync(filename),
364345
fsReadFileAsync(filename)
365346
])
366-
.then(([size, source]) => {
367-
return {
368-
size,
369-
source
370-
};
371-
})
372-
.catch(() => Promise.reject(new Error('HtmlWebpackPlugin: could not load file ' + filename)))
373-
.then(results => {
374-
const basename = path.basename(filename);
375-
compilation.fileDependencies.add(filename);
376-
compilation.assets[basename] = {
377-
source: () => results.source,
378-
size: () => results.size.size
379-
};
380-
return basename;
381-
});
347+
.then(([size, source]) => {
348+
return {
349+
size,
350+
source
351+
};
352+
})
353+
.catch(() => Promise.reject(new Error('HtmlWebpackPlugin: could not load file ' + filename)))
354+
.then(results => {
355+
const basename = path.basename(filename);
356+
compilation.fileDependencies.add(filename);
357+
compilation.assets[basename] = {
358+
source: () => results.source,
359+
size: () => results.size.size
360+
};
361+
return basename;
362+
});
382363
}
383364

384365
/**
385366
* Helper to sort chunks
367+
* @param {string[]} entryNames
368+
* @param {string|((entryNameA: string, entryNameB: string) => number)} sortMode
369+
* @param {WebpackCompilation} compilation
386370
*/
387-
sortChunks (chunks, sortMode, compilation) {
371+
sortEntryChunks (entryNames, sortMode, compilation) {
388372
// Custom function
389373
if (typeof sortMode === 'function') {
390-
return chunks.sort(sortMode);
374+
return entryNames.sort(sortMode);
391375
}
392376
// Check if the given sort mode is a valid chunkSorter sort mode
393377
if (typeof chunkSorter[sortMode] !== 'undefined') {
394-
return chunkSorter[sortMode](chunks, this.options, compilation);
378+
return chunkSorter[sortMode](entryNames, compilation, this.options);
395379
}
396380
throw new Error('"' + sortMode + '" is not a valid chunk sort mode');
397381
}
@@ -403,20 +387,7 @@ class HtmlWebpackPlugin {
403387
* @param {string[]} excludedChunks
404388
*/
405389
filterChunks (chunks, includedChunks, excludedChunks) {
406-
return chunks.filter(chunk => {
407-
const chunkName = chunk.names[0];
408-
// This chunk doesn't have a name. This script can't handled it.
409-
if (chunkName === undefined) {
410-
return false;
411-
}
412-
// Skip if the chunk should be lazy loaded
413-
if (typeof chunk.isInitial === 'function') {
414-
if (!chunk.isInitial()) {
415-
return false;
416-
}
417-
} else if (!chunk.initial) {
418-
return false;
419-
}
390+
return chunks.filter(chunkName => {
420391
// Skip if the chunks should be filtered and the given chunk was not added explicity
421392
if (Array.isArray(includedChunks) && includedChunks.indexOf(chunkName) === -1) {
422393
return false;
@@ -435,9 +406,10 @@ class HtmlWebpackPlugin {
435406
}
436407

437408
/**
438-
*
409+
* The htmlWebpackPluginAssets extracts the asset information of a webpack compilation
410+
* for all given entry names
439411
* @param {WebpackCompilation} compilation
440-
* @param {any} chunks
412+
* @param {string[]} entryNames
441413
* @returns {{
442414
publicPath: string,
443415
js: Array<{entryName: string, path: string}>,
@@ -446,10 +418,14 @@ class HtmlWebpackPlugin {
446418
favicon?: string
447419
}}
448420
*/
449-
htmlWebpackPluginAssets (compilation, chunks) {
421+
htmlWebpackPluginAssets (compilation, entryNames) {
450422
const compilationHash = compilation.hash;
451423

452-
// Use the configured public path or build a relative path
424+
/**
425+
* @type {string} the configured public path to the asset root
426+
* if a publicPath is set in the current webpack config use it otherwise
427+
* fallback to a realtive path
428+
*/
453429
let publicPath = typeof compilation.options.output.publicPath !== 'undefined'
454430
// If a hard coded public path exists use it
455431
? compilation.mainTemplate.getPublicPath({hash: compilationHash})
@@ -478,50 +454,46 @@ class HtmlWebpackPlugin {
478454
// Will contain all css files
479455
css: [],
480456
// Will contain the html5 appcache manifest files if it exists
481-
manifest: Object.keys(compilation.assets).find(assetFile => path.extname(assetFile) === '.appcache')
457+
manifest: Object.keys(compilation.assets).find(assetFile => path.extname(assetFile) === '.appcache'),
458+
// Favicon
459+
favicon: undefined
482460
};
483461

484462
// Append a hash for cache busting
485463
if (this.options.hash) {
486464
assets.manifest = this.appendHash(assets.manifest, compilationHash);
487465
}
488466

489-
for (let i = 0; i < chunks.length; i++) {
490-
const chunk = chunks[i];
491-
const chunkName = chunk.names[0];
492-
493-
// Prepend the public path to all chunk files
494-
let chunkFiles = [].concat(chunk.files).map(chunkFile => publicPath + chunkFile);
495-
496-
// Append a hash for cache busting
497-
if (this.options.hash) {
498-
chunkFiles = chunkFiles.map(chunkFile => this.appendHash(chunkFile, compilationHash));
499-
}
500-
501-
// Webpack outputs an array for each chunk when using sourcemaps
502-
// or when one chunk hosts js and css simultaneously
503-
const js = chunkFiles.find(chunkFile => /.js($|\?)/.test(chunkFile));
504-
if (js) {
505-
assets.js.push({
506-
entryName: chunkName,
507-
path: js
467+
// Extract paths to .js and .css files from the current compilation
468+
const extensionRegexp = /\.(css|js)(\?|$)/;
469+
for (let i = 0; i < entryNames.length; i++) {
470+
const entryName = entryNames[i];
471+
const entryPointFiles = compilation.entrypoints.get(entryName).getFiles();
472+
// Prepend the publicPath and append the hash depending on the
473+
// webpack.output.publicPath and hashOptions
474+
// E.g. bundle.js -> /bundle.js?hash
475+
const entryPointPublicPaths = entryPointFiles
476+
.map(chunkFile => {
477+
const entryPointPublicPath = publicPath + chunkFile;
478+
return this.options.hash
479+
? this.appendHash(entryPointPublicPath, compilationHash)
480+
: entryPointPublicPath;
508481
});
509-
}
510482

511-
// Gather all css files
512-
const css = chunkFiles.filter(chunkFile => /.css($|\?)/.test(chunkFile));
513-
css.forEach((cssPath) => {
514-
assets.css.push({
515-
entryName: chunkName,
516-
path: cssPath
483+
entryPointPublicPaths.forEach((entryPointPublicPaths) => {
484+
const extMatch = extensionRegexp.exec(entryPointPublicPaths);
485+
// Skip if the public path is not a .css or .js file
486+
if (!extMatch) {
487+
return;
488+
}
489+
// ext will contain .js or .css
490+
const ext = extMatch[1];
491+
assets[ext].push({
492+
entryName: entryName,
493+
path: entryPointPublicPaths
517494
});
518495
});
519496
}
520-
521-
// Duplicate css assets can occur on occasion if more than one chunk
522-
// requires the same css.
523-
assets.css = _.uniq(assets.css);
524-
525497
return assets;
526498
}
527499

@@ -728,12 +700,12 @@ class HtmlWebpackPlugin {
728700
);
729701
}
730702
return compilation.hooks[eventName].promise(pluginArgs)
731-
.then(result => {
732-
if (requiresResult && !result) {
733-
throw new Error('Using ' + eventName + ' did not return a result.');
734-
}
735-
return result;
736-
});
703+
.then(result => {
704+
if (requiresResult && !result) {
705+
throw new Error('Using ' + eventName + ' did not return a result.');
706+
}
707+
return result;
708+
});
737709
};
738710
}
739711
}

‎lib/chunksorter.js

+24-19
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,47 @@
11
// @ts-check
2+
/** @typedef {import("webpack/lib/Compilation.js")} WebpackCompilation */
23
'use strict';
34

45
// Import webpack types using commonjs
56
// As we use only the type we have to prevent warnings about unused varaibles
67
/* eslint-disable */
78
const WebpackCompilation = require('webpack/lib/Compilation');
89
/* eslint-enable */
10+
11+
/**
12+
* @type {{[sortmode: string] : (entryPointNames: Array<string>, compilation, htmlWebpackPluginOptions) => Array<string> }}
13+
* This file contains different sort methods for the entry chunks names
14+
*/
15+
const sortFunctions = {};
16+
module.exports = sortFunctions;
17+
918
/**
1019
* Performs identity mapping (no-sort).
1120
* @param {Array} chunks the chunks to sort
1221
* @return {Array} The sorted chunks
1322
*/
14-
module.exports.none = chunks => chunks;
23+
sortFunctions.none = chunks => chunks;
1524

1625
/**
1726
* Sort manually by the chunks
18-
* @param {Array} chunks the chunks to sort
19-
* @return {Array} The sorted chunks
27+
* @param {string[]} entryPointNames the chunks to sort
28+
* @param {WebpackCompilation} compilation the webpack compilation
29+
* @param htmlWebpackPluginOptions the plugin options
30+
* @return {string[]} The sorted chunks
2031
*/
21-
module.exports.manual = (chunks, options) => {
22-
const specifyChunks = options.chunks;
23-
const chunksResult = [];
24-
let filterResult = [];
25-
if (Array.isArray(specifyChunks)) {
26-
for (var i = 0; i < specifyChunks.length; i++) {
27-
filterResult = chunks.filter(chunk => {
28-
if (chunk.names[0] && chunk.names[0] === specifyChunks[i]) {
29-
return true;
30-
}
31-
return false;
32-
});
33-
filterResult.length > 0 && chunksResult.push(filterResult[0]);
34-
}
32+
sortFunctions.manual = (entryPointNames, compilation, htmlWebpackPluginOptions) => {
33+
const chunks = htmlWebpackPluginOptions.chunks;
34+
if (!Array.isArray(chunks)) {
35+
return entryPointNames;
3536
}
36-
return chunksResult;
37+
// Remove none existing entries from
38+
// htmlWebpackPluginOptions.chunks
39+
return chunks.filter((entryPointName) => {
40+
return compilation.entrypoints.has(entryPointName);
41+
});
3742
};
3843

3944
/**
4045
* Defines the default sorter.
4146
*/
42-
module.exports.auto = module.exports.none;
47+
sortFunctions.auto = module.exports.none;

‎spec/BasicSpec.js

+2-35
Original file line numberDiff line numberDiff line change
@@ -812,39 +812,6 @@ describe('HtmlWebpackPlugin', function () {
812812
shouldExpectWarnings);
813813
});
814814

815-
it('passes chunks to the html-webpack-plugin-alter-asset-tags event', function (done) {
816-
var chunks;
817-
var examplePlugin = {
818-
apply: function (compiler) {
819-
compiler.plugin('compilation', function (compilation) {
820-
tapCompilationEvent(compilation, 'html-webpack-plugin-alter-asset-tags', function (object, callback) {
821-
chunks = object.chunks;
822-
callback();
823-
});
824-
});
825-
}
826-
};
827-
828-
var shouldExpectWarnings = webpackMajorVersion < 4;
829-
testHtmlPlugin({
830-
entry: {
831-
app: path.join(__dirname, 'fixtures/index.js')
832-
},
833-
output: {
834-
path: OUTPUT_DIR,
835-
filename: '[name]_bundle.js'
836-
},
837-
plugins: [
838-
new HtmlWebpackPlugin(),
839-
examplePlugin
840-
]
841-
}, [], null, function () {
842-
expect(chunks).toBeDefined();
843-
done();
844-
}, false,
845-
shouldExpectWarnings);
846-
});
847-
848815
it('allows events to add a no-value attribute', function (done) {
849816
var examplePlugin = {
850817
apply: function (compiler) {
@@ -1527,10 +1494,10 @@ describe('HtmlWebpackPlugin', function () {
15271494
plugins: [
15281495
new HtmlWebpackPlugin({
15291496
chunksSortMode: function (a, b) {
1530-
if (a.names[0] < b.names[0]) {
1497+
if (a < b) {
15311498
return 1;
15321499
}
1533-
if (a.names[0] > b.names[0]) {
1500+
if (a > b) {
15341501
return -1;
15351502
}
15361503
return 0;

0 commit comments

Comments
 (0)
Please sign in to comment.