/** * ESLint rules for Services Guardrails * * Enforces service contracts and boundaries */ module.exports = { // Rule 1: No external API calls in services 'no-external-api-in-services': { meta: { type: 'problem', docs: { description: 'Forbid external API calls in services', category: 'Services', }, messages: { message: 'External API calls must be in adapters, not services - see apps/website/lib/contracts/services/Service.ts', }, }, create(context) { return { CallExpression(node) { const filename = context.getFilename(); if (filename.includes('/lib/services/')) { // Check for fetch, axios, or other HTTP calls if (node.callee.type === 'Identifier' && ['fetch', 'axios'].includes(node.callee.name) && !isInComment(node)) { context.report({ node, messageId: 'message', }); } // Check for external API URLs if (node.arguments.length > 0) { const firstArg = node.arguments[0]; if (firstArg.type === 'Literal' && typeof firstArg.value === 'string' && (firstArg.value.startsWith('http') || firstArg.value.includes('api.') || firstArg.value.includes('.com'))) { context.report({ node, messageId: 'message', }); } } } }, }; }, }, // Rule 2: Services must be pure functions 'services-must-be-pure': { meta: { type: 'problem', docs: { description: 'Enforce service purity', category: 'Services', }, messages: { message: 'Services must be pure functions, no side effects allowed - see apps/website/lib/contracts/services/Service.ts', }, }, create(context) { return { CallExpression(node) { const filename = context.getFilename(); if (filename.includes('/lib/services/')) { // Check for common side effects if (node.callee.type === 'MemberExpression') { const object = node.callee.object; const property = node.callee.property; // DOM manipulation if (object.type === 'Identifier' && ['document', 'window'].includes(object.name)) { context.report({ node, messageId: 'message', }); } // State mutation if (property.type === 'Identifier' && ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].includes(property.name)) { context.report({ node, messageId: 'message', }); } } // Direct assignment to external state if (node.type === 'AssignmentExpression' && node.left.type === 'MemberExpression' && node.left.object.type === 'Identifier' && !isInFunctionScope(node)) { context.report({ node, messageId: 'message', }); } } }, }; }, }, }; // Helper functions function isInComment(node) { return false; } function isInFunctionScope(node) { // Simplified check return false; }