an MDX compiler
npm install xdm[![Build][build-badge]][build]
[![Coverage][coverage-badge]][coverage]
[![Downloads][downloads-badge]][downloads]
[![Size][size-badge]][size]
xdm was a fork (complete rewrite?) that envisioned how mdx-js/mdx could work.
All of xdm has now been ported into mdx-js/mdx making xdm no longer needed.
Use [mdx-js/mdx][mdxjs] instead.
Read the original readme
xdm is an MDX compiler that focusses on two things:
1. Compiling the MDX syntax (markdown + JSX) to JavaScript
2. Making it easier to use the MDX syntax in different places
This is mostly things I wrote for @mdx-js/mdx which are not slated to be
released (soon?) plus some further changes that I think are good ideas (source
maps, ESM only, defaulting to an automatic JSX runtime, no Babel, smallish
browser size, more docs, import/exports in evaluate, esbuild and Rollup
plugins).
There are also some cool experimental features in [👩🔬 Lab][lab]!
Use Node 12 or later.
Then install xdm with either npm or yarn.
[npm][]:
``sh`
npm install xdm
[yarn][]:
`sh`
yarn add xdm
This package is ESM only:
Node 12+ is needed to use it and it must be imported instead of required.
* What is MDX?
* Use
* API
* compile(file, options?)
* compileSync(file, options?)
* evaluate(file, options)
* evaluateSync(file, options)
* run(functionBody, options)
* runSync(functionBody, options)
* createProcessor(options)
* 👩🔬 Lab
* Importing .mdx files directly
* Requiring .mdx files directly
* Importing .md and .mdx files from the web in esbuild
* MDX syntax
* Markdown
* JSX
* ESM
* Expressions
* MDX content
* Components
* Layout
* Integrations
* Bundlers
* Build systems
* Compilers
* Site generators
* Hyperscript implementations (frameworks)
* Runtime libraries
* Guides
* GitHub flavored markdown (GFM)
* Syntax highlighting
* Math
* Frontmatter
* Plugins
* Types
* Differences from @mdx-js/mdx
* Architecture
* Security
* Related
* License
MDX is different things.
The term is sometimes used for a compiler, typically implying @mdx-js/mdx, butmdxc
there are more.
First there was [][mdxc].@mdx-js/mdx
Then came [][mdxjs].mdsvex
There’s also [][mdsvex].
And now there’s xdm too.
Sometimes the term is used for a runtime/helper library.
xdm has no runtime: it’s not needed!
Most often the term is used for the format: markdown + JS(X) (there are some
[caveats][]):
`mdxHello, world!
See?
Most of markdown works!
Those XML-like things are not HTML though: they’re JSX.
Note that there are some differences in how JSX should be authored: for example,
React expects
className, whereas Vue expects class.
See [§ MDX syntax][mdx-syntax] below for more on how the format works.Use
This section describes how to use the API.
See [§ MDX syntax][mdx-syntax] on how the format works.
See [§ Integrations][integrations] on how to use xdm with Babel, esbuild,
Rollup, webpack, etc.
Say we have an MDX document,
example.mdx:`mdx
export const Thing = () => <>World!>Hello,
`First, a rough idea of what the result will be.
The below is not the actual output, but it might help to form a mental model:
`js
/ @jsxRuntime automatic @jsxImportSource react /export const Thing = () => <>World!>
export default function MDXContent() {
return
Hello,
}
`Some observations:
* The output is serialized JavaScript that still needs to be evaluated
* A comment is injected to configure how JSX is handled
* It’s a complete file with import/exports
* A component (
MDXContent) is exportedNow for how to get the actual output.
Add some code in
example.js to compile example.mdx to JavaScript:`js
import {promises as fs} from 'node:fs'
import {compile} from 'xdm'main()
async function main() {
const compiled = await compile(await fs.readFile('example.mdx'))
console.log(String(compiled))
}
`The actual output of running
node example.js is:`js
/ @jsxRuntime automatic @jsxImportSource react /
import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from 'react/jsx-runtime'export const Thing = () => _jsx(_Fragment, {children: 'World!'})
function MDXContent(props = {}) {
let {wrapper: MDXLayout} = props.components || ({})
return MDXLayout
? _jsx(MDXLayout, Object.assign({}, props, {children: _jsx(_createMdxContent, {})}))
: _createMdxContent()
function _createMdxContent() {
let _components = Object.assign({h1: 'h1'}, props.components)
return _jsxs(_components.h1, {children: ['Hello, ', _jsx(Thing, {})]})
}
}
export default MDXContent
`Some more observations:
* JSX is compiled away to function calls and an import of React†
* The content component can be given
{components: {wrapper: MyLayout}} to
wrap the whole content
* The content component can be given {components: {h1: MyComponent}} to use
something else for the heading† xdm is not coupled to React.
You can also use it with Preact, Vue, Emotion,
Theme UI, etc.
See [§ MDX content][mdx-content] below on how to use the result.
API
xdm exports the following identifiers:
[compile][compile],
compileSync,
[evaluate][eval],
evaluateSync,
[run][run],
runSync, and
createProcessor.
There is no default export.xdm/esbuild.js exports a function as the default export that returns an
[esbuild][] plugin.xdm/rollup.js exports a function as the default export that returns a
[Rollup][] plugin.xdm/webpack.cjs exports a [webpack][] loader as the default export.There is also
xdm/esm-loader.js and xdm/register.cjs, see [👩🔬 Lab][lab]
for more info.$3
Compile MDX to JS.
######
fileMDX document to parse (
string, [Buffer][buffer] in UTF-8, [vfile][vfile],
or anything that can be given to vfile).
Example
`js
import {VFile} from 'vfile'
import {compile} from 'xdm'await compile(':)')
await compile(Buffer.from(':-)'))
await compile({path: 'path/to/file.mdx', value: '🥳'})
await compile(new VFile({path: 'path/to/file.mdx', value: '🤭'}))
`######
options.remarkPluginsList of [remark plugins][remark-plugins], presets, and pairs.
Example
`js
import remarkFrontmatter from 'remark-frontmatter' // YAML and such.
import remarkGfm from 'remark-gfm' // Tables, strikethrough, tasklists, literal URLs.await compile(file, {remarkPlugins: [remarkGfm]}) // One plugin.
await compile(file, {remarkPlugins: [[remarkFrontmatter, 'toml']]}) // A plugin with options.
await compile(file, {remarkPlugins: [remarkGfm, remarkFrontmatter]}) // Two plugins.
await compile(file, {remarkPlugins: [[remarkGfm, {singleTilde: false}], remarkFrontmatter]}) // Two plugins, first w/ options.
`######
options.rehypePluginsList of [rehype plugins][rehype-plugins], presets, and pairs.
Example
`js
import rehypeKatex from 'rehype-katex' // Render math with KaTeX.
import remarkMath from 'remark-math' // Support math like $so$.await compile(file, {remarkPlugins: [remarkMath], rehypePlugins: [rehypeKatex]})
await compile(file, {
remarkPlugins: [remarkMath],
// A plugin with options:
rehypePlugins: [[rehypeKatex, {throwOnError: true, strict: true}]]
})
`######
options.recmaPluginsList of recma plugins.
This is a new ecosystem, currently in beta, to transform
esast trees (JavaScript).
######
options.remarkRehypeOptionsOptions to pass through to [
remark-rehype][remark-rehype].
The option allowDangerousHtml will always be set to true and the MDX nodes
are passed through.
In particular, you might want to pass clobberPrefix, footnoteLabel, and
footnoteBackLabel.
Example
`js
await compile({value: '…'}, {remarkRehypeOptions: {clobberPrefix: 'comment-1'}})
`######
options.mdExtensionsList of markdown extensions, with dot (
string[], default: ['.md',).######
options.mdxExtensionsList of MDX extensions, with dot (
string[], default: ['.mdx']).
Has no effect in compile or evaluate, but does affect [esbuild][],
[Rollup][], and the experimental ESM loader + register hook (see [👩🔬
Lab][lab]).######
options.formatFormat the file is in (
'detect' | 'mdx' | 'md', default: 'detect').*
'detect' — use 'markdown' for files with an extension in mdExtensions
and 'mdx' otherwise
* 'mdx' — treat file as [MDX][mdx-syntax]
* 'md' — treat file as plain vanilla markdownThe format cannot be detected if a file is passed without a path or extension:
mdx will be assumed.
So pass a full vfile (with path) or an object with a path.
Example
`js
compile({value: '…'}) // Seen as MDX
compile({value: '…'}, {format: 'md'}) // Seen as markdown
compile({value: '…', path: 'readme.md'}) // Seen as markdown// Please do not use
.md for MDX as other tools won’t know how to handle it.
compile({value: '…', path: 'readme.md'}, {format: 'mdx'}) // Seen as MDX
compile({value: '…', path: 'readme.md'}, {mdExtensions: []}) // Seen as MDX
`This option mostly affects [esbuild][] and [Rollup][] plugins, and the
experimental ESM loader + register hook (see [👩🔬 Lab][lab]), because in those
it affects which files are “registered”:
*
format: 'mdx' registers the extensions in options.mdxExtensions
* format: 'md' registers the extensions in options.mdExtensions
* format: 'detect' registers both lists of extensions######
options.outputFormatOutput format to generate (
'program' | 'function-body', default: 'program').
In most cases 'program' should be used, as it results in a whole program.
Internally, [evaluate][eval] uses outputFormat: 'function-body' to compile
to code that can be evaled with [run][run].
In some cases, you might want to do what evaluate does in separate steps
yourself, such as when compiling on the server and running on the client.The
'program' format will use import statements to import the runtime (and
optionally provider) and use an export statement to yield the MDXContent
component.The
'function-body' format will get the 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][usedynamicimport].
Example
A module
example.js:`js
import {compile} from 'xdm'main('export const no = 3.14\n\n# hi {no}')
async function main(code) {
console.log(String(await compile(code, {outputFormat: 'program'}))) // Default
console.log(String(await compile(code, {outputFormat: 'function-body'})))
}
`…yields:
`js
import {Fragment as _Fragment, jsx as _jsx} from 'react/jsx-runtime'
export const no = 3.14
function MDXContent(props = {}) { / … / }
export default MDXContent
``js
const {Fragment: _Fragment, jsx: _jsx} = arguments[0]
const no = 3.14
function MDXContent(props = {}) { / … / }
return {no, default: MDXContent}
`######
options.useDynamicImportWhether to compile to dynamic import expressions (
boolean, default: false).
This option applies when [options.outputFormat][outputformat] is
'function-body'.xdm 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
useDynamicImport on, you should probably set [options.baseUrl][baseurl] too.
Example
Say we have a couple modules:
`js
// meta.js:
export const title = 'World'// numbers.js:
export const no = 3.14
// example.js:
import {compileSync} from 'xdm'
const code =
import {name} from './meta.js'console.log(String(compileSync(code, {outputFormat: 'function-body', useDynamicImport: true})))
`
…now running node example.js yields:
`js`
const {Fragment: _Fragment, jsx: _jsx, jsxs: _jsxs} = arguments[0]
const {name} = await import('./meta.js')
const {no} = await import('./numbers.js')
function MDXContent(props = {}) { / … / }
return {no, default: MDXContent}
###### options.baseUrl
Resolve relative import (and export … from) from this URL (string?,import.meta.url
example: ).
Relative specifiers are non-absolute URLs that start with /, ./, or .././index.js
For example: , ./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.
Example
Say we have a module example.js:
`js
import {compile} from 'xdm'
main()
async function main() {
const code = 'export {number} from "./data.js"\n\n# hi'
const baseUrl = 'https://a.full/url' // Typically import.meta.url`
console.log(String(await compile(code, {baseUrl})))
}
…now running node example.js yields:
`js`
import {Fragment as _Fragment, jsx as _jsx} from 'react/jsx-runtime'
export {number} from 'https://a.full/data.js'
function MDXContent(props = {}) { / … / }
export default MDXContent
###### options.development
Whether to add extra information to error messages in generated code
(boolean?, default: false).true
The default can be set to in Node.js through environment variables: setNODE_ENV=development.
Example
Say we had some MDX that references a component that can be passed or provided
at runtime:
`mdx`
Note
And a module to evaluate that:
`js
import {promises as fs} from 'node:fs'
import * as runtime from 'react/jsx-runtime'
import {evaluate} from 'xdm'
main()
async function main() {
const path = 'example.mdx'
const value = await fs.readFile(path)
const MDXContent = (await evaluate({path, value}, runtime)).default
console.log(MDXContent())
}
`
Running that would normally (production) yield:
`txtNoteIcon
Error: Expected component to be defined: you likely forgot to import, pass, or provide it.`
at _missingMdxReference (eval at run (…/xdm/lib/run.js:18:10),
at _createMdxContent (eval at run (…/xdm/lib/run.js:18:10),
at MDXContent (eval at run (…/xdm/lib/run.js:18:10),
at main (…/example.js:11:15)
But if we change add development: true to our example:
`diff`
@@ -7,6 +7,6 @@ main()
async function main() {
const path = 'example.mdx'
const value = await fs.readFile(path)
- const MDXContent = (await evaluate({path, value}, runtime)).default
+ const MDXContent = (await evaluate({path, value}, {development: true, ...runtime})).default
console.log(MDXContent({}))
}
And we’d run it again, we’d get:
`txtNoteIcon
Error: Expected component to be defined: you likely forgot to import, pass, or provide it.1:9-1:21
It’s referenced in your code at in example.mdx`
provide it.
at _missingMdxReference (eval at run (…/xdm/lib/run.js:18:10),
at _createMdxContent (eval at run (…/xdm/lib/run.js:18:10),
at MDXContent (eval at run (…/xdm/lib/run.js:18:10),
at main (…/example.js:11:15)
###### options.SourceMapGenerator
The SourceMapGenerator class from [source-map][source-map] (optional).map
When given, the resulting file will have a field set to a source map (in
object form).
Example
Assuming example.mdx from [§ Use][use] exists, then:
`js
import {promises as fs} from 'node:fs'
import {SourceMapGenerator} from 'source-map'
import {compile} from 'xdm'
main()
async function main() {
const file = await compile(
{path: 'example.mdx', value: await fs.readFile('example.mdx')},
{SourceMapGenerator}
)
console.log(file.map)
}
`
…yields:
`js`
{
version: 3,
sources: ['example.mdx'],
names: ['Thing'],
mappings: ';;aAAaA,QAAQ;YAAQ;;;;;;;;iBAE3B',
file: 'example.mdx'
}
###### options.providerImportSource
Place to import a provider from (string?, example: '@mdx-js/react').useMDXComponents
Useful for runtimes that support context (React, Preact).
The provider must export a , which is called to access an
object of components.
Example
If file is the contents of example.mdx from [§ Use][use], then:
`js`
compile(file, {providerImportSource: '@mdx-js/react'})
…yields this difference:
`diff
/ @jsxRuntime automatic @jsxImportSource react /
import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from 'react/jsx-runtime'
+import {useMDXComponents as _provideComponents} from '@mdx-js/react'
export const Thing = () => _jsx(_Fragment, {children: 'World!'})
function MDXContent(props = {}) {
- let {wrapper: MDXLayout} = props.components || ({})
+ let {wrapper: MDXLayout} = Object.assign({}, _provideComponents(), props.components)
return MDXLayout
? _jsx(MDXLayout, Object.assign({}, props, {children: _jsx(_createMdxContent, {})}))
: _createMdxContent()
function _createMdxContent() {
- let _components = Object.assign({h1: 'h1'}, props.components)
+ let _components = Object.assign({h1: 'h1'}, _provideComponents(), props.components)
return _jsxs(_components.h1, {children: ['Hello, ', _jsx(Thing, {})]})
}
}
export default MDXContent
`
###### options.jsx
Whether to keep JSX (boolean?, default: false).
The default is to compile JSX away so that the resulting file is immediately
runnable.
Example
If file is the contents of example.mdx from [§ Use][use], then:
`js`
compile(file, {jsx: true})
…yields this difference:
`diff
/ @jsxRuntime automatic @jsxImportSource react /
-import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from 'react/jsx-runtime'
-export const Thing = () => _jsx(_Fragment, {children: 'World!'})
+export const Thing = () => <>World!>
function MDXContent(props = {}) {
let {wrapper: MDXLayout} = props.components || ({})
return MDXLayout
- ? _jsx(MDXLayout, Object.assign({}, props, {children: _jsx(_createMdxContent, {})}))
+ ?
: _createMdxContent()
function _createMdxContent() {
let _components = Object.assign({h1: 'h1'}, props.components)
- return _jsxs(_components.h1, {children: ['Hello, ', _jsx(Thing, {})]})
+ return <_components.h1>{"Hello, "}
}
}
export default MDXContent
`
###### options.jsxRuntime
JSX runtime to use ('automatic' | 'classic', default: 'automatic').h('p')
The classic runtime compiles to calls such as , the automatic runtimeimport _jsx from '$importSource/jsx-runtime'\n_jsx('p')
compiles to .
Example
If file is the contents of example.mdx from [§ Use][use], then:
`js`
compile(file, {jsxRuntime: 'classic'})
…yields this difference:
`diff
-/ @jsxRuntime automatic @jsxImportSource react /
-import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from 'react/jsx-runtime'
+/ @jsxRuntime classic @jsx React.createElement @jsxFrag React.Fragment /
+import React from 'react'
-export const Thing = () => _jsx(_Fragment, {children: 'World!'})
+export const Thing = () => React.createElement(React.Fragment, null, 'World!')
…
`
###### options.jsxImportSource
Place to import automatic JSX runtimes from (string?, default: 'react').automatic
When in the runtime, this is used to define an import for_Fragment, _jsx, and _jsxs.
Example
If file is the contents of example.mdx from [§ Use][use], then:
`js`
compile(file, {jsxImportSource: 'preact'})
…yields this difference:
`diff`
-/ @jsxRuntime automatic @jsxImportSource react /
-import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from 'react/jsx-runtime'
+/ @jsxRuntime automatic @jsxImportSource preact /
+import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from 'preact/jsx-runtime'
###### options.pragma
Pragma for JSX (string?, default: 'React.createElement').classic
When in the runtime, this is used as an identifier for function calls: to React.createElement('x').
You should most probably define pragmaFrag and pragmaImportSource too when
changing this.
Example
If file is the contents of example.mdx from [§ Use][use], then:
`js`
compile(file, {
jsxRuntime: 'classic',
pragma: 'preact.createElement',
pragmaFrag: 'preact.Fragment',
pragmaImportSource: 'preact/compat'
})
…yields this difference:
`diff
-/ @jsxRuntime classic @jsx React.createElement @jsxFrag React.Fragment /
-import React from 'react'
+/ @jsxRuntime classic @jsx preact.createElement @jsxFrag preact.Fragment /
+import preact from 'preact/compat'
-export const Thing = () => React.createElement(React.Fragment, null, 'World!')
+export const Thing = () => preact.createElement(preact.Fragment, null, 'World!')
…
`
###### options.pragmaFrag
Pragma for JSX fragments (string?, default: 'React.Fragment').classic
When in the runtime, this is used as an identifier for fragments: <>React.createElement(React.Fragment)
to .
See options.pragma for an example.
###### options.pragmaImportSource
Where to import the identifier of pragma from (string?, default: 'react').classic
When in the runtime, this is used to import the pragma function.pragma
To illustrate with an example: when is 'a.b' and pragmaImportSource'c'
is this following will be generated: import a from 'c'.
See options.pragma for an example.
###### Returns
Promise — Promise that resolves to the compiled JS as a [vfile][].
Example
`js
import remarkPresetLintConsistent from 'remark-preset-lint-consistent' // Lint rules to check for consistent markdown.
import {reporter} from 'vfile-reporter'
import {compile} from 'xdm'
main()
async function main() {
const file = await compile('like this or _like this_?', {remarkPlugins: [remarkPresetLintConsistent]})
console.error(reporter(file))
}
`
Yields:
`txt*
1:16-1:27 warning Emphasis should use as a marker emphasis-marker remark-lint
⚠ 1 warning
`
Compile MDX to JS.
Synchronous version of compile.compile
When possible please use the async .
> ☢️ Danger: It’s called evaluate because it evals JavaScript.
Compile and [run][] MDX.
When possible, please use compile, write to a file, and then run with Node orevaluate
bundle with [esbuild][]/[Rollup][]/[webpack][].
But if you trust your content, can work.
Typically, import (or export … from) do not work here.import()
They can be compiled to dynamic by passingoptions.useDynamicImport
[][usedynamicimport].
###### file
See [compile][compile].
###### options
Most options are the same as [compile][compile], with the following
exceptions:
* providerImportSource is replaced by useMDXComponentsjsx
and pragma* options are replaced by jsx, jsxs, and FragmentoutputFormat
* is set to function-body
###### options.jsx
###### options.jsxs
###### options.Fragment
These three options are required.
They come from an automatic JSX runtime that you must import yourself.
Example
`js
import * as runtime from 'react/jsx-runtime'
const {default: Content} = await evaluate('# hi', {...runtime, ...otherOptions})
`
###### options.useMDXComponents
Needed if you want to support a provider.
Example
`js
import * as provider from '@mdx-js/react'
import * as runtime from 'react/jsx-runtime'
const {default: Content} = await evaluate('# hi', {...provider, ...runtime, ...otherOptions})
`
###### Returns
Promise — Promise that resolves to something that looks a bit like adefault
module: an object with a field set to the component and anything else
that was exported from the MDX file available too.
Example
Assuming the contents of example.mdx from [§ Use][use] was in file, then:
`js
import * as runtime from 'react/jsx-runtime'
import {evaluate} from 'xdm'
console.log(await evaluate(file, {...runtime}))
`
…yields:
`js`
{Thing: [Function: Thing], default: [Function: MDXContent]}
> ☢️ Danger: It’s called evaluate because it evals JavaScript.
Compile and run MDX.
Synchronous version of [evaluate][eval].evaluate
When possible please use the async .
> ☢️ Danger: This evals JavaScript.
Run MDX compiled as [options.outputFormat: 'function-body'][outputformat].
###### options
You can pass jsx, jsxs, and Fragment from an automatic JSX runtime asoptions.useMDXComponents
You can pass from a provider in options as well if the MDXoptions.providerImportSource: '#'
is compiled with (the exact value of thiscompile
option doesn’t matter).
All other options have to be passed to instead.
###### Returns
Promise — See evaluate
Example
On the server:
`js
import {compile} from 'xdm'
const code = String(await compile('# hi', {outputFormat: 'function-body'}))
`
On the client:
`js
import {run} from 'xdm'
import * as runtime from 'react/jsx-runtime'
const code = '' // To do: get code from server somehow.
const {default: Content} = await run(code, runtime)
`
…yields:
`js`
[Function: MDXContent]
> ☢️ Danger: This evals JavaScript.
Run MDX.
Synchronous version of [run][run].run
When possible please use the async .
Create a unified processor to compile MDX to JS.
Has the same options as [compile][compile], but returns a configuredprocessor.
Note that format: 'detect' does not work here: only 'md' or 'mdx' are'mdx'
allowed (and is the default).
This section describes experimental features!
These do not adhere to semver and could break at any time!
ESM loaders are an experimental
feature in Node, slated to change.
Still, they let projects “hijack” imports, to do all sorts of fancy things!
xdm comes with experimental support for importing .mdx files withxdm/esm-loader.js
on-the-fly compilation, using :
Assuming example.mdx from [§ Use][use] exists, and our module example.js
looks as follows:
`js
import {renderToStaticMarkup} from 'react-dom/server'
import React from 'react'
import Content from './example.mdx'
console.log(renderToStaticMarkup(React.createElement(Content)))
`
Running that with:
`sh`
node --experimental-loader=xdm/esm-loader.js example.js
…yields:
`html`Hello, World!
To pass options, you can make your own loader, such as this my-loader.js:
`js
import {createLoader} from 'xdm/esm-loader.js'
// Load is for Node 17+, the rest for 12-16.
const {load, getFormat, transformSource} = createLoader(/ Options… /)
export {load, getFormat, transformSource}
`
Which can then be used with node --experimental-loader=./my-loader.js.
Node itself does not yet support multiple loaders, but it is possible to combine
multiple loaders with
@node-loader/core.
require.extensions
is a deprecated feature in Node.
Still, it lets projects “hijack” require calls to do fancy things..mdx
xdm comes with support for requiring files with on-the-flyxdm/register.cjs
evaluation, using :
Assuming example.mdx from [§ Use][use] exists, and our script example.cjs
looks as follows:
`js
const React = require('react')
const {renderToStaticMarkup} = require('react-dom/server')
const Content = require('./example.mdx')
console.log(renderToStaticMarkup(React.createElement(Content)))
`
Running that with:
`sh`
node -r xdm/register.cjs example.cjs
…yields:
`html`Hello, World!
To pass options, you can make your own hook, such as this my-hook.cjs:
`js
'use strict'
const register = require('xdm/lib/integration/require.cjs')
register({/ Options… /})
`
Which can then be used with node -r ./my-hook.cjs.
The register hook uses [evaluateSync][eval].import
That means (and export … from) are not supported when requiring.mdx files.
> ⚠️ Note that this includes remote code in your bundle.
> Make sure you trust it!
> See [§ Security][security] for more info.
When passing allowDangerousRemoteMdx to the esbuild loader, MD(X) and JS fileshttp:
can be imported from and https: urls.index.mdx
Take this file:
`jsx
import Readme from 'https://raw.githubusercontent.com/wooorm/xdm/main/readme.md'
Embed the xdm readme like so:
`
And a module build.js:
`js
import xdm from 'xdm/esbuild.js'
import esbuild from 'esbuild'
await esbuild.build({
entryPoints: ['index.mdx'],
outfile: 'output.js',
format: 'esm',
plugins: [xdm({allowDangerousRemoteMdx: true, / Other options… /})]
})
`
Running that (node build.js) and evaluating output.js (depends on how you
evaluate React stuff) would give:
` Embed the xdm readme like so: MIT © …jsx`xdm
{/ … /}
> Note!
> You don’t have to use this syntax.
> Or use it always.
> With [format][format], you can opt-in gradually or not at all.
The MDX syntax is a mix between markdown and JSX.
Markdown often feels more natural to type than HTML (or JSX) for the common
things (like emphasis, headings).
JSX is an extension to JavaScript that looks like HTML but makes it convenient
to use components (reusable things).
See this description
for a more formal description of the syntax.
This gives us something along the lines of [literate programming][lit].
MDX also gives us an odd mix of two languages: markdown is whitespace sensitive
and forgiving (what you type may not “work”, but it won’t crash) whereas
JavaScript is whitespace insensitive and does crash on typos.
Weirdly enough they combine pretty well!
It’s important to know markdown
(see this cheatsheet and tutorial for help)
and have experience with JavaScript (specifically
JSX) to write (and enjoy writing) MDX.
Some common gotchas with writing MDX are
documented here.
Most of markdown ([CommonMark][]) works:
``mdxHeading (rank 1)
Heading 2
$3
#### 4
##### 5
###### 6
> Block quote
* Unordered
* List
1. Ordered
2. List
A paragraph, introducing a thematic break:
*
`js`
some.code()
a link, an !image, some emphasis,
something strong, and finally a little code().``
Some other features often used with markdown are:
* GFM — autolink literals, strikethrough, tables, tasklists
(see guide below)
* Frontmatter — YAML
(see guide below)
* Math
(see guide below)
* Syntax highlighting
(see guide below)
There are many more things possible by configuring
[remark plugins][remark-plugins] and [rehype plugins][rehype-plugins].
There are also a couple specific remark/rehype/recma plugins that work with
xdm: see [plugins][].
#### Caveats
Some markdown features don’t work in MDX:
`mdx
Indented code works in markdown, but not in MDX:
console.log(1) // this is a paragraph in MDX!
The reason for that is so that you can nicely indent your components.
A different one is “autolinks”:
The reason is that they look a lot like JSX components, and we prefer being unambiguous.
If you want links, use descriptive text.
HTML doesn’t work, because MDX has JSX instead (see next section).
And you must escape less than (<) and opening braces ({) like so: \< or \{.`
More on this is
documented here.
Most of JSX works.
Here’s some that looks a lot like HTML (but is JSX):
`jsHeading!
HTML is a lovely language.
And here is markdown in JSX!
`
You can also use components, but note that you must either define them locally
or pass them in later (see [§ MDX content][mdx-content]):
`js
Or access the thisOne component on the myComponents object:
x={1}
label={'this is a string, not markdown!'}
icon={
/>
`
More on this is
documented here.
To define things from within MDX, use ESM:
`js
import {External} from './some/place.js'
export const Local = props =>
An
`
ESM can also be used for other things:
`js
import {MyChart} from './chart-component.js'
import data from './population.js'
export const pi = 3.14
`
Braces can be used to embed JavaScript expressions in MDX:
`mdx
export const pi = 3.14
Two 🍰 is: {pi * 2}
`
Expressions can be empty or contain just a comment:
`mdx`
{/ A comment! /}
All content (headings, paragraphs, etc) you write are exported as the default
export from a compiled MDX file as a component.
It’s possible to pass props in.
The special prop components is used to determine how to render components.message.mdx
This includes both JSX and markdown syntax.
Say we have a file:
`mdxHello,
Remember when we first met in {props.year}?
`
This file could be imported from JavaScript and passed components like so:
`js
import Message from './message.mdx' // Assumes an integration is used to compile MDX -> JS.
`
You can also change the things that come from markdown:
`jsh1
// Map (# heading) to use h2s.em
h1: 'h2',
// Rewrite s (like so) to i with a red foreground color.'wrapper'
em: (props) => ,
// Pass a layout (using the special key).`
wrapper: ({components, ...props}) => ,
// Pass a component.
Planet: () => 'Venus'
}}
year={1962}
/>
The following keys can be passed in components:
* HTML equivalents for the things you write with markdown (such as h1 for# heading
)†wrapper
* , which defines the layout (but local layout takes precedence)
anything else that is a valid JavaScript identifier (foo,Components
, _, $x, a1) for the things you write with JSX (like
or , note that locally defined components take
precedence)‡
† Normally, in markdown, those are: a, blockquote, br, code, em,h1, h2, h3, h4, h5, h6, hr, img, li, ol, p, pre,strong, and ul.remark-gfm
With [][gfm] (see guide below), youdel
can also use: , section, sup, table, tbody, td, th, thead,tr
and .
Other remark plugins that add support for new constructs and advertise that they
work with rehype, will also work with xdm.
‡ The rules for whether a name in JSX (x in ) is a literal tag name
or not, are as follows:
* If there’s a dot, it’s a member expression ( -> h(a.b))
* Otherwise, if the name is not a valid identifier, it’s a literal ( ->h('a-b')
)
* Otherwise, if it starts with a lowercase, it’s a literal ( -> h('a'))
* Otherwise, it’s an identifier ( -> h(A))
Layouts are components that wrap the whole content.
They can be defined from within MDX using a default export:
`js
export default function Layout({children}) {
return
}
All the things.
`
The layout can also be imported and then exported with an export … from:
`js`
export {Layout as default} from './components.js'
The layout can also be passed as components.wrapper (but a local one takes
precedence).
#### esbuild
Install xdm and use xdm/esbuild.js.build
Add something along these lines to your call:
`js
import xdm from 'xdm/esbuild.js'
import esbuild from 'esbuild'
await esbuild.build({
entryPoints: ['index.mdx'],
outfile: 'output.js',
format: 'esm',
plugins: [xdm({/ Options… /})]
})
`
esbuild takes care of turning modern JavaScript features into syntax that works
wherever you want it to.
No Babel needed.
See esbuild’s docs for more info.
options are the same as from [compile][compile] with the addition of:
###### options.allowDangerousRemoteMdx
Whether to allow importing from http: and https: URLs (boolean, default:false)..md
See [§ Importing and .mdx files from the web in
esbuild][import-from-web].
> ⚠️ Note that this evaluates any JavaScript and MDX found over the wire!
#### Rollup
Install xdm and use xdm/rollup.js.rollup.config.js
Add something along these lines to your :
`js
import path from 'node:path'
import xdm from 'xdm/rollup.js'
export default {
// …
plugins: [
// …
xdm({/ Options… /})
]
}
`
If you use modern JavaScript features you might want to use Babel through
@rollup/plugin-babel
to compile to code that works:
`js
// …
import {babel} from '@rollup/plugin-babel'
export default {
// …
plugins: [
// …
xdm({/ Options… /}),
babel({
// Also run on what used to be .mdx (but is now JS):`
extensions: ['.js', '.jsx', '.es6', '.es', '.mjs', '.mdx', '.md'],
// Other options…
})
]
}
Source maps are supported.
You do not need to pass options.SourceMapGenerator.
options are the same as from [compile][compile], with the additions of:
###### options.include
###### options.exclude
List of [picomatch][pico] patterns to include and/or excludestring
(, RegExp, Array, default: []).
#### Webpack
Install xdm and use xdm/webpack.cjs.webpack.config.js
Add something along these lines to your :
`js`
module.exports = {
module: {
// …
rules: [
// …
{test: /\.mdx?$/, use: [{loader: 'xdm/webpack.cjs', options: {}}]}
]
}
}
Source maps are supported based on how you configure webpack.
You do not need to pass options.SourceMapGenerator.
If you use modern JavaScript features you might want to use Babel through
babel-loader to compile to
code that works:
`jsxdm
// …
use: [
// Note that Webpack runs right-to-left: is used first, thenbabel-loader
// .`
{loader: 'babel-loader', options: {}},
{loader: 'xdm/webpack.cjs', options: {}}
]
// …
Note that webpack-cli doesn’t support loaders in ESM directly or evenxdm
indirectly.
Because itself is ESM, this means the xdm/webpack.cjs loader (evenwebpack-cli
though it’s CJS) doesn’t work with (it does work when using thewebpack-cli
webpack API).
To use this loader with , set the DISABLE_V8_COMPILE_CACHE=1
environment variable.
See
GH-11 for
details.
`sh`
DISABLE_V8_COMPILE_CACHE=1 webpack
#### Snowpack
Snowpack uses [Rollup][] (for local files) which can
be extended.
Unfortunately, snowpack.config.js is currently, ironically, CommonJS.import('xdm/rollup.js')
So figuring out a way to and use it in Snowpack, is
left as an exercise to the reader.
#### Vite
Vite supports [Rollup][] plugins directly in plugins invite.config.js
your .
#### WMR
WMR supports [Rollup][] plugins directly by
adding them to plugins
in wmr.config.mjs.
`js
import {defineConfig} from 'wmr'
import xdm from 'xdm/rollup.js'
export default defineConfig({
plugins: [
xdm({/ Options… /})
]
})
`
See § Preact if you want to use Preact.
#### Babel
You should probably use webpack or Rollup instead of Babel directly as that
gives the neatest interface.
It is possible to use xdm in Babel and it’s fast, because it skips xdm
serialization and Babel parsing, if Babel is used anyway.
Babel does not support syntax extensions to its parser (it has “syntax” plugins
but those in fact turn certain flags on or off).
It does support setting a different parser.
Which in turn lets us choose whether to use the xdm or @babel/parser.
This Babel plugin, plugin.js:
`js
import path from 'node:path'
import parser from '@babel/parser'
import estreeToBabel from 'estree-to-babel'
import {compileSync} from 'xdm'
export function babelPluginSyntaxMdx() {
// Tell Babel to use a different parser.
return {parserOverride: babelParserWithMdx}
}
// A Babel parser that parses .mdx files with xdm and passes any other things
// through to the normal Babel parser.
function babelParserWithMdx(value, options) {
if (
options.sourceFilename &&
/\.mdx?$/.test(path.extname(options.sourceFilename))
) {
// Babel does not support async parsers, unfortunately.
return compileSync(
{value, path: options.sourceFilename},
// Tell xdm to return a Babel tree instead of serialized JS.
{recmaPlugins: [recmaBabel]}
).result
}
return parser.parse(value, options)
}
// A “recma” plugin is a unified plugin that runs on the estree (used by xdm
// and much of the JS ecosystem but not Babel).
// This plugin defines 'estree-to-babel' as the compiler, which means thatcompileSync
// the resulting Babel tree is given back by .`
function recmaBabel() {
Object.assign(this, {Compiler: estreeToBabel})
}
Can be used like so with the Babel API:
`js
import babel from '@babel/core'
import {babelPluginSyntaxMdx} from './plugin.js'
// Note that a filename must be set for our plugin to know it’s MDX instead of JS.
await babel.transformAsync(file, {filename: 'example.mdx', plugins: [babelPluginSyntaxMdx]})
`
#### Create React App (CRA)
Create a new app with CRA and
change directory to enter it:
`sh`
npx create-react-app my-app
cd my-app
Install xdm as a dev dependency:
`sh`
yarn add xdm --dev
Now we can add our MDX content.
Create an MDX file Content.mdx in the src/ folder:
`mdx
export const Box = () => (
)
This is markdown with JSX: MDX!
`
To use that content in the app, replace the contents of App.js in the src/
folder with:
`js
/ eslint-disable import/no-webpack-loader-syntax /
import Content from '!xdm/webpack.cjs!./Content.mdx'
export default function App() {
return
}
`
Done!
To start the development server run:
`sh`
yarn start
#### Next.js
Next uses webpack.
Install xdm and extendnext.config.js
Next’s config
in a file like so:
`js.mdx
module.exports = {
// Support MDX files as pages:
pageExtensions: ['mdx', 'md', 'tsx', 'ts', 'jsx', 'js'],
// Support loading :
webpack(config) {
config.module.rules.push({
test: /\.mdx?$/,
use: [{loader: 'xdm/webpack.cjs', options: {/ Options… /}}]
})
return config
}
}
`
#### React
Works out of the box.
> What about React server components?
>
> While they are currently alpha and not shipping soon, there is an
> experimental demo
> combining xdm with RSC.
You can set providerImportSource to '@mdx-js/react' (which has to be
installed) to support context-based components passing.
`js
import {MDXProvider} from '@mdx-js/react'
import Post from './post.mdx' // Assumes an integration is used to compile MDX -> JS.
`
But the above can also be written without configuring and importing a provider:
`js
import Post from './post.mdx'
`
#### Preact
Define a different import source in [options][compile]:
`js`
compile(file, {jsxImportSource: 'preact'})
You can set providerImportSource to '@mdx-js/preact' (which has to be@mdx-js/preact
installed) to support context-based components passing.
See React above for more information (but use ).
#### Svelte
Use [mdsvex][]!
#### Vue
Use Vue 3, which adds support for functional components and fragments, two
features heavily used in MDX.
Vue has a special way to compile JSX: xdm can’t do it but Babel can.
Tell xdm to keep the JSX:
`js`
const jsx = String(await compile(file, {jsx: true}))
Then compile the JSX away with Babel and
@vue/babel-plugin-jsx:
`js
import babel from '@babel/core'
const js = (await babel.transformAsync(jsx, {plugins: ['@vue/babel-plugin-jsx']})).code
`
You are probably already using [webpack][] and/or [Rollup][] with Vue.
If not directly, then perhaps through something like Vue CLI.
In which case, see the above sections on these tools for how to use them, but
configure them as shown in this section to import .mdx files.
#### Emotion
Define a different import source in [options][compile] at compile time:
`js`
compile(file, {jsxImportSource: '@emotion/react'})
Otherwise, Emotion is React based, so see the React section for more info.
#### Theme UI
Theme UI is a React-specific library that requires using context to access its
effective components.
This can be done at the place where you’re using MDX content at runtime:
`js
import {base} from '@theme-ui/preset-base'
import {components, ThemeProvider} from 'theme-ui'
import Post from './post.mdx' // Assumes an integration is used to compile MDX -> JS.
`
If using a providerImportSource set to '@mdx-js/react' while compiling,
Theme UI automatically injects its components into that context:
`js
import {base} from '@theme-ui/preset-base'
import {ThemeProvider} from 'theme-ui'
import Post from './post.mdx'
`
Otherwise, Theme UI is Emotion and React based, so see their sections for more
info.
To support GFM (autolink literals, strikethrough, tables, and tasklists) use
remark-gfm.
Say we have an MDX file like this:
`mdxGFM
www.example.com, https://example.com, and contact@example.com.
A note[^1]
[^1]: Big note.
~one~ or ~~two~~ tildes.
| a | b | c | d |
| - | :- | -: | :-: |
* [ ] to do
* [x] done
`
Then do something like this:
`js
import {promises as fs} from 'node:fs'
import remarkGfm from 'remark-gfm'
import {compile} from 'xdm'
main()
async function main() {
console.log(
String(
await compile(await fs.readFile('example.mdx'), {remarkPlugins: [remarkGfm]})
)
)
}
`
Show equivalent JSX
` A note1js`GFM
Autolink literals
www.example.com,
https://example.com, and
contact@example.com.Footnote
Strikethrough
one or two tildes.Table
a
b
c
d
Tasklist
to do
done
Footnotes
Big note.
↩
There are two ways to accomplish syntax highlighting: at compile time or at
runtime.
Doing it at compile time means much less code is sent down the wire (syntax
highlighting needs a lot of code).
Doing it at runtime gives flexibility.
#### Syntax highlighting at compile time
Use either rehype-highlight
(highlight.js) or @mapbox/rehype-prism
(Prism) by doing something like this:
`js
import rehypeHighlight from 'rehype-highlight'
import {compile} from 'xdm'
main(~~~js
console.log(1)
~~~)
async function main(code) {
console.log(
String(await compile(code, {rehypePlugins: [rehypeHighlight]}))
)
}
`
…you still need to load a relevant style sheet.
Show equivalent JSX
`js`
console.log(
1)
#### Syntax highlighting at run time
Use for example
react-syntax-highlighter,
by doing something like this:
`js
import SyntaxHighlighter from 'react-syntax-highlighter'
import Post from './example.mdx' // Assumes an integration is used to compile MDX -> JS.
function code({className, ...props}) {
const match = /language-(\w+)/.exec(className || '')
return match
?
:
}
`
Show equivalent JSX
`js
className="language-js"
style={{
display: 'block',
overflowX: 'auto',
padding: '0.5em',
background: '#F0F0F0',
color: '#444'
}}
>
console.
log
(
1
)
#### Syntax highlighting with the
meta fieldMarkdown supports a meta string for code:
``markdown
`js filename="index.js"
console.log(1)
`
``This is a hidden part of markdown: it’s normally not rendered.
But as the above example shows, it’s a useful place to put some extra fields.
xdm doesn’t know whether you’re handling code as a component or what the
format of that meta string is, so it defaults to how markdown handles it:
meta
is ignored.remark-mdx-code-meta,
it lets you type JSX attributes in the meta part and exposes them on the
pre component.Or you can do it yourself, however you want, by writing a custom plugin to
interpret the
meta field.
For example, it’s possible to pass that string as a prop by writing a rehype
plugin:`js
function rehypeMetaAsAttribute() {
return transform
}function transform(tree) {
visit(tree, 'element', onelement)
}
function onelement(node) {
if (node.tagName === 'code' && node.data && node.data.meta) {
node.properties.meta = node.data.meta
}
}
`This would yields the following JSX:
`jsx
console.log(1)
`Note that the
meta attribute is not valid HTML, so make sure to handle code
with a component.The meta string in this example looks a lot like HTML attributes.
What if we wanted to parse that string and add each “attribute” as a prop?
Using the same rehype plugin as above, but with a different
onelement
function, that can be achieved:`js
const re = /\b([-\w]+)(?:=(?:"([^"])"|'([^'])'|([^"'\s]+)))?/g// …
function onelement(node) {
let match
if (node.tagName === 'code' && node.data && node.data.meta) {
re.lastIndex = 0 // Reset regex.
while ((match = re.exec(node.data.meta))) {
node.properties[match[1]] = match[2] || match[3] || match[4] || ''
}
}
}
`This would yields the following JSX:
`jsx
console.log(1)
`Note that the these added attributes are not valid HTML, so make sure to handle
code with a component.$3
remark-math
and either
rehype-katex
(KaTeX) or
rehype-mathjax
(MathJax) by doing something like this:`js
import rehypeKatex from 'rehype-katex'
import remarkMath from 'remark-math'
import {compile} from 'xdm'main()
async function main() {
console.log(
String(
// You only need one backslash in an MDX file but because this is JS wrapping it,
// a double backslash is needed.
await compile('# $\\sqrt{a^2 + b^2}$', {
remarkPlugins: [remarkMath],
rehypePlugins: [rehypeKatex]
})
)
)
}
`…you still need to load a KaTeX style sheet when using
rehype-katex.
Show equivalent JSX
`js
`$3
Frontmatter, typically in YAML format, is frequently combined with markdown.
MDX comes with support for ESM (import/exports) which is a powerful dynamic
alternative.
Say we had this
post.mdx:`mdx
export const name = 'World'
export const title = 'Hi, ' + name + '!'{title}
`Used like so:
`js
import * as Post from './post.mdx' // Assumes an integration is used to compile MDX -> JS.console.log(Post.title) // Prints 'Hi, World!'
`Still, you might prefer frontmatter because it lets you define data that can be
extracted from files without (or before) compiling:
Say our
post.mdx with frontmatter looked like this:`mdx
---
title: Hi, World!
---Hi, World!
`Then without compiling or evaluating that file the metadata can be accessed like
so:
`js
import {promises as fs} from 'node:fs'
import yaml from 'js-yaml'main()
async function main() {
console.log(yaml.loadAll(await fs.readFile('example.mdx'))[0]) // Prints
{title: 'Hi, World!'}
}
`xdm doesn’t understand YAML frontmatter by default but can understand it
using remark-frontmatter:`js
import {promises as fs} from 'node:fs'
import remarkFrontmatter from 'remark-frontmatter'
import {compile} from 'xdm'main()
async function main() {
console.log(
await compile(await fs.readFile('example.mdx'), {
remarkPlugins: [remarkFrontmatter]
})
)
}
`Now it “works”: the frontmatter is ignored.
But it’s not available from inside the MDX.
What if we wanted to use frontmatter from inside the MDX file too?
Like so?
`mdx
---
title: Hi, World!
---{frontmatter.title}
`remark-mdx-frontmatter
does.Plugins
xdm has several extension points:
* Components and a layout (wrapper) can be defined internally or passed at
runtime (see [§ MDX content][mdx-content])
* Plugins can hook into several stages of compilation ([remark
plugins][remark-plugins], [rehype plugins][rehype-plugins], and the new
recma plugins)
There are also a few of these extensions made specifically for MDX:
###### Components
None yet!
###### Plugins
rehype-mdx-title
— expose the page title as a string
* remark-mdx-code-meta
— interpret the code meta field as JSX props
* remark-mdx-images
— change image sources to JavaScript imports
* remark-mdx-frontmatter
— change frontmatter (YAML) metadata to exportsTypes
This package is fully typed with TypeScript.
To enable types for imported
.mdx, .md, etcetera files, first make sure
the TypeScript JSX namespace is typed (such as by importing the react
types).
Then install @types/mdx, which adds types to import statements of supported
files.`js
import Post from './post.mdx' // Post is now typed.
`Differences from
@mdx-js/mdxAPI (build):
* Remove
skipExport or wrapExport options
* Add support for automatic JSX runtime
* Add support for non-react classic runtime
* Add support for source maps
* Add evaluate instead of runtime package to eval MDX
* Remove JSX from output (by default)
* Default to automatic JSX runtime
* No GFM by defaultAPI (run):
* No providers by default
* No runtime at all
*
exports work in evaluate
* Add support for compiling import statements to dynamic import expressions
* Add support for resolving import/export sourcesInput:
* ± same as
main branch of @mdx-js/mdx
* Fix JSX tags to prevent
* Plain markdown can be loaded (format: 'md')Output:
* No
isMDXContent prop on the MDXContent component
* Missing components throw instead of warn
* Sandbox: when passing components: {h1 = () => ...} that component gets
used for # heading but not for
* Local components (including layouts) precede over given components
* Remove support for passing parent.child combos (ol.li) for components
* Remove support for passing inlineCode component (use pre and/or code
instead)
* Support for import and exports in evaluate
* Fix a bug with encoding " in attributesExperiments:
* Add support for
import Content from './file.mdx' in Node
* Add support for require('./file.mdx') in Node
* Add support allowDangerousRemoteMdx in esbuild to load MD(X) from the webArchitecture
To understand what this project does, it’s very important to first understand
what unified does: please read through the
unifiedjs/unified readme (the part
until you hit the API section is required reading).xdm is a unified pipeline — wrapped so that most folks don’t need to know
about unified:
core.js#L76-L102.
The processor goes through these steps:1. Parse MDX (serialized markdown with embedded JSX, ESM, and expressions)
to mdast (markdown syntax tree)
2. Transform through remark (markdown ecosystem)
3. Transform mdast to hast (HTML syntax tree)
4. Transform through rehype (HTML ecosystem)
5. Transform hast to esast (JS syntax tree)
6. Do the work needed to get a component
7. Transform through recma (JS ecosystem)
8. Serialize esast as JavaScript
The input is MDX (serialized markdown with embedded JSX, ESM, and
expressions).
The markdown is parsed with [
micromark][micromark] and the embedded JS with
one of its extensions
micromark-extension-mdxjs
(which in turn uses [acorn][]).
Then mdast-util-from-markdown
and its extension
mdast-util-mdx are used to
turn the results from the parser into a syntax tree:
mdast.Markdown is closest to the source format.
This is where [remark plugins][remark-plugins] come in.
Typically, there shouldn’t be much going on here.
But perhaps you want to support GFM (tables and such) or frontmatter?
Then you can add a plugin here:
remark-gfm or remark-frontmatter,
respectively.After markdown, we go to hast (HTML).
This transformation is done by
mdast-util-to-hast.
Wait, why, what does HTML have to do with it?
Part of the reason is that we care about HTML semantics: we want to know that
something is an , not whether it’s a link with a resource (text)
or a reference to a defined link definition ([text][id]\n\n[id]: url).
So an HTML AST is closer to where we want to go.
Another reason is that there are many things folks need when they go MDX -> JS,
markdown -> HTML, or even folks who only process their HTML -> HTML: use cases
other than xdm.
By having a single AST in these cases and writing a plugin that works on that
AST, that plugin can supports all these use cases (for example,
rehype-highlight
for syntax highlighting or
rehype-katex
for math).
So, this is where [rehype plugins][rehype-plugins] come in: most of the plugins,
probably.Then we go to JavaScript: esast (JS; an
AST which is compatible with estree but looks a bit more like other unist ASTs).
This transformation is done by
hast-util-to-estree.
This is a new ecosystem that does not have utilities or plugins yet.
But it’s where xdm does its thing: where it adds imports/exports, where it
compiles JSX away into _jsx()` calls, and where