Skip to content

Commit aef689e

Browse files
authored
feat: add api route support for vercel environment (#307)
* feat: add build for vercel * feat: add build api route for vercel * feat: add e2e tests for vercel api
1 parent 799f03f commit aef689e

File tree

14 files changed

+1564
-73
lines changed

14 files changed

+1564
-73
lines changed

packages/rxbot/src/command/commands/build/build-vercel.ts

Lines changed: 87 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { existsSync } from "fs";
22
import path from "path";
33
import { rspack } from "@rspack/core";
4+
import { RouteInfo } from "@rx-lab/common";
45
import fs from "fs/promises";
56
import nunjucks from "nunjucks";
67
import {
7-
VERCEL_SEND_MESSAGE_TEMPLATE,
8+
VERCEL_API_ROUTE_TEMPLATE,
89
VERCEL_WEBHOOK_FUNCTION_TEMPLATE,
910
} from "../../../templates/vercel";
1011

@@ -28,53 +29,74 @@ const VERCEL_CONFIG_FILE_NAME = "config.json";
2829
* @param content
2930
*/
3031
async function writeVercelFunctionToDisk(apiRoute: string, content: string) {
31-
const functionOutputFolder =
32-
path.resolve(VERCEL_FUNCTIONS_FOLDER, apiRoute) + ".func";
32+
try {
33+
let functionOutputFolder = path.join(
34+
path.resolve(VERCEL_FUNCTIONS_FOLDER),
35+
apiRoute + ".func",
36+
);
3337

34-
// check if the folder exists
35-
if (!existsSync(functionOutputFolder)) {
36-
await fs.mkdir(functionOutputFolder, { recursive: true });
37-
}
38+
// check if the folder exists
39+
if (!existsSync(functionOutputFolder)) {
40+
await fs.mkdir(functionOutputFolder, { recursive: true });
41+
}
3842

39-
const configFilePath = path.join(
40-
functionOutputFolder,
41-
VERCEL_FUNCTION_CONFIG_FILENAME,
42-
);
43+
const configFilePath = path.join(
44+
functionOutputFolder,
45+
VERCEL_FUNCTION_CONFIG_FILENAME,
46+
);
4347

44-
await build(content, VERCEL_FUNCTION_FILE_NAME, functionOutputFolder);
45-
// write config
46-
await fs.writeFile(
47-
configFilePath,
48-
JSON.stringify(
49-
{
50-
runtime: "nodejs20.x",
51-
handler: "index.js",
52-
launcherType: "Nodejs",
53-
shouldAddHelpers: true,
54-
},
55-
null,
56-
2,
57-
),
58-
);
48+
await build(content, VERCEL_FUNCTION_FILE_NAME, functionOutputFolder);
49+
// write config
50+
await fs.writeFile(
51+
configFilePath,
52+
JSON.stringify(
53+
{
54+
runtime: "nodejs20.x",
55+
handler: "index.js",
56+
launcherType: "Nodejs",
57+
shouldAddHelpers: true,
58+
},
59+
null,
60+
2,
61+
),
62+
);
63+
} catch (e) {
64+
console.error(e);
65+
throw e;
66+
}
5967
}
6068

6169
/**
6270
* Generate vercel function
6371
* @param outputDir The generated source code output directory
72+
* @param route The route information
6473
* @param type The type of function to generate
6574
*/
6675
async function generateVercelFunction(
6776
outputDir: string,
68-
type: "webhook" | "send-message",
77+
type: "webhook" | "api",
78+
route?: RouteInfo,
6979
): Promise<string> {
7080
switch (type) {
7181
case "webhook":
7282
return nunjucks.renderString(VERCEL_WEBHOOK_FUNCTION_TEMPLATE, {
7383
outputDir,
7484
});
75-
case "send-message":
76-
return nunjucks.renderString(VERCEL_SEND_MESSAGE_TEMPLATE, {
85+
case "api":
86+
// import the api
87+
const api = await route?.api!();
88+
const supportedMethods = [];
89+
for (const method of ["GET", "POST", "PUT", "DELETE", "PATCH"]) {
90+
//@ts-expect-error
91+
if (api[method]) {
92+
supportedMethods.push(method);
93+
}
94+
}
95+
96+
return nunjucks.renderString(VERCEL_API_ROUTE_TEMPLATE, {
7797
outputDir,
98+
methods: supportedMethods,
99+
path: route!.route,
78100
});
79101
default:
80102
throw new Error(`Unsupported function type: ${type}`);
@@ -174,18 +196,49 @@ async function writeVercelConfigFile() {
174196
);
175197
}
176198

199+
/**
200+
* Recursively process routes and generate API functions
201+
* @param routes Array of route information
202+
* @param outputFolder Output folder path
203+
*/
204+
async function processRoutes(routes: RouteInfo[], outputFolder: string) {
205+
for (const route of routes) {
206+
// Generate API function if route has api field
207+
if (route.api) {
208+
const apiFunction = await generateVercelFunction(
209+
outputFolder,
210+
"api",
211+
route,
212+
);
213+
await writeVercelFunctionToDisk(route.route, apiFunction);
214+
}
215+
216+
// Recursively process sub-routes if they exist
217+
if (route.subRoutes && route.subRoutes.length > 0) {
218+
await processRoutes(route.subRoutes, outputFolder);
219+
}
220+
}
221+
}
222+
177223
export async function buildVercel({ outputFolder }: Options) {
178224
await removeVercelFolder();
179225
// create output folder
180226
await fs.mkdir(VERCEL_OUTPUT_FOLDER, { recursive: true });
227+
// get route file
228+
const routeFile = path.resolve(outputFolder, "main.js");
229+
// node require
230+
const nativeRequire = require("module").createRequire(process.cwd());
231+
delete nativeRequire.cache[nativeRequire.resolve(routeFile)];
232+
// Now import the fresh version
233+
const { ROUTE_FILE } = nativeRequire(routeFile);
234+
181235
// build webhook function
182236
const webhookFunction = await generateVercelFunction(outputFolder, "webhook");
183-
const sendMessageFunction = await generateVercelFunction(
184-
outputFolder,
185-
"send-message",
186-
);
187237
// write webhook function to disk
188238
await writeVercelFunctionToDisk("api/webhook", webhookFunction);
189-
await writeVercelFunctionToDisk("api/message", sendMessageFunction);
239+
240+
// Process all routes recursively
241+
await processRoutes(ROUTE_FILE.routes, outputFolder);
242+
190243
await writeVercelConfigFile();
191244
}

packages/rxbot/src/command/commands/build/build.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,22 @@ export default async function runBuild(
2323
},
2424
) {
2525
Logger.info(
26-
`Building the app in $${srcFolder} for ${options.environment} environment`,
26+
`Building the app in ${srcFolder} for ${options.environment} environment`,
2727
"blue",
2828
);
2929

30-
Logger.info("Build app completed successfully", "green");
3130
switch (options.environment) {
3231
case "vercel":
3332
Logger.info("Building specifically for Vercel", "blue");
3433
await buildApp(srcFolder, outputFolder, options.hasAdapterFile ?? true, {
3534
plugins: [new VercelEnvironmentPlugin()],
3635
});
3736
await buildVercel({
38-
outputFolder: path.resolve(outputFolder, DEFAULT_OUTPUT_FOLDER),
37+
outputFolder: path.resolve(
38+
process.cwd(),
39+
outputFolder,
40+
DEFAULT_OUTPUT_FOLDER,
41+
),
3942
});
4043
Logger.info("Build for Vercel completed successfully", "green");
4144
break;
@@ -44,6 +47,7 @@ export default async function runBuild(
4447
sourceMap: options.sourceMap,
4548
plugins: [],
4649
});
50+
Logger.info("Build app completed successfully", "green");
4751
break;
4852
}
4953
}

packages/rxbot/src/command/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import build from "./commands/build/build";
2+
import runBuild from "./commands/build/build";
23
import deploy from "./commands/deploy/deploy";
34
import dev from "./commands/dev";
45

56
import { Config } from "./commands/deploy/deploy";
67

7-
export { build, dev, deploy };
8+
export { build, dev, deploy, runBuild };
89
export type { Config };

packages/rxbot/src/templates/vercel/templates.ts

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -30,34 +30,36 @@ export async function POST(request: Request) {
3030
}
3131
`;
3232

33-
export const VERCEL_SEND_MESSAGE_TEMPLATE = `
33+
export const VERCEL_API_ROUTE_TEMPLATE = `
3434
import { Core } from "@rx-lab/core";
3535
import { adapter, storage, ROUTE_FILE } from "{{outputDir}}/main.js";
3636
import { waitUntil } from "@vercel/functions";
37+
import { matchRouteWithPath } from "@rx-lab/router";
3738
3839
global.core = null;
39-
export async function POST(request: Request) {
40-
if (global.core === null) {
41-
global.core = await Core.Start({
42-
adapter,
43-
storage,
44-
routeFile: ROUTE_FILE,
45-
});
46-
}
47-
const result = async () => {
48-
await global.core.sendMessage(await request.json());
49-
}
50-
51-
waitUntil(result().catch(console.error));
52-
return new Response(
53-
JSON.stringify({
54-
status: "ok",
55-
}),
56-
{
57-
headers: {
58-
"content-type": "application/json",
59-
},
60-
},
61-
);
40+
{% for method in methods %}
41+
export async function {{method}}(req: Request) {
42+
if (global.core === null) {
43+
global.core = await Core.Start({
44+
adapter,
45+
storage,
46+
routeFile: ROUTE_FILE,
47+
});
48+
}
49+
const matchedRoute = await matchRouteWithPath(
50+
ROUTE_FILE.routes,
51+
"{{path}}"
52+
);
53+
const api = matchedRoute.api;
54+
const handler = api["{{method}}"];
55+
const functionRequest = {
56+
req,
57+
storage: storage,
58+
routeInfoFile: ROUTE_FILE,
59+
core: global.core,
60+
};
61+
const resp = await handler(functionRequest);
62+
return resp;
6263
}
64+
{% endfor %}
6365
`;

0 commit comments

Comments
 (0)