diff --git a/.changeset/heavy-tomatoes-know.md b/.changeset/heavy-tomatoes-know.md new file mode 100644 index 000000000..ae8fd2dac --- /dev/null +++ b/.changeset/heavy-tomatoes-know.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Apply MDX `components` export when rendering as a content collection entry diff --git a/packages/astro/src/content/internal.ts b/packages/astro/src/content/internal.ts index 2b8f8ba6f..04280166f 100644 --- a/packages/astro/src/content/internal.ts +++ b/packages/astro/src/content/internal.ts @@ -1,3 +1,4 @@ +import { AstroError, AstroErrorData } from '../core/errors/index.js'; import { prependForwardSlash } from '../core/path.js'; import { @@ -120,21 +121,32 @@ async function render({ id: string; collectionToRenderEntryMap: CollectionToEntryMap; }) { - const lazyImport = collectionToRenderEntryMap[collection]?.[id]; - if (!lazyImport) throw new Error(`${String(collection)} → ${String(id)} does not exist.`); + const UnexpectedRenderError = new AstroError({ + ...AstroErrorData.UnknownContentCollectionError, + message: `Unexpected error while rendering ${String(collection)} → ${String(id)}.`, + }); - const mod = await lazyImport(); + const lazyImport = collectionToRenderEntryMap[collection]?.[id]; + if (typeof lazyImport !== 'function') throw UnexpectedRenderError; + + const baseMod = await lazyImport(); + if (baseMod == null || typeof baseMod !== 'object') throw UnexpectedRenderError; + + const { collectedStyles, collectedLinks, collectedScripts, getMod } = baseMod; + if (typeof getMod !== 'function') throw UnexpectedRenderError; + const mod = await getMod(); + if (mod == null || typeof mod !== 'object') throw UnexpectedRenderError; const Content = createComponent({ - factory(result, props, slots) { + factory(result, baseProps, slots) { let styles = '', links = '', scripts = ''; - if (Array.isArray(mod?.collectedStyles)) { - styles = mod.collectedStyles.map((style: any) => renderStyleElement(style)).join(''); + if (Array.isArray(collectedStyles)) { + styles = collectedStyles.map((style: any) => renderStyleElement(style)).join(''); } - if (Array.isArray(mod?.collectedLinks)) { - links = mod.collectedLinks + if (Array.isArray(collectedLinks)) { + links = collectedLinks .map((link: any) => { return renderUniqueStylesheet(result, { href: prependForwardSlash(link), @@ -142,8 +154,17 @@ async function render({ }) .join(''); } - if (Array.isArray(mod?.collectedScripts)) { - scripts = mod.collectedScripts.map((script: any) => renderScriptElement(script)).join(''); + if (Array.isArray(collectedScripts)) { + scripts = collectedScripts.map((script: any) => renderScriptElement(script)).join(''); + } + + let props = baseProps; + // Auto-apply MDX components export + if (id.endsWith('mdx')) { + props = { + components: mod.components ?? {}, + ...baseProps, + }; } return createHeadAndContent( diff --git a/packages/astro/src/content/vite-plugin-content-assets.ts b/packages/astro/src/content/vite-plugin-content-assets.ts index bacfc1653..eccff8716 100644 --- a/packages/astro/src/content/vite-plugin-content-assets.ts +++ b/packages/astro/src/content/vite-plugin-content-assets.ts @@ -36,7 +36,9 @@ export function astroContentAssetPropagationPlugin({ mode }: { mode: string }): if (isPropagatedAsset(id)) { const basePath = id.split('?')[0]; const code = ` - export { Content, getHeadings, frontmatter } from ${JSON.stringify(basePath)}; + export async function getMod() { + return import(${JSON.stringify(basePath)}); + } export const collectedLinks = ${JSON.stringify(LINKS_PLACEHOLDER)}; export const collectedStyles = ${JSON.stringify(STYLES_PLACEHOLDER)}; export const collectedScripts = ${JSON.stringify(SCRIPTS_PLACEHOLDER)}; diff --git a/packages/astro/test/content-collections-render.test.js b/packages/astro/test/content-collections-render.test.js index b19db2e94..5915fada8 100644 --- a/packages/astro/test/content-collections-render.test.js +++ b/packages/astro/test/content-collections-render.test.js @@ -69,6 +69,15 @@ describe('Content Collections - render()', () => { '`WithScripts.astro` hoisted script included unexpectedly.' ).to.be.undefined; }); + + it('Applies MDX components export', async () => { + const html = await fixture.readFile('/launch-week-components-export/index.html'); + const $ = cheerio.load(html); + + const h2 = $('h2'); + expect(h2).to.have.a.lengthOf(1); + expect(h2.attr('data-components-export-applied')).to.equal('true'); + }); }); describe('Build - SSR', () => { @@ -108,6 +117,18 @@ describe('Content Collections - render()', () => { // Includes styles expect($('link[rel=stylesheet]')).to.have.a.lengthOf(0); }); + + it('Applies MDX components export', async () => { + const app = await fixture.loadTestAdapterApp(); + const request = new Request('http://example.com/launch-week-components-export'); + const response = await app.render(request); + const html = await response.text(); + const $ = cheerio.load(html); + + const h2 = $('h2'); + expect(h2).to.have.a.lengthOf(1); + expect(h2.attr('data-components-export-applied')).to.equal('true'); + }); }); describe('Dev - SSG', () => { @@ -160,5 +181,17 @@ describe('Content Collections - render()', () => { // Includes inline script expect($('script[data-is-inline]')).to.have.a.lengthOf(1); }); + + it('Applies MDX components export', async () => { + const response = await fixture.fetch('/launch-week-components-export', { method: 'GET' }); + expect(response.status).to.equal(200); + + const html = await response.text(); + const $ = cheerio.load(html); + + const h2 = $('h2'); + expect(h2).to.have.a.lengthOf(1); + expect(h2.attr('data-components-export-applied')).to.equal('true'); + }); }); }); diff --git a/packages/astro/test/fixtures/content/src/components/H2.astro b/packages/astro/test/fixtures/content/src/components/H2.astro new file mode 100644 index 000000000..d1ad359c2 --- /dev/null +++ b/packages/astro/test/fixtures/content/src/components/H2.astro @@ -0,0 +1,4 @@ +--- +--- + +