-
-
Notifications
You must be signed in to change notification settings - Fork 798
feat(vercel): allow overriding function config by route #4124
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
base: main
Are you sure you want to change the base?
Changes from all commits
da1808e
24d92dc
ba05eb0
2758d9b
af5775a
0b8d4c5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ import { defu } from "defu"; | |
| import { writeFile } from "../_utils/fs.ts"; | ||
| import type { Nitro, NitroRouteRules } from "nitro/types"; | ||
| import { dirname, relative, resolve } from "pathe"; | ||
| import { createRouter, addRoute, findRoute } from "rou3"; | ||
| import { joinURL, withLeadingSlash, withoutLeadingSlash } from "ufo"; | ||
| import type { | ||
| PrerenderFunctionConfig, | ||
|
|
@@ -48,15 +49,26 @@ export async function generateFunctionFiles(nitro: Nitro) { | |
| const buildConfig = generateBuildConfig(nitro, o11Routes); | ||
| await writeFile(buildConfigPath, JSON.stringify(buildConfig, null, 2)); | ||
|
|
||
| const functionConfigPath = resolve(nitro.options.output.serverDir, ".vc-config.json"); | ||
| const functionConfig: VercelServerlessFunctionConfig = { | ||
| const baseFunctionConfig: VercelServerlessFunctionConfig = { | ||
| handler: "index.mjs", | ||
| launcherType: "Nodejs", | ||
| shouldAddHelpers: false, | ||
| supportsResponseStreaming: true, | ||
| ...nitro.options.vercel?.functions, | ||
| }; | ||
| await writeFile(functionConfigPath, JSON.stringify(functionConfig, null, 2)); | ||
| const functionConfigPath = resolve(nitro.options.output.serverDir, ".vc-config.json"); | ||
| await writeFile(functionConfigPath, JSON.stringify(baseFunctionConfig, null, 2)); | ||
|
|
||
| // Build rou3 router for routeFunctionConfig matching | ||
| const routeFunctionConfig = nitro.options.vercel?.routeFunctionConfig; | ||
| const hasRouteFunctionConfig = routeFunctionConfig && Object.keys(routeFunctionConfig).length > 0; | ||
| let routeFuncRouter: ReturnType<typeof createRouter<VercelServerlessFunctionConfig>> | undefined; | ||
| if (hasRouteFunctionConfig) { | ||
| routeFuncRouter = createRouter<VercelServerlessFunctionConfig>(); | ||
| for (const [pattern, overrides] of Object.entries(routeFunctionConfig)) { | ||
| addRoute(routeFuncRouter, "", pattern, overrides); | ||
| } | ||
| } | ||
|
|
||
| // Write ISR functions | ||
| for (const [key, value] of Object.entries(nitro.options.routeRules)) { | ||
|
|
@@ -70,18 +82,49 @@ export async function generateFunctionFiles(nitro: Nitro) { | |
| normalizeRouteDest(key) + ISR_SUFFIX | ||
| ); | ||
| await fsp.mkdir(dirname(funcPrefix), { recursive: true }); | ||
| await fsp.symlink( | ||
| "./" + relative(dirname(funcPrefix), nitro.options.output.serverDir), | ||
| funcPrefix + ".func", | ||
| "junction" | ||
| ); | ||
|
|
||
| const match = routeFuncRouter && findRoute(routeFuncRouter, "", key); | ||
| if (match) { | ||
| await createFunctionDirWithCustomConfig( | ||
| funcPrefix + ".func", | ||
| nitro.options.output.serverDir, | ||
| baseFunctionConfig, | ||
| match.data | ||
| ); | ||
| } else { | ||
| await fsp.symlink( | ||
| "./" + relative(dirname(funcPrefix), nitro.options.output.serverDir), | ||
| funcPrefix + ".func", | ||
| "junction" | ||
| ); | ||
| } | ||
|
|
||
| await writePrerenderConfig( | ||
| funcPrefix + ".prerender-config.json", | ||
| value.isr, | ||
| nitro.options.vercel?.config?.bypassToken | ||
| ); | ||
| } | ||
|
|
||
| // Write routeFunctionConfig custom function directories | ||
| const createdFuncDirs = new Set<string>(); | ||
| if (hasRouteFunctionConfig) { | ||
| for (const [pattern, overrides] of Object.entries(routeFunctionConfig!)) { | ||
| const funcDir = resolve( | ||
| nitro.options.output.serverDir, | ||
| "..", | ||
| normalizeRouteDest(pattern) + ".func" | ||
| ); | ||
| await createFunctionDirWithCustomConfig( | ||
| funcDir, | ||
| nitro.options.output.serverDir, | ||
| baseFunctionConfig, | ||
| overrides | ||
| ); | ||
| createdFuncDirs.add(funcDir); | ||
| } | ||
| } | ||
|
|
||
| // Write observability routes | ||
| if (o11Routes.length === 0) { | ||
| return; | ||
|
|
@@ -94,12 +137,29 @@ export async function generateFunctionFiles(nitro: Nitro) { | |
| continue; // #3563 | ||
| } | ||
| const funcPrefix = resolve(nitro.options.output.serverDir, "..", route.dest); | ||
| await fsp.mkdir(dirname(funcPrefix), { recursive: true }); | ||
| await fsp.symlink( | ||
| "./" + relative(dirname(funcPrefix), nitro.options.output.serverDir), | ||
| funcPrefix + ".func", | ||
| "junction" | ||
| ); | ||
| const funcDir = funcPrefix + ".func"; | ||
|
|
||
| // Skip if already created by routeFunctionConfig | ||
| if (createdFuncDirs.has(funcDir)) { | ||
| continue; | ||
| } | ||
|
|
||
| const match = routeFuncRouter && findRoute(routeFuncRouter, "", route.src); | ||
| if (match) { | ||
| await createFunctionDirWithCustomConfig( | ||
| funcDir, | ||
| nitro.options.output.serverDir, | ||
| baseFunctionConfig, | ||
| match.data | ||
| ); | ||
| } else { | ||
| await fsp.mkdir(dirname(funcPrefix), { recursive: true }); | ||
| await fsp.symlink( | ||
| "./" + relative(dirname(funcPrefix), nitro.options.output.serverDir), | ||
| funcDir, | ||
| "junction" | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -273,6 +333,13 @@ function generateBuildConfig(nitro: Nitro, o11Routes?: ObservabilityRoute[]) { | |
| ), | ||
| }; | ||
| }), | ||
| // Route function config routes | ||
| ...(nitro.options.vercel?.routeFunctionConfig | ||
| ? Object.keys(nitro.options.vercel.routeFunctionConfig).map((pattern) => ({ | ||
| src: joinURL(nitro.options.baseURL, normalizeRouteSrc(pattern)), | ||
| dest: withLeadingSlash(normalizeRouteDest(pattern)), | ||
| })) | ||
| : []), | ||
|
Comment on lines
+336
to
+342
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Route-function routes should be specificity-ordered and baseURL-aware. Current emission order depends on object key insertion, so wildcard patterns can shadow specific ones. Also, these π‘ Proposed fix+ const routeFunctionPatterns = nitro.options.vercel?.routeFunctionConfig
+ ? Object.keys(nitro.options.vercel.routeFunctionConfig).sort(
+ (a, b) => b.split(/\/(?!\*)/).length - a.split(/\/(?!\*)/).length
+ )
+ : [];
+
config.routes!.push(
@@
- ...(nitro.options.vercel?.routeFunctionConfig
- ? Object.keys(nitro.options.vercel.routeFunctionConfig).map((pattern) => ({
- src: normalizeRouteSrc(pattern),
+ ...routeFunctionPatterns.map((pattern) => ({
+ src: joinURL(nitro.options.baseURL, normalizeRouteSrc(pattern)),
dest: withLeadingSlash(normalizeRouteDest(pattern)),
- }))
- : []),
+ })),π€ Prompt for AI Agents |
||
| // Observability routes | ||
| ...(o11Routes || []).map((route) => ({ | ||
| src: joinURL(nitro.options.baseURL, route.src), | ||
|
|
@@ -512,6 +579,30 @@ function normalizeRouteDest(route: string) { | |
| ); | ||
| } | ||
|
|
||
| async function createFunctionDirWithCustomConfig( | ||
| funcDir: string, | ||
| serverDir: string, | ||
| baseFunctionConfig: VercelServerlessFunctionConfig, | ||
| overrides: VercelServerlessFunctionConfig | ||
| ) { | ||
| await fsp.mkdir(funcDir, { recursive: true }); | ||
| const entries = await fsp.readdir(serverDir); | ||
| for (const entry of entries) { | ||
| if (entry === ".vc-config.json") { | ||
| continue; | ||
| } | ||
| const target = "./" + relative(funcDir, resolve(serverDir, entry)); | ||
| await fsp.symlink(target, resolve(funcDir, entry), "junction"); | ||
| } | ||
| const mergedConfig = defu(overrides, baseFunctionConfig); | ||
| for (const [key, value] of Object.entries(overrides)) { | ||
| if (Array.isArray(value)) { | ||
| (mergedConfig as Record<string, unknown>)[key] = value; | ||
| } | ||
| } | ||
| await writeFile(resolve(funcDir, ".vc-config.json"), JSON.stringify(mergedConfig, null, 2)); | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| async function writePrerenderConfig( | ||
| filename: string, | ||
| isrConfig: NitroRouteRules["isr"], | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pi0 what should we call the key? I'm not satisfied with this, just used it as a placeholder for now. Also as it accepts a list of routes perhaps the key should be plural with
routeFunctionsConfigif we decide not to change it to something else altogether?