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) { | 
