/* MIT License http://www.opensource.org/licenses/mit-license.php */ "use strict"; const { CSS_IMPORT_TYPE, CSS_TYPE, JAVASCRIPT_TYPE } = require("../ModuleSourceTypeConstants"); const HtmlGenerator = require("../html/HtmlGenerator"); const makeSerializable = require("../util/makeSerializable"); const ModuleDependency = require("./ModuleDependency"); /** @typedef {import("webpack-sources").ReplaceSource} ReplaceSource */ /** @typedef {import("../Chunk")} Chunk */ /** @typedef {import("../ChunkGraph")} ChunkGraph */ /** @typedef {import("../Dependency")} Dependency */ /** @typedef {import("../DependencyTemplate").DependencyTemplateContext} DependencyTemplateContext */ /** @typedef {import("../Entrypoint")} Entrypoint */ /** @typedef {import("../javascript/JavascriptParser").Range} Range */ /** @typedef {import("../serialization/ObjectMiddleware").ObjectDeserializerContext} ObjectDeserializerContext */ /** @typedef {import("../serialization/ObjectMiddleware").ObjectSerializerContext} ObjectSerializerContext */ /** @typedef {"script-classic" | "script-module" | "modulepreload" | "stylesheet"} HtmlScriptElementKind */ class HtmlScriptSrcDependency extends ModuleDependency { /** * Creates an instance of HtmlScriptSrcDependency. * @param {string} request request * @param {Range} range range of the attribute value in the source * @param {string} entryName name of the entry this script src is bundled into * @param {string=} category dependency category used for resolving and grouping * @param {HtmlScriptElementKind=} elementKind shape of the originating HTML element; used when expanding sibling tags for split/runtime chunks * @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 * @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 */ constructor( request, range, entryName, category, elementKind, tagStart, tagOpenEnd ) { super(request); this.range = range; this.entryName = entryName; /** @type {string} */ this._category = category || "commonjs"; /** @type {HtmlScriptElementKind} */ this.elementKind = elementKind || "script-classic"; /** @type {number} */ this.tagStart = tagStart === undefined ? -1 : tagStart; /** @type {number} */ this.tagOpenEnd = tagOpenEnd === undefined ? -1 : tagOpenEnd; } get type() { return "html script src"; } get category() { return this._category; } /** * Serializes this instance into the provided serializer context. * @param {ObjectSerializerContext} context context */ serialize(context) { const { write } = context; write(this.entryName); write(this._category); write(this.elementKind); write(this.tagStart); write(this.tagOpenEnd); super.serialize(context); } /** * Restores this instance from the provided deserializer context. * @param {ObjectDeserializerContext} context context */ deserialize(context) { const { read } = context; this.entryName = read(); this._category = read(); this.elementKind = read(); this.tagStart = read(); this.tagOpenEnd = read(); super.deserialize(context); } } /** * @param {Entrypoint} entrypoint entrypoint * @returns {Chunk[]} every chunk this entrypoint needs in load order: the * runtime chunk first (when `optimization.runtimeChunk` splits it off), then * any intermediate chunks (e.g. from `optimization.splitChunks`), and finally * the entry chunk itself. The entry chunk is always returned last so callers * can identify it as the tag whose `src`/`href` attribute is being rewritten * in place. Chunks that are already loaded by an ancestor (`dependOn`) entry's * own script tag — i.e. the parent entrypoint's entry chunk *and* its runtime * chunk — are skipped, otherwise they would be loaded twice when the same * HTML contains tags for both the leader and the dependant entries. */ const getEntrypointChunksInLoadOrder = (entrypoint) => { const entryChunk = /** @type {Chunk} */ (entrypoint.getEntrypointChunk()); const runtimeChunk = entrypoint.getRuntimeChunk(); /** @type {Set} */ const chunksLoadedByAncestorTags = new Set(); /** @type {Set} */ const visitedGroups = new Set(); const walk = (/** @type {import("../ChunkGroup")} */ group) => { if (visitedGroups.has(group)) return; visitedGroups.add(group); for (const parent of group.parentsIterable) { if ( typeof (/** @type {Entrypoint} */ (parent).getEntrypointChunk) === "function" ) { const parentEntry = /** @type {Entrypoint} */ (parent).getEntrypointChunk(); if (parentEntry) chunksLoadedByAncestorTags.add(parentEntry); const parentRuntime = /** @type {Entrypoint} */ (parent).getRuntimeChunk(); if (parentRuntime) chunksLoadedByAncestorTags.add(parentRuntime); } walk(parent); } }; walk(entrypoint); /** @type {Chunk[]} */ const ordered = []; /** @type {Set} */ const seen = new Set(); const push = (/** @type {Chunk | null | undefined} */ chunk) => { if (!chunk || seen.has(chunk) || chunk === entryChunk) return; if (chunksLoadedByAncestorTags.has(chunk)) return; seen.add(chunk); ordered.push(chunk); }; if (runtimeChunk !== entryChunk) { push(runtimeChunk); } for (const chunk of entrypoint.chunks) { push(chunk); } ordered.push(entryChunk); return ordered; }; /** * Whether webpack will emit a `.js` file for this chunk that must be * loaded with a `` for script tags) */ const cloneTagWithUrl = ( originalTag, srcStartInTag, srcEndInTag, newUrl, elementKind ) => { let body = originalTag.slice(0, srcStartInTag) + newUrl + originalTag.slice(srcEndInTag); // Strip dangerous-to-copy attributes from the cloned tag — currently // just `integrity`. The match handles all three quoting styles // (`"…"`, `'…'`, unquoted) and the bare-attribute form. body = body.replace( /\s+integrity(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+))?(?=[\s/>])/gi, "" ); if (elementKind === "script-module") { if (/\stype\s*=/i.test(body)) { body = body.replace( /(\stype\s*=\s*)(?:"[^"]*"|'[^']*'|[^\s>]+)/i, '$1"module"' ); } else { body = body.replace(/^` is a void element — no closing tag. ``. return elementKind === "modulepreload" || elementKind === "stylesheet" ? body : `${body}`; }; HtmlScriptSrcDependency.Template = class HtmlScriptSrcDependencyTemplate extends ( ModuleDependency.Template ) { /** * Applies the plugin by registering its hooks on the compiler. * @param {Dependency} dependency the dependency for which the template should be applied * @param {ReplaceSource} source the current replace source which can be modified * @param {DependencyTemplateContext} templateContext the context object * @returns {void} */ apply(dependency, source, templateContext) { const { runtimeTemplate } = templateContext; const dep = /** @type {HtmlScriptSrcDependency} */ (dependency); const compilation = runtimeTemplate.compilation; const { chunkGraph } = compilation; const entrypoint = /** @type {Entrypoint | undefined} */ ( compilation.entrypoints.get(dep.entryName) ); if (!entrypoint) { source.replace(dep.range[0], dep.range[1] - 1, "data:,"); return; } const orderedChunks = getEntrypointChunksInLoadOrder(entrypoint); const entryChunk = orderedChunks[orderedChunks.length - 1]; const isStylesheet = dep.elementKind === "stylesheet"; // Rewrite src/href to a chunk-URL sentinel (resolved by renderManifest): // `.css` for ``, `.js` for everything else. const entryContentHashType = isStylesheet ? "css" : "javascript"; const entryUrl = HtmlGenerator.makeChunkUrlSentinel( entryChunk, entryContentHashType ); source.replace(dep.range[0], dep.range[1] - 1, entryUrl); if (dep.tagStart < 0 || dep.tagOpenEnd <= dep.tagStart) { return; } // The browser must load every chunk the entry needs, not just the // entry chunk. For `