Skip to content

Commit

Permalink
Split support in the Vercel Serverless adapter (#7514)
Browse files Browse the repository at this point in the history
* start of vercel split support

* Split Mode with the Vercel Adapter

* Write routes into the config.json

* Add a changeset

* Add docs

* Better changeset
  • Loading branch information
matthewp committed Jun 29, 2023
1 parent 5df4853 commit 154af8f
Show file tree
Hide file tree
Showing 10 changed files with 160 additions and 28 deletions.
21 changes: 21 additions & 0 deletions .changeset/tricky-snails-poke.md
Original file line number Diff line number Diff line change
@@ -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
}
});
```
18 changes: 18 additions & 0 deletions packages/integrations/vercel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
2 changes: 1 addition & 1 deletion packages/integrations/vercel/src/lib/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import nodePath from 'node:path';
import { fileURLToPath } from 'node:url';

export async function writeJson<T>(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) {
Expand Down
78 changes: 51 additions & 27 deletions packages/integrations/vercel/src/serverless/adapter.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';

Expand Down Expand Up @@ -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<RouteData, URL>;

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,
Expand Down Expand Up @@ -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') {
Expand All @@ -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)) || [];
Expand All @@ -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)) || [],
});

// 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',
});
const routeDefinitions: { src: string; dest: string }[] = [];

// 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
Expand All @@ -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);
},
},
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/serverless';

export default defineConfig({
adapter: vercel()
});
9 changes: 9 additions & 0 deletions packages/integrations/vercel/test/fixtures/basic/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "@test/astro-vercel-basic",
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/vercel": "workspace:*",
"astro": "workspace:*"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<html>
<head>
<title>One</title>
</head>
<body>
<h1>One</h1>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<html>
<head>
<title>Two</title>
</head>
<body>
<h1>Two</h1>
</body>
</html>
29 changes: 29 additions & 0 deletions packages/integrations/vercel/test/split.test.js
Original file line number Diff line number Diff line change
@@ -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);
})
});
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 154af8f

Please sign in to comment.