mirror of
https://github.com/withastro/astro.git
synced 2025-03-17 23:11:29 -05:00
feat(vercel): ISR (#9714)
* feat(vercel): isr * bypass token * exclusion of certain paths * add test * remove search params in dev mode * Apply suggestions from code review Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com> * Apply suggestions from code review * Apply suggestions from code review * fix missing await * escape src for regex * cleanup * revalidate -> expiration * update type docs * always exclude /_image * add changeset * Apply suggestions from code review * always create serverless function for /_image * Apply suggestions from code review Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * Apply suggestions from code review --------- Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com> Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
parent
88628a4f9e
commit
e2fe51c828
10 changed files with 308 additions and 103 deletions
54
.changeset/flat-snakes-hammer.md
Normal file
54
.changeset/flat-snakes-hammer.md
Normal file
|
@ -0,0 +1,54 @@
|
|||
---
|
||||
"@astrojs/vercel": minor
|
||||
---
|
||||
|
||||
Introduces a new config option, `isr`, that allows you to deploy your project as an ISR function. [ISR (Incremental Static Regeneration)](https://vercel.com/docs/incremental-static-regeneration) caches your on-demand rendered pages in the same way as prerendered pages after first request.
|
||||
|
||||
To enable this feature, set `isr` to true in your Vercel adapter configuration in `astro.config.mjs`:
|
||||
|
||||
```js
|
||||
export default defineConfig({
|
||||
output: "server",
|
||||
adapter: vercel({ isr: true })
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
## Cache invalidation options
|
||||
|
||||
By default, ISR responses are cached for the duration of your deployment. You can further control caching by setting an `expiration` time or prevent caching entirely for certain routes.
|
||||
|
||||
### Time-based invalidation
|
||||
|
||||
You can change the length of time to cache routes this by configuring an `expiration` value in seconds:
|
||||
|
||||
```js
|
||||
export default defineConfig({
|
||||
output: "server",
|
||||
adapter: vercel({
|
||||
isr: {
|
||||
// caches all pages on first request and saves for 1 day
|
||||
expiration: 60 * 60 * 24
|
||||
}
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Manual invalidation
|
||||
|
||||
To implement Vercel's [Draft mode](https://vercel.com/docs/build-output-api/v3/features#draft-mode), or [On-Demand Incremental Static Regeneration (ISR)](https://vercel.com/docs/build-output-api/v3/features#on-demand-incremental-static-regeneration-isr), you can create a bypass token and provide it to the `isr` config along with the paths to exclude from caching:
|
||||
|
||||
```js
|
||||
export default defineConfig({
|
||||
output: "server",
|
||||
adapter: vercel({
|
||||
isr: {
|
||||
// A secret random string that you create.
|
||||
bypassToken: "005556d774a8",
|
||||
// Paths that will always be served fresh.
|
||||
exclude: [ "/api/invalidate" ]
|
||||
}
|
||||
})
|
||||
})
|
||||
```
|
||||
|
|
@ -76,6 +76,10 @@ function getRedirectStatus(route: RouteData): number {
|
|||
return 301;
|
||||
}
|
||||
|
||||
export function escapeRegex(content: string) {
|
||||
return `^${getMatchPattern([[{ content, dynamic: false, spread: false }]])}$`
|
||||
}
|
||||
|
||||
export function getRedirects(routes: RouteData[], config: AstroConfig): VercelRoute[] {
|
||||
let redirects: VercelRoute[] = [];
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
} from '../image/shared.js';
|
||||
import { removeDir, writeJson } from '../lib/fs.js';
|
||||
import { copyDependenciesToFunction } from '../lib/nft.js';
|
||||
import { getRedirects } from '../lib/redirects.js';
|
||||
import { escapeRegex, getRedirects } from '../lib/redirects.js';
|
||||
import {
|
||||
getSpeedInsightsViteConfig,
|
||||
type VercelSpeedInsightsConfig,
|
||||
|
@ -35,6 +35,7 @@ const PACKAGE_NAME = '@astrojs/vercel/serverless';
|
|||
* with the original path as the value of this header.
|
||||
*/
|
||||
export const ASTRO_PATH_HEADER = 'x-astro-path';
|
||||
export const ASTRO_PATH_PARAM = 'x_astro_path';
|
||||
|
||||
/**
|
||||
* The edge function calls the node server at /_render,
|
||||
|
@ -48,6 +49,11 @@ export const VERCEL_EDGE_MIDDLEWARE_FILE = 'vercel-edge-middleware';
|
|||
export const NODE_PATH = '_render';
|
||||
const MIDDLEWARE_PATH = '_middleware';
|
||||
|
||||
// This isn't documented by vercel anywhere, but unlike serverless
|
||||
// and edge functions, isr functions are not passed the original path.
|
||||
// Instead, we have to use $0 to refer to the regex match from "src".
|
||||
const ISR_PATH = `/_isr?${ASTRO_PATH_PARAM}=$0`;
|
||||
|
||||
// https://vercel.com/docs/concepts/functions/serverless-functions/runtimes/node-js#node.js-version
|
||||
const SUPPORTED_NODE_VERSIONS: Record<
|
||||
string,
|
||||
|
@ -123,6 +129,36 @@ export interface VercelServerlessConfig {
|
|||
|
||||
/** The maximum duration (in seconds) that Serverless Functions can run before timing out. See the [Vercel documentation](https://vercel.com/docs/functions/serverless-functions/runtimes#maxduration) for the default and maximum limit for your account plan. */
|
||||
maxDuration?: number;
|
||||
|
||||
/** Whether to cache on-demand rendered pages in the same way as static files. */
|
||||
isr?: boolean | VercelISRConfig;
|
||||
}
|
||||
|
||||
interface VercelISRConfig {
|
||||
/**
|
||||
* A secret random string that you create.
|
||||
* Its presence in the `__prerender_bypass` cookie will result in fresh responses being served, bypassing the cache. See Vercel’s documentation on [Draft Mode](https://vercel.com/docs/build-output-api/v3/features#draft-mode) for more information.
|
||||
* Its presence in the `x-prerender-revalidate` header will result in a fresh response which will then be cached for all future requests to be used. See Vercel’s documentation on [On-Demand Incremental Static Regeneration (ISR)](https://vercel.com/docs/build-output-api/v3/features#on-demand-incremental-static-regeneration-isr) for more information.
|
||||
*
|
||||
* @default `undefined`
|
||||
*/
|
||||
bypassToken?: string;
|
||||
|
||||
/**
|
||||
* Expiration time (in seconds) before the pages will be re-generated.
|
||||
*
|
||||
* Setting to `false` means that the page will stay cached as long as the current deployment is in production.
|
||||
*
|
||||
* @default `false`
|
||||
*/
|
||||
expiration?: number | false;
|
||||
|
||||
/**
|
||||
* Paths that will always be served by a serverless function instead of an ISR function.
|
||||
*
|
||||
* @default `[]`
|
||||
*/
|
||||
exclude?: string[];
|
||||
}
|
||||
|
||||
export default function vercelServerless({
|
||||
|
@ -136,6 +172,7 @@ export default function vercelServerless({
|
|||
functionPerRoute = false,
|
||||
edgeMiddleware = false,
|
||||
maxDuration,
|
||||
isr = false,
|
||||
}: VercelServerlessConfig = {}): AstroIntegration {
|
||||
if (maxDuration) {
|
||||
if (typeof maxDuration !== 'number') {
|
||||
|
@ -154,8 +191,6 @@ export default function vercelServerless({
|
|||
// Extra files to be merged with `includeFiles` during build
|
||||
const extraFilesToInclude: URL[] = [];
|
||||
|
||||
const NTF_CACHE = Object.create(null);
|
||||
|
||||
return {
|
||||
name: PACKAGE_NAME,
|
||||
hooks: {
|
||||
|
@ -225,6 +260,20 @@ export default function vercelServerless({
|
|||
);
|
||||
}
|
||||
},
|
||||
'astro:server:setup' ({ server }) {
|
||||
// isr functions do not have access to search params, this middleware removes them for the dev mode
|
||||
if (isr) {
|
||||
const exclude_ = typeof isr === "object" ? isr.exclude ?? [] : [];
|
||||
// we create a regex to emulate vercel's production behavior
|
||||
const exclude = exclude_.concat("/_image").map(ex => new RegExp(escapeRegex(ex)));
|
||||
server.middlewares.use(function removeIsrParams(req, _, next) {
|
||||
const { pathname } = new URL(`https://example.com${req.url}`);
|
||||
if (exclude.some(ex => ex.test(pathname))) return next();
|
||||
req.url = pathname;
|
||||
return next();
|
||||
})
|
||||
}
|
||||
},
|
||||
'astro:build:ssr': async ({ entryPoints, middlewareEntryPoint }) => {
|
||||
_entryPoints = entryPoints;
|
||||
_middlewareEntryPoint = middlewareEntryPoint;
|
||||
|
@ -257,7 +306,7 @@ export default function vercelServerless({
|
|||
.concat(extraFilesToInclude);
|
||||
const excludeFiles = _excludeFiles.map((file) => new URL(file, _config.root));
|
||||
|
||||
const runtime = getRuntime(process, logger);
|
||||
const builder = new VercelBuilder(_config, excludeFiles, includeFiles, logger, maxDuration);
|
||||
|
||||
// Multiple entrypoint support
|
||||
if (_entryPoints.size) {
|
||||
|
@ -273,45 +322,42 @@ export default function vercelServerless({
|
|||
? getRouteFuncName(route)
|
||||
: getFallbackFuncName(entryFile);
|
||||
|
||||
await createFunctionFolder({
|
||||
functionName: func,
|
||||
runtime,
|
||||
entry: entryFile,
|
||||
config: _config,
|
||||
logger,
|
||||
NTF_CACHE,
|
||||
includeFiles,
|
||||
excludeFiles,
|
||||
maxDuration,
|
||||
});
|
||||
await builder.buildServerlessFolder(entryFile, func);
|
||||
|
||||
routeDefinitions.push({
|
||||
src: route.pattern.source,
|
||||
dest: func,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await createFunctionFolder({
|
||||
functionName: NODE_PATH,
|
||||
runtime,
|
||||
entry: new URL(_serverEntry, _buildTempFolder),
|
||||
config: _config,
|
||||
logger,
|
||||
NTF_CACHE,
|
||||
includeFiles,
|
||||
excludeFiles,
|
||||
maxDuration,
|
||||
});
|
||||
const dest = _middlewareEntryPoint ? MIDDLEWARE_PATH : NODE_PATH;
|
||||
for (const route of routes) {
|
||||
if (!route.prerender) routeDefinitions.push({ src: route.pattern.source, dest });
|
||||
const entryFile = new URL(_serverEntry, _buildTempFolder)
|
||||
if (isr) {
|
||||
const isrConfig = typeof isr === "object" ? isr : {};
|
||||
await builder.buildServerlessFolder(entryFile, NODE_PATH);
|
||||
if (isrConfig.exclude?.length) {
|
||||
const dest = _middlewareEntryPoint ? MIDDLEWARE_PATH : NODE_PATH;
|
||||
for (const route of isrConfig.exclude) {
|
||||
// vercel interprets src as a regex pattern, so we need to escape it
|
||||
routeDefinitions.push({ src: escapeRegex(route), dest })
|
||||
}
|
||||
}
|
||||
await builder.buildISRFolder(entryFile, '_isr', isrConfig);
|
||||
for (const route of routes) {
|
||||
const src = route.pattern.source;
|
||||
const dest = src.startsWith("^\\/_image") ? NODE_PATH : ISR_PATH;
|
||||
if (!route.prerender) routeDefinitions.push({ src, dest });
|
||||
}
|
||||
}
|
||||
else {
|
||||
await builder.buildServerlessFolder(entryFile, NODE_PATH);
|
||||
const dest = _middlewareEntryPoint ? MIDDLEWARE_PATH : NODE_PATH;
|
||||
for (const route of routes) {
|
||||
if (!route.prerender) routeDefinitions.push({ src: route.pattern.source, dest });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (_middlewareEntryPoint) {
|
||||
await createMiddlewareFolder({
|
||||
functionName: MIDDLEWARE_PATH,
|
||||
entry: _middlewareEntryPoint,
|
||||
config: _config,
|
||||
});
|
||||
await builder.buildMiddlewareFolder(_middlewareEntryPoint, MIDDLEWARE_PATH);
|
||||
}
|
||||
const fourOhFourRoute = routes.find((route) => route.pathname === '/404');
|
||||
// Output configuration
|
||||
|
@ -366,80 +412,78 @@ export default function vercelServerless({
|
|||
|
||||
type Runtime = `nodejs${string}.x`;
|
||||
|
||||
interface CreateMiddlewareFolderArgs {
|
||||
config: AstroConfig;
|
||||
entry: URL;
|
||||
functionName: string;
|
||||
}
|
||||
class VercelBuilder {
|
||||
readonly NTF_CACHE = {}
|
||||
|
||||
async function createMiddlewareFolder({ functionName, entry, config }: CreateMiddlewareFolderArgs) {
|
||||
const functionFolder = new URL(`./functions/${functionName}.func/`, config.outDir);
|
||||
constructor(
|
||||
readonly config: AstroConfig,
|
||||
readonly excludeFiles: URL[],
|
||||
readonly includeFiles: URL[],
|
||||
readonly logger: AstroIntegrationLogger,
|
||||
readonly maxDuration?: number,
|
||||
readonly runtime = getRuntime(process, logger)
|
||||
) {}
|
||||
|
||||
await generateEdgeMiddleware(
|
||||
entry,
|
||||
new URL(VERCEL_EDGE_MIDDLEWARE_FILE, config.srcDir),
|
||||
new URL('./middleware.mjs', functionFolder)
|
||||
);
|
||||
async buildServerlessFolder(entry: URL, functionName: string) {
|
||||
const { config, includeFiles, excludeFiles, logger, NTF_CACHE, runtime, maxDuration } = this;
|
||||
// .vercel/output/functions/<name>.func/
|
||||
const functionFolder = new URL(`./functions/${functionName}.func/`, config.outDir);
|
||||
const packageJson = new URL(`./functions/${functionName}.func/package.json`, config.outDir);
|
||||
const vcConfig = new URL(`./functions/${functionName}.func/.vc-config.json`, config.outDir);
|
||||
|
||||
await writeJson(new URL(`./.vc-config.json`, functionFolder), {
|
||||
runtime: 'edge',
|
||||
entrypoint: 'middleware.mjs',
|
||||
});
|
||||
}
|
||||
// Copy necessary files (e.g. node_modules/)
|
||||
const { handler } = await copyDependenciesToFunction(
|
||||
{
|
||||
entry,
|
||||
outDir: functionFolder,
|
||||
includeFiles,
|
||||
excludeFiles,
|
||||
logger,
|
||||
},
|
||||
NTF_CACHE
|
||||
);
|
||||
|
||||
interface CreateFunctionFolderArgs {
|
||||
functionName: string;
|
||||
runtime: Runtime;
|
||||
entry: URL;
|
||||
config: AstroConfig;
|
||||
logger: AstroIntegrationLogger;
|
||||
NTF_CACHE: any;
|
||||
includeFiles: URL[];
|
||||
excludeFiles: URL[];
|
||||
maxDuration: number | undefined;
|
||||
}
|
||||
// Enable ESM
|
||||
// https://aws.amazon.com/blogs/compute/using-node-js-es-modules-and-top-level-await-in-aws-lambda/
|
||||
await writeJson(packageJson, { type: 'module' });
|
||||
|
||||
async function createFunctionFolder({
|
||||
functionName,
|
||||
runtime,
|
||||
entry,
|
||||
config,
|
||||
logger,
|
||||
NTF_CACHE,
|
||||
includeFiles,
|
||||
excludeFiles,
|
||||
maxDuration,
|
||||
}: CreateFunctionFolderArgs) {
|
||||
// .vercel/output/functions/<name>.func/
|
||||
const functionFolder = new URL(`./functions/${functionName}.func/`, config.outDir);
|
||||
const packageJson = new URL(`./functions/${functionName}.func/package.json`, config.outDir);
|
||||
const vcConfig = new URL(`./functions/${functionName}.func/.vc-config.json`, config.outDir);
|
||||
// Serverless function config
|
||||
// https://vercel.com/docs/build-output-api/v3#vercel-primitives/serverless-functions/configuration
|
||||
await writeJson(vcConfig, {
|
||||
runtime,
|
||||
handler: handler.replaceAll('\\', '/'),
|
||||
launcherType: 'Nodejs',
|
||||
maxDuration,
|
||||
supportsResponseStreaming: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Copy necessary files (e.g. node_modules/)
|
||||
const { handler } = await copyDependenciesToFunction(
|
||||
{
|
||||
async buildISRFolder(entry: URL, functionName: string, isr: VercelISRConfig) {
|
||||
await this.buildServerlessFolder(entry, functionName);
|
||||
const prerenderConfig = new URL(`./functions/${functionName}.prerender-config.json`, this.config.outDir)
|
||||
// https://vercel.com/docs/build-output-api/v3/primitives#prerender-configuration-file
|
||||
await writeJson(prerenderConfig, {
|
||||
expiration: isr.expiration ?? false,
|
||||
bypassToken: isr.bypassToken,
|
||||
allowQuery: [ASTRO_PATH_PARAM],
|
||||
passQuery: true
|
||||
});
|
||||
}
|
||||
|
||||
async buildMiddlewareFolder(entry: URL, functionName: string) {
|
||||
const functionFolder = new URL(`./functions/${functionName}.func/`, this.config.outDir);
|
||||
|
||||
await generateEdgeMiddleware(
|
||||
entry,
|
||||
outDir: functionFolder,
|
||||
includeFiles,
|
||||
excludeFiles,
|
||||
logger,
|
||||
},
|
||||
NTF_CACHE
|
||||
);
|
||||
|
||||
// Enable ESM
|
||||
// https://aws.amazon.com/blogs/compute/using-node-js-es-modules-and-top-level-await-in-aws-lambda/
|
||||
await writeJson(packageJson, { type: 'module' });
|
||||
|
||||
// Serverless function config
|
||||
// https://vercel.com/docs/build-output-api/v3#vercel-primitives/serverless-functions/configuration
|
||||
await writeJson(vcConfig, {
|
||||
runtime,
|
||||
handler: handler.replaceAll('\\', '/'),
|
||||
launcherType: 'Nodejs',
|
||||
maxDuration,
|
||||
supportsResponseStreaming: true,
|
||||
});
|
||||
new URL(VERCEL_EDGE_MIDDLEWARE_FILE, this.config.srcDir),
|
||||
new URL('./middleware.mjs', functionFolder)
|
||||
);
|
||||
|
||||
await writeJson(new URL(`./.vc-config.json`, functionFolder), {
|
||||
runtime: 'edge',
|
||||
entrypoint: 'middleware.mjs',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getRuntime(process: NodeJS.Process, logger: AstroIntegrationLogger): Runtime {
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
import type { SSRManifest } from 'astro';
|
||||
import { applyPolyfills, NodeApp } from 'astro/app/node';
|
||||
import type { IncomingMessage, ServerResponse } from 'node:http';
|
||||
import { ASTRO_PATH_HEADER, ASTRO_LOCALS_HEADER } from './adapter.js';
|
||||
import { ASTRO_PATH_HEADER, ASTRO_PATH_PARAM, ASTRO_LOCALS_HEADER } from './adapter.js';
|
||||
|
||||
applyPolyfills();
|
||||
|
||||
export const createExports = (manifest: SSRManifest) => {
|
||||
const app = new NodeApp(manifest);
|
||||
const handler = async (req: IncomingMessage, res: ServerResponse) => {
|
||||
const url = new URL(`https://example.com${req.url}`)
|
||||
const clientAddress = req.headers['x-forwarded-for'] as string | undefined;
|
||||
const localsHeader = req.headers[ASTRO_LOCALS_HEADER];
|
||||
const realPath = req.headers[ASTRO_PATH_HEADER];
|
||||
const realPath = req.headers[ASTRO_PATH_HEADER] ?? url.searchParams.get(ASTRO_PATH_PARAM);
|
||||
if (typeof realPath === 'string') {
|
||||
req.url = realPath;
|
||||
}
|
||||
|
@ -26,3 +27,7 @@ export const createExports = (manifest: SSRManifest) => {
|
|||
|
||||
return { default: handler };
|
||||
};
|
||||
|
||||
// HACK: prevent warning
|
||||
// @astrojs-ssr-virtual-entry (22:23) "start" is not exported by "dist/serverless/entrypoint.js", imported by "@astrojs-ssr-virtual-entry".
|
||||
export function start() {}
|
||||
|
|
13
packages/integrations/vercel/test/fixtures/isr/astro.config.mjs
vendored
Normal file
13
packages/integrations/vercel/test/fixtures/isr/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
import vercel from '@astrojs/vercel/serverless';
|
||||
|
||||
export default defineConfig({
|
||||
output: "server",
|
||||
adapter: vercel({
|
||||
isr: {
|
||||
bypassToken: "1c9e601d-9943-4e7c-9575-005556d774a8",
|
||||
expiration: 120,
|
||||
exclude: ["/two"]
|
||||
}
|
||||
})
|
||||
});
|
9
packages/integrations/vercel/test/fixtures/isr/package.json
vendored
Normal file
9
packages/integrations/vercel/test/fixtures/isr/package.json
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "@test/vercel-isr",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@astrojs/vercel": "workspace:*",
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
8
packages/integrations/vercel/test/fixtures/isr/src/pages/one.astro
vendored
Normal file
8
packages/integrations/vercel/test/fixtures/isr/src/pages/one.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>One</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>One</h1>
|
||||
</body>
|
||||
</html>
|
8
packages/integrations/vercel/test/fixtures/isr/src/pages/two.astro
vendored
Normal file
8
packages/integrations/vercel/test/fixtures/isr/src/pages/two.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Two</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Two</h1>
|
||||
</body>
|
||||
</html>
|
51
packages/integrations/vercel/test/isr.test.js
Normal file
51
packages/integrations/vercel/test/isr.test.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
import { loadFixture } from "./test-utils.js";
|
||||
import { expect } from "chai";
|
||||
|
||||
describe("ISR", () => {
|
||||
/** @type {import('./test-utils.js').Fixture} */
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: "./fixtures/isr/",
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
it("generates expected prerender config", async () => {
|
||||
const vcConfig = JSON.parse(
|
||||
await fixture.readFile("../.vercel/output/functions/_isr.prerender-config.json")
|
||||
);
|
||||
expect(vcConfig).to.deep.include({
|
||||
"expiration": 120,
|
||||
"bypassToken": "1c9e601d-9943-4e7c-9575-005556d774a8",
|
||||
"allowQuery": ["x_astro_path"],
|
||||
"passQuery": true
|
||||
})
|
||||
})
|
||||
|
||||
it("generates expected routes", async () => {
|
||||
const deploymentConfig = JSON.parse(
|
||||
await fixture.readFile("../.vercel/output/config.json")
|
||||
);
|
||||
// the first two are /_astro/*, and filesystem routes
|
||||
expect(deploymentConfig.routes.slice(2)).to.deep.equal([
|
||||
{
|
||||
"src": "^/two$",
|
||||
"dest": "_render"
|
||||
},
|
||||
{
|
||||
"src": "^\\/_image$",
|
||||
"dest": "_render"
|
||||
},
|
||||
{
|
||||
"src": "^\\/one\\/?$",
|
||||
"dest": "/_isr?x_astro_path=$0"
|
||||
},
|
||||
{
|
||||
"src": "^\\/two\\/?$",
|
||||
"dest": "/_isr?x_astro_path=$0"
|
||||
}
|
||||
])
|
||||
})
|
||||
})
|
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
|
@ -4764,6 +4764,15 @@ importers:
|
|||
specifier: workspace:*
|
||||
version: link:../../../../../astro
|
||||
|
||||
packages/integrations/vercel/test/fixtures/isr:
|
||||
dependencies:
|
||||
'@astrojs/vercel':
|
||||
specifier: workspace:*
|
||||
version: link:../../..
|
||||
astro:
|
||||
specifier: workspace:*
|
||||
version: link:../../../../../astro
|
||||
|
||||
packages/integrations/vercel/test/fixtures/max-duration:
|
||||
dependencies:
|
||||
'@astrojs/vercel':
|
||||
|
|
Loading…
Add table
Reference in a new issue