Vendor things

This commit is contained in:
John Doty 2024-03-08 11:03:01 -08:00
parent 5deceec006
commit 977e3c17e5
19434 changed files with 10682014 additions and 0 deletions

View file

@ -0,0 +1,210 @@
use std::ops::DerefMut;
use swc_atoms::js_word;
use swc_common::DUMMY_SP;
use swc_ecma_ast::*;
use swc_ecma_utils::quote_ident;
use swc_ecma_visit::{as_folder, noop_visit_mut_type, Fold, VisitMut, VisitMutWith};
#[cfg(test)]
mod tests;
/// `@babel/plugin-transform-react-display-name`
///
/// Add displayName to React.createClass calls
pub fn display_name() -> impl Fold + VisitMut {
as_folder(DisplayName)
}
struct DisplayName;
impl VisitMut for DisplayName {
noop_visit_mut_type!();
fn visit_mut_assign_expr(&mut self, expr: &mut AssignExpr) {
expr.visit_mut_children_with(self);
if expr.op != op!("=") {
return;
}
if let Some(
Expr::Member(MemberExpr {
prop: MemberProp::Ident(prop),
..
})
| Expr::SuperProp(SuperPropExpr {
prop: SuperProp::Ident(prop),
..
}),
) = expr.left.as_expr()
{
return expr.right.visit_mut_with(&mut Folder {
name: Some(Box::new(Expr::Lit(Lit::Str(Str {
span: prop.span,
raw: None,
value: prop.sym.clone(),
})))),
});
};
if let Some(ident) = expr.left.as_ident() {
expr.right.visit_mut_with(&mut Folder {
name: Some(Box::new(Expr::Lit(Lit::Str(Str {
span: ident.span,
raw: None,
value: ident.sym.clone(),
})))),
});
}
}
fn visit_mut_module_decl(&mut self, decl: &mut ModuleDecl) {
decl.visit_mut_children_with(self);
if let ModuleDecl::ExportDefaultExpr(e) = decl {
e.visit_mut_with(&mut Folder {
name: Some(Box::new(Expr::Lit(Lit::Str(Str {
span: DUMMY_SP,
raw: None,
value: "input".into(),
})))),
});
}
}
fn visit_mut_prop(&mut self, prop: &mut Prop) {
prop.visit_mut_children_with(self);
if let Prop::KeyValue(KeyValueProp { key, value }) = prop {
value.visit_mut_with(&mut Folder {
name: Some(match key {
PropName::Ident(ref i) => Box::new(Expr::Lit(Lit::Str(Str {
span: i.span,
raw: None,
value: i.sym.clone(),
}))),
PropName::Str(ref s) => Box::new(Expr::Lit(Lit::Str(s.clone()))),
PropName::Num(ref n) => Box::new(Expr::Lit(Lit::Num(n.clone()))),
PropName::BigInt(ref b) => Box::new(Expr::Lit(Lit::BigInt(b.clone()))),
PropName::Computed(ref c) => c.expr.clone(),
}),
});
}
}
fn visit_mut_var_declarator(&mut self, decl: &mut VarDeclarator) {
if let Pat::Ident(ref ident) = decl.name {
decl.init.visit_mut_with(&mut Folder {
name: Some(Box::new(Expr::Lit(Lit::Str(Str {
span: ident.id.span,
value: ident.id.sym.clone(),
raw: None,
})))),
});
}
}
}
struct Folder {
name: Option<Box<Expr>>,
}
impl VisitMut for Folder {
noop_visit_mut_type!();
/// Don't recurse into array.
fn visit_mut_array_lit(&mut self, _: &mut ArrayLit) {}
fn visit_mut_call_expr(&mut self, expr: &mut CallExpr) {
expr.visit_mut_children_with(self);
if is_create_class_call(expr) {
let name = match self.name.take() {
Some(name) => name,
None => return,
};
add_display_name(expr, name)
}
}
/// Don't recurse into object.
fn visit_mut_object_lit(&mut self, _: &mut ObjectLit) {}
}
fn is_create_class_call(call: &CallExpr) -> bool {
let callee = match &call.callee {
Callee::Super(_) | Callee::Import(_) => return false,
Callee::Expr(callee) => &**callee,
};
match callee {
Expr::Member(MemberExpr {
obj,
prop:
MemberProp::Ident(Ident {
sym: js_word!("createClass"),
..
}),
..
}) => {
if let Expr::Ident(Ident {
sym: js_word!("React"),
..
}) = &**obj
{
return true;
}
}
Expr::Ident(Ident {
sym: js_word!("createReactClass"),
..
}) => return true,
_ => {}
}
false
}
fn add_display_name(call: &mut CallExpr, name: Box<Expr>) {
let props = match call.args.first_mut() {
Some(&mut ExprOrSpread { ref mut expr, .. }) => match expr.deref_mut() {
Expr::Object(ObjectLit { ref mut props, .. }) => props,
_ => return,
},
_ => return,
};
for prop in &*props {
if is_key_display_name(prop) {
return;
}
}
props.push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
key: PropName::Ident(quote_ident!("displayName")),
value: name,
}))));
}
fn is_key_display_name(prop: &PropOrSpread) -> bool {
match *prop {
PropOrSpread::Prop(ref prop) => match **prop {
Prop::Shorthand(ref i) => i.sym == js_word!("displayName"),
Prop::Method(MethodProp { ref key, .. })
| Prop::Getter(GetterProp { ref key, .. })
| Prop::Setter(SetterProp { ref key, .. })
| Prop::KeyValue(KeyValueProp { ref key, .. }) => match *key {
PropName::Ident(ref i) => i.sym == js_word!("displayName"),
PropName::Str(ref s) => s.value == js_word!("displayName"),
PropName::Num(..) => false,
PropName::BigInt(..) => false,
PropName::Computed(..) => false,
},
Prop::Assign(..) => unreachable!("invalid syntax"),
},
_ => false,
// TODO(kdy1): maybe.. handle spread
}
}

View file

@ -0,0 +1,102 @@
use swc_ecma_transforms_testing::test;
use super::*;
fn tr() -> impl Fold {
display_name()
}
test!(
::swc_ecma_parser::Syntax::default(),
|_| tr(),
assignment_expression,
r#"
foo = createReactClass({});
bar = React.createClass({});
"#,
r#"
foo = createReactClass({
displayName: "foo"
});
bar = React.createClass({
displayName: "bar"
});
"#
);
test!(
::swc_ecma_parser::Syntax::default(),
|_| tr(),
nested,
r#"
var foo = qux(createReactClass({}));
var bar = qux(React.createClass({}));
"#,
r#"
var foo = qux(createReactClass({
displayName: "foo"
}));
var bar = qux(React.createClass({
displayName: "bar"
}));
"#
);
test!(
::swc_ecma_parser::Syntax::default(),
|_| tr(),
object_property,
r#"
({
foo: createReactClass({})
});
({
bar: React.createClass({})
});
"#,
r#"
({
foo: createReactClass({
displayName: "foo"
})
});
({
bar: React.createClass({
displayName: "bar"
})
});"#
);
test!(
::swc_ecma_parser::Syntax::default(),
|_| tr(),
variable_declarator,
r#"
var foo = createReactClass({});
var bar = React.createClass({});
"#,
r#"
var foo = createReactClass({
displayName: "foo"
});
var bar = React.createClass({
displayName: "bar"
});
"#
);
test!(
::swc_ecma_parser::Syntax::default(),
|_| tr(),
assignment_expression_with_member,
r#"
foo.x = createReactClass({});
class A extends B { render() { super.x = React.createClass({}) } };
"#,
r#"
foo.x = createReactClass({
displayName: "x"
});
class A extends B { render() { super.x = React.createClass({ displayName: "x" }) } };
"#
);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,30 @@
use swc_atoms::js_word;
use swc_ecma_ast::*;
/// We want to use React.createElement, even in the case of
/// jsx, for <div {...props} key={key} /> to distinguish it
/// from <div key={key} {...props} />. This is an intermediary
/// step while we deprecate key spread from props. Afterwards,
/// we will stop using createElement in the transform.
pub(super) fn should_use_create_element(attrs: &[JSXAttrOrSpread]) -> bool {
let mut seen_prop_spread = false;
for attr in attrs {
if seen_prop_spread
&& match attr {
JSXAttrOrSpread::JSXAttr(attr) => match &attr.name {
JSXAttrName::Ident(i) => i.sym == js_word!("key"),
JSXAttrName::JSXNamespacedName(_) => false,
},
_ => false,
}
{
return true;
}
if let JSXAttrOrSpread::SpreadElement(_) = attr {
seen_prop_spread = true;
}
}
false
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,47 @@
use swc_common::DUMMY_SP;
use swc_ecma_ast::*;
use swc_ecma_transforms_base::perf::Parallel;
use swc_ecma_utils::quote_ident;
use swc_ecma_visit::{as_folder, noop_visit_mut_type, Fold, VisitMut};
#[cfg(test)]
mod tests;
/// `@babel/plugin-transform-react-jsx-self`
///
/// Add a __self prop to all JSX Elements
pub fn jsx_self(dev: bool) -> impl Fold + VisitMut {
as_folder(JsxSelf { dev })
}
#[derive(Clone, Copy)]
struct JsxSelf {
dev: bool,
}
impl Parallel for JsxSelf {
fn create(&self) -> Self {
*self
}
fn merge(&mut self, _: Self) {}
}
impl VisitMut for JsxSelf {
noop_visit_mut_type!();
fn visit_mut_jsx_opening_element(&mut self, n: &mut JSXOpeningElement) {
if !self.dev {
return;
}
n.attrs.push(JSXAttrOrSpread::JSXAttr(JSXAttr {
span: DUMMY_SP,
name: JSXAttrName::Ident(quote_ident!("__self")),
value: Some(JSXAttrValue::JSXExprContainer(JSXExprContainer {
span: DUMMY_SP,
expr: JSXExpr::Expr(Box::new(ThisExpr { span: DUMMY_SP }.into())),
})),
}));
}
}

View file

@ -0,0 +1,18 @@
use swc_ecma_transforms_testing::test;
use super::*;
fn tr() -> impl Fold {
jsx_self(true)
}
test!(
::swc_ecma_parser::Syntax::Es(::swc_ecma_parser::EsConfig {
jsx: true,
..Default::default()
}),
|_| tr(),
basic_sample,
r#"var x = <sometag />"#,
r#"var x = <sometag __self={this} />;"#
);

View file

@ -0,0 +1,72 @@
use swc_common::{sync::Lrc, SourceMap, DUMMY_SP};
use swc_ecma_ast::*;
use swc_ecma_transforms_base::perf::Parallel;
use swc_ecma_utils::quote_ident;
use swc_ecma_visit::{as_folder, noop_visit_mut_type, Fold, VisitMut};
#[cfg(test)]
mod tests;
/// `@babel/plugin-transform-react-jsx-source`
pub fn jsx_src(dev: bool, cm: Lrc<SourceMap>) -> impl Fold + VisitMut {
as_folder(JsxSrc { cm, dev })
}
#[derive(Clone)]
struct JsxSrc {
cm: Lrc<SourceMap>,
dev: bool,
}
impl Parallel for JsxSrc {
fn create(&self) -> Self {
self.clone()
}
fn merge(&mut self, _: Self) {}
}
impl VisitMut for JsxSrc {
noop_visit_mut_type!();
fn visit_mut_jsx_opening_element(&mut self, e: &mut JSXOpeningElement) {
if !self.dev || e.span == DUMMY_SP {
return;
}
let loc = self.cm.lookup_char_pos(e.span.lo);
let file_name = loc.file.name.to_string();
e.attrs.push(JSXAttrOrSpread::JSXAttr(JSXAttr {
span: DUMMY_SP,
name: JSXAttrName::Ident(quote_ident!("__source")),
value: Some(JSXAttrValue::JSXExprContainer(JSXExprContainer {
span: DUMMY_SP,
expr: JSXExpr::Expr(Box::new(
ObjectLit {
span: DUMMY_SP,
props: vec![
PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
key: PropName::Ident(quote_ident!("fileName")),
value: Box::new(Expr::Lit(Lit::Str(Str {
span: DUMMY_SP,
raw: None,
value: file_name.into(),
}))),
}))),
PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
key: PropName::Ident(quote_ident!("lineNumber")),
value: loc.line.into(),
}))),
PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
key: PropName::Ident(quote_ident!("columnNumber")),
value: (loc.col.0 + 1).into(),
}))),
],
}
.into(),
)),
})),
}));
}
}

View file

@ -0,0 +1,66 @@
use swc_common::{sync::Lrc, FilePathMapping};
use swc_ecma_transforms_testing::{test, test_exec};
use super::*;
fn tr() -> impl Fold {
let cm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
jsx_src(true, cm)
}
test_exec!(
ignore,
::swc_ecma_parser::Syntax::Es(::swc_ecma_parser::EsConfig {
jsx: true,
..Default::default()
}),
|_| tr(),
basic_sample,
r#"
var actual = transform(
'var x = <sometag />',
Object.assign({}, opts, { filename: '/fake/path/mock.js' })
).code;
var expected = multiline([
'var _jsxFileName = "/fake/path/mock.js";',
'var x = <sometag __source={{',
' fileName: _jsxFileName,',
' lineNumber: 1',
'}} />;',
]);
expect(actual).toBe(expected);
"#
);
test!(
::swc_ecma_parser::Syntax::Es(::swc_ecma_parser::EsConfig {
jsx: true,
..Default::default()
}),
|_| tr(),
no_jsx,
r#"var x = 42;"#,
r#"var x = 42;"#
);
test_exec!(
ignore,
::swc_ecma_parser::Syntax::Es(::swc_ecma_parser::EsConfig {
jsx: true,
..Default::default()
}),
|_| tr(),
with_source,
r#"
var actual = transform(
'var x = <sometag __source="custom" />;',
Object.assign({}, opts, { filename: '/fake/path/mock.js' })
).code;
var expected = 'var x = <sometag __source="custom" />;';
expect(actual).toBe(expected);
"#
);

View file

@ -0,0 +1,71 @@
#![deny(clippy::all)]
#![allow(clippy::arc_with_non_send_sync)]
use swc_common::{chain, comments::Comments, sync::Lrc, Mark, SourceMap};
use swc_ecma_visit::{Fold, VisitMut};
pub use self::{
display_name::display_name,
jsx::*,
jsx_self::jsx_self,
jsx_src::jsx_src,
pure_annotations::pure_annotations,
refresh::{options::RefreshOptions, refresh},
};
mod display_name;
mod jsx;
mod jsx_self;
mod jsx_src;
mod pure_annotations;
mod refresh;
/// `@babel/preset-react`
///
/// Preset for all React plugins.
///
///
/// `top_level_mark` should be [Mark] passed to
/// [swc_ecma_transforms_base::resolver::resolver_with_mark].
///
///
///
/// # Note
///
/// This pass uses [swc_ecma_utils::HANDLER].
pub fn react<C>(
cm: Lrc<SourceMap>,
comments: Option<C>,
mut options: Options,
top_level_mark: Mark,
unresolved_mark: Mark,
) -> impl Fold + VisitMut
where
C: Comments + Clone,
{
let Options { development, .. } = options;
let development = development.unwrap_or(false);
let refresh_options = options.refresh.take();
chain!(
jsx_src(development, cm.clone()),
jsx_self(development),
refresh(
development,
refresh_options,
cm.clone(),
comments.clone(),
top_level_mark
),
jsx(
cm,
comments.clone(),
options,
top_level_mark,
unresolved_mark
),
display_name(),
pure_annotations(comments),
)
}

View file

@ -0,0 +1,136 @@
use swc_atoms::{js_word, JsWord};
use swc_common::{collections::AHashMap, comments::Comments};
use swc_ecma_ast::*;
use swc_ecma_visit::{as_folder, noop_visit_mut_type, Fold, VisitMut, VisitMutWith};
#[cfg(test)]
mod tests;
/// This pass adds a /*#__PURE__#/ annotation to calls to known pure top-level
/// React methods, so that terser and other minifiers can safely remove them
/// during dead code elimination.
/// See https://reactjs.org/docs/react-api.html
pub fn pure_annotations<C>(comments: Option<C>) -> impl Fold + VisitMut
where
C: Comments,
{
as_folder(PureAnnotations {
imports: Default::default(),
comments,
})
}
struct PureAnnotations<C>
where
C: Comments,
{
imports: AHashMap<Id, (JsWord, JsWord)>,
comments: Option<C>,
}
impl<C> VisitMut for PureAnnotations<C>
where
C: Comments,
{
noop_visit_mut_type!();
fn visit_mut_module(&mut self, module: &mut Module) {
// Pass 1: collect imports
for item in &module.body {
if let ModuleItem::ModuleDecl(ModuleDecl::Import(import)) = item {
let src_str = &*import.src.value;
if src_str != "react" && src_str != "react-dom" {
continue;
}
for specifier in &import.specifiers {
let src = import.src.value.clone();
match specifier {
ImportSpecifier::Named(named) => {
let imported = match &named.imported {
Some(ModuleExportName::Ident(imported)) => imported.sym.clone(),
Some(ModuleExportName::Str(..)) => named.local.sym.clone(),
None => named.local.sym.clone(),
};
self.imports.insert(named.local.to_id(), (src, imported));
}
ImportSpecifier::Default(default) => {
self.imports
.insert(default.local.to_id(), (src, js_word!("default")));
}
ImportSpecifier::Namespace(ns) => {
self.imports.insert(ns.local.to_id(), (src, "*".into()));
}
}
}
}
}
if self.imports.is_empty() {
return;
}
// Pass 2: add pure annotations.
module.visit_mut_children_with(self);
}
fn visit_mut_call_expr(&mut self, call: &mut CallExpr) {
let is_react_call = match &call.callee {
Callee::Expr(expr) => match &**expr {
Expr::Ident(ident) => {
if let Some((src, specifier)) = self.imports.get(&ident.to_id()) {
is_pure(src, specifier)
} else {
false
}
}
Expr::Member(member) => match &*member.obj {
Expr::Ident(ident) => {
if let Some((src, specifier)) = self.imports.get(&ident.to_id()) {
if &**specifier == "default" || &**specifier == "*" {
match &member.prop {
MemberProp::Ident(ident) => is_pure(src, &ident.sym),
_ => false,
}
} else {
false
}
} else {
false
}
}
_ => false,
},
_ => false,
},
_ => false,
};
if is_react_call {
if let Some(comments) = &self.comments {
comments.add_pure_comment(call.span.lo);
}
}
call.visit_mut_children_with(self);
}
}
fn is_pure(src: &JsWord, specifier: &JsWord) -> bool {
match &**src {
"react" => matches!(
&**specifier,
"cloneElement"
| "createContext"
| "createElement"
| "createFactory"
| "createRef"
| "forwardRef"
| "isValidElement"
| "memo"
| "lazy"
),
"react-dom" => matches!(&**specifier, "createPortal"),
_ => false,
}
}

View file

@ -0,0 +1,314 @@
use swc_common::{comments::SingleThreadedComments, sync::Lrc, FileName, Mark, SourceMap};
use swc_ecma_codegen::{text_writer::JsWriter, Emitter};
use swc_ecma_parser::{Parser, StringInput};
use swc_ecma_transforms_base::resolver;
use swc_ecma_transforms_testing::Tester;
use swc_ecma_visit::FoldWith;
use super::*;
fn parse(
tester: &mut Tester,
src: &str,
) -> Result<(Module, Lrc<SourceMap>, Lrc<SingleThreadedComments>), ()> {
let syntax = ::swc_ecma_parser::Syntax::Es(::swc_ecma_parser::EsConfig {
jsx: true,
..Default::default()
});
let source_map = Lrc::new(SourceMap::default());
let source_file = source_map.new_source_file(FileName::Anon, src.into());
let comments = Lrc::new(SingleThreadedComments::default());
let module = {
let mut p = Parser::new(syntax, StringInput::from(&*source_file), Some(&comments));
let res = p
.parse_module()
.map_err(|e| e.into_diagnostic(tester.handler).emit());
for e in p.take_errors() {
e.into_diagnostic(tester.handler).emit()
}
res?
};
Ok((module, source_map, comments))
}
fn emit(
source_map: Lrc<SourceMap>,
comments: Lrc<SingleThreadedComments>,
program: &Module,
) -> String {
let mut src_map_buf = vec![];
let mut buf = vec![];
{
let writer = Box::new(JsWriter::new(
source_map.clone(),
"\n",
&mut buf,
Some(&mut src_map_buf),
));
let mut emitter = Emitter {
cfg: Default::default(),
comments: Some(&comments),
cm: source_map,
wr: writer,
};
emitter.emit_module(program).unwrap();
}
String::from_utf8(buf).unwrap()
}
fn run_test(input: &str, expected: &str) {
Tester::run(|tester| {
let unresolved_mark = Mark::new();
let top_level_mark = Mark::new();
let (actual, actual_sm, actual_comments) = parse(tester, input)?;
let actual = actual
.fold_with(&mut resolver(unresolved_mark, top_level_mark, false))
.fold_with(&mut crate::react(
actual_sm.clone(),
Some(&actual_comments),
Default::default(),
top_level_mark,
unresolved_mark,
));
let actual_src = emit(actual_sm, actual_comments, &actual);
let (expected, expected_sm, expected_comments) = parse(tester, expected)?;
let expected_src = emit(expected_sm, expected_comments, &expected);
if actual_src != expected_src {
println!(">>>>> Orig <<<<<\n{}", input);
println!(">>>>> Code <<<<<\n{}", actual_src);
panic!(
r#"assertion failed: `(left == right)`
{}"#,
::testing::diff(&actual_src, &expected_src),
);
}
Ok(())
});
}
macro_rules! test {
($test_name:ident, $input:expr, $expected:expr) => {
#[test]
fn $test_name() {
run_test($input, $expected)
}
};
}
test!(
forward_ref,
r#"
import {forwardRef} from 'react';
const Comp = forwardRef((props, ref) => null);
"#,
r#"
import {forwardRef} from 'react';
const Comp = /*#__PURE__*/ forwardRef((props, ref) => null);
"#
);
test!(
create_element,
r#"
import React from 'react';
React.createElement('div');
"#,
r#"
import React from 'react';
/*#__PURE__*/ React.createElement('div');
"#
);
test!(
create_element_jsx,
r#"
import React from 'react';
const x = <div />;
"#,
r#"
import React from 'react';
const x = /*#__PURE__*/ React.createElement("div", null);
"#
);
test!(
create_element_fragment_jsx,
r#"
import React from 'react';
const x = <><div /></>;
"#,
r#"
import React from 'react';
const x = /*#__PURE__*/ React.createElement(React.Fragment, null, /*#__PURE__*/ React.createElement("div", null));
"#
);
test!(
clone_element,
r#"
import React from 'react';
React.cloneElement(React.createElement('div'));
"#,
r#"
import React from 'react';
/*#__PURE__*/ React.cloneElement(/*#__PURE__*/ React.createElement('div'));
"#
);
test!(
create_context,
r#"
import { createContext } from 'react';
const context = createContext({});
"#,
r#"
import { createContext } from 'react';
const context = /*#__PURE__*/createContext({});
"#
);
test!(
create_factory,
r#"
import {createFactory} from 'react';
const div = createFactory('div');
"#,
r#"
import { createFactory } from 'react';
const div = /*#__PURE__*/createFactory('div');
"#
);
test!(
create_ref,
r#"
import React from 'react';
React.createRef();
"#,
r#"
import React from 'react';
/*#__PURE__*/ React.createRef();
"#
);
test!(
is_valid_element,
r#"
import React from 'react';
const isElement = React.isValidElement(React.createElement('div'));
"#,
r#"
import React from 'react';
const isElement = /*#__PURE__*/React.isValidElement( /*#__PURE__*/React.createElement('div'));
"#
);
test!(
lazy,
r#"
import React from 'react';
const SomeComponent = React.lazy(() => import('./SomeComponent'));
"#,
r#"
import React from 'react';
const SomeComponent = /*#__PURE__*/React.lazy(() => import('./SomeComponent'));
"#
);
test!(
memo,
r#"
import React from 'react';
const Comp = React.memo((props) => null);
"#,
r#"
import React from 'react';
const Comp = /*#__PURE__*/React.memo(props => null);
"#
);
test!(
create_portal,
r#"
import * as React from 'react';
import ReactDOM from 'react-dom';
const Portal = ReactDOM.createPortal(React.createElement('div'), document.getElementById('test'));
"#,
r#"
import * as React from 'react';
import ReactDOM from 'react-dom';
const Portal = /*#__PURE__*/ReactDOM.createPortal( /*#__PURE__*/React.createElement('div'), document.getElementById('test'));
"#
);
test!(
non_pure_react,
r#"
import {useState} from 'react';
useState(2);
"#,
r#"
import {useState} from 'react';
useState(2);
"#
);
test!(
non_pure_react_dom,
r#"
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(React.createElement('div'));
"#,
r#"
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(/*#__PURE__*/React.createElement('div'));
"#
);
test!(
non_react_named,
r#"
import {createElement} from 'foo';
createElement('hi');
"#,
r#"
import {createElement} from 'foo';
createElement('hi');
"#
);
test!(
non_react_namespace,
r#"
import * as foo from 'foo';
foo.createElement('hi');
"#,
r#"
import * as foo from 'foo';
foo.createElement('hi');
"#
);
test!(
non_react_default,
r#"
import foo from 'foo';
foo.createElement('hi');
"#,
r#"
import foo from 'foo';
foo.createElement('hi');
"#
);

View file

@ -0,0 +1,497 @@
use std::{fmt::Write, mem};
use sha1::{Digest, Sha1};
use swc_common::{util::take::Take, SourceMap, SourceMapper, Spanned, SyntaxContext, DUMMY_SP};
use swc_ecma_ast::*;
use swc_ecma_utils::{private_ident, quote_ident, ExprFactory};
use swc_ecma_visit::{
noop_visit_mut_type, noop_visit_type, Visit, VisitMut, VisitMutWith, VisitWith,
};
use super::util::{is_builtin_hook, make_call_expr, make_call_stmt};
use crate::RefreshOptions;
// function that use hooks
struct HookSig {
handle: Ident,
// need to add an extra register, or already inlined
hooks: Vec<Hook>,
}
impl HookSig {
fn new(hooks: Vec<Hook>) -> Self {
HookSig {
handle: private_ident!("_s"),
hooks,
}
}
}
struct Hook {
callee: HookCall,
key: String,
}
// we only consider two kinds of callee as hook call
#[allow(clippy::large_enum_variant)]
enum HookCall {
Ident(Ident),
Member(Expr, Ident), // for obj and prop
}
pub struct HookRegister<'a> {
pub options: &'a RefreshOptions,
pub ident: Vec<Ident>,
pub extra_stmt: Vec<Stmt>,
pub current_scope: Vec<SyntaxContext>,
pub cm: &'a SourceMap,
pub should_reset: bool,
}
impl<'a> HookRegister<'a> {
pub fn gen_hook_handle(&mut self) -> Stmt {
VarDecl {
span: DUMMY_SP,
kind: VarDeclKind::Var,
decls: self
.ident
.take()
.into_iter()
.map(|id| VarDeclarator {
span: DUMMY_SP,
name: id.into(),
init: Some(Box::new(make_call_expr(quote_ident!(self
.options
.refresh_sig
.clone())))),
definite: false,
})
.collect(),
declare: false,
}
.into()
}
// The second call is around the function itself. This is used to associate a
// type with a signature.
// Unlike with $RefreshReg$, this needs to work for nested declarations too.
fn wrap_with_register(&self, handle: Ident, func: Expr, hooks: Vec<Hook>) -> Expr {
let mut args = vec![func.as_arg()];
let mut sign = Vec::new();
let mut custom_hook = Vec::new();
for hook in hooks {
let name = match &hook.callee {
HookCall::Ident(i) => i,
HookCall::Member(_, i) => i,
};
sign.push(format!("{}{{{}}}", name.sym, hook.key));
match &hook.callee {
HookCall::Ident(ident) if !is_builtin_hook(ident) => {
custom_hook.push(hook.callee);
}
HookCall::Member(Expr::Ident(obj_ident), prop) if !is_builtin_hook(prop) => {
if obj_ident.sym.as_ref() != "React" {
custom_hook.push(hook.callee);
}
}
_ => (),
};
}
let sign = sign.join("\n");
let sign = if self.options.emit_full_signatures {
sign
} else {
let mut hasher = Sha1::new();
hasher.update(sign);
base64::encode(hasher.finalize())
};
args.push(
Expr::Lit(Lit::Str(Str {
span: DUMMY_SP,
raw: None,
value: sign.into(),
}))
.as_arg(),
);
let mut should_reset = self.should_reset;
let mut custom_hook_in_scope = Vec::new();
for hook in custom_hook {
let ident = match &hook {
HookCall::Ident(ident) => Some(ident),
HookCall::Member(Expr::Ident(ident), _) => Some(ident),
_ => None,
};
if !ident
.map(|id| self.current_scope.contains(&id.span.ctxt))
.unwrap_or(false)
{
// We don't have anything to put in the array because Hook is out of scope.
// Since it could potentially have been edited, remount the component.
should_reset = true;
} else {
custom_hook_in_scope.push(hook);
}
}
if should_reset || !custom_hook_in_scope.is_empty() {
args.push(should_reset.as_arg());
}
if !custom_hook_in_scope.is_empty() {
let elems = custom_hook_in_scope
.into_iter()
.map(|hook| {
Some(
match hook {
HookCall::Ident(ident) => Expr::Ident(ident),
HookCall::Member(obj, prop) => Expr::Member(MemberExpr {
span: DUMMY_SP,
obj: Box::new(obj),
prop: MemberProp::Ident(prop),
}),
}
.as_arg(),
)
})
.collect();
args.push(
Function {
is_generator: false,
is_async: false,
params: Vec::new(),
decorators: Vec::new(),
span: DUMMY_SP,
body: Some(BlockStmt {
span: DUMMY_SP,
stmts: vec![Stmt::Return(ReturnStmt {
span: DUMMY_SP,
arg: Some(Box::new(Expr::Array(ArrayLit {
span: DUMMY_SP,
elems,
}))),
})],
}),
type_params: None,
return_type: None,
}
.as_arg(),
);
}
Expr::Call(CallExpr {
span: DUMMY_SP,
callee: handle.as_callee(),
args,
type_args: None,
})
}
fn gen_hook_register_stmt(&mut self, ident: Ident, sig: HookSig) {
self.ident.push(sig.handle.clone());
self.extra_stmt.push(Stmt::Expr(ExprStmt {
span: DUMMY_SP,
expr: Box::new(self.wrap_with_register(sig.handle, Expr::Ident(ident), sig.hooks)),
}))
}
}
impl<'a> VisitMut for HookRegister<'a> {
noop_visit_mut_type!();
fn visit_mut_block_stmt(&mut self, b: &mut BlockStmt) {
let old_ident = self.ident.take();
let old_stmts = self.extra_stmt.take();
self.current_scope.push(b.span.ctxt);
let stmt_count = b.stmts.len();
let stmts = mem::replace(&mut b.stmts, Vec::with_capacity(stmt_count));
for mut stmt in stmts {
stmt.visit_mut_children_with(self);
b.stmts.push(stmt);
b.stmts.append(&mut self.extra_stmt);
}
if !self.ident.is_empty() {
b.stmts.insert(0, self.gen_hook_handle())
}
self.current_scope.pop();
self.ident = old_ident;
self.extra_stmt = old_stmts;
}
fn visit_mut_expr(&mut self, e: &mut Expr) {
e.visit_mut_children_with(self);
match e {
Expr::Fn(FnExpr { function: f, .. }) if f.body.is_some() => {
let sig = collect_hooks(&mut f.body.as_mut().unwrap().stmts, self.cm);
if let Some(HookSig { handle, hooks }) = sig {
self.ident.push(handle.clone());
*e = self.wrap_with_register(handle, e.take(), hooks);
}
}
Expr::Arrow(ArrowExpr { body, .. }) => {
let sig = collect_hooks_arrow(body, self.cm);
if let Some(HookSig { handle, hooks }) = sig {
self.ident.push(handle.clone());
*e = self.wrap_with_register(handle, e.take(), hooks);
}
}
_ => (),
}
}
fn visit_mut_var_decl(&mut self, n: &mut VarDecl) {
// we don't want visit_mut_expr to mess up with function name inference
// so intercept it here
for decl in n.decls.iter_mut() {
if let VarDeclarator {
// it doesn't quite make sense for other Pat to appear here
name: Pat::Ident(BindingIdent { id, .. }),
init: Some(init),
..
} = decl
{
match init.as_mut() {
Expr::Fn(FnExpr { function: f, .. }) if f.body.is_some() => {
f.body.visit_mut_with(self);
if let Some(sig) =
collect_hooks(&mut f.body.as_mut().unwrap().stmts, self.cm)
{
self.gen_hook_register_stmt(id.clone(), sig);
}
}
Expr::Arrow(ArrowExpr { body, .. }) => {
body.visit_mut_with(self);
if let Some(sig) = collect_hooks_arrow(body, self.cm) {
self.gen_hook_register_stmt(id.clone(), sig);
}
}
_ => self.visit_mut_expr(init),
}
} else {
decl.visit_mut_children_with(self)
}
}
}
fn visit_mut_default_decl(&mut self, d: &mut DefaultDecl) {
d.visit_mut_children_with(self);
// only when expr has ident
match d {
DefaultDecl::Fn(FnExpr {
ident: Some(ident),
function: f,
}) if f.body.is_some() => {
if let Some(sig) = collect_hooks(&mut f.body.as_mut().unwrap().stmts, self.cm) {
self.gen_hook_register_stmt(ident.clone(), sig);
}
}
_ => {}
}
}
fn visit_mut_fn_decl(&mut self, f: &mut FnDecl) {
f.visit_mut_children_with(self);
if let Some(body) = &mut f.function.body {
if let Some(sig) = collect_hooks(&mut body.stmts, self.cm) {
self.gen_hook_register_stmt(f.ident.clone(), sig);
}
}
}
}
fn collect_hooks(stmts: &mut Vec<Stmt>, cm: &SourceMap) -> Option<HookSig> {
let mut hook = HookCollector {
state: Vec::new(),
cm,
};
stmts.visit_with(&mut hook);
if !hook.state.is_empty() {
let sig = HookSig::new(hook.state);
stmts.insert(0, make_call_stmt(sig.handle.clone()));
Some(sig)
} else {
None
}
}
fn collect_hooks_arrow(body: &mut BlockStmtOrExpr, cm: &SourceMap) -> Option<HookSig> {
match body {
BlockStmtOrExpr::BlockStmt(block) => collect_hooks(&mut block.stmts, cm),
BlockStmtOrExpr::Expr(expr) => {
let mut hook = HookCollector {
state: Vec::new(),
cm,
};
expr.visit_with(&mut hook);
if !hook.state.is_empty() {
let sig = HookSig::new(hook.state);
*body = BlockStmtOrExpr::BlockStmt(BlockStmt {
span: expr.span(),
stmts: vec![
make_call_stmt(sig.handle.clone()),
Stmt::Return(ReturnStmt {
span: expr.span(),
arg: Some(Box::new(expr.as_mut().take())),
}),
],
});
Some(sig)
} else {
None
}
}
}
}
struct HookCollector<'a> {
state: Vec<Hook>,
cm: &'a SourceMap,
}
fn is_hook_like(s: &str) -> bool {
if let Some(s) = s.strip_prefix("use") {
s.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
} else {
false
}
}
impl<'a> HookCollector<'a> {
fn get_hook_from_call_expr(&self, expr: &CallExpr, lhs: Option<&Pat>) -> Option<Hook> {
let callee = if let Callee::Expr(callee) = &expr.callee {
Some(callee.as_ref())
} else {
None
}?;
let mut hook_call = None;
let ident = match callee {
Expr::Ident(ident) => {
hook_call = Some(HookCall::Ident(ident.clone()));
Some(ident)
}
// hook cannot be used in class, so we're fine without SuperProp
Expr::Member(MemberExpr {
obj,
prop: MemberProp::Ident(ident),
..
}) => {
hook_call = Some(HookCall::Member(*obj.clone(), ident.clone()));
Some(ident)
}
_ => None,
}?;
let name = if is_hook_like(&ident.sym) {
Some(ident)
} else {
None
}?;
let mut key = if let Some(name) = lhs {
self.cm.span_to_snippet(name.span()).unwrap_or_default()
} else {
String::new()
};
// Some built-in Hooks reset on edits to arguments.
if &name.sym == "useState" && !expr.args.is_empty() {
// useState first argument is initial state.
let _ = write!(
key,
"({})",
self.cm
.span_to_snippet(expr.args[0].span())
.unwrap_or_default()
);
} else if &name.sym == "useReducer" && expr.args.len() > 1 {
// useReducer second argument is initial state.
let _ = write!(
key,
"({})",
self.cm
.span_to_snippet(expr.args[1].span())
.unwrap_or_default()
);
}
let callee = hook_call?;
Some(Hook { callee, key })
}
fn get_hook_from_expr(&self, expr: &Expr, lhs: Option<&Pat>) -> Option<Hook> {
if let Expr::Call(call) = expr {
self.get_hook_from_call_expr(call, lhs)
} else {
None
}
}
}
impl<'a> Visit for HookCollector<'a> {
noop_visit_type!();
fn visit_block_stmt_or_expr(&mut self, _: &BlockStmtOrExpr) {}
fn visit_block_stmt(&mut self, _: &BlockStmt) {}
fn visit_expr(&mut self, expr: &Expr) {
expr.visit_children_with(self);
if let Expr::Call(call) = expr {
if let Some(hook) = self.get_hook_from_call_expr(call, None) {
self.state.push(hook)
}
}
}
fn visit_stmt(&mut self, stmt: &Stmt) {
match stmt {
Stmt::Expr(ExprStmt { expr, .. }) => {
if let Some(hook) = self.get_hook_from_expr(expr, None) {
self.state.push(hook)
} else {
stmt.visit_children_with(self)
}
}
Stmt::Decl(Decl::Var(var_decl)) => {
for decl in &var_decl.decls {
if let Some(init) = &decl.init {
if let Some(hook) = self.get_hook_from_expr(init, Some(&decl.name)) {
self.state.push(hook)
} else {
stmt.visit_children_with(self)
}
} else {
stmt.visit_children_with(self)
}
}
}
Stmt::Return(ReturnStmt { arg: Some(arg), .. }) => {
if let Some(hook) = self.get_hook_from_expr(arg.as_ref(), None) {
self.state.push(hook)
} else {
stmt.visit_children_with(self)
}
}
_ => stmt.visit_children_with(self),
}
}
}

View file

@ -0,0 +1,493 @@
use swc_common::{
collections::AHashSet, comments::Comments, sync::Lrc, util::take::Take, BytePos, Mark,
SourceMap, SourceMapper, Span, Spanned, SyntaxContext, DUMMY_SP,
};
use swc_ecma_ast::*;
use swc_ecma_utils::{private_ident, quote_ident, quote_str, ExprFactory};
use swc_ecma_visit::{as_folder, Fold, Visit, VisitMut, VisitMutWith};
use self::{
hook::HookRegister,
util::{collect_ident_in_jsx, is_body_arrow_fn, is_import_or_require, make_assign_stmt},
};
pub mod options;
use options::RefreshOptions;
mod hook;
mod util;
#[cfg(test)]
mod tests;
struct Hoc {
insert: bool,
reg: Vec<(Ident, Id)>,
hook: Option<HocHook>,
}
struct HocHook {
callee: Callee,
rest_arg: Vec<ExprOrSpread>,
}
enum Persist {
Hoc(Hoc),
Component(Ident),
None,
}
fn get_persistent_id(ident: &Ident) -> Persist {
if ident.sym.starts_with(|c: char| c.is_ascii_uppercase()) {
if cfg!(debug_assertions) && ident.span.ctxt == SyntaxContext::empty() {
panic!("`{}` should be resolved", ident)
}
Persist::Component(ident.clone())
} else {
Persist::None
}
}
/// `react-refresh/babel`
/// https://github.com/facebook/react/blob/main/packages/react-refresh/src/ReactFreshBabelPlugin.js
pub fn refresh<C: Comments>(
dev: bool,
options: Option<RefreshOptions>,
cm: Lrc<SourceMap>,
comments: Option<C>,
global_mark: Mark,
) -> impl Fold + VisitMut {
as_folder(Refresh {
enable: dev && options.is_some(),
cm,
comments,
should_reset: false,
options: options.unwrap_or_default(),
global_mark,
})
}
struct Refresh<C: Comments> {
enable: bool,
options: RefreshOptions,
cm: Lrc<SourceMap>,
should_reset: bool,
comments: Option<C>,
global_mark: Mark,
}
impl<C: Comments> Refresh<C> {
fn get_persistent_id_from_var_decl(
&self,
var_decl: &mut VarDecl,
used_in_jsx: &AHashSet<Id>,
hook_reg: &mut HookRegister,
) -> Persist {
// We only handle the case when a single variable is declared
if let [VarDeclarator {
name: Pat::Ident(binding),
init: Some(init_expr),
..
}] = var_decl.decls.as_mut_slice()
{
if used_in_jsx.contains(&binding.to_id()) && !is_import_or_require(init_expr) {
match init_expr.as_ref() {
// TaggedTpl is for something like styled.div`...`
Expr::Arrow(_) | Expr::Fn(_) | Expr::TaggedTpl(_) | Expr::Call(_) => {
return Persist::Component(binding.id.clone())
}
_ => (),
}
}
if let Persist::Component(persistent_id) = get_persistent_id(&binding.id) {
return match init_expr.as_mut() {
Expr::Fn(_) => Persist::Component(persistent_id),
Expr::Arrow(ArrowExpr { body, .. }) => {
// Ignore complex function expressions like
// let Foo = () => () => {}
if is_body_arrow_fn(body) {
Persist::None
} else {
Persist::Component(persistent_id)
}
}
// Maybe a HOC.
Expr::Call(call_expr) => {
let res = self.get_persistent_id_from_possible_hoc(
call_expr,
vec![(private_ident!("_c"), persistent_id.to_id())],
hook_reg,
);
if let Persist::Hoc(Hoc {
insert,
reg,
hook: Some(hook),
}) = res
{
make_hook_reg(init_expr.as_mut(), hook);
Persist::Hoc(Hoc {
insert,
reg,
hook: None,
})
} else {
res
}
}
_ => Persist::None,
};
}
}
Persist::None
}
fn get_persistent_id_from_possible_hoc(
&self,
call_expr: &mut CallExpr,
mut reg: Vec<(Ident, Id)>,
hook_reg: &mut HookRegister,
) -> Persist {
let first_arg = match call_expr.args.as_mut_slice() {
[first, ..] => &mut first.expr,
_ => return Persist::None,
};
let callee = if let Callee::Expr(expr) = &call_expr.callee {
expr
} else {
return Persist::None;
};
let hoc_name = match callee.as_ref() {
Expr::Ident(fn_name) => fn_name.sym.to_string(),
// original react implement use `getSource` so we just follow them
Expr::Member(member) => self.cm.span_to_snippet(member.span).unwrap_or_default(),
_ => return Persist::None,
};
let reg_str = (
format!("{}${}", reg.last().unwrap().1 .0, &hoc_name).into(),
SyntaxContext::empty(),
);
match first_arg.as_mut() {
Expr::Call(expr) => {
let reg_ident = private_ident!("_c");
reg.push((reg_ident.clone(), reg_str));
if let Persist::Hoc(hoc) =
self.get_persistent_id_from_possible_hoc(expr, reg, hook_reg)
{
let mut first = first_arg.take();
if let Some(HocHook { callee, rest_arg }) = &hoc.hook {
let span = first.span();
let mut args = vec![first.as_arg()];
args.extend(rest_arg.clone());
first = Box::new(Expr::Call(CallExpr {
span,
callee: callee.clone(),
args,
type_args: None,
}))
}
*first_arg = Box::new(make_assign_stmt(reg_ident, first));
Persist::Hoc(hoc)
} else {
Persist::None
}
}
Expr::Fn(_) | Expr::Arrow(_) => {
let reg_ident = private_ident!("_c");
let mut first = first_arg.take();
first.visit_mut_with(hook_reg);
let hook = if let Expr::Call(call) = first.as_ref() {
let res = Some(HocHook {
callee: call.callee.clone(),
rest_arg: call.args[1..].to_owned(),
});
*first_arg = Box::new(make_assign_stmt(reg_ident.clone(), first));
res
} else {
*first_arg = Box::new(make_assign_stmt(reg_ident.clone(), first));
None
};
reg.push((reg_ident, reg_str));
Persist::Hoc(Hoc {
reg,
insert: true,
hook,
})
}
// export default hoc(Foo)
// const X = hoc(Foo)
Expr::Ident(ident) => {
if let Persist::Component(_) = get_persistent_id(ident) {
Persist::Hoc(Hoc {
reg,
insert: true,
hook: None,
})
} else {
Persist::None
}
}
_ => Persist::None,
}
}
}
/// We let user do /* @refresh reset */ to reset state in the whole file.
impl<C> Visit for Refresh<C>
where
C: Comments,
{
fn visit_span(&mut self, n: &Span) {
if self.should_reset {
return;
}
let mut should_refresh = self.should_reset;
if let Some(comments) = &self.comments {
if !n.hi.is_dummy() {
comments.with_leading(n.hi - BytePos(1), |comments| {
if comments.iter().any(|c| c.text.contains("@refresh reset")) {
should_refresh = true
}
});
}
comments.with_leading(n.lo, |comments| {
if comments.iter().any(|c| c.text.contains("@refresh reset")) {
should_refresh = true
}
});
comments.with_trailing(n.lo, |comments| {
if comments.iter().any(|c| c.text.contains("@refresh reset")) {
should_refresh = true
}
});
}
self.should_reset = should_refresh;
}
}
// TODO: figure out if we can insert all registers at once
impl<C: Comments> VisitMut for Refresh<C> {
// Does anyone write react without esmodule?
// fn visit_mut_script(&mut self, _: &mut Script) {}
fn visit_mut_module(&mut self, n: &mut Module) {
if !self.enable {
return;
}
// to collect comments
self.visit_module(n);
self.visit_mut_module_items(&mut n.body);
}
fn visit_mut_module_items(&mut self, module_items: &mut Vec<ModuleItem>) {
let used_in_jsx = collect_ident_in_jsx(module_items);
let mut items = Vec::with_capacity(module_items.len());
let mut refresh_regs = Vec::<(Ident, Id)>::new();
let mut hook_visitor = HookRegister {
options: &self.options,
ident: Vec::new(),
extra_stmt: Vec::new(),
current_scope: vec![SyntaxContext::empty().apply_mark(self.global_mark)],
cm: &self.cm,
should_reset: self.should_reset,
};
for mut item in module_items.take() {
let persistent_id = match &mut item {
// function Foo() {}
ModuleItem::Stmt(Stmt::Decl(Decl::Fn(FnDecl { ident, .. }))) => {
get_persistent_id(ident)
}
// export function Foo() {}
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
decl: Decl::Fn(FnDecl { ident, .. }),
..
})) => get_persistent_id(ident),
// export default function Foo() {}
ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(ExportDefaultDecl {
decl:
DefaultDecl::Fn(FnExpr {
// We don't currently handle anonymous default exports.
ident: Some(ident),
..
}),
..
})) => get_persistent_id(ident),
// const Foo = () => {}
// export const Foo = () => {}
ModuleItem::Stmt(Stmt::Decl(Decl::Var(var_decl)))
| ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
decl: Decl::Var(var_decl),
..
})) => {
self.get_persistent_id_from_var_decl(var_decl, &used_in_jsx, &mut hook_visitor)
}
// This code path handles nested cases like:
// export default memo(() => {})
// In those cases it is more plausible people will omit names
// so they're worth handling despite possible false positives.
ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(ExportDefaultExpr {
expr,
span,
})) => {
if let Expr::Call(call) = expr.as_mut() {
if let Persist::Hoc(Hoc { reg, hook, .. }) = self
.get_persistent_id_from_possible_hoc(
call,
vec![(
private_ident!("_c"),
("%default%".into(), SyntaxContext::empty()),
)],
&mut hook_visitor,
)
{
if let Some(hook) = hook {
make_hook_reg(expr.as_mut(), hook)
}
item = ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(
ExportDefaultExpr {
expr: Box::new(make_assign_stmt(reg[0].0.clone(), expr.take())),
span: *span,
},
));
Persist::Hoc(Hoc {
insert: false,
reg,
hook: None,
})
} else {
Persist::None
}
} else {
Persist::None
}
}
_ => Persist::None,
};
if let Persist::Hoc(_) = persistent_id {
// we need to make hook transform happens after component for
// HOC
items.push(item);
} else {
item.visit_mut_children_with(&mut hook_visitor);
items.push(item);
items.extend(
hook_visitor
.extra_stmt
.take()
.into_iter()
.map(ModuleItem::Stmt),
);
}
match persistent_id {
Persist::None => (),
Persist::Component(persistent_id) => {
let registration_handle = private_ident!("_c");
refresh_regs.push((registration_handle.clone(), persistent_id.to_id()));
items.push(ModuleItem::Stmt(Stmt::Expr(ExprStmt {
span: DUMMY_SP,
expr: Box::new(make_assign_stmt(
registration_handle,
Box::new(Expr::Ident(persistent_id)),
)),
})));
}
Persist::Hoc(mut hoc) => {
hoc.reg = hoc.reg.into_iter().rev().collect();
if hoc.insert {
let (ident, name) = hoc.reg.last().unwrap();
items.push(ModuleItem::Stmt(Stmt::Expr(ExprStmt {
span: DUMMY_SP,
expr: Box::new(make_assign_stmt(
ident.clone(),
Box::new(Expr::Ident(Ident::new(
name.0.clone(),
DUMMY_SP.with_ctxt(name.1),
))),
)),
})))
}
refresh_regs.append(&mut hoc.reg);
}
}
}
if !hook_visitor.ident.is_empty() {
items.insert(0, ModuleItem::Stmt(hook_visitor.gen_hook_handle()));
}
// Insert
// ```
// var _c, _c1;
// ```
if !refresh_regs.is_empty() {
items.push(
VarDecl {
span: DUMMY_SP,
kind: VarDeclKind::Var,
declare: false,
decls: refresh_regs
.iter()
.map(|(handle, _)| VarDeclarator {
span: DUMMY_SP,
name: handle.clone().into(),
init: None,
definite: false,
})
.collect(),
}
.into(),
);
}
// Insert
// ```
// $RefreshReg$(_c, "Hello");
// $RefreshReg$(_c1, "Foo");
// ```
let refresh_reg = self.options.refresh_reg.as_str();
for (handle, persistent_id) in refresh_regs {
items.push(ModuleItem::Stmt(Stmt::Expr(ExprStmt {
span: DUMMY_SP,
expr: Box::new(Expr::Call(CallExpr {
span: DUMMY_SP,
callee: quote_ident!(refresh_reg).as_callee(),
args: vec![handle.as_arg(), quote_str!(persistent_id.0).as_arg()],
type_args: None,
})),
})));
}
*module_items = items
}
fn visit_mut_ts_module_decl(&mut self, _: &mut TsModuleDecl) {}
}
fn make_hook_reg(expr: &mut Expr, mut hook: HocHook) {
let span = expr.span();
let mut args = vec![expr.take().as_arg()];
args.append(&mut hook.rest_arg);
*expr = Expr::Call(CallExpr {
span,
callee: hook.callee,
args,
type_args: None,
});
}

View file

@ -0,0 +1,52 @@
use serde::{Deserialize, Deserializer, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
pub struct RefreshOptions {
#[serde(default = "default_refresh_reg")]
pub refresh_reg: String,
#[serde(default = "default_refresh_sig")]
pub refresh_sig: String,
#[serde(default = "default_emit_full_signatures")]
pub emit_full_signatures: bool,
}
fn default_refresh_reg() -> String {
"$RefreshReg$".to_string()
}
fn default_refresh_sig() -> String {
"$RefreshSig$".to_string()
}
fn default_emit_full_signatures() -> bool {
// Prefer to hash when we can
// This makes it deterministically compact, even if there's
// e.g. a useState initializer with some code inside.
false
}
impl Default for RefreshOptions {
fn default() -> Self {
RefreshOptions {
refresh_reg: default_refresh_reg(),
refresh_sig: default_refresh_sig(),
emit_full_signatures: default_emit_full_signatures(),
}
}
}
#[derive(Deserialize)]
#[serde(untagged)]
enum BoolOrRefresh {
Bool(bool),
Refresh(RefreshOptions),
}
pub fn deserialize_refresh<'de, D>(deserializer: D) -> Result<Option<RefreshOptions>, D::Error>
where
D: Deserializer<'de>,
{
match BoolOrRefresh::deserialize(deserializer)? {
BoolOrRefresh::Refresh(refresh) => Ok(Some(refresh)),
BoolOrRefresh::Bool(true) => Ok(Some(Default::default())),
BoolOrRefresh::Bool(false) => Ok(None),
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,130 @@
use swc_common::{collections::AHashSet, Spanned, SyntaxContext, DUMMY_SP};
use swc_ecma_ast::*;
use swc_ecma_utils::ExprFactory;
use swc_ecma_visit::{noop_visit_type, Visit, VisitWith};
pub fn is_builtin_hook(name: &Ident) -> bool {
matches!(
name.sym.as_ref(),
"useState"
| "useReducer"
| "useEffect"
| "useLayoutEffect"
| "useMemo"
| "useCallback"
| "useRef"
| "useContext"
| "useImperativeHandle"
| "useDebugValue"
)
}
pub fn is_body_arrow_fn(body: &BlockStmtOrExpr) -> bool {
if let BlockStmtOrExpr::Expr(body) = body {
body.is_arrow()
} else {
false
}
}
fn assert_hygiene(e: &Expr) {
if !cfg!(debug_assertions) {
return;
}
if let Expr::Ident(i) = e {
if i.span.ctxt == SyntaxContext::empty() {
panic!("`{}` should be resolved", i)
}
}
}
pub fn make_assign_stmt(handle: Ident, expr: Box<Expr>) -> Expr {
assert_hygiene(&expr);
Expr::Assign(AssignExpr {
span: expr.span(),
op: op!("="),
left: PatOrExpr::Pat(handle.into()),
right: expr,
})
}
pub fn make_call_stmt(handle: Ident) -> Stmt {
Stmt::Expr(ExprStmt {
span: DUMMY_SP,
expr: Box::new(make_call_expr(handle)),
})
}
pub fn make_call_expr(handle: Ident) -> Expr {
Expr::Call(CallExpr {
span: DUMMY_SP,
callee: handle.as_callee(),
args: Vec::new(),
type_args: None,
})
}
pub fn is_import_or_require(expr: &Expr) -> bool {
match expr {
Expr::Call(CallExpr {
callee: Callee::Expr(expr),
..
}) => {
if let Expr::Ident(ident) = expr.as_ref() {
ident.sym.contains("require")
} else {
false
}
}
Expr::Call(CallExpr {
callee: Callee::Import(_),
..
}) => true,
_ => false,
}
}
pub struct UsedInJsx(AHashSet<Id>);
impl Visit for UsedInJsx {
noop_visit_type!();
fn visit_call_expr(&mut self, n: &CallExpr) {
n.visit_children_with(self);
if let Callee::Expr(expr) = &n.callee {
let ident = match expr.as_ref() {
Expr::Ident(ident) => ident,
Expr::Member(MemberExpr {
prop: MemberProp::Ident(ident),
..
}) => ident,
_ => return,
};
if matches!(
ident.sym.as_ref(),
"createElement" | "jsx" | "jsxDEV" | "jsxs"
) {
if let Some(ExprOrSpread { expr, .. }) = n.args.get(0) {
if let Expr::Ident(ident) = expr.as_ref() {
self.0.insert(ident.to_id());
}
}
}
}
}
fn visit_jsx_opening_element(&mut self, n: &JSXOpeningElement) {
if let JSXElementName::Ident(ident) = &n.name {
self.0.insert(ident.to_id());
}
}
}
pub fn collect_ident_in_jsx<V: VisitWith<UsedInJsx>>(item: &V) -> AHashSet<Id> {
let mut visitor = UsedInJsx(AHashSet::default());
item.visit_with(&mut visitor);
visitor.0
}