diff options
Diffstat (limited to 'packages/integrations/mdx/src')
-rw-r--r-- | packages/integrations/mdx/src/astro-data-utils.ts | 82 | ||||
-rw-r--r-- | packages/integrations/mdx/src/index.ts | 110 | ||||
-rw-r--r-- | packages/integrations/mdx/src/utils.ts | 4 |
3 files changed, 144 insertions, 52 deletions
diff --git a/packages/integrations/mdx/src/astro-data-utils.ts b/packages/integrations/mdx/src/astro-data-utils.ts new file mode 100644 index 000000000..bfbc74461 --- /dev/null +++ b/packages/integrations/mdx/src/astro-data-utils.ts @@ -0,0 +1,82 @@ +import { name as isValidIdentifierName } from 'estree-util-is-identifier-name'; +import type { VFile } from 'vfile'; +import type { MdxjsEsm } from 'mdast-util-mdx'; +import type { MarkdownAstroData } from 'astro'; +import type { Data } from 'vfile'; +import { jsToTreeNode } from './utils.js'; + +export function remarkInitializeAstroData() { + return function (tree: any, vfile: VFile) { + if (!vfile.data.astro) { + vfile.data.astro = { frontmatter: {} }; + } + }; +} + +export function rehypeApplyFrontmatterExport( + pageFrontmatter: Record<string, any>, + exportName = 'frontmatter' +) { + return function (tree: any, vfile: VFile) { + if (!isValidIdentifierName(exportName)) { + throw new Error( + `[MDX] ${JSON.stringify( + exportName + )} is not a valid frontmatter export name! Make sure "frontmatterOptions.name" could be used as a JS export (i.e. "export const frontmatterName = ...")` + ); + } + const { frontmatter: injectedFrontmatter } = safelyGetAstroData(vfile.data); + const frontmatter = { ...injectedFrontmatter, ...pageFrontmatter }; + let exportNodes: MdxjsEsm[] = []; + if (!exportName) { + exportNodes = Object.entries(frontmatter).map(([k, v]) => { + if (!isValidIdentifierName(k)) { + throw new Error( + `[MDX] A remark or rehype plugin tried to inject ${JSON.stringify( + k + )} as a top-level export, which is not a valid export name.` + ); + } + return jsToTreeNode(`export const ${k} = ${JSON.stringify(v)};`); + }); + } else { + exportNodes = [jsToTreeNode(`export const ${exportName} = ${JSON.stringify(frontmatter)};`)]; + } + tree.children = exportNodes.concat(tree.children); + }; +} + +/** + * Copied from markdown utils + * @see "vite-plugin-utils" + */ +function isValidAstroData(obj: unknown): obj is MarkdownAstroData { + if (typeof obj === 'object' && obj !== null && obj.hasOwnProperty('frontmatter')) { + const { frontmatter } = obj as any; + try { + // ensure frontmatter is JSON-serializable + JSON.stringify(frontmatter); + } catch { + return false; + } + return typeof frontmatter === 'object' && frontmatter !== null; + } + return false; +} + +/** + * Copied from markdown utils + * @see "vite-plugin-utils" + */ +export function safelyGetAstroData(vfileData: Data): MarkdownAstroData { + const { astro } = vfileData; + + if (!astro) return { frontmatter: {} }; + if (!isValidAstroData(astro)) { + throw Error( + `[MDX] A remark or rehype plugin tried to add invalid frontmatter. Ensure "astro.frontmatter" is a JSON object!` + ); + } + + return astro; +} diff --git a/packages/integrations/mdx/src/index.ts b/packages/integrations/mdx/src/index.ts index a7abb0c33..3b1ceaa4c 100644 --- a/packages/integrations/mdx/src/index.ts +++ b/packages/integrations/mdx/src/index.ts @@ -1,19 +1,18 @@ import { compile as mdxCompile, nodeTypes } from '@mdx-js/mdx'; import mdxPlugin, { Options as MdxRollupPluginOptions } from '@mdx-js/rollup'; -import type { AstroIntegration } from 'astro'; +import type { AstroIntegration, AstroConfig } from 'astro'; +import { remarkInitializeAstroData, rehypeApplyFrontmatterExport } from './astro-data-utils.js'; import { parse as parseESM } from 'es-module-lexer'; import rehypeRaw from 'rehype-raw'; -import remarkFrontmatter from 'remark-frontmatter'; import remarkGfm from 'remark-gfm'; import type { RemarkMdxFrontmatterOptions } from 'remark-mdx-frontmatter'; -import remarkMdxFrontmatter from 'remark-mdx-frontmatter'; import remarkShikiTwoslash from 'remark-shiki-twoslash'; import remarkSmartypants from 'remark-smartypants'; import { VFile } from 'vfile'; import type { Plugin as VitePlugin } from 'vite'; import rehypeCollectHeadings from './rehype-collect-headings.js'; import remarkPrism from './remark-prism.js'; -import { getFileInfo, getFrontmatter } from './utils.js'; +import { getFileInfo, parseFrontmatter } from './utils.js'; type WithExtends<T> = T | { extends: T }; @@ -37,44 +36,52 @@ function handleExtends<T>(config: WithExtends<T[] | undefined>, defaults: T[] = return [...defaults, ...(config?.extends ?? [])]; } +function getRemarkPlugins( + mdxOptions: MdxOptions, + config: AstroConfig +): MdxRollupPluginOptions['remarkPlugins'] { + let remarkPlugins = [ + // Initialize vfile.data.astroExports before all plugins are run + remarkInitializeAstroData, + ...handleExtends(mdxOptions.remarkPlugins, DEFAULT_REMARK_PLUGINS), + ]; + if (config.markdown.syntaxHighlight === 'shiki') { + // Default export still requires ".default" chaining for some reason + // Workarounds tried: + // - "import * as remarkShikiTwoslash" + // - "import { default as remarkShikiTwoslash }" + const shikiTwoslash = (remarkShikiTwoslash as any).default ?? remarkShikiTwoslash; + remarkPlugins.push([shikiTwoslash, config.markdown.shikiConfig]); + } + if (config.markdown.syntaxHighlight === 'prism') { + remarkPlugins.push(remarkPrism); + } + return remarkPlugins; +} + +function getRehypePlugins( + mdxOptions: MdxOptions, + config: AstroConfig +): MdxRollupPluginOptions['rehypePlugins'] { + let rehypePlugins = handleExtends(mdxOptions.rehypePlugins, DEFAULT_REHYPE_PLUGINS); + + if (config.markdown.syntaxHighlight === 'shiki' || config.markdown.syntaxHighlight === 'prism') { + rehypePlugins.push([rehypeRaw, { passThrough: nodeTypes }]); + } + + return rehypePlugins; +} + export default function mdx(mdxOptions: MdxOptions = {}): AstroIntegration { return { name: '@astrojs/mdx', hooks: { 'astro:config:setup': ({ updateConfig, config, addPageExtension, command }: any) => { addPageExtension('.mdx'); - let remarkPlugins = handleExtends(mdxOptions.remarkPlugins, DEFAULT_REMARK_PLUGINS); - let rehypePlugins = handleExtends(mdxOptions.rehypePlugins, DEFAULT_REHYPE_PLUGINS); - - if (config.markdown.syntaxHighlight === 'shiki') { - remarkPlugins.push([ - // Default export still requires ".default" chaining for some reason - // Workarounds tried: - // - "import * as remarkShikiTwoslash" - // - "import { default as remarkShikiTwoslash }" - (remarkShikiTwoslash as any).default ?? remarkShikiTwoslash, - config.markdown.shikiConfig, - ]); - rehypePlugins.push([rehypeRaw, { passThrough: nodeTypes }]); - } - - if (config.markdown.syntaxHighlight === 'prism') { - remarkPlugins.push(remarkPrism); - rehypePlugins.push([rehypeRaw, { passThrough: nodeTypes }]); - } - - remarkPlugins.push(remarkFrontmatter); - remarkPlugins.push([ - remarkMdxFrontmatter, - { - name: 'frontmatter', - ...mdxOptions.frontmatterOptions, - }, - ]); const mdxPluginOpts: MdxRollupPluginOptions = { - remarkPlugins, - rehypePlugins, + remarkPlugins: getRemarkPlugins(mdxOptions, config), + rehypePlugins: getRehypePlugins(mdxOptions, config), jsx: true, jsxImportSource: 'astro', // Note: disable `.md` support @@ -93,24 +100,27 @@ export default function mdx(mdxOptions: MdxOptions = {}): AstroIntegration { async transform(code, id) { if (!id.endsWith('mdx')) return; - // If user overrides our default YAML parser, - // do not attempt to parse the `layout` via gray-matter - if (!mdxOptions.frontmatterOptions?.parsers) { - const frontmatter = getFrontmatter(code, id); - if (frontmatter.layout) { - const { layout, ...content } = frontmatter; - code += `\n\nexport default async function({ children }) {\nconst Layout = (await import(${JSON.stringify( - frontmatter.layout - )})).default;\nconst frontmatter=${JSON.stringify( - content - )};\nreturn <Layout frontmatter={frontmatter} content={frontmatter} headings={getHeadings()}>{children}</Layout> }`; - } + let { data: frontmatter, content: pageContent } = parseFrontmatter(code, id); + if (frontmatter.layout) { + const { layout, ...contentProp } = frontmatter; + pageContent += `\n\nexport default async function({ children }) {\nconst Layout = (await import(${JSON.stringify( + frontmatter.layout + )})).default;\nconst frontmatter=${JSON.stringify( + contentProp + )};\nreturn <Layout frontmatter={frontmatter} content={frontmatter} headings={getHeadings()}>{children}</Layout> }`; } - const compiled = await mdxCompile( - new VFile({ value: code, path: id }), - mdxPluginOpts - ); + const compiled = await mdxCompile(new VFile({ value: pageContent, path: id }), { + ...mdxPluginOpts, + rehypePlugins: [ + ...(mdxPluginOpts.rehypePlugins ?? []), + () => + rehypeApplyFrontmatterExport( + frontmatter, + mdxOptions.frontmatterOptions?.name + ), + ], + }); return { code: String(compiled.value), diff --git a/packages/integrations/mdx/src/utils.ts b/packages/integrations/mdx/src/utils.ts index b5f7082dc..f5135ebc2 100644 --- a/packages/integrations/mdx/src/utils.ts +++ b/packages/integrations/mdx/src/utils.ts @@ -47,9 +47,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 getFrontmatter(code: string, id: string) { +export function parseFrontmatter(code: string, id: string) { try { - return matter(code).data; + return matter(code); } catch (e: any) { if (e.name === 'YAMLException') { const err: SSRError = e; |