Skip to content

Commit e1775f4

Browse files
committed
feat(transformer): add ES2020 export namespace from transformation
1 parent 7261530 commit e1775f4

File tree

9 files changed

+245
-0
lines changed

9 files changed

+245
-0
lines changed
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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/main/packages/babel-plugin-transform-export-namespace-from>
26+
27+
use oxc_allocator::TakeIn;
28+
use oxc_ast::{NONE, ast::*};
29+
use oxc_semantic::SymbolFlags;
30+
use oxc_span::SPAN;
31+
use oxc_traverse::Traverse;
32+
33+
use crate::{context::TraverseCtx, state::TransformState};
34+
35+
pub struct ExportNamespaceFrom<'a, 'ctx> {
36+
_ctx: &'ctx crate::context::TransformCtx<'a>,
37+
}
38+
39+
impl<'a, 'ctx> ExportNamespaceFrom<'a, 'ctx> {
40+
pub fn new(ctx: &'ctx crate::context::TransformCtx<'a>) -> Self {
41+
Self { _ctx: ctx }
42+
}
43+
}
44+
45+
impl<'a> Traverse<'a, TransformState<'a>> for ExportNamespaceFrom<'a, '_> {
46+
fn exit_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
47+
let mut new_statements = ctx.ast.vec_with_capacity(program.body.len());
48+
49+
for stmt in program.body.take_in(ctx.ast) {
50+
match stmt {
51+
Statement::ExportAllDeclaration(export_all) if export_all.exported.is_some() => {
52+
// Transform `export * as ns from "mod"` to:
53+
// `import * as _ns from "mod"; export { _ns as ns };`
54+
55+
let ExportAllDeclaration { span, exported, source, export_kind, .. } =
56+
export_all.unbox();
57+
58+
// Create a unique binding for the import
59+
let binding = ctx.generate_uid("ns", program.scope_id(), SymbolFlags::Import);
60+
61+
// Create `import * as _ns from "mod"`
62+
let import_specifier = ImportDeclarationSpecifier::ImportNamespaceSpecifier(
63+
ctx.ast.alloc_import_namespace_specifier(
64+
SPAN,
65+
binding.create_binding_identifier(ctx),
66+
),
67+
);
68+
69+
let import_decl = ctx.ast.alloc_import_declaration(
70+
SPAN,
71+
Some(ctx.ast.vec1(import_specifier)),
72+
source,
73+
None, // phase
74+
NONE, // with_clause
75+
export_kind,
76+
);
77+
new_statements.push(Statement::ImportDeclaration(import_decl));
78+
79+
// Create `export { _ns as ns }`
80+
let local =
81+
ModuleExportName::IdentifierReference(binding.create_read_reference(ctx));
82+
let export_specifier =
83+
ctx.ast.export_specifier(span, local, exported.unwrap(), export_kind);
84+
85+
let export_named_decl = ctx.ast.alloc_export_named_declaration(
86+
SPAN,
87+
None,
88+
ctx.ast.vec1(export_specifier),
89+
None,
90+
export_kind,
91+
NONE,
92+
);
93+
new_statements.push(Statement::ExportNamedDeclaration(export_named_decl));
94+
}
95+
_ => {
96+
new_statements.push(stmt);
97+
}
98+
}
99+
}
100+
101+
program.body = new_statements;
102+
}
103+
}

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
@@ -212,6 +212,7 @@ impl<'a> Traverse<'a, TransformState<'a>> for TransformerImpl<'a, '_> {
212212
typescript.exit_program(program, ctx);
213213
}
214214
self.x2_es2022.exit_program(program, ctx);
215+
self.x2_es2020.exit_program(program, ctx);
215216
self.x2_es2018.exit_program(program, ctx);
216217
self.common.exit_program(program, ctx);
217218
}

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
@@ -74,6 +74,7 @@ impl EnvOptions {
7474
},
7575
es2019: ES2019Options { optional_catch_binding: true },
7676
es2020: ES2020Options {
77+
export_namespace_from: true,
7778
nullish_coalescing_operator: true,
7879
// Turn this on would throw error for all bigints.
7980
big_int: false,
@@ -150,6 +151,7 @@ impl From<EngineTargets> for EnvOptions {
150151
optional_catch_binding: o.has_feature(ES2019OptionalCatchBinding),
151152
},
152153
es2020: ES2020Options {
154+
export_namespace_from: o.has_feature(ES2020ExportNamespaceFrom),
153155
nullish_coalescing_operator: o.has_feature(ES2020NullishCoalescingOperator),
154156
big_int: o.has_feature(ES2020BigInt),
155157
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
@@ -226,6 +226,8 @@ impl TryFrom<&BabelOptions> for TransformOptions {
226226
};
227227

228228
let es2020 = ES2020Options {
229+
export_namespace_from: options.plugins.export_namespace_from
230+
|| env.es2020.export_namespace_from,
229231
optional_chaining: options.plugins.optional_chaining || env.es2020.optional_chaining,
230232
nullish_coalescing_operator: options.plugins.nullish_coalescing_operator
231233
|| env.es2020.nullish_coalescing_operator,
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
use oxc_transformer::{BabelOptions, ES2020Options, EnvOptions, TransformOptions};
2+
3+
use crate::test;
4+
5+
#[test]
6+
fn test_export_namespace_from() {
7+
let cases = [
8+
(
9+
r#"export * as ns from "mod";"#,
10+
r#"import * as _ns from 'mod';
11+
export { _ns as ns };
12+
"#,
13+
),
14+
(
15+
r#"export * as foo from "bar";"#,
16+
r#"import * as _ns from 'bar';
17+
export { _ns as foo };
18+
"#,
19+
),
20+
(
21+
r#"export * as myModule from "./module";"#,
22+
r#"import * as _ns from './module';
23+
export { _ns as myModule };
24+
"#,
25+
),
26+
(
27+
r#"export * as default from "mod";"#,
28+
r#"import * as _ns from 'mod';
29+
export { _ns as default };
30+
"#,
31+
),
32+
];
33+
34+
let options = TransformOptions {
35+
env: EnvOptions {
36+
es2020: ES2020Options { export_namespace_from: true, ..ES2020Options::default() },
37+
..EnvOptions::default()
38+
},
39+
..TransformOptions::default()
40+
};
41+
42+
for (input, expected) in cases {
43+
let result = test(input, &options);
44+
assert_eq!(result, Ok(expected.to_string()), "Failed to transform: {}", input);
45+
}
46+
}
47+
48+
#[test]
49+
fn test_export_namespace_from_disabled() {
50+
let input = r#"export * as ns from "mod";"#;
51+
let options = TransformOptions {
52+
env: EnvOptions {
53+
es2020: ES2020Options { export_namespace_from: false, ..ES2020Options::default() },
54+
..EnvOptions::default()
55+
},
56+
..TransformOptions::default()
57+
};
58+
59+
let result = test(input, &options);
60+
// When disabled, the transformation should not happen
61+
assert_eq!(result, Ok("export * as ns from 'mod';\n".to_string()));
62+
}
63+
64+
#[test]
65+
fn test_export_star_without_alias() {
66+
// Regular export * should not be transformed
67+
let input = r#"export * from "mod";"#;
68+
let options = TransformOptions {
69+
env: EnvOptions {
70+
es2020: ES2020Options { export_namespace_from: true, ..ES2020Options::default() },
71+
..EnvOptions::default()
72+
},
73+
..TransformOptions::default()
74+
};
75+
76+
let result = test(input, &options);
77+
// Should remain unchanged
78+
assert_eq!(result, Ok("export * from 'mod';\n".to_string()));
79+
}
80+
81+
#[test]
82+
fn test_export_namespace_from_with_babel_plugin() {
83+
// Test using babel plugin option
84+
let input = r#"export * as ns from "mod";"#;
85+
let babel_options = BabelOptions {
86+
plugins: serde_json::from_str(r#"[["transform-export-namespace-from"]]"#).unwrap(),
87+
..BabelOptions::default()
88+
};
89+
let options = TransformOptions::try_from(&babel_options).unwrap();
90+
91+
let result = test(input, &options);
92+
assert_eq!(result, Ok("import * as _ns from 'mod';\nexport { _ns as ns };\n".to_string()));
93+
}
94+
95+
#[test]
96+
fn test_export_namespace_from_with_old_target() {
97+
// Test that it works with old browser targets that don't support the syntax
98+
let input = r#"export * as ns from "mod";"#;
99+
let options = TransformOptions {
100+
env: EnvOptions::from_browserslist_query("chrome 70").unwrap(),
101+
..TransformOptions::default()
102+
};
103+
104+
let result = test(input, &options);
105+
// Chrome 70 doesn't support export * as, so it should be transformed
106+
assert_eq!(result, Ok("import * as _ns from 'mod';\nexport { _ns as ns };\n".to_string()));
107+
}
108+
109+
#[test]
110+
fn test_export_namespace_from_with_modern_target() {
111+
// Test that it doesn't transform for modern browsers that support the syntax
112+
let input = r#"export * as ns from "mod";"#;
113+
let options = TransformOptions {
114+
env: EnvOptions::from_browserslist_query("chrome 90").unwrap(),
115+
..TransformOptions::default()
116+
};
117+
118+
let result = test(input, &options);
119+
// Chrome 90 supports export * as, so it should not be transformed
120+
assert_eq!(result, Ok("export * as ns from 'mod';\n".to_string()));
121+
}

crates/oxc_transformer/tests/integrations/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod es_target;
2+
mod export_namespace_from;
23
mod targets;
34

45
use std::path::Path;

0 commit comments

Comments
 (0)