131 lines
4.1 KiB
JavaScript
131 lines
4.1 KiB
JavaScript
/**
|
|
* ESLint Rule: Server Actions Must Use Mutations
|
|
*
|
|
* Ensures server actions use mutations instead of direct service calls
|
|
*/
|
|
|
|
module.exports = {
|
|
meta: {
|
|
type: 'problem',
|
|
docs: {
|
|
description: 'Ensure server actions use mutations instead of direct service calls',
|
|
category: 'Server Actions',
|
|
recommended: true,
|
|
},
|
|
messages: {
|
|
mustUseMutations: 'Server actions must use Mutations, not direct Service or API Client calls. See apps/website/docs/architecture/write/MUTATIONS.md',
|
|
noDirectService: 'Direct service calls in server actions are not allowed',
|
|
noMutationUsage: 'Server actions should instantiate and call mutations',
|
|
},
|
|
schema: [],
|
|
},
|
|
|
|
create(context) {
|
|
const filename = context.getFilename();
|
|
const isServerAction = filename.includes('/app/') &&
|
|
(filename.endsWith('.ts') || filename.endsWith('.tsx')) &&
|
|
!filename.endsWith('.test.ts') &&
|
|
!filename.endsWith('.test.tsx');
|
|
|
|
if (!isServerAction) {
|
|
return {};
|
|
}
|
|
|
|
let hasUseServerDirective = false;
|
|
let hasServiceImport = false;
|
|
let hasApiClientImport = false;
|
|
let hasMutationImport = false;
|
|
const newExpressions = [];
|
|
const callExpressions = [];
|
|
|
|
return {
|
|
// Check for 'use server' directive
|
|
ExpressionStatement(node) {
|
|
if (node.expression.type === 'Literal' && node.expression.value === 'use server') {
|
|
hasUseServerDirective = true;
|
|
}
|
|
},
|
|
|
|
// Check imports
|
|
ImportDeclaration(node) {
|
|
const importPath = node.source.value;
|
|
|
|
if (importPath.includes('/lib/services/')) {
|
|
hasServiceImport = true;
|
|
}
|
|
if (importPath.includes('/lib/api/') || importPath.includes('/api/')) {
|
|
hasApiClientImport = true;
|
|
}
|
|
if (importPath.includes('/lib/mutations/')) {
|
|
hasMutationImport = true;
|
|
}
|
|
},
|
|
|
|
// Track all NewExpression (instantiations)
|
|
NewExpression(node) {
|
|
newExpressions.push(node);
|
|
},
|
|
|
|
// Track all CallExpression (method calls)
|
|
CallExpression(node) {
|
|
callExpressions.push(node);
|
|
},
|
|
|
|
'Program:exit'() {
|
|
// Only check files with 'use server' directive
|
|
if (!hasUseServerDirective) return;
|
|
|
|
// Check for direct service/API client instantiation
|
|
const hasDirectServiceInstantiation = newExpressions.some(node => {
|
|
if (node.callee.type === 'Identifier') {
|
|
const calleeName = node.callee.name;
|
|
return calleeName.endsWith('Service') || calleeName.endsWith('ApiClient') || calleeName.endsWith('Client');
|
|
}
|
|
return false;
|
|
});
|
|
|
|
// Check for direct service method calls
|
|
const hasDirectServiceCall = callExpressions.some(node => {
|
|
if (node.callee.type === 'MemberExpression' &&
|
|
node.callee.object.type === 'Identifier') {
|
|
const objName = node.callee.object.name;
|
|
return objName.endsWith('Service') || objName.endsWith('ApiClient');
|
|
}
|
|
return false;
|
|
});
|
|
|
|
// Check if mutations are being used
|
|
const hasMutationUsage = newExpressions.some(node => {
|
|
if (node.callee.type === 'Identifier') {
|
|
return node.callee.name.endsWith('Mutation');
|
|
}
|
|
return false;
|
|
});
|
|
|
|
// Report violations
|
|
if (hasDirectServiceInstantiation && !hasMutationUsage) {
|
|
context.report({
|
|
node: context.getSourceCode().ast,
|
|
messageId: 'mustUseMutations',
|
|
});
|
|
}
|
|
|
|
if (hasDirectServiceCall) {
|
|
context.report({
|
|
node: context.getSourceCode().ast,
|
|
messageId: 'noDirectService',
|
|
});
|
|
}
|
|
|
|
// If imports exist but no mutation usage
|
|
if ((hasServiceImport || hasApiClientImport) && !hasMutationImport) {
|
|
context.report({
|
|
node: context.getSourceCode().ast,
|
|
messageId: 'mustUseMutations',
|
|
});
|
|
}
|
|
},
|
|
};
|
|
},
|
|
};
|