mrkeyoor.com_
Thu 06 Aug 23:54 UTC
npmWeb Frontendupdated 06 Aug 2026

@mdx-js/mdx

@mdx-js/mdx is the core compiler for MDX, the format that lets you write JSX, ESM imports/exports, and JS expressions inside markdown. compile() turns an MDX document into a JavaScript module: a unified pipeline parses markdown to mdast, runs remark plugins, converts to hast for rehype plugins, then to an estree for recma, and prints an ESM module whose default export is a component that renders through a JSX runtime. evaluate() compiles and runs a string in one step against a runtime you pass in (react/jsx-runtime, preact, and so on), and run() executes code that was compiled with outputFormat 'function-body'. Nearly every docs stack, from @next/mdx to Astro's MDX integration to Docusaurus and next-mdx-remote, sits on top of this package.

Verdict

The engine under practically every markdown-with-components stack, well maintained and honestly documented. Use it directly only when you are the one building the pipeline; in an app, take the framework integration, and never point evaluate() at content you did not write.

API stability4/5v3 shipped October 2023 and has needed only 3.1.0 (October 2024) and 3.1.1 (August 2025) since; compile, evaluate, and run are unchanged. The risk is coupling: a major bump here historically forces matching majors across your remark/rehype plugins, and the classic JSX runtime options are already deprecated for removal in the next major.
Docs5/5mdxjs.com has guides, a live playground, and migration docs, and the package README documents every ProcessorOptions field with before/after output examples. It is also unusually honest: it tells you to use an integration instead of this package, and flags evaluate as eval with danger markers.
Maintenance4/5Unified collective project pushed August 2026 with only 18 open issues counting PRs, and v3 tracks maintained Node versions. Release cadence is slow though: one release (3.1.1) in the roughly 22 months after 3.1.0's October 2024 predecessor window, so bug fixes can sit unreleased for a while.
Ecosystem5/5About 9.8M weekly downloads, and it is the compiler underneath @next/mdx, next-mdx-remote, @astrojs/mdx, and Docusaurus, with the whole remark/rehype/recma plugin catalog attached. If a markdown transform exists, it plugs in here.

Use it if

  • You are compiling MDX at runtime from a database or CMS, where bundler integrations cannot help; this is exactly what next-mdx-remote wraps
  • You are building your own static site generator or docs pipeline and need direct control over the compile step, plugins, and output
  • You need the compile-on-server, run-on-client split: compile with outputFormat 'function-body', ship the string, execute it with run() so the client never loads the compiler
  • You want the remark/rehype plugin catalog (GFM tables, syntax highlighting, math, heading slugs) applied to component-flavored markdown
  • You target a non-React framework: jsxImportSource points the output at preact, vue, or anything else with an automatic JSX runtime
Skip it if

Setup reality

npm install @mdx-js/mdx is one command with no peer dependencies and no native builds, and v3 runs on Node 16+. The friction starts after install. It is ESM-only, so CJS codebases and Jest need config surgery or a migration. The JSX runtime is not a dependency; you bring react/jsx-runtime (or preact's) yourself and spread it into evaluate/run, and forgetting baseUrl: import.meta.url breaks any MDX that uses import or export from. The real trap is the plugin version matrix: remark and rehype plugins must match the unified majors MDX 3 uses, so an old tutorial's remark-gfm 3 against today's MDX fails with cryptic tree errors, and every unified ecosystem major bump ripples through your whole plugin list at once.

Patterns

Compile an MDX file to a JavaScript modulecompile-mdx-to-js

import fs from 'node:fs/promises'
import {compile} from '@mdx-js/mdx'

const compiled = await compile(await fs.readFile('post.mdx'))
await fs.writeFile('post.js', String(compiled))

The output is an ESM module that imports react/jsx-runtime and default-exports an MDXContent component; it is code to bundle or write to disk, not HTML. Pass {path, value} instead of a bare string if you want file names in error messages.

Compile and run an MDX string in one stepevaluate-mdx-string

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

const {default: Content, ...exports} = await evaluate(mdxSource, {
  ...runtime,
  baseUrl: import.meta.url
})
// <Content components={{h1: MyHeading}} />

evaluate is eval: only feed it content you trust. The runtime is required, not bundled, and skipping baseUrl makes any import/export-from inside the MDX throw at run time. Anything the document exports comes back on the module object next to default.

Compile on the server, run on the clientsplit-compile-and-run

// server
import {compile} from '@mdx-js/mdx'
const code = String(await compile(src, {outputFormat: 'function-body'}))

// client, after fetching `code`
import {run} from '@mdx-js/mdx'
import * as runtime from 'react/jsx-runtime'
const {default: Content} = await run(code, {...runtime, baseUrl: import.meta.url})

function-body output takes the runtime from arguments[0] and returns its exports, so the client only needs run(), not the 124 KB compiler. It is still eval on the client, so only ship code your own server compiled.

Add GFM tables and other pluginsadd-remark-rehype-plugins

import {compile} from '@mdx-js/mdx'
import remarkGfm from 'remark-gfm'
import rehypeSlug from 'rehype-slug'

const file = await compile(src, {
  remarkPlugins: [remarkGfm],
  rehypePlugins: [rehypeSlug]
})
// plugin with options: remarkPlugins: [[remarkGfm, {singleTilde: false}]]

Plain MDX has no tables, strikethrough, or footnotes; remark-gfm adds them. Plugin majors must line up with the unified internals of MDX 3 (remark-gfm 4, remark-frontmatter 5), and an old major usually fails with a confusing tree-shape error rather than a clear version message.

Swap rendered elements via the components propinject-components-prop

import Post from './post.js' // compiled MDX

<Post components={{
  h1: TitleWithAnchor,
  a: FancyLink,
  pre: CodeBlock,
  wrapper: ArticleLayout
}} />

Keys replace elements generated from markdown syntax (# becomes your h1); literal JSX the author wrote is untouched. The special wrapper key wraps the whole document, which is the cheap way to add a layout without a provider.

Inject components app-wide with a providerprovider-use-mdx-components

// compile step
await compile(src, {providerImportSource: '@mdx-js/react'})

// app
import {MDXProvider} from '@mdx-js/react'

<MDXProvider components={{h2: H2, code: Code}}>
  <Post />
</MDXProvider>

The provider only takes effect if the MDX was compiled with providerImportSource; without it the generated code never calls useMDXComponents. It is React context under the hood, so the MDX docs themselves suggest the components prop where you can, for performance.

Read YAML frontmatter as a named exportfrontmatter-to-export

import {compile} from '@mdx-js/mdx'
import remarkFrontmatter from 'remark-frontmatter'
import remarkMdxFrontmatter from 'remark-mdx-frontmatter'

const file = await compile(src, {
  remarkPlugins: [
    remarkFrontmatter,
    [remarkMdxFrontmatter, {name: 'frontmatter'}]
  ]
})
// output gains: export const frontmatter = {title: '...'}

remark-frontmatter alone only stops the YAML from rendering as a paragraph; remark-mdx-frontmatter is what turns it into a real export. With evaluate() the object comes back on the returned module, so you can read metadata and render with one compile.

Treat input as markdown, not MDXcompile-plain-markdown

import {compile} from '@mdx-js/mdx'

// '<' and '{' are plain text again, no JSX or ESM parsing
const file = await compile(cmsBody, {format: 'md'})

In 'mdx' format a lone '<' or '{' in prose is a syntax error, which breaks a lot of pre-existing markdown. format 'md' turns those features off while keeping the component output and components prop. compile() defaults to detecting by file extension; createProcessor() does not detect and defaults to 'mdx'.

Catch and report bad MDXhandle-compile-errors

import {compile} from '@mdx-js/mdx'

try {
  await compile(userMdx, {format: 'mdx'})
} catch (error) {
  // VFileMessage with position info
  console.error(`${error.line}:${error.column} ${error.reason}`)
}

Parse failures throw a VFileMessage carrying line, column, and reason (for example 'Unexpected end of file in expression'), which is what you surface to authors in a CMS preview. Lint-style remark plugins do not throw; their findings land as warnings on file.messages instead.

Get useful errors for missing componentsdebug-missing-components

import {evaluate} from '@mdx-js/mdx'
import * as devRuntime from 'react/jsx-dev-runtime'

const {default: Content} = await evaluate(src, {
  development: true,
  ...devRuntime,
  baseUrl: import.meta.url
})

In production mode 'Expected component X to be defined' tells you nothing about where; development: true appends the source position and file name. It needs the jsx-dev-runtime module, and the bundler integrations flip this flag for you based on the build mode.

Compile for a framework other than Reacttarget-preact-or-vue

import {compile} from '@mdx-js/mdx'

const file = await compile(src, {jsxImportSource: 'preact'})
// output imports from 'preact/jsx-runtime' instead of 'react/jsx-runtime'

Any package exposing an automatic JSX runtime works; for evaluate() you skip this option and just spread the other runtime in. The classic runtime options (jsxRuntime: 'classic', pragma) still exist but are deprecated and slated for removal in the next major.

Reuse one processor for many filesreuse-processor

import {createProcessor} from '@mdx-js/mdx'
import remarkGfm from 'remark-gfm'

const processor = createProcessor({remarkPlugins: [remarkGfm]})

for (const doc of docs) {
  const file = await processor.process(doc) // vfile with .value
}

compile() builds a fresh unified pipeline per call, so a site build over hundreds of files should create the processor once. Note createProcessor rejects format: 'detect'; you pick 'md' or 'mdx' up front.

Alternatives

PackageRegistryPick it when
@next/mdxnpmNext.js with local .mdx files: it wires this compiler into the build so you just import pages
next-mdx-remotenpmMDX strings from a CMS in Next.js: it wraps compile/run for you, including the RSC story
react-markdownnpmUser-submitted or plain markdown: renders to React elements with no eval and no JSX syntax to trip on
markednpmYou only need markdown to HTML strings, fast, with no component layer at all