Files
gridpilot.gg/apps/website/eslint-rules/server-actions-return-result.js
2026-01-14 02:02:24 +01:00

169 lines
5.5 KiB
JavaScript

/**
* ESLint Rule: Server Actions Must Return Result Type
*
* Ensures server actions return Result<T, E> types instead of arbitrary objects
* This enforces the standardized error handling pattern across all server actions
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Ensure server actions return Result<T, E> types from lib/contracts/Result.ts',
category: 'Server Actions',
recommended: true,
},
messages: {
mustReturnResult: 'Server actions must return Result<T, E> type. Expected: Promise<Result<SuccessType, ErrorType>>',
invalidReturnType: 'Return type must be Result<T, E> or Promise<Result<T, E>>',
missingResultImport: 'Server actions must import Result from @/lib/contracts/Result',
},
schema: [],
},
create(context) {
const filename = context.getFilename();
const isServerActionFile = filename.includes('/app/') &&
(filename.endsWith('.ts') || filename.endsWith('.tsx')) &&
!filename.endsWith('.test.ts') &&
!filename.endsWith('.test.tsx');
if (!isServerActionFile) {
return {};
}
let hasUseServerDirective = false;
let hasResultImport = false;
const functionDeclarations = [];
return {
// Check for 'use server' directive
ExpressionStatement(node) {
if (node.expression.type === 'Literal' && node.expression.value === 'use server') {
hasUseServerDirective = true;
}
},
// Check for Result import
ImportDeclaration(node) {
const importPath = node.source.value;
if (importPath === '@/lib/contracts/Result' || importPath === '@/lib/contracts/Result.ts') {
hasResultImport = true;
}
},
// Track function declarations and exports
FunctionDeclaration(node) {
if (node.async && node.id && node.id.name) {
functionDeclarations.push({
name: node.id.name,
node: node,
returnType: node.returnType,
body: node.body,
});
}
},
// Track arrow function exports
ExportNamedDeclaration(node) {
if (node.declaration && node.declaration.type === 'FunctionDeclaration') {
if (node.declaration.async && node.declaration.id) {
functionDeclarations.push({
name: node.declaration.id.name,
node: node.declaration,
returnType: node.declaration.returnType,
body: node.declaration.body,
});
}
} else if (node.declaration && node.declaration.type === 'VariableDeclaration') {
node.declaration.declarations.forEach(decl => {
if (decl.init && decl.init.type === 'ArrowFunctionExpression' && decl.init.async) {
functionDeclarations.push({
name: decl.id.name,
node: decl.init,
returnType: decl.init.returnType,
body: decl.init.body,
});
}
});
}
},
'Program:exit'() {
// Only check files with 'use server' directive
if (!hasUseServerDirective) return;
// Report if no Result import
if (!hasResultImport) {
context.report({
node: context.getSourceCode().ast,
messageId: 'missingResultImport',
});
}
// Check each server action function
functionDeclarations.forEach(func => {
const returnType = func.returnType;
// Check if return type annotation exists and is correct
if (!returnType) {
context.report({
node: func.node,
messageId: 'mustReturnResult',
});
return;
}
// Helper function to check if a type is Result
function isResultType(typeNode) {
if (!typeNode) return false;
// Direct Result type
if (typeNode.typeName && typeNode.typeName.name === 'Result') {
return true;
}
// Promise<Result<...>>
if (typeNode.typeName && typeNode.typeName.name === 'Promise') {
const typeParams = typeNode.typeParameters || typeNode.typeArguments;
if (typeParams && typeParams.params && typeParams.params[0]) {
return isResultType(typeParams.params[0]);
}
}
return false;
}
// Check if it's a Promise<Result<...>> or Result<...>
if (returnType.typeAnnotation) {
const typeAnnotation = returnType.typeAnnotation;
if (isResultType(typeAnnotation)) {
// Valid: Result<...> or Promise<Result<...>>
return;
} else if (typeAnnotation.type === 'TSUnionType') {
// Check if union contains only Result types
const hasNonResult = typeAnnotation.types.some(type => !isResultType(type));
if (hasNonResult) {
context.report({
node: returnType,
messageId: 'invalidReturnType',
});
}
} else {
context.report({
node: returnType,
messageId: 'invalidReturnType',
});
}
} else {
context.report({
node: returnType,
messageId: 'invalidReturnType',
});
}
});
},
};
},
};