diff options
Diffstat (limited to 'packages/integrations/mdx')
14 files changed, 150 insertions, 44 deletions
diff --git a/packages/integrations/mdx/CHANGELOG.md b/packages/integrations/mdx/CHANGELOG.md index 5867b3eeb..b15b97308 100644 --- a/packages/integrations/mdx/CHANGELOG.md +++ b/packages/integrations/mdx/CHANGELOG.md @@ -1,5 +1,28 @@ # @astrojs/mdx +## 4.0.0-alpha.2 + +### Patch Changes + +- [#11861](https://github.com/withastro/astro/pull/11861) [`3ab3b4e`](https://github.com/withastro/astro/commit/3ab3b4efbcdd2aabea5f949deedf51a5acefae59) Thanks [@bluwy](https://github.com/bluwy)! - Updates `@astrojs/markdown-remark` and handle its breaking changes + +- Updated dependencies [[`3ab3b4e`](https://github.com/withastro/astro/commit/3ab3b4efbcdd2aabea5f949deedf51a5acefae59), [`560ef15`](https://github.com/withastro/astro/commit/560ef15ad23bd137b56ef1048eb2df548b99fdce), [`3ab3b4e`](https://github.com/withastro/astro/commit/3ab3b4efbcdd2aabea5f949deedf51a5acefae59)]: + - @astrojs/markdown-remark@6.0.0-alpha.1 + +## 4.0.0-alpha.1 + +### Minor Changes + +- [#11741](https://github.com/withastro/astro/pull/11741) [`6617491`](https://github.com/withastro/astro/commit/6617491c3bc2bde87f7867d7dec2580781852cfc) Thanks [@bluwy](https://github.com/bluwy)! - Updates adapter server entrypoint to use `@astrojs/mdx/server.js` + + This is an internal change. Handling JSX in your `.mdx` files has been moved from Astro internals and is now the responsibility of this integration. You should not notice a change in your project, and no update to your code is required. + +## 4.0.0-alpha.0 + +- Updated dependencies [[`b6fbdaa`](https://github.com/withastro/astro/commit/b6fbdaa94a9ecec706a99e1938fbf5cd028c72e0), [`89bab1e`](https://github.com/withastro/astro/commit/89bab1e70786123fbe933a9d7a1b80c9334dcc5f), [`d74617c`](https://github.com/withastro/astro/commit/d74617cbd3278feba05909ec83db2d73d57a153e), [`83a2a64`](https://github.com/withastro/astro/commit/83a2a648418ad30f4eb781d1c1b5f2d8a8ac846e), [`e90f559`](https://github.com/withastro/astro/commit/e90f5593d23043579611452a84b9e18ad2407ef9), [`2df49a6`](https://github.com/withastro/astro/commit/2df49a6fb4f6d92fe45f7429430abe63defeacd6), [`8a53517`](https://github.com/withastro/astro/commit/8a5351737d6a14fc55f1dafad8f3b04079e81af6)]: + - astro@5.0.0-alpha.0 + - @astrojs/markdown-remark@6.0.0-alpha.0 + ## 3.1.8 ### Patch Changes diff --git a/packages/integrations/mdx/package.json b/packages/integrations/mdx/package.json index 45228f1f0..c2333d7ac 100644 --- a/packages/integrations/mdx/package.json +++ b/packages/integrations/mdx/package.json @@ -1,7 +1,7 @@ { "name": "@astrojs/mdx", "description": "Add support for MDX pages in your Astro site", - "version": "3.1.8", + "version": "4.0.0-beta.2", "type": "module", "types": "./dist/index.d.ts", "author": "withastro", @@ -20,6 +20,7 @@ "homepage": "https://docs.astro.build/en/guides/integrations-guide/mdx/", "exports": { ".": "./dist/index.js", + "./server.js": "./dist/server.js", "./package.json": "./package.json" }, "files": [ @@ -38,7 +39,6 @@ "acorn": "^8.13.0", "es-module-lexer": "^1.5.4", "estree-util-visit": "^2.0.0", - "gray-matter": "^4.0.3", "hast-util-to-html": "^9.0.3", "kleur": "^4.1.5", "rehype-raw": "^7.0.0", @@ -49,7 +49,7 @@ "vfile": "^6.0.3" }, "peerDependencies": { - "astro": "^4.8.0" + "astro": "^5.0.0-alpha.0" }, "devDependencies": { "@types/estree": "^1.0.6", @@ -70,7 +70,7 @@ "remark-toc": "^9.0.0", "shiki": "^1.22.0", "unified": "^11.0.5", - "vite": "^5.4.9" + "vite": "6.0.0-beta.2" }, "engines": { "node": "^18.17.1 || ^20.3.0 || >=21.0.0" 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) { diff --git a/packages/integrations/mdx/test/css-head-mdx.test.js b/packages/integrations/mdx/test/css-head-mdx.test.js index 4d4df5cec..96ee7c900 100644 --- a/packages/integrations/mdx/test/css-head-mdx.test.js +++ b/packages/integrations/mdx/test/css-head-mdx.test.js @@ -15,7 +15,6 @@ describe('Head injection w/ MDX', () => { integrations: [mdx()], // test suite was authored when inlineStylesheets defaulted to never build: { inlineStylesheets: 'never' }, - experimental: { contentLayer: true }, }); }); @@ -24,14 +23,14 @@ describe('Head injection w/ MDX', () => { await fixture.build(); }); - it('only injects contents into head', async () => { + it('injects content styles into head', async () => { const html = await fixture.readFile('/indexThree/index.html'); const { document } = parseHTML(html); const links = document.querySelectorAll('head link[rel=stylesheet]'); assert.equal(links.length, 1); - const scripts = document.querySelectorAll('head script[type=module]'); + const scripts = document.querySelectorAll('script[type=module]'); assert.equal(scripts.length, 1); }); @@ -50,7 +49,7 @@ describe('Head injection w/ MDX', () => { const links = document.querySelectorAll('head link[rel=stylesheet]'); assert.equal(links.length, 1); - const scripts = document.querySelectorAll('head script[type=module]'); + const scripts = document.querySelectorAll('script[type=module]'); assert.equal(scripts.length, 1); }); diff --git a/packages/integrations/mdx/test/fixtures/mdx-images/src/content/config.ts b/packages/integrations/mdx/test/fixtures/mdx-images/src/content/config.ts new file mode 100644 index 000000000..14443e78d --- /dev/null +++ b/packages/integrations/mdx/test/fixtures/mdx-images/src/content/config.ts @@ -0,0 +1,5 @@ +import { defineCollection, z } from 'astro:content'; + +const blog = defineCollection({}); + +export const collections = { blog }; diff --git a/packages/integrations/mdx/test/fixtures/mdx-images/tsconfig.json b/packages/integrations/mdx/test/fixtures/mdx-images/tsconfig.json index b5bf6a715..c193287fc 100644 --- a/packages/integrations/mdx/test/fixtures/mdx-images/tsconfig.json +++ b/packages/integrations/mdx/test/fixtures/mdx-images/tsconfig.json @@ -5,5 +5,7 @@ "paths": { "~/assets/*": ["src/assets/*"] }, - } + }, + "include": [".astro/types.d.ts", "**/*"], + "exclude": ["dist"] } diff --git a/packages/integrations/mdx/test/mdx-vite-env-vars.test.js b/packages/integrations/mdx/test/mdx-vite-env-vars.test.js index 80a9b1cec..213386ceb 100644 --- a/packages/integrations/mdx/test/mdx-vite-env-vars.test.js +++ b/packages/integrations/mdx/test/mdx-vite-env-vars.test.js @@ -57,8 +57,8 @@ describe('MDX - Vite env vars', () => { const dataAttrDump = document.querySelector('[data-env-dump]'); assert.notEqual(dataAttrDump, null); - assert.notEqual(dataAttrDump.getAttribute('data-env-prod'), null); - assert.equal(dataAttrDump.getAttribute('data-env-dev'), null); + assert.equal(dataAttrDump.getAttribute('data-env-prod'), 'true'); + assert.equal(dataAttrDump.getAttribute('data-env-dev'), 'false'); assert.equal(dataAttrDump.getAttribute('data-env-base-url'), '/'); assert.equal(dataAttrDump.getAttribute('data-env-mode'), 'production'); }); |