@thednp/rpc 0.0.1 → 0.0.5
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 +5 -5
- package/CLAUDE.md +1 -0
- package/README.md +195 -47
- package/dist/express/express.d.mts +110 -16
- package/dist/express/express.d.mts.map +1 -1
- package/dist/express/express.mjs +113 -20
- package/dist/express/express.mjs.map +1 -1
- package/dist/fastify/fastify.d.mts +83 -5
- package/dist/fastify/fastify.d.mts.map +1 -1
- package/dist/fastify/fastify.mjs +84 -18
- package/dist/fastify/fastify.mjs.map +1 -1
- package/dist/fastify/plugin/fastify/plugin.d.mts +64 -9
- package/dist/fastify/plugin/fastify/plugin.d.mts.map +1 -1
- package/dist/fastify/plugin/fastify/plugin.mjs +73 -18
- package/dist/fastify/plugin/fastify/plugin.mjs.map +1 -1
- package/dist/helpers/helpers.d.mts +66 -4
- package/dist/helpers/helpers.d.mts.map +1 -1
- package/dist/helpers/helpers.mjs +34 -7
- package/dist/helpers/helpers.mjs.map +1 -1
- package/dist/hono/hono.d.mts +75 -6
- package/dist/hono/hono.d.mts.map +1 -1
- package/dist/hono/hono.mjs +84 -24
- package/dist/hono/hono.mjs.map +1 -1
- package/dist/index.d.mts +210 -19
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +137 -31
- package/dist/index.mjs.map +1 -1
- package/dist/koa/koa.d.mts +52 -0
- package/dist/koa/koa.d.mts.map +1 -1
- package/dist/koa/koa.mjs +86 -16
- package/dist/koa/koa.mjs.map +1 -1
- package/dist/server/server.d.mts +111 -10
- package/dist/server/server.d.mts.map +1 -1
- package/dist/server/server.mjs +117 -17
- package/dist/server/server.mjs.map +1 -1
- package/package.json +48 -31
- package/wiki/adapters.md +0 -143
- package/wiki/best-practices.md +0 -201
- package/wiki/client-usage.md +0 -62
- package/wiki/configuration.md +0 -77
- package/wiki/getting-started.md +0 -76
- package/wiki/index.md +0 -26
- package/wiki/security.md +0 -54
- package/wiki/server-functions.md +0 -93
- package/wiki/setup.md +0 -77
package/dist/server/server.d.mts
CHANGED
|
@@ -3,33 +3,115 @@ import "@thednp/rpc";
|
|
|
3
3
|
import "express";
|
|
4
4
|
import "hono";
|
|
5
5
|
import "@hono/node-server";
|
|
6
|
+
import "hono/factory";
|
|
6
7
|
import "fastify";
|
|
8
|
+
import "fastify-plugin";
|
|
7
9
|
import "koa";
|
|
8
10
|
//#region src/types.d.ts
|
|
11
|
+
/**
|
|
12
|
+
* Content types the RPC client modules send with each request.
|
|
13
|
+
*/
|
|
9
14
|
type ContentType = "application/json" | "text/plain";
|
|
15
|
+
/**
|
|
16
|
+
* Fetch `credentials` policy used by the generated client modules.
|
|
17
|
+
*/
|
|
18
|
+
type Credentials = "same-origin" | "include" | "omit";
|
|
19
|
+
/**
|
|
20
|
+
* Options for a single server function, controlling how the generated
|
|
21
|
+
* client module serializes the request body and sends credentials.
|
|
22
|
+
*/
|
|
10
23
|
interface ServerFunctionOptions {
|
|
24
|
+
/**
|
|
25
|
+
* Content type used for the request body.
|
|
26
|
+
* @default "application/json"
|
|
27
|
+
*/
|
|
11
28
|
contentType: ContentType;
|
|
29
|
+
/**
|
|
30
|
+
* Fetch credentials policy.
|
|
31
|
+
* @default "same-origin"
|
|
32
|
+
*/
|
|
33
|
+
credentials?: Credentials;
|
|
34
|
+
/**
|
|
35
|
+
* HTTP method used for the RPC request.
|
|
36
|
+
* GET requests send arguments as an `?args=` JSON query parameter
|
|
37
|
+
* (a fetch request body is not allowed on GET).
|
|
38
|
+
* @default "POST"
|
|
39
|
+
*/
|
|
40
|
+
method?: "GET" | "POST";
|
|
12
41
|
}
|
|
13
42
|
// primitives and their compositions
|
|
43
|
+
/**
|
|
44
|
+
* Primitive JSON values, including `undefined` for optional parameters.
|
|
45
|
+
*/
|
|
14
46
|
type JsonPrimitive = string | number | boolean | null | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* A JSON object whose values are JSON values or arrays.
|
|
49
|
+
*/
|
|
15
50
|
type JsonObject = {
|
|
16
51
|
[key: string]: JsonValue | JsonArray;
|
|
17
52
|
};
|
|
53
|
+
/**
|
|
54
|
+
* A JSON array of JSON values.
|
|
55
|
+
*/
|
|
18
56
|
type JsonArray = JsonValue[];
|
|
57
|
+
/**
|
|
58
|
+
* Any JSON-serializable value: primitive, array, or object.
|
|
59
|
+
*/
|
|
19
60
|
type JsonValue = JsonPrimitive | JsonArray | JsonObject;
|
|
61
|
+
/**
|
|
62
|
+
* Server function initialization signature, identical to `ServerFunction`.
|
|
63
|
+
* Used when registering a function with `createServerFunction`.
|
|
64
|
+
*/
|
|
20
65
|
type ServerFunctionInit<TArgs extends JsonArray = JsonArray, TResult extends JsonValue = JsonValue> = (signal: AbortSignal, ...args: TArgs) => Promise<TResult>;
|
|
66
|
+
/**
|
|
67
|
+
* Client-side stub signature generated for each server function.
|
|
68
|
+
* Returns a promise-backed `data` handle plus a `cancel` function
|
|
69
|
+
* that aborts the underlying fetch request.
|
|
70
|
+
*/
|
|
21
71
|
type ClientFunction<TArgs extends JsonArray = JsonArray, TResult extends JsonValue = JsonValue> = (...args: TArgs) => {
|
|
72
|
+
/** Promise resolving to the server response data */
|
|
22
73
|
data: Promise<TResult>;
|
|
74
|
+
/** Aborts the in-flight request with the given reason */
|
|
23
75
|
cancel: (reason: string) => void;
|
|
24
76
|
};
|
|
77
|
+
/**
|
|
78
|
+
* A client function augmented with its registered export name and
|
|
79
|
+
* per-function options (content type, credentials).
|
|
80
|
+
*/
|
|
25
81
|
type ClientFunctionWithOptions = ClientFunction & {
|
|
82
|
+
/** Registered export name of the server function */
|
|
26
83
|
name: string;
|
|
84
|
+
/** Per-function content type and credentials options */
|
|
27
85
|
options?: ServerFunctionOptions;
|
|
28
86
|
};
|
|
87
|
+
/**
|
|
88
|
+
* Internal plugin options accepted by `getClientModules`.
|
|
89
|
+
*/
|
|
90
|
+
interface RpcPluginOptionsInternal {
|
|
91
|
+
/** RPC endpoint prefix (e.g. "__rpc") */
|
|
92
|
+
rpcPrefix: string;
|
|
93
|
+
/** Framework adapter name */
|
|
94
|
+
adapter?: string | undefined;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Partial Vite config used when scanning server files outside a running dev server.
|
|
98
|
+
*/
|
|
99
|
+
type ScanConfig = Pick<ResolvedConfig, "root" | "base"> & {
|
|
100
|
+
/** Vite server options override (e.g. `middlewareMode`) */
|
|
101
|
+
server?: Partial<ResolvedConfig["server"]>;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Entry in the server functions map: registered name, client handler,
|
|
105
|
+
* optional per-function options, and the original export name.
|
|
106
|
+
*/
|
|
29
107
|
interface ServerFnEntry {
|
|
108
|
+
/** Registered RPC function name (used in the URL path) */
|
|
30
109
|
name: string;
|
|
110
|
+
/** Client-side handler stub for this function */
|
|
31
111
|
handler: ClientFunctionWithOptions;
|
|
112
|
+
/** Per-function content type and credentials options */
|
|
32
113
|
options?: ServerFunctionOptions;
|
|
114
|
+
/** Original export name from the server module */
|
|
33
115
|
exportName?: string;
|
|
34
116
|
}
|
|
35
117
|
/**
|
|
@@ -47,9 +129,9 @@ interface RpcPluginOptions {
|
|
|
47
129
|
* @default "__rpc"
|
|
48
130
|
* @example
|
|
49
131
|
* // Results in endpoints like: /api/rpc/myFunction
|
|
50
|
-
*
|
|
132
|
+
* rpcPrefix: "api/rpc"
|
|
51
133
|
*/
|
|
52
|
-
|
|
134
|
+
rpcPrefix: "__rpc" | string;
|
|
53
135
|
/**
|
|
54
136
|
* Option to set an adapter for the middleware connection. The default is _express_,
|
|
55
137
|
* which is the most popular and battle tested server app. The _express_ adapter is
|
|
@@ -63,29 +145,48 @@ interface RpcPluginOptions {
|
|
|
63
145
|
declare const serverFunctionsMap: Map<string, ServerFnEntry>;
|
|
64
146
|
//#endregion
|
|
65
147
|
//#region src/scanForServerFiles.d.ts
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
148
|
+
/**
|
|
149
|
+
* Scans `src/api/` for server function files (`server.ts`, `server.js`, `server.mjs`, `server.mts`)
|
|
150
|
+
* and populates the global `serverFunctionsMap` with their exported functions.
|
|
151
|
+
* Uses Vite's SSR module loading to resolve and execute each file.
|
|
152
|
+
* @param initialCfg - Optional Vite config overrides (root, base, server)
|
|
153
|
+
* @param devServer - Optional running Vite dev server instance; when provided, skips creating a new one
|
|
154
|
+
*/
|
|
69
155
|
declare const scanForServerFiles: (initialCfg?: ScanConfig, devServer?: ViteDevServer) => Promise<void>;
|
|
70
156
|
//#endregion
|
|
71
157
|
//#region src/createFunction.d.ts
|
|
158
|
+
/**
|
|
159
|
+
* Creates a server-side RPC function.
|
|
160
|
+
* Registers the function in the server functions map and returns a client-compatible
|
|
161
|
+
* wrapper that exposes `data` (Promise) and `cancel` (function) for request lifecycle control.
|
|
162
|
+
* @param name - Unique identifier used by the RPC router to dispatch requests
|
|
163
|
+
* @param handler - The actual implementation receiving an AbortSignal followed by JSON-serializable arguments
|
|
164
|
+
* @param fnOptions - Optional contentType and credentials settings
|
|
165
|
+
* @returns A client stub with `data` promise and `cancel` method, auto-registered in the server map
|
|
166
|
+
*/
|
|
72
167
|
declare function createServerFunction<TArgs extends JsonArray = JsonArray, TResult extends JsonValue = JsonValue>(name: string, handler: ServerFunctionInit<TArgs, TResult>, fnOptions?: Partial<ServerFunctionOptions>): ClientFunction<TArgs, TResult>;
|
|
73
168
|
//#endregion
|
|
74
169
|
//#region src/getClientModules.d.ts
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
170
|
+
/**
|
|
171
|
+
* Generates the complete client-side module bundle by iterating all registered server functions
|
|
172
|
+
* and producing fetch-based stubs for each. The result is transformed by Vite (or Oxc) during
|
|
173
|
+
* the dev server or production build.
|
|
174
|
+
* @param initialOptions - Plugin options containing rpcPrefix and optional adapter
|
|
175
|
+
* @returns A string of JavaScript code with all client RPC modules and their import dependencies
|
|
176
|
+
*/
|
|
79
177
|
declare const getClientModules: (initialOptions: RpcPluginOptionsInternal) => string;
|
|
80
178
|
//#endregion
|
|
81
179
|
//#region src/options.d.ts
|
|
82
180
|
declare const defaultServerFnOptions: {
|
|
83
181
|
contentType: "application/json";
|
|
182
|
+
credentials: "same-origin";
|
|
183
|
+
method: "POST";
|
|
84
184
|
};
|
|
85
185
|
declare const defaultRPCOptions: RpcPluginOptions;
|
|
86
186
|
declare const defaultMiddlewareOptions: {
|
|
87
|
-
|
|
187
|
+
rpcPrefix: undefined;
|
|
88
188
|
path: undefined;
|
|
189
|
+
origin: undefined;
|
|
89
190
|
};
|
|
90
191
|
//#endregion
|
|
91
192
|
export { createServerFunction, defaultMiddlewareOptions, defaultRPCOptions, defaultServerFnOptions, getClientModules, scanForServerFiles, serverFunctionsMap };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.mts","names":[],"sources":["../../src/types.d.ts","../../src/functionsMap.ts","../../src/scanForServerFiles.ts","../../src/createFunction.ts","../../src/getClientModules.ts","../../src/options.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"server.d.mts","names":[],"sources":["../../src/types.d.ts","../../src/functionsMap.ts","../../src/scanForServerFiles.ts","../../src/createFunction.ts","../../src/getClientModules.ts","../../src/options.ts"],"mappings":";;;;;;;;;;;;;KA2DY;;;;KAKA;;;;;UAaK;;;;;EAKf,aAAa;;;;;EAKb,cAAc;;;;;;;EAOd;;;;;;KAOU;;;;KAIA;GAAgB,cAAc,YAAY;;;;;KAI1C,YAAY;;;;KAIZ,YAAY,gBAAgB,YAAY;;;;;KAsCxC,mBACV,cAAc,YAAY,WAC1B,gBAAgB,YAAY,cACzB,QAAQ,gBAAgB,MAAM,UAAU,QAAQ;;;;;;KAOzC,eACV,cAAc,YAAY,WAC1B,gBAAgB,YAAY,iBACtB,MAAM;;EAEZ,MAAM,QAAQ;;EAEd,SAAS;;;;;;KAOC,4BAA4B;;EAEtC;;EAEA,UAAU;;;;;UAMK;;EAEf;;EAEA;;;;;KAMU,aAAa,KAAK;;EAE5B,SAAS,QAAQ;;;;;;UAOF;;EAEf;;EAEA,SAAS;;EAET,UAAU;;EAEV;;;;;;;;UASe;;;;;;;;;;;EAWf;;;;;;;EAQA;;;;cC9OW,oBAAoB,YAAY;;;;;;;;;;cCgBhC,qBAAkB,aAChB,YAAU,YACX,kBACX;;;;;;;;;;;;iBCAa,qBACd,cAAc,YAAY,WAC1B,gBAAgB,YAAY,WAE5B,cACA,SAAS,mBAAmB,OAAO,UACnC,YAAW,QAAQ,yBAClB,eAAe,OAAO;;;;;;;;;;cCkDZ,mBAAgB,gBACX;;;cCzEL;;;;;cAMA,mBAAmB;cAKnB"}
|
package/dist/server/server.mjs
CHANGED
|
@@ -5,8 +5,24 @@ import process from "node:process";
|
|
|
5
5
|
//#region src/functionsMap.ts
|
|
6
6
|
const serverFunctionsMap = /* @__PURE__ */ new Map();
|
|
7
7
|
//#endregion
|
|
8
|
+
//#region src/constants.ts
|
|
9
|
+
const OPERATION_ABORTED = "Operation aborted";
|
|
10
|
+
const NO_SERVER_FUNCTION_FOUND = "No server function found.";
|
|
11
|
+
const ERROR_LOADING_FILE = "Error loading file:";
|
|
12
|
+
/** Error message when a value fails the safe-identifier validation. @param label - What kind of value was being validated. @param name - The rejected value */
|
|
13
|
+
const INVALID_IDENTIFIER = (label, name) => `Invalid ${label}: "${name}" must match /^[A-Za-z_$][A-Za-z0-9_$]*$/`;
|
|
14
|
+
/** 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 */
|
|
15
|
+
const INVALID_PATH_SEGMENT = (label, segment) => `Invalid ${label}: "${segment}" must match /^[A-Za-z0-9_$][A-Za-z0-9_$/-]*$/`;
|
|
16
|
+
//#endregion
|
|
8
17
|
//#region src/scanForServerFiles.ts
|
|
9
18
|
let isScanned = false;
|
|
19
|
+
/**
|
|
20
|
+
* Scans `src/api/` for server function files (`server.ts`, `server.js`, `server.mjs`, `server.mts`)
|
|
21
|
+
* and populates the global `serverFunctionsMap` with their exported functions.
|
|
22
|
+
* Uses Vite's SSR module loading to resolve and execute each file.
|
|
23
|
+
* @param initialCfg - Optional Vite config overrides (root, base, server)
|
|
24
|
+
* @param devServer - Optional running Vite dev server instance; when provided, skips creating a new one
|
|
25
|
+
*/
|
|
10
26
|
const scanForServerFiles = async (initialCfg, devServer) => {
|
|
11
27
|
if (isScanned && !devServer) return;
|
|
12
28
|
const config = !initialCfg && !devServer || !initialCfg ? {
|
|
@@ -33,7 +49,7 @@ const scanForServerFiles = async (initialCfg, devServer) => {
|
|
|
33
49
|
const apiDir = join(config.root, "src", "api");
|
|
34
50
|
let files;
|
|
35
51
|
try {
|
|
36
|
-
files = (await readdir(apiDir, { withFileTypes: true })).filter((f) => svFiles.
|
|
52
|
+
files = (await readdir(apiDir, { withFileTypes: true })).filter((f) => svFiles.includes(f.name)).map((f) => join(apiDir, f.name));
|
|
37
53
|
} catch (_e) {
|
|
38
54
|
files = [];
|
|
39
55
|
}
|
|
@@ -42,7 +58,7 @@ const scanForServerFiles = async (initialCfg, devServer) => {
|
|
|
42
58
|
const moduleExports = await server.ssrLoadModule(file);
|
|
43
59
|
const moduleEntries = Object.entries(moduleExports);
|
|
44
60
|
if (!moduleEntries.length) {
|
|
45
|
-
console.warn(
|
|
61
|
+
console.warn(NO_SERVER_FUNCTION_FOUND);
|
|
46
62
|
return;
|
|
47
63
|
}
|
|
48
64
|
for (const [exportName, exportValue] of moduleEntries) {
|
|
@@ -55,7 +71,7 @@ const scanForServerFiles = async (initialCfg, devServer) => {
|
|
|
55
71
|
});
|
|
56
72
|
}
|
|
57
73
|
} catch (error) {
|
|
58
|
-
console.error(
|
|
74
|
+
console.error(ERROR_LOADING_FILE, file, error);
|
|
59
75
|
}
|
|
60
76
|
} finally {
|
|
61
77
|
if (!devServer && server) await server.close();
|
|
@@ -64,24 +80,38 @@ const scanForServerFiles = async (initialCfg, devServer) => {
|
|
|
64
80
|
};
|
|
65
81
|
//#endregion
|
|
66
82
|
//#region src/options.ts
|
|
67
|
-
const defaultServerFnOptions = {
|
|
83
|
+
const defaultServerFnOptions = {
|
|
84
|
+
contentType: "application/json",
|
|
85
|
+
credentials: "same-origin",
|
|
86
|
+
method: "POST"
|
|
87
|
+
};
|
|
68
88
|
const defaultRPCOptions = {
|
|
69
|
-
|
|
89
|
+
rpcPrefix: "__rpc",
|
|
70
90
|
adapter: "express"
|
|
71
91
|
};
|
|
72
92
|
const defaultMiddlewareOptions = {
|
|
73
|
-
|
|
74
|
-
path: void 0
|
|
93
|
+
rpcPrefix: void 0,
|
|
94
|
+
path: void 0,
|
|
95
|
+
origin: void 0
|
|
75
96
|
};
|
|
76
97
|
//#endregion
|
|
77
98
|
//#region src/createFunction.ts
|
|
99
|
+
/**
|
|
100
|
+
* Creates a server-side RPC function.
|
|
101
|
+
* Registers the function in the server functions map and returns a client-compatible
|
|
102
|
+
* wrapper that exposes `data` (Promise) and `cancel` (function) for request lifecycle control.
|
|
103
|
+
* @param name - Unique identifier used by the RPC router to dispatch requests
|
|
104
|
+
* @param handler - The actual implementation receiving an AbortSignal followed by JSON-serializable arguments
|
|
105
|
+
* @param fnOptions - Optional contentType and credentials settings
|
|
106
|
+
* @returns A client stub with `data` promise and `cancel` method, auto-registered in the server map
|
|
107
|
+
*/
|
|
78
108
|
function createServerFunction(name, handler, fnOptions = {}) {
|
|
79
109
|
const options = Object.assign({}, defaultServerFnOptions, fnOptions);
|
|
80
110
|
const wrappedFunction = (...args) => {
|
|
81
111
|
const controller = new AbortController();
|
|
82
112
|
const cancel = (reason) => controller.abort(reason);
|
|
83
113
|
const fetcher = async () => {
|
|
84
|
-
if (controller.signal.aborted) throw new Error(
|
|
114
|
+
if (controller.signal.aborted) throw new Error(OPERATION_ABORTED);
|
|
85
115
|
return await handler(controller.signal, ...args);
|
|
86
116
|
};
|
|
87
117
|
return {
|
|
@@ -93,12 +123,12 @@ function createServerFunction(name, handler, fnOptions = {}) {
|
|
|
93
123
|
name: {
|
|
94
124
|
value: name,
|
|
95
125
|
enumerable: true,
|
|
96
|
-
configurable:
|
|
126
|
+
configurable: false
|
|
97
127
|
},
|
|
98
128
|
options: {
|
|
99
129
|
value: options,
|
|
100
130
|
enumerable: true,
|
|
101
|
-
configurable:
|
|
131
|
+
configurable: false
|
|
102
132
|
}
|
|
103
133
|
});
|
|
104
134
|
serverFunctionsMap.set(name, {
|
|
@@ -109,21 +139,78 @@ function createServerFunction(name, handler, fnOptions = {}) {
|
|
|
109
139
|
return wrappedFunction;
|
|
110
140
|
}
|
|
111
141
|
//#endregion
|
|
112
|
-
//#region src/
|
|
142
|
+
//#region src/validate.ts
|
|
113
143
|
const SAFE_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
114
144
|
const SAFE_PATH_SEGMENT = /^[A-Za-z0-9_$][A-Za-z0-9_$/-]*$/;
|
|
145
|
+
const CREDENTIALS_VALUES = [
|
|
146
|
+
"same-origin",
|
|
147
|
+
"include",
|
|
148
|
+
"omit"
|
|
149
|
+
];
|
|
150
|
+
/**
|
|
151
|
+
* Validates that a string is a safe JavaScript identifier.
|
|
152
|
+
* Used to prevent code injection when interpolating export names into generated client code.
|
|
153
|
+
* @param name - The string to validate
|
|
154
|
+
* @param label - Human-readable label for error messages (e.g. "export name")
|
|
155
|
+
* @returns The validated name if it passes
|
|
156
|
+
* @throws Error if the name contains characters outside /^[A-Za-z_$][A-Za-z0-9_$]*$/
|
|
157
|
+
*/
|
|
115
158
|
function validateIdentifier(name, label) {
|
|
116
|
-
if (!SAFE_IDENTIFIER.test(name)) throw new Error(
|
|
159
|
+
if (!SAFE_IDENTIFIER.test(name)) throw new Error(INVALID_IDENTIFIER(label, name));
|
|
117
160
|
return name;
|
|
118
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Validates that a string is a safe path segment for RPC routing.
|
|
164
|
+
* Allows alphanumeric characters, underscores, dollar signs, hyphens, and forward slashes.
|
|
165
|
+
* @param segment - The string to validate
|
|
166
|
+
* @param label - Human-readable label for error messages (e.g. "rpcPrefix")
|
|
167
|
+
* @returns The validated segment if it passes
|
|
168
|
+
* @throws Error if the segment contains disallowed characters
|
|
169
|
+
*/
|
|
119
170
|
function validatePathSegment(segment, label) {
|
|
120
|
-
if (!SAFE_PATH_SEGMENT.test(segment)) throw new Error(
|
|
171
|
+
if (!SAFE_PATH_SEGMENT.test(segment)) throw new Error(INVALID_PATH_SEGMENT(label, segment));
|
|
121
172
|
return segment;
|
|
122
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* Validates and normalizes the credentials option.
|
|
176
|
+
* Accepts "same-origin", "include", or "omit"; defaults to "same-origin" when undefined.
|
|
177
|
+
* @param value - Credentials value to validate
|
|
178
|
+
* @returns The validated credentials string
|
|
179
|
+
* @throws Error if the value is not one of the accepted credentials
|
|
180
|
+
*/
|
|
181
|
+
function validateCredentials(value) {
|
|
182
|
+
const creds = value || "same-origin";
|
|
183
|
+
if (!CREDENTIALS_VALUES.includes(creds)) throw new Error(`Invalid credentials: "${value}" must be one of ${CREDENTIALS_VALUES.join(", ")}`);
|
|
184
|
+
return creds;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Validates and normalizes the HTTP method option for a server function.
|
|
188
|
+
* Accepts "GET" or "POST" (case-insensitive); defaults to "POST" when undefined.
|
|
189
|
+
* @param value - Method value to validate
|
|
190
|
+
* @returns The validated uppercase method string
|
|
191
|
+
* @throws Error if the value is not "GET" or "POST"
|
|
192
|
+
*/
|
|
193
|
+
function validateMethod(value) {
|
|
194
|
+
const method = (value || "POST").toUpperCase();
|
|
195
|
+
if (method !== "GET" && method !== "POST") throw new Error(`Invalid method: "${value}" must be one of GET, POST`);
|
|
196
|
+
return method;
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region src/getClientModules.ts
|
|
200
|
+
/**
|
|
201
|
+
* Generates a JavaScript client module string for a single server function.
|
|
202
|
+
* All interpolated values are validated to prevent code injection.
|
|
203
|
+
* @param fnName - Registered RPC function name (validated as path segment)
|
|
204
|
+
* @param fnEntry - Export name used in the generated module (validated as identifier)
|
|
205
|
+
* @param options - Content type, credentials, and RPC prefix settings
|
|
206
|
+
* @returns A string of JavaScript code exporting the client stub
|
|
207
|
+
*/
|
|
123
208
|
const getModule = (fnName, fnEntry, options) => {
|
|
124
209
|
const safeFnName = validatePathSegment(fnName, "function name");
|
|
125
210
|
const safeFnEntry = validateIdentifier(fnEntry, "export name");
|
|
126
|
-
const safePrefix = validatePathSegment(options.
|
|
211
|
+
const safePrefix = validatePathSegment(options.rpcPrefix, "rpcPrefix");
|
|
212
|
+
const credentials = validateCredentials(options.credentials);
|
|
213
|
+
const method = validateMethod(options.method);
|
|
127
214
|
let body = "";
|
|
128
215
|
let headers = "{}";
|
|
129
216
|
switch (options.contentType) {
|
|
@@ -135,17 +222,30 @@ const getModule = (fnName, fnEntry, options) => {
|
|
|
135
222
|
body = `JSON.stringify(args)`;
|
|
136
223
|
headers = `{ 'Content-Type': 'application/json' }`;
|
|
137
224
|
}
|
|
225
|
+
if (method === "GET") {
|
|
226
|
+
body = `JSON.stringify(args)`;
|
|
227
|
+
headers = `{}`;
|
|
228
|
+
}
|
|
138
229
|
return `
|
|
139
230
|
export const ${safeFnEntry} = (...args) => {
|
|
140
231
|
const body = ${body};
|
|
141
232
|
const headers = ${headers};
|
|
142
|
-
const
|
|
233
|
+
const prefix = "${safePrefix}";
|
|
143
234
|
const name = "${safeFnName}";
|
|
144
|
-
|
|
235
|
+
const credentials = "${credentials}";
|
|
236
|
+
const method = "${method}";
|
|
237
|
+
return innerModule(body, headers, credentials, prefix, name, method);
|
|
145
238
|
}`.trim();
|
|
146
239
|
};
|
|
240
|
+
/**
|
|
241
|
+
* Generates the complete client-side module bundle by iterating all registered server functions
|
|
242
|
+
* and producing fetch-based stubs for each. The result is transformed by Vite (or Oxc) during
|
|
243
|
+
* the dev server or production build.
|
|
244
|
+
* @param initialOptions - Plugin options containing rpcPrefix and optional adapter
|
|
245
|
+
* @returns A string of JavaScript code with all client RPC modules and their import dependencies
|
|
246
|
+
*/
|
|
147
247
|
const getClientModules = (initialOptions) => {
|
|
148
|
-
validatePathSegment(initialOptions.
|
|
248
|
+
validatePathSegment(initialOptions.rpcPrefix, "rpcPrefix");
|
|
149
249
|
return `
|
|
150
250
|
|
|
151
251
|
import { innerModule } from "@thednp/rpc/helpers";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.mjs","names":[],"sources":["../../src/functionsMap.ts","../../src/scanForServerFiles.ts","../../src/options.ts","../../src/createFunction.ts","../../src/getClientModules.ts"],"sourcesContent":["import type { ServerFnEntry } from \"./types.d.ts\";\n\nexport const serverFunctionsMap = new Map<string, ServerFnEntry>();\n","import type { ResolvedConfig, ViteDevServer } from \"vite\";\nimport type { ClientFunctionWithOptions } from \"./types.d.ts\";\nimport { createServer } from \"vite\";\nimport { readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport process from \"node:process\";\n\nimport { serverFunctionsMap } from \"./functionsMap.ts\";\n\ntype ScanConfig = Pick<ResolvedConfig, \"root\" | \"base\"> & {\n server?: Partial<ResolvedConfig[\"server\"]>;\n};\n\nlet isScanned = false;\nexport const scanForServerFiles = async (\n initialCfg?: ScanConfig,\n devServer?: ViteDevServer,\n) => {\n if (isScanned && !devServer) {\n return;\n }\n const config = (!initialCfg && !devServer) || !initialCfg\n ? {\n root: process.cwd(),\n base: process.env.BASE || \"/\",\n server: { middlewareMode: true },\n }\n : {\n ...initialCfg,\n root: process.cwd(),\n };\n\n let server = devServer;\n if (!server) {\n server = await createServer({\n server: config.server,\n appType: \"custom\",\n base: config.base,\n root: config.root,\n });\n }\n\n const svFiles = [\"server.ts\", \"server.js\", \"server.mjs\", \"server.mts\"];\n const apiDir = join(config.root, \"src\", \"api\");\n let files: string[];\n try {\n files = (await readdir(apiDir, { withFileTypes: true }))\n .filter((f) => svFiles.some((fn) => f.name.includes(fn)))\n .map((f) => join(apiDir, f.name));\n } catch (_e) {\n files = [];\n }\n\n try {\n for (const file of files) {\n try {\n const moduleExports = (await server.ssrLoadModule(file)) as Record<\n string,\n ClientFunctionWithOptions\n >;\n const moduleEntries = Object.entries(moduleExports);\n if (!moduleEntries.length) {\n console.warn(\"No server function found.\");\n return;\n }\n\n for (const [exportName, exportValue] of moduleEntries) {\n // const registeredName = exportValue?.name ?? exportName;\n const registeredName = exportValue.name;\n serverFunctionsMap.set(registeredName, {\n name: registeredName,\n handler: exportValue,\n options: exportValue?.options,\n exportName,\n });\n }\n } catch (error) {\n console.error(\"Error loading file:\", file, error);\n }\n }\n } finally {\n if (!devServer && server) {\n await server.close();\n }\n isScanned = true;\n }\n};\n","import type {\n MiddlewareOptions,\n RpcPluginOptions,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\n\nexport const defaultServerFnOptions = {\n contentType: \"application/json\",\n} satisfies ServerFunctionOptions;\n\nexport const defaultRPCOptions: RpcPluginOptions = {\n rpcPreffix: \"__rpc\",\n adapter: \"express\",\n};\n\nexport const defaultMiddlewareOptions = {\n rpcPreffix: undefined,\n path: undefined,\n} satisfies MiddlewareOptions;\n","// /@thednp/rpc/src/createFn.ts\nimport type {\n ClientFunction,\n JsonArray,\n JsonValue,\n ServerFunctionInit,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\nimport { serverFunctionsMap } from \"./functionsMap.ts\";\nimport { defaultServerFnOptions } from \"./options.ts\";\n\nexport function createServerFunction<\n TArgs extends JsonArray = JsonArray,\n TResult extends JsonValue = JsonValue,\n>(\n name: string,\n handler: ServerFunctionInit<TArgs, TResult>,\n fnOptions: Partial<ServerFunctionOptions> = {},\n) {\n const options = Object.assign({}, defaultServerFnOptions, fnOptions);\n\n const wrappedFunction: ClientFunction<TArgs, TResult> = (...args: TArgs) => {\n const controller = new AbortController();\n const cancel = (reason: string) => controller.abort(reason);\n\n const fetcher = async () => {\n if (controller.signal.aborted) {\n throw new Error(\"Operation aborted\");\n }\n\n return await handler(controller.signal, ...args);\n };\n\n return {\n data: fetcher(),\n cancel,\n };\n };\n\n Object.defineProperties(wrappedFunction, {\n name: { value: name, enumerable: true, configurable: true },\n options: { value: options, enumerable: true, configurable: true },\n });\n\n serverFunctionsMap.set(\n name,\n {\n name,\n handler: wrappedFunction as never,\n options,\n },\n );\n\n return wrappedFunction;\n}\n","// Internal type that accepts all adapters\ninterface RpcPluginOptionsInternal {\n rpcPreffix: string;\n adapter?: string | undefined;\n}\n\nimport type { ServerFunctionOptions } from \"./types.d.ts\";\nimport { serverFunctionsMap } from \"./functionsMap.ts\";\n\n// Safe identifier pattern: must match JS identifier rules (letters, digits, $, _)\n// and must not contain regex metacharacters or template literal interpolation.\nconst SAFE_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;\n// Safe URL path segment: identifier characters plus \"/\" for nested prefixes like \"api/rpc\"\nconst SAFE_PATH_SEGMENT = /^[A-Za-z0-9_$][A-Za-z0-9_$/-]*$/;\n\nfunction validateIdentifier(name: string, label: string): string {\n // istanbul ignore if\n if (!SAFE_IDENTIFIER.test(name)) {\n throw new Error(\n `Invalid ${label}: \"${name}\" must match /^[A-Za-z_$][A-Za-z0-9_$]*$/`,\n );\n }\n return name;\n}\n\nfunction validatePathSegment(segment: string, label: string): string {\n // istanbul ignore if\n if (!SAFE_PATH_SEGMENT.test(segment)) {\n throw new Error(\n `Invalid ${label}: \"${segment}\" must match /^[A-Za-z0-9_$][A-Za-z0-9_$/-]*$/`,\n );\n }\n return segment;\n}\n\nconst getModule = (\n fnName: string,\n fnEntry: string,\n options: Partial<ServerFunctionOptions> & {\n contentType: ServerFunctionOptions[\"contentType\"];\n rpcPreffix: string;\n },\n) => {\n // Validate all interpolated strings to prevent code injection\n const safeFnName = validatePathSegment(fnName, \"function name\");\n const safeFnEntry = validateIdentifier(fnEntry, \"export name\");\n const safePrefix = validatePathSegment(options.rpcPreffix, \"rpcPreffix\");\n\n let body = \"\";\n let headers = \"{}\";\n switch (options.contentType) {\n case \"text/plain\":\n {\n body = `args[0]`;\n headers = `{ 'Content-Type': 'text/plain' }`;\n }\n break;\n default: {\n body = `JSON.stringify(args)`;\n headers = `{ 'Content-Type': 'application/json' }`;\n }\n }\n\n const output = `\nexport const ${safeFnEntry} = (...args) => {\n const body = ${body};\n const headers = ${headers};\n const preffix = \"${safePrefix}\";\n const name = \"${safeFnName}\";\n return innerModule(body, headers, preffix, name);\n}`;\n\n return output.trim();\n};\n\nexport const getClientModules = (initialOptions: RpcPluginOptionsInternal) => {\n // Validate prefix once at the top level\n validatePathSegment(initialOptions.rpcPreffix, \"rpcPreffix\");\n\n return `\n// Client-side RPC modules\nimport { innerModule } from \"@thednp/rpc/helpers\";\n${\n Array.from(serverFunctionsMap.entries())\n .filter(([, entry]) => entry.exportName)\n .map(([registeredName, entry]) =>\n getModule(registeredName, entry.exportName!, {\n ...initialOptions,\n ...((entry.options as ServerFunctionOptions) || {}),\n })\n )\n .join(\"\\n\")\n }\n`.trim();\n};\n"],"mappings":";;;;;AAEA,MAAa,qCAAqB,IAAI,IAA2B;;;ACWjE,IAAI,YAAY;AAChB,MAAa,qBAAqB,OAChC,YACA,cACG;CACH,IAAI,aAAa,CAAC,WAChB;CAEF,MAAM,SAAU,CAAC,cAAc,CAAC,aAAc,CAAC,aAC3C;EACA,MAAM,QAAQ,IAAI;EAClB,MAAM,QAAQ,IAAI,QAAQ;EAC1B,QAAQ,EAAE,gBAAgB,KAAK;CACjC,IACE;EACA,GAAG;EACH,MAAM,QAAQ,IAAI;CACpB;CAEF,IAAI,SAAS;CACb,IAAI,CAAC,QACH,SAAS,MAAM,aAAa;EAC1B,QAAQ,OAAO;EACf,SAAS;EACT,MAAM,OAAO;EACb,MAAM,OAAO;CACf,CAAC;CAGH,MAAM,UAAU;EAAC;EAAa;EAAa;EAAc;CAAY;CACrE,MAAM,SAAS,KAAK,OAAO,MAAM,OAAO,KAAK;CAC7C,IAAI;CACJ,IAAI;EACF,SAAS,MAAM,QAAQ,QAAQ,EAAE,eAAe,KAAK,CAAC,EAAA,CACnD,QAAQ,MAAM,QAAQ,MAAM,OAAO,EAAE,KAAK,SAAS,EAAE,CAAC,CAAC,CAAC,CACxD,KAAK,MAAM,KAAK,QAAQ,EAAE,IAAI,CAAC;CACpC,SAAS,IAAI;EACX,QAAQ,CAAC;CACX;CAEA,IAAI;EACF,KAAK,MAAM,QAAQ,OACjB,IAAI;GACF,MAAM,gBAAiB,MAAM,OAAO,cAAc,IAAI;GAItD,MAAM,gBAAgB,OAAO,QAAQ,aAAa;GAClD,IAAI,CAAC,cAAc,QAAQ;IACzB,QAAQ,KAAK,2BAA2B;IACxC;GACF;GAEA,KAAK,MAAM,CAAC,YAAY,gBAAgB,eAAe;IAErD,MAAM,iBAAiB,YAAY;IACnC,mBAAmB,IAAI,gBAAgB;KACrC,MAAM;KACN,SAAS;KACT,SAAS,aAAa;KACtB;IACF,CAAC;GACH;EACF,SAAS,OAAO;GACd,QAAQ,MAAM,uBAAuB,MAAM,KAAK;EAClD;CAEJ,UAAU;EACR,IAAI,CAAC,aAAa,QAChB,MAAM,OAAO,MAAM;EAErB,YAAY;CACd;AACF;;;AChFA,MAAa,yBAAyB,EACpC,aAAa,mBACf;AAEA,MAAa,oBAAsC;CACjD,YAAY;CACZ,SAAS;AACX;AAEA,MAAa,2BAA2B;CACtC,YAAY,KAAA;CACZ,MAAM,KAAA;AACR;;;ACPA,SAAgB,qBAId,MACA,SACA,YAA4C,CAAC,GAC7C;CACA,MAAM,UAAU,OAAO,OAAO,CAAC,GAAG,wBAAwB,SAAS;CAEnE,MAAM,mBAAmD,GAAG,SAAgB;EAC1E,MAAM,aAAa,IAAI,gBAAgB;EACvC,MAAM,UAAU,WAAmB,WAAW,MAAM,MAAM;EAE1D,MAAM,UAAU,YAAY;GAC1B,IAAI,WAAW,OAAO,SACpB,MAAM,IAAI,MAAM,mBAAmB;GAGrC,OAAO,MAAM,QAAQ,WAAW,QAAQ,GAAG,IAAI;EACjD;EAEA,OAAO;GACL,MAAM,QAAQ;GACd;EACF;CACF;CAEA,OAAO,iBAAiB,iBAAiB;EACvC,MAAM;GAAE,OAAO;GAAM,YAAY;GAAM,cAAc;EAAK;EAC1D,SAAS;GAAE,OAAO;GAAS,YAAY;GAAM,cAAc;EAAK;CAClE,CAAC;CAED,mBAAmB,IACjB,MACA;EACE;EACA,SAAS;EACT;CACF,CACF;CAEA,OAAO;AACT;;;AC3CA,MAAM,kBAAkB;AAExB,MAAM,oBAAoB;AAE1B,SAAS,mBAAmB,MAAc,OAAuB;CAE/D,IAAI,CAAC,gBAAgB,KAAK,IAAI,GAC5B,MAAM,IAAI,MACR,WAAW,MAAM,KAAK,KAAK,0CAC7B;CAEF,OAAO;AACT;AAEA,SAAS,oBAAoB,SAAiB,OAAuB;CAEnE,IAAI,CAAC,kBAAkB,KAAK,OAAO,GACjC,MAAM,IAAI,MACR,WAAW,MAAM,KAAK,QAAQ,+CAChC;CAEF,OAAO;AACT;AAEA,MAAM,aACJ,QACA,SACA,YAIG;CAEH,MAAM,aAAa,oBAAoB,QAAQ,eAAe;CAC9D,MAAM,cAAc,mBAAmB,SAAS,aAAa;CAC7D,MAAM,aAAa,oBAAoB,QAAQ,YAAY,YAAY;CAEvE,IAAI,OAAO;CACX,IAAI,UAAU;CACd,QAAQ,QAAQ,aAAhB;EACE,KAAK;GAED,OAAO;GACP,UAAU;GAEZ;EACF;GACE,OAAO;GACP,UAAU;CAEd;CAWA,OAAO;eARM,YAAY;iBACV,KAAK;oBACF,QAAQ;qBACP,WAAW;kBACd,WAAW;;GAIb,KAAK;AACrB;AAEA,MAAa,oBAAoB,mBAA6C;CAE5E,oBAAoB,eAAe,YAAY,YAAY;CAE3D,OAAO;;;EAIL,MAAM,KAAK,mBAAmB,QAAQ,CAAC,CAAC,CACrC,QAAQ,GAAG,WAAW,MAAM,UAAU,CAAC,CACvC,KAAK,CAAC,gBAAgB,WACrB,UAAU,gBAAgB,MAAM,YAAa;EAC3C,GAAG;EACH,GAAK,MAAM,WAAqC,CAAC;CACnD,CAAC,CACH,CAAC,CACA,KAAK,IAAI,EACb;EACD,KAAK;AACP"}
|
|
1
|
+
{"version":3,"file":"server.mjs","names":[],"sources":["../../src/functionsMap.ts","../../src/constants.ts","../../src/scanForServerFiles.ts","../../src/options.ts","../../src/createFunction.ts","../../src/validate.ts","../../src/getClientModules.ts"],"sourcesContent":["import type { ServerFnEntry } from \"./types.d.ts\";\n\nexport const serverFunctionsMap: Map<string, ServerFnEntry> = new Map<\n string,\n ServerFnEntry\n>();\n","export const OPERATION_ABORTED = \"Operation aborted\";\n\nexport const REQUEST_CANCELLED = \"Request was cancelled\";\n\nexport const FETCH_ERROR_PREFIX = \"Fetch error: \";\n\nexport const NO_SERVER_FUNCTION_FOUND = \"No server function found.\";\n\nexport const ERROR_LOADING_FILE = \"Error loading file:\";\n\nexport const FUNCTION_NOT_FOUND = \"Function not found\";\n\nexport const METHOD_NOT_ALLOWED = \"Method Not Allowed\";\n\nexport const REQUEST_FORBIDDEN = \"Forbidden\";\n\nexport const INTERNAL_SERVER_ERROR = \"Internal Server Error\";\n\nexport const CLIENT_DISCONNECTED = \"client disconnected\";\n\n/** Returns a warning when a middleware name is reused, preventing registration conflicts. @param name - The duplicate middleware name */\nexport const MIDDLEWARE_NAME_USED = (name: string) =>\n `The middleware name \"${name}\" is already used.`;\n\n/** Error message when a value fails the safe-identifier validation. @param label - What kind of value was being validated. @param name - The rejected value */\nexport const INVALID_IDENTIFIER = (label: string, name: string) =>\n `Invalid ${label}: \"${name}\" must match /^[A-Za-z_$][A-Za-z0-9_$]*$/`;\n\n/** Error message when a value fails the safe-path-segment validation. @param label - What kind of value was being validated. @param segment - The rejected value */\nexport const INVALID_PATH_SEGMENT = (label: string, segment: string) =>\n `Invalid ${label}: \"${segment}\" must match /^[A-Za-z0-9_$][A-Za-z0-9_$/-]*$/`;\n\n/** Warning message when a specified RPC config file cannot be resolved on disk. @param configFile - The requested config filename. @param configFilePath - The resolved absolute path */\nexport const CONFIG_FILE_NOT_FOUND = (\n configFile: string,\n configFilePath: string,\n) =>\n ` ⚠︎ The specified RPC config file ${configFile} cannot be found at ${configFilePath}, loading the defaults..`;\n\nexport const NO_CONFIG_FOUND = ` ⚡︎ No RPC config found, loading the defaults..`;\n\nexport const FAILED_LOAD_CONFIG = ` ⚠︎ Failed to load RPC config:`;\n","import type { ViteDevServer } from \"vite\";\nimport type { ClientFunctionWithOptions, ScanConfig } from \"./types.d.ts\";\nimport { createServer } from \"vite\";\nimport { readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport process from \"node:process\";\n\nimport { serverFunctionsMap } from \"./functionsMap.ts\";\nimport { ERROR_LOADING_FILE, NO_SERVER_FUNCTION_FOUND } from \"./constants.ts\";\n\nlet isScanned = false;\n/**\n * Scans `src/api/` for server function files (`server.ts`, `server.js`, `server.mjs`, `server.mts`)\n * and populates the global `serverFunctionsMap` with their exported functions.\n * Uses Vite's SSR module loading to resolve and execute each file.\n * @param initialCfg - Optional Vite config overrides (root, base, server)\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 const config = (!initialCfg && !devServer) || !initialCfg\n ? {\n root: process.cwd(),\n base: process.env.BASE || \"/\",\n server: { middlewareMode: true },\n }\n : {\n ...initialCfg,\n root: process.cwd(),\n };\n\n let server = devServer;\n if (!server) {\n server = await createServer({\n server: config.server,\n appType: \"custom\",\n base: config.base,\n root: config.root,\n });\n }\n\n const svFiles = [\"server.ts\", \"server.js\", \"server.mjs\", \"server.mts\"];\n const apiDir = join(config.root, \"src\", \"api\");\n let files: string[];\n try {\n files = (await readdir(apiDir, { withFileTypes: true }))\n .filter((f) => svFiles.includes(f.name))\n .map((f) => join(apiDir, f.name));\n } catch (_e) {\n files = [];\n }\n\n try {\n for (const file of files) {\n try {\n const moduleExports = (await server.ssrLoadModule(file)) as Record<\n string,\n ClientFunctionWithOptions\n >;\n const moduleEntries = Object.entries(moduleExports);\n if (!moduleEntries.length) {\n console.warn(NO_SERVER_FUNCTION_FOUND);\n return;\n }\n\n for (const [exportName, exportValue] of moduleEntries) {\n // const registeredName = exportValue?.name ?? exportName;\n const registeredName = exportValue.name;\n serverFunctionsMap.set(registeredName, {\n name: registeredName,\n handler: exportValue,\n options: exportValue?.options,\n exportName,\n });\n }\n } catch (error) {\n console.error(ERROR_LOADING_FILE, file, error);\n }\n }\n } finally {\n if (!devServer && server) {\n await server.close();\n }\n isScanned = true;\n }\n};\n","import type {\n MiddlewareOptions,\n RpcPluginOptions,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\n\nexport const defaultServerFnOptions = {\n contentType: \"application/json\",\n credentials: \"same-origin\",\n method: \"POST\",\n} satisfies ServerFunctionOptions;\n\nexport const defaultRPCOptions: RpcPluginOptions = {\n rpcPrefix: \"__rpc\",\n adapter: \"express\",\n};\n\nexport const defaultMiddlewareOptions = {\n rpcPrefix: undefined,\n path: undefined,\n origin: undefined,\n} satisfies MiddlewareOptions;\n","// /@thednp/rpc/src/createFn.ts\nimport type {\n ClientFunction,\n JsonArray,\n JsonValue,\n ServerFunctionInit,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\nimport { serverFunctionsMap } from \"./functionsMap.ts\";\nimport { defaultServerFnOptions } from \"./options.ts\";\nimport { OPERATION_ABORTED } from \"./constants.ts\";\n\n/**\n * Creates a server-side RPC function.\n * Registers the function in the server functions map and returns a client-compatible\n * wrapper that exposes `data` (Promise) and `cancel` (function) for request lifecycle control.\n * @param name - Unique identifier used by the RPC router to dispatch requests\n * @param handler - The actual implementation receiving an AbortSignal followed by JSON-serializable arguments\n * @param fnOptions - Optional contentType and credentials settings\n * @returns A client stub with `data` promise and `cancel` method, auto-registered in the server map\n */\nexport function createServerFunction<\n TArgs extends JsonArray = JsonArray,\n TResult extends JsonValue = JsonValue,\n>(\n name: string,\n handler: ServerFunctionInit<TArgs, TResult>,\n fnOptions: Partial<ServerFunctionOptions> = {},\n): ClientFunction<TArgs, TResult> {\n const options = Object.assign({}, defaultServerFnOptions, fnOptions);\n\n const wrappedFunction: ClientFunction<TArgs, TResult> = (...args: TArgs) => {\n const controller = new AbortController();\n const cancel = (reason: string) => controller.abort(reason);\n\n const fetcher = async () => {\n if (controller.signal.aborted) {\n throw new Error(OPERATION_ABORTED);\n }\n\n return await handler(controller.signal, ...args);\n };\n\n return {\n data: fetcher(),\n cancel,\n };\n };\n\n Object.defineProperties(wrappedFunction, {\n name: { value: name, enumerable: true, configurable: false },\n options: { value: options, enumerable: true, configurable: false },\n });\n\n serverFunctionsMap.set(\n name,\n {\n name,\n handler: wrappedFunction as never,\n options,\n },\n );\n\n return wrappedFunction;\n}\n","import type { Credentials } from \"./types.d.ts\";\nimport { INVALID_IDENTIFIER, INVALID_PATH_SEGMENT } from \"./constants.ts\";\n\nconst SAFE_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;\nconst SAFE_PATH_SEGMENT = /^[A-Za-z0-9_$][A-Za-z0-9_$/-]*$/;\nconst CREDENTIALS_VALUES: readonly Credentials[] = [\n \"same-origin\",\n \"include\",\n \"omit\",\n];\n\n/**\n * Validates that a string is a safe JavaScript identifier.\n * Used to prevent code injection when interpolating export names into generated client code.\n * @param name - The string to validate\n * @param label - Human-readable label for error messages (e.g. \"export name\")\n * @returns The validated name if it passes\n * @throws Error if the name contains characters outside /^[A-Za-z_$][A-Za-z0-9_$]*$/\n */\nexport function validateIdentifier(name: string, label: string): string {\n if (!SAFE_IDENTIFIER.test(name)) {\n throw new Error(INVALID_IDENTIFIER(label, name));\n }\n return name;\n}\n\n/**\n * Validates that a string is a safe path segment for RPC routing.\n * Allows alphanumeric characters, underscores, dollar signs, hyphens, and forward slashes.\n * @param segment - The string to validate\n * @param label - Human-readable label for error messages (e.g. \"rpcPrefix\")\n * @returns The validated segment if it passes\n * @throws Error if the segment contains disallowed characters\n */\nexport function validatePathSegment(segment: string, label: string): string {\n if (!SAFE_PATH_SEGMENT.test(segment)) {\n throw new Error(INVALID_PATH_SEGMENT(label, segment));\n }\n return segment;\n}\n\n/**\n * Validates and normalizes the credentials option.\n * Accepts \"same-origin\", \"include\", or \"omit\"; defaults to \"same-origin\" when undefined.\n * @param value - Credentials value to validate\n * @returns The validated credentials string\n * @throws Error if the value is not one of the accepted credentials\n */\nexport function validateCredentials(value?: string): Credentials {\n const creds = value || \"same-origin\";\n if (!CREDENTIALS_VALUES.includes(creds as Credentials)) {\n throw new Error(\n `Invalid credentials: \"${value}\" must be one of ${\n CREDENTIALS_VALUES.join(\", \")\n }`,\n );\n }\n return creds as Credentials;\n}\n\n/**\n * Validates and normalizes the HTTP method option for a server function.\n * Accepts \"GET\" or \"POST\" (case-insensitive); defaults to \"POST\" when undefined.\n * @param value - Method value to validate\n * @returns The validated uppercase method string\n * @throws Error if the value is not \"GET\" or \"POST\"\n */\nexport function validateMethod(value?: string): \"GET\" | \"POST\" {\n const method = (value || \"POST\").toUpperCase();\n if (method !== \"GET\" && method !== \"POST\") {\n throw new Error(`Invalid method: \"${value}\" must be one of GET, POST`);\n }\n return method;\n}\n","/**\n * @module Client module generation.\n */\nimport type {\n RpcPluginOptionsInternal,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\nimport { serverFunctionsMap } from \"./functionsMap.ts\";\nimport {\n validateCredentials,\n validateIdentifier,\n validateMethod,\n validatePathSegment,\n} from \"./validate.ts\";\n\n/**\n * Generates a JavaScript client module string for a single server function.\n * All interpolated values are validated to prevent code injection.\n * @param fnName - Registered RPC function name (validated as path segment)\n * @param fnEntry - Export name used in the generated module (validated as identifier)\n * @param options - Content type, credentials, and RPC prefix settings\n * @returns A string of JavaScript code exporting the client stub\n */\nconst getModule = (\n fnName: string,\n fnEntry: string,\n options: Partial<ServerFunctionOptions> & {\n contentType: ServerFunctionOptions[\"contentType\"];\n rpcPrefix: string;\n },\n): string => {\n // Validate all interpolated strings to prevent code injection\n const safeFnName = validatePathSegment(fnName, \"function name\");\n const safeFnEntry = validateIdentifier(fnEntry, \"export name\");\n const safePrefix = validatePathSegment(options.rpcPrefix, \"rpcPrefix\");\n const credentials = validateCredentials(options.credentials);\n const method = validateMethod(options.method);\n let body = \"\";\n let headers = \"{}\";\n switch (options.contentType) {\n case \"text/plain\":\n {\n body = `args[0]`;\n headers = `{ 'Content-Type': 'text/plain' }`;\n }\n break;\n default: {\n body = `JSON.stringify(args)`;\n headers = `{ 'Content-Type': 'application/json' }`;\n }\n }\n // GET requests cannot carry a body: args travel as a JSON query parameter\n if (method === \"GET\") {\n body = `JSON.stringify(args)`;\n headers = `{}`;\n }\n\n const output = `\nexport const ${safeFnEntry} = (...args) => {\n const body = ${body};\n const headers = ${headers};\n const prefix = \"${safePrefix}\";\n const name = \"${safeFnName}\";\n const credentials = \"${credentials}\";\n const method = \"${method}\";\n return innerModule(body, headers, credentials, prefix, name, method);\n}`;\n\n return output.trim();\n};\n\n/**\n * Generates the complete client-side module bundle by iterating all registered server functions\n * and producing fetch-based stubs for each. The result is transformed by Vite (or Oxc) during\n * the dev server or production build.\n * @param initialOptions - Plugin options containing rpcPrefix and optional adapter\n * @returns A string of JavaScript code with all client RPC modules and their import dependencies\n */\nexport const getClientModules = (\n initialOptions: RpcPluginOptionsInternal,\n): string => {\n // Validate prefix once at the top level\n validatePathSegment(initialOptions.rpcPrefix, \"rpcPrefix\");\n\n return `\n// Client-side RPC modules\nimport { innerModule } from \"@thednp/rpc/helpers\";\n${\n Array.from(serverFunctionsMap.entries())\n .filter(([, entry]) => entry.exportName)\n .map(([registeredName, entry]) =>\n getModule(registeredName, entry.exportName!, {\n ...initialOptions,\n ...((entry.options as ServerFunctionOptions) || {}),\n })\n )\n .join(\"\\n\")\n }\n`.trim();\n};\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,24 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thednp/rpc",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"author": "thednp",
|
|
4
6
|
"description": "⚡ A Vite plugin for creating server functions with automatic Remote Procedure Calls (RPC)",
|
|
5
7
|
"homepage": "https://github.com/thednp/rpc#readme",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"rpc",
|
|
10
|
+
"vite",
|
|
11
|
+
"vite-plugin",
|
|
12
|
+
"express",
|
|
13
|
+
"fastify",
|
|
14
|
+
"hono",
|
|
15
|
+
"koa"
|
|
16
|
+
],
|
|
6
17
|
"bugs": {
|
|
7
18
|
"url": "https://github.com/thednp/rpc/issues"
|
|
8
19
|
},
|
|
9
20
|
"repository": {
|
|
10
21
|
"type": "git",
|
|
11
|
-
"url": "
|
|
22
|
+
"url": "https://github.com/thednp/rpc.git"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public",
|
|
26
|
+
"provenance": false
|
|
12
27
|
},
|
|
13
|
-
"license": "MIT",
|
|
14
|
-
"author": "thednp",
|
|
15
28
|
"type": "module",
|
|
16
29
|
"types": "./dist/index.d.mts",
|
|
30
|
+
"sideEffects": false,
|
|
17
31
|
"files": [
|
|
18
32
|
"dist",
|
|
19
|
-
"wiki",
|
|
20
33
|
"README.md",
|
|
21
34
|
"AGENTS.md",
|
|
35
|
+
"CLAUDE.md",
|
|
22
36
|
"LICENSE"
|
|
23
37
|
],
|
|
24
38
|
"exports": {
|
|
@@ -32,31 +46,11 @@
|
|
|
32
46
|
"./server": "./dist/server/server.mjs",
|
|
33
47
|
"./package.json": "./package.json"
|
|
34
48
|
},
|
|
35
|
-
"dependencies": {
|
|
36
|
-
"vite": "^8.1.5"
|
|
37
|
-
},
|
|
38
|
-
"devDependencies": {
|
|
39
|
-
"@hono/node-server": "^2.0.12",
|
|
40
|
-
"@types/express": "^5.0.6",
|
|
41
|
-
"@types/koa": "^3.0.3",
|
|
42
|
-
"@types/node": "^26.1.2",
|
|
43
|
-
"@vitest/coverage-istanbul": "^4.1.10",
|
|
44
|
-
"@vitest/ui": "^4.1.10",
|
|
45
|
-
"fastify": "^5.10.0",
|
|
46
|
-
"fastify-plugin": "^6.0.0",
|
|
47
|
-
"hono": "^4.12.32",
|
|
48
|
-
"koa": "^3.2.1",
|
|
49
|
-
"picocolors": "^1.1.1",
|
|
50
|
-
"tsdown": "^0.22.14",
|
|
51
|
-
"typescript": "^7.0.2",
|
|
52
|
-
"vite-plugin-strip-comments": "^0.0.10",
|
|
53
|
-
"vitest": "^4.1.10"
|
|
54
|
-
},
|
|
55
49
|
"scripts": {
|
|
56
50
|
"test": "vitest",
|
|
57
51
|
"test-ui": "vitest --ui",
|
|
58
|
-
"test-dev": "node dev-test",
|
|
59
|
-
"test-prod": "node dev-test --mode=preview",
|
|
52
|
+
"test-dev": "node scripts/dev-test",
|
|
53
|
+
"test-prod": "node scripts/dev-test --mode=preview",
|
|
60
54
|
"format": "deno fmt src tests examples/**/src",
|
|
61
55
|
"clean": "rm -rf examples/**/dist examples/**/node_modules examples/**/package-lock.json examples/**/pnpm-lock.yaml examples/**/bun.lockb examples/**/deno.lock",
|
|
62
56
|
"dev": "cd examples/spa && pnpm up && pnpm dev",
|
|
@@ -69,10 +63,33 @@
|
|
|
69
63
|
"lint:ts": "deno lint src",
|
|
70
64
|
"fix:ts": "deno lint src --fix",
|
|
71
65
|
"check:ts": "tsc -noEmit",
|
|
72
|
-
"up:examples": "pnpm up -r --latest --filter \"./examples/*\"",
|
|
73
|
-
"up:examples_1": "for d in examples/*/; do (cd \"$d\" && pnpm up --latest); done",
|
|
66
|
+
"up:examples": "pnpm up -r --latest --filter \"./examples/*\" && pnpm ci -r --filter \"./examples/*\"",
|
|
74
67
|
"up:root": "pnpm up --latest",
|
|
68
|
+
"up:deno": "deno update && deno -A scripts/update-deno.js",
|
|
75
69
|
"upd": "pnpm up:examples && pnpm up:root",
|
|
70
|
+
"audit:src": "node scripts/audit-src.js",
|
|
71
|
+
"prepublishOnly": "pnpm upd && pnpm up:deno && pnpm lint && pnpm format && pnpm audit:src && pnpm build",
|
|
76
72
|
"build": "tsdown"
|
|
77
|
-
}
|
|
78
|
-
|
|
73
|
+
},
|
|
74
|
+
"dependencies": {
|
|
75
|
+
"express": "^5.2.1",
|
|
76
|
+
"fastify": "^5.11.0",
|
|
77
|
+
"fastify-plugin": "^6.0.0",
|
|
78
|
+
"hono": "^4.12.33",
|
|
79
|
+
"koa": "^3.2.1",
|
|
80
|
+
"vite": "^8.2.0"
|
|
81
|
+
},
|
|
82
|
+
"devDependencies": {
|
|
83
|
+
"@hono/node-server": "^2.0.12",
|
|
84
|
+
"@types/express": "^5.0.6",
|
|
85
|
+
"@types/koa": "^3.0.3",
|
|
86
|
+
"@types/node": "^26.1.2",
|
|
87
|
+
"@vitest/coverage-istanbul": "^4.1.10",
|
|
88
|
+
"@vitest/ui": "^4.1.10",
|
|
89
|
+
"tsdown": "^0.22.14",
|
|
90
|
+
"typescript": "^7.0.2",
|
|
91
|
+
"vite-plugin-strip-comments": "^0.0.10",
|
|
92
|
+
"vitest": "^4.1.10"
|
|
93
|
+
},
|
|
94
|
+
"packageManager": "pnpm@11.18.0"
|
|
95
|
+
}
|