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