source: frontend/node_modules/webpack/lib/dependencies/CommonJsExportsParserPlugin.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 12 days ago

Fix frontend appearance

  • Property mode set to 100644
File size: 14.6 KB
RevLine 
[9af201e]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 { evaluateToString } = require("../javascript/JavascriptParserHelpers");
10const formatLocation = require("../util/formatLocation");
11const { propertyAccess } = require("../util/property");
12const CommonJsExportRequireDependency = require("./CommonJsExportRequireDependency");
13const CommonJsExportsDependency = require("./CommonJsExportsDependency");
14const CommonJsSelfReferenceDependency = require("./CommonJsSelfReferenceDependency");
15const DynamicExports = require("./DynamicExports");
16const HarmonyExports = require("./HarmonyExports");
17const ModuleDecoratorDependency = require("./ModuleDecoratorDependency");
18
19/** @typedef {import("estree").AssignmentExpression} AssignmentExpression */
20/** @typedef {import("estree").CallExpression} CallExpression */
21/** @typedef {import("estree").Expression} Expression */
22/** @typedef {import("estree").Super} Super */
23/** @typedef {import("../Dependency").DependencyLocation} DependencyLocation */
24/** @typedef {import("../ModuleGraph")} ModuleGraph */
25/** @typedef {import("../ExportsInfo").ExportInfoName} ExportInfoName */
26/** @typedef {import("../javascript/BasicEvaluatedExpression")} BasicEvaluatedExpression */
27/** @typedef {import("../javascript/JavascriptParser")} JavascriptParser */
28/** @typedef {import("../javascript/JavascriptParser").Range} Range */
29/** @typedef {import("../javascript/JavascriptParser").Members} Members */
30/** @typedef {import("../javascript/JavascriptParser").StatementPath} StatementPath */
31/** @typedef {import("./CommonJsDependencyHelpers").CommonJSDependencyBaseKeywords} CommonJSDependencyBaseKeywords */
32/** @typedef {import("../Module").BuildMeta} BuildMeta */
33
34/**
35 * This function takes a generic expression and detects whether it is an ObjectExpression.
36 * This is used in the context of parsing CommonJS exports to get the value of the property descriptor
37 * when the `exports` object is assigned to `Object.defineProperty`.
38 *
39 * In CommonJS modules, the `exports` object can be assigned to `Object.defineProperty` and therefore
40 * webpack has to detect this case and get the value key of the property descriptor. See the following example
41 * for more information: https://astexplorer.net/#/gist/83ce51a4e96e59d777df315a6d111da6/8058ead48a1bb53c097738225db0967ef7f70e57
42 *
43 * This would be an example of a CommonJS module that exports an object with a property descriptor:
44 * ```js
45 * Object.defineProperty(exports, "__esModule", { value: true });
46 * exports.foo = void 0;
47 * exports.foo = "bar";
48 * ```
49 * @param {Expression} expr expression
50 * @returns {Expression | undefined} returns the value of property descriptor
51 */
52const getValueOfPropertyDescription = (expr) => {
53 if (expr.type !== "ObjectExpression") return;
54 for (const property of expr.properties) {
55 if (property.type === "SpreadElement" || property.computed) continue;
56 const key = property.key;
57 if (key.type !== "Identifier" || key.name !== "value") continue;
58 return /** @type {Expression} */ (property.value);
59 }
60};
61
62/**
63 * The purpose of this function is to check whether an expression is a truthy literal or not. This is
64 * useful when parsing CommonJS exports, because CommonJS modules can export any value, including falsy
65 * values like `null` and `false`. However, exports should only be created if the exported value is truthy.
66 * @param {Expression} expr expression being checked
67 * @returns {boolean} true, when the expression is a truthy literal
68 */
69const isTruthyLiteral = (expr) => {
70 switch (expr.type) {
71 case "Literal":
72 return Boolean(expr.value);
73 case "UnaryExpression":
74 if (expr.operator === "!") return isFalsyLiteral(expr.argument);
75 }
76 return false;
77};
78
79/**
80 * The purpose of this function is to check whether an expression is a falsy literal or not. This is
81 * useful when parsing CommonJS exports, because CommonJS modules can export any value, including falsy
82 * values like `null` and `false`. However, exports should only be created if the exported value is truthy.
83 * @param {Expression} expr expression being checked
84 * @returns {boolean} true, when the expression is a falsy literal
85 */
86const isFalsyLiteral = (expr) => {
87 switch (expr.type) {
88 case "Literal":
89 return !expr.value;
90 case "UnaryExpression":
91 if (expr.operator === "!") return isTruthyLiteral(expr.argument);
92 }
93 return false;
94};
95
96/**
97 * Parses require call.
98 * @param {JavascriptParser} parser the parser
99 * @param {Expression} expr expression
100 * @returns {{ argument: BasicEvaluatedExpression, ids: ExportInfoName[] } | undefined} parsed call
101 */
102const parseRequireCall = (parser, expr) => {
103 /** @type {ExportInfoName[]} */
104 const ids = [];
105 while (expr.type === "MemberExpression") {
106 if (expr.object.type === "Super") return;
107 if (!expr.property) return;
108 const prop = expr.property;
109 if (expr.computed) {
110 if (prop.type !== "Literal") return;
111 ids.push(`${prop.value}`);
112 } else {
113 if (prop.type !== "Identifier") return;
114 ids.push(prop.name);
115 }
116 expr = expr.object;
117 }
118 if (expr.type !== "CallExpression" || expr.arguments.length !== 1) return;
119 const callee = expr.callee;
120 if (
121 callee.type !== "Identifier" ||
122 parser.getVariableInfo(callee.name) !== "require"
123 ) {
124 return;
125 }
126 const arg = expr.arguments[0];
127 if (arg.type === "SpreadElement") return;
128 const argValue = parser.evaluateExpression(arg);
129 return { argument: argValue, ids: ids.reverse() };
130};
131
132const PLUGIN_NAME = "CommonJsExportsParserPlugin";
133
134class CommonJsExportsParserPlugin {
135 /**
136 * Creates an instance of CommonJsExportsParserPlugin.
137 * @param {ModuleGraph} moduleGraph module graph
138 */
139 constructor(moduleGraph) {
140 this.moduleGraph = moduleGraph;
141 }
142
143 /**
144 * Applies the plugin by registering its hooks on the compiler.
145 * @param {JavascriptParser} parser the parser
146 * @returns {void}
147 */
148 apply(parser) {
149 const enableStructuredExports = () => {
150 DynamicExports.enable(parser.state);
151 };
152
153 /**
154 * Checks namespace.
155 * @param {boolean} topLevel true, when the export is on top level
156 * @param {Members} members members of the export
157 * @param {Expression | undefined} valueExpr expression for the value
158 * @returns {void}
159 */
160 const checkNamespace = (topLevel, members, valueExpr) => {
161 if (!DynamicExports.isEnabled(parser.state)) return;
162 if (members.length > 0 && members[0] === "__esModule") {
163 if (valueExpr && isTruthyLiteral(valueExpr) && topLevel) {
164 DynamicExports.setFlagged(parser.state);
165 } else {
166 DynamicExports.setDynamic(parser.state);
167 }
168 }
169 };
170 /**
171 * Processes the provided reason.
172 * @param {string=} reason reason
173 */
174 const bailout = (reason) => {
175 DynamicExports.bailout(parser.state);
176 if (reason) bailoutHint(reason);
177 };
178 /**
179 * Processes the provided reason.
180 * @param {string} reason reason
181 */
182 const bailoutHint = (reason) => {
183 this.moduleGraph
184 .getOptimizationBailout(parser.state.module)
185 .push(`CommonJS bailout: ${reason}`);
186 };
187
188 // metadata //
189 parser.hooks.evaluateTypeof
190 .for("module")
191 .tap(PLUGIN_NAME, evaluateToString("object"));
192 parser.hooks.evaluateTypeof
193 .for("exports")
194 .tap(PLUGIN_NAME, evaluateToString("object"));
195
196 // exporting //
197
198 /**
199 * Handle assign export.
200 * @param {AssignmentExpression} expr expression
201 * @param {CommonJSDependencyBaseKeywords} base commonjs base keywords
202 * @param {Members} members members of the export
203 * @returns {boolean | undefined} true, when the expression was handled
204 */
205 const handleAssignExport = (expr, base, members) => {
206 if (HarmonyExports.isEnabled(parser.state)) return;
207 // Handle reexporting
208 const requireCall = parseRequireCall(parser, expr.right);
209 if (
210 requireCall &&
211 requireCall.argument.isString() &&
212 (members.length === 0 || members[0] !== "__esModule")
213 ) {
214 enableStructuredExports();
215 // It's possible to reexport __esModule, so we must convert to a dynamic module
216 if (members.length === 0) DynamicExports.setDynamic(parser.state);
217 const dep = new CommonJsExportRequireDependency(
218 /** @type {Range} */ (expr.range),
219 null,
220 base,
221 members,
222 /** @type {string} */ (requireCall.argument.string),
223 requireCall.ids,
224 !parser.isStatementLevelExpression(expr)
225 );
226 dep.loc = /** @type {DependencyLocation} */ (expr.loc);
227 dep.optional = Boolean(parser.scope.inTry);
228 parser.state.module.addDependency(dep);
229 /** @type {BuildMeta} */ (
230 parser.state.module.buildMeta
231 ).treatAsCommonJs = true;
232
233 return true;
234 }
235 if (members.length === 0) return;
236 enableStructuredExports();
237 const remainingMembers = members;
238 checkNamespace(
239 /** @type {StatementPath} */
240 (parser.statementPath).length === 1 &&
241 parser.isStatementLevelExpression(expr),
242 remainingMembers,
243 expr.right
244 );
245 const dep = new CommonJsExportsDependency(
246 /** @type {Range} */ (expr.left.range),
247 null,
248 base,
249 remainingMembers
250 );
251 dep.loc = /** @type {DependencyLocation} */ (expr.loc);
252 parser.state.module.addDependency(dep);
253 /** @type {BuildMeta} */ (parser.state.module.buildMeta).treatAsCommonJs =
254 true;
255 parser.walkExpression(expr.right);
256 return true;
257 };
258 parser.hooks.assignMemberChain
259 .for("exports")
260 .tap(PLUGIN_NAME, (expr, members) =>
261 handleAssignExport(expr, "exports", members)
262 );
263 parser.hooks.assignMemberChain
264 .for("this")
265 .tap(PLUGIN_NAME, (expr, members) => {
266 if (!parser.scope.topLevelScope) return;
267 return handleAssignExport(expr, "this", members);
268 });
269 parser.hooks.assignMemberChain
270 .for("module")
271 .tap(PLUGIN_NAME, (expr, members) => {
272 if (members[0] !== "exports") return;
273 return handleAssignExport(expr, "module.exports", members.slice(1));
274 });
275 parser.hooks.call
276 .for("Object.defineProperty")
277 .tap(PLUGIN_NAME, (expression) => {
278 const expr = /** @type {CallExpression} */ (expression);
279 if (!parser.isStatementLevelExpression(expr)) return;
280 if (expr.arguments.length !== 3) return;
281 if (expr.arguments[0].type === "SpreadElement") return;
282 if (expr.arguments[1].type === "SpreadElement") return;
283 if (expr.arguments[2].type === "SpreadElement") return;
284 const exportsArg = parser.evaluateExpression(expr.arguments[0]);
285 if (!exportsArg.isIdentifier()) return;
286 if (
287 exportsArg.identifier !== "exports" &&
288 exportsArg.identifier !== "module.exports" &&
289 (exportsArg.identifier !== "this" || !parser.scope.topLevelScope)
290 ) {
291 return;
292 }
293 const propertyArg = parser.evaluateExpression(expr.arguments[1]);
294 const property = propertyArg.asString();
295 if (typeof property !== "string") return;
296 enableStructuredExports();
297 const descArg = expr.arguments[2];
298 checkNamespace(
299 /** @type {StatementPath} */
300 (parser.statementPath).length === 1,
301 [property],
302 getValueOfPropertyDescription(descArg)
303 );
304 const dep = new CommonJsExportsDependency(
305 /** @type {Range} */ (expr.range),
306 /** @type {Range} */ (expr.arguments[2].range),
307 `Object.defineProperty(${exportsArg.identifier})`,
308 [property]
309 );
310 dep.loc = /** @type {DependencyLocation} */ (expr.loc);
311 parser.state.module.addDependency(dep);
312 /** @type {BuildMeta} */ (
313 parser.state.module.buildMeta
314 ).treatAsCommonJs = true;
315
316 parser.walkExpression(expr.arguments[2]);
317 return true;
318 });
319
320 // Self reference //
321
322 /**
323 * Handle access export.
324 * @param {Expression | Super} expr expression
325 * @param {CommonJSDependencyBaseKeywords} base commonjs base keywords
326 * @param {Members} members members of the export
327 * @param {CallExpression=} call call expression
328 * @returns {boolean | void} true, when the expression was handled
329 */
330 const handleAccessExport = (expr, base, members, call) => {
331 if (HarmonyExports.isEnabled(parser.state)) return;
332 if (members.length === 0) {
333 bailout(
334 `${base} is used directly at ${formatLocation(
335 /** @type {DependencyLocation} */ (expr.loc)
336 )}`
337 );
338 }
339 if (call && members.length === 1) {
340 bailoutHint(
341 `${base}${propertyAccess(
342 members
343 )}(...) prevents optimization as ${base} is passed as call context at ${formatLocation(
344 /** @type {DependencyLocation} */ (expr.loc)
345 )}`
346 );
347 }
348 const dep = new CommonJsSelfReferenceDependency(
349 /** @type {Range} */ (expr.range),
350 base,
351 members,
352 Boolean(call)
353 );
354 dep.loc = /** @type {DependencyLocation} */ (expr.loc);
355 parser.state.module.addDependency(dep);
356 /** @type {BuildMeta} */ (parser.state.module.buildMeta).treatAsCommonJs =
357 true;
358
359 if (call) {
360 parser.walkExpressions(call.arguments);
361 }
362 return true;
363 };
364 parser.hooks.callMemberChain
365 .for("exports")
366 .tap(PLUGIN_NAME, (expr, members) =>
367 handleAccessExport(expr.callee, "exports", members, expr)
368 );
369 parser.hooks.expressionMemberChain
370 .for("exports")
371 .tap(PLUGIN_NAME, (expr, members) =>
372 handleAccessExport(expr, "exports", members)
373 );
374 parser.hooks.expression
375 .for("exports")
376 .tap(PLUGIN_NAME, (expr) => handleAccessExport(expr, "exports", []));
377 parser.hooks.callMemberChain
378 .for("module")
379 .tap(PLUGIN_NAME, (expr, members) => {
380 if (members[0] !== "exports") return;
381 return handleAccessExport(
382 expr.callee,
383 "module.exports",
384 members.slice(1),
385 expr
386 );
387 });
388 parser.hooks.expressionMemberChain
389 .for("module")
390 .tap(PLUGIN_NAME, (expr, members) => {
391 if (members[0] !== "exports") return;
392 return handleAccessExport(expr, "module.exports", members.slice(1));
393 });
394 parser.hooks.expression
395 .for("module.exports")
396 .tap(PLUGIN_NAME, (expr) =>
397 handleAccessExport(expr, "module.exports", [])
398 );
399 parser.hooks.callMemberChain
400 .for("this")
401 .tap(PLUGIN_NAME, (expr, members) => {
402 if (!parser.scope.topLevelScope) return;
403 return handleAccessExport(expr.callee, "this", members, expr);
404 });
405 parser.hooks.expressionMemberChain
406 .for("this")
407 .tap(PLUGIN_NAME, (expr, members) => {
408 if (!parser.scope.topLevelScope) return;
409 return handleAccessExport(expr, "this", members);
410 });
411 parser.hooks.expression.for("this").tap(PLUGIN_NAME, (expr) => {
412 if (!parser.scope.topLevelScope) return;
413 return handleAccessExport(expr, "this", []);
414 });
415
416 // Bailouts //
417 parser.hooks.expression.for("module").tap(PLUGIN_NAME, (expr) => {
418 bailout();
419 const isHarmony = HarmonyExports.isEnabled(parser.state);
420 const dep = new ModuleDecoratorDependency(
421 isHarmony
422 ? RuntimeGlobals.harmonyModuleDecorator
423 : RuntimeGlobals.nodeModuleDecorator,
424 !isHarmony
425 );
426 dep.loc = /** @type {DependencyLocation} */ (expr.loc);
427 parser.state.module.addDependency(dep);
428 return true;
429 });
430 }
431}
432
433module.exports = CommonJsExportsParserPlugin;
Note: See TracBrowser for help on using the repository browser.