//! Turn a JavaScript AST, coming from MD(X), into a component. //! //! Port of , //! by the same author. extern crate swc_ecma_ast; use crate::test_utils::{ hast_util_to_swc::Program, swc_utils::{bytepos_to_point, prefix_error_with_point, span_to_position}, }; use markdown::{ unist::{Point, Position}, Location, }; /// JSX runtimes (default: `JsxRuntime: Automatic`). #[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, /// Pragma for JSX fragments (used in classic runtime). /// /// Default: `React.Fragment`. pub pragma_frag: Option, /// Where to import the identifier of `pragma` from (used in classic runtime). /// /// Default: `react`. pub pragma_import_source: Option, /// Place to import automatic JSX runtimes from (used in automatic runtime). /// /// Default: `react`. pub jsx_import_source: Option, /// JSX runtime to use. /// /// Default: `automatic`. pub jsx_runtime: Option, } 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 mdx_plugin_recma_document( program: &mut Program, options: &Options, location: Option<&Location>, ) -> Result<(), 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(); let mut layout = false; let mut layout_position = None; 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, )) => { if layout { return Err(create_double_layout_message( bytepos_to_point(&decl.span.lo, location).as_ref(), layout_position.as_ref(), )); } layout = true; layout_position = span_to_position(&decl.span, location); match decl.decl { swc_ecma_ast::DefaultDecl::Class(cls) => { replacements.push(create_layout_decl(swc_ecma_ast::Expr::Class(cls))) } swc_ecma_ast::DefaultDecl::Fn(func) => { replacements.push(create_layout_decl(swc_ecma_ast::Expr::Fn(func))) } swc_ecma_ast::DefaultDecl::TsInterfaceDecl(_) => { return Err( prefix_error_with_point( "Cannot use TypeScript interface declarations as default export in MDX files. The default export is reserved for a layout, which must be a component".into(), bytepos_to_point(&decl.span.lo, location).as_ref() ) ); } } } swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::ExportDefaultExpr( expr, )) => { if layout { return Err(create_double_layout_message( bytepos_to_point(&expr.span.lo, location).as_ref(), layout_position.as_ref(), )); } layout = true; layout_position = span_to_position(&expr.span, location); 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 { if layout { return Err(create_double_layout_message( bytepos_to_point(&ident.span.lo, location).as_ref(), layout_position.as_ref(), )); } layout = true; layout_position = span_to_position(&ident.span, location); 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(mut x)) => { // SWC is currently crashing when generating code, w/o source // map, if an actual location is set on this node. x.span = swc_common::DUMMY_SP; // Pass through. replacements.push(swc_ecma_ast::ModuleItem::ModuleDecl( swc_ecma_ast::ModuleDecl::Import(x), )); } swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::ExportDecl(_)) | swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::ExportAll(_)) | 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(()) } /// Create a content component. fn create_mdx_content( expr: Option, has_internal_layout: bool, ) -> Vec { // ```jsx // xxx // ``` 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 ? xxx : _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 xxx // } // ``` 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, }, )))) } /// Create an error message about multiple layouts. fn create_double_layout_message(at: Option<&Point>, previous: Option<&Position>) -> String { prefix_error_with_point( format!( "Cannot specify multiple layouts{}", if let Some(previous) = previous { format!(" (previous: {:?})", previous) } else { "".into() } ), at, ) }