Skip to content

Commit

Permalink
Fix Server Islands in Vercel (#11491)
Browse files Browse the repository at this point in the history
* Fix Server Islands in Vercel

* Add a changeset

* Get server islands pattern from the segments

* Move getPattern so it can be used at runtime

* Fix build
  • Loading branch information
matthewp authored Jul 18, 2024
1 parent 1a26c6d commit fe3afeb
Show file tree
Hide file tree
Showing 15 changed files with 170 additions and 74 deletions.
7 changes: 7 additions & 0 deletions .changeset/tidy-shrimps-grab.md
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 2 additions & 1 deletion packages/astro/src/container/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
6 changes: 5 additions & 1 deletion packages/astro/src/core/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -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,
});
Expand Down
8 changes: 4 additions & 4 deletions packages/astro/src/core/routing/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
56 changes: 2 additions & 54 deletions packages/astro/src/core/routing/manifest/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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.
Expand Down
57 changes: 57 additions & 0 deletions packages/astro/src/core/routing/manifest/pattern.ts
Original file line number Diff line number Diff line change
@@ -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 '\\/?$';
}
30 changes: 19 additions & 11 deletions packages/astro/src/core/server-islands/endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
4 changes: 2 additions & 2 deletions packages/astro/src/vite-plugin-astro-server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand All @@ -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);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "@test/vercel-server-islands",
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/vercel": "workspace:*",
"astro": "workspace:*"
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>I'm an island</h1>
Original file line number Diff line number Diff line change
@@ -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>
29 changes: 29 additions & 0 deletions packages/integrations/vercel/test/server-islands.test.js
Original file line number Diff line number Diff line change
@@ -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');
});
});
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit fe3afeb

Please sign in to comment.