156 lines
4.6 KiB
JavaScript
156 lines
4.6 KiB
JavaScript
/**
|
|
* ESLint rules for Services Guardrails
|
|
*
|
|
* Enforces service contracts and boundaries
|
|
*/
|
|
|
|
module.exports = {
|
|
// Rule 1: Services must be marked with @server-safe or @client-only
|
|
'services-must-be-marked': {
|
|
meta: {
|
|
type: 'problem',
|
|
docs: {
|
|
description: 'Enforce service safety marking',
|
|
category: 'Services',
|
|
},
|
|
messages: {
|
|
message: 'Services must be explicitly marked with @server-safe or @client-only comment - see apps/website/lib/contracts/services/Service.ts',
|
|
},
|
|
},
|
|
create(context) {
|
|
return {
|
|
Program(node) {
|
|
const filename = context.getFilename();
|
|
if (filename.includes('/lib/services/') && filename.endsWith('.ts')) {
|
|
const sourceCode = context.getSourceCode();
|
|
const text = sourceCode.getText();
|
|
|
|
const hasServerSafe = text.includes('@server-safe');
|
|
const hasClientOnly = text.includes('@client-only');
|
|
|
|
if (!hasServerSafe && !hasClientOnly) {
|
|
context.report({
|
|
loc: { line: 1, column: 0 },
|
|
messageId: 'message',
|
|
});
|
|
}
|
|
}
|
|
},
|
|
};
|
|
},
|
|
},
|
|
|
|
// Rule 2: 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 3: 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;
|
|
} |