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;
|
---|