diff options
Diffstat (limited to 'packages/integrations/mdx/src')
-rw-r--r-- | packages/integrations/mdx/src/index.ts | 20 | ||||
-rw-r--r-- | packages/integrations/mdx/src/plugins.ts | 2 | ||||
-rw-r--r-- | packages/integrations/mdx/src/rehype-apply-frontmatter-export.ts | 8 | ||||
-rw-r--r-- | packages/integrations/mdx/src/rehype-collect-headings.ts | 6 | ||||
-rw-r--r-- | packages/integrations/mdx/src/rehype-images-to-component.ts | 10 | ||||
-rw-r--r-- | packages/integrations/mdx/src/server.ts | 73 | ||||
-rw-r--r-- | packages/integrations/mdx/src/utils.ts | 6 | ||||
-rw-r--r-- | packages/integrations/mdx/src/vite-plugin-mdx.ts | 18 |
8 files changed, 110 insertions, 33 deletions
diff --git a/packages/integrations/mdx/src/index.ts b/packages/integrations/mdx/src/index.ts index de29003ff..dcb13bc62 100644 --- a/packages/integrations/mdx/src/index.ts +++ b/packages/integrations/mdx/src/index.ts @@ -8,11 +8,10 @@ import type { ContentEntryType, HookParameters, } from 'astro'; -import astroJSXRenderer from 'astro/jsx/renderer.js'; import type { Options as RemarkRehypeOptions } from 'remark-rehype'; import type { PluggableList } from 'unified'; import type { OptimizeOptions } from './rehype-optimize-static.js'; -import { ignoreStringPlugins, parseFrontmatter } from './utils.js'; +import { ignoreStringPlugins, safeParseFrontmatter } from './utils.js'; import { vitePluginMdxPostprocess } from './vite-plugin-mdx-postprocess.js'; import { vitePluginMdx } from './vite-plugin-mdx.js'; @@ -37,7 +36,7 @@ type SetupHookParams = HookParameters<'astro:config:setup'> & { export function getContainerRenderer(): ContainerRenderer { return { name: 'astro:jsx', - serverEntrypoint: 'astro/jsx/server.js', + serverEntrypoint: '@astrojs/mdx/server.js', }; } @@ -53,17 +52,20 @@ export default function mdx(partialMdxOptions: Partial<MdxOptions> = {}): AstroI const { updateConfig, config, addPageExtension, addContentEntryType, addRenderer } = params as SetupHookParams; - addRenderer(astroJSXRenderer); + addRenderer({ + name: 'astro:jsx', + serverEntrypoint: '@astrojs/mdx/server.js', + }); addPageExtension('.mdx'); addContentEntryType({ extensions: ['.mdx'], async getEntryInfo({ fileUrl, contents }: { fileUrl: URL; contents: string }) { - const parsed = parseFrontmatter(contents, fileURLToPath(fileUrl)); + const parsed = safeParseFrontmatter(contents, fileURLToPath(fileUrl)); return { - data: parsed.data, - body: parsed.content, - slug: parsed.data.slug, - rawData: parsed.matter, + data: parsed.frontmatter, + body: parsed.content.trim(), + slug: parsed.frontmatter.slug, + rawData: parsed.rawFrontmatter, }; }, contentModuleTypes: await fs.readFile( diff --git a/packages/integrations/mdx/src/plugins.ts b/packages/integrations/mdx/src/plugins.ts index 082e8f6fd..77c76243c 100644 --- a/packages/integrations/mdx/src/plugins.ts +++ b/packages/integrations/mdx/src/plugins.ts @@ -83,7 +83,7 @@ function getRehypePlugins(mdxOptions: MdxOptions): PluggableList { } rehypePlugins.push( - // Render info from `vfile.data.astro.data.frontmatter` as JS + // Render info from `vfile.data.astro.frontmatter` as JS rehypeApplyFrontmatterExport, // Analyze MDX nodes and attach to `vfile.data.__astroMetadata` rehypeAnalyzeAstroMetadata, diff --git a/packages/integrations/mdx/src/rehype-apply-frontmatter-export.ts b/packages/integrations/mdx/src/rehype-apply-frontmatter-export.ts index 1b981a68e..cc1f4d141 100644 --- a/packages/integrations/mdx/src/rehype-apply-frontmatter-export.ts +++ b/packages/integrations/mdx/src/rehype-apply-frontmatter-export.ts @@ -1,18 +1,16 @@ -import { InvalidAstroDataError } from '@astrojs/markdown-remark'; -import { safelyGetAstroData } from '@astrojs/markdown-remark/dist/internal.js'; +import { isFrontmatterValid } from '@astrojs/markdown-remark'; import type { VFile } from 'vfile'; import { jsToTreeNode } from './utils.js'; export function rehypeApplyFrontmatterExport() { return function (tree: any, vfile: VFile) { - const astroData = safelyGetAstroData(vfile.data); - if (astroData instanceof InvalidAstroDataError) + const frontmatter = vfile.data.astro?.frontmatter; + if (!frontmatter || !isFrontmatterValid(frontmatter)) throw new Error( // Copied from Astro core `errors-data` // TODO: find way to import error data from core '[MDX] A remark or rehype plugin attempted to inject invalid frontmatter. Ensure "astro.frontmatter" is set to a valid JSON object that is not `null` or `undefined`.', ); - const { frontmatter } = astroData; const exportNodes = [ jsToTreeNode(`export const frontmatter = ${JSON.stringify(frontmatter)};`), ]; diff --git a/packages/integrations/mdx/src/rehype-collect-headings.ts b/packages/integrations/mdx/src/rehype-collect-headings.ts index fafc59721..a51e8e9f0 100644 --- a/packages/integrations/mdx/src/rehype-collect-headings.ts +++ b/packages/integrations/mdx/src/rehype-collect-headings.ts @@ -1,9 +1,9 @@ -import type { MarkdownHeading, MarkdownVFile } from '@astrojs/markdown-remark'; +import type { VFile } from 'vfile'; import { jsToTreeNode } from './utils.js'; export function rehypeInjectHeadingsExport() { - return function (tree: any, file: MarkdownVFile) { - const headings: MarkdownHeading[] = file.data.__astroHeadings || []; + return function (tree: any, file: VFile) { + const headings = file.data.astro?.headings ?? []; tree.children.unshift( jsToTreeNode(`export function getHeadings() { return ${JSON.stringify(headings)} }`), ); diff --git a/packages/integrations/mdx/src/rehype-images-to-component.ts b/packages/integrations/mdx/src/rehype-images-to-component.ts index 95b500784..da2f25ee5 100644 --- a/packages/integrations/mdx/src/rehype-images-to-component.ts +++ b/packages/integrations/mdx/src/rehype-images-to-component.ts @@ -1,8 +1,8 @@ -import type { MarkdownVFile } from '@astrojs/markdown-remark'; import type { Properties, Root } from 'hast'; import type { MdxJsxAttribute, MdxjsEsm } from 'mdast-util-mdx'; import type { MdxJsxFlowElementHast } from 'mdast-util-mdx-jsx'; import { visit } from 'unist-util-visit'; +import type { VFile } from 'vfile'; import { jsToTreeNode } from './utils.js'; export const ASTRO_IMAGE_ELEMENT = 'astro-image'; @@ -72,18 +72,18 @@ function getImageComponentAttributes(props: Properties): MdxJsxAttribute[] { } export function rehypeImageToComponent() { - return function (tree: Root, file: MarkdownVFile) { - if (!file.data.imagePaths) return; + return function (tree: Root, file: VFile) { + if (!file.data.astro?.imagePaths) return; const importsStatements: MdxjsEsm[] = []; const importedImages = new Map<string, string>(); visit(tree, 'element', (node, index, parent) => { - if (!file.data.imagePaths || node.tagName !== 'img' || !node.properties.src) return; + if (!file.data.astro?.imagePaths || node.tagName !== 'img' || !node.properties.src) return; const src = decodeURI(String(node.properties.src)); - if (!file.data.imagePaths.has(src)) return; + if (!file.data.astro.imagePaths?.includes(src)) return; let importName = importedImages.get(src); diff --git a/packages/integrations/mdx/src/server.ts b/packages/integrations/mdx/src/server.ts new file mode 100644 index 000000000..79934eb32 --- /dev/null +++ b/packages/integrations/mdx/src/server.ts @@ -0,0 +1,73 @@ +import type { NamedSSRLoadedRendererValue } from 'astro'; +import { AstroError } from 'astro/errors'; +import { AstroJSX, jsx } from 'astro/jsx-runtime'; +import { renderJSX } from 'astro/runtime/server/index.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, + { default: children = null, ...slotted } = {}, +) { + if (typeof Component !== 'function') return false; + const slots: Record<string, any> = {}; + for (const [key, value] of Object.entries(slotted)) { + const name = slotName(key); + slots[name] = value; + } + try { + const result = await Component({ ...props, ...slots, children }); + return result[AstroJSX]; + } catch (e) { + throwEnhancedErrorIfMdxComponent(e as Error, Component); + } + return false; +} + +export async function renderToStaticMarkup( + this: any, + Component: any, + props = {}, + { default: children = null, ...slotted } = {}, +) { + const slots: Record<string, any> = {}; + for (const [key, value] of Object.entries(slotted)) { + const name = slotName(key); + slots[name] = value; + } + + const { result } = this; + 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')]) { + // if it's an existing AstroError, we don't need to re-throw, keep the original hint + if (AstroError.is(error)) return; + // Mimic the fields of the internal `AstroError` class (not from `astro/errors`) to + // provide better title and hint for the error overlay + (error as any).title = error.name; + (error as any).hint = + `This issue often occurs when your MDX component encounters runtime errors.`; + throw error; + } +} + +const renderer: NamedSSRLoadedRendererValue = { + name: 'astro:jsx', + check, + renderToStaticMarkup, +}; + +export default renderer; diff --git a/packages/integrations/mdx/src/utils.ts b/packages/integrations/mdx/src/utils.ts index ad98abb9e..7dcd4a14c 100644 --- a/packages/integrations/mdx/src/utils.ts +++ b/packages/integrations/mdx/src/utils.ts @@ -1,7 +1,7 @@ +import { parseFrontmatter } from '@astrojs/markdown-remark'; import type { Options as AcornOpts } from 'acorn'; import { parse } from 'acorn'; import type { AstroConfig, AstroIntegrationLogger, SSRError } from 'astro'; -import matter from 'gray-matter'; import { bold } from 'kleur/colors'; import type { MdxjsEsm } from 'mdast-util-mdx'; import type { PluggableList } from 'unified'; @@ -48,9 +48,9 @@ export function getFileInfo(id: string, config: AstroConfig): FileInfo { * Match YAML exception handling from Astro core errors * @see 'astro/src/core/errors.ts' */ -export function parseFrontmatter(code: string, id: string) { +export function safeParseFrontmatter(code: string, id: string) { try { - return matter(code); + return parseFrontmatter(code, { frontmatter: 'empty-with-spaces' }); } catch (e: any) { if (e.name === 'YAMLException') { const err: SSRError = e; diff --git a/packages/integrations/mdx/src/vite-plugin-mdx.ts b/packages/integrations/mdx/src/vite-plugin-mdx.ts index 5a409d40d..eea530c1c 100644 --- a/packages/integrations/mdx/src/vite-plugin-mdx.ts +++ b/packages/integrations/mdx/src/vite-plugin-mdx.ts @@ -1,11 +1,10 @@ -import { setVfileFrontmatter } from '@astrojs/markdown-remark'; import type { SSRError } from 'astro'; import { getAstroMetadata } from 'astro/jsx/rehype.js'; import { VFile } from 'vfile'; import type { Plugin } from 'vite'; import type { MdxOptions } from './index.js'; import { createMdxProcessor } from './plugins.js'; -import { parseFrontmatter } from './utils.js'; +import { safeParseFrontmatter } from './utils.js'; export function vitePluginMdx(mdxOptions: MdxOptions): Plugin { let processor: ReturnType<typeof createMdxProcessor> | undefined; @@ -39,12 +38,17 @@ export function vitePluginMdx(mdxOptions: MdxOptions): Plugin { async transform(code, id) { if (!id.endsWith('.mdx')) return; - const { data: frontmatter, content: pageContent, matter } = parseFrontmatter(code, id); - const frontmatterLines = matter ? matter.match(/\n/g)?.join('') + '\n\n' : ''; + const { frontmatter, content } = safeParseFrontmatter(code, id); - const vfile = new VFile({ value: frontmatterLines + pageContent, path: id }); - // Ensure `data.astro` is available to all remark plugins - setVfileFrontmatter(vfile, frontmatter); + const vfile = new VFile({ + value: content, + path: id, + data: { + astro: { + frontmatter, + }, + }, + }); // Lazily initialize the MDX processor if (!processor) { |