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