173 lines
5.5 KiB
JavaScript
173 lines
5.5 KiB
JavaScript
/**
|
|
* ESLint rule to enforce Service function format
|
|
*
|
|
* Services in lib/services/ must:
|
|
* 1. Be classes named *Service (not functions)
|
|
* 2. Create their own dependencies (API Client, Logger, ErrorReporter)
|
|
* 3. Return Result types
|
|
* 4. NOT use redirect() or process.exit()
|
|
* 5. CAN use console.error() for logging (allowed)
|
|
*/
|
|
|
|
module.exports = {
|
|
meta: {
|
|
type: 'problem',
|
|
docs: {
|
|
description: 'Enforce proper Service class format',
|
|
category: 'Services',
|
|
recommended: true,
|
|
},
|
|
fixable: null,
|
|
schema: [],
|
|
messages: {
|
|
notAClass: 'Services must be classes named *Service, not functions. Found function "{{name}}" in lib/services/',
|
|
noRedirect: 'Services cannot use redirect(). Use PageQueries or Client Components for navigation.',
|
|
noProcessExit: 'Services cannot use process.exit().',
|
|
multipleExports: 'Service files should only export the Service class.',
|
|
mustReturnResult: 'Service methods must return Result<T, DomainError>.',
|
|
},
|
|
},
|
|
|
|
create(context) {
|
|
const filename = context.getFilename();
|
|
const isInServices = filename.includes('/lib/services/');
|
|
let hasRedirect = false;
|
|
let hasProcessExit = false;
|
|
let hasMultipleExports = false;
|
|
let hasFunctionExport = false;
|
|
let functionName = '';
|
|
let hasResultReturningMethod = false;
|
|
|
|
return {
|
|
// Track function declarations
|
|
FunctionDeclaration(node) {
|
|
if (isInServices && node.id && node.id.name) {
|
|
hasFunctionExport = true;
|
|
functionName = node.id.name;
|
|
}
|
|
},
|
|
|
|
// Track function expressions
|
|
FunctionExpression(node) {
|
|
if (isInServices && node.parent && node.parent.type === 'VariableDeclarator') {
|
|
hasFunctionExport = true;
|
|
functionName = node.parent.id.name;
|
|
}
|
|
},
|
|
|
|
// Track arrow functions
|
|
ArrowFunctionExpression(node) {
|
|
if (isInServices && node.parent && node.parent.type === 'VariableDeclarator') {
|
|
hasFunctionExport = true;
|
|
functionName = node.parent.id.name;
|
|
}
|
|
},
|
|
|
|
// Track redirect and process.exit calls
|
|
CallExpression(node) {
|
|
if (isInServices) {
|
|
// Check for redirect()
|
|
if (node.callee.type === 'Identifier' && node.callee.name === 'redirect') {
|
|
hasRedirect = true;
|
|
}
|
|
|
|
// Check for process.exit()
|
|
if (node.callee.type === 'MemberExpression' &&
|
|
node.callee.object.type === 'Identifier' &&
|
|
node.callee.object.name === 'process' &&
|
|
node.callee.property.name === 'exit') {
|
|
hasProcessExit = true;
|
|
}
|
|
}
|
|
},
|
|
|
|
// Track exports
|
|
ExportNamedDeclaration(node) {
|
|
if (isInServices) {
|
|
if (node.declaration) {
|
|
if (node.declaration.type === 'ClassDeclaration') {
|
|
const className = node.declaration.id?.name;
|
|
if (className && !className.endsWith('Service')) {
|
|
hasMultipleExports = true;
|
|
}
|
|
} else if (node.declaration.type === 'FunctionDeclaration') {
|
|
hasFunctionExport = true;
|
|
functionName = node.declaration.id?.name || '';
|
|
} else {
|
|
// Interface, type alias, const, etc.
|
|
hasMultipleExports = true;
|
|
}
|
|
} else if (node.specifiers && node.specifiers.length > 0) {
|
|
hasMultipleExports = true;
|
|
}
|
|
}
|
|
},
|
|
|
|
// Check for Result-returning methods
|
|
MethodDefinition(node) {
|
|
if (isInServices && node.key.type === 'Identifier') {
|
|
const returnType = node.value?.returnType;
|
|
|
|
if (returnType &&
|
|
returnType.typeAnnotation &&
|
|
returnType.typeAnnotation.typeName &&
|
|
returnType.typeAnnotation.typeName.name === 'Promise' &&
|
|
returnType.typeAnnotation.typeParameters &&
|
|
returnType.typeAnnotation.typeParameters.params.length > 0) {
|
|
|
|
const resultType = returnType.typeAnnotation.typeParameters.params[0];
|
|
|
|
if (resultType.type === 'TSTypeReference' &&
|
|
resultType.typeName &&
|
|
resultType.typeName.type === 'Identifier' &&
|
|
resultType.typeName.name === 'Result') {
|
|
hasResultReturningMethod = true;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
'Program:exit'() {
|
|
if (!isInServices) return;
|
|
|
|
// Check for function exports
|
|
if (hasFunctionExport) {
|
|
context.report({
|
|
node: context.getSourceCode().ast,
|
|
messageId: 'notAClass',
|
|
data: { name: functionName },
|
|
});
|
|
}
|
|
|
|
// Check for redirect
|
|
if (hasRedirect) {
|
|
context.report({
|
|
node: context.getSourceCode().ast,
|
|
messageId: 'noRedirect',
|
|
});
|
|
}
|
|
|
|
// Check for process.exit
|
|
if (hasProcessExit) {
|
|
context.report({
|
|
node: context.getSourceCode().ast,
|
|
messageId: 'noProcessExit',
|
|
});
|
|
}
|
|
|
|
// Check for multiple exports
|
|
if (hasMultipleExports && !hasFunctionExport) {
|
|
context.report({
|
|
node: context.getSourceCode().ast,
|
|
messageId: 'multipleExports',
|
|
});
|
|
}
|
|
|
|
// Check for Result-returning methods (warn if none found)
|
|
// Note: This is a soft check - services might have private methods that don't return Result
|
|
// The important thing is that the public API methods do
|
|
},
|
|
};
|
|
},
|
|
};
|