You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
	
	
		
			408 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
		
		
			
		
	
	
			408 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
| 
											9 months ago
										 | /* | ||
|  | 	MIT License http://www.opensource.org/licenses/mit-license.php
 | ||
|  | 	Author Tobias Koppers @sokra | ||
|  | */ | ||
|  | 
 | ||
|  | "use strict"; | ||
|  | 
 | ||
|  | const RuntimeGlobals = require("../RuntimeGlobals"); | ||
|  | const formatLocation = require("../formatLocation"); | ||
|  | const { evaluateToString } = require("../javascript/JavascriptParserHelpers"); | ||
|  | const propertyAccess = require("../util/propertyAccess"); | ||
|  | const CommonJsExportRequireDependency = require("./CommonJsExportRequireDependency"); | ||
|  | const CommonJsExportsDependency = require("./CommonJsExportsDependency"); | ||
|  | const CommonJsSelfReferenceDependency = require("./CommonJsSelfReferenceDependency"); | ||
|  | const DynamicExports = require("./DynamicExports"); | ||
|  | const HarmonyExports = require("./HarmonyExports"); | ||
|  | const ModuleDecoratorDependency = require("./ModuleDecoratorDependency"); | ||
|  | 
 | ||
|  | /** @typedef {import("estree").AssignmentExpression} AssignmentExpression */ | ||
|  | /** @typedef {import("estree").CallExpression} CallExpression */ | ||
|  | /** @typedef {import("estree").Expression} Expression */ | ||
|  | /** @typedef {import("estree").Super} Super */ | ||
|  | 
 | ||
|  | /** @typedef {import("../ModuleGraph")} ModuleGraph */ | ||
|  | /** @typedef {import("../NormalModule")} NormalModule */ | ||
|  | /** @typedef {import("../javascript/BasicEvaluatedExpression")} BasicEvaluatedExpression */ | ||
|  | /** @typedef {import("../javascript/JavascriptParser")} JavascriptParser */ | ||
|  | /** @typedef {import("./CommonJsDependencyHelpers").CommonJSDependencyBaseKeywords} CommonJSDependencyBaseKeywords */ | ||
|  | 
 | ||
|  | /** | ||
|  |  * This function takes a generic expression and detects whether it is an ObjectExpression. | ||
|  |  * This is used in the context of parsing CommonJS exports to get the value of the property descriptor | ||
|  |  * when the `exports` object is assigned to `Object.defineProperty`. | ||
|  |  * | ||
|  |  * In CommonJS modules, the `exports` object can be assigned to `Object.defineProperty` and therefore | ||
|  |  * webpack has to detect this case and get the value key of the property descriptor. See the following example | ||
|  |  * for more information: https://astexplorer.net/#/gist/83ce51a4e96e59d777df315a6d111da6/8058ead48a1bb53c097738225db0967ef7f70e57
 | ||
|  |  * | ||
|  |  * This would be an example of a CommonJS module that exports an object with a property descriptor: | ||
|  |  * ```js
 | ||
|  |  * Object.defineProperty(exports, "__esModule", { value: true }); | ||
|  |  * exports.foo = void 0; | ||
|  |  * exports.foo = "bar"; | ||
|  |  * ```
 | ||
|  |  * | ||
|  |  * @param {TODO} expr expression | ||
|  |  * @returns {Expression | undefined} returns the value of property descriptor | ||
|  |  */ | ||
|  | const getValueOfPropertyDescription = expr => { | ||
|  | 	if (expr.type !== "ObjectExpression") return; | ||
|  | 	for (const property of expr.properties) { | ||
|  | 		if (property.computed) continue; | ||
|  | 		const key = property.key; | ||
|  | 		if (key.type !== "Identifier" || key.name !== "value") continue; | ||
|  | 		return property.value; | ||
|  | 	} | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * The purpose of this function is to check whether an expression is a truthy literal or not. This is | ||
|  |  * useful when parsing CommonJS exports, because CommonJS modules can export any value, including falsy | ||
|  |  * values like `null` and `false`. However, exports should only be created if the exported value is truthy. | ||
|  |  * | ||
|  |  * @param {Expression} expr expression being checked | ||
|  |  * @returns {boolean} true, when the expression is a truthy literal | ||
|  |  * | ||
|  |  */ | ||
|  | const isTruthyLiteral = expr => { | ||
|  | 	switch (expr.type) { | ||
|  | 		case "Literal": | ||
|  | 			return !!expr.value; | ||
|  | 		case "UnaryExpression": | ||
|  | 			if (expr.operator === "!") return isFalsyLiteral(expr.argument); | ||
|  | 	} | ||
|  | 	return false; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * The purpose of this function is to check whether an expression is a falsy literal or not. This is | ||
|  |  * useful when parsing CommonJS exports, because CommonJS modules can export any value, including falsy | ||
|  |  * values like `null` and `false`. However, exports should only be created if the exported value is truthy. | ||
|  |  * | ||
|  |  * @param {Expression} expr expression being checked | ||
|  |  * @returns {boolean} true, when the expression is a falsy literal | ||
|  |  */ | ||
|  | const isFalsyLiteral = expr => { | ||
|  | 	switch (expr.type) { | ||
|  | 		case "Literal": | ||
|  | 			return !expr.value; | ||
|  | 		case "UnaryExpression": | ||
|  | 			if (expr.operator === "!") return isTruthyLiteral(expr.argument); | ||
|  | 	} | ||
|  | 	return false; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * @param {JavascriptParser} parser the parser | ||
|  |  * @param {Expression} expr expression | ||
|  |  * @returns {{ argument: BasicEvaluatedExpression, ids: string[] } | undefined} parsed call | ||
|  |  */ | ||
|  | const parseRequireCall = (parser, expr) => { | ||
|  | 	const ids = []; | ||
|  | 	while (expr.type === "MemberExpression") { | ||
|  | 		if (expr.object.type === "Super") return; | ||
|  | 		if (!expr.property) return; | ||
|  | 		const prop = expr.property; | ||
|  | 		if (expr.computed) { | ||
|  | 			if (prop.type !== "Literal") return; | ||
|  | 			ids.push(`${prop.value}`); | ||
|  | 		} else { | ||
|  | 			if (prop.type !== "Identifier") return; | ||
|  | 			ids.push(prop.name); | ||
|  | 		} | ||
|  | 		expr = expr.object; | ||
|  | 	} | ||
|  | 	if (expr.type !== "CallExpression" || expr.arguments.length !== 1) return; | ||
|  | 	const callee = expr.callee; | ||
|  | 	if ( | ||
|  | 		callee.type !== "Identifier" || | ||
|  | 		parser.getVariableInfo(callee.name) !== "require" | ||
|  | 	) { | ||
|  | 		return; | ||
|  | 	} | ||
|  | 	const arg = expr.arguments[0]; | ||
|  | 	if (arg.type === "SpreadElement") return; | ||
|  | 	const argValue = parser.evaluateExpression(arg); | ||
|  | 	return { argument: argValue, ids: ids.reverse() }; | ||
|  | }; | ||
|  | 
 | ||
|  | class CommonJsExportsParserPlugin { | ||
|  | 	/** | ||
|  | 	 * @param {ModuleGraph} moduleGraph module graph | ||
|  | 	 */ | ||
|  | 	constructor(moduleGraph) { | ||
|  | 		this.moduleGraph = moduleGraph; | ||
|  | 	} | ||
|  | 
 | ||
|  | 	/** | ||
|  | 	 * @param {JavascriptParser} parser the parser | ||
|  | 	 * @returns {void} | ||
|  | 	 */ | ||
|  | 	apply(parser) { | ||
|  | 		const enableStructuredExports = () => { | ||
|  | 			DynamicExports.enable(parser.state); | ||
|  | 		}; | ||
|  | 
 | ||
|  | 		/** | ||
|  | 		 * @param {boolean} topLevel true, when the export is on top level | ||
|  | 		 * @param {string[]} members members of the export | ||
|  | 		 * @param {Expression | undefined} valueExpr expression for the value | ||
|  | 		 * @returns {void} | ||
|  | 		 */ | ||
|  | 		const checkNamespace = (topLevel, members, valueExpr) => { | ||
|  | 			if (!DynamicExports.isEnabled(parser.state)) return; | ||
|  | 			if (members.length > 0 && members[0] === "__esModule") { | ||
|  | 				if (valueExpr && isTruthyLiteral(valueExpr) && topLevel) { | ||
|  | 					DynamicExports.setFlagged(parser.state); | ||
|  | 				} else { | ||
|  | 					DynamicExports.setDynamic(parser.state); | ||
|  | 				} | ||
|  | 			} | ||
|  | 		}; | ||
|  | 		/** | ||
|  | 		 * @param {string=} reason reason | ||
|  | 		 */ | ||
|  | 		const bailout = reason => { | ||
|  | 			DynamicExports.bailout(parser.state); | ||
|  | 			if (reason) bailoutHint(reason); | ||
|  | 		}; | ||
|  | 		/** | ||
|  | 		 * @param {string} reason reason | ||
|  | 		 */ | ||
|  | 		const bailoutHint = reason => { | ||
|  | 			this.moduleGraph | ||
|  | 				.getOptimizationBailout(parser.state.module) | ||
|  | 				.push(`CommonJS bailout: ${reason}`); | ||
|  | 		}; | ||
|  | 
 | ||
|  | 		// metadata //
 | ||
|  | 		parser.hooks.evaluateTypeof | ||
|  | 			.for("module") | ||
|  | 			.tap("CommonJsExportsParserPlugin", evaluateToString("object")); | ||
|  | 		parser.hooks.evaluateTypeof | ||
|  | 			.for("exports") | ||
|  | 			.tap("CommonJsPlugin", evaluateToString("object")); | ||
|  | 
 | ||
|  | 		// exporting //
 | ||
|  | 
 | ||
|  | 		/** | ||
|  | 		 * @param {AssignmentExpression} expr expression | ||
|  | 		 * @param {CommonJSDependencyBaseKeywords} base commonjs base keywords | ||
|  | 		 * @param {string[]} members members of the export | ||
|  | 		 * @returns {boolean} true, when the expression was handled | ||
|  | 		 */ | ||
|  | 		const handleAssignExport = (expr, base, members) => { | ||
|  | 			if (HarmonyExports.isEnabled(parser.state)) return; | ||
|  | 			// Handle reexporting
 | ||
|  | 			const requireCall = parseRequireCall(parser, expr.right); | ||
|  | 			if ( | ||
|  | 				requireCall && | ||
|  | 				requireCall.argument.isString() && | ||
|  | 				(members.length === 0 || members[0] !== "__esModule") | ||
|  | 			) { | ||
|  | 				enableStructuredExports(); | ||
|  | 				// It's possible to reexport __esModule, so we must convert to a dynamic module
 | ||
|  | 				if (members.length === 0) DynamicExports.setDynamic(parser.state); | ||
|  | 				const dep = new CommonJsExportRequireDependency( | ||
|  | 					expr.range, | ||
|  | 					null, | ||
|  | 					base, | ||
|  | 					members, | ||
|  | 					requireCall.argument.string, | ||
|  | 					requireCall.ids, | ||
|  | 					!parser.isStatementLevelExpression(expr) | ||
|  | 				); | ||
|  | 				dep.loc = expr.loc; | ||
|  | 				dep.optional = !!parser.scope.inTry; | ||
|  | 				parser.state.module.addDependency(dep); | ||
|  | 				return true; | ||
|  | 			} | ||
|  | 			if (members.length === 0) return; | ||
|  | 			enableStructuredExports(); | ||
|  | 			const remainingMembers = members; | ||
|  | 			checkNamespace( | ||
|  | 				parser.statementPath.length === 1 && | ||
|  | 					parser.isStatementLevelExpression(expr), | ||
|  | 				remainingMembers, | ||
|  | 				expr.right | ||
|  | 			); | ||
|  | 			const dep = new CommonJsExportsDependency( | ||
|  | 				expr.left.range, | ||
|  | 				null, | ||
|  | 				base, | ||
|  | 				remainingMembers | ||
|  | 			); | ||
|  | 			dep.loc = expr.loc; | ||
|  | 			parser.state.module.addDependency(dep); | ||
|  | 			parser.walkExpression(expr.right); | ||
|  | 			return true; | ||
|  | 		}; | ||
|  | 		parser.hooks.assignMemberChain | ||
|  | 			.for("exports") | ||
|  | 			.tap("CommonJsExportsParserPlugin", (expr, members) => { | ||
|  | 				return handleAssignExport(expr, "exports", members); | ||
|  | 			}); | ||
|  | 		parser.hooks.assignMemberChain | ||
|  | 			.for("this") | ||
|  | 			.tap("CommonJsExportsParserPlugin", (expr, members) => { | ||
|  | 				if (!parser.scope.topLevelScope) return; | ||
|  | 				return handleAssignExport(expr, "this", members); | ||
|  | 			}); | ||
|  | 		parser.hooks.assignMemberChain | ||
|  | 			.for("module") | ||
|  | 			.tap("CommonJsExportsParserPlugin", (expr, members) => { | ||
|  | 				if (members[0] !== "exports") return; | ||
|  | 				return handleAssignExport(expr, "module.exports", members.slice(1)); | ||
|  | 			}); | ||
|  | 		parser.hooks.call | ||
|  | 			.for("Object.defineProperty") | ||
|  | 			.tap("CommonJsExportsParserPlugin", expression => { | ||
|  | 				const expr = /** @type {CallExpression} */ (expression); | ||
|  | 				if (!parser.isStatementLevelExpression(expr)) return; | ||
|  | 				if (expr.arguments.length !== 3) return; | ||
|  | 				if (expr.arguments[0].type === "SpreadElement") return; | ||
|  | 				if (expr.arguments[1].type === "SpreadElement") return; | ||
|  | 				if (expr.arguments[2].type === "SpreadElement") return; | ||
|  | 				const exportsArg = parser.evaluateExpression(expr.arguments[0]); | ||
|  | 				if (!exportsArg.isIdentifier()) return; | ||
|  | 				if ( | ||
|  | 					exportsArg.identifier !== "exports" && | ||
|  | 					exportsArg.identifier !== "module.exports" && | ||
|  | 					(exportsArg.identifier !== "this" || !parser.scope.topLevelScope) | ||
|  | 				) { | ||
|  | 					return; | ||
|  | 				} | ||
|  | 				const propertyArg = parser.evaluateExpression(expr.arguments[1]); | ||
|  | 				const property = propertyArg.asString(); | ||
|  | 				if (typeof property !== "string") return; | ||
|  | 				enableStructuredExports(); | ||
|  | 				const descArg = expr.arguments[2]; | ||
|  | 				checkNamespace( | ||
|  | 					parser.statementPath.length === 1, | ||
|  | 					[property], | ||
|  | 					getValueOfPropertyDescription(descArg) | ||
|  | 				); | ||
|  | 				const dep = new CommonJsExportsDependency( | ||
|  | 					expr.range, | ||
|  | 					expr.arguments[2].range, | ||
|  | 					`Object.defineProperty(${exportsArg.identifier})`, | ||
|  | 					[property] | ||
|  | 				); | ||
|  | 				dep.loc = expr.loc; | ||
|  | 				parser.state.module.addDependency(dep); | ||
|  | 
 | ||
|  | 				parser.walkExpression(expr.arguments[2]); | ||
|  | 				return true; | ||
|  | 			}); | ||
|  | 
 | ||
|  | 		// Self reference //
 | ||
|  | 
 | ||
|  | 		/** | ||
|  | 		 * @param {Expression | Super} expr expression | ||
|  | 		 * @param {CommonJSDependencyBaseKeywords} base commonjs base keywords | ||
|  | 		 * @param {string[]} members members of the export | ||
|  | 		 * @param {CallExpression=} call call expression | ||
|  | 		 * @returns {boolean | void} true, when the expression was handled | ||
|  | 		 */ | ||
|  | 		const handleAccessExport = (expr, base, members, call = undefined) => { | ||
|  | 			if (HarmonyExports.isEnabled(parser.state)) return; | ||
|  | 			if (members.length === 0) { | ||
|  | 				bailout(`${base} is used directly at ${formatLocation(expr.loc)}`); | ||
|  | 			} | ||
|  | 			if (call && members.length === 1) { | ||
|  | 				bailoutHint( | ||
|  | 					`${base}${propertyAccess( | ||
|  | 						members | ||
|  | 					)}(...) prevents optimization as ${base} is passed as call context at ${formatLocation( | ||
|  | 						expr.loc | ||
|  | 					)}`
 | ||
|  | 				); | ||
|  | 			} | ||
|  | 			const dep = new CommonJsSelfReferenceDependency( | ||
|  | 				expr.range, | ||
|  | 				base, | ||
|  | 				members, | ||
|  | 				!!call | ||
|  | 			); | ||
|  | 			dep.loc = expr.loc; | ||
|  | 			parser.state.module.addDependency(dep); | ||
|  | 			if (call) { | ||
|  | 				parser.walkExpressions(call.arguments); | ||
|  | 			} | ||
|  | 			return true; | ||
|  | 		}; | ||
|  | 		parser.hooks.callMemberChain | ||
|  | 			.for("exports") | ||
|  | 			.tap("CommonJsExportsParserPlugin", (expr, members) => { | ||
|  | 				return handleAccessExport(expr.callee, "exports", members, expr); | ||
|  | 			}); | ||
|  | 		parser.hooks.expressionMemberChain | ||
|  | 			.for("exports") | ||
|  | 			.tap("CommonJsExportsParserPlugin", (expr, members) => { | ||
|  | 				return handleAccessExport(expr, "exports", members); | ||
|  | 			}); | ||
|  | 		parser.hooks.expression | ||
|  | 			.for("exports") | ||
|  | 			.tap("CommonJsExportsParserPlugin", expr => { | ||
|  | 				return handleAccessExport(expr, "exports", []); | ||
|  | 			}); | ||
|  | 		parser.hooks.callMemberChain | ||
|  | 			.for("module") | ||
|  | 			.tap("CommonJsExportsParserPlugin", (expr, members) => { | ||
|  | 				if (members[0] !== "exports") return; | ||
|  | 				return handleAccessExport( | ||
|  | 					expr.callee, | ||
|  | 					"module.exports", | ||
|  | 					members.slice(1), | ||
|  | 					expr | ||
|  | 				); | ||
|  | 			}); | ||
|  | 		parser.hooks.expressionMemberChain | ||
|  | 			.for("module") | ||
|  | 			.tap("CommonJsExportsParserPlugin", (expr, members) => { | ||
|  | 				if (members[0] !== "exports") return; | ||
|  | 				return handleAccessExport(expr, "module.exports", members.slice(1)); | ||
|  | 			}); | ||
|  | 		parser.hooks.expression | ||
|  | 			.for("module.exports") | ||
|  | 			.tap("CommonJsExportsParserPlugin", expr => { | ||
|  | 				return handleAccessExport(expr, "module.exports", []); | ||
|  | 			}); | ||
|  | 		parser.hooks.callMemberChain | ||
|  | 			.for("this") | ||
|  | 			.tap("CommonJsExportsParserPlugin", (expr, members) => { | ||
|  | 				if (!parser.scope.topLevelScope) return; | ||
|  | 				return handleAccessExport(expr.callee, "this", members, expr); | ||
|  | 			}); | ||
|  | 		parser.hooks.expressionMemberChain | ||
|  | 			.for("this") | ||
|  | 			.tap("CommonJsExportsParserPlugin", (expr, members) => { | ||
|  | 				if (!parser.scope.topLevelScope) return; | ||
|  | 				return handleAccessExport(expr, "this", members); | ||
|  | 			}); | ||
|  | 		parser.hooks.expression | ||
|  | 			.for("this") | ||
|  | 			.tap("CommonJsExportsParserPlugin", expr => { | ||
|  | 				if (!parser.scope.topLevelScope) return; | ||
|  | 				return handleAccessExport(expr, "this", []); | ||
|  | 			}); | ||
|  | 
 | ||
|  | 		// Bailouts //
 | ||
|  | 		parser.hooks.expression.for("module").tap("CommonJsPlugin", expr => { | ||
|  | 			bailout(); | ||
|  | 			const isHarmony = HarmonyExports.isEnabled(parser.state); | ||
|  | 			const dep = new ModuleDecoratorDependency( | ||
|  | 				isHarmony | ||
|  | 					? RuntimeGlobals.harmonyModuleDecorator | ||
|  | 					: RuntimeGlobals.nodeModuleDecorator, | ||
|  | 				!isHarmony | ||
|  | 			); | ||
|  | 			dep.loc = expr.loc; | ||
|  | 			parser.state.module.addDependency(dep); | ||
|  | 			return true; | ||
|  | 		}); | ||
|  | 	} | ||
|  | } | ||
|  | module.exports = CommonJsExportsParserPlugin; |