ConstExportsPlugin.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Haijie Xie @hai-x
  4. */
  5. "use strict";
  6. const { UsageState } = require("../ExportsInfo");
  7. const {
  8. JAVASCRIPT_MODULE_TYPE_AUTO,
  9. JAVASCRIPT_MODULE_TYPE_ESM
  10. } = require("../ModuleTypeConstants");
  11. const BasicEvaluatedExpression = require("../javascript/BasicEvaluatedExpression");
  12. const { VariableInfoFlags } = require("../javascript/JavascriptParser");
  13. const {
  14. InlinedUsedName,
  15. enableInlineExports,
  16. toInlinedValue
  17. } = require("./InlineExports");
  18. /** @import { InlinedValue } from "./InlineExports" */
  19. /** @import Compiler from "../Compiler" */
  20. /** @import ExportsInfo from "../ExportsInfo" */
  21. /** @import JavascriptParser from "../javascript/JavascriptParser" */
  22. /**
  23. * @import {
  24. * JavascriptModuleBuildInfo
  25. * } from "../javascript/JavascriptModule"
  26. */
  27. /** @typedef {{ inlineExports: boolean }} ConstValueParserPluginOptions */
  28. const PLUGIN_NAME = "ConstExportsPlugin";
  29. // Parser tag for top-level `const X = <primitive>` bindings
  30. const CONST_BINDING_TAG = Symbol("const binding");
  31. class ConstExportsPlugin {
  32. /**
  33. * @param {ConstValueParserPluginOptions} options plugin options
  34. */
  35. constructor(options) {
  36. /** @type {ConstValueParserPluginOptions} */
  37. this.options = options;
  38. }
  39. /**
  40. * @param {Compiler} compiler the compiler instance
  41. * @returns {void}
  42. */
  43. apply(compiler) {
  44. compiler.hooks.compilation.tap(
  45. PLUGIN_NAME,
  46. (compilation, { normalModuleFactory }) => {
  47. /**
  48. * @param {JavascriptParser} parser the parser
  49. * @returns {void}
  50. */
  51. const handleInlineExports = (parser) => {
  52. parser.hooks.program.tap(PLUGIN_NAME, () => {
  53. const buildInfo =
  54. /** @type {JavascriptModuleBuildInfo | undefined} */
  55. (parser.state.module.buildInfo);
  56. if (buildInfo) buildInfo.inlineExports = true;
  57. });
  58. // Propagate inlined constant through evaluator so chained constants and uses see the literal
  59. parser.hooks.evaluateIdentifier
  60. .for(CONST_BINDING_TAG)
  61. .tap(PLUGIN_NAME, (expr) => {
  62. const tagData =
  63. /** @type {{ value?: InlinedValue } | undefined} */
  64. (parser.currentTagData);
  65. if (!tagData || !tagData.value) return;
  66. const { value } = tagData;
  67. const eval_ = new BasicEvaluatedExpression().setRange(
  68. /** @type {[number, number]} */ (expr.range)
  69. );
  70. switch (value.kind) {
  71. case "null":
  72. return eval_.setNull();
  73. case "undefined":
  74. return eval_.setUndefined();
  75. case "boolean":
  76. return eval_.setBoolean(/** @type {boolean} */ (value.value));
  77. case "number":
  78. return eval_.setNumber(/** @type {number} */ (value.value));
  79. case "string":
  80. return eval_.setString(/** @type {string} */ (value.value));
  81. }
  82. });
  83. };
  84. /**
  85. * @param {JavascriptParser} parser the parser
  86. * @returns {void}
  87. */
  88. const handleConstValue = (parser) => {
  89. // Only const is tracked; function/class names can be reassigned in sloppy mode.
  90. // Re-exports always use getters: cross-module bindings may be mutable,
  91. // and SideEffectsFlagPlugin can rewire connections skipping the template.
  92. parser.hooks.preDeclarator.tap(
  93. PLUGIN_NAME,
  94. (declarator, statement) => {
  95. // Detect top-level `const` declarations:
  96. // - Tag ALL const bindings with CONST_BINDING_TAG (for const detection via tag system)
  97. // - Carry inlined primitive value in tag data when eligible (for inline optimization)
  98. if (statement.kind !== "const") return;
  99. if (parser.scope.topLevelScope !== true) return;
  100. if (declarator.id.type === "Identifier") {
  101. let inlinedValue;
  102. if (this.options.inlineExports && declarator.init) {
  103. const evaluated = parser.evaluateExpression(declarator.init);
  104. inlinedValue = toInlinedValue(evaluated);
  105. }
  106. parser.tagVariable(
  107. declarator.id.name,
  108. CONST_BINDING_TAG,
  109. inlinedValue ? { value: inlinedValue } : {},
  110. VariableInfoFlags.Normal
  111. );
  112. } else {
  113. // Handle destructuring patterns (ObjectPattern, ArrayPattern, etc.)
  114. parser.enterPattern(declarator.id, (name) => {
  115. parser.tagVariable(
  116. name,
  117. CONST_BINDING_TAG,
  118. {},
  119. VariableInfoFlags.Normal
  120. );
  121. });
  122. }
  123. }
  124. );
  125. };
  126. /**
  127. * @param {JavascriptParser} parser the parser
  128. * @returns {void}
  129. */
  130. const handler = (parser) => {
  131. handleConstValue(parser);
  132. if (this.options.inlineExports) {
  133. handleInlineExports(parser);
  134. }
  135. };
  136. normalModuleFactory.hooks.parser
  137. .for(JAVASCRIPT_MODULE_TYPE_AUTO)
  138. .tap(PLUGIN_NAME, handler);
  139. normalModuleFactory.hooks.parser
  140. .for(JAVASCRIPT_MODULE_TYPE_ESM)
  141. .tap(PLUGIN_NAME, handler);
  142. if (!this.options.inlineExports) return;
  143. // Lets HarmonyImportSpecifierDependency.getCondition skip the inline check
  144. // (and stay unconditional) when this plugin is not active
  145. enableInlineExports(compilation.moduleGraph);
  146. const moduleGraph = compilation.moduleGraph;
  147. compilation.hooks.optimizeDependencies.tap(PLUGIN_NAME, (modules) => {
  148. /** @type {Set<ExportsInfo>} */
  149. const visited = new Set();
  150. /** @type {ExportsInfo[]} */
  151. let queue = [];
  152. for (const module of modules) {
  153. queue.push(moduleGraph.getExportsInfo(module));
  154. }
  155. while (queue.length > 0) {
  156. const items = queue;
  157. queue = [];
  158. for (const exportsInfo of items) {
  159. if (visited.has(exportsInfo)) continue;
  160. visited.add(exportsInfo);
  161. // Other-export usage means we can't safely inline (some non-statically-known consumer)
  162. if (
  163. exportsInfo.otherExportsInfo.getUsed(undefined) !==
  164. UsageState.Unused
  165. ) {
  166. continue;
  167. }
  168. for (const exportInfo of exportsInfo.ownedExports) {
  169. const inlined = exportInfo.canInline();
  170. const doInline =
  171. !exportInfo.hasUsedName() &&
  172. inlined !== undefined &&
  173. exportInfo.provided === true;
  174. if (doInline) {
  175. exportInfo.setUsedName(new InlinedUsedName(inlined));
  176. exportsInfo.markInlinedExports();
  177. }
  178. if (exportInfo.exportsInfoOwned && exportInfo.exportsInfo) {
  179. const used = exportInfo.getUsed(undefined);
  180. if (
  181. used === UsageState.OnlyPropertiesUsed ||
  182. used === UsageState.Unused
  183. ) {
  184. queue.push(exportInfo.exportsInfo);
  185. }
  186. }
  187. }
  188. }
  189. }
  190. });
  191. }
  192. );
  193. }
  194. }
  195. ConstExportsPlugin.CONST_BINDING_TAG = CONST_BINDING_TAG;
  196. module.exports = ConstExportsPlugin;