@seam-rpc/server 4.2.0 → 4.3.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.
@@ -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. |
@@ -116,17 +157,18 @@ import { callApi, Result, RpcError } from "@seam-rpc/client";
116
157
  hasInput = true;
117
158
  }
118
159
  }
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}` : ""}>`;
160
+ // const dataType = proc._def.output ? convert(proc._def.output) : "void";
161
+ // const errors = Object.entries(proc._def.errors ?? {});
162
+ // const errorType = errors.map(e => `RpcError<"${e[0]}", ${convert(e[1] as any)}>`).join(" | ");
163
+ // const fullReturnType = `Promise<Result<${dataType}${errors.length > 0 ? `, ${errorType}` : ""}>>`;
164
+ const fullReturnType = procMetadata[name].returnType;
123
165
  const params = hasInput ? `input: ${inputType}` : "";
124
166
  const args = [`"${routerName}"`, `"${name}"`];
125
167
  if (hasInput)
126
168
  args.push("input");
127
169
  const call = `callApi(${args.join(", ")})`;
128
- const comment = comments[name] ? comments[name] + "\n" : "";
129
- const func = `${comment}export function ${name}(${params}): Promise<${fullReturnType}> {
170
+ const comment = procMetadata[name].comment ? procMetadata[name].comment + "\n" : "";
171
+ const func = `${comment}export function ${name}(${params}): ${fullReturnType} {
130
172
  return ${call};
131
173
  }`;
132
174
  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.0",
4
4
  "main": "dist/index.js",
5
5
  "type": "module",
6
6
  "bin": {