mirror of
https://github.com/withastro/astro.git
synced 2025-03-24 23:21:57 -05:00
Fix Server Islands in Vercel (#11491)
* 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
This commit is contained in:
parent
1a26c6dfe5
commit
fe3afebd65
15 changed files with 170 additions and 74 deletions
7
.changeset/tidy-shrimps-grab.md
Normal file
7
.changeset/tidy-shrimps-grab.md
Normal file
|
@ -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.
|
|
@ -20,7 +20,8 @@ import { Logger } from '../core/logger/core.js';
|
||||||
import { nodeLogDestination } from '../core/logger/node.js';
|
import { nodeLogDestination } from '../core/logger/node.js';
|
||||||
import { removeLeadingForwardSlash } from '../core/path.js';
|
import { removeLeadingForwardSlash } from '../core/path.js';
|
||||||
import { RenderContext } from '../core/render-context.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 type { AstroComponentFactory } from '../runtime/server/index.js';
|
||||||
import { ContainerPipeline } from './pipeline.js';
|
import { ContainerPipeline } from './pipeline.js';
|
||||||
|
|
||||||
|
|
|
@ -88,7 +88,7 @@ export class App {
|
||||||
|
|
||||||
constructor(manifest: SSRManifest, streaming = true) {
|
constructor(manifest: SSRManifest, streaming = true) {
|
||||||
this.#manifest = manifest;
|
this.#manifest = manifest;
|
||||||
this.#manifestData = injectDefaultRoutes({
|
this.#manifestData = injectDefaultRoutes(manifest, {
|
||||||
routes: manifest.routes.map((route) => route.routeData),
|
routes: manifest.routes.map((route) => route.routeData),
|
||||||
});
|
});
|
||||||
this.#baseWithoutTrailingSlash = removeTrailingForwardSlash(this.#manifest.base);
|
this.#baseWithoutTrailingSlash = removeTrailingForwardSlash(this.#manifest.base);
|
||||||
|
|
|
@ -32,6 +32,7 @@ import { collectPagesData } from './page-data.js';
|
||||||
import { staticBuild, viteBuild } from './static-build.js';
|
import { staticBuild, viteBuild } from './static-build.js';
|
||||||
import type { StaticBuildOptions } from './types.js';
|
import type { StaticBuildOptions } from './types.js';
|
||||||
import { getTimeStat } from './util.js';
|
import { getTimeStat } from './util.js';
|
||||||
|
import { getServerIslandRouteData } from '../server-islands/endpoint.js';
|
||||||
|
|
||||||
export interface BuildOptions {
|
export interface BuildOptions {
|
||||||
/**
|
/**
|
||||||
|
@ -216,7 +217,10 @@ class AstroBuilder {
|
||||||
pages: pageNames,
|
pages: pageNames,
|
||||||
routes: Object.values(allPages)
|
routes: Object.values(allPages)
|
||||||
.flat()
|
.flat()
|
||||||
.map((pageData) => pageData.route),
|
.map((pageData) => pageData.route).concat(
|
||||||
|
this.settings.config.experimental.serverIslands ?
|
||||||
|
[ getServerIslandRouteData(this.settings.config) ] : []
|
||||||
|
),
|
||||||
logging: this.logger,
|
logging: this.logger,
|
||||||
cacheManifest: internals.cacheManifestUsed,
|
cacheManifest: internals.cacheManifestUsed,
|
||||||
});
|
});
|
||||||
|
|
|
@ -12,10 +12,10 @@ import {
|
||||||
ensure404Route,
|
ensure404Route,
|
||||||
} from './astro-designed-error-pages.js';
|
} from './astro-designed-error-pages.js';
|
||||||
|
|
||||||
export function injectDefaultRoutes(manifest: ManifestData) {
|
export function injectDefaultRoutes(ssrManifest: SSRManifest, routeManifest: ManifestData) {
|
||||||
ensure404Route(manifest);
|
ensure404Route(routeManifest);
|
||||||
ensureServerIslandRoute(manifest);
|
ensureServerIslandRoute(ssrManifest, routeManifest);
|
||||||
return manifest;
|
return routeManifest;
|
||||||
}
|
}
|
||||||
|
|
||||||
type DefaultRouteParams = {
|
type DefaultRouteParams = {
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { removeLeadingForwardSlash, slash } from '../../path.js';
|
||||||
import { resolvePages } from '../../util.js';
|
import { resolvePages } from '../../util.js';
|
||||||
import { routeComparator } from '../priority.js';
|
import { routeComparator } from '../priority.js';
|
||||||
import { getRouteGenerator } from './generator.js';
|
import { getRouteGenerator } from './generator.js';
|
||||||
|
import { getPattern } from './pattern.js';
|
||||||
const require = createRequire(import.meta.url);
|
const require = createRequire(import.meta.url);
|
||||||
|
|
||||||
interface Item {
|
interface Item {
|
||||||
|
@ -70,59 +71,6 @@ export function getParts(part: string, file: string) {
|
||||||
return result;
|
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 = '') {
|
export function validateSegment(segment: string, file = '') {
|
||||||
if (!file) file = segment;
|
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]`
|
* For example, `/foo/[bar]` and `/foo/[baz]` or `/foo/[...bar]` and `/foo/[...baz]`
|
||||||
* but not `/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 (a.type === 'fallback' || b.type === 'fallback') {
|
||||||
// If either route is a fallback route, they don't collide.
|
// If either route is a fallback route, they don't collide.
|
||||||
// Fallbacks are always added below other routes exactly to avoid collisions.
|
// Fallbacks are always added below other routes exactly to avoid collisions.
|
||||||
|
|
57
packages/astro/src/core/routing/manifest/pattern.ts
Normal file
57
packages/astro/src/core/routing/manifest/pattern.ts
Normal file
|
@ -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 '\\/?$';
|
||||||
|
}
|
|
@ -11,33 +11,41 @@ import {
|
||||||
renderTemplate,
|
renderTemplate,
|
||||||
} from '../../runtime/server/index.js';
|
} from '../../runtime/server/index.js';
|
||||||
import { createSlotValueFromString } from '../../runtime/server/render/slot.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_ROUTE = '/_server-islands/[name]';
|
||||||
export const SERVER_ISLAND_COMPONENT = '_server-islands.astro';
|
export const SERVER_ISLAND_COMPONENT = '_server-islands.astro';
|
||||||
|
|
||||||
export function ensureServerIslandRoute(manifest: ManifestData) {
|
type ConfigFields = Pick<SSRManifest, 'base' | 'trailingSlash'>;
|
||||||
if (manifest.routes.some((route) => route.route === '/_server-islands/[name]')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
export function getServerIslandRouteData(config: ConfigFields) {
|
||||||
|
const segments = [
|
||||||
|
[{ content: '_server-islands', dynamic: false, spread: false }],
|
||||||
|
[{ content: 'name', dynamic: true, spread: false }],
|
||||||
|
];
|
||||||
const route: RouteData = {
|
const route: RouteData = {
|
||||||
type: 'page',
|
type: 'page',
|
||||||
component: SERVER_ISLAND_COMPONENT,
|
component: SERVER_ISLAND_COMPONENT,
|
||||||
generate: () => '',
|
generate: () => '',
|
||||||
params: ['name'],
|
params: ['name'],
|
||||||
segments: [
|
segments,
|
||||||
[{ content: '_server-islands', dynamic: false, spread: false }],
|
pattern: getPattern(segments, config.base, config.trailingSlash),
|
||||||
[{ content: 'name', dynamic: true, spread: false }],
|
|
||||||
],
|
|
||||||
// eslint-disable-next-line
|
|
||||||
pattern: /^\/_server-islands\/([^/]+?)$/,
|
|
||||||
prerender: false,
|
prerender: false,
|
||||||
isIndex: false,
|
isIndex: false,
|
||||||
fallbackRoutes: [],
|
fallbackRoutes: [],
|
||||||
route: SERVER_ISLAND_ROUTE,
|
route: SERVER_ISLAND_ROUTE,
|
||||||
};
|
};
|
||||||
|
return route;
|
||||||
|
}
|
||||||
|
|
||||||
manifest.routes.push(route);
|
|
||||||
|
|
||||||
|
export function ensureServerIslandRoute(config: ConfigFields, routeManifest: ManifestData) {
|
||||||
|
if (routeManifest.routes.some((route) => route.route === '/_server-islands/[name]')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
routeManifest.routes.push(getServerIslandRouteData(config));
|
||||||
}
|
}
|
||||||
|
|
||||||
type RenderOptions = {
|
type RenderOptions = {
|
||||||
|
|
|
@ -35,7 +35,7 @@ export default function createVitePluginAstroServer({
|
||||||
configureServer(viteServer) {
|
configureServer(viteServer) {
|
||||||
const loader = createViteLoader(viteServer);
|
const loader = createViteLoader(viteServer);
|
||||||
const manifest = createDevelopmentManifest(settings);
|
const manifest = createDevelopmentManifest(settings);
|
||||||
let manifestData: ManifestData = injectDefaultRoutes(
|
let manifestData: ManifestData = injectDefaultRoutes(manifest,
|
||||||
createRouteManifest({ settings, fsMod }, logger)
|
createRouteManifest({ settings, fsMod }, logger)
|
||||||
);
|
);
|
||||||
const pipeline = DevPipeline.create(manifestData, { loader, logger, manifest, settings });
|
const pipeline = DevPipeline.create(manifestData, { loader, logger, manifest, settings });
|
||||||
|
@ -46,7 +46,7 @@ export default function createVitePluginAstroServer({
|
||||||
function rebuildManifest(needsManifestRebuild: boolean) {
|
function rebuildManifest(needsManifestRebuild: boolean) {
|
||||||
pipeline.clearRouteCache();
|
pipeline.clearRouteCache();
|
||||||
if (needsManifestRebuild) {
|
if (needsManifestRebuild) {
|
||||||
manifestData = injectDefaultRoutes(createRouteManifest({ settings }, logger));
|
manifestData = injectDefaultRoutes(manifest, createRouteManifest({ settings }, logger));
|
||||||
pipeline.setManifestData(manifestData);
|
pipeline.setManifestData(manifestData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
10
packages/integrations/vercel/test/fixtures/server-islands/astro.config.mjs
vendored
Normal file
10
packages/integrations/vercel/test/fixtures/server-islands/astro.config.mjs
vendored
Normal file
|
@ -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,
|
||||||
|
}
|
||||||
|
});
|
10
packages/integrations/vercel/test/fixtures/server-islands/package.json
vendored
Normal file
10
packages/integrations/vercel/test/fixtures/server-islands/package.json
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"name": "@test/vercel-server-islands",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/vercel": "workspace:*",
|
||||||
|
"astro": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
1
packages/integrations/vercel/test/fixtures/server-islands/src/components/Island.astro
vendored
Normal file
1
packages/integrations/vercel/test/fixtures/server-islands/src/components/Island.astro
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<h1>I'm an island</h1>
|
12
packages/integrations/vercel/test/fixtures/server-islands/src/pages/index.astro
vendored
Normal file
12
packages/integrations/vercel/test/fixtures/server-islands/src/pages/index.astro
vendored
Normal file
|
@ -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
packages/integrations/vercel/test/server-islands.test.js
Normal file
29
packages/integrations/vercel/test/server-islands.test.js
Normal file
|
@ -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
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
|
@ -5542,6 +5542,15 @@ importers:
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../../../../astro
|
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:
|
packages/integrations/vercel/test/fixtures/serverless-prerender:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@astrojs/vercel':
|
'@astrojs/vercel':
|
||||||
|
|
Loading…
Add table
Reference in a new issue