Skip to content

Commit 765a887

Browse files
authored
Validate non-prerendered routes after resolution (#15890)
1 parent c43ef8a commit 765a887

8 files changed

Lines changed: 95 additions & 19 deletions

File tree

.changeset/fair-buttons-float.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'astro': patch
3+
---
4+
5+
Fixes `astro:actions` validation to check resolved routes, so projects using default static output with at least one `prerender = false` page or endpoint no longer fail during startup.

packages/astro/src/actions/integration.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { AstroError } from '../core/errors/errors.js';
22
import { ActionsWithoutServerOutputError } from '../core/errors/errors-data.js';
3+
import { hasNonPrerenderedProjectRoute } from '../core/routing/helpers.js';
34
import { viteID } from '../core/util.js';
45
import type { AstroSettings } from '../types/astro.js';
56
import type { AstroIntegration } from '../types/public/integrations.js';
@@ -29,22 +30,23 @@ export default function astroIntegrationActionsRouteHandler({
2930
});
3031
},
3132
'astro:config:done': async (params) => {
32-
if (params.buildOutput === 'static') {
33-
const error = new AstroError(ActionsWithoutServerOutputError);
34-
error.stack = undefined;
35-
throw error;
36-
}
37-
3833
const stringifiedActionsImport = JSON.stringify(
3934
viteID(new URL(`./${filename}`, params.config.srcDir)),
4035
);
4136
settings.injectedTypes.push({
4237
filename: ACTIONS_TYPES_FILE,
4338
content: `declare module "astro:actions" {
44-
export const actions: typeof import(${stringifiedActionsImport})["server"];
39+
export const actions: typeof import(${stringifiedActionsImport})["server"];
4540
}`,
4641
});
4742
},
43+
'astro:routes:resolved': ({ routes }) => {
44+
if (!hasNonPrerenderedProjectRoute(routes)) {
45+
const error = new AstroError(ActionsWithoutServerOutputError);
46+
error.stack = undefined;
47+
throw error;
48+
}
49+
},
4850
},
4951
};
5052
}

packages/astro/src/core/routing/helpers.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { RouteData } from '../../types/public/internal.js';
2+
import type { IntegrationResolvedRoute } from '../../types/public/integrations.js';
23
import type { RouteInfo } from '../app/types.js';
34
import type { RoutesList } from '../../types/astro.js';
45
import { isRoute404, isRoute500 } from './internal/route-errors.js';
@@ -62,3 +63,27 @@ export function getCustom404Route(manifestData: RoutesList): RouteData | undefin
6263
export function getCustom500Route(manifestData: RoutesList): RouteData | undefined {
6364
return manifestData.routes.find((r) => isRoute500(r.route));
6465
}
66+
67+
export function hasNonPrerenderedProjectRoute(
68+
routes: Array<Pick<RouteData, 'type' | 'origin' | 'prerender'>>,
69+
options?: { includeEndpoints?: boolean },
70+
): boolean;
71+
export function hasNonPrerenderedProjectRoute(
72+
routes: Array<Pick<IntegrationResolvedRoute, 'type' | 'origin' | 'isPrerendered'>>,
73+
options?: { includeEndpoints?: boolean },
74+
): boolean;
75+
export function hasNonPrerenderedProjectRoute(
76+
routes: Array<
77+
| Pick<RouteData, 'type' | 'origin' | 'prerender'>
78+
| Pick<IntegrationResolvedRoute, 'type' | 'origin' | 'isPrerendered'>
79+
>,
80+
options?: { includeEndpoints?: boolean },
81+
): boolean {
82+
const includeEndpoints = options?.includeEndpoints ?? true;
83+
const routeTypes: ReadonlyArray<string> = includeEndpoints ? ['page', 'endpoint'] : ['page'];
84+
85+
return routes.some((route) => {
86+
const isPrerendered = 'isPrerendered' in route ? route.isPrerendered : route.prerender;
87+
return routeTypes.includes(route.type) && route.origin === 'project' && !isPrerendered;
88+
});
89+
}

packages/astro/src/vite-plugin-renderers/index.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ConfigEnv, Plugin as VitePlugin } from 'vite';
22
import { ASTRO_VITE_ENVIRONMENT_NAMES } from '../core/constants.js';
3+
import { hasNonPrerenderedProjectRoute } from '../core/routing/helpers.js';
34
import type { AstroSettings, RoutesList } from '../types/astro.js';
45

56
export const ASTRO_RENDERERS_MODULE_ID = 'virtual:astro:renderers';
@@ -11,17 +12,6 @@ interface PluginOptions {
1112
command: ConfigEnv['command'];
1213
}
1314

14-
/**
15-
* Checks whether any non-prerendered route needs component rendering (i.e., is a page).
16-
* Internal routes like `_server-islands` are excluded because they only need renderers
17-
* when server islands are actually used, and those are detected separately during the build.
18-
*/
19-
function ssrBuildNeedsRenderers(routesList: RoutesList): boolean {
20-
return routesList.routes.some(
21-
(route) => route.type === 'page' && !route.prerender && route.origin !== 'internal',
22-
);
23-
}
24-
2515
export default function vitePluginRenderers(options: PluginOptions): VitePlugin {
2616
const renderers = options.settings.renderers;
2717

@@ -47,7 +37,9 @@ export default function vitePluginRenderers(options: PluginOptions): VitePlugin
4737
options.command === 'build' &&
4838
this.environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.ssr &&
4939
renderers.length > 0 &&
50-
!ssrBuildNeedsRenderers(options.routesList)
40+
!hasNonPrerenderedProjectRoute(options.routesList.routes, {
41+
includeEndpoints: false,
42+
})
5143
) {
5244
return { code: `export const renderers = [];` };
5345
}

packages/astro/test/actions.test.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,41 @@ describe('Astro Actions', () => {
636636
});
637637
});
638638

639+
describe('Astro Actions in static mode with prerender = false routes', () => {
640+
/** @type {import('./test-utils').Fixture} */
641+
let fixture;
642+
let devServer;
643+
644+
before(async () => {
645+
fixture = await loadFixture({
646+
root: './fixtures/actions-static-prerender-false/',
647+
});
648+
devServer = await fixture.startDevServer();
649+
});
650+
651+
after(async () => {
652+
await devServer?.stop();
653+
});
654+
655+
it('starts in dev and exposes action RPC routes', async () => {
656+
assert.ok(devServer, 'Expected dev server to start');
657+
658+
const res = await fixture.fetch('/_actions/ping', {
659+
method: 'POST',
660+
headers: {
661+
'Content-Type': 'application/json',
662+
},
663+
body: '{}',
664+
});
665+
666+
assert.equal(res.ok, true);
667+
assert.equal(res.headers.get('Content-Type'), 'application/json+devalue');
668+
669+
const data = devalue.parse(await res.text());
670+
assert.equal(data.ok, true);
671+
});
672+
});
673+
639674
it('Base path should be used', async () => {
640675
const fixture = await loadFixture({
641676
root: './fixtures/actions/',
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { defineConfig } from 'astro/config';
2+
3+
export default defineConfig({});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { defineAction } from 'astro:actions';
2+
3+
export const server = {
4+
ping: defineAction({
5+
handler: async () => {
6+
return { ok: true };
7+
},
8+
}),
9+
};
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
export const prerender = false;
3+
---
4+
5+
<p>Live page</p>

0 commit comments

Comments
 (0)