CommonJsExportsParserPlugin.js 14 KB

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