GithubHelp home page GithubHelp logo

wooorm / xdm Goto Github PK

View Code? Open in Web Editor NEW
591.0 8.0 18.0 1.71 MB

Just a *really* good MDX compiler. No runtime. With esbuild, Rollup, and webpack plugins

Home Page: http://wooorm.com/xdm/

License: MIT License

JavaScript 100.00%
mdx mdxjs markdown jsx babel webpack rollup esbuild

xdm's Introduction

xdm

Build Coverage Downloads 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 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!

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?

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. Then came @mdx-js/mdx. 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):

## 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 below for more on how the format works.

Use

This section describes how to use the API. See Β§ MDX syntax on how the format works. See Β§ 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 below on how to use the result.

API

xdm exports the following identifiers: compile, compileSync, evaluate, evaluateSync, 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 for more info.

compile(file, options?)

Compile MDX to JS.

file

MDX document to parse (string, Buffer in UTF-8, vfile, or anything that can be given to vfile).

Example
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.remarkPlugins

List of remark plugins, presets, and pairs.

Example
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.rehypePlugins

List of rehype plugins, presets, and pairs.

Example
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.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. 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
await compile({value: '…'}, {remarkRehypeOptions: {clobberPrefix: 'comment-1'}})
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).

options.format

Format 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
  • 'md' β€” treat file as plain vanilla markdown

The 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
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), 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.outputFormat

Output format to generate ('program' | 'function-body', default: 'program'). In most cases 'program' should be used, as it results in a whole program. Internally, evaluate uses outputFormat: 'function-body' to compile to code that can be evaled with 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.

Example

A module example.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:

import {Fragment as _Fragment, jsx as _jsx} from 'react/jsx-runtime'
export const no = 3.14
function MDXContent(props = {}) { /* … */ }
export default MDXContent
const {Fragment: _Fragment, jsx: _jsx} = arguments[0]
const no = 3.14
function MDXContent(props = {}) { /* … */ }
return {no, default: MDXContent}
options.useDynamicImport

Whether to compile to dynamic import expressions (boolean, default: false). This option applies when options.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 too.

Example

Say we have a couple modules:

// 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'
export {no} from './numbers.js'

# hi {name}!`

console.log(String(compileSync(code, {outputFormat: 'function-body', useDynamicImport: true})))

…now running node example.js yields:

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?, example: import.meta.url).

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.

Example

Say we have a module example.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:

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). The default can be set to true in Node.js through environment variables: set NODE_ENV=development.

Example

Say we had some MDX that references a component that can be passed or provided at runtime:

**Note**<NoteIcon />: some stuff.

And a module to evaluate that:

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:

Error: Expected component `NoteIcon` to be defined: you likely forgot to import, pass, or provide it.
    at _missingMdxReference (eval at run (…/xdm/lib/run.js:18:10), <anonymous>:27:9)
    at _createMdxContent (eval at run (…/xdm/lib/run.js:18:10), <anonymous>:15:20)
    at MDXContent (eval at run (…/xdm/lib/run.js:18:10), <anonymous>:9:9)
    at main (…/example.js:11:15)

But if we change add development: true to our example:

@@ -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:

Error: Expected component `NoteIcon` to be defined: you likely forgot to import, pass, or provide it.
It’s referenced in your code at `1:9-1:21` in `example.mdx`
provide it.
    at _missingMdxReference (eval at run (…/xdm/lib/run.js:18:10), <anonymous>:27:9)
    at _createMdxContent (eval at run (…/xdm/lib/run.js:18:10), <anonymous>:15:20)
    at MDXContent (eval at run (…/xdm/lib/run.js:18:10), <anonymous>:9:9)
    at main (…/example.js:11:15)
options.SourceMapGenerator

The SourceMapGenerator class from source-map (optional). When given, the resulting file will have a map field set to a source map (in object form).

Example

Assuming example.mdx from Β§ Use exists, then:

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:

{
  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'). Useful for runtimes that support context (React, Preact). The provider must export a useMDXComponents, which is called to access an object of components.

Example

If file is the contents of example.mdx from Β§ Use, then:

compile(file, {providerImportSource: '@mdx-js/react'})

…yields this difference:

 /* @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, then:

compile(file, {jsx: true})

…yields this difference:

 /* @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, {})}))
+    ? <MDXLayout {...props}><_createMdxContent /></MDXLayout>
     : _createMdxContent()
   function _createMdxContent() {
     let _components = Object.assign({h1: 'h1'}, props.components)
-    return _jsxs(_components.h1, {children: ['Hello, ', _jsx(Thing, {})]})
+    return <_components.h1>{"Hello, "}<Thing /></_components.h1>
   }
 }

 export default MDXContent
options.jsxRuntime

JSX runtime to use ('automatic' | 'classic', default: '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').

Example

If file is the contents of example.mdx from Β§ Use, then:

compile(file, {jsxRuntime: 'classic'})

…yields this difference:

-/* @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'). When in the automatic runtime, this is used to define an import for _Fragment, _jsx, and _jsxs.

Example

If file is the contents of example.mdx from Β§ Use, then:

compile(file, {jsxImportSource: 'preact'})

…yields this difference:

-/* @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'). When in the classic runtime, this is used as an identifier for function calls: <x /> 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, then:

compile(file, {
  jsxRuntime: 'classic',
  pragma: 'preact.createElement',
  pragmaFrag: 'preact.Fragment',
  pragmaImportSource: 'preact/compat'
})

…yields this difference:

-/* @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'). When in the classic runtime, this is used as an identifier for fragments: <> to React.createElement(React.Fragment).

See options.pragma for an example.

options.pragmaImportSource

Where to import the identifier of pragma from (string?, default: 'react'). When in the classic runtime, this is used to import the pragma function. To illustrate with an example: when pragma is 'a.b' and pragmaImportSource is 'c' this following will be generated: import a from 'c'.

See options.pragma for an example.

Returns

Promise<VFile> β€” Promise that resolves to the compiled JS as a vfile.

Example
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:

  1:16-1:27  warning  Emphasis should use `*` as a marker  emphasis-marker  remark-lint

⚠ 1 warning

compileSync(file, options?)

Compile MDX to JS. Synchronous version of compile. When possible please use the async compile.

evaluate(file, options)

☒️ 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 or bundle with esbuild/Rollup/webpack. But if you trust your content, evaluate can work.

Typically, import (or export … from) do not work here. They can be compiled to dynamic import() by passing options.useDynamicImport.

file

See compile.

options

Most options are the same as compile, with the following exceptions:

  • providerImportSource is replaced by useMDXComponents
  • jsx* and pragma* options are replaced by jsx, jsxs, and Fragment
  • outputFormat 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
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
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<Module> β€” Promise that resolves to something that looks a bit like a module: an object with a default 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 was in file, then:

import * as runtime from 'react/jsx-runtime'
import {evaluate} from 'xdm'

console.log(await evaluate(file, {...runtime}))

…yields:

{Thing: [Function: Thing], default: [Function: MDXContent]}

evaluateSync(file, options)

☒️ Danger: It’s called evaluate because it evals JavaScript.

Compile and run MDX. Synchronous version of evaluate. When possible please use the async evaluate.

run(functionBody, options)

☒️ Danger: This evals JavaScript.

Run MDX compiled as options.outputFormat: 'function-body'.

options

You can pass jsx, jsxs, and Fragment from an automatic JSX runtime as options. You can pass useMDXComponents from a provider in options as well if the MDX is compiled with options.providerImportSource: '#' (the exact value of this option doesn’t matter). All other options have to be passed to compile instead.

Returns

Promise<Module> β€” See evaluate

Example

On the server:

import {compile} from 'xdm'

const code = String(await compile('# hi', {outputFormat: 'function-body'}))

On the client:

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:

[Function: MDXContent]

runSync(functionBody, options)

☒️ Danger: This evals JavaScript.

Run MDX. Synchronous version of run. When possible please use the async run.

createProcessor(options)

Create a unified processor to compile MDX to JS. Has the same options as compile, but returns a configured processor.

Note that format: 'detect' does not work here: only 'md' or 'mdx' are allowed (and 'mdx' is the default).

πŸ‘©β€πŸ”¬ Lab

This section describes experimental features! These do not adhere to semver and could break at any time!

Importing .mdx files directly

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 with on-the-fly compilation, using xdm/esm-loader.js:

Assuming example.mdx from Β§ Use exists, and our module example.js looks as follows:

import {renderToStaticMarkup} from 'react-dom/server'
import React from 'react'
import Content from './example.mdx'

console.log(renderToStaticMarkup(React.createElement(Content)))

Running that with:

node --experimental-loader=xdm/esm-loader.js example.js

…yields:

<h1>Hello, World!</h1>

To pass options, you can make your own loader, such as this my-loader.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.

Requiring .mdx files directly

require.extensions is a deprecated feature in Node. Still, it lets projects β€œhijack” require calls to do fancy things. xdm comes with support for requiring .mdx files with on-the-fly evaluation, using xdm/register.cjs:

Assuming example.mdx from Β§ Use exists, and our script example.cjs looks as follows:

const React = require('react')
const {renderToStaticMarkup} = require('react-dom/server')
const Content = require('./example.mdx')

console.log(renderToStaticMarkup(React.createElement(Content)))

Running that with:

node -r xdm/register.cjs example.cjs

…yields:

<h1>Hello, World!</h1>

To pass options, you can make your own hook, such as this my-hook.cjs:

'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. That means import (and export … from) are not supported when requiring .mdx files.

Importing .md and .mdx files from the web in esbuild

⚠️ Note that this includes remote code in your bundle. Make sure you trust it! See § Security for more info.

When passing allowDangerousRemoteMdx to the esbuild loader, MD(X) and JS files can be imported from http: and https: urls. Take this index.mdx file:

import Readme from 'https://raw.githubusercontent.com/wooorm/xdm/main/readme.md'

Embed the xdm readme like so:

<Readme />

And a module build.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:

<p>Embed the xdm readme like so:</p>
<h1>xdm</h1>
{/* … */}
<p><a href="https://github.com/wooorm/xdm/blob/main/license">MIT</a> Β© …</p>

MDX syntax

Note! You don’t have to use this syntax. Or use it always. With 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.

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.

Markdown

Most of markdown (CommonMark) works:

# Heading (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](https://example.com), an ![image](./image.png), some *emphasis*,
something **strong**, and finally a little `code()`.

Some other features often used with markdown are:

There are many more things possible by configuring remark plugins and 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:

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”:

<svg:rect> and <[email protected]> are links in markdown, but they crash xdm.
The reason is that they look a lot like JSX components, and we prefer being unambiguous.
If you want links, use [descriptive text](https://and-the-link-here.com).

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.

JSX

Most of JSX works. Here’s some that looks a lot like HTML (but is JSX):

<h1>Heading!</h1>

<abbr title="HyperText Markup Language">HTML</abbr> is a lovely language.

<section>
  And here is *markdown* in **JSX**!
</section>

You can also use components, but note that you must either define them locally or pass them in later (see Β§ MDX content):

<MyComponent id="123" />

Or access the `thisOne` component on the `myComponents` object: <myComponents.thisOne />

<Component
  open
  x={1}
  label={'this is a string, *not* markdown!'}
  icon={<Icon />}
/>

More on this is documented here.

ESM

To define things from within MDX, use ESM:

import {External} from './some/place.js'

export const Local = props => <span style={{color: 'red'}} {...props} />

An <External /> component and <Local>a local component</Local>.

ESM can also be used for other things:

import {MyChart} from './chart-component.js'
import data from './population.js'
export const pi = 3.14

<MyChart data={data} label={'Something with ' + pi} />

Expressions

Braces can be used to embed JavaScript expressions in MDX:

export const pi = 3.14

Two 🍰 is: {pi * 2}

Expressions can be empty or contain just a comment:

{/* A comment! */}

MDX content

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. This includes both JSX and markdown syntax. Say we have a message.mdx file:

# Hello, *<Planet />*!

Remember when we first met in {props.year}?

This file could be imported from JavaScript and passed components like so:

import Message from './message.mdx' // Assumes an integration is used to compile MDX -> JS.

<Message components={{Planet: () => 'Venus'}} year={1962} />

You can also change the things that come from markdown:

<Message
  components={{
    // Map `h1` (`# heading`) to use `h2`s.
    h1: 'h2',
    // Rewrite `em`s (`*like so*`) to `i` with a red foreground color.
    em: (props) => <i style={{color: 'red'}} {...props} />,
    // Pass a layout (using the special `'wrapper'` key).
    wrapper: ({components, ...props}) => <main {...props} />,
    // Pass a component.
    Planet: () => 'Venus'
  }}
  year={1962}
/>

Components

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 <So /> or <like.so />, 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. With remark-gfm (see guide below), you can also use: del, section, sup, table, tbody, td, th, thead, and tr. 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 <x>) is a literal tag name or not, are as follows:

  • If there’s a dot, it’s a member expression (<a.b> -> h(a.b))
  • Otherwise, if the name is not a valid identifier, it’s a literal (<a-b> -> h('a-b'))
  • Otherwise, if it starts with a lowercase, it’s a literal (<a> -> h('a'))
  • Otherwise, it’s an identifier (<A> -> h(A))

Layout

Layouts are components that wrap the whole content. They can be defined from within MDX using a default export:

export default function Layout({children}) {
  return <main>{children}</main>;
}

All the things.

The layout can also be imported and then exported with an export … from:

export {Layout as default} from './components.js'

The layout can also be passed as components.wrapper (but a local one takes precedence).

Integrations

Bundlers

esbuild

Install xdm and use xdm/esbuild.js. Add something along these lines to your build call:

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 with the addition of:

options.allowDangerousRemoteMdx

Whether to allow importing from http: and https: URLs (boolean, default: false). See Β§ Importing .md and .mdx files from the web in esbuild.

⚠️ Note that this evaluates any JavaScript and MDX found over the wire!

Rollup

Install xdm and use xdm/rollup.js. Add something along these lines to your rollup.config.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:

// …
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, with the additions of:

options.include
options.exclude

List of picomatch patterns to include and/or exclude (string, RegExp, Array<string|RegExp>, default: []).

Webpack

Install xdm and use xdm/webpack.cjs. Add something along these lines to your webpack.config.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:

// …
use: [
  // Note that Webpack runs right-to-left: `xdm` is used first, then
  // `babel-loader`.
  {loader: 'babel-loader', options: {}},
  {loader: 'xdm/webpack.cjs', options: {}}
]
// …

Note that webpack-cli doesn’t support loaders in ESM directly or even indirectly. Because xdm itself is ESM, this means the xdm/webpack.cjs loader (even though it’s CJS) doesn’t work with webpack-cli (it does work when using the webpack API). To use this loader with webpack-cli, set the DISABLE_V8_COMPILE_CACHE=1 environment variable. See GH-11 for details.

DISABLE_V8_COMPILE_CACHE=1 webpack

Build systems

Snowpack

Snowpack uses Rollup (for local files) which can be extended. Unfortunately, snowpack.config.js is currently, ironically, CommonJS. So figuring out a way to import('xdm/rollup.js') and use it in Snowpack, is left as an exercise to the reader.

Vite

Vite supports Rollup plugins directly in plugins in your vite.config.js.

WMR

WMR supports Rollup plugins directly by adding them to plugins in wmr.config.mjs.

import {defineConfig} from 'wmr'
import xdm from 'xdm/rollup.js'

export default defineConfig({
  plugins: [
    xdm({/* Options… */})
  ]
})

See Β§ Preact if you want to use Preact.

Compilers

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:

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 that
// the resulting Babel tree is given back by `compileSync`.
function recmaBabel() {
  Object.assign(this, {Compiler: estreeToBabel})
}

Can be used like so with the Babel API:

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]})

Site generators

Create React App (CRA)

Create a new app with CRA and change directory to enter it:

npx create-react-app my-app
cd my-app

Install xdm as a dev dependency:

yarn add xdm --dev

Now we can add our MDX content. Create an MDX file Content.mdx in the src/ folder:

export const Box = () => (
  <div style={{padding: 20, backgroundColor: 'tomato'}} />
)

# Hello, world!

This is **markdown** with <span style={{color: "red"}}>JSX</span>: MDX!

<Box />

To use that content in the app, replace the contents of App.js in the src/ folder with:

/* eslint-disable import/no-webpack-loader-syntax */
import Content from '!xdm/webpack.cjs!./Content.mdx'

export default function App() {
  return <Content />
}

Done! To start the development server run:

yarn start

Next.js

Next uses webpack. Install xdm and extend Next’s config in a next.config.js file like so:

module.exports = {
  // Support MDX files as pages:
  pageExtensions: ['mdx', 'md', 'tsx', 'ts', 'jsx', 'js'],
  // Support loading `.mdx`:
  webpack(config) {
    config.module.rules.push({
      test: /\.mdx?$/,
      use: [{loader: 'xdm/webpack.cjs', options: {/* Options… */}}]
    })

    return config
  }
}

Hyperscript implementations (frameworks)

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.

import {MDXProvider} from '@mdx-js/react'
import Post from './post.mdx' // Assumes an integration is used to compile MDX -> JS.

<MDXProvider components={{em: props => <i {...props} />}}>
  <Post />
</MDXProvider>

But the above can also be written without configuring and importing a provider:

import Post from './post.mdx'

<Post components={{em: props => <i {...props} />}} />

Preact

Define a different import source in options:

compile(file, {jsxImportSource: 'preact'})

You can set providerImportSource to '@mdx-js/preact' (which has to be installed) to support context-based components passing. See React above for more information (but use @mdx-js/preact).

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:

const jsx = String(await compile(file, {jsx: true}))

Then compile the JSX away with Babel and @vue/babel-plugin-jsx:

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.

Runtime libraries

Emotion

Define a different import source in options at compile time:

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:

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.

<ThemeProvider theme={base}>
  <Post components={components} />
</ThemeProvider>

If using a providerImportSource set to '@mdx-js/react' while compiling, Theme UI automatically injects its components into that context:

import {base} from '@theme-ui/preset-base'
import {ThemeProvider} from 'theme-ui'
import Post from './post.mdx'

<ThemeProvider theme={base}>
  <Post />
</ThemeProvider>

Otherwise, Theme UI is Emotion and React based, so see their sections for more info.

Guides

GitHub flavored markdown (GFM)

To support GFM (autolink literals, strikethrough, tables, and tasklists) use remark-gfm. Say we have an MDX file like this:

# GFM

## Autolink literals

www.example.com, https://example.com, and [email protected].

## Footnote

A note[^1]

[^1]: Big note.

## Strikethrough

~one~ or ~~two~~ tildes.

## Table

| a | b  |  c |  d  |
| - | :- | -: | :-: |

## Tasklist

* [ ] to do
* [x] done

Then do something like this:

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
<h1>GFM</h1>
<h2>Autolink literals</h2>
<p>
  <a href="http://www.example.com">www.example.com</a>,
  <a href="https://example.com">https://example.com</a>, and
  <a href="mailto:[email protected]">contact@example.com</a>.
</p>
<h2>Footnote</h2>
<p>A note<sup><a href="#user-content-fn-1" id="user-content-fnref-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup></p>
<h2>Strikethrough</h2>
<p>
  <del>one</del> or <del>two</del> tildes.
</p>
<h2>Table</h2>
<table>
  <thead>
    <tr>
      <th>a</th>
      <th align="left">b</th>
      <th align="right">c</th>
      <th align="center">d</th>
    </tr>
  </thead>
</table>
<h2>Tasklist</h2>
<ul className="contains-task-list">
  <li className="task-list-item">
    <input type="checkbox" disabled /> to do
  </li>
  <li className="task-list-item">
    <input type="checkbox" checked disabled /> done
  </li>
</ul>
<section data-footnotes className="footnotes">
<h2 id="footnote-label" className="sr-only">Footnotes</h2>
<ol>
  <li id="user-content-fn-1">
    <p>
      Big note.
      <a href="#user-content-fnref-1" data-footnote-backref className="data-footnote-backref" aria-label="Back to content">↩</a>
    </p>
  </li>
</ol>
</section>

Syntax highlighting

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:

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
<pre>
  <code className="hljs language-js">
    <span className="hljs-built_in">console</span>.log(
    <span className="hljs-number">1</span>)
  </code>
</pre>

Syntax highlighting at run time

Use for example react-syntax-highlighter, by doing something like this:

import SyntaxHighlighter from 'react-syntax-highlighter'
import Post from './example.mdx' // Assumes an integration is used to compile MDX -> JS.

<Post components={{code}} />

function code({className, ...props}) {
  const match = /language-(\w+)/.exec(className || '')
  return match
    ? <SyntaxHighlighter language={match[1]} PreTag="div" {...props} />
    : <code className={className} {...props} />
}
Show equivalent JSX
<pre>
  <div
    className="language-js"
    style={{
      display: 'block',
      overflowX: 'auto',
      padding: '0.5em',
      background: '#F0F0F0',
      color: '#444'
    }}
  >
    <code style={{whiteSpace: 'pre'}}>
      <span>console.</span>
      <span style={{color: '#397300'}}>log</span>
      <span>(</span>
      <span style={{color: '#880000'}}>1</span>
      <span>)</span>
    </code>
  </div>
</pre>

Syntax highlighting with the meta field

Markdown supports a meta string for code:

```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.

The short answer is: use 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:

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:

<pre>
  <code class="language-js" meta='filename="index.js"'>
    console.log(1)
  </code>
</pre>

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:

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:

<pre>
  <code class="language-js" filename="index.js">
    console.log(1)
  </code>
</pre>

Note that the these added attributes are not valid HTML, so make sure to handle code with a component.

Math

Use remark-math and either rehype-katex (KaTeX) or rehype-mathjax (MathJax) by doing something like this:

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
<h1>
  <span className="math math-inline">
    <span className="katex">
      <span className="katex-mathml">
        <math xmlns="http://www.w3.org/1998/Math/MathML">…</math>
      </span>
      <span className="katex-html" aria-hidden="true">…</span>
    </span>
  </span>
</h1>

Frontmatter

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:

export const name = 'World'
export const title = 'Hi, ' + name + '!'

# {title}

Used like so:

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:

---
title: Hi, World!
---

# Hi, World!

Then without compiling or evaluating that file the metadata can be accessed like so:

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:

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?

---
title: Hi, World!
---

# {frontmatter.title}

That’s what remark-mdx-frontmatter does.

Plugins

xdm has several extension points:

There are also a few of these extensions made specifically for MDX:

Components

None yet!

Plugins

Types

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.

import Post from './post.mdx' // `Post` is now typed.

Differences from @mdx-js/mdx

API (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 default

API (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 sources

Input:

  • Β± same as main branch of @mdx-js/mdx
  • Fix JSX tags to prevent <p><h1 /></p>
  • 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 <h1>heading</h1>
  • 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 attributes

Experiments:

  • 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 web

Architecture

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 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 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 <a>, not whether it’s a link with a resource ([text](url)) 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 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 it does the other cool things that it provides.

Finally, The output is serialized JavaScript. That final step is done by astring, a small and fast JS generator.

Security

MDX is unsafe: it’s a programming language. You might want to look into using <iframe>s with sandbox, but security is hard, and that doesn’t seem to be 100%. For Node, vm2 sounds interesting. But you should probably also sandbox the whole OS (Docker?), perform rate limiting, and make sure processes can be killed when taking too long.

Related

A lot of things are going on in xdm: parsing markdown to a syntax tree, handling JavaScript (and JS) inside that markdown, converting to an HTML syntax tree, converting that to a Js syntax tree, all the while running several transforms, before finally serializing JavaScript.

Most of the work is done by:

  • micromark β€” Handles parsing of markdown (CommonMark)
  • acorn β€” Handles parsing of JS (ECMAScript)
  • unifiedjs.com β€” Ties it all together

License

MIT Β© Titus Wormer, Compositor, and Vercel, Inc.

xdm's People

Contributors

arcath avatar chenxsan avatar christianmurphy avatar danielolaviobr avatar deadcoder0904 avatar elmassimo avatar jensmeindertsma avatar kripod avatar mdynnl avatar mike-mcdonald avatar mrv1k avatar remcohaszing avatar rschristian avatar silvenon avatar tristandubbeld avatar wooorm avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

xdm's Issues

Could not parse expression with acorn: Unexpected token


Initial checklist

  • I read the support docs
  • I read the contributing guide
  • I agree to follow the code of conduct
  • I searched issues and couldn’t find anything (or linked relevant results below)

Latest xdm.

Steps to reproduce

  1. open https://stackblitz.com/edit/node-krxxhd?file=index.js
  2. run node index.js

It seems the return is the culprit.

Expected behavior

No error to happen.

Actual behavior

Using markdown content like - async () => {return;} would throw an error.

Error: Could not parse expression with acorn: Unexpected token
    at async ModuleJob.run (https://node-krxxhd.jw.staticblitz.com/blitz.f91df8ee833f18bedf806af681eeaccd3addac72.js:6:702354)
    at async Loader.import (https://node-krxxhd.jw.staticblitz.com/blitz.f91df8ee833f18bedf806af681eeaccd3addac72.js:6:995064)
    at async Object.r.loadESM (https://node-krxxhd.jw.staticblitz.com/blitz.f91df8ee833f18bedf806af681eeaccd3addac72.js:6:278979) {
  name: '1:16',
  reason: 'Could not parse expression with acorn: Unexpected token',
  line: 1,
  column: 16,
  source: 'micromark-extension-mdx-expression',
  ruleId: 'acorn',
  position: {
    start: { line: 1, column: 16, offset: 15 },
    end: { line: null, column: null }
  }
}

Removing return would fix it.

Support for codeblock meta string?

Hi πŸ‘‹

I started testing out xdm on my MDX based blog and so far I love it. I come from mdx-js and I found out that xdm does not parse code blocks pre the same way as mdx-js does.

For example I'd often define arbitrary key/values after specifying the language for my codeblock

```js title=foo

// Code block

or even some arbitrary syntax to highlight specific lines

```js {1-2}
// Code block

and I was always able to get back those values as props on the React side of things.

(Another example from the mdx-js docs: https://mdxjs.com/guides/live-code passing live=true to render an alternative codeblock component)

However, I don't seem to be able to get the same behavior with xdm. Do you have any tips or workaround for this? Or is this simply not supported?

Thank you in advance!

`SyntaxError` for components used directly and as objects

when using namespaced components i'm getting a SyntaxError: Identifier has already been declared.

this is what i did:

import { compile, evaluate } from "xdm";
import * as runtime from "react/jsx-runtime.js";

main();

async function main() {
  const file = `
  <Hello>Text</Hello>

  <Hello.World>Text</Hello.World>
  `;
  await evaluate(file, { ...runtime });
  // const code = await compile(file);
  // console.log(String(code));
}

the compiled code has the following lines:

const _components = Object.assign({
  Hello: _missingComponent("Hello")
}, _props.components), {Hello, wrapper: MDXLayout, Hello} = _components;

Add `format` option, defaulting to `'mdx'`, optionally `'detect'` to treat `.md` as plain markdown

Problem:

  • Given MDX is moving a bit further from markdown:
    <div>
      *this was not emphasis before, in alignment with markdown, but now it is*
    </div>
  • And given that folks will also have plain markdown,
  • And finally given that xdm comes with a lot of nice tooling for using MD(X) in esbuild/rollup/webpack/snowpack/vite/RSC/etc,
  • Therefore it seems like a good addition to support plain markdown by not adding remark-mdx, in a flag.

Solution:

  • The detect value would detect mdx or md based on a file extension
  • mdx / md would toggle remark-mdx on or off
  • To not break current usage, the default will be mdx, if that doesn’t matter, the default could be detect
  • Folks can pass rehype-raw if they want the HTML in their markdown to β€œwork”. If they didn’t set it, we’d strip it out. Include rehype-raw would be pretty big, so I’d prefer not importing it by default.
  • We’d need to turn on allowDangerousHtml for remark-rehype, which can always be there, given that we’re either not getting it (if remark-mdx is there), or compiling it away (if rehype-raw is there), or filtering any remaining html nodes out

Clarify hast properties to jsx props processing

I am trying to use the @mapbox/rehype-prism library to apply syntax highlighting within my mdx files.
It uses refractor (prism) to create hast output that later gets turned into jsx by xdm.
My code looks roughly like this:

example.mdx

# Some code
```javascript
const someCode = () => {}
```

compile.mjs

import fs from "fs/promises"

import { compile } from "xdm"
import rehypePrism from "@mapbox/rehype-prism"

const mdxContent = await fs.readFile(new URL("./example.mdx", import.meta.url))
const xdmOptions = {
  rehypePlugins: [rehypePrism],
  jsxImportSource: "bruh"
}

const result = await compile(mdxContent, xdmOptions)
console.log(result)

My library, bruh, uses jsx props as html attributes directly, meaning no className or htmlFor (just like html).
When I write jsx within mdx files, I noticed that xdm correctly will turn input jsx props directly into output jsx props.
But, with hast, className (and similar) properties will stay in the output.
Even when I wrote an ad hoc rehype plugin to rename the className array into a class string, xdm still output className jsx props.

My understanding is that the hast result is being transformed somewhere down the plugin pipeline with xdm, and presumably hast properties are renamed for compatibility with react.

I do not know how to make xdm treat hast properties such that the output jsx props are identical to what the attributes would be if the hast was instead serialized into html.

When I read through the README, I also noticed a mention that vue, like bruh, also expects class instead of className. I would then assume that my issue with hast property name translation would also affect vue, meaning that vue jsx users would have problems with @mapbox/rehype-prism (and any other rehype plugins producing className's and related properties).

Feature request: Annotate the output VFile with exported symbols.

Use-case

Suppose I have an mdx file like:

export const name = "ggoodman";

# Hello world

The resulting JavaScript will be an ES Module having a default export that is a react function component and a named export name. It would be possible to use this information at compile-time to generate type definitions for the .mdx file with accurate export names.

Importing xdm on Next.js results in Webpack fail: reading from "node:assert" is not handled by plugins


Choosing question instead of issue because I feel this is more related to Next.js than xdm, but wanted to make sure I'm not missing anything obvious.

Initial checklist

  • I read the support docs
  • I read the contributing guide
  • I agree to follow the code of conduct
  • I searched issues and couldn’t find anything (or linked relevant results below)

Affected packages and versions: latest, Node.js lts

Steps to reproduce

Run npx create-next-app, turn on ESM Externals in next.config.js, then import xdm from any file in the pages directory.

import Head from 'next/head';
import { evaluate } from 'xdm';

export default function Home() {
  return (
    <div>
      <Head>
        <title>Create Next App</title>
      </Head>
    </div>
  );
}

throws error on npm run dev:

error - node:assert
Module build failed: UnhandledSchemeError: Reading from "node:assert" is not handled by plugins (Unhandled scheme).
Webpack supports "data:" and "file:" URIs by default.
You may need an additional plugin to handle "node:" URIs.
Could not find files for / in .next/build-manifest.json
Could not find files for / in .next/build-manifest.json

A bit confused as to why this is the only package that Next.js doesn't agree with in my dependencies

Add support for `import x from "y"` in `evaluate`

evaluate currently creates a β€œcontained” MDX file: it can export things, and you can pass things in, but it can’t import anything.

However, as evaluate is an async function, what if when we encounter import x from "y", we compile it to const x = await import("y").default instead?

an export {y} from "z" would become:

const {y} = await import("z")
// ...
return {
  y,
  // ...
}

Apparently there’s also an AsyncFunction global, so we can use new AsyncFunction(...) instead of new Function(...)

It can’t work in evaluateSync, so we’d still throw there on imports.

And it can’t work with the require hook, as that has to be sync...

Cannot close `paragraph` ($position): a different token (`mdxJsxTextTag`, $position) is open

First of all, this might not be the right repo for this error report - apologies if I've gotten it wrong, there's a lot of stuff going in in the code πŸ˜…

I'm running mdx-bundler on Next.js, and I'm getting crashes when I have embedded Codepen iframes in my MDX files. I think it has to do with how Codepen has text within the iframe - they look something like this:

<iframe
  height="265"
  scrolling="no"
  title="Extend Background Color"
  src="//codepen.io/IanMitchell/embed/BJRzML/?height=265&theme-id=0&default-tab=css,result&embed-version=2"
  frameBorder="no"
  allowtransparency="true"
  allowFullScreen={true}
  width="100%"
>
  See the Pen{" "}
  <a href="https://codepen.io/IanMitchell/pen/BJRzML/">
    Extend Background Color
  </a>{" "}
  by Ian Mitchell (<a href="https://codepen.io/IanMitchell">@IanMitchell</a>) on{" "}
  <a href="https://codepen.io">CodePen</a>.
</iframe>

If I remove all of the inner content of the iframe and make it a self-closing tag, things work perfectly. When there is content between the tags though, I get the following error:

node_modules/mdast-util-from-markdown/dist/index.js:419:12: error: [inMemory] Cannot close `paragraph` (22:3-22:20): a different token (`mdxJsxTextTag`, 22:17-22:20) is open

Add a migrating from mdx-js/mdx@1 guide

Initial checklist

  • I read the support docs
  • I read the contributing guide
  • I agree to follow the code of conduct
  • I searched issues and couldn’t find anything (or linked relevant results below)

Subject

More and more users are moving. Some stuff like remark-gfm not being there should be explained somewhere.

Problem

There are a few differences, now detailed at the end in a boring list of differences.

Solution

Migration guide. Either for mdx-js/mdx@1, mdx-js/mdx@2, or both?

Alternatives

πŸ€·β€β™‚οΈ

creating an XDM equivelent to the MDX playground

Hey, so I was looking to create an MDX playground and came across the one on the main MDXjs site (https://mdxjs.com/playground/) and I wondered if it would be possible to create one using XDM. So looking at https://github.com/mdx-js/mdx/blob/5169bf1f5d7b730b6f20a5eecee0b9ea0d977e56/packages/gatsby-theme-mdx/src/components/playground-editor.js and Chris Biscardi's post https://www.christopherbiscardi.com/post/using-mdxprovider-host-elements-in-react-live-scope I thought I'd give it a go. However, I think I may be misunderstanding how to use XDM in this instance because compileSync doesn't seem to output what the transformCode prop expects. Also, I was under the impression that we didn't need to use @mdx-js/react and the MDXProvider (which the MDX examples use) to render the content but this could be wrong?

Here's some code:

import React, { useState } from 'react';
import { LiveProvider, LiveEditor, LiveError, LivePreview } from 'react-live';
import { compileSync } from 'xdm';
import './App.css';

const transformCode = (mdx) => {
  let code = compileSync(mdx);

  // I know this is wrong but I'm wondering how we correctly render this
  return `render(${code.contents})`;
};

function App(props) {
  const [code, setCode] = useState('# Hello, world!\n\n This is some markup!');

  return (
    <div className="App">
      <LiveProvider
        code={code}
        transformCode={(code) => transformCode(code)}
      >
        <LiveEditor />
        <LiveError />
        <LivePreview />
      </LiveProvider>
    </div>
  );
}

export default App;

Any help on this will be greatly appreciated and help me to understand how to use XDM more

Cannot use `<!--` for comments with xdm, but Prettier cannot use `{/**/}` for comments


Initial checklist

  • I read the support docs
  • I read the contributing guide
  • I agree to follow the code of conduct
  • I searched issues and couldn’t find anything (or linked relevant results below)

This one is close, but not quite the same: #44

Affected packages and versions: Latest

Steps to reproduce

Here's a reproduction: https://codesandbox.io/s/xdm-create-react-app-starter-forked-fxd07

It's clear that this is expected (designed?) behavior. However, if my MDX is in a file that I format with prettier, prettier will convert that {/* comment */} into {/_ comment _/} which ... is not what I want πŸ˜…

Expected behavior

I expect to be able to use code comments without prettier changing it. Is this a prettier or xdm bug?

Actual behavior

The only code comment option at my disposal is not allowed by prettier.

My use case

I need to be able to have comments for prettier-ignore. In particular, one of my blog posts is trying to demonstrate that prettier can help you find bugs so I need that code block to not be formatted by prettier to illustrate my point. As of right now, my workaround is to put the comment within the code block, but that makes it visible to the reader which is distracting from the point.

Vue Router calls generated MDXContent function without props parameter, causing TypeError


Initial checklist

  • I read the support docs
  • I read the contributing guide
  • I agree to follow the code of conduct
  • I searched issues and couldn’t find anything (or linked relevant results below)

Affected packages and versions: [email protected]

Steps to reproduce

  1. Download linked example (I spent too much time trying to get it running directly and gave up, sorry)
  2. Run npm install or yarn install
  3. Start app: npm run serve and open browser to 'localhost:8080'
  4. Open web console
  5. Note errors in the console pointing to trying to access components property on undefined

Link to code example: https://codesandbox.io/s/dank-tree-b1ubs

Expected behavior

Should see output of Page.mdx underneath Vue logo

Actual behavior

The content of Page.mdx is not shown. Error:

vue-router.esm-bundler.js?6c02:3264 TypeError: Cannot read properties of undefined (reading 'components')
    at MDXContent ...

Notes

  • I think adding a check or default to the generated function seems appropriate since all I'm seeing it used for is accessing components which may be an empty object anyways.
    • Something like:
    function MDXContent(props = {}) {
     const _components = Object.assign({
       h1: "h1",
       p: "p"
     }, props && props.components), {wrapper: MDXLayout} = _components;
     const _content = ...
     return MDXLayout ? <MDXLayout {...props}>{_content}</MDXLayout> : _content;
    }
    export default MDXContent;
  • I'm willing to help but I've never seen the unified/remark framework before so it will take some time to get up to speed.

How to make XDM handle type = 'html' MDAST nodes?

I have a need where I need to insert raw HTML as a MDAST Node. I cannot use a remark plugin for that, for complicated reasons.
However, XDM doesn't seem to be able to handle such a node directly, and I'm not really sure what to do.

Reproduction:

import { compile } from 'xdm';
import { Processor } from 'xdm/lib/core';
import { visit } from 'unist-util-visit';

const doc = `

## Quo non vix

Dissilit animo Pandione ardua situ quis ense, Triumphum matre. Mentita
pudibundaque quid: que mutantur fervoribus sunt ver negari.

<Test />
`;

async function main() {
  const compiled = await compile(doc, {
    jsx: true,
    remarkPlugins: [plugin],
  });
  console.log(String(compiled));
}

main();

function plugin(this: Processor) {
  return async function transform(tree: Node) {
    const promises: Promise<void>[] = [];

    visit(tree, (node: Node, index: number, parent: Node) => {
      if (node.name === 'Test') {
        node.type = 'html';
        node.value = '<span>Test</span>';
      }
    });

    await Promise.all(promises);
  };
}

This is the error I get:

Error: Cannot handle unknown node `raw`

Full stack-trace:

/media/[user]/0CC5166B0CC5166B/Work/docs-helper/out.js:170880
  throw new Error("Cannot handle unknown node `" + node.type + "`");
        ^

Error: Cannot handle unknown node `raw`
    at Object.unknown2 (/media/[user]/0CC5166B0CC5166B/Work/docs-helper/out.js:170880:9)
    at Object.one3 [as handle] (/media/[user]/0CC5166B0CC5166B/Work/docs-helper/out.js:170813:17)
    at all3 (/media/[user]/0CC5166B0CC5166B/Work/docs-helper/out.js:171122:28)
    at Object.root2 (/media/[user]/0CC5166B0CC5166B/Work/docs-helper/out.js:171080:20)
    at Object.one3 [as handle] (/media/[user]/0CC5166B0CC5166B/Work/docs-helper/out.js:170813:17)
    at toEstree (/media/[user]/0CC5166B0CC5166B/Work/docs-helper/out.js:170856:24)
    at /media/[user]/0CC5166B0CC5166B/Work/docs-helper/out.js:171220:20
    at wrapped (/media/[user]/0CC5166B0CC5166B/Work/docs-helper/out.js:160343:16)
    at next (/media/[user]/0CC5166B0CC5166B/Work/docs-helper/out.js:160319:23)
    at done (/media/[user]/0CC5166B0CC5166B/Work/docs-helper/out.js:160364:7)

Any help would be appreciated πŸ™

xdm webpack loader doesn’t work with webpack cli

The issue can be reproduced from within this repository. I use Webpack 4, but the same issue can be reproduced using Webpack 5.

In this repository, add the following files:

webpack.config.cjs:

module.exports = {
  module: {
    rules: [
      {
        test: /\.mdx?$/,
        loader: require.resolve('./webpack.cjs')
      }
    ]
  }
};

src/index.js:

import './foo.md'

src/foo.md:

<Hello />

Now running the following command yields an error:

$ yarn webpack --mode production

The first time the following error is thrown:

(node:619531) UnhandledPromiseRejectionWarning: TypeError [ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING]: A dynamic import callback was not specified.
    at exports.importModuleDynamicallyCallback (internal/process/esm_loader.js:34:9)
    at Object.module.exports (/home/remco/Projects/xdm/webpack.cjs:14:3)
    at LOADER_EXECUTION (/home/remco/Projects/xdm/node_modules/loader-runner/lib/LoaderRunner.js:132:14)
    at runSyncOrAsync (/home/remco/Projects/xdm/node_modules/loader-runner/lib/LoaderRunner.js:133:4)
    at iterateNormalLoaders (/home/remco/Projects/xdm/node_modules/loader-runner/lib/LoaderRunner.js:250:2)
    at Array.<anonymous> (/home/remco/Projects/xdm/node_modules/loader-runner/lib/LoaderRunner.js:223:4)
    at runCallbacks (/home/remco/Projects/xdm/node_modules/enhanced-resolve/lib/CachedInputFileSystem.js:27:15)
    at /home/remco/Projects/xdm/node_modules/enhanced-resolve/lib/CachedInputFileSystem.js:200:4
    at /home/remco/Projects/xdm/node_modules/graceful-fs/graceful-fs.js:123:16
    at FSReqCallback.readFileAfterClose [as oncomplete] (internal/fs/read_file_context.js:63:3)
(Use `node --trace-warnings ...` to show where the warning was created)
(node:619531) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 2)
(node:619531) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

After a retry, this error is thrown:

(node:620590) UnhandledPromiseRejectionWarning: TypeError: Invalid host defined options
    at Object.module.exports (/home/remco/Projects/xdm/webpack.cjs:14:3)
    at LOADER_EXECUTION (/home/remco/Projects/xdm/node_modules/loader-runner/lib/LoaderRunner.js:132:14)
    at runSyncOrAsync (/home/remco/Projects/xdm/node_modules/loader-runner/lib/LoaderRunner.js:133:4)
    at iterateNormalLoaders (/home/remco/Projects/xdm/node_modules/loader-runner/lib/LoaderRunner.js:250:2)
    at Array.<anonymous> (/home/remco/Projects/xdm/node_modules/loader-runner/lib/LoaderRunner.js:223:4)
    at runCallbacks (/home/remco/Projects/xdm/node_modules/enhanced-resolve/lib/CachedInputFileSystem.js:27:15)
    at /home/remco/Projects/xdm/node_modules/enhanced-resolve/lib/CachedInputFileSystem.js:200:4
    at /home/remco/Projects/xdm/node_modules/graceful-fs/graceful-fs.js:123:16
    at FSReqCallback.readFileAfterClose [as oncomplete] (internal/fs/read_file_context.js:63:3)
(Use `node --trace-warnings ...` to show where the warning was created)
(node:620590) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 2)
(node:620590) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

Afterwards the errors seem to alternate randomly.

I did not run into this issue when using koa-webpack. This may explain why this issue isn’t caught by any tests.

Unexpected <p>'s

Hi Titus, thanks for xdm and for all the work you've done and continue to do in this space.

I build and maintain all the marketing and documentation sites for Modulz, Stitches and Radix

Recently I've upgraded nearly all of them from next-mdx-enhanced to next-mdx-remote but I've come across mdx-bundler by Kent (powered by xdm) and have decided to give it a go for the Stitches docs

Overall the migration was painless, but there's one particular issue that I'd like to raise.

In some of our documentation pages, we have a Preview component, where we render a React Component as an example of the code block we're demoing, like so:

image

The mdx responsive for that preview is as follow:

<Preview>
  <div style={{ display: 'flex', gap: '20px', alignItems: 'center' }}>
    <DemoButton color="gray" size="small">
      Button
    </DemoButton>
    <DemoButton color="violet" size="small">
      Button
    </DemoButton>
    <DemoButton color="gray" size="large">
      Button
    </DemoButton>
    <DemoButton color="violet" size="large">
      Button
    </DemoButton>
  </div>
</Preview>

But the above, generated this:

<div data-preview="true">
  <div style="display: flex; gap: 20px; align-items: center">
    <button>
      <p>Button</p>
    </button>
    <button>
      <p>Button</p>
    </button>
    <button>
      <p>Button</p>
    </button>
    <button>
      <p>Button</p>
    </button>
  </div>
</div>

Notice how each the text node in button is wrapped in p.

I've read #42 and #38 which were somewhat related, and based on your suggestions there, I was able to "solve this problem" in the following ways:

Close button on same line

<DemoButton color="gray" size="small">Button</DemoButton>

This works, but whenever I save prettier formats the code and breaks it again πŸ™„ Disabling prettier for mdx is not an option for us, unfortunately.

Wrap Preview in {}

{<Preview>
  <div style={{ display: 'flex', gap: '20px', alignItems: 'center' }}>
    <DemoButton color="gray" size="small">Button</DemoButton>
    <DemoButton color="violet" size="small">Button</DemoButton>
    <DemoButton color="gray" size="large">Button</DemoButton>
    <DemoButton color="violet" size="large">Button</DemoButton>
  </div>
</Preview>}

This works, but Im worried its "unconventional". It seems that it requires our team to learn some new syntax, and I can see things going wrong as our team and documentation sites grow. But maybe I should embrace that this is more like MDX-like, and not "mdx"? I dunno.


I'd like to ask, from your perspective, does it make sense that the text is wrapped in paragraphs?
Am I limited to the solutions above?

Thanks again!

How to pass data (frontmatter, exports) to layout?

Thank you for creating this library!

I would like to know how to access named (non-default) exports of MDX files via webpack (Next.js) from the rendering component. My main use-case is loading metadata, e.g. title for blog posts and then embedding the latter inside a <title> element:

---
title: Hello, world!
---

export { BlogPostLayout as default } from "./BlogPostLayout";

# Hello, world!

This is my first post.
// BlogPostLayout.jsx

export function BlogPost({ meta, children }) {
	console.log({ meta }); // undefined – should be populated with frontmatter data
	return (
		<article>
			{children}
		</article>
	);
}

rollup giving error

Rollupjs is giving error whenever i import 'xdm/rollup.js'
[!] Error: While loading the Rollup configuration from "rollup.config.js", Node tried to require an ES module from a CommonJS file, which is not supported. A common cause is if there is a package.json file with "type": "module" in the same folder. You can try to fix this by changing the extension of your configuration file to ".cjs" or ".mjs" depending on the content, which will prevent Rollup from trying to preprocess the file but rather hand it to Node directly.

How to force xdm to support HTML comments?

Is there any way to manually configure (or add a plugin to) XDM to support HTML comments <!-- ... --> in addition / instead of JSX comments {/* ... */}? Some tools like prettier still don't work with JSX comments.

I tried adding a remark plugin but it seems like XDM crashes before that step. Using regex to remove HTML comments prior to the compilation step somewhat screws up line numbering (though I think it's workable if you add empty lines to replace the deleted lines).

Compiling MDX in browser?

Is it possible to use xdm to compile MDX in the browser in order to build something like the MDX Playground?

I tried calling xdm.compile on the browser, but received a process is not defined error.

ESM in Gatsby

Is it possible to use this library with Gatsby? I can't seem to import xdm in Gatsby.

  • The following gatsby-node.js file:

    (async function() {
      let xdm = await import("xdm");
      console.log("xdm:", xdm);
    }());

    Fails with Error: TypeError [ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING]: A dynamic import callback was not specified.,

  • Importing xdm with esm:

    import { compile } from "xdm";
    console.log(compile);

    fails with SyntaxError: Cannot use import statement outside a module.

  • Adding type: "module" to package.json doesn't seem to work with Gatsby (fails with TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension "" for D:\Code\test-site\.cache\tmp-3340-dY2ATXgGrBY4 or a similar error)

  • Adding esm to Gatsby with the esm package fails with Error [ERR_REQUIRE_ESM]: Must use import to load ES Module.

Is it possible to use xdm with Gatsby as an alternative to gatsby-plugin-mdx?

esbuild-xdm plugin: Cannot read property 'start' of undefined

Initial checklist

  • I read the support docs
  • I read the contributing guide
  • I agree to follow the code of conduct
  • I searched issues and couldn’t find anything (or linked relevant results below)

Steps to reproduce

I'm trying to use shiki with a remark plugin using remark-shiki.

Whenever I include the plugin, I hit the following error:

node_modules/esbuild/lib/main.js:869:27: error: [plugin: esbuild-xdm] Cannot read property 'start' of undefined
    869 β”‚               let result = await callback2({
        β•΅                            ^
    at onload (.../node_modules/xdm/lib/integration/esbuild.js:86:32)
...
node_modules/esbuild/lib/main.js:736:22: note: This error came from the "onLoad" callback registered here
    736 β”‚         let promise = setup({

I wasn't sure this was an issue with the plugin or not, so I confirmed the same error occurs with a basic plugin:

export default function testPlugin() {
  return async () => {
    const highlighter = await shiki.getHighlighter({
      theme: 'github',
    });
  };
}

question: Can interpolation occur in link urls and references?

MDX supports interpolation in titles and link content. For example:

import { title } from 'introduction'

# {title}

[{title}](https://example.com)

is equivalent to:

<h1>Introduction</h1>
<a href="https://example.com">Introduction</a>

Question

Is there a way to interpolate link URLs and references? For example:

import { title, url } from 'introduction'

[this link]: {url}

Visit [this link] or [click here]({url})

To achieve the following:

<p>Visit <a href="/intro">this link</a> or <a href="/intro">click here</a></p> 

esbuild errors result in "Cannot read property 'line' of undefined" error


Initial checklist

  • I read the support docs
  • I read the contributing guide
  • I agree to follow the code of conduct
  • I searched issues and couldn’t find anything (or linked relevant results below)

Affected packages and versions: 1.12.2

Steps to reproduce

Still working on figuring out how to reproduce this reliably (for a separate issue I'm working on actually).

Expected behavior

Useful error messages when compiling fails.

Actual behavior

When there's an error in compiling MDX, xdm tries to construct a helpful error message, but ends up erroring out in the process resulting in a very unhelpful error message. Here's what it looks like:

> ../node_modules/esbuild/lib/main.js:869:27: error: [plugin: esbuild-xdm] Cannot read property 'line' of undefined
   869 β”‚               let result = await callback2({
       β•΅                            ^
   at onload (file:///Users/kentcdodds/code/remix-kentcdodds/node_modules/xdm/lib/integration/esbuild.js:172:17)
   at processTicksAndRejections (node:internal/process/task_queues:96:5)
   at callback (/Users/kentcdodds/code/remix-kentcdodds/node_modules/esbuild/lib/main.js:869:28)
   at handleRequest (/Users/kentcdodds/code/remix-kentcdodds/node_modules/esbuild/lib/main.js:652:30)
  ../node_modules/esbuild/lib/main.js:736:22: note: This error came from the "onLoad" callback registered here
   736 β”‚         let promise = setup({
       β•΅                       ^
   at setup (file:///Users/kentcdodds/code/remix-kentcdodds/node_modules/xdm/lib/integration/esbuild.js:69:11)
   at handlePlugins (/Users/kentcdodds/code/remix-kentcdodds/node_modules/esbuild/lib/main.js:736:23)
   at Object.buildOrServe (/Users/kentcdodds/code/remix-kentcdodds/node_modules/esbuild/lib/main.js:1024:7)
   at /Users/kentcdodds/code/remix-kentcdodds/node_modules/esbuild/lib/main.js:1754:17
   at new Promise (<anonymous>)
   at Object.build (/Users/kentcdodds/code/remix-kentcdodds/node_modules/esbuild/lib/main.js:1753:14)
   at Object.build (/Users/kentcdodds/code/remix-kentcdodds/node_modules/esbuild/lib/main.js:1629:51)
   at bundleMDX (/Users/kentcdodds/code/remix-kentcdodds/node_modules/mdx-bundler/dist/index.js:202:33)
   at processTicksAndRejections (node:internal/process/task_queues:96:5)
   at compileMdx (/Users/kentcdodds/code/remix-kentcdodds/build/index.js:664:18)
   at getFreshValue (/Users/kentcdodds/code/remix-kentcdodds/build/index.js:1234:17)
   at cachified (/Users/kentcdodds/code/remix-kentcdodds/build/index.js:1137:15)
   at getFreshValue (/Users/kentcdodds/code/remix-kentcdodds/build/index.js:1165:10)
   at cachified (/Users/kentcdodds/code/remix-kentcdodds/build/index.js:1137:15)
   at Object.loader18 (/Users/kentcdodds/code/remix-kentcdodds/build/index.js:8457:22)

This is going through mdx-bundler. The line that's erroring out is this one:

start.line != null &&

If I add console.error(error) right here, I get this output:

TypeError: Cannot read property '11' of undefined
    at lineStart (file:///Users/kentcdodds/code/remix-kentcdodds/node_modules/micromark-extension-gfm-table/lib/syntax.js:563:30)
    at go (/Users/kentcdodds/code/remix-kentcdodds/node_modules/micromark/dist/util/create-tokenizer.js:136:13)
    at main (/Users/kentcdodds/code/remix-kentcdodds/node_modules/micromark/dist/util/create-tokenizer.js:127:11)
    at Object.write (/Users/kentcdodds/code/remix-kentcdodds/node_modules/micromark/dist/util/create-tokenizer.js:70:5)
    at continueFlow (/Users/kentcdodds/code/remix-kentcdodds/node_modules/micromark/dist/initialize/document.js:119:15)
    at flowContinue (/Users/kentcdodds/code/remix-kentcdodds/node_modules/micromark/dist/initialize/document.js:97:7)
    at go (/Users/kentcdodds/code/remix-kentcdodds/node_modules/micromark/dist/util/create-tokenizer.js:136:13)
    at main (/Users/kentcdodds/code/remix-kentcdodds/node_modules/micromark/dist/util/create-tokenizer.js:130:9)
    at Object.write (/Users/kentcdodds/code/remix-kentcdodds/node_modules/micromark/dist/util/create-tokenizer.js:70:5)
    at fromMarkdown (/Users/kentcdodds/code/remix-kentcdodds/node_modules/xdm/node_modules/mdast-util-from-markdown/dist/index.js:26:34)
    at parse (/Users/kentcdodds/code/remix-kentcdodds/node_modules/xdm/node_modules/remark-parse/index.js:13:12)
    at Function.parse (/Users/kentcdodds/code/remix-kentcdodds/node_modules/xdm/node_modules/unified/index.js:271:12)
    at pipelineParse (/Users/kentcdodds/code/remix-kentcdodds/node_modules/xdm/node_modules/unified/index.js:23:16)
    at wrapped (/Users/kentcdodds/code/remix-kentcdodds/node_modules/xdm/node_modules/trough/wrap.js:25:19)
    at next (/Users/kentcdodds/code/remix-kentcdodds/node_modules/xdm/node_modules/trough/index.js:57:24)
    at Object.run (/Users/kentcdodds/code/remix-kentcdodds/node_modules/xdm/node_modules/trough/index.js:31:10) {
  fatal: true
}

I wouldn't call that very helpful, but it directs me at least to the source of the error much better πŸ˜…

I think the solution is to simply handle errors that lack location information better.

rehype-katex options

Hi, thanks for this awesome project. I have a question: according to rehype-katex doc, katex options could be passed along. However, I’m not seeing how I could pass katex options (e.g. macro definitions) when specifying rehype-katex as a rehype plugin.

Thanks!

Performance issues in a live editor using evaluate

Background

I am building a live editor using xdm evaluate. Functions the exact same as the MDX playground. From what I understand, a new React node is generated by evaluate for every keyboard event. This causes considerable keyboard input lag especially when you start embedding some and JSX components. I have tried to make iframe components load on click and initially render without src attribute but this didn't have an impact on input lag. I've also tried memoizing my evaluate call.

Reproduce input lag

Copy and paste this into the MDX Playground:

# Hello, world!

<button>Here is a button</button>

Try back space on this sentence to delete (will see lag). Iframes and React components reload on every single keystroke.

<iframe src="https://codesandbox.io/embed/nice-cloud-3chkc?fontsize=14&hidenavigation=1&theme=dark" />
<iframe src="https://codesandbox.io/embed/nice-cloud-3chkc?fontsize=14&hidenavigation=1&theme=dark" />
<iframe src="https://codesandbox.io/embed/nice-cloud-3chkc?fontsize=14&hidenavigation=1&theme=dark" />
<iframe src="https://codesandbox.io/embed/nice-cloud-3chkc?fontsize=14&hidenavigation=1&theme=dark" />
<iframe src="https://codesandbox.io/embed/nice-cloud-3chkc?fontsize=14&hidenavigation=1&theme=dark" />
<iframe src="https://codesandbox.io/embed/nice-cloud-3chkc?fontsize=14&hidenavigation=1&theme=dark" />
<iframe src="https://codesandbox.io/embed/nice-cloud-3chkc?fontsize=14&hidenavigation=1&theme=dark" />
<iframe src="https://codesandbox.io/embed/nice-cloud-3chkc?fontsize=14&hidenavigation=1&theme=dark" />
<iframe src="https://codesandbox.io/embed/nice-cloud-3chkc?fontsize=14&hidenavigation=1&theme=dark" />
<iframe src="https://codesandbox.io/embed/nice-cloud-3chkc?fontsize=14&hidenavigation=1&theme=dark" />
<iframe src="https://codesandbox.io/embed/nice-cloud-3chkc?fontsize=14&hidenavigation=1&theme=dark" />
<iframe src="https://codesandbox.io/embed/nice-cloud-3chkc?fontsize=14&hidenavigation=1&theme=dark" />
<iframe src="https://codesandbox.io/embed/nice-cloud-3chkc?fontsize=14&hidenavigation=1&theme=dark" />

Question

How can I make this editor more performant? Can it be done better using compile every few seconds?

Demoboard

Demoboard has an excellent live editor with no input lag, however compromises by not having a live output. Can I do same thing it does by using compile? And would I be able to avoid input lag but still have a live output?

Thank you!

'mdast-util-mdx' is expected to be of type CommonJS, which does not support named exports.


Initial checklist

  • I read the support docs
  • I read the contributing guide
  • I agree to follow the code of conduct
  • I searched issues and couldn’t find anything (or linked relevant results below)

Affected packages and versions: TODO

Steps to reproduce

SyntaxError: The requested module 'mdast-util-mdx' is expected to be of type CommonJS, which does not support named exports. CommonJS modules can be imported by importing the default export.
For example:
import pkg from 'mdast-util-mdx';
const {fromMarkdown, toMarkdown} = pkg;
    at ModuleJob._instantiate (internal/modules/esm/module_job.js:97:21)
    at async ModuleJob.run (internal/modules/esm/module_job.js:143:20)
    at async Loader.import (internal/modules/esm/loader.js:182:24)
    at async bundleMDX (/home/burhanuday/burhan/projects/portfolio-and-blog/node_modules/mdx-bundler/dist/index.js:60:27)
    at async getFileBySlug (webpack-internal:///./lib/mdx.js:83:7)
    at async getStaticProps (webpack-internal:///./pages/blog/[...slug].js:42:16)
    at async renderToHTML (/home/burhanuday/burhan/projects/portfolio-and-blog/node_modules/next/dist/next-server/server/render.js:27:1780)
    at async /home/burhanuday/burhan/projects/portfolio-and-blog/node_modules/next/dist/next-server/server/next-server.js:112:97
    at async __wrapper (/home/burhanuday/burhan/projects/portfolio-and-blog/node_modules/next/dist/lib/coalesced-function.js:1:330)
    at async DevServer.renderToHTMLWithComponents (/home/burhanuday/burhan/projects/portfolio-and-blog/node_modules/next/dist/next-server/server/next-server.js:137:387)

Gets fixed by replacing in /lib/plugin/remark-mdx.js on line 14

import {fromMarkdown, toMarkdown} from 'mdast-util-mdx'

with

import pkg from 'mdast-util-mdx';
const {fromMarkdown, toMarkdown} = pkg;

`Error: Not supported` with webpack and Next

Hey there!

Wrote a really quick plugin to make this work with webpack in next.js:

const xdmPlugin = config => ({
    ...config,
    webpack(wpcfg, ...a) {
        wpcfg = {
            ...wpcfg,
            module: {
                ...wpcfg.module,
                rules: [
                    ...wpcfg.module.rules,
                    { test: /\.mdx$/, use: [{loader: 'xdm/webpack.cjs', options: {}}] }
                ]
            }
        }

        if (config.webpack) wpcfg = config.webpack(wpcfg, ...a);
        return wpcfg;
    }
})

This just merges in the rules as specified in the readme. However, when I run yarn next dev, I get:

(node:3735) UnhandledPromiseRejectionWarning: Error: Not supported
    at Object.module.exports (/mnt/c/Users/thoma/OneDrive/Documents/devel/linear/node_modules/xdm/webpack.cjs:14:3)
    at LOADER_EXECUTION (/mnt/c/Users/thoma/OneDrive/Documents/devel/linear/node_modules/loader-runner/lib/LoaderRunner.js:119:14)
    at runSyncOrAsync (/mnt/c/Users/thoma/OneDrive/Documents/devel/linear/node_modules/loader-runner/lib/LoaderRunner.js:120:4)
    at iterateNormalLoaders (/mnt/c/Users/thoma/OneDrive/Documents/devel/linear/node_modules/loader-runner/lib/LoaderRunner.js:232:2)
    at Array.<anonymous> (/mnt/c/Users/thoma/OneDrive/Documents/devel/linear/node_modules/loader-runner/lib/LoaderRunner.js:205:4)
    at Storage.finished (/mnt/c/Users/thoma/OneDrive/Documents/devel/linear/node_modules/enhanced-resolve/lib/CachedInputFileSystem.js:55:16)  
    at provider (/mnt/c/Users/thoma/OneDrive/Documents/devel/linear/node_modules/enhanced-resolve/lib/CachedInputFileSystem.js:91:9)
    at /mnt/c/Users/thoma/OneDrive/Documents/devel/linear/node_modules/graceful-fs/graceful-fs.js:123:16
    at FSReqWrap.readFileAfterClose [as oncomplete] (internal/fs/read_file_context.js:53:3)

This corresponds to the import() line in webpack.cjs:

'use strict'

/**
 * Webpack loader
 *
 * @remarks
 * To do: once webpack supports ESM loaders, remove this wrapper.
 *
 * @param {string} code
 */
module.exports = function (code) {
  var callback = this.async()
  // Note that `import()` caches, so this should be fast enough.
  import('./lib/integration/webpack.js').then((module) => // <- this line
    module.loader.call(this, code, callback)
  )
}

I'm not sure what this means, frankly

Pass components prop to Next page

(I created a CodeSandbox, but it isn't showing anything. You're welcome to take a look at the original repo. If so my problem is demonstrated on the mdx-pages branch. Clone the repo, checkout mdx-pages branch, install dependencies with npm install, start server with npm run dev and visit http://localhost:3000/about. Notice the markdown paragraph is not styled.)


I have a Next app that uses emotion for styles. I followed the examples to edit next.config.js, and XDM is processing pages/about/index.mdx correctly. Inside pages/about/index.mdx I am wrapping the page with a layout. This was all very straightforward, so thank you for that!

I'd like to pass the components prop to pages/about/index.mdx, but I'm not clear how. I tried adding <MDXProvider ...> to pages/_app.tsx and then to components/templates/PrimaryMarkdown.tsx. In both cases a plain <p> is rendered. I also tried cloning children in the layout (to pass extra props), but that didn't work either.

How to migrate remark plugins that depend on `type: html` in the MDAST?

To my (very limited) understanding, the MDAST of the mdx compiler supported nodes with type: html (and type: jsx):

{
    type: "html",
    value: "<div>some html</div>",
}

However, the xdm compiler doesn't seem to support this.

Is there a recommended way to migrate remark plugins (like gatsby-remark-image) that use type: html or type: jsx? (I hacked together a solution that used another remark plugin to convert nodes of type: html into a custom JSX component that renders the raw HTML, but I'm unsure if this is the best approach.) Also, should the README specify that the MDAST of mdx and xdm are not the same?

As an unrelated side note, I've just about finished migrating my Gatsby site to use xdm instead of gatsby-plugin-mdx, and I've seen some significant build time improvements as a result! Thanks for creating this library!

Regarding `inlineCode` under `xdm` and `@mdx-js/loader`

I'm replacing @mdx-js/loader with xdm in my project https://github.com/chenxsan/voidjs/pull/426/files#diff-a94978bc24b042c57f0140409ac8c8101cad618fd24c90a2d88136e193b92e14, everything works fine (that's impressive!) but one.

With @mdx-js/loader, I can customize inlineCode targeting inline code like below:

// `this is inline code` I want to target
const inlineCode = (props) => <code className="bg-gray-200 px-2" {...props} />
<MDXProvider components={{inlineCode}>
  // ...
</MDXProvider>

image

However with xdm, inline code seem to be handled by code:

// `this is inline code` I want to target
const code = (props) => <code className="bg-gray-200 px-2" {...props} />
<MDXProvider components={{code}>
  // ...
</MDXProvider>

image

But I didn't find anything related to this change in the doc.

Local components are overridden with erronous ones

The following MDX code

export function Foo({ Div = 'div' }) {
  return <Div />;
}

Yields:

function _missingComponent(name) {
  return function () {
    throw new Error(
      "Component `" + name + "` was not imported, exported, or given"
    );
  };
}

export function Foo({ Div = "div" }) {
  const _components = {
      Div: _missingComponent("Div"),
    },
    { Div } = _components;
  return <Div />;
}

This also happens for components defined in the function body, i,e.:

export function Foo() {
  const Div = props.Div
  return <Div />;
}

I have my doubts if undefined components should be handled by xdm at all. If it’s unhandled, the user would get the runtime error Uncaught ReferenceError: Div is not defined, which is a familiar error we all know and love hate.

HTML table rows get rendered with paragraph tags

The following input in an mdx file:

<table>
    <tbody>
        <tr>
            <td>hello</td>
            <td>world</td>
        </tr>
        <tr>
            <td>0</td>
            <td>1</td>
        </tr>
    </tbody>
</table>

has the following output:

<table>
    <tbody>
        <tr>
            <p>
                <td>hello</td>
                <td>world</td>
            </p>
        </tr>
        <tr>
            <p>
                <td>0</td>
                <td>1</td>
            </p>
        </tr>
    </tbody>
</table>

The additional paragraph tags cause the table formatting to break. Is there a recommended workaround to this? (I'm not using GFM tables because I want to apply custom styles to some cells of the table.)

This works as expected but looks more messy:

<table>
    <tbody>
        <tr><td>hello</td>
            <td>world</td></tr>
        <tr><td>0</td>
            <td>1</td></tr>
    </tbody>
</table>

Output: (the <p> tag gets moved under tbody but it renders fine...)

<table>
    <tbody>
        <p>
            <tr>
                <td>hello</td>
                <td>world</td>
            </tr>
            <tr>
                <td>0</td>
                <td>1</td>
            </tr>
        </p>
    </tbody>
</table>

Code blocks inside components are processed incorrectly.

Initial checklist

  • I read the support docs
  • I read the contributing guide
  • I agree to follow the code of conduct
  • I searched issues and couldn’t find anything (or linked relevant results below)

Affected packages and versions: [email protected]

Steps to reproduce

MDX content:

<MyComp>
```js
import { Cyriac } from 'foo';
const Bar = () => <span>Hey</span>;
return Cyriac(Bar);
```
</MyComp>

RΓ©sulting XDM output:

import React from "react";
/*@jsxRuntime automatic @jsxImportSource react*/
function MDXContent(props = {}) {
  const _components = Object.assign({
    pre: "pre",
    code: "code"
  }, props.components), {ComponentWrapper, MyComp, wrapper: MDXLayout} = _components;
  const _content = <>
  <ComponentWrapper>
    <MyComp>
      <_components.pre>
        <_components.code className="language-js">
          {"import { Test } from 'foo';\nconst Bar = () => <span>Hey</span>;\nreturn Test(Bar);\n'}</_components.code></_components.pre></MyComp></ComponentWrapper></>;
  // ===================================================================>should be double quote ^
  return MDXLayout ? <MDXLayout {...props}>{_content}</MDXLayout> : _content;
}
export default MDXContent;

As you can see, the content of the codeblock is converted to a string, but the starting quote doesn't match the ending.

Link to code example: TODO

Expected behavior

import React from "react";
/*@jsxRuntime automatic @jsxImportSource react*/
function MDXContent(props = {}) {
  const _components = Object.assign({
    pre: "pre",
    code: "code"
  }, props.components), {ComponentWrapper, MyComp, wrapper: MDXLayout} = _components;
  const _content = <>
  <ComponentWrapper>
    <MyComp>
      <_components.pre>
        <_components.code className="language-js">
          {"import { Test } from 'foo';\nconst Bar = () => <span>Hey</span>;\nreturn Test(Bar);\n"}</_components.code></_components.pre></MyComp></ComponentWrapper></>;
  return MDXLayout ? <MDXLayout {...props}>{_content}</MDXLayout> : _content;
}
export default MDXContent;

Unexpected `p` in jsx output

Here's the code to run in Node REPL once you have xdm installed:

import('xdm').then(({compileSync}) => console.log(compileSync(`<div>
Author:
<a rel="author" href="https://github.com/">
  Sam
</a>
</div>
`)))

Here's the result:

VFile {
  data: {},
  messages: [],
  history: [],
  cwd: '/Users/sam/Documents/githubRepos/xdm-test',
  contents: '/*@jsxRuntime automatic @jsxImportSource react*/\n' +
    'import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime";\n' +
    'function MDXContent(_props) {\n' +
    '  const _components = Object.assign({\n' +
    '    p: "p"\n' +
    '  }, _props.components), {wrapper: MDXLayout} = _components;\n' +
    '  const _content = _jsx(_Fragment, {\n' +
    '    children: _jsxs("div", {\n' +
    '      children: [_jsx(_components.p, {\n' +
    '        children: "Author:"\n' +
    '      }), _jsx("a", {\n' +
    '        rel: "author",\n' +
    '        href: "https://github.com/",\n' +
    '        children: _jsx(_components.p, {\n' +
    '          children: "Sam"\n' +
    '        })\n' +
    '      })]\n' +
    '    })\n' +
    '  });\n' +
    '  return MDXLayout ? _jsx(MDXLayout, Object.assign({}, _props, {\n' +
    '    children: _content\n' +
    '  })) : _content;\n' +
    '}\n' +
    'export default MDXContent;\n'
}

As you can see in the result, there's a p around Author text, which I believe is wrong considering those are JSX syntax.

And here's the result of mdxjs playground :

/* @jsxRuntime classic */
/* @jsx mdx */
const layoutProps = {
  
};
const MDXLayout = "wrapper"
function MDXContent({
  components,
  ...props
}) {
  return <MDXLayout {...layoutProps} {...props} components={components} mdxType="MDXLayout">
    <div>
  Author:
  <a rel="author" href="https://github.com/">
    Sam
  </a>
    </div>
    </MDXLayout>;
}
;
MDXContent.isMDXComponent = true;

Wrapper for every component

Hi. I'm rather new to the MDX ecosystem and I'd like some pointers on what would be the best way to add a wrapper to every JSX processed by XDM.
I'd like like this:

# Hello, <Thing />

<Demo />

to turn into:

# Hello, <ComponentWrapper><Thing /><ComponentWrapper />

<ComponentWrapper><Demo /><ComponentWrapper />

Now, a naΓ―ve approach would be to transform the MDX file (through regex, or mdast manipulation?) before sending them to XDM, but I thought that maybe there'd be a better way with a process that plugs in directly into MDX pipeline.

Any pointer?

How to compile MDX string to JS function, rather than MDX file to JS module

I just found this project and I'm excited about moving past some of the issues that seem to have stalled at @mdx-js, but I'm not sure how to work with MDX source code that doesn't come from a file.

I'm working with MDX content from a CMS, so I'm doing a two-step process:

  1. compile the MDX at the data-fetching stage to a string (requires both @mdx-js/mdx and @babel/core)
  2. receive this string as props, and use an MDXRenderer component (which I also wrote myself, but the API is modelled on gatsby-plugin-mdx), which takes the code (as well as optional components prop), to execute it as a function.

Can I do this with xdm? The documentation seems aimed at .mdx files which are imported and thus compiled to modules, but I need a component function. My MDX source strings won't contain any import or export rules, but they will contain components that I'll need to pass in through a context provider or to their components prop.

Upgrade micromark deps?

Initial checklist

  • I read the support docs
  • I read the contributing guide
  • I agree to follow the code of conduct
  • I searched issues and couldn’t find anything (or linked relevant results below)

Affected packages and versions: 1.12.2

Steps to reproduce

I'm struggling to figure out how to reproduce this issue 😬 I'm experiencing it in https://github.com/kentcdodds/remix-kentcdodds but you'd need a remix license to run that πŸ˜…

Expected behavior

I expect to be able to use the latest version of all packages together without trouble.

Actual behavior

Basically, when I use the latest version of remark-gfm with the latest version of xdm, I run into the issue mentioned here: #75 and I'm pretty sure it has to do with the fact that xdm is still using (transitively) an old version of remark-parser and micromark:

npm ls micromark
[email protected] /Users/kentcdodds/code/remix-kentcdodds
β”œβ”€β”¬ [email protected]
β”‚ └─┬ [email protected]
β”‚   β”œβ”€β”¬ [email protected]
β”‚   β”‚ β”œβ”€β”¬ [email protected]
β”‚   β”‚ β”‚ └── [email protected]
β”‚   β”‚ β”œβ”€β”¬ [email protected]
β”‚   β”‚ β”‚ └── [email protected]
β”‚   β”‚ β”œβ”€β”¬ [email protected]
β”‚   β”‚ β”‚ └── [email protected]
β”‚   β”‚ └── [email protected]
β”‚   └─┬ [email protected]
β”‚     └─┬ [email protected]
β”‚       └── [email protected]
└─┬ [email protected]
  └─┬ [email protected]
    └── [email protected]

The only workaround I've found so far is to downgrade remark-gfm to 1.0.0.

WMR does not support `.mdx` files

I'm looking to give XDM here a try with WMR, but am running into issues with loading .mdx or .md files, specifically the following:

Loading module from β€œhttp://localhost:3000/content/docs/foo.mdx” was blocked because of a disallowed MIME type (β€œtext/mdx”).
Uncaught (in promise) TypeError: error loading dynamically imported module
Uncaught (in promise) TypeError: error loading dynamically imported module

Vague error message, but according to WMR's debug logs, it looks like the Rollup plugin isn't correctly handling the files and WMR is just serving the .mdx file directly. While I have no doubt that this is user error on my part, the ReadMe is 2400 lines long, full of expandable content sections, a maze of links back on itself, and no full samples. Reading through the Rollup, WMR, and Preact sections have me believing I just need to add the xdm/rollup.js plugin and set the jsxImportSource, but seemingly that doesn't result in a usable build.

// wmr.config.js
import { defineConfig } from 'wmr';
import xdm from 'xdm/rollup.js';

export default defineConfig({
    plugins: [
        xdm({ jsxImportSource: 'preact' }),
    ]
});

// bar.jsx
import Foo from './content/docs/foo.mdx';

export default function Bar() {
    return <Foo />;
}

// foo.mdx
## Hello world

<div class="foo">bar</div>

Having some examples would help massively in helping people get up and running (if they don't already exist somewhere), as the current ReadMe isn't really conducive to getting something usable.

As an API guide though, I don't believe I've seen anything more detailed, wow. Seriously impressive.

Warning: source-map provides its own type definitions, so you don't need @types/source-map installed

@types/source-map can be removed as it looks like source-map bundles its own types now:

"@types/source-map": "^0.5.0",

I got a warning while installing mdx-bundler so I found out xdm uses it:

npm WARN deprecated @types/[email protected]: This is a stub types definition for source-map (https://github.com/mozilla/source-map). source-map provides its own type definitions, so you don't need @types/source-map installed!

But I'm not sure as they have last released the version 2 years ago!

Outputs contains conflicting const declarations if plugins use _components

In remark-mdx-images I the following input yields the following output

input

![Alt text](image.png)

output

/*@jsxRuntime automatic @jsxImportSource react*/
import __0___image_png__ from "./image.png";
function MDXContent(_props) {
  const _components = Object.assign({
    p: "p"
  }, _props.components), {wrapper: MDXLayout, _components} = _components;
  const _content = <><_components.p><_components.img alt="Alt text" src={__0___image_png__} /></_components.p></>;
  return MDXLayout ? <MDXLayout {..._props}>{_content}</MDXLayout> : _content;
}
export default MDXContent;

In other words: _components is declared twice if a remark mdx plugin yields usage of _components. This also occurs when the jsx option or providerImportSource is set.

I’m using _components, because you recommended me to do so in a chat. I’m not a great fan about this, because _components feels like it should be an internal API. I think it makes sense if remark plugins could do something like this:

function remarkMdxFoo() {
  this.data('mdxComponentsIdentifier') // Returns '_components'
}

Using MDXProvider for evaluate, options.useMDXComponents

I am having trouble using an MDXProvider from @mdx-js/react for my MDX output after using evaluate. This application is a live runtime editor using evaluate. Here is a minimal reproduction:

evaluate - note useMDXComponents

// /lib/xdm.js

import { evaluate } from 'xdm';
import { useMDXComponents } from '@mdx-js/react';
import * as runtime from 'react/jsx-runtime.js';

export const evaluateMDX = async (mdx, setMDXOutput) => {
  const { default: Content } = await evaluate(mdx, {
    ...runtime,
    useMDXComponents,
    useDynamicImport: true,
  });

  setMDXOutput(Content);
};

Implementation in client using <MDXProvider />

// /components/MDXContent.js

import { useState, useMemo } from 'react';
import { evaluateMDX } from '@lib/xdm';
import { MDXProvider } from '@mdx-js/react';
import MDXComponents from '@components/MDXComponents';

export default function MDXContent({ mdx }) {
  const [MDXOutput, setMDXOutput] = useState('');

  useMemo(() => {
    evaluateMDX(mdx, setMDXOutput);
  }, [mdx]);

  return (
    <MDXProvider components={MDXComponents}>
      {MDXOutput}
    </MDXProvider>
  );
}

Output

No error messages, however the <MDXProvider /> doesn't replace any of the components defined in my MDXComponents.

Versions

Guesses

I think something really obvious is just flying over my head, or perhaps there is a version incompatibility as I'm on the previous version of xdm.

No `main` field in `package.json` leads to Next issue

Hi there! I'm using mdx-bundler in a next.js project and wanted to try Node version 16. When running yarn dev I get the following warning:

(node:6755) [DEP0151] DeprecationWarning: No "main" or "exports" field defined in the package.json for /node_modules/xdm/ resolving the main entry point "index.js", imported from /node_modules/mdx-bundler/dist/index.js.
Default "index" lookups for the main are deprecated for ES modules.
(Use `node --trace-deprecation ...` to show where the warning was created)

It seems that in version 16 either main or exports is required in the package.json. This issue is not breaking anything (it defaults to the right file) but it does clog up the console a little bit.

How to get import statements detected?

When I install xdm as a dependency, running the following:

xdm.compileSync(`import foo from 'foo'`)

interprets the import statement as text:

/*@jsxRuntime automatic @jsxImportSource react*/
import {Fragment as _Fragment, jsx as _jsx} from "react/jsx-runtime";
function MDXContent(_props) {
  const _components = Object.assign({}, _props.components), {wrapper: MDXLayout} = _components;
  const _content = _jsx(_Fragment, {
    children: "import foo from 'foo'"
  });
  return MDXLayout ? _jsx(MDXLayout, Object.assign({}, _props, {
    children: _content
  })) : _content;
}
export default MDXContent;

But the same thing works fine inside the xdm repo! πŸ˜…

/*@jsxRuntime automatic @jsxImportSource react*/
import {Fragment as _Fragment, jsx as _jsx} from "react/jsx-runtime";
import foo from 'foo';
// ...

Any ideas why? πŸ™

I'm using the latest xdm, 1.7.0.

Using JSX as props for a component


Initial checklist

  • I read the support docs
  • I read the contributing guide
  • I agree to follow the code of conduct
  • I searched issues and couldn’t find anything (or linked relevant results below)

Affected packages and versions: [email protected]

Steps to reproduce

  1. Create some MDX that includes a React component. Include a prop that has JSX passed to it, with extra spaces between the JSX tags and the text content.
  2. Run that MDX through @mdx-js/mdx. The MDX should render without error. The text content inside the JSX should be rendered as if it were inline MDX, converting symbols like **bold** to <strong>bold</strong>.
  3. Run that MDX through xdm. An error is thrown: Error: Unexpected end of file in expression, expected a corresponding closing brace for \{``

Link to code example: https://codesandbox.io/s/mdx-jsmdx-vs-xdm-9357n?file=/src/index.js

Expected behavior

XDM should provide similar results to @mdx-js/mdx, properly parsing the MDX and converting MDX symbols inside the prop into HTML/JSX elements.

Actual behavior

An error is thrown, indicating that XDM can't parse the MDX content for some reason.

Where to replace code blocks with custom JavaScript

Given a function createCodeSnippetComponent that transforms a snippet of source code into a JavaScript function representing a React component that renders that code, how and where would I hook into the pipeline to swap the node?

Specifically, if the createCodeSnippetComponent returns a string representation of the React component, where might I be able to replace the whole fenced code block with an MDX element representing that code?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.