@seam-rpc/server 1.1.1 → 1.1.2
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/dist/bin/gen-client.js +103 -0
- package/dist/bin/gen-config.js +44 -0
- package/dist/bin/index.js +17 -0
- package/dist/index.js +90 -0
- package/package.json +1 -1
|
@@ -0,0 +1,103 @@
|
|
|
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 paramsText = node.parameters
|
|
69
|
+
.map((p) => {
|
|
70
|
+
const paramName = p.name.getText();
|
|
71
|
+
const optional = p.questionToken ? "?" : "";
|
|
72
|
+
const type = p.type ? p.type.getText() : "any";
|
|
73
|
+
return `${paramName}${optional}: ${type}`;
|
|
74
|
+
})
|
|
75
|
+
.join(", ");
|
|
76
|
+
const returnTypeText = node.type?.getText() ?? "any";
|
|
77
|
+
const finalReturnType = returnTypeText.startsWith("Promise<")
|
|
78
|
+
? returnTypeText
|
|
79
|
+
: `Promise<${returnTypeText}>`;
|
|
80
|
+
signature += `${paramsText}): ${finalReturnType} { return callApi("${routerName}", "${funcName}", [${node.parameters.map(e => e.name.getText()).join(", ")}]); }`;
|
|
81
|
+
apiDef.push(signature);
|
|
82
|
+
}
|
|
83
|
+
else if ((ts.isInterfaceDeclaration(node) ||
|
|
84
|
+
ts.isTypeAliasDeclaration(node) ||
|
|
85
|
+
ts.isEnumDeclaration(node))
|
|
86
|
+
&& hasExportModifier(node)) {
|
|
87
|
+
const text = node.getFullText(sourceFile).trim();
|
|
88
|
+
typeDefs.push(text);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
const content = [imports.join("\n"), typeDefs.join("\n"), apiDef.join("\n")].join("\n");
|
|
92
|
+
fs.writeFileSync(path.resolve(outputPath, path.basename(file)), content, "utf-8");
|
|
93
|
+
return file;
|
|
94
|
+
}
|
|
95
|
+
function hasExportModifier(node) {
|
|
96
|
+
if (ts.isVariableStatement(node) ||
|
|
97
|
+
ts.isFunctionDeclaration(node) ||
|
|
98
|
+
ts.isClassDeclaration(node) ||
|
|
99
|
+
ts.isInterfaceDeclaration(node)) {
|
|
100
|
+
return !!node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
@@ -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.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
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 function createSeamSpace(app, fileHandler) {
|
|
7
|
+
if (!fileHandler) {
|
|
8
|
+
let multer;
|
|
9
|
+
try {
|
|
10
|
+
multer = require("multer");
|
|
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
|
+
result = await routerDefinition[req.params.funcName](...args);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
console.error(`Error at API function at router "${path}". Sent error to client.`, error);
|
|
62
|
+
res.status(400).send({ error: String(error) });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const { json, files, paths } = extractFiles({ result });
|
|
67
|
+
if (files.length === 0) {
|
|
68
|
+
res.json(json);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const form = new FormData();
|
|
72
|
+
form.append("json", JSON.stringify(json));
|
|
73
|
+
form.append("paths", JSON.stringify(paths));
|
|
74
|
+
files.forEach((file, index) => {
|
|
75
|
+
form.append(`file-${index}`, Buffer.from(file.data), {
|
|
76
|
+
filename: file.fileName || `file-${index}`,
|
|
77
|
+
contentType: file.mimeType || "application/octet-stream",
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
res.writeHead(200, form.getHeaders());
|
|
81
|
+
form.pipe(res);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
console.error(`Internal error at API function at router "${path}". Returned 500 Internal Server Error to client.`, error);
|
|
85
|
+
res.status(500).send({ error: String(error) });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
this.app.use(path, router);
|
|
89
|
+
}
|
|
90
|
+
}
|