summaryrefslogtreecommitdiff
path: root/packages/integrations/vercel/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/integrations/vercel/src')
-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
5 files changed, 129 insertions, 8 deletions
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);
+}`;
+}