CommonJsExportsParserPlugin.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const RuntimeGlobals = require("../RuntimeGlobals");
  7. const formatLocation = require("../formatLocation");
  8. const { evaluateToString } = require("../javascript/JavascriptParserHelpers");
  9. const propertyAccess = require("../util/propertyAccess");
  10. const CommonJsExportRequireDependency = require("./CommonJsExportRequireDependency");
  11. const CommonJsExportsDependency = require("./CommonJsExportsDependency");
  12. const CommonJsSelfReferenceDependency = require("./CommonJsSelfReferenceDependency");
  13. const DynamicExports = require("./DynamicExports");
  14. const HarmonyExports = require("./HarmonyExports");
  15. const ModuleDecoratorDependency = require("./ModuleDecoratorDependency");
  16. /** @typedef {import("estree").Expression} ExpressionNode */
  17. /** @typedef {import("../NormalModule")} NormalModule */
  18. /** @typedef {import("../javascript/BasicEvaluatedExpression")} BasicEvaluatedExpression */
  19. /** @typedef {import("../javascript/JavascriptParser")} JavascriptParser */
  20. const getValueOfPropertyDescription = expr => {
  21. if (expr.type !== "ObjectExpression") return;
  22. for (const property of expr.properties) {
  23. if (property.computed) continue;
  24. const key = property.key;
  25. if (key.type !== "Identifier" || key.name !== "value") continue;
  26. return property.value;
  27. }
  28. };
  29. const isTruthyLiteral = expr => {
  30. switch (expr.type) {
  31. case "Literal":
  32. return !!expr.value;
  33. case "UnaryExpression":
  34. if (expr.operator === "!") return isFalsyLiteral(expr.argument);
  35. }
  36. return false;
  37. };
  38. const isFalsyLiteral = expr => {
  39. switch (expr.type) {
  40. case "Literal":
  41. return !expr.value;
  42. case "UnaryExpression":
  43. if (expr.operator === "!") return isTruthyLiteral(expr.argument);
  44. }
  45. return false;
  46. };
  47. /**
  48. * @param {JavascriptParser} parser the parser
  49. * @param {ExpressionNode} expr expression
  50. * @returns {{ argument: BasicEvaluatedExpression, ids: string[] } | undefined} parsed call
  51. */
  52. const parseRequireCall = (parser, expr) => {
  53. const ids = [];
  54. while (expr.type === "MemberExpression") {
  55. if (expr.object.type === "Super") return;
  56. if (!expr.property) return;
  57. const prop = expr.property;
  58. if (expr.computed) {
  59. if (prop.type !== "Literal") return;
  60. ids.push(`${prop.value}`);
  61. } else {
  62. if (prop.type !== "Identifier") return;
  63. ids.push(prop.name);
  64. }
  65. expr = expr.object;
  66. }
  67. if (expr.type !== "CallExpression" || expr.arguments.length !== 1) return;
  68. const callee = expr.callee;
  69. if (
  70. callee.type !== "Identifier" ||
  71. parser.getVariableInfo(callee.name) !== "require"
  72. ) {
  73. return;
  74. }
  75. const arg = expr.arguments[0];
  76. if (arg.type === "SpreadElement") return;
  77. const argValue = parser.evaluateExpression(arg);
  78. return { argument: argValue, ids: ids.reverse() };
  79. };
  80. class CommonJsExportsParserPlugin {
  81. constructor(moduleGraph) {
  82. this.moduleGraph = moduleGraph;
  83. }
  84. /**
  85. * @param {JavascriptParser} parser the parser
  86. */
  87. apply(parser) {
  88. const enableStructuredExports = () => {
  89. DynamicExports.enable(parser.state);
  90. };
  91. const checkNamespace = (topLevel, members, valueExpr) => {
  92. if (!DynamicExports.isEnabled(parser.state)) return;
  93. if (members.length > 0 && members[0] === "__esModule") {
  94. if (valueExpr && isTruthyLiteral(valueExpr) && topLevel) {
  95. DynamicExports.setFlagged(parser.state);
  96. } else {
  97. DynamicExports.setDynamic(parser.state);
  98. }
  99. }
  100. };
  101. const bailout = reason => {
  102. DynamicExports.bailout(parser.state);
  103. if (reason) bailoutHint(reason);
  104. };
  105. const bailoutHint = reason => {
  106. this.moduleGraph
  107. .getOptimizationBailout(parser.state.module)
  108. .push(`CommonJS bailout: ${reason}`);
  109. };
  110. // metadata //
  111. parser.hooks.evaluateTypeof
  112. .for("module")
  113. .tap("CommonJsExportsParserPlugin", evaluateToString("object"));
  114. parser.hooks.evaluateTypeof
  115. .for("exports")
  116. .tap("CommonJsPlugin", evaluateToString("object"));
  117. // exporting //
  118. const handleAssignExport = (expr, base, members) => {
  119. if (HarmonyExports.isEnabled(parser.state)) return;
  120. // Handle reexporting
  121. const requireCall = parseRequireCall(parser, expr.right);
  122. if (
  123. requireCall &&
  124. requireCall.argument.isString() &&
  125. (members.length === 0 || members[0] !== "__esModule")
  126. ) {
  127. enableStructuredExports();
  128. // It's possible to reexport __esModule, so we must convert to a dynamic module
  129. if (members.length === 0) DynamicExports.setDynamic(parser.state);
  130. const dep = new CommonJsExportRequireDependency(
  131. expr.range,
  132. null,
  133. base,
  134. members,
  135. requireCall.argument.string,
  136. requireCall.ids,
  137. !parser.isStatementLevelExpression(expr)
  138. );
  139. dep.loc = expr.loc;
  140. dep.optional = !!parser.scope.inTry;
  141. parser.state.module.addDependency(dep);
  142. return true;
  143. }
  144. if (members.length === 0) return;
  145. enableStructuredExports();
  146. const remainingMembers = members;
  147. checkNamespace(
  148. parser.statementPath.length === 1 &&
  149. parser.isStatementLevelExpression(expr),
  150. remainingMembers,
  151. expr.right
  152. );
  153. const dep = new CommonJsExportsDependency(
  154. expr.left.range,
  155. null,
  156. base,
  157. remainingMembers
  158. );
  159. dep.loc = expr.loc;
  160. parser.state.module.addDependency(dep);
  161. parser.walkExpression(expr.right);
  162. return true;
  163. };
  164. parser.hooks.assignMemberChain
  165. .for("exports")
  166. .tap("CommonJsExportsParserPlugin", (expr, members) => {
  167. return handleAssignExport(expr, "exports", members);
  168. });
  169. parser.hooks.assignMemberChain
  170. .for("this")
  171. .tap("CommonJsExportsParserPlugin", (expr, members) => {
  172. if (!parser.scope.topLevelScope) return;
  173. return handleAssignExport(expr, "this", members);
  174. });
  175. parser.hooks.assignMemberChain
  176. .for("module")
  177. .tap("CommonJsExportsParserPlugin", (expr, members) => {
  178. if (members[0] !== "exports") return;
  179. return handleAssignExport(expr, "module.exports", members.slice(1));
  180. });
  181. parser.hooks.call
  182. .for("Object.defineProperty")
  183. .tap("CommonJsExportsParserPlugin", expression => {
  184. const expr = /** @type {import("estree").CallExpression} */ (
  185. expression
  186. );
  187. if (!parser.isStatementLevelExpression(expr)) return;
  188. if (expr.arguments.length !== 3) return;
  189. if (expr.arguments[0].type === "SpreadElement") return;
  190. if (expr.arguments[1].type === "SpreadElement") return;
  191. if (expr.arguments[2].type === "SpreadElement") return;
  192. const exportsArg = parser.evaluateExpression(expr.arguments[0]);
  193. if (!exportsArg || !exportsArg.isIdentifier()) return;
  194. if (
  195. exportsArg.identifier !== "exports" &&
  196. exportsArg.identifier !== "module.exports" &&
  197. (exportsArg.identifier !== "this" || !parser.scope.topLevelScope)
  198. ) {
  199. return;
  200. }
  201. const propertyArg = parser.evaluateExpression(expr.arguments[1]);
  202. if (!propertyArg) return;
  203. const property = propertyArg.asString();
  204. if (typeof property !== "string") return;
  205. enableStructuredExports();
  206. const descArg = expr.arguments[2];
  207. checkNamespace(
  208. parser.statementPath.length === 1,
  209. [property],
  210. getValueOfPropertyDescription(descArg)
  211. );
  212. const dep = new CommonJsExportsDependency(
  213. expr.range,
  214. expr.arguments[2].range,
  215. `Object.defineProperty(${exportsArg.identifier})`,
  216. [property]
  217. );
  218. dep.loc = expr.loc;
  219. parser.state.module.addDependency(dep);
  220. parser.walkExpression(expr.arguments[2]);
  221. return true;
  222. });
  223. // Self reference //
  224. const handleAccessExport = (expr, base, members, call = undefined) => {
  225. if (HarmonyExports.isEnabled(parser.state)) return;
  226. if (members.length === 0) {
  227. bailout(`${base} is used directly at ${formatLocation(expr.loc)}`);
  228. }
  229. if (call && members.length === 1) {
  230. bailoutHint(
  231. `${base}${propertyAccess(
  232. members
  233. )}(...) prevents optimization as ${base} is passed as call context at ${formatLocation(
  234. expr.loc
  235. )}`
  236. );
  237. }
  238. const dep = new CommonJsSelfReferenceDependency(
  239. expr.range,
  240. base,
  241. members,
  242. !!call
  243. );
  244. dep.loc = expr.loc;
  245. parser.state.module.addDependency(dep);
  246. if (call) {
  247. parser.walkExpressions(call.arguments);
  248. }
  249. return true;
  250. };
  251. parser.hooks.callMemberChain
  252. .for("exports")
  253. .tap("CommonJsExportsParserPlugin", (expr, members) => {
  254. return handleAccessExport(expr.callee, "exports", members, expr);
  255. });
  256. parser.hooks.expressionMemberChain
  257. .for("exports")
  258. .tap("CommonJsExportsParserPlugin", (expr, members) => {
  259. return handleAccessExport(expr, "exports", members);
  260. });
  261. parser.hooks.expression
  262. .for("exports")
  263. .tap("CommonJsExportsParserPlugin", expr => {
  264. return handleAccessExport(expr, "exports", []);
  265. });
  266. parser.hooks.callMemberChain
  267. .for("module")
  268. .tap("CommonJsExportsParserPlugin", (expr, members) => {
  269. if (members[0] !== "exports") return;
  270. return handleAccessExport(
  271. expr.callee,
  272. "module.exports",
  273. members.slice(1),
  274. expr
  275. );
  276. });
  277. parser.hooks.expressionMemberChain
  278. .for("module")
  279. .tap("CommonJsExportsParserPlugin", (expr, members) => {
  280. if (members[0] !== "exports") return;
  281. return handleAccessExport(expr, "module.exports", members.slice(1));
  282. });
  283. parser.hooks.expression
  284. .for("module.exports")
  285. .tap("CommonJsExportsParserPlugin", expr => {
  286. return handleAccessExport(expr, "module.exports", []);
  287. });
  288. parser.hooks.callMemberChain
  289. .for("this")
  290. .tap("CommonJsExportsParserPlugin", (expr, members) => {
  291. if (!parser.scope.topLevelScope) return;
  292. return handleAccessExport(expr.callee, "this", members, expr);
  293. });
  294. parser.hooks.expressionMemberChain
  295. .for("this")
  296. .tap("CommonJsExportsParserPlugin", (expr, members) => {
  297. if (!parser.scope.topLevelScope) return;
  298. return handleAccessExport(expr, "this", members);
  299. });
  300. parser.hooks.expression
  301. .for("this")
  302. .tap("CommonJsExportsParserPlugin", expr => {
  303. if (!parser.scope.topLevelScope) return;
  304. return handleAccessExport(expr, "this", []);
  305. });
  306. // Bailouts //
  307. parser.hooks.expression.for("module").tap("CommonJsPlugin", expr => {
  308. bailout();
  309. const isHarmony = HarmonyExports.isEnabled(parser.state);
  310. const dep = new ModuleDecoratorDependency(
  311. isHarmony
  312. ? RuntimeGlobals.harmonyModuleDecorator
  313. : RuntimeGlobals.nodeModuleDecorator,
  314. !isHarmony
  315. );
  316. dep.loc = expr.loc;
  317. parser.state.module.addDependency(dep);
  318. return true;
  319. });
  320. }
  321. }
  322. module.exports = CommonJsExportsParserPlugin;