TemplatedPathPlugin.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Jason Anderson @diurnalist
  4. */
  5. "use strict";
  6. const mime = require("mime-types");
  7. const { basename, extname } = require("path");
  8. const util = require("util");
  9. const Chunk = require("./Chunk");
  10. const Module = require("./Module");
  11. const { parseResource } = require("./util/identifier");
  12. /** @typedef {import("./Compilation").AssetInfo} AssetInfo */
  13. /** @typedef {import("./Compilation").PathData} PathData */
  14. /** @typedef {import("./Compiler")} Compiler */
  15. const REGEXP = /\[\\*([\w:]+)\\*\]/gi;
  16. const prepareId = id => {
  17. if (typeof id !== "string") return id;
  18. if (/^"\s\+*.*\+\s*"$/.test(id)) {
  19. const match = /^"\s\+*\s*(.*)\s*\+\s*"$/.exec(id);
  20. return `" + (${match[1]} + "").replace(/(^[.-]|[^a-zA-Z0-9_-])+/g, "_") + "`;
  21. }
  22. return id.replace(/(^[.-]|[^a-zA-Z0-9_-])+/g, "_");
  23. };
  24. const hashLength = (replacer, handler, assetInfo, hashName) => {
  25. const fn = (match, arg, input) => {
  26. let result;
  27. const length = arg && parseInt(arg, 10);
  28. if (length && handler) {
  29. result = handler(length);
  30. } else {
  31. const hash = replacer(match, arg, input);
  32. result = length ? hash.slice(0, length) : hash;
  33. }
  34. if (assetInfo) {
  35. assetInfo.immutable = true;
  36. if (Array.isArray(assetInfo[hashName])) {
  37. assetInfo[hashName] = [...assetInfo[hashName], result];
  38. } else if (assetInfo[hashName]) {
  39. assetInfo[hashName] = [assetInfo[hashName], result];
  40. } else {
  41. assetInfo[hashName] = result;
  42. }
  43. }
  44. return result;
  45. };
  46. return fn;
  47. };
  48. const replacer = (value, allowEmpty) => {
  49. const fn = (match, arg, input) => {
  50. if (typeof value === "function") {
  51. value = value();
  52. }
  53. if (value === null || value === undefined) {
  54. if (!allowEmpty) {
  55. throw new Error(
  56. `Path variable ${match} not implemented in this context: ${input}`
  57. );
  58. }
  59. return "";
  60. } else {
  61. return `${value}`;
  62. }
  63. };
  64. return fn;
  65. };
  66. const deprecationCache = new Map();
  67. const deprecatedFunction = (() => () => {})();
  68. const deprecated = (fn, message, code) => {
  69. let d = deprecationCache.get(message);
  70. if (d === undefined) {
  71. d = util.deprecate(deprecatedFunction, message, code);
  72. deprecationCache.set(message, d);
  73. }
  74. return (...args) => {
  75. d();
  76. return fn(...args);
  77. };
  78. };
  79. /**
  80. * @param {string | function(PathData, AssetInfo=): string} path the raw path
  81. * @param {PathData} data context data
  82. * @param {AssetInfo} assetInfo extra info about the asset (will be written to)
  83. * @returns {string} the interpolated path
  84. */
  85. const replacePathVariables = (path, data, assetInfo) => {
  86. const chunkGraph = data.chunkGraph;
  87. /** @type {Map<string, Function>} */
  88. const replacements = new Map();
  89. // Filename context
  90. //
  91. // Placeholders
  92. //
  93. // for /some/path/file.js?query#fragment:
  94. // [file] - /some/path/file.js
  95. // [query] - ?query
  96. // [fragment] - #fragment
  97. // [base] - file.js
  98. // [path] - /some/path/
  99. // [name] - file
  100. // [ext] - .js
  101. if (typeof data.filename === "string") {
  102. // check that filename is data uri
  103. let match = data.filename.match(/^data:([^;,]+)/);
  104. if (match) {
  105. const ext = mime.extension(match[1]);
  106. const emptyReplacer = replacer("", true);
  107. replacements.set("file", emptyReplacer);
  108. replacements.set("query", emptyReplacer);
  109. replacements.set("fragment", emptyReplacer);
  110. replacements.set("path", emptyReplacer);
  111. replacements.set("base", emptyReplacer);
  112. replacements.set("name", emptyReplacer);
  113. replacements.set("ext", replacer(ext ? `.${ext}` : "", true));
  114. // Legacy
  115. replacements.set(
  116. "filebase",
  117. deprecated(
  118. emptyReplacer,
  119. "[filebase] is now [base]",
  120. "DEP_WEBPACK_TEMPLATE_PATH_PLUGIN_REPLACE_PATH_VARIABLES_FILENAME"
  121. )
  122. );
  123. } else {
  124. const { path: file, query, fragment } = parseResource(data.filename);
  125. const ext = extname(file);
  126. const base = basename(file);
  127. const name = base.slice(0, base.length - ext.length);
  128. const path = file.slice(0, file.length - base.length);
  129. replacements.set("file", replacer(file));
  130. replacements.set("query", replacer(query, true));
  131. replacements.set("fragment", replacer(fragment, true));
  132. replacements.set("path", replacer(path, true));
  133. replacements.set("base", replacer(base));
  134. replacements.set("name", replacer(name));
  135. replacements.set("ext", replacer(ext, true));
  136. // Legacy
  137. replacements.set(
  138. "filebase",
  139. deprecated(
  140. replacer(base),
  141. "[filebase] is now [base]",
  142. "DEP_WEBPACK_TEMPLATE_PATH_PLUGIN_REPLACE_PATH_VARIABLES_FILENAME"
  143. )
  144. );
  145. }
  146. }
  147. // Compilation context
  148. //
  149. // Placeholders
  150. //
  151. // [fullhash] - data.hash (3a4b5c6e7f)
  152. //
  153. // Legacy Placeholders
  154. //
  155. // [hash] - data.hash (3a4b5c6e7f)
  156. if (data.hash) {
  157. const hashReplacer = hashLength(
  158. replacer(data.hash),
  159. data.hashWithLength,
  160. assetInfo,
  161. "fullhash"
  162. );
  163. replacements.set("fullhash", hashReplacer);
  164. // Legacy
  165. replacements.set(
  166. "hash",
  167. deprecated(
  168. hashReplacer,
  169. "[hash] is now [fullhash] (also consider using [chunkhash] or [contenthash], see documentation for details)",
  170. "DEP_WEBPACK_TEMPLATE_PATH_PLUGIN_REPLACE_PATH_VARIABLES_HASH"
  171. )
  172. );
  173. }
  174. // Chunk Context
  175. //
  176. // Placeholders
  177. //
  178. // [id] - chunk.id (0.js)
  179. // [name] - chunk.name (app.js)
  180. // [chunkhash] - chunk.hash (7823t4t4.js)
  181. // [contenthash] - chunk.contentHash[type] (3256u3zg.js)
  182. if (data.chunk) {
  183. const chunk = data.chunk;
  184. const contentHashType = data.contentHashType;
  185. const idReplacer = replacer(chunk.id);
  186. const nameReplacer = replacer(chunk.name || chunk.id);
  187. const chunkhashReplacer = hashLength(
  188. replacer(chunk instanceof Chunk ? chunk.renderedHash : chunk.hash),
  189. "hashWithLength" in chunk ? chunk.hashWithLength : undefined,
  190. assetInfo,
  191. "chunkhash"
  192. );
  193. const contenthashReplacer = hashLength(
  194. replacer(
  195. data.contentHash ||
  196. (contentHashType &&
  197. chunk.contentHash &&
  198. chunk.contentHash[contentHashType])
  199. ),
  200. data.contentHashWithLength ||
  201. ("contentHashWithLength" in chunk && chunk.contentHashWithLength
  202. ? chunk.contentHashWithLength[contentHashType]
  203. : undefined),
  204. assetInfo,
  205. "contenthash"
  206. );
  207. replacements.set("id", idReplacer);
  208. replacements.set("name", nameReplacer);
  209. replacements.set("chunkhash", chunkhashReplacer);
  210. replacements.set("contenthash", contenthashReplacer);
  211. }
  212. // Module Context
  213. //
  214. // Placeholders
  215. //
  216. // [id] - module.id (2.png)
  217. // [hash] - module.hash (6237543873.png)
  218. //
  219. // Legacy Placeholders
  220. //
  221. // [moduleid] - module.id (2.png)
  222. // [modulehash] - module.hash (6237543873.png)
  223. if (data.module) {
  224. const module = data.module;
  225. const idReplacer = replacer(() =>
  226. prepareId(
  227. module instanceof Module ? chunkGraph.getModuleId(module) : module.id
  228. )
  229. );
  230. const moduleHashReplacer = hashLength(
  231. replacer(() =>
  232. module instanceof Module
  233. ? chunkGraph.getRenderedModuleHash(module, data.runtime)
  234. : module.hash
  235. ),
  236. "hashWithLength" in module ? module.hashWithLength : undefined,
  237. assetInfo,
  238. "modulehash"
  239. );
  240. const contentHashReplacer = hashLength(
  241. replacer(data.contentHash),
  242. undefined,
  243. assetInfo,
  244. "contenthash"
  245. );
  246. replacements.set("id", idReplacer);
  247. replacements.set("modulehash", moduleHashReplacer);
  248. replacements.set("contenthash", contentHashReplacer);
  249. replacements.set(
  250. "hash",
  251. data.contentHash ? contentHashReplacer : moduleHashReplacer
  252. );
  253. // Legacy
  254. replacements.set(
  255. "moduleid",
  256. deprecated(
  257. idReplacer,
  258. "[moduleid] is now [id]",
  259. "DEP_WEBPACK_TEMPLATE_PATH_PLUGIN_REPLACE_PATH_VARIABLES_MODULE_ID"
  260. )
  261. );
  262. }
  263. // Other things
  264. if (data.url) {
  265. replacements.set("url", replacer(data.url));
  266. }
  267. if (typeof data.runtime === "string") {
  268. replacements.set(
  269. "runtime",
  270. replacer(() => prepareId(data.runtime))
  271. );
  272. } else {
  273. replacements.set("runtime", replacer("_"));
  274. }
  275. if (typeof path === "function") {
  276. path = path(data, assetInfo);
  277. }
  278. path = path.replace(REGEXP, (match, content) => {
  279. if (content.length + 2 === match.length) {
  280. const contentMatch = /^(\w+)(?::(\w+))?$/.exec(content);
  281. if (!contentMatch) return match;
  282. const [, kind, arg] = contentMatch;
  283. const replacer = replacements.get(kind);
  284. if (replacer !== undefined) {
  285. return replacer(match, arg, path);
  286. }
  287. } else if (match.startsWith("[\\") && match.endsWith("\\]")) {
  288. return `[${match.slice(2, -2)}]`;
  289. }
  290. return match;
  291. });
  292. return path;
  293. };
  294. const plugin = "TemplatedPathPlugin";
  295. class TemplatedPathPlugin {
  296. /**
  297. * Apply the plugin
  298. * @param {Compiler} compiler the compiler instance
  299. * @returns {void}
  300. */
  301. apply(compiler) {
  302. compiler.hooks.compilation.tap(plugin, compilation => {
  303. compilation.hooks.assetPath.tap(plugin, replacePathVariables);
  304. });
  305. }
  306. }
  307. module.exports = TemplatedPathPlugin;