webpack
Version:
Packs ECMAScript/CommonJs/AMD modules for the browser. Allows you to split your codebase into multiple bundles, which can be loaded on demand. Supports loaders to preprocess files, i.e. json, jsx, es7, css, less, ... and your custom stuff.
1,296 lines • 54.4 kB
JavaScript
/*
MIT License http://www.opensource.org/licenses/mit-license.php
*/
"use strict";
const { pathToFileURL } = require("url");
const { AsyncSeriesHook, AsyncSeriesWaterfallHook } = require("tapable");
const { RawSource } = require("webpack-sources");
const Compilation = require("../Compilation");
const EntryOptionPlugin = require("../EntryOptionPlugin");
const EntryPlugin = require("../EntryPlugin");
const HotUpdateChunk = require("../HotUpdateChunk");
const { HTML_TYPE } = require("../ModuleSourceTypeConstants");
const { HTML_MODULE_TYPE } = require("../ModuleTypeConstants");
const NormalModule = require("../NormalModule");
const { toTemplateSourceFileName } = require("../TemplatedPathPlugin");
const ConstDependency = require("../dependencies/ConstDependency");
const StaticExportsDependency = require("../dependencies/StaticExportsDependency");
const WebpackError = require("../errors/WebpackError");
const JavascriptModulesPlugin = require("../javascript/JavascriptModulesPlugin");
const { compareModulesByFullName } = require("../util/comparators");
const createHash = require("../util/createHash");
const createHooksRegistry = require("../util/createHooksRegistry");
const { getUndoPath, makePathsRelative } = require("../util/identifier");
const implicitTypeLoaderFallback = require("../util/implicitTypeLoaderFallback");
const lazyModule = require("../util/lazyModule");
const memoize = require("../util/memoize");
const { digestNonNumericOnly } = require("../util/nonNumericOnlyHash");
const preloadModuleType = require("../util/preloadModuleType");
const {
PUBLIC_PATH_AUTO: autoPlaceholder
} = require("../util/publicPathPlaceholder");
const { removeBOMFromResult } = require("../util/removeBOM");
const HtmlModule = require("./HtmlModule");
const getMimeTypes = memoize(() => require("../util/mimeTypes"));
const getHtmlSyntax = memoize(() => require("./syntax"));
// Lazy: this plugin is applied on every build (`experiments.html` defaults on),
// but the HTML parser and tokenizer are only reached once HTML is in the graph.
const getHtmlParser = lazyModule(() => require("./HtmlParser"));
// html generation only happens once a build actually has an html module
const getHtmlGenerator = memoize(() => require("./HtmlGenerator"));
/**
* @import {
* EntryDescriptionNormalized,
* OutputHtmlOptions,
* HtmlFaviconIcon
* } from "../../declarations/WebpackOptions"
*/
/** @typedef {import("../../declarations/WebpackOptions").OutputHtmlOptions["favicon"]} FaviconOption */
/** @typedef {import("../../declarations/WebpackOptions").OutputHtmlOptions["manifest"]} ManifestOption */
/** @typedef {Exclude<HtmlFaviconIcon, string>} FaviconIcon */
/** @import Compiler from "../Compiler" */
/** @import { HtmlModuleBuildInfo } from "./HtmlModule" */
/** @import HtmlParser from "./HtmlParser" */
/** @typedef {{ request: string, entryName: string, type: "script" | "script-module" | "modulepreload" | "stylesheet" | "html" | "preload" | "prefetch", css?: boolean }} HtmlEntryInfo */
/** @typedef {{ outputName: string }} HtmlTransformHtmlContext */
/** @typedef {{ outputName: string }} HtmlEmittedContext */
/**
* A tag to inject into an emitted page as a structured descriptor. Injected
* verbatim (not re-bundled) at the `injectTo` position.
* @typedef {object} HtmlTagDescriptor
* @property {string} tag tag name, e.g. `"script"` / `"link"` / `"meta"`
* @property {Record<string, string | boolean | undefined>=} attrs attributes; `true` renders a bare boolean attribute, `false`/`undefined` is omitted
* @property {string=} children inner content (ignored for void elements like `<link>`/`<meta>`)
* @property {("head" | "body" | "head-prepend" | "body-prepend")=} injectTo placement; defaults to `"head"`
* @property {boolean=} voidTag force a void element (no closing tag); inferred from the tag name when omitted
*/
/** @typedef {{ outputName: string, html: string }} HtmlInjectTagsContext */
/**
* A `<script>`/`<link>`/`<style>`/`<meta>` tag already present in an emitted
* page, exposed for in-place mutation by `transformTags`. Mutate `attrs`, set
* `remove`, or change `injectTo` to move it; don't reorder the array itself.
* @typedef {object} HtmlMutableTag
* @property {string} tag the (lowercased) tag name
* @property {Record<string, string | boolean | undefined>} attrs mutable attributes; a string value renders `name="value"`, `true` a bare attribute, `false`/`undefined`/deleting the key drops it
* @property {("head" | "body" | "head-prepend" | "body-prepend")=} injectTo the tag's current region (`"head"`/`"body"`); set a different value to move it there (`*-prepend` to the region's start)
* @property {boolean=} remove set true to delete the whole element
*/
/** @typedef {{ outputName: string, html: string }} HtmlTransformTagsContext */
const createCompilationHooks = () => ({
/**
* Called with the list of extra tags to inject into each page (initially empty) plus the current HTML; push `HtmlTagDescriptor`s and return the list — webpack serializes and places them by `injectTo`. A structured alternative to the string-level `transformHtml` for adding tags; runs before CSP so injected inline tags are hashed.
* @type {AsyncSeriesWaterfallHook<[HtmlTagDescriptor[], HtmlInjectTagsContext]>}
* @since 5.109.0
*/
injectTags: new AsyncSeriesWaterfallHook(["tags", "context"]),
/**
* Called with the page's `<script>`/`<link>`/`<style>`/`<meta>` tags (webpack's own and any injected) as mutable descriptors; mutate `attrs` (add a `nonce`/`data-*`, switch `defer`↔`async`, …), set `remove: true`, or change `injectTo` to move a tag between `<head>` and `<body>`, and webpack rewrites the changed tags. Add new tags with `injectTags` instead.
* @type {AsyncSeriesHook<[HtmlMutableTag[], HtmlTransformTagsContext]>}
* @since 5.109.0
*/
transformTags: new AsyncSeriesHook(["tags", "context"]),
/**
* Called with each emitted page's final HTML (all sentinels resolved) just before it is written; return the (possibly transformed) HTML — e.g. to minify, inject a CSP meta, or rewrite tags.
* @type {AsyncSeriesWaterfallHook<[string, HtmlTransformHtmlContext]>}
* @since 5.109.0
*/
transformHtml: new AsyncSeriesWaterfallHook(["html", "context"]),
/**
* Called once each page's HTML asset has been finalized — a post-emit notification (nothing to return).
* @type {AsyncSeriesHook<[HtmlEmittedContext]>}
* @since 5.109.0
*/
htmlEmitted: new AsyncSeriesHook(["context"])
});
/**
* @typedef {ReturnType<typeof createCompilationHooks>} HtmlCompilationHooks
*/
const PLUGIN_NAME = "HtmlModulesPlugin";
// Built-in default favicon (the webpack logo). Referenced as a `file:` request
// so `<link rel="icon">` in the synthetic wrapper flows through the normal
// asset pipeline — emitted hashed, with the correct publicPath, no direct IO.
// Resolved from `__filename` (a sibling of this module) — no `path` needed.
const DEFAULT_FAVICON = new URL("./favicon.svg", pathToFileURL(__filename))
.href;
// mime-db is heavy — only load it when a favicon actually needs a `type`.
/**
* Resolves `output.html.favicon` for one page to `[rel, icon]` pairs.
* `false`/absent → none; `true` → the webpack logo; a string → one `icon`
* link; an object maps each `rel` to an icon (a path string, an attributes
* object, or an array of those for several icons under one `rel`); a function
* receives the page name and returns any of those. Each icon is normalized to
* an object so `faviconLinkTag` reads one shape.
* @param {FaviconOption} favicon favicon option
* @param {string} name page/entry name
* @returns {[string, FaviconIcon][]} rel/icon pairs, in order
*/
const resolveFaviconLinks = (favicon, name) => {
const value = typeof favicon === "function" ? favicon(name) : favicon;
if (value === true) return [["icon", { href: DEFAULT_FAVICON }]];
if (typeof value === "string") return [["icon", { href: value }]];
if (value && typeof value === "object") {
/** @type {[string, FaviconIcon][]} */
const pairs = [];
for (const [rel, icon] of Object.entries(value)) {
for (const one of Array.isArray(icon) ? icon : [icon]) {
pairs.push([rel, typeof one === "string" ? { href: one } : one]);
}
}
return pairs;
}
return [];
};
/**
* @param {string} rel link relation
* @param {FaviconIcon} icon icon `href` plus optional link attributes
* @returns {string} a `<link rel=… [attrs…] href=…>` tag; `type` defaults to the file format
*/
const faviconLinkTag = (rel, icon) => {
const { escapeAttribute } = getHtmlSyntax();
const type = icon.type || getMimeTypes().lookup(icon.href) || "";
const attrs = [
`rel="${escapeAttribute(rel)}"`,
icon.sizes && `sizes="${escapeAttribute(icon.sizes)}"`,
type && `type="${escapeAttribute(type)}"`,
icon.media && `media="${escapeAttribute(icon.media)}"`,
icon.color && `color="${escapeAttribute(icon.color)}"`,
icon.crossorigin && `crossorigin="${escapeAttribute(icon.crossorigin)}"`,
`href="${escapeAttribute(icon.href)}"`
];
return `<link ${attrs.filter(Boolean).join(" ")}>`;
};
/**
* Resolves `output.html.manifest` for one page to a `<link rel="manifest">`
* tag, or `""` when unset. A string is a path to an existing `.webmanifest`
* file; an object is serialized to a base64 `data:application/manifest+json`
* URL (routed to `asset/webmanifest` by a default rule, so its relative icon
* `src`s are hashed like any request); a function receives the page name.
* Base64 (not percent-encoding) is required because this tag is embedded in
* the outer `data:text/html` wrapper, which is itself percent-decoded — a
* percent-encoded inner URL would be corrupted by that decode.
* @param {ManifestOption} manifest manifest option
* @param {string} name page/entry name
* @returns {string} the `<link>` tag, or ""
*/
const manifestLinkTag = (manifest, name) => {
const value = typeof manifest === "function" ? manifest(name) : manifest;
if (!value) return "";
const href =
typeof value === "string"
? value
: `data:application/manifest+json;base64,${Buffer.from(
JSON.stringify(value),
"utf8"
).toString("base64")}`;
return `<link rel="manifest" href="${getHtmlSyntax().escapeAttribute(
href
)}">`;
};
// Decided while the page's HTML is generated, which happens once per module —
// and one synthetic page module can back several entries — so an entry's `html`
// object can't override it.
/** @type {"inline"[]} */
const GLOBAL_ONLY_HTML_OPTIONS = ["inline"];
/**
* SRI is on when `integrity` names at least one algorithm.
* @param {OutputHtmlOptions["integrity"]} integrity integrity option
* @returns {boolean} true when integrity attributes must be emitted
*/
const isIntegrityEnabled = (integrity) =>
integrity === true ||
typeof integrity === "function" ||
(Array.isArray(integrity) && integrity.length > 0);
// `\.html`/`\.css` request matchers for the synthetic `output.html` wrapper.
const HTML_REQUEST_RE = /\.html(\?|$)/i;
const CSS_REQUEST_RE = /\.css(\?|$)/i;
// The wrapper's own resource — the media type must end here, so that a
// `data:text/htmlx,` entry (JavaScript to `DataUriPlugin`) is not one.
const HTML_DATA_URL_RE = /^data:text\/html[,;]/i;
/**
* Requests an `output.html` page must load for an entry: its `dependOn`
* ancestors first (transitive, deduped — so a diamond loads each once), then
* the entry's own imports. `.html` imports are dropped (own HTML entries).
* @param {string} name entry name
* @param {Record<string, EntryDescriptionNormalized>} entries normalized static entries
* @returns {string[]} deduped requests in load order
*/
const collectHtmlEntryRequests = (name, entries) => {
/** @type {string[]} */
const requests = [];
/** @type {Set<string>} */
const seenRequests = new Set();
/** @type {Set<string>} */
const visited = new Set();
/**
* Collects an entry's non-html requests, `dependOn` entries included.
* @param {string} entryName entry to walk
* @returns {void}
*/
const walk = (entryName) => {
if (visited.has(entryName)) return;
visited.add(entryName);
const desc = entries[entryName];
if (!desc) return;
if (desc.dependOn) {
for (const dep of desc.dependOn) walk(dep);
}
if (!desc.import) return;
for (const request of desc.import) {
if (HTML_REQUEST_RE.test(request) || seenRequests.has(request)) continue;
seenRequests.add(request);
requests.push(request);
}
};
walk(name);
return requests;
};
/**
* @param {string} name definition name in `schemas/WebpackOptions.json`
* @returns {EXPECTED_OBJECT} a schema referencing `#/definitions/<name>`
*/
const getSchema = (name) => {
const { definitions } = require("../../schemas/WebpackOptions.json");
return {
definitions,
oneOf: [{ $ref: `#/definitions/${name}` }]
};
};
const generatorValidationOptions = {
name: "Html Modules Plugin",
baseDataPath: "generator"
};
const parserValidationOptions = {
name: "Html Modules Plugin",
baseDataPath: "parser"
};
class HtmlModulesPlugin {
/**
* `output.hashFunction`/`hashSalt`/`hashDigest`/`hashDigestLength`
* digest of `content`, with `nonNumericOnlyHash` applied — webpack's
* standard `[contenthash]` recipe.
* @param {string | Buffer} content content to hash
* @param {import("../../declarations/WebpackOptions").Output} outputOptions output options
* @returns {string} content hash
*/
static computeContentHash(content, outputOptions) {
const hash = createHash(
/** @type {import("../../declarations/WebpackOptions").HashFunction} */
(outputOptions.hashFunction)
);
if (outputOptions.hashSalt) hash.update(outputOptions.hashSalt);
hash.update(content);
return digestNonNumericOnly(
hash,
/** @type {string} */ (outputOptions.hashDigest),
/** @type {number} */ (outputOptions.hashDigestLength)
);
}
/**
* Filename template for an extracted HTML page: `output.htmlFilename` for
* initial chunks, `output.htmlChunkFilename` otherwise — the HTML counterpart
* of `CssModulesPlugin.getChunkFilenameTemplate`.
* @param {import("../Chunk")} chunk chunk
* @param {import("../../declarations/WebpackOptions").Output} outputOptions output options
* @returns {import("../Chunk").ChunkFilenameTemplate} used filename template
*/
static getChunkFilenameTemplate(chunk, outputOptions) {
return chunk.canBeInitial()
? /** @type {import("../Chunk").ChunkFilenameTemplate} */ (
outputOptions.htmlFilename
)
: /** @type {import("../Chunk").ChunkFilenameTemplate} */ (
outputOptions.htmlChunkFilename
);
}
/**
* Applies the plugin by registering its hooks on the compiler.
* @param {Compiler} compiler the compiler instance
* @returns {void}
*/
apply(compiler) {
const { output } = compiler.options;
const htmlOption = output.html;
const globalHtmlOptions =
typeof htmlOption === "object" ? htmlOption : undefined;
// Collected while entries are resolved (`entryOption`, before the first
// compilation) and reported on every compilation; a Set dedupes entries
// that hit the same problem.
/** @type {Set<string>} */
const optionWarnings = new Set();
compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
for (const message of optionWarnings) {
compilation.warnings.push(new WebpackError(message));
}
});
// Resolved options per entry that owns a generated page, read back when the
// page is emitted (`csp`, `integrity`) — one synthetic page module can back
// several entries, so those can only be applied per emitted asset.
/** @type {Map<string, OutputHtmlOptions>} */
const htmlOptionsByEntry = new Map();
// SRI sentinels are emitted while the page's HTML is generated, before the
// owning entry is known, so any page asking for integrity turns them on for
// all of them; pages that don't want SRI drop them again on emit. Authored
// `.html` pages never reach the entry hook, hence the global seed.
let anyIntegrity = isIntegrityEnabled(
globalHtmlOptions && globalHtmlOptions.integrity
);
/**
* Options for one page: an entry's `html` object overrides `output.html`
* option by option, `true` inherits it as-is.
* @param {EntryDescriptionNormalized["html"]} html the entry's `html` value
* @param {string} name entry name
* @returns {OutputHtmlOptions} resolved options
*/
const resolveHtmlOptions = (html, name) => {
const options =
typeof html !== "object"
? globalHtmlOptions || {}
: globalHtmlOptions
? { ...globalHtmlOptions, ...html }
: html;
if (typeof html === "object") {
for (const option of GLOBAL_ONLY_HTML_OPTIONS) {
if (html[option] !== undefined) {
optionWarnings.add(
`entry "${name}" html.${option} is ignored — ${option} is resolved once per generated page and can only be set on output.html.`
);
}
}
}
htmlOptionsByEntry.set(name, options);
if (isIntegrityEnabled(options.integrity)) anyIntegrity = true;
return options;
};
// `output.html` (or an entry's `html`) wraps a non-HTML entry in a
// synthetic HTML module so the existing pipeline injects its JS/CSS
// chunks and applies the `template` option. `dependOn` ancestors are
// injected first so the page loads the shared/runtime chunks before
// the entry's own (see `collectHtmlEntryRequests`). `crossOriginLoading`
// and SRI are applied to the injected tags centrally in
// HtmlEntryDependency.
EntryOptionPlugin.getHooks(compiler).entry.tap(
PLUGIN_NAME,
(context, name, desc) => {
// Resolved for every entry, including the authored `.html` ones that
// need no wrapper below — their page reads back `csp`/`integrity` too.
const htmlObj = resolveHtmlOptions(desc.html, name);
const html = desc.html !== undefined ? desc.html : htmlOption;
const imports = desc.import;
if (
!html ||
!imports ||
imports.every((r) => HTML_REQUEST_RE.test(r))
) {
return;
}
const scriptLoading = htmlObj.scriptLoading || "auto";
// ESM output emits `type="module"` (already deferred), so
// scriptLoading is ignored under output.module — warn on an
// explicit defer/blocking.
let scriptAttr = " defer";
if (output.module) {
scriptAttr = "";
if (scriptLoading === "defer" || scriptLoading === "blocking") {
const source =
typeof desc.html === "object" &&
desc.html.scriptLoading !== undefined
? `entry "${name}" html`
: "output.html";
optionWarnings.add(
`${source}.scriptLoading: "${scriptLoading}" is ignored with output.module — ES module scripts are always deferred.`
);
}
} else if (scriptLoading === "blocking") {
scriptAttr = "";
}
const entries = compiler.options.entry;
const requests =
typeof entries === "object" && entries[name]
? collectHtmlEntryRequests(name, entries)
: imports.filter((r) => !HTML_REQUEST_RE.test(r));
const links = [];
const scripts = [];
for (const r of requests) {
if (CSS_REQUEST_RE.test(r)) {
links.push(`<link rel="stylesheet" href="${r}">`);
} else {
scripts.push(`<script${scriptAttr} src="${r}"></script>`);
}
}
const headTags = getHtmlSyntax().buildHeadTags(htmlObj);
// Injected into webpack-generated pages only; each icon's href is a
// normal request the parser turns into a hashed asset.
const faviconTag = resolveFaviconLinks(htmlObj.favicon, name)
.map(([rel, icon]) => faviconLinkTag(rel, icon))
.join("");
const manifestTag = manifestLinkTag(htmlObj.manifest, name);
const inject = htmlObj.inject;
const scriptsInHead =
inject === "head" || (inject !== "body" && output.module);
const scriptsFirst = output.module || scriptAttr === " defer";
const headScripts = scriptsInHead ? scripts.join("") : "";
return `data:text/html,<!doctype html><html><head>${faviconTag}${manifestTag}${headTags}${
scriptsFirst
? headScripts + links.join("")
: links.join("") + headScripts
}</head><body>${scriptsInHead ? "" : scripts.join("")}</body></html>`;
}
);
// Per-chunk `RawSource` reused across builds when bytes are unchanged:
// keeping identity stable avoids invalidating `RealContentHashPlugin|analyse`.
/** @type {Map<string, { content: string, source: import("webpack-sources").RawSource }>} */
const sentinelResolvedSourceCache = new Map();
// `<script src>` and `<link rel="modulepreload">` references collected
// by HtmlParser become real compilation entries here. The `script`
// and `script-module` groups are chained via a leader-only dependOn so
// they share a runtime — the first entry of the group owns it and
// every subsequent entry sets `dependOn: [leader]`. Modulepreload
// entries are emitted as independent entries (no dependOn) so they
// can never be imported as a runtime leader by a later script —
// that's what keeps the "preload but don't execute" contract of
// `<link rel="modulepreload">` intact.
// Per-compilation state: the HTML modules seen during make (so
// `finishMake` creates their entries without scanning the whole module
// graph), the stylesheet entry names (read in `afterChunks`) and the html
// options of every emitted page, keyed by its asset name.
/** @type {WeakMap<import("../Compilation"), { htmlModules: Set<import("../Module")>, stylesheetEntries: Set<string>, htmlAssetOptions: Map<string, OutputHtmlOptions> }>} */
const compilationState = new WeakMap();
/**
* @param {import("../Compilation")} compilation compilation
* @returns {{ htmlModules: Set<import("../Module")>, stylesheetEntries: Set<string>, htmlAssetOptions: Map<string, OutputHtmlOptions> }} per-compilation state
*/
const getState = (compilation) => {
let state = compilationState.get(compilation);
if (state === undefined) {
state = {
htmlModules: new Set(),
stylesheetEntries: new Set(),
htmlAssetOptions: new Map()
};
compilationState.set(compilation, state);
}
return state;
};
compiler.hooks.finishMake.tapAsync(PLUGIN_NAME, (compilation, callback) => {
const { htmlModules, stylesheetEntries } = getState(compilation);
// Collect the entries an HTML module asks for. Only the script chains
// (`script`, `script-module`) share a runtime via a leader-only
// `dependOn`; `modulepreload`, `stylesheet` and `html` links are all
// independent entries (a CSS or page entry must not chain into a JS
// leader, or the chunk would mix unrelated outputs).
/** @type {(module: import("../Module")) => { context: string, request: string, name: string, dependOn: string[] | undefined }[]} */
const collectEntrySpecs = (module) => {
const { htmlEntries } = /** @type {HtmlModuleBuildInfo} */ (
module.buildInfo
);
if (!htmlEntries) return [];
const context = /** @type {string} */ (module.context);
const specs = [];
for (const [groupKind, group] of Object.entries(htmlEntries)) {
const isChainGroup =
groupKind === "script" || groupKind === "script-module";
/** @type {string | undefined} */
let leaderName;
for (const entry of group) {
const dependOn =
isChainGroup && leaderName !== undefined
? [leaderName]
: undefined;
if (isChainGroup && leaderName === undefined) {
leaderName = entry.entryName;
}
// Stylesheet entries and `as="style"` preload/prefetch entries emit
// a CSS chunk, so they need the CSS filename template below.
if (groupKind === "stylesheet" || entry.css) {
stylesheetEntries.add(entry.entryName);
}
specs.push({
context,
request: entry.request,
name: entry.entryName,
dependOn
});
}
}
return specs;
};
// Push one collected reference as a compilation entry; resolves with
// the built entry module.
/** @type {(spec: { context: string, request: string, name: string, dependOn: string[] | undefined }) => Promise<import("../Module") | null>} */
const addEntry = (spec) =>
new Promise((resolve, reject) => {
compilation.addEntry(
spec.context,
EntryPlugin.createDependency(spec.request, { name: spec.name }),
{
name: spec.name,
// Each entry gets its own filename from the synthetic entry name so
// it doesn't collide with `output.filename`. CSS entries set their
// `.css` name via `cssFilenameTemplate` below; `html` page entries
// emit their file via `renderManifest`.
filename: compilation.outputOptions.chunkFilename || "[name].js",
dependOn: spec.dependOn
},
(err, entryModule) =>
err ? reject(err) : resolve(entryModule || null)
);
});
// Create one HTML module's entries. A `type: "html"` link is itself an
// HTML entry, so after it builds we recurse into it — the same handling
// every HTML entry already gets. `processed` guards diamonds and cycles.
/** @type {WeakSet<import("../Module")>} */
const processed = new WeakSet();
/** @type {(module: import("../Module")) => Promise<void>} */
const processEntry = async (module) => {
if (processed.has(module)) return;
processed.add(module);
await Promise.all(
collectEntrySpecs(module).map(async (spec) => {
const entryModule = await addEntry(spec);
if (entryModule && entryModule.type === HTML_MODULE_TYPE) {
await processEntry(entryModule);
}
})
);
};
// Seed with the HTML modules seen during make (config entries /
// imports, fresh or cache-restored); linked pages are reached by
// recursion above.
Promise.all([...htmlModules].map(processEntry)).then(
() => callback(),
callback
);
});
// `csp` injects a `<meta http-equiv="Content-Security-Policy">` and
// `integrity` resolves the SRI sentinels — both once the page's inline
// content is final (in `processAssets` below), per page, so an entry's
// `html` can override them.
// `"auto"` marks implicit enablement (no user rule for HTML files) — see
// `applyExperimentsDefaults`. Only then loaders win over the built-in type
// (e.g. html-webpack-plugin's template loader in its child compiler).
// TODO webpack 6: html defaults to `true`, drop this implicit-only fallback.
const implicitlyEnabled = compiler.options.experiments.html === "auto";
compiler.hooks.compilation.tap(
PLUGIN_NAME,
(compilation, { normalModuleFactory }) => {
if (implicitlyEnabled) {
implicitTypeLoaderFallback(
normalModuleFactory,
PLUGIN_NAME,
/\.html$/i,
HTML_MODULE_TYPE
);
}
const { htmlModules } = getState(compilation);
/**
* Records HTML modules as they are built or restored, so `finishMake`
* creates their entries without scanning the whole module graph.
* @param {import("../Module")} module built or restored module
* @returns {void}
*/
const trackHtmlModule = (module) => {
if (module.type !== HTML_MODULE_TYPE) return;
// enabled here, not on the compilation hook: an integrity build
// with no html module must not load the generator
if (anyIntegrity) {
getHtmlGenerator().enableIntegritySentinels(compilation);
}
htmlModules.add(module);
};
compilation.hooks.succeedModule.tap(PLUGIN_NAME, trackHtmlModule);
compilation.hooks.stillValidModule.tap(PLUGIN_NAME, trackHtmlModule);
// Resolve integrity and inline sentinels after `RealContentHashPlugin`
// so final bytes are in place. Only HTML pages carry these sentinels —
// never JS chunks that embed an HTML string.
// SRI only takes effect on a cross-origin subresource fetched
// with CORS; without `output.crossOriginLoading` the browser
// silently ignores `integrity` on cross-origin loads. Warn once
// so a CDN deployment doesn't ship no-op integrity attributes.
if (anyIntegrity && !compilation.outputOptions.crossOriginLoading) {
compilation.warnings.push(
new WebpackError(
'output.html.integrity is set but output.crossOriginLoading is not. Browsers ignore Subresource Integrity on cross-origin requests made without CORS; set output.crossOriginLoading (e.g. "anonymous") if any asset is served from a different origin.'
)
);
}
compilation.hooks.processAssets.tapPromise(
{
name: PLUGIN_NAME,
stage: Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_HASH + 1
},
async (assets) => {
const { htmlAssetOptions } = getState(compilation);
const hooks = HtmlModulesPlugin.getCompilationHooks(compilation);
/** @type {Set<string>} */
const inlinedFiles = new Set();
for (const name of Object.keys(assets)) {
const pageOptions = htmlAssetOptions.get(name);
if (pageOptions === undefined) continue;
const content = assets[name].source();
if (typeof content !== "string") continue;
const { csp, integrity } = pageOptions;
let resolved = content;
// Sentinels are emitted whenever any page wants SRI, so a page
// that doesn't drops them again here.
if (resolved.includes("__WEBPACK_HTML_INTEGRITY__")) {
resolved = isIntegrityEnabled(integrity)
? getHtmlGenerator().resolveChunkIntegritySentinels(
resolved,
compilation,
/** @type {import("./HtmlGenerator").HtmlIntegrity} */ (
integrity
)
)
: getHtmlGenerator().stripChunkIntegritySentinels(resolved);
}
if (resolved.includes("__WEBPACK_HTML_INLINE__")) {
resolved = getHtmlGenerator().resolveChunkInlineSentinels(
resolved,
compilation,
name,
inlinedFiles
);
}
// `injectTags` (add tags), `transformTags` (mutate/move/remove the
// page's own and injected tags), and CSP all run over a single
// parse: collect the tags/anchors once, let the hooks act, then
// render every edit — placement, attribute rewrites, moves,
// removals, CSP hashes/nonce/meta — in one pass. Injected inline
// `<script>`/`<style>` are hashed like the page's own.
const injected = await hooks.injectTags.promise([], {
outputName: name,
html: resolved
});
const transformTags = hooks.transformTags.taps.length > 0;
if (injected.length > 0 || transformTags || csp) {
const model = getHtmlGenerator().collectHtml(resolved);
getHtmlGenerator().addInjectedTags(model, injected);
if (transformTags) {
await hooks.transformTags.promise(model.tags, {
outputName: name,
html: resolved
});
}
resolved = getHtmlGenerator().renderHtml(resolved, model, csp);
}
// Final, fully-resolved HTML — let plugins transform it (minify,
// inject a CSP meta, rewrite tags) before it is written.
resolved = await hooks.transformHtml.promise(resolved, {
outputName: name
});
if (resolved !== content) {
compilation.updateAsset(name, new RawSource(resolved));
}
await hooks.htmlEmitted.promise({ outputName: name });
}
// An inlined chunk file is dead weight once nothing else references
// it by URL (another page's tag, or the runtime's async chunk map);
// a content-hashed filename makes a substring hit a real reference.
/** @type {Set<string>} */
const deletionCandidates = new Set(inlinedFiles);
// A wrapper entry's chunk only rebuilds the page's own markup; the
// page loads the `<script src>` entries the parser split out of it.
for (const entrypoint of compilation.entrypoints.values()) {
const chunk = entrypoint.getEntrypointChunk();
if (!chunk) continue;
let wrapperOnly = false;
for (const entryModule of compilation.chunkGraph.getChunkEntryModulesIterable(
chunk
)) {
const { resource } = /** @type {import("../NormalModule")} */ (
entryModule
);
wrapperOnly =
typeof resource === "string" &&
HTML_DATA_URL_RE.test(resource);
if (!wrapperOnly) break;
}
if (!wrapperOnly) continue;
for (const file of chunk.files) deletionCandidates.add(file);
}
// Materialize each other asset's source once (it can be expensive to
// render) and test all candidates against it, rather than
// re-materializing every asset per candidate.
if (deletionCandidates.size > 0) {
/** @type {Set<string>} */
const stillReferenced = new Set();
/** @type {string[]} */
const pending = [];
/**
* @param {string} name asset whose source may name a candidate
*/
const markReferenced = (name) => {
const source = compilation.assets[name].source();
if (typeof source !== "string") return;
for (const file of deletionCandidates) {
if (!stillReferenced.has(file) && source.includes(file)) {
stillReferenced.add(file);
pending.push(file);
}
}
};
for (const name of Object.keys(compilation.assets)) {
if (
deletionCandidates.has(name) ||
stillReferenced.size === deletionCandidates.size
) {
continue;
}
markReferenced(name);
}
// A kept candidate can reference another through its async chunk
// map, so follow each until nothing new is kept.
while (pending.length > 0) {
markReferenced(/** @type {string} */ (pending.pop()));
}
for (const file of deletionCandidates) {
if (!stillReferenced.has(file)) compilation.deleteAsset(file);
}
}
}
);
// CSS entries created by `<link rel="stylesheet">` in HTML need
// their `.css` filename set via `chunk.cssFilenameTemplate`
// (the field `CssModulesPlugin.getChunkFilenameTemplate` reads).
// Compilation only flows `options.filename` to `chunk.filenameTemplate`,
// which controls JS emit — there's no entry-level `cssFilename`.
// Set it ourselves after chunks are created so each stylesheet
// entry emits to a distinct file derived from `output.cssFilename`
// (or `output.cssChunkFilename` for non-initial CSS chunks).
compilation.hooks.afterChunks.tap(PLUGIN_NAME, () => {
const { stylesheetEntries } = getState(compilation);
if (stylesheetEntries.size === 0) return;
for (const entryName of stylesheetEntries) {
const entrypoint = compilation.entrypoints.get(entryName);
if (!entrypoint) continue;
const chunk = entrypoint.getEntrypointChunk();
if (!chunk) continue;
// Each html-derived stylesheet entry uses the
// `cssChunkFilename` template — even though the entry
// chunk technically `canBeInitial()`, we deliberately
// avoid `cssFilename` here because that template often
// has no per-entry placeholder (it's derived from
// `output.filename`, which can be a literal like
// `bundle0.js`), and multiple `<link rel="stylesheet">`
// tags would then collide on the same emitted `.css`
// file. `cssChunkFilename` is derived from
// `output.chunkFilename` which webpack auto-extends
// with `[id].` when needed, guaranteeing uniqueness.
chunk.cssFilenameTemplate =
compilation.outputOptions.cssChunkFilename;
}
});
compilation.dependencyTemplates.set(
StaticExportsDependency,
new StaticExportsDependency.Template()
);
// `ConstDependency` is used by HtmlParser to insert
// ` type="module"` into the rewritten <script> tag when
// `output.module` is on. Register its template so the HTML
// generator runs the insertion.
compilation.dependencyTemplates.set(
ConstDependency,
new ConstDependency.Template()
);
// registered when the first html module is prepared, so a build
// without html never loads these dependency classes
normalModuleFactory.hooks.prepareModuleType
.for(HTML_MODULE_TYPE)
.tap(PLUGIN_NAME, () => {
const HtmlSourceDependency = require("../dependencies/HtmlSourceDependency");
const HtmlEntryDependency = require("../dependencies/HtmlEntryDependency");
const HtmlInlineScriptDependency = require("../dependencies/HtmlInlineScriptDependency");
const HtmlInlineStyleDependency = require("../dependencies/HtmlInlineStyleDependency");
const HtmlInlineHtmlDependency = require("../dependencies/HtmlInlineHtmlDependency");
compilation.dependencyFactories.set(
HtmlSourceDependency,
normalModuleFactory
);
compilation.dependencyTemplates.set(
HtmlSourceDependency,
new HtmlSourceDependency.Template()
);
compilation.dependencyFactories.set(
HtmlEntryDependency,
normalModuleFactory
);
compilation.dependencyTemplates.set(
HtmlEntryDependency,
new HtmlEntryDependency.Template()
);
// Inline `<script>` content is bundled as its own entry — the
// same pipeline that handles `<script src>` — via a
// `data:text/javascript,...` request. The dependency
// template rewrites the original tag to `<script src=…>`.
compilation.dependencyFactories.set(
HtmlInlineScriptDependency,
normalModuleFactory
);
compilation.dependencyTemplates.set(
HtmlInlineScriptDependency,
new HtmlInlineScriptDependency.Template()
);
// Inline `<style>` content is routed through the CSS pipeline
// as a `data:text/css` module. The dependency template reads
// the processed CSS text from the CSS module's code
// generation data (`css-text` channel set by CssGenerator
// when `exportType` is `"text"`).
compilation.dependencyFactories.set(
HtmlInlineStyleDependency,
normalModuleFactory
);
compilation.dependencyTemplates.set(
HtmlInlineStyleDependency,
new HtmlInlineStyleDependency.Template()
);
// `<iframe srcdoc>` content is routed back through the HTML
// pipeline as a `data:text/html` module; the template reads the
// processed HTML from the nested module's `html` channel.
compilation.dependencyFactories.set(
HtmlInlineHtmlDependency,
normalModuleFactory
);
compilation.dependencyTemplates.set(
HtmlInlineHtmlDependency,
new HtmlInlineHtmlDependency.Template()
);
});
preloadModuleType(normalModuleFactory, PLUGIN_NAME, [
[HTML_MODULE_TYPE, [getHtmlParser]]
]);
normalModuleFactory.hooks.createModuleClass
.for(HTML_MODULE_TYPE)
.tap(
PLUGIN_NAME,
(createData, _resolveData) => new HtmlModule(createData)
);
normalModuleFactory.hooks.createParser
.for(HTML_MODULE_TYPE)
.tap(PLUGIN_NAME, (parserOptions) => {
compiler.validate(
() => getSchema("HtmlParserOptions"),
parserOptions,
parserValidationOptions,
(options) =>
require("../../schemas/plugins/HtmlParserOptions.check")(
options
)
);
return new (getHtmlParser.loaded())(parserOptions);
});
normalModuleFactory.hooks.createGenerator
.for(HTML_MODULE_TYPE)
.tap(PLUGIN_NAME, (generatorOptions) => {
compiler.validate(
() => getSchema("HtmlGeneratorOptions"),
generatorOptions,
generatorValidationOptions,
(options) =>
require("../../schemas/plugins/HtmlGeneratorOptions.check")(
options
)
);
return new (getHtmlGenerator())(
generatorOptions,
compilation.moduleGraph
);
});
NormalModule.getCompilationHooks(compilation).processResult.tap(
PLUGIN_NAME,
(result, module) => {
if (module.type === HTML_MODULE_TYPE) {
const [source, ...rest] = removeBOMFromResult(result);
// `applyTemplate` is a no-op unless `module.parser.html.template`
// is set. Running it here (where the returned source becomes the
// module's stored source) keeps the parser's dependency offsets
// and the generator's render base in sync.
const parser = /** @type {HtmlParser} */ (module.parser);
return [parser.applyTemplate(source, module), ...rest];
}
return result;
}
);
// Emit extracted `.html` files for any HTML module that opted
// into extraction. The opt-in is computed by
// `HtmlGenerator#_shouldExtract`: `module.generator.html.extract:
// true` always extracts, `false` never extracts, and when
// `extract` is unset the generator extracts iff the HTML module
// is a compilation entry — the iteration below picks up only
// modules whose generator reported the `html` source type, so
// that decision is honored implicitly. The HTML content is read
// from the generator's secondary `"html"` source type (see
// HtmlGenerator#generate). The filename template comes from
// `output.htmlFilename` (initial chunks) or
// `output.htmlChunkFilename` (non-initial chunks), mirroring
// the CSS pipeline. Path data follows the asset-module pattern —
// `module` + a relative source `filename`, with `chunk`
// intentionally omitted so `[name]` resolves to the HTML
// source's basename (e.g. `page` for `./page.html`) rather
// than the importing chunk's name (e.g. `main`). A per-module
// content hash is computed from the rewritten HTML so the
// template's `[contenthash]` placeholder works; the
// compilation hash is also forwarded so `[fullhash]` /
// `[hash]` work in user-supplied templates.
// Sentinel-resolved content and its content hash depend only on
// the HTML-type module source — not on the chunk or output
// filename — yet `renderManifest` runs per `(chunk, module)`.
// Memoize so a module landing in multiple chunks resolves and
// hashes its sentinels once. Scoped to this compilation because
// sentinel resolution embeds chunk filenames, which change
// across rebuilds; weakly keyed by source so entries release.
/** @type {WeakMap<import("webpack-sources").Source, { resolvedContent: string, contentHash: string }>} */
const resolvedSentinelHashCache = new WeakMap();
/**
* Computes an HTML page's emitted filename, chunk-URL-resolved content and
* content hash. Links to other pages stay sentinels, so a page's filename
* never depends on the filenames of the pages it links to.
* @param {NormalModule} module the page's html module
* @param {import("../Chunk")} chunk chunk the page is emitted from
* @returns {{ resolvedContent: string, contentHash: string, filename: string, info: import("../Compilation").AssetInfo } | undefined} the page's emit, or `undefined` when it generates no html
*/
const computePageEmit = (module, chunk) => {
const { chunkGraph, outputOptions } = compilation;
const codeGenResult =
/** @type {import("../CodeGenerationResults")} */ (
compilation.codeGenerationResults
).get(module, chunk.runtime);
const placeholderSource = codeGenResult.sources.get(HTML_TYPE);
if (!placeholderSource) return undefined;
let cached = resolvedSentinelHashCache.get(placeholderSource);
if (cached === undefined) {
// Resolve chunk-URL sentinels *before* hashing so the HTML's
// `[contenthash]` invalidates with the referenced chunks'
// filenames. Inlined chunks have no URL, so tag their inline
// sentinels with the chunk content hash — that keeps the emitted
// bytes (and RealContentHashPlugin's later recompute)
// content-dependent, so the page hash tracks inlined content too.
// Asset URL sentinels (from `HtmlEntryDependency` resource-hint
// tags) resolve here too — deferred from template.apply so it
// doesn't race with asset-module codegen.
const resolvedContent = getHtmlGenerator().resolveAssetUrlSentinels(
getHtmlGenerator().embedInlineChunkHashes(
getHtmlGenerator().resolveChunkUrlSentinels(
/** @type {string} */ (placeholderSource.source()),
compilation
),
compilation
),
compilation
);
cached = {
resolvedContent,
contentHash: HtmlModulesPlugin.computeContentHash(
resolvedContent,
outputOptions
)
};
resolvedSentinelHashCache.set(placeholderSource, cached);
}
const resource = module.getResource() || module.resource;
// Synthetic `output.html` entries are `data:text/html` modules with no
// real basename — name the file after the entry instead.
const sourceFilename =
resource.startsWith("data:text/html") && chunk.name
? chunk.name
: toTemplateSourceFileName(
makePathsRelative(
compiler.context,
/** @type {string} */ (resource),
compiler.root
).replace(/^\.\//, ""),
compilation
);
const filenameTemplate = HtmlModulesPlugin.getChunkFilenameTemplate(
chunk,
outputOptions
);
const { path: filename, info } = compilation.getPathWithInfo(
/** @type {import("../TemplatedPathPlugin").TemplatePath} */
(filenameTemplate),
{
module,
runtime: chunk.runtime,
chunkGraph,
contentHash: cached.contentHash,
contentHashType: HTML_TYPE,
filename: sourceFilename,
hash: compilation.hash
}
);
return {
resolvedContent: cached.resolvedContent,
contentHash: cached.contentHash,
filename,
info
};
};
/** @type {Map<string, string>} */
const htmlPageFilenameCache = new Map();
/**
* Emitted filename of a linked `type: "html"` page, read from the page's
* own entry chunk so chunk render order cannot affect it.
* @param {string} entryName entry name of the linked page
* @returns {string} the page's emitted filename, `"data:,"` when it has none
*/
const computePageFilenameByEntry = (entryName) => {
const cached = htmlPageFilenameCache.get(entryName);
if (cached !== undefined) return cached;
let filename = "data:,";
const entrypoint = compilation.entrypoints.get(entryName);
const chunk = entrypoint && entrypoint.getEntrypointChunk();
if (chunk) {
const modules =
compilation.chunkGraph.getOrderedChunkModulesIterableBySourceType(
chunk,
HTML_TYPE,
compareModulesByFullName(compilation.compiler)
);
const module = modules && modules[Symbol.iterator]().next().value;
if (module) {
const emit = computePageEmit(
/** @type {NormalModule} */ (module),
chunk
);
if (emit) filename = emit.filename;
}
}
htmlPageFilenameCache.set(entryName, filename);
return filename;
};
compilation.hooks.renderManifest.tap(
PLUGIN_NAME,
(result, { chunk }) => {
// HMR's `HotUpdateChunk`s flow through the same hook
// but aren't real output chunks — extracting `.html`
// for them would create stray hot-update HTML files.
// `CssModulesPlugin` early-returns for the same reason.
if (chunk instanceof HotUpdateChunk) return result;
const { chunkGraph } = compilation;
const modules =
chunkGraph.getOrderedChunkModulesIterableBySourceType(
chunk,
HTML_TYPE,
compareModulesByFullName(compilation.compiler)
);
if (!modules) return result;
const outputOptions = compilation.outputOptions;
for (const module of modules) {
const normalModule = /** @type {NormalModule} */ (module);
// `<iframe srcdoc>` modules expose `html` only so HtmlInlineHtmlDependency
// can write the processed markup back into the host attribute — they are
// never standalone pages. `generator.extract: "inline"` marks them so no
// `.html` file is emitted; without this they'd collide on the
// `data:text/html` → chunk-name path (every srcdoc module in a chunk would
// emit `<chunk>.html`). An O(1) flag check, not a scan.
const generatorOptions = normalModule.generatorOptions;
if (generatorOptions && generatorOptions.extract === "inline") {
continue;
}
const emit = computePageEmit(normalModule, chunk);
if (!emit) continue;
const { resolvedContent, contentHash, filename, info } = emit;
// Resolve any remaining `[webpack/auto]` placeholders to an undo
// path computed from the emitted HTML's location, so asset/chunk
// URLs stay relative to `output.path` even when the page emits into
// a subdirectory. A relative `<base href>` prepends `../`s so the
// base can't misdirect the rewritten URLs (see `HtmlParser`).
const basePrefix =
/** @type {HtmlModuleBuildInfo} */ (normalModule.buildInfo)
.baseUrlPrefix || "";
const undoPath =
basePrefix +
getUndoPath(
filename,
/** @type {string} */ (outputOptions.path),
false
);
// Resolve linked-page (`type: "html"`) sentinels to each page's
// emitted filename before the undo path, so the href is relative to
// this page.
const finalContent = getHtmlGenerator()
.resolveHtmlPageUrlSentinels(
resolvedContent,
computePageFilenameByEntry
)
.split(autoPlaceholder)
.join(undoPath);
const finalSource = new RawSource(finalContent);
// The same HTML module can land in multiple chunks with different
// `htmlFilename`/`htmlChunkFilename` shapes → different `undoPath`s and
// final content for the same module id. Include the emitted filename in
// the asset cache key and the post-undo-path content in the hash so the
// asset cache can't reuse one variant's bytes at another variant's URL.
// Unchanged content reuses the memoized hash instead of re-digesting.
const finalContentHash =
finalContent === resolvedContent
? contentHash
: HtmlModulesPlugin.computeContentHash(
finalContent,
outputOptions
);
// Track HTML pages (never JS chunks) with the html options of the
// entry that owns them, read back by the emit passes below.
getState(compilation).htmlAssetOptions.set(
filename,
(chunk.name && htmlOptionsByEntry.get(chunk.name)) ||
globalHtmlOptions ||
{}
);
result.push({
render: () => finalSource,
filename,
info,
auxiliary: true,
identifier: `htmlModule${chunkGraph.getModuleId(
module
)}|${filename}`,
hash: finalContentHash
});
}
return result;
}
);
// Resolve sentinels at JS chunk render time so later passes
// (SourceMapDevToolPlugin, size optimizers, RealContentHash) see resolved bytes.
const jsHooks =
JavascriptModulesPlugin.getCompilationHooks(compilation);
jsHooks.render.tap(PLUGIN_NAME, (source, renderContext) => {
// No HTML modules ⇒ no sentinels; skip materializing the JS source.
if (htmlModules.size === 0) return source;
const raw = source.source();
if (typeof raw !== "string") return source;
// Every sentinel shares this prefix — one scan answers the common
// sentinel-free case instead of five.
if (!raw.includes("__WEBPACK_HTML_")) {
return source;
}
// Exact re-check on a prefix hit: the autoPlaceholder strip below is
// not sentinel-guarded, so bare-prefix false positives must bail here.
if (
!raw.includes("__WEBPACK_HTML_CHUNK_URL__") &&
!raw.includes("__WEBPACK_HTML_PAGE_URL__") &&
!raw.includes("__WEBPACK_HTML_INTEGRITY__") &&
!raw.includes("__WEBPACK_HTML_INLINE__") &&
!raw.includes("__WEBPACK_HTML_ASSET_URL__")
) {
return source;
}
// Strip integrity and inline sentinels (not resolve): a JS chunk
// can't hold real SRI hashes for its own not-yet-final bytes, and
// inline content belongs only in the final .html, never in JS bundles.
const resolved = getHtmlGenerator()
.stripChunkIntegritySentinels(
getHtmlGenerator()
.resolveAssetUrlSentinels(
getHtmlGenerator().resolveHtmlPageUrlSentinels(
getHtmlGenerator().resolveChunkUrlSentinels(
raw,
compilation
),
computePageFilenameByEntry
),
compilation
)
.split(autoPlaceholder)
.join("")
)
.replace(
/__WEBPACK_HTML_INLINE__[0-9a-f]+__[a-z]+(?:__[0-9a-f]+)?__END__/g,
""
);
if (resolved === raw) return source;
const chunkId = String(renderContext.chunk.id);
const prior = sentinelResolvedSourceCache.get(chunkId);
if (prior !== undefined && prior.content === resolved) {
return prior.source;
}
const newSource = new RawSource(resolved);
sentinelResolvedSourceCache.set(chunkId, {
content: resolved,
source: newSource
});
return newSource;
});
// Prune cache entries for chunks no longer in the graph so a
// long watch session can't accumulate stale entries.
compilation.hooks.afterSeal.tap(PLUGIN_NAME, () => {
if (sentinelResolvedSourceCache.size === 0) return;
const live = new Set();
for (const chunk of compilation.chunks) {
live.add(String(chunk.id));
}
for (const id of sentinelResolvedSourceCache.keys()) {
if (!live.has(id)) sentinelResolvedSourceCache.delete(id);
}
});
}
);
}
}
/**
* Per-compilation hooks for the experimental HTML support.
* @param {Compilation} compilation the compilation
* @returns {HtmlCompilationHooks} the hooks
*/
HtmlModulesPlugin.getCompilationHooks = createHooksRegistry(
createCompilationHooks
);
module.exports = HtmlModulesPlugin;