feat(es/react): Support script within automatic runtime (#7126)

**Description:**

Inject `const { jsx: _jsx} = require("react/jsx-runtime")` when sourceType is `script`

**Related issue:**

 - Closes https://github.com/swc-project/swc/issues/7119.
This commit is contained in:
Fy 2023-03-27 14:39:37 +08:00 committed by GitHub
parent 47f15f9f28
commit 05a2815e6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 480 additions and 104 deletions

View File

@ -749,7 +749,13 @@ impl Options {
custom_before_pass(&program),
// handle jsx
Optional::new(
react::react(cm.clone(), comments, transform.react, top_level_mark),
react::react(
cm.clone(),
comments,
transform.react,
top_level_mark,
unresolved_mark
),
syntax.jsx()
),
pass,

View File

@ -146,6 +146,7 @@ impl Load for Loader {
None,
Default::default(),
top_level_mark,
unresolved_mark,
))
.fold_with(&mut inject_helpers(unresolved_mark))
});

View File

@ -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;
@ -187,6 +189,7 @@ pub fn jsx<C>(
comments: Option<C>,
options: Options,
top_level_mark: Mark,
unresolved_mark: Mark,
) -> impl Fold + VisitMut
where
C: Comments,
@ -194,6 +197,7 @@ where
as_folder(Jsx {
cm: cm.clone(),
top_level_mark,
unresolved_mark,
runtime: options.runtime.unwrap_or_default(),
import_source: options
.import_source
@ -232,6 +236,7 @@ where
cm: Lrc<SourceMap>,
top_level_mark: Mark,
unresolved_mark: Mark,
runtime: Runtime,
/// For automatic runtime.
@ -376,6 +381,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();
@ -477,7 +538,7 @@ where
/// # Automatic
///
///
/// <div></div> => jsx('div', null);
///
/// # Classic
///
@ -975,111 +1036,114 @@ 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,
});
self.inject_runtime(&mut module.body, |imports, src, stmts| {
let specifiers = imports
.into_iter()
.map(|(local, imported)| {
ImportSpecifier::Named(ImportNamedSpecifier {
span: DUMMY_SP,
local,
imported: Some(ModuleExportName::Ident(imported)),
is_type_only: false,
})
})
.collect();
prepend_stmt(
&mut module.body,
stmts,
ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl {
span: DUMMY_SP,
specifiers: vec![specifier],
specifiers,
src: Str {
span: DUMMY_SP,
raw: None,
value: self.import_source.clone(),
value: src.into(),
}
.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,
})
.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,
})
.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);
prepend_stmt(
&mut module.body,
ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl {
span: DUMMY_SP,
specifiers: imports,
src: Str {
span: DUMMY_SP,
raw: None,
value: value.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 {
let mark = self.unresolved_mark;
self.inject_runtime(&mut script.body, |imports, src, stmts| {
prepend_stmt(stmts, add_require(imports, src, mark))
});
}
}
}
// const { createElement } = require('react')
// const { jsx: jsx } = require('react/jsx-runtime')
fn add_require(imports: Vec<(Ident, Ident)>, src: &str, unresolved_mark: Mark) -> 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.apply_mark(unresolved_mark),
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>

View File

@ -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};
@ -24,7 +30,8 @@ fn tr(t: &mut Tester, options: Options, top_level_mark: Mark) -> impl Fold {
t.cm.clone(),
Some(t.comments.clone()),
options,
top_level_mark
top_level_mark,
unresolved_mark
),
display_name(),
classes(Some(t.comments.clone()), Default::default()),
@ -71,7 +78,8 @@ fn fixture_tr(t: &mut Tester, mut options: FixtureOptions) -> impl Fold {
t.cm.clone(),
Some(t.comments.clone()),
options.options,
top_level_mark
top_level_mark,
unresolved_mark,
),
display_name(),
pure_annotations(Some(t.comments.clone()))
@ -94,7 +102,8 @@ fn integration_tr(t: &mut Tester, mut options: FixtureOptions) -> impl Fold {
t.cm.clone(),
Some(t.comments.clone()),
options.options,
top_level_mark
top_level_mark,
unresolved_mark
),
display_name(),
)
@ -1338,6 +1347,7 @@ test!(
}),
|t| {
let top_level_mark = Mark::fresh(Mark::root());
let unresolved_mark = Mark::fresh(Mark::root());
chain!(
classes(Some(t.comments.clone()), Default::default()),
@ -1345,7 +1355,8 @@ test!(
t.cm.clone(),
Some(t.comments.clone()),
Default::default(),
top_level_mark
top_level_mark,
unresolved_mark
)
)
},
@ -1419,7 +1430,8 @@ test!(
t.cm.clone(),
Some(t.comments.clone()),
Default::default(),
top_level_mark
top_level_mark,
unresolved_mark
)
)
},
@ -1483,3 +1495,74 @@ 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 unresolved_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,
unresolved_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(())
})
}

View File

@ -37,6 +37,7 @@ pub fn react<C>(
comments: Option<C>,
mut options: Options,
top_level_mark: Mark,
unresolved_mark: Mark,
) -> impl Fold + VisitMut
where
C: Comments + Clone,
@ -56,7 +57,13 @@ where
comments.clone(),
top_level_mark
),
jsx(cm, comments.clone(), options, top_level_mark),
jsx(
cm,
comments.clone(),
options,
top_level_mark,
unresolved_mark
),
display_name(),
pure_annotations(comments),
)

View File

@ -74,6 +74,7 @@ fn run_test(input: &str, expected: &str) {
Some(&actual_comments),
Default::default(),
top_level_mark,
unresolved_mark,
));
let actual_src = emit(actual_sm, actual_comments, &actual);

View File

@ -0,0 +1,8 @@
const App = (
<div>
<div />
<>
<div key={1}>hoge</div>
</>
</div>
);

View File

@ -0,0 +1 @@
{ "runtime": "automatic", "development": true }

View File

@ -0,0 +1,23 @@
const { jsxDEV: _jsxDEV , Fragment: _Fragment } = require("react/jsx-dev-runtime");
const App = /*#__PURE__*/ _jsxDEV("div", {
children: [
/*#__PURE__*/ _jsxDEV("div", {}, void 0, false, {
fileName: "input.js",
lineNumber: 3,
columnNumber: 9
}, this),
/*#__PURE__*/ _jsxDEV(_Fragment, {
children: /*#__PURE__*/ _jsxDEV("div", {
children: "hoge"
}, 1, false, {
fileName: "input.js",
lineNumber: 5,
columnNumber: 13
}, this)
}, void 0, false)
]
}, void 0, true, {
fileName: "input.js",
lineNumber: 2,
columnNumber: 5
}, this);

View File

@ -0,0 +1,8 @@
const App = (
<div>
<div />
<>
<div key={1}>hoge</div>
</>
</div>
);

View File

@ -0,0 +1 @@
{ "runtime": "automatic" }

View File

@ -0,0 +1,11 @@
const { jsx: _jsx , jsxs: _jsxs , Fragment: _Fragment } = require("react/jsx-runtime");
const App = /*#__PURE__*/ _jsxs("div", {
children: [
/*#__PURE__*/ _jsx("div", {}),
/*#__PURE__*/ _jsx(_Fragment, {
children: /*#__PURE__*/ _jsx("div", {
children: "hoge"
}, 1)
})
]
});

View File

@ -0,0 +1,6 @@
var x = (
<>
<div>hoge</div>
<div>fuga</div>
</>
);

View File

@ -0,0 +1 @@
{ "runtime": "automatic", "development": true }

View File

@ -0,0 +1,19 @@
const { jsxDEV: _jsxDEV , Fragment: _Fragment } = require("react/jsx-dev-runtime");
var x = /*#__PURE__*/ _jsxDEV(_Fragment, {
children: [
/*#__PURE__*/ _jsxDEV("div", {
children: "hoge"
}, void 0, false, {
fileName: "input.js",
lineNumber: 3,
columnNumber: 9
}, this),
/*#__PURE__*/ _jsxDEV("div", {
children: "fuga"
}, void 0, false, {
fileName: "input.js",
lineNumber: 4,
columnNumber: 9
}, this)
]
}, void 0, true);

View File

@ -0,0 +1,6 @@
const App = (
<>
<div>hoge</div>
<div>fuga</div>
</>
);

View File

@ -0,0 +1 @@
{ "runtime": "automatic", "development": true }

View File

@ -0,0 +1,19 @@
const { jsxDEV: _jsxDEV , Fragment: _Fragment } = require("react/jsx-dev-runtime");
const App = /*#__PURE__*/ _jsxDEV(_Fragment, {
children: [
/*#__PURE__*/ _jsxDEV("div", {
children: "hoge"
}, void 0, false, {
fileName: "input.js",
lineNumber: 3,
columnNumber: 9
}, this),
/*#__PURE__*/ _jsxDEV("div", {
children: "fuga"
}, void 0, false, {
fileName: "input.js",
lineNumber: 4,
columnNumber: 9
}, this)
]
}, void 0, true);

View File

@ -0,0 +1,9 @@
/**@jsxRuntime automatic */
const App = (
<div>
<div />
<>
<div>hoge</div>
</>
</div>
);

View File

@ -0,0 +1 @@
{ "development": true }

View File

@ -0,0 +1,23 @@
/**@jsxRuntime automatic */ const { jsxDEV: _jsxDEV , Fragment: _Fragment } = require("react/jsx-dev-runtime");
const App = /*#__PURE__*/ _jsxDEV("div", {
children: [
/*#__PURE__*/ _jsxDEV("div", {}, void 0, false, {
fileName: "input.js",
lineNumber: 4,
columnNumber: 9
}, this),
/*#__PURE__*/ _jsxDEV(_Fragment, {
children: /*#__PURE__*/ _jsxDEV("div", {
children: "hoge"
}, void 0, false, {
fileName: "input.js",
lineNumber: 6,
columnNumber: 13
}, this)
}, void 0, false)
]
}, void 0, true, {
fileName: "input.js",
lineNumber: 3,
columnNumber: 5
}, this);

View File

@ -0,0 +1,7 @@
// https://github.com/babel/babel/issues/12522
require("react-app-polyfill/ie11");
require("react-app-polyfill/stable");
const ReactDOM = require("react-dom");
ReactDOM.render(<p>Hello, World!</p>, document.getElementById("root"));

View File

@ -0,0 +1,8 @@
// https://github.com/babel/babel/issues/12522
const { jsx: _jsx } = require("react/jsx-runtime");
require("react-app-polyfill/ie11");
require("react-app-polyfill/stable");
const ReactDOM = require("react-dom");
ReactDOM.render(/*#__PURE__*/ _jsx("p", {
children: "Hello, World!"
}), document.getElementById("root"));

View File

@ -0,0 +1,10 @@
var x = (
<>
<div>
<div key="1" />
<div key="2" meow="wolf" />
<div key="3" />
<div {...props} key="4" />
</div>
</>
);

View File

@ -0,0 +1,17 @@
const { jsx: _jsx , jsxs: _jsxs , Fragment: _Fragment } = require("react/jsx-runtime");
const { createElement: _createElement } = require("react");
var x = /*#__PURE__*/ _jsx(_Fragment, {
children: /*#__PURE__*/ _jsxs("div", {
children: [
/*#__PURE__*/ _jsx("div", {}, "1"),
/*#__PURE__*/ _jsx("div", {
meow: "wolf"
}, "2"),
/*#__PURE__*/ _jsx("div", {}, "3"),
/*#__PURE__*/ _createElement("div", {
...props,
key: "4"
})
]
})
});

View File

@ -0,0 +1,15 @@
const Bar = () => {
const Foo = () => {
const Component = ({ thing, ..._react }) => {
if (!thing) {
var _react2 = "something useless";
var b = _react3();
var c = _react5();
var jsx = 1;
var _jsx = 2;
return <div />;
}
return <span />;
};
};
};

View File

@ -0,0 +1,16 @@
const { jsx: _jsx } = require("react/jsx-runtime");
const Bar = ()=>{
const Foo = ()=>{
const Component = ({ thing , ..._react })=>{
if (!thing) {
var _react2 = "something useless";
var b = _react3();
var c = _react5();
var jsx = 1;
var _jsx1 = 2;
return /*#__PURE__*/ _jsx("div", {});
}
return /*#__PURE__*/ _jsx("span", {});
};
};
};

View File

@ -0,0 +1 @@
{ "runtime": "automatic" }