| 1 | /*
|
|---|
| 2 | MIT License http://www.opensource.org/licenses/mit-license.php
|
|---|
| 3 | */
|
|---|
| 4 |
|
|---|
| 5 | "use strict";
|
|---|
| 6 |
|
|---|
| 7 | const RuntimeGlobals = require("../RuntimeGlobals");
|
|---|
| 8 | const RuntimeModule = require("../RuntimeModule");
|
|---|
| 9 | const Template = require("../Template");
|
|---|
| 10 | const { first } = require("../util/SetHelpers");
|
|---|
| 11 |
|
|---|
| 12 | /** @typedef {import("../Chunk")} Chunk */
|
|---|
| 13 | /** @typedef {import("../Chunk").ChunkId} ChunkId */
|
|---|
| 14 | /** @typedef {import("../ChunkGraph")} ChunkGraph */
|
|---|
| 15 | /** @typedef {import("../Compilation")} Compilation */
|
|---|
| 16 | /** @typedef {import("../Compilation").HashWithLengthFunction} HashWithLengthFunction */
|
|---|
| 17 | /** @typedef {import("../Chunk").ChunkFilenameTemplate} ChunkFilenameTemplate */
|
|---|
| 18 |
|
|---|
| 19 | class GetChunkFilenameRuntimeModule extends RuntimeModule {
|
|---|
| 20 | /**
|
|---|
| 21 | * @param {string} contentType the contentType to use the content hash for
|
|---|
| 22 | * @param {string} name kind of filename
|
|---|
| 23 | * @param {string} global function name to be assigned
|
|---|
| 24 | * @param {(chunk: Chunk) => ChunkFilenameTemplate | false} getFilenameForChunk functor to get the filename or function
|
|---|
| 25 | * @param {boolean} allChunks when false, only async chunks are included
|
|---|
| 26 | */
|
|---|
| 27 | constructor(contentType, name, global, getFilenameForChunk, allChunks) {
|
|---|
| 28 | super(`get ${name} chunk filename`);
|
|---|
| 29 | /** @type {string} */
|
|---|
| 30 | this.contentType = contentType;
|
|---|
| 31 | /** @type {string} */
|
|---|
| 32 | this.global = global;
|
|---|
| 33 | /** @type {(chunk: Chunk) => ChunkFilenameTemplate | false} */
|
|---|
| 34 | this.getFilenameForChunk = getFilenameForChunk;
|
|---|
| 35 | /** @type {boolean} */
|
|---|
| 36 | this.allChunks = allChunks;
|
|---|
| 37 | /** @type {boolean} */
|
|---|
| 38 | this.dependentHash = true;
|
|---|
| 39 | }
|
|---|
| 40 |
|
|---|
| 41 | /**
|
|---|
| 42 | * Generates runtime code for this runtime module.
|
|---|
| 43 | * @returns {string | null} runtime code
|
|---|
| 44 | */
|
|---|
| 45 | generate() {
|
|---|
| 46 | const { global, contentType, getFilenameForChunk, allChunks } = this;
|
|---|
| 47 | const compilation = /** @type {Compilation} */ (this.compilation);
|
|---|
| 48 | const chunkGraph = /** @type {ChunkGraph} */ (this.chunkGraph);
|
|---|
| 49 | const chunk = /** @type {Chunk} */ (this.chunk);
|
|---|
| 50 | const { runtimeTemplate } = compilation;
|
|---|
| 51 |
|
|---|
| 52 | /** @type {Map<ChunkFilenameTemplate, Set<Chunk>>} */
|
|---|
| 53 | const chunkFilenames = new Map();
|
|---|
| 54 | let maxChunks = 0;
|
|---|
| 55 | /** @type {string | undefined} */
|
|---|
| 56 | let dynamicFilename;
|
|---|
| 57 |
|
|---|
| 58 | /**
|
|---|
| 59 | * @param {Chunk} c the chunk
|
|---|
| 60 | * @returns {void}
|
|---|
| 61 | */
|
|---|
| 62 | const addChunk = (c) => {
|
|---|
| 63 | const chunkFilename = getFilenameForChunk(c);
|
|---|
| 64 | if (chunkFilename) {
|
|---|
| 65 | let set = chunkFilenames.get(chunkFilename);
|
|---|
| 66 | if (set === undefined) {
|
|---|
| 67 | chunkFilenames.set(chunkFilename, (set = new Set()));
|
|---|
| 68 | }
|
|---|
| 69 | set.add(c);
|
|---|
| 70 | if (typeof chunkFilename === "string") {
|
|---|
| 71 | if (set.size < maxChunks) return;
|
|---|
| 72 | if (set.size === maxChunks) {
|
|---|
| 73 | if (
|
|---|
| 74 | chunkFilename.length <
|
|---|
| 75 | /** @type {string} */ (dynamicFilename).length
|
|---|
| 76 | ) {
|
|---|
| 77 | return;
|
|---|
| 78 | }
|
|---|
| 79 |
|
|---|
| 80 | if (
|
|---|
| 81 | chunkFilename.length ===
|
|---|
| 82 | /** @type {string} */ (dynamicFilename).length &&
|
|---|
| 83 | chunkFilename < /** @type {string} */ (dynamicFilename)
|
|---|
| 84 | ) {
|
|---|
| 85 | return;
|
|---|
| 86 | }
|
|---|
| 87 | }
|
|---|
| 88 | maxChunks = set.size;
|
|---|
| 89 | dynamicFilename = chunkFilename;
|
|---|
| 90 | }
|
|---|
| 91 | }
|
|---|
| 92 | };
|
|---|
| 93 |
|
|---|
| 94 | /** @type {string[]} */
|
|---|
| 95 | const includedChunksMessages = [];
|
|---|
| 96 | if (allChunks) {
|
|---|
| 97 | includedChunksMessages.push("all chunks");
|
|---|
| 98 | for (const c of chunk.getAllReferencedChunks()) {
|
|---|
| 99 | addChunk(c);
|
|---|
| 100 | }
|
|---|
| 101 | } else {
|
|---|
| 102 | includedChunksMessages.push("async chunks");
|
|---|
| 103 | for (const c of chunk.getAllAsyncChunks()) {
|
|---|
| 104 | addChunk(c);
|
|---|
| 105 | }
|
|---|
| 106 | const includeEntries = chunkGraph
|
|---|
| 107 | .getTreeRuntimeRequirements(chunk)
|
|---|
| 108 | .has(RuntimeGlobals.ensureChunkIncludeEntries);
|
|---|
| 109 | if (includeEntries) {
|
|---|
| 110 | includedChunksMessages.push("chunks that the entrypoint depends on");
|
|---|
| 111 | for (const c of chunkGraph.getRuntimeChunkDependentChunksIterable(
|
|---|
| 112 | chunk
|
|---|
| 113 | )) {
|
|---|
| 114 | addChunk(c);
|
|---|
| 115 | }
|
|---|
| 116 | }
|
|---|
| 117 | }
|
|---|
| 118 | for (const entrypoint of chunk.getAllReferencedAsyncEntrypoints()) {
|
|---|
| 119 | addChunk(entrypoint.chunks[entrypoint.chunks.length - 1]);
|
|---|
| 120 | }
|
|---|
| 121 |
|
|---|
| 122 | /** @type {Map<string, Set<string | number | null>>} */
|
|---|
| 123 | const staticUrls = new Map();
|
|---|
| 124 | /** @type {Set<Chunk>} */
|
|---|
| 125 | const dynamicUrlChunks = new Set();
|
|---|
| 126 |
|
|---|
| 127 | /**
|
|---|
| 128 | * @param {Chunk} c the chunk
|
|---|
| 129 | * @param {ChunkFilenameTemplate} chunkFilename the filename template for the chunk
|
|---|
| 130 | * @returns {void}
|
|---|
| 131 | */
|
|---|
| 132 | const addStaticUrl = (c, chunkFilename) => {
|
|---|
| 133 | /**
|
|---|
| 134 | * @param {ChunkId} value a value
|
|---|
| 135 | * @returns {string} string to put in quotes
|
|---|
| 136 | */
|
|---|
| 137 | const unquotedStringify = (value) => {
|
|---|
| 138 | const str = `${value}`;
|
|---|
| 139 | if (str.length >= 5 && str === `${c.id}`) {
|
|---|
| 140 | // This is shorter and generates the same result
|
|---|
| 141 | return '" + chunkId + "';
|
|---|
| 142 | }
|
|---|
| 143 | const s = JSON.stringify(str);
|
|---|
| 144 | return s.slice(1, -1);
|
|---|
| 145 | };
|
|---|
| 146 | /**
|
|---|
| 147 | * @param {string} value string
|
|---|
| 148 | * @returns {HashWithLengthFunction} string to put in quotes with length
|
|---|
| 149 | */
|
|---|
| 150 | const unquotedStringifyWithLength = (value) => (length) =>
|
|---|
| 151 | unquotedStringify(`${value}`.slice(0, length));
|
|---|
| 152 | const chunkFilenameValue =
|
|---|
| 153 | typeof chunkFilename === "function"
|
|---|
| 154 | ? JSON.stringify(
|
|---|
| 155 | chunkFilename({
|
|---|
| 156 | chunk: c,
|
|---|
| 157 | contentHashType: contentType
|
|---|
| 158 | })
|
|---|
| 159 | )
|
|---|
| 160 | : JSON.stringify(chunkFilename);
|
|---|
| 161 | const staticChunkFilename = compilation.getPath(chunkFilenameValue, {
|
|---|
| 162 | hash: `" + ${RuntimeGlobals.getFullHash}() + "`,
|
|---|
| 163 | hashWithLength: (length) =>
|
|---|
| 164 | `" + ${RuntimeGlobals.getFullHash}().slice(0, ${length}) + "`,
|
|---|
| 165 | chunk: {
|
|---|
| 166 | id: unquotedStringify(/** @type {ChunkId} */ (c.id)),
|
|---|
| 167 | hash: unquotedStringify(/** @type {string} */ (c.renderedHash)),
|
|---|
| 168 | hashWithLength: unquotedStringifyWithLength(
|
|---|
| 169 | /** @type {string} */ (c.renderedHash)
|
|---|
| 170 | ),
|
|---|
| 171 | name: unquotedStringify(c.name || /** @type {ChunkId} */ (c.id)),
|
|---|
| 172 | contentHash: {
|
|---|
| 173 | [contentType]: unquotedStringify(c.contentHash[contentType])
|
|---|
| 174 | },
|
|---|
| 175 | contentHashWithLength: {
|
|---|
| 176 | [contentType]: unquotedStringifyWithLength(
|
|---|
| 177 | c.contentHash[contentType]
|
|---|
| 178 | )
|
|---|
| 179 | }
|
|---|
| 180 | },
|
|---|
| 181 | contentHashType: contentType
|
|---|
| 182 | });
|
|---|
| 183 | let set = staticUrls.get(staticChunkFilename);
|
|---|
| 184 | if (set === undefined) {
|
|---|
| 185 | staticUrls.set(staticChunkFilename, (set = new Set()));
|
|---|
| 186 | }
|
|---|
| 187 | set.add(c.id);
|
|---|
| 188 | };
|
|---|
| 189 |
|
|---|
| 190 | for (const [filename, chunks] of chunkFilenames) {
|
|---|
| 191 | if (filename !== dynamicFilename) {
|
|---|
| 192 | for (const c of chunks) addStaticUrl(c, filename);
|
|---|
| 193 | } else {
|
|---|
| 194 | for (const c of chunks) dynamicUrlChunks.add(c);
|
|---|
| 195 | }
|
|---|
| 196 | }
|
|---|
| 197 |
|
|---|
| 198 | /**
|
|---|
| 199 | * @param {(chunk: Chunk) => string | number} fn function from chunk to value
|
|---|
| 200 | * @returns {string} code with static mapping of results of fn
|
|---|
| 201 | */
|
|---|
| 202 | const createMap = (fn) => {
|
|---|
| 203 | /** @type {Record<ChunkId, ChunkId>} */
|
|---|
| 204 | const obj = {};
|
|---|
| 205 | let useId = false;
|
|---|
| 206 | /** @type {ChunkId | undefined} */
|
|---|
| 207 | let lastKey;
|
|---|
| 208 | let entries = 0;
|
|---|
| 209 | for (const c of dynamicUrlChunks) {
|
|---|
| 210 | const value = fn(c);
|
|---|
| 211 | if (value === c.id) {
|
|---|
| 212 | useId = true;
|
|---|
| 213 | } else {
|
|---|
| 214 | obj[/** @type {ChunkId} */ (c.id)] = value;
|
|---|
| 215 | lastKey = /** @type {ChunkId} */ (c.id);
|
|---|
| 216 | entries++;
|
|---|
| 217 | }
|
|---|
| 218 | }
|
|---|
| 219 | if (entries === 0) return "chunkId";
|
|---|
| 220 | if (entries === 1) {
|
|---|
| 221 | return useId
|
|---|
| 222 | ? `(chunkId === ${JSON.stringify(lastKey)} ? ${JSON.stringify(
|
|---|
| 223 | obj[/** @type {ChunkId} */ (lastKey)]
|
|---|
| 224 | )} : chunkId)`
|
|---|
| 225 | : JSON.stringify(obj[/** @type {ChunkId} */ (lastKey)]);
|
|---|
| 226 | }
|
|---|
| 227 | return useId
|
|---|
| 228 | ? `(${JSON.stringify(obj)}[chunkId] || chunkId)`
|
|---|
| 229 | : `${JSON.stringify(obj)}[chunkId]`;
|
|---|
| 230 | };
|
|---|
| 231 |
|
|---|
| 232 | /**
|
|---|
| 233 | * @param {(chunk: Chunk) => string | number} fn function from chunk to value
|
|---|
| 234 | * @returns {string} code with static mapping of results of fn for including in quoted string
|
|---|
| 235 | */
|
|---|
| 236 | const mapExpr = (fn) => `" + ${createMap(fn)} + "`;
|
|---|
| 237 |
|
|---|
| 238 | /**
|
|---|
| 239 | * @param {(chunk: Chunk) => string | number} fn function from chunk to value
|
|---|
| 240 | * @returns {HashWithLengthFunction} function which generates code with static mapping of results of fn for including in quoted string for specific length
|
|---|
| 241 | */
|
|---|
| 242 | const mapExprWithLength = (fn) => (length) =>
|
|---|
| 243 | `" + ${createMap((c) => `${fn(c)}`.slice(0, length))} + "`;
|
|---|
| 244 |
|
|---|
| 245 | const url =
|
|---|
| 246 | dynamicFilename &&
|
|---|
| 247 | compilation.getPath(JSON.stringify(dynamicFilename), {
|
|---|
| 248 | hash: `" + ${RuntimeGlobals.getFullHash}() + "`,
|
|---|
| 249 | hashWithLength: (length) =>
|
|---|
| 250 | `" + ${RuntimeGlobals.getFullHash}().slice(0, ${length}) + "`,
|
|---|
| 251 | chunk: {
|
|---|
| 252 | id: '" + chunkId + "',
|
|---|
| 253 | hash: mapExpr((c) => /** @type {string} */ (c.renderedHash)),
|
|---|
| 254 | hashWithLength: mapExprWithLength(
|
|---|
| 255 | (c) => /** @type {string} */ (c.renderedHash)
|
|---|
| 256 | ),
|
|---|
| 257 | name: mapExpr((c) => c.name || /** @type {ChunkId} */ (c.id)),
|
|---|
| 258 | contentHash: {
|
|---|
| 259 | [contentType]: mapExpr((c) => c.contentHash[contentType])
|
|---|
| 260 | },
|
|---|
| 261 | contentHashWithLength: {
|
|---|
| 262 | [contentType]: mapExprWithLength((c) => c.contentHash[contentType])
|
|---|
| 263 | }
|
|---|
| 264 | },
|
|---|
| 265 | contentHashType: contentType
|
|---|
| 266 | });
|
|---|
| 267 |
|
|---|
| 268 | return Template.asString([
|
|---|
| 269 | `// This function allow to reference ${includedChunksMessages.join(
|
|---|
| 270 | " and "
|
|---|
| 271 | )}`,
|
|---|
| 272 | `${global} = ${runtimeTemplate.basicFunction(
|
|---|
| 273 | "chunkId",
|
|---|
| 274 |
|
|---|
| 275 | staticUrls.size > 0
|
|---|
| 276 | ? [
|
|---|
| 277 | "// return url for filenames not based on template",
|
|---|
| 278 | // it minimizes to `x===1?"...":x===2?"...":"..."`
|
|---|
| 279 | Template.asString(
|
|---|
| 280 | Array.from(staticUrls, ([url, ids]) => {
|
|---|
| 281 | const condition =
|
|---|
| 282 | ids.size === 1
|
|---|
| 283 | ? `chunkId === ${JSON.stringify(first(ids))}`
|
|---|
| 284 | : `{${Array.from(
|
|---|
| 285 | ids,
|
|---|
| 286 | (id) => `${JSON.stringify(id)}:1`
|
|---|
| 287 | ).join(",")}}[chunkId]`;
|
|---|
| 288 | return `if (${condition}) return ${url};`;
|
|---|
| 289 | })
|
|---|
| 290 | ),
|
|---|
| 291 | "// return url for filenames based on template",
|
|---|
| 292 | `return ${url};`
|
|---|
| 293 | ]
|
|---|
| 294 | : ["// return url for filenames based on template", `return ${url};`]
|
|---|
| 295 | )};`
|
|---|
| 296 | ]);
|
|---|
| 297 | }
|
|---|
| 298 | }
|
|---|
| 299 |
|
|---|
| 300 | module.exports = GetChunkFilenameRuntimeModule;
|
|---|