vite-plugin-server-actions
Version:
Server actions for Vite - call backend functions directly from your frontend with automatic API generation, TypeScript support, and zero configuration
536 lines (505 loc) • 18.1 kB
JavaScript
import { parse } from "@babel/parser";
import traverse from "@babel/traverse";
/**
* Extract exported functions from JavaScript/TypeScript code using AST parsing
* @param {string} code - The source code to parse
* @param {string} filename - The filename (for better error messages)
* @returns {Array<{name: string, isAsync: boolean, isDefault: boolean, type: string, params: Array, returnType: string|null, jsdoc: string|null}>}
*/
export function extractExportedFunctions(code, filename = "unknown") {
const functions = [];
try {
// Parse the code into an AST
const ast = parse(code, {
sourceType: "module",
plugins: [
"typescript",
"jsx",
"decorators-legacy",
"dynamicImport",
"exportDefaultFrom",
"exportNamespaceFrom",
"topLevelAwait",
"classProperties",
"classPrivateProperties",
"classPrivateMethods",
],
});
// Traverse the AST to find exported functions
const traverseFn = traverse.default || traverse;
traverseFn(ast, {
// Handle: export function name() {} or export async function name() {}
ExportNamedDeclaration(path) {
const declaration = path.node.declaration;
if (declaration && declaration.type === "FunctionDeclaration") {
if (declaration.id) {
functions.push({
name: declaration.id.name,
isAsync: declaration.async || false,
isDefault: false,
type: "function",
params: extractDetailedParams(declaration.params),
returnType: extractTypeAnnotation(declaration.returnType),
jsdoc: extractJSDoc(path.node.leadingComments),
});
}
}
// Handle: export const name = () => {} or export const name = async () => {}
if (declaration && declaration.type === "VariableDeclaration") {
declaration.declarations.forEach((decl) => {
if (
decl.init &&
(decl.init.type === "ArrowFunctionExpression" || decl.init.type === "FunctionExpression")
) {
functions.push({
name: decl.id.name,
isAsync: decl.init.async || false,
isDefault: false,
type: "arrow",
params: extractDetailedParams(decl.init.params),
returnType: extractTypeAnnotation(decl.init.returnType),
jsdoc: extractJSDoc(declaration.leadingComments),
});
}
});
}
},
// Handle: export default function() {} or export default async function name() {}
ExportDefaultDeclaration(path) {
const declaration = path.node.declaration;
if (declaration.type === "FunctionDeclaration") {
functions.push({
name: declaration.id ? declaration.id.name : "default",
isAsync: declaration.async || false,
isDefault: true,
type: "function",
params: extractDetailedParams(declaration.params),
returnType: extractTypeAnnotation(declaration.returnType),
jsdoc: extractJSDoc(path.node.leadingComments),
});
}
// Handle: export default () => {} or export default async () => {}
if (declaration.type === "ArrowFunctionExpression" || declaration.type === "FunctionExpression") {
functions.push({
name: "default",
isAsync: declaration.async || false,
isDefault: true,
type: "arrow",
params: extractDetailedParams(declaration.params),
returnType: extractTypeAnnotation(declaration.returnType),
jsdoc: extractJSDoc(path.node.leadingComments),
});
}
},
// Handle: export { functionName } or export { internalName as publicName }
ExportSpecifier(path) {
// We need to track these and match them with function declarations
const localName = path.node.local.name;
const exportedName = path.node.exported.name;
// Look for the function in the module scope
const binding = path.scope.getBinding(localName);
if (binding && binding.path.isFunctionDeclaration()) {
functions.push({
name: exportedName,
isAsync: binding.path.node.async || false,
isDefault: false,
type: "renamed",
params: extractDetailedParams(binding.path.node.params),
returnType: extractTypeAnnotation(binding.path.node.returnType),
jsdoc: extractJSDoc(binding.path.node.leadingComments),
});
}
// Check if it's a variable with arrow function
if (binding && binding.path.isVariableDeclarator()) {
const init = binding.path.node.init;
if (init && (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression")) {
functions.push({
name: exportedName,
isAsync: init.async || false,
isDefault: false,
type: "renamed-arrow",
params: extractDetailedParams(init.params),
returnType: extractTypeAnnotation(init.returnType),
jsdoc: extractJSDoc(binding.path.node.leadingComments),
});
}
}
},
});
} catch (error) {
console.error(`Failed to parse ${filename}: ${error.message}`);
// Return empty array on parse error rather than throwing
return [];
}
// Remove duplicates and return
const uniqueFunctions = Array.from(new Map(functions.map((fn) => [fn.name, fn])).values());
return uniqueFunctions;
}
/**
* Validate if a function name is valid JavaScript identifier
* @param {string} name - The function name to validate
* @returns {boolean}
*/
export function isValidFunctionName(name) {
// Check if it's a valid JavaScript identifier
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
}
/**
* Extract detailed parameter information from function parameters
* @param {Array} params - Array of parameter AST nodes
* @returns {Array<{name: string, type: string|null, defaultValue: string|null, isOptional: boolean, isRest: boolean}>}
*/
export function extractDetailedParams(params) {
if (!params) return [];
return params.map((param) => {
const paramInfo = {
name: "",
type: null,
defaultValue: null,
isOptional: false,
isRest: false,
};
if (param.type === "Identifier") {
paramInfo.name = param.name;
paramInfo.type = extractTypeAnnotation(param.typeAnnotation);
paramInfo.isOptional = param.optional || false;
} else if (param.type === "AssignmentPattern") {
// Handle default parameters: function(name = 'default')
paramInfo.name = param.left.name;
paramInfo.type = extractTypeAnnotation(param.left.typeAnnotation);
paramInfo.defaultValue = generateCode(param.right);
paramInfo.isOptional = true;
} else if (param.type === "RestElement") {
// Handle rest parameters: function(...args)
paramInfo.name = `...${param.argument.name}`;
paramInfo.type = extractTypeAnnotation(param.typeAnnotation);
paramInfo.isRest = true;
} else if (param.type === "ObjectPattern") {
// Handle destructuring: function({name, age})
paramInfo.name = generateCode(param);
paramInfo.type = extractTypeAnnotation(param.typeAnnotation);
paramInfo.isOptional = param.optional || false;
} else if (param.type === "ArrayPattern") {
// Handle array destructuring: function([first, second])
paramInfo.name = generateCode(param);
paramInfo.type = extractTypeAnnotation(param.typeAnnotation);
paramInfo.isOptional = param.optional || false;
}
return paramInfo;
});
}
/**
* Extract type annotation as string
* @param {object} typeAnnotation - Type annotation AST node
* @returns {string|null}
*/
export function extractTypeAnnotation(typeAnnotation) {
if (!typeAnnotation || !typeAnnotation.typeAnnotation) return null;
return generateCode(typeAnnotation.typeAnnotation);
}
/**
* Extract JSDoc comments
* @param {Array} comments - Array of comment nodes
* @returns {string|null}
*/
export function extractJSDoc(comments) {
if (!comments) return null;
const jsdocComment = comments.find((comment) => comment.type === "CommentBlock" && comment.value.startsWith("*"));
return jsdocComment ? `/*${jsdocComment.value}*/` : null;
}
/**
* Generate code string from AST node (simplified)
* @param {object} node - AST node
* @returns {string}
*/
function generateCode(node) {
if (!node) return "";
try {
// Simple code generation for common cases
switch (node.type) {
case "Identifier":
return node.name;
case "StringLiteral":
return `"${node.value}"`;
case "NumericLiteral":
return String(node.value);
case "BooleanLiteral":
return String(node.value);
case "NullLiteral":
return "null";
case "TSStringKeyword":
return "string";
case "TSNumberKeyword":
return "number";
case "TSBooleanKeyword":
return "boolean";
case "TSAnyKeyword":
return "any";
case "TSUnknownKeyword":
return "unknown";
case "TSVoidKeyword":
return "void";
case "TSArrayType":
return `${generateCode(node.elementType)}[]`;
case "TSUnionType":
return node.types.map((type) => generateCode(type)).join(" | ");
case "TSLiteralType":
return generateCode(node.literal);
case "ObjectPattern":
const props = node.properties
.map((prop) => {
if (prop.type === "ObjectProperty") {
return prop.key.name;
} else if (prop.type === "RestElement") {
return `...${prop.argument.name}`;
}
return "";
})
.filter(Boolean);
return `{${props.join(", ")}}`;
case "ArrayPattern":
const elements = node.elements.map((elem, i) =>
elem ? (elem.type === "Identifier" ? elem.name : `_${i}`) : `_${i}`,
);
return `[${elements.join(", ")}]`;
case "TSTypeReference":
// Handle type references like Todo, CreateTodoInput, etc.
if (node.typeName) {
let typeName = "";
// Handle qualified names like z.infer
if (node.typeName.type === "TSQualifiedName") {
typeName = generateCode(node.typeName);
} else if (node.typeName.type === "Identifier") {
typeName = node.typeName.name;
} else {
return "unknown";
}
// Handle generic types like Promise<T>, Array<T>
if (node.typeParameters && node.typeParameters.params && node.typeParameters.params.length > 0) {
const typeArgs = node.typeParameters.params.map((param) => generateCode(param)).join(", ");
return `${typeName}<${typeArgs}>`;
}
return typeName;
}
return "unknown";
case "TSTypeLiteral":
// Handle object type literals
if (node.members && node.members.length > 0) {
const members = node.members
.map((member) => {
if (member.type === "TSPropertySignature" && member.key) {
const key = member.key.type === "Identifier" ? member.key.name : "unknown";
const type = member.typeAnnotation ? generateCode(member.typeAnnotation.typeAnnotation) : "any";
const optional = member.optional ? "?" : "";
return `${key}${optional}: ${type}`;
}
return "";
})
.filter(Boolean);
return `{ ${members.join("; ")} }`;
}
return "{}";
case "TSInterfaceDeclaration":
// Handle interface declarations
return node.id ? node.id.name : "unknown";
case "TSNullKeyword":
return "null";
case "TSUndefinedKeyword":
return "undefined";
case "TSFunctionType":
// Handle function type signatures
const funcParams = node.parameters
.map((param) => {
let paramStr = "";
const paramName = param.name ? param.name : "_";
const paramType = param.typeAnnotation ? generateCode(param.typeAnnotation.typeAnnotation) : "any";
// Handle rest parameters
if (param.type === "RestElement") {
paramStr = `...${param.argument.name}: ${paramType}`;
} else {
paramStr = `${paramName}`;
// Handle optional parameters
if (param.optional) {
paramStr += "?";
}
paramStr += `: ${paramType}`;
}
return paramStr;
})
.join(", ");
const funcReturn = node.typeAnnotation ? generateCode(node.typeAnnotation.typeAnnotation) : "void";
return `(${funcParams}) => ${funcReturn}`;
case "TSIntersectionType":
// Handle intersection types: A & B & C
return node.types.map((type) => generateCode(type)).join(" & ");
case "TSTupleType":
// Handle tuple types: [string, number, boolean]
const tupleElements = node.elementTypes.map((elem) => generateCode(elem)).join(", ");
return `[${tupleElements}]`;
case "TSIndexSignature":
// Handle index signatures: { [key: string]: any }
if (node.parameters && node.parameters.length > 0) {
const param = node.parameters[0];
const keyName = param.name || "key";
const keyType = param.typeAnnotation ? generateCode(param.typeAnnotation.typeAnnotation) : "string";
const valueType = node.typeAnnotation ? generateCode(node.typeAnnotation.typeAnnotation) : "any";
return `[${keyName}: ${keyType}]: ${valueType}`;
}
return "[key: string]: any";
case "TSBigIntKeyword":
return "bigint";
case "TSSymbolKeyword":
return "symbol";
case "TSNeverKeyword":
return "never";
case "TSThisType":
return "this";
case "TSTemplateLiteralType":
// Handle template literal types
if (node.quasis && node.types) {
let result = "";
for (let i = 0; i < node.quasis.length; i++) {
result += node.quasis[i].value.raw;
if (i < node.types.length) {
result += "${" + generateCode(node.types[i]) + "}";
}
}
return "`" + result + "`";
}
return "`${string}`";
case "TemplateLiteral":
// Handle template literals (non-type version)
if (node.quasis && node.expressions) {
let result = "";
for (let i = 0; i < node.quasis.length; i++) {
result += node.quasis[i].value.raw;
if (i < node.expressions.length) {
result += "${" + generateCode(node.expressions[i]) + "}";
}
}
return "`" + result + "`";
}
return "`${string}`";
case "TSConditionalType":
// Handle conditional types: T extends U ? X : Y
const checkType = generateCode(node.checkType);
const extendsType = generateCode(node.extendsType);
const trueType = generateCode(node.trueType);
const falseType = generateCode(node.falseType);
return `${checkType} extends ${extendsType} ? ${trueType} : ${falseType}`;
case "TSTypeOperator":
// Handle type operators like readonly, keyof
const operator = node.operator;
const typeArg = generateCode(node.typeAnnotation);
return `${operator} ${typeArg}`;
case "TSIndexedAccessType":
// Handle indexed access types: T[K]
const objectType = generateCode(node.objectType);
const indexType = generateCode(node.indexType);
return `${objectType}[${indexType}]`;
case "TSMappedType":
// Handle mapped types: { [K in T]: U }
let mapped = "{";
if (node.readonly) {
mapped += node.readonly === "+" ? "readonly " : "-readonly ";
}
mapped += "[";
if (node.typeParameter) {
mapped += node.typeParameter.name;
if (node.typeParameter.constraint) {
mapped += " in " + generateCode(node.typeParameter.constraint);
}
}
mapped += "]";
if (node.optional) {
mapped += node.optional === "+" ? "?" : "-?";
}
mapped += ": ";
if (node.typeAnnotation) {
mapped += generateCode(node.typeAnnotation);
}
mapped += "}";
return mapped;
case "TSTypePredicate":
// Handle type predicates: value is Type
const paramName = node.parameterName ? node.parameterName.name : "value";
const predicateType = node.typeAnnotation ? generateCode(node.typeAnnotation.typeAnnotation) : "unknown";
return `${paramName} is ${predicateType}`;
case "TSParenthesizedType":
// Handle parenthesized types: (string | number)
return `(${generateCode(node.typeAnnotation)})`;
case "TSTypeQuery":
// Handle typeof operator: typeof someValue
const exprName = node.exprName;
if (exprName.type === "Identifier") {
return `typeof ${exprName.name}`;
}
return "typeof unknown";
case "TSQualifiedName":
// Handle qualified names like A.B.C
if (node.left.type === "TSQualifiedName") {
return generateCode(node.left) + "." + node.right.name;
} else if (node.left.type === "Identifier") {
return node.left.name + "." + node.right.name;
}
return "unknown";
case "TSOptionalType":
// Handle optional types in function parameters
return generateCode(node.typeAnnotation);
case "TSRestType":
// Handle rest types: ...Type[]
return "..." + generateCode(node.typeAnnotation);
case "TSNamedTupleMember":
// Handle named tuple members
let namedTuple = "";
if (node.label) {
namedTuple += node.label.name + ": ";
}
namedTuple += generateCode(node.elementType);
if (node.optional) {
namedTuple += "?";
}
return namedTuple;
case "TSInferType":
// Handle infer types: infer T
return `infer ${node.typeParameter.name}`;
case "TSImportType":
// Handle import types: import("module").Type
let importStr = `import("${node.argument.value}")`;
if (node.qualifier) {
// Handle nested qualifiers like import("./types").users.Admin
if (node.qualifier.type === "TSQualifiedName") {
// Recursively build the qualified name
const buildQualifiedName = (qName) => {
if (qName.left.type === "TSQualifiedName") {
return buildQualifiedName(qName.left) + "." + qName.right.name;
} else {
return qName.left.name + "." + qName.right.name;
}
};
importStr += "." + buildQualifiedName(node.qualifier);
} else {
importStr += "." + node.qualifier.name;
}
}
if (node.typeParameters) {
const typeArgs = node.typeParameters.params.map((param) => generateCode(param)).join(", ");
importStr += `<${typeArgs}>`;
}
return importStr;
default:
// Fallback for complex types
return node.type || "unknown";
}
} catch (error) {
return "unknown";
}
}
/**
* Extract function parameter names from AST (legacy compatibility)
* @param {object} functionNode - The function AST node
* @returns {Array<string>}
*/
export function extractFunctionParams(functionNode) {
return extractDetailedParams(functionNode.params).map((param) => param.name);
}