orbit-rpc 0.0.1 → 0.2.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/dist/index.d.mts +12 -0
- package/dist/index.mjs +410 -0
- package/package.json +45 -5
- package/index.js +0 -1
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Plugin } from "vite";
|
|
2
|
+
|
|
3
|
+
//#region src/plugin.d.ts
|
|
4
|
+
interface OrbitRpcConfig {
|
|
5
|
+
/** routes ディレクトリのパス(デフォルト: "src/routes") */
|
|
6
|
+
routesDir?: string;
|
|
7
|
+
/** RPC エンドポイントのプレフィックス(デフォルト: "/rpc") */
|
|
8
|
+
rpcBase?: string;
|
|
9
|
+
}
|
|
10
|
+
declare function orbitRpc(config?: OrbitRpcConfig): Plugin[];
|
|
11
|
+
//#endregion
|
|
12
|
+
export { type OrbitRpcConfig, orbitRpc };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
//#region src/scanner.ts
|
|
4
|
+
/**
|
|
5
|
+
* routes ディレクトリから server.ts ファイルをスキャンし、
|
|
6
|
+
* エクスポートされた関数名と引数の型情報を抽出する。
|
|
7
|
+
* 隣接する schema.ts があれば Zod スキーマとの対応も解決する。
|
|
8
|
+
*/
|
|
9
|
+
async function scanServerModules(root, routesDir) {
|
|
10
|
+
const absoluteRoutesDir = path.resolve(root, routesDir);
|
|
11
|
+
if (!fs.existsSync(absoluteRoutesDir)) return [];
|
|
12
|
+
const modules = [];
|
|
13
|
+
await walk(absoluteRoutesDir, absoluteRoutesDir, modules);
|
|
14
|
+
return modules;
|
|
15
|
+
}
|
|
16
|
+
async function walk(dir, routesRoot, modules) {
|
|
17
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
18
|
+
const serverFile = entries.find((e) => e.isFile() && /^server\.ts$/.test(e.name));
|
|
19
|
+
if (serverFile) {
|
|
20
|
+
const filePath = path.join(dir, serverFile.name);
|
|
21
|
+
const routePrefix = dirToRoutePrefix(path.relative(routesRoot, dir));
|
|
22
|
+
const schemaPath = path.join(dir, "schema.ts");
|
|
23
|
+
const hasSchema = entries.some((e) => e.isFile() && e.name === "schema.ts");
|
|
24
|
+
const functions = extractExportedFunctions(filePath, hasSchema ? extractSchemaMap(schemaPath) : /* @__PURE__ */ new Map());
|
|
25
|
+
if (functions.length > 0) modules.push({
|
|
26
|
+
filePath,
|
|
27
|
+
schemaFilePath: hasSchema ? schemaPath : void 0,
|
|
28
|
+
routePrefix,
|
|
29
|
+
functions
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
for (const entry of entries) if (entry.isDirectory() && !entry.name.startsWith("_")) await walk(path.join(dir, entry.name), routesRoot, modules);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* schema.ts から「型名 → Zod スキーマ名」のマップを抽出する。
|
|
36
|
+
*
|
|
37
|
+
* 認識するパターン:
|
|
38
|
+
* export type TaskForm = z.infer<typeof taskFormSchema>;
|
|
39
|
+
* → Map { "TaskForm" => "taskFormSchema" }
|
|
40
|
+
*/
|
|
41
|
+
function extractSchemaMap(filePath) {
|
|
42
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
43
|
+
const map = /* @__PURE__ */ new Map();
|
|
44
|
+
for (const match of content.matchAll(/export\s+type\s+(\w+)\s*=\s*z\.infer\s*<\s*typeof\s+(\w+)\s*>/g)) map.set(match[1], match[2]);
|
|
45
|
+
return map;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* server.ts から export された関数を抽出する。
|
|
49
|
+
* 各引数の型名を解析し、schemaMap にマッチすれば schemaName を紐付ける。
|
|
50
|
+
*/
|
|
51
|
+
function extractExportedFunctions(filePath, schemaMap) {
|
|
52
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
53
|
+
const functions = [];
|
|
54
|
+
for (const match of content.matchAll(/export\s+(?:async\s+)?function\s+(\w+)\s*\(/g)) {
|
|
55
|
+
const name = match[1];
|
|
56
|
+
if (functions.some((f) => f.name === name)) continue;
|
|
57
|
+
const params = parseParams(extractBalancedParens(content, match.index + match[0].length - 1), schemaMap);
|
|
58
|
+
functions.push({
|
|
59
|
+
name,
|
|
60
|
+
params
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
for (const match of content.matchAll(/export\s+const\s+(\w+)\s*=\s*(?:async\s+)?\(/g)) {
|
|
64
|
+
const name = match[1];
|
|
65
|
+
if (functions.some((f) => f.name === name)) continue;
|
|
66
|
+
const params = parseParams(extractBalancedParens(content, match.index + match[0].length - 1), schemaMap);
|
|
67
|
+
functions.push({
|
|
68
|
+
name,
|
|
69
|
+
params
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return functions;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 開き括弧の位置から対応する閉じ括弧までの中身を返す。
|
|
76
|
+
* ネストされた括弧(デフォルト引数内の関数呼び出し等)に対応。
|
|
77
|
+
*/
|
|
78
|
+
function extractBalancedParens(content, openIndex) {
|
|
79
|
+
if (content[openIndex] !== "(") return "";
|
|
80
|
+
let depth = 0;
|
|
81
|
+
for (let i = openIndex; i < content.length; i++) if (content[i] === "(") depth++;
|
|
82
|
+
else if (content[i] === ")") {
|
|
83
|
+
depth--;
|
|
84
|
+
if (depth === 0) return content.slice(openIndex + 1, i);
|
|
85
|
+
}
|
|
86
|
+
return "";
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 関数の引数文字列をパースして FunctionParam 配列にする。
|
|
90
|
+
* デフォルト値内のカンマやネストに対応するため、括弧の深さを追跡する。
|
|
91
|
+
*
|
|
92
|
+
* 例: "input: TaskForm, signal?: AbortSignal"
|
|
93
|
+
* → [{ name: "input", typeName: "TaskForm", schemaName: "taskFormSchema" },
|
|
94
|
+
* { name: "signal", typeName: "AbortSignal" }]
|
|
95
|
+
*/
|
|
96
|
+
function parseParams(paramsStr, schemaMap) {
|
|
97
|
+
if (!paramsStr.trim()) return [];
|
|
98
|
+
const parts = splitTopLevelCommas(paramsStr);
|
|
99
|
+
const params = [];
|
|
100
|
+
for (const part of parts) {
|
|
101
|
+
const withoutDefault = part.replace(/\s*=\s*[\s\S]*$/, "").trim();
|
|
102
|
+
if (!withoutDefault) continue;
|
|
103
|
+
const paramMatch = withoutDefault.match(/^(\w+)\??\s*:\s*(\w+)/);
|
|
104
|
+
if (paramMatch) {
|
|
105
|
+
const name = paramMatch[1];
|
|
106
|
+
const typeName = paramMatch[2];
|
|
107
|
+
const schemaName = schemaMap.get(typeName);
|
|
108
|
+
params.push({
|
|
109
|
+
name,
|
|
110
|
+
typeName,
|
|
111
|
+
schemaName
|
|
112
|
+
});
|
|
113
|
+
} else {
|
|
114
|
+
const nameOnly = withoutDefault.match(/^(\w+)/);
|
|
115
|
+
if (nameOnly) params.push({ name: nameOnly[1] });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return params;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* トップレベルのカンマで文字列を分割する。
|
|
122
|
+
* 括弧 (), {}, <> 内のカンマは分割しない。
|
|
123
|
+
*/
|
|
124
|
+
function splitTopLevelCommas(str) {
|
|
125
|
+
const parts = [];
|
|
126
|
+
let current = "";
|
|
127
|
+
let depth = 0;
|
|
128
|
+
for (const ch of str) if (ch === "(" || ch === "{" || ch === "<") {
|
|
129
|
+
depth++;
|
|
130
|
+
current += ch;
|
|
131
|
+
} else if (ch === ")" || ch === "}" || ch === ">") {
|
|
132
|
+
depth--;
|
|
133
|
+
current += ch;
|
|
134
|
+
} else if (ch === "," && depth === 0) {
|
|
135
|
+
parts.push(current);
|
|
136
|
+
current = "";
|
|
137
|
+
} else current += ch;
|
|
138
|
+
if (current.trim()) parts.push(current);
|
|
139
|
+
return parts;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* ディレクトリの相対パスをルートプレフィックスに変換する。
|
|
143
|
+
* "" → ""
|
|
144
|
+
* "tasks" → "/tasks"
|
|
145
|
+
* "users/[id]" → "/users/:id"
|
|
146
|
+
*/
|
|
147
|
+
function dirToRoutePrefix(relativePath) {
|
|
148
|
+
if (relativePath === "") return "";
|
|
149
|
+
const segments = relativePath.split(path.sep).filter((seg) => !/^\(.+\)$/.test(seg)).map((seg) => {
|
|
150
|
+
const match = seg.match(/^\[(.+)]$/);
|
|
151
|
+
return match ? `:${match[1]}` : seg;
|
|
152
|
+
});
|
|
153
|
+
return segments.length === 0 ? "" : `/${segments.join("/")}`;
|
|
154
|
+
}
|
|
155
|
+
//#endregion
|
|
156
|
+
//#region src/plugin.ts
|
|
157
|
+
/**
|
|
158
|
+
* Orbit RPC Vite プラグイン。
|
|
159
|
+
*
|
|
160
|
+
* 2つの仕事をする:
|
|
161
|
+
* 1. クライアント側: server.ts の import を HTTP fetch スタブに差し替え
|
|
162
|
+
* 2. dev サーバー: /rpc/* リクエストを受けて server.ts の関数を実行
|
|
163
|
+
*
|
|
164
|
+
* schema.ts に Zod スキーマがあれば、自動でバリデーションを適用する。
|
|
165
|
+
*/
|
|
166
|
+
const VIRTUAL_SERVER_ID = "virtual:orbit-rpc/server";
|
|
167
|
+
const RESOLVED_VIRTUAL_SERVER_ID = `\0${VIRTUAL_SERVER_ID}`;
|
|
168
|
+
function orbitRpc(config = {}) {
|
|
169
|
+
const routesDir = config.routesDir ?? "src/routes";
|
|
170
|
+
const rpcBase = config.rpcBase ?? "/rpc";
|
|
171
|
+
let root;
|
|
172
|
+
let serverModules = [];
|
|
173
|
+
let scanned = false;
|
|
174
|
+
return [{
|
|
175
|
+
name: "orbit-rpc:transform",
|
|
176
|
+
configResolved(resolvedConfig) {
|
|
177
|
+
root = resolvedConfig.root;
|
|
178
|
+
},
|
|
179
|
+
async buildStart() {
|
|
180
|
+
serverModules = await scanServerModules(root, routesDir);
|
|
181
|
+
scanned = true;
|
|
182
|
+
},
|
|
183
|
+
resolveId(id) {
|
|
184
|
+
if (id === VIRTUAL_SERVER_ID) return RESOLVED_VIRTUAL_SERVER_ID;
|
|
185
|
+
},
|
|
186
|
+
async load(id) {
|
|
187
|
+
if (id === RESOLVED_VIRTUAL_SERVER_ID) {
|
|
188
|
+
if (!scanned) serverModules = await scanServerModules(root, routesDir);
|
|
189
|
+
return generateHonoApp(serverModules, root, rpcBase);
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
async transform(code, id) {
|
|
193
|
+
if (!id.endsWith("/server.ts")) return null;
|
|
194
|
+
const routesPath = path.resolve(root, routesDir);
|
|
195
|
+
if (!id.startsWith(routesPath)) return null;
|
|
196
|
+
if (this.environment?.name === "ssr") return null;
|
|
197
|
+
const mod = serverModules.find((m) => m.filePath === id);
|
|
198
|
+
if (!mod) {
|
|
199
|
+
serverModules = await scanServerModules(root, routesDir);
|
|
200
|
+
const freshMod = serverModules.find((m) => m.filePath === id);
|
|
201
|
+
if (!freshMod) return null;
|
|
202
|
+
return generateClientStub(freshMod, rpcBase);
|
|
203
|
+
}
|
|
204
|
+
return generateClientStub(mod, rpcBase);
|
|
205
|
+
}
|
|
206
|
+
}, {
|
|
207
|
+
name: "orbit-rpc:dev-server",
|
|
208
|
+
configureServer(server) {
|
|
209
|
+
const routesPath = path.resolve(root, routesDir);
|
|
210
|
+
const onFileChange = async (file) => {
|
|
211
|
+
if (!file.startsWith(routesPath)) return;
|
|
212
|
+
if (!file.endsWith("/server.ts") && !file.endsWith("/schema.ts")) return;
|
|
213
|
+
serverModules = await scanServerModules(root, routesDir);
|
|
214
|
+
};
|
|
215
|
+
server.watcher.on("add", onFileChange);
|
|
216
|
+
server.watcher.on("change", onFileChange);
|
|
217
|
+
server.watcher.on("unlink", onFileChange);
|
|
218
|
+
server.middlewares.use(async (req, res, next) => {
|
|
219
|
+
if (!req.url?.startsWith(rpcBase)) return next();
|
|
220
|
+
try {
|
|
221
|
+
const result = await handleRpcRequest(server, req, rpcBase, serverModules);
|
|
222
|
+
res.setHeader("Content-Type", "application/json");
|
|
223
|
+
res.end(JSON.stringify(result ?? null));
|
|
224
|
+
} catch (err) {
|
|
225
|
+
const message = err instanceof Error ? err.message : "Internal Server Error";
|
|
226
|
+
res.statusCode = err instanceof RpcError ? err.status : 500;
|
|
227
|
+
res.setHeader("Content-Type", "application/json");
|
|
228
|
+
res.end(JSON.stringify({ error: message }));
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}];
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* server.ts の中身を RPC スタブに差し替える。
|
|
236
|
+
*
|
|
237
|
+
* 例: routePrefix="/tasks", rpcBase="/rpc"
|
|
238
|
+
* export async function getTasks(signal) { ... }
|
|
239
|
+
* → export async function getTasks(signal) {
|
|
240
|
+
* const res = await fetch("/rpc/tasks/getTasks", { method: "POST", signal });
|
|
241
|
+
* ...
|
|
242
|
+
* }
|
|
243
|
+
*/
|
|
244
|
+
function generateClientStub(mod, rpcBase) {
|
|
245
|
+
const lines = [];
|
|
246
|
+
for (const fn of mod.functions) {
|
|
247
|
+
const endpoint = `${rpcBase}${mod.routePrefix}/${fn.name}`;
|
|
248
|
+
lines.push(`export async function ${fn.name}(...args) {`);
|
|
249
|
+
lines.push(` const signal = args[args.length - 1] instanceof AbortSignal ? args.pop() : undefined;`);
|
|
250
|
+
lines.push(` const hasArgs = args.length > 0;`);
|
|
251
|
+
lines.push(` const res = await fetch("${endpoint}", {`);
|
|
252
|
+
lines.push(` method: "POST",`);
|
|
253
|
+
lines.push(` signal,`);
|
|
254
|
+
lines.push(` ...(hasArgs ? {`);
|
|
255
|
+
lines.push(` headers: { "Content-Type": "application/json" },`);
|
|
256
|
+
lines.push(` body: JSON.stringify(args),`);
|
|
257
|
+
lines.push(` } : {}),`);
|
|
258
|
+
lines.push(` });`);
|
|
259
|
+
lines.push(` if (!res.ok) {`);
|
|
260
|
+
lines.push(` const body = await res.json().catch(() => ({}));`);
|
|
261
|
+
lines.push(` throw new Error(body.error || \`RPC error: \${res.status}\`);`);
|
|
262
|
+
lines.push(` }`);
|
|
263
|
+
lines.push(` const text = await res.text();`);
|
|
264
|
+
lines.push(` return text ? JSON.parse(text) : undefined;`);
|
|
265
|
+
lines.push(`}`);
|
|
266
|
+
lines.push(``);
|
|
267
|
+
}
|
|
268
|
+
return lines.join("\n");
|
|
269
|
+
}
|
|
270
|
+
var RpcError = class extends Error {
|
|
271
|
+
constructor(message, status) {
|
|
272
|
+
super(message);
|
|
273
|
+
this.status = status;
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
/**
|
|
277
|
+
* dev サーバーで RPC リクエストを処理する。
|
|
278
|
+
*
|
|
279
|
+
* URL: POST /rpc/{routePrefix}/{functionName}
|
|
280
|
+
* Body: JSON(関数の引数)
|
|
281
|
+
*
|
|
282
|
+
* schema.ts に Zod スキーマがあれば、引数をバリデーションしてから関数を実行する。
|
|
283
|
+
*/
|
|
284
|
+
async function handleRpcRequest(server, req, rpcBase, modules) {
|
|
285
|
+
if (req.method !== "POST") throw new RpcError("Method not allowed", 405);
|
|
286
|
+
const rpcPath = req.url.slice(rpcBase.length);
|
|
287
|
+
const lastSlash = rpcPath.lastIndexOf("/");
|
|
288
|
+
if (lastSlash === -1) throw new RpcError("Invalid RPC path", 400);
|
|
289
|
+
const routePrefix = rpcPath.slice(0, lastSlash) || "";
|
|
290
|
+
const functionName = decodeURIComponent(rpcPath.slice(lastSlash + 1));
|
|
291
|
+
const mod = modules.find((m) => m.routePrefix === routePrefix);
|
|
292
|
+
if (!mod) throw new RpcError(`Module not found: ${routePrefix}`, 404);
|
|
293
|
+
const fnDef = mod.functions.find((f) => f.name === functionName);
|
|
294
|
+
if (!fnDef) throw new RpcError(`Function not found: ${functionName}`, 404);
|
|
295
|
+
const fn = (await server.ssrLoadModule(mod.filePath))[functionName];
|
|
296
|
+
if (typeof fn !== "function") throw new RpcError(`${functionName} is not a function`, 500);
|
|
297
|
+
const body = await readBody(req);
|
|
298
|
+
let args;
|
|
299
|
+
try {
|
|
300
|
+
args = body ? JSON.parse(body) : [];
|
|
301
|
+
} catch {
|
|
302
|
+
throw new RpcError("Invalid JSON in request body", 400);
|
|
303
|
+
}
|
|
304
|
+
if (!Array.isArray(args)) throw new RpcError("Request body must be a JSON array", 400);
|
|
305
|
+
return fn(...await validateArgs(args, fnDef, mod, server));
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* 各引数を対応する Zod スキーマでバリデーションする。
|
|
309
|
+
* スキーマが紐付いていない引数はそのまま通す。
|
|
310
|
+
* 引数が不足していてもスキーマがあればバリデーションを実行する(Zod が required エラーを出す)。
|
|
311
|
+
*/
|
|
312
|
+
async function validateArgs(args, fnDef, mod, server) {
|
|
313
|
+
if (!fnDef.params.some((p) => p.schemaName) || !mod.schemaFilePath) return args;
|
|
314
|
+
const schemaModule = await server.ssrLoadModule(mod.schemaFilePath);
|
|
315
|
+
const validated = [...args];
|
|
316
|
+
for (let i = 0; i < fnDef.params.length; i++) {
|
|
317
|
+
const param = fnDef.params[i];
|
|
318
|
+
if (!param.schemaName) continue;
|
|
319
|
+
const schema = schemaModule[param.schemaName];
|
|
320
|
+
if (schema && typeof schema.safeParse === "function") {
|
|
321
|
+
const result = schema.safeParse(args[i]);
|
|
322
|
+
if (!result.success) {
|
|
323
|
+
const issues = result.error.issues.map((issue) => issue.path.length > 0 ? `${issue.path.join(".")}: ${issue.message}` : issue.message).join(", ");
|
|
324
|
+
throw new RpcError(`Validation error on "${param.name}": ${issues}`, 400);
|
|
325
|
+
}
|
|
326
|
+
validated[i] = result.data;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return validated;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* 本番用 Hono アプリのコードを生成する。
|
|
333
|
+
*
|
|
334
|
+
* import app from "virtual:orbit-rpc/server" で使える。
|
|
335
|
+
* Cloudflare Workers の場合は export default app; するだけ。
|
|
336
|
+
*
|
|
337
|
+
* schema.ts に Zod スキーマがあれば、.parse() によるバリデーションを含む。
|
|
338
|
+
*/
|
|
339
|
+
function generateHonoApp(modules, root, rpcBase) {
|
|
340
|
+
const lines = [];
|
|
341
|
+
lines.push(`import { Hono } from "hono";`);
|
|
342
|
+
lines.push(``);
|
|
343
|
+
const validModules = [];
|
|
344
|
+
for (const [i, mod] of modules.entries()) {
|
|
345
|
+
if (mod.routePrefix.includes(":")) {
|
|
346
|
+
console.warn(`[orbit-rpc] Dynamic route prefix "${mod.routePrefix}" is not supported for RPC. Skipping.`);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
validModules.push([i, mod]);
|
|
350
|
+
const importPath = mod.filePath.split(path.sep).join("/");
|
|
351
|
+
lines.push(`import * as mod${i} from "${importPath}";`);
|
|
352
|
+
if (mod.schemaFilePath) {
|
|
353
|
+
const schemaPath = mod.schemaFilePath.split(path.sep).join("/");
|
|
354
|
+
lines.push(`import * as schema${i} from "${schemaPath}";`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
lines.push(``);
|
|
358
|
+
lines.push(`const app = new Hono();`);
|
|
359
|
+
lines.push(``);
|
|
360
|
+
for (const [i, mod] of validModules) for (const fn of mod.functions) {
|
|
361
|
+
const endpoint = `${rpcBase}${mod.routePrefix}/${fn.name}`;
|
|
362
|
+
lines.push(`app.post("${endpoint}", async (c) => {`);
|
|
363
|
+
lines.push(` try {`);
|
|
364
|
+
lines.push(` const body = await c.req.text();`);
|
|
365
|
+
lines.push(` if (body.length > 1048576) return c.json({ error: "Payload too large" }, 413);`);
|
|
366
|
+
lines.push(` let args;`);
|
|
367
|
+
lines.push(` try { args = body ? JSON.parse(body) : []; } catch { return c.json({ error: "Invalid JSON in request body" }, 400); }`);
|
|
368
|
+
lines.push(` if (!Array.isArray(args)) return c.json({ error: "Request body must be a JSON array" }, 400);`);
|
|
369
|
+
if (mod.schemaFilePath) for (let pi = 0; pi < fn.params.length; pi++) {
|
|
370
|
+
const param = fn.params[pi];
|
|
371
|
+
if (param.schemaName) {
|
|
372
|
+
lines.push(` {`);
|
|
373
|
+
lines.push(` const r = schema${i}.${param.schemaName}.safeParse(args[${pi}]);`);
|
|
374
|
+
lines.push(` if (!r.success) { const msg = r.error.issues.map(i => i.path.length ? i.path.join(".") + ": " + i.message : i.message).join(", "); return c.json({ error: "Validation error on \\"${param.name}\\": " + msg }, 400); }`);
|
|
375
|
+
lines.push(` args[${pi}] = r.data;`);
|
|
376
|
+
lines.push(` }`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
lines.push(` const result = await mod${i}.${fn.name}(...args);`);
|
|
380
|
+
lines.push(` return c.json(result ?? null);`);
|
|
381
|
+
lines.push(` } catch (err) {`);
|
|
382
|
+
lines.push(` console.error(err);`);
|
|
383
|
+
lines.push(` return c.json({ error: "Internal Server Error" }, 500);`);
|
|
384
|
+
lines.push(` }`);
|
|
385
|
+
lines.push(`});`);
|
|
386
|
+
lines.push(``);
|
|
387
|
+
}
|
|
388
|
+
lines.push(`export default app;`);
|
|
389
|
+
return lines.join("\n");
|
|
390
|
+
}
|
|
391
|
+
const MAX_BODY_SIZE = 1024 * 1024;
|
|
392
|
+
function readBody(req) {
|
|
393
|
+
return new Promise((resolve, reject) => {
|
|
394
|
+
let data = "";
|
|
395
|
+
let size = 0;
|
|
396
|
+
req.on("data", (chunk) => {
|
|
397
|
+
size += chunk.length;
|
|
398
|
+
if (size > MAX_BODY_SIZE) {
|
|
399
|
+
req.destroy?.();
|
|
400
|
+
reject(new RpcError("Request body too large", 413));
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
data += chunk.toString();
|
|
404
|
+
});
|
|
405
|
+
req.on("end", () => resolve(data));
|
|
406
|
+
req.on("error", reject);
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
//#endregion
|
|
410
|
+
export { orbitRpc };
|
package/package.json
CHANGED
|
@@ -1,11 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orbit-rpc",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "RPC layer for Orbit — auto-converts server.ts functions to Hono routes",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"fullstack",
|
|
7
|
+
"hono",
|
|
8
|
+
"react",
|
|
9
|
+
"rpc",
|
|
10
|
+
"vite"
|
|
11
|
+
],
|
|
5
12
|
"license": "MIT",
|
|
6
|
-
"
|
|
7
|
-
"
|
|
13
|
+
"author": "ashunar0",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/ashunar0/orbit-router",
|
|
17
|
+
"directory": "packages/orbit-rpc"
|
|
18
|
+
},
|
|
8
19
|
"files": [
|
|
9
|
-
"
|
|
10
|
-
]
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"types": "./dist/index.d.mts",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.mts",
|
|
27
|
+
"import": "./dist/index.mjs"
|
|
28
|
+
},
|
|
29
|
+
"./package.json": "./package.json"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"hono": "^4.7.11"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^25.5.0",
|
|
36
|
+
"bumpp": "^11.0.1",
|
|
37
|
+
"typescript": "^5.9.3",
|
|
38
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@0.1.12",
|
|
39
|
+
"vite-plus": "0.1.12"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"vite": ">=6.0.0"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "vp pack",
|
|
46
|
+
"dev": "vp pack --watch",
|
|
47
|
+
"test": "vp test",
|
|
48
|
+
"typecheck": "tsc --noEmit",
|
|
49
|
+
"release": "bumpp"
|
|
50
|
+
}
|
|
11
51
|
}
|
package/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|