Files
gridpilot.gg/apps/website/eslint-rules/rsc-boundary-rules.js
2026-01-12 13:02:36 +01:00

370 lines
9.7 KiB
JavaScript

/**
* ESLint rules for RSC Boundary Guardrails
*
* Enforces server-side code boundaries in Next.js app directory
*/
module.exports = {
// Rule 1: No ContainerManager in server code
'no-container-manager-in-server': {
meta: {
type: 'problem',
docs: {
description: 'Forbid ContainerManager usage in server code',
category: 'RSC Boundary',
},
messages: {
message: 'ContainerManager usage forbidden in server code',
},
},
create(context) {
return {
Identifier(node) {
if (node.name === 'ContainerManager' && !isInComment(node)) {
context.report({
node,
messageId: 'message',
});
}
},
};
},
},
// Rule 2: No PageDataFetcher.fetch() in server code
'no-page-data-fetcher-fetch-in-server': {
meta: {
type: 'problem',
docs: {
description: 'Forbid PageDataFetcher.fetch() in server code',
category: 'RSC Boundary',
},
messages: {
message: 'PageDataFetcher.fetch() forbidden in server code',
},
},
create(context) {
return {
CallExpression(node) {
if (node.callee.type === 'MemberExpression' &&
node.callee.object.name === 'PageDataFetcher' &&
node.callee.property.name === 'fetch' &&
!isInComment(node)) {
context.report({
node,
messageId: 'message',
});
}
},
};
},
},
// Rule 3: No ViewModels/ViewModels imports in server code
'no-view-models-in-server': {
meta: {
type: 'problem',
docs: {
description: 'Forbid ViewModels/Presenters imports in server code',
category: 'RSC Boundary',
},
messages: {
message: 'ViewModels or Presenters import forbidden in server code',
},
},
create(context) {
return {
ImportDeclaration(node) {
const importPath = node.source.value;
if ((importPath.includes('@/lib/view-models/') ||
importPath.includes('@/lib/presenters/')) &&
!isInComment(node)) {
context.report({
node,
messageId: 'message',
});
}
},
};
},
},
// Rule 4: No Presenter imports in server code
'no-presenters-in-server': {
meta: {
type: 'problem',
docs: {
description: 'Forbid Presenter imports in server code',
category: 'RSC Boundary',
},
messages: {
message: 'Presenter import forbidden in server code',
},
},
create(context) {
return {
ImportDeclaration(node) {
const importPath = node.source.value;
if ((importPath.includes('@/lib/presenters/') ||
importPath.includes('@/lib/presenters/')) &&
!isInComment(node)) {
context.report({
node,
messageId: 'message',
});
}
},
};
},
},
// Rule 5: No Intl usage in presentation paths
'no-intl-in-presentation': {
meta: {
type: 'problem',
docs: {
description: 'Forbid Intl.* or toLocale* usage in presentation paths',
category: 'RSC Boundary',
},
messages: {
message: 'Intl.* or toLocale* usage forbidden in presentation paths',
},
},
create(context) {
return {
MemberExpression(node) {
if ((node.object.name === 'Intl' ||
(node.property && node.property.name && node.property.name.startsWith('toLocale'))) &&
!isInComment(node)) {
context.report({
node,
messageId: 'message',
});
}
},
};
},
},
// Rule 6: No sorting/filtering/reduce in server code
'no-sorting-filtering-in-server': {
meta: {
type: 'problem',
docs: {
description: 'Forbid sorting/filtering/reduce operations in server code',
category: 'RSC Boundary',
},
messages: {
message: 'Sorting/filtering/reduce operations forbidden in server code',
},
},
create(context) {
return {
CallExpression(node) {
if (node.callee.type === 'MemberExpression' &&
['sort', 'filter', 'reduce'].includes(node.callee.property.name) &&
!isInComment(node) &&
!node.loc.start.line.toString().includes('null check')) {
context.report({
node,
messageId: 'message',
});
}
},
};
},
},
// Rule 7: No DisplayObjects imports in server code
'no-display-objects-in-server': {
meta: {
type: 'problem',
docs: {
description: 'Forbid DisplayObjects imports in server code',
category: 'RSC Boundary',
},
messages: {
message: 'DisplayObjects import forbidden in server code',
},
},
create(context) {
return {
ImportDeclaration(node) {
const importPath = node.source.value;
if (importPath.includes('@/lib/display-objects/') &&
!isInComment(node)) {
context.report({
node,
messageId: 'message',
});
}
},
};
},
},
// Rule 8: No unsafe services imports in server code
'no-unsafe-services-in-server': {
meta: {
type: 'problem',
docs: {
description: 'Forbid unsafe services imports in server code',
category: 'RSC Boundary',
},
messages: {
message: 'Services import must be explicitly marked as server-safe',
},
},
create(context) {
return {
ImportDeclaration(node) {
const importPath = node.source.value;
if (importPath.includes('@/lib/services/') &&
!isInComment(node) &&
!isMarkedServerSafe(context.getSourceCode().getText(node))) {
context.report({
node,
messageId: 'message',
});
}
},
};
},
},
// Rule 9: No DI imports in server code
'no-di-in-server': {
meta: {
type: 'problem',
docs: {
description: 'Forbid DI imports in server code',
category: 'RSC Boundary',
},
messages: {
message: 'DI import forbidden in server code',
},
},
create(context) {
return {
ImportDeclaration(node) {
const importPath = node.source.value;
if (importPath.includes('@/lib/di/') &&
!isInComment(node)) {
context.report({
node,
messageId: 'message',
});
}
},
};
},
},
// Rule 10: No local helpers in server code
'no-local-helpers-in-server': {
meta: {
type: 'problem',
docs: {
description: 'Forbid local helper functions in server code',
category: 'RSC Boundary',
},
messages: {
message: 'Local helper functions forbidden (only assert*/invariant* allowed)',
},
},
create(context) {
return {
FunctionDeclaration(node) {
if (!node.id.name.startsWith('assert') &&
!node.id.name.startsWith('invariant') &&
!isInComment(node)) {
context.report({
node,
messageId: 'message',
});
}
},
VariableDeclarator(node) {
if (node.init &&
(node.init.type === 'FunctionExpression' || node.init.type === 'ArrowFunctionExpression') &&
!node.id.name.startsWith('assert') &&
!node.id.name.startsWith('invariant') &&
!isInComment(node)) {
context.report({
node,
messageId: 'message',
});
}
},
};
},
},
// Rule 11: No object construction in server code
'no-object-construction-in-server': {
meta: {
type: 'problem',
docs: {
description: 'Forbid object construction with new in server code',
category: 'RSC Boundary',
},
messages: {
message: 'Object construction with new forbidden (use PageQueries)',
},
},
create(context) {
return {
NewExpression(node) {
if (node.callee.type === 'Identifier' &&
/^[A-Z]/.test(node.callee.name) &&
!isInComment(node)) {
context.report({
node,
messageId: 'message',
});
}
},
};
},
},
// Rule 12: No ContainerManager calls in server code
'no-container-manager-calls-in-server': {
meta: {
type: 'problem',
docs: {
description: 'Forbid ContainerManager calls in server code',
category: 'RSC Boundary',
},
messages: {
message: 'ContainerManager calls forbidden in server code',
},
},
create(context) {
return {
CallExpression(node) {
if (node.callee.type === 'MemberExpression' &&
node.callee.object.name === 'ContainerManager' &&
(node.callee.property.name === 'getInstance' ||
node.callee.property.name === 'getContainer') &&
!isInComment(node)) {
context.report({
node,
messageId: 'message',
});
}
},
};
},
},
};
// Helper functions
function isInComment(node) {
// This is a simplified check - in practice you'd need to check the actual comment
return false;
}
function isMarkedServerSafe(text) {
return text.includes('// @server-safe') || text.includes('/* @server-safe */');
}