| 1 | /*
|
|---|
| 2 | MIT License http://www.opensource.org/licenses/mit-license.php
|
|---|
| 3 | */
|
|---|
| 4 |
|
|---|
| 5 | "use strict";
|
|---|
| 6 |
|
|---|
| 7 | const vm = require("vm");
|
|---|
| 8 | const Parser = require("../Parser");
|
|---|
| 9 | const ConstDependency = require("../dependencies/ConstDependency");
|
|---|
| 10 | const HtmlInlineScriptDependency = require("../dependencies/HtmlInlineScriptDependency");
|
|---|
| 11 | const HtmlInlineStyleDependency = require("../dependencies/HtmlInlineStyleDependency");
|
|---|
| 12 | const HtmlScriptSrcDependency = require("../dependencies/HtmlScriptSrcDependency");
|
|---|
| 13 | const HtmlSourceDependency = require("../dependencies/HtmlSourceDependency");
|
|---|
| 14 | const StaticExportsDependency = require("../dependencies/StaticExportsDependency");
|
|---|
| 15 | const CommentCompilationWarning = require("../errors/CommentCompilationWarning");
|
|---|
| 16 | const ModuleDependencyError = require("../errors/ModuleDependencyError");
|
|---|
| 17 | const UnsupportedFeatureWarning = require("../errors/UnsupportedFeatureWarning");
|
|---|
| 18 | const WebpackError = require("../errors/WebpackError");
|
|---|
| 19 | const LocConverter = require("../util/LocConverter");
|
|---|
| 20 | const createHash = require("../util/createHash");
|
|---|
| 21 | const { contextify } = require("../util/identifier");
|
|---|
| 22 | const {
|
|---|
| 23 | createMagicCommentContext,
|
|---|
| 24 | webpackCommentRegExp
|
|---|
| 25 | } = require("../util/magicComment");
|
|---|
| 26 | const walkHtmlTokens = require("./walkHtmlTokens");
|
|---|
| 27 |
|
|---|
| 28 | /** @typedef {import("../Module").BuildInfo} BuildInfo */
|
|---|
| 29 | /** @typedef {import("../Module").BuildMeta} BuildMeta */
|
|---|
| 30 | /** @typedef {import("../Parser").ParserState} ParserState */
|
|---|
| 31 | /** @typedef {import("../Parser").PreparsedAst} PreparsedAst */
|
|---|
| 32 |
|
|---|
| 33 | const HORIZONTAL_TAB = "\u0009".charCodeAt(0);
|
|---|
| 34 | const NEWLINE = "\u000A".charCodeAt(0);
|
|---|
| 35 | const FORM_FEED = "\u000C".charCodeAt(0);
|
|---|
| 36 | const CARRIAGE_RETURN = "\u000D".charCodeAt(0);
|
|---|
| 37 | const SPACE = "\u0020".charCodeAt(0);
|
|---|
| 38 | const COMMA = ",".charCodeAt(0);
|
|---|
| 39 | const LEFT_PARENTHESIS = "(".charCodeAt(0);
|
|---|
| 40 | const RIGHT_PARENTHESIS = ")".charCodeAt(0);
|
|---|
| 41 | const SMALL_LETTER_W = "w".charCodeAt(0);
|
|---|
| 42 | const SMALL_LETTER_X = "x".charCodeAt(0);
|
|---|
| 43 | const SMALL_LETTER_H = "h".charCodeAt(0);
|
|---|
| 44 |
|
|---|
| 45 | /**
|
|---|
| 46 | * @param {number} char char
|
|---|
| 47 | * @returns {boolean} true when ASCII whitespace, otherwise false
|
|---|
| 48 | */
|
|---|
| 49 | function isASCIIWhitespace(char) {
|
|---|
| 50 | return (
|
|---|
| 51 | // Horizontal tab
|
|---|
| 52 | char === HORIZONTAL_TAB ||
|
|---|
| 53 | // New line
|
|---|
| 54 | char === NEWLINE ||
|
|---|
| 55 | // Form feed
|
|---|
| 56 | char === FORM_FEED ||
|
|---|
| 57 | // Carriage return
|
|---|
| 58 | char === CARRIAGE_RETURN ||
|
|---|
| 59 | // Space
|
|---|
| 60 | char === SPACE
|
|---|
| 61 | );
|
|---|
| 62 | }
|
|---|
| 63 |
|
|---|
| 64 | /** @typedef {[string, number, number]} ParsedSource */
|
|---|
| 65 |
|
|---|
| 66 | // eslint-disable-next-line no-control-regex
|
|---|
| 67 | const IGNORE_CHARS_REGEXP = /[\u0000-\u001F\u007F-\u009F\u00A0]/g;
|
|---|
| 68 |
|
|---|
| 69 | /**
|
|---|
| 70 | * @param {string} input input
|
|---|
| 71 | * @returns {ParsedSource[]} parsed src
|
|---|
| 72 | */
|
|---|
| 73 | const parseSrc = (input) => {
|
|---|
| 74 | const len = input.length;
|
|---|
| 75 | if (len === 0) throw new Error("Must be non-empty");
|
|---|
| 76 |
|
|---|
| 77 | let start = 0;
|
|---|
| 78 | let end = len;
|
|---|
| 79 |
|
|---|
| 80 | while (start < end) {
|
|---|
| 81 | const code = input.charCodeAt(start);
|
|---|
| 82 | if (code > 32 && code !== 160) break;
|
|---|
| 83 | start++;
|
|---|
| 84 | }
|
|---|
| 85 |
|
|---|
| 86 | if (start === end) throw new Error("Must be non-empty");
|
|---|
| 87 |
|
|---|
| 88 | while (end > start) {
|
|---|
| 89 | const code = input.charCodeAt(end - 1);
|
|---|
| 90 | if (code > 32 && code !== 160) break;
|
|---|
| 91 | end--;
|
|---|
| 92 | }
|
|---|
| 93 |
|
|---|
| 94 | let value = input.slice(start, end);
|
|---|
| 95 |
|
|---|
| 96 | if (IGNORE_CHARS_REGEXP.test(value)) {
|
|---|
| 97 | value = value.replace(IGNORE_CHARS_REGEXP, "");
|
|---|
| 98 | if (value.length === 0) throw new Error("Must be non-empty");
|
|---|
| 99 | }
|
|---|
| 100 |
|
|---|
| 101 | return [[value, start, end]];
|
|---|
| 102 | };
|
|---|
| 103 |
|
|---|
| 104 | // HTML `<style>` content is rawtext: it ends at the first `</style>`
|
|---|
| 105 | // where the tag name is followed by whitespace, `>` or `/`. The
|
|---|
| 106 | // lookahead (rather than a consuming character class) keeps the match
|
|---|
| 107 | // from running past the first `>` into a later tag — the
|
|---|
| 108 | // `[^>]*` only consumes any (rarely-seen) end-tag attributes before the
|
|---|
| 109 | // closing `>` of the end tag itself.
|
|---|
| 110 | const STYLE_END_REGEXP = /<\/style(?=[\s/>])[^>]*>/gi;
|
|---|
| 111 |
|
|---|
| 112 | // (Don't use \s, to avoid matching non-breaking space)
|
|---|
| 113 | // eslint-disable-next-line no-control-regex
|
|---|
| 114 | const LEADING_SPACES_REGEXP = /^[ \t\n\r\u000C]+/;
|
|---|
| 115 | // eslint-disable-next-line no-control-regex
|
|---|
| 116 | const LEADING_COMMAS_OR_SPACES_REGEXP = /^[, \t\n\r\u000C]+/;
|
|---|
| 117 | // eslint-disable-next-line no-control-regex
|
|---|
| 118 | const LEADING_NOT_SPACES = /^[^ \t\n\r\u000C]+/;
|
|---|
| 119 | const TRAILING_COMMAS_REGEXP = /[,]+$/;
|
|---|
| 120 | const NON_NEGATIVE_INTEGER_REGEXP = /^\d+$/;
|
|---|
| 121 | // ( Positive or negative or unsigned integers or decimals, without or without exponents.
|
|---|
| 122 | // Must include at least one digit.
|
|---|
| 123 | // According to spec tests any decimal point must be followed by a digit.
|
|---|
| 124 | // No leading plus sign is allowed.)
|
|---|
| 125 | // https://html.spec.whatwg.org/multipage/infrastructure.html#valid-floating-point-number
|
|---|
| 126 | const FLOATING_POINT_REGEXP =
|
|---|
| 127 | /^-?(?:[0-9]+|[0-9]*\.[0-9]+)(?:[eE][+-]?[0-9]+)?$/;
|
|---|
| 128 |
|
|---|
| 129 | /**
|
|---|
| 130 | * @param {string} input input
|
|---|
| 131 | * @returns {ParsedSource[]} parsed srcset
|
|---|
| 132 | */
|
|---|
| 133 | const parseSrcset = (input) => {
|
|---|
| 134 | // 1. Let input be the value passed to this algorithm.
|
|---|
| 135 | const inputLength = input.length;
|
|---|
| 136 |
|
|---|
| 137 | /** @type {string | undefined} */
|
|---|
| 138 | let url;
|
|---|
| 139 | /** @type {string[]} */
|
|---|
| 140 | let descriptors;
|
|---|
| 141 | /** @type {string} */
|
|---|
| 142 | let currentDescriptor;
|
|---|
| 143 | /** @type {string} */
|
|---|
| 144 | let state;
|
|---|
| 145 | /** @type {number} */
|
|---|
| 146 | let charCode;
|
|---|
| 147 | /** @type {number} */
|
|---|
| 148 | let position = 0;
|
|---|
| 149 | /** @type {number} */
|
|---|
| 150 | let start;
|
|---|
| 151 |
|
|---|
| 152 | /** @type {[string, number, number][]} */
|
|---|
| 153 | const candidates = [];
|
|---|
| 154 |
|
|---|
| 155 | /**
|
|---|
| 156 | * @param {RegExp} regExp reg exp to collect characters
|
|---|
| 157 | * @returns {string | undefined} characters
|
|---|
| 158 | */
|
|---|
| 159 | function collectCharacters(regExp) {
|
|---|
| 160 | /** @type {string} */
|
|---|
| 161 | let chars;
|
|---|
| 162 | const match = regExp.exec(input.slice(Math.max(0, position)));
|
|---|
| 163 |
|
|---|
| 164 | if (match) {
|
|---|
| 165 | [chars] = match;
|
|---|
| 166 | position += chars.length;
|
|---|
| 167 |
|
|---|
| 168 | return chars;
|
|---|
| 169 | }
|
|---|
| 170 | }
|
|---|
| 171 |
|
|---|
| 172 | /**
|
|---|
| 173 | * @returns {void}
|
|---|
| 174 | */
|
|---|
| 175 | function parseDescriptors() {
|
|---|
| 176 | // 9. Descriptor parser: Let error be no.
|
|---|
| 177 | let pError = false;
|
|---|
| 178 |
|
|---|
| 179 | // 10. Let width be absent.
|
|---|
| 180 | // 11. Let density be absent.
|
|---|
| 181 | // 12. Let future-compat-h be absent. (We're implementing it now as h)
|
|---|
| 182 | /** @type {number | undefined} */
|
|---|
| 183 | let width;
|
|---|
| 184 | /** @type {number | undefined} */
|
|---|
| 185 | let density;
|
|---|
| 186 | /** @type {number | undefined} */
|
|---|
| 187 | let height;
|
|---|
| 188 | /** @type {string | undefined} */
|
|---|
| 189 | let desc;
|
|---|
| 190 |
|
|---|
| 191 | // 13. For each descriptor in descriptors, run the appropriate set of steps
|
|---|
| 192 | // from the following list:
|
|---|
| 193 | for (let i = 0; i < descriptors.length; i++) {
|
|---|
| 194 | desc = descriptors[i];
|
|---|
| 195 |
|
|---|
| 196 | const lastChar = desc[desc.length - 1].charCodeAt(0);
|
|---|
| 197 | const value = desc.slice(0, Math.max(0, desc.length - 1));
|
|---|
| 198 |
|
|---|
| 199 | // If the descriptor consists of a valid non-negative integer followed by
|
|---|
| 200 | // a U+0077 LATIN SMALL LETTER W character
|
|---|
| 201 | if (
|
|---|
| 202 | NON_NEGATIVE_INTEGER_REGEXP.test(value) &&
|
|---|
| 203 | lastChar === SMALL_LETTER_W
|
|---|
| 204 | ) {
|
|---|
| 205 | // If width and density are not both absent, then let error be yes.
|
|---|
| 206 | if (width || density) {
|
|---|
| 207 | pError = true;
|
|---|
| 208 | }
|
|---|
| 209 |
|
|---|
| 210 | const intVal = Number.parseInt(value, 10);
|
|---|
| 211 |
|
|---|
| 212 | // Apply the rules for parsing non-negative integers to the descriptor.
|
|---|
| 213 | // If the result is zero, let error be yes.
|
|---|
| 214 | // Otherwise, let width be the result.
|
|---|
| 215 | if (intVal === 0) {
|
|---|
| 216 | pError = true;
|
|---|
| 217 | } else {
|
|---|
| 218 | width = intVal;
|
|---|
| 219 | }
|
|---|
| 220 | }
|
|---|
| 221 | // If the descriptor consists of a valid floating-point number followed by
|
|---|
| 222 | // a U+0078 LATIN SMALL LETTER X character
|
|---|
| 223 | else if (
|
|---|
| 224 | FLOATING_POINT_REGEXP.test(value) &&
|
|---|
| 225 | lastChar === SMALL_LETTER_X
|
|---|
| 226 | ) {
|
|---|
| 227 | // If width, density and future-compat-h are not all absent, then let error
|
|---|
| 228 | // be yes.
|
|---|
| 229 | if (width || density || height) {
|
|---|
| 230 | pError = true;
|
|---|
| 231 | }
|
|---|
| 232 |
|
|---|
| 233 | const floatVal = Number.parseFloat(value);
|
|---|
| 234 |
|
|---|
| 235 | // Apply the rules for parsing floating-point number values to the descriptor.
|
|---|
| 236 | // If the result is less than zero, let error be yes. Otherwise, let density
|
|---|
| 237 | // be the result.
|
|---|
| 238 | if (floatVal < 0) {
|
|---|
| 239 | pError = true;
|
|---|
| 240 | } else {
|
|---|
| 241 | density = floatVal;
|
|---|
| 242 | }
|
|---|
| 243 | }
|
|---|
| 244 | // If the descriptor consists of a valid non-negative integer followed by
|
|---|
| 245 | // a U+0068 LATIN SMALL LETTER H character
|
|---|
| 246 | else if (
|
|---|
| 247 | NON_NEGATIVE_INTEGER_REGEXP.test(value) &&
|
|---|
| 248 | lastChar === SMALL_LETTER_H
|
|---|
| 249 | ) {
|
|---|
| 250 | // If height and density are not both absent, then let error be yes.
|
|---|
| 251 | if (height || density) {
|
|---|
| 252 | pError = true;
|
|---|
| 253 | }
|
|---|
| 254 |
|
|---|
| 255 | const intVal = Number.parseInt(value, 10);
|
|---|
| 256 |
|
|---|
| 257 | // Apply the rules for parsing non-negative integers to the descriptor.
|
|---|
| 258 | // If the result is zero, let error be yes. Otherwise, let future-compat-h
|
|---|
| 259 | // be the result.
|
|---|
| 260 | if (intVal === 0) {
|
|---|
| 261 | pError = true;
|
|---|
| 262 | } else {
|
|---|
| 263 | height = intVal;
|
|---|
| 264 | }
|
|---|
| 265 |
|
|---|
| 266 | // Anything else, Let error be yes.
|
|---|
| 267 | } else {
|
|---|
| 268 | pError = true;
|
|---|
| 269 | }
|
|---|
| 270 | }
|
|---|
| 271 |
|
|---|
| 272 | // 15. If error is still no, then append a new image source to candidates whose
|
|---|
| 273 | // URL is url, associated with a width width if not absent and a pixel
|
|---|
| 274 | // density density if not absent. Otherwise, there is a parse error.
|
|---|
| 275 | if (!pError) {
|
|---|
| 276 | candidates.push([
|
|---|
| 277 | /** @type {string} */ (url),
|
|---|
| 278 | start,
|
|---|
| 279 | start + /** @type {string} */ (url).length
|
|---|
| 280 | ]);
|
|---|
| 281 | } else {
|
|---|
| 282 | throw new Error(
|
|---|
| 283 | `Invalid srcset descriptor found in '${input}' at '${desc}'`
|
|---|
| 284 | );
|
|---|
| 285 | }
|
|---|
| 286 | }
|
|---|
| 287 |
|
|---|
| 288 | /**
|
|---|
| 289 | * @returns {void}
|
|---|
| 290 | */
|
|---|
| 291 | function tokenize() {
|
|---|
| 292 | // 8.1. Descriptor tokenizer: Skip whitespace
|
|---|
| 293 | collectCharacters(LEADING_SPACES_REGEXP);
|
|---|
| 294 |
|
|---|
| 295 | // 8.2. Let current descriptor be the empty string.
|
|---|
| 296 | currentDescriptor = "";
|
|---|
| 297 |
|
|---|
| 298 | // 8.3. Let state be in descriptor.
|
|---|
| 299 | state = "in descriptor";
|
|---|
| 300 |
|
|---|
| 301 | while (true) {
|
|---|
| 302 | // 8.4. Let charCode be the character at position.
|
|---|
| 303 | charCode = input.charCodeAt(position);
|
|---|
| 304 |
|
|---|
| 305 | // Do the following depending on the value of state.
|
|---|
| 306 | // For the purpose of this step, "EOF" is a special character representing
|
|---|
| 307 | // that position is past the end of input.
|
|---|
| 308 |
|
|---|
| 309 | // In descriptor
|
|---|
| 310 | if (state === "in descriptor") {
|
|---|
| 311 | // Do the following, depending on the value of charCode:
|
|---|
| 312 |
|
|---|
| 313 | // Space character
|
|---|
| 314 | // If current descriptor is not empty, append current descriptor to
|
|---|
| 315 | // descriptors and let current descriptor be the empty string.
|
|---|
| 316 | // Set state to after descriptor.
|
|---|
| 317 | if (isASCIIWhitespace(charCode)) {
|
|---|
| 318 | if (currentDescriptor) {
|
|---|
| 319 | descriptors.push(currentDescriptor);
|
|---|
| 320 | currentDescriptor = "";
|
|---|
| 321 | state = "after descriptor";
|
|---|
| 322 | }
|
|---|
| 323 | }
|
|---|
| 324 | // U+002C COMMA (,)
|
|---|
| 325 | // Advance position to the next character in input. If current descriptor
|
|---|
| 326 | // is not empty, append current descriptor to descriptors. Jump to the step
|
|---|
| 327 | // labeled descriptor parser.
|
|---|
| 328 | else if (charCode === COMMA) {
|
|---|
| 329 | position += 1;
|
|---|
| 330 |
|
|---|
| 331 | if (currentDescriptor) {
|
|---|
| 332 | descriptors.push(currentDescriptor);
|
|---|
| 333 | }
|
|---|
| 334 |
|
|---|
| 335 | parseDescriptors();
|
|---|
| 336 |
|
|---|
| 337 | return;
|
|---|
| 338 | }
|
|---|
| 339 | // U+0028 LEFT PARENTHESIS (()
|
|---|
| 340 | // Append charCode to current descriptor. Set state to in parens.
|
|---|
| 341 | else if (charCode === LEFT_PARENTHESIS) {
|
|---|
| 342 | currentDescriptor += input.charAt(position);
|
|---|
| 343 | state = "in parens";
|
|---|
| 344 | }
|
|---|
| 345 | // EOF
|
|---|
| 346 | // If current descriptor is not empty, append current descriptor to
|
|---|
| 347 | // descriptors. Jump to the step labeled descriptor parser.
|
|---|
| 348 | else if (Number.isNaN(charCode)) {
|
|---|
| 349 | if (currentDescriptor) {
|
|---|
| 350 | descriptors.push(currentDescriptor);
|
|---|
| 351 | }
|
|---|
| 352 |
|
|---|
| 353 | parseDescriptors();
|
|---|
| 354 |
|
|---|
| 355 | return;
|
|---|
| 356 |
|
|---|
| 357 | // Anything else
|
|---|
| 358 | // Append charCode to current descriptor.
|
|---|
| 359 | } else {
|
|---|
| 360 | currentDescriptor += input.charAt(position);
|
|---|
| 361 | }
|
|---|
| 362 | }
|
|---|
| 363 | // In parens
|
|---|
| 364 | else if (state === "in parens") {
|
|---|
| 365 | // U+0029 RIGHT PARENTHESIS ())
|
|---|
| 366 | // Append charCode to current descriptor. Set state to in descriptor.
|
|---|
| 367 | if (charCode === RIGHT_PARENTHESIS) {
|
|---|
| 368 | currentDescriptor += input.charAt(position);
|
|---|
| 369 | state = "in descriptor";
|
|---|
| 370 | }
|
|---|
| 371 | // EOF
|
|---|
| 372 | // Append current descriptor to descriptors. Jump to the step labeled
|
|---|
| 373 | // descriptor parser.
|
|---|
| 374 | else if (Number.isNaN(charCode)) {
|
|---|
| 375 | descriptors.push(currentDescriptor);
|
|---|
| 376 | parseDescriptors();
|
|---|
| 377 | return;
|
|---|
| 378 | }
|
|---|
| 379 | // Anything else
|
|---|
| 380 | // Append charCode to current descriptor.
|
|---|
| 381 | else {
|
|---|
| 382 | currentDescriptor += input.charAt(position);
|
|---|
| 383 | }
|
|---|
| 384 | }
|
|---|
| 385 | // After descriptor
|
|---|
| 386 | else if (state === "after descriptor") {
|
|---|
| 387 | // Do the following, depending on the value of charCode:
|
|---|
| 388 | if (isASCIIWhitespace(charCode)) {
|
|---|
| 389 | // Space character: Stay in this state.
|
|---|
| 390 | }
|
|---|
| 391 | // EOF: Jump to the step labeled descriptor parser.
|
|---|
| 392 | else if (Number.isNaN(charCode)) {
|
|---|
| 393 | parseDescriptors();
|
|---|
| 394 | return;
|
|---|
| 395 | }
|
|---|
| 396 | // Anything else
|
|---|
| 397 | // Set state to in descriptor. Set position to the previous character in input.
|
|---|
| 398 | else {
|
|---|
| 399 | state = "in descriptor";
|
|---|
| 400 | position -= 1;
|
|---|
| 401 | }
|
|---|
| 402 | }
|
|---|
| 403 |
|
|---|
| 404 | // Advance position to the next character in input.
|
|---|
| 405 | position += 1;
|
|---|
| 406 | }
|
|---|
| 407 | }
|
|---|
| 408 |
|
|---|
| 409 | // 3. Let candidates be an initially empty source set.
|
|---|
| 410 | // const candidates = []; // Moved to top
|
|---|
| 411 |
|
|---|
| 412 | // 4. Splitting loop: Collect a sequence of characters that are space
|
|---|
| 413 | // characters or U+002C COMMA characters. If any U+002C COMMA characters
|
|---|
| 414 | // were collected, that is a parse error.
|
|---|
| 415 |
|
|---|
| 416 | while (true) {
|
|---|
| 417 | collectCharacters(LEADING_COMMAS_OR_SPACES_REGEXP);
|
|---|
| 418 |
|
|---|
| 419 | // 5. If position is past the end of input, return candidates and abort these steps.
|
|---|
| 420 | if (position >= inputLength) {
|
|---|
| 421 | if (candidates.length === 0) {
|
|---|
| 422 | throw new Error("Must contain one or more image candidate strings");
|
|---|
| 423 | }
|
|---|
| 424 |
|
|---|
| 425 | // (we're done, this is the sole return path)
|
|---|
| 426 | return candidates;
|
|---|
| 427 | }
|
|---|
| 428 |
|
|---|
| 429 | // 6. Collect a sequence of characters that are not space characters,
|
|---|
| 430 | // and let that be url.
|
|---|
| 431 | start = position;
|
|---|
| 432 | url = collectCharacters(LEADING_NOT_SPACES);
|
|---|
| 433 |
|
|---|
| 434 | // 7. Let descriptors be a new empty list.
|
|---|
| 435 | descriptors = [];
|
|---|
| 436 |
|
|---|
| 437 | // 8. If url ends with a U+002C COMMA character (,), follow these sub steps:
|
|---|
| 438 | // (1). Remove all trailing U+002C COMMA characters from url. If this removed
|
|---|
| 439 | // more than one character, that is a parse error.
|
|---|
| 440 | if (url && url.charCodeAt(url.length - 1) === COMMA) {
|
|---|
| 441 | url = url.replace(TRAILING_COMMAS_REGEXP, "");
|
|---|
| 442 |
|
|---|
| 443 | // (Jump ahead to step 9 to skip tokenization and just push the candidate).
|
|---|
| 444 | parseDescriptors();
|
|---|
| 445 | }
|
|---|
| 446 | // Otherwise, follow these sub steps:
|
|---|
| 447 | else {
|
|---|
| 448 | tokenize();
|
|---|
| 449 | }
|
|---|
| 450 |
|
|---|
| 451 | // 16. Return to the step labeled splitting loop.
|
|---|
| 452 | }
|
|---|
| 453 | };
|
|---|
| 454 |
|
|---|
| 455 | /**
|
|---|
| 456 | * @param {Map<string, string>} attributes attributes
|
|---|
| 457 | * @param {string} name name
|
|---|
| 458 | * @returns {string | undefined} attribute value
|
|---|
| 459 | */
|
|---|
| 460 | const getAttributeValue = (attributes, name) => attributes.get(name);
|
|---|
| 461 |
|
|---|
| 462 | /** @type {Map<string, Set<string>>} */
|
|---|
| 463 | const META = new Map([
|
|---|
| 464 | [
|
|---|
| 465 | "name",
|
|---|
| 466 | new Set([
|
|---|
| 467 | // msapplication-TileImage
|
|---|
| 468 | "msapplication-tileimage",
|
|---|
| 469 | "msapplication-square70x70logo",
|
|---|
| 470 | "msapplication-square150x150logo",
|
|---|
| 471 | "msapplication-wide310x150logo",
|
|---|
| 472 | "msapplication-square310x310logo",
|
|---|
| 473 | "msapplication-config",
|
|---|
| 474 | // TODO Do we need to parser it?
|
|---|
| 475 | // "msapplication-task",
|
|---|
| 476 | "twitter:image"
|
|---|
| 477 | ])
|
|---|
| 478 | ],
|
|---|
| 479 | [
|
|---|
| 480 | "property",
|
|---|
| 481 | new Set([
|
|---|
| 482 | "og:image",
|
|---|
| 483 | "og:image:url",
|
|---|
| 484 | "og:image:secure_url",
|
|---|
| 485 | "og:audio",
|
|---|
| 486 | "og:audio:secure_url",
|
|---|
| 487 | "og:video",
|
|---|
| 488 | "og:video:secure_url",
|
|---|
| 489 | "vk:image"
|
|---|
| 490 | ])
|
|---|
| 491 | ],
|
|---|
| 492 | [
|
|---|
| 493 | "itemprop",
|
|---|
| 494 | new Set([
|
|---|
| 495 | "image",
|
|---|
| 496 | "logo",
|
|---|
| 497 | "screenshot",
|
|---|
| 498 | "thumbnailurl",
|
|---|
| 499 | "contenturl",
|
|---|
| 500 | "downloadurl",
|
|---|
| 501 | "duringmedia",
|
|---|
| 502 | "embedurl",
|
|---|
| 503 | "installurl",
|
|---|
| 504 | "layoutimage"
|
|---|
| 505 | ])
|
|---|
| 506 | ]
|
|---|
| 507 | ]);
|
|---|
| 508 |
|
|---|
| 509 | /**
|
|---|
| 510 | * @param {Map<string, string>} attributes attributes
|
|---|
| 511 | * @returns {boolean} true when need to parse, otherwise false
|
|---|
| 512 | */
|
|---|
| 513 | const filterLinkItemprop = (attributes) => {
|
|---|
| 514 | const value = getAttributeValue(attributes, "itemprop");
|
|---|
| 515 | if (!value) return false;
|
|---|
| 516 | const allowedAttributes = META.get("itemprop");
|
|---|
| 517 | if (!allowedAttributes) return false;
|
|---|
| 518 |
|
|---|
| 519 | return allowedAttributes.has(value.trim().toLowerCase());
|
|---|
| 520 | };
|
|---|
| 521 |
|
|---|
| 522 | /**
|
|---|
| 523 | * @param {Map<string, string>} attributes attributes
|
|---|
| 524 | * @returns {boolean} true when need to parse, otherwise false
|
|---|
| 525 | */
|
|---|
| 526 | const filterLinkHref = (attributes) => {
|
|---|
| 527 | const rel = getAttributeValue(attributes, "rel");
|
|---|
| 528 | if (!rel) return false;
|
|---|
| 529 | const usedRels = rel.trim().toLowerCase().split(" ").filter(Boolean);
|
|---|
| 530 | const allowedRels = [
|
|---|
| 531 | "stylesheet",
|
|---|
| 532 | "icon",
|
|---|
| 533 | "mask-icon",
|
|---|
| 534 | "apple-touch-icon",
|
|---|
| 535 | "apple-touch-icon-precomposed",
|
|---|
| 536 | "apple-touch-startup-image",
|
|---|
| 537 | "manifest",
|
|---|
| 538 | "prefetch",
|
|---|
| 539 | "preload",
|
|---|
| 540 | "modulepreload"
|
|---|
| 541 | ];
|
|---|
| 542 |
|
|---|
| 543 | return allowedRels.some((value) => usedRels.includes(value));
|
|---|
| 544 | };
|
|---|
| 545 |
|
|---|
| 546 | /**
|
|---|
| 547 | * @param {Map<string, string>} attributes attributes
|
|---|
| 548 | * @returns {boolean} true when need to parse, otherwise false
|
|---|
| 549 | */
|
|---|
| 550 | const filterLinkUnion = (attributes) =>
|
|---|
| 551 | filterLinkHref(attributes) || filterLinkItemprop(attributes);
|
|---|
| 552 |
|
|---|
| 553 | /**
|
|---|
| 554 | * @param {Map<string, string>} attributes attributes
|
|---|
| 555 | * @returns {boolean} true when need to parse, otherwise false
|
|---|
| 556 | */
|
|---|
| 557 | const filterMetaContent = (attributes) => {
|
|---|
| 558 | for (const item of META) {
|
|---|
| 559 | const [key, allowedNames] = item;
|
|---|
| 560 | const name = getAttributeValue(attributes, key);
|
|---|
| 561 | if (!name) continue;
|
|---|
| 562 |
|
|---|
| 563 | return allowedNames.has(name.trim().toLowerCase());
|
|---|
| 564 | }
|
|---|
| 565 |
|
|---|
| 566 | return false;
|
|---|
| 567 | };
|
|---|
| 568 |
|
|---|
| 569 | /**
|
|---|
| 570 | * @param {Map<string, string>} attributes attributes
|
|---|
| 571 | * @returns {boolean} true when the script element opts into ES module semantics
|
|---|
| 572 | */
|
|---|
| 573 | const isModuleScript = (attributes) => {
|
|---|
| 574 | const type = getAttributeValue(attributes, "type");
|
|---|
| 575 | if (!type) return false;
|
|---|
| 576 | return type.trim().toLowerCase() === "module";
|
|---|
| 577 | };
|
|---|
| 578 |
|
|---|
| 579 | // HTML `<script>` `type` values that the browser treats as executable
|
|---|
| 580 | // JavaScript. Anything outside this set (e.g. `application/ld+json`,
|
|---|
| 581 | // `importmap`, `application/wasm`) is a data block — webpack must not
|
|---|
| 582 | // try to bundle it as a JS entry; it should pass through as an asset URL.
|
|---|
| 583 | const JS_SCRIPT_TYPES = new Set([
|
|---|
| 584 | "",
|
|---|
| 585 | "module",
|
|---|
| 586 | "text/javascript",
|
|---|
| 587 | "application/javascript",
|
|---|
| 588 | "text/ecmascript",
|
|---|
| 589 | "application/ecmascript"
|
|---|
| 590 | ]);
|
|---|
| 591 |
|
|---|
| 592 | /**
|
|---|
| 593 | * @param {Map<string, string>} attributes attributes
|
|---|
| 594 | * @returns {boolean} true when the script element's `type` is executable JS
|
|---|
| 595 | */
|
|---|
| 596 | const isExecutableJsScript = (attributes) => {
|
|---|
| 597 | const type = getAttributeValue(attributes, "type");
|
|---|
| 598 | if (type === undefined) return true;
|
|---|
| 599 | return JS_SCRIPT_TYPES.has(type.trim().toLowerCase());
|
|---|
| 600 | };
|
|---|
| 601 |
|
|---|
| 602 | /**
|
|---|
| 603 | * @param {Map<string, string>} attributes attributes
|
|---|
| 604 | * @returns {boolean} true when the link points at an ES module that should be bundled as an entry chunk
|
|---|
| 605 | */
|
|---|
| 606 | const isLinkModulePreload = (attributes) => {
|
|---|
| 607 | const rel = getAttributeValue(attributes, "rel");
|
|---|
| 608 | if (!rel) return false;
|
|---|
| 609 | return rel.trim().toLowerCase().split(/\s+/).includes("modulepreload");
|
|---|
| 610 | };
|
|---|
| 611 |
|
|---|
| 612 | /**
|
|---|
| 613 | * @param {Map<string, string>} attributes attributes
|
|---|
| 614 | * @returns {boolean} true when the link is a `<link rel="stylesheet">` that should be bundled as a CSS entry chunk
|
|---|
| 615 | */
|
|---|
| 616 | const isLinkStylesheet = (attributes) => {
|
|---|
| 617 | const rel = getAttributeValue(attributes, "rel");
|
|---|
| 618 | if (!rel) return false;
|
|---|
| 619 | return rel.trim().toLowerCase().split(/\s+/).includes("stylesheet");
|
|---|
| 620 | };
|
|---|
| 621 |
|
|---|
| 622 | /** @type {Map<string, Map<string, { parse: (input: string) => ParsedSource[] | undefined, filter?: (attributes: Map<string, string>) => boolean, entry?: boolean | ((attributes: Map<string, string>) => boolean), entryCategory?: string }>>} */
|
|---|
| 623 | const DEFAULT_SOURCES = new Map([
|
|---|
| 624 | [
|
|---|
| 625 | "audio",
|
|---|
| 626 | new Map([
|
|---|
| 627 | [
|
|---|
| 628 | "src",
|
|---|
| 629 | {
|
|---|
| 630 | parse: parseSrc
|
|---|
| 631 | }
|
|---|
| 632 | ]
|
|---|
| 633 | ])
|
|---|
| 634 | ],
|
|---|
| 635 | [
|
|---|
| 636 | "embed",
|
|---|
| 637 | new Map([
|
|---|
| 638 | [
|
|---|
| 639 | "src",
|
|---|
| 640 | {
|
|---|
| 641 | parse: parseSrc
|
|---|
| 642 | }
|
|---|
| 643 | ]
|
|---|
| 644 | ])
|
|---|
| 645 | ],
|
|---|
| 646 | [
|
|---|
| 647 | "img",
|
|---|
| 648 | new Map([
|
|---|
| 649 | [
|
|---|
| 650 | "src",
|
|---|
| 651 | {
|
|---|
| 652 | parse: parseSrc
|
|---|
| 653 | }
|
|---|
| 654 | ],
|
|---|
| 655 | [
|
|---|
| 656 | "srcset",
|
|---|
| 657 | {
|
|---|
| 658 | parse: parseSrcset
|
|---|
| 659 | }
|
|---|
| 660 | ]
|
|---|
| 661 | ])
|
|---|
| 662 | ],
|
|---|
| 663 | [
|
|---|
| 664 | "input",
|
|---|
| 665 | new Map([
|
|---|
| 666 | [
|
|---|
| 667 | "src",
|
|---|
| 668 | {
|
|---|
| 669 | parse: parseSrc
|
|---|
| 670 | }
|
|---|
| 671 | ]
|
|---|
| 672 | ])
|
|---|
| 673 | ],
|
|---|
| 674 | [
|
|---|
| 675 | "link",
|
|---|
| 676 | new Map([
|
|---|
| 677 | [
|
|---|
| 678 | "href",
|
|---|
| 679 | {
|
|---|
| 680 | parse: parseSrc,
|
|---|
| 681 | filter: filterLinkUnion,
|
|---|
| 682 | entry: isLinkModulePreload,
|
|---|
| 683 | entryCategory: "esm"
|
|---|
| 684 | }
|
|---|
| 685 | ],
|
|---|
| 686 | [
|
|---|
| 687 | "imagesrcset",
|
|---|
| 688 | {
|
|---|
| 689 | parse: parseSrcset,
|
|---|
| 690 | filter: filterLinkHref
|
|---|
| 691 | }
|
|---|
| 692 | ]
|
|---|
| 693 | ])
|
|---|
| 694 | ],
|
|---|
| 695 | [
|
|---|
| 696 | "meta",
|
|---|
| 697 | new Map([
|
|---|
| 698 | [
|
|---|
| 699 | "content",
|
|---|
| 700 | {
|
|---|
| 701 | parse: parseSrc,
|
|---|
| 702 | filter: filterMetaContent
|
|---|
| 703 | }
|
|---|
| 704 | ]
|
|---|
| 705 | ])
|
|---|
| 706 | ],
|
|---|
| 707 | [
|
|---|
| 708 | "object",
|
|---|
| 709 | new Map([
|
|---|
| 710 | [
|
|---|
| 711 | "data",
|
|---|
| 712 | {
|
|---|
| 713 | parse: parseSrc
|
|---|
| 714 | }
|
|---|
| 715 | ]
|
|---|
| 716 | ])
|
|---|
| 717 | ],
|
|---|
| 718 | [
|
|---|
| 719 | "script",
|
|---|
| 720 | new Map([
|
|---|
| 721 | [
|
|---|
| 722 | "src",
|
|---|
| 723 | {
|
|---|
| 724 | parse: parseSrc,
|
|---|
| 725 | // Only executable-JS scripts become entries. Non-JS
|
|---|
| 726 | // `<script>` types (e.g. `application/ld+json`,
|
|---|
| 727 | // `importmap`) fall through to HtmlSourceDependency so
|
|---|
| 728 | // the browser keeps seeing them as data blocks, with
|
|---|
| 729 | // the asset URL rewritten like any other resource.
|
|---|
| 730 | entry: isExecutableJsScript
|
|---|
| 731 | }
|
|---|
| 732 | ]
|
|---|
| 733 | ])
|
|---|
| 734 | ],
|
|---|
| 735 | [
|
|---|
| 736 | "source",
|
|---|
| 737 | new Map([
|
|---|
| 738 | [
|
|---|
| 739 | "src",
|
|---|
| 740 | {
|
|---|
| 741 | parse: parseSrc
|
|---|
| 742 | }
|
|---|
| 743 | ],
|
|---|
| 744 | [
|
|---|
| 745 | "srcset",
|
|---|
| 746 | {
|
|---|
| 747 | parse: parseSrcset
|
|---|
| 748 | }
|
|---|
| 749 | ]
|
|---|
| 750 | ])
|
|---|
| 751 | ],
|
|---|
| 752 | [
|
|---|
| 753 | "track",
|
|---|
| 754 | new Map([
|
|---|
| 755 | [
|
|---|
| 756 | "src",
|
|---|
| 757 | {
|
|---|
| 758 | parse: parseSrc
|
|---|
| 759 | }
|
|---|
| 760 | ]
|
|---|
| 761 | ])
|
|---|
| 762 | ],
|
|---|
| 763 | [
|
|---|
| 764 | "video",
|
|---|
| 765 | new Map([
|
|---|
| 766 | [
|
|---|
| 767 | "poster",
|
|---|
| 768 | {
|
|---|
| 769 | parse: parseSrc
|
|---|
| 770 | }
|
|---|
| 771 | ],
|
|---|
| 772 | [
|
|---|
| 773 | "src",
|
|---|
| 774 | {
|
|---|
| 775 | parse: parseSrc
|
|---|
| 776 | }
|
|---|
| 777 | ]
|
|---|
| 778 | ])
|
|---|
| 779 | ],
|
|---|
| 780 | // SVG
|
|---|
| 781 | [
|
|---|
| 782 | "image",
|
|---|
| 783 | new Map([
|
|---|
| 784 | [
|
|---|
| 785 | "xlink:href",
|
|---|
| 786 | {
|
|---|
| 787 | parse: parseSrc
|
|---|
| 788 | }
|
|---|
| 789 | ],
|
|---|
| 790 | [
|
|---|
| 791 | "href",
|
|---|
| 792 | {
|
|---|
| 793 | parse: parseSrc
|
|---|
| 794 | }
|
|---|
| 795 | ]
|
|---|
| 796 | ])
|
|---|
| 797 | ],
|
|---|
| 798 | [
|
|---|
| 799 | "use",
|
|---|
| 800 | new Map([
|
|---|
| 801 | [
|
|---|
| 802 | "xlink:href",
|
|---|
| 803 | {
|
|---|
| 804 | parse: parseSrc
|
|---|
| 805 | }
|
|---|
| 806 | ],
|
|---|
| 807 | [
|
|---|
| 808 | "href",
|
|---|
| 809 | {
|
|---|
| 810 | parse: parseSrc
|
|---|
| 811 | }
|
|---|
| 812 | ]
|
|---|
| 813 | ])
|
|---|
| 814 | ]
|
|---|
| 815 | ]);
|
|---|
| 816 |
|
|---|
| 817 | class HtmlParser extends Parser {
|
|---|
| 818 | /**
|
|---|
| 819 | * Creates an instance of HtmlParser.
|
|---|
| 820 | * @param {(string | typeof import("../util/Hash"))=} hashFunction algorithm or constructor used by `output.hashFunction`; falls back to the default when omitted
|
|---|
| 821 | * @param {string=} context compilation context used to contextify the HTML module's identifier when seeding the entry-name hash
|
|---|
| 822 | * @param {boolean=} outputModule whether `output.module` is enabled; when true, classic `<script src>` tags get `type="module"` auto-inserted so the rewritten src can load the emitted ES module chunk
|
|---|
| 823 | * @param {boolean=} css whether `experiments.css` is enabled; when true, inline `<style>` bodies are routed through the CSS pipeline as `data:text/css` modules
|
|---|
| 824 | */
|
|---|
| 825 | constructor(hashFunction, context, outputModule, css) {
|
|---|
| 826 | super();
|
|---|
| 827 | this.magicCommentContext = createMagicCommentContext();
|
|---|
| 828 | this.hashFunction = hashFunction;
|
|---|
| 829 | this.context = context;
|
|---|
| 830 | this.outputModule = outputModule;
|
|---|
| 831 | this.css = css;
|
|---|
| 832 | }
|
|---|
| 833 |
|
|---|
| 834 | /**
|
|---|
| 835 | * Parses the provided source and updates the parser state.
|
|---|
| 836 | * @param {string | Buffer | PreparsedAst} source the source to parse
|
|---|
| 837 | * @param {ParserState} state the parser state
|
|---|
| 838 | * @returns {ParserState} the parser state
|
|---|
| 839 | */
|
|---|
| 840 | parse(source, state) {
|
|---|
| 841 | if (Buffer.isBuffer(source)) {
|
|---|
| 842 | source = source.toString("utf8");
|
|---|
| 843 | } else if (typeof source === "object") {
|
|---|
| 844 | throw new Error("webpackAst is unexpected for the HtmlParser");
|
|---|
| 845 | }
|
|---|
| 846 | if (source[0] === "\uFEFF") {
|
|---|
| 847 | source = source.slice(1);
|
|---|
| 848 | }
|
|---|
| 849 |
|
|---|
| 850 | const locConverter = new LocConverter(source);
|
|---|
| 851 |
|
|---|
| 852 | const module = state.module;
|
|---|
| 853 |
|
|---|
| 854 | // Stable, per-HTML-module prefix used when generating entry names for
|
|---|
| 855 | // script src / modulepreload references so they don't collide across
|
|---|
| 856 | // HTML modules in the same compilation. We hash the module's resource
|
|---|
| 857 | // path (a plain absolute path) — going through `contextify` against
|
|---|
| 858 | // the compilation root keeps the hash machine-stable for the same
|
|---|
| 859 | // project layout. Note: `module.identifier()` returns `html|<path>`
|
|---|
| 860 | // for HTML modules, which doesn't start with `/`, so contextify would
|
|---|
| 861 | // leave it absolute. `module.resource` is the bare path.
|
|---|
| 862 | /** @type {string} */
|
|---|
| 863 | const resource =
|
|---|
| 864 | /** @type {EXPECTED_ANY} */ (module).resource || module.identifier();
|
|---|
| 865 | const moduleHash = createHash(this.hashFunction || "md4")
|
|---|
| 866 | .update(this.context ? contextify(this.context, resource) : resource)
|
|---|
| 867 | .digest("hex")
|
|---|
| 868 | .slice(0, 8);
|
|---|
| 869 |
|
|---|
| 870 | /** @typedef {{ nameStart: number, nameEnd: number, valueStart: number, valueEnd: number }} AttrToken */
|
|---|
| 871 |
|
|---|
| 872 | /** @type {AttrToken[]} */
|
|---|
| 873 | const pendingAttributes = [];
|
|---|
| 874 |
|
|---|
| 875 | /**
|
|---|
| 876 | * Reconciles the rewritten `<script>` tag's `type` attribute with the
|
|---|
| 877 | * emitted chunk's actual format. Used by both the `<script src>` and
|
|---|
| 878 | * inline `<script>` paths so the two stay in sync.
|
|---|
| 879 | * @param {AttrToken | undefined} typeAttr existing `type` attribute, if any
|
|---|
| 880 | * @param {number} nameEnd position right after `<script` (for inserts)
|
|---|
| 881 | * @param {"classic" | "esm-script"} kind chunk kind decided by the parser
|
|---|
| 882 | * @param {string} input full source string
|
|---|
| 883 | * @returns {void}
|
|---|
| 884 | */
|
|---|
| 885 | const reconcileScriptTypeAttr = (typeAttr, nameEnd, kind, input) => {
|
|---|
| 886 | if (this.outputModule && kind === "classic") {
|
|---|
| 887 | // Chunk is an ES module; upgrade the tag.
|
|---|
| 888 | if (typeAttr && typeAttr.valueStart !== -1) {
|
|---|
| 889 | module.addPresentationalDependency(
|
|---|
| 890 | new ConstDependency("module", [
|
|---|
| 891 | typeAttr.valueStart,
|
|---|
| 892 | typeAttr.valueEnd
|
|---|
| 893 | ])
|
|---|
| 894 | );
|
|---|
| 895 | } else {
|
|---|
| 896 | module.addPresentationalDependency(
|
|---|
| 897 | new ConstDependency(' type="module"', nameEnd)
|
|---|
| 898 | );
|
|---|
| 899 | }
|
|---|
| 900 | } else if (!this.outputModule && kind === "esm-script" && typeAttr) {
|
|---|
| 901 | // Chunk is a classic IIFE; drop `type="module"` so the
|
|---|
| 902 | // browser doesn't load it under module semantics.
|
|---|
| 903 | let attrEnd;
|
|---|
| 904 | if (typeAttr.valueStart === -1) {
|
|---|
| 905 | attrEnd = typeAttr.nameEnd;
|
|---|
| 906 | } else if (
|
|---|
| 907 | input[typeAttr.valueEnd] === '"' ||
|
|---|
| 908 | input[typeAttr.valueEnd] === "'"
|
|---|
| 909 | ) {
|
|---|
| 910 | attrEnd = typeAttr.valueEnd + 1;
|
|---|
| 911 | } else {
|
|---|
| 912 | attrEnd = typeAttr.valueEnd;
|
|---|
| 913 | }
|
|---|
| 914 | // Consume one leading whitespace char so we don't leave a
|
|---|
| 915 | // double space between `<script` and the next attribute.
|
|---|
| 916 | let attrStart = typeAttr.nameStart;
|
|---|
| 917 | if (
|
|---|
| 918 | attrStart > 0 &&
|
|---|
| 919 | isASCIIWhitespace(input.charCodeAt(attrStart - 1))
|
|---|
| 920 | ) {
|
|---|
| 921 | attrStart -= 1;
|
|---|
| 922 | }
|
|---|
| 923 | module.addPresentationalDependency(
|
|---|
| 924 | new ConstDependency("", [attrStart, attrEnd])
|
|---|
| 925 | );
|
|---|
| 926 | }
|
|---|
| 927 | };
|
|---|
| 928 |
|
|---|
| 929 | // Inline `<script>` body extraction is deferred to the matching
|
|---|
| 930 | // `closeTag` event so the walker's script-data state machine
|
|---|
| 931 | // (including its escaped/double-escaped sub-states) decides where
|
|---|
| 932 | // the body ends. This is the spec-compliant way to find the close
|
|---|
| 933 | // — a plain `</script>` regex would split too early inside
|
|---|
| 934 | // `<!--<script>…</script>-->` patterns.
|
|---|
| 935 | /** @type {null | { contentStart: number, attrs: Map<string, string>, typeAttr: AttrToken | undefined, nameEnd: number }} */
|
|---|
| 936 | let pendingInlineScript = null;
|
|---|
| 937 |
|
|---|
| 938 | // Script src / modulepreload references are collected per-category
|
|---|
| 939 | // during the walk; HtmlModulesPlugin later turns them into real
|
|---|
| 940 | // entries. Classic <script src> and <script type="module" src> are
|
|---|
| 941 | // chained via a leader-only dependOn so they share a runtime.
|
|---|
| 942 | // `<link rel="modulepreload">` entries are kept independent — they
|
|---|
| 943 | // must preload without running, so they can never become a runtime
|
|---|
| 944 | // leader that other entries would import.
|
|---|
| 945 | /**
|
|---|
| 946 | * @typedef {object} EntryScriptInfo
|
|---|
| 947 | * @property {string} request
|
|---|
| 948 | * @property {string} entryName
|
|---|
| 949 | * @property {"classic" | "esm-script" | "modulepreload" | "stylesheet"} kind
|
|---|
| 950 | */
|
|---|
| 951 | /** @type {EntryScriptInfo[]} */
|
|---|
| 952 | const classicEntries = [];
|
|---|
| 953 | /** @type {EntryScriptInfo[]} */
|
|---|
| 954 | const esmScriptEntries = [];
|
|---|
| 955 | /** @type {EntryScriptInfo[]} */
|
|---|
| 956 | const modulePreloadEntries = [];
|
|---|
| 957 | /** @type {EntryScriptInfo[]} */
|
|---|
| 958 | const stylesheetEntries = [];
|
|---|
| 959 |
|
|---|
| 960 | let nextEntryIndex = 0;
|
|---|
| 961 |
|
|---|
| 962 | /**
|
|---|
| 963 | * Tracks the `webpackIgnore` value from the most recent comment that
|
|---|
| 964 | * appears before the next tag. Reset whenever a tag is emitted or a
|
|---|
| 965 | * comment without a `webpackIgnore` value is encountered.
|
|---|
| 966 | * @type {boolean | undefined}
|
|---|
| 967 | */
|
|---|
| 968 | let pendingWebpackIgnore;
|
|---|
| 969 |
|
|---|
| 970 | const magicCommentContext = this.magicCommentContext;
|
|---|
| 971 |
|
|---|
| 972 | // TODO implement full HTML parser (WASM)
|
|---|
| 973 | walkHtmlTokens(source, 0, {
|
|---|
| 974 | comment: (input, start, end) => {
|
|---|
| 975 | // Only proper `<!-- ... -->` comments carry magic comments.
|
|---|
| 976 | // `walkHtmlTokens` also dispatches this callback for bogus
|
|---|
| 977 | // comments such as `<!DOCTYPE …>` and `<?…>`, which must not
|
|---|
| 978 | // be parsed as magic comments.
|
|---|
| 979 | if (
|
|---|
| 980 | end - start < 7 ||
|
|---|
| 981 | input.charCodeAt(start) !== 0x3c /* < */ ||
|
|---|
| 982 | input.charCodeAt(start + 1) !== 0x21 /* ! */ ||
|
|---|
| 983 | input.charCodeAt(start + 2) !== 0x2d /* - */ ||
|
|---|
| 984 | input.charCodeAt(start + 3) !== 0x2d /* - */ ||
|
|---|
| 985 | input.charCodeAt(end - 1) !== 0x3e /* > */ ||
|
|---|
| 986 | input.charCodeAt(end - 2) !== 0x2d /* - */ ||
|
|---|
| 987 | input.charCodeAt(end - 3) !== 0x2d /* - */
|
|---|
| 988 | ) {
|
|---|
| 989 | pendingWebpackIgnore = undefined;
|
|---|
| 990 | return end;
|
|---|
| 991 | }
|
|---|
| 992 | const contentStart = start + 4;
|
|---|
| 993 | const contentEnd = end - 3;
|
|---|
| 994 | const value = input.slice(contentStart, contentEnd);
|
|---|
| 995 | if (!webpackCommentRegExp.test(value)) {
|
|---|
| 996 | pendingWebpackIgnore = undefined;
|
|---|
| 997 | return end;
|
|---|
| 998 | }
|
|---|
| 999 | /** @type {Record<string, EXPECTED_ANY>} */
|
|---|
| 1000 | let options;
|
|---|
| 1001 | try {
|
|---|
| 1002 | options = vm.runInContext(
|
|---|
| 1003 | `(function(){return {${value}};})()`,
|
|---|
| 1004 | magicCommentContext
|
|---|
| 1005 | );
|
|---|
| 1006 | } catch (err) {
|
|---|
| 1007 | const { line: sl, column: sc } = locConverter.get(start);
|
|---|
| 1008 | const { line: el, column: ec } = locConverter.get(end);
|
|---|
| 1009 | module.addWarning(
|
|---|
| 1010 | new CommentCompilationWarning(
|
|---|
| 1011 | `Compilation error while processing magic comment(-s): /*${value}*/: ${
|
|---|
| 1012 | /** @type {Error} */ (err).message
|
|---|
| 1013 | }`,
|
|---|
| 1014 | {
|
|---|
| 1015 | start: { line: sl, column: sc },
|
|---|
| 1016 | end: { line: el, column: ec }
|
|---|
| 1017 | }
|
|---|
| 1018 | )
|
|---|
| 1019 | );
|
|---|
| 1020 | pendingWebpackIgnore = undefined;
|
|---|
| 1021 | return end;
|
|---|
| 1022 | }
|
|---|
| 1023 | if (options.webpackIgnore === undefined) {
|
|---|
| 1024 | pendingWebpackIgnore = undefined;
|
|---|
| 1025 | return end;
|
|---|
| 1026 | }
|
|---|
| 1027 | if (typeof options.webpackIgnore !== "boolean") {
|
|---|
| 1028 | const { line: sl, column: sc } = locConverter.get(start);
|
|---|
| 1029 | const { line: el, column: ec } = locConverter.get(end);
|
|---|
| 1030 | module.addWarning(
|
|---|
| 1031 | new UnsupportedFeatureWarning(
|
|---|
| 1032 | `\`webpackIgnore\` expected a boolean, but received: ${options.webpackIgnore}.`,
|
|---|
| 1033 | {
|
|---|
| 1034 | start: { line: sl, column: sc },
|
|---|
| 1035 | end: { line: el, column: ec }
|
|---|
| 1036 | }
|
|---|
| 1037 | )
|
|---|
| 1038 | );
|
|---|
| 1039 | pendingWebpackIgnore = undefined;
|
|---|
| 1040 | return end;
|
|---|
| 1041 | }
|
|---|
| 1042 | pendingWebpackIgnore = options.webpackIgnore;
|
|---|
| 1043 | return end;
|
|---|
| 1044 | },
|
|---|
| 1045 | attribute: (
|
|---|
| 1046 | input,
|
|---|
| 1047 | nameStart,
|
|---|
| 1048 | nameEnd,
|
|---|
| 1049 | valueStart,
|
|---|
| 1050 | valueEnd,
|
|---|
| 1051 | quoteType
|
|---|
| 1052 | ) => {
|
|---|
| 1053 | pendingAttributes.push({ nameStart, nameEnd, valueStart, valueEnd });
|
|---|
| 1054 | if (valueStart === -1) return nameEnd;
|
|---|
| 1055 | return quoteType !== walkHtmlTokens.QUOTE_NONE
|
|---|
| 1056 | ? valueEnd + 1
|
|---|
| 1057 | : valueEnd;
|
|---|
| 1058 | },
|
|---|
| 1059 | closeTag: (input, start, end, nameStart, nameEnd) => {
|
|---|
| 1060 | pendingWebpackIgnore = undefined;
|
|---|
| 1061 | if (pendingInlineScript) {
|
|---|
| 1062 | const elName = input.slice(nameStart, nameEnd).toLowerCase();
|
|---|
| 1063 | if (elName === "script") {
|
|---|
| 1064 | const ps = pendingInlineScript;
|
|---|
| 1065 | pendingInlineScript = null;
|
|---|
| 1066 | const contentStart = ps.contentStart;
|
|---|
| 1067 | const contentEnd = start; // start of `</script>`
|
|---|
| 1068 | const jsContent = input.slice(contentStart, contentEnd);
|
|---|
| 1069 | if (jsContent.trim() === "") return end;
|
|---|
| 1070 |
|
|---|
| 1071 | // Base64-encode the JS body so the data URI round-trips
|
|---|
| 1072 | // arbitrary JavaScript text, including non-ASCII source
|
|---|
| 1073 | // (`decodeDataURI` decodes non-base64 bodies as ASCII,
|
|---|
| 1074 | // which would corrupt Unicode string literals or
|
|---|
| 1075 | // identifiers).
|
|---|
| 1076 | const request = `data:text/javascript;base64,${Buffer.from(
|
|---|
| 1077 | jsContent,
|
|---|
| 1078 | "utf8"
|
|---|
| 1079 | ).toString("base64")}`;
|
|---|
| 1080 |
|
|---|
| 1081 | const useEsmEntry = isModuleScript(ps.attrs);
|
|---|
| 1082 | const entryName = `__html_${moduleHash}_${nextEntryIndex++}`;
|
|---|
| 1083 | /** @type {"classic" | "esm-script"} */
|
|---|
| 1084 | const kind = useEsmEntry ? "esm-script" : "classic";
|
|---|
| 1085 | const { line: sl, column: sc } = locConverter.get(contentStart);
|
|---|
| 1086 | const { line: el, column: ec } = locConverter.get(contentEnd);
|
|---|
| 1087 | const dep = new HtmlInlineScriptDependency(
|
|---|
| 1088 | request,
|
|---|
| 1089 | ps.nameEnd,
|
|---|
| 1090 | [contentStart, contentEnd],
|
|---|
| 1091 | entryName,
|
|---|
| 1092 | useEsmEntry ? "esm" : "commonjs"
|
|---|
| 1093 | );
|
|---|
| 1094 | dep.setLoc(sl, sc, el, ec);
|
|---|
| 1095 | module.addPresentationalDependency(dep);
|
|---|
| 1096 | reconcileScriptTypeAttr(ps.typeAttr, ps.nameEnd, kind, input);
|
|---|
| 1097 | const collection =
|
|---|
| 1098 | kind === "classic" ? classicEntries : esmScriptEntries;
|
|---|
| 1099 | collection.push({ request, entryName, kind });
|
|---|
| 1100 | }
|
|---|
| 1101 | }
|
|---|
| 1102 | return end;
|
|---|
| 1103 | },
|
|---|
| 1104 | openTag: (input, start, end, nameStart, nameEnd) => {
|
|---|
| 1105 | const ignore = pendingWebpackIgnore === true;
|
|---|
| 1106 | pendingWebpackIgnore = undefined;
|
|---|
| 1107 | if (ignore) {
|
|---|
| 1108 | // For `<script>` and `<style>` we don't emit a dependency,
|
|---|
| 1109 | // but we must NOT advance past the close tag either: the
|
|---|
| 1110 | // walker is already in script-data/rawtext state for
|
|---|
| 1111 | // those tags and will consume the body and emit the
|
|---|
| 1112 | // matching `closeTag` itself. Returning a position past
|
|---|
| 1113 | // `</script>`/`</style>` would leave the walker stuck in
|
|---|
| 1114 | // rawtext mode, swallowing later markup.
|
|---|
| 1115 | pendingAttributes.length = 0;
|
|---|
| 1116 | return end;
|
|---|
| 1117 | }
|
|---|
| 1118 | const elementName = input.slice(nameStart, nameEnd).toLowerCase();
|
|---|
| 1119 |
|
|---|
| 1120 | // `<style>` is rawtext: capture the inline CSS and hand it to
|
|---|
| 1121 | // the CSS pipeline as a virtual `data:text/css` module with
|
|---|
| 1122 | // `exportType: "text"`. We use the regex only to discover the
|
|---|
| 1123 | // `</style>` position so we can slice the body; the walker is
|
|---|
| 1124 | // already in rawtext state for `<style>` and will emit the
|
|---|
| 1125 | // matching `closeTag` event itself, so we must return `end`
|
|---|
| 1126 | // (the position right after the opening tag's `>`) rather
|
|---|
| 1127 | // than advancing past `</style>`. Only `<style>` tags whose
|
|---|
| 1128 | // `type` attribute is absent, empty, or `text/css` are
|
|---|
| 1129 | // processed; other types are left untouched.
|
|---|
| 1130 | if (elementName === "style") {
|
|---|
| 1131 | /** @type {string | undefined} */
|
|---|
| 1132 | let typeValue;
|
|---|
| 1133 | for (const attr of pendingAttributes) {
|
|---|
| 1134 | const attrName = input
|
|---|
| 1135 | .slice(attr.nameStart, attr.nameEnd)
|
|---|
| 1136 | .toLowerCase();
|
|---|
| 1137 | if (attrName === "type") {
|
|---|
| 1138 | typeValue =
|
|---|
| 1139 | attr.valueStart !== -1
|
|---|
| 1140 | ? input.slice(attr.valueStart, attr.valueEnd)
|
|---|
| 1141 | : "";
|
|---|
| 1142 | break;
|
|---|
| 1143 | }
|
|---|
| 1144 | }
|
|---|
| 1145 | pendingAttributes.length = 0;
|
|---|
| 1146 |
|
|---|
| 1147 | STYLE_END_REGEXP.lastIndex = end;
|
|---|
| 1148 | const closeMatch = STYLE_END_REGEXP.exec(input);
|
|---|
| 1149 | if (!closeMatch) return end;
|
|---|
| 1150 | const contentStart = end;
|
|---|
| 1151 | const contentEnd = closeMatch.index;
|
|---|
| 1152 |
|
|---|
| 1153 | const trimmedType =
|
|---|
| 1154 | typeValue !== undefined ? typeValue.trim().toLowerCase() : "";
|
|---|
| 1155 | if (
|
|---|
| 1156 | typeValue !== undefined &&
|
|---|
| 1157 | trimmedType !== "" &&
|
|---|
| 1158 | trimmedType !== "text/css"
|
|---|
| 1159 | ) {
|
|---|
| 1160 | return end;
|
|---|
| 1161 | }
|
|---|
| 1162 |
|
|---|
| 1163 | // Inline-style processing requires the CSS pipeline; when
|
|---|
| 1164 | // `experiments.css` is off, leave the body alone — the
|
|---|
| 1165 | // walker is in rawtext mode for `<style>` and will skip
|
|---|
| 1166 | // over the body to the matching `</style>` itself.
|
|---|
| 1167 | if (!this.css) {
|
|---|
| 1168 | return end;
|
|---|
| 1169 | }
|
|---|
| 1170 |
|
|---|
| 1171 | const cssContent = input.slice(contentStart, contentEnd);
|
|---|
| 1172 | if (cssContent.trim() === "") {
|
|---|
| 1173 | return end;
|
|---|
| 1174 | }
|
|---|
| 1175 |
|
|---|
| 1176 | // URL-encode the CSS body so the data URI parser's `(.*)$`
|
|---|
| 1177 | // body group matches even when the source has newlines or
|
|---|
| 1178 | // other characters that would otherwise break the regex.
|
|---|
| 1179 | const request = `data:text/css,${encodeURIComponent(cssContent)}`;
|
|---|
| 1180 |
|
|---|
| 1181 | const { line: sl, column: sc } = locConverter.get(contentStart);
|
|---|
| 1182 | const { line: el, column: ec } = locConverter.get(contentEnd);
|
|---|
| 1183 | const dep = new HtmlInlineStyleDependency(request, [
|
|---|
| 1184 | contentStart,
|
|---|
| 1185 | contentEnd
|
|---|
| 1186 | ]);
|
|---|
| 1187 | dep.setLoc(sl, sc, el, ec);
|
|---|
| 1188 | module.addDependency(dep);
|
|---|
| 1189 | module.addCodeGenerationDependency(dep);
|
|---|
| 1190 | return end;
|
|---|
| 1191 | }
|
|---|
| 1192 |
|
|---|
| 1193 | const sources = DEFAULT_SOURCES.get(elementName);
|
|---|
| 1194 |
|
|---|
| 1195 | if (!sources) {
|
|---|
| 1196 | pendingAttributes.length = 0;
|
|---|
| 1197 | return end;
|
|---|
| 1198 | }
|
|---|
| 1199 |
|
|---|
| 1200 | /** @type {Map<string, string> | undefined} */
|
|---|
| 1201 | let attributesMap;
|
|---|
| 1202 | const getAttributesMap = () => {
|
|---|
| 1203 | if (attributesMap) return attributesMap;
|
|---|
| 1204 | attributesMap = new Map();
|
|---|
| 1205 | for (const attr of pendingAttributes) {
|
|---|
| 1206 | const name = input
|
|---|
| 1207 | .slice(attr.nameStart, attr.nameEnd)
|
|---|
| 1208 | .toLowerCase();
|
|---|
| 1209 | const value =
|
|---|
| 1210 | attr.valueStart !== -1
|
|---|
| 1211 | ? input.slice(attr.valueStart, attr.valueEnd)
|
|---|
| 1212 | : "";
|
|---|
| 1213 | attributesMap.set(name, value);
|
|---|
| 1214 | }
|
|---|
| 1215 | return attributesMap;
|
|---|
| 1216 | };
|
|---|
| 1217 |
|
|---|
| 1218 | for (const attr of pendingAttributes) {
|
|---|
| 1219 | const attributeName = input
|
|---|
| 1220 | .slice(attr.nameStart, attr.nameEnd)
|
|---|
| 1221 | .toLowerCase();
|
|---|
| 1222 | const sourceItem = sources.get(attributeName);
|
|---|
| 1223 |
|
|---|
| 1224 | if (!sourceItem) continue;
|
|---|
| 1225 |
|
|---|
| 1226 | // TODO(html-entities): We should ideally decode entities here using
|
|---|
| 1227 | // `walkHtmlTokens.decodeHtmlEntities(input.slice(...))` so that URLs
|
|---|
| 1228 | // like `image.png?a=1&b=2` are correctly resolved as `&`.
|
|---|
| 1229 | // However, doing so currently breaks `srcset` parsing tests (e.g. `errors.js`)
|
|---|
| 1230 | // which explicitly expect whitespace entities like `	` to NOT be decoded
|
|---|
| 1231 | // before the srcset parser runs. A follow-up PR should implement selective
|
|---|
| 1232 | // decoding for specific URL attributes.
|
|---|
| 1233 | const attributeValue =
|
|---|
| 1234 | attr.valueStart !== -1
|
|---|
| 1235 | ? input.slice(attr.valueStart, attr.valueEnd)
|
|---|
| 1236 | : "";
|
|---|
| 1237 |
|
|---|
| 1238 | if (!attributeValue) continue;
|
|---|
| 1239 |
|
|---|
| 1240 | if (
|
|---|
| 1241 | typeof sourceItem.filter === "function" &&
|
|---|
| 1242 | !sourceItem.filter(getAttributesMap())
|
|---|
| 1243 | ) {
|
|---|
| 1244 | continue;
|
|---|
| 1245 | }
|
|---|
| 1246 |
|
|---|
| 1247 | /** @type {ParsedSource[] | undefined} */
|
|---|
| 1248 | let parsedAttributeValue;
|
|---|
| 1249 |
|
|---|
| 1250 | try {
|
|---|
| 1251 | parsedAttributeValue = sourceItem.parse(attributeValue);
|
|---|
| 1252 | } catch (err) {
|
|---|
| 1253 | const { line: sl, column: sc } = locConverter.get(attr.valueStart);
|
|---|
| 1254 | const { line: el, column: ec } = locConverter.get(attr.valueEnd);
|
|---|
| 1255 |
|
|---|
| 1256 | module.addError(
|
|---|
| 1257 | new ModuleDependencyError(
|
|---|
| 1258 | module,
|
|---|
| 1259 | new WebpackError(
|
|---|
| 1260 | `Bad value for attribute "${attributeName}" on element "${elementName}": ${
|
|---|
| 1261 | /** @type {Error} */ (err).message
|
|---|
| 1262 | }`
|
|---|
| 1263 | ),
|
|---|
| 1264 | {
|
|---|
| 1265 | start: { line: sl, column: sc },
|
|---|
| 1266 | end: { line: el, column: ec }
|
|---|
| 1267 | }
|
|---|
| 1268 | )
|
|---|
| 1269 | );
|
|---|
| 1270 | }
|
|---|
| 1271 |
|
|---|
| 1272 | if (!parsedAttributeValue) continue;
|
|---|
| 1273 |
|
|---|
| 1274 | // `<link rel="stylesheet">` is upgraded to an entry only when
|
|---|
| 1275 | // `experiments.css` is on — that's the mode where webpack can
|
|---|
| 1276 | // bundle the CSS into its own chunk. Without it, the
|
|---|
| 1277 | // stylesheet href stays a plain asset URL. Scope this to the
|
|---|
| 1278 | // `href` attribute only: `<link>` also exposes
|
|---|
| 1279 | // `imagesrcset` URLs which must continue to flow through
|
|---|
| 1280 | // the regular asset rewriting path even on a stylesheet
|
|---|
| 1281 | // link.
|
|---|
| 1282 | const isStylesheetEntry =
|
|---|
| 1283 | this.css &&
|
|---|
| 1284 | elementName === "link" &&
|
|---|
| 1285 | attributeName === "href" &&
|
|---|
| 1286 | isLinkStylesheet(getAttributesMap());
|
|---|
| 1287 | const isEntry =
|
|---|
| 1288 | isStylesheetEntry ||
|
|---|
| 1289 | sourceItem.entry === true ||
|
|---|
| 1290 | (typeof sourceItem.entry === "function" &&
|
|---|
| 1291 | sourceItem.entry(getAttributesMap()));
|
|---|
| 1292 |
|
|---|
| 1293 | // `<script type="module" src>` and `<link rel="modulepreload">`
|
|---|
| 1294 | // reference ES modules; everything else under `<script src>` is a
|
|---|
| 1295 | // classic script. The category drives ESM vs CommonJS resolution
|
|---|
| 1296 | // of the entry — the chunk format is controlled by the user via
|
|---|
| 1297 | // `output.module` / `experiments.outputModule`.
|
|---|
| 1298 | const useEsmEntry =
|
|---|
| 1299 | (elementName === "script" && isModuleScript(getAttributesMap())) ||
|
|---|
| 1300 | sourceItem.entryCategory === "esm";
|
|---|
| 1301 |
|
|---|
| 1302 | for (const parsedSource of parsedAttributeValue) {
|
|---|
| 1303 | const [value, innerStart, innerEnd] = parsedSource;
|
|---|
| 1304 | if (value.startsWith("#")) continue;
|
|---|
| 1305 | const sourceStart = attr.valueStart + innerStart;
|
|---|
| 1306 | const sourceEnd = attr.valueStart + innerEnd;
|
|---|
| 1307 | const { line: sl, column: sc } = locConverter.get(sourceStart);
|
|---|
| 1308 | const { line: el, column: ec } = locConverter.get(sourceEnd);
|
|---|
| 1309 | if (isEntry) {
|
|---|
| 1310 | const entryName = `__html_${moduleHash}_${nextEntryIndex++}`;
|
|---|
| 1311 | const isStylesheetLink =
|
|---|
| 1312 | elementName === "link" && isLinkStylesheet(getAttributesMap());
|
|---|
| 1313 | /** @type {"classic" | "esm-script" | "modulepreload" | "stylesheet"} */
|
|---|
| 1314 | const kind =
|
|---|
| 1315 | elementName === "link"
|
|---|
| 1316 | ? isStylesheetLink
|
|---|
| 1317 | ? "stylesheet"
|
|---|
| 1318 | : "modulepreload"
|
|---|
| 1319 | : useEsmEntry
|
|---|
| 1320 | ? "esm-script"
|
|---|
| 1321 | : "classic";
|
|---|
| 1322 | // With `output.module` enabled, a classic `<script src>` is
|
|---|
| 1323 | // upgraded in place to `<script type="module" src>` (see the
|
|---|
| 1324 | // ConstDependency insertion below). Account for that in the
|
|---|
| 1325 | // dependency's `elementKind` so sibling tags emitted by the
|
|---|
| 1326 | // template for additional entry chunks (runtime / split chunks)
|
|---|
| 1327 | // also use `type="module"`.
|
|---|
| 1328 | const willBeModuleScript =
|
|---|
| 1329 | kind === "esm-script" ||
|
|---|
| 1330 | (this.outputModule &&
|
|---|
| 1331 | kind === "classic" &&
|
|---|
| 1332 | elementName === "script");
|
|---|
| 1333 | /** @type {"script-classic" | "script-module" | "modulepreload" | "stylesheet"} */
|
|---|
| 1334 | const elementKind =
|
|---|
| 1335 | kind === "modulepreload"
|
|---|
| 1336 | ? "modulepreload"
|
|---|
| 1337 | : kind === "stylesheet"
|
|---|
| 1338 | ? "stylesheet"
|
|---|
| 1339 | : willBeModuleScript
|
|---|
| 1340 | ? "script-module"
|
|---|
| 1341 | : "script-classic";
|
|---|
| 1342 | const dep = new HtmlScriptSrcDependency(
|
|---|
| 1343 | value,
|
|---|
| 1344 | [sourceStart, sourceEnd],
|
|---|
| 1345 | entryName,
|
|---|
| 1346 | // `<link rel="stylesheet">` is bundled as a CSS entry —
|
|---|
| 1347 | // using a non-"url" category so the default `.css` rule
|
|---|
| 1348 | // (which gives the resolved module the CSS module type)
|
|---|
| 1349 | // wins over the `dependency: "url"` → asset rule.
|
|---|
| 1350 | kind === "stylesheet"
|
|---|
| 1351 | ? "css-import"
|
|---|
| 1352 | : useEsmEntry
|
|---|
| 1353 | ? "esm"
|
|---|
| 1354 | : sourceItem.entryCategory,
|
|---|
| 1355 | elementKind,
|
|---|
| 1356 | start,
|
|---|
| 1357 | end
|
|---|
| 1358 | );
|
|---|
| 1359 | dep.setLoc(sl, sc, el, ec);
|
|---|
| 1360 | module.addPresentationalDependency(dep);
|
|---|
| 1361 | // Reconcile the rewritten `<script>` tag's `type`
|
|---|
| 1362 | // attribute with the chunk's actual format. See
|
|---|
| 1363 | // `reconcileScriptTypeAttr` for the rules.
|
|---|
| 1364 | if (
|
|---|
| 1365 | elementName === "script" &&
|
|---|
| 1366 | (kind === "classic" || kind === "esm-script")
|
|---|
| 1367 | ) {
|
|---|
| 1368 | /** @type {AttrToken | undefined} */
|
|---|
| 1369 | let typeAttr;
|
|---|
| 1370 | for (const a of pendingAttributes) {
|
|---|
| 1371 | if (
|
|---|
| 1372 | input.slice(a.nameStart, a.nameEnd).toLowerCase() === "type"
|
|---|
| 1373 | ) {
|
|---|
| 1374 | typeAttr = a;
|
|---|
| 1375 | break;
|
|---|
| 1376 | }
|
|---|
| 1377 | }
|
|---|
| 1378 | reconcileScriptTypeAttr(typeAttr, nameEnd, kind, input);
|
|---|
| 1379 | }
|
|---|
| 1380 | const collection =
|
|---|
| 1381 | kind === "classic"
|
|---|
| 1382 | ? classicEntries
|
|---|
| 1383 | : kind === "esm-script"
|
|---|
| 1384 | ? esmScriptEntries
|
|---|
| 1385 | : kind === "stylesheet"
|
|---|
| 1386 | ? stylesheetEntries
|
|---|
| 1387 | : modulePreloadEntries;
|
|---|
| 1388 | collection.push({ request: value, entryName, kind });
|
|---|
| 1389 | } else {
|
|---|
| 1390 | const dep = new HtmlSourceDependency(value, [
|
|---|
| 1391 | sourceStart,
|
|---|
| 1392 | sourceEnd
|
|---|
| 1393 | ]);
|
|---|
| 1394 | dep.setLoc(sl, sc, el, ec);
|
|---|
| 1395 | module.addDependency(dep);
|
|---|
| 1396 | module.addCodeGenerationDependency(dep);
|
|---|
| 1397 | }
|
|---|
| 1398 | }
|
|---|
| 1399 | }
|
|---|
| 1400 |
|
|---|
| 1401 | // `<script>` is rawtext (the "script data state" in the HTML
|
|---|
| 1402 | // tokenizer): its body must never be reparsed as HTML,
|
|---|
| 1403 | // regardless of whether the tag has a `src` attribute. The
|
|---|
| 1404 | // walker is already in script-data state for `<script>` and
|
|---|
| 1405 | // will emit the matching `closeTag` event itself, so we
|
|---|
| 1406 | // return `end` and defer body extraction to the closeTag
|
|---|
| 1407 | // callback (which gets the spec-correct `</script>` position
|
|---|
| 1408 | // even in escaped/double-escaped script-data sub-states).
|
|---|
| 1409 | // When the tag has no `src` and its body is non-empty, the
|
|---|
| 1410 | // closeTag handler bundles the inline JS as its own entry —
|
|---|
| 1411 | // the same pipeline that processes `<script src>` — by
|
|---|
| 1412 | // issuing a `data:text/javascript;base64,...` virtual request
|
|---|
| 1413 | // and adding a dependency that rewrites the tag to
|
|---|
| 1414 | // `<script src="…">` at render time. Only `<script>` tags
|
|---|
| 1415 | // whose `type` attribute is absent, empty, or a recognized
|
|---|
| 1416 | // JS mimetype are processed as JS; other types (e.g.
|
|---|
| 1417 | // `application/ld+json`, `importmap`) are left untouched.
|
|---|
| 1418 | if (elementName === "script") {
|
|---|
| 1419 | const attrs = getAttributesMap();
|
|---|
| 1420 | // Use attribute presence, not value: a `<script src>` with
|
|---|
| 1421 | // an empty or valueless `src` still ignores its inline
|
|---|
| 1422 | // body in the browser, so we must not bundle the body.
|
|---|
| 1423 | const hasSrc = attrs.has("src");
|
|---|
| 1424 |
|
|---|
| 1425 | /** @type {AttrToken | undefined} */
|
|---|
| 1426 | let typeAttr;
|
|---|
| 1427 | for (const a of pendingAttributes) {
|
|---|
| 1428 | if (input.slice(a.nameStart, a.nameEnd).toLowerCase() === "type") {
|
|---|
| 1429 | typeAttr = a;
|
|---|
| 1430 | break;
|
|---|
| 1431 | }
|
|---|
| 1432 | }
|
|---|
| 1433 | pendingAttributes.length = 0;
|
|---|
| 1434 |
|
|---|
| 1435 | if (hasSrc || !isExecutableJsScript(attrs)) {
|
|---|
| 1436 | // `<script src>` body is ignored by the browser, and
|
|---|
| 1437 | // non-JS `<script type>` (e.g. importmap, JSON-LD)
|
|---|
| 1438 | // passes through unchanged. Either way the walker
|
|---|
| 1439 | // consumes the body and emits the close tag itself.
|
|---|
| 1440 | return end;
|
|---|
| 1441 | }
|
|---|
| 1442 |
|
|---|
| 1443 | pendingInlineScript = {
|
|---|
| 1444 | contentStart: end,
|
|---|
| 1445 | attrs,
|
|---|
| 1446 | typeAttr,
|
|---|
| 1447 | nameEnd
|
|---|
| 1448 | };
|
|---|
| 1449 | return end;
|
|---|
| 1450 | }
|
|---|
| 1451 |
|
|---|
| 1452 | pendingAttributes.length = 0;
|
|---|
| 1453 | return end;
|
|---|
| 1454 | }
|
|---|
| 1455 | });
|
|---|
| 1456 |
|
|---|
| 1457 | const buildInfo = /** @type {BuildInfo} */ (module.buildInfo);
|
|---|
| 1458 | buildInfo.strict = true;
|
|---|
| 1459 | // Hand off the collected entries to HtmlModulesPlugin; it creates the
|
|---|
| 1460 | // real compilation entries during the finishMake hook. The classic
|
|---|
| 1461 | // and esm-script groups are chained via a leader-only dependOn so
|
|---|
| 1462 | // they share a runtime; modulepreload entries are emitted as
|
|---|
| 1463 | // independent entries since `<link rel=modulepreload>` must preload
|
|---|
| 1464 | // without running.
|
|---|
| 1465 | if (
|
|---|
| 1466 | classicEntries.length > 0 ||
|
|---|
| 1467 | esmScriptEntries.length > 0 ||
|
|---|
| 1468 | modulePreloadEntries.length > 0 ||
|
|---|
| 1469 | stylesheetEntries.length > 0
|
|---|
| 1470 | ) {
|
|---|
| 1471 | /** @type {Record<string, EntryScriptInfo[]>} */
|
|---|
| 1472 | (buildInfo.htmlEntryScripts) = {
|
|---|
| 1473 | classic: classicEntries,
|
|---|
| 1474 | "esm-script": esmScriptEntries,
|
|---|
| 1475 | modulepreload: modulePreloadEntries,
|
|---|
| 1476 | stylesheet: stylesheetEntries
|
|---|
| 1477 | };
|
|---|
| 1478 | }
|
|---|
| 1479 |
|
|---|
| 1480 | const buildMeta = /** @type {BuildMeta} */ (state.module.buildMeta);
|
|---|
| 1481 | buildMeta.exportsType = "default";
|
|---|
| 1482 |
|
|---|
| 1483 | state.module.addDependency(new StaticExportsDependency(["default"], true));
|
|---|
| 1484 |
|
|---|
| 1485 | return state;
|
|---|
| 1486 | }
|
|---|
| 1487 | }
|
|---|
| 1488 |
|
|---|
| 1489 | module.exports = HtmlParser;
|
|---|