You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1595 lines
49 KiB
1595 lines
49 KiB
4 weeks ago
|
// @ts-check
|
||
|
"use strict";
|
||
|
|
||
|
const promisify = require("util").promisify;
|
||
|
|
||
|
const vm = require("vm");
|
||
|
const fs = require("fs");
|
||
|
const _uniq = require("lodash/uniq");
|
||
|
const path = require("path");
|
||
|
const { CachedChildCompilation } = require("./lib/cached-child-compiler");
|
||
|
|
||
|
const {
|
||
|
createHtmlTagObject,
|
||
|
htmlTagObjectToString,
|
||
|
HtmlTagArray,
|
||
|
} = require("./lib/html-tags");
|
||
|
const prettyError = require("./lib/errors.js");
|
||
|
const chunkSorter = require("./lib/chunksorter.js");
|
||
|
const { AsyncSeriesWaterfallHook } = require("tapable");
|
||
|
|
||
|
/** @typedef {import("./typings").HtmlTagObject} HtmlTagObject */
|
||
|
/** @typedef {import("./typings").Options} HtmlWebpackOptions */
|
||
|
/** @typedef {import("./typings").ProcessedOptions} ProcessedHtmlWebpackOptions */
|
||
|
/** @typedef {import("./typings").TemplateParameter} TemplateParameter */
|
||
|
/** @typedef {import("webpack").Compiler} Compiler */
|
||
|
/** @typedef {import("webpack").Compilation} Compilation */
|
||
|
/** @typedef {Required<Compilation["outputOptions"]["publicPath"]>} PublicPath */
|
||
|
/** @typedef {ReturnType<Compiler["getInfrastructureLogger"]>} Logger */
|
||
|
/** @typedef {Compilation["entrypoints"] extends Map<string, infer I> ? I : never} Entrypoint */
|
||
|
/** @typedef {Array<{ name: string, source: import('webpack').sources.Source, info?: import('webpack').AssetInfo }>} PreviousEmittedAssets */
|
||
|
/** @typedef {{ publicPath: string, js: Array<string>, css: Array<string>, manifest?: string, favicon?: string }} AssetsInformationByGroups */
|
||
|
/** @typedef {import("./typings").Hooks} HtmlWebpackPluginHooks */
|
||
|
/**
|
||
|
* @type {WeakMap<Compilation, HtmlWebpackPluginHooks>}}
|
||
|
*/
|
||
|
const compilationHooksMap = new WeakMap();
|
||
|
|
||
|
class HtmlWebpackPlugin {
|
||
|
// The following is the API definition for all available hooks
|
||
|
// For the TypeScript definition, see the Hooks type in typings.d.ts
|
||
|
/**
|
||
|
beforeAssetTagGeneration:
|
||
|
AsyncSeriesWaterfallHook<{
|
||
|
assets: {
|
||
|
publicPath: string,
|
||
|
js: Array<string>,
|
||
|
css: Array<string>,
|
||
|
favicon?: string | undefined,
|
||
|
manifest?: string | undefined
|
||
|
},
|
||
|
outputName: string,
|
||
|
plugin: HtmlWebpackPlugin
|
||
|
}>,
|
||
|
alterAssetTags:
|
||
|
AsyncSeriesWaterfallHook<{
|
||
|
assetTags: {
|
||
|
scripts: Array<HtmlTagObject>,
|
||
|
styles: Array<HtmlTagObject>,
|
||
|
meta: Array<HtmlTagObject>,
|
||
|
},
|
||
|
publicPath: string,
|
||
|
outputName: string,
|
||
|
plugin: HtmlWebpackPlugin
|
||
|
}>,
|
||
|
alterAssetTagGroups:
|
||
|
AsyncSeriesWaterfallHook<{
|
||
|
headTags: Array<HtmlTagObject | HtmlTagObject>,
|
||
|
bodyTags: Array<HtmlTagObject | HtmlTagObject>,
|
||
|
publicPath: string,
|
||
|
outputName: string,
|
||
|
plugin: HtmlWebpackPlugin
|
||
|
}>,
|
||
|
afterTemplateExecution:
|
||
|
AsyncSeriesWaterfallHook<{
|
||
|
html: string,
|
||
|
headTags: Array<HtmlTagObject | HtmlTagObject>,
|
||
|
bodyTags: Array<HtmlTagObject | HtmlTagObject>,
|
||
|
outputName: string,
|
||
|
plugin: HtmlWebpackPlugin,
|
||
|
}>,
|
||
|
beforeEmit:
|
||
|
AsyncSeriesWaterfallHook<{
|
||
|
html: string,
|
||
|
outputName: string,
|
||
|
plugin: HtmlWebpackPlugin,
|
||
|
}>,
|
||
|
afterEmit:
|
||
|
AsyncSeriesWaterfallHook<{
|
||
|
outputName: string,
|
||
|
plugin: HtmlWebpackPlugin
|
||
|
}>
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* Returns all public hooks of the html webpack plugin for the given compilation
|
||
|
*
|
||
|
* @param {Compilation} compilation
|
||
|
* @returns {HtmlWebpackPluginHooks}
|
||
|
*/
|
||
|
static getCompilationHooks(compilation) {
|
||
|
let hooks = compilationHooksMap.get(compilation);
|
||
|
|
||
|
if (!hooks) {
|
||
|
hooks = {
|
||
|
beforeAssetTagGeneration: new AsyncSeriesWaterfallHook(["pluginArgs"]),
|
||
|
alterAssetTags: new AsyncSeriesWaterfallHook(["pluginArgs"]),
|
||
|
alterAssetTagGroups: new AsyncSeriesWaterfallHook(["pluginArgs"]),
|
||
|
afterTemplateExecution: new AsyncSeriesWaterfallHook(["pluginArgs"]),
|
||
|
beforeEmit: new AsyncSeriesWaterfallHook(["pluginArgs"]),
|
||
|
afterEmit: new AsyncSeriesWaterfallHook(["pluginArgs"]),
|
||
|
};
|
||
|
compilationHooksMap.set(compilation, hooks);
|
||
|
}
|
||
|
|
||
|
return hooks;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {HtmlWebpackOptions} [options]
|
||
|
*/
|
||
|
constructor(options) {
|
||
|
/** @type {HtmlWebpackOptions} */
|
||
|
// TODO remove me in the next major release
|
||
|
this.userOptions = options || {};
|
||
|
this.version = HtmlWebpackPlugin.version;
|
||
|
|
||
|
// Default options
|
||
|
/** @type {ProcessedHtmlWebpackOptions} */
|
||
|
const defaultOptions = {
|
||
|
template: "auto",
|
||
|
templateContent: false,
|
||
|
templateParameters: templateParametersGenerator,
|
||
|
filename: "index.html",
|
||
|
publicPath:
|
||
|
this.userOptions.publicPath === undefined
|
||
|
? "auto"
|
||
|
: this.userOptions.publicPath,
|
||
|
hash: false,
|
||
|
inject: this.userOptions.scriptLoading === "blocking" ? "body" : "head",
|
||
|
scriptLoading: "defer",
|
||
|
compile: true,
|
||
|
favicon: false,
|
||
|
minify: "auto",
|
||
|
cache: true,
|
||
|
showErrors: true,
|
||
|
chunks: "all",
|
||
|
excludeChunks: [],
|
||
|
chunksSortMode: "auto",
|
||
|
meta: {},
|
||
|
base: false,
|
||
|
title: "Webpack App",
|
||
|
xhtml: false,
|
||
|
};
|
||
|
|
||
|
/** @type {ProcessedHtmlWebpackOptions} */
|
||
|
this.options = Object.assign(defaultOptions, this.userOptions);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
*
|
||
|
* @param {Compiler} compiler
|
||
|
* @returns {void}
|
||
|
*/
|
||
|
apply(compiler) {
|
||
|
this.logger = compiler.getInfrastructureLogger("HtmlWebpackPlugin");
|
||
|
|
||
|
const options = this.options;
|
||
|
|
||
|
options.template = this.getTemplatePath(
|
||
|
this.options.template,
|
||
|
compiler.context,
|
||
|
);
|
||
|
|
||
|
// Assert correct option spelling
|
||
|
if (
|
||
|
options.scriptLoading !== "defer" &&
|
||
|
options.scriptLoading !== "blocking" &&
|
||
|
options.scriptLoading !== "module" &&
|
||
|
options.scriptLoading !== "systemjs-module"
|
||
|
) {
|
||
|
/** @type {Logger} */
|
||
|
(this.logger).error(
|
||
|
'The "scriptLoading" option need to be set to "defer", "blocking" or "module" or "systemjs-module"',
|
||
|
);
|
||
|
}
|
||
|
|
||
|
if (
|
||
|
options.inject !== true &&
|
||
|
options.inject !== false &&
|
||
|
options.inject !== "head" &&
|
||
|
options.inject !== "body"
|
||
|
) {
|
||
|
/** @type {Logger} */
|
||
|
(this.logger).error(
|
||
|
'The `inject` option needs to be set to true, false, "head" or "body',
|
||
|
);
|
||
|
}
|
||
|
|
||
|
if (
|
||
|
this.options.templateParameters !== false &&
|
||
|
typeof this.options.templateParameters !== "function" &&
|
||
|
typeof this.options.templateParameters !== "object"
|
||
|
) {
|
||
|
/** @type {Logger} */
|
||
|
(this.logger).error(
|
||
|
"The `templateParameters` has to be either a function or an object or false",
|
||
|
);
|
||
|
}
|
||
|
|
||
|
// Default metaOptions if no template is provided
|
||
|
if (
|
||
|
!this.userOptions.template &&
|
||
|
options.templateContent === false &&
|
||
|
options.meta
|
||
|
) {
|
||
|
options.meta = Object.assign(
|
||
|
{},
|
||
|
options.meta,
|
||
|
{
|
||
|
// TODO remove in the next major release
|
||
|
// From https://developer.mozilla.org/en-US/docs/Mozilla/Mobile/Viewport_meta_tag
|
||
|
viewport: "width=device-width, initial-scale=1",
|
||
|
},
|
||
|
this.userOptions.meta,
|
||
|
);
|
||
|
}
|
||
|
|
||
|
// entryName to fileName conversion function
|
||
|
const userOptionFilename =
|
||
|
this.userOptions.filename || this.options.filename;
|
||
|
const filenameFunction =
|
||
|
typeof userOptionFilename === "function"
|
||
|
? userOptionFilename
|
||
|
: // Replace '[name]' with entry name
|
||
|
(entryName) => userOptionFilename.replace(/\[name\]/g, entryName);
|
||
|
|
||
|
/** output filenames for the given entry names */
|
||
|
const entryNames = Object.keys(compiler.options.entry);
|
||
|
const outputFileNames = new Set(
|
||
|
(entryNames.length ? entryNames : ["main"]).map(filenameFunction),
|
||
|
);
|
||
|
|
||
|
// Hook all options into the webpack compiler
|
||
|
outputFileNames.forEach((outputFileName) => {
|
||
|
// Instance variables to keep caching information for multiple builds
|
||
|
const assetJson = { value: undefined };
|
||
|
/**
|
||
|
* store the previous generated asset to emit them even if the content did not change
|
||
|
* to support watch mode for third party plugins like the clean-webpack-plugin or the compression plugin
|
||
|
* @type {PreviousEmittedAssets}
|
||
|
*/
|
||
|
const previousEmittedAssets = [];
|
||
|
|
||
|
// Inject child compiler plugin
|
||
|
const childCompilerPlugin = new CachedChildCompilation(compiler);
|
||
|
|
||
|
if (!this.options.templateContent) {
|
||
|
childCompilerPlugin.addEntry(this.options.template);
|
||
|
}
|
||
|
|
||
|
// convert absolute filename into relative so that webpack can
|
||
|
// generate it at correct location
|
||
|
let filename = outputFileName;
|
||
|
|
||
|
if (path.resolve(filename) === path.normalize(filename)) {
|
||
|
const outputPath =
|
||
|
/** @type {string} - Once initialized the path is always a string */ (
|
||
|
compiler.options.output.path
|
||
|
);
|
||
|
|
||
|
filename = path.relative(outputPath, filename);
|
||
|
}
|
||
|
|
||
|
compiler.hooks.thisCompilation.tap(
|
||
|
"HtmlWebpackPlugin",
|
||
|
/**
|
||
|
* Hook into the webpack compilation
|
||
|
* @param {Compilation} compilation
|
||
|
*/
|
||
|
(compilation) => {
|
||
|
compilation.hooks.processAssets.tapAsync(
|
||
|
{
|
||
|
name: "HtmlWebpackPlugin",
|
||
|
stage:
|
||
|
/**
|
||
|
* Generate the html after minification and dev tooling is done
|
||
|
*/
|
||
|
compiler.webpack.Compilation
|
||
|
.PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE,
|
||
|
},
|
||
|
/**
|
||
|
* Hook into the process assets hook
|
||
|
* @param {any} _
|
||
|
* @param {(err?: Error) => void} callback
|
||
|
*/
|
||
|
(_, callback) => {
|
||
|
this.generateHTML(
|
||
|
compiler,
|
||
|
compilation,
|
||
|
filename,
|
||
|
childCompilerPlugin,
|
||
|
previousEmittedAssets,
|
||
|
assetJson,
|
||
|
callback,
|
||
|
);
|
||
|
},
|
||
|
);
|
||
|
},
|
||
|
);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Helper to return the absolute template path with a fallback loader
|
||
|
*
|
||
|
* @private
|
||
|
* @param {string} template The path to the template e.g. './index.html'
|
||
|
* @param {string} context The webpack base resolution path for relative paths e.g. process.cwd()
|
||
|
*/
|
||
|
getTemplatePath(template, context) {
|
||
|
if (template === "auto") {
|
||
|
template = path.resolve(context, "src/index.ejs");
|
||
|
if (!fs.existsSync(template)) {
|
||
|
template = path.join(__dirname, "default_index.ejs");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// If the template doesn't use a loader use the lodash template loader
|
||
|
if (template.indexOf("!") === -1) {
|
||
|
template =
|
||
|
require.resolve("./lib/loader.js") +
|
||
|
"!" +
|
||
|
path.resolve(context, template);
|
||
|
}
|
||
|
|
||
|
// Resolve template path
|
||
|
return template.replace(
|
||
|
/([!])([^/\\][^!?]+|[^/\\!?])($|\?[^!?\n]+$)/,
|
||
|
(match, prefix, filepath, postfix) =>
|
||
|
prefix + path.resolve(filepath) + postfix,
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return all chunks from the compilation result which match the exclude and include filters
|
||
|
*
|
||
|
* @private
|
||
|
* @param {any} chunks
|
||
|
* @param {string[]|'all'} includedChunks
|
||
|
* @param {string[]} excludedChunks
|
||
|
*/
|
||
|
filterEntryChunks(chunks, includedChunks, excludedChunks) {
|
||
|
return chunks.filter((chunkName) => {
|
||
|
// Skip if the chunks should be filtered and the given chunk was not added explicity
|
||
|
if (
|
||
|
Array.isArray(includedChunks) &&
|
||
|
includedChunks.indexOf(chunkName) === -1
|
||
|
) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// Skip if the chunks should be filtered and the given chunk was excluded explicity
|
||
|
if (
|
||
|
Array.isArray(excludedChunks) &&
|
||
|
excludedChunks.indexOf(chunkName) !== -1
|
||
|
) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// Add otherwise
|
||
|
return true;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Helper to sort chunks
|
||
|
*
|
||
|
* @private
|
||
|
* @param {string[]} entryNames
|
||
|
* @param {string|((entryNameA: string, entryNameB: string) => number)} sortMode
|
||
|
* @param {Compilation} compilation
|
||
|
*/
|
||
|
sortEntryChunks(entryNames, sortMode, compilation) {
|
||
|
// Custom function
|
||
|
if (typeof sortMode === "function") {
|
||
|
return entryNames.sort(sortMode);
|
||
|
}
|
||
|
// Check if the given sort mode is a valid chunkSorter sort mode
|
||
|
if (typeof chunkSorter[sortMode] !== "undefined") {
|
||
|
return chunkSorter[sortMode](entryNames, compilation, this.options);
|
||
|
}
|
||
|
throw new Error('"' + sortMode + '" is not a valid chunk sort mode');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Encode each path component using `encodeURIComponent` as files can contain characters
|
||
|
* which needs special encoding in URLs like `+ `.
|
||
|
*
|
||
|
* Valid filesystem characters which need to be encoded for urls:
|
||
|
*
|
||
|
* # pound, % percent, & ampersand, { left curly bracket, } right curly bracket,
|
||
|
* \ back slash, < left angle bracket, > right angle bracket, * asterisk, ? question mark,
|
||
|
* blank spaces, $ dollar sign, ! exclamation point, ' single quotes, " double quotes,
|
||
|
* : colon, @ at sign, + plus sign, ` backtick, | pipe, = equal sign
|
||
|
*
|
||
|
* However the query string must not be encoded:
|
||
|
*
|
||
|
* fo:demonstration-path/very fancy+name.js?path=/home?value=abc&value=def#zzz
|
||
|
* ^ ^ ^ ^ ^ ^ ^ ^^ ^ ^ ^ ^ ^
|
||
|
* | | | | | | | || | | | | |
|
||
|
* encoded | | encoded | | || | | | | |
|
||
|
* ignored ignored ignored ignored ignored
|
||
|
*
|
||
|
* @private
|
||
|
* @param {string} filePath
|
||
|
*/
|
||
|
urlencodePath(filePath) {
|
||
|
// People use the filepath in quite unexpected ways.
|
||
|
// Try to extract the first querystring of the url:
|
||
|
//
|
||
|
// some+path/demo.html?value=abc?def
|
||
|
//
|
||
|
const queryStringStart = filePath.indexOf("?");
|
||
|
const urlPath =
|
||
|
queryStringStart === -1 ? filePath : filePath.substr(0, queryStringStart);
|
||
|
const queryString = filePath.substr(urlPath.length);
|
||
|
// Encode all parts except '/' which are not part of the querystring:
|
||
|
const encodedUrlPath = urlPath.split("/").map(encodeURIComponent).join("/");
|
||
|
return encodedUrlPath + queryString;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Appends a cache busting hash to the query string of the url
|
||
|
* E.g. http://localhost:8080/ -> http://localhost:8080/?50c9096ba6183fd728eeb065a26ec175
|
||
|
*
|
||
|
* @private
|
||
|
* @param {string | undefined} url
|
||
|
* @param {string} hash
|
||
|
*/
|
||
|
appendHash(url, hash) {
|
||
|
if (!url) {
|
||
|
return url;
|
||
|
}
|
||
|
|
||
|
return url + (url.indexOf("?") === -1 ? "?" : "&") + hash;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Generate the relative or absolute base url to reference images, css, and javascript files
|
||
|
* from within the html file - the publicPath
|
||
|
*
|
||
|
* @private
|
||
|
* @param {Compilation} compilation
|
||
|
* @param {string} filename
|
||
|
* @param {string | 'auto'} customPublicPath
|
||
|
* @returns {string}
|
||
|
*/
|
||
|
getPublicPath(compilation, filename, customPublicPath) {
|
||
|
/**
|
||
|
* @type {string} the configured public path to the asset root
|
||
|
* if a path publicPath is set in the current webpack config use it otherwise
|
||
|
* fallback to a relative path
|
||
|
*/
|
||
|
const webpackPublicPath = compilation.getAssetPath(
|
||
|
/** @type {NonNullable<Compilation["outputOptions"]["publicPath"]>} */ (
|
||
|
compilation.outputOptions.publicPath
|
||
|
),
|
||
|
{ hash: compilation.hash },
|
||
|
);
|
||
|
// Webpack 5 introduced "auto" as default value
|
||
|
const isPublicPathDefined = webpackPublicPath !== "auto";
|
||
|
|
||
|
let publicPath =
|
||
|
// If the html-webpack-plugin options contain a custom public path unset it
|
||
|
customPublicPath !== "auto"
|
||
|
? customPublicPath
|
||
|
: isPublicPathDefined
|
||
|
? // If a hard coded public path exists use it
|
||
|
webpackPublicPath
|
||
|
: // If no public path was set get a relative url path
|
||
|
path
|
||
|
.relative(
|
||
|
path.resolve(
|
||
|
/** @type {string} */ (compilation.options.output.path),
|
||
|
path.dirname(filename),
|
||
|
),
|
||
|
/** @type {string} */ (compilation.options.output.path),
|
||
|
)
|
||
|
.split(path.sep)
|
||
|
.join("/");
|
||
|
|
||
|
if (publicPath.length && publicPath.substr(-1, 1) !== "/") {
|
||
|
publicPath += "/";
|
||
|
}
|
||
|
|
||
|
return publicPath;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* The getAssetsForHTML extracts the asset information of a webpack compilation for all given entry names.
|
||
|
*
|
||
|
* @private
|
||
|
* @param {Compilation} compilation
|
||
|
* @param {string} outputName
|
||
|
* @param {string[]} entryNames
|
||
|
* @returns {AssetsInformationByGroups}
|
||
|
*/
|
||
|
getAssetsInformationByGroups(compilation, outputName, entryNames) {
|
||
|
/** The public path used inside the html file */
|
||
|
const publicPath = this.getPublicPath(
|
||
|
compilation,
|
||
|
outputName,
|
||
|
this.options.publicPath,
|
||
|
);
|
||
|
/**
|
||
|
* @type {AssetsInformationByGroups}
|
||
|
*/
|
||
|
const assets = {
|
||
|
// The public path
|
||
|
publicPath,
|
||
|
// Will contain all js and mjs files
|
||
|
js: [],
|
||
|
// Will contain all css files
|
||
|
css: [],
|
||
|
// Will contain the html5 appcache manifest files if it exists
|
||
|
manifest: Object.keys(compilation.assets).find(
|
||
|
(assetFile) => path.extname(assetFile) === ".appcache",
|
||
|
),
|
||
|
// Favicon
|
||
|
favicon: undefined,
|
||
|
};
|
||
|
|
||
|
// Append a hash for cache busting
|
||
|
if (this.options.hash && assets.manifest) {
|
||
|
assets.manifest = this.appendHash(
|
||
|
assets.manifest,
|
||
|
/** @type {string} */ (compilation.hash),
|
||
|
);
|
||
|
}
|
||
|
|
||
|
// Extract paths to .js, .mjs and .css files from the current compilation
|
||
|
const entryPointPublicPathMap = {};
|
||
|
const extensionRegexp = /\.(css|js|mjs)(\?|$)/;
|
||
|
|
||
|
for (let i = 0; i < entryNames.length; i++) {
|
||
|
const entryName = entryNames[i];
|
||
|
/** entryPointUnfilteredFiles - also includes hot module update files */
|
||
|
const entryPointUnfilteredFiles = /** @type {Entrypoint} */ (
|
||
|
compilation.entrypoints.get(entryName)
|
||
|
).getFiles();
|
||
|
const entryPointFiles = entryPointUnfilteredFiles.filter((chunkFile) => {
|
||
|
const asset = compilation.getAsset(chunkFile);
|
||
|
|
||
|
if (!asset) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
// Prevent hot-module files from being included:
|
||
|
const assetMetaInformation = asset.info || {};
|
||
|
|
||
|
return !(
|
||
|
assetMetaInformation.hotModuleReplacement ||
|
||
|
assetMetaInformation.development
|
||
|
);
|
||
|
});
|
||
|
// Prepend the publicPath and append the hash depending on the
|
||
|
// webpack.output.publicPath and hashOptions
|
||
|
// E.g. bundle.js -> /bundle.js?hash
|
||
|
const entryPointPublicPaths = entryPointFiles.map((chunkFile) => {
|
||
|
const entryPointPublicPath = publicPath + this.urlencodePath(chunkFile);
|
||
|
return this.options.hash
|
||
|
? this.appendHash(
|
||
|
entryPointPublicPath,
|
||
|
/** @type {string} */ (compilation.hash),
|
||
|
)
|
||
|
: entryPointPublicPath;
|
||
|
});
|
||
|
|
||
|
entryPointPublicPaths.forEach((entryPointPublicPath) => {
|
||
|
const extMatch = extensionRegexp.exec(
|
||
|
/** @type {string} */ (entryPointPublicPath),
|
||
|
);
|
||
|
|
||
|
// Skip if the public path is not a .css, .mjs or .js file
|
||
|
if (!extMatch) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Skip if this file is already known
|
||
|
// (e.g. because of common chunk optimizations)
|
||
|
if (entryPointPublicPathMap[entryPointPublicPath]) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
entryPointPublicPathMap[entryPointPublicPath] = true;
|
||
|
|
||
|
// ext will contain .js or .css, because .mjs recognizes as .js
|
||
|
const ext = extMatch[1] === "mjs" ? "js" : extMatch[1];
|
||
|
|
||
|
assets[ext].push(entryPointPublicPath);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
return assets;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Once webpack is done with compiling the template into a NodeJS code this function
|
||
|
* evaluates it to generate the html result
|
||
|
*
|
||
|
* The evaluateCompilationResult is only a class function to allow spying during testing.
|
||
|
* Please change that in a further refactoring
|
||
|
*
|
||
|
* @param {string} source
|
||
|
* @param {string} publicPath
|
||
|
* @param {string} templateFilename
|
||
|
* @returns {Promise<string | (() => string | Promise<string>)>}
|
||
|
*/
|
||
|
evaluateCompilationResult(source, publicPath, templateFilename) {
|
||
|
if (!source) {
|
||
|
return Promise.reject(
|
||
|
new Error("The child compilation didn't provide a result"),
|
||
|
);
|
||
|
}
|
||
|
|
||
|
// The LibraryTemplatePlugin stores the template result in a local variable.
|
||
|
// By adding it to the end the value gets extracted during evaluation
|
||
|
if (source.indexOf("HTML_WEBPACK_PLUGIN_RESULT") >= 0) {
|
||
|
source += ";\nHTML_WEBPACK_PLUGIN_RESULT";
|
||
|
}
|
||
|
|
||
|
const templateWithoutLoaders = templateFilename
|
||
|
.replace(/^.+!/, "")
|
||
|
.replace(/\?.+$/, "");
|
||
|
const vmContext = vm.createContext({
|
||
|
...global,
|
||
|
HTML_WEBPACK_PLUGIN: true,
|
||
|
require: require,
|
||
|
htmlWebpackPluginPublicPath: publicPath,
|
||
|
__filename: templateWithoutLoaders,
|
||
|
__dirname: path.dirname(templateWithoutLoaders),
|
||
|
AbortController: global.AbortController,
|
||
|
AbortSignal: global.AbortSignal,
|
||
|
Blob: global.Blob,
|
||
|
Buffer: global.Buffer,
|
||
|
ByteLengthQueuingStrategy: global.ByteLengthQueuingStrategy,
|
||
|
BroadcastChannel: global.BroadcastChannel,
|
||
|
CompressionStream: global.CompressionStream,
|
||
|
CountQueuingStrategy: global.CountQueuingStrategy,
|
||
|
Crypto: global.Crypto,
|
||
|
CryptoKey: global.CryptoKey,
|
||
|
CustomEvent: global.CustomEvent,
|
||
|
DecompressionStream: global.DecompressionStream,
|
||
|
Event: global.Event,
|
||
|
EventTarget: global.EventTarget,
|
||
|
File: global.File,
|
||
|
FormData: global.FormData,
|
||
|
Headers: global.Headers,
|
||
|
MessageChannel: global.MessageChannel,
|
||
|
MessageEvent: global.MessageEvent,
|
||
|
MessagePort: global.MessagePort,
|
||
|
PerformanceEntry: global.PerformanceEntry,
|
||
|
PerformanceMark: global.PerformanceMark,
|
||
|
PerformanceMeasure: global.PerformanceMeasure,
|
||
|
PerformanceObserver: global.PerformanceObserver,
|
||
|
PerformanceObserverEntryList: global.PerformanceObserverEntryList,
|
||
|
PerformanceResourceTiming: global.PerformanceResourceTiming,
|
||
|
ReadableByteStreamController: global.ReadableByteStreamController,
|
||
|
ReadableStream: global.ReadableStream,
|
||
|
ReadableStreamBYOBReader: global.ReadableStreamBYOBReader,
|
||
|
ReadableStreamBYOBRequest: global.ReadableStreamBYOBRequest,
|
||
|
ReadableStreamDefaultController: global.ReadableStreamDefaultController,
|
||
|
ReadableStreamDefaultReader: global.ReadableStreamDefaultReader,
|
||
|
Response: global.Response,
|
||
|
Request: global.Request,
|
||
|
SubtleCrypto: global.SubtleCrypto,
|
||
|
DOMException: global.DOMException,
|
||
|
TextDecoder: global.TextDecoder,
|
||
|
TextDecoderStream: global.TextDecoderStream,
|
||
|
TextEncoder: global.TextEncoder,
|
||
|
TextEncoderStream: global.TextEncoderStream,
|
||
|
TransformStream: global.TransformStream,
|
||
|
TransformStreamDefaultController: global.TransformStreamDefaultController,
|
||
|
URL: global.URL,
|
||
|
URLSearchParams: global.URLSearchParams,
|
||
|
WebAssembly: global.WebAssembly,
|
||
|
WritableStream: global.WritableStream,
|
||
|
WritableStreamDefaultController: global.WritableStreamDefaultController,
|
||
|
WritableStreamDefaultWriter: global.WritableStreamDefaultWriter,
|
||
|
});
|
||
|
|
||
|
const vmScript = new vm.Script(source, {
|
||
|
filename: templateWithoutLoaders,
|
||
|
});
|
||
|
|
||
|
// Evaluate code and cast to string
|
||
|
let newSource;
|
||
|
|
||
|
try {
|
||
|
newSource = vmScript.runInContext(vmContext);
|
||
|
} catch (e) {
|
||
|
return Promise.reject(e);
|
||
|
}
|
||
|
|
||
|
if (
|
||
|
typeof newSource === "object" &&
|
||
|
newSource.__esModule &&
|
||
|
newSource.default !== undefined
|
||
|
) {
|
||
|
newSource = newSource.default;
|
||
|
}
|
||
|
|
||
|
return typeof newSource === "string" || typeof newSource === "function"
|
||
|
? Promise.resolve(newSource)
|
||
|
: Promise.reject(
|
||
|
new Error(
|
||
|
'The loader "' + templateWithoutLoaders + "\" didn't return html.",
|
||
|
),
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Add toString methods for easier rendering inside the template
|
||
|
*
|
||
|
* @private
|
||
|
* @param {Array<HtmlTagObject>} assetTagGroup
|
||
|
* @returns {Array<HtmlTagObject>}
|
||
|
*/
|
||
|
prepareAssetTagGroupForRendering(assetTagGroup) {
|
||
|
const xhtml = this.options.xhtml;
|
||
|
return HtmlTagArray.from(
|
||
|
assetTagGroup.map((assetTag) => {
|
||
|
const copiedAssetTag = Object.assign({}, assetTag);
|
||
|
copiedAssetTag.toString = function () {
|
||
|
return htmlTagObjectToString(this, xhtml);
|
||
|
};
|
||
|
return copiedAssetTag;
|
||
|
}),
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Generate the template parameters for the template function
|
||
|
*
|
||
|
* @private
|
||
|
* @param {Compilation} compilation
|
||
|
* @param {AssetsInformationByGroups} assetsInformationByGroups
|
||
|
* @param {{
|
||
|
headTags: HtmlTagObject[],
|
||
|
bodyTags: HtmlTagObject[]
|
||
|
}} assetTags
|
||
|
* @returns {Promise<{[key: any]: any}>}
|
||
|
*/
|
||
|
getTemplateParameters(compilation, assetsInformationByGroups, assetTags) {
|
||
|
const templateParameters = this.options.templateParameters;
|
||
|
|
||
|
if (templateParameters === false) {
|
||
|
return Promise.resolve({});
|
||
|
}
|
||
|
|
||
|
if (
|
||
|
typeof templateParameters !== "function" &&
|
||
|
typeof templateParameters !== "object"
|
||
|
) {
|
||
|
throw new Error(
|
||
|
"templateParameters has to be either a function or an object",
|
||
|
);
|
||
|
}
|
||
|
|
||
|
const templateParameterFunction =
|
||
|
typeof templateParameters === "function"
|
||
|
? // A custom function can overwrite the entire template parameter preparation
|
||
|
templateParameters
|
||
|
: // If the template parameters is an object merge it with the default values
|
||
|
(compilation, assetsInformationByGroups, assetTags, options) =>
|
||
|
Object.assign(
|
||
|
{},
|
||
|
templateParametersGenerator(
|
||
|
compilation,
|
||
|
assetsInformationByGroups,
|
||
|
assetTags,
|
||
|
options,
|
||
|
),
|
||
|
templateParameters,
|
||
|
);
|
||
|
const preparedAssetTags = {
|
||
|
headTags: this.prepareAssetTagGroupForRendering(assetTags.headTags),
|
||
|
bodyTags: this.prepareAssetTagGroupForRendering(assetTags.bodyTags),
|
||
|
};
|
||
|
return Promise.resolve().then(() =>
|
||
|
templateParameterFunction(
|
||
|
compilation,
|
||
|
assetsInformationByGroups,
|
||
|
preparedAssetTags,
|
||
|
this.options,
|
||
|
),
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* This function renders the actual html by executing the template function
|
||
|
*
|
||
|
* @private
|
||
|
* @param {(templateParameters) => string | Promise<string>} templateFunction
|
||
|
* @param {AssetsInformationByGroups} assetsInformationByGroups
|
||
|
* @param {{
|
||
|
headTags: HtmlTagObject[],
|
||
|
bodyTags: HtmlTagObject[]
|
||
|
}} assetTags
|
||
|
* @param {Compilation} compilation
|
||
|
* @returns Promise<string>
|
||
|
*/
|
||
|
executeTemplate(
|
||
|
templateFunction,
|
||
|
assetsInformationByGroups,
|
||
|
assetTags,
|
||
|
compilation,
|
||
|
) {
|
||
|
// Template processing
|
||
|
const templateParamsPromise = this.getTemplateParameters(
|
||
|
compilation,
|
||
|
assetsInformationByGroups,
|
||
|
assetTags,
|
||
|
);
|
||
|
|
||
|
return templateParamsPromise.then((templateParams) => {
|
||
|
try {
|
||
|
// If html is a promise return the promise
|
||
|
// If html is a string turn it into a promise
|
||
|
return templateFunction(templateParams);
|
||
|
} catch (e) {
|
||
|
// @ts-ignore
|
||
|
compilation.errors.push(new Error("Template execution failed: " + e));
|
||
|
return Promise.reject(e);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Html Post processing
|
||
|
*
|
||
|
* @private
|
||
|
* @param {Compiler} compiler The compiler instance
|
||
|
* @param {any} originalHtml The input html
|
||
|
* @param {AssetsInformationByGroups} assetsInformationByGroups
|
||
|
* @param {{headTags: HtmlTagObject[], bodyTags: HtmlTagObject[]}} assetTags The asset tags to inject
|
||
|
* @returns {Promise<string>}
|
||
|
*/
|
||
|
postProcessHtml(
|
||
|
compiler,
|
||
|
originalHtml,
|
||
|
assetsInformationByGroups,
|
||
|
assetTags,
|
||
|
) {
|
||
|
let html = originalHtml;
|
||
|
|
||
|
if (typeof html !== "string") {
|
||
|
return Promise.reject(
|
||
|
new Error(
|
||
|
"Expected html to be a string but got " + JSON.stringify(html),
|
||
|
),
|
||
|
);
|
||
|
}
|
||
|
|
||
|
if (this.options.inject) {
|
||
|
const htmlRegExp = /(<html[^>]*>)/i;
|
||
|
const headRegExp = /(<\/head\s*>)/i;
|
||
|
const bodyRegExp = /(<\/body\s*>)/i;
|
||
|
const metaViewportRegExp = /<meta[^>]+name=["']viewport["'][^>]*>/i;
|
||
|
const body = assetTags.bodyTags.map((assetTagObject) =>
|
||
|
htmlTagObjectToString(assetTagObject, this.options.xhtml),
|
||
|
);
|
||
|
const head = assetTags.headTags
|
||
|
.filter((item) => {
|
||
|
if (
|
||
|
item.tagName === "meta" &&
|
||
|
item.attributes &&
|
||
|
item.attributes.name === "viewport" &&
|
||
|
metaViewportRegExp.test(html)
|
||
|
) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
})
|
||
|
.map((assetTagObject) =>
|
||
|
htmlTagObjectToString(assetTagObject, this.options.xhtml),
|
||
|
);
|
||
|
|
||
|
if (body.length) {
|
||
|
if (bodyRegExp.test(html)) {
|
||
|
// Append assets to body element
|
||
|
html = html.replace(bodyRegExp, (match) => body.join("") + match);
|
||
|
} else {
|
||
|
// Append scripts to the end of the file if no <body> element exists:
|
||
|
html += body.join("");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (head.length) {
|
||
|
// Create a head tag if none exists
|
||
|
if (!headRegExp.test(html)) {
|
||
|
if (!htmlRegExp.test(html)) {
|
||
|
html = "<head></head>" + html;
|
||
|
} else {
|
||
|
html = html.replace(htmlRegExp, (match) => match + "<head></head>");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Append assets to head element
|
||
|
html = html.replace(headRegExp, (match) => head.join("") + match);
|
||
|
}
|
||
|
|
||
|
// Inject manifest into the opening html tag
|
||
|
if (assetsInformationByGroups.manifest) {
|
||
|
html = html.replace(/(<html[^>]*)(>)/i, (match, start, end) => {
|
||
|
// Append the manifest only if no manifest was specified
|
||
|
if (/\smanifest\s*=/.test(match)) {
|
||
|
return match;
|
||
|
}
|
||
|
return (
|
||
|
start +
|
||
|
' manifest="' +
|
||
|
assetsInformationByGroups.manifest +
|
||
|
'"' +
|
||
|
end
|
||
|
);
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// TODO avoid this logic and use https://github.com/webpack-contrib/html-minimizer-webpack-plugin under the hood in the next major version
|
||
|
// Check if webpack is running in production mode
|
||
|
// @see https://github.com/webpack/webpack/blob/3366421f1784c449f415cda5930a8e445086f688/lib/WebpackOptionsDefaulter.js#L12-L14
|
||
|
const isProductionLikeMode =
|
||
|
compiler.options.mode === "production" || !compiler.options.mode;
|
||
|
const needMinify =
|
||
|
this.options.minify === true ||
|
||
|
typeof this.options.minify === "object" ||
|
||
|
(this.options.minify === "auto" && isProductionLikeMode);
|
||
|
|
||
|
if (!needMinify) {
|
||
|
return Promise.resolve(html);
|
||
|
}
|
||
|
|
||
|
const minifyOptions =
|
||
|
typeof this.options.minify === "object"
|
||
|
? this.options.minify
|
||
|
: {
|
||
|
// https://www.npmjs.com/package/html-minifier-terser#options-quick-reference
|
||
|
collapseWhitespace: true,
|
||
|
keepClosingSlash: true,
|
||
|
removeComments: true,
|
||
|
removeRedundantAttributes: true,
|
||
|
removeScriptTypeAttributes: true,
|
||
|
removeStyleLinkTypeAttributes: true,
|
||
|
useShortDoctype: true,
|
||
|
};
|
||
|
|
||
|
try {
|
||
|
html = require("html-minifier-terser").minify(html, minifyOptions);
|
||
|
} catch (e) {
|
||
|
const isParseError = String(e.message).indexOf("Parse Error") === 0;
|
||
|
|
||
|
if (isParseError) {
|
||
|
e.message =
|
||
|
"html-webpack-plugin could not minify the generated output.\n" +
|
||
|
"In production mode the html minification is enabled by default.\n" +
|
||
|
"If you are not generating a valid html output please disable it manually.\n" +
|
||
|
"You can do so by adding the following setting to your HtmlWebpackPlugin config:\n|\n|" +
|
||
|
" minify: false\n|\n" +
|
||
|
"See https://github.com/jantimon/html-webpack-plugin#options for details.\n\n" +
|
||
|
"For parser dedicated bugs please create an issue here:\n" +
|
||
|
"https://danielruf.github.io/html-minifier-terser/" +
|
||
|
"\n" +
|
||
|
e.message;
|
||
|
}
|
||
|
|
||
|
return Promise.reject(e);
|
||
|
}
|
||
|
|
||
|
return Promise.resolve(html);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Helper to return a sorted unique array of all asset files out of the asset object
|
||
|
* @private
|
||
|
*/
|
||
|
getAssetFiles(assets) {
|
||
|
const files = _uniq(
|
||
|
Object.keys(assets)
|
||
|
.filter((assetType) => assetType !== "chunks" && assets[assetType])
|
||
|
.reduce((files, assetType) => files.concat(assets[assetType]), []),
|
||
|
);
|
||
|
files.sort();
|
||
|
return files;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Converts a favicon file from disk to a webpack resource and returns the url to the resource
|
||
|
*
|
||
|
* @private
|
||
|
* @param {Compiler} compiler
|
||
|
* @param {string|false} favicon
|
||
|
* @param {Compilation} compilation
|
||
|
* @param {string} publicPath
|
||
|
* @param {PreviousEmittedAssets} previousEmittedAssets
|
||
|
* @returns {Promise<string|undefined>}
|
||
|
*/
|
||
|
generateFavicon(
|
||
|
compiler,
|
||
|
favicon,
|
||
|
compilation,
|
||
|
publicPath,
|
||
|
previousEmittedAssets,
|
||
|
) {
|
||
|
if (!favicon) {
|
||
|
return Promise.resolve(undefined);
|
||
|
}
|
||
|
|
||
|
const filename = path.resolve(compilation.compiler.context, favicon);
|
||
|
|
||
|
return promisify(compilation.inputFileSystem.readFile)(filename)
|
||
|
.then((buf) => {
|
||
|
const source = new compiler.webpack.sources.RawSource(
|
||
|
/** @type {string | Buffer} */ (buf),
|
||
|
false,
|
||
|
);
|
||
|
const name = path.basename(filename);
|
||
|
|
||
|
compilation.fileDependencies.add(filename);
|
||
|
compilation.emitAsset(name, source);
|
||
|
previousEmittedAssets.push({ name, source });
|
||
|
|
||
|
const faviconPath = publicPath + name;
|
||
|
|
||
|
if (this.options.hash) {
|
||
|
return this.appendHash(
|
||
|
faviconPath,
|
||
|
/** @type {string} */ (compilation.hash),
|
||
|
);
|
||
|
}
|
||
|
|
||
|
return faviconPath;
|
||
|
})
|
||
|
.catch(() =>
|
||
|
Promise.reject(
|
||
|
new Error("HtmlWebpackPlugin: could not load file " + filename),
|
||
|
),
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Generate all tags script for the given file paths
|
||
|
*
|
||
|
* @private
|
||
|
* @param {Array<string>} jsAssets
|
||
|
* @returns {Array<HtmlTagObject>}
|
||
|
*/
|
||
|
generatedScriptTags(jsAssets) {
|
||
|
// @ts-ignore
|
||
|
return jsAssets.map((src) => {
|
||
|
const attributes = {};
|
||
|
|
||
|
if (this.options.scriptLoading === "defer") {
|
||
|
attributes.defer = true;
|
||
|
} else if (this.options.scriptLoading === "module") {
|
||
|
attributes.type = "module";
|
||
|
} else if (this.options.scriptLoading === "systemjs-module") {
|
||
|
attributes.type = "systemjs-module";
|
||
|
}
|
||
|
|
||
|
attributes.src = src;
|
||
|
|
||
|
return {
|
||
|
tagName: "script",
|
||
|
voidTag: false,
|
||
|
meta: { plugin: "html-webpack-plugin" },
|
||
|
attributes,
|
||
|
};
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Generate all style tags for the given file paths
|
||
|
*
|
||
|
* @private
|
||
|
* @param {Array<string>} cssAssets
|
||
|
* @returns {Array<HtmlTagObject>}
|
||
|
*/
|
||
|
generateStyleTags(cssAssets) {
|
||
|
return cssAssets.map((styleAsset) => ({
|
||
|
tagName: "link",
|
||
|
voidTag: true,
|
||
|
meta: { plugin: "html-webpack-plugin" },
|
||
|
attributes: {
|
||
|
href: styleAsset,
|
||
|
rel: "stylesheet",
|
||
|
},
|
||
|
}));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Generate an optional base tag
|
||
|
*
|
||
|
* @param {string | {[attributeName: string]: string}} base
|
||
|
* @returns {Array<HtmlTagObject>}
|
||
|
*/
|
||
|
generateBaseTag(base) {
|
||
|
return [
|
||
|
{
|
||
|
tagName: "base",
|
||
|
voidTag: true,
|
||
|
meta: { plugin: "html-webpack-plugin" },
|
||
|
// attributes e.g. { href:"http://example.com/page.html" target:"_blank" }
|
||
|
attributes:
|
||
|
typeof base === "string"
|
||
|
? {
|
||
|
href: base,
|
||
|
}
|
||
|
: base,
|
||
|
},
|
||
|
];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Generate all meta tags for the given meta configuration
|
||
|
*
|
||
|
* @private
|
||
|
* @param {false | {[name: string]: false | string | {[attributeName: string]: string|boolean}}} metaOptions
|
||
|
* @returns {Array<HtmlTagObject>}
|
||
|
*/
|
||
|
generatedMetaTags(metaOptions) {
|
||
|
if (metaOptions === false) {
|
||
|
return [];
|
||
|
}
|
||
|
|
||
|
// Make tags self-closing in case of xhtml
|
||
|
// Turn { "viewport" : "width=500, initial-scale=1" } into
|
||
|
// [{ name:"viewport" content:"width=500, initial-scale=1" }]
|
||
|
const metaTagAttributeObjects = Object.keys(metaOptions)
|
||
|
.map((metaName) => {
|
||
|
const metaTagContent = metaOptions[metaName];
|
||
|
return typeof metaTagContent === "string"
|
||
|
? {
|
||
|
name: metaName,
|
||
|
content: metaTagContent,
|
||
|
}
|
||
|
: metaTagContent;
|
||
|
})
|
||
|
.filter((attribute) => attribute !== false);
|
||
|
|
||
|
// Turn [{ name:"viewport" content:"width=500, initial-scale=1" }] into
|
||
|
// the html-webpack-plugin tag structure
|
||
|
return metaTagAttributeObjects.map((metaTagAttributes) => {
|
||
|
if (metaTagAttributes === false) {
|
||
|
throw new Error("Invalid meta tag");
|
||
|
}
|
||
|
return {
|
||
|
tagName: "meta",
|
||
|
voidTag: true,
|
||
|
meta: { plugin: "html-webpack-plugin" },
|
||
|
attributes: metaTagAttributes,
|
||
|
};
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Generate a favicon tag for the given file path
|
||
|
*
|
||
|
* @private
|
||
|
* @param {string} favicon
|
||
|
* @returns {Array<HtmlTagObject>}
|
||
|
*/
|
||
|
generateFaviconTag(favicon) {
|
||
|
return [
|
||
|
{
|
||
|
tagName: "link",
|
||
|
voidTag: true,
|
||
|
meta: { plugin: "html-webpack-plugin" },
|
||
|
attributes: {
|
||
|
rel: "icon",
|
||
|
href: favicon,
|
||
|
},
|
||
|
},
|
||
|
];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Group assets to head and body tags
|
||
|
*
|
||
|
* @param {{
|
||
|
scripts: Array<HtmlTagObject>;
|
||
|
styles: Array<HtmlTagObject>;
|
||
|
meta: Array<HtmlTagObject>;
|
||
|
}} assetTags
|
||
|
* @param {"body" | "head"} scriptTarget
|
||
|
* @returns {{
|
||
|
headTags: Array<HtmlTagObject>;
|
||
|
bodyTags: Array<HtmlTagObject>;
|
||
|
}}
|
||
|
*/
|
||
|
groupAssetsByElements(assetTags, scriptTarget) {
|
||
|
/** @type {{ headTags: Array<HtmlTagObject>; bodyTags: Array<HtmlTagObject>; }} */
|
||
|
const result = {
|
||
|
headTags: [...assetTags.meta, ...assetTags.styles],
|
||
|
bodyTags: [],
|
||
|
};
|
||
|
|
||
|
// Add script tags to head or body depending on
|
||
|
// the htmlPluginOptions
|
||
|
if (scriptTarget === "body") {
|
||
|
result.bodyTags.push(...assetTags.scripts);
|
||
|
} else {
|
||
|
// If script loading is blocking add the scripts to the end of the head
|
||
|
// If script loading is non-blocking add the scripts in front of the css files
|
||
|
const insertPosition =
|
||
|
this.options.scriptLoading === "blocking"
|
||
|
? result.headTags.length
|
||
|
: assetTags.meta.length;
|
||
|
|
||
|
result.headTags.splice(insertPosition, 0, ...assetTags.scripts);
|
||
|
}
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Replace [contenthash] in filename
|
||
|
*
|
||
|
* @see https://survivejs.com/webpack/optimizing/adding-hashes-to-filenames/
|
||
|
*
|
||
|
* @private
|
||
|
* @param {Compiler} compiler
|
||
|
* @param {string} filename
|
||
|
* @param {string|Buffer} fileContent
|
||
|
* @param {Compilation} compilation
|
||
|
* @returns {{ path: string, info: {} }}
|
||
|
*/
|
||
|
replacePlaceholdersInFilename(compiler, filename, fileContent, compilation) {
|
||
|
if (/\[\\*([\w:]+)\\*\]/i.test(filename) === false) {
|
||
|
return { path: filename, info: {} };
|
||
|
}
|
||
|
|
||
|
const hash = compiler.webpack.util.createHash(
|
||
|
compilation.outputOptions.hashFunction,
|
||
|
);
|
||
|
|
||
|
hash.update(fileContent);
|
||
|
|
||
|
if (compilation.outputOptions.hashSalt) {
|
||
|
hash.update(compilation.outputOptions.hashSalt);
|
||
|
}
|
||
|
|
||
|
const contentHash = /** @type {string} */ (
|
||
|
hash
|
||
|
.digest(compilation.outputOptions.hashDigest)
|
||
|
.slice(0, compilation.outputOptions.hashDigestLength)
|
||
|
);
|
||
|
|
||
|
return compilation.getPathWithInfo(filename, {
|
||
|
contentHash,
|
||
|
chunk: {
|
||
|
hash: contentHash,
|
||
|
// @ts-ignore
|
||
|
contentHash,
|
||
|
},
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Function to generate HTML file.
|
||
|
*
|
||
|
* @private
|
||
|
* @param {Compiler} compiler
|
||
|
* @param {Compilation} compilation
|
||
|
* @param {string} outputName
|
||
|
* @param {CachedChildCompilation} childCompilerPlugin
|
||
|
* @param {PreviousEmittedAssets} previousEmittedAssets
|
||
|
* @param {{ value: string | undefined }} assetJson
|
||
|
* @param {(err?: Error) => void} callback
|
||
|
*/
|
||
|
generateHTML(
|
||
|
compiler,
|
||
|
compilation,
|
||
|
outputName,
|
||
|
childCompilerPlugin,
|
||
|
previousEmittedAssets,
|
||
|
assetJson,
|
||
|
callback,
|
||
|
) {
|
||
|
// Get all entry point names for this html file
|
||
|
const entryNames = Array.from(compilation.entrypoints.keys());
|
||
|
const filteredEntryNames = this.filterEntryChunks(
|
||
|
entryNames,
|
||
|
this.options.chunks,
|
||
|
this.options.excludeChunks,
|
||
|
);
|
||
|
const sortedEntryNames = this.sortEntryChunks(
|
||
|
filteredEntryNames,
|
||
|
this.options.chunksSortMode,
|
||
|
compilation,
|
||
|
);
|
||
|
const templateResult = this.options.templateContent
|
||
|
? { mainCompilationHash: compilation.hash }
|
||
|
: childCompilerPlugin.getCompilationEntryResult(this.options.template);
|
||
|
|
||
|
if ("error" in templateResult) {
|
||
|
compilation.errors.push(
|
||
|
prettyError(templateResult.error, compiler.context).toString(),
|
||
|
);
|
||
|
}
|
||
|
|
||
|
// If the child compilation was not executed during a previous main compile run
|
||
|
// it is a cached result
|
||
|
const isCompilationCached =
|
||
|
templateResult.mainCompilationHash !== compilation.hash;
|
||
|
/** Generated file paths from the entry point names */
|
||
|
const assetsInformationByGroups = this.getAssetsInformationByGroups(
|
||
|
compilation,
|
||
|
outputName,
|
||
|
sortedEntryNames,
|
||
|
);
|
||
|
// If the template and the assets did not change we don't have to emit the html
|
||
|
const newAssetJson = JSON.stringify(
|
||
|
this.getAssetFiles(assetsInformationByGroups),
|
||
|
);
|
||
|
|
||
|
if (
|
||
|
isCompilationCached &&
|
||
|
this.options.cache &&
|
||
|
assetJson.value === newAssetJson
|
||
|
) {
|
||
|
previousEmittedAssets.forEach(({ name, source, info }) => {
|
||
|
compilation.emitAsset(name, source, info);
|
||
|
});
|
||
|
return callback();
|
||
|
} else {
|
||
|
previousEmittedAssets.length = 0;
|
||
|
assetJson.value = newAssetJson;
|
||
|
}
|
||
|
|
||
|
// The html-webpack plugin uses a object representation for the html-tags which will be injected
|
||
|
// to allow altering them more easily
|
||
|
// Just before they are converted a third-party-plugin author might change the order and content
|
||
|
const assetsPromise = this.generateFavicon(
|
||
|
compiler,
|
||
|
this.options.favicon,
|
||
|
compilation,
|
||
|
assetsInformationByGroups.publicPath,
|
||
|
previousEmittedAssets,
|
||
|
).then((faviconPath) => {
|
||
|
assetsInformationByGroups.favicon = faviconPath;
|
||
|
return HtmlWebpackPlugin.getCompilationHooks(
|
||
|
compilation,
|
||
|
).beforeAssetTagGeneration.promise({
|
||
|
assets: assetsInformationByGroups,
|
||
|
outputName,
|
||
|
plugin: this,
|
||
|
});
|
||
|
});
|
||
|
|
||
|
// Turn the js and css paths into grouped HtmlTagObjects
|
||
|
const assetTagGroupsPromise = assetsPromise
|
||
|
// And allow third-party-plugin authors to reorder and change the assetTags before they are grouped
|
||
|
.then(({ assets }) =>
|
||
|
HtmlWebpackPlugin.getCompilationHooks(
|
||
|
compilation,
|
||
|
).alterAssetTags.promise({
|
||
|
assetTags: {
|
||
|
scripts: this.generatedScriptTags(assets.js),
|
||
|
styles: this.generateStyleTags(assets.css),
|
||
|
meta: [
|
||
|
...(this.options.base !== false
|
||
|
? this.generateBaseTag(this.options.base)
|
||
|
: []),
|
||
|
...this.generatedMetaTags(this.options.meta),
|
||
|
...(assets.favicon
|
||
|
? this.generateFaviconTag(assets.favicon)
|
||
|
: []),
|
||
|
],
|
||
|
},
|
||
|
outputName,
|
||
|
publicPath: assetsInformationByGroups.publicPath,
|
||
|
plugin: this,
|
||
|
}),
|
||
|
)
|
||
|
.then(({ assetTags }) => {
|
||
|
// Inject scripts to body unless it set explicitly to head
|
||
|
const scriptTarget =
|
||
|
this.options.inject === "head" ||
|
||
|
(this.options.inject !== "body" &&
|
||
|
this.options.scriptLoading !== "blocking")
|
||
|
? "head"
|
||
|
: "body";
|
||
|
// Group assets to `head` and `body` tag arrays
|
||
|
const assetGroups = this.groupAssetsByElements(assetTags, scriptTarget);
|
||
|
// Allow third-party-plugin authors to reorder and change the assetTags once they are grouped
|
||
|
return HtmlWebpackPlugin.getCompilationHooks(
|
||
|
compilation,
|
||
|
).alterAssetTagGroups.promise({
|
||
|
headTags: assetGroups.headTags,
|
||
|
bodyTags: assetGroups.bodyTags,
|
||
|
outputName,
|
||
|
publicPath: assetsInformationByGroups.publicPath,
|
||
|
plugin: this,
|
||
|
});
|
||
|
});
|
||
|
|
||
|
// Turn the compiled template into a nodejs function or into a nodejs string
|
||
|
const templateEvaluationPromise = Promise.resolve().then(() => {
|
||
|
if ("error" in templateResult) {
|
||
|
return this.options.showErrors
|
||
|
? prettyError(templateResult.error, compiler.context).toHtml()
|
||
|
: "ERROR";
|
||
|
}
|
||
|
|
||
|
// Allow to use a custom function / string instead
|
||
|
if (this.options.templateContent !== false) {
|
||
|
return this.options.templateContent;
|
||
|
}
|
||
|
|
||
|
// Once everything is compiled evaluate the html factory and replace it with its content
|
||
|
if ("compiledEntry" in templateResult) {
|
||
|
const compiledEntry = templateResult.compiledEntry;
|
||
|
const assets = compiledEntry.assets;
|
||
|
|
||
|
// Store assets from child compiler to re-emit them later
|
||
|
for (const name in assets) {
|
||
|
previousEmittedAssets.push({
|
||
|
name,
|
||
|
source: assets[name].source,
|
||
|
info: assets[name].info,
|
||
|
});
|
||
|
}
|
||
|
|
||
|
return this.evaluateCompilationResult(
|
||
|
compiledEntry.content,
|
||
|
assetsInformationByGroups.publicPath,
|
||
|
this.options.template,
|
||
|
);
|
||
|
}
|
||
|
|
||
|
return Promise.reject(
|
||
|
new Error("Child compilation contained no compiledEntry"),
|
||
|
);
|
||
|
});
|
||
|
const templateExecutionPromise = Promise.all([
|
||
|
assetsPromise,
|
||
|
assetTagGroupsPromise,
|
||
|
templateEvaluationPromise,
|
||
|
])
|
||
|
// Execute the template
|
||
|
.then(([assetsHookResult, assetTags, compilationResult]) =>
|
||
|
typeof compilationResult !== "function"
|
||
|
? compilationResult
|
||
|
: this.executeTemplate(
|
||
|
compilationResult,
|
||
|
assetsHookResult.assets,
|
||
|
{ headTags: assetTags.headTags, bodyTags: assetTags.bodyTags },
|
||
|
compilation,
|
||
|
),
|
||
|
);
|
||
|
|
||
|
const injectedHtmlPromise = Promise.all([
|
||
|
assetTagGroupsPromise,
|
||
|
templateExecutionPromise,
|
||
|
])
|
||
|
// Allow plugins to change the html before assets are injected
|
||
|
.then(([assetTags, html]) => {
|
||
|
const pluginArgs = {
|
||
|
html,
|
||
|
headTags: assetTags.headTags,
|
||
|
bodyTags: assetTags.bodyTags,
|
||
|
plugin: this,
|
||
|
outputName,
|
||
|
};
|
||
|
return HtmlWebpackPlugin.getCompilationHooks(
|
||
|
compilation,
|
||
|
).afterTemplateExecution.promise(pluginArgs);
|
||
|
})
|
||
|
.then(({ html, headTags, bodyTags }) => {
|
||
|
return this.postProcessHtml(compiler, html, assetsInformationByGroups, {
|
||
|
headTags,
|
||
|
bodyTags,
|
||
|
});
|
||
|
});
|
||
|
|
||
|
const emitHtmlPromise = injectedHtmlPromise
|
||
|
// Allow plugins to change the html after assets are injected
|
||
|
.then((html) => {
|
||
|
const pluginArgs = { html, plugin: this, outputName };
|
||
|
return HtmlWebpackPlugin.getCompilationHooks(compilation)
|
||
|
.beforeEmit.promise(pluginArgs)
|
||
|
.then((result) => result.html);
|
||
|
})
|
||
|
.catch((err) => {
|
||
|
// In case anything went wrong the promise is resolved
|
||
|
// with the error message and an error is logged
|
||
|
compilation.errors.push(prettyError(err, compiler.context).toString());
|
||
|
return this.options.showErrors
|
||
|
? prettyError(err, compiler.context).toHtml()
|
||
|
: "ERROR";
|
||
|
})
|
||
|
.then((html) => {
|
||
|
const filename = outputName.replace(
|
||
|
/\[templatehash([^\]]*)\]/g,
|
||
|
require("util").deprecate(
|
||
|
(match, options) => `[contenthash${options}]`,
|
||
|
"[templatehash] is now [contenthash]",
|
||
|
),
|
||
|
);
|
||
|
const replacedFilename = this.replacePlaceholdersInFilename(
|
||
|
compiler,
|
||
|
filename,
|
||
|
html,
|
||
|
compilation,
|
||
|
);
|
||
|
const source = new compiler.webpack.sources.RawSource(html, false);
|
||
|
|
||
|
// Add the evaluated html code to the webpack assets
|
||
|
compilation.emitAsset(
|
||
|
replacedFilename.path,
|
||
|
source,
|
||
|
replacedFilename.info,
|
||
|
);
|
||
|
previousEmittedAssets.push({ name: replacedFilename.path, source });
|
||
|
|
||
|
return replacedFilename.path;
|
||
|
})
|
||
|
.then((finalOutputName) =>
|
||
|
HtmlWebpackPlugin.getCompilationHooks(compilation)
|
||
|
.afterEmit.promise({
|
||
|
outputName: finalOutputName,
|
||
|
plugin: this,
|
||
|
})
|
||
|
.catch((err) => {
|
||
|
/** @type {Logger} */
|
||
|
(this.logger).error(err);
|
||
|
return null;
|
||
|
})
|
||
|
.then(() => null),
|
||
|
);
|
||
|
|
||
|
// Once all files are added to the webpack compilation
|
||
|
// let the webpack compiler continue
|
||
|
emitHtmlPromise.then(() => {
|
||
|
callback();
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* The default for options.templateParameter
|
||
|
* Generate the template parameters
|
||
|
*
|
||
|
* Generate the template parameters for the template function
|
||
|
* @param {Compilation} compilation
|
||
|
* @param {AssetsInformationByGroups} assets
|
||
|
* @param {{
|
||
|
headTags: HtmlTagObject[],
|
||
|
bodyTags: HtmlTagObject[]
|
||
|
}} assetTags
|
||
|
* @param {ProcessedHtmlWebpackOptions} options
|
||
|
* @returns {TemplateParameter}
|
||
|
*/
|
||
|
function templateParametersGenerator(compilation, assets, assetTags, options) {
|
||
|
return {
|
||
|
compilation: compilation,
|
||
|
webpackConfig: compilation.options,
|
||
|
htmlWebpackPlugin: {
|
||
|
tags: assetTags,
|
||
|
files: assets,
|
||
|
options: options,
|
||
|
},
|
||
|
};
|
||
|
}
|
||
|
|
||
|
// Statics:
|
||
|
/**
|
||
|
* The major version number of this plugin
|
||
|
*/
|
||
|
HtmlWebpackPlugin.version = 5;
|
||
|
|
||
|
/**
|
||
|
* A static helper to get the hooks for this plugin
|
||
|
*
|
||
|
* Usage: HtmlWebpackPlugin.getHooks(compilation).HOOK_NAME.tapAsync('YourPluginName', () => { ... });
|
||
|
*/
|
||
|
// TODO remove me in the next major release in favor getCompilationHooks
|
||
|
HtmlWebpackPlugin.getHooks = HtmlWebpackPlugin.getCompilationHooks;
|
||
|
HtmlWebpackPlugin.createHtmlTagObject = createHtmlTagObject;
|
||
|
|
||
|
module.exports = HtmlWebpackPlugin;
|