UNPKG

15.8 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 WebpackError = require("./WebpackError");
10const ConstDependency = require("./dependencies/ConstDependency");
11const BasicEvaluatedExpression = require("./javascript/BasicEvaluatedExpression");
12const {
13 evaluateToString,
14 toConstantDependency
15} = require("./javascript/JavascriptParserHelpers");
16
17/** @typedef {import("estree").Expression} Expression */
18/** @typedef {import("./Compiler")} Compiler */
19/** @typedef {import("./NormalModule")} NormalModule */
20/** @typedef {import("./RuntimeTemplate")} RuntimeTemplate */
21/** @typedef {import("./javascript/JavascriptParser")} JavascriptParser */
22
23/** @typedef {null|undefined|RegExp|Function|string|number|boolean|bigint|undefined} CodeValuePrimitive */
24/** @typedef {RecursiveArrayOrRecord<CodeValuePrimitive|RuntimeValue>} CodeValue */
25
26/**
27 * @typedef {Object} RuntimeValueOptions
28 * @property {string[]=} fileDependencies
29 * @property {string[]=} contextDependencies
30 * @property {string[]=} missingDependencies
31 * @property {string[]=} buildDependencies
32 * @property {string|function(): string=} version
33 */
34
35class RuntimeValue {
36 /**
37 * @param {function({ module: NormalModule, key: string, readonly version: string | undefined }): CodeValuePrimitive} fn generator function
38 * @param {true | string[] | RuntimeValueOptions=} options options
39 */
40 constructor(fn, options) {
41 this.fn = fn;
42 if (Array.isArray(options)) {
43 options = {
44 fileDependencies: options
45 };
46 }
47 this.options = options || {};
48 }
49
50 get fileDependencies() {
51 return this.options === true ? true : this.options.fileDependencies;
52 }
53
54 /**
55 * @param {JavascriptParser} parser the parser
56 * @param {Map<string, string>} valueCacheVersions valueCacheVersions
57 * @param {string} key the defined key
58 * @returns {CodeValuePrimitive} code
59 */
60 exec(parser, valueCacheVersions, key) {
61 const buildInfo = parser.state.module.buildInfo;
62 if (this.options === true) {
63 buildInfo.cacheable = false;
64 } else {
65 if (this.options.fileDependencies) {
66 for (const dep of this.options.fileDependencies) {
67 buildInfo.fileDependencies.add(dep);
68 }
69 }
70 if (this.options.contextDependencies) {
71 for (const dep of this.options.contextDependencies) {
72 buildInfo.contextDependencies.add(dep);
73 }
74 }
75 if (this.options.missingDependencies) {
76 for (const dep of this.options.missingDependencies) {
77 buildInfo.missingDependencies.add(dep);
78 }
79 }
80 if (this.options.buildDependencies) {
81 for (const dep of this.options.buildDependencies) {
82 buildInfo.buildDependencies.add(dep);
83 }
84 }
85 }
86
87 return this.fn({
88 module: parser.state.module,
89 key,
90 get version() {
91 return valueCacheVersions.get(VALUE_DEP_PREFIX + key);
92 }
93 });
94 }
95
96 getCacheVersion() {
97 return this.options === true
98 ? undefined
99 : (typeof this.options.version === "function"
100 ? this.options.version()
101 : this.options.version) || "unset";
102 }
103}
104
105/**
106 * @param {any[]|{[k: string]: any}} obj obj
107 * @param {JavascriptParser} parser Parser
108 * @param {Map<string, string>} valueCacheVersions valueCacheVersions
109 * @param {string} key the defined key
110 * @param {RuntimeTemplate} runtimeTemplate the runtime template
111 * @param {boolean|undefined|null=} asiSafe asi safe (undefined: unknown, null: unneeded)
112 * @returns {string} code converted to string that evaluates
113 */
114const stringifyObj = (
115 obj,
116 parser,
117 valueCacheVersions,
118 key,
119 runtimeTemplate,
120 asiSafe
121) => {
122 let code;
123 let arr = Array.isArray(obj);
124 if (arr) {
125 code = `[${obj
126 .map(code =>
127 toCode(code, parser, valueCacheVersions, key, runtimeTemplate, null)
128 )
129 .join(",")}]`;
130 } else {
131 code = `{${Object.keys(obj)
132 .map(key => {
133 const code = obj[key];
134 return (
135 JSON.stringify(key) +
136 ":" +
137 toCode(code, parser, valueCacheVersions, key, runtimeTemplate, null)
138 );
139 })
140 .join(",")}}`;
141 }
142
143 switch (asiSafe) {
144 case null:
145 return code;
146 case true:
147 return arr ? code : `(${code})`;
148 case false:
149 return arr ? `;${code}` : `;(${code})`;
150 default:
151 return `Object(${code})`;
152 }
153};
154
155/**
156 * Convert code to a string that evaluates
157 * @param {CodeValue} code Code to evaluate
158 * @param {JavascriptParser} parser Parser
159 * @param {Map<string, string>} valueCacheVersions valueCacheVersions
160 * @param {string} key the defined key
161 * @param {RuntimeTemplate} runtimeTemplate the runtime template
162 * @param {boolean|undefined|null=} asiSafe asi safe (undefined: unknown, null: unneeded)
163 * @returns {string} code converted to string that evaluates
164 */
165const toCode = (
166 code,
167 parser,
168 valueCacheVersions,
169 key,
170 runtimeTemplate,
171 asiSafe
172) => {
173 if (code === null) {
174 return "null";
175 }
176 if (code === undefined) {
177 return "undefined";
178 }
179 if (Object.is(code, -0)) {
180 return "-0";
181 }
182 if (code instanceof RuntimeValue) {
183 return toCode(
184 code.exec(parser, valueCacheVersions, key),
185 parser,
186 valueCacheVersions,
187 key,
188 runtimeTemplate,
189 asiSafe
190 );
191 }
192 if (code instanceof RegExp && code.toString) {
193 return code.toString();
194 }
195 if (typeof code === "function" && code.toString) {
196 return "(" + code.toString() + ")";
197 }
198 if (typeof code === "object") {
199 return stringifyObj(
200 code,
201 parser,
202 valueCacheVersions,
203 key,
204 runtimeTemplate,
205 asiSafe
206 );
207 }
208 if (typeof code === "bigint") {
209 return runtimeTemplate.supportsBigIntLiteral()
210 ? `${code}n`
211 : `BigInt("${code}")`;
212 }
213 return code + "";
214};
215
216const toCacheVersion = code => {
217 if (code === null) {
218 return "null";
219 }
220 if (code === undefined) {
221 return "undefined";
222 }
223 if (Object.is(code, -0)) {
224 return "-0";
225 }
226 if (code instanceof RuntimeValue) {
227 return code.getCacheVersion();
228 }
229 if (code instanceof RegExp && code.toString) {
230 return code.toString();
231 }
232 if (typeof code === "function" && code.toString) {
233 return "(" + code.toString() + ")";
234 }
235 if (typeof code === "object") {
236 const items = Object.keys(code).map(key => ({
237 key,
238 value: toCacheVersion(code[key])
239 }));
240 if (items.some(({ value }) => value === undefined)) return undefined;
241 return `{${items.map(({ key, value }) => `${key}: ${value}`).join(", ")}}`;
242 }
243 if (typeof code === "bigint") {
244 return `${code}n`;
245 }
246 return code + "";
247};
248
249const VALUE_DEP_PREFIX = "webpack/DefinePlugin ";
250
251class DefinePlugin {
252 /**
253 * Create a new define plugin
254 * @param {Record<string, CodeValue>} definitions A map of global object definitions
255 */
256 constructor(definitions) {
257 this.definitions = definitions;
258 }
259
260 /**
261 * @param {function({ module: NormalModule, key: string, readonly version: string | undefined }): CodeValuePrimitive} fn generator function
262 * @param {true | string[] | RuntimeValueOptions=} options options
263 * @returns {RuntimeValue} runtime value
264 */
265 static runtimeValue(fn, options) {
266 return new RuntimeValue(fn, options);
267 }
268
269 /**
270 * Apply the plugin
271 * @param {Compiler} compiler the compiler instance
272 * @returns {void}
273 */
274 apply(compiler) {
275 const definitions = this.definitions;
276 compiler.hooks.compilation.tap(
277 "DefinePlugin",
278 (compilation, { normalModuleFactory }) => {
279 compilation.dependencyTemplates.set(
280 ConstDependency,
281 new ConstDependency.Template()
282 );
283 const { runtimeTemplate } = compilation;
284
285 /**
286 * Handler
287 * @param {JavascriptParser} parser Parser
288 * @returns {void}
289 */
290 const handler = parser => {
291 const addValueDependency = key => {
292 const { buildInfo } = parser.state.module;
293 if (!buildInfo.valueDependencies)
294 buildInfo.valueDependencies = new Map();
295 buildInfo.valueDependencies.set(
296 VALUE_DEP_PREFIX + key,
297 compilation.valueCacheVersions.get(VALUE_DEP_PREFIX + key)
298 );
299 };
300
301 const withValueDependency = (key, fn) => (...args) => {
302 addValueDependency(key);
303 return fn(...args);
304 };
305
306 /**
307 * Walk definitions
308 * @param {Object} definitions Definitions map
309 * @param {string} prefix Prefix string
310 * @returns {void}
311 */
312 const walkDefinitions = (definitions, prefix) => {
313 Object.keys(definitions).forEach(key => {
314 const code = definitions[key];
315 if (
316 code &&
317 typeof code === "object" &&
318 !(code instanceof RuntimeValue) &&
319 !(code instanceof RegExp)
320 ) {
321 walkDefinitions(code, prefix + key + ".");
322 applyObjectDefine(prefix + key, code);
323 return;
324 }
325 applyDefineKey(prefix, key);
326 applyDefine(prefix + key, code);
327 });
328 };
329
330 /**
331 * Apply define key
332 * @param {string} prefix Prefix
333 * @param {string} key Key
334 * @returns {void}
335 */
336 const applyDefineKey = (prefix, key) => {
337 const splittedKey = key.split(".");
338 splittedKey.slice(1).forEach((_, i) => {
339 const fullKey = prefix + splittedKey.slice(0, i + 1).join(".");
340 parser.hooks.canRename.for(fullKey).tap("DefinePlugin", () => {
341 addValueDependency(key);
342 return true;
343 });
344 });
345 };
346
347 /**
348 * Apply Code
349 * @param {string} key Key
350 * @param {CodeValue} code Code
351 * @returns {void}
352 */
353 const applyDefine = (key, code) => {
354 const originalKey = key;
355 const isTypeof = /^typeof\s+/.test(key);
356 if (isTypeof) key = key.replace(/^typeof\s+/, "");
357 let recurse = false;
358 let recurseTypeof = false;
359 if (!isTypeof) {
360 parser.hooks.canRename.for(key).tap("DefinePlugin", () => {
361 addValueDependency(originalKey);
362 return true;
363 });
364 parser.hooks.evaluateIdentifier
365 .for(key)
366 .tap("DefinePlugin", expr => {
367 /**
368 * this is needed in case there is a recursion in the DefinePlugin
369 * to prevent an endless recursion
370 * e.g.: new DefinePlugin({
371 * "a": "b",
372 * "b": "a"
373 * });
374 */
375 if (recurse) return;
376 addValueDependency(originalKey);
377 recurse = true;
378 const res = parser.evaluate(
379 toCode(
380 code,
381 parser,
382 compilation.valueCacheVersions,
383 key,
384 runtimeTemplate,
385 null
386 )
387 );
388 recurse = false;
389 res.setRange(expr.range);
390 return res;
391 });
392 parser.hooks.expression.for(key).tap("DefinePlugin", expr => {
393 addValueDependency(originalKey);
394 const strCode = toCode(
395 code,
396 parser,
397 compilation.valueCacheVersions,
398 originalKey,
399 runtimeTemplate,
400 !parser.isAsiPosition(expr.range[0])
401 );
402 if (/__webpack_require__\s*(!?\.)/.test(strCode)) {
403 return toConstantDependency(parser, strCode, [
404 RuntimeGlobals.require
405 ])(expr);
406 } else if (/__webpack_require__/.test(strCode)) {
407 return toConstantDependency(parser, strCode, [
408 RuntimeGlobals.requireScope
409 ])(expr);
410 } else {
411 return toConstantDependency(parser, strCode)(expr);
412 }
413 });
414 }
415 parser.hooks.evaluateTypeof.for(key).tap("DefinePlugin", expr => {
416 /**
417 * this is needed in case there is a recursion in the DefinePlugin
418 * to prevent an endless recursion
419 * e.g.: new DefinePlugin({
420 * "typeof a": "typeof b",
421 * "typeof b": "typeof a"
422 * });
423 */
424 if (recurseTypeof) return;
425 recurseTypeof = true;
426 addValueDependency(originalKey);
427 const codeCode = toCode(
428 code,
429 parser,
430 compilation.valueCacheVersions,
431 originalKey,
432 runtimeTemplate,
433 null
434 );
435 const typeofCode = isTypeof
436 ? codeCode
437 : "typeof (" + codeCode + ")";
438 const res = parser.evaluate(typeofCode);
439 recurseTypeof = false;
440 res.setRange(expr.range);
441 return res;
442 });
443 parser.hooks.typeof.for(key).tap("DefinePlugin", expr => {
444 addValueDependency(originalKey);
445 const codeCode = toCode(
446 code,
447 parser,
448 compilation.valueCacheVersions,
449 originalKey,
450 runtimeTemplate,
451 null
452 );
453 const typeofCode = isTypeof
454 ? codeCode
455 : "typeof (" + codeCode + ")";
456 const res = parser.evaluate(typeofCode);
457 if (!res.isString()) return;
458 return toConstantDependency(
459 parser,
460 JSON.stringify(res.string)
461 ).bind(parser)(expr);
462 });
463 };
464
465 /**
466 * Apply Object
467 * @param {string} key Key
468 * @param {Object} obj Object
469 * @returns {void}
470 */
471 const applyObjectDefine = (key, obj) => {
472 parser.hooks.canRename.for(key).tap("DefinePlugin", () => {
473 addValueDependency(key);
474 return true;
475 });
476 parser.hooks.evaluateIdentifier
477 .for(key)
478 .tap("DefinePlugin", expr => {
479 addValueDependency(key);
480 return new BasicEvaluatedExpression()
481 .setTruthy()
482 .setSideEffects(false)
483 .setRange(expr.range);
484 });
485 parser.hooks.evaluateTypeof
486 .for(key)
487 .tap(
488 "DefinePlugin",
489 withValueDependency(key, evaluateToString("object"))
490 );
491 parser.hooks.expression.for(key).tap("DefinePlugin", expr => {
492 addValueDependency(key);
493 const strCode = stringifyObj(
494 obj,
495 parser,
496 compilation.valueCacheVersions,
497 key,
498 runtimeTemplate,
499 !parser.isAsiPosition(expr.range[0])
500 );
501
502 if (/__webpack_require__\s*(!?\.)/.test(strCode)) {
503 return toConstantDependency(parser, strCode, [
504 RuntimeGlobals.require
505 ])(expr);
506 } else if (/__webpack_require__/.test(strCode)) {
507 return toConstantDependency(parser, strCode, [
508 RuntimeGlobals.requireScope
509 ])(expr);
510 } else {
511 return toConstantDependency(parser, strCode)(expr);
512 }
513 });
514 parser.hooks.typeof
515 .for(key)
516 .tap(
517 "DefinePlugin",
518 withValueDependency(
519 key,
520 toConstantDependency(parser, JSON.stringify("object"))
521 )
522 );
523 };
524
525 walkDefinitions(definitions, "");
526 };
527
528 normalModuleFactory.hooks.parser
529 .for("javascript/auto")
530 .tap("DefinePlugin", handler);
531 normalModuleFactory.hooks.parser
532 .for("javascript/dynamic")
533 .tap("DefinePlugin", handler);
534 normalModuleFactory.hooks.parser
535 .for("javascript/esm")
536 .tap("DefinePlugin", handler);
537
538 /**
539 * Walk definitions
540 * @param {Object} definitions Definitions map
541 * @param {string} prefix Prefix string
542 * @returns {void}
543 */
544 const walkDefinitionsForValues = (definitions, prefix) => {
545 Object.keys(definitions).forEach(key => {
546 const code = definitions[key];
547 const version = toCacheVersion(code);
548 const name = VALUE_DEP_PREFIX + prefix + key;
549 const oldVersion = compilation.valueCacheVersions.get(name);
550 if (oldVersion === undefined) {
551 compilation.valueCacheVersions.set(name, version);
552 } else if (oldVersion !== version) {
553 const warning = new WebpackError(
554 `DefinePlugin\nConflicting values for '${prefix + key}'`
555 );
556 warning.details = `'${oldVersion}' !== '${version}'`;
557 warning.hideStack = true;
558 compilation.warnings.push(warning);
559 }
560 if (
561 code &&
562 typeof code === "object" &&
563 !(code instanceof RuntimeValue) &&
564 !(code instanceof RegExp)
565 ) {
566 walkDefinitionsForValues(code, prefix + key + ".");
567 }
568 });
569 };
570
571 walkDefinitionsForValues(definitions, "");
572 }
573 );
574 }
575}
576module.exports = DefinePlugin;