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