vite-plugin-server-actions 0.1.1 → 1.0.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/LICENSE +21 -0
- package/README.md +606 -105
- package/index.d.ts +200 -0
- package/package.json +74 -11
- package/src/build-utils.js +101 -0
- package/src/index.js +531 -58
- package/src/middleware.js +89 -0
- package/src/openapi.js +369 -0
- package/src/types.ts +35 -0
- package/src/validation.js +307 -0
- package/.editorconfig +0 -20
- package/.nvmrc +0 -1
- package/.prettierrc +0 -7
- package/examples/todo-app/.prettierrc +0 -17
- package/examples/todo-app/README.md +0 -58
- package/examples/todo-app/index.html +0 -12
- package/examples/todo-app/jsconfig.json +0 -32
- package/examples/todo-app/package.json +0 -22
- package/examples/todo-app/src/App.svelte +0 -155
- package/examples/todo-app/src/actions/auth.server.js +0 -14
- package/examples/todo-app/src/actions/todo.server.js +0 -57
- package/examples/todo-app/src/app.css +0 -0
- package/examples/todo-app/src/main.js +0 -8
- package/examples/todo-app/svelte.config.js +0 -7
- package/examples/todo-app/todos.json +0 -27
- package/examples/todo-app/vite.config.js +0 -12
- package/examples/todo-app/yarn.lock +0 -658
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 (
|
|
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
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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];
|
|
252
|
+
|
|
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
|
+
}
|
|
37
261
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
466
|
+
.flatMap(([moduleName, { functions, filePath }]) =>
|
|
467
|
+
functions
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
483
|
+
`;
|
|
484
|
+
})
|
|
485
|
+
.join("\n")
|
|
486
|
+
.trim(),
|
|
487
|
+
)
|
|
488
|
+
.join("\n")
|
|
489
|
+
.trim()}
|
|
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
|
+
}
|
|
147
511
|
|
|
148
512
|
// Start server
|
|
149
513
|
// --------------------------------------------------
|
|
150
514
|
const port = process.env.PORT || 3000;
|
|
151
|
-
app.listen(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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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";
|