HtmlParser.js 71 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. */
  4. "use strict";
  5. const Parser = require("../Parser");
  6. const ConstDependency = require("../dependencies/ConstDependency");
  7. const HtmlEntryDependency = require("../dependencies/HtmlEntryDependency");
  8. const HtmlInlineHtmlDependency = require("../dependencies/HtmlInlineHtmlDependency");
  9. const HtmlInlineScriptDependency = require("../dependencies/HtmlInlineScriptDependency");
  10. const HtmlInlineStyleDependency = require("../dependencies/HtmlInlineStyleDependency");
  11. const HtmlSourceDependency = require("../dependencies/HtmlSourceDependency");
  12. const StaticExportsDependency = require("../dependencies/StaticExportsDependency");
  13. const WebpackError = require("../errors/WebpackError");
  14. const ResourceHintPlugin = require("../prefetch/ResourceHintPlugin");
  15. const parseResourceHintOptions = require("../prefetch/parseResourceHintOptions");
  16. const LazySet = require("../util/LazySet");
  17. const LocConverter = require("../util/LocConverter");
  18. const createHash = require("../util/createHash");
  19. const { contextify } = require("../util/identifier");
  20. const {
  21. createMagicCommentContext,
  22. parseMagicComment,
  23. webpackCommentRegExp
  24. } = require("../util/magicComment");
  25. const memoize = require("../util/memoize");
  26. const { JAVASCRIPT_SCRIPT_TYPES } = require("./data");
  27. const {
  28. NS_HTML,
  29. NS_SVG,
  30. NodeType,
  31. SVG_TAG_ADJUST,
  32. SourceProcessor,
  33. baseTag,
  34. buildHeadTags,
  35. decodeEntities,
  36. escapeAttribute,
  37. escapeText,
  38. isAsciiWhitespace,
  39. metaTag,
  40. parseCssUrls,
  41. parseMsapplicationTask,
  42. parseSrc,
  43. parseSrcset
  44. } = require("./syntax");
  45. const getUnsupportedFeatureWarning = memoize(() =>
  46. require("../errors/UnsupportedFeatureWarning")
  47. );
  48. const getModuleDependencyError = memoize(() =>
  49. require("../errors/ModuleDependencyError")
  50. );
  51. const getCommentCompilationWarning = memoize(() =>
  52. require("../errors/CommentCompilationWarning")
  53. );
  54. /** @import { HtmlParserOptions } from "../../declarations/WebpackOptions" */
  55. /** @import { Range } from "../javascript/JavascriptParser" */
  56. /** @import Module, { BuildInfo, BuildMeta } from "../Module" */
  57. /** @import { FileSystemDependencies } from "../Compilation" */
  58. /** @import NormalModule from "../NormalModule" */
  59. /** @import { ParserState, PreparsedAst } from "../Parser" */
  60. /** @import { HtmlModuleBuildInfo } from "./HtmlModule" */
  61. /**
  62. * @typedef {object} HtmlTemplateContext
  63. * @property {Module} module the html module being transformed
  64. * @property {string} resource absolute path of the module's resource
  65. * @property {(dependency: string) => void} addDependency register a file (e.g. a template partial) as a build dependency so editing it triggers a rebuild
  66. * @property {(dependency: string) => void} addContextDependency register a directory as a build dependency
  67. * @property {(dependency: string) => void} addMissingDependency register a not-yet-existing path as a build dependency so creating it triggers a rebuild
  68. * @property {(dependency: string) => void} addBuildDependency register a build dependency (e.g. a template engine config) so changing it invalidates the cache
  69. * @property {(warning: Error | string) => void} emitWarning report a non-fatal warning on the module
  70. * @property {(error: Error | string) => void} emitError report an error on the module
  71. */
  72. /** @typedef {(source: string, context: HtmlTemplateContext) => string} HtmlTemplateFunction */
  73. /** @import { ParsedSource } from "./syntax" */
  74. // Cheap pre-filter for a `style="..."` attribute: only route it through the
  75. // CSS pipeline when it can hold a URL-bearing function (`url()` / `src()` /
  76. // `image()` / `image-set()`), otherwise the processed text equals the input.
  77. const STYLE_ATTR_URL_REGEXP = /url\(|src\(|image\(|image-set\(/i;
  78. // Cheap pre-filter for a `css-url` attribute value (an SVG presentation
  79. // attribute such as `fill`, `clip-path`, …): only tokenize values that can
  80. // hold a `url(...)` FuncIRI. Most are plain colors/keywords (`#fff`, `red`).
  81. const FUNC_IRI_URL_REGEXP = /url\(/i;
  82. // Cheap pre-filter for `<iframe srcdoc>` markup: only spin up a nested HTML
  83. // module when the document can reference an asset — via any attribute (`src=`,
  84. // `href=`, `style=`, …), a CSS `url(...)`, or a CSS `@import`. Pure
  85. // formatting/text markup (`<p>hi</p>`) rewrites to itself, so skip it.
  86. const SRCDOC_ASSET_REGEXP = /[=]|url\(|@import/i;
  87. // A URL carrying its own scheme (`https:`, `data:`, …) ignores the document
  88. // base per the URL spec, so `<base href>` never rewrites it.
  89. const ABSOLUTE_URL_SCHEME_REGEXP = /^[a-zA-Z][a-zA-Z\d+\-.]*:/;
  90. // a `charset=` declaration inside an `http-equiv="content-type"` content value
  91. const CHARSET_DECLARATION_REGEXP = /charset\s*=/i;
  92. // CSP/fetch attributes copied verbatim onto a synthesized sibling `<link>` /
  93. // `<script>` (`HtmlEntryDependency`). Fixed output order, independent of
  94. // source order.
  95. const COPYABLE_SIBLING_ATTRS = ["nonce", "crossorigin", "referrerpolicy"];
  96. const CC_QUOTATION = '"'.charCodeAt(0);
  97. const CC_APOSTROPHE = "'".charCodeAt(0);
  98. const CC_SLASH = "/".charCodeAt(0);
  99. /**
  100. * Byte-exact source span of an attribute including the single leading
  101. * whitespace (` name`, ` name=value`, ` name="value"`), mirroring the
  102. * tokenizer's end-of-attribute rule (`valueEnd + 1` past the closing quote).
  103. * @param {import("./syntax").HtmlPath} path walk path (used only for attribute-ref reads)
  104. * @param {string} source HTML source
  105. * @param {import("./syntax").HtmlAttributeRef} attr attribute ref
  106. * @returns {string} the attribute's source slice
  107. */
  108. const attrSourceSpan = (path, source, attr) => {
  109. const valueStart = path.attributeValueStart(attr);
  110. const valueEnd = path.attributeValueEnd(attr);
  111. const end =
  112. valueStart === -1
  113. ? path.attributeNameEnd(attr)
  114. : source.charCodeAt(valueStart - 1) === CC_QUOTATION ||
  115. source.charCodeAt(valueStart - 1) === CC_APOSTROPHE
  116. ? valueEnd + 1
  117. : valueEnd;
  118. return source.slice(path.attributeNameStart(attr) - 1, end);
  119. };
  120. /**
  121. * Source span of a tag's `integrity` attribute (offsets relative to
  122. * `elementStart`, leading whitespace and quotes included), so the template can
  123. * drop the content-specific author value without re-parsing. Null when absent.
  124. * @param {import("./syntax").HtmlPath} path walk path (for attribute reads)
  125. * @param {string} source HTML source
  126. * @param {number} elementStart offset of the tag's opening `<`
  127. * @returns {Range | null} the span, or null when there is no `integrity`
  128. */
  129. const captureIntegrityRange = (path, source, elementStart) => {
  130. const integrityAttr = path.findAttribute("integrity");
  131. if (integrityAttr === 0 || path.attributeNameStart(integrityAttr) < 0) {
  132. return null;
  133. }
  134. let rs = path.attributeNameStart(integrityAttr);
  135. while (rs > 0 && isAsciiWhitespace(source.charCodeAt(rs - 1))) {
  136. rs--;
  137. }
  138. let re;
  139. const ivs = path.attributeValueStart(integrityAttr);
  140. if (ivs === -1) {
  141. re = path.attributeNameEnd(integrityAttr);
  142. } else {
  143. const q = source.charCodeAt(ivs - 1);
  144. const ive = path.attributeValueEnd(integrityAttr);
  145. re = q === CC_QUOTATION || q === CC_APOSTROPHE ? ive + 1 : ive;
  146. }
  147. return [rs - elementStart, re - elementStart];
  148. };
  149. /**
  150. * @param {Map<string, string>} attributes attributes
  151. * @param {string} name name
  152. * @returns {string | undefined} attribute value
  153. */
  154. const getAttributeValue = (attributes, name) => attributes.get(name);
  155. /** @type {Map<string, Set<string>>} */
  156. const META = new Map([
  157. [
  158. "name",
  159. new Set([
  160. // msapplication-TileImage
  161. "msapplication-tileimage",
  162. "msapplication-square70x70logo",
  163. "msapplication-square150x150logo",
  164. "msapplication-wide310x150logo",
  165. "msapplication-square310x310logo",
  166. "msapplication-config",
  167. // Only the `icon-uri` part is an asset, see `parseMsapplicationTask`
  168. "msapplication-task",
  169. "twitter:image",
  170. "twitter:image:src",
  171. // Twitter player card: URL to the raw video/media stream
  172. "twitter:player:stream",
  173. // Legacy preview-image hint
  174. "thumbnail"
  175. ])
  176. ],
  177. [
  178. "property",
  179. new Set([
  180. "og:image",
  181. "og:image:url",
  182. "og:image:secure_url",
  183. "og:audio",
  184. "og:audio:secure_url",
  185. "og:video",
  186. "og:video:secure_url",
  187. "vk:image"
  188. ])
  189. ],
  190. [
  191. "itemprop",
  192. new Set([
  193. "image",
  194. "logo",
  195. "screenshot",
  196. "thumbnailurl",
  197. "contenturl",
  198. "downloadurl",
  199. "duringmedia",
  200. "embedurl",
  201. "installurl",
  202. "layoutimage"
  203. ])
  204. ]
  205. ]);
  206. /**
  207. * @param {Map<string, string>} attributes attributes
  208. * @returns {boolean} true when need to parse, otherwise false
  209. */
  210. const filterLinkItemprop = (attributes) => {
  211. const itemprop = getAttributeValue(attributes, "itemprop");
  212. if (!itemprop) return false;
  213. const allowedAttributes = META.get("itemprop");
  214. if (!allowedAttributes) return false;
  215. return allowedAttributes.has(itemprop.trim().toLowerCase());
  216. };
  217. // `<link rel>` values whose `href`/`imagesrcset` webpack treats as a reference.
  218. const ALLOWED_LINK_RELS = new Set([
  219. "stylesheet",
  220. "icon",
  221. "mask-icon",
  222. "apple-touch-icon",
  223. "apple-touch-icon-precomposed",
  224. "apple-touch-startup-image",
  225. // Routed to the `asset/webmanifest` module type, which parses its icon URLs.
  226. "manifest",
  227. "prefetch",
  228. "preload",
  229. "modulepreload",
  230. // Legacy preview-image hint (`<link rel="image_src" href>`)
  231. "image_src"
  232. ]);
  233. /**
  234. * @param {Map<string, string>} attributes attributes
  235. * @returns {boolean} true when need to parse, otherwise false
  236. */
  237. const filterLinkHref = (attributes) => {
  238. const rel = getAttributeValue(attributes, "rel");
  239. if (!rel) return false;
  240. const usedRels = rel.trim().toLowerCase().split(/\s+/);
  241. for (let i = 0; i < usedRels.length; i++) {
  242. if (ALLOWED_LINK_RELS.has(usedRels[i])) return true;
  243. }
  244. return false;
  245. };
  246. /**
  247. * @param {Map<string, string>} attributes attributes
  248. * @returns {boolean} true when need to parse, otherwise false
  249. */
  250. const filterLinkUnion = (attributes) =>
  251. filterLinkHref(attributes) || filterLinkItemprop(attributes);
  252. /**
  253. * @param {Map<string, string>} attributes attributes
  254. * @returns {boolean} true when need to parse, otherwise false
  255. */
  256. const filterMetaContent = (attributes) => {
  257. for (const item of META) {
  258. const [key, allowedNames] = item;
  259. const name = getAttributeValue(attributes, key);
  260. if (!name) continue;
  261. // Check every present attribute, not only the first one
  262. if (allowedNames.has(name.trim().toLowerCase())) return true;
  263. }
  264. return false;
  265. };
  266. /**
  267. * `<param value>` (obsolete `<object>`/`<applet>` child) is only a URL when
  268. * `valuetype="ref"`; otherwise it's an opaque string.
  269. * @param {Map<string, string>} attributes attributes
  270. * @returns {boolean} true when `value` is a URL reference
  271. */
  272. const filterParamRef = (attributes) => {
  273. const valuetype = getAttributeValue(attributes, "valuetype");
  274. if (!valuetype) return false;
  275. return valuetype.trim().toLowerCase() === "ref";
  276. };
  277. /**
  278. * @param {Map<string, string>} attributes attributes
  279. * @returns {boolean} true when the script element opts into ES module semantics
  280. */
  281. const isModuleScript = (attributes) => {
  282. const type = getAttributeValue(attributes, "type");
  283. if (!type) return false;
  284. return type.trim().toLowerCase() === "module";
  285. };
  286. // HTML `<script>` `type` values that the browser treats as executable
  287. /**
  288. * @param {Map<string, string>} attributes attributes
  289. * @returns {boolean} true when the script element's `type` is executable JS
  290. */
  291. const isExecutableJsScript = (attributes) => {
  292. const type = getAttributeValue(attributes, "type");
  293. if (type === undefined) return true;
  294. return JAVASCRIPT_SCRIPT_TYPES.has(type.trim().toLowerCase());
  295. };
  296. /**
  297. * @param {Map<string, string>} attributes attributes
  298. * @returns {boolean} true when the link points at an ES module that should be bundled as an entry chunk
  299. */
  300. const isLinkModulePreload = (attributes) => {
  301. const rel = getAttributeValue(attributes, "rel");
  302. if (!rel) return false;
  303. return rel.trim().toLowerCase().split(/\s+/).includes("modulepreload");
  304. };
  305. /**
  306. * @param {Map<string, string>} attributes attributes
  307. * @returns {boolean} true when the link is a `<link rel="stylesheet">` that should be bundled as a CSS entry chunk
  308. */
  309. const isLinkStylesheet = (attributes) => {
  310. const rel = getAttributeValue(attributes, "rel");
  311. if (!rel) return false;
  312. return rel.trim().toLowerCase().split(/\s+/).includes("stylesheet");
  313. };
  314. /** @typedef {"src" | "srcset" | "css-url" | "msapplication-task" | "script" | "script-module" | "modulepreload" | "stylesheet" | "html" | "preload" | "prefetch" | "stylesheet-style" | "stylesheet-style-attribute" | "srcdoc"} SourceType */
  315. /** Entry types: a `type` whose value is loaded as its own compilation entry chunk. */
  316. /** @typedef {"script" | "script-module" | "modulepreload" | "stylesheet" | "html" | "preload" | "prefetch"} EntrySourceType */
  317. /** @typedef {SourceType | ((attrs: Map<string, string>, css: boolean) => SourceType)} SourceTypeOrResolver */
  318. /** @typedef {(attributes: Map<string, string>, value: string) => boolean} SourceFilter */
  319. /** @typedef {{ tag?: string, attribute: string, type: SourceType, filter?: SourceFilter }} SourceEntry */
  320. /** 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). */
  321. /** @typedef {{ type: SourceTypeOrResolver, filter?: SourceFilter, namespace?: number }} SourceItem */
  322. /** A tag→attribute lookup; a `undefined` value marks a built-in source disabled via `sources` (`type: false`). */
  323. /** @typedef {Record<string, SourceItem | undefined>} SourceBucket */
  324. /** @typedef {Record<string, SourceBucket>} SourceTable */
  325. /** A source whose value is the element's text content (a `<style>`/`<script>` body) rather than an attribute. */
  326. /** @typedef {{ type: SourceTypeOrResolver, filter?: (attributes: Map<string, string>) => boolean }} ContentSourceItem */
  327. /**
  328. * Builds a null-prototype dictionary from the given property bags
  329. * (later bags win; `undefined` bags are skipped). A null prototype is
  330. * essential here: the tables are indexed by HTML tag and attribute
  331. * names, so a plain object would let names like `__proto__`,
  332. * `constructor`, or `toString` resolve to inherited values at lookup
  333. * time — producing bogus dependencies and letting `sources: false` be
  334. * bypassed.
  335. * @param {...(Record<string, EXPECTED_ANY> | undefined)} bags property bags
  336. * @returns {EXPECTED_ANY} null-prototype dictionary
  337. */
  338. const dict = (...bags) => Object.assign(Object.create(null), ...bags);
  339. // Shared `SourceItem` singletons used by the built-in defaults —
  340. // keeping one instance per kind stabilizes V8's hidden classes across
  341. // lookups in the walk. `srcset` is its own type; the walk picks the
  342. // `parseSrcset` parser for it and otherwise treats it like `src`.
  343. /** @type {SourceItem} */
  344. const PLAIN_SRC = { type: "src", filter: undefined };
  345. /** @type {SourceItem} */
  346. const PLAIN_SRCSET = { type: "srcset", filter: undefined };
  347. /**
  348. * `<link href>` is polymorphic: `rel="modulepreload"` → an ESM
  349. * preload entry, `rel="stylesheet"` (with `experiments.css`) → a CSS
  350. * entry, otherwise a plain asset URL.
  351. * @type {SourceItem}
  352. */
  353. const LINK_HREF = {
  354. type: (attrs, css) => {
  355. if (isLinkModulePreload(attrs)) return "modulepreload";
  356. if (css && isLinkStylesheet(attrs)) return "stylesheet";
  357. // `rel="preload"/"prefetch"` with `as="script"` (or `as="style"` when
  358. // `experiments.css` is on) becomes a bundled, non-executing entry whose
  359. // `href` is rewritten to the built chunk; other `as` targets (image,
  360. // font, fetch, …) stay plain asset URLs.
  361. const rel = getAttributeValue(attrs, "rel");
  362. if (rel) {
  363. const rels = rel.trim().toLowerCase().split(/\s+/);
  364. const kind = rels.includes("preload")
  365. ? "preload"
  366. : rels.includes("prefetch")
  367. ? "prefetch"
  368. : undefined;
  369. if (kind) {
  370. const as = getAttributeValue(attrs, "as");
  371. const asLower = as ? as.trim().toLowerCase() : "";
  372. if (asLower === "script" || (css && asLower === "style")) return kind;
  373. }
  374. }
  375. return "src";
  376. },
  377. filter: filterLinkUnion
  378. };
  379. /**
  380. * `<script src>`: non-JS types (e.g. `application/ld+json`,
  381. * `importmap`) stay plain asset URLs so the browser keeps seeing them
  382. * as data blocks; `type="module"` opts into the ESM entry chunk;
  383. * everything else in `JAVASCRIPT_SCRIPT_TYPES` is a classic script.
  384. * @type {SourceItem}
  385. */
  386. const SCRIPT_SRC = {
  387. type: (attrs) =>
  388. isExecutableJsScript(attrs)
  389. ? isModuleScript(attrs)
  390. ? "script-module"
  391. : "script"
  392. : "src",
  393. filter: undefined
  394. };
  395. /**
  396. * `<meta content>`: most referenced names hold a single URL (`src`); the
  397. * `msapplication-task` value is a `;`-delimited list whose `icon-uri` part
  398. * is the only asset, so it selects the `msapplication-task` parser.
  399. * @type {SourceItem}
  400. */
  401. const META_CONTENT = {
  402. type: (attrs) => {
  403. const name = attrs.get("name");
  404. return name !== undefined &&
  405. name.trim().toLowerCase() === "msapplication-task"
  406. ? "msapplication-task"
  407. : "src";
  408. },
  409. filter: filterMetaContent
  410. };
  411. // Built-in lookup table, written directly in its final resolved shape:
  412. // `DEFAULT_SOURCES_BY_TAG[tag][attribute] = item`. No module-load
  413. // loop, no separate array representation — every parser created with
  414. // the default `sources` config just references this table and pays
  415. // zero per-parser work.
  416. /** @type {Record<string, Record<string, SourceItem>>} */
  417. const DEFAULT_SOURCES_BY_TAG = dict({
  418. // Obsolete Java-applet element; `code`/`object` are single class/object URLs.
  419. applet: dict({ code: PLAIN_SRC, object: PLAIN_SRC }),
  420. audio: dict({ src: PLAIN_SRC }),
  421. // Deprecated presentational `background` attribute — an image URL.
  422. body: dict({ background: PLAIN_SRC }),
  423. embed: dict({ src: PLAIN_SRC }),
  424. // `srcdoc` is an entity-encoded HTML document parsed and rewritten as a
  425. // nested module — its handling lives in the generic source loop so the
  426. // `sources` option can re-target it (or other tags/attributes) freely.
  427. iframe: dict({ srcdoc: { type: "srcdoc", filter: undefined } }),
  428. img: dict({ src: PLAIN_SRC, srcset: PLAIN_SRCSET }),
  429. input: dict({ src: PLAIN_SRC }),
  430. link: dict({
  431. href: LINK_HREF,
  432. imagesrcset: { type: "srcset", filter: filterLinkHref }
  433. }),
  434. meta: dict({ content: META_CONTENT }),
  435. // MathML `<mglyph src>` references an image.
  436. mglyph: dict({ src: PLAIN_SRC }),
  437. // `classid` is a single object URI (`codebase`/`archive` are a base/list, skipped).
  438. object: dict({ data: PLAIN_SRC, classid: PLAIN_SRC }),
  439. // Obsolete `<param valuetype="ref" value="url">` child of `<object>`.
  440. param: dict({ value: { type: "src", filter: filterParamRef } }),
  441. // `href`/`xlink:href` reference the source of SVG `<script>` elements
  442. script: dict({
  443. src: SCRIPT_SRC,
  444. href: SCRIPT_SRC,
  445. "xlink:href": SCRIPT_SRC
  446. }),
  447. source: dict({ src: PLAIN_SRC, srcset: PLAIN_SRCSET }),
  448. // Deprecated presentational `background` attribute — an image URL.
  449. table: dict({ background: PLAIN_SRC }),
  450. td: dict({ background: PLAIN_SRC }),
  451. th: dict({ background: PLAIN_SRC }),
  452. track: dict({ src: PLAIN_SRC }),
  453. video: dict({ poster: PLAIN_SRC, src: PLAIN_SRC }),
  454. // SVG. Tag names match the tree builder's adjusted camelCase
  455. // (`feImage`/`textPath`/`linearGradient`/`radialGradient`). `href`/
  456. // `xlink:href` reference another element/resource; fragment-only `#id`
  457. // values are left untouched, so only external `file.svg#id` is rewritten.
  458. // `color-profile`'s `xlink:href` points at an external ICC profile file.
  459. // `cursor`/`font-face-uri` point at an external image/font file; `altGlyph`/
  460. // `glyphRef`/`tref` are legacy element references (removed in SVG 2).
  461. altGlyph: dict({ "xlink:href": PLAIN_SRC, href: PLAIN_SRC }),
  462. "color-profile": dict({ "xlink:href": PLAIN_SRC, href: PLAIN_SRC }),
  463. cursor: dict({ "xlink:href": PLAIN_SRC, href: PLAIN_SRC }),
  464. feImage: dict({ "xlink:href": PLAIN_SRC, href: PLAIN_SRC }),
  465. filter: dict({ "xlink:href": PLAIN_SRC, href: PLAIN_SRC }),
  466. "font-face-uri": dict({ "xlink:href": PLAIN_SRC, href: PLAIN_SRC }),
  467. glyphRef: dict({ "xlink:href": PLAIN_SRC, href: PLAIN_SRC }),
  468. image: dict({ "xlink:href": PLAIN_SRC, href: PLAIN_SRC }),
  469. linearGradient: dict({ "xlink:href": PLAIN_SRC, href: PLAIN_SRC }),
  470. mpath: dict({ "xlink:href": PLAIN_SRC, href: PLAIN_SRC }),
  471. pattern: dict({ "xlink:href": PLAIN_SRC, href: PLAIN_SRC }),
  472. radialGradient: dict({ "xlink:href": PLAIN_SRC, href: PLAIN_SRC }),
  473. textPath: dict({ "xlink:href": PLAIN_SRC, href: PLAIN_SRC }),
  474. tref: dict({ "xlink:href": PLAIN_SRC, href: PLAIN_SRC }),
  475. use: dict({ "xlink:href": PLAIN_SRC, href: PLAIN_SRC })
  476. // CSS `url(...)` references in SVG presentation attributes (fill, stroke,
  477. // clip-path, …) are handled separately during the walk (`parseCssUrls`).
  478. });
  479. // SVG presentation attributes (`fill`, `stroke`, …) carry CSS `url(...)`
  480. // FuncIRIs; `parseCssUrls` extracts external refs as assets (internal
  481. // `url(#id)` is left untouched). They apply to every SVG-namespace element, so
  482. // the item is `namespace`-gated rather than placed in a tag bucket. The filter
  483. // is a cheap pre-check skipping the common plain values (`red`, `none`, `#fff`).
  484. /** @type {SourceItem} */
  485. const SVG_CSS_URL = {
  486. type: "css-url",
  487. filter: (attributes, value) => FUNC_IRI_URL_REGEXP.test(value),
  488. namespace: NS_SVG
  489. };
  490. // The global `style=""` attribute (a CSS declaration list), available on every
  491. // tag and even with `sources: false`. Like every `stylesheet-style*` source it
  492. // only emits when `experiments.css` is on (checked where the dependency is
  493. // created); the filter is a cheap pre-check skipping declarations without a
  494. // `url()`/`src()`/… .
  495. /** @type {SourceItem} */
  496. const STYLE_ATTRIBUTE = {
  497. type: "stylesheet-style-attribute",
  498. filter: (attributes, value) => STYLE_ATTR_URL_REGEXP.test(value)
  499. };
  500. // Any-tag sources — applied to every element as a fallback and folded into
  501. // each tag bucket, so the walk resolves one object per element and reads one
  502. // property per attribute. `DEFAULT` (svg presentation + style) is active
  503. // unless `sources: false`; only the always-on style attribute survives `false`.
  504. /** @type {Record<string, SourceItem>} */
  505. const DEFAULT_ANY_SOURCES = dict({ style: STYLE_ATTRIBUTE });
  506. for (const name of [
  507. "fill",
  508. "stroke",
  509. "clip-path",
  510. "mask",
  511. "filter",
  512. "marker",
  513. "marker-start",
  514. "marker-mid",
  515. "marker-end",
  516. "cursor"
  517. ]) {
  518. DEFAULT_ANY_SOURCES[name] = SVG_CSS_URL;
  519. }
  520. /** @type {Record<string, SourceItem>} */
  521. const ALWAYS_ANY_SOURCES = dict({ style: STYLE_ATTRIBUTE });
  522. // Reserved key holding the any-tag sources inside the per-tag table — the
  523. // schema forbids an empty `tag`, so it never collides with a real tag. The
  524. // walk falls back to it for elements without their own bucket.
  525. const ANY_TAG = "";
  526. /**
  527. * Folds the any-tag sources into every per-tag bucket (tag-specific entries
  528. * win) and stores them under `ANY_TAG`, so the whole source model is one
  529. * object and the walk's per-attribute lookup is a single property read.
  530. * @param {SourceTable} byTag per-tag sources
  531. * @param {SourceBucket} any any-tag sources
  532. * @returns {SourceTable} folded table
  533. */
  534. const foldAnySources = (byTag, any) => {
  535. for (const tag of Object.keys(byTag)) {
  536. byTag[tag] = dict(any, byTag[tag]);
  537. }
  538. byTag[ANY_TAG] = any;
  539. return byTag;
  540. };
  541. // Default table (per-tag defaults + any-tag sources folded in) and the
  542. // `sources: false` table (only the always-on `style=""` attribute). Both are
  543. // precomputed so the common cases reference them with zero per-parser work.
  544. /** @type {SourceTable} */
  545. const DEFAULT_SOURCES_FOLDED = foldAnySources(
  546. dict(DEFAULT_SOURCES_BY_TAG),
  547. DEFAULT_ANY_SOURCES
  548. );
  549. /** @type {SourceTable} */
  550. const DISABLED_SOURCES_FOLDED = foldAnySources(dict(), ALWAYS_ANY_SOURCES);
  551. /**
  552. * Inline `<script>` body: classic JS unless `type="module"` opts into ESM.
  553. * @param {Map<string, string>} attrs attributes
  554. * @returns {SourceType} the entry type for the inline script
  555. */
  556. const scriptContentType = (attrs) =>
  557. isModuleScript(attrs) ? "script-module" : "script";
  558. /**
  559. * Inline `<script>` body: only an executable JS block with no external source
  560. * carries content webpack should bundle.
  561. * @param {Map<string, string>} attrs attributes
  562. * @returns {boolean} true when the inline body should be bundled
  563. */
  564. const scriptContentFilter = (attrs) =>
  565. !attrs.has("src") &&
  566. !attrs.has("href") &&
  567. !attrs.has("xlink:href") &&
  568. isExecutableJsScript(attrs);
  569. /**
  570. * `<style>` body is a stylesheet only when its `type` is empty or `text/css`.
  571. * @param {Map<string, string>} attrs attributes
  572. * @returns {boolean} true when the body is CSS
  573. */
  574. const styleContentFilter = (attrs) => {
  575. const type = attrs.get("type");
  576. if (type === undefined) return true;
  577. const t = type.trim().toLowerCase();
  578. return t === "" || t === "text/css";
  579. };
  580. // Element-body sources, independent of the `sources` option (even `sources:
  581. // false`): inline `<script>` bodies are always processed; the `<style>` body
  582. // only emits when `experiments.css` is on (checked where the dependency is
  583. // created, like every `stylesheet-style*` source).
  584. /** @type {Record<string, ContentSourceItem>} */
  585. const CONTENT_SOURCES = dict({
  586. script: { type: scriptContentType, filter: scriptContentFilter },
  587. style: { type: "stylesheet-style", filter: styleContentFilter }
  588. });
  589. class HtmlParser extends Parser {
  590. /**
  591. * Creates an instance of HtmlParser.
  592. * @param {HtmlParserOptions} options parser options (from `module.parser.html`; always passed by the `createParser` hook)
  593. */
  594. constructor(options) {
  595. super();
  596. this.magicCommentContext = createMagicCommentContext();
  597. // Read by HtmlModulesPlugin's `processResult` hook, which transforms
  598. // the module source before it is stored and parsed.
  599. /** @type {HtmlTemplateFunction | undefined} */
  600. this.template = options.template;
  601. /** @type {import("../../declarations/WebpackOptions").UrlHintRule[] | undefined} */
  602. this._urlHints = options.urlHints;
  603. // `as` selects the parse mode — the HTML analog of the CSS parser's `as`.
  604. // `"document"` (default) parses a full page; any other value is the
  605. // context element whose inner HTML the source is parsed as (a fragment),
  606. // so context-sensitive tags (`<tr>`, `<td>`, `<option>`, …) in a partial
  607. // are kept, not dropped. `"template"` is the neutral generic fragment.
  608. /** @type {string | undefined} */
  609. this.fragmentContext =
  610. options.as === undefined || options.as === "document"
  611. ? undefined
  612. : options.as;
  613. // One source model: a per-tag table that also holds the any-tag sources
  614. // under `ANY_TAG`. The common cases reference precomputed tables.
  615. /** @type {SourceTable} */
  616. this.sourcesByTag = DEFAULT_SOURCES_FOLDED;
  617. const sources = options.sources;
  618. if (sources === undefined || sources === true) return;
  619. if (sources === false) {
  620. // Only the always-on `style=""` attribute survives; nothing else is
  621. // extracted (svg presentation, `<script src>`, `<link>` entries, …).
  622. this.sourcesByTag = DISABLED_SOURCES_FOLDED;
  623. return;
  624. }
  625. // User array — `"..."` anywhere opts the per-tag defaults in as the
  626. // base; the built-in any-tag sources (svg presentation + style) are
  627. // always present. A user entry with no `tag` is an any-tag source (e.g.
  628. // `{ attribute: "data-style", type: "stylesheet-style-attribute" }`).
  629. // User entries override regardless of position. `dict()` keeps every
  630. // table null-prototype (see its doc), and per-tag writes rebuild the
  631. // bucket so the aliased default buckets stay intact.
  632. /** @type {SourceTable} */
  633. const byTag = sources.includes("...")
  634. ? dict(DEFAULT_SOURCES_BY_TAG)
  635. : dict();
  636. /** @type {SourceBucket} */
  637. const any = dict(DEFAULT_ANY_SOURCES);
  638. for (const entry of sources) {
  639. if (entry === "...") continue;
  640. // `type: false` disables a built-in source: writing `undefined` over the
  641. // attribute makes the walk's `sources[name]` lookup skip it (see below).
  642. /** @type {SourceItem | undefined} */
  643. const item =
  644. entry.type === false
  645. ? undefined
  646. : {
  647. type: entry.type,
  648. filter:
  649. typeof entry.filter === "function" ? entry.filter : undefined
  650. };
  651. const attr = entry.attribute.toLowerCase();
  652. if (entry.tag === undefined) {
  653. any[attr] = item;
  654. } else {
  655. const tag = entry.tag.toLowerCase();
  656. byTag[tag] = dict(byTag[tag], { [attr]: item });
  657. // The AST carries adjusted camelCase names for foreign-content
  658. // tags (e.g. `feImage`) — register the entry under both.
  659. const adjusted = SVG_TAG_ADJUST[tag];
  660. if (adjusted !== undefined) {
  661. byTag[adjusted] = dict(byTag[adjusted], { [attr]: item });
  662. }
  663. }
  664. }
  665. /** @type {SourceTable} */
  666. this.sourcesByTag = foldAnySources(byTag, any);
  667. }
  668. /**
  669. * Runs the `template` option over the source and returns the transformed
  670. * html. Called from HtmlModulesPlugin's `processResult`, where the return
  671. * value becomes the module's stored source so the parser (which records
  672. * dependency offsets against it) and the generator (which renders from
  673. * `module.originalSource()`) stay in agreement.
  674. * @param {string | Buffer} source the original source
  675. * @param {NormalModule} module the html module
  676. * @returns {string | Buffer} the transformed source
  677. */
  678. applyTemplate(source, module) {
  679. if (!this.template) return source;
  680. // `processResult` runs after `_doBuild` has initialized these
  681. // dependency sets, so they are always present here.
  682. const buildInfo = /** @type {BuildInfo} */ (module.buildInfo);
  683. const fileDependencies = /** @type {FileSystemDependencies} */ (
  684. buildInfo.fileDependencies
  685. );
  686. const contextDependencies = /** @type {FileSystemDependencies} */ (
  687. buildInfo.contextDependencies
  688. );
  689. const missingDependencies = /** @type {FileSystemDependencies} */ (
  690. buildInfo.missingDependencies
  691. );
  692. const transformed = this.template(
  693. typeof source === "string" ? source : source.toString("utf8"),
  694. {
  695. module,
  696. resource: module.resource,
  697. addDependency: (dependency) => {
  698. fileDependencies.add(dependency);
  699. },
  700. addContextDependency: (dependency) => {
  701. contextDependencies.add(dependency);
  702. },
  703. addMissingDependency: (dependency) => {
  704. missingDependencies.add(dependency);
  705. },
  706. addBuildDependency: (dependency) => {
  707. if (buildInfo.buildDependencies === undefined) {
  708. buildInfo.buildDependencies = new LazySet();
  709. }
  710. buildInfo.buildDependencies.add(dependency);
  711. },
  712. emitWarning: (warning) =>
  713. module.addWarning(
  714. warning instanceof Error ? warning : new WebpackError(warning)
  715. ),
  716. emitError: (error) =>
  717. module.addError(
  718. error instanceof Error ? error : new WebpackError(error)
  719. )
  720. }
  721. );
  722. if (typeof transformed !== "string") {
  723. throw new Error(
  724. "The `template` html parser option must return a string."
  725. );
  726. }
  727. return transformed;
  728. }
  729. /**
  730. * Parses the provided source and updates the parser state.
  731. * @param {string | Buffer | PreparsedAst} source the source to parse
  732. * @param {ParserState} state the parser state
  733. * @returns {ParserState} the parser state
  734. */
  735. parse(source, state) {
  736. if (Buffer.isBuffer(source)) {
  737. source = source.toString("utf8");
  738. } else if (typeof source === "object") {
  739. throw new Error("webpackAst is unexpected for the HtmlParser");
  740. }
  741. if (source[0] === "\uFEFF") {
  742. source = source.slice(1);
  743. }
  744. const locConverter = new LocConverter(source);
  745. const module = state.module;
  746. const compilation = state.compilation;
  747. const { hashFunction, module: outputModule } = compilation.outputOptions;
  748. const context = compilation.compiler.context;
  749. const css = Boolean(compilation.options.experiments.css);
  750. const urlHints = this._urlHints;
  751. // Stable, per-HTML-module prefix used when generating entry names for
  752. // script src / modulepreload references so they don't collide across
  753. // HTML modules in the same compilation. We hash the module's resource
  754. // path (a plain absolute path) — going through `contextify` against
  755. // the compilation root keeps the hash machine-stable for the same
  756. // project layout. Note: `module.identifier()` returns `html|<path>`
  757. // for HTML modules, which doesn't start with `/`, so contextify would
  758. // leave it absolute. `module.resource` is the bare path.
  759. /** @type {string} */
  760. const resource =
  761. /** @type {EXPECTED_ANY} */ (module).resource || module.identifier();
  762. const moduleHash = createHash(hashFunction || "md4")
  763. .update(context ? contextify(context, resource) : resource)
  764. .digest("hex")
  765. .slice(0, 8);
  766. // `output.html` head injection: facts are collected during the walk and
  767. // become presentational dependencies afterwards — no later asset pass has
  768. // to re-parse the page. `data:` documents are skipped: synthetic entry
  769. // wrappers already carry the tags and iframe srcdoc pages must stay
  770. // untouched.
  771. const htmlOutputOptions =
  772. compilation.options && compilation.options.output
  773. ? compilation.options.output.html
  774. : undefined;
  775. const headOptions =
  776. typeof htmlOutputOptions === "object" &&
  777. (htmlOutputOptions.title !== undefined ||
  778. htmlOutputOptions.meta !== undefined ||
  779. htmlOutputOptions.base !== undefined) &&
  780. !resource.startsWith("data:")
  781. ? htmlOutputOptions
  782. : undefined;
  783. let doctypeEnd = -1;
  784. let htmlTagEnd = -1;
  785. // insertion offsets just inside the (possibly implicit) head
  786. let headStart = 0;
  787. let headLastEnd = 0;
  788. let headSeen = false;
  789. let inHead = false;
  790. // `<template>` contents are inert
  791. let headTemplateDepth = 0;
  792. // Depth of open `<template>` elements anywhere in the tree. Template
  793. // content is an inert, separate fragment per spec, so a `<base>` inside
  794. // it must NOT set the document base (`headTemplateDepth` only covers
  795. // templates seen while in `<head>`, so it can't guard body templates).
  796. let templateDepth = 0;
  797. let headHasTitle = false;
  798. let headHasBase = false;
  799. let headCharsetEnd = -1;
  800. /** @type {Set<string> | undefined} */
  801. let headMetaNames;
  802. // Script src / modulepreload references are collected per-type
  803. // during the walk; HtmlModulesPlugin later turns them into real
  804. // entries. `script` and `script-module` entries are chained via a
  805. // leader-only dependOn so they share a runtime.
  806. // `<link rel="modulepreload">` entries are kept independent — they
  807. // must preload without running, so they can never become a runtime
  808. // leader that other entries would import.
  809. /**
  810. * @typedef {object} HtmlEntryInfo
  811. * @property {string} request
  812. * @property {string} entryName
  813. * @property {"script" | "script-module" | "modulepreload" | "stylesheet" | "html" | "preload" | "prefetch"} type
  814. * @property {boolean=} css entry emits a CSS chunk (`<link rel="preload" as="style">`), so it needs the CSS filename template
  815. */
  816. /** @type {HtmlEntryInfo[]} */
  817. const scriptEntries = [];
  818. /** @type {HtmlEntryInfo[]} */
  819. const scriptModuleEntries = [];
  820. /** @type {HtmlEntryInfo[]} */
  821. const modulePreloadEntries = [];
  822. /** @type {HtmlEntryInfo[]} */
  823. const stylesheetEntries = [];
  824. // `type: "html"` links — each referenced `.html` becomes its own page
  825. // entry (`<a href="page.html">`), independent (no dependOn).
  826. /** @type {HtmlEntryInfo[]} */
  827. const htmlLinkEntries = [];
  828. // `<link rel="preload"/"prefetch">` of a bundled resource: each becomes an
  829. // independent, non-executing entry whose `href` is rewritten to the built
  830. // chunk's URL (JS for `as="script"`, CSS for `as="style"`).
  831. /** @type {HtmlEntryInfo[]} */
  832. const preloadEntries = [];
  833. /** @type {HtmlEntryInfo[]} */
  834. const prefetchEntries = [];
  835. // Offset of the first classic blocking script tag; injected stylesheet
  836. // `<link>`s stay ahead of it. `defer`/`async`/module scripts don't
  837. // anchor — they execute after parsing (and after pending stylesheets),
  838. // so CSS may follow their tags (the order Vite emits).
  839. let firstBlockingScriptStart = -1;
  840. let nextEntryIndex = 0;
  841. // `<base href>` resolves the relative URLs that follow it. A relative base
  842. // (`./assets/`) rewrites them into a subdirectory — still bundled; a
  843. // root-relative or absolute base (`/`, `https://cdn/`) points them outside
  844. // the build, so those URLs are left untouched. Resolved in the walk below
  845. // from the first `<base href>` seen (`documentBase` undefined until then).
  846. /** @type {string | undefined} */
  847. let documentBase;
  848. /** @type {string | undefined} */
  849. let baseDir;
  850. let baseIsExternal = false;
  851. // Prepended to the emitted URLs' auto-public-path undo path so the base
  852. // doesn't misdirect them: the browser resolves rewritten (relative)
  853. // output URLs against the base dir, so one `../` per base segment cancels
  854. // it. Undefined for absolute publicPath (no undo path is emitted).
  855. /** @type {string | undefined} */
  856. let baseUrlPrefix;
  857. /**
  858. * Tracks the `webpackIgnore` value from the most recent comment that
  859. * appears before the next tag. Reset whenever a tag is emitted or a
  860. * comment without a `webpackIgnore` value is encountered.
  861. * @type {boolean | undefined}
  862. */
  863. let pendingWebpackIgnore;
  864. /** @type {boolean | undefined} */
  865. let pendingWebpackInline;
  866. /**
  867. * Tracks the resource-hint values from the most recent `<!-- webpackPrefetch: … -->`
  868. * (etc.) comment so the next asset URL attribute picks them up.
  869. * @type {import("../prefetch/parseResourceHintOptions").ResourceHintOptions | undefined}
  870. */
  871. let pendingHints;
  872. const magicCommentContext = this.magicCommentContext;
  873. /**
  874. * @param {import("./syntax").HtmlPath} path walk path (used only for attribute-ref reads)
  875. * @param {import("./syntax").HtmlAttributeRef} typeAttr type attribute ref (0 = none)
  876. * @param {number} nameEnd end offset of the tag name
  877. * @param {string} type type of the script
  878. * @param {string} input source string
  879. */
  880. const reconcileScriptTypeAttr = (path, typeAttr, nameEnd, type, input) => {
  881. const valueStart =
  882. typeAttr !== 0 ? path.attributeValueStart(typeAttr) : -1;
  883. const valueEnd = typeAttr !== 0 ? path.attributeValueEnd(typeAttr) : -1;
  884. if (outputModule && type === "script") {
  885. // Chunk is an ES module; upgrade the tag.
  886. if (typeAttr !== 0 && valueStart !== -1) {
  887. module.addPresentationalDependency(
  888. new ConstDependency("module", [valueStart, valueEnd])
  889. );
  890. } else {
  891. module.addPresentationalDependency(
  892. new ConstDependency(' type="module"', nameEnd)
  893. );
  894. }
  895. } else if (!outputModule && type === "script-module" && typeAttr !== 0) {
  896. // Chunk is a classic IIFE; drop `type="module"` so the
  897. // browser doesn't load it under module semantics.
  898. let attrEnd;
  899. if (valueStart === -1) {
  900. attrEnd = path.attributeNameEnd(typeAttr);
  901. } else if (input[valueEnd] === '"' || input[valueEnd] === "'") {
  902. attrEnd = valueEnd + 1;
  903. } else {
  904. attrEnd = valueEnd;
  905. }
  906. let attrStart = path.attributeNameStart(typeAttr);
  907. if (
  908. attrStart > 0 &&
  909. isAsciiWhitespace(input.charCodeAt(attrStart - 1))
  910. ) {
  911. attrStart -= 1;
  912. }
  913. module.addPresentationalDependency(
  914. new ConstDependency("", [attrStart, attrEnd])
  915. );
  916. }
  917. };
  918. /**
  919. * @param {string} mime mime type
  920. * @param {string} text inline text
  921. * @returns {string} a `data:` request for the text
  922. */
  923. const dataUri = (mime, text) =>
  924. `data:${mime};base64,${Buffer.from(text, "utf8").toString("base64")}`;
  925. /**
  926. * @param {ConstDependency | HtmlSourceDependency | HtmlEntryDependency | HtmlInlineStyleDependency | HtmlInlineScriptDependency | HtmlInlineHtmlDependency} dep dependency
  927. * @param {number} start raw start offset
  928. * @param {number} end raw end offset
  929. */
  930. const setLoc = (dep, start, end) => {
  931. const s = locConverter.get(start);
  932. const e = locConverter.get(end);
  933. dep.setLoc(s.line, s.column, e.line, e.column);
  934. };
  935. /**
  936. * Classifies the first `<base href>` seen in the walk, setting
  937. * `documentBase`/`baseDir`/`baseIsExternal`/`baseUrlPrefix` for the URLs
  938. * that follow it.
  939. * @param {import("./syntax").HtmlPath} path walk path (for attribute reads)
  940. * @param {import("./syntax").HtmlAttributeRef} hrefAttr the base's href attribute
  941. */
  942. const resolveDocumentBase = (path, hrefAttr) => {
  943. documentBase = decodeEntities(path.attributeValue(hrefAttr), true).trim();
  944. if (!documentBase) return;
  945. if (
  946. ABSOLUTE_URL_SCHEME_REGEXP.test(documentBase) ||
  947. documentBase.charCodeAt(0) === CC_SLASH
  948. ) {
  949. baseIsExternal = true;
  950. return;
  951. }
  952. // Normalize the base path into its descending directory segments,
  953. // resolving `.`/`..` like the URL spec. The final segment is the
  954. // referenced file (dropped) unless the href ends with `/` or a
  955. // dot-segment; `up` counts `..`s that climb above the document dir.
  956. const parts = documentBase.split("/");
  957. /** @type {string[]} */
  958. const dirs = [];
  959. let up = 0;
  960. for (let i = 0; i < parts.length; i++) {
  961. const part = parts[i];
  962. if (part === "" || part === ".") continue;
  963. if (part === "..") {
  964. if (dirs.length > 0) dirs.pop();
  965. else up++;
  966. continue;
  967. }
  968. // A trailing plain segment (no following `/`) is the file, not a dir.
  969. if (i === parts.length - 1) continue;
  970. dirs.push(part);
  971. }
  972. baseDir = `${"../".repeat(up)}${dirs.length > 0 ? `${dirs.join("/")}/` : ""}`;
  973. // A base above the document dir can't be cancelled with `../` (the
  974. // document's own dir name is unknown) — leave the output URLs
  975. // un-prefixed; they still resolve under an absolute publicPath.
  976. if (up === 0 && dirs.length > 0) {
  977. baseUrlPrefix = "../".repeat(dirs.length);
  978. }
  979. };
  980. // Lazy decoded-attribute map for the current element — parse-scoped
  981. // to avoid a closure and memo slot per element.
  982. /** @type {import("./syntax").HtmlPath | undefined} */
  983. let attrMapPath;
  984. let attrMapCount = 0;
  985. /** @type {Map<string, string> | undefined} */
  986. let currentAttributesMap;
  987. const getAttributesMap = () => {
  988. if (currentAttributesMap) return currentAttributesMap;
  989. currentAttributesMap = new Map();
  990. const path = /** @type {import("./syntax").HtmlPath} */ (attrMapPath);
  991. for (let i = 0; i < attrMapCount; i++) {
  992. const attr = path.attributeAt(i);
  993. // Decoded values — filters and type resolvers compare what
  994. // the browser sees (e.g. `rel="&#105;con"` means `icon`)
  995. currentAttributesMap.set(
  996. path.attributeName(attr),
  997. decodeEntities(path.attributeValue(attr), true)
  998. );
  999. }
  1000. return currentAttributesMap;
  1001. };
  1002. // TODO implement full HTML parser (WASM)
  1003. // The walker descends into children (and `<template>` content) itself;
  1004. // the Element `exit` clears a pending `webpackIgnore` once an element's
  1005. // children are done (the old `walkChildren` behaviour).
  1006. new SourceProcessor()
  1007. .use({
  1008. [NodeType.Comment]: (path) => {
  1009. const start = path.start();
  1010. const end = path.end();
  1011. // comments count toward the head-end insertion anchor
  1012. if (inHead && headTemplateDepth === 0 && end > headLastEnd) {
  1013. headLastEnd = end;
  1014. }
  1015. // Only proper `<!-- ... -->` comments carry magic comments.
  1016. if (
  1017. end - start < 7 ||
  1018. source.charCodeAt(start) !== 0x3c ||
  1019. source.charCodeAt(start + 1) !== 0x21 ||
  1020. source.charCodeAt(start + 2) !== 0x2d ||
  1021. source.charCodeAt(start + 3) !== 0x2d ||
  1022. source.charCodeAt(end - 1) !== 0x3e ||
  1023. source.charCodeAt(end - 2) !== 0x2d ||
  1024. source.charCodeAt(end - 3) !== 0x2d
  1025. ) {
  1026. pendingWebpackIgnore = undefined;
  1027. pendingWebpackInline = undefined;
  1028. pendingHints = undefined;
  1029. return;
  1030. }
  1031. const value = path.data();
  1032. if (!webpackCommentRegExp.test(value)) {
  1033. pendingWebpackIgnore = undefined;
  1034. pendingWebpackInline = undefined;
  1035. pendingHints = undefined;
  1036. return;
  1037. }
  1038. /** @type {Record<string, EXPECTED_ANY>} */
  1039. let options;
  1040. try {
  1041. options = parseMagicComment(value, magicCommentContext);
  1042. } catch (err) {
  1043. const { line: sl, column: sc } = locConverter.get(start);
  1044. const { line: el, column: ec } = locConverter.get(end);
  1045. module.addWarning(
  1046. new (getCommentCompilationWarning())(
  1047. `Compilation error while processing magic comment(-s): /*${value}*/: ${
  1048. /** @type {Error} */ (err).message
  1049. }`,
  1050. {
  1051. start: { line: sl, column: sc },
  1052. end: { line: el, column: ec }
  1053. }
  1054. )
  1055. );
  1056. pendingWebpackIgnore = undefined;
  1057. pendingWebpackInline = undefined;
  1058. pendingHints = undefined;
  1059. return;
  1060. }
  1061. if (
  1062. options.webpackIgnore === undefined &&
  1063. options.webpackInline === undefined &&
  1064. options.webpackPrefetch === undefined &&
  1065. options.webpackPreload === undefined &&
  1066. options.webpackFetchPriority === undefined
  1067. ) {
  1068. pendingWebpackIgnore = undefined;
  1069. pendingWebpackInline = undefined;
  1070. pendingHints = undefined;
  1071. return;
  1072. }
  1073. if (
  1074. options.webpackIgnore !== undefined &&
  1075. typeof options.webpackIgnore !== "boolean"
  1076. ) {
  1077. const { line: sl, column: sc } = locConverter.get(start);
  1078. const { line: el, column: ec } = locConverter.get(end);
  1079. module.addWarning(
  1080. new (getUnsupportedFeatureWarning())(
  1081. `\`webpackIgnore\` expected a boolean, but received: ${options.webpackIgnore}.`,
  1082. {
  1083. start: { line: sl, column: sc },
  1084. end: { line: el, column: ec }
  1085. }
  1086. )
  1087. );
  1088. pendingWebpackIgnore = undefined;
  1089. pendingWebpackInline = undefined;
  1090. pendingHints = undefined;
  1091. return;
  1092. }
  1093. if (
  1094. options.webpackInline !== undefined &&
  1095. typeof options.webpackInline !== "boolean"
  1096. ) {
  1097. const { line: sl, column: sc } = locConverter.get(start);
  1098. const { line: el, column: ec } = locConverter.get(end);
  1099. module.addWarning(
  1100. new (getUnsupportedFeatureWarning())(
  1101. `\`webpackInline\` expected a boolean, but received: ${options.webpackInline}.`,
  1102. {
  1103. start: { line: sl, column: sc },
  1104. end: { line: el, column: ec }
  1105. }
  1106. )
  1107. );
  1108. pendingWebpackIgnore = undefined;
  1109. pendingWebpackInline = undefined;
  1110. pendingHints = undefined;
  1111. return;
  1112. }
  1113. pendingWebpackIgnore = options.webpackIgnore;
  1114. pendingWebpackInline = options.webpackInline;
  1115. if (
  1116. options.webpackPrefetch !== undefined ||
  1117. options.webpackPreload !== undefined ||
  1118. options.webpackFetchPriority !== undefined
  1119. ) {
  1120. const s = locConverter.get(start);
  1121. const e = locConverter.get(end);
  1122. const parsedHints = parseResourceHintOptions(options, module, {
  1123. start: { line: s.line, column: s.column },
  1124. end: { line: e.line, column: e.column }
  1125. });
  1126. pendingHints =
  1127. parsedHints.prefetch !== undefined ||
  1128. parsedHints.preload !== undefined ||
  1129. parsedHints.fetchPriority !== undefined
  1130. ? parsedHints
  1131. : undefined;
  1132. } else {
  1133. pendingHints = undefined;
  1134. }
  1135. },
  1136. [NodeType.Doctype]: (path) => {
  1137. // only visited when head injection is active (see `skip` below)
  1138. if (doctypeEnd === -1) doctypeEnd = path.end();
  1139. pendingWebpackIgnore = undefined;
  1140. pendingWebpackInline = undefined;
  1141. pendingHints = undefined;
  1142. },
  1143. [NodeType.Element]: {
  1144. // Children (and `<template>` content) are walked by the
  1145. // processor after `enter`; `exit` clears a pending
  1146. // `webpackIgnore` once they're done.
  1147. enter: (path) => {
  1148. const ignore = pendingWebpackIgnore === true;
  1149. const forceInline = pendingWebpackInline === true;
  1150. pendingWebpackIgnore = undefined;
  1151. pendingWebpackInline = undefined;
  1152. // `<head>` anchors are collected for every page — they place
  1153. // `inject: "head"` siblings and resource hints; the
  1154. // title/meta/base facts only when configured. Structural, so it
  1155. // runs even for `webpackIgnore`d elements.
  1156. {
  1157. const name = path.tagName();
  1158. if (name === "template" && path.namespace() === NS_HTML) {
  1159. templateDepth++;
  1160. }
  1161. if (headTemplateDepth > 0) {
  1162. if (name === "template" && path.namespace() === NS_HTML) {
  1163. headTemplateDepth++;
  1164. }
  1165. } else if (inHead) {
  1166. if (headOptions !== undefined && path.namespace() === NS_HTML) {
  1167. switch (name) {
  1168. case "title":
  1169. headHasTitle = true;
  1170. break;
  1171. case "base":
  1172. headHasBase = true;
  1173. break;
  1174. case "meta": {
  1175. if (path.findAttribute("charset") !== 0) {
  1176. headCharsetEnd = path.tagEnd();
  1177. } else {
  1178. const httpEquiv = path.findAttribute("http-equiv");
  1179. const contentAttr =
  1180. httpEquiv !== 0 &&
  1181. decodeEntities(
  1182. path.attributeValue(httpEquiv),
  1183. true
  1184. ).toLowerCase() === "content-type"
  1185. ? path.findAttribute("content")
  1186. : 0;
  1187. if (
  1188. contentAttr !== 0 &&
  1189. CHARSET_DECLARATION_REGEXP.test(
  1190. decodeEntities(
  1191. path.attributeValue(contentAttr),
  1192. true
  1193. )
  1194. )
  1195. ) {
  1196. headCharsetEnd = path.tagEnd();
  1197. }
  1198. }
  1199. const nameAttr = path.findAttribute("name");
  1200. const propertyAttr = path.findAttribute("property");
  1201. if (nameAttr !== 0 || propertyAttr !== 0) {
  1202. if (headMetaNames === undefined) {
  1203. headMetaNames = new Set();
  1204. }
  1205. if (nameAttr !== 0) {
  1206. headMetaNames.add(
  1207. decodeEntities(
  1208. path.attributeValue(nameAttr),
  1209. true
  1210. ).toLowerCase()
  1211. );
  1212. }
  1213. if (propertyAttr !== 0) {
  1214. headMetaNames.add(
  1215. decodeEntities(
  1216. path.attributeValue(propertyAttr),
  1217. true
  1218. ).toLowerCase()
  1219. );
  1220. }
  1221. }
  1222. break;
  1223. }
  1224. case "template":
  1225. headTemplateDepth++;
  1226. break;
  1227. }
  1228. } else if (
  1229. name === "template" &&
  1230. path.namespace() === NS_HTML
  1231. ) {
  1232. headTemplateDepth++;
  1233. }
  1234. if (path.end() > headLastEnd) headLastEnd = path.end();
  1235. } else if (!headSeen && path.namespace() === NS_HTML) {
  1236. if (name === "html") {
  1237. htmlTagEnd = path.tagEnd();
  1238. } else if (name === "head") {
  1239. headSeen = true;
  1240. inHead = true;
  1241. // explicit head has real offsets; an implicit one (head
  1242. // tags are optional in HTML5) anchors after `<html>`,
  1243. // the doctype, or the document start
  1244. const tagEnd = path.tagEnd();
  1245. headStart =
  1246. tagEnd > 0
  1247. ? tagEnd
  1248. : htmlTagEnd > 0
  1249. ? htmlTagEnd
  1250. : doctypeEnd > 0
  1251. ? doctypeEnd
  1252. : 0;
  1253. headLastEnd = headStart;
  1254. }
  1255. }
  1256. }
  1257. if (ignore) {
  1258. return;
  1259. }
  1260. const elementName = path.tagName();
  1261. const attributeCount = path.attributeCount();
  1262. const elementStart = path.start();
  1263. const tagEnd = path.tagEnd();
  1264. const nameEnd = path.nameEnd();
  1265. // The first `<base href>` freezes the base for later URLs — but
  1266. // only one in the real document tree, not inside an inert
  1267. // `<template>` (whose contents are a separate fragment).
  1268. if (
  1269. documentBase === undefined &&
  1270. templateDepth === 0 &&
  1271. elementName === "base" &&
  1272. path.namespace() === NS_HTML
  1273. ) {
  1274. const hrefAttr = path.findAttribute("href");
  1275. if (hrefAttr !== 0) resolveDocumentBase(path, hrefAttr);
  1276. }
  1277. // Rebind the parse-scoped lazy attribute map to this element.
  1278. attrMapPath = path;
  1279. attrMapCount = attributeCount;
  1280. currentAttributesMap = undefined;
  1281. // Each matched attribute is a source; the element body (inline
  1282. // `<style>`/`<script>`) is one more — content rather than an attribute.
  1283. // All are dispatched by the same `switch (type)`.
  1284. const sources =
  1285. this.sourcesByTag[elementName] || this.sourcesByTag[ANY_TAG];
  1286. const bodyItem = CONTENT_SOURCES[elementName];
  1287. // Iterate the attributes, then one extra step for the element
  1288. // body (`i === attributeCount`); the attribute ref is only read on the
  1289. // attribute steps, so it stays in bounds.
  1290. for (let i = 0; i < attributeCount + 1; i++) {
  1291. const content = i === attributeCount;
  1292. /** @type {SourceType} */
  1293. let type;
  1294. /** @type {string} */
  1295. let value;
  1296. /** @type {number} */
  1297. let start;
  1298. /** @type {number} */
  1299. let end;
  1300. /** @type {import("./syntax").HtmlAttributeRef} */
  1301. let attr = 0;
  1302. if (content) {
  1303. if (!bodyItem) continue;
  1304. if (bodyItem.filter && !bodyItem.filter(getAttributesMap())) {
  1305. continue;
  1306. }
  1307. // `skip.text` drops the body `Text` node; the raw content
  1308. // span is [`tagEnd`, `contentEnd`] on the element.
  1309. const bodyStart = tagEnd;
  1310. const bodyEnd = path.contentEnd();
  1311. if (bodyEnd <= bodyStart) continue;
  1312. value = source.slice(bodyStart, bodyEnd);
  1313. if (value.trim() === "") continue;
  1314. start = bodyStart;
  1315. end = bodyEnd;
  1316. type =
  1317. typeof bodyItem.type === "function"
  1318. ? bodyItem.type(getAttributesMap(), css)
  1319. : bodyItem.type;
  1320. } else {
  1321. attr = path.attributeAt(i);
  1322. const item = sources[path.attributeName(attr)];
  1323. if (!item) continue;
  1324. if (
  1325. item.namespace !== undefined &&
  1326. path.namespace() !== item.namespace
  1327. ) {
  1328. continue;
  1329. }
  1330. // Adoption-agency clones carry no offsets; skip blank values.
  1331. if (path.attributeValueStart(attr) === -1) {
  1332. continue;
  1333. }
  1334. value = path.attributeValue(attr);
  1335. if (!value || !/\S/.test(value)) continue;
  1336. const filter = item.filter;
  1337. if (filter) {
  1338. // The filter also sees the decoded value (the browser tokenizes it
  1339. // decoded, e.g. `&#117;rl(`), so a value-only check needn't re-read it.
  1340. const decoded = value.includes("&")
  1341. ? decodeEntities(value, true)
  1342. : value;
  1343. if (!filter(getAttributesMap(), decoded)) continue;
  1344. }
  1345. start = path.attributeValueStart(attr);
  1346. end = path.attributeValueEnd(attr);
  1347. type =
  1348. typeof item.type === "function"
  1349. ? item.type(getAttributesMap(), css)
  1350. : item.type;
  1351. }
  1352. // A resolved `type` selects the parse algorithm and the dependency
  1353. // kind — the only dispatch in the parser.
  1354. switch (type) {
  1355. // Inline CSS — a `<style>` body (raw, a full stylesheet) or an
  1356. // attribute value (decoded, re-escaped on write-back; `*-attribute`
  1357. // is a declaration list). Needs `experiments.css`.
  1358. case "stylesheet-style":
  1359. case "stylesheet-style-attribute": {
  1360. if (!css) break;
  1361. const cssText = content ? value : decodeEntities(value, true);
  1362. if (cssText.trim() === "") break;
  1363. const dep = new HtmlInlineStyleDependency(
  1364. dataUri("text/css", cssText),
  1365. [start, end],
  1366. content ? false : type === "stylesheet-style-attribute",
  1367. !content
  1368. );
  1369. setLoc(dep, start, end);
  1370. module.addDependency(dep);
  1371. module.addCodeGenerationDependency(dep);
  1372. break;
  1373. }
  1374. // `<iframe srcdoc>`: an entity-encoded HTML document. Feed the
  1375. // decoded markup back through the HTML pipeline as a nested
  1376. // `data:text/html` module. Per spec the value is a full document
  1377. // whose URL is `about:srcdoc`, so its base URL is inherited from
  1378. // this document — asset URLs resolve against this file's context.
  1379. case "srcdoc": {
  1380. const htmlText = decodeEntities(value, true);
  1381. // Only spin up a nested module when there is markup that can
  1382. // reference an asset; text/formatting-only markup is identical
  1383. // after rewriting (see `SRCDOC_ASSET_REGEXP`).
  1384. if (
  1385. !htmlText.includes("<") ||
  1386. !SRCDOC_ASSET_REGEXP.test(htmlText)
  1387. ) {
  1388. break;
  1389. }
  1390. const dep = new HtmlInlineHtmlDependency(
  1391. dataUri("text/html", htmlText),
  1392. [start, end]
  1393. );
  1394. setLoc(dep, start, end);
  1395. module.addDependency(dep);
  1396. module.addCodeGenerationDependency(dep);
  1397. break;
  1398. }
  1399. // Inline `<script>` body — bundled as its own entry chunk.
  1400. case "script":
  1401. case "script-module":
  1402. if (content) {
  1403. const scriptType =
  1404. /** @type {"script" | "script-module"} */ (type);
  1405. const request = dataUri("text/javascript", value);
  1406. const entryName = `__html_${moduleHash}_${nextEntryIndex++}`;
  1407. const dep = new HtmlInlineScriptDependency(
  1408. request,
  1409. nameEnd,
  1410. [start, end],
  1411. entryName,
  1412. scriptType === "script-module" ? "esm" : "commonjs"
  1413. );
  1414. setLoc(dep, start, end);
  1415. module.addPresentationalDependency(dep);
  1416. reconcileScriptTypeAttr(
  1417. path,
  1418. path.findAttribute("type"),
  1419. nameEnd,
  1420. scriptType,
  1421. source
  1422. );
  1423. (scriptType === "script"
  1424. ? scriptEntries
  1425. : scriptModuleEntries
  1426. ).push({
  1427. request,
  1428. entryName,
  1429. type: scriptType
  1430. });
  1431. break;
  1432. }
  1433. // falls through — external `<script src>` joins the URL-bearing types
  1434. case "src":
  1435. case "srcset":
  1436. case "css-url":
  1437. case "msapplication-task":
  1438. case "modulepreload":
  1439. case "stylesheet":
  1440. case "html":
  1441. case "preload":
  1442. case "prefetch": {
  1443. // Parse the value into one or more URLs (decoding character
  1444. // references, mapping spans back to raw offsets), then emit a plain
  1445. // asset reference or an entry chunk per URL.
  1446. const parse =
  1447. type === "srcset"
  1448. ? parseSrcset
  1449. : type === "css-url"
  1450. ? parseCssUrls
  1451. : type === "msapplication-task"
  1452. ? parseMsapplicationTask
  1453. : parseSrc;
  1454. let text = value;
  1455. /** @type {number[] | undefined} */
  1456. let map;
  1457. if (value.includes("&")) {
  1458. ({ text, map } = decodeEntities(value, true, true));
  1459. if (!/\S/.test(text)) break;
  1460. }
  1461. /** @type {ParsedSource[] | undefined} */
  1462. let parsed;
  1463. try {
  1464. parsed = parse(text);
  1465. } catch (err) {
  1466. const ds = locConverter.get(start);
  1467. const de = locConverter.get(end);
  1468. module.addError(
  1469. new (getModuleDependencyError())(
  1470. module,
  1471. new WebpackError(
  1472. `Bad value for attribute "${path.attributeName(
  1473. attr
  1474. )}" on element "${elementName}": ${
  1475. /** @type {Error} */ (err).message
  1476. }`
  1477. ),
  1478. {
  1479. start: { line: ds.line, column: ds.column },
  1480. end: { line: de.line, column: de.column }
  1481. }
  1482. )
  1483. );
  1484. break;
  1485. }
  1486. if (!parsed) break;
  1487. for (const [url, us, ue] of parsed) {
  1488. // Internal `url(#id)` / fragment-only refs aren't assets.
  1489. if (!url || url.startsWith("#")) continue;
  1490. // Resolve the request against `<base href>`. Only relative
  1491. // URLs are affected (scheme / `//` / `/` URLs ignore the
  1492. // base); an external base drops them from the build.
  1493. let request = url;
  1494. if (
  1495. documentBase &&
  1496. url.charCodeAt(0) !== CC_SLASH &&
  1497. !ABSOLUTE_URL_SCHEME_REGEXP.test(url)
  1498. ) {
  1499. if (baseIsExternal) continue;
  1500. request = baseDir + url;
  1501. }
  1502. const s = start + (map ? map[us] : us);
  1503. const e = start + (map ? map[ue] : ue);
  1504. // `html` links the URL as its own emitted page: the same
  1505. // `HtmlEntryDependency` as `<script src>` / `<link>`, but the `html`
  1506. // kind rewrites to the page's filename and emits no sibling tags.
  1507. // Recorded as an independent entry (no `dependOn`), like an entry point.
  1508. if (type === "html") {
  1509. const entryName = `__html_${moduleHash}_${nextEntryIndex++}`;
  1510. const dep = new HtmlEntryDependency(
  1511. request,
  1512. [s, e],
  1513. entryName,
  1514. undefined,
  1515. "html"
  1516. );
  1517. setLoc(dep, s, e);
  1518. module.addPresentationalDependency(dep);
  1519. htmlLinkEntries.push({ request, entryName, type });
  1520. continue;
  1521. }
  1522. // `rel="preload"/"prefetch"` of a bundled resource: an
  1523. // independent, non-executing entry whose `href` is rewritten to
  1524. // the built chunk's URL (CSS for `as="style"`, JS otherwise). No
  1525. // sibling tags — a resource hint references exactly one URL.
  1526. if (type === "preload" || type === "prefetch") {
  1527. const as = getAttributeValue(getAttributesMap(), "as");
  1528. const asStyle = as
  1529. ? as.trim().toLowerCase() === "style"
  1530. : false;
  1531. const entryName = `__html_${moduleHash}_${nextEntryIndex++}`;
  1532. // Tag metadata so the template can mirror `crossorigin`/
  1533. // `integrity` onto a native `preload` `<link>` (integrity-
  1534. // eligible per the SRI spec). `prefetch` gets none — the spec
  1535. // doesn't list it — so its `integrity` is left uncaptured.
  1536. const isNativeLink = elementName === "link";
  1537. const dep = new HtmlEntryDependency(
  1538. request,
  1539. [s, e],
  1540. entryName,
  1541. asStyle ? "css-import" : undefined,
  1542. type,
  1543. elementStart,
  1544. tagEnd,
  1545. isNativeLink,
  1546. "",
  1547. nameEnd,
  1548. isNativeLink && path.findAttribute("crossorigin") !== 0,
  1549. -1,
  1550. type === "preload" && isNativeLink
  1551. ? captureIntegrityRange(path, source, elementStart)
  1552. : null,
  1553. null
  1554. );
  1555. setLoc(dep, s, e);
  1556. module.addPresentationalDependency(dep);
  1557. (type === "preload"
  1558. ? preloadEntries
  1559. : prefetchEntries
  1560. ).push({
  1561. request,
  1562. entryName,
  1563. type,
  1564. css: asStyle
  1565. });
  1566. continue;
  1567. }
  1568. // `src`/`srcset`/`css-url`/`msapplication-task` are plain assets;
  1569. // the rest are narrowed to entry types here and share one
  1570. // `HtmlEntryDependency`, recorded for HtmlModulesPlugin.
  1571. if (
  1572. type === "script" ||
  1573. type === "script-module" ||
  1574. type === "modulepreload" ||
  1575. type === "stylesheet"
  1576. ) {
  1577. const entryName = `__html_${moduleHash}_${nextEntryIndex++}`;
  1578. const willBeModuleScript =
  1579. type === "script-module" ||
  1580. (outputModule && type === "script");
  1581. /** @type {EntrySourceType} */
  1582. const elementKind =
  1583. type === "modulepreload"
  1584. ? "modulepreload"
  1585. : type === "stylesheet"
  1586. ? "stylesheet"
  1587. : willBeModuleScript
  1588. ? "script-module"
  1589. : "script";
  1590. const entryCategory =
  1591. type === "stylesheet"
  1592. ? "css-import"
  1593. : type === "script-module" || type === "modulepreload"
  1594. ? "esm"
  1595. : undefined;
  1596. // Native = the loading element is the tag the template clones
  1597. // verbatim (`<script>` / `<link>`); a custom element is not.
  1598. const nativeTag =
  1599. elementKind === "stylesheet" ||
  1600. elementKind === "modulepreload"
  1601. ? "link"
  1602. : "script";
  1603. // CSP/fetch attributes the template copies onto siblings.
  1604. let copyableAttrsText = "";
  1605. let hasOwnCrossOrigin = false;
  1606. if (elementStart >= 0) {
  1607. for (const copyableName of COPYABLE_SIBLING_ATTRS) {
  1608. const copyableAttr = path.findAttribute(copyableName);
  1609. if (copyableAttr !== 0) {
  1610. copyableAttrsText += attrSourceSpan(
  1611. path,
  1612. source,
  1613. copyableAttr
  1614. );
  1615. if (copyableName === "crossorigin") {
  1616. hasOwnCrossOrigin = true;
  1617. }
  1618. }
  1619. }
  1620. }
  1621. // Blocking-ness of the tag as *emitted*:
  1622. // `reconcileScriptTypeAttr` upgrades every script entry
  1623. // to `type="module"` under `output.module` and strips an
  1624. // authored `type="module"` otherwise — so only non-module
  1625. // output without `defer`/`async` blocks. Non-native
  1626. // elements anchor conservatively (their synthesized
  1627. // siblings may be plain blocking `<script>`s).
  1628. if (
  1629. firstBlockingScriptStart === -1 &&
  1630. (type === "script" || type === "script-module") &&
  1631. !outputModule &&
  1632. (elementName !== nativeTag ||
  1633. (path.findAttribute("defer") === 0 &&
  1634. path.findAttribute("async") === 0))
  1635. ) {
  1636. firstBlockingScriptStart = elementStart;
  1637. }
  1638. // Sibling-clone attribute edits, captured from the parsed
  1639. // attributes (offsets relative to the tag) so the template
  1640. // needn't re-parse the tag text: `integrity` is dropped
  1641. // (content-specific) and `script-module` clones are forced
  1642. // to `type="module"` (its value range, else inserted).
  1643. const integrityRange =
  1644. elementStart >= 0 && elementName === nativeTag
  1645. ? captureIntegrityRange(path, source, elementStart)
  1646. : null;
  1647. /** @type {Range | null} */
  1648. let typeValueRange = null;
  1649. if (
  1650. elementStart >= 0 &&
  1651. elementName === nativeTag &&
  1652. elementKind === "script-module"
  1653. ) {
  1654. const typeAttr = path.findAttribute("type");
  1655. if (
  1656. typeAttr !== 0 &&
  1657. path.attributeValueStart(typeAttr) >= 0
  1658. ) {
  1659. typeValueRange = [
  1660. path.attributeValueStart(typeAttr) - elementStart,
  1661. path.attributeValueEnd(typeAttr) - elementStart
  1662. ];
  1663. }
  1664. }
  1665. const dep = new HtmlEntryDependency(
  1666. request,
  1667. [s, e],
  1668. entryName,
  1669. entryCategory,
  1670. elementKind,
  1671. elementStart,
  1672. tagEnd,
  1673. elementName === nativeTag,
  1674. copyableAttrsText,
  1675. nameEnd,
  1676. hasOwnCrossOrigin,
  1677. firstBlockingScriptStart,
  1678. integrityRange,
  1679. typeValueRange,
  1680. forceInline,
  1681. headSeen && headStart > 0 ? headStart : -1,
  1682. headSeen && headLastEnd > 0 ? headLastEnd : -1
  1683. );
  1684. setLoc(dep, s, e);
  1685. module.addPresentationalDependency(dep);
  1686. if (
  1687. elementName === "script" &&
  1688. (type === "script" || type === "script-module")
  1689. ) {
  1690. reconcileScriptTypeAttr(
  1691. path,
  1692. path.findAttribute("type"),
  1693. nameEnd,
  1694. type,
  1695. source
  1696. );
  1697. }
  1698. (type === "script"
  1699. ? scriptEntries
  1700. : type === "script-module"
  1701. ? scriptModuleEntries
  1702. : type === "stylesheet"
  1703. ? stylesheetEntries
  1704. : modulePreloadEntries
  1705. ).push({ request, entryName, type });
  1706. } else {
  1707. const dep = new HtmlSourceDependency(request, [s, e]);
  1708. ResourceHintPlugin.applyDefaults(
  1709. dep,
  1710. ResourceHintPlugin.matchUrlHints(urlHints, request)
  1711. );
  1712. // `<!-- webpackPrefetch: true -->` (etc.) immediately
  1713. // before this tag wins over the project-wide default.
  1714. if (pendingHints) {
  1715. ResourceHintPlugin.applyParsedHints(dep, pendingHints);
  1716. pendingHints = undefined;
  1717. }
  1718. setLoc(dep, s, e);
  1719. module.addDependency(dep);
  1720. module.addCodeGenerationDependency(dep);
  1721. }
  1722. }
  1723. break;
  1724. }
  1725. }
  1726. }
  1727. },
  1728. exit: (path) => {
  1729. if (path.namespace() === NS_HTML) {
  1730. const name = path.tagName();
  1731. if (name === "template") {
  1732. if (templateDepth > 0) templateDepth--;
  1733. if (headTemplateDepth > 0) headTemplateDepth--;
  1734. } else if (name === "head") {
  1735. inHead = false;
  1736. }
  1737. }
  1738. pendingWebpackIgnore = undefined;
  1739. pendingWebpackInline = undefined;
  1740. pendingHints = undefined;
  1741. }
  1742. }
  1743. })
  1744. // Skip AST output this walk never reads: `text` (script/style bodies are
  1745. // read by offset via `contentEnd`) and `doctype` — unless head injection
  1746. // needs the doctype as an insertion anchor; keep `comments` for
  1747. // magic comments (`webpackIgnore`).
  1748. .process(source, {
  1749. fragmentContext: this.fragmentContext,
  1750. skip: { text: true, doctype: headOptions === undefined }
  1751. });
  1752. // `output.html` head injection: existing tags win, injected values are
  1753. // escaped, insertions at the same offset are merged to keep their order.
  1754. if (headOptions !== undefined && headSeen) {
  1755. const { title, meta, base } = headOptions;
  1756. let startTags = "";
  1757. if (meta && meta.charset && headCharsetEnd === -1) {
  1758. startTags += `<meta charset="${escapeAttribute(meta.charset)}">`;
  1759. }
  1760. // <base> must follow the charset declaration per spec
  1761. let baseAfterCharset = "";
  1762. if (base && !headHasBase) {
  1763. if (headCharsetEnd !== -1) baseAfterCharset = baseTag(base);
  1764. else startTags += baseTag(base);
  1765. }
  1766. let endTags = "";
  1767. if (meta) {
  1768. for (const [name, value] of Object.entries(meta)) {
  1769. if (
  1770. name === "charset" ||
  1771. (headMetaNames !== undefined &&
  1772. headMetaNames.has(name.toLowerCase()))
  1773. ) {
  1774. continue;
  1775. }
  1776. endTags += metaTag(name, value);
  1777. }
  1778. }
  1779. if (title && !headHasTitle) {
  1780. endTags += `<title>${escapeText(title)}</title>`;
  1781. }
  1782. /** @type {[number, string][]} */
  1783. const inserts = [];
  1784. if (startTags) inserts.push([headStart, startTags]);
  1785. if (baseAfterCharset) inserts.push([headCharsetEnd, baseAfterCharset]);
  1786. if (endTags) inserts.push([headLastEnd, endTags]);
  1787. inserts.sort((a, b) => a[0] - b[0]);
  1788. for (let i = 0; i < inserts.length; i++) {
  1789. const at = inserts[i][0];
  1790. let tags = inserts[i][1];
  1791. while (i + 1 < inserts.length && inserts[i + 1][0] === at) {
  1792. tags += inserts[++i][1];
  1793. }
  1794. module.addPresentationalDependency(new ConstDependency(tags, at));
  1795. }
  1796. }
  1797. const buildInfo = /** @type {HtmlModuleBuildInfo} */ (module.buildInfo);
  1798. buildInfo.strict = true;
  1799. if (baseUrlPrefix !== undefined) buildInfo.baseUrlPrefix = baseUrlPrefix;
  1800. // Hand off the collected entries to HtmlModulesPlugin; it creates the
  1801. // real compilation entries during the finishMake hook. The `script`
  1802. // and `script-module` groups are chained via a leader-only dependOn
  1803. // so they share a runtime; `modulepreload` and `stylesheet` entries
  1804. // are emitted as independent entries since `<link rel=modulepreload>`
  1805. // must preload without running and stylesheets have no runtime. `html`
  1806. // links are independent page entries too.
  1807. if (
  1808. scriptEntries.length > 0 ||
  1809. scriptModuleEntries.length > 0 ||
  1810. modulePreloadEntries.length > 0 ||
  1811. stylesheetEntries.length > 0 ||
  1812. htmlLinkEntries.length > 0 ||
  1813. preloadEntries.length > 0 ||
  1814. prefetchEntries.length > 0
  1815. ) {
  1816. buildInfo.htmlEntries = {
  1817. script: scriptEntries,
  1818. "script-module": scriptModuleEntries,
  1819. modulepreload: modulePreloadEntries,
  1820. stylesheet: stylesheetEntries,
  1821. html: htmlLinkEntries,
  1822. preload: preloadEntries,
  1823. prefetch: prefetchEntries
  1824. };
  1825. }
  1826. const buildMeta = /** @type {BuildMeta} */ (state.module.buildMeta);
  1827. buildMeta.exportsType = "default";
  1828. state.module.addDependency(new StaticExportsDependency(["default"], true));
  1829. return state;
  1830. }
  1831. }
  1832. HtmlParser.buildHeadTags = buildHeadTags;
  1833. module.exports = HtmlParser;