@thednp/rpc 0.2.1 → 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 +33 -19
- package/CHANGELOG.md +38 -0
- package/README.md +22 -9
- package/dist/express/express.d.mts +4 -2
- package/dist/express/express.d.mts.map +1 -1
- package/dist/express/express.mjs +41 -8
- package/dist/express/express.mjs.map +1 -1
- package/dist/fastify/fastify.d.mts.map +1 -1
- package/dist/fastify/fastify.mjs +37 -6
- 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 +37 -6
- package/dist/fastify/plugin/fastify/plugin.mjs.map +1 -1
- package/dist/h3/h3.d.mts.map +1 -1
- package/dist/h3/h3.mjs +47 -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 +37 -6
- 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 +47 -16
- package/dist/koa/koa.mjs.map +1 -1
- package/dist/server/server.d.mts +219 -26
- package/dist/server/server.d.mts.map +1 -1
- package/dist/server/server.mjs +101 -68
- package/dist/server/server.mjs.map +1 -1
- package/package.json +20 -18
package/dist/koa/koa.mjs
CHANGED
|
@@ -1,15 +1,4 @@
|
|
|
1
|
-
import { escapeRegExp, formatError, hasContentTypeMismatch, provideRequestContext, safeURL, scanForServerFiles
|
|
2
|
-
//#region src/constants.ts
|
|
3
|
-
const FUNCTION_NOT_FOUND = "Function not found";
|
|
4
|
-
const METHOD_NOT_ALLOWED = "Method Not Allowed";
|
|
5
|
-
const REQUEST_FORBIDDEN = "Forbidden";
|
|
6
|
-
const UNSUPPORTED_MEDIA_TYPE = "Unsupported Media Type";
|
|
7
|
-
const BAD_REQUEST = "Bad Request";
|
|
8
|
-
const CLIENT_DISCONNECTED = "client disconnected";
|
|
9
|
-
/** Returns a warning when a middleware name is reused, preventing registration conflicts. @param name - The duplicate middleware name */
|
|
10
|
-
const MIDDLEWARE_NAME_USED = (name) => `The middleware name "${name}" is already used.`;
|
|
11
|
-
//#endregion
|
|
12
|
-
//#region src/options.ts
|
|
1
|
+
import { escapeRegExp, formatError, getGlobalPrefix, hasContentTypeMismatch, provideRequestContext, safeURL, scanForServerFiles } from "@thednp/rpc/server";
|
|
13
2
|
const defaultRPCOptions = {
|
|
14
3
|
rpcPrefix: "__rpc",
|
|
15
4
|
adapter: "express",
|
|
@@ -22,6 +11,42 @@ const defaultMiddlewareOptions = {
|
|
|
22
11
|
origin: void 0
|
|
23
12
|
};
|
|
24
13
|
//#endregion
|
|
14
|
+
//#region src/functionsMap.ts
|
|
15
|
+
/**
|
|
16
|
+
* Global symbol under which the shared `serverFunctionsByPrefix` map is stored
|
|
17
|
+
* on `globalThis`. Keeping it on a `Symbol.for` key makes it instance-stable
|
|
18
|
+
* across the bundled entry copies (`index.mjs`, `server.mjs`, `express.mjs`,
|
|
19
|
+
* ...) and dev-server hot reloads, exactly like the request-context storage in
|
|
20
|
+
* `context.ts`. Without this, `scanForServerFiles` (bundled into the plugin)
|
|
21
|
+
* would populate a map copy the adapter middleware could not read.
|
|
22
|
+
*/
|
|
23
|
+
const functionsMapSymbol = Symbol.for("thednp.rpc.functionsMap");
|
|
24
|
+
/**
|
|
25
|
+
* Map of rpcPrefix -> Map of function names -> ServerFnEntry
|
|
26
|
+
* Enables multiple RPC instances with different prefixes to coexist
|
|
27
|
+
* without name collisions.
|
|
28
|
+
*/
|
|
29
|
+
const serverFunctionsByPrefix = globalThis[functionsMapSymbol] ??= /* @__PURE__ */ new Map();
|
|
30
|
+
/**
|
|
31
|
+
* Gets or creates the function map for a specific prefix.
|
|
32
|
+
* @param prefix - The RPC prefix (e.g., "__rpc", "v1:rpc", "admin:rpc")
|
|
33
|
+
* @returns Map of function names to ServerFnEntry for that prefix
|
|
34
|
+
*/
|
|
35
|
+
const getFunctionsForPrefix = (prefix) => {
|
|
36
|
+
if (!serverFunctionsByPrefix.has(prefix)) serverFunctionsByPrefix.set(prefix, /* @__PURE__ */ new Map());
|
|
37
|
+
return serverFunctionsByPrefix.get(prefix);
|
|
38
|
+
};
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/constants.ts
|
|
41
|
+
const FUNCTION_NOT_FOUND = "Function not found";
|
|
42
|
+
const METHOD_NOT_ALLOWED = "Method Not Allowed";
|
|
43
|
+
const REQUEST_FORBIDDEN = "Forbidden";
|
|
44
|
+
const UNSUPPORTED_MEDIA_TYPE = "Unsupported Media Type";
|
|
45
|
+
const BAD_REQUEST = "Bad Request";
|
|
46
|
+
const CLIENT_DISCONNECTED = "client disconnected";
|
|
47
|
+
/** Returns a warning when a middleware name is reused, preventing registration conflicts. @param name - The duplicate middleware name */
|
|
48
|
+
const MIDDLEWARE_NAME_USED = (name) => `The middleware name "${name}" is already used.`;
|
|
49
|
+
//#endregion
|
|
25
50
|
//#region src/koa/helpers.ts
|
|
26
51
|
/**
|
|
27
52
|
* Convenience function to load RPC config and attach the RPC middleware to a Koa app.
|
|
@@ -143,7 +168,7 @@ const middlewareStack = /* @__PURE__ */ new Set();
|
|
|
143
168
|
const createMiddleware = (initialOptions = {}) => {
|
|
144
169
|
const options = Object.assign({}, defaultMiddlewareOptions, initialOptions);
|
|
145
170
|
const middlewareName = options.name;
|
|
146
|
-
|
|
171
|
+
let rpcPrefix = options.rpcPrefix;
|
|
147
172
|
const path = options.path;
|
|
148
173
|
const handler = options.handler;
|
|
149
174
|
let name = middlewareName;
|
|
@@ -157,10 +182,15 @@ const createMiddleware = (initialOptions = {}) => {
|
|
|
157
182
|
const pathMatcher = path ? typeof path === "string" ? new RegExp(path) : path : null;
|
|
158
183
|
const middlewareHandler = async (ctx, next) => {
|
|
159
184
|
const url = safeURL(ctx.url).pathname;
|
|
160
|
-
if (serverFunctionsMap.size === 0) await scanForServerFiles();
|
|
161
185
|
if (!handler) return next();
|
|
162
186
|
if (pathMatcher && !pathMatcher.test(url)) return next();
|
|
163
187
|
if (prefixRegex && !prefixRegex.test(url)) return next();
|
|
188
|
+
rpcPrefix = rpcPrefix ?? "__rpc";
|
|
189
|
+
if (getFunctionsForPrefix(rpcPrefix).size === 0) await scanForServerFiles({
|
|
190
|
+
rpcPrefix,
|
|
191
|
+
serverFiles: options.serverFiles,
|
|
192
|
+
scanRoot: options.scanRoot
|
|
193
|
+
});
|
|
164
194
|
await handler(ctx, next);
|
|
165
195
|
};
|
|
166
196
|
Object.defineProperty(middlewareHandler, "name", { value: name });
|
|
@@ -176,8 +206,9 @@ const createMiddleware = (initialOptions = {}) => {
|
|
|
176
206
|
const createRPCMiddleware = (initialOptions = {}) => {
|
|
177
207
|
const options = Object.assign({}, defaultMiddlewareOptions, { rpcPrefix: defaultRPCOptions.rpcPrefix }, initialOptions);
|
|
178
208
|
const rpcPrefix = options.rpcPrefix;
|
|
209
|
+
const prefix = rpcPrefix || getGlobalPrefix() || "__rpc";
|
|
179
210
|
const prefixRegex = rpcPrefix ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`) : null;
|
|
180
|
-
const prefixReplace = `/${
|
|
211
|
+
const prefixReplace = `/${prefix}/`;
|
|
181
212
|
return createMiddleware({
|
|
182
213
|
...options,
|
|
183
214
|
handler: async (ctx, _next) => {
|
|
@@ -192,7 +223,7 @@ const createRPCMiddleware = (initialOptions = {}) => {
|
|
|
192
223
|
return;
|
|
193
224
|
}
|
|
194
225
|
const functionName = url.replace(prefixReplace, "");
|
|
195
|
-
const serverFunction =
|
|
226
|
+
const serverFunction = getFunctionsForPrefix(prefix).get(functionName);
|
|
196
227
|
if (!serverFunction) {
|
|
197
228
|
ctx.status = 404;
|
|
198
229
|
ctx.body = { error: FUNCTION_NOT_FOUND };
|
package/dist/koa/koa.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"koa.mjs","names":[],"sources":["../../src/constants.ts","../../src/options.ts","../../src/koa/helpers.ts","../../src/koa/createMiddleware.ts"],"sourcesContent":["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","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","// src/koa/helpers.ts\nimport type { Buffer } from \"node:buffer\";\nimport type { ViteDevServer } from \"vite\";\nimport { createRPCMiddleware } from \"./createMiddleware.ts\";\nimport type { BodyResult } from \"@thednp/rpc\";\nimport type { Koa } from \"./index.ts\";\nimport type { KoaContext } from \"./types.d.ts\";\n\n/**\n * Convenience function to load RPC config and attach the RPC middleware to a Koa app.\n * Dynamically imports loadRPCConfig and registers the middleware.\n * @param app - Koa application instance\n */\nexport async function attachRPC(app: Koa) {\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\n const config = await loadRPCConfig();\n const { adapter: _adapter, ...options } = config;\n app.use(createRPCMiddleware(options));\n}\n\n/**\n * Attaches Vite's dev server middlewares to a Koa app for development mode.\n * Bridges Koa's context-based middleware to Vite's Connect-compatible middleware stack\n * by forwarding Koa body, wrapping res.end, and delegating back to Koa on 404 or unhandled routes.\n * @param app - Koa application instance\n * @param vite - Running Vite dev server\n */\nexport function attachVite(app: Koa, vite: ViteDevServer): void {\n app.use(async (ctx: KoaContext, next) => {\n const req = ctx.req;\n const res = ctx.res;\n\n // Forward Koa body to req.body for Express/RPC middleware compatibility\n const requestBody = ctx.request?.body;\n if (requestBody !== undefined) {\n Object.assign(req, { body: requestBody });\n }\n\n const originalEnd = res.end.bind(res);\n let viteHandled = false;\n // @ts-ignore - Koa res.end type mismatch with Node's\n res.end = function (...args: unknown[]) {\n viteHandled = true;\n return originalEnd(args[0]);\n };\n\n await new Promise<void>((resolve) => {\n vite.middlewares(req, res, () => resolve(undefined));\n });\n\n // @ts-ignore - Koa res.end type mismatch with Node's\n res.end = originalEnd;\n\n if (!viteHandled || res.statusCode === 404) {\n await next();\n }\n });\n}\n\n/**\n * Reads and parses the HTTP request body from a Koa context.\n * If koa-body or another body parser already consumed the stream,\n * uses the pre-parsed body from `ctx.request.body`.\n * @param ctx - Koa context\n * @returns A promise resolving to the parsed body with its content type\n */\nexport const readBody = (\n ctx: KoaContext,\n): Promise<BodyResult> => {\n const contentType = ctx.request.headers[\"content-type\"]?.toLowerCase() || \"\";\n\n return new Promise((resolve, reject) => {\n // If an koa-body already consumed the stream\n // via app.use(koaBody()), use ctx.request.body directly\n const isJSON = contentType.includes(\"json\");\n const isMultipart = contentType.includes(\"multipart/form-data\");\n const isUrlEncoded = contentType.includes(\"urlencoded\");\n const reqBody = ctx.request.body;\n if (reqBody !== undefined) {\n resolve({\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 return;\n }\n\n // OR read the body normally\n let body = \"\";\n\n const toggleListeners = (add?: boolean) => {\n const method = add ? \"on\" : \"off\";\n ctx.req[method](\"data\", onData);\n ctx.req[method](\"end\", onEnd);\n ctx.req[method](\"error\", onError);\n };\n\n const onData = (chunk: Buffer) => {\n // chunks.push(chunk);\n body += chunk.toString();\n };\n\n const onEnd = () => {\n toggleListeners();\n const isJSON = contentType.includes(\"json\");\n const isMultipart = contentType.includes(\"multipart/form-data\");\n const isUrlEncoded = contentType.includes(\"urlencoded\");\n try {\n const data = isMultipart\n ? { raw: body }\n : isUrlEncoded\n ? Object.fromEntries(new URLSearchParams(body))\n : JSON.parse(body);\n resolve({\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 ? (data as Record<string, unknown>) : data,\n } as BodyResult);\n } catch (_er) {\n resolve({ contentType: \"text/plain\", data: String(body) });\n }\n };\n\n const onError = (err: Error) => {\n toggleListeners();\n reject(err);\n };\n\n toggleListeners(true);\n });\n};\n\n/**\n * Issues an HTTP redirect on a Koa context. Koa's `ctx.redirect(location)`\n * defaults to `302` and sets the `Location` header; the status code must be\n * overridden *after* the call (setting it before is ignored, see\n * koajs/koa#857). Defaults to `303 See Other` for convention\n * (Post/Redirect/Get).\n * @param ctx - Koa context\n * @param location - The URL to redirect to\n * @param status - HTTP status code, defaults to 303\n */\nexport const redirect = (\n ctx: KoaContext,\n location: string,\n status = 303,\n): void => {\n ctx.redirect(location);\n ctx.status = status;\n};\n","// src/koa/createMiddleware.ts\nimport type { Context, Next } from \"koa\";\nimport type { KoaMiddlewareFn, KoaMiddlewareOptions } from \"./types.d.ts\";\nimport type { JsonValue } from \"@thednp/rpc\";\nimport type { RequestEvent } from \"@thednp/rpc/server\";\nimport {\n escapeRegExp,\n formatError,\n hasContentTypeMismatch,\n provideRequestContext,\n safeURL,\n scanForServerFiles,\n serverFunctionsMap,\n} from \"@thednp/rpc/server\";\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\";\n\nimport { defaultMiddlewareOptions, defaultRPCOptions } from \"../options.ts\";\nimport { readBody, redirect as koaRedirect } from \"./helpers.ts\";\n\nlet middlewareCount = 0;\nconst middlewareStack = new Set<string>();\n\n/**\n * Creates a Koa middleware with optional path and rpcPrefix filtering.\n * Middleware names are deduplicated. Prefix and path regexes are compiled once at creation time.\n * Koa URL is normalized via `new URL()` to strip query strings before matching.\n * @param initialOptions - Options for rpcPrefix, path matching, and the handler function\n * @returns A Koa middleware function\n */\nexport const createMiddleware: KoaMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n initialOptions,\n ) as KoaMiddlewareOptions;\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 = async (ctx: Context, next: Next) => {\n const url = safeURL(ctx.url).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 await handler(ctx, next);\n };\n\n Object.defineProperty(middlewareHandler, \"name\", {\n value: name,\n });\n\n return middlewareHandler;\n};\n\n/**\n * Creates the Koa 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 sets the JSON-serialized result on ctx.body.\n * @param initialOptions - Options including rpcPrefix for URL routing\n * @returns A Koa middleware function\n */\nexport const createRPCMiddleware: KoaMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n { rpcPrefix: defaultRPCOptions.rpcPrefix },\n initialOptions,\n ) as KoaMiddlewareOptions;\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 (ctx: Context, _next: Next) => {\n const reqUrl = safeURL(ctx.url);\n const url = reqUrl.pathname;\n // const { rpcPrefix } = 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 next\n if (prefixRegex && !prefixRegex.test(url)) {\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 = ctx.headers.origin;\n if (origin && requestOrigin && requestOrigin !== origin) {\n ctx.status = 403;\n ctx.body = { error: REQUEST_FORBIDDEN };\n return;\n }\n\n const functionName = url.replace(prefixReplace, \"\");\n const serverFunction = serverFunctionsMap.get(functionName);\n\n if (!serverFunction) {\n ctx.status = 404;\n ctx.body = { error: FUNCTION_NOT_FOUND };\n return;\n }\n\n try {\n const method = serverFunction.options?.method || \"POST\";\n if (ctx.method.toUpperCase() !== method) {\n ctx.status = 405;\n ctx.body = { error: METHOD_NOT_ALLOWED };\n return;\n }\n\n let args: JsonValue[] = [];\n if (method === \"GET\") {\n const raw = reqUrl.searchParams.get(\"args\");\n if (raw) {\n const parsed: unknown = JSON.parse(raw);\n if (!Array.isArray(parsed)) {\n ctx.status = 400;\n ctx.body = { error: BAD_REQUEST };\n return;\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 ctx.headers[\"content-type\"],\n )\n ) {\n ctx.status = 415;\n ctx.body = { error: UNSUPPORTED_MEDIA_TYPE };\n return;\n }\n const body = await readBody(ctx);\n args = Array.isArray(body.data)\n ? body.data as JsonValue[]\n : [body.data as JsonValue];\n }\n const requestEvent: RequestEvent = {\n request: ctx.req,\n response: ctx,\n nativeEvent: ctx,\n locals: ctx.state,\n functionName,\n redirect: (location, status = 303) => {\n requestEvent.redirected = { location, status };\n koaRedirect(ctx, location, status);\n },\n send: (status, body, headers) => {\n requestEvent.sent = { status, body, headers };\n if (headers) {\n for (const [name, value] of Object.entries(headers)) {\n ctx.set(name, value);\n }\n }\n ctx.status = status;\n ctx.body = body;\n },\n };\n const { data: resultData, cancel } = provideRequestContext(\n requestEvent,\n () => serverFunction.handler(...args),\n );\n const onClose = () => cancel(CLIENT_DISCONNECTED);\n ctx.req.on(\"close\", onClose);\n const result = await resultData;\n ctx.req.off(\"close\", onClose);\n\n // Skip the JSON send when the server function issued a redirect or\n // short-circuited with `send`; the bound Koa adapter already set\n // ctx.status/ctx.body.\n // istanbul ignore else\n if (!requestEvent.redirected && !requestEvent.sent) {\n ctx.status = 200;\n ctx.body = { data: result };\n }\n } catch (err) {\n console.error(String(err));\n const isProduction = process.env.NODE_ENV === \"production\";\n ctx.status = 500;\n ctx.body = formatError(err, isProduction);\n }\n },\n });\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"koa.mjs","names":[],"sources":["../../src/options.ts","../../src/functionsMap.ts","../../src/constants.ts","../../src/koa/helpers.ts","../../src/koa/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/koa/helpers.ts\nimport type { Buffer } from \"node:buffer\";\nimport type { ViteDevServer } from \"vite\";\nimport { createRPCMiddleware } from \"./createMiddleware.ts\";\nimport type { BodyResult } from \"@thednp/rpc\";\nimport type { Koa } from \"./index.ts\";\nimport type { KoaContext } from \"./types.d.ts\";\n\n/**\n * Convenience function to load RPC config and attach the RPC middleware to a Koa app.\n * Dynamically imports loadRPCConfig and registers the middleware.\n * @param app - Koa application instance\n */\nexport async function attachRPC(app: Koa) {\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\n const config = await loadRPCConfig();\n const { adapter: _adapter, ...options } = config;\n app.use(createRPCMiddleware(options));\n}\n\n/**\n * Attaches Vite's dev server middlewares to a Koa app for development mode.\n * Bridges Koa's context-based middleware to Vite's Connect-compatible middleware stack\n * by forwarding Koa body, wrapping res.end, and delegating back to Koa on 404 or unhandled routes.\n * @param app - Koa application instance\n * @param vite - Running Vite dev server\n */\nexport function attachVite(app: Koa, vite: ViteDevServer): void {\n app.use(async (ctx: KoaContext, next) => {\n const req = ctx.req;\n const res = ctx.res;\n\n // Forward Koa body to req.body for Express/RPC middleware compatibility\n const requestBody = ctx.request?.body;\n if (requestBody !== undefined) {\n Object.assign(req, { body: requestBody });\n }\n\n const originalEnd = res.end.bind(res);\n let viteHandled = false;\n // @ts-ignore - Koa res.end type mismatch with Node's\n res.end = function (...args: unknown[]) {\n viteHandled = true;\n return originalEnd(args[0]);\n };\n\n await new Promise<void>((resolve) => {\n vite.middlewares(req, res, () => resolve(undefined));\n });\n\n // @ts-ignore - Koa res.end type mismatch with Node's\n res.end = originalEnd;\n\n if (!viteHandled || res.statusCode === 404) {\n await next();\n }\n });\n}\n\n/**\n * Reads and parses the HTTP request body from a Koa context.\n * If koa-body or another body parser already consumed the stream,\n * uses the pre-parsed body from `ctx.request.body`.\n * @param ctx - Koa context\n * @returns A promise resolving to the parsed body with its content type\n */\nexport const readBody = (\n ctx: KoaContext,\n): Promise<BodyResult> => {\n const contentType = ctx.request.headers[\"content-type\"]?.toLowerCase() || \"\";\n\n return new Promise((resolve, reject) => {\n // If an koa-body already consumed the stream\n // via app.use(koaBody()), use ctx.request.body directly\n const isJSON = contentType.includes(\"json\");\n const isMultipart = contentType.includes(\"multipart/form-data\");\n const isUrlEncoded = contentType.includes(\"urlencoded\");\n const reqBody = ctx.request.body;\n if (reqBody !== undefined) {\n resolve({\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 return;\n }\n\n // OR read the body normally\n let body = \"\";\n\n const toggleListeners = (add?: boolean) => {\n const method = add ? \"on\" : \"off\";\n ctx.req[method](\"data\", onData);\n ctx.req[method](\"end\", onEnd);\n ctx.req[method](\"error\", onError);\n };\n\n const onData = (chunk: Buffer) => {\n // chunks.push(chunk);\n body += chunk.toString();\n };\n\n const onEnd = () => {\n toggleListeners();\n const isJSON = contentType.includes(\"json\");\n const isMultipart = contentType.includes(\"multipart/form-data\");\n const isUrlEncoded = contentType.includes(\"urlencoded\");\n try {\n const data = isMultipart\n ? { raw: body }\n : isUrlEncoded\n ? Object.fromEntries(new URLSearchParams(body))\n : JSON.parse(body);\n resolve({\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 ? (data as Record<string, unknown>) : data,\n } as BodyResult);\n } catch (_er) {\n resolve({ contentType: \"text/plain\", data: String(body) });\n }\n };\n\n const onError = (err: Error) => {\n toggleListeners();\n reject(err);\n };\n\n toggleListeners(true);\n });\n};\n\n/**\n * Issues an HTTP redirect on a Koa context. Koa's `ctx.redirect(location)`\n * defaults to `302` and sets the `Location` header; the status code must be\n * overridden *after* the call (setting it before is ignored, see\n * koajs/koa#857). Defaults to `303 See Other` for convention\n * (Post/Redirect/Get).\n * @param ctx - Koa context\n * @param location - The URL to redirect to\n * @param status - HTTP status code, defaults to 303\n */\nexport const redirect = (\n ctx: KoaContext,\n location: string,\n status = 303,\n): void => {\n ctx.redirect(location);\n ctx.status = status;\n};\n","// src/koa/createMiddleware.ts\nimport type { Context, Next } from \"koa\";\nimport type { KoaMiddlewareFn, KoaMiddlewareOptions } from \"./types.d.ts\";\nimport type { JsonValue } from \"@thednp/rpc\";\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 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\";\n\nimport {\n defaultMiddlewareOptions,\n defaultPrefix,\n defaultRPCOptions,\n} from \"../options.ts\";\nimport { readBody, redirect as koaRedirect } from \"./helpers.ts\";\n\nlet middlewareCount = 0;\nconst middlewareStack = new Set<string>();\n\n/**\n * Creates a Koa middleware with optional path and rpcPrefix filtering.\n * Middleware names are deduplicated. Prefix and path regexes are compiled once at creation time.\n * Koa URL is normalized via `new URL()` to strip query strings before matching.\n * @param initialOptions - Options for rpcPrefix, path matching, and the handler function\n * @returns A Koa middleware function\n */\nexport const createMiddleware: KoaMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n initialOptions,\n ) as KoaMiddlewareOptions;\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 = async (ctx: Context, next: Next) => {\n const url = safeURL(ctx.url).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: (options as unknown as { serverFiles?: \"exact\" | \"glob\" })\n .serverFiles,\n scanRoot: (options as unknown as { scanRoot?: string }).scanRoot,\n } as never);\n }\n\n await handler(ctx, next);\n };\n\n Object.defineProperty(middlewareHandler, \"name\", {\n value: name,\n });\n\n return middlewareHandler;\n};\n\n/**\n * Creates the Koa 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 sets the JSON-serialized result on ctx.body.\n * @param initialOptions - Options including rpcPrefix for URL routing\n * @returns A Koa middleware function\n */\nexport const createRPCMiddleware: KoaMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n { rpcPrefix: defaultRPCOptions.rpcPrefix },\n initialOptions,\n ) as KoaMiddlewareOptions;\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 (ctx: Context, _next: Next) => {\n const reqUrl = safeURL(ctx.url);\n const url = reqUrl.pathname;\n // const { rpcPrefix } = 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 next\n if (prefixRegex && !prefixRegex.test(url)) {\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 = ctx.headers.origin;\n if (origin && requestOrigin && requestOrigin !== origin) {\n ctx.status = 403;\n ctx.body = { error: REQUEST_FORBIDDEN };\n return;\n }\n\n const functionName = url.replace(prefixReplace, \"\");\n const serverFunction = getFunctionsForPrefix(prefix).get(functionName);\n\n if (!serverFunction) {\n ctx.status = 404;\n ctx.body = { error: FUNCTION_NOT_FOUND };\n return;\n }\n\n try {\n const method = serverFunction.options?.method || \"POST\";\n if (ctx.method.toUpperCase() !== method) {\n ctx.status = 405;\n ctx.body = { error: METHOD_NOT_ALLOWED };\n return;\n }\n\n let args: JsonValue[] = [];\n if (method === \"GET\") {\n const raw = reqUrl.searchParams.get(\"args\");\n if (raw) {\n const parsed: unknown = JSON.parse(raw);\n if (!Array.isArray(parsed)) {\n ctx.status = 400;\n ctx.body = { error: BAD_REQUEST };\n return;\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 ctx.headers[\"content-type\"],\n )\n ) {\n ctx.status = 415;\n ctx.body = { error: UNSUPPORTED_MEDIA_TYPE };\n return;\n }\n const body = await readBody(ctx);\n args = Array.isArray(body.data)\n ? body.data as JsonValue[]\n : [body.data as JsonValue];\n }\n const requestEvent: RequestEvent = {\n request: ctx.req,\n response: ctx,\n nativeEvent: ctx,\n locals: ctx.state,\n functionName,\n redirect: (location, status = 303) => {\n requestEvent.redirected = { location, status };\n koaRedirect(ctx, location, status);\n },\n send: (status, body, headers) => {\n requestEvent.sent = { status, body, headers };\n if (headers) {\n for (const [name, value] of Object.entries(headers)) {\n ctx.set(name, value);\n }\n }\n ctx.status = status;\n ctx.body = body;\n },\n };\n const { data: resultData, cancel } = provideRequestContext(\n requestEvent,\n () => serverFunction.handler(...args),\n );\n const onClose = () => cancel(CLIENT_DISCONNECTED);\n ctx.req.on(\"close\", onClose);\n const result = await resultData;\n ctx.req.off(\"close\", onClose);\n\n // Skip the JSON send when the server function issued a redirect or\n // short-circuited with `send`; the bound Koa adapter already set\n // ctx.status/ctx.body.\n // istanbul ignore else\n if (!requestEvent.redirected && !requestEvent.sent) {\n ctx.status = 200;\n ctx.body = { data: result };\n }\n } catch (err) {\n console.error(String(err));\n const isProduction = process.env.NODE_ENV === \"production\";\n ctx.status = 500;\n ctx.body = formatError(err, isProduction);\n }\n },\n });\n};\n"],"mappings":"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"}
|
package/dist/server/server.d.mts
CHANGED
|
@@ -1,15 +1,95 @@
|
|
|
1
|
-
import { ResolvedConfig, ViteDevServer } from "vite";
|
|
1
|
+
import { Connect, ResolvedConfig, ViteDevServer } from "vite";
|
|
2
2
|
import "@thednp/rpc";
|
|
3
|
-
import "
|
|
4
|
-
import "
|
|
3
|
+
import { IncomingMessage, ServerResponse } from "node:http";
|
|
4
|
+
import { NextFunction, Request, Response as Response$1 } from "express";
|
|
5
|
+
import { MiddlewareHandler } from "hono";
|
|
5
6
|
import "@hono/node-server";
|
|
6
7
|
import "hono/utils/http-status";
|
|
7
8
|
import "hono/factory";
|
|
8
|
-
import "fastify";
|
|
9
|
+
import { FastifyReply, FastifyRequest, HookHandlerDoneFunction } from "fastify";
|
|
9
10
|
import "fastify-plugin";
|
|
10
|
-
import "koa";
|
|
11
|
-
import "h3";
|
|
11
|
+
import { Context, Next } from "koa";
|
|
12
|
+
import { Middleware } from "h3";
|
|
13
|
+
//#region src/express/types.d.ts
|
|
14
|
+
/**
|
|
15
|
+
* Express/Connect middleware handler signature used by the RPC middleware.
|
|
16
|
+
*/
|
|
17
|
+
interface ExpressMiddlewareHooks {
|
|
18
|
+
/**
|
|
19
|
+
* The handler invoked for each matched request.
|
|
20
|
+
* @param req - Node or Express request object
|
|
21
|
+
* @param res - Node or Express response object
|
|
22
|
+
* @param next - Connect or Express next function
|
|
23
|
+
*/
|
|
24
|
+
handler: (req: IncomingMessage | Request, res: ServerResponse | Response$1, next: Connect.NextFunction | NextFunction) => Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/hono/types.d.ts
|
|
28
|
+
/**
|
|
29
|
+
* Hono middleware handler signature used by the RPC middleware.
|
|
30
|
+
*/
|
|
31
|
+
interface HonoMiddlewareHooks {
|
|
32
|
+
/** Hono middleware handler */
|
|
33
|
+
handler: MiddlewareHandler;
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/fastify/types.d.ts
|
|
37
|
+
/**
|
|
38
|
+
* Fastify middleware handler signature used by the RPC middleware.
|
|
39
|
+
*/
|
|
40
|
+
interface FastifyMiddlewareHooks {
|
|
41
|
+
/**
|
|
42
|
+
* The handler invoked for each matched request.
|
|
43
|
+
* @param req - Fastify request object
|
|
44
|
+
* @param res - Fastify reply object
|
|
45
|
+
* @param done - Fastify hook completion callback
|
|
46
|
+
*/
|
|
47
|
+
handler: (req: FastifyRequest, res: FastifyReply, done: HookHandlerDoneFunction) => Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/koa/types.d.ts
|
|
51
|
+
/**
|
|
52
|
+
* Koa middleware handler signature used by the RPC middleware.
|
|
53
|
+
*/
|
|
54
|
+
interface KoaMiddlewareHooks {
|
|
55
|
+
/**
|
|
56
|
+
* The handler invoked for each matched request.
|
|
57
|
+
* @param ctx - Koa context object
|
|
58
|
+
* @param next - Koa next function
|
|
59
|
+
*/
|
|
60
|
+
handler: (ctx: Context, next: Next) => Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region src/h3/types.d.ts
|
|
64
|
+
/**
|
|
65
|
+
* h3 middleware handler signature used by the RPC middleware.
|
|
66
|
+
*/
|
|
67
|
+
interface H3MiddlewareHooks {
|
|
68
|
+
/**
|
|
69
|
+
* The handler invoked for each matched request.
|
|
70
|
+
* @param event - h3 event object
|
|
71
|
+
* @param next - h3 next function
|
|
72
|
+
*/
|
|
73
|
+
handler: Middleware;
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
12
76
|
//#region src/types.d.ts
|
|
77
|
+
/**
|
|
78
|
+
* Maps each supported framework adapter to its middleware hooks (handler signatures).
|
|
79
|
+
* Used to keep the middleware options type-safe per adapter.
|
|
80
|
+
*/
|
|
81
|
+
interface FrameworkHooks {
|
|
82
|
+
/** Express/Connect middleware handler signature */
|
|
83
|
+
express: ExpressMiddlewareHooks;
|
|
84
|
+
/** Hono middleware handler signature */
|
|
85
|
+
hono: HonoMiddlewareHooks;
|
|
86
|
+
/** Fastify middleware handler signature */
|
|
87
|
+
fastify: FastifyMiddlewareHooks;
|
|
88
|
+
/** Koa middleware handler signature */
|
|
89
|
+
koa: KoaMiddlewareHooks;
|
|
90
|
+
/** h3 middleware handler signature */
|
|
91
|
+
h3: H3MiddlewareHooks;
|
|
92
|
+
}
|
|
13
93
|
/**
|
|
14
94
|
* Content types the RPC client modules send with each request.
|
|
15
95
|
*/
|
|
@@ -40,6 +120,11 @@ interface ServerFunctionOptions {
|
|
|
40
120
|
* @default "POST"
|
|
41
121
|
*/
|
|
42
122
|
method?: "GET" | "POST";
|
|
123
|
+
/**
|
|
124
|
+
* RPC endpoint prefix
|
|
125
|
+
* @default "__rpc"
|
|
126
|
+
*/
|
|
127
|
+
rpcPrefix?: string;
|
|
43
128
|
}
|
|
44
129
|
// primitives and their compositions
|
|
45
130
|
/**
|
|
@@ -98,12 +183,14 @@ interface RpcPluginOptionsInternal {
|
|
|
98
183
|
/**
|
|
99
184
|
* Partial Vite config used when scanning server files outside a running dev server.
|
|
100
185
|
*/
|
|
101
|
-
|
|
186
|
+
interface ScanConfig extends Pick<ResolvedConfig, "base"> {
|
|
102
187
|
root?: string;
|
|
103
188
|
server?: Partial<ResolvedConfig["server"]>;
|
|
104
189
|
serverFiles?: "exact" | "glob";
|
|
105
190
|
scanRoot?: string;
|
|
106
|
-
|
|
191
|
+
/** Default rpcPrefix to register scanned functions under when a function does not declare its own. Defaults to `__rpc` for backward compatibility. */
|
|
192
|
+
rpcPrefix?: string;
|
|
193
|
+
}
|
|
107
194
|
/**
|
|
108
195
|
* Entry in the server functions map: registered name, client handler,
|
|
109
196
|
* optional per-function options, and the original export name.
|
|
@@ -158,8 +245,91 @@ interface RpcPluginOptions {
|
|
|
158
245
|
*/
|
|
159
246
|
serverFiles?: "exact" | "glob";
|
|
160
247
|
}
|
|
248
|
+
interface MiddlewareOptions<A extends RpcPluginOptions["adapter"] = "express"> {
|
|
249
|
+
/**
|
|
250
|
+
* Name for the middleware (used for identification in Express stack)
|
|
251
|
+
*/
|
|
252
|
+
name?: string;
|
|
253
|
+
/**
|
|
254
|
+
* Path pattern to match for middleware execution.
|
|
255
|
+
* Accepts string or RegExp to filter requests based on URL path.
|
|
256
|
+
*
|
|
257
|
+
* @example
|
|
258
|
+
* // String path
|
|
259
|
+
* path: "/api/v1"
|
|
260
|
+
*
|
|
261
|
+
* // RegExp pattern
|
|
262
|
+
* path: /^\/api\/v[0-9]+/
|
|
263
|
+
*/
|
|
264
|
+
path?: string | RegExp;
|
|
265
|
+
/**
|
|
266
|
+
* RPC prefix without leading slash (e.g. "__rpc")
|
|
267
|
+
* Leading slash will be added automatically by the middleware.
|
|
268
|
+
* This prefix defines the base path for all RPC endpoints.
|
|
269
|
+
* @default string
|
|
270
|
+
* @example
|
|
271
|
+
* // Results in endpoints like: /api/rpc/myFunction
|
|
272
|
+
* rpcPrefix: "api/rpc"
|
|
273
|
+
*/
|
|
274
|
+
rpcPrefix?: string | false;
|
|
275
|
+
/**
|
|
276
|
+
* Allowed request origin (e.g. "https://example.com").
|
|
277
|
+
* When set, any request carrying an `Origin` header that does not match
|
|
278
|
+
* is rejected with a 403 Forbidden response. Requests without an `Origin`
|
|
279
|
+
* header (curl, native clients) pass through unchecked.
|
|
280
|
+
* When unset (default), no origin validation is performed.
|
|
281
|
+
*/
|
|
282
|
+
origin?: string;
|
|
283
|
+
/**
|
|
284
|
+
* Server file matching mode. Use `"exact"` for `server.ts|js|mjs|mts`
|
|
285
|
+
* names, or `"glob"` to match `**\/*.server.{ts,js,mjs,mts}` inside the
|
|
286
|
+
* scan root. Only used for the lazy production scan when the middleware
|
|
287
|
+
* populates its prefix map on first request.
|
|
288
|
+
* @default "exact"
|
|
289
|
+
*/
|
|
290
|
+
serverFiles?: "exact" | "glob";
|
|
291
|
+
/**
|
|
292
|
+
* Root directory for scanning server files. Defaults to `<root>/src/api`.
|
|
293
|
+
* Only used for the lazy production scan.
|
|
294
|
+
*/
|
|
295
|
+
scanRoot?: string;
|
|
296
|
+
/**
|
|
297
|
+
* Async handler for request processing.
|
|
298
|
+
* Core middleware function that processes incoming requests.
|
|
299
|
+
*
|
|
300
|
+
* @param req - The incoming request object
|
|
301
|
+
* @param res - The server response object
|
|
302
|
+
* @param next - Function to pass control to the next middleware
|
|
303
|
+
*
|
|
304
|
+
* @example
|
|
305
|
+
* handler: async (req, res, next) => {
|
|
306
|
+
* // Process request
|
|
307
|
+
* const data = await processRequest(req);
|
|
308
|
+
*
|
|
309
|
+
* // Send response
|
|
310
|
+
* sendResponse(res, { data }, 200);
|
|
311
|
+
* }
|
|
312
|
+
*/
|
|
313
|
+
handler?: FrameworkHooks[A]["handler"];
|
|
314
|
+
}
|
|
161
315
|
//#endregion
|
|
162
316
|
//#region src/functionsMap.d.ts
|
|
317
|
+
/**
|
|
318
|
+
* Map of rpcPrefix -> Map of function names -> ServerFnEntry
|
|
319
|
+
* Enables multiple RPC instances with different prefixes to coexist
|
|
320
|
+
* without name collisions.
|
|
321
|
+
*/
|
|
322
|
+
declare const serverFunctionsByPrefix: Map<string, Map<string, ServerFnEntry>>;
|
|
323
|
+
/**
|
|
324
|
+
* Gets or creates the function map for a specific prefix.
|
|
325
|
+
* @param prefix - The RPC prefix (e.g., "__rpc", "v1:rpc", "admin:rpc")
|
|
326
|
+
* @returns Map of function names to ServerFnEntry for that prefix
|
|
327
|
+
*/
|
|
328
|
+
declare const getFunctionsForPrefix: (prefix: string) => Map<string, ServerFnEntry>;
|
|
329
|
+
/**
|
|
330
|
+
* Backward compatibility: default map for the default prefix.
|
|
331
|
+
* Legacy code can still use serverFunctionsMap.set(name, entry).
|
|
332
|
+
*/
|
|
163
333
|
declare const serverFunctionsMap: Map<string, ServerFnEntry>;
|
|
164
334
|
//#endregion
|
|
165
335
|
//#region src/scanForServerFiles.d.ts
|
|
@@ -167,7 +337,7 @@ declare const serverFunctionsMap: Map<string, ServerFnEntry>;
|
|
|
167
337
|
declare const scannedServerFiles: Set<string>;
|
|
168
338
|
/**
|
|
169
339
|
* Scans `src/api/` (or an explicit `scanRoot`) for server function files
|
|
170
|
-
* and populates the
|
|
340
|
+
* and populates the server functions map (scoped by rpcPrefix) with their exported functions.
|
|
171
341
|
* Uses Vite's SSR module loading to resolve and execute each file.
|
|
172
342
|
*
|
|
173
343
|
* Supports two matching modes via `config.serverFiles`:
|
|
@@ -179,22 +349,49 @@ declare const scannedServerFiles: Set<string>;
|
|
|
179
349
|
declare const scanForServerFiles: (initialCfg?: ScanConfig, devServer?: ViteDevServer) => Promise<void>;
|
|
180
350
|
//#endregion
|
|
181
351
|
//#region src/createFunction.d.ts
|
|
352
|
+
/**
|
|
353
|
+
* Extended options for createServerFunction, including rpcPrefix for multi-instance support.
|
|
354
|
+
*/
|
|
355
|
+
interface CreateServerFunctionOptions extends Partial<ServerFunctionOptions> {
|
|
356
|
+
/**
|
|
357
|
+
* RPC prefix for this function. Enables multiple RPC instances with different prefixes.
|
|
358
|
+
* When using multi-prefix setup, functions with the same name but different prefixes
|
|
359
|
+
* can coexist without collision.
|
|
360
|
+
* @default "__rpc"
|
|
361
|
+
* @example
|
|
362
|
+
* // v1 API
|
|
363
|
+
* export const login = createServerFunction(
|
|
364
|
+
* "login",
|
|
365
|
+
* async (signal, email, password) => ({...}),
|
|
366
|
+
* { rpcPrefix: "v1:rpc" },
|
|
367
|
+
* );
|
|
368
|
+
*
|
|
369
|
+
* // v2 API - same function name, different prefix
|
|
370
|
+
* export const login = createServerFunction(
|
|
371
|
+
* "login",
|
|
372
|
+
* async (signal, credentials) => ({...}),
|
|
373
|
+
* { rpcPrefix: "v2:rpc" },
|
|
374
|
+
* );
|
|
375
|
+
*/
|
|
376
|
+
rpcPrefix?: string;
|
|
377
|
+
}
|
|
182
378
|
/**
|
|
183
379
|
* Creates a server-side RPC function.
|
|
184
|
-
* Registers the function in the server functions map and returns
|
|
185
|
-
* wrapper that exposes `data` (Promise) and `cancel` (function)
|
|
380
|
+
* Registers the function in the server functions map (scoped by rpcPrefix) and returns
|
|
381
|
+
* a client-compatible wrapper that exposes `data` (Promise) and `cancel` (function)
|
|
382
|
+
* for request lifecycle control.
|
|
186
383
|
* @param name - Unique identifier used by the RPC router to dispatch requests
|
|
187
384
|
* @param handler - The actual implementation receiving an AbortSignal followed by JSON-serializable arguments
|
|
188
|
-
* @param fnOptions - Optional contentType and
|
|
385
|
+
* @param fnOptions - Optional contentType, credentials, and rpcPrefix settings
|
|
189
386
|
* @returns A client stub with `data` promise and `cancel` method, auto-registered in the server map
|
|
190
387
|
*/
|
|
191
|
-
declare function createServerFunction<TArgs extends JsonArray = JsonArray, TResult extends JsonValue = JsonValue>(name: string, handler: ServerFunctionInit<TArgs, TResult>, fnOptions?:
|
|
388
|
+
declare function createServerFunction<TArgs extends JsonArray = JsonArray, TResult extends JsonValue = JsonValue>(name: string, handler: ServerFunctionInit<TArgs, TResult>, fnOptions?: CreateServerFunctionOptions): ClientFunction<TArgs, TResult>;
|
|
192
389
|
//#endregion
|
|
193
390
|
//#region src/getClientModules.d.ts
|
|
194
391
|
/**
|
|
195
392
|
* Generates the complete client-side module bundle by iterating all registered server functions
|
|
196
|
-
* and producing fetch-based stubs for each. The result is transformed by Vite
|
|
197
|
-
* the dev server or production build.
|
|
393
|
+
* for a specific prefix and producing fetch-based stubs for each. The result is transformed by Vite
|
|
394
|
+
* (or Oxc) during the dev server or production build.
|
|
198
395
|
* @param initialOptions - Plugin options containing rpcPrefix and optional adapter
|
|
199
396
|
* @returns A string of JavaScript code with all client RPC modules and their import dependencies
|
|
200
397
|
*/
|
|
@@ -267,6 +464,9 @@ declare function escapeRegExp(s: string): string;
|
|
|
267
464
|
* @returns A URL object; never throws
|
|
268
465
|
*/
|
|
269
466
|
declare const safeURL: (rawUrl: string, base?: string) => URL;
|
|
467
|
+
/** Global rpcPrefix from the last loaded config / middleware — fallback for functions without explicit prefix. */
|
|
468
|
+
declare const getGlobalPrefix: () => string | undefined;
|
|
469
|
+
declare const setGlobalPrefix: (prefix: string | undefined) => void;
|
|
270
470
|
//#endregion
|
|
271
471
|
//#region src/context.d.ts
|
|
272
472
|
/**
|
|
@@ -404,17 +604,10 @@ interface RequestMeta {
|
|
|
404
604
|
declare const getRequestMeta: (event: RequestEvent) => RequestMeta;
|
|
405
605
|
//#endregion
|
|
406
606
|
//#region src/options.d.ts
|
|
407
|
-
declare const defaultServerFnOptions:
|
|
408
|
-
|
|
409
|
-
credentials: "same-origin";
|
|
410
|
-
method: "POST";
|
|
411
|
-
};
|
|
607
|
+
declare const defaultServerFnOptions: ServerFunctionOptions;
|
|
608
|
+
declare const defaultPrefix = "__rpc";
|
|
412
609
|
declare const defaultRPCOptions: RpcPluginOptions;
|
|
413
|
-
declare const defaultMiddlewareOptions:
|
|
414
|
-
rpcPrefix: undefined;
|
|
415
|
-
path: undefined;
|
|
416
|
-
origin: undefined;
|
|
417
|
-
};
|
|
610
|
+
declare const defaultMiddlewareOptions: MiddlewareOptions;
|
|
418
611
|
//#endregion
|
|
419
|
-
export { RPCError, RequestEvent, RequestMeta, createServerFunction, defaultMiddlewareOptions, defaultRPCOptions, defaultServerFnOptions, escapeRegExp, formatError, getClientModules, getRequestContext, getRequestMeta, hasContentTypeMismatch, isFormContentType, provideRequestContext, redirect, safeURL, scanForServerFiles, scannedServerFiles, sendResponse, serverFunctionsMap, walkGlobFiles };
|
|
612
|
+
export { CreateServerFunctionOptions, RPCError, RequestEvent, RequestMeta, createServerFunction, defaultMiddlewareOptions, defaultPrefix, defaultRPCOptions, defaultServerFnOptions, escapeRegExp, formatError, getClientModules, getFunctionsForPrefix, getGlobalPrefix, getRequestContext, getRequestMeta, hasContentTypeMismatch, isFormContentType, provideRequestContext, redirect, safeURL, scanForServerFiles, scannedServerFiles, sendResponse, serverFunctionsByPrefix, serverFunctionsMap, setGlobalPrefix, walkGlobFiles };
|
|
420
613
|
//# sourceMappingURL=server.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.mts","names":[],"sources":["../../src/types.d.ts","../../src/functionsMap.ts","../../src/scanForServerFiles.ts","../../src/createFunction.ts","../../src/getClientModules.ts","../../src/server-helpers.ts","../../src/context.ts","../../src/options.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"server.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/functionsMap.ts","../../src/scanForServerFiles.ts","../../src/createFunction.ts","../../src/getClientModules.ts","../../src/server-helpers.ts","../../src/context.ts","../../src/options.ts"],"mappings":"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"}
|