HarmonyDetectionParserPlugin.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const { JAVASCRIPT_MODULE_TYPE_ESM } = require("../ModuleTypeConstants");
  7. const memoize = require("../util/memoize");
  8. const DynamicExports = require("./DynamicExports");
  9. const HarmonyCompatibilityDependency = require("./HarmonyCompatibilityDependency");
  10. const HarmonyExports = require("./HarmonyExports");
  11. const {
  12. IMPORT_META_NAMES,
  13. IMPORT_META_STAGE_ESM_DETECTION
  14. } = require("./ImportMetaPlugin");
  15. const TopLevelAwaitDependency = require("./TopLevelAwaitDependency");
  16. const getEnvironmentNotSupportAsyncWarning = memoize(() =>
  17. require("../errors/EnvironmentNotSupportAsyncWarning")
  18. );
  19. /** @import { BuildInfo, BuildMeta } from "../Module" */
  20. /** @import JavascriptParser, { Range } from "../javascript/JavascriptParser" */
  21. const PLUGIN_NAME = "HarmonyDetectionParserPlugin";
  22. module.exports = class HarmonyDetectionParserPlugin {
  23. /**
  24. * Applies the plugin by registering its hooks on the compiler.
  25. * @param {JavascriptParser} parser the parser
  26. * @returns {void}
  27. */
  28. apply(parser) {
  29. // `import.meta`/top-level `await` only parse as modules, so their presence
  30. // alone marks the module as ESM (matching Node.js syntax detection).
  31. /**
  32. * @param {boolean} isStrictHarmony strict harmony mode should be enabled
  33. * @returns {void}
  34. */
  35. const enableHarmony = (isStrictHarmony) => {
  36. if (HarmonyExports.isEnabled(parser.state)) return;
  37. const module = parser.state.module;
  38. const compatDep = new HarmonyCompatibilityDependency();
  39. compatDep.loc = {
  40. start: {
  41. line: -1,
  42. column: 0
  43. },
  44. end: {
  45. line: -1,
  46. column: 0
  47. },
  48. index: -3
  49. };
  50. module.addPresentationalDependency(compatDep);
  51. DynamicExports.bailout(parser.state);
  52. HarmonyExports.enable(parser.state, isStrictHarmony);
  53. parser.scope.isStrict = true;
  54. };
  55. parser.hooks.program.tap(PLUGIN_NAME, (ast) => {
  56. const isStrictHarmony =
  57. parser.state.module.type === JAVASCRIPT_MODULE_TYPE_ESM;
  58. const isHarmony =
  59. isStrictHarmony ||
  60. ast.body.some(
  61. (statement) =>
  62. statement.type === "ImportDeclaration" ||
  63. statement.type === "ExportDefaultDeclaration" ||
  64. statement.type === "ExportNamedDeclaration" ||
  65. statement.type === "ExportAllDeclaration"
  66. );
  67. if (isHarmony) {
  68. enableHarmony(isStrictHarmony);
  69. }
  70. });
  71. parser.hooks.topLevelAwait.tap(PLUGIN_NAME, (node) => {
  72. const module = parser.state.module;
  73. enableHarmony(false);
  74. /** @type {BuildMeta} */
  75. (module.buildMeta).async = true;
  76. const buildInfo = /** @type {BuildInfo} */ (module.buildInfo);
  77. if (node.type === "AwaitExpression") {
  78. // Record for a possible `await` -> `(yield …)` rewrite when the module
  79. // is lowered to a generator (target without `async`/`await`).
  80. module.addPresentationalDependency(
  81. new TopLevelAwaitDependency(
  82. /** @type {Range} */ (node.range),
  83. /** @type {Range} */ (node.argument.range)
  84. )
  85. );
  86. } else {
  87. // `for await…of` and `await using` can't be expressed as a generator;
  88. // never lower it.
  89. buildInfo.usesTopLevelAwaitForOf = true;
  90. }
  91. const { runtimeTemplate } = parser.state.compilation;
  92. if (
  93. !runtimeTemplate.supportsAsyncFunction() &&
  94. (!runtimeTemplate.supportsGenerator() ||
  95. buildInfo.usesTopLevelAwaitForOf)
  96. ) {
  97. module.addWarning(
  98. new (getEnvironmentNotSupportAsyncWarning())(module, "topLevelAwait")
  99. );
  100. }
  101. });
  102. // Using `import.meta` only parses as a module, so it marks the module ESM.
  103. // Non-bailing; staged before the bailing DefinePlugin and ImportMetaPlugin
  104. // replacement taps so it observes every access; unknown members go
  105. // through `unhandledExpressionMemberChain`.
  106. const onImportMeta = () => {
  107. enableHarmony(false);
  108. };
  109. const earlyTapOptions = {
  110. name: PLUGIN_NAME,
  111. stage: IMPORT_META_STAGE_ESM_DETECTION
  112. };
  113. for (const name of IMPORT_META_NAMES) {
  114. parser.hooks.expression.for(name).tap(earlyTapOptions, onImportMeta);
  115. parser.hooks.typeof.for(name).tap(earlyTapOptions, onImportMeta);
  116. }
  117. parser.hooks.expressionMemberChain
  118. .for("import.meta")
  119. .tap(earlyTapOptions, onImportMeta);
  120. parser.hooks.unhandledExpressionMemberChain
  121. .for("import.meta")
  122. .tap(earlyTapOptions, onImportMeta);
  123. /**
  124. * Returns true if in harmony.
  125. * @returns {boolean | undefined} true if in harmony
  126. */
  127. const skipInHarmony = () => {
  128. if (HarmonyExports.isEnabled(parser.state)) {
  129. return true;
  130. }
  131. };
  132. /**
  133. * Returns null if in harmony.
  134. * @returns {null | undefined} null if in harmony
  135. */
  136. const nullInHarmony = () => {
  137. if (HarmonyExports.isEnabled(parser.state)) {
  138. return null;
  139. }
  140. };
  141. /**
  142. * Walks call arguments so import bindings used inside callbacks are
  143. * still tracked, then skips default AMD/CommonJS handling.
  144. * @param {import("estree").CallExpression} expr call expression
  145. * @returns {boolean | undefined} true if in harmony
  146. */
  147. const walkArgumentsAndSkipInHarmony = (expr) => {
  148. if (HarmonyExports.isEnabled(parser.state)) {
  149. if (expr.arguments) parser.walkExpressions(expr.arguments);
  150. return true;
  151. }
  152. };
  153. const nonHarmonyIdentifiers = ["define", "exports"];
  154. for (const identifier of nonHarmonyIdentifiers) {
  155. parser.hooks.evaluateTypeof
  156. .for(identifier)
  157. .tap(PLUGIN_NAME, nullInHarmony);
  158. parser.hooks.typeof.for(identifier).tap(PLUGIN_NAME, skipInHarmony);
  159. parser.hooks.evaluate.for(identifier).tap(PLUGIN_NAME, nullInHarmony);
  160. parser.hooks.expression.for(identifier).tap(PLUGIN_NAME, skipInHarmony);
  161. parser.hooks.call
  162. .for(identifier)
  163. .tap(
  164. PLUGIN_NAME,
  165. identifier === "define"
  166. ? walkArgumentsAndSkipInHarmony
  167. : skipInHarmony
  168. );
  169. }
  170. }
  171. };