Skip to content

Commit dd5c1d0

Browse files
committed
[WIP] Test new vercel middleware integration method
1 parent 32b9c43 commit dd5c1d0

File tree

14 files changed

+291
-316
lines changed

14 files changed

+291
-316
lines changed

astro.config.mjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import tailwindcss from "@tailwindcss/vite";
66
import starlightOpenAPI from "starlight-openapi";
77
import starlightDocSearch from "@astrojs/starlight-docsearch";
88

9+
import { tsImport } from "tsx/esm/api";
910
import vercel from "@astrojs/vercel";
1011
import remarkMath from "remark-math";
1112
import rehypeKatex from "rehype-katex";
@@ -27,6 +28,11 @@ import { devServerFileWatcher } from "./src/integrations/dev-server-file-watcher
2728
// import { isMoveReferenceEnabled } from "./src/utils/isMoveReferenceEnabled";
2829
// import { rehypeAddDebug } from "./src/plugins";
2930

31+
const vercelMiddlewareIntegration = await tsImport(
32+
"./integrations/vercel-middleware/index.ts",
33+
import.meta.url,
34+
).then((m) => m.default);
35+
3036
const ALGOLIA_APP_ID = ENV.ALGOLIA_APP_ID;
3137
const ALGOLIA_SEARCH_API_KEY = ENV.ALGOLIA_SEARCH_API_KEY;
3238
const ALGOLIA_INDEX_NAME = ENV.ALGOLIA_INDEX_NAME;
@@ -196,6 +202,7 @@ export default defineConfig({
196202
],
197203
},
198204
}),
205+
vercelMiddlewareIntegration(),
199206
],
200207
adapter: process.env.VERCEL
201208
? vercel({
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { fileURLToPath } from "node:url";
2+
import type { AstroConfig, AstroIntegration, IntegrationResolvedRoute } from "astro";
3+
import { getAstroMiddlewarePath } from "./utils/astro";
4+
import { buildMiddlewareMatcherRegexp } from "./utils/matcher";
5+
import { generateEdgeMiddlewareFile } from "./utils/middleware";
6+
import {
7+
generateFunctionConfig,
8+
getFunctionDir,
9+
insertMiddlewareRoute,
10+
} from "./utils/vercelOutput";
11+
12+
const INTEGRATION_NAME = "vercel-middleware";
13+
const VERCEL_ADAPTER_LINK = "[@astrojs/vercel](https://www.npmjs.com/package/@astrojs/vercel)";
14+
const VERCEL_MIDDLEWARE_FUNCTION_NAME = "_middleware";
15+
16+
export default function vercelMiddlewareIntegration() {
17+
let astroConfig: AstroConfig;
18+
let resolvedRoutes: IntegrationResolvedRoute[];
19+
20+
const integration: AstroIntegration = {
21+
name: INTEGRATION_NAME,
22+
hooks: {
23+
"astro:routes:resolved": ({ routes, logger }) => {
24+
logger.info("Resolving routes for Vercel middleware matcher…");
25+
resolvedRoutes = routes;
26+
},
27+
"astro:config:done": ({ config }) => {
28+
astroConfig = config;
29+
},
30+
"astro:build:done": async ({ logger }) => {
31+
if (
32+
!astroConfig.integrations.some((integration) => integration.name === "@astrojs/vercel")
33+
) {
34+
logger.error(`${VERCEL_ADAPTER_LINK} must be installed to use ${INTEGRATION_NAME}.`);
35+
return;
36+
}
37+
38+
if (astroConfig.adapter?.name !== "@astrojs/vercel") {
39+
logger.error(
40+
`${VERCEL_ADAPTER_LINK} must be used as adapter for proper ${INTEGRATION_NAME} work.`,
41+
);
42+
return;
43+
}
44+
45+
const rootDir = fileURLToPath(astroConfig.root);
46+
47+
logger.info("Looking for Astro middleware…");
48+
const astroMiddlewarePath = await getAstroMiddlewarePath(rootDir);
49+
50+
if (!astroMiddlewarePath) {
51+
logger.warn("Astro middleware not found. Skipping Vercel middleware build.");
52+
return;
53+
}
54+
55+
logger.info(`Found middleware file at: ${astroMiddlewarePath}`);
56+
logger.info("Building Vercel middleware…");
57+
logger.info("Compiling edge middleware file…");
58+
const functionDir = getFunctionDir(rootDir, VERCEL_MIDDLEWARE_FUNCTION_NAME);
59+
const middlewareEntrypoint = await generateEdgeMiddlewareFile(
60+
rootDir,
61+
astroMiddlewarePath,
62+
functionDir,
63+
);
64+
logger.info("Creating edge middleware Vercel config file…");
65+
await generateFunctionConfig(functionDir, middlewareEntrypoint);
66+
67+
logger.info("Collecting routes which must be handled by middleware…");
68+
const matcher = buildMiddlewareMatcherRegexp(resolvedRoutes);
69+
70+
logger.info("Inserting generated middleware into vercel output config…");
71+
await insertMiddlewareRoute(rootDir, matcher, VERCEL_MIDDLEWARE_FUNCTION_NAME);
72+
73+
logger.info("Successfully created middleware function for Vercel deployment.");
74+
},
75+
},
76+
};
77+
78+
return integration;
79+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import path from "node:path";
2+
import fs from "fs/promises";
3+
4+
const POSSIBLE_ASTRO_MIDDLEWARE_PATHS = [
5+
"src/middleware.ts",
6+
"src/middleware/index.ts",
7+
"src/middleware.js",
8+
"src/middleware/index.js",
9+
];
10+
11+
export async function getAstroMiddlewarePath(projectRootDir: string) {
12+
for (const possiblePath of POSSIBLE_ASTRO_MIDDLEWARE_PATHS) {
13+
const fullPath = path.join(projectRootDir, possiblePath);
14+
const exists = await fs
15+
.stat(fullPath)
16+
.then(() => true)
17+
.catch(() => false);
18+
19+
if (exists) {
20+
return possiblePath;
21+
}
22+
}
23+
24+
return null;
25+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { IntegrationResolvedRoute } from "astro";
2+
3+
export function buildMiddlewareMatcherRegexp(routes: IntegrationResolvedRoute[]) {
4+
const groupedRoutes = Object.groupBy(routes, (r) => r.origin);
5+
const dontMatchPatterns =
6+
groupedRoutes.internal?.map((r) => stripPatternRegexp(r.patternRegex)) ?? [];
7+
const matchPatterns = groupedRoutes.project?.map((r) => stripPatternRegexp(r.patternRegex)) ?? [];
8+
9+
// The regex is constructed to first negate any paths that match the internal patterns
10+
// and then allow paths that match the project patterns.
11+
// For example it can output such regexp: /^(?!.*\/(_server-islands\/[^\/]+\/?|_image\/?)$)(?:\/(.*?))?\/?)$/;
12+
return `^(?!.*(${dontMatchPatterns.join("|")})$)(?:${matchPatterns.join("|")})$`;
13+
}
14+
15+
const PATTERN_STRIP_LINE_START = /^\^/;
16+
const PATTERN_STRIP_LINE_END = /\$$/;
17+
function stripPatternRegexp(pattern: RegExp) {
18+
return pattern
19+
.toString()
20+
.slice(1, -1)
21+
.replace(PATTERN_STRIP_LINE_START, "")
22+
.replace(PATTERN_STRIP_LINE_END, "");
23+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { builtinModules } from "node:module";
2+
import path from "node:path";
3+
4+
// https://docs.astro.build/en/guides/middleware/
5+
const MIDDLEWARE_ENTRYPOINT = "middleware.mjs";
6+
7+
export async function generateEdgeMiddlewareFile(
8+
projectRootDir: string,
9+
middlewarePath: string,
10+
functionDir: string,
11+
) {
12+
const esbuild = await import("esbuild");
13+
const middlewareModule = getMiddlewareTemplate(middlewarePath);
14+
const outfile = path.join(functionDir, MIDDLEWARE_ENTRYPOINT);
15+
16+
await esbuild.build({
17+
stdin: {
18+
contents: middlewareModule,
19+
resolveDir: projectRootDir,
20+
},
21+
target: "esnext",
22+
platform: "browser",
23+
conditions: ["edge-light", "workerd", "worker"],
24+
outfile,
25+
allowOverwrite: true,
26+
format: "esm",
27+
bundle: true,
28+
minify: false,
29+
plugins: [
30+
{
31+
name: "esbuild-namespace-node-built-in-modules",
32+
setup(build) {
33+
const filter = new RegExp(builtinModules.map((mod) => `(^${mod}$)`).join("|"));
34+
build.onResolve({ filter }, (args) => ({
35+
path: "node:" + args.path,
36+
external: true,
37+
}));
38+
},
39+
},
40+
],
41+
});
42+
43+
return MIDDLEWARE_ENTRYPOINT;
44+
}
45+
46+
function getMiddlewareTemplate(middlewarePath: string) {
47+
return `
48+
import { createContext, trySerializeLocals } from 'astro/middleware';
49+
import { next } from "@vercel/functions";
50+
import { onRequest } from "${middlewarePath}";
51+
52+
export default async function middleware(request, context) {
53+
const url = new URL(request.url);
54+
const ctx = createContext({ request, params: {} });
55+
Object.assign(ctx.locals, { vercel: { edge: context } });
56+
57+
return onRequest(ctx, next);
58+
}`;
59+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import fs from "node:fs/promises";
2+
import path from "node:path";
3+
4+
interface VercelConfig {
5+
version: 3;
6+
routes: {
7+
src: string;
8+
dest?: string;
9+
middlewarePath?: string;
10+
continue?: boolean;
11+
}[];
12+
}
13+
14+
const VERCEL_OUTPUT_DIR = ".vercel/output";
15+
16+
export function getFunctionDir(rootDir: string, functionName: string) {
17+
return path.join(rootDir, VERCEL_OUTPUT_DIR, `functions/${functionName}.func/`);
18+
}
19+
20+
export async function insertMiddlewareRoute(
21+
rootDir: string,
22+
matcher: string,
23+
middlewareName: string,
24+
) {
25+
const vercelConfigPath = path.join(rootDir, VERCEL_OUTPUT_DIR, "config.json");
26+
27+
const vercelConfig = JSON.parse(await fs.readFile(vercelConfigPath, "utf-8")) as VercelConfig;
28+
// the first two are /_astro/*, and filesystem routes
29+
vercelConfig.routes.splice(1, 0, {
30+
src: matcher,
31+
middlewarePath: middlewareName,
32+
continue: true,
33+
});
34+
await fs.writeFile(vercelConfigPath, JSON.stringify(vercelConfig, null, 2));
35+
}
36+
export async function generateFunctionConfig(functionDir: string, entrypoint: string) {
37+
const config = {
38+
runtime: "edge",
39+
deploymentTarget: "v8-worker",
40+
entrypoint,
41+
};
42+
43+
await fs.writeFile(path.join(functionDir, ".vc-config.json"), JSON.stringify(config, null, 2));
44+
}

package.json

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,10 @@
66
"migrate": "cd scripts/migration && pnpm install && pnpm build && pnpm migrate",
77
"audit-sidebar": "tsx scripts/audit-sidebar.ts",
88
"postmigrate": "pnpm format:content",
9-
"prebuild": "pnpm build:middleware-matcher && pnpm build:middleware",
10-
"predev": "pnpm build:middleware-matcher && pnpm build:middleware --watch &",
11-
"dev": "astro dev",
9+
"prebuild": "node ./scripts/generate-middleware-matcher.js",
10+
"dev": "pnpm prebuild && astro dev",
1211
"start": "astro dev",
13-
"build": "pnpm prebuild && astro build && pnpm build:generate-middleware-function",
14-
"build:middleware-matcher": "node ./scripts/generate-middleware-matcher.js",
15-
"build:middleware": "node ./scripts/generate-middleware.js",
16-
"build:generate-middleware-function": "node ./scripts/generate-middleware-function.js",
12+
"build": "pnpm prebuild && astro build",
1713
"preview": "astro preview",
1814
"astro": "astro",
1915
"check": "astro check",
@@ -58,6 +54,7 @@
5854
"@types/react-dom": "^19.1.6",
5955
"@vercel/analytics": "^1.5.0",
6056
"@vercel/edge": "^1.2.2",
57+
"@vercel/functions": "^2.2.13",
6158
"@vercel/og": "^0.6.8",
6259
"@vercel/speed-insights": "^1.2.0",
6360
"astro": "^5.13.2",
@@ -101,6 +98,7 @@
10198
"@types/node": "^22.16.5",
10299
"@vercel/node": "^5.3.6",
103100
"astro-eslint-parser": "^1.2.2",
101+
"esbuild": ">=0.25.0",
104102
"eslint": "^9.31.0",
105103
"eslint-config-flat-gitignore": "^2.1.0",
106104
"eslint-config-prettier": "^10.1.8",

pnpm-lock.yaml

Lines changed: 14 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)