/** * ESLint rule to enforce Service function format * * Services in lib/services/ must: * 1. Be classes named *Service (not functions) * 2. Not have side effects (no redirect, console.log, etc.) * 3. Use builders for data transformation */ 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/', hasSideEffects: 'Services must be pure. Found side effect: {{effect}}', noRedirect: 'Services cannot use redirect(). Use PageQueries or Client Components for navigation.', multipleExports: 'Service files should only export the Service class.', }, }, create(context) { const filename = context.getFilename(); const isInServices = filename.includes('/lib/services/'); let hasSideEffect = false; let sideEffectType = ''; let hasMultipleExports = false; let hasFunctionExport = false; let functionName = ''; 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 calls CallExpression(node) { if (isInServices) { // Check for redirect() if (node.callee.type === 'Identifier' && node.callee.name === 'redirect') { hasSideEffect = true; sideEffectType = 'redirect()'; } // Check for console.log, console.error, etc. if (node.callee.type === 'MemberExpression' && node.callee.object.type === 'Identifier' && node.callee.object.name === 'console') { hasSideEffect = true; sideEffectType = 'console.' + (node.callee.property.name || 'call'); } // Check for process.exit() if (node.callee.type === 'MemberExpression' && node.callee.object.type === 'Identifier' && node.callee.object.name === 'process' && node.callee.property.name === 'exit') { hasSideEffect = true; sideEffectType = 'process.exit()'; } } }, // 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; } } }, 'Program:exit'() { if (!isInServices) return; // Check for function exports if (hasFunctionExport) { context.report({ node: context.getSourceCode().ast, messageId: 'notAClass', data: { name: functionName }, }); } // Check for side effects if (hasSideEffect) { context.report({ node: context.getSourceCode().ast, messageId: sideEffectType === 'redirect()' ? 'noRedirect' : 'hasSideEffects', data: { effect: sideEffectType }, }); } // Check for multiple exports if (hasMultipleExports && !hasFunctionExport) { context.report({ node: context.getSourceCode().ast, messageId: 'multipleExports', }); } }, }; }, };