From 84683f2582ab26e5d173c4128287edf223531a98 Mon Sep 17 00:00:00 2001 From: lilnasy <69170106+lilnasy@users.noreply.github.com> Date: Sat, 23 Dec 2023 13:44:47 +0000 Subject: [PATCH 01/19] feat(vercel): isr --- .../vercel/src/serverless/adapter.ts | 21 +++++++++++++++++++ .../vercel/src/serverless/entrypoint.ts | 3 +++ 2 files changed, 24 insertions(+) diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index f1c75e4b4d9e..0f5683cdf88c 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -122,6 +122,9 @@ 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; } export default function vercelServerless({ @@ -135,6 +138,7 @@ export default function vercelServerless({ functionPerRoute = false, edgeMiddleware = false, maxDuration, + isr = false, }: VercelServerlessConfig = {}): AstroIntegration { if (maxDuration) { if (typeof maxDuration !== 'number') { @@ -189,6 +193,9 @@ export default function vercelServerless({ }, vite: { ...getSpeedInsightsViteConfig(speedInsights?.enabled), + define: { + "import.meta.env.ASTRO_VERCEL_ISR": isr ? "true" : "false" + }, ssr: { external: ['@vercel/nft'], }, @@ -282,6 +289,7 @@ export default function vercelServerless({ includeFiles, excludeFiles, maxDuration, + isr, }); routeDefinitions.push({ src: route.pattern.source, @@ -299,6 +307,7 @@ export default function vercelServerless({ includeFiles, excludeFiles, maxDuration, + isr, }); const dest = _middlewareEntryPoint ? MIDDLEWARE_PATH : NODE_PATH; for (const route of routes) { @@ -396,6 +405,7 @@ interface CreateFunctionFolderArgs { includeFiles: URL[]; excludeFiles: URL[]; maxDuration: number | undefined; + isr: boolean } async function createFunctionFolder({ @@ -408,11 +418,13 @@ async function createFunctionFolder({ includeFiles, excludeFiles, maxDuration, + isr }: CreateFunctionFolderArgs) { // .vercel/output/functions/.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); + const prerenderConfig = new URL(`./functions/${functionName}.prerender-config.json`, config.outDir) // Copy necessary files (e.g. node_modules/) const { handler } = await copyDependenciesToFunction( @@ -439,6 +451,15 @@ async function createFunctionFolder({ maxDuration, supportsResponseStreaming: true, }); + + if (isr) { + // https://vercel.com/docs/build-output-api/v3/primitives#prerender-configuration-file + await writeJson(prerenderConfig, { + expiration: false, + allowQuery: ["vercel_original_path"], + passQuery: true + }); + } } function getRuntime(process: NodeJS.Process, logger: AstroIntegrationLogger): Runtime { diff --git a/packages/integrations/vercel/src/serverless/entrypoint.ts b/packages/integrations/vercel/src/serverless/entrypoint.ts index fe88c367d320..b1e6ff125249 100644 --- a/packages/integrations/vercel/src/serverless/entrypoint.ts +++ b/packages/integrations/vercel/src/serverless/entrypoint.ts @@ -8,6 +8,9 @@ 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 originalPath = url.searchParams.get('vercel_original_path') + if (originalPath) req.url = originalPath const clientAddress = req.headers['x-forwarded-for'] as string | undefined; const localsHeader = req.headers[ASTRO_LOCALS_HEADER]; const realPath = req.headers[ASTRO_PATH_HEADER]; From 85d02971d071987e95861c712749063c8084e409 Mon Sep 17 00:00:00 2001 From: lilnasy <69170106+lilnasy@users.noreply.github.com> Date: Sat, 23 Dec 2023 13:45:26 +0000 Subject: [PATCH 02/19] bypass token --- .../integrations/vercel/src/serverless/adapter.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index 0f5683cdf88c..655a1252c760 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -124,7 +124,12 @@ export interface VercelServerlessConfig { maxDuration?: number; /** Whether to cache on-demand rendered pages in the same way as static files. */ - isr?: boolean; + isr?: boolean | VercelISRConfig; +} + +interface VercelISRConfig { + /** A random string that you create. Its presence in the `__prerender_bypass` cookie will result in fresh responses being served. */ + bypassToken?: string; } export default function vercelServerless({ @@ -405,7 +410,7 @@ interface CreateFunctionFolderArgs { includeFiles: URL[]; excludeFiles: URL[]; maxDuration: number | undefined; - isr: boolean + isr: boolean | VercelISRConfig; } async function createFunctionFolder({ @@ -451,11 +456,12 @@ async function createFunctionFolder({ maxDuration, supportsResponseStreaming: true, }); - + if (isr) { // https://vercel.com/docs/build-output-api/v3/primitives#prerender-configuration-file await writeJson(prerenderConfig, { expiration: false, + bypassToken: typeof isr === "object" ? isr.bypassToken : undefined, allowQuery: ["vercel_original_path"], passQuery: true }); From 33df953247404b035bd1fd755b0fcaea69177880 Mon Sep 17 00:00:00 2001 From: lilnasy <69170106+lilnasy@users.noreply.github.com> Date: Wed, 10 Jan 2024 18:59:59 +0000 Subject: [PATCH 03/19] exclusion of certain paths --- .../vercel/src/serverless/adapter.ts | 92 +++++++++++++++---- .../vercel/src/serverless/entrypoint.ts | 10 +- 2 files changed, 79 insertions(+), 23 deletions(-) diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index 655a1252c760..216519263889 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -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, @@ -128,8 +134,27 @@ export interface VercelServerlessConfig { } interface VercelISRConfig { - /** A random string that you create. Its presence in the `__prerender_bypass` cookie will result in fresh responses being served. */ + /** + * A random string that you create. + * Its presence in the `__prerender_bypass` cookie will result in fresh responses being served, bypassing the cache. + * + * By default, none. + */ bypassToken?: string; + + /** + * Expiration time (in seconds) before the pages will be re-generated. + * + * By default, as long as the current deployment is in production. + */ + revalidate?: number; + + /** + * Paths that will always be served fresh. + * + * By default, none. + */ + exclude?: string[]; } export default function vercelServerless({ @@ -198,9 +223,6 @@ export default function vercelServerless({ }, vite: { ...getSpeedInsightsViteConfig(speedInsights?.enabled), - define: { - "import.meta.env.ASTRO_VERCEL_ISR": isr ? "true" : "false" - }, ssr: { external: ['@vercel/nft'], }, @@ -302,21 +324,49 @@ export default function vercelServerless({ }); } } else { - await createFunctionFolder({ - functionName: NODE_PATH, - runtime, - entry: new URL(_serverEntry, _buildTempFolder), - config: _config, - logger, - NTF_CACHE, - includeFiles, - excludeFiles, - maxDuration, - isr, - }); + if (isr) { + await createFunctionFolder({ + functionName: '_isr', + runtime, + entry: new URL(_serverEntry, _buildTempFolder), + config: _config, + logger, + NTF_CACHE, + includeFiles, + excludeFiles, + maxDuration, + isr, + }); + } + if (isr === false || (typeof isr === "object" && isr.exclude)) { + await createFunctionFolder({ + functionName: NODE_PATH, + runtime, + entry: new URL(_serverEntry, _buildTempFolder), + config: _config, + logger, + NTF_CACHE, + includeFiles, + excludeFiles, + maxDuration, + isr: false, + }); + } + if (typeof isr === "object" && isr.exclude) { + for (const route of isr.exclude) { + routeDefinitions.push({ + src: route, + dest: NODE_PATH, + }) + } + } const dest = _middlewareEntryPoint ? MIDDLEWARE_PATH : NODE_PATH; for (const route of routes) { if (!route.prerender) routeDefinitions.push({ src: route.pattern.source, dest }); + routeDefinitions.push({ + src: route.pattern.source, + dest: isr ? ISR_PATH : NODE_PATH, + }); } } if (_middlewareEntryPoint) { @@ -458,11 +508,15 @@ async function createFunctionFolder({ }); if (isr) { + const { + revalidate: expiration = false, + bypassToken = undefined, + } = typeof isr === "object" ? isr : {}; // https://vercel.com/docs/build-output-api/v3/primitives#prerender-configuration-file await writeJson(prerenderConfig, { - expiration: false, - bypassToken: typeof isr === "object" ? isr.bypassToken : undefined, - allowQuery: ["vercel_original_path"], + expiration, + bypassToken, + allowQuery: [ASTRO_PATH_PARAM], passQuery: true }); } diff --git a/packages/integrations/vercel/src/serverless/entrypoint.ts b/packages/integrations/vercel/src/serverless/entrypoint.ts index b1e6ff125249..31777ed6ce70 100644 --- a/packages/integrations/vercel/src/serverless/entrypoint.ts +++ b/packages/integrations/vercel/src/serverless/entrypoint.ts @@ -1,7 +1,7 @@ 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(); @@ -9,11 +9,9 @@ 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 originalPath = url.searchParams.get('vercel_original_path') - if (originalPath) req.url = originalPath 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; } @@ -29,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() {} From 8d763090a9d57b99cb20626ea2c7348926cab85c Mon Sep 17 00:00:00 2001 From: lilnasy <69170106+lilnasy@users.noreply.github.com> Date: Thu, 25 Jan 2024 21:22:57 +0000 Subject: [PATCH 04/19] add test --- .../vercel/test/fixtures/isr/astro.config.mjs | 13 ++++++++++ .../vercel/test/fixtures/isr/package.json | 9 +++++++ .../test/fixtures/isr/src/pages/one.astro | 8 ++++++ .../test/fixtures/isr/src/pages/two.astro | 8 ++++++ packages/integrations/vercel/test/isr.test.js | 26 +++++++++++++++++++ pnpm-lock.yaml | 9 +++++++ 6 files changed, 73 insertions(+) create mode 100644 packages/integrations/vercel/test/fixtures/isr/astro.config.mjs create mode 100644 packages/integrations/vercel/test/fixtures/isr/package.json create mode 100644 packages/integrations/vercel/test/fixtures/isr/src/pages/one.astro create mode 100644 packages/integrations/vercel/test/fixtures/isr/src/pages/two.astro create mode 100644 packages/integrations/vercel/test/isr.test.js diff --git a/packages/integrations/vercel/test/fixtures/isr/astro.config.mjs b/packages/integrations/vercel/test/fixtures/isr/astro.config.mjs new file mode 100644 index 000000000000..cdc8c1f22ad0 --- /dev/null +++ b/packages/integrations/vercel/test/fixtures/isr/astro.config.mjs @@ -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", + revalidate: 120, + exclude: ["/two"] + } + }) +}); diff --git a/packages/integrations/vercel/test/fixtures/isr/package.json b/packages/integrations/vercel/test/fixtures/isr/package.json new file mode 100644 index 000000000000..a1fd601d3b1a --- /dev/null +++ b/packages/integrations/vercel/test/fixtures/isr/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/vercel-isr", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/vercel": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/vercel/test/fixtures/isr/src/pages/one.astro b/packages/integrations/vercel/test/fixtures/isr/src/pages/one.astro new file mode 100644 index 000000000000..0c7fb90a735e --- /dev/null +++ b/packages/integrations/vercel/test/fixtures/isr/src/pages/one.astro @@ -0,0 +1,8 @@ + + + One + + +

One

+ + diff --git a/packages/integrations/vercel/test/fixtures/isr/src/pages/two.astro b/packages/integrations/vercel/test/fixtures/isr/src/pages/two.astro new file mode 100644 index 000000000000..e7ba9910e2a6 --- /dev/null +++ b/packages/integrations/vercel/test/fixtures/isr/src/pages/two.astro @@ -0,0 +1,8 @@ + + + Two + + +

Two

+ + diff --git a/packages/integrations/vercel/test/isr.test.js b/packages/integrations/vercel/test/isr.test.js new file mode 100644 index 000000000000..a8a7064aefb8 --- /dev/null +++ b/packages/integrations/vercel/test/isr.test.js @@ -0,0 +1,26 @@ +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 + }) + }) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c85679e9a996..dd8b13407618 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4797,6 +4797,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': From 8c4af724cd481af8be4c5c9984e444ce8bf2aaff Mon Sep 17 00:00:00 2001 From: lilnasy <69170106+lilnasy@users.noreply.github.com> Date: Thu, 25 Jan 2024 21:51:35 +0000 Subject: [PATCH 05/19] remove search params in dev mode --- .../integrations/vercel/src/serverless/adapter.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index 216519263889..86c0691d516d 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -258,6 +258,19 @@ 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 ?? [] : []; + const exclude = exclude_.concat("/_image").map(ex => new RegExp(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; From 3d9800d55b5bcc837fc31cac15d22ca814e70e7c Mon Sep 17 00:00:00 2001 From: Arsh <69170106+lilnasy@users.noreply.github.com> Date: Sat, 27 Jan 2024 00:31:24 +0530 Subject: [PATCH 06/19] Apply suggestions from code review Co-authored-by: Nate Moore --- packages/integrations/vercel/src/serverless/adapter.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index 86c0691d516d..f163693585b3 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -351,7 +351,7 @@ export default function vercelServerless({ isr, }); } - if (isr === false || (typeof isr === "object" && isr.exclude)) { + if (isr === false || (isr && typeof isr === "object" && isr.exclude)) { await createFunctionFolder({ functionName: NODE_PATH, runtime, @@ -365,7 +365,7 @@ export default function vercelServerless({ isr: false, }); } - if (typeof isr === "object" && isr.exclude) { + if (isr && typeof isr === "object" && isr.exclude) { for (const route of isr.exclude) { routeDefinitions.push({ src: route, From cff749a3be229011511b49d6a5b1921c2f77e9db Mon Sep 17 00:00:00 2001 From: Arsh <69170106+lilnasy@users.noreply.github.com> Date: Sat, 27 Jan 2024 00:44:16 +0530 Subject: [PATCH 07/19] Apply suggestions from code review --- packages/integrations/vercel/src/serverless/adapter.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index f163693585b3..789d852ecf69 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -135,8 +135,9 @@ export interface VercelServerlessConfig { interface VercelISRConfig { /** - * A random string that you create. + * A secret random string that you create. * Its presence in the `__prerender_bypass` cookie will result in fresh responses being served, bypassing the cache. + * 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. * * By default, none. */ From a34c215cce49b75da7f2824fe1e5c79cfccaad5f Mon Sep 17 00:00:00 2001 From: lilnasy <69170106+lilnasy@users.noreply.github.com> Date: Mon, 29 Jan 2024 15:13:46 +0000 Subject: [PATCH 08/19] Apply suggestions from code review --- .../vercel/src/serverless/adapter.ts | 165 +++++++----------- 1 file changed, 63 insertions(+), 102 deletions(-) diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index 789d852ecf69..2794732f302a 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -305,6 +305,7 @@ export default function vercelServerless({ 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) { @@ -320,18 +321,8 @@ export default function vercelServerless({ ? getRouteFuncName(route) : getFallbackFuncName(entryFile); - await createFunctionFolder({ - functionName: func, - runtime, - entry: entryFile, - config: _config, - logger, - NTF_CACHE, - includeFiles, - excludeFiles, - maxDuration, - isr, - }); + await builder.buildServerlessFolder(entryFile, func); + routeDefinitions.push({ src: route.pattern.source, dest: func, @@ -339,41 +330,18 @@ export default function vercelServerless({ } } else { if (isr) { - await createFunctionFolder({ - functionName: '_isr', - runtime, - entry: new URL(_serverEntry, _buildTempFolder), - config: _config, - logger, - NTF_CACHE, - includeFiles, - excludeFiles, - maxDuration, - isr, - }); - } - if (isr === false || (isr && typeof isr === "object" && isr.exclude)) { - await createFunctionFolder({ - functionName: NODE_PATH, - runtime, - entry: new URL(_serverEntry, _buildTempFolder), - config: _config, - logger, - NTF_CACHE, - includeFiles, - excludeFiles, - maxDuration, - isr: false, - }); - } - if (isr && typeof isr === "object" && isr.exclude) { - for (const route of isr.exclude) { - routeDefinitions.push({ - src: route, - dest: NODE_PATH, - }) + const isrConfig = typeof isr === "object" ? isr : {}; + await builder.buildISRFolder(new URL(_serverEntry, _buildTempFolder), '_isr', isrConfig); + if (isrConfig.exclude?.length) { + await builder.buildServerlessFolder(new URL(_serverEntry, _buildTempFolder), NODE_PATH); + for (const route of isrConfig.exclude) { + routeDefinitions.push({ src: route, dest: NODE_PATH }) + } } } + else { + await builder.buildServerlessFolder(new URL(_serverEntry, _buildTempFolder), NODE_PATH); + } const dest = _middlewareEntryPoint ? MIDDLEWARE_PATH : NODE_PATH; for (const route of routes) { if (!route.prerender) routeDefinitions.push({ src: route.pattern.source, dest }); @@ -466,70 +434,63 @@ async function createMiddlewareFolder({ functionName, entry, config }: CreateMid interface CreateFunctionFolderArgs { functionName: string; - runtime: Runtime; entry: URL; - config: AstroConfig; - logger: AstroIntegrationLogger; - NTF_CACHE: any; - includeFiles: URL[]; - excludeFiles: URL[]; - maxDuration: number | undefined; isr: boolean | VercelISRConfig; } -async function createFunctionFolder({ - functionName, - runtime, - entry, - config, - logger, - NTF_CACHE, - includeFiles, - excludeFiles, - maxDuration, - isr -}: CreateFunctionFolderArgs) { - // .vercel/output/functions/.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); - const prerenderConfig = new URL(`./functions/${functionName}.prerender-config.json`, config.outDir) - - // Copy necessary files (e.g. node_modules/) - const { handler } = await copyDependenciesToFunction( - { - entry, - outDir: functionFolder, - includeFiles, - excludeFiles, - logger, - }, - NTF_CACHE - ); +class VercelBuilder { + readonly NTF_CACHE = {} + + constructor( + readonly config: AstroConfig, + readonly excludeFiles: URL[], + readonly includeFiles: URL[], + readonly logger: AstroIntegrationLogger, + readonly maxDuration?: number, + readonly runtime = getRuntime(process, logger) + ) {} + + async buildServerlessFolder(entry: URL, functionName: string) { + const { config, includeFiles, excludeFiles, logger, NTF_CACHE, runtime, maxDuration } = this; + // .vercel/output/functions/.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); + + // Copy necessary files (e.g. node_modules/) + const { handler } = await copyDependenciesToFunction( + { + 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, - }); - - if (isr) { - const { - revalidate: expiration = false, - bypassToken = undefined, - } = typeof isr === "object" ? isr : {}; + // 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, + }); + } + + async buildISRFolder(entry: URL, functionName: string, isr: VercelISRConfig) { + 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, - bypassToken, + expiration: isr.revalidate ?? false, + bypassToken: isr.bypassToken, allowQuery: [ASTRO_PATH_PARAM], passQuery: true }); From 469bf447acdb27caffae525032c51adb00ee1b3e Mon Sep 17 00:00:00 2001 From: lilnasy <69170106+lilnasy@users.noreply.github.com> Date: Mon, 29 Jan 2024 15:34:36 +0000 Subject: [PATCH 09/19] fix missing await --- packages/integrations/vercel/src/serverless/adapter.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index 2794732f302a..eeb309fb1448 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -304,7 +304,6 @@ 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 @@ -329,18 +328,19 @@ export default function vercelServerless({ }); } } else { + const entryFile = new URL(_serverEntry, _buildTempFolder) if (isr) { const isrConfig = typeof isr === "object" ? isr : {}; - await builder.buildISRFolder(new URL(_serverEntry, _buildTempFolder), '_isr', isrConfig); + await builder.buildISRFolder(entryFile, '_isr', isrConfig); if (isrConfig.exclude?.length) { - await builder.buildServerlessFolder(new URL(_serverEntry, _buildTempFolder), NODE_PATH); + await builder.buildServerlessFolder(entryFile, NODE_PATH); for (const route of isrConfig.exclude) { routeDefinitions.push({ src: route, dest: NODE_PATH }) } } } else { - await builder.buildServerlessFolder(new URL(_serverEntry, _buildTempFolder), NODE_PATH); + await builder.buildServerlessFolder(entryFile, NODE_PATH); } const dest = _middlewareEntryPoint ? MIDDLEWARE_PATH : NODE_PATH; for (const route of routes) { @@ -485,7 +485,7 @@ class VercelBuilder { } async buildISRFolder(entry: URL, functionName: string, isr: VercelISRConfig) { - this.buildServerlessFolder(entry, functionName); + 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, { From 96be331f917ae34992179ff9517c2996fa403399 Mon Sep 17 00:00:00 2001 From: lilnasy <69170106+lilnasy@users.noreply.github.com> Date: Mon, 29 Jan 2024 16:24:58 +0000 Subject: [PATCH 10/19] escape src for regex --- packages/integrations/vercel/src/lib/redirects.ts | 4 ++++ packages/integrations/vercel/src/serverless/adapter.ts | 8 +++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/integrations/vercel/src/lib/redirects.ts b/packages/integrations/vercel/src/lib/redirects.ts index 46c97d349691..6e03f8a37dfc 100644 --- a/packages/integrations/vercel/src/lib/redirects.ts +++ b/packages/integrations/vercel/src/lib/redirects.ts @@ -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[] = []; diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index eeb309fb1448..783fb75fe2ef 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -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, @@ -263,7 +263,8 @@ export default function vercelServerless({ // 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 ?? [] : []; - const exclude = exclude_.concat("/_image").map(ex => new RegExp(ex)); + // 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(); @@ -335,7 +336,8 @@ export default function vercelServerless({ if (isrConfig.exclude?.length) { await builder.buildServerlessFolder(entryFile, NODE_PATH); for (const route of isrConfig.exclude) { - routeDefinitions.push({ src: route, dest: NODE_PATH }) + // vercel interprets src as a regex pattern, so we need to escape it + routeDefinitions.push({ src: escapeRegex(route), dest: NODE_PATH }) } } } From ab64e3decd3ed4f4bcdb7c11b43f77de0142541d Mon Sep 17 00:00:00 2001 From: lilnasy <69170106+lilnasy@users.noreply.github.com> Date: Mon, 29 Jan 2024 16:28:36 +0000 Subject: [PATCH 11/19] cleanup --- .../vercel/src/serverless/adapter.ts | 50 ++++++------------- 1 file changed, 16 insertions(+), 34 deletions(-) diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index 783fb75fe2ef..eecc360bb306 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -188,8 +188,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: { @@ -354,11 +352,7 @@ export default function vercelServerless({ } } 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 @@ -413,33 +407,6 @@ export default function vercelServerless({ type Runtime = `nodejs${string}.x`; -interface CreateMiddlewareFolderArgs { - config: AstroConfig; - entry: URL; - functionName: string; -} - -async function createMiddlewareFolder({ functionName, entry, config }: CreateMiddlewareFolderArgs) { - const functionFolder = new URL(`./functions/${functionName}.func/`, config.outDir); - - await generateEdgeMiddleware( - entry, - new URL(VERCEL_EDGE_MIDDLEWARE_FILE, config.srcDir), - new URL('./middleware.mjs', functionFolder) - ); - - await writeJson(new URL(`./.vc-config.json`, functionFolder), { - runtime: 'edge', - entrypoint: 'middleware.mjs', - }); -} - -interface CreateFunctionFolderArgs { - functionName: string; - entry: URL; - isr: boolean | VercelISRConfig; -} - class VercelBuilder { readonly NTF_CACHE = {} @@ -497,6 +464,21 @@ class VercelBuilder { passQuery: true }); } + + async buildMiddlewareFolder(entry: URL, functionName: string) { + const functionFolder = new URL(`./functions/${functionName}.func/`, this.config.outDir); + + await generateEdgeMiddleware( + entry, + 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 { From 280823a903b212ec6504f35d80dad2bd4c446641 Mon Sep 17 00:00:00 2001 From: lilnasy <69170106+lilnasy@users.noreply.github.com> Date: Mon, 29 Jan 2024 17:40:10 +0000 Subject: [PATCH 12/19] revalidate -> expiration --- .../integrations/vercel/src/serverless/adapter.ts | 12 +++++++----- .../vercel/test/fixtures/isr/astro.config.mjs | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index eecc360bb306..b066b9c1112d 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -139,21 +139,23 @@ interface VercelISRConfig { * Its presence in the `__prerender_bypass` cookie will result in fresh responses being served, bypassing the cache. * 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. * - * By default, none. + * @default `undefined` */ bypassToken?: string; /** * Expiration time (in seconds) before the pages will be re-generated. * - * By default, as long as the current deployment is in production. + * Setting to `false` means that the page will stay cached as long as the current deployment is in production. + * + * @default `false` */ - revalidate?: number; + expiration?: number | false; /** * Paths that will always be served fresh. * - * By default, none. + * @default `[]` */ exclude?: string[]; } @@ -458,7 +460,7 @@ class VercelBuilder { 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.revalidate ?? false, + expiration: isr.expiration ?? false, bypassToken: isr.bypassToken, allowQuery: [ASTRO_PATH_PARAM], passQuery: true diff --git a/packages/integrations/vercel/test/fixtures/isr/astro.config.mjs b/packages/integrations/vercel/test/fixtures/isr/astro.config.mjs index cdc8c1f22ad0..e7545c6f2a34 100644 --- a/packages/integrations/vercel/test/fixtures/isr/astro.config.mjs +++ b/packages/integrations/vercel/test/fixtures/isr/astro.config.mjs @@ -6,7 +6,7 @@ export default defineConfig({ adapter: vercel({ isr: { bypassToken: "1c9e601d-9943-4e7c-9575-005556d774a8", - revalidate: 120, + expiration: 120, exclude: ["/two"] } }) From 3e87dbf020d0aa64fd160f06868a9c96cb921b87 Mon Sep 17 00:00:00 2001 From: lilnasy <69170106+lilnasy@users.noreply.github.com> Date: Mon, 29 Jan 2024 17:47:42 +0000 Subject: [PATCH 13/19] update type docs --- packages/integrations/vercel/src/serverless/adapter.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index b066b9c1112d..e3917b545127 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -136,8 +136,8 @@ export interface VercelServerlessConfig { 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. - * 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. + * 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` */ @@ -153,7 +153,7 @@ interface VercelISRConfig { expiration?: number | false; /** - * Paths that will always be served fresh. + * Paths that will always be served by a serverless function instead of an ISR function. * * @default `[]` */ From 550538b4bb716783cd9c2b39e612a13f74b76275 Mon Sep 17 00:00:00 2001 From: lilnasy <69170106+lilnasy@users.noreply.github.com> Date: Mon, 29 Jan 2024 19:06:23 +0000 Subject: [PATCH 14/19] always exclude /_image --- .../vercel/src/serverless/adapter.ts | 22 ++++++++-------- packages/integrations/vercel/test/isr.test.js | 25 +++++++++++++++++++ 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index e3917b545127..67d7477b939a 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -332,25 +332,27 @@ export default function vercelServerless({ const entryFile = new URL(_serverEntry, _buildTempFolder) if (isr) { const isrConfig = typeof isr === "object" ? isr : {}; - await builder.buildISRFolder(entryFile, '_isr', isrConfig); if (isrConfig.exclude?.length) { await builder.buildServerlessFolder(entryFile, NODE_PATH); + 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: NODE_PATH }) + 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 }); - routeDefinitions.push({ - src: route.pattern.source, - dest: isr ? ISR_PATH : 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) { diff --git a/packages/integrations/vercel/test/isr.test.js b/packages/integrations/vercel/test/isr.test.js index a8a7064aefb8..d10181c8d3a0 100644 --- a/packages/integrations/vercel/test/isr.test.js +++ b/packages/integrations/vercel/test/isr.test.js @@ -23,4 +23,29 @@ describe("ISR", () => { "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" + } + ]) + }) }) From 102fc1cfbc2b49a0a92ba16f8671e1c8f9b47487 Mon Sep 17 00:00:00 2001 From: lilnasy <69170106+lilnasy@users.noreply.github.com> Date: Thu, 1 Feb 2024 15:30:03 +0000 Subject: [PATCH 15/19] add changeset --- .changeset/flat-snakes-hammer.md | 50 ++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .changeset/flat-snakes-hammer.md diff --git a/.changeset/flat-snakes-hammer.md b/.changeset/flat-snakes-hammer.md new file mode 100644 index 000000000000..ce93a34466ce --- /dev/null +++ b/.changeset/flat-snakes-hammer.md @@ -0,0 +1,50 @@ +--- +"@astrojs/vercel": minor +--- + +Introduces a new config options, `isr`, that allows 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 `astro.config.mjs`: + +```js +export default defineConfig({ + output: "server", + adapter: vercel({ isr: true }) +}) +``` + +##### Time-based invalidation + +By default, ISR responses are cached for the duration of your deployment. You can change this by setting `isr` to an object with an `expiration` field: + +```js +export default defineConfig({ + output: "server", + adapter: vercel({ + isr: { + // caches all pages on first request and saves for 1 day + revalidate: 60 * 60 * 24 + } + }) +}) +``` + +##### Manual invalidation + +To implement [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: + +```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" ] + } + }) +}) +``` + +Note that ISR function requests do not include search params, similar to [requests](https://docs.astro.build/en/reference/api-reference/#astrorequest) in static mode. From aa27e59bcdf38cab8cebddc03d249fd5771022a2 Mon Sep 17 00:00:00 2001 From: Arsh <69170106+lilnasy@users.noreply.github.com> Date: Mon, 5 Feb 2024 20:16:20 +0530 Subject: [PATCH 16/19] Apply suggestions from code review --- .changeset/flat-snakes-hammer.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/flat-snakes-hammer.md b/.changeset/flat-snakes-hammer.md index ce93a34466ce..f7540f2a9288 100644 --- a/.changeset/flat-snakes-hammer.md +++ b/.changeset/flat-snakes-hammer.md @@ -23,7 +23,7 @@ export default defineConfig({ adapter: vercel({ isr: { // caches all pages on first request and saves for 1 day - revalidate: 60 * 60 * 24 + expiration: 60 * 60 * 24 } }) }) From 9b8f1afa715a693ce004b544c9d45dbd5ae81600 Mon Sep 17 00:00:00 2001 From: lilnasy <69170106+lilnasy@users.noreply.github.com> Date: Mon, 5 Feb 2024 15:01:53 +0000 Subject: [PATCH 17/19] always create serverless function for /_image --- packages/integrations/vercel/src/serverless/adapter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index 67d7477b939a..880e35acf703 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -332,8 +332,8 @@ export default function vercelServerless({ const entryFile = new URL(_serverEntry, _buildTempFolder) if (isr) { const isrConfig = typeof isr === "object" ? isr : {}; + await builder.buildServerlessFolder(entryFile, NODE_PATH); if (isrConfig.exclude?.length) { - await builder.buildServerlessFolder(entryFile, NODE_PATH); 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 From 89544980d4e9dab739cb3a87faff01eee8050b68 Mon Sep 17 00:00:00 2001 From: Arsh <69170106+lilnasy@users.noreply.github.com> Date: Mon, 5 Feb 2024 21:09:22 +0530 Subject: [PATCH 18/19] Apply suggestions from code review Co-authored-by: Sarah Rainsberger --- .changeset/flat-snakes-hammer.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/.changeset/flat-snakes-hammer.md b/.changeset/flat-snakes-hammer.md index f7540f2a9288..49b8d2ca5617 100644 --- a/.changeset/flat-snakes-hammer.md +++ b/.changeset/flat-snakes-hammer.md @@ -2,9 +2,9 @@ "@astrojs/vercel": minor --- -Introduces a new config options, `isr`, that allows 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. +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 `astro.config.mjs`: +To enable this feature, set `isr` to true in your Vercel adapter configuration in `astro.config.mjs`: ```js export default defineConfig({ @@ -13,9 +13,14 @@ export default defineConfig({ }) ``` -##### Time-based invalidation -By default, ISR responses are cached for the duration of your deployment. You can change this by setting `isr` to an object with an `expiration` field: +## 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({ @@ -29,9 +34,9 @@ export default defineConfig({ }) ``` -##### Manual invalidation +### Manual invalidation -To implement [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: +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({ From f51f9a870552576e9b6a5d2026e4f308cc026138 Mon Sep 17 00:00:00 2001 From: Arsh <69170106+lilnasy@users.noreply.github.com> Date: Mon, 5 Feb 2024 23:55:03 +0530 Subject: [PATCH 19/19] Apply suggestions from code review --- .changeset/flat-snakes-hammer.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.changeset/flat-snakes-hammer.md b/.changeset/flat-snakes-hammer.md index 49b8d2ca5617..787d2899dfa9 100644 --- a/.changeset/flat-snakes-hammer.md +++ b/.changeset/flat-snakes-hammer.md @@ -52,4 +52,3 @@ export default defineConfig({ }) ``` -Note that ISR function requests do not include search params, similar to [requests](https://docs.astro.build/en/reference/api-reference/#astrorequest) in static mode.