aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/tidy-shrimps-grab.md7
-rw-r--r--packages/astro/src/container/index.ts3
-rw-r--r--packages/astro/src/core/app/index.ts2
-rw-r--r--packages/astro/src/core/build/index.ts6
-rw-r--r--packages/astro/src/core/routing/default.ts8
-rw-r--r--packages/astro/src/core/routing/manifest/create.ts56
-rw-r--r--packages/astro/src/core/routing/manifest/pattern.ts57
-rw-r--r--packages/astro/src/core/server-islands/endpoint.ts30
-rw-r--r--packages/astro/src/vite-plugin-astro-server/plugin.ts4
-rw-r--r--packages/integrations/vercel/test/fixtures/server-islands/astro.config.mjs10
-rw-r--r--packages/integrations/vercel/test/fixtures/server-islands/package.json10
-rw-r--r--packages/integrations/vercel/test/fixtures/server-islands/src/components/Island.astro1
-rw-r--r--packages/integrations/vercel/test/fixtures/server-islands/src/pages/index.astro12
-rw-r--r--packages/integrations/vercel/test/server-islands.test.js29
-rw-r--r--pnpm-lock.yaml9
15 files changed, 170 insertions, 74 deletions
diff --git a/.changeset/tidy-shrimps-grab.md b/.changeset/tidy-shrimps-grab.md
new file mode 100644
index 000000000..55e52375e
--- /dev/null
+++ b/.changeset/tidy-shrimps-grab.md
@@ -0,0 +1,7 @@
+---
+'astro': patch
+---
+
+Fix for Server Islands in Vercel adapter
+
+Vercel, and probably other adapters only allow pre-defined routes. This makes it so that the `astro:build:done` hook includes the `_server-islands/` route as part of the route data, which is used to configure available routes.
diff --git a/packages/astro/src/container/index.ts b/packages/astro/src/container/index.ts
index 758d65505..a8641b5ea 100644
--- a/packages/astro/src/container/index.ts
+++ b/packages/astro/src/container/index.ts
@@ -20,7 +20,8 @@ import { Logger } from '../core/logger/core.js';
import { nodeLogDestination } from '../core/logger/node.js';
import { removeLeadingForwardSlash } from '../core/path.js';
import { RenderContext } from '../core/render-context.js';
-import { getParts, getPattern, validateSegment } from '../core/routing/manifest/create.js';
+import { getParts, validateSegment } from '../core/routing/manifest/create.js';
+import { getPattern } from '../core/routing/manifest/pattern.js';
import type { AstroComponentFactory } from '../runtime/server/index.js';
import { ContainerPipeline } from './pipeline.js';
diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts
index 04a7d90a8..a2b1d4f2e 100644
--- a/packages/astro/src/core/app/index.ts
+++ b/packages/astro/src/core/app/index.ts
@@ -88,7 +88,7 @@ export class App {
constructor(manifest: SSRManifest, streaming = true) {
this.#manifest = manifest;
- this.#manifestData = injectDefaultRoutes({
+ this.#manifestData = injectDefaultRoutes(manifest, {
routes: manifest.routes.map((route) => route.routeData),
});
this.#baseWithoutTrailingSlash = removeTrailingForwardSlash(this.#manifest.base);
diff --git a/packages/astro/src/core/build/index.ts b/packages/astro/src/core/build/index.ts
index 7933b77f9..23e7b7835 100644
--- a/packages/astro/src/core/build/index.ts
+++ b/packages/astro/src/core/build/index.ts
@@ -32,6 +32,7 @@ import { collectPagesData } from './page-data.js';
import { staticBuild, viteBuild } from './static-build.js';
import type { StaticBuildOptions } from './types.js';
import { getTimeStat } from './util.js';
+import { getServerIslandRouteData } from '../server-islands/endpoint.js';
export interface BuildOptions {
/**
@@ -216,7 +217,10 @@ class AstroBuilder {
pages: pageNames,
routes: Object.values(allPages)
.flat()
- .map((pageData) => pageData.route),
+ .map((pageData) => pageData.route).concat(
+ this.settings.config.experimental.serverIslands ?
+ [ getServerIslandRouteData(this.settings.config) ] : []
+ ),
logging: this.logger,
cacheManifest: internals.cacheManifestUsed,
});
diff --git a/packages/astro/src/core/routing/default.ts b/packages/astro/src/core/routing/default.ts
index f617bd0dc..dd3c8cc53 100644
--- a/packages/astro/src/core/routing/default.ts
+++ b/packages/astro/src/core/routing/default.ts
@@ -12,10 +12,10 @@ import {
ensure404Route,
} from './astro-designed-error-pages.js';
-export function injectDefaultRoutes(manifest: ManifestData) {
- ensure404Route(manifest);
- ensureServerIslandRoute(manifest);
- return manifest;
+export function injectDefaultRoutes(ssrManifest: SSRManifest, routeManifest: ManifestData) {
+ ensure404Route(routeManifest);
+ ensureServerIslandRoute(ssrManifest, routeManifest);
+ return routeManifest;
}
type DefaultRouteParams = {
diff --git a/packages/astro/src/core/routing/manifest/create.ts b/packages/astro/src/core/routing/manifest/create.ts
index 4a36c8536..b022c383b 100644
--- a/packages/astro/src/core/routing/manifest/create.ts
+++ b/packages/astro/src/core/routing/manifest/create.ts
@@ -22,6 +22,7 @@ import { removeLeadingForwardSlash, slash } from '../../path.js';
import { resolvePages } from '../../util.js';
import { routeComparator } from '../priority.js';
import { getRouteGenerator } from './generator.js';
+import { getPattern } from './pattern.js';
const require = createRequire(import.meta.url);
interface Item {
@@ -70,59 +71,6 @@ export function getParts(part: string, file: string) {
return result;
}
-export function getPattern(
- segments: RoutePart[][],
- base: AstroConfig['base'],
- addTrailingSlash: AstroConfig['trailingSlash']
-) {
- const pathname = segments
- .map((segment) => {
- if (segment.length === 1 && segment[0].spread) {
- return '(?:\\/(.*?))?';
- } else {
- return (
- '\\/' +
- segment
- .map((part) => {
- if (part.spread) {
- return '(.*?)';
- } else if (part.dynamic) {
- return '([^/]+?)';
- } else {
- return part.content
- .normalize()
- .replace(/\?/g, '%3F')
- .replace(/#/g, '%23')
- .replace(/%5B/g, '[')
- .replace(/%5D/g, ']')
- .replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
- }
- })
- .join('')
- );
- }
- })
- .join('');
-
- const trailing =
- addTrailingSlash && segments.length ? getTrailingSlashPattern(addTrailingSlash) : '$';
- let initial = '\\/';
- if (addTrailingSlash === 'never' && base !== '/') {
- initial = '';
- }
- return new RegExp(`^${pathname || initial}${trailing}`);
-}
-
-function getTrailingSlashPattern(addTrailingSlash: AstroConfig['trailingSlash']): string {
- if (addTrailingSlash === 'always') {
- return '\\/$';
- }
- if (addTrailingSlash === 'never') {
- return '$';
- }
- return '\\/?$';
-}
-
export function validateSegment(segment: string, file = '') {
if (!file) file = segment;
@@ -486,7 +434,7 @@ function isStaticSegment(segment: RoutePart[]) {
* For example, `/foo/[bar]` and `/foo/[baz]` or `/foo/[...bar]` and `/foo/[...baz]`
* but not `/foo/[bar]` and `/foo/[...baz]`.
*/
-function detectRouteCollision(a: RouteData, b: RouteData, config: AstroConfig, logger: Logger) {
+function detectRouteCollision(a: RouteData, b: RouteData, _config: AstroConfig, logger: Logger) {
if (a.type === 'fallback' || b.type === 'fallback') {
// If either route is a fallback route, they don't collide.
// Fallbacks are always added below other routes exactly to avoid collisions.
diff --git a/packages/astro/src/core/routing/manifest/pattern.ts b/packages/astro/src/core/routing/manifest/pattern.ts
new file mode 100644
index 000000000..320d02e20
--- /dev/null
+++ b/packages/astro/src/core/routing/manifest/pattern.ts
@@ -0,0 +1,57 @@
+import type {
+ AstroConfig,
+ RoutePart,
+} from '../../../@types/astro.js';
+
+export function getPattern(
+ segments: RoutePart[][],
+ base: AstroConfig['base'],
+ addTrailingSlash: AstroConfig['trailingSlash']
+) {
+ const pathname = segments
+ .map((segment) => {
+ if (segment.length === 1 && segment[0].spread) {
+ return '(?:\\/(.*?))?';
+ } else {
+ return (
+ '\\/' +
+ segment
+ .map((part) => {
+ if (part.spread) {
+ return '(.*?)';
+ } else if (part.dynamic) {
+ return '([^/]+?)';
+ } else {
+ return part.content
+ .normalize()
+ .replace(/\?/g, '%3F')
+ .replace(/#/g, '%23')
+ .replace(/%5B/g, '[')
+ .replace(/%5D/g, ']')
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ }
+ })
+ .join('')
+ );
+ }
+ })
+ .join('');
+
+ const trailing =
+ addTrailingSlash && segments.length ? getTrailingSlashPattern(addTrailingSlash) : '$';
+ let initial = '\\/';
+ if (addTrailingSlash === 'never' && base !== '/') {
+ initial = '';
+ }
+ return new RegExp(`^${pathname || initial}${trailing}`);
+}
+
+function getTrailingSlashPattern(addTrailingSlash: AstroConfig['trailingSlash']): string {
+ if (addTrailingSlash === 'always') {
+ return '\\/$';
+ }
+ if (addTrailingSlash === 'never') {
+ return '$';
+ }
+ return '\\/?$';
+}
diff --git a/packages/astro/src/core/server-islands/endpoint.ts b/packages/astro/src/core/server-islands/endpoint.ts
index 7b6857e1a..1e01d0828 100644
--- a/packages/astro/src/core/server-islands/endpoint.ts
+++ b/packages/astro/src/core/server-islands/endpoint.ts
@@ -11,33 +11,41 @@ import {
renderTemplate,
} from '../../runtime/server/index.js';
import { createSlotValueFromString } from '../../runtime/server/render/slot.js';
+import { getPattern } from '../routing/manifest/pattern.js';
export const SERVER_ISLAND_ROUTE = '/_server-islands/[name]';
export const SERVER_ISLAND_COMPONENT = '_server-islands.astro';
-export function ensureServerIslandRoute(manifest: ManifestData) {
- if (manifest.routes.some((route) => route.route === '/_server-islands/[name]')) {
- return;
- }
+type ConfigFields = Pick<SSRManifest, 'base' | 'trailingSlash'>;
+export function getServerIslandRouteData(config: ConfigFields) {
+ const segments = [
+ [{ content: '_server-islands', dynamic: false, spread: false }],
+ [{ content: 'name', dynamic: true, spread: false }],
+ ];
const route: RouteData = {
type: 'page',
component: SERVER_ISLAND_COMPONENT,
generate: () => '',
params: ['name'],
- segments: [
- [{ content: '_server-islands', dynamic: false, spread: false }],
- [{ content: 'name', dynamic: true, spread: false }],
- ],
- // eslint-disable-next-line
- pattern: /^\/_server-islands\/([^/]+?)$/,
+ segments,
+ pattern: getPattern(segments, config.base, config.trailingSlash),
prerender: false,
isIndex: false,
fallbackRoutes: [],
route: SERVER_ISLAND_ROUTE,
};
+ return route;
+}
+
+
+
+export function ensureServerIslandRoute(config: ConfigFields, routeManifest: ManifestData) {
+ if (routeManifest.routes.some((route) => route.route === '/_server-islands/[name]')) {
+ return;
+ }
- manifest.routes.push(route);
+ routeManifest.routes.push(getServerIslandRouteData(config));
}
type RenderOptions = {
diff --git a/packages/astro/src/vite-plugin-astro-server/plugin.ts b/packages/astro/src/vite-plugin-astro-server/plugin.ts
index f9d0073bb..56cba80ec 100644
--- a/packages/astro/src/vite-plugin-astro-server/plugin.ts
+++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts
@@ -35,7 +35,7 @@ export default function createVitePluginAstroServer({
configureServer(viteServer) {
const loader = createViteLoader(viteServer);
const manifest = createDevelopmentManifest(settings);
- let manifestData: ManifestData = injectDefaultRoutes(
+ let manifestData: ManifestData = injectDefaultRoutes(manifest,
createRouteManifest({ settings, fsMod }, logger)
);
const pipeline = DevPipeline.create(manifestData, { loader, logger, manifest, settings });
@@ -46,7 +46,7 @@ export default function createVitePluginAstroServer({
function rebuildManifest(needsManifestRebuild: boolean) {
pipeline.clearRouteCache();
if (needsManifestRebuild) {
- manifestData = injectDefaultRoutes(createRouteManifest({ settings }, logger));
+ manifestData = injectDefaultRoutes(manifest, createRouteManifest({ settings }, logger));
pipeline.setManifestData(manifestData);
}
}
diff --git a/packages/integrations/vercel/test/fixtures/server-islands/astro.config.mjs b/packages/integrations/vercel/test/fixtures/server-islands/astro.config.mjs
new file mode 100644
index 000000000..534197429
--- /dev/null
+++ b/packages/integrations/vercel/test/fixtures/server-islands/astro.config.mjs
@@ -0,0 +1,10 @@
+import vercel from '@astrojs/vercel/serverless';
+import { defineConfig } from 'astro/config';
+
+export default defineConfig({
+ output: "server",
+ adapter: vercel(),
+ experimental: {
+ serverIslands: true,
+ }
+});
diff --git a/packages/integrations/vercel/test/fixtures/server-islands/package.json b/packages/integrations/vercel/test/fixtures/server-islands/package.json
new file mode 100644
index 000000000..a21ff176a
--- /dev/null
+++ b/packages/integrations/vercel/test/fixtures/server-islands/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@test/vercel-server-islands",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@astrojs/vercel": "workspace:*",
+ "astro": "workspace:*"
+ }
+}
+
diff --git a/packages/integrations/vercel/test/fixtures/server-islands/src/components/Island.astro b/packages/integrations/vercel/test/fixtures/server-islands/src/components/Island.astro
new file mode 100644
index 000000000..9d2832bc1
--- /dev/null
+++ b/packages/integrations/vercel/test/fixtures/server-islands/src/components/Island.astro
@@ -0,0 +1 @@
+<h1>I'm an island</h1>
diff --git a/packages/integrations/vercel/test/fixtures/server-islands/src/pages/index.astro b/packages/integrations/vercel/test/fixtures/server-islands/src/pages/index.astro
new file mode 100644
index 000000000..835126c2b
--- /dev/null
+++ b/packages/integrations/vercel/test/fixtures/server-islands/src/pages/index.astro
@@ -0,0 +1,12 @@
+---
+import Island from '../components/Island.astro';
+---
+<html>
+ <head>
+ <title>One</title>
+ </head>
+ <body>
+ <h1>One</h1>
+ <Island server:defer />
+ </body>
+</html>
diff --git a/packages/integrations/vercel/test/server-islands.test.js b/packages/integrations/vercel/test/server-islands.test.js
new file mode 100644
index 000000000..060492584
--- /dev/null
+++ b/packages/integrations/vercel/test/server-islands.test.js
@@ -0,0 +1,29 @@
+import assert from 'node:assert/strict';
+import { before, describe, it } from 'node:test';
+import { loadFixture } from './test-utils.js';
+
+describe('Server Islands', () => {
+ /** @type {import('./test-utils.js').Fixture} */
+ let fixture;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: './fixtures/server-islands/',
+ });
+ await fixture.build();
+ });
+
+ it('server islands route is in the config', async () => {
+ const config = JSON.parse(
+ await fixture.readFile('../.vercel/output/config.json')
+ );
+ let found = null;
+ for(let route of config.routes) {
+ if(route.src?.includes('_server-islands')) {
+ found = route;
+ break;
+ }
+ }
+ assert.notEqual(found, null, 'Default server islands route included');
+ });
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b73390f55..33056a56e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -5542,6 +5542,15 @@ importers:
specifier: workspace:*
version: link:../../../../../astro
+ packages/integrations/vercel/test/fixtures/server-islands:
+ dependencies:
+ '@astrojs/vercel':
+ specifier: workspace:*
+ version: link:../../..
+ astro:
+ specifier: workspace:*
+ version: link:../../../../../astro
+
packages/integrations/vercel/test/fixtures/serverless-prerender:
dependencies:
'@astrojs/vercel':