/** * 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 - see apps/website/lib/contracts/view-models/ViewModel.ts', }, }, 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 - see apps/website/lib/contracts/view-models/ViewModel.ts', }, }, 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 - see apps/website/lib/contracts/view-models/ViewModel.ts', }, }, 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 - see apps/website/lib/contracts/view-models/ViewModel.ts', }, }, 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 - see apps/website/lib/contracts/view-models/ViewModel.ts', }, }, 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 - see apps/website/lib/contracts/view-models/ViewModel.ts', }, }, create(context) { const sourceCode = context.getSourceCode(); const hasUseClient = sourceCode.getText().includes("'use client'") || sourceCode.getText().includes('"use client"'); return { CallExpression(node) { if (hasUseClient) return; 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 - see apps/website/lib/contracts/view-models/ViewModel.ts', }, }, 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 - see apps/website/lib/contracts/view-models/ViewModel.ts', }, }, 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 - see apps/website/lib/contracts/view-models/ViewModel.ts', }, }, 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) - see apps/website/lib/contracts/view-models/ViewModel.ts', }, }, create(context) { const sourceCode = context.getSourceCode(); const hasUseClient = sourceCode.getText().includes("'use client'") || sourceCode.getText().includes('"use client"'); return { FunctionDeclaration(node) { if (hasUseClient) return; // Skip if this is the main component (default export or ends with Page/Template) const filename = context.getFilename(); const isMainComponent = (node.parent && node.parent.type === 'ExportDefaultDeclaration') || (node.id && node.id.name && (node.id.name.endsWith('Page') || node.id.name.endsWith('Template') || node.id.name.endsWith('Component'))); if (isMainComponent) return; // Only flag nested helper functions if (!node.id.name.startsWith('assert') && !node.id.name.startsWith('invariant') && !isInComment(node)) { context.report({ node, messageId: 'message', }); } }, VariableDeclarator(node) { if (hasUseClient) return; // Skip if this is the main component const isMainComponent = (node.parent && node.parent.parent && node.parent.parent.type === 'ExportDefaultDeclaration') || (node.id && node.id.name && (node.id.name.endsWith('Page') || node.id.name.endsWith('Template') || node.id.name.endsWith('Component'))); if (isMainComponent) return; 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) - see apps/website/lib/contracts/view-models/ViewModel.ts', }, }, create(context) { const sourceCode = context.getSourceCode(); const hasUseClient = sourceCode.getText().includes("'use client'") || sourceCode.getText().includes('"use client"'); return { NewExpression(node) { if (hasUseClient) return; if (node.callee.type === 'Identifier' && /^[A-Z]/.test(node.callee.name) && !node.callee.name.endsWith('PageQuery') && !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 - see apps/website/lib/contracts/view-models/ViewModel.ts', }, }, 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 */'); }