CommonJsExportsParserPlugin.js 15 KB

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