From 87712f2342f37bf0f85deb3bf511f90e03b1c396 Mon Sep 17 00:00:00 2001 From: lilnasy <69170106+lilnasy@users.noreply.github.com> Date: Wed, 18 Oct 2023 23:10:30 +0000 Subject: [PATCH 01/10] feat(vercel): maxDuration config --- .../integrations/vercel/src/serverless/adapter.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index de6ddb83ea40..0b444459e03d 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -84,6 +84,10 @@ export interface VercelServerlessConfig { devImageService?: DevImageService; edgeMiddleware?: boolean; functionPerRoute?: boolean; + /** + * Maximum duration (in seconds) that will be allowed for the Serverless Function. + */ + maxDuration?: number; } export default function vercelServerless({ @@ -97,6 +101,7 @@ export default function vercelServerless({ devImageService = 'sharp', functionPerRoute = false, edgeMiddleware = false, + maxDuration, }: VercelServerlessConfig = {}): AstroIntegration { let _config: AstroConfig; let buildTempFolder: URL; @@ -111,7 +116,8 @@ export default function vercelServerless({ funcName: string, entry: URL, inc: URL[], - logger: AstroIntegrationLogger + logger: AstroIntegrationLogger, + options: { maxDuration?: number } ) { const functionFolder = new URL(`./functions/${funcName}.func/`, _config.outDir); @@ -139,6 +145,7 @@ export default function vercelServerless({ runtime: getRuntime(), handler, launcherType: 'Nodejs', + maxDuration: options.maxDuration, }); } @@ -261,7 +268,7 @@ You can set functionPerRoute: false to prevent surpassing the limit.` ? getRouteFuncName(route) : getFallbackFuncName(entryFile); - await createFunctionFolder(func, entryFile, filesToInclude, logger); + await createFunctionFolder(func, entryFile, filesToInclude, logger, { maxDuration }); routeDefinitions.push({ src: route.pattern.source, dest: func, @@ -272,7 +279,8 @@ You can set functionPerRoute: false to prevent surpassing the limit.` 'render', new URL(serverEntry, buildTempFolder), filesToInclude, - logger + logger, + { maxDuration } ); routeDefinitions.push({ src: '/.*', dest: 'render' }); } From 7854f231a3061fbadadaa59a08ab0b2a3d4b986f Mon Sep 17 00:00:00 2001 From: lilnasy <69170106+lilnasy@users.noreply.github.com> Date: Wed, 18 Oct 2023 23:30:30 +0000 Subject: [PATCH 02/10] factor out createFunctionFolder --- .../vercel/src/serverless/adapter.ts | 114 +++++++++++------- 1 file changed, 70 insertions(+), 44 deletions(-) diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index 0b444459e03d..e3a690a96997 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -112,43 +112,6 @@ export default function vercelServerless({ const NTF_CACHE = Object.create(null); - async function createFunctionFolder( - funcName: string, - entry: URL, - inc: URL[], - logger: AstroIntegrationLogger, - options: { maxDuration?: number } - ) { - const functionFolder = new URL(`./functions/${funcName}.func/`, _config.outDir); - - // Copy necessary files (e.g. node_modules/) - const { handler } = await copyDependenciesToFunction( - { - entry, - outDir: functionFolder, - includeFiles: inc, - excludeFiles: excludeFiles?.map((file) => new URL(file, _config.root)) || [], - 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(new URL(`./package.json`, functionFolder), { - type: 'module', - }); - - // Serverless function config - // https://vercel.com/docs/build-output-api/v3#vercel-primitives/serverless-functions/configuration - await writeJson(new URL(`./.vc-config.json`, functionFolder), { - runtime: getRuntime(), - handler, - launcherType: 'Nodejs', - maxDuration: options.maxDuration, - }); - } - return { name: PACKAGE_NAME, hooks: { @@ -268,20 +231,32 @@ You can set functionPerRoute: false to prevent surpassing the limit.` ? getRouteFuncName(route) : getFallbackFuncName(entryFile); - await createFunctionFolder(func, entryFile, filesToInclude, logger, { maxDuration }); + await createFunctionFolder({ + functionName: func, + entry: entryFile, + config: _config, + logger, + NTF_CACHE, + includeFiles: filesToInclude, + excludeFiles, + maxDuration + }); routeDefinitions.push({ src: route.pattern.source, dest: func, }); } } else { - await createFunctionFolder( - 'render', - new URL(serverEntry, buildTempFolder), - filesToInclude, + await createFunctionFolder({ + functionName: 'render', + entry: new URL(serverEntry, buildTempFolder), + config: _config, logger, - { maxDuration } - ); + NTF_CACHE, + includeFiles: filesToInclude, + excludeFiles, + maxDuration + }); routeDefinitions.push({ src: '/.*', dest: 'render' }); } @@ -322,6 +297,57 @@ You can set functionPerRoute: false to prevent surpassing the limit.` }; } +interface CreateFunctionFolderArgs { + functionName: string + entry: URL + config: AstroConfig + logger: AstroIntegrationLogger + NTF_CACHE: any + includeFiles: URL[] + excludeFiles?: string[] + maxDuration?: number +} + +async function createFunctionFolder({ + functionName, + entry, + config, + logger, + NTF_CACHE, + includeFiles, + excludeFiles, + maxDuration, +}: CreateFunctionFolderArgs) { + const functionFolder = new URL(`./functions/${functionName}.func/`, config.outDir); + + // Copy necessary files (e.g. node_modules/) + const { handler } = await copyDependenciesToFunction( + { + entry, + outDir: functionFolder, + includeFiles, + excludeFiles: excludeFiles?.map((file) => new URL(file, config.root)) || [], + 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(new URL(`./package.json`, functionFolder), { + type: 'module', + }); + + // Serverless function config + // https://vercel.com/docs/build-output-api/v3#vercel-primitives/serverless-functions/configuration + await writeJson(new URL(`./.vc-config.json`, functionFolder), { + runtime: getRuntime(), + handler, + launcherType: 'Nodejs', + maxDuration, + }); +} + function validateRuntime() { const version = process.version.slice(1); // 'v16.5.0' --> '16.5.0' const major = version.split('.')[0]; // '16.5.0' --> '16' From 32ee7cc545a0f5bc0b34a09070d00d89ff0e6072 Mon Sep 17 00:00:00 2001 From: lilnasy <69170106+lilnasy@users.noreply.github.com> Date: Wed, 18 Oct 2023 23:40:36 +0000 Subject: [PATCH 03/10] document public options --- .../vercel/src/serverless/adapter.ts | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index e3a690a96997..e19a50f31a99 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -75,18 +75,35 @@ export interface VercelServerlessConfig { * @deprecated */ analytics?: boolean; + + /** Configuration for [Vercel Web Analytics](https://vercel.com/docs/concepts/analytics). */ webAnalytics?: VercelWebAnalyticsConfig; + + /** Configuration for [Vercel Speed Insights](https://vercel.com/docs/concepts/speed-insights). */ speedInsights?: VercelSpeedInsightsConfig; + + /** Force files to be bundled with your function. This is helpful when you notice missing files. */ includeFiles?: string[]; + + /** Exclude any files from the bundling process that would otherwise be included. */ excludeFiles?: string[]; + + /** When enabled, an Image Service powered by the Vercel Image Optimization API will be automatically configured and used in production. In development, the image service specified by devImageService will be used instead. */ imageService?: boolean; + + /** Configuration options for [Vercel’s Image Optimization API](https://vercel.com/docs/concepts/image-optimization). See [Vercel’s image configuration documentation](https://vercel.com/docs/build-output-api/v3/configuration#images) for a complete list of supported parameters. */ imagesConfig?: VercelImageConfig; + + /** Allows you to configure which image service to use in development when imageService is enabled. */ devImageService?: DevImageService; + + /** Whether to create the Vercel Edge middleware from an Astro middleware in your code base. */ edgeMiddleware?: boolean; + + /** Whether to split builds into a separate function for each route. */ functionPerRoute?: boolean; - /** - * Maximum duration (in seconds) that will be allowed for the Serverless Function. - */ + + /** Maximum duration (in seconds) that will be allowed for the Serverless Function. */ maxDuration?: number; } From 961de407ca655dce8125b9db0fdd92aac441f8b8 Mon Sep 17 00:00:00 2001 From: lilnasy <69170106+lilnasy@users.noreply.github.com> Date: Wed, 18 Oct 2023 23:42:10 +0000 Subject: [PATCH 04/10] add changeset --- .changeset/lazy-actors-enjoy.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .changeset/lazy-actors-enjoy.md diff --git a/.changeset/lazy-actors-enjoy.md b/.changeset/lazy-actors-enjoy.md new file mode 100644 index 000000000000..3896041b4846 --- /dev/null +++ b/.changeset/lazy-actors-enjoy.md @@ -0,0 +1,14 @@ +--- +'@astrojs/vercel': minor +--- + +You can now configure how long your functions can run before timing out. + +```diff +export default defineConfig({ + output: "server", + adapter: vercel({ ++ maxDuration: 60 + }), +}); +``` From e75f4f6d91e3dd85e3013f942cdd92d8376b261b Mon Sep 17 00:00:00 2001 From: lilnasy <69170106+lilnasy@users.noreply.github.com> Date: Thu, 19 Oct 2023 22:12:21 +0000 Subject: [PATCH 05/10] add test --- .../fixtures/max-duration/astro.config.mjs | 9 +++++++++ .../test/fixtures/max-duration/package.json | 10 ++++++++++ .../fixtures/max-duration/src/pages/one.astro | 8 ++++++++ .../fixtures/max-duration/src/pages/two.astro | 8 ++++++++ .../vercel/test/max-duration.test.js | 19 +++++++++++++++++++ pnpm-lock.yaml | 9 +++++++++ 6 files changed, 63 insertions(+) create mode 100644 packages/integrations/vercel/test/fixtures/max-duration/astro.config.mjs create mode 100644 packages/integrations/vercel/test/fixtures/max-duration/package.json create mode 100644 packages/integrations/vercel/test/fixtures/max-duration/src/pages/one.astro create mode 100644 packages/integrations/vercel/test/fixtures/max-duration/src/pages/two.astro create mode 100644 packages/integrations/vercel/test/max-duration.test.js diff --git a/packages/integrations/vercel/test/fixtures/max-duration/astro.config.mjs b/packages/integrations/vercel/test/fixtures/max-duration/astro.config.mjs new file mode 100644 index 000000000000..a02d60c14a56 --- /dev/null +++ b/packages/integrations/vercel/test/fixtures/max-duration/astro.config.mjs @@ -0,0 +1,9 @@ +import { defineConfig } from 'astro/config'; +import vercel from '@astrojs/vercel/serverless'; + +export default defineConfig({ + output: "server", + adapter: vercel({ + maxDuration: 60 + }) +}); diff --git a/packages/integrations/vercel/test/fixtures/max-duration/package.json b/packages/integrations/vercel/test/fixtures/max-duration/package.json new file mode 100644 index 000000000000..9a45d782edd9 --- /dev/null +++ b/packages/integrations/vercel/test/fixtures/max-duration/package.json @@ -0,0 +1,10 @@ +{ + "name": "@test/vercel-max-duration", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/vercel": "workspace:*", + "astro": "workspace:*" + } +} + \ No newline at end of file diff --git a/packages/integrations/vercel/test/fixtures/max-duration/src/pages/one.astro b/packages/integrations/vercel/test/fixtures/max-duration/src/pages/one.astro new file mode 100644 index 000000000000..0c7fb90a735e --- /dev/null +++ b/packages/integrations/vercel/test/fixtures/max-duration/src/pages/one.astro @@ -0,0 +1,8 @@ + + + One + + +

One

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

Two

+ + diff --git a/packages/integrations/vercel/test/max-duration.test.js b/packages/integrations/vercel/test/max-duration.test.js new file mode 100644 index 000000000000..9826dcdfbb58 --- /dev/null +++ b/packages/integrations/vercel/test/max-duration.test.js @@ -0,0 +1,19 @@ +import { loadFixture } from './test-utils.js'; +import { expect } from 'chai'; + +describe('maxDuration', () => { + /** @type {import('./test-utils.js').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/max-duration/', + }); + await fixture.build(); + }); + + it('makes it to vercel function configuration', async () => { + const vcConfig = JSON.parse(await fixture.readFile('../.vercel/output/functions/render.func/.vc-config.json')); + expect(vcConfig).to.deep.include({ maxDuration: 60 }); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a19fcf7c8bc3..cfaf9df8d1ef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4605,6 +4605,15 @@ importers: specifier: workspace:* version: link:../../../../../astro + packages/integrations/vercel/test/fixtures/max-duration: + dependencies: + '@astrojs/vercel': + specifier: workspace:* + version: link:../../.. + astro: + specifier: workspace:* + version: link:../../../../../astro + packages/integrations/vercel/test/fixtures/middleware-with-edge-file: dependencies: '@astrojs/vercel': From 0eb004e6e76094d943dc9da08a62b16ad24533fb Mon Sep 17 00:00:00 2001 From: lilnasy <69170106+lilnasy@users.noreply.github.com> Date: Thu, 19 Oct 2023 22:18:29 +0000 Subject: [PATCH 06/10] add docs --- packages/integrations/vercel/README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/integrations/vercel/README.md b/packages/integrations/vercel/README.md index 143a8db5c37b..44c0bc456dfc 100644 --- a/packages/integrations/vercel/README.md +++ b/packages/integrations/vercel/README.md @@ -261,6 +261,26 @@ export default defineConfig({ }); ``` +### maxDuration + +**Type:** `number`
+**Available for:** Serverless + +Use this property to extend or limit the maximum duration (in seconds) that the Serverless Function can run before timing out. See [Vercel documentation](https://vercel.com/docs/functions/serverless-functions/runtimes#maxduration) to find out the valid range for your plan. + +```js +// astro.config.mjs +import { defineConfig } from 'astro/config'; +import vercel from '@astrojs/vercel/serverless'; + +export default defineConfig({ + output: "server", + adapter: vercel({ ++ maxDuration: 60 + }), +}); +``` + ### Function bundling configuration The Vercel adapter combines all of your routes into a single function by default. From fcd92b22923ac1e062a0a40e72df60cd0a70f959 Mon Sep 17 00:00:00 2001 From: lilnasy <69170106+lilnasy@users.noreply.github.com> Date: Fri, 20 Oct 2023 14:37:21 +0000 Subject: [PATCH 07/10] warn on longer than 900s --- .../vercel/src/serverless/adapter.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index e19a50f31a99..59a707f8d2bb 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -27,6 +27,7 @@ import { type VercelWebAnalyticsConfig, } from '../lib/web-analytics.js'; import { generateEdgeMiddleware } from './middleware.js'; +import { type } from 'node:os'; const PACKAGE_NAME = '@astrojs/vercel/serverless'; export const ASTRO_LOCALS_HEADER = 'x-astro-locals'; @@ -103,7 +104,7 @@ export interface VercelServerlessConfig { /** Whether to split builds into a separate function for each route. */ functionPerRoute?: boolean; - /** Maximum duration (in seconds) that will be allowed for the Serverless Function. */ + /** Maximum duration (in seconds) that the function can for before timing out. See [Vercel documentation](https://vercel.com/docs/functions/serverless-functions/runtimes#maxduration) for the allowed values for your plan. */ maxDuration?: number; } @@ -120,6 +121,16 @@ export default function vercelServerless({ edgeMiddleware = false, maxDuration, }: VercelServerlessConfig = {}): AstroIntegration { + + if (maxDuration) { + if (typeof maxDuration !== 'number') { + throw new TypeError(`maxDuration must be a number`, { cause: maxDuration }); + } + if (maxDuration <= 0) { + throw new TypeError(`maxDuration must be a positive number`, { cause: maxDuration }); + } + } + let _config: AstroConfig; let buildTempFolder: URL; let serverEntry: string; @@ -133,6 +144,12 @@ export default function vercelServerless({ name: PACKAGE_NAME, hooks: { 'astro:config:setup': async ({ command, config, updateConfig, injectScript, logger }) => { + + if (maxDuration && maxDuration > 900) { + logger.warn(`maxDuration is set to ${maxDuration} seconds, which is longer than the maximum allowed duration of 900 seconds.`) + logger.warn(`Please make sure that your plan allows for this duration. See https://vercel.com/docs/functions/serverless-functions/runtimes#maxduration for more information.`) + } + if (webAnalytics?.enabled || analytics) { if (analytics) { logger.warn( From fc7ebd4cb6d84231df5b988e2be9afb0398660da Mon Sep 17 00:00:00 2001 From: lilnasy <69170106+lilnasy@users.noreply.github.com> Date: Fri, 20 Oct 2023 14:40:22 +0000 Subject: [PATCH 08/10] remove accidental import --- packages/integrations/vercel/src/serverless/adapter.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index 59a707f8d2bb..09a602e4e42b 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -27,7 +27,6 @@ import { type VercelWebAnalyticsConfig, } from '../lib/web-analytics.js'; import { generateEdgeMiddleware } from './middleware.js'; -import { type } from 'node:os'; const PACKAGE_NAME = '@astrojs/vercel/serverless'; export const ASTRO_LOCALS_HEADER = 'x-astro-locals'; From 630279c039800ef7b353c4d047c6caeb9602fb53 Mon Sep 17 00:00:00 2001 From: Arsh <69170106+lilnasy@users.noreply.github.com> Date: Fri, 20 Oct 2023 21:31:03 +0530 Subject: [PATCH 09/10] Apply suggestions from code review Co-authored-by: Sarah Rainsberger --- packages/integrations/vercel/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/integrations/vercel/README.md b/packages/integrations/vercel/README.md index 44c0bc456dfc..e6eb62ba895d 100644 --- a/packages/integrations/vercel/README.md +++ b/packages/integrations/vercel/README.md @@ -266,9 +266,9 @@ export default defineConfig({ **Type:** `number`
**Available for:** Serverless -Use this property to extend or limit the maximum duration (in seconds) that the Serverless Function can run before timing out. See [Vercel documentation](https://vercel.com/docs/functions/serverless-functions/runtimes#maxduration) to find out the valid range for your plan. +Use this property to extend or limit 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. -```js +```diff lang="js" // astro.config.mjs import { defineConfig } from 'astro/config'; import vercel from '@astrojs/vercel/serverless'; From 7085f7f2038dcde58a25348208a7a86a5b32310a Mon Sep 17 00:00:00 2001 From: Arsh <69170106+lilnasy@users.noreply.github.com> Date: Fri, 20 Oct 2023 21:34:36 +0530 Subject: [PATCH 10/10] Apply suggestions from code review --- 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 09a602e4e42b..ef8828132331 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -103,7 +103,7 @@ export interface VercelServerlessConfig { /** Whether to split builds into a separate function for each route. */ functionPerRoute?: boolean; - /** Maximum duration (in seconds) that the function can for before timing out. See [Vercel documentation](https://vercel.com/docs/functions/serverless-functions/runtimes#maxduration) for the allowed values for your plan. */ + /** 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; }