Repository: oxc-project/oxc (Rust)

Issue:
transformer: warn top level await usage if not supported
A warning is emitted when BigInt is used and the target does not support it ([playground](https://playground.oxc.rs/#eNptUj1vGzEM/SuGZg9Np9ZFp6YGAvQjyJCd0fEuSnTkgeQ5Dg7+76Euljwkk6RHkXzvkUuIYRcik9qmZ9783FzRj7ANHHZLkJnKkRNZ2PWQFbehZxnBDOUjcnPBTIC0BMLOZHYgKWcw7K4xZhCw5B3b75fHZKgTRGzQCDTkyzPyOAnqJUUjTx5+L66v4wPn+or9cP522oYJRAvXJeDRkNT7ul7To2uEnPnlDm0W+j+bpg73M8VCrZYqPVEOeAviuRVd8+6/3ZBJ8oqxBRSdtqX4W4TljDqH4t/Kwe/NmHdSBjKgmxtQv365+u6kZsVr7BPhnuVXBtV9wty1FnicUNKIZJDdS3YrWyuPjska+hcNOjBoPD6fwRLUdUyuBoXAXazWRe5wwHUDqIy4GfykHfvWnM0gYltrVSTjUP+uVerkivw6VlfO0x88YCv6jDj9gxHriNdUd5jz3t0uGQeUB9a6E0UPPWF085bL7eRwt9pX4HY7FbzYXDWd3gBEIxCc)). Similar to that, it would be helpful if a warning is emitted for top-level await as well.


Discussion:

Patch:
diff --git a/crates/oxc_compat/src/es_features.rs b/crates/oxc_compat/src/es_features.rs
index 8502fde8a858e..6fcf6f38fb7e3 100644
--- a/crates/oxc_compat/src/es_features.rs
+++ b/crates/oxc_compat/src/es_features.rs
@@ -56,6 +56,7 @@ pub enum ESFeature {
     ES2026ExplicitResourceManagement,
     ES2020ExportNamespaceFrom,
     ES2020ArbitraryModuleNamespaceNames,
+    ES2022TopLevelAwait,
 }
 pub fn features() -> &'static FxHashMap<ESFeature, EngineTargets> {
     use ESFeature::*;
@@ -922,6 +923,23 @@ pub fn features() -> &'static FxHashMap<ESFeature, EngineTargets> {
                     (Es, Version(2020u16, 0, 0)),
                 ])),
             ),
+            (
+                ES2022TopLevelAwait,
+                EngineTargets::new(FxHashMap::from_iter([
+                    (Samsung, Version(15u16, 0u16, 0u16)),
+                    (Node, Version(14u16, 8u16, 0u16)),
+                    (Firefox, Version(89u16, 0u16, 0u16)),
+                    (Chrome, Version(89u16, 0u16, 0u16)),
+                    (Safari, Version(15u16, 0u16, 0u16)),
+                    (Ios, Version(15u16, 0u16, 0u16)),
+                    (Edge, Version(89u16, 0u16, 0u16)),
+                    (OperaMobile, Version(63u16, 0u16, 0u16)),
+                    (Deno, Version(1u16, 0u16, 0u16)),
+                    (Electron, Version(12u16, 0u16, 0u16)),
+                    (Opera, Version(75u16, 0u16, 0u16)),
+                    (Es, Version(2022u16, 0, 0)),
+                ])),
+            ),
         ])
     })
 }
diff --git a/crates/oxc_transformer/src/es2022/mod.rs b/crates/oxc_transformer/src/es2022/mod.rs
index c8a151c14157a..79ed896f9fd72 100644
--- a/crates/oxc_transformer/src/es2022/mod.rs
+++ b/crates/oxc_transformer/src/es2022/mod.rs
@@ -1,4 +1,5 @@
 use oxc_ast::ast::*;
+use oxc_diagnostics::OxcDiagnostic;
 use oxc_traverse::Traverse;
 
 use crate::{
@@ -16,6 +17,9 @@ use class_static_block::ClassStaticBlock;
 pub use options::ES2022Options;
 
 pub struct ES2022<'a, 'ctx> {
+    ctx: &'ctx TransformCtx<'a>,
+    options: ES2022Options,
+
     // Plugins
     class_static_block: Option<ClassStaticBlock>,
     class_properties: Option<ClassProperties<'a, 'ctx>>,
@@ -43,7 +47,7 @@ impl<'a, 'ctx> ES2022<'a, 'ctx> {
                     if options.class_static_block { Some(ClassStaticBlock::new()) } else { None };
                 (class_static_block, None)
             };
-        Self { class_static_block, class_properties }
+        Self { ctx, options, class_static_block, class_properties }
     }
 }
 
@@ -127,4 +131,24 @@ impl<'a> Traverse<'a, TransformState<'a>> for ES2022<'a, '_> {
             class_properties.exit_static_block(block, ctx);
         }
     }
+
+    fn enter_await_expression(
+        &mut self,
+        node: &mut AwaitExpression<'a>,
+        ctx: &mut TraverseCtx<'a>,
+    ) {
+        if self.options.top_level_await && Self::is_top_level(ctx) {
+            let warning = OxcDiagnostic::warn(
+                "Top-level await is not available in the configured target environment.",
+            )
+            .with_label(node.span);
+            self.ctx.error(warning);
+        }
+    }
+}
+
+impl ES2022<'_, '_> {
+    fn is_top_level(ctx: &TraverseCtx) -> bool {
+        ctx.current_hoist_scope_id() == ctx.scoping().root_scope_id()
+    }
 }
diff --git a/crates/oxc_transformer/src/es2022/options.rs b/crates/oxc_transformer/src/es2022/options.rs
index b33b166e82890..3719102adb2db 100644
--- a/crates/oxc_transformer/src/es2022/options.rs
+++ b/crates/oxc_transformer/src/es2022/options.rs
@@ -10,4 +10,7 @@ pub struct ES2022Options {
 
     #[serde(skip)]
     pub class_properties: Option<ClassPropertiesOptions>,
+
+    #[serde(skip)]
+    pub top_level_await: bool,
 }
diff --git a/crates/oxc_transformer/src/lib.rs b/crates/oxc_transformer/src/lib.rs
index 1bbf13405d2d1..9b2c855b10203 100644
--- a/crates/oxc_transformer/src/lib.rs
+++ b/crates/oxc_transformer/src/lib.rs
@@ -251,6 +251,14 @@ impl<'a> Traverse<'a, TransformState<'a>> for TransformerImpl<'a, '_> {
         self.x2_es2020.enter_big_int_literal(node, ctx);
     }
 
+    fn enter_await_expression(
+        &mut self,
+        node: &mut AwaitExpression<'a>,
+        ctx: &mut TraverseCtx<'a>,
+    ) {
+        self.x2_es2022.enter_await_expression(node, ctx);
+    }
+
     fn enter_import_specifier(
         &mut self,
         node: &mut ImportSpecifier<'a>,
diff --git a/crates/oxc_transformer/src/options/env.rs b/crates/oxc_transformer/src/options/env.rs
index 55c6b94884993..324ba6d4de813 100644
--- a/crates/oxc_transformer/src/options/env.rs
+++ b/crates/oxc_transformer/src/options/env.rs
@@ -88,6 +88,8 @@ impl EnvOptions {
             es2022: ES2022Options {
                 class_static_block: true,
                 class_properties: Some(ClassPropertiesOptions::default()),
+                // Turn this on would throw error for all top-level awaits.
+                top_level_await: false,
             },
             es2026: ES2026Options { explicit_resource_management: true },
         }
@@ -169,6 +171,7 @@ impl From<EngineTargets> for EnvOptions {
             es2022: ES2022Options {
                 class_static_block: o.has_feature(ES2022ClassStaticBlock),
                 class_properties: o.has_feature(ES2022ClassProperties).then(Default::default),
+                top_level_await: o.has_feature(ES2022TopLevelAwait),
             },
             es2026: ES2026Options {
                 explicit_resource_management: o.has_feature(ES2026ExplicitResourceManagement),
diff --git a/crates/oxc_transformer/src/options/mod.rs b/crates/oxc_transformer/src/options/mod.rs
index c13e97f70c436..a3e461fe41c18 100644
--- a/crates/oxc_transformer/src/options/mod.rs
+++ b/crates/oxc_transformer/src/options/mod.rs
@@ -244,6 +244,7 @@ impl TryFrom<&BabelOptions> for TransformOptions {
         let es2022 = ES2022Options {
             class_static_block: options.plugins.class_static_block || env.es2022.class_static_block,
             class_properties: options.plugins.class_properties.or(env.es2022.class_properties),
+            top_level_await: env.es2022.top_level_await,
         };
 
         if !errors.is_empty() {
diff --git a/tasks/compat_data/custom-compat-data.js b/tasks/compat_data/custom-compat-data.js
index 5b239d4c08799..0ea40459a148a 100644
--- a/tasks/compat_data/custom-compat-data.js
+++ b/tasks/compat_data/custom-compat-data.js
@@ -45,4 +45,25 @@ const customEs2020 = [
   },
 ].map(f('ES2020'));
 
-module.exports = [...customEs2020];
+const customEs2022 = [
+  {
+    name: 'TopLevelAwait',
+    babel: null,
+    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await#browser_compatibility
+    targets: {
+      chrome: '89',
+      opera: '75',
+      edge: '89',
+      firefox: '89',
+      safari: '15.0',
+      node: '14.8',
+      deno: '1.0',
+      ios: '15.0',
+      samsung: '15.0',
+      opera_mobile: '63',
+      electron: '12.0',
+    },
+  },
+].map(f('ES2022'));
+
+module.exports = [...customEs2020, ...customEs2022];
diff --git a/tasks/compat_data/data.json b/tasks/compat_data/data.json
index 786e1c983fc21..1683eb24a61e4 100644
--- a/tasks/compat_data/data.json
+++ b/tasks/compat_data/data.json
@@ -1083,5 +1083,23 @@
       "electron": "12.0"
     },
     "es": "ES2020"
+  },
+  {
+    "name": "TopLevelAwait",
+    "babel": null,
+    "targets": {
+      "chrome": "89",
+      "opera": "75",
+      "edge": "89",
+      "firefox": "89",
+      "safari": "15.0",
+      "node": "14.8",
+      "deno": "1.0",
+      "ios": "15.0",
+      "samsung": "15.0",
+      "opera_mobile": "63",
+      "electron": "12.0"
+    },
+    "es": "ES2022"
   }
 ]


Test patch:
diff --git a/crates/oxc_transformer/tests/integrations/es_target.rs b/crates/oxc_transformer/tests/integrations/es_target.rs
index 17f968f961da4..5f20f126b5bec 100644
--- a/crates/oxc_transformer/tests/integrations/es_target.rs
+++ b/crates/oxc_transformer/tests/integrations/es_target.rs
@@ -22,6 +22,10 @@ fn es_target() {
         ("es2021", "class foo { static {} }"),
         ("es2021", "class Foo { #a; }"),
         ("es2019", r#"export { foo as "string-name" };"#), // test arbitrary module namespace names warning
+        ("es2021", "await fetch('/')"),                    // test top-level await target error
+        ("es2022", "await fetch('/')"),                    // no error for es2022
+        ("es2021", "async function f() { await fetch('/'); }"), // no error inside async function
+        ("es2021", "{ await fetch('/'); }"), // test top-level await in block, should also error
     ];
 
     // Test no transformation for esnext.
diff --git a/crates/oxc_transformer/tests/integrations/snapshots/es_target.snap b/crates/oxc_transformer/tests/integrations/snapshots/es_target.snap
index 7e69447e2bf0f..70bcc2ebeaa5f 100644
--- a/crates/oxc_transformer/tests/integrations/snapshots/es_target.snap
+++ b/crates/oxc_transformer/tests/integrations/snapshots/es_target.snap
@@ -97,3 +97,35 @@ export { foo as "string-name" };
  1 | export { foo as "string-name" };
    :                 ^^^^^^^^^^^^^
    `----
+
+########## 12 es2021
+await fetch('/')
+----------
+
+  ! Top-level await is not available in the configured target environment.
+   ,----
+ 1 | await fetch('/')
+   : ^^^^^^^^^^^^^^^^
+   `----
+
+########## 13 es2022
+await fetch('/')
+----------
+await fetch('/');
+
+########## 14 es2021
+async function f() { await fetch('/'); }
+----------
+async function f() {
+	await fetch('/');
+}
+
+########## 15 es2021
+{ await fetch('/'); }
+----------
+
+  ! Top-level await is not available in the configured target environment.
+   ,----
+ 1 | { await fetch('/'); }
+   :   ^^^^^^^^^^^^^^^^
+   `----