@thednp/rpc 0.2.0 → 0.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.
- package/AGENTS.md +54 -38
- package/CHANGELOG.md +85 -0
- package/README.md +27 -12
- package/dist/express/express.d.mts +5 -3
- package/dist/express/express.d.mts.map +1 -1
- package/dist/express/express.mjs +95 -20
- package/dist/express/express.mjs.map +1 -1
- package/dist/fastify/fastify.d.mts.map +1 -1
- package/dist/fastify/fastify.mjs +59 -10
- package/dist/fastify/fastify.mjs.map +1 -1
- package/dist/fastify/plugin/fastify/plugin.d.mts +13 -0
- package/dist/fastify/plugin/fastify/plugin.d.mts.map +1 -1
- package/dist/fastify/plugin/fastify/plugin.mjs +59 -10
- package/dist/fastify/plugin/fastify/plugin.mjs.map +1 -1
- package/dist/h3/h3.d.mts.map +1 -1
- package/dist/h3/h3.mjs +69 -16
- package/dist/h3/h3.mjs.map +1 -1
- package/dist/helpers/helpers.d.mts +63 -5
- package/dist/helpers/helpers.d.mts.map +1 -1
- package/dist/helpers/helpers.mjs +58 -1
- package/dist/helpers/helpers.mjs.map +1 -1
- package/dist/hono/hono.d.mts.map +1 -1
- package/dist/hono/hono.mjs +59 -8
- package/dist/hono/hono.mjs.map +1 -1
- package/dist/index.d.mts +48 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +84 -50
- package/dist/index.mjs.map +1 -1
- package/dist/koa/koa.d.mts.map +1 -1
- package/dist/koa/koa.mjs +70 -19
- package/dist/koa/koa.mjs.map +1 -1
- package/dist/server/server.d.mts +302 -27
- package/dist/server/server.d.mts.map +1 -1
- package/dist/server/server.mjs +180 -69
- package/dist/server/server.mjs.map +1 -1
- package/package.json +20 -18
package/dist/hono/hono.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hono.d.mts","names":[],"sources":["../../src/hono/types.d.ts","../../src/hono/createMiddleware.ts","../../src/hono/helpers.ts"],"mappings":";;;;;;;;;;;KAOY,mBAAmB;EAAoB;;;;;KAKvC,wBAAwB;;;;UAKnB;;EAEf,SAAS;;;;;;KAOC,oBAAoB,UAAU,sCACxC,iBAAiB,QAAQ,kBAAkB,QACxC;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"hono.d.mts","names":[],"sources":["../../src/hono/types.d.ts","../../src/hono/createMiddleware.ts","../../src/hono/helpers.ts"],"mappings":";;;;;;;;;;;KAOY,mBAAmB;EAAoB;;;;;KAKvC,wBAAwB;;;;UAKnB;;EAEf,SAAS;;;;;;KAOC,oBAAoB,UAAU,sCACxC,iBAAiB,QAAQ,kBAAkB,QACxC;;;;;;;;;;cCkBQ,kBAAkB;;;;;;;;cAgFlB,qBAAqB;;;;;;;;iBC3GZ,UAAU,KAAK,OAAI;;;;;;;cAgB5B,aAAU,KAAS,MAAI,MAAQ;;;;;;;;;cAY/B,iBAAc,MACnB,kBACL,kBAAkB;EAAmB,UAAU;;;;;;;;cAgDrC,WAAQ,GAChB,YACF,QAAQ;;;;;;;;;;;cA4DE,WAAQ,GAChB,SAAO,kBACM,SACR,uBACP"}
|
package/dist/hono/hono.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createMiddleware as createMiddleware$1 } from "hono/factory";
|
|
2
|
-
import { escapeRegExp, formatError, hasContentTypeMismatch, provideRequestContext,
|
|
3
|
-
//#region src/options.ts
|
|
2
|
+
import { escapeRegExp, formatError, getGlobalPrefix, hasContentTypeMismatch, provideRequestContext, safeURL, scanForServerFiles } from "@thednp/rpc/server";
|
|
4
3
|
const defaultRPCOptions = {
|
|
5
4
|
rpcPrefix: "__rpc",
|
|
6
5
|
adapter: "express",
|
|
@@ -13,11 +12,38 @@ const defaultMiddlewareOptions = {
|
|
|
13
12
|
origin: void 0
|
|
14
13
|
};
|
|
15
14
|
//#endregion
|
|
15
|
+
//#region src/functionsMap.ts
|
|
16
|
+
/**
|
|
17
|
+
* Global symbol under which the shared `serverFunctionsByPrefix` map is stored
|
|
18
|
+
* on `globalThis`. Keeping it on a `Symbol.for` key makes it instance-stable
|
|
19
|
+
* across the bundled entry copies (`index.mjs`, `server.mjs`, `express.mjs`,
|
|
20
|
+
* ...) and dev-server hot reloads, exactly like the request-context storage in
|
|
21
|
+
* `context.ts`. Without this, `scanForServerFiles` (bundled into the plugin)
|
|
22
|
+
* would populate a map copy the adapter middleware could not read.
|
|
23
|
+
*/
|
|
24
|
+
const functionsMapSymbol = Symbol.for("thednp.rpc.functionsMap");
|
|
25
|
+
/**
|
|
26
|
+
* Map of rpcPrefix -> Map of function names -> ServerFnEntry
|
|
27
|
+
* Enables multiple RPC instances with different prefixes to coexist
|
|
28
|
+
* without name collisions.
|
|
29
|
+
*/
|
|
30
|
+
const serverFunctionsByPrefix = globalThis[functionsMapSymbol] ??= /* @__PURE__ */ new Map();
|
|
31
|
+
/**
|
|
32
|
+
* Gets or creates the function map for a specific prefix.
|
|
33
|
+
* @param prefix - The RPC prefix (e.g., "__rpc", "v1:rpc", "admin:rpc")
|
|
34
|
+
* @returns Map of function names to ServerFnEntry for that prefix
|
|
35
|
+
*/
|
|
36
|
+
const getFunctionsForPrefix = (prefix) => {
|
|
37
|
+
if (!serverFunctionsByPrefix.has(prefix)) serverFunctionsByPrefix.set(prefix, /* @__PURE__ */ new Map());
|
|
38
|
+
return serverFunctionsByPrefix.get(prefix);
|
|
39
|
+
};
|
|
40
|
+
//#endregion
|
|
16
41
|
//#region src/constants.ts
|
|
17
42
|
const FUNCTION_NOT_FOUND = "Function not found";
|
|
18
43
|
const METHOD_NOT_ALLOWED = "Method Not Allowed";
|
|
19
44
|
const REQUEST_FORBIDDEN = "Forbidden";
|
|
20
45
|
const UNSUPPORTED_MEDIA_TYPE = "Unsupported Media Type";
|
|
46
|
+
const BAD_REQUEST = "Bad Request";
|
|
21
47
|
const CLIENT_DISCONNECTED = "client disconnected";
|
|
22
48
|
/** Returns a warning when a middleware name is reused, preventing registration conflicts. @param name - The duplicate middleware name */
|
|
23
49
|
const MIDDLEWARE_NAME_USED = (name) => `The middleware name "${name}" is already used.`;
|
|
@@ -134,7 +160,7 @@ const middlewareStack = /* @__PURE__ */ new Set();
|
|
|
134
160
|
const createMiddleware = (initialOptions = {}) => {
|
|
135
161
|
const options = Object.assign({}, defaultMiddlewareOptions, initialOptions);
|
|
136
162
|
const middlewareName = options.name;
|
|
137
|
-
|
|
163
|
+
let rpcPrefix = options.rpcPrefix;
|
|
138
164
|
const path = options.path;
|
|
139
165
|
const handler = options.handler;
|
|
140
166
|
let name = middlewareName;
|
|
@@ -147,11 +173,16 @@ const createMiddleware = (initialOptions = {}) => {
|
|
|
147
173
|
const prefixRegex = rpcPrefix ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`) : null;
|
|
148
174
|
const pathMatcher = path ? typeof path === "string" ? new RegExp(path) : path : null;
|
|
149
175
|
const middlewareHandler = createMiddleware$1(async (c, next) => {
|
|
150
|
-
const url =
|
|
151
|
-
if (serverFunctionsMap.size === 0) await scanForServerFiles();
|
|
176
|
+
const url = safeURL(c.req.path).pathname;
|
|
152
177
|
if (!handler) return next();
|
|
153
178
|
if (pathMatcher && !pathMatcher.test(url)) return next();
|
|
154
179
|
if (prefixRegex && !prefixRegex.test(url)) return next();
|
|
180
|
+
rpcPrefix = rpcPrefix ?? "__rpc";
|
|
181
|
+
if (getFunctionsForPrefix(rpcPrefix).size === 0) await scanForServerFiles({
|
|
182
|
+
rpcPrefix,
|
|
183
|
+
serverFiles: options.serverFiles,
|
|
184
|
+
scanRoot: options.scanRoot
|
|
185
|
+
});
|
|
155
186
|
return await handler(c, next);
|
|
156
187
|
});
|
|
157
188
|
Object.defineProperty(middlewareHandler, "name", { value: name });
|
|
@@ -167,8 +198,9 @@ const createMiddleware = (initialOptions = {}) => {
|
|
|
167
198
|
const createRPCMiddleware = (initialOptions = {}) => {
|
|
168
199
|
const options = Object.assign({}, defaultMiddlewareOptions, { rpcPrefix: defaultRPCOptions.rpcPrefix }, initialOptions);
|
|
169
200
|
const rpcPrefix = options.rpcPrefix;
|
|
201
|
+
const prefix = rpcPrefix || getGlobalPrefix() || "__rpc";
|
|
170
202
|
const prefixRegex = rpcPrefix ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`) : null;
|
|
171
|
-
const prefixReplace = `/${
|
|
203
|
+
const prefixReplace = `/${prefix}/`;
|
|
172
204
|
return createMiddleware({
|
|
173
205
|
...options,
|
|
174
206
|
handler: async (c, _next) => {
|
|
@@ -178,7 +210,7 @@ const createRPCMiddleware = (initialOptions = {}) => {
|
|
|
178
210
|
const requestOrigin = c.req.header("origin");
|
|
179
211
|
if (origin && requestOrigin && requestOrigin !== origin) return c.json({ error: REQUEST_FORBIDDEN }, 403);
|
|
180
212
|
const functionName = reqPath.replace(prefixReplace, "");
|
|
181
|
-
const serverFunction =
|
|
213
|
+
const serverFunction = getFunctionsForPrefix(prefix).get(functionName);
|
|
182
214
|
if (!serverFunction) return c.json({ error: FUNCTION_NOT_FOUND }, 404);
|
|
183
215
|
try {
|
|
184
216
|
const method = serverFunction.options?.method || "POST";
|
|
@@ -186,7 +218,11 @@ const createRPCMiddleware = (initialOptions = {}) => {
|
|
|
186
218
|
let args = [];
|
|
187
219
|
if (method === "GET") {
|
|
188
220
|
const raw = c.req.query("args");
|
|
189
|
-
if (raw)
|
|
221
|
+
if (raw) {
|
|
222
|
+
const parsed = JSON.parse(raw);
|
|
223
|
+
if (!Array.isArray(parsed)) return c.json({ error: BAD_REQUEST }, 400);
|
|
224
|
+
args = parsed;
|
|
225
|
+
}
|
|
190
226
|
} else {
|
|
191
227
|
if (hasContentTypeMismatch(serverFunction.options?.contentType ?? "application/json", c.req.header("content-type"))) return c.json({ error: UNSUPPORTED_MEDIA_TYPE }, 415);
|
|
192
228
|
const body = await readBody(c);
|
|
@@ -197,11 +233,19 @@ const createRPCMiddleware = (initialOptions = {}) => {
|
|
|
197
233
|
response: c.res,
|
|
198
234
|
nativeEvent: c,
|
|
199
235
|
locals: {},
|
|
236
|
+
functionName,
|
|
200
237
|
redirect: (location, status = 303) => {
|
|
201
238
|
requestEvent.redirected = {
|
|
202
239
|
location,
|
|
203
240
|
status
|
|
204
241
|
};
|
|
242
|
+
},
|
|
243
|
+
send: (status, body, headers) => {
|
|
244
|
+
requestEvent.sent = {
|
|
245
|
+
status,
|
|
246
|
+
body,
|
|
247
|
+
headers
|
|
248
|
+
};
|
|
205
249
|
}
|
|
206
250
|
};
|
|
207
251
|
const fnResult = provideRequestContext(requestEvent, () => serverFunction.handler(...args));
|
|
@@ -210,6 +254,13 @@ const createRPCMiddleware = (initialOptions = {}) => {
|
|
|
210
254
|
const result = await fnResult.data;
|
|
211
255
|
c.env.incoming?.off("close", onAbort);
|
|
212
256
|
if (requestEvent.redirected) return c.redirect(requestEvent.redirected.location, requestEvent.redirected.status);
|
|
257
|
+
if (requestEvent.sent) {
|
|
258
|
+
const { status, body, headers } = requestEvent.sent;
|
|
259
|
+
return c.body(JSON.stringify(body), status, {
|
|
260
|
+
"content-type": "application/json",
|
|
261
|
+
...headers
|
|
262
|
+
});
|
|
263
|
+
}
|
|
213
264
|
return c.json({ data: result }, 200);
|
|
214
265
|
} catch (err) {
|
|
215
266
|
console.error(String(err));
|
package/dist/hono/hono.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hono.mjs","names":["createMiddleware","createHonoMiddleware"],"sources":["../../src/options.ts","../../src/constants.ts","../../src/hono/helpers.ts","../../src/hono/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 credentials: \"same-origin\",\n method: \"POST\",\n} satisfies ServerFunctionOptions;\n\nexport const defaultRPCOptions: RpcPluginOptions = {\n rpcPrefix: \"__rpc\",\n adapter: \"express\",\n serverFiles: \"exact\",\n scanRoot: undefined,\n};\n\nexport const defaultMiddlewareOptions = {\n rpcPrefix: undefined,\n path: undefined,\n origin: undefined,\n} satisfies MiddlewareOptions;\n","export const OPERATION_ABORTED = \"Operation aborted\";\n\nexport const REQUEST_CANCELLED = \"Request was cancelled\";\n\nexport const FETCH_ERROR_PREFIX = \"Fetch error: \";\n\nexport const NO_SERVER_FUNCTION_FOUND = \"No server function found.\";\n\nexport const ERROR_LOADING_FILE = \"Error loading file:\";\n\nexport const FUNCTION_NOT_FOUND = \"Function not found\";\n\nexport const METHOD_NOT_ALLOWED = \"Method Not Allowed\";\n\nexport const REQUEST_FORBIDDEN = \"Forbidden\";\n\nexport const UNSUPPORTED_MEDIA_TYPE = \"Unsupported Media Type\";\n\nexport const INTERNAL_SERVER_ERROR = \"Internal Server Error\";\n\nexport const CLIENT_DISCONNECTED = \"client disconnected\";\n\n/** Returns a warning when a middleware name is reused, preventing registration conflicts. @param name - The duplicate middleware name */\nexport const MIDDLEWARE_NAME_USED = (name: string) =>\n `The middleware name \"${name}\" is already used.`;\n\n/** Error message when a value fails the safe-identifier validation. @param label - What kind of value was being validated. @param name - The rejected value */\nexport const INVALID_IDENTIFIER = (label: string, name: string) =>\n `Invalid ${label}: \"${name}\" must match /^[A-Za-z_$][A-Za-z0-9_$]*$/`;\n\n/** Error message when a value fails the safe-path-segment validation. @param label - What kind of value was being validated. @param segment - The rejected value */\nexport const INVALID_PATH_SEGMENT = (label: string, segment: string) =>\n `Invalid ${label}: \"${segment}\" must match /^[A-Za-z0-9_$][A-Za-z0-9_$/-]*$/`;\n\n/** Warning message when a specified RPC config file cannot be resolved on disk. @param configFile - The requested config filename. @param configFilePath - The resolved absolute path */\nexport const CONFIG_FILE_NOT_FOUND = (\n configFile: string,\n configFilePath: string,\n) =>\n ` ⚠︎ The specified RPC config file ${configFile} cannot be found at ${configFilePath}, loading the defaults..`;\n\nexport const NO_CONFIG_FOUND = ` ⚡︎ No RPC config found, loading the defaults..`;\n\nexport const FAILED_LOAD_CONFIG = ` ⚠︎ Failed to load RPC config:`;\n\n/** Error template for duplicate server function names across files. @param name - The duplicate registered name */\nexport const DUPLICATE_FUNCTION_NAME = (name: string) =>\n `Duplicate server function \"${name}\" detected. Each server function must have a unique name. Remove or rename the duplicate.`;\n","// src/hono/helpers.ts\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type { HttpBindings } from \"@hono/node-server\";\nimport type { Context, Hono } from \"hono\";\nimport type { ViteDevServer } from \"vite\";\nimport type {\n ContentfulStatusCode,\n RedirectStatusCode,\n} from \"hono/utils/http-status\";\nimport type { BodyResult } from \"@thednp/rpc\";\nimport type { IncomingWithBody } from \"./types.d.ts\";\nimport { createMiddleware } from \"hono/factory\";\nimport { createRPCMiddleware } from \"./createMiddleware.ts\";\n\n/**\n * Convenience function to load RPC config and attach the RPC middleware to a Hono app.\n * Dynamically imports loadRPCConfig and registers the middleware.\n * @param app - Hono application instance\n */\nexport async function attachRPC(app: Hono) {\n // The main plugin entry statically imports Vite, so loadRPCConfig is\n // imported lazily: function bundles that never call attachRPC (e.g.\n // serverless functions) keep Vite out of the bundle (or externalized).\n const { loadRPCConfig } = await import(\"@thednp/rpc\");\n const { adapter: _adapter, ...options } = await loadRPCConfig();\n\n app.use(createRPCMiddleware(options));\n}\n\n/**\n * Attaches Vite's dev server middlewares to a Hono app for development mode.\n * Uses the viteMiddleware wrapper to bridge Vite's Connect-compatible stack into Hono.\n * @param app - Hono application instance\n * @param vite - Running Vite dev server\n */\nexport const attachVite = (app: Hono, vite: ViteDevServer): void => {\n app.use(viteMiddleware(vite));\n};\n\n/**\n * Creates a Hono-compatible middleware from a Vite dev server middleware stack.\n * Bridges the Connect/Express middleware interface to Hono's context-based request/response model.\n * Supports both Node.js and Bun runtimes with separate polyfill paths.\n * @param vite - Running Vite dev server\n * @returns A Hono middleware function\n * @see https://github.com/honojs/hono/issues/3162#issuecomment-2331118049\n */\nexport const viteMiddleware = (\n vite: ViteDevServer,\n): ReturnType<typeof createMiddleware<{ Bindings: HttpBindings }>> => {\n return createMiddleware<{ Bindings: HttpBindings }>((c, next) => {\n return new Promise((resolve) => {\n // Node.js\n // @ts-expect-error - NodeJS is different\n // istanbul ignore if\n if (typeof Bun === \"undefined\") {\n vite.middlewares(c.env.incoming, c.env.outgoing, () => resolve(next()));\n return;\n }\n\n /* istanbul ignore next */ {\n // Bun\n let sent = false;\n const headers = new Headers();\n // Polyfill the node:http IncommingMessage and ServerResponse\n vite.middlewares(\n {\n url: new URL(c.req.path, \"http://localhost\").pathname,\n method: c.req.raw.method,\n headers: Object.fromEntries(c.req.raw.headers),\n } as IncomingMessage,\n {\n setHeader(name, value: string) {\n headers.set(name, value);\n return this;\n },\n end(body) {\n sent = true;\n resolve(\n // @ts-expect-error - weird\n c.body(body, c.res.status as ContentfulStatusCode, headers),\n );\n },\n } as ServerResponse,\n () => sent || resolve(next()),\n );\n }\n });\n });\n};\n\n/**\n * Reads and parses the HTTP request body from a Hono context.\n * Supports JSON and text content types, with pre-parsed body detection for server-side environments.\n * @param c - Hono request context\n * @returns A promise resolving to the parsed body with its content type\n */\nexport const readBody = async (\n c: Context,\n): Promise<BodyResult> => {\n const contentType = c.req.header(\"content-type\")?.toLowerCase() || \"\";\n const isJSON = contentType.includes(\"json\");\n const isMultipart = contentType.includes(\"multipart/form-data\");\n const isUrlEncoded = contentType.includes(\"urlencoded\");\n const incoming = (c.env as HttpBindings).incoming as\n | IncomingWithBody\n | undefined;\n if (incoming?.body !== undefined) {\n const reqBody = incoming.body;\n return {\n contentType: isMultipart\n ? \"multipart/form-data\"\n : isJSON\n ? \"application/json\"\n : isUrlEncoded\n ? \"application/x-www-form-urlencoded\"\n : \"text/plain\",\n data: isMultipart\n ? (reqBody as Record<string, unknown>)\n : isJSON\n ? reqBody\n : isUrlEncoded\n ? (reqBody as Record<string, unknown>)\n : String(reqBody),\n } as BodyResult;\n }\n if (isJSON) {\n const data = await c.req.json();\n return {\n contentType: \"application/json\",\n data,\n };\n }\n\n const text = await c.req.text();\n return {\n contentType: isMultipart\n ? \"multipart/form-data\"\n : isUrlEncoded\n ? \"application/x-www-form-urlencoded\"\n : \"text/plain\",\n data: isMultipart\n ? ({ raw: text } as Record<string, unknown>)\n : isUrlEncoded\n ? Object.fromEntries(new URLSearchParams(text))\n : String(text),\n } as BodyResult;\n};\n\n/**\n * Issues an HTTP redirect on a Hono context. Hono's `c.redirect(location,\n * status)` returns a `Response` object that the handler must return (it never\n * writes directly). Defaults to `303 See Other` for convention\n * (Post/Redirect/Get).\n * @param c - Hono context\n * @param location - The URL to redirect to\n * @param status - HTTP status code, defaults to 303\n * @returns A Hono `Response` to return from the handler\n */\nexport const redirect = (\n c: Context,\n location: string,\n status: RedirectStatusCode = 303,\n): Response => {\n return c.redirect(location, status);\n};\n","// src/hono/createMiddleware.ts\nimport type { Context, Next } from \"hono\";\nimport type { RedirectStatusCode } from \"hono/utils/http-status\";\nimport type { HonoMiddlewareFn, HonoMiddlewareOptions } from \"./types.d.ts\";\nimport type { JsonValue } from \"@thednp/rpc\";\nimport { createMiddleware as createHonoMiddleware } from \"hono/factory\";\nimport type { RequestEvent } from \"@thednp/rpc/server\";\nimport {\n escapeRegExp,\n formatError,\n hasContentTypeMismatch,\n provideRequestContext,\n scanForServerFiles,\n serverFunctionsMap,\n} from \"@thednp/rpc/server\";\nimport { defaultMiddlewareOptions, defaultRPCOptions } from \"../options.ts\";\nimport {\n CLIENT_DISCONNECTED,\n FUNCTION_NOT_FOUND,\n METHOD_NOT_ALLOWED,\n MIDDLEWARE_NAME_USED,\n REQUEST_FORBIDDEN,\n UNSUPPORTED_MEDIA_TYPE,\n} from \"../constants.ts\";\nimport { readBody } from \"./helpers.ts\";\n\nlet middlewareCount = 0;\nconst middlewareStack = new Set<string>();\n\n/**\n * Creates a Hono middleware with optional path and rpcPrefix filtering.\n * Middleware names are deduplicated. Prefix and path regexes are compiled once at creation time.\n * Uses Hono's factory `createMiddleware` to wrap the handler.\n * @param initialOptions - Options for rpcPrefix, path matching, and the handler function\n * @returns A Hono middleware function\n */\nexport const createMiddleware: HonoMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n initialOptions,\n ) as HonoMiddlewareOptions;\n\n const middlewareName = options.name;\n const rpcPrefix = options.rpcPrefix;\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(MIDDLEWARE_NAME_USED(name));\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 = rpcPrefix\n ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`)\n : null;\n const pathMatcher: RegExp | null = path\n ? (typeof path === \"string\" ? new RegExp(path) : path)\n : null;\n\n const middlewareHandler = createHonoMiddleware(\n async (c: Context, next: Next) => {\n const reqUrl = new URL(c.req.path, \"http://localhost\");\n const url = reqUrl.pathname;\n\n if (serverFunctionsMap.size === 0) {\n await scanForServerFiles();\n }\n\n // No need to continue when no handler provided\n if (!handler) {\n return next();\n }\n\n if (pathMatcher && !pathMatcher.test(url)) {\n return next();\n }\n\n if (prefixRegex && !prefixRegex.test(url)) {\n return next();\n }\n\n return (await handler(c, next)) as Response;\n },\n );\n\n Object.defineProperty(middlewareHandler, \"name\", {\n value: name,\n });\n\n return middlewareHandler;\n};\n\n/**\n * Creates the Hono RPC middleware that routes incoming requests to registered server functions.\n * Wraps the generic createMiddleware with the RPC handler that reads the body, dispatches\n * to the matching function, and returns the JSON-serialized result.\n * @param initialOptions - Options including rpcPrefix for URL routing\n * @returns A Hono middleware function\n */\nexport const createRPCMiddleware: HonoMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n { rpcPrefix: defaultRPCOptions.rpcPrefix },\n initialOptions,\n ) as HonoMiddlewareOptions;\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 rpcPrefix = options.rpcPrefix;\n const prefixRegex = rpcPrefix\n ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`)\n : /* istanbul ignore next */ null;\n const prefixReplace = `/${rpcPrefix}/`;\n\n return createMiddleware({\n ...options,\n handler: async (c: Context, _next: Next) => {\n const { path: reqPath } = c.req;\n // const { rpcPrefix: prefix } = options;\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(reqPath)) {\n /* istanbul ignore next */\n return;\n }\n\n // Optional origin check: reject requests whose Origin header does not\n // match the configured origin. Requests without an Origin header\n // (curl, native clients) pass through unchecked.\n const origin = options.origin;\n const requestOrigin = c.req.header(\"origin\");\n if (origin && requestOrigin && requestOrigin !== origin) {\n return c.json({ error: REQUEST_FORBIDDEN }, 403);\n }\n\n const functionName = reqPath.replace(prefixReplace, \"\");\n const serverFunction = serverFunctionsMap.get(functionName);\n\n if (!serverFunction) {\n return c.json({ error: FUNCTION_NOT_FOUND }, 404);\n }\n\n try {\n const method = serverFunction.options?.method || \"POST\";\n if (c.req.method.toUpperCase() !== method) {\n return c.json({ error: METHOD_NOT_ALLOWED }, 405);\n }\n\n let args: JsonValue[] = [];\n if (method === \"GET\") {\n const raw = c.req.query(\"args\");\n // istanbul ignore else\n if (raw) args = JSON.parse(raw);\n } else {\n // Content-type enforcement: strict for json/text, lenient between forms.\n // Requests without a Content-Type header are exempt (curl/GET compat).\n // Checked BEFORE readBody so mismatched bodies are never buffered.\n if (\n hasContentTypeMismatch(\n serverFunction.options?.contentType ?? \"application/json\",\n c.req.header(\"content-type\"),\n )\n ) {\n return c.json({ error: UNSUPPORTED_MEDIA_TYPE }, 415);\n }\n const body = await readBody(c);\n args = Array.isArray(body.data)\n ? body.data as JsonValue[]\n : [body.data as JsonValue];\n }\n const requestEvent: RequestEvent = {\n request: c.req,\n response: c.res,\n nativeEvent: c,\n locals: {},\n // Hono's `c.redirect` returns a `Response` (never writes directly),\n // so the bound redirect only records the intent; the middleware uses\n // it after the dispatch to `return c.redirect(location, status)`.\n redirect: (location, status = 303) => {\n requestEvent.redirected = { location, status };\n },\n };\n const fnResult = provideRequestContext(\n requestEvent,\n () => serverFunction.handler(...args),\n );\n const onAbort = () => fnResult.cancel(CLIENT_DISCONNECTED);\n // The runtime adapter may be absent in some Hono environments\n // (e.g. standalone serverless adapters), so guard the close hook.\n c.env.incoming?.on(\"close\", onAbort);\n const result = await fnResult.data;\n c.env.incoming?.off(\"close\", onAbort);\n\n if (requestEvent.redirected) {\n return c.redirect(\n requestEvent.redirected.location,\n requestEvent.redirected.status as RedirectStatusCode,\n ) as Response;\n }\n\n return c.json({ data: result }, 200);\n } catch (err) {\n console.error(String(err));\n const isProduction = process.env.NODE_ENV === \"production\";\n return c.json(formatError(err, isProduction), 500);\n }\n },\n });\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"hono.mjs","names":["createMiddleware","createHonoMiddleware"],"sources":["../../src/options.ts","../../src/functionsMap.ts","../../src/constants.ts","../../src/hono/helpers.ts","../../src/hono/createMiddleware.ts"],"sourcesContent":["import type {\n MiddlewareOptions,\n RpcPluginOptions,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\n\nexport const defaultServerFnOptions: ServerFunctionOptions = {\n contentType: \"application/json\",\n credentials: \"same-origin\",\n method: \"POST\",\n};\n\nexport const defaultPrefix = \"__rpc\";\n\nexport const defaultRPCOptions: RpcPluginOptions = {\n rpcPrefix: defaultPrefix,\n adapter: \"express\",\n serverFiles: \"exact\",\n scanRoot: undefined,\n};\n\nexport const defaultMiddlewareOptions: MiddlewareOptions = {\n rpcPrefix: undefined,\n path: undefined,\n origin: undefined,\n};\n","import type { ServerFnEntry } from \"./types.d.ts\";\nimport { defaultPrefix } from \"./options.ts\";\n\n/**\n * Global symbol under which the shared `serverFunctionsByPrefix` map is stored\n * on `globalThis`. Keeping it on a `Symbol.for` key makes it instance-stable\n * across the bundled entry copies (`index.mjs`, `server.mjs`, `express.mjs`,\n * ...) and dev-server hot reloads, exactly like the request-context storage in\n * `context.ts`. Without this, `scanForServerFiles` (bundled into the plugin)\n * would populate a map copy the adapter middleware could not read.\n */\nconst functionsMapSymbol = Symbol.for(\"thednp.rpc.functionsMap\");\n\n/**\n * Map of rpcPrefix -> Map of function names -> ServerFnEntry\n * Enables multiple RPC instances with different prefixes to coexist\n * without name collisions.\n */\nexport const serverFunctionsByPrefix: Map<\n string,\n Map<string, ServerFnEntry>\n> = ((globalThis as Record<symbol, Map<string, Map<string, ServerFnEntry>>>)[\n functionsMapSymbol\n] ??= new Map());\n\n/**\n * Gets or creates the function map for a specific prefix.\n * @param prefix - The RPC prefix (e.g., \"__rpc\", \"v1:rpc\", \"admin:rpc\")\n * @returns Map of function names to ServerFnEntry for that prefix\n */\nexport const getFunctionsForPrefix = (\n prefix: string,\n): Map<string, ServerFnEntry> => {\n if (!serverFunctionsByPrefix.has(prefix)) {\n serverFunctionsByPrefix.set(prefix, new Map());\n }\n return serverFunctionsByPrefix.get(prefix)!;\n};\n\n/**\n * Backward compatibility: default map for the default prefix.\n * Legacy code can still use serverFunctionsMap.set(name, entry).\n */\nexport const serverFunctionsMap: Map<string, ServerFnEntry> = {\n get: (key: string) => getFunctionsForPrefix(defaultPrefix).get(key),\n set: (key: string, value: ServerFnEntry) =>\n getFunctionsForPrefix(defaultPrefix).set(key, value),\n has: (key: string) => getFunctionsForPrefix(defaultPrefix).has(key),\n delete: (key: string) => getFunctionsForPrefix(defaultPrefix).delete(key),\n clear: () => getFunctionsForPrefix(defaultPrefix).clear(),\n get size() {\n return getFunctionsForPrefix(defaultPrefix).size;\n },\n entries: () => getFunctionsForPrefix(defaultPrefix).entries(),\n keys: () => getFunctionsForPrefix(defaultPrefix).keys(),\n values: () => getFunctionsForPrefix(defaultPrefix).values(),\n forEach: (\n callback: (\n value: ServerFnEntry,\n key: string,\n map: Map<string, ServerFnEntry>,\n ) => void,\n ) => getFunctionsForPrefix(defaultPrefix).forEach(callback),\n [Symbol.iterator]: () =>\n getFunctionsForPrefix(defaultPrefix)[Symbol.iterator](),\n} as unknown as Map<string, ServerFnEntry>;\n","export const OPERATION_ABORTED = \"Operation aborted\";\n\nexport const REQUEST_CANCELLED = \"Request was cancelled\";\n\nexport const FETCH_ERROR_PREFIX = \"Fetch error: \";\n\nexport const NO_SERVER_FUNCTION_FOUND = \"No server function found.\";\n\nexport const ERROR_LOADING_FILE = \"Error loading file:\";\n\nexport const FUNCTION_NOT_FOUND = \"Function not found\";\n\nexport const METHOD_NOT_ALLOWED = \"Method Not Allowed\";\n\nexport const REQUEST_FORBIDDEN = \"Forbidden\";\n\nexport const UNSUPPORTED_MEDIA_TYPE = \"Unsupported Media Type\";\n\nexport const BAD_REQUEST = \"Bad Request\";\n\nexport const INTERNAL_SERVER_ERROR = \"Internal Server Error\";\n\nexport const CLIENT_DISCONNECTED = \"client disconnected\";\n\n/** Returns a warning when a middleware name is reused, preventing registration conflicts. @param name - The duplicate middleware name */\nexport const MIDDLEWARE_NAME_USED = (name: string) =>\n `The middleware name \"${name}\" is already used.`;\n\n/** Error message when a value fails the safe-identifier validation. @param label - What kind of value was being validated. @param name - The rejected value */\nexport const INVALID_IDENTIFIER = (label: string, name: string) =>\n `Invalid ${label}: \"${name}\" must match /^[A-Za-z_$][A-Za-z0-9_$]*$/`;\n\n/** Error message when a value fails the safe-path-segment validation. @param label - What kind of value was being validated. @param segment - The rejected value */\nexport const INVALID_PATH_SEGMENT = (label: string, segment: string) =>\n `Invalid ${label}: \"${segment}\" must match /^[A-Za-z0-9_$@:][A-Za-z0-9_$@:/-]*$/`;\n\n/** Warning message when a specified RPC config file cannot be resolved on disk. @param configFile - The requested config filename. @param configFilePath - The resolved absolute path */\nexport const CONFIG_FILE_NOT_FOUND = (\n configFile: string,\n configFilePath: string,\n) =>\n ` ⚠︎ The specified RPC config file ${configFile} cannot be found at ${configFilePath}, loading the defaults..`;\n\nexport const NO_CONFIG_FOUND = ` ⚡︎ No RPC config found, loading the defaults..`;\n\nexport const FAILED_LOAD_CONFIG = ` ⚠︎ Failed to load RPC config:`;\n\n/** Error template for duplicate server function names across files. @param name - The duplicate registered name */\nexport const DUPLICATE_FUNCTION_NAME = (name: string) =>\n `Duplicate server function \"${name}\" detected. Each server function must have a unique name. Remove or rename the duplicate.`;\n","// src/hono/helpers.ts\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type { HttpBindings } from \"@hono/node-server\";\nimport type { Context, Hono } from \"hono\";\nimport type { ViteDevServer } from \"vite\";\nimport type {\n ContentfulStatusCode,\n RedirectStatusCode,\n} from \"hono/utils/http-status\";\nimport type { BodyResult } from \"@thednp/rpc\";\nimport type { IncomingWithBody } from \"./types.d.ts\";\nimport { createMiddleware } from \"hono/factory\";\nimport { createRPCMiddleware } from \"./createMiddleware.ts\";\n\n/**\n * Convenience function to load RPC config and attach the RPC middleware to a Hono app.\n * Dynamically imports loadRPCConfig and registers the middleware.\n * @param app - Hono application instance\n */\nexport async function attachRPC(app: Hono) {\n // The main plugin entry statically imports Vite, so loadRPCConfig is\n // imported lazily: function bundles that never call attachRPC (e.g.\n // serverless functions) keep Vite out of the bundle (or externalized).\n const { loadRPCConfig } = await import(\"@thednp/rpc\");\n const { adapter: _adapter, ...options } = await loadRPCConfig();\n\n app.use(createRPCMiddleware(options));\n}\n\n/**\n * Attaches Vite's dev server middlewares to a Hono app for development mode.\n * Uses the viteMiddleware wrapper to bridge Vite's Connect-compatible stack into Hono.\n * @param app - Hono application instance\n * @param vite - Running Vite dev server\n */\nexport const attachVite = (app: Hono, vite: ViteDevServer): void => {\n app.use(viteMiddleware(vite));\n};\n\n/**\n * Creates a Hono-compatible middleware from a Vite dev server middleware stack.\n * Bridges the Connect/Express middleware interface to Hono's context-based request/response model.\n * Supports both Node.js and Bun runtimes with separate polyfill paths.\n * @param vite - Running Vite dev server\n * @returns A Hono middleware function\n * @see https://github.com/honojs/hono/issues/3162#issuecomment-2331118049\n */\nexport const viteMiddleware = (\n vite: ViteDevServer,\n): ReturnType<typeof createMiddleware<{ Bindings: HttpBindings }>> => {\n return createMiddleware<{ Bindings: HttpBindings }>((c, next) => {\n return new Promise((resolve) => {\n // Node.js\n // @ts-expect-error - NodeJS is different\n // istanbul ignore if\n if (typeof Bun === \"undefined\") {\n vite.middlewares(c.env.incoming, c.env.outgoing, () => resolve(next()));\n return;\n }\n\n /* istanbul ignore next */ {\n // Bun\n let sent = false;\n const headers = new Headers();\n // Polyfill the node:http IncommingMessage and ServerResponse\n vite.middlewares(\n {\n url: new URL(c.req.path, \"http://localhost\").pathname,\n method: c.req.raw.method,\n headers: Object.fromEntries(c.req.raw.headers),\n } as IncomingMessage,\n {\n setHeader(name, value: string) {\n headers.set(name, value);\n return this;\n },\n end(body) {\n sent = true;\n resolve(\n // @ts-expect-error - weird\n c.body(body, c.res.status as ContentfulStatusCode, headers),\n );\n },\n } as ServerResponse,\n () => sent || resolve(next()),\n );\n }\n });\n });\n};\n\n/**\n * Reads and parses the HTTP request body from a Hono context.\n * Supports JSON and text content types, with pre-parsed body detection for server-side environments.\n * @param c - Hono request context\n * @returns A promise resolving to the parsed body with its content type\n */\nexport const readBody = async (\n c: Context,\n): Promise<BodyResult> => {\n const contentType = c.req.header(\"content-type\")?.toLowerCase() || \"\";\n const isJSON = contentType.includes(\"json\");\n const isMultipart = contentType.includes(\"multipart/form-data\");\n const isUrlEncoded = contentType.includes(\"urlencoded\");\n const incoming = (c.env as HttpBindings).incoming as\n | IncomingWithBody\n | undefined;\n if (incoming?.body !== undefined) {\n const reqBody = incoming.body;\n return {\n contentType: isMultipart\n ? \"multipart/form-data\"\n : isJSON\n ? \"application/json\"\n : isUrlEncoded\n ? \"application/x-www-form-urlencoded\"\n : \"text/plain\",\n data: isMultipart\n ? (reqBody as Record<string, unknown>)\n : isJSON\n ? reqBody\n : isUrlEncoded\n ? (reqBody as Record<string, unknown>)\n : String(reqBody),\n } as BodyResult;\n }\n if (isJSON) {\n const data = await c.req.json();\n return {\n contentType: \"application/json\",\n data,\n };\n }\n\n const text = await c.req.text();\n return {\n contentType: isMultipart\n ? \"multipart/form-data\"\n : isUrlEncoded\n ? \"application/x-www-form-urlencoded\"\n : \"text/plain\",\n data: isMultipart\n ? ({ raw: text } as Record<string, unknown>)\n : isUrlEncoded\n ? Object.fromEntries(new URLSearchParams(text))\n : String(text),\n } as BodyResult;\n};\n\n/**\n * Issues an HTTP redirect on a Hono context. Hono's `c.redirect(location,\n * status)` returns a `Response` object that the handler must return (it never\n * writes directly). Defaults to `303 See Other` for convention\n * (Post/Redirect/Get).\n * @param c - Hono context\n * @param location - The URL to redirect to\n * @param status - HTTP status code, defaults to 303\n * @returns A Hono `Response` to return from the handler\n */\nexport const redirect = (\n c: Context,\n location: string,\n status: RedirectStatusCode = 303,\n): Response => {\n return c.redirect(location, status);\n};\n","// src/hono/createMiddleware.ts\nimport type { Context, Next } from \"hono\";\nimport type {\n ContentfulStatusCode,\n RedirectStatusCode,\n} from \"hono/utils/http-status\";\nimport type { HonoMiddlewareFn, HonoMiddlewareOptions } from \"./types.d.ts\";\nimport type { JsonValue } from \"@thednp/rpc\";\nimport { createMiddleware as createHonoMiddleware } from \"hono/factory\";\nimport type { RequestEvent } from \"@thednp/rpc/server\";\nimport {\n escapeRegExp,\n formatError,\n getGlobalPrefix,\n hasContentTypeMismatch,\n provideRequestContext,\n safeURL,\n scanForServerFiles,\n} from \"@thednp/rpc/server\";\nimport { getFunctionsForPrefix } from \"../functionsMap.ts\";\nimport {\n defaultMiddlewareOptions,\n defaultPrefix,\n defaultRPCOptions,\n} from \"../options.ts\";\nimport {\n BAD_REQUEST,\n CLIENT_DISCONNECTED,\n FUNCTION_NOT_FOUND,\n METHOD_NOT_ALLOWED,\n MIDDLEWARE_NAME_USED,\n REQUEST_FORBIDDEN,\n UNSUPPORTED_MEDIA_TYPE,\n} from \"../constants.ts\";\nimport { readBody } from \"./helpers.ts\";\n\nlet middlewareCount = 0;\nconst middlewareStack = new Set<string>();\n\n/**\n * Creates a Hono middleware with optional path and rpcPrefix filtering.\n * Middleware names are deduplicated. Prefix and path regexes are compiled once at creation time.\n * Uses Hono's factory `createMiddleware` to wrap the handler.\n * @param initialOptions - Options for rpcPrefix, path matching, and the handler function\n * @returns A Hono middleware function\n */\nexport const createMiddleware: HonoMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n initialOptions,\n ) as HonoMiddlewareOptions;\n\n const middlewareName = options.name;\n let rpcPrefix = options.rpcPrefix;\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(MIDDLEWARE_NAME_USED(name));\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 = rpcPrefix\n ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`)\n : null;\n const pathMatcher: RegExp | null = path\n ? (typeof path === \"string\" ? new RegExp(path) : path)\n : null;\n\n const middlewareHandler = createHonoMiddleware(\n async (c: Context, next: Next) => {\n const reqUrl = safeURL(c.req.path);\n const url = reqUrl.pathname;\n\n // No need to continue when no handler provided\n if (!handler) {\n return next();\n }\n\n if (pathMatcher && !pathMatcher.test(url)) {\n return next();\n }\n\n if (prefixRegex && !prefixRegex.test(url)) {\n return next();\n }\n\n rpcPrefix = (rpcPrefix ?? defaultPrefix) as string;\n\n // When serving from production server, scan for server files\n if (getFunctionsForPrefix(rpcPrefix).size === 0) {\n await scanForServerFiles({\n rpcPrefix,\n serverFiles:\n (options as unknown as { serverFiles?: \"exact\" | \"glob\" })\n .serverFiles,\n scanRoot: (options as unknown as { scanRoot?: string }).scanRoot,\n } as never);\n }\n\n return (await handler(c, next)) as Response;\n },\n );\n\n Object.defineProperty(middlewareHandler, \"name\", {\n value: name,\n });\n\n return middlewareHandler;\n};\n\n/**\n * Creates the Hono RPC middleware that routes incoming requests to registered server functions.\n * Wraps the generic createMiddleware with the RPC handler that reads the body, dispatches\n * to the matching function, and returns the JSON-serialized result.\n * @param initialOptions - Options including rpcPrefix for URL routing\n * @returns A Hono middleware function\n */\nexport const createRPCMiddleware: HonoMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n { rpcPrefix: defaultRPCOptions.rpcPrefix },\n initialOptions,\n ) as HonoMiddlewareOptions;\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 rpcPrefix = options.rpcPrefix;\n const prefix = rpcPrefix || getGlobalPrefix() || defaultPrefix;\n const prefixRegex = rpcPrefix\n ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`)\n : /* istanbul ignore next */ null;\n const prefixReplace = `/${prefix}/`;\n\n return createMiddleware({\n ...options,\n handler: async (c: Context, _next: Next) => {\n const { path: reqPath } = c.req;\n // const { rpcPrefix: prefix } = options;\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(reqPath)) {\n /* istanbul ignore next */\n return;\n }\n\n // Optional origin check: reject requests whose Origin header does not\n // match the configured origin. Requests without an Origin header\n // (curl, native clients) pass through unchecked.\n const origin = options.origin;\n const requestOrigin = c.req.header(\"origin\");\n if (origin && requestOrigin && requestOrigin !== origin) {\n return c.json({ error: REQUEST_FORBIDDEN }, 403);\n }\n\n const functionName = reqPath.replace(prefixReplace, \"\");\n const serverFunction = getFunctionsForPrefix(prefix).get(functionName);\n\n if (!serverFunction) {\n return c.json({ error: FUNCTION_NOT_FOUND }, 404);\n }\n\n try {\n const method = serverFunction.options?.method || \"POST\";\n if (c.req.method.toUpperCase() !== method) {\n return c.json({ error: METHOD_NOT_ALLOWED }, 405);\n }\n\n let args: JsonValue[] = [];\n if (method === \"GET\") {\n const raw = c.req.query(\"args\");\n if (raw) {\n const parsed: unknown = JSON.parse(raw);\n if (!Array.isArray(parsed)) {\n return c.json({ error: BAD_REQUEST }, 400);\n }\n args = parsed as JsonValue[];\n }\n } else {\n // Content-type enforcement: strict for json/text, lenient between forms.\n // Requests without a Content-Type header are exempt (curl/GET compat).\n // Checked BEFORE readBody so mismatched bodies are never buffered.\n if (\n hasContentTypeMismatch(\n serverFunction.options?.contentType ?? \"application/json\",\n c.req.header(\"content-type\"),\n )\n ) {\n return c.json({ error: UNSUPPORTED_MEDIA_TYPE }, 415);\n }\n const body = await readBody(c);\n args = Array.isArray(body.data)\n ? body.data as JsonValue[]\n : [body.data as JsonValue];\n }\n const requestEvent: RequestEvent = {\n request: c.req,\n response: c.res,\n nativeEvent: c,\n locals: {},\n functionName,\n // Hono's `c.redirect`/`c.json` return a `Response` (never write\n // directly), so the bound redirect/send only record the intent; the\n // middleware uses them after the dispatch to return the Response.\n redirect: (location, status = 303) => {\n requestEvent.redirected = { location, status };\n },\n send: (status, body, headers) => {\n requestEvent.sent = { status, body, headers };\n },\n };\n const fnResult = provideRequestContext(\n requestEvent,\n () => serverFunction.handler(...args),\n );\n const onAbort = () => fnResult.cancel(CLIENT_DISCONNECTED);\n // The runtime adapter may be absent in some Hono environments\n // (e.g. standalone serverless adapters), so guard the close hook.\n c.env.incoming?.on(\"close\", onAbort);\n const result = await fnResult.data;\n c.env.incoming?.off(\"close\", onAbort);\n\n if (requestEvent.redirected) {\n return c.redirect(\n requestEvent.redirected.location,\n requestEvent.redirected.status as RedirectStatusCode,\n ) as Response;\n }\n\n if (requestEvent.sent) {\n const { status, body, headers } = requestEvent.sent;\n return c.body(JSON.stringify(body), status as ContentfulStatusCode, {\n \"content-type\": \"application/json\",\n ...headers,\n });\n }\n\n return c.json({ data: result }, 200);\n } catch (err) {\n console.error(String(err));\n const isProduction = process.env.NODE_ENV === \"production\";\n return c.json(formatError(err, isProduction), 500);\n }\n },\n });\n};\n"],"mappings":"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"}
|
package/dist/index.d.mts
CHANGED
|
@@ -197,6 +197,11 @@ interface ServerFunctionOptions {
|
|
|
197
197
|
* @default "POST"
|
|
198
198
|
*/
|
|
199
199
|
method?: "GET" | "POST";
|
|
200
|
+
/**
|
|
201
|
+
* RPC endpoint prefix
|
|
202
|
+
* @default "__rpc"
|
|
203
|
+
*/
|
|
204
|
+
rpcPrefix?: string;
|
|
200
205
|
}
|
|
201
206
|
// primitives and their compositions
|
|
202
207
|
/**
|
|
@@ -279,12 +284,14 @@ interface RpcPluginOptionsInternal {
|
|
|
279
284
|
/**
|
|
280
285
|
* Partial Vite config used when scanning server files outside a running dev server.
|
|
281
286
|
*/
|
|
282
|
-
|
|
287
|
+
interface ScanConfig extends Pick<ResolvedConfig, "base"> {
|
|
283
288
|
root?: string;
|
|
284
289
|
server?: Partial<ResolvedConfig["server"]>;
|
|
285
290
|
serverFiles?: "exact" | "glob";
|
|
286
291
|
scanRoot?: string;
|
|
287
|
-
|
|
292
|
+
/** Default rpcPrefix to register scanned functions under when a function does not declare its own. Defaults to `__rpc` for backward compatibility. */
|
|
293
|
+
rpcPrefix?: string;
|
|
294
|
+
}
|
|
288
295
|
/**
|
|
289
296
|
* Entry in the server functions map: registered name, client handler,
|
|
290
297
|
* optional per-function options, and the original export name.
|
|
@@ -374,6 +381,19 @@ interface MiddlewareOptions<A extends RpcPluginOptions["adapter"] = "express"> {
|
|
|
374
381
|
* When unset (default), no origin validation is performed.
|
|
375
382
|
*/
|
|
376
383
|
origin?: string;
|
|
384
|
+
/**
|
|
385
|
+
* Server file matching mode. Use `"exact"` for `server.ts|js|mjs|mts`
|
|
386
|
+
* names, or `"glob"` to match `**\/*.server.{ts,js,mjs,mts}` inside the
|
|
387
|
+
* scan root. Only used for the lazy production scan when the middleware
|
|
388
|
+
* populates its prefix map on first request.
|
|
389
|
+
* @default "exact"
|
|
390
|
+
*/
|
|
391
|
+
serverFiles?: "exact" | "glob";
|
|
392
|
+
/**
|
|
393
|
+
* Root directory for scanning server files. Defaults to `<root>/src/api`.
|
|
394
|
+
* Only used for the lazy production scan.
|
|
395
|
+
*/
|
|
396
|
+
scanRoot?: string;
|
|
377
397
|
/**
|
|
378
398
|
* Async handler for request processing.
|
|
379
399
|
* Core middleware function that processes incoming requests.
|
|
@@ -393,13 +413,36 @@ interface MiddlewareOptions<A extends RpcPluginOptions["adapter"] = "express"> {
|
|
|
393
413
|
*/
|
|
394
414
|
handler?: FrameworkHooks[A]["handler"];
|
|
395
415
|
}
|
|
416
|
+
/**
|
|
417
|
+
* Options for a manual client stub created via `getClientStub`.
|
|
418
|
+
* Mirrors `ServerFunctionOptions` but client-only.
|
|
419
|
+
*/
|
|
420
|
+
interface StubOptions {
|
|
421
|
+
/**
|
|
422
|
+
* HTTP method for the stub.
|
|
423
|
+
* @default "POST"
|
|
424
|
+
*/
|
|
425
|
+
method: "GET" | "POST";
|
|
426
|
+
/**
|
|
427
|
+
* Fetch credentials policy.
|
|
428
|
+
* @default "same-origin"
|
|
429
|
+
*/
|
|
430
|
+
credentials: Credentials;
|
|
431
|
+
/**
|
|
432
|
+
* Content type for the request body. Only `"application/json"` is used for
|
|
433
|
+
* most stubs; other values are for `text/plain`, `application/x-www-form-urlencoded`,
|
|
434
|
+
* and `multipart/form-data` handlers.
|
|
435
|
+
* @default "application/json"
|
|
436
|
+
*/
|
|
437
|
+
contentType: ContentType;
|
|
438
|
+
}
|
|
396
439
|
/**
|
|
397
440
|
* Return shape of `innerModule`: a promise of the response data plus
|
|
398
441
|
* a `cancel` function to abort the underlying fetch request.
|
|
399
442
|
*/
|
|
400
|
-
type InnerModReturn = {
|
|
443
|
+
type InnerModReturn<T extends JsonValue> = {
|
|
401
444
|
/** Promise resolving to the server response data */
|
|
402
|
-
data: Promise<
|
|
445
|
+
data: Promise<T | void>;
|
|
403
446
|
/** Aborts the in-flight request with the given reason */
|
|
404
447
|
cancel: (reason: string) => void;
|
|
405
448
|
};
|
|
@@ -429,5 +472,5 @@ declare const loadRPCConfig: (f?: string) => Promise<RpcPluginOptions>;
|
|
|
429
472
|
*/
|
|
430
473
|
declare function rpcPlugin(devOptions?: Partial<RpcPluginOptions>): Plugin;
|
|
431
474
|
//#endregion
|
|
432
|
-
export { type BodyResult, type ClientFunction, type ClientFunctionWithOptions, type ContentType, type Credentials, type FrameworkHooks, type FrameworkMiddlewareFn, type InnerModReturn, type JsonArray, type JsonObject, type JsonPrimitive, type JsonValue, type MiddlewareOptions, type RpcPluginOptions, type RpcPluginOptionsInternal, type ScanConfig, type ServerFnArgs, type ServerFnEntry, type ServerFunction, type ServerFunctionInit, type ServerFunctionOptions, type SupportableContentType, rpcPlugin as default, defineConfig, loadRPCConfig };
|
|
475
|
+
export { type BodyResult, type ClientFunction, type ClientFunctionWithOptions, type ContentType, type Credentials, type FrameworkHooks, type FrameworkMiddlewareFn, type InnerModReturn, type JsonArray, type JsonObject, type JsonPrimitive, type JsonValue, type MiddlewareOptions, type RpcPluginOptions, type RpcPluginOptionsInternal, type ScanConfig, type ServerFnArgs, type ServerFnEntry, type ServerFunction, type ServerFunctionInit, type ServerFunctionOptions, type StubOptions, type SupportableContentType, rpcPlugin as default, defineConfig, loadRPCConfig };
|
|
433
476
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.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/h3/types.d.ts","../src/types.d.ts","../src/index.ts"],"mappings":";;;;;;;;;;;;;;;;KAgBY,2BAA2B;;;;;KAM3B,uBACV,UAAU,2CAEV,iBAAiB,QAAQ,8BACtB;;;;UAKY;;;;;;;EAOf,UACE,KAAK,kBAAkB,SACvB,KAAK,iBAAiB,YACtB,MAAM,QAAQ,eAAe,iBAC1B;;;;;;;UCzBU;;EAEf,SAAS;;;;;;KAOC,oBAAoB,UAAU,wCACxC,iBAAiB,QAAQ,oBAAkB,QACxC;;;;;;KCWO,2BAA2B;;;;;KAM3B,uBACV,UAAU,2CAEV,iBAAiB,QAAQ,8BACtB;;;;UAKY;;;;;;;EAOf,UACE,KAAK,gBACL,KAAK,cACL,MAAM,4BACH;;;;;;;KCtDK,uBAAuB;;;;UAalB;;;;;;EAMf,UAAU,KAAK,SAAS,MAAM,SAAS;;;;;;KAO7B,mBAAmB,UAAU,uCACvC,iBAAiB,QAAQ,0BACtB;;;;;;KChCO,sBAAsB;;;;UAKjB;;;;;;EAMf,SAAS;;;;;;KAOC,kBAAkB,UAAU,sCACtC,iBAAiB,QAAQ,yBACtB;;;;;;;UCJY;;EAEf,SAAS;;EAET,MAAM;;EAEN,SAAS;;EAET,KAAK;;EAEL,IAAI;;;;;UAMW;;EAEf,SAAS;;EAET,MAAM;;EAEN,SAAS;;EAET,KAAK;;EAEL,IAAI;;;;;;KAOM;;;;KAUA;;;;KASA;;;;KAKA;EACN;EAAiC,MAAM;;EACvC;EAA2B;;EAE7B;EACA,MAAM;;EAEJ;EAAoC,MAAM;;;;;;UAM/B;;;;;EAKf,aAAa;;;;;EAKb,cAAc;;;;;;;EAOd;;;;;;KAOU;;;;KAIA;GAAgB,cAAc,YAAY;;;;;KAI1C,aAAa,WAAW;;;;KAIxB,YAAY,gBAAgB,YAAY;;;;;;;;;;;;;;;;;;;KAuBxC,mBAAmB;;;;;KAMnB,eACV,cAAc,YAAY,WAC1B,gBAAgB,YAAY,cACzB,QAAQ,gBAAgB,MAAM,UAAU,QAAQ;;;;;KAMzC,mBACV,cAAc,WAAW,YAAY,WACrC,gBAAgB,YAAY,cACzB,QAAQ,gBAAgB,MAAM,UAAU,QAAQ;;;;;;KAOzC,eACV,cAAc,YAAY,WAC1B,gBAAgB,YAAY,iBACtB,MAAM;;EAEZ,MAAM,QAAQ;;EAEd,SAAS;;;;;;KAOC,4BAA4B;;EAEtC;;EAEA,UAAU;;;;;UAMK;;EAEf;;EAEA;;;;;
|
|
1
|
+
{"version":3,"file":"index.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/h3/types.d.ts","../src/types.d.ts","../src/index.ts"],"mappings":";;;;;;;;;;;;;;;;KAgBY,2BAA2B;;;;;KAM3B,uBACV,UAAU,2CAEV,iBAAiB,QAAQ,8BACtB;;;;UAKY;;;;;;;EAOf,UACE,KAAK,kBAAkB,SACvB,KAAK,iBAAiB,YACtB,MAAM,QAAQ,eAAe,iBAC1B;;;;;;;UCzBU;;EAEf,SAAS;;;;;;KAOC,oBAAoB,UAAU,wCACxC,iBAAiB,QAAQ,oBAAkB,QACxC;;;;;;KCWO,2BAA2B;;;;;KAM3B,uBACV,UAAU,2CAEV,iBAAiB,QAAQ,8BACtB;;;;UAKY;;;;;;;EAOf,UACE,KAAK,gBACL,KAAK,cACL,MAAM,4BACH;;;;;;;KCtDK,uBAAuB;;;;UAalB;;;;;;EAMf,UAAU,KAAK,SAAS,MAAM,SAAS;;;;;;KAO7B,mBAAmB,UAAU,uCACvC,iBAAiB,QAAQ,0BACtB;;;;;;KChCO,sBAAsB;;;;UAKjB;;;;;;EAMf,SAAS;;;;;;KAOC,kBAAkB,UAAU,sCACtC,iBAAiB,QAAQ,yBACtB;;;;;;;UCJY;;EAEf,SAAS;;EAET,MAAM;;EAEN,SAAS;;EAET,KAAK;;EAEL,IAAI;;;;;UAMW;;EAEf,SAAS;;EAET,MAAM;;EAEN,SAAS;;EAET,KAAK;;EAEL,IAAI;;;;;;KAOM;;;;KAUA;;;;KASA;;;;KAKA;EACN;EAAiC,MAAM;;EACvC;EAA2B;;EAE7B;EACA,MAAM;;EAEJ;EAAoC,MAAM;;;;;;UAM/B;;;;;EAKf,aAAa;;;;;EAKb,cAAc;;;;;;;EAOd;;;;;EAKA;;;;;;KAOU;;;;KAIA;GAAgB,cAAc,YAAY;;;;;KAI1C,aAAa,WAAW;;;;KAIxB,YAAY,gBAAgB,YAAY;;;;;;;;;;;;;;;;;;;KAuBxC,mBAAmB;;;;;KAMnB,eACV,cAAc,YAAY,WAC1B,gBAAgB,YAAY,cACzB,QAAQ,gBAAgB,MAAM,UAAU,QAAQ;;;;;KAMzC,mBACV,cAAc,WAAW,YAAY,WACrC,gBAAgB,YAAY,cACzB,QAAQ,gBAAgB,MAAM,UAAU,QAAQ;;;;;;KAOzC,eACV,cAAc,YAAY,WAC1B,gBAAgB,YAAY,iBACtB,MAAM;;EAEZ,MAAM,QAAQ;;EAEd,SAAS;;;;;;KAOC,4BAA4B;;EAEtC;;EAEA,UAAU;;;;;UAMK;;EAEf;;EAEA;;;;;UAMe,mBAAmB,KAAK;EACvC;EACA,SAAS,QAAQ;EACjB;EACA;;EAEA;;;;;;UAOe;;EAEf;;EAEA,SAAS;;EAET,UAAU;;EAEV;;;;;;;;UASe;;;;;;;;;;;EAWf;;;;;;;EAQA;;;;;;;EAQA;;;;;;;EAQA;;UAGe,kBACf,UAAU;;;;EAKV;;;;;;;;;;;;EAaA,gBAAgB;;;;;;;;;;EAWhB;;;;;;;;EASA;;;;;;;;EASA;;;;;EAMA;;;;;;;;;;;;;;;;;;EAmBA,UAAU,eAAe;;;;;;UAOV;;;;;EAKf;;;;;EAKA,aAAa;;;;;;;EAOb,aAAa;;;;;;KAOH,eAAe,UAAU;;EAEnC,MAAM,QAAQ;;EAEd,SAAS;;;;;;;;;;cCxVL,eAAe,GAAG,QAAQ,sBAAsB;;;;;;;;cAehD,gBAAgB,eAAe,QAAQ;;;;;;;;iBAkGpC,UACP,aAAY,QAAQ,oBACnB"}
|
package/dist/index.mjs
CHANGED
|
@@ -3,10 +3,12 @@ import { join, resolve } from "node:path";
|
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
5
|
import { readdir } from "node:fs/promises";
|
|
6
|
+
import { setGlobalPrefix } from "@thednp/rpc/server";
|
|
6
7
|
import { createRPCMiddleware } from "@thednp/rpc/express";
|
|
7
8
|
//#region src/options.ts
|
|
9
|
+
const defaultPrefix = "__rpc";
|
|
8
10
|
const defaultRPCOptions = {
|
|
9
|
-
rpcPrefix:
|
|
11
|
+
rpcPrefix: defaultPrefix,
|
|
10
12
|
adapter: "express",
|
|
11
13
|
serverFiles: "exact",
|
|
12
14
|
scanRoot: void 0
|
|
@@ -18,7 +20,7 @@ const ERROR_LOADING_FILE = "Error loading file:";
|
|
|
18
20
|
/** Error message when a value fails the safe-identifier validation. @param label - What kind of value was being validated. @param name - The rejected value */
|
|
19
21
|
const INVALID_IDENTIFIER = (label, name) => `Invalid ${label}: "${name}" must match /^[A-Za-z_$][A-Za-z0-9_$]*$/`;
|
|
20
22
|
/** Error message when a value fails the safe-path-segment validation. @param label - What kind of value was being validated. @param segment - The rejected value */
|
|
21
|
-
const INVALID_PATH_SEGMENT = (label, segment) => `Invalid ${label}: "${segment}" must match /^[A-Za-z0-9_
|
|
23
|
+
const INVALID_PATH_SEGMENT = (label, segment) => `Invalid ${label}: "${segment}" must match /^[A-Za-z0-9_$@:][A-Za-z0-9_$@:/-]*$/`;
|
|
22
24
|
/** Warning message when a specified RPC config file cannot be resolved on disk. @param configFile - The requested config filename. @param configFilePath - The resolved absolute path */
|
|
23
25
|
const CONFIG_FILE_NOT_FOUND = (configFile, configFilePath) => ` ⚠︎ The specified RPC config file ${configFile} cannot be found at ${configFilePath}, loading the defaults..`;
|
|
24
26
|
const NO_CONFIG_FOUND = ` ⚡︎ No RPC config found, loading the defaults..`;
|
|
@@ -27,11 +29,53 @@ const FAILED_LOAD_CONFIG = ` ⚠︎ Failed to load RPC config:`;
|
|
|
27
29
|
const DUPLICATE_FUNCTION_NAME = (name) => `Duplicate server function "${name}" detected. Each server function must have a unique name. Remove or rename the duplicate.`;
|
|
28
30
|
//#endregion
|
|
29
31
|
//#region src/functionsMap.ts
|
|
30
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Global symbol under which the shared `serverFunctionsByPrefix` map is stored
|
|
34
|
+
* on `globalThis`. Keeping it on a `Symbol.for` key makes it instance-stable
|
|
35
|
+
* across the bundled entry copies (`index.mjs`, `server.mjs`, `express.mjs`,
|
|
36
|
+
* ...) and dev-server hot reloads, exactly like the request-context storage in
|
|
37
|
+
* `context.ts`. Without this, `scanForServerFiles` (bundled into the plugin)
|
|
38
|
+
* would populate a map copy the adapter middleware could not read.
|
|
39
|
+
*/
|
|
40
|
+
const functionsMapSymbol = Symbol.for("thednp.rpc.functionsMap");
|
|
41
|
+
/**
|
|
42
|
+
* Map of rpcPrefix -> Map of function names -> ServerFnEntry
|
|
43
|
+
* Enables multiple RPC instances with different prefixes to coexist
|
|
44
|
+
* without name collisions.
|
|
45
|
+
*/
|
|
46
|
+
const serverFunctionsByPrefix = globalThis[functionsMapSymbol] ??= /* @__PURE__ */ new Map();
|
|
47
|
+
/**
|
|
48
|
+
* Gets or creates the function map for a specific prefix.
|
|
49
|
+
* @param prefix - The RPC prefix (e.g., "__rpc", "v1:rpc", "admin:rpc")
|
|
50
|
+
* @returns Map of function names to ServerFnEntry for that prefix
|
|
51
|
+
*/
|
|
52
|
+
const getFunctionsForPrefix = (prefix) => {
|
|
53
|
+
if (!serverFunctionsByPrefix.has(prefix)) serverFunctionsByPrefix.set(prefix, /* @__PURE__ */ new Map());
|
|
54
|
+
return serverFunctionsByPrefix.get(prefix);
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Backward compatibility: default map for the default prefix.
|
|
58
|
+
* Legacy code can still use serverFunctionsMap.set(name, entry).
|
|
59
|
+
*/
|
|
60
|
+
const serverFunctionsMap = {
|
|
61
|
+
get: (key) => getFunctionsForPrefix(defaultPrefix).get(key),
|
|
62
|
+
set: (key, value) => getFunctionsForPrefix(defaultPrefix).set(key, value),
|
|
63
|
+
has: (key) => getFunctionsForPrefix(defaultPrefix).has(key),
|
|
64
|
+
delete: (key) => getFunctionsForPrefix(defaultPrefix).delete(key),
|
|
65
|
+
clear: () => getFunctionsForPrefix(defaultPrefix).clear(),
|
|
66
|
+
get size() {
|
|
67
|
+
return getFunctionsForPrefix(defaultPrefix).size;
|
|
68
|
+
},
|
|
69
|
+
entries: () => getFunctionsForPrefix(defaultPrefix).entries(),
|
|
70
|
+
keys: () => getFunctionsForPrefix(defaultPrefix).keys(),
|
|
71
|
+
values: () => getFunctionsForPrefix(defaultPrefix).values(),
|
|
72
|
+
forEach: (callback) => getFunctionsForPrefix(defaultPrefix).forEach(callback),
|
|
73
|
+
[Symbol.iterator]: () => getFunctionsForPrefix(defaultPrefix)[Symbol.iterator]()
|
|
74
|
+
};
|
|
31
75
|
//#endregion
|
|
32
76
|
//#region src/validate.ts
|
|
33
77
|
const SAFE_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
34
|
-
const SAFE_PATH_SEGMENT = /^[A-Za-z0-9_
|
|
78
|
+
const SAFE_PATH_SEGMENT = /^[A-Za-z0-9_$@:][A-Za-z0-9_$@:/-]*$/;
|
|
35
79
|
const CREDENTIALS_VALUES = [
|
|
36
80
|
"same-origin",
|
|
37
81
|
"include",
|
|
@@ -51,7 +95,8 @@ function validateIdentifier(name, label) {
|
|
|
51
95
|
}
|
|
52
96
|
/**
|
|
53
97
|
* Validates that a string is a safe path segment for RPC routing.
|
|
54
|
-
* Allows alphanumeric characters, underscores, dollar signs, at signs,
|
|
98
|
+
* Allows alphanumeric characters, underscores, dollar signs, at signs,
|
|
99
|
+
* colons, hyphens, and forward slashes.
|
|
55
100
|
* @param segment - The string to validate
|
|
56
101
|
* @param label - Human-readable label for error messages (e.g. "rpcPrefix")
|
|
57
102
|
* @returns The validated segment if it passes
|
|
@@ -101,53 +146,28 @@ const getModule = (fnName, fnEntry, options) => {
|
|
|
101
146
|
const safePrefix = validatePathSegment(options.rpcPrefix, "rpcPrefix");
|
|
102
147
|
const credentials = validateCredentials(options.credentials);
|
|
103
148
|
const method = validateMethod(options.method);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
headers = `{ 'Content-Type': 'text/plain' }`;
|
|
110
|
-
break;
|
|
111
|
-
case "application/x-www-form-urlencoded":
|
|
112
|
-
body = `new URLSearchParams(args[0]).toString()`;
|
|
113
|
-
headers = `{ 'Content-Type': 'application/x-www-form-urlencoded' }`;
|
|
114
|
-
break;
|
|
115
|
-
case "multipart/form-data":
|
|
116
|
-
body = `args[0]`;
|
|
117
|
-
headers = `{}`;
|
|
118
|
-
break;
|
|
119
|
-
default:
|
|
120
|
-
body = `JSON.stringify(args)`;
|
|
121
|
-
headers = `{ 'Content-Type': 'application/json' }`;
|
|
122
|
-
}
|
|
123
|
-
if (method === "GET") {
|
|
124
|
-
body = `JSON.stringify(args)`;
|
|
125
|
-
headers = `{}`;
|
|
126
|
-
}
|
|
149
|
+
const contentType = options.contentType ?? "application/json";
|
|
150
|
+
const opts = [];
|
|
151
|
+
if (method !== "POST") opts.push(`method: "${method}"`);
|
|
152
|
+
if (credentials !== "same-origin") opts.push(`credentials: "${credentials}"`);
|
|
153
|
+
if (contentType !== "application/json") opts.push(`contentType: "${contentType}"`);
|
|
127
154
|
return `
|
|
128
|
-
export const ${safeFnEntry} = (
|
|
129
|
-
const body = ${body};
|
|
130
|
-
const headers = ${headers};
|
|
131
|
-
const prefix = "${safePrefix}";
|
|
132
|
-
const name = "${safeFnName}";
|
|
133
|
-
const credentials = "${credentials}";
|
|
134
|
-
const method = "${method}";
|
|
135
|
-
return innerModule(body, headers, credentials, prefix, name, method);
|
|
136
|
-
}`.trim();
|
|
155
|
+
export const ${safeFnEntry} = getClientStub("${safePrefix}", "${safeFnName}"${opts.length ? `, { ${opts.join(", ")} }` : ""});`.trim();
|
|
137
156
|
};
|
|
138
157
|
/**
|
|
139
158
|
* Generates the complete client-side module bundle by iterating all registered server functions
|
|
140
|
-
* and producing fetch-based stubs for each. The result is transformed by Vite
|
|
141
|
-
* the dev server or production build.
|
|
159
|
+
* for a specific prefix and producing fetch-based stubs for each. The result is transformed by Vite
|
|
160
|
+
* (or Oxc) during the dev server or production build.
|
|
142
161
|
* @param initialOptions - Plugin options containing rpcPrefix and optional adapter
|
|
143
162
|
* @returns A string of JavaScript code with all client RPC modules and their import dependencies
|
|
144
163
|
*/
|
|
145
164
|
const getClientModules = (initialOptions) => {
|
|
146
165
|
validatePathSegment(initialOptions.rpcPrefix, "rpcPrefix");
|
|
166
|
+
const prefixMap = getFunctionsForPrefix(initialOptions.rpcPrefix);
|
|
147
167
|
return `
|
|
148
168
|
|
|
149
|
-
import {
|
|
150
|
-
${Array.from(
|
|
169
|
+
import { getClientStub } from "@thednp/rpc/helpers";
|
|
170
|
+
${Array.from(prefixMap.entries()).filter(([, entry]) => entry.exportName).map(([registeredName, entry]) => getModule(registeredName, entry.exportName, {
|
|
151
171
|
...initialOptions,
|
|
152
172
|
...entry.options || {}
|
|
153
173
|
})).join("\n")}`.trim();
|
|
@@ -191,7 +211,7 @@ const EXACT_NAMES = [
|
|
|
191
211
|
];
|
|
192
212
|
/**
|
|
193
213
|
* Scans `src/api/` (or an explicit `scanRoot`) for server function files
|
|
194
|
-
* and populates the
|
|
214
|
+
* and populates the server functions map (scoped by rpcPrefix) with their exported functions.
|
|
195
215
|
* Uses Vite's SSR module loading to resolve and execute each file.
|
|
196
216
|
*
|
|
197
217
|
* Supports two matching modes via `config.serverFiles`:
|
|
@@ -249,16 +269,21 @@ const scanForServerFiles = async (initialCfg, devServer) => {
|
|
|
249
269
|
}
|
|
250
270
|
for (const [exportName, exportValue] of moduleEntries) {
|
|
251
271
|
const registeredName = exportValue.name;
|
|
252
|
-
|
|
272
|
+
const prefix = exportValue.options?.rpcPrefix || config.rpcPrefix || "__rpc";
|
|
273
|
+
const seenKey = `${prefix}:${registeredName}`;
|
|
274
|
+
if (seenNames.has(seenKey)) {
|
|
253
275
|
if (process.env.NODE_ENV !== "production") throw new Error(DUPLICATE_FUNCTION_NAME(registeredName));
|
|
254
276
|
console.warn(DUPLICATE_FUNCTION_NAME(registeredName));
|
|
255
277
|
continue;
|
|
256
278
|
}
|
|
257
|
-
seenNames.add(
|
|
258
|
-
|
|
279
|
+
seenNames.add(seenKey);
|
|
280
|
+
const prefixMap = getFunctionsForPrefix(prefix);
|
|
281
|
+
const existing = prefixMap.get(registeredName);
|
|
282
|
+
if (existing) existing.exportName = exportName;
|
|
283
|
+
else prefixMap.set(registeredName, {
|
|
259
284
|
name: registeredName,
|
|
260
285
|
handler: exportValue,
|
|
261
|
-
options: exportValue
|
|
286
|
+
options: exportValue.options,
|
|
262
287
|
exportName
|
|
263
288
|
});
|
|
264
289
|
}
|
|
@@ -323,6 +348,7 @@ const loadRPCConfig = async (configFile) => {
|
|
|
323
348
|
if (!existsSync(configFilePath)) {
|
|
324
349
|
console.warn(CONFIG_FILE_NOT_FOUND(configFile, configFilePath));
|
|
325
350
|
RPCConfig = defaultRPCOptions;
|
|
351
|
+
setGlobalPrefix(defaultRPCOptions.rpcPrefix);
|
|
326
352
|
return defaultRPCOptions;
|
|
327
353
|
}
|
|
328
354
|
const result = await loadConfigFile(env, configFile);
|
|
@@ -331,11 +357,15 @@ const loadRPCConfig = async (configFile) => {
|
|
|
331
357
|
...defaultRPCOptions,
|
|
332
358
|
configFile: configFilePath
|
|
333
359
|
}, result.config);
|
|
360
|
+
setGlobalPrefix(RPCConfig.rpcPrefix);
|
|
334
361
|
return RPCConfig;
|
|
335
362
|
}
|
|
336
363
|
RPCConfig = defaultRPCOptions;
|
|
337
364
|
}
|
|
338
|
-
if (RPCConfig !== void 0)
|
|
365
|
+
if (RPCConfig !== void 0) {
|
|
366
|
+
setGlobalPrefix(RPCConfig.rpcPrefix);
|
|
367
|
+
return RPCConfig;
|
|
368
|
+
}
|
|
339
369
|
for (const file of defaultConfigFiles) {
|
|
340
370
|
const configFilePath = resolve(env.root, file);
|
|
341
371
|
if (!existsSync(configFilePath)) continue;
|
|
@@ -354,6 +384,7 @@ const loadRPCConfig = async (configFile) => {
|
|
|
354
384
|
RPCConfig = defaultRPCOptions;
|
|
355
385
|
console.warn(FAILED_LOAD_CONFIG, error);
|
|
356
386
|
}
|
|
387
|
+
setGlobalPrefix(RPCConfig.rpcPrefix);
|
|
357
388
|
return RPCConfig;
|
|
358
389
|
};
|
|
359
390
|
/**
|
|
@@ -383,7 +414,8 @@ function rpcPlugin(devOptions = {}) {
|
|
|
383
414
|
const scanCfg = {
|
|
384
415
|
...config,
|
|
385
416
|
serverFiles: options.serverFiles,
|
|
386
|
-
scanRoot: options.scanRoot
|
|
417
|
+
scanRoot: options.scanRoot,
|
|
418
|
+
rpcPrefix: options.rpcPrefix
|
|
387
419
|
};
|
|
388
420
|
await scanForServerFiles(scanCfg, viteServer);
|
|
389
421
|
}
|
|
@@ -396,7 +428,8 @@ function rpcPlugin(devOptions = {}) {
|
|
|
396
428
|
const scanCfg = {
|
|
397
429
|
...config,
|
|
398
430
|
serverFiles: options.serverFiles,
|
|
399
|
-
scanRoot: options.scanRoot
|
|
431
|
+
scanRoot: options.scanRoot,
|
|
432
|
+
rpcPrefix: options.rpcPrefix
|
|
400
433
|
};
|
|
401
434
|
await scanForServerFiles(scanCfg);
|
|
402
435
|
}
|
|
@@ -408,7 +441,8 @@ function rpcPlugin(devOptions = {}) {
|
|
|
408
441
|
const scanCfg = {
|
|
409
442
|
...config,
|
|
410
443
|
serverFiles: options.serverFiles,
|
|
411
|
-
scanRoot: options.scanRoot
|
|
444
|
+
scanRoot: options.scanRoot,
|
|
445
|
+
rpcPrefix: options.rpcPrefix
|
|
412
446
|
};
|
|
413
447
|
await scanForServerFiles(scanCfg);
|
|
414
448
|
}
|