vite-plugin-server-actions 1.0.1 → 1.2.0
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/README.md +162 -52
- package/index.d.ts +76 -32
- package/package.json +23 -14
- package/src/ast-parser.js +535 -0
- package/src/build-utils.js +10 -12
- package/src/dev-validator.js +272 -0
- package/src/error-enhancer.js +283 -0
- package/src/index.js +428 -80
- package/src/openapi.js +67 -29
- package/src/security.js +118 -0
- package/src/type-generator.js +378 -0
- package/src/types.ts +1 -1
- package/src/validation-runtime.js +100 -0
- package/src/validation.js +126 -21
package/src/index.js
CHANGED
|
@@ -3,10 +3,126 @@ import path from "path";
|
|
|
3
3
|
import express from "express";
|
|
4
4
|
import { rollup } from "rollup";
|
|
5
5
|
import { minimatch } from "minimatch";
|
|
6
|
+
import esbuild from "esbuild";
|
|
7
|
+
import { createRequire } from "module";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
import os from "os";
|
|
6
10
|
import { middleware } from "./middleware.js";
|
|
7
11
|
import { defaultSchemaDiscovery, createValidationMiddleware } from "./validation.js";
|
|
8
12
|
import { OpenAPIGenerator, setupOpenAPIEndpoints } from "./openapi.js";
|
|
9
13
|
import { generateValidationCode } from "./build-utils.js";
|
|
14
|
+
import { extractExportedFunctions, isValidFunctionName } from "./ast-parser.js";
|
|
15
|
+
import { generateTypeDefinitions, generateEnhancedClientProxy } from "./type-generator.js";
|
|
16
|
+
import { sanitizePath, isValidModuleName, createSecureModuleName, createErrorResponse } from "./security.js";
|
|
17
|
+
import {
|
|
18
|
+
enhanceFunctionNotFoundError,
|
|
19
|
+
enhanceParsingError,
|
|
20
|
+
enhanceValidationError,
|
|
21
|
+
enhanceModuleLoadError,
|
|
22
|
+
createDevelopmentWarning,
|
|
23
|
+
} from "./error-enhancer.js";
|
|
24
|
+
import {
|
|
25
|
+
validateFunctionSignature,
|
|
26
|
+
validateFileStructure,
|
|
27
|
+
createDevelopmentFeedback,
|
|
28
|
+
validateSchemaAttachment,
|
|
29
|
+
} from "./dev-validator.js";
|
|
30
|
+
|
|
31
|
+
// Module cache moved to plugin instance to avoid cross-instance pollution
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Import a module, handling TypeScript files in development
|
|
35
|
+
* @param {string} id - Module path
|
|
36
|
+
* @param {any} viteServer - Vite dev server instance
|
|
37
|
+
* @param {Map} cache - Module cache for this plugin instance
|
|
38
|
+
* @returns {Promise<any>} - Imported module
|
|
39
|
+
*/
|
|
40
|
+
async function importModule(id, viteServer = null, cache = new Map()) {
|
|
41
|
+
// In production or for JS files, use regular import
|
|
42
|
+
if (process.env.NODE_ENV === "production" || !id.endsWith(".ts")) {
|
|
43
|
+
return import(id);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Use Vite's SSR module loader if available (preferred method)
|
|
47
|
+
if (viteServer && viteServer.ssrLoadModule) {
|
|
48
|
+
try {
|
|
49
|
+
// Clear from cache if it exists to ensure fresh load
|
|
50
|
+
if (cache.has(id)) {
|
|
51
|
+
cache.delete(id);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const module = await viteServer.ssrLoadModule(id);
|
|
55
|
+
cache.set(id, module);
|
|
56
|
+
return module;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error(`Failed to load module ${id} via Vite SSR:`, error);
|
|
59
|
+
// Fall through to manual compilation
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check cache first
|
|
64
|
+
if (cache.has(id)) {
|
|
65
|
+
return cache.get(id);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Fallback: Manual TypeScript compilation (when Vite server is not available)
|
|
69
|
+
// Retry logic for TypeScript compilation failures
|
|
70
|
+
let retryCount = 0;
|
|
71
|
+
const maxRetries = 3;
|
|
72
|
+
|
|
73
|
+
while (retryCount < maxRetries) {
|
|
74
|
+
try {
|
|
75
|
+
// Read and transform TypeScript file
|
|
76
|
+
const tsCode = await fs.readFile(id, "utf-8");
|
|
77
|
+
|
|
78
|
+
// Transform imports to be relative to the original file location
|
|
79
|
+
const result = await esbuild.transform(tsCode, {
|
|
80
|
+
loader: "ts",
|
|
81
|
+
target: "node16",
|
|
82
|
+
format: "esm",
|
|
83
|
+
sourcefile: id,
|
|
84
|
+
sourcemap: "inline",
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Create a temporary file in the same directory as the original
|
|
88
|
+
// This ensures relative imports work correctly
|
|
89
|
+
const dir = path.dirname(id);
|
|
90
|
+
const basename = path.basename(id, ".ts");
|
|
91
|
+
const tmpFile = path.join(dir, `.${basename}.tmp.mjs`);
|
|
92
|
+
|
|
93
|
+
// Write compiled JavaScript
|
|
94
|
+
await fs.writeFile(tmpFile, result.code, "utf-8");
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
// Add a small delay to ensure file is written
|
|
98
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
99
|
+
|
|
100
|
+
// Import the compiled module with cache busting
|
|
101
|
+
const module = await import(`${tmpFile}?t=${Date.now()}`);
|
|
102
|
+
|
|
103
|
+
// Cache the module
|
|
104
|
+
cache.set(id, module);
|
|
105
|
+
|
|
106
|
+
// Clean up temp file immediately
|
|
107
|
+
await fs.unlink(tmpFile).catch(() => {});
|
|
108
|
+
|
|
109
|
+
return module;
|
|
110
|
+
} catch (importError) {
|
|
111
|
+
// Clean up on error
|
|
112
|
+
await fs.unlink(tmpFile).catch(() => {});
|
|
113
|
+
throw importError;
|
|
114
|
+
}
|
|
115
|
+
} catch (error) {
|
|
116
|
+
retryCount++;
|
|
117
|
+
if (retryCount >= maxRetries) {
|
|
118
|
+
console.error(`Failed to import TypeScript module ${id} after ${maxRetries} attempts:`, error);
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
// Wait before retry
|
|
122
|
+
await new Promise((resolve) => setTimeout(resolve, 100 * retryCount));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
10
126
|
|
|
11
127
|
// Utility functions for path transformation
|
|
12
128
|
export const pathUtils = {
|
|
@@ -19,7 +135,7 @@ export const pathUtils = {
|
|
|
19
135
|
return filePath
|
|
20
136
|
.replace(/\//g, "_") // Replace slashes with underscores
|
|
21
137
|
.replace(/\./g, "_") // Replace dots with underscores
|
|
22
|
-
.replace(/
|
|
138
|
+
.replace(/_server_(js|ts)$/, ""); // Remove .server.js or .server.ts extension
|
|
23
139
|
},
|
|
24
140
|
|
|
25
141
|
/**
|
|
@@ -31,7 +147,7 @@ export const pathUtils = {
|
|
|
31
147
|
createCleanRoute: (filePath, functionName) => {
|
|
32
148
|
const cleanPath = filePath
|
|
33
149
|
.replace(/^src\//, "") // Remove src/ prefix
|
|
34
|
-
.replace(/\.server\.js$/, ""); // Remove .server.js suffix
|
|
150
|
+
.replace(/\.server\.(js|ts)$/, ""); // Remove .server.js or .server.ts suffix
|
|
35
151
|
return `${cleanPath}/${functionName}`;
|
|
36
152
|
},
|
|
37
153
|
|
|
@@ -44,7 +160,7 @@ export const pathUtils = {
|
|
|
44
160
|
createLegacyRoute: (filePath, functionName) => {
|
|
45
161
|
const legacyPath = filePath
|
|
46
162
|
.replace(/\//g, "_") // Replace slashes with underscores
|
|
47
|
-
.replace(/\.server\.js$/, ""); // Remove .server.js extension
|
|
163
|
+
.replace(/\.server\.(js|ts)$/, ""); // Remove .server.js or .server.ts extension
|
|
48
164
|
return `${legacyPath}/${functionName}`;
|
|
49
165
|
},
|
|
50
166
|
|
|
@@ -55,14 +171,14 @@ export const pathUtils = {
|
|
|
55
171
|
* @returns {string} - Minimal route (e.g., "actions/todo.server/create")
|
|
56
172
|
*/
|
|
57
173
|
createMinimalRoute: (filePath, functionName) => {
|
|
58
|
-
const minimalPath = filePath.replace(/\.js$/, ""); // Just remove .js
|
|
174
|
+
const minimalPath = filePath.replace(/\.(js|ts)$/, ""); // Just remove .js or .ts
|
|
59
175
|
return `${minimalPath}/${functionName}`;
|
|
60
176
|
},
|
|
61
177
|
};
|
|
62
178
|
|
|
63
179
|
const DEFAULT_OPTIONS = {
|
|
64
180
|
apiPrefix: "/api",
|
|
65
|
-
include: ["**/*.server.js"],
|
|
181
|
+
include: ["**/*.server.js", "**/*.server.ts"],
|
|
66
182
|
exclude: [],
|
|
67
183
|
middleware: [],
|
|
68
184
|
moduleNameTransform: pathUtils.createModuleName,
|
|
@@ -70,7 +186,7 @@ const DEFAULT_OPTIONS = {
|
|
|
70
186
|
// Default to clean hierarchical paths: /api/actions/todo/create
|
|
71
187
|
const cleanPath = filePath
|
|
72
188
|
.replace(/^src\//, "") // Remove src/ prefix
|
|
73
|
-
.replace(/\.server\.js$/, ""); // Remove .server.js suffix
|
|
189
|
+
.replace(/\.server\.(js|ts)$/, ""); // Remove .server.js or .server.ts suffix
|
|
74
190
|
return `${cleanPath}/${functionName}`;
|
|
75
191
|
},
|
|
76
192
|
validation: {
|
|
@@ -114,10 +230,12 @@ export default function serverActions(userOptions = {}) {
|
|
|
114
230
|
|
|
115
231
|
const serverFunctions = new Map();
|
|
116
232
|
const schemaDiscovery = defaultSchemaDiscovery;
|
|
233
|
+
const tsModuleCache = new Map(); // Per-instance cache for TypeScript modules
|
|
117
234
|
let app;
|
|
118
235
|
let openAPIGenerator;
|
|
119
236
|
let validationMiddleware = null;
|
|
120
237
|
let viteConfig = null;
|
|
238
|
+
let viteDevServer = null;
|
|
121
239
|
|
|
122
240
|
// Initialize OpenAPI generator if enabled
|
|
123
241
|
if (options.openAPI.enabled) {
|
|
@@ -142,16 +260,41 @@ export default function serverActions(userOptions = {}) {
|
|
|
142
260
|
},
|
|
143
261
|
|
|
144
262
|
configureServer(server) {
|
|
263
|
+
viteDevServer = server;
|
|
145
264
|
app = express();
|
|
146
265
|
app.use(express.json());
|
|
147
266
|
|
|
267
|
+
// Clean up on HMR
|
|
268
|
+
if (server.watcher) {
|
|
269
|
+
server.watcher.on("change", (file) => {
|
|
270
|
+
// If a server file changed, remove it from the map
|
|
271
|
+
if (shouldProcessFile(file, options)) {
|
|
272
|
+
// Clear TypeScript cache for this file
|
|
273
|
+
if (file.endsWith(".ts")) {
|
|
274
|
+
tsModuleCache.delete(file);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
for (const [moduleName, moduleInfo] of serverFunctions.entries()) {
|
|
278
|
+
if (moduleInfo.id === file) {
|
|
279
|
+
serverFunctions.delete(moduleName);
|
|
280
|
+
schemaDiscovery.clear(); // Clear associated schemas
|
|
281
|
+
console.log(`[HMR] Cleaned up server module: ${moduleName}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
148
288
|
// Setup dynamic OpenAPI endpoints in development
|
|
149
289
|
if (process.env.NODE_ENV !== "production" && options.openAPI.enabled && openAPIGenerator) {
|
|
150
290
|
// OpenAPI spec endpoint - generates spec dynamically from current serverFunctions
|
|
151
291
|
app.get(options.openAPI.specPath, (req, res) => {
|
|
292
|
+
// Get the actual port from the request
|
|
293
|
+
const port = req.get("host")?.split(":")[1] || viteConfig.server?.port || 5173;
|
|
152
294
|
const openAPISpec = openAPIGenerator.generateSpec(serverFunctions, schemaDiscovery, {
|
|
153
295
|
apiPrefix: options.apiPrefix,
|
|
154
296
|
routeTransform: options.routeTransform,
|
|
297
|
+
port,
|
|
155
298
|
});
|
|
156
299
|
|
|
157
300
|
// Add a note if no functions are found
|
|
@@ -185,7 +328,7 @@ export default function serverActions(userOptions = {}) {
|
|
|
185
328
|
// Wait for server to start and get the actual port, then log URLs
|
|
186
329
|
server.httpServer?.on("listening", () => {
|
|
187
330
|
const address = server.httpServer.address();
|
|
188
|
-
const port = address?.port || 5173;
|
|
331
|
+
const port = address?.port || viteConfig.server?.port || 5173;
|
|
189
332
|
// Always use localhost for consistent display
|
|
190
333
|
const host = "localhost";
|
|
191
334
|
|
|
@@ -211,52 +354,140 @@ export default function serverActions(userOptions = {}) {
|
|
|
211
354
|
}
|
|
212
355
|
|
|
213
356
|
server.middlewares.use(app);
|
|
357
|
+
|
|
358
|
+
// Show development feedback after server is ready
|
|
359
|
+
if (process.env.NODE_ENV === "development") {
|
|
360
|
+
server.httpServer?.on("listening", () => {
|
|
361
|
+
// Delay to appear after Vite's startup messages
|
|
362
|
+
global.setTimeout(() => {
|
|
363
|
+
if (serverFunctions.size > 0) {
|
|
364
|
+
console.log(createDevelopmentFeedback(serverFunctions));
|
|
365
|
+
}
|
|
366
|
+
}, 100);
|
|
367
|
+
});
|
|
368
|
+
}
|
|
214
369
|
},
|
|
215
370
|
|
|
216
|
-
async resolveId(source, importer) {
|
|
371
|
+
async resolveId(source, importer, resolveOptions) {
|
|
372
|
+
// Skip SSR resolution
|
|
373
|
+
if (resolveOptions?.ssr) {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Handle server file imports from client code
|
|
217
378
|
if (importer && shouldProcessFile(source, options)) {
|
|
218
379
|
const resolvedPath = path.resolve(path.dirname(importer), source);
|
|
219
380
|
return resolvedPath;
|
|
220
381
|
}
|
|
382
|
+
|
|
383
|
+
// Handle TypeScript imports from server files
|
|
384
|
+
if (importer && shouldProcessFile(importer, options)) {
|
|
385
|
+
// Check if this is a relative import
|
|
386
|
+
if (source.startsWith(".") || source.startsWith("/")) {
|
|
387
|
+
// Try to resolve TypeScript file
|
|
388
|
+
const basePath = path.resolve(path.dirname(importer), source);
|
|
389
|
+
const possiblePaths = [
|
|
390
|
+
basePath,
|
|
391
|
+
`${basePath}.ts`,
|
|
392
|
+
`${basePath}.tsx`,
|
|
393
|
+
path.join(basePath, "index.ts"),
|
|
394
|
+
path.join(basePath, "index.tsx"),
|
|
395
|
+
];
|
|
396
|
+
|
|
397
|
+
for (const possiblePath of possiblePaths) {
|
|
398
|
+
try {
|
|
399
|
+
const stats = await fs.stat(possiblePath);
|
|
400
|
+
// Only return if it's a file, not a directory
|
|
401
|
+
if (stats.isFile()) {
|
|
402
|
+
return possiblePath;
|
|
403
|
+
}
|
|
404
|
+
} catch {
|
|
405
|
+
// File doesn't exist, try next
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return null;
|
|
221
412
|
},
|
|
222
413
|
|
|
223
|
-
async load(id) {
|
|
414
|
+
async load(id, loadOptions) {
|
|
224
415
|
if (shouldProcessFile(id, options)) {
|
|
416
|
+
// Check if this is an SSR request - if so, let Vite handle the actual module
|
|
417
|
+
if (loadOptions?.ssr) {
|
|
418
|
+
return null; // Let Vite handle SSR loading of the actual module
|
|
419
|
+
}
|
|
225
420
|
try {
|
|
226
421
|
const code = await fs.readFile(id, "utf-8");
|
|
227
422
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
relativePath = id;
|
|
423
|
+
// Sanitize the file path for security
|
|
424
|
+
const sanitizedPath = sanitizePath(id, process.cwd());
|
|
425
|
+
if (!sanitizedPath) {
|
|
426
|
+
throw new Error(`Invalid file path detected: ${id}`);
|
|
233
427
|
}
|
|
234
428
|
|
|
429
|
+
let relativePath = path.relative(process.cwd(), sanitizedPath);
|
|
430
|
+
|
|
235
431
|
// Normalize path separators
|
|
236
432
|
relativePath = relativePath.replace(/\\/g, "/").replace(/^\//, "");
|
|
237
433
|
|
|
238
434
|
// Generate module name for internal use (must be valid identifier)
|
|
239
|
-
const moduleName = options.moduleNameTransform(relativePath);
|
|
435
|
+
const moduleName = createSecureModuleName(options.moduleNameTransform(relativePath));
|
|
240
436
|
|
|
241
437
|
// Validate module name
|
|
242
|
-
if (!moduleName
|
|
438
|
+
if (!isValidModuleName(moduleName)) {
|
|
243
439
|
throw new Error(`Invalid server module name: ${moduleName}`);
|
|
244
440
|
}
|
|
245
441
|
|
|
246
|
-
|
|
442
|
+
// Use AST parser to extract exported functions with detailed information
|
|
443
|
+
const exportedFunctions = extractExportedFunctions(code, id);
|
|
247
444
|
const functions = [];
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
445
|
+
const functionDetails = [];
|
|
446
|
+
|
|
447
|
+
for (const fn of exportedFunctions) {
|
|
448
|
+
// Skip default exports for now (could be supported in future)
|
|
449
|
+
if (fn.isDefault) {
|
|
450
|
+
console.warn(
|
|
451
|
+
createDevelopmentWarning("Default Export Skipped", `Default exports are not currently supported`, {
|
|
452
|
+
filePath: relativePath,
|
|
453
|
+
suggestion: "Use named exports instead: export async function myFunction() {}",
|
|
454
|
+
}),
|
|
455
|
+
);
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
252
458
|
|
|
253
459
|
// Validate function name
|
|
254
|
-
if (!
|
|
255
|
-
console.warn(
|
|
460
|
+
if (!isValidFunctionName(fn.name)) {
|
|
461
|
+
console.warn(
|
|
462
|
+
createDevelopmentWarning(
|
|
463
|
+
"Invalid Function Name",
|
|
464
|
+
`Function name '${fn.name}' is not a valid JavaScript identifier`,
|
|
465
|
+
{
|
|
466
|
+
filePath: relativePath,
|
|
467
|
+
suggestion:
|
|
468
|
+
"Function names must start with a letter, $, or _ and contain only letters, numbers, $, and _",
|
|
469
|
+
},
|
|
470
|
+
),
|
|
471
|
+
);
|
|
256
472
|
continue;
|
|
257
473
|
}
|
|
258
474
|
|
|
259
|
-
functions
|
|
475
|
+
// Warn about non-async functions
|
|
476
|
+
if (!fn.isAsync) {
|
|
477
|
+
console.warn(
|
|
478
|
+
createDevelopmentWarning(
|
|
479
|
+
"Non-Async Function",
|
|
480
|
+
`Function '${fn.name}' is not async. Server actions should typically be async`,
|
|
481
|
+
{
|
|
482
|
+
filePath: relativePath,
|
|
483
|
+
suggestion: "Consider changing to: export async function " + fn.name + "() {}",
|
|
484
|
+
},
|
|
485
|
+
),
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
functions.push(fn.name);
|
|
490
|
+
functionDetails.push(fn);
|
|
260
491
|
}
|
|
261
492
|
|
|
262
493
|
// Check for duplicate function names within the same module
|
|
@@ -265,16 +496,52 @@ export default function serverActions(userOptions = {}) {
|
|
|
265
496
|
console.warn(`Duplicate function names detected in ${id}`);
|
|
266
497
|
}
|
|
267
498
|
|
|
268
|
-
|
|
499
|
+
// Store both simple function names and detailed information
|
|
500
|
+
serverFunctions.set(moduleName, {
|
|
501
|
+
functions: uniqueFunctions,
|
|
502
|
+
functionDetails,
|
|
503
|
+
id,
|
|
504
|
+
filePath: relativePath,
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
// Development-time validation and feedback
|
|
508
|
+
if (process.env.NODE_ENV === "development") {
|
|
509
|
+
// Validate file structure
|
|
510
|
+
const fileWarnings = validateFileStructure(functionDetails, relativePath);
|
|
511
|
+
fileWarnings.forEach((warning) => console.warn(warning));
|
|
512
|
+
|
|
513
|
+
// Validate individual function signatures
|
|
514
|
+
functionDetails.forEach((func) => {
|
|
515
|
+
const funcWarnings = validateFunctionSignature(func, relativePath);
|
|
516
|
+
funcWarnings.forEach((warning) => console.warn(warning));
|
|
517
|
+
});
|
|
518
|
+
}
|
|
269
519
|
|
|
270
520
|
// Discover schemas from module if validation is enabled (development only)
|
|
271
|
-
|
|
521
|
+
// Skip TypeScript files to avoid SSR loading issues
|
|
522
|
+
if (options.validation.enabled && process.env.NODE_ENV !== "production" && !id.endsWith(".ts")) {
|
|
272
523
|
try {
|
|
273
|
-
const module = await
|
|
524
|
+
const module = await importModule(id, viteDevServer, tsModuleCache);
|
|
274
525
|
schemaDiscovery.discoverFromModule(module, moduleName);
|
|
526
|
+
|
|
527
|
+
// Validate schema attachment in development
|
|
528
|
+
if (process.env.NODE_ENV === "development") {
|
|
529
|
+
const schemaWarnings = validateSchemaAttachment(module, uniqueFunctions, relativePath);
|
|
530
|
+
schemaWarnings.forEach((warning) => console.warn(warning));
|
|
531
|
+
}
|
|
275
532
|
} catch (error) {
|
|
276
|
-
|
|
533
|
+
const enhancedError = enhanceModuleLoadError(id, error);
|
|
534
|
+
console.warn(enhancedError.message);
|
|
535
|
+
|
|
536
|
+
if (process.env.NODE_ENV === "development" && enhancedError.suggestions) {
|
|
537
|
+
enhancedError.suggestions.forEach((suggestion) => {
|
|
538
|
+
console.info(` 💡 ${suggestion}`);
|
|
539
|
+
});
|
|
540
|
+
}
|
|
277
541
|
}
|
|
542
|
+
} else if (options.validation.enabled && id.endsWith(".ts")) {
|
|
543
|
+
// For TypeScript files, defer schema discovery to request time
|
|
544
|
+
console.log(`[Vite Server Actions] Deferring schema discovery for TypeScript file: ${relativePath}`);
|
|
278
545
|
}
|
|
279
546
|
|
|
280
547
|
// Setup routes in development mode only
|
|
@@ -318,11 +585,29 @@ export default function serverActions(userOptions = {}) {
|
|
|
318
585
|
// Apply middleware before the handler
|
|
319
586
|
app.post(endpoint, ...contextMiddlewares, async (req, res) => {
|
|
320
587
|
try {
|
|
321
|
-
const module = await
|
|
588
|
+
const module = await importModule(id, viteDevServer, tsModuleCache);
|
|
589
|
+
|
|
590
|
+
// Lazy schema discovery for TypeScript files
|
|
591
|
+
if (
|
|
592
|
+
options.validation.enabled &&
|
|
593
|
+
id.endsWith(".ts") &&
|
|
594
|
+
!schemaDiscovery.hasSchema(moduleName, functionName)
|
|
595
|
+
) {
|
|
596
|
+
try {
|
|
597
|
+
schemaDiscovery.discoverFromModule(module, moduleName);
|
|
598
|
+
} catch (err) {
|
|
599
|
+
console.warn(`Failed to discover schemas for ${moduleName}:`, err.message);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
322
602
|
|
|
323
603
|
// Check if function exists in module
|
|
324
604
|
if (typeof module[functionName] !== "function") {
|
|
325
|
-
|
|
605
|
+
// Get available functions for better error message
|
|
606
|
+
const availableFunctions = Object.keys(module).filter((key) => typeof module[key] === "function");
|
|
607
|
+
|
|
608
|
+
const enhancedError = enhanceFunctionNotFoundError(functionName, moduleName, availableFunctions);
|
|
609
|
+
|
|
610
|
+
throw new Error(enhancedError.message);
|
|
326
611
|
}
|
|
327
612
|
|
|
328
613
|
// Validate request body is array for function arguments
|
|
@@ -331,25 +616,48 @@ export default function serverActions(userOptions = {}) {
|
|
|
331
616
|
}
|
|
332
617
|
|
|
333
618
|
const result = await module[functionName](...req.body);
|
|
334
|
-
|
|
619
|
+
if (result === undefined) {
|
|
620
|
+
res.status(204).end();
|
|
621
|
+
} else {
|
|
622
|
+
res.json(result);
|
|
623
|
+
}
|
|
335
624
|
} catch (error) {
|
|
336
625
|
console.error(`Error in ${functionName}: ${error.message}`);
|
|
337
626
|
|
|
338
627
|
if (error.message.includes("not found") || error.message.includes("not a function")) {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
628
|
+
// Extract available functions from the error context if available
|
|
629
|
+
const availableFunctionsMatch = error.message.match(/Available functions: ([^]+)/);
|
|
630
|
+
const availableFunctions = availableFunctionsMatch ? availableFunctionsMatch[1].split(", ") : [];
|
|
631
|
+
|
|
632
|
+
res.status(404).json(
|
|
633
|
+
createErrorResponse(404, "Function not found", "FUNCTION_NOT_FOUND", {
|
|
634
|
+
functionName,
|
|
635
|
+
moduleName,
|
|
636
|
+
availableFunctions: availableFunctions.length > 0 ? availableFunctions : undefined,
|
|
637
|
+
suggestion: `Try one of: ${availableFunctions.join(", ") || "none available"}`,
|
|
638
|
+
}),
|
|
639
|
+
);
|
|
343
640
|
} else if (error.message.includes("Request body")) {
|
|
344
|
-
res.status(400).json(
|
|
345
|
-
error
|
|
346
|
-
|
|
347
|
-
|
|
641
|
+
res.status(400).json(
|
|
642
|
+
createErrorResponse(400, error.message, "INVALID_REQUEST_BODY", {
|
|
643
|
+
suggestion: "Send an array of arguments: [arg1, arg2, ...]",
|
|
644
|
+
}),
|
|
645
|
+
);
|
|
348
646
|
} else {
|
|
349
|
-
res.status(500).json(
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
647
|
+
res.status(500).json(
|
|
648
|
+
createErrorResponse(
|
|
649
|
+
500,
|
|
650
|
+
"Internal server error",
|
|
651
|
+
"INTERNAL_ERROR",
|
|
652
|
+
process.env.NODE_ENV !== "production"
|
|
653
|
+
? {
|
|
654
|
+
message: error.message,
|
|
655
|
+
stack: error.stack,
|
|
656
|
+
suggestion: "Check server logs for more details",
|
|
657
|
+
}
|
|
658
|
+
: { suggestion: "Contact support if this persists" },
|
|
659
|
+
),
|
|
660
|
+
);
|
|
353
661
|
}
|
|
354
662
|
}
|
|
355
663
|
});
|
|
@@ -357,11 +665,29 @@ export default function serverActions(userOptions = {}) {
|
|
|
357
665
|
}
|
|
358
666
|
// OpenAPI endpoints will be set up during configureServer after all modules are loaded
|
|
359
667
|
|
|
360
|
-
|
|
668
|
+
// Use enhanced client proxy generator if we have detailed function information
|
|
669
|
+
if (functionDetails.length > 0) {
|
|
670
|
+
return generateEnhancedClientProxy(moduleName, functionDetails, options, relativePath);
|
|
671
|
+
} else {
|
|
672
|
+
// Fallback to basic proxy for backwards compatibility
|
|
673
|
+
return generateClientProxy(moduleName, uniqueFunctions, options, relativePath);
|
|
674
|
+
}
|
|
361
675
|
} catch (error) {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
676
|
+
const enhancedError = enhanceParsingError(id, error);
|
|
677
|
+
console.error(enhancedError.message);
|
|
678
|
+
|
|
679
|
+
// Provide helpful suggestions in development
|
|
680
|
+
if (process.env.NODE_ENV === "development" && enhancedError.suggestions.length > 0) {
|
|
681
|
+
console.info("[Vite Server Actions] 💡 Suggestions:");
|
|
682
|
+
enhancedError.suggestions.forEach((suggestion) => {
|
|
683
|
+
console.info(` • ${suggestion}`);
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Return error comment with context instead of failing the build
|
|
688
|
+
return `// Failed to load server actions from ${id}
|
|
689
|
+
// Error: ${error.message}
|
|
690
|
+
// ${enhancedError.suggestions.length > 0 ? "Suggestions: " + enhancedError.suggestions.join(", ") : ""}`;
|
|
365
691
|
}
|
|
366
692
|
}
|
|
367
693
|
},
|
|
@@ -398,6 +724,22 @@ export default function serverActions(userOptions = {}) {
|
|
|
398
724
|
}
|
|
399
725
|
},
|
|
400
726
|
},
|
|
727
|
+
{
|
|
728
|
+
name: "typescript-transform",
|
|
729
|
+
async load(id) {
|
|
730
|
+
// Handle TypeScript files
|
|
731
|
+
if (id.endsWith(".ts")) {
|
|
732
|
+
const code = await fs.readFile(id, "utf-8");
|
|
733
|
+
const result = await esbuild.transform(code, {
|
|
734
|
+
loader: "ts",
|
|
735
|
+
target: "node16",
|
|
736
|
+
format: "esm",
|
|
737
|
+
});
|
|
738
|
+
return result.code;
|
|
739
|
+
}
|
|
740
|
+
return null;
|
|
741
|
+
},
|
|
742
|
+
},
|
|
401
743
|
{
|
|
402
744
|
name: "external-modules",
|
|
403
745
|
resolveId(source) {
|
|
@@ -424,12 +766,23 @@ export default function serverActions(userOptions = {}) {
|
|
|
424
766
|
source: bundledCode,
|
|
425
767
|
});
|
|
426
768
|
|
|
769
|
+
// Generate and emit TypeScript definitions
|
|
770
|
+
const typeDefinitions = generateTypeDefinitions(serverFunctions, options);
|
|
771
|
+
this.emitFile({
|
|
772
|
+
type: "asset",
|
|
773
|
+
fileName: "actions.d.ts",
|
|
774
|
+
source: typeDefinitions,
|
|
775
|
+
});
|
|
776
|
+
|
|
427
777
|
// Generate OpenAPI spec if enabled
|
|
428
778
|
let openAPISpec = null;
|
|
429
779
|
if (options.openAPI.enabled) {
|
|
780
|
+
// Use PORT env var for production builds, defaulting to 3000
|
|
781
|
+
const port = process.env.PORT || 3000;
|
|
430
782
|
openAPISpec = openAPIGenerator.generateSpec(serverFunctions, schemaDiscovery, {
|
|
431
783
|
apiPrefix: options.apiPrefix,
|
|
432
784
|
routeTransform: options.routeTransform,
|
|
785
|
+
port,
|
|
433
786
|
});
|
|
434
787
|
|
|
435
788
|
// Emit OpenAPI spec as a separate file
|
|
@@ -441,7 +794,7 @@ export default function serverActions(userOptions = {}) {
|
|
|
441
794
|
}
|
|
442
795
|
|
|
443
796
|
// Generate validation code if enabled
|
|
444
|
-
const validationCode = generateValidationCode(options, serverFunctions);
|
|
797
|
+
const validationCode = await generateValidationCode(options, serverFunctions);
|
|
445
798
|
|
|
446
799
|
// Generate server.js
|
|
447
800
|
const serverCode = `
|
|
@@ -450,6 +803,7 @@ export default function serverActions(userOptions = {}) {
|
|
|
450
803
|
${options.openAPI.enabled && options.openAPI.swaggerUI ? "import swaggerUi from 'swagger-ui-express';" : ""}
|
|
451
804
|
${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
805
|
${validationCode.imports}
|
|
806
|
+
${validationCode.validationRuntime}
|
|
453
807
|
|
|
454
808
|
const app = express();
|
|
455
809
|
${validationCode.setup}
|
|
@@ -474,10 +828,22 @@ export default function serverActions(userOptions = {}) {
|
|
|
474
828
|
app.post('${options.apiPrefix}/${routePath}', ${middlewareCall}async (req, res) => {
|
|
475
829
|
try {
|
|
476
830
|
const result = await serverActions.${moduleName}.${functionName}(...req.body);
|
|
477
|
-
|
|
831
|
+
if (result === undefined) {
|
|
832
|
+
res.status(204).end();
|
|
833
|
+
} else {
|
|
834
|
+
res.json(result);
|
|
835
|
+
}
|
|
478
836
|
} catch (error) {
|
|
479
837
|
console.error(\`Error in ${functionName}: \${error.message}\`);
|
|
480
|
-
|
|
838
|
+
const status = error.status || 500;
|
|
839
|
+
res.status(status).json({
|
|
840
|
+
error: true,
|
|
841
|
+
status,
|
|
842
|
+
message: status === 500 ? 'Internal server error' : error.message,
|
|
843
|
+
code: error.code || 'SERVER_ACTION_ERROR',
|
|
844
|
+
timestamp: new Date().toISOString(),
|
|
845
|
+
...(process.env.NODE_ENV !== 'production' ? { details: { message: error.message, stack: error.stack } } : {})
|
|
846
|
+
});
|
|
481
847
|
}
|
|
482
848
|
});
|
|
483
849
|
`;
|
|
@@ -543,24 +909,13 @@ function generateClientProxy(moduleName, functions, options, filePath) {
|
|
|
543
909
|
|
|
544
910
|
let clientProxy = `\n// vite-server-actions: ${moduleName}\n`;
|
|
545
911
|
|
|
546
|
-
//
|
|
912
|
+
// Mark this as a legitimate client proxy module
|
|
547
913
|
if (isDev) {
|
|
548
914
|
clientProxy += `
|
|
549
|
-
// Development-only
|
|
915
|
+
// Development-only marker for client proxy module
|
|
550
916
|
if (typeof window !== 'undefined') {
|
|
551
|
-
|
|
552
|
-
|
|
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
|
-
}
|
|
917
|
+
window.__VITE_SERVER_ACTIONS_PROXY__ = window.__VITE_SERVER_ACTIONS_PROXY__ || {};
|
|
918
|
+
window.__VITE_SERVER_ACTIONS_PROXY__['${moduleName}'] = true;
|
|
564
919
|
}
|
|
565
920
|
`;
|
|
566
921
|
}
|
|
@@ -575,11 +930,6 @@ if (typeof window !== 'undefined') {
|
|
|
575
930
|
${
|
|
576
931
|
isDev
|
|
577
932
|
? `
|
|
578
|
-
// Development-only: Mark that we're in a valid proxy context
|
|
579
|
-
if (typeof window !== 'undefined') {
|
|
580
|
-
window.__VITE_SERVER_ACTIONS_PROXY__ = true;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
933
|
// Validate arguments in development
|
|
584
934
|
if (args.some(arg => typeof arg === 'function')) {
|
|
585
935
|
console.warn(
|
|
@@ -615,16 +965,18 @@ if (typeof window !== 'undefined') {
|
|
|
615
965
|
}
|
|
616
966
|
|
|
617
967
|
console.log("[Vite Server Actions] ✅ - ${functionName} executed successfully");
|
|
968
|
+
|
|
969
|
+
// Handle 204 No Content responses (function returned undefined)
|
|
970
|
+
if (response.status === 204) {
|
|
971
|
+
return undefined;
|
|
972
|
+
}
|
|
973
|
+
|
|
618
974
|
const result = await response.json();
|
|
619
975
|
|
|
620
976
|
${
|
|
621
977
|
isDev
|
|
622
978
|
? `
|
|
623
|
-
|
|
624
|
-
if (typeof window !== 'undefined') {
|
|
625
|
-
window.__VITE_SERVER_ACTIONS_PROXY__ = false;
|
|
626
|
-
}
|
|
627
|
-
`
|
|
979
|
+
`
|
|
628
980
|
: ""
|
|
629
981
|
}
|
|
630
982
|
|
|
@@ -636,11 +988,7 @@ if (typeof window !== 'undefined') {
|
|
|
636
988
|
${
|
|
637
989
|
isDev
|
|
638
990
|
? `
|
|
639
|
-
|
|
640
|
-
if (typeof window !== 'undefined') {
|
|
641
|
-
window.__VITE_SERVER_ACTIONS_PROXY__ = false;
|
|
642
|
-
}
|
|
643
|
-
`
|
|
991
|
+
`
|
|
644
992
|
: ""
|
|
645
993
|
}
|
|
646
994
|
|