html-webpack-plugin 5.5.3 → 5.5.4

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') {
82
+ /** @type {Logger} */
83
+ (this.logger).error('The "scriptLoading" option need to be set to "defer", "blocking" or "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,201 @@ 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';
793
797
  }
794
- }));
798
+
799
+ attributes.src = src;
800
+
801
+ return {
802
+ tagName: 'script',
803
+ voidTag: false,
804
+ meta: { plugin: 'html-webpack-plugin' },
805
+ attributes
806
+ };
807
+ });
795
808
  }
796
809
 
797
810
  /**
798
811
  * Generate all style tags for the given file paths
812
+ *
813
+ * @private
799
814
  * @param {Array<string>} cssAssets
800
815
  * @returns {Array<HtmlTagObject>}
801
816
  */
802
- function generateStyleTags (cssAssets) {
817
+ generateStyleTags (cssAssets) {
803
818
  return cssAssets.map(styleAsset => ({
804
819
  tagName: 'link',
805
820
  voidTag: true,
@@ -813,41 +828,34 @@ function hookIntoCompiler (compiler, options, plugin) {
813
828
 
814
829
  /**
815
830
  * 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
- }
831
+ *
832
+ * @param {string | {[attributeName: string]: string}} base
833
+ * @returns {Array<HtmlTagObject>}
834
+ */
835
+ generateBaseTag (base) {
836
+ return [{
837
+ tagName: 'base',
838
+ voidTag: true,
839
+ meta: { plugin: 'html-webpack-plugin' },
840
+ // attributes e.g. { href:"http://example.com/page.html" target:"_blank" }
841
+ attributes: typeof base === 'string' ? {
842
+ href: base
843
+ } : base
844
+ }];
835
845
  }
836
846
 
837
847
  /**
838
848
  * 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) {
849
+ *
850
+ * @private
851
+ * @param {false | {[name: string]: false | string | {[attributeName: string]: string|boolean}}} metaOptions
852
+ * @returns {Array<HtmlTagObject>}
853
+ */
854
+ generatedMetaTags (metaOptions) {
848
855
  if (metaOptions === false) {
849
856
  return [];
850
857
  }
858
+
851
859
  // Make tags self-closing in case of xhtml
852
860
  // Turn { "viewport" : "width=500, initial-scale=1" } into
853
861
  // [{ name:"viewport" content:"width=500, initial-scale=1" }]
@@ -860,8 +868,9 @@ function hookIntoCompiler (compiler, options, plugin) {
860
868
  } : metaTagContent;
861
869
  })
862
870
  .filter((attribute) => attribute !== false);
863
- // Turn [{ name:"viewport" content:"width=500, initial-scale=1" }] into
864
- // the html-webpack-plugin tag structure
871
+
872
+ // Turn [{ name:"viewport" content:"width=500, initial-scale=1" }] into
873
+ // the html-webpack-plugin tag structure
865
874
  return metaTagAttributeObjects.map((metaTagAttributes) => {
866
875
  if (metaTagAttributes === false) {
867
876
  throw new Error('Invalid meta tag');
@@ -877,39 +886,38 @@ function hookIntoCompiler (compiler, options, plugin) {
877
886
 
878
887
  /**
879
888
  * Generate a favicon tag for the given file path
880
- * @param {string| undefined} faviconPath
889
+ *
890
+ * @private
891
+ * @param {string} favicon
881
892
  * @returns {Array<HtmlTagObject>}
882
893
  */
883
- function generateFaviconTags (faviconPath) {
884
- if (!faviconPath) {
885
- return [];
886
- }
894
+ generateFaviconTag (favicon) {
887
895
  return [{
888
896
  tagName: 'link',
889
897
  voidTag: true,
890
898
  meta: { plugin: 'html-webpack-plugin' },
891
899
  attributes: {
892
900
  rel: 'icon',
893
- href: faviconPath
901
+ href: favicon
894
902
  }
895
903
  }];
896
904
  }
897
905
 
898
906
  /**
899
- * Group assets to head and bottom tags
907
+ * Group assets to head and body tags
900
908
  *
901
909
  * @param {{
902
910
  scripts: Array<HtmlTagObject>;
903
911
  styles: Array<HtmlTagObject>;
904
912
  meta: Array<HtmlTagObject>;
905
913
  }} assetTags
906
- * @param {"body" | "head"} scriptTarget
907
- * @returns {{
914
+ * @param {"body" | "head"} scriptTarget
915
+ * @returns {{
908
916
  headTags: Array<HtmlTagObject>;
909
917
  bodyTags: Array<HtmlTagObject>;
910
918
  }}
911
- */
912
- function generateAssetGroups (assetTags, scriptTarget) {
919
+ */
920
+ groupAssetsByElements (assetTags, scriptTarget) {
913
921
  /** @type {{ headTags: Array<HtmlTagObject>; bodyTags: Array<HtmlTagObject>; }} */
914
922
  const result = {
915
923
  headTags: [
@@ -918,214 +926,242 @@ function hookIntoCompiler (compiler, options, plugin) {
918
926
  ],
919
927
  bodyTags: []
920
928
  };
929
+
921
930
  // Add script tags to head or body depending on
922
931
  // the htmlPluginOptions
923
932
  if (scriptTarget === 'body') {
924
933
  result.bodyTags.push(...assetTags.scripts);
925
934
  } else {
926
935
  // 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;
936
+ // If script loading is non-blocking add the scripts in front of the css files
937
+ const insertPosition = this.options.scriptLoading === 'blocking' ? result.headTags.length : assetTags.meta.length;
938
+
929
939
  result.headTags.splice(insertPosition, 0, ...assetTags.scripts);
930
940
  }
931
- return result;
932
- }
933
941
 
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
- }));
942
+ return result;
950
943
  }
951
944
 
952
945
  /**
953
- * Injects the assets into the given html string
946
+ * Replace [contenthash] in filename
954
947
  *
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
948
+ * @see https://survivejs.com/webpack/optimizing/adding-hashes-to-filenames/
963
949
  *
964
- * @returns {string}
950
+ * @private
951
+ * @param {Compiler} compiler
952
+ * @param {string} filename
953
+ * @param {string|Buffer} fileContent
954
+ * @param {Compilation} compilation
955
+ * @returns {{ path: string, info: {} }}
965
956
  */
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
- }
957
+ replacePlaceholdersInFilename (compiler, filename, fileContent, compilation) {
958
+ if (/\[\\*([\w:]+)\\*\]/i.test(filename) === false) {
959
+ return { path: filename, info: {} };
988
960
  }
989
961
 
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
- }
962
+ const hash = compiler.webpack.util.createHash(compilation.outputOptions.hashFunction);
1003
963
 
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
- }
964
+ hash.update(fileContent);
1016
965
 
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;
966
+ if (compilation.outputOptions.hashSalt) {
967
+ hash.update(compilation.outputOptions.hashSalt);
1026
968
  }
1027
- return url + (url.indexOf('?') === -1 ? '?' : '&') + hash;
1028
- }
1029
969
 
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
- }
970
+ const contentHash = hash.digest(compilation.outputOptions.hashDigest).slice(0, compilation.outputOptions.hashDigestLength);
1064
971
 
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');
972
+ return compilation.getPathWithInfo(
973
+ filename,
974
+ {
975
+ contentHash,
976
+ chunk: {
977
+ hash: contentHash,
978
+ contentHash
979
+ }
1077
980
  }
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);
981
+ );
1087
982
  }
1088
983
 
1089
984
  /**
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
985
+ * Function to generate HTML file.
1095
986
  *
1096
- * @param {string} html
987
+ * @private
988
+ * @param {Compiler} compiler
989
+ * @param {Compilation} compilation
990
+ * @param {string} outputName
991
+ * @param {CachedChildCompilation} childCompilerPlugin
992
+ * @param {PreviousEmittedAssets} previousEmittedAssets
993
+ * @param {{ value: string | undefined }} assetJson
994
+ * @param {(err?: Error) => void} callback
1097
995
  */
1098
- function minifyHtml (html) {
1099
- if (typeof options.minify !== 'object') {
1100
- return html;
996
+ generateHTML (
997
+ compiler,
998
+ compilation,
999
+ outputName,
1000
+ childCompilerPlugin,
1001
+ previousEmittedAssets,
1002
+ assetJson,
1003
+ callback
1004
+ ) {
1005
+ // Get all entry point names for this html file
1006
+ const entryNames = Array.from(compilation.entrypoints.keys());
1007
+ const filteredEntryNames = this.filterEntryChunks(entryNames, this.options.chunks, this.options.excludeChunks);
1008
+ const sortedEntryNames = this.sortEntryChunks(filteredEntryNames, this.options.chunksSortMode, compilation);
1009
+ const templateResult = this.options.templateContent
1010
+ ? { mainCompilationHash: compilation.hash }
1011
+ : childCompilerPlugin.getCompilationEntryResult(this.options.template);
1012
+
1013
+ if ('error' in templateResult) {
1014
+ compilation.errors.push(prettyError(templateResult.error, compiler.context).toString());
1101
1015
  }
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;
1016
+
1017
+ // If the child compilation was not executed during a previous main compile run
1018
+ // it is a cached result
1019
+ const isCompilationCached = templateResult.mainCompilationHash !== compilation.hash;
1020
+ /** Generated file paths from the entry point names */
1021
+ const assetsInformationByGroups = this.getAssetsInformationByGroups(compilation, outputName, sortedEntryNames);
1022
+ // If the template and the assets did not change we don't have to emit the html
1023
+ const newAssetJson = JSON.stringify(this.getAssetFiles(assetsInformationByGroups));
1024
+
1025
+ if (isCompilationCached && this.options.cache && assetJson.value === newAssetJson) {
1026
+ previousEmittedAssets.forEach(({ name, source, info }) => {
1027
+ compilation.emitAsset(name, source, info);
1028
+ });
1029
+ return callback();
1030
+ } else {
1031
+ previousEmittedAssets.length = 0;
1032
+ assetJson.value = newAssetJson;
1118
1033
  }
1119
- }
1120
1034
 
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;
1035
+ // The html-webpack plugin uses a object representation for the html-tags which will be injected
1036
+ // to allow altering them more easily
1037
+ // Just before they are converted a third-party-plugin author might change the order and content
1038
+ const assetsPromise = this.generateFavicon(compiler, this.options.favicon, compilation, assetsInformationByGroups.publicPath, previousEmittedAssets)
1039
+ .then((faviconPath) => {
1040
+ assetsInformationByGroups.favicon = faviconPath;
1041
+ return getHtmlWebpackPluginHooks(compilation).beforeAssetTagGeneration.promise({
1042
+ assets: assetsInformationByGroups,
1043
+ outputName,
1044
+ plugin: this
1045
+ });
1046
+ });
1047
+
1048
+ // Turn the js and css paths into grouped HtmlTagObjects
1049
+ const assetTagGroupsPromise = assetsPromise
1050
+ // And allow third-party-plugin authors to reorder and change the assetTags before they are grouped
1051
+ .then(({ assets }) => getHtmlWebpackPluginHooks(compilation).alterAssetTags.promise({
1052
+ assetTags: {
1053
+ scripts: this.generatedScriptTags(assets.js),
1054
+ styles: this.generateStyleTags(assets.css),
1055
+ meta: [
1056
+ ...(this.options.base !== false ? this.generateBaseTag(this.options.base) : []),
1057
+ ...this.generatedMetaTags(this.options.meta),
1058
+ ...(assets.favicon ? this.generateFaviconTag(assets.favicon) : [])
1059
+ ]
1060
+ },
1061
+ outputName,
1062
+ publicPath: assetsInformationByGroups.publicPath,
1063
+ plugin: this
1064
+ }))
1065
+ .then(({ assetTags }) => {
1066
+ // Inject scripts to body unless it set explicitly to head
1067
+ const scriptTarget = this.options.inject === 'head' ||
1068
+ (this.options.inject !== 'body' && this.options.scriptLoading !== 'blocking') ? 'head' : 'body';
1069
+ // Group assets to `head` and `body` tag arrays
1070
+ const assetGroups = this.groupAssetsByElements(assetTags, scriptTarget);
1071
+ // Allow third-party-plugin authors to reorder and change the assetTags once they are grouped
1072
+ return getHtmlWebpackPluginHooks(compilation).alterAssetTagGroups.promise({
1073
+ headTags: assetGroups.headTags,
1074
+ bodyTags: assetGroups.bodyTags,
1075
+ outputName,
1076
+ publicPath: assetsInformationByGroups.publicPath,
1077
+ plugin: this
1078
+ });
1079
+ });
1080
+
1081
+ // Turn the compiled template into a nodejs function or into a nodejs string
1082
+ const templateEvaluationPromise = Promise.resolve()
1083
+ .then(() => {
1084
+ if ('error' in templateResult) {
1085
+ return this.options.showErrors ? prettyError(templateResult.error, compiler.context).toHtml() : 'ERROR';
1086
+ }
1087
+
1088
+ // Allow to use a custom function / string instead
1089
+ if (this.options.templateContent !== false) {
1090
+ return this.options.templateContent;
1091
+ }
1092
+
1093
+ // Once everything is compiled evaluate the html factory and replace it with its content
1094
+ if ('compiledEntry' in templateResult) {
1095
+ const compiledEntry = templateResult.compiledEntry;
1096
+ const assets = compiledEntry.assets;
1097
+
1098
+ // Store assets from child compiler to reemit them later
1099
+ for (const name in assets) {
1100
+ previousEmittedAssets.push({ name, source: assets[name].source, info: assets[name].info });
1101
+ }
1102
+
1103
+ return this.evaluateCompilationResult(compiledEntry.content, assetsInformationByGroups.publicPath, this.options.template);
1104
+ }
1105
+
1106
+ return Promise.reject(new Error('Child compilation contained no compiledEntry'));
1107
+ });
1108
+ const templateExectutionPromise = Promise.all([assetsPromise, assetTagGroupsPromise, templateEvaluationPromise])
1109
+ // Execute the template
1110
+ .then(([assetsHookResult, assetTags, compilationResult]) => typeof compilationResult !== 'function'
1111
+ ? compilationResult
1112
+ : this.executeTemplate(compilationResult, assetsHookResult.assets, { headTags: assetTags.headTags, bodyTags: assetTags.bodyTags }, compilation));
1113
+
1114
+ const injectedHtmlPromise = Promise.all([assetTagGroupsPromise, templateExectutionPromise])
1115
+ // Allow plugins to change the html before assets are injected
1116
+ .then(([assetTags, html]) => {
1117
+ const pluginArgs = { html, headTags: assetTags.headTags, bodyTags: assetTags.bodyTags, plugin: this, outputName };
1118
+ return getHtmlWebpackPluginHooks(compilation).afterTemplateExecution.promise(pluginArgs);
1119
+ })
1120
+ .then(({ html, headTags, bodyTags }) => {
1121
+ return this.postProcessHtml(compiler, html, assetsInformationByGroups, { headTags, bodyTags });
1122
+ });
1123
+
1124
+ const emitHtmlPromise = injectedHtmlPromise
1125
+ // Allow plugins to change the html after assets are injected
1126
+ .then((html) => {
1127
+ const pluginArgs = { html, plugin: this, outputName };
1128
+ return getHtmlWebpackPluginHooks(compilation).beforeEmit.promise(pluginArgs)
1129
+ .then(result => result.html);
1130
+ })
1131
+ .catch(err => {
1132
+ // In case anything went wrong the promise is resolved
1133
+ // with the error message and an error is logged
1134
+ compilation.errors.push(prettyError(err, compiler.context).toString());
1135
+ return this.options.showErrors ? prettyError(err, compiler.context).toHtml() : 'ERROR';
1136
+ })
1137
+ .then(html => {
1138
+ const filename = outputName.replace(/\[templatehash([^\]]*)\]/g, require('util').deprecate(
1139
+ (match, options) => `[contenthash${options}]`,
1140
+ '[templatehash] is now [contenthash]')
1141
+ );
1142
+ const replacedFilename = this.replacePlaceholdersInFilename(compiler, filename, html, compilation);
1143
+ const source = new compiler.webpack.sources.RawSource(html, false);
1144
+
1145
+ // Add the evaluated html code to the webpack assets
1146
+ compilation.emitAsset(replacedFilename.path, source, replacedFilename.info);
1147
+ previousEmittedAssets.push({ name: replacedFilename.path, source });
1148
+
1149
+ return replacedFilename.path;
1150
+ })
1151
+ .then((finalOutputName) => getHtmlWebpackPluginHooks(compilation).afterEmit.promise({
1152
+ outputName: finalOutputName,
1153
+ plugin: this
1154
+ }).catch(err => {
1155
+ /** @type {Logger} */
1156
+ (this.logger).error(err);
1157
+ return null;
1158
+ }).then(() => null));
1159
+
1160
+ // Once all files are added to the webpack compilation
1161
+ // let the webpack compiler continue
1162
+ emitHtmlPromise.then(() => {
1163
+ callback();
1164
+ });
1129
1165
  }
1130
1166
  }
1131
1167
 
@@ -1134,14 +1170,8 @@ function hookIntoCompiler (compiler, options, plugin) {
1134
1170
  * Generate the template parameters
1135
1171
  *
1136
1172
  * 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
1173
+ * @param {Compilation} compilation
1174
+ * @param {AssetsInformationByGroups} assets
1145
1175
  * @param {{
1146
1176
  headTags: HtmlTagObject[],
1147
1177
  bodyTags: HtmlTagObject[]