@thednp/rpc 0.0.13 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/options.ts","../src/express/helpers.ts","../src/tools.ts","../src/constants.ts","../src/express/createMiddleware.ts","../src/index.ts"],"sourcesContent":["import type {\n MiddlewareOptions,\n RpcPluginOptions,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\n\nexport const defaultServerFnOptions = {\n contentType: \"application/json\",\n credentials: \"same-origin\",\n method: \"POST\",\n} satisfies ServerFunctionOptions;\n\nexport const defaultRPCOptions: RpcPluginOptions = {\n rpcPrefix: \"__rpc\",\n adapter: \"express\",\n};\n\nexport const defaultMiddlewareOptions = {\n rpcPrefix: undefined,\n path: undefined,\n origin: undefined,\n} satisfies MiddlewareOptions;\n","// src/express/helpers.ts\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type {\n Request as ExpressRequest,\n Response as ExpressResponse,\n} from \"express\";\nimport type { BodyResult, JsonValue } from \"@thednp/rpc\";\nimport type { Buffer } from \"node:buffer\";\nimport type { ViteDevServer } from \"vite\";\nimport type { Express } from \"express\";\nimport { createRPCMiddleware } from \"./createMiddleware.ts\";\nimport type { RequestDetails, ResponseDetails } from \"./types.d.ts\";\n\n/**\n * Convenience function to load RPC config and attach the RPC middleware to an Express app.\n * Dynamically imports loadRPCConfig and creates the middleware with loaded options.\n * @param app - Express application instance\n */\nexport async function attachRPC(app: Express) {\n const { loadRPCConfig } = await import(\"@thednp/rpc\");\n const { adapter: _adapter, ...options } = await loadRPCConfig();\n app.use(createRPCMiddleware(options));\n}\n\n/**\n * Attaches Vite's dev server middlewares to an Express app for development mode.\n * @param app - Express application instance\n * @param vite - Running Vite dev server\n */\nexport function attachVite(app: Express, vite: ViteDevServer) {\n app.use(vite.middlewares);\n}\n\n/**\n * Reads and parses the HTTP request body from an Express or Node IncomingMessage.\n * If a body parser middleware (e.g. express.json()) already consumed the stream,\n * uses the pre-parsed body from `req.body`.\n * @param req - Express or Node.js IncomingMessage\n * @returns A promise resolving to the parsed body with its content type\n */\nexport const readBody = (\n req: ExpressRequest | IncomingMessage,\n): Promise<BodyResult> => {\n return new Promise((resolve, reject) => {\n // If an Express body parser already consumed the stream\n // via app.use(express.json()), use req.body directly\n if (hasPreParsedBody(req) && req.body !== undefined) {\n // istanbul ignore next\n const contentType = req.headers[\"content-type\"]?.toLowerCase() || \"\";\n const isJSON = contentType.includes(\"json\");\n resolve({\n contentType: isJSON ? \"application/json\" : \"text/plain\",\n data: isJSON ? req.body : String(req.body),\n } as BodyResult);\n return;\n }\n\n // Else we parse the body right away\n let body = \"\";\n\n const toggleListeners = (add?: boolean) => {\n const method = add ? \"on\" : \"off\";\n req[method](\"data\", onData);\n req[method](\"end\", onEnd);\n req[method](\"error\", onError);\n };\n\n const onData = (chunk: Buffer) => {\n body += chunk.toString();\n };\n\n const onEnd = () => {\n toggleListeners();\n const incomingType = req.headers[\"content-type\"]?.toLowerCase() || \"\";\n const isJSON = incomingType.includes(\"json\");\n try {\n const data = JSON.parse(body);\n resolve({\n contentType: isJSON ? \"application/json\" : \"text/plain\",\n data,\n });\n } catch (_e) {\n resolve({ contentType: \"text/plain\", data: String(body) });\n }\n };\n\n const onError = (err: Error) => {\n toggleListeners();\n\n reject(err);\n };\n\n toggleListeners(true);\n });\n};\n\n/**\n * Type guard that checks whether a request is an Express Request (has `originalUrl`).\n * @param req - A Node IncomingMessage or Express Request\n * @returns True if the request is an Express Request\n */\nexport const isExpressRequest = (\n req: IncomingMessage | ExpressRequest,\n): req is ExpressRequest => {\n return \"originalUrl\" in req;\n};\n\n/**\n * Type guard that checks whether a response is an Express Response (has `json` and `send` methods).\n * @param res - A Node ServerResponse or Express Response\n * @returns True if the response is an Express Response\n */\nexport const isExpressResponse = (\n res: ServerResponse | ExpressResponse,\n): res is ExpressResponse => {\n return \"json\" in res && \"send\" in res;\n};\n\n/**\n * Type guard that checks whether a request has a pre-parsed body (`body` property).\n * Used to detect if a body-parser middleware already consumed the stream.\n * @param req - A Node IncomingMessage or Express Request\n * @returns True if the request has a body property\n */\nexport const hasPreParsedBody = (\n req: IncomingMessage | ExpressRequest,\n): req is ExpressRequest => {\n return \"body\" in req;\n};\n\n/**\n * Extracts normalized request details from an Express or Node IncomingMessage.\n * Parses the URL to extract pathname, search string, and search params.\n * @param request - Express or Node.js request object\n * @returns Normalized request details including URL, headers, and method\n */\nexport const getRequestDetails = (\n request: ExpressRequest | IncomingMessage,\n): RequestDetails => {\n const rawUrl = (\n isExpressRequest(request) ? request.originalUrl : request.url\n ) as string;\n const url = new URL(rawUrl, \"http://localhost\");\n\n return {\n url: url.pathname,\n search: url.search,\n searchParams: url.searchParams,\n headers: request.headers,\n method: request.method,\n };\n};\n\n/**\n * Wraps an Express or Node ServerResponse with a uniform API for setting headers,\n * status codes, and sending JSON responses. Handles the Express vs raw Node API differences.\n * @param response - Express or Node.js server response object\n * @returns A ResponseDetails object with setHeader, setStatusCode, and sendResponse helpers\n */\nexport const getResponseDetails = (\n response: ExpressResponse | ServerResponse,\n): ResponseDetails => {\n const isResponseSent = response.headersSent || response.writableEnded;\n\n const setHeader = (name: string, value: string) => {\n if (isExpressResponse(response)) {\n response.header(name, value);\n } else {\n response.setHeader(name, value);\n }\n };\n\n const setStatusCode = (code: number) => {\n if (isExpressResponse(response)) {\n response.status(code);\n } else {\n response.statusCode = code;\n }\n };\n\n const sendResponse = (code: number, output: Record<string, JsonValue>) => {\n setStatusCode(code);\n setHeader(\"Content-Type\", \"application/json\");\n\n if (isExpressResponse(response)) {\n response.send(JSON.stringify(output));\n } else {\n response.end(JSON.stringify(output));\n }\n };\n\n return {\n isResponseSent,\n setHeader,\n statusCode: response.statusCode,\n setStatusCode,\n sendResponse,\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","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 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","// src/express/createMidleware.ts\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type {\n NextFunction,\n Request as ExpressRequest,\n Response as ExpressResponse,\n} from \"express\";\nimport type {\n ExpressMiddlewareFn,\n ExpressMiddlewareOptions,\n} from \"./types.d.ts\";\nimport type { Connect } from \"vite\";\n// import type { JsonArray, JsonValue } from \"@thednp/rpc\";\nimport type { JsonValue } from \"../types.d.ts\";\nimport { scanForServerFiles, serverFunctionsMap } from \"@thednp/rpc/server\";\nimport { defaultMiddlewareOptions, defaultRPCOptions } from \"../options.ts\";\nimport { getRequestDetails, getResponseDetails, readBody } from \"./helpers.ts\";\nimport { escapeRegExp } from \"../tools.ts\";\nimport {\n CLIENT_DISCONNECTED,\n FUNCTION_NOT_FOUND,\n INTERNAL_SERVER_ERROR,\n METHOD_NOT_ALLOWED,\n MIDDLEWARE_NAME_USED,\n REQUEST_FORBIDDEN,\n} from \"../constants.ts\";\n\nlet middlewareCount = 0;\nconst middlewareStack = new Set<string>();\n\n/**\n * Creates an Express middleware with optional path and rpcPrefix filtering.\n * Middleware names are deduplicated — reusing a name throws an error.\n * Prefix and path regexes are compiled once at creation time (hoisted) for performance.\n * @param initialOptions - Options for rpcPrefix, path matching, and the handler function\n * @returns An Express middleware function\n */\nexport const createMiddleware: ExpressMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n initialOptions,\n ) as ExpressMiddlewareOptions;\n const middlewareName = options.name;\n const rpcPrefix = options.rpcPrefix;\n const path = options.path;\n const handler = options.handler;\n\n let name = middlewareName;\n if (!name) {\n name = \"viteRPCMiddleware-\" + middlewareCount;\n middlewareCount += 1;\n }\n if (middlewareStack.has(name)) {\n throw new Error(MIDDLEWARE_NAME_USED(name));\n }\n middlewareStack.add(name);\n\n // Hoist regex compilation out of per-request path. Escape the prefix to\n // prevent regex injection via metacharacters in the config string.\n const prefixRegex: RegExp | null = rpcPrefix\n ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`)\n : null;\n const pathMatcher: RegExp | null = path\n ? (typeof path === \"string\" ? new RegExp(path) : path)\n : null;\n\n const middlewareHandler = async (\n req: IncomingMessage | ExpressRequest,\n res: ServerResponse | ExpressResponse,\n next: Connect.NextFunction | NextFunction,\n ) => {\n const { url } = getRequestDetails(req);\n\n // When serving from production server, scan for server files\n if (serverFunctionsMap.size === 0) {\n await scanForServerFiles();\n }\n\n // No need to continue when no handler provided\n if (!handler) {\n return next?.();\n }\n\n // Path matching\n if (pathMatcher && !pathMatcher.test(url)) return next?.();\n\n // rpcPrefix matching (boundary-safe via escaped regex)\n if (prefixRegex && !prefixRegex.test(url)) {\n return next?.();\n }\n\n // Execute handler\n await handler(req, res, next);\n };\n\n Object.defineProperty(middlewareHandler, \"name\", {\n value: name,\n });\n\n return middlewareHandler;\n};\n\n/**\n * Creates the Express RPC middleware that routes incoming requests to registered server functions.\n * Reads the request body, dispatches to the matching function via serverFunctionsMap,\n * and sends the JSON-serialized result. Handles client disconnection via abort signals.\n * @param initialOptions - Options including rpcPrefix for URL routing\n * @returns An Express middleware function\n */\nexport const createRPCMiddleware: ExpressMiddlewareFn = (\n initialOptions = {},\n) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n { rpcPrefix: defaultRPCOptions.rpcPrefix },\n initialOptions,\n ) as ExpressMiddlewareOptions;\n\n // Hoist prefix regex (escaped) and the literal prefix-for-replace out of the\n // per-request handler to avoid regex injection and per-request compilation.\n const rpcPrefix = options.rpcPrefix;\n const prefixRegex = rpcPrefix\n ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`)\n : /* istanbul ignore next */ null;\n const prefixReplace = `/${rpcPrefix}/`;\n\n return createMiddleware({\n ...options,\n handler: async (\n req: IncomingMessage | ExpressRequest,\n res: ServerResponse | ExpressResponse,\n _next: NextFunction | Connect.NextFunction,\n ) => {\n const { url: path, searchParams } = getRequestDetails(req);\n const { sendResponse } = getResponseDetails(res);\n\n // Validate the url starts with the prefix via the escaped regex\n // istanbul ignore next\n if (prefixRegex && !prefixRegex.test(path)) {\n // falls through to next handler (never reached in practice; the outer\n // createMiddleware already gates on this, but kept for defense-in-depth)\n return;\n }\n\n // Optional origin check: reject requests whose Origin header does not\n // match the configured origin. Requests without an Origin header\n // (curl, native clients) pass through unchecked.\n const origin = options.origin;\n const requestOrigin = req.headers.origin;\n if (origin && requestOrigin && requestOrigin !== origin) {\n sendResponse(403, { error: REQUEST_FORBIDDEN });\n return;\n }\n\n const functionName = path.replace(prefixReplace, \"\");\n const serverFunction = serverFunctionsMap.get(functionName);\n\n if (!serverFunction) {\n sendResponse(404, { error: FUNCTION_NOT_FOUND });\n return;\n }\n\n try {\n const method = serverFunction.options?.method || \"POST\";\n if (req.method?.toUpperCase() !== method) {\n sendResponse(405, { error: METHOD_NOT_ALLOWED });\n return;\n }\n\n let args: JsonValue[] = [];\n if (method === \"GET\") {\n const raw = searchParams.get(\"args\");\n // istanbul ignore else\n if (raw) args = JSON.parse(raw);\n } else {\n const body = await readBody(req);\n args = Array.isArray(body.data) ? body.data : [body.data];\n }\n const { data, cancel } = serverFunction.handler(...args);\n const onClose = () => cancel(CLIENT_DISCONNECTED);\n\n req.on(\"close\", onClose);\n const result = await data;\n req.off(\"close\", onClose);\n\n // istanbul ignore else\n if (!res.headersSent) sendResponse(200, { data: result });\n } catch (err) {\n console.error(String(err));\n sendResponse(500, { error: INTERNAL_SERVER_ERROR });\n }\n },\n });\n};\n","/** @module Main entrypoint for the RPC Vite plugin. Exports `rpcPlugin` (default), `defineConfig`, and `loadRPCConfig`. */\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 { createRPCMiddleware } from \"./express/createMiddleware.ts\";\nimport { defaultRPCOptions } from \"./options.ts\";\nimport type { RpcPluginOptions } from \"./types.d.ts\";\nimport {\n CONFIG_FILE_NOT_FOUND,\n FAILED_LOAD_CONFIG,\n NO_CONFIG_FOUND,\n} from \"./constants.ts\";\n\nimport {\n getClientModules,\n scanForServerFiles,\n serverFunctionsMap,\n} from \"@thednp/rpc/server\";\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\n/**\n * Type-safe helper to create an RPC configuration object.\n * Merges the provided partial config with built-in defaults.\n * @param uniConfig - System-wide RPC configuration overrides\n * @returns Complete RPC plugin options with defaults applied\n */\nconst defineConfig: (c: Partial<RpcPluginOptions>) => RpcPluginOptions = (\n uniConfig: Partial<RpcPluginOptions>,\n) => {\n return mergeConfig(defaultRPCOptions, uniConfig) as RpcPluginOptions;\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 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 return RPCConfig;\n }\n // istanbul ignore next - this is a necessary fallback here\n RPCConfig = defaultRPCOptions;\n }\n\n if (RPCConfig !== undefined) {\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 // return defaultRPCOptions as RpcPluginOptions;\n // RPCConfig = defaultRPCOptions;\n } catch (error) {\n RPCConfig = defaultRPCOptions;\n console.warn(FAILED_LOAD_CONFIG, error);\n // return defaultRPCOptions as RpcPluginOptions;\n }\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<unknown> {\n // Internal type - adapters are handled at runtime\n let options: RpcPluginOptions & { rpcPrefix: string };\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 // config() {\n // return {\n // // optimizeDeps: {\n // // noDiscovery: true,\n // // include: [\"@thednp/rpc\"],\n // // },\n // ssr: {\n // noExternal: [\"@thednp/rpc\"],\n // },\n // };\n // },\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 await scanForServerFiles(config, 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 await scanForServerFiles(config);\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 if (serverFunctionsMap.size === 0) {\n await scanForServerFiles(config);\n }\n\n const vite = await import(\"vite\");\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 { defineConfig, 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 = {\n contentType: \"application/json\",\n credentials: \"same-origin\",\n method: \"POST\",\n} satisfies ServerFunctionOptions;\n\nexport const defaultRPCOptions: RpcPluginOptions = {\n rpcPrefix: \"__rpc\",\n adapter: \"express\",\n serverFiles: \"exact\",\n scanRoot: undefined,\n};\n\nexport const defaultMiddlewareOptions = {\n rpcPrefix: undefined,\n path: undefined,\n origin: undefined,\n} satisfies MiddlewareOptions;\n","export const OPERATION_ABORTED = \"Operation aborted\";\n\nexport const REQUEST_CANCELLED = \"Request was cancelled\";\n\nexport const FETCH_ERROR_PREFIX = \"Fetch error: \";\n\nexport const NO_SERVER_FUNCTION_FOUND = \"No server function found.\";\n\nexport const ERROR_LOADING_FILE = \"Error loading file:\";\n\nexport const FUNCTION_NOT_FOUND = \"Function not found\";\n\nexport const METHOD_NOT_ALLOWED = \"Method Not Allowed\";\n\nexport const REQUEST_FORBIDDEN = \"Forbidden\";\n\nexport const 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\";\n\nexport const serverFunctionsMap: Map<string, ServerFnEntry> = new Map<\n string,\n ServerFnEntry\n>();\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, 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 { serverFunctionsMap } 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 let body = \"\";\n let headers = \"{}\";\n switch (options.contentType) {\n case \"text/plain\":\n {\n body = `args[0]`;\n headers = `{ 'Content-Type': 'text/plain' }`;\n }\n break;\n case \"multipart/form-data\":\n {\n // FormData carries its own multipart content type with a random boundary,\n // so the browser must generate the header (setting it manually strips the boundary)\n body = `args[0]`;\n headers = `{}`;\n }\n break;\n default: {\n body = `JSON.stringify(args)`;\n headers = `{ 'Content-Type': 'application/json' }`;\n }\n }\n // GET requests cannot carry a body: args travel as a JSON query parameter\n if (method === \"GET\") {\n body = `JSON.stringify(args)`;\n headers = `{}`;\n }\n\n const output = `\nexport const ${safeFnEntry} = (...args) => {\n const body = ${body};\n const headers = ${headers};\n const prefix = \"${safePrefix}\";\n const name = \"${safeFnName}\";\n const credentials = \"${credentials}\";\n const method = \"${method}\";\n return innerModule(body, headers, credentials, prefix, name, method);\n}`;\n\n return output.trim();\n};\n\n/**\n * Generates the complete client-side module bundle by iterating all registered server functions\n * and producing fetch-based stubs for each. The result is transformed by Vite (or Oxc) during\n * 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 const entries = Array.from(serverFunctionsMap.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 const output = `\n// Client-side RPC modules\nimport { innerModule } 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, and `walkGlobFiles` for recursively discovering `*.server.*` files. Never import this module in client code — it is server-only. */\nimport type { 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","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 { serverFunctionsMap } from \"./functionsMap.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 global `serverFunctionsMap` 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 const { createServer, normalizePath } = await import(\"vite\");\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 // (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 // `createServerFunction` auto-registers its name into the map at module\n // load, so the global map cannot be used for duplicate detection across\n // scans (dev re-scans the same modules via the same or fresh Vite graphs).\n // Track names seen in THIS scan run only: a name repeated within one scan\n // (e.g. two files exporting the same function name) is a genuine conflict.\n for (const [exportName, exportValue] of moduleEntries) {\n const registeredName = exportValue.name;\n if (seenNames.has(registeredName)) {\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(registeredName);\n serverFunctionsMap.set(registeredName, {\n name: registeredName,\n handler: exportValue,\n options: exportValue?.options,\n exportName,\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), `defineConfig`, and `loadRPCConfig`. */\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\";\nimport {\n scanForServerFiles,\n scannedServerFiles,\n} from \"./scanForServerFiles.ts\";\nimport { serverFunctionsMap } from \"./functionsMap.ts\";\n\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\n/**\n * Type-safe helper to create an RPC configuration object.\n * Merges the provided partial config with built-in defaults.\n * @param uniConfig - System-wide RPC configuration overrides\n * @returns Complete RPC plugin options with defaults applied\n */\nconst defineConfig: (c: Partial<RpcPluginOptions>) => RpcPluginOptions = (\n uniConfig: Partial<RpcPluginOptions>,\n) => {\n return mergeConfig(defaultRPCOptions, uniConfig) as RpcPluginOptions;\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 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 return RPCConfig;\n }\n // istanbul ignore next - this is a necessary fallback here\n RPCConfig = defaultRPCOptions;\n }\n\n if (RPCConfig !== undefined) {\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 // return defaultRPCOptions as RpcPluginOptions;\n // RPCConfig = defaultRPCOptions;\n } catch (error) {\n RPCConfig = defaultRPCOptions;\n console.warn(FAILED_LOAD_CONFIG, error);\n // return defaultRPCOptions as RpcPluginOptions;\n }\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 // config() {\n // return {\n // // optimizeDeps: {\n // // noDiscovery: true,\n // // include: [\"@thednp/rpc\"],\n // // },\n // ssr: {\n // noExternal: [\"@thednp/rpc\"],\n // },\n // };\n // },\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 };\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 };\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 };\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 { defineConfig, loadRPCConfig };\nexport type * from \"./types.d.ts\";\nexport {};\n"],"mappings":"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"}
@@ -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;;;;;;;;;;cCXQ,kBAAkB;;;;;;;;cAoElB,qBAAqB;;;;;;;;iBCnFZ,UAAU,KAAK,MAAG;;;;;;;;iBAexB,WAAW,KAAK,KAAK,MAAM;;;;;;;;cAuC9B,WAAQ,KACd,eACJ,QAAQ"}
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;;;;;;;;;;cCXQ,kBAAkB;;;;;;;;cAoElB,qBAAqB;;;;;;;;iBCnFZ,UAAU,KAAK,MAAG;;;;;;;;iBAkBxB,WAAW,KAAK,KAAK,MAAM;;;;;;;;cAuC9B,WAAQ,KACd,eACJ,QAAQ"}
package/dist/koa/koa.mjs CHANGED
@@ -20,10 +20,51 @@ const CLIENT_DISCONNECTED = "client disconnected";
20
20
  /** Returns a warning when a middleware name is reused, preventing registration conflicts. @param name - The duplicate middleware name */
21
21
  const MIDDLEWARE_NAME_USED = (name) => `The middleware name "${name}" is already used.`;
22
22
  //#endregion
23
+ //#region src/server-helpers.ts
24
+ /**
25
+ * A typed error thrown from server functions.
26
+ * The middleware serializes the `message` and `code` in the response,
27
+ * allowing clients to recognise and handle specific error conditions.
28
+ */
29
+ var RPCError = class extends Error {
30
+ /** Machine-readable error code (e.g. "VALIDATION_FAILED", "UNAUTHORIZED") */
31
+ code;
32
+ /** Optional diagnostic payload */
33
+ data;
34
+ constructor(message, code = "INTERNAL", data) {
35
+ super(message);
36
+ this.name = "RPCError";
37
+ this.code = code;
38
+ this.data = data;
39
+ }
40
+ };
41
+ /**
42
+ * Formats an error for the RPC middleware response.
43
+ * In development the full `RPCError` payload is included so developers
44
+ * can quickly identify issues. Unexpected exceptions never expose their
45
+ * message — only the generic "Internal Server Error" is sent, preventing
46
+ * information disclosure; server-side diagnostics are preserved via the
47
+ * middleware's `console.error` logging.
48
+ */
49
+ const formatError = (err, isProduction) => {
50
+ if (isProduction) return { error: INTERNAL_SERVER_ERROR };
51
+ if (err instanceof RPCError) {
52
+ const payload = {
53
+ error: err.message || "Internal Server Error",
54
+ code: err.code
55
+ };
56
+ if (err.data !== void 0) payload.data = err.data;
57
+ return payload;
58
+ }
59
+ return { error: INTERNAL_SERVER_ERROR };
60
+ };
61
+ //#endregion
23
62
  //#region src/options.ts
24
63
  const defaultRPCOptions = {
25
64
  rpcPrefix: "__rpc",
26
- adapter: "express"
65
+ adapter: "express",
66
+ serverFiles: "exact",
67
+ scanRoot: void 0
27
68
  };
28
69
  const defaultMiddlewareOptions = {
29
70
  rpcPrefix: void 0,
@@ -79,11 +120,12 @@ const readBody = (ctx) => {
79
120
  const contentType = ctx.request.headers["content-type"]?.toLowerCase() || "";
80
121
  return new Promise((resolve, reject) => {
81
122
  const isJSON = contentType.includes("json");
123
+ const isMultipart = contentType.includes("multipart/form-data");
82
124
  const reqBody = ctx.request.body;
83
125
  if (reqBody !== void 0) {
84
126
  resolve({
85
- contentType: isJSON ? "application/json" : "text/plain",
86
- data: isJSON ? reqBody : String(reqBody)
127
+ contentType: isMultipart ? "multipart/form-data" : isJSON ? "application/json" : "text/plain",
128
+ data: isMultipart ? reqBody : isJSON ? reqBody : String(reqBody)
87
129
  });
88
130
  return;
89
131
  }
@@ -100,11 +142,12 @@ const readBody = (ctx) => {
100
142
  const onEnd = () => {
101
143
  toggleListeners();
102
144
  const isJSON = contentType.includes("json");
145
+ const isMultipart = contentType.includes("multipart/form-data");
103
146
  try {
104
- const data = JSON.parse(body);
147
+ const data = isMultipart ? { raw: body } : JSON.parse(body);
105
148
  resolve({
106
- contentType: isJSON ? "application/json" : "text/plain",
107
- data
149
+ contentType: isMultipart ? "multipart/form-data" : isJSON ? "application/json" : "text/plain",
150
+ data: isMultipart ? data : data
108
151
  });
109
152
  } catch (_er) {
110
153
  resolve({
@@ -213,8 +256,9 @@ const createRPCMiddleware = (initialOptions = {}) => {
213
256
  ctx.body = { data: result };
214
257
  } catch (err) {
215
258
  console.error(String(err));
259
+ const isProduction = process.env.NODE_ENV === "production";
216
260
  ctx.status = 500;
217
- ctx.body = { error: INTERNAL_SERVER_ERROR };
261
+ ctx.body = formatError(err, isProduction);
218
262
  }
219
263
  }
220
264
  });
@@ -1 +1 @@
1
- {"version":3,"file":"koa.mjs","names":[],"sources":["../../src/tools.ts","../../src/constants.ts","../../src/options.ts","../../src/koa/helpers.ts","../../src/koa/createMiddleware.ts"],"sourcesContent":["/**\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","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 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","import type {\n MiddlewareOptions,\n RpcPluginOptions,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\n\nexport const defaultServerFnOptions = {\n contentType: \"application/json\",\n credentials: \"same-origin\",\n method: \"POST\",\n} satisfies ServerFunctionOptions;\n\nexport const defaultRPCOptions: RpcPluginOptions = {\n rpcPrefix: \"__rpc\",\n adapter: \"express\",\n};\n\nexport const defaultMiddlewareOptions = {\n rpcPrefix: undefined,\n path: undefined,\n origin: undefined,\n} satisfies MiddlewareOptions;\n","// src/koa/helpers.ts\nimport type { Buffer } from \"node:buffer\";\nimport type { ViteDevServer } from \"vite\";\nimport { createRPCMiddleware } from \"./createMiddleware.ts\";\nimport type { BodyResult } from \"@thednp/rpc\";\nimport type { Koa } from \"./index.ts\";\nimport type { KoaContext } from \"./types.d.ts\";\n\n/**\n * Convenience function to load RPC config and attach the RPC middleware to a Koa app.\n * Dynamically imports loadRPCConfig and registers the middleware.\n * @param app - Koa application instance\n */\nexport async function attachRPC(app: Koa) {\n const { loadRPCConfig } = await import(\"@thednp/rpc\");\n\n const config = await loadRPCConfig();\n const { adapter: _adapter, ...options } = config;\n app.use(createRPCMiddleware(options));\n}\n\n/**\n * Attaches Vite's dev server middlewares to a Koa app for development mode.\n * Bridges Koa's context-based middleware to Vite's Connect-compatible middleware stack\n * by forwarding Koa body, wrapping res.end, and delegating back to Koa on 404 or unhandled routes.\n * @param app - Koa application instance\n * @param vite - Running Vite dev server\n */\nexport function attachVite(app: Koa, vite: ViteDevServer): void {\n app.use(async (ctx: KoaContext, next) => {\n const req = ctx.req;\n const res = ctx.res;\n\n // Forward Koa body to req.body for Express/RPC middleware compatibility\n const requestBody = ctx.request?.body;\n if (requestBody !== undefined) {\n Object.assign(req, { body: requestBody });\n }\n\n const originalEnd = res.end.bind(res);\n let viteHandled = false;\n // @ts-ignore - Koa res.end type mismatch with Node's\n res.end = function (...args: unknown[]) {\n viteHandled = true;\n return originalEnd(args[0]);\n };\n\n await new Promise<void>((resolve) => {\n vite.middlewares(req, res, () => resolve(undefined));\n });\n\n // @ts-ignore - Koa res.end type mismatch with Node's\n res.end = originalEnd;\n\n if (!viteHandled || res.statusCode === 404) {\n await next();\n }\n });\n}\n\n/**\n * Reads and parses the HTTP request body from a Koa context.\n * If koa-body or another body parser already consumed the stream,\n * uses the pre-parsed body from `ctx.request.body`.\n * @param ctx - Koa context\n * @returns A promise resolving to the parsed body with its content type\n */\nexport const readBody = (\n ctx: KoaContext,\n): Promise<BodyResult> => {\n const contentType = ctx.request.headers[\"content-type\"]?.toLowerCase() || \"\";\n\n return new Promise((resolve, reject) => {\n // If an koa-body already consumed the stream\n // via app.use(koaBody()), use ctx.request.body directly\n const isJSON = contentType.includes(\"json\");\n const reqBody = ctx.request.body;\n if (reqBody !== undefined) {\n resolve({\n contentType: isJSON ? \"application/json\" : \"text/plain\",\n data: isJSON ? reqBody : String(reqBody),\n } as BodyResult);\n return;\n }\n\n // OR read the body normally\n let body = \"\";\n\n const toggleListeners = (add?: boolean) => {\n const method = add ? \"on\" : \"off\";\n ctx.req[method](\"data\", onData);\n ctx.req[method](\"end\", onEnd);\n ctx.req[method](\"error\", onError);\n };\n\n const onData = (chunk: Buffer) => {\n // chunks.push(chunk);\n body += chunk.toString();\n };\n\n const onEnd = () => {\n toggleListeners();\n const isJSON = contentType.includes(\"json\");\n try {\n const data = JSON.parse(body);\n resolve({\n contentType: isJSON ? \"application/json\" : \"text/plain\",\n data,\n });\n } catch (_er) {\n resolve({ contentType: \"text/plain\", data: String(body) });\n }\n };\n\n const onError = (err: Error) => {\n toggleListeners();\n reject(err);\n };\n\n toggleListeners(true);\n });\n};\n","// src/koa/createMiddleware.ts\nimport type { Context, Next } from \"koa\";\nimport type { KoaMiddlewareFn, KoaMiddlewareOptions } from \"./types.d.ts\";\nimport type { JsonValue } from \"../types.d.ts\";\nimport { escapeRegExp } from \"../tools.ts\";\nimport {\n CLIENT_DISCONNECTED,\n FUNCTION_NOT_FOUND,\n INTERNAL_SERVER_ERROR,\n METHOD_NOT_ALLOWED,\n MIDDLEWARE_NAME_USED,\n REQUEST_FORBIDDEN,\n} from \"../constants.ts\";\n\nimport { scanForServerFiles, serverFunctionsMap } from \"@thednp/rpc/server\";\nimport { defaultMiddlewareOptions, defaultRPCOptions } from \"../options.ts\";\nimport { readBody } from \"./helpers.ts\";\n\nlet middlewareCount = 0;\nconst middlewareStack = new Set<string>();\n\n/**\n * Creates a Koa middleware with optional path and rpcPrefix filtering.\n * Middleware names are deduplicated. Prefix and path regexes are compiled once at creation time.\n * Koa URL is normalized via `new URL()` to strip query strings before matching.\n * @param initialOptions - Options for rpcPrefix, path matching, and the handler function\n * @returns A Koa middleware function\n */\nexport const createMiddleware: KoaMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n initialOptions,\n ) as KoaMiddlewareOptions;\n\n const middlewareName = options.name;\n const rpcPrefix = options.rpcPrefix;\n const path = options.path;\n const handler = options.handler;\n\n let name = middlewareName;\n if (!name) {\n name = \"viteRPCMiddleware-\" + middlewareCount;\n middlewareCount += 1;\n }\n if (middlewareStack.has(name)) {\n throw new Error(MIDDLEWARE_NAME_USED(name));\n }\n middlewareStack.add(name);\n\n // Hoist regex compilation out of per-request path. Escape the prefix to\n // prevent regex injection via metacharacters in the config string.\n const prefixRegex: RegExp | null = rpcPrefix\n ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`)\n : null;\n const pathMatcher: RegExp | null = path\n ? (typeof path === \"string\" ? new RegExp(path) : path)\n : null;\n\n const middlewareHandler = async (ctx: Context, next: Next) => {\n const url = new URL(ctx.url, \"http://localhost\").pathname;\n\n if (serverFunctionsMap.size === 0) {\n await scanForServerFiles();\n }\n\n // No need to continue when no handler provided\n if (!handler) {\n return next();\n }\n\n if (pathMatcher && !pathMatcher.test(url)) {\n return next();\n }\n\n if (prefixRegex && !prefixRegex.test(url)) {\n return next();\n }\n\n await handler(ctx, next);\n };\n\n Object.defineProperty(middlewareHandler, \"name\", {\n value: name,\n });\n\n return middlewareHandler;\n};\n\n/**\n * Creates the Koa RPC middleware that routes incoming requests to registered server functions.\n * Wraps the generic createMiddleware with the RPC handler that reads the body, dispatches\n * to the matching function, and sets the JSON-serialized result on ctx.body.\n * @param initialOptions - Options including rpcPrefix for URL routing\n * @returns A Koa middleware function\n */\nexport const createRPCMiddleware: KoaMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n { rpcPrefix: defaultRPCOptions.rpcPrefix },\n initialOptions,\n ) as KoaMiddlewareOptions;\n\n // Hoist prefix regex (escaped) and the literal prefix-for-replace out of the\n // per-request handler to avoid regex injection and per-request compilation.\n const rpcPrefix = options.rpcPrefix;\n const prefixRegex = rpcPrefix\n ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`)\n : /* istanbul ignore next */ null;\n const prefixReplace = `/${rpcPrefix}/`;\n\n return createMiddleware({\n ...options,\n handler: async (ctx: Context, _next: Next) => {\n const reqUrl = new URL(ctx.url, \"http://localhost\");\n const url = reqUrl.pathname;\n // const { rpcPrefix } = options;\n\n // Defense-in-depth: validate prefix match via escaped regex even though\n // the outer createMiddleware gates on the same prefix already.\n // istanbul ignore next\n if (prefixRegex && !prefixRegex.test(url)) {\n return;\n }\n\n // Optional origin check: reject requests whose Origin header does not\n // match the configured origin. Requests without an Origin header\n // (curl, native clients) pass through unchecked.\n const origin = options.origin;\n const requestOrigin = ctx.headers.origin;\n if (origin && requestOrigin && requestOrigin !== origin) {\n ctx.status = 403;\n ctx.body = { error: REQUEST_FORBIDDEN };\n return;\n }\n\n const functionName = url.replace(prefixReplace, \"\");\n const serverFunction = serverFunctionsMap.get(functionName);\n\n if (!serverFunction) {\n ctx.status = 404;\n ctx.body = { error: FUNCTION_NOT_FOUND };\n return;\n }\n\n try {\n const method = serverFunction.options?.method || \"POST\";\n if (ctx.method.toUpperCase() !== method) {\n ctx.status = 405;\n ctx.body = { error: METHOD_NOT_ALLOWED };\n return;\n }\n\n let args: JsonValue[] = [];\n if (method === \"GET\") {\n const raw = reqUrl.searchParams.get(\"args\");\n if (raw) args = JSON.parse(raw);\n } else {\n const body = await readBody(ctx);\n args = Array.isArray(body.data) ? body.data : [body.data];\n }\n const { data: resultData, cancel } = serverFunction.handler(...args);\n const onClose = () => cancel(CLIENT_DISCONNECTED);\n ctx.req.on(\"close\", onClose);\n const result = await resultData;\n ctx.req.off(\"close\", onClose);\n ctx.status = 200;\n ctx.body = { data: result };\n } catch (err) {\n console.error(String(err));\n ctx.status = 500;\n ctx.body = { error: INTERNAL_SERVER_ERROR };\n }\n },\n });\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"koa.mjs","names":[],"sources":["../../src/tools.ts","../../src/constants.ts","../../src/server-helpers.ts","../../src/options.ts","../../src/koa/helpers.ts","../../src/koa/createMiddleware.ts"],"sourcesContent":["/**\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","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 INTERNAL_SERVER_ERROR = \"Internal Server Error\";\n\nexport const CLIENT_DISCONNECTED = \"client disconnected\";\n\n/** Returns a warning when a middleware name is reused, preventing registration conflicts. @param name - The duplicate middleware name */\nexport const MIDDLEWARE_NAME_USED = (name: string) =>\n `The middleware name \"${name}\" is already used.`;\n\n/** Error message when a value fails the safe-identifier validation. @param label - What kind of value was being validated. @param name - The rejected value */\nexport const INVALID_IDENTIFIER = (label: string, name: string) =>\n `Invalid ${label}: \"${name}\" must match /^[A-Za-z_$][A-Za-z0-9_$]*$/`;\n\n/** Error message when a value fails the safe-path-segment validation. @param label - What kind of value was being validated. @param segment - The rejected value */\nexport const INVALID_PATH_SEGMENT = (label: string, segment: string) =>\n `Invalid ${label}: \"${segment}\" must match /^[A-Za-z0-9_$][A-Za-z0-9_$/-]*$/`;\n\n/** Warning message when a specified RPC config file cannot be resolved on disk. @param configFile - The requested config filename. @param configFilePath - The resolved absolute path */\nexport const CONFIG_FILE_NOT_FOUND = (\n configFile: string,\n configFilePath: string,\n) =>\n ` ⚠︎ The specified RPC config file ${configFile} cannot be found at ${configFilePath}, loading the defaults..`;\n\nexport const NO_CONFIG_FOUND = ` ⚡︎ No RPC config found, loading the defaults..`;\n\nexport const FAILED_LOAD_CONFIG = ` ⚠︎ Failed to load RPC config:`;\n\n/** Error template for duplicate server function names across files. @param name - The duplicate registered name */\nexport const DUPLICATE_FUNCTION_NAME = (name: string) =>\n `Duplicate server function \"${name}\" detected. Each server function must have a unique name. Remove or rename the duplicate.`;\n","/** @module Server-side utilities. Exports the `RPCError` class for typed server-side errors, `formatError` for middleware error responses, and `walkGlobFiles` for recursively discovering `*.server.*` files. Never import this module in client code — it is server-only. */\nimport type { 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","import type {\n MiddlewareOptions,\n RpcPluginOptions,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\n\nexport const defaultServerFnOptions = {\n contentType: \"application/json\",\n credentials: \"same-origin\",\n method: \"POST\",\n} satisfies ServerFunctionOptions;\n\nexport const defaultRPCOptions: RpcPluginOptions = {\n rpcPrefix: \"__rpc\",\n adapter: \"express\",\n serverFiles: \"exact\",\n scanRoot: undefined,\n};\n\nexport const defaultMiddlewareOptions = {\n rpcPrefix: undefined,\n path: undefined,\n origin: undefined,\n} satisfies MiddlewareOptions;\n","// src/koa/helpers.ts\nimport type { Buffer } from \"node:buffer\";\nimport type { ViteDevServer } from \"vite\";\nimport { createRPCMiddleware } from \"./createMiddleware.ts\";\nimport type { BodyResult } from \"@thednp/rpc\";\nimport type { Koa } from \"./index.ts\";\nimport type { KoaContext } from \"./types.d.ts\";\n\n/**\n * Convenience function to load RPC config and attach the RPC middleware to a Koa app.\n * Dynamically imports loadRPCConfig and registers the middleware.\n * @param app - Koa application instance\n */\nexport async function attachRPC(app: Koa) {\n // The main plugin entry statically imports Vite, so loadRPCConfig is\n // imported lazily: function bundles that never call attachRPC (e.g.\n // serverless functions) keep Vite out of the bundle (or externalized).\n const { loadRPCConfig } = await import(\"@thednp/rpc\");\n\n const config = await loadRPCConfig();\n const { adapter: _adapter, ...options } = config;\n app.use(createRPCMiddleware(options));\n}\n\n/**\n * Attaches Vite's dev server middlewares to a Koa app for development mode.\n * Bridges Koa's context-based middleware to Vite's Connect-compatible middleware stack\n * by forwarding Koa body, wrapping res.end, and delegating back to Koa on 404 or unhandled routes.\n * @param app - Koa application instance\n * @param vite - Running Vite dev server\n */\nexport function attachVite(app: Koa, vite: ViteDevServer): void {\n app.use(async (ctx: KoaContext, next) => {\n const req = ctx.req;\n const res = ctx.res;\n\n // Forward Koa body to req.body for Express/RPC middleware compatibility\n const requestBody = ctx.request?.body;\n if (requestBody !== undefined) {\n Object.assign(req, { body: requestBody });\n }\n\n const originalEnd = res.end.bind(res);\n let viteHandled = false;\n // @ts-ignore - Koa res.end type mismatch with Node's\n res.end = function (...args: unknown[]) {\n viteHandled = true;\n return originalEnd(args[0]);\n };\n\n await new Promise<void>((resolve) => {\n vite.middlewares(req, res, () => resolve(undefined));\n });\n\n // @ts-ignore - Koa res.end type mismatch with Node's\n res.end = originalEnd;\n\n if (!viteHandled || res.statusCode === 404) {\n await next();\n }\n });\n}\n\n/**\n * Reads and parses the HTTP request body from a Koa context.\n * If koa-body or another body parser already consumed the stream,\n * uses the pre-parsed body from `ctx.request.body`.\n * @param ctx - Koa context\n * @returns A promise resolving to the parsed body with its content type\n */\nexport const readBody = (\n ctx: KoaContext,\n): Promise<BodyResult> => {\n const contentType = ctx.request.headers[\"content-type\"]?.toLowerCase() || \"\";\n\n return new Promise((resolve, reject) => {\n // If an koa-body already consumed the stream\n // via app.use(koaBody()), use ctx.request.body directly\n const isJSON = contentType.includes(\"json\");\n const isMultipart = contentType.includes(\"multipart/form-data\");\n const reqBody = ctx.request.body;\n if (reqBody !== undefined) {\n resolve({\n contentType: isMultipart\n ? \"multipart/form-data\"\n : isJSON\n ? \"application/json\"\n : \"text/plain\",\n data: isMultipart\n ? (reqBody as Record<string, unknown>)\n : isJSON\n ? reqBody\n : String(reqBody),\n } as BodyResult);\n return;\n }\n\n // OR read the body normally\n let body = \"\";\n\n const toggleListeners = (add?: boolean) => {\n const method = add ? \"on\" : \"off\";\n ctx.req[method](\"data\", onData);\n ctx.req[method](\"end\", onEnd);\n ctx.req[method](\"error\", onError);\n };\n\n const onData = (chunk: Buffer) => {\n // chunks.push(chunk);\n body += chunk.toString();\n };\n\n const onEnd = () => {\n toggleListeners();\n const isJSON = contentType.includes(\"json\");\n const isMultipart = contentType.includes(\"multipart/form-data\");\n try {\n const data = isMultipart ? { raw: body } : JSON.parse(body);\n resolve({\n contentType: isMultipart\n ? \"multipart/form-data\"\n : isJSON\n ? \"application/json\"\n : \"text/plain\",\n data: isMultipart ? (data as Record<string, unknown>) : data,\n } as BodyResult);\n } catch (_er) {\n resolve({ contentType: \"text/plain\", data: String(body) });\n }\n };\n\n const onError = (err: Error) => {\n toggleListeners();\n reject(err);\n };\n\n toggleListeners(true);\n });\n};\n","// src/koa/createMiddleware.ts\nimport type { Context, Next } from \"koa\";\nimport type { KoaMiddlewareFn, KoaMiddlewareOptions } from \"./types.d.ts\";\nimport type { JsonValue } from \"../types.d.ts\";\nimport { escapeRegExp } from \"../tools.ts\";\nimport { formatError } from \"../server-helpers.ts\";\nimport {\n CLIENT_DISCONNECTED,\n FUNCTION_NOT_FOUND,\n METHOD_NOT_ALLOWED,\n MIDDLEWARE_NAME_USED,\n REQUEST_FORBIDDEN,\n} from \"../constants.ts\";\n\nimport { scanForServerFiles, serverFunctionsMap } from \"@thednp/rpc/server\";\nimport { defaultMiddlewareOptions, defaultRPCOptions } from \"../options.ts\";\nimport { readBody } from \"./helpers.ts\";\n\nlet middlewareCount = 0;\nconst middlewareStack = new Set<string>();\n\n/**\n * Creates a Koa middleware with optional path and rpcPrefix filtering.\n * Middleware names are deduplicated. Prefix and path regexes are compiled once at creation time.\n * Koa URL is normalized via `new URL()` to strip query strings before matching.\n * @param initialOptions - Options for rpcPrefix, path matching, and the handler function\n * @returns A Koa middleware function\n */\nexport const createMiddleware: KoaMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n initialOptions,\n ) as KoaMiddlewareOptions;\n\n const middlewareName = options.name;\n const rpcPrefix = options.rpcPrefix;\n const path = options.path;\n const handler = options.handler;\n\n let name = middlewareName;\n if (!name) {\n name = \"viteRPCMiddleware-\" + middlewareCount;\n middlewareCount += 1;\n }\n if (middlewareStack.has(name)) {\n throw new Error(MIDDLEWARE_NAME_USED(name));\n }\n middlewareStack.add(name);\n\n // Hoist regex compilation out of per-request path. Escape the prefix to\n // prevent regex injection via metacharacters in the config string.\n const prefixRegex: RegExp | null = rpcPrefix\n ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`)\n : null;\n const pathMatcher: RegExp | null = path\n ? (typeof path === \"string\" ? new RegExp(path) : path)\n : null;\n\n const middlewareHandler = async (ctx: Context, next: Next) => {\n const url = new URL(ctx.url, \"http://localhost\").pathname;\n\n if (serverFunctionsMap.size === 0) {\n await scanForServerFiles();\n }\n\n // No need to continue when no handler provided\n if (!handler) {\n return next();\n }\n\n if (pathMatcher && !pathMatcher.test(url)) {\n return next();\n }\n\n if (prefixRegex && !prefixRegex.test(url)) {\n return next();\n }\n\n await handler(ctx, next);\n };\n\n Object.defineProperty(middlewareHandler, \"name\", {\n value: name,\n });\n\n return middlewareHandler;\n};\n\n/**\n * Creates the Koa RPC middleware that routes incoming requests to registered server functions.\n * Wraps the generic createMiddleware with the RPC handler that reads the body, dispatches\n * to the matching function, and sets the JSON-serialized result on ctx.body.\n * @param initialOptions - Options including rpcPrefix for URL routing\n * @returns A Koa middleware function\n */\nexport const createRPCMiddleware: KoaMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n { rpcPrefix: defaultRPCOptions.rpcPrefix },\n initialOptions,\n ) as KoaMiddlewareOptions;\n\n // Hoist prefix regex (escaped) and the literal prefix-for-replace out of the\n // per-request handler to avoid regex injection and per-request compilation.\n const rpcPrefix = options.rpcPrefix;\n const prefixRegex = rpcPrefix\n ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`)\n : /* istanbul ignore next */ null;\n const prefixReplace = `/${rpcPrefix}/`;\n\n return createMiddleware({\n ...options,\n handler: async (ctx: Context, _next: Next) => {\n const reqUrl = new URL(ctx.url, \"http://localhost\");\n const url = reqUrl.pathname;\n // const { rpcPrefix } = options;\n\n // Defense-in-depth: validate prefix match via escaped regex even though\n // the outer createMiddleware gates on the same prefix already.\n // istanbul ignore next\n if (prefixRegex && !prefixRegex.test(url)) {\n return;\n }\n\n // Optional origin check: reject requests whose Origin header does not\n // match the configured origin. Requests without an Origin header\n // (curl, native clients) pass through unchecked.\n const origin = options.origin;\n const requestOrigin = ctx.headers.origin;\n if (origin && requestOrigin && requestOrigin !== origin) {\n ctx.status = 403;\n ctx.body = { error: REQUEST_FORBIDDEN };\n return;\n }\n\n const functionName = url.replace(prefixReplace, \"\");\n const serverFunction = serverFunctionsMap.get(functionName);\n\n if (!serverFunction) {\n ctx.status = 404;\n ctx.body = { error: FUNCTION_NOT_FOUND };\n return;\n }\n\n try {\n const method = serverFunction.options?.method || \"POST\";\n if (ctx.method.toUpperCase() !== method) {\n ctx.status = 405;\n ctx.body = { error: METHOD_NOT_ALLOWED };\n return;\n }\n\n let args: JsonValue[] = [];\n if (method === \"GET\") {\n const raw = reqUrl.searchParams.get(\"args\");\n if (raw) args = JSON.parse(raw);\n } else {\n const body = await readBody(ctx);\n args = Array.isArray(body.data)\n ? body.data as JsonValue[]\n : [body.data as JsonValue];\n }\n const { data: resultData, cancel } = serverFunction.handler(...args);\n const onClose = () => cancel(CLIENT_DISCONNECTED);\n ctx.req.on(\"close\", onClose);\n const result = await resultData;\n ctx.req.off(\"close\", onClose);\n ctx.status = 200;\n ctx.body = { data: result };\n } catch (err) {\n console.error(String(err));\n const isProduction = process.env.NODE_ENV === \"production\";\n ctx.status = 500;\n ctx.body = formatError(err, isProduction);\n }\n },\n });\n};\n"],"mappings":"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"}
@@ -11,7 +11,7 @@ import "koa";
11
11
  /**
12
12
  * Content types the RPC client modules send with each request.
13
13
  */
14
- type ContentType = "application/json" | "text/plain";
14
+ type ContentType = "application/json" | "text/plain" | "multipart/form-data";
15
15
  /**
16
16
  * Fetch `credentials` policy used by the generated client modules.
17
17
  */
@@ -53,7 +53,7 @@ type JsonObject = {
53
53
  /**
54
54
  * A JSON array of JSON values.
55
55
  */
56
- type JsonArray = JsonValue[];
56
+ type JsonArray = (FormData | JsonValue)[];
57
57
  /**
58
58
  * Any JSON-serializable value: primitive, array, or object.
59
59
  */
@@ -62,7 +62,7 @@ type JsonValue = JsonPrimitive | JsonArray | JsonObject;
62
62
  * Server function initialization signature, identical to `ServerFunction`.
63
63
  * Used when registering a function with `createServerFunction`.
64
64
  */
65
- type ServerFunctionInit<TArgs extends JsonArray = JsonArray, TResult extends JsonValue = JsonValue> = (signal: AbortSignal, ...args: TArgs) => Promise<TResult>;
65
+ type ServerFunctionInit<TArgs extends FormData | JsonArray = JsonArray, TResult extends JsonValue = JsonValue> = (signal: AbortSignal, ...args: TArgs) => Promise<TResult>;
66
66
  /**
67
67
  * Client-side stub signature generated for each server function.
68
68
  * Returns a promise-backed `data` handle plus a `cancel` function
@@ -96,9 +96,11 @@ interface RpcPluginOptionsInternal {
96
96
  /**
97
97
  * Partial Vite config used when scanning server files outside a running dev server.
98
98
  */
99
- type ScanConfig = Pick<ResolvedConfig, "root" | "base"> & {
100
- /** Vite server options override (e.g. `middlewareMode`) */
99
+ type ScanConfig = Pick<ResolvedConfig, "base"> & {
100
+ root?: string;
101
101
  server?: Partial<ResolvedConfig["server"]>;
102
+ serverFiles?: "exact" | "glob";
103
+ scanRoot?: string;
102
104
  };
103
105
  /**
104
106
  * Entry in the server functions map: registered name, client handler,
@@ -139,17 +141,37 @@ interface RpcPluginOptions {
139
141
  * @default express
140
142
  */
141
143
  adapter: "express" | "hono" | "fastify" | "koa";
144
+ /**
145
+ * Root directory from which the plugin scans for server files.
146
+ * Defaults to `<root>/src/api`. Use this in monorepos where server files
147
+ * live in a shared package outside the current project root.
148
+ * @default undefined (resolves to src/api relative to the Vite root)
149
+ */
150
+ scanRoot?: string;
151
+ /**
152
+ * Server file matching mode. Use `"exact"` (default) for the classic
153
+ * `server.ts|js|mjs|mts` names, or `"glob"` to match `**\/*.server.{ts,js,mjs,mts}`
154
+ * inside the scan root.
155
+ * @default "exact"
156
+ */
157
+ serverFiles?: "exact" | "glob";
142
158
  }
143
159
  //#endregion
144
160
  //#region src/functionsMap.d.ts
145
161
  declare const serverFunctionsMap: Map<string, ServerFnEntry>;
146
162
  //#endregion
147
163
  //#region src/scanForServerFiles.d.ts
164
+ /** Absolute ids (normalized) of the scanned server function files. */
165
+ declare const scannedServerFiles: Set<string>;
148
166
  /**
149
- * Scans `src/api/` for server function files (`server.ts`, `server.js`, `server.mjs`, `server.mts`)
167
+ * Scans `src/api/` (or an explicit `scanRoot`) for server function files
150
168
  * and populates the global `serverFunctionsMap` with their exported functions.
151
169
  * Uses Vite's SSR module loading to resolve and execute each file.
152
- * @param initialCfg - Optional Vite config overrides (root, base, server)
170
+ *
171
+ * Supports two matching modes via `config.serverFiles`:
172
+ * `"exact"` — classic `server.ts|js|mjs|mts` names in the api directory
173
+ * `"glob"` — recursively walking `scanRoot` to match `*.server.{ts,js,mjs,mts}`
174
+ * @param initialCfg - Optional Vite config overrides (root, base, server, serverFiles, scanRoot)
153
175
  * @param devServer - Optional running Vite dev server instance; when provided, skips creating a new one
154
176
  */
155
177
  declare const scanForServerFiles: (initialCfg?: ScanConfig, devServer?: ViteDevServer) => Promise<void>;
@@ -176,6 +198,34 @@ declare function createServerFunction<TArgs extends JsonArray = JsonArray, TResu
176
198
  */
177
199
  declare const getClientModules: (initialOptions: RpcPluginOptionsInternal) => string;
178
200
  //#endregion
201
+ //#region src/server-helpers.d.ts
202
+ /**
203
+ * Recursively walks `dir` and collects absolute paths to files whose
204
+ * basename matches the `*.server.{ts,js,mjs,mts}` glob pattern.
205
+ */
206
+ declare const walkGlobFiles: (dir: string) => Promise<string[]>;
207
+ /**
208
+ * A typed error thrown from server functions.
209
+ * The middleware serializes the `message` and `code` in the response,
210
+ * allowing clients to recognise and handle specific error conditions.
211
+ */
212
+ declare class RPCError extends Error {
213
+ /** Machine-readable error code (e.g. "VALIDATION_FAILED", "UNAUTHORIZED") */
214
+ code: string;
215
+ /** Optional diagnostic payload */
216
+ data?: JsonValue;
217
+ constructor(message: string, code?: string, data?: JsonValue);
218
+ }
219
+ /**
220
+ * Formats an error for the RPC middleware response.
221
+ * In development the full `RPCError` payload is included so developers
222
+ * can quickly identify issues. Unexpected exceptions never expose their
223
+ * message — only the generic "Internal Server Error" is sent, preventing
224
+ * information disclosure; server-side diagnostics are preserved via the
225
+ * middleware's `console.error` logging.
226
+ */
227
+ declare const formatError: (err: unknown, isProduction: boolean) => JsonObject;
228
+ //#endregion
179
229
  //#region src/options.d.ts
180
230
  declare const defaultServerFnOptions: {
181
231
  contentType: "application/json";
@@ -189,5 +239,5 @@ declare const defaultMiddlewareOptions: {
189
239
  origin: undefined;
190
240
  };
191
241
  //#endregion
192
- export { createServerFunction, defaultMiddlewareOptions, defaultRPCOptions, defaultServerFnOptions, getClientModules, scanForServerFiles, serverFunctionsMap };
242
+ export { RPCError, createServerFunction, defaultMiddlewareOptions, defaultRPCOptions, defaultServerFnOptions, formatError, getClientModules, scanForServerFiles, scannedServerFiles, serverFunctionsMap, walkGlobFiles };
193
243
  //# sourceMappingURL=server.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.mts","names":[],"sources":["../../src/types.d.ts","../../src/functionsMap.ts","../../src/scanForServerFiles.ts","../../src/createFunction.ts","../../src/getClientModules.ts","../../src/options.ts"],"mappings":";;;;;;;;;;;;;KA2DY;;;;KAKA;;;;;UAaK;;;;;EAKf,aAAa;;;;;EAKb,cAAc;;;;;;;EAOd;;;;;;KAOU;;;;KAIA;GAAgB,cAAc,YAAY;;;;;KAI1C,YAAY;;;;KAIZ,YAAY,gBAAgB,YAAY;;;;;KAsCxC,mBACV,cAAc,YAAY,WAC1B,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;;;;;KAMU,aAAa,KAAK;;EAE5B,SAAS,QAAQ;;;;;;UAOF;;EAEf;;EAEA,SAAS;;EAET,UAAU;;EAEV;;;;;;;;UASe;;;;;;;;;;;EAWf;;;;;;;EAQA;;;;cC9OW,oBAAoB,YAAY;;;;;;;;;;cCgBhC,qBAAkB,aAChB,YAAU,YACX,kBACX;;;;;;;;;;;;iBCAa,qBACd,cAAc,YAAY,WAC1B,gBAAgB,YAAY,WAE5B,cACA,SAAS,mBAAmB,OAAO,UACnC,YAAW,QAAQ,yBAClB,eAAe,OAAO;;;;;;;;;;cCkDZ,mBAAgB,gBACX;;;cCzEL;;;;;cAMA,mBAAmB;cAKnB"}
1
+ {"version":3,"file":"server.d.mts","names":[],"sources":["../../src/types.d.ts","../../src/functionsMap.ts","../../src/scanForServerFiles.ts","../../src/createFunction.ts","../../src/getClientModules.ts","../../src/server-helpers.ts","../../src/options.ts"],"mappings":";;;;;;;;;;;;;KA2DY;;;;KAQA;;;;;UAcK;;;;;EAKf,aAAa;;;;;EAKb,cAAc;;;;;;;EAOd;;;;;;KAOU;;;;KAIA;GAAgB,cAAc,YAAY;;;;;KAI1C,aAAa,WAAW;;;;KAIxB,YAAY,gBAAgB,YAAY;;;;;KAsCxC,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;;;;;KAMU,aAAa,KAAK;EAC5B;EACA,SAAS,QAAQ;EACjB;EACA;;;;;;UAOe;;EAEf;;EAEA,SAAS;;EAET,UAAU;;EAEV;;;;;;;;UASe;;;;;;;;;;;EAWf;;;;;;;EAQA;;;;;;;EAQA;;;;;;;EAQA;;;;cCpQW,oBAAoB,YAAY;;;;cCehC,oBAAoB;;;;;;;;;;;;cAepB,qBAAkB,aAChB,YAAU,YACX,kBACX;;;;;;;;;;;;iBCda,qBACd,cAAc,YAAY,WAC1B,gBAAgB,YAAY,WAE5B,cACA,SAAS,mBAAmB,OAAO,UACnC,YAAW,QAAQ,yBAClB,eAAe,OAAO;;;;;;;;;;cC0DZ,mBAAgB,gBACX;;;;;;;cC1EL,gBAAa,gBAAwB;;;;;;cA4BrC,iBAAiB;;EAE5B;;EAEA,OAAO;EACP,YAAY,iBAAiB,eAAmB,OAAO;;;;;;;;;;cAgB5C,cAAW,cACV,0BAEX;;;cC3DU;;;;;cAMA,mBAAmB;cAOnB"}