| 1 | /*
|
|---|
| 2 | MIT License http://www.opensource.org/licenses/mit-license.php
|
|---|
| 3 | Author Jason Anderson @diurnalist
|
|---|
| 4 | */
|
|---|
| 5 |
|
|---|
| 6 | "use strict";
|
|---|
| 7 |
|
|---|
| 8 | const { basename, extname } = require("path");
|
|---|
| 9 | const util = require("util");
|
|---|
| 10 | const Chunk = require("./Chunk");
|
|---|
| 11 | const Module = require("./Module");
|
|---|
| 12 | const { parseResource } = require("./util/identifier");
|
|---|
| 13 | const memoize = require("./util/memoize");
|
|---|
| 14 |
|
|---|
| 15 | const getMimeTypes = memoize(() => require("./util/mimeTypes"));
|
|---|
| 16 |
|
|---|
| 17 | /** @typedef {import("./ChunkGraph")} ChunkGraph */
|
|---|
| 18 | /** @typedef {import("./ChunkGraph").ModuleId} ModuleId */
|
|---|
| 19 | /** @typedef {import("./Compilation").AssetInfo} AssetInfo */
|
|---|
| 20 | /** @typedef {import("./Compilation").PathData} PathData */
|
|---|
| 21 | /** @typedef {import("./Compilation").PathDataChunk} PathDataChunk */
|
|---|
| 22 | /** @typedef {import("./Compilation").PathDataModule} PathDataModule */
|
|---|
| 23 | /** @typedef {import("./Compiler")} Compiler */
|
|---|
| 24 |
|
|---|
| 25 | const REGEXP = /\[\\*([\w:]+)\\*\]/g;
|
|---|
| 26 |
|
|---|
| 27 | /** @type {PathData["prepareId"]} */
|
|---|
| 28 | const prepareId = (id) => {
|
|---|
| 29 | if (typeof id !== "string") return id;
|
|---|
| 30 |
|
|---|
| 31 | if (/^"\s\+*.*\+\s*"$/.test(id)) {
|
|---|
| 32 | const match = /^"\s\+*\s*(.*)\s*\+\s*"$/.exec(id);
|
|---|
| 33 |
|
|---|
| 34 | return `" + (${
|
|---|
| 35 | /** @type {string[]} */ (match)[1]
|
|---|
| 36 | } + "").replace(/(^[.-]|[^a-zA-Z0-9_-])+/g, "_") + "`;
|
|---|
| 37 | }
|
|---|
| 38 |
|
|---|
| 39 | return id.replace(/(^[.-]|[^a-z0-9_-])+/gi, "_");
|
|---|
| 40 | };
|
|---|
| 41 |
|
|---|
| 42 | /**
|
|---|
| 43 | * Defines the replacer function callback.
|
|---|
| 44 | * @callback ReplacerFunction
|
|---|
| 45 | * @param {string} match
|
|---|
| 46 | * @param {string | undefined} arg
|
|---|
| 47 | * @param {string} input
|
|---|
| 48 | */
|
|---|
| 49 |
|
|---|
| 50 | /**
|
|---|
| 51 | * Returns hash replacer function.
|
|---|
| 52 | * @param {ReplacerFunction} replacer replacer
|
|---|
| 53 | * @param {((arg0: number) => string) | undefined} handler handler
|
|---|
| 54 | * @param {AssetInfo | undefined} assetInfo asset info
|
|---|
| 55 | * @param {string} hashName hash name
|
|---|
| 56 | * @returns {Replacer} hash replacer function
|
|---|
| 57 | */
|
|---|
| 58 | const hashLength = (replacer, handler, assetInfo, hashName) => {
|
|---|
| 59 | /** @type {Replacer} */
|
|---|
| 60 | const fn = (match, arg, input) => {
|
|---|
| 61 | /** @type {string} */
|
|---|
| 62 | let result;
|
|---|
| 63 | const length = arg && Number.parseInt(arg, 10);
|
|---|
| 64 |
|
|---|
| 65 | if (length && handler) {
|
|---|
| 66 | result = handler(length);
|
|---|
| 67 | } else {
|
|---|
| 68 | const hash = replacer(match, arg, input);
|
|---|
| 69 |
|
|---|
| 70 | result = length ? hash.slice(0, length) : hash;
|
|---|
| 71 | }
|
|---|
| 72 | if (assetInfo) {
|
|---|
| 73 | assetInfo.immutable = true;
|
|---|
| 74 | if (Array.isArray(assetInfo[hashName])) {
|
|---|
| 75 | assetInfo[hashName] = [...assetInfo[hashName], result];
|
|---|
| 76 | } else if (assetInfo[hashName]) {
|
|---|
| 77 | assetInfo[hashName] = [assetInfo[hashName], result];
|
|---|
| 78 | } else {
|
|---|
| 79 | assetInfo[hashName] = result;
|
|---|
| 80 | }
|
|---|
| 81 | }
|
|---|
| 82 | return result;
|
|---|
| 83 | };
|
|---|
| 84 |
|
|---|
| 85 | return fn;
|
|---|
| 86 | };
|
|---|
| 87 |
|
|---|
| 88 | /** @typedef {(match: string, arg: string | undefined, input: string) => string} Replacer */
|
|---|
| 89 |
|
|---|
| 90 | /**
|
|---|
| 91 | * Returns replacer.
|
|---|
| 92 | * @param {string | number | null | undefined | (() => string | number | null | undefined)} value value
|
|---|
| 93 | * @param {boolean=} allowEmpty allow empty
|
|---|
| 94 | * @returns {Replacer} replacer
|
|---|
| 95 | */
|
|---|
| 96 | const replacer = (value, allowEmpty) => {
|
|---|
| 97 | /** @type {Replacer} */
|
|---|
| 98 | const fn = (match, arg, input) => {
|
|---|
| 99 | if (typeof value === "function") {
|
|---|
| 100 | value = value();
|
|---|
| 101 | }
|
|---|
| 102 | if (value === null || value === undefined) {
|
|---|
| 103 | if (!allowEmpty) {
|
|---|
| 104 | throw new Error(
|
|---|
| 105 | `Path variable ${match} not implemented in this context: ${input}`
|
|---|
| 106 | );
|
|---|
| 107 | }
|
|---|
| 108 |
|
|---|
| 109 | return "";
|
|---|
| 110 | }
|
|---|
| 111 |
|
|---|
| 112 | return `${value}`;
|
|---|
| 113 | };
|
|---|
| 114 |
|
|---|
| 115 | return fn;
|
|---|
| 116 | };
|
|---|
| 117 |
|
|---|
| 118 | /** @type {Map<string, (...args: EXPECTED_ANY[]) => EXPECTED_ANY>} */
|
|---|
| 119 | const deprecationCache = new Map();
|
|---|
| 120 | const deprecatedFunction = (() => () => {})();
|
|---|
| 121 | /**
|
|---|
| 122 | * Returns function with deprecation output.
|
|---|
| 123 | * @template {(...args: EXPECTED_ANY[]) => EXPECTED_ANY} T
|
|---|
| 124 | * @param {T} fn function
|
|---|
| 125 | * @param {string} message message
|
|---|
| 126 | * @param {string} code code
|
|---|
| 127 | * @returns {T} function with deprecation output
|
|---|
| 128 | */
|
|---|
| 129 | const deprecated = (fn, message, code) => {
|
|---|
| 130 | let d = deprecationCache.get(message);
|
|---|
| 131 | if (d === undefined) {
|
|---|
| 132 | d = util.deprecate(deprecatedFunction, message, code);
|
|---|
| 133 | deprecationCache.set(message, d);
|
|---|
| 134 | }
|
|---|
| 135 | return /** @type {T} */ (
|
|---|
| 136 | (...args) => {
|
|---|
| 137 | d();
|
|---|
| 138 | return fn(...args);
|
|---|
| 139 | }
|
|---|
| 140 | );
|
|---|
| 141 | };
|
|---|
| 142 |
|
|---|
| 143 | /**
|
|---|
| 144 | * Callback used to compute a path from contextual data. The type parameter
|
|---|
| 145 | * narrows the `pathData` shape when the caller knows it operates in a chunk
|
|---|
| 146 | * (`PathDataChunk`) or module (`PathDataModule`) context — defaults to the
|
|---|
| 147 | * fully-optional `PathData` for backward compatibility.
|
|---|
| 148 | * @template {PathData} [T=PathData]
|
|---|
| 149 | * @typedef {(pathData: T, assetInfo?: AssetInfo) => string} TemplatePathFn
|
|---|
| 150 | */
|
|---|
| 151 |
|
|---|
| 152 | /**
|
|---|
| 153 | * Either a raw template string (e.g. `"[name].[contenthash].js"`) or a
|
|---|
| 154 | * generic `TemplatePathFn`. Method signatures that need to thread a narrowed
|
|---|
| 155 | * `PathData` shape spell the function side out as `TemplatePathFn<T>`
|
|---|
| 156 | * directly — `TemplatePath` itself stays a plain alias so local JSDoc
|
|---|
| 157 | * re-imports keep a single shared identity.
|
|---|
| 158 | * @typedef {string | TemplatePathFn} TemplatePath
|
|---|
| 159 | */
|
|---|
| 160 |
|
|---|
| 161 | /**
|
|---|
| 162 | * Returns the interpolated path.
|
|---|
| 163 | * @template {PathData} [T=PathData]
|
|---|
| 164 | * @param {string | TemplatePathFn<T>} path the raw path
|
|---|
| 165 | * @param {T} data context data
|
|---|
| 166 | * @param {AssetInfo=} assetInfo extra info about the asset (will be written to)
|
|---|
| 167 | * @returns {string} the interpolated path
|
|---|
| 168 | */
|
|---|
| 169 | const interpolate = (path, data, assetInfo) => {
|
|---|
| 170 | const chunkGraph = data.chunkGraph;
|
|---|
| 171 |
|
|---|
| 172 | /** @type {Map<string, Replacer>} */
|
|---|
| 173 | const replacements = new Map();
|
|---|
| 174 |
|
|---|
| 175 | // Filename context
|
|---|
| 176 | //
|
|---|
| 177 | // Placeholders
|
|---|
| 178 | //
|
|---|
| 179 | // for /some/path/file.js?query#fragment:
|
|---|
| 180 | // [file] - /some/path/file.js
|
|---|
| 181 | // [query] - ?query
|
|---|
| 182 | // [fragment] - #fragment
|
|---|
| 183 | // [base] - file.js
|
|---|
| 184 | // [path] - /some/path/
|
|---|
| 185 | // [name] - file
|
|---|
| 186 | // [ext] - .js
|
|---|
| 187 | if (typeof data.filename === "string") {
|
|---|
| 188 | // check that filename is data uri
|
|---|
| 189 | const match = data.filename.match(/^data:([^;,]+)/);
|
|---|
| 190 | if (match) {
|
|---|
| 191 | const ext = getMimeTypes().extension(match[1]);
|
|---|
| 192 | const emptyReplacer = replacer("", true);
|
|---|
| 193 | // "XXXX" used for `updateHash`, so we don't need it here
|
|---|
| 194 | const contentHash =
|
|---|
| 195 | data.contentHash && !/X+/.test(data.contentHash)
|
|---|
| 196 | ? data.contentHash
|
|---|
| 197 | : false;
|
|---|
| 198 | const baseReplacer = contentHash ? replacer(contentHash) : emptyReplacer;
|
|---|
| 199 |
|
|---|
| 200 | replacements.set("file", emptyReplacer);
|
|---|
| 201 | replacements.set("query", emptyReplacer);
|
|---|
| 202 | replacements.set("fragment", emptyReplacer);
|
|---|
| 203 | replacements.set("path", emptyReplacer);
|
|---|
| 204 | replacements.set("base", baseReplacer);
|
|---|
| 205 | replacements.set("name", baseReplacer);
|
|---|
| 206 | replacements.set("ext", replacer(ext ? `.${ext}` : "", true));
|
|---|
| 207 | // Legacy
|
|---|
| 208 | replacements.set(
|
|---|
| 209 | "filebase",
|
|---|
| 210 | deprecated(
|
|---|
| 211 | baseReplacer,
|
|---|
| 212 | "[filebase] is now [base]",
|
|---|
| 213 | "DEP_WEBPACK_TEMPLATE_PATH_PLUGIN_REPLACE_PATH_VARIABLES_FILENAME"
|
|---|
| 214 | )
|
|---|
| 215 | );
|
|---|
| 216 | } else {
|
|---|
| 217 | const { path: file, query, fragment } = parseResource(data.filename);
|
|---|
| 218 |
|
|---|
| 219 | const ext = extname(file);
|
|---|
| 220 | const base = basename(file);
|
|---|
| 221 | const name = base.slice(0, base.length - ext.length);
|
|---|
| 222 | const path = file.slice(0, file.length - base.length);
|
|---|
| 223 |
|
|---|
| 224 | replacements.set("file", replacer(file));
|
|---|
| 225 | replacements.set("query", replacer(query, true));
|
|---|
| 226 | replacements.set("fragment", replacer(fragment, true));
|
|---|
| 227 | replacements.set("path", replacer(path, true));
|
|---|
| 228 | replacements.set("base", replacer(base));
|
|---|
| 229 | replacements.set("name", replacer(name));
|
|---|
| 230 | replacements.set("ext", replacer(ext, true));
|
|---|
| 231 | // Legacy
|
|---|
| 232 | replacements.set(
|
|---|
| 233 | "filebase",
|
|---|
| 234 | deprecated(
|
|---|
| 235 | replacer(base),
|
|---|
| 236 | "[filebase] is now [base]",
|
|---|
| 237 | "DEP_WEBPACK_TEMPLATE_PATH_PLUGIN_REPLACE_PATH_VARIABLES_FILENAME"
|
|---|
| 238 | )
|
|---|
| 239 | );
|
|---|
| 240 | }
|
|---|
| 241 | }
|
|---|
| 242 |
|
|---|
| 243 | // Compilation context
|
|---|
| 244 | //
|
|---|
| 245 | // Placeholders
|
|---|
| 246 | //
|
|---|
| 247 | // [fullhash] - data.hash (3a4b5c6e7f)
|
|---|
| 248 | //
|
|---|
| 249 | // Legacy Placeholders
|
|---|
| 250 | //
|
|---|
| 251 | // [hash] - data.hash (3a4b5c6e7f)
|
|---|
| 252 | if (data.hash) {
|
|---|
| 253 | const hashReplacer = hashLength(
|
|---|
| 254 | replacer(data.hash),
|
|---|
| 255 | data.hashWithLength,
|
|---|
| 256 | assetInfo,
|
|---|
| 257 | "fullhash"
|
|---|
| 258 | );
|
|---|
| 259 |
|
|---|
| 260 | replacements.set("fullhash", hashReplacer);
|
|---|
| 261 |
|
|---|
| 262 | // Legacy
|
|---|
| 263 | replacements.set(
|
|---|
| 264 | "hash",
|
|---|
| 265 | deprecated(
|
|---|
| 266 | hashReplacer,
|
|---|
| 267 | "[hash] is now [fullhash] (also consider using [chunkhash] or [contenthash], see documentation for details)",
|
|---|
| 268 | "DEP_WEBPACK_TEMPLATE_PATH_PLUGIN_REPLACE_PATH_VARIABLES_HASH"
|
|---|
| 269 | )
|
|---|
| 270 | );
|
|---|
| 271 | }
|
|---|
| 272 |
|
|---|
| 273 | // Chunk Context
|
|---|
| 274 | //
|
|---|
| 275 | // Placeholders
|
|---|
| 276 | //
|
|---|
| 277 | // [id] - chunk.id (0.js)
|
|---|
| 278 | // [name] - chunk.name (app.js)
|
|---|
| 279 | // [chunkhash] - chunk.hash (7823t4t4.js)
|
|---|
| 280 | // [contenthash] - chunk.contentHash[type] (3256u3zg.js)
|
|---|
| 281 | if (data.chunk) {
|
|---|
| 282 | const chunk = data.chunk;
|
|---|
| 283 |
|
|---|
| 284 | const contentHashType = data.contentHashType;
|
|---|
| 285 |
|
|---|
| 286 | const idReplacer = replacer(chunk.id);
|
|---|
| 287 | const nameReplacer = replacer(chunk.name || chunk.id);
|
|---|
| 288 | const chunkhashReplacer = hashLength(
|
|---|
| 289 | replacer(chunk instanceof Chunk ? chunk.renderedHash : chunk.hash),
|
|---|
| 290 | "hashWithLength" in chunk ? chunk.hashWithLength : undefined,
|
|---|
| 291 | assetInfo,
|
|---|
| 292 | "chunkhash"
|
|---|
| 293 | );
|
|---|
| 294 | const contenthashReplacer = hashLength(
|
|---|
| 295 | replacer(
|
|---|
| 296 | data.contentHash ||
|
|---|
| 297 | (contentHashType &&
|
|---|
| 298 | chunk.contentHash &&
|
|---|
| 299 | chunk.contentHash[contentHashType])
|
|---|
| 300 | ),
|
|---|
| 301 | data.contentHashWithLength ||
|
|---|
| 302 | ("contentHashWithLength" in chunk && chunk.contentHashWithLength
|
|---|
| 303 | ? chunk.contentHashWithLength[/** @type {string} */ (contentHashType)]
|
|---|
| 304 | : undefined),
|
|---|
| 305 | assetInfo,
|
|---|
| 306 | "contenthash"
|
|---|
| 307 | );
|
|---|
| 308 |
|
|---|
| 309 | replacements.set("id", idReplacer);
|
|---|
| 310 | replacements.set("name", nameReplacer);
|
|---|
| 311 | replacements.set("chunkhash", chunkhashReplacer);
|
|---|
| 312 | replacements.set("contenthash", contenthashReplacer);
|
|---|
| 313 | }
|
|---|
| 314 |
|
|---|
| 315 | // Module Context
|
|---|
| 316 | //
|
|---|
| 317 | // Placeholders
|
|---|
| 318 | //
|
|---|
| 319 | // [id] - module.id (2.png)
|
|---|
| 320 | // [hash] - module.hash (6237543873.png)
|
|---|
| 321 | //
|
|---|
| 322 | // Legacy Placeholders
|
|---|
| 323 | //
|
|---|
| 324 | // [moduleid] - module.id (2.png)
|
|---|
| 325 | // [modulehash] - module.hash (6237543873.png)
|
|---|
| 326 | if (data.module) {
|
|---|
| 327 | const module = data.module;
|
|---|
| 328 |
|
|---|
| 329 | const idReplacer = replacer(() =>
|
|---|
| 330 | (data.prepareId || prepareId)(
|
|---|
| 331 | module instanceof Module
|
|---|
| 332 | ? /** @type {ModuleId} */
|
|---|
| 333 | (/** @type {ChunkGraph} */ (chunkGraph).getModuleId(module))
|
|---|
| 334 | : module.id
|
|---|
| 335 | )
|
|---|
| 336 | );
|
|---|
| 337 | const moduleHashReplacer = hashLength(
|
|---|
| 338 | replacer(() =>
|
|---|
| 339 | module instanceof Module
|
|---|
| 340 | ? /** @type {ChunkGraph} */
|
|---|
| 341 | (chunkGraph).getRenderedModuleHash(module, data.runtime)
|
|---|
| 342 | : module.hash
|
|---|
| 343 | ),
|
|---|
| 344 | "hashWithLength" in module ? module.hashWithLength : undefined,
|
|---|
| 345 | assetInfo,
|
|---|
| 346 | "modulehash"
|
|---|
| 347 | );
|
|---|
| 348 | const contentHashReplacer = hashLength(
|
|---|
| 349 | replacer(/** @type {string} */ (data.contentHash)),
|
|---|
| 350 | undefined,
|
|---|
| 351 | assetInfo,
|
|---|
| 352 | "contenthash"
|
|---|
| 353 | );
|
|---|
| 354 |
|
|---|
| 355 | replacements.set("id", idReplacer);
|
|---|
| 356 | replacements.set("modulehash", moduleHashReplacer);
|
|---|
| 357 | replacements.set("contenthash", contentHashReplacer);
|
|---|
| 358 | replacements.set(
|
|---|
| 359 | "hash",
|
|---|
| 360 | data.contentHash ? contentHashReplacer : moduleHashReplacer
|
|---|
| 361 | );
|
|---|
| 362 | // Legacy
|
|---|
| 363 | replacements.set(
|
|---|
| 364 | "moduleid",
|
|---|
| 365 | deprecated(
|
|---|
| 366 | idReplacer,
|
|---|
| 367 | "[moduleid] is now [id]",
|
|---|
| 368 | "DEP_WEBPACK_TEMPLATE_PATH_PLUGIN_REPLACE_PATH_VARIABLES_MODULE_ID"
|
|---|
| 369 | )
|
|---|
| 370 | );
|
|---|
| 371 | }
|
|---|
| 372 |
|
|---|
| 373 | // Other things
|
|---|
| 374 | if (data.url) {
|
|---|
| 375 | replacements.set("url", replacer(data.url));
|
|---|
| 376 | }
|
|---|
| 377 | if (typeof data.runtime === "string") {
|
|---|
| 378 | replacements.set(
|
|---|
| 379 | "runtime",
|
|---|
| 380 | replacer(() =>
|
|---|
| 381 | (data.prepareId || prepareId)(/** @type {string} */ (data.runtime))
|
|---|
| 382 | )
|
|---|
| 383 | );
|
|---|
| 384 | } else {
|
|---|
| 385 | replacements.set("runtime", replacer("_"));
|
|---|
| 386 | }
|
|---|
| 387 |
|
|---|
| 388 | if (typeof path === "function") {
|
|---|
| 389 | path = path(data, assetInfo);
|
|---|
| 390 | }
|
|---|
| 391 |
|
|---|
| 392 | path = path.replace(REGEXP, (match, content) => {
|
|---|
| 393 | if (content.length + 2 === match.length) {
|
|---|
| 394 | const contentMatch = /^(\w+)(?::(\w+))?$/.exec(content);
|
|---|
| 395 | if (!contentMatch) return match;
|
|---|
| 396 | const [, kind, arg] = contentMatch;
|
|---|
| 397 | const replacer = replacements.get(kind);
|
|---|
| 398 | if (replacer !== undefined) {
|
|---|
| 399 | return replacer(match, arg, /** @type {string} */ (path));
|
|---|
| 400 | }
|
|---|
| 401 | } else if (match.startsWith("[\\") && match.endsWith("\\]")) {
|
|---|
| 402 | return `[${match.slice(2, -2)}]`;
|
|---|
| 403 | }
|
|---|
| 404 | return match;
|
|---|
| 405 | });
|
|---|
| 406 |
|
|---|
| 407 | return path;
|
|---|
| 408 | };
|
|---|
| 409 |
|
|---|
| 410 | const plugin = "TemplatedPathPlugin";
|
|---|
| 411 |
|
|---|
| 412 | class TemplatedPathPlugin {
|
|---|
| 413 | /**
|
|---|
| 414 | * Applies the plugin by registering its hooks on the compiler.
|
|---|
| 415 | * @param {Compiler} compiler the compiler instance
|
|---|
| 416 | * @returns {void}
|
|---|
| 417 | */
|
|---|
| 418 | apply(compiler) {
|
|---|
| 419 | compiler.hooks.compilation.tap(plugin, (compilation) => {
|
|---|
| 420 | compilation.hooks.assetPath.tap(plugin, interpolate);
|
|---|
| 421 | });
|
|---|
| 422 | }
|
|---|
| 423 | }
|
|---|
| 424 |
|
|---|
| 425 | module.exports = TemplatedPathPlugin;
|
|---|
| 426 | module.exports.interpolate = interpolate;
|
|---|