From 6cd714b0ce41743eb6eadbba8a8fc68e52b13465 Mon Sep 17 00:00:00 2001 From: paulj Date: Wed, 21 Jan 2026 18:21:02 -0500 Subject: [PATCH 1/4] fix(DOCS-A0W): Replace mdx-bundler/client with local getMDXComponent Fixes DOCS-A0W - "Cannot find module 'mdx-bundler/client'" and "require() of ES Module not supported" errors in Vercel serverless. Root cause: mdx-bundler has CJS/ESM compatibility issues where client/index.js uses require() to load ../dist/client.js, but the parent package has "type": "module" causing Node.js to reject the require() call at runtime. Solution: Inline the getMDXComponent function (~30 lines) which only depends on React. This eliminates the runtime dependency on mdx-bundler/client entirely, allowing full exclusion of mdx-bundler from serverless bundles. Changes: - Add src/getMDXComponent.ts with local implementation - Update 4 files to import from local module - Simplify next.config.ts exclusion to 'node_modules/mdx-bundler/**/*' Bundle impact: Better - can now fully exclude mdx-bundler instead of partial 'dist/!(client*)' pattern which may not work reliably. Co-Authored-By: Claude --- app/[[...path]]/page.tsx | 2 +- next.config.ts | 30 ++++++++------ src/components/apiPage/index.tsx | 2 +- src/components/include.tsx | 2 +- src/components/platformContent.tsx | 2 +- src/getMDXComponent.ts | 65 ++++++++++++++++++++++++++++++ 6 files changed, 86 insertions(+), 17 deletions(-) create mode 100644 src/getMDXComponent.ts diff --git a/app/[[...path]]/page.tsx b/app/[[...path]]/page.tsx index 89936c5168c0c..7d3c950ed1d5a 100644 --- a/app/[[...path]]/page.tsx +++ b/app/[[...path]]/page.tsx @@ -1,6 +1,5 @@ import {Fragment, useMemo} from 'react'; import * as Sentry from '@sentry/nextjs'; -import {getMDXComponent} from 'mdx-bundler/client'; import {Metadata} from 'next'; import {notFound} from 'next/navigation'; @@ -20,6 +19,7 @@ import { getPreviousNode, nodeForPath, } from 'sentry-docs/docTree'; +import {getMDXComponent} from 'sentry-docs/getMDXComponent'; import {isDeveloperDocs} from 'sentry-docs/isDeveloperDocs'; import { getDevDocsFrontMatter, diff --git a/next.config.ts b/next.config.ts index b89fa77ad5236..1e6c3d46b7033 100644 --- a/next.config.ts +++ b/next.config.ts @@ -10,8 +10,10 @@ import {redirects} from './redirects.js'; // output is used at runtime, so bundling these ~150-200MB of dependencies would bloat // functions unnecessarily and cause deployment failures. // -// Note: mdx-bundler/client (getMDXComponent) is a tiny runtime module needed by -// app/[[...path]]/page.tsx, so we exclude only the build parts (dist/!(client)*). +// Note: We use a local getMDXComponent implementation (src/getMDXComponent.ts) +// instead of mdx-bundler/client to avoid CJS/ESM compatibility issues at runtime. +// This allows us to fully exclude mdx-bundler from serverless bundles. +// Fixes: DOCS-A0W const outputFileTracingExcludes = process.env.NEXT_PUBLIC_DEVELOPER_DOCS ? { '/**/*': [ @@ -30,9 +32,11 @@ const outputFileTracingExcludes = process.env.NEXT_PUBLIC_DEVELOPER_DOCS 'node_modules/@prettier/**/*', 'node_modules/sharp/**/*', 'node_modules/mermaid/**/*', - // Exclude MDX processing dependencies (but keep mdx-bundler/client for runtime) - 'node_modules/mdx-bundler/dist/!(client*)', - 'node_modules/mdx-bundler/node_modules/**/*', + // Exclude MDX processing dependencies + // Note: We use a local getMDXComponent implementation (src/getMDXComponent.ts) + // instead of mdx-bundler/client to avoid CJS/ESM compatibility issues at runtime. + // Fixes: DOCS-A0W + 'node_modules/mdx-bundler/**/*', 'node_modules/rehype-preset-minify/**/*', 'node_modules/rehype-prism-plus/**/*', 'node_modules/rehype-prism-diff/**/*', @@ -59,9 +63,11 @@ const outputFileTracingExcludes = process.env.NEXT_PUBLIC_DEVELOPER_DOCS 'node_modules/@prettier/**/*', 'node_modules/sharp/**/*', 'node_modules/mermaid/**/*', - // Exclude MDX processing dependencies (but keep mdx-bundler/client for runtime) - 'node_modules/mdx-bundler/dist/!(client*)', - 'node_modules/mdx-bundler/node_modules/**/*', + // Exclude MDX processing dependencies + // Note: We use a local getMDXComponent implementation (src/getMDXComponent.ts) + // instead of mdx-bundler/client to avoid CJS/ESM compatibility issues at runtime. + // Fixes: DOCS-A0W + 'node_modules/mdx-bundler/**/*', 'node_modules/rehype-preset-minify/**/*', 'node_modules/rehype-prism-plus/**/*', 'node_modules/rehype-prism-diff/**/*', @@ -139,11 +145,9 @@ const nextConfig = { '@esbuild/linux-x64', '@esbuild/win32-x64', // Note: mdx-bundler is intentionally NOT in serverExternalPackages. - // The package is ESM-only ("type": "module") and cannot be require()'d at runtime. - // Keeping it out allows webpack to bundle mdx-bundler/client properly while - // outputFileTracingExcludes still prevents the heavy build-time parts from - // being included in the serverless function bundle. - // Fixes: DOCS-A0W + // We use a local getMDXComponent implementation (src/getMDXComponent.ts) and + // webpack bundles the mdx-bundler code that's imported for API pages. + // This avoids CJS/ESM compatibility issues at runtime. Fixes: DOCS-A0W 'sharp', '@aws-sdk/client-s3', '@google-cloud/storage', diff --git a/src/components/apiPage/index.tsx b/src/components/apiPage/index.tsx index 1fe9ae31d6f23..fbe52d2c8ffd0 100644 --- a/src/components/apiPage/index.tsx +++ b/src/components/apiPage/index.tsx @@ -1,8 +1,8 @@ import {Fragment, ReactElement, useMemo} from 'react'; import {bundleMDX} from 'mdx-bundler'; -import {getMDXComponent} from 'mdx-bundler/client'; import {type API} from 'sentry-docs/build/resolveOpenAPI'; +import {getMDXComponent} from 'sentry-docs/getMDXComponent'; import {mdxComponents} from 'sentry-docs/mdxComponents'; import remarkCodeTabs from 'sentry-docs/remark-code-tabs'; import remarkCodeTitles from 'sentry-docs/remark-code-title'; diff --git a/src/components/include.tsx b/src/components/include.tsx index 89657516e97e6..04e4a941ae725 100644 --- a/src/components/include.tsx +++ b/src/components/include.tsx @@ -1,6 +1,6 @@ import {useMemo} from 'react'; -import {getMDXComponent} from 'mdx-bundler/client'; +import {getMDXComponent} from 'sentry-docs/getMDXComponent'; import {getFileBySlugWithCache} from 'sentry-docs/mdx'; import {mdxComponents} from 'sentry-docs/mdxComponents'; diff --git a/src/components/platformContent.tsx b/src/components/platformContent.tsx index 1ff6e6de34902..6721fb0d8f534 100644 --- a/src/components/platformContent.tsx +++ b/src/components/platformContent.tsx @@ -1,9 +1,9 @@ import fs from 'fs'; import {cache, useMemo} from 'react'; -import {getMDXComponent} from 'mdx-bundler/client'; import {getCurrentGuide, getDocsRootNode, getPlatform} from 'sentry-docs/docTree'; +import {getMDXComponent} from 'sentry-docs/getMDXComponent'; import {getFileBySlugWithCache} from 'sentry-docs/mdx'; import {mdxComponents} from 'sentry-docs/mdxComponents'; import {serverContext} from 'sentry-docs/serverContext'; diff --git a/src/getMDXComponent.ts b/src/getMDXComponent.ts new file mode 100644 index 0000000000000..22babc58417c6 --- /dev/null +++ b/src/getMDXComponent.ts @@ -0,0 +1,65 @@ +/** + * Local implementation of getMDXComponent from mdx-bundler/client. + * + * This eliminates the runtime dependency on mdx-bundler/client which has + * CJS/ESM compatibility issues when loaded in Vercel serverless functions. + * The mdx-bundler package uses "type": "module" but the client/ subdirectory + * uses CommonJS require() to load ../dist/client.js, causing: + * "require() of ES Module not supported" + * + * Since getMDXComponent only needs React at runtime (and the compiled MDX code + * is just a string), we can safely inline this implementation. + * + * Fixes: DOCS-A0W + * @see https://github.com/kentcdodds/mdx-bundler/blob/main/src/client.js + */ +import type {ComponentType, FunctionComponent} from 'react'; +// These namespace imports are required - the MDX runtime expects React, ReactDOM, +// and jsx_runtime objects in scope to call methods like React.createElement() +// eslint-disable-next-line no-restricted-imports +import * as React from 'react'; +import * as jsxRuntime from 'react/jsx-runtime'; +// eslint-disable-next-line no-restricted-imports +import * as ReactDOM from 'react-dom'; + +export interface MDXContentProps { + [key: string]: unknown; + components?: Record; +} + +/** + * Takes the compiled MDX code string from bundleMDX and returns a React component. + * + * @param code - The string of code you got from bundleMDX + * @param globals - Any variables your MDX needs to have accessible when it runs + * @returns A React component that renders the MDX content + */ +export function getMDXComponent( + code: string, + globals?: Record +): FunctionComponent { + const mdxExport = getMDXExport(code, globals); + return mdxExport.default; +} + +/** + * Takes the compiled MDX code string from bundleMDX and returns all exports. + * + * @param code - The string of code you got from bundleMDX + * @param globals - Any variables your MDX needs to have accessible when it runs + * @returns All exports from the MDX module, including the default component + */ +export function getMDXExport< + ExportedObject = {default: FunctionComponent}, +>(code: string, globals?: Record): ExportedObject { + const scope = { + React, + ReactDOM, + _jsx_runtime: jsxRuntime, + ...globals, + }; + // The MDX runtime requires dynamic function construction to evaluate compiled code + // eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func + const fn = new Function(...Object.keys(scope), code); + return fn(...Object.values(scope)); +} From e810778c1c6b52b9562fd1721562a2ad4b8c06cc Mon Sep 17 00:00:00 2001 From: paulj Date: Wed, 21 Jan 2026 18:31:56 -0500 Subject: [PATCH 2/4] fix(DOCS-A3H): Pre-compile API markdown at build time Fixes DOCS-A3H - "The package @esbuild/linux-x64 could not be found" errors on API documentation pages. Root cause: ApiPage used bundleMDX() at runtime to compile markdown descriptions, but bundleMDX requires esbuild which is excluded from serverless bundles to save space. Solution: Pre-compile markdown to HTML at build time in resolveOpenAPI.ts using remark/rehype (already available). Store as `descriptionHtml` and render with dangerouslySetInnerHTML in ApiPage. Changes: - Add compileMarkdownToHtml() in resolveOpenAPI.ts using unified/remark - Add descriptionHtml field to API type - Compile markdown during API data processing - Update ApiPage to render pre-compiled HTML - Remove bundleMDX, getMDXComponent, mdxComponents imports from ApiPage Bundle impact: Better - removes runtime dependency on esbuild entirely for API pages. remark/unified only used at build time. Co-Authored-By: Claude --- src/build/resolveOpenAPI.ts | 46 +++++++++++++++++++++- src/components/apiPage/index.tsx | 66 ++++++++------------------------ 2 files changed, 59 insertions(+), 53 deletions(-) diff --git a/src/build/resolveOpenAPI.ts b/src/build/resolveOpenAPI.ts index 42ac5b6992766..312c5194853b0 100644 --- a/src/build/resolveOpenAPI.ts +++ b/src/build/resolveOpenAPI.ts @@ -4,8 +4,29 @@ import {promises as fs} from 'fs'; +import rehypeStringify from 'rehype-stringify'; +import remarkGfm from 'remark-gfm'; +import remarkParse from 'remark-parse'; +import remarkRehype from 'remark-rehype'; +import {unified} from 'unified'; + import {DeRefedOpenAPI} from './open-api/types'; +/** + * Compile markdown to HTML at build time. + * This avoids needing esbuild/mdx-bundler at runtime. + * Fixes: DOCS-A3H + */ +async function compileMarkdownToHtml(markdown: string): Promise { + const result = await unified() + .use(remarkParse) + .use(remarkGfm) + .use(remarkRehype) + .use(rehypeStringify) + .process(markdown); + return String(result); +} + // SENTRY_API_SCHEMA_SHA is used in the sentry-docs GHA workflow in getsentry/sentry-api-schema. // DO NOT change variable name unless you change it in the sentry-docs GHA workflow in getsentry/sentry-api-schema. const SENTRY_API_SCHEMA_SHA = 'b7cec710553c59d251cc6e50172c26455b53a640'; @@ -73,6 +94,9 @@ export type API = { server: string; slug: string; bodyContentType?: string; + /** Pre-compiled HTML from descriptionMarkdown (built at build time to avoid runtime esbuild) */ + descriptionHtml?: string; + /** Raw markdown description from OpenAPI spec */ descriptionMarkdown?: string; requestBodyContent?: any; security?: {[key: string]: string[]}; @@ -119,14 +143,18 @@ async function apiCategoriesUncached(): Promise { }; }); + // Collect all APIs first, then compile markdown in parallel + const apiPromises: Promise[] = []; + Object.entries(data.paths).forEach(([apiPath, methods]) => { Object.entries(methods).forEach(([method, apiData]) => { let server = 'https://sentry.io'; if (apiData.servers && apiData.servers[0]) { server = apiData.servers[0].url; } + apiData.tags.forEach(tag => { - categoryMap[tag].apis.push({ + const api: API = { apiPath, method, name: apiData.operationId, @@ -165,11 +193,25 @@ async function apiCategoriesUncached(): Promise { ...rest, }; }), - }); + }; + + categoryMap[tag].apis.push(api); + + // Pre-compile markdown to HTML at build time (fixes DOCS-A3H) + if (api.descriptionMarkdown) { + apiPromises.push( + compileMarkdownToHtml(api.descriptionMarkdown).then(html => { + api.descriptionHtml = html; + }) + ); + } }); }); }); + // Wait for all markdown compilation to complete + await Promise.all(apiPromises); + const categories = Object.values(categoryMap); categories.sort((a, b) => a.name.localeCompare(b.name)); categories.forEach(c => { diff --git a/src/components/apiPage/index.tsx b/src/components/apiPage/index.tsx index fbe52d2c8ffd0..316ab44a11f17 100644 --- a/src/components/apiPage/index.tsx +++ b/src/components/apiPage/index.tsx @@ -1,11 +1,6 @@ -import {Fragment, ReactElement, useMemo} from 'react'; -import {bundleMDX} from 'mdx-bundler'; +import {Fragment} from 'react'; import {type API} from 'sentry-docs/build/resolveOpenAPI'; -import {getMDXComponent} from 'sentry-docs/getMDXComponent'; -import {mdxComponents} from 'sentry-docs/mdxComponents'; -import remarkCodeTabs from 'sentry-docs/remark-code-tabs'; -import remarkCodeTitles from 'sentry-docs/remark-code-title'; import './styles.scss'; @@ -13,6 +8,16 @@ import {ApiExamples} from '../apiExamples/apiExamples'; import {DocPage} from '../docPage'; import {SmartLink} from '../smartLink'; +/** + * Renders pre-compiled HTML from markdown. + * The HTML is compiled at build time in resolveOpenAPI.ts to avoid + * needing esbuild/mdx-bundler at runtime. + * Fixes: DOCS-A3H + */ +function MarkdownHtml({html}: {html: string}) { + return
; +} + function Params({params}) { return (
@@ -59,7 +64,8 @@ function Params({params}) { )} - {param.description && parseMarkdown(param.description)} + {/* Parameter descriptions are simple text, render directly */} + {param.description} )} @@ -74,49 +80,6 @@ const getScopes = (data, securityScheme) => { return obj[securityScheme]; }; -// https://stackoverflow.com/a/38137700 -function cssToObj(css) { - const obj = {}, - s = css - .toLowerCase() - .replace(/-(.)/g, function (_, g) { - return g.toUpperCase(); - }) - .replace(/;\s?$/g, '') - .split(/:|;/g); - for (let i = 0; i < s.length; i += 2) { - obj[s[i].replace(/\s/g, '')] = s[i + 1].replace(/^\s+|\s+$/g, ''); - } - return obj; -} - -async function parseMarkdown(source: string): Promise { - // Source uses string styles, but MDX requires object styles. - source = source.replace(/style="([^"]+)"/g, (_, style) => { - return `style={${JSON.stringify(cssToObj(style))}}`; - }); - const {code} = await bundleMDX({ - source, - cwd: process.cwd(), - mdxOptions(options) { - options.remarkPlugins = [remarkCodeTitles, remarkCodeTabs]; - return options; - }, - esbuildOptions: options => { - options.loader = { - ...options.loader, - '.js': 'jsx', - }; - return options; - }, - }); - function MDXLayoutRenderer({mdxSource, ...rest}) { - const MDXLayout = useMemo(() => getMDXComponent(mdxSource), [mdxSource]); - return ; - } - return ; -} - type Props = { api: API; }; @@ -141,7 +104,8 @@ export function ApiPage({api}: Props) {
{api.summary &&

{api.summary}

} - {api.descriptionMarkdown && parseMarkdown(api.descriptionMarkdown)} + {/* Use pre-compiled HTML instead of runtime bundleMDX (fixes DOCS-A3H) */} + {api.descriptionHtml && } {!!api.pathParameters.length && (
From feea019de008df8e52e463080b2e1b05dd00f622 Mon Sep 17 00:00:00 2001 From: paulj Date: Wed, 21 Jan 2026 18:37:49 -0500 Subject: [PATCH 3/4] fix(DOCS-A3F): Handle sitemap gracefully when doctree unavailable Fixes DOCS-A3F - "ENOENT: no such file or directory, scandir" errors on /sitemap.xml endpoint. Root cause: When serverless function doesn't have doctree.json bundled, getDocsRootNode() throws an error causing 500 response on /sitemap.xml. Solution: Wrap sitemap generation in try/catch and return minimal sitemap (homepage only) when doctree isn't available. The full static sitemap is served from CDN cache for most requests anyway. Changes: - Add try/catch around getDocsRootNode() in sitemap() - Return [{url: baseUrl}] as fallback instead of crashing - Add JSDoc explaining the fix Bundle impact: None - no new dependencies, no bundle size change. Co-Authored-By: Claude --- app/sitemap.ts | 21 ++++++++++++++++++--- src/build/resolveOpenAPI.ts | 6 +++--- src/components/apiPage/index.tsx | 2 +- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/app/sitemap.ts b/app/sitemap.ts index 7e312fb65c56e..47f4289f557c4 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -24,14 +24,29 @@ function extractSlugsFromDocTree(node: DocNode): string[] { return slugs; } +/** + * Generate sitemap from pre-computed doctree. + * If doctree isn't available (serverless edge case), return minimal sitemap. + * Fixes: DOCS-A3F + */ export default async function sitemap(): Promise { - const rootNode = await getDocsRootNode(); const baseUrl = isDeveloperDocs ? 'https://develop.sentry.dev' : 'https://docs.sentry.io'; - const paths = extractSlugsFromDocTree(rootNode); - return docsToSitemap(paths, baseUrl); + try { + const rootNode = await getDocsRootNode(); + const paths = extractSlugsFromDocTree(rootNode); + return docsToSitemap(paths, baseUrl); + } catch (error) { + // If doctree.json is not available in serverless function, + // return minimal sitemap with just the homepage. + // This prevents 500 errors on /sitemap.xml while the static + // sitemap is still served from CDN cache for most requests. + // Fixes: DOCS-A3F + console.warn('Sitemap: doctree not available, returning minimal sitemap', error); + return [{url: baseUrl + '/'}]; + } } function docsToSitemap(paths: string[], baseUrl: string): MetadataRoute.Sitemap { diff --git a/src/build/resolveOpenAPI.ts b/src/build/resolveOpenAPI.ts index 312c5194853b0..2297de30df8be 100644 --- a/src/build/resolveOpenAPI.ts +++ b/src/build/resolveOpenAPI.ts @@ -77,7 +77,7 @@ type APIResponse = { content_type: string; schema: any; example?: APIExample; - examples?: {[key: string]: APIExample}; + examples?: { [key: string]: APIExample }; }; }; @@ -99,7 +99,7 @@ export type API = { /** Raw markdown description from OpenAPI spec */ descriptionMarkdown?: string; requestBodyContent?: any; - security?: {[key: string]: string[]}; + security?: { [key: string]: string[] }; summary?: string; }; @@ -133,7 +133,7 @@ export function apiCategories(): Promise { async function apiCategoriesUncached(): Promise { const data = await resolveOpenAPI(); - const categoryMap: {[name: string]: APICategory} = {}; + const categoryMap: { [name: string]: APICategory } = {}; data.tags.forEach(tag => { categoryMap[tag.name] = { name: tag['x-sidebar-name'] || tag.name, diff --git a/src/components/apiPage/index.tsx b/src/components/apiPage/index.tsx index 316ab44a11f17..75017a2a8804a 100644 --- a/src/components/apiPage/index.tsx +++ b/src/components/apiPage/index.tsx @@ -14,7 +14,7 @@ import {SmartLink} from '../smartLink'; * needing esbuild/mdx-bundler at runtime. * Fixes: DOCS-A3H */ -function MarkdownHtml({html}: {html: string}) { +function MarkdownHtml({html}: { html: string }) { return
; } From 20ae29c52d7d8300dcc8f18e19ca9c7caa374782 Mon Sep 17 00:00:00 2001 From: "getsantry[bot]" <66042841+getsantry[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 23:54:38 +0000 Subject: [PATCH 4/4] [getsentry/action-github-commit] Auto commit --- src/build/resolveOpenAPI.ts | 6 +++--- src/components/apiPage/index.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/build/resolveOpenAPI.ts b/src/build/resolveOpenAPI.ts index 2297de30df8be..312c5194853b0 100644 --- a/src/build/resolveOpenAPI.ts +++ b/src/build/resolveOpenAPI.ts @@ -77,7 +77,7 @@ type APIResponse = { content_type: string; schema: any; example?: APIExample; - examples?: { [key: string]: APIExample }; + examples?: {[key: string]: APIExample}; }; }; @@ -99,7 +99,7 @@ export type API = { /** Raw markdown description from OpenAPI spec */ descriptionMarkdown?: string; requestBodyContent?: any; - security?: { [key: string]: string[] }; + security?: {[key: string]: string[]}; summary?: string; }; @@ -133,7 +133,7 @@ export function apiCategories(): Promise { async function apiCategoriesUncached(): Promise { const data = await resolveOpenAPI(); - const categoryMap: { [name: string]: APICategory } = {}; + const categoryMap: {[name: string]: APICategory} = {}; data.tags.forEach(tag => { categoryMap[tag.name] = { name: tag['x-sidebar-name'] || tag.name, diff --git a/src/components/apiPage/index.tsx b/src/components/apiPage/index.tsx index 75017a2a8804a..316ab44a11f17 100644 --- a/src/components/apiPage/index.tsx +++ b/src/components/apiPage/index.tsx @@ -14,7 +14,7 @@ import {SmartLink} from '../smartLink'; * needing esbuild/mdx-bundler at runtime. * Fixes: DOCS-A3H */ -function MarkdownHtml({html}: { html: string }) { +function MarkdownHtml({html}: {html: string}) { return
; }