Files
gridpilot.gg/apps/website/eslint-rules/service-function-format.js
2026-01-13 00:16:14 +01:00

144 lines
4.5 KiB
JavaScript

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