Sitelet https://github.com/withastro/astro/pull/17841/files
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/metal-pans-allow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Fixes SSR manifest containing stale `entryModules` references to prerender-only chunks that no longer exist in the final build output
10 changes: 10 additions & 0 deletions packages/astro/src/core/build/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ export interface BuildInternals {
// Used to render pages with the correct specifiers.
entrySpecifierToBundleMap: Map<string, string>;

/**
* Specifiers written to `entrySpecifierToBundleMap` by the prerender environment
* that have not been overwritten by the SSR or client environments. These point
* to chunk files inside the prerender output directory, which is deleted after
* page generation. They must be stripped from the SSR manifest to avoid dangling
* references.
*/
prerenderOnlyEntrySpecifiers: Set<string>;

/**
* A map for page-specific information.
*/
Expand Down Expand Up @@ -167,6 +176,7 @@ export function createBuildInternals(): BuildInternals {
prerenderCssAssetByModuleKey: new Map(),
inlinedScripts: new Map(),
entrySpecifierToBundleMap: new Map<string, string>(),
prerenderOnlyEntrySpecifiers: new Set<string>(),
pagesByKeys: new Map(),
pagesByViteID: new Map(),
pagesByClientOnly: new Map(),
Expand Down
10 changes: 9 additions & 1 deletion packages/astro/src/core/build/plugins/plugin-internals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export function pluginInternals(
);
}
await Promise.all(promises);
const isPrerender = this.environment?.name === ASTRO_VITE_ENVIRONMENT_NAMES.prerender;
for (const [_, chunk] of Object.entries(bundle)) {
if (chunk.fileName.startsWith(options.settings.config.build.assets)) {
internals.clientChunksAndAssets.add(chunk.fileName);
Expand All @@ -86,7 +87,14 @@ export function pluginInternals(
if (chunk.type === 'chunk' && chunk.facadeModuleId) {
const specifiers = mapping.get(chunk.facadeModuleId) || new Set([chunk.facadeModuleId]);
for (const specifier of specifiers) {
internals.entrySpecifierToBundleMap.set(normalizeEntryId(specifier), chunk.fileName);
const normalizedId = normalizeEntryId(specifier);
internals.entrySpecifierToBundleMap.set(normalizedId, chunk.fileName);
if (isPrerender) {
internals.prerenderOnlyEntrySpecifiers.add(normalizedId);
} else {
// Present in SSR or client — no longer prerender-only
internals.prerenderOnlyEntrySpecifiers.delete(normalizedId);
}
}
}
}
Expand Down
23 changes: 22 additions & 1 deletion packages/astro/src/core/build/plugins/plugin-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ export async function manifestBuildPostHook(
// HTML on disk already has them inlined, and the SSR worker never renders
// these routes. Stripping keeps the entry chunk small on platforms like
// Cloudflare Workers that re-parse it on every cold isolate start.
const ssrManifest = stripPrerenderedRouteStyles(manifest);
let ssrManifest = stripPrerenderedRouteStyles(manifest);
ssrManifest = stripPrerenderOnlyEntryModules(ssrManifest, internals);
const code = injectManifest(ssrManifest, ssrManifestChunk.code);
mutate(ssrManifestChunk.fileName, code, false);
}
Expand Down Expand Up @@ -173,6 +174,26 @@ function stripPrerenderedRouteStyles(manifest: SerializedSSRManifest): Serialize
return stripped ? { ...manifest, routes } : manifest;
}

/**
* Returns a copy of the manifest with `entryModules` entries removed for
* specifiers that were only emitted by the prerender environment. Those
* chunks live in the prerender output directory, which is deleted after
* page generation, so referencing them from the SSR manifest would produce
* dangling asset URLs at runtime.
*/
function stripPrerenderOnlyEntryModules(
manifest: SerializedSSRManifest,
internals: BuildInternals,
): SerializedSSRManifest {
if (internals.prerenderOnlyEntrySpecifiers.size === 0) return manifest;
const filtered = Object.fromEntries(
Object.entries(manifest.entryModules).filter(
([key]) => !internals.prerenderOnlyEntrySpecifiers.has(key),
),
);
return { ...manifest, entryModules: filtered };
}

async function buildManifest(
opts: StaticBuildOptions,
internals: BuildInternals,
Expand Down
39 changes: 39 additions & 0 deletions packages/astro/test/units/build/prerender-entry-specifiers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { createBuildInternals } from '../../../dist/core/build/internal.js';

describe('prerenderOnlyEntrySpecifiers', () => {
it('is initialized as an empty set', () => {
const internals = createBuildInternals();
assert.ok(internals.prerenderOnlyEntrySpecifiers instanceof Set);
assert.equal(internals.prerenderOnlyEntrySpecifiers.size, 0);
});

it('tracks specifiers added by the prerender environment', () => {
const internals = createBuildInternals();
internals.prerenderOnlyEntrySpecifiers.add('/project/src/lib/lazy.mjs');
assert.ok(internals.prerenderOnlyEntrySpecifiers.has('/project/src/lib/lazy.mjs'));
});

it('removes specifier when also present in SSR or client environment', () => {
const internals = createBuildInternals();
// Simulates prerender writing first
internals.prerenderOnlyEntrySpecifiers.add('/project/src/lib/shared.mjs');
// Simulates SSR overwriting — the specifier is no longer prerender-only
internals.prerenderOnlyEntrySpecifiers.delete('/project/src/lib/shared.mjs');
assert.equal(internals.prerenderOnlyEntrySpecifiers.has('/project/src/lib/shared.mjs'), false);
});

it('retains specifiers not overwritten by later environments', () => {
const internals = createBuildInternals();
// Prerender adds two entries
internals.prerenderOnlyEntrySpecifiers.add('/project/src/lib/lazy.mjs');
internals.prerenderOnlyEntrySpecifiers.add('/project/src/lib/shared.mjs');
// SSR overwrites only shared.mjs
internals.prerenderOnlyEntrySpecifiers.delete('/project/src/lib/shared.mjs');
// lazy.mjs remains prerender-only
assert.ok(internals.prerenderOnlyEntrySpecifiers.has('/project/src/lib/lazy.mjs'));
assert.equal(internals.prerenderOnlyEntrySpecifiers.has('/project/src/lib/shared.mjs'), false);
assert.equal(internals.prerenderOnlyEntrySpecifiers.size, 1);
});
});
Loading