CommonJsExportsParserPlugin.js 14 KB

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