@thednp/rpc 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,8 +2,69 @@ import { readdir } from "node:fs/promises";
2
2
  import { join, resolve } from "node:path";
3
3
  import process from "node:process";
4
4
  import { AsyncLocalStorage } from "node:async_hooks";
5
+ //#region src/options.ts
6
+ const defaultServerFnOptions = {
7
+ contentType: "application/json",
8
+ credentials: "same-origin",
9
+ method: "POST"
10
+ };
11
+ const defaultPrefix = "__rpc";
12
+ const defaultRPCOptions = {
13
+ rpcPrefix: defaultPrefix,
14
+ adapter: "express",
15
+ serverFiles: "exact",
16
+ scanRoot: void 0
17
+ };
18
+ const defaultMiddlewareOptions = {
19
+ rpcPrefix: void 0,
20
+ path: void 0,
21
+ origin: void 0
22
+ };
23
+ //#endregion
5
24
  //#region src/functionsMap.ts
6
- const serverFunctionsMap = /* @__PURE__ */ new Map();
25
+ /**
26
+ * Global symbol under which the shared `serverFunctionsByPrefix` map is stored
27
+ * on `globalThis`. Keeping it on a `Symbol.for` key makes it instance-stable
28
+ * across the bundled entry copies (`index.mjs`, `server.mjs`, `express.mjs`,
29
+ * ...) and dev-server hot reloads, exactly like the request-context storage in
30
+ * `context.ts`. Without this, `scanForServerFiles` (bundled into the plugin)
31
+ * would populate a map copy the adapter middleware could not read.
32
+ */
33
+ const functionsMapSymbol = Symbol.for("thednp.rpc.functionsMap");
34
+ /**
35
+ * Map of rpcPrefix -> Map of function names -> ServerFnEntry
36
+ * Enables multiple RPC instances with different prefixes to coexist
37
+ * without name collisions.
38
+ */
39
+ const serverFunctionsByPrefix = globalThis[functionsMapSymbol] ??= /* @__PURE__ */ new Map();
40
+ /**
41
+ * Gets or creates the function map for a specific prefix.
42
+ * @param prefix - The RPC prefix (e.g., "__rpc", "v1:rpc", "admin:rpc")
43
+ * @returns Map of function names to ServerFnEntry for that prefix
44
+ */
45
+ const getFunctionsForPrefix = (prefix) => {
46
+ if (!serverFunctionsByPrefix.has(prefix)) serverFunctionsByPrefix.set(prefix, /* @__PURE__ */ new Map());
47
+ return serverFunctionsByPrefix.get(prefix);
48
+ };
49
+ /**
50
+ * Backward compatibility: default map for the default prefix.
51
+ * Legacy code can still use serverFunctionsMap.set(name, entry).
52
+ */
53
+ const serverFunctionsMap = {
54
+ get: (key) => getFunctionsForPrefix(defaultPrefix).get(key),
55
+ set: (key, value) => getFunctionsForPrefix(defaultPrefix).set(key, value),
56
+ has: (key) => getFunctionsForPrefix(defaultPrefix).has(key),
57
+ delete: (key) => getFunctionsForPrefix(defaultPrefix).delete(key),
58
+ clear: () => getFunctionsForPrefix(defaultPrefix).clear(),
59
+ get size() {
60
+ return getFunctionsForPrefix(defaultPrefix).size;
61
+ },
62
+ entries: () => getFunctionsForPrefix(defaultPrefix).entries(),
63
+ keys: () => getFunctionsForPrefix(defaultPrefix).keys(),
64
+ values: () => getFunctionsForPrefix(defaultPrefix).values(),
65
+ forEach: (callback) => getFunctionsForPrefix(defaultPrefix).forEach(callback),
66
+ [Symbol.iterator]: () => getFunctionsForPrefix(defaultPrefix)[Symbol.iterator]()
67
+ };
7
68
  //#endregion
8
69
  //#region src/constants.ts
9
70
  const OPERATION_ABORTED = "Operation aborted";
@@ -13,7 +74,7 @@ const INTERNAL_SERVER_ERROR = "Internal Server Error";
13
74
  /** Error message when a value fails the safe-identifier validation. @param label - What kind of value was being validated. @param name - The rejected value */
14
75
  const INVALID_IDENTIFIER = (label, name) => `Invalid ${label}: "${name}" must match /^[A-Za-z_$][A-Za-z0-9_$]*$/`;
15
76
  /** 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 */
16
- const INVALID_PATH_SEGMENT = (label, segment) => `Invalid ${label}: "${segment}" must match /^[A-Za-z0-9_$][A-Za-z0-9_$/-]*$/`;
77
+ const INVALID_PATH_SEGMENT = (label, segment) => `Invalid ${label}: "${segment}" must match /^[A-Za-z0-9_$@:][A-Za-z0-9_$@:/-]*$/`;
17
78
  /** Error template for duplicate server function names across files. @param name - The duplicate registered name */
18
79
  const DUPLICATE_FUNCTION_NAME = (name) => `Duplicate server function "${name}" detected. Each server function must have a unique name. Remove or rename the duplicate.`;
19
80
  //#endregion
@@ -112,6 +173,34 @@ const hasContentTypeMismatch = (declared, rawHeader) => {
112
173
  function escapeRegExp(s) {
113
174
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
114
175
  }
176
+ const SAFE_URL_BASE = "http://localhost";
177
+ /**
178
+ * Parses a raw request URL against a fixed base without ever throwing.
179
+ * Malformed request-targets (e.g. `/\`, `//`, `/\/`) make the WHATWG URL
180
+ * parser throw `TypeError: Invalid URL`; the adapters call this while
181
+ * building the per-request URL **before** their dispatch `try` block, so an
182
+ * unhandled rejection there crashes raw `node:http` hosts (and Express 4).
183
+ * On failure we fall back to the base root: the resulting pathname never
184
+ * matches the RPC prefix, so the request is treated as non-RPC and falls
185
+ * through to `next()` / 404 instead of crashing the process.
186
+ * @param rawUrl - Raw request URL (path + optional query string)
187
+ * @param base - Optional base URL, defaults to a fixed localhost origin
188
+ * @returns A URL object; never throws
189
+ */
190
+ const safeURL = (rawUrl, base = SAFE_URL_BASE) => {
191
+ try {
192
+ return new URL(rawUrl, base);
193
+ } catch {
194
+ return new URL("/", base);
195
+ }
196
+ };
197
+ const globalPrefixSymbol = Symbol.for("thednp.rpc.globalPrefix");
198
+ /** Global rpcPrefix from the last loaded config / middleware — fallback for functions without explicit prefix. */
199
+ const getGlobalPrefix = () => globalThis[globalPrefixSymbol];
200
+ const setGlobalPrefix = (prefix) => {
201
+ if (prefix) globalThis[globalPrefixSymbol] = prefix;
202
+ else delete globalThis[globalPrefixSymbol];
203
+ };
115
204
  //#endregion
116
205
  //#region src/scanForServerFiles.ts
117
206
  let isScanned = false;
@@ -125,7 +214,7 @@ const EXACT_NAMES = [
125
214
  ];
126
215
  /**
127
216
  * Scans `src/api/` (or an explicit `scanRoot`) for server function files
128
- * and populates the global `serverFunctionsMap` with their exported functions.
217
+ * and populates the server functions map (scoped by rpcPrefix) with their exported functions.
129
218
  * Uses Vite's SSR module loading to resolve and execute each file.
130
219
  *
131
220
  * Supports two matching modes via `config.serverFiles`:
@@ -183,16 +272,21 @@ const scanForServerFiles = async (initialCfg, devServer) => {
183
272
  }
184
273
  for (const [exportName, exportValue] of moduleEntries) {
185
274
  const registeredName = exportValue.name;
186
- if (seenNames.has(registeredName)) {
275
+ const prefix = exportValue.options?.rpcPrefix || config.rpcPrefix || "__rpc";
276
+ const seenKey = `${prefix}:${registeredName}`;
277
+ if (seenNames.has(seenKey)) {
187
278
  if (process.env.NODE_ENV !== "production") throw new Error(DUPLICATE_FUNCTION_NAME(registeredName));
188
279
  console.warn(DUPLICATE_FUNCTION_NAME(registeredName));
189
280
  continue;
190
281
  }
191
- seenNames.add(registeredName);
192
- serverFunctionsMap.set(registeredName, {
282
+ seenNames.add(seenKey);
283
+ const prefixMap = getFunctionsForPrefix(prefix);
284
+ const existing = prefixMap.get(registeredName);
285
+ if (existing) existing.exportName = exportName;
286
+ else prefixMap.set(registeredName, {
193
287
  name: registeredName,
194
288
  handler: exportValue,
195
- options: exportValue?.options,
289
+ options: exportValue.options,
196
290
  exportName
197
291
  });
198
292
  }
@@ -203,36 +297,20 @@ const scanForServerFiles = async (initialCfg, devServer) => {
203
297
  }
204
298
  };
205
299
  //#endregion
206
- //#region src/options.ts
207
- const defaultServerFnOptions = {
208
- contentType: "application/json",
209
- credentials: "same-origin",
210
- method: "POST"
211
- };
212
- const defaultRPCOptions = {
213
- rpcPrefix: "__rpc",
214
- adapter: "express",
215
- serverFiles: "exact",
216
- scanRoot: void 0
217
- };
218
- const defaultMiddlewareOptions = {
219
- rpcPrefix: void 0,
220
- path: void 0,
221
- origin: void 0
222
- };
223
- //#endregion
224
300
  //#region src/createFunction.ts
225
301
  /**
226
302
  * Creates a server-side RPC function.
227
- * Registers the function in the server functions map and returns a client-compatible
228
- * wrapper that exposes `data` (Promise) and `cancel` (function) for request lifecycle control.
303
+ * Registers the function in the server functions map (scoped by rpcPrefix) and returns
304
+ * a client-compatible wrapper that exposes `data` (Promise) and `cancel` (function)
305
+ * for request lifecycle control.
229
306
  * @param name - Unique identifier used by the RPC router to dispatch requests
230
307
  * @param handler - The actual implementation receiving an AbortSignal followed by JSON-serializable arguments
231
- * @param fnOptions - Optional contentType and credentials settings
308
+ * @param fnOptions - Optional contentType, credentials, and rpcPrefix settings
232
309
  * @returns A client stub with `data` promise and `cancel` method, auto-registered in the server map
233
310
  */
234
311
  function createServerFunction(name, handler, fnOptions = {}) {
235
312
  const options = Object.assign({}, defaultServerFnOptions, fnOptions);
313
+ const rpcPrefix = fnOptions.rpcPrefix || getGlobalPrefix() || "__rpc";
236
314
  const wrappedFunction = (...args) => {
237
315
  const controller = new AbortController();
238
316
  const cancel = (reason) => controller.abort(reason);
@@ -257,7 +335,7 @@ function createServerFunction(name, handler, fnOptions = {}) {
257
335
  configurable: false
258
336
  }
259
337
  });
260
- serverFunctionsMap.set(name, {
338
+ getFunctionsForPrefix(rpcPrefix).set(name, {
261
339
  name,
262
340
  handler: wrappedFunction,
263
341
  options
@@ -267,7 +345,7 @@ function createServerFunction(name, handler, fnOptions = {}) {
267
345
  //#endregion
268
346
  //#region src/validate.ts
269
347
  const SAFE_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
270
- const SAFE_PATH_SEGMENT = /^[A-Za-z0-9_$@][A-Za-z0-9_$@/-]*$/;
348
+ const SAFE_PATH_SEGMENT = /^[A-Za-z0-9_$@:][A-Za-z0-9_$@:/-]*$/;
271
349
  const CREDENTIALS_VALUES = [
272
350
  "same-origin",
273
351
  "include",
@@ -287,7 +365,8 @@ function validateIdentifier(name, label) {
287
365
  }
288
366
  /**
289
367
  * Validates that a string is a safe path segment for RPC routing.
290
- * Allows alphanumeric characters, underscores, dollar signs, at signs, hyphens, and forward slashes.
368
+ * Allows alphanumeric characters, underscores, dollar signs, at signs,
369
+ * colons, hyphens, and forward slashes.
291
370
  * @param segment - The string to validate
292
371
  * @param label - Human-readable label for error messages (e.g. "rpcPrefix")
293
372
  * @returns The validated segment if it passes
@@ -337,60 +416,35 @@ const getModule = (fnName, fnEntry, options) => {
337
416
  const safePrefix = validatePathSegment(options.rpcPrefix, "rpcPrefix");
338
417
  const credentials = validateCredentials(options.credentials);
339
418
  const method = validateMethod(options.method);
340
- let body = "";
341
- let headers = "{}";
342
- switch (options.contentType) {
343
- case "text/plain":
344
- body = `args[0]`;
345
- headers = `{ 'Content-Type': 'text/plain' }`;
346
- break;
347
- case "application/x-www-form-urlencoded":
348
- body = `new URLSearchParams(args[0]).toString()`;
349
- headers = `{ 'Content-Type': 'application/x-www-form-urlencoded' }`;
350
- break;
351
- case "multipart/form-data":
352
- body = `args[0]`;
353
- headers = `{}`;
354
- break;
355
- default:
356
- body = `JSON.stringify(args)`;
357
- headers = `{ 'Content-Type': 'application/json' }`;
358
- }
359
- if (method === "GET") {
360
- body = `JSON.stringify(args)`;
361
- headers = `{}`;
362
- }
419
+ const contentType = options.contentType ?? "application/json";
420
+ const opts = [];
421
+ if (method !== "POST") opts.push(`method: "${method}"`);
422
+ if (credentials !== "same-origin") opts.push(`credentials: "${credentials}"`);
423
+ if (contentType !== "application/json") opts.push(`contentType: "${contentType}"`);
363
424
  return `
364
- export const ${safeFnEntry} = (...args) => {
365
- const body = ${body};
366
- const headers = ${headers};
367
- const prefix = "${safePrefix}";
368
- const name = "${safeFnName}";
369
- const credentials = "${credentials}";
370
- const method = "${method}";
371
- return innerModule(body, headers, credentials, prefix, name, method);
372
- }`.trim();
425
+ export const ${safeFnEntry} = getClientStub("${safePrefix}", "${safeFnName}"${opts.length ? `, { ${opts.join(", ")} }` : ""});`.trim();
373
426
  };
374
427
  /**
375
428
  * Generates the complete client-side module bundle by iterating all registered server functions
376
- * and producing fetch-based stubs for each. The result is transformed by Vite (or Oxc) during
377
- * the dev server or production build.
429
+ * for a specific prefix and producing fetch-based stubs for each. The result is transformed by Vite
430
+ * (or Oxc) during the dev server or production build.
378
431
  * @param initialOptions - Plugin options containing rpcPrefix and optional adapter
379
432
  * @returns A string of JavaScript code with all client RPC modules and their import dependencies
380
433
  */
381
434
  const getClientModules = (initialOptions) => {
382
435
  validatePathSegment(initialOptions.rpcPrefix, "rpcPrefix");
436
+ const prefixMap = getFunctionsForPrefix(initialOptions.rpcPrefix);
383
437
  return `
384
438
 
385
- import { innerModule } from "@thednp/rpc/helpers";
386
- ${Array.from(serverFunctionsMap.entries()).filter(([, entry]) => entry.exportName).map(([registeredName, entry]) => getModule(registeredName, entry.exportName, {
439
+ import { getClientStub } from "@thednp/rpc/helpers";
440
+ ${Array.from(prefixMap.entries()).filter(([, entry]) => entry.exportName).map(([registeredName, entry]) => getModule(registeredName, entry.exportName, {
387
441
  ...initialOptions,
388
442
  ...entry.options || {}
389
443
  })).join("\n")}`.trim();
390
444
  };
391
445
  //#endregion
392
446
  //#region src/context.ts
393
- /** @module Server-side request context. Exports the `RequestEvent` shape, `provideRequestContext` to establish it around a dispatch, `getRequestContext` to read it from anywhere inside the async tree, and `redirect` for framework-level redirects. Never import this module in client code — it is server-only. */
447
+ /** @module Server-side request context. Exports the `RequestEvent` shape, `provideRequestContext` to establish it around a dispatch, `getRequestContext` to read it from anywhere inside the async tree, `redirect` and `sendResponse` for framework-level short-circuits, and `getRequestMeta` for normalized request access. Never import this module in client code — it is server-only. */
394
448
  /**
395
449
  * Global symbol under which the shared `AsyncLocalStorage` instance is stored
396
450
  * on `globalThis`. Keeping it on a `Symbol.for` key makes it instance-stable
@@ -428,7 +482,64 @@ const getRequestContext = () => {
428
482
  const redirect = (location, status = 303) => {
429
483
  getRequestContext().redirect(location, status);
430
484
  };
485
+ /**
486
+ * Sends a raw JSON response for the current request, bypassing the standard
487
+ * `{ data }` shape. Reads the adapter-bound `send` from the current request
488
+ * context — callable from anywhere inside a server-function tree. Any code in
489
+ * the async tree of a dispatch can call this (e.g. custom middleware) to
490
+ * short-circuit with a specific status code (401, 413, 429, ...).
491
+ * @param status - HTTP status code
492
+ * @param body - JSON-serializable response body
493
+ * @param headers - Optional response headers
494
+ * @throws When called outside of a request
495
+ */
496
+ const sendResponse = (status, body, headers) => {
497
+ getRequestContext().send(status, body, headers);
498
+ };
499
+ const pickHeader = (headers, name) => {
500
+ const value = headers[name];
501
+ if (typeof value === "string") return value;
502
+ if (Array.isArray(value)) return value[0];
503
+ };
504
+ /** Normalizes any headers shape into a plain lower-cased record. */
505
+ const toHeaderRecord = (headers) => {
506
+ if (!headers) return {};
507
+ if (typeof headers.forEach === "function") {
508
+ const record = {};
509
+ headers.forEach((value, key) => {
510
+ record[key] = value;
511
+ });
512
+ return record;
513
+ }
514
+ return headers;
515
+ };
516
+ /**
517
+ * Reads normalized, adapter-agnostic request metadata from the current request
518
+ * context. Works with Express `req`, Fastify `req`, Koa `ctx.req`,
519
+ * Hono `c.req` and h3 `event.req` by feature-detecting the request shape
520
+ * (`originalUrl`/`url`/`path`, raw `headers` map vs `Headers`-like API).
521
+ * @param event - The request context to read, typically the result of
522
+ * {@link getRequestContext}
523
+ */
524
+ const getRequestMeta = (event) => {
525
+ const req = event.request;
526
+ const method = (req?.method ?? "GET").toUpperCase();
527
+ const rawUrl = req?.originalUrl ?? req?.url ?? req?.path ?? "";
528
+ const url = safeURL(rawUrl);
529
+ const headers = toHeaderRecord(req?.headers);
530
+ const hostHeader = pickHeader(headers, "host");
531
+ return {
532
+ method,
533
+ pathname: url.pathname,
534
+ search: url.search,
535
+ searchParams: url.searchParams,
536
+ headers,
537
+ host: hostHeader,
538
+ ip: req?.ip ?? req?.socket?.remoteAddress,
539
+ protocol: req?.protocol ?? url.protocol.replace(":", "")
540
+ };
541
+ };
431
542
  //#endregion
432
- export { RPCError, createServerFunction, defaultMiddlewareOptions, defaultRPCOptions, defaultServerFnOptions, escapeRegExp, formatError, getClientModules, getRequestContext, hasContentTypeMismatch, isFormContentType, provideRequestContext, redirect, scanForServerFiles, scannedServerFiles, serverFunctionsMap, walkGlobFiles };
543
+ export { RPCError, createServerFunction, defaultMiddlewareOptions, defaultPrefix, defaultRPCOptions, defaultServerFnOptions, escapeRegExp, formatError, getClientModules, getFunctionsForPrefix, getGlobalPrefix, getRequestContext, getRequestMeta, hasContentTypeMismatch, isFormContentType, provideRequestContext, redirect, safeURL, scanForServerFiles, scannedServerFiles, sendResponse, serverFunctionsByPrefix, serverFunctionsMap, setGlobalPrefix, walkGlobFiles };
433
544
 
434
545
  //# sourceMappingURL=server.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.mjs","names":[],"sources":["../../src/functionsMap.ts","../../src/constants.ts","../../src/server-helpers.ts","../../src/scanForServerFiles.ts","../../src/options.ts","../../src/createFunction.ts","../../src/validate.ts","../../src/getClientModules.ts","../../src/context.ts"],"sourcesContent":["import type { ServerFnEntry } from \"./types.d.ts\";\n\nexport const serverFunctionsMap: Map<string, ServerFnEntry> = new Map<\n string,\n ServerFnEntry\n>();\n","export const OPERATION_ABORTED = \"Operation aborted\";\n\nexport const REQUEST_CANCELLED = \"Request was cancelled\";\n\nexport const FETCH_ERROR_PREFIX = \"Fetch error: \";\n\nexport const NO_SERVER_FUNCTION_FOUND = \"No server function found.\";\n\nexport const ERROR_LOADING_FILE = \"Error loading file:\";\n\nexport const FUNCTION_NOT_FOUND = \"Function not found\";\n\nexport const METHOD_NOT_ALLOWED = \"Method Not Allowed\";\n\nexport const REQUEST_FORBIDDEN = \"Forbidden\";\n\nexport const UNSUPPORTED_MEDIA_TYPE = \"Unsupported Media Type\";\n\nexport const 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, `isFormContentType` and `hasContentTypeMismatch` for content-type validation, and `walkGlobFiles` for recursively discovering `*.server.*` files. Never import this module in client code — it is server-only. */\nimport type { ContentType, JsonObject, JsonValue } from \"./types.d.ts\";\nimport { readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport { INTERNAL_SERVER_ERROR } from \"./constants.ts\";\n\nconst GLOB_REGEX = /^.+\\.server\\.(ts|js|mjs|mts)$/;\n\n/**\n * Recursively walks `dir` and collects absolute paths to files whose\n * basename matches the `*.server.{ts,js,mjs,mts}` glob pattern.\n */\nexport const walkGlobFiles = async (dir: string): Promise<string[]> => {\n const results: string[] = [];\n const stack = [dir];\n while (stack.length) {\n const current = stack.pop()!;\n let entries;\n try {\n entries = await readdir(current, { withFileTypes: true });\n } catch (_e) {\n continue;\n }\n for (const entry of entries) {\n const fullPath = join(current, entry.name);\n if (entry.isFile() && GLOB_REGEX.test(entry.name)) {\n results.push(fullPath);\n } else if (entry.isDirectory()) {\n stack.push(fullPath);\n }\n }\n }\n return results;\n};\n\n/**\n * A typed error thrown from server functions.\n * The middleware serializes the `message` and `code` in the response,\n * allowing clients to recognise and handle specific error conditions.\n */\nexport class RPCError extends Error {\n /** Machine-readable error code (e.g. \"VALIDATION_FAILED\", \"UNAUTHORIZED\") */\n code: string;\n /** Optional diagnostic payload */\n data?: JsonValue;\n constructor(message: string, code = \"INTERNAL\", data?: JsonValue) {\n super(message);\n this.name = \"RPCError\";\n this.code = code;\n this.data = data;\n }\n}\n\n/**\n * Formats an error for the RPC middleware response.\n * In development the full `RPCError` payload is included so developers\n * can quickly identify issues. Unexpected exceptions never expose their\n * message — only the generic \"Internal Server Error\" is sent, preventing\n * information disclosure; server-side diagnostics are preserved via the\n * middleware's `console.error` logging.\n */\nexport const formatError = (\n err: unknown,\n isProduction: boolean,\n): JsonObject => {\n if (isProduction) {\n return { error: INTERNAL_SERVER_ERROR };\n }\n if (err instanceof RPCError) {\n const payload: JsonObject = {\n error: err.message || INTERNAL_SERVER_ERROR,\n code: err.code,\n };\n if (err.data !== undefined) payload.data = err.data;\n return payload;\n }\n return { error: INTERNAL_SERVER_ERROR };\n};\n\n/**\n * Checks whether a content type maps to a form encoding\n * (`multipart/form-data` or `application/x-www-form-urlencoded`).\n * Form-declared functions accept either encoding so native browser\n * submissions (urlencoded) keep working without JavaScript.\n */\nexport const isFormContentType = (contentType: string): boolean =>\n contentType === \"multipart/form-data\" ||\n contentType === \"application/x-www-form-urlencoded\";\n\n/**\n * Detects whether an incoming request's `Content-Type` header conflicts\n * with the function's declared content type. JSON and text functions are\n * enforced strictly (exact match wins), while form functions accept both\n * form encodings because the nojs fallback submits urlencoded forms to\n * multipart-declared endpoints. Requests without a `Content-Type` header\n * (curl, GET, legacy clients) are exempt from enforcement.\n * @param declared - The declared `contentType` from the server function options\n * @param rawHeader - The raw `Content-Type` request header, if present\n */\nexport const hasContentTypeMismatch = (\n declared: ContentType,\n rawHeader: string | undefined,\n): boolean => {\n // No Content-Type header → exempt (url bar, GET, curl compatibility)\n if (!rawHeader) return false;\n // Strip parameters (charset, boundary) before comparison\n const incomingType = rawHeader.trim().toLowerCase().split(\";\")[0].trim();\n if (isFormContentType(declared)) {\n // Forms: reject only non-form encodings (lenient between the two)\n return !isFormContentType(incomingType);\n }\n return incomingType !== declared;\n};\n\n/**\n * Escapes special regex metacharacters in a string.\n * Used to safely embed user-configurable values (like rpcPrefix) into regular expressions,\n * preventing ReDoS and regex injection attacks.\n * @param s - The raw string to escape\n * @returns The escaped string safe for use in new RegExp()\n */\nexport function escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n","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","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","// /@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\";\nimport { OPERATION_ABORTED } from \"./constants.ts\";\n\n/**\n * Creates a server-side RPC function.\n * Registers the function in the server functions map and returns a client-compatible\n * wrapper that exposes `data` (Promise) and `cancel` (function) for request lifecycle control.\n * @param name - Unique identifier used by the RPC router to dispatch requests\n * @param handler - The actual implementation receiving an AbortSignal followed by JSON-serializable arguments\n * @param fnOptions - Optional contentType and credentials settings\n * @returns A client stub with `data` promise and `cancel` method, auto-registered in the server map\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): ClientFunction<TArgs, TResult> {\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: false },\n options: { value: options, enumerable: true, configurable: false },\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","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 \"application/x-www-form-urlencoded\":\n {\n body = `new URLSearchParams(args[0]).toString()`;\n headers = `{ 'Content-Type': 'application/x-www-form-urlencoded' }`;\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 request context. Exports the `RequestEvent` shape, `provideRequestContext` to establish it around a dispatch, `getRequestContext` to read it from anywhere inside the async tree, and `redirect` for framework-level redirects. Never import this module in client code — it is server-only. */\n\n// @thednp/rpc/src/context.ts\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\n/**\n * Global symbol under which the shared `AsyncLocalStorage` instance is stored\n * on `globalThis`. Keeping it on a `Symbol.for` key makes it instance-stable\n * across module copies and dev-server hot reloads, mirroring\n * `solid-js/web`'s own request-context storage.\n */\nconst requestContextSymbol = Symbol.for(\"thednp.rpc.requestContext\");\n\n/**\n * A per-request context established by the framework adapters around\n * server-function dispatch, mirroring Solid Start's `FetchEvent`. Any code\n * running in the async tree of a dispatch can read the current context through\n * {@link getRequestContext} instead of threading `req`/`res` (or the framework\n * `Context` object) through every nested call. This module is server-only and\n * must never be imported by client code.\n *\n * Each adapter extends this with framework-specific request/response accessors:\n * - Express: `req`/`res` plus `nativeEvent = { req, res }`\n * - Fastify: `request`/`reply` plus `nativeEvent = request`\n * - Koa: `ctx` plus `nativeEvent = ctx`\n * - Hono: `c` (the Hono `Context`) plus `nativeEvent = c`\n * - h3: `event` (the h3 `H3Event`) plus `nativeEvent = event`\n */\nexport interface RequestEvent {\n /** Adapter-specific native event kept for deep framework access */\n nativeEvent?: unknown;\n /** Adapter request object */\n request: unknown;\n /** Adapter response object */\n response: unknown;\n /**\n * Bound adapter-native redirect. Performing a redirect sets `redirected`\n * so the middleware can skip the JSON `{ data }` send.\n * @param location - The URL to redirect to\n * @param status - HTTP status code, defaults to `303 See Other`\n */\n redirect: (location: string, status?: number) => void;\n /**\n * Set by `redirect` once a redirect has been issued. The middleware checks\n * this after `await`ing the server function to avoid double-responding.\n */\n redirected?: { location: string; status: number };\n /** Per-request app data shared across the async tree of the dispatch */\n locals: Record<string, unknown>;\n [prop: string]: unknown;\n}\n\n// Instance-stable across module copies and dev-server hot reloads, exactly like\n// `solid-js/web`'s `provideRequestEvent` (which stores on a globalThis symbol).\nconst requestContextStorage: AsyncLocalStorage<RequestEvent> =\n ((globalThis as Record<symbol, AsyncLocalStorage<RequestEvent>>)[\n requestContextSymbol\n ] ??= new AsyncLocalStorage<RequestEvent>());\n\n/**\n * Runs `cb` with `init` as the current request context. Use inside the\n * adapters around server-function dispatch (the async tree under `cb` can then\n * read the context via {@link getRequestContext}).\n * @param init - The request context for the duration of `cb`\n * @param cb - The work that needs access to the request context\n */\nexport const provideRequestContext = <T>(\n init: RequestEvent,\n cb: () => T,\n): T => requestContextStorage.run(init, cb);\n\n/**\n * Returns the current request context, or throws when called outside of a\n * request (e.g. module scope or a background task).\n * @throws When no request context is established\n */\nexport const getRequestContext = (): RequestEvent => {\n const ctx = requestContextStorage.getStore();\n if (!ctx) {\n throw new Error(\"RequestEvent is not available outside of a request\");\n }\n return ctx;\n};\n\n/**\n * Redirects the current request to `location`. Reads the adapter-bound\n * `redirect` from the current request context — callable from anywhere inside\n * a server-function tree (no `res` threading needed).\n * @param location - The URL to redirect to\n * @param status - HTTP status code, defaults to `303 See Other`\n * @throws When called outside of a request\n */\nexport const redirect = (location: string, status = 303): void => {\n getRequestContext().redirect(location, status);\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"server.mjs","names":[],"sources":["../../src/options.ts","../../src/functionsMap.ts","../../src/constants.ts","../../src/server-helpers.ts","../../src/scanForServerFiles.ts","../../src/createFunction.ts","../../src/validate.ts","../../src/getClientModules.ts","../../src/context.ts"],"sourcesContent":["import type {\n MiddlewareOptions,\n RpcPluginOptions,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\n\nexport const defaultServerFnOptions: ServerFunctionOptions = {\n contentType: \"application/json\",\n credentials: \"same-origin\",\n method: \"POST\",\n};\n\nexport const defaultPrefix = \"__rpc\";\n\nexport const defaultRPCOptions: RpcPluginOptions = {\n rpcPrefix: defaultPrefix,\n adapter: \"express\",\n serverFiles: \"exact\",\n scanRoot: undefined,\n};\n\nexport const defaultMiddlewareOptions: MiddlewareOptions = {\n rpcPrefix: undefined,\n path: undefined,\n origin: undefined,\n};\n","import type { ServerFnEntry } from \"./types.d.ts\";\nimport { defaultPrefix } from \"./options.ts\";\n\n/**\n * Global symbol under which the shared `serverFunctionsByPrefix` map is stored\n * on `globalThis`. Keeping it on a `Symbol.for` key makes it instance-stable\n * across the bundled entry copies (`index.mjs`, `server.mjs`, `express.mjs`,\n * ...) and dev-server hot reloads, exactly like the request-context storage in\n * `context.ts`. Without this, `scanForServerFiles` (bundled into the plugin)\n * would populate a map copy the adapter middleware could not read.\n */\nconst functionsMapSymbol = Symbol.for(\"thednp.rpc.functionsMap\");\n\n/**\n * Map of rpcPrefix -> Map of function names -> ServerFnEntry\n * Enables multiple RPC instances with different prefixes to coexist\n * without name collisions.\n */\nexport const serverFunctionsByPrefix: Map<\n string,\n Map<string, ServerFnEntry>\n> = ((globalThis as Record<symbol, Map<string, Map<string, ServerFnEntry>>>)[\n functionsMapSymbol\n] ??= new Map());\n\n/**\n * Gets or creates the function map for a specific prefix.\n * @param prefix - The RPC prefix (e.g., \"__rpc\", \"v1:rpc\", \"admin:rpc\")\n * @returns Map of function names to ServerFnEntry for that prefix\n */\nexport const getFunctionsForPrefix = (\n prefix: string,\n): Map<string, ServerFnEntry> => {\n if (!serverFunctionsByPrefix.has(prefix)) {\n serverFunctionsByPrefix.set(prefix, new Map());\n }\n return serverFunctionsByPrefix.get(prefix)!;\n};\n\n/**\n * Backward compatibility: default map for the default prefix.\n * Legacy code can still use serverFunctionsMap.set(name, entry).\n */\nexport const serverFunctionsMap: Map<string, ServerFnEntry> = {\n get: (key: string) => getFunctionsForPrefix(defaultPrefix).get(key),\n set: (key: string, value: ServerFnEntry) =>\n getFunctionsForPrefix(defaultPrefix).set(key, value),\n has: (key: string) => getFunctionsForPrefix(defaultPrefix).has(key),\n delete: (key: string) => getFunctionsForPrefix(defaultPrefix).delete(key),\n clear: () => getFunctionsForPrefix(defaultPrefix).clear(),\n get size() {\n return getFunctionsForPrefix(defaultPrefix).size;\n },\n entries: () => getFunctionsForPrefix(defaultPrefix).entries(),\n keys: () => getFunctionsForPrefix(defaultPrefix).keys(),\n values: () => getFunctionsForPrefix(defaultPrefix).values(),\n forEach: (\n callback: (\n value: ServerFnEntry,\n key: string,\n map: Map<string, ServerFnEntry>,\n ) => void,\n ) => getFunctionsForPrefix(defaultPrefix).forEach(callback),\n [Symbol.iterator]: () =>\n getFunctionsForPrefix(defaultPrefix)[Symbol.iterator](),\n} as unknown as Map<string, ServerFnEntry>;\n","export const OPERATION_ABORTED = \"Operation aborted\";\n\nexport const REQUEST_CANCELLED = \"Request was cancelled\";\n\nexport const FETCH_ERROR_PREFIX = \"Fetch error: \";\n\nexport const NO_SERVER_FUNCTION_FOUND = \"No server function found.\";\n\nexport const ERROR_LOADING_FILE = \"Error loading file:\";\n\nexport const FUNCTION_NOT_FOUND = \"Function not found\";\n\nexport const METHOD_NOT_ALLOWED = \"Method Not Allowed\";\n\nexport const REQUEST_FORBIDDEN = \"Forbidden\";\n\nexport const UNSUPPORTED_MEDIA_TYPE = \"Unsupported Media Type\";\n\nexport const BAD_REQUEST = \"Bad Request\";\n\nexport const INTERNAL_SERVER_ERROR = \"Internal Server Error\";\n\nexport const CLIENT_DISCONNECTED = \"client disconnected\";\n\n/** Returns a warning when a middleware name is reused, preventing registration conflicts. @param name - The duplicate middleware name */\nexport const MIDDLEWARE_NAME_USED = (name: string) =>\n `The middleware name \"${name}\" is already used.`;\n\n/** Error message when a value fails the safe-identifier validation. @param label - What kind of value was being validated. @param name - The rejected value */\nexport const INVALID_IDENTIFIER = (label: string, name: string) =>\n `Invalid ${label}: \"${name}\" must match /^[A-Za-z_$][A-Za-z0-9_$]*$/`;\n\n/** Error message when a value fails the safe-path-segment validation. @param label - What kind of value was being validated. @param segment - The rejected value */\nexport const INVALID_PATH_SEGMENT = (label: string, segment: string) =>\n `Invalid ${label}: \"${segment}\" must match /^[A-Za-z0-9_$@:][A-Za-z0-9_$@:/-]*$/`;\n\n/** Warning message when a specified RPC config file cannot be resolved on disk. @param configFile - The requested config filename. @param configFilePath - The resolved absolute path */\nexport const CONFIG_FILE_NOT_FOUND = (\n configFile: string,\n configFilePath: string,\n) =>\n ` ⚠︎ The specified RPC config file ${configFile} cannot be found at ${configFilePath}, loading the defaults..`;\n\nexport const NO_CONFIG_FOUND = ` ⚡︎ No RPC config found, loading the defaults..`;\n\nexport const FAILED_LOAD_CONFIG = ` ⚠︎ Failed to load RPC config:`;\n\n/** Error template for duplicate server function names across files. @param name - The duplicate registered name */\nexport const DUPLICATE_FUNCTION_NAME = (name: string) =>\n `Duplicate server function \"${name}\" detected. Each server function must have a unique name. Remove or rename the duplicate.`;\n","/** @module Server-side utilities. Exports the `RPCError` class for typed server-side errors, `formatError` for middleware error responses, `isFormContentType` and `hasContentTypeMismatch` for content-type validation, and `walkGlobFiles` for recursively discovering `*.server.*` files. Never import this module in client code — it is server-only. */\nimport type { ContentType, JsonObject, JsonValue } from \"./types.d.ts\";\nimport { readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport { INTERNAL_SERVER_ERROR } from \"./constants.ts\";\n\nconst GLOB_REGEX = /^.+\\.server\\.(ts|js|mjs|mts)$/;\n\n/**\n * Recursively walks `dir` and collects absolute paths to files whose\n * basename matches the `*.server.{ts,js,mjs,mts}` glob pattern.\n */\nexport const walkGlobFiles = async (dir: string): Promise<string[]> => {\n const results: string[] = [];\n const stack = [dir];\n while (stack.length) {\n const current = stack.pop()!;\n let entries;\n try {\n entries = await readdir(current, { withFileTypes: true });\n } catch (_e) {\n continue;\n }\n for (const entry of entries) {\n const fullPath = join(current, entry.name);\n if (entry.isFile() && GLOB_REGEX.test(entry.name)) {\n results.push(fullPath);\n } else if (entry.isDirectory()) {\n stack.push(fullPath);\n }\n }\n }\n return results;\n};\n\n/**\n * A typed error thrown from server functions.\n * The middleware serializes the `message` and `code` in the response,\n * allowing clients to recognise and handle specific error conditions.\n */\nexport class RPCError extends Error {\n /** Machine-readable error code (e.g. \"VALIDATION_FAILED\", \"UNAUTHORIZED\") */\n code: string;\n /** Optional diagnostic payload */\n data?: JsonValue;\n constructor(message: string, code = \"INTERNAL\", data?: JsonValue) {\n super(message);\n this.name = \"RPCError\";\n this.code = code;\n this.data = data;\n }\n}\n\n/**\n * Formats an error for the RPC middleware response.\n * In development the full `RPCError` payload is included so developers\n * can quickly identify issues. Unexpected exceptions never expose their\n * message — only the generic \"Internal Server Error\" is sent, preventing\n * information disclosure; server-side diagnostics are preserved via the\n * middleware's `console.error` logging.\n */\nexport const formatError = (\n err: unknown,\n isProduction: boolean,\n): JsonObject => {\n if (isProduction) {\n return { error: INTERNAL_SERVER_ERROR };\n }\n if (err instanceof RPCError) {\n const payload: JsonObject = {\n error: err.message || INTERNAL_SERVER_ERROR,\n code: err.code,\n };\n if (err.data !== undefined) payload.data = err.data;\n return payload;\n }\n return { error: INTERNAL_SERVER_ERROR };\n};\n\n/**\n * Checks whether a content type maps to a form encoding\n * (`multipart/form-data` or `application/x-www-form-urlencoded`).\n * Form-declared functions accept either encoding so native browser\n * submissions (urlencoded) keep working without JavaScript.\n */\nexport const isFormContentType = (contentType: string): boolean =>\n contentType === \"multipart/form-data\" ||\n contentType === \"application/x-www-form-urlencoded\";\n\n/**\n * Detects whether an incoming request's `Content-Type` header conflicts\n * with the function's declared content type. JSON and text functions are\n * enforced strictly (exact match wins), while form functions accept both\n * form encodings because the nojs fallback submits urlencoded forms to\n * multipart-declared endpoints. Requests without a `Content-Type` header\n * (curl, GET, legacy clients) are exempt from enforcement.\n * @param declared - The declared `contentType` from the server function options\n * @param rawHeader - The raw `Content-Type` request header, if present\n */\nexport const hasContentTypeMismatch = (\n declared: ContentType,\n rawHeader: string | undefined,\n): boolean => {\n // No Content-Type header → exempt (url bar, GET, curl compatibility)\n if (!rawHeader) return false;\n // Strip parameters (charset, boundary) before comparison\n const incomingType = rawHeader.trim().toLowerCase().split(\";\")[0].trim();\n if (isFormContentType(declared)) {\n // Forms: reject only non-form encodings (lenient between the two)\n return !isFormContentType(incomingType);\n }\n return incomingType !== declared;\n};\n\n/**\n * Escapes special regex metacharacters in a string.\n * Used to safely embed user-configurable values (like rpcPrefix) into regular expressions,\n * preventing ReDoS and regex injection attacks.\n * @param s - The raw string to escape\n * @returns The escaped string safe for use in new RegExp()\n */\nexport function escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\nconst SAFE_URL_BASE = \"http://localhost\";\n\n/**\n * Parses a raw request URL against a fixed base without ever throwing.\n * Malformed request-targets (e.g. `/\\`, `//`, `/\\/`) make the WHATWG URL\n * parser throw `TypeError: Invalid URL`; the adapters call this while\n * building the per-request URL **before** their dispatch `try` block, so an\n * unhandled rejection there crashes raw `node:http` hosts (and Express 4).\n * On failure we fall back to the base root: the resulting pathname never\n * matches the RPC prefix, so the request is treated as non-RPC and falls\n * through to `next()` / 404 instead of crashing the process.\n * @param rawUrl - Raw request URL (path + optional query string)\n * @param base - Optional base URL, defaults to a fixed localhost origin\n * @returns A URL object; never throws\n */\nexport const safeURL = (rawUrl: string, base = SAFE_URL_BASE): URL => {\n try {\n return new URL(rawUrl, base);\n } catch {\n return new URL(\"/\", base);\n }\n};\n\nconst globalPrefixSymbol = Symbol.for(\"thednp.rpc.globalPrefix\");\n\n/** Global rpcPrefix from the last loaded config / middleware — fallback for functions without explicit prefix. */\nexport const getGlobalPrefix = (): string | undefined =>\n (globalThis as unknown as Record<symbol, string | undefined>)[\n globalPrefixSymbol\n ];\n\nexport const setGlobalPrefix = (prefix: string | undefined): void => {\n if (prefix) {\n (globalThis as unknown as Record<symbol, string | undefined>)[\n globalPrefixSymbol\n ] = prefix;\n } else {\n delete (globalThis as unknown as Record<symbol, string | undefined>)[\n globalPrefixSymbol\n ];\n }\n};\n","import type { ViteDevServer } from \"vite\";\nimport type { ClientFunctionWithOptions, ScanConfig } from \"./types.d.ts\";\nimport { readdir } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\nimport process from \"node:process\";\n\nimport { getFunctionsForPrefix } from \"./functionsMap.ts\";\nimport { defaultPrefix } from \"./options.ts\";\nimport { walkGlobFiles } from \"./server-helpers.ts\";\nimport {\n DUPLICATE_FUNCTION_NAME,\n ERROR_LOADING_FILE,\n NO_SERVER_FUNCTION_FOUND,\n} from \"./constants.ts\";\n\nlet isScanned = false;\n\n/** Absolute ids (normalized) of the scanned server function files. */\nexport const scannedServerFiles: Set<string> = new Set<string>();\n\nconst EXACT_NAMES = [\"server.ts\", \"server.js\", \"server.mjs\", \"server.mts\"];\n\n/**\n * Scans `src/api/` (or an explicit `scanRoot`) for server function files\n * and populates the server functions map (scoped by rpcPrefix) with their exported functions.\n * Uses Vite's SSR module loading to resolve and execute each file.\n *\n * Supports two matching modes via `config.serverFiles`:\n * `\"exact\"` — classic `server.ts|js|mjs|mts` names in the api directory\n * `\"glob\"` — recursively walking `scanRoot` to match `*.server.{ts,js,mjs,mts}`\n * @param initialCfg - Optional Vite config overrides (root, base, server, serverFiles, scanRoot)\n * @param devServer - Optional running Vite dev server instance; when provided, skips creating a new one\n */\nexport const scanForServerFiles = async (\n initialCfg?: ScanConfig,\n devServer?: ViteDevServer,\n): Promise<void> => {\n if (isScanned && !devServer) {\n return;\n }\n // Vite is only needed to spin up the internal dev server that loads the\n // server function files, so it is imported lazily rather than at the top\n // of the module. This keeps consumers of the standalone server entry that\n // register their functions directly (e.g. serverless functions bundling\n // the API module) free of a static Vite dependency — when Vite is\n // externalized by the function bundler, this lazy import is left as a\n // runtime require that is never executed.\n 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 // Keyed by `${prefix}:${registeredName}` so the same name can coexist\n // under different rpcPrefixes (see the registration loop below).\n const seenNames = new Set<string>();\n\n let files: string[];\n try {\n if (serverFiles === \"glob\") {\n files = await walkGlobFiles(resolvedScanRoot);\n } else {\n files = (await readdir(resolvedScanRoot, { withFileTypes: true }))\n .filter((f) => EXACT_NAMES.includes(f.name))\n .map((f) => join(resolvedScanRoot, f.name));\n }\n } catch (_e) {\n files = [];\n }\n\n try {\n for (const file of files) {\n scannedServerFiles.add(normalizePath(file));\n let moduleExports: Record<string, ClientFunctionWithOptions>;\n try {\n moduleExports = (await server.ssrLoadModule(file)) as Record<\n string,\n ClientFunctionWithOptions\n >;\n } catch (error) {\n console.error(ERROR_LOADING_FILE, file, error);\n continue;\n }\n const moduleEntries = Object.entries(moduleExports);\n if (!moduleEntries.length) {\n console.warn(NO_SERVER_FUNCTION_FOUND);\n return;\n }\n\n // Register each export into its prefix-scoped map, recording the\n // original export name so `getClientModules` can emit the matching\n // client stub. `createServerFunction` already auto-registers its name\n // into the appropriate prefix-scoped map at module load, so a function\n // may already exist here — in that case only the export name is added.\n // Track names seen in THIS scan run only, keyed by prefix: a name\n // repeated within one scan under the same prefix (e.g. two files\n // exporting the same function name) is a genuine conflict.\n for (const [exportName, exportValue] of moduleEntries) {\n const registeredName = exportValue.name;\n const prefix = exportValue.options?.rpcPrefix ||\n config.rpcPrefix ||\n defaultPrefix;\n const seenKey = `${prefix}:${registeredName}`;\n if (seenNames.has(seenKey)) {\n if (process.env.NODE_ENV !== \"production\") {\n throw new Error(DUPLICATE_FUNCTION_NAME(registeredName));\n }\n console.warn(DUPLICATE_FUNCTION_NAME(registeredName));\n continue;\n }\n seenNames.add(seenKey);\n const prefixMap = getFunctionsForPrefix(prefix);\n const existing = prefixMap.get(registeredName);\n if (existing) {\n existing.exportName = exportName;\n } else {\n prefixMap.set(registeredName, {\n name: registeredName,\n handler: exportValue,\n options: exportValue.options,\n exportName,\n });\n }\n }\n }\n } finally {\n if (!devServer && server) {\n await server.close();\n }\n isScanned = true;\n }\n};\n","/** @module Server function creation and registration. */\nimport type {\n ClientFunction,\n JsonArray,\n JsonValue,\n ServerFunctionInit,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\nimport { getFunctionsForPrefix } from \"./functionsMap.ts\";\nimport { defaultPrefix, defaultServerFnOptions } from \"./options.ts\";\nimport { getGlobalPrefix } from \"./server-helpers.ts\";\nimport { OPERATION_ABORTED } from \"./constants.ts\";\n\n/**\n * Extended options for createServerFunction, including rpcPrefix for multi-instance support.\n */\nexport interface CreateServerFunctionOptions\n extends Partial<ServerFunctionOptions> {\n /**\n * RPC prefix for this function. Enables multiple RPC instances with different prefixes.\n * When using multi-prefix setup, functions with the same name but different prefixes\n * can coexist without collision.\n * @default \"__rpc\"\n * @example\n * // v1 API\n * export const login = createServerFunction(\n * \"login\",\n * async (signal, email, password) => ({...}),\n * { rpcPrefix: \"v1:rpc\" },\n * );\n *\n * // v2 API - same function name, different prefix\n * export const login = createServerFunction(\n * \"login\",\n * async (signal, credentials) => ({...}),\n * { rpcPrefix: \"v2:rpc\" },\n * );\n */\n rpcPrefix?: string;\n}\n\n/**\n * Creates a server-side RPC function.\n * Registers the function in the server functions map (scoped by rpcPrefix) and returns\n * a client-compatible wrapper that exposes `data` (Promise) and `cancel` (function)\n * for request lifecycle control.\n * @param name - Unique identifier used by the RPC router to dispatch requests\n * @param handler - The actual implementation receiving an AbortSignal followed by JSON-serializable arguments\n * @param fnOptions - Optional contentType, credentials, and rpcPrefix settings\n * @returns A client stub with `data` promise and `cancel` method, auto-registered in the server map\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: CreateServerFunctionOptions = {},\n): ClientFunction<TArgs, TResult> {\n const options = Object.assign({}, defaultServerFnOptions, fnOptions);\n const rpcPrefix = fnOptions.rpcPrefix || getGlobalPrefix() || defaultPrefix;\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: false },\n options: { value: options, enumerable: true, configurable: false },\n });\n\n // Register to prefix-scoped map\n const prefixMap = getFunctionsForPrefix(rpcPrefix);\n prefixMap.set(name, {\n name,\n handler: wrappedFunction as never,\n options,\n });\n\n return wrappedFunction;\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,\n * colons, hyphens, and forward slashes.\n * @param segment - The string to validate\n * @param label - Human-readable label for error messages (e.g. \"rpcPrefix\")\n * @returns The validated segment if it passes\n * @throws Error if the segment contains disallowed characters\n */\nexport function validatePathSegment(segment: string, label: string): string {\n if (!SAFE_PATH_SEGMENT.test(segment)) {\n throw new Error(INVALID_PATH_SEGMENT(label, segment));\n }\n return segment;\n}\n\n/**\n * Validates and normalizes the credentials option.\n * Accepts \"same-origin\", \"include\", or \"omit\"; defaults to \"same-origin\" when undefined.\n * @param value - Credentials value to validate\n * @returns The validated credentials string\n * @throws Error if the value is not one of the accepted credentials\n */\nexport function validateCredentials(value?: string): Credentials {\n const creds = value || \"same-origin\";\n if (!CREDENTIALS_VALUES.includes(creds as Credentials)) {\n throw new Error(\n `Invalid credentials: \"${value}\" must be one of ${\n CREDENTIALS_VALUES.join(\", \")\n }`,\n );\n }\n return creds as Credentials;\n}\n\n/**\n * Validates and normalizes the HTTP method option for a server function.\n * Accepts \"GET\" or \"POST\" (case-insensitive); defaults to \"POST\" when undefined.\n * @param value - Method value to validate\n * @returns The validated uppercase method string\n * @throws Error if the value is not \"GET\" or \"POST\"\n */\nexport function validateMethod(value?: string): \"GET\" | \"POST\" {\n const method = (value || \"POST\").toUpperCase();\n if (method !== \"GET\" && method !== \"POST\") {\n throw new Error(`Invalid method: \"${value}\" must be one of GET, POST`);\n }\n return method;\n}\n","/**\n * @module Client module generation.\n */\nimport type {\n RpcPluginOptionsInternal,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\nimport { getFunctionsForPrefix } from \"./functionsMap.ts\";\nimport {\n validateCredentials,\n validateIdentifier,\n validateMethod,\n validatePathSegment,\n} from \"./validate.ts\";\n\n/**\n * Generates a JavaScript client module string for a single server function.\n * All interpolated values are validated to prevent code injection.\n * @param fnName - Registered RPC function name (validated as path segment)\n * @param fnEntry - Export name used in the generated module (validated as identifier)\n * @param options - Content type, credentials, and RPC prefix settings\n * @returns A string of JavaScript code exporting the client stub\n */\nconst getModule = (\n fnName: string,\n fnEntry: string,\n options: Partial<ServerFunctionOptions> & {\n contentType: ServerFunctionOptions[\"contentType\"];\n rpcPrefix: string;\n },\n): string => {\n // Validate all interpolated strings to prevent code injection\n const safeFnName = validatePathSegment(fnName, \"function name\");\n const safeFnEntry = validateIdentifier(fnEntry, \"export name\");\n const safePrefix = validatePathSegment(options.rpcPrefix, \"rpcPrefix\");\n const credentials = validateCredentials(options.credentials);\n const method = validateMethod(options.method);\n const contentType =\n (options.contentType ?? \"application/json\") as ServerFunctionOptions[\n \"contentType\"\n ];\n\n const opts: string[] = [];\n if (method !== \"POST\") opts.push(`method: \"${method}\"`);\n if (credentials !== \"same-origin\") opts.push(`credentials: \"${credentials}\"`);\n if (contentType !== \"application/json\") {\n opts.push(`contentType: \"${contentType}\"`);\n }\n const optsStr = opts.length ? `, { ${opts.join(\", \")} }` : \"\";\n\n const output = `\n export const ${safeFnEntry} = getClientStub(\"${safePrefix}\", \"${safeFnName}\"${optsStr});`;\n\n return output.trim();\n};\n\n/**\n * Generates the complete client-side module bundle by iterating all registered server functions\n * for a specific prefix and producing fetch-based stubs for each. The result is transformed by Vite\n * (or Oxc) during the dev server or production build.\n * @param initialOptions - Plugin options containing rpcPrefix and optional adapter\n * @returns A string of JavaScript code with all client RPC modules and their import dependencies\n */\nexport const getClientModules = (\n initialOptions: RpcPluginOptionsInternal,\n): string => {\n // Validate prefix once at the top level\n validatePathSegment(initialOptions.rpcPrefix, \"rpcPrefix\");\n\n // Get functions registered for this specific prefix\n const prefixMap = getFunctionsForPrefix(initialOptions.rpcPrefix);\n const entries = Array.from(prefixMap.entries())\n .filter(([, entry]) => entry.exportName)\n .map(([registeredName, entry]) =>\n getModule(registeredName, entry.exportName!, {\n ...initialOptions,\n ...((entry.options as ServerFunctionOptions) || {}),\n })\n )\n .join(\"\\n\");\n\n const output = `\n// Client-side RPC modules for prefix: ${initialOptions.rpcPrefix}\nimport { getClientStub } from \"@thednp/rpc/helpers\";\n${entries}`;\n\n return output.trim();\n};\n","/** @module Server-side request context. Exports the `RequestEvent` shape, `provideRequestContext` to establish it around a dispatch, `getRequestContext` to read it from anywhere inside the async tree, `redirect` and `sendResponse` for framework-level short-circuits, and `getRequestMeta` for normalized request access. Never import this module in client code — it is server-only. */\n\n// @thednp/rpc/src/context.ts\nimport { AsyncLocalStorage } from \"node:async_hooks\";\nimport type { JsonValue } from \"./types.d.ts\";\nimport { safeURL } from \"./server-helpers.ts\";\n\n/**\n * Global symbol under which the shared `AsyncLocalStorage` instance is stored\n * on `globalThis`. Keeping it on a `Symbol.for` key makes it instance-stable\n * across module copies and dev-server hot reloads, mirroring\n * `solid-js/web`'s own request-context storage.\n */\nconst requestContextSymbol = Symbol.for(\"thednp.rpc.requestContext\");\n\n/**\n * A per-request context established by the framework adapters around\n * server-function dispatch, mirroring Solid Start's `FetchEvent`. Any code\n * running in the async tree of a dispatch can read the current context through\n * {@link getRequestContext} instead of threading `req`/`res` (or the framework\n * `Context` object) through every nested call. This module is server-only and\n * must never be imported by client code.\n *\n * Each adapter extends this with framework-specific request/response accessors:\n * - Express: `req`/`res` plus `nativeEvent = { req, res }`\n * - Fastify: `request`/`reply` plus `nativeEvent = request`\n * - Koa: `ctx` plus `nativeEvent = ctx`\n * - Hono: `c` (the Hono `Context`) plus `nativeEvent = c`\n * - h3: `event` (the h3 `H3Event`) plus `nativeEvent = event`\n */\nexport interface RequestEvent {\n /** Adapter-specific native event kept for deep framework access */\n nativeEvent?: unknown;\n /** Adapter request object */\n request: unknown;\n /** Adapter response object */\n response: unknown;\n /**\n * Bound adapter-native redirect. Performing a redirect sets `redirected`\n * so the middleware can skip the JSON `{ data }` send.\n * @param location - The URL to redirect to\n * @param status - HTTP status code, defaults to `303 See Other`\n */\n redirect: (location: string, status?: number) => void;\n /**\n * Set by `redirect` once a redirect has been issued. The middleware checks\n * this after `await`ing the server function to avoid double-responding.\n */\n redirected?: { location: string; status: number };\n /**\n * Bound adapter-native response short-circuit. Writes the given status and\n * JSON body (plus optional headers) directly, bypassing the standard\n * `{ data }` response. Setting `sent` makes the middleware skip the JSON\n * `{ data }` send, mirroring `redirect`/`redirected`.\n * @param status - HTTP status code (e.g. 401, 413, 429)\n * @param body - JSON-serializable response body\n * @param headers - Optional response headers (e.g. `{ \"Retry-After\": \"60\" }`)\n */\n send: (\n status: number,\n body: JsonValue,\n headers?: Record<string, string>,\n ) => void;\n /**\n * Set by `send` once a response has been issued. The middleware checks this\n * after `await`ing the server function to avoid double-responding.\n */\n sent?: { status: number; body: JsonValue; headers?: Record<string, string> };\n /**\n * The matched RPC function name for the current request, when available.\n * Useful for per-function rate limiting or auditing inside middleware.\n */\n functionName?: string;\n /** Per-request app data shared across the async tree of the dispatch */\n locals: Record<string, unknown>;\n [prop: string]: unknown;\n}\n\n// Instance-stable across module copies and dev-server hot reloads, exactly like\n// `solid-js/web`'s `provideRequestEvent` (which stores on a globalThis symbol).\nconst requestContextStorage: AsyncLocalStorage<RequestEvent> =\n ((globalThis as Record<symbol, AsyncLocalStorage<RequestEvent>>)[\n requestContextSymbol\n ] ??= new AsyncLocalStorage<RequestEvent>());\n\n/**\n * Runs `cb` with `init` as the current request context. Use inside the\n * adapters around server-function dispatch (the async tree under `cb` can then\n * read the context via {@link getRequestContext}).\n * @param init - The request context for the duration of `cb`\n * @param cb - The work that needs access to the request context\n */\nexport const provideRequestContext = <T>(\n init: RequestEvent,\n cb: () => T,\n): T => requestContextStorage.run(init, cb);\n\n/**\n * Returns the current request context, or throws when called outside of a\n * request (e.g. module scope or a background task).\n * @throws When no request context is established\n */\nexport const getRequestContext = (): RequestEvent => {\n const ctx = requestContextStorage.getStore();\n if (!ctx) {\n throw new Error(\"RequestEvent is not available outside of a request\");\n }\n return ctx;\n};\n\n/**\n * Redirects the current request to `location`. Reads the adapter-bound\n * `redirect` from the current request context — callable from anywhere inside\n * a server-function tree (no `res` threading needed).\n * @param location - The URL to redirect to\n * @param status - HTTP status code, defaults to `303 See Other`\n * @throws When called outside of a request\n */\nexport const redirect = (location: string, status = 303): void => {\n getRequestContext().redirect(location, status);\n};\n\n/**\n * Sends a raw JSON response for the current request, bypassing the standard\n * `{ data }` shape. Reads the adapter-bound `send` from the current request\n * context — callable from anywhere inside a server-function tree. Any code in\n * the async tree of a dispatch can call this (e.g. custom middleware) to\n * short-circuit with a specific status code (401, 413, 429, ...).\n * @param status - HTTP status code\n * @param body - JSON-serializable response body\n * @param headers - Optional response headers\n * @throws When called outside of a request\n */\nexport const sendResponse = (\n status: number,\n body: JsonValue,\n headers?: Record<string, string>,\n): void => {\n getRequestContext().send(status, body, headers);\n};\n\n/**\n * Normalized, adapter-agnostic view of the current request. Reads the request\n * object off the current request context and normalizes it across the five\n * adapter request shapes (Express `req`, Fastify `req`, Koa `ctx.req`,\n * Hono `c.req`, h3 `event.req`) so middleware can be written once.\n */\nexport interface RequestMeta {\n /** HTTP method, upper-cased (e.g. \"GET\", \"POST\") */\n method: string;\n /** URL pathname (e.g. \"/__rpc/greet\") */\n pathname: string;\n /** Raw search string including the leading \"?\", or \"\" when absent */\n search: string;\n /** Parsed search params */\n searchParams: URLSearchParams;\n /** Request headers, lower-cased */\n headers: Record<string, string | string[] | undefined>;\n /** Host header value (e.g. \"localhost:5173\"), when present */\n host?: string;\n /** Client IP when the framework exposes it (e.g. Fastify `req.ip`) */\n ip?: string;\n /** Request protocol (\"http\" or \"https\"), when determinable */\n protocol?: string;\n}\n\nconst pickHeader = (\n headers: Record<string, string | string[] | undefined>,\n name: string,\n): string | undefined => {\n const value = headers[name];\n if (typeof value === \"string\") return value;\n if (Array.isArray(value)) return value[0];\n return undefined;\n};\n\n/** Normalizes any headers shape into a plain lower-cased record. */\nconst toHeaderRecord = (\n headers: unknown,\n): Record<string, string | string[] | undefined> => {\n if (!headers) return {};\n // Headers-like object (h3 Request, Hono c.req.raw.headers, fetch Headers)\n if (typeof (headers as Headers).forEach === \"function\") {\n const record: Record<string, string> = {};\n (headers as Headers).forEach((value, key) => {\n record[key] = value;\n });\n return record;\n }\n // Plain map (Express req.headers, Fastify req.headers, Koa ctx.req.headers)\n return headers as Record<string, string | string[] | undefined>;\n};\n\n/**\n * Reads normalized, adapter-agnostic request metadata from the current request\n * context. Works with Express `req`, Fastify `req`, Koa `ctx.req`,\n * Hono `c.req` and h3 `event.req` by feature-detecting the request shape\n * (`originalUrl`/`url`/`path`, raw `headers` map vs `Headers`-like API).\n * @param event - The request context to read, typically the result of\n * {@link getRequestContext}\n */\nexport const getRequestMeta = (event: RequestEvent): RequestMeta => {\n const req = event.request as {\n method?: string;\n originalUrl?: string;\n url?: string;\n path?: string;\n headers?: unknown;\n header?: (name: string) => string | string[] | undefined;\n ip?: string;\n protocol?: string;\n socket?: { remoteAddress?: string };\n } | undefined;\n\n const method = (req?.method ?? \"GET\").toUpperCase();\n const rawUrl = req?.originalUrl ?? req?.url ?? req?.path ?? \"\";\n const url = safeURL(rawUrl);\n const headers = toHeaderRecord(req?.headers);\n const hostHeader = pickHeader(headers, \"host\");\n\n return {\n method,\n pathname: url.pathname,\n search: url.search,\n searchParams: url.searchParams,\n headers,\n host: hostHeader,\n ip: req?.ip ?? req?.socket?.remoteAddress,\n protocol: req?.protocol ?? url.protocol.replace(\":\", \"\"),\n };\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thednp/rpc",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "license": "MIT",
5
5
  "author": "thednp",
6
6
  "description": "⚡ A Vite plugin for creating server functions with automatic Remote Procedure Calls (RPC)",
@@ -49,20 +49,22 @@
49
49
  "./package.json": "./package.json"
50
50
  },
51
51
  "scripts": {
52
- "test": "vitest",
53
- "test-ui": "vitest --ui",
54
- "test-dev": "node scripts/dev-test",
55
- "test-prod": "node scripts/dev-test --mode=preview",
52
+ "test": "vitest run --coverage",
53
+ "test:watch": "vitest --watch --coverage",
54
+ "test:ui": "vitest --ui",
55
+ "test:dev": "node scripts/dev-test",
56
+ "test:prod": "node scripts/dev-test --mode=preview",
56
57
  "format": "deno fmt src tests examples/**/src",
57
58
  "clean": "rm -rf examples/**/dist examples/**/node_modules examples/**/package-lock.json examples/**/pnpm-lock.yaml examples/**/bun.lockb examples/**/deno.lock",
58
59
  "dev": "cd examples/spa && pnpm up && pnpm dev",
59
- "dev:koa": "cd examples/koa && pnpm up && pnpm dev",
60
+ "dev:advanced": "cd examples/advanced && pnpm up && pnpm dev",
60
61
  "dev:express": "cd examples/express && pnpm up && pnpm dev",
61
- "dev:react-query": "cd examples/react-query && pnpm up && pnpm dev",
62
- "dev:solid-query": "cd examples/solid-query && pnpm up && pnpm dev",
63
62
  "dev:fastify": "cd examples/fastify && pnpm up && pnpm dev",
64
- "dev:hono": "cd examples/hono && pnpm up && pnpm dev",
65
63
  "dev:h3": "cd examples/h3 && pnpm up && pnpm dev",
64
+ "dev:hono": "cd examples/hono && pnpm up && pnpm dev",
65
+ "dev:koa": "cd examples/koa && pnpm up && pnpm dev",
66
+ "dev:react-query": "cd examples/react-query && pnpm up && pnpm dev",
67
+ "dev:solid-query": "cd examples/solid-query && pnpm up && pnpm dev",
66
68
  "dev:ssr": "cd examples/ssr && pnpm up && pnpm dev",
67
69
  "lint": "pnpm lint:ts && pnpm check:ts",
68
70
  "lint:ts": "deno lint src",
@@ -81,24 +83,24 @@
81
83
  },
82
84
  "dependencies": {
83
85
  "express": "^5.2.1",
84
- "fastify": "^5.11.3",
86
+ "fastify": "^5.12.1",
85
87
  "fastify-plugin": "^6.0.0",
86
- "h3": "2.0.1-rc.26",
87
- "hono": "^4.13.1",
88
+ "h3": "2.0.1-rc.28",
89
+ "hono": "^4.13.3",
88
90
  "koa": "^3.2.1",
89
- "vite": "^8.2.1"
91
+ "vite": "^8.2.2"
90
92
  },
91
93
  "devDependencies": {
92
- "@hono/node-server": "^2.1.0",
94
+ "@hono/node-server": "^2.1.1",
93
95
  "@types/express": "^5.0.6",
94
96
  "@types/koa": "^3.0.3",
95
97
  "@types/node": "^26.2.0",
96
- "@vitest/coverage-istanbul": "^4.1.10",
97
- "@vitest/ui": "^4.1.10",
98
+ "@vitest/coverage-istanbul": "^4.1.11",
99
+ "@vitest/ui": "^4.1.11",
98
100
  "tsdown": "^0.22.14",
99
101
  "typescript": "^7.0.2",
100
102
  "vite-plugin-strip-comments": "^0.0.10",
101
- "vitest": "^4.1.10"
103
+ "vitest": "^4.1.11"
102
104
  },
103
- "packageManager": "pnpm@11.20.0"
105
+ "packageManager": "pnpm@11.22.0"
104
106
  }