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 { 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';
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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.
|
||||
|
|
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,
|
||||
} 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;
|
||||
}
|
||||
|
||||
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 = {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
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:*
|
||||
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':
|
||||
|
|
Loading…
Add table
Reference in a new issue