| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249 |
- /*
- 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 removeBOM = 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;
- /**
- * 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 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);
- // 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) 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.
- // 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.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] = 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 = 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
- };
- };
- // 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 = 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;
|