diff --git a/.changeset/shaggy-emus-reply.md b/.changeset/shaggy-emus-reply.md new file mode 100644 index 000000000000..b94867496e31 --- /dev/null +++ b/.changeset/shaggy-emus-reply.md @@ -0,0 +1,5 @@ +--- +"astro": patch +--- + +fix(cloudflare): Attempt at a fix for style rendering inside non-Node runtimes diff --git a/packages/astro/dev-only.d.ts b/packages/astro/dev-only.d.ts index 96f3a94d7924..8e4d2cb7c82c 100644 --- a/packages/astro/dev-only.d.ts +++ b/packages/astro/dev-only.d.ts @@ -78,6 +78,11 @@ declare module 'virtual:astro:dev-css-all' { export const devCSSMap: Map Promise<{ css: Set }>>; } +declare module 'virtual:astro:dev-component-metadata' { + import type { SSRComponentMetadata } from './src/types/public/internal.js'; + export const componentMetadata: Map; +} + declare module 'virtual:astro:app' { export const createApp: import('./src/core/app/types.js').CreateApp; } diff --git a/packages/astro/src/core/app/dev/pipeline.ts b/packages/astro/src/core/app/dev/pipeline.ts index 846740729c3b..c1425894050a 100644 --- a/packages/astro/src/core/app/dev/pipeline.ts +++ b/packages/astro/src/core/app/dev/pipeline.ts @@ -5,6 +5,7 @@ import type { RouteData, SSRElement, } from '../../../types/public/index.js'; +import type { SSRResult } from '../../../types/public/internal.js'; import { type HeadElements, Pipeline, type TryRewriteResult } from '../../base-pipeline.js'; import { ASTRO_VERSION } from '../../constants.js'; import { createModuleScriptElement, createStylesheetElementSet } from '../../render/ssr-element.js'; @@ -131,7 +132,20 @@ export class NonRunnablePipeline extends Pipeline { return { scripts, styles, links }; } - componentMetadata() {} + async componentMetadata(): Promise { + // Import component metadata from the virtual module exposed by the head-metadata plugin. + // This module is dynamically generated from the SSR environment's module graph, + // which contains propagation hints and containsHead flags set during resolveId/transform. + // This is needed because NonRunnablePipeline (e.g. Cloudflare workerd) cannot access + // the Vite module graph directly at render time. + try { + const { componentMetadata } = await import('virtual:astro:dev-component-metadata'); + return componentMetadata; + } catch { + // If the virtual module is not available, fall back to empty metadata. + return new Map(); + } + } async getComponentByRoute(routeData: RouteData): Promise { try { diff --git a/packages/astro/src/runtime/server/render/astro/instance.ts b/packages/astro/src/runtime/server/render/astro/instance.ts index 78c4eb678314..03478f41195b 100644 --- a/packages/astro/src/runtime/server/render/astro/instance.ts +++ b/packages/astro/src/runtime/server/render/astro/instance.ts @@ -77,6 +77,9 @@ export class AstroComponentInstance { private renderImpl(destination: RenderDestination, returnValue: AstroFactoryReturnValue) { if (isHeadAndContent(returnValue)) { + if (returnValue.head) { + this.result._metadata.extraHead.push(returnValue.head); + } return returnValue.content.render(destination); } else { return renderChild(destination, returnValue); @@ -111,7 +114,8 @@ export function createAstroComponentInstance( ) { validateComponentProps(props, result.clientDirectives, displayName); const instance = new AstroComponentInstance(result, props, slots, factory); - if (isAPropagatingComponent(result, factory)) { + const isPropagating = isAPropagatingComponent(result, factory); + if (isPropagating) { result._metadata.propagators.add(instance); } return instance; diff --git a/packages/astro/src/vite-plugin-head/index.ts b/packages/astro/src/vite-plugin-head/index.ts index bebc75c17ab9..0e867c424721 100644 --- a/packages/astro/src/vite-plugin-head/index.ts +++ b/packages/astro/src/vite-plugin-head/index.ts @@ -11,8 +11,12 @@ import { ASTRO_VITE_ENVIRONMENT_NAMES } from '../core/constants.js'; // Detect this in comments, both in .astro components and in js/ts files. const injectExp = /(?:^\/\/|\/\/!)\s*astro-head-inject/; -export default function configHeadVitePlugin(): vite.Plugin { +export const DEV_COMPONENT_METADATA_ID = 'virtual:astro:dev-component-metadata'; +const DEV_COMPONENT_METADATA_RESOLVED_ID = '\0' + DEV_COMPONENT_METADATA_ID; + +export default function configHeadVitePlugin(): vite.Plugin[] { let environment: DevEnvironment; + let server: vite.ViteDevServer; function propagateMetadata< P extends keyof PluginMetadata['astro'], @@ -43,47 +47,97 @@ export default function configHeadVitePlugin(): vite.Plugin { } } - return { - name: 'astro:head-metadata', - enforce: 'pre', - apply: 'serve', - configureServer(server) { - environment = server.environments[ASTRO_VITE_ENVIRONMENT_NAMES.ssr]; - }, - resolveId(source, importer) { - if (importer) { - // Do propagation any time a new module is imported. This is because - // A module with propagation might be loaded before one of its parent pages - // is loaded, in which case that parent page won't have the in-tree and containsHead - // values. Walking up the tree in resolveId ensures that they do - return this.resolve(source, importer, { skipSelf: true }).then((result) => { - if (result) { - let info = this.getModuleInfo(result.id); - const astro = info && getAstroMetadata(info); - if (astro) { - if (astro.propagation === 'self' || astro.propagation === 'in-tree') { - propagateMetadata.call(this, importer, 'propagation', 'in-tree'); + return [ + { + name: 'astro:head-metadata', + enforce: 'pre', + apply: 'serve', + configureServer(_server) { + server = _server; + environment = server.environments[ASTRO_VITE_ENVIRONMENT_NAMES.ssr]; + }, + resolveId(source, importer) { + if (importer) { + // Do propagation any time a new module is imported. This is because + // A module with propagation might be loaded before one of its parent pages + // is loaded, in which case that parent page won't have the in-tree and containsHead + // values. Walking up the tree in resolveId ensures that they do + return this.resolve(source, importer, { skipSelf: true }).then((result) => { + if (result) { + let info = this.getModuleInfo(result.id); + const astro = info && getAstroMetadata(info); + if (astro) { + if (astro.propagation === 'self' || astro.propagation === 'in-tree') { + propagateMetadata.call(this, importer, 'propagation', 'in-tree'); + } + if (astro.containsHead) { + propagateMetadata.call(this, importer, 'containsHead', true); + } } - if (astro.containsHead) { - propagateMetadata.call(this, importer, 'containsHead', true); + } + return result; + }); + } + }, + transform(source, id) { + let info = this.getModuleInfo(id); + if (info && getAstroMetadata(info)?.containsHead) { + propagateMetadata.call(this, id, 'containsHead', true); + } + + if (injectExp.test(source)) { + propagateMetadata.call(this, id, 'propagation', 'in-tree'); + } + }, + }, + { + // Virtual module that exposes componentMetadata collected by the head-metadata plugin. + // This is used by NonRunnablePipeline (e.g. Cloudflare workerd) which cannot access + // the Vite module graph directly at render time. + name: 'astro:dev-component-metadata', + apply: 'serve', + resolveId: { + filter: { + id: new RegExp(`^${DEV_COMPONENT_METADATA_ID}$`), + }, + handler() { + return DEV_COMPONENT_METADATA_RESOLVED_ID; + }, + }, + load: { + filter: { + id: new RegExp(`^\\0${DEV_COMPONENT_METADATA_ID}$`), + }, + handler() { + // Collect all component metadata from the SSR environment's module graph. + // The head-metadata plugin has already propagated 'in-tree' and 'containsHead' + // up through the module graph during resolveId/transform. + const entries: [string, SSRComponentMetadata][] = []; + const ssrEnv = server?.environments[ASTRO_VITE_ENVIRONMENT_NAMES.ssr]; + if (ssrEnv) { + for (const [id] of ssrEnv.moduleGraph.idToModuleMap) { + const info = ssrEnv.pluginContainer.getModuleInfo(id); + if (info) { + const astro = getAstroMetadata(info); + if (astro && (astro.propagation !== 'none' || astro.containsHead)) { + entries.push([ + id, + { + propagation: astro.propagation || 'none', + containsHead: astro.containsHead || false, + }, + ]); + } } } } - return result; - }); - } + return { + code: `export const componentMetadata = new Map(${JSON.stringify(entries)});`, + }; + }, + }, }, - transform(source, id) { - let info = this.getModuleInfo(id); - if (info && getAstroMetadata(info)?.containsHead) { - propagateMetadata.call(this, id, 'containsHead', true); - } - - if (injectExp.test(source)) { - propagateMetadata.call(this, id, 'propagation', 'in-tree'); - } - }, - }; + ]; } export function astroHeadBuildPlugin(internals: BuildInternals): vite.Plugin {