vite-plugin-server-actions 0.1.1 → 1.0.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/src/index.js CHANGED
@@ -1,62 +1,378 @@
1
1
  import fs from "fs/promises";
2
2
  import path from "path";
3
3
  import express from "express";
4
- // TODO: find a way to not use rollup directly
5
4
  import { rollup } from "rollup";
5
+ import { minimatch } from "minimatch";
6
+ import { middleware } from "./middleware.js";
7
+ import { defaultSchemaDiscovery, createValidationMiddleware } from "./validation.js";
8
+ import { OpenAPIGenerator, setupOpenAPIEndpoints } from "./openapi.js";
9
+ import { generateValidationCode } from "./build-utils.js";
10
+
11
+ // Utility functions for path transformation
12
+ export const pathUtils = {
13
+ /**
14
+ * Default path normalizer - creates underscore-separated module names (preserves original behavior)
15
+ * @param {string} filePath - Relative file path (e.g., "src/actions/todo.server.js")
16
+ * @returns {string} - Normalized module name (e.g., "src_actions_todo")
17
+ */
18
+ createModuleName: (filePath) => {
19
+ return filePath
20
+ .replace(/\//g, "_") // Replace slashes with underscores
21
+ .replace(/\./g, "_") // Replace dots with underscores
22
+ .replace(/_server_js$/, ""); // Remove .server.js extension
23
+ },
24
+
25
+ /**
26
+ * Clean route transformer - creates hierarchical paths: /api/actions/todo/create
27
+ * @param {string} filePath - Relative file path (e.g., "src/actions/todo.server.js")
28
+ * @param {string} functionName - Function name (e.g., "create")
29
+ * @returns {string} - Clean route (e.g., "actions/todo/create")
30
+ */
31
+ createCleanRoute: (filePath, functionName) => {
32
+ const cleanPath = filePath
33
+ .replace(/^src\//, "") // Remove src/ prefix
34
+ .replace(/\.server\.js$/, ""); // Remove .server.js suffix
35
+ return `${cleanPath}/${functionName}`;
36
+ },
37
+
38
+ /**
39
+ * Legacy route transformer - creates underscore-separated paths: /api/src_actions_todo/create
40
+ * @param {string} filePath - Relative file path (e.g., "src/actions/todo.server.js")
41
+ * @param {string} functionName - Function name (e.g., "create")
42
+ * @returns {string} - Legacy route (e.g., "src_actions_todo/create")
43
+ */
44
+ createLegacyRoute: (filePath, functionName) => {
45
+ const legacyPath = filePath
46
+ .replace(/\//g, "_") // Replace slashes with underscores
47
+ .replace(/\.server\.js$/, ""); // Remove .server.js extension
48
+ return `${legacyPath}/${functionName}`;
49
+ },
50
+
51
+ /**
52
+ * Minimal route transformer - keeps original structure: /api/actions/todo.server/create
53
+ * @param {string} filePath - Relative file path (e.g., "actions/todo.server.js")
54
+ * @param {string} functionName - Function name (e.g., "create")
55
+ * @returns {string} - Minimal route (e.g., "actions/todo.server/create")
56
+ */
57
+ createMinimalRoute: (filePath, functionName) => {
58
+ const minimalPath = filePath.replace(/\.js$/, ""); // Just remove .js
59
+ return `${minimalPath}/${functionName}`;
60
+ },
61
+ };
62
+
63
+ const DEFAULT_OPTIONS = {
64
+ apiPrefix: "/api",
65
+ include: ["**/*.server.js"],
66
+ exclude: [],
67
+ middleware: [],
68
+ moduleNameTransform: pathUtils.createModuleName,
69
+ routeTransform: (filePath, functionName) => {
70
+ // Default to clean hierarchical paths: /api/actions/todo/create
71
+ const cleanPath = filePath
72
+ .replace(/^src\//, "") // Remove src/ prefix
73
+ .replace(/\.server\.js$/, ""); // Remove .server.js suffix
74
+ return `${cleanPath}/${functionName}`;
75
+ },
76
+ validation: {
77
+ enabled: false,
78
+ adapter: "zod",
79
+ },
80
+ };
81
+
82
+ function shouldProcessFile(filePath, options) {
83
+ // Normalize the options to arrays
84
+ const includePatterns = Array.isArray(options.include) ? options.include : [options.include];
85
+ const excludePatterns = Array.isArray(options.exclude) ? options.exclude : [options.exclude];
86
+
87
+ // Check if file matches any include pattern
88
+ const isIncluded = includePatterns.some((pattern) => minimatch(filePath, pattern));
89
+
90
+ // Check if file matches any exclude pattern
91
+ const isExcluded = excludePatterns.length > 0 && excludePatterns.some((pattern) => minimatch(filePath, pattern));
92
+
93
+ return isIncluded && !isExcluded;
94
+ }
95
+
96
+ export default function serverActions(userOptions = {}) {
97
+ const options = {
98
+ ...DEFAULT_OPTIONS,
99
+ ...userOptions,
100
+ validation: { ...DEFAULT_OPTIONS.validation, ...userOptions.validation },
101
+ openAPI: {
102
+ enabled: false,
103
+ info: {
104
+ title: "Server Actions API",
105
+ version: "1.0.0",
106
+ description: "Auto-generated API documentation for Vite Server Actions",
107
+ },
108
+ docsPath: "/api/docs",
109
+ specPath: "/api/openapi.json",
110
+ swaggerUI: true,
111
+ ...userOptions.openAPI,
112
+ },
113
+ };
6
114
 
7
- export default function serverActions() {
8
115
  const serverFunctions = new Map();
116
+ const schemaDiscovery = defaultSchemaDiscovery;
9
117
  let app;
118
+ let openAPIGenerator;
119
+ let validationMiddleware = null;
120
+ let viteConfig = null;
121
+
122
+ // Initialize OpenAPI generator if enabled
123
+ if (options.openAPI.enabled) {
124
+ openAPIGenerator = new OpenAPIGenerator({
125
+ info: options.openAPI.info,
126
+ });
127
+ }
128
+
129
+ // Initialize validation middleware if enabled
130
+ if (options.validation.enabled) {
131
+ validationMiddleware = createValidationMiddleware({
132
+ schemaDiscovery,
133
+ });
134
+ }
10
135
 
11
136
  return {
12
137
  name: "vite-plugin-server-actions",
13
138
 
139
+ configResolved(config) {
140
+ // Store Vite config for later use
141
+ viteConfig = config;
142
+ },
143
+
14
144
  configureServer(server) {
15
145
  app = express();
16
146
  app.use(express.json());
147
+
148
+ // Setup dynamic OpenAPI endpoints in development
149
+ if (process.env.NODE_ENV !== "production" && options.openAPI.enabled && openAPIGenerator) {
150
+ // OpenAPI spec endpoint - generates spec dynamically from current serverFunctions
151
+ app.get(options.openAPI.specPath, (req, res) => {
152
+ const openAPISpec = openAPIGenerator.generateSpec(serverFunctions, schemaDiscovery, {
153
+ apiPrefix: options.apiPrefix,
154
+ routeTransform: options.routeTransform,
155
+ });
156
+
157
+ // Add a note if no functions are found
158
+ if (serverFunctions.size === 0) {
159
+ openAPISpec.info.description =
160
+ (openAPISpec.info.description || "") +
161
+ "\n\nNote: No server functions found yet. Try refreshing after accessing your app to trigger module loading.";
162
+ }
163
+
164
+ res.json(openAPISpec);
165
+ });
166
+
167
+ // Swagger UI setup
168
+ if (options.openAPI.swaggerUI) {
169
+ try {
170
+ // Dynamic import swagger-ui-express
171
+ import("swagger-ui-express")
172
+ .then(({ default: swaggerUi }) => {
173
+ const docsPath = options.openAPI.docsPath;
174
+
175
+ app.use(
176
+ docsPath,
177
+ swaggerUi.serve,
178
+ swaggerUi.setup(null, {
179
+ swaggerOptions: {
180
+ url: options.openAPI.specPath,
181
+ },
182
+ }),
183
+ );
184
+
185
+ // Wait for server to start and get the actual port, then log URLs
186
+ server.httpServer?.on("listening", () => {
187
+ const address = server.httpServer.address();
188
+ const port = address?.port || 5173;
189
+ // Always use localhost for consistent display
190
+ const host = "localhost";
191
+
192
+ // Delay to appear after Vite's startup messages
193
+ global.setTimeout(() => {
194
+ if (viteConfig?.logger) {
195
+ console.log(` \x1b[2;32m➜\x1b[0m API Docs: http://${host}:${port}${docsPath}`);
196
+ console.log(` \x1b[2;32m➜\x1b[0m OpenAPI: http://${host}:${port}${options.openAPI.specPath}`);
197
+ } else {
198
+ console.log(`📖 API Documentation: http://${host}:${port}${docsPath}`);
199
+ console.log(`📄 OpenAPI Spec: http://${host}:${port}${options.openAPI.specPath}`);
200
+ }
201
+ }, 50); // Small delay to appear after Vite's ready message
202
+ });
203
+ })
204
+ .catch((error) => {
205
+ console.warn("Swagger UI setup failed:", error.message);
206
+ });
207
+ } catch (error) {
208
+ console.warn("Swagger UI setup failed:", error.message);
209
+ }
210
+ }
211
+ }
212
+
17
213
  server.middlewares.use(app);
18
214
  },
19
215
 
20
216
  async resolveId(source, importer) {
21
- if (source.endsWith(".server.js") && importer) {
217
+ if (importer && shouldProcessFile(source, options)) {
22
218
  const resolvedPath = path.resolve(path.dirname(importer), source);
23
219
  return resolvedPath;
24
220
  }
25
221
  },
26
222
 
27
223
  async load(id) {
28
- if (id.endsWith(".server.js")) {
29
- const code = await fs.readFile(id, "utf-8");
30
- const moduleName = path.basename(id, ".server.js");
31
- const exportRegex = /export\s+(async\s+)?function\s+(\w+)/g;
32
- const functions = [];
33
- let match;
34
- while ((match = exportRegex.exec(code)) !== null) {
35
- functions.push(match[2]);
36
- }
224
+ if (shouldProcessFile(id, options)) {
225
+ try {
226
+ const code = await fs.readFile(id, "utf-8");
227
+
228
+ let relativePath = path.relative(process.cwd(), id);
229
+
230
+ // If the file is outside the project root, use the absolute path
231
+ if (relativePath.startsWith("..")) {
232
+ relativePath = id;
233
+ }
234
+
235
+ // Normalize path separators
236
+ relativePath = relativePath.replace(/\\/g, "/").replace(/^\//, "");
237
+
238
+ // Generate module name for internal use (must be valid identifier)
239
+ const moduleName = options.moduleNameTransform(relativePath);
240
+
241
+ // Validate module name
242
+ if (!moduleName || moduleName.includes("..")) {
243
+ throw new Error(`Invalid server module name: ${moduleName}`);
244
+ }
245
+
246
+ const exportRegex = /export\s+(async\s+)?function\s+(\w+)/g;
247
+ const functions = [];
248
+ let match;
249
+
250
+ while ((match = exportRegex.exec(code)) !== null) {
251
+ const functionName = match[2];
37
252
 
38
- serverFunctions.set(moduleName, { functions, id });
39
-
40
- if (process.env.NODE_ENV !== "production") {
41
- functions.forEach((functionName) => {
42
- const endpoint = `/api/${moduleName}/${functionName}`;
43
- app.post(endpoint, async (req, res) => {
44
- try {
45
- const module = await import(id);
46
- const result = await module[functionName](...req.body);
47
- res.json(result || "* No response *");
48
- } catch (error) {
49
- console.error(`Error in ${functionName}: ${error.message}`);
50
- res.status(500).json({ error: error.message });
253
+ // Validate function name
254
+ if (!functionName || !/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(functionName)) {
255
+ console.warn(`Skipping invalid function name: ${functionName} in ${id}`);
256
+ continue;
257
+ }
258
+
259
+ functions.push(functionName);
260
+ }
261
+
262
+ // Check for duplicate function names within the same module
263
+ const uniqueFunctions = [...new Set(functions)];
264
+ if (uniqueFunctions.length !== functions.length) {
265
+ console.warn(`Duplicate function names detected in ${id}`);
266
+ }
267
+
268
+ serverFunctions.set(moduleName, { functions: uniqueFunctions, id, filePath: relativePath });
269
+
270
+ // Discover schemas from module if validation is enabled (development only)
271
+ if (options.validation.enabled && process.env.NODE_ENV !== "production") {
272
+ try {
273
+ const module = await import(id);
274
+ schemaDiscovery.discoverFromModule(module, moduleName);
275
+ } catch (error) {
276
+ console.warn(`Failed to discover schemas from ${id}: ${error.message}`);
277
+ }
278
+ }
279
+
280
+ // Setup routes in development mode only
281
+ if (process.env.NODE_ENV !== "production" && app) {
282
+ // Normalize middleware to array (create a fresh copy to avoid mutation)
283
+ const middlewares = Array.isArray(options.middleware)
284
+ ? [...options.middleware] // Create a copy
285
+ : options.middleware
286
+ ? [options.middleware]
287
+ : [];
288
+
289
+ // Add validation middleware if enabled
290
+ if (validationMiddleware) {
291
+ middlewares.push(validationMiddleware);
292
+ }
293
+
294
+ uniqueFunctions.forEach((functionName) => {
295
+ const routePath = options.routeTransform(relativePath, functionName);
296
+ const endpoint = `${options.apiPrefix}/${routePath}`;
297
+
298
+ // Create a context-aware validation middleware if validation is enabled
299
+ const contextMiddlewares = [...middlewares];
300
+ if (validationMiddleware && options.validation.enabled) {
301
+ // Replace the generic validation middleware with a context-aware one
302
+ const lastIdx = contextMiddlewares.length - 1;
303
+ if (contextMiddlewares[lastIdx] === validationMiddleware) {
304
+ contextMiddlewares[lastIdx] = (req, res, next) => {
305
+ // Add context to request for validation
306
+ // Get the schema directly from schemaDiscovery
307
+ const schema = schemaDiscovery.getSchema(moduleName, functionName);
308
+ req.validationContext = {
309
+ moduleName, // For error messages
310
+ functionName, // For error messages
311
+ schema, // Direct schema access
312
+ };
313
+ return validationMiddleware(req, res, next);
314
+ };
315
+ }
51
316
  }
317
+
318
+ // Apply middleware before the handler
319
+ app.post(endpoint, ...contextMiddlewares, async (req, res) => {
320
+ try {
321
+ const module = await import(id);
322
+
323
+ // Check if function exists in module
324
+ if (typeof module[functionName] !== "function") {
325
+ throw new Error(`Function ${functionName} not found or not a function`);
326
+ }
327
+
328
+ // Validate request body is array for function arguments
329
+ if (!Array.isArray(req.body)) {
330
+ throw new Error("Request body must be an array of function arguments");
331
+ }
332
+
333
+ const result = await module[functionName](...req.body);
334
+ res.json(result || "* No response *");
335
+ } catch (error) {
336
+ console.error(`Error in ${functionName}: ${error.message}`);
337
+
338
+ if (error.message.includes("not found") || error.message.includes("not a function")) {
339
+ res.status(404).json({
340
+ error: "Function not found",
341
+ details: error.message,
342
+ });
343
+ } else if (error.message.includes("Request body")) {
344
+ res.status(400).json({
345
+ error: "Bad request",
346
+ details: error.message,
347
+ });
348
+ } else {
349
+ res.status(500).json({
350
+ error: "Internal server error",
351
+ details: error.message,
352
+ });
353
+ }
354
+ }
355
+ });
52
356
  });
53
- });
357
+ }
358
+ // OpenAPI endpoints will be set up during configureServer after all modules are loaded
359
+
360
+ return generateClientProxy(moduleName, uniqueFunctions, options, relativePath);
361
+ } catch (error) {
362
+ console.error(`Failed to process server file ${id}: ${error.message}`);
363
+ // Return empty proxy instead of failing the build
364
+ return `// Failed to load server actions from ${id}: ${error.message}`;
54
365
  }
55
- return generateClientProxy(moduleName, functions);
56
366
  }
57
367
  },
58
368
 
59
- async generateBundle(options, bundle) {
369
+ transform(code, id) {
370
+ // This hook is not needed since we handle the transformation in the load hook
371
+ // The warning was incorrectly flagging legitimate imports that are being transformed
372
+ return null;
373
+ },
374
+
375
+ async generateBundle(outputOptions, bundle) {
60
376
  // Create a virtual entry point for all server functions
61
377
  const virtualEntryId = "virtual:server-actions-entry";
62
378
  let virtualModuleContent = "";
@@ -85,7 +401,7 @@ export default function serverActions() {
85
401
  {
86
402
  name: "external-modules",
87
403
  resolveId(source) {
88
- if (!source.endsWith(".server.js") && !source.startsWith(".") && !path.isAbsolute(source)) {
404
+ if (!shouldProcessFile(source, options) && !source.startsWith(".") && !path.isAbsolute(source)) {
89
405
  return { id: source, external: true };
90
406
  }
91
407
  },
@@ -99,7 +415,6 @@ export default function serverActions() {
99
415
  throw new Error("Failed to bundle server functions");
100
416
  }
101
417
 
102
- // Get the bundled code
103
418
  const bundledCode = output[0].code;
104
419
 
105
420
  // Emit the bundled server functions
@@ -109,12 +424,36 @@ export default function serverActions() {
109
424
  source: bundledCode,
110
425
  });
111
426
 
427
+ // Generate OpenAPI spec if enabled
428
+ let openAPISpec = null;
429
+ if (options.openAPI.enabled) {
430
+ openAPISpec = openAPIGenerator.generateSpec(serverFunctions, schemaDiscovery, {
431
+ apiPrefix: options.apiPrefix,
432
+ routeTransform: options.routeTransform,
433
+ });
434
+
435
+ // Emit OpenAPI spec as a separate file
436
+ this.emitFile({
437
+ type: "asset",
438
+ fileName: "openapi.json",
439
+ source: JSON.stringify(openAPISpec, null, 2),
440
+ });
441
+ }
442
+
443
+ // Generate validation code if enabled
444
+ const validationCode = generateValidationCode(options, serverFunctions);
445
+
112
446
  // Generate server.js
113
- let serverCode = `
447
+ const serverCode = `
114
448
  import express from 'express';
115
449
  import * as serverActions from './actions.js';
450
+ ${options.openAPI.enabled && options.openAPI.swaggerUI ? "import swaggerUi from 'swagger-ui-express';" : ""}
451
+ ${options.openAPI.enabled ? "import { readFileSync } from 'fs';\nimport { fileURLToPath } from 'url';\nimport { dirname, join } from 'path';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\nconst openAPISpec = JSON.parse(readFileSync(join(__dirname, 'openapi.json'), 'utf-8'));" : ""}
452
+ ${validationCode.imports}
116
453
 
117
454
  const app = express();
455
+ ${validationCode.setup}
456
+ ${validationCode.middlewareFactory}
118
457
 
119
458
  // Middleware
120
459
  // --------------------------------------------------
@@ -124,11 +463,15 @@ export default function serverActions() {
124
463
  // Server functions
125
464
  // --------------------------------------------------
126
465
  ${Array.from(serverFunctions.entries())
127
- .flatMap(([moduleName, { functions }]) =>
466
+ .flatMap(([moduleName, { functions, filePath }]) =>
128
467
  functions
129
- .map(
130
- (functionName) => `
131
- app.post('/api/${moduleName}/${functionName}', async (req, res) => {
468
+ .map((functionName) => {
469
+ const routePath = options.routeTransform(filePath, functionName);
470
+ const middlewareCall = options.validation?.enabled
471
+ ? `createContextualValidationMiddleware('${moduleName}', '${functionName}'), `
472
+ : "";
473
+ return `
474
+ app.post('${options.apiPrefix}/${routePath}', ${middlewareCall}async (req, res) => {
132
475
  try {
133
476
  const result = await serverActions.${moduleName}.${functionName}(...req.body);
134
477
  res.json(result || "* No response *");
@@ -137,25 +480,54 @@ export default function serverActions() {
137
480
  res.status(500).json({ error: error.message });
138
481
  }
139
482
  });
140
- `,
141
- )
483
+ `;
484
+ })
142
485
  .join("\n")
143
486
  .trim(),
144
487
  )
145
488
  .join("\n")
146
489
  .trim()}
147
490
 
491
+ ${
492
+ options.openAPI.enabled
493
+ ? `
494
+ // OpenAPI endpoints
495
+ // --------------------------------------------------
496
+ app.get('${options.openAPI.specPath}', (req, res) => {
497
+ res.json(openAPISpec);
498
+ });
499
+
500
+ ${
501
+ options.openAPI.swaggerUI
502
+ ? `
503
+ // Swagger UI
504
+ app.use('${options.openAPI.docsPath}', swaggerUi.serve, swaggerUi.setup(openAPISpec));
505
+ `
506
+ : ""
507
+ }
508
+ `
509
+ : ""
510
+ }
511
+
148
512
  // Start server
149
513
  // --------------------------------------------------
150
514
  const port = process.env.PORT || 3000;
151
- app.listen(port, () => console.log(\`🚀 Server listening: http://localhost:\${port}\`));
515
+ app.listen(port, () => {
516
+ console.log(\`🚀 Server listening: http://localhost:\${port}\`);
517
+ ${
518
+ options.openAPI.enabled
519
+ ? `
520
+ console.log(\`📖 API Documentation: http://localhost:\${port}${options.openAPI.docsPath}\`);
521
+ console.log(\`📄 OpenAPI Spec: http://localhost:\${port}${options.openAPI.specPath}\`);
522
+ `
523
+ : ""
524
+ }
525
+ });
152
526
 
153
527
  // List all server functions
154
528
  // --------------------------------------------------
155
529
  `;
156
530
 
157
- // TODO: Add a way to list all server functions in the console
158
-
159
531
  this.emitFile({
160
532
  type: "asset",
161
533
  fileName: "server.js",
@@ -165,28 +537,129 @@ export default function serverActions() {
165
537
  };
166
538
  }
167
539
 
168
- function generateClientProxy(moduleName, functions) {
540
+ function generateClientProxy(moduleName, functions, options, filePath) {
541
+ // Add development-only safety checks
542
+ const isDev = process.env.NODE_ENV !== "production";
543
+
169
544
  let clientProxy = `\n// vite-server-actions: ${moduleName}\n`;
545
+
546
+ // Add a guard to prevent direct imports of server code
547
+ if (isDev) {
548
+ clientProxy += `
549
+ // Development-only safety check
550
+ if (typeof window !== 'undefined') {
551
+ // This code is running in the browser
552
+ const serverFileError = new Error(
553
+ '[Vite Server Actions] SECURITY WARNING: Server file "${moduleName}" is being imported in client code! ' +
554
+ 'This could expose server-side code to the browser. Only import server actions through the plugin.'
555
+ );
556
+ serverFileError.name = 'ServerCodeInClientError';
557
+
558
+ // Check if we're in a server action proxy context
559
+ if (!window.__VITE_SERVER_ACTIONS_PROXY__) {
560
+ console.error(serverFileError);
561
+ // In development, we'll warn but not throw to avoid breaking HMR
562
+ console.error('Stack trace:', serverFileError.stack);
563
+ }
564
+ }
565
+ `;
566
+ }
567
+
170
568
  functions.forEach((functionName) => {
569
+ const routePath = options.routeTransform(filePath, functionName);
570
+
171
571
  clientProxy += `
172
572
  export async function ${functionName}(...args) {
173
573
  console.log("[Vite Server Actions] 🚀 - Executing ${functionName}");
174
- const response = await fetch('/api/${moduleName}/${functionName}', {
175
- method: 'POST',
176
- headers: { 'Content-Type': 'application/json' },
177
- body: JSON.stringify(args)
178
- });
179
-
180
- if (!response.ok) {
181
- console.log("[Vite Server Actions] ❗ - Error in ${functionName}");
182
- throw new Error('Server request failed');
574
+
575
+ ${
576
+ isDev
577
+ ? `
578
+ // Development-only: Mark that we're in a valid proxy context
579
+ if (typeof window !== 'undefined') {
580
+ window.__VITE_SERVER_ACTIONS_PROXY__ = true;
183
581
  }
582
+
583
+ // Validate arguments in development
584
+ if (args.some(arg => typeof arg === 'function')) {
585
+ console.warn(
586
+ '[Vite Server Actions] Warning: Functions cannot be serialized and sent to the server. ' +
587
+ 'Function arguments will be converted to null.'
588
+ );
589
+ }
590
+ `
591
+ : ""
592
+ }
593
+
594
+ try {
595
+ const response = await fetch('${options.apiPrefix}/${routePath}', {
596
+ method: 'POST',
597
+ headers: { 'Content-Type': 'application/json' },
598
+ body: JSON.stringify(args)
599
+ });
184
600
 
185
- console.log("[Vite Server Actions] ✅ - ${functionName} executed successfully");
601
+ if (!response.ok) {
602
+ let errorData;
603
+ try {
604
+ errorData = await response.json();
605
+ } catch {
606
+ errorData = { error: 'Unknown error', details: 'Failed to parse error response' };
607
+ }
608
+
609
+ console.error("[Vite Server Actions] ❗ - Error in ${functionName}:", errorData);
610
+
611
+ const error = new Error(errorData.error || 'Server request failed');
612
+ error.details = errorData.details;
613
+ error.status = response.status;
614
+ throw error;
615
+ }
186
616
 
187
- return response.json();
617
+ console.log("[Vite Server Actions] ✅ - ${functionName} executed successfully");
618
+ const result = await response.json();
619
+
620
+ ${
621
+ isDev
622
+ ? `
623
+ // Development-only: Clear the proxy context
624
+ if (typeof window !== 'undefined') {
625
+ window.__VITE_SERVER_ACTIONS_PROXY__ = false;
626
+ }
627
+ `
628
+ : ""
629
+ }
630
+
631
+ return result;
632
+
633
+ } catch (error) {
634
+ console.error("[Vite Server Actions] ❗ - Network or execution error in ${functionName}:", error.message);
635
+
636
+ ${
637
+ isDev
638
+ ? `
639
+ // Development-only: Clear the proxy context on error
640
+ if (typeof window !== 'undefined') {
641
+ window.__VITE_SERVER_ACTIONS_PROXY__ = false;
642
+ }
643
+ `
644
+ : ""
645
+ }
646
+
647
+ // Re-throw with more context if it's not already our custom error
648
+ if (!error.details) {
649
+ const networkError = new Error(\`Failed to execute server action '\${functionName}': \${error.message}\`);
650
+ networkError.originalError = error;
651
+ throw networkError;
652
+ }
653
+
654
+ throw error;
655
+ }
188
656
  }
189
657
  `;
190
658
  });
191
659
  return clientProxy;
192
660
  }
661
+
662
+ // Export built-in middleware and validation utilities
663
+ export { middleware };
664
+ export { createValidationMiddleware, ValidationAdapter, ZodAdapter, SchemaDiscovery, adapters } from "./validation.js";
665
+ export { OpenAPIGenerator, setupOpenAPIEndpoints, createSwaggerMiddleware } from "./openapi.js";