feat: replace mdx-bundler with @mdx-js/mdx#16985
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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.
9a427f3 to
8fef117
Compare
…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/.
…nk lint for featureInfo
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 '.'
…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).
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
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); |
There was a problem hiding this comment.
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)
| // Match markdown image syntax:  — 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; |
There was a problem hiding this comment.
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 . 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.


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()withoutputFormat: 'function-body'remark-copy-imagesplugin replacesremarkMdxImages+ esbuild file loader (~60 lines)getMDXComponentpasses jsx runtime asarguments[0]instead of named scope variablesDocsChangelogmoved from MDX import to global component map (only MDX file with a real import)Next.js 16 fixes
images.localPatternsfor mdx-images with?v=query stringsnext/image<Image>(stricter types in v16)Reactimport (compile error in v16)pages/directory (_error.jsx→global-error.jsx)prism-sentryCSS to fix invalid::mozselectionselector (Turbopack rejects it)Cleanup
serverExternalPackagesandoutputFileTracingExcludesgenerate-doctreescript but is no longer in the Next.js bundleBuild results (local)
What to watch
=WxHalt text syntax (e.g. /organization/integrations/source-code-mgmt/github/)