html-webpack-plugin 5.5.3 → 5.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -1,11 +1,4 @@
1
1
  // @ts-check
2
- // Import types
3
- /** @typedef {import("./typings").HtmlTagObject} HtmlTagObject */
4
- /** @typedef {import("./typings").Options} HtmlWebpackOptions */
5
- /** @typedef {import("./typings").ProcessedOptions} ProcessedHtmlWebpackOptions */
6
- /** @typedef {import("./typings").TemplateParameter} TemplateParameter */
7
- /** @typedef {import("webpack/lib/Compiler.js")} WebpackCompiler */
8
- /** @typedef {import("webpack/lib/Compilation.js")} WebpackCompilation */
9
2
  'use strict';
10
3
 
11
4
  const promisify = require('util').promisify;
@@ -17,13 +10,19 @@ const path = require('path');
17
10
  const { CachedChildCompilation } = require('./lib/cached-child-compiler');
18
11
 
19
12
  const { createHtmlTagObject, htmlTagObjectToString, HtmlTagArray } = require('./lib/html-tags');
20
-
21
13
  const prettyError = require('./lib/errors.js');
22
14
  const chunkSorter = require('./lib/chunksorter.js');
23
15
  const getHtmlWebpackPluginHooks = require('./lib/hooks.js').getHtmlWebpackPluginHooks;
24
- const { assert } = require('console');
25
16
 
26
- const fsReadFileAsync = promisify(fs.readFile);
17
+ /** @typedef {import("./typings").HtmlTagObject} HtmlTagObject */
18
+ /** @typedef {import("./typings").Options} HtmlWebpackOptions */
19
+ /** @typedef {import("./typings").ProcessedOptions} ProcessedHtmlWebpackOptions */
20
+ /** @typedef {import("./typings").TemplateParameter} TemplateParameter */
21
+ /** @typedef {import("webpack").Compiler} Compiler */
22
+ /** @typedef {ReturnType<Compiler["getInfrastructureLogger"]>} Logger */
23
+ /** @typedef {import("webpack/lib/Compilation.js")} Compilation */
24
+ /** @typedef {Array<{ name: string, source: import('webpack').sources.Source, info?: import('webpack').AssetInfo }>} PreviousEmittedAssets */
25
+ /** @typedef {{ publicPath: string, js: Array<string>, css: Array<string>, manifest?: string, favicon?: string }} AssetsInformationByGroups */
27
26
 
28
27
  class HtmlWebpackPlugin {
29
28
  /**
@@ -31,60 +30,89 @@ class HtmlWebpackPlugin {
31
30
  */
32
31
  constructor (options) {
33
32
  /** @type {HtmlWebpackOptions} */
33
+ // TODO remove me in the next major release
34
34
  this.userOptions = options || {};
35
35
  this.version = HtmlWebpackPlugin.version;
36
+
37
+ // Default options
38
+ /** @type {ProcessedHtmlWebpackOptions} */
39
+ const defaultOptions = {
40
+ template: 'auto',
41
+ templateContent: false,
42
+ templateParameters: templateParametersGenerator,
43
+ filename: 'index.html',
44
+ publicPath: this.userOptions.publicPath === undefined ? 'auto' : this.userOptions.publicPath,
45
+ hash: false,
46
+ inject: this.userOptions.scriptLoading === 'blocking' ? 'body' : 'head',
47
+ scriptLoading: 'defer',
48
+ compile: true,
49
+ favicon: false,
50
+ minify: 'auto',
51
+ cache: true,
52
+ showErrors: true,
53
+ chunks: 'all',
54
+ excludeChunks: [],
55
+ chunksSortMode: 'auto',
56
+ meta: {},
57
+ base: false,
58
+ title: 'Webpack App',
59
+ xhtml: false
60
+ };
61
+
62
+ /** @type {ProcessedHtmlWebpackOptions} */
63
+ this.options = Object.assign(defaultOptions, this.userOptions);
36
64
  }
37
65
 
66
+ /**
67
+ *
68
+ * @param {Compiler} compiler
69
+ * @returns {void}
70
+ */
38
71
  apply (compiler) {
72
+ this.logger = compiler.getInfrastructureLogger('HtmlWebpackPlugin');
73
+
39
74
  // Wait for configuration preset plugions to apply all configure webpack defaults
40
75
  compiler.hooks.initialize.tap('HtmlWebpackPlugin', () => {
41
- const userOptions = this.userOptions;
42
-
43
- // Default options
44
- /** @type {ProcessedHtmlWebpackOptions} */
45
- const defaultOptions = {
46
- template: 'auto',
47
- templateContent: false,
48
- templateParameters: templateParametersGenerator,
49
- filename: 'index.html',
50
- publicPath: userOptions.publicPath === undefined ? 'auto' : userOptions.publicPath,
51
- hash: false,
52
- inject: userOptions.scriptLoading === 'blocking' ? 'body' : 'head',
53
- scriptLoading: 'defer',
54
- compile: true,
55
- favicon: false,
56
- minify: 'auto',
57
- cache: true,
58
- showErrors: true,
59
- chunks: 'all',
60
- excludeChunks: [],
61
- chunksSortMode: 'auto',
62
- meta: {},
63
- base: false,
64
- title: 'Webpack App',
65
- xhtml: false
66
- };
76
+ const options = this.options;
67
77
 
68
- /** @type {ProcessedHtmlWebpackOptions} */
69
- const options = Object.assign(defaultOptions, userOptions);
70
- this.options = options;
78
+ options.template = this.getTemplatePath(this.options.template, compiler.context);
71
79
 
72
80
  // Assert correct option spelling
73
- assert(options.scriptLoading === 'defer' || options.scriptLoading === 'blocking' || options.scriptLoading === 'module', 'scriptLoading needs to be set to "defer", "blocking" or "module"');
74
- assert(options.inject === true || options.inject === false || options.inject === 'head' || options.inject === 'body', 'inject needs to be set to true, false, "head" or "body');
81
+ if (options.scriptLoading !== 'defer' && options.scriptLoading !== 'blocking' && options.scriptLoading !== 'module' && options.scriptLoading !== 'systemjs-module') {
82
+ /** @type {Logger} */
83
+ (this.logger).error('The "scriptLoading" option need to be set to "defer", "blocking" or "module" or "systemjs-module"');
84
+ }
85
+
86
+ if (options.inject !== true && options.inject !== false && options.inject !== 'head' && options.inject !== 'body') {
87
+ /** @type {Logger} */
88
+ (this.logger).error('The `inject` option needs to be set to true, false, "head" or "body');
89
+ }
90
+
91
+ if (
92
+ this.options.templateParameters !== false &&
93
+ typeof this.options.templateParameters !== 'function' &&
94
+ typeof this.options.templateParameters !== 'object'
95
+ ) {
96
+ /** @type {Logger} */
97
+ (this.logger).error('The `templateParameters` has to be either a function or an object or false');
98
+ }
75
99
 
76
100
  // Default metaOptions if no template is provided
77
- if (!userOptions.template && options.templateContent === false && options.meta) {
78
- const defaultMeta = {
79
- // TODO remove in the next major release
80
- // From https://developer.mozilla.org/en-US/docs/Mozilla/Mobile/Viewport_meta_tag
81
- viewport: 'width=device-width, initial-scale=1'
82
- };
83
- options.meta = Object.assign({}, options.meta, defaultMeta, userOptions.meta);
101
+ if (!this.userOptions.template && options.templateContent === false && options.meta) {
102
+ options.meta = Object.assign(
103
+ {},
104
+ options.meta,
105
+ {
106
+ // TODO remove in the next major release
107
+ // From https://developer.mozilla.org/en-US/docs/Mozilla/Mobile/Viewport_meta_tag
108
+ viewport: 'width=device-width, initial-scale=1'
109
+ },
110
+ this.userOptions.meta
111
+ );
84
112
  }
85
113
 
86
114
  // entryName to fileName conversion function
87
- const userOptionFilename = userOptions.filename || defaultOptions.filename;
115
+ const userOptionFilename = this.userOptions.filename || this.options.filename;
88
116
  const filenameFunction = typeof userOptionFilename === 'function'
89
117
  ? userOptionFilename
90
118
  // Replace '[name]' with entry name
@@ -94,19 +122,313 @@ class HtmlWebpackPlugin {
94
122
  const entryNames = Object.keys(compiler.options.entry);
95
123
  const outputFileNames = new Set((entryNames.length ? entryNames : ['main']).map(filenameFunction));
96
124
 
97
- /** Option for every entry point */
98
- const entryOptions = Array.from(outputFileNames).map((filename) => ({
99
- ...options,
100
- filename
101
- }));
102
-
103
125
  // Hook all options into the webpack compiler
104
- entryOptions.forEach((instanceOptions) => {
105
- hookIntoCompiler(compiler, instanceOptions, this);
126
+ outputFileNames.forEach((outputFileName) => {
127
+ // Instance variables to keep caching information for multiple builds
128
+ const assetJson = { value: undefined };
129
+ /**
130
+ * store the previous generated asset to emit them even if the content did not change
131
+ * to support watch mode for third party plugins like the clean-webpack-plugin or the compression plugin
132
+ * @type {PreviousEmittedAssets}
133
+ */
134
+ const previousEmittedAssets = [];
135
+
136
+ // Inject child compiler plugin
137
+ const childCompilerPlugin = new CachedChildCompilation(compiler);
138
+
139
+ if (!this.options.templateContent) {
140
+ childCompilerPlugin.addEntry(this.options.template);
141
+ }
142
+
143
+ // convert absolute filename into relative so that webpack can
144
+ // generate it at correct location
145
+ let filename = outputFileName;
146
+
147
+ if (path.resolve(filename) === path.normalize(filename)) {
148
+ const outputPath = /** @type {string} - Once initialized the path is always a string */(compiler.options.output.path);
149
+
150
+ filename = path.relative(outputPath, filename);
151
+ }
152
+
153
+ compiler.hooks.thisCompilation.tap('HtmlWebpackPlugin',
154
+ /**
155
+ * Hook into the webpack compilation
156
+ * @param {Compilation} compilation
157
+ */
158
+ (compilation) => {
159
+ compilation.hooks.processAssets.tapAsync(
160
+ {
161
+ name: 'HtmlWebpackPlugin',
162
+ stage:
163
+ /**
164
+ * Generate the html after minification and dev tooling is done
165
+ */
166
+ compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE
167
+ },
168
+ /**
169
+ * Hook into the process assets hook
170
+ * @param {any} _
171
+ * @param {(err?: Error) => void} callback
172
+ */
173
+ (_, callback) => {
174
+ this.generateHTML(compiler, compilation, filename, childCompilerPlugin, previousEmittedAssets, assetJson, callback);
175
+ });
176
+ });
106
177
  });
107
178
  });
108
179
  }
109
180
 
181
+ /**
182
+ * Helper to return the absolute template path with a fallback loader
183
+ *
184
+ * @private
185
+ * @param {string} template The path to the template e.g. './index.html'
186
+ * @param {string} context The webpack base resolution path for relative paths e.g. process.cwd()
187
+ */
188
+ getTemplatePath (template, context) {
189
+ if (template === 'auto') {
190
+ template = path.resolve(context, 'src/index.ejs');
191
+ if (!fs.existsSync(template)) {
192
+ template = path.join(__dirname, 'default_index.ejs');
193
+ }
194
+ }
195
+
196
+ // If the template doesn't use a loader use the lodash template loader
197
+ if (template.indexOf('!') === -1) {
198
+ template = require.resolve('./lib/loader.js') + '!' + path.resolve(context, template);
199
+ }
200
+
201
+ // Resolve template path
202
+ return template.replace(
203
+ /([!])([^/\\][^!?]+|[^/\\!?])($|\?[^!?\n]+$)/,
204
+ (match, prefix, filepath, postfix) => prefix + path.resolve(filepath) + postfix);
205
+ }
206
+
207
+ /**
208
+ * Return all chunks from the compilation result which match the exclude and include filters
209
+ *
210
+ * @private
211
+ * @param {any} chunks
212
+ * @param {string[]|'all'} includedChunks
213
+ * @param {string[]} excludedChunks
214
+ */
215
+ filterEntryChunks (chunks, includedChunks, excludedChunks) {
216
+ return chunks.filter(chunkName => {
217
+ // Skip if the chunks should be filtered and the given chunk was not added explicity
218
+ if (Array.isArray(includedChunks) && includedChunks.indexOf(chunkName) === -1) {
219
+ return false;
220
+ }
221
+
222
+ // Skip if the chunks should be filtered and the given chunk was excluded explicity
223
+ if (Array.isArray(excludedChunks) && excludedChunks.indexOf(chunkName) !== -1) {
224
+ return false;
225
+ }
226
+
227
+ // Add otherwise
228
+ return true;
229
+ });
230
+ }
231
+
232
+ /**
233
+ * Helper to sort chunks
234
+ *
235
+ * @private
236
+ * @param {string[]} entryNames
237
+ * @param {string|((entryNameA: string, entryNameB: string) => number)} sortMode
238
+ * @param {Compilation} compilation
239
+ */
240
+ sortEntryChunks (entryNames, sortMode, compilation) {
241
+ // Custom function
242
+ if (typeof sortMode === 'function') {
243
+ return entryNames.sort(sortMode);
244
+ }
245
+ // Check if the given sort mode is a valid chunkSorter sort mode
246
+ if (typeof chunkSorter[sortMode] !== 'undefined') {
247
+ return chunkSorter[sortMode](entryNames, compilation, this.options);
248
+ }
249
+ throw new Error('"' + sortMode + '" is not a valid chunk sort mode');
250
+ }
251
+
252
+ /**
253
+ * Encode each path component using `encodeURIComponent` as files can contain characters
254
+ * which needs special encoding in URLs like `+ `.
255
+ *
256
+ * Valid filesystem characters which need to be encoded for urls:
257
+ *
258
+ * # pound, % percent, & ampersand, { left curly bracket, } right curly bracket,
259
+ * \ back slash, < left angle bracket, > right angle bracket, * asterisk, ? question mark,
260
+ * blank spaces, $ dollar sign, ! exclamation point, ' single quotes, " double quotes,
261
+ * : colon, @ at sign, + plus sign, ` backtick, | pipe, = equal sign
262
+ *
263
+ * However the query string must not be encoded:
264
+ *
265
+ * fo:demonstration-path/very fancy+name.js?path=/home?value=abc&value=def#zzz
266
+ * ^ ^ ^ ^ ^ ^ ^ ^^ ^ ^ ^ ^ ^
267
+ * | | | | | | | || | | | | |
268
+ * encoded | | encoded | | || | | | | |
269
+ * ignored ignored ignored ignored ignored
270
+ *
271
+ * @private
272
+ * @param {string} filePath
273
+ */
274
+ urlencodePath (filePath) {
275
+ // People use the filepath in quite unexpected ways.
276
+ // Try to extract the first querystring of the url:
277
+ //
278
+ // some+path/demo.html?value=abc?def
279
+ //
280
+ const queryStringStart = filePath.indexOf('?');
281
+ const urlPath = queryStringStart === -1 ? filePath : filePath.substr(0, queryStringStart);
282
+ const queryString = filePath.substr(urlPath.length);
283
+ // Encode all parts except '/' which are not part of the querystring:
284
+ const encodedUrlPath = urlPath.split('/').map(encodeURIComponent).join('/');
285
+ return encodedUrlPath + queryString;
286
+ }
287
+
288
+ /**
289
+ * Appends a cache busting hash to the query string of the url
290
+ * E.g. http://localhost:8080/ -> http://localhost:8080/?50c9096ba6183fd728eeb065a26ec175
291
+ *
292
+ * @private
293
+ * @param {string} url
294
+ * @param {string} hash
295
+ */
296
+ appendHash (url, hash) {
297
+ if (!url) {
298
+ return url;
299
+ }
300
+ return url + (url.indexOf('?') === -1 ? '?' : '&') + hash;
301
+ }
302
+
303
+ /**
304
+ * Generate the relative or absolute base url to reference images, css, and javascript files
305
+ * from within the html file - the publicPath
306
+ *
307
+ * @private
308
+ * @param {Compilation} compilation
309
+ * @param {string} filename
310
+ * @param {string | 'auto'} customPublicPath
311
+ * @returns {string}
312
+ */
313
+ getPublicPath (compilation, filename, customPublicPath) {
314
+ /**
315
+ * @type {string} the configured public path to the asset root
316
+ * if a path publicPath is set in the current webpack config use it otherwise
317
+ * fallback to a relative path
318
+ */
319
+ const webpackPublicPath = compilation.getAssetPath(compilation.outputOptions.publicPath, { hash: compilation.hash });
320
+ // Webpack 5 introduced "auto" as default value
321
+ const isPublicPathDefined = webpackPublicPath !== 'auto';
322
+
323
+ let publicPath =
324
+ // If the html-webpack-plugin options contain a custom public path uset it
325
+ customPublicPath !== 'auto'
326
+ ? customPublicPath
327
+ : (isPublicPathDefined
328
+ // If a hard coded public path exists use it
329
+ ? webpackPublicPath
330
+ // If no public path was set get a relative url path
331
+ : path.relative(path.resolve(compilation.options.output.path, path.dirname(filename)), compilation.options.output.path)
332
+ .split(path.sep).join('/')
333
+ );
334
+
335
+ if (publicPath.length && publicPath.substr(-1, 1) !== '/') {
336
+ publicPath += '/';
337
+ }
338
+
339
+ return publicPath;
340
+ }
341
+
342
+ /**
343
+ * The getAssetsForHTML extracts the asset information of a webpack compilation for all given entry names.
344
+ *
345
+ * @private
346
+ * @param {Compilation} compilation
347
+ * @param {string} outputName
348
+ * @param {string[]} entryNames
349
+ * @returns {AssetsInformationByGroups}
350
+ */
351
+ getAssetsInformationByGroups (compilation, outputName, entryNames) {
352
+ /** The public path used inside the html file */
353
+ const publicPath = this.getPublicPath(compilation, outputName, this.options.publicPath);
354
+ /**
355
+ * @type {AssetsInformationByGroups}
356
+ */
357
+ const assets = {
358
+ // The public path
359
+ publicPath,
360
+ // Will contain all js and mjs files
361
+ js: [],
362
+ // Will contain all css files
363
+ css: [],
364
+ // Will contain the html5 appcache manifest files if it exists
365
+ manifest: Object.keys(compilation.assets).find(assetFile => path.extname(assetFile) === '.appcache'),
366
+ // Favicon
367
+ favicon: undefined
368
+ };
369
+
370
+ // Append a hash for cache busting
371
+ if (this.options.hash && assets.manifest) {
372
+ assets.manifest = this.appendHash(assets.manifest, /** @type {string} */ (compilation.hash));
373
+ }
374
+
375
+ // Extract paths to .js, .mjs and .css files from the current compilation
376
+ const entryPointPublicPathMap = {};
377
+ const extensionRegexp = /\.(css|js|mjs)(\?|$)/;
378
+
379
+ for (let i = 0; i < entryNames.length; i++) {
380
+ const entryName = entryNames[i];
381
+ /** entryPointUnfilteredFiles - also includes hot module update files */
382
+ const entryPointUnfilteredFiles = compilation.entrypoints.get(entryName).getFiles();
383
+ const entryPointFiles = entryPointUnfilteredFiles.filter((chunkFile) => {
384
+ const asset = compilation.getAsset(chunkFile);
385
+
386
+ if (!asset) {
387
+ return true;
388
+ }
389
+
390
+ // Prevent hot-module files from being included:
391
+ const assetMetaInformation = asset.info || {};
392
+
393
+ return !(assetMetaInformation.hotModuleReplacement || assetMetaInformation.development);
394
+ });
395
+ // Prepend the publicPath and append the hash depending on the
396
+ // webpack.output.publicPath and hashOptions
397
+ // E.g. bundle.js -> /bundle.js?hash
398
+ const entryPointPublicPaths = entryPointFiles
399
+ .map(chunkFile => {
400
+ const entryPointPublicPath = publicPath + this.urlencodePath(chunkFile);
401
+ return this.options.hash
402
+ ? this.appendHash(entryPointPublicPath, compilation.hash)
403
+ : entryPointPublicPath;
404
+ });
405
+
406
+ entryPointPublicPaths.forEach((entryPointPublicPath) => {
407
+ const extMatch = extensionRegexp.exec(entryPointPublicPath);
408
+
409
+ // Skip if the public path is not a .css, .mjs or .js file
410
+ if (!extMatch) {
411
+ return;
412
+ }
413
+
414
+ // Skip if this file is already known
415
+ // (e.g. because of common chunk optimizations)
416
+ if (entryPointPublicPathMap[entryPointPublicPath]) {
417
+ return;
418
+ }
419
+
420
+ entryPointPublicPathMap[entryPointPublicPath] = true;
421
+
422
+ // ext will contain .js or .css, because .mjs recognizes as .js
423
+ const ext = extMatch[1] === 'mjs' ? 'js' : extMatch[1];
424
+
425
+ assets[ext].push(entryPointPublicPath);
426
+ });
427
+ }
428
+
429
+ return assets;
430
+ }
431
+
110
432
  /**
111
433
  * Once webpack is done with compiling the template into a NodeJS code this function
112
434
  * evaluates it to generate the html result
@@ -115,6 +437,7 @@ class HtmlWebpackPlugin {
115
437
  * Please change that in a further refactoring
116
438
  *
117
439
  * @param {string} source
440
+ * @param {string} publicPath
118
441
  * @param {string} templateFilename
119
442
  * @returns {Promise<string | (() => string | Promise<string>)>}
120
443
  */
@@ -122,11 +445,13 @@ class HtmlWebpackPlugin {
122
445
  if (!source) {
123
446
  return Promise.reject(new Error('The child compilation didn\'t provide a result'));
124
447
  }
448
+
125
449
  // The LibraryTemplatePlugin stores the template result in a local variable.
126
450
  // By adding it to the end the value gets extracted during evaluation
127
451
  if (source.indexOf('HTML_WEBPACK_PLUGIN_RESULT') >= 0) {
128
452
  source += ';\nHTML_WEBPACK_PLUGIN_RESULT';
129
453
  }
454
+
130
455
  const templateWithoutLoaders = templateFilename.replace(/^.+!/, '').replace(/\?.+$/, '');
131
456
  const vmContext = vm.createContext({
132
457
  ...global,
@@ -184,314 +509,102 @@ class HtmlWebpackPlugin {
184
509
  WritableStreamDefaultController: global.WritableStreamDefaultController,
185
510
  WritableStreamDefaultWriter: global.WritableStreamDefaultWriter
186
511
  });
512
+
187
513
  const vmScript = new vm.Script(source, { filename: templateWithoutLoaders });
514
+
188
515
  // Evaluate code and cast to string
189
516
  let newSource;
517
+
190
518
  try {
191
519
  newSource = vmScript.runInContext(vmContext);
192
520
  } catch (e) {
193
521
  return Promise.reject(e);
194
522
  }
523
+
195
524
  if (typeof newSource === 'object' && newSource.__esModule && newSource.default) {
196
525
  newSource = newSource.default;
197
526
  }
527
+
198
528
  return typeof newSource === 'string' || typeof newSource === 'function'
199
529
  ? Promise.resolve(newSource)
200
530
  : Promise.reject(new Error('The loader "' + templateWithoutLoaders + '" didn\'t return html.'));
201
531
  }
202
- }
203
532
 
204
- /**
205
- * connect the html-webpack-plugin to the webpack compiler lifecycle hooks
206
- *
207
- * @param {import('webpack').Compiler} compiler
208
- * @param {ProcessedHtmlWebpackOptions} options
209
- * @param {HtmlWebpackPlugin} plugin
210
- */
211
- function hookIntoCompiler (compiler, options, plugin) {
212
- const webpack = compiler.webpack;
213
- // Instance variables to keep caching information
214
- // for multiple builds
215
- let assetJson;
216
533
  /**
217
- * store the previous generated asset to emit them even if the content did not change
218
- * to support watch mode for third party plugins like the clean-webpack-plugin or the compression plugin
219
- * @type {Array<{html: string, name: string}>}
534
+ * Add toString methods for easier rendering inside the template
535
+ *
536
+ * @private
537
+ * @param {Array<HtmlTagObject>} assetTagGroup
538
+ * @returns {Array<HtmlTagObject>}
220
539
  */
221
- let previousEmittedAssets = [];
222
-
223
- options.template = getFullTemplatePath(options.template, compiler.context);
224
-
225
- // Inject child compiler plugin
226
- const childCompilerPlugin = new CachedChildCompilation(compiler);
227
- if (!options.templateContent) {
228
- childCompilerPlugin.addEntry(options.template);
229
- }
230
-
231
- // convert absolute filename into relative so that webpack can
232
- // generate it at correct location
233
- const filename = options.filename;
234
- if (path.resolve(filename) === path.normalize(filename)) {
235
- const outputPath = /** @type {string} - Once initialized the path is always a string */(compiler.options.output.path);
236
- options.filename = path.relative(outputPath, filename);
237
- }
238
-
239
- // Check if webpack is running in production mode
240
- // @see https://github.com/webpack/webpack/blob/3366421f1784c449f415cda5930a8e445086f688/lib/WebpackOptionsDefaulter.js#L12-L14
241
- const isProductionLikeMode = compiler.options.mode === 'production' || !compiler.options.mode;
242
-
243
- const minify = options.minify;
244
- if (minify === true || (minify === 'auto' && isProductionLikeMode)) {
245
- /** @type { import('html-minifier-terser').Options } */
246
- options.minify = {
247
- // https://www.npmjs.com/package/html-minifier-terser#options-quick-reference
248
- collapseWhitespace: true,
249
- keepClosingSlash: true,
250
- removeComments: true,
251
- removeRedundantAttributes: true,
252
- removeScriptTypeAttributes: true,
253
- removeStyleLinkTypeAttributes: true,
254
- useShortDoctype: true
255
- };
540
+ prepareAssetTagGroupForRendering (assetTagGroup) {
541
+ const xhtml = this.options.xhtml;
542
+ return HtmlTagArray.from(assetTagGroup.map((assetTag) => {
543
+ const copiedAssetTag = Object.assign({}, assetTag);
544
+ copiedAssetTag.toString = function () {
545
+ return htmlTagObjectToString(this, xhtml);
546
+ };
547
+ return copiedAssetTag;
548
+ }));
256
549
  }
257
550
 
258
- compiler.hooks.thisCompilation.tap('HtmlWebpackPlugin',
259
- /**
260
- * Hook into the webpack compilation
261
- * @param {WebpackCompilation} compilation
262
- */
263
- (compilation) => {
264
- compilation.hooks.processAssets.tapAsync(
265
- {
266
- name: 'HtmlWebpackPlugin',
267
- stage:
268
- /**
269
- * Generate the html after minification and dev tooling is done
270
- */
271
- webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE
272
- },
273
- /**
274
- * Hook into the process assets hook
275
- * @param {WebpackCompilation} compilationAssets
276
- * @param {(err?: Error) => void} callback
277
- */
278
- (compilationAssets, callback) => {
279
- // Get all entry point names for this html file
280
- const entryNames = Array.from(compilation.entrypoints.keys());
281
- const filteredEntryNames = filterChunks(entryNames, options.chunks, options.excludeChunks);
282
- const sortedEntryNames = sortEntryChunks(filteredEntryNames, options.chunksSortMode, compilation);
283
-
284
- const templateResult = options.templateContent
285
- ? { mainCompilationHash: compilation.hash }
286
- : childCompilerPlugin.getCompilationEntryResult(options.template);
287
-
288
- if ('error' in templateResult) {
289
- compilation.errors.push(prettyError(templateResult.error, compiler.context).toString());
290
- }
291
-
292
- // If the child compilation was not executed during a previous main compile run
293
- // it is a cached result
294
- const isCompilationCached = templateResult.mainCompilationHash !== compilation.hash;
295
-
296
- /** The public path used inside the html file */
297
- const htmlPublicPath = getPublicPath(compilation, options.filename, options.publicPath);
298
-
299
- /** Generated file paths from the entry point names */
300
- const assets = htmlWebpackPluginAssets(compilation, sortedEntryNames, htmlPublicPath);
301
-
302
- // If the template and the assets did not change we don't have to emit the html
303
- const newAssetJson = JSON.stringify(getAssetFiles(assets));
304
- if (isCompilationCached && options.cache && assetJson === newAssetJson) {
305
- previousEmittedAssets.forEach(({ name, html }) => {
306
- compilation.emitAsset(name, new webpack.sources.RawSource(html, false));
307
- });
308
- return callback();
309
- } else {
310
- previousEmittedAssets = [];
311
- assetJson = newAssetJson;
312
- }
313
-
314
- // The html-webpack plugin uses a object representation for the html-tags which will be injected
315
- // to allow altering them more easily
316
- // Just before they are converted a third-party-plugin author might change the order and content
317
- const assetsPromise = getFaviconPublicPath(options.favicon, compilation, assets.publicPath)
318
- .then((faviconPath) => {
319
- assets.favicon = faviconPath;
320
- return getHtmlWebpackPluginHooks(compilation).beforeAssetTagGeneration.promise({
321
- assets: assets,
322
- outputName: options.filename,
323
- plugin: plugin
324
- });
325
- });
326
-
327
- // Turn the js and css paths into grouped HtmlTagObjects
328
- const assetTagGroupsPromise = assetsPromise
329
- // And allow third-party-plugin authors to reorder and change the assetTags before they are grouped
330
- .then(({ assets }) => getHtmlWebpackPluginHooks(compilation).alterAssetTags.promise({
331
- assetTags: {
332
- scripts: generatedScriptTags(assets.js),
333
- styles: generateStyleTags(assets.css),
334
- meta: [
335
- ...generateBaseTag(options.base),
336
- ...generatedMetaTags(options.meta),
337
- ...generateFaviconTags(assets.favicon)
338
- ]
339
- },
340
- outputName: options.filename,
341
- publicPath: htmlPublicPath,
342
- plugin: plugin
343
- }))
344
- .then(({ assetTags }) => {
345
- // Inject scripts to body unless it set explicitly to head
346
- const scriptTarget = options.inject === 'head' ||
347
- (options.inject !== 'body' && options.scriptLoading !== 'blocking') ? 'head' : 'body';
348
- // Group assets to `head` and `body` tag arrays
349
- const assetGroups = generateAssetGroups(assetTags, scriptTarget);
350
- // Allow third-party-plugin authors to reorder and change the assetTags once they are grouped
351
- return getHtmlWebpackPluginHooks(compilation).alterAssetTagGroups.promise({
352
- headTags: assetGroups.headTags,
353
- bodyTags: assetGroups.bodyTags,
354
- outputName: options.filename,
355
- publicPath: htmlPublicPath,
356
- plugin: plugin
357
- });
358
- });
359
-
360
- // Turn the compiled template into a nodejs function or into a nodejs string
361
- const templateEvaluationPromise = Promise.resolve()
362
- .then(() => {
363
- if ('error' in templateResult) {
364
- return options.showErrors ? prettyError(templateResult.error, compiler.context).toHtml() : 'ERROR';
365
- }
366
- // Allow to use a custom function / string instead
367
- if (options.templateContent !== false) {
368
- return options.templateContent;
369
- }
370
- // Once everything is compiled evaluate the html factory
371
- // and replace it with its content
372
- return ('compiledEntry' in templateResult)
373
- ? plugin.evaluateCompilationResult(templateResult.compiledEntry.content, htmlPublicPath, options.template)
374
- : Promise.reject(new Error('Child compilation contained no compiledEntry'));
375
- });
376
- const templateExectutionPromise = Promise.all([assetsPromise, assetTagGroupsPromise, templateEvaluationPromise])
377
- // Execute the template
378
- .then(([assetsHookResult, assetTags, compilationResult]) => typeof compilationResult !== 'function'
379
- ? compilationResult
380
- : executeTemplate(compilationResult, assetsHookResult.assets, { headTags: assetTags.headTags, bodyTags: assetTags.bodyTags }, compilation));
381
-
382
- const injectedHtmlPromise = Promise.all([assetTagGroupsPromise, templateExectutionPromise])
383
- // Allow plugins to change the html before assets are injected
384
- .then(([assetTags, html]) => {
385
- const pluginArgs = { html, headTags: assetTags.headTags, bodyTags: assetTags.bodyTags, plugin: plugin, outputName: options.filename };
386
- return getHtmlWebpackPluginHooks(compilation).afterTemplateExecution.promise(pluginArgs);
387
- })
388
- .then(({ html, headTags, bodyTags }) => {
389
- return postProcessHtml(html, assets, { headTags, bodyTags });
390
- });
391
-
392
- const emitHtmlPromise = injectedHtmlPromise
393
- // Allow plugins to change the html after assets are injected
394
- .then((html) => {
395
- const pluginArgs = { html, plugin: plugin, outputName: options.filename };
396
- return getHtmlWebpackPluginHooks(compilation).beforeEmit.promise(pluginArgs)
397
- .then(result => result.html);
398
- })
399
- .catch(err => {
400
- // In case anything went wrong the promise is resolved
401
- // with the error message and an error is logged
402
- compilation.errors.push(prettyError(err, compiler.context).toString());
403
- return options.showErrors ? prettyError(err, compiler.context).toHtml() : 'ERROR';
404
- })
405
- .then(html => {
406
- const filename = options.filename.replace(/\[templatehash([^\]]*)\]/g, require('util').deprecate(
407
- (match, options) => `[contenthash${options}]`,
408
- '[templatehash] is now [contenthash]')
409
- );
410
- const replacedFilename = replacePlaceholdersInFilename(filename, html, compilation);
411
- // Add the evaluated html code to the webpack assets
412
- compilation.emitAsset(replacedFilename.path, new webpack.sources.RawSource(html, false), replacedFilename.info);
413
- previousEmittedAssets.push({ name: replacedFilename.path, html });
414
- return replacedFilename.path;
415
- })
416
- .then((finalOutputName) => getHtmlWebpackPluginHooks(compilation).afterEmit.promise({
417
- outputName: finalOutputName,
418
- plugin: plugin
419
- }).catch(err => {
420
- console.error(err);
421
- return null;
422
- }).then(() => null));
423
-
424
- // Once all files are added to the webpack compilation
425
- // let the webpack compiler continue
426
- emitHtmlPromise.then(() => {
427
- callback();
428
- });
429
- });
430
- });
431
-
432
551
  /**
433
552
  * Generate the template parameters for the template function
434
- * @param {WebpackCompilation} compilation
435
- * @param {{
436
- publicPath: string,
437
- js: Array<string>,
438
- css: Array<string>,
439
- manifest?: string,
440
- favicon?: string
441
- }} assets
553
+ *
554
+ * @private
555
+ * @param {Compilation} compilation
556
+ * @param {AssetsInformationByGroups} assetsInformationByGroups
442
557
  * @param {{
443
558
  headTags: HtmlTagObject[],
444
559
  bodyTags: HtmlTagObject[]
445
560
  }} assetTags
446
561
  * @returns {Promise<{[key: any]: any}>}
447
562
  */
448
- function getTemplateParameters (compilation, assets, assetTags) {
449
- const templateParameters = options.templateParameters;
563
+ getTemplateParameters (compilation, assetsInformationByGroups, assetTags) {
564
+ const templateParameters = this.options.templateParameters;
565
+
450
566
  if (templateParameters === false) {
451
567
  return Promise.resolve({});
452
568
  }
569
+
453
570
  if (typeof templateParameters !== 'function' && typeof templateParameters !== 'object') {
454
571
  throw new Error('templateParameters has to be either a function or an object');
455
572
  }
573
+
456
574
  const templateParameterFunction = typeof templateParameters === 'function'
457
575
  // A custom function can overwrite the entire template parameter preparation
458
576
  ? templateParameters
459
577
  // If the template parameters is an object merge it with the default values
460
- : (compilation, assets, assetTags, options) => Object.assign({},
461
- templateParametersGenerator(compilation, assets, assetTags, options),
578
+ : (compilation, assetsInformationByGroups, assetTags, options) => Object.assign({},
579
+ templateParametersGenerator(compilation, assetsInformationByGroups, assetTags, options),
462
580
  templateParameters
463
581
  );
464
582
  const preparedAssetTags = {
465
- headTags: prepareAssetTagGroupForRendering(assetTags.headTags),
466
- bodyTags: prepareAssetTagGroupForRendering(assetTags.bodyTags)
583
+ headTags: this.prepareAssetTagGroupForRendering(assetTags.headTags),
584
+ bodyTags: this.prepareAssetTagGroupForRendering(assetTags.bodyTags)
467
585
  };
468
586
  return Promise
469
587
  .resolve()
470
- .then(() => templateParameterFunction(compilation, assets, preparedAssetTags, options));
588
+ .then(() => templateParameterFunction(compilation, assetsInformationByGroups, preparedAssetTags, this.options));
471
589
  }
472
590
 
473
591
  /**
474
592
  * This function renders the actual html by executing the template function
475
593
  *
594
+ * @private
476
595
  * @param {(templateParameters) => string | Promise<string>} templateFunction
477
- * @param {{
478
- publicPath: string,
479
- js: Array<string>,
480
- css: Array<string>,
481
- manifest?: string,
482
- favicon?: string
483
- }} assets
596
+ * @param {AssetsInformationByGroups} assetsInformationByGroups
484
597
  * @param {{
485
598
  headTags: HtmlTagObject[],
486
599
  bodyTags: HtmlTagObject[]
487
600
  }} assetTags
488
- * @param {WebpackCompilation} compilation
489
- *
601
+ * @param {Compilation} compilation
490
602
  * @returns Promise<string>
491
603
  */
492
- function executeTemplate (templateFunction, assets, assetTags, compilation) {
604
+ executeTemplate (templateFunction, assetsInformationByGroups, assetTags, compilation) {
493
605
  // Template processing
494
- const templateParamsPromise = getTemplateParameters(compilation, assets, assetTags);
606
+ const templateParamsPromise = this.getTemplateParameters(compilation, assetsInformationByGroups, assetTags);
607
+
495
608
  return templateParamsPromise.then((templateParams) => {
496
609
  try {
497
610
  // If html is a promise return the promise
@@ -507,299 +620,203 @@ function hookIntoCompiler (compiler, options, plugin) {
507
620
  /**
508
621
  * Html Post processing
509
622
  *
510
- * @param {any} html
511
- * The input html
512
- * @param {any} assets
513
- * @param {{
514
- headTags: HtmlTagObject[],
515
- bodyTags: HtmlTagObject[]
516
- }} assetTags
517
- * The asset tags to inject
518
- *
623
+ * @private
624
+ * @param {Compiler} compiler The compiler instance
625
+ * @param {any} originalHtml The input html
626
+ * @param {AssetsInformationByGroups} assetsInformationByGroups
627
+ * @param {{headTags: HtmlTagObject[], bodyTags: HtmlTagObject[]}} assetTags The asset tags to inject
519
628
  * @returns {Promise<string>}
520
629
  */
521
- function postProcessHtml (html, assets, assetTags) {
630
+ postProcessHtml (compiler, originalHtml, assetsInformationByGroups, assetTags) {
631
+ let html = originalHtml;
632
+
522
633
  if (typeof html !== 'string') {
523
634
  return Promise.reject(new Error('Expected html to be a string but got ' + JSON.stringify(html)));
524
635
  }
525
- const htmlAfterInjection = options.inject
526
- ? injectAssetsIntoHtml(html, assets, assetTags)
527
- : html;
528
- const htmlAfterMinification = minifyHtml(htmlAfterInjection);
529
- return Promise.resolve(htmlAfterMinification);
530
- }
531
-
532
- /*
533
- * Pushes the content of the given filename to the compilation assets
534
- * @param {string} filename
535
- * @param {WebpackCompilation} compilation
536
- *
537
- * @returns {string} file basename
538
- */
539
- function addFileToAssets (filename, compilation) {
540
- filename = path.resolve(compilation.compiler.context, filename);
541
- return fsReadFileAsync(filename)
542
- .then(source => new webpack.sources.RawSource(source, false))
543
- .catch(() => Promise.reject(new Error('HtmlWebpackPlugin: could not load file ' + filename)))
544
- .then(rawSource => {
545
- const basename = path.basename(filename);
546
- compilation.fileDependencies.add(filename);
547
- compilation.emitAsset(basename, rawSource);
548
- return basename;
549
- });
550
- }
551
636
 
552
- /**
553
- * Replace [contenthash] in filename
554
- *
555
- * @see https://survivejs.com/webpack/optimizing/adding-hashes-to-filenames/
556
- *
557
- * @param {string} filename
558
- * @param {string|Buffer} fileContent
559
- * @param {WebpackCompilation} compilation
560
- * @returns {{ path: string, info: {} }}
561
- */
562
- function replacePlaceholdersInFilename (filename, fileContent, compilation) {
563
- if (/\[\\*([\w:]+)\\*\]/i.test(filename) === false) {
564
- return { path: filename, info: {} };
565
- }
566
- const hash = compiler.webpack.util.createHash(compilation.outputOptions.hashFunction);
567
- hash.update(fileContent);
568
- if (compilation.outputOptions.hashSalt) {
569
- hash.update(compilation.outputOptions.hashSalt);
570
- }
571
- const contentHash = hash.digest(compilation.outputOptions.hashDigest).slice(0, compilation.outputOptions.hashDigestLength);
572
- return compilation.getPathWithInfo(
573
- filename,
574
- {
575
- contentHash,
576
- chunk: {
577
- hash: contentHash,
578
- contentHash
579
- }
580
- }
581
- );
582
- }
583
-
584
- /**
585
- * Helper to sort chunks
586
- * @param {string[]} entryNames
587
- * @param {string|((entryNameA: string, entryNameB: string) => number)} sortMode
588
- * @param {WebpackCompilation} compilation
589
- */
590
- function sortEntryChunks (entryNames, sortMode, compilation) {
591
- // Custom function
592
- if (typeof sortMode === 'function') {
593
- return entryNames.sort(sortMode);
594
- }
595
- // Check if the given sort mode is a valid chunkSorter sort mode
596
- if (typeof chunkSorter[sortMode] !== 'undefined') {
597
- return chunkSorter[sortMode](entryNames, compilation, options);
598
- }
599
- throw new Error('"' + sortMode + '" is not a valid chunk sort mode');
600
- }
601
-
602
- /**
603
- * Return all chunks from the compilation result which match the exclude and include filters
604
- * @param {any} chunks
605
- * @param {string[]|'all'} includedChunks
606
- * @param {string[]} excludedChunks
607
- */
608
- function filterChunks (chunks, includedChunks, excludedChunks) {
609
- return chunks.filter(chunkName => {
610
- // Skip if the chunks should be filtered and the given chunk was not added explicity
611
- if (Array.isArray(includedChunks) && includedChunks.indexOf(chunkName) === -1) {
612
- return false;
613
- }
614
- // Skip if the chunks should be filtered and the given chunk was excluded explicity
615
- if (Array.isArray(excludedChunks) && excludedChunks.indexOf(chunkName) !== -1) {
616
- return false;
617
- }
618
- // Add otherwise
619
- return true;
620
- });
621
- }
622
-
623
- /**
624
- * Generate the relative or absolute base url to reference images, css, and javascript files
625
- * from within the html file - the publicPath
626
- *
627
- * @param {WebpackCompilation} compilation
628
- * @param {string} childCompilationOutputName
629
- * @param {string | 'auto'} customPublicPath
630
- * @returns {string}
631
- */
632
- function getPublicPath (compilation, childCompilationOutputName, customPublicPath) {
633
- const compilationHash = compilation.hash;
634
-
635
- /**
636
- * @type {string} the configured public path to the asset root
637
- * if a path publicPath is set in the current webpack config use it otherwise
638
- * fallback to a relative path
639
- */
640
- const webpackPublicPath = compilation.getAssetPath(compilation.outputOptions.publicPath, { hash: compilationHash });
641
-
642
- // Webpack 5 introduced "auto" as default value
643
- const isPublicPathDefined = webpackPublicPath !== 'auto';
644
-
645
- let publicPath =
646
- // If the html-webpack-plugin options contain a custom public path uset it
647
- customPublicPath !== 'auto'
648
- ? customPublicPath
649
- : (isPublicPathDefined
650
- // If a hard coded public path exists use it
651
- ? webpackPublicPath
652
- // If no public path was set get a relative url path
653
- : path.relative(path.resolve(compilation.options.output.path, path.dirname(childCompilationOutputName)), compilation.options.output.path)
654
- .split(path.sep).join('/')
655
- );
656
-
657
- if (publicPath.length && publicPath.substr(-1, 1) !== '/') {
658
- publicPath += '/';
659
- }
660
-
661
- return publicPath;
662
- }
663
-
664
- /**
665
- * The htmlWebpackPluginAssets extracts the asset information of a webpack compilation
666
- * for all given entry names
667
- * @param {WebpackCompilation} compilation
668
- * @param {string[]} entryNames
669
- * @param {string | 'auto'} publicPath
670
- * @returns {{
671
- publicPath: string,
672
- js: Array<string>,
673
- css: Array<string>,
674
- manifest?: string,
675
- favicon?: string
676
- }}
677
- */
678
- function htmlWebpackPluginAssets (compilation, entryNames, publicPath) {
679
- const compilationHash = compilation.hash;
680
- /**
681
- * @type {{
682
- publicPath: string,
683
- js: Array<string>,
684
- css: Array<string>,
685
- manifest?: string,
686
- favicon?: string
687
- }}
688
- */
689
- const assets = {
690
- // The public path
691
- publicPath,
692
- // Will contain all js and mjs files
693
- js: [],
694
- // Will contain all css files
695
- css: [],
696
- // Will contain the html5 appcache manifest files if it exists
697
- manifest: Object.keys(compilation.assets).find(assetFile => path.extname(assetFile) === '.appcache'),
698
- // Favicon
699
- favicon: undefined
700
- };
701
-
702
- // Append a hash for cache busting
703
- if (options.hash && assets.manifest) {
704
- assets.manifest = appendHash(assets.manifest, compilationHash);
705
- }
706
-
707
- // Extract paths to .js, .mjs and .css files from the current compilation
708
- const entryPointPublicPathMap = {};
709
- const extensionRegexp = /\.(css|js|mjs)(\?|$)/;
710
- for (let i = 0; i < entryNames.length; i++) {
711
- const entryName = entryNames[i];
712
- /** entryPointUnfilteredFiles - also includes hot module update files */
713
- const entryPointUnfilteredFiles = compilation.entrypoints.get(entryName).getFiles();
714
-
715
- const entryPointFiles = entryPointUnfilteredFiles.filter((chunkFile) => {
716
- const asset = compilation.getAsset(chunkFile);
717
- if (!asset) {
718
- return true;
719
- }
720
- // Prevent hot-module files from being included:
721
- const assetMetaInformation = asset.info || {};
722
- return !(assetMetaInformation.hotModuleReplacement || assetMetaInformation.development);
723
- });
724
-
725
- // Prepend the publicPath and append the hash depending on the
726
- // webpack.output.publicPath and hashOptions
727
- // E.g. bundle.js -> /bundle.js?hash
728
- const entryPointPublicPaths = entryPointFiles
729
- .map(chunkFile => {
730
- const entryPointPublicPath = publicPath + urlencodePath(chunkFile);
731
- return options.hash
732
- ? appendHash(entryPointPublicPath, compilationHash)
733
- : entryPointPublicPath;
734
- });
637
+ if (this.options.inject) {
638
+ const htmlRegExp = /(<html[^>]*>)/i;
639
+ const headRegExp = /(<\/head\s*>)/i;
640
+ const bodyRegExp = /(<\/body\s*>)/i;
641
+ const metaViewportRegExp = /<meta[^>]+name=["']viewport["'][^>]*>/i;
642
+ const body = assetTags.bodyTags.map((assetTagObject) => htmlTagObjectToString(assetTagObject, this.options.xhtml));
643
+ const head = assetTags.headTags.filter((item) => {
644
+ if (item.tagName === 'meta' && item.attributes && item.attributes.name === 'viewport' && metaViewportRegExp.test(html)) {
645
+ return false;
646
+ }
735
647
 
736
- entryPointPublicPaths.forEach((entryPointPublicPath) => {
737
- const extMatch = extensionRegexp.exec(entryPointPublicPath);
738
- // Skip if the public path is not a .css, .mjs or .js file
739
- if (!extMatch) {
740
- return;
648
+ return true;
649
+ }).map((assetTagObject) => htmlTagObjectToString(assetTagObject, this.options.xhtml));
650
+
651
+ if (body.length) {
652
+ if (bodyRegExp.test(html)) {
653
+ // Append assets to body element
654
+ html = html.replace(bodyRegExp, match => body.join('') + match);
655
+ } else {
656
+ // Append scripts to the end of the file if no <body> element exists:
657
+ html += body.join('');
741
658
  }
742
- // Skip if this file is already known
743
- // (e.g. because of common chunk optimizations)
744
- if (entryPointPublicPathMap[entryPointPublicPath]) {
745
- return;
659
+ }
660
+
661
+ if (head.length) {
662
+ // Create a head tag if none exists
663
+ if (!headRegExp.test(html)) {
664
+ if (!htmlRegExp.test(html)) {
665
+ html = '<head></head>' + html;
666
+ } else {
667
+ html = html.replace(htmlRegExp, match => match + '<head></head>');
668
+ }
746
669
  }
747
- entryPointPublicPathMap[entryPointPublicPath] = true;
748
- // ext will contain .js or .css, because .mjs recognizes as .js
749
- const ext = extMatch[1] === 'mjs' ? 'js' : extMatch[1];
750
- assets[ext].push(entryPointPublicPath);
751
- });
670
+
671
+ // Append assets to head element
672
+ html = html.replace(headRegExp, match => head.join('') + match);
673
+ }
674
+
675
+ // Inject manifest into the opening html tag
676
+ if (assetsInformationByGroups.manifest) {
677
+ html = html.replace(/(<html[^>]*)(>)/i, (match, start, end) => {
678
+ // Append the manifest only if no manifest was specified
679
+ if (/\smanifest\s*=/.test(match)) {
680
+ return match;
681
+ }
682
+ return start + ' manifest="' + assetsInformationByGroups.manifest + '"' + end;
683
+ });
684
+ }
752
685
  }
753
- return assets;
686
+
687
+ // TODO avoid this logic and use https://github.com/webpack-contrib/html-minimizer-webpack-plugin under the hood in the next major version
688
+ // Check if webpack is running in production mode
689
+ // @see https://github.com/webpack/webpack/blob/3366421f1784c449f415cda5930a8e445086f688/lib/WebpackOptionsDefaulter.js#L12-L14
690
+ const isProductionLikeMode = compiler.options.mode === 'production' || !compiler.options.mode;
691
+ const needMinify = this.options.minify === true || typeof this.options.minify === 'object' || (this.options.minify === 'auto' && isProductionLikeMode);
692
+
693
+ if (!needMinify) {
694
+ return Promise.resolve(html);
695
+ }
696
+
697
+ const minifyOptions = typeof this.options.minify === 'object'
698
+ ? this.options.minify
699
+ : {
700
+ // https://www.npmjs.com/package/html-minifier-terser#options-quick-reference
701
+ collapseWhitespace: true,
702
+ keepClosingSlash: true,
703
+ removeComments: true,
704
+ removeRedundantAttributes: true,
705
+ removeScriptTypeAttributes: true,
706
+ removeStyleLinkTypeAttributes: true,
707
+ useShortDoctype: true
708
+ };
709
+
710
+ try {
711
+ html = require('html-minifier-terser').minify(html, minifyOptions);
712
+ } catch (e) {
713
+ const isParseError = String(e.message).indexOf('Parse Error') === 0;
714
+
715
+ if (isParseError) {
716
+ e.message = 'html-webpack-plugin could not minify the generated output.\n' +
717
+ 'In production mode the html minifcation is enabled by default.\n' +
718
+ 'If you are not generating a valid html output please disable it manually.\n' +
719
+ 'You can do so by adding the following setting to your HtmlWebpackPlugin config:\n|\n|' +
720
+ ' minify: false\n|\n' +
721
+ 'See https://github.com/jantimon/html-webpack-plugin#options for details.\n\n' +
722
+ 'For parser dedicated bugs please create an issue here:\n' +
723
+ 'https://danielruf.github.io/html-minifier-terser/' +
724
+ '\n' + e.message;
725
+ }
726
+
727
+ return Promise.reject(e);
728
+ }
729
+
730
+ return Promise.resolve(html);
731
+ }
732
+
733
+ /**
734
+ * Helper to return a sorted unique array of all asset files out of the asset object
735
+ * @private
736
+ */
737
+ getAssetFiles (assets) {
738
+ const files = _.uniq(Object.keys(assets).filter(assetType => assetType !== 'chunks' && assets[assetType]).reduce((files, assetType) => files.concat(assets[assetType]), []));
739
+ files.sort();
740
+ return files;
754
741
  }
755
742
 
756
743
  /**
757
- * Converts a favicon file from disk to a webpack resource
758
- * and returns the url to the resource
744
+ * Converts a favicon file from disk to a webpack resource and returns the url to the resource
759
745
  *
760
- * @param {string|false} faviconFilePath
761
- * @param {WebpackCompilation} compilation
746
+ * @private
747
+ * @param {Compiler} compiler
748
+ * @param {string|false} favicon
749
+ * @param {Compilation} compilation
762
750
  * @param {string} publicPath
751
+ * @param {PreviousEmittedAssets} previousEmittedAssets
763
752
  * @returns {Promise<string|undefined>}
764
753
  */
765
- function getFaviconPublicPath (faviconFilePath, compilation, publicPath) {
766
- if (!faviconFilePath) {
754
+ generateFavicon (compiler, favicon, compilation, publicPath, previousEmittedAssets) {
755
+ if (!favicon) {
767
756
  return Promise.resolve(undefined);
768
757
  }
769
- return addFileToAssets(faviconFilePath, compilation)
770
- .then((faviconName) => {
771
- const faviconPath = publicPath + faviconName;
772
- if (options.hash) {
773
- return appendHash(faviconPath, compilation.hash);
758
+
759
+ const filename = path.resolve(compilation.compiler.context, favicon);
760
+
761
+ return promisify(compilation.inputFileSystem.readFile)(filename)
762
+ .then((buf) => {
763
+ const source = new compiler.webpack.sources.RawSource(/** @type {string | Buffer} */ (buf), false);
764
+ const name = path.basename(filename);
765
+
766
+ compilation.fileDependencies.add(filename);
767
+ compilation.emitAsset(name, source);
768
+ previousEmittedAssets.push({ name, source });
769
+
770
+ const faviconPath = publicPath + name;
771
+
772
+ if (this.options.hash) {
773
+ return this.appendHash(faviconPath, /** @type {string} */ (compilation.hash));
774
774
  }
775
+
775
776
  return faviconPath;
776
- });
777
+ })
778
+ .catch(() => Promise.reject(new Error('HtmlWebpackPlugin: could not load file ' + filename)));
777
779
  }
778
780
 
779
781
  /**
780
782
  * Generate all tags script for the given file paths
783
+ *
784
+ * @private
781
785
  * @param {Array<string>} jsAssets
782
786
  * @returns {Array<HtmlTagObject>}
783
787
  */
784
- function generatedScriptTags (jsAssets) {
785
- return jsAssets.map(scriptAsset => ({
786
- tagName: 'script',
787
- voidTag: false,
788
- meta: { plugin: 'html-webpack-plugin' },
789
- attributes: {
790
- defer: options.scriptLoading === 'defer',
791
- type: options.scriptLoading === 'module' ? 'module' : undefined,
792
- src: scriptAsset
788
+ generatedScriptTags (jsAssets) {
789
+ // @ts-ignore
790
+ return jsAssets.map(src => {
791
+ const attributes = {};
792
+
793
+ if (this.options.scriptLoading === 'defer') {
794
+ attributes.defer = true;
795
+ } else if (this.options.scriptLoading === 'module') {
796
+ attributes.type = 'module';
797
+ } else if (this.options.scriptLoading === 'systemjs-module') {
798
+ attributes.type = 'systemjs-module';
793
799
  }
794
- }));
800
+
801
+ attributes.src = src;
802
+
803
+ return {
804
+ tagName: 'script',
805
+ voidTag: false,
806
+ meta: { plugin: 'html-webpack-plugin' },
807
+ attributes
808
+ };
809
+ });
795
810
  }
796
811
 
797
812
  /**
798
813
  * Generate all style tags for the given file paths
814
+ *
815
+ * @private
799
816
  * @param {Array<string>} cssAssets
800
817
  * @returns {Array<HtmlTagObject>}
801
818
  */
802
- function generateStyleTags (cssAssets) {
819
+ generateStyleTags (cssAssets) {
803
820
  return cssAssets.map(styleAsset => ({
804
821
  tagName: 'link',
805
822
  voidTag: true,
@@ -813,41 +830,34 @@ function hookIntoCompiler (compiler, options, plugin) {
813
830
 
814
831
  /**
815
832
  * Generate an optional base tag
816
- * @param { false
817
- | string
818
- | {[attributeName: string]: string} // attributes e.g. { href:"http://example.com/page.html" target:"_blank" }
819
- } baseOption
820
- * @returns {Array<HtmlTagObject>}
821
- */
822
- function generateBaseTag (baseOption) {
823
- if (baseOption === false) {
824
- return [];
825
- } else {
826
- return [{
827
- tagName: 'base',
828
- voidTag: true,
829
- meta: { plugin: 'html-webpack-plugin' },
830
- attributes: (typeof baseOption === 'string') ? {
831
- href: baseOption
832
- } : baseOption
833
- }];
834
- }
833
+ *
834
+ * @param {string | {[attributeName: string]: string}} base
835
+ * @returns {Array<HtmlTagObject>}
836
+ */
837
+ generateBaseTag (base) {
838
+ return [{
839
+ tagName: 'base',
840
+ voidTag: true,
841
+ meta: { plugin: 'html-webpack-plugin' },
842
+ // attributes e.g. { href:"http://example.com/page.html" target:"_blank" }
843
+ attributes: typeof base === 'string' ? {
844
+ href: base
845
+ } : base
846
+ }];
835
847
  }
836
848
 
837
849
  /**
838
850
  * Generate all meta tags for the given meta configuration
839
- * @param {false | {
840
- [name: string]:
841
- false // disabled
842
- | string // name content pair e.g. {viewport: 'width=device-width, initial-scale=1, shrink-to-fit=no'}`
843
- | {[attributeName: string]: string|boolean} // custom properties e.g. { name:"viewport" content:"width=500, initial-scale=1" }
844
- }} metaOptions
845
- * @returns {Array<HtmlTagObject>}
846
- */
847
- function generatedMetaTags (metaOptions) {
851
+ *
852
+ * @private
853
+ * @param {false | {[name: string]: false | string | {[attributeName: string]: string|boolean}}} metaOptions
854
+ * @returns {Array<HtmlTagObject>}
855
+ */
856
+ generatedMetaTags (metaOptions) {
848
857
  if (metaOptions === false) {
849
858
  return [];
850
859
  }
860
+
851
861
  // Make tags self-closing in case of xhtml
852
862
  // Turn { "viewport" : "width=500, initial-scale=1" } into
853
863
  // [{ name:"viewport" content:"width=500, initial-scale=1" }]
@@ -860,8 +870,9 @@ function hookIntoCompiler (compiler, options, plugin) {
860
870
  } : metaTagContent;
861
871
  })
862
872
  .filter((attribute) => attribute !== false);
863
- // Turn [{ name:"viewport" content:"width=500, initial-scale=1" }] into
864
- // the html-webpack-plugin tag structure
873
+
874
+ // Turn [{ name:"viewport" content:"width=500, initial-scale=1" }] into
875
+ // the html-webpack-plugin tag structure
865
876
  return metaTagAttributeObjects.map((metaTagAttributes) => {
866
877
  if (metaTagAttributes === false) {
867
878
  throw new Error('Invalid meta tag');
@@ -877,39 +888,38 @@ function hookIntoCompiler (compiler, options, plugin) {
877
888
 
878
889
  /**
879
890
  * Generate a favicon tag for the given file path
880
- * @param {string| undefined} faviconPath
891
+ *
892
+ * @private
893
+ * @param {string} favicon
881
894
  * @returns {Array<HtmlTagObject>}
882
895
  */
883
- function generateFaviconTags (faviconPath) {
884
- if (!faviconPath) {
885
- return [];
886
- }
896
+ generateFaviconTag (favicon) {
887
897
  return [{
888
898
  tagName: 'link',
889
899
  voidTag: true,
890
900
  meta: { plugin: 'html-webpack-plugin' },
891
901
  attributes: {
892
902
  rel: 'icon',
893
- href: faviconPath
903
+ href: favicon
894
904
  }
895
905
  }];
896
906
  }
897
907
 
898
908
  /**
899
- * Group assets to head and bottom tags
909
+ * Group assets to head and body tags
900
910
  *
901
911
  * @param {{
902
912
  scripts: Array<HtmlTagObject>;
903
913
  styles: Array<HtmlTagObject>;
904
914
  meta: Array<HtmlTagObject>;
905
915
  }} assetTags
906
- * @param {"body" | "head"} scriptTarget
907
- * @returns {{
916
+ * @param {"body" | "head"} scriptTarget
917
+ * @returns {{
908
918
  headTags: Array<HtmlTagObject>;
909
919
  bodyTags: Array<HtmlTagObject>;
910
920
  }}
911
- */
912
- function generateAssetGroups (assetTags, scriptTarget) {
921
+ */
922
+ groupAssetsByElements (assetTags, scriptTarget) {
913
923
  /** @type {{ headTags: Array<HtmlTagObject>; bodyTags: Array<HtmlTagObject>; }} */
914
924
  const result = {
915
925
  headTags: [
@@ -918,214 +928,242 @@ function hookIntoCompiler (compiler, options, plugin) {
918
928
  ],
919
929
  bodyTags: []
920
930
  };
931
+
921
932
  // Add script tags to head or body depending on
922
933
  // the htmlPluginOptions
923
934
  if (scriptTarget === 'body') {
924
935
  result.bodyTags.push(...assetTags.scripts);
925
936
  } else {
926
937
  // If script loading is blocking add the scripts to the end of the head
927
- // If script loading is non-blocking add the scripts infront of the css files
928
- const insertPosition = options.scriptLoading === 'blocking' ? result.headTags.length : assetTags.meta.length;
938
+ // If script loading is non-blocking add the scripts in front of the css files
939
+ const insertPosition = this.options.scriptLoading === 'blocking' ? result.headTags.length : assetTags.meta.length;
940
+
929
941
  result.headTags.splice(insertPosition, 0, ...assetTags.scripts);
930
942
  }
931
- return result;
932
- }
933
943
 
934
- /**
935
- * Add toString methods for easier rendering
936
- * inside the template
937
- *
938
- * @param {Array<HtmlTagObject>} assetTagGroup
939
- * @returns {Array<HtmlTagObject>}
940
- */
941
- function prepareAssetTagGroupForRendering (assetTagGroup) {
942
- const xhtml = options.xhtml;
943
- return HtmlTagArray.from(assetTagGroup.map((assetTag) => {
944
- const copiedAssetTag = Object.assign({}, assetTag);
945
- copiedAssetTag.toString = function () {
946
- return htmlTagObjectToString(this, xhtml);
947
- };
948
- return copiedAssetTag;
949
- }));
944
+ return result;
950
945
  }
951
946
 
952
947
  /**
953
- * Injects the assets into the given html string
948
+ * Replace [contenthash] in filename
954
949
  *
955
- * @param {string} html
956
- * The input html
957
- * @param {any} assets
958
- * @param {{
959
- headTags: HtmlTagObject[],
960
- bodyTags: HtmlTagObject[]
961
- }} assetTags
962
- * The asset tags to inject
950
+ * @see https://survivejs.com/webpack/optimizing/adding-hashes-to-filenames/
963
951
  *
964
- * @returns {string}
952
+ * @private
953
+ * @param {Compiler} compiler
954
+ * @param {string} filename
955
+ * @param {string|Buffer} fileContent
956
+ * @param {Compilation} compilation
957
+ * @returns {{ path: string, info: {} }}
965
958
  */
966
- function injectAssetsIntoHtml (html, assets, assetTags) {
967
- const htmlRegExp = /(<html[^>]*>)/i;
968
- const headRegExp = /(<\/head\s*>)/i;
969
- const bodyRegExp = /(<\/body\s*>)/i;
970
- const metaViewportRegExp = /<meta[^>]+name=["']viewport["'][^>]*>/i;
971
- const body = assetTags.bodyTags.map((assetTagObject) => htmlTagObjectToString(assetTagObject, options.xhtml));
972
- const head = assetTags.headTags.filter((item) => {
973
- if (item.tagName === 'meta' && item.attributes && item.attributes.name === 'viewport' && metaViewportRegExp.test(html)) {
974
- return false;
975
- }
976
-
977
- return true;
978
- }).map((assetTagObject) => htmlTagObjectToString(assetTagObject, options.xhtml));
979
-
980
- if (body.length) {
981
- if (bodyRegExp.test(html)) {
982
- // Append assets to body element
983
- html = html.replace(bodyRegExp, match => body.join('') + match);
984
- } else {
985
- // Append scripts to the end of the file if no <body> element exists:
986
- html += body.join('');
987
- }
959
+ replacePlaceholdersInFilename (compiler, filename, fileContent, compilation) {
960
+ if (/\[\\*([\w:]+)\\*\]/i.test(filename) === false) {
961
+ return { path: filename, info: {} };
988
962
  }
989
963
 
990
- if (head.length) {
991
- // Create a head tag if none exists
992
- if (!headRegExp.test(html)) {
993
- if (!htmlRegExp.test(html)) {
994
- html = '<head></head>' + html;
995
- } else {
996
- html = html.replace(htmlRegExp, match => match + '<head></head>');
997
- }
998
- }
999
-
1000
- // Append assets to head element
1001
- html = html.replace(headRegExp, match => head.join('') + match);
1002
- }
964
+ const hash = compiler.webpack.util.createHash(compilation.outputOptions.hashFunction);
1003
965
 
1004
- // Inject manifest into the opening html tag
1005
- if (assets.manifest) {
1006
- html = html.replace(/(<html[^>]*)(>)/i, (match, start, end) => {
1007
- // Append the manifest only if no manifest was specified
1008
- if (/\smanifest\s*=/.test(match)) {
1009
- return match;
1010
- }
1011
- return start + ' manifest="' + assets.manifest + '"' + end;
1012
- });
1013
- }
1014
- return html;
1015
- }
966
+ hash.update(fileContent);
1016
967
 
1017
- /**
1018
- * Appends a cache busting hash to the query string of the url
1019
- * E.g. http://localhost:8080/ -> http://localhost:8080/?50c9096ba6183fd728eeb065a26ec175
1020
- * @param {string} url
1021
- * @param {string} hash
1022
- */
1023
- function appendHash (url, hash) {
1024
- if (!url) {
1025
- return url;
968
+ if (compilation.outputOptions.hashSalt) {
969
+ hash.update(compilation.outputOptions.hashSalt);
1026
970
  }
1027
- return url + (url.indexOf('?') === -1 ? '?' : '&') + hash;
1028
- }
1029
971
 
1030
- /**
1031
- * Encode each path component using `encodeURIComponent` as files can contain characters
1032
- * which needs special encoding in URLs like `+ `.
1033
- *
1034
- * Valid filesystem characters which need to be encoded for urls:
1035
- *
1036
- * # pound, % percent, & ampersand, { left curly bracket, } right curly bracket,
1037
- * \ back slash, < left angle bracket, > right angle bracket, * asterisk, ? question mark,
1038
- * blank spaces, $ dollar sign, ! exclamation point, ' single quotes, " double quotes,
1039
- * : colon, @ at sign, + plus sign, ` backtick, | pipe, = equal sign
1040
- *
1041
- * However the query string must not be encoded:
1042
- *
1043
- * fo:demonstration-path/very fancy+name.js?path=/home?value=abc&value=def#zzz
1044
- * ^ ^ ^ ^ ^ ^ ^ ^^ ^ ^ ^ ^ ^
1045
- * | | | | | | | || | | | | |
1046
- * encoded | | encoded | | || | | | | |
1047
- * ignored ignored ignored ignored ignored
1048
- *
1049
- * @param {string} filePath
1050
- */
1051
- function urlencodePath (filePath) {
1052
- // People use the filepath in quite unexpected ways.
1053
- // Try to extract the first querystring of the url:
1054
- //
1055
- // some+path/demo.html?value=abc?def
1056
- //
1057
- const queryStringStart = filePath.indexOf('?');
1058
- const urlPath = queryStringStart === -1 ? filePath : filePath.substr(0, queryStringStart);
1059
- const queryString = filePath.substr(urlPath.length);
1060
- // Encode all parts except '/' which are not part of the querystring:
1061
- const encodedUrlPath = urlPath.split('/').map(encodeURIComponent).join('/');
1062
- return encodedUrlPath + queryString;
1063
- }
972
+ const contentHash = hash.digest(compilation.outputOptions.hashDigest).slice(0, compilation.outputOptions.hashDigestLength);
1064
973
 
1065
- /**
1066
- * Helper to return the absolute template path with a fallback loader
1067
- * @param {string} template
1068
- * The path to the template e.g. './index.html'
1069
- * @param {string} context
1070
- * The webpack base resolution path for relative paths e.g. process.cwd()
1071
- */
1072
- function getFullTemplatePath (template, context) {
1073
- if (template === 'auto') {
1074
- template = path.resolve(context, 'src/index.ejs');
1075
- if (!fs.existsSync(template)) {
1076
- template = path.join(__dirname, 'default_index.ejs');
974
+ return compilation.getPathWithInfo(
975
+ filename,
976
+ {
977
+ contentHash,
978
+ chunk: {
979
+ hash: contentHash,
980
+ contentHash
981
+ }
1077
982
  }
1078
- }
1079
- // If the template doesn't use a loader use the lodash template loader
1080
- if (template.indexOf('!') === -1) {
1081
- template = require.resolve('./lib/loader.js') + '!' + path.resolve(context, template);
1082
- }
1083
- // Resolve template path
1084
- return template.replace(
1085
- /([!])([^/\\][^!?]+|[^/\\!?])($|\?[^!?\n]+$)/,
1086
- (match, prefix, filepath, postfix) => prefix + path.resolve(filepath) + postfix);
983
+ );
1087
984
  }
1088
985
 
1089
986
  /**
1090
- * Minify the given string using html-minifier-terser
1091
- *
1092
- * As this is a breaking change to html-webpack-plugin 3.x
1093
- * provide an extended error message to explain how to get back
1094
- * to the old behaviour
987
+ * Function to generate HTML file.
1095
988
  *
1096
- * @param {string} html
989
+ * @private
990
+ * @param {Compiler} compiler
991
+ * @param {Compilation} compilation
992
+ * @param {string} outputName
993
+ * @param {CachedChildCompilation} childCompilerPlugin
994
+ * @param {PreviousEmittedAssets} previousEmittedAssets
995
+ * @param {{ value: string | undefined }} assetJson
996
+ * @param {(err?: Error) => void} callback
1097
997
  */
1098
- function minifyHtml (html) {
1099
- if (typeof options.minify !== 'object') {
1100
- return html;
998
+ generateHTML (
999
+ compiler,
1000
+ compilation,
1001
+ outputName,
1002
+ childCompilerPlugin,
1003
+ previousEmittedAssets,
1004
+ assetJson,
1005
+ callback
1006
+ ) {
1007
+ // Get all entry point names for this html file
1008
+ const entryNames = Array.from(compilation.entrypoints.keys());
1009
+ const filteredEntryNames = this.filterEntryChunks(entryNames, this.options.chunks, this.options.excludeChunks);
1010
+ const sortedEntryNames = this.sortEntryChunks(filteredEntryNames, this.options.chunksSortMode, compilation);
1011
+ const templateResult = this.options.templateContent
1012
+ ? { mainCompilationHash: compilation.hash }
1013
+ : childCompilerPlugin.getCompilationEntryResult(this.options.template);
1014
+
1015
+ if ('error' in templateResult) {
1016
+ compilation.errors.push(prettyError(templateResult.error, compiler.context).toString());
1101
1017
  }
1102
- try {
1103
- return require('html-minifier-terser').minify(html, options.minify);
1104
- } catch (e) {
1105
- const isParseError = String(e.message).indexOf('Parse Error') === 0;
1106
- if (isParseError) {
1107
- e.message = 'html-webpack-plugin could not minify the generated output.\n' +
1108
- 'In production mode the html minifcation is enabled by default.\n' +
1109
- 'If you are not generating a valid html output please disable it manually.\n' +
1110
- 'You can do so by adding the following setting to your HtmlWebpackPlugin config:\n|\n|' +
1111
- ' minify: false\n|\n' +
1112
- 'See https://github.com/jantimon/html-webpack-plugin#options for details.\n\n' +
1113
- 'For parser dedicated bugs please create an issue here:\n' +
1114
- 'https://danielruf.github.io/html-minifier-terser/' +
1115
- '\n' + e.message;
1116
- }
1117
- throw e;
1018
+
1019
+ // If the child compilation was not executed during a previous main compile run
1020
+ // it is a cached result
1021
+ const isCompilationCached = templateResult.mainCompilationHash !== compilation.hash;
1022
+ /** Generated file paths from the entry point names */
1023
+ const assetsInformationByGroups = this.getAssetsInformationByGroups(compilation, outputName, sortedEntryNames);
1024
+ // If the template and the assets did not change we don't have to emit the html
1025
+ const newAssetJson = JSON.stringify(this.getAssetFiles(assetsInformationByGroups));
1026
+
1027
+ if (isCompilationCached && this.options.cache && assetJson.value === newAssetJson) {
1028
+ previousEmittedAssets.forEach(({ name, source, info }) => {
1029
+ compilation.emitAsset(name, source, info);
1030
+ });
1031
+ return callback();
1032
+ } else {
1033
+ previousEmittedAssets.length = 0;
1034
+ assetJson.value = newAssetJson;
1118
1035
  }
1119
- }
1120
1036
 
1121
- /**
1122
- * Helper to return a sorted unique array of all asset files out of the
1123
- * asset object
1124
- */
1125
- function getAssetFiles (assets) {
1126
- const files = _.uniq(Object.keys(assets).filter(assetType => assetType !== 'chunks' && assets[assetType]).reduce((files, assetType) => files.concat(assets[assetType]), []));
1127
- files.sort();
1128
- return files;
1037
+ // The html-webpack plugin uses a object representation for the html-tags which will be injected
1038
+ // to allow altering them more easily
1039
+ // Just before they are converted a third-party-plugin author might change the order and content
1040
+ const assetsPromise = this.generateFavicon(compiler, this.options.favicon, compilation, assetsInformationByGroups.publicPath, previousEmittedAssets)
1041
+ .then((faviconPath) => {
1042
+ assetsInformationByGroups.favicon = faviconPath;
1043
+ return getHtmlWebpackPluginHooks(compilation).beforeAssetTagGeneration.promise({
1044
+ assets: assetsInformationByGroups,
1045
+ outputName,
1046
+ plugin: this
1047
+ });
1048
+ });
1049
+
1050
+ // Turn the js and css paths into grouped HtmlTagObjects
1051
+ const assetTagGroupsPromise = assetsPromise
1052
+ // And allow third-party-plugin authors to reorder and change the assetTags before they are grouped
1053
+ .then(({ assets }) => getHtmlWebpackPluginHooks(compilation).alterAssetTags.promise({
1054
+ assetTags: {
1055
+ scripts: this.generatedScriptTags(assets.js),
1056
+ styles: this.generateStyleTags(assets.css),
1057
+ meta: [
1058
+ ...(this.options.base !== false ? this.generateBaseTag(this.options.base) : []),
1059
+ ...this.generatedMetaTags(this.options.meta),
1060
+ ...(assets.favicon ? this.generateFaviconTag(assets.favicon) : [])
1061
+ ]
1062
+ },
1063
+ outputName,
1064
+ publicPath: assetsInformationByGroups.publicPath,
1065
+ plugin: this
1066
+ }))
1067
+ .then(({ assetTags }) => {
1068
+ // Inject scripts to body unless it set explicitly to head
1069
+ const scriptTarget = this.options.inject === 'head' ||
1070
+ (this.options.inject !== 'body' && this.options.scriptLoading !== 'blocking') ? 'head' : 'body';
1071
+ // Group assets to `head` and `body` tag arrays
1072
+ const assetGroups = this.groupAssetsByElements(assetTags, scriptTarget);
1073
+ // Allow third-party-plugin authors to reorder and change the assetTags once they are grouped
1074
+ return getHtmlWebpackPluginHooks(compilation).alterAssetTagGroups.promise({
1075
+ headTags: assetGroups.headTags,
1076
+ bodyTags: assetGroups.bodyTags,
1077
+ outputName,
1078
+ publicPath: assetsInformationByGroups.publicPath,
1079
+ plugin: this
1080
+ });
1081
+ });
1082
+
1083
+ // Turn the compiled template into a nodejs function or into a nodejs string
1084
+ const templateEvaluationPromise = Promise.resolve()
1085
+ .then(() => {
1086
+ if ('error' in templateResult) {
1087
+ return this.options.showErrors ? prettyError(templateResult.error, compiler.context).toHtml() : 'ERROR';
1088
+ }
1089
+
1090
+ // Allow to use a custom function / string instead
1091
+ if (this.options.templateContent !== false) {
1092
+ return this.options.templateContent;
1093
+ }
1094
+
1095
+ // Once everything is compiled evaluate the html factory and replace it with its content
1096
+ if ('compiledEntry' in templateResult) {
1097
+ const compiledEntry = templateResult.compiledEntry;
1098
+ const assets = compiledEntry.assets;
1099
+
1100
+ // Store assets from child compiler to reemit them later
1101
+ for (const name in assets) {
1102
+ previousEmittedAssets.push({ name, source: assets[name].source, info: assets[name].info });
1103
+ }
1104
+
1105
+ return this.evaluateCompilationResult(compiledEntry.content, assetsInformationByGroups.publicPath, this.options.template);
1106
+ }
1107
+
1108
+ return Promise.reject(new Error('Child compilation contained no compiledEntry'));
1109
+ });
1110
+ const templateExectutionPromise = Promise.all([assetsPromise, assetTagGroupsPromise, templateEvaluationPromise])
1111
+ // Execute the template
1112
+ .then(([assetsHookResult, assetTags, compilationResult]) => typeof compilationResult !== 'function'
1113
+ ? compilationResult
1114
+ : this.executeTemplate(compilationResult, assetsHookResult.assets, { headTags: assetTags.headTags, bodyTags: assetTags.bodyTags }, compilation));
1115
+
1116
+ const injectedHtmlPromise = Promise.all([assetTagGroupsPromise, templateExectutionPromise])
1117
+ // Allow plugins to change the html before assets are injected
1118
+ .then(([assetTags, html]) => {
1119
+ const pluginArgs = { html, headTags: assetTags.headTags, bodyTags: assetTags.bodyTags, plugin: this, outputName };
1120
+ return getHtmlWebpackPluginHooks(compilation).afterTemplateExecution.promise(pluginArgs);
1121
+ })
1122
+ .then(({ html, headTags, bodyTags }) => {
1123
+ return this.postProcessHtml(compiler, html, assetsInformationByGroups, { headTags, bodyTags });
1124
+ });
1125
+
1126
+ const emitHtmlPromise = injectedHtmlPromise
1127
+ // Allow plugins to change the html after assets are injected
1128
+ .then((html) => {
1129
+ const pluginArgs = { html, plugin: this, outputName };
1130
+ return getHtmlWebpackPluginHooks(compilation).beforeEmit.promise(pluginArgs)
1131
+ .then(result => result.html);
1132
+ })
1133
+ .catch(err => {
1134
+ // In case anything went wrong the promise is resolved
1135
+ // with the error message and an error is logged
1136
+ compilation.errors.push(prettyError(err, compiler.context).toString());
1137
+ return this.options.showErrors ? prettyError(err, compiler.context).toHtml() : 'ERROR';
1138
+ })
1139
+ .then(html => {
1140
+ const filename = outputName.replace(/\[templatehash([^\]]*)\]/g, require('util').deprecate(
1141
+ (match, options) => `[contenthash${options}]`,
1142
+ '[templatehash] is now [contenthash]')
1143
+ );
1144
+ const replacedFilename = this.replacePlaceholdersInFilename(compiler, filename, html, compilation);
1145
+ const source = new compiler.webpack.sources.RawSource(html, false);
1146
+
1147
+ // Add the evaluated html code to the webpack assets
1148
+ compilation.emitAsset(replacedFilename.path, source, replacedFilename.info);
1149
+ previousEmittedAssets.push({ name: replacedFilename.path, source });
1150
+
1151
+ return replacedFilename.path;
1152
+ })
1153
+ .then((finalOutputName) => getHtmlWebpackPluginHooks(compilation).afterEmit.promise({
1154
+ outputName: finalOutputName,
1155
+ plugin: this
1156
+ }).catch(err => {
1157
+ /** @type {Logger} */
1158
+ (this.logger).error(err);
1159
+ return null;
1160
+ }).then(() => null));
1161
+
1162
+ // Once all files are added to the webpack compilation
1163
+ // let the webpack compiler continue
1164
+ emitHtmlPromise.then(() => {
1165
+ callback();
1166
+ });
1129
1167
  }
1130
1168
  }
1131
1169
 
@@ -1134,14 +1172,8 @@ function hookIntoCompiler (compiler, options, plugin) {
1134
1172
  * Generate the template parameters
1135
1173
  *
1136
1174
  * Generate the template parameters for the template function
1137
- * @param {WebpackCompilation} compilation
1138
- * @param {{
1139
- publicPath: string,
1140
- js: Array<string>,
1141
- css: Array<string>,
1142
- manifest?: string,
1143
- favicon?: string
1144
- }} assets
1175
+ * @param {Compilation} compilation
1176
+ * @param {AssetsInformationByGroups} assets
1145
1177
  * @param {{
1146
1178
  headTags: HtmlTagObject[],
1147
1179
  bodyTags: HtmlTagObject[]