summaryrefslogtreecommitdiff
path: root/packages/integrations/mdx/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/integrations/mdx/src')
-rw-r--r--packages/integrations/mdx/src/index.ts20
-rw-r--r--packages/integrations/mdx/src/plugins.ts2
-rw-r--r--packages/integrations/mdx/src/rehype-apply-frontmatter-export.ts8
-rw-r--r--packages/integrations/mdx/src/rehype-collect-headings.ts6
-rw-r--r--packages/integrations/mdx/src/rehype-images-to-component.ts10
-rw-r--r--packages/integrations/mdx/src/server.ts73
-rw-r--r--packages/integrations/mdx/src/utils.ts6
-rw-r--r--packages/integrations/mdx/src/vite-plugin-mdx.ts18
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) {