/** * ESLint rules for Client-Only Guardrails * * Enforces client-side only boundaries */ module.exports = { // Rule 1: No server-side code in client-only files 'no-server-code-in-client-only': { meta: { type: 'problem', docs: { description: 'Forbid server-side code in client-only files', category: 'Client-Only', }, messages: { message: 'Client-only files cannot contain server-side code - see apps/website/lib/contracts/view-models/ViewModel.ts', }, }, create(context) { return { Program(node) { const filename = context.getFilename(); if (filename.includes('/app/') && filename.endsWith('.tsx') && !filename.endsWith('page.tsx') && !filename.endsWith('layout.tsx')) { const sourceCode = context.getSourceCode(); const text = sourceCode.getText(); // Check for server-side patterns const serverPatterns = [ /getServerSideProps/, /cookies\(\)/, /headers\(\)/, /next\/headers/, ]; for (const pattern of serverPatterns) { if (pattern.test(text)) { context.report({ loc: { line: 1, column: 0 }, messageId: 'message', }); break; } } } }, }; }, }, // Rule 2: Client-only files must have 'use client' directive 'client-only-must-have-directive': { meta: { type: 'problem', docs: { description: 'Enforce use client directive', category: 'Client-Only', }, messages: { message: 'Client-only files must have "use client" directive at the top - see apps/website/lib/contracts/view-models/ViewModel.ts', }, }, create(context) { return { Program(node) { const filename = context.getFilename(); if (filename.includes('/app/') && filename.endsWith('.tsx') && !filename.endsWith('page.tsx') && !filename.endsWith('layout.tsx')) { const sourceCode = context.getSourceCode(); // Check for 'use client' as a string literal directive // This can be either a comment or a string literal statement const comments = sourceCode.getAllComments(); const firstComment = comments[0]; let hasDirective = false; // Check if it's a comment if (firstComment && firstComment.type === 'Line' && (firstComment.value.trim() === '"use client"' || firstComment.value.trim() === 'use client')) { hasDirective = true; } // Check if it's a string literal statement (the actual Next.js way) if (!hasDirective && node.body.length > 0) { const firstStmt = node.body[0]; if (firstStmt && firstStmt.type === 'ExpressionStatement' && firstStmt.expression.type === 'Literal' && (firstStmt.expression.value === 'use client' || firstStmt.expression.value === '"use client"')) { hasDirective = true; } } if (!hasDirective) { context.report({ loc: { line: 1, column: 0 }, messageId: 'message', }); } } }, }; }, }, };