@thednp/rpc 0.3.2 → 0.3.3

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 +1 @@
1
- {"version":3,"file":"hono.mjs","names":["createMiddleware","createHonoMiddleware"],"sources":["../../src/options.ts","../../src/functionsMap.ts","../../src/constants.ts","../../src/hono/helpers.ts","../../src/hono/createMiddleware.ts"],"sourcesContent":["import type {\n MiddlewareOptions,\n RpcPluginOptions,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\n\nexport const defaultServerFnOptions: ServerFunctionOptions = {\n contentType: \"application/json\",\n credentials: \"same-origin\",\n method: \"POST\",\n};\n\nexport const defaultPrefix = \"__rpc\";\n\nexport const defaultRPCOptions: RpcPluginOptions = {\n rpcPrefix: defaultPrefix,\n adapter: \"express\",\n serverFiles: \"exact\",\n scanRoot: undefined,\n};\n\nexport const defaultMiddlewareOptions: MiddlewareOptions = {\n rpcPrefix: undefined,\n path: undefined,\n origin: undefined,\n};\n","import type { ServerFnEntry } from \"./types.d.ts\";\nimport { defaultPrefix } from \"./options.ts\";\n\n/**\n * Global symbol under which the shared `serverFunctionsByPrefix` map is stored\n * on `globalThis`. Keeping it on a `Symbol.for` key makes it instance-stable\n * across the bundled entry copies (`index.mjs`, `server.mjs`, `express.mjs`,\n * ...) and dev-server hot reloads, exactly like the request-context storage in\n * `context.ts`. Without this, `scanForServerFiles` (bundled into the plugin)\n * would populate a map copy the adapter middleware could not read.\n */\nconst functionsMapSymbol = Symbol.for(\"thednp.rpc.functionsMap\");\n\n/**\n * Map of rpcPrefix -> Map of function names -> ServerFnEntry\n * Enables multiple RPC instances with different prefixes to coexist\n * without name collisions.\n */\nexport const serverFunctionsByPrefix: Map<\n string,\n Map<string, ServerFnEntry>\n> = ((globalThis as Record<symbol, Map<string, Map<string, ServerFnEntry>>>)[\n functionsMapSymbol\n] ??= new Map());\n\n/**\n * Gets or creates the function map for a specific prefix.\n * @param prefix - The RPC prefix (e.g., \"__rpc\", \"v1:rpc\", \"admin:rpc\")\n * @returns Map of function names to ServerFnEntry for that prefix\n */\nexport const getFunctionsForPrefix = (\n prefix: string,\n): Map<string, ServerFnEntry> => {\n if (!serverFunctionsByPrefix.has(prefix)) {\n serverFunctionsByPrefix.set(prefix, new Map());\n }\n return serverFunctionsByPrefix.get(prefix)!;\n};\n\n/**\n * Backward compatibility: default map for the default prefix.\n * Legacy code can still use serverFunctionsMap.set(name, entry).\n */\nexport const serverFunctionsMap: Map<string, ServerFnEntry> = {\n get: (key: string) => getFunctionsForPrefix(defaultPrefix).get(key),\n set: (key: string, value: ServerFnEntry) =>\n getFunctionsForPrefix(defaultPrefix).set(key, value),\n has: (key: string) => getFunctionsForPrefix(defaultPrefix).has(key),\n delete: (key: string) => getFunctionsForPrefix(defaultPrefix).delete(key),\n clear: () => getFunctionsForPrefix(defaultPrefix).clear(),\n get size() {\n return getFunctionsForPrefix(defaultPrefix).size;\n },\n entries: () => getFunctionsForPrefix(defaultPrefix).entries(),\n keys: () => getFunctionsForPrefix(defaultPrefix).keys(),\n values: () => getFunctionsForPrefix(defaultPrefix).values(),\n forEach: (\n callback: (\n value: ServerFnEntry,\n key: string,\n map: Map<string, ServerFnEntry>,\n ) => void,\n ) => getFunctionsForPrefix(defaultPrefix).forEach(callback),\n [Symbol.iterator]: () =>\n getFunctionsForPrefix(defaultPrefix)[Symbol.iterator](),\n} as unknown as Map<string, ServerFnEntry>;\n","export const OPERATION_ABORTED = \"Operation aborted\";\n\nexport const REQUEST_CANCELLED = \"Request was cancelled\";\n\nexport const FETCH_ERROR_PREFIX = \"Fetch error: \";\n\nexport const NO_SERVER_FUNCTION_FOUND = \"No server function found.\";\n\nexport const ERROR_LOADING_FILE = \"Error loading file:\";\n\nexport const FUNCTION_NOT_FOUND = \"Function not found\";\n\nexport const METHOD_NOT_ALLOWED = \"Method Not Allowed\";\n\nexport const REQUEST_FORBIDDEN = \"Forbidden\";\n\nexport const UNSUPPORTED_MEDIA_TYPE = \"Unsupported Media Type\";\n\nexport const BAD_REQUEST = \"Bad Request\";\n\nexport const INTERNAL_SERVER_ERROR = \"Internal Server Error\";\n\nexport const CLIENT_DISCONNECTED = \"client disconnected\";\n\n/** Returns a warning when a middleware name is reused, preventing registration conflicts. @param name - The duplicate middleware name */\nexport const MIDDLEWARE_NAME_USED = (name: string) =>\n `The middleware name \"${name}\" is already used.`;\n\n/** Error message when a value fails the safe-identifier validation. @param label - What kind of value was being validated. @param name - The rejected value */\nexport const INVALID_IDENTIFIER = (label: string, name: string) =>\n `Invalid ${label}: \"${name}\" must match /^[A-Za-z_$][A-Za-z0-9_$]*$/`;\n\n/** Error message when a value fails the safe-path-segment validation. @param label - What kind of value was being validated. @param segment - The rejected value */\nexport const INVALID_PATH_SEGMENT = (label: string, segment: string) =>\n `Invalid ${label}: \"${segment}\" must match /^[A-Za-z0-9_$@:][A-Za-z0-9_$@:/-]*$/`;\n\n/** Warning message when a specified RPC config file cannot be resolved on disk. @param configFile - The requested config filename. @param configFilePath - The resolved absolute path */\nexport const CONFIG_FILE_NOT_FOUND = (\n configFile: string,\n configFilePath: string,\n) =>\n ` ⚠︎ The specified RPC config file ${configFile} cannot be found at ${configFilePath}, loading the defaults..`;\n\nexport const NO_CONFIG_FOUND = ` ⚡︎ No RPC config found, loading the defaults..`;\n\nexport const FAILED_LOAD_CONFIG = ` ⚠︎ Failed to load RPC config:`;\n\n/** Error template for duplicate server function names across files. @param name - The duplicate registered name */\nexport const DUPLICATE_FUNCTION_NAME = (name: string) =>\n `Duplicate server function \"${name}\" detected. Each server function must have a unique name. Remove or rename the duplicate.`;\n","// src/hono/helpers.ts\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type { HttpBindings } from \"@hono/node-server\";\nimport type { Context, Hono } from \"hono\";\nimport type { ViteDevServer } from \"vite\";\nimport type {\n ContentfulStatusCode,\n RedirectStatusCode,\n} from \"hono/utils/http-status\";\nimport type { BodyResult } from \"@thednp/rpc\";\nimport type { IncomingWithBody } from \"./types.d.ts\";\nimport { createMiddleware } from \"hono/factory\";\nimport { createRPCMiddleware } from \"./createMiddleware.ts\";\n\n/**\n * Convenience function to load RPC config and attach the RPC middleware to a Hono app.\n * Dynamically imports loadRPCConfig and registers the middleware.\n * @param app - Hono application instance\n */\nexport async function attachRPC(app: Hono) {\n // The main plugin entry statically imports Vite, so loadRPCConfig is\n // imported lazily: function bundles that never call attachRPC (e.g.\n // serverless functions) keep Vite out of the bundle (or externalized).\n const { loadRPCConfig } = await import(\"@thednp/rpc\");\n const { adapter: _adapter, ...options } = await loadRPCConfig();\n\n app.use(createRPCMiddleware(options));\n}\n\n/**\n * Attaches Vite's dev server middlewares to a Hono app for development mode.\n * Uses the viteMiddleware wrapper to bridge Vite's Connect-compatible stack into Hono.\n * @param app - Hono application instance\n * @param vite - Running Vite dev server\n */\nexport const attachVite = (app: Hono, vite: ViteDevServer): void => {\n app.use(viteMiddleware(vite));\n};\n\n/**\n * Creates a Hono-compatible middleware from a Vite dev server middleware stack.\n * Bridges the Connect/Express middleware interface to Hono's context-based request/response model.\n * Supports both Node.js and Bun runtimes with separate polyfill paths.\n * @param vite - Running Vite dev server\n * @returns A Hono middleware function\n * @see https://github.com/honojs/hono/issues/3162#issuecomment-2331118049\n */\nexport const viteMiddleware = (\n vite: ViteDevServer,\n): ReturnType<typeof createMiddleware<{ Bindings: HttpBindings }>> => {\n return createMiddleware<{ Bindings: HttpBindings }>((c, next) => {\n return new Promise((resolve) => {\n // Node.js\n // @ts-expect-error - NodeJS is different\n // istanbul ignore if\n if (typeof Bun === \"undefined\") {\n vite.middlewares(c.env.incoming, c.env.outgoing, () => resolve(next()));\n return;\n }\n\n /* istanbul ignore next */ {\n // Bun\n let sent = false;\n const headers = new Headers();\n // Polyfill the node:http IncommingMessage and ServerResponse\n vite.middlewares(\n {\n url: new URL(c.req.path, \"http://localhost\").pathname,\n method: c.req.raw.method,\n headers: Object.fromEntries(c.req.raw.headers),\n } as IncomingMessage,\n {\n setHeader(name, value: string) {\n headers.set(name, value);\n return this;\n },\n end(body) {\n sent = true;\n resolve(\n // @ts-expect-error - weird\n c.body(body, c.res.status as ContentfulStatusCode, headers),\n );\n },\n } as ServerResponse,\n () => sent || resolve(next()),\n );\n }\n });\n });\n};\n\n/**\n * Reads and parses the HTTP request body from a Hono context.\n * Supports JSON and text content types, with pre-parsed body detection for server-side environments.\n * @param c - Hono request context\n * @returns A promise resolving to the parsed body with its content type\n */\nexport const readBody = async (\n c: Context,\n): Promise<BodyResult> => {\n const contentType = c.req.header(\"content-type\")?.toLowerCase() || \"\";\n const isJSON = contentType.includes(\"json\");\n const isMultipart = contentType.includes(\"multipart/form-data\");\n const isUrlEncoded = contentType.includes(\"urlencoded\");\n const incoming = (c.env as HttpBindings).incoming as\n | IncomingWithBody\n | undefined;\n if (incoming?.body !== undefined) {\n const reqBody = incoming.body;\n return {\n contentType: isMultipart\n ? \"multipart/form-data\"\n : isJSON\n ? \"application/json\"\n : isUrlEncoded\n ? \"application/x-www-form-urlencoded\"\n : \"text/plain\",\n data: isMultipart\n ? (reqBody as Record<string, unknown>)\n : isJSON\n ? reqBody\n : isUrlEncoded\n ? (reqBody as Record<string, unknown>)\n : String(reqBody),\n } as BodyResult;\n }\n if (isJSON) {\n const data = await c.req.json();\n return {\n contentType: \"application/json\",\n data,\n };\n }\n\n const text = await c.req.text();\n return {\n contentType: isMultipart\n ? \"multipart/form-data\"\n : isUrlEncoded\n ? \"application/x-www-form-urlencoded\"\n : \"text/plain\",\n data: isMultipart\n ? ({ raw: text } as Record<string, unknown>)\n : isUrlEncoded\n ? Object.fromEntries(new URLSearchParams(text))\n : String(text),\n } as BodyResult;\n};\n\n/**\n * Issues an HTTP redirect on a Hono context. Hono's `c.redirect(location,\n * status)` returns a `Response` object that the handler must return (it never\n * writes directly). Defaults to `303 See Other` for convention\n * (Post/Redirect/Get).\n * @param c - Hono context\n * @param location - The URL to redirect to\n * @param status - HTTP status code, defaults to 303\n * @returns A Hono `Response` to return from the handler\n */\nexport const redirect = (\n c: Context,\n location: string,\n status: RedirectStatusCode = 303,\n): Response => {\n return c.redirect(location, status);\n};\n","// src/hono/createMiddleware.ts\nimport type { Context, Next } from \"hono\";\nimport type {\n ContentfulStatusCode,\n RedirectStatusCode,\n} from \"hono/utils/http-status\";\nimport type { HonoMiddlewareFn, HonoMiddlewareOptions } from \"./types.d.ts\";\nimport type { JsonValue } from \"@thednp/rpc\";\nimport { createMiddleware as createHonoMiddleware } from \"hono/factory\";\nimport type { RequestEvent } from \"@thednp/rpc/server\";\nimport {\n escapeRegExp,\n formatError,\n getGlobalPrefix,\n hasContentTypeMismatch,\n provideRequestContext,\n safeURL,\n scanForServerFiles,\n} from \"@thednp/rpc/server\";\nimport { getFunctionsForPrefix } from \"../functionsMap.ts\";\nimport {\n defaultMiddlewareOptions,\n defaultPrefix,\n defaultRPCOptions,\n} from \"../options.ts\";\nimport {\n BAD_REQUEST,\n CLIENT_DISCONNECTED,\n FUNCTION_NOT_FOUND,\n METHOD_NOT_ALLOWED,\n MIDDLEWARE_NAME_USED,\n REQUEST_FORBIDDEN,\n UNSUPPORTED_MEDIA_TYPE,\n} from \"../constants.ts\";\nimport { readBody } from \"./helpers.ts\";\n\nlet middlewareCount = 0;\nconst middlewareStack = new Set<string>();\n\n/**\n * Creates a Hono middleware with optional path and rpcPrefix filtering.\n * Middleware names are deduplicated. Prefix and path regexes are compiled once at creation time.\n * Uses Hono's factory `createMiddleware` to wrap the handler.\n * @param initialOptions - Options for rpcPrefix, path matching, and the handler function\n * @returns A Hono middleware function\n */\nexport const createMiddleware: HonoMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n initialOptions,\n ) as HonoMiddlewareOptions;\n\n const middlewareName = options.name;\n let rpcPrefix = options.rpcPrefix;\n const path = options.path;\n const handler = options.handler;\n\n let name = middlewareName;\n if (!name) {\n name = \"viteRPCMiddleware-\" + middlewareCount;\n middlewareCount += 1;\n }\n if (middlewareStack.has(name)) {\n throw new Error(MIDDLEWARE_NAME_USED(name));\n }\n middlewareStack.add(name);\n\n // Hoist regex compilation out of per-request path. Escape the prefix to\n // prevent regex injection via metacharacters in the config string.\n const prefixRegex: RegExp | null = rpcPrefix\n ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`)\n : null;\n const pathMatcher: RegExp | null = path\n ? (typeof path === \"string\" ? new RegExp(path) : path)\n : null;\n\n const middlewareHandler = createHonoMiddleware(\n async (c: Context, next: Next) => {\n const reqUrl = safeURL(c.req.path);\n const url = reqUrl.pathname;\n\n // No need to continue when no handler provided\n if (!handler) {\n return next();\n }\n\n if (pathMatcher && !pathMatcher.test(url)) {\n return next();\n }\n\n if (prefixRegex && !prefixRegex.test(url)) {\n return next();\n }\n\n rpcPrefix = (rpcPrefix ?? defaultPrefix) as string;\n\n // When serving from production server, scan for server files\n if (getFunctionsForPrefix(rpcPrefix).size === 0) {\n await scanForServerFiles({\n rpcPrefix,\n serverFiles:\n (options as unknown as { serverFiles?: \"exact\" | \"glob\" })\n .serverFiles,\n scanRoot: (options as unknown as { scanRoot?: string }).scanRoot,\n } as never);\n }\n\n return (await handler(c, next)) as Response;\n },\n );\n\n Object.defineProperty(middlewareHandler, \"name\", {\n value: name,\n });\n\n return middlewareHandler;\n};\n\n/**\n * Creates the Hono RPC middleware that routes incoming requests to registered server functions.\n * Wraps the generic createMiddleware with the RPC handler that reads the body, dispatches\n * to the matching function, and returns the JSON-serialized result.\n * @param initialOptions - Options including rpcPrefix for URL routing\n * @returns A Hono middleware function\n */\nexport const createRPCMiddleware: HonoMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n { rpcPrefix: defaultRPCOptions.rpcPrefix },\n initialOptions,\n ) as HonoMiddlewareOptions;\n\n // Hoist prefix regex (escaped) and the literal prefix-for-replace out of the\n // per-request handler to avoid regex injection and per-request compilation.\n const rpcPrefix = options.rpcPrefix;\n const prefix = rpcPrefix || getGlobalPrefix() || defaultPrefix;\n const prefixRegex = rpcPrefix\n ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`)\n : /* istanbul ignore next */ null;\n const prefixReplace = `/${prefix}/`;\n\n return createMiddleware({\n ...options,\n handler: async (c: Context, _next: Next) => {\n const { path: reqPath } = c.req;\n // const { rpcPrefix: prefix } = options;\n\n // Defense-in-depth: validate prefix match via escaped regex even though\n // the outer createMiddleware gates on the same prefix already.\n // istanbul ignore if\n if (prefixRegex && !prefixRegex.test(reqPath)) {\n /* istanbul ignore next */\n return;\n }\n\n // Optional origin check: reject requests whose Origin header does not\n // match the configured origin. Requests without an Origin header\n // (curl, native clients) pass through unchecked.\n const origin = options.origin;\n const requestOrigin = c.req.header(\"origin\");\n if (origin && requestOrigin && requestOrigin !== origin) {\n return c.json({ error: REQUEST_FORBIDDEN }, 403);\n }\n\n const functionName = reqPath.replace(prefixReplace, \"\");\n const serverFunction = getFunctionsForPrefix(prefix).get(functionName);\n\n if (!serverFunction) {\n return c.json({ error: FUNCTION_NOT_FOUND }, 404);\n }\n\n try {\n const method = serverFunction.options?.method || \"POST\";\n if (c.req.method.toUpperCase() !== method) {\n return c.json({ error: METHOD_NOT_ALLOWED }, 405);\n }\n\n let args: JsonValue[] = [];\n if (method === \"GET\") {\n const raw = c.req.query(\"args\");\n if (raw) {\n const parsed: unknown = JSON.parse(raw);\n if (!Array.isArray(parsed)) {\n return c.json({ error: BAD_REQUEST }, 400);\n }\n args = parsed as JsonValue[];\n }\n } else {\n // Content-type enforcement: strict for json/text, lenient between forms.\n // Requests without a Content-Type header are exempt (curl/GET compat).\n // Checked BEFORE readBody so mismatched bodies are never buffered.\n if (\n hasContentTypeMismatch(\n serverFunction.options?.contentType ?? \"application/json\",\n c.req.header(\"content-type\"),\n )\n ) {\n return c.json({ error: UNSUPPORTED_MEDIA_TYPE }, 415);\n }\n const body = await readBody(c);\n args = Array.isArray(body.data)\n ? body.data as JsonValue[]\n : [body.data as JsonValue];\n }\n const requestEvent: RequestEvent = {\n request: c.req,\n response: c.res,\n nativeEvent: c,\n locals: {},\n functionName,\n // Hono's `c.redirect`/`c.json` return a `Response` (never write\n // directly), so the bound redirect/send only record the intent; the\n // middleware uses them after the dispatch to return the Response.\n redirect: (location, status = 303) => {\n requestEvent.redirected = { location, status };\n },\n send: (status, body, headers) => {\n requestEvent.sent = { status, body, headers };\n },\n };\n const fnResult = provideRequestContext(\n requestEvent,\n () => serverFunction.handler(...args),\n );\n const onAbort = () => fnResult.cancel(CLIENT_DISCONNECTED);\n // The runtime adapter may be absent in some Hono environments\n // (e.g. standalone serverless adapters), so guard the close hook.\n c.env.incoming?.on(\"close\", onAbort);\n const result = await fnResult.data;\n c.env.incoming?.off(\"close\", onAbort);\n\n if (requestEvent.redirected) {\n return c.redirect(\n requestEvent.redirected.location,\n requestEvent.redirected.status as RedirectStatusCode,\n ) as Response;\n }\n\n if (requestEvent.sent) {\n const { status, body, headers } = requestEvent.sent;\n return c.body(JSON.stringify(body), status as ContentfulStatusCode, {\n \"content-type\": \"application/json\",\n ...headers,\n });\n }\n\n return c.json({ data: result }, 200);\n } catch (err) {\n console.error(String(err));\n const isProduction = process.env.NODE_ENV === \"production\";\n return c.json(formatError(err, isProduction), 500);\n }\n },\n });\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"hono.mjs","names":["createMiddleware","createHonoMiddleware"],"sources":["../../src/options.ts","../../src/functionsMap.ts","../../src/constants.ts","../../src/hono/helpers.ts","../../src/hono/createMiddleware.ts"],"sourcesContent":["import type {\n MiddlewareOptions,\n RpcPluginOptions,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\n\nexport const defaultServerFnOptions: ServerFunctionOptions = {\n contentType: \"application/json\",\n credentials: \"same-origin\",\n method: \"POST\",\n};\n\nexport const defaultPrefix = \"__rpc\";\n\nexport const defaultRPCOptions: RpcPluginOptions = {\n rpcPrefix: defaultPrefix,\n adapter: \"express\",\n serverFiles: \"exact\",\n scanRoot: undefined,\n};\n\nexport const defaultMiddlewareOptions: MiddlewareOptions = {\n rpcPrefix: undefined,\n path: undefined,\n origin: undefined,\n};\n","import type { ServerFnEntry } from \"./types.d.ts\";\nimport { defaultPrefix } from \"./options.ts\";\n\n/**\n * Global symbol under which the shared `serverFunctionsByPrefix` map is stored\n * on `globalThis`. Keeping it on a `Symbol.for` key makes it instance-stable\n * across the bundled entry copies (`index.mjs`, `server.mjs`, `express.mjs`,\n * ...) and dev-server hot reloads, exactly like the request-context storage in\n * `context.ts`. Without this, `scanForServerFiles` (bundled into the plugin)\n * would populate a map copy the adapter middleware could not read.\n */\nconst functionsMapSymbol = Symbol.for(\"thednp.rpc.functionsMap\");\n\n/**\n * Map of rpcPrefix -> Map of function names -> ServerFnEntry\n * Enables multiple RPC instances with different prefixes to coexist\n * without name collisions.\n */\nexport const serverFunctionsByPrefix: Map<\n string,\n Map<string, ServerFnEntry>\n> = ((globalThis as Record<symbol, Map<string, Map<string, ServerFnEntry>>>)[\n functionsMapSymbol\n] ??= new Map());\n\n/**\n * Gets or creates the function map for a specific prefix.\n * @param prefix - The RPC prefix (e.g., \"__rpc\", \"v1:rpc\", \"admin:rpc\")\n * @returns Map of function names to ServerFnEntry for that prefix\n */\nexport const getFunctionsForPrefix = (\n prefix: string,\n): Map<string, ServerFnEntry> => {\n if (!serverFunctionsByPrefix.has(prefix)) {\n serverFunctionsByPrefix.set(prefix, new Map());\n }\n return serverFunctionsByPrefix.get(prefix)!;\n};\n\n/**\n * Backward compatibility: default map for the default prefix.\n * Legacy code can still use serverFunctionsMap.set(name, entry).\n */\nexport const serverFunctionsMap: Map<string, ServerFnEntry> = {\n get: (key: string) => getFunctionsForPrefix(defaultPrefix).get(key),\n set: (key: string, value: ServerFnEntry) =>\n getFunctionsForPrefix(defaultPrefix).set(key, value),\n has: (key: string) => getFunctionsForPrefix(defaultPrefix).has(key),\n delete: (key: string) => getFunctionsForPrefix(defaultPrefix).delete(key),\n clear: () => getFunctionsForPrefix(defaultPrefix).clear(),\n get size() {\n return getFunctionsForPrefix(defaultPrefix).size;\n },\n entries: () => getFunctionsForPrefix(defaultPrefix).entries(),\n keys: () => getFunctionsForPrefix(defaultPrefix).keys(),\n values: () => getFunctionsForPrefix(defaultPrefix).values(),\n forEach: (\n callback: (\n value: ServerFnEntry,\n key: string,\n map: Map<string, ServerFnEntry>,\n ) => void,\n ) => getFunctionsForPrefix(defaultPrefix).forEach(callback),\n [Symbol.iterator]: () =>\n getFunctionsForPrefix(defaultPrefix)[Symbol.iterator](),\n} as unknown as Map<string, ServerFnEntry>;\n","export const OPERATION_ABORTED = \"Operation aborted\";\n\nexport const REQUEST_CANCELLED = \"Request was cancelled\";\n\nexport const FETCH_ERROR_PREFIX = \"Fetch error: \";\n\nexport const NO_SERVER_FUNCTION_FOUND = \"No server function found.\";\n\nexport const ERROR_LOADING_FILE = \"Error loading file:\";\n\nexport const FUNCTION_NOT_FOUND = \"Function not found\";\n\nexport const METHOD_NOT_ALLOWED = \"Method Not Allowed\";\n\nexport const REQUEST_FORBIDDEN = \"Forbidden\";\n\nexport const UNSUPPORTED_MEDIA_TYPE = \"Unsupported Media Type\";\n\nexport const BAD_REQUEST = \"Bad Request\";\n\nexport const INTERNAL_SERVER_ERROR = \"Internal Server Error\";\n\nexport const CLIENT_DISCONNECTED = \"client disconnected\";\n\n/** Returns a warning when a middleware name is reused, preventing registration conflicts. @param name - The duplicate middleware name */\nexport const MIDDLEWARE_NAME_USED = (name: string) =>\n `The middleware name \"${name}\" is already used.`;\n\n/** Error message when a value fails the safe-identifier validation. @param label - What kind of value was being validated. @param name - The rejected value */\nexport const INVALID_IDENTIFIER = (label: string, name: string) =>\n `Invalid ${label}: \"${name}\" must match /^[A-Za-z_$][A-Za-z0-9_$]*$/`;\n\n/** Error message when a value fails the safe-path-segment validation. @param label - What kind of value was being validated. @param segment - The rejected value */\nexport const INVALID_PATH_SEGMENT = (label: string, segment: string) =>\n `Invalid ${label}: \"${segment}\" must match /^[A-Za-z0-9_$@:][A-Za-z0-9_$@:/-]*$/`;\n\n/** Warning message when a specified RPC config file cannot be resolved on disk. @param configFile - The requested config filename. @param configFilePath - The resolved absolute path */\nexport const CONFIG_FILE_NOT_FOUND = (\n configFile: string,\n configFilePath: string,\n) =>\n ` ⚠︎ The specified RPC config file ${configFile} cannot be found at ${configFilePath}, loading the defaults..`;\n\nexport const NO_CONFIG_FOUND =\n ` ⚡︎ No RPC config found, loading the defaults..`;\n\nexport const FAILED_LOAD_CONFIG = ` ⚠︎ Failed to load RPC config:`;\n\n/** Error template for duplicate server function names across files. @param name - The duplicate registered name */\nexport const DUPLICATE_FUNCTION_NAME = (name: string) =>\n `Duplicate server function \"${name}\" detected. Each server function must have a unique name. Remove or rename the duplicate.`;\n","// src/hono/helpers.ts\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type { HttpBindings } from \"@hono/node-server\";\nimport type { Context, Hono } from \"hono\";\nimport type { ViteDevServer } from \"vite\";\nimport type {\n ContentfulStatusCode,\n RedirectStatusCode,\n} from \"hono/utils/http-status\";\nimport type { BodyResult } from \"@thednp/rpc\";\nimport type { IncomingWithBody } from \"./types.d.ts\";\nimport { createMiddleware } from \"hono/factory\";\nimport { createRPCMiddleware } from \"./createMiddleware.ts\";\n\n/**\n * Convenience function to load RPC config and attach the RPC middleware to a Hono app.\n * Dynamically imports loadRPCConfig and registers the middleware.\n * @param app - Hono application instance\n */\nexport async function attachRPC(app: Hono) {\n // The main plugin entry statically imports Vite, so loadRPCConfig is\n // imported lazily: function bundles that never call attachRPC (e.g.\n // serverless functions) keep Vite out of the bundle (or externalized).\n const { loadRPCConfig } = await import(\"@thednp/rpc\");\n const { adapter: _adapter, ...options } = await loadRPCConfig();\n\n app.use(createRPCMiddleware(options));\n}\n\n/**\n * Attaches Vite's dev server middlewares to a Hono app for development mode.\n * Uses the viteMiddleware wrapper to bridge Vite's Connect-compatible stack into Hono.\n * @param app - Hono application instance\n * @param vite - Running Vite dev server\n */\nexport const attachVite = (app: Hono, vite: ViteDevServer): void => {\n app.use(viteMiddleware(vite));\n};\n\n/**\n * Creates a Hono-compatible middleware from a Vite dev server middleware stack.\n * Bridges the Connect/Express middleware interface to Hono's context-based request/response model.\n * Supports both Node.js and Bun runtimes with separate polyfill paths.\n * @param vite - Running Vite dev server\n * @returns A Hono middleware function\n * @see https://github.com/honojs/hono/issues/3162#issuecomment-2331118049\n */\nexport const viteMiddleware = (\n vite: ViteDevServer,\n): ReturnType<typeof createMiddleware<{ Bindings: HttpBindings }>> => {\n return createMiddleware<{ Bindings: HttpBindings }>((c, next) => {\n return new Promise((resolve) => {\n // Node.js\n // @ts-expect-error - NodeJS is different\n // istanbul ignore if\n if (typeof Bun === \"undefined\") {\n vite.middlewares(c.env.incoming, c.env.outgoing, () => resolve(next()));\n return;\n }\n\n /* istanbul ignore next */ {\n // Bun\n let sent = false;\n const headers = new Headers();\n // Polyfill the node:http IncommingMessage and ServerResponse\n vite.middlewares(\n {\n url: new URL(c.req.path, \"http://localhost\").pathname,\n method: c.req.raw.method,\n headers: Object.fromEntries(c.req.raw.headers),\n } as IncomingMessage,\n {\n setHeader(name, value: string) {\n headers.set(name, value);\n return this;\n },\n end(body) {\n sent = true;\n resolve(\n // @ts-expect-error - weird\n c.body(body, c.res.status as ContentfulStatusCode, headers),\n );\n },\n } as ServerResponse,\n () => sent || resolve(next()),\n );\n }\n });\n });\n};\n\n/**\n * Reads and parses the HTTP request body from a Hono context.\n * Supports JSON and text content types, with pre-parsed body detection for server-side environments.\n * @param c - Hono request context\n * @returns A promise resolving to the parsed body with its content type\n */\nexport const readBody = async (\n c: Context,\n): Promise<BodyResult> => {\n const contentType = c.req.header(\"content-type\")?.toLowerCase() || \"\";\n const isJSON = contentType.includes(\"json\");\n const isMultipart = contentType.includes(\"multipart/form-data\");\n const isUrlEncoded = contentType.includes(\"urlencoded\");\n const incoming = (c.env as HttpBindings).incoming as\n | IncomingWithBody\n | undefined;\n if (incoming?.body !== undefined) {\n const reqBody = incoming.body;\n return {\n contentType: isMultipart\n ? \"multipart/form-data\"\n : isJSON\n ? \"application/json\"\n : isUrlEncoded\n ? \"application/x-www-form-urlencoded\"\n : \"text/plain\",\n data: isMultipart\n ? (reqBody as Record<string, unknown>)\n : isJSON\n ? reqBody\n : isUrlEncoded\n ? (reqBody as Record<string, unknown>)\n : String(reqBody),\n } as BodyResult;\n }\n if (isJSON) {\n const data = await c.req.json();\n return {\n contentType: \"application/json\",\n data,\n };\n }\n\n const text = await c.req.text();\n return {\n contentType: isMultipart\n ? \"multipart/form-data\"\n : isUrlEncoded\n ? \"application/x-www-form-urlencoded\"\n : \"text/plain\",\n data: isMultipart\n ? ({ raw: text } as Record<string, unknown>)\n : isUrlEncoded\n ? Object.fromEntries(new URLSearchParams(text))\n : String(text),\n } as BodyResult;\n};\n\n/**\n * Issues an HTTP redirect on a Hono context. Hono's `c.redirect(location,\n * status)` returns a `Response` object that the handler must return (it never\n * writes directly). Defaults to `303 See Other` for convention\n * (Post/Redirect/Get).\n * @param c - Hono context\n * @param location - The URL to redirect to\n * @param status - HTTP status code, defaults to 303\n * @returns A Hono `Response` to return from the handler\n */\nexport const redirect = (\n c: Context,\n location: string,\n status: RedirectStatusCode = 303,\n): Response => {\n return c.redirect(location, status);\n};\n","// src/hono/createMiddleware.ts\nimport type { Context, Next } from \"hono\";\nimport type {\n ContentfulStatusCode,\n RedirectStatusCode,\n} from \"hono/utils/http-status\";\nimport type { HonoMiddlewareFn, HonoMiddlewareOptions } from \"./types.d.ts\";\nimport type { JsonValue } from \"@thednp/rpc\";\nimport { createMiddleware as createHonoMiddleware } from \"hono/factory\";\nimport type { RequestEvent } from \"@thednp/rpc/server\";\nimport {\n escapeRegExp,\n formatError,\n getGlobalPrefix,\n hasContentTypeMismatch,\n provideRequestContext,\n safeURL,\n scanForServerFiles,\n} from \"@thednp/rpc/server\";\nimport { getFunctionsForPrefix } from \"../functionsMap.ts\";\nimport {\n defaultMiddlewareOptions,\n defaultPrefix,\n defaultRPCOptions,\n} from \"../options.ts\";\nimport {\n BAD_REQUEST,\n CLIENT_DISCONNECTED,\n FUNCTION_NOT_FOUND,\n METHOD_NOT_ALLOWED,\n MIDDLEWARE_NAME_USED,\n REQUEST_FORBIDDEN,\n UNSUPPORTED_MEDIA_TYPE,\n} from \"../constants.ts\";\nimport { readBody } from \"./helpers.ts\";\n\nlet middlewareCount = 0;\nconst middlewareStack = new Set<string>();\n\n/**\n * Creates a Hono middleware with optional path and rpcPrefix filtering.\n * Middleware names are deduplicated. Prefix and path regexes are compiled once at creation time.\n * Uses Hono's factory `createMiddleware` to wrap the handler.\n * @param initialOptions - Options for rpcPrefix, path matching, and the handler function\n * @returns A Hono middleware function\n */\nexport const createMiddleware: HonoMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n initialOptions,\n ) as HonoMiddlewareOptions;\n\n const middlewareName = options.name;\n let rpcPrefix = options.rpcPrefix;\n const path = options.path;\n const handler = options.handler;\n\n let name = middlewareName;\n if (!name) {\n name = \"viteRPCMiddleware-\" + middlewareCount;\n middlewareCount += 1;\n }\n if (middlewareStack.has(name)) {\n throw new Error(MIDDLEWARE_NAME_USED(name));\n }\n middlewareStack.add(name);\n\n // Hoist regex compilation out of per-request path. Escape the prefix to\n // prevent regex injection via metacharacters in the config string.\n const prefixRegex: RegExp | null = rpcPrefix\n ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`)\n : null;\n const pathMatcher: RegExp | null = path\n ? (typeof path === \"string\" ? new RegExp(path) : path)\n : null;\n\n const middlewareHandler = createHonoMiddleware(\n async (c: Context, next: Next) => {\n const reqUrl = safeURL(c.req.path);\n const url = reqUrl.pathname;\n\n // No need to continue when no handler provided\n if (!handler) {\n return next();\n }\n\n if (pathMatcher && !pathMatcher.test(url)) {\n return next();\n }\n\n if (prefixRegex && !prefixRegex.test(url)) {\n return next();\n }\n\n rpcPrefix = (rpcPrefix ?? defaultPrefix) as string;\n\n // When serving from production server, scan for server files\n if (getFunctionsForPrefix(rpcPrefix).size === 0) {\n await scanForServerFiles({\n rpcPrefix,\n serverFiles:\n (options as unknown as { serverFiles?: \"exact\" | \"glob\" })\n .serverFiles,\n scanRoot: (options as unknown as { scanRoot?: string }).scanRoot,\n } as never);\n }\n\n return (await handler(c, next)) as Response;\n },\n );\n\n Object.defineProperty(middlewareHandler, \"name\", {\n value: name,\n });\n\n return middlewareHandler;\n};\n\n/**\n * Creates the Hono RPC middleware that routes incoming requests to registered server functions.\n * Wraps the generic createMiddleware with the RPC handler that reads the body, dispatches\n * to the matching function, and returns the JSON-serialized result.\n * @param initialOptions - Options including rpcPrefix for URL routing\n * @returns A Hono middleware function\n */\nexport const createRPCMiddleware: HonoMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n { rpcPrefix: defaultRPCOptions.rpcPrefix },\n initialOptions,\n ) as HonoMiddlewareOptions;\n\n // Hoist prefix regex (escaped) and the literal prefix-for-replace out of the\n // per-request handler to avoid regex injection and per-request compilation.\n const rpcPrefix = options.rpcPrefix;\n const prefix = rpcPrefix || getGlobalPrefix() || defaultPrefix;\n const prefixRegex = rpcPrefix\n ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`)\n : /* istanbul ignore next */ null;\n const prefixReplace = `/${prefix}/`;\n\n return createMiddleware({\n ...options,\n handler: async (c: Context, _next: Next) => {\n const { path: reqPath } = c.req;\n // const { rpcPrefix: prefix } = options;\n\n // Defense-in-depth: validate prefix match via escaped regex even though\n // the outer createMiddleware gates on the same prefix already.\n // istanbul ignore if\n if (prefixRegex && !prefixRegex.test(reqPath)) {\n /* istanbul ignore next */\n return;\n }\n\n // Optional origin check: reject requests whose Origin header does not\n // match the configured origin. Requests without an Origin header\n // (curl, native clients) pass through unchecked.\n const origin = options.origin;\n const requestOrigin = c.req.header(\"origin\");\n if (origin && requestOrigin && requestOrigin !== origin) {\n return c.json({ error: REQUEST_FORBIDDEN }, 403);\n }\n\n const functionName = reqPath.replace(prefixReplace, \"\");\n const serverFunction = getFunctionsForPrefix(prefix).get(functionName);\n\n if (!serverFunction) {\n return c.json({ error: FUNCTION_NOT_FOUND }, 404);\n }\n\n try {\n const method = serverFunction.options?.method || \"POST\";\n if (c.req.method.toUpperCase() !== method) {\n return c.json({ error: METHOD_NOT_ALLOWED }, 405);\n }\n\n let args: JsonValue[] = [];\n if (method === \"GET\") {\n const raw = c.req.query(\"args\");\n if (raw) {\n const parsed: unknown = JSON.parse(raw);\n if (!Array.isArray(parsed)) {\n return c.json({ error: BAD_REQUEST }, 400);\n }\n args = parsed as JsonValue[];\n }\n } else {\n // Content-type enforcement: strict for json/text, lenient between forms.\n // Requests without a Content-Type header are exempt (curl/GET compat).\n // Checked BEFORE readBody so mismatched bodies are never buffered.\n if (\n hasContentTypeMismatch(\n serverFunction.options?.contentType ?? \"application/json\",\n c.req.header(\"content-type\"),\n )\n ) {\n return c.json({ error: UNSUPPORTED_MEDIA_TYPE }, 415);\n }\n const body = await readBody(c);\n args = Array.isArray(body.data)\n ? body.data as JsonValue[]\n : [body.data as JsonValue];\n }\n const requestEvent: RequestEvent = {\n request: c.req,\n response: c.res,\n nativeEvent: c,\n locals: {},\n functionName,\n // Hono's `c.redirect`/`c.json` return a `Response` (never write\n // directly), so the bound redirect/send only record the intent; the\n // middleware uses them after the dispatch to return the Response.\n redirect: (location, status = 303) => {\n requestEvent.redirected = { location, status };\n },\n send: (status, body, headers) => {\n requestEvent.sent = { status, body, headers };\n },\n };\n const fnResult = provideRequestContext(\n requestEvent,\n () => serverFunction.handler(...args),\n );\n const onAbort = () => fnResult.cancel(CLIENT_DISCONNECTED);\n // The runtime adapter may be absent in some Hono environments\n // (e.g. standalone serverless adapters), so guard the close hook.\n c.env.incoming?.on(\"close\", onAbort);\n const result = await fnResult.data;\n c.env.incoming?.off(\"close\", onAbort);\n\n if (requestEvent.redirected) {\n return c.redirect(\n requestEvent.redirected.location,\n requestEvent.redirected.status as RedirectStatusCode,\n ) as Response;\n }\n\n if (requestEvent.sent) {\n const { status, body, headers } = requestEvent.sent;\n return c.body(JSON.stringify(body), status as ContentfulStatusCode, {\n \"content-type\": \"application/json\",\n ...headers,\n });\n }\n\n return c.json({ data: result }, 200);\n } catch (err) {\n console.error(String(err));\n const isProduction = process.env.NODE_ENV === \"production\";\n return c.json(formatError(err, isProduction), 500);\n }\n },\n });\n};\n"],"mappings":"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"}
package/dist/index.d.mts CHANGED
@@ -245,18 +245,18 @@ type ServerFnArgs = [...JsonArray];
245
245
  * Server-side handler signature: receives the `AbortSignal` first,
246
246
  * followed by any serializable arguments.
247
247
  */
248
- type ServerFunction<TArgs extends JsonArray = JsonArray, TResult extends JsonValue = JsonValue> = (signal: AbortSignal, ...args: TArgs) => Promise<TResult>;
248
+ type ServerFunction<TArgs extends JsonArray = JsonArray, TResult = JsonValue> = (signal: AbortSignal, ...args: TArgs) => Promise<TResult>;
249
249
  /**
250
250
  * Server function initialization signature, identical to `ServerFunction`.
251
251
  * Used when registering a function with `createServerFunction`.
252
252
  */
253
- type ServerFunctionInit<TArgs extends FormData | JsonArray = JsonArray, TResult extends JsonValue = JsonValue> = (signal: AbortSignal, ...args: TArgs) => Promise<TResult>;
253
+ type ServerFunctionInit<TArgs extends FormData | JsonArray = JsonArray, TResult = JsonValue> = (signal: AbortSignal, ...args: TArgs) => Promise<TResult>;
254
254
  /**
255
255
  * Client-side stub signature generated for each server function.
256
256
  * Returns a promise-backed `data` handle plus a `cancel` function
257
257
  * that aborts the underlying fetch request.
258
258
  */
259
- type ClientFunction<TArgs extends JsonArray = JsonArray, TResult extends JsonValue = JsonValue> = (...args: TArgs) => {
259
+ type ClientFunction<TArgs extends JsonArray = JsonArray, TResult = JsonValue> = (...args: TArgs) => {
260
260
  /** Promise resolving to the server response data */
261
261
  data: Promise<TResult>;
262
262
  /** Aborts the in-flight request with the given reason */
@@ -345,6 +345,13 @@ interface RpcPluginOptions {
345
345
  * @default "exact"
346
346
  */
347
347
  serverFiles?: "exact" | "glob";
348
+ /**
349
+ * Suppress the "no RPC config found" warning when no config file is
350
+ * discovered. Useful when the plugin is wrapped by another tool that
351
+ * provides configuration externally (e.g. a meta-framework adapter).
352
+ * @default false
353
+ */
354
+ silent?: boolean;
348
355
  }
349
356
  interface MiddlewareOptions<A extends RpcPluginOptions["adapter"] = "express"> {
350
357
  /**
@@ -453,9 +460,12 @@ type InnerModReturn<T extends JsonValue> = {
453
460
  * Searches in order: `rpc.config.ts`, `rpc.config.js`, `rpc.config.mjs`, `rpc.config.mts`,
454
461
  * `.rpcrc.ts`, `.rpcrc.js`. Falls back to defaults if none found.
455
462
  * @param configFile - Optional explicit config file path; skips file search when provided
463
+ * @param opts - Optional settings; `silent` suppresses the "no config found" warning
456
464
  * @returns Resolved RPC plugin options
457
465
  */
458
- declare const loadRPCConfig: (f?: string) => Promise<RpcPluginOptions>;
466
+ declare const loadRPCConfig: (configFile?: string, opts?: {
467
+ silent?: boolean;
468
+ }) => Promise<RpcPluginOptions>;
459
469
  /**
460
470
  * Vite plugin that enables automatic RPC generation.
461
471
  * Transforms server function imports into fetch-based client stubs during development and production builds.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/express/types.d.ts","../src/hono/types.d.ts","../src/fastify/types.d.ts","../src/koa/types.d.ts","../src/h3/types.d.ts","../src/types.d.ts","../src/index.ts"],"mappings":";;;;;;;;;;;;;;;;KAgBY,2BAA2B;;;;;KAM3B,uBACV,UAAU,2CAEV,iBAAiB,QAAQ,8BACtB;;;;UAKY;;;;;;;EAOf,UACE,KAAK,kBAAkB,SACvB,KAAK,iBAAiB,YACtB,MAAM,QAAQ,eAAe,iBAC1B;;;;;;;UCzBU;;EAEf,SAAS;;;;;;KAOC,oBAAoB,UAAU,wCACxC,iBAAiB,QAAQ,oBAAkB,QACxC;;;;;;KCWO,2BAA2B;;;;;KAM3B,uBACV,UAAU,2CAEV,iBAAiB,QAAQ,8BACtB;;;;UAKY;;;;;;;EAOf,UACE,KAAK,gBACL,KAAK,cACL,MAAM,4BACH;;;;;;;KCtDK,uBAAuB;;;;UAalB;;;;;;EAMf,UAAU,KAAK,SAAS,MAAM,SAAS;;;;;;KAO7B,mBAAmB,UAAU,uCACvC,iBAAiB,QAAQ,0BACtB;;;;;;KChCO,sBAAsB;;;;UAKjB;;;;;;EAMf,SAAS;;;;;;KAOC,kBAAkB,UAAU,sCACtC,iBAAiB,QAAQ,yBACtB;;;;;;;UCJY;;EAEf,SAAS;;EAET,MAAM;;EAEN,SAAS;;EAET,KAAK;;EAEL,IAAI;;;;;UAMW;;EAEf,SAAS;;EAET,MAAM;;EAEN,SAAS;;EAET,KAAK;;EAEL,IAAI;;;;;;KAOM;;;;KAUA;;;;KASA;;;;KAKA;EACN;EAAiC,MAAM;;EACvC;EAA2B;;EAE7B;EACA,MAAM;;EAEJ;EAAoC,MAAM;;;;;;UAM/B;;;;;EAKf,aAAa;;;;;EAKb,cAAc;;;;;;;EAOd;;;;;EAKA;;;;;;KAOU;;;;KAIA;GAAgB,cAAc,YAAY;;;;;KAI1C,aAAa,WAAW;;;;KAIxB,YAAY,gBAAgB,YAAY;;;;;;;;;;;;;;;;;;;KAuBxC,mBAAmB;;;;;KAMnB,eACV,cAAc,YAAY,WAC1B,gBAAgB,YAAY,cACzB,QAAQ,gBAAgB,MAAM,UAAU,QAAQ;;;;;KAMzC,mBACV,cAAc,WAAW,YAAY,WACrC,gBAAgB,YAAY,cACzB,QAAQ,gBAAgB,MAAM,UAAU,QAAQ;;;;;;KAOzC,eACV,cAAc,YAAY,WAC1B,gBAAgB,YAAY,iBACtB,MAAM;;EAEZ,MAAM,QAAQ;;EAEd,SAAS;;;;;;KAOC,4BAA4B;;EAEtC;;EAEA,UAAU;;;;;UAMK;;EAEf;;EAEA;;;;;UAMe,mBAAmB,KAAK;EACvC;EACA,SAAS,QAAQ;EACjB;EACA;;EAEA;;;;;;UAOe;;EAEf;;EAEA,SAAS;;EAET,UAAU;;EAEV;;;;;;;;UASe;;;;;;;;;;;EAWf;;;;;;;EAQA;;;;;;;EAQA;;;;;;;EAQA;;UAGe,kBACf,UAAU;;;;EAKV;;;;;;;;;;;;EAaA,gBAAgB;;;;;;;;;;EAWhB;;;;;;;;EASA;;;;;;;;EASA;;;;;EAMA;;;;;;;;;;;;;;;;;;EAmBA,UAAU,eAAe;;;;;;UAOV;;;;;EAKf;;;;;EAKA,aAAa;;;;;;;EAOb,aAAa;;;;;;KAOH,eAAe,UAAU;;EAEnC,MAAM,QAAQ;;EAEd,SAAS;;;;;;;;;;;cCrVL,gBAAgB,eAAe,QAAQ;;;;;;;;iBA+FpC,UACP,aAAY,QAAQ,oBACnB"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/express/types.d.ts","../src/hono/types.d.ts","../src/fastify/types.d.ts","../src/koa/types.d.ts","../src/h3/types.d.ts","../src/types.d.ts","../src/index.ts"],"mappings":";;;;;;;;;;;;;;;;KAgBY,2BAA2B;;;;;KAM3B,uBACV,UAAU,2CAEV,iBAAiB,QAAQ,8BACtB;;;;UAKY;;;;;;;EAOf,UACE,KAAK,kBAAkB,SACvB,KAAK,iBAAiB,YACtB,MAAM,QAAQ,eAAe,iBAC1B;;;;;;;UCzBU;;EAEf,SAAS;;;;;;KAOC,oBAAoB,UAAU,wCACxC,iBAAiB,QAAQ,oBAAkB,QACxC;;;;;;KCWO,2BAA2B;;;;;KAM3B,uBACV,UAAU,2CAEV,iBAAiB,QAAQ,8BACtB;;;;UAKY;;;;;;;EAOf,UACE,KAAK,gBACL,KAAK,cACL,MAAM,4BACH;;;;;;;KCtDK,uBAAuB;;;;UAalB;;;;;;EAMf,UAAU,KAAK,SAAS,MAAM,SAAS;;;;;;KAO7B,mBAAmB,UAAU,uCACvC,iBAAiB,QAAQ,0BACtB;;;;;;KChCO,sBAAsB;;;;UAKjB;;;;;;EAMf,SAAS;;;;;;KAOC,kBAAkB,UAAU,sCACtC,iBAAiB,QAAQ,yBACtB;;;;;;;UCJY;;EAEf,SAAS;;EAET,MAAM;;EAEN,SAAS;;EAET,KAAK;;EAEL,IAAI;;;;;UAMW;;EAEf,SAAS;;EAET,MAAM;;EAEN,SAAS;;EAET,KAAK;;EAEL,IAAI;;;;;;KAOM;;;;KAUA;;;;KASA;;;;KAKA;EACN;EAAiC,MAAM;;EACvC;EAA2B;;EAE7B;EACA,MAAM;;EAEJ;EAAoC,MAAM;;;;;;UAM/B;;;;;EAKf,aAAa;;;;;EAKb,cAAc;;;;;;;EAOd;;;;;EAKA;;;;;;KAOU;;;;KAIA;GAAgB,cAAc,YAAY;;;;;KAI1C,aAAa,WAAW;;;;KAIxB,YAAY,gBAAgB,YAAY;;;;;;;;;;;;;;;;;;;KAuBxC,mBAAmB;;;;;KAMnB,eACV,cAAc,YAAY,WAC1B,UAAU,cACP,QAAQ,gBAAgB,MAAM,UAAU,QAAQ;;;;;KAMzC,mBACV,cAAc,WAAW,YAAY,WACrC,UAAU,cACP,QAAQ,gBAAgB,MAAM,UAAU,QAAQ;;;;;;KAOzC,eACV,cAAc,YAAY,WAC1B,UAAU,iBACJ,MAAM;;EAEZ,MAAM,QAAQ;;EAEd,SAAS;;;;;;KAOC,4BAA4B;;EAEtC;;EAEA,UAAU;;;;;UAMK;;EAEf;;EAEA;;;;;UAMe,mBAAmB,KAAK;EACvC;EACA,SAAS,QAAQ;EACjB;EACA;;EAEA;;;;;;UAOe;;EAEf;;EAEA,SAAS;;EAET,UAAU;;EAEV;;;;;;;;UASe;;;;;;;;;;;EAWf;;;;;;;EAQA;;;;;;;EAQA;;;;;;;EAQA;;;;;;;EAQA;;UAGe,kBACf,UAAU;;;;EAKV;;;;;;;;;;;;EAaA,gBAAgB;;;;;;;;;;EAWhB;;;;;;;;EASA;;;;;;;;EASA;;;;;EAMA;;;;;;;;;;;;;;;;;;EAmBA,UAAU,eAAe;;;;;;UAOV;;;;;EAKf;;;;;EAKA,aAAa;;;;;;;EAOb,aAAa;;;;;;KAOH,eAAe,UAAU;;EAEnC,MAAM,QAAQ;;EAEd,SAAS;;;;;;;;;;;;cC5VL,gBACJ,qBACA;EAAS;MACN,QAAQ;;;;;;;;iBA6FJ,UACP,aAAY,QAAQ,oBACnB"}
package/dist/index.mjs CHANGED
@@ -23,8 +23,8 @@ const INVALID_IDENTIFIER = (label, name) => `Invalid ${label}: "${name}" must ma
23
23
  const INVALID_PATH_SEGMENT = (label, segment) => `Invalid ${label}: "${segment}" must match /^[A-Za-z0-9_$@:][A-Za-z0-9_$@:/-]*$/`;
24
24
  /** Warning message when a specified RPC config file cannot be resolved on disk. @param configFile - The requested config filename. @param configFilePath - The resolved absolute path */
25
25
  const CONFIG_FILE_NOT_FOUND = (configFile, configFilePath) => ` ⚠︎ The specified RPC config file ${configFile} cannot be found at ${configFilePath}, loading the defaults..`;
26
- const NO_CONFIG_FOUND = ` ⚡︎ No RPC config found, loading the defaults..`;
27
- const FAILED_LOAD_CONFIG = ` ⚠︎ Failed to load RPC config:`;
26
+ const NO_CONFIG_FOUND = ` ⚡︎ No RPC config found, loading the defaults..`;
27
+ const FAILED_LOAD_CONFIG = ` ⚠︎ Failed to load RPC config:`;
28
28
  /** Error template for duplicate server function names across files. @param name - The duplicate registered name */
29
29
  const DUPLICATE_FUNCTION_NAME = (name) => `Duplicate server function "${name}" detected. Each server function must have a unique name. Remove or rename the duplicate.`;
30
30
  //#endregion
@@ -323,9 +323,10 @@ let RPCConfig;
323
323
  * Searches in order: `rpc.config.ts`, `rpc.config.js`, `rpc.config.mjs`, `rpc.config.mts`,
324
324
  * `.rpcrc.ts`, `.rpcrc.js`. Falls back to defaults if none found.
325
325
  * @param configFile - Optional explicit config file path; skips file search when provided
326
+ * @param opts - Optional settings; `silent` suppresses the "no config found" warning
326
327
  * @returns Resolved RPC plugin options
327
328
  */
328
- const loadRPCConfig = async (configFile) => {
329
+ const loadRPCConfig = async (configFile, opts) => {
329
330
  try {
330
331
  const env = {
331
332
  command: "serve",
@@ -376,7 +377,7 @@ const loadRPCConfig = async (configFile) => {
376
377
  }
377
378
  }
378
379
  RPCConfig = defaultRPCOptions;
379
- console.warn(NO_CONFIG_FOUND);
380
+ if (!opts?.silent) console.warn(NO_CONFIG_FOUND);
380
381
  } catch (error) {
381
382
  RPCConfig = defaultRPCOptions;
382
383
  console.warn(FAILED_LOAD_CONFIG, error);
@@ -400,7 +401,7 @@ function rpcPlugin(devOptions = {}) {
400
401
  name: "vite-plugin-universal-rpc",
401
402
  enforce: "pre",
402
403
  async configResolved(resolvedConfig) {
403
- const uniConfig = await loadRPCConfig();
404
+ const uniConfig = await loadRPCConfig(void 0, { silent: devOptions.silent });
404
405
  options = mergeConfig(uniConfig, devOptions);
405
406
  config = resolvedConfig;
406
407
  },
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/options.ts","../src/constants.ts","../src/functionsMap.ts","../src/validate.ts","../src/getClientModules.ts","../src/server-helpers.ts","../src/scanForServerFiles.ts","../src/index.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","export const OPERATION_ABORTED = \"Operation aborted\";\n\nexport const REQUEST_CANCELLED = \"Request was cancelled\";\n\nexport const FETCH_ERROR_PREFIX = \"Fetch error: \";\n\nexport const NO_SERVER_FUNCTION_FOUND = \"No server function found.\";\n\nexport const ERROR_LOADING_FILE = \"Error loading file:\";\n\nexport const FUNCTION_NOT_FOUND = \"Function not found\";\n\nexport const METHOD_NOT_ALLOWED = \"Method Not Allowed\";\n\nexport const REQUEST_FORBIDDEN = \"Forbidden\";\n\nexport const UNSUPPORTED_MEDIA_TYPE = \"Unsupported Media Type\";\n\nexport const BAD_REQUEST = \"Bad Request\";\n\nexport const INTERNAL_SERVER_ERROR = \"Internal Server Error\";\n\nexport const CLIENT_DISCONNECTED = \"client disconnected\";\n\n/** Returns a warning when a middleware name is reused, preventing registration conflicts. @param name - The duplicate middleware name */\nexport const MIDDLEWARE_NAME_USED = (name: string) =>\n `The middleware name \"${name}\" is already used.`;\n\n/** Error message when a value fails the safe-identifier validation. @param label - What kind of value was being validated. @param name - The rejected value */\nexport const INVALID_IDENTIFIER = (label: string, name: string) =>\n `Invalid ${label}: \"${name}\" must match /^[A-Za-z_$][A-Za-z0-9_$]*$/`;\n\n/** Error message when a value fails the safe-path-segment validation. @param label - What kind of value was being validated. @param segment - The rejected value */\nexport const INVALID_PATH_SEGMENT = (label: string, segment: string) =>\n `Invalid ${label}: \"${segment}\" must match /^[A-Za-z0-9_$@:][A-Za-z0-9_$@:/-]*$/`;\n\n/** Warning message when a specified RPC config file cannot be resolved on disk. @param configFile - The requested config filename. @param configFilePath - The resolved absolute path */\nexport const CONFIG_FILE_NOT_FOUND = (\n configFile: string,\n configFilePath: string,\n) =>\n ` ⚠︎ The specified RPC config file ${configFile} cannot be found at ${configFilePath}, loading the defaults..`;\n\nexport const NO_CONFIG_FOUND = ` ⚡︎ No RPC config found, loading the defaults..`;\n\nexport const FAILED_LOAD_CONFIG = ` ⚠︎ Failed to load RPC config:`;\n\n/** Error template for duplicate server function names across files. @param name - The duplicate registered name */\nexport const DUPLICATE_FUNCTION_NAME = (name: string) =>\n `Duplicate server function \"${name}\" detected. Each server function must have a unique name. Remove or rename the duplicate.`;\n","import type { 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","import type { Credentials } from \"./types.d.ts\";\nimport { INVALID_IDENTIFIER, INVALID_PATH_SEGMENT } from \"./constants.ts\";\n\nconst SAFE_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;\nconst SAFE_PATH_SEGMENT = /^[A-Za-z0-9_$@:][A-Za-z0-9_$@:/-]*$/;\nconst CREDENTIALS_VALUES: readonly Credentials[] = [\n \"same-origin\",\n \"include\",\n \"omit\",\n];\n\n/**\n * Validates that a string is a safe JavaScript identifier.\n * Used to prevent code injection when interpolating export names into generated client code.\n * @param name - The string to validate\n * @param label - Human-readable label for error messages (e.g. \"export name\")\n * @returns The validated name if it passes\n * @throws Error if the name contains characters outside /^[A-Za-z_$][A-Za-z0-9_$]*$/\n */\nexport function validateIdentifier(name: string, label: string): string {\n if (!SAFE_IDENTIFIER.test(name)) {\n throw new Error(INVALID_IDENTIFIER(label, name));\n }\n return name;\n}\n\n/**\n * Validates that a string is a safe path segment for RPC routing.\n * Allows alphanumeric characters, underscores, dollar signs, at signs,\n * colons, hyphens, and forward slashes.\n * @param segment - The string to validate\n * @param label - Human-readable label for error messages (e.g. \"rpcPrefix\")\n * @returns The validated segment if it passes\n * @throws Error if the segment contains disallowed characters\n */\nexport function validatePathSegment(segment: string, label: string): string {\n if (!SAFE_PATH_SEGMENT.test(segment)) {\n throw new Error(INVALID_PATH_SEGMENT(label, segment));\n }\n return segment;\n}\n\n/**\n * Validates and normalizes the credentials option.\n * Accepts \"same-origin\", \"include\", or \"omit\"; defaults to \"same-origin\" when undefined.\n * @param value - Credentials value to validate\n * @returns The validated credentials string\n * @throws Error if the value is not one of the accepted credentials\n */\nexport function validateCredentials(value?: string): Credentials {\n const creds = value || \"same-origin\";\n if (!CREDENTIALS_VALUES.includes(creds as Credentials)) {\n throw new Error(\n `Invalid credentials: \"${value}\" must be one of ${\n CREDENTIALS_VALUES.join(\", \")\n }`,\n );\n }\n return creds as Credentials;\n}\n\n/**\n * Validates and normalizes the HTTP method option for a server function.\n * Accepts \"GET\" or \"POST\" (case-insensitive); defaults to \"POST\" when undefined.\n * @param value - Method value to validate\n * @returns The validated uppercase method string\n * @throws Error if the value is not \"GET\" or \"POST\"\n */\nexport function validateMethod(value?: string): \"GET\" | \"POST\" {\n const method = (value || \"POST\").toUpperCase();\n if (method !== \"GET\" && method !== \"POST\") {\n throw new Error(`Invalid method: \"${value}\" must be one of GET, POST`);\n }\n return method;\n}\n","/**\n * @module Client module generation.\n */\nimport type {\n RpcPluginOptionsInternal,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\nimport { getFunctionsForPrefix } from \"./functionsMap.ts\";\nimport {\n validateCredentials,\n validateIdentifier,\n validateMethod,\n validatePathSegment,\n} from \"./validate.ts\";\n\n/**\n * Generates a JavaScript client module string for a single server function.\n * All interpolated values are validated to prevent code injection.\n * @param fnName - Registered RPC function name (validated as path segment)\n * @param fnEntry - Export name used in the generated module (validated as identifier)\n * @param options - Content type, credentials, and RPC prefix settings\n * @returns A string of JavaScript code exporting the client stub\n */\nconst getModule = (\n fnName: string,\n fnEntry: string,\n options: Partial<ServerFunctionOptions> & {\n contentType: ServerFunctionOptions[\"contentType\"];\n rpcPrefix: string;\n },\n): string => {\n // Validate all interpolated strings to prevent code injection\n const safeFnName = validatePathSegment(fnName, \"function name\");\n const safeFnEntry = validateIdentifier(fnEntry, \"export name\");\n const safePrefix = validatePathSegment(options.rpcPrefix, \"rpcPrefix\");\n const credentials = validateCredentials(options.credentials);\n const method = validateMethod(options.method);\n const contentType =\n (options.contentType ?? \"application/json\") as ServerFunctionOptions[\n \"contentType\"\n ];\n\n const opts: string[] = [];\n if (method !== \"POST\") opts.push(`method: \"${method}\"`);\n if (credentials !== \"same-origin\") opts.push(`credentials: \"${credentials}\"`);\n if (contentType !== \"application/json\") {\n opts.push(`contentType: \"${contentType}\"`);\n }\n const optsStr = opts.length ? `, { ${opts.join(\", \")} }` : \"\";\n\n const output = `\n export const ${safeFnEntry} = getClientStub(\"${safePrefix}\", \"${safeFnName}\"${optsStr});`;\n\n return output.trim();\n};\n\n/**\n * Generates the complete client-side module bundle by iterating all registered server functions\n * for a specific prefix and producing fetch-based stubs for each. The result is transformed by Vite\n * (or Oxc) during the dev server or production build.\n * @param initialOptions - Plugin options containing rpcPrefix and optional adapter\n * @returns A string of JavaScript code with all client RPC modules and their import dependencies\n */\nexport const getClientModules = (\n initialOptions: RpcPluginOptionsInternal,\n): string => {\n // Validate prefix once at the top level\n validatePathSegment(initialOptions.rpcPrefix, \"rpcPrefix\");\n\n // Get functions registered for this specific prefix\n const prefixMap = getFunctionsForPrefix(initialOptions.rpcPrefix);\n const entries = Array.from(prefixMap.entries())\n .filter(([, entry]) => entry.exportName)\n .map(([registeredName, entry]) =>\n getModule(registeredName, entry.exportName!, {\n ...initialOptions,\n ...((entry.options as ServerFunctionOptions) || {}),\n })\n )\n .join(\"\\n\");\n\n const output = `\n// Client-side RPC modules for prefix: ${initialOptions.rpcPrefix}\nimport { getClientStub } from \"@thednp/rpc/helpers\";\n${entries}`;\n\n return output.trim();\n};\n","/** @module Server-side utilities. Exports the `RPCError` class for typed server-side errors, `formatError` for middleware error responses, `isFormContentType` and `hasContentTypeMismatch` for content-type validation, and `walkGlobFiles` for recursively discovering `*.server.*` files. Never import this module in client code — it is server-only. */\nimport type { ContentType, JsonObject, JsonValue } from \"./types.d.ts\";\nimport { readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport { INTERNAL_SERVER_ERROR } from \"./constants.ts\";\n\nconst GLOB_REGEX = /^.+\\.server\\.(ts|js|mjs|mts)$/;\n\n/**\n * Recursively walks `dir` and collects absolute paths to files whose\n * basename matches the `*.server.{ts,js,mjs,mts}` glob pattern.\n */\nexport const walkGlobFiles = async (dir: string): Promise<string[]> => {\n const results: string[] = [];\n const stack = [dir];\n while (stack.length) {\n const current = stack.pop()!;\n let entries;\n try {\n entries = await readdir(current, { withFileTypes: true });\n } catch (_e) {\n continue;\n }\n for (const entry of entries) {\n const fullPath = join(current, entry.name);\n if (entry.isFile() && GLOB_REGEX.test(entry.name)) {\n results.push(fullPath);\n } else if (entry.isDirectory()) {\n stack.push(fullPath);\n }\n }\n }\n return results;\n};\n\n/**\n * A typed error thrown from server functions.\n * The middleware serializes the `message` and `code` in the response,\n * allowing clients to recognise and handle specific error conditions.\n */\nexport class RPCError extends Error {\n /** Machine-readable error code (e.g. \"VALIDATION_FAILED\", \"UNAUTHORIZED\") */\n code: string;\n /** Optional diagnostic payload */\n data?: JsonValue;\n constructor(message: string, code = \"INTERNAL\", data?: JsonValue) {\n super(message);\n this.name = \"RPCError\";\n this.code = code;\n this.data = data;\n }\n}\n\n/**\n * Formats an error for the RPC middleware response.\n * In development the full `RPCError` payload is included so developers\n * can quickly identify issues. Unexpected exceptions never expose their\n * message — only the generic \"Internal Server Error\" is sent, preventing\n * information disclosure; server-side diagnostics are preserved via the\n * middleware's `console.error` logging.\n */\nexport const formatError = (\n err: unknown,\n isProduction: boolean,\n): JsonObject => {\n if (isProduction) {\n return { error: INTERNAL_SERVER_ERROR };\n }\n if (err instanceof RPCError) {\n const payload: JsonObject = {\n error: err.message || INTERNAL_SERVER_ERROR,\n code: err.code,\n };\n if (err.data !== undefined) payload.data = err.data;\n return payload;\n }\n return { error: INTERNAL_SERVER_ERROR };\n};\n\n/**\n * Checks whether a content type maps to a form encoding\n * (`multipart/form-data` or `application/x-www-form-urlencoded`).\n * Form-declared functions accept either encoding so native browser\n * submissions (urlencoded) keep working without JavaScript.\n */\nexport const isFormContentType = (contentType: string): boolean =>\n contentType === \"multipart/form-data\" ||\n contentType === \"application/x-www-form-urlencoded\";\n\n/**\n * Detects whether an incoming request's `Content-Type` header conflicts\n * with the function's declared content type. JSON and text functions are\n * enforced strictly (exact match wins), while form functions accept both\n * form encodings because the nojs fallback submits urlencoded forms to\n * multipart-declared endpoints. Requests without a `Content-Type` header\n * (curl, GET, legacy clients) are exempt from enforcement.\n * @param declared - The declared `contentType` from the server function options\n * @param rawHeader - The raw `Content-Type` request header, if present\n */\nexport const hasContentTypeMismatch = (\n declared: ContentType,\n rawHeader: string | undefined,\n): boolean => {\n // No Content-Type header → exempt (url bar, GET, curl compatibility)\n if (!rawHeader) return false;\n // Strip parameters (charset, boundary) before comparison\n const incomingType = rawHeader.trim().toLowerCase().split(\";\")[0].trim();\n if (isFormContentType(declared)) {\n // Forms: reject only non-form encodings (lenient between the two)\n return !isFormContentType(incomingType);\n }\n return incomingType !== declared;\n};\n\n/**\n * Escapes special regex metacharacters in a string.\n * Used to safely embed user-configurable values (like rpcPrefix) into regular expressions,\n * preventing ReDoS and regex injection attacks.\n * @param s - The raw string to escape\n * @returns The escaped string safe for use in new RegExp()\n */\nexport function escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\nconst SAFE_URL_BASE = \"http://localhost\";\n\n/**\n * Parses a raw request URL against a fixed base without ever throwing.\n * Malformed request-targets (e.g. `/\\`, `//`, `/\\/`) make the WHATWG URL\n * parser throw `TypeError: Invalid URL`; the adapters call this while\n * building the per-request URL **before** their dispatch `try` block, so an\n * unhandled rejection there crashes raw `node:http` hosts (and Express 4).\n * On failure we fall back to the base root: the resulting pathname never\n * matches the RPC prefix, so the request is treated as non-RPC and falls\n * through to `next()` / 404 instead of crashing the process.\n * @param rawUrl - Raw request URL (path + optional query string)\n * @param base - Optional base URL, defaults to a fixed localhost origin\n * @returns A URL object; never throws\n */\nexport const safeURL = (rawUrl: string, base = SAFE_URL_BASE): URL => {\n try {\n return new URL(rawUrl, base);\n } catch {\n return new URL(\"/\", base);\n }\n};\n\nconst globalPrefixSymbol = Symbol.for(\"thednp.rpc.globalPrefix\");\n\n/** Global rpcPrefix from the last loaded config / middleware — fallback for functions without explicit prefix. */\nexport const getGlobalPrefix = (): string | undefined =>\n (globalThis as unknown as Record<symbol, string | undefined>)[\n globalPrefixSymbol\n ];\n\nexport const setGlobalPrefix = (prefix: string | undefined): void => {\n if (prefix) {\n (globalThis as unknown as Record<symbol, string | undefined>)[\n globalPrefixSymbol\n ] = prefix;\n } else {\n delete (globalThis as unknown as Record<symbol, string | undefined>)[\n globalPrefixSymbol\n ];\n }\n};\n","import type { ViteDevServer } from \"vite\";\nimport type { ClientFunctionWithOptions, ScanConfig } from \"./types.d.ts\";\nimport { readdir } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\nimport process from \"node:process\";\n\nimport { getFunctionsForPrefix } from \"./functionsMap.ts\";\nimport { defaultPrefix } from \"./options.ts\";\nimport { walkGlobFiles } from \"./server-helpers.ts\";\nimport {\n DUPLICATE_FUNCTION_NAME,\n ERROR_LOADING_FILE,\n NO_SERVER_FUNCTION_FOUND,\n} from \"./constants.ts\";\n\nlet isScanned = false;\n\n/** Absolute ids (normalized) of the scanned server function files. */\nexport const scannedServerFiles: Set<string> = new Set<string>();\n\nconst EXACT_NAMES = [\"server.ts\", \"server.js\", \"server.mjs\", \"server.mts\"];\n\n/**\n * Scans `src/api/` (or an explicit `scanRoot`) for server function files\n * and populates the server functions map (scoped by rpcPrefix) with their exported functions.\n * Uses Vite's SSR module loading to resolve and execute each file.\n *\n * Supports two matching modes via `config.serverFiles`:\n * `\"exact\"` — classic `server.ts|js|mjs|mts` names in the api directory\n * `\"glob\"` — recursively walking `scanRoot` to match `*.server.{ts,js,mjs,mts}`\n * @param initialCfg - Optional Vite config overrides (root, base, server, serverFiles, scanRoot)\n * @param devServer - Optional running Vite dev server instance; when provided, skips creating a new one\n */\nexport const scanForServerFiles = async (\n initialCfg?: ScanConfig,\n devServer?: ViteDevServer,\n): Promise<void> => {\n if (isScanned && !devServer) {\n return;\n }\n // Vite is only needed to spin up the internal dev server that loads the\n // server function files, so it is imported lazily rather than at the top\n // of the module. This keeps consumers of the standalone server entry that\n // register their functions directly (e.g. serverless functions bundling\n // the API module) free of a static Vite dependency — when Vite is\n // externalized by the function bundler, this lazy import is left as a\n // runtime require that is never executed.\n let createServer: typeof import(\"vite\").createServer;\n let normalizePath: typeof import(\"vite\").normalizePath;\n try {\n ({ createServer, normalizePath } = await import(\"vite\"));\n } catch {\n // Vite is not installed in this environment (e.g. a serverless bundle\n // where Vite is externalized or absent). Server functions must have been\n // imported directly into the prefix-scoped map; nothing to scan — exit\n // gracefully instead of crashing the host's cold start with\n // `Cannot find module 'vite'`.\n return;\n }\n const config = (!initialCfg && !devServer) || !initialCfg\n ? {\n root: process.cwd(),\n base: process.env.BASE || \"/\",\n server: { middlewareMode: true },\n }\n : {\n ...initialCfg,\n };\n\n let server = devServer;\n if (!server) {\n server = await createServer({\n server: { ...config.server, ws: false },\n appType: \"custom\",\n base: config.base || \"/\",\n root: config.root || process.cwd(),\n // The internal server is only used to load the server function files:\n // skip the project config so its plugins (including this one) do not\n // re-trigger a nested scan via `configureServer`.\n configFile: false,\n // The internal server never serves a page or HMR, so no dependency\n // optimization or WebSocket server is needed. Without `ws: false`, the\n // middleware-mode server creates a standalone HMR WebSocket on port\n // 24678, and concurrent scans (e.g. the Express middleware's lazy scan\n // racing the plugin scan) fail with EADDRINUSE. Without `noDiscovery`,\n // the default optimizers scan the project entry and pre-bundle the whole\n // `vite` package (imported by the linked @thednp/rpc dist files),\n // hanging startup at 2+ GB RSS.\n optimizeDeps: { noDiscovery: true },\n ssr: { optimizeDeps: { noDiscovery: true } },\n });\n }\n\n const root = config.root || process.cwd();\n const resolvedScanRoot = resolve(\n root,\n config.scanRoot ?? join(root, \"src\", \"api\"),\n );\n const serverFiles: \"exact\" | \"glob\" = config.serverFiles ??\n \"exact\";\n\n // Names registered during this scan run, used for duplicate detection.\n // Keyed by `${prefix}:${registeredName}` so the same name can coexist\n // under different rpcPrefixes (see the registration loop below).\n const seenNames = new Set<string>();\n\n let files: string[];\n try {\n if (serverFiles === \"glob\") {\n files = await walkGlobFiles(resolvedScanRoot);\n } else {\n files = (await readdir(resolvedScanRoot, { withFileTypes: true }))\n .filter((f) => EXACT_NAMES.includes(f.name))\n .map((f) => join(resolvedScanRoot, f.name));\n }\n } catch (_e) {\n files = [];\n }\n\n try {\n for (const file of files) {\n scannedServerFiles.add(normalizePath(file));\n let moduleExports: Record<string, ClientFunctionWithOptions>;\n try {\n moduleExports = (await server.ssrLoadModule(file)) as Record<\n string,\n ClientFunctionWithOptions\n >;\n } catch (error) {\n console.error(ERROR_LOADING_FILE, file, error);\n continue;\n }\n const moduleEntries = Object.entries(moduleExports);\n if (!moduleEntries.length) {\n console.warn(NO_SERVER_FUNCTION_FOUND);\n return;\n }\n\n // Register each export into its prefix-scoped map, recording the\n // original export name so `getClientModules` can emit the matching\n // client stub. `createServerFunction` already auto-registers its name\n // into the appropriate prefix-scoped map at module load, so a function\n // may already exist here — in that case only the export name is added.\n // Track names seen in THIS scan run only, keyed by prefix: a name\n // repeated within one scan under the same prefix (e.g. two files\n // exporting the same function name) is a genuine conflict.\n for (const [exportName, exportValue] of moduleEntries) {\n const registeredName = exportValue.name;\n const prefix = exportValue.options?.rpcPrefix ||\n config.rpcPrefix ||\n defaultPrefix;\n const seenKey = `${prefix}:${registeredName}`;\n if (seenNames.has(seenKey)) {\n if (process.env.NODE_ENV !== \"production\") {\n throw new Error(DUPLICATE_FUNCTION_NAME(registeredName));\n }\n console.warn(DUPLICATE_FUNCTION_NAME(registeredName));\n continue;\n }\n seenNames.add(seenKey);\n const prefixMap = getFunctionsForPrefix(prefix);\n const existing = prefixMap.get(registeredName);\n if (existing) {\n existing.exportName = exportName;\n } else {\n prefixMap.set(registeredName, {\n name: registeredName,\n handler: exportValue,\n options: exportValue.options,\n exportName,\n });\n }\n }\n }\n } finally {\n if (!devServer && server) {\n await server.close();\n }\n isScanned = true;\n }\n};\n","/** @module Main entrypoint for the RPC Vite plugin. Exports `rpcPlugin` (default) and `loadRPCConfig`. For a Vite-free `defineConfig`, use `@thednp/rpc/config`. */\nimport type { ConfigEnv, Plugin, ResolvedConfig, ViteDevServer } from \"vite\";\nimport { loadConfigFromFile, mergeConfig } from \"vite\";\nimport { resolve } from \"node:path\";\nimport process from \"node:process\";\nimport { existsSync } from \"node:fs\";\nimport { defaultRPCOptions } from \"./options.ts\";\nimport type { RpcPluginOptions, ScanConfig } from \"./types.d.ts\";\nimport {\n CONFIG_FILE_NOT_FOUND,\n FAILED_LOAD_CONFIG,\n NO_CONFIG_FOUND,\n} from \"./constants.ts\";\n\nimport { getClientModules } from \"./getClientModules.ts\";\n// DEV server only\nimport {\n scanForServerFiles,\n scannedServerFiles,\n} from \"./scanForServerFiles.ts\";\nimport { serverFunctionsMap } from \"./functionsMap.ts\";\n\nimport { setGlobalPrefix } from \"@thednp/rpc/server\";\nimport { createRPCMiddleware } from \"@thednp/rpc/express\";\n\n/**\n * Loads and transforms a single RPC config file using Vite's config loader.\n * @param env - Vite config environment\n * @param file - Config file path (e.g. \"rpc.config.ts\")\n * @returns The loaded config augmented with the configFile path, or null on failure\n */\nconst loadConfigFile = async (env: ConfigEnv, file: string) => {\n const result = await loadConfigFromFile(env, file) as {\n path: string;\n config: Partial<RpcPluginOptions>;\n dependencies: string[];\n } | null;\n return result\n ? { ...result, config: { ...result.config, configFile: file } }\n : /* istanbul ignore next */ null;\n};\n\nlet RPCConfig: RpcPluginOptions;\n\n/**\n * Loads the RPC configuration by searching for config files in the project root.\n * Searches in order: `rpc.config.ts`, `rpc.config.js`, `rpc.config.mjs`, `rpc.config.mts`,\n * `.rpcrc.ts`, `.rpcrc.js`. Falls back to defaults if none found.\n * @param configFile - Optional explicit config file path; skips file search when provided\n * @returns Resolved RPC plugin options\n */\nconst loadRPCConfig: (f?: string) => Promise<RpcPluginOptions> = async (\n configFile?: string,\n) => {\n try {\n // istanbul ignore next\n const env: ConfigEnv & { root: string } = {\n command: \"serve\",\n root: process.cwd(),\n mode: process.env.NODE_ENV || \"development\",\n };\n const defaultConfigFiles = [\n \"rpc.config.ts\",\n \"rpc.config.js\",\n \"rpc.config.mjs\",\n \"rpc.config.mts\",\n \".rpcrc.ts\",\n \".rpcrc.js\",\n ];\n\n // If specific config file provided\n if (configFile) {\n const configFilePath = resolve(env.root, configFile);\n if (!existsSync(configFilePath)) {\n console.warn(CONFIG_FILE_NOT_FOUND(configFile, configFilePath));\n RPCConfig = defaultRPCOptions;\n setGlobalPrefix(defaultRPCOptions.rpcPrefix);\n return defaultRPCOptions as RpcPluginOptions;\n }\n\n const result = await loadConfigFile(env, configFile);\n // istanbul ignore else\n if (result && typeof result === \"object\") {\n RPCConfig = mergeConfig(\n {\n ...defaultRPCOptions,\n configFile: configFilePath,\n },\n result.config,\n ) as RpcPluginOptions;\n\n setGlobalPrefix(RPCConfig.rpcPrefix);\n return RPCConfig;\n }\n // istanbul ignore next - this is a necessary fallback here\n RPCConfig = defaultRPCOptions;\n }\n\n if (RPCConfig !== undefined) {\n setGlobalPrefix(RPCConfig.rpcPrefix);\n\n return RPCConfig;\n }\n\n // Try default config files\n for (const file of defaultConfigFiles) {\n const configFilePath = resolve(env.root, file);\n // istanbul ignore else\n if (!existsSync(configFilePath)) {\n continue;\n }\n\n const result = await loadConfigFile(env, file);\n // istanbul ignore else\n if (result) {\n RPCConfig = mergeConfig(\n {\n ...defaultRPCOptions,\n configFile: configFilePath,\n },\n result.config,\n ) as RpcPluginOptions;\n\n return RPCConfig;\n }\n }\n RPCConfig = defaultRPCOptions;\n // Last call load defaults no matter what\n console.warn(NO_CONFIG_FOUND);\n } catch (error) {\n RPCConfig = defaultRPCOptions;\n console.warn(FAILED_LOAD_CONFIG, error);\n }\n\n setGlobalPrefix(RPCConfig.rpcPrefix);\n\n return RPCConfig;\n};\n\n/**\n * Vite plugin that enables automatic RPC generation.\n * Transforms server function imports into fetch-based client stubs during development and production builds.\n * In dev mode, attaches the RPC middleware to Vite's Connect server.\n * @param devOptions - Development-only overrides (merged on top of config file values)\n * @returns A Vite plugin object\n */\nfunction rpcPlugin(\n devOptions: Partial<RpcPluginOptions> = {},\n): Plugin {\n // Internal type - adapters are handled at runtime\n let options: RpcPluginOptions & { rpcPrefix: string } = mergeConfig(\n defaultRPCOptions,\n devOptions,\n ) as RpcPluginOptions;\n let config: ResolvedConfig;\n let viteServer: ViteDevServer;\n let isOxc = true;\n\n return {\n name: \"vite-plugin-universal-rpc\",\n enforce: \"pre\",\n // Plugin methods\n async configResolved(resolvedConfig) {\n const uniConfig = await loadRPCConfig();\n options = mergeConfig(uniConfig, devOptions) as RpcPluginOptions;\n\n config = resolvedConfig;\n },\n async configureServer(server) {\n viteServer = server;\n const { adapter: _adapter, ...rest } = options;\n // istanbul ignore else\n if (serverFunctionsMap.size === 0) {\n const scanCfg: ScanConfig = {\n ...config,\n serverFiles: options.serverFiles,\n scanRoot: options.scanRoot,\n rpcPrefix: options.rpcPrefix,\n };\n await scanForServerFiles(scanCfg, viteServer);\n }\n\n // in dev mode we always use express/connect adapter\n server.middlewares.use(createRPCMiddleware(rest));\n },\n\n async buildStart() {\n const viteVersion = this.meta?.viteVersion;\n isOxc = Number(viteVersion[0]) >= 8;\n\n // Prepare the server functions\n if (!viteServer && config) {\n const scanCfg: ScanConfig = {\n ...config,\n serverFiles: options.serverFiles,\n scanRoot: options.scanRoot,\n rpcPrefix: options.rpcPrefix,\n };\n await scanForServerFiles(scanCfg);\n }\n },\n async transform(code: string, id: string, ops?: { ssr?: boolean }) {\n // Only transform files with server functions for client builds\n if (\n !code.includes(\"createServerFunction\") || // any other file is unchanged\n ops?.ssr || // file loaded on server remains unchanged\n (code.includes(\"createServerFunction\") &&\n typeof process === \"undefined\") // file loaded in client IS CHANGED\n ) {\n return null;\n }\n\n const vite = await import(\"vite\");\n\n if (serverFunctionsMap.size === 0) {\n const scanCfg: ScanConfig = {\n ...config,\n serverFiles: options.serverFiles,\n scanRoot: options.scanRoot,\n rpcPrefix: options.rpcPrefix,\n };\n await scanForServerFiles(scanCfg);\n }\n\n // Only transform modules that were scanned as server function files:\n // pages or docs mentioning `createServerFunction` in prose must not\n // be rewritten into the generated client bundle.\n const idPath = vite.normalizePath(id.split(\"?\")[0]);\n if (!scannedServerFiles.has(idPath)) {\n return null;\n }\n\n const transformer = isOxc ? \"transformWithOxc\" : \"transformWithEsbuild\";\n const langProp = isOxc ? \"lang\" : \"loader\";\n const source = getClientModules({\n rpcPrefix: options.rpcPrefix,\n adapter: options.adapter,\n });\n\n const result = await vite[transformer](source, id, {\n [langProp]: \"js\",\n sourcemap: true,\n // target: \"es2023\"\n });\n\n return {\n code: result.code,\n map: result.map\n ? typeof result.map === \"string\"\n ? JSON.parse(result.map)\n : /* istanbul ignore next @preserve */ result.map\n : /* istanbul ignore next @preserve */ null,\n };\n },\n } satisfies Plugin;\n}\n\nexport { rpcPlugin as default };\nexport { loadRPCConfig };\nexport type * from \"./types.d.ts\";\nexport {};\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/options.ts","../src/constants.ts","../src/functionsMap.ts","../src/validate.ts","../src/getClientModules.ts","../src/server-helpers.ts","../src/scanForServerFiles.ts","../src/index.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","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 =\n ` ⚡︎ No RPC config found, loading the defaults..`;\n\nexport const FAILED_LOAD_CONFIG = ` ⚠︎ Failed to load RPC config:`;\n\n/** Error template for duplicate server function names across files. @param name - The duplicate registered name */\nexport const DUPLICATE_FUNCTION_NAME = (name: string) =>\n `Duplicate server function \"${name}\" detected. Each server function must have a unique name. Remove or rename the duplicate.`;\n","import type { 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","import type { Credentials } from \"./types.d.ts\";\nimport { INVALID_IDENTIFIER, INVALID_PATH_SEGMENT } from \"./constants.ts\";\n\nconst SAFE_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;\nconst SAFE_PATH_SEGMENT = /^[A-Za-z0-9_$@:][A-Za-z0-9_$@:/-]*$/;\nconst CREDENTIALS_VALUES: readonly Credentials[] = [\n \"same-origin\",\n \"include\",\n \"omit\",\n];\n\n/**\n * Validates that a string is a safe JavaScript identifier.\n * Used to prevent code injection when interpolating export names into generated client code.\n * @param name - The string to validate\n * @param label - Human-readable label for error messages (e.g. \"export name\")\n * @returns The validated name if it passes\n * @throws Error if the name contains characters outside /^[A-Za-z_$][A-Za-z0-9_$]*$/\n */\nexport function validateIdentifier(name: string, label: string): string {\n if (!SAFE_IDENTIFIER.test(name)) {\n throw new Error(INVALID_IDENTIFIER(label, name));\n }\n return name;\n}\n\n/**\n * Validates that a string is a safe path segment for RPC routing.\n * Allows alphanumeric characters, underscores, dollar signs, at signs,\n * colons, hyphens, and forward slashes.\n * @param segment - The string to validate\n * @param label - Human-readable label for error messages (e.g. \"rpcPrefix\")\n * @returns The validated segment if it passes\n * @throws Error if the segment contains disallowed characters\n */\nexport function validatePathSegment(segment: string, label: string): string {\n if (!SAFE_PATH_SEGMENT.test(segment)) {\n throw new Error(INVALID_PATH_SEGMENT(label, segment));\n }\n return segment;\n}\n\n/**\n * Validates and normalizes the credentials option.\n * Accepts \"same-origin\", \"include\", or \"omit\"; defaults to \"same-origin\" when undefined.\n * @param value - Credentials value to validate\n * @returns The validated credentials string\n * @throws Error if the value is not one of the accepted credentials\n */\nexport function validateCredentials(value?: string): Credentials {\n const creds = value || \"same-origin\";\n if (!CREDENTIALS_VALUES.includes(creds as Credentials)) {\n throw new Error(\n `Invalid credentials: \"${value}\" must be one of ${\n CREDENTIALS_VALUES.join(\", \")\n }`,\n );\n }\n return creds as Credentials;\n}\n\n/**\n * Validates and normalizes the HTTP method option for a server function.\n * Accepts \"GET\" or \"POST\" (case-insensitive); defaults to \"POST\" when undefined.\n * @param value - Method value to validate\n * @returns The validated uppercase method string\n * @throws Error if the value is not \"GET\" or \"POST\"\n */\nexport function validateMethod(value?: string): \"GET\" | \"POST\" {\n const method = (value || \"POST\").toUpperCase();\n if (method !== \"GET\" && method !== \"POST\") {\n throw new Error(`Invalid method: \"${value}\" must be one of GET, POST`);\n }\n return method;\n}\n","/**\n * @module Client module generation.\n */\nimport type {\n RpcPluginOptionsInternal,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\nimport { getFunctionsForPrefix } from \"./functionsMap.ts\";\nimport {\n validateCredentials,\n validateIdentifier,\n validateMethod,\n validatePathSegment,\n} from \"./validate.ts\";\n\n/**\n * Generates a JavaScript client module string for a single server function.\n * All interpolated values are validated to prevent code injection.\n * @param fnName - Registered RPC function name (validated as path segment)\n * @param fnEntry - Export name used in the generated module (validated as identifier)\n * @param options - Content type, credentials, and RPC prefix settings\n * @returns A string of JavaScript code exporting the client stub\n */\nconst getModule = (\n fnName: string,\n fnEntry: string,\n options: Partial<ServerFunctionOptions> & {\n contentType: ServerFunctionOptions[\"contentType\"];\n rpcPrefix: string;\n },\n): string => {\n // Validate all interpolated strings to prevent code injection\n const safeFnName = validatePathSegment(fnName, \"function name\");\n const safeFnEntry = validateIdentifier(fnEntry, \"export name\");\n const safePrefix = validatePathSegment(options.rpcPrefix, \"rpcPrefix\");\n const credentials = validateCredentials(options.credentials);\n const method = validateMethod(options.method);\n const contentType =\n (options.contentType ?? \"application/json\") as ServerFunctionOptions[\n \"contentType\"\n ];\n\n const opts: string[] = [];\n if (method !== \"POST\") opts.push(`method: \"${method}\"`);\n if (credentials !== \"same-origin\") opts.push(`credentials: \"${credentials}\"`);\n if (contentType !== \"application/json\") {\n opts.push(`contentType: \"${contentType}\"`);\n }\n const optsStr = opts.length ? `, { ${opts.join(\", \")} }` : \"\";\n\n const output = `\n export const ${safeFnEntry} = getClientStub(\"${safePrefix}\", \"${safeFnName}\"${optsStr});`;\n\n return output.trim();\n};\n\n/**\n * Generates the complete client-side module bundle by iterating all registered server functions\n * for a specific prefix and producing fetch-based stubs for each. The result is transformed by Vite\n * (or Oxc) during the dev server or production build.\n * @param initialOptions - Plugin options containing rpcPrefix and optional adapter\n * @returns A string of JavaScript code with all client RPC modules and their import dependencies\n */\nexport const getClientModules = (\n initialOptions: RpcPluginOptionsInternal,\n): string => {\n // Validate prefix once at the top level\n validatePathSegment(initialOptions.rpcPrefix, \"rpcPrefix\");\n\n // Get functions registered for this specific prefix\n const prefixMap = getFunctionsForPrefix(initialOptions.rpcPrefix);\n const entries = Array.from(prefixMap.entries())\n .filter(([, entry]) => entry.exportName)\n .map(([registeredName, entry]) =>\n getModule(registeredName, entry.exportName!, {\n ...initialOptions,\n ...((entry.options as ServerFunctionOptions) || {}),\n })\n )\n .join(\"\\n\");\n\n const output = `\n// Client-side RPC modules for prefix: ${initialOptions.rpcPrefix}\nimport { getClientStub } from \"@thednp/rpc/helpers\";\n${entries}`;\n\n return output.trim();\n};\n","/** @module Server-side utilities. Exports the `RPCError` class for typed server-side errors, `formatError` for middleware error responses, `isFormContentType` and `hasContentTypeMismatch` for content-type validation, and `walkGlobFiles` for recursively discovering `*.server.*` files. Never import this module in client code — it is server-only. */\nimport type { ContentType, JsonObject, JsonValue } from \"./types.d.ts\";\nimport { readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport { INTERNAL_SERVER_ERROR } from \"./constants.ts\";\n\nconst GLOB_REGEX = /^.+\\.server\\.(ts|js|mjs|mts)$/;\n\n/**\n * Recursively walks `dir` and collects absolute paths to files whose\n * basename matches the `*.server.{ts,js,mjs,mts}` glob pattern.\n */\nexport const walkGlobFiles = async (dir: string): Promise<string[]> => {\n const results: string[] = [];\n const stack = [dir];\n while (stack.length) {\n const current = stack.pop()!;\n let entries;\n try {\n entries = await readdir(current, { withFileTypes: true });\n } catch (_e) {\n continue;\n }\n for (const entry of entries) {\n const fullPath = join(current, entry.name);\n if (entry.isFile() && GLOB_REGEX.test(entry.name)) {\n results.push(fullPath);\n } else if (entry.isDirectory()) {\n stack.push(fullPath);\n }\n }\n }\n return results;\n};\n\n/**\n * A typed error thrown from server functions.\n * The middleware serializes the `message` and `code` in the response,\n * allowing clients to recognise and handle specific error conditions.\n */\nexport class RPCError extends Error {\n /** Machine-readable error code (e.g. \"VALIDATION_FAILED\", \"UNAUTHORIZED\") */\n code: string;\n /** Optional diagnostic payload */\n data?: JsonValue;\n constructor(message: string, code = \"INTERNAL\", data?: JsonValue) {\n super(message);\n this.name = \"RPCError\";\n this.code = code;\n this.data = data;\n }\n}\n\n/**\n * Formats an error for the RPC middleware response.\n * In development the full `RPCError` payload is included so developers\n * can quickly identify issues. Unexpected exceptions never expose their\n * message — only the generic \"Internal Server Error\" is sent, preventing\n * information disclosure; server-side diagnostics are preserved via the\n * middleware's `console.error` logging.\n */\nexport const formatError = (\n err: unknown,\n isProduction: boolean,\n): JsonObject => {\n if (isProduction) {\n return { error: INTERNAL_SERVER_ERROR };\n }\n if (err instanceof RPCError) {\n const payload: JsonObject = {\n error: err.message || INTERNAL_SERVER_ERROR,\n code: err.code,\n };\n if (err.data !== undefined) payload.data = err.data;\n return payload;\n }\n return { error: INTERNAL_SERVER_ERROR };\n};\n\n/**\n * Checks whether a content type maps to a form encoding\n * (`multipart/form-data` or `application/x-www-form-urlencoded`).\n * Form-declared functions accept either encoding so native browser\n * submissions (urlencoded) keep working without JavaScript.\n */\nexport const isFormContentType = (contentType: string): boolean =>\n contentType === \"multipart/form-data\" ||\n contentType === \"application/x-www-form-urlencoded\";\n\n/**\n * Detects whether an incoming request's `Content-Type` header conflicts\n * with the function's declared content type. JSON and text functions are\n * enforced strictly (exact match wins), while form functions accept both\n * form encodings because the nojs fallback submits urlencoded forms to\n * multipart-declared endpoints. Requests without a `Content-Type` header\n * (curl, GET, legacy clients) are exempt from enforcement.\n * @param declared - The declared `contentType` from the server function options\n * @param rawHeader - The raw `Content-Type` request header, if present\n */\nexport const hasContentTypeMismatch = (\n declared: ContentType,\n rawHeader: string | undefined,\n): boolean => {\n // No Content-Type header → exempt (url bar, GET, curl compatibility)\n if (!rawHeader) return false;\n // Strip parameters (charset, boundary) before comparison\n const incomingType = rawHeader.trim().toLowerCase().split(\";\")[0].trim();\n if (isFormContentType(declared)) {\n // Forms: reject only non-form encodings (lenient between the two)\n return !isFormContentType(incomingType);\n }\n return incomingType !== declared;\n};\n\n/**\n * Escapes special regex metacharacters in a string.\n * Used to safely embed user-configurable values (like rpcPrefix) into regular expressions,\n * preventing ReDoS and regex injection attacks.\n * @param s - The raw string to escape\n * @returns The escaped string safe for use in new RegExp()\n */\nexport function escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\nconst SAFE_URL_BASE = \"http://localhost\";\n\n/**\n * Parses a raw request URL against a fixed base without ever throwing.\n * Malformed request-targets (e.g. `/\\`, `//`, `/\\/`) make the WHATWG URL\n * parser throw `TypeError: Invalid URL`; the adapters call this while\n * building the per-request URL **before** their dispatch `try` block, so an\n * unhandled rejection there crashes raw `node:http` hosts (and Express 4).\n * On failure we fall back to the base root: the resulting pathname never\n * matches the RPC prefix, so the request is treated as non-RPC and falls\n * through to `next()` / 404 instead of crashing the process.\n * @param rawUrl - Raw request URL (path + optional query string)\n * @param base - Optional base URL, defaults to a fixed localhost origin\n * @returns A URL object; never throws\n */\nexport const safeURL = (rawUrl: string, base = SAFE_URL_BASE): URL => {\n try {\n return new URL(rawUrl, base);\n } catch {\n return new URL(\"/\", base);\n }\n};\n\nconst globalPrefixSymbol = Symbol.for(\"thednp.rpc.globalPrefix\");\n\n/** Global rpcPrefix from the last loaded config / middleware — fallback for functions without explicit prefix. */\nexport const getGlobalPrefix = (): string | undefined =>\n (globalThis as unknown as Record<symbol, string | undefined>)[\n globalPrefixSymbol\n ];\n\nexport const setGlobalPrefix = (prefix: string | undefined): void => {\n if (prefix) {\n (globalThis as unknown as Record<symbol, string | undefined>)[\n globalPrefixSymbol\n ] = prefix;\n } else {\n delete (globalThis as unknown as Record<symbol, string | undefined>)[\n globalPrefixSymbol\n ];\n }\n};\n","import type { ViteDevServer } from \"vite\";\nimport type { ClientFunctionWithOptions, ScanConfig } from \"./types.d.ts\";\nimport { readdir } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\nimport process from \"node:process\";\n\nimport { getFunctionsForPrefix } from \"./functionsMap.ts\";\nimport { defaultPrefix } from \"./options.ts\";\nimport { walkGlobFiles } from \"./server-helpers.ts\";\nimport {\n DUPLICATE_FUNCTION_NAME,\n ERROR_LOADING_FILE,\n NO_SERVER_FUNCTION_FOUND,\n} from \"./constants.ts\";\n\nlet isScanned = false;\n\n/** Absolute ids (normalized) of the scanned server function files. */\nexport const scannedServerFiles: Set<string> = new Set<string>();\n\nconst EXACT_NAMES = [\"server.ts\", \"server.js\", \"server.mjs\", \"server.mts\"];\n\n/**\n * Scans `src/api/` (or an explicit `scanRoot`) for server function files\n * and populates the server functions map (scoped by rpcPrefix) with their exported functions.\n * Uses Vite's SSR module loading to resolve and execute each file.\n *\n * Supports two matching modes via `config.serverFiles`:\n * `\"exact\"` — classic `server.ts|js|mjs|mts` names in the api directory\n * `\"glob\"` — recursively walking `scanRoot` to match `*.server.{ts,js,mjs,mts}`\n * @param initialCfg - Optional Vite config overrides (root, base, server, serverFiles, scanRoot)\n * @param devServer - Optional running Vite dev server instance; when provided, skips creating a new one\n */\nexport const scanForServerFiles = async (\n initialCfg?: ScanConfig,\n devServer?: ViteDevServer,\n): Promise<void> => {\n if (isScanned && !devServer) {\n return;\n }\n // Vite is only needed to spin up the internal dev server that loads the\n // server function files, so it is imported lazily rather than at the top\n // of the module. This keeps consumers of the standalone server entry that\n // register their functions directly (e.g. serverless functions bundling\n // the API module) free of a static Vite dependency — when Vite is\n // externalized by the function bundler, this lazy import is left as a\n // runtime require that is never executed.\n let createServer: typeof import(\"vite\").createServer;\n let normalizePath: typeof import(\"vite\").normalizePath;\n try {\n ({ createServer, normalizePath } = await import(\"vite\"));\n } catch {\n // Vite is not installed in this environment (e.g. a serverless bundle\n // where Vite is externalized or absent). Server functions must have been\n // imported directly into the prefix-scoped map; nothing to scan — exit\n // gracefully instead of crashing the host's cold start with\n // `Cannot find module 'vite'`.\n return;\n }\n const config = (!initialCfg && !devServer) || !initialCfg\n ? {\n root: process.cwd(),\n base: process.env.BASE || \"/\",\n server: { middlewareMode: true },\n }\n : {\n ...initialCfg,\n };\n\n let server = devServer;\n if (!server) {\n server = await createServer({\n server: { ...config.server, ws: false },\n appType: \"custom\",\n base: config.base || \"/\",\n root: config.root || process.cwd(),\n // The internal server is only used to load the server function files:\n // skip the project config so its plugins (including this one) do not\n // re-trigger a nested scan via `configureServer`.\n configFile: false,\n // The internal server never serves a page or HMR, so no dependency\n // optimization or WebSocket server is needed. Without `ws: false`, the\n // middleware-mode server creates a standalone HMR WebSocket on port\n // 24678, and concurrent scans (e.g. the Express middleware's lazy scan\n // racing the plugin scan) fail with EADDRINUSE. Without `noDiscovery`,\n // the default optimizers scan the project entry and pre-bundle the whole\n // `vite` package (imported by the linked @thednp/rpc dist files),\n // hanging startup at 2+ GB RSS.\n optimizeDeps: { noDiscovery: true },\n ssr: { optimizeDeps: { noDiscovery: true } },\n });\n }\n\n const root = config.root || process.cwd();\n const resolvedScanRoot = resolve(\n root,\n config.scanRoot ?? join(root, \"src\", \"api\"),\n );\n const serverFiles: \"exact\" | \"glob\" = config.serverFiles ??\n \"exact\";\n\n // Names registered during this scan run, used for duplicate detection.\n // Keyed by `${prefix}:${registeredName}` so the same name can coexist\n // under different rpcPrefixes (see the registration loop below).\n const seenNames = new Set<string>();\n\n let files: string[];\n try {\n if (serverFiles === \"glob\") {\n files = await walkGlobFiles(resolvedScanRoot);\n } else {\n files = (await readdir(resolvedScanRoot, { withFileTypes: true }))\n .filter((f) => EXACT_NAMES.includes(f.name))\n .map((f) => join(resolvedScanRoot, f.name));\n }\n } catch (_e) {\n files = [];\n }\n\n try {\n for (const file of files) {\n scannedServerFiles.add(normalizePath(file));\n let moduleExports: Record<string, ClientFunctionWithOptions>;\n try {\n moduleExports = (await server.ssrLoadModule(file)) as Record<\n string,\n ClientFunctionWithOptions\n >;\n } catch (error) {\n console.error(ERROR_LOADING_FILE, file, error);\n continue;\n }\n const moduleEntries = Object.entries(moduleExports);\n if (!moduleEntries.length) {\n console.warn(NO_SERVER_FUNCTION_FOUND);\n return;\n }\n\n // Register each export into its prefix-scoped map, recording the\n // original export name so `getClientModules` can emit the matching\n // client stub. `createServerFunction` already auto-registers its name\n // into the appropriate prefix-scoped map at module load, so a function\n // may already exist here — in that case only the export name is added.\n // Track names seen in THIS scan run only, keyed by prefix: a name\n // repeated within one scan under the same prefix (e.g. two files\n // exporting the same function name) is a genuine conflict.\n for (const [exportName, exportValue] of moduleEntries) {\n const registeredName = exportValue.name;\n const prefix = exportValue.options?.rpcPrefix ||\n config.rpcPrefix ||\n defaultPrefix;\n const seenKey = `${prefix}:${registeredName}`;\n if (seenNames.has(seenKey)) {\n if (process.env.NODE_ENV !== \"production\") {\n throw new Error(DUPLICATE_FUNCTION_NAME(registeredName));\n }\n console.warn(DUPLICATE_FUNCTION_NAME(registeredName));\n continue;\n }\n seenNames.add(seenKey);\n const prefixMap = getFunctionsForPrefix(prefix);\n const existing = prefixMap.get(registeredName);\n if (existing) {\n existing.exportName = exportName;\n } else {\n prefixMap.set(registeredName, {\n name: registeredName,\n handler: exportValue,\n options: exportValue.options,\n exportName,\n });\n }\n }\n }\n } finally {\n if (!devServer && server) {\n await server.close();\n }\n isScanned = true;\n }\n};\n","/** @module Main entrypoint for the RPC Vite plugin. Exports `rpcPlugin` (default) and `loadRPCConfig`. For a Vite-free `defineConfig`, use `@thednp/rpc/config`. */\nimport type { ConfigEnv, Plugin, ResolvedConfig, ViteDevServer } from \"vite\";\nimport { loadConfigFromFile, mergeConfig } from \"vite\";\nimport { resolve } from \"node:path\";\nimport process from \"node:process\";\nimport { existsSync } from \"node:fs\";\nimport { defaultRPCOptions } from \"./options.ts\";\nimport type { RpcPluginOptions, ScanConfig } from \"./types.d.ts\";\nimport {\n CONFIG_FILE_NOT_FOUND,\n FAILED_LOAD_CONFIG,\n NO_CONFIG_FOUND,\n} from \"./constants.ts\";\n\nimport { getClientModules } from \"./getClientModules.ts\";\n// DEV server only\nimport {\n scanForServerFiles,\n scannedServerFiles,\n} from \"./scanForServerFiles.ts\";\nimport { serverFunctionsMap } from \"./functionsMap.ts\";\n\nimport { setGlobalPrefix } from \"@thednp/rpc/server\";\nimport { createRPCMiddleware } from \"@thednp/rpc/express\";\n\n/**\n * Loads and transforms a single RPC config file using Vite's config loader.\n * @param env - Vite config environment\n * @param file - Config file path (e.g. \"rpc.config.ts\")\n * @returns The loaded config augmented with the configFile path, or null on failure\n */\nconst loadConfigFile = async (env: ConfigEnv, file: string) => {\n const result = await loadConfigFromFile(env, file) as {\n path: string;\n config: Partial<RpcPluginOptions>;\n dependencies: string[];\n } | null;\n return result\n ? { ...result, config: { ...result.config, configFile: file } }\n : /* istanbul ignore next */ null;\n};\n\nlet RPCConfig: RpcPluginOptions;\n\n/**\n * Loads the RPC configuration by searching for config files in the project root.\n * Searches in order: `rpc.config.ts`, `rpc.config.js`, `rpc.config.mjs`, `rpc.config.mts`,\n * `.rpcrc.ts`, `.rpcrc.js`. Falls back to defaults if none found.\n * @param configFile - Optional explicit config file path; skips file search when provided\n * @param opts - Optional settings; `silent` suppresses the \"no config found\" warning\n * @returns Resolved RPC plugin options\n */\nconst loadRPCConfig: (\n configFile?: string,\n opts?: { silent?: boolean },\n) => Promise<RpcPluginOptions> = async (configFile?, opts?) => {\n try {\n // istanbul ignore next\n const env: ConfigEnv & { root: string } = {\n command: \"serve\",\n root: process.cwd(),\n mode: process.env.NODE_ENV || \"development\",\n };\n const defaultConfigFiles = [\n \"rpc.config.ts\",\n \"rpc.config.js\",\n \"rpc.config.mjs\",\n \"rpc.config.mts\",\n \".rpcrc.ts\",\n \".rpcrc.js\",\n ];\n\n // If specific config file provided\n if (configFile) {\n const configFilePath = resolve(env.root, configFile);\n if (!existsSync(configFilePath)) {\n console.warn(CONFIG_FILE_NOT_FOUND(configFile, configFilePath));\n RPCConfig = defaultRPCOptions;\n setGlobalPrefix(defaultRPCOptions.rpcPrefix);\n return defaultRPCOptions as RpcPluginOptions;\n }\n\n const result = await loadConfigFile(env, configFile);\n // istanbul ignore else\n if (result && typeof result === \"object\") {\n RPCConfig = mergeConfig(\n {\n ...defaultRPCOptions,\n configFile: configFilePath,\n },\n result.config,\n ) as RpcPluginOptions;\n\n setGlobalPrefix(RPCConfig.rpcPrefix);\n return RPCConfig;\n }\n // istanbul ignore next - this is a necessary fallback here\n RPCConfig = defaultRPCOptions;\n }\n\n if (RPCConfig !== undefined) {\n setGlobalPrefix(RPCConfig.rpcPrefix);\n\n return RPCConfig;\n }\n\n // Try default config files\n for (const file of defaultConfigFiles) {\n const configFilePath = resolve(env.root, file);\n // istanbul ignore else\n if (!existsSync(configFilePath)) {\n continue;\n }\n\n const result = await loadConfigFile(env, file);\n // istanbul ignore else\n if (result) {\n RPCConfig = mergeConfig(\n {\n ...defaultRPCOptions,\n configFile: configFilePath,\n },\n result.config,\n ) as RpcPluginOptions;\n\n return RPCConfig;\n }\n }\n RPCConfig = defaultRPCOptions;\n // Last call load defaults no matter what\n if (!opts?.silent) console.warn(NO_CONFIG_FOUND);\n } catch (error) {\n RPCConfig = defaultRPCOptions;\n console.warn(FAILED_LOAD_CONFIG, error);\n }\n\n setGlobalPrefix(RPCConfig.rpcPrefix);\n\n return RPCConfig;\n};\n\n/**\n * Vite plugin that enables automatic RPC generation.\n * Transforms server function imports into fetch-based client stubs during development and production builds.\n * In dev mode, attaches the RPC middleware to Vite's Connect server.\n * @param devOptions - Development-only overrides (merged on top of config file values)\n * @returns A Vite plugin object\n */\nfunction rpcPlugin(\n devOptions: Partial<RpcPluginOptions> = {},\n): Plugin {\n // Internal type - adapters are handled at runtime\n let options: RpcPluginOptions & { rpcPrefix: string } = mergeConfig(\n defaultRPCOptions,\n devOptions,\n ) as RpcPluginOptions;\n let config: ResolvedConfig;\n let viteServer: ViteDevServer;\n let isOxc = true;\n\n return {\n name: \"vite-plugin-universal-rpc\",\n enforce: \"pre\",\n // Plugin methods\n async configResolved(resolvedConfig) {\n const uniConfig = await loadRPCConfig(undefined, {\n silent: devOptions.silent,\n });\n options = mergeConfig(uniConfig, devOptions) as RpcPluginOptions;\n\n config = resolvedConfig;\n },\n async configureServer(server) {\n viteServer = server;\n const { adapter: _adapter, ...rest } = options;\n // istanbul ignore else\n if (serverFunctionsMap.size === 0) {\n const scanCfg: ScanConfig = {\n ...config,\n serverFiles: options.serverFiles,\n scanRoot: options.scanRoot,\n rpcPrefix: options.rpcPrefix,\n };\n await scanForServerFiles(scanCfg, viteServer);\n }\n\n // in dev mode we always use express/connect adapter\n server.middlewares.use(createRPCMiddleware(rest));\n },\n\n async buildStart() {\n const viteVersion = this.meta?.viteVersion;\n isOxc = Number(viteVersion[0]) >= 8;\n\n // Prepare the server functions\n if (!viteServer && config) {\n const scanCfg: ScanConfig = {\n ...config,\n serverFiles: options.serverFiles,\n scanRoot: options.scanRoot,\n rpcPrefix: options.rpcPrefix,\n };\n await scanForServerFiles(scanCfg);\n }\n },\n async transform(code: string, id: string, ops?: { ssr?: boolean }) {\n // Only transform files with server functions for client builds\n if (\n !code.includes(\"createServerFunction\") || // any other file is unchanged\n ops?.ssr || // file loaded on server remains unchanged\n (code.includes(\"createServerFunction\") &&\n typeof process === \"undefined\") // file loaded in client IS CHANGED\n ) {\n return null;\n }\n\n const vite = await import(\"vite\");\n\n if (serverFunctionsMap.size === 0) {\n const scanCfg: ScanConfig = {\n ...config,\n serverFiles: options.serverFiles,\n scanRoot: options.scanRoot,\n rpcPrefix: options.rpcPrefix,\n };\n await scanForServerFiles(scanCfg);\n }\n\n // Only transform modules that were scanned as server function files:\n // pages or docs mentioning `createServerFunction` in prose must not\n // be rewritten into the generated client bundle.\n const idPath = vite.normalizePath(id.split(\"?\")[0]);\n if (!scannedServerFiles.has(idPath)) {\n return null;\n }\n\n const transformer = isOxc ? \"transformWithOxc\" : \"transformWithEsbuild\";\n const langProp = isOxc ? \"lang\" : \"loader\";\n const source = getClientModules({\n rpcPrefix: options.rpcPrefix,\n adapter: options.adapter,\n });\n\n const result = await vite[transformer](source, id, {\n [langProp]: \"js\",\n sourcemap: true,\n // target: \"es2023\"\n });\n\n return {\n code: result.code,\n map: result.map\n ? typeof result.map === \"string\"\n ? JSON.parse(result.map)\n : /* istanbul ignore next @preserve */ result.map\n : /* istanbul ignore next @preserve */ null,\n };\n },\n } satisfies Plugin;\n}\n\nexport { rpcPlugin as default };\nexport { loadRPCConfig };\nexport type * from \"./types.d.ts\";\nexport {};\n"],"mappings":"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"}
@@ -40,7 +40,7 @@ type KoaMiddlewareFn = <A extends RpcPluginOptions["adapter"] = "koa">(initialOp
40
40
  * @param initialOptions - Options for rpcPrefix, path matching, and the handler function
41
41
  * @returns A Koa middleware function
42
42
  */
43
- declare const createMiddleware: KoaMiddlewareFn;
43
+ export declare const createMiddleware: KoaMiddlewareFn;
44
44
  /**
45
45
  * Creates the Koa RPC middleware that routes incoming requests to registered server functions.
46
46
  * Wraps the generic createMiddleware with the RPC handler that reads the body, dispatches
@@ -48,7 +48,7 @@ declare const createMiddleware: KoaMiddlewareFn;
48
48
  * @param initialOptions - Options including rpcPrefix for URL routing
49
49
  * @returns A Koa middleware function
50
50
  */
51
- declare const createRPCMiddleware: KoaMiddlewareFn;
51
+ export declare const createRPCMiddleware: KoaMiddlewareFn;
52
52
  //#endregion
53
53
  //#region src/koa/helpers.d.ts
54
54
  /**
@@ -56,7 +56,7 @@ declare const createRPCMiddleware: KoaMiddlewareFn;
56
56
  * Dynamically imports loadRPCConfig and registers the middleware.
57
57
  * @param app - Koa application instance
58
58
  */
59
- declare function attachRPC(app: Koa): Promise<void>;
59
+ export declare function attachRPC(app: Koa): Promise<void>;
60
60
  /**
61
61
  * Attaches Vite's dev server middlewares to a Koa app for development mode.
62
62
  * Bridges Koa's context-based middleware to Vite's Connect-compatible middleware stack
@@ -64,7 +64,7 @@ declare function attachRPC(app: Koa): Promise<void>;
64
64
  * @param app - Koa application instance
65
65
  * @param vite - Running Vite dev server
66
66
  */
67
- declare function attachVite(app: Koa, vite: ViteDevServer): void;
67
+ export declare function attachVite(app: Koa, vite: ViteDevServer): void;
68
68
  /**
69
69
  * Reads and parses the HTTP request body from a Koa context.
70
70
  * If koa-body or another body parser already consumed the stream,
@@ -72,7 +72,7 @@ declare function attachVite(app: Koa, vite: ViteDevServer): void;
72
72
  * @param ctx - Koa context
73
73
  * @returns A promise resolving to the parsed body with its content type
74
74
  */
75
- declare const readBody: (ctx: KoaContext) => Promise<BodyResult>;
75
+ export declare const readBody: (ctx: KoaContext) => Promise<BodyResult>;
76
76
  /**
77
77
  * Issues an HTTP redirect on a Koa context. Koa's `ctx.redirect(location)`
78
78
  * defaults to `302` and sets the `Location` header; the status code must be
@@ -83,7 +83,7 @@ declare const readBody: (ctx: KoaContext) => Promise<BodyResult>;
83
83
  * @param location - The URL to redirect to
84
84
  * @param status - HTTP status code, defaults to 303
85
85
  */
86
- declare const redirect: (ctx: KoaContext, location: string, status?: number) => void;
86
+ export declare const redirect: (ctx: KoaContext, location: string, status?: number) => void;
87
87
  //#endregion
88
- export { type Koa, type KoaContext, type KoaMiddlewareFn, type KoaMiddlewareHooks, type KoaMiddlewareOptions, type KoaNext, attachRPC, attachVite, createMiddleware, createRPCMiddleware, readBody, redirect };
88
+ export type { Koa, KoaContext, KoaMiddlewareFn, KoaMiddlewareHooks, KoaMiddlewareOptions, KoaNext };
89
89
  //# sourceMappingURL=koa.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"koa.d.mts","names":[],"sources":["../../src/koa/types.d.ts","../../src/koa/createMiddleware.ts","../../src/koa/helpers.ts"],"mappings":";;;;;;;KAWY,uBAAuB;;;;UAKlB,mBAAmB;;EAElC,SAAS;IAAuB,gBAAgB;;;;;;UAMjC;;;;;;EAMf,UAAU,KAAK,SAAS,MAAM,SAAS;;;;;;KAO7B,mBAAmB,UAAU,qCACvC,iBAAiB,QAAQ,0BACtB;;;;;;;;;;cCGQ,kBAAkB;;;;;;;;cA4ElB,qBAAqB;;;;;;;;iBCzGZ,UAAU,KAAK,MAAG;;;;;;;;iBAkBxB,WAAW,KAAK,KAAK,MAAM;;;;;;;;cAuC9B,WAAQ,KACd,eACJ,QAAQ;;;;;;;;;;;cA0FE,WAAQ,KACd,YAAU,kBACC"}
1
+ {"version":3,"file":"koa.d.mts","names":[],"sources":["../../src/koa/types.d.ts","../../src/koa/createMiddleware.ts","../../src/koa/helpers.ts"],"mappings":";;;;;;;KAWY,uBAAuB;;;;UAKlB,mBAAmB;;EAElC,SAAS;IAAuB,gBAAgB;;;;;;UAMjC;;;;;;EAMf,UAAU,KAAK,SAAS,MAAM,SAAS;;;;;;KAO7B,mBAAmB,UAAU,qCACvC,iBAAiB,QAAQ,0BACtB;;;;;;;;;;qBCGQ,kBAAkB;;;;;;;;qBA4ElB,qBAAqB;;;;;;;;wBCzGZ,UAAU,KAAK,MAAG;;;;;;;;wBAkBxB,WAAW,KAAK,KAAK,MAAM;;;;;;;;qBAuC9B,WAAQ,KACd,eACJ,QAAQ;;;;;;;;;;;qBA0FE,WAAQ,KACd,YAAU,kBACC"}