website refactor
This commit is contained in:
243
apps/website/eslint-rules/server-actions-interface.js
Normal file
243
apps/website/eslint-rules/server-actions-interface.js
Normal file
@@ -0,0 +1,243 @@
|
||||
/**
|
||||
* ESLint Rule: Server Actions Interface Compliance
|
||||
*
|
||||
* Ensures server actions follow a specific interface pattern:
|
||||
* - Must be async functions
|
||||
* - Must use mutations for business logic
|
||||
* - Must handle errors properly using Result type
|
||||
* - Should not contain business logic directly
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Ensure server actions implement the correct interface pattern',
|
||||
category: 'Server Actions',
|
||||
recommended: true,
|
||||
},
|
||||
messages: {
|
||||
mustBeAsync: 'Server actions must be async functions',
|
||||
mustUseMutations: 'Server actions must use mutations for business logic',
|
||||
mustHandleErrors: 'Server actions must handle errors using Result type',
|
||||
noBusinessLogic: 'Server actions should be thin wrappers, business logic belongs in mutations',
|
||||
invalidActionPattern: 'Server actions should follow pattern: async function actionName(input) { const result = await mutation.execute(input); return result; }',
|
||||
missingMutationUsage: 'Server action must instantiate and call a mutation',
|
||||
},
|
||||
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;
|
||||
const serverActionFunctions = [];
|
||||
|
||||
return {
|
||||
// Check for 'use server' directive
|
||||
ExpressionStatement(node) {
|
||||
if (node.expression.type === 'Literal' && node.expression.value === 'use server') {
|
||||
hasUseServerDirective = true;
|
||||
}
|
||||
},
|
||||
|
||||
// Track async function declarations
|
||||
FunctionDeclaration(node) {
|
||||
if (node.async && node.id && node.id.name) {
|
||||
serverActionFunctions.push({
|
||||
name: node.id.name,
|
||||
node: node,
|
||||
body: node.body,
|
||||
params: node.params,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Track async arrow function exports
|
||||
ExportNamedDeclaration(node) {
|
||||
if (node.declaration && node.declaration.type === 'FunctionDeclaration') {
|
||||
if (node.declaration.async && node.declaration.id) {
|
||||
serverActionFunctions.push({
|
||||
name: node.declaration.id.name,
|
||||
node: node.declaration,
|
||||
body: node.declaration.body,
|
||||
params: node.declaration.params,
|
||||
});
|
||||
}
|
||||
} else if (node.declaration && node.declaration.type === 'VariableDeclaration') {
|
||||
node.declaration.declarations.forEach(decl => {
|
||||
if (decl.init && decl.init.type === 'ArrowFunctionExpression' && decl.init.async) {
|
||||
serverActionFunctions.push({
|
||||
name: decl.id.name,
|
||||
node: decl.init,
|
||||
body: decl.init.body,
|
||||
params: decl.init.params,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
'Program:exit'() {
|
||||
// Only check files with 'use server' directive
|
||||
if (!hasUseServerDirective) return;
|
||||
|
||||
// Check each server action function
|
||||
serverActionFunctions.forEach(func => {
|
||||
// Check if function is async
|
||||
if (!func.node.async) {
|
||||
context.report({
|
||||
node: func.node,
|
||||
messageId: 'mustBeAsync',
|
||||
});
|
||||
}
|
||||
|
||||
// Analyze function body for mutation usage and error handling
|
||||
const bodyAnalysis = analyzeFunctionBody(func.body);
|
||||
|
||||
if (!bodyAnalysis.hasMutationUsage) {
|
||||
context.report({
|
||||
node: func.node,
|
||||
messageId: 'missingMutationUsage',
|
||||
});
|
||||
}
|
||||
|
||||
if (!bodyAnalysis.hasErrorHandling) {
|
||||
context.report({
|
||||
node: func.node,
|
||||
messageId: 'mustHandleErrors',
|
||||
});
|
||||
}
|
||||
|
||||
if (bodyAnalysis.hasBusinessLogic) {
|
||||
context.report({
|
||||
node: func.node,
|
||||
messageId: 'noBusinessLogic',
|
||||
});
|
||||
}
|
||||
|
||||
// Check if function follows the expected pattern
|
||||
if (!bodyAnalysis.followsPattern) {
|
||||
context.report({
|
||||
node: func.node,
|
||||
messageId: 'invalidActionPattern',
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Analyze function body to check for proper patterns
|
||||
*/
|
||||
function analyzeFunctionBody(body) {
|
||||
const analysis = {
|
||||
hasMutationUsage: false,
|
||||
hasErrorHandling: false,
|
||||
hasBusinessLogic: false,
|
||||
followsPattern: false,
|
||||
};
|
||||
|
||||
if (!body || body.type !== 'BlockStatement') {
|
||||
return analysis;
|
||||
}
|
||||
|
||||
const statements = body.body;
|
||||
let hasMutationInstantiation = false;
|
||||
let hasMutationExecute = false;
|
||||
let hasResultCheck = false;
|
||||
let hasReturnResult = false;
|
||||
|
||||
// Check for mutation usage in a more flexible way
|
||||
statements.forEach(stmt => {
|
||||
// Look for: const mutation = new SomeMutation()
|
||||
if (stmt.type === 'VariableDeclaration') {
|
||||
stmt.declarations.forEach(decl => {
|
||||
if (decl.init && decl.init.type === 'NewExpression') {
|
||||
if (decl.init.callee.type === 'Identifier' &&
|
||||
decl.init.callee.name.endsWith('Mutation')) {
|
||||
hasMutationInstantiation = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Look for: const result = await mutation.execute(...)
|
||||
if (decl.init && decl.init.type === 'AwaitExpression') {
|
||||
const awaitExpr = decl.init.argument;
|
||||
if (awaitExpr.type === 'CallExpression') {
|
||||
const callee = awaitExpr.callee;
|
||||
if (callee.type === 'MemberExpression' &&
|
||||
callee.property.type === 'Identifier' &&
|
||||
callee.property.name === 'execute') {
|
||||
// Check if the object is a mutation or result of mutation
|
||||
if (callee.object.type === 'Identifier') {
|
||||
const objName = callee.object.name;
|
||||
// Could be 'mutation' or 'result' (from mutation.execute)
|
||||
if (objName === 'mutation' || objName === 'result' || objName.endsWith('Mutation')) {
|
||||
hasMutationExecute = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Look for error handling: if (result.isErr())
|
||||
if (stmt.type === 'IfStatement') {
|
||||
if (stmt.test.type === 'CallExpression') {
|
||||
const callee = stmt.test.callee;
|
||||
if (callee.type === 'MemberExpression' &&
|
||||
callee.property.type === 'Identifier' &&
|
||||
(callee.property.name === 'isErr' || callee.property.name === 'isOk')) {
|
||||
hasResultCheck = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Look for return statements that return Result
|
||||
if (stmt.type === 'ReturnStatement' && stmt.argument) {
|
||||
if (stmt.argument.type === 'CallExpression') {
|
||||
const callee = stmt.argument.callee;
|
||||
if (callee.type === 'MemberExpression' &&
|
||||
callee.object.type === 'Identifier' &&
|
||||
callee.object.name === 'Result') {
|
||||
hasReturnResult = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
analysis.hasMutationUsage = hasMutationInstantiation && hasMutationExecute;
|
||||
analysis.hasErrorHandling = hasResultCheck;
|
||||
analysis.hasReturnResult = hasReturnResult;
|
||||
|
||||
// Check if follows pattern: mutation instantiation → execute → error handling → return Result
|
||||
analysis.followsPattern = analysis.hasMutationUsage &&
|
||||
analysis.hasErrorHandling &&
|
||||
analysis.hasReturnResult;
|
||||
|
||||
// Check for business logic (direct service calls, complex calculations, etc.)
|
||||
const hasDirectServiceCall = statements.some(stmt => {
|
||||
if (stmt.type === 'ExpressionStatement' && stmt.expression.type === 'CallExpression') {
|
||||
const callee = stmt.expression.callee;
|
||||
if (callee.type === 'MemberExpression' && callee.object.type === 'Identifier') {
|
||||
return callee.object.name.endsWith('Service');
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
analysis.hasBusinessLogic = hasDirectServiceCall;
|
||||
|
||||
return analysis;
|
||||
}
|
||||
Reference in New Issue
Block a user