TemplatedPathPlugin.js 5.6 KB

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