summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/integrations/vercel/README.md74
-rw-r--r--packages/integrations/vercel/package.json5
-rw-r--r--packages/integrations/vercel/src/lib/fs.ts4
-rw-r--r--packages/integrations/vercel/src/lib/nft.ts7
-rw-r--r--packages/integrations/vercel/src/serverless/adapter.ts35
-rw-r--r--packages/integrations/vercel/src/serverless/entrypoint.ts10
-rw-r--r--packages/integrations/vercel/src/serverless/middleware.ts81
-rw-r--r--packages/integrations/vercel/test/edge-middleware.test.js30
-rw-r--r--packages/integrations/vercel/test/edge-middleware.test.js.snap40
-rw-r--r--packages/integrations/vercel/test/fixtures/middleware/astro.config.mjs10
-rw-r--r--packages/integrations/vercel/test/fixtures/middleware/package.json9
-rw-r--r--packages/integrations/vercel/test/fixtures/middleware/src/middleware.js8
-rw-r--r--packages/integrations/vercel/test/fixtures/middleware/src/pages/index.astro0
-rw-r--r--packages/integrations/vercel/test/fixtures/middleware/src/vercel-edge-middleware.js5
14 files changed, 307 insertions, 11 deletions
diff --git a/packages/integrations/vercel/README.md b/packages/integrations/vercel/README.md
index 41a5591dc..19d841a2f 100644
--- a/packages/integrations/vercel/README.md
+++ b/packages/integrations/vercel/README.md
@@ -233,9 +233,9 @@ export default defineConfig({
});
```
-### Vercel Middleware
+### Vercel Edge 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).
+You can use Vercel Edge middleware to intercept a request and redirect before sending a response. Vercel middleware can run for Edge, SSR, and Static deployments. You may not need to install this package for your middleware. `@vercel/edge` is only required to use some middleware features such as geolocation. For more information see [Vercel’s middleware documentation](https://vercel.com/docs/concepts/functions/edge-middleware).
1. Add a `middleware.js` file to the root of your project:
@@ -262,6 +262,76 @@ You can use Vercel middleware to intercept a request and redirect before sending
> **Warning**
> **Trying to rewrite?** Currently rewriting a request with middleware only works for static files.
+### Vercel Edge Middleware with Astro middleware
+
+The `@astrojs/vercel/serverless` adapter can automatically create the Vercel Edge middleware from an Astro middleware in your code base.
+
+This is an opt-in feature, and the `build.excludeMiddleware` option needs to be set to `true`:
+
+```js
+// astro.config.mjs
+import {defineConfig} from "astro/config";
+import vercel from "@astrojs/vercel";
+export default defineConfig({
+ output: "server",
+ adapter: vercel(),
+ build: {
+ excludeMiddleware: true
+ }
+})
+```
+
+Optionally, you can create a file recognized by the adapter named `vercel-edge-middleware.(js|ts)` in the [`srcDir`](https://docs.astro.build/en/reference/configuration-reference/#srcdir) folder to create [`Astro.locals`](https://docs.astro.build/en/reference/api-reference/#astrolocals).
+
+Typings requires the [`@vercel/edge`](https://www.npmjs.com/package/@vercel/edge) package.
+
+```js
+// src/vercel-edge-middleware.js
+/**
+ *
+ * @param options.request {Request}
+ * @param options.context {import("@vercel/edge").RequestContext}
+ * @returns {object}
+ */
+export default function({ request, context }) {
+ // do something with request and context
+ return {
+ title: "Spider-man's blog"
+ }
+}
+```
+
+If you use TypeScript, you can type the function as follows:
+
+```ts
+// src/vercel-edge-middleware.ts
+import type {RequestContext} from "@vercel/edge";
+
+export default function ({request, context}: { request: Request, context: RequestContext }) {
+ // do something with request and context
+ return {
+ title: "Spider-man's blog"
+ }
+}
+```
+
+The data returned by this function will be passed to Astro middleware.
+
+The function:
+- must export a **default** function;
+- must **return** an `object`;
+- accepts an object with a `request` and `context` as properties;
+- `request` is typed as [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request);
+- `context` is typed as [`RequestContext`](https://vercel.com/docs/concepts/functions/edge-functions/vercel-edge-package#requestcontext);
+
+#### Limitations and constraints
+
+When you opt in to this feature, there are few constraints to note:
+- The Vercel Edge middleware will always be the **first** function to receive the `Request` and the last function to receive `Response`. This an architectural constraint that follows the [boundaries set by Vercel](https://vercel.com/docs/concepts/functions/edge-middleware).
+- Only `request` and `context` may be used to produce an `Astro.locals` object. Operations like redirects, etc. should be delegated to Astro middleware.
+- `Astro.locals` **must be serializable**. Failing to do so will result in a **runtime error**. This means that you **cannot** store complex types like `Map`, `function`, `Set`, etc.
+
+
## Troubleshooting
**A few known complex packages (example: [puppeteer](https://github.com/puppeteer/puppeteer)) do not support bundling and therefore will not work properly with this adapter.** By default, Vercel doesn't include npm installed files & packages from your project's `./node_modules` folder. To address this, the `@astrojs/vercel` adapter automatically bundles your final build output using `esbuild`.
diff --git a/packages/integrations/vercel/package.json b/packages/integrations/vercel/package.json
index 3899b9069..a039ee5a8 100644
--- a/packages/integrations/vercel/package.json
+++ b/packages/integrations/vercel/package.json
@@ -64,10 +64,13 @@
},
"devDependencies": {
"@types/set-cookie-parser": "^2.4.2",
+ "@vercel/edge": "^0.3.4",
"astro": "workspace:*",
"astro-scripts": "workspace:*",
"chai": "^4.3.7",
+ "chai-jest-snapshot": "^2.0.0",
"cheerio": "1.0.0-rc.12",
- "mocha": "^9.2.2"
+ "mocha": "^9.2.2",
+ "rollup": "^3.20.1"
}
}
diff --git a/packages/integrations/vercel/src/lib/fs.ts b/packages/integrations/vercel/src/lib/fs.ts
index 18fbe85d2..51b12d52f 100644
--- a/packages/integrations/vercel/src/lib/fs.ts
+++ b/packages/integrations/vercel/src/lib/fs.ts
@@ -86,3 +86,7 @@ export async function copyFilesToFunction(
return commonAncestor;
}
+
+export async function writeFile(path: PathLike, content: string) {
+ await fs.writeFile(path, content, { encoding: 'utf-8' });
+}
diff --git a/packages/integrations/vercel/src/lib/nft.ts b/packages/integrations/vercel/src/lib/nft.ts
index 46604db90..752f87251 100644
--- a/packages/integrations/vercel/src/lib/nft.ts
+++ b/packages/integrations/vercel/src/lib/nft.ts
@@ -1,7 +1,5 @@
-import { nodeFileTrace } from '@vercel/nft';
import { relative as relativePath } from 'node:path';
import { fileURLToPath } from 'node:url';
-
import { copyFilesToFunction } from './fs.js';
export async function copyDependenciesToFunction({
@@ -23,6 +21,11 @@ export async function copyDependenciesToFunction({
base = new URL('../', base);
}
+ // The Vite bundle includes an import to `@vercel/nft` for some reason,
+ // and that trips up `@vercel/nft` itself during the adapter build. Using a
+ // dynamic import helps prevent the issue.
+ // TODO: investigate why
+ const { nodeFileTrace } = await import('@vercel/nft');
const result = await nodeFileTrace([entryPath], {
base: fileURLToPath(base),
});
diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts
index 007fb8537..9d799a7bf 100644
--- a/packages/integrations/vercel/src/serverless/adapter.ts
+++ b/packages/integrations/vercel/src/serverless/adapter.ts
@@ -13,8 +13,12 @@ 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 { generateEdgeMiddleware } from './middleware.js';
+import { fileURLToPath } from 'node:url';
const PACKAGE_NAME = '@astrojs/vercel/serverless';
+export const ASTRO_LOCALS_HEADER = 'x-astro-locals';
+export const VERCEL_EDGE_MIDDLEWARE_FILE = 'vercel-edge-middleware';
function getAdapter(): AstroAdapter {
return {
@@ -70,6 +74,8 @@ export default function vercelServerless({
});
}
+ const filesToInclude = includeFiles?.map((file) => new URL(file, _config.root)) || [];
+
return {
name: PACKAGE_NAME,
hooks: {
@@ -106,17 +112,32 @@ export default function vercelServerless({
`);
}
},
- 'astro:build:ssr': async ({ entryPoints }) => {
+
+ 'astro:build:ssr': async ({ entryPoints, middlewareEntryPoint }) => {
_entryPoints = entryPoints;
+ if (middlewareEntryPoint) {
+ const outPath = fileURLToPath(buildTempFolder);
+ const vercelEdgeMiddlewareHandlerPath = new URL(
+ VERCEL_EDGE_MIDDLEWARE_FILE,
+ _config.srcDir
+ );
+ const bundledMiddlewarePath = await generateEdgeMiddleware(
+ middlewareEntryPoint,
+ outPath,
+ vercelEdgeMiddlewareHandlerPath
+ );
+ // let's tell the adapter that we need to save this file
+ filesToInclude.push(bundledMiddlewarePath);
+ }
},
+
'astro:build:done': async ({ routes }) => {
// Merge any includes from `vite.assetsInclude
- const inc = includeFiles?.map((file) => new URL(file, _config.root)) || [];
if (_config.vite.assetsInclude) {
const mergeGlobbedIncludes = (globPattern: unknown) => {
if (typeof globPattern === 'string') {
const entries = glob.sync(globPattern).map((p) => pathToFileURL(p));
- inc.push(...entries);
+ filesToInclude.push(...entries);
} else if (Array.isArray(globPattern)) {
for (const pattern of globPattern) {
mergeGlobbedIncludes(pattern);
@@ -133,14 +154,18 @@ export default function vercelServerless({
if (_entryPoints.size) {
for (const [route, entryFile] of _entryPoints) {
const func = basename(entryFile.toString()).replace(/\.mjs$/, '');
- await createFunctionFolder(func, entryFile, inc);
+ await createFunctionFolder(func, entryFile, filesToInclude);
routeDefinitions.push({
src: route.pattern.source,
dest: func,
});
}
} else {
- await createFunctionFolder('render', new URL(serverEntry, buildTempFolder), inc);
+ await createFunctionFolder(
+ 'render',
+ new URL(serverEntry, buildTempFolder),
+ filesToInclude
+ );
routeDefinitions.push({ src: '/.*', dest: 'render' });
}
diff --git a/packages/integrations/vercel/src/serverless/entrypoint.ts b/packages/integrations/vercel/src/serverless/entrypoint.ts
index 71ad2bfae..3c0e22a28 100644
--- a/packages/integrations/vercel/src/serverless/entrypoint.ts
+++ b/packages/integrations/vercel/src/serverless/entrypoint.ts
@@ -4,6 +4,7 @@ import { App } from 'astro/app';
import type { IncomingMessage, ServerResponse } from 'node:http';
import { getRequest, setResponse } from './request-transform';
+import { ASTRO_LOCALS_HEADER } from './adapter';
polyfill(globalThis, {
exclude: 'window document',
@@ -28,7 +29,14 @@ export const createExports = (manifest: SSRManifest) => {
return res.end('Not found');
}
- await setResponse(app, res, await app.render(request, routeData));
+ let locals = {};
+ if (request.headers.has(ASTRO_LOCALS_HEADER)) {
+ let localsAsString = request.headers.get(ASTRO_LOCALS_HEADER);
+ if (localsAsString) {
+ locals = JSON.parse(localsAsString);
+ }
+ }
+ await setResponse(app, res, await app.render(request, routeData, locals));
};
return { default: handler };
diff --git a/packages/integrations/vercel/src/serverless/middleware.ts b/packages/integrations/vercel/src/serverless/middleware.ts
new file mode 100644
index 000000000..2f05756c6
--- /dev/null
+++ b/packages/integrations/vercel/src/serverless/middleware.ts
@@ -0,0 +1,81 @@
+import { fileURLToPath, pathToFileURL } from 'node:url';
+import { join } from 'node:path';
+import { ASTRO_LOCALS_HEADER } from './adapter.js';
+import { existsSync } from 'fs';
+
+/**
+ * It generates the Vercel Edge Middleware file.
+ *
+ * It creates a temporary file, the edge middleware, with some dynamic info.
+ *
+ * Then this file gets bundled with esbuild. The bundle phase will inline the Astro middleware code.
+ *
+ * @param astroMiddlewareEntryPoint
+ * @param outPath
+ * @returns {Promise<URL>} The path to the bundled file
+ */
+export async function generateEdgeMiddleware(
+ astroMiddlewareEntryPointPath: URL,
+ outPath: string,
+ vercelEdgeMiddlewareHandlerPath: URL
+): Promise<URL> {
+ const entryPointPathURLAsString = JSON.stringify(
+ fileURLToPath(astroMiddlewareEntryPointPath).replace(/\\/g, '/')
+ );
+
+ const code = edgeMiddlewareTemplate(entryPointPathURLAsString, vercelEdgeMiddlewareHandlerPath);
+ // https://vercel.com/docs/concepts/functions/edge-middleware#create-edge-middleware
+ const bundledFilePath = join(outPath, 'middleware.mjs');
+ const esbuild = await import('esbuild');
+ await esbuild.build({
+ stdin: {
+ contents: code,
+ resolveDir: process.cwd(),
+ },
+ target: 'es2020',
+ platform: 'browser',
+ // https://runtime-keys.proposal.wintercg.org/#edge-light
+ conditions: ['edge-light', 'worker', 'browser'],
+ external: ['astro/middleware'],
+ outfile: bundledFilePath,
+ allowOverwrite: true,
+ format: 'esm',
+ bundle: true,
+ minify: false,
+ });
+ return pathToFileURL(bundledFilePath);
+}
+
+function edgeMiddlewareTemplate(middlewarePath: string, vercelEdgeMiddlewareHandlerPath: URL) {
+ const filePathEdgeMiddleware = fileURLToPath(vercelEdgeMiddlewareHandlerPath);
+ let handlerTemplateImport = '';
+ let handlerTemplateCall = '{}';
+ if (existsSync(filePathEdgeMiddleware) + '.js' || existsSync(filePathEdgeMiddleware) + '.ts') {
+ const stringified = JSON.stringify(filePathEdgeMiddleware.replace(/\\/g, '/'));
+ handlerTemplateImport = `import handler from ${stringified}`;
+ handlerTemplateCall = `handler({ request, context })`;
+ } else {
+ }
+ return `
+ ${handlerTemplateImport}
+import { onRequest } from ${middlewarePath};
+import { createContext, trySerializeLocals } from 'astro/middleware';
+export default async function middleware(request, context) {
+ const url = new URL(request.url);
+ const ctx = createContext({
+ request,
+ params: {}
+ });
+ ctx.locals = ${handlerTemplateCall};
+ const next = async () => {
+ const response = await fetch(url, {
+ headers: {
+ ${JSON.stringify(ASTRO_LOCALS_HEADER)}: trySerializeLocals(ctx.locals)
+ }
+ });
+ return response;
+ };
+
+ return onRequest(ctx, next);
+}`;
+}
diff --git a/packages/integrations/vercel/test/edge-middleware.test.js b/packages/integrations/vercel/test/edge-middleware.test.js
new file mode 100644
index 000000000..dd4b25b67
--- /dev/null
+++ b/packages/integrations/vercel/test/edge-middleware.test.js
@@ -0,0 +1,30 @@
+import { loadFixture } from './test-utils.js';
+import { expect, use } from 'chai';
+import chaiJestSnapshot from 'chai-jest-snapshot';
+
+use(chaiJestSnapshot);
+
+describe('Serverless prerender', () => {
+ /** @type {import('./test-utils').Fixture} */
+ let fixture;
+
+ beforeEach(function () {
+ chaiJestSnapshot.configureUsingMochaContext(this);
+ });
+
+ before(async () => {
+ chaiJestSnapshot.resetSnapshotRegistry();
+ fixture = await loadFixture({
+ root: './fixtures/middleware/',
+ });
+ });
+
+ it('build successfully the middleware edge file', async () => {
+ await fixture.build();
+ const contents = await fixture.readFile(
+ // this is abysmal...
+ '../.vercel/output/functions/render.func/packages/integrations/vercel/test/fixtures/middleware/dist/middleware.mjs'
+ );
+ expect(contents).to.matchSnapshot();
+ });
+});
diff --git a/packages/integrations/vercel/test/edge-middleware.test.js.snap b/packages/integrations/vercel/test/edge-middleware.test.js.snap
new file mode 100644
index 000000000..fe82ccff9
--- /dev/null
+++ b/packages/integrations/vercel/test/edge-middleware.test.js.snap
@@ -0,0 +1,40 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Serverless prerender build successfully the middleware edge file 1`] = `
+"// test/fixtures/middleware/src/vercel-edge-middleware.js
+function vercel_edge_middleware_default({ request, context }) {
+ return {
+ title: \\"Hello world\\"
+ };
+}
+
+// test/fixtures/middleware/dist/middleware2.mjs
+var onRequest = async (context, next) => {
+ const response = await next();
+ return response;
+};
+
+// <stdin>
+import { createContext, trySerializeLocals } from \\"astro/middleware\\";
+async function middleware(request, context) {
+ const url = new URL(request.url);
+ const ctx = createContext({
+ request,
+ params: {}
+ });
+ ctx.locals = vercel_edge_middleware_default({ request, context });
+ const next = async () => {
+ const response = await fetch(url, {
+ headers: {
+ \\"x-astro-locals\\": trySerializeLocals(ctx.locals)
+ }
+ });
+ return response;
+ };
+ return onRequest(ctx, next);
+}
+export {
+ middleware as default
+};
+"
+`;
diff --git a/packages/integrations/vercel/test/fixtures/middleware/astro.config.mjs b/packages/integrations/vercel/test/fixtures/middleware/astro.config.mjs
new file mode 100644
index 000000000..321a8bde3
--- /dev/null
+++ b/packages/integrations/vercel/test/fixtures/middleware/astro.config.mjs
@@ -0,0 +1,10 @@
+import {defineConfig} from "astro/config";
+import vercel from "@astrojs/vercel/serverless";
+
+export default defineConfig({
+ adapter: vercel(),
+ build: {
+ excludeMiddleware: true
+ },
+ output: 'server'
+}); \ No newline at end of file
diff --git a/packages/integrations/vercel/test/fixtures/middleware/package.json b/packages/integrations/vercel/test/fixtures/middleware/package.json
new file mode 100644
index 000000000..9ba60852d
--- /dev/null
+++ b/packages/integrations/vercel/test/fixtures/middleware/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@test/vercel-edge-middleware",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@astrojs/vercel": "workspace:*",
+ "astro": "workspace:*"
+ }
+}
diff --git a/packages/integrations/vercel/test/fixtures/middleware/src/middleware.js b/packages/integrations/vercel/test/fixtures/middleware/src/middleware.js
new file mode 100644
index 000000000..349a0aa79
--- /dev/null
+++ b/packages/integrations/vercel/test/fixtures/middleware/src/middleware.js
@@ -0,0 +1,8 @@
+/**
+ * @type {import("astro").MiddlewareResponseHandler}
+ */
+export const onRequest = async (context, next) => {
+ const test = 'something';
+ const response = await next();
+ return response;
+};
diff --git a/packages/integrations/vercel/test/fixtures/middleware/src/pages/index.astro b/packages/integrations/vercel/test/fixtures/middleware/src/pages/index.astro
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/packages/integrations/vercel/test/fixtures/middleware/src/pages/index.astro
diff --git a/packages/integrations/vercel/test/fixtures/middleware/src/vercel-edge-middleware.js b/packages/integrations/vercel/test/fixtures/middleware/src/vercel-edge-middleware.js
new file mode 100644
index 000000000..bf69edb3e
--- /dev/null
+++ b/packages/integrations/vercel/test/fixtures/middleware/src/vercel-edge-middleware.js
@@ -0,0 +1,5 @@
+export default function ({ request, context }) {
+ return {
+ title: 'Hello world',
+ };
+}