diff options
author | Titus Wormer <tituswormer@gmail.com> | 2022-10-12 17:40:01 +0200 |
---|---|---|
committer | Titus Wormer <tituswormer@gmail.com> | 2022-10-12 17:40:01 +0200 |
commit | 54d84a4fd326dcfae871e203d673d55cbf40a279 (patch) | |
tree | 977bc7e7acc017e86d06aa6ee07bda93ba58631f | |
parent | ae9382b7f9466c8df421f0f64850a69a4c658581 (diff) | |
download | markdown-rs-54d84a4fd326dcfae871e203d673d55cbf40a279.tar.gz markdown-rs-54d84a4fd326dcfae871e203d673d55cbf40a279.tar.bz2 markdown-rs-54d84a4fd326dcfae871e203d673d55cbf40a279.zip |
Add internals to compile JSX away (wip)
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | tests/test_utils/mdx.rs | 221 | ||||
-rw-r--r-- | tests/test_utils/mdx_plugin_recma_document.rs | 2 | ||||
-rw-r--r-- | tests/test_utils/mod.rs | 1 | ||||
-rw-r--r-- | tests/xxx_mdx.rs | 154 |
5 files changed, 378 insertions, 1 deletions
@@ -30,6 +30,7 @@ swc_ecma_codegen = "0.127" swc_common = "0.29" swc_ecma_ast = "0.94" swc_ecma_parser = "0.122" +swc_ecma_transforms_react = "0.155" swc_ecma_visit = "0.80" [build-dependencies] diff --git a/tests/test_utils/mdx.rs b/tests/test_utils/mdx.rs new file mode 100644 index 0000000..28238d0 --- /dev/null +++ b/tests/test_utils/mdx.rs @@ -0,0 +1,221 @@ +use crate::test_utils::{ + hast_util_to_swc::hast_util_to_swc, + mdast_util_to_hast::mdast_util_to_hast, + mdx_plugin_recma_document::{mdx_plugin_recma_document, Options as DocumentOptions}, + mdx_plugin_recma_jsx_rewrite::{mdx_plugin_recma_jsx_rewrite, Options as RewriteOptions}, + swc::{parse_esm, parse_expression, serialize}, +}; +use micromark::{micromark_to_mdast, Constructs, Location, ParseOptions}; + +pub use super::mdx_plugin_recma_document::JsxRuntime; + +use swc_common::comments::{Comments, SingleThreadedComments}; +// use swc_ecma_transforms_react::{jsx, Options as JsxBuildOptions}; +// use swc_ecma_visit::VisitMutWith; + +// To do: use `Format`. +/// Format the file is in (default: `Format::Detect`). +#[allow(dead_code)] +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub enum Format { + /// Use `Format::Markdown` for files with an extension in `md_extensions` + /// and `Format::Mdx` otherwise. + #[default] + Detect, + /// Treat file as MDX. + Mdx, + /// Treat file as plain vanilla markdown. + Markdown, +} + +// To do: use `OutputFormat`. +/// Output format to generate (default: `OutputFormat::Program`). +#[allow(dead_code)] +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub enum OutputFormat { + /// The `Program` format will use import statements to import the JSX + /// runtime (and optionally provider) and use an export statement to yield + /// the `MDXContent` component. + #[default] + Program, + /// The `FunctionBody` format will get the JSX runtime (and optionally + /// provider) from `arguments[0]`, rewrite export statements, and use a + /// return statement to yield what was exported. + /// Normally, this output format will throw on `import` (and + /// `export … from`) statements, but you can support them by setting + /// `options.useDynamicImport`. + FunctionBody, +} + +/// Configuration (optional). +#[derive(Clone, Debug, Default)] +pub struct Options { + // /// List of markdown extensions, with dot. + // /// + // /// Default: `vec![".md".into(), ".markdown".into(), ".mdown".into(), ".mkdn".into(), ".mkd".into(), ".mdwn".into(), ".mkdown".into(), ".ron".into()]`. + // pub md_extensions: Option<Vec<String>>, + // /// List of MDX extensions, with dot. + // /// + // /// Default: `vec![".mdx".into()]`. + // pub mdx_extensions: Option<Vec<String>>, + // /// Format the file is in (default: `Format::Detect`). + // pub format: Format, + // /// To do: support `output_format: FunctionBody + // /// Output format to generate (default: `OutputFormat::Program`). + // /// + // /// In most cases `OutputFormat::Program` should be used, as it results in a + // /// whole program. + // /// `OutputFormat::FunctionBody` can be used to compile to code that can be + // /// `eval`ed. + // /// In some cases, you might want to do that, such as when compiling on the + // /// server and running on the client. + // pub output_format: OutputFormat, + // /// Whether to compile to dynamic import expressions (default: + // /// `false`). + // /// + // /// This option applies when `options.output_format` is + // /// `OutputFormat::FunctionBody`. + // /// + // /// This project can turn import statements (`import x from 'y'`) into + // /// dynamic imports (`const {x} = await import('y')`). + // /// This is useful because import statements only work at the top level of + // /// JavaScript modules, whereas `import()` is available inside function + // /// bodies. + // /// + // /// When you turn `use_dynamic_import` on, you should probably set + // /// `options.base_url` too. + // pub use_dynamic_import: bool, + // /// Resolve `import`s (and `export … from`, `import.meta.url`) from this + // /// URL (default: `None`, example: `Some("https://example.com/".into())`). + // /// + // /// Relative specifiers are non-absolute URLs that start with `/`, `./`, or + // /// `../`. + // /// For example: `/index.js`, `./folder/file.js`, or `../main.js`. + // /// + // /// This option is useful when code will run in a different place. + // /// One example is when `.mdx` files are in path *a* but compiled to path + // /// *b* and imports should run relative the path *b*. + // /// Another example is when evaluating code, whether in Node or a browser. + // pub base_url: Option<String>, + /// Whether to add extra information to error messages in generated code + /// (default: `false`). + pub development: bool, + + // To do: some alternative to generate source maps. + // SourceMapGenerator + /// Place to import a provider from (default: `None`, example: + /// `Some("@mdx-js/react").into()`). + /// + /// Useful for runtimes that support context (React, Preact). + /// The provider must export a `useMDXComponents`, which is called to + /// access an object of components. + pub provider_import_source: Option<String>, + + /// Whether to keep JSX (default: `false`). + /// + /// The default is to compile JSX away so that the resulting file is + /// immediately runnable. + pub jsx: bool, + + /// JSX runtime to use (default: `Some(JsxRuntime::Automatic)`). + /// + /// The classic runtime compiles to calls such as `h('p')`, the automatic + /// runtime compiles to + /// `import _jsx from '$importSource/jsx-runtime'\n_jsx('p')`. + pub jsx_runtime: Option<JsxRuntime>, + + /// Place to import automatic JSX runtimes from (`Option<String>`, default: + /// `Some("react".into())`). + /// + /// When in the automatic runtime, this is used to define an import for + /// `_Fragment`, `_jsx`, and `_jsxs`. + pub jsx_import_source: Option<String>, + + /// Pragma for JSX (default: `Some("React.createElement".into())`). + /// + /// When in the classic runtime, this is used as an identifier for function + /// calls: `<x />` to `React.createElement('x')`. + /// + /// You should most probably define `pragma_frag` and `pragma_import_source` + /// too when changing this. + pub pragma: Option<String>, + + /// Pragma for JSX fragments (default: `Some("React.Fragment".into())`). + /// + /// When in the classic runtime, this is used as an identifier for + /// fragments: `<>` to `React.createElement(React.Fragment)`. + /// + /// You should most probably define `pragma` and `pragma_import_source` + /// too when changing this. + pub pragma_frag: Option<String>, + + /// Where to import the identifier of `pragma` from (default: + /// `Some("react".into())`). + /// + /// When in the classic runtime, this is used to import the `pragma` + /// function. + /// To illustrate with an example: when `pragma` is `"a.b"` and + /// `pragma_import_source` is `"c"`, the following will be generated: + /// `import a from 'c'`. + pub pragma_import_source: Option<String>, +} + +#[allow(dead_code)] +pub fn mdx(value: &str, filepath: Option<String>, options: &Options) -> Result<String, String> { + let parse_options = ParseOptions { + constructs: Constructs::mdx(), + mdx_esm_parse: Some(Box::new(parse_esm)), + mdx_expression_parse: Some(Box::new(parse_expression)), + ..ParseOptions::default() + }; + let document_options = DocumentOptions { + pragma: options.pragma.clone(), + pragma_frag: options.pragma_frag.clone(), + pragma_import_source: options.pragma_import_source.clone(), + jsx_import_source: options.jsx_import_source.clone(), + jsx_runtime: options.jsx_runtime, + }; + let rewrite_options = RewriteOptions { + development: options.development, + provider_import_source: options.provider_import_source.clone(), + }; + + let location = Location::new(value.as_bytes()); + let mdast = micromark_to_mdast(value, &parse_options)?; + let hast = mdast_util_to_hast(&mdast); + let mut program = hast_util_to_swc(&hast, filepath, Some(&location))?; + mdx_plugin_recma_document(&mut program, &document_options, Some(&location))?; + mdx_plugin_recma_jsx_rewrite(&mut program, &rewrite_options, Some(&location)); + + // Add our comments. + let comments = SingleThreadedComments::default(); + + for c in program.comments { + comments.add_leading(c.span.lo, c); + } + + println!("comments for swc: {:?}", comments); + + if !options.jsx { + // let cm = Lrc::new(SourceMap::new(FilePathMapping::empty())); + // let build_options = JsxBuildOptions { + // next: Some(true), + // throw_if_namespace: Some(false), + // development: Some(options.development), + // use_spread: Some(true), + // refresh: None, + // runtime: None, + // import_source: None, + // pragma: None, + // pragma_frag: None, + // use_builtins: None, + // }; + // let mark = Mark::fresh(Mark::default()); + // program + // .module + // .visit_mut_with(&mut jsx(cm, Some(comments), build_options, mark)); + } + + // To do: include comments. + Ok(serialize(&program.module)) +} diff --git a/tests/test_utils/mdx_plugin_recma_document.rs b/tests/test_utils/mdx_plugin_recma_document.rs index 5c0d423..4c35b7a 100644 --- a/tests/test_utils/mdx_plugin_recma_document.rs +++ b/tests/test_utils/mdx_plugin_recma_document.rs @@ -13,7 +13,7 @@ use micromark::{ Location, }; -/// JSX runtimes. +/// JSX runtimes (default: `JsxRuntime: Automatic`). #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum JsxRuntime { /// Automatic runtime. diff --git a/tests/test_utils/mod.rs b/tests/test_utils/mod.rs index 8d1f144..de203c1 100644 --- a/tests/test_utils/mod.rs +++ b/tests/test_utils/mod.rs @@ -1,6 +1,7 @@ pub mod hast; pub mod hast_util_to_swc; pub mod mdast_util_to_hast; +pub mod mdx; pub mod mdx_plugin_recma_document; pub mod mdx_plugin_recma_jsx_rewrite; pub mod swc; diff --git a/tests/xxx_mdx.rs b/tests/xxx_mdx.rs new file mode 100644 index 0000000..59125b3 --- /dev/null +++ b/tests/xxx_mdx.rs @@ -0,0 +1,154 @@ +extern crate micromark; +extern crate swc_common; +extern crate swc_ecma_ast; +extern crate swc_ecma_codegen; +mod test_utils; +use pretty_assertions::assert_eq; +use test_utils::mdx::{mdx, JsxRuntime, Options}; + +#[test] +fn mdx_test() -> Result<(), String> { + // To do: JSX should be compiled away. + assert_eq!( + mdx("", Some("example.mdx".into()), &Options::default())?, + "function _createMdxContent(props) { + return <></>; +} +function MDXContent(props = {}) { + const { wrapper: MDXLayout } = props.components || {}; + return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); +} +export default MDXContent; +", + "should work", + ); + + // To do: JSX should be compiled away. + assert_eq!( + mdx("<A />", Some("example.mdx".into()), &Options { + development: true, + ..Default::default() + })?, + "function _createMdxContent(props) { + const { A } = props.components || {}; + if (!A) _missingMdxReference(\"A\", true, \"1:1-1:6\"); + return <A />; +} +function MDXContent(props = {}) { + const { wrapper: MDXLayout } = props.components || {}; + return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); +} +export default MDXContent; +function _missingMdxReference(id, component, place) { + throw new Error(\"Expected \" + (component ? \"component\" : \"object\") + \" `\" + id + \"` to be defined: you likely forgot to import, pass, or provide it.\" + (place ? \"\\nIt’s referenced in your code at `\" + place + \"` in `example.mdx`\" : \"\")); +} +", + "should support `options.development: true`", + ); + + // To do: JSX should be compiled away. + assert_eq!( + mdx("<A />", Some("example.mdx".into()), &Options { + provider_import_source: Some("@mdx-js/react".into()), + ..Default::default() + })?, + "import { useMDXComponents as _provideComponents } from \"@mdx-js/react\"; +function _createMdxContent(props) { + const { A } = Object.assign({}, _provideComponents(), props.components); + if (!A) _missingMdxReference(\"A\", true); + return <A />; +} +function MDXContent(props = {}) { + const { wrapper: MDXLayout } = Object.assign({}, _provideComponents(), props.components); + return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _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 `options.provider_import_source`", + ); + + assert_eq!( + mdx("", Some("example.mdx".into()), &Options { + jsx: true, + ..Default::default() + })?, + "function _createMdxContent(props) { + return <></>; +} +function MDXContent(props = {}) { + const { wrapper: MDXLayout } = props.components || {}; + return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); +} +export default MDXContent; +", + "should support `options.jsx: true`", + ); + + // To do: JSX should be compiled away. + // To do: should use calls of `React.createElement` / `React.Fragment`. + assert_eq!( + mdx("", Some("example.mdx".into()), &Options { + jsx_runtime: Some(JsxRuntime::Classic), + ..Default::default() + })?, + "import { React } from \"react\"; +function _createMdxContent(props) { + return <></>; +} +function MDXContent(props = {}) { + const { wrapper: MDXLayout } = props.components || {}; + return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); +} +export default MDXContent; +", + "should support `options.jsx_runtime: JsxRuntime::Classic`", + ); + + // To do: JSX should be compiled away. + // To do: should import `_jsx` and such. + // To do: should use calls of `_jsx`, etc. + assert_eq!( + mdx("", Some("example.mdx".into()), &Options { + jsx_import_source: Some("preact".into()), + ..Default::default() + })?, + "function _createMdxContent(props) { + return <></>; +} +function MDXContent(props = {}) { + const { wrapper: MDXLayout } = props.components || {}; + return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); +} +export default MDXContent; +", + "should support `options.jsx_import_source: Some(\"preact\".into())`", + ); + + // To do: JSX should be compiled away. + // To do: should use calls of `a.b`, symbol of `a.c`. + assert_eq!( + mdx("", Some("example.mdx".into()), &Options { + jsx_runtime: Some(JsxRuntime::Classic), + pragma: Some("a.b".into()), + pragma_frag: Some("a.c".into()), + pragma_import_source: Some("d".into()), + ..Default::default() + })?, + "import { a } from \"d\"; +function _createMdxContent(props) { + return <></>; +} +function MDXContent(props = {}) { + const { wrapper: MDXLayout } = props.components || {}; + return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); +} +export default MDXContent; +", + "should support `options.pragma`, `options.pragma_frag`, `options.pragma_import_source`", + ); + + Ok(()) +} |