UNPKG

50.6 kBJavaScriptView Raw
1// @ts-check
2"use strict";
3
4const promisify = require("util").promisify;
5
6const vm = require("vm");
7const fs = require("fs");
8const _uniq = require("lodash/uniq");
9const path = require("path");
10const { CachedChildCompilation } = require("./lib/cached-child-compiler");
11
12const {
13 createHtmlTagObject,
14 htmlTagObjectToString,
15 HtmlTagArray,
16} = require("./lib/html-tags");
17const prettyError = require("./lib/errors.js");
18const chunkSorter = require("./lib/chunksorter.js");
19const { AsyncSeriesWaterfallHook } = require("tapable");
20
21/** @typedef {import("./typings").HtmlTagObject} HtmlTagObject */
22/** @typedef {import("./typings").Options} HtmlWebpackOptions */
23/** @typedef {import("./typings").ProcessedOptions} ProcessedHtmlWebpackOptions */
24/** @typedef {import("./typings").TemplateParameter} TemplateParameter */
25/** @typedef {import("webpack").Compiler} Compiler */
26/** @typedef {import("webpack").Compilation} Compilation */
27/** @typedef {Required<Compilation["outputOptions"]["publicPath"]>} PublicPath */
28/** @typedef {ReturnType<Compiler["getInfrastructureLogger"]>} Logger */
29/** @typedef {Compilation["entrypoints"] extends Map<string, infer I> ? I : never} Entrypoint */
30/** @typedef {Array<{ name: string, source: import('webpack').sources.Source, info?: import('webpack').AssetInfo }>} PreviousEmittedAssets */
31/** @typedef {{ publicPath: string, js: Array<string>, css: Array<string>, manifest?: string, favicon?: string }} AssetsInformationByGroups */
32/** @typedef {import("./typings").Hooks} HtmlWebpackPluginHooks */
33/**
34 * @type {WeakMap<Compilation, HtmlWebpackPluginHooks>}}
35 */
36const compilationHooksMap = new WeakMap();
37
38class HtmlWebpackPlugin {
39 // The following is the API definition for all available hooks
40 // For the TypeScript definition, see the Hooks type in typings.d.ts
41 /**
42 beforeAssetTagGeneration:
43 AsyncSeriesWaterfallHook<{
44 assets: {
45 publicPath: string,
46 js: Array<string>,
47 css: Array<string>,
48 favicon?: string | undefined,
49 manifest?: string | undefined
50 },
51 outputName: string,
52 plugin: HtmlWebpackPlugin
53 }>,
54 alterAssetTags:
55 AsyncSeriesWaterfallHook<{
56 assetTags: {
57 scripts: Array<HtmlTagObject>,
58 styles: Array<HtmlTagObject>,
59 meta: Array<HtmlTagObject>,
60 },
61 publicPath: string,
62 outputName: string,
63 plugin: HtmlWebpackPlugin
64 }>,
65 alterAssetTagGroups:
66 AsyncSeriesWaterfallHook<{
67 headTags: Array<HtmlTagObject | HtmlTagObject>,
68 bodyTags: Array<HtmlTagObject | HtmlTagObject>,
69 publicPath: string,
70 outputName: string,
71 plugin: HtmlWebpackPlugin
72 }>,
73 afterTemplateExecution:
74 AsyncSeriesWaterfallHook<{
75 html: string,
76 headTags: Array<HtmlTagObject | HtmlTagObject>,
77 bodyTags: Array<HtmlTagObject | HtmlTagObject>,
78 outputName: string,
79 plugin: HtmlWebpackPlugin,
80 }>,
81 beforeEmit:
82 AsyncSeriesWaterfallHook<{
83 html: string,
84 outputName: string,
85 plugin: HtmlWebpackPlugin,
86 }>,
87 afterEmit:
88 AsyncSeriesWaterfallHook<{
89 outputName: string,
90 plugin: HtmlWebpackPlugin
91 }>
92 */
93
94 /**
95 * Returns all public hooks of the html webpack plugin for the given compilation
96 *
97 * @param {Compilation} compilation
98 * @returns {HtmlWebpackPluginHooks}
99 */
100 static getCompilationHooks(compilation) {
101 let hooks = compilationHooksMap.get(compilation);
102
103 if (!hooks) {
104 hooks = {
105 beforeAssetTagGeneration: new AsyncSeriesWaterfallHook(["pluginArgs"]),
106 alterAssetTags: new AsyncSeriesWaterfallHook(["pluginArgs"]),
107 alterAssetTagGroups: new AsyncSeriesWaterfallHook(["pluginArgs"]),
108 afterTemplateExecution: new AsyncSeriesWaterfallHook(["pluginArgs"]),
109 beforeEmit: new AsyncSeriesWaterfallHook(["pluginArgs"]),
110 afterEmit: new AsyncSeriesWaterfallHook(["pluginArgs"]),
111 };
112 compilationHooksMap.set(compilation, hooks);
113 }
114
115 return hooks;
116 }
117
118 /**
119 * @param {HtmlWebpackOptions} [options]
120 */
121 constructor(options) {
122 /** @type {HtmlWebpackOptions} */
123 // TODO remove me in the next major release
124 this.userOptions = options || {};
125 this.version = HtmlWebpackPlugin.version;
126
127 // Default options
128 /** @type {ProcessedHtmlWebpackOptions} */
129 const defaultOptions = {
130 template: "auto",
131 templateContent: false,
132 templateParameters: templateParametersGenerator,
133 filename: "index.html",
134 publicPath:
135 this.userOptions.publicPath === undefined
136 ? "auto"
137 : this.userOptions.publicPath,
138 hash: false,
139 inject: this.userOptions.scriptLoading === "blocking" ? "body" : "head",
140 scriptLoading: "defer",
141 compile: true,
142 favicon: false,
143 minify: "auto",
144 cache: true,
145 showErrors: true,
146 chunks: "all",
147 excludeChunks: [],
148 chunksSortMode: "auto",
149 meta: {},
150 base: false,
151 title: "Webpack App",
152 xhtml: false,
153 };
154
155 /** @type {ProcessedHtmlWebpackOptions} */
156 this.options = Object.assign(defaultOptions, this.userOptions);
157 }
158
159 /**
160 *
161 * @param {Compiler} compiler
162 * @returns {void}
163 */
164 apply(compiler) {
165 this.logger = compiler.getInfrastructureLogger("HtmlWebpackPlugin");
166
167 const options = this.options;
168
169 options.template = this.getTemplatePath(
170 this.options.template,
171 compiler.context,
172 );
173
174 // Assert correct option spelling
175 if (
176 options.scriptLoading !== "defer" &&
177 options.scriptLoading !== "blocking" &&
178 options.scriptLoading !== "module" &&
179 options.scriptLoading !== "systemjs-module"
180 ) {
181 /** @type {Logger} */
182 (this.logger).error(
183 'The "scriptLoading" option need to be set to "defer", "blocking" or "module" or "systemjs-module"',
184 );
185 }
186
187 if (
188 options.inject !== true &&
189 options.inject !== false &&
190 options.inject !== "head" &&
191 options.inject !== "body"
192 ) {
193 /** @type {Logger} */
194 (this.logger).error(
195 'The `inject` option needs to be set to true, false, "head" or "body',
196 );
197 }
198
199 if (
200 this.options.templateParameters !== false &&
201 typeof this.options.templateParameters !== "function" &&
202 typeof this.options.templateParameters !== "object"
203 ) {
204 /** @type {Logger} */
205 (this.logger).error(
206 "The `templateParameters` has to be either a function or an object or false",
207 );
208 }
209
210 // Default metaOptions if no template is provided
211 if (
212 !this.userOptions.template &&
213 options.templateContent === false &&
214 options.meta
215 ) {
216 options.meta = Object.assign(
217 {},
218 options.meta,
219 {
220 // TODO remove in the next major release
221 // From https://developer.mozilla.org/en-US/docs/Mozilla/Mobile/Viewport_meta_tag
222 viewport: "width=device-width, initial-scale=1",
223 },
224 this.userOptions.meta,
225 );
226 }
227
228 // entryName to fileName conversion function
229 const userOptionFilename =
230 this.userOptions.filename || this.options.filename;
231 const filenameFunction =
232 typeof userOptionFilename === "function"
233 ? userOptionFilename
234 : // Replace '[name]' with entry name
235 (entryName) => userOptionFilename.replace(/\[name\]/g, entryName);
236
237 /** output filenames for the given entry names */
238 const entryNames = Object.keys(compiler.options.entry);
239 const outputFileNames = new Set(
240 (entryNames.length ? entryNames : ["main"]).map(filenameFunction),
241 );
242
243 // Hook all options into the webpack compiler
244 outputFileNames.forEach((outputFileName) => {
245 // Instance variables to keep caching information for multiple builds
246 const assetJson = { value: undefined };
247 /**
248 * store the previous generated asset to emit them even if the content did not change
249 * to support watch mode for third party plugins like the clean-webpack-plugin or the compression plugin
250 * @type {PreviousEmittedAssets}
251 */
252 const previousEmittedAssets = [];
253
254 // Inject child compiler plugin
255 const childCompilerPlugin = new CachedChildCompilation(compiler);
256
257 if (!this.options.templateContent) {
258 childCompilerPlugin.addEntry(this.options.template);
259 }
260
261 // convert absolute filename into relative so that webpack can
262 // generate it at correct location
263 let filename = outputFileName;
264
265 if (path.resolve(filename) === path.normalize(filename)) {
266 const outputPath =
267 /** @type {string} - Once initialized the path is always a string */ (
268 compiler.options.output.path
269 );
270
271 filename = path.relative(outputPath, filename);
272 }
273
274 compiler.hooks.thisCompilation.tap(
275 "HtmlWebpackPlugin",
276 /**
277 * Hook into the webpack compilation
278 * @param {Compilation} compilation
279 */
280 (compilation) => {
281 compilation.hooks.processAssets.tapAsync(
282 {
283 name: "HtmlWebpackPlugin",
284 stage:
285 /**
286 * Generate the html after minification and dev tooling is done
287 */
288 compiler.webpack.Compilation
289 .PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE,
290 },
291 /**
292 * Hook into the process assets hook
293 * @param {any} _
294 * @param {(err?: Error) => void} callback
295 */
296 (_, callback) => {
297 this.generateHTML(
298 compiler,
299 compilation,
300 filename,
301 childCompilerPlugin,
302 previousEmittedAssets,
303 assetJson,
304 callback,
305 );
306 },
307 );
308 },
309 );
310 });
311 }
312
313 /**
314 * Helper to return the absolute template path with a fallback loader
315 *
316 * @private
317 * @param {string} template The path to the template e.g. './index.html'
318 * @param {string} context The webpack base resolution path for relative paths e.g. process.cwd()
319 */
320 getTemplatePath(template, context) {
321 if (template === "auto") {
322 template = path.resolve(context, "src/index.ejs");
323 if (!fs.existsSync(template)) {
324 template = path.join(__dirname, "default_index.ejs");
325 }
326 }
327
328 // If the template doesn't use a loader use the lodash template loader
329 if (template.indexOf("!") === -1) {
330 template =
331 require.resolve("./lib/loader.js") +
332 "!" +
333 path.resolve(context, template);
334 }
335
336 // Resolve template path
337 return template.replace(
338 /([!])([^/\\][^!?]+|[^/\\!?])($|\?[^!?\n]+$)/,
339 (match, prefix, filepath, postfix) =>
340 prefix + path.resolve(filepath) + postfix,
341 );
342 }
343
344 /**
345 * Return all chunks from the compilation result which match the exclude and include filters
346 *
347 * @private
348 * @param {any} chunks
349 * @param {string[]|'all'} includedChunks
350 * @param {string[]} excludedChunks
351 */
352 filterEntryChunks(chunks, includedChunks, excludedChunks) {
353 return chunks.filter((chunkName) => {
354 // Skip if the chunks should be filtered and the given chunk was not added explicity
355 if (
356 Array.isArray(includedChunks) &&
357 includedChunks.indexOf(chunkName) === -1
358 ) {
359 return false;
360 }
361
362 // Skip if the chunks should be filtered and the given chunk was excluded explicity
363 if (
364 Array.isArray(excludedChunks) &&
365 excludedChunks.indexOf(chunkName) !== -1
366 ) {
367 return false;
368 }
369
370 // Add otherwise
371 return true;
372 });
373 }
374
375 /**
376 * Helper to sort chunks
377 *
378 * @private
379 * @param {string[]} entryNames
380 * @param {string|((entryNameA: string, entryNameB: string) => number)} sortMode
381 * @param {Compilation} compilation
382 */
383 sortEntryChunks(entryNames, sortMode, compilation) {
384 // Custom function
385 if (typeof sortMode === "function") {
386 return entryNames.sort(sortMode);
387 }
388 // Check if the given sort mode is a valid chunkSorter sort mode
389 if (typeof chunkSorter[sortMode] !== "undefined") {
390 return chunkSorter[sortMode](entryNames, compilation, this.options);
391 }
392 throw new Error('"' + sortMode + '" is not a valid chunk sort mode');
393 }
394
395 /**
396 * Encode each path component using `encodeURIComponent` as files can contain characters
397 * which needs special encoding in URLs like `+ `.
398 *
399 * Valid filesystem characters which need to be encoded for urls:
400 *
401 * # pound, % percent, & ampersand, { left curly bracket, } right curly bracket,
402 * \ back slash, < left angle bracket, > right angle bracket, * asterisk, ? question mark,
403 * blank spaces, $ dollar sign, ! exclamation point, ' single quotes, " double quotes,
404 * : colon, @ at sign, + plus sign, ` backtick, | pipe, = equal sign
405 *
406 * However the query string must not be encoded:
407 *
408 * fo:demonstration-path/very fancy+name.js?path=/home?value=abc&value=def#zzz
409 * ^ ^ ^ ^ ^ ^ ^ ^^ ^ ^ ^ ^ ^
410 * | | | | | | | || | | | | |
411 * encoded | | encoded | | || | | | | |
412 * ignored ignored ignored ignored ignored
413 *
414 * @private
415 * @param {string} filePath
416 */
417 urlencodePath(filePath) {
418 // People use the filepath in quite unexpected ways.
419 // Try to extract the first querystring of the url:
420 //
421 // some+path/demo.html?value=abc?def
422 //
423 const queryStringStart = filePath.indexOf("?");
424 const urlPath =
425 queryStringStart === -1 ? filePath : filePath.substr(0, queryStringStart);
426 const queryString = filePath.substr(urlPath.length);
427 // Encode all parts except '/' which are not part of the querystring:
428 const encodedUrlPath = urlPath.split("/").map(encodeURIComponent).join("/");
429 return encodedUrlPath + queryString;
430 }
431
432 /**
433 * Appends a cache busting hash to the query string of the url
434 * E.g. http://localhost:8080/ -> http://localhost:8080/?50c9096ba6183fd728eeb065a26ec175
435 *
436 * @private
437 * @param {string | undefined} url
438 * @param {string} hash
439 */
440 appendHash(url, hash) {
441 if (!url) {
442 return url;
443 }
444
445 return url + (url.indexOf("?") === -1 ? "?" : "&") + hash;
446 }
447
448 /**
449 * Generate the relative or absolute base url to reference images, css, and javascript files
450 * from within the html file - the publicPath
451 *
452 * @private
453 * @param {Compilation} compilation
454 * @param {string} filename
455 * @param {string | 'auto'} customPublicPath
456 * @returns {string}
457 */
458 getPublicPath(compilation, filename, customPublicPath) {
459 /**
460 * @type {string} the configured public path to the asset root
461 * if a path publicPath is set in the current webpack config use it otherwise
462 * fallback to a relative path
463 */
464 const webpackPublicPath = compilation.getAssetPath(
465 /** @type {NonNullable<Compilation["outputOptions"]["publicPath"]>} */ (
466 compilation.outputOptions.publicPath
467 ),
468 { hash: compilation.hash },
469 );
470 // Webpack 5 introduced "auto" as default value
471 const isPublicPathDefined = webpackPublicPath !== "auto";
472
473 let publicPath =
474 // If the html-webpack-plugin options contain a custom public path unset it
475 customPublicPath !== "auto"
476 ? customPublicPath
477 : isPublicPathDefined
478 ? // If a hard coded public path exists use it
479 webpackPublicPath
480 : // If no public path was set get a relative url path
481 path
482 .relative(
483 path.resolve(
484 /** @type {string} */ (compilation.options.output.path),
485 path.dirname(filename),
486 ),
487 /** @type {string} */ (compilation.options.output.path),
488 )
489 .split(path.sep)
490 .join("/");
491
492 if (publicPath.length && publicPath.substr(-1, 1) !== "/") {
493 publicPath += "/";
494 }
495
496 return publicPath;
497 }
498
499 /**
500 * The getAssetsForHTML extracts the asset information of a webpack compilation for all given entry names.
501 *
502 * @private
503 * @param {Compilation} compilation
504 * @param {string} outputName
505 * @param {string[]} entryNames
506 * @returns {AssetsInformationByGroups}
507 */
508 getAssetsInformationByGroups(compilation, outputName, entryNames) {
509 /** The public path used inside the html file */
510 const publicPath = this.getPublicPath(
511 compilation,
512 outputName,
513 this.options.publicPath,
514 );
515 /**
516 * @type {AssetsInformationByGroups}
517 */
518 const assets = {
519 // The public path
520 publicPath,
521 // Will contain all js and mjs files
522 js: [],
523 // Will contain all css files
524 css: [],
525 // Will contain the html5 appcache manifest files if it exists
526 manifest: Object.keys(compilation.assets).find(
527 (assetFile) => path.extname(assetFile) === ".appcache",
528 ),
529 // Favicon
530 favicon: undefined,
531 };
532
533 // Append a hash for cache busting
534 if (this.options.hash && assets.manifest) {
535 assets.manifest = this.appendHash(
536 assets.manifest,
537 /** @type {string} */ (compilation.hash),
538 );
539 }
540
541 // Extract paths to .js, .mjs and .css files from the current compilation
542 const entryPointPublicPathMap = {};
543 const extensionRegexp = /\.(css|js|mjs)(\?|$)/;
544
545 for (let i = 0; i < entryNames.length; i++) {
546 const entryName = entryNames[i];
547 /** entryPointUnfilteredFiles - also includes hot module update files */
548 const entryPointUnfilteredFiles = /** @type {Entrypoint} */ (
549 compilation.entrypoints.get(entryName)
550 ).getFiles();
551 const entryPointFiles = entryPointUnfilteredFiles.filter((chunkFile) => {
552 const asset = compilation.getAsset(chunkFile);
553
554 if (!asset) {
555 return true;
556 }
557
558 // Prevent hot-module files from being included:
559 const assetMetaInformation = asset.info || {};
560
561 return !(
562 assetMetaInformation.hotModuleReplacement ||
563 assetMetaInformation.development
564 );
565 });
566 // Prepend the publicPath and append the hash depending on the
567 // webpack.output.publicPath and hashOptions
568 // E.g. bundle.js -> /bundle.js?hash
569 const entryPointPublicPaths = entryPointFiles.map((chunkFile) => {
570 const entryPointPublicPath = publicPath + this.urlencodePath(chunkFile);
571 return this.options.hash
572 ? this.appendHash(
573 entryPointPublicPath,
574 /** @type {string} */ (compilation.hash),
575 )
576 : entryPointPublicPath;
577 });
578
579 entryPointPublicPaths.forEach((entryPointPublicPath) => {
580 const extMatch = extensionRegexp.exec(
581 /** @type {string} */ (entryPointPublicPath),
582 );
583
584 // Skip if the public path is not a .css, .mjs or .js file
585 if (!extMatch) {
586 return;
587 }
588
589 // Skip if this file is already known
590 // (e.g. because of common chunk optimizations)
591 if (entryPointPublicPathMap[entryPointPublicPath]) {
592 return;
593 }
594
595 entryPointPublicPathMap[entryPointPublicPath] = true;
596
597 // ext will contain .js or .css, because .mjs recognizes as .js
598 const ext = extMatch[1] === "mjs" ? "js" : extMatch[1];
599
600 assets[ext].push(entryPointPublicPath);
601 });
602 }
603
604 return assets;
605 }
606
607 /**
608 * Once webpack is done with compiling the template into a NodeJS code this function
609 * evaluates it to generate the html result
610 *
611 * The evaluateCompilationResult is only a class function to allow spying during testing.
612 * Please change that in a further refactoring
613 *
614 * @param {string} source
615 * @param {string} publicPath
616 * @param {string} templateFilename
617 * @returns {Promise<string | (() => string | Promise<string>)>}
618 */
619 evaluateCompilationResult(source, publicPath, templateFilename) {
620 if (!source) {
621 return Promise.reject(
622 new Error("The child compilation didn't provide a result"),
623 );
624 }
625
626 // The LibraryTemplatePlugin stores the template result in a local variable.
627 // By adding it to the end the value gets extracted during evaluation
628 if (source.indexOf("HTML_WEBPACK_PLUGIN_RESULT") >= 0) {
629 source += ";\nHTML_WEBPACK_PLUGIN_RESULT";
630 }
631
632 const templateWithoutLoaders = templateFilename
633 .replace(/^.+!/, "")
634 .replace(/\?.+$/, "");
635 const vmContext = vm.createContext({
636 ...global,
637 HTML_WEBPACK_PLUGIN: true,
638 require: require,
639 htmlWebpackPluginPublicPath: publicPath,
640 __filename: templateWithoutLoaders,
641 __dirname: path.dirname(templateWithoutLoaders),
642 AbortController: global.AbortController,
643 AbortSignal: global.AbortSignal,
644 Blob: global.Blob,
645 Buffer: global.Buffer,
646 ByteLengthQueuingStrategy: global.ByteLengthQueuingStrategy,
647 BroadcastChannel: global.BroadcastChannel,
648 CompressionStream: global.CompressionStream,
649 CountQueuingStrategy: global.CountQueuingStrategy,
650 Crypto: global.Crypto,
651 CryptoKey: global.CryptoKey,
652 CustomEvent: global.CustomEvent,
653 DecompressionStream: global.DecompressionStream,
654 Event: global.Event,
655 EventTarget: global.EventTarget,
656 File: global.File,
657 FormData: global.FormData,
658 Headers: global.Headers,
659 MessageChannel: global.MessageChannel,
660 MessageEvent: global.MessageEvent,
661 MessagePort: global.MessagePort,
662 PerformanceEntry: global.PerformanceEntry,
663 PerformanceMark: global.PerformanceMark,
664 PerformanceMeasure: global.PerformanceMeasure,
665 PerformanceObserver: global.PerformanceObserver,
666 PerformanceObserverEntryList: global.PerformanceObserverEntryList,
667 PerformanceResourceTiming: global.PerformanceResourceTiming,
668 ReadableByteStreamController: global.ReadableByteStreamController,
669 ReadableStream: global.ReadableStream,
670 ReadableStreamBYOBReader: global.ReadableStreamBYOBReader,
671 ReadableStreamBYOBRequest: global.ReadableStreamBYOBRequest,
672 ReadableStreamDefaultController: global.ReadableStreamDefaultController,
673 ReadableStreamDefaultReader: global.ReadableStreamDefaultReader,
674 Response: global.Response,
675 Request: global.Request,
676 SubtleCrypto: global.SubtleCrypto,
677 DOMException: global.DOMException,
678 TextDecoder: global.TextDecoder,
679 TextDecoderStream: global.TextDecoderStream,
680 TextEncoder: global.TextEncoder,
681 TextEncoderStream: global.TextEncoderStream,
682 TransformStream: global.TransformStream,
683 TransformStreamDefaultController: global.TransformStreamDefaultController,
684 URL: global.URL,
685 URLSearchParams: global.URLSearchParams,
686 WebAssembly: global.WebAssembly,
687 WritableStream: global.WritableStream,
688 WritableStreamDefaultController: global.WritableStreamDefaultController,
689 WritableStreamDefaultWriter: global.WritableStreamDefaultWriter,
690 });
691
692 const vmScript = new vm.Script(source, {
693 filename: templateWithoutLoaders,
694 });
695
696 // Evaluate code and cast to string
697 let newSource;
698
699 try {
700 newSource = vmScript.runInContext(vmContext);
701 } catch (e) {
702 return Promise.reject(e);
703 }
704
705 if (
706 typeof newSource === "object" &&
707 newSource.__esModule &&
708 newSource.default !== undefined
709 ) {
710 newSource = newSource.default;
711 }
712
713 return typeof newSource === "string" || typeof newSource === "function"
714 ? Promise.resolve(newSource)
715 : Promise.reject(
716 new Error(
717 'The loader "' + templateWithoutLoaders + "\" didn't return html.",
718 ),
719 );
720 }
721
722 /**
723 * Add toString methods for easier rendering inside the template
724 *
725 * @private
726 * @param {Array<HtmlTagObject>} assetTagGroup
727 * @returns {Array<HtmlTagObject>}
728 */
729 prepareAssetTagGroupForRendering(assetTagGroup) {
730 const xhtml = this.options.xhtml;
731 return HtmlTagArray.from(
732 assetTagGroup.map((assetTag) => {
733 const copiedAssetTag = Object.assign({}, assetTag);
734 copiedAssetTag.toString = function () {
735 return htmlTagObjectToString(this, xhtml);
736 };
737 return copiedAssetTag;
738 }),
739 );
740 }
741
742 /**
743 * Generate the template parameters for the template function
744 *
745 * @private
746 * @param {Compilation} compilation
747 * @param {AssetsInformationByGroups} assetsInformationByGroups
748 * @param {{
749 headTags: HtmlTagObject[],
750 bodyTags: HtmlTagObject[]
751 }} assetTags
752 * @returns {Promise<{[key: any]: any}>}
753 */
754 getTemplateParameters(compilation, assetsInformationByGroups, assetTags) {
755 const templateParameters = this.options.templateParameters;
756
757 if (templateParameters === false) {
758 return Promise.resolve({});
759 }
760
761 if (
762 typeof templateParameters !== "function" &&
763 typeof templateParameters !== "object"
764 ) {
765 throw new Error(
766 "templateParameters has to be either a function or an object",
767 );
768 }
769
770 const templateParameterFunction =
771 typeof templateParameters === "function"
772 ? // A custom function can overwrite the entire template parameter preparation
773 templateParameters
774 : // If the template parameters is an object merge it with the default values
775 (compilation, assetsInformationByGroups, assetTags, options) =>
776 Object.assign(
777 {},
778 templateParametersGenerator(
779 compilation,
780 assetsInformationByGroups,
781 assetTags,
782 options,
783 ),
784 templateParameters,
785 );
786 const preparedAssetTags = {
787 headTags: this.prepareAssetTagGroupForRendering(assetTags.headTags),
788 bodyTags: this.prepareAssetTagGroupForRendering(assetTags.bodyTags),
789 };
790 return Promise.resolve().then(() =>
791 templateParameterFunction(
792 compilation,
793 assetsInformationByGroups,
794 preparedAssetTags,
795 this.options,
796 ),
797 );
798 }
799
800 /**
801 * This function renders the actual html by executing the template function
802 *
803 * @private
804 * @param {(templateParameters) => string | Promise<string>} templateFunction
805 * @param {AssetsInformationByGroups} assetsInformationByGroups
806 * @param {{
807 headTags: HtmlTagObject[],
808 bodyTags: HtmlTagObject[]
809 }} assetTags
810 * @param {Compilation} compilation
811 * @returns Promise<string>
812 */
813 executeTemplate(
814 templateFunction,
815 assetsInformationByGroups,
816 assetTags,
817 compilation,
818 ) {
819 // Template processing
820 const templateParamsPromise = this.getTemplateParameters(
821 compilation,
822 assetsInformationByGroups,
823 assetTags,
824 );
825
826 return templateParamsPromise.then((templateParams) => {
827 try {
828 // If html is a promise return the promise
829 // If html is a string turn it into a promise
830 return templateFunction(templateParams);
831 } catch (e) {
832 // @ts-ignore
833 compilation.errors.push(new Error("Template execution failed: " + e));
834 return Promise.reject(e);
835 }
836 });
837 }
838
839 /**
840 * Html Post processing
841 *
842 * @private
843 * @param {Compiler} compiler The compiler instance
844 * @param {any} originalHtml The input html
845 * @param {AssetsInformationByGroups} assetsInformationByGroups
846 * @param {{headTags: HtmlTagObject[], bodyTags: HtmlTagObject[]}} assetTags The asset tags to inject
847 * @returns {Promise<string>}
848 */
849 postProcessHtml(
850 compiler,
851 originalHtml,
852 assetsInformationByGroups,
853 assetTags,
854 ) {
855 let html = originalHtml;
856
857 if (typeof html !== "string") {
858 return Promise.reject(
859 new Error(
860 "Expected html to be a string but got " + JSON.stringify(html),
861 ),
862 );
863 }
864
865 if (this.options.inject) {
866 const htmlRegExp = /(<html[^>]*>)/i;
867 const headRegExp = /(<\/head\s*>)/i;
868 const bodyRegExp = /(<\/body\s*>)/i;
869 const metaViewportRegExp = /<meta[^>]+name=["']viewport["'][^>]*>/i;
870 const body = assetTags.bodyTags.map((assetTagObject) =>
871 htmlTagObjectToString(assetTagObject, this.options.xhtml),
872 );
873 const head = assetTags.headTags
874 .filter((item) => {
875 if (
876 item.tagName === "meta" &&
877 item.attributes &&
878 item.attributes.name === "viewport" &&
879 metaViewportRegExp.test(html)
880 ) {
881 return false;
882 }
883
884 return true;
885 })
886 .map((assetTagObject) =>
887 htmlTagObjectToString(assetTagObject, this.options.xhtml),
888 );
889
890 if (body.length) {
891 if (bodyRegExp.test(html)) {
892 // Append assets to body element
893 html = html.replace(bodyRegExp, (match) => body.join("") + match);
894 } else {
895 // Append scripts to the end of the file if no <body> element exists:
896 html += body.join("");
897 }
898 }
899
900 if (head.length) {
901 // Create a head tag if none exists
902 if (!headRegExp.test(html)) {
903 if (!htmlRegExp.test(html)) {
904 html = "<head></head>" + html;
905 } else {
906 html = html.replace(htmlRegExp, (match) => match + "<head></head>");
907 }
908 }
909
910 // Append assets to head element
911 html = html.replace(headRegExp, (match) => head.join("") + match);
912 }
913
914 // Inject manifest into the opening html tag
915 if (assetsInformationByGroups.manifest) {
916 html = html.replace(/(<html[^>]*)(>)/i, (match, start, end) => {
917 // Append the manifest only if no manifest was specified
918 if (/\smanifest\s*=/.test(match)) {
919 return match;
920 }
921 return (
922 start +
923 ' manifest="' +
924 assetsInformationByGroups.manifest +
925 '"' +
926 end
927 );
928 });
929 }
930 }
931
932 // TODO avoid this logic and use https://github.com/webpack-contrib/html-minimizer-webpack-plugin under the hood in the next major version
933 // Check if webpack is running in production mode
934 // @see https://github.com/webpack/webpack/blob/3366421f1784c449f415cda5930a8e445086f688/lib/WebpackOptionsDefaulter.js#L12-L14
935 const isProductionLikeMode =
936 compiler.options.mode === "production" || !compiler.options.mode;
937 const needMinify =
938 this.options.minify === true ||
939 typeof this.options.minify === "object" ||
940 (this.options.minify === "auto" && isProductionLikeMode);
941
942 if (!needMinify) {
943 return Promise.resolve(html);
944 }
945
946 const minifyOptions =
947 typeof this.options.minify === "object"
948 ? this.options.minify
949 : {
950 // https://www.npmjs.com/package/html-minifier-terser#options-quick-reference
951 collapseWhitespace: true,
952 keepClosingSlash: true,
953 removeComments: true,
954 removeRedundantAttributes: true,
955 removeScriptTypeAttributes: true,
956 removeStyleLinkTypeAttributes: true,
957 useShortDoctype: true,
958 };
959
960 try {
961 html = require("html-minifier-terser").minify(html, minifyOptions);
962 } catch (e) {
963 const isParseError = String(e.message).indexOf("Parse Error") === 0;
964
965 if (isParseError) {
966 e.message =
967 "html-webpack-plugin could not minify the generated output.\n" +
968 "In production mode the html minification is enabled by default.\n" +
969 "If you are not generating a valid html output please disable it manually.\n" +
970 "You can do so by adding the following setting to your HtmlWebpackPlugin config:\n|\n|" +
971 " minify: false\n|\n" +
972 "See https://github.com/jantimon/html-webpack-plugin#options for details.\n\n" +
973 "For parser dedicated bugs please create an issue here:\n" +
974 "https://danielruf.github.io/html-minifier-terser/" +
975 "\n" +
976 e.message;
977 }
978
979 return Promise.reject(e);
980 }
981
982 return Promise.resolve(html);
983 }
984
985 /**
986 * Helper to return a sorted unique array of all asset files out of the asset object
987 * @private
988 */
989 getAssetFiles(assets) {
990 const files = _uniq(
991 Object.keys(assets)
992 .filter((assetType) => assetType !== "chunks" && assets[assetType])
993 .reduce((files, assetType) => files.concat(assets[assetType]), []),
994 );
995 files.sort();
996 return files;
997 }
998
999 /**
1000 * Converts a favicon file from disk to a webpack resource and returns the url to the resource
1001 *
1002 * @private
1003 * @param {Compiler} compiler
1004 * @param {string|false} favicon
1005 * @param {Compilation} compilation
1006 * @param {string} publicPath
1007 * @param {PreviousEmittedAssets} previousEmittedAssets
1008 * @returns {Promise<string|undefined>}
1009 */
1010 generateFavicon(
1011 compiler,
1012 favicon,
1013 compilation,
1014 publicPath,
1015 previousEmittedAssets,
1016 ) {
1017 if (!favicon) {
1018 return Promise.resolve(undefined);
1019 }
1020
1021 const filename = path.resolve(compilation.compiler.context, favicon);
1022
1023 return promisify(compilation.inputFileSystem.readFile)(filename)
1024 .then((buf) => {
1025 const source = new compiler.webpack.sources.RawSource(
1026 /** @type {string | Buffer} */ (buf),
1027 false,
1028 );
1029 const name = path.basename(filename);
1030
1031 compilation.fileDependencies.add(filename);
1032 compilation.emitAsset(name, source);
1033 previousEmittedAssets.push({ name, source });
1034
1035 const faviconPath = publicPath + name;
1036
1037 if (this.options.hash) {
1038 return this.appendHash(
1039 faviconPath,
1040 /** @type {string} */ (compilation.hash),
1041 );
1042 }
1043
1044 return faviconPath;
1045 })
1046 .catch(() =>
1047 Promise.reject(
1048 new Error("HtmlWebpackPlugin: could not load file " + filename),
1049 ),
1050 );
1051 }
1052
1053 /**
1054 * Generate all tags script for the given file paths
1055 *
1056 * @private
1057 * @param {Array<string>} jsAssets
1058 * @returns {Array<HtmlTagObject>}
1059 */
1060 generatedScriptTags(jsAssets) {
1061 // @ts-ignore
1062 return jsAssets.map((src) => {
1063 const attributes = {};
1064
1065 if (this.options.scriptLoading === "defer") {
1066 attributes.defer = true;
1067 } else if (this.options.scriptLoading === "module") {
1068 attributes.type = "module";
1069 } else if (this.options.scriptLoading === "systemjs-module") {
1070 attributes.type = "systemjs-module";
1071 }
1072
1073 attributes.src = src;
1074
1075 return {
1076 tagName: "script",
1077 voidTag: false,
1078 meta: { plugin: "html-webpack-plugin" },
1079 attributes,
1080 };
1081 });
1082 }
1083
1084 /**
1085 * Generate all style tags for the given file paths
1086 *
1087 * @private
1088 * @param {Array<string>} cssAssets
1089 * @returns {Array<HtmlTagObject>}
1090 */
1091 generateStyleTags(cssAssets) {
1092 return cssAssets.map((styleAsset) => ({
1093 tagName: "link",
1094 voidTag: true,
1095 meta: { plugin: "html-webpack-plugin" },
1096 attributes: {
1097 href: styleAsset,
1098 rel: "stylesheet",
1099 },
1100 }));
1101 }
1102
1103 /**
1104 * Generate an optional base tag
1105 *
1106 * @param {string | {[attributeName: string]: string}} base
1107 * @returns {Array<HtmlTagObject>}
1108 */
1109 generateBaseTag(base) {
1110 return [
1111 {
1112 tagName: "base",
1113 voidTag: true,
1114 meta: { plugin: "html-webpack-plugin" },
1115 // attributes e.g. { href:"http://example.com/page.html" target:"_blank" }
1116 attributes:
1117 typeof base === "string"
1118 ? {
1119 href: base,
1120 }
1121 : base,
1122 },
1123 ];
1124 }
1125
1126 /**
1127 * Generate all meta tags for the given meta configuration
1128 *
1129 * @private
1130 * @param {false | {[name: string]: false | string | {[attributeName: string]: string|boolean}}} metaOptions
1131 * @returns {Array<HtmlTagObject>}
1132 */
1133 generatedMetaTags(metaOptions) {
1134 if (metaOptions === false) {
1135 return [];
1136 }
1137
1138 // Make tags self-closing in case of xhtml
1139 // Turn { "viewport" : "width=500, initial-scale=1" } into
1140 // [{ name:"viewport" content:"width=500, initial-scale=1" }]
1141 const metaTagAttributeObjects = Object.keys(metaOptions)
1142 .map((metaName) => {
1143 const metaTagContent = metaOptions[metaName];
1144 return typeof metaTagContent === "string"
1145 ? {
1146 name: metaName,
1147 content: metaTagContent,
1148 }
1149 : metaTagContent;
1150 })
1151 .filter((attribute) => attribute !== false);
1152
1153 // Turn [{ name:"viewport" content:"width=500, initial-scale=1" }] into
1154 // the html-webpack-plugin tag structure
1155 return metaTagAttributeObjects.map((metaTagAttributes) => {
1156 if (metaTagAttributes === false) {
1157 throw new Error("Invalid meta tag");
1158 }
1159 return {
1160 tagName: "meta",
1161 voidTag: true,
1162 meta: { plugin: "html-webpack-plugin" },
1163 attributes: metaTagAttributes,
1164 };
1165 });
1166 }
1167
1168 /**
1169 * Generate a favicon tag for the given file path
1170 *
1171 * @private
1172 * @param {string} favicon
1173 * @returns {Array<HtmlTagObject>}
1174 */
1175 generateFaviconTag(favicon) {
1176 return [
1177 {
1178 tagName: "link",
1179 voidTag: true,
1180 meta: { plugin: "html-webpack-plugin" },
1181 attributes: {
1182 rel: "icon",
1183 href: favicon,
1184 },
1185 },
1186 ];
1187 }
1188
1189 /**
1190 * Group assets to head and body tags
1191 *
1192 * @param {{
1193 scripts: Array<HtmlTagObject>;
1194 styles: Array<HtmlTagObject>;
1195 meta: Array<HtmlTagObject>;
1196 }} assetTags
1197 * @param {"body" | "head"} scriptTarget
1198 * @returns {{
1199 headTags: Array<HtmlTagObject>;
1200 bodyTags: Array<HtmlTagObject>;
1201 }}
1202 */
1203 groupAssetsByElements(assetTags, scriptTarget) {
1204 /** @type {{ headTags: Array<HtmlTagObject>; bodyTags: Array<HtmlTagObject>; }} */
1205 const result = {
1206 headTags: [...assetTags.meta, ...assetTags.styles],
1207 bodyTags: [],
1208 };
1209
1210 // Add script tags to head or body depending on
1211 // the htmlPluginOptions
1212 if (scriptTarget === "body") {
1213 result.bodyTags.push(...assetTags.scripts);
1214 } else {
1215 // If script loading is blocking add the scripts to the end of the head
1216 // If script loading is non-blocking add the scripts in front of the css files
1217 const insertPosition =
1218 this.options.scriptLoading === "blocking"
1219 ? result.headTags.length
1220 : assetTags.meta.length;
1221
1222 result.headTags.splice(insertPosition, 0, ...assetTags.scripts);
1223 }
1224
1225 return result;
1226 }
1227
1228 /**
1229 * Replace [contenthash] in filename
1230 *
1231 * @see https://survivejs.com/webpack/optimizing/adding-hashes-to-filenames/
1232 *
1233 * @private
1234 * @param {Compiler} compiler
1235 * @param {string} filename
1236 * @param {string|Buffer} fileContent
1237 * @param {Compilation} compilation
1238 * @returns {{ path: string, info: {} }}
1239 */
1240 replacePlaceholdersInFilename(compiler, filename, fileContent, compilation) {
1241 if (/\[\\*([\w:]+)\\*\]/i.test(filename) === false) {
1242 return { path: filename, info: {} };
1243 }
1244
1245 const hash = compiler.webpack.util.createHash(
1246 compilation.outputOptions.hashFunction,
1247 );
1248
1249 hash.update(fileContent);
1250
1251 if (compilation.outputOptions.hashSalt) {
1252 hash.update(compilation.outputOptions.hashSalt);
1253 }
1254
1255 const contentHash = /** @type {string} */ (
1256 hash
1257 .digest(compilation.outputOptions.hashDigest)
1258 .slice(0, compilation.outputOptions.hashDigestLength)
1259 );
1260
1261 return compilation.getPathWithInfo(filename, {
1262 contentHash,
1263 chunk: {
1264 hash: contentHash,
1265 // @ts-ignore
1266 contentHash,
1267 },
1268 });
1269 }
1270
1271 /**
1272 * Function to generate HTML file.
1273 *
1274 * @private
1275 * @param {Compiler} compiler
1276 * @param {Compilation} compilation
1277 * @param {string} outputName
1278 * @param {CachedChildCompilation} childCompilerPlugin
1279 * @param {PreviousEmittedAssets} previousEmittedAssets
1280 * @param {{ value: string | undefined }} assetJson
1281 * @param {(err?: Error) => void} callback
1282 */
1283 generateHTML(
1284 compiler,
1285 compilation,
1286 outputName,
1287 childCompilerPlugin,
1288 previousEmittedAssets,
1289 assetJson,
1290 callback,
1291 ) {
1292 // Get all entry point names for this html file
1293 const entryNames = Array.from(compilation.entrypoints.keys());
1294 const filteredEntryNames = this.filterEntryChunks(
1295 entryNames,
1296 this.options.chunks,
1297 this.options.excludeChunks,
1298 );
1299 const sortedEntryNames = this.sortEntryChunks(
1300 filteredEntryNames,
1301 this.options.chunksSortMode,
1302 compilation,
1303 );
1304 const templateResult = this.options.templateContent
1305 ? { mainCompilationHash: compilation.hash }
1306 : childCompilerPlugin.getCompilationEntryResult(this.options.template);
1307
1308 if ("error" in templateResult) {
1309 compilation.errors.push(
1310 prettyError(templateResult.error, compiler.context).toString(),
1311 );
1312 }
1313
1314 // If the child compilation was not executed during a previous main compile run
1315 // it is a cached result
1316 const isCompilationCached =
1317 templateResult.mainCompilationHash !== compilation.hash;
1318 /** Generated file paths from the entry point names */
1319 const assetsInformationByGroups = this.getAssetsInformationByGroups(
1320 compilation,
1321 outputName,
1322 sortedEntryNames,
1323 );
1324 // If the template and the assets did not change we don't have to emit the html
1325 const newAssetJson = JSON.stringify(
1326 this.getAssetFiles(assetsInformationByGroups),
1327 );
1328
1329 if (
1330 isCompilationCached &&
1331 this.options.cache &&
1332 assetJson.value === newAssetJson
1333 ) {
1334 previousEmittedAssets.forEach(({ name, source, info }) => {
1335 compilation.emitAsset(name, source, info);
1336 });
1337 return callback();
1338 } else {
1339 previousEmittedAssets.length = 0;
1340 assetJson.value = newAssetJson;
1341 }
1342
1343 // The html-webpack plugin uses a object representation for the html-tags which will be injected
1344 // to allow altering them more easily
1345 // Just before they are converted a third-party-plugin author might change the order and content
1346 const assetsPromise = this.generateFavicon(
1347 compiler,
1348 this.options.favicon,
1349 compilation,
1350 assetsInformationByGroups.publicPath,
1351 previousEmittedAssets,
1352 ).then((faviconPath) => {
1353 assetsInformationByGroups.favicon = faviconPath;
1354 return HtmlWebpackPlugin.getCompilationHooks(
1355 compilation,
1356 ).beforeAssetTagGeneration.promise({
1357 assets: assetsInformationByGroups,
1358 outputName,
1359 plugin: this,
1360 });
1361 });
1362
1363 // Turn the js and css paths into grouped HtmlTagObjects
1364 const assetTagGroupsPromise = assetsPromise
1365 // And allow third-party-plugin authors to reorder and change the assetTags before they are grouped
1366 .then(({ assets }) =>
1367 HtmlWebpackPlugin.getCompilationHooks(
1368 compilation,
1369 ).alterAssetTags.promise({
1370 assetTags: {
1371 scripts: this.generatedScriptTags(assets.js),
1372 styles: this.generateStyleTags(assets.css),
1373 meta: [
1374 ...(this.options.base !== false
1375 ? this.generateBaseTag(this.options.base)
1376 : []),
1377 ...this.generatedMetaTags(this.options.meta),
1378 ...(assets.favicon
1379 ? this.generateFaviconTag(assets.favicon)
1380 : []),
1381 ],
1382 },
1383 outputName,
1384 publicPath: assetsInformationByGroups.publicPath,
1385 plugin: this,
1386 }),
1387 )
1388 .then(({ assetTags }) => {
1389 // Inject scripts to body unless it set explicitly to head
1390 const scriptTarget =
1391 this.options.inject === "head" ||
1392 (this.options.inject !== "body" &&
1393 this.options.scriptLoading !== "blocking")
1394 ? "head"
1395 : "body";
1396 // Group assets to `head` and `body` tag arrays
1397 const assetGroups = this.groupAssetsByElements(assetTags, scriptTarget);
1398 // Allow third-party-plugin authors to reorder and change the assetTags once they are grouped
1399 return HtmlWebpackPlugin.getCompilationHooks(
1400 compilation,
1401 ).alterAssetTagGroups.promise({
1402 headTags: assetGroups.headTags,
1403 bodyTags: assetGroups.bodyTags,
1404 outputName,
1405 publicPath: assetsInformationByGroups.publicPath,
1406 plugin: this,
1407 });
1408 });
1409
1410 // Turn the compiled template into a nodejs function or into a nodejs string
1411 const templateEvaluationPromise = Promise.resolve().then(() => {
1412 if ("error" in templateResult) {
1413 return this.options.showErrors
1414 ? prettyError(templateResult.error, compiler.context).toHtml()
1415 : "ERROR";
1416 }
1417
1418 // Allow to use a custom function / string instead
1419 if (this.options.templateContent !== false) {
1420 return this.options.templateContent;
1421 }
1422
1423 // Once everything is compiled evaluate the html factory and replace it with its content
1424 if ("compiledEntry" in templateResult) {
1425 const compiledEntry = templateResult.compiledEntry;
1426 const assets = compiledEntry.assets;
1427
1428 // Store assets from child compiler to re-emit them later
1429 for (const name in assets) {
1430 previousEmittedAssets.push({
1431 name,
1432 source: assets[name].source,
1433 info: assets[name].info,
1434 });
1435 }
1436
1437 return this.evaluateCompilationResult(
1438 compiledEntry.content,
1439 assetsInformationByGroups.publicPath,
1440 this.options.template,
1441 );
1442 }
1443
1444 return Promise.reject(
1445 new Error("Child compilation contained no compiledEntry"),
1446 );
1447 });
1448 const templateExecutionPromise = Promise.all([
1449 assetsPromise,
1450 assetTagGroupsPromise,
1451 templateEvaluationPromise,
1452 ])
1453 // Execute the template
1454 .then(([assetsHookResult, assetTags, compilationResult]) =>
1455 typeof compilationResult !== "function"
1456 ? compilationResult
1457 : this.executeTemplate(
1458 compilationResult,
1459 assetsHookResult.assets,
1460 { headTags: assetTags.headTags, bodyTags: assetTags.bodyTags },
1461 compilation,
1462 ),
1463 );
1464
1465 const injectedHtmlPromise = Promise.all([
1466 assetTagGroupsPromise,
1467 templateExecutionPromise,
1468 ])
1469 // Allow plugins to change the html before assets are injected
1470 .then(([assetTags, html]) => {
1471 const pluginArgs = {
1472 html,
1473 headTags: assetTags.headTags,
1474 bodyTags: assetTags.bodyTags,
1475 plugin: this,
1476 outputName,
1477 };
1478 return HtmlWebpackPlugin.getCompilationHooks(
1479 compilation,
1480 ).afterTemplateExecution.promise(pluginArgs);
1481 })
1482 .then(({ html, headTags, bodyTags }) => {
1483 return this.postProcessHtml(compiler, html, assetsInformationByGroups, {
1484 headTags,
1485 bodyTags,
1486 });
1487 });
1488
1489 const emitHtmlPromise = injectedHtmlPromise
1490 // Allow plugins to change the html after assets are injected
1491 .then((html) => {
1492 const pluginArgs = { html, plugin: this, outputName };
1493 return HtmlWebpackPlugin.getCompilationHooks(compilation)
1494 .beforeEmit.promise(pluginArgs)
1495 .then((result) => result.html);
1496 })
1497 .catch((err) => {
1498 // In case anything went wrong the promise is resolved
1499 // with the error message and an error is logged
1500 compilation.errors.push(prettyError(err, compiler.context).toString());
1501 return this.options.showErrors
1502 ? prettyError(err, compiler.context).toHtml()
1503 : "ERROR";
1504 })
1505 .then((html) => {
1506 const filename = outputName.replace(
1507 /\[templatehash([^\]]*)\]/g,
1508 require("util").deprecate(
1509 (match, options) => `[contenthash${options}]`,
1510 "[templatehash] is now [contenthash]",
1511 ),
1512 );
1513 const replacedFilename = this.replacePlaceholdersInFilename(
1514 compiler,
1515 filename,
1516 html,
1517 compilation,
1518 );
1519 const source = new compiler.webpack.sources.RawSource(html, false);
1520
1521 // Add the evaluated html code to the webpack assets
1522 compilation.emitAsset(
1523 replacedFilename.path,
1524 source,
1525 replacedFilename.info,
1526 );
1527 previousEmittedAssets.push({ name: replacedFilename.path, source });
1528
1529 return replacedFilename.path;
1530 })
1531 .then((finalOutputName) =>
1532 HtmlWebpackPlugin.getCompilationHooks(compilation)
1533 .afterEmit.promise({
1534 outputName: finalOutputName,
1535 plugin: this,
1536 })
1537 .catch((err) => {
1538 /** @type {Logger} */
1539 (this.logger).error(err);
1540 return null;
1541 })
1542 .then(() => null),
1543 );
1544
1545 // Once all files are added to the webpack compilation
1546 // let the webpack compiler continue
1547 emitHtmlPromise.then(() => {
1548 callback();
1549 });
1550 }
1551}
1552
1553/**
1554 * The default for options.templateParameter
1555 * Generate the template parameters
1556 *
1557 * Generate the template parameters for the template function
1558 * @param {Compilation} compilation
1559 * @param {AssetsInformationByGroups} assets
1560 * @param {{
1561 headTags: HtmlTagObject[],
1562 bodyTags: HtmlTagObject[]
1563 }} assetTags
1564 * @param {ProcessedHtmlWebpackOptions} options
1565 * @returns {TemplateParameter}
1566 */
1567function templateParametersGenerator(compilation, assets, assetTags, options) {
1568 return {
1569 compilation: compilation,
1570 webpackConfig: compilation.options,
1571 htmlWebpackPlugin: {
1572 tags: assetTags,
1573 files: assets,
1574 options: options,
1575 },
1576 };
1577}
1578
1579// Statics:
1580/**
1581 * The major version number of this plugin
1582 */
1583HtmlWebpackPlugin.version = 5;
1584
1585/**
1586 * A static helper to get the hooks for this plugin
1587 *
1588 * Usage: HtmlWebpackPlugin.getHooks(compilation).HOOK_NAME.tapAsync('YourPluginName', () => { ... });
1589 */
1590// TODO remove me in the next major release in favor getCompilationHooks
1591HtmlWebpackPlugin.getHooks = HtmlWebpackPlugin.getCompilationHooks;
1592HtmlWebpackPlugin.createHtmlTagObject = createHtmlTagObject;
1593
1594module.exports = HtmlWebpackPlugin;