`third_party/node/node_modules` is generated by `./mach vendor node`. Differential Revision: https://phabricator.services.mozilla.com/D320322
1112 lines
46 KiB
JavaScript
1112 lines
46 KiB
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 ConstDependency = require("../dependencies/ConstDependency");
|
|
const HtmlEntryDependency = require("../dependencies/HtmlEntryDependency");
|
|
const HtmlInlineHtmlDependency = require("../dependencies/HtmlInlineHtmlDependency");
|
|
const HtmlInlineScriptDependency = require("../dependencies/HtmlInlineScriptDependency");
|
|
const HtmlInlineStyleDependency = require("../dependencies/HtmlInlineStyleDependency");
|
|
const HtmlSourceDependency = require("../dependencies/HtmlSourceDependency");
|
|
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 memoize = require("../util/memoize");
|
|
const { digestNonNumericOnly } = require("../util/nonNumericOnlyHash");
|
|
const {
|
|
PUBLIC_PATH_AUTO: autoPlaceholder
|
|
} = require("../util/publicPathPlaceholder");
|
|
const removeBOM = require("../util/removeBOM");
|
|
const HtmlGenerator = require("./HtmlGenerator");
|
|
const HtmlModule = require("./HtmlModule");
|
|
const HtmlParser = require("./HtmlParser");
|
|
const { escapeAttribute } = require("./syntax");
|
|
|
|
/** @typedef {import("../../declarations/WebpackOptions").EntryDescriptionNormalized} EntryDescriptionNormalized */
|
|
/** @typedef {import("../../declarations/WebpackOptions").OutputHtmlOptions["favicon"]} FaviconOption */
|
|
/** @typedef {import("../../declarations/WebpackOptions").OutputHtmlOptions["manifest"]} ManifestOption */
|
|
/** @typedef {import("../../declarations/WebpackOptions").HtmlFaviconIcon} HtmlFaviconIcon */
|
|
/** @typedef {Exclude<HtmlFaviconIcon, string>} FaviconIcon */
|
|
/** @typedef {import("../Compiler")} Compiler */
|
|
/** @typedef {import("./HtmlModule").HtmlModuleBuildInfo} HtmlModuleBuildInfo */
|
|
/** @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 */
|
|
/**
|
|
* @typedef {object} HtmlCompilationHooks
|
|
* @property {AsyncSeriesWaterfallHook<[HtmlTagDescriptor[], HtmlInjectTagsContext]>} injectTags 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
|
|
* @property {AsyncSeriesHook<[HtmlMutableTag[], HtmlTransformTagsContext]>} transformTags 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
|
|
* @property {AsyncSeriesWaterfallHook<[string, HtmlTransformHtmlContext]>} transformHtml 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
|
|
* @property {AsyncSeriesHook<[HtmlEmittedContext]>} htmlEmitted called once each page's HTML asset has been finalized — a post-emit notification (nothing to return)
|
|
*/
|
|
|
|
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`.
|
|
const getMimeTypes = memoize(() => require("../util/mimeTypes"));
|
|
|
|
/**
|
|
* 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 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="${escapeAttribute(href)}">`;
|
|
};
|
|
|
|
// `\.html`/`\.css` request matchers for the synthetic `output.html` wrapper.
|
|
const HTML_REQUEST_RE = /\.html(\?|$)/i;
|
|
const CSS_REQUEST_RE = /\.css(\?|$)/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();
|
|
const walk = (/** @type {string} */ 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 scriptLoading =
|
|
(typeof htmlOption === "object" && htmlOption.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") {
|
|
compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
|
|
compilation.warnings.push(
|
|
new WebpackError(
|
|
`output.html.scriptLoading: "${scriptLoading}" is ignored with output.module — ES module scripts are always deferred.`
|
|
)
|
|
);
|
|
});
|
|
}
|
|
} else if (scriptLoading === "blocking") {
|
|
scriptAttr = "";
|
|
}
|
|
|
|
// `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) => {
|
|
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 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 htmlObj =
|
|
typeof html === "object"
|
|
? html
|
|
: typeof htmlOption === "object"
|
|
? htmlOption
|
|
: {};
|
|
const headTags = HtmlParser.buildHeadTags(htmlObj);
|
|
// Injected into webpack-generated pages only; each icon's href is a
|
|
// normal request the parser turns into a hashed asset.
|
|
const favicon =
|
|
typeof html === "object" && html.favicon !== undefined
|
|
? html.favicon
|
|
: false;
|
|
const faviconTag = resolveFaviconLinks(favicon, name)
|
|
.map(([rel, icon]) => faviconLinkTag(rel, icon))
|
|
.join("");
|
|
const manifest =
|
|
typeof html === "object" && html.manifest !== undefined
|
|
? html.manifest
|
|
: false;
|
|
const manifestTag = manifestLinkTag(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) and the stylesheet entry names (read in `afterChunks`).
|
|
/** @type {WeakMap<import("../Compilation"), { htmlModules: Set<import("../Module")>, stylesheetEntries: Set<string>, htmlAssetNames: Set<string> }>} */
|
|
const compilationState = new WeakMap();
|
|
/**
|
|
* @param {import("../Compilation")} compilation compilation
|
|
* @returns {{ htmlModules: Set<import("../Module")>, stylesheetEntries: Set<string>, htmlAssetNames: Set<string> }} per-compilation state
|
|
*/
|
|
const getState = (compilation) => {
|
|
let state = compilationState.get(compilation);
|
|
if (state === undefined) {
|
|
state = {
|
|
htmlModules: new Set(),
|
|
stylesheetEntries: new Set(),
|
|
htmlAssetNames: new Set()
|
|
};
|
|
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
|
|
);
|
|
});
|
|
|
|
const integrity =
|
|
typeof htmlOption === "object" ? htmlOption.integrity : undefined;
|
|
const integrityOn =
|
|
integrity === true ||
|
|
typeof integrity === "function" ||
|
|
(Array.isArray(integrity) && integrity.length > 0);
|
|
// `output.html.csp` injects a `<meta http-equiv="Content-Security-Policy">`
|
|
// once the page's inline content is final (in `processAssets` below).
|
|
const csp = typeof htmlOption === "object" ? htmlOption.csp : undefined;
|
|
// `"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);
|
|
// Record HTML modules as they appear — freshly built
|
|
// (`succeedModule`) or restored from cache (`stillValidModule`) —
|
|
// so `finishMake` creates their entries without scanning every
|
|
// module in the graph. Tracking only records; entries are still
|
|
// added (and awaited) in `finishMake`.
|
|
const trackHtmlModule = (/** @type {import("../Module")} */ module) => {
|
|
if (module.type === HTML_MODULE_TYPE) 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 (integrityOn && !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 { htmlAssetNames } = getState(compilation);
|
|
const hooks = HtmlModulesPlugin.getCompilationHooks(compilation);
|
|
/** @type {Set<string>} */
|
|
const inlinedFiles = new Set();
|
|
for (const name of Object.keys(assets)) {
|
|
if (!htmlAssetNames.has(name)) continue;
|
|
const content = assets[name].source();
|
|
if (typeof content !== "string") continue;
|
|
let resolved = content;
|
|
if (
|
|
integrityOn &&
|
|
resolved.includes("__WEBPACK_HTML_INTEGRITY__")
|
|
) {
|
|
resolved = HtmlGenerator.resolveChunkIntegritySentinels(
|
|
resolved,
|
|
compilation,
|
|
/** @type {import("./HtmlGenerator").HtmlIntegrity} */ (
|
|
integrity
|
|
)
|
|
);
|
|
}
|
|
if (resolved.includes("__WEBPACK_HTML_INLINE__")) {
|
|
resolved = HtmlGenerator.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 = HtmlGenerator.collectHtml(resolved);
|
|
HtmlGenerator.addInjectedTags(model, injected);
|
|
if (transformTags) {
|
|
await hooks.transformTags.promise(model.tags, {
|
|
outputName: name,
|
|
html: resolved
|
|
});
|
|
}
|
|
resolved = HtmlGenerator.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.
|
|
// Materialize each other asset's source once (it can be expensive to
|
|
// render) and test all inlined files against it, rather than
|
|
// re-materializing every asset per inlined file.
|
|
if (inlinedFiles.size > 0) {
|
|
/** @type {Set<string>} */
|
|
const stillReferenced = new Set();
|
|
for (const name of Object.keys(compilation.assets)) {
|
|
if (
|
|
inlinedFiles.has(name) ||
|
|
stillReferenced.size === inlinedFiles.size
|
|
) {
|
|
continue;
|
|
}
|
|
const source = compilation.assets[name].source();
|
|
if (typeof source !== "string") continue;
|
|
for (const file of inlinedFiles) {
|
|
if (!stillReferenced.has(file) && source.includes(file)) {
|
|
stillReferenced.add(file);
|
|
}
|
|
}
|
|
}
|
|
for (const file of inlinedFiles) {
|
|
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.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()
|
|
);
|
|
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()
|
|
);
|
|
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 HtmlParser(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 HtmlGenerator(generatorOptions, compilation.moduleGraph);
|
|
});
|
|
|
|
NormalModule.getCompilationHooks(compilation).processResult.tap(
|
|
PLUGIN_NAME,
|
|
(result, module) => {
|
|
if (module.type === HTML_MODULE_TYPE) {
|
|
const [source, ...rest] = 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(removeBOM(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();
|
|
|
|
// Compute a linked/entry HTML page's emitted filename + chunk-URL-resolved
|
|
// content + content hash for one (module, chunk). Shared by the emit loop
|
|
// below and the page-link resolver. The content hash covers only the
|
|
// chunk-URL-resolved source — links to *other* pages stay sentinels here,
|
|
// so a page's filename doesn't depend on the filenames of pages it links to.
|
|
const computePageEmit = (
|
|
/** @type {NormalModule} */ module,
|
|
/** @type {import("../Chunk")} */ 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 = HtmlGenerator.resolveAssetUrlSentinels(
|
|
HtmlGenerator.embedInlineChunkHashes(
|
|
HtmlGenerator.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
|
|
: makePathsRelative(
|
|
compiler.context,
|
|
/** @type {string} */ (resource),
|
|
compiler.root
|
|
).replace(/^\.\//, "");
|
|
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
|
|
};
|
|
};
|
|
|
|
// Emitted filename of a linked `type: "html"` page, keyed by its entry
|
|
// name. Looked up from the page's own entry chunk so it's independent of
|
|
// chunk render order; cached for the compilation.
|
|
/** @type {Map<string, string>} */
|
|
const htmlPageFilenameCache = new Map();
|
|
const computePageFilenameByEntry = (
|
|
/** @type {string} */ 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 = HtmlGenerator.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 for the integrity sentinel pass (never JS chunks).
|
|
getState(compilation).htmlAssetNames.add(filename);
|
|
|
|
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;
|
|
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 = HtmlGenerator.stripChunkIntegritySentinels(
|
|
HtmlGenerator.resolveAssetUrlSentinels(
|
|
HtmlGenerator.resolveHtmlPageUrlSentinels(
|
|
HtmlGenerator.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(
|
|
() =>
|
|
/** @type {HtmlCompilationHooks} */ ({
|
|
injectTags: new AsyncSeriesWaterfallHook(["tags", "context"]),
|
|
transformTags: new AsyncSeriesHook(["tags", "context"]),
|
|
transformHtml: new AsyncSeriesWaterfallHook(["html", "context"]),
|
|
htmlEmitted: new AsyncSeriesHook(["context"])
|
|
})
|
|
);
|
|
|
|
module.exports = HtmlModulesPlugin;
|