diff --git a/.changeset/dull-monkeys-grab.md b/.changeset/dull-monkeys-grab.md new file mode 100644 index 000000000..c7ba4dcdb --- /dev/null +++ b/.changeset/dull-monkeys-grab.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fix component usage in imported markdown files diff --git a/packages/astro/src/core/render/dev/index.ts b/packages/astro/src/core/render/dev/index.ts index e71284085..1522086f2 100644 --- a/packages/astro/src/core/render/dev/index.ts +++ b/packages/astro/src/core/render/dev/index.ts @@ -1,5 +1,5 @@ import { fileURLToPath } from 'url'; -import type * as vite from 'vite'; +import type { HtmlTagDescriptor, ViteDevServer } from 'vite'; import type { AstroConfig, AstroRenderer, @@ -15,9 +15,9 @@ import { prependForwardSlash } from '../../../core/path.js'; import { RouteCache } from '../route-cache.js'; import { createModuleScriptElementWithSrcSet } from '../ssr-element.js'; import { getStylesForURL } from './css.js'; -import { getHmrScript } from './hmr.js'; import { injectTags } from './html.js'; import { isBuildingToSSR } from '../../util.js'; +import { collectMdMetadata } from '../util.js'; export interface SSROptions { /** an instance of the AstroConfig */ @@ -37,7 +37,7 @@ export interface SSROptions { /** pass in route cache because SSR can’t manage cache-busting */ routeCache: RouteCache; /** Vite instance */ - viteServer: vite.ViteDevServer; + viteServer: ViteDevServer; /** Request */ request: Request; } @@ -51,7 +51,7 @@ export type RenderResponse = const svelteStylesRE = /svelte\?svelte&type=style/; async function loadRenderer( - viteServer: vite.ViteDevServer, + viteServer: ViteDevServer, renderer: AstroRenderer ): Promise { // Vite modules can be out-of-date when using an un-resolved url @@ -65,7 +65,7 @@ async function loadRenderer( } export async function loadRenderers( - viteServer: vite.ViteDevServer, + viteServer: ViteDevServer, astroConfig: AstroConfig ): Promise { return Promise.all(astroConfig._ctx.renderers.map((r) => loadRenderer(viteServer, r))); @@ -80,6 +80,15 @@ export async function preload({ const renderers = await loadRenderers(viteServer, astroConfig); // Load the module from the Vite SSR Runtime. const mod = (await viteServer.ssrLoadModule(fileURLToPath(filePath))) as ComponentInstance; + if (viteServer.config.mode === 'development' || !mod?.$$metadata) { + return [renderers, mod]; + } + + // append all nested markdown metadata to mod.$$metadata + const modGraph = await viteServer.moduleGraph.getModuleByUrl(fileURLToPath(filePath)); + if (modGraph) { + await collectMdMetadata(mod.$$metadata, modGraph, viteServer); + } return [renderers, mod]; } @@ -179,7 +188,7 @@ export async function render( } // inject tags - const tags: vite.HtmlTagDescriptor[] = []; + const tags: HtmlTagDescriptor[] = []; // add injected tags let html = injectTags(content.html, tags); diff --git a/packages/astro/src/core/render/util.ts b/packages/astro/src/core/render/util.ts index 6ef412a9c..da34dfd3c 100644 --- a/packages/astro/src/core/render/util.ts +++ b/packages/astro/src/core/render/util.ts @@ -1,4 +1,6 @@ import npath from 'path-browserify'; +import type { ModuleNode, ViteDevServer } from 'vite'; +import type { Metadata } from '../../runtime/server/metadata.js'; /** Normalize URL to its canonical form */ export function createCanonicalURL(url: string, base?: string): URL { @@ -30,9 +32,59 @@ export const STYLE_EXTENSIONS = new Set([ '.less', ]); +// duplicate const from vite-plugin-markdown +// can't import directly due to Deno bundling issue +// (node fs import failing during prod builds) +const MARKDOWN_IMPORT_FLAG = '?mdImport'; + const cssRe = new RegExp( `\\.(${Array.from(STYLE_EXTENSIONS) .map((s) => s.slice(1)) .join('|')})($|\\?)` ); export const isCSSRequest = (request: string): boolean => cssRe.test(request); + +// During prod builds, some modules have dependencies we should preload by hand +// Ex. markdown files imported asynchronously or via Astro.glob(...) +// This calls each md file's $$loadMetadata to discover those dependencies +// and writes all results to the input `metadata` object +const seenMdMetadata = new Set(); +export async function collectMdMetadata( + metadata: Metadata, + modGraph: ModuleNode, + viteServer: ViteDevServer, +) { + const importedModules = [...(modGraph?.importedModules ?? [])]; + await Promise.all( + importedModules.map(async (importedModule) => { + // recursively check for importedModules + if (!importedModule.id || seenMdMetadata.has(importedModule.id)) return; + + seenMdMetadata.add(importedModule.id); + await collectMdMetadata(metadata, importedModule, viteServer); + + if (!importedModule?.id?.endsWith(MARKDOWN_IMPORT_FLAG)) return; + + const mdSSRMod = await viteServer.ssrLoadModule(importedModule.id); + const mdMetadata = (await mdSSRMod.$$loadMetadata?.()) as Metadata; + if (!mdMetadata) return; + + for (let mdMod of mdMetadata.modules) { + mdMod.specifier = mdMetadata.resolvePath(mdMod.specifier); + metadata.modules.push(mdMod); + } + for (let mdHoisted of mdMetadata.hoisted) { + metadata.hoisted.push(mdHoisted); + } + for (let mdHydrated of mdMetadata.hydratedComponents) { + metadata.hydratedComponents.push(mdHydrated); + } + for (let mdClientOnly of mdMetadata.clientOnlyComponents) { + metadata.clientOnlyComponents.push(mdClientOnly); + } + for (let mdHydrationDirective of mdMetadata.hydrationDirectives) { + metadata.hydrationDirectives.add(mdHydrationDirective); + } + }) + ); +} diff --git a/packages/astro/src/vite-plugin-markdown/index.ts b/packages/astro/src/vite-plugin-markdown/index.ts index 9c7577fc3..d75351ca3 100644 --- a/packages/astro/src/vite-plugin-markdown/index.ts +++ b/packages/astro/src/vite-plugin-markdown/index.ts @@ -89,6 +89,10 @@ export default function markdown({ config }: AstroPluginOptions): Plugin { export const frontmatter = ${JSON.stringify(frontmatter)}; export const file = ${JSON.stringify(fileId)}; export const url = ${JSON.stringify(fileUrl)}; + + export function $$loadMetadata() { + return load().then((m) => m.$$metadata) + } // Deferred export default async function load() { @@ -109,10 +113,10 @@ export default function markdown({ config }: AstroPluginOptions): Plugin { // directly as a page in Vite, or it was a deferred render from a JS module. // This returns the compiled markdown -> astro component that renders to HTML. if (id.endsWith('.md')) { - const source = await fs.promises.readFile(id, 'utf8'); + const filename = normalizeFilename(id); + const source = await fs.promises.readFile(filename, 'utf8'); const renderOpts = config.markdown; - const filename = normalizeFilename(id); const fileUrl = new URL(`file://${filename}`); const isPage = fileUrl.pathname.startsWith(resolvePages(config).pathname); const hasInjectedScript = isPage && config._ctx.scripts.some((s) => s.stage === 'page-ssr'); @@ -142,7 +146,7 @@ ${setup}`.trim(); // Transform from `.astro` to valid `.ts` let { code: tsResult } = await transform(astroResult, { - pathname: fileUrl.pathname.slice(config.root.pathname.length - 1), + pathname: '/@fs' + prependForwardSlash(fileUrl.pathname), projectRoot: config.root.toString(), site: config.site ? new URL(config.base, config.site).toString() : undefined, sourcefile: id, diff --git a/packages/astro/test/fixtures/astro-markdown/astro.config.mjs b/packages/astro/test/fixtures/astro-markdown/astro.config.mjs index 08916b1fe..e1986197c 100644 --- a/packages/astro/test/fixtures/astro-markdown/astro.config.mjs +++ b/packages/astro/test/fixtures/astro-markdown/astro.config.mjs @@ -1,7 +1,8 @@ import { defineConfig } from 'astro/config'; import preact from '@astrojs/preact'; +import svelte from "@astrojs/svelte"; // https://astro.build/config export default defineConfig({ - integrations: [preact()], + integrations: [preact(), svelte()] }); diff --git a/packages/astro/test/fixtures/astro-markdown/package.json b/packages/astro/test/fixtures/astro-markdown/package.json index bf24faecd..e5a54cf7c 100644 --- a/packages/astro/test/fixtures/astro-markdown/package.json +++ b/packages/astro/test/fixtures/astro-markdown/package.json @@ -4,6 +4,7 @@ "private": true, "dependencies": { "@astrojs/preact": "workspace:*", + "@astrojs/svelte": "workspace:*", "astro": "workspace:*" } } diff --git a/packages/astro/test/fixtures/astro-markdown/src/components/SvelteButton.svelte b/packages/astro/test/fixtures/astro-markdown/src/components/SvelteButton.svelte new file mode 100644 index 000000000..74f3ff6a9 --- /dev/null +++ b/packages/astro/test/fixtures/astro-markdown/src/components/SvelteButton.svelte @@ -0,0 +1,11 @@ + + + + + diff --git a/packages/astro/test/fixtures/astro-markdown/src/imported-md/plain.md b/packages/astro/test/fixtures/astro-markdown/src/imported-md/plain.md new file mode 100644 index 000000000..d548b3356 --- /dev/null +++ b/packages/astro/test/fixtures/astro-markdown/src/imported-md/plain.md @@ -0,0 +1,6 @@ +--- +--- + +## Plain jane + +I am plain markdown! diff --git a/packages/astro/test/fixtures/astro-markdown/src/imported-md/with-components.md b/packages/astro/test/fixtures/astro-markdown/src/imported-md/with-components.md new file mode 100644 index 000000000..dc6c23bf9 --- /dev/null +++ b/packages/astro/test/fixtures/astro-markdown/src/imported-md/with-components.md @@ -0,0 +1,17 @@ +--- +setup: | + import Counter from '../components/Counter.jsx' + import Hello from '../components/Hello.jsx' + import SvelteButton from '../components/SvelteButton.svelte' +--- + +## With components + +### Non-hydrated + + + +### Hydrated + + + diff --git a/packages/astro/test/fixtures/astro-markdown/src/pages/imported-md/with-components.astro b/packages/astro/test/fixtures/astro-markdown/src/pages/imported-md/with-components.astro new file mode 100644 index 000000000..97cd8f211 --- /dev/null +++ b/packages/astro/test/fixtures/astro-markdown/src/pages/imported-md/with-components.astro @@ -0,0 +1,9 @@ +--- +import Layout from '../../layouts/content.astro' + +const posts = await Astro.glob('../../imported-md/*.md') +--- + + + {posts.map(({ Content }) => )} + diff --git a/packages/astro/test/test-utils.js b/packages/astro/test/test-utils.js index 8b4e9d310..bbdd86b57 100644 --- a/packages/astro/test/test-utils.js +++ b/packages/astro/test/test-utils.js @@ -118,7 +118,7 @@ export async function loadFixture(inlineConfig) { let devServer; return { - build: (opts = {}) => build(config, { mode: 'development', logging, telemetry, ...opts }), + build: (opts = {}) => build(config, { logging, telemetry, ...opts }), startDevServer: async (opts = {}) => { devServer = await dev(config, { logging, telemetry, ...opts }); config.server.port = devServer.address.port; // update port diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ea9a4cc41..df5a78140 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -854,9 +854,11 @@ importers: packages/astro/test/fixtures/astro-markdown: specifiers: '@astrojs/preact': workspace:* + '@astrojs/svelte': workspace:* astro: workspace:* dependencies: '@astrojs/preact': link:../../../../integrations/preact + '@astrojs/svelte': link:../../../../integrations/svelte astro: link:../../.. packages/astro/test/fixtures/astro-markdown-css: