Skip to navigation
9-13 minutes read
By Titus Wormer

Note: This is an old migration guide. See § Migrating from v2 to v3. The below is kept as is for historical purposes.

Migrating from v1 to v2

Contents

ESM

When upgrading to version 2, you might run into import problems. Our packages are now ESM only. You have to load them with import foo from 'foo' instead of const foo = require('foo'). Please see ¶ ESM in § Troubleshooting MDX.

Update @mdx-js/* packages

You need to make changes when upgrading some mdx-js/* packages. In this section we will discuss the needed changes for each package. If you’re experiencing problems, please see § Troubleshooting MDX for common errors and how to solve them.

@mdx-js/loader

To update our webpack loader @mdx-js/loader, please first update to recent versions of webpack and React. Then make sure ESM is supported. ESM is explained above. If you’re having trouble using ESM in your webpack config, here’s an example that works with our previous @mdx-js/loader (1.6.22):

Expand example of a webpack.config.js in ESM
webpack.config.js
import {fileURLToPath} from 'node:url'
import webpack from 'webpack'

const config = {
  context: fileURLToPath(new URL('src/', import.meta.url)),
  entry: ['./index.js'],
  mode: 'none',
  module: {
    rules: [
      {
        test: /\.mdx?$/,
        use: [
          {
            loader: 'babel-loader',
            options: {presets: ['@babel/preset-env', '@babel/preset-react']}
          },
          {
            loader: '@mdx-js/loader',
            /** @type {import('@mdx-js/loader').Options} */
            options: {}
          }
        ]
      }
    ]
  },
  output: {
    filename: 'bundle.js',
    path: fileURLToPath(new URL('dest/', import.meta.url))
  }
};

export default config

Then install MDX version 2:

Shell
npm install @mdx-js/loader @mdx-js/react remark-gfm

You can update your code as follows:

Before:

webpack.config.js
// …
{
  test: /\.mdx?$/,
  use: [
    {
      loader: 'babel-loader',
      options: {
        presets: [
          '@babel/preset-env',
          '@babel/preset-react'
        ]
      }
    },
    {
      loader: '@mdx-js/loader',
      /** @type {import('@mdx-js/loader').Options} */
      options: {}
    }
  ]
}
// …

After:

webpack.config.js
import remarkGfm from 'remark-gfm'

// …
{
  test: /\.mdx?$/,
  use: [
    {
      loader: 'babel-loader',
      options: {
        presets: [
          '@babel/preset-env'
        ]
      }
    },
    {
      loader: '@mdx-js/loader',
      /** @type {import('@mdx-js/loader').Options} */
      options: {
        providerImportSource: '@mdx-js/react',
        remarkPlugins: [remarkGfm]
      }
    }
  ]
}
// …

The above changes get MDX 2 close to how MDX 1 worked. You can make it simpler:

  • babel-loader is optional. It compiles modern JavaScript to JavaScript your users support. If you don’t need that you can remove it before @mdx-js/loader
  • remark-gfm is optional. It adds support for autolink literals, footnotes, strikethrough, tables, and task lists. If you don’t want them, you can uninstall it, remove the import, and remove it from options.remarkPlugins. More info in our guide on GFM
  • @mdx-js/react is optional. It adds support for context based components passing. If you don’t need that, you can uninstall it and remove options.providerImportSource. More info in ¶ MDX provider in § Using MDX

@mdx-js/loader now supports Preact and other JSX runtimes through configuration. Using Preact as an example:

webpack.config.js
// …
{
  loader: '@mdx-js/loader',
  /** @type {import('@mdx-js/loader').Options} */
  options: {
    jsxImportSource: 'preact',
    // Optional: either remove the following line or install `@mdx-js/preact`.
    providerImportSource: '@mdx-js/preact'
  }
}
// …

For more information, please see § API in @mdx-js/loader.

Changes

The options accepted by the loader changed:

  • renderer is replaced by jsxImportSource, providerImportSource, and others options. More info in § API in @mdx-js/mdx
  • Other options were undocumented but passed to @mdx-js/mdx. See @mdx-js/mdx below if needed

For more information, please see § API in @mdx-js/loader.

@mdx-js/react, @mdx-js/preact, @mdx-js/vue

To update our framework integrations, please first update to recent versions of React, Preact, or Vue 3. Then make sure ESM is supported. ESM is explained above. Then install version 2:

Shell
npm install @mdx-js/react # Change `react` to `preact` or `vue` if needed

Note that these packages now only add support for context based components passing. If you don’t need that, you can uninstall them. More info in ¶ MDX provider in § Using MDX.

Changes

The interface of changed slightly:

  • The named export mdx, which was a createElement function, is removed. You can use your framework’s functions instead: React.createElement, h from preact, etc.

@mdx-js/mdx

To update our core compiler @mdx-js/mdx, first make sure ESM is supported. ESM is explained above. Then install version 2:

Shell
npm install @mdx-js/mdx @mdx-js/react remark-gfm

You can update your code as follows:

Before:

old.js
import mdx from '@mdx-js/mdx'

const result = await mdx('# hi')

console.log(result)

After:

new.js
import {compile} from '@mdx-js/mdx'
import remarkGfm from 'remark-gfm'

const result = await compile('# hi', {
  providerImportSource: '@mdx-js/react',
  remarkPlugins: [remarkGfm]
})

console.log(String(result))

The above changes get MDX 2 close to how MDX 1 worked. You can make it simpler:

  • remark-gfm is optional. It adds support for autolink literals, footnotes, strikethrough, tables, and task lists. If you don’t want them, you can uninstall it, remove the import, and remove it from options.remarkPlugins. More info in our guide on GFM
  • @mdx-js/react is optional. It adds support for context based components passing. If you don’t need that, you can uninstall it and remove options.providerImportSource. More info in ¶ MDX provider in § Using MDX

@mdx-js/mdx now supports Preact and other JSX runtimes through configuration. Using Preact as an example:

new-preact.js
import {compile} from '@mdx-js/mdx'

const result = await compile('# hi', {jsxImportSource: 'preact'})

For more information, please see § API in @mdx-js/mdx

Changes

The interface of @mdx-js/mdx changed:

  • The default export is replaced by the named export compile
  • The named export sync is replaced by compileSync
  • The named export createCompiler is replaced by createProcessor
  • The named export createMdxAstCompiler is removed, use remark with remark-mdx instead
  • The return value of compile, compileSync are now VFiles instead of strings
  • There are new exports evaluate, evaluateSync to eval (compile and run) MDX

Options in version 1 were undocumented. If you used them:

  • filepath is replaced by accepting a VFile or compatible object as the first argument file
  • compilers is replaced by recmaPlugins
  • hastPlugins is replaced by rehypePlugins
  • mdPlugins is replaced by options.remarkPlugins
  • skipExport is removed, evaluate can do this automatically
  • wrapExport is removed, use a custom recma plugin instead
  • There are many new options to support different JSX runtimes, non-React JSX, compile JSX away, source maps, normal markdown, and otherwise change how MDX is compiled

For more information, please see § API in @mdx-js/mdx.

@mdx-js/runtime

We’ve deprecated @mdx-js/runtime. Our core compiler @mdx-js/mdx can now do the same and more. To update, first make sure ESM is supported. ESM is explained above. Please also make sure you are using a recent version of React. Then uninstall @mdx-js/runtime and install @mdx-js/mdx and @mdx-js/react:

Shell
npm uninstall @mdx-js/runtime
npm install @mdx-js/mdx @mdx-js/react

You can update your code as follows:

Before:

old.js
import MDX from '@mdx-js/runtime'

const components = {/* … */}
const value = '# hi'

export default function () {
  return <MDX components={components}>
    {value}
  </MDX>
}

After:

new.js
import * as runtime from 'react/jsx-runtime'
import * as provider from '@mdx-js/react'
import {evaluate} from '@mdx-js/mdx'

const components = {/* … */}
const value = '# hi'
const {default: Content} = await evaluate(value, {...provider, ...runtime})

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

The above changes get MDX 2 close to how MDX 1 worked. You can make it simpler:

@mdx-js/mdx now supports Preact and other JSX runtimes through configuration. Using Emotion as an example:

new-emotion.js
// …
import * as runtime from '@emotion/react/jsx-runtime'
// …

For more information, please see evaluate in @mdx-js/mdx.

Changes
  • The package @mdx-js/runtime is replaced by evaluate from @mdx-js/mdx
  • evaluate supports any JSX runtime and providers are optional
  • evaluate yields an MDXContent component just like how importing and MDX file works
  • evaluate supports imports and exports inside evaluated MDX

For more information, please see evaluate in @mdx-js/mdx.

remark-mdx

To update our remark plugin remark-mdx, first make sure ESM is supported. ESM is explained above. Then install version 2:

Shell
npm install remark-mdx

For more information, please see § Use in remark-mdx.

@mdx-js/vue-loader

We’ve deprecated @mdx-js/vue-loader. Our main loader @mdx-js/loader can now do the same and more. To update, first remove @mdx-js/vue-loader and upgrade to Vue 3. Then, see ¶ Vue in § Getting started on how to use Vue with MDX.

@mdx-js/parcel-plugin-mdx

We’ve deprecated @mdx-js/parcel-plugin-mdx. Parcel 2 has their own plugin. To update, first remove @mdx-js/parcel-plugin-mdx and upgrade to Parcel 2. Then, see ¶ Parcel in § Getting started on how to use Parcel with MDX.

Other packages

We removed several packages doing internal work for us. These packages are:

  • @mdx-js/util — no longer needed internal helpers
  • @mdx-js/test-util — no longer needed test helpers, evaluate can do them
  • gatsby-theme-mdx — no longer needed website template
  • remark-mdx-remove-imports, babel-plugin-extract-import-names — no longer needed transforms, our compiler handles imports
  • remark-mdx-remove-exports, babel-plugin-remove-export-keywords — no longer needed transforms, our compiler handles exports
  • babel-plugin-html-attributes-to-jsx — no longer needed transform, something similar is done by hast-util-to-estree
  • babel-plugin-apply-mdx-type-props — no longer needed transform due to the new architecture
  • create-mdx — no longer needed, we recommend to start your project with whatever other integrations you prefer and then add MDX to it

Update MDX files

Now you’ve updated our packages, you can update your MDX files. In version 2, we improved the syntax of the MDX format. When upgrading, you’ll likely get errors. Read those error messages carefully, as they typically explain what happened where and how to fix it. See § Troubleshooting MDX for common errors and how to solve them.

JSX

Let’s kick off with a couple of things that are now possible with JSX in MDX. You don’t need blank lines between JSX and markdown anymore:

<hgroup>
# This is a heading now
</hgroup>

You can now indent your JSX and markdown:

<article>
  <hgroup>
    # This is a heading now, not code or plain text
  </hgroup>
  <section>
    ```js
    // if you do want code blocks, use fenced code
    ```
  </section>
</article>

You can use markdown “inlines” but not “blocks” inside JSX if the text and tags are on the same line:

<div># this is not a heading but *this* is emphasis</div>

Text and tags on one line don’t produce blocks so they don’t produce <p>s either. On separate lines, they do:

<div>
  This is a `p`.
</div>

We differentiate using this rule (same line or not). Not based on semantics of elements in HTML. So you can build incorrect HTML (which you shouldn’t):

<h1 className="main">
  Don’t do this: it’s a `p` in an `h1`
</h1>

<h1 className="main">Do this: an `h1` with `code`</h1>

It’s not possible to wrap “blocks” if text and tags are on the same line but the corresponding tags are on different lines:

Welcome! <a href="about.html">

This is home of...

# The Falcons!</a>

That’s because to parse markdown, we first have to divide it into “blocks”. So in this case two paragraphs and a heading. Leaving an opening a tag in the first paragraph and a stray closing a tag in the heading.

We also fixed several cases where JSX was not parsed or handled correctly or even crashed. More on JSX in MDX is in ¶ JSX in § What is MDX?

Expressions

You can now use expressions in MDX to include JavaScript. You can also use them as an escape hatch if you ever just want strings or JSX rather than markdown:

{
  <h1>
    This just JSX, these *asterisks* have no meaning.
  </h1>
}

This is just {'`text`'}, not code.

More on expressions in MDX is in ¶ Expressions in § What is MDX?

ESM

You can more easily embed components in MDX because blank lines are allowed:

export function Button(props) {
  const style = {color: 'red'}

  return <button style={style} {...props} />
}

We also fixed several cases where import and export were not parsed or handled correctly or even crashed. More on ESM in MDX is in ¶ ESM in § What is MDX?

Markdown

We turned off several things that are allowed in regular markdown to make MDX less ambiguous. More on markdown in MDX and where it differs is in ¶ Markdown in § What is MDX?

GFM

We turned off GFM features in MDX by default. GFM extends CommonMark to add autolink literals, footnotes, strikethrough, tables, and task lists. If you do want these features, you can use a plugin. See our guide on GFM.

Update MDX content

The interface of the generated JavaScript from MDX changed:

  • Missing components now throw an error rather than render their children and emit a warning to the console, which is closer to how frameworks like React handle missing undefined JSX components
  • parent.child combos such as ol.li, to pass components, were removed, we recommend to style your ols, uls, and lis separately
  • The special component name inlineCode was removed, we recommend to use pre for the block version of code, and code for both the block and inline versions
  • MDXContent.isMDXContent was removed, we recommend treating MDXContent like any other component
  • Locally defined or imported components precede over passed components
  • We now “sandbox” components, for lack of a better name. It means that when you pass a component for h1, it does get used for # hi but not for <h1>hi</h1>
  • You can now pass and use objects of components: if you pass components={{theme}}, where theme is an object with a Box component, it can be used with: <theme.Box>stuff</theme.Box>
  • the values exports from an MDX file are no longer passed to layouts, you can achieve the same with:
    TypeScript
    import * as everything from './example.mdx'
    const {default: Content, ...exported} = everything
    <Content {...exported} />
    

We also fixed several cases where defined, imported, or passed components weren’t handled correctly or even crashed.


Phew. You made it! Welcome on the MDX 2 train, it’s been a journey. We know it wasn’t the easiest to migrate, we’re happy you’re still here, we’ll try to make migration easier next time. With our new AST, we’re able to create codemods from now on. <3