@thednp/rpc 0.0.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.
Files changed (45) hide show
  1. package/AGENTS.md +142 -0
  2. package/LICENSE +21 -0
  3. package/README.md +178 -0
  4. package/dist/express/express.d.mts +39 -0
  5. package/dist/express/express.d.mts.map +1 -0
  6. package/dist/express/express.mjs +179 -0
  7. package/dist/express/express.mjs.map +1 -0
  8. package/dist/fastify/fastify.d.mts +45 -0
  9. package/dist/fastify/fastify.d.mts.map +1 -0
  10. package/dist/fastify/fastify.mjs +164 -0
  11. package/dist/fastify/fastify.mjs.map +1 -0
  12. package/dist/fastify/plugin/fastify/plugin.d.mts +114 -0
  13. package/dist/fastify/plugin/fastify/plugin.d.mts.map +1 -0
  14. package/dist/fastify/plugin/fastify/plugin.mjs +151 -0
  15. package/dist/fastify/plugin/fastify/plugin.mjs.map +1 -0
  16. package/dist/helpers/helpers.d.mts +9 -0
  17. package/dist/helpers/helpers.d.mts.map +1 -0
  18. package/dist/helpers/helpers.mjs +36 -0
  19. package/dist/helpers/helpers.mjs.map +1 -0
  20. package/dist/hono/hono.d.mts +49 -0
  21. package/dist/hono/hono.d.mts.map +1 -0
  22. package/dist/hono/hono.mjs +153 -0
  23. package/dist/hono/hono.mjs.map +1 -0
  24. package/dist/index.d.mts +192 -0
  25. package/dist/index.d.mts.map +1 -0
  26. package/dist/index.mjs +296 -0
  27. package/dist/index.mjs.map +1 -0
  28. package/dist/koa/koa.d.mts +26 -0
  29. package/dist/koa/koa.d.mts.map +1 -0
  30. package/dist/koa/koa.mjs +155 -0
  31. package/dist/koa/koa.mjs.map +1 -0
  32. package/dist/server/server.d.mts +92 -0
  33. package/dist/server/server.d.mts.map +1 -0
  34. package/dist/server/server.mjs +161 -0
  35. package/dist/server/server.mjs.map +1 -0
  36. package/package.json +78 -0
  37. package/wiki/adapters.md +143 -0
  38. package/wiki/best-practices.md +201 -0
  39. package/wiki/client-usage.md +62 -0
  40. package/wiki/configuration.md +77 -0
  41. package/wiki/getting-started.md +76 -0
  42. package/wiki/index.md +26 -0
  43. package/wiki/security.md +54 -0
  44. package/wiki/server-functions.md +93 -0
  45. package/wiki/setup.md +77 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/options.ts","../src/express/helpers.ts","../src/tools.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} satisfies ServerFunctionOptions;\n\nexport const defaultRPCOptions: RpcPluginOptions = {\n rpcPreffix: \"__rpc\",\n adapter: \"express\",\n};\n\nexport const defaultMiddlewareOptions = {\n rpcPreffix: undefined,\n path: 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 { Buffer } from \"node:buffer\";\nimport type { ViteDevServer } from \"vite\";\nimport type { Express } from \"express\";\nimport { createRPCMiddleware } from \"./createMiddleware.ts\";\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\nexport function attachVite(app: Express, vite: ViteDevServer) {\n app.use(vite.middlewares);\n}\n\n// src/express/helpers.ts\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\nexport const isExpressRequest = (\n req: IncomingMessage | ExpressRequest,\n): req is ExpressRequest => {\n return \"originalUrl\" in req;\n};\n\nexport const isExpressResponse = (\n res: ServerResponse | ExpressResponse,\n): res is ExpressResponse => {\n return \"json\" in res && \"send\" in res;\n};\n\nexport const hasPreParsedBody = (\n req: IncomingMessage | ExpressRequest,\n): req is ExpressRequest => {\n return \"body\" in req;\n};\n\nexport const getRequestDetails = (\n request: ExpressRequest | IncomingMessage,\n) => {\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\nexport const getResponseDetails = (\n response: ExpressResponse | ServerResponse,\n) => {\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","export function escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\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 { scanForServerFiles, serverFunctionsMap } from \"@thednp/rpc/server\";\nimport { defaultMiddlewareOptions, defaultRPCOptions } from \"../options.ts\";\nimport { getRequestDetails, getResponseDetails, readBody } from \"./helpers.ts\";\nimport { escapeRegExp } from \"../tools.ts\";\n\nlet middlewareCount = 0;\nconst middlewareStack = new Set<string>();\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 rpcPreffix = options.rpcPreffix;\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(`The middleware name \"${name}\" is already used.`);\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 = rpcPreffix\n ? new RegExp(`^/${escapeRegExp(rpcPreffix)}/`)\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 // rpcPreffix 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// Create RPC middleware\nexport const createRPCMiddleware: ExpressMiddlewareFn = (\n initialOptions = {},\n) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n { rpcPreffix: defaultRPCOptions.rpcPreffix },\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 rpcPreffix = options.rpcPreffix;\n const prefixRegex = rpcPreffix\n ? new RegExp(`^/${escapeRegExp(rpcPreffix)}/`)\n : /* istanbul ignore next */ null;\n const prefixReplace = `/${rpcPreffix}/`;\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 } = 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(url)) {\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 const functionName = url.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 body = await readBody(req);\n const args = Array.isArray(body.data) ? body.data : [body.data];\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","import 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\";\n\nimport {\n getClientModules,\n scanForServerFiles,\n serverFunctionsMap,\n} from \"@thednp/rpc/server\";\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 * Utility to define `@thednp/rpc` configuration file similar to vite.\n * @param uniConfig a system wide RPC configuration\n */\nconst defineConfig = (uniConfig: Partial<RpcPluginOptions>) => {\n return mergeConfig(defaultRPCOptions, uniConfig) as RpcPluginOptions;\n};\n\nlet RPCConfig: RpcPluginOptions;\n\n/**\n * Utility to load `@thednp/rpc` configuration file system wide.\n * @param configFile an optional parameter to specify a file within your project scope\n */\nasync function loadRPCConfig(configFile?: string) {\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(\n ` ⚠︎ The specified RPC config file ${configFile} cannot be found at ${configFilePath}, loading the defaults..`,\n );\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(\n ` ⚡︎ No RPC config found, loading the defaults..`,\n );\n // return defaultRPCOptions as RpcPluginOptions;\n // RPCConfig = defaultRPCOptions;\n } catch (error) {\n RPCConfig = defaultRPCOptions;\n console.warn(\n ` ⚠︎ Failed to load RPC config:`,\n error,\n );\n // return defaultRPCOptions as RpcPluginOptions;\n }\n\n return RPCConfig;\n}\n\nfunction rpcPlugin(\n devOptions: Partial<RpcPluginOptions> = {},\n): Plugin<unknown> {\n // Internal type - adapters are handled at runtime\n let options: RpcPluginOptions & { rpcPreffix: 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 rpcPreffix: options.rpcPreffix,\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":";;;;;;AAUA,MAAa,oBAAsC;CACjD,YAAY;CACZ,SAAS;AACX;AAEA,MAAa,2BAA2B;CACtC,YAAY,KAAA;CACZ,MAAM,KAAA;AACR;;;ACKA,MAAa,YACX,QACwB;CACxB,OAAO,IAAI,SAAS,SAAS,WAAW;EAGtC,IAAI,iBAAiB,GAAG,KAAK,IAAI,SAAS,KAAA,GAAW;GAGnD,MAAM,UADc,IAAI,QAAQ,eAAe,EAAE,YAAY,KAAK,GAAA,CACvC,SAAS,MAAM;GAC1C,QAAQ;IACN,aAAa,SAAS,qBAAqB;IAC3C,MAAM,SAAS,IAAI,OAAO,OAAO,IAAI,IAAI;GAC3C,CAAe;GACf;EACF;EAGA,IAAI,OAAO;EAEX,MAAM,mBAAmB,QAAkB;GACzC,MAAM,SAAS,MAAM,OAAO;GAC5B,IAAI,OAAO,CAAC,QAAQ,MAAM;GAC1B,IAAI,OAAO,CAAC,OAAO,KAAK;GACxB,IAAI,OAAO,CAAC,SAAS,OAAO;EAC9B;EAEA,MAAM,UAAU,UAAkB;GAChC,QAAQ,MAAM,SAAS;EACzB;EAEA,MAAM,cAAc;GAClB,gBAAgB;GAEhB,MAAM,UADe,IAAI,QAAQ,eAAe,EAAE,YAAY,KAAK,GAAA,CACvC,SAAS,MAAM;GAC3C,IAAI;IACF,MAAM,OAAO,KAAK,MAAM,IAAI;IAC5B,QAAQ;KACN,aAAa,SAAS,qBAAqB;KAC3C;IACF,CAAC;GACH,SAAS,IAAI;IACX,QAAQ;KAAE,aAAa;KAAc,MAAM,OAAO,IAAI;IAAE,CAAC;GAC3D;EACF;EAEA,MAAM,WAAW,QAAe;GAC9B,gBAAgB;GAEhB,OAAO,GAAG;EACZ;EAEA,gBAAgB,IAAI;CACtB,CAAC;AACH;AAEA,MAAa,oBACX,QAC0B;CAC1B,OAAO,iBAAiB;AAC1B;AAEA,MAAa,qBACX,QAC2B;CAC3B,OAAO,UAAU,OAAO,UAAU;AACpC;AAEA,MAAa,oBACX,QAC0B;CAC1B,OAAO,UAAU;AACnB;AAEA,MAAa,qBACX,YACG;CACH,MAAM,SACJ,iBAAiB,OAAO,IAAI,QAAQ,cAAc,QAAQ;CAE5D,MAAM,MAAM,IAAI,IAAI,QAAQ,kBAAkB;CAE9C,OAAO;EACL,KAAK,IAAI;EACT,QAAQ,IAAI;EACZ,cAAc,IAAI;EAClB,SAAS,QAAQ;EACjB,QAAQ,QAAQ;CAClB;AACF;AAEA,MAAa,sBACX,aACG;CACH,MAAM,iBAAiB,SAAS,eAAe,SAAS;CAExD,MAAM,aAAa,MAAc,UAAkB;EACjD,IAAI,kBAAkB,QAAQ,GAC5B,SAAS,OAAO,MAAM,KAAK;OAE3B,SAAS,UAAU,MAAM,KAAK;CAElC;CAEA,MAAM,iBAAiB,SAAiB;EACtC,IAAI,kBAAkB,QAAQ,GAC5B,SAAS,OAAO,IAAI;OAEpB,SAAS,aAAa;CAE1B;CAEA,MAAM,gBAAgB,MAAc,WAAsC;EACxE,cAAc,IAAI;EAClB,UAAU,gBAAgB,kBAAkB;EAE5C,IAAI,kBAAkB,QAAQ,GAC5B,SAAS,KAAK,KAAK,UAAU,MAAM,CAAC;OAEpC,SAAS,IAAI,KAAK,UAAU,MAAM,CAAC;CAEvC;CAEA,OAAO;EACL;EACA;EACA,YAAY,SAAS;EACrB;EACA;CACF;AACF;;;ACzJA,SAAgB,aAAa,GAAmB;CAC9C,OAAO,EAAE,QAAQ,uBAAuB,MAAM;AAChD;;;ACgBA,IAAI,kBAAkB;AACtB,MAAM,kCAAkB,IAAI,IAAY;AAExC,MAAa,oBAAyC,iBAAiB,CAAC,MAAM;CAC5E,MAAM,UAAU,OAAO,OACrB,CAAC,GACD,0BACA,cACF;CACA,MAAM,iBAAiB,QAAQ;CAC/B,MAAM,aAAa,QAAQ;CAC3B,MAAM,OAAO,QAAQ;CACrB,MAAM,UAAU,QAAQ;CAExB,IAAI,OAAO;CACX,IAAI,CAAC,MAAM;EACT,OAAO,uBAAuB;EAC9B,mBAAmB;CACrB;CACA,IAAI,gBAAgB,IAAI,IAAI,GAC1B,MAAM,IAAI,MAAM,wBAAwB,KAAK,mBAAmB;CAElE,gBAAgB,IAAI,IAAI;CAIxB,MAAM,cAA6B,aAC/B,IAAI,OAAO,KAAK,aAAa,UAAU,EAAE,EAAE,IAC3C;CACJ,MAAM,cAA6B,OAC9B,OAAO,SAAS,WAAW,IAAI,OAAO,IAAI,IAAI,OAC/C;CAEJ,MAAM,oBAAoB,OACxB,KACA,MACA,SACG;EACH,MAAM,EAAE,QAAQ,kBAAkB,GAAG;EAGrC,IAAI,mBAAmB,SAAS,GAC9B,MAAM,mBAAmB;EAI3B,IAAI,CAAC,SACH,OAAO,OAAO;EAIhB,IAAI,eAAe,CAAC,YAAY,KAAK,GAAG,GAAG,OAAO,OAAO;EAGzD,IAAI,eAAe,CAAC,YAAY,KAAK,GAAG,GACtC,OAAO,OAAO;EAIhB,MAAM,QAAQ,KAAK,MAAM,IAAI;CAC/B;CAEA,OAAO,eAAe,mBAAmB,QAAQ,EAC/C,OAAO,KACT,CAAC;CAED,OAAO;AACT;AAGA,MAAa,uBACX,iBAAiB,CAAC,MACf;CACH,MAAM,UAAU,OAAO,OACrB,CAAC,GACD,0BACA,EAAE,YAAY,kBAAkB,WAAW,GAC3C,cACF;CAIA,MAAM,aAAa,QAAQ;CAC3B,MAAM,cAAc,aAChB,IAAI,OAAO,KAAK,aAAa,UAAU,EAAE,EAAE,IAC1C;CACL,MAAM,gBAAgB,IAAI,WAAW;CAErC,OAAO,iBAAiB;EACtB,GAAG;EACH,SAAS,OACP,KACA,KACA,UACG;GACH,MAAM,EAAE,QAAQ,kBAAkB,GAAG;GACrC,MAAM,EAAE,iBAAiB,mBAAmB,GAAG;GAI/C,IAAI,eAAe,CAAC,YAAY,KAAK,GAAG,GAGtC;GAGF,MAAM,eAAe,IAAI,QAAQ,eAAe,EAAE;GAClD,MAAM,iBAAiB,mBAAmB,IAAI,YAAY;GAE1D,IAAI,CAAC,gBAAgB;IACnB,aAAa,KAAK,EAAE,OAAO,qBAAqB,CAAC;IACjD;GACF;GAEA,IAAI;IACF,MAAM,OAAO,MAAM,SAAS,GAAG;IAC/B,MAAM,OAAO,MAAM,QAAQ,KAAK,IAAI,IAAI,KAAK,OAAO,CAAC,KAAK,IAAI;IAC9D,MAAM,EAAE,MAAM,WAAW,eAAe,QAAQ,GAAG,IAAI;IACvD,MAAM,gBAAgB,OAAO,qBAAqB;IAElD,IAAI,GAAG,SAAS,OAAO;IACvB,MAAM,SAAS,MAAM;IACrB,IAAI,IAAI,SAAS,OAAO;IAGxB,IAAI,CAAC,IAAI,aAAa,aAAa,KAAK,EAAE,MAAM,OAAO,CAAC;GAC1D,SAAS,KAAK;IACZ,QAAQ,MAAM,OAAO,GAAG,CAAC;IACzB,aAAa,KAAK,EAAE,OAAO,wBAAwB,CAAC;GACtD;EACF;CACF,CAAC;AACH;;;ACvIA,MAAM,iBAAiB,OAAO,KAAgB,SAAiB;CAC7D,MAAM,SAAS,MAAM,mBAAmB,KAAK,IAAI;CAKjD,OAAO,SACH;EAAE,GAAG;EAAQ,QAAQ;GAAE,GAAG,OAAO;GAAQ,YAAY;EAAK;CAAE,IAC3D;AACP;;;;;AAMA,MAAM,gBAAgB,cAAyC;CAC7D,OAAO,YAAY,mBAAmB,SAAS;AACjD;AAEA,IAAI;;;;;AAMJ,eAAe,cAAc,YAAqB;CAChD,IAAI;EAEF,MAAM,MAAoC;GACxC,SAAS;GACT,MAAM,QAAQ,IAAI;GAClB,MAAM,QAAQ,IAAI,YAAY;EAChC;EACA,MAAM,qBAAqB;GACzB;GACA;GACA;GACA;GACA;GACA;EACF;EAGA,IAAI,YAAY;GACd,MAAM,iBAAiB,QAAQ,IAAI,MAAM,UAAU;GACnD,IAAI,CAAC,WAAW,cAAc,GAAG;IAC/B,QAAQ,KACN,sCAAsC,WAAW,sBAAsB,eAAe,yBACxF;IACA,YAAY;IACZ,OAAO;GACT;GAEA,MAAM,SAAS,MAAM,eAAe,KAAK,UAAU;GAEnD,IAAI,UAAU,OAAO,WAAW,UAAU;IACxC,YAAY,YACV;KACE,GAAG;KACH,YAAY;IACd,GACA,OAAO,MACT;IAEA,OAAO;GACT;GAEA,YAAY;EACd;EAEA,IAAI,cAAc,KAAA,GAChB,OAAO;EAIT,KAAK,MAAM,QAAQ,oBAAoB;GACrC,MAAM,iBAAiB,QAAQ,IAAI,MAAM,IAAI;GAE7C,IAAI,CAAC,WAAW,cAAc,GAC5B;GAGF,MAAM,SAAS,MAAM,eAAe,KAAK,IAAI;GAE7C,IAAI,QAAQ;IACV,YAAY,YACV;KACE,GAAG;KACH,YAAY;IACd,GACA,OAAO,MACT;IAEA,OAAO;GACT;EACF;EACA,YAAY;EAEZ,QAAQ,KACN,iDACF;CAGF,SAAS,OAAO;EACd,YAAY;EACZ,QAAQ,KACN,kCACA,KACF;CAEF;CAEA,OAAO;AACT;AAEA,SAAS,UACP,aAAwC,CAAC,GACxB;CAEjB,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI,QAAQ;CAEZ,OAAO;EACL,MAAM;EACN,SAAS;EAaT,MAAM,eAAe,gBAAgB;GAEnC,UAAU,YAAY,MADE,cAAc,GACL,UAAU;GAE3C,SAAS;EACX;EACA,MAAM,gBAAgB,QAAQ;GAC5B,aAAa;GACb,MAAM,EAAE,SAAS,UAAU,GAAG,SAAS;GAEvC,IAAI,mBAAmB,SAAS,GAC9B,MAAM,mBAAmB,QAAQ,UAAU;GAI7C,OAAO,YAAY,IAAI,oBAAoB,IAAI,CAAC;EAClD;EAEA,MAAM,aAAa;GACjB,MAAM,cAAc,KAAK,MAAM;GAC/B,QAAQ,OAAO,YAAY,EAAE,KAAK;GAGlC,IAAI,CAAC,cAAc,QACjB,MAAM,mBAAmB,MAAM;EAEnC;EACA,MAAM,UAAU,MAAc,IAAY,KAAyB;GAEjE,IACE,CAAC,KAAK,SAAS,sBAAsB,KACrC,KAAK,OACJ,KAAK,SAAS,sBAAsB,KACnC,OAAO,YAAY,aAErB,OAAO;GAGT,IAAI,mBAAmB,SAAS,GAC9B,MAAM,mBAAmB,MAAM;GAGjC,MAAM,OAAO,MAAM,OAAO;GAC1B,MAAM,cAAc,QAAQ,qBAAqB;GACjD,MAAM,WAAW,QAAQ,SAAS;GAClC,MAAM,SAAS,iBAAiB;IAC9B,YAAY,QAAQ;IACpB,SAAS,QAAQ;GACnB,CAAC;GAED,MAAM,SAAS,MAAM,KAAK,YAAY,CAAC,QAAQ,IAAI;KAChD,WAAW;IACZ,WAAW;GAEb,CAAC;GAED,OAAO;IACL,MAAM,OAAO;IACb,KAAK,OAAO,MACR,OAAO,OAAO,QAAQ,WACpB,KAAK,MAAM,OAAO,GAAG,IACpB,OAAO,MACT;GACP;EACF;CACF;AACF"}
@@ -0,0 +1,26 @@
1
+ import Koa, { Context, Next } from "koa";
2
+ import { BodyResult, JsonValue, MiddlewareOptions, RpcPluginOptions } from "@thednp/rpc";
3
+ import { ViteDevServer } from "vite";
4
+ //#region src/koa/types.d.ts
5
+ type KoaMiddlewareOptions = MiddlewareOptions<"koa">;
6
+ interface KoaContext extends Context {
7
+ request: Context["request"] & {
8
+ body?: string | JsonValue;
9
+ };
10
+ }
11
+ interface KoaMiddlewareHooks {
12
+ handler: (ctx: Context, next: Next) => Promise<void>;
13
+ }
14
+ type KoaMiddlewareFn = <A extends RpcPluginOptions["adapter"] = "koa">(initialOptions?: Partial<KoaMiddlewareOptions>) => KoaMiddlewareHooks["handler"];
15
+ //#endregion
16
+ //#region src/koa/createMiddleware.d.ts
17
+ declare const createMiddleware: KoaMiddlewareFn;
18
+ declare const createRPCMiddleware: KoaMiddlewareFn;
19
+ //#endregion
20
+ //#region src/koa/helpers.d.ts
21
+ declare function attachRPC(app: Koa): Promise<void>;
22
+ declare function attachVite(app: Koa, vite: ViteDevServer): void;
23
+ declare const readBody: (ctx: KoaContext) => Promise<BodyResult>;
24
+ //#endregion
25
+ export { type Koa, type KoaContext, type KoaMiddlewareFn, type KoaMiddlewareHooks, type KoaMiddlewareOptions, attachRPC, attachVite, createMiddleware, createRPCMiddleware, readBody };
26
+ //# sourceMappingURL=koa.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"koa.d.mts","names":[],"sources":["../../src/koa/types.d.ts","../../src/koa/createMiddleware.ts","../../src/koa/helpers.ts"],"mappings":";;;;KAQY,uBAAuB;UAElB,mBAAmB;EAClC,SAAS;IAAuB,gBAAgB;;;UAGjC;EACf,UAAU,KAAK,SAAS,MAAM,SAAS;;KAG7B,mBAAmB,UAAU,qCACvC,iBAAiB,QAAQ,0BACtB;;;cCPQ,kBAAkB;cA6DlB,qBAAqB;;;iBClEZ,UAAU,KAAK,MAAG;iBAQxB,WAAW,KAAK,KAAK,MAAM;cAgC9B,WAAQ,KACd,eACJ,QAAQ"}
@@ -0,0 +1,155 @@
1
+ import { scanForServerFiles, serverFunctionsMap } from "@thednp/rpc/server";
2
+ //#region src/tools.ts
3
+ function escapeRegExp(s) {
4
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5
+ }
6
+ //#endregion
7
+ //#region src/options.ts
8
+ const defaultRPCOptions = {
9
+ rpcPreffix: "__rpc",
10
+ adapter: "express"
11
+ };
12
+ const defaultMiddlewareOptions = {
13
+ rpcPreffix: void 0,
14
+ path: void 0
15
+ };
16
+ //#endregion
17
+ //#region src/koa/helpers.ts
18
+ async function attachRPC(app) {
19
+ const { loadRPCConfig } = await import("@thednp/rpc");
20
+ const { adapter: _adapter, ...options } = await loadRPCConfig();
21
+ app.use(createRPCMiddleware(options));
22
+ }
23
+ function attachVite(app, vite) {
24
+ app.use(async (ctx, next) => {
25
+ const req = ctx.req;
26
+ const res = ctx.res;
27
+ const requestBody = ctx.request?.body;
28
+ if (requestBody !== void 0) Object.assign(req, { body: requestBody });
29
+ const originalEnd = res.end.bind(res);
30
+ let viteHandled = false;
31
+ res.end = function(...args) {
32
+ viteHandled = true;
33
+ return originalEnd(args[0]);
34
+ };
35
+ await new Promise((resolve) => {
36
+ vite.middlewares(req, res, () => resolve(void 0));
37
+ });
38
+ res.end = originalEnd;
39
+ if (!viteHandled || res.statusCode === 404) await next();
40
+ });
41
+ }
42
+ const readBody = (ctx) => {
43
+ const contentType = ctx.request.headers["content-type"]?.toLowerCase() || "";
44
+ return new Promise((resolve, reject) => {
45
+ const isJSON = contentType.includes("json");
46
+ const reqBody = ctx.request.body;
47
+ if (reqBody !== void 0) {
48
+ resolve({
49
+ contentType: isJSON ? "application/json" : "text/plain",
50
+ data: isJSON ? reqBody : String(reqBody)
51
+ });
52
+ return;
53
+ }
54
+ let body = "";
55
+ const toggleListeners = (add) => {
56
+ const method = add ? "on" : "off";
57
+ ctx.req[method]("data", onData);
58
+ ctx.req[method]("end", onEnd);
59
+ ctx.req[method]("error", onError);
60
+ };
61
+ const onData = (chunk) => {
62
+ body += chunk.toString();
63
+ };
64
+ const onEnd = () => {
65
+ toggleListeners();
66
+ const isJSON = contentType.includes("json");
67
+ try {
68
+ const data = JSON.parse(body);
69
+ resolve({
70
+ contentType: isJSON ? "application/json" : "text/plain",
71
+ data
72
+ });
73
+ } catch (_er) {
74
+ resolve({
75
+ contentType: "text/plain",
76
+ data: String(body)
77
+ });
78
+ }
79
+ };
80
+ const onError = (err) => {
81
+ toggleListeners();
82
+ reject(err);
83
+ };
84
+ toggleListeners(true);
85
+ });
86
+ };
87
+ //#endregion
88
+ //#region src/koa/createMiddleware.ts
89
+ let middlewareCount = 0;
90
+ const middlewareStack = /* @__PURE__ */ new Set();
91
+ const createMiddleware = (initialOptions = {}) => {
92
+ const options = Object.assign({}, defaultMiddlewareOptions, initialOptions);
93
+ const middlewareName = options.name;
94
+ const rpcPreffix = options.rpcPreffix;
95
+ const path = options.path;
96
+ const handler = options.handler;
97
+ let name = middlewareName;
98
+ if (!name) {
99
+ name = "viteRPCMiddleware-" + middlewareCount;
100
+ middlewareCount += 1;
101
+ }
102
+ if (middlewareStack.has(name)) throw new Error(`The middleware name "${name}" is already used.`);
103
+ middlewareStack.add(name);
104
+ const prefixRegex = rpcPreffix ? new RegExp(`^/${escapeRegExp(rpcPreffix)}/`) : null;
105
+ const pathMatcher = path ? typeof path === "string" ? new RegExp(path) : path : null;
106
+ const middlewareHandler = async (ctx, next) => {
107
+ const url = new URL(ctx.url, "http://localhost").pathname;
108
+ if (serverFunctionsMap.size === 0) await scanForServerFiles();
109
+ if (!handler) return next();
110
+ if (pathMatcher && !pathMatcher.test(url)) return next();
111
+ if (prefixRegex && !prefixRegex.test(url)) return next();
112
+ await handler(ctx, next);
113
+ };
114
+ Object.defineProperty(middlewareHandler, "name", { value: name });
115
+ return middlewareHandler;
116
+ };
117
+ const createRPCMiddleware = (initialOptions = {}) => {
118
+ const options = Object.assign({}, defaultMiddlewareOptions, { rpcPreffix: defaultRPCOptions.rpcPreffix }, initialOptions);
119
+ const rpcPreffix = options.rpcPreffix;
120
+ const prefixRegex = rpcPreffix ? new RegExp(`^/${escapeRegExp(rpcPreffix)}/`) : null;
121
+ const prefixReplace = `/${rpcPreffix}/`;
122
+ return createMiddleware({
123
+ ...options,
124
+ handler: async (ctx, _next) => {
125
+ const url = new URL(ctx.url, "http://localhost").pathname;
126
+ if (prefixRegex && !prefixRegex.test(url)) return;
127
+ const functionName = url.replace(prefixReplace, "");
128
+ const serverFunction = serverFunctionsMap.get(functionName);
129
+ if (!serverFunction) {
130
+ ctx.status = 404;
131
+ ctx.body = { error: "Function not found" };
132
+ return;
133
+ }
134
+ try {
135
+ const body = await readBody(ctx);
136
+ const args = Array.isArray(body.data) ? body.data : [body.data];
137
+ const { data: resultData, cancel } = serverFunction.handler(...args);
138
+ const onClose = () => cancel("client disconnected");
139
+ ctx.req.on("close", onClose);
140
+ const result = await resultData;
141
+ ctx.req.off("close", onClose);
142
+ ctx.status = 200;
143
+ ctx.body = { data: result };
144
+ } catch (err) {
145
+ console.error(String(err));
146
+ ctx.status = 500;
147
+ ctx.body = { error: "Internal Server Error" };
148
+ }
149
+ }
150
+ });
151
+ };
152
+ //#endregion
153
+ export { attachRPC, attachVite, createMiddleware, createRPCMiddleware, readBody };
154
+
155
+ //# sourceMappingURL=koa.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"koa.mjs","names":[],"sources":["../../src/tools.ts","../../src/options.ts","../../src/koa/helpers.ts","../../src/koa/createMiddleware.ts"],"sourcesContent":["export function escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n","import type {\n MiddlewareOptions,\n RpcPluginOptions,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\n\nexport const defaultServerFnOptions = {\n contentType: \"application/json\",\n} satisfies ServerFunctionOptions;\n\nexport const defaultRPCOptions: RpcPluginOptions = {\n rpcPreffix: \"__rpc\",\n adapter: \"express\",\n};\n\nexport const defaultMiddlewareOptions = {\n rpcPreffix: undefined,\n path: undefined,\n} satisfies MiddlewareOptions;\n","// src/koa/helpers.ts\nimport { 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\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\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\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\";\n// import type { JsonArray, JsonValue } from \"@thednp/rpc\";\nimport type { KoaMiddlewareFn, KoaMiddlewareOptions } from \"./types.d.ts\";\nimport { escapeRegExp } from \"../tools.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\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 rpcPreffix = options.rpcPreffix;\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(`The middleware name \"${name}\" is already used.`);\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 = rpcPreffix\n ? new RegExp(`^/${escapeRegExp(rpcPreffix)}/`)\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\nexport const createRPCMiddleware: KoaMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n { rpcPreffix: defaultRPCOptions.rpcPreffix },\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 rpcPreffix = options.rpcPreffix;\n const prefixRegex = rpcPreffix\n ? new RegExp(`^/${escapeRegExp(rpcPreffix)}/`)\n : /* istanbul ignore next */ null;\n const prefixReplace = `/${rpcPreffix}/`;\n\n return createMiddleware({\n ...options,\n handler: async (ctx: Context, _next: Next) => {\n const url = new URL(ctx.url, \"http://localhost\").pathname;\n // const { rpcPreffix } = 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 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 body = await readBody(ctx);\n const args = Array.isArray(body.data) ? body.data : [body.data];\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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"}
@@ -0,0 +1,92 @@
1
+ import { ResolvedConfig, ViteDevServer } from "vite";
2
+ import "@thednp/rpc";
3
+ import "express";
4
+ import "hono";
5
+ import "@hono/node-server";
6
+ import "fastify";
7
+ import "koa";
8
+ //#region src/types.d.ts
9
+ type ContentType = "application/json" | "text/plain";
10
+ interface ServerFunctionOptions {
11
+ contentType: ContentType;
12
+ }
13
+ // primitives and their compositions
14
+ type JsonPrimitive = string | number | boolean | null | undefined;
15
+ type JsonObject = {
16
+ [key: string]: JsonValue | JsonArray;
17
+ };
18
+ type JsonArray = JsonValue[];
19
+ type JsonValue = JsonPrimitive | JsonArray | JsonObject;
20
+ type ServerFunctionInit<TArgs extends JsonArray = JsonArray, TResult extends JsonValue = JsonValue> = (signal: AbortSignal, ...args: TArgs) => Promise<TResult>;
21
+ type ClientFunction<TArgs extends JsonArray = JsonArray, TResult extends JsonValue = JsonValue> = (...args: TArgs) => {
22
+ data: Promise<TResult>;
23
+ cancel: (reason: string) => void;
24
+ };
25
+ type ClientFunctionWithOptions = ClientFunction & {
26
+ name: string;
27
+ options?: ServerFunctionOptions;
28
+ };
29
+ interface ServerFnEntry {
30
+ name: string;
31
+ handler: ClientFunctionWithOptions;
32
+ options?: ServerFunctionOptions;
33
+ exportName?: string;
34
+ }
35
+ /**
36
+ * ### @thednp/rpc
37
+ * The plugin configuration allows for granular control of your
38
+ * application RPC calls. The default settings are optimized for development
39
+ * environments while providing a secure foundation for production use.
40
+ */
41
+ interface RpcPluginOptions {
42
+ // RPC Middleware Options
43
+ /**
44
+ * RPC prefix without leading slash (e.g. "__rpc")
45
+ * Leading slash will be added automatically by the middleware.
46
+ * This prefix defines the base path for all RPC endpoints.
47
+ * @default "__rpc"
48
+ * @example
49
+ * // Results in endpoints like: /api/rpc/myFunction
50
+ * rpcPreffix: "api/rpc"
51
+ */
52
+ rpcPreffix: "__rpc" | string;
53
+ /**
54
+ * Option to set an adapter for the middleware connection. The default is _express_,
55
+ * which is the most popular and battle tested server app. The _express_ adapter is
56
+ * also compatible with the vite's Connect development server.
57
+ * @default express
58
+ */
59
+ adapter: "express" | "hono" | "fastify" | "koa";
60
+ }
61
+ //#endregion
62
+ //#region src/functionsMap.d.ts
63
+ declare const serverFunctionsMap: Map<string, ServerFnEntry>;
64
+ //#endregion
65
+ //#region src/scanForServerFiles.d.ts
66
+ type ScanConfig = Pick<ResolvedConfig, "root" | "base"> & {
67
+ server?: Partial<ResolvedConfig["server"]>;
68
+ };
69
+ declare const scanForServerFiles: (initialCfg?: ScanConfig, devServer?: ViteDevServer) => Promise<void>;
70
+ //#endregion
71
+ //#region src/createFunction.d.ts
72
+ declare function createServerFunction<TArgs extends JsonArray = JsonArray, TResult extends JsonValue = JsonValue>(name: string, handler: ServerFunctionInit<TArgs, TResult>, fnOptions?: Partial<ServerFunctionOptions>): ClientFunction<TArgs, TResult>;
73
+ //#endregion
74
+ //#region src/getClientModules.d.ts
75
+ interface RpcPluginOptionsInternal {
76
+ rpcPreffix: string;
77
+ adapter?: string | undefined;
78
+ }
79
+ declare const getClientModules: (initialOptions: RpcPluginOptionsInternal) => string;
80
+ //#endregion
81
+ //#region src/options.d.ts
82
+ declare const defaultServerFnOptions: {
83
+ contentType: "application/json";
84
+ };
85
+ declare const defaultRPCOptions: RpcPluginOptions;
86
+ declare const defaultMiddlewareOptions: {
87
+ rpcPreffix: undefined;
88
+ path: undefined;
89
+ };
90
+ //#endregion
91
+ export { createServerFunction, defaultMiddlewareOptions, defaultRPCOptions, defaultServerFnOptions, getClientModules, scanForServerFiles, serverFunctionsMap };
92
+ //# sourceMappingURL=server.d.mts.map
@@ -0,0 +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":";;;;;;;;KAmCY;UAMK;EACf,aAAa;;;KAIH;KACA;GAAgB,cAAc,YAAY;;KAC1C,YAAY;KACZ,YAAY,gBAAgB,YAAY;KA2BxC,mBACV,cAAc,YAAY,WAC1B,gBAAgB,YAAY,cACzB,QAAQ,gBAAgB,MAAM,UAAU,QAAQ;KAEzC,eACV,cAAc,YAAY,WAC1B,gBAAgB,YAAY,iBACtB,MAAM;EACZ,MAAM,QAAQ;EACd,SAAS;;KAGC,4BAA4B;EACtC;EACA,UAAU;;UAGK;EACf;EACA,SAAS;EACT,UAAU;EACV;;;;;;;;UAee;;;;;;;;;;;EAWf;;;;;;;EAQA;;;;cClIW,oBAAkB,YAAA;;;KCO1B,aAAa,KAAK;EACrB,SAAS,QAAQ;;cAIN,qBAAkB,aAChB,YAAU,YACX,kBAAa;;;iBCLX,qBACd,cAAc,YAAY,WAC1B,gBAAgB,YAAY,WAE5B,cACA,SAAS,mBAAmB,OAAO,UACnC,YAAW,QAAQ,yBAA2B,eAAA,OAAA;;;UChBtC;EACR;EACA;;cAwEW,mBAAgB,gBAAoB;;;cCrEpC;EAAA;;cAIA,mBAAmB;cAKnB;EAAA;EAAA"}
@@ -0,0 +1,161 @@
1
+ import { createServer } from "vite";
2
+ import { readdir } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import process from "node:process";
5
+ //#region src/functionsMap.ts
6
+ const serverFunctionsMap = /* @__PURE__ */ new Map();
7
+ //#endregion
8
+ //#region src/scanForServerFiles.ts
9
+ let isScanned = false;
10
+ const scanForServerFiles = async (initialCfg, devServer) => {
11
+ if (isScanned && !devServer) return;
12
+ const config = !initialCfg && !devServer || !initialCfg ? {
13
+ root: process.cwd(),
14
+ base: process.env.BASE || "/",
15
+ server: { middlewareMode: true }
16
+ } : {
17
+ ...initialCfg,
18
+ root: process.cwd()
19
+ };
20
+ let server = devServer;
21
+ if (!server) server = await createServer({
22
+ server: config.server,
23
+ appType: "custom",
24
+ base: config.base,
25
+ root: config.root
26
+ });
27
+ const svFiles = [
28
+ "server.ts",
29
+ "server.js",
30
+ "server.mjs",
31
+ "server.mts"
32
+ ];
33
+ const apiDir = join(config.root, "src", "api");
34
+ let files;
35
+ try {
36
+ files = (await readdir(apiDir, { withFileTypes: true })).filter((f) => svFiles.some((fn) => f.name.includes(fn))).map((f) => join(apiDir, f.name));
37
+ } catch (_e) {
38
+ files = [];
39
+ }
40
+ try {
41
+ for (const file of files) try {
42
+ const moduleExports = await server.ssrLoadModule(file);
43
+ const moduleEntries = Object.entries(moduleExports);
44
+ if (!moduleEntries.length) {
45
+ console.warn("No server function found.");
46
+ return;
47
+ }
48
+ for (const [exportName, exportValue] of moduleEntries) {
49
+ const registeredName = exportValue.name;
50
+ serverFunctionsMap.set(registeredName, {
51
+ name: registeredName,
52
+ handler: exportValue,
53
+ options: exportValue?.options,
54
+ exportName
55
+ });
56
+ }
57
+ } catch (error) {
58
+ console.error("Error loading file:", file, error);
59
+ }
60
+ } finally {
61
+ if (!devServer && server) await server.close();
62
+ isScanned = true;
63
+ }
64
+ };
65
+ //#endregion
66
+ //#region src/options.ts
67
+ const defaultServerFnOptions = { contentType: "application/json" };
68
+ const defaultRPCOptions = {
69
+ rpcPreffix: "__rpc",
70
+ adapter: "express"
71
+ };
72
+ const defaultMiddlewareOptions = {
73
+ rpcPreffix: void 0,
74
+ path: void 0
75
+ };
76
+ //#endregion
77
+ //#region src/createFunction.ts
78
+ function createServerFunction(name, handler, fnOptions = {}) {
79
+ const options = Object.assign({}, defaultServerFnOptions, fnOptions);
80
+ const wrappedFunction = (...args) => {
81
+ const controller = new AbortController();
82
+ const cancel = (reason) => controller.abort(reason);
83
+ const fetcher = async () => {
84
+ if (controller.signal.aborted) throw new Error("Operation aborted");
85
+ return await handler(controller.signal, ...args);
86
+ };
87
+ return {
88
+ data: fetcher(),
89
+ cancel
90
+ };
91
+ };
92
+ Object.defineProperties(wrappedFunction, {
93
+ name: {
94
+ value: name,
95
+ enumerable: true,
96
+ configurable: true
97
+ },
98
+ options: {
99
+ value: options,
100
+ enumerable: true,
101
+ configurable: true
102
+ }
103
+ });
104
+ serverFunctionsMap.set(name, {
105
+ name,
106
+ handler: wrappedFunction,
107
+ options
108
+ });
109
+ return wrappedFunction;
110
+ }
111
+ //#endregion
112
+ //#region src/getClientModules.ts
113
+ const SAFE_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
114
+ const SAFE_PATH_SEGMENT = /^[A-Za-z0-9_$][A-Za-z0-9_$/-]*$/;
115
+ function validateIdentifier(name, label) {
116
+ if (!SAFE_IDENTIFIER.test(name)) throw new Error(`Invalid ${label}: "${name}" must match /^[A-Za-z_$][A-Za-z0-9_$]*$/`);
117
+ return name;
118
+ }
119
+ function validatePathSegment(segment, label) {
120
+ if (!SAFE_PATH_SEGMENT.test(segment)) throw new Error(`Invalid ${label}: "${segment}" must match /^[A-Za-z0-9_$][A-Za-z0-9_$/-]*$/`);
121
+ return segment;
122
+ }
123
+ const getModule = (fnName, fnEntry, options) => {
124
+ const safeFnName = validatePathSegment(fnName, "function name");
125
+ const safeFnEntry = validateIdentifier(fnEntry, "export name");
126
+ const safePrefix = validatePathSegment(options.rpcPreffix, "rpcPreffix");
127
+ let body = "";
128
+ let headers = "{}";
129
+ switch (options.contentType) {
130
+ case "text/plain":
131
+ body = `args[0]`;
132
+ headers = `{ 'Content-Type': 'text/plain' }`;
133
+ break;
134
+ default:
135
+ body = `JSON.stringify(args)`;
136
+ headers = `{ 'Content-Type': 'application/json' }`;
137
+ }
138
+ return `
139
+ export const ${safeFnEntry} = (...args) => {
140
+ const body = ${body};
141
+ const headers = ${headers};
142
+ const preffix = "${safePrefix}";
143
+ const name = "${safeFnName}";
144
+ return innerModule(body, headers, preffix, name);
145
+ }`.trim();
146
+ };
147
+ const getClientModules = (initialOptions) => {
148
+ validatePathSegment(initialOptions.rpcPreffix, "rpcPreffix");
149
+ return `
150
+
151
+ import { innerModule } from "@thednp/rpc/helpers";
152
+ ${Array.from(serverFunctionsMap.entries()).filter(([, entry]) => entry.exportName).map(([registeredName, entry]) => getModule(registeredName, entry.exportName, {
153
+ ...initialOptions,
154
+ ...entry.options || {}
155
+ })).join("\n")}
156
+ `.trim();
157
+ };
158
+ //#endregion
159
+ export { createServerFunction, defaultMiddlewareOptions, defaultRPCOptions, defaultServerFnOptions, getClientModules, scanForServerFiles, serverFunctionsMap };
160
+
161
+ //# sourceMappingURL=server.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.mjs","names":[],"sources":["../../src/functionsMap.ts","../../src/scanForServerFiles.ts","../../src/options.ts","../../src/createFunction.ts","../../src/getClientModules.ts"],"sourcesContent":["import type { ServerFnEntry } from \"./types.d.ts\";\n\nexport const serverFunctionsMap = new Map<string, ServerFnEntry>();\n","import type { ResolvedConfig, ViteDevServer } from \"vite\";\nimport type { ClientFunctionWithOptions } from \"./types.d.ts\";\nimport { createServer } from \"vite\";\nimport { readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport process from \"node:process\";\n\nimport { serverFunctionsMap } from \"./functionsMap.ts\";\n\ntype ScanConfig = Pick<ResolvedConfig, \"root\" | \"base\"> & {\n server?: Partial<ResolvedConfig[\"server\"]>;\n};\n\nlet isScanned = false;\nexport const scanForServerFiles = async (\n initialCfg?: ScanConfig,\n devServer?: ViteDevServer,\n) => {\n if (isScanned && !devServer) {\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 root: process.cwd(),\n };\n\n let server = devServer;\n if (!server) {\n server = await createServer({\n server: config.server,\n appType: \"custom\",\n base: config.base,\n root: config.root,\n });\n }\n\n const svFiles = [\"server.ts\", \"server.js\", \"server.mjs\", \"server.mts\"];\n const apiDir = join(config.root, \"src\", \"api\");\n let files: string[];\n try {\n files = (await readdir(apiDir, { withFileTypes: true }))\n .filter((f) => svFiles.some((fn) => f.name.includes(fn)))\n .map((f) => join(apiDir, f.name));\n } catch (_e) {\n files = [];\n }\n\n try {\n for (const file of files) {\n try {\n const moduleExports = (await server.ssrLoadModule(file)) as Record<\n string,\n ClientFunctionWithOptions\n >;\n const moduleEntries = Object.entries(moduleExports);\n if (!moduleEntries.length) {\n console.warn(\"No server function found.\");\n return;\n }\n\n for (const [exportName, exportValue] of moduleEntries) {\n // const registeredName = exportValue?.name ?? exportName;\n const registeredName = exportValue.name;\n serverFunctionsMap.set(registeredName, {\n name: registeredName,\n handler: exportValue,\n options: exportValue?.options,\n exportName,\n });\n }\n } catch (error) {\n console.error(\"Error loading file:\", file, error);\n }\n }\n } finally {\n if (!devServer && server) {\n await server.close();\n }\n isScanned = true;\n }\n};\n","import type {\n MiddlewareOptions,\n RpcPluginOptions,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\n\nexport const defaultServerFnOptions = {\n contentType: \"application/json\",\n} satisfies ServerFunctionOptions;\n\nexport const defaultRPCOptions: RpcPluginOptions = {\n rpcPreffix: \"__rpc\",\n adapter: \"express\",\n};\n\nexport const defaultMiddlewareOptions = {\n rpcPreffix: undefined,\n path: undefined,\n} satisfies MiddlewareOptions;\n","// /@thednp/rpc/src/createFn.ts\nimport type {\n ClientFunction,\n JsonArray,\n JsonValue,\n ServerFunctionInit,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\nimport { serverFunctionsMap } from \"./functionsMap.ts\";\nimport { defaultServerFnOptions } from \"./options.ts\";\n\nexport function createServerFunction<\n TArgs extends JsonArray = JsonArray,\n TResult extends JsonValue = JsonValue,\n>(\n name: string,\n handler: ServerFunctionInit<TArgs, TResult>,\n fnOptions: Partial<ServerFunctionOptions> = {},\n) {\n const options = Object.assign({}, defaultServerFnOptions, fnOptions);\n\n const wrappedFunction: ClientFunction<TArgs, TResult> = (...args: TArgs) => {\n const controller = new AbortController();\n const cancel = (reason: string) => controller.abort(reason);\n\n const fetcher = async () => {\n if (controller.signal.aborted) {\n throw new Error(\"Operation aborted\");\n }\n\n return await handler(controller.signal, ...args);\n };\n\n return {\n data: fetcher(),\n cancel,\n };\n };\n\n Object.defineProperties(wrappedFunction, {\n name: { value: name, enumerable: true, configurable: true },\n options: { value: options, enumerable: true, configurable: true },\n });\n\n serverFunctionsMap.set(\n name,\n {\n name,\n handler: wrappedFunction as never,\n options,\n },\n );\n\n return wrappedFunction;\n}\n","// Internal type that accepts all adapters\ninterface RpcPluginOptionsInternal {\n rpcPreffix: string;\n adapter?: string | undefined;\n}\n\nimport type { ServerFunctionOptions } from \"./types.d.ts\";\nimport { serverFunctionsMap } from \"./functionsMap.ts\";\n\n// Safe identifier pattern: must match JS identifier rules (letters, digits, $, _)\n// and must not contain regex metacharacters or template literal interpolation.\nconst SAFE_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;\n// Safe URL path segment: identifier characters plus \"/\" for nested prefixes like \"api/rpc\"\nconst SAFE_PATH_SEGMENT = /^[A-Za-z0-9_$][A-Za-z0-9_$/-]*$/;\n\nfunction validateIdentifier(name: string, label: string): string {\n // istanbul ignore if\n if (!SAFE_IDENTIFIER.test(name)) {\n throw new Error(\n `Invalid ${label}: \"${name}\" must match /^[A-Za-z_$][A-Za-z0-9_$]*$/`,\n );\n }\n return name;\n}\n\nfunction validatePathSegment(segment: string, label: string): string {\n // istanbul ignore if\n if (!SAFE_PATH_SEGMENT.test(segment)) {\n throw new Error(\n `Invalid ${label}: \"${segment}\" must match /^[A-Za-z0-9_$][A-Za-z0-9_$/-]*$/`,\n );\n }\n return segment;\n}\n\nconst getModule = (\n fnName: string,\n fnEntry: string,\n options: Partial<ServerFunctionOptions> & {\n contentType: ServerFunctionOptions[\"contentType\"];\n rpcPreffix: string;\n },\n) => {\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.rpcPreffix, \"rpcPreffix\");\n\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 default: {\n body = `JSON.stringify(args)`;\n headers = `{ 'Content-Type': 'application/json' }`;\n }\n }\n\n const output = `\nexport const ${safeFnEntry} = (...args) => {\n const body = ${body};\n const headers = ${headers};\n const preffix = \"${safePrefix}\";\n const name = \"${safeFnName}\";\n return innerModule(body, headers, preffix, name);\n}`;\n\n return output.trim();\n};\n\nexport const getClientModules = (initialOptions: RpcPluginOptionsInternal) => {\n // Validate prefix once at the top level\n validatePathSegment(initialOptions.rpcPreffix, \"rpcPreffix\");\n\n return `\n// Client-side RPC modules\nimport { innerModule } from \"@thednp/rpc/helpers\";\n${\n 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 }\n`.trim();\n};\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "@thednp/rpc",
3
+ "version": "0.0.1",
4
+ "description": "⚡ A Vite plugin for creating server functions with automatic Remote Procedure Calls (RPC)",
5
+ "homepage": "https://github.com/thednp/rpc#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/thednp/rpc/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/thednp/rpc.git"
12
+ },
13
+ "license": "MIT",
14
+ "author": "thednp",
15
+ "type": "module",
16
+ "types": "./dist/index.d.mts",
17
+ "files": [
18
+ "dist",
19
+ "wiki",
20
+ "README.md",
21
+ "AGENTS.md",
22
+ "LICENSE"
23
+ ],
24
+ "exports": {
25
+ ".": "./dist/index.mjs",
26
+ "./express": "./dist/express/express.mjs",
27
+ "./fastify": "./dist/fastify/fastify.mjs",
28
+ "./fastify/plugin": "./dist/fastify/plugin/fastify/plugin.mjs",
29
+ "./helpers": "./dist/helpers/helpers.mjs",
30
+ "./hono": "./dist/hono/hono.mjs",
31
+ "./koa": "./dist/koa/koa.mjs",
32
+ "./server": "./dist/server/server.mjs",
33
+ "./package.json": "./package.json"
34
+ },
35
+ "dependencies": {
36
+ "vite": "^8.1.5"
37
+ },
38
+ "devDependencies": {
39
+ "@hono/node-server": "^2.0.12",
40
+ "@types/express": "^5.0.6",
41
+ "@types/koa": "^3.0.3",
42
+ "@types/node": "^26.1.2",
43
+ "@vitest/coverage-istanbul": "^4.1.10",
44
+ "@vitest/ui": "^4.1.10",
45
+ "fastify": "^5.10.0",
46
+ "fastify-plugin": "^6.0.0",
47
+ "hono": "^4.12.32",
48
+ "koa": "^3.2.1",
49
+ "picocolors": "^1.1.1",
50
+ "tsdown": "^0.22.14",
51
+ "typescript": "^7.0.2",
52
+ "vite-plugin-strip-comments": "^0.0.10",
53
+ "vitest": "^4.1.10"
54
+ },
55
+ "scripts": {
56
+ "test": "vitest",
57
+ "test-ui": "vitest --ui",
58
+ "test-dev": "node dev-test",
59
+ "test-prod": "node dev-test --mode=preview",
60
+ "format": "deno fmt src tests examples/**/src",
61
+ "clean": "rm -rf examples/**/dist examples/**/node_modules examples/**/package-lock.json examples/**/pnpm-lock.yaml examples/**/bun.lockb examples/**/deno.lock",
62
+ "dev": "cd examples/spa && pnpm up && pnpm dev",
63
+ "dev:koa": "cd examples/koa && pnpm up && pnpm dev",
64
+ "dev:express": "cd examples/express && pnpm up && pnpm dev",
65
+ "dev:fastify": "cd examples/fastify && pnpm up && pnpm dev",
66
+ "dev:hono": "cd examples/hono && pnpm up && pnpm dev",
67
+ "dev:ssr": "cd examples/ssr && pnpm up && pnpm dev",
68
+ "lint": "pnpm lint:ts && pnpm check:ts",
69
+ "lint:ts": "deno lint src",
70
+ "fix:ts": "deno lint src --fix",
71
+ "check:ts": "tsc -noEmit",
72
+ "up:examples": "pnpm up -r --latest --filter \"./examples/*\"",
73
+ "up:examples_1": "for d in examples/*/; do (cd \"$d\" && pnpm up --latest); done",
74
+ "up:root": "pnpm up --latest",
75
+ "upd": "pnpm up:examples && pnpm up:root",
76
+ "build": "tsdown"
77
+ }
78
+ }