@seam-rpc/server 4.2.0 → 4.3.1

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.
@@ -0,0 +1,193 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import fg from "fast-glob";
4
+ import ts from "typescript";
5
+ import * as z from "zod";
6
+ import { zodToTs, createAuxiliaryTypeStore, printNode } from "zod-to-ts";
7
+ import { existsSync, writeFileSync } from "fs";
8
+ export async function genClient() {
9
+ const args = process.argv;
10
+ let config;
11
+ if (args.length == 3) {
12
+ // Use config
13
+ // Check if config exists
14
+ if (!fs.existsSync("./seam-rpc.config.json"))
15
+ return console.error("\x1b[31mCommand arguments omitted and no config file found.\x1b[0m\n"
16
+ + "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.");
17
+ // Load config from config file
18
+ config = JSON.parse(fs.readFileSync("./seam-rpc.config.json", "utf-8"));
19
+ }
20
+ else if (args.length == 5 || args.length == 6) {
21
+ // Use command args
22
+ // Load config from command args
23
+ config = {
24
+ inputFiles: args[3],
25
+ outputFolder: args[4]
26
+ };
27
+ }
28
+ else {
29
+ // Invalid command usage
30
+ return console.error("Usage: seam-rpc gen-client <input-files> <output-folder>");
31
+ }
32
+ const inputFiles = await fg(config.inputFiles);
33
+ const outputPath = path.resolve(config.outputFolder);
34
+ const rootPath = path.resolve(".");
35
+ try {
36
+ const outputFiles = [];
37
+ for (const inputFile of inputFiles) {
38
+ // console.log(getProcedureInfo(inputFile));
39
+ const outputFile = await generateClientFile(inputFile, outputPath);
40
+ outputFiles.push(removeRootPath(outputFile, rootPath));
41
+ }
42
+ console.log("\x1b[32m%s\x1b[0m\n\x1b[36m%s\x1b[0m", `✅ Successfully generated client files at ${removeRootPath(outputPath, rootPath)}`, `${outputFiles.join("\n")}`);
43
+ }
44
+ catch (err) {
45
+ console.error("❌ Failed to generate client file:", err.message);
46
+ process.exit(1);
47
+ }
48
+ }
49
+ // interface ProcedureInfo {
50
+ // name: string;
51
+ // inputType: string;
52
+ // outputType: string;
53
+ // comments: string;
54
+ // }
55
+ // export function getProcedureInfo(filePath: string): ProcedureInfo[] {
56
+ // const fullPath = path.resolve(filePath);
57
+ // const sourceText = fs.readFileSync(fullPath, "utf8");
58
+ // const sourceFile = ts.createSourceFile(
59
+ // fullPath,
60
+ // sourceText,
61
+ // ts.ScriptTarget.Latest,
62
+ // true,
63
+ // ts.ScriptKind.TS
64
+ // );
65
+ // const procedures: ProcedureInfo[] = [];
66
+ // function visit(node: ts.Node) {
67
+ // // Look for const assignments
68
+ // if (ts.isVariableStatement(node)) {
69
+ // node.declarationList.declarations.forEach((decl) => {
70
+ // if (ts.isIdentifier(decl.name) && decl.initializer) {
71
+ // let varName = decl.name.text;
72
+ // let inputType = "";
73
+ // let outputType = "";
74
+ // let comments = "";
75
+ // const jsDocs = ts.getJSDocCommentsAndTags(node); // node = VariableStatement
76
+ // // Get JSDoc comments
77
+ // if (jsDocs.length > 0) {
78
+ // comments = jsDocs.map(doc => doc.getText()).join("\n");
79
+ // }
80
+ // // Check for chain calls: .input(...).output(...)
81
+ // if (ts.isCallExpression(decl.initializer) || ts.isPropertyAccessExpression(decl.initializer)) {
82
+ // function extractChain(expr: ts.Expression) {
83
+ // if (ts.isCallExpression(expr)) {
84
+ // if (ts.isPropertyAccessExpression(expr.expression)) {
85
+ // const propName = expr.expression.name.text;
86
+ // if (propName === "input" && expr.arguments.length > 0) {
87
+ // inputType = expr.arguments[0].getText();
88
+ // } else if (propName === "output" && expr.arguments.length > 0) {
89
+ // outputType = expr.arguments[0].getText();
90
+ // }
91
+ // extractChain(expr.expression.expression);
92
+ // }
93
+ // } else if (ts.isPropertyAccessExpression(expr)) {
94
+ // extractChain(expr.expression);
95
+ // }
96
+ // }
97
+ // extractChain(decl.initializer);
98
+ // }
99
+ // if (inputType || outputType || comments) {
100
+ // procedures.push({ name: varName, inputType, outputType, comments });
101
+ // }
102
+ // }
103
+ // });
104
+ // }
105
+ // ts.forEachChild(node, visit);
106
+ // }
107
+ // visit(sourceFile);
108
+ // return procedures;
109
+ // }
110
+ function removeRootPath(path, rootPath) {
111
+ return "." + path.slice(rootPath.length);
112
+ }
113
+ async function generateClientFile(sourcePath, outputPath) {
114
+ const sourceFile = path.resolve(process.cwd(), sourcePath);
115
+ if (!existsSync(sourceFile)) {
116
+ console.error(`File ${sourceFile} not found`);
117
+ process.exit(1);
118
+ }
119
+ const module = await import("file://" + sourceFile);
120
+ const procedures = module.default;
121
+ const routerName = path.basename(sourceFile, path.extname(sourceFile));
122
+ let fileContent = `/* Auto-generated by SeamRPC - DO NOT EDIT */
123
+
124
+ import { callApi } from \"@seam-rpc/client\";
125
+
126
+ `;
127
+ // Parse the source file using TypeScript API
128
+ const program = ts.createProgram([sourceFile], {});
129
+ const tsFile = program.getSourceFile(sourceFile);
130
+ // Helper to get JSDoc comment for a variable
131
+ function getJsDocComment(node) {
132
+ const jsDocs = node.jsDoc;
133
+ if (!jsDocs || jsDocs.length === 0)
134
+ return "";
135
+ return jsDocs.map(doc => doc.comment).filter(Boolean).join("\n");
136
+ }
137
+ const functions = [];
138
+ for (const [procName, proc] of Object.entries(procedures)) {
139
+ // Input
140
+ const input = [];
141
+ if (proc._def.input) {
142
+ for (const [paramName, param] of Object.entries(proc._def.input)) {
143
+ input.push(`${paramName}${param instanceof z.ZodOptional ? "?" : ""}: ${convert(param)}`);
144
+ }
145
+ }
146
+ // Output
147
+ const returnType = proc._def.output ? convert(proc._def.output) : "void";
148
+ // Input
149
+ const hasInput = input.length != 0;
150
+ const params = hasInput ? `input: { ${input.join(", ")} }` : "";
151
+ // Function body
152
+ const body = `return callApi("${routerName}", "${procName}"${hasInput ? ", input" : ""});`;
153
+ // Comments
154
+ let comments = "";
155
+ ts.forEachChild(tsFile, node => {
156
+ if (ts.isVariableStatement(node)) {
157
+ node.declarationList.declarations.forEach(decl => {
158
+ console.log(decl.name.getText(), procName);
159
+ if (decl.name.getText() === procName) {
160
+ const doc = getJsDocComment(decl);
161
+ if (doc)
162
+ comments = `/** ${doc} */\n`;
163
+ }
164
+ });
165
+ }
166
+ });
167
+ const func = `${comments}export function ${procName}(${params}): Promise<${returnType}> { ${body} }`;
168
+ functions.push(func);
169
+ }
170
+ fileContent += functions.join("\n\n");
171
+ writeFileSync(`${outputPath}/${routerName}.ts`, fileContent, "utf-8");
172
+ return sourceFile;
173
+ }
174
+ function convert(schema) {
175
+ const auxiliaryTypeStore = createAuxiliaryTypeStore();
176
+ const { node } = zodToTs(schema, { auxiliaryTypeStore });
177
+ return printNode(node);
178
+ }
179
+ // function zodToTs(schema: z.ZodType): { value: string, definition?: string } {
180
+ // if (schema instanceof z.ZodString) {
181
+ // return { value: "string" };
182
+ // }
183
+ // if (schema instanceof z.ZodNumber) {
184
+ // return { value: "number" };
185
+ // }
186
+ // if (schema instanceof z.ZodArray) {
187
+ // return { value: zodToTs(schema.def.type) + "[]" };
188
+ // }
189
+ // if (schema instanceof z.ZodOptional) {
190
+ // return {value: };
191
+ // }
192
+ // return { value: "any" };
193
+ // }
@@ -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
+ }
@@ -4,5 +4,9 @@ interface GenerateOptions {
4
4
  jsPath: string;
5
5
  outputPath: string;
6
6
  }
7
+ export declare function getProcedureMetadata(filePath: string): Record<string, {
8
+ comment: string;
9
+ returnType?: string;
10
+ }>;
7
11
  export declare function generateClientFile({ tsPath, jsPath, outputPath, }: GenerateOptions): Promise<string>;
8
12
  export {};
@@ -15,20 +15,19 @@ export async function generateClient() {
15
15
  function removeRootPath(path) {
16
16
  return "." + path.slice(rootPath.length);
17
17
  }
18
- try {
19
- const outputFiles = [];
20
- for (let i = 0; i < sourceFiles.length; i++) {
21
- const sourceFile = sourceFiles[i];
22
- const path = await generateClientFile({ tsPath: sourceFile, jsPath: compiledFolder, outputPath });
23
- const symb = i < sourceFiles.length - 1 ? " ├╴" : " └╴";
24
- outputFiles.push(symb + removeRootPath(path));
25
- }
26
- console.log("\x1b[32m\x1b[1m%s\x1b[0m\n\x1b[94m%s\x1b[0m", `✅ Successfully generated client files at ${config.outputFolder}`, `${outputFiles.join("\n")}`);
27
- }
28
- catch (err) {
29
- console.error("❌ Failed to generate client file:", err.message + "\n\n", err.stack);
30
- process.exit(1);
18
+ // try {
19
+ const outputFiles = [];
20
+ for (let i = 0; i < sourceFiles.length; i++) {
21
+ const sourceFile = sourceFiles[i];
22
+ const path = await generateClientFile({ tsPath: sourceFile, jsPath: compiledFolder, outputPath });
23
+ const symb = i < sourceFiles.length - 1 ? " ├╴" : " └╴";
24
+ outputFiles.push(symb + removeRootPath(path));
31
25
  }
26
+ console.log("\x1b[32m\x1b[1m%s\x1b[0m\n\x1b[94m%s\x1b[0m", `✅ Successfully generated client files at ${config.outputFolder}`, `${outputFiles.join("\n")}`);
27
+ // } catch (err: any) {
28
+ // console.error("❌ Failed to generate client file:", err.message + "\n\n", err.stack);
29
+ // process.exit(1);
30
+ // }
32
31
  }
33
32
  function loadConfig() {
34
33
  // Check if config exists
@@ -40,22 +39,64 @@ function loadConfig() {
40
39
  return JSON.parse(fs.readFileSync("./seam-rpc.config.json", "utf-8"));
41
40
  }
42
41
  // Extracts comments from TS file.
43
- function getProcedureComments(filePath) {
42
+ export function getProcedureMetadata(filePath) {
44
43
  const sourceText = fs.readFileSync(filePath, "utf8");
45
- const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true);
46
- const comments = {};
44
+ const program = ts.createProgram([filePath], {
45
+ target: ts.ScriptTarget.Latest,
46
+ module: ts.ModuleKind.CommonJS,
47
+ strict: true,
48
+ });
49
+ const checker = program.getTypeChecker();
50
+ const sourceFile = program.getSourceFile(filePath);
51
+ if (!sourceFile)
52
+ return {};
53
+ const metadata = {};
54
+ function getHandlerReturnType(node) {
55
+ let current = node;
56
+ while (current) {
57
+ if (ts.isCallExpression(current) &&
58
+ ts.isPropertyAccessExpression(current.expression)) {
59
+ const prop = current.expression;
60
+ if (prop.name.text === "handler") {
61
+ const handlerFn = current.arguments[0];
62
+ if (handlerFn &&
63
+ (ts.isArrowFunction(handlerFn) ||
64
+ ts.isFunctionExpression(handlerFn))) {
65
+ const signature = checker.getSignatureFromDeclaration(handlerFn);
66
+ if (!signature)
67
+ return;
68
+ const returnType = checker.getReturnTypeOfSignature(signature);
69
+ return checker.typeToString(returnType);
70
+ }
71
+ }
72
+ current = prop.expression;
73
+ continue;
74
+ }
75
+ break;
76
+ }
77
+ return;
78
+ }
47
79
  function visit(node) {
48
80
  if (ts.isVariableStatement(node)) {
49
81
  const comment = getFullComment(node, sourceText);
50
82
  for (const decl of node.declarationList.declarations) {
51
- if (ts.isIdentifier(decl.name))
52
- comments[decl.name.text] = comment;
83
+ if (!ts.isIdentifier(decl.name))
84
+ continue;
85
+ const name = decl.name.text;
86
+ let returnType;
87
+ if (decl.initializer) {
88
+ returnType = getHandlerReturnType(decl.initializer);
89
+ }
90
+ metadata[name] = {
91
+ comment,
92
+ returnType,
93
+ };
53
94
  }
54
95
  }
55
96
  ts.forEachChild(node, visit);
56
97
  }
57
98
  visit(sourceFile);
58
- return comments;
99
+ return metadata;
59
100
  }
60
101
  function getFullComment(node, sourceText) {
61
102
  const ranges = ts.getLeadingCommentRanges(sourceText, node.pos);
@@ -88,7 +129,7 @@ export async function generateClientFile({ tsPath, jsPath, outputPath, }) {
88
129
  const mod = await import("file://" + jsFile);
89
130
  const procedures = mod.default;
90
131
  const routerName = path.basename(jsFile, path.extname(jsFile));
91
- const comments = getProcedureComments(tsFile);
132
+ const procMetadata = getProcedureMetadata(tsFile);
92
133
  let output = `/**
93
134
  * +===================================+
94
135
  * | File auto-generated by SeamRPC. |
@@ -105,28 +146,35 @@ import { callApi, Result, RpcError } from "@seam-rpc/client";
105
146
  let inputType = "void";
106
147
  let hasInput = false;
107
148
  if (proc._def.input) {
108
- const fields = [];
109
- for (const [key, schema] of Object.entries(proc._def.input)) {
110
- const isOptional = schema.isOptional?.() ?? false;
111
- const tsType = convert(schema);
112
- fields.push(`${key}${isOptional ? "?" : ""}: ${tsType}`);
149
+ try {
150
+ const fields = [];
151
+ for (const [key, schema] of Object.entries(proc._def.input)) {
152
+ const isOptional = schema.isOptional?.() ?? false;
153
+ const tsType = convert(schema);
154
+ fields.push(`${key}${isOptional ? "?" : ""}: ${tsType}`);
155
+ }
156
+ if (fields.length > 0) {
157
+ inputType = `{ ${fields.join("; ")} }`;
158
+ hasInput = true;
159
+ }
113
160
  }
114
- if (fields.length > 0) {
115
- inputType = `{ ${fields.join("; ")} }`;
116
- hasInput = true;
161
+ catch (e) {
162
+ console.error(`Failed to process input of procedure "${name}".`);
163
+ process.exit(1);
117
164
  }
118
165
  }
119
- const dataType = proc._def.output ? convert(proc._def.output) : "void";
120
- const errors = Object.entries(proc._def.errors ?? {});
121
- const errorType = errors.map(e => `RpcError<"${e[0]}", ${convert(e[1])}>`).join(" | ");
122
- const fullReturnType = `Result<${dataType}${errors.length > 0 ? `, ${errorType}` : ""}>`;
166
+ // const dataType = proc._def.output ? convert(proc._def.output) : "void";
167
+ // const errors = Object.entries(proc._def.errors ?? {});
168
+ // const errorType = errors.map(e => `RpcError<"${e[0]}", ${convert(e[1] as any)}>`).join(" | ");
169
+ // const fullReturnType = `Promise<Result<${dataType}${errors.length > 0 ? `, ${errorType}` : ""}>>`;
170
+ const fullReturnType = procMetadata[name].returnType;
123
171
  const params = hasInput ? `input: ${inputType}` : "";
124
172
  const args = [`"${routerName}"`, `"${name}"`];
125
173
  if (hasInput)
126
174
  args.push("input");
127
175
  const call = `callApi(${args.join(", ")})`;
128
- const comment = comments[name] ? comments[name] + "\n" : "";
129
- const func = `${comment}export function ${name}(${params}): Promise<${fullReturnType}> {
176
+ const comment = procMetadata[name].comment ? procMetadata[name].comment + "\n" : "";
177
+ const func = `${comment}export function ${name}(${params}): ${fullReturnType} {
130
178
  return ${call};
131
179
  }`;
132
180
  functions.push(func);
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { Result } from "@seam-rpc/core";
1
2
  import EventEmitter from "events";
2
3
  import express, { Express, NextFunction, Request, RequestHandler, Response } from "express";
3
4
  import * as z from "zod";
@@ -29,7 +30,7 @@ export interface SeamEvents {
29
30
  inputValidationError: [error: unknown, context: SeamErrorContext];
30
31
  outputValidationError: [error: unknown, context: SeamErrorContext];
31
32
  }
32
- type ProcedureHandler<Input extends ProcedureInput, Output extends ProcedureOutput, Errors extends ProcedureErrors> = (options: ProcedureOptions<Input, Errors>) => z.infer<Output> | Promise<z.infer<Output>>;
33
+ type ProcedureHandler<Input extends ProcedureInput, Output extends ProcedureOutput, Errors extends ProcedureErrors> = (options: ProcedureOptions<Input, Errors>) => Result<z.infer<Output>, unknown> | Promise<Result<z.infer<Output>, unknown>>;
33
34
  interface ProcedureOptions<Input extends ProcedureInput, Errors extends ProcedureErrors> {
34
35
  input: Simplify<ProcedureInputData<Input>>;
35
36
  ctx: SeamContext;
@@ -0,0 +1,54 @@
1
+ export class ValString {
2
+ constructor(value) {
3
+ this._isValid = true;
4
+ this._value = value;
5
+ }
6
+ get isValid() {
7
+ return this._isValid;
8
+ }
9
+ min(value) {
10
+ if (this._isValid)
11
+ this._isValid = this._value.length >= value;
12
+ return this;
13
+ }
14
+ max(value) {
15
+ if (this._isValid)
16
+ this._isValid = this._value.length <= value;
17
+ return this;
18
+ }
19
+ length(value) {
20
+ if (this._isValid)
21
+ this._isValid = this._value.length == value;
22
+ return this;
23
+ }
24
+ startsWith(value) {
25
+ if (this._isValid)
26
+ this._isValid = this._value.startsWith(value);
27
+ return this;
28
+ }
29
+ endsWith(value) {
30
+ if (this._isValid)
31
+ this._isValid = this._value.endsWith(value);
32
+ return this;
33
+ }
34
+ includes(value) {
35
+ if (this._isValid)
36
+ this._isValid = this._value.includes(value);
37
+ return this;
38
+ }
39
+ regex(regExpr) {
40
+ // if (this._isValid)
41
+ // this._isValid = this._value.startsWith(value);
42
+ return this;
43
+ }
44
+ email() {
45
+ return this;
46
+ }
47
+ }
48
+ function createUser(name, age, data) {
49
+ name.min(3).max(50);
50
+ age.gte(1).lt(150);
51
+ data.description?.max(250);
52
+ }
53
+ function validate(data) {
54
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seam-rpc/server",
3
- "version": "4.2.0",
3
+ "version": "4.3.1",
4
4
  "main": "dist/index.js",
5
5
  "type": "module",
6
6
  "bin": {