@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.
Files changed (45) hide show
  1. package/AGENTS.md +5 -5
  2. package/CLAUDE.md +1 -0
  3. package/README.md +195 -47
  4. package/dist/express/express.d.mts +110 -16
  5. package/dist/express/express.d.mts.map +1 -1
  6. package/dist/express/express.mjs +113 -20
  7. package/dist/express/express.mjs.map +1 -1
  8. package/dist/fastify/fastify.d.mts +83 -5
  9. package/dist/fastify/fastify.d.mts.map +1 -1
  10. package/dist/fastify/fastify.mjs +84 -18
  11. package/dist/fastify/fastify.mjs.map +1 -1
  12. package/dist/fastify/plugin/fastify/plugin.d.mts +64 -9
  13. package/dist/fastify/plugin/fastify/plugin.d.mts.map +1 -1
  14. package/dist/fastify/plugin/fastify/plugin.mjs +73 -18
  15. package/dist/fastify/plugin/fastify/plugin.mjs.map +1 -1
  16. package/dist/helpers/helpers.d.mts +66 -4
  17. package/dist/helpers/helpers.d.mts.map +1 -1
  18. package/dist/helpers/helpers.mjs +34 -7
  19. package/dist/helpers/helpers.mjs.map +1 -1
  20. package/dist/hono/hono.d.mts +75 -6
  21. package/dist/hono/hono.d.mts.map +1 -1
  22. package/dist/hono/hono.mjs +84 -24
  23. package/dist/hono/hono.mjs.map +1 -1
  24. package/dist/index.d.mts +210 -19
  25. package/dist/index.d.mts.map +1 -1
  26. package/dist/index.mjs +137 -31
  27. package/dist/index.mjs.map +1 -1
  28. package/dist/koa/koa.d.mts +52 -0
  29. package/dist/koa/koa.d.mts.map +1 -1
  30. package/dist/koa/koa.mjs +86 -16
  31. package/dist/koa/koa.mjs.map +1 -1
  32. package/dist/server/server.d.mts +111 -10
  33. package/dist/server/server.d.mts.map +1 -1
  34. package/dist/server/server.mjs +117 -17
  35. package/dist/server/server.mjs.map +1 -1
  36. package/package.json +48 -31
  37. package/wiki/adapters.md +0 -143
  38. package/wiki/best-practices.md +0 -201
  39. package/wiki/client-usage.md +0 -62
  40. package/wiki/configuration.md +0 -77
  41. package/wiki/getting-started.md +0 -76
  42. package/wiki/index.md +0 -26
  43. package/wiki/security.md +0 -54
  44. package/wiki/server-functions.md +0 -93
  45. package/wiki/setup.md +0 -77
@@ -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
- * rpcPreffix: "api/rpc"
132
+ * rpcPrefix: "api/rpc"
51
133
  */
52
- rpcPreffix: "__rpc" | string;
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
- type ScanConfig = Pick<ResolvedConfig, "root" | "base"> & {
67
- server?: Partial<ResolvedConfig["server"]>;
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
- interface RpcPluginOptionsInternal {
76
- rpcPreffix: string;
77
- adapter?: string | undefined;
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
- rpcPreffix: undefined;
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":";;;;;;;;KAmCY;UAMK;EACf,aAAa;;;KAIH;KACA;GAAgB,cAAc,YAAY;;KAC1C,YAAY;KACZ,YAAY,gBAAgB,YAAY;KA2BxC,mBACV,cAAc,YAAY,WAC1B,gBAAgB,YAAY,cACzB,QAAQ,gBAAgB,MAAM,UAAU,QAAQ;KAEzC,eACV,cAAc,YAAY,WAC1B,gBAAgB,YAAY,iBACtB,MAAM;EACZ,MAAM,QAAQ;EACd,SAAS;;KAGC,4BAA4B;EACtC;EACA,UAAU;;UAGK;EACf;EACA,SAAS;EACT,UAAU;EACV;;;;;;;;UAee;;;;;;;;;;;EAWf;;;;;;;EAQA;;;;cClIW,oBAAkB,YAAA;;;KCO1B,aAAa,KAAK;EACrB,SAAS,QAAQ;;cAIN,qBAAkB,aAChB,YAAU,YACX,kBAAa;;;iBCLX,qBACd,cAAc,YAAY,WAC1B,gBAAgB,YAAY,WAE5B,cACA,SAAS,mBAAmB,OAAO,UACnC,YAAW,QAAQ,yBAA2B,eAAA,OAAA;;;UChBtC;EACR;EACA;;cAwEW,mBAAgB,gBAAoB;;;cCrEpC;EAAA;;cAIA,mBAAmB;cAKnB;EAAA;EAAA"}
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"}
@@ -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.some((fn) => f.name.includes(fn))).map((f) => join(apiDir, f.name));
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("No server function found.");
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("Error loading file:", file, 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 = { contentType: "application/json" };
83
+ const defaultServerFnOptions = {
84
+ contentType: "application/json",
85
+ credentials: "same-origin",
86
+ method: "POST"
87
+ };
68
88
  const defaultRPCOptions = {
69
- rpcPreffix: "__rpc",
89
+ rpcPrefix: "__rpc",
70
90
  adapter: "express"
71
91
  };
72
92
  const defaultMiddlewareOptions = {
73
- rpcPreffix: void 0,
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("Operation aborted");
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: true
126
+ configurable: false
97
127
  },
98
128
  options: {
99
129
  value: options,
100
130
  enumerable: true,
101
- configurable: true
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/getClientModules.ts
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(`Invalid ${label}: "${name}" must match /^[A-Za-z_$][A-Za-z0-9_$]*$/`);
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(`Invalid ${label}: "${segment}" must match /^[A-Za-z0-9_$][A-Za-z0-9_$/-]*$/`);
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.rpcPreffix, "rpcPreffix");
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 preffix = "${safePrefix}";
233
+ const prefix = "${safePrefix}";
143
234
  const name = "${safeFnName}";
144
- return innerModule(body, headers, preffix, name);
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.rpcPreffix, "rpcPreffix");
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":"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"}
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.1",
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": "git+https://github.com/thednp/rpc.git"
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
+ }