@seam-rpc/server 1.1.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/dist/bin/gen-client.js +101 -0
- package/dist/bin/gen-config.js +44 -0
- package/dist/bin/index.js +17 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +95 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -142,3 +142,19 @@ export async function updateUser(userId: string, userData: UserData): Promise<vo
|
|
|
142
142
|
- The generated client files contain all imports from the api implementation file in the backend that import from the current relative folder (`./`). This is the simplest way I have to include imports (at least for now). It may import functions and unused symbols but that shouldn't be too worrying.
|
|
143
143
|
- Don't include backend/server functions inside the server api files.
|
|
144
144
|
- Only exported functions will be included in the client generated files.
|
|
145
|
+
|
|
146
|
+
## Supported types
|
|
147
|
+
SeamRPC supports the following types (at least for now):
|
|
148
|
+
- string
|
|
149
|
+
- number
|
|
150
|
+
- boolean
|
|
151
|
+
- array
|
|
152
|
+
- object
|
|
153
|
+
- null
|
|
154
|
+
- undefined
|
|
155
|
+
|
|
156
|
+
Classes are technically supported, in that the data is serialized to JSON.
|
|
157
|
+
|
|
158
|
+
Other JavaScript types are not supported, although SeamRPC doesn't prevent you from using them, in which case they might lead to unexpected beahviour or even errors.
|
|
159
|
+
|
|
160
|
+
The Date object type is not supported (at least for now). However, you can use `number` and pass `Date.now()` or `string` and pass `new Date().toString()`. This is not different than a normal HTTP request using JSON. SeamRPC also uses JSON behind the scenes, that's why there's these limitations, which could be overcome but I've decided not to because it would probably add more overhead to the logic.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import fg from "fast-glob";
|
|
5
|
+
export async function genClient() {
|
|
6
|
+
const args = process.argv;
|
|
7
|
+
let config;
|
|
8
|
+
if (args.length == 3) {
|
|
9
|
+
if (!fs.existsSync("./seam-rpc.config.json"))
|
|
10
|
+
return console.error("\x1b[31mCommand arguments omitted and no config file found.\x1b[0m\n"
|
|
11
|
+
+ "Either define a config file with \x1b[36mseam-rpc gen-config\x1b[0m or generate the client files using \x1b[36mseam-rpc gen-client <input-files> <output-folder> [global-types-file]\x1b[0m.");
|
|
12
|
+
config = JSON.parse(fs.readFileSync("./seam-rpc.config.json", "utf-8"));
|
|
13
|
+
}
|
|
14
|
+
else if (args.length == 5 || args.length == 6) {
|
|
15
|
+
config = {
|
|
16
|
+
inputFiles: args[3],
|
|
17
|
+
outputFolder: args[4]
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
return console.error("Usage: seam-rpc gen-client <input-files> <output-folder>");
|
|
22
|
+
}
|
|
23
|
+
const inputFiles = await fg(config.inputFiles);
|
|
24
|
+
const outputPath = path.resolve(config.outputFolder);
|
|
25
|
+
const rootPath = path.resolve(".");
|
|
26
|
+
try {
|
|
27
|
+
const outputFiles = [];
|
|
28
|
+
for (const inputFile of inputFiles) {
|
|
29
|
+
const outputFile = generateClientFile(inputFile, outputPath);
|
|
30
|
+
outputFiles.push(removeRootPath(outputFile, rootPath));
|
|
31
|
+
}
|
|
32
|
+
console.log("\x1b[32m%s\x1b[0m\n\x1b[36m%s\x1b[0m", `✅ Successfully generated client files at ${removeRootPath(outputPath, rootPath)}`, `${outputFiles.join("\n")}`);
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
console.error("❌ Failed to generate client file:", err.message);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function removeRootPath(path, rootPath) {
|
|
40
|
+
return "." + path.slice(rootPath.length);
|
|
41
|
+
}
|
|
42
|
+
function generateClientFile(inputFile, outputPath) {
|
|
43
|
+
const file = path.resolve(process.cwd(), inputFile);
|
|
44
|
+
if (!fs.existsSync(file)) {
|
|
45
|
+
console.error(`File ${file} not found`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
const imports = ["import { callApi, SeamFile, ISeamFile } from \"@seam-rpc/client\";"];
|
|
49
|
+
const apiDef = [];
|
|
50
|
+
const typeDefs = [];
|
|
51
|
+
const routerName = path.basename(file, path.extname(file));
|
|
52
|
+
const fileContent = fs.readFileSync(file, "utf-8");
|
|
53
|
+
const sourceFile = ts.createSourceFile(file, fileContent, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
54
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
55
|
+
if (ts.isImportDeclaration(node)) {
|
|
56
|
+
const moduleSpecifier = node.moduleSpecifier.getText().replace(/['"]/g, "");
|
|
57
|
+
if (moduleSpecifier.startsWith("./"))
|
|
58
|
+
imports.push(node.getText());
|
|
59
|
+
}
|
|
60
|
+
else if (ts.isFunctionDeclaration(node) && hasExportModifier(node)) {
|
|
61
|
+
if (!node.name) {
|
|
62
|
+
console.error("Missing function name.");
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
const funcName = node.name.getText();
|
|
66
|
+
const jsDoc = ts.getJSDocCommentsAndTags(node).map(e => e.getFullText()).filter(Boolean).join("\n");
|
|
67
|
+
let signature = `${jsDoc}\nexport function ${funcName}(`;
|
|
68
|
+
const params = node.parameters.filter(p => !(p.type && p.type.getText() === "SeamContext"));
|
|
69
|
+
const paramsText = params
|
|
70
|
+
.map((p) => {
|
|
71
|
+
const paramName = p.name.getText();
|
|
72
|
+
const optional = p.questionToken ? "?" : "";
|
|
73
|
+
const type = p.type ? p.type.getText() : "any";
|
|
74
|
+
return `${paramName}${optional}: ${type}`;
|
|
75
|
+
})
|
|
76
|
+
.join(", ");
|
|
77
|
+
const returnTypeText = node.type?.getText() ?? "any";
|
|
78
|
+
signature += `${paramsText}): ${returnTypeText} { return callApi("${routerName}", "${funcName}", [${params.map(e => e.name.getText()).join(", ")}]); }`;
|
|
79
|
+
apiDef.push(signature);
|
|
80
|
+
}
|
|
81
|
+
else if ((ts.isInterfaceDeclaration(node) ||
|
|
82
|
+
ts.isTypeAliasDeclaration(node) ||
|
|
83
|
+
ts.isEnumDeclaration(node))
|
|
84
|
+
&& hasExportModifier(node)) {
|
|
85
|
+
const text = node.getFullText(sourceFile).trim();
|
|
86
|
+
typeDefs.push(text);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
const content = [imports.join("\n"), typeDefs.join("\n"), apiDef.join("\n")].join("\n");
|
|
90
|
+
fs.writeFileSync(path.resolve(outputPath, path.basename(file)), content, "utf-8");
|
|
91
|
+
return file;
|
|
92
|
+
}
|
|
93
|
+
function hasExportModifier(node) {
|
|
94
|
+
if (ts.isVariableStatement(node) ||
|
|
95
|
+
ts.isFunctionDeclaration(node) ||
|
|
96
|
+
ts.isClassDeclaration(node) ||
|
|
97
|
+
ts.isInterfaceDeclaration(node)) {
|
|
98
|
+
return !!node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import readline from "readline";
|
|
3
|
+
export async function genConfig() {
|
|
4
|
+
const args = process.argv;
|
|
5
|
+
let inputFiles = "./src/api/*";
|
|
6
|
+
let outputFolder = "../client/src/api";
|
|
7
|
+
if (args.length == 5) {
|
|
8
|
+
inputFiles = args[3];
|
|
9
|
+
outputFolder = args[4];
|
|
10
|
+
}
|
|
11
|
+
else if (args.length > 3) {
|
|
12
|
+
return console.error("Usage: seam-rpc gen-config [input-files] [output-folder]");
|
|
13
|
+
}
|
|
14
|
+
if (fs.existsSync("./seam-rpc.config.json")) {
|
|
15
|
+
const rl = readline.createInterface({
|
|
16
|
+
input: process.stdin,
|
|
17
|
+
output: process.stdout,
|
|
18
|
+
});
|
|
19
|
+
rl.question("Config file already exists. Do you want to overwrite it? [Y/n] ", answer => {
|
|
20
|
+
if (answer && answer.toLowerCase() != "y" && answer.toLowerCase() != "yes") {
|
|
21
|
+
console.log("Operation canceled.");
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
rl.close();
|
|
25
|
+
generateConfig();
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
generateConfig();
|
|
30
|
+
}
|
|
31
|
+
function generateConfig() {
|
|
32
|
+
const config = {
|
|
33
|
+
inputFiles,
|
|
34
|
+
outputFolder
|
|
35
|
+
};
|
|
36
|
+
try {
|
|
37
|
+
fs.writeFileSync("./seam-rpc.config.json", JSON.stringify(config, null, 4), "utf-8");
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
console.log("\x1b[31mFailed to generate config file ./seam-rpc.config.json\x1b[0m\n" + e);
|
|
41
|
+
}
|
|
42
|
+
console.log(`\x1b[32mSuccessfully generated config file ./seam-rpc.config.json\x1b[0m`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { genClient } from "./gen-client.js";
|
|
3
|
+
import { genConfig } from "./gen-config.js";
|
|
4
|
+
main();
|
|
5
|
+
function main() {
|
|
6
|
+
const args = process.argv;
|
|
7
|
+
switch (args[2]) {
|
|
8
|
+
case "gen-client":
|
|
9
|
+
genClient();
|
|
10
|
+
break;
|
|
11
|
+
case "gen-config":
|
|
12
|
+
genConfig();
|
|
13
|
+
break;
|
|
14
|
+
default:
|
|
15
|
+
console.log("Commands:\n- seam-rpc gen-client <input-files> <output-folder>\n- seam-rpc gen-config [input-files] [output-folder]");
|
|
16
|
+
}
|
|
17
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { SeamFile, ISeamFile } from "@seam-rpc/core";
|
|
2
|
-
import { Express, RequestHandler } from "express";
|
|
2
|
+
import { Express, NextFunction, Request, RequestHandler, Response } from "express";
|
|
3
3
|
export { SeamFile, ISeamFile };
|
|
4
4
|
export interface RouterDefinition {
|
|
5
5
|
[funcName: string]: (...args: any[]) => Promise<any>;
|
|
6
6
|
}
|
|
7
|
-
export declare function createSeamSpace(app: Express, fileHandler?: RequestHandler): SeamSpace
|
|
7
|
+
export declare function createSeamSpace(app: Express, fileHandler?: RequestHandler): Promise<SeamSpace>;
|
|
8
8
|
export declare class SeamSpace {
|
|
9
9
|
private app;
|
|
10
10
|
private fileHandler;
|
|
@@ -12,3 +12,8 @@ export declare class SeamSpace {
|
|
|
12
12
|
constructor(app: Express, fileHandler: RequestHandler);
|
|
13
13
|
createRouter(path: string, routerDefinition: RouterDefinition): void;
|
|
14
14
|
}
|
|
15
|
+
export interface SeamContext {
|
|
16
|
+
request: Request;
|
|
17
|
+
response: Response;
|
|
18
|
+
next: NextFunction;
|
|
19
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { SeamFile, extractFiles, injectFiles } from "@seam-rpc/core";
|
|
2
|
+
import express, { Router } from "express";
|
|
3
|
+
import FormData from "form-data";
|
|
4
|
+
export { SeamFile };
|
|
5
|
+
;
|
|
6
|
+
export async function createSeamSpace(app, fileHandler) {
|
|
7
|
+
if (!fileHandler) {
|
|
8
|
+
let multer;
|
|
9
|
+
try {
|
|
10
|
+
multer = (await import("multer")).default;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
throw new Error("Multer is required as default file handler. Install it or provide a custom fileHandler.");
|
|
14
|
+
}
|
|
15
|
+
const upload = multer();
|
|
16
|
+
fileHandler = upload.any();
|
|
17
|
+
}
|
|
18
|
+
return new SeamSpace(app, fileHandler);
|
|
19
|
+
}
|
|
20
|
+
export class SeamSpace {
|
|
21
|
+
constructor(app, fileHandler) {
|
|
22
|
+
this.app = app;
|
|
23
|
+
this.fileHandler = fileHandler;
|
|
24
|
+
this.jsonParser = express.json();
|
|
25
|
+
}
|
|
26
|
+
createRouter(path, routerDefinition) {
|
|
27
|
+
const router = Router();
|
|
28
|
+
router.post("/:funcName", async (req, res, next) => {
|
|
29
|
+
if (!(req.params.funcName in routerDefinition))
|
|
30
|
+
return res.sendStatus(404);
|
|
31
|
+
const contentType = req.headers["content-type"] || "";
|
|
32
|
+
const runMiddleware = (middleware) => new Promise((resolve, reject) => middleware(req, res, err => (err ? reject(err) : resolve())));
|
|
33
|
+
if (contentType.startsWith("application/json")) {
|
|
34
|
+
await runMiddleware(this.jsonParser);
|
|
35
|
+
}
|
|
36
|
+
else if (contentType.startsWith("multipart/form-data")) {
|
|
37
|
+
await runMiddleware(this.fileHandler);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
return res.status(415).send("Unsupported content type");
|
|
41
|
+
}
|
|
42
|
+
let args;
|
|
43
|
+
if (contentType.startsWith("application/json")) {
|
|
44
|
+
args = req.body;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
// multipart/form-data
|
|
48
|
+
args = JSON.parse(req.body.json);
|
|
49
|
+
const paths = JSON.parse(req.body.paths);
|
|
50
|
+
const files = (req.files ?? []).map((file, index) => ({
|
|
51
|
+
path: paths[index],
|
|
52
|
+
file: new SeamFile(file.buffer, file.originalname, file.mimetype),
|
|
53
|
+
}));
|
|
54
|
+
injectFiles(args, files);
|
|
55
|
+
}
|
|
56
|
+
let result;
|
|
57
|
+
try {
|
|
58
|
+
const ctx = {
|
|
59
|
+
request: req,
|
|
60
|
+
response: res,
|
|
61
|
+
next
|
|
62
|
+
};
|
|
63
|
+
result = await routerDefinition[req.params.funcName](...args, ctx);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
console.error(`Error at API function at router "${path}". Sent error to client.`, error);
|
|
67
|
+
res.status(400).send({ error: String(error) });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const { json, files, paths } = extractFiles({ result });
|
|
72
|
+
if (files.length === 0) {
|
|
73
|
+
res.json(json);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const form = new FormData();
|
|
77
|
+
form.append("json", JSON.stringify(json));
|
|
78
|
+
form.append("paths", JSON.stringify(paths));
|
|
79
|
+
files.forEach((file, index) => {
|
|
80
|
+
form.append(`file-${index}`, Buffer.from(file.data), {
|
|
81
|
+
filename: file.fileName || `file-${index}`,
|
|
82
|
+
contentType: file.mimeType || "application/octet-stream",
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
res.writeHead(200, form.getHeaders());
|
|
86
|
+
form.pipe(res);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
console.error(`Internal error at API function at router "${path}". Returned 500 Internal Server Error to client.`, error);
|
|
90
|
+
res.status(500).send({ error: String(error) });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
this.app.use(path, router);
|
|
94
|
+
}
|
|
95
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seam-rpc/server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@types/express": "^5.0.3",
|
|
31
|
+
"@types/multer": "^2.0.0",
|
|
31
32
|
"@types/node": "^24.3.1",
|
|
32
33
|
"express": "^5.1.0",
|
|
33
34
|
"multer": "^2.0.2"
|