`third_party/node/node_modules` is generated by `./mach vendor node`. Differential Revision: https://phabricator.services.mozilla.com/D320322
1971 lines
73 KiB
JavaScript
1971 lines
73 KiB
JavaScript
/*
|
|
MIT License http://www.opensource.org/licenses/mit-license.php
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
const Parser = require("../Parser");
|
|
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 CommentCompilationWarning = require("../errors/CommentCompilationWarning");
|
|
const ModuleDependencyError = require("../errors/ModuleDependencyError");
|
|
const UnsupportedFeatureWarning = require("../errors/UnsupportedFeatureWarning");
|
|
const WebpackError = require("../errors/WebpackError");
|
|
const ResourceHintPlugin = require("../prefetch/ResourceHintPlugin");
|
|
const parseResourceHintOptions = require("../prefetch/parseResourceHintOptions");
|
|
const LazySet = require("../util/LazySet");
|
|
const LocConverter = require("../util/LocConverter");
|
|
const createHash = require("../util/createHash");
|
|
const { contextify } = require("../util/identifier");
|
|
const {
|
|
createMagicCommentContext,
|
|
parseMagicComment,
|
|
webpackCommentRegExp
|
|
} = require("../util/magicComment");
|
|
const {
|
|
NS_HTML,
|
|
NS_SVG,
|
|
NodeType,
|
|
SVG_TAG_ADJUST,
|
|
SourceProcessor,
|
|
decodeEntities,
|
|
escapeAttribute,
|
|
escapeText,
|
|
isAsciiWhitespace,
|
|
parseCssUrls,
|
|
parseMsapplicationTask,
|
|
parseSrc,
|
|
parseSrcset
|
|
} = require("./syntax");
|
|
|
|
/** @typedef {import("../../declarations/WebpackOptions").HtmlParserOptions} HtmlParserOptions */
|
|
/** @typedef {import("../javascript/JavascriptParser").Range} Range */
|
|
/** @typedef {import("../Module")} Module */
|
|
/** @typedef {import("../Module").BuildInfo} BuildInfo */
|
|
/** @typedef {import("../Compilation").FileSystemDependencies} FileSystemDependencies */
|
|
/** @typedef {import("../Module").BuildMeta} BuildMeta */
|
|
/** @typedef {import("../NormalModule")} NormalModule */
|
|
/** @typedef {import("../Parser").ParserState} ParserState */
|
|
/** @typedef {import("../Parser").PreparsedAst} PreparsedAst */
|
|
/** @typedef {import("./HtmlModule").HtmlModuleBuildInfo} HtmlModuleBuildInfo */
|
|
|
|
/**
|
|
* @typedef {object} HtmlTemplateContext
|
|
* @property {Module} module the html module being transformed
|
|
* @property {string} resource absolute path of the module's resource
|
|
* @property {(dependency: string) => void} addDependency register a file (e.g. a template partial) as a build dependency so editing it triggers a rebuild
|
|
* @property {(dependency: string) => void} addContextDependency register a directory as a build dependency
|
|
* @property {(dependency: string) => void} addMissingDependency register a not-yet-existing path as a build dependency so creating it triggers a rebuild
|
|
* @property {(dependency: string) => void} addBuildDependency register a build dependency (e.g. a template engine config) so changing it invalidates the cache
|
|
* @property {(warning: Error | string) => void} emitWarning report a non-fatal warning on the module
|
|
* @property {(error: Error | string) => void} emitError report an error on the module
|
|
*/
|
|
/** @typedef {(source: string, context: HtmlTemplateContext) => string} HtmlTemplateFunction */
|
|
|
|
/** @typedef {import("./syntax").ParsedSource} ParsedSource */
|
|
|
|
// Cheap pre-filter for a `style="..."` attribute: only route it through the
|
|
// CSS pipeline when it can hold a URL-bearing function (`url()` / `src()` /
|
|
// `image()` / `image-set()`), otherwise the processed text equals the input.
|
|
const STYLE_ATTR_URL_REGEXP = /url\(|src\(|image\(|image-set\(/i;
|
|
|
|
// Cheap pre-filter for a `css-url` attribute value (an SVG presentation
|
|
// attribute such as `fill`, `clip-path`, …): only tokenize values that can
|
|
// hold a `url(...)` FuncIRI. Most are plain colors/keywords (`#fff`, `red`).
|
|
const FUNC_IRI_URL_REGEXP = /url\(/i;
|
|
|
|
// Cheap pre-filter for `<iframe srcdoc>` markup: only spin up a nested HTML
|
|
// module when the document can reference an asset — via any attribute (`src=`,
|
|
// `href=`, `style=`, …), a CSS `url(...)`, or a CSS `@import`. Pure
|
|
// formatting/text markup (`<p>hi</p>`) rewrites to itself, so skip it.
|
|
const SRCDOC_ASSET_REGEXP = /[=]|url\(|@import/i;
|
|
|
|
// A URL carrying its own scheme (`https:`, `data:`, …) ignores the document
|
|
// base per the URL spec, so `<base href>` never rewrites it.
|
|
const ABSOLUTE_URL_SCHEME_REGEXP = /^[a-zA-Z][a-zA-Z\d+\-.]*:/;
|
|
|
|
// a `charset=` declaration inside an `http-equiv="content-type"` content value
|
|
const CHARSET_DECLARATION_REGEXP = /charset\s*=/i;
|
|
|
|
/**
|
|
* @param {string} name meta name
|
|
* @param {string} content meta content
|
|
* @returns {string} meta tag
|
|
*/
|
|
const metaTag = (name, content) => {
|
|
// og: uses property=; all others (including twitter:) use name=
|
|
const attr = name.startsWith("og:")
|
|
? `property="${escapeAttribute(name)}"`
|
|
: `name="${escapeAttribute(name)}"`;
|
|
return `<meta ${attr} content="${escapeAttribute(content)}">`;
|
|
};
|
|
|
|
/**
|
|
* @param {string | { href: string, target?: string }} base base option
|
|
* @returns {string} base tag
|
|
*/
|
|
const baseTag = (base) => {
|
|
const href = typeof base === "string" ? base : base.href;
|
|
const targetAttr =
|
|
typeof base === "object" && base.target
|
|
? ` target="${escapeAttribute(base.target)}"`
|
|
: "";
|
|
return `<base href="${escapeAttribute(href)}"${targetAttr}>`;
|
|
};
|
|
|
|
/**
|
|
* Serializes every head tag an `output.html` options object asks for, in
|
|
* spec order: charset, base, meta tags, title. Used where a page is created
|
|
* from scratch (HtmlModulesPlugin's synthetic entry wrapper); authored pages
|
|
* instead merge the options against their existing tags during `parse` —
|
|
* existing tags win.
|
|
* @param {import("../../declarations/WebpackOptions").OutputHtmlOptions} opts html options
|
|
* @returns {string} head tags string
|
|
*/
|
|
const buildHeadTags = (opts) => {
|
|
let out = "";
|
|
const meta = opts.meta;
|
|
if (meta && meta.charset) {
|
|
out += `<meta charset="${escapeAttribute(meta.charset)}">`;
|
|
}
|
|
if (opts.base) out += baseTag(opts.base);
|
|
if (meta) {
|
|
for (const [name, content] of Object.entries(meta)) {
|
|
if (name === "charset") continue;
|
|
out += metaTag(name, content);
|
|
}
|
|
}
|
|
if (opts.title) {
|
|
out += `<title>${escapeText(opts.title)}</title>`;
|
|
}
|
|
return out;
|
|
};
|
|
|
|
// CSP/fetch attributes copied verbatim onto a synthesized sibling `<link>` /
|
|
// `<script>` (`HtmlEntryDependency`). Fixed output order, independent of
|
|
// source order.
|
|
const COPYABLE_SIBLING_ATTRS = ["nonce", "crossorigin", "referrerpolicy"];
|
|
|
|
const CC_QUOTATION = '"'.charCodeAt(0);
|
|
const CC_APOSTROPHE = "'".charCodeAt(0);
|
|
const CC_SLASH = "/".charCodeAt(0);
|
|
|
|
/**
|
|
* Byte-exact source span of an attribute including the single leading
|
|
* whitespace (` name`, ` name=value`, ` name="value"`), mirroring the
|
|
* tokenizer's end-of-attribute rule (`valueEnd + 1` past the closing quote).
|
|
* @param {import("./syntax").HtmlPath} path walk path (used only for attribute-ref reads)
|
|
* @param {string} source HTML source
|
|
* @param {import("./syntax").HtmlAttributeRef} attr attribute ref
|
|
* @returns {string} the attribute's source slice
|
|
*/
|
|
const attrSourceSpan = (path, source, attr) => {
|
|
const valueStart = path.attributeValueStart(attr);
|
|
const valueEnd = path.attributeValueEnd(attr);
|
|
const end =
|
|
valueStart === -1
|
|
? path.attributeNameEnd(attr)
|
|
: source.charCodeAt(valueStart - 1) === CC_QUOTATION ||
|
|
source.charCodeAt(valueStart - 1) === CC_APOSTROPHE
|
|
? valueEnd + 1
|
|
: valueEnd;
|
|
return source.slice(path.attributeNameStart(attr) - 1, end);
|
|
};
|
|
|
|
/**
|
|
* Source span of a tag's `integrity` attribute (offsets relative to
|
|
* `elementStart`, leading whitespace and quotes included), so the template can
|
|
* drop the content-specific author value without re-parsing. Null when absent.
|
|
* @param {import("./syntax").HtmlPath} path walk path (for attribute reads)
|
|
* @param {string} source HTML source
|
|
* @param {number} elementStart offset of the tag's opening `<`
|
|
* @returns {Range | null} the span, or null when there is no `integrity`
|
|
*/
|
|
const captureIntegrityRange = (path, source, elementStart) => {
|
|
const integrityAttr = path.findAttribute("integrity");
|
|
if (integrityAttr === 0 || path.attributeNameStart(integrityAttr) < 0) {
|
|
return null;
|
|
}
|
|
let rs = path.attributeNameStart(integrityAttr);
|
|
while (rs > 0 && isAsciiWhitespace(source.charCodeAt(rs - 1))) {
|
|
rs--;
|
|
}
|
|
let re;
|
|
const ivs = path.attributeValueStart(integrityAttr);
|
|
if (ivs === -1) {
|
|
re = path.attributeNameEnd(integrityAttr);
|
|
} else {
|
|
const q = source.charCodeAt(ivs - 1);
|
|
const ive = path.attributeValueEnd(integrityAttr);
|
|
re = q === CC_QUOTATION || q === CC_APOSTROPHE ? ive + 1 : ive;
|
|
}
|
|
return [rs - elementStart, re - elementStart];
|
|
};
|
|
|
|
/**
|
|
* @param {Map<string, string>} attributes attributes
|
|
* @param {string} name name
|
|
* @returns {string | undefined} attribute value
|
|
*/
|
|
const getAttributeValue = (attributes, name) => attributes.get(name);
|
|
|
|
/** @type {Map<string, Set<string>>} */
|
|
const META = new Map([
|
|
[
|
|
"name",
|
|
new Set([
|
|
// msapplication-TileImage
|
|
"msapplication-tileimage",
|
|
"msapplication-square70x70logo",
|
|
"msapplication-square150x150logo",
|
|
"msapplication-wide310x150logo",
|
|
"msapplication-square310x310logo",
|
|
"msapplication-config",
|
|
// Only the `icon-uri` part is an asset, see `parseMsapplicationTask`
|
|
"msapplication-task",
|
|
"twitter:image",
|
|
"twitter:image:src",
|
|
// Twitter player card: URL to the raw video/media stream
|
|
"twitter:player:stream",
|
|
// Legacy preview-image hint
|
|
"thumbnail"
|
|
])
|
|
],
|
|
[
|
|
"property",
|
|
new Set([
|
|
"og:image",
|
|
"og:image:url",
|
|
"og:image:secure_url",
|
|
"og:audio",
|
|
"og:audio:secure_url",
|
|
"og:video",
|
|
"og:video:secure_url",
|
|
"vk:image"
|
|
])
|
|
],
|
|
[
|
|
"itemprop",
|
|
new Set([
|
|
"image",
|
|
"logo",
|
|
"screenshot",
|
|
"thumbnailurl",
|
|
"contenturl",
|
|
"downloadurl",
|
|
"duringmedia",
|
|
"embedurl",
|
|
"installurl",
|
|
"layoutimage"
|
|
])
|
|
]
|
|
]);
|
|
|
|
/**
|
|
* @param {Map<string, string>} attributes attributes
|
|
* @returns {boolean} true when need to parse, otherwise false
|
|
*/
|
|
const filterLinkItemprop = (attributes) => {
|
|
const itemprop = getAttributeValue(attributes, "itemprop");
|
|
if (!itemprop) return false;
|
|
const allowedAttributes = META.get("itemprop");
|
|
if (!allowedAttributes) return false;
|
|
|
|
return allowedAttributes.has(itemprop.trim().toLowerCase());
|
|
};
|
|
|
|
// `<link rel>` values whose `href`/`imagesrcset` webpack treats as a reference.
|
|
const ALLOWED_LINK_RELS = new Set([
|
|
"stylesheet",
|
|
"icon",
|
|
"mask-icon",
|
|
"apple-touch-icon",
|
|
"apple-touch-icon-precomposed",
|
|
"apple-touch-startup-image",
|
|
// Routed to the `asset/webmanifest` module type, which parses its icon URLs.
|
|
"manifest",
|
|
"prefetch",
|
|
"preload",
|
|
"modulepreload",
|
|
// Legacy preview-image hint (`<link rel="image_src" href>`)
|
|
"image_src"
|
|
]);
|
|
|
|
/**
|
|
* @param {Map<string, string>} attributes attributes
|
|
* @returns {boolean} true when need to parse, otherwise false
|
|
*/
|
|
const filterLinkHref = (attributes) => {
|
|
const rel = getAttributeValue(attributes, "rel");
|
|
if (!rel) return false;
|
|
const usedRels = rel.trim().toLowerCase().split(/\s+/);
|
|
for (let i = 0; i < usedRels.length; i++) {
|
|
if (ALLOWED_LINK_RELS.has(usedRels[i])) return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* @param {Map<string, string>} attributes attributes
|
|
* @returns {boolean} true when need to parse, otherwise false
|
|
*/
|
|
const filterLinkUnion = (attributes) =>
|
|
filterLinkHref(attributes) || filterLinkItemprop(attributes);
|
|
|
|
/**
|
|
* @param {Map<string, string>} attributes attributes
|
|
* @returns {boolean} true when need to parse, otherwise false
|
|
*/
|
|
const filterMetaContent = (attributes) => {
|
|
for (const item of META) {
|
|
const [key, allowedNames] = item;
|
|
const name = getAttributeValue(attributes, key);
|
|
if (!name) continue;
|
|
// Check every present attribute, not only the first one
|
|
if (allowedNames.has(name.trim().toLowerCase())) return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* `<param value>` (obsolete `<object>`/`<applet>` child) is only a URL when
|
|
* `valuetype="ref"`; otherwise it's an opaque string.
|
|
* @param {Map<string, string>} attributes attributes
|
|
* @returns {boolean} true when `value` is a URL reference
|
|
*/
|
|
const filterParamRef = (attributes) => {
|
|
const valuetype = getAttributeValue(attributes, "valuetype");
|
|
if (!valuetype) return false;
|
|
return valuetype.trim().toLowerCase() === "ref";
|
|
};
|
|
|
|
/**
|
|
* @param {Map<string, string>} attributes attributes
|
|
* @returns {boolean} true when the script element opts into ES module semantics
|
|
*/
|
|
const isModuleScript = (attributes) => {
|
|
const type = getAttributeValue(attributes, "type");
|
|
if (!type) return false;
|
|
return type.trim().toLowerCase() === "module";
|
|
};
|
|
|
|
// HTML `<script>` `type` values that the browser treats as executable
|
|
// JavaScript. Anything outside this set (e.g. `application/ld+json`,
|
|
// `importmap`, `application/wasm`) is a data block — webpack must not
|
|
// try to bundle it as a JS entry; it should pass through as an asset URL.
|
|
const JS_SCRIPT_TYPES = new Set([
|
|
"",
|
|
"module",
|
|
"text/javascript",
|
|
"application/javascript",
|
|
"text/ecmascript",
|
|
"application/ecmascript"
|
|
]);
|
|
|
|
/**
|
|
* @param {Map<string, string>} attributes attributes
|
|
* @returns {boolean} true when the script element's `type` is executable JS
|
|
*/
|
|
const isExecutableJsScript = (attributes) => {
|
|
const type = getAttributeValue(attributes, "type");
|
|
if (type === undefined) return true;
|
|
return JS_SCRIPT_TYPES.has(type.trim().toLowerCase());
|
|
};
|
|
|
|
/**
|
|
* @param {Map<string, string>} attributes attributes
|
|
* @returns {boolean} true when the link points at an ES module that should be bundled as an entry chunk
|
|
*/
|
|
const isLinkModulePreload = (attributes) => {
|
|
const rel = getAttributeValue(attributes, "rel");
|
|
if (!rel) return false;
|
|
return rel.trim().toLowerCase().split(/\s+/).includes("modulepreload");
|
|
};
|
|
|
|
/**
|
|
* @param {Map<string, string>} attributes attributes
|
|
* @returns {boolean} true when the link is a `<link rel="stylesheet">` that should be bundled as a CSS entry chunk
|
|
*/
|
|
const isLinkStylesheet = (attributes) => {
|
|
const rel = getAttributeValue(attributes, "rel");
|
|
if (!rel) return false;
|
|
return rel.trim().toLowerCase().split(/\s+/).includes("stylesheet");
|
|
};
|
|
|
|
/** @typedef {"src" | "srcset" | "css-url" | "msapplication-task" | "script" | "script-module" | "modulepreload" | "stylesheet" | "html" | "preload" | "prefetch" | "stylesheet-style" | "stylesheet-style-attribute" | "srcdoc"} SourceType */
|
|
/** Entry types: a `type` whose value is loaded as its own compilation entry chunk. */
|
|
/** @typedef {"script" | "script-module" | "modulepreload" | "stylesheet" | "html" | "preload" | "prefetch"} EntrySourceType */
|
|
/** @typedef {SourceType | ((attrs: Map<string, string>, css: boolean) => SourceType)} SourceTypeOrResolver */
|
|
/** @typedef {(attributes: Map<string, string>, value: string) => boolean} SourceFilter */
|
|
/** @typedef {{ tag?: string, attribute: string, type: SourceType, filter?: SourceFilter }} SourceEntry */
|
|
/** A `type` plus optional gates: `filter` (the decoded attribute map + the decoded value; return false to skip — covers both cross-attribute checks and cheap value checks) and `namespace` (restrict to an element namespace, e.g. SVG). */
|
|
/** @typedef {{ type: SourceTypeOrResolver, filter?: SourceFilter, namespace?: number }} SourceItem */
|
|
/** A tag→attribute lookup; a `undefined` value marks a built-in source disabled via `sources` (`type: false`). */
|
|
/** @typedef {Record<string, SourceItem | undefined>} SourceBucket */
|
|
/** @typedef {Record<string, SourceBucket>} SourceTable */
|
|
/** A source whose value is the element's text content (a `<style>`/`<script>` body) rather than an attribute. */
|
|
/** @typedef {{ type: SourceTypeOrResolver, filter?: (attributes: Map<string, string>) => boolean }} ContentSourceItem */
|
|
|
|
/**
|
|
* Builds a null-prototype dictionary from the given property bags
|
|
* (later bags win; `undefined` bags are skipped). A null prototype is
|
|
* essential here: the tables are indexed by HTML tag and attribute
|
|
* names, so a plain object would let names like `__proto__`,
|
|
* `constructor`, or `toString` resolve to inherited values at lookup
|
|
* time — producing bogus dependencies and letting `sources: false` be
|
|
* bypassed.
|
|
* @param {...(Record<string, EXPECTED_ANY> | undefined)} bags property bags
|
|
* @returns {EXPECTED_ANY} null-prototype dictionary
|
|
*/
|
|
const dict = (...bags) => Object.assign(Object.create(null), ...bags);
|
|
|
|
// Shared `SourceItem` singletons used by the built-in defaults —
|
|
// keeping one instance per kind stabilizes V8's hidden classes across
|
|
// lookups in the walk. `srcset` is its own type; the walk picks the
|
|
// `parseSrcset` parser for it and otherwise treats it like `src`.
|
|
/** @type {SourceItem} */
|
|
const PLAIN_SRC = { type: "src", filter: undefined };
|
|
/** @type {SourceItem} */
|
|
const PLAIN_SRCSET = { type: "srcset", filter: undefined };
|
|
|
|
/**
|
|
* `<link href>` is polymorphic: `rel="modulepreload"` → an ESM
|
|
* preload entry, `rel="stylesheet"` (with `experiments.css`) → a CSS
|
|
* entry, otherwise a plain asset URL.
|
|
* @type {SourceItem}
|
|
*/
|
|
const LINK_HREF = {
|
|
type: (attrs, css) => {
|
|
if (isLinkModulePreload(attrs)) return "modulepreload";
|
|
if (css && isLinkStylesheet(attrs)) return "stylesheet";
|
|
// `rel="preload"/"prefetch"` with `as="script"` (or `as="style"` when
|
|
// `experiments.css` is on) becomes a bundled, non-executing entry whose
|
|
// `href` is rewritten to the built chunk; other `as` targets (image,
|
|
// font, fetch, …) stay plain asset URLs.
|
|
const rel = getAttributeValue(attrs, "rel");
|
|
if (rel) {
|
|
const rels = rel.trim().toLowerCase().split(/\s+/);
|
|
const kind = rels.includes("preload")
|
|
? "preload"
|
|
: rels.includes("prefetch")
|
|
? "prefetch"
|
|
: undefined;
|
|
if (kind) {
|
|
const as = getAttributeValue(attrs, "as");
|
|
const asLower = as ? as.trim().toLowerCase() : "";
|
|
if (asLower === "script" || (css && asLower === "style")) return kind;
|
|
}
|
|
}
|
|
return "src";
|
|
},
|
|
filter: filterLinkUnion
|
|
};
|
|
|
|
/**
|
|
* `<script src>`: non-JS types (e.g. `application/ld+json`,
|
|
* `importmap`) stay plain asset URLs so the browser keeps seeing them
|
|
* as data blocks; `type="module"` opts into the ESM entry chunk;
|
|
* everything else in `JS_SCRIPT_TYPES` is a classic script.
|
|
* @type {SourceItem}
|
|
*/
|
|
const SCRIPT_SRC = {
|
|
type: (attrs) =>
|
|
isExecutableJsScript(attrs)
|
|
? isModuleScript(attrs)
|
|
? "script-module"
|
|
: "script"
|
|
: "src",
|
|
filter: undefined
|
|
};
|
|
|
|
/**
|
|
* `<meta content>`: most referenced names hold a single URL (`src`); the
|
|
* `msapplication-task` value is a `;`-delimited list whose `icon-uri` part
|
|
* is the only asset, so it selects the `msapplication-task` parser.
|
|
* @type {SourceItem}
|
|
*/
|
|
const META_CONTENT = {
|
|
type: (attrs) => {
|
|
const name = attrs.get("name");
|
|
return name !== undefined &&
|
|
name.trim().toLowerCase() === "msapplication-task"
|
|
? "msapplication-task"
|
|
: "src";
|
|
},
|
|
filter: filterMetaContent
|
|
};
|
|
|
|
// Built-in lookup table, written directly in its final resolved shape:
|
|
// `DEFAULT_SOURCES_BY_TAG[tag][attribute] = item`. No module-load
|
|
// loop, no separate array representation — every parser created with
|
|
// the default `sources` config just references this table and pays
|
|
// zero per-parser work.
|
|
/** @type {Record<string, Record<string, SourceItem>>} */
|
|
const DEFAULT_SOURCES_BY_TAG = dict({
|
|
// Obsolete Java-applet element; `code`/`object` are single class/object URLs.
|
|
applet: dict({ code: PLAIN_SRC, object: PLAIN_SRC }),
|
|
audio: dict({ src: PLAIN_SRC }),
|
|
// Deprecated presentational `background` attribute — an image URL.
|
|
body: dict({ background: PLAIN_SRC }),
|
|
embed: dict({ src: PLAIN_SRC }),
|
|
// `srcdoc` is an entity-encoded HTML document parsed and rewritten as a
|
|
// nested module — its handling lives in the generic source loop so the
|
|
// `sources` option can re-target it (or other tags/attributes) freely.
|
|
iframe: dict({ srcdoc: { type: "srcdoc", filter: undefined } }),
|
|
img: dict({ src: PLAIN_SRC, srcset: PLAIN_SRCSET }),
|
|
input: dict({ src: PLAIN_SRC }),
|
|
link: dict({
|
|
href: LINK_HREF,
|
|
imagesrcset: { type: "srcset", filter: filterLinkHref }
|
|
}),
|
|
meta: dict({ content: META_CONTENT }),
|
|
// MathML `<mglyph src>` references an image.
|
|
mglyph: dict({ src: PLAIN_SRC }),
|
|
// `classid` is a single object URI (`codebase`/`archive` are a base/list, skipped).
|
|
object: dict({ data: PLAIN_SRC, classid: PLAIN_SRC }),
|
|
// Obsolete `<param valuetype="ref" value="url">` child of `<object>`.
|
|
param: dict({ value: { type: "src", filter: filterParamRef } }),
|
|
// `href`/`xlink:href` reference the source of SVG `<script>` elements
|
|
script: dict({
|
|
src: SCRIPT_SRC,
|
|
href: SCRIPT_SRC,
|
|
"xlink:href": SCRIPT_SRC
|
|
}),
|
|
source: dict({ src: PLAIN_SRC, srcset: PLAIN_SRCSET }),
|
|
// Deprecated presentational `background` attribute — an image URL.
|
|
table: dict({ background: PLAIN_SRC }),
|
|
td: dict({ background: PLAIN_SRC }),
|
|
th: dict({ background: PLAIN_SRC }),
|
|
track: dict({ src: PLAIN_SRC }),
|
|
video: dict({ poster: PLAIN_SRC, src: PLAIN_SRC }),
|
|
// SVG. Tag names match the tree builder's adjusted camelCase
|
|
// (`feImage`/`textPath`/`linearGradient`/`radialGradient`). `href`/
|
|
// `xlink:href` reference another element/resource; fragment-only `#id`
|
|
// values are left untouched, so only external `file.svg#id` is rewritten.
|
|
// `color-profile`'s `xlink:href` points at an external ICC profile file.
|
|
// `cursor`/`font-face-uri` point at an external image/font file; `altGlyph`/
|
|
// `glyphRef`/`tref` are legacy element references (removed in SVG 2).
|
|
altGlyph: dict({ "xlink:href": PLAIN_SRC, href: PLAIN_SRC }),
|
|
"color-profile": dict({ "xlink:href": PLAIN_SRC, href: PLAIN_SRC }),
|
|
cursor: dict({ "xlink:href": PLAIN_SRC, href: PLAIN_SRC }),
|
|
feImage: dict({ "xlink:href": PLAIN_SRC, href: PLAIN_SRC }),
|
|
filter: dict({ "xlink:href": PLAIN_SRC, href: PLAIN_SRC }),
|
|
"font-face-uri": dict({ "xlink:href": PLAIN_SRC, href: PLAIN_SRC }),
|
|
glyphRef: dict({ "xlink:href": PLAIN_SRC, href: PLAIN_SRC }),
|
|
image: dict({ "xlink:href": PLAIN_SRC, href: PLAIN_SRC }),
|
|
linearGradient: dict({ "xlink:href": PLAIN_SRC, href: PLAIN_SRC }),
|
|
mpath: dict({ "xlink:href": PLAIN_SRC, href: PLAIN_SRC }),
|
|
pattern: dict({ "xlink:href": PLAIN_SRC, href: PLAIN_SRC }),
|
|
radialGradient: dict({ "xlink:href": PLAIN_SRC, href: PLAIN_SRC }),
|
|
textPath: dict({ "xlink:href": PLAIN_SRC, href: PLAIN_SRC }),
|
|
tref: dict({ "xlink:href": PLAIN_SRC, href: PLAIN_SRC }),
|
|
use: dict({ "xlink:href": PLAIN_SRC, href: PLAIN_SRC })
|
|
// CSS `url(...)` references in SVG presentation attributes (fill, stroke,
|
|
// clip-path, …) are handled separately during the walk (`parseCssUrls`).
|
|
});
|
|
|
|
// SVG presentation attributes (`fill`, `stroke`, …) carry CSS `url(...)`
|
|
// FuncIRIs; `parseCssUrls` extracts external refs as assets (internal
|
|
// `url(#id)` is left untouched). They apply to every SVG-namespace element, so
|
|
// the item is `namespace`-gated rather than placed in a tag bucket. The filter
|
|
// is a cheap pre-check skipping the common plain values (`red`, `none`, `#fff`).
|
|
/** @type {SourceItem} */
|
|
const SVG_CSS_URL = {
|
|
type: "css-url",
|
|
filter: (attributes, value) => FUNC_IRI_URL_REGEXP.test(value),
|
|
namespace: NS_SVG
|
|
};
|
|
|
|
// The global `style=""` attribute (a CSS declaration list), available on every
|
|
// tag and even with `sources: false`. Like every `stylesheet-style*` source it
|
|
// only emits when `experiments.css` is on (checked where the dependency is
|
|
// created); the filter is a cheap pre-check skipping declarations without a
|
|
// `url()`/`src()`/… .
|
|
/** @type {SourceItem} */
|
|
const STYLE_ATTRIBUTE = {
|
|
type: "stylesheet-style-attribute",
|
|
filter: (attributes, value) => STYLE_ATTR_URL_REGEXP.test(value)
|
|
};
|
|
|
|
// Any-tag sources — applied to every element as a fallback and folded into
|
|
// each tag bucket, so the walk resolves one object per element and reads one
|
|
// property per attribute. `DEFAULT` (svg presentation + style) is active
|
|
// unless `sources: false`; only the always-on style attribute survives `false`.
|
|
/** @type {Record<string, SourceItem>} */
|
|
const DEFAULT_ANY_SOURCES = dict({ style: STYLE_ATTRIBUTE });
|
|
for (const name of [
|
|
"fill",
|
|
"stroke",
|
|
"clip-path",
|
|
"mask",
|
|
"filter",
|
|
"marker",
|
|
"marker-start",
|
|
"marker-mid",
|
|
"marker-end",
|
|
"cursor"
|
|
]) {
|
|
DEFAULT_ANY_SOURCES[name] = SVG_CSS_URL;
|
|
}
|
|
/** @type {Record<string, SourceItem>} */
|
|
const ALWAYS_ANY_SOURCES = dict({ style: STYLE_ATTRIBUTE });
|
|
|
|
// Reserved key holding the any-tag sources inside the per-tag table — the
|
|
// schema forbids an empty `tag`, so it never collides with a real tag. The
|
|
// walk falls back to it for elements without their own bucket.
|
|
const ANY_TAG = "";
|
|
|
|
/**
|
|
* Folds the any-tag sources into every per-tag bucket (tag-specific entries
|
|
* win) and stores them under `ANY_TAG`, so the whole source model is one
|
|
* object and the walk's per-attribute lookup is a single property read.
|
|
* @param {SourceTable} byTag per-tag sources
|
|
* @param {SourceBucket} any any-tag sources
|
|
* @returns {SourceTable} folded table
|
|
*/
|
|
const foldAnySources = (byTag, any) => {
|
|
for (const tag of Object.keys(byTag)) {
|
|
byTag[tag] = dict(any, byTag[tag]);
|
|
}
|
|
byTag[ANY_TAG] = any;
|
|
return byTag;
|
|
};
|
|
|
|
// Default table (per-tag defaults + any-tag sources folded in) and the
|
|
// `sources: false` table (only the always-on `style=""` attribute). Both are
|
|
// precomputed so the common cases reference them with zero per-parser work.
|
|
/** @type {SourceTable} */
|
|
const DEFAULT_SOURCES_FOLDED = foldAnySources(
|
|
dict(DEFAULT_SOURCES_BY_TAG),
|
|
DEFAULT_ANY_SOURCES
|
|
);
|
|
/** @type {SourceTable} */
|
|
const DISABLED_SOURCES_FOLDED = foldAnySources(dict(), ALWAYS_ANY_SOURCES);
|
|
|
|
/**
|
|
* Inline `<script>` body: classic JS unless `type="module"` opts into ESM.
|
|
* @param {Map<string, string>} attrs attributes
|
|
* @returns {SourceType} the entry type for the inline script
|
|
*/
|
|
const scriptContentType = (attrs) =>
|
|
isModuleScript(attrs) ? "script-module" : "script";
|
|
|
|
/**
|
|
* Inline `<script>` body: only an executable JS block with no external source
|
|
* carries content webpack should bundle.
|
|
* @param {Map<string, string>} attrs attributes
|
|
* @returns {boolean} true when the inline body should be bundled
|
|
*/
|
|
const scriptContentFilter = (attrs) =>
|
|
!attrs.has("src") &&
|
|
!attrs.has("href") &&
|
|
!attrs.has("xlink:href") &&
|
|
isExecutableJsScript(attrs);
|
|
|
|
/**
|
|
* `<style>` body is a stylesheet only when its `type` is empty or `text/css`.
|
|
* @param {Map<string, string>} attrs attributes
|
|
* @returns {boolean} true when the body is CSS
|
|
*/
|
|
const styleContentFilter = (attrs) => {
|
|
const type = attrs.get("type");
|
|
if (type === undefined) return true;
|
|
const t = type.trim().toLowerCase();
|
|
return t === "" || t === "text/css";
|
|
};
|
|
|
|
// Element-body sources, independent of the `sources` option (even `sources:
|
|
// false`): inline `<script>` bodies are always processed; the `<style>` body
|
|
// only emits when `experiments.css` is on (checked where the dependency is
|
|
// created, like every `stylesheet-style*` source).
|
|
/** @type {Record<string, ContentSourceItem>} */
|
|
const CONTENT_SOURCES = dict({
|
|
script: { type: scriptContentType, filter: scriptContentFilter },
|
|
style: { type: "stylesheet-style", filter: styleContentFilter }
|
|
});
|
|
|
|
class HtmlParser extends Parser {
|
|
/**
|
|
* Creates an instance of HtmlParser.
|
|
* @param {HtmlParserOptions} options parser options (from `module.parser.html`; always passed by the `createParser` hook)
|
|
*/
|
|
constructor(options) {
|
|
super();
|
|
this.magicCommentContext = createMagicCommentContext();
|
|
// Read by HtmlModulesPlugin's `processResult` hook, which transforms
|
|
// the module source before it is stored and parsed.
|
|
/** @type {HtmlTemplateFunction | undefined} */
|
|
this.template = options.template;
|
|
/** @type {import("../../declarations/WebpackOptions").UrlHintRule[] | undefined} */
|
|
this._urlHints = options.urlHints;
|
|
// `as` selects the parse mode — the HTML analog of the CSS parser's `as`.
|
|
// `"document"` (default) parses a full page; any other value is the
|
|
// context element whose inner HTML the source is parsed as (a fragment),
|
|
// so context-sensitive tags (`<tr>`, `<td>`, `<option>`, …) in a partial
|
|
// are kept, not dropped. `"template"` is the neutral generic fragment.
|
|
/** @type {string | undefined} */
|
|
this.fragmentContext =
|
|
options.as === undefined || options.as === "document"
|
|
? undefined
|
|
: options.as;
|
|
// One source model: a per-tag table that also holds the any-tag sources
|
|
// under `ANY_TAG`. The common cases reference precomputed tables.
|
|
/** @type {SourceTable} */
|
|
this.sourcesByTag = DEFAULT_SOURCES_FOLDED;
|
|
|
|
const sources = options.sources;
|
|
if (sources === undefined || sources === true) return;
|
|
if (sources === false) {
|
|
// Only the always-on `style=""` attribute survives; nothing else is
|
|
// extracted (svg presentation, `<script src>`, `<link>` entries, …).
|
|
this.sourcesByTag = DISABLED_SOURCES_FOLDED;
|
|
return;
|
|
}
|
|
|
|
// User array — `"..."` anywhere opts the per-tag defaults in as the
|
|
// base; the built-in any-tag sources (svg presentation + style) are
|
|
// always present. A user entry with no `tag` is an any-tag source (e.g.
|
|
// `{ attribute: "data-style", type: "stylesheet-style-attribute" }`).
|
|
// User entries override regardless of position. `dict()` keeps every
|
|
// table null-prototype (see its doc), and per-tag writes rebuild the
|
|
// bucket so the aliased default buckets stay intact.
|
|
/** @type {SourceTable} */
|
|
const byTag = sources.includes("...")
|
|
? dict(DEFAULT_SOURCES_BY_TAG)
|
|
: dict();
|
|
/** @type {SourceBucket} */
|
|
const any = dict(DEFAULT_ANY_SOURCES);
|
|
for (const entry of sources) {
|
|
if (entry === "...") continue;
|
|
// `type: false` disables a built-in source: writing `undefined` over the
|
|
// attribute makes the walk's `sources[name]` lookup skip it (see below).
|
|
/** @type {SourceItem | undefined} */
|
|
const item =
|
|
entry.type === false
|
|
? undefined
|
|
: {
|
|
type: entry.type,
|
|
filter:
|
|
typeof entry.filter === "function" ? entry.filter : undefined
|
|
};
|
|
const attr = entry.attribute.toLowerCase();
|
|
if (entry.tag === undefined) {
|
|
any[attr] = item;
|
|
} else {
|
|
const tag = entry.tag.toLowerCase();
|
|
byTag[tag] = dict(byTag[tag], { [attr]: item });
|
|
// The AST carries adjusted camelCase names for foreign-content
|
|
// tags (e.g. `feImage`) — register the entry under both.
|
|
const adjusted = SVG_TAG_ADJUST[tag];
|
|
if (adjusted !== undefined) {
|
|
byTag[adjusted] = dict(byTag[adjusted], { [attr]: item });
|
|
}
|
|
}
|
|
}
|
|
/** @type {SourceTable} */
|
|
this.sourcesByTag = foldAnySources(byTag, any);
|
|
}
|
|
|
|
/**
|
|
* Runs the `template` option over the source and returns the transformed
|
|
* html. Called from HtmlModulesPlugin's `processResult`, where the return
|
|
* value becomes the module's stored source so the parser (which records
|
|
* dependency offsets against it) and the generator (which renders from
|
|
* `module.originalSource()`) stay in agreement.
|
|
* @param {string | Buffer} source the original source
|
|
* @param {NormalModule} module the html module
|
|
* @returns {string | Buffer} the transformed source
|
|
*/
|
|
applyTemplate(source, module) {
|
|
if (!this.template) return source;
|
|
// `processResult` runs after `_doBuild` has initialized these
|
|
// dependency sets, so they are always present here.
|
|
const buildInfo = /** @type {BuildInfo} */ (module.buildInfo);
|
|
const fileDependencies = /** @type {FileSystemDependencies} */ (
|
|
buildInfo.fileDependencies
|
|
);
|
|
const contextDependencies = /** @type {FileSystemDependencies} */ (
|
|
buildInfo.contextDependencies
|
|
);
|
|
const missingDependencies = /** @type {FileSystemDependencies} */ (
|
|
buildInfo.missingDependencies
|
|
);
|
|
const transformed = this.template(
|
|
typeof source === "string" ? source : source.toString("utf8"),
|
|
{
|
|
module,
|
|
resource: module.resource,
|
|
addDependency: (dependency) => {
|
|
fileDependencies.add(dependency);
|
|
},
|
|
addContextDependency: (dependency) => {
|
|
contextDependencies.add(dependency);
|
|
},
|
|
addMissingDependency: (dependency) => {
|
|
missingDependencies.add(dependency);
|
|
},
|
|
addBuildDependency: (dependency) => {
|
|
if (buildInfo.buildDependencies === undefined) {
|
|
buildInfo.buildDependencies = new LazySet();
|
|
}
|
|
buildInfo.buildDependencies.add(dependency);
|
|
},
|
|
emitWarning: (warning) =>
|
|
module.addWarning(
|
|
warning instanceof Error ? warning : new WebpackError(warning)
|
|
),
|
|
emitError: (error) =>
|
|
module.addError(
|
|
error instanceof Error ? error : new WebpackError(error)
|
|
)
|
|
}
|
|
);
|
|
if (typeof transformed !== "string") {
|
|
throw new Error(
|
|
"The `template` html parser option must return a string."
|
|
);
|
|
}
|
|
return transformed;
|
|
}
|
|
|
|
/**
|
|
* Parses the provided source and updates the parser state.
|
|
* @param {string | Buffer | PreparsedAst} source the source to parse
|
|
* @param {ParserState} state the parser state
|
|
* @returns {ParserState} the parser state
|
|
*/
|
|
parse(source, state) {
|
|
if (Buffer.isBuffer(source)) {
|
|
source = source.toString("utf8");
|
|
} else if (typeof source === "object") {
|
|
throw new Error("webpackAst is unexpected for the HtmlParser");
|
|
}
|
|
if (source[0] === "\uFEFF") {
|
|
source = source.slice(1);
|
|
}
|
|
|
|
const locConverter = new LocConverter(source);
|
|
|
|
const module = state.module;
|
|
const compilation = state.compilation;
|
|
const { hashFunction, module: outputModule } = compilation.outputOptions;
|
|
const context = compilation.compiler.context;
|
|
const css = Boolean(compilation.options.experiments.css);
|
|
const urlHints = this._urlHints;
|
|
|
|
// Stable, per-HTML-module prefix used when generating entry names for
|
|
// script src / modulepreload references so they don't collide across
|
|
// HTML modules in the same compilation. We hash the module's resource
|
|
// path (a plain absolute path) — going through `contextify` against
|
|
// the compilation root keeps the hash machine-stable for the same
|
|
// project layout. Note: `module.identifier()` returns `html|<path>`
|
|
// for HTML modules, which doesn't start with `/`, so contextify would
|
|
// leave it absolute. `module.resource` is the bare path.
|
|
/** @type {string} */
|
|
const resource =
|
|
/** @type {EXPECTED_ANY} */ (module).resource || module.identifier();
|
|
const moduleHash = createHash(hashFunction || "md4")
|
|
.update(context ? contextify(context, resource) : resource)
|
|
.digest("hex")
|
|
.slice(0, 8);
|
|
|
|
// `output.html` head injection: facts are collected during the walk and
|
|
// become presentational dependencies afterwards — no later asset pass has
|
|
// to re-parse the page. `data:` documents are skipped: synthetic entry
|
|
// wrappers already carry the tags and iframe srcdoc pages must stay
|
|
// untouched.
|
|
const htmlOutputOptions =
|
|
compilation.options && compilation.options.output
|
|
? compilation.options.output.html
|
|
: undefined;
|
|
const headOptions =
|
|
typeof htmlOutputOptions === "object" &&
|
|
(htmlOutputOptions.title !== undefined ||
|
|
htmlOutputOptions.meta !== undefined ||
|
|
htmlOutputOptions.base !== undefined) &&
|
|
!resource.startsWith("data:")
|
|
? htmlOutputOptions
|
|
: undefined;
|
|
let doctypeEnd = -1;
|
|
let htmlTagEnd = -1;
|
|
// insertion offsets just inside the (possibly implicit) head
|
|
let headStart = 0;
|
|
let headLastEnd = 0;
|
|
let headSeen = false;
|
|
let inHead = false;
|
|
// `<template>` contents are inert
|
|
let headTemplateDepth = 0;
|
|
// Depth of open `<template>` elements anywhere in the tree. Template
|
|
// content is an inert, separate fragment per spec, so a `<base>` inside
|
|
// it must NOT set the document base (`headTemplateDepth` only covers
|
|
// templates seen while in `<head>`, so it can't guard body templates).
|
|
let templateDepth = 0;
|
|
let headHasTitle = false;
|
|
let headHasBase = false;
|
|
let headCharsetEnd = -1;
|
|
/** @type {Set<string> | undefined} */
|
|
let headMetaNames;
|
|
|
|
// Script src / modulepreload references are collected per-type
|
|
// during the walk; HtmlModulesPlugin later turns them into real
|
|
// entries. `script` and `script-module` entries are chained via a
|
|
// leader-only dependOn so they share a runtime.
|
|
// `<link rel="modulepreload">` entries are kept independent — they
|
|
// must preload without running, so they can never become a runtime
|
|
// leader that other entries would import.
|
|
/**
|
|
* @typedef {object} HtmlEntryInfo
|
|
* @property {string} request
|
|
* @property {string} entryName
|
|
* @property {"script" | "script-module" | "modulepreload" | "stylesheet" | "html" | "preload" | "prefetch"} type
|
|
* @property {boolean=} css entry emits a CSS chunk (`<link rel="preload" as="style">`), so it needs the CSS filename template
|
|
*/
|
|
/** @type {HtmlEntryInfo[]} */
|
|
const scriptEntries = [];
|
|
/** @type {HtmlEntryInfo[]} */
|
|
const scriptModuleEntries = [];
|
|
/** @type {HtmlEntryInfo[]} */
|
|
const modulePreloadEntries = [];
|
|
/** @type {HtmlEntryInfo[]} */
|
|
const stylesheetEntries = [];
|
|
// `type: "html"` links — each referenced `.html` becomes its own page
|
|
// entry (`<a href="page.html">`), independent (no dependOn).
|
|
/** @type {HtmlEntryInfo[]} */
|
|
const htmlLinkEntries = [];
|
|
// `<link rel="preload"/"prefetch">` of a bundled resource: each becomes an
|
|
// independent, non-executing entry whose `href` is rewritten to the built
|
|
// chunk's URL (JS for `as="script"`, CSS for `as="style"`).
|
|
/** @type {HtmlEntryInfo[]} */
|
|
const preloadEntries = [];
|
|
/** @type {HtmlEntryInfo[]} */
|
|
const prefetchEntries = [];
|
|
|
|
// Offset of the first classic blocking script tag; injected stylesheet
|
|
// `<link>`s stay ahead of it. `defer`/`async`/module scripts don't
|
|
// anchor — they execute after parsing (and after pending stylesheets),
|
|
// so CSS may follow their tags (the order Vite emits).
|
|
let firstBlockingScriptStart = -1;
|
|
|
|
let nextEntryIndex = 0;
|
|
|
|
// `<base href>` resolves the relative URLs that follow it. A relative base
|
|
// (`./assets/`) rewrites them into a subdirectory — still bundled; a
|
|
// root-relative or absolute base (`/`, `https://cdn/`) points them outside
|
|
// the build, so those URLs are left untouched. Resolved in the walk below
|
|
// from the first `<base href>` seen (`documentBase` undefined until then).
|
|
/** @type {string | undefined} */
|
|
let documentBase;
|
|
/** @type {string | undefined} */
|
|
let baseDir;
|
|
let baseIsExternal = false;
|
|
// Prepended to the emitted URLs' auto-public-path undo path so the base
|
|
// doesn't misdirect them: the browser resolves rewritten (relative)
|
|
// output URLs against the base dir, so one `../` per base segment cancels
|
|
// it. Undefined for absolute publicPath (no undo path is emitted).
|
|
/** @type {string | undefined} */
|
|
let baseUrlPrefix;
|
|
|
|
/**
|
|
* Tracks the `webpackIgnore` value from the most recent comment that
|
|
* appears before the next tag. Reset whenever a tag is emitted or a
|
|
* comment without a `webpackIgnore` value is encountered.
|
|
* @type {boolean | undefined}
|
|
*/
|
|
let pendingWebpackIgnore;
|
|
/** @type {boolean | undefined} */
|
|
let pendingWebpackInline;
|
|
/**
|
|
* Tracks the resource-hint values from the most recent `<!-- webpackPrefetch: … -->`
|
|
* (etc.) comment so the next asset URL attribute picks them up.
|
|
* @type {import("../prefetch/parseResourceHintOptions").ResourceHintOptions | undefined}
|
|
*/
|
|
let pendingHints;
|
|
|
|
const magicCommentContext = this.magicCommentContext;
|
|
|
|
/**
|
|
* @param {import("./syntax").HtmlPath} path walk path (used only for attribute-ref reads)
|
|
* @param {import("./syntax").HtmlAttributeRef} typeAttr type attribute ref (0 = none)
|
|
* @param {number} nameEnd end offset of the tag name
|
|
* @param {string} type type of the script
|
|
* @param {string} input source string
|
|
*/
|
|
const reconcileScriptTypeAttr = (path, typeAttr, nameEnd, type, input) => {
|
|
const valueStart =
|
|
typeAttr !== 0 ? path.attributeValueStart(typeAttr) : -1;
|
|
const valueEnd = typeAttr !== 0 ? path.attributeValueEnd(typeAttr) : -1;
|
|
if (outputModule && type === "script") {
|
|
// Chunk is an ES module; upgrade the tag.
|
|
if (typeAttr !== 0 && valueStart !== -1) {
|
|
module.addPresentationalDependency(
|
|
new ConstDependency("module", [valueStart, valueEnd])
|
|
);
|
|
} else {
|
|
module.addPresentationalDependency(
|
|
new ConstDependency(' type="module"', nameEnd)
|
|
);
|
|
}
|
|
} else if (!outputModule && type === "script-module" && typeAttr !== 0) {
|
|
// Chunk is a classic IIFE; drop `type="module"` so the
|
|
// browser doesn't load it under module semantics.
|
|
let attrEnd;
|
|
if (valueStart === -1) {
|
|
attrEnd = path.attributeNameEnd(typeAttr);
|
|
} else if (input[valueEnd] === '"' || input[valueEnd] === "'") {
|
|
attrEnd = valueEnd + 1;
|
|
} else {
|
|
attrEnd = valueEnd;
|
|
}
|
|
let attrStart = path.attributeNameStart(typeAttr);
|
|
if (
|
|
attrStart > 0 &&
|
|
isAsciiWhitespace(input.charCodeAt(attrStart - 1))
|
|
) {
|
|
attrStart -= 1;
|
|
}
|
|
module.addPresentationalDependency(
|
|
new ConstDependency("", [attrStart, attrEnd])
|
|
);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @param {string} mime mime type
|
|
* @param {string} text inline text
|
|
* @returns {string} a `data:` request for the text
|
|
*/
|
|
const dataUri = (mime, text) =>
|
|
`data:${mime};base64,${Buffer.from(text, "utf8").toString("base64")}`;
|
|
|
|
/**
|
|
* @param {ConstDependency | HtmlSourceDependency | HtmlEntryDependency | HtmlInlineStyleDependency | HtmlInlineScriptDependency | HtmlInlineHtmlDependency} dep dependency
|
|
* @param {number} start raw start offset
|
|
* @param {number} end raw end offset
|
|
*/
|
|
const setLoc = (dep, start, end) => {
|
|
const s = locConverter.get(start);
|
|
const e = locConverter.get(end);
|
|
dep.setLoc(s.line, s.column, e.line, e.column);
|
|
};
|
|
|
|
/**
|
|
* Classifies the first `<base href>` seen in the walk, setting
|
|
* `documentBase`/`baseDir`/`baseIsExternal`/`baseUrlPrefix` for the URLs
|
|
* that follow it.
|
|
* @param {import("./syntax").HtmlPath} path walk path (for attribute reads)
|
|
* @param {import("./syntax").HtmlAttributeRef} hrefAttr the base's href attribute
|
|
*/
|
|
const resolveDocumentBase = (path, hrefAttr) => {
|
|
documentBase = decodeEntities(path.attributeValue(hrefAttr), true).trim();
|
|
if (!documentBase) return;
|
|
if (
|
|
ABSOLUTE_URL_SCHEME_REGEXP.test(documentBase) ||
|
|
documentBase.charCodeAt(0) === CC_SLASH
|
|
) {
|
|
baseIsExternal = true;
|
|
return;
|
|
}
|
|
// Normalize the base path into its descending directory segments,
|
|
// resolving `.`/`..` like the URL spec. The final segment is the
|
|
// referenced file (dropped) unless the href ends with `/` or a
|
|
// dot-segment; `up` counts `..`s that climb above the document dir.
|
|
const parts = documentBase.split("/");
|
|
/** @type {string[]} */
|
|
const dirs = [];
|
|
let up = 0;
|
|
for (let i = 0; i < parts.length; i++) {
|
|
const part = parts[i];
|
|
if (part === "" || part === ".") continue;
|
|
if (part === "..") {
|
|
if (dirs.length > 0) dirs.pop();
|
|
else up++;
|
|
continue;
|
|
}
|
|
// A trailing plain segment (no following `/`) is the file, not a dir.
|
|
if (i === parts.length - 1) continue;
|
|
dirs.push(part);
|
|
}
|
|
baseDir = `${"../".repeat(up)}${dirs.length > 0 ? `${dirs.join("/")}/` : ""}`;
|
|
// A base above the document dir can't be cancelled with `../` (the
|
|
// document's own dir name is unknown) — leave the output URLs
|
|
// un-prefixed; they still resolve under an absolute publicPath.
|
|
if (up === 0 && dirs.length > 0) {
|
|
baseUrlPrefix = "../".repeat(dirs.length);
|
|
}
|
|
};
|
|
|
|
// Lazy decoded-attribute map for the current element — parse-scoped
|
|
// to avoid a closure and memo slot per element.
|
|
/** @type {import("./syntax").HtmlPath | undefined} */
|
|
let attrMapPath;
|
|
let attrMapCount = 0;
|
|
/** @type {Map<string, string> | undefined} */
|
|
let currentAttributesMap;
|
|
const getAttributesMap = () => {
|
|
if (currentAttributesMap) return currentAttributesMap;
|
|
currentAttributesMap = new Map();
|
|
const path = /** @type {import("./syntax").HtmlPath} */ (attrMapPath);
|
|
for (let i = 0; i < attrMapCount; i++) {
|
|
const attr = path.attributeAt(i);
|
|
// Decoded values — filters and type resolvers compare what
|
|
// the browser sees (e.g. `rel="icon"` means `icon`)
|
|
currentAttributesMap.set(
|
|
path.attributeName(attr),
|
|
decodeEntities(path.attributeValue(attr), true)
|
|
);
|
|
}
|
|
return currentAttributesMap;
|
|
};
|
|
|
|
// TODO implement full HTML parser (WASM)
|
|
// The walker descends into children (and `<template>` content) itself;
|
|
// the Element `exit` clears a pending `webpackIgnore` once an element's
|
|
// children are done (the old `walkChildren` behaviour).
|
|
new SourceProcessor()
|
|
.use({
|
|
[NodeType.Comment]: (path) => {
|
|
const start = path.start();
|
|
const end = path.end();
|
|
// comments count toward the head-end insertion anchor
|
|
if (inHead && headTemplateDepth === 0 && end > headLastEnd) {
|
|
headLastEnd = end;
|
|
}
|
|
// Only proper `<!-- ... -->` comments carry magic comments.
|
|
if (
|
|
end - start < 7 ||
|
|
source.charCodeAt(start) !== 0x3c ||
|
|
source.charCodeAt(start + 1) !== 0x21 ||
|
|
source.charCodeAt(start + 2) !== 0x2d ||
|
|
source.charCodeAt(start + 3) !== 0x2d ||
|
|
source.charCodeAt(end - 1) !== 0x3e ||
|
|
source.charCodeAt(end - 2) !== 0x2d ||
|
|
source.charCodeAt(end - 3) !== 0x2d
|
|
) {
|
|
pendingWebpackIgnore = undefined;
|
|
pendingWebpackInline = undefined;
|
|
pendingHints = undefined;
|
|
return;
|
|
}
|
|
const value = path.data();
|
|
if (!webpackCommentRegExp.test(value)) {
|
|
pendingWebpackIgnore = undefined;
|
|
pendingWebpackInline = undefined;
|
|
pendingHints = undefined;
|
|
return;
|
|
}
|
|
/** @type {Record<string, EXPECTED_ANY>} */
|
|
let options;
|
|
try {
|
|
options = parseMagicComment(value, magicCommentContext);
|
|
} catch (err) {
|
|
const { line: sl, column: sc } = locConverter.get(start);
|
|
const { line: el, column: ec } = locConverter.get(end);
|
|
module.addWarning(
|
|
new CommentCompilationWarning(
|
|
`Compilation error while processing magic comment(-s): /*${value}*/: ${
|
|
/** @type {Error} */ (err).message
|
|
}`,
|
|
{
|
|
start: { line: sl, column: sc },
|
|
end: { line: el, column: ec }
|
|
}
|
|
)
|
|
);
|
|
pendingWebpackIgnore = undefined;
|
|
pendingWebpackInline = undefined;
|
|
pendingHints = undefined;
|
|
return;
|
|
}
|
|
if (
|
|
options.webpackIgnore === undefined &&
|
|
options.webpackInline === undefined &&
|
|
options.webpackPrefetch === undefined &&
|
|
options.webpackPreload === undefined &&
|
|
options.webpackFetchPriority === undefined
|
|
) {
|
|
pendingWebpackIgnore = undefined;
|
|
pendingWebpackInline = undefined;
|
|
pendingHints = undefined;
|
|
return;
|
|
}
|
|
if (
|
|
options.webpackIgnore !== undefined &&
|
|
typeof options.webpackIgnore !== "boolean"
|
|
) {
|
|
const { line: sl, column: sc } = locConverter.get(start);
|
|
const { line: el, column: ec } = locConverter.get(end);
|
|
module.addWarning(
|
|
new UnsupportedFeatureWarning(
|
|
`\`webpackIgnore\` expected a boolean, but received: ${options.webpackIgnore}.`,
|
|
{
|
|
start: { line: sl, column: sc },
|
|
end: { line: el, column: ec }
|
|
}
|
|
)
|
|
);
|
|
pendingWebpackIgnore = undefined;
|
|
pendingWebpackInline = undefined;
|
|
pendingHints = undefined;
|
|
return;
|
|
}
|
|
if (
|
|
options.webpackInline !== undefined &&
|
|
typeof options.webpackInline !== "boolean"
|
|
) {
|
|
const { line: sl, column: sc } = locConverter.get(start);
|
|
const { line: el, column: ec } = locConverter.get(end);
|
|
module.addWarning(
|
|
new UnsupportedFeatureWarning(
|
|
`\`webpackInline\` expected a boolean, but received: ${options.webpackInline}.`,
|
|
{
|
|
start: { line: sl, column: sc },
|
|
end: { line: el, column: ec }
|
|
}
|
|
)
|
|
);
|
|
pendingWebpackIgnore = undefined;
|
|
pendingWebpackInline = undefined;
|
|
pendingHints = undefined;
|
|
return;
|
|
}
|
|
pendingWebpackIgnore = options.webpackIgnore;
|
|
pendingWebpackInline = options.webpackInline;
|
|
if (
|
|
options.webpackPrefetch !== undefined ||
|
|
options.webpackPreload !== undefined ||
|
|
options.webpackFetchPriority !== undefined
|
|
) {
|
|
const s = locConverter.get(start);
|
|
const e = locConverter.get(end);
|
|
const parsedHints = parseResourceHintOptions(options, module, {
|
|
start: { line: s.line, column: s.column },
|
|
end: { line: e.line, column: e.column }
|
|
});
|
|
pendingHints =
|
|
parsedHints.prefetch !== undefined ||
|
|
parsedHints.preload !== undefined ||
|
|
parsedHints.fetchPriority !== undefined
|
|
? parsedHints
|
|
: undefined;
|
|
} else {
|
|
pendingHints = undefined;
|
|
}
|
|
},
|
|
[NodeType.Doctype]: (path) => {
|
|
// only visited when head injection is active (see `skip` below)
|
|
if (doctypeEnd === -1) doctypeEnd = path.end();
|
|
pendingWebpackIgnore = undefined;
|
|
pendingWebpackInline = undefined;
|
|
pendingHints = undefined;
|
|
},
|
|
[NodeType.Element]: {
|
|
// Children (and `<template>` content) are walked by the
|
|
// processor after `enter`; `exit` clears a pending
|
|
// `webpackIgnore` once they're done.
|
|
enter: (path) => {
|
|
const ignore = pendingWebpackIgnore === true;
|
|
const forceInline = pendingWebpackInline === true;
|
|
pendingWebpackIgnore = undefined;
|
|
pendingWebpackInline = undefined;
|
|
|
|
// `<head>` anchors are collected for every page — they place
|
|
// `inject: "head"` siblings and resource hints; the
|
|
// title/meta/base facts only when configured. Structural, so it
|
|
// runs even for `webpackIgnore`d elements.
|
|
{
|
|
const name = path.tagName();
|
|
if (name === "template" && path.namespace() === NS_HTML) {
|
|
templateDepth++;
|
|
}
|
|
if (headTemplateDepth > 0) {
|
|
if (name === "template" && path.namespace() === NS_HTML) {
|
|
headTemplateDepth++;
|
|
}
|
|
} else if (inHead) {
|
|
if (headOptions !== undefined && path.namespace() === NS_HTML) {
|
|
switch (name) {
|
|
case "title":
|
|
headHasTitle = true;
|
|
break;
|
|
case "base":
|
|
headHasBase = true;
|
|
break;
|
|
case "meta": {
|
|
if (path.findAttribute("charset") !== 0) {
|
|
headCharsetEnd = path.tagEnd();
|
|
} else {
|
|
const httpEquiv = path.findAttribute("http-equiv");
|
|
const contentAttr =
|
|
httpEquiv !== 0 &&
|
|
decodeEntities(
|
|
path.attributeValue(httpEquiv),
|
|
true
|
|
).toLowerCase() === "content-type"
|
|
? path.findAttribute("content")
|
|
: 0;
|
|
if (
|
|
contentAttr !== 0 &&
|
|
CHARSET_DECLARATION_REGEXP.test(
|
|
decodeEntities(
|
|
path.attributeValue(contentAttr),
|
|
true
|
|
)
|
|
)
|
|
) {
|
|
headCharsetEnd = path.tagEnd();
|
|
}
|
|
}
|
|
const nameAttr = path.findAttribute("name");
|
|
const propertyAttr = path.findAttribute("property");
|
|
if (nameAttr !== 0 || propertyAttr !== 0) {
|
|
if (headMetaNames === undefined) {
|
|
headMetaNames = new Set();
|
|
}
|
|
if (nameAttr !== 0) {
|
|
headMetaNames.add(
|
|
decodeEntities(
|
|
path.attributeValue(nameAttr),
|
|
true
|
|
).toLowerCase()
|
|
);
|
|
}
|
|
if (propertyAttr !== 0) {
|
|
headMetaNames.add(
|
|
decodeEntities(
|
|
path.attributeValue(propertyAttr),
|
|
true
|
|
).toLowerCase()
|
|
);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case "template":
|
|
headTemplateDepth++;
|
|
break;
|
|
}
|
|
} else if (
|
|
name === "template" &&
|
|
path.namespace() === NS_HTML
|
|
) {
|
|
headTemplateDepth++;
|
|
}
|
|
if (path.end() > headLastEnd) headLastEnd = path.end();
|
|
} else if (!headSeen && path.namespace() === NS_HTML) {
|
|
if (name === "html") {
|
|
htmlTagEnd = path.tagEnd();
|
|
} else if (name === "head") {
|
|
headSeen = true;
|
|
inHead = true;
|
|
// explicit head has real offsets; an implicit one (head
|
|
// tags are optional in HTML5) anchors after `<html>`,
|
|
// the doctype, or the document start
|
|
const tagEnd = path.tagEnd();
|
|
headStart =
|
|
tagEnd > 0
|
|
? tagEnd
|
|
: htmlTagEnd > 0
|
|
? htmlTagEnd
|
|
: doctypeEnd > 0
|
|
? doctypeEnd
|
|
: 0;
|
|
headLastEnd = headStart;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ignore) {
|
|
return;
|
|
}
|
|
|
|
const elementName = path.tagName();
|
|
const attributeCount = path.attributeCount();
|
|
const elementStart = path.start();
|
|
const tagEnd = path.tagEnd();
|
|
const nameEnd = path.nameEnd();
|
|
|
|
// The first `<base href>` freezes the base for later URLs — but
|
|
// only one in the real document tree, not inside an inert
|
|
// `<template>` (whose contents are a separate fragment).
|
|
if (
|
|
documentBase === undefined &&
|
|
templateDepth === 0 &&
|
|
elementName === "base" &&
|
|
path.namespace() === NS_HTML
|
|
) {
|
|
const hrefAttr = path.findAttribute("href");
|
|
if (hrefAttr !== 0) resolveDocumentBase(path, hrefAttr);
|
|
}
|
|
|
|
// Rebind the parse-scoped lazy attribute map to this element.
|
|
attrMapPath = path;
|
|
attrMapCount = attributeCount;
|
|
currentAttributesMap = undefined;
|
|
|
|
// Each matched attribute is a source; the element body (inline
|
|
// `<style>`/`<script>`) is one more — content rather than an attribute.
|
|
// All are dispatched by the same `switch (type)`.
|
|
const sources =
|
|
this.sourcesByTag[elementName] || this.sourcesByTag[ANY_TAG];
|
|
const bodyItem = CONTENT_SOURCES[elementName];
|
|
// Iterate the attributes, then one extra step for the element
|
|
// body (`i === attributeCount`); the attribute ref is only read on the
|
|
// attribute steps, so it stays in bounds.
|
|
for (let i = 0; i < attributeCount + 1; i++) {
|
|
const content = i === attributeCount;
|
|
/** @type {SourceType} */
|
|
let type;
|
|
/** @type {string} */
|
|
let value;
|
|
/** @type {number} */
|
|
let start;
|
|
/** @type {number} */
|
|
let end;
|
|
/** @type {import("./syntax").HtmlAttributeRef} */
|
|
let attr = 0;
|
|
if (content) {
|
|
if (!bodyItem) continue;
|
|
if (bodyItem.filter && !bodyItem.filter(getAttributesMap())) {
|
|
continue;
|
|
}
|
|
// `skip.text` drops the body `Text` node; the raw content
|
|
// span is [`tagEnd`, `contentEnd`] on the element.
|
|
const bodyStart = tagEnd;
|
|
const bodyEnd = path.contentEnd();
|
|
if (bodyEnd <= bodyStart) continue;
|
|
value = source.slice(bodyStart, bodyEnd);
|
|
if (value.trim() === "") continue;
|
|
start = bodyStart;
|
|
end = bodyEnd;
|
|
type =
|
|
typeof bodyItem.type === "function"
|
|
? bodyItem.type(getAttributesMap(), css)
|
|
: bodyItem.type;
|
|
} else {
|
|
attr = path.attributeAt(i);
|
|
const item = sources[path.attributeName(attr)];
|
|
if (!item) continue;
|
|
if (
|
|
item.namespace !== undefined &&
|
|
path.namespace() !== item.namespace
|
|
) {
|
|
continue;
|
|
}
|
|
// Adoption-agency clones carry no offsets; skip blank values.
|
|
if (path.attributeValueStart(attr) === -1) {
|
|
continue;
|
|
}
|
|
value = path.attributeValue(attr);
|
|
if (!value || !/\S/.test(value)) continue;
|
|
const filter = item.filter;
|
|
if (filter) {
|
|
// The filter also sees the decoded value (the browser tokenizes it
|
|
// decoded, e.g. `url(`), so a value-only check needn't re-read it.
|
|
const decoded = value.includes("&")
|
|
? decodeEntities(value, true)
|
|
: value;
|
|
if (!filter(getAttributesMap(), decoded)) continue;
|
|
}
|
|
start = path.attributeValueStart(attr);
|
|
end = path.attributeValueEnd(attr);
|
|
type =
|
|
typeof item.type === "function"
|
|
? item.type(getAttributesMap(), css)
|
|
: item.type;
|
|
}
|
|
// A resolved `type` selects the parse algorithm and the dependency
|
|
// kind — the only dispatch in the parser.
|
|
switch (type) {
|
|
// Inline CSS — a `<style>` body (raw, a full stylesheet) or an
|
|
// attribute value (decoded, re-escaped on write-back; `*-attribute`
|
|
// is a declaration list). Needs `experiments.css`.
|
|
case "stylesheet-style":
|
|
case "stylesheet-style-attribute": {
|
|
if (!css) break;
|
|
const cssText = content ? value : decodeEntities(value, true);
|
|
if (cssText.trim() === "") break;
|
|
const dep = new HtmlInlineStyleDependency(
|
|
dataUri("text/css", cssText),
|
|
[start, end],
|
|
content ? false : type === "stylesheet-style-attribute",
|
|
!content
|
|
);
|
|
setLoc(dep, start, end);
|
|
module.addDependency(dep);
|
|
module.addCodeGenerationDependency(dep);
|
|
break;
|
|
}
|
|
// `<iframe srcdoc>`: an entity-encoded HTML document. Feed the
|
|
// decoded markup back through the HTML pipeline as a nested
|
|
// `data:text/html` module. Per spec the value is a full document
|
|
// whose URL is `about:srcdoc`, so its base URL is inherited from
|
|
// this document — asset URLs resolve against this file's context.
|
|
case "srcdoc": {
|
|
const htmlText = decodeEntities(value, true);
|
|
// Only spin up a nested module when there is markup that can
|
|
// reference an asset; text/formatting-only markup is identical
|
|
// after rewriting (see `SRCDOC_ASSET_REGEXP`).
|
|
if (
|
|
!htmlText.includes("<") ||
|
|
!SRCDOC_ASSET_REGEXP.test(htmlText)
|
|
) {
|
|
break;
|
|
}
|
|
const dep = new HtmlInlineHtmlDependency(
|
|
dataUri("text/html", htmlText),
|
|
[start, end]
|
|
);
|
|
setLoc(dep, start, end);
|
|
module.addDependency(dep);
|
|
module.addCodeGenerationDependency(dep);
|
|
break;
|
|
}
|
|
// Inline `<script>` body — bundled as its own entry chunk.
|
|
case "script":
|
|
case "script-module":
|
|
if (content) {
|
|
const scriptType =
|
|
/** @type {"script" | "script-module"} */ (type);
|
|
const request = dataUri("text/javascript", value);
|
|
const entryName = `__html_${moduleHash}_${nextEntryIndex++}`;
|
|
const dep = new HtmlInlineScriptDependency(
|
|
request,
|
|
nameEnd,
|
|
[start, end],
|
|
entryName,
|
|
scriptType === "script-module" ? "esm" : "commonjs"
|
|
);
|
|
setLoc(dep, start, end);
|
|
module.addPresentationalDependency(dep);
|
|
reconcileScriptTypeAttr(
|
|
path,
|
|
path.findAttribute("type"),
|
|
nameEnd,
|
|
scriptType,
|
|
source
|
|
);
|
|
(scriptType === "script"
|
|
? scriptEntries
|
|
: scriptModuleEntries
|
|
).push({
|
|
request,
|
|
entryName,
|
|
type: scriptType
|
|
});
|
|
break;
|
|
}
|
|
// falls through — external `<script src>` joins the URL-bearing types
|
|
case "src":
|
|
case "srcset":
|
|
case "css-url":
|
|
case "msapplication-task":
|
|
case "modulepreload":
|
|
case "stylesheet":
|
|
case "html":
|
|
case "preload":
|
|
case "prefetch": {
|
|
// Parse the value into one or more URLs (decoding character
|
|
// references, mapping spans back to raw offsets), then emit a plain
|
|
// asset reference or an entry chunk per URL.
|
|
const parse =
|
|
type === "srcset"
|
|
? parseSrcset
|
|
: type === "css-url"
|
|
? parseCssUrls
|
|
: type === "msapplication-task"
|
|
? parseMsapplicationTask
|
|
: parseSrc;
|
|
let text = value;
|
|
/** @type {number[] | undefined} */
|
|
let map;
|
|
if (value.includes("&")) {
|
|
({ text, map } = decodeEntities(value, true, true));
|
|
if (!/\S/.test(text)) break;
|
|
}
|
|
/** @type {ParsedSource[] | undefined} */
|
|
let parsed;
|
|
try {
|
|
parsed = parse(text);
|
|
} catch (err) {
|
|
const ds = locConverter.get(start);
|
|
const de = locConverter.get(end);
|
|
module.addError(
|
|
new ModuleDependencyError(
|
|
module,
|
|
new WebpackError(
|
|
`Bad value for attribute "${path.attributeName(
|
|
attr
|
|
)}" on element "${elementName}": ${
|
|
/** @type {Error} */ (err).message
|
|
}`
|
|
),
|
|
{
|
|
start: { line: ds.line, column: ds.column },
|
|
end: { line: de.line, column: de.column }
|
|
}
|
|
)
|
|
);
|
|
break;
|
|
}
|
|
if (!parsed) break;
|
|
for (const [url, us, ue] of parsed) {
|
|
// Internal `url(#id)` / fragment-only refs aren't assets.
|
|
if (!url || url.startsWith("#")) continue;
|
|
// Resolve the request against `<base href>`. Only relative
|
|
// URLs are affected (scheme / `//` / `/` URLs ignore the
|
|
// base); an external base drops them from the build.
|
|
let request = url;
|
|
if (
|
|
documentBase &&
|
|
url.charCodeAt(0) !== CC_SLASH &&
|
|
!ABSOLUTE_URL_SCHEME_REGEXP.test(url)
|
|
) {
|
|
if (baseIsExternal) continue;
|
|
request = baseDir + url;
|
|
}
|
|
const s = start + (map ? map[us] : us);
|
|
const e = start + (map ? map[ue] : ue);
|
|
// `html` links the URL as its own emitted page: the same
|
|
// `HtmlEntryDependency` as `<script src>` / `<link>`, but the `html`
|
|
// kind rewrites to the page's filename and emits no sibling tags.
|
|
// Recorded as an independent entry (no `dependOn`), like an entry point.
|
|
if (type === "html") {
|
|
const entryName = `__html_${moduleHash}_${nextEntryIndex++}`;
|
|
const dep = new HtmlEntryDependency(
|
|
request,
|
|
[s, e],
|
|
entryName,
|
|
undefined,
|
|
"html"
|
|
);
|
|
setLoc(dep, s, e);
|
|
module.addPresentationalDependency(dep);
|
|
htmlLinkEntries.push({ request, entryName, type });
|
|
continue;
|
|
}
|
|
// `rel="preload"/"prefetch"` of a bundled resource: an
|
|
// independent, non-executing entry whose `href` is rewritten to
|
|
// the built chunk's URL (CSS for `as="style"`, JS otherwise). No
|
|
// sibling tags — a resource hint references exactly one URL.
|
|
if (type === "preload" || type === "prefetch") {
|
|
const as = getAttributeValue(getAttributesMap(), "as");
|
|
const asStyle = as
|
|
? as.trim().toLowerCase() === "style"
|
|
: false;
|
|
const entryName = `__html_${moduleHash}_${nextEntryIndex++}`;
|
|
// Tag metadata so the template can mirror `crossorigin`/
|
|
// `integrity` onto a native `preload` `<link>` (integrity-
|
|
// eligible per the SRI spec). `prefetch` gets none — the spec
|
|
// doesn't list it — so its `integrity` is left uncaptured.
|
|
const isNativeLink = elementName === "link";
|
|
const dep = new HtmlEntryDependency(
|
|
request,
|
|
[s, e],
|
|
entryName,
|
|
asStyle ? "css-import" : undefined,
|
|
type,
|
|
elementStart,
|
|
tagEnd,
|
|
isNativeLink,
|
|
"",
|
|
nameEnd,
|
|
isNativeLink && path.findAttribute("crossorigin") !== 0,
|
|
-1,
|
|
type === "preload" && isNativeLink
|
|
? captureIntegrityRange(path, source, elementStart)
|
|
: null,
|
|
null
|
|
);
|
|
setLoc(dep, s, e);
|
|
module.addPresentationalDependency(dep);
|
|
(type === "preload"
|
|
? preloadEntries
|
|
: prefetchEntries
|
|
).push({
|
|
request,
|
|
entryName,
|
|
type,
|
|
css: asStyle
|
|
});
|
|
continue;
|
|
}
|
|
// `src`/`srcset`/`css-url`/`msapplication-task` are plain assets;
|
|
// the rest are narrowed to entry types here and share one
|
|
// `HtmlEntryDependency`, recorded for HtmlModulesPlugin.
|
|
if (
|
|
type === "script" ||
|
|
type === "script-module" ||
|
|
type === "modulepreload" ||
|
|
type === "stylesheet"
|
|
) {
|
|
const entryName = `__html_${moduleHash}_${nextEntryIndex++}`;
|
|
const willBeModuleScript =
|
|
type === "script-module" ||
|
|
(outputModule && type === "script");
|
|
/** @type {EntrySourceType} */
|
|
const elementKind =
|
|
type === "modulepreload"
|
|
? "modulepreload"
|
|
: type === "stylesheet"
|
|
? "stylesheet"
|
|
: willBeModuleScript
|
|
? "script-module"
|
|
: "script";
|
|
const entryCategory =
|
|
type === "stylesheet"
|
|
? "css-import"
|
|
: type === "script-module" || type === "modulepreload"
|
|
? "esm"
|
|
: undefined;
|
|
// Native = the loading element is the tag the template clones
|
|
// verbatim (`<script>` / `<link>`); a custom element is not.
|
|
const nativeTag =
|
|
elementKind === "stylesheet" ||
|
|
elementKind === "modulepreload"
|
|
? "link"
|
|
: "script";
|
|
// CSP/fetch attributes the template copies onto siblings.
|
|
let copyableAttrsText = "";
|
|
let hasOwnCrossOrigin = false;
|
|
if (elementStart >= 0) {
|
|
for (const copyableName of COPYABLE_SIBLING_ATTRS) {
|
|
const copyableAttr = path.findAttribute(copyableName);
|
|
if (copyableAttr !== 0) {
|
|
copyableAttrsText += attrSourceSpan(
|
|
path,
|
|
source,
|
|
copyableAttr
|
|
);
|
|
if (copyableName === "crossorigin") {
|
|
hasOwnCrossOrigin = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Blocking-ness of the tag as *emitted*:
|
|
// `reconcileScriptTypeAttr` upgrades every script entry
|
|
// to `type="module"` under `output.module` and strips an
|
|
// authored `type="module"` otherwise — so only non-module
|
|
// output without `defer`/`async` blocks. Non-native
|
|
// elements anchor conservatively (their synthesized
|
|
// siblings may be plain blocking `<script>`s).
|
|
if (
|
|
firstBlockingScriptStart === -1 &&
|
|
(type === "script" || type === "script-module") &&
|
|
!outputModule &&
|
|
(elementName !== nativeTag ||
|
|
(path.findAttribute("defer") === 0 &&
|
|
path.findAttribute("async") === 0))
|
|
) {
|
|
firstBlockingScriptStart = elementStart;
|
|
}
|
|
// Sibling-clone attribute edits, captured from the parsed
|
|
// attributes (offsets relative to the tag) so the template
|
|
// needn't re-parse the tag text: `integrity` is dropped
|
|
// (content-specific) and `script-module` clones are forced
|
|
// to `type="module"` (its value range, else inserted).
|
|
const integrityRange =
|
|
elementStart >= 0 && elementName === nativeTag
|
|
? captureIntegrityRange(path, source, elementStart)
|
|
: null;
|
|
/** @type {Range | null} */
|
|
let typeValueRange = null;
|
|
if (
|
|
elementStart >= 0 &&
|
|
elementName === nativeTag &&
|
|
elementKind === "script-module"
|
|
) {
|
|
const typeAttr = path.findAttribute("type");
|
|
if (
|
|
typeAttr !== 0 &&
|
|
path.attributeValueStart(typeAttr) >= 0
|
|
) {
|
|
typeValueRange = [
|
|
path.attributeValueStart(typeAttr) - elementStart,
|
|
path.attributeValueEnd(typeAttr) - elementStart
|
|
];
|
|
}
|
|
}
|
|
const dep = new HtmlEntryDependency(
|
|
request,
|
|
[s, e],
|
|
entryName,
|
|
entryCategory,
|
|
elementKind,
|
|
elementStart,
|
|
tagEnd,
|
|
elementName === nativeTag,
|
|
copyableAttrsText,
|
|
nameEnd,
|
|
hasOwnCrossOrigin,
|
|
firstBlockingScriptStart,
|
|
integrityRange,
|
|
typeValueRange,
|
|
forceInline,
|
|
headSeen && headStart > 0 ? headStart : -1,
|
|
headSeen && headLastEnd > 0 ? headLastEnd : -1
|
|
);
|
|
setLoc(dep, s, e);
|
|
module.addPresentationalDependency(dep);
|
|
if (
|
|
elementName === "script" &&
|
|
(type === "script" || type === "script-module")
|
|
) {
|
|
reconcileScriptTypeAttr(
|
|
path,
|
|
path.findAttribute("type"),
|
|
nameEnd,
|
|
type,
|
|
source
|
|
);
|
|
}
|
|
(type === "script"
|
|
? scriptEntries
|
|
: type === "script-module"
|
|
? scriptModuleEntries
|
|
: type === "stylesheet"
|
|
? stylesheetEntries
|
|
: modulePreloadEntries
|
|
).push({ request, entryName, type });
|
|
} else {
|
|
const dep = new HtmlSourceDependency(request, [s, e]);
|
|
ResourceHintPlugin.applyDefaults(
|
|
dep,
|
|
ResourceHintPlugin.matchUrlHints(urlHints, request)
|
|
);
|
|
// `<!-- webpackPrefetch: true -->` (etc.) immediately
|
|
// before this tag wins over the project-wide default.
|
|
if (pendingHints) {
|
|
ResourceHintPlugin.applyParsedHints(dep, pendingHints);
|
|
pendingHints = undefined;
|
|
}
|
|
setLoc(dep, s, e);
|
|
module.addDependency(dep);
|
|
module.addCodeGenerationDependency(dep);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
exit: (path) => {
|
|
if (path.namespace() === NS_HTML) {
|
|
const name = path.tagName();
|
|
if (name === "template") {
|
|
if (templateDepth > 0) templateDepth--;
|
|
if (headTemplateDepth > 0) headTemplateDepth--;
|
|
} else if (name === "head") {
|
|
inHead = false;
|
|
}
|
|
}
|
|
pendingWebpackIgnore = undefined;
|
|
pendingWebpackInline = undefined;
|
|
pendingHints = undefined;
|
|
}
|
|
}
|
|
})
|
|
// Skip AST output this walk never reads: `text` (script/style bodies are
|
|
// read by offset via `contentEnd`) and `doctype` — unless head injection
|
|
// needs the doctype as an insertion anchor; keep `comments` for
|
|
// magic comments (`webpackIgnore`).
|
|
.process(source, {
|
|
fragmentContext: this.fragmentContext,
|
|
skip: { text: true, doctype: headOptions === undefined }
|
|
});
|
|
|
|
// `output.html` head injection: existing tags win, injected values are
|
|
// escaped, insertions at the same offset are merged to keep their order.
|
|
if (headOptions !== undefined && headSeen) {
|
|
const { title, meta, base } = headOptions;
|
|
let startTags = "";
|
|
if (meta && meta.charset && headCharsetEnd === -1) {
|
|
startTags += `<meta charset="${escapeAttribute(meta.charset)}">`;
|
|
}
|
|
// <base> must follow the charset declaration per spec
|
|
let baseAfterCharset = "";
|
|
if (base && !headHasBase) {
|
|
if (headCharsetEnd !== -1) baseAfterCharset = baseTag(base);
|
|
else startTags += baseTag(base);
|
|
}
|
|
let endTags = "";
|
|
if (meta) {
|
|
for (const [name, value] of Object.entries(meta)) {
|
|
if (
|
|
name === "charset" ||
|
|
(headMetaNames !== undefined &&
|
|
headMetaNames.has(name.toLowerCase()))
|
|
) {
|
|
continue;
|
|
}
|
|
endTags += metaTag(name, value);
|
|
}
|
|
}
|
|
if (title && !headHasTitle) {
|
|
endTags += `<title>${escapeText(title)}</title>`;
|
|
}
|
|
/** @type {[number, string][]} */
|
|
const inserts = [];
|
|
if (startTags) inserts.push([headStart, startTags]);
|
|
if (baseAfterCharset) inserts.push([headCharsetEnd, baseAfterCharset]);
|
|
if (endTags) inserts.push([headLastEnd, endTags]);
|
|
inserts.sort((a, b) => a[0] - b[0]);
|
|
for (let i = 0; i < inserts.length; i++) {
|
|
const at = inserts[i][0];
|
|
let tags = inserts[i][1];
|
|
while (i + 1 < inserts.length && inserts[i + 1][0] === at) {
|
|
tags += inserts[++i][1];
|
|
}
|
|
module.addPresentationalDependency(new ConstDependency(tags, at));
|
|
}
|
|
}
|
|
|
|
const buildInfo = /** @type {HtmlModuleBuildInfo} */ (module.buildInfo);
|
|
buildInfo.strict = true;
|
|
if (baseUrlPrefix !== undefined) buildInfo.baseUrlPrefix = baseUrlPrefix;
|
|
// Hand off the collected entries to HtmlModulesPlugin; it creates the
|
|
// real compilation entries during the finishMake hook. The `script`
|
|
// and `script-module` groups are chained via a leader-only dependOn
|
|
// so they share a runtime; `modulepreload` and `stylesheet` entries
|
|
// are emitted as independent entries since `<link rel=modulepreload>`
|
|
// must preload without running and stylesheets have no runtime. `html`
|
|
// links are independent page entries too.
|
|
if (
|
|
scriptEntries.length > 0 ||
|
|
scriptModuleEntries.length > 0 ||
|
|
modulePreloadEntries.length > 0 ||
|
|
stylesheetEntries.length > 0 ||
|
|
htmlLinkEntries.length > 0 ||
|
|
preloadEntries.length > 0 ||
|
|
prefetchEntries.length > 0
|
|
) {
|
|
buildInfo.htmlEntries = {
|
|
script: scriptEntries,
|
|
"script-module": scriptModuleEntries,
|
|
modulepreload: modulePreloadEntries,
|
|
stylesheet: stylesheetEntries,
|
|
html: htmlLinkEntries,
|
|
preload: preloadEntries,
|
|
prefetch: prefetchEntries
|
|
};
|
|
}
|
|
|
|
const buildMeta = /** @type {BuildMeta} */ (state.module.buildMeta);
|
|
buildMeta.exportsType = "default";
|
|
|
|
state.module.addDependency(new StaticExportsDependency(["default"], true));
|
|
|
|
return state;
|
|
}
|
|
}
|
|
|
|
HtmlParser.buildHeadTags = buildHeadTags;
|
|
|
|
module.exports = HtmlParser;
|