diff options
Diffstat (limited to 'tests/test_utils')
-rw-r--r-- | tests/test_utils/mod.rs | 1 | ||||
-rw-r--r-- | tests/test_utils/to_document.rs | 601 | ||||
-rw-r--r-- | tests/test_utils/to_swc.rs | 53 |
3 files changed, 653 insertions, 2 deletions
diff --git a/tests/test_utils/mod.rs b/tests/test_utils/mod.rs index 87c1e1e..aa1d509 100644 --- a/tests/test_utils/mod.rs +++ b/tests/test_utils/mod.rs @@ -1,4 +1,5 @@ pub mod hast; pub mod swc; +pub mod to_document; pub mod to_hast; pub mod to_swc; diff --git a/tests/test_utils/to_document.rs b/tests/test_utils/to_document.rs new file mode 100644 index 0000000..b44d029 --- /dev/null +++ b/tests/test_utils/to_document.rs @@ -0,0 +1,601 @@ +extern crate swc_common; +extern crate swc_ecma_ast; +use crate::test_utils::to_swc::Program; + +/// JSX runtimes. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum JsxRuntime { + /// Automatic runtime. + /// + /// With the automatic runtime, some module is expected to exist somewhere. + /// That modules is expected to expose a certain API. + /// The compiler adds an import of that module and compiles JSX away to + /// function calls that use that API. + #[default] + Automatic, + /// Classic runtime. + /// + /// With the classic runtime, you define two values yourself in each file, + /// which are expected to work a certain way. + /// The compiler compiles JSX away to function calls using those two values. + Classic, +} + +/// Configuration. +#[derive(Debug, PartialEq, Eq)] +pub struct Options { + /// Pragma for JSX (used in classic runtime). + /// + /// Default: `React.createElement`. + pub pragma: Option<String>, + /// Pragma for JSX fragments (used in classic runtime). + /// + /// Default: `React.Fragment`. + pub pragma_frag: Option<String>, + /// Where to import the identifier of `pragma` from (used in classic runtime). + /// + /// Default: `react`. + pub pragma_import_source: Option<String>, + /// Place to import automatic JSX runtimes from (used in automatic runtime). + /// + /// Default: `react`. + pub jsx_import_source: Option<String>, + /// JSX runtime to use. + /// + /// Default: `automatic`. + pub jsx_runtime: Option<JsxRuntime>, +} + +impl Default for Options { + /// Use the automatic JSX runtime with React. + fn default() -> Self { + Self { + pragma: None, + pragma_frag: None, + pragma_import_source: None, + jsx_import_source: None, + jsx_runtime: Some(JsxRuntime::default()), + } + } +} + +#[allow(dead_code)] +pub fn to_document(mut program: Program, options: &Options) -> Result<Program, String> { + // New body children. + let mut replacements = vec![]; + + // Inject JSX configuration comment. + if let Some(runtime) = &options.jsx_runtime { + let mut pragmas = vec![]; + let react = &"react".into(); + let create_element = &"React.createElement".into(); + let fragment = &"React.Fragment".into(); + + if *runtime == JsxRuntime::Automatic { + pragmas.push("@jsxRuntime automatic".into()); + pragmas.push(format!( + "@jsxImportSource {}", + if let Some(jsx_import_source) = &options.jsx_import_source { + jsx_import_source + } else { + react + } + )); + } else { + pragmas.push("@jsxRuntime classic".into()); + pragmas.push(format!( + "@jsx {}", + if let Some(pragma) = &options.pragma { + pragma + } else { + create_element + } + )); + pragmas.push(format!( + "@jsxFrag {}", + if let Some(pragma_frag) = &options.pragma_frag { + pragma_frag + } else { + fragment + } + )); + } + + if !pragmas.is_empty() { + program.comments.insert( + 0, + swc_common::comments::Comment { + kind: swc_common::comments::CommentKind::Block, + text: pragmas.join(" ").into(), + span: swc_common::DUMMY_SP, + }, + ); + } + } + + // Inject an import in the classic runtime for the pragma (and presumably, + // fragment). + if options.jsx_runtime == Some(JsxRuntime::Classic) { + let pragma = if let Some(pragma) = &options.pragma { + pragma + } else { + "React" + }; + let sym = pragma.split('.').next().expect("first item always exists"); + + replacements.push(swc_ecma_ast::ModuleItem::ModuleDecl( + swc_ecma_ast::ModuleDecl::Import(swc_ecma_ast::ImportDecl { + specifiers: vec![swc_ecma_ast::ImportSpecifier::Named( + swc_ecma_ast::ImportNamedSpecifier { + local: swc_ecma_ast::Ident { + sym: sym.into(), + optional: false, + span: swc_common::DUMMY_SP, + }, + imported: None, + span: swc_common::DUMMY_SP, + is_type_only: false, + }, + )], + src: Box::new(swc_ecma_ast::Str { + value: (if let Some(source) = &options.pragma_import_source { + source.clone() + } else { + "react".into() + }) + .into(), + span: swc_common::DUMMY_SP, + raw: None, + }), + type_only: false, + asserts: None, + span: swc_common::DUMMY_SP, + }), + )); + } + + // Find the `export default`, the JSX expression, and leave the rest as it + // is. + let mut input = program.module.body.split_off(0); + input.reverse(); + // To do: place position in this. + let mut layout = false; + let content = true; + + while let Some(module_item) = input.pop() { + match module_item { + // ```js + // export default props => <>{props.children}</> + // ``` + // + // Treat it as an inline layout declaration. + // + // In estree, the below two are the same node (`ExportDefault`). + swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::ExportDefaultDecl( + decl, + )) => { + // To do: use positional info. + if layout { + return Err("Cannot specify multiple layouts".into()); + } + + // To do: set positional info. + layout = true; + replacements.push(create_layout_decl(match decl.decl { + swc_ecma_ast::DefaultDecl::Class(cls) => swc_ecma_ast::Expr::Class(cls), + swc_ecma_ast::DefaultDecl::Fn(func) => swc_ecma_ast::Expr::Fn(func), + swc_ecma_ast::DefaultDecl::TsInterfaceDecl(_) => { + // To do: improved error? Not sure what a real example of this is? + unreachable!("Cannot use TypeScript interface declarations as default export in MDX") + }, + })); + } + swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::ExportDefaultExpr( + expr, + )) => { + // To do: use positional info. + if layout { + return Err("Cannot specify multiple layouts".into()); + } + + // To do: set positional info. + layout = true; + replacements.push(create_layout_decl(*expr.expr)); + } + // ```js + // export {a, b as c} from 'd' + // export {a, b as c} + // ``` + swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::ExportNamed(mut named_export)) => { + // SWC is currently crashing when generating code, w/o source + // map, if an actual location is set on this node. + named_export.span = swc_common::DUMMY_SP; + + let mut index = 0; + let mut id = None; + + while index < named_export.specifiers.len() { + let mut take = false; + // Note: the `swc_ecma_ast::ExportSpecifier::Default` + // branch of this looks interesting, but as far as I + // understand it *is not* valid ES. + // `export a from 'b'` is a syntax error, even in SWC. + if let swc_ecma_ast::ExportSpecifier::Named(named) = &named_export.specifiers[index] { + if let Some(swc_ecma_ast::ModuleExportName::Ident(ident)) = &named.exported { + if ident.sym.as_ref() == "default" { + // For some reason the AST supports strings + // instead of identifiers. + // Looks like some TC39 proposal. Ignore for now + // and only do things if this is an ID. + if let swc_ecma_ast::ModuleExportName::Ident(ident) = &named.orig { + // To do: use positional info. + if layout { + return Err("Cannot specify multiple layouts".into()); + } + // To do: set positional info. + layout = true; + take = true; + id = Some(ident.clone()); + } + } + } + } + + if take { + named_export.specifiers.remove(index); + } else { + index += 1; + } + } + + if let Some(id) = id { + let source = named_export.src.clone(); + + // If there was just a default export, we can drop the original node. + if !named_export.specifiers.is_empty() { + // Pass through. + replacements.push(swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::ExportNamed(named_export))); + } + + // It’s an `export {x} from 'y'`, so generate an import. + if let Some(source) = source { + replacements.push(swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::Import(swc_ecma_ast::ImportDecl { + specifiers: vec![swc_ecma_ast::ImportSpecifier::Named( + swc_ecma_ast::ImportNamedSpecifier { + local: swc_ecma_ast::Ident { + sym: "MDXLayout".into(), + optional: false, + span: swc_common::DUMMY_SP, + }, + imported: Some(swc_ecma_ast::ModuleExportName::Ident( id)), + span: swc_common::DUMMY_SP, + is_type_only: false, + }, + )], + src: source, + type_only: false, + asserts: None, + span: swc_common::DUMMY_SP, + }))) + } + // It’s an `export {x}`, so generate a variable declaration. + else { + replacements.push(create_layout_decl(swc_ecma_ast::Expr::Ident(id))); + } + } else { + // Pass through. + replacements.push(swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::ExportNamed(named_export))); + } + } + swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::Import(_)) + | swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::ExportDecl(_)) + | swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::ExportAll(_)) + // To do: handle TS things? + | swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::TsImportEquals(_)) + | swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::TsExportAssignment( + _, + )) + | swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::TsNamespaceExport( + _, + )) => { + // Pass through. + replacements.push(module_item); + } + swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Expr(expr_stmt)) => { + match *expr_stmt.expr { + swc_ecma_ast::Expr::JSXElement(elem) => { + replacements.append(&mut create_mdx_content( + Some(swc_ecma_ast::Expr::JSXElement(elem)), + layout, + )); + } + swc_ecma_ast::Expr::JSXFragment(mut frag) => { + // Unwrap if possible. + if frag.children.len() == 1 { + let item = frag.children.pop().unwrap(); + + if let swc_ecma_ast::JSXElementChild::JSXElement(elem) = item { + replacements.append(&mut create_mdx_content( + Some(swc_ecma_ast::Expr::JSXElement(elem)), + layout, + )); + continue; + } + + frag.children.push(item) + } + + replacements.append(&mut create_mdx_content( + Some(swc_ecma_ast::Expr::JSXFragment(frag)), + layout, + )); + } + _ => { + // Pass through. + replacements.push(swc_ecma_ast::ModuleItem::Stmt( + swc_ecma_ast::Stmt::Expr(expr_stmt), + )); + } + } + } + swc_ecma_ast::ModuleItem::Stmt(stmt) => { + replacements.push(swc_ecma_ast::ModuleItem::Stmt(stmt)); + } + } + } + + // Generate an empty component. + if !content { + replacements.append(&mut create_mdx_content(None, layout)); + } + + // ```jsx + // export default MDXContent + // ``` + replacements.push(swc_ecma_ast::ModuleItem::ModuleDecl( + swc_ecma_ast::ModuleDecl::ExportDefaultExpr(swc_ecma_ast::ExportDefaultExpr { + expr: Box::new(swc_ecma_ast::Expr::Ident(swc_ecma_ast::Ident { + sym: "MDXContent".into(), + optional: false, + span: swc_common::DUMMY_SP, + })), + span: swc_common::DUMMY_SP, + }), + )); + + program.module.body = replacements; + + Ok(program) +} + +/// Create a content component. +fn create_mdx_content( + expr: Option<swc_ecma_ast::Expr>, + has_internal_layout: bool, +) -> Vec<swc_ecma_ast::ModuleItem> { + // ```jsx + // <MDXLayout {...props}>xxx</MDXLayout> + // ``` + let mut result = swc_ecma_ast::Expr::JSXElement(Box::new(swc_ecma_ast::JSXElement { + opening: swc_ecma_ast::JSXOpeningElement { + name: swc_ecma_ast::JSXElementName::Ident(swc_ecma_ast::Ident { + sym: "MDXLayout".into(), + optional: false, + span: swc_common::DUMMY_SP, + }), + attrs: vec![swc_ecma_ast::JSXAttrOrSpread::SpreadElement( + swc_ecma_ast::SpreadElement { + dot3_token: swc_common::DUMMY_SP, + expr: Box::new(swc_ecma_ast::Expr::Ident(swc_ecma_ast::Ident { + sym: "props".into(), + optional: false, + span: swc_common::DUMMY_SP, + })), + }, + )], + self_closing: false, + type_args: None, + span: swc_common::DUMMY_SP, + }, + closing: Some(swc_ecma_ast::JSXClosingElement { + name: swc_ecma_ast::JSXElementName::Ident(swc_ecma_ast::Ident { + sym: "MDXLayout".into(), + optional: false, + span: swc_common::DUMMY_SP, + }), + span: swc_common::DUMMY_SP, + }), + // ```jsx + // <_createMdxContent {...props} /> + // ``` + children: vec![swc_ecma_ast::JSXElementChild::JSXElement(Box::new( + swc_ecma_ast::JSXElement { + opening: swc_ecma_ast::JSXOpeningElement { + name: swc_ecma_ast::JSXElementName::Ident(swc_ecma_ast::Ident { + sym: "_createMdxContent".into(), + optional: false, + span: swc_common::DUMMY_SP, + }), + attrs: vec![swc_ecma_ast::JSXAttrOrSpread::SpreadElement( + swc_ecma_ast::SpreadElement { + dot3_token: swc_common::DUMMY_SP, + expr: Box::new(swc_ecma_ast::Expr::Ident(swc_ecma_ast::Ident { + sym: "props".into(), + optional: false, + span: swc_common::DUMMY_SP, + })), + }, + )], + self_closing: true, + type_args: None, + span: swc_common::DUMMY_SP, + }, + closing: None, + children: vec![], + span: swc_common::DUMMY_SP, + }, + ))], + span: swc_common::DUMMY_SP, + })); + + if !has_internal_layout { + // ```jsx + // MDXLayout ? <MDXLayout>xxx</MDXLayout> : _createMdxContent(props) + // ``` + result = swc_ecma_ast::Expr::Cond(swc_ecma_ast::CondExpr { + test: Box::new(swc_ecma_ast::Expr::Ident(swc_ecma_ast::Ident { + sym: "MDXLayout".into(), + optional: false, + span: swc_common::DUMMY_SP, + })), + cons: Box::new(result), + alt: Box::new(swc_ecma_ast::Expr::Call(swc_ecma_ast::CallExpr { + callee: swc_ecma_ast::Callee::Expr(Box::new(swc_ecma_ast::Expr::Ident( + swc_ecma_ast::Ident { + sym: "_createMdxContent".into(), + optional: false, + span: swc_common::DUMMY_SP, + }, + ))), + args: vec![swc_ecma_ast::ExprOrSpread { + spread: None, + expr: Box::new(swc_ecma_ast::Expr::Ident(swc_ecma_ast::Ident { + sym: "props".into(), + optional: false, + span: swc_common::DUMMY_SP, + })), + }], + type_args: None, + span: swc_common::DUMMY_SP, + })), + span: swc_common::DUMMY_SP, + }); + } + + // ```jsx + // function _createMdxContent(props) { + // return xxx + // } + // ``` + let create_mdx_content = swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Decl( + swc_ecma_ast::Decl::Fn(swc_ecma_ast::FnDecl { + ident: swc_ecma_ast::Ident { + sym: "_createMdxContent".into(), + optional: false, + span: swc_common::DUMMY_SP, + }, + declare: false, + function: Box::new(swc_ecma_ast::Function { + params: vec![swc_ecma_ast::Param { + pat: swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent { + id: swc_ecma_ast::Ident { + sym: "props".into(), + optional: false, + span: swc_common::DUMMY_SP, + }, + type_ann: None, + }), + decorators: vec![], + span: swc_common::DUMMY_SP, + }], + decorators: vec![], + body: Some(swc_ecma_ast::BlockStmt { + stmts: vec![swc_ecma_ast::Stmt::Return(swc_ecma_ast::ReturnStmt { + arg: Some(Box::new(expr.unwrap_or({ + swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Null(swc_ecma_ast::Null { + span: swc_common::DUMMY_SP, + })) + }))), + span: swc_common::DUMMY_SP, + })], + span: swc_common::DUMMY_SP, + }), + is_generator: false, + is_async: false, + type_params: None, + return_type: None, + span: swc_common::DUMMY_SP, + }), + }), + )); + + // ```jsx + // function MDXContent(props = {}) { + // return <MDXLayout>xxx</MDXLayout> + // } + // ``` + let mdx_content = swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Decl( + swc_ecma_ast::Decl::Fn(swc_ecma_ast::FnDecl { + ident: swc_ecma_ast::Ident { + sym: "MDXContent".into(), + optional: false, + span: swc_common::DUMMY_SP, + }, + declare: false, + function: Box::new(swc_ecma_ast::Function { + params: vec![swc_ecma_ast::Param { + pat: swc_ecma_ast::Pat::Assign(swc_ecma_ast::AssignPat { + left: Box::new(swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent { + id: swc_ecma_ast::Ident { + sym: "props".into(), + optional: false, + span: swc_common::DUMMY_SP, + }, + type_ann: None, + })), + right: Box::new(swc_ecma_ast::Expr::Object(swc_ecma_ast::ObjectLit { + props: vec![], + span: swc_common::DUMMY_SP, + })), + span: swc_common::DUMMY_SP, + type_ann: None, + }), + decorators: vec![], + span: swc_common::DUMMY_SP, + }], + decorators: vec![], + body: Some(swc_ecma_ast::BlockStmt { + stmts: vec![swc_ecma_ast::Stmt::Return(swc_ecma_ast::ReturnStmt { + arg: Some(Box::new(result)), + span: swc_common::DUMMY_SP, + })], + span: swc_common::DUMMY_SP, + }), + is_generator: false, + is_async: false, + type_params: None, + return_type: None, + span: swc_common::DUMMY_SP, + }), + }), + )); + + vec![create_mdx_content, mdx_content] +} + +/// Create a layout, inside the document. +fn create_layout_decl(expr: swc_ecma_ast::Expr) -> swc_ecma_ast::ModuleItem { + // ```jsx + // const MDXLayout = xxx + // ``` + swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Decl(swc_ecma_ast::Decl::Var(Box::new( + swc_ecma_ast::VarDecl { + kind: swc_ecma_ast::VarDeclKind::Const, + declare: false, + decls: vec![swc_ecma_ast::VarDeclarator { + name: swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent { + id: swc_ecma_ast::Ident { + sym: "MDXLayout".into(), + optional: false, + span: swc_common::DUMMY_SP, + }, + type_ann: None, + }), + init: Some(Box::new(expr)), + span: swc_common::DUMMY_SP, + definite: false, + }], + span: swc_common::DUMMY_SP, + }, + )))) +} diff --git a/tests/test_utils/to_swc.rs b/tests/test_utils/to_swc.rs index 1d25f47..59d60ee 100644 --- a/tests/test_utils/to_swc.rs +++ b/tests/test_utils/to_swc.rs @@ -345,11 +345,44 @@ fn transform_root( node: &hast::Node, _root: &hast::Root, ) -> Result<Option<swc_ecma_ast::JSXElementChild>, String> { - let children = all(context, node)?; + let mut children = all(context, node)?; + let mut queue = vec![]; + let mut nodes = vec![]; + let mut seen = false; + + children.reverse(); + + // Remove initial/final whitespace. + while let Some(child) = children.pop() { + let mut stash = false; + + if let swc_ecma_ast::JSXElementChild::JSXExprContainer(container) = &child { + if let swc_ecma_ast::JSXExpr::Expr(expr) = &container.expr { + if let swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(str)) = (*expr).as_ref() { + if inter_element_whitespace(str.value.as_ref()) { + stash = true; + } + } + } + } + + if stash { + if seen { + queue.push(child); + } + } else { + if !queue.is_empty() { + nodes.append(&mut queue); + } + nodes.push(child); + seen = true; + } + } + // To do: remove whitespace? // To do: return a single child if there is one? Ok(Some(swc_ecma_ast::JSXElementChild::JSXFragment( - create_fragment(children, node), + create_fragment(nodes, node), ))) } @@ -493,6 +526,22 @@ fn position_to_span(position: Option<&Position>) -> swc_common::Span { }) } +fn inter_element_whitespace(value: &str) -> bool { + let bytes = value.as_bytes(); + let mut index = 0; + + while index < bytes.len() { + match bytes[index] { + // To do: form feed. + b'\t' | b'\r' | b'\n' | b' ' => {} + _ => return false, + } + index += 1; + } + + true +} + /// Different kinds of JSX names. enum JsxName<'a> { // `a.b.c` |