From f513ba602c9d9bc2f1984bf994381d30cdea6a0d Mon Sep 17 00:00:00 2001 From: bluwy Date: Wed, 24 Apr 2024 17:38:22 +0800 Subject: [PATCH] Tag MDX component for faster checks when rendering --- .changeset/chilly-items-help.md | 5 ++ .changeset/tame-avocados-relax.md | 5 ++ packages/astro/src/jsx/server.ts | 38 +++++++---- .../mdx/src/vite-plugin-mdx-postprocess.ts | 65 ++++++++++++++----- 4 files changed, 82 insertions(+), 31 deletions(-) create mode 100644 .changeset/chilly-items-help.md create mode 100644 .changeset/tame-avocados-relax.md diff --git a/.changeset/chilly-items-help.md b/.changeset/chilly-items-help.md new file mode 100644 index 000000000000..7e868474e32c --- /dev/null +++ b/.changeset/chilly-items-help.md @@ -0,0 +1,5 @@ +--- +"astro": patch +--- + +Improves the error message when failed to render MDX components diff --git a/.changeset/tame-avocados-relax.md b/.changeset/tame-avocados-relax.md new file mode 100644 index 000000000000..9b6a36881c03 --- /dev/null +++ b/.changeset/tame-avocados-relax.md @@ -0,0 +1,5 @@ +--- +"@astrojs/mdx": patch +--- + +Tags the MDX component export for quicker component checks while rendering diff --git a/packages/astro/src/jsx/server.ts b/packages/astro/src/jsx/server.ts index d445ee3a559b..2ed308c37e82 100644 --- a/packages/astro/src/jsx/server.ts +++ b/packages/astro/src/jsx/server.ts @@ -4,6 +4,8 @@ import { renderJSX } from '../runtime/server/jsx.js'; const slotName = (str: string) => str.trim().replace(/[-_]([a-z])/g, (_, w) => w.toUpperCase()); +// NOTE: In practice, MDX components are always tagged with `__astro_tag_component__`, so the right renderer +// is used directly, and this check is not often used to return true. export async function check( Component: any, props: any, @@ -19,18 +21,7 @@ export async function check( const result = await Component({ ...props, ...slots, children }); return result[AstroJSX]; } catch (e) { - const error = e as Error; - // if the exception is from an mdx component - // throw an error - if (Component[Symbol.for('mdx-component')]) { - throw new AstroError({ - message: error.message, - title: error.name, - hint: `This issue often occurs when your MDX component encounters runtime errors.`, - name: error.name, - stack: error.stack, - }); - } + throwEnhancedErrorIfMdxComponent(e as Error, Component); } return false; } @@ -48,8 +39,27 @@ export async function renderToStaticMarkup( } const { result } = this; - const html = await renderJSX(result, jsx(Component, { ...props, ...slots, children })); - return { html }; + try { + const html = await renderJSX(result, jsx(Component, { ...props, ...slots, children })); + return { html }; + } catch (e) { + throwEnhancedErrorIfMdxComponent(e as Error, Component); + throw e; + } +} + +function throwEnhancedErrorIfMdxComponent(error: Error, Component: any) { + // if the exception is from an mdx component + // throw an error + if (Component[Symbol.for('mdx-component')]) { + throw new AstroError({ + message: error.message, + title: error.name, + hint: `This issue often occurs when your MDX component encounters runtime errors.`, + name: error.name, + stack: error.stack, + }); + } } export default { diff --git a/packages/integrations/mdx/src/vite-plugin-mdx-postprocess.ts b/packages/integrations/mdx/src/vite-plugin-mdx-postprocess.ts index c60504be6c9c..4f06d422a911 100644 --- a/packages/integrations/mdx/src/vite-plugin-mdx-postprocess.ts +++ b/packages/integrations/mdx/src/vite-plugin-mdx-postprocess.ts @@ -8,11 +8,14 @@ import { } from './remark-images-to-component.js'; import { type FileInfo, getFileInfo } from './utils.js'; +const fragmentImportRegex = /[\s,{]Fragment[\s,}]/; +const astroTagComponentImportRegex = /[\s,{]__astro_tag_component__[\s,}]/; + // These transforms must happen *after* JSX runtime transformations export function vitePluginMdxPostprocess(astroConfig: AstroConfig): Plugin { return { name: '@astrojs/mdx-postprocess', - transform(code, id) { + transform(code, id, opts) { if (!id.endsWith('.mdx')) return; const fileInfo = getFileInfo(id, astroConfig); @@ -22,7 +25,7 @@ export function vitePluginMdxPostprocess(astroConfig: AstroConfig): Plugin { code = injectFragmentImport(code, imports); code = injectMetadataExports(code, exports, fileInfo); code = transformContentExport(code, exports); - code = annotateContentExport(code, id); + code = annotateContentExport(code, id, !!opts?.ssr, imports); // The code transformations above are append-only, so the line/column mappings are the same // and we can omit the sourcemap for performance. @@ -31,23 +34,12 @@ export function vitePluginMdxPostprocess(astroConfig: AstroConfig): Plugin { }; } -const fragmentImportRegex = /[\s,{](?:Fragment,|Fragment\s*\})/; - /** - * Inject `Fragment` identifier import if not already present. It should already be injected, - * but check just to be safe. - * - * TODO: Double-check if we no longer need this function. + * Inject `Fragment` identifier import if not already present. */ function injectFragmentImport(code: string, imports: readonly ImportSpecifier[]) { - const importsFromJSXRuntime = imports - .filter(({ n }) => n === 'astro/jsx-runtime') - .map(({ ss, se }) => code.substring(ss, se)); - const hasFragmentImport = importsFromJSXRuntime.some((statement) => - fragmentImportRegex.test(statement) - ); - if (!hasFragmentImport) { - code = `import { Fragment } from "astro/jsx-runtime"\n` + code; + if (!isSpecifierImported(code, imports, fragmentImportRegex, 'astro/jsx-runtime')) { + code += `\nimport { Fragment } from 'astro/jsx-runtime';`; } return code; } @@ -103,7 +95,12 @@ export default Content;`; /** * Add properties to the `Content` export. */ -function annotateContentExport(code: string, id: string) { +function annotateContentExport( + code: string, + id: string, + ssr: boolean, + imports: readonly ImportSpecifier[] +) { // Mark `Content` as MDX component code += `\nContent[Symbol.for('mdx-component')] = true`; // Ensure styles and scripts are injected into a `` when a layout is not applied @@ -111,5 +108,39 @@ function annotateContentExport(code: string, id: string) { // Assign the `moduleId` metadata to `Content` code += `\nContent.moduleId = ${JSON.stringify(id)};`; + // Tag the `Content` export as "astro:jsx" so it's quicker to identify how to render this component + if (ssr) { + if ( + !isSpecifierImported( + code, + imports, + astroTagComponentImportRegex, + 'astro/runtime/server/index.js' + ) + ) { + code += `\nimport { __astro_tag_component__ } from 'astro/runtime/server/index.js';`; + } + code += `\n__astro_tag_component__(Content, 'astro:jsx');`; + } + return code; } + +/** + * Check whether the `specifierRegex` matches for an import of `source` in the `code`. + */ +function isSpecifierImported( + code: string, + imports: readonly ImportSpecifier[], + specifierRegex: RegExp, + source: string +) { + for (const imp of imports) { + if (imp.n !== source) continue; + + const importStatement = code.slice(imp.ss, imp.se); + if (specifierRegex.test(importStatement)) return true; + } + + return false; +}