@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/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 { getClientModules, scanForServerFiles, serverFunctionsMap } from "@thednp/rpc/server";
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
- const defaultMiddlewareOptions = {
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/express/helpers.ts
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
- * Reads and parses the HTTP request body from an Express or Node IncomingMessage.
20
- * If a body parser middleware (e.g. express.json()) already consumed the stream,
21
- * uses the pre-parsed body from `req.body`.
22
- * @param req - Express or Node.js IncomingMessage
23
- * @returns A promise resolving to the parsed body with its content type
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
- const readBody = (req) => {
26
- return new Promise((resolve, reject) => {
27
- if (hasPreParsedBody(req) && req.body !== void 0) {
28
- const isJSON = (req.headers["content-type"]?.toLowerCase() || "").includes("json");
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
- * Type guard that checks whether a request is an Express Request (has `originalUrl`).
70
- * @param req - A Node IncomingMessage or Express Request
71
- * @returns True if the request is an Express Request
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
- const isExpressRequest = (req) => {
74
- return "originalUrl" in req;
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
- * Type guard that checks whether a response is an Express Response (has `json` and `send` methods).
78
- * @param res - A Node ServerResponse or Express Response
79
- * @returns True if the response is an Express Response
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
- const isExpressResponse = (res) => {
82
- return "json" in res && "send" in res;
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
- * Type guard that checks whether a request has a pre-parsed body (`body` property).
86
- * Used to detect if a body-parser middleware already consumed the stream.
87
- * @param req - A Node IncomingMessage or Express Request
88
- * @returns True if the request has a body property
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
- const hasPreParsedBody = (req) => {
91
- return "body" in req;
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
- * Extracts normalized request details from an Express or Node IncomingMessage.
95
- * Parses the URL to extract pathname, search string, and search params.
96
- * @param request - Express or Node.js request object
97
- * @returns Normalized request details including URL, headers, and method
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 getRequestDetails = (request) => {
100
- const rawUrl = isExpressRequest(request) ? request.originalUrl : request.url;
101
- const url = new URL(rawUrl, "http://localhost");
102
- return {
103
- url: url.pathname,
104
- search: url.search,
105
- searchParams: url.searchParams,
106
- headers: request.headers,
107
- method: request.method
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
- * Wraps an Express or Node ServerResponse with a uniform API for setting headers,
112
- * status codes, and sending JSON responses. Handles the Express vs raw Node API differences.
113
- * @param response - Express or Node.js server response object
114
- * @returns A ResponseDetails object with setHeader, setStatusCode, and sendResponse helpers
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 getResponseDetails = (response) => {
117
- const isResponseSent = response.headersSent || response.writableEnded;
118
- const setHeader = (name, value) => {
119
- if (isExpressResponse(response)) response.header(name, value);
120
- else response.setHeader(name, value);
121
- };
122
- const setStatusCode = (code) => {
123
- if (isExpressResponse(response)) response.status(code);
124
- else response.statusCode = code;
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/tools.ts
152
+ //#region src/server-helpers.ts
153
+ const GLOB_REGEX = /^.+\.server\.(ts|js|mjs|mts)$/;
142
154
  /**
143
- * Escapes special regex metacharacters in a string.
144
- * Used to safely embed user-configurable values (like rpcPrefix) into regular expressions,
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
- function escapeRegExp(s) {
150
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
151
- }
152
- //#endregion
153
- //#region src/constants.ts
154
- const FUNCTION_NOT_FOUND = "Function not found";
155
- const METHOD_NOT_ALLOWED = "Method Not Allowed";
156
- const REQUEST_FORBIDDEN = "Forbidden";
157
- const INTERNAL_SERVER_ERROR = "Internal Server Error";
158
- const CLIENT_DISCONNECTED = "client disconnected";
159
- /** Returns a warning when a middleware name is reused, preventing registration conflicts. @param name - The duplicate middleware name */
160
- const MIDDLEWARE_NAME_USED = (name) => `The middleware name "${name}" is already used.`;
161
- /** 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 */
162
- const CONFIG_FILE_NOT_FOUND = (configFile, configFilePath) => ` ⚠︎ The specified RPC config file ${configFile} cannot be found at ${configFilePath}, loading the defaults..`;
163
- const NO_CONFIG_FOUND = ` ⚡︎ No RPC config found, loading the defaults..`;
164
- const FAILED_LOAD_CONFIG = ` ⚠︎ Failed to load RPC config:`;
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
- if (middlewareStack.has(name)) throw new Error(MIDDLEWARE_NAME_USED(name));
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
- * Creates the Express RPC middleware that routes incoming requests to registered server functions.
204
- * Reads the request body, dispatches to the matching function via serverFunctionsMap,
205
- * and sends the JSON-serialized result. Handles client disconnection via abort signals.
206
- * @param initialOptions - Options including rpcPrefix for URL routing
207
- * @returns An Express middleware function
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 createRPCMiddleware = (initialOptions = {}) => {
210
- const options = Object.assign({}, defaultMiddlewareOptions, { rpcPrefix: defaultRPCOptions.rpcPrefix }, initialOptions);
211
- const rpcPrefix = options.rpcPrefix;
212
- const prefixRegex = rpcPrefix ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`) : null;
213
- const prefixReplace = `/${rpcPrefix}/`;
214
- return createMiddleware({
215
- ...options,
216
- handler: async (req, res, _next) => {
217
- const { url: path, searchParams } = getRequestDetails(req);
218
- const { sendResponse } = getResponseDetails(res);
219
- if (prefixRegex && !prefixRegex.test(path)) return;
220
- const origin = options.origin;
221
- const requestOrigin = req.headers.origin;
222
- if (origin && requestOrigin && requestOrigin !== origin) {
223
- sendResponse(403, { error: REQUEST_FORBIDDEN });
224
- return;
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 functionName = path.replace(prefixReplace, "");
227
- const serverFunction = serverFunctionsMap.get(functionName);
228
- if (!serverFunction) {
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
- try {
233
- const method = serverFunction.options?.method || "POST";
234
- if (req.method?.toUpperCase() !== method) {
235
- sendResponse(405, { error: METHOD_NOT_ALLOWED });
236
- return;
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
- const { data, cancel } = serverFunction.handler(...args);
247
- const onClose = () => cancel(CLIENT_DISCONNECTED);
248
- req.on("close", onClose);
249
- const result = await data;
250
- req.off("close", onClose);
251
- if (!res.headersSent) sendResponse(200, { data: result });
252
- } catch (err) {
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) await scanForServerFiles(config, viteServer);
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) await scanForServerFiles(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({