@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.
@@ -1,6 +1,5 @@
1
- import { escapeRegExp, formatError, hasContentTypeMismatch, provideRequestContext, safeURL, scanForServerFiles, serverFunctionsMap } from "@thednp/rpc/server";
1
+ import { escapeRegExp, formatError, getGlobalPrefix, hasContentTypeMismatch, provideRequestContext, safeURL, scanForServerFiles } from "@thednp/rpc/server";
2
2
  import fp from "fastify-plugin";
3
- //#region src/options.ts
4
3
  const defaultRPCOptions = {
5
4
  rpcPrefix: "__rpc",
6
5
  adapter: "express",
@@ -13,6 +12,32 @@ 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";
@@ -142,7 +167,7 @@ const middlewareStack = /* @__PURE__ */ new Set();
142
167
  const createMiddleware = (initialOptions = {}) => {
143
168
  const options = Object.assign({}, defaultMiddlewareOptions, initialOptions);
144
169
  const middlewareName = options.name;
145
- const rpcPrefix = options.rpcPrefix;
170
+ let rpcPrefix = options.rpcPrefix;
146
171
  const path = options.path;
147
172
  const handler = options.handler;
148
173
  let name = middlewareName;
@@ -156,7 +181,6 @@ const createMiddleware = (initialOptions = {}) => {
156
181
  const pathMatcher = path ? typeof path === "string" ? new RegExp(path) : path : null;
157
182
  const middlewareHandler = async (req, reply, done) => {
158
183
  const url = safeURL(req.url).pathname;
159
- if (serverFunctionsMap.size === 0) await scanForServerFiles();
160
184
  if (!handler) {
161
185
  done();
162
186
  return;
@@ -169,6 +193,12 @@ const createMiddleware = (initialOptions = {}) => {
169
193
  done();
170
194
  return;
171
195
  }
196
+ rpcPrefix = rpcPrefix ?? "__rpc";
197
+ if (getFunctionsForPrefix(rpcPrefix).size === 0) await scanForServerFiles({
198
+ rpcPrefix,
199
+ serverFiles: options.serverFiles,
200
+ scanRoot: options.scanRoot
201
+ });
172
202
  await handler(req, reply, done);
173
203
  };
174
204
  Object.defineProperty(middlewareHandler, "name", { value: name });
@@ -184,8 +214,9 @@ const createMiddleware = (initialOptions = {}) => {
184
214
  const createRPCMiddleware = (initialOptions = {}) => {
185
215
  const options = Object.assign({}, defaultMiddlewareOptions, { rpcPrefix: defaultRPCOptions.rpcPrefix }, initialOptions);
186
216
  const rpcPrefix = options.rpcPrefix;
217
+ const prefix = rpcPrefix || getGlobalPrefix() || "__rpc";
187
218
  const prefixRegex = rpcPrefix ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`) : null;
188
- const prefixReplace = `/${rpcPrefix}/`;
219
+ const prefixReplace = `/${prefix}/`;
189
220
  return createMiddleware({
190
221
  ...options,
191
222
  handler: async (req, reply, _done) => {
@@ -199,7 +230,7 @@ const createRPCMiddleware = (initialOptions = {}) => {
199
230
  return;
200
231
  }
201
232
  const functionName = url.replace(prefixReplace, "");
202
- const serverFunction = serverFunctionsMap.get(functionName);
233
+ const serverFunction = getFunctionsForPrefix(prefix).get(functionName);
203
234
  if (!serverFunction) {
204
235
  reply.status(404).send({ error: FUNCTION_NOT_FOUND });
205
236
  return;
@@ -1 +1 @@
1
- {"version":3,"file":"fastify.mjs","names":["fastifyRpcPlugin"],"sources":["../../src/options.ts","../../src/constants.ts","../../src/fastify/plugin.ts","../../src/fastify/helpers.ts","../../src/fastify/createMiddleware.ts"],"sourcesContent":["import type {\n MiddlewareOptions,\n RpcPluginOptions,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\n\nexport const defaultServerFnOptions = {\n contentType: \"application/json\",\n 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 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","/** @module Fastify plugin. Exports the RPC plugin wrapped with `fastify-plugin` for lifecycle-compatible registration. */\n// inspired by https://github.com/royalswe/vike-fastify-boilerplate/blob/main/server/index.ts\nimport fp from \"fastify-plugin\";\nimport type { MiddlewareOptions } from \"../types.d.ts\";\nimport type {\n FastifyRPCPlugin,\n RegisteredFastifyRPCPlugin,\n} from \"./types.d.ts\";\nimport { createRPCMiddleware } from \"./createMiddleware.ts\";\n\nexport type { MiddlewareOptions };\n\nconst RpcPlugin: FastifyRPCPlugin = (\n fastify,\n initialOptions,\n done,\n) => {\n // Register RPC middleware as preHandler hook\n const rpcMiddleware = createRPCMiddleware(initialOptions);\n fastify.addHook(\"preHandler\", async (request, reply) => {\n const next = () =>\n new Promise((resolve) => {\n rpcMiddleware(request, reply, resolve);\n });\n await next();\n });\n\n done();\n};\n\n// Export the plugin wrapped with fastify-plugin\nconst rpcPlugin = fp(RpcPlugin, {\n name: \"uni-rpc-fastify-plugin\",\n}) as RegisteredFastifyRPCPlugin;\n\nexport { rpcPlugin as default };\n","// @thednp/rpc/src/fastify/helpers.ts\nimport type { FastifyReply, FastifyRequest } from \"fastify\";\nimport type { ViteDevServer } from \"vite\";\nimport type { FastifyInstance } from \"fastify\";\nimport type { Buffer } from \"node:buffer\";\nimport type { BodyResult, JsonValue } from \"../types.d.ts\";\nimport fastifyRpcPlugin from \"./plugin.ts\";\n\n/**\n * Convenience function to load RPC config and register the RPC plugin to a Fastify instance.\n * Dynamically imports loadRPCConfig and registers the fastify-rpc plugin.\n * @param app - Fastify instance\n */\nexport async function attachRPC(app: FastifyInstance) {\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 await app.register(fastifyRpcPlugin, options);\n}\n\n/**\n * Attaches Vite's dev server middlewares to a Fastify instance for development mode.\n * Uses an `onRequest` hook to delegate to Vite's connect-compatible middleware stack.\n * @param app - Fastify instance\n * @param vite - Running Vite dev server\n */\nexport function attachVite(app: FastifyInstance, vite: ViteDevServer) {\n app.addHook(\"onRequest\", async (request, reply) => {\n const next = () =>\n new Promise((resolve) => {\n vite.middlewares(request.raw, reply.raw, resolve);\n });\n await next();\n });\n}\n\n/**\n * Reads and parses the HTTP request body from a Fastify request.\n * If Fastify's body parser already consumed the stream, uses the pre-parsed body from `req.body`.\n * @param req - Fastify request object\n * @returns A promise resolving to the parsed body with its content type\n */\nexport const readBody = (\n req: FastifyRequest,\n): Promise<BodyResult> => {\n return new Promise((resolve, reject) => {\n const contentType = req.headers[\"content-type\"]?.toLowerCase() || \"\";\n const reqBody = req.body as JsonValue;\n\n if (reqBody !== undefined) {\n const isJSON = contentType.includes(\"json\");\n const isMultipart = contentType.includes(\"multipart/form-data\");\n const isUrlEncoded = contentType.includes(\"urlencoded\");\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 const toggleListeners = (add?: boolean) => {\n const method = add ? \"on\" : \"off\";\n req.raw[method](\"data\", onData);\n req.raw[method](\"end\", onEnd);\n req.raw[method](\"error\", onError);\n };\n\n let body = \"\";\n\n const onData = (chunk: Buffer) => {\n body += chunk.toString();\n };\n\n const onEnd = () => {\n toggleListeners();\n const isJSON = contentType.includes(\"json\");\n 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 (_e) {\n resolve({ contentType: \"text/plain\", data: String(body) });\n }\n };\n const onError = (err: Error) => {\n toggleListeners();\n reject(err);\n };\n\n toggleListeners(true);\n });\n};\n\n/**\n * Issues an HTTP redirect on a Fastify reply using the native\n * `reply.redirect(location, status)` API (Fastify v5 signature: destination\n * URL first, status code optional). Defaults to `303 See Other` for\n * convention (Post/Redirect/Get).\n * @param reply - Fastify reply object\n * @param location - The URL to redirect to\n * @param status - HTTP status code, defaults to 303\n */\nexport const redirect = (\n reply: FastifyReply,\n location: string,\n status = 303,\n): void => {\n reply.redirect(location, status);\n};\n","// src/fastify/createMiddleware.ts\nimport type {\n FastifyReply,\n FastifyRequest,\n HookHandlerDoneFunction,\n} from \"fastify\";\nimport type {\n FastifyMiddlewareFn,\n FastifyMiddlewareOptions,\n} from \"./types.d.ts\";\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 { defaultMiddlewareOptions, defaultRPCOptions } 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, redirect as fastifyRedirect } from \"./helpers.ts\";\n\nlet middlewareCount = 0;\nconst middlewareStack = new Set<string>();\n\n/**\n * Creates a Fastify preHandler hook with optional path and rpcPrefix filtering.\n * Middleware names are deduplicated. Prefix and path regexes are compiled once at creation time.\n * @param initialOptions - Options for rpcPrefix, path matching, and the handler function\n * @returns A Fastify preHandler hook function\n */\nexport const createMiddleware: FastifyMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n initialOptions,\n ) as FastifyMiddlewareOptions;\n\n const middlewareName = options.name;\n const 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 (\n req: FastifyRequest,\n reply: FastifyReply,\n done: HookHandlerDoneFunction,\n ) => {\n const reqUrl = safeURL(req.url);\n const url = reqUrl.pathname;\n\n // When serving from production server, scan for server files\n if (serverFunctionsMap.size === 0) {\n await scanForServerFiles();\n }\n\n // No need to continue when no handler provided\n if (!handler) {\n done();\n return;\n }\n\n // Path matching\n if (pathMatcher && !pathMatcher.test(url)) {\n done();\n return;\n }\n\n // rpcPrefix matching (boundary-safe via escaped regex)\n if (prefixRegex && !prefixRegex.test(url)) {\n done();\n return;\n }\n\n // Execute handler\n await handler(req, reply, done);\n };\n\n Object.defineProperty(middlewareHandler, \"name\", {\n value: name,\n });\n\n return middlewareHandler;\n};\n\n/**\n * Creates the Fastify 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 sends the JSON-serialized result.\n * @param initialOptions - Options including rpcPrefix for URL routing\n * @returns A Fastify preHandler hook function\n */\nexport const createRPCMiddleware: FastifyMiddlewareFn = (\n initialOptions = {},\n) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n { rpcPrefix: defaultRPCOptions.rpcPrefix },\n initialOptions,\n ) as FastifyMiddlewareOptions;\n\n // Hoist prefix regex (escaped) and the literal prefix-for-replace out of the\n // per-request handler to avoid regex injection and per-request compilation.\n const 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 (\n req: FastifyRequest,\n reply: FastifyReply,\n _done: HookHandlerDoneFunction,\n ) => {\n const reqUrl = safeURL(req.url);\n const url = reqUrl.pathname;\n\n // Defense-in-depth: validate prefix match via escaped regex even though\n // the outer createMiddleware gates on the same prefix already.\n // istanbul ignore if\n if (prefixRegex && !prefixRegex.test(url)) {\n return;\n }\n\n // 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 = req.headers.origin;\n if (origin && requestOrigin && requestOrigin !== origin) {\n reply.status(403).send({ error: REQUEST_FORBIDDEN });\n return;\n }\n\n const functionName = url.replace(prefixReplace, \"\");\n const serverFunction = serverFunctionsMap.get(functionName);\n\n if (!serverFunction) {\n reply.status(404).send({\n error: FUNCTION_NOT_FOUND,\n });\n return;\n }\n\n try {\n const method = serverFunction.options?.method || \"POST\";\n if (req.method.toUpperCase() !== method) {\n reply.status(405).send({ 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 reply.status(400).send({ 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 req.headers[\"content-type\"],\n )\n ) {\n reply.status(415).send({ error: UNSUPPORTED_MEDIA_TYPE });\n return;\n }\n const body = await readBody(req);\n args = Array.isArray(body.data)\n ? body.data as JsonValue[]\n : [body.data as JsonValue];\n }\n const requestEvent: RequestEvent = {\n request: req,\n response: reply,\n nativeEvent: req,\n locals: {},\n functionName,\n redirect: (location, status = 303) => {\n requestEvent.redirected = { location, status };\n fastifyRedirect(reply, 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 reply.header(name, value);\n }\n }\n reply.status(status).send(body);\n },\n };\n const { data: dataResult, cancel } = provideRequestContext(\n requestEvent,\n () => serverFunction.handler(...args),\n );\n const onClose = () => cancel(CLIENT_DISCONNECTED);\n\n req.raw.on(\"close\", onClose);\n const data = await dataResult;\n req.raw.off(\"close\", onClose);\n\n // istanbul ignore else\n if (\n !requestEvent.redirected &&\n !requestEvent.sent &&\n !reply.raw.headersSent\n ) {\n reply.status(200).send({ data });\n }\n } catch (err) {\n console.error(String(err));\n const isProduction = process.env.NODE_ENV === \"production\";\n reply.status(500).send(formatError(err, isProduction));\n }\n },\n });\n};\n"],"mappings":";;;AAYA,MAAa,oBAAsC;CACjD,WAAW;CACX,SAAS;CACT,aAAa;CACb,UAAU,KAAA;AACZ;AAEA,MAAa,2BAA2B;CACtC,WAAW,KAAA;CACX,MAAM,KAAA;CACN,QAAQ,KAAA;AACV;;;ACbA,MAAa,qBAAqB;AAElC,MAAa,qBAAqB;AAElC,MAAa,oBAAoB;AAEjC,MAAa,yBAAyB;AAEtC,MAAa,cAAc;AAI3B,MAAa,sBAAsB;;AAGnC,MAAa,wBAAwB,SACnC,wBAAwB,KAAK;;;;ACd/B,MAAM,aACJ,SACA,gBACA,SACG;CAEH,MAAM,gBAAgB,oBAAoB,cAAc;CACxD,QAAQ,QAAQ,cAAc,OAAO,SAAS,UAAU;EACtD,MAAM,aACJ,IAAI,SAAS,YAAY;GACvB,cAAc,SAAS,OAAO,OAAO;EACvC,CAAC;EACH,MAAM,KAAK;CACb,CAAC;CAED,KAAK;AACP;AAGA,MAAM,YAAY,GAAG,WAAW,EAC9B,MAAM,yBACR,CAAC;;;;;;;;ACpBD,eAAsB,UAAU,KAAsB;CAIpD,MAAM,EAAE,kBAAkB,MAAM,OAAO;CACvC,MAAM,EAAE,SAAS,UAAU,GAAG,YAAY,MAAM,cAAc;CAC9D,MAAM,IAAI,SAASA,WAAkB,OAAO;AAC9C;;;;;;;AAQA,SAAgB,WAAW,KAAsB,MAAqB;CACpE,IAAI,QAAQ,aAAa,OAAO,SAAS,UAAU;EACjD,MAAM,aACJ,IAAI,SAAS,YAAY;GACvB,KAAK,YAAY,QAAQ,KAAK,MAAM,KAAK,OAAO;EAClD,CAAC;EACH,MAAM,KAAK;CACb,CAAC;AACH;;;;;;;AAQA,MAAa,YACX,QACwB;CACxB,OAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,cAAc,IAAI,QAAQ,eAAe,EAAE,YAAY,KAAK;EAClE,MAAM,UAAU,IAAI;EAEpB,IAAI,YAAY,KAAA,GAAW;GACzB,MAAM,SAAS,YAAY,SAAS,MAAM;GAC1C,MAAM,cAAc,YAAY,SAAS,qBAAqB;GAC9D,MAAM,eAAe,YAAY,SAAS,YAAY;GACtD,QAAQ;IACN,aAAa,cACT,wBACA,SACA,qBACA,eACA,sCACA;IACJ,MAAM,cACD,UACD,SACA,UACA,eACC,UACD,OAAO,OAAO;GACpB,CAAe;GACf;EACF;EAEA,MAAM,mBAAmB,QAAkB;GACzC,MAAM,SAAS,MAAM,OAAO;GAC5B,IAAI,IAAI,OAAO,CAAC,QAAQ,MAAM;GAC9B,IAAI,IAAI,OAAO,CAAC,OAAO,KAAK;GAC5B,IAAI,IAAI,OAAO,CAAC,SAAS,OAAO;EAClC;EAEA,IAAI,OAAO;EAEX,MAAM,UAAU,UAAkB;GAChC,QAAQ,MAAM,SAAS;EACzB;EAEA,MAAM,cAAc;GAClB,gBAAgB;GAChB,MAAM,SAAS,YAAY,SAAS,MAAM;GAC1C,MAAM,cAAc,YAAY,SAAS,qBAAqB;GAC9D,MAAM,eAAe,YAAY,SAAS,YAAY;GACtD,IAAI;IACF,MAAM,OAAO,cACT,EAAE,KAAK,KAAK,IACZ,eACA,OAAO,YAAY,IAAI,gBAAgB,IAAI,CAAC,IAC5C,KAAK,MAAM,IAAI;IACnB,QAAQ;KACN,aAAa,cACT,wBACA,SACA,qBACA,eACA,sCACA;KACJ,MAAM,cAAe,OAAmC;IAC1D,CAAe;GACjB,SAAS,IAAI;IACX,QAAQ;KAAE,aAAa;KAAc,MAAM,OAAO,IAAI;IAAE,CAAC;GAC3D;EACF;EACA,MAAM,WAAW,QAAe;GAC9B,gBAAgB;GAChB,OAAO,GAAG;EACZ;EAEA,gBAAgB,IAAI;CACtB,CAAC;AACH;;;;;;;;;;AAWA,MAAa,YACX,OACA,UACA,SAAS,QACA;CACT,MAAM,SAAS,UAAU,MAAM;AACjC;;;ACvGA,IAAI,kBAAkB;AACtB,MAAM,kCAAkB,IAAI,IAAY;;;;;;;AAQxC,MAAa,oBAAyC,iBAAiB,CAAC,MAAM;CAC5E,MAAM,UAAU,OAAO,OACrB,CAAC,GACD,0BACA,cACF;CAEA,MAAM,iBAAiB,QAAQ;CAC/B,MAAM,YAAY,QAAQ;CAC1B,MAAM,OAAO,QAAQ;CACrB,MAAM,UAAU,QAAQ;CAExB,IAAI,OAAO;CACX,IAAI,CAAC,MAAM;EACT,OAAO,uBAAuB;EAC9B,mBAAmB;CACrB;CACA,IAAI,gBAAgB,IAAI,IAAI,GAC1B,MAAM,IAAI,MAAM,qBAAqB,IAAI,CAAC;CAE5C,gBAAgB,IAAI,IAAI;CAIxB,MAAM,cAA6B,YAC/B,IAAI,OAAO,KAAK,aAAa,SAAS,EAAE,EAAE,IAC1C;CACJ,MAAM,cAA6B,OAC9B,OAAO,SAAS,WAAW,IAAI,OAAO,IAAI,IAAI,OAC/C;CAEJ,MAAM,oBAAoB,OACxB,KACA,OACA,SACG;EAEH,MAAM,MADS,QAAQ,IAAI,GACV,CAAC,CAAC;EAGnB,IAAI,mBAAmB,SAAS,GAC9B,MAAM,mBAAmB;EAI3B,IAAI,CAAC,SAAS;GACZ,KAAK;GACL;EACF;EAGA,IAAI,eAAe,CAAC,YAAY,KAAK,GAAG,GAAG;GACzC,KAAK;GACL;EACF;EAGA,IAAI,eAAe,CAAC,YAAY,KAAK,GAAG,GAAG;GACzC,KAAK;GACL;EACF;EAGA,MAAM,QAAQ,KAAK,OAAO,IAAI;CAChC;CAEA,OAAO,eAAe,mBAAmB,QAAQ,EAC/C,OAAO,KACT,CAAC;CAED,OAAO;AACT;;;;;;;;AASA,MAAa,uBACX,iBAAiB,CAAC,MACf;CACH,MAAM,UAAU,OAAO,OACrB,CAAC,GACD,0BACA,EAAE,WAAW,kBAAkB,UAAU,GACzC,cACF;CAIA,MAAM,YAAY,QAAQ;CAC1B,MAAM,cAAc,YAChB,IAAI,OAAO,KAAK,aAAa,SAAS,EAAE,EAAE,IACzC;CACL,MAAM,gBAAgB,IAAI,UAAU;CAEpC,OAAO,iBAAiB;EACtB,GAAG;EACH,SAAS,OACP,KACA,OACA,UACG;GACH,MAAM,SAAS,QAAQ,IAAI,GAAG;GAC9B,MAAM,MAAM,OAAO;GAKnB,IAAI,eAAe,CAAC,YAAY,KAAK,GAAG,GACtC;GAMF,MAAM,SAAS,QAAQ;GACvB,MAAM,gBAAgB,IAAI,QAAQ;GAClC,IAAI,UAAU,iBAAiB,kBAAkB,QAAQ;IACvD,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,EAAE,OAAO,kBAAkB,CAAC;IACnD;GACF;GAEA,MAAM,eAAe,IAAI,QAAQ,eAAe,EAAE;GAClD,MAAM,iBAAiB,mBAAmB,IAAI,YAAY;GAE1D,IAAI,CAAC,gBAAgB;IACnB,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,EACrB,OAAO,mBACT,CAAC;IACD;GACF;GAEA,IAAI;IACF,MAAM,SAAS,eAAe,SAAS,UAAU;IACjD,IAAI,IAAI,OAAO,YAAY,MAAM,QAAQ;KACvC,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,EAAE,OAAO,mBAAmB,CAAC;KACpD;IACF;IAEA,IAAI,OAAoB,CAAC;IACzB,IAAI,WAAW,OAAO;KACpB,MAAM,MAAM,OAAO,aAAa,IAAI,MAAM;KAC1C,IAAI,KAAK;MACP,MAAM,SAAkB,KAAK,MAAM,GAAG;MACtC,IAAI,CAAC,MAAM,QAAQ,MAAM,GAAG;OAC1B,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,EAAE,OAAO,YAAY,CAAC;OAC7C;MACF;MACA,OAAO;KACT;IACF,OAAO;KAIL,IACE,uBACE,eAAe,SAAS,eAAe,oBACvC,IAAI,QAAQ,eACd,GACA;MACA,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,EAAE,OAAO,uBAAuB,CAAC;MACxD;KACF;KACA,MAAM,OAAO,MAAM,SAAS,GAAG;KAC/B,OAAO,MAAM,QAAQ,KAAK,IAAI,IAC1B,KAAK,OACL,CAAC,KAAK,IAAiB;IAC7B;IACA,MAAM,eAA6B;KACjC,SAAS;KACT,UAAU;KACV,aAAa;KACb,QAAQ,CAAC;KACT;KACA,WAAW,UAAU,SAAS,QAAQ;MACpC,aAAa,aAAa;OAAE;OAAU;MAAO;MAC7C,SAAgB,OAAO,UAAU,MAAM;KACzC;KACA,OAAO,QAAQ,MAAM,YAAY;MAC/B,aAAa,OAAO;OAAE;OAAQ;OAAM;MAAQ;MAC5C,IAAI,SACF,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,OAAO,GAChD,MAAM,OAAO,MAAM,KAAK;MAG5B,MAAM,OAAO,MAAM,CAAC,CAAC,KAAK,IAAI;KAChC;IACF;IACA,MAAM,EAAE,MAAM,YAAY,WAAW,sBACnC,oBACM,eAAe,QAAQ,GAAG,IAAI,CACtC;IACA,MAAM,gBAAgB,OAAO,mBAAmB;IAEhD,IAAI,IAAI,GAAG,SAAS,OAAO;IAC3B,MAAM,OAAO,MAAM;IACnB,IAAI,IAAI,IAAI,SAAS,OAAO;IAG5B,IACE,CAAC,aAAa,cACd,CAAC,aAAa,QACd,CAAC,MAAM,IAAI,aAEX,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,CAAC;GAEnC,SAAS,KAAK;IACZ,QAAQ,MAAM,OAAO,GAAG,CAAC;IACzB,MAAM,eAAe,QAAQ,IAAI,aAAa;IAC9C,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,YAAY,KAAK,YAAY,CAAC;GACvD;EACF;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"fastify.mjs","names":["fastifyRpcPlugin"],"sources":["../../src/options.ts","../../src/functionsMap.ts","../../src/constants.ts","../../src/fastify/plugin.ts","../../src/fastify/helpers.ts","../../src/fastify/createMiddleware.ts"],"sourcesContent":["import type {\n MiddlewareOptions,\n RpcPluginOptions,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\n\nexport const defaultServerFnOptions: 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","/** @module Fastify plugin. Exports the RPC plugin wrapped with `fastify-plugin` for lifecycle-compatible registration. */\n// inspired by https://github.com/royalswe/vike-fastify-boilerplate/blob/main/server/index.ts\nimport fp from \"fastify-plugin\";\nimport type { MiddlewareOptions } from \"../types.d.ts\";\nimport type {\n FastifyRPCPlugin,\n RegisteredFastifyRPCPlugin,\n} from \"./types.d.ts\";\nimport { createRPCMiddleware } from \"./createMiddleware.ts\";\n\nexport type { MiddlewareOptions };\n\nconst RpcPlugin: FastifyRPCPlugin = (\n fastify,\n initialOptions,\n done,\n) => {\n // Register RPC middleware as preHandler hook\n const rpcMiddleware = createRPCMiddleware(initialOptions);\n fastify.addHook(\"preHandler\", async (request, reply) => {\n const next = () =>\n new Promise((resolve) => {\n rpcMiddleware(request, reply, resolve);\n });\n await next();\n });\n\n done();\n};\n\n// Export the plugin wrapped with fastify-plugin\nconst rpcPlugin = fp(RpcPlugin, {\n name: \"uni-rpc-fastify-plugin\",\n}) as RegisteredFastifyRPCPlugin;\n\nexport { rpcPlugin as default };\n","// @thednp/rpc/src/fastify/helpers.ts\nimport type { FastifyReply, FastifyRequest } from \"fastify\";\nimport type { ViteDevServer } from \"vite\";\nimport type { FastifyInstance } from \"fastify\";\nimport type { Buffer } from \"node:buffer\";\nimport type { BodyResult, JsonValue } from \"../types.d.ts\";\nimport fastifyRpcPlugin from \"./plugin.ts\";\n\n/**\n * Convenience function to load RPC config and register the RPC plugin to a Fastify instance.\n * Dynamically imports loadRPCConfig and registers the fastify-rpc plugin.\n * @param app - Fastify instance\n */\nexport async function attachRPC(app: FastifyInstance) {\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 await app.register(fastifyRpcPlugin, options);\n}\n\n/**\n * Attaches Vite's dev server middlewares to a Fastify instance for development mode.\n * Uses an `onRequest` hook to delegate to Vite's connect-compatible middleware stack.\n * @param app - Fastify instance\n * @param vite - Running Vite dev server\n */\nexport function attachVite(app: FastifyInstance, vite: ViteDevServer) {\n app.addHook(\"onRequest\", async (request, reply) => {\n const next = () =>\n new Promise((resolve) => {\n vite.middlewares(request.raw, reply.raw, resolve);\n });\n await next();\n });\n}\n\n/**\n * Reads and parses the HTTP request body from a Fastify request.\n * If Fastify's body parser already consumed the stream, uses the pre-parsed body from `req.body`.\n * @param req - Fastify request object\n * @returns A promise resolving to the parsed body with its content type\n */\nexport const readBody = (\n req: FastifyRequest,\n): Promise<BodyResult> => {\n return new Promise((resolve, reject) => {\n const contentType = req.headers[\"content-type\"]?.toLowerCase() || \"\";\n const reqBody = req.body as JsonValue;\n\n if (reqBody !== undefined) {\n const isJSON = contentType.includes(\"json\");\n const isMultipart = contentType.includes(\"multipart/form-data\");\n const isUrlEncoded = contentType.includes(\"urlencoded\");\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 const toggleListeners = (add?: boolean) => {\n const method = add ? \"on\" : \"off\";\n req.raw[method](\"data\", onData);\n req.raw[method](\"end\", onEnd);\n req.raw[method](\"error\", onError);\n };\n\n let body = \"\";\n\n const onData = (chunk: Buffer) => {\n body += chunk.toString();\n };\n\n const onEnd = () => {\n toggleListeners();\n const isJSON = contentType.includes(\"json\");\n 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 (_e) {\n resolve({ contentType: \"text/plain\", data: String(body) });\n }\n };\n const onError = (err: Error) => {\n toggleListeners();\n reject(err);\n };\n\n toggleListeners(true);\n });\n};\n\n/**\n * Issues an HTTP redirect on a Fastify reply using the native\n * `reply.redirect(location, status)` API (Fastify v5 signature: destination\n * URL first, status code optional). Defaults to `303 See Other` for\n * convention (Post/Redirect/Get).\n * @param reply - Fastify reply object\n * @param location - The URL to redirect to\n * @param status - HTTP status code, defaults to 303\n */\nexport const redirect = (\n reply: FastifyReply,\n location: string,\n status = 303,\n): void => {\n reply.redirect(location, status);\n};\n","// src/fastify/createMiddleware.ts\nimport type {\n FastifyReply,\n FastifyRequest,\n HookHandlerDoneFunction,\n} from \"fastify\";\nimport type {\n FastifyMiddlewareFn,\n FastifyMiddlewareOptions,\n} from \"./types.d.ts\";\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 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, redirect as fastifyRedirect } from \"./helpers.ts\";\n\nlet middlewareCount = 0;\nconst middlewareStack = new Set<string>();\n\n/**\n * Creates a Fastify preHandler hook with optional path and rpcPrefix filtering.\n * Middleware names are deduplicated. Prefix and path regexes are compiled once at creation time.\n * @param initialOptions - Options for rpcPrefix, path matching, and the handler function\n * @returns A Fastify preHandler hook function\n */\nexport const createMiddleware: FastifyMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n initialOptions,\n ) as FastifyMiddlewareOptions;\n\n const middlewareName = options.name;\n 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 (\n req: FastifyRequest,\n reply: FastifyReply,\n done: HookHandlerDoneFunction,\n ) => {\n const reqUrl = safeURL(req.url);\n const url = reqUrl.pathname;\n\n // No need to continue when no handler provided\n if (!handler) {\n done();\n return;\n }\n\n // Path matching\n if (pathMatcher && !pathMatcher.test(url)) {\n done();\n return;\n }\n\n // rpcPrefix matching (boundary-safe via escaped regex)\n if (prefixRegex && !prefixRegex.test(url)) {\n done();\n return;\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 // Execute handler\n await handler(req, reply, done);\n };\n\n Object.defineProperty(middlewareHandler, \"name\", {\n value: name,\n });\n\n return middlewareHandler;\n};\n\n/**\n * Creates the Fastify 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 sends the JSON-serialized result.\n * @param initialOptions - Options including rpcPrefix for URL routing\n * @returns A Fastify preHandler hook function\n */\nexport const createRPCMiddleware: FastifyMiddlewareFn = (\n initialOptions = {},\n) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n { rpcPrefix: defaultRPCOptions.rpcPrefix },\n initialOptions,\n ) as FastifyMiddlewareOptions;\n\n // Hoist prefix regex (escaped) and the literal prefix-for-replace out of the\n // per-request handler to avoid regex injection and per-request compilation.\n const 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 (\n req: FastifyRequest,\n reply: FastifyReply,\n _done: HookHandlerDoneFunction,\n ) => {\n const reqUrl = safeURL(req.url);\n const url = reqUrl.pathname;\n\n // Defense-in-depth: validate prefix match via escaped regex even though\n // the outer createMiddleware gates on the same prefix already.\n // istanbul ignore if\n if (prefixRegex && !prefixRegex.test(url)) {\n return;\n }\n\n // 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 = req.headers.origin;\n if (origin && requestOrigin && requestOrigin !== origin) {\n reply.status(403).send({ 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 reply.status(404).send({\n error: FUNCTION_NOT_FOUND,\n });\n return;\n }\n\n try {\n const method = serverFunction.options?.method || \"POST\";\n if (req.method.toUpperCase() !== method) {\n reply.status(405).send({ 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 reply.status(400).send({ 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 req.headers[\"content-type\"],\n )\n ) {\n reply.status(415).send({ error: UNSUPPORTED_MEDIA_TYPE });\n return;\n }\n const body = await readBody(req);\n args = Array.isArray(body.data)\n ? body.data as JsonValue[]\n : [body.data as JsonValue];\n }\n const requestEvent: RequestEvent = {\n request: req,\n response: reply,\n nativeEvent: req,\n locals: {},\n functionName,\n redirect: (location, status = 303) => {\n requestEvent.redirected = { location, status };\n fastifyRedirect(reply, 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 reply.header(name, value);\n }\n }\n reply.status(status).send(body);\n },\n };\n const { data: dataResult, cancel } = provideRequestContext(\n requestEvent,\n () => serverFunction.handler(...args),\n );\n const onClose = () => cancel(CLIENT_DISCONNECTED);\n\n req.raw.on(\"close\", onClose);\n const data = await dataResult;\n req.raw.off(\"close\", onClose);\n\n // istanbul ignore else\n if (\n !requestEvent.redirected &&\n !requestEvent.sent &&\n !reply.raw.headersSent\n ) {\n reply.status(200).send({ data });\n }\n } catch (err) {\n console.error(String(err));\n const isProduction = process.env.NODE_ENV === \"production\";\n reply.status(500).send(formatError(err, isProduction));\n }\n },\n });\n};\n"],"mappings":";;AAcA,MAAa,oBAAsC;CACjD,WAAW;CACX,SAAS;CACT,aAAa;CACb,UAAU,KAAA;AACZ;AAEA,MAAa,2BAA8C;CACzD,WAAW,KAAA;CACX,MAAM,KAAA;CACN,QAAQ,KAAA;AACV;;;;;;;;;;;ACdA,MAAM,qBAAqB,OAAO,IAAI,yBAAyB;;;;;;AAO/D,MAAa,0BAGR,WACH,wCACI,IAAI,IAAI;;;;;;AAOd,MAAa,yBACX,WAC+B;CAC/B,IAAI,CAAC,wBAAwB,IAAI,MAAM,GACrC,wBAAwB,IAAI,wBAAQ,IAAI,IAAI,CAAC;CAE/C,OAAO,wBAAwB,IAAI,MAAM;AAC3C;;;AC3BA,MAAa,qBAAqB;AAElC,MAAa,qBAAqB;AAElC,MAAa,oBAAoB;AAEjC,MAAa,yBAAyB;AAEtC,MAAa,cAAc;AAI3B,MAAa,sBAAsB;;AAGnC,MAAa,wBAAwB,SACnC,wBAAwB,KAAK;;;;ACd/B,MAAM,aACJ,SACA,gBACA,SACG;CAEH,MAAM,gBAAgB,oBAAoB,cAAc;CACxD,QAAQ,QAAQ,cAAc,OAAO,SAAS,UAAU;EACtD,MAAM,aACJ,IAAI,SAAS,YAAY;GACvB,cAAc,SAAS,OAAO,OAAO;EACvC,CAAC;EACH,MAAM,KAAK;CACb,CAAC;CAED,KAAK;AACP;AAGA,MAAM,YAAY,GAAG,WAAW,EAC9B,MAAM,yBACR,CAAC;;;;;;;;ACpBD,eAAsB,UAAU,KAAsB;CAIpD,MAAM,EAAE,kBAAkB,MAAM,OAAO;CACvC,MAAM,EAAE,SAAS,UAAU,GAAG,YAAY,MAAM,cAAc;CAC9D,MAAM,IAAI,SAASA,WAAkB,OAAO;AAC9C;;;;;;;AAQA,SAAgB,WAAW,KAAsB,MAAqB;CACpE,IAAI,QAAQ,aAAa,OAAO,SAAS,UAAU;EACjD,MAAM,aACJ,IAAI,SAAS,YAAY;GACvB,KAAK,YAAY,QAAQ,KAAK,MAAM,KAAK,OAAO;EAClD,CAAC;EACH,MAAM,KAAK;CACb,CAAC;AACH;;;;;;;AAQA,MAAa,YACX,QACwB;CACxB,OAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,cAAc,IAAI,QAAQ,eAAe,EAAE,YAAY,KAAK;EAClE,MAAM,UAAU,IAAI;EAEpB,IAAI,YAAY,KAAA,GAAW;GACzB,MAAM,SAAS,YAAY,SAAS,MAAM;GAC1C,MAAM,cAAc,YAAY,SAAS,qBAAqB;GAC9D,MAAM,eAAe,YAAY,SAAS,YAAY;GACtD,QAAQ;IACN,aAAa,cACT,wBACA,SACA,qBACA,eACA,sCACA;IACJ,MAAM,cACD,UACD,SACA,UACA,eACC,UACD,OAAO,OAAO;GACpB,CAAe;GACf;EACF;EAEA,MAAM,mBAAmB,QAAkB;GACzC,MAAM,SAAS,MAAM,OAAO;GAC5B,IAAI,IAAI,OAAO,CAAC,QAAQ,MAAM;GAC9B,IAAI,IAAI,OAAO,CAAC,OAAO,KAAK;GAC5B,IAAI,IAAI,OAAO,CAAC,SAAS,OAAO;EAClC;EAEA,IAAI,OAAO;EAEX,MAAM,UAAU,UAAkB;GAChC,QAAQ,MAAM,SAAS;EACzB;EAEA,MAAM,cAAc;GAClB,gBAAgB;GAChB,MAAM,SAAS,YAAY,SAAS,MAAM;GAC1C,MAAM,cAAc,YAAY,SAAS,qBAAqB;GAC9D,MAAM,eAAe,YAAY,SAAS,YAAY;GACtD,IAAI;IACF,MAAM,OAAO,cACT,EAAE,KAAK,KAAK,IACZ,eACA,OAAO,YAAY,IAAI,gBAAgB,IAAI,CAAC,IAC5C,KAAK,MAAM,IAAI;IACnB,QAAQ;KACN,aAAa,cACT,wBACA,SACA,qBACA,eACA,sCACA;KACJ,MAAM,cAAe,OAAmC;IAC1D,CAAe;GACjB,SAAS,IAAI;IACX,QAAQ;KAAE,aAAa;KAAc,MAAM,OAAO,IAAI;IAAE,CAAC;GAC3D;EACF;EACA,MAAM,WAAW,QAAe;GAC9B,gBAAgB;GAChB,OAAO,GAAG;EACZ;EAEA,gBAAgB,IAAI;CACtB,CAAC;AACH;;;;;;;;;;AAWA,MAAa,YACX,OACA,UACA,SAAS,QACA;CACT,MAAM,SAAS,UAAU,MAAM;AACjC;;;AClGA,IAAI,kBAAkB;AACtB,MAAM,kCAAkB,IAAI,IAAY;;;;;;;AAQxC,MAAa,oBAAyC,iBAAiB,CAAC,MAAM;CAC5E,MAAM,UAAU,OAAO,OACrB,CAAC,GACD,0BACA,cACF;CAEA,MAAM,iBAAiB,QAAQ;CAC/B,IAAI,YAAY,QAAQ;CACxB,MAAM,OAAO,QAAQ;CACrB,MAAM,UAAU,QAAQ;CAExB,IAAI,OAAO;CACX,IAAI,CAAC,MAAM;EACT,OAAO,uBAAuB;EAC9B,mBAAmB;CACrB;CACA,IAAI,gBAAgB,IAAI,IAAI,GAC1B,MAAM,IAAI,MAAM,qBAAqB,IAAI,CAAC;CAE5C,gBAAgB,IAAI,IAAI;CAIxB,MAAM,cAA6B,YAC/B,IAAI,OAAO,KAAK,aAAa,SAAS,EAAE,EAAE,IAC1C;CACJ,MAAM,cAA6B,OAC9B,OAAO,SAAS,WAAW,IAAI,OAAO,IAAI,IAAI,OAC/C;CAEJ,MAAM,oBAAoB,OACxB,KACA,OACA,SACG;EAEH,MAAM,MADS,QAAQ,IAAI,GACV,CAAC,CAAC;EAGnB,IAAI,CAAC,SAAS;GACZ,KAAK;GACL;EACF;EAGA,IAAI,eAAe,CAAC,YAAY,KAAK,GAAG,GAAG;GACzC,KAAK;GACL;EACF;EAGA,IAAI,eAAe,CAAC,YAAY,KAAK,GAAG,GAAG;GACzC,KAAK;GACL;EACF;EAEA,YAAa,aAAA;EAGb,IAAI,sBAAsB,SAAS,CAAC,CAAC,SAAS,GAC5C,MAAM,mBAAmB;GACvB;GACA,aAAc,QACX;GACH,UAAW,QAA6C;EAC1D,CAAU;EAIZ,MAAM,QAAQ,KAAK,OAAO,IAAI;CAChC;CAEA,OAAO,eAAe,mBAAmB,QAAQ,EAC/C,OAAO,KACT,CAAC;CAED,OAAO;AACT;;;;;;;;AASA,MAAa,uBACX,iBAAiB,CAAC,MACf;CACH,MAAM,UAAU,OAAO,OACrB,CAAC,GACD,0BACA,EAAE,WAAW,kBAAkB,UAAU,GACzC,cACF;CAIA,MAAM,YAAY,QAAQ;CAC1B,MAAM,SAAS,aAAa,gBAAgB,KAAA;CAC5C,MAAM,cAAc,YAChB,IAAI,OAAO,KAAK,aAAa,SAAS,EAAE,EAAE,IACzC;CACL,MAAM,gBAAgB,IAAI,OAAO;CAEjC,OAAO,iBAAiB;EACtB,GAAG;EACH,SAAS,OACP,KACA,OACA,UACG;GACH,MAAM,SAAS,QAAQ,IAAI,GAAG;GAC9B,MAAM,MAAM,OAAO;GAKnB,IAAI,eAAe,CAAC,YAAY,KAAK,GAAG,GACtC;GAMF,MAAM,SAAS,QAAQ;GACvB,MAAM,gBAAgB,IAAI,QAAQ;GAClC,IAAI,UAAU,iBAAiB,kBAAkB,QAAQ;IACvD,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,EAAE,OAAO,kBAAkB,CAAC;IACnD;GACF;GAEA,MAAM,eAAe,IAAI,QAAQ,eAAe,EAAE;GAClD,MAAM,iBAAiB,sBAAsB,MAAM,CAAC,CAAC,IAAI,YAAY;GAErE,IAAI,CAAC,gBAAgB;IACnB,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,EACrB,OAAO,mBACT,CAAC;IACD;GACF;GAEA,IAAI;IACF,MAAM,SAAS,eAAe,SAAS,UAAU;IACjD,IAAI,IAAI,OAAO,YAAY,MAAM,QAAQ;KACvC,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,EAAE,OAAO,mBAAmB,CAAC;KACpD;IACF;IAEA,IAAI,OAAoB,CAAC;IACzB,IAAI,WAAW,OAAO;KACpB,MAAM,MAAM,OAAO,aAAa,IAAI,MAAM;KAC1C,IAAI,KAAK;MACP,MAAM,SAAkB,KAAK,MAAM,GAAG;MACtC,IAAI,CAAC,MAAM,QAAQ,MAAM,GAAG;OAC1B,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,EAAE,OAAO,YAAY,CAAC;OAC7C;MACF;MACA,OAAO;KACT;IACF,OAAO;KAIL,IACE,uBACE,eAAe,SAAS,eAAe,oBACvC,IAAI,QAAQ,eACd,GACA;MACA,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,EAAE,OAAO,uBAAuB,CAAC;MACxD;KACF;KACA,MAAM,OAAO,MAAM,SAAS,GAAG;KAC/B,OAAO,MAAM,QAAQ,KAAK,IAAI,IAC1B,KAAK,OACL,CAAC,KAAK,IAAiB;IAC7B;IACA,MAAM,eAA6B;KACjC,SAAS;KACT,UAAU;KACV,aAAa;KACb,QAAQ,CAAC;KACT;KACA,WAAW,UAAU,SAAS,QAAQ;MACpC,aAAa,aAAa;OAAE;OAAU;MAAO;MAC7C,SAAgB,OAAO,UAAU,MAAM;KACzC;KACA,OAAO,QAAQ,MAAM,YAAY;MAC/B,aAAa,OAAO;OAAE;OAAQ;OAAM;MAAQ;MAC5C,IAAI,SACF,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,OAAO,GAChD,MAAM,OAAO,MAAM,KAAK;MAG5B,MAAM,OAAO,MAAM,CAAC,CAAC,KAAK,IAAI;KAChC;IACF;IACA,MAAM,EAAE,MAAM,YAAY,WAAW,sBACnC,oBACM,eAAe,QAAQ,GAAG,IAAI,CACtC;IACA,MAAM,gBAAgB,OAAO,mBAAmB;IAEhD,IAAI,IAAI,GAAG,SAAS,OAAO;IAC3B,MAAM,OAAO,MAAM;IACnB,IAAI,IAAI,IAAI,SAAS,OAAO;IAG5B,IACE,CAAC,aAAa,cACd,CAAC,aAAa,QACd,CAAC,MAAM,IAAI,aAEX,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,CAAC;GAEnC,SAAS,KAAK;IACZ,QAAQ,MAAM,OAAO,GAAG,CAAC;IACzB,MAAM,eAAe,QAAQ,IAAI,aAAa;IAC9C,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,YAAY,KAAK,YAAY,CAAC;GACvD;EACF;CACF,CAAC;AACH"}
@@ -173,6 +173,19 @@ interface MiddlewareOptions<A extends RpcPluginOptions["adapter"] = "express"> {
173
173
  * When unset (default), no origin validation is performed.
174
174
  */
175
175
  origin?: string;
176
+ /**
177
+ * Server file matching mode. Use `"exact"` for `server.ts|js|mjs|mts`
178
+ * names, or `"glob"` to match `**\/*.server.{ts,js,mjs,mts}` inside the
179
+ * scan root. Only used for the lazy production scan when the middleware
180
+ * populates its prefix map on first request.
181
+ * @default "exact"
182
+ */
183
+ serverFiles?: "exact" | "glob";
184
+ /**
185
+ * Root directory for scanning server files. Defaults to `<root>/src/api`.
186
+ * Only used for the lazy production scan.
187
+ */
188
+ scanRoot?: string;
176
189
  /**
177
190
  * Async handler for request processing.
178
191
  * Core middleware function that processes incoming requests.
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.mts","names":[],"sources":["../../../../src/express/types.d.ts","../../../../src/hono/types.d.ts","../../../../src/fastify/types.d.ts","../../../../src/koa/types.d.ts","../../../../src/h3/types.d.ts","../../../../src/types.d.ts","../../../../src/fastify/plugin.ts"],"mappings":";;;;;;;;;;;;;;;;UA+BiB;;;;;;;EAOf,UACE,KAAK,kBAAkB,SACvB,KAAK,iBAAiB,YACtB,MAAM,QAAQ,eAAe,iBAC1B;;;;;;;UCzBU;;EAEf,SAAS;;;;;;;KCEC,uBAAuB;;;;KAKvB,6BAA6B,WAAW;;;;UA4BnC;;;;;;;EAOf,UACE,KAAK,gBACL,KAAK,cACL,MAAM,4BACH;;;;;;;UCzCU;;;;;;EAMf,UAAU,KAAK,SAAS,MAAM,SAAS;;;;;;;UClBxB;;;;;;EAMf,SAAS;;;;;;;;UCKM;;EAEf,SAAS;;EAET,MAAM;;EAEN,SAAS;;EAET,KAAK;;EAEL,IAAI;;;;;;;;UA8MW;;;;;;;;;;;EAWf;;;;;;;EAQA;;;;;;;EAQA;;;;;;;EAQA;;UAGe,kBACf,UAAU;;;;EAKV;;;;;;;;;;;;EAaA,gBAAgB;;;;;;;;;;EAWhB;;;;;;;;EASA;;;;;;;;;;;;;;;;;;EAmBA,UAAU,eAAe;;;;cChTrB,WAEA"}
1
+ {"version":3,"file":"plugin.d.mts","names":[],"sources":["../../../../src/express/types.d.ts","../../../../src/hono/types.d.ts","../../../../src/fastify/types.d.ts","../../../../src/koa/types.d.ts","../../../../src/h3/types.d.ts","../../../../src/types.d.ts","../../../../src/fastify/plugin.ts"],"mappings":";;;;;;;;;;;;;;;;UA+BiB;;;;;;;EAOf,UACE,KAAK,kBAAkB,SACvB,KAAK,iBAAiB,YACtB,MAAM,QAAQ,eAAe,iBAC1B;;;;;;;UCzBU;;EAEf,SAAS;;;;;;;KCEC,uBAAuB;;;;KAKvB,6BAA6B,WAAW;;;;UA4BnC;;;;;;;EAOf,UACE,KAAK,gBACL,KAAK,cACL,MAAM,4BACH;;;;;;;UCzCU;;;;;;EAMf,UAAU,KAAK,SAAS,MAAM,SAAS;;;;;;;UClBxB;;;;;;EAMf,SAAS;;;;;;;;UCKM;;EAEf,SAAS;;EAET,MAAM;;EAEN,SAAS;;EAET,KAAK;;EAEL,IAAI;;;;;;;;UAqNW;;;;;;;;;;;EAWf;;;;;;;EAQA;;;;;;;EAQA;;;;;;;EAQA;;UAGe,kBACf,UAAU;;;;EAKV;;;;;;;;;;;;EAaA,gBAAgB;;;;;;;;;;EAWhB;;;;;;;;EASA;;;;;;;;EASA;;;;;EAMA;;;;;;;;;;;;;;;;;;EAmBA,UAAU,eAAe;;;;cCtUrB,WAEA"}
@@ -1,6 +1,5 @@
1
1
  import fp from "fastify-plugin";
2
- import { escapeRegExp, formatError, hasContentTypeMismatch, provideRequestContext, safeURL, scanForServerFiles, serverFunctionsMap } from "@thednp/rpc/server";
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,6 +12,32 @@ 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";
@@ -104,7 +129,7 @@ const middlewareStack = /* @__PURE__ */ new Set();
104
129
  const createMiddleware = (initialOptions = {}) => {
105
130
  const options = Object.assign({}, defaultMiddlewareOptions, initialOptions);
106
131
  const middlewareName = options.name;
107
- const rpcPrefix = options.rpcPrefix;
132
+ let rpcPrefix = options.rpcPrefix;
108
133
  const path = options.path;
109
134
  const handler = options.handler;
110
135
  let name = middlewareName;
@@ -118,7 +143,6 @@ const createMiddleware = (initialOptions = {}) => {
118
143
  const pathMatcher = path ? typeof path === "string" ? new RegExp(path) : path : null;
119
144
  const middlewareHandler = async (req, reply, done) => {
120
145
  const url = safeURL(req.url).pathname;
121
- if (serverFunctionsMap.size === 0) await scanForServerFiles();
122
146
  if (!handler) {
123
147
  done();
124
148
  return;
@@ -131,6 +155,12 @@ const createMiddleware = (initialOptions = {}) => {
131
155
  done();
132
156
  return;
133
157
  }
158
+ rpcPrefix = rpcPrefix ?? "__rpc";
159
+ if (getFunctionsForPrefix(rpcPrefix).size === 0) await scanForServerFiles({
160
+ rpcPrefix,
161
+ serverFiles: options.serverFiles,
162
+ scanRoot: options.scanRoot
163
+ });
134
164
  await handler(req, reply, done);
135
165
  };
136
166
  Object.defineProperty(middlewareHandler, "name", { value: name });
@@ -146,8 +176,9 @@ const createMiddleware = (initialOptions = {}) => {
146
176
  const createRPCMiddleware = (initialOptions = {}) => {
147
177
  const options = Object.assign({}, defaultMiddlewareOptions, { rpcPrefix: defaultRPCOptions.rpcPrefix }, initialOptions);
148
178
  const rpcPrefix = options.rpcPrefix;
179
+ const prefix = rpcPrefix || getGlobalPrefix() || "__rpc";
149
180
  const prefixRegex = rpcPrefix ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`) : null;
150
- const prefixReplace = `/${rpcPrefix}/`;
181
+ const prefixReplace = `/${prefix}/`;
151
182
  return createMiddleware({
152
183
  ...options,
153
184
  handler: async (req, reply, _done) => {
@@ -161,7 +192,7 @@ const createRPCMiddleware = (initialOptions = {}) => {
161
192
  return;
162
193
  }
163
194
  const functionName = url.replace(prefixReplace, "");
164
- const serverFunction = serverFunctionsMap.get(functionName);
195
+ const serverFunction = getFunctionsForPrefix(prefix).get(functionName);
165
196
  if (!serverFunction) {
166
197
  reply.status(404).send({ error: FUNCTION_NOT_FOUND });
167
198
  return;
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.mjs","names":[],"sources":["../../../../src/options.ts","../../../../src/constants.ts","../../../../src/fastify/helpers.ts","../../../../src/fastify/createMiddleware.ts","../../../../src/fastify/plugin.ts"],"sourcesContent":["import type {\n MiddlewareOptions,\n RpcPluginOptions,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\n\nexport const defaultServerFnOptions = {\n contentType: \"application/json\",\n 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 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","// @thednp/rpc/src/fastify/helpers.ts\nimport type { FastifyReply, FastifyRequest } from \"fastify\";\nimport type { ViteDevServer } from \"vite\";\nimport type { FastifyInstance } from \"fastify\";\nimport type { Buffer } from \"node:buffer\";\nimport type { BodyResult, JsonValue } from \"../types.d.ts\";\nimport fastifyRpcPlugin from \"./plugin.ts\";\n\n/**\n * Convenience function to load RPC config and register the RPC plugin to a Fastify instance.\n * Dynamically imports loadRPCConfig and registers the fastify-rpc plugin.\n * @param app - Fastify instance\n */\nexport async function attachRPC(app: FastifyInstance) {\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 await app.register(fastifyRpcPlugin, options);\n}\n\n/**\n * Attaches Vite's dev server middlewares to a Fastify instance for development mode.\n * Uses an `onRequest` hook to delegate to Vite's connect-compatible middleware stack.\n * @param app - Fastify instance\n * @param vite - Running Vite dev server\n */\nexport function attachVite(app: FastifyInstance, vite: ViteDevServer) {\n app.addHook(\"onRequest\", async (request, reply) => {\n const next = () =>\n new Promise((resolve) => {\n vite.middlewares(request.raw, reply.raw, resolve);\n });\n await next();\n });\n}\n\n/**\n * Reads and parses the HTTP request body from a Fastify request.\n * If Fastify's body parser already consumed the stream, uses the pre-parsed body from `req.body`.\n * @param req - Fastify request object\n * @returns A promise resolving to the parsed body with its content type\n */\nexport const readBody = (\n req: FastifyRequest,\n): Promise<BodyResult> => {\n return new Promise((resolve, reject) => {\n const contentType = req.headers[\"content-type\"]?.toLowerCase() || \"\";\n const reqBody = req.body as JsonValue;\n\n if (reqBody !== undefined) {\n const isJSON = contentType.includes(\"json\");\n const isMultipart = contentType.includes(\"multipart/form-data\");\n const isUrlEncoded = contentType.includes(\"urlencoded\");\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 const toggleListeners = (add?: boolean) => {\n const method = add ? \"on\" : \"off\";\n req.raw[method](\"data\", onData);\n req.raw[method](\"end\", onEnd);\n req.raw[method](\"error\", onError);\n };\n\n let body = \"\";\n\n const onData = (chunk: Buffer) => {\n body += chunk.toString();\n };\n\n const onEnd = () => {\n toggleListeners();\n const isJSON = contentType.includes(\"json\");\n 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 (_e) {\n resolve({ contentType: \"text/plain\", data: String(body) });\n }\n };\n const onError = (err: Error) => {\n toggleListeners();\n reject(err);\n };\n\n toggleListeners(true);\n });\n};\n\n/**\n * Issues an HTTP redirect on a Fastify reply using the native\n * `reply.redirect(location, status)` API (Fastify v5 signature: destination\n * URL first, status code optional). Defaults to `303 See Other` for\n * convention (Post/Redirect/Get).\n * @param reply - Fastify reply object\n * @param location - The URL to redirect to\n * @param status - HTTP status code, defaults to 303\n */\nexport const redirect = (\n reply: FastifyReply,\n location: string,\n status = 303,\n): void => {\n reply.redirect(location, status);\n};\n","// src/fastify/createMiddleware.ts\nimport type {\n FastifyReply,\n FastifyRequest,\n HookHandlerDoneFunction,\n} from \"fastify\";\nimport type {\n FastifyMiddlewareFn,\n FastifyMiddlewareOptions,\n} from \"./types.d.ts\";\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 { defaultMiddlewareOptions, defaultRPCOptions } 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, redirect as fastifyRedirect } from \"./helpers.ts\";\n\nlet middlewareCount = 0;\nconst middlewareStack = new Set<string>();\n\n/**\n * Creates a Fastify preHandler hook with optional path and rpcPrefix filtering.\n * Middleware names are deduplicated. Prefix and path regexes are compiled once at creation time.\n * @param initialOptions - Options for rpcPrefix, path matching, and the handler function\n * @returns A Fastify preHandler hook function\n */\nexport const createMiddleware: FastifyMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n initialOptions,\n ) as FastifyMiddlewareOptions;\n\n const middlewareName = options.name;\n const 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 (\n req: FastifyRequest,\n reply: FastifyReply,\n done: HookHandlerDoneFunction,\n ) => {\n const reqUrl = safeURL(req.url);\n const url = reqUrl.pathname;\n\n // When serving from production server, scan for server files\n if (serverFunctionsMap.size === 0) {\n await scanForServerFiles();\n }\n\n // No need to continue when no handler provided\n if (!handler) {\n done();\n return;\n }\n\n // Path matching\n if (pathMatcher && !pathMatcher.test(url)) {\n done();\n return;\n }\n\n // rpcPrefix matching (boundary-safe via escaped regex)\n if (prefixRegex && !prefixRegex.test(url)) {\n done();\n return;\n }\n\n // Execute handler\n await handler(req, reply, done);\n };\n\n Object.defineProperty(middlewareHandler, \"name\", {\n value: name,\n });\n\n return middlewareHandler;\n};\n\n/**\n * Creates the Fastify 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 sends the JSON-serialized result.\n * @param initialOptions - Options including rpcPrefix for URL routing\n * @returns A Fastify preHandler hook function\n */\nexport const createRPCMiddleware: FastifyMiddlewareFn = (\n initialOptions = {},\n) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n { rpcPrefix: defaultRPCOptions.rpcPrefix },\n initialOptions,\n ) as FastifyMiddlewareOptions;\n\n // Hoist prefix regex (escaped) and the literal prefix-for-replace out of the\n // per-request handler to avoid regex injection and per-request compilation.\n const 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 (\n req: FastifyRequest,\n reply: FastifyReply,\n _done: HookHandlerDoneFunction,\n ) => {\n const reqUrl = safeURL(req.url);\n const url = reqUrl.pathname;\n\n // Defense-in-depth: validate prefix match via escaped regex even though\n // the outer createMiddleware gates on the same prefix already.\n // istanbul ignore if\n if (prefixRegex && !prefixRegex.test(url)) {\n return;\n }\n\n // 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 = req.headers.origin;\n if (origin && requestOrigin && requestOrigin !== origin) {\n reply.status(403).send({ error: REQUEST_FORBIDDEN });\n return;\n }\n\n const functionName = url.replace(prefixReplace, \"\");\n const serverFunction = serverFunctionsMap.get(functionName);\n\n if (!serverFunction) {\n reply.status(404).send({\n error: FUNCTION_NOT_FOUND,\n });\n return;\n }\n\n try {\n const method = serverFunction.options?.method || \"POST\";\n if (req.method.toUpperCase() !== method) {\n reply.status(405).send({ 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 reply.status(400).send({ 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 req.headers[\"content-type\"],\n )\n ) {\n reply.status(415).send({ error: UNSUPPORTED_MEDIA_TYPE });\n return;\n }\n const body = await readBody(req);\n args = Array.isArray(body.data)\n ? body.data as JsonValue[]\n : [body.data as JsonValue];\n }\n const requestEvent: RequestEvent = {\n request: req,\n response: reply,\n nativeEvent: req,\n locals: {},\n functionName,\n redirect: (location, status = 303) => {\n requestEvent.redirected = { location, status };\n fastifyRedirect(reply, 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 reply.header(name, value);\n }\n }\n reply.status(status).send(body);\n },\n };\n const { data: dataResult, cancel } = provideRequestContext(\n requestEvent,\n () => serverFunction.handler(...args),\n );\n const onClose = () => cancel(CLIENT_DISCONNECTED);\n\n req.raw.on(\"close\", onClose);\n const data = await dataResult;\n req.raw.off(\"close\", onClose);\n\n // istanbul ignore else\n if (\n !requestEvent.redirected &&\n !requestEvent.sent &&\n !reply.raw.headersSent\n ) {\n reply.status(200).send({ data });\n }\n } catch (err) {\n console.error(String(err));\n const isProduction = process.env.NODE_ENV === \"production\";\n reply.status(500).send(formatError(err, isProduction));\n }\n },\n });\n};\n","/** @module Fastify plugin. Exports the RPC plugin wrapped with `fastify-plugin` for lifecycle-compatible registration. */\n// inspired by https://github.com/royalswe/vike-fastify-boilerplate/blob/main/server/index.ts\nimport fp from \"fastify-plugin\";\nimport type { MiddlewareOptions } from \"../types.d.ts\";\nimport type {\n FastifyRPCPlugin,\n RegisteredFastifyRPCPlugin,\n} from \"./types.d.ts\";\nimport { createRPCMiddleware } from \"./createMiddleware.ts\";\n\nexport type { MiddlewareOptions };\n\nconst RpcPlugin: FastifyRPCPlugin = (\n fastify,\n initialOptions,\n done,\n) => {\n // Register RPC middleware as preHandler hook\n const rpcMiddleware = createRPCMiddleware(initialOptions);\n fastify.addHook(\"preHandler\", async (request, reply) => {\n const next = () =>\n new Promise((resolve) => {\n rpcMiddleware(request, reply, resolve);\n });\n await next();\n });\n\n done();\n};\n\n// Export the plugin wrapped with fastify-plugin\nconst rpcPlugin = fp(RpcPlugin, {\n name: \"uni-rpc-fastify-plugin\",\n}) as RegisteredFastifyRPCPlugin;\n\nexport { rpcPlugin as default };\n"],"mappings":"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"}
1
+ {"version":3,"file":"plugin.mjs","names":[],"sources":["../../../../src/options.ts","../../../../src/functionsMap.ts","../../../../src/constants.ts","../../../../src/fastify/helpers.ts","../../../../src/fastify/createMiddleware.ts","../../../../src/fastify/plugin.ts"],"sourcesContent":["import type {\n MiddlewareOptions,\n RpcPluginOptions,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\n\nexport const defaultServerFnOptions: 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","// @thednp/rpc/src/fastify/helpers.ts\nimport type { FastifyReply, FastifyRequest } from \"fastify\";\nimport type { ViteDevServer } from \"vite\";\nimport type { FastifyInstance } from \"fastify\";\nimport type { Buffer } from \"node:buffer\";\nimport type { BodyResult, JsonValue } from \"../types.d.ts\";\nimport fastifyRpcPlugin from \"./plugin.ts\";\n\n/**\n * Convenience function to load RPC config and register the RPC plugin to a Fastify instance.\n * Dynamically imports loadRPCConfig and registers the fastify-rpc plugin.\n * @param app - Fastify instance\n */\nexport async function attachRPC(app: FastifyInstance) {\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 await app.register(fastifyRpcPlugin, options);\n}\n\n/**\n * Attaches Vite's dev server middlewares to a Fastify instance for development mode.\n * Uses an `onRequest` hook to delegate to Vite's connect-compatible middleware stack.\n * @param app - Fastify instance\n * @param vite - Running Vite dev server\n */\nexport function attachVite(app: FastifyInstance, vite: ViteDevServer) {\n app.addHook(\"onRequest\", async (request, reply) => {\n const next = () =>\n new Promise((resolve) => {\n vite.middlewares(request.raw, reply.raw, resolve);\n });\n await next();\n });\n}\n\n/**\n * Reads and parses the HTTP request body from a Fastify request.\n * If Fastify's body parser already consumed the stream, uses the pre-parsed body from `req.body`.\n * @param req - Fastify request object\n * @returns A promise resolving to the parsed body with its content type\n */\nexport const readBody = (\n req: FastifyRequest,\n): Promise<BodyResult> => {\n return new Promise((resolve, reject) => {\n const contentType = req.headers[\"content-type\"]?.toLowerCase() || \"\";\n const reqBody = req.body as JsonValue;\n\n if (reqBody !== undefined) {\n const isJSON = contentType.includes(\"json\");\n const isMultipart = contentType.includes(\"multipart/form-data\");\n const isUrlEncoded = contentType.includes(\"urlencoded\");\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 const toggleListeners = (add?: boolean) => {\n const method = add ? \"on\" : \"off\";\n req.raw[method](\"data\", onData);\n req.raw[method](\"end\", onEnd);\n req.raw[method](\"error\", onError);\n };\n\n let body = \"\";\n\n const onData = (chunk: Buffer) => {\n body += chunk.toString();\n };\n\n const onEnd = () => {\n toggleListeners();\n const isJSON = contentType.includes(\"json\");\n 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 (_e) {\n resolve({ contentType: \"text/plain\", data: String(body) });\n }\n };\n const onError = (err: Error) => {\n toggleListeners();\n reject(err);\n };\n\n toggleListeners(true);\n });\n};\n\n/**\n * Issues an HTTP redirect on a Fastify reply using the native\n * `reply.redirect(location, status)` API (Fastify v5 signature: destination\n * URL first, status code optional). Defaults to `303 See Other` for\n * convention (Post/Redirect/Get).\n * @param reply - Fastify reply object\n * @param location - The URL to redirect to\n * @param status - HTTP status code, defaults to 303\n */\nexport const redirect = (\n reply: FastifyReply,\n location: string,\n status = 303,\n): void => {\n reply.redirect(location, status);\n};\n","// src/fastify/createMiddleware.ts\nimport type {\n FastifyReply,\n FastifyRequest,\n HookHandlerDoneFunction,\n} from \"fastify\";\nimport type {\n FastifyMiddlewareFn,\n FastifyMiddlewareOptions,\n} from \"./types.d.ts\";\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 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, redirect as fastifyRedirect } from \"./helpers.ts\";\n\nlet middlewareCount = 0;\nconst middlewareStack = new Set<string>();\n\n/**\n * Creates a Fastify preHandler hook with optional path and rpcPrefix filtering.\n * Middleware names are deduplicated. Prefix and path regexes are compiled once at creation time.\n * @param initialOptions - Options for rpcPrefix, path matching, and the handler function\n * @returns A Fastify preHandler hook function\n */\nexport const createMiddleware: FastifyMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n initialOptions,\n ) as FastifyMiddlewareOptions;\n\n const middlewareName = options.name;\n 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 (\n req: FastifyRequest,\n reply: FastifyReply,\n done: HookHandlerDoneFunction,\n ) => {\n const reqUrl = safeURL(req.url);\n const url = reqUrl.pathname;\n\n // No need to continue when no handler provided\n if (!handler) {\n done();\n return;\n }\n\n // Path matching\n if (pathMatcher && !pathMatcher.test(url)) {\n done();\n return;\n }\n\n // rpcPrefix matching (boundary-safe via escaped regex)\n if (prefixRegex && !prefixRegex.test(url)) {\n done();\n return;\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 // Execute handler\n await handler(req, reply, done);\n };\n\n Object.defineProperty(middlewareHandler, \"name\", {\n value: name,\n });\n\n return middlewareHandler;\n};\n\n/**\n * Creates the Fastify 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 sends the JSON-serialized result.\n * @param initialOptions - Options including rpcPrefix for URL routing\n * @returns A Fastify preHandler hook function\n */\nexport const createRPCMiddleware: FastifyMiddlewareFn = (\n initialOptions = {},\n) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n { rpcPrefix: defaultRPCOptions.rpcPrefix },\n initialOptions,\n ) as FastifyMiddlewareOptions;\n\n // Hoist prefix regex (escaped) and the literal prefix-for-replace out of the\n // per-request handler to avoid regex injection and per-request compilation.\n const 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 (\n req: FastifyRequest,\n reply: FastifyReply,\n _done: HookHandlerDoneFunction,\n ) => {\n const reqUrl = safeURL(req.url);\n const url = reqUrl.pathname;\n\n // Defense-in-depth: validate prefix match via escaped regex even though\n // the outer createMiddleware gates on the same prefix already.\n // istanbul ignore if\n if (prefixRegex && !prefixRegex.test(url)) {\n return;\n }\n\n // 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 = req.headers.origin;\n if (origin && requestOrigin && requestOrigin !== origin) {\n reply.status(403).send({ 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 reply.status(404).send({\n error: FUNCTION_NOT_FOUND,\n });\n return;\n }\n\n try {\n const method = serverFunction.options?.method || \"POST\";\n if (req.method.toUpperCase() !== method) {\n reply.status(405).send({ 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 reply.status(400).send({ 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 req.headers[\"content-type\"],\n )\n ) {\n reply.status(415).send({ error: UNSUPPORTED_MEDIA_TYPE });\n return;\n }\n const body = await readBody(req);\n args = Array.isArray(body.data)\n ? body.data as JsonValue[]\n : [body.data as JsonValue];\n }\n const requestEvent: RequestEvent = {\n request: req,\n response: reply,\n nativeEvent: req,\n locals: {},\n functionName,\n redirect: (location, status = 303) => {\n requestEvent.redirected = { location, status };\n fastifyRedirect(reply, 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 reply.header(name, value);\n }\n }\n reply.status(status).send(body);\n },\n };\n const { data: dataResult, cancel } = provideRequestContext(\n requestEvent,\n () => serverFunction.handler(...args),\n );\n const onClose = () => cancel(CLIENT_DISCONNECTED);\n\n req.raw.on(\"close\", onClose);\n const data = await dataResult;\n req.raw.off(\"close\", onClose);\n\n // istanbul ignore else\n if (\n !requestEvent.redirected &&\n !requestEvent.sent &&\n !reply.raw.headersSent\n ) {\n reply.status(200).send({ data });\n }\n } catch (err) {\n console.error(String(err));\n const isProduction = process.env.NODE_ENV === \"production\";\n reply.status(500).send(formatError(err, isProduction));\n }\n },\n });\n};\n","/** @module Fastify plugin. Exports the RPC plugin wrapped with `fastify-plugin` for lifecycle-compatible registration. */\n// inspired by https://github.com/royalswe/vike-fastify-boilerplate/blob/main/server/index.ts\nimport fp from \"fastify-plugin\";\nimport type { MiddlewareOptions } from \"../types.d.ts\";\nimport type {\n FastifyRPCPlugin,\n RegisteredFastifyRPCPlugin,\n} from \"./types.d.ts\";\nimport { createRPCMiddleware } from \"./createMiddleware.ts\";\n\nexport type { MiddlewareOptions };\n\nconst RpcPlugin: FastifyRPCPlugin = (\n fastify,\n initialOptions,\n done,\n) => {\n // Register RPC middleware as preHandler hook\n const rpcMiddleware = createRPCMiddleware(initialOptions);\n fastify.addHook(\"preHandler\", async (request, reply) => {\n const next = () =>\n new Promise((resolve) => {\n rpcMiddleware(request, reply, resolve);\n });\n await next();\n });\n\n done();\n};\n\n// Export the plugin wrapped with fastify-plugin\nconst rpcPlugin = fp(RpcPlugin, {\n name: \"uni-rpc-fastify-plugin\",\n}) as RegisteredFastifyRPCPlugin;\n\nexport { rpcPlugin as default };\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"h3.d.mts","names":[],"sources":["../../src/h3/types.d.ts","../../src/h3/createMiddleware.ts","../../src/h3/helpers.ts"],"mappings":";;;;;;;KAOY,sBAAsB;;;;UAKjB;;;;;;EAMf,SAAS;;;;;;KAOC,kBAAkB,UAAU,oCACtC,iBAAiB,QAAQ,yBACtB;;;;KAKO,QAAQ;;;;KAKR,kBAAkB;EAAY;;;;;;;;;;;cCF7B,kBAAkB;;;;;;;;cAoElB,qBAAqB;;;;;;;;iBCzFZ,UAAU,KAAK,QAAK;;;;;;;cAgB7B,aAAU,KAAS,OAAK,MAAQ;;;;;;;;cAWhC,iBAAc,MAAU,kBAAgB;;;;;;;cA6ExC,WAAQ,OAAiB,YAAU,QAAQ;;;;;;;;;cAmC3C,WAAQ,kBACH,oBAEf"}
1
+ {"version":3,"file":"h3.d.mts","names":[],"sources":["../../src/h3/types.d.ts","../../src/h3/createMiddleware.ts","../../src/h3/helpers.ts"],"mappings":";;;;;;;KAOY,sBAAsB;;;;UAKjB;;;;;;EAMf,SAAS;;;;;;KAOC,kBAAkB,UAAU,oCACtC,iBAAiB,QAAQ,yBACtB;;;;KAKO,QAAQ;;;;KAKR,kBAAkB;EAAY;;;;;;;;;;;cCG7B,kBAAkB;;;;;;;;cA4ElB,qBAAqB;;;;;;;;iBCtGZ,UAAU,KAAK,QAAK;;;;;;;cAgB7B,aAAU,KAAS,OAAK,MAAQ;;;;;;;;cAWhC,iBAAc,MAAU,kBAAgB;;;;;;;cA6ExC,WAAQ,OAAiB,YAAU,QAAQ;;;;;;;;;cAmC3C,WAAQ,kBACH,oBAEf"}
package/dist/h3/h3.mjs CHANGED
@@ -1,16 +1,5 @@
1
- import { escapeRegExp, formatError, hasContentTypeMismatch, provideRequestContext, scanForServerFiles, serverFunctionsMap } from "@thednp/rpc/server";
1
+ import { escapeRegExp, formatError, getGlobalPrefix, hasContentTypeMismatch, provideRequestContext, scanForServerFiles } from "@thednp/rpc/server";
2
2
  import { HTTPResponse, redirect as redirect$1 } from "h3";
3
- //#region src/constants.ts
4
- const FUNCTION_NOT_FOUND = "Function not found";
5
- const METHOD_NOT_ALLOWED = "Method Not Allowed";
6
- const REQUEST_FORBIDDEN = "Forbidden";
7
- const UNSUPPORTED_MEDIA_TYPE = "Unsupported Media Type";
8
- const BAD_REQUEST = "Bad Request";
9
- const CLIENT_DISCONNECTED = "client disconnected";
10
- /** Returns a warning when a middleware name is reused, preventing registration conflicts. @param name - The duplicate middleware name */
11
- const MIDDLEWARE_NAME_USED = (name) => `The middleware name "${name}" is already used.`;
12
- //#endregion
13
- //#region src/options.ts
14
3
  const defaultRPCOptions = {
15
4
  rpcPrefix: "__rpc",
16
5
  adapter: "express",
@@ -23,6 +12,42 @@ const defaultMiddlewareOptions = {
23
12
  origin: void 0
24
13
  };
25
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
41
+ //#region src/constants.ts
42
+ const FUNCTION_NOT_FOUND = "Function not found";
43
+ const METHOD_NOT_ALLOWED = "Method Not Allowed";
44
+ const REQUEST_FORBIDDEN = "Forbidden";
45
+ const UNSUPPORTED_MEDIA_TYPE = "Unsupported Media Type";
46
+ const BAD_REQUEST = "Bad Request";
47
+ const CLIENT_DISCONNECTED = "client disconnected";
48
+ /** Returns a warning when a middleware name is reused, preventing registration conflicts. @param name - The duplicate middleware name */
49
+ const MIDDLEWARE_NAME_USED = (name) => `The middleware name "${name}" is already used.`;
50
+ //#endregion
26
51
  //#region src/h3/helpers.ts
27
52
  /**
28
53
  * Convenience function to load RPC config and attach the RPC middleware to an h3 app.
@@ -141,7 +166,7 @@ const middlewareStack = /* @__PURE__ */ new Set();
141
166
  const createMiddleware = (initialOptions = {}) => {
142
167
  const options = Object.assign({}, defaultMiddlewareOptions, initialOptions);
143
168
  const middlewareName = options.name;
144
- const rpcPrefix = options.rpcPrefix;
169
+ let rpcPrefix = options.rpcPrefix;
145
170
  const path = options.path;
146
171
  const handler = options.handler;
147
172
  let name = middlewareName;
@@ -155,10 +180,15 @@ const createMiddleware = (initialOptions = {}) => {
155
180
  const pathMatcher = path ? typeof path === "string" ? new RegExp(path) : path : null;
156
181
  const middlewareHandler = async (event, next) => {
157
182
  const url = event.url.pathname;
158
- if (serverFunctionsMap.size === 0) await scanForServerFiles();
159
183
  if (!handler) return next();
160
184
  if (pathMatcher && !pathMatcher.test(url)) return next();
161
185
  if (prefixRegex && !prefixRegex.test(url)) return next();
186
+ rpcPrefix = rpcPrefix || getGlobalPrefix() || "__rpc";
187
+ if (getFunctionsForPrefix(rpcPrefix).size === 0) await scanForServerFiles({
188
+ rpcPrefix,
189
+ serverFiles: options.serverFiles,
190
+ scanRoot: options.scanRoot
191
+ });
162
192
  return handler(event, next);
163
193
  };
164
194
  Object.defineProperty(middlewareHandler, "name", { value: name });
@@ -174,8 +204,9 @@ const createMiddleware = (initialOptions = {}) => {
174
204
  const createRPCMiddleware = (initialOptions = {}) => {
175
205
  const options = Object.assign({}, defaultMiddlewareOptions, { rpcPrefix: defaultRPCOptions.rpcPrefix }, initialOptions);
176
206
  const rpcPrefix = options.rpcPrefix;
207
+ const prefix = rpcPrefix || "__rpc";
177
208
  const prefixRegex = rpcPrefix ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`) : null;
178
- const prefixReplace = `/${rpcPrefix}/`;
209
+ const prefixReplace = `/${prefix}/`;
179
210
  return createMiddleware({
180
211
  ...options,
181
212
  handler: async (event, _next) => {
@@ -188,7 +219,7 @@ const createRPCMiddleware = (initialOptions = {}) => {
188
219
  return { error: REQUEST_FORBIDDEN };
189
220
  }
190
221
  const functionName = url.replace(prefixReplace, "");
191
- const serverFunction = serverFunctionsMap.get(functionName);
222
+ const serverFunction = getFunctionsForPrefix(prefix).get(functionName);
192
223
  if (!serverFunction) {
193
224
  event.res.status = 404;
194
225
  return { error: FUNCTION_NOT_FOUND };