diff --git a/crates/oxc_transformer/src/es2020/export_namespace_from.rs b/crates/oxc_transformer/src/es2020/export_namespace_from.rs new file mode 100644 index 0000000000000..4ee4fd159d8ff --- /dev/null +++ b/crates/oxc_transformer/src/es2020/export_namespace_from.rs @@ -0,0 +1,120 @@ +//! ES2020: Export Namespace From +//! +//! This plugin transforms `export * as ns from "mod"` to `import * as _ns from "mod"; export { _ns as ns }`. +//! +//! > This plugin is included in `preset-env`, in ES2020 +//! +//! ## Example +//! +//! Input: +//! ```js +//! export * as ns from "mod"; +//! ``` +//! +//! Output: +//! ```js +//! import * as _ns from "mod"; +//! export { _ns as ns }; +//! ``` +//! +//! ## Implementation +//! +//! Implementation based on [@babel/plugin-transform-export-namespace-from](https://babeljs.io/docs/babel-plugin-transform-export-namespace-from). +//! +//! ## References: +//! * Babel plugin implementation: +//! * "export ns from" TC39 proposal: + +use oxc_allocator::TakeIn; +use oxc_ast::{NONE, ast::*}; +use oxc_semantic::SymbolFlags; +use oxc_span::SPAN; +use oxc_traverse::Traverse; + +use crate::{ + context::{TransformCtx, TraverseCtx}, + state::TransformState, +}; + +pub struct ExportNamespaceFrom<'a, 'ctx> { + _ctx: &'ctx TransformCtx<'a>, +} + +impl<'a, 'ctx> ExportNamespaceFrom<'a, 'ctx> { + pub fn new(ctx: &'ctx TransformCtx<'a>) -> Self { + Self { _ctx: ctx } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for ExportNamespaceFrom<'a, '_> { + fn exit_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { + // Early return if there's no `export * as ns from "mod"` to transform + let has_export_namespace = program.body.iter().any( + |stmt| matches!(stmt, Statement::ExportAllDeclaration(decl) if decl.exported.is_some()), + ); + if !has_export_namespace { + return; + } + + let mut new_statements = ctx.ast.vec_with_capacity(program.body.len()); + + for stmt in program.body.take_in(ctx.ast) { + match stmt { + Statement::ExportAllDeclaration(export_all) if export_all.exported.is_some() => { + // Transform `export * as ns from "mod"` to: + // `import * as _ns from "mod"; export { _ns as ns };` + + let ExportAllDeclaration { span, exported, source, export_kind, .. } = + export_all.unbox(); + let exported_name = exported.unwrap(); + + // Create a unique binding for the import based on the exported name + let binding = ctx.generate_uid_based_on_node( + &exported_name, + program.scope_id(), + SymbolFlags::Import, + ); + + // Create `import * as _ns from "mod"` + let import_specifier = ImportDeclarationSpecifier::ImportNamespaceSpecifier( + ctx.ast.alloc_import_namespace_specifier( + SPAN, + binding.create_binding_identifier(ctx), + ), + ); + + let import_decl = ctx.ast.alloc_import_declaration( + SPAN, + Some(ctx.ast.vec1(import_specifier)), + source, + None, + NONE, + export_kind, + ); + new_statements.push(Statement::ImportDeclaration(import_decl)); + + // Create `export { _ns as ns }` + let local = + ModuleExportName::IdentifierReference(binding.create_read_reference(ctx)); + let export_specifier = + ctx.ast.export_specifier(span, local, exported_name, export_kind); + + let export_named_decl = ctx.ast.alloc_export_named_declaration( + span, + None, + ctx.ast.vec1(export_specifier), + None, + export_kind, + NONE, + ); + new_statements.push(Statement::ExportNamedDeclaration(export_named_decl)); + } + _ => { + new_statements.push(stmt); + } + } + } + + program.body = new_statements; + } +} diff --git a/crates/oxc_transformer/src/es2020/mod.rs b/crates/oxc_transformer/src/es2020/mod.rs index 53004f995ab20..98f5c6f223c80 100644 --- a/crates/oxc_transformer/src/es2020/mod.rs +++ b/crates/oxc_transformer/src/es2020/mod.rs @@ -7,9 +7,11 @@ use crate::{ state::TransformState, }; +mod export_namespace_from; mod nullish_coalescing_operator; mod optional_chaining; mod options; +use export_namespace_from::ExportNamespaceFrom; use nullish_coalescing_operator::NullishCoalescingOperator; pub use optional_chaining::OptionalChaining; pub use options::ES2020Options; @@ -19,6 +21,7 @@ pub struct ES2020<'a, 'ctx> { options: ES2020Options, // Plugins + export_namespace_from: ExportNamespaceFrom<'a, 'ctx>, nullish_coalescing_operator: NullishCoalescingOperator<'a, 'ctx>, optional_chaining: OptionalChaining<'a, 'ctx>, } @@ -28,6 +31,7 @@ impl<'a, 'ctx> ES2020<'a, 'ctx> { Self { ctx, options, + export_namespace_from: ExportNamespaceFrom::new(ctx), nullish_coalescing_operator: NullishCoalescingOperator::new(ctx), optional_chaining: OptionalChaining::new(ctx), } @@ -35,6 +39,12 @@ impl<'a, 'ctx> ES2020<'a, 'ctx> { } impl<'a> Traverse<'a, TransformState<'a>> for ES2020<'a, '_> { + fn exit_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { + if self.options.export_namespace_from { + self.export_namespace_from.exit_program(program, ctx); + } + } + fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { if self.options.nullish_coalescing_operator { self.nullish_coalescing_operator.enter_expression(expr, ctx); diff --git a/crates/oxc_transformer/src/es2020/options.rs b/crates/oxc_transformer/src/es2020/options.rs index c452ef1584d2e..607971cfae425 100644 --- a/crates/oxc_transformer/src/es2020/options.rs +++ b/crates/oxc_transformer/src/es2020/options.rs @@ -3,6 +3,9 @@ use serde::Deserialize; #[derive(Debug, Default, Clone, Copy, Deserialize)] #[serde(default, rename_all = "camelCase", deny_unknown_fields)] pub struct ES2020Options { + #[serde(skip)] + pub export_namespace_from: bool, + #[serde(skip)] pub nullish_coalescing_operator: bool, diff --git a/crates/oxc_transformer/src/lib.rs b/crates/oxc_transformer/src/lib.rs index 858500f8f95e4..b77fbc4a6b88b 100644 --- a/crates/oxc_transformer/src/lib.rs +++ b/crates/oxc_transformer/src/lib.rs @@ -210,6 +210,7 @@ impl<'a> Traverse<'a, TransformState<'a>> for TransformerImpl<'a, '_> { typescript.exit_program(program, ctx); } self.x2_es2022.exit_program(program, ctx); + self.x2_es2020.exit_program(program, ctx); self.x2_es2018.exit_program(program, ctx); self.common.exit_program(program, ctx); } diff --git a/crates/oxc_transformer/src/options/babel/plugins.rs b/crates/oxc_transformer/src/options/babel/plugins.rs index 26999b7096b66..b47a359a5fa72 100644 --- a/crates/oxc_transformer/src/options/babel/plugins.rs +++ b/crates/oxc_transformer/src/options/babel/plugins.rs @@ -63,6 +63,7 @@ pub struct BabelPlugins { // ES2019 pub optional_catch_binding: bool, // ES2020 + pub export_namespace_from: bool, pub optional_chaining: bool, pub nullish_coalescing_operator: bool, // ES2021 @@ -148,6 +149,7 @@ impl TryFrom for BabelPlugins { } "transform-async-generator-functions" => p.async_generator_functions = true, "transform-optional-catch-binding" => p.optional_catch_binding = true, + "transform-export-namespace-from" => p.export_namespace_from = true, "transform-optional-chaining" => p.optional_chaining = true, "transform-nullish-coalescing-operator" => p.nullish_coalescing_operator = true, "transform-logical-assignment-operators" => p.logical_assignment_operators = true, diff --git a/crates/oxc_transformer/src/options/env.rs b/crates/oxc_transformer/src/options/env.rs index d2cb7c1e3df6c..3d4de7c31564d 100644 --- a/crates/oxc_transformer/src/options/env.rs +++ b/crates/oxc_transformer/src/options/env.rs @@ -77,6 +77,7 @@ impl EnvOptions { }, es2019: ES2019Options { optional_catch_binding: true }, es2020: ES2020Options { + export_namespace_from: true, nullish_coalescing_operator: true, // Turn this on would throw error for all bigints. big_int: false, @@ -154,6 +155,7 @@ impl From for EnvOptions { optional_catch_binding: o.has_feature(ES2019OptionalCatchBinding), }, es2020: ES2020Options { + export_namespace_from: o.has_feature(ES2020ExportNamespaceFrom), nullish_coalescing_operator: o.has_feature(ES2020NullishCoalescingOperator), big_int: o.has_feature(ES2020BigInt), optional_chaining: o.has_feature(ES2020OptionalChaining), diff --git a/crates/oxc_transformer/src/options/mod.rs b/crates/oxc_transformer/src/options/mod.rs index 3bee209d188c0..e475264d9b583 100644 --- a/crates/oxc_transformer/src/options/mod.rs +++ b/crates/oxc_transformer/src/options/mod.rs @@ -227,6 +227,8 @@ impl TryFrom<&BabelOptions> for TransformOptions { }; let es2020 = ES2020Options { + export_namespace_from: options.plugins.export_namespace_from + || env.es2020.export_namespace_from, optional_chaining: options.plugins.optional_chaining || env.es2020.optional_chaining, nullish_coalescing_operator: options.plugins.nullish_coalescing_operator || env.es2020.nullish_coalescing_operator, diff --git a/crates/oxc_traverse/src/ast_operations/gather_node_parts.rs b/crates/oxc_traverse/src/ast_operations/gather_node_parts.rs index e5833295f7524..8290f8bcd43b1 100644 --- a/crates/oxc_traverse/src/ast_operations/gather_node_parts.rs +++ b/crates/oxc_traverse/src/ast_operations/gather_node_parts.rs @@ -87,6 +87,16 @@ impl<'a> GatherNodeParts<'a> for ExportSpecifier<'a> { } } +impl<'a> GatherNodeParts<'a> for ModuleExportName<'a> { + fn gather(&self, f: &mut F) { + match self { + ModuleExportName::IdentifierName(ident) => ident.gather(f), + ModuleExportName::IdentifierReference(ident) => ident.gather(f), + ModuleExportName::StringLiteral(lit) => lit.gather(f), + } + } +} + impl<'a> GatherNodeParts<'a> for ImportSpecifier<'a> { fn gather(&self, f: &mut F) { self.local.gather(f); diff --git a/tasks/coverage/snapshots/semantic_typescript.snap b/tasks/coverage/snapshots/semantic_typescript.snap index 2ab10ceb1a86f..c9854e9f4693a 100644 --- a/tasks/coverage/snapshots/semantic_typescript.snap +++ b/tasks/coverage/snapshots/semantic_typescript.snap @@ -7435,8 +7435,8 @@ rebuilt : [] semantic Error: tasks/coverage/typescript/tests/cases/compiler/declarationEmitDoesNotUseReexportedNamespaceAsLocal.ts Bindings mismatch: -after transform: ScopeId(0): ["add", "x"] -rebuilt : ScopeId(0): ["x"] +after transform: ScopeId(0): ["_Q", "add", "x"] +rebuilt : ScopeId(0): ["_Q", "x"] Scope children mismatch: after transform: ScopeId(0): [ScopeId(1)] rebuilt : ScopeId(0): [] @@ -37758,8 +37758,8 @@ rebuilt : ["x"] semantic Error: tasks/coverage/typescript/tests/cases/conformance/es2022/arbitraryModuleNamespaceIdentifiers/arbitraryModuleNamespaceIdentifiers_module.ts Bindings mismatch: -after transform: ScopeId(0): ["importStarTestA", "importTest", "reimportTest", "someValue", "typeA", "typeB", "typeC", "valueX", "valueY", "valueZ"] -rebuilt : ScopeId(0): ["importStarTestA", "importTest", "reimportTest", "someValue", "valueX", "valueY", "valueZ"] +after transform: ScopeId(0): ["_Z", "importStarTestA", "importTest", "reimportTest", "someValue", "typeA", "typeB", "typeC", "valueX", "valueY", "valueZ"] +rebuilt : ScopeId(0): ["_Z", "importStarTestA", "importTest", "reimportTest", "someValue", "valueX", "valueY", "valueZ"] Scope children mismatch: after transform: ScopeId(0): [ScopeId(1), ScopeId(2)] rebuilt : ScopeId(0): [] diff --git a/tasks/transform_conformance/snapshots/babel.snap.md b/tasks/transform_conformance/snapshots/babel.snap.md index 3c78f9123ea75..a1903bbb2a25e 100644 --- a/tasks/transform_conformance/snapshots/babel.snap.md +++ b/tasks/transform_conformance/snapshots/babel.snap.md @@ -1,16 +1,17 @@ commit: 41d96516 -Passed: 706/1213 +Passed: 712/1217 # All Passed: * babel-plugin-transform-logical-assignment-operators +* babel-plugin-transform-export-namespace-from * babel-plugin-transform-optional-catch-binding * babel-plugin-transform-react-display-name * babel-plugin-transform-react-jsx-self * babel-plugin-transform-react-jsx-source -# babel-preset-env (43/130) +# babel-preset-env (45/130) * dynamic-import/auto-esm-unsupported-import-unsupported/input.mjs x Output mismatch @@ -47,12 +48,6 @@ x Output mismatch * export-namespace-from/auto-export-namespace-not-supported/input.mjs x Output mismatch -* export-namespace-from/false-export-namespace-not-supported/input.mjs -x Output mismatch - -* export-namespace-from/false-export-namespace-not-supported-caller-supported/input.mjs -x Output mismatch - * modules/auto-cjs/input.mjs x Output mismatch diff --git a/tasks/transform_conformance/src/constants.rs b/tasks/transform_conformance/src/constants.rs index 424b610c473e4..0ead07c4cd489 100644 --- a/tasks/transform_conformance/src/constants.rs +++ b/tasks/transform_conformance/src/constants.rs @@ -14,7 +14,7 @@ pub const PLUGINS: &[&str] = &[ "babel-plugin-transform-logical-assignment-operators", // "babel-plugin-transform-numeric-separator", // ES2020 - // "babel-plugin-transform-export-namespace-from", + "babel-plugin-transform-export-namespace-from", // "babel-plugin-transform-dynamic-import", "babel-plugin-transform-nullish-coalescing-operator", "babel-plugin-transform-optional-chaining",