TemplatedPathPlugin.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Jason Anderson @diurnalist
  4. */
  5. "use strict";
  6. const REGEXP_HASH = /\[hash(?::(\d+))?\]/gi,
  7. REGEXP_CHUNKHASH = /\[chunkhash(?::(\d+))?\]/gi,
  8. REGEXP_MODULEHASH = /\[modulehash(?::(\d+))?\]/gi,
  9. REGEXP_CONTENTHASH = /\[contenthash(?::(\d+))?\]/gi,
  10. REGEXP_NAME = /\[name\]/gi,
  11. REGEXP_ID = /\[id\]/gi,
  12. REGEXP_MODULEID = /\[moduleid\]/gi,
  13. REGEXP_FILE = /\[file\]/gi,
  14. REGEXP_QUERY = /\[query\]/gi,
  15. REGEXP_FILEBASE = /\[filebase\]/gi,
  16. REGEXP_URL = /\[url\]/gi;
  17. // Using global RegExp for .test is dangerous
  18. // We use a normal RegExp instead of .test
  19. const REGEXP_HASH_FOR_TEST = new RegExp(REGEXP_HASH.source, "i"),
  20. REGEXP_CHUNKHASH_FOR_TEST = new RegExp(REGEXP_CHUNKHASH.source, "i"),
  21. REGEXP_CONTENTHASH_FOR_TEST = new RegExp(REGEXP_CONTENTHASH.source, "i"),
  22. REGEXP_NAME_FOR_TEST = new RegExp(REGEXP_NAME.source, "i");
  23. const withHashLength = (replacer, handlerFn, assetInfo) => {
  24. const fn = (match, hashLength, ...args) => {
  25. if (assetInfo) assetInfo.immutable = true;
  26. const length = hashLength && parseInt(hashLength, 10);
  27. if (length && handlerFn) {
  28. return handlerFn(length);
  29. }
  30. const hash = replacer(match, hashLength, ...args);
  31. return length ? hash.slice(0, length) : hash;
  32. };
  33. return fn;
  34. };
  35. const getReplacer = (value, allowEmpty) => {
  36. const fn = (match, ...args) => {
  37. // last argument in replacer is the entire input string
  38. const input = args[args.length - 1];
  39. if (value === null || value === undefined) {
  40. if (!allowEmpty) {
  41. throw new Error(
  42. `Path variable ${match} not implemented in this context: ${input}`
  43. );
  44. }
  45. return "";
  46. } else {
  47. return `${escapePathVariables(value)}`;
  48. }
  49. };
  50. return fn;
  51. };
  52. const escapePathVariables = value => {
  53. return typeof value === "string"
  54. ? value.replace(/\[(\\*[\w:]+\\*)\]/gi, "[\\$1\\]")
  55. : value;
  56. };
  57. const replacePathVariables = (path, data, assetInfo) => {
  58. const chunk = data.chunk;
  59. const chunkId = chunk && chunk.id;
  60. const chunkName = chunk && (chunk.name || chunk.id);
  61. const chunkHash = chunk && (chunk.renderedHash || chunk.hash);
  62. const chunkHashWithLength = chunk && chunk.hashWithLength;
  63. const contentHashType = data.contentHashType;
  64. const contentHash =
  65. (chunk && chunk.contentHash && chunk.contentHash[contentHashType]) ||
  66. data.contentHash;
  67. const contentHashWithLength =
  68. (chunk &&
  69. chunk.contentHashWithLength &&
  70. chunk.contentHashWithLength[contentHashType]) ||
  71. data.contentHashWithLength;
  72. const module = data.module;
  73. const moduleId = module && module.id;
  74. const moduleHash = module && (module.renderedHash || module.hash);
  75. const moduleHashWithLength = module && module.hashWithLength;
  76. if (typeof path === "function") {
  77. path = path(data);
  78. }
  79. if (
  80. data.noChunkHash &&
  81. (REGEXP_CHUNKHASH_FOR_TEST.test(path) ||
  82. REGEXP_CONTENTHASH_FOR_TEST.test(path))
  83. ) {
  84. throw new Error(
  85. `Cannot use [chunkhash] or [contenthash] for chunk in '${path}' (use [hash] instead)`
  86. );
  87. }
  88. return (
  89. path
  90. .replace(
  91. REGEXP_HASH,
  92. withHashLength(getReplacer(data.hash), data.hashWithLength, assetInfo)
  93. )
  94. .replace(
  95. REGEXP_CHUNKHASH,
  96. withHashLength(getReplacer(chunkHash), chunkHashWithLength, assetInfo)
  97. )
  98. .replace(
  99. REGEXP_CONTENTHASH,
  100. withHashLength(
  101. getReplacer(contentHash),
  102. contentHashWithLength,
  103. assetInfo
  104. )
  105. )
  106. .replace(
  107. REGEXP_MODULEHASH,
  108. withHashLength(getReplacer(moduleHash), moduleHashWithLength, assetInfo)
  109. )
  110. .replace(REGEXP_ID, getReplacer(chunkId))
  111. .replace(REGEXP_MODULEID, getReplacer(moduleId))
  112. .replace(REGEXP_NAME, getReplacer(chunkName))
  113. .replace(REGEXP_FILE, getReplacer(data.filename))
  114. .replace(REGEXP_FILEBASE, getReplacer(data.basename))
  115. // query is optional, it's OK if it's in a path but there's nothing to replace it with
  116. .replace(REGEXP_QUERY, getReplacer(data.query, true))
  117. // only available in sourceMappingURLComment
  118. .replace(REGEXP_URL, getReplacer(data.url))
  119. .replace(/\[\\(\\*[\w:]+\\*)\\\]/gi, "[$1]")
  120. );
  121. };
  122. class TemplatedPathPlugin {
  123. apply(compiler) {
  124. compiler.hooks.compilation.tap("TemplatedPathPlugin", compilation => {
  125. const mainTemplate = compilation.mainTemplate;
  126. mainTemplate.hooks.assetPath.tap(
  127. "TemplatedPathPlugin",
  128. replacePathVariables
  129. );
  130. mainTemplate.hooks.globalHash.tap(
  131. "TemplatedPathPlugin",
  132. (chunk, paths) => {
  133. const outputOptions = mainTemplate.outputOptions;
  134. const publicPath = outputOptions.publicPath || "";
  135. const filename = outputOptions.filename || "";
  136. const chunkFilename =
  137. outputOptions.chunkFilename || outputOptions.filename;
  138. if (
  139. REGEXP_HASH_FOR_TEST.test(publicPath) ||
  140. REGEXP_CHUNKHASH_FOR_TEST.test(publicPath) ||
  141. REGEXP_CONTENTHASH_FOR_TEST.test(publicPath) ||
  142. REGEXP_NAME_FOR_TEST.test(publicPath)
  143. )
  144. return true;
  145. if (REGEXP_HASH_FOR_TEST.test(filename)) return true;
  146. if (REGEXP_HASH_FOR_TEST.test(chunkFilename)) return true;
  147. if (REGEXP_HASH_FOR_TEST.test(paths.join("|"))) return true;
  148. }
  149. );
  150. mainTemplate.hooks.hashForChunk.tap(
  151. "TemplatedPathPlugin",
  152. (hash, chunk) => {
  153. const outputOptions = mainTemplate.outputOptions;
  154. const chunkFilename =
  155. outputOptions.chunkFilename || outputOptions.filename;
  156. if (REGEXP_CHUNKHASH_FOR_TEST.test(chunkFilename)) {
  157. hash.update(JSON.stringify(chunk.getChunkMaps(true).hash));
  158. }
  159. if (REGEXP_CONTENTHASH_FOR_TEST.test(chunkFilename)) {
  160. hash.update(
  161. JSON.stringify(
  162. chunk.getChunkMaps(true).contentHash.javascript || {}
  163. )
  164. );
  165. }
  166. if (REGEXP_NAME_FOR_TEST.test(chunkFilename)) {
  167. hash.update(JSON.stringify(chunk.getChunkMaps(true).name));
  168. }
  169. }
  170. );
  171. });
  172. }
  173. }
  174. module.exports = TemplatedPathPlugin;