@thednp/rpc 0.3.6 → 0.3.7
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.
- package/AGENTS.md +25 -7
- package/CHANGELOG.md +116 -0
- package/README.md +2 -2
- package/dist/config/config.d.mts +0 -7
- package/dist/config/config.d.mts.map +1 -1
- package/dist/config/config.mjs +5 -1
- package/dist/config/config.mjs.map +1 -1
- package/dist/express/express.d.mts +2 -9
- package/dist/express/express.d.mts.map +1 -1
- package/dist/express/express.mjs +70 -22
- package/dist/express/express.mjs.map +1 -1
- package/dist/fastify/fastify.d.mts +2 -8
- package/dist/fastify/fastify.d.mts.map +1 -1
- package/dist/fastify/fastify.mjs +72 -22
- package/dist/fastify/fastify.mjs.map +1 -1
- package/dist/fastify/plugin/fastify/plugin.d.mts +13 -49
- package/dist/fastify/plugin/fastify/plugin.d.mts.map +1 -1
- package/dist/fastify/plugin/fastify/plugin.mjs +71 -21
- package/dist/fastify/plugin/fastify/plugin.mjs.map +1 -1
- package/dist/h3/h3.d.mts +2 -2
- package/dist/h3/h3.d.mts.map +1 -1
- package/dist/h3/h3.mjs +68 -21
- package/dist/h3/h3.mjs.map +1 -1
- package/dist/helpers/helpers.d.mts.map +1 -1
- package/dist/helpers/helpers.mjs +2 -0
- package/dist/helpers/helpers.mjs.map +1 -1
- package/dist/hono/hono.d.mts +2 -2
- package/dist/hono/hono.d.mts.map +1 -1
- package/dist/hono/hono.mjs +93 -23
- package/dist/hono/hono.mjs.map +1 -1
- package/dist/index.d.mts +26 -19
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +62 -28
- package/dist/index.mjs.map +1 -1
- package/dist/koa/koa.d.mts +2 -9
- package/dist/koa/koa.d.mts.map +1 -1
- package/dist/koa/koa.mjs +74 -22
- package/dist/koa/koa.mjs.map +1 -1
- package/dist/server/server.d.mts +107 -13
- package/dist/server/server.d.mts.map +1 -1
- package/dist/server/server.mjs +146 -16
- package/dist/server/server.mjs.map +1 -1
- package/llms.txt +4 -2
- package/package.json +5 -3
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Connect, Plugin, ResolvedConfig } from "vite";
|
|
2
|
-
import {
|
|
2
|
+
import { AdapterName as AdapterName$1, MiddlewareOptions as MiddlewareOptions$1 } from "@thednp/rpc";
|
|
3
3
|
import { IncomingMessage, ServerResponse } from "node:http";
|
|
4
4
|
import { NextFunction, Request, Response as Response$1 } from "express";
|
|
5
5
|
import { MiddlewareHandler } from "hono";
|
|
@@ -19,7 +19,7 @@ type ExpressMiddlewareOptions = MiddlewareOptions$1<"express">;
|
|
|
19
19
|
* Express middleware factory: takes optional initial options and returns
|
|
20
20
|
* the Express/Connect-compatible handler.
|
|
21
21
|
*/
|
|
22
|
-
type ExpressMiddlewareFn = <A extends
|
|
22
|
+
type ExpressMiddlewareFn = <A extends AdapterName$1 = "express">(initialOptions?: Partial<ExpressMiddlewareOptions>) => ExpressMiddlewareHooks["handler"];
|
|
23
23
|
/**
|
|
24
24
|
* Express/Connect middleware handler signature used by the RPC middleware.
|
|
25
25
|
*/
|
|
@@ -45,7 +45,7 @@ interface HonoMiddlewareHooks {
|
|
|
45
45
|
* Hono middleware factory: takes optional initial options and returns
|
|
46
46
|
* the Hono-compatible handler.
|
|
47
47
|
*/
|
|
48
|
-
type HonoMiddlewareFn = <A extends
|
|
48
|
+
type HonoMiddlewareFn = <A extends AdapterName$1 = "hono">(initialOptions?: Partial<MiddlewareOptions$1<A>>) => HonoMiddlewareHooks["handler"];
|
|
49
49
|
//#endregion
|
|
50
50
|
//#region src/fastify/types.d.ts
|
|
51
51
|
/**
|
|
@@ -56,7 +56,7 @@ type FastifyMiddlewareOptions = MiddlewareOptions$1<"fastify">;
|
|
|
56
56
|
* Fastify middleware factory: takes optional initial options and returns
|
|
57
57
|
* the Fastify-compatible handler.
|
|
58
58
|
*/
|
|
59
|
-
type FastifyMiddlewareFn = <A extends
|
|
59
|
+
type FastifyMiddlewareFn = <A extends AdapterName$1 = "fastify">(initialOptions?: Partial<FastifyMiddlewareOptions>) => FastifyMiddlewareHooks["handler"];
|
|
60
60
|
/**
|
|
61
61
|
* Fastify middleware handler signature used by the RPC middleware.
|
|
62
62
|
*/
|
|
@@ -90,7 +90,7 @@ interface KoaMiddlewareHooks {
|
|
|
90
90
|
* Koa middleware factory: takes optional initial options and returns
|
|
91
91
|
* the Koa-compatible handler.
|
|
92
92
|
*/
|
|
93
|
-
type KoaMiddlewareFn = <A extends
|
|
93
|
+
type KoaMiddlewareFn = <A extends AdapterName$1 = "koa">(initialOptions?: Partial<KoaMiddlewareOptions>) => KoaMiddlewareHooks["handler"];
|
|
94
94
|
//#endregion
|
|
95
95
|
//#region src/h3/types.d.ts
|
|
96
96
|
/**
|
|
@@ -112,9 +112,20 @@ interface H3MiddlewareHooks {
|
|
|
112
112
|
* h3 middleware factory: takes optional initial options and returns
|
|
113
113
|
* the h3-compatible handler.
|
|
114
114
|
*/
|
|
115
|
-
type H3MiddlewareFn = <A extends
|
|
115
|
+
type H3MiddlewareFn = <A extends AdapterName$1 = "h3">(initialOptions?: Partial<H3MiddlewareOptions>) => H3MiddlewareHooks["handler"];
|
|
116
116
|
//#endregion
|
|
117
117
|
//#region src/types.d.ts
|
|
118
|
+
/**
|
|
119
|
+
* Every framework adapter rpc ships a middleware for.
|
|
120
|
+
*
|
|
121
|
+
* This is the key type for {@link FrameworkHooks}, so `MiddlewareOptions<A>`
|
|
122
|
+
* can type each adapter's `handler` signature. It is a *type* only — the
|
|
123
|
+
* adapter you get is the one you import (`@thednp/rpc/express`,
|
|
124
|
+
* `@thednp/rpc/hono`, …). There is deliberately no config option that selects
|
|
125
|
+
* it: a runtime value could only ever disagree with the subpath actually
|
|
126
|
+
* mounted, and nothing read it.
|
|
127
|
+
*/
|
|
128
|
+
type AdapterName = "express" | "hono" | "h3" | "fastify" | "koa";
|
|
118
129
|
/**
|
|
119
130
|
* Maps each supported framework adapter to its middleware hooks (handler signatures).
|
|
120
131
|
* Used to keep the middleware options type-safe per adapter.
|
|
@@ -274,12 +285,13 @@ type ClientFunctionWithOptions = ClientFunction & {
|
|
|
274
285
|
};
|
|
275
286
|
/**
|
|
276
287
|
* Internal plugin options accepted by `getClientModules`.
|
|
288
|
+
*
|
|
289
|
+
* Only the prefix: the generated stubs are adapter-agnostic, since they are
|
|
290
|
+
* plain `fetch` calls.
|
|
277
291
|
*/
|
|
278
292
|
interface RpcPluginOptionsInternal {
|
|
279
293
|
/** RPC endpoint prefix (e.g. "__rpc") */
|
|
280
294
|
rpcPrefix: string;
|
|
281
|
-
/** Framework adapter name */
|
|
282
|
-
adapter?: string | undefined;
|
|
283
295
|
}
|
|
284
296
|
/**
|
|
285
297
|
* Partial Vite config used when scanning server files outside a running dev server.
|
|
@@ -324,13 +336,6 @@ interface RpcPluginOptions {
|
|
|
324
336
|
* rpcPrefix: "api/rpc"
|
|
325
337
|
*/
|
|
326
338
|
rpcPrefix: "__rpc" | string;
|
|
327
|
-
/**
|
|
328
|
-
* Option to set an adapter for the middleware connection. The default is _express_,
|
|
329
|
-
* which is the most popular and battle tested server app. The _express_ adapter is
|
|
330
|
-
* also compatible with the vite's Connect development server.
|
|
331
|
-
* @default express
|
|
332
|
-
*/
|
|
333
|
-
adapter: "express" | "hono" | "h3" | "fastify" | "koa";
|
|
334
339
|
/**
|
|
335
340
|
* Root directory from which the plugin scans for server files.
|
|
336
341
|
* Defaults to `<root>/src/api`. Use this in monorepos where server files
|
|
@@ -353,7 +358,7 @@ interface RpcPluginOptions {
|
|
|
353
358
|
*/
|
|
354
359
|
silent?: boolean;
|
|
355
360
|
}
|
|
356
|
-
interface MiddlewareOptions<A extends
|
|
361
|
+
interface MiddlewareOptions<A extends AdapterName = "express"> {
|
|
357
362
|
/**
|
|
358
363
|
* Name for the middleware (used for identification in Express stack)
|
|
359
364
|
*/
|
|
@@ -379,7 +384,7 @@ interface MiddlewareOptions<A extends RpcPluginOptions["adapter"] = "express"> {
|
|
|
379
384
|
* // Results in endpoints like: /api/rpc/myFunction
|
|
380
385
|
* rpcPrefix: "api/rpc"
|
|
381
386
|
*/
|
|
382
|
-
rpcPrefix?: string
|
|
387
|
+
rpcPrefix?: string;
|
|
383
388
|
/**
|
|
384
389
|
* Allowed request origin(s) — a single origin string or an allowlist of them
|
|
385
390
|
* (e.g. `"https://example.com"` or
|
|
@@ -480,7 +485,9 @@ type InnerModReturn<T extends JsonValue> = {
|
|
|
480
485
|
* @param opts - Optional settings; `silent` suppresses the "no config found" warning
|
|
481
486
|
* @returns Resolved RPC plugin options
|
|
482
487
|
*/
|
|
483
|
-
declare const loadRPCConfig: (configFile?: string
|
|
488
|
+
declare const loadRPCConfig: (configFile?: string | {
|
|
489
|
+
silent?: boolean;
|
|
490
|
+
}, opts?: {
|
|
484
491
|
silent?: boolean;
|
|
485
492
|
}) => Promise<RpcPluginOptions>;
|
|
486
493
|
/**
|
|
@@ -492,5 +499,5 @@ declare const loadRPCConfig: (configFile?: string, opts?: {
|
|
|
492
499
|
*/
|
|
493
500
|
declare function rpcPlugin(devOptions?: Partial<RpcPluginOptions>): Plugin;
|
|
494
501
|
//#endregion
|
|
495
|
-
export { type BodyResult, type ClientFunction, type ClientFunctionWithOptions, type ContentType, type Credentials, type FrameworkHooks, type FrameworkMiddlewareFn, type InnerModReturn, type JsonArray, type JsonObject, type JsonPrimitive, type JsonValue, type MiddlewareOptions, type RpcPluginOptions, type RpcPluginOptionsInternal, type ScanConfig, type ServerFnArgs, type ServerFnEntry, type ServerFunction, type ServerFunctionInit, type ServerFunctionOptions, type StubOptions, type SupportableContentType, rpcPlugin as default, loadRPCConfig };
|
|
502
|
+
export { type AdapterName, type BodyResult, type ClientFunction, type ClientFunctionWithOptions, type ContentType, type Credentials, type FrameworkHooks, type FrameworkMiddlewareFn, type InnerModReturn, type JsonArray, type JsonObject, type JsonPrimitive, type JsonValue, type MiddlewareOptions, type RpcPluginOptions, type RpcPluginOptionsInternal, type ScanConfig, type ServerFnArgs, type ServerFnEntry, type ServerFunction, type ServerFunctionInit, type ServerFunctionOptions, type StubOptions, type SupportableContentType, rpcPlugin as default, loadRPCConfig };
|
|
496
503
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":["MiddlewareOptions","
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":["MiddlewareOptions","AdapterName","Response","AdapterName","MiddlewareOptions","MiddlewareOptions","AdapterName","MiddlewareOptions","AdapterName","MiddlewareOptions","AdapterName"],"sources":["../src/express/types.d.ts","../src/hono/types.d.ts","../src/fastify/types.d.ts","../src/koa/types.d.ts","../src/h3/types.d.ts","../src/types.d.ts","../src/index.ts"],"mappings":";;;;;;;;;;;;;;;;KAgBY,2BAA2BA;;;;;KAM3B,uBACV,UAAUC,2BAEV,iBAAiB,QAAQ,8BACtB;;;;UAKY;;;;;;;EAOf,UACE,KAAK,kBAAkB,SACvB,KAAK,iBAAiBC,YACtB,MAAM,QAAQ,eAAe,iBAC1B;;;;;;;UCzBU;;EAEf,SAAS;;;;;;KAOC,oBAAoB,UAAUC,wBACxC,iBAAiB,QAAQC,oBAAkB,QACxC;;;;;;KCWO,2BAA2BC;;;;;KAM3B,uBACV,UAAUC,2BAEV,iBAAiB,QAAQ,8BACtB;;;;UAKY;;;;;;;EAOf,UACE,KAAK,gBACL,KAAK,cACL,MAAM,4BACH;;;;;;;KCzDK,uBAAuBC;;;;UAalB;;;;;;EAMf,UAAU,KAAK,SAAS,MAAM,SAAS;;;;;;KAO7B,mBAAmB,UAAUC,uBACvC,iBAAiB,QAAQ,0BACtB;;;;;;KC7BO,sBAAsBC;;;;UAKjB;;;;;;EAMf,SAAS;;;;;;KAOC,kBAAkB,UAAUC,sBACtC,iBAAiB,QAAQ,yBACtB;;;;;;;;;;;;;KCEO;;;;;UAMK;;EAEf,SAAS;;EAET,MAAM;;EAEN,SAAS;;EAET,KAAK;;EAEL,IAAI;;;;;UAMW;;EAEf,SAAS;;EAET,MAAM;;EAEN,SAAS;;EAET,KAAK;;EAEL,IAAI;;;;;;KAOM;;;;KAUA;;;;KASA;;;;KAKA;EACN;EAAiC,MAAM;;EACvC;EAA2B;;EAE7B;EACA,MAAM;;EAEJ;EAAoC,MAAM;;;;;;UAM/B;;;;;EAKf,aAAa;;;;;EAKb,cAAc;;;;;;;EAOd;;;;;EAKA;;;;;;KAOU;;;;KAIA;GAAgB,cAAc,YAAY;;;;;KAI1C,aAAa,WAAW;;;;KAIxB,YAAY,gBAAgB,YAAY;;;;;;;;;;;;;;;;;;;KAuBxC,mBAAmB;;;;;KAMnB,eACV,cAAc,YAAY,WAC1B,UAAU,cACP,QAAQ,gBAAgB,MAAM,UAAU,QAAQ;;;;;KAMzC,mBACV,cAAc,WAAW,YAAY,WACrC,UAAU,cACP,QAAQ,gBAAgB,MAAM,UAAU,QAAQ;;;;;;KAOzC,eACV,cAAc,YAAY,WAC1B,UAAU,iBACJ,MAAM;;EAEZ,MAAM,QAAQ;;EAEd,SAAS;;;;;;KAOC,4BAA4B;;EAEtC;;EAEA,UAAU;;;;;;;;UASK;;EAEf;;;;;UAMe,mBAAmB,KAAK;EACvC;EACA,SAAS,QAAQ;EACjB;EACA;;EAEA;;;;;;UAOe;;EAEf;;EAEA,SAAS;;EAET,UAAU;;EAEV;;;;;;;;UASe;;;;;;;;;;;EAWf;;;;;;;EAQA;;;;;;;EAQA;;;;;;;EAQA;;UAGe,kBACf,UAAU;;;;EAKV;;;;;;;;;;;;EAaA,gBAAgB;;;;;;;;;;EAWhB;;;;;;;;;;;;;;;;;;;;;;;;;EA0BA;;;;;;;;EASA;;;;;EAMA;;;;;;;;;;;;;;;;;;EAmBA,UAAU,eAAe;;;;;;UAOV;;;;;EAKf;;;;;EAKA,aAAa;;;;;;;EAOb,aAAa;;;;;;KAOH,eAAe,UAAU;;EAEnC,MAAM,QAAQ;;EAEd,SAAS;;;;;;;;;;;;cC3VL,gBACJ;EAAwB;GACxB;EAAS;MACN,QAAQ;;;;;;;;iBAmGJ,UACP,aAAY,QAAQ,oBACnB"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as vite from "vite";
|
|
1
2
|
import { loadConfigFromFile, mergeConfig } from "vite";
|
|
2
3
|
import { join, resolve } from "node:path";
|
|
3
4
|
import process from "node:process";
|
|
@@ -6,16 +7,27 @@ import { readdir } from "node:fs/promises";
|
|
|
6
7
|
import { setGlobalPrefix } from "@thednp/rpc/server";
|
|
7
8
|
import { createRPCMiddleware } from "@thednp/rpc/express";
|
|
8
9
|
//#region src/options.ts
|
|
10
|
+
/**
|
|
11
|
+
* The built-in RPC endpoint prefix, used when neither an explicit prefix nor a
|
|
12
|
+
* global one (`getGlobalPrefix`) is supplied. Kept for backward compatibility
|
|
13
|
+
* with pre-multi-prefix setups, where every function lived under this one map.
|
|
14
|
+
*/
|
|
9
15
|
const defaultPrefix = "__rpc";
|
|
16
|
+
/**
|
|
17
|
+
* Baseline plugin options. `defineConfig` merges a user's partial config over
|
|
18
|
+
* these, and `loadRPCConfig` merges a loaded config file over them, so every
|
|
19
|
+
* option has a defined value even when a config file omits it.
|
|
20
|
+
*/
|
|
10
21
|
const defaultRPCOptions = {
|
|
11
22
|
rpcPrefix: defaultPrefix,
|
|
12
|
-
adapter: "express",
|
|
13
23
|
serverFiles: "exact",
|
|
14
24
|
scanRoot: void 0
|
|
15
25
|
};
|
|
16
26
|
//#endregion
|
|
17
27
|
//#region src/constants.ts
|
|
28
|
+
/** Warning logged when a scanned server module exports nothing. */
|
|
18
29
|
const NO_SERVER_FUNCTION_FOUND = "No server function found.";
|
|
30
|
+
/** Error logged when a server function file cannot be loaded by Vite's SSR loader. */
|
|
19
31
|
const ERROR_LOADING_FILE = "Error loading file:";
|
|
20
32
|
/** Error message when a value fails the safe-identifier validation. @param label - What kind of value was being validated. @param name - The rejected value */
|
|
21
33
|
const INVALID_IDENTIFIER = (label, name) => `Invalid ${label}: "${name}" must match /^[A-Za-z_$][A-Za-z0-9_$]*$/`;
|
|
@@ -23,7 +35,9 @@ const INVALID_IDENTIFIER = (label, name) => `Invalid ${label}: "${name}" must ma
|
|
|
23
35
|
const INVALID_PATH_SEGMENT = (label, segment) => `Invalid ${label}: "${segment}" must match /^[A-Za-z0-9_$@:][A-Za-z0-9_$@:/-]*$/`;
|
|
24
36
|
/** Warning message when a specified RPC config file cannot be resolved on disk. @param configFile - The requested config filename. @param configFilePath - The resolved absolute path */
|
|
25
37
|
const CONFIG_FILE_NOT_FOUND = (configFile, configFilePath) => ` ⚠︎ The specified RPC config file ${configFile} cannot be found at ${configFilePath}, loading the defaults..`;
|
|
38
|
+
/** Warning logged when no config file is discovered and the defaults are used. */
|
|
26
39
|
const NO_CONFIG_FOUND = ` ⚡︎ No RPC config found, loading the defaults..`;
|
|
40
|
+
/** Warning logged when a config file exists but could not be loaded; the defaults are used. */
|
|
27
41
|
const FAILED_LOAD_CONFIG = ` ⚠︎ Failed to load RPC config:`;
|
|
28
42
|
/** Error template for duplicate server function names across files. @param name - The duplicate registered name */
|
|
29
43
|
const DUPLICATE_FUNCTION_NAME = (name) => `Duplicate server function "${name}" detected. Each server function must have a unique name. Remove or rename the duplicate.`;
|
|
@@ -158,7 +172,10 @@ const getModule = (fnName, fnEntry, options) => {
|
|
|
158
172
|
* Generates the complete client-side module bundle by iterating all registered server functions
|
|
159
173
|
* for a specific prefix and producing fetch-based stubs for each. The result is transformed by Vite
|
|
160
174
|
* (or Oxc) during the dev server or production build.
|
|
161
|
-
*
|
|
175
|
+
*
|
|
176
|
+
* The generated stubs are plain `fetch` calls, so they are adapter-agnostic —
|
|
177
|
+
* only the prefix is needed.
|
|
178
|
+
* @param initialOptions - Plugin options containing the rpcPrefix
|
|
162
179
|
* @returns A string of JavaScript code with all client RPC modules and their import dependencies
|
|
163
180
|
*/
|
|
164
181
|
const getClientModules = (initialOptions) => {
|
|
@@ -200,7 +217,17 @@ const walkGlobFiles = async (dir) => {
|
|
|
200
217
|
};
|
|
201
218
|
//#endregion
|
|
202
219
|
//#region src/scanForServerFiles.ts
|
|
203
|
-
|
|
220
|
+
/**
|
|
221
|
+
* Scan targets already performed, so a lazy re-scan is not repeated.
|
|
222
|
+
*
|
|
223
|
+
* Keyed by everything that determines the outcome — the resolved scan root
|
|
224
|
+
* (which files are read), the matching mode, and the prefix prefix-less
|
|
225
|
+
* functions register under. A single process-wide boolean used to be enough
|
|
226
|
+
* only while there was one prefix: the *first* scan suppressed every later
|
|
227
|
+
* one, so a second RPC instance on a different prefix asked for a lazy scan,
|
|
228
|
+
* got an early return, and answered 404 for every function it owned.
|
|
229
|
+
*/
|
|
230
|
+
const scannedTargets = /* @__PURE__ */ new Set();
|
|
204
231
|
/** Absolute ids (normalized) of the scanned server function files. */
|
|
205
232
|
const scannedServerFiles = /* @__PURE__ */ new Set();
|
|
206
233
|
const EXACT_NAMES = [
|
|
@@ -221,7 +248,11 @@ const EXACT_NAMES = [
|
|
|
221
248
|
* @param devServer - Optional running Vite dev server instance; when provided, skips creating a new one
|
|
222
249
|
*/
|
|
223
250
|
const scanForServerFiles = async (initialCfg, devServer) => {
|
|
224
|
-
|
|
251
|
+
const root = initialCfg?.root || process.cwd();
|
|
252
|
+
const resolvedScanRoot = resolve(root, initialCfg?.scanRoot ?? join(root, "src", "api"));
|
|
253
|
+
const serverFiles = initialCfg?.serverFiles ?? "exact";
|
|
254
|
+
const target = `${resolvedScanRoot}|${serverFiles}|${initialCfg?.rpcPrefix ?? "__rpc"}`;
|
|
255
|
+
if (scannedTargets.has(target) && !devServer) return;
|
|
225
256
|
let createServer;
|
|
226
257
|
let normalizePath;
|
|
227
258
|
try {
|
|
@@ -229,7 +260,7 @@ const scanForServerFiles = async (initialCfg, devServer) => {
|
|
|
229
260
|
} catch {
|
|
230
261
|
return;
|
|
231
262
|
}
|
|
232
|
-
const config = !initialCfg
|
|
263
|
+
const config = !initialCfg ? {
|
|
233
264
|
root: process.cwd(),
|
|
234
265
|
base: process.env.BASE || "/",
|
|
235
266
|
server: { middlewareMode: true }
|
|
@@ -247,9 +278,6 @@ const scanForServerFiles = async (initialCfg, devServer) => {
|
|
|
247
278
|
optimizeDeps: { noDiscovery: true },
|
|
248
279
|
ssr: { optimizeDeps: { noDiscovery: true } }
|
|
249
280
|
});
|
|
250
|
-
const root = config.root || process.cwd();
|
|
251
|
-
const resolvedScanRoot = resolve(root, config.scanRoot ?? join(root, "src", "api"));
|
|
252
|
-
const serverFiles = config.serverFiles ?? "exact";
|
|
253
281
|
const seenNames = /* @__PURE__ */ new Set();
|
|
254
282
|
let files;
|
|
255
283
|
try {
|
|
@@ -271,7 +299,7 @@ const scanForServerFiles = async (initialCfg, devServer) => {
|
|
|
271
299
|
const moduleEntries = Object.entries(moduleExports);
|
|
272
300
|
if (!moduleEntries.length) {
|
|
273
301
|
console.warn(NO_SERVER_FUNCTION_FOUND);
|
|
274
|
-
|
|
302
|
+
continue;
|
|
275
303
|
}
|
|
276
304
|
for (const [exportName, exportValue] of moduleEntries) {
|
|
277
305
|
const registeredName = exportValue.name;
|
|
@@ -296,7 +324,7 @@ const scanForServerFiles = async (initialCfg, devServer) => {
|
|
|
296
324
|
}
|
|
297
325
|
} finally {
|
|
298
326
|
if (!devServer && server) await server.close();
|
|
299
|
-
|
|
327
|
+
scannedTargets.add(target);
|
|
300
328
|
}
|
|
301
329
|
};
|
|
302
330
|
//#endregion
|
|
@@ -319,6 +347,20 @@ const loadConfigFile = async (env, file) => {
|
|
|
319
347
|
};
|
|
320
348
|
let RPCConfig;
|
|
321
349
|
/**
|
|
350
|
+
* Merges a loaded config file over the built-in defaults, recording the
|
|
351
|
+
* resolved file path. Assigns the module-level `RPCConfig` cache.
|
|
352
|
+
* @param config - The config object exported by the config file
|
|
353
|
+
* @param configFilePath - Absolute path of the config file that was loaded
|
|
354
|
+
* @returns The merged RPC plugin options
|
|
355
|
+
*/
|
|
356
|
+
const mergeLoaded = (config, configFilePath) => {
|
|
357
|
+
RPCConfig = mergeConfig({
|
|
358
|
+
...defaultRPCOptions,
|
|
359
|
+
configFile: configFilePath
|
|
360
|
+
}, config);
|
|
361
|
+
return RPCConfig;
|
|
362
|
+
};
|
|
363
|
+
/**
|
|
322
364
|
* Loads the RPC configuration by searching for config files in the project root.
|
|
323
365
|
* Searches in order: `rpc.config.ts`, `rpc.config.js`, `rpc.config.mjs`, `rpc.config.mts`,
|
|
324
366
|
* `.rpcrc.ts`, `.rpcrc.js`. Falls back to defaults if none found.
|
|
@@ -327,6 +369,10 @@ let RPCConfig;
|
|
|
327
369
|
* @returns Resolved RPC plugin options
|
|
328
370
|
*/
|
|
329
371
|
const loadRPCConfig = async (configFile, opts) => {
|
|
372
|
+
if (typeof configFile === "object" && configFile !== null) {
|
|
373
|
+
opts = configFile;
|
|
374
|
+
configFile = void 0;
|
|
375
|
+
}
|
|
330
376
|
try {
|
|
331
377
|
const env = {
|
|
332
378
|
command: "serve",
|
|
@@ -351,11 +397,7 @@ const loadRPCConfig = async (configFile, opts) => {
|
|
|
351
397
|
}
|
|
352
398
|
const result = await loadConfigFile(env, configFile);
|
|
353
399
|
if (result && typeof result === "object") {
|
|
354
|
-
|
|
355
|
-
...defaultRPCOptions,
|
|
356
|
-
configFile: configFilePath
|
|
357
|
-
}, result.config);
|
|
358
|
-
setGlobalPrefix(RPCConfig.rpcPrefix);
|
|
400
|
+
setGlobalPrefix(mergeLoaded(result.config, configFilePath).rpcPrefix);
|
|
359
401
|
return RPCConfig;
|
|
360
402
|
}
|
|
361
403
|
RPCConfig = defaultRPCOptions;
|
|
@@ -369,10 +411,7 @@ const loadRPCConfig = async (configFile, opts) => {
|
|
|
369
411
|
if (!existsSync(configFilePath)) continue;
|
|
370
412
|
const result = await loadConfigFile(env, file);
|
|
371
413
|
if (result) {
|
|
372
|
-
|
|
373
|
-
...defaultRPCOptions,
|
|
374
|
-
configFile: configFilePath
|
|
375
|
-
}, result.config);
|
|
414
|
+
setGlobalPrefix(mergeLoaded(result.config, configFilePath).rpcPrefix);
|
|
376
415
|
return RPCConfig;
|
|
377
416
|
}
|
|
378
417
|
}
|
|
@@ -407,7 +446,6 @@ function rpcPlugin(devOptions = {}) {
|
|
|
407
446
|
},
|
|
408
447
|
async configureServer(server) {
|
|
409
448
|
viteServer = server;
|
|
410
|
-
const { adapter: _adapter, ...rest } = options;
|
|
411
449
|
if (serverFunctionsMap.size === 0) {
|
|
412
450
|
const scanCfg = {
|
|
413
451
|
...config,
|
|
@@ -417,11 +455,11 @@ function rpcPlugin(devOptions = {}) {
|
|
|
417
455
|
};
|
|
418
456
|
await scanForServerFiles(scanCfg, viteServer);
|
|
419
457
|
}
|
|
420
|
-
server.middlewares.use(createRPCMiddleware(
|
|
458
|
+
server.middlewares.use(createRPCMiddleware(options));
|
|
421
459
|
},
|
|
422
460
|
async buildStart() {
|
|
423
461
|
const viteVersion = this.meta?.viteVersion;
|
|
424
|
-
isOxc = Number(viteVersion
|
|
462
|
+
isOxc = Number.parseInt(viteVersion, 10) >= 8;
|
|
425
463
|
if (!viteServer && config) {
|
|
426
464
|
const scanCfg = {
|
|
427
465
|
...config,
|
|
@@ -433,8 +471,7 @@ function rpcPlugin(devOptions = {}) {
|
|
|
433
471
|
}
|
|
434
472
|
},
|
|
435
473
|
async transform(code, id, ops) {
|
|
436
|
-
if (!code.includes("createServerFunction") || ops?.ssr
|
|
437
|
-
const vite = await import("vite");
|
|
474
|
+
if (!code.includes("createServerFunction") || ops?.ssr) return null;
|
|
438
475
|
if (serverFunctionsMap.size === 0) {
|
|
439
476
|
const scanCfg = {
|
|
440
477
|
...config,
|
|
@@ -448,10 +485,7 @@ function rpcPlugin(devOptions = {}) {
|
|
|
448
485
|
if (!scannedServerFiles.has(idPath)) return null;
|
|
449
486
|
const transformer = isOxc ? "transformWithOxc" : "transformWithEsbuild";
|
|
450
487
|
const langProp = isOxc ? "lang" : "loader";
|
|
451
|
-
const source = getClientModules({
|
|
452
|
-
rpcPrefix: options.rpcPrefix,
|
|
453
|
-
adapter: options.adapter
|
|
454
|
-
});
|
|
488
|
+
const source = getClientModules({ rpcPrefix: options.rpcPrefix });
|
|
455
489
|
const result = await vite[transformer](source, id, {
|
|
456
490
|
[langProp]: "js",
|
|
457
491
|
sourcemap: true
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/options.ts","../src/constants.ts","../src/functionsMap.ts","../src/validate.ts","../src/getClientModules.ts","../src/server-helpers.ts","../src/scanForServerFiles.ts","../src/index.ts"],"sourcesContent":["import type {\n MiddlewareOptions,\n RpcPluginOptions,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\n\nexport const defaultServerFnOptions: ServerFunctionOptions = {\n contentType: \"application/json\",\n credentials: \"same-origin\",\n method: \"POST\",\n};\n\nexport const defaultPrefix = \"__rpc\";\n\nexport const defaultRPCOptions: RpcPluginOptions = {\n rpcPrefix: defaultPrefix,\n adapter: \"express\",\n serverFiles: \"exact\",\n scanRoot: undefined,\n};\n\nexport const defaultMiddlewareOptions: MiddlewareOptions = {\n rpcPrefix: undefined,\n path: undefined,\n origin: undefined,\n};\n","export const OPERATION_ABORTED = \"Operation aborted\";\n\nexport const REQUEST_CANCELLED = \"Request was cancelled\";\n\nexport const FETCH_ERROR_PREFIX = \"Fetch error: \";\n\nexport const NO_SERVER_FUNCTION_FOUND = \"No server function found.\";\n\nexport const ERROR_LOADING_FILE = \"Error loading file:\";\n\nexport const FUNCTION_NOT_FOUND = \"Function not found\";\n\nexport const METHOD_NOT_ALLOWED = \"Method Not Allowed\";\n\nexport const REQUEST_FORBIDDEN = \"Forbidden\";\n\nexport const UNSUPPORTED_MEDIA_TYPE = \"Unsupported Media Type\";\n\nexport const BAD_REQUEST = \"Bad Request\";\n\nexport const INTERNAL_SERVER_ERROR = \"Internal Server Error\";\n\nexport const CLIENT_DISCONNECTED = \"client disconnected\";\n\n/** Returns a warning when a middleware name is reused, preventing registration conflicts. @param name - The duplicate middleware name */\nexport const MIDDLEWARE_NAME_USED = (name: string) =>\n `The middleware name \"${name}\" is already used.`;\n\n/** Error message when a value fails the safe-identifier validation. @param label - What kind of value was being validated. @param name - The rejected value */\nexport const INVALID_IDENTIFIER = (label: string, name: string) =>\n `Invalid ${label}: \"${name}\" must match /^[A-Za-z_$][A-Za-z0-9_$]*$/`;\n\n/** Error message when a value fails the safe-path-segment validation. @param label - What kind of value was being validated. @param segment - The rejected value */\nexport const INVALID_PATH_SEGMENT = (label: string, segment: string) =>\n `Invalid ${label}: \"${segment}\" must match /^[A-Za-z0-9_$@:][A-Za-z0-9_$@:/-]*$/`;\n\n/** Warning message when a specified RPC config file cannot be resolved on disk. @param configFile - The requested config filename. @param configFilePath - The resolved absolute path */\nexport const CONFIG_FILE_NOT_FOUND = (\n configFile: string,\n configFilePath: string,\n) =>\n ` ⚠︎ The specified RPC config file ${configFile} cannot be found at ${configFilePath}, loading the defaults..`;\n\nexport const NO_CONFIG_FOUND =\n ` ⚡︎ No RPC config found, loading the defaults..`;\n\nexport const FAILED_LOAD_CONFIG = ` ⚠︎ Failed to load RPC config:`;\n\n/** Error template for duplicate server function names across files. @param name - The duplicate registered name */\nexport const DUPLICATE_FUNCTION_NAME = (name: string) =>\n `Duplicate server function \"${name}\" detected. Each server function must have a unique name. Remove or rename the duplicate.`;\n","import type { ServerFnEntry } from \"./types.d.ts\";\nimport { defaultPrefix } from \"./options.ts\";\n\n/**\n * Global symbol under which the shared `serverFunctionsByPrefix` map is stored\n * on `globalThis`. Keeping it on a `Symbol.for` key makes it instance-stable\n * across the bundled entry copies (`index.mjs`, `server.mjs`, `express.mjs`,\n * ...) and dev-server hot reloads, exactly like the request-context storage in\n * `context.ts`. Without this, `scanForServerFiles` (bundled into the plugin)\n * would populate a map copy the adapter middleware could not read.\n */\nconst functionsMapSymbol = Symbol.for(\"thednp.rpc.functionsMap\");\n\n/**\n * Map of rpcPrefix -> Map of function names -> ServerFnEntry\n * Enables multiple RPC instances with different prefixes to coexist\n * without name collisions.\n */\nexport const serverFunctionsByPrefix: Map<\n string,\n Map<string, ServerFnEntry>\n> = ((globalThis as Record<symbol, Map<string, Map<string, ServerFnEntry>>>)[\n functionsMapSymbol\n] ??= new Map());\n\n/**\n * Gets or creates the function map for a specific prefix.\n * @param prefix - The RPC prefix (e.g., \"__rpc\", \"v1:rpc\", \"admin:rpc\")\n * @returns Map of function names to ServerFnEntry for that prefix\n */\nexport const getFunctionsForPrefix = (\n prefix: string,\n): Map<string, ServerFnEntry> => {\n if (!serverFunctionsByPrefix.has(prefix)) {\n serverFunctionsByPrefix.set(prefix, new Map());\n }\n return serverFunctionsByPrefix.get(prefix)!;\n};\n\n/**\n * Backward compatibility: default map for the default prefix.\n * Legacy code can still use serverFunctionsMap.set(name, entry).\n */\nexport const serverFunctionsMap: Map<string, ServerFnEntry> = {\n get: (key: string) => getFunctionsForPrefix(defaultPrefix).get(key),\n set: (key: string, value: ServerFnEntry) =>\n getFunctionsForPrefix(defaultPrefix).set(key, value),\n has: (key: string) => getFunctionsForPrefix(defaultPrefix).has(key),\n delete: (key: string) => getFunctionsForPrefix(defaultPrefix).delete(key),\n clear: () => getFunctionsForPrefix(defaultPrefix).clear(),\n get size() {\n return getFunctionsForPrefix(defaultPrefix).size;\n },\n entries: () => getFunctionsForPrefix(defaultPrefix).entries(),\n keys: () => getFunctionsForPrefix(defaultPrefix).keys(),\n values: () => getFunctionsForPrefix(defaultPrefix).values(),\n forEach: (\n callback: (\n value: ServerFnEntry,\n key: string,\n map: Map<string, ServerFnEntry>,\n ) => void,\n ) => getFunctionsForPrefix(defaultPrefix).forEach(callback),\n [Symbol.iterator]: () =>\n getFunctionsForPrefix(defaultPrefix)[Symbol.iterator](),\n} as unknown as Map<string, ServerFnEntry>;\n","import type { Credentials } from \"./types.d.ts\";\nimport { INVALID_IDENTIFIER, INVALID_PATH_SEGMENT } from \"./constants.ts\";\n\nconst SAFE_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;\nconst SAFE_PATH_SEGMENT = /^[A-Za-z0-9_$@:][A-Za-z0-9_$@:/-]*$/;\nconst CREDENTIALS_VALUES: readonly Credentials[] = [\n \"same-origin\",\n \"include\",\n \"omit\",\n];\n\n/**\n * Validates that a string is a safe JavaScript identifier.\n * Used to prevent code injection when interpolating export names into generated client code.\n * @param name - The string to validate\n * @param label - Human-readable label for error messages (e.g. \"export name\")\n * @returns The validated name if it passes\n * @throws Error if the name contains characters outside /^[A-Za-z_$][A-Za-z0-9_$]*$/\n */\nexport function validateIdentifier(name: string, label: string): string {\n if (!SAFE_IDENTIFIER.test(name)) {\n throw new Error(INVALID_IDENTIFIER(label, name));\n }\n return name;\n}\n\n/**\n * Validates that a string is a safe path segment for RPC routing.\n * Allows alphanumeric characters, underscores, dollar signs, at signs,\n * colons, hyphens, and forward slashes.\n * @param segment - The string to validate\n * @param label - Human-readable label for error messages (e.g. \"rpcPrefix\")\n * @returns The validated segment if it passes\n * @throws Error if the segment contains disallowed characters\n */\nexport function validatePathSegment(segment: string, label: string): string {\n if (!SAFE_PATH_SEGMENT.test(segment)) {\n throw new Error(INVALID_PATH_SEGMENT(label, segment));\n }\n return segment;\n}\n\n/**\n * Validates and normalizes the credentials option.\n * Accepts \"same-origin\", \"include\", or \"omit\"; defaults to \"same-origin\" when undefined.\n * @param value - Credentials value to validate\n * @returns The validated credentials string\n * @throws Error if the value is not one of the accepted credentials\n */\nexport function validateCredentials(value?: string): Credentials {\n const creds = value || \"same-origin\";\n if (!CREDENTIALS_VALUES.includes(creds as Credentials)) {\n throw new Error(\n `Invalid credentials: \"${value}\" must be one of ${\n CREDENTIALS_VALUES.join(\", \")\n }`,\n );\n }\n return creds as Credentials;\n}\n\n/**\n * Validates and normalizes the HTTP method option for a server function.\n * Accepts \"GET\" or \"POST\" (case-insensitive); defaults to \"POST\" when undefined.\n * @param value - Method value to validate\n * @returns The validated uppercase method string\n * @throws Error if the value is not \"GET\" or \"POST\"\n */\nexport function validateMethod(value?: string): \"GET\" | \"POST\" {\n const method = (value || \"POST\").toUpperCase();\n if (method !== \"GET\" && method !== \"POST\") {\n throw new Error(`Invalid method: \"${value}\" must be one of GET, POST`);\n }\n return method;\n}\n","/**\n * @module Client module generation.\n */\nimport type {\n RpcPluginOptionsInternal,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\nimport { getFunctionsForPrefix } from \"./functionsMap.ts\";\nimport {\n validateCredentials,\n validateIdentifier,\n validateMethod,\n validatePathSegment,\n} from \"./validate.ts\";\n\n/**\n * Generates a JavaScript client module string for a single server function.\n * All interpolated values are validated to prevent code injection.\n * @param fnName - Registered RPC function name (validated as path segment)\n * @param fnEntry - Export name used in the generated module (validated as identifier)\n * @param options - Content type, credentials, and RPC prefix settings\n * @returns A string of JavaScript code exporting the client stub\n */\nconst getModule = (\n fnName: string,\n fnEntry: string,\n options: Partial<ServerFunctionOptions> & {\n contentType: ServerFunctionOptions[\"contentType\"];\n rpcPrefix: string;\n },\n): string => {\n // Validate all interpolated strings to prevent code injection\n const safeFnName = validatePathSegment(fnName, \"function name\");\n const safeFnEntry = validateIdentifier(fnEntry, \"export name\");\n const safePrefix = validatePathSegment(options.rpcPrefix, \"rpcPrefix\");\n const credentials = validateCredentials(options.credentials);\n const method = validateMethod(options.method);\n const contentType =\n (options.contentType ?? \"application/json\") as ServerFunctionOptions[\n \"contentType\"\n ];\n\n const opts: string[] = [];\n if (method !== \"POST\") opts.push(`method: \"${method}\"`);\n if (credentials !== \"same-origin\") opts.push(`credentials: \"${credentials}\"`);\n if (contentType !== \"application/json\") {\n opts.push(`contentType: \"${contentType}\"`);\n }\n const optsStr = opts.length ? `, { ${opts.join(\", \")} }` : \"\";\n\n const output = `\n export const ${safeFnEntry} = getClientStub(\"${safePrefix}\", \"${safeFnName}\"${optsStr});`;\n\n return output.trim();\n};\n\n/**\n * Generates the complete client-side module bundle by iterating all registered server functions\n * for a specific prefix and producing fetch-based stubs for each. The result is transformed by Vite\n * (or Oxc) during the dev server or production build.\n * @param initialOptions - Plugin options containing rpcPrefix and optional adapter\n * @returns A string of JavaScript code with all client RPC modules and their import dependencies\n */\nexport const getClientModules = (\n initialOptions: RpcPluginOptionsInternal,\n): string => {\n // Validate prefix once at the top level\n validatePathSegment(initialOptions.rpcPrefix, \"rpcPrefix\");\n\n // Get functions registered for this specific prefix\n const prefixMap = getFunctionsForPrefix(initialOptions.rpcPrefix);\n const entries = Array.from(prefixMap.entries())\n .filter(([, entry]) => entry.exportName)\n .map(([registeredName, entry]) =>\n getModule(registeredName, entry.exportName!, {\n ...initialOptions,\n ...((entry.options as ServerFunctionOptions) || {}),\n })\n )\n .join(\"\\n\");\n\n const output = `\n// Client-side RPC modules for prefix: ${initialOptions.rpcPrefix}\nimport { getClientStub } from \"@thednp/rpc/helpers\";\n${entries}`;\n\n return output.trim();\n};\n","/** @module Server-side utilities. Exports the `RPCError` class for typed server-side errors, `formatError` for middleware error responses, `isFormContentType` and `hasContentTypeMismatch` for content-type validation, and `walkGlobFiles` for recursively discovering `*.server.*` files. Never import this module in client code — it is server-only. */\nimport type { ContentType, JsonObject, JsonValue } from \"./types.d.ts\";\nimport { readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport { INTERNAL_SERVER_ERROR } from \"./constants.ts\";\n\nconst GLOB_REGEX = /^.+\\.server\\.(ts|js|mjs|mts)$/;\n\n/**\n * Recursively walks `dir` and collects absolute paths to files whose\n * basename matches the `*.server.{ts,js,mjs,mts}` glob pattern.\n */\nexport const walkGlobFiles = async (dir: string): Promise<string[]> => {\n const results: string[] = [];\n const stack = [dir];\n while (stack.length) {\n const current = stack.pop()!;\n let entries;\n try {\n entries = await readdir(current, { withFileTypes: true });\n } catch (_e) {\n continue;\n }\n for (const entry of entries) {\n const fullPath = join(current, entry.name);\n if (entry.isFile() && GLOB_REGEX.test(entry.name)) {\n results.push(fullPath);\n } else if (entry.isDirectory()) {\n stack.push(fullPath);\n }\n }\n }\n return results;\n};\n\n/**\n * A typed error thrown from server functions.\n * The middleware serializes the `message` and `code` in the response,\n * allowing clients to recognise and handle specific error conditions.\n */\nexport class RPCError extends Error {\n /** Machine-readable error code (e.g. \"VALIDATION_FAILED\", \"UNAUTHORIZED\") */\n code: string;\n /** Optional diagnostic payload */\n data?: JsonValue;\n constructor(message: string, code = \"INTERNAL\", data?: JsonValue) {\n super(message);\n this.name = \"RPCError\";\n this.code = code;\n this.data = data;\n }\n}\n\n/**\n * Formats an error for the RPC middleware response.\n * In development the full `RPCError` payload is included so developers\n * can quickly identify issues. Unexpected exceptions never expose their\n * message — only the generic \"Internal Server Error\" is sent, preventing\n * information disclosure; server-side diagnostics are preserved via the\n * middleware's `console.error` logging.\n */\nexport const formatError = (\n err: unknown,\n isProduction: boolean,\n): JsonObject => {\n if (isProduction) {\n return { error: INTERNAL_SERVER_ERROR };\n }\n if (err instanceof RPCError) {\n const payload: JsonObject = {\n error: err.message || INTERNAL_SERVER_ERROR,\n code: err.code,\n };\n if (err.data !== undefined) payload.data = err.data;\n return payload;\n }\n return { error: INTERNAL_SERVER_ERROR };\n};\n\n/**\n * Checks whether a content type maps to a form encoding\n * (`multipart/form-data` or `application/x-www-form-urlencoded`).\n * Form-declared functions accept either encoding so native browser\n * submissions (urlencoded) keep working without JavaScript.\n */\nexport const isFormContentType = (contentType: string): boolean =>\n contentType === \"multipart/form-data\" ||\n contentType === \"application/x-www-form-urlencoded\";\n\n/**\n * Detects whether an incoming request's `Content-Type` header conflicts\n * with the function's declared content type. JSON and text functions are\n * enforced strictly (exact match wins), while form functions accept both\n * form encodings because the nojs fallback submits urlencoded forms to\n * multipart-declared endpoints. Requests without a `Content-Type` header\n * (curl, GET, legacy clients) are exempt from enforcement.\n * @param declared - The declared `contentType` from the server function options\n * @param rawHeader - The raw `Content-Type` request header, if present\n */\nexport const hasContentTypeMismatch = (\n declared: ContentType,\n rawHeader: string | undefined,\n): boolean => {\n // No Content-Type header → exempt (url bar, GET, curl compatibility)\n if (!rawHeader) return false;\n // Strip parameters (charset, boundary) before comparison\n const incomingType = rawHeader.trim().toLowerCase().split(\";\")[0].trim();\n if (isFormContentType(declared)) {\n // Forms: reject only non-form encodings (lenient between the two)\n return !isFormContentType(incomingType);\n }\n return incomingType !== declared;\n};\n\n/**\n * Decides whether a request may proceed, given the configured origin allowlist\n * and the two headers a browser can be made to reveal.\n *\n * Three tiers, evaluated in order — the first tier with a signal decides:\n *\n * 1. `origin` option unset → everything passes. No validation is performed.\n * 2. `Origin` present → the allowlist decides, exactly as {@link isOriginAllowed}.\n * 3. `Origin` absent but `Sec-Fetch-Site` present → allow only `same-origin`\n * and `none`; anything else (including an unrecognised value) is rejected.\n * 4. Both absent → passes. This is the deliberate, documented curl/native hole.\n *\n * Tier 2 must short-circuit ahead of tier 3. `Sec-Fetch-Site` is a coarse\n * four-value enum that cannot name a host, so on its own it would reject a\n * legitimate request from an allowlisted sibling subdomain (`same-site`). The\n * allowlist exists precisely to admit that case, and it can only do so while\n * `Origin` survives. `Sec-Fetch-Site` earns a vote only once the precise signal\n * has been stripped away by something in the chain — at which point there is\n * nothing left to trust, so it fails closed.\n *\n * Browsers never strip `Origin` themselves, so tier 3 can only fire when a\n * proxy, sanitising middleware, or misconfigured CDN removed it. No legitimate\n * browser request can regress.\n *\n * An empty (or whitespace-only) header value counts as **absent**, not as an\n * unrecognised signal. No browser emits an empty `Sec-Fetch-Site`, and adapters\n * disagree on what their header accessor returns for a missing header (Node's\n * `req.headers` yields `undefined`, Hono's `c.req.header()` may yield `\"\"`).\n * Normalising here keeps all five adapters behaving identically instead of\n * inheriting whichever convention their framework happens to use.\n * @param allowed - The configured `origin` option, if any\n * @param origin - The raw `Origin` request header, if present\n * @param site - The raw `Sec-Fetch-Site` request header, if present\n * @returns `true` when the request may proceed\n */\nexport const isOriginRequestAllowed = (\n allowed: string | string[] | undefined,\n origin: string | undefined,\n site: string | undefined,\n): boolean => {\n if (!allowed) return true; // tier 1 — the check is opt-in\n if (origin?.trim()) return isOriginAllowed(allowed, origin); // tier 2 — precise\n if (!site?.trim()) return true; // tier 4 — curl / native client\n // tier 3 — precision lost, so fail closed\n return site === \"same-origin\" || site === \"none\";\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\n/**\n * Decides whether a request's `Origin` header is allowed by the configured\n * allowlist. Shared by all five adapters so the rule lives in exactly one place.\n *\n * - No `allowed` value (option unset) → everything passes: no validation.\n * - No `requestOrigin` header → passes, preserving curl/native-client access.\n * - Otherwise the header must match one of the entries exactly.\n *\n * A single string and a one-element array behave identically, so widening\n * `origin` to `string | string[]` is backward compatible.\n *\n * `Origin: null` (sandboxed iframes, `file://`, extension pages) is rejected\n * whenever an allowlist is set, because it never equals a real origin.\n * @param allowed - The configured `origin` option, if any\n * @param requestOrigin - The raw `Origin` request header, if present\n * @returns `true` when the request may proceed\n */\nexport const isOriginAllowed = (\n allowed: string | string[] | undefined,\n requestOrigin: string | undefined,\n): boolean => {\n if (!allowed || !requestOrigin) return true;\n return Array.isArray(allowed)\n ? allowed.includes(requestOrigin)\n : requestOrigin === allowed;\n};\n\nconst SAFE_URL_BASE = \"http://localhost\";\n\n/**\n * Parses a raw request URL against a fixed base without ever throwing.\n * Malformed request-targets (e.g. `/\\`, `//`, `/\\/`) make the WHATWG URL\n * parser throw `TypeError: Invalid URL`; the adapters call this while\n * building the per-request URL **before** their dispatch `try` block, so an\n * unhandled rejection there crashes raw `node:http` hosts (and Express 4).\n * On failure we fall back to the base root: the resulting pathname never\n * matches the RPC prefix, so the request is treated as non-RPC and falls\n * through to `next()` / 404 instead of crashing the process.\n * @param rawUrl - Raw request URL (path + optional query string)\n * @param base - Optional base URL, defaults to a fixed localhost origin\n * @returns A URL object; never throws\n */\nexport const safeURL = (rawUrl: string, base = SAFE_URL_BASE): URL => {\n try {\n return new URL(rawUrl, base);\n } catch {\n return new URL(\"/\", base);\n }\n};\n\nconst globalPrefixSymbol = Symbol.for(\"thednp.rpc.globalPrefix\");\n\n/** Global rpcPrefix from the last loaded config / middleware — fallback for functions without explicit prefix. */\nexport const getGlobalPrefix = (): string | undefined =>\n (globalThis as unknown as Record<symbol, string | undefined>)[\n globalPrefixSymbol\n ];\n\nexport const setGlobalPrefix = (prefix: string | undefined): void => {\n if (prefix) {\n (globalThis as unknown as Record<symbol, string | undefined>)[\n globalPrefixSymbol\n ] = prefix;\n } else {\n delete (globalThis as unknown as Record<symbol, string | undefined>)[\n globalPrefixSymbol\n ];\n }\n};\n","import type { ViteDevServer } from \"vite\";\nimport type { ClientFunctionWithOptions, ScanConfig } from \"./types.d.ts\";\nimport { readdir } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\nimport process from \"node:process\";\n\nimport { getFunctionsForPrefix } from \"./functionsMap.ts\";\nimport { defaultPrefix } from \"./options.ts\";\nimport { walkGlobFiles } from \"./server-helpers.ts\";\nimport {\n DUPLICATE_FUNCTION_NAME,\n ERROR_LOADING_FILE,\n NO_SERVER_FUNCTION_FOUND,\n} from \"./constants.ts\";\n\nlet isScanned = false;\n\n/** Absolute ids (normalized) of the scanned server function files. */\nexport const scannedServerFiles: Set<string> = new Set<string>();\n\nconst EXACT_NAMES = [\"server.ts\", \"server.js\", \"server.mjs\", \"server.mts\"];\n\n/**\n * Scans `src/api/` (or an explicit `scanRoot`) for server function files\n * and populates the server functions map (scoped by rpcPrefix) with their exported functions.\n * Uses Vite's SSR module loading to resolve and execute each file.\n *\n * Supports two matching modes via `config.serverFiles`:\n * `\"exact\"` — classic `server.ts|js|mjs|mts` names in the api directory\n * `\"glob\"` — recursively walking `scanRoot` to match `*.server.{ts,js,mjs,mts}`\n * @param initialCfg - Optional Vite config overrides (root, base, server, serverFiles, scanRoot)\n * @param devServer - Optional running Vite dev server instance; when provided, skips creating a new one\n */\nexport const scanForServerFiles = async (\n initialCfg?: ScanConfig,\n devServer?: ViteDevServer,\n): Promise<void> => {\n if (isScanned && !devServer) {\n return;\n }\n // Vite is only needed to spin up the internal dev server that loads the\n // server function files, so it is imported lazily rather than at the top\n // of the module. This keeps consumers of the standalone server entry that\n // register their functions directly (e.g. serverless functions bundling\n // the API module) free of a static Vite dependency — when Vite is\n // externalized by the function bundler, this lazy import is left as a\n // runtime require that is never executed.\n let createServer: typeof import(\"vite\").createServer;\n let normalizePath: typeof import(\"vite\").normalizePath;\n try {\n ({ createServer, normalizePath } = await import(\"vite\"));\n } catch {\n // Vite is not installed in this environment (e.g. a serverless bundle\n // where Vite is externalized or absent). Server functions must have been\n // imported directly into the prefix-scoped map; nothing to scan — exit\n // gracefully instead of crashing the host's cold start with\n // `Cannot find module 'vite'`.\n return;\n }\n const config = (!initialCfg && !devServer) || !initialCfg\n ? {\n root: process.cwd(),\n base: process.env.BASE || \"/\",\n server: { middlewareMode: true },\n }\n : {\n ...initialCfg,\n };\n\n let server = devServer;\n if (!server) {\n server = await createServer({\n server: { ...config.server, ws: false },\n appType: \"custom\",\n base: config.base || \"/\",\n root: config.root || process.cwd(),\n // The internal server is only used to load the server function files:\n // skip the project config so its plugins (including this one) do not\n // re-trigger a nested scan via `configureServer`.\n configFile: false,\n // The internal server never serves a page or HMR, so no dependency\n // optimization or WebSocket server is needed. Without `ws: false`, the\n // middleware-mode server creates a standalone HMR WebSocket on port\n // 24678, and concurrent scans (e.g. the Express middleware's lazy scan\n // racing the plugin scan) fail with EADDRINUSE. Without `noDiscovery`,\n // the default optimizers scan the project entry and pre-bundle the whole\n // `vite` package (imported by the linked @thednp/rpc dist files),\n // hanging startup at 2+ GB RSS.\n optimizeDeps: { noDiscovery: true },\n ssr: { optimizeDeps: { noDiscovery: true } },\n });\n }\n\n const root = config.root || process.cwd();\n const resolvedScanRoot = resolve(\n root,\n config.scanRoot ?? join(root, \"src\", \"api\"),\n );\n const serverFiles: \"exact\" | \"glob\" = config.serverFiles ??\n \"exact\";\n\n // Names registered during this scan run, used for duplicate detection.\n // Keyed by `${prefix}:${registeredName}` so the same name can coexist\n // under different rpcPrefixes (see the registration loop below).\n const seenNames = new Set<string>();\n\n let files: string[];\n try {\n if (serverFiles === \"glob\") {\n files = await walkGlobFiles(resolvedScanRoot);\n } else {\n files = (await readdir(resolvedScanRoot, { withFileTypes: true }))\n .filter((f) => EXACT_NAMES.includes(f.name))\n .map((f) => join(resolvedScanRoot, f.name));\n }\n } catch (_e) {\n files = [];\n }\n\n try {\n for (const file of files) {\n scannedServerFiles.add(normalizePath(file));\n let moduleExports: Record<string, ClientFunctionWithOptions>;\n try {\n moduleExports = (await server.ssrLoadModule(file)) as Record<\n string,\n ClientFunctionWithOptions\n >;\n } catch (error) {\n console.error(ERROR_LOADING_FILE, file, error);\n continue;\n }\n const moduleEntries = Object.entries(moduleExports);\n if (!moduleEntries.length) {\n console.warn(NO_SERVER_FUNCTION_FOUND);\n return;\n }\n\n // Register each export into its prefix-scoped map, recording the\n // original export name so `getClientModules` can emit the matching\n // client stub. `createServerFunction` already auto-registers its name\n // into the appropriate prefix-scoped map at module load, so a function\n // may already exist here — in that case only the export name is added.\n // Track names seen in THIS scan run only, keyed by prefix: a name\n // repeated within one scan under the same prefix (e.g. two files\n // exporting the same function name) is a genuine conflict.\n for (const [exportName, exportValue] of moduleEntries) {\n const registeredName = exportValue.name;\n const prefix = exportValue.options?.rpcPrefix ||\n config.rpcPrefix ||\n defaultPrefix;\n const seenKey = `${prefix}:${registeredName}`;\n if (seenNames.has(seenKey)) {\n if (process.env.NODE_ENV !== \"production\") {\n throw new Error(DUPLICATE_FUNCTION_NAME(registeredName));\n }\n console.warn(DUPLICATE_FUNCTION_NAME(registeredName));\n continue;\n }\n seenNames.add(seenKey);\n const prefixMap = getFunctionsForPrefix(prefix);\n const existing = prefixMap.get(registeredName);\n if (existing) {\n existing.exportName = exportName;\n } else {\n prefixMap.set(registeredName, {\n name: registeredName,\n handler: exportValue,\n options: exportValue.options,\n exportName,\n });\n }\n }\n }\n } finally {\n if (!devServer && server) {\n await server.close();\n }\n isScanned = true;\n }\n};\n","/** @module Main entrypoint for the RPC Vite plugin. Exports `rpcPlugin` (default) and `loadRPCConfig`. For a Vite-free `defineConfig`, use `@thednp/rpc/config`. */\nimport type { ConfigEnv, Plugin, ResolvedConfig, ViteDevServer } from \"vite\";\nimport { loadConfigFromFile, mergeConfig } from \"vite\";\nimport { resolve } from \"node:path\";\nimport process from \"node:process\";\nimport { existsSync } from \"node:fs\";\nimport { defaultRPCOptions } from \"./options.ts\";\nimport type { RpcPluginOptions, ScanConfig } from \"./types.d.ts\";\nimport {\n CONFIG_FILE_NOT_FOUND,\n FAILED_LOAD_CONFIG,\n NO_CONFIG_FOUND,\n} from \"./constants.ts\";\n\nimport { getClientModules } from \"./getClientModules.ts\";\n// DEV server only\nimport {\n scanForServerFiles,\n scannedServerFiles,\n} from \"./scanForServerFiles.ts\";\nimport { serverFunctionsMap } from \"./functionsMap.ts\";\n\nimport { setGlobalPrefix } from \"@thednp/rpc/server\";\nimport { createRPCMiddleware } from \"@thednp/rpc/express\";\n\n/**\n * Loads and transforms a single RPC config file using Vite's config loader.\n * @param env - Vite config environment\n * @param file - Config file path (e.g. \"rpc.config.ts\")\n * @returns The loaded config augmented with the configFile path, or null on failure\n */\nconst loadConfigFile = async (env: ConfigEnv, file: string) => {\n const result = await loadConfigFromFile(env, file) as {\n path: string;\n config: Partial<RpcPluginOptions>;\n dependencies: string[];\n } | null;\n return result\n ? { ...result, config: { ...result.config, configFile: file } }\n : /* istanbul ignore next */ null;\n};\n\nlet RPCConfig: RpcPluginOptions;\n\n/**\n * Loads the RPC configuration by searching for config files in the project root.\n * Searches in order: `rpc.config.ts`, `rpc.config.js`, `rpc.config.mjs`, `rpc.config.mts`,\n * `.rpcrc.ts`, `.rpcrc.js`. Falls back to defaults if none found.\n * @param configFile - Optional explicit config file path; skips file search when provided\n * @param opts - Optional settings; `silent` suppresses the \"no config found\" warning\n * @returns Resolved RPC plugin options\n */\nconst loadRPCConfig: (\n configFile?: string,\n opts?: { silent?: boolean },\n) => Promise<RpcPluginOptions> = async (configFile?, opts?) => {\n try {\n // istanbul ignore next\n const env: ConfigEnv & { root: string } = {\n command: \"serve\",\n root: process.cwd(),\n mode: process.env.NODE_ENV || \"development\",\n };\n const defaultConfigFiles = [\n \"rpc.config.ts\",\n \"rpc.config.js\",\n \"rpc.config.mjs\",\n \"rpc.config.mts\",\n \".rpcrc.ts\",\n \".rpcrc.js\",\n ];\n\n // If specific config file provided\n if (configFile) {\n const configFilePath = resolve(env.root, configFile);\n if (!existsSync(configFilePath)) {\n console.warn(CONFIG_FILE_NOT_FOUND(configFile, configFilePath));\n RPCConfig = defaultRPCOptions;\n setGlobalPrefix(defaultRPCOptions.rpcPrefix);\n return defaultRPCOptions as RpcPluginOptions;\n }\n\n const result = await loadConfigFile(env, configFile);\n // istanbul ignore else\n if (result && typeof result === \"object\") {\n RPCConfig = mergeConfig(\n {\n ...defaultRPCOptions,\n configFile: configFilePath,\n },\n result.config,\n ) as RpcPluginOptions;\n\n setGlobalPrefix(RPCConfig.rpcPrefix);\n return RPCConfig;\n }\n // istanbul ignore next - this is a necessary fallback here\n RPCConfig = defaultRPCOptions;\n }\n\n if (RPCConfig !== undefined) {\n setGlobalPrefix(RPCConfig.rpcPrefix);\n\n return RPCConfig;\n }\n\n // Try default config files\n for (const file of defaultConfigFiles) {\n const configFilePath = resolve(env.root, file);\n // istanbul ignore else\n if (!existsSync(configFilePath)) {\n continue;\n }\n\n const result = await loadConfigFile(env, file);\n // istanbul ignore else\n if (result) {\n RPCConfig = mergeConfig(\n {\n ...defaultRPCOptions,\n configFile: configFilePath,\n },\n result.config,\n ) as RpcPluginOptions;\n\n return RPCConfig;\n }\n }\n RPCConfig = defaultRPCOptions;\n // Last call load defaults no matter what\n if (!opts?.silent) console.warn(NO_CONFIG_FOUND);\n } catch (error) {\n RPCConfig = defaultRPCOptions;\n console.warn(FAILED_LOAD_CONFIG, error);\n }\n\n setGlobalPrefix(RPCConfig.rpcPrefix);\n\n return RPCConfig;\n};\n\n/**\n * Vite plugin that enables automatic RPC generation.\n * Transforms server function imports into fetch-based client stubs during development and production builds.\n * In dev mode, attaches the RPC middleware to Vite's Connect server.\n * @param devOptions - Development-only overrides (merged on top of config file values)\n * @returns A Vite plugin object\n */\nfunction rpcPlugin(\n devOptions: Partial<RpcPluginOptions> = {},\n): Plugin {\n // Internal type - adapters are handled at runtime\n let options: RpcPluginOptions & { rpcPrefix: string } = mergeConfig(\n defaultRPCOptions,\n devOptions,\n ) as RpcPluginOptions;\n let config: ResolvedConfig;\n let viteServer: ViteDevServer;\n let isOxc = true;\n\n return {\n name: \"vite-plugin-universal-rpc\",\n enforce: \"pre\",\n // Plugin methods\n async configResolved(resolvedConfig) {\n const uniConfig = await loadRPCConfig(undefined, {\n silent: devOptions.silent,\n });\n options = mergeConfig(uniConfig, devOptions) as RpcPluginOptions;\n\n config = resolvedConfig;\n },\n async configureServer(server) {\n viteServer = server;\n const { adapter: _adapter, ...rest } = options;\n // istanbul ignore else\n if (serverFunctionsMap.size === 0) {\n const scanCfg: ScanConfig = {\n ...config,\n serverFiles: options.serverFiles,\n scanRoot: options.scanRoot,\n rpcPrefix: options.rpcPrefix,\n };\n await scanForServerFiles(scanCfg, viteServer);\n }\n\n // in dev mode we always use express/connect adapter\n server.middlewares.use(createRPCMiddleware(rest));\n },\n\n async buildStart() {\n const viteVersion = this.meta?.viteVersion;\n isOxc = Number(viteVersion[0]) >= 8;\n\n // Prepare the server functions\n if (!viteServer && config) {\n const scanCfg: ScanConfig = {\n ...config,\n serverFiles: options.serverFiles,\n scanRoot: options.scanRoot,\n rpcPrefix: options.rpcPrefix,\n };\n await scanForServerFiles(scanCfg);\n }\n },\n async transform(code: string, id: string, ops?: { ssr?: boolean }) {\n // Only transform files with server functions for client builds\n if (\n !code.includes(\"createServerFunction\") || // any other file is unchanged\n ops?.ssr || // file loaded on server remains unchanged\n (code.includes(\"createServerFunction\") &&\n typeof process === \"undefined\") // file loaded in client IS CHANGED\n ) {\n return null;\n }\n\n const vite = await import(\"vite\");\n\n if (serverFunctionsMap.size === 0) {\n const scanCfg: ScanConfig = {\n ...config,\n serverFiles: options.serverFiles,\n scanRoot: options.scanRoot,\n rpcPrefix: options.rpcPrefix,\n };\n await scanForServerFiles(scanCfg);\n }\n\n // Only transform modules that were scanned as server function files:\n // pages or docs mentioning `createServerFunction` in prose must not\n // be rewritten into the generated client bundle.\n const idPath = vite.normalizePath(id.split(\"?\")[0]);\n if (!scannedServerFiles.has(idPath)) {\n return null;\n }\n\n const transformer = isOxc ? \"transformWithOxc\" : \"transformWithEsbuild\";\n const langProp = isOxc ? \"lang\" : \"loader\";\n const source = getClientModules({\n rpcPrefix: options.rpcPrefix,\n adapter: options.adapter,\n });\n\n const result = await vite[transformer](source, id, {\n [langProp]: \"js\",\n sourcemap: true,\n // target: \"es2023\"\n });\n\n return {\n code: result.code,\n map: result.map\n ? typeof result.map === \"string\"\n ? JSON.parse(result.map)\n : /* istanbul ignore next @preserve */ result.map\n : /* istanbul ignore next @preserve */ null,\n };\n },\n } satisfies Plugin;\n}\n\nexport { rpcPlugin as default };\nexport { loadRPCConfig };\nexport type * from \"./types.d.ts\";\nexport {};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/options.ts","../src/constants.ts","../src/functionsMap.ts","../src/validate.ts","../src/getClientModules.ts","../src/server-helpers.ts","../src/scanForServerFiles.ts","../src/index.ts"],"sourcesContent":["import type {\n MiddlewareOptions,\n RpcPluginOptions,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\n\n/**\n * Defaults applied to a server function that declares no `method`,\n * `credentials`, or `contentType` of its own.\n */\nexport const defaultServerFnOptions: ServerFunctionOptions = {\n contentType: \"application/json\",\n credentials: \"same-origin\",\n method: \"POST\",\n};\n\n/**\n * The built-in RPC endpoint prefix, used when neither an explicit prefix nor a\n * global one (`getGlobalPrefix`) is supplied. Kept for backward compatibility\n * with pre-multi-prefix setups, where every function lived under this one map.\n */\nexport const defaultPrefix = \"__rpc\";\n\n/**\n * Baseline plugin options. `defineConfig` merges a user's partial config over\n * these, and `loadRPCConfig` merges a loaded config file over them, so every\n * option has a defined value even when a config file omits it.\n */\nexport const defaultRPCOptions: RpcPluginOptions = {\n rpcPrefix: defaultPrefix,\n serverFiles: \"exact\",\n scanRoot: undefined,\n};\n\n/**\n * Baseline middleware options. Note `rpcPrefix` is `undefined` rather than\n * `defaultPrefix` on purpose: leaving it unset lets `resolveRPCPrefix` fall\n * through to the global prefix, which is what makes a published global prefix\n * reach the middleware.\n */\nexport const defaultMiddlewareOptions: MiddlewareOptions = {\n rpcPrefix: undefined,\n path: undefined,\n origin: undefined,\n};\n","/**\n * @module User-facing message strings.\n *\n * Two shapes live here: plain message constants (the exact text an RPC\n * response body carries) and message *factories* for the cases that need a\n * value interpolated. Both are part of the wire contract for the bodies below,\n * so the casing is deliberate — e.g. a client matching on\n * `METHOD_NOT_ALLOWED` must see `\"Method Not Allowed\"`, not `\"Method not\n * allowed\"`. These strings are also what keeps error responses generic: they\n * never include the requested function name, so a response cannot be used to\n * enumerate what exists.\n */\n/** Thrown-name for an operation stopped by its own `cancel()`. */\nexport const OPERATION_ABORTED = \"Operation aborted\";\n\n/** Warning text used when a request is cancelled by an HTTP 408/499 response. */\nexport const REQUEST_CANCELLED = \"Request was cancelled\";\n\n/** Prefix of the `Error` message the client helpers throw for a non-OK HTTP response. The status text is appended; the response body is deliberately not read, so server-side detail never reaches the client through this path. */\nexport const FETCH_ERROR_PREFIX = \"Fetch error: \";\n\n/** Warning logged when a scanned server module exports nothing. */\nexport const NO_SERVER_FUNCTION_FOUND = \"No server function found.\";\n\n/** Error logged when a server function file cannot be loaded by Vite's SSR loader. */\nexport const ERROR_LOADING_FILE = \"Error loading file:\";\n\n/** Body of a 404. Deliberately does not name the requested function. */\nexport const FUNCTION_NOT_FOUND = \"Function not found\";\n\n/** Body of a 405, returned when the HTTP method does not match the function's declared method. */\nexport const METHOD_NOT_ALLOWED = \"Method Not Allowed\";\n\n/** Body of a 403, returned when the optional origin allowlist rejects the request. */\nexport const REQUEST_FORBIDDEN = \"Forbidden\";\n\n/** Body of a 415, returned when the request's `Content-Type` does not satisfy the function's declared `contentType`. */\nexport const UNSUPPORTED_MEDIA_TYPE = \"Unsupported Media Type\";\n\n/** Body of a 413, returned when the request body exceeds the host's configured size limit. */\nexport const PAYLOAD_TOO_LARGE = \"Payload Too Large\";\n\n/** Body of a 400, returned when a GET `?args=` value parses but is not an array. */\nexport const BAD_REQUEST = \"Bad Request\";\n\n/** Body of a 500. Always generic — never the underlying error, so internals cannot leak. */\nexport const INTERNAL_SERVER_ERROR = \"Internal Server Error\";\n\n/** Abort reason used when the client disconnects mid-dispatch. */\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\n/** Warning logged when no config file is discovered and the defaults are used. */\nexport const NO_CONFIG_FOUND =\n ` ⚡︎ No RPC config found, loading the defaults..`;\n\n/** Warning logged when a config file exists but could not be loaded; the defaults are used. */\nexport const FAILED_LOAD_CONFIG = ` ⚠︎ Failed to load RPC config:`;\n\n/** Error template for duplicate server function names across files. @param name - The duplicate registered name */\nexport const DUPLICATE_FUNCTION_NAME = (name: string) =>\n `Duplicate server function \"${name}\" detected. Each server function must have a unique name. Remove or rename the duplicate.`;\n","import type { ServerFnEntry } from \"./types.d.ts\";\nimport { defaultPrefix } from \"./options.ts\";\n\n/**\n * Global symbol under which the shared `serverFunctionsByPrefix` map is stored\n * on `globalThis`. Keeping it on a `Symbol.for` key makes it instance-stable\n * across the bundled entry copies (`index.mjs`, `server.mjs`, `express.mjs`,\n * ...) and dev-server hot reloads, exactly like the request-context storage in\n * `context.ts`. Without this, `scanForServerFiles` (bundled into the plugin)\n * would populate a map copy the adapter middleware could not read.\n */\nconst functionsMapSymbol = Symbol.for(\"thednp.rpc.functionsMap\");\n\n/**\n * Map of rpcPrefix -> Map of function names -> ServerFnEntry\n * Enables multiple RPC instances with different prefixes to coexist\n * without name collisions.\n */\nexport const serverFunctionsByPrefix: Map<\n string,\n Map<string, ServerFnEntry>\n> = ((globalThis as Record<symbol, Map<string, Map<string, ServerFnEntry>>>)[\n functionsMapSymbol\n] ??= new Map());\n\n/**\n * Gets or creates the function map for a specific prefix.\n * @param prefix - The RPC prefix (e.g., \"__rpc\", \"v1:rpc\", \"admin:rpc\")\n * @returns Map of function names to ServerFnEntry for that prefix\n */\nexport const getFunctionsForPrefix = (\n prefix: string,\n): Map<string, ServerFnEntry> => {\n if (!serverFunctionsByPrefix.has(prefix)) {\n serverFunctionsByPrefix.set(prefix, new Map());\n }\n return serverFunctionsByPrefix.get(prefix)!;\n};\n\n/**\n * Backward compatibility: default map for the default prefix.\n * Legacy code can still use serverFunctionsMap.set(name, entry).\n */\nexport const serverFunctionsMap: Map<string, ServerFnEntry> = {\n get: (key: string) => getFunctionsForPrefix(defaultPrefix).get(key),\n set: (key: string, value: ServerFnEntry) =>\n getFunctionsForPrefix(defaultPrefix).set(key, value),\n has: (key: string) => getFunctionsForPrefix(defaultPrefix).has(key),\n delete: (key: string) => getFunctionsForPrefix(defaultPrefix).delete(key),\n clear: () => getFunctionsForPrefix(defaultPrefix).clear(),\n get size() {\n return getFunctionsForPrefix(defaultPrefix).size;\n },\n entries: () => getFunctionsForPrefix(defaultPrefix).entries(),\n keys: () => getFunctionsForPrefix(defaultPrefix).keys(),\n values: () => getFunctionsForPrefix(defaultPrefix).values(),\n forEach: (\n callback: (\n value: ServerFnEntry,\n key: string,\n map: Map<string, ServerFnEntry>,\n ) => void,\n ) => getFunctionsForPrefix(defaultPrefix).forEach(callback),\n [Symbol.iterator]: () =>\n getFunctionsForPrefix(defaultPrefix)[Symbol.iterator](),\n} as unknown as Map<string, ServerFnEntry>;\n","import type { Credentials } from \"./types.d.ts\";\nimport { INVALID_IDENTIFIER, INVALID_PATH_SEGMENT } from \"./constants.ts\";\n\nconst SAFE_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;\nconst SAFE_PATH_SEGMENT = /^[A-Za-z0-9_$@:][A-Za-z0-9_$@:/-]*$/;\nconst CREDENTIALS_VALUES: readonly Credentials[] = [\n \"same-origin\",\n \"include\",\n \"omit\",\n];\n\n/**\n * Validates that a string is a safe JavaScript identifier.\n * Used to prevent code injection when interpolating export names into generated client code.\n * @param name - The string to validate\n * @param label - Human-readable label for error messages (e.g. \"export name\")\n * @returns The validated name if it passes\n * @throws Error if the name contains characters outside /^[A-Za-z_$][A-Za-z0-9_$]*$/\n */\nexport function validateIdentifier(name: string, label: string): string {\n if (!SAFE_IDENTIFIER.test(name)) {\n throw new Error(INVALID_IDENTIFIER(label, name));\n }\n return name;\n}\n\n/**\n * Validates that a string is a safe path segment for RPC routing.\n * Allows alphanumeric characters, underscores, dollar signs, at signs,\n * colons, hyphens, and forward slashes.\n * @param segment - The string to validate\n * @param label - Human-readable label for error messages (e.g. \"rpcPrefix\")\n * @returns The validated segment if it passes\n * @throws Error if the segment contains disallowed characters\n */\nexport function validatePathSegment(segment: string, label: string): string {\n if (!SAFE_PATH_SEGMENT.test(segment)) {\n throw new Error(INVALID_PATH_SEGMENT(label, segment));\n }\n return segment;\n}\n\n/**\n * Validates and normalizes the credentials option.\n * Accepts \"same-origin\", \"include\", or \"omit\"; defaults to \"same-origin\" when undefined.\n * @param value - Credentials value to validate\n * @returns The validated credentials string\n * @throws Error if the value is not one of the accepted credentials\n */\nexport function validateCredentials(value?: string): Credentials {\n const creds = value || \"same-origin\";\n if (!CREDENTIALS_VALUES.includes(creds as Credentials)) {\n throw new Error(\n `Invalid credentials: \"${value}\" must be one of ${\n CREDENTIALS_VALUES.join(\", \")\n }`,\n );\n }\n return creds as Credentials;\n}\n\n/**\n * Validates and normalizes the HTTP method option for a server function.\n * Accepts \"GET\" or \"POST\" (case-insensitive); defaults to \"POST\" when undefined.\n * @param value - Method value to validate\n * @returns The validated uppercase method string\n * @throws Error if the value is not \"GET\" or \"POST\"\n */\nexport function validateMethod(value?: string): \"GET\" | \"POST\" {\n const method = (value || \"POST\").toUpperCase();\n if (method !== \"GET\" && method !== \"POST\") {\n throw new Error(`Invalid method: \"${value}\" must be one of GET, POST`);\n }\n return method;\n}\n","/**\n * @module Client module generation.\n */\nimport type {\n RpcPluginOptionsInternal,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\nimport { getFunctionsForPrefix } from \"./functionsMap.ts\";\nimport {\n validateCredentials,\n validateIdentifier,\n validateMethod,\n validatePathSegment,\n} from \"./validate.ts\";\n\n/**\n * Generates a JavaScript client module string for a single server function.\n * All interpolated values are validated to prevent code injection.\n * @param fnName - Registered RPC function name (validated as path segment)\n * @param fnEntry - Export name used in the generated module (validated as identifier)\n * @param options - Content type, credentials, and RPC prefix settings\n * @returns A string of JavaScript code exporting the client stub\n */\nconst getModule = (\n fnName: string,\n fnEntry: string,\n options: Partial<ServerFunctionOptions> & {\n contentType: ServerFunctionOptions[\"contentType\"];\n rpcPrefix: string;\n },\n): string => {\n // Validate all interpolated strings to prevent code injection\n const safeFnName = validatePathSegment(fnName, \"function name\");\n const safeFnEntry = validateIdentifier(fnEntry, \"export name\");\n const safePrefix = validatePathSegment(options.rpcPrefix, \"rpcPrefix\");\n const credentials = validateCredentials(options.credentials);\n const method = validateMethod(options.method);\n const contentType =\n (options.contentType ?? \"application/json\") as ServerFunctionOptions[\n \"contentType\"\n ];\n\n const opts: string[] = [];\n if (method !== \"POST\") opts.push(`method: \"${method}\"`);\n if (credentials !== \"same-origin\") opts.push(`credentials: \"${credentials}\"`);\n if (contentType !== \"application/json\") {\n opts.push(`contentType: \"${contentType}\"`);\n }\n const optsStr = opts.length ? `, { ${opts.join(\", \")} }` : \"\";\n\n const output = `\n export const ${safeFnEntry} = getClientStub(\"${safePrefix}\", \"${safeFnName}\"${optsStr});`;\n\n return output.trim();\n};\n\n/**\n * Generates the complete client-side module bundle by iterating all registered server functions\n * for a specific prefix and producing fetch-based stubs for each. The result is transformed by Vite\n * (or Oxc) during the dev server or production build.\n *\n * The generated stubs are plain `fetch` calls, so they are adapter-agnostic —\n * only the prefix is needed.\n * @param initialOptions - Plugin options containing the rpcPrefix\n * @returns A string of JavaScript code with all client RPC modules and their import dependencies\n */\nexport const getClientModules = (\n initialOptions: RpcPluginOptionsInternal,\n): string => {\n // Validate prefix once at the top level\n validatePathSegment(initialOptions.rpcPrefix, \"rpcPrefix\");\n\n // Get functions registered for this specific prefix\n const prefixMap = getFunctionsForPrefix(initialOptions.rpcPrefix);\n const entries = Array.from(prefixMap.entries())\n .filter(([, entry]) => entry.exportName)\n .map(([registeredName, entry]) =>\n getModule(registeredName, entry.exportName!, {\n ...initialOptions,\n ...((entry.options as ServerFunctionOptions) || {}),\n })\n )\n .join(\"\\n\");\n\n const output = `\n// Client-side RPC modules for prefix: ${initialOptions.rpcPrefix}\nimport { getClientStub } from \"@thednp/rpc/helpers\";\n${entries}`;\n\n return output.trim();\n};\n","/** @module Server-side utilities. Exports the `RPCError` class for typed server-side errors, `formatError` for middleware error responses, `isFormContentType` and `hasContentTypeMismatch` for content-type validation, and `walkGlobFiles` for recursively discovering `*.server.*` files. Never import this module in client code — it is server-only. */\nimport type { ContentType, JsonObject, JsonValue } from \"./types.d.ts\";\nimport { readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport {\n BAD_REQUEST,\n INTERNAL_SERVER_ERROR,\n PAYLOAD_TOO_LARGE,\n UNSUPPORTED_MEDIA_TYPE,\n} from \"./constants.ts\";\nimport { defaultPrefix } from \"./options.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 */\n/**\n * An error carrying an HTTP status, so the dispatch can answer that status\n * instead of flattening every failure to a `500`.\n */\nexport interface ClientHttpError extends Error {\n status?: number;\n statusCode?: number;\n}\n\n/**\n * Tags an error with an HTTP status for the dispatch to surface.\n *\n * Used where a malformed *request* is the fault — a body that does not parse\n * under a declared JSON `Content-Type`, a GET `?args=` value that is not valid\n * JSON. Every host framework rpc supports answers `400` for these (Express\n * `entity.parse.failed`, Fastify `FST_ERR_CTP_INVALID_JSON_BODY`, koa-bodyparser,\n * and h3's own `readBody`), and treating one as a server fault both misreports\n * the fault and turns a trivial client mistake into a log entry.\n * @param status - The HTTP status to answer with\n * @param message - Internal diagnostic message; never sent to the client\n * @returns An `Error` carrying `status`\n */\nexport const httpError = (status: number, message: string): ClientHttpError => {\n const err = new Error(message) as ClientHttpError;\n err.status = status;\n return err;\n};\n\n/**\n * Recognises an error that should produce a `4xx` response rather than a `500`.\n *\n * Matches the `status` / `statusCode` convention used by h3's `HTTPError`, the\n * `http-errors` objects Express's `body-parser` throws, and anything else that\n * carries a numeric 4xx. Shared by all five adapters so a host-framework\n * signal and an rpc-raised one are handled by the same rule.\n * @param err - The caught error\n * @returns True when the error denotes a client (4xx) fault\n */\nconst readClientStatus = (err: unknown): number | undefined => {\n const candidate = err as ClientHttpError | null | undefined;\n // Reads both conventions: h3's `HTTPError` and rpc's `httpError` use\n // `status`, while the `http-errors` objects Express's `body-parser` throws\n // and Koa's `ctx.throw` use `statusCode`.\n const status = candidate?.status ?? candidate?.statusCode;\n return typeof status === \"number\" && status >= 400 && status < 500\n ? status\n : undefined;\n};\n\n/**\n * Recognises an error that should produce a `4xx` response rather than a `500`.\n * Matches the `status` / `statusCode` convention used by h3's `HTTPError`, the\n * `http-errors` objects Express's `body-parser` throws, and anything else\n * carrying a numeric 4xx. Shared by all five adapters so a host-framework\n * signal and an rpc-raised one are handled by the same rule.\n * @param err - The caught error\n * @returns True when the error denotes a client (4xx) fault\n */\nexport const isClientHttpError = (err: unknown): boolean =>\n readClientStatus(err) !== undefined;\n\n/**\n * Reads the status to answer for a client error. Defaults to `400` rather than\n * `500` so an unrecognised 4xx is never reported as a server fault.\n * @param err - The caught error\n * @returns The 4xx status to answer with\n */\nexport const clientErrorStatus = (err: unknown): number =>\n readClientStatus(err) ?? 400;\n\nexport const clientErrorMessage = (status: number): string => {\n if (status === 413) return PAYLOAD_TOO_LARGE;\n if (status === 415) return UNSUPPORTED_MEDIA_TYPE;\n return BAD_REQUEST;\n};\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 * Decides whether a request may proceed, given the configured origin allowlist\n * and the two headers a browser can be made to reveal.\n *\n * Four tiers, evaluated in order — the first tier with a signal decides:\n *\n * 1. `origin` option unset → everything passes. No validation is performed.\n * 2. `Origin` present → the allowlist decides, exactly as {@link isOriginAllowed}.\n * 3. `Origin` absent but `Sec-Fetch-Site` present → allow only `same-origin`\n * and `none`; anything else (including an unrecognised value) is rejected.\n * 4. Both absent → passes. This is the deliberate, documented curl/native hole.\n *\n * Tier 2 must short-circuit ahead of tier 3. `Sec-Fetch-Site` is a coarse\n * four-value enum that cannot name a host, so on its own it would reject a\n * legitimate request from an allowlisted sibling subdomain (`same-site`). The\n * allowlist exists precisely to admit that case, and it can only do so while\n * `Origin` survives. `Sec-Fetch-Site` earns a vote only once the precise signal\n * has been stripped away by something in the chain — at which point there is\n * nothing left to trust, so it fails closed.\n *\n * Browsers never strip `Origin` themselves, so tier 3 can only fire when a\n * proxy, sanitising middleware, or misconfigured CDN removed it. No legitimate\n * browser request can regress.\n *\n * An empty (or whitespace-only) header value counts as **absent**, not as an\n * unrecognised signal. No browser emits an empty `Sec-Fetch-Site`, and adapters\n * disagree on what their header accessor returns for a missing header (Node's\n * `req.headers` yields `undefined`, Hono's `c.req.header()` may yield `\"\"`).\n * Normalising here keeps all five adapters behaving identically instead of\n * inheriting whichever convention their framework happens to use.\n * @param allowed - The configured `origin` option, if any\n * @param origin - The raw `Origin` request header, if present\n * @param site - The raw `Sec-Fetch-Site` request header, if present\n * @returns `true` when the request may proceed\n */\nexport const isOriginRequestAllowed = (\n allowed: string | string[] | undefined,\n origin: string | undefined,\n site: string | undefined,\n): boolean => {\n if (!allowed) return true; // tier 1 — the check is opt-in\n if (origin?.trim()) return isOriginAllowed(allowed, origin); // tier 2 — precise\n if (!site?.trim()) return true; // tier 4 — curl / native client\n // tier 3 — precision lost, so fail closed\n return site === \"same-origin\" || site === \"none\";\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\n/**\n * Decides whether a request's `Origin` header is allowed by the configured\n * allowlist. Shared by all five adapters so the rule lives in exactly one place.\n *\n * - No `allowed` value (option unset) → everything passes: no validation.\n * - No `requestOrigin` header → passes, preserving curl/native-client access.\n * - Otherwise the header must match one of the entries exactly.\n *\n * A single string and a one-element array behave identically, so widening\n * `origin` to `string | string[]` is backward compatible.\n *\n * `Origin: null` (sandboxed iframes, `file://`, extension pages) is rejected\n * whenever an allowlist is set, because it never equals a real origin.\n * @param allowed - The configured `origin` option, if any\n * @param requestOrigin - The raw `Origin` request header, if present\n * @returns `true` when the request may proceed\n */\nexport const isOriginAllowed = (\n allowed: string | string[] | undefined,\n requestOrigin: string | undefined,\n): boolean => {\n if (!allowed || !requestOrigin) return true;\n return Array.isArray(allowed)\n ? allowed.includes(requestOrigin)\n : requestOrigin === allowed;\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\n/**\n * Publishes the global RPC prefix, consulted by `resolveRPCPrefix` whenever no\n * explicit prefix is supplied. `loadRPCConfig` calls this on every return path\n * so a loaded config is the fallback for later registrations and dispatches.\n *\n * Stored on a `Symbol.for` key on `globalThis` so it stays instance-stable\n * across the bundled entry copies (`server.mjs`, `express.mjs`, ...) and dev\n * server hot reloads — the same technique as the request-context storage.\n * @param prefix - The prefix to publish, or `undefined` to clear it\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\n/**\n * Resolves the effective RPC prefix: the explicit one when given, otherwise\n * the global prefix set by `setGlobalPrefix` / `loadRPCConfig`, otherwise the\n * built-in default.\n *\n * Every adapter resolves its prefix through this single function — in both the\n * outer `createMiddleware` gate and the `createRPCMiddleware` dispatch — so the\n * two halves of a request can never disagree, and so a prefix registered by\n * `createServerFunction` (which resolves the same way) is always the prefix the\n * middleware looks up. Resolving the two sides independently is what allowed\n * h3 to drift from the other four adapters, and what left the documented\n * global-prefix flow returning 404 on all of them.\n * @param rpcPrefix - Explicit prefix from config or middleware options\n * @returns The prefix to gate on, look up in, and strip from the request path\n */\nexport const resolveRPCPrefix = (rpcPrefix?: string): string =>\n rpcPrefix || getGlobalPrefix() || defaultPrefix;\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\n/**\n * Scan targets already performed, so a lazy re-scan is not repeated.\n *\n * Keyed by everything that determines the outcome — the resolved scan root\n * (which files are read), the matching mode, and the prefix prefix-less\n * functions register under. A single process-wide boolean used to be enough\n * only while there was one prefix: the *first* scan suppressed every later\n * one, so a second RPC instance on a different prefix asked for a lazy scan,\n * got an early return, and answered 404 for every function it owned.\n */\nconst scannedTargets = new Set<string>();\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 // Resolve the scan target up front, before anything expensive. Importing\n // Vite and standing up an internal server are the costly parts, and neither\n // is needed once a scan is known to be a repeat.\n const root = initialCfg?.root || process.cwd();\n const resolvedScanRoot = resolve(\n root,\n initialCfg?.scanRoot ?? join(root, \"src\", \"api\"),\n );\n const serverFiles = initialCfg?.serverFiles ?? \"exact\";\n const target = `${resolvedScanRoot}|${serverFiles}|${\n initialCfg?.rpcPrefix ?? defaultPrefix\n }`;\n\n if (scannedTargets.has(target) && !devServer) {\n return;\n }\n // Vite is only needed to spin up the internal dev server that loads the\n // server function files, so it is imported lazily rather than at the top\n // of the module. This keeps consumers of the standalone server entry that\n // register their functions directly (e.g. serverless functions bundling\n // the API module) free of a static Vite dependency — when Vite is\n // externalized by the function bundler, this lazy import is left as a\n // runtime require that is never executed.\n let createServer: typeof import(\"vite\").createServer;\n let normalizePath: typeof import(\"vite\").normalizePath;\n try {\n ({ createServer, normalizePath } = await import(\"vite\"));\n } catch {\n // Vite is not installed in this environment (e.g. a serverless bundle\n // where Vite is externalized or absent). Server functions must have been\n // imported directly into the prefix-scoped map; nothing to scan — exit\n // gracefully instead of crashing the host's cold start with\n // `Cannot find module 'vite'`.\n return;\n }\n const config = !initialCfg\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 // 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 // Warn and move on: returning here would abandon every remaining\n // file in the scan, silently dropping their functions.\n console.warn(NO_SERVER_FUNCTION_FOUND);\n continue;\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 scannedTargets.add(target);\n }\n};\n","/** @module Main entrypoint for the RPC Vite plugin. Exports `rpcPlugin` (default) and `loadRPCConfig`. For a Vite-free `defineConfig`, use `@thednp/rpc/config`. */\nimport type { ConfigEnv, Plugin, ResolvedConfig, ViteDevServer } from \"vite\";\nimport { loadConfigFromFile, mergeConfig } from \"vite\";\n// Namespace import: `transformWithOxc` only exists on Vite 8+, and the\n// `isOxc` version check exists precisely to stay compatible with older\n// resolutions. A namespace import keeps both members optional instead of\n// turning a missing named export into a module link-time error.\nimport * as vite from \"vite\";\nimport { resolve } from \"node:path\";\nimport process from \"node:process\";\nimport { existsSync } from \"node:fs\";\nimport { defaultRPCOptions } from \"./options.ts\";\nimport type { RpcPluginOptions, ScanConfig } from \"./types.d.ts\";\nimport {\n CONFIG_FILE_NOT_FOUND,\n FAILED_LOAD_CONFIG,\n NO_CONFIG_FOUND,\n} from \"./constants.ts\";\n\nimport { getClientModules } from \"./getClientModules.ts\";\n// DEV server only\nimport {\n scanForServerFiles,\n scannedServerFiles,\n} from \"./scanForServerFiles.ts\";\nimport { serverFunctionsMap } from \"./functionsMap.ts\";\n\nimport { setGlobalPrefix } from \"@thednp/rpc/server\";\nimport { createRPCMiddleware } from \"@thednp/rpc/express\";\n\n/**\n * Loads and transforms a single RPC config file using Vite's config loader.\n * @param env - Vite config environment\n * @param file - Config file path (e.g. \"rpc.config.ts\")\n * @returns The loaded config augmented with the configFile path, or null on failure\n */\nconst loadConfigFile = async (env: ConfigEnv, file: string) => {\n const result = await loadConfigFromFile(env, file) as {\n path: string;\n config: Partial<RpcPluginOptions>;\n dependencies: string[];\n } | null;\n return result\n ? { ...result, config: { ...result.config, configFile: file } }\n : /* istanbul ignore next */ null;\n};\n\nlet RPCConfig: RpcPluginOptions;\n\n/**\n * Merges a loaded config file over the built-in defaults, recording the\n * resolved file path. Assigns the module-level `RPCConfig` cache.\n * @param config - The config object exported by the config file\n * @param configFilePath - Absolute path of the config file that was loaded\n * @returns The merged RPC plugin options\n */\nconst mergeLoaded = (\n config: Partial<RpcPluginOptions>,\n configFilePath: string,\n): RpcPluginOptions => {\n RPCConfig = mergeConfig(\n { ...defaultRPCOptions, configFile: configFilePath },\n config,\n ) as RpcPluginOptions;\n return RPCConfig;\n};\n\n/**\n * Loads the RPC configuration by searching for config files in the project root.\n * Searches in order: `rpc.config.ts`, `rpc.config.js`, `rpc.config.mjs`, `rpc.config.mts`,\n * `.rpcrc.ts`, `.rpcrc.js`. Falls back to defaults if none found.\n * @param configFile - Optional explicit config file path; skips file search when provided\n * @param opts - Optional settings; `silent` suppresses the \"no config found\" warning\n * @returns Resolved RPC plugin options\n */\nconst loadRPCConfig: (\n configFile?: string | { silent?: boolean },\n opts?: { silent?: boolean },\n) => Promise<RpcPluginOptions> = async (configFile?, opts?) => {\n // `loadRPCConfig({ silent: true })` is a documented call form. Without this\n // normalisation the options object is treated as a config path, `resolve`\n // throws on the non-string, and the catch below silently downgrades the\n // resolved config to the defaults.\n if (typeof configFile === \"object\" && configFile !== null) {\n opts = configFile;\n configFile = undefined;\n }\n try {\n // istanbul ignore next\n const env: ConfigEnv & { root: string } = {\n command: \"serve\",\n root: process.cwd(),\n mode: process.env.NODE_ENV || \"development\",\n };\n const defaultConfigFiles = [\n \"rpc.config.ts\",\n \"rpc.config.js\",\n \"rpc.config.mjs\",\n \"rpc.config.mts\",\n \".rpcrc.ts\",\n \".rpcrc.js\",\n ];\n\n // If specific config file provided\n if (configFile) {\n const configFilePath = resolve(env.root, configFile);\n if (!existsSync(configFilePath)) {\n console.warn(CONFIG_FILE_NOT_FOUND(configFile, configFilePath));\n RPCConfig = defaultRPCOptions;\n setGlobalPrefix(defaultRPCOptions.rpcPrefix);\n return defaultRPCOptions as RpcPluginOptions;\n }\n\n const result = await loadConfigFile(env, configFile);\n // istanbul ignore else\n if (result && typeof result === \"object\") {\n setGlobalPrefix(\n mergeLoaded(result.config, configFilePath).rpcPrefix,\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 setGlobalPrefix(RPCConfig.rpcPrefix);\n\n return RPCConfig;\n }\n\n // Try default config files\n for (const file of defaultConfigFiles) {\n const configFilePath = resolve(env.root, file);\n // istanbul ignore else\n if (!existsSync(configFilePath)) {\n continue;\n }\n\n const result = await loadConfigFile(env, file);\n // istanbul ignore else\n if (result) {\n // Every return path must publish the prefix, not just the explicit\n // `configFile` and no-config ones: this discovery path is the common\n // case (an `rpc.config.ts` exists), so skipping it left functions\n // registering under the default while the middleware dispatched on\n // the configured prefix.\n setGlobalPrefix(\n mergeLoaded(result.config, configFilePath).rpcPrefix,\n );\n return RPCConfig;\n }\n }\n RPCConfig = defaultRPCOptions;\n // Last call load defaults no matter what\n if (!opts?.silent) console.warn(NO_CONFIG_FOUND);\n } catch (error) {\n // Falls back to the defaults. Note this also resets the cache, so a\n // failed load downgrades a previously loaded config for the rest of the\n // process — the documented contract (and the tests) require the fallback,\n // so the stale value is not preserved here.\n RPCConfig = defaultRPCOptions;\n console.warn(FAILED_LOAD_CONFIG, error);\n }\n\n setGlobalPrefix(RPCConfig.rpcPrefix);\n\n return RPCConfig;\n};\n\n/**\n * Vite plugin that enables automatic RPC generation.\n * Transforms server function imports into fetch-based client stubs during development and production builds.\n * In dev mode, attaches the RPC middleware to Vite's Connect server.\n * @param devOptions - Development-only overrides (merged on top of config file values)\n * @returns A Vite plugin object\n */\nfunction rpcPlugin(\n devOptions: Partial<RpcPluginOptions> = {},\n): Plugin {\n // `rpcPrefix` is required on RpcPluginOptions but defaulted here, so the\n // resolved options always carry one for the scan and the client stubs.\n let options: RpcPluginOptions & { rpcPrefix: string } = mergeConfig(\n defaultRPCOptions,\n devOptions,\n ) as RpcPluginOptions;\n let config: ResolvedConfig;\n let viteServer: ViteDevServer;\n let isOxc = true;\n\n return {\n name: \"vite-plugin-universal-rpc\",\n enforce: \"pre\",\n // Plugin methods\n async configResolved(resolvedConfig) {\n const uniConfig = await loadRPCConfig(undefined, {\n silent: devOptions.silent,\n });\n options = mergeConfig(uniConfig, devOptions) as RpcPluginOptions;\n\n config = resolvedConfig;\n },\n async configureServer(server) {\n viteServer = server;\n // istanbul ignore else\n if (serverFunctionsMap.size === 0) {\n const scanCfg: ScanConfig = {\n ...config,\n serverFiles: options.serverFiles,\n scanRoot: options.scanRoot,\n rpcPrefix: options.rpcPrefix,\n };\n await scanForServerFiles(scanCfg, viteServer);\n }\n\n // in dev mode we always use the express/connect middleware, since the\n // Vite dev server is Connect-based — there is no adapter to select.\n server.middlewares.use(createRPCMiddleware(options));\n },\n\n async buildStart() {\n const viteVersion = this.meta?.viteVersion;\n isOxc = Number.parseInt(viteVersion, 10) >= 8;\n\n // Prepare the server functions\n if (!viteServer && config) {\n const scanCfg: ScanConfig = {\n ...config,\n serverFiles: options.serverFiles,\n scanRoot: options.scanRoot,\n rpcPrefix: options.rpcPrefix,\n };\n await scanForServerFiles(scanCfg);\n }\n },\n async transform(code: string, id: string, ops?: { ssr?: boolean }) {\n // Only transform files with server functions for client builds: any\n // other file is unchanged, and a file loaded on the server (SSR) is\n // left as-is.\n if (!code.includes(\"createServerFunction\") || ops?.ssr) {\n return null;\n }\n\n if (serverFunctionsMap.size === 0) {\n const scanCfg: ScanConfig = {\n ...config,\n serverFiles: options.serverFiles,\n scanRoot: options.scanRoot,\n rpcPrefix: options.rpcPrefix,\n };\n await scanForServerFiles(scanCfg);\n }\n\n // Only transform modules that were scanned as server function files:\n // pages or docs mentioning `createServerFunction` in prose must not\n // be rewritten into the generated client bundle.\n const idPath = vite.normalizePath(id.split(\"?\")[0]);\n if (!scannedServerFiles.has(idPath)) {\n return null;\n }\n\n const transformer = isOxc ? \"transformWithOxc\" : \"transformWithEsbuild\";\n const langProp = isOxc ? \"lang\" : \"loader\";\n const source = getClientModules({ rpcPrefix: options.rpcPrefix });\n\n const result = await vite[transformer](source, id, {\n [langProp]: \"js\",\n sourcemap: true,\n // target: \"es2023\"\n });\n\n return {\n code: result.code,\n map: result.map\n ? typeof result.map === \"string\"\n ? JSON.parse(result.map)\n : /* istanbul ignore next @preserve */ result.map\n : /* istanbul ignore next @preserve */ null,\n };\n },\n } satisfies Plugin;\n}\n\nexport { rpcPlugin as default };\nexport { loadRPCConfig };\nexport type * from \"./types.d.ts\";\nexport {};\n"],"mappings":"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"}
|