@thednp/rpc 0.0.13 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +16 -14
- package/CHANGELOG.md +276 -0
- package/README.md +27 -8
- package/dist/express/express.d.mts.map +1 -1
- package/dist/express/express.mjs +55 -9
- package/dist/express/express.mjs.map +1 -1
- package/dist/fastify/fastify.d.mts +4 -1
- package/dist/fastify/fastify.d.mts.map +1 -1
- package/dist/fastify/fastify.mjs +51 -7
- package/dist/fastify/fastify.mjs.map +1 -1
- package/dist/fastify/plugin/fastify/plugin.d.mts +14 -0
- package/dist/fastify/plugin/fastify/plugin.d.mts.map +1 -1
- package/dist/fastify/plugin/fastify/plugin.mjs +51 -7
- package/dist/fastify/plugin/fastify/plugin.mjs.map +1 -1
- package/dist/helpers/helpers.d.mts +2 -2
- package/dist/helpers/helpers.d.mts.map +1 -1
- package/dist/helpers/helpers.mjs +1 -1
- package/dist/helpers/helpers.mjs.map +1 -1
- package/dist/hono/hono.d.mts +4 -1
- package/dist/hono/hono.d.mts.map +1 -1
- package/dist/hono/hono.mjs +51 -7
- package/dist/hono/hono.mjs.map +1 -1
- package/dist/index.d.mts +25 -6
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +257 -226
- package/dist/index.mjs.map +1 -1
- package/dist/koa/koa.d.mts.map +1 -1
- package/dist/koa/koa.mjs +51 -7
- package/dist/koa/koa.mjs.map +1 -1
- package/dist/server/server.d.mts +58 -8
- package/dist/server/server.d.mts.map +1 -1
- package/dist/server/server.mjs +124 -29
- package/dist/server/server.mjs.map +1 -1
- package/package.json +11 -9
package/dist/index.mjs
CHANGED
|
@@ -1,260 +1,268 @@
|
|
|
1
1
|
import { loadConfigFromFile, mergeConfig } from "vite";
|
|
2
|
-
import { resolve } from "node:path";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
|
-
import {
|
|
5
|
+
import { readdir } from "node:fs/promises";
|
|
6
|
+
import { createRPCMiddleware } from "@thednp/rpc/express";
|
|
6
7
|
//#region src/options.ts
|
|
7
8
|
const defaultRPCOptions = {
|
|
8
9
|
rpcPrefix: "__rpc",
|
|
9
|
-
adapter: "express"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
rpcPrefix: void 0,
|
|
13
|
-
path: void 0,
|
|
14
|
-
origin: void 0
|
|
10
|
+
adapter: "express",
|
|
11
|
+
serverFiles: "exact",
|
|
12
|
+
scanRoot: void 0
|
|
15
13
|
};
|
|
16
14
|
//#endregion
|
|
17
|
-
//#region src/
|
|
15
|
+
//#region src/constants.ts
|
|
16
|
+
const NO_SERVER_FUNCTION_FOUND = "No server function found.";
|
|
17
|
+
const ERROR_LOADING_FILE = "Error loading file:";
|
|
18
|
+
/** Error message when a value fails the safe-identifier validation. @param label - What kind of value was being validated. @param name - The rejected value */
|
|
19
|
+
const INVALID_IDENTIFIER = (label, name) => `Invalid ${label}: "${name}" must match /^[A-Za-z_$][A-Za-z0-9_$]*$/`;
|
|
20
|
+
/** 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 */
|
|
21
|
+
const INVALID_PATH_SEGMENT = (label, segment) => `Invalid ${label}: "${segment}" must match /^[A-Za-z0-9_$][A-Za-z0-9_$/-]*$/`;
|
|
22
|
+
/** 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 */
|
|
23
|
+
const CONFIG_FILE_NOT_FOUND = (configFile, configFilePath) => ` ⚠︎ The specified RPC config file ${configFile} cannot be found at ${configFilePath}, loading the defaults..`;
|
|
24
|
+
const NO_CONFIG_FOUND = ` ⚡︎ No RPC config found, loading the defaults..`;
|
|
25
|
+
const FAILED_LOAD_CONFIG = ` ⚠︎ Failed to load RPC config:`;
|
|
26
|
+
/** Error template for duplicate server function names across files. @param name - The duplicate registered name */
|
|
27
|
+
const DUPLICATE_FUNCTION_NAME = (name) => `Duplicate server function "${name}" detected. Each server function must have a unique name. Remove or rename the duplicate.`;
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/functionsMap.ts
|
|
30
|
+
const serverFunctionsMap = /* @__PURE__ */ new Map();
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/validate.ts
|
|
33
|
+
const SAFE_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
34
|
+
const SAFE_PATH_SEGMENT = /^[A-Za-z0-9_$@][A-Za-z0-9_$@/-]*$/;
|
|
35
|
+
const CREDENTIALS_VALUES = [
|
|
36
|
+
"same-origin",
|
|
37
|
+
"include",
|
|
38
|
+
"omit"
|
|
39
|
+
];
|
|
18
40
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* @param
|
|
23
|
-
* @returns
|
|
41
|
+
* Validates that a string is a safe JavaScript identifier.
|
|
42
|
+
* Used to prevent code injection when interpolating export names into generated client code.
|
|
43
|
+
* @param name - The string to validate
|
|
44
|
+
* @param label - Human-readable label for error messages (e.g. "export name")
|
|
45
|
+
* @returns The validated name if it passes
|
|
46
|
+
* @throws Error if the name contains characters outside /^[A-Za-z_$][A-Za-z0-9_$]*$/
|
|
24
47
|
*/
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
resolve({
|
|
30
|
-
contentType: isJSON ? "application/json" : "text/plain",
|
|
31
|
-
data: isJSON ? req.body : String(req.body)
|
|
32
|
-
});
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
let body = "";
|
|
36
|
-
const toggleListeners = (add) => {
|
|
37
|
-
const method = add ? "on" : "off";
|
|
38
|
-
req[method]("data", onData);
|
|
39
|
-
req[method]("end", onEnd);
|
|
40
|
-
req[method]("error", onError);
|
|
41
|
-
};
|
|
42
|
-
const onData = (chunk) => {
|
|
43
|
-
body += chunk.toString();
|
|
44
|
-
};
|
|
45
|
-
const onEnd = () => {
|
|
46
|
-
toggleListeners();
|
|
47
|
-
const isJSON = (req.headers["content-type"]?.toLowerCase() || "").includes("json");
|
|
48
|
-
try {
|
|
49
|
-
const data = JSON.parse(body);
|
|
50
|
-
resolve({
|
|
51
|
-
contentType: isJSON ? "application/json" : "text/plain",
|
|
52
|
-
data
|
|
53
|
-
});
|
|
54
|
-
} catch (_e) {
|
|
55
|
-
resolve({
|
|
56
|
-
contentType: "text/plain",
|
|
57
|
-
data: String(body)
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
const onError = (err) => {
|
|
62
|
-
toggleListeners();
|
|
63
|
-
reject(err);
|
|
64
|
-
};
|
|
65
|
-
toggleListeners(true);
|
|
66
|
-
});
|
|
67
|
-
};
|
|
48
|
+
function validateIdentifier(name, label) {
|
|
49
|
+
if (!SAFE_IDENTIFIER.test(name)) throw new Error(INVALID_IDENTIFIER(label, name));
|
|
50
|
+
return name;
|
|
51
|
+
}
|
|
68
52
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* @
|
|
53
|
+
* Validates that a string is a safe path segment for RPC routing.
|
|
54
|
+
* Allows alphanumeric characters, underscores, dollar signs, at signs, hyphens, and forward slashes.
|
|
55
|
+
* @param segment - The string to validate
|
|
56
|
+
* @param label - Human-readable label for error messages (e.g. "rpcPrefix")
|
|
57
|
+
* @returns The validated segment if it passes
|
|
58
|
+
* @throws Error if the segment contains disallowed characters
|
|
72
59
|
*/
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
60
|
+
function validatePathSegment(segment, label) {
|
|
61
|
+
if (!SAFE_PATH_SEGMENT.test(segment)) throw new Error(INVALID_PATH_SEGMENT(label, segment));
|
|
62
|
+
return segment;
|
|
63
|
+
}
|
|
76
64
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* @
|
|
65
|
+
* Validates and normalizes the credentials option.
|
|
66
|
+
* Accepts "same-origin", "include", or "omit"; defaults to "same-origin" when undefined.
|
|
67
|
+
* @param value - Credentials value to validate
|
|
68
|
+
* @returns The validated credentials string
|
|
69
|
+
* @throws Error if the value is not one of the accepted credentials
|
|
80
70
|
*/
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
};
|
|
71
|
+
function validateCredentials(value) {
|
|
72
|
+
const creds = value || "same-origin";
|
|
73
|
+
if (!CREDENTIALS_VALUES.includes(creds)) throw new Error(`Invalid credentials: "${value}" must be one of ${CREDENTIALS_VALUES.join(", ")}`);
|
|
74
|
+
return creds;
|
|
75
|
+
}
|
|
84
76
|
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* @param
|
|
88
|
-
* @returns
|
|
77
|
+
* Validates and normalizes the HTTP method option for a server function.
|
|
78
|
+
* Accepts "GET" or "POST" (case-insensitive); defaults to "POST" when undefined.
|
|
79
|
+
* @param value - Method value to validate
|
|
80
|
+
* @returns The validated uppercase method string
|
|
81
|
+
* @throws Error if the value is not "GET" or "POST"
|
|
89
82
|
*/
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
};
|
|
83
|
+
function validateMethod(value) {
|
|
84
|
+
const method = (value || "POST").toUpperCase();
|
|
85
|
+
if (method !== "GET" && method !== "POST") throw new Error(`Invalid method: "${value}" must be one of GET, POST`);
|
|
86
|
+
return method;
|
|
87
|
+
}
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region src/getClientModules.ts
|
|
93
90
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* @param
|
|
97
|
-
* @
|
|
91
|
+
* Generates a JavaScript client module string for a single server function.
|
|
92
|
+
* All interpolated values are validated to prevent code injection.
|
|
93
|
+
* @param fnName - Registered RPC function name (validated as path segment)
|
|
94
|
+
* @param fnEntry - Export name used in the generated module (validated as identifier)
|
|
95
|
+
* @param options - Content type, credentials, and RPC prefix settings
|
|
96
|
+
* @returns A string of JavaScript code exporting the client stub
|
|
98
97
|
*/
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
98
|
+
const getModule = (fnName, fnEntry, options) => {
|
|
99
|
+
const safeFnName = validatePathSegment(fnName, "function name");
|
|
100
|
+
const safeFnEntry = validateIdentifier(fnEntry, "export name");
|
|
101
|
+
const safePrefix = validatePathSegment(options.rpcPrefix, "rpcPrefix");
|
|
102
|
+
const credentials = validateCredentials(options.credentials);
|
|
103
|
+
const method = validateMethod(options.method);
|
|
104
|
+
let body = "";
|
|
105
|
+
let headers = "{}";
|
|
106
|
+
switch (options.contentType) {
|
|
107
|
+
case "text/plain":
|
|
108
|
+
body = `args[0]`;
|
|
109
|
+
headers = `{ 'Content-Type': 'text/plain' }`;
|
|
110
|
+
break;
|
|
111
|
+
case "multipart/form-data":
|
|
112
|
+
body = `args[0]`;
|
|
113
|
+
headers = `{}`;
|
|
114
|
+
break;
|
|
115
|
+
default:
|
|
116
|
+
body = `JSON.stringify(args)`;
|
|
117
|
+
headers = `{ 'Content-Type': 'application/json' }`;
|
|
118
|
+
}
|
|
119
|
+
if (method === "GET") {
|
|
120
|
+
body = `JSON.stringify(args)`;
|
|
121
|
+
headers = `{}`;
|
|
122
|
+
}
|
|
123
|
+
return `
|
|
124
|
+
export const ${safeFnEntry} = (...args) => {
|
|
125
|
+
const body = ${body};
|
|
126
|
+
const headers = ${headers};
|
|
127
|
+
const prefix = "${safePrefix}";
|
|
128
|
+
const name = "${safeFnName}";
|
|
129
|
+
const credentials = "${credentials}";
|
|
130
|
+
const method = "${method}";
|
|
131
|
+
return innerModule(body, headers, credentials, prefix, name, method);
|
|
132
|
+
}`.trim();
|
|
109
133
|
};
|
|
110
134
|
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* @
|
|
135
|
+
* Generates the complete client-side module bundle by iterating all registered server functions
|
|
136
|
+
* and producing fetch-based stubs for each. The result is transformed by Vite (or Oxc) during
|
|
137
|
+
* the dev server or production build.
|
|
138
|
+
* @param initialOptions - Plugin options containing rpcPrefix and optional adapter
|
|
139
|
+
* @returns A string of JavaScript code with all client RPC modules and their import dependencies
|
|
115
140
|
*/
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
};
|
|
126
|
-
const sendResponse = (code, output) => {
|
|
127
|
-
setStatusCode(code);
|
|
128
|
-
setHeader("Content-Type", "application/json");
|
|
129
|
-
if (isExpressResponse(response)) response.send(JSON.stringify(output));
|
|
130
|
-
else response.end(JSON.stringify(output));
|
|
131
|
-
};
|
|
132
|
-
return {
|
|
133
|
-
isResponseSent,
|
|
134
|
-
setHeader,
|
|
135
|
-
statusCode: response.statusCode,
|
|
136
|
-
setStatusCode,
|
|
137
|
-
sendResponse
|
|
138
|
-
};
|
|
141
|
+
const getClientModules = (initialOptions) => {
|
|
142
|
+
validatePathSegment(initialOptions.rpcPrefix, "rpcPrefix");
|
|
143
|
+
return `
|
|
144
|
+
|
|
145
|
+
import { innerModule } from "@thednp/rpc/helpers";
|
|
146
|
+
${Array.from(serverFunctionsMap.entries()).filter(([, entry]) => entry.exportName).map(([registeredName, entry]) => getModule(registeredName, entry.exportName, {
|
|
147
|
+
...initialOptions,
|
|
148
|
+
...entry.options || {}
|
|
149
|
+
})).join("\n")}`.trim();
|
|
139
150
|
};
|
|
140
151
|
//#endregion
|
|
141
|
-
//#region src/
|
|
152
|
+
//#region src/server-helpers.ts
|
|
153
|
+
const GLOB_REGEX = /^.+\.server\.(ts|js|mjs|mts)$/;
|
|
142
154
|
/**
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* preventing ReDoS and regex injection attacks.
|
|
146
|
-
* @param s - The raw string to escape
|
|
147
|
-
* @returns The escaped string safe for use in new RegExp()
|
|
155
|
+
* Recursively walks `dir` and collects absolute paths to files whose
|
|
156
|
+
* basename matches the `*.server.{ts,js,mjs,mts}` glob pattern.
|
|
148
157
|
*/
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
//#endregion
|
|
166
|
-
//#region src/express/createMiddleware.ts
|
|
167
|
-
let middlewareCount = 0;
|
|
168
|
-
const middlewareStack = /* @__PURE__ */ new Set();
|
|
169
|
-
/**
|
|
170
|
-
* Creates an Express middleware with optional path and rpcPrefix filtering.
|
|
171
|
-
* Middleware names are deduplicated — reusing a name throws an error.
|
|
172
|
-
* Prefix and path regexes are compiled once at creation time (hoisted) for performance.
|
|
173
|
-
* @param initialOptions - Options for rpcPrefix, path matching, and the handler function
|
|
174
|
-
* @returns An Express middleware function
|
|
175
|
-
*/
|
|
176
|
-
const createMiddleware = (initialOptions = {}) => {
|
|
177
|
-
const options = Object.assign({}, defaultMiddlewareOptions, initialOptions);
|
|
178
|
-
const middlewareName = options.name;
|
|
179
|
-
const rpcPrefix = options.rpcPrefix;
|
|
180
|
-
const path = options.path;
|
|
181
|
-
const handler = options.handler;
|
|
182
|
-
let name = middlewareName;
|
|
183
|
-
if (!name) {
|
|
184
|
-
name = "viteRPCMiddleware-" + middlewareCount;
|
|
185
|
-
middlewareCount += 1;
|
|
158
|
+
const walkGlobFiles = async (dir) => {
|
|
159
|
+
const results = [];
|
|
160
|
+
const stack = [dir];
|
|
161
|
+
while (stack.length) {
|
|
162
|
+
const current = stack.pop();
|
|
163
|
+
let entries;
|
|
164
|
+
try {
|
|
165
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
166
|
+
} catch (_e) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
for (const entry of entries) {
|
|
170
|
+
const fullPath = join(current, entry.name);
|
|
171
|
+
if (entry.isFile() && GLOB_REGEX.test(entry.name)) results.push(fullPath);
|
|
172
|
+
else if (entry.isDirectory()) stack.push(fullPath);
|
|
173
|
+
}
|
|
186
174
|
}
|
|
187
|
-
|
|
188
|
-
middlewareStack.add(name);
|
|
189
|
-
const prefixRegex = rpcPrefix ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`) : null;
|
|
190
|
-
const pathMatcher = path ? typeof path === "string" ? new RegExp(path) : path : null;
|
|
191
|
-
const middlewareHandler = async (req, res, next) => {
|
|
192
|
-
const { url } = getRequestDetails(req);
|
|
193
|
-
if (serverFunctionsMap.size === 0) await scanForServerFiles();
|
|
194
|
-
if (!handler) return next?.();
|
|
195
|
-
if (pathMatcher && !pathMatcher.test(url)) return next?.();
|
|
196
|
-
if (prefixRegex && !prefixRegex.test(url)) return next?.();
|
|
197
|
-
await handler(req, res, next);
|
|
198
|
-
};
|
|
199
|
-
Object.defineProperty(middlewareHandler, "name", { value: name });
|
|
200
|
-
return middlewareHandler;
|
|
175
|
+
return results;
|
|
201
176
|
};
|
|
177
|
+
//#endregion
|
|
178
|
+
//#region src/scanForServerFiles.ts
|
|
179
|
+
let isScanned = false;
|
|
180
|
+
/** Absolute ids (normalized) of the scanned server function files. */
|
|
181
|
+
const scannedServerFiles = /* @__PURE__ */ new Set();
|
|
182
|
+
const EXACT_NAMES = [
|
|
183
|
+
"server.ts",
|
|
184
|
+
"server.js",
|
|
185
|
+
"server.mjs",
|
|
186
|
+
"server.mts"
|
|
187
|
+
];
|
|
202
188
|
/**
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
189
|
+
* Scans `src/api/` (or an explicit `scanRoot`) for server function files
|
|
190
|
+
* and populates the global `serverFunctionsMap` with their exported functions.
|
|
191
|
+
* Uses Vite's SSR module loading to resolve and execute each file.
|
|
192
|
+
*
|
|
193
|
+
* Supports two matching modes via `config.serverFiles`:
|
|
194
|
+
* `"exact"` — classic `server.ts|js|mjs|mts` names in the api directory
|
|
195
|
+
* `"glob"` — recursively walking `scanRoot` to match `*.server.{ts,js,mjs,mts}`
|
|
196
|
+
* @param initialCfg - Optional Vite config overrides (root, base, server, serverFiles, scanRoot)
|
|
197
|
+
* @param devServer - Optional running Vite dev server instance; when provided, skips creating a new one
|
|
208
198
|
*/
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
199
|
+
const scanForServerFiles = async (initialCfg, devServer) => {
|
|
200
|
+
if (isScanned && !devServer) return;
|
|
201
|
+
const { createServer, normalizePath } = await import("vite");
|
|
202
|
+
const config = !initialCfg && !devServer || !initialCfg ? {
|
|
203
|
+
root: process.cwd(),
|
|
204
|
+
base: process.env.BASE || "/",
|
|
205
|
+
server: { middlewareMode: true }
|
|
206
|
+
} : { ...initialCfg };
|
|
207
|
+
let server = devServer;
|
|
208
|
+
if (!server) server = await createServer({
|
|
209
|
+
server: {
|
|
210
|
+
...config.server,
|
|
211
|
+
ws: false
|
|
212
|
+
},
|
|
213
|
+
appType: "custom",
|
|
214
|
+
base: config.base || "/",
|
|
215
|
+
root: config.root || process.cwd(),
|
|
216
|
+
configFile: false,
|
|
217
|
+
optimizeDeps: { noDiscovery: true },
|
|
218
|
+
ssr: { optimizeDeps: { noDiscovery: true } }
|
|
219
|
+
});
|
|
220
|
+
const root = config.root || process.cwd();
|
|
221
|
+
const resolvedScanRoot = resolve(root, config.scanRoot ?? join(root, "src", "api"));
|
|
222
|
+
const serverFiles = config.serverFiles ?? "exact";
|
|
223
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
224
|
+
let files;
|
|
225
|
+
try {
|
|
226
|
+
if (serverFiles === "glob") files = await walkGlobFiles(resolvedScanRoot);
|
|
227
|
+
else files = (await readdir(resolvedScanRoot, { withFileTypes: true })).filter((f) => EXACT_NAMES.includes(f.name)).map((f) => join(resolvedScanRoot, f.name));
|
|
228
|
+
} catch (_e) {
|
|
229
|
+
files = [];
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
for (const file of files) {
|
|
233
|
+
scannedServerFiles.add(normalizePath(file));
|
|
234
|
+
let moduleExports;
|
|
235
|
+
try {
|
|
236
|
+
moduleExports = await server.ssrLoadModule(file);
|
|
237
|
+
} catch (error) {
|
|
238
|
+
console.error(ERROR_LOADING_FILE, file, error);
|
|
239
|
+
continue;
|
|
225
240
|
}
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
sendResponse(404, { error: FUNCTION_NOT_FOUND });
|
|
241
|
+
const moduleEntries = Object.entries(moduleExports);
|
|
242
|
+
if (!moduleEntries.length) {
|
|
243
|
+
console.warn(NO_SERVER_FUNCTION_FOUND);
|
|
230
244
|
return;
|
|
231
245
|
}
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
if (
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
let args = [];
|
|
239
|
-
if (method === "GET") {
|
|
240
|
-
const raw = searchParams.get("args");
|
|
241
|
-
if (raw) args = JSON.parse(raw);
|
|
242
|
-
} else {
|
|
243
|
-
const body = await readBody(req);
|
|
244
|
-
args = Array.isArray(body.data) ? body.data : [body.data];
|
|
246
|
+
for (const [exportName, exportValue] of moduleEntries) {
|
|
247
|
+
const registeredName = exportValue.name;
|
|
248
|
+
if (seenNames.has(registeredName)) {
|
|
249
|
+
if (process.env.NODE_ENV !== "production") throw new Error(DUPLICATE_FUNCTION_NAME(registeredName));
|
|
250
|
+
console.warn(DUPLICATE_FUNCTION_NAME(registeredName));
|
|
251
|
+
continue;
|
|
245
252
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
console.error(String(err));
|
|
254
|
-
sendResponse(500, { error: INTERNAL_SERVER_ERROR });
|
|
253
|
+
seenNames.add(registeredName);
|
|
254
|
+
serverFunctionsMap.set(registeredName, {
|
|
255
|
+
name: registeredName,
|
|
256
|
+
handler: exportValue,
|
|
257
|
+
options: exportValue?.options,
|
|
258
|
+
exportName
|
|
259
|
+
});
|
|
255
260
|
}
|
|
256
261
|
}
|
|
257
|
-
}
|
|
262
|
+
} finally {
|
|
263
|
+
if (!devServer && server) await server.close();
|
|
264
|
+
isScanned = true;
|
|
265
|
+
}
|
|
258
266
|
};
|
|
259
267
|
//#endregion
|
|
260
268
|
//#region src/index.ts
|
|
@@ -352,7 +360,7 @@ const loadRPCConfig = async (configFile) => {
|
|
|
352
360
|
* @returns A Vite plugin object
|
|
353
361
|
*/
|
|
354
362
|
function rpcPlugin(devOptions = {}) {
|
|
355
|
-
let options;
|
|
363
|
+
let options = mergeConfig(defaultRPCOptions, devOptions);
|
|
356
364
|
let config;
|
|
357
365
|
let viteServer;
|
|
358
366
|
let isOxc = true;
|
|
@@ -367,18 +375,41 @@ function rpcPlugin(devOptions = {}) {
|
|
|
367
375
|
async configureServer(server) {
|
|
368
376
|
viteServer = server;
|
|
369
377
|
const { adapter: _adapter, ...rest } = options;
|
|
370
|
-
if (serverFunctionsMap.size === 0)
|
|
378
|
+
if (serverFunctionsMap.size === 0) {
|
|
379
|
+
const scanCfg = {
|
|
380
|
+
...config,
|
|
381
|
+
serverFiles: options.serverFiles,
|
|
382
|
+
scanRoot: options.scanRoot
|
|
383
|
+
};
|
|
384
|
+
await scanForServerFiles(scanCfg, viteServer);
|
|
385
|
+
}
|
|
371
386
|
server.middlewares.use(createRPCMiddleware(rest));
|
|
372
387
|
},
|
|
373
388
|
async buildStart() {
|
|
374
389
|
const viteVersion = this.meta?.viteVersion;
|
|
375
390
|
isOxc = Number(viteVersion[0]) >= 8;
|
|
376
|
-
if (!viteServer && config)
|
|
391
|
+
if (!viteServer && config) {
|
|
392
|
+
const scanCfg = {
|
|
393
|
+
...config,
|
|
394
|
+
serverFiles: options.serverFiles,
|
|
395
|
+
scanRoot: options.scanRoot
|
|
396
|
+
};
|
|
397
|
+
await scanForServerFiles(scanCfg);
|
|
398
|
+
}
|
|
377
399
|
},
|
|
378
400
|
async transform(code, id, ops) {
|
|
379
401
|
if (!code.includes("createServerFunction") || ops?.ssr || code.includes("createServerFunction") && typeof process === "undefined") return null;
|
|
380
|
-
if (serverFunctionsMap.size === 0) await scanForServerFiles(config);
|
|
381
402
|
const vite = await import("vite");
|
|
403
|
+
if (serverFunctionsMap.size === 0) {
|
|
404
|
+
const scanCfg = {
|
|
405
|
+
...config,
|
|
406
|
+
serverFiles: options.serverFiles,
|
|
407
|
+
scanRoot: options.scanRoot
|
|
408
|
+
};
|
|
409
|
+
await scanForServerFiles(scanCfg);
|
|
410
|
+
}
|
|
411
|
+
const idPath = vite.normalizePath(id.split("?")[0]);
|
|
412
|
+
if (!scannedServerFiles.has(idPath)) return null;
|
|
382
413
|
const transformer = isOxc ? "transformWithOxc" : "transformWithEsbuild";
|
|
383
414
|
const langProp = isOxc ? "lang" : "loader";
|
|
384
415
|
const source = getClientModules({
|