UNPKG

9.97 kBJavaScriptView Raw
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Tobias Koppers @sokra
4*/
5
6"use strict";
7
8const RuntimeGlobals = require("./RuntimeGlobals");
9const ConstDependency = require("./dependencies/ConstDependency");
10const BasicEvaluatedExpression = require("./javascript/BasicEvaluatedExpression");
11const {
12 approve,
13 evaluateToString,
14 toConstantDependency
15} = require("./javascript/JavascriptParserHelpers");
16
17/** @typedef {import("estree").Expression} Expression */
18/** @typedef {import("./Compiler")} Compiler */
19/** @typedef {import("./RuntimeTemplate")} RuntimeTemplate */
20/** @typedef {import("./javascript/JavascriptParser")} JavascriptParser */
21
22/** @typedef {null|undefined|RegExp|Function|string|number|boolean|bigint|undefined} CodeValuePrimitive */
23/** @typedef {RecursiveArrayOrRecord<CodeValuePrimitive|RuntimeValue>} CodeValue */
24
25class RuntimeValue {
26 constructor(fn, fileDependencies) {
27 this.fn = fn;
28 this.fileDependencies = fileDependencies || [];
29 }
30
31 exec(parser) {
32 const buildInfo = parser.state.module.buildInfo;
33 if (this.fileDependencies === true) {
34 buildInfo.cacheable = false;
35 } else {
36 for (const fileDependency of this.fileDependencies) {
37 buildInfo.fileDependencies.add(fileDependency);
38 }
39 }
40
41 return this.fn({ module: parser.state.module });
42 }
43}
44
45/**
46 * @param {any[]|{[k: string]: any}} obj obj
47 * @param {JavascriptParser} parser Parser
48 * @param {RuntimeTemplate} runtimeTemplate the runtime template
49 * @param {boolean|undefined|null=} asiSafe asi safe (undefined: unknown, null: unneeded)
50 * @returns {string} code converted to string that evaluates
51 */
52const stringifyObj = (obj, parser, runtimeTemplate, asiSafe) => {
53 let code;
54 let arr = Array.isArray(obj);
55 if (arr) {
56 code = `[${obj
57 .map(code => toCode(code, parser, runtimeTemplate, null))
58 .join(",")}]`;
59 } else {
60 code = `{${Object.keys(obj)
61 .map(key => {
62 const code = obj[key];
63 return (
64 JSON.stringify(key) +
65 ":" +
66 toCode(code, parser, runtimeTemplate, null)
67 );
68 })
69 .join(",")}}`;
70 }
71
72 switch (asiSafe) {
73 case null:
74 return code;
75 case true:
76 return arr ? code : `(${code})`;
77 case false:
78 return arr ? `;${code}` : `;(${code})`;
79 default:
80 return `Object(${code})`;
81 }
82};
83
84/**
85 * Convert code to a string that evaluates
86 * @param {CodeValue} code Code to evaluate
87 * @param {JavascriptParser} parser Parser
88 * @param {RuntimeTemplate} runtimeTemplate the runtime template
89 * @param {boolean|undefined|null=} asiSafe asi safe (undefined: unknown, null: unneeded)
90 * @returns {string} code converted to string that evaluates
91 */
92const toCode = (code, parser, runtimeTemplate, asiSafe) => {
93 if (code === null) {
94 return "null";
95 }
96 if (code === undefined) {
97 return "undefined";
98 }
99 if (Object.is(code, -0)) {
100 return "-0";
101 }
102 if (code instanceof RuntimeValue) {
103 return toCode(code.exec(parser), parser, runtimeTemplate, asiSafe);
104 }
105 if (code instanceof RegExp && code.toString) {
106 return code.toString();
107 }
108 if (typeof code === "function" && code.toString) {
109 return "(" + code.toString() + ")";
110 }
111 if (typeof code === "object") {
112 return stringifyObj(code, parser, runtimeTemplate, asiSafe);
113 }
114 if (typeof code === "bigint") {
115 return runtimeTemplate.supportsBigIntLiteral()
116 ? `${code}n`
117 : `BigInt("${code}")`;
118 }
119 return code + "";
120};
121
122class DefinePlugin {
123 /**
124 * Create a new define plugin
125 * @param {Record<string, CodeValue>} definitions A map of global object definitions
126 */
127 constructor(definitions) {
128 this.definitions = definitions;
129 }
130
131 static runtimeValue(fn, fileDependencies) {
132 return new RuntimeValue(fn, fileDependencies);
133 }
134
135 /**
136 * Apply the plugin
137 * @param {Compiler} compiler the compiler instance
138 * @returns {void}
139 */
140 apply(compiler) {
141 const definitions = this.definitions;
142 compiler.hooks.compilation.tap(
143 "DefinePlugin",
144 (compilation, { normalModuleFactory }) => {
145 compilation.dependencyTemplates.set(
146 ConstDependency,
147 new ConstDependency.Template()
148 );
149 const { runtimeTemplate } = compilation;
150
151 /**
152 * Handler
153 * @param {JavascriptParser} parser Parser
154 * @returns {void}
155 */
156 const handler = parser => {
157 /**
158 * Walk definitions
159 * @param {Object} definitions Definitions map
160 * @param {string} prefix Prefix string
161 * @returns {void}
162 */
163 const walkDefinitions = (definitions, prefix) => {
164 Object.keys(definitions).forEach(key => {
165 const code = definitions[key];
166 if (
167 code &&
168 typeof code === "object" &&
169 !(code instanceof RuntimeValue) &&
170 !(code instanceof RegExp)
171 ) {
172 walkDefinitions(code, prefix + key + ".");
173 applyObjectDefine(prefix + key, code);
174 return;
175 }
176 applyDefineKey(prefix, key);
177 applyDefine(prefix + key, code);
178 });
179 };
180
181 /**
182 * Apply define key
183 * @param {string} prefix Prefix
184 * @param {string} key Key
185 * @returns {void}
186 */
187 const applyDefineKey = (prefix, key) => {
188 const splittedKey = key.split(".");
189 splittedKey.slice(1).forEach((_, i) => {
190 const fullKey = prefix + splittedKey.slice(0, i + 1).join(".");
191 parser.hooks.canRename.for(fullKey).tap("DefinePlugin", approve);
192 });
193 };
194
195 /**
196 * Apply Code
197 * @param {string} key Key
198 * @param {CodeValue} code Code
199 * @returns {void}
200 */
201 const applyDefine = (key, code) => {
202 const isTypeof = /^typeof\s+/.test(key);
203 if (isTypeof) key = key.replace(/^typeof\s+/, "");
204 let recurse = false;
205 let recurseTypeof = false;
206 if (!isTypeof) {
207 parser.hooks.canRename.for(key).tap("DefinePlugin", approve);
208 parser.hooks.evaluateIdentifier
209 .for(key)
210 .tap("DefinePlugin", expr => {
211 /**
212 * this is needed in case there is a recursion in the DefinePlugin
213 * to prevent an endless recursion
214 * e.g.: new DefinePlugin({
215 * "a": "b",
216 * "b": "a"
217 * });
218 */
219 if (recurse) return;
220 recurse = true;
221 const res = parser.evaluate(
222 toCode(code, parser, runtimeTemplate, null)
223 );
224 recurse = false;
225 res.setRange(expr.range);
226 return res;
227 });
228 parser.hooks.expression.for(key).tap("DefinePlugin", expr => {
229 const strCode = toCode(
230 code,
231 parser,
232 runtimeTemplate,
233 !parser.isAsiPosition(expr.range[0])
234 );
235 if (/__webpack_require__\s*(!?\.)/.test(strCode)) {
236 return toConstantDependency(parser, strCode, [
237 RuntimeGlobals.require
238 ])(expr);
239 } else if (/__webpack_require__/.test(strCode)) {
240 return toConstantDependency(parser, strCode, [
241 RuntimeGlobals.requireScope
242 ])(expr);
243 } else {
244 return toConstantDependency(parser, strCode)(expr);
245 }
246 });
247 }
248 parser.hooks.evaluateTypeof.for(key).tap("DefinePlugin", expr => {
249 /**
250 * this is needed in case there is a recursion in the DefinePlugin
251 * to prevent an endless recursion
252 * e.g.: new DefinePlugin({
253 * "typeof a": "typeof b",
254 * "typeof b": "typeof a"
255 * });
256 */
257 if (recurseTypeof) return;
258 recurseTypeof = true;
259 const typeofCode = isTypeof
260 ? toCode(code, parser, runtimeTemplate, null)
261 : "typeof (" +
262 toCode(code, parser, runtimeTemplate, null) +
263 ")";
264 const res = parser.evaluate(typeofCode);
265 recurseTypeof = false;
266 res.setRange(expr.range);
267 return res;
268 });
269 parser.hooks.typeof.for(key).tap("DefinePlugin", expr => {
270 const typeofCode = isTypeof
271 ? toCode(code, parser, runtimeTemplate, null)
272 : "typeof (" +
273 toCode(code, parser, runtimeTemplate, null) +
274 ")";
275 const res = parser.evaluate(typeofCode);
276 if (!res.isString()) return;
277 return toConstantDependency(
278 parser,
279 JSON.stringify(res.string)
280 ).bind(parser)(expr);
281 });
282 };
283
284 /**
285 * Apply Object
286 * @param {string} key Key
287 * @param {Object} obj Object
288 * @returns {void}
289 */
290 const applyObjectDefine = (key, obj) => {
291 parser.hooks.canRename.for(key).tap("DefinePlugin", approve);
292 parser.hooks.evaluateIdentifier
293 .for(key)
294 .tap("DefinePlugin", expr =>
295 new BasicEvaluatedExpression()
296 .setTruthy()
297 .setSideEffects(false)
298 .setRange(expr.range)
299 );
300 parser.hooks.evaluateTypeof
301 .for(key)
302 .tap("DefinePlugin", evaluateToString("object"));
303 parser.hooks.expression.for(key).tap("DefinePlugin", expr => {
304 const strCode = stringifyObj(
305 obj,
306 parser,
307 runtimeTemplate,
308 !parser.isAsiPosition(expr.range[0])
309 );
310
311 if (/__webpack_require__\s*(!?\.)/.test(strCode)) {
312 return toConstantDependency(parser, strCode, [
313 RuntimeGlobals.require
314 ])(expr);
315 } else if (/__webpack_require__/.test(strCode)) {
316 return toConstantDependency(parser, strCode, [
317 RuntimeGlobals.requireScope
318 ])(expr);
319 } else {
320 return toConstantDependency(parser, strCode)(expr);
321 }
322 });
323 parser.hooks.typeof
324 .for(key)
325 .tap(
326 "DefinePlugin",
327 toConstantDependency(parser, JSON.stringify("object"))
328 );
329 };
330
331 walkDefinitions(definitions, "");
332 };
333
334 normalModuleFactory.hooks.parser
335 .for("javascript/auto")
336 .tap("DefinePlugin", handler);
337 normalModuleFactory.hooks.parser
338 .for("javascript/dynamic")
339 .tap("DefinePlugin", handler);
340 normalModuleFactory.hooks.parser
341 .for("javascript/esm")
342 .tap("DefinePlugin", handler);
343 }
344 );
345 }
346}
347module.exports = DefinePlugin;