mirror of
https://github.com/withastro/astro.git
synced 2025-01-20 22:12:38 -05:00
fix(vercel): edge middleware (#9585)
* create vercel edge middleware remove getVercelOutput * handle node built-in modules * edge function to node fetch * adjust tests * add test * add changeset * function paths as constants * ensure node built-in modules are namespaced with `node:` * x-astro-path as constant * appease linter * add comments for ASTRO_PATH_HEADER and ASTRO_LOCALS_HEADER --------- Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>
This commit is contained in:
parent
a50926a6b6
commit
05adaaa2d2
9 changed files with 161 additions and 84 deletions
5
.changeset/brown-parents-sniff.md
Normal file
5
.changeset/brown-parents-sniff.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"@astrojs/vercel": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fixes an issue where edge middleware did not work.
|
|
@ -31,8 +31,6 @@ export async function getFilesFromFolder(dir: URL) {
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getVercelOutput = (root: URL) => new URL('./.vercel/output/', root);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copies files into a folder keeping the folder structure intact.
|
* Copies files into a folder keeping the folder structure intact.
|
||||||
* The resulting file tree will start at the common ancestor.
|
* The resulting file tree will start at the common ancestor.
|
||||||
|
|
|
@ -8,14 +8,14 @@ import type {
|
||||||
import { AstroError } from 'astro/errors';
|
import { AstroError } from 'astro/errors';
|
||||||
import glob from 'fast-glob';
|
import glob from 'fast-glob';
|
||||||
import { basename } from 'node:path';
|
import { basename } from 'node:path';
|
||||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
import { pathToFileURL } from 'node:url';
|
||||||
import {
|
import {
|
||||||
getAstroImageConfig,
|
getAstroImageConfig,
|
||||||
getDefaultImageConfig,
|
getDefaultImageConfig,
|
||||||
type DevImageService,
|
type DevImageService,
|
||||||
type VercelImageConfig,
|
type VercelImageConfig,
|
||||||
} from '../image/shared.js';
|
} 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 { copyDependenciesToFunction } from '../lib/nft.js';
|
||||||
import { getRedirects } from '../lib/redirects.js';
|
import { getRedirects } from '../lib/redirects.js';
|
||||||
import {
|
import {
|
||||||
|
@ -29,9 +29,25 @@ import {
|
||||||
import { generateEdgeMiddleware } from './middleware.js';
|
import { generateEdgeMiddleware } from './middleware.js';
|
||||||
|
|
||||||
const PACKAGE_NAME = '@astrojs/vercel/serverless';
|
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 ASTRO_LOCALS_HEADER = 'x-astro-locals';
|
||||||
export const VERCEL_EDGE_MIDDLEWARE_FILE = 'vercel-edge-middleware';
|
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
|
// https://vercel.com/docs/concepts/functions/serverless-functions/runtimes/node-js#node.js-version
|
||||||
const SUPPORTED_NODE_VERSIONS: Record<
|
const SUPPORTED_NODE_VERSIONS: Record<
|
||||||
string,
|
string,
|
||||||
|
@ -111,8 +127,8 @@ export interface VercelServerlessConfig {
|
||||||
export default function vercelServerless({
|
export default function vercelServerless({
|
||||||
webAnalytics,
|
webAnalytics,
|
||||||
speedInsights,
|
speedInsights,
|
||||||
includeFiles,
|
includeFiles: _includeFiles = [],
|
||||||
excludeFiles = [],
|
excludeFiles: _excludeFiles = [],
|
||||||
imageService,
|
imageService,
|
||||||
imagesConfig,
|
imagesConfig,
|
||||||
devImageService = 'sharp',
|
devImageService = 'sharp',
|
||||||
|
@ -130,9 +146,10 @@ export default function vercelServerless({
|
||||||
}
|
}
|
||||||
|
|
||||||
let _config: AstroConfig;
|
let _config: AstroConfig;
|
||||||
let buildTempFolder: URL;
|
let _buildTempFolder: URL;
|
||||||
let serverEntry: string;
|
let _serverEntry: string;
|
||||||
let _entryPoints: Map<RouteData, URL>;
|
let _entryPoints: Map<RouteData, URL>;
|
||||||
|
let _middlewareEntryPoint: URL | undefined;
|
||||||
// Extra files to be merged with `includeFiles` during build
|
// Extra files to be merged with `includeFiles` during build
|
||||||
const extraFilesToInclude: URL[] = [];
|
const extraFilesToInclude: URL[] = [];
|
||||||
|
|
||||||
|
@ -162,13 +179,12 @@ export default function vercelServerless({
|
||||||
if (command === 'build' && speedInsights?.enabled) {
|
if (command === 'build' && speedInsights?.enabled) {
|
||||||
injectScript('page', 'import "@astrojs/vercel/speed-insights"');
|
injectScript('page', 'import "@astrojs/vercel/speed-insights"');
|
||||||
}
|
}
|
||||||
const outDir = getVercelOutput(config.root);
|
|
||||||
updateConfig({
|
updateConfig({
|
||||||
outDir,
|
outDir: new URL('./.vercel/output/', config.root),
|
||||||
build: {
|
build: {
|
||||||
serverEntry: 'entry.mjs',
|
client: new URL('./.vercel/output/static/', config.root),
|
||||||
client: new URL('./static/', outDir),
|
server: new URL('./.vercel/output/_functions/', config.root),
|
||||||
server: new URL('./dist/', config.root),
|
|
||||||
redirects: false,
|
redirects: false,
|
||||||
},
|
},
|
||||||
vite: {
|
vite: {
|
||||||
|
@ -195,10 +211,12 @@ export default function vercelServerless({
|
||||||
`\tYou can set functionPerRoute: false to prevent surpassing the limit.\n`
|
`\tYou can set functionPerRoute: false to prevent surpassing the limit.\n`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setAdapter(getAdapter({ functionPerRoute, edgeMiddleware }));
|
setAdapter(getAdapter({ functionPerRoute, edgeMiddleware }));
|
||||||
|
|
||||||
_config = config;
|
_config = config;
|
||||||
buildTempFolder = config.build.server;
|
_buildTempFolder = config.build.server;
|
||||||
serverEntry = config.build.serverEntry;
|
_serverEntry = config.build.serverEntry;
|
||||||
|
|
||||||
if (config.output === 'static') {
|
if (config.output === 'static') {
|
||||||
throw new AstroError(
|
throw new AstroError(
|
||||||
|
@ -208,20 +226,7 @@ export default function vercelServerless({
|
||||||
},
|
},
|
||||||
'astro:build:ssr': async ({ entryPoints, middlewareEntryPoint }) => {
|
'astro:build:ssr': async ({ entryPoints, middlewareEntryPoint }) => {
|
||||||
_entryPoints = entryPoints;
|
_entryPoints = entryPoints;
|
||||||
if (middlewareEntryPoint) {
|
_middlewareEntryPoint = 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);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
'astro:build:done': async ({ routes, logger }) => {
|
'astro:build:done': async ({ routes, logger }) => {
|
||||||
// Merge any includes from `vite.assetsInclude
|
// Merge any includes from `vite.assetsInclude
|
||||||
|
@ -240,9 +245,14 @@ export default function vercelServerless({
|
||||||
mergeGlobbedIncludes(_config.vite.assetsInclude);
|
mergeGlobbedIncludes(_config.vite.assetsInclude);
|
||||||
}
|
}
|
||||||
|
|
||||||
const routeDefinitions: { src: string; dest: string }[] = [];
|
const routeDefinitions: Array<{
|
||||||
const filesToInclude = includeFiles?.map((file) => new URL(file, _config.root)) || [];
|
src: string
|
||||||
filesToInclude.push(...extraFilesToInclude);
|
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);
|
const runtime = getRuntime(process, logger);
|
||||||
|
|
||||||
|
@ -267,7 +277,7 @@ export default function vercelServerless({
|
||||||
config: _config,
|
config: _config,
|
||||||
logger,
|
logger,
|
||||||
NTF_CACHE,
|
NTF_CACHE,
|
||||||
includeFiles: filesToInclude,
|
includeFiles,
|
||||||
excludeFiles,
|
excludeFiles,
|
||||||
maxDuration,
|
maxDuration,
|
||||||
});
|
});
|
||||||
|
@ -278,24 +288,28 @@ export default function vercelServerless({
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await createFunctionFolder({
|
await createFunctionFolder({
|
||||||
functionName: 'render',
|
functionName: NODE_PATH,
|
||||||
runtime,
|
runtime,
|
||||||
entry: new URL(serverEntry, buildTempFolder),
|
entry: new URL(_serverEntry, _buildTempFolder),
|
||||||
config: _config,
|
config: _config,
|
||||||
logger,
|
logger,
|
||||||
NTF_CACHE,
|
NTF_CACHE,
|
||||||
includeFiles: filesToInclude,
|
includeFiles,
|
||||||
excludeFiles,
|
excludeFiles,
|
||||||
maxDuration,
|
maxDuration,
|
||||||
});
|
});
|
||||||
|
const dest = _middlewareEntryPoint ? MIDDLEWARE_PATH : NODE_PATH;
|
||||||
for (const route of routes) {
|
for (const route of routes) {
|
||||||
if (route.prerender) continue;
|
if (!route.prerender) routeDefinitions.push({ src: route.pattern.source, dest });
|
||||||
routeDefinitions.push({
|
|
||||||
src: route.pattern.source,
|
|
||||||
dest: 'render',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (_middlewareEntryPoint) {
|
||||||
|
await createMiddlewareFolder({
|
||||||
|
functionName: MIDDLEWARE_PATH,
|
||||||
|
entry: _middlewareEntryPoint,
|
||||||
|
config: _config,
|
||||||
|
});
|
||||||
|
}
|
||||||
const fourOhFourRoute = routes.find((route) => route.pathname === '/404');
|
const fourOhFourRoute = routes.find((route) => route.pathname === '/404');
|
||||||
// Output configuration
|
// Output configuration
|
||||||
// https://vercel.com/docs/build-output-api/v3#build-output-configuration
|
// https://vercel.com/docs/build-output-api/v3#build-output-configuration
|
||||||
|
@ -314,7 +328,9 @@ export default function vercelServerless({
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
src: '/.*',
|
src: '/.*',
|
||||||
dest: fourOhFourRoute.prerender ? '/404.html' : 'render',
|
dest: fourOhFourRoute.prerender ? '/404.html'
|
||||||
|
: _middlewareEntryPoint ? MIDDLEWARE_PATH
|
||||||
|
: NODE_PATH,
|
||||||
status: 404,
|
status: 404,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -337,7 +353,7 @@ export default function vercelServerless({
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remove temporary folder
|
// Remove temporary folder
|
||||||
await removeDir(buildTempFolder);
|
await removeDir(_buildTempFolder);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -345,6 +361,31 @@ export default function vercelServerless({
|
||||||
|
|
||||||
type Runtime = `nodejs${string}.x`;
|
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 {
|
interface CreateFunctionFolderArgs {
|
||||||
functionName: string;
|
functionName: string;
|
||||||
runtime: Runtime;
|
runtime: Runtime;
|
||||||
|
@ -353,7 +394,7 @@ interface CreateFunctionFolderArgs {
|
||||||
logger: AstroIntegrationLogger;
|
logger: AstroIntegrationLogger;
|
||||||
NTF_CACHE: any;
|
NTF_CACHE: any;
|
||||||
includeFiles: URL[];
|
includeFiles: URL[];
|
||||||
excludeFiles: string[];
|
excludeFiles: URL[];
|
||||||
maxDuration: number | undefined;
|
maxDuration: number | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -379,7 +420,7 @@ async function createFunctionFolder({
|
||||||
entry,
|
entry,
|
||||||
outDir: functionFolder,
|
outDir: functionFolder,
|
||||||
includeFiles,
|
includeFiles,
|
||||||
excludeFiles: excludeFiles.map((file) => new URL(file, config.root)),
|
excludeFiles,
|
||||||
logger,
|
logger,
|
||||||
},
|
},
|
||||||
NTF_CACHE
|
NTF_CACHE
|
||||||
|
@ -393,7 +434,7 @@ async function createFunctionFolder({
|
||||||
// https://vercel.com/docs/build-output-api/v3#vercel-primitives/serverless-functions/configuration
|
// https://vercel.com/docs/build-output-api/v3#vercel-primitives/serverless-functions/configuration
|
||||||
await writeJson(vcConfig, {
|
await writeJson(vcConfig, {
|
||||||
runtime,
|
runtime,
|
||||||
handler,
|
handler: handler.replaceAll("\\","/"),
|
||||||
launcherType: 'Nodejs',
|
launcherType: 'Nodejs',
|
||||||
maxDuration,
|
maxDuration,
|
||||||
supportsResponseStreaming: true,
|
supportsResponseStreaming: true,
|
||||||
|
@ -411,15 +452,18 @@ function getRuntime(process: NodeJS.Process, logger: AstroIntegrationLogger): Ru
|
||||||
`\tYour project will use Node.js 18 as the runtime instead.\n` +
|
`\tYour project will use Node.js 18 as the runtime instead.\n` +
|
||||||
`\tConsider switching your local version to 18.\n`
|
`\tConsider switching your local version to 18.\n`
|
||||||
);
|
);
|
||||||
|
return 'nodejs18.x';
|
||||||
}
|
}
|
||||||
if (support.status === 'current') {
|
if (support.status === 'current') {
|
||||||
return `nodejs${major}.x`;
|
return `nodejs${major}.x`;
|
||||||
} else if (support?.status === 'beta') {
|
}
|
||||||
|
if (support.status === 'beta') {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Your project is being built for Node.js ${major} as the runtime, which is currently in beta for Vercel Serverless Functions.`
|
`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`;
|
return `nodejs${major}.x`;
|
||||||
} else if (support.status === 'deprecated') {
|
}
|
||||||
|
if (support.status === 'deprecated') {
|
||||||
const removeDate = new Intl.DateTimeFormat(undefined, { dateStyle: 'long' }).format(
|
const removeDate = new Intl.DateTimeFormat(undefined, { dateStyle: 'long' }).format(
|
||||||
support.removal
|
support.removal
|
||||||
);
|
);
|
||||||
|
@ -430,14 +474,6 @@ function getRuntime(process: NodeJS.Process, logger: AstroIntegrationLogger): Ru
|
||||||
`\tConsider upgrading your local version to 18.\n`
|
`\tConsider upgrading your local version to 18.\n`
|
||||||
);
|
);
|
||||||
return `nodejs${major}.x`;
|
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';
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import type { SSRManifest } from 'astro';
|
import type { SSRManifest } from 'astro';
|
||||||
import { applyPolyfills, NodeApp } from 'astro/app/node';
|
import { applyPolyfills, NodeApp } from 'astro/app/node';
|
||||||
import type { IncomingMessage, ServerResponse } from 'node:http';
|
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();
|
applyPolyfills();
|
||||||
|
|
||||||
|
@ -10,6 +10,10 @@ export const createExports = (manifest: SSRManifest) => {
|
||||||
const handler = async (req: IncomingMessage, res: ServerResponse) => {
|
const handler = async (req: IncomingMessage, res: ServerResponse) => {
|
||||||
const clientAddress = req.headers['x-forwarded-for'] as string | undefined;
|
const clientAddress = req.headers['x-forwarded-for'] as string | undefined;
|
||||||
const localsHeader = req.headers[ASTRO_LOCALS_HEADER];
|
const localsHeader = req.headers[ASTRO_LOCALS_HEADER];
|
||||||
|
const realPath = req.headers[ASTRO_PATH_HEADER];
|
||||||
|
if (typeof realPath === 'string') {
|
||||||
|
req.url = realPath;
|
||||||
|
}
|
||||||
const locals =
|
const locals =
|
||||||
typeof localsHeader === 'string'
|
typeof localsHeader === 'string'
|
||||||
? JSON.parse(localsHeader)
|
? JSON.parse(localsHeader)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { existsSync } from 'node:fs';
|
import { existsSync } from 'node:fs';
|
||||||
import { join } from 'node:path';
|
|
||||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||||
import { ASTRO_LOCALS_HEADER } from './adapter.js';
|
import { builtinModules } from 'node:module';
|
||||||
|
import { ASTRO_LOCALS_HEADER, ASTRO_PATH_HEADER, NODE_PATH } from './adapter.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* It generates the Vercel Edge Middleware file.
|
* It generates the Vercel Edge Middleware file.
|
||||||
|
@ -16,16 +16,12 @@ import { ASTRO_LOCALS_HEADER } from './adapter.js';
|
||||||
*/
|
*/
|
||||||
export async function generateEdgeMiddleware(
|
export async function generateEdgeMiddleware(
|
||||||
astroMiddlewareEntryPointPath: URL,
|
astroMiddlewareEntryPointPath: URL,
|
||||||
outPath: string,
|
vercelEdgeMiddlewareHandlerPath: URL,
|
||||||
vercelEdgeMiddlewareHandlerPath: URL
|
outPath: URL,
|
||||||
): Promise<URL> {
|
): Promise<URL> {
|
||||||
const entryPointPathURLAsString = JSON.stringify(
|
const code = edgeMiddlewareTemplate(astroMiddlewareEntryPointPath, vercelEdgeMiddlewareHandlerPath);
|
||||||
fileURLToPath(astroMiddlewareEntryPointPath).replace(/\\/g, '/')
|
|
||||||
);
|
|
||||||
|
|
||||||
const code = edgeMiddlewareTemplate(entryPointPathURLAsString, vercelEdgeMiddlewareHandlerPath);
|
|
||||||
// https://vercel.com/docs/concepts/functions/edge-middleware#create-edge-middleware
|
// 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');
|
const esbuild = await import('esbuild');
|
||||||
await esbuild.build({
|
await esbuild.build({
|
||||||
stdin: {
|
stdin: {
|
||||||
|
@ -36,17 +32,27 @@ export async function generateEdgeMiddleware(
|
||||||
platform: 'browser',
|
platform: 'browser',
|
||||||
// https://runtime-keys.proposal.wintercg.org/#edge-light
|
// https://runtime-keys.proposal.wintercg.org/#edge-light
|
||||||
conditions: ['edge-light', 'worker', 'browser'],
|
conditions: ['edge-light', 'worker', 'browser'],
|
||||||
external: ['astro/middleware'],
|
|
||||||
outfile: bundledFilePath,
|
outfile: bundledFilePath,
|
||||||
allowOverwrite: true,
|
allowOverwrite: true,
|
||||||
format: 'esm',
|
format: 'esm',
|
||||||
bundle: true,
|
bundle: true,
|
||||||
minify: false,
|
minify: false,
|
||||||
|
// ensure node built-in modules are namespaced with `node:`
|
||||||
|
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);
|
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);
|
const filePathEdgeMiddleware = fileURLToPath(vercelEdgeMiddlewareHandlerPath);
|
||||||
let handlerTemplateImport = '';
|
let handlerTemplateImport = '';
|
||||||
let handlerTemplateCall = '{}';
|
let handlerTemplateCall = '{}';
|
||||||
|
@ -61,20 +67,20 @@ function edgeMiddlewareTemplate(middlewarePath: string, vercelEdgeMiddlewareHand
|
||||||
import { onRequest } from ${middlewarePath};
|
import { onRequest } from ${middlewarePath};
|
||||||
import { createContext, trySerializeLocals } from 'astro/middleware';
|
import { createContext, trySerializeLocals } from 'astro/middleware';
|
||||||
export default async function middleware(request, context) {
|
export default async function middleware(request, context) {
|
||||||
const url = new URL(request.url);
|
|
||||||
const ctx = createContext({
|
const ctx = createContext({
|
||||||
request,
|
request,
|
||||||
params: {}
|
params: {}
|
||||||
});
|
});
|
||||||
ctx.locals = ${handlerTemplateCall};
|
ctx.locals = ${handlerTemplateCall};
|
||||||
const next = async () => {
|
const { origin } = new URL(request.url);
|
||||||
const response = await fetch(url, {
|
const next = () =>
|
||||||
|
fetch(new URL('${NODE_PATH}', request.url), {
|
||||||
headers: {
|
headers: {
|
||||||
${JSON.stringify(ASTRO_LOCALS_HEADER)}: trySerializeLocals(ctx.locals)
|
...Object.fromEntries(request.headers.entries()),
|
||||||
|
'${ASTRO_PATH_HEADER}': request.url.replace(origin, ''),
|
||||||
|
'${ASTRO_LOCALS_HEADER}': trySerializeLocals(ctx.locals)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
return response;
|
|
||||||
};
|
|
||||||
|
|
||||||
return onRequest(ctx, next);
|
return onRequest(ctx, next);
|
||||||
}`;
|
}`;
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
type DevImageService,
|
type DevImageService,
|
||||||
type VercelImageConfig,
|
type VercelImageConfig,
|
||||||
} from '../image/shared.js';
|
} 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 { isServerLikeOutput } from '../lib/prerender.js';
|
||||||
import { getRedirects } from '../lib/redirects.js';
|
import { getRedirects } from '../lib/redirects.js';
|
||||||
import {
|
import {
|
||||||
|
@ -79,7 +79,7 @@ export default function vercelStatic({
|
||||||
if (command === 'build' && speedInsights?.enabled) {
|
if (command === 'build' && speedInsights?.enabled) {
|
||||||
injectScript('page', 'import "@astrojs/vercel/speed-insights"');
|
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({
|
updateConfig({
|
||||||
outDir,
|
outDir,
|
||||||
build: {
|
build: {
|
||||||
|
@ -110,12 +110,12 @@ export default function vercelStatic({
|
||||||
// Ensure to have `.vercel/output` empty.
|
// Ensure to have `.vercel/output` empty.
|
||||||
// This is because, when building to static, outDir = .vercel/output/static/,
|
// This is because, when building to static, outDir = .vercel/output/static/,
|
||||||
// so .vercel/output itself won't get cleaned.
|
// 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 }) => {
|
'astro:build:done': async ({ routes }) => {
|
||||||
// Output configuration
|
// Output configuration
|
||||||
// https://vercel.com/docs/build-output-api/v3#build-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,
|
version: 3,
|
||||||
routes: [
|
routes: [
|
||||||
...getRedirects(routes, _config),
|
...getRedirects(routes, _config),
|
||||||
|
|
|
@ -3,6 +3,34 @@ import chaiJestSnapshot from 'chai-jest-snapshot';
|
||||||
import { loadFixture } from './test-utils.js';
|
import { loadFixture } from './test-utils.js';
|
||||||
|
|
||||||
describe('Vercel edge middleware', () => {
|
describe('Vercel edge middleware', () => {
|
||||||
|
/** @type {import('../../../astro/test/test-utils.js').Fixture} */
|
||||||
|
let build;
|
||||||
|
before(async () => {
|
||||||
|
build = await loadFixture({
|
||||||
|
root: './fixtures/middleware-with-edge-file/',
|
||||||
|
});
|
||||||
|
await build.build();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('an edge function is created', async () => {
|
||||||
|
const contents = await build.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 build.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?
|
// TODO: The path here seems to be inconsistent?
|
||||||
it.skip('with edge handle file, should successfully build the middleware', async () => {
|
it.skip('with edge handle file, should successfully build the middleware', async () => {
|
||||||
const fixture = await loadFixture({
|
const fixture = await loadFixture({
|
||||||
|
|
|
@ -14,7 +14,7 @@ describe('maxDuration', () => {
|
||||||
|
|
||||||
it('makes it to vercel function configuration', async () => {
|
it('makes it to vercel function configuration', async () => {
|
||||||
const vcConfig = JSON.parse(
|
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 });
|
expect(vcConfig).to.deep.include({ maxDuration: 60 });
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { loadFixture } from './test-utils.js';
|
import { loadFixture } from './test-utils.js';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
|
|
||||||
describe('maxDuration', () => {
|
describe('streaming', () => {
|
||||||
/** @type {import('./test-utils.js').Fixture} */
|
/** @type {import('./test-utils.js').Fixture} */
|
||||||
let fixture;
|
let fixture;
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ describe('maxDuration', () => {
|
||||||
|
|
||||||
it('makes it to vercel function configuration', async () => {
|
it('makes it to vercel function configuration', async () => {
|
||||||
const vcConfig = JSON.parse(
|
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 });
|
expect(vcConfig).to.deep.include({ supportsResponseStreaming: true });
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue