vite-plugin-server-actions 1.0.0 → 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,535 @@
1
+ import { parse } from "@babel/parser";
2
+ import traverse from "@babel/traverse";
3
+
4
+ /**
5
+ * Extract exported functions from JavaScript/TypeScript code using AST parsing
6
+ * @param {string} code - The source code to parse
7
+ * @param {string} filename - The filename (for better error messages)
8
+ * @returns {Array<{name: string, isAsync: boolean, isDefault: boolean, type: string, params: Array, returnType: string|null, jsdoc: string|null}>}
9
+ */
10
+ export function extractExportedFunctions(code, filename = "unknown") {
11
+ const functions = [];
12
+
13
+ try {
14
+ // Parse the code into an AST
15
+ const ast = parse(code, {
16
+ sourceType: "module",
17
+ plugins: [
18
+ "typescript",
19
+ "jsx",
20
+ "decorators-legacy",
21
+ "dynamicImport",
22
+ "exportDefaultFrom",
23
+ "exportNamespaceFrom",
24
+ "topLevelAwait",
25
+ "classProperties",
26
+ "classPrivateProperties",
27
+ "classPrivateMethods",
28
+ ],
29
+ });
30
+
31
+ // Traverse the AST to find exported functions
32
+ const traverseFn = traverse.default || traverse;
33
+ traverseFn(ast, {
34
+ // Handle: export function name() {} or export async function name() {}
35
+ ExportNamedDeclaration(path) {
36
+ const declaration = path.node.declaration;
37
+
38
+ if (declaration && declaration.type === "FunctionDeclaration") {
39
+ if (declaration.id) {
40
+ functions.push({
41
+ name: declaration.id.name,
42
+ isAsync: declaration.async || false,
43
+ isDefault: false,
44
+ type: "function",
45
+ params: extractDetailedParams(declaration.params),
46
+ returnType: extractTypeAnnotation(declaration.returnType),
47
+ jsdoc: extractJSDoc(path.node.leadingComments),
48
+ });
49
+ }
50
+ }
51
+
52
+ // Handle: export const name = () => {} or export const name = async () => {}
53
+ if (declaration && declaration.type === "VariableDeclaration") {
54
+ declaration.declarations.forEach((decl) => {
55
+ if (
56
+ decl.init &&
57
+ (decl.init.type === "ArrowFunctionExpression" || decl.init.type === "FunctionExpression")
58
+ ) {
59
+ functions.push({
60
+ name: decl.id.name,
61
+ isAsync: decl.init.async || false,
62
+ isDefault: false,
63
+ type: "arrow",
64
+ params: extractDetailedParams(decl.init.params),
65
+ returnType: extractTypeAnnotation(decl.init.returnType),
66
+ jsdoc: extractJSDoc(declaration.leadingComments),
67
+ });
68
+ }
69
+ });
70
+ }
71
+ },
72
+
73
+ // Handle: export default function() {} or export default async function name() {}
74
+ ExportDefaultDeclaration(path) {
75
+ const declaration = path.node.declaration;
76
+
77
+ if (declaration.type === "FunctionDeclaration") {
78
+ functions.push({
79
+ name: declaration.id ? declaration.id.name : "default",
80
+ isAsync: declaration.async || false,
81
+ isDefault: true,
82
+ type: "function",
83
+ params: extractDetailedParams(declaration.params),
84
+ returnType: extractTypeAnnotation(declaration.returnType),
85
+ jsdoc: extractJSDoc(path.node.leadingComments),
86
+ });
87
+ }
88
+
89
+ // Handle: export default () => {} or export default async () => {}
90
+ if (declaration.type === "ArrowFunctionExpression" || declaration.type === "FunctionExpression") {
91
+ functions.push({
92
+ name: "default",
93
+ isAsync: declaration.async || false,
94
+ isDefault: true,
95
+ type: "arrow",
96
+ params: extractDetailedParams(declaration.params),
97
+ returnType: extractTypeAnnotation(declaration.returnType),
98
+ jsdoc: extractJSDoc(path.node.leadingComments),
99
+ });
100
+ }
101
+ },
102
+
103
+ // Handle: export { functionName } or export { internalName as publicName }
104
+ ExportSpecifier(path) {
105
+ // We need to track these and match them with function declarations
106
+ const localName = path.node.local.name;
107
+ const exportedName = path.node.exported.name;
108
+
109
+ // Look for the function in the module scope
110
+ const binding = path.scope.getBinding(localName);
111
+ if (binding && binding.path.isFunctionDeclaration()) {
112
+ functions.push({
113
+ name: exportedName,
114
+ isAsync: binding.path.node.async || false,
115
+ isDefault: false,
116
+ type: "renamed",
117
+ params: extractDetailedParams(binding.path.node.params),
118
+ returnType: extractTypeAnnotation(binding.path.node.returnType),
119
+ jsdoc: extractJSDoc(binding.path.node.leadingComments),
120
+ });
121
+ }
122
+
123
+ // Check if it's a variable with arrow function
124
+ if (binding && binding.path.isVariableDeclarator()) {
125
+ const init = binding.path.node.init;
126
+ if (init && (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression")) {
127
+ functions.push({
128
+ name: exportedName,
129
+ isAsync: init.async || false,
130
+ isDefault: false,
131
+ type: "renamed-arrow",
132
+ params: extractDetailedParams(init.params),
133
+ returnType: extractTypeAnnotation(init.returnType),
134
+ jsdoc: extractJSDoc(binding.path.node.leadingComments),
135
+ });
136
+ }
137
+ }
138
+ },
139
+ });
140
+ } catch (error) {
141
+ console.error(`Failed to parse ${filename}: ${error.message}`);
142
+ // Return empty array on parse error rather than throwing
143
+ return [];
144
+ }
145
+
146
+ // Remove duplicates and return
147
+ const uniqueFunctions = Array.from(new Map(functions.map((fn) => [fn.name, fn])).values());
148
+
149
+ return uniqueFunctions;
150
+ }
151
+
152
+ /**
153
+ * Validate if a function name is valid JavaScript identifier
154
+ * @param {string} name - The function name to validate
155
+ * @returns {boolean}
156
+ */
157
+ export function isValidFunctionName(name) {
158
+ // Check if it's a valid JavaScript identifier
159
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
160
+ }
161
+
162
+ /**
163
+ * Extract detailed parameter information from function parameters
164
+ * @param {Array} params - Array of parameter AST nodes
165
+ * @returns {Array<{name: string, type: string|null, defaultValue: string|null, isOptional: boolean, isRest: boolean}>}
166
+ */
167
+ export function extractDetailedParams(params) {
168
+ if (!params) return [];
169
+
170
+ return params.map((param) => {
171
+ const paramInfo = {
172
+ name: "",
173
+ type: null,
174
+ defaultValue: null,
175
+ isOptional: false,
176
+ isRest: false,
177
+ };
178
+
179
+ if (param.type === "Identifier") {
180
+ paramInfo.name = param.name;
181
+ paramInfo.type = extractTypeAnnotation(param.typeAnnotation);
182
+ paramInfo.isOptional = param.optional || false;
183
+ } else if (param.type === "AssignmentPattern") {
184
+ // Handle default parameters: function(name = 'default')
185
+ paramInfo.name = param.left.name;
186
+ paramInfo.type = extractTypeAnnotation(param.left.typeAnnotation);
187
+ paramInfo.defaultValue = generateCode(param.right);
188
+ paramInfo.isOptional = true;
189
+ } else if (param.type === "RestElement") {
190
+ // Handle rest parameters: function(...args)
191
+ paramInfo.name = `...${param.argument.name}`;
192
+ paramInfo.type = extractTypeAnnotation(param.typeAnnotation);
193
+ paramInfo.isRest = true;
194
+ } else if (param.type === "ObjectPattern") {
195
+ // Handle destructuring: function({name, age})
196
+ paramInfo.name = generateCode(param);
197
+ paramInfo.type = extractTypeAnnotation(param.typeAnnotation);
198
+ paramInfo.isOptional = param.optional || false;
199
+ } else if (param.type === "ArrayPattern") {
200
+ // Handle array destructuring: function([first, second])
201
+ paramInfo.name = generateCode(param);
202
+ paramInfo.type = extractTypeAnnotation(param.typeAnnotation);
203
+ paramInfo.isOptional = param.optional || false;
204
+ }
205
+
206
+ return paramInfo;
207
+ });
208
+ }
209
+
210
+ /**
211
+ * Extract type annotation as string
212
+ * @param {object} typeAnnotation - Type annotation AST node
213
+ * @returns {string|null}
214
+ */
215
+ export function extractTypeAnnotation(typeAnnotation) {
216
+ if (!typeAnnotation || !typeAnnotation.typeAnnotation) return null;
217
+
218
+ return generateCode(typeAnnotation.typeAnnotation);
219
+ }
220
+
221
+ /**
222
+ * Extract JSDoc comments
223
+ * @param {Array} comments - Array of comment nodes
224
+ * @returns {string|null}
225
+ */
226
+ export function extractJSDoc(comments) {
227
+ if (!comments) return null;
228
+
229
+ const jsdocComment = comments.find((comment) => comment.type === "CommentBlock" && comment.value.startsWith("*"));
230
+
231
+ return jsdocComment ? `/*${jsdocComment.value}*/` : null;
232
+ }
233
+
234
+ /**
235
+ * Generate code string from AST node (simplified)
236
+ * @param {object} node - AST node
237
+ * @returns {string}
238
+ */
239
+ function generateCode(node) {
240
+ if (!node) return "";
241
+
242
+ try {
243
+ // Simple code generation for common cases
244
+ switch (node.type) {
245
+ case "Identifier":
246
+ return node.name;
247
+ case "StringLiteral":
248
+ return `"${node.value}"`;
249
+ case "NumericLiteral":
250
+ return String(node.value);
251
+ case "BooleanLiteral":
252
+ return String(node.value);
253
+ case "NullLiteral":
254
+ return "null";
255
+ case "TSStringKeyword":
256
+ return "string";
257
+ case "TSNumberKeyword":
258
+ return "number";
259
+ case "TSBooleanKeyword":
260
+ return "boolean";
261
+ case "TSAnyKeyword":
262
+ return "any";
263
+ case "TSUnknownKeyword":
264
+ return "unknown";
265
+ case "TSVoidKeyword":
266
+ return "void";
267
+ case "TSArrayType":
268
+ return `${generateCode(node.elementType)}[]`;
269
+ case "TSUnionType":
270
+ return node.types.map((type) => generateCode(type)).join(" | ");
271
+ case "TSLiteralType":
272
+ return generateCode(node.literal);
273
+ case "ObjectPattern":
274
+ const props = node.properties
275
+ .map((prop) => {
276
+ if (prop.type === "ObjectProperty") {
277
+ return prop.key.name;
278
+ } else if (prop.type === "RestElement") {
279
+ return `...${prop.argument.name}`;
280
+ }
281
+ return "";
282
+ })
283
+ .filter(Boolean);
284
+ return `{${props.join(", ")}}`;
285
+ case "ArrayPattern":
286
+ const elements = node.elements.map((elem, i) =>
287
+ elem ? (elem.type === "Identifier" ? elem.name : `_${i}`) : `_${i}`,
288
+ );
289
+ return `[${elements.join(", ")}]`;
290
+ case "TSTypeReference":
291
+ // Handle type references like Todo, CreateTodoInput, etc.
292
+ if (node.typeName) {
293
+ let typeName = "";
294
+
295
+ // Handle qualified names like z.infer
296
+ if (node.typeName.type === "TSQualifiedName") {
297
+ typeName = generateCode(node.typeName);
298
+ } else if (node.typeName.type === "Identifier") {
299
+ typeName = node.typeName.name;
300
+ } else {
301
+ return "unknown";
302
+ }
303
+
304
+ // Handle generic types like Promise<T>, Array<T>
305
+ if (node.typeParameters && node.typeParameters.params && node.typeParameters.params.length > 0) {
306
+ const typeArgs = node.typeParameters.params.map((param) => generateCode(param)).join(", ");
307
+ return `${typeName}<${typeArgs}>`;
308
+ }
309
+ return typeName;
310
+ }
311
+ return "unknown";
312
+ case "TSTypeLiteral":
313
+ // Handle object type literals
314
+ if (node.members && node.members.length > 0) {
315
+ const members = node.members
316
+ .map((member) => {
317
+ if (member.type === "TSPropertySignature" && member.key) {
318
+ const key = member.key.type === "Identifier" ? member.key.name : "unknown";
319
+ const type = member.typeAnnotation ? generateCode(member.typeAnnotation.typeAnnotation) : "any";
320
+ const optional = member.optional ? "?" : "";
321
+ return `${key}${optional}: ${type}`;
322
+ }
323
+ return "";
324
+ })
325
+ .filter(Boolean);
326
+ return `{ ${members.join("; ")} }`;
327
+ }
328
+ return "{}";
329
+ case "TSInterfaceDeclaration":
330
+ // Handle interface declarations
331
+ return node.id ? node.id.name : "unknown";
332
+ case "TSNullKeyword":
333
+ return "null";
334
+ case "TSUndefinedKeyword":
335
+ return "undefined";
336
+ case "TSFunctionType":
337
+ // Handle function type signatures
338
+ const funcParams = node.parameters
339
+ .map((param) => {
340
+ let paramStr = "";
341
+ const paramName = param.name ? param.name : "_";
342
+ const paramType = param.typeAnnotation ? generateCode(param.typeAnnotation.typeAnnotation) : "any";
343
+
344
+ // Handle rest parameters
345
+ if (param.type === "RestElement") {
346
+ paramStr = `...${param.argument.name}: ${paramType}`;
347
+ } else {
348
+ paramStr = `${paramName}`;
349
+ // Handle optional parameters
350
+ if (param.optional) {
351
+ paramStr += "?";
352
+ }
353
+ paramStr += `: ${paramType}`;
354
+ }
355
+
356
+ return paramStr;
357
+ })
358
+ .join(", ");
359
+ const funcReturn = node.typeAnnotation ? generateCode(node.typeAnnotation.typeAnnotation) : "void";
360
+ return `(${funcParams}) => ${funcReturn}`;
361
+ case "TSIntersectionType":
362
+ // Handle intersection types: A & B & C
363
+ return node.types.map((type) => generateCode(type)).join(" & ");
364
+ case "TSTupleType":
365
+ // Handle tuple types: [string, number, boolean]
366
+ const tupleElements = node.elementTypes.map((elem) => generateCode(elem)).join(", ");
367
+ return `[${tupleElements}]`;
368
+ case "TSIndexSignature":
369
+ // Handle index signatures: { [key: string]: any }
370
+ if (node.parameters && node.parameters.length > 0) {
371
+ const param = node.parameters[0];
372
+ const keyName = param.name || "key";
373
+ const keyType = param.typeAnnotation ? generateCode(param.typeAnnotation.typeAnnotation) : "string";
374
+ const valueType = node.typeAnnotation ? generateCode(node.typeAnnotation.typeAnnotation) : "any";
375
+ return `[${keyName}: ${keyType}]: ${valueType}`;
376
+ }
377
+ return "[key: string]: any";
378
+ case "TSBigIntKeyword":
379
+ return "bigint";
380
+ case "TSSymbolKeyword":
381
+ return "symbol";
382
+ case "TSNeverKeyword":
383
+ return "never";
384
+ case "TSThisType":
385
+ return "this";
386
+ case "TSTemplateLiteralType":
387
+ // Handle template literal types
388
+ if (node.quasis && node.types) {
389
+ let result = "";
390
+ for (let i = 0; i < node.quasis.length; i++) {
391
+ result += node.quasis[i].value.raw;
392
+ if (i < node.types.length) {
393
+ result += "${" + generateCode(node.types[i]) + "}";
394
+ }
395
+ }
396
+ return "`" + result + "`";
397
+ }
398
+ return "`${string}`";
399
+ case "TemplateLiteral":
400
+ // Handle template literals (non-type version)
401
+ if (node.quasis && node.expressions) {
402
+ let result = "";
403
+ for (let i = 0; i < node.quasis.length; i++) {
404
+ result += node.quasis[i].value.raw;
405
+ if (i < node.expressions.length) {
406
+ result += "${" + generateCode(node.expressions[i]) + "}";
407
+ }
408
+ }
409
+ return "`" + result + "`";
410
+ }
411
+ return "`${string}`";
412
+ case "TSConditionalType":
413
+ // Handle conditional types: T extends U ? X : Y
414
+ const checkType = generateCode(node.checkType);
415
+ const extendsType = generateCode(node.extendsType);
416
+ const trueType = generateCode(node.trueType);
417
+ const falseType = generateCode(node.falseType);
418
+ return `${checkType} extends ${extendsType} ? ${trueType} : ${falseType}`;
419
+ case "TSTypeOperator":
420
+ // Handle type operators like readonly, keyof
421
+ const operator = node.operator;
422
+ const typeArg = generateCode(node.typeAnnotation);
423
+ return `${operator} ${typeArg}`;
424
+ case "TSIndexedAccessType":
425
+ // Handle indexed access types: T[K]
426
+ const objectType = generateCode(node.objectType);
427
+ const indexType = generateCode(node.indexType);
428
+ return `${objectType}[${indexType}]`;
429
+ case "TSMappedType":
430
+ // Handle mapped types: { [K in T]: U }
431
+ let mapped = "{";
432
+ if (node.readonly) {
433
+ mapped += node.readonly === "+" ? "readonly " : "-readonly ";
434
+ }
435
+ mapped += "[";
436
+ if (node.typeParameter) {
437
+ mapped += node.typeParameter.name;
438
+ if (node.typeParameter.constraint) {
439
+ mapped += " in " + generateCode(node.typeParameter.constraint);
440
+ }
441
+ }
442
+ mapped += "]";
443
+ if (node.optional) {
444
+ mapped += node.optional === "+" ? "?" : "-?";
445
+ }
446
+ mapped += ": ";
447
+ if (node.typeAnnotation) {
448
+ mapped += generateCode(node.typeAnnotation);
449
+ }
450
+ mapped += "}";
451
+ return mapped;
452
+ case "TSTypePredicate":
453
+ // Handle type predicates: value is Type
454
+ const paramName = node.parameterName ? node.parameterName.name : "value";
455
+ const predicateType = node.typeAnnotation ? generateCode(node.typeAnnotation.typeAnnotation) : "unknown";
456
+ return `${paramName} is ${predicateType}`;
457
+ case "TSParenthesizedType":
458
+ // Handle parenthesized types: (string | number)
459
+ return `(${generateCode(node.typeAnnotation)})`;
460
+ case "TSTypeQuery":
461
+ // Handle typeof operator: typeof someValue
462
+ const exprName = node.exprName;
463
+ if (exprName.type === "Identifier") {
464
+ return `typeof ${exprName.name}`;
465
+ }
466
+ return "typeof unknown";
467
+ case "TSQualifiedName":
468
+ // Handle qualified names like A.B.C
469
+ if (node.left.type === "TSQualifiedName") {
470
+ return generateCode(node.left) + "." + node.right.name;
471
+ } else if (node.left.type === "Identifier") {
472
+ return node.left.name + "." + node.right.name;
473
+ }
474
+ return "unknown";
475
+ case "TSOptionalType":
476
+ // Handle optional types in function parameters
477
+ return generateCode(node.typeAnnotation);
478
+ case "TSRestType":
479
+ // Handle rest types: ...Type[]
480
+ return "..." + generateCode(node.typeAnnotation);
481
+ case "TSNamedTupleMember":
482
+ // Handle named tuple members
483
+ let namedTuple = "";
484
+ if (node.label) {
485
+ namedTuple += node.label.name + ": ";
486
+ }
487
+ namedTuple += generateCode(node.elementType);
488
+ if (node.optional) {
489
+ namedTuple += "?";
490
+ }
491
+ return namedTuple;
492
+ case "TSInferType":
493
+ // Handle infer types: infer T
494
+ return `infer ${node.typeParameter.name}`;
495
+ case "TSImportType":
496
+ // Handle import types: import("module").Type
497
+ let importStr = `import("${node.argument.value}")`;
498
+ if (node.qualifier) {
499
+ // Handle nested qualifiers like import("./types").users.Admin
500
+ if (node.qualifier.type === "TSQualifiedName") {
501
+ // Recursively build the qualified name
502
+ const buildQualifiedName = (qName) => {
503
+ if (qName.left.type === "TSQualifiedName") {
504
+ return buildQualifiedName(qName.left) + "." + qName.right.name;
505
+ } else {
506
+ return qName.left.name + "." + qName.right.name;
507
+ }
508
+ };
509
+ importStr += "." + buildQualifiedName(node.qualifier);
510
+ } else {
511
+ importStr += "." + node.qualifier.name;
512
+ }
513
+ }
514
+ if (node.typeParameters) {
515
+ const typeArgs = node.typeParameters.params.map((param) => generateCode(param)).join(", ");
516
+ importStr += `<${typeArgs}>`;
517
+ }
518
+ return importStr;
519
+ default:
520
+ // Fallback for complex types
521
+ return node.type || "unknown";
522
+ }
523
+ } catch (error) {
524
+ return "unknown";
525
+ }
526
+ }
527
+
528
+ /**
529
+ * Extract function parameter names from AST (legacy compatibility)
530
+ * @param {object} functionNode - The function AST node
531
+ * @returns {Array<string>}
532
+ */
533
+ export function extractFunctionParams(functionNode) {
534
+ return extractDetailedParams(functionNode.params).map((param) => param.name);
535
+ }
@@ -1,5 +1,6 @@
1
1
  import { createRequire } from "module";
2
2
  import { pathToFileURL } from "url";
3
+ import fs from "fs/promises";
3
4
 
4
5
  /**
5
6
  * Extract schemas from server modules during build time
@@ -37,27 +38,23 @@ export async function extractSchemas(serverFunctions) {
37
38
  /**
38
39
  * Generate validation setup code for production
39
40
  */
40
- export function generateValidationCode(options, serverFunctions) {
41
+ export async function generateValidationCode(options, serverFunctions) {
41
42
  if (!options.validation?.enabled) {
42
43
  return {
43
44
  imports: "",
44
45
  setup: "",
45
46
  middlewareFactory: "",
47
+ validationRuntime: "",
46
48
  };
47
49
  }
48
50
 
49
- // Generate imports
50
- const imports = `
51
- import { createValidationMiddleware, SchemaDiscovery } from '../../../src/validation.js';
51
+ // Read the validation runtime code that will be embedded
52
+ const validationRuntimePath = new URL("./validation-runtime.js", import.meta.url);
53
+ const validationRuntime = `
54
+ // Embedded validation runtime
55
+ ${await fs.readFile(validationRuntimePath, "utf-8")}
52
56
  `;
53
57
 
54
- // Generate schema imports from the bundled actions
55
- const schemaImports = Array.from(serverFunctions.entries())
56
- .map(([moduleName, { functions }]) => {
57
- return functions.map((fn) => `// Import schema for ${moduleName}.${fn} if it exists`).join("\n");
58
- })
59
- .join("\n");
60
-
61
58
  // Generate setup code
62
59
  const setup = `
63
60
  // Setup validation
@@ -66,17 +63,17 @@ const validationMiddleware = createValidationMiddleware({ schemaDiscovery });
66
63
 
67
64
  // Register schemas from server actions
68
65
  ${Array.from(serverFunctions.entries())
69
- .map(([moduleName, { functions }]) => {
70
- return functions
71
- .map(
72
- (fn) => `
66
+ .map(([moduleName, { functions }]) => {
67
+ return functions
68
+ .map(
69
+ (fn) => `
73
70
  if (serverActions.${moduleName}.${fn}.schema) {
74
71
  schemaDiscovery.registerSchema('${moduleName}', '${fn}', serverActions.${moduleName}.${fn}.schema);
75
72
  }`,
76
- )
77
- .join("\n");
78
- })
79
- .join("\n")}
73
+ )
74
+ .join("\n");
75
+ })
76
+ .join("\n")}
80
77
  `;
81
78
 
82
79
  // Generate middleware factory
@@ -94,8 +91,9 @@ function createContextualValidationMiddleware(moduleName, functionName) {
94
91
  `;
95
92
 
96
93
  return {
97
- imports: imports + schemaImports,
94
+ imports: "",
98
95
  setup,
99
96
  middlewareFactory,
97
+ validationRuntime,
100
98
  };
101
99
  }