From 154af8f5ead25b3cf100cfd445329bd1d3fe876a Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Thu, 29 Jun 2023 16:18:28 -0400 Subject: [PATCH] Split support in the Vercel Serverless adapter (#7514) * start of vercel split support * Split Mode with the Vercel Adapter * Write routes into the config.json * Add a changeset * Add docs * Better changeset --- .changeset/tricky-snails-poke.md | 21 +++++ packages/integrations/vercel/README.md | 18 +++++ packages/integrations/vercel/src/lib/fs.ts | 2 +- .../vercel/src/serverless/adapter.ts | 78 ++++++++++++------- .../test/fixtures/basic/astro.config.mjs | 6 ++ .../vercel/test/fixtures/basic/package.json | 9 +++ .../test/fixtures/basic/src/pages/one.astro | 8 ++ .../test/fixtures/basic/src/pages/two.astro | 8 ++ .../integrations/vercel/test/split.test.js | 29 +++++++ pnpm-lock.yaml | 9 +++ 10 files changed, 160 insertions(+), 28 deletions(-) create mode 100644 .changeset/tricky-snails-poke.md create mode 100644 packages/integrations/vercel/test/fixtures/basic/astro.config.mjs create mode 100644 packages/integrations/vercel/test/fixtures/basic/package.json create mode 100644 packages/integrations/vercel/test/fixtures/basic/src/pages/one.astro create mode 100644 packages/integrations/vercel/test/fixtures/basic/src/pages/two.astro create mode 100644 packages/integrations/vercel/test/split.test.js diff --git a/.changeset/tricky-snails-poke.md b/.changeset/tricky-snails-poke.md new file mode 100644 index 0000000000..815942d1a4 --- /dev/null +++ b/.changeset/tricky-snails-poke.md @@ -0,0 +1,21 @@ +--- +'@astrojs/vercel': minor +--- + +Split support in Vercel Serverless + +The Vercel adapter builds to a single function by default. Astro 2.7 added support for splitting your build into separate entry points per page. If you use this configuration the Vercel adapter will generate a separate function for each page. This can help reduce the size of each function so they are only bundling code used on that page. + +```js +// astro.config.mjs +import { defineConfig } from 'astro/config'; +import vercel from '@astrojs/vercel/serverless'; + +export default defineConfig({ + output: 'server', + adapter: vercel(), + build: { + split: true + } +}); +``` diff --git a/packages/integrations/vercel/README.md b/packages/integrations/vercel/README.md index fd85498394..c32a5f57cb 100644 --- a/packages/integrations/vercel/README.md +++ b/packages/integrations/vercel/README.md @@ -215,6 +215,24 @@ export default defineConfig({ }); ``` +### Per-page functions + +The Vercel adapter builds to a single function by default. Astro 2.7 added support for splitting your build into separate entry points per page. If you use this configuration the Vercel adapter will generate a separate function for each page. This can help reduce the size of each function so they are only bundling code used on that page. + +```js +// astro.config.mjs +import { defineConfig } from 'astro/config'; +import vercel from '@astrojs/vercel/serverless'; + +export default defineConfig({ + output: 'server', + adapter: vercel(), + build: { + split: true + } +}); +``` + ### Vercel Middleware You can use Vercel middleware to intercept a request and redirect before sending a response. Vercel middleware can run for Edge, SSR, and Static deployments. You don't need to install `@vercel/edge` to write middleware, but you do need to install it to use features such as geolocation. For more information see [Vercel’s middleware documentation](https://vercel.com/docs/concepts/functions/edge-middleware). diff --git a/packages/integrations/vercel/src/lib/fs.ts b/packages/integrations/vercel/src/lib/fs.ts index 875a0ae9c8..18fbe85d29 100644 --- a/packages/integrations/vercel/src/lib/fs.ts +++ b/packages/integrations/vercel/src/lib/fs.ts @@ -4,7 +4,7 @@ import nodePath from 'node:path'; import { fileURLToPath } from 'node:url'; export async function writeJson(path: PathLike, data: T) { - await fs.writeFile(path, JSON.stringify(data), { encoding: 'utf-8' }); + await fs.writeFile(path, JSON.stringify(data, null, '\t'), { encoding: 'utf-8' }); } export async function removeDir(dir: PathLike) { diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index 8a18707702..52fb9b34b9 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -1,4 +1,4 @@ -import type { AstroAdapter, AstroConfig, AstroIntegration } from 'astro'; +import type { AstroAdapter, AstroConfig, AstroIntegration, RouteData } from 'astro'; import glob from 'fast-glob'; import { pathToFileURL } from 'url'; @@ -12,6 +12,7 @@ import { exposeEnv } from '../lib/env.js'; import { getVercelOutput, removeDir, writeJson } from '../lib/fs.js'; import { copyDependenciesToFunction } from '../lib/nft.js'; import { getRedirects } from '../lib/redirects.js'; +import { basename } from 'node:path'; const PACKAGE_NAME = '@astrojs/vercel/serverless'; @@ -40,8 +41,34 @@ export default function vercelServerless({ }: VercelServerlessConfig = {}): AstroIntegration { let _config: AstroConfig; let buildTempFolder: URL; - let functionFolder: URL; let serverEntry: string; + let _entryPoints: Map; + + async function createFunctionFolder(funcName: string, entry: URL, inc: URL[]) { + 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)) || [], + }); + + // 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', + }); + } return { name: PACKAGE_NAME, @@ -70,7 +97,6 @@ export default function vercelServerless({ setAdapter(getAdapter()); _config = config; buildTempFolder = config.build.server; - functionFolder = new URL('./functions/render.func/', config.outDir); serverEntry = config.build.serverEntry; if (config.output === 'static') { @@ -80,6 +106,9 @@ export default function vercelServerless({ `); } }, + 'astro:build:ssr': async ({ entryPoints }) => { + _entryPoints = entryPoints; + }, 'astro:build:done': async ({ routes }) => { // Merge any includes from `vite.assetsInclude const inc = includeFiles?.map((file) => new URL(file, _config.root)) || []; @@ -98,30 +127,22 @@ export default function vercelServerless({ mergeGlobbedIncludes(_config.vite.assetsInclude); } - // Copy necessary files (e.g. node_modules/) - const { handler } = await copyDependenciesToFunction({ - entry: new URL(serverEntry, buildTempFolder), - outDir: functionFolder, - includeFiles: inc, - excludeFiles: excludeFiles?.map((file) => new URL(file, _config.root)) || [], - }); + const routeDefinitions: { src: string; dest: string }[] = []; - // Remove temporary folder - await removeDir(buildTempFolder); - - // 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', - }); + // Multiple entrypoint support + if(_entryPoints.size) { + for(const [route, entryFile] of _entryPoints) { + const func = basename(entryFile.toString()).replace(/\.mjs$/, ''); + await createFunctionFolder(func, entryFile, inc); + routeDefinitions.push({ + src: route.pattern.source, + dest: func + }); + } + } else { + await createFunctionFolder('render', new URL(serverEntry, buildTempFolder), inc); + routeDefinitions.push({ src: '/.*', dest: 'render' }); + } // Output configuration // https://vercel.com/docs/build-output-api/v3#build-output-configuration @@ -130,12 +151,15 @@ export default function vercelServerless({ routes: [ ...getRedirects(routes, _config), { handle: 'filesystem' }, - { src: '/.*', dest: 'render' }, + ...routeDefinitions ], ...(imageService || imagesConfig ? { images: imagesConfig ? imagesConfig : defaultImageConfig } : {}), }); + + // Remove temporary folder + await removeDir(buildTempFolder); }, }, }; diff --git a/packages/integrations/vercel/test/fixtures/basic/astro.config.mjs b/packages/integrations/vercel/test/fixtures/basic/astro.config.mjs new file mode 100644 index 0000000000..664b64d569 --- /dev/null +++ b/packages/integrations/vercel/test/fixtures/basic/astro.config.mjs @@ -0,0 +1,6 @@ +import { defineConfig } from 'astro/config'; +import vercel from '@astrojs/vercel/serverless'; + +export default defineConfig({ + adapter: vercel() +}); diff --git a/packages/integrations/vercel/test/fixtures/basic/package.json b/packages/integrations/vercel/test/fixtures/basic/package.json new file mode 100644 index 0000000000..89fb910ff8 --- /dev/null +++ b/packages/integrations/vercel/test/fixtures/basic/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/astro-vercel-basic", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/vercel": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/vercel/test/fixtures/basic/src/pages/one.astro b/packages/integrations/vercel/test/fixtures/basic/src/pages/one.astro new file mode 100644 index 0000000000..0c7fb90a73 --- /dev/null +++ b/packages/integrations/vercel/test/fixtures/basic/src/pages/one.astro @@ -0,0 +1,8 @@ + + + One + + +

One

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

Two

+ + diff --git a/packages/integrations/vercel/test/split.test.js b/packages/integrations/vercel/test/split.test.js new file mode 100644 index 0000000000..b89a428be1 --- /dev/null +++ b/packages/integrations/vercel/test/split.test.js @@ -0,0 +1,29 @@ +import { loadFixture } from './test-utils.js'; +import { expect } from 'chai'; + +describe('build: split', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/basic/', + output: 'server', + build: { + split: true, + } + }); + await fixture.build(); + }); + + it('creates separate functions for each page', async () => { + const files = await fixture.readdir('../.vercel/output/functions/') + expect(files.length).to.equal(2); + }); + + it('creates the route definitions in the config.json', async () => { + const json = await fixture.readFile('../.vercel/output/config.json'); + const config = JSON.parse(json); + expect(config.routes).to.have.a.lengthOf(3); + }) +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 76aae821fc..c5649d326d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4902,6 +4902,15 @@ importers: specifier: ^9.2.2 version: 9.2.2 + packages/integrations/vercel/test/fixtures/basic: + dependencies: + '@astrojs/vercel': + specifier: workspace:* + version: link:../../.. + astro: + specifier: workspace:* + version: link:../../../../../astro + packages/integrations/vercel/test/fixtures/image: dependencies: '@astrojs/vercel':