UNPKG

16.7 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 createHash = require("./util/createHash");
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} */ (
93 valueCacheVersions.get(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 `/*#__PURE__*/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_hash";
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 mainHash = createHash(compilation.outputOptions.hashFunction);
290 mainHash.update(
291 /** @type {string} */ (
292 compilation.valueCacheVersions.get(VALUE_DEP_MAIN)
293 ) || ""
294 );
295
296 /**
297 * Handler
298 * @param {JavascriptParser} parser Parser
299 * @returns {void}
300 */
301 const handler = parser => {
302 const mainValue = compilation.valueCacheVersions.get(VALUE_DEP_MAIN);
303 parser.hooks.program.tap("DefinePlugin", () => {
304 const { buildInfo } = parser.state.module;
305 if (!buildInfo.valueDependencies)
306 buildInfo.valueDependencies = new Map();
307 buildInfo.valueDependencies.set(VALUE_DEP_MAIN, mainValue);
308 });
309
310 const addValueDependency = key => {
311 const { buildInfo } = parser.state.module;
312 buildInfo.valueDependencies.set(
313 VALUE_DEP_PREFIX + key,
314 compilation.valueCacheVersions.get(VALUE_DEP_PREFIX + key)
315 );
316 };
317
318 const withValueDependency =
319 (key, fn) =>
320 (...args) => {
321 addValueDependency(key);
322 return fn(...args);
323 };
324
325 /**
326 * Walk definitions
327 * @param {Object} definitions Definitions map
328 * @param {string} prefix Prefix string
329 * @returns {void}
330 */
331 const walkDefinitions = (definitions, prefix) => {
332 Object.keys(definitions).forEach(key => {
333 const code = definitions[key];
334 if (
335 code &&
336 typeof code === "object" &&
337 !(code instanceof RuntimeValue) &&
338 !(code instanceof RegExp)
339 ) {
340 walkDefinitions(code, prefix + key + ".");
341 applyObjectDefine(prefix + key, code);
342 return;
343 }
344 applyDefineKey(prefix, key);
345 applyDefine(prefix + key, code);
346 });
347 };
348
349 /**
350 * Apply define key
351 * @param {string} prefix Prefix
352 * @param {string} key Key
353 * @returns {void}
354 */
355 const applyDefineKey = (prefix, key) => {
356 const splittedKey = key.split(".");
357 splittedKey.slice(1).forEach((_, i) => {
358 const fullKey = prefix + splittedKey.slice(0, i + 1).join(".");
359 parser.hooks.canRename.for(fullKey).tap("DefinePlugin", () => {
360 addValueDependency(key);
361 return true;
362 });
363 });
364 };
365
366 /**
367 * Apply Code
368 * @param {string} key Key
369 * @param {CodeValue} code Code
370 * @returns {void}
371 */
372 const applyDefine = (key, code) => {
373 const originalKey = key;
374 const isTypeof = /^typeof\s+/.test(key);
375 if (isTypeof) key = key.replace(/^typeof\s+/, "");
376 let recurse = false;
377 let recurseTypeof = false;
378 if (!isTypeof) {
379 parser.hooks.canRename.for(key).tap("DefinePlugin", () => {
380 addValueDependency(originalKey);
381 return true;
382 });
383 parser.hooks.evaluateIdentifier
384 .for(key)
385 .tap("DefinePlugin", expr => {
386 /**
387 * this is needed in case there is a recursion in the DefinePlugin
388 * to prevent an endless recursion
389 * e.g.: new DefinePlugin({
390 * "a": "b",
391 * "b": "a"
392 * });
393 */
394 if (recurse) return;
395 addValueDependency(originalKey);
396 recurse = true;
397 const res = parser.evaluate(
398 toCode(
399 code,
400 parser,
401 compilation.valueCacheVersions,
402 key,
403 runtimeTemplate,
404 null
405 )
406 );
407 recurse = false;
408 res.setRange(expr.range);
409 return res;
410 });
411 parser.hooks.expression.for(key).tap("DefinePlugin", expr => {
412 addValueDependency(originalKey);
413 const strCode = toCode(
414 code,
415 parser,
416 compilation.valueCacheVersions,
417 originalKey,
418 runtimeTemplate,
419 !parser.isAsiPosition(expr.range[0])
420 );
421 if (/__webpack_require__\s*(!?\.)/.test(strCode)) {
422 return toConstantDependency(parser, strCode, [
423 RuntimeGlobals.require
424 ])(expr);
425 } else if (/__webpack_require__/.test(strCode)) {
426 return toConstantDependency(parser, strCode, [
427 RuntimeGlobals.requireScope
428 ])(expr);
429 } else {
430 return toConstantDependency(parser, strCode)(expr);
431 }
432 });
433 }
434 parser.hooks.evaluateTypeof.for(key).tap("DefinePlugin", expr => {
435 /**
436 * this is needed in case there is a recursion in the DefinePlugin
437 * to prevent an endless recursion
438 * e.g.: new DefinePlugin({
439 * "typeof a": "typeof b",
440 * "typeof b": "typeof a"
441 * });
442 */
443 if (recurseTypeof) return;
444 recurseTypeof = true;
445 addValueDependency(originalKey);
446 const codeCode = toCode(
447 code,
448 parser,
449 compilation.valueCacheVersions,
450 originalKey,
451 runtimeTemplate,
452 null
453 );
454 const typeofCode = isTypeof
455 ? codeCode
456 : "typeof (" + codeCode + ")";
457 const res = parser.evaluate(typeofCode);
458 recurseTypeof = false;
459 res.setRange(expr.range);
460 return res;
461 });
462 parser.hooks.typeof.for(key).tap("DefinePlugin", expr => {
463 addValueDependency(originalKey);
464 const codeCode = toCode(
465 code,
466 parser,
467 compilation.valueCacheVersions,
468 originalKey,
469 runtimeTemplate,
470 null
471 );
472 const typeofCode = isTypeof
473 ? codeCode
474 : "typeof (" + codeCode + ")";
475 const res = parser.evaluate(typeofCode);
476 if (!res.isString()) return;
477 return toConstantDependency(
478 parser,
479 JSON.stringify(res.string)
480 ).bind(parser)(expr);
481 });
482 };
483
484 /**
485 * Apply Object
486 * @param {string} key Key
487 * @param {Object} obj Object
488 * @returns {void}
489 */
490 const applyObjectDefine = (key, obj) => {
491 parser.hooks.canRename.for(key).tap("DefinePlugin", () => {
492 addValueDependency(key);
493 return true;
494 });
495 parser.hooks.evaluateIdentifier
496 .for(key)
497 .tap("DefinePlugin", expr => {
498 addValueDependency(key);
499 return new BasicEvaluatedExpression()
500 .setTruthy()
501 .setSideEffects(false)
502 .setRange(expr.range);
503 });
504 parser.hooks.evaluateTypeof
505 .for(key)
506 .tap(
507 "DefinePlugin",
508 withValueDependency(key, evaluateToString("object"))
509 );
510 parser.hooks.expression.for(key).tap("DefinePlugin", expr => {
511 addValueDependency(key);
512 const strCode = stringifyObj(
513 obj,
514 parser,
515 compilation.valueCacheVersions,
516 key,
517 runtimeTemplate,
518 !parser.isAsiPosition(expr.range[0])
519 );
520
521 if (/__webpack_require__\s*(!?\.)/.test(strCode)) {
522 return toConstantDependency(parser, strCode, [
523 RuntimeGlobals.require
524 ])(expr);
525 } else if (/__webpack_require__/.test(strCode)) {
526 return toConstantDependency(parser, strCode, [
527 RuntimeGlobals.requireScope
528 ])(expr);
529 } else {
530 return toConstantDependency(parser, strCode)(expr);
531 }
532 });
533 parser.hooks.typeof
534 .for(key)
535 .tap(
536 "DefinePlugin",
537 withValueDependency(
538 key,
539 toConstantDependency(parser, JSON.stringify("object"))
540 )
541 );
542 };
543
544 walkDefinitions(definitions, "");
545 };
546
547 normalModuleFactory.hooks.parser
548 .for("javascript/auto")
549 .tap("DefinePlugin", handler);
550 normalModuleFactory.hooks.parser
551 .for("javascript/dynamic")
552 .tap("DefinePlugin", handler);
553 normalModuleFactory.hooks.parser
554 .for("javascript/esm")
555 .tap("DefinePlugin", handler);
556
557 /**
558 * Walk definitions
559 * @param {Object} definitions Definitions map
560 * @param {string} prefix Prefix string
561 * @returns {void}
562 */
563 const walkDefinitionsForValues = (definitions, prefix) => {
564 Object.keys(definitions).forEach(key => {
565 const code = definitions[key];
566 const version = toCacheVersion(code);
567 const name = VALUE_DEP_PREFIX + prefix + key;
568 mainHash.update("|" + prefix + key);
569 const oldVersion = compilation.valueCacheVersions.get(name);
570 if (oldVersion === undefined) {
571 compilation.valueCacheVersions.set(name, version);
572 } else if (oldVersion !== version) {
573 const warning = new WebpackError(
574 `DefinePlugin\nConflicting values for '${prefix + key}'`
575 );
576 warning.details = `'${oldVersion}' !== '${version}'`;
577 warning.hideStack = true;
578 compilation.warnings.push(warning);
579 }
580 if (
581 code &&
582 typeof code === "object" &&
583 !(code instanceof RuntimeValue) &&
584 !(code instanceof RegExp)
585 ) {
586 walkDefinitionsForValues(code, prefix + key + ".");
587 }
588 });
589 };
590
591 walkDefinitionsForValues(definitions, "");
592
593 compilation.valueCacheVersions.set(
594 VALUE_DEP_MAIN,
595 /** @type {string} */ (mainHash.digest("hex").slice(0, 8))
596 );
597 }
598 );
599 }
600}
601module.exports = DefinePlugin;