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
Next Next commit
create vercel edge middleware
remove getVercelOutput
  • Loading branch information
lilnasy committed Jan 17, 2024
commit 5c7d8ca0bc1aa223adc9b687635a91fc3fe1fb2b
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
103 changes: 64 additions & 39 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 Down Expand Up @@ -111,8 +111,8 @@ export interface VercelServerlessConfig {
export default function vercelServerless({
webAnalytics,
speedInsights,
includeFiles,
excludeFiles = [],
includeFiles: _includeFiles = [],
excludeFiles: _excludeFiles = [],
imageService,
imagesConfig,
devImageService = 'sharp',
Expand All @@ -133,6 +133,7 @@ export default function vercelServerless({
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 +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: {
Expand All @@ -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;
Expand All @@ -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
Expand All @@ -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);

Expand All @@ -267,7 +261,7 @@ export default function vercelServerless({
config: _config,
logger,
NTF_CACHE,
includeFiles: filesToInclude,
includeFiles,
excludeFiles,
maxDuration,
});
Expand All @@ -284,7 +278,7 @@ export default function vercelServerless({
config: _config,
logger,
NTF_CACHE,
includeFiles: filesToInclude,
includeFiles,
excludeFiles,
maxDuration,
});
Expand All @@ -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',
Copy link
Member

Choose a reason for hiding this comment

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

Why _middleware and not middleware?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Vercel routes the folder names to a path on the deployed website so we avoid interfering this way.

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

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 +380,7 @@ interface CreateFunctionFolderArgs {
logger: AstroIntegrationLogger;
NTF_CACHE: any;
includeFiles: URL[];
excludeFiles: string[];
excludeFiles: URL[];
maxDuration: number | undefined;
}

Expand All @@ -379,7 +406,7 @@ async function createFunctionFolder({
entry,
outDir: functionFolder,
includeFiles,
excludeFiles: excludeFiles.map((file) => new URL(file, config.root)),
excludeFiles,
logger,
},
NTF_CACHE
Expand Down Expand Up @@ -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';
}
27 changes: 12 additions & 15 deletions packages/integrations/vercel/src/serverless/middleware.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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<URL> {
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: {
Expand All @@ -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',
Expand All @@ -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 = '{}';
Expand All @@ -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;
})
lilnasy marked this conversation as resolved.
Show resolved Hide resolved
};

return onRequest(ctx, next);
Expand Down
8 changes: 4 additions & 4 deletions packages/integrations/vercel/src/static/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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),
Expand Down