From fb6850ee41be12d0b1c0322ee1d710e470fb3969 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Wed, 1 Apr 2026 08:54:55 -0400 Subject: [PATCH] Include injected routes when determining whether renderers are needed in SSR builds --- .changeset/ssr-renderers-injected-routes.md | 5 ++++ packages/astro/src/actions/integration.ts | 4 +-- packages/astro/src/core/routing/helpers.ts | 16 ++++++----- .../astro/src/vite-plugin-renderers/index.ts | 5 ++-- .../units/routing/routing-helpers.test.js | 28 +++++++++++++++++++ 5 files changed, 47 insertions(+), 11 deletions(-) create mode 100644 .changeset/ssr-renderers-injected-routes.md create mode 100644 packages/astro/test/units/routing/routing-helpers.test.js diff --git a/.changeset/ssr-renderers-injected-routes.md b/.changeset/ssr-renderers-injected-routes.md new file mode 100644 index 000000000000..9f3dfe8f2378 --- /dev/null +++ b/.changeset/ssr-renderers-injected-routes.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes SSR builds failing with "No matching renderer found" when a project only has injected routes and no `src/pages/` directory diff --git a/packages/astro/src/actions/integration.ts b/packages/astro/src/actions/integration.ts index 0a5f155698f3..e8a5d5a002c2 100644 --- a/packages/astro/src/actions/integration.ts +++ b/packages/astro/src/actions/integration.ts @@ -1,6 +1,6 @@ import { AstroError } from '../core/errors/errors.js'; import { ActionsWithoutServerOutputError } from '../core/errors/errors-data.js'; -import { hasNonPrerenderedProjectRoute } from '../core/routing/helpers.js'; +import { hasNonPrerenderedRoute } from '../core/routing/helpers.js'; import { viteID } from '../core/util.js'; import type { AstroSettings } from '../types/astro.js'; import type { AstroIntegration } from '../types/public/integrations.js'; @@ -41,7 +41,7 @@ export default function astroIntegrationActionsRouteHandler({ }); }, 'astro:routes:resolved': ({ routes }) => { - if (!hasNonPrerenderedProjectRoute(routes)) { + if (!hasNonPrerenderedRoute(routes)) { const error = new AstroError(ActionsWithoutServerOutputError); error.stack = undefined; throw error; diff --git a/packages/astro/src/core/routing/helpers.ts b/packages/astro/src/core/routing/helpers.ts index 291daad45fa5..59388e10cfb5 100644 --- a/packages/astro/src/core/routing/helpers.ts +++ b/packages/astro/src/core/routing/helpers.ts @@ -75,26 +75,28 @@ export function routeHasHtmlExtension(route: RouteData): boolean { ); } -export function hasNonPrerenderedProjectRoute( +export function hasNonPrerenderedRoute( routes: Array>, - options?: { includeEndpoints?: boolean }, + options?: { includeEndpoints?: boolean; includeExternal?: boolean }, ): boolean; -export function hasNonPrerenderedProjectRoute( +export function hasNonPrerenderedRoute( routes: Array>, - options?: { includeEndpoints?: boolean }, + options?: { includeEndpoints?: boolean; includeExternal?: boolean }, ): boolean; -export function hasNonPrerenderedProjectRoute( +export function hasNonPrerenderedRoute( routes: Array< | Pick | Pick >, - options?: { includeEndpoints?: boolean }, + options?: { includeEndpoints?: boolean; includeExternal?: boolean }, ): boolean { const includeEndpoints = options?.includeEndpoints ?? true; + const includeExternal = options?.includeExternal ?? false; const routeTypes: ReadonlyArray = includeEndpoints ? ['page', 'endpoint'] : ['page']; + const origins: ReadonlyArray = includeExternal ? ['project', 'external'] : ['project']; return routes.some((route) => { const isPrerendered = 'isPrerendered' in route ? route.isPrerendered : route.prerender; - return routeTypes.includes(route.type) && route.origin === 'project' && !isPrerendered; + return routeTypes.includes(route.type) && origins.includes(route.origin) && !isPrerendered; }); } diff --git a/packages/astro/src/vite-plugin-renderers/index.ts b/packages/astro/src/vite-plugin-renderers/index.ts index dbd0bcb9f159..0c7677aa31e0 100644 --- a/packages/astro/src/vite-plugin-renderers/index.ts +++ b/packages/astro/src/vite-plugin-renderers/index.ts @@ -1,6 +1,6 @@ import type { ConfigEnv, Plugin as VitePlugin } from 'vite'; import { ASTRO_VITE_ENVIRONMENT_NAMES } from '../core/constants.js'; -import { hasNonPrerenderedProjectRoute } from '../core/routing/helpers.js'; +import { hasNonPrerenderedRoute } from '../core/routing/helpers.js'; import type { ServerIslandsState } from '../core/server-islands/shared-state.js'; import type { AstroSettings, RoutesList } from '../types/astro.js'; @@ -40,8 +40,9 @@ export default function vitePluginRenderers(options: PluginOptions): VitePlugin this.environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.ssr && renderers.length > 0 && !options.serverIslandsState.hasIslands() && - !hasNonPrerenderedProjectRoute(options.routesList.routes, { + !hasNonPrerenderedRoute(options.routesList.routes, { includeEndpoints: false, + includeExternal: true, }) ) { return { code: `export const renderers = [];` }; diff --git a/packages/astro/test/units/routing/routing-helpers.test.js b/packages/astro/test/units/routing/routing-helpers.test.js new file mode 100644 index 000000000000..8f01ae7abf4e --- /dev/null +++ b/packages/astro/test/units/routing/routing-helpers.test.js @@ -0,0 +1,28 @@ +// @ts-check +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { hasNonPrerenderedRoute } from '../../../dist/core/routing/helpers.js'; + +describe('hasNonPrerenderedRoute', () => { + it('returns true when a non-prerendered project page exists', () => { + const routes = [{ type: 'page', origin: 'project', prerender: false }]; + assert.equal(hasNonPrerenderedRoute(routes), true); + }); + + it('returns false when all project pages are prerendered', () => { + const routes = [{ type: 'page', origin: 'project', prerender: true }]; + assert.equal(hasNonPrerenderedRoute(routes), false); + }); + + it('excludes endpoints when includeEndpoints is false', () => { + const routes = [{ type: 'endpoint', origin: 'project', prerender: false }]; + assert.equal(hasNonPrerenderedRoute(routes, { includeEndpoints: false }), false); + assert.equal(hasNonPrerenderedRoute(routes, { includeEndpoints: true }), true); + }); + + it('returns true for injected (external) non-prerendered pages when includeExternal is true', () => { + const routes = [{ type: 'page', origin: 'external', prerender: false }]; + assert.equal(hasNonPrerenderedRoute(routes, { includeExternal: true }), true); + assert.equal(hasNonPrerenderedRoute(routes), false); + }); +});