Xdm
Just a *really* good MDX compiler. No runtime. With esbuild, Rollup, and webpack plugins
Install / Use
/learn @wooorm/XdmREADME
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.
xdm is an MDX compiler that focusses on two things:
- Compiling the MDX syntax (markdown + JSX) to JavaScript
- 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]!
Install
Use Node 12 or later.
Then install xdm with either npm or yarn.
[npm][]:
npm install xdm
[yarn][]:
yarn add xdm
This package is ESM only:
Node 12+ is needed to use it and it must be imported instead of required.
Contents
- What is MDX?
- Use
- API
- 👩🔬 Lab
- MDX syntax
- MDX content
- Integrations
- Guides
- Plugins
- Types
- Differences from
@mdx-js/mdx - Architecture
- Security
- Related
- License
What is MDX?
MDX is different things.
The term is sometimes used for a compiler, typically implying @mdx-js/mdx, but
there are more.
First there was [mdxc][mdxc].
Then came [@mdx-js/mdx][mdxjs].
There’s also [mdsvex][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][]):
## Hello, world!
<div className="note">
> Some notable things in a block quote!
</div>
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:
export const Thing = () => <>World!</>
# Hello, <Thing />
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:
/* @jsxRuntime automatic @jsxImportSource react */
export const Thing = () => <>World!</>
export default function MDXContent() {
return <h1>Hello, <Thing /></h1>
}
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 exported
Now for how to get the actual output.
Add some code in example.js to compile example.mdx to JavaScript:
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:
/* @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.
compile(file, options?)
Compile MDX to JS.
file
MDX document to parse (string, [Buffer][buffer] in UTF-8, [vfile][vfile],
or anything that can be given to vfile).
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: '🤭'}))
</details>
options.remarkPlugins
List of [remark plugins][remark-plugins], presets, and pairs.
<details> <summary>Example</summary>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.
</details>
options.rehypePlugins
List of [rehype plugins][rehype-plugins], presets, and pairs.
<details> <summary>Example</summary>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}]]
})
</details>
options.recmaPlugins
List of recma plugins. This is a new ecosystem, currently in beta, to transform esast trees (JavaScript).
options.remarkRehypeOptions
Options 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.
await compile({value: '…'}, {remarkRehypeOptions: {clobberPrefix: 'comment-1'}})
</details>
options.mdExtensions
List of markdown extensions, with dot (string[], default: ['.md', '.markdown', '.mdown', '.mkdn', '.mkd', '.mdwn', '.mkdown', '.ron']).
options.mdxExtensions
List 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.format
Format the file is in ('detect' | 'mdx' | 'md', default: 'detect').
'detect'— use'markdown'for files with an extension inmdExtensionsand'mdx'otherwise'mdx'— treat file as [MDX][mdx-syntax]
