@thednp/rpc 0.0.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.
- package/AGENTS.md +142 -0
- package/LICENSE +21 -0
- package/README.md +178 -0
- package/dist/express/express.d.mts +39 -0
- package/dist/express/express.d.mts.map +1 -0
- package/dist/express/express.mjs +179 -0
- package/dist/express/express.mjs.map +1 -0
- package/dist/fastify/fastify.d.mts +45 -0
- package/dist/fastify/fastify.d.mts.map +1 -0
- package/dist/fastify/fastify.mjs +164 -0
- package/dist/fastify/fastify.mjs.map +1 -0
- package/dist/fastify/plugin/fastify/plugin.d.mts +114 -0
- package/dist/fastify/plugin/fastify/plugin.d.mts.map +1 -0
- package/dist/fastify/plugin/fastify/plugin.mjs +151 -0
- package/dist/fastify/plugin/fastify/plugin.mjs.map +1 -0
- package/dist/helpers/helpers.d.mts +9 -0
- package/dist/helpers/helpers.d.mts.map +1 -0
- package/dist/helpers/helpers.mjs +36 -0
- package/dist/helpers/helpers.mjs.map +1 -0
- package/dist/hono/hono.d.mts +49 -0
- package/dist/hono/hono.d.mts.map +1 -0
- package/dist/hono/hono.mjs +153 -0
- package/dist/hono/hono.mjs.map +1 -0
- package/dist/index.d.mts +192 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +296 -0
- package/dist/index.mjs.map +1 -0
- package/dist/koa/koa.d.mts +26 -0
- package/dist/koa/koa.d.mts.map +1 -0
- package/dist/koa/koa.mjs +155 -0
- package/dist/koa/koa.mjs.map +1 -0
- package/dist/server/server.d.mts +92 -0
- package/dist/server/server.d.mts.map +1 -0
- package/dist/server/server.mjs +161 -0
- package/dist/server/server.mjs.map +1 -0
- package/package.json +78 -0
- package/wiki/adapters.md +143 -0
- package/wiki/best-practices.md +201 -0
- package/wiki/client-usage.md +62 -0
- package/wiki/configuration.md +77 -0
- package/wiki/getting-started.md +76 -0
- package/wiki/index.md +26 -0
- package/wiki/security.md +54 -0
- package/wiki/server-functions.md +93 -0
- package/wiki/setup.md +77 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { scanForServerFiles, serverFunctionsMap } from "@thednp/rpc/server";
|
|
2
|
+
import fp from "fastify-plugin";
|
|
3
|
+
//#region src/options.ts
|
|
4
|
+
const defaultRPCOptions = {
|
|
5
|
+
rpcPreffix: "__rpc",
|
|
6
|
+
adapter: "express"
|
|
7
|
+
};
|
|
8
|
+
const defaultMiddlewareOptions = {
|
|
9
|
+
rpcPreffix: void 0,
|
|
10
|
+
path: void 0
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/tools.ts
|
|
14
|
+
function escapeRegExp(s) {
|
|
15
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/fastify/plugin.ts
|
|
19
|
+
const RpcPlugin = (fastify, initialOptions, done) => {
|
|
20
|
+
const rpcMiddleware = createRPCMiddleware(initialOptions);
|
|
21
|
+
fastify.addHook("preHandler", async (request, reply) => {
|
|
22
|
+
const next = () => new Promise((resolve) => {
|
|
23
|
+
rpcMiddleware(request, reply, resolve);
|
|
24
|
+
});
|
|
25
|
+
await next();
|
|
26
|
+
});
|
|
27
|
+
done();
|
|
28
|
+
};
|
|
29
|
+
const rpcPlugin = fp(RpcPlugin, { name: "uni-rpc-fastify-plugin" });
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/fastify/helpers.ts
|
|
32
|
+
async function attachRPC(app) {
|
|
33
|
+
const { loadRPCConfig } = await import("@thednp/rpc");
|
|
34
|
+
const { adapter: _adapter, ...options } = await loadRPCConfig();
|
|
35
|
+
await app.register(rpcPlugin, options);
|
|
36
|
+
}
|
|
37
|
+
function attachVite(app, vite) {
|
|
38
|
+
app.addHook("onRequest", async (request, reply) => {
|
|
39
|
+
const next = () => new Promise((resolve) => {
|
|
40
|
+
vite.middlewares(request.raw, reply.raw, resolve);
|
|
41
|
+
});
|
|
42
|
+
await next();
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const readBody = (req) => {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const contentType = req.headers["content-type"]?.toLowerCase() || "";
|
|
48
|
+
const reqBody = req.body;
|
|
49
|
+
if (reqBody !== void 0) {
|
|
50
|
+
const isJSON = contentType.includes("json");
|
|
51
|
+
resolve({
|
|
52
|
+
contentType: isJSON ? "application/json" : "text/plain",
|
|
53
|
+
data: isJSON ? reqBody : String(reqBody)
|
|
54
|
+
});
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const toggleListeners = (add) => {
|
|
58
|
+
const method = add ? "on" : "off";
|
|
59
|
+
req.raw[method]("data", onData);
|
|
60
|
+
req.raw[method]("end", onEnd);
|
|
61
|
+
req.raw[method]("error", onError);
|
|
62
|
+
};
|
|
63
|
+
let body = "";
|
|
64
|
+
const onData = (chunk) => {
|
|
65
|
+
body += chunk.toString();
|
|
66
|
+
};
|
|
67
|
+
const onEnd = () => {
|
|
68
|
+
toggleListeners();
|
|
69
|
+
const isJSON = contentType.includes("json");
|
|
70
|
+
try {
|
|
71
|
+
const data = JSON.parse(body);
|
|
72
|
+
resolve({
|
|
73
|
+
contentType: isJSON ? "application/json" : "text/plain",
|
|
74
|
+
data
|
|
75
|
+
});
|
|
76
|
+
} catch (_e) {
|
|
77
|
+
resolve({
|
|
78
|
+
contentType: "text/plain",
|
|
79
|
+
data: String(body)
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const onError = (err) => {
|
|
84
|
+
toggleListeners();
|
|
85
|
+
reject(err);
|
|
86
|
+
};
|
|
87
|
+
toggleListeners(true);
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/fastify/createMiddleware.ts
|
|
92
|
+
let middlewareCount = 0;
|
|
93
|
+
const middlewareStack = /* @__PURE__ */ new Set();
|
|
94
|
+
const createMiddleware = (initialOptions = {}) => {
|
|
95
|
+
const options = Object.assign({}, defaultMiddlewareOptions, initialOptions);
|
|
96
|
+
const middlewareName = options.name;
|
|
97
|
+
const rpcPreffix = options.rpcPreffix;
|
|
98
|
+
const path = options.path;
|
|
99
|
+
const handler = options.handler;
|
|
100
|
+
let name = middlewareName;
|
|
101
|
+
if (!name) {
|
|
102
|
+
name = "viteRPCMiddleware-" + middlewareCount;
|
|
103
|
+
middlewareCount += 1;
|
|
104
|
+
}
|
|
105
|
+
if (middlewareStack.has(name)) throw new Error(`The middleware name "${name}" is already used.`);
|
|
106
|
+
middlewareStack.add(name);
|
|
107
|
+
const prefixRegex = rpcPreffix ? new RegExp(`^/${escapeRegExp(rpcPreffix)}/`) : null;
|
|
108
|
+
const pathMatcher = path ? typeof path === "string" ? new RegExp(path) : path : null;
|
|
109
|
+
const middlewareHandler = async (req, _reply, done) => {
|
|
110
|
+
const url = new URL(req.url, "http://localhost").pathname;
|
|
111
|
+
if (serverFunctionsMap.size === 0) await scanForServerFiles();
|
|
112
|
+
if (!handler) {
|
|
113
|
+
done();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (pathMatcher && !pathMatcher.test(url)) {
|
|
117
|
+
done();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (prefixRegex && !prefixRegex.test(url)) {
|
|
121
|
+
done();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
await handler(req, _reply, done);
|
|
125
|
+
};
|
|
126
|
+
Object.defineProperty(middlewareHandler, "name", { value: name });
|
|
127
|
+
return middlewareHandler;
|
|
128
|
+
};
|
|
129
|
+
const createRPCMiddleware = (initialOptions = {}) => {
|
|
130
|
+
const options = Object.assign({}, defaultMiddlewareOptions, { rpcPreffix: defaultRPCOptions.rpcPreffix }, initialOptions);
|
|
131
|
+
const rpcPreffix = options.rpcPreffix;
|
|
132
|
+
const prefixRegex = rpcPreffix ? new RegExp(`^/${escapeRegExp(rpcPreffix)}/`) : null;
|
|
133
|
+
const prefixReplace = `/${rpcPreffix}/`;
|
|
134
|
+
return createMiddleware({
|
|
135
|
+
...options,
|
|
136
|
+
handler: async (req, reply, _done) => {
|
|
137
|
+
const url = new URL(req.url, "http://localhost").pathname;
|
|
138
|
+
if (prefixRegex && !prefixRegex.test(url)) return;
|
|
139
|
+
const functionName = url.replace(prefixReplace, "");
|
|
140
|
+
const serverFunction = serverFunctionsMap.get(functionName);
|
|
141
|
+
if (!serverFunction) {
|
|
142
|
+
reply.status(404).send({ error: "Function not found" });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
const body = await readBody(req);
|
|
147
|
+
const args = Array.isArray(body.data) ? body.data : [body.data];
|
|
148
|
+
const { data: dataResult, cancel } = serverFunction.handler(...args);
|
|
149
|
+
const onClose = () => cancel("client disconnected");
|
|
150
|
+
req.raw.on("close", onClose);
|
|
151
|
+
const data = await dataResult;
|
|
152
|
+
req.raw.off("close", onClose);
|
|
153
|
+
if (!reply.raw.headersSent) reply.status(200).send({ data });
|
|
154
|
+
} catch (err) {
|
|
155
|
+
console.error(String(err));
|
|
156
|
+
reply.status(500).send({ error: "Internal Server Error" });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
};
|
|
161
|
+
//#endregion
|
|
162
|
+
export { attachRPC, attachVite, createMiddleware, createRPCMiddleware, readBody };
|
|
163
|
+
|
|
164
|
+
//# sourceMappingURL=fastify.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fastify.mjs","names":["fastifyRpcPlugin"],"sources":["../../src/options.ts","../../src/tools.ts","../../src/fastify/plugin.ts","../../src/fastify/helpers.ts","../../src/fastify/createMiddleware.ts"],"sourcesContent":["import type {\n MiddlewareOptions,\n RpcPluginOptions,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\n\nexport const defaultServerFnOptions = {\n contentType: \"application/json\",\n} satisfies ServerFunctionOptions;\n\nexport const defaultRPCOptions: RpcPluginOptions = {\n rpcPreffix: \"__rpc\",\n adapter: \"express\",\n};\n\nexport const defaultMiddlewareOptions = {\n rpcPreffix: undefined,\n path: undefined,\n} satisfies MiddlewareOptions;\n","export function escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n","// src/fastify/plugin.ts\n// inspired by https://github.com/royalswe/vike-fastify-boilerplate/blob/main/server/index.ts\nimport type { FastifyInstance } from \"fastify\";\nimport fp from \"fastify-plugin\";\nimport type { MiddlewareOptions } from \"../types.d.ts\";\nimport { createRPCMiddleware } from \"./createMiddleware.ts\";\n\nexport type { MiddlewareOptions };\n\n// Define the plugin factory\nconst RpcPlugin = (\n fastify: FastifyInstance,\n initialOptions: Partial<MiddlewareOptions<\"fastify\">>,\n done: () => void,\n) => {\n // Register RPC middleware as preHandler hook\n const rpcMiddleware = createRPCMiddleware(initialOptions);\n fastify.addHook(\"preHandler\", async (request, reply) => {\n const next = () =>\n new Promise((resolve) => {\n rpcMiddleware(request, reply, resolve);\n });\n await next();\n });\n\n done();\n};\n\n// Export the plugin wrapped with fastify-plugin\nconst rpcPlugin = fp(RpcPlugin, {\n name: \"uni-rpc-fastify-plugin\",\n});\n\nexport { rpcPlugin as default };\n","// @thednp/rpc/src/fastify/helpers.ts\nimport type { FastifyRequest } from \"fastify\";\nimport type { ViteDevServer } from \"vite\";\nimport type { FastifyInstance } from \"fastify\";\nimport { Buffer } from \"node:buffer\";\nimport fastifyRpcPlugin from \"./plugin.ts\";\nimport type { BodyResult, JsonValue } from \"../types.d.ts\";\n\nexport async function attachRPC(app: FastifyInstance) {\n const { loadRPCConfig } = await import(\"@thednp/rpc\");\n const { adapter: _adapter, ...options } = await loadRPCConfig();\n await app.register(fastifyRpcPlugin, options);\n}\n\nexport function attachVite(app: FastifyInstance, vite: ViteDevServer) {\n app.addHook(\"onRequest\", async (request, reply) => {\n const next = () =>\n new Promise((resolve) => {\n vite.middlewares(request.raw, reply.raw, resolve);\n });\n await next();\n });\n}\n\nexport const readBody = (\n req: FastifyRequest,\n): Promise<BodyResult> => {\n return new Promise((resolve, reject) => {\n const contentType = req.headers[\"content-type\"]?.toLowerCase() || \"\";\n const reqBody = req.body as JsonValue;\n\n if (reqBody !== undefined) {\n const isJSON = contentType.includes(\"json\");\n resolve({\n contentType: isJSON ? \"application/json\" : \"text/plain\",\n data: isJSON ? reqBody : String(reqBody),\n } as BodyResult);\n return;\n }\n\n const toggleListeners = (add?: boolean) => {\n const method = add ? \"on\" : \"off\";\n req.raw[method](\"data\", onData);\n req.raw[method](\"end\", onEnd);\n req.raw[method](\"error\", onError);\n };\n\n let body = \"\";\n\n const onData = (chunk: Buffer) => {\n body += chunk.toString();\n };\n\n const onEnd = () => {\n toggleListeners();\n const isJSON = contentType.includes(\"json\");\n try {\n const data = JSON.parse(body);\n resolve({\n contentType: isJSON ? \"application/json\" : \"text/plain\",\n data,\n });\n } catch (_e) {\n resolve({ contentType: \"text/plain\", data: String(body) });\n }\n };\n const onError = (err: Error) => {\n toggleListeners();\n reject(err);\n };\n\n toggleListeners(true);\n });\n};\n","// src/fastify/createMiddleware.ts\nimport type {\n FastifyReply,\n FastifyRequest,\n HookHandlerDoneFunction,\n} from \"fastify\";\nimport type {\n FastifyMiddlewareFn,\n FastifyMiddlewareOptions,\n} from \"./types.d.ts\";\n// import type { JsonArray, JsonValue } from \"@thednp/rpc\";\nimport { scanForServerFiles, serverFunctionsMap } from \"@thednp/rpc/server\";\nimport { defaultMiddlewareOptions, defaultRPCOptions } from \"../options.ts\";\nimport { escapeRegExp } from \"../tools.ts\";\nimport { readBody } from \"./helpers.ts\";\n\nlet middlewareCount = 0;\nconst middlewareStack = new Set<string>();\n\n// Define the middleware function for Fastify\nexport const createMiddleware: FastifyMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n initialOptions,\n ) as FastifyMiddlewareOptions;\n\n const middlewareName = options.name;\n const rpcPreffix = options.rpcPreffix;\n const path = options.path;\n const handler = options.handler;\n\n let name = middlewareName;\n if (!name) {\n name = \"viteRPCMiddleware-\" + middlewareCount;\n middlewareCount += 1;\n }\n if (middlewareStack.has(name)) {\n throw new Error(`The middleware name \"${name}\" is already used.`);\n }\n middlewareStack.add(name);\n\n // Hoist regex compilation out of per-request path. Escape the prefix to\n // prevent regex injection via metacharacters in the config string.\n const prefixRegex: RegExp | null = rpcPreffix\n ? new RegExp(`^/${escapeRegExp(rpcPreffix)}/`)\n : null;\n const pathMatcher: RegExp | null = path\n ? (typeof path === \"string\" ? new RegExp(path) : path)\n : null;\n\n const middlewareHandler = async (\n req: FastifyRequest,\n _reply: FastifyReply,\n done: HookHandlerDoneFunction,\n ) => {\n const reqUrl = new URL(req.url, \"http://localhost\");\n const url = reqUrl.pathname;\n\n // When serving from production server, scan for server files\n if (serverFunctionsMap.size === 0) {\n await scanForServerFiles();\n }\n\n // No need to continue when no handler provided\n if (!handler) {\n done();\n return;\n }\n\n // Path matching\n if (pathMatcher && !pathMatcher.test(url)) {\n done();\n return;\n }\n\n // rpcPreffix matching (boundary-safe via escaped regex)\n if (prefixRegex && !prefixRegex.test(url)) {\n done();\n return;\n }\n\n // Execute handler\n await handler(req, _reply, done);\n };\n\n Object.defineProperty(middlewareHandler, \"name\", {\n value: name,\n });\n\n return middlewareHandler;\n};\n\nexport const createRPCMiddleware: FastifyMiddlewareFn = (\n initialOptions = {},\n) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n { rpcPreffix: defaultRPCOptions.rpcPreffix },\n initialOptions,\n ) as FastifyMiddlewareOptions;\n\n // Hoist prefix regex (escaped) and the literal prefix-for-replace out of the\n // per-request handler to avoid regex injection and per-request compilation.\n const rpcPreffix = options.rpcPreffix;\n const prefixRegex = rpcPreffix\n ? new RegExp(`^/${escapeRegExp(rpcPreffix)}/`)\n : /* istanbul ignore next */ null;\n const prefixReplace = `/${rpcPreffix}/`;\n\n return createMiddleware({\n ...options,\n handler: async (\n req: FastifyRequest,\n reply: FastifyReply,\n _done: HookHandlerDoneFunction,\n ) => {\n const reqUrl = new URL(req.url, \"http://localhost\");\n const url = reqUrl.pathname;\n\n // Defense-in-depth: validate prefix match via escaped regex even though\n // the outer createMiddleware gates on the same prefix already.\n // istanbul ignore if\n if (prefixRegex && !prefixRegex.test(url)) {\n return;\n }\n\n const functionName = url.replace(prefixReplace, \"\");\n const serverFunction = serverFunctionsMap.get(functionName);\n\n if (!serverFunction) {\n reply.status(404).send({\n error: \"Function not found\",\n });\n return;\n }\n\n try {\n const body = await readBody(req);\n const args = Array.isArray(body.data) ? body.data : [body.data];\n const { data: dataResult, cancel } = serverFunction.handler(...args);\n const onClose = () => cancel(\"client disconnected\");\n\n req.raw.on(\"close\", onClose);\n const data = await dataResult;\n req.raw.off(\"close\", onClose);\n\n // istanbul ignore else\n if (!reply.raw.headersSent) reply.status(200).send({ data });\n } catch (err) {\n console.error(String(err));\n reply.status(500).send({ error: \"Internal Server Error\" });\n }\n },\n });\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { FastifyInstance, FastifyReply, FastifyRequest, HookHandlerDoneFunction } from "fastify";
|
|
2
|
+
import { Connect } from "vite";
|
|
3
|
+
import "@thednp/rpc";
|
|
4
|
+
import { IncomingMessage, ServerResponse } from "node:http";
|
|
5
|
+
import { NextFunction, Request, Response as Response$1 } from "express";
|
|
6
|
+
import { MiddlewareHandler } from "hono";
|
|
7
|
+
import "@hono/node-server";
|
|
8
|
+
import { Context, Next } from "koa";
|
|
9
|
+
//#region src/express/types.d.ts
|
|
10
|
+
interface ExpressMiddlewareHooks {
|
|
11
|
+
handler: (req: IncomingMessage | Request, res: ServerResponse | Response$1, next: Connect.NextFunction | NextFunction) => Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/hono/types.d.ts
|
|
15
|
+
interface HonoMiddlewareHooks {
|
|
16
|
+
handler: MiddlewareHandler;
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/fastify/types.d.ts
|
|
20
|
+
interface FastifyMiddlewareHooks {
|
|
21
|
+
handler: (req: FastifyRequest, res: FastifyReply, done: HookHandlerDoneFunction) => Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/koa/types.d.ts
|
|
25
|
+
interface KoaMiddlewareHooks {
|
|
26
|
+
handler: (ctx: Context, next: Next) => Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/types.d.ts
|
|
30
|
+
interface FrameworkHooks {
|
|
31
|
+
express: ExpressMiddlewareHooks;
|
|
32
|
+
hono: HonoMiddlewareHooks;
|
|
33
|
+
fastify: FastifyMiddlewareHooks;
|
|
34
|
+
koa: KoaMiddlewareHooks;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* ### @thednp/rpc
|
|
38
|
+
* The plugin configuration allows for granular control of your
|
|
39
|
+
* application RPC calls. The default settings are optimized for development
|
|
40
|
+
* environments while providing a secure foundation for production use.
|
|
41
|
+
*/
|
|
42
|
+
interface RpcPluginOptions {
|
|
43
|
+
// RPC Middleware Options
|
|
44
|
+
/**
|
|
45
|
+
* RPC prefix without leading slash (e.g. "__rpc")
|
|
46
|
+
* Leading slash will be added automatically by the middleware.
|
|
47
|
+
* This prefix defines the base path for all RPC endpoints.
|
|
48
|
+
* @default "__rpc"
|
|
49
|
+
* @example
|
|
50
|
+
* // Results in endpoints like: /api/rpc/myFunction
|
|
51
|
+
* rpcPreffix: "api/rpc"
|
|
52
|
+
*/
|
|
53
|
+
rpcPreffix: "__rpc" | string;
|
|
54
|
+
/**
|
|
55
|
+
* Option to set an adapter for the middleware connection. The default is _express_,
|
|
56
|
+
* which is the most popular and battle tested server app. The _express_ adapter is
|
|
57
|
+
* also compatible with the vite's Connect development server.
|
|
58
|
+
* @default express
|
|
59
|
+
*/
|
|
60
|
+
adapter: "express" | "hono" | "fastify" | "koa";
|
|
61
|
+
}
|
|
62
|
+
interface MiddlewareOptions<A extends RpcPluginOptions["adapter"] = "express"> {
|
|
63
|
+
/**
|
|
64
|
+
* Name for the middleware (used for identification in Express stack)
|
|
65
|
+
*/
|
|
66
|
+
name?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Path pattern to match for middleware execution.
|
|
69
|
+
* Accepts string or RegExp to filter requests based on URL path.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* // String path
|
|
73
|
+
* path: "/api/v1"
|
|
74
|
+
*
|
|
75
|
+
* // RegExp pattern
|
|
76
|
+
* path: /^\/api\/v[0-9]+/
|
|
77
|
+
*/
|
|
78
|
+
path?: string | RegExp;
|
|
79
|
+
/**
|
|
80
|
+
* RPC prefix without leading slash (e.g. "__rpc")
|
|
81
|
+
* Leading slash will be added automatically by the middleware.
|
|
82
|
+
* This prefix defines the base path for all RPC endpoints.
|
|
83
|
+
* @default string
|
|
84
|
+
* @example
|
|
85
|
+
* // Results in endpoints like: /api/rpc/myFunction
|
|
86
|
+
* rpcPreffix: "api/rpc"
|
|
87
|
+
*/
|
|
88
|
+
rpcPreffix?: string | false;
|
|
89
|
+
/**
|
|
90
|
+
* Async handler for request processing.
|
|
91
|
+
* Core middleware function that processes incoming requests.
|
|
92
|
+
*
|
|
93
|
+
* @param req - The incoming request object
|
|
94
|
+
* @param res - The server response object
|
|
95
|
+
* @param next - Function to pass control to the next middleware
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* handler: async (req, res, next) => {
|
|
99
|
+
* // Process request
|
|
100
|
+
* const data = await processRequest(req);
|
|
101
|
+
*
|
|
102
|
+
* // Send response
|
|
103
|
+
* sendResponse(res, { data }, 200);
|
|
104
|
+
* }
|
|
105
|
+
*/
|
|
106
|
+
handler?: FrameworkHooks[A]["handler"];
|
|
107
|
+
}
|
|
108
|
+
//#endregion
|
|
109
|
+
//#region src/fastify/plugin.d.ts
|
|
110
|
+
declare const RpcPlugin: (fastify: FastifyInstance, initialOptions: Partial<MiddlewareOptions<"fastify">>, done: () => void) => void;
|
|
111
|
+
declare const rpcPlugin: typeof RpcPlugin;
|
|
112
|
+
//#endregion
|
|
113
|
+
export { type MiddlewareOptions, rpcPlugin as default };
|
|
114
|
+
//# sourceMappingURL=plugin.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.d.mts","names":[],"sources":["../../../../src/express/types.d.ts","../../../../src/hono/types.d.ts","../../../../src/fastify/types.d.ts","../../../../src/koa/types.d.ts","../../../../src/types.d.ts","../../../../src/fastify/plugin.ts"],"mappings":";;;;;;;;;UAaiB;EACf,UACE,KAAK,kBAAkB,SACvB,KAAK,iBAAiB,YACtB,MAAM,QAAQ,eAAe,iBAC1B;;;;UCbU;EACf,SAAS;;;;UCSM;EACf,UACE,KAAK,gBACL,KAAK,cACL,MAAM,4BACH;;;;UCNU;EACf,UAAU,KAAK,SAAS,MAAM,SAAS;;;;UCAxB;EACf,SAAS;EACT,MAAM;EACN,SAAS;EACT,KAAK;;;;;;;;UA8FU;;;;;;;;;;;EAWf;;;;;;;EAQA;;UAGe,kBACf,UAAU;;;;EAKV;;;;;;;;;;;;EAaA,gBAAgB;;;;;;;;;;EAWhB;;;;;;;;;;;;;;;;;;EAmBA,UAAU,eAAe;;;;cC9KrB,YAAS,SACJ,iBAAe,gBACR,QAAQ,+BAA6B;cAiBjD,kBAAS"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import fp from "fastify-plugin";
|
|
2
|
+
import { scanForServerFiles, serverFunctionsMap } from "@thednp/rpc/server";
|
|
3
|
+
//#region src/options.ts
|
|
4
|
+
const defaultRPCOptions = {
|
|
5
|
+
rpcPreffix: "__rpc",
|
|
6
|
+
adapter: "express"
|
|
7
|
+
};
|
|
8
|
+
const defaultMiddlewareOptions = {
|
|
9
|
+
rpcPreffix: void 0,
|
|
10
|
+
path: void 0
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/tools.ts
|
|
14
|
+
function escapeRegExp(s) {
|
|
15
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/fastify/helpers.ts
|
|
19
|
+
const readBody = (req) => {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const contentType = req.headers["content-type"]?.toLowerCase() || "";
|
|
22
|
+
const reqBody = req.body;
|
|
23
|
+
if (reqBody !== void 0) {
|
|
24
|
+
const isJSON = contentType.includes("json");
|
|
25
|
+
resolve({
|
|
26
|
+
contentType: isJSON ? "application/json" : "text/plain",
|
|
27
|
+
data: isJSON ? reqBody : String(reqBody)
|
|
28
|
+
});
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const toggleListeners = (add) => {
|
|
32
|
+
const method = add ? "on" : "off";
|
|
33
|
+
req.raw[method]("data", onData);
|
|
34
|
+
req.raw[method]("end", onEnd);
|
|
35
|
+
req.raw[method]("error", onError);
|
|
36
|
+
};
|
|
37
|
+
let body = "";
|
|
38
|
+
const onData = (chunk) => {
|
|
39
|
+
body += chunk.toString();
|
|
40
|
+
};
|
|
41
|
+
const onEnd = () => {
|
|
42
|
+
toggleListeners();
|
|
43
|
+
const isJSON = contentType.includes("json");
|
|
44
|
+
try {
|
|
45
|
+
const data = JSON.parse(body);
|
|
46
|
+
resolve({
|
|
47
|
+
contentType: isJSON ? "application/json" : "text/plain",
|
|
48
|
+
data
|
|
49
|
+
});
|
|
50
|
+
} catch (_e) {
|
|
51
|
+
resolve({
|
|
52
|
+
contentType: "text/plain",
|
|
53
|
+
data: String(body)
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const onError = (err) => {
|
|
58
|
+
toggleListeners();
|
|
59
|
+
reject(err);
|
|
60
|
+
};
|
|
61
|
+
toggleListeners(true);
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/fastify/createMiddleware.ts
|
|
66
|
+
let middlewareCount = 0;
|
|
67
|
+
const middlewareStack = /* @__PURE__ */ new Set();
|
|
68
|
+
const createMiddleware = (initialOptions = {}) => {
|
|
69
|
+
const options = Object.assign({}, defaultMiddlewareOptions, initialOptions);
|
|
70
|
+
const middlewareName = options.name;
|
|
71
|
+
const rpcPreffix = options.rpcPreffix;
|
|
72
|
+
const path = options.path;
|
|
73
|
+
const handler = options.handler;
|
|
74
|
+
let name = middlewareName;
|
|
75
|
+
if (!name) {
|
|
76
|
+
name = "viteRPCMiddleware-" + middlewareCount;
|
|
77
|
+
middlewareCount += 1;
|
|
78
|
+
}
|
|
79
|
+
if (middlewareStack.has(name)) throw new Error(`The middleware name "${name}" is already used.`);
|
|
80
|
+
middlewareStack.add(name);
|
|
81
|
+
const prefixRegex = rpcPreffix ? new RegExp(`^/${escapeRegExp(rpcPreffix)}/`) : null;
|
|
82
|
+
const pathMatcher = path ? typeof path === "string" ? new RegExp(path) : path : null;
|
|
83
|
+
const middlewareHandler = async (req, _reply, done) => {
|
|
84
|
+
const url = new URL(req.url, "http://localhost").pathname;
|
|
85
|
+
if (serverFunctionsMap.size === 0) await scanForServerFiles();
|
|
86
|
+
if (!handler) {
|
|
87
|
+
done();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (pathMatcher && !pathMatcher.test(url)) {
|
|
91
|
+
done();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (prefixRegex && !prefixRegex.test(url)) {
|
|
95
|
+
done();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
await handler(req, _reply, done);
|
|
99
|
+
};
|
|
100
|
+
Object.defineProperty(middlewareHandler, "name", { value: name });
|
|
101
|
+
return middlewareHandler;
|
|
102
|
+
};
|
|
103
|
+
const createRPCMiddleware = (initialOptions = {}) => {
|
|
104
|
+
const options = Object.assign({}, defaultMiddlewareOptions, { rpcPreffix: defaultRPCOptions.rpcPreffix }, initialOptions);
|
|
105
|
+
const rpcPreffix = options.rpcPreffix;
|
|
106
|
+
const prefixRegex = rpcPreffix ? new RegExp(`^/${escapeRegExp(rpcPreffix)}/`) : null;
|
|
107
|
+
const prefixReplace = `/${rpcPreffix}/`;
|
|
108
|
+
return createMiddleware({
|
|
109
|
+
...options,
|
|
110
|
+
handler: async (req, reply, _done) => {
|
|
111
|
+
const url = new URL(req.url, "http://localhost").pathname;
|
|
112
|
+
if (prefixRegex && !prefixRegex.test(url)) return;
|
|
113
|
+
const functionName = url.replace(prefixReplace, "");
|
|
114
|
+
const serverFunction = serverFunctionsMap.get(functionName);
|
|
115
|
+
if (!serverFunction) {
|
|
116
|
+
reply.status(404).send({ error: "Function not found" });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const body = await readBody(req);
|
|
121
|
+
const args = Array.isArray(body.data) ? body.data : [body.data];
|
|
122
|
+
const { data: dataResult, cancel } = serverFunction.handler(...args);
|
|
123
|
+
const onClose = () => cancel("client disconnected");
|
|
124
|
+
req.raw.on("close", onClose);
|
|
125
|
+
const data = await dataResult;
|
|
126
|
+
req.raw.off("close", onClose);
|
|
127
|
+
if (!reply.raw.headersSent) reply.status(200).send({ data });
|
|
128
|
+
} catch (err) {
|
|
129
|
+
console.error(String(err));
|
|
130
|
+
reply.status(500).send({ error: "Internal Server Error" });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/fastify/plugin.ts
|
|
137
|
+
const RpcPlugin = (fastify, initialOptions, done) => {
|
|
138
|
+
const rpcMiddleware = createRPCMiddleware(initialOptions);
|
|
139
|
+
fastify.addHook("preHandler", async (request, reply) => {
|
|
140
|
+
const next = () => new Promise((resolve) => {
|
|
141
|
+
rpcMiddleware(request, reply, resolve);
|
|
142
|
+
});
|
|
143
|
+
await next();
|
|
144
|
+
});
|
|
145
|
+
done();
|
|
146
|
+
};
|
|
147
|
+
const rpcPlugin = fp(RpcPlugin, { name: "uni-rpc-fastify-plugin" });
|
|
148
|
+
//#endregion
|
|
149
|
+
export { rpcPlugin as default };
|
|
150
|
+
|
|
151
|
+
//# sourceMappingURL=plugin.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.mjs","names":[],"sources":["../../../../src/options.ts","../../../../src/tools.ts","../../../../src/fastify/helpers.ts","../../../../src/fastify/createMiddleware.ts","../../../../src/fastify/plugin.ts"],"sourcesContent":["import type {\n MiddlewareOptions,\n RpcPluginOptions,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\n\nexport const defaultServerFnOptions = {\n contentType: \"application/json\",\n} satisfies ServerFunctionOptions;\n\nexport const defaultRPCOptions: RpcPluginOptions = {\n rpcPreffix: \"__rpc\",\n adapter: \"express\",\n};\n\nexport const defaultMiddlewareOptions = {\n rpcPreffix: undefined,\n path: undefined,\n} satisfies MiddlewareOptions;\n","export function escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n","// @thednp/rpc/src/fastify/helpers.ts\nimport type { FastifyRequest } from \"fastify\";\nimport type { ViteDevServer } from \"vite\";\nimport type { FastifyInstance } from \"fastify\";\nimport { Buffer } from \"node:buffer\";\nimport fastifyRpcPlugin from \"./plugin.ts\";\nimport type { BodyResult, JsonValue } from \"../types.d.ts\";\n\nexport async function attachRPC(app: FastifyInstance) {\n const { loadRPCConfig } = await import(\"@thednp/rpc\");\n const { adapter: _adapter, ...options } = await loadRPCConfig();\n await app.register(fastifyRpcPlugin, options);\n}\n\nexport function attachVite(app: FastifyInstance, vite: ViteDevServer) {\n app.addHook(\"onRequest\", async (request, reply) => {\n const next = () =>\n new Promise((resolve) => {\n vite.middlewares(request.raw, reply.raw, resolve);\n });\n await next();\n });\n}\n\nexport const readBody = (\n req: FastifyRequest,\n): Promise<BodyResult> => {\n return new Promise((resolve, reject) => {\n const contentType = req.headers[\"content-type\"]?.toLowerCase() || \"\";\n const reqBody = req.body as JsonValue;\n\n if (reqBody !== undefined) {\n const isJSON = contentType.includes(\"json\");\n resolve({\n contentType: isJSON ? \"application/json\" : \"text/plain\",\n data: isJSON ? reqBody : String(reqBody),\n } as BodyResult);\n return;\n }\n\n const toggleListeners = (add?: boolean) => {\n const method = add ? \"on\" : \"off\";\n req.raw[method](\"data\", onData);\n req.raw[method](\"end\", onEnd);\n req.raw[method](\"error\", onError);\n };\n\n let body = \"\";\n\n const onData = (chunk: Buffer) => {\n body += chunk.toString();\n };\n\n const onEnd = () => {\n toggleListeners();\n const isJSON = contentType.includes(\"json\");\n try {\n const data = JSON.parse(body);\n resolve({\n contentType: isJSON ? \"application/json\" : \"text/plain\",\n data,\n });\n } catch (_e) {\n resolve({ contentType: \"text/plain\", data: String(body) });\n }\n };\n const onError = (err: Error) => {\n toggleListeners();\n reject(err);\n };\n\n toggleListeners(true);\n });\n};\n","// src/fastify/createMiddleware.ts\nimport type {\n FastifyReply,\n FastifyRequest,\n HookHandlerDoneFunction,\n} from \"fastify\";\nimport type {\n FastifyMiddlewareFn,\n FastifyMiddlewareOptions,\n} from \"./types.d.ts\";\n// import type { JsonArray, JsonValue } from \"@thednp/rpc\";\nimport { scanForServerFiles, serverFunctionsMap } from \"@thednp/rpc/server\";\nimport { defaultMiddlewareOptions, defaultRPCOptions } from \"../options.ts\";\nimport { escapeRegExp } from \"../tools.ts\";\nimport { readBody } from \"./helpers.ts\";\n\nlet middlewareCount = 0;\nconst middlewareStack = new Set<string>();\n\n// Define the middleware function for Fastify\nexport const createMiddleware: FastifyMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n initialOptions,\n ) as FastifyMiddlewareOptions;\n\n const middlewareName = options.name;\n const rpcPreffix = options.rpcPreffix;\n const path = options.path;\n const handler = options.handler;\n\n let name = middlewareName;\n if (!name) {\n name = \"viteRPCMiddleware-\" + middlewareCount;\n middlewareCount += 1;\n }\n if (middlewareStack.has(name)) {\n throw new Error(`The middleware name \"${name}\" is already used.`);\n }\n middlewareStack.add(name);\n\n // Hoist regex compilation out of per-request path. Escape the prefix to\n // prevent regex injection via metacharacters in the config string.\n const prefixRegex: RegExp | null = rpcPreffix\n ? new RegExp(`^/${escapeRegExp(rpcPreffix)}/`)\n : null;\n const pathMatcher: RegExp | null = path\n ? (typeof path === \"string\" ? new RegExp(path) : path)\n : null;\n\n const middlewareHandler = async (\n req: FastifyRequest,\n _reply: FastifyReply,\n done: HookHandlerDoneFunction,\n ) => {\n const reqUrl = new URL(req.url, \"http://localhost\");\n const url = reqUrl.pathname;\n\n // When serving from production server, scan for server files\n if (serverFunctionsMap.size === 0) {\n await scanForServerFiles();\n }\n\n // No need to continue when no handler provided\n if (!handler) {\n done();\n return;\n }\n\n // Path matching\n if (pathMatcher && !pathMatcher.test(url)) {\n done();\n return;\n }\n\n // rpcPreffix matching (boundary-safe via escaped regex)\n if (prefixRegex && !prefixRegex.test(url)) {\n done();\n return;\n }\n\n // Execute handler\n await handler(req, _reply, done);\n };\n\n Object.defineProperty(middlewareHandler, \"name\", {\n value: name,\n });\n\n return middlewareHandler;\n};\n\nexport const createRPCMiddleware: FastifyMiddlewareFn = (\n initialOptions = {},\n) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n { rpcPreffix: defaultRPCOptions.rpcPreffix },\n initialOptions,\n ) as FastifyMiddlewareOptions;\n\n // Hoist prefix regex (escaped) and the literal prefix-for-replace out of the\n // per-request handler to avoid regex injection and per-request compilation.\n const rpcPreffix = options.rpcPreffix;\n const prefixRegex = rpcPreffix\n ? new RegExp(`^/${escapeRegExp(rpcPreffix)}/`)\n : /* istanbul ignore next */ null;\n const prefixReplace = `/${rpcPreffix}/`;\n\n return createMiddleware({\n ...options,\n handler: async (\n req: FastifyRequest,\n reply: FastifyReply,\n _done: HookHandlerDoneFunction,\n ) => {\n const reqUrl = new URL(req.url, \"http://localhost\");\n const url = reqUrl.pathname;\n\n // Defense-in-depth: validate prefix match via escaped regex even though\n // the outer createMiddleware gates on the same prefix already.\n // istanbul ignore if\n if (prefixRegex && !prefixRegex.test(url)) {\n return;\n }\n\n const functionName = url.replace(prefixReplace, \"\");\n const serverFunction = serverFunctionsMap.get(functionName);\n\n if (!serverFunction) {\n reply.status(404).send({\n error: \"Function not found\",\n });\n return;\n }\n\n try {\n const body = await readBody(req);\n const args = Array.isArray(body.data) ? body.data : [body.data];\n const { data: dataResult, cancel } = serverFunction.handler(...args);\n const onClose = () => cancel(\"client disconnected\");\n\n req.raw.on(\"close\", onClose);\n const data = await dataResult;\n req.raw.off(\"close\", onClose);\n\n // istanbul ignore else\n if (!reply.raw.headersSent) reply.status(200).send({ data });\n } catch (err) {\n console.error(String(err));\n reply.status(500).send({ error: \"Internal Server Error\" });\n }\n },\n });\n};\n","// src/fastify/plugin.ts\n// inspired by https://github.com/royalswe/vike-fastify-boilerplate/blob/main/server/index.ts\nimport type { FastifyInstance } from \"fastify\";\nimport fp from \"fastify-plugin\";\nimport type { MiddlewareOptions } from \"../types.d.ts\";\nimport { createRPCMiddleware } from \"./createMiddleware.ts\";\n\nexport type { MiddlewareOptions };\n\n// Define the plugin factory\nconst RpcPlugin = (\n fastify: FastifyInstance,\n initialOptions: Partial<MiddlewareOptions<\"fastify\">>,\n done: () => void,\n) => {\n // Register RPC middleware as preHandler hook\n const rpcMiddleware = createRPCMiddleware(initialOptions);\n fastify.addHook(\"preHandler\", async (request, reply) => {\n const next = () =>\n new Promise((resolve) => {\n rpcMiddleware(request, reply, resolve);\n });\n await next();\n });\n\n done();\n};\n\n// Export the plugin wrapped with fastify-plugin\nconst rpcPlugin = fp(RpcPlugin, {\n name: \"uni-rpc-fastify-plugin\",\n});\n\nexport { rpcPlugin as default };\n"],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region src/helpers.d.ts
|
|
2
|
+
declare const handleResponse: (response: Response) => Promise<any>;
|
|
3
|
+
declare const innerModule: (body: BodyInit, headers: HeadersInit, preffix: string, name: string) => {
|
|
4
|
+
data: Promise<any>;
|
|
5
|
+
cancel: (reason: string) => void;
|
|
6
|
+
};
|
|
7
|
+
//#endregion
|
|
8
|
+
export { handleResponse, innerModule };
|
|
9
|
+
//# sourceMappingURL=helpers.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.d.mts","names":[],"sources":["../../src/helpers.ts"],"mappings":";cACa,iBAAc,UAAoB,aAAQ;cAY1C,cAAW,MAChB,UAAQ,SACL,aAAW,iBACL;;EAIS,SAAA"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//#region src/helpers.ts
|
|
2
|
+
const handleResponse = async (response) => {
|
|
3
|
+
if (!response.ok) {
|
|
4
|
+
if (response.status === 499 || response.status === 408) return console.warn("Request was cancelled");
|
|
5
|
+
throw new Error("Fetch error: " + response.statusText);
|
|
6
|
+
}
|
|
7
|
+
const result = await response.json();
|
|
8
|
+
if (result.error) throw new Error(result.error);
|
|
9
|
+
return result.data;
|
|
10
|
+
};
|
|
11
|
+
const innerModule = (body, headers, preffix, name) => {
|
|
12
|
+
const controller = new AbortController();
|
|
13
|
+
const cancel = (reason) => controller.abort(reason);
|
|
14
|
+
const fetcher = async () => {
|
|
15
|
+
try {
|
|
16
|
+
const response = await fetch(`/${preffix}/${name}`, {
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers,
|
|
19
|
+
credentials: "include",
|
|
20
|
+
body,
|
|
21
|
+
signal: controller.signal
|
|
22
|
+
});
|
|
23
|
+
return await handleResponse(response);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
throw err;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
return {
|
|
29
|
+
data: fetcher(),
|
|
30
|
+
cancel
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
export { handleResponse, innerModule };
|
|
35
|
+
|
|
36
|
+
//# sourceMappingURL=helpers.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.mjs","names":[],"sources":["../../src/helpers.ts"],"sourcesContent":["// Client-side RPC sub-modules\nexport const handleResponse = async (response: Response) => {\n if (!response.ok) {\n if (response.status === 499 || response.status === 408) {\n return console.warn(\"Request was cancelled\");\n }\n throw new Error(\"Fetch error: \" + response.statusText);\n }\n const result = await response.json();\n if (result.error) throw new Error(result.error);\n return result.data;\n};\n\nexport const innerModule = (\n body: BodyInit,\n headers: HeadersInit,\n preffix: string,\n name: string,\n) => {\n const controller = new AbortController();\n const cancel = (reason: string) => controller.abort(reason);\n\n const fetcher = async () => {\n try {\n const response = await fetch(`/${preffix}/${name}`, {\n method: \"POST\",\n headers,\n credentials: \"include\",\n body,\n signal: controller.signal,\n });\n return await handleResponse(response);\n } catch (err) {\n throw err;\n }\n };\n\n return {\n data: fetcher(),\n cancel,\n };\n};\n"],"mappings":";AACA,MAAa,iBAAiB,OAAO,aAAuB;CAC1D,IAAI,CAAC,SAAS,IAAI;EAChB,IAAI,SAAS,WAAW,OAAO,SAAS,WAAW,KACjD,OAAO,QAAQ,KAAK,uBAAuB;EAE7C,MAAM,IAAI,MAAM,kBAAkB,SAAS,UAAU;CACvD;CACA,MAAM,SAAS,MAAM,SAAS,KAAK;CACnC,IAAI,OAAO,OAAO,MAAM,IAAI,MAAM,OAAO,KAAK;CAC9C,OAAO,OAAO;AAChB;AAEA,MAAa,eACX,MACA,SACA,SACA,SACG;CACH,MAAM,aAAa,IAAI,gBAAgB;CACvC,MAAM,UAAU,WAAmB,WAAW,MAAM,MAAM;CAE1D,MAAM,UAAU,YAAY;EAC1B,IAAI;GACF,MAAM,WAAW,MAAM,MAAM,IAAI,QAAQ,GAAG,QAAQ;IAClD,QAAQ;IACR;IACA,aAAa;IACb;IACA,QAAQ,WAAW;GACrB,CAAC;GACD,OAAO,MAAM,eAAe,QAAQ;EACtC,SAAS,KAAK;GACZ,MAAM;EACR;CACF;CAEA,OAAO;EACL,MAAM,QAAQ;EACd;CACF;AACF"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Context, Hono, MiddlewareHandler } from "hono";
|
|
2
|
+
import { MiddlewareOptions, RpcPluginOptions } from "@thednp/rpc";
|
|
3
|
+
import { HttpBindings } from "@hono/node-server";
|
|
4
|
+
import { ViteDevServer } from "vite";
|
|
5
|
+
import "express";
|
|
6
|
+
import "fastify";
|
|
7
|
+
import "koa";
|
|
8
|
+
//#region src/hono/types.d.ts
|
|
9
|
+
type HonoMiddlewareOptions = MiddlewareOptions<"hono">;
|
|
10
|
+
interface HonoMiddlewareHooks {
|
|
11
|
+
handler: MiddlewareHandler;
|
|
12
|
+
}
|
|
13
|
+
type HonoMiddlewareFn = <A extends RpcPluginOptions["adapter"] = "hono">(initialOptions?: Partial<MiddlewareOptions<A>>) => HonoMiddlewareHooks["handler"];
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/hono/createMiddleware.d.ts
|
|
16
|
+
declare const createMiddleware: HonoMiddlewareFn;
|
|
17
|
+
declare const createRPCMiddleware: HonoMiddlewareFn;
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/types.d.ts
|
|
20
|
+
type BodyResult = {
|
|
21
|
+
contentType: "application/json";
|
|
22
|
+
data: JsonValue;
|
|
23
|
+
} | {
|
|
24
|
+
contentType: "text/plain";
|
|
25
|
+
data: string;
|
|
26
|
+
};
|
|
27
|
+
// primitives and their compositions
|
|
28
|
+
type JsonPrimitive = string | number | boolean | null | undefined;
|
|
29
|
+
type JsonObject = {
|
|
30
|
+
[key: string]: JsonValue | JsonArray;
|
|
31
|
+
};
|
|
32
|
+
type JsonArray = JsonValue[];
|
|
33
|
+
type JsonValue = JsonPrimitive | JsonArray | JsonObject;
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/hono/helpers.d.ts
|
|
36
|
+
declare function attachRPC(app: Hono): Promise<void>;
|
|
37
|
+
declare function attachVite(app: Hono, vite: ViteDevServer): void;
|
|
38
|
+
/**
|
|
39
|
+
* Creates a hono compatible middleware for a given vite development server.
|
|
40
|
+
* @see https://github.com/honojs/hono/issues/3162#issuecomment-2331118049
|
|
41
|
+
* @param vite the vite development server
|
|
42
|
+
*/
|
|
43
|
+
declare const viteMiddleware: (vite: ViteDevServer) => import("hono").MiddlewareHandler<{
|
|
44
|
+
Bindings: HttpBindings;
|
|
45
|
+
}, string, {}, Response>;
|
|
46
|
+
declare const readBody: (c: Context) => Promise<BodyResult>;
|
|
47
|
+
//#endregion
|
|
48
|
+
export { type HonoMiddlewareFn, type HonoMiddlewareHooks, type HonoMiddlewareOptions, attachRPC, attachVite, createMiddleware, createRPCMiddleware, readBody, viteMiddleware };
|
|
49
|
+
//# sourceMappingURL=hono.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hono.d.mts","names":[],"sources":["../../src/hono/types.d.ts","../../src/hono/createMiddleware.ts","../../src/types.d.ts","../../src/hono/helpers.ts"],"mappings":";;;;;;;;KAGY,wBAAwB;UAEnB;EACf,SAAS;;KAGC,oBAAoB,UAAU,sCACxC,iBAAiB,QAAQ,kBAAkB,QACxC;;;cCEQ,kBAAkB;cAmElB,qBAAqB;;;KC3CtB;EACN;EAAiC,MAAM;;EACvC;EAA2B;;;KAOrB;KACA;GAAgB,cAAc,YAAY;;KAC1C,YAAY;KACZ,YAAY,gBAAgB,YAAY;;;iBCvC9B,UAAU,KAAK,OAAI;iBAOzB,WAAW,KAAK,MAAM,MAAM;;;;;;cAS/B,iBAAc,MAAU,iCAAa;EACZ,UAAA;eAuCrC;cAEY,WAAQ,GAChB,YACF,QAAQ"}
|