TemplatedPathPlugin.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Jason Anderson @diurnalist
  4. */
  5. "use strict";
  6. const { basename, extname, posix } = require("path");
  7. const util = require("util");
  8. const Chunk = require("./Chunk");
  9. const Module = require("./Module");
  10. const {
  11. decode: decodeBase,
  12. encode: encodeBase
  13. } = require("./util/hash/hash-digest");
  14. const {
  15. ABSOLUTE_PATH_REGEXP,
  16. WINDOWS_ABS_PATH_REGEXP,
  17. WINDOWS_PATH_SEPARATOR_REGEXP,
  18. parseResource
  19. } = require("./util/identifier");
  20. const memoize = require("./util/memoize");
  21. const getMimeTypes = memoize(() => require("./util/mimeTypes"));
  22. /** @import ChunkGraph, { ModuleId } from "./ChunkGraph" */
  23. /**
  24. * @import Compilation, {
  25. * AssetInfo,
  26. * HashWithDigestFunction,
  27. * PathData,
  28. * PathDataChunk,
  29. * PathDataModule
  30. * } from "./Compilation"
  31. */
  32. /** @import Compiler from "./Compiler" */
  33. const REGEXP = /\[\\*([\w:]+)\\*\]/g;
  34. /**
  35. * Placeholder kinds present in a template string, cached so the scan is not
  36. * repeated for reused templates (e.g. `output.filename`, `localIdentName`).
  37. * Bounded so dynamic (function-built) paths can't grow it without limit.
  38. * @type {Map<string, Set<string>>}
  39. */
  40. const presentKindsCache = new Map();
  41. const PRESENT_KINDS_CACHE_MAX = 1000;
  42. /**
  43. * Returns the placeholder kinds (`[kind]` / `[kind:arg]`) a template references,
  44. * matching the replace pass below so guarding replacer construction by it stays
  45. * output-identical.
  46. * @param {string} path template string (already known to contain `[`)
  47. * @returns {Set<string>} placeholder kinds present
  48. */
  49. const getPresentKinds = (path) => {
  50. const cached = presentKindsCache.get(path);
  51. if (cached !== undefined) return cached;
  52. /** @type {Set<string>} */
  53. const kinds = new Set();
  54. // `RegExp.exec` loop rather than `String.matchAll` (Node.js 12+) so this
  55. // stays compatible with the supported Node.js 10 range.
  56. REGEXP.lastIndex = 0;
  57. /** @type {RegExpExecArray | null} */
  58. let m;
  59. while ((m = REGEXP.exec(path)) !== null) {
  60. const content = /** @type {string} */ (m[1]);
  61. if (content.length + 2 === m[0].length) {
  62. const cm = /^(\w+)(?::\w+)?(?::\w+)?$/.exec(content);
  63. if (cm) kinds.add(cm[1]);
  64. }
  65. }
  66. if (presentKindsCache.size >= PRESENT_KINDS_CACHE_MAX) {
  67. presentKindsCache.clear();
  68. }
  69. presentKindsCache.set(path, kinds);
  70. return kinds;
  71. };
  72. // `[fullhash:<digest>]`/`[hash:<digest>]` — a non-numeric first argument is a digest
  73. // (a numeric one is just a length). A digest re-encodes the hash rather than reading
  74. // it, so no stand-in or runtime expression can take its place: the settled value has
  75. // to be inlined instead.
  76. const FULL_HASH_DIGEST_REGEXP = /\[(?:fullhash|hash):(?!\d+\])\w/;
  77. /**
  78. * @param {string} template path template
  79. * @returns {boolean} true when it references `[fullhash:<digest>]`/`[hash:<digest>]`
  80. */
  81. const usesFullHashDigest = (template) => FULL_HASH_DIGEST_REGEXP.test(template);
  82. const PARENT_SEGMENTS_REGEXP = /^(?:\.\.\/)+/;
  83. /**
  84. * Maps a `[path]`/`[file]` value into the output directory: an absolute root or
  85. * a leading `..` becomes `_` (as `file-loader` did), so a module resolved
  86. * outside of `context` can't emit its asset outside of `output.path`.
  87. * @param {string} path relative path of the source file
  88. * @returns {string} path that stays inside the output directory
  89. */
  90. const toContainedPath = (path) => {
  91. if (!path) return path;
  92. // A resource on another Windows drive stays absolute and win32-separated
  93. // when made relative to the context.
  94. const normalized = posix.normalize(
  95. WINDOWS_ABS_PATH_REGEXP.test(path)
  96. ? path.replace(WINDOWS_PATH_SEPARATOR_REGEXP, "/")
  97. : path
  98. );
  99. return normalized
  100. .replace(ABSOLUTE_PATH_REGEXP, "_/")
  101. .replace(PARENT_SEGMENTS_REGEXP, (match) => "_/".repeat(match.length / 3));
  102. };
  103. /**
  104. * Source file name a filename template sees. `[path]`/`[file]` of a module
  105. * resolved outside of `context` start with `..` and would emit the asset
  106. * outside of `output.path`, so they are contained like
  107. * `[containedpath]`/`[containedfile]` with `experiments.futureDefaults`.
  108. * @param {string} sourceFilename context-relative path of the source file
  109. * @param {Compilation} compilation compilation
  110. * @returns {string} source file name for the filename template
  111. */
  112. const toTemplateSourceFileName = (sourceFilename, compilation) =>
  113. // TODO webpack 6: remove the check, always contain
  114. compilation.options.experiments.futureDefaults
  115. ? toContainedPath(sourceFilename)
  116. : sourceFilename;
  117. /** @type {PathData["prepareId"]} */
  118. const prepareId = (id) => {
  119. if (typeof id !== "string") return id;
  120. if (/^"\s\+*.*\+\s*"$/.test(id)) {
  121. const match = /^"\s\+*\s*(.*)\s*\+\s*"$/.exec(id);
  122. return `" + (${
  123. /** @type {string[]} */ (match)[1]
  124. } + "").replace(/(^[.-]|[^a-zA-Z0-9_-])+/g, "_") + "`;
  125. }
  126. return id.replace(/(^[.-]|[^a-z0-9_-])+/gi, "_");
  127. };
  128. /**
  129. * Defines the replacer function callback.
  130. * @callback ReplacerFunction
  131. * @param {string} match
  132. * @param {string | undefined} arg
  133. * @param {string} input
  134. */
  135. /** @typedef {"26" | "32" | "36" | "49" | "52" | "58" | "62"} Base */
  136. // `[<digest>]` placeholders may request a digest webpack does not store the
  137. // hash in; loader-utils calls the URL-safe base64 `base64safe`.
  138. const BASE_DIGEST = /^base(\d+)$/;
  139. const SUPPORTED_BASES = new Set(["26", "32", "36", "49", "52", "58", "62"]);
  140. // Node < 14.18 lacks the `base64url` Buffer encoding; fall back to base64 + swap.
  141. let isBase64UrlSupported = false;
  142. try {
  143. isBase64UrlSupported = Boolean(Buffer.from("", "base64url"));
  144. } catch (_err) {
  145. // Nothing
  146. }
  147. /**
  148. * @param {string} digest digest name from a `[<hash>:<digest>]` placeholder
  149. * @returns {boolean} whether a hash can be re-encoded into this digest
  150. */
  151. const isSupportedDigest = (digest) => {
  152. if (digest === "base64url" || digest === "base64safe") return true;
  153. const base = BASE_DIGEST.exec(digest);
  154. if (base) return base[1] === "64" || SUPPORTED_BASES.has(base[1]);
  155. return Buffer.isEncoding(digest);
  156. };
  157. /**
  158. * Decodes an already-digested hash string back into its raw bytes.
  159. * @param {string} value digested hash
  160. * @param {string} digest digest the value is encoded in
  161. * @returns {Buffer} raw bytes
  162. */
  163. const digestToBuffer = (value, digest) => {
  164. const base = BASE_DIGEST.exec(digest);
  165. if (base && Number(base[1]) !== 64) {
  166. return decodeBase(value, /** @type {Base} */ (base[1]));
  167. }
  168. if (
  169. (digest === "base64url" || digest === "base64safe") &&
  170. !isBase64UrlSupported
  171. ) {
  172. return Buffer.from(value.replace(/-/g, "+").replace(/_/g, "/"), "base64");
  173. }
  174. return Buffer.from(
  175. value,
  176. /** @type {BufferEncoding} */ (
  177. digest === "base64safe" ? "base64url" : digest
  178. )
  179. );
  180. };
  181. /**
  182. * Encodes raw bytes into the requested digest.
  183. * @param {Buffer} buffer raw bytes
  184. * @param {string} digest target digest
  185. * @returns {string} encoded hash
  186. */
  187. const bufferToDigest = (buffer, digest) => {
  188. const base = BASE_DIGEST.exec(digest);
  189. if (base && Number(base[1]) !== 64) {
  190. return encodeBase(buffer, /** @type {Base} */ (base[1]));
  191. }
  192. if (
  193. (digest === "base64url" || digest === "base64safe") &&
  194. !isBase64UrlSupported
  195. ) {
  196. return buffer
  197. .toString("base64")
  198. .replace(/\+/g, "-")
  199. .replace(/\//g, "_")
  200. .replace(/[=]+$/, "");
  201. }
  202. return buffer.toString(
  203. /** @type {BufferEncoding} */ (
  204. digest === "base64safe" ? "base64url" : digest
  205. )
  206. );
  207. };
  208. /**
  209. * Re-encodes a digested hash into another digest (e.g. `[contenthash:base64]`).
  210. * The source is already truncated to `output.hashDigestLength`, so the result is
  211. * derived from those bytes, not the full content. Throws on an unknown digest so
  212. * a typo fails loudly rather than silently keeping the original encoding.
  213. * @param {string} value digested hash
  214. * @param {string} fromDigest digest the value is encoded in
  215. * @param {string} toDigest requested digest
  216. * @returns {string} re-encoded hash
  217. */
  218. const reEncodeDigest = (value, fromDigest, toDigest) => {
  219. if (toDigest === fromDigest) return value;
  220. if (!isSupportedDigest(toDigest)) {
  221. throw new Error(
  222. `Unsupported hash digest "${toDigest}" in path template (use hex, base64, base64url, or base26/32/36/49/52/58/62)`
  223. );
  224. }
  225. return bufferToDigest(digestToBuffer(value, fromDigest), toDigest);
  226. };
  227. /**
  228. * Returns hash replacer function.
  229. * @param {ReplacerFunction} replacer replacer
  230. * @param {((arg0: number) => string) | undefined} handler handler
  231. * @param {AssetInfo | undefined} assetInfo asset info
  232. * @param {string} hashName hash name
  233. * @param {string} sourceDigest digest the stored hash is encoded in
  234. * @param {string=} fullValue untruncated hash, re-encoded for `[<hash>:<digest>]` so the result carries full entropy instead of the `hashDigestLength`-truncated value
  235. * @param {boolean=} recordDigest record the inline digest on `assetInfo.contenthashDigest` so `RealContentHashPlugin` re-encodes the recomputed hash in it
  236. * @param {HashWithDigestFunction=} digestHandler builds the value for `[<hash>:<digest>]` in a per-chunk runtime context (the runtime chunk-filename map), where a single re-encode of the whole expression is impossible
  237. * @returns {Replacer} hash replacer function
  238. */
  239. const hashLength = (
  240. replacer,
  241. handler,
  242. assetInfo,
  243. hashName,
  244. sourceDigest,
  245. fullValue,
  246. recordDigest,
  247. digestHandler
  248. ) => {
  249. /** @type {Replacer} */
  250. const fn = (match, arg, input, digest) => {
  251. /** @type {string} */
  252. let result;
  253. const length = arg && Number.parseInt(arg, 10);
  254. if (digest && digestHandler) {
  255. result = digestHandler(digest, length || undefined);
  256. } else if (digest) {
  257. const hash = reEncodeDigest(
  258. fullValue !== undefined ? fullValue : replacer(match, arg, input),
  259. sourceDigest,
  260. digest
  261. );
  262. result = length ? hash.slice(0, length) : hash;
  263. } else if (length && handler) {
  264. result = handler(length);
  265. } else {
  266. const hash = replacer(match, arg, input);
  267. result = length ? hash.slice(0, length) : hash;
  268. }
  269. if (assetInfo) {
  270. assetInfo.immutable = true;
  271. if (digest && recordDigest) {
  272. // `base64safe` is encoded as `base64url`; record what the value is in.
  273. (assetInfo.contenthashDigest || (assetInfo.contenthashDigest = {}))[
  274. result
  275. ] = digest === "base64safe" ? "base64url" : digest;
  276. }
  277. if (Array.isArray(assetInfo[hashName])) {
  278. assetInfo[hashName] = [...assetInfo[hashName], result];
  279. } else if (assetInfo[hashName]) {
  280. assetInfo[hashName] = [assetInfo[hashName], result];
  281. } else {
  282. assetInfo[hashName] = result;
  283. }
  284. }
  285. return result;
  286. };
  287. return fn;
  288. };
  289. /** @typedef {(match: string, arg: string | undefined, input: string, digest?: string) => string} Replacer */
  290. /**
  291. * Returns replacer.
  292. * @param {string | number | null | undefined | (() => string | number | null | undefined)} value value
  293. * @param {boolean=} allowEmpty allow empty
  294. * @returns {Replacer} replacer
  295. */
  296. const replacer = (value, allowEmpty) => {
  297. /** @type {Replacer} */
  298. const fn = (match, arg, input) => {
  299. if (typeof value === "function") {
  300. value = value();
  301. }
  302. if (value === null || value === undefined) {
  303. if (!allowEmpty) {
  304. throw new Error(
  305. `Path variable ${match} not implemented in this context: ${input}`
  306. );
  307. }
  308. return "";
  309. }
  310. return `${value}`;
  311. };
  312. return fn;
  313. };
  314. /** @type {Map<string, (...args: EXPECTED_ANY[]) => EXPECTED_ANY>} */
  315. const deprecationCache = new Map();
  316. const deprecatedFunction = (() => () => {})();
  317. /**
  318. * Returns function with deprecation output.
  319. * @template {(...args: EXPECTED_ANY[]) => EXPECTED_ANY} T
  320. * @param {T} fn function
  321. * @param {string} message message
  322. * @param {string} code code
  323. * @returns {T} function with deprecation output
  324. */
  325. const deprecated = (fn, message, code) => {
  326. let d = deprecationCache.get(message);
  327. if (d === undefined) {
  328. d = util.deprecate(deprecatedFunction, message, code);
  329. deprecationCache.set(message, d);
  330. }
  331. return /** @type {T} */ (
  332. (...args) => {
  333. d();
  334. return fn(...args);
  335. }
  336. );
  337. };
  338. /**
  339. * Callback used to compute a path from contextual data. The type parameter
  340. * narrows the `pathData` shape when the caller knows it operates in a chunk
  341. * (`PathDataChunk`) or module (`PathDataModule`) context — defaults to the
  342. * fully-optional `PathData` for backward compatibility.
  343. * @template {PathData} [T=PathData]
  344. * @typedef {(pathData: T, assetInfo?: AssetInfo) => string} TemplatePathFn
  345. */
  346. /**
  347. * Either a raw template string (e.g. `"[name].[contenthash].js"`) or a
  348. * generic `TemplatePathFn`. Method signatures that need to thread a narrowed
  349. * `PathData` shape spell the function side out as `TemplatePathFn<T>`
  350. * directly — `TemplatePath` itself stays a plain alias so local JSDoc
  351. * re-imports keep a single shared identity.
  352. * @typedef {string | TemplatePathFn} TemplatePath
  353. */
  354. /**
  355. * Returns the interpolated path.
  356. * @template {PathData} [T=PathData]
  357. * @param {string | TemplatePathFn<T>} path the raw path
  358. * @param {T} data context data
  359. * @param {AssetInfo=} assetInfo extra info about the asset (will be written to)
  360. * @returns {string} the interpolated path
  361. */
  362. const interpolate = (path, data, assetInfo) => {
  363. if (typeof path === "function") {
  364. path = path(data, assetInfo);
  365. }
  366. // Literal paths carry no `[placeholder]`, so the whole replacement table
  367. // and regex pass are pure overhead — building replacers has no side effects
  368. // (those only fire when a replacer is invoked), so the output is identical.
  369. if (!path.includes("[")) {
  370. return path;
  371. }
  372. // Only build replacers for placeholders the template actually uses — most
  373. // templates reference a handful, so building the whole table per call is
  374. // wasted work. Replacer construction has no side effects (those fire only
  375. // when a replacer is invoked, which happens for present kinds), so this is
  376. // output-identical.
  377. const presentKinds = getPresentKinds(path);
  378. const chunkGraph = data.chunkGraph;
  379. // Digest the stored hashes are encoded in, so `[hash:<digest>]` can re-encode.
  380. const sourceDigest = data.hashDigest || "hex";
  381. /** @type {Map<string, Replacer>} */
  382. const replacements = new Map();
  383. // Filename context
  384. //
  385. // Placeholders
  386. //
  387. // for /some/path/file.js?query#fragment:
  388. // [file] - /some/path/file.js
  389. // [query] - ?query
  390. // [fragment] - #fragment
  391. // [base] - file.js
  392. // [path] - /some/path/
  393. // [name] - file
  394. // [ext] - .js
  395. //
  396. // [containedfile] and [containedpath] are [file] and [path] kept inside
  397. // `output.path`
  398. if (
  399. typeof data.filename === "string" &&
  400. (presentKinds.has("file") ||
  401. presentKinds.has("query") ||
  402. presentKinds.has("fragment") ||
  403. presentKinds.has("path") ||
  404. presentKinds.has("base") ||
  405. presentKinds.has("name") ||
  406. presentKinds.has("ext") ||
  407. presentKinds.has("filebase") ||
  408. presentKinds.has("containedfile") ||
  409. presentKinds.has("containedpath"))
  410. ) {
  411. // check that filename is data uri
  412. const match = data.filename.match(/^data:([^;,]+)/);
  413. if (match) {
  414. const ext = getMimeTypes().extension(match[1]);
  415. const emptyReplacer = replacer("", true);
  416. // "XXXX" used for `updateHash`, so we don't need it here
  417. const contentHash =
  418. data.contentHash && !/X+/.test(data.contentHash)
  419. ? data.contentHash
  420. : false;
  421. const baseReplacer = contentHash ? replacer(contentHash) : emptyReplacer;
  422. if (presentKinds.has("file")) replacements.set("file", emptyReplacer);
  423. if (presentKinds.has("query")) replacements.set("query", emptyReplacer);
  424. if (presentKinds.has("fragment")) {
  425. replacements.set("fragment", emptyReplacer);
  426. }
  427. if (presentKinds.has("path")) replacements.set("path", emptyReplacer);
  428. if (presentKinds.has("containedfile")) {
  429. replacements.set("containedfile", emptyReplacer);
  430. }
  431. if (presentKinds.has("containedpath")) {
  432. replacements.set("containedpath", emptyReplacer);
  433. }
  434. if (presentKinds.has("base")) replacements.set("base", baseReplacer);
  435. if (presentKinds.has("name")) replacements.set("name", baseReplacer);
  436. if (presentKinds.has("ext")) {
  437. replacements.set("ext", replacer(ext ? `.${ext}` : "", true));
  438. }
  439. // Legacy
  440. if (presentKinds.has("filebase")) {
  441. replacements.set(
  442. "filebase",
  443. deprecated(
  444. baseReplacer,
  445. "[filebase] is now [base]",
  446. "DEP_WEBPACK_TEMPLATE_PATH_PLUGIN_REPLACE_PATH_VARIABLES_FILENAME"
  447. )
  448. );
  449. }
  450. } else {
  451. const { path: file, query, fragment } = parseResource(data.filename);
  452. const ext = extname(file);
  453. const base = basename(file);
  454. const name = base.slice(0, base.length - ext.length);
  455. const path = file.slice(0, file.length - base.length);
  456. if (presentKinds.has("file")) replacements.set("file", replacer(file));
  457. if (presentKinds.has("query")) {
  458. replacements.set("query", replacer(query, true));
  459. }
  460. if (presentKinds.has("fragment")) {
  461. replacements.set("fragment", replacer(fragment, true));
  462. }
  463. if (presentKinds.has("path")) {
  464. replacements.set("path", replacer(path, true));
  465. }
  466. if (presentKinds.has("containedfile")) {
  467. replacements.set("containedfile", replacer(toContainedPath(file)));
  468. }
  469. if (presentKinds.has("containedpath")) {
  470. replacements.set(
  471. "containedpath",
  472. replacer(toContainedPath(path), true)
  473. );
  474. }
  475. if (presentKinds.has("base")) replacements.set("base", replacer(base));
  476. if (presentKinds.has("name")) replacements.set("name", replacer(name));
  477. if (presentKinds.has("ext")) replacements.set("ext", replacer(ext, true));
  478. // Legacy
  479. if (presentKinds.has("filebase")) {
  480. replacements.set(
  481. "filebase",
  482. deprecated(
  483. replacer(base),
  484. "[filebase] is now [base]",
  485. "DEP_WEBPACK_TEMPLATE_PATH_PLUGIN_REPLACE_PATH_VARIABLES_FILENAME"
  486. )
  487. );
  488. }
  489. }
  490. }
  491. // Compilation context
  492. //
  493. // Placeholders
  494. //
  495. // [fullhash] - data.hash (3a4b5c6e7f)
  496. //
  497. // Legacy Placeholders
  498. //
  499. // [hash] - data.hash (3a4b5c6e7f)
  500. if (data.hash && (presentKinds.has("fullhash") || presentKinds.has("hash"))) {
  501. const hashReplacer = hashLength(
  502. replacer(data.hash),
  503. data.hashWithLength,
  504. assetInfo,
  505. "fullhash",
  506. data.fullHashDigest || sourceDigest,
  507. data.fullHash,
  508. undefined,
  509. data.hashWithDigest
  510. );
  511. if (presentKinds.has("fullhash")) {
  512. replacements.set("fullhash", hashReplacer);
  513. }
  514. // Legacy — but a css-loader-style `[hash]` local ident is not deprecated
  515. if (presentKinds.has("hash")) {
  516. replacements.set(
  517. "hash",
  518. data.hashAsFullHash
  519. ? hashReplacer
  520. : deprecated(
  521. hashReplacer,
  522. "[hash] is now [fullhash] (also consider using [chunkhash] or [contenthash], see documentation for details)",
  523. "DEP_WEBPACK_TEMPLATE_PATH_PLUGIN_REPLACE_PATH_VARIABLES_HASH"
  524. )
  525. );
  526. }
  527. }
  528. // Chunk Context
  529. //
  530. // Placeholders
  531. //
  532. // [id] - chunk.id (0.js)
  533. // [name] - chunk.name (app.js)
  534. // [chunkhash] - chunk.hash (7823t4t4.js)
  535. // [contenthash] - chunk.contentHash[type] (3256u3zg.js)
  536. if (data.chunk) {
  537. const chunk = data.chunk;
  538. const contentHashType = data.contentHashType;
  539. if (presentKinds.has("id")) replacements.set("id", replacer(chunk.id));
  540. if (presentKinds.has("name")) {
  541. replacements.set("name", replacer(chunk.name || chunk.id));
  542. }
  543. if (presentKinds.has("chunkhash")) {
  544. replacements.set(
  545. "chunkhash",
  546. hashLength(
  547. replacer(chunk instanceof Chunk ? chunk.renderedHash : chunk.hash),
  548. "hashWithLength" in chunk ? chunk.hashWithLength : undefined,
  549. assetInfo,
  550. "chunkhash",
  551. sourceDigest,
  552. chunk.hash,
  553. undefined,
  554. "hashWithDigest" in chunk ? chunk.hashWithDigest : undefined
  555. )
  556. );
  557. }
  558. if (presentKinds.has("contenthash")) {
  559. const ct = /** @type {string} */ (contentHashType);
  560. replacements.set(
  561. "contenthash",
  562. hashLength(
  563. replacer(
  564. data.contentHash ||
  565. (contentHashType &&
  566. chunk.contentHash &&
  567. chunk.contentHash[contentHashType])
  568. ),
  569. data.contentHashWithLength ||
  570. ("contentHashWithLength" in chunk && chunk.contentHashWithLength
  571. ? chunk.contentHashWithLength[ct]
  572. : undefined),
  573. assetInfo,
  574. "contenthash",
  575. sourceDigest,
  576. // full content digest, so a static `[contenthash:<digest>]` re-encodes
  577. // from full entropy (the runtime path uses `contentHashWithDigest`)
  578. "contentHashFull" in chunk && chunk.contentHashFull
  579. ? chunk.contentHashFull[ct]
  580. : undefined,
  581. data.realContentHash,
  582. "contentHashWithDigest" in chunk && chunk.contentHashWithDigest
  583. ? chunk.contentHashWithDigest[ct]
  584. : undefined
  585. )
  586. );
  587. }
  588. }
  589. // Module Context
  590. //
  591. // Placeholders
  592. //
  593. // [id] - module.id (2.png)
  594. // [hash] - module.hash (6237543873.png)
  595. //
  596. // Legacy Placeholders
  597. //
  598. // [moduleid] - module.id (2.png)
  599. // [modulehash] - module.hash (6237543873.png)
  600. if (data.module) {
  601. const module = data.module;
  602. const needId = presentKinds.has("id");
  603. const needModuleId = presentKinds.has("moduleid");
  604. // `data.hashAsFullHash` keeps `[hash]` as the `[fullhash]` alias (CSS local
  605. // idents) instead of repurposing it to the module hash; `[modulehash]` stays.
  606. const needHash = presentKinds.has("hash") && !data.hashAsFullHash;
  607. if (needId || needModuleId) {
  608. const idReplacer = replacer(() =>
  609. (data.prepareId || prepareId)(
  610. module instanceof Module
  611. ? /** @type {ModuleId} */
  612. (/** @type {ChunkGraph} */ (chunkGraph).getModuleId(module))
  613. : module.id
  614. )
  615. );
  616. if (needId) replacements.set("id", idReplacer);
  617. // Legacy
  618. if (needModuleId) {
  619. replacements.set(
  620. "moduleid",
  621. deprecated(
  622. idReplacer,
  623. "[moduleid] is now [id]",
  624. "DEP_WEBPACK_TEMPLATE_PATH_PLUGIN_REPLACE_PATH_VARIABLES_MODULE_ID"
  625. )
  626. );
  627. }
  628. }
  629. // `[hash]` aliases module content hash when present, else module hash.
  630. const wantModuleHash =
  631. presentKinds.has("modulehash") || (needHash && !data.contentHash);
  632. const wantContentHash =
  633. presentKinds.has("contenthash") || (needHash && data.contentHash);
  634. /** @type {Replacer | undefined} */
  635. let moduleHashReplacer;
  636. /** @type {Replacer | undefined} */
  637. let contentHashReplacer;
  638. if (wantModuleHash) {
  639. moduleHashReplacer = hashLength(
  640. replacer(() =>
  641. module instanceof Module
  642. ? /** @type {ChunkGraph} */
  643. (chunkGraph).getRenderedModuleHash(module, data.runtime)
  644. : module.hash
  645. ),
  646. "hashWithLength" in module ? module.hashWithLength : undefined,
  647. assetInfo,
  648. "modulehash",
  649. sourceDigest,
  650. // `getModuleHash` is the untruncated digest (`getRenderedModuleHash` is
  651. // the truncated one), so `[modulehash:<digest>]` re-encodes full entropy
  652. module instanceof Module
  653. ? /** @type {ChunkGraph} */ (chunkGraph).getModuleHash(
  654. module,
  655. data.runtime
  656. )
  657. : module.hash
  658. );
  659. if (presentKinds.has("modulehash")) {
  660. replacements.set("modulehash", moduleHashReplacer);
  661. }
  662. }
  663. if (wantContentHash) {
  664. contentHashReplacer = hashLength(
  665. replacer(/** @type {string} */ (data.contentHash)),
  666. undefined,
  667. assetInfo,
  668. "contenthash",
  669. sourceDigest,
  670. // full content digest, so a static `[contenthash:<digest>]` re-encodes
  671. // from full entropy (asset modules supply it via `contentHashFull`)
  672. data.contentHashFull,
  673. data.realContentHash
  674. );
  675. if (presentKinds.has("contenthash")) {
  676. replacements.set("contenthash", contentHashReplacer);
  677. }
  678. }
  679. if (needHash) {
  680. replacements.set(
  681. "hash",
  682. /** @type {Replacer} */
  683. (data.contentHash ? contentHashReplacer : moduleHashReplacer)
  684. );
  685. }
  686. }
  687. // Other things
  688. //
  689. // Placeholders
  690. //
  691. // [url] - data.url
  692. // [uniqueName] - data.uniqueName (output.uniqueName)
  693. // [uniquename] - alias of [uniqueName]
  694. if (data.url && presentKinds.has("url")) {
  695. replacements.set("url", replacer(data.url));
  696. }
  697. if (
  698. data.uniqueName !== undefined &&
  699. (presentKinds.has("uniqueName") || presentKinds.has("uniquename"))
  700. ) {
  701. const uniqueNameReplacer = replacer(data.uniqueName);
  702. if (presentKinds.has("uniqueName")) {
  703. replacements.set("uniqueName", uniqueNameReplacer);
  704. }
  705. if (presentKinds.has("uniquename")) {
  706. replacements.set("uniquename", uniqueNameReplacer);
  707. }
  708. }
  709. if (presentKinds.has("runtime")) {
  710. if (typeof data.runtime === "string") {
  711. replacements.set(
  712. "runtime",
  713. replacer(() =>
  714. (data.prepareId || prepareId)(/** @type {string} */ (data.runtime))
  715. )
  716. );
  717. } else {
  718. replacements.set("runtime", replacer("_"));
  719. }
  720. }
  721. path = path.replace(REGEXP, (match, content) => {
  722. if (content.length + 2 === match.length) {
  723. const contentMatch = /^(\w+)(?::(\w+))?(?::(\w+))?$/.exec(content);
  724. if (!contentMatch) return match;
  725. const [, kind, arg1, arg2] = contentMatch;
  726. const replacer = replacements.get(kind);
  727. if (replacer !== undefined) {
  728. // `[kind:length]`, `[kind:digest]` or `[kind:digest:length]`.
  729. /** @type {string | undefined} */
  730. let digest;
  731. /** @type {string | undefined} */
  732. let length = arg1;
  733. if (arg2 !== undefined) {
  734. digest = arg1;
  735. length = arg2;
  736. } else if (arg1 !== undefined && !/^\d+$/.test(arg1)) {
  737. digest = arg1;
  738. length = undefined;
  739. }
  740. return replacer(match, length, /** @type {string} */ (path), digest);
  741. }
  742. } else if (match.startsWith("[\\") && match.endsWith("\\]")) {
  743. return `[${match.slice(2, -2)}]`;
  744. }
  745. return match;
  746. });
  747. return path;
  748. };
  749. const plugin = "TemplatedPathPlugin";
  750. class TemplatedPathPlugin {
  751. /**
  752. * Applies the plugin by registering its hooks on the compiler.
  753. * @param {Compiler} compiler the compiler instance
  754. * @returns {void}
  755. */
  756. apply(compiler) {
  757. compiler.hooks.compilation.tap(plugin, (compilation) => {
  758. compilation.hooks.assetPath.tap(plugin, (path, data, assetInfo) => {
  759. // Default from output options so `[uniqueName]` resolves in every template
  760. if (data.uniqueName === undefined) {
  761. data.uniqueName = compilation.outputOptions.uniqueName;
  762. }
  763. // Digest the stored hashes use, so `[hash:<digest>]` can re-encode them
  764. if (data.hashDigest === undefined) {
  765. data.hashDigest = compilation.outputOptions.hashDigest;
  766. }
  767. // Untruncated compilation hash, so `[fullhash:<digest>]` keeps full entropy
  768. if (data.fullHash === undefined && data.hash === compilation.hash) {
  769. data.fullHash = compilation.fullHash;
  770. }
  771. // `RealContentHashPlugin` rehashes content; flag it so an inline digest
  772. // on `[contenthash]` is recorded and the recomputed hash re-encodes in it.
  773. if (data.realContentHash === undefined) {
  774. data.realContentHash = Boolean(
  775. compilation.options.optimization.realContentHash
  776. );
  777. }
  778. return interpolate(path, data, assetInfo);
  779. });
  780. });
  781. }
  782. }
  783. TemplatedPathPlugin.getPresentKinds = getPresentKinds;
  784. TemplatedPathPlugin.interpolate = interpolate;
  785. TemplatedPathPlugin.reEncodeDigest = reEncodeDigest;
  786. TemplatedPathPlugin.toContainedPath = toContainedPath;
  787. TemplatedPathPlugin.toTemplateSourceFileName = toTemplateSourceFileName;
  788. TemplatedPathPlugin.usesFullHashDigest = usesFullHashDigest;
  789. module.exports = TemplatedPathPlugin;