UNPKG

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