Skip to content

Commit bc4f0a1

Browse files
Copilotsapphi-redDunqingautofix-ci[bot]graphite-app[bot]
authored
feat(transformer): add ES2020 export namespace from transformation (#14277)
✅ **Implementation Complete: ES2020 Export Namespace From Transformation** Successfully implemented the transformation for `export * as ns from "mod"` to convert it to: ```js import * as _ns from "mod"; export { _ns as ns }; ``` ## Changes Made: - [x] Core transformation in `es2020/export_namespace_from.rs` - [x] ES2020 module integration and options - [x] Browser compatibility mapping in `oxc_compat` (auto-generated from custom compat data) - [x] Babel plugin support (`transform-export-namespace-from`) - [x] Target-based transformation (works with old browsers, skips modern ones) - [x] Code optimizations (removed clones, reserved capacity, used vec1) - [x] Custom compat data infrastructure for features not in compat-table - [x] Early return optimization to avoid unnecessary allocations - [x] Implemented `GatherNodeParts` for `ModuleExportName` to avoid string allocations - [x] All tests passing ✅ ## Code Review Addressed: - Reserved capacity for statements vector - Removed unnecessary clones via destructuring - Used `ctx.ast.vec1()` for single-element vectors - Created custom compat data system to avoid manual edits - Imported `TransformCtx` directly - Added early return when no export namespace declarations exist - Implemented `GatherNodeParts` for `ModuleExportName` and use `generate_uid_based_on_node` ## Example: **Input:** ```js export * as ns from "mod"; ``` **Output:** ```js import * as _ns from 'mod'; export { _ns as ns }; ``` <!-- START COPILOT CODING AGENT SUFFIX --> <details> <summary>Original prompt</summary> > > ---- > > *This section details on the original issue you should resolve* > > <issue_title>transformer: transform `export * as ns from "mod";` to `import * as ns from "mod"; export { ns }`</issue_title> > <issue_description>This transformation is supported by babel: https://babeljs.io/docs/babel-plugin-transform-export-namespace-from > ```js > export * as ns from "mod"; > > // to > > import * as _ns from "mod"; > export { _ns as ns }; > ```</issue_description> > > ## Comments on the Issue (you are @copilot in this section) > > <comments> > </comments> > </details> Fixes #14257 <!-- START COPILOT CODING AGENT TIPS --> --- ✨ Let Copilot coding agent [set things up for you](https://github.com/oxc-project/oxc/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo. --------- Signed-off-by: Dunqing <[email protected]> Co-authored-by: sapphi-red <[email protected]> Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: Dunqing <[email protected]> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Dunqing <[email protected]> Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
1 parent a631dd7 commit bc4f0a1

File tree

11 files changed

+158
-13
lines changed

11 files changed

+158
-13
lines changed
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
//! ES2020: Export Namespace From
2+
//!
3+
//! This plugin transforms `export * as ns from "mod"` to `import * as _ns from "mod"; export { _ns as ns }`.
4+
//!
5+
//! > This plugin is included in `preset-env`, in ES2020
6+
//!
7+
//! ## Example
8+
//!
9+
//! Input:
10+
//! ```js
11+
//! export * as ns from "mod";
12+
//! ```
13+
//!
14+
//! Output:
15+
//! ```js
16+
//! import * as _ns from "mod";
17+
//! export { _ns as ns };
18+
//! ```
19+
//!
20+
//! ## Implementation
21+
//!
22+
//! Implementation based on [@babel/plugin-transform-export-namespace-from](https://babeljs.io/docs/babel-plugin-transform-export-namespace-from).
23+
//!
24+
//! ## References:
25+
//! * Babel plugin implementation: <https://github.com/babel/babel/tree/v7.28.4/packages/babel-plugin-transform-export-namespace-from>
26+
//! * "export ns from" TC39 proposal: <https://github.com/tc39/proposal-export-ns-from>
27+
28+
use oxc_allocator::TakeIn;
29+
use oxc_ast::{NONE, ast::*};
30+
use oxc_semantic::SymbolFlags;
31+
use oxc_span::SPAN;
32+
use oxc_traverse::Traverse;
33+
34+
use crate::{
35+
context::{TransformCtx, TraverseCtx},
36+
state::TransformState,
37+
};
38+
39+
pub struct ExportNamespaceFrom<'a, 'ctx> {
40+
_ctx: &'ctx TransformCtx<'a>,
41+
}
42+
43+
impl<'a, 'ctx> ExportNamespaceFrom<'a, 'ctx> {
44+
pub fn new(ctx: &'ctx TransformCtx<'a>) -> Self {
45+
Self { _ctx: ctx }
46+
}
47+
}
48+
49+
impl<'a> Traverse<'a, TransformState<'a>> for ExportNamespaceFrom<'a, '_> {
50+
fn exit_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
51+
// Early return if there's no `export * as ns from "mod"` to transform
52+
let has_export_namespace = program.body.iter().any(
53+
|stmt| matches!(stmt, Statement::ExportAllDeclaration(decl) if decl.exported.is_some()),
54+
);
55+
if !has_export_namespace {
56+
return;
57+
}
58+
59+
let mut new_statements = ctx.ast.vec_with_capacity(program.body.len());
60+
61+
for stmt in program.body.take_in(ctx.ast) {
62+
match stmt {
63+
Statement::ExportAllDeclaration(export_all) if export_all.exported.is_some() => {
64+
// Transform `export * as ns from "mod"` to:
65+
// `import * as _ns from "mod"; export { _ns as ns };`
66+
67+
let ExportAllDeclaration { span, exported, source, export_kind, .. } =
68+
export_all.unbox();
69+
let exported_name = exported.unwrap();
70+
71+
// Create a unique binding for the import based on the exported name
72+
let binding = ctx.generate_uid_based_on_node(
73+
&exported_name,
74+
program.scope_id(),
75+
SymbolFlags::Import,
76+
);
77+
78+
// Create `import * as _ns from "mod"`
79+
let import_specifier = ImportDeclarationSpecifier::ImportNamespaceSpecifier(
80+
ctx.ast.alloc_import_namespace_specifier(
81+
SPAN,
82+
binding.create_binding_identifier(ctx),
83+
),
84+
);
85+
86+
let import_decl = ctx.ast.alloc_import_declaration(
87+
SPAN,
88+
Some(ctx.ast.vec1(import_specifier)),
89+
source,
90+
None,
91+
NONE,
92+
export_kind,
93+
);
94+
new_statements.push(Statement::ImportDeclaration(import_decl));
95+
96+
// Create `export { _ns as ns }`
97+
let local =
98+
ModuleExportName::IdentifierReference(binding.create_read_reference(ctx));
99+
let export_specifier =
100+
ctx.ast.export_specifier(span, local, exported_name, export_kind);
101+
102+
let export_named_decl = ctx.ast.alloc_export_named_declaration(
103+
span,
104+
None,
105+
ctx.ast.vec1(export_specifier),
106+
None,
107+
export_kind,
108+
NONE,
109+
);
110+
new_statements.push(Statement::ExportNamedDeclaration(export_named_decl));
111+
}
112+
_ => {
113+
new_statements.push(stmt);
114+
}
115+
}
116+
}
117+
118+
program.body = new_statements;
119+
}
120+
}

crates/oxc_transformer/src/es2020/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ use crate::{
77
state::TransformState,
88
};
99

10+
mod export_namespace_from;
1011
mod nullish_coalescing_operator;
1112
mod optional_chaining;
1213
mod options;
14+
use export_namespace_from::ExportNamespaceFrom;
1315
use nullish_coalescing_operator::NullishCoalescingOperator;
1416
pub use optional_chaining::OptionalChaining;
1517
pub use options::ES2020Options;
@@ -19,6 +21,7 @@ pub struct ES2020<'a, 'ctx> {
1921
options: ES2020Options,
2022

2123
// Plugins
24+
export_namespace_from: ExportNamespaceFrom<'a, 'ctx>,
2225
nullish_coalescing_operator: NullishCoalescingOperator<'a, 'ctx>,
2326
optional_chaining: OptionalChaining<'a, 'ctx>,
2427
}
@@ -28,13 +31,20 @@ impl<'a, 'ctx> ES2020<'a, 'ctx> {
2831
Self {
2932
ctx,
3033
options,
34+
export_namespace_from: ExportNamespaceFrom::new(ctx),
3135
nullish_coalescing_operator: NullishCoalescingOperator::new(ctx),
3236
optional_chaining: OptionalChaining::new(ctx),
3337
}
3438
}
3539
}
3640

3741
impl<'a> Traverse<'a, TransformState<'a>> for ES2020<'a, '_> {
42+
fn exit_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
43+
if self.options.export_namespace_from {
44+
self.export_namespace_from.exit_program(program, ctx);
45+
}
46+
}
47+
3848
fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
3949
if self.options.nullish_coalescing_operator {
4050
self.nullish_coalescing_operator.enter_expression(expr, ctx);

crates/oxc_transformer/src/es2020/options.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ use serde::Deserialize;
33
#[derive(Debug, Default, Clone, Copy, Deserialize)]
44
#[serde(default, rename_all = "camelCase", deny_unknown_fields)]
55
pub struct ES2020Options {
6+
#[serde(skip)]
7+
pub export_namespace_from: bool,
8+
69
#[serde(skip)]
710
pub nullish_coalescing_operator: bool,
811

crates/oxc_transformer/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ impl<'a> Traverse<'a, TransformState<'a>> for TransformerImpl<'a, '_> {
210210
typescript.exit_program(program, ctx);
211211
}
212212
self.x2_es2022.exit_program(program, ctx);
213+
self.x2_es2020.exit_program(program, ctx);
213214
self.x2_es2018.exit_program(program, ctx);
214215
self.common.exit_program(program, ctx);
215216
}

crates/oxc_transformer/src/options/babel/plugins.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ pub struct BabelPlugins {
6363
// ES2019
6464
pub optional_catch_binding: bool,
6565
// ES2020
66+
pub export_namespace_from: bool,
6667
pub optional_chaining: bool,
6768
pub nullish_coalescing_operator: bool,
6869
// ES2021
@@ -148,6 +149,7 @@ impl TryFrom<PluginPresetEntries> for BabelPlugins {
148149
}
149150
"transform-async-generator-functions" => p.async_generator_functions = true,
150151
"transform-optional-catch-binding" => p.optional_catch_binding = true,
152+
"transform-export-namespace-from" => p.export_namespace_from = true,
151153
"transform-optional-chaining" => p.optional_chaining = true,
152154
"transform-nullish-coalescing-operator" => p.nullish_coalescing_operator = true,
153155
"transform-logical-assignment-operators" => p.logical_assignment_operators = true,

crates/oxc_transformer/src/options/env.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ impl EnvOptions {
7777
},
7878
es2019: ES2019Options { optional_catch_binding: true },
7979
es2020: ES2020Options {
80+
export_namespace_from: true,
8081
nullish_coalescing_operator: true,
8182
// Turn this on would throw error for all bigints.
8283
big_int: false,
@@ -154,6 +155,7 @@ impl From<EngineTargets> for EnvOptions {
154155
optional_catch_binding: o.has_feature(ES2019OptionalCatchBinding),
155156
},
156157
es2020: ES2020Options {
158+
export_namespace_from: o.has_feature(ES2020ExportNamespaceFrom),
157159
nullish_coalescing_operator: o.has_feature(ES2020NullishCoalescingOperator),
158160
big_int: o.has_feature(ES2020BigInt),
159161
optional_chaining: o.has_feature(ES2020OptionalChaining),

crates/oxc_transformer/src/options/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,8 @@ impl TryFrom<&BabelOptions> for TransformOptions {
227227
};
228228

229229
let es2020 = ES2020Options {
230+
export_namespace_from: options.plugins.export_namespace_from
231+
|| env.es2020.export_namespace_from,
230232
optional_chaining: options.plugins.optional_chaining || env.es2020.optional_chaining,
231233
nullish_coalescing_operator: options.plugins.nullish_coalescing_operator
232234
|| env.es2020.nullish_coalescing_operator,

crates/oxc_traverse/src/ast_operations/gather_node_parts.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,16 @@ impl<'a> GatherNodeParts<'a> for ExportSpecifier<'a> {
8787
}
8888
}
8989

90+
impl<'a> GatherNodeParts<'a> for ModuleExportName<'a> {
91+
fn gather<F: FnMut(&str)>(&self, f: &mut F) {
92+
match self {
93+
ModuleExportName::IdentifierName(ident) => ident.gather(f),
94+
ModuleExportName::IdentifierReference(ident) => ident.gather(f),
95+
ModuleExportName::StringLiteral(lit) => lit.gather(f),
96+
}
97+
}
98+
}
99+
90100
impl<'a> GatherNodeParts<'a> for ImportSpecifier<'a> {
91101
fn gather<F: FnMut(&str)>(&self, f: &mut F) {
92102
self.local.gather(f);

tasks/coverage/snapshots/semantic_typescript.snap

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7435,8 +7435,8 @@ rebuilt : []
74357435

74367436
semantic Error: tasks/coverage/typescript/tests/cases/compiler/declarationEmitDoesNotUseReexportedNamespaceAsLocal.ts
74377437
Bindings mismatch:
7438-
after transform: ScopeId(0): ["add", "x"]
7439-
rebuilt : ScopeId(0): ["x"]
7438+
after transform: ScopeId(0): ["_Q", "add", "x"]
7439+
rebuilt : ScopeId(0): ["_Q", "x"]
74407440
Scope children mismatch:
74417441
after transform: ScopeId(0): [ScopeId(1)]
74427442
rebuilt : ScopeId(0): []
@@ -37758,8 +37758,8 @@ rebuilt : ["x"]
3775837758

3775937759
semantic Error: tasks/coverage/typescript/tests/cases/conformance/es2022/arbitraryModuleNamespaceIdentifiers/arbitraryModuleNamespaceIdentifiers_module.ts
3776037760
Bindings mismatch:
37761-
after transform: ScopeId(0): ["importStarTestA", "importTest", "reimportTest", "someValue", "typeA", "typeB", "typeC", "valueX", "valueY", "valueZ"]
37762-
rebuilt : ScopeId(0): ["importStarTestA", "importTest", "reimportTest", "someValue", "valueX", "valueY", "valueZ"]
37761+
after transform: ScopeId(0): ["_Z", "importStarTestA", "importTest", "reimportTest", "someValue", "typeA", "typeB", "typeC", "valueX", "valueY", "valueZ"]
37762+
rebuilt : ScopeId(0): ["_Z", "importStarTestA", "importTest", "reimportTest", "someValue", "valueX", "valueY", "valueZ"]
3776337763
Scope children mismatch:
3776437764
after transform: ScopeId(0): [ScopeId(1), ScopeId(2)]
3776537765
rebuilt : ScopeId(0): []

tasks/transform_conformance/snapshots/babel.snap.md

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
commit: 41d96516
22

3-
Passed: 706/1213
3+
Passed: 712/1217
44

55
# All Passed:
66
* babel-plugin-transform-logical-assignment-operators
7+
* babel-plugin-transform-export-namespace-from
78
* babel-plugin-transform-optional-catch-binding
89
* babel-plugin-transform-react-display-name
910
* babel-plugin-transform-react-jsx-self
1011
* babel-plugin-transform-react-jsx-source
1112

1213

13-
# babel-preset-env (43/130)
14+
# babel-preset-env (45/130)
1415
* dynamic-import/auto-esm-unsupported-import-unsupported/input.mjs
1516
x Output mismatch
1617

@@ -47,12 +48,6 @@ x Output mismatch
4748
* export-namespace-from/auto-export-namespace-not-supported/input.mjs
4849
x Output mismatch
4950

50-
* export-namespace-from/false-export-namespace-not-supported/input.mjs
51-
x Output mismatch
52-
53-
* export-namespace-from/false-export-namespace-not-supported-caller-supported/input.mjs
54-
x Output mismatch
55-
5651
* modules/auto-cjs/input.mjs
5752
x Output mismatch
5853

0 commit comments

Comments
 (0)