mrkeyoor.com_
Mon 21 Sept 01:58 UTC
npmWeb Frontendupdated 20 Sept 2026

@mdx-js/mdx review

@mdx-js/mdx 3.1.1 turns Markdown mixed with JSX, expressions, and ESM declarations into JavaScript component modules. Its compile path can run remark transforms on the Markdown tree, rehype transforms on the HTML tree, and recma transforms on the JavaScript tree. evaluate() compiles and executes trusted input, while run() executes function-body output prepared elsewhere. The current patch declares acorn as a direct dependency, fixes declaration types for import and export attributes, and accompanies fixes in the repository's esbuild and Rollup adapters. Our full browser import was 445.5 KB minified and 127.9 KB gzipped.

Verdict

@mdx-js/mdx 3.1.1 installed 111 packages in 11.7 seconds, and our full browser import measured 127.9 KB gzipped with no audit findings. Use the compiler directly for a trusted custom content pipeline; take a framework adapter for ordinary builds and a non-executable Markdown renderer for untrusted text.

We installed it

Lab card: what happened when we installed @mdx-js/mdxScreenshot of @mdx-js/mdx documentation
Install✓ · 11.7s111 packages on disk · 10 MB
ImportESM import works · require() works · ESM package with exports map
Browser127.9 KBgzipped (445.5 KB minified), bundled with esbuild
TypesTypeScript types bundled
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does @mdx-js/mdx install cleanly?

Yes. In a fresh container with an empty cache, npm install @mdx-js/mdx finished in 12 seconds, leaving 111 packages and 10 MB on disk. npm audit reported no known vulnerabilities.

How much does @mdx-js/mdx add to a browser bundle?

127.9 KB gzipped (445.5 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.

Does @mdx-js/mdx work with both ESM and CommonJS?

Yes. Both import '@mdx-js/mdx' and require('@mdx-js/mdx') worked in Node 22 in our run. The package is published as ESM with an exports map.

Does @mdx-js/mdx include TypeScript types?

Yes, type declarations ship inside the package, so no @types install is needed.

@mdx-js/mdx or @next/mdx: which should you use?

@next/mdx: Use it for source-tree MDX in Next.js when the framework should own compilation and routing. @mdx-js/mdx 3.1.1 installed 111 packages in 11.7 seconds, and our full browser import measured 127.9 KB gzipped with no audit findings.

When should you not use @mdx-js/mdx?

Your framework already has an MDX adapter. @next/mdx, @mdx-js/rollup, or @mdx-js/loader handles build integration with fewer custom moving parts.

API stability4/5Version 3 continues to center on compile(), evaluate(), run(), and createProcessor(), with explicit choices for output format, JSX runtime, development output, and three plugin layers. Release 3.1.1 fixes dependency and declaration details without changing those main calls. A major upgrade still coordinates MDX syntax, unified trees, plugin majors, the JSX runtime, and Node support, so the top-level API is steadier than the whole compiler stack.
Docs5/5The package documentation defines compiler options, shows generated code, explains component mapping and alternate JSX runtimes, links each integration, and labels evaluate as an eval-equivalent security risk. Migration pages and a browser playground make syntax changes testable. It also directs framework users to the relevant adapter instead of treating direct compilation as universal. Compatibility details for individual plugins remain distributed across their own projects.
Maintenance5/5GitHub reports 19,751 stars, only 20 open issues and pull requests, an unarchived repository, and a push on 2026-08-25. Package release 3.1.1 dates to 2025-08-29 and fixed the missing acorn declaration plus import-attribute types. The same monorepo release also repaired esbuild error handling and Vite query support in the Rollup adapter, while newer repository activity continues across the compiler and integrations.
Ecosystem5/5npm counted 10,574,261 downloads from 2026-08-19 through 2026-08-25. The project has adapters for webpack, Rollup and Vite, esbuild, Next.js, and multiple JSX runtimes, while unified supplies a large transform catalog. That gives a compiler author many choices, but a deployed pipeline is a compatibility set spanning @mdx-js/mdx, plugins, a framework adapter, and the runtime rather than one isolated dependency.

Use it if

  • You are building a docs or publishing compiler and need direct control over remark, rehype, and recma stages.
  • Trusted MDX stored outside the source tree must become a React, Preact, or other JSX-runtime component.
  • Compilation should happen during a build or on a server, with function-body output executed in a separate runtime step.
  • Authors truly need imports, exports, expressions, and named components inside prose instead of plain Markdown.
Skip it if

Setup reality

We installed @mdx-js/mdx 3.1.1 in a clean, unprivileged Node 22 Bookworm container. npm took 11.7 seconds, put 111 packages on disk, and used 10 MB. The package declares 25 direct dependencies and zero peers, with 384 KB unpacked. It includes TypeScript declarations, is ESM with an exports map, and worked through both require() and ESM import in our Node 22 sandbox. npm audit reported zero known vulnerabilities.

There are no credentials or required config files. compile() returns JavaScript source, not rendered HTML. evaluate() and run() need functions from a JSX runtime, and imports resolved during evaluation need a baseUrl. Pass a VFile with path and value when useful file names should appear in diagnostics. Version 3.1.1 now lists acorn directly instead of relying on its presence elsewhere in the resolved graph.

Plugin versions are the common failure point. MDX 3 expects current unified, remark, rehype, and recma conventions; an old visitor or AST assumption can fail deep inside processing. Pin compatible plugin majors and test real documents. format: 'md' treats braces and JSX-looking text as Markdown, while createProcessor() requires one fixed format and can process many files without recreating the pipeline each time.

Our esbuild measurement for an all-exports browser import was 445.5 KB minified and 127.9 KB gzipped. Precompiled component output is a different and usually smaller artifact. Compile at build time or on a trusted server. Function-body output can move the compiler out of the request path, but run() still executes code with every capability supplied in its options. Unknown-user text should stay in a non-executable Markdown renderer.

Patterns

Emit a JavaScript component module compile-mdx-file

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

const source = await fs.readFile('article.mdx')
const file = await compile({path: 'article.mdx', value: source})
await fs.writeFile('article.js', String(file))

compile returns JavaScript, not HTML. Supplying article.mdx as the VFile path gives parser and plugin errors a useful filename.

Create a component from trusted source evaluate-trusted-mdx

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

const module = await evaluate(source, {
  ...runtime,
  baseUrl: import.meta.url,
})
const Content = module.default

evaluate executes the MDX as JavaScript. Only pass content trusted to the same degree as source code in the application repository.

Move compilation out of the runtime compile-then-run

// trusted build service
const code = String(await compile(source, {
  outputFormat: 'function-body',
}))

// application runtime
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 avoids loading the compiler in the final render step. run still executes the supplied code and is not a sandbox.

Add GFM and heading identifiers configure-tree-plugins

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

const file = await compile(source, {
  remarkPlugins: [remarkGfm],
  rehypePlugins: [rehypeSlug],
})

remark plugins receive the Markdown tree and rehype plugins receive the HTML tree. Match their major versions to the unified stack used by MDX 3.

Replace generated elements at render time map-markdown-elements

<Content components={{
  h1: PageTitle,
  a: TrackedLink,
  pre: CodeBlock,
  wrapper: ArticleLayout,
}} />

This mapping changes elements produced from Markdown syntax. A component explicitly written in MDX keeps the identifier chosen by the author.

Read component mappings from a provider provide-components-through-context

// compiler option
await compile(source, {
  providerImportSource: '@mdx-js/react',
})

// React tree
<MDXProvider components={{h2: SectionTitle, code: InlineCode}}>
  <Post />
</MDXProvider>

The provider works only when compilation emits the matching useMDXComponents import through providerImportSource.

Convert YAML metadata into an export export-frontmatter-data

import remarkFrontmatter from 'remark-frontmatter'
import remarkMdxFrontmatter from 'remark-mdx-frontmatter'

const file = await compile(source, {
  remarkPlugins: [
    remarkFrontmatter,
    [remarkMdxFrontmatter, {name: 'frontmatter'}],
  ],
})

remark-frontmatter recognizes the YAML block; remark-mdx-frontmatter creates the named JavaScript export. Both plugins are required for this result.

Treat braces and tags as ordinary Markdown parse-plain-markdown

const file = await compile(markdown, {
  format: 'md',
})

md format disables MDX expressions, JSX, imports, and exports. Use it for legacy documents that contain literal braces or angle brackets.

Return the location of invalid syntax report-source-position

try {
  await compile({path: 'draft.mdx', value: source})
} catch (error) {
  console.error({
    line: error.line,
    column: error.column,
    reason: error.reason,
  })
}

Parse failures expose VFileMessage positions. Nonfatal lint or transform messages may instead be collected on the returned file.messages array.

Keep source locations during evaluation enable-development-output

import * as runtime from 'react/jsx-dev-runtime'

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

development: true expects jsx-dev-runtime. Mixing it with the production runtime causes missing helper errors.

Compile imports for Preact target-preact-runtime

const file = await compile(source, {
  jsxImportSource: 'preact',
})

Generated module output imports preact/jsx-runtime. evaluate receives runtime functions directly instead of using jsxImportSource for execution.

Process many files with one configuration reuse-configured-processor

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

const processor = createProcessor({
  format: 'mdx',
  remarkPlugins: [remarkGfm],
})

for (const document of documents) {
  const file = await processor.process(document)
  save(String(file))
}

createProcessor requires a fixed md or mdx format. It cannot auto-detect a different format for each document in the loop.

Alternatives

PackageRegistryPick it when
@next/mdxnpmUse it for source-tree MDX in Next.js when the framework should own compilation and routing.
next-mdx-remotenpmUse it for remotely stored MDX in Next.js when its serialization model fits your trust boundary.
react-markdownnpmUse it to render non-executable Markdown as React elements, especially for user-authored content.
markednpmUse it when an HTML string is enough and component syntax has no role in the content.

More web frontend guides

postcss · react · react-dom · tailwindcss · htmlparser2 · tailwind-merge · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.