Skip to content
Draft
5 changes: 5 additions & 0 deletions .changeset/shaggy-emus-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"astro": patch
---

fix(cloudflare): Attempt at a fix for style rendering inside non-Node runtimes
5 changes: 5 additions & 0 deletions packages/astro/dev-only.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ declare module 'virtual:astro:dev-css-all' {
export const devCSSMap: Map<string, () => Promise<{ css: Set<ImportedDevStyles> }>>;
}

declare module 'virtual:astro:dev-component-metadata' {
import type { SSRComponentMetadata } from './src/types/public/internal.js';
export const componentMetadata: Map<string, SSRComponentMetadata>;
}

declare module 'virtual:astro:app' {
export const createApp: import('./src/core/app/types.js').CreateApp;
}
16 changes: 15 additions & 1 deletion packages/astro/src/core/app/dev/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -131,7 +132,20 @@ export class NonRunnablePipeline extends Pipeline {
return { scripts, styles, links };
}

componentMetadata() {}
async componentMetadata(): Promise<SSRResult['componentMetadata']> {
// 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<ComponentInstance> {
try {
Expand Down
6 changes: 5 additions & 1 deletion packages/astro/src/runtime/server/render/astro/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
128 changes: 91 additions & 37 deletions packages/astro/src/vite-plugin-head/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down Expand Up @@ -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 {
Expand Down
Loading