vite-plugin-server-actions 0.1.0 → 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.
@@ -0,0 +1,89 @@
1
+ import util from "util";
2
+ import { createValidationMiddleware } from "./validation.js";
3
+
4
+ /**
5
+ * Built-in logging middleware for server actions
6
+ * Logs the action being triggered and the request body with syntax highlighting
7
+ */
8
+ export function loggingMiddleware(req, res, next) {
9
+ const timestamp = new Date().toISOString();
10
+ const method = req.method;
11
+ const url = req.url;
12
+
13
+ // Extract action name from URL (format: /api/module_name/functionName)
14
+ const urlParts = url.split("/");
15
+ const functionName = urlParts[urlParts.length - 1];
16
+ const moduleName = urlParts[urlParts.length - 2];
17
+
18
+ // Log action trigger
19
+ console.log(`\n[${timestamp}] šŸš€ Server Action Triggered`);
20
+ console.log(`ā”œā”€ Module: ${moduleName}`);
21
+ console.log(`ā”œā”€ Function: ${functionName}`);
22
+ console.log(`ā”œā”€ Method: ${method}`);
23
+ console.log(`└─ Endpoint: ${url}`);
24
+
25
+ // Log request body with syntax highlighting
26
+ if (req.body && Object.keys(req.body).length > 0) {
27
+ console.log("\nšŸ“¦ Request Body:");
28
+ // Use util.inspect for colored output
29
+ console.log(
30
+ util.inspect(req.body, {
31
+ colors: true,
32
+ depth: null,
33
+ compact: false,
34
+ }),
35
+ );
36
+ } else {
37
+ console.log("\nšŸ“¦ Request Body: (empty)");
38
+ }
39
+
40
+ // Track response time
41
+ const startTime = Date.now();
42
+
43
+ // Override res.json to log response
44
+ const originalJson = res.json.bind(res);
45
+ res.json = function (data) {
46
+ const duration = Date.now() - startTime;
47
+ console.log(`\nāœ… Response sent in ${duration}ms`);
48
+ if (data) {
49
+ console.log("šŸ“¤ Response data:");
50
+ console.log(
51
+ util.inspect(data, {
52
+ colors: true,
53
+ depth: null,
54
+ compact: false,
55
+ maxArrayLength: 10, // Limit array output
56
+ }),
57
+ );
58
+ }
59
+ console.log("─".repeat(50));
60
+ return originalJson(data);
61
+ };
62
+
63
+ // Handle errors
64
+ const originalStatus = res.status.bind(res);
65
+ res.status = function (statusCode) {
66
+ if (statusCode >= 400) {
67
+ const duration = Date.now() - startTime;
68
+ console.log(`\nāŒ Error response (${statusCode}) sent in ${duration}ms`);
69
+ }
70
+ return originalStatus(statusCode);
71
+ };
72
+
73
+ next();
74
+ }
75
+
76
+ /**
77
+ * Create validation middleware with custom options
78
+ * @param {object} options - Validation options
79
+ * @returns {function} Validation middleware
80
+ */
81
+ export function createValidationMiddlewareWithOptions(options = {}) {
82
+ return createValidationMiddleware(options);
83
+ }
84
+
85
+ // Export a namespace for all built-in middleware
86
+ export const middleware = {
87
+ logging: loggingMiddleware,
88
+ validation: createValidationMiddleware,
89
+ };
package/src/openapi.js ADDED
@@ -0,0 +1,369 @@
1
+ import swaggerUi from "swagger-ui-express";
2
+ import { defaultAdapter } from "./validation.js";
3
+
4
+ /**
5
+ * OpenAPI specification generator
6
+ */
7
+ export class OpenAPIGenerator {
8
+ constructor(options = {}) {
9
+ this.adapter = options.adapter || defaultAdapter;
10
+ this.info = {
11
+ title: "Server Actions API",
12
+ version: "1.0.0",
13
+ description: "Auto-generated API documentation for Vite Server Actions",
14
+ ...options.info,
15
+ };
16
+ this.servers = options.servers || [
17
+ {
18
+ url: "http://localhost:5173",
19
+ description: "Development server",
20
+ },
21
+ ];
22
+ }
23
+
24
+ /**
25
+ * Generate complete OpenAPI specification
26
+ * @param {Map} serverFunctions - Map of server functions
27
+ * @param {SchemaDiscovery} schemaDiscovery - Schema discovery instance
28
+ * @param {object} options - Generation options
29
+ * @returns {object} Complete OpenAPI 3.0 specification
30
+ */
31
+ generateSpec(serverFunctions, schemaDiscovery, options = {}) {
32
+ const spec = {
33
+ openapi: "3.0.3",
34
+ info: this.info,
35
+ servers: this.servers,
36
+ paths: {},
37
+ components: {
38
+ schemas: {},
39
+ },
40
+ };
41
+
42
+ // Generate paths for each server function
43
+ for (const [moduleName, { functions, filePath }] of serverFunctions) {
44
+ for (const functionName of functions) {
45
+ // Use routeTransform if provided, otherwise fall back to legacy format
46
+ let routePath;
47
+ if (options.routeTransform && filePath) {
48
+ routePath = options.routeTransform(filePath, functionName);
49
+ } else {
50
+ // Fallback to legacy format for backward compatibility
51
+ routePath = `${moduleName}/${functionName}`;
52
+ }
53
+
54
+ const path = `${options.apiPrefix || "/api"}/${routePath}`;
55
+ const schema = schemaDiscovery.getSchema(moduleName, functionName);
56
+
57
+ spec.paths[path] = this.generatePathItem(moduleName, functionName, schema);
58
+ }
59
+ }
60
+
61
+ return spec;
62
+ }
63
+
64
+ /**
65
+ * Generate OpenAPI path item for a server function
66
+ * @param {string} moduleName - Module name
67
+ * @param {string} functionName - Function name
68
+ * @param {any} schema - Validation schema
69
+ * @returns {object} OpenAPI path item
70
+ */
71
+ generatePathItem(moduleName, functionName, schema) {
72
+ const operationId = `${moduleName}_${functionName}`;
73
+ const tags = [moduleName];
74
+
75
+ const pathItem = {
76
+ post: {
77
+ operationId,
78
+ tags,
79
+ summary: `Execute ${functionName}`,
80
+ description: `Execute the ${functionName} server action from ${moduleName} module`,
81
+ requestBody: {
82
+ required: true,
83
+ content: {
84
+ "application/json": {
85
+ schema: this.generateRequestSchema(schema),
86
+ },
87
+ },
88
+ },
89
+ responses: {
90
+ 200: {
91
+ description: "Successful response",
92
+ content: {
93
+ "application/json": {
94
+ schema: {
95
+ type: "object",
96
+ description: "Function result",
97
+ },
98
+ },
99
+ },
100
+ },
101
+ 400: {
102
+ description: "Validation error",
103
+ content: {
104
+ "application/json": {
105
+ schema: this.getErrorSchema(),
106
+ },
107
+ },
108
+ },
109
+ 404: {
110
+ description: "Function not found",
111
+ content: {
112
+ "application/json": {
113
+ schema: this.getErrorSchema(),
114
+ },
115
+ },
116
+ },
117
+ 500: {
118
+ description: "Internal server error",
119
+ content: {
120
+ "application/json": {
121
+ schema: this.getErrorSchema(),
122
+ },
123
+ },
124
+ },
125
+ },
126
+ },
127
+ };
128
+
129
+ return pathItem;
130
+ }
131
+
132
+ /**
133
+ * Generate request schema for a server function
134
+ * @param {any} schema - Validation schema
135
+ * @returns {object} OpenAPI schema for request body
136
+ */
137
+ generateRequestSchema(schema) {
138
+ if (!schema) {
139
+ return {
140
+ type: "array",
141
+ description: "Function arguments array",
142
+ items: {
143
+ type: "object",
144
+ description: "Function argument",
145
+ },
146
+ };
147
+ }
148
+
149
+ // Server functions receive arguments as an array
150
+ // But if schema is defined, we assume it validates the first argument
151
+ return {
152
+ type: "array",
153
+ description: "Function arguments",
154
+ items: this.adapter.toOpenAPISchema(schema),
155
+ };
156
+ }
157
+
158
+ /**
159
+ * Get standard error response schema
160
+ * @returns {object} OpenAPI error schema
161
+ */
162
+ getErrorSchema() {
163
+ return {
164
+ type: "object",
165
+ properties: {
166
+ error: {
167
+ type: "string",
168
+ description: "Error message",
169
+ },
170
+ details: {
171
+ type: "string",
172
+ description: "Error details",
173
+ },
174
+ validationErrors: {
175
+ type: "array",
176
+ description: "Validation errors (if applicable)",
177
+ items: {
178
+ type: "object",
179
+ properties: {
180
+ path: {
181
+ type: "string",
182
+ description: "Field path",
183
+ },
184
+ message: {
185
+ type: "string",
186
+ description: "Error message",
187
+ },
188
+ code: {
189
+ type: "string",
190
+ description: "Error code",
191
+ },
192
+ },
193
+ },
194
+ },
195
+ },
196
+ required: ["error"],
197
+ };
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Create Swagger UI middleware
203
+ * @param {object} openAPISpec - OpenAPI specification
204
+ * @param {object} options - Swagger UI options
205
+ * @returns {Array} Array of middleware functions
206
+ */
207
+ export function createSwaggerMiddleware(openAPISpec, options = {}) {
208
+ const swaggerOptions = {
209
+ customCss: `
210
+ .swagger-ui .topbar { display: none; }
211
+ .swagger-ui .info .title { color: #3b82f6; }
212
+ `,
213
+ customSiteTitle: "Server Actions API Documentation",
214
+ ...options.swaggerOptions,
215
+ };
216
+
217
+ return [swaggerUi.serve, swaggerUi.setup(openAPISpec, swaggerOptions)];
218
+ }
219
+
220
+ /**
221
+ * Setup OpenAPI endpoints for development
222
+ * @param {Express} app - Express app instance
223
+ * @param {object} openAPISpec - OpenAPI specification
224
+ * @param {object} options - Setup options
225
+ */
226
+ export function setupOpenAPIEndpoints(app, openAPISpec, options = {}) {
227
+ const docsPath = options.docsPath || "/api/docs";
228
+ const specPath = options.specPath || "/api/openapi.json";
229
+
230
+ // Serve OpenAPI specification as JSON
231
+ app.get(specPath, (req, res) => {
232
+ res.json(openAPISpec);
233
+ });
234
+
235
+ // Serve Swagger UI
236
+ if (options.enableSwaggerUI !== false) {
237
+ const swaggerMiddleware = createSwaggerMiddleware(openAPISpec, options);
238
+ app.use(docsPath, ...swaggerMiddleware);
239
+
240
+ console.log(`šŸ“– API Documentation: http://localhost:${process.env.PORT || 5173}${docsPath}`);
241
+ console.log(`šŸ“„ OpenAPI Spec: http://localhost:${process.env.PORT || 5173}${specPath}`);
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Generate OpenAPI-compatible parameter descriptions from JSDoc
247
+ * @param {string} jsdoc - JSDoc comment string
248
+ * @returns {Array} Array of parameter descriptions
249
+ */
250
+ export function parseJSDocParameters(jsdoc) {
251
+ if (!jsdoc) {
252
+ return [];
253
+ }
254
+
255
+ const paramRegex = /@param\s+\{([^}]+)\}\s+(\[?[\w.]+\]?)\s*-?\s*(.*)/g;
256
+ const parameters = [];
257
+ let match;
258
+
259
+ while ((match = paramRegex.exec(jsdoc)) !== null) {
260
+ const [, type, name, description] = match;
261
+ const isOptional = name.startsWith("[") && name.endsWith("]");
262
+ const paramName = name.replace(/^\[|\]$/g, "");
263
+
264
+ parameters.push({
265
+ name: paramName,
266
+ type: type.toLowerCase(),
267
+ description: description.trim(),
268
+ required: !isOptional,
269
+ });
270
+ }
271
+
272
+ return parameters;
273
+ }
274
+
275
+ /**
276
+ * Enhanced OpenAPI generator with JSDoc support
277
+ */
278
+ export class EnhancedOpenAPIGenerator extends OpenAPIGenerator {
279
+ /**
280
+ * Generate path item with JSDoc enhancement
281
+ * @param {string} moduleName - Module name
282
+ * @param {string} functionName - Function name
283
+ * @param {any} schema - Validation schema
284
+ * @param {string} jsdoc - JSDoc comment
285
+ * @returns {object} Enhanced OpenAPI path item
286
+ */
287
+ generatePathItemWithJSDoc(moduleName, functionName, schema, jsdoc) {
288
+ const pathItem = this.generatePathItem(moduleName, functionName, schema);
289
+
290
+ if (jsdoc) {
291
+ const jsDocParams = parseJSDocParameters(jsdoc);
292
+
293
+ // Extract description from JSDoc
294
+ const descriptionMatch = jsdoc.match(/\/\*\*\s*\n\s*\*\s*([^@\n]*)/);
295
+ if (descriptionMatch) {
296
+ pathItem.post.description = descriptionMatch[1].trim();
297
+ }
298
+
299
+ // Enhance request schema with JSDoc information
300
+ if (jsDocParams.length > 0 && !schema) {
301
+ pathItem.post.requestBody.content["application/json"].schema = {
302
+ type: "array",
303
+ description: "Function arguments",
304
+ items: this.generateSchemaFromJSDoc(jsDocParams),
305
+ };
306
+ }
307
+ }
308
+
309
+ return pathItem;
310
+ }
311
+
312
+ /**
313
+ * Generate OpenAPI schema from JSDoc parameters
314
+ * @param {Array} jsDocParams - JSDoc parameter descriptions
315
+ * @returns {object} OpenAPI schema
316
+ */
317
+ generateSchemaFromJSDoc(jsDocParams) {
318
+ if (jsDocParams.length === 1) {
319
+ // Single parameter
320
+ return this.jsDocTypeToOpenAPISchema(jsDocParams[0]);
321
+ }
322
+
323
+ // Multiple parameters - create object schema
324
+ const properties = {};
325
+ const required = [];
326
+
327
+ for (const param of jsDocParams) {
328
+ properties[param.name] = this.jsDocTypeToOpenAPISchema(param);
329
+ if (param.required) {
330
+ required.push(param.name);
331
+ }
332
+ }
333
+
334
+ return {
335
+ type: "object",
336
+ properties,
337
+ required: required.length > 0 ? required : undefined,
338
+ };
339
+ }
340
+
341
+ /**
342
+ * Convert JSDoc type to OpenAPI schema
343
+ * @param {object} param - JSDoc parameter object
344
+ * @returns {object} OpenAPI schema
345
+ */
346
+ jsDocTypeToOpenAPISchema(param) {
347
+ const { type, description } = param;
348
+
349
+ switch (type.toLowerCase()) {
350
+ case "string":
351
+ return { type: "string", description };
352
+ case "number":
353
+ return { type: "number", description };
354
+ case "boolean":
355
+ return { type: "boolean", description };
356
+ case "object":
357
+ return { type: "object", description };
358
+ case "array":
359
+ return { type: "array", items: { type: "object" }, description };
360
+ default:
361
+ // Handle union types like 'low'|'medium'|'high'
362
+ if (type.includes("|")) {
363
+ const enumValues = type.split("|").map((v) => v.replace(/['"]/g, "").trim());
364
+ return { type: "string", enum: enumValues, description };
365
+ }
366
+ return { type: "object", description };
367
+ }
368
+ }
369
+ }
package/src/types.ts ADDED
@@ -0,0 +1,35 @@
1
+ import type { Plugin } from "vite";
2
+ import type { Express } from "express";
3
+
4
+ export interface ServerActionOptions {
5
+ /**
6
+ * Custom API prefix for server action endpoints
7
+ * @default "/api"
8
+ */
9
+ apiPrefix?: string;
10
+
11
+ /**
12
+ * Include patterns for server action files
13
+ * @default "**\/*.server.js"
14
+ */
15
+ include?: string | string[];
16
+
17
+ /**
18
+ * Exclude patterns for server action files
19
+ */
20
+ exclude?: string | string[];
21
+ }
22
+
23
+ export interface ServerFunction {
24
+ name: string;
25
+ isAsync: boolean;
26
+ }
27
+
28
+ export interface ServerModule {
29
+ functions: string[];
30
+ id: string;
31
+ }
32
+
33
+ export interface ServerActionsPlugin extends Plugin {
34
+ name: "vite-plugin-server-actions";
35
+ }