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