/** * 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.', }, }, 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 }, }; }, };