| 1 | /*
|
|---|
| 2 | MIT License http://www.opensource.org/licenses/mit-license.php
|
|---|
| 3 | */
|
|---|
| 4 |
|
|---|
| 5 | "use strict";
|
|---|
| 6 |
|
|---|
| 7 | const {
|
|---|
| 8 | CSS_IMPORT_TYPE,
|
|---|
| 9 | CSS_TYPE,
|
|---|
| 10 | JAVASCRIPT_TYPE
|
|---|
| 11 | } = require("../ModuleSourceTypeConstants");
|
|---|
| 12 | const makeSerializable = require("../util/makeSerializable");
|
|---|
| 13 | const CssUrlDependency = require("./CssUrlDependency");
|
|---|
| 14 | const ModuleDependency = require("./ModuleDependency");
|
|---|
| 15 |
|
|---|
| 16 | /** @typedef {import("webpack-sources").ReplaceSource} ReplaceSource */
|
|---|
| 17 | /** @typedef {import("../Chunk")} Chunk */
|
|---|
| 18 | /** @typedef {import("../ChunkGraph")} ChunkGraph */
|
|---|
| 19 | /** @typedef {import("../Dependency")} Dependency */
|
|---|
| 20 | /** @typedef {import("../DependencyTemplate").DependencyTemplateContext} DependencyTemplateContext */
|
|---|
| 21 | /** @typedef {import("../Entrypoint")} Entrypoint */
|
|---|
| 22 | /** @typedef {import("../javascript/JavascriptParser").Range} Range */
|
|---|
| 23 | /** @typedef {import("../serialization/ObjectMiddleware").ObjectDeserializerContext} ObjectDeserializerContext */
|
|---|
| 24 | /** @typedef {import("../serialization/ObjectMiddleware").ObjectSerializerContext} ObjectSerializerContext */
|
|---|
| 25 |
|
|---|
| 26 | /** @typedef {"script-classic" | "script-module" | "modulepreload" | "stylesheet"} HtmlScriptElementKind */
|
|---|
| 27 |
|
|---|
| 28 | class HtmlScriptSrcDependency extends ModuleDependency {
|
|---|
| 29 | /**
|
|---|
| 30 | * Creates an instance of HtmlScriptSrcDependency.
|
|---|
| 31 | * @param {string} request request
|
|---|
| 32 | * @param {Range} range range of the attribute value in the source
|
|---|
| 33 | * @param {string} entryName name of the entry this script src is bundled into
|
|---|
| 34 | * @param {string=} category dependency category used for resolving and grouping
|
|---|
| 35 | * @param {HtmlScriptElementKind=} elementKind shape of the originating HTML element; used when expanding sibling tags for split/runtime chunks
|
|---|
| 36 | * @param {number=} tagStart position of the opening `<` of the originating tag in the source; sibling tags emitted for additional entry chunks are inserted right before this
|
|---|
| 37 | * @param {number=} tagOpenEnd position of the character immediately after the opening tag's `>` in the source; combined with `tagStart` this lets the template clone the original opening tag verbatim (preserving attributes like `nonce`, `crossorigin`, `referrerpolicy`, `defer`, `async`) when generating sibling tags
|
|---|
| 38 | */
|
|---|
| 39 | constructor(
|
|---|
| 40 | request,
|
|---|
| 41 | range,
|
|---|
| 42 | entryName,
|
|---|
| 43 | category,
|
|---|
| 44 | elementKind,
|
|---|
| 45 | tagStart,
|
|---|
| 46 | tagOpenEnd
|
|---|
| 47 | ) {
|
|---|
| 48 | super(request);
|
|---|
| 49 | this.range = range;
|
|---|
| 50 | this.entryName = entryName;
|
|---|
| 51 | /** @type {string} */
|
|---|
| 52 | this._category = category || "commonjs";
|
|---|
| 53 | /** @type {HtmlScriptElementKind} */
|
|---|
| 54 | this.elementKind = elementKind || "script-classic";
|
|---|
| 55 | /** @type {number} */
|
|---|
| 56 | this.tagStart = tagStart === undefined ? -1 : tagStart;
|
|---|
| 57 | /** @type {number} */
|
|---|
| 58 | this.tagOpenEnd = tagOpenEnd === undefined ? -1 : tagOpenEnd;
|
|---|
| 59 | }
|
|---|
| 60 |
|
|---|
| 61 | get type() {
|
|---|
| 62 | return "html script src";
|
|---|
| 63 | }
|
|---|
| 64 |
|
|---|
| 65 | get category() {
|
|---|
| 66 | return this._category;
|
|---|
| 67 | }
|
|---|
| 68 |
|
|---|
| 69 | /**
|
|---|
| 70 | * Serializes this instance into the provided serializer context.
|
|---|
| 71 | * @param {ObjectSerializerContext} context context
|
|---|
| 72 | */
|
|---|
| 73 | serialize(context) {
|
|---|
| 74 | const { write } = context;
|
|---|
| 75 | write(this.entryName);
|
|---|
| 76 | write(this._category);
|
|---|
| 77 | write(this.elementKind);
|
|---|
| 78 | write(this.tagStart);
|
|---|
| 79 | write(this.tagOpenEnd);
|
|---|
| 80 | super.serialize(context);
|
|---|
| 81 | }
|
|---|
| 82 |
|
|---|
| 83 | /**
|
|---|
| 84 | * Restores this instance from the provided deserializer context.
|
|---|
| 85 | * @param {ObjectDeserializerContext} context context
|
|---|
| 86 | */
|
|---|
| 87 | deserialize(context) {
|
|---|
| 88 | const { read } = context;
|
|---|
| 89 | this.entryName = read();
|
|---|
| 90 | this._category = read();
|
|---|
| 91 | this.elementKind = read();
|
|---|
| 92 | this.tagStart = read();
|
|---|
| 93 | this.tagOpenEnd = read();
|
|---|
| 94 | super.deserialize(context);
|
|---|
| 95 | }
|
|---|
| 96 | }
|
|---|
| 97 |
|
|---|
| 98 | /**
|
|---|
| 99 | * @param {Chunk} chunk a chunk
|
|---|
| 100 | * @param {import("../Compilation")} compilation compilation
|
|---|
| 101 | * @param {"javascript" | "css"} contentHashType which content hash to plug into the filename template
|
|---|
| 102 | * @returns {string} chunk filename path (no public-path prefix)
|
|---|
| 103 | */
|
|---|
| 104 | const getChunkFilename = (chunk, compilation, contentHashType) => {
|
|---|
| 105 | const outputOptions = compilation.outputOptions;
|
|---|
| 106 | let filenameTemplate;
|
|---|
| 107 | if (contentHashType === "css") {
|
|---|
| 108 | // For a CSS-typed chunk, use the same template the CSS pipeline
|
|---|
| 109 | // will use when it actually emits the `.css` file, so the `<link
|
|---|
| 110 | // rel="stylesheet" href>` URL we write into the HTML matches the
|
|---|
| 111 | // asset on disk.
|
|---|
| 112 | filenameTemplate =
|
|---|
| 113 | require("../css/CssModulesPlugin").getChunkFilenameTemplate(
|
|---|
| 114 | chunk,
|
|---|
| 115 | outputOptions
|
|---|
| 116 | );
|
|---|
| 117 | } else {
|
|---|
| 118 | filenameTemplate =
|
|---|
| 119 | chunk.filenameTemplate ||
|
|---|
| 120 | (chunk.canBeInitial()
|
|---|
| 121 | ? outputOptions.filename
|
|---|
| 122 | : outputOptions.chunkFilename);
|
|---|
| 123 | }
|
|---|
| 124 |
|
|---|
| 125 | return compilation.getPath(filenameTemplate, {
|
|---|
| 126 | chunk,
|
|---|
| 127 | contentHashType
|
|---|
| 128 | });
|
|---|
| 129 | };
|
|---|
| 130 |
|
|---|
| 131 | /**
|
|---|
| 132 | * @param {Entrypoint} entrypoint entrypoint
|
|---|
| 133 | * @returns {Chunk[]} every chunk this entrypoint needs in load order: the
|
|---|
| 134 | * runtime chunk first (when `optimization.runtimeChunk` splits it off), then
|
|---|
| 135 | * any intermediate chunks (e.g. from `optimization.splitChunks`), and finally
|
|---|
| 136 | * the entry chunk itself. The entry chunk is always returned last so callers
|
|---|
| 137 | * can identify it as the tag whose `src`/`href` attribute is being rewritten
|
|---|
| 138 | * in place. Chunks that are already loaded by an ancestor (`dependOn`) entry's
|
|---|
| 139 | * own script tag — i.e. the parent entrypoint's entry chunk *and* its runtime
|
|---|
| 140 | * chunk — are skipped, otherwise they would be loaded twice when the same
|
|---|
| 141 | * HTML contains tags for both the leader and the dependant entries.
|
|---|
| 142 | */
|
|---|
| 143 | const getEntrypointChunksInLoadOrder = (entrypoint) => {
|
|---|
| 144 | const entryChunk = /** @type {Chunk} */ (entrypoint.getEntrypointChunk());
|
|---|
| 145 | const runtimeChunk = entrypoint.getRuntimeChunk();
|
|---|
| 146 |
|
|---|
| 147 | /** @type {Set<Chunk>} */
|
|---|
| 148 | const chunksLoadedByAncestorTags = new Set();
|
|---|
| 149 | /** @type {Set<import("../ChunkGroup")>} */
|
|---|
| 150 | const visitedGroups = new Set();
|
|---|
| 151 | const walk = (/** @type {import("../ChunkGroup")} */ group) => {
|
|---|
| 152 | if (visitedGroups.has(group)) return;
|
|---|
| 153 | visitedGroups.add(group);
|
|---|
| 154 | for (const parent of group.parentsIterable) {
|
|---|
| 155 | if (
|
|---|
| 156 | typeof (/** @type {Entrypoint} */ (parent).getEntrypointChunk) ===
|
|---|
| 157 | "function"
|
|---|
| 158 | ) {
|
|---|
| 159 | const parentEntry =
|
|---|
| 160 | /** @type {Entrypoint} */
|
|---|
| 161 | (parent).getEntrypointChunk();
|
|---|
| 162 | if (parentEntry) chunksLoadedByAncestorTags.add(parentEntry);
|
|---|
| 163 | const parentRuntime =
|
|---|
| 164 | /** @type {Entrypoint} */
|
|---|
| 165 | (parent).getRuntimeChunk();
|
|---|
| 166 | if (parentRuntime) chunksLoadedByAncestorTags.add(parentRuntime);
|
|---|
| 167 | }
|
|---|
| 168 | walk(parent);
|
|---|
| 169 | }
|
|---|
| 170 | };
|
|---|
| 171 | walk(entrypoint);
|
|---|
| 172 |
|
|---|
| 173 | /** @type {Chunk[]} */
|
|---|
| 174 | const ordered = [];
|
|---|
| 175 | /** @type {Set<Chunk>} */
|
|---|
| 176 | const seen = new Set();
|
|---|
| 177 | const push = (/** @type {Chunk | null | undefined} */ chunk) => {
|
|---|
| 178 | if (!chunk || seen.has(chunk) || chunk === entryChunk) return;
|
|---|
| 179 | if (chunksLoadedByAncestorTags.has(chunk)) return;
|
|---|
| 180 | seen.add(chunk);
|
|---|
| 181 | ordered.push(chunk);
|
|---|
| 182 | };
|
|---|
| 183 | if (runtimeChunk !== entryChunk) {
|
|---|
| 184 | push(runtimeChunk);
|
|---|
| 185 | }
|
|---|
| 186 | for (const chunk of entrypoint.chunks) {
|
|---|
| 187 | push(chunk);
|
|---|
| 188 | }
|
|---|
| 189 | ordered.push(entryChunk);
|
|---|
| 190 | return ordered;
|
|---|
| 191 | };
|
|---|
| 192 |
|
|---|
| 193 | /**
|
|---|
| 194 | * Whether webpack will emit a `.js` file for this chunk that must be
|
|---|
| 195 | * loaded with a `<script>` tag. Covers three independent reasons a
|
|---|
| 196 | * chunk needs JS output: it owns one or more JS-source-type modules;
|
|---|
| 197 | * it has entry modules whose source types include JavaScript (entry
|
|---|
| 198 | * modules don't show up in `getChunkModulesIterableBySourceType` until
|
|---|
| 199 | * they're connected as regular modules — this is why
|
|---|
| 200 | * `JavascriptModulesPlugin#_chunkHasJs` checks them separately); or it
|
|---|
| 201 | * is a runtime chunk — `chunk.hasRuntime()` — which produces a `.js`
|
|---|
| 202 | * file holding the webpack runtime, but its `RuntimeModule`s live in
|
|---|
| 203 | * a separate `runtimeModules` set and are *not* surfaced via
|
|---|
| 204 | * `getChunkModulesIterableBySourceType`. Missing the runtime case
|
|---|
| 205 | * would cause a `runtimeChunk`-split chunk to fall out of the
|
|---|
| 206 | * `<script>` list and re-emerge after the chunks that depend on it,
|
|---|
| 207 | * producing `__webpack_require__ is not defined` at load time.
|
|---|
| 208 | * @param {Chunk} chunk chunk
|
|---|
| 209 | * @param {ChunkGraph} chunkGraph chunk graph
|
|---|
| 210 | * @returns {boolean} true if the chunk emits a `.js` file
|
|---|
| 211 | */
|
|---|
| 212 | const chunkHasJs = (chunk, chunkGraph) => {
|
|---|
| 213 | if (chunk.hasRuntime()) return true;
|
|---|
| 214 | if (chunkGraph.getNumberOfEntryModules(chunk) > 0) {
|
|---|
| 215 | for (const module of chunkGraph.getChunkEntryModulesIterable(chunk)) {
|
|---|
| 216 | if (chunkGraph.getModuleSourceTypes(module).has(JAVASCRIPT_TYPE)) {
|
|---|
| 217 | return true;
|
|---|
| 218 | }
|
|---|
| 219 | }
|
|---|
| 220 | }
|
|---|
| 221 | return Boolean(
|
|---|
| 222 | chunkGraph.getChunkModulesIterableBySourceType(chunk, JAVASCRIPT_TYPE)
|
|---|
| 223 | );
|
|---|
| 224 | };
|
|---|
| 225 |
|
|---|
| 226 | /**
|
|---|
| 227 | * Whether webpack will emit a `.css` file for this chunk that must be
|
|---|
| 228 | * loaded with a `<link rel="stylesheet">` tag. Matches
|
|---|
| 229 | * `CssModulesPlugin.chunkHasCss` exactly — both regular CSS modules
|
|---|
| 230 | * and pure `@import` placeholder modules count, since the latter
|
|---|
| 231 | * still contribute a `.css` asset to the chunk.
|
|---|
| 232 | * @param {Chunk} chunk chunk
|
|---|
| 233 | * @param {ChunkGraph} chunkGraph chunk graph
|
|---|
| 234 | * @returns {boolean} true if the chunk emits a `.css` file
|
|---|
| 235 | */
|
|---|
| 236 | const chunkHasCss = (chunk, chunkGraph) =>
|
|---|
| 237 | Boolean(chunkGraph.getChunkModulesIterableBySourceType(chunk, CSS_TYPE)) ||
|
|---|
| 238 | Boolean(
|
|---|
| 239 | chunkGraph.getChunkModulesIterableBySourceType(chunk, CSS_IMPORT_TYPE)
|
|---|
| 240 | );
|
|---|
| 241 |
|
|---|
| 242 | /**
|
|---|
| 243 | * Compare two chunks for a deterministic tie-break in CSS link ordering.
|
|---|
| 244 | * `chunk.name` and `chunk.id` are both stable strings (when present);
|
|---|
| 245 | * one of them is set for every chunk webpack emits. We can't rely on
|
|---|
| 246 | * `Array.prototype.sort` being stable — webpack still supports Node
|
|---|
| 247 | * 10.13 where V8's sort is not guaranteed stable for arrays larger
|
|---|
| 248 | * than ten elements — so any time `firstCssModulePostOrderIndex`
|
|---|
| 249 | * returns the same value for two chunks (most commonly when several
|
|---|
| 250 | * chunks have no reachable CSS module in the entrypoint's dependency
|
|---|
| 251 | * walk and all map to `Infinity`) this comparator picks the canonical
|
|---|
| 252 | * order.
|
|---|
| 253 | * @param {Chunk} a first chunk
|
|---|
| 254 | * @param {Chunk} b second chunk
|
|---|
| 255 | * @returns {-1 | 0 | 1} ordering
|
|---|
| 256 | */
|
|---|
| 257 | const compareChunksForCssTieBreak = (a, b) => {
|
|---|
| 258 | const an = `${a.name || ""} ${a.id === null || a.id === undefined ? "" : a.id}`;
|
|---|
| 259 | const bn = `${b.name || ""} ${b.id === null || b.id === undefined ? "" : b.id}`;
|
|---|
| 260 | if (an < bn) return -1;
|
|---|
| 261 | if (an > bn) return 1;
|
|---|
| 262 | return 0;
|
|---|
| 263 | };
|
|---|
| 264 |
|
|---|
| 265 | /**
|
|---|
| 266 | * Smallest post-order index among the CSS modules of a chunk, taken
|
|---|
| 267 | * from the entrypoint's view of the dependency graph. Used to sort
|
|---|
| 268 | * sibling CSS chunks so they appear in source import order in the
|
|---|
| 269 | * extracted HTML — `entrypoint.chunks` itself does not give that
|
|---|
| 270 | * ordering for arbitrary splitChunks layouts. Considers both
|
|---|
| 271 | * `CSS_TYPE` and `CSS_IMPORT_TYPE` modules so a chunk made up
|
|---|
| 272 | * exclusively of `@import` placeholder modules (e.g. when splitChunks
|
|---|
| 273 | * separates them from their target CSS) still sorts by its true
|
|---|
| 274 | * source position rather than collapsing to `Infinity` and relying on
|
|---|
| 275 | * the chunk-name tie-breaker.
|
|---|
| 276 | * @param {Chunk} chunk chunk
|
|---|
| 277 | * @param {Entrypoint} entrypoint entrypoint the chunk belongs to
|
|---|
| 278 | * @param {ChunkGraph} chunkGraph chunk graph
|
|---|
| 279 | * @returns {number} the lowest post-order index of any CSS or
|
|---|
| 280 | * CSS-import module in the chunk, or `Number.POSITIVE_INFINITY` when
|
|---|
| 281 | * no such module has a defined index (e.g. for a module the
|
|---|
| 282 | * entrypoint never reached on its own dependency walk — runtime-only
|
|---|
| 283 | * modules, modules reached via `dependOn`, etc.) so such chunks sort
|
|---|
| 284 | * last among CSS chunks
|
|---|
| 285 | */
|
|---|
| 286 | const firstCssModulePostOrderIndex = (chunk, entrypoint, chunkGraph) => {
|
|---|
| 287 | let min = Number.POSITIVE_INFINITY;
|
|---|
| 288 | for (const sourceType of [CSS_TYPE, CSS_IMPORT_TYPE]) {
|
|---|
| 289 | const modules = chunkGraph.getChunkModulesIterableBySourceType(
|
|---|
| 290 | chunk,
|
|---|
| 291 | sourceType
|
|---|
| 292 | );
|
|---|
| 293 | if (!modules) continue;
|
|---|
| 294 | for (const module of modules) {
|
|---|
| 295 | const idx = entrypoint.getModulePostOrderIndex(module);
|
|---|
| 296 | if (idx !== undefined && idx < min) min = idx;
|
|---|
| 297 | }
|
|---|
| 298 | }
|
|---|
| 299 | return min;
|
|---|
| 300 | };
|
|---|
| 301 |
|
|---|
| 302 | const COPYABLE_LINK_ATTRS = ["nonce", "crossorigin", "referrerpolicy"];
|
|---|
| 303 |
|
|---|
| 304 | /**
|
|---|
| 305 | * Build a fresh `<link rel="stylesheet" href="…">` for a CSS chunk that
|
|---|
| 306 | * was pulled in by a `<script src>` entry — the originating tag was a
|
|---|
| 307 | * `<script>`, but the chunk is CSS so cloning the script tag verbatim
|
|---|
| 308 | * would produce nonsense (`<script src="…\.css">`). Copy
|
|---|
| 309 | * `nonce`/`crossorigin`/`referrerpolicy` from the original element so
|
|---|
| 310 | * the same CSP and fetch policy applies; `defer`/`async`/`type` have no
|
|---|
| 311 | * meaning on `<link>` and are dropped.
|
|---|
| 312 | * @param {string} originalTag the originating `<script>`/`<link>` tag's source text
|
|---|
| 313 | * @param {string} href URL for the stylesheet
|
|---|
| 314 | * @returns {string} the sibling `<link>` tag's HTML
|
|---|
| 315 | */
|
|---|
| 316 | const buildStylesheetLink = (originalTag, href) => {
|
|---|
| 317 | let extra = "";
|
|---|
| 318 | for (const attr of COPYABLE_LINK_ATTRS) {
|
|---|
| 319 | // Match ` <attr>`, ` <attr>=value`, ` <attr>="value"`, ` <attr>='value'`.
|
|---|
| 320 | const re = new RegExp(
|
|---|
| 321 | `\\s${attr}(?:\\s*=\\s*(?:"[^"]*"|'[^']*'|[^\\s>]+))?(?=[\\s/>])`,
|
|---|
| 322 | "i"
|
|---|
| 323 | );
|
|---|
| 324 | const m = originalTag.match(re);
|
|---|
| 325 | if (m) extra += m[0];
|
|---|
| 326 | }
|
|---|
| 327 | const safeHref = href.replace(/"/g, """);
|
|---|
| 328 | return `<link rel="stylesheet" href="${safeHref}"${extra}>`;
|
|---|
| 329 | };
|
|---|
| 330 |
|
|---|
| 331 | /**
|
|---|
| 332 | * Clone the original `<script>`/`<link>` opening tag with its `src`/`href`
|
|---|
| 333 | * value swapped for a different chunk URL. Reusing the source text verbatim
|
|---|
| 334 | * preserves attributes such as `nonce`, `crossorigin`, `referrerpolicy`,
|
|---|
| 335 | * `defer`, and `async` so the sibling tags load with the same semantics as
|
|---|
| 336 | * the entry tag that's already there. `integrity` is dropped because it's
|
|---|
| 337 | * content-specific. When the original tag was upgraded to a module script
|
|---|
| 338 | * (either by the author or by the `output.module` auto-upgrade in
|
|---|
| 339 | * `HtmlParser`), the sibling is forced to `type="module"` regardless of what
|
|---|
| 340 | * the source originally said.
|
|---|
| 341 | * @param {string} originalTag the opening tag's source text including `>`
|
|---|
| 342 | * @param {number} srcStartInTag offset of the src/href value start within `originalTag`
|
|---|
| 343 | * @param {number} srcEndInTag offset of the src/href value end within `originalTag`
|
|---|
| 344 | * @param {string} newUrl URL to put into the cloned tag's src/href slot
|
|---|
| 345 | * @param {HtmlScriptElementKind} elementKind shape of the originating tag
|
|---|
| 346 | * @returns {string} the sibling tag's HTML (including a closing `</script>` for script tags)
|
|---|
| 347 | */
|
|---|
| 348 | const cloneTagWithUrl = (
|
|---|
| 349 | originalTag,
|
|---|
| 350 | srcStartInTag,
|
|---|
| 351 | srcEndInTag,
|
|---|
| 352 | newUrl,
|
|---|
| 353 | elementKind
|
|---|
| 354 | ) => {
|
|---|
| 355 | let body =
|
|---|
| 356 | originalTag.slice(0, srcStartInTag) +
|
|---|
| 357 | newUrl +
|
|---|
| 358 | originalTag.slice(srcEndInTag);
|
|---|
| 359 |
|
|---|
| 360 | // Strip dangerous-to-copy attributes from the cloned tag — currently
|
|---|
| 361 | // just `integrity`. The match handles all three quoting styles
|
|---|
| 362 | // (`"…"`, `'…'`, unquoted) and the bare-attribute form.
|
|---|
| 363 | body = body.replace(
|
|---|
| 364 | /\s+integrity(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+))?(?=[\s/>])/gi,
|
|---|
| 365 | ""
|
|---|
| 366 | );
|
|---|
| 367 |
|
|---|
| 368 | if (elementKind === "script-module") {
|
|---|
| 369 | if (/\stype\s*=/i.test(body)) {
|
|---|
| 370 | body = body.replace(
|
|---|
| 371 | /(\stype\s*=\s*)(?:"[^"]*"|'[^']*'|[^\s>]+)/i,
|
|---|
| 372 | '$1"module"'
|
|---|
| 373 | );
|
|---|
| 374 | } else {
|
|---|
| 375 | body = body.replace(/^<script\b/i, '<script type="module"');
|
|---|
| 376 | }
|
|---|
| 377 | }
|
|---|
| 378 |
|
|---|
| 379 | // `<link>` is a void element — no closing tag. `<script>` needs `</script>`.
|
|---|
| 380 | return elementKind === "modulepreload" || elementKind === "stylesheet"
|
|---|
| 381 | ? body
|
|---|
| 382 | : `${body}</script>`;
|
|---|
| 383 | };
|
|---|
| 384 |
|
|---|
| 385 | HtmlScriptSrcDependency.Template = class HtmlScriptSrcDependencyTemplate extends (
|
|---|
| 386 | ModuleDependency.Template
|
|---|
| 387 | ) {
|
|---|
| 388 | /**
|
|---|
| 389 | * Applies the plugin by registering its hooks on the compiler.
|
|---|
| 390 | * @param {Dependency} dependency the dependency for which the template should be applied
|
|---|
| 391 | * @param {ReplaceSource} source the current replace source which can be modified
|
|---|
| 392 | * @param {DependencyTemplateContext} templateContext the context object
|
|---|
| 393 | * @returns {void}
|
|---|
| 394 | */
|
|---|
| 395 | apply(dependency, source, templateContext) {
|
|---|
| 396 | const { runtimeTemplate } = templateContext;
|
|---|
| 397 | const dep = /** @type {HtmlScriptSrcDependency} */ (dependency);
|
|---|
| 398 | const compilation = runtimeTemplate.compilation;
|
|---|
| 399 | const { chunkGraph } = compilation;
|
|---|
| 400 | const entrypoint = /** @type {Entrypoint | undefined} */ (
|
|---|
| 401 | compilation.entrypoints.get(dep.entryName)
|
|---|
| 402 | );
|
|---|
| 403 |
|
|---|
| 404 | if (!entrypoint) {
|
|---|
| 405 | source.replace(dep.range[0], dep.range[1] - 1, "data:,");
|
|---|
| 406 | return;
|
|---|
| 407 | }
|
|---|
| 408 |
|
|---|
| 409 | const orderedChunks = getEntrypointChunksInLoadOrder(entrypoint);
|
|---|
| 410 | const entryChunk = orderedChunks[orderedChunks.length - 1];
|
|---|
| 411 | const isStylesheet = dep.elementKind === "stylesheet";
|
|---|
| 412 |
|
|---|
| 413 | // Rewrite the originating tag's src/href to the entry chunk's
|
|---|
| 414 | // primary asset for that element kind: `.css` for
|
|---|
| 415 | // `<link rel="stylesheet">`, `.js` for everything else.
|
|---|
| 416 | const entryContentHashType = isStylesheet ? "css" : "javascript";
|
|---|
| 417 | const entryUrl = `${CssUrlDependency.PUBLIC_PATH_AUTO}${getChunkFilename(
|
|---|
| 418 | entryChunk,
|
|---|
| 419 | compilation,
|
|---|
| 420 | entryContentHashType
|
|---|
| 421 | )}`;
|
|---|
| 422 | source.replace(dep.range[0], dep.range[1] - 1, entryUrl);
|
|---|
| 423 |
|
|---|
| 424 | if (dep.tagStart < 0 || dep.tagOpenEnd <= dep.tagStart) {
|
|---|
| 425 | return;
|
|---|
| 426 | }
|
|---|
| 427 |
|
|---|
| 428 | // The browser must load every chunk the entry needs, not just the
|
|---|
| 429 | // entry chunk. For `<script>` entries that's the JS for sibling
|
|---|
| 430 | // chunks plus — critically — the CSS for any chunk that holds
|
|---|
| 431 | // stylesheets imported transitively from the JS source. Previously
|
|---|
| 432 | // every sibling was cloned as a `<script>` pointing at a `.js`
|
|---|
| 433 | // filename, so CSS chunks ended up as `<script src="foo.css">`
|
|---|
| 434 | // pointing at non-existent `.js` files (the bug in
|
|---|
| 435 | // html-webpack-plugin#1838 / webpack/mini-css-extract-plugin#959,
|
|---|
| 436 | // magnified here because the entry chunk's own CSS was emitted to
|
|---|
| 437 | // disk but never linked from the HTML at all).
|
|---|
| 438 | const originalContent = /** @type {string} */ (source.original().source());
|
|---|
| 439 | const originalTag = originalContent.slice(dep.tagStart, dep.tagOpenEnd);
|
|---|
| 440 | const srcStartInTag = dep.range[0] - dep.tagStart;
|
|---|
| 441 | const srcEndInTag = dep.range[1] - dep.tagStart;
|
|---|
| 442 |
|
|---|
| 443 | /**
|
|---|
| 444 | * @param {Chunk} chunk chunk to emit a sibling tag for
|
|---|
| 445 | * @param {"javascript" | "css"} kind content type slice of the chunk to emit
|
|---|
| 446 | * @returns {string} a single sibling tag's HTML
|
|---|
| 447 | */
|
|---|
| 448 | const buildSibling = (chunk, kind) => {
|
|---|
| 449 | const url = `${CssUrlDependency.PUBLIC_PATH_AUTO}${getChunkFilename(
|
|---|
| 450 | chunk,
|
|---|
| 451 | compilation,
|
|---|
| 452 | kind
|
|---|
| 453 | )}`;
|
|---|
| 454 | if (kind === "css" && !isStylesheet) {
|
|---|
| 455 | // Originating tag is `<script>` (or `<link rel=modulepreload>`)
|
|---|
| 456 | // but this chunk is CSS — emit a fresh `<link>` rather than
|
|---|
| 457 | // cloning the script.
|
|---|
| 458 | return buildStylesheetLink(originalTag, url);
|
|---|
| 459 | }
|
|---|
| 460 | return cloneTagWithUrl(
|
|---|
| 461 | originalTag,
|
|---|
| 462 | srcStartInTag,
|
|---|
| 463 | srcEndInTag,
|
|---|
| 464 | url,
|
|---|
| 465 | dep.elementKind
|
|---|
| 466 | );
|
|---|
| 467 | };
|
|---|
| 468 |
|
|---|
| 469 | const siblings = [];
|
|---|
| 470 |
|
|---|
| 471 | if (isStylesheet) {
|
|---|
| 472 | // `<link rel="stylesheet">` entries are CSS-only — every sibling
|
|---|
| 473 | // chunk in the entrypoint is also CSS. Keep cloning the original
|
|---|
| 474 | // `<link>` for them so attributes like `media` carry over.
|
|---|
| 475 | for (let i = 0; i < orderedChunks.length - 1; i++) {
|
|---|
| 476 | siblings.push(buildSibling(orderedChunks[i], "css"));
|
|---|
| 477 | }
|
|---|
| 478 | } else {
|
|---|
| 479 | // CSS chunks are emitted before JS chunks so the cascade is set
|
|---|
| 480 | // up before any script runs. Within CSS the order needs to match
|
|---|
| 481 | // the source's import order — `entrypoint.chunks` alone doesn't
|
|---|
| 482 | // give us that for arbitrary splitChunks layouts (splitChunks
|
|---|
| 483 | // inserts each new chunk before the entry chunk via
|
|---|
| 484 | // `insertChunk(_, before)`, so split CSS siblings end up in
|
|---|
| 485 | // *reverse* of the order they were processed — exactly the
|
|---|
| 486 | // html-webpack-plugin#1838 / mini-css-extract#959 symptom). We
|
|---|
| 487 | // re-derive the order from the entrypoint's module post-order
|
|---|
| 488 | // index, which mirrors the dependency walk and so reflects the
|
|---|
| 489 | // import order.
|
|---|
| 490 | /** @type {{ chunk: Chunk, index: number }[]} */
|
|---|
| 491 | const cssChunkOrder = [];
|
|---|
| 492 | /** @type {Chunk[]} */
|
|---|
| 493 | const jsChunks = [];
|
|---|
| 494 | for (let i = 0; i < orderedChunks.length - 1; i++) {
|
|---|
| 495 | const chunk = orderedChunks[i];
|
|---|
| 496 | const hasCss = chunkHasCss(chunk, chunkGraph);
|
|---|
| 497 | const hasJs = chunkHasJs(chunk, chunkGraph);
|
|---|
| 498 | if (hasCss) {
|
|---|
| 499 | cssChunkOrder.push({
|
|---|
| 500 | chunk,
|
|---|
| 501 | index: firstCssModulePostOrderIndex(chunk, entrypoint, chunkGraph)
|
|---|
| 502 | });
|
|---|
| 503 | }
|
|---|
| 504 | // Anything that isn't CSS-only stays on the JS lane, in the
|
|---|
| 505 | // `orderedChunks` order — that preserves the runtime-first /
|
|---|
| 506 | // vendor-before-entry invariant of `getEntrypointChunksInLoadOrder`.
|
|---|
| 507 | // Chunks that produce no `.js` and no `.css` (e.g. wasm-only
|
|---|
| 508 | // or asset-only) still get a `<script>` clone here so we
|
|---|
| 509 | // keep prior behavior for users who relied on it.
|
|---|
| 510 | if (hasJs || !hasCss) jsChunks.push(chunk);
|
|---|
| 511 | }
|
|---|
| 512 | // If the entry chunk itself contains CSS (entry JS imports CSS
|
|---|
| 513 | // without splitChunks separating it), fold it into the same CSS
|
|---|
| 514 | // ordering so the entry-chunk `<link>` lands in the correct
|
|---|
| 515 | // cascade position relative to sibling CSS chunks.
|
|---|
| 516 | if (chunkHasCss(entryChunk, chunkGraph)) {
|
|---|
| 517 | cssChunkOrder.push({
|
|---|
| 518 | chunk: entryChunk,
|
|---|
| 519 | index: firstCssModulePostOrderIndex(
|
|---|
| 520 | entryChunk,
|
|---|
| 521 | entrypoint,
|
|---|
| 522 | chunkGraph
|
|---|
| 523 | )
|
|---|
| 524 | });
|
|---|
| 525 | }
|
|---|
| 526 | cssChunkOrder.sort((a, b) => {
|
|---|
| 527 | // Direct subtraction would yield `NaN` when both indices are
|
|---|
| 528 | // `Infinity` (the documented fallback for chunks whose CSS
|
|---|
| 529 | // modules the entrypoint's walk never reaches), and
|
|---|
| 530 | // `Array#sort` doesn't promise stable ordering on the legacy
|
|---|
| 531 | // Node 10 targets this repo still supports — so the
|
|---|
| 532 | // tie-breaker must always run when the indices match,
|
|---|
| 533 | // including the `Infinity === Infinity` case.
|
|---|
| 534 | if (a.index < b.index) return -1;
|
|---|
| 535 | if (a.index > b.index) return 1;
|
|---|
| 536 | return compareChunksForCssTieBreak(a.chunk, b.chunk);
|
|---|
| 537 | });
|
|---|
| 538 | for (const { chunk } of cssChunkOrder) {
|
|---|
| 539 | siblings.push(buildSibling(chunk, "css"));
|
|---|
| 540 | }
|
|---|
| 541 | for (const chunk of jsChunks) {
|
|---|
| 542 | siblings.push(buildSibling(chunk, "javascript"));
|
|---|
| 543 | }
|
|---|
| 544 | }
|
|---|
| 545 |
|
|---|
| 546 | if (siblings.length > 0) {
|
|---|
| 547 | source.insert(dep.tagStart, siblings.join(""));
|
|---|
| 548 | }
|
|---|
| 549 | }
|
|---|
| 550 | };
|
|---|
| 551 |
|
|---|
| 552 | makeSerializable(
|
|---|
| 553 | HtmlScriptSrcDependency,
|
|---|
| 554 | "webpack/lib/dependencies/HtmlScriptSrcDependency"
|
|---|
| 555 | );
|
|---|
| 556 |
|
|---|
| 557 | module.exports = HtmlScriptSrcDependency;
|
|---|