Skip to content

Commit

Permalink
fix(react): 🐛 import undefined jsx-runtime with script source type
Browse files Browse the repository at this point in the history
  • Loading branch information
JSerFeng committed Mar 23, 2023
1 parent e77bc0a commit bb8e97a
Show file tree
Hide file tree
Showing 27 changed files with 442 additions and 93 deletions.
240 changes: 150 additions & 90 deletions crates/swc_ecma_transforms_react/src/jsx/mod.rs
Expand Up @@ -18,7 +18,9 @@ use swc_common::{
use swc_config::merge::Merge;
use swc_ecma_ast::*;
use swc_ecma_parser::{parse_file_as_expr, Syntax};
use swc_ecma_utils::{drop_span, prepend_stmt, private_ident, quote_ident, undefined, ExprFactory};
use swc_ecma_utils::{
drop_span, prepend_stmt, private_ident, quote_ident, undefined, ExprFactory, StmtLike,
};
use swc_ecma_visit::{as_folder, noop_visit_mut_type, Fold, VisitMut, VisitMutWith};

use self::static_check::should_use_create_element;
Expand Down Expand Up @@ -376,6 +378,62 @@ impl<C> Jsx<C>
where
C: Comments,
{
fn inject_runtime<T, F>(&mut self, body: &mut Vec<T>, inject: F)
where
T: StmtLike,
// Fn(Vec<(local, imported)>, src, body)
F: Fn(Vec<(Ident, Ident)>, &str, &mut Vec<T>),
{
if self.runtime == Runtime::Automatic {
if let Some(local) = self.import_create_element.take() {
inject(
vec![(local, quote_ident!("createElement"))],
&self.import_source,
body,
);
}

let imports = self.import_jsx.take();
let imports = if self.development {
imports
.map(|local| (local, quote_ident!("jsxDEV")))
.into_iter()
.chain(
self.import_fragment
.take()
.map(|local| (local, quote_ident!("Fragment"))),
)
.collect::<Vec<_>>()
} else {
imports
.map(|local| (local, quote_ident!("jsx")))
.into_iter()
.chain(
self.import_jsxs
.take()
.map(|local| (local, quote_ident!("jsxs"))),
)
.chain(
self.import_fragment
.take()
.map(|local| (local, quote_ident!("Fragment"))),
)
.collect::<Vec<_>>()
};

if !imports.is_empty() {
let jsx_runtime = if self.development {
"jsx-dev-runtime"
} else {
"jsx-runtime"
};

let value = format!("{}/{}", self.import_source, jsx_runtime);
inject(imports, &value, body)
}
}
}

fn jsx_frag_to_expr(&mut self, el: JSXFragment) -> Expr {
let span = el.span();

Expand Down Expand Up @@ -477,7 +535,7 @@ where

/// # Automatic
///
///
/// <div></div> => jsx('div', null);
///
/// # Classic
///
Expand Down Expand Up @@ -975,113 +1033,115 @@ where
module.visit_mut_children_with(self);

if self.runtime == Runtime::Automatic {
if let Some(local) = self.import_create_element.take() {
let specifier = ImportSpecifier::Named(ImportNamedSpecifier {
span: DUMMY_SP,
local,
imported: Some(ModuleExportName::Ident(Ident::new(
"createElement".into(),
DUMMY_SP,
))),
is_type_only: false,
});
prepend_stmt(
&mut module.body,
ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl {
span: DUMMY_SP,
specifiers: vec![specifier],
src: Str {
span: DUMMY_SP,
raw: None,
value: self.import_source.clone(),
}
.into(),
type_only: Default::default(),
asserts: Default::default(),
})),
);
}

let imports = self.import_jsx.take();
let imports = if self.development {
imports
.map(|local| ImportNamedSpecifier {
span: DUMMY_SP,
local,
imported: Some(ModuleExportName::Ident(quote_ident!("jsxDEV"))),
is_type_only: false,
})
self.inject_runtime(&mut module.body, |imports, src, stmts| {
let specifiers = imports
.into_iter()
.chain(
self.import_fragment
.take()
.map(|local| ImportNamedSpecifier {
span: DUMMY_SP,
local,
imported: Some(ModuleExportName::Ident(quote_ident!("Fragment"))),
is_type_only: false,
}),
)
.map(ImportSpecifier::Named)
.collect::<Vec<_>>()
} else {
imports
.map(|local| ImportNamedSpecifier {
span: DUMMY_SP,
local,
imported: Some(ModuleExportName::Ident(quote_ident!("jsx"))),
is_type_only: false,
.map(|(local, imported)| {
ImportSpecifier::Named(ImportNamedSpecifier {
span: DUMMY_SP,
local,
imported: Some(ModuleExportName::Ident(imported)),
is_type_only: false,
})
})
.into_iter()
.chain(self.import_jsxs.take().map(|local| ImportNamedSpecifier {
span: DUMMY_SP,
local,
imported: Some(ModuleExportName::Ident(quote_ident!("jsxs"))),
is_type_only: false,
}))
.chain(
self.import_fragment
.take()
.map(|local| ImportNamedSpecifier {
span: DUMMY_SP,
local,
imported: Some(ModuleExportName::Ident(quote_ident!("Fragment"))),
is_type_only: false,
}),
)
.map(ImportSpecifier::Named)
.collect::<Vec<_>>()
};

if !imports.is_empty() {
let jsx_runtime = if self.development {
"jsx-dev-runtime"
} else {
"jsx-runtime"
};

let value = format!("{}/{}", self.import_source, jsx_runtime);
.collect();

prepend_stmt(
&mut module.body,
stmts,
ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl {
span: DUMMY_SP,
specifiers: imports,
specifiers,
src: Str {
span: DUMMY_SP,
raw: None,
value: value.into(),
value: src.into(),
}
.into(),
type_only: Default::default(),
asserts: Default::default(),
})),
);
)
});
}
}

fn visit_mut_script(&mut self, script: &mut Script) {
self.parse_directives(script.span);

for item in &script.body {
let span = item.span();
if self.parse_directives(span) {
break;
}
}

script.visit_mut_children_with(self);

if self.runtime == Runtime::Automatic {
self.inject_runtime(&mut script.body, |imports, src, stmts| {
prepend_stmt(stmts, add_require(imports, src))
});
}
}
}

// const { createElement } = require('react')
// const { jsx: jsx } = require('react/jsx-runtime')
fn add_require(imports: Vec<(Ident, Ident)>, src: &str) -> Stmt {
Stmt::Decl(Decl::Var(Box::new(VarDecl {
span: DUMMY_SP,
kind: VarDeclKind::Const,
declare: false,
decls: vec![VarDeclarator {
span: DUMMY_SP,
name: Pat::Object(ObjectPat {
span: DUMMY_SP,
props: imports
.into_iter()
.map(|(local, imported)| {
if imported.sym != local.sym {
ObjectPatProp::KeyValue(KeyValuePatProp {
key: PropName::Ident(imported),
value: Box::new(Pat::Ident(BindingIdent {
id: local,
type_ann: None,
})),
})
} else {
ObjectPatProp::Assign(AssignPatProp {
span: DUMMY_SP,
key: local,
value: None,
})
}
})
.collect(),
optional: false,
type_ann: None,
}),
// require('react')
init: Some(Box::new(Expr::Call(CallExpr {
span: DUMMY_SP,
callee: Callee::Expr(Box::new(Expr::Ident(Ident {
span: DUMMY_SP,
sym: js_word!("require"),
optional: false,
}))),
args: vec![ExprOrSpread {
spread: None,
expr: Box::new(Expr::Lit(Lit::Str(Str {
span: DUMMY_SP,
value: src.into(),
raw: None,
}))),
}],
type_args: None,
}))),
definite: false,
}],
})))
}

impl<C> Jsx<C>
where
C: Comments,
Expand Down
81 changes: 78 additions & 3 deletions crates/swc_ecma_transforms_react/src/jsx/tests.rs
@@ -1,16 +1,22 @@
#![allow(dead_code)]

use std::path::PathBuf;
use std::{
fs,
path::{Path, PathBuf},
};

use swc_common::{chain, Mark};
use swc_ecma_parser::EsConfig;
use swc_ecma_transforms_base::resolver;
use swc_ecma_codegen::{Config, Emitter};
use swc_ecma_parser::{EsConfig, Parser, StringInput};
use swc_ecma_transforms_base::{fixer::fixer, hygiene, resolver};
use swc_ecma_transforms_compat::{
es2015::{arrow, classes},
es3::property_literals,
};
use swc_ecma_transforms_module::common_js::common_js;
use swc_ecma_transforms_testing::{parse_options, test, test_fixture, FixtureTestConfig, Tester};
use swc_ecma_visit::FoldWith;
use testing::NormalizedOutput;

use super::*;
use crate::{display_name, pure_annotations, react};
Expand Down Expand Up @@ -1483,3 +1489,72 @@ fn integration(input: PathBuf) {
},
);
}

#[testing::fixture("tests/script/**/input.js")]
fn script(input: PathBuf) {
let output = input.with_file_name("output.js");

let options = parse_options(input.parent().unwrap());

let input = fs::read_to_string(&input).unwrap();

test_script(&input, &output, options);
}

fn test_script(src: &str, output: &Path, options: Options) {
Tester::run(|tester| {
let fm = tester
.cm
.new_source_file(FileName::Real("input.js".into()), src.into());

let syntax = Syntax::Es(EsConfig {
jsx: true,
..Default::default()
});

let mut parser = Parser::new(syntax, StringInput::from(&*fm), Some(&tester.comments));

let script = parser.parse_script().unwrap();

let top_level_mark = Mark::new();

let script = script.fold_with(&mut chain!(
resolver(Mark::new(), top_level_mark, false),
react(
tester.cm.clone(),
Some(&tester.comments),
options,
top_level_mark,
),
hygiene::hygiene(),
fixer(Some(&tester.comments))
));

let mut buf = vec![];

let mut emitter = Emitter {
cfg: Config {
target: Default::default(),
ascii_only: true,
minify: false,
omit_last_semi: true,
},
cm: tester.cm.clone(),
wr: Box::new(swc_ecma_codegen::text_writer::JsWriter::new(
tester.cm.clone(),
"\n",
&mut buf,
None,
)),
comments: Some(&tester.comments),
};

// println!("Emitting: {:?}", module);
emitter.emit_script(&script).unwrap();

let s = String::from_utf8_lossy(&buf).to_string();
assert!(NormalizedOutput::new_raw(s).compare_to_file(output).is_ok());

Ok(())
})
}
@@ -0,0 +1,8 @@
const App = (
<div>
<div />
<>
<div key={1}>hoge</div>
</>
</div>
);
@@ -0,0 +1 @@
{ "runtime": "automatic", "development": true }

0 comments on commit bb8e97a

Please sign in to comment.