GetChunkFilenameRuntimeModule.js 9.1 KB

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