From 4a861dcb4a596d8e862a328f053d6c6a53fad76b Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Thu, 20 Oct 2022 18:43:19 +0200 Subject: [PATCH] Add more tests on rewriting JSX to accept components --- src/mdx_plugin_recma_jsx_rewrite.rs | 441 +++++++++++++++++++++------- src/swc_util_build_jsx.rs | 21 +- src/swc_utils.rs | 51 ++++ 3 files changed, 403 insertions(+), 110 deletions(-) diff --git a/src/mdx_plugin_recma_jsx_rewrite.rs b/src/mdx_plugin_recma_jsx_rewrite.rs index 9cf4a97..f9118de 100644 --- a/src/mdx_plugin_recma_jsx_rewrite.rs +++ b/src/mdx_plugin_recma_jsx_rewrite.rs @@ -10,8 +10,8 @@ use crate::swc_utils::{ create_binary_expression, create_bool_expression, create_call_expression, create_ident, create_ident_expression, create_member, create_member_expression_from_str, create_member_prop_from_str, create_object_expression, create_prop_name, create_str, - create_str_expression, is_identifier_name, is_literal_name, position_to_string, - span_to_position, + create_str_expression, is_identifier_name, is_literal_name, jsx_member_to_parts, + position_to_string, span_to_position, }; use markdown::{unist::Position, Location}; use swc_common::util::take::Take; @@ -87,6 +87,31 @@ enum Func<'a> { Arrow(&'a mut ArrowExpr), } +/// Non-literal reference. +#[derive(Debug, Default, Clone)] +struct Reference { + /// Name. + /// + /// ```jsx + /// "a.b.c" + /// "A" + /// ``` + name: String, + /// Component or not. + component: bool, + /// Positional info + position: Option, +} + +/// Alias. +#[derive(Debug, Default, Clone)] +struct Alias { + /// Unsafe. + original: String, + /// Safe. + safe: String, +} + /// Info for a function scope. #[derive(Debug, Default, Clone)] struct Info { @@ -99,22 +124,10 @@ struct Info { /// Used literals (``). tags: Vec, /// List of JSX identifiers of literal tags that are not valid JS - /// identifiers in the shape of `Vec<(invalid, valid)>`. - /// - /// Example: - /// - /// ```rust ignore - /// vec![("a-b".into(), "_component0".into())] - /// ``` - aliases: Vec<(String, String)>, - /// Non-literal references in the shape of `Vec<(name, is_component)>`. - /// - /// Example: - /// - /// ```rust ignore - /// vec![("a".into(), false), ("a.b".into(), true)] - /// ``` - references: Vec<(String, bool, Option)>, + /// identifiers. + aliases: Vec, + /// Non-literal references (components and objects). + references: Vec, } /// Scope (block or function/global). @@ -306,18 +319,17 @@ impl<'a> State<'a> { while let Some(key) = actual.pop() { // `wrapper: MDXLayout` if key == "MDXLayout" { - let pat = Pat::Ident(BindingIdent { + let binding = BindingIdent { id: create_ident(&key), type_ann: None, - }); + }; let prop = KeyValuePatProp { key: create_prop_name("wrapper"), - value: Box::new(pat), + value: Box::new(Pat::Ident(binding)), }; props.push(ObjectPatProp::KeyValue(prop)); - } - // `MyComponent` - else { + } else { + // `MyComponent` let prop = AssignPatProp { key: create_ident(&key), value: None, @@ -364,16 +376,16 @@ impl<'a> State<'a> { if !info.aliases.is_empty() { info.aliases.reverse(); - while let Some((id, name)) = info.aliases.pop() { + while let Some(alias) = info.aliases.pop() { let declarator = VarDeclarator { span: swc_common::DUMMY_SP, name: Pat::Ident(BindingIdent { - id: create_ident(&name), + id: create_ident(&alias.safe), type_ann: None, }), init: Some(Box::new(Expr::Member(create_member( create_ident_expression("_components"), - create_member_prop_from_str(&id), + create_member_prop_from_str(&alias.original), )))), definite: false, }; @@ -408,22 +420,22 @@ impl<'a> State<'a> { // if (!a) _missingMdxReference("a", false); // if (!a.b) _missingMdxReference("a.b", true); // ``` - for (id, component, position) in info.references { + for reference in info.references { self.create_error_helper = true; let mut args = vec![ ExprOrSpread { spread: None, - expr: Box::new(create_str_expression(&id)), + expr: Box::new(create_str_expression(&reference.name)), }, ExprOrSpread { spread: None, - expr: Box::new(create_bool_expression(component)), + expr: Box::new(create_bool_expression(reference.component)), }, ]; // Add the source location if it exists and if `development` is on. - if let Some(position) = position.as_ref() { + if let Some(position) = reference.position.as_ref() { if self.development { args.push(ExprOrSpread { spread: None, @@ -432,19 +444,21 @@ impl<'a> State<'a> { } } + let test = Expr::Unary(UnaryExpr { + op: UnaryOp::Bang, + arg: Box::new(create_member_expression_from_str(&reference.name)), + span: swc_common::DUMMY_SP, + }); + let cons = Stmt::Expr(ExprStmt { + span: swc_common::DUMMY_SP, + expr: Box::new(create_call_expression( + Callee::Expr(Box::new(create_ident_expression("_missingMdxReference"))), + args, + )), + }); let statement = Stmt::If(IfStmt { - test: Box::new(Expr::Unary(UnaryExpr { - op: UnaryOp::Bang, - arg: Box::new(create_member_expression_from_str(&id)), - span: swc_common::DUMMY_SP, - })), - cons: Box::new(Stmt::Expr(ExprStmt { - span: swc_common::DUMMY_SP, - expr: Box::new(create_call_expression( - Callee::Expr(Box::new(create_ident_expression("_missingMdxReference"))), - args, - )), - })), + test: Box::new(test), + cons: Box::new(cons), alt: None, span: swc_common::DUMMY_SP, }); @@ -455,23 +469,11 @@ impl<'a> State<'a> { if !statements.is_empty() { let mut body: &mut BlockStmt = match func { Func::Expr(expr) => { - if expr.function.body.is_none() { - let block = BlockStmt { - stmts: vec![], - span: swc_common::DUMMY_SP, - }; - expr.function.body = Some(block); - } + // Always exists if we have components in it. expr.function.body.as_mut().unwrap() } Func::Decl(decl) => { - if decl.function.body.is_none() { - let block = BlockStmt { - stmts: vec![], - span: swc_common::DUMMY_SP, - }; - decl.function.body = Some(block); - } + // Always exists if we have components in it. decl.function.body.as_mut().unwrap() } Func::Arrow(arr) => { @@ -588,7 +590,7 @@ impl<'a> State<'a> { } // `{key}` or `{key = value}` ObjectPatProp::Assign(d) => { - self.add_id(d.key.to_string(), block); + self.add_id(d.key.sym.to_string(), block); } } index += 1; @@ -613,47 +615,35 @@ impl<'a> VisitMut for State<'a> { match &node.opening.name { // ``, ``, ``. JSXElementName::JSXMemberExpr(d) => { - let mut ids = vec![]; - let mut mem = d; - loop { - ids.push(mem.prop.sym.to_string()); - match &mem.obj { - JSXObject::Ident(d) => { - ids.push(d.sym.to_string()); - break; - } - JSXObject::JSXMemberExpr(d) => { - mem = d; - } - } - } - ids.reverse(); - let primary_id = ids.first().unwrap().clone(); - let in_scope = self.in_scope(&primary_id); + let ids = jsx_member_to_parts(d); + let primary_id = (*ids[0]).to_string(); - if !in_scope { + if !self.in_scope(&primary_id) { let info_mut = self.current_top_level_info_mut().unwrap(); let mut index = 1; while index <= ids.len() { - let full_id = ids[0..index].join("."); + let name = ids[0..index].join("."); let component = index == ids.len(); let reference = - info_mut.references.iter_mut().find(|d| d.0 == full_id); + info_mut.references.iter_mut().find(|d| d.name == name); + if let Some(reference) = reference { if component { - reference.1 = true; + reference.component = true; } } else { - info_mut.references.push(( - full_id, + let reference = Reference { + name, component, - position.clone(), - )); + position: position.clone(), + }; + info_mut.references.push(reference); } index += 1; } + // We only need to get the first ID. if !info_mut.objects.contains(&primary_id) { info_mut.objects.push(primary_id); } @@ -684,12 +674,15 @@ impl<'a> VisitMut for State<'a> { JSXElementName::JSXMemberExpr(member) } } else { - let reference = info.aliases.iter().find(|d| d.0 == id); - let name = if let Some(reference) = reference { - reference.1.clone() + let alias = info.aliases.iter().find(|d| d.original == id); + let name = if let Some(alias) = alias { + alias.safe.clone() } else { let name = format!("_component{}", info.aliases.len()); - invalid = Some((id.clone(), name.clone())); + invalid = Some(Alias { + original: id.clone(), + safe: name.clone(), + }); name }; @@ -712,9 +705,9 @@ impl<'a> VisitMut for State<'a> { node.opening.name = name; } else { + // A component. let mut is_layout = false; - // The MDXLayout is wrapped in a if let Some(name) = &info.name { if name == "MDXContent" && id == "MDXLayout" { is_layout = true; @@ -726,11 +719,17 @@ impl<'a> VisitMut for State<'a> { if !is_layout { let reference = - info_mut.references.iter_mut().find(|d| d.0 == id); + info_mut.references.iter_mut().find(|d| d.name == id); + if let Some(reference) = reference { - reference.1 = true; + reference.component = true; } else { - info_mut.references.push((id.clone(), true, position)); + let reference = Reference { + name: id.clone(), + component: true, + position, + }; + info_mut.references.push(reference); } } @@ -1138,16 +1137,20 @@ function _missingMdxReference(id, component) { ); assert_eq!( - compile(", , ", &Options::default())?, + compile(", , , , ", &Options::default())?, "function _createMdxContent(props) { const _components = Object.assign({ p: \"p\" - }, props.components), { X , Y } = _components; + }, props.components), { X , Y , a } = _components; if (!X) _missingMdxReference(\"X\", true); if (!X.y) _missingMdxReference(\"X.y\", true); if (!Y) _missingMdxReference(\"Y\", false); if (!Y.Z) _missingMdxReference(\"Y.Z\", true); - return <_components.p >{\", \"}{\", \"}; + if (!a) _missingMdxReference(\"a\", false); + if (!a.b) _missingMdxReference(\"a.b\", true); + if (!a.b.c) _missingMdxReference(\"a.b.c\", false); + if (!a.b.c.d) _missingMdxReference(\"a.b.c.d\", true); + return <_components.p >{\", \"}{\", \"}{\", \"}{\", \"}; } function MDXContent(props = {}) { const { wrapper: MDXLayout } = props.components || {}; @@ -1180,13 +1183,32 @@ export default MDXContent; ); assert_eq!( - compile("# ", &Options::default())?, + compile( + "import * as X from './a.js'\n\n", + &Options::default() + )?, + "import * as X from './a.js'; +function _createMdxContent(props) { + return ; +} +function MDXContent(props = {}) { + const { wrapper: MDXLayout } = props.components || {}; + return MDXLayout ? <_createMdxContent {...props}/> : _createMdxContent(props); +} +export default MDXContent; +", + "should not support passing in a component if one is defined locally (namespace import)", + ); + + assert_eq!( + compile("# , , ", &Options::default())?, "function _createMdxContent(props) { const _components = Object.assign({ h1: \"h1\", - \"a-b\": \"a-b\" - }, props.components), _component0 = _components[\"a-b\"]; - return <_components.h1 ><_component0 />; + \"a-b\": \"a-b\", + \"qwe-rty\": \"qwe-rty\" + }, props.components), _component0 = _components[\"a-b\"], _component1 = _components[\"qwe-rty\"]; + return <_components.h1 ><_component0 />{\", \"}<_component1 />{\", \"}<_component0 />; } function MDXContent(props = {}) { const { wrapper: MDXLayout } = props.components || {}; @@ -1307,6 +1329,28 @@ export default MDXContent; "should not support passing components in locally defined components", ); + assert_eq!( + compile( + "export class A {} + + +", + &Options::default() + )?, + "export class A { +} +function _createMdxContent(props) { + return ; +} +function MDXContent(props = {}) { + const { wrapper: MDXLayout } = props.components || {}; + return MDXLayout ? <_createMdxContent {...props}/> : _createMdxContent(props); +} +export default MDXContent; +", + "should be aware of classes", + ); + assert_eq!( compile( "export function A() { @@ -1341,6 +1385,207 @@ function _missingMdxReference(id, component) { "should support providing components in locally defined components", ); + assert_eq!( + compile( + "export const A = () => ", + &Options { + provider_import_source: Some("x".into()), + ..Options::default() + } + )?, + "import { useMDXComponents as _provideComponents } from \"x\"; +export const A = ()=>{ + const { B } = _provideComponents(); + if (!B) _missingMdxReference(\"B\", true); + return ; +}; +function _createMdxContent(props) { + return <>; +} +function MDXContent(props = {}) { + const { wrapper: MDXLayout } = Object.assign({}, _provideComponents(), props.components); + return MDXLayout ? <_createMdxContent {...props}/> : _createMdxContent(props); +} +export default MDXContent; +function _missingMdxReference(id, component) { + throw new Error(\"Expected \" + (component ? \"component\" : \"object\") + \" `\" + id + \"` to be defined: you likely forgot to import, pass, or provide it.\"); +} +", + "should support providing components in locally defined arrow functions", + ); + + assert_eq!( + compile( + "export function X(x) { + let [A] = x + let [...B] = x + let {C} = x + // let {...D} = x - this currently crashes SWC. + let {_: E} = x + let {F = _} = x; + return <> +}", + &Options { + provider_import_source: Some("x".into()), + ..Options::default() + } + )?, + "import { useMDXComponents as _provideComponents } from \"x\"; +export function X(x) { + const { D , G } = _provideComponents(); + if (!D) _missingMdxReference(\"D\", true); + if (!G) _missingMdxReference(\"G\", true); + let [A] = x; + let [...B] = x; + let { C } = x; + let { _: E } = x; + let { F =_ } = x; + return <>; +} +function _createMdxContent(props) { + return <>; +} +function MDXContent(props = {}) { + const { wrapper: MDXLayout } = Object.assign({}, _provideComponents(), props.components); + return MDXLayout ? <_createMdxContent {...props}/> : _createMdxContent(props); +} +export default MDXContent; +function _missingMdxReference(id, component) { + throw new Error(\"Expected \" + (component ? \"component\" : \"object\") + \" `\" + id + \"` to be defined: you likely forgot to import, pass, or provide it.\"); +} +", + "should support providing components in top-level components, aware of scopes", + ); + + assert_eq!( + compile( + "export function A() { + while (true) { + let B = true; + break; + } + + do { + let B = true; + break; + } while (true) + + for (;;) { + let B = true; + break; + } + + for (a in b) { + let B = true; + break; + } + + for (a of b) { + let B = true; + break; + } + + try { + let B = true; + } catch (B) { + let B = true; + } + + (function () { + let B = true; + })() + + (() => { + let B = true; + })() + + return +}", + &Options { + provider_import_source: Some("x".into()), + ..Options::default() + } + )?, + "import { useMDXComponents as _provideComponents } from \"x\"; +export function A() { + const { B } = _provideComponents(); + if (!B) _missingMdxReference(\"B\", true); + while(true){ + let B = true; + break; + } + do { + let B = true; + break; + }while (true) + for(;;){ + let B = true; + break; + } + for(a in b){ + let B = true; + break; + } + for (a of b){ + let B = true; + break; + } + try { + let B = true; + } catch (B) { + let B = true; + } + (function() { + let B = true; + })()(()=>{ + let B = true; + })(); + return ; +} +function _createMdxContent(props) { + return <>; +} +function MDXContent(props = {}) { + const { wrapper: MDXLayout } = Object.assign({}, _provideComponents(), props.components); + return MDXLayout ? <_createMdxContent {...props}/> : _createMdxContent(props); +} +export default MDXContent; +function _missingMdxReference(id, component) { + throw new Error(\"Expected \" + (component ? \"component\" : \"object\") + \" `\" + id + \"` to be defined: you likely forgot to import, pass, or provide it.\"); +} +", + "should support providing components in top-level components, aware of scopes", + ); + + assert_eq!( + compile( + "export const A = function B() { return }", + &Options { + provider_import_source: Some("x".into()), + ..Options::default() + } + )?, + "import { useMDXComponents as _provideComponents } from \"x\"; +export const A = function B() { + const { C } = _provideComponents(); + if (!C) _missingMdxReference(\"C\", true); + return ; +}; +function _createMdxContent(props) { + return <>; +} +function MDXContent(props = {}) { + const { wrapper: MDXLayout } = Object.assign({}, _provideComponents(), props.components); + return MDXLayout ? <_createMdxContent {...props}/> : _createMdxContent(props); +} +export default MDXContent; +function _missingMdxReference(id, component) { + throw new Error(\"Expected \" + (component ? \"component\" : \"object\") + \" `\" + id + \"` to be defined: you likely forgot to import, pass, or provide it.\"); +} +", + "should support providing components in locally defined function expressions", + ); + assert_eq!( compile( "export function A() { diff --git a/src/swc_util_build_jsx.rs b/src/swc_util_build_jsx.rs index 55452bd..50baad2 100644 --- a/src/swc_util_build_jsx.rs +++ b/src/swc_util_build_jsx.rs @@ -20,8 +20,8 @@ use swc_common::{ use swc_ecma_ast::{ ArrayLit, CallExpr, Callee, Expr, ExprOrSpread, ImportDecl, ImportNamedSpecifier, ImportSpecifier, JSXAttrOrSpread, JSXAttrValue, JSXElement, JSXElementChild, JSXExpr, - JSXExprContainer, JSXFragment, KeyValueProp, Lit, ModuleDecl, ModuleExportName, ModuleItem, - Prop, PropName, PropOrSpread, ThisExpr, + JSXFragment, KeyValueProp, Lit, ModuleDecl, ModuleExportName, ModuleItem, Prop, PropName, + PropOrSpread, ThisExpr, }; use swc_ecma_visit::{noop_visit_mut_type, VisitMut, VisitMutWith}; @@ -211,16 +211,14 @@ impl<'a> State<'a> { let mut result = vec![]; children.reverse(); while let Some(child) = children.pop() { - if let JSXElementChild::JSXSpreadChild(_) = child { - return Err("Spread children not supported in Babel, SWC, or React".into()); - } - match child { - JSXElementChild::JSXExprContainer(JSXExprContainer { - expr: JSXExpr::Expr(expression), - .. - }) => { - result.push(*expression); + JSXElementChild::JSXSpreadChild(_) => { + return Err("Spread children not supported in Babel, SWC, or React".into()); + } + JSXElementChild::JSXExprContainer(container) => { + if let JSXExpr::Expr(expression) = container.expr { + result.push(*expression); + } } JSXElementChild::JSXText(text) => { let value = jsx_text_to_value(text.value.as_ref()); @@ -234,7 +232,6 @@ impl<'a> State<'a> { JSXElementChild::JSXFragment(fragment) => { result.push(self.jsx_fragment_to_expression(fragment)?); } - _ => {} } } diff --git a/src/swc_utils.rs b/src/swc_utils.rs index 50aa771..6fb6d89 100644 --- a/src/swc_utils.rs +++ b/src/swc_utils.rs @@ -504,6 +504,30 @@ pub fn parse_jsx_name(name: &str) -> JsxName { } } +/// Get the identifiers used in a JSX member expression. +/// +/// `Foo.Bar` -> `vec!["Foo", "Bar"]` +pub fn jsx_member_to_parts(node: &JSXMemberExpr) -> Vec<&str> { + let mut parts = vec![]; + let mut member_opt = Some(node); + + while let Some(member) = member_opt { + parts.push(member.prop.sym.as_ref()); + match &member.obj { + JSXObject::Ident(d) => { + parts.push(d.sym.as_ref()); + member_opt = None; + } + JSXObject::JSXMemberExpr(node) => { + member_opt = Some(node); + } + } + } + + parts.reverse(); + parts +} + /// Check if a text value is inter-element whitespace. /// /// See: . @@ -558,4 +582,31 @@ mod tests { fn point_opt_to_string_test() { assert_eq!(point_opt_to_string(None), "0:0", "should support no point"); } + + #[test] + fn jsx_member_to_parts_test() { + assert_eq!( + jsx_member_to_parts(&JSXMemberExpr { + prop: create_ident("a"), + obj: JSXObject::Ident(create_ident("b")) + }), + vec!["b", "a"], + "should support a member with 2 items" + ); + + assert_eq!( + jsx_member_to_parts(&JSXMemberExpr { + prop: create_ident("a"), + obj: JSXObject::JSXMemberExpr(Box::new(JSXMemberExpr { + prop: create_ident("b"), + obj: JSXObject::JSXMemberExpr(Box::new(JSXMemberExpr { + prop: create_ident("c"), + obj: JSXObject::Ident(create_ident("d")) + })) + })) + }), + vec!["d", "c", "b", "a"], + "should support a member with 4 items" + ); + } }