UNPKG

5.83 kBJavaScriptView Raw
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Jason Anderson @diurnalist
4*/
5"use strict";
6
7const REGEXP_HASH = /\[hash(?::(\d+))?\]/gi,
8 REGEXP_CHUNKHASH = /\[chunkhash(?::(\d+))?\]/gi,
9 REGEXP_MODULEHASH = /\[modulehash(?::(\d+))?\]/gi,
10 REGEXP_CONTENTHASH = /\[contenthash(?::(\d+))?\]/gi,
11 REGEXP_NAME = /\[name\]/gi,
12 REGEXP_ID = /\[id\]/gi,
13 REGEXP_MODULEID = /\[moduleid\]/gi,
14 REGEXP_FILE = /\[file\]/gi,
15 REGEXP_QUERY = /\[query\]/gi,
16 REGEXP_FILEBASE = /\[filebase\]/gi,
17 REGEXP_URL = /\[url\]/gi;
18
19// Using global RegExp for .test is dangerous
20// We use a normal RegExp instead of .test
21const REGEXP_HASH_FOR_TEST = new RegExp(REGEXP_HASH.source, "i"),
22 REGEXP_CHUNKHASH_FOR_TEST = new RegExp(REGEXP_CHUNKHASH.source, "i"),
23 REGEXP_CONTENTHASH_FOR_TEST = new RegExp(REGEXP_CONTENTHASH.source, "i"),
24 REGEXP_NAME_FOR_TEST = new RegExp(REGEXP_NAME.source, "i");
25
26const withHashLength = (replacer, handlerFn, assetInfo) => {
27 const fn = (match, hashLength, ...args) => {
28 if (assetInfo) assetInfo.immutable = true;
29 const length = hashLength && parseInt(hashLength, 10);
30 if (length && handlerFn) {
31 return handlerFn(length);
32 }
33 const hash = replacer(match, hashLength, ...args);
34 return length ? hash.slice(0, length) : hash;
35 };
36 return fn;
37};
38
39const getReplacer = (value, allowEmpty) => {
40 const fn = (match, ...args) => {
41 // last argument in replacer is the entire input string
42 const input = args[args.length - 1];
43 if (value === null || value === undefined) {
44 if (!allowEmpty) {
45 throw new Error(
46 `Path variable ${match} not implemented in this context: ${input}`
47 );
48 }
49 return "";
50 } else {
51 return `${escapePathVariables(value)}`;
52 }
53 };
54 return fn;
55};
56
57const escapePathVariables = value => {
58 return typeof value === "string"
59 ? value.replace(/\[(\\*[\w:]+\\*)\]/gi, "[\\$1\\]")
60 : value;
61};
62
63const replacePathVariables = (path, data, assetInfo) => {
64 const chunk = data.chunk;
65 const chunkId = chunk && chunk.id;
66 const chunkName = chunk && (chunk.name || chunk.id);
67 const chunkHash = chunk && (chunk.renderedHash || chunk.hash);
68 const chunkHashWithLength = chunk && chunk.hashWithLength;
69 const contentHashType = data.contentHashType;
70 const contentHash =
71 (chunk && chunk.contentHash && chunk.contentHash[contentHashType]) ||
72 data.contentHash;
73 const contentHashWithLength =
74 (chunk &&
75 chunk.contentHashWithLength &&
76 chunk.contentHashWithLength[contentHashType]) ||
77 data.contentHashWithLength;
78 const module = data.module;
79 const moduleId = module && module.id;
80 const moduleHash = module && (module.renderedHash || module.hash);
81 const moduleHashWithLength = module && module.hashWithLength;
82
83 if (typeof path === "function") {
84 path = path(data);
85 }
86
87 if (
88 data.noChunkHash &&
89 (REGEXP_CHUNKHASH_FOR_TEST.test(path) ||
90 REGEXP_CONTENTHASH_FOR_TEST.test(path))
91 ) {
92 throw new Error(
93 `Cannot use [chunkhash] or [contenthash] for chunk in '${path}' (use [hash] instead)`
94 );
95 }
96
97 return (
98 path
99 .replace(
100 REGEXP_HASH,
101 withHashLength(getReplacer(data.hash), data.hashWithLength, assetInfo)
102 )
103 .replace(
104 REGEXP_CHUNKHASH,
105 withHashLength(getReplacer(chunkHash), chunkHashWithLength, assetInfo)
106 )
107 .replace(
108 REGEXP_CONTENTHASH,
109 withHashLength(
110 getReplacer(contentHash),
111 contentHashWithLength,
112 assetInfo
113 )
114 )
115 .replace(
116 REGEXP_MODULEHASH,
117 withHashLength(getReplacer(moduleHash), moduleHashWithLength, assetInfo)
118 )
119 .replace(REGEXP_ID, getReplacer(chunkId))
120 .replace(REGEXP_MODULEID, getReplacer(moduleId))
121 .replace(REGEXP_NAME, getReplacer(chunkName))
122 .replace(REGEXP_FILE, getReplacer(data.filename))
123 .replace(REGEXP_FILEBASE, getReplacer(data.basename))
124 // query is optional, it's OK if it's in a path but there's nothing to replace it with
125 .replace(REGEXP_QUERY, getReplacer(data.query, true))
126 // only available in sourceMappingURLComment
127 .replace(REGEXP_URL, getReplacer(data.url))
128 .replace(/\[\\(\\*[\w:]+\\*)\\\]/gi, "[$1]")
129 );
130};
131
132class TemplatedPathPlugin {
133 apply(compiler) {
134 compiler.hooks.compilation.tap("TemplatedPathPlugin", compilation => {
135 const mainTemplate = compilation.mainTemplate;
136
137 mainTemplate.hooks.assetPath.tap(
138 "TemplatedPathPlugin",
139 replacePathVariables
140 );
141
142 mainTemplate.hooks.globalHash.tap(
143 "TemplatedPathPlugin",
144 (chunk, paths) => {
145 const outputOptions = mainTemplate.outputOptions;
146 const publicPath = outputOptions.publicPath || "";
147 const filename = outputOptions.filename || "";
148 const chunkFilename =
149 outputOptions.chunkFilename || outputOptions.filename;
150 if (
151 REGEXP_HASH_FOR_TEST.test(publicPath) ||
152 REGEXP_CHUNKHASH_FOR_TEST.test(publicPath) ||
153 REGEXP_CONTENTHASH_FOR_TEST.test(publicPath) ||
154 REGEXP_NAME_FOR_TEST.test(publicPath)
155 )
156 return true;
157 if (REGEXP_HASH_FOR_TEST.test(filename)) return true;
158 if (REGEXP_HASH_FOR_TEST.test(chunkFilename)) return true;
159 if (REGEXP_HASH_FOR_TEST.test(paths.join("|"))) return true;
160 }
161 );
162
163 mainTemplate.hooks.hashForChunk.tap(
164 "TemplatedPathPlugin",
165 (hash, chunk) => {
166 const outputOptions = mainTemplate.outputOptions;
167 const chunkFilename =
168 outputOptions.chunkFilename || outputOptions.filename;
169 if (REGEXP_CHUNKHASH_FOR_TEST.test(chunkFilename)) {
170 hash.update(JSON.stringify(chunk.getChunkMaps(true).hash));
171 }
172 if (REGEXP_CONTENTHASH_FOR_TEST.test(chunkFilename)) {
173 hash.update(
174 JSON.stringify(
175 chunk.getChunkMaps(true).contentHash.javascript || {}
176 )
177 );
178 }
179 if (REGEXP_NAME_FOR_TEST.test(chunkFilename)) {
180 hash.update(JSON.stringify(chunk.getChunkMaps(true).name));
181 }
182 }
183 );
184 });
185 }
186}
187
188module.exports = TemplatedPathPlugin;