Skip to content

feat: replace mdx-bundler with @mdx-js/mdx#16985

Open
sergical wants to merge 17 commits intomasterfrom
sdybskiy/turbopack-mdx-migration
Open

feat: replace mdx-bundler with @mdx-js/mdx#16985
sergical wants to merge 17 commits intomasterfrom
sdybskiy/turbopack-mdx-migration

Conversation

@sergical
Copy link
Member

Summary

Replace mdx-bundler (esbuild) with @mdx-js/mdx compile() for MDX compilation, removing the native esbuild binary dependency. This enables Turbopack compatibility and upgrades to Next.js 16.1.6.

What changed

MDX pipeline (src/mdx.ts)

  • bundleMDX()@mdx-js/mdx compile() with outputFormat: 'function-body'
  • New remark-copy-images plugin replaces remarkMdxImages + esbuild file loader (~60 lines)
  • getMDXComponent passes jsx runtime as arguments[0] instead of named scope variables
  • Frontmatter stripped via gray-matter before compilation (mdx-bundler did this internally)
  • DocsChangelog moved from MDX import to global component map (only MDX file with a real import)

Next.js 16 fixes

  • images.localPatterns for mdx-images with ?v= query strings
  • Removed prop spread into next/image <Image> (stricter types in v16)
  • Removed unused React import (compile error in v16)
  • Removed legacy pages/ directory (_error.jsxglobal-error.jsx)
  • Vendored prism-sentry CSS to fix invalid ::mozselection selector (Turbopack rejects it)

Cleanup

  • Removed esbuild, mdx-bundler from serverExternalPackages and outputFileTracingExcludes
  • esbuild stays as a dep for generate-doctree script but is no longer in the Next.js bundle

Build results (local)

Build Compile Pages Tests
Next 16 webpack 2.6 min 9,479 ✅ 131 ✅
Next 16 turbopack 8.5 sec 9,480 ✅ 131 ✅

What to watch

  • Vercel preview deployment (does the deploy succeed?)
  • Image rendering on pages with relative images (e.g. /organization/dynamic-sampling/)
  • Pages with =WxH alt text syntax (e.g. /organization/integrations/source-code-mgmt/github/)
  • Changelog page (component now from global map instead of MDX import)

@vercel
Copy link

vercel bot commented Mar 17, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
develop-docs Ready Ready Preview, Comment Mar 17, 2026 7:28pm
sentry-docs Ready Ready Preview, Comment Mar 17, 2026 7:28pm

Request Review

Replace mdx-bundler (esbuild) with @mdx-js/mdx compile() for MDX compilation.
This removes the native esbuild binary dependency, enabling Turbopack compatibility.

Upgrade Next.js from 15.5.12 to 16.1.6.

MDX pipeline changes:
- New remark-copy-images plugin replaces remarkMdxImages + esbuild file loader
- getMDXComponent now passes jsx runtime as arguments[0] for function-body output
- Strip frontmatter via gray-matter before compile (mdx-bundler did this internally)
- Move DocsChangelog from MDX import to global component map

Next.js 16 compatibility fixes:
- Add images.localPatterns for mdx-images with query strings
- Remove prop spread into next/image Image component (stricter types)
- Remove unused React import (now a compile error)
- Remove legacy pages/ directory (pages/_error.jsx → app/global-error.jsx)
- Vendor prism-sentry CSS to fix invalid ::mozselection selector

Verified: 9,480 pages build successfully with both webpack and turbopack.
131 tests pass.
…typo

CI has cached .next/cache/mdx-bundler/ entries from prior builds compiled
with mdx-bundler's output format (expects _jsx_runtime in scope). The new
@mdx-js/mdx function-body output uses arguments[0] for jsx runtime.

Renaming the cache dir from mdx-bundler → mdx-compile ensures old cached
entries are never loaded.
4 includes/ files imported FeatureInfo directly in MDX. With @mdx-js/mdx
(unlike mdx-bundler), import statements in MDX produce async code that
fails in the function-body output format used by getMDXComponent.

Move FeatureInfo to the global mdxComponents map, same pattern as
DocsChangelog. All MDX files are now import-free.
…y limit

Next.js 16 Turbopack generates ~7 RSC segment files per route (for
incremental prefetching), producing an 85k-file build output that exceeds
Vercel's 80MB deployment upload limit.

Using --webpack for production builds restores the v15-style output format
(~3 files per route). Turbopack is still used for local dev.

The Turbopack output format issue is expected to improve in Next.js 16.2
(data duplication fix). We can revisit removing --webpack then.
These packages are no longer used after migrating to @mdx-js/mdx compile().
- esbuild: was used by mdx-bundler for image file copying + generate-doctree
  script (replaced with ts-node --transpile-only)
- mdx-bundler: replaced by @mdx-js/mdx compile() with function-body output
- remark-mdx-images: replaced by remark-copy-images (local plugin)
Next.js 16's Vercel adapter produces a deploy payload exceeding the 80MB
limit, even with --webpack. This is independent of our MDX changes.

Reverting to Next 15 while keeping all MDX pipeline changes:
- mdx-bundler → @mdx-js/mdx compile() ✅
- esbuild, mdx-bundler, remark-mdx-images removed ✅
- remark-copy-images plugin ✅
- getMDXComponent function-body format ✅

Next.js 16 upgrade can be revisited when Vercel increases the deploy limit
or Next 16.2 reduces output size.
…m function bundle

The 80MB deploy failure was caused by public/mdx-images/ (363MB) and
public/md-exports/ (50MB) being included in the [[...path]] serverless
function bundle. These are static assets served from CDN — they don't
belong in the function.

Added to outputFileTracingExcludes for [[...path]]:
- public/mdx-images/**/*
- public/md-exports/**/*
- **/*.pdf

This mirrors the exclusions already present on the sitemap.xml and
platform-redirect routes.
Next.js 16 deploys exceed Vercel's 80MB serverless function limit
even with outputFileTracingExcludes. This is a Vercel adapter issue
with Next 16's output format, not related to our MDX changes.

This commit keeps all MDX pipeline improvements on Next 15:
- mdx-bundler → @mdx-js/mdx compile() ✅
- esbuild, mdx-bundler, remark-mdx-images removed ✅
- remark-copy-images plugin ✅
- getMDXComponent function-body format ✅
- generate-doctree uses ts-node instead of esbuild CLI ✅
- Added mdx-images/md-exports to outputFileTracingExcludes ✅

Next.js 16 upgrade tracked separately — needs Vercel limit increase
or ISR to reduce function bundle size.
…onent

Two fixes:

1. Images broken on cached builds: remark-copy-images only runs during
   MDX compilation, which is skipped on cache hits. Added
   copyImagesFromSource() that runs BEFORE the cache check, scanning
   the raw MDX source for image references and copying them to
   public/mdx-images/. This mirrors the old assetsCacheDir approach.

2. DocsChangelog broken: was an async server component that can't work
   inside new Function() MDX evaluation. Converted to a 'use client'
   component that fetches entries on the client side. This also fixes
   the pre-existing 404 on production /changelog/.
@sergical sergical changed the title feat: replace mdx-bundler with @mdx-js/mdx, upgrade to Next.js 16 feat: replace mdx-bundler with @mdx-js/mdx Mar 17, 2026
sergical and others added 2 commits March 17, 2026 14:44
14 MDX files use bare relative paths like 'img/foo.png' instead of
'./img/foo.png'. The old remarkMdxImages plugin normalized these by
prepending './'. Our remark-copy-images now does the same.

Also broadened the copyImagesFromSource regex to match any relative
image path, not just those starting with '.'
sergical and others added 2 commits March 17, 2026 15:11
…ign image extensions

1. remarkExtractFrontmatter is now a no-op since gray-matter strips
   frontmatter before compile(). Removed from plugin chain.

2. copyImagesFromSource regex now matches the same image extensions
   as the remark plugin (added webp, avif, ico, bmp, tiff).
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

// Copy images referenced in the source to public/mdx-images/ BEFORE the cache check.
// This ensures images exist even when MDX compilation is served from cache.
const cwd = path.dirname(sourcePath);
copyImagesFromSource(source, cwd, outdir);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unhandled sync I/O crash on read-only filesystem

Medium Severity

copyImagesFromSource is called unconditionally after an async mkdir that gracefully catches read-only FS errors. But copyImagesFromSource itself calls mkdirSync and copyFileSync without any try/catch. If the prior mkdir failed (read-only FS, e.g. Vercel Lambda), copyImagesFromSource's mkdirSync also fails but throws an unhandled exception, crashing the request. This is worsened by the new outputFileTracingExcludes entry for public/mdx-images/**/* on the catch-all path, meaning that directory won't exist at runtime.

Additional Locations (1)
Fix in Cursor Fix in Web

// Match markdown image syntax: ![...](path.ext) — both ./relative and bare relative
// Match any image extension to stay in sync with the remark plugin
const imageRegex =
/!\[[^\]]*\]\(([^)\s:]+\.(png|jpe?g|gif|svg|webp|avif|ico|bmp|tiff?)[^)]*)\)/gi;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regex misses images with markdown title text

Low Severity

The imageRegex in copyImagesFromSource captures trailing title text as part of the URL for images like ![Alt](./img.png "title"). The [^)\s:]+ part stops at the space, but backtracking causes the [^)]* portion to greedily capture "title" into the match. The resulting cleanUrl becomes ./img.png "title", failing existsSync. Since this function runs before the cache check to guarantee images exist on cache hits, any image with a markdown title would be silently skipped and potentially missing in CI builds with warm caches.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant