From 5c7d8ca0bc1aa223adc9b687635a91fc3fe1fb2b Mon Sep 17 00:00:00 2001 From: lilnasy <69170106+lilnasy@users.noreply.github.com> Date: Fri, 29 Dec 2023 22:34:55 +0000 Subject: [PATCH 01/11] create vercel edge middleware remove getVercelOutput --- packages/integrations/vercel/src/lib/fs.ts | 2 - .../vercel/src/serverless/adapter.ts | 103 +++++++++++------- .../vercel/src/serverless/middleware.ts | 27 ++--- .../integrations/vercel/src/static/adapter.ts | 8 +- 4 files changed, 80 insertions(+), 60 deletions(-) diff --git a/packages/integrations/vercel/src/lib/fs.ts b/packages/integrations/vercel/src/lib/fs.ts index b3dadcfc2ed5..152ee7d7c0d3 100644 --- a/packages/integrations/vercel/src/lib/fs.ts +++ b/packages/integrations/vercel/src/lib/fs.ts @@ -31,8 +31,6 @@ export async function getFilesFromFolder(dir: URL) { return files; } -export const getVercelOutput = (root: URL) => new URL('./.vercel/output/', root); - /** * Copies files into a folder keeping the folder structure intact. * The resulting file tree will start at the common ancestor. diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index 5a3c92b02abb..51a066cf897a 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -8,14 +8,14 @@ import type { import { AstroError } from 'astro/errors'; import glob from 'fast-glob'; import { basename } from 'node:path'; -import { fileURLToPath, pathToFileURL } from 'node:url'; +import { pathToFileURL } from 'node:url'; import { getAstroImageConfig, getDefaultImageConfig, type DevImageService, type VercelImageConfig, } from '../image/shared.js'; -import { getVercelOutput, removeDir, writeJson } from '../lib/fs.js'; +import { removeDir, writeJson } from '../lib/fs.js'; import { copyDependenciesToFunction } from '../lib/nft.js'; import { getRedirects } from '../lib/redirects.js'; import { @@ -111,8 +111,8 @@ export interface VercelServerlessConfig { export default function vercelServerless({ webAnalytics, speedInsights, - includeFiles, - excludeFiles = [], + includeFiles: _includeFiles = [], + excludeFiles: _excludeFiles = [], imageService, imagesConfig, devImageService = 'sharp', @@ -133,6 +133,7 @@ export default function vercelServerless({ let buildTempFolder: URL; let serverEntry: string; let _entryPoints: Map; + let _middlewareEntryPoint: URL | undefined; // Extra files to be merged with `includeFiles` during build const extraFilesToInclude: URL[] = []; @@ -162,13 +163,12 @@ export default function vercelServerless({ if (command === 'build' && speedInsights?.enabled) { injectScript('page', 'import "@astrojs/vercel/speed-insights"'); } - const outDir = getVercelOutput(config.root); + updateConfig({ - outDir, + outDir: new URL('./.vercel/output/', config.root), build: { - serverEntry: 'entry.mjs', - client: new URL('./static/', outDir), - server: new URL('./dist/', config.root), + client: new URL('./.vercel/output/static/', config.root), + server: new URL('./.vercel/output/_functions/', config.root), redirects: false, }, vite: { @@ -195,7 +195,9 @@ export default function vercelServerless({ `\tYou can set functionPerRoute: false to prevent surpassing the limit.\n` ); } + setAdapter(getAdapter({ functionPerRoute, edgeMiddleware })); + _config = config; buildTempFolder = config.build.server; serverEntry = config.build.serverEntry; @@ -208,20 +210,7 @@ export default function vercelServerless({ }, '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 - extraFilesToInclude.push(bundledMiddlewarePath); - } + _middlewareEntryPoint = middlewareEntryPoint; }, 'astro:build:done': async ({ routes, logger }) => { // Merge any includes from `vite.assetsInclude @@ -240,9 +229,14 @@ export default function vercelServerless({ mergeGlobbedIncludes(_config.vite.assetsInclude); } - const routeDefinitions: { src: string; dest: string }[] = []; - const filesToInclude = includeFiles?.map((file) => new URL(file, _config.root)) || []; - filesToInclude.push(...extraFilesToInclude); + const routeDefinitions: Array<{ + src: string + dest: string + middlewarePath?: string + }> = []; + + const includeFiles = _includeFiles.map((file) => new URL(file, _config.root)).concat(extraFilesToInclude); + const excludeFiles = _excludeFiles.map((file) => new URL(file, _config.root)); const runtime = getRuntime(process, logger); @@ -267,7 +261,7 @@ export default function vercelServerless({ config: _config, logger, NTF_CACHE, - includeFiles: filesToInclude, + includeFiles, excludeFiles, maxDuration, }); @@ -284,7 +278,7 @@ export default function vercelServerless({ config: _config, logger, NTF_CACHE, - includeFiles: filesToInclude, + includeFiles, excludeFiles, maxDuration, }); @@ -293,9 +287,17 @@ export default function vercelServerless({ routeDefinitions.push({ src: route.pattern.source, dest: 'render', + middlewarePath: _middlewareEntryPoint ? "_middleware" : undefined }); } } + if (_middlewareEntryPoint) { + await createMiddlewareFolder({ + functionName: '_middleware', + entry: _middlewareEntryPoint, + config: _config, + }); + } const fourOhFourRoute = routes.find((route) => route.pathname === '/404'); // Output configuration // https://vercel.com/docs/build-output-api/v3#build-output-configuration @@ -345,6 +347,31 @@ export default function vercelServerless({ type Runtime = `nodejs${string}.x`; +interface CreateMiddlewareFolderArgs { + config: AstroConfig + entry: URL + functionName: string +} + +async function createMiddlewareFolder({ + functionName, + entry, + config, +}: CreateMiddlewareFolderArgs) { + const functionFolder = new URL(`./functions/${functionName}.func/`, config.outDir); + + await generateEdgeMiddleware( + entry, + new URL(VERCEL_EDGE_MIDDLEWARE_FILE, config.srcDir), + new URL('./middleware.mjs', functionFolder), + ) + + await writeJson(new URL(`./.vc-config.json`, functionFolder), { + runtime: 'edge', + entrypoint: 'middleware.mjs', + }); +} + interface CreateFunctionFolderArgs { functionName: string; runtime: Runtime; @@ -353,7 +380,7 @@ interface CreateFunctionFolderArgs { logger: AstroIntegrationLogger; NTF_CACHE: any; includeFiles: URL[]; - excludeFiles: string[]; + excludeFiles: URL[]; maxDuration: number | undefined; } @@ -379,7 +406,7 @@ async function createFunctionFolder({ entry, outDir: functionFolder, includeFiles, - excludeFiles: excludeFiles.map((file) => new URL(file, config.root)), + excludeFiles, logger, }, NTF_CACHE @@ -430,14 +457,12 @@ function getRuntime(process: NodeJS.Process, logger: AstroIntegrationLogger): Ru `\tConsider upgrading your local version to 18.\n` ); return `nodejs${major}.x`; - } else { - logger.warn( - `\n` + - `\tThe local Node.js version (${major}) is not supported by Vercel Serverless Functions.\n` + - `\tYour project will use Node.js 18 as the runtime instead.\n` + - `\tConsider switching your local version to 18.\n` - ); - return 'nodejs18.x'; } - return `nodejs${major}.x`; + logger.warn( + `\n` + + `\tThe local Node.js version (${major}) is not supported by Vercel Serverless Functions.\n` + + `\tYour project will use Node.js 18 as the runtime instead.\n` + + `\tConsider switching your local version to 18.\n` + ); + return 'nodejs18.x'; } diff --git a/packages/integrations/vercel/src/serverless/middleware.ts b/packages/integrations/vercel/src/serverless/middleware.ts index b3ada80d15bf..b61380ddc030 100644 --- a/packages/integrations/vercel/src/serverless/middleware.ts +++ b/packages/integrations/vercel/src/serverless/middleware.ts @@ -1,5 +1,4 @@ import { existsSync } from 'node:fs'; -import { join } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { ASTRO_LOCALS_HEADER } from './adapter.js'; @@ -16,16 +15,12 @@ import { ASTRO_LOCALS_HEADER } from './adapter.js'; */ export async function generateEdgeMiddleware( astroMiddlewareEntryPointPath: URL, - outPath: string, - vercelEdgeMiddlewareHandlerPath: URL + vercelEdgeMiddlewareHandlerPath: URL, + outPath: URL, ): Promise { - const entryPointPathURLAsString = JSON.stringify( - fileURLToPath(astroMiddlewareEntryPointPath).replace(/\\/g, '/') - ); - - const code = edgeMiddlewareTemplate(entryPointPathURLAsString, vercelEdgeMiddlewareHandlerPath); + const code = edgeMiddlewareTemplate(astroMiddlewareEntryPointPath, vercelEdgeMiddlewareHandlerPath); // https://vercel.com/docs/concepts/functions/edge-middleware#create-edge-middleware - const bundledFilePath = join(outPath, 'middleware.mjs'); + const bundledFilePath = fileURLToPath(outPath); const esbuild = await import('esbuild'); await esbuild.build({ stdin: { @@ -36,7 +31,6 @@ export async function generateEdgeMiddleware( platform: 'browser', // https://runtime-keys.proposal.wintercg.org/#edge-light conditions: ['edge-light', 'worker', 'browser'], - external: ['astro/middleware'], outfile: bundledFilePath, allowOverwrite: true, format: 'esm', @@ -46,7 +40,10 @@ export async function generateEdgeMiddleware( return pathToFileURL(bundledFilePath); } -function edgeMiddlewareTemplate(middlewarePath: string, vercelEdgeMiddlewareHandlerPath: URL) { +function edgeMiddlewareTemplate(astroMiddlewareEntryPointPath: URL, vercelEdgeMiddlewareHandlerPath: URL) { + const middlewarePath = JSON.stringify( + fileURLToPath(astroMiddlewareEntryPointPath).replace(/\\/g, '/') + ); const filePathEdgeMiddleware = fileURLToPath(vercelEdgeMiddlewareHandlerPath); let handlerTemplateImport = ''; let handlerTemplateCall = '{}'; @@ -68,12 +65,12 @@ export default async function middleware(request, context) { }); ctx.locals = ${handlerTemplateCall}; const next = async () => { - const response = await fetch(url, { + return new Response(null, { headers: { - ${JSON.stringify(ASTRO_LOCALS_HEADER)}: trySerializeLocals(ctx.locals) + 'x-middleware-next': '1', + 'x-astro-locals': trySerializeLocals(ctx.locals) } - }); - return response; + }) }; return onRequest(ctx, next); diff --git a/packages/integrations/vercel/src/static/adapter.ts b/packages/integrations/vercel/src/static/adapter.ts index 5924af759a5b..7bc793e40e8f 100644 --- a/packages/integrations/vercel/src/static/adapter.ts +++ b/packages/integrations/vercel/src/static/adapter.ts @@ -6,7 +6,7 @@ import { type DevImageService, type VercelImageConfig, } from '../image/shared.js'; -import { emptyDir, getVercelOutput, writeJson } from '../lib/fs.js'; +import { emptyDir, writeJson } from '../lib/fs.js'; import { isServerLikeOutput } from '../lib/prerender.js'; import { getRedirects } from '../lib/redirects.js'; import { @@ -79,7 +79,7 @@ export default function vercelStatic({ if (command === 'build' && speedInsights?.enabled) { injectScript('page', 'import "@astrojs/vercel/speed-insights"'); } - const outDir = new URL('./static/', getVercelOutput(config.root)); + const outDir = new URL('./.vercel/output/static/', config.root); updateConfig({ outDir, build: { @@ -110,12 +110,12 @@ export default function vercelStatic({ // Ensure to have `.vercel/output` empty. // This is because, when building to static, outDir = .vercel/output/static/, // so .vercel/output itself won't get cleaned. - await emptyDir(getVercelOutput(_config.root)); + await emptyDir(new URL('./.vercel/output/', _config.root)); }, 'astro:build:done': async ({ routes }) => { // Output configuration // https://vercel.com/docs/build-output-api/v3#build-output-configuration - await writeJson(new URL(`./config.json`, getVercelOutput(_config.root)), { + await writeJson(new URL('./.vercel/output/config.json', _config.root), { version: 3, routes: [ ...getRedirects(routes, _config), From b014bb194037b31b4fb66d1d91d10c62d12d4d02 Mon Sep 17 00:00:00 2001 From: lilnasy <69170106+lilnasy@users.noreply.github.com> Date: Wed, 17 Jan 2024 22:38:47 +0000 Subject: [PATCH 02/11] handle node built-in modules --- .../integrations/vercel/src/serverless/adapter.ts | 13 +++++-------- .../vercel/src/serverless/middleware.ts | 9 ++++++++- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index 51a066cf897a..4f9111e1f2b1 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -438,15 +438,18 @@ function getRuntime(process: NodeJS.Process, logger: AstroIntegrationLogger): Ru `\tYour project will use Node.js 18 as the runtime instead.\n` + `\tConsider switching your local version to 18.\n` ); + return 'nodejs18.x'; } if (support.status === 'current') { return `nodejs${major}.x`; - } else if (support?.status === 'beta') { + } + if (support.status === 'beta') { logger.warn( `Your project is being built for Node.js ${major} as the runtime, which is currently in beta for Vercel Serverless Functions.` ); return `nodejs${major}.x`; - } else if (support.status === 'deprecated') { + } + if (support.status === 'deprecated') { const removeDate = new Intl.DateTimeFormat(undefined, { dateStyle: 'long' }).format( support.removal ); @@ -458,11 +461,5 @@ function getRuntime(process: NodeJS.Process, logger: AstroIntegrationLogger): Ru ); return `nodejs${major}.x`; } - logger.warn( - `\n` + - `\tThe local Node.js version (${major}) is not supported by Vercel Serverless Functions.\n` + - `\tYour project will use Node.js 18 as the runtime instead.\n` + - `\tConsider switching your local version to 18.\n` - ); return 'nodejs18.x'; } diff --git a/packages/integrations/vercel/src/serverless/middleware.ts b/packages/integrations/vercel/src/serverless/middleware.ts index b61380ddc030..0c0995c1ac9c 100644 --- a/packages/integrations/vercel/src/serverless/middleware.ts +++ b/packages/integrations/vercel/src/serverless/middleware.ts @@ -1,6 +1,6 @@ import { existsSync } from 'node:fs'; import { fileURLToPath, pathToFileURL } from 'node:url'; -import { ASTRO_LOCALS_HEADER } from './adapter.js'; +import { builtinModules } from 'node:module'; /** * It generates the Vercel Edge Middleware file. @@ -36,6 +36,13 @@ export async function generateEdgeMiddleware( format: 'esm', bundle: true, minify: false, + plugins: [{ + name: 'esbuild-namespace-node-built-in-modules', + setup(build) { + const filter = new RegExp(builtinModules.map((mod) => `(^${mod}$)`).join('|')); + build.onResolve({ filter }, (args) => ({ path: 'node:' + args.path, external: true })); + }, + }] }); return pathToFileURL(bundledFilePath); } From 8081fade49890fb62b510cb80de23f263f08c09f Mon Sep 17 00:00:00 2001 From: lilnasy <69170106+lilnasy@users.noreply.github.com> Date: Thu, 18 Jan 2024 00:04:05 +0000 Subject: [PATCH 03/11] edge function to node fetch --- .../vercel/src/serverless/adapter.ts | 28 +++++++++---------- .../vercel/src/serverless/entrypoint.ts | 4 +++ .../vercel/src/serverless/middleware.ts | 10 +++---- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index 4f9111e1f2b1..ffe294bb2eb9 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -130,8 +130,8 @@ export default function vercelServerless({ } let _config: AstroConfig; - let buildTempFolder: URL; - let serverEntry: string; + let _buildTempFolder: URL; + let _serverEntry: string; let _entryPoints: Map; let _middlewareEntryPoint: URL | undefined; // Extra files to be merged with `includeFiles` during build @@ -199,8 +199,8 @@ export default function vercelServerless({ setAdapter(getAdapter({ functionPerRoute, edgeMiddleware })); _config = config; - buildTempFolder = config.build.server; - serverEntry = config.build.serverEntry; + _buildTempFolder = config.build.server; + _serverEntry = config.build.serverEntry; if (config.output === 'static') { throw new AstroError( @@ -272,9 +272,9 @@ export default function vercelServerless({ } } else { await createFunctionFolder({ - functionName: 'render', + functionName: '_render', runtime, - entry: new URL(serverEntry, buildTempFolder), + entry: new URL(_serverEntry, _buildTempFolder), config: _config, logger, NTF_CACHE, @@ -282,13 +282,9 @@ export default function vercelServerless({ excludeFiles, maxDuration, }); + const dest = _middlewareEntryPoint ? '_middleware' : '_render'; for (const route of routes) { - if (route.prerender) continue; - routeDefinitions.push({ - src: route.pattern.source, - dest: 'render', - middlewarePath: _middlewareEntryPoint ? "_middleware" : undefined - }); + if (!route.prerender) routeDefinitions.push({ src: route.pattern.source, dest }); } } if (_middlewareEntryPoint) { @@ -316,7 +312,9 @@ export default function vercelServerless({ ? [ { src: '/.*', - dest: fourOhFourRoute.prerender ? '/404.html' : 'render', + dest: fourOhFourRoute.prerender ? '/404.html' + : _middlewareEntryPoint ? '_middleware' + : 'render', status: 404, }, ] @@ -339,7 +337,7 @@ export default function vercelServerless({ }); // Remove temporary folder - await removeDir(buildTempFolder); + await removeDir(_buildTempFolder); }, }, }; @@ -420,7 +418,7 @@ async function createFunctionFolder({ // https://vercel.com/docs/build-output-api/v3#vercel-primitives/serverless-functions/configuration await writeJson(vcConfig, { runtime, - handler, + handler: handler.replaceAll("\\","/"), launcherType: 'Nodejs', maxDuration, supportsResponseStreaming: true, diff --git a/packages/integrations/vercel/src/serverless/entrypoint.ts b/packages/integrations/vercel/src/serverless/entrypoint.ts index dedc7fcc1a64..4f50e2e819e8 100644 --- a/packages/integrations/vercel/src/serverless/entrypoint.ts +++ b/packages/integrations/vercel/src/serverless/entrypoint.ts @@ -10,6 +10,10 @@ export const createExports = (manifest: SSRManifest) => { const handler = async (req: IncomingMessage, res: ServerResponse) => { const clientAddress = req.headers['x-forwarded-for'] as string | undefined; const localsHeader = req.headers[ASTRO_LOCALS_HEADER]; + const realPath = req.headers['x-astro-path']; + if (typeof realPath === 'string') { + req.url = realPath; + } const locals = typeof localsHeader === 'string' ? JSON.parse(localsHeader) diff --git a/packages/integrations/vercel/src/serverless/middleware.ts b/packages/integrations/vercel/src/serverless/middleware.ts index 0c0995c1ac9c..7773c74da305 100644 --- a/packages/integrations/vercel/src/serverless/middleware.ts +++ b/packages/integrations/vercel/src/serverless/middleware.ts @@ -65,20 +65,20 @@ function edgeMiddlewareTemplate(astroMiddlewareEntryPointPath: URL, vercelEdgeMi 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 () => { - return new Response(null, { + const { origin } = new URL(request.url); + const next = () => + fetch(new URL('/_render', request.url), { headers: { - 'x-middleware-next': '1', + ...Object.fromEntries(request.headers.entries()), + 'x-astro-path': request.url.replace(origin, ''), 'x-astro-locals': trySerializeLocals(ctx.locals) } }) - }; return onRequest(ctx, next); }`; From 94fd81645b27ffd154bcf64d4aa589543a26fe08 Mon Sep 17 00:00:00 2001 From: lilnasy <69170106+lilnasy@users.noreply.github.com> Date: Thu, 18 Jan 2024 00:15:43 +0000 Subject: [PATCH 04/11] adjust tests --- packages/integrations/vercel/test/max-duration.test.js | 2 +- packages/integrations/vercel/test/streaming.test.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/integrations/vercel/test/max-duration.test.js b/packages/integrations/vercel/test/max-duration.test.js index 8f0af3b89df4..79991290515c 100644 --- a/packages/integrations/vercel/test/max-duration.test.js +++ b/packages/integrations/vercel/test/max-duration.test.js @@ -14,7 +14,7 @@ describe('maxDuration', () => { it('makes it to vercel function configuration', async () => { const vcConfig = JSON.parse( - await fixture.readFile('../.vercel/output/functions/render.func/.vc-config.json') + await fixture.readFile('../.vercel/output/functions/_render.func/.vc-config.json') ); expect(vcConfig).to.deep.include({ maxDuration: 60 }); }); diff --git a/packages/integrations/vercel/test/streaming.test.js b/packages/integrations/vercel/test/streaming.test.js index 93dc95c39508..205946973a8c 100644 --- a/packages/integrations/vercel/test/streaming.test.js +++ b/packages/integrations/vercel/test/streaming.test.js @@ -1,7 +1,7 @@ import { loadFixture } from './test-utils.js'; import { expect } from 'chai'; -describe('maxDuration', () => { +describe('streaming', () => { /** @type {import('./test-utils.js').Fixture} */ let fixture; @@ -14,7 +14,7 @@ describe('maxDuration', () => { it('makes it to vercel function configuration', async () => { const vcConfig = JSON.parse( - await fixture.readFile('../.vercel/output/functions/render.func/.vc-config.json') + await fixture.readFile('../.vercel/output/functions/_render.func/.vc-config.json') ); expect(vcConfig).to.deep.include({ supportsResponseStreaming: true }); }); From 20858ff4fbc1e88fa11d3669aaf672be3e6087cb Mon Sep 17 00:00:00 2001 From: lilnasy <69170106+lilnasy@users.noreply.github.com> Date: Thu, 18 Jan 2024 00:34:06 +0000 Subject: [PATCH 05/11] add test --- .../vercel/test/edge-middleware.test.js | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/integrations/vercel/test/edge-middleware.test.js b/packages/integrations/vercel/test/edge-middleware.test.js index deaeb416020c..0b864298dcbc 100644 --- a/packages/integrations/vercel/test/edge-middleware.test.js +++ b/packages/integrations/vercel/test/edge-middleware.test.js @@ -3,6 +3,34 @@ import chaiJestSnapshot from 'chai-jest-snapshot'; import { loadFixture } from './test-utils.js'; describe('Vercel edge middleware', () => { + /** @type {import('../../../astro/test/test-utils.js').Fixture} */ + let fixture; + before(async () => { + fixture = await loadFixture({ + root: './fixtures/middleware-with-edge-file/', + }); + await fixture.build(); + }); + + it('an edge function is created', async () => { + const contents = await fixture.readFile( + '../.vercel/output/functions/_middleware.func/.vc-config.json' + ); + expect(JSON.parse(contents)).to.deep.include({ + "runtime": "edge", + "entrypoint": "middleware.mjs" + }); + }); + + + it('deployment config points to the middleware edge function', async () => { + const contents = await fixture.readFile( + '../.vercel/output/config.json' + ); + const { routes } = JSON.parse(contents); + expect(routes.some(route => route.dest === '_middleware')).to.be.true; + }); + // TODO: The path here seems to be inconsistent? it.skip('with edge handle file, should successfully build the middleware', async () => { const fixture = await loadFixture({ From 0c3f4c7053295e743952f9275ff1b644480d7f24 Mon Sep 17 00:00:00 2001 From: lilnasy <69170106+lilnasy@users.noreply.github.com> Date: Thu, 18 Jan 2024 00:35:20 +0000 Subject: [PATCH 06/11] add changeset --- .changeset/brown-parents-sniff.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/brown-parents-sniff.md diff --git a/.changeset/brown-parents-sniff.md b/.changeset/brown-parents-sniff.md new file mode 100644 index 000000000000..8063156aada8 --- /dev/null +++ b/.changeset/brown-parents-sniff.md @@ -0,0 +1,5 @@ +--- +"@astrojs/vercel": patch +--- + +Fixes an issue where edge middleware did not work. From 8f708c46a98f14f94f14cc65c6117c6670570bf8 Mon Sep 17 00:00:00 2001 From: lilnasy <69170106+lilnasy@users.noreply.github.com> Date: Thu, 18 Jan 2024 17:56:43 +0000 Subject: [PATCH 07/11] function paths as constants --- .../integrations/vercel/src/serverless/adapter.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index ffe294bb2eb9..83b735c5ab7d 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -32,6 +32,11 @@ const PACKAGE_NAME = '@astrojs/vercel/serverless'; export const ASTRO_LOCALS_HEADER = 'x-astro-locals'; export const VERCEL_EDGE_MIDDLEWARE_FILE = 'vercel-edge-middleware'; +// Vercel routes the folder names to a path on the deployed website. +// We attempt to avoid interfering by prefixing with an underscore. +const NODE_PATH = '_render'; +const MIDDLEWARE_PATH = '_middleware'; + // https://vercel.com/docs/concepts/functions/serverless-functions/runtimes/node-js#node.js-version const SUPPORTED_NODE_VERSIONS: Record< string, @@ -272,7 +277,7 @@ export default function vercelServerless({ } } else { await createFunctionFolder({ - functionName: '_render', + functionName: NODE_PATH, runtime, entry: new URL(_serverEntry, _buildTempFolder), config: _config, @@ -282,14 +287,14 @@ export default function vercelServerless({ excludeFiles, maxDuration, }); - const dest = _middlewareEntryPoint ? '_middleware' : '_render'; + const dest = _middlewareEntryPoint ? MIDDLEWARE_PATH : NODE_PATH; for (const route of routes) { if (!route.prerender) routeDefinitions.push({ src: route.pattern.source, dest }); } } if (_middlewareEntryPoint) { await createMiddlewareFolder({ - functionName: '_middleware', + functionName: MIDDLEWARE_PATH, entry: _middlewareEntryPoint, config: _config, }); @@ -313,8 +318,8 @@ export default function vercelServerless({ { src: '/.*', dest: fourOhFourRoute.prerender ? '/404.html' - : _middlewareEntryPoint ? '_middleware' - : 'render', + : _middlewareEntryPoint ? MIDDLEWARE_PATH + : NODE_PATH, status: 404, }, ] From a2d6eef35866593cab07b5c046192490b9b14b5c Mon Sep 17 00:00:00 2001 From: lilnasy <69170106+lilnasy@users.noreply.github.com> Date: Thu, 18 Jan 2024 17:57:53 +0000 Subject: [PATCH 08/11] ensure node built-in modules are namespaced with `node:` --- packages/integrations/vercel/src/serverless/middleware.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/integrations/vercel/src/serverless/middleware.ts b/packages/integrations/vercel/src/serverless/middleware.ts index 7773c74da305..1baac8e7ae0a 100644 --- a/packages/integrations/vercel/src/serverless/middleware.ts +++ b/packages/integrations/vercel/src/serverless/middleware.ts @@ -36,6 +36,7 @@ export async function generateEdgeMiddleware( format: 'esm', bundle: true, minify: false, + // ensure node built-in modules are namespaced with `node:` plugins: [{ name: 'esbuild-namespace-node-built-in-modules', setup(build) { From c1df03f9222826059e13b6e41930aa79bcbcd13e Mon Sep 17 00:00:00 2001 From: lilnasy <69170106+lilnasy@users.noreply.github.com> Date: Thu, 18 Jan 2024 18:02:25 +0000 Subject: [PATCH 09/11] x-astro-path as constant --- packages/integrations/vercel/src/serverless/adapter.ts | 3 ++- packages/integrations/vercel/src/serverless/entrypoint.ts | 4 ++-- packages/integrations/vercel/src/serverless/middleware.ts | 7 ++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index 83b735c5ab7d..dd3aefdd9aaf 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -29,12 +29,13 @@ import { import { generateEdgeMiddleware } from './middleware.js'; const PACKAGE_NAME = '@astrojs/vercel/serverless'; +export const ASTRO_PATH_HEADER = 'x-astro-path'; export const ASTRO_LOCALS_HEADER = 'x-astro-locals'; export const VERCEL_EDGE_MIDDLEWARE_FILE = 'vercel-edge-middleware'; // Vercel routes the folder names to a path on the deployed website. // We attempt to avoid interfering by prefixing with an underscore. -const NODE_PATH = '_render'; +export const NODE_PATH = '_render'; const MIDDLEWARE_PATH = '_middleware'; // https://vercel.com/docs/concepts/functions/serverless-functions/runtimes/node-js#node.js-version diff --git a/packages/integrations/vercel/src/serverless/entrypoint.ts b/packages/integrations/vercel/src/serverless/entrypoint.ts index 4f50e2e819e8..a300b8a12f8b 100644 --- a/packages/integrations/vercel/src/serverless/entrypoint.ts +++ b/packages/integrations/vercel/src/serverless/entrypoint.ts @@ -1,7 +1,7 @@ import type { SSRManifest } from 'astro'; import { applyPolyfills, NodeApp } from 'astro/app/node'; import type { IncomingMessage, ServerResponse } from 'node:http'; -import { ASTRO_LOCALS_HEADER } from './adapter.js'; +import { ASTRO_PATH_HEADER, ASTRO_LOCALS_HEADER } from './adapter.js'; applyPolyfills(); @@ -10,7 +10,7 @@ export const createExports = (manifest: SSRManifest) => { const handler = async (req: IncomingMessage, res: ServerResponse) => { const clientAddress = req.headers['x-forwarded-for'] as string | undefined; const localsHeader = req.headers[ASTRO_LOCALS_HEADER]; - const realPath = req.headers['x-astro-path']; + const realPath = req.headers[ASTRO_PATH_HEADER]; if (typeof realPath === 'string') { req.url = realPath; } diff --git a/packages/integrations/vercel/src/serverless/middleware.ts b/packages/integrations/vercel/src/serverless/middleware.ts index 1baac8e7ae0a..81abaee434ee 100644 --- a/packages/integrations/vercel/src/serverless/middleware.ts +++ b/packages/integrations/vercel/src/serverless/middleware.ts @@ -1,6 +1,7 @@ import { existsSync } from 'node:fs'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { builtinModules } from 'node:module'; +import { ASTRO_LOCALS_HEADER, ASTRO_PATH_HEADER, NODE_PATH } from './adapter.js'; /** * It generates the Vercel Edge Middleware file. @@ -73,11 +74,11 @@ export default async function middleware(request, context) { ctx.locals = ${handlerTemplateCall}; const { origin } = new URL(request.url); const next = () => - fetch(new URL('/_render', request.url), { + fetch(new URL('${NODE_PATH}', request.url), { headers: { ...Object.fromEntries(request.headers.entries()), - 'x-astro-path': request.url.replace(origin, ''), - 'x-astro-locals': trySerializeLocals(ctx.locals) + '${ASTRO_PATH_HEADER}': request.url.replace(origin, ''), + '${ASTRO_LOCALS_HEADER}': trySerializeLocals(ctx.locals) } }) From 6ffb099b9cae31b5581ae44b6f43378e916d3798 Mon Sep 17 00:00:00 2001 From: lilnasy <69170106+lilnasy@users.noreply.github.com> Date: Thu, 18 Jan 2024 18:10:19 +0000 Subject: [PATCH 10/11] appease linter --- .../integrations/vercel/test/edge-middleware.test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/integrations/vercel/test/edge-middleware.test.js b/packages/integrations/vercel/test/edge-middleware.test.js index 0b864298dcbc..91146a41c0f8 100644 --- a/packages/integrations/vercel/test/edge-middleware.test.js +++ b/packages/integrations/vercel/test/edge-middleware.test.js @@ -4,16 +4,16 @@ import { loadFixture } from './test-utils.js'; describe('Vercel edge middleware', () => { /** @type {import('../../../astro/test/test-utils.js').Fixture} */ - let fixture; + let build; before(async () => { - fixture = await loadFixture({ + build = await loadFixture({ root: './fixtures/middleware-with-edge-file/', }); - await fixture.build(); + await build.build(); }); it('an edge function is created', async () => { - const contents = await fixture.readFile( + const contents = await build.readFile( '../.vercel/output/functions/_middleware.func/.vc-config.json' ); expect(JSON.parse(contents)).to.deep.include({ @@ -24,7 +24,7 @@ describe('Vercel edge middleware', () => { it('deployment config points to the middleware edge function', async () => { - const contents = await fixture.readFile( + const contents = await build.readFile( '../.vercel/output/config.json' ); const { routes } = JSON.parse(contents); From f7d295cea679449cf5dd09552626f6ecfbd50e38 Mon Sep 17 00:00:00 2001 From: lilnasy <69170106+lilnasy@users.noreply.github.com> Date: Fri, 19 Jan 2024 14:57:42 +0000 Subject: [PATCH 11/11] add comments for ASTRO_PATH_HEADER and ASTRO_LOCALS_HEADER --- packages/integrations/vercel/src/serverless/adapter.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index dd3aefdd9aaf..4f2a5092ec54 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -29,7 +29,17 @@ import { import { generateEdgeMiddleware } from './middleware.js'; const PACKAGE_NAME = '@astrojs/vercel/serverless'; + +/** + * The edge function calls the node server at /_render, + * with the original path as the value of this header. + */ export const ASTRO_PATH_HEADER = 'x-astro-path'; + +/** + * The edge function calls the node server at /_render, + * with the locals serialized into this header. + */ export const ASTRO_LOCALS_HEADER = 'x-astro-locals'; export const VERCEL_EDGE_MIDDLEWARE_FILE = 'vercel-edge-middleware';