Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(vercel): edge middleware #9585

Merged
merged 12 commits into from
Jan 22, 2024
5 changes: 5 additions & 0 deletions .changeset/brown-parents-sniff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@astrojs/vercel": patch
---

Fixes an issue where edge middleware did not work.
2 changes: 0 additions & 2 deletions packages/integrations/vercel/src/lib/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
146 changes: 91 additions & 55 deletions packages/integrations/vercel/src/serverless/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -29,9 +29,25 @@ 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';

// Vercel routes the folder names to a path on the deployed website.
// We attempt to avoid interfering by prefixing with an underscore.
export 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,
Expand Down Expand Up @@ -111,8 +127,8 @@ export interface VercelServerlessConfig {
export default function vercelServerless({
webAnalytics,
speedInsights,
includeFiles,
excludeFiles = [],
includeFiles: _includeFiles = [],
excludeFiles: _excludeFiles = [],
imageService,
imagesConfig,
devImageService = 'sharp',
Expand All @@ -130,9 +146,10 @@ export default function vercelServerless({
}

let _config: AstroConfig;
let buildTempFolder: URL;
let serverEntry: string;
let _buildTempFolder: URL;
let _serverEntry: string;
let _entryPoints: Map<RouteData, URL>;
let _middlewareEntryPoint: URL | undefined;
// Extra files to be merged with `includeFiles` during build
const extraFilesToInclude: URL[] = [];

Expand Down Expand Up @@ -162,13 +179,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: {
Expand All @@ -195,10 +211,12 @@ 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;
_buildTempFolder = config.build.server;
_serverEntry = config.build.serverEntry;

if (config.output === 'static') {
throw new AstroError(
Expand All @@ -208,20 +226,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
Expand All @@ -240,9 +245,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);

Expand All @@ -267,7 +277,7 @@ export default function vercelServerless({
config: _config,
logger,
NTF_CACHE,
includeFiles: filesToInclude,
includeFiles,
excludeFiles,
maxDuration,
});
Expand All @@ -278,24 +288,28 @@ export default function vercelServerless({
}
} else {
await createFunctionFolder({
functionName: 'render',
functionName: NODE_PATH,
runtime,
entry: new URL(serverEntry, buildTempFolder),
entry: new URL(_serverEntry, _buildTempFolder),
config: _config,
logger,
NTF_CACHE,
includeFiles: filesToInclude,
includeFiles,
excludeFiles,
maxDuration,
});
const dest = _middlewareEntryPoint ? MIDDLEWARE_PATH : NODE_PATH;
for (const route of routes) {
if (route.prerender) continue;
routeDefinitions.push({
src: route.pattern.source,
dest: 'render',
});
if (!route.prerender) routeDefinitions.push({ src: route.pattern.source, dest });
}
}
if (_middlewareEntryPoint) {
await createMiddlewareFolder({
functionName: MIDDLEWARE_PATH,
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
Expand All @@ -314,7 +328,9 @@ export default function vercelServerless({
? [
{
src: '/.*',
dest: fourOhFourRoute.prerender ? '/404.html' : 'render',
dest: fourOhFourRoute.prerender ? '/404.html'
: _middlewareEntryPoint ? MIDDLEWARE_PATH
: NODE_PATH,
status: 404,
},
]
Expand All @@ -337,14 +353,39 @@ export default function vercelServerless({
});

// Remove temporary folder
await removeDir(buildTempFolder);
await removeDir(_buildTempFolder);
},
},
};
}

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',
});
}

Comment on lines +364 to +388
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We create an edge function, complete with its own folder and vc-config.

interface CreateFunctionFolderArgs {
functionName: string;
runtime: Runtime;
Expand All @@ -353,7 +394,7 @@ interface CreateFunctionFolderArgs {
logger: AstroIntegrationLogger;
NTF_CACHE: any;
includeFiles: URL[];
excludeFiles: string[];
excludeFiles: URL[];
maxDuration: number | undefined;
}

Expand All @@ -379,7 +420,7 @@ async function createFunctionFolder({
entry,
outDir: functionFolder,
includeFiles,
excludeFiles: excludeFiles.map((file) => new URL(file, config.root)),
excludeFiles,
logger,
},
NTF_CACHE
Expand All @@ -393,7 +434,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("\\","/"),
ematipico marked this conversation as resolved.
Show resolved Hide resolved
launcherType: 'Nodejs',
maxDuration,
supportsResponseStreaming: true,
Expand All @@ -411,15 +452,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
);
Expand All @@ -430,14 +474,6 @@ 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`;
return 'nodejs18.x';
}
6 changes: 5 additions & 1 deletion packages/integrations/vercel/src/serverless/entrypoint.ts
Original file line number Diff line number Diff line change
@@ -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();

Expand All @@ -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[ASTRO_PATH_HEADER];
if (typeof realPath === 'string') {
req.url = realPath;
}
const locals =
typeof localsHeader === 'string'
? JSON.parse(localsHeader)
Expand Down
Loading