GetChunkFilenameRuntimeModule.js 9.3 KB

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