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.
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Development-time validation and feedback system
3
+ * Provides real-time feedback to developers about their server actions
4
+ */
5
+
6
+ import { createDevelopmentWarning, generateHelpfulSuggestions } from "./error-enhancer.js";
7
+
8
+ /**
9
+ * Validate function parameters and provide feedback
10
+ * @param {Object} func - Function information from AST
11
+ * @param {string} filePath - File path for context
12
+ * @returns {Array<string>} - Array of validation warnings
13
+ */
14
+ export function validateFunctionSignature(func, filePath) {
15
+ const warnings = [];
16
+
17
+ // Check for proper parameter typing
18
+ if (func.params && func.params.length > 0) {
19
+ const untypedParams = func.params.filter((param) => !param.type);
20
+
21
+ if (untypedParams.length > 0) {
22
+ warnings.push(
23
+ createDevelopmentWarning(
24
+ "Missing Type Annotations",
25
+ `Parameters ${untypedParams.map((p) => p.name).join(", ")} in '${func.name}' lack type annotations`,
26
+ {
27
+ filePath,
28
+ suggestion: "Add TypeScript types for better development experience and type safety",
29
+ },
30
+ ),
31
+ );
32
+ }
33
+ }
34
+
35
+ // Check for proper return type annotation
36
+ if (!func.returnType && func.isAsync) {
37
+ warnings.push(
38
+ createDevelopmentWarning(
39
+ "Missing Return Type",
40
+ `Async function '${func.name}' should have a return type annotation`,
41
+ {
42
+ filePath,
43
+ suggestion: "Add return type like: Promise<MyReturnType>",
44
+ },
45
+ ),
46
+ );
47
+ }
48
+
49
+ // Check for JSDoc documentation
50
+ if (!func.jsdoc) {
51
+ warnings.push(
52
+ createDevelopmentWarning("Missing Documentation", `Function '${func.name}' lacks JSDoc documentation`, {
53
+ filePath,
54
+ suggestion: "Add JSDoc comments to document what this function does",
55
+ }),
56
+ );
57
+ }
58
+
59
+ // Check for complex parameter patterns that might be hard to serialize
60
+ if (func.params) {
61
+ const complexParams = func.params.filter((param) => param.name.includes("{") || param.name.includes("["));
62
+
63
+ if (complexParams.length > 0) {
64
+ warnings.push(
65
+ createDevelopmentWarning(
66
+ "Complex Parameter Destructuring",
67
+ `Function '${func.name}' uses complex destructuring that might be hard to serialize`,
68
+ {
69
+ filePath,
70
+ suggestion: "Consider using simple parameters and destructure inside the function",
71
+ },
72
+ ),
73
+ );
74
+ }
75
+ }
76
+
77
+ return warnings;
78
+ }
79
+
80
+ /**
81
+ * Validate server action file structure
82
+ * @param {Array} functionDetails - Array of function details
83
+ * @param {string} filePath - File path for context
84
+ * @returns {Array<string>} - Array of validation warnings
85
+ */
86
+ export function validateFileStructure(functionDetails, filePath) {
87
+ const warnings = [];
88
+
89
+ // Check if file has any functions
90
+ if (functionDetails.length === 0) {
91
+ warnings.push(
92
+ createDevelopmentWarning("No Functions Found", "No exported functions found in server action file", {
93
+ filePath,
94
+ suggestion: "Make sure to export your functions: export async function myFunction() {}",
95
+ }),
96
+ );
97
+ return warnings;
98
+ }
99
+
100
+ // Check for too many functions in one file
101
+ if (functionDetails.length > 10) {
102
+ warnings.push(
103
+ createDevelopmentWarning(
104
+ "Large File",
105
+ `File contains ${functionDetails.length} functions. Consider splitting into smaller modules`,
106
+ {
107
+ filePath,
108
+ suggestion: "Group related functions and split into multiple .server.js files",
109
+ },
110
+ ),
111
+ );
112
+ }
113
+
114
+ // Check for naming consistency
115
+ const functionNames = functionDetails.map((fn) => fn.name);
116
+ const hasInconsistentNaming = checkNamingConsistency(functionNames);
117
+
118
+ if (hasInconsistentNaming) {
119
+ warnings.push(
120
+ createDevelopmentWarning("Inconsistent Naming", "Function names use inconsistent naming patterns", {
121
+ filePath,
122
+ suggestion: "Use consistent naming: camelCase (getUserById) or snake_case (get_user_by_id)",
123
+ }),
124
+ );
125
+ }
126
+
127
+ return warnings;
128
+ }
129
+
130
+ /**
131
+ * Validate function arguments at runtime (development only)
132
+ * @param {string} functionName - Name of the function being called
133
+ * @param {Array} args - Arguments being passed
134
+ * @param {Object} functionInfo - Function metadata
135
+ * @returns {Array<string>} - Array of validation warnings
136
+ */
137
+ export function validateRuntimeArguments(functionName, args, functionInfo) {
138
+ const warnings = [];
139
+
140
+ if (process.env.NODE_ENV !== "development") {
141
+ return warnings; // Only validate in development
142
+ }
143
+
144
+ // Check argument count
145
+ if (functionInfo && functionInfo.params) {
146
+ const requiredParams = functionInfo.params.filter((p) => !p.isOptional && !p.isRest);
147
+ const maxParams = functionInfo.params.filter((p) => !p.isRest).length;
148
+
149
+ if (args.length < requiredParams.length) {
150
+ warnings.push(
151
+ `Function '${functionName}' expects at least ${requiredParams.length} arguments, got ${args.length}`,
152
+ );
153
+ }
154
+
155
+ if (args.length > maxParams && !functionInfo.params.some((p) => p.isRest)) {
156
+ warnings.push(`Function '${functionName}' expects at most ${maxParams} arguments, got ${args.length}`);
157
+ }
158
+ }
159
+
160
+ // Check for non-serializable arguments
161
+ args.forEach((arg, index) => {
162
+ if (typeof arg === "function") {
163
+ warnings.push(`Argument ${index + 1} is a function and cannot be serialized`);
164
+ } else if (arg instanceof Date) {
165
+ warnings.push(`Argument ${index + 1} is a Date object. Consider passing as ISO string`);
166
+ } else if (arg instanceof RegExp) {
167
+ warnings.push(`Argument ${index + 1} is a RegExp and cannot be serialized`);
168
+ } else if (arg && typeof arg === "object" && arg.constructor !== Object && !Array.isArray(arg)) {
169
+ warnings.push(`Argument ${index + 1} is a custom object instance that may not serialize properly`);
170
+ }
171
+ });
172
+
173
+ return warnings;
174
+ }
175
+
176
+ /**
177
+ * Generate development-time type information
178
+ * @param {Object} functionInfo - Function information
179
+ * @returns {string} - TypeScript-like type definition
180
+ */
181
+ export function generateTypeInfo(functionInfo) {
182
+ const { name, params, returnType, isAsync } = functionInfo;
183
+
184
+ const paramStrings = params.map((param) => {
185
+ let paramStr = param.name;
186
+ if (param.type) {
187
+ paramStr += `: ${param.type}`;
188
+ }
189
+ if (param.defaultValue) {
190
+ paramStr += ` = ${param.defaultValue}`;
191
+ }
192
+ return paramStr;
193
+ });
194
+
195
+ const returnTypeStr = returnType || "any";
196
+ const finalReturnType = isAsync ? `Promise<${returnTypeStr}>` : returnTypeStr;
197
+
198
+ return `function ${name}(${paramStrings.join(", ")}): ${finalReturnType}`;
199
+ }
200
+
201
+ /**
202
+ * Check naming consistency across functions
203
+ * @param {Array<string>} functionNames - Array of function names
204
+ * @returns {boolean} - True if naming is inconsistent
205
+ */
206
+ function checkNamingConsistency(functionNames) {
207
+ if (functionNames.length < 2) return false;
208
+
209
+ const camelCaseCount = functionNames.filter((name) => /^[a-z][a-zA-Z0-9]*$/.test(name)).length;
210
+ const snakeCaseCount = functionNames.filter((name) => /^[a-z][a-z0-9_]*$/.test(name) && name.includes("_")).length;
211
+ const pascalCaseCount = functionNames.filter((name) => /^[A-Z][a-zA-Z0-9]*$/.test(name)).length;
212
+
213
+ // If multiple naming styles are used significantly, it's inconsistent
214
+ const styles = [camelCaseCount, snakeCaseCount, pascalCaseCount].filter((count) => count > 0);
215
+ return styles.length > 1 && Math.max(...styles) < functionNames.length * 0.8;
216
+ }
217
+
218
+ /**
219
+ * Create development feedback for the console
220
+ * @param {Object} serverFunctions - Map of server functions
221
+ * @returns {string} - Formatted feedback message
222
+ */
223
+ export function createDevelopmentFeedback(serverFunctions) {
224
+ let feedback = "\n[Vite Server Actions] 📋 Development Feedback:\n";
225
+
226
+ const totalFunctions = Array.from(serverFunctions.values()).reduce(
227
+ (sum, module) => sum + (module.functions?.length || 0),
228
+ 0,
229
+ );
230
+
231
+ feedback += ` 📊 Found ${totalFunctions} server actions across ${serverFunctions.size} modules\n`;
232
+
233
+ // List modules and their functions
234
+ for (const [moduleName, moduleInfo] of serverFunctions) {
235
+ const { functions, filePath } = moduleInfo;
236
+ feedback += ` 📁 ${filePath}: ${functions.join(", ")}\n`;
237
+ }
238
+
239
+ feedback += "\n 💡 Tips:\n";
240
+ feedback += " • Add TypeScript types for better IntelliSense\n";
241
+ feedback += " • Use Zod schemas for runtime validation\n";
242
+ feedback += " • Keep functions focused and well-documented\n";
243
+
244
+ return feedback;
245
+ }
246
+
247
+ /**
248
+ * Validate Zod schema attachment
249
+ * @param {Object} moduleExports - Exported module
250
+ * @param {Array} functionNames - Array of function names
251
+ * @param {string} filePath - File path for context
252
+ * @returns {Array<string>} - Array of validation suggestions
253
+ */
254
+ export function validateSchemaAttachment(moduleExports, functionNames, filePath) {
255
+ const suggestions = [];
256
+
257
+ functionNames.forEach((funcName) => {
258
+ const func = moduleExports[funcName];
259
+ if (func && typeof func === "function") {
260
+ if (!func.schema) {
261
+ suggestions.push(
262
+ createDevelopmentWarning("Missing Validation Schema", `Function '${funcName}' has no attached Zod schema`, {
263
+ filePath,
264
+ suggestion: `Add: ${funcName}.schema = z.object({ /* your schema */ });`,
265
+ }),
266
+ );
267
+ }
268
+ }
269
+ });
270
+
271
+ return suggestions;
272
+ }
@@ -0,0 +1,283 @@
1
+ /**
2
+ * Enhanced error handling with context and suggestions
3
+ * Provides developer-friendly error messages with actionable suggestions
4
+ */
5
+
6
+ /**
7
+ * Create an enhanced error message with context and suggestions
8
+ * @param {string} errorType - Type of error
9
+ * @param {string} originalMessage - Original error message
10
+ * @param {Object} context - Error context information
11
+ * @returns {string}
12
+ */
13
+ export function createEnhancedError(errorType, originalMessage, context = {}) {
14
+ const { filePath, functionName, availableFunctions, suggestion } = context;
15
+
16
+ let enhancedMessage = `[Vite Server Actions] ${errorType}: ${originalMessage}`;
17
+
18
+ if (filePath) {
19
+ enhancedMessage += `\n 📁 File: ${filePath}`;
20
+ }
21
+
22
+ if (functionName) {
23
+ enhancedMessage += `\n 🔧 Function: ${functionName}`;
24
+ }
25
+
26
+ if (availableFunctions && availableFunctions.length > 0) {
27
+ enhancedMessage += `\n 📋 Available functions: ${availableFunctions.join(", ")}`;
28
+ }
29
+
30
+ if (suggestion) {
31
+ enhancedMessage += `\n 💡 Suggestion: ${suggestion}`;
32
+ }
33
+
34
+ return enhancedMessage;
35
+ }
36
+
37
+ /**
38
+ * Enhance function not found errors
39
+ * @param {string} functionName - The function that wasn't found
40
+ * @param {string} moduleName - Module where function was expected
41
+ * @param {Array} availableFunctions - List of available functions
42
+ * @returns {Object}
43
+ */
44
+ export function enhanceFunctionNotFoundError(functionName, moduleName, availableFunctions = []) {
45
+ const suggestions = [];
46
+
47
+ // Check for similar function names (typos)
48
+ const similarFunctions = availableFunctions.filter((fn) => levenshteinDistance(fn, functionName) <= 2);
49
+
50
+ if (similarFunctions.length > 0) {
51
+ suggestions.push(`Did you mean: ${similarFunctions.join(", ")}?`);
52
+ }
53
+
54
+ // Check for common naming patterns
55
+ const namingPatterns = [
56
+ { pattern: /^get/, suggestion: "For data fetching, consider: fetch, load, or retrieve" },
57
+ { pattern: /^create/, suggestion: "For creation, consider: add, insert, or save" },
58
+ { pattern: /^update/, suggestion: "For updates, consider: edit, modify, or change" },
59
+ { pattern: /^delete/, suggestion: "For deletion, consider: remove, destroy, or clear" },
60
+ ];
61
+
62
+ const matchingPattern = namingPatterns.find((p) => p.pattern.test(functionName));
63
+ if (matchingPattern) {
64
+ suggestions.push(matchingPattern.suggestion);
65
+ }
66
+
67
+ if (availableFunctions.length === 0) {
68
+ suggestions.push("No functions are exported from this module. Make sure to export your functions.");
69
+ }
70
+
71
+ return {
72
+ message: createEnhancedError(
73
+ "Function Not Found",
74
+ `Function '${functionName}' not found in module '${moduleName}'`,
75
+ {
76
+ functionName,
77
+ availableFunctions,
78
+ suggestion: suggestions.join(" "),
79
+ },
80
+ ),
81
+ code: "FUNCTION_NOT_FOUND",
82
+ suggestions,
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Enhance AST parsing errors
88
+ * @param {string} filePath - File that failed to parse
89
+ * @param {Error} originalError - Original parsing error
90
+ * @returns {Object}
91
+ */
92
+ export function enhanceParsingError(filePath, originalError) {
93
+ const suggestions = [];
94
+
95
+ if (originalError.message.includes("Unexpected token")) {
96
+ suggestions.push("Check for syntax errors in your server action file");
97
+ suggestions.push("Ensure all functions are properly exported");
98
+ }
99
+
100
+ if (originalError.message.includes("Identifier")) {
101
+ suggestions.push("Function names must be valid JavaScript identifiers");
102
+ suggestions.push("Function names cannot start with numbers or contain special characters");
103
+ }
104
+
105
+ if (originalError.message.includes("duplicate")) {
106
+ suggestions.push("Each function name must be unique within the same file");
107
+ suggestions.push("Consider renaming duplicate functions or using different export patterns");
108
+ }
109
+
110
+ return {
111
+ message: createEnhancedError("Parsing Error", `Failed to parse server action file: ${originalError.message}`, {
112
+ filePath,
113
+ suggestion: suggestions.join(" "),
114
+ }),
115
+ code: "PARSE_ERROR",
116
+ suggestions,
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Enhance validation errors
122
+ * @param {Array} validationErrors - Array of validation errors
123
+ * @param {string} functionName - Function that failed validation
124
+ * @returns {Object}
125
+ */
126
+ export function enhanceValidationError(validationErrors, functionName) {
127
+ const suggestions = [];
128
+
129
+ // Analyze common validation patterns
130
+ const hasTypeErrors = validationErrors.some(
131
+ (err) => err.message.includes("Expected") || err.message.includes("Invalid"),
132
+ );
133
+
134
+ if (hasTypeErrors) {
135
+ suggestions.push("Check the types of arguments you're passing to the function");
136
+ }
137
+
138
+ const hasRequiredErrors = validationErrors.some(
139
+ (err) => err.message.includes("required") || err.message.includes("missing"),
140
+ );
141
+
142
+ if (hasRequiredErrors) {
143
+ suggestions.push("Make sure all required parameters are provided");
144
+ }
145
+
146
+ const hasFormatErrors = validationErrors.some(
147
+ (err) => err.message.includes("format") || err.message.includes("pattern"),
148
+ );
149
+
150
+ if (hasFormatErrors) {
151
+ suggestions.push("Check the format of string inputs (email, URL, etc.)");
152
+ }
153
+
154
+ return {
155
+ message: createEnhancedError("Validation Error", `Validation failed for function '${functionName}'`, {
156
+ functionName,
157
+ suggestion: suggestions.join(" "),
158
+ }),
159
+ code: "VALIDATION_ERROR",
160
+ suggestions,
161
+ };
162
+ }
163
+
164
+ /**
165
+ * Enhance module loading errors
166
+ * @param {string} modulePath - Path to module that failed to load
167
+ * @param {Error} originalError - Original loading error
168
+ * @returns {Object}
169
+ */
170
+ export function enhanceModuleLoadError(modulePath, originalError) {
171
+ const suggestions = [];
172
+
173
+ if (originalError.code === "ENOENT") {
174
+ suggestions.push("Make sure the file exists and the path is correct");
175
+ suggestions.push("Check that your build process hasn't moved or renamed the file");
176
+ }
177
+
178
+ if (originalError.message.includes("import")) {
179
+ suggestions.push("Verify all import statements in your server action file");
180
+ suggestions.push("Make sure imported modules are installed and available");
181
+ }
182
+
183
+ if (originalError.message.includes("export")) {
184
+ suggestions.push("Ensure your functions are properly exported");
185
+ suggestions.push("Use 'export function' or 'export const' for your server actions");
186
+ }
187
+
188
+ return {
189
+ message: createEnhancedError("Module Load Error", `Failed to load server action module: ${originalError.message}`, {
190
+ filePath: modulePath,
191
+ suggestion: suggestions.join(" "),
192
+ }),
193
+ code: "MODULE_LOAD_ERROR",
194
+ suggestions,
195
+ };
196
+ }
197
+
198
+ /**
199
+ * Enhance development warnings with helpful context
200
+ * @param {string} warningType - Type of warning
201
+ * @param {string} message - Warning message
202
+ * @param {Object} context - Additional context
203
+ * @returns {string}
204
+ */
205
+ export function createDevelopmentWarning(warningType, message, context = {}) {
206
+ let warning = `[Vite Server Actions] ⚠️ ${warningType}: ${message}`;
207
+
208
+ if (context.filePath) {
209
+ warning += `\n 📁 File: ${context.filePath}`;
210
+ }
211
+
212
+ if (context.suggestion) {
213
+ warning += `\n 💡 Tip: ${context.suggestion}`;
214
+ }
215
+
216
+ return warning;
217
+ }
218
+
219
+ /**
220
+ * Calculate Levenshtein distance between two strings
221
+ * @param {string} a - First string
222
+ * @param {string} b - Second string
223
+ * @returns {number}
224
+ */
225
+ function levenshteinDistance(a, b) {
226
+ const matrix = [];
227
+
228
+ for (let i = 0; i <= b.length; i++) {
229
+ matrix[i] = [i];
230
+ }
231
+
232
+ for (let j = 0; j <= a.length; j++) {
233
+ matrix[0][j] = j;
234
+ }
235
+
236
+ for (let i = 1; i <= b.length; i++) {
237
+ for (let j = 1; j <= a.length; j++) {
238
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
239
+ matrix[i][j] = matrix[i - 1][j - 1];
240
+ } else {
241
+ matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
242
+ }
243
+ }
244
+ }
245
+
246
+ return matrix[b.length][a.length];
247
+ }
248
+
249
+ /**
250
+ * Generate helpful suggestions based on common mistakes
251
+ * @param {string} errorContext - Context where error occurred
252
+ * @param {Object} additionalInfo - Additional information about the error
253
+ * @returns {Array<string>}
254
+ */
255
+ export function generateHelpfulSuggestions(errorContext, additionalInfo = {}) {
256
+ const suggestions = [];
257
+
258
+ switch (errorContext) {
259
+ case "no-functions-found":
260
+ suggestions.push("Make sure your functions are exported: export async function myFunction() {}");
261
+ suggestions.push("Check that your file ends with .server.js or .server.ts");
262
+ suggestions.push("Verify the file is in a location matched by your include patterns");
263
+ break;
264
+
265
+ case "async-function-required":
266
+ suggestions.push("Server actions should be async functions");
267
+ suggestions.push("Change 'export function' to 'export async function'");
268
+ break;
269
+
270
+ case "invalid-arguments":
271
+ suggestions.push("All function arguments must be JSON-serializable");
272
+ suggestions.push("Functions, classes, and other complex objects cannot be passed");
273
+ suggestions.push("Consider passing plain objects, arrays, strings, and numbers only");
274
+ break;
275
+
276
+ case "type-safety":
277
+ suggestions.push("Add TypeScript types to your server actions for better development experience");
278
+ suggestions.push("Use Zod schemas for runtime validation");
279
+ break;
280
+ }
281
+
282
+ return suggestions;
283
+ }