feat(transformer): implement some of needs_explicit_esm for typescript (#1047)

Co-authored-by: magic-akari <akari.ccino@gmail.com>
This commit is contained in:
Boshen 2023-10-24 17:30:01 +08:00 committed by GitHub
parent a442fad3b7
commit af1a76bafa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 287 additions and 189 deletions

View file

@ -1864,7 +1864,8 @@ pub struct ImportExpression<'a> {
pub struct ImportDeclaration<'a> {
#[cfg_attr(feature = "serde", serde(flatten))]
pub span: Span,
pub specifiers: Vec<'a, ImportDeclarationSpecifier>,
/// `None` for `import 'foo'`, `Some([])` for `import {} from 'foo'`
pub specifiers: Option<Vec<'a, ImportDeclarationSpecifier>>,
pub source: StringLiteral,
pub assertions: Option<Vec<'a, ImportAttribute>>, // Some(vec![]) for empty assertion
pub import_kind: ImportOrExportKind, // `import type { foo } from 'bar'`
@ -1885,12 +1886,13 @@ pub enum ImportDeclarationSpecifier {
// import {imported} from "source"
// import {imported as local} from "source"
#[derive(Debug, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize), serde(tag = "type"))]
#[cfg_attr(feature = "serde", derive(Serialize), serde(tag = "type", rename_all = "camelCase"))]
pub struct ImportSpecifier {
#[cfg_attr(feature = "serde", serde(flatten))]
pub span: Span,
pub imported: ModuleExportName,
pub local: BindingIdentifier,
pub import_kind: ImportOrExportKind,
}
// import local from "source"

View file

@ -975,7 +975,7 @@ impl<'a> AstBuilder<'a> {
pub fn import_declaration(
&self,
span: Span,
specifiers: Vec<'a, ImportDeclarationSpecifier>,
specifiers: Option<Vec<'a, ImportDeclarationSpecifier>>,
source: StringLiteral,
assertions: Option<Vec<'a, ImportAttribute>>,
import_kind: ImportOrExportKind,

View file

@ -142,11 +142,21 @@ impl<'a> BoundNames for ModuleDeclaration<'a> {
impl<'a> BoundNames for ImportDeclaration<'a> {
fn bound_names<F: FnMut(&BindingIdentifier)>(&self, f: &mut F) {
self.specifiers.iter().for_each(|specifier| match specifier {
ImportDeclarationSpecifier::ImportSpecifier(specifier) => f(&specifier.local),
ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => f(&specifier.local),
ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => f(&specifier.local),
});
if let Some(specifiers) = &self.specifiers {
for specifier in specifiers {
match specifier {
ImportDeclarationSpecifier::ImportSpecifier(specifier) => {
f(&specifier.local);
}
ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => {
f(&specifier.local);
}
ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => {
f(&specifier.local);
}
}
}
}
}
}

View file

@ -1279,8 +1279,10 @@ pub trait Visit<'a>: Sized {
}
fn visit_import_declaration(&mut self, decl: &ImportDeclaration<'a>) {
for specifier in &decl.specifiers {
self.visit_import_declaration_specifier(specifier);
if let Some(specifiers) = &decl.specifiers {
for specifier in specifiers {
self.visit_import_declaration_specifier(specifier);
}
}
// TODO: source
// TODO: assertions

View file

@ -897,8 +897,10 @@ pub trait VisitMut<'a>: Sized {
}
fn visit_import_declaration(&mut self, decl: &mut ImportDeclaration<'a>) {
for specifier in decl.specifiers.iter_mut() {
self.visit_import_declaration_specifier(specifier);
if let Some(specifiers) = &mut decl.specifiers {
for specifier in specifiers.iter_mut() {
self.visit_import_declaration_specifier(specifier);
}
}
// TODO: source
// TODO: assertions

View file

@ -642,72 +642,74 @@ impl<'a, const MINIFY: bool> Gen<MINIFY> for FormalParameters<'a> {
impl<'a, const MINIFY: bool> Gen<MINIFY> for ImportDeclaration<'a> {
fn gen(&self, p: &mut Codegen<{ MINIFY }>, ctx: Context) {
p.print_str(b"import ");
if self.specifiers.is_empty() {
p.print(b'\'');
p.print_str(self.source.value.as_bytes());
p.print(b'\'');
self.assertions.gen(p, ctx);
p.print_semicolon_after_statement();
return;
}
if let Some(specifiers) = &self.specifiers {
if specifiers.is_empty() {
p.print(b'\'');
p.print_str(self.source.value.as_bytes());
p.print(b'\'');
self.assertions.gen(p, ctx);
p.print_semicolon_after_statement();
return;
}
let mut in_block = false;
for (index, specifier) in self.specifiers.iter().enumerate() {
match specifier {
ImportDeclarationSpecifier::ImportDefaultSpecifier(spec) => {
if in_block {
p.print_str(b"},");
in_block = false;
} else if index != 0 {
p.print_comma();
}
spec.local.gen(p, ctx);
}
ImportDeclarationSpecifier::ImportNamespaceSpecifier(spec) => {
if in_block {
p.print_str(b"},");
in_block = false;
} else if index != 0 {
p.print_comma();
}
p.print_str(b"* as ");
spec.local.gen(p, ctx);
}
ImportDeclarationSpecifier::ImportSpecifier(spec) => {
if in_block {
p.print_comma();
} else {
if index != 0 {
let mut in_block = false;
for (index, specifier) in specifiers.iter().enumerate() {
match specifier {
ImportDeclarationSpecifier::ImportDefaultSpecifier(spec) => {
if in_block {
p.print_str(b"},");
in_block = false;
} else if index != 0 {
p.print_comma();
}
in_block = true;
p.print(b'{');
}
let imported_name = match &spec.imported {
ModuleExportName::Identifier(identifier) => {
identifier.gen(p, ctx);
identifier.name.as_bytes()
}
ModuleExportName::StringLiteral(literal) => {
literal.gen(p, ctx);
literal.value.as_bytes()
}
};
let local_name = spec.local.name.as_bytes();
if imported_name != local_name {
p.print_str(b" as ");
spec.local.gen(p, ctx);
}
ImportDeclarationSpecifier::ImportNamespaceSpecifier(spec) => {
if in_block {
p.print_str(b"},");
in_block = false;
} else if index != 0 {
p.print_comma();
}
p.print_str(b"* as ");
spec.local.gen(p, ctx);
}
ImportDeclarationSpecifier::ImportSpecifier(spec) => {
if in_block {
p.print_comma();
} else {
if index != 0 {
p.print_comma();
}
in_block = true;
p.print(b'{');
}
let imported_name = match &spec.imported {
ModuleExportName::Identifier(identifier) => {
identifier.gen(p, ctx);
identifier.name.as_bytes()
}
ModuleExportName::StringLiteral(literal) => {
literal.gen(p, ctx);
literal.value.as_bytes()
}
};
let local_name = spec.local.name.as_bytes();
if imported_name != local_name {
p.print_str(b" as ");
spec.local.gen(p, ctx);
}
}
}
}
if in_block {
p.print(b'}');
}
p.print_str(b" from ");
}
if in_block {
p.print(b'}');
}
p.print_str(b" from ");
self.source.gen(p, ctx);
self.assertions.gen(p, ctx);
p.print_semicolon_after_statement();
@ -747,11 +749,15 @@ impl<'a, const MINIFY: bool> Gen<MINIFY> for ExportNamedDeclaration<'a> {
None => {
p.print(b'{');
if !self.specifiers.is_empty() {
p.print_soft_space();
p.print_list(&self.specifiers, ctx);
p.print_soft_space();
}
p.print(b'}');
if let Some(source) = &self.source {
p.print_soft_space();
p.print_str(b"from");
p.print_soft_space();
source.gen(p, ctx);
}
p.needs_semicolon = true;

View file

@ -566,81 +566,83 @@ impl<'a> Gen for FormalParameters<'a> {
impl<'a> Gen for ImportDeclaration<'a> {
fn gen(&self, p: &mut Formatter) {
p.print_str(b"import ");
if self.specifiers.is_empty() {
p.print(b'\'');
p.print_str(self.source.value.as_bytes());
p.print(b'\'');
self.assertions.gen(p);
p.print_semicolon_after_statement();
return;
}
if let Some(specifiers) = &self.specifiers {
if specifiers.is_empty() {
p.print(b'\'');
p.print_str(self.source.value.as_bytes());
p.print(b'\'');
self.assertions.gen(p);
p.print_semicolon_after_statement();
return;
}
let mut in_block = false;
for (index, specifier) in self.specifiers.iter().enumerate() {
match specifier {
ImportDeclarationSpecifier::ImportDefaultSpecifier(spec) => {
if in_block {
p.print_space();
p.print_str(b"},");
p.print_space();
in_block = false;
} else if index != 0 {
p.print_comma();
p.print_space();
}
spec.local.gen(p);
}
ImportDeclarationSpecifier::ImportNamespaceSpecifier(spec) => {
if in_block {
p.print_space();
p.print_str(b"},");
p.print_space();
in_block = false;
} else if index != 0 {
p.print_comma();
p.print_space();
}
p.print_str(b"* as ");
spec.local.gen(p);
}
ImportDeclarationSpecifier::ImportSpecifier(spec) => {
if in_block {
p.print_comma();
} else {
if index != 0 {
let mut in_block = false;
for (index, specifier) in specifiers.iter().enumerate() {
match specifier {
ImportDeclarationSpecifier::ImportDefaultSpecifier(spec) => {
if in_block {
p.print_space();
p.print_str(b"},");
p.print_space();
in_block = false;
} else if index != 0 {
p.print_comma();
p.print_space();
}
in_block = true;
p.print(b'{');
}
p.print_space();
let imported_name = match &spec.imported {
ModuleExportName::Identifier(identifier) => {
identifier.gen(p);
identifier.name.as_bytes()
}
ModuleExportName::StringLiteral(literal) => {
literal.gen(p);
literal.value.as_bytes()
}
};
let local_name = spec.local.name.as_bytes();
if imported_name != local_name {
p.print_str(b" as ");
spec.local.gen(p);
}
ImportDeclarationSpecifier::ImportNamespaceSpecifier(spec) => {
if in_block {
p.print_space();
p.print_str(b"},");
p.print_space();
in_block = false;
} else if index != 0 {
p.print_comma();
p.print_space();
}
p.print_str(b"* as ");
spec.local.gen(p);
}
ImportDeclarationSpecifier::ImportSpecifier(spec) => {
if in_block {
p.print_comma();
} else {
if index != 0 {
p.print_comma();
p.print_space();
}
in_block = true;
p.print(b'{');
}
p.print_space();
let imported_name = match &spec.imported {
ModuleExportName::Identifier(identifier) => {
identifier.gen(p);
identifier.name.as_bytes()
}
ModuleExportName::StringLiteral(literal) => {
literal.gen(p);
literal.value.as_bytes()
}
};
let local_name = spec.local.name.as_bytes();
if imported_name != local_name {
p.print_str(b" as ");
spec.local.gen(p);
}
}
}
}
if in_block {
p.print_space();
p.print(b'}');
}
p.print_str(b" from ");
}
if in_block {
p.print_space();
p.print(b'}');
}
p.print_str(b" from ");
self.source.gen(p);
self.assertions.gen(p);
p.print_semicolon_after_statement();

View file

@ -369,12 +369,13 @@ fn resolve_to_jest_fn<'a>(
};
if import_decl.source.value == "@jest/globals" {
let original = import_decl.specifiers.iter().find_map(|specifier| match specifier {
ImportDeclarationSpecifier::ImportSpecifier(import_specifier) => {
Some(import_specifier.imported.name())
}
_ => None,
});
let original =
import_decl.specifiers.iter().flatten().find_map(|specifier| match specifier {
ImportDeclarationSpecifier::ImportSpecifier(import_specifier) => {
Some(import_specifier.imported.name())
}
_ => None,
});
return Some(ResolvedJestFn { local: &ident.name, kind: JestFnFrom::Import, original });
}

View file

@ -52,9 +52,9 @@ impl<'a> Parser<'a> {
let specifiers = if self.at(Kind::Str) {
// import "source"
self.ast.new_vec()
None
} else {
self.parse_import_declaration_specifiers()?
Some(self.parse_import_declaration_specifiers()?)
};
let source = self.parse_literal_string()?;
@ -388,7 +388,7 @@ impl<'a> Parser<'a> {
let imported = IdentifierName { span: local.span, name: local.name.clone() };
(ModuleExportName::Identifier(imported), local)
};
Ok(ImportSpecifier { span: self.end_span(specifier_span), imported, local })
Ok(ImportSpecifier { span: self.end_span(specifier_span), imported, local, import_kind })
}
// ModuleExportName :

View file

@ -1643,6 +1643,7 @@ mod import {
.import
.specifiers
.iter()
.flatten()
.filter_map(|the_specifier| {
if let ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) =
the_specifier
@ -1685,6 +1686,7 @@ mod import {
.import
.specifiers
.iter()
.flatten()
.filter_map(|the_specifier| {
if let ImportDeclarationSpecifier::ImportSpecifier(specifier) =
the_specifier

View file

@ -214,29 +214,31 @@ impl ModuleRecordBuilder {
return;
}
let module_request = NameSpan::new(decl.source.value.clone(), decl.source.span);
for specifier in &decl.specifiers {
let (import_name, local_name) = match specifier {
ImportDeclarationSpecifier::ImportSpecifier(specifier) => (
ImportImportName::Name(NameSpan::new(
specifier.imported.name().clone(),
specifier.imported.span(),
)),
NameSpan::new(specifier.local.name.clone(), specifier.local.span),
),
ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => (
ImportImportName::NamespaceObject,
NameSpan::new(specifier.local.name.clone(), specifier.local.span),
),
ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => (
ImportImportName::Default(specifier.span),
NameSpan::new(specifier.local.name.clone(), specifier.local.span),
),
};
self.add_import_entry(ImportEntry {
module_request: module_request.clone(),
import_name,
local_name,
});
if let Some(specifiers) = &decl.specifiers {
for specifier in specifiers {
let (import_name, local_name) = match specifier {
ImportDeclarationSpecifier::ImportSpecifier(specifier) => (
ImportImportName::Name(NameSpan::new(
specifier.imported.name().clone(),
specifier.imported.span(),
)),
NameSpan::new(specifier.local.name.clone(), specifier.local.span),
),
ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => (
ImportImportName::NamespaceObject,
NameSpan::new(specifier.local.name.clone(), specifier.local.span),
),
ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => (
ImportImportName::Default(specifier.span),
NameSpan::new(specifier.local.name.clone(), specifier.local.span),
),
};
self.add_import_entry(ImportEntry {
module_request: module_request.clone(),
import_name,
local_name,
});
}
}
self.add_module_request(&module_request);
}

View file

@ -13,7 +13,7 @@ use oxc_transformer::{TransformOptions, TransformTarget, Transformer};
// or `just watch "run -p oxc_transformer --example transformer"`
fn main() {
let name = env::args().nth(1).unwrap_or_else(|| "test.js".to_string());
let name = env::args().nth(1).unwrap_or_else(|| "test.tsx".to_string());
let path = Path::new(&name);
let source_text = std::fs::read_to_string(path).expect("{name} not found");
let allocator = Allocator::default();
@ -31,7 +31,7 @@ fn main() {
let codegen_options = CodegenOptions;
let printed = Codegen::<false>::new(source_text.len(), codegen_options).build(&ret.program);
println!("Original:\n");
println!("{printed}");
println!("{printed}\n");
let semantic = SemanticBuilder::new(&source_text, source_type).build(&ret.program).semantic;
let (symbols, scopes) = semantic.into_symbol_table_and_scope_tree();

View file

@ -96,6 +96,15 @@ impl<'a> Transformer<'a> {
}
impl<'a> VisitMut<'a> for Transformer<'a> {
fn visit_program(&mut self, program: &mut Program<'a>) {
for directive in program.directives.iter_mut() {
self.visit_directive(directive);
}
self.typescript.as_mut().map(|t| t.transform_program(program));
self.visit_statements(&mut program.body);
}
fn visit_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>) {
for stmt in stmts.iter_mut() {
self.visit_statement(stmt);

View file

@ -1,5 +1,5 @@
use oxc_ast::ast::*;
use oxc_ast::AstBuilder;
use oxc_ast::{ast::*, AstBuilder};
use oxc_span::Span;
use std::rc::Rc;
@ -9,12 +9,12 @@ use std::rc::Rc;
/// * <https://babeljs.io/docs/babel-plugin-transform-typescript>
/// * <https://github.com/babel/babel/tree/main/packages/babel-plugin-transform-typescript>
pub struct TypeScript<'a> {
_ast: Rc<AstBuilder<'a>>,
ast: Rc<AstBuilder<'a>>,
}
impl<'a> TypeScript<'a> {
pub fn new(_ast: Rc<AstBuilder<'a>>) -> Self {
Self { _ast }
pub fn new(ast: Rc<AstBuilder<'a>>) -> Self {
Self { ast }
}
#[allow(clippy::unused_self)]
@ -23,4 +23,73 @@ impl<'a> TypeScript<'a> {
params.items.remove(0);
}
}
/// * Remove the top level import / export statements that are types
/// * Adds `export {}` if all import / export statements are removed, this is used to tell
/// downstream tools that this file is in ESM.
pub fn transform_program(&self, program: &mut Program<'a>) {
let mut needs_explicit_esm = false;
for stmt in program.body.iter_mut() {
if let Statement::ModuleDeclaration(module_decl) = stmt {
needs_explicit_esm = true;
match &mut **module_decl {
ModuleDeclaration::ExportNamedDeclaration(decl) => {
decl.specifiers.retain(|specifier| specifier.export_kind.is_value());
}
ModuleDeclaration::ImportDeclaration(decl) => {
if let Some(specifiers) = &mut decl.specifiers {
specifiers.retain(|specifier| match specifier {
ImportDeclarationSpecifier::ImportSpecifier(s) => {
s.import_kind.is_value()
}
_ => false,
});
}
}
_ => {}
}
}
}
program.body.retain(|stmt| match stmt {
Statement::ModuleDeclaration(module_decl) => match &**module_decl {
ModuleDeclaration::ImportDeclaration(decl) => {
if decl.import_kind.is_type() {
return false;
}
if decl.specifiers.as_ref().is_some_and(|specifiers| specifiers.is_empty()) {
// TODO: verbatim_module_syntax
return false;
}
true
}
ModuleDeclaration::ExportNamedDeclaration(decl) => {
if decl.export_kind.is_type() {
return false;
}
if decl.declaration.is_none() && decl.specifiers.is_empty() {
return false;
}
true
}
_ => true,
},
_ => true,
});
if needs_explicit_esm
&& !program.body.iter().any(|s| matches!(s, Statement::ModuleDeclaration(_)))
{
let empty_export = self.ast.export_named_declaration(
Span::default(),
None,
self.ast.new_vec(),
None,
ImportOrExportKind::Value,
);
let export_decl = ModuleDeclaration::ExportNamedDeclaration(empty_export);
program.body.push(self.ast.module_declaration(export_decl));
}
}
}

View file

@ -1,4 +1,4 @@
Passed: 164/1083
Passed: 173/1083
# All Passed:
* babel-plugin-transform-numeric-separator
@ -705,7 +705,7 @@ Passed: 164/1083
* unicode-regex/negated-set/input.js
* unicode-regex/slash/input.js
# babel-plugin-transform-typescript (73/181)
# babel-plugin-transform-typescript (82/181)
* class/abstract-class-decorated/input.ts
* class/abstract-class-decorated-method/input.ts
* class/abstract-class-decorated-parameter/input.ts
@ -742,33 +742,26 @@ Passed: 164/1083
* enum/ts5.0-const-foldable/input.ts
* exports/declared-types/input.ts
* exports/export-const-enums/input.ts
* exports/export-type/input.ts
* exports/export-type-from/input.ts
* exports/export-type-star-from/input.ts
* exports/export=-to-cjs/input.ts
* exports/imported-types/input.ts
* exports/imported-types-only-remove-type-imports/input.ts
* exports/issue-9916-3/input.ts
* exports/type-only-export-specifier-1/input.ts
* exports/type-only-export-specifier-2/input.ts
* exports/type-only-export-specifier-3/input.ts
* function/overloads-exports/input.mjs
* imports/elide-injected/input.ts
* imports/elide-no-import-specifiers/input.ts
* imports/elide-preact/input.ts
* imports/elide-react/input.ts
* imports/elide-type-referenced-in-imports-equal-no/input.ts
* imports/elide-typeof/input.ts
* imports/elision/input.ts
* imports/elision-export-type/input.ts
* imports/elision-locations/input.ts
* imports/elision-qualifiedname/input.ts
* imports/elision-rename/input.ts
* imports/enum-id/input.ts
* imports/enum-value/input.ts
* imports/import-named-type/input.ts
* imports/import-named-type-default-and-named/input.ts
* imports/import-removed-exceptions/input.ts
* imports/import-type/input.ts
* imports/import-type-func-with-duplicate-name/input.ts
* imports/import-type-not-removed/input.ts
* imports/import=-declaration/input.ts
* imports/import=-module-to-cjs/input.ts
* imports/only-remove-type-imports/input.ts
@ -776,8 +769,6 @@ Passed: 164/1083
* imports/property-signature/input.ts
* imports/type-only-export-specifier-1/input.ts
* imports/type-only-export-specifier-2/input.ts
* imports/type-only-import-specifier-1/input.ts
* imports/type-only-import-specifier-2/input.ts
* imports/type-only-import-specifier-3/input.ts
* imports/type-only-import-specifier-4/input.ts
* namespace/alias/input.ts