/** * ESLint rules for Template Purity Guardrails * * Enforces pure template components without business logic */ module.exports = { // Rule 1: No ViewModels/DisplayObjects in templates 'no-view-models-in-templates': { meta: { type: 'problem', docs: { description: 'Forbid ViewModels/DisplayObjects imports in templates', category: 'Template Purity', }, messages: { message: 'ViewModels or DisplayObjects import forbidden in templates - see apps/website/lib/contracts/view-data/ViewData.ts', }, }, create(context) { return { ImportDeclaration(node) { const importPath = node.source.value; if ((importPath.includes('@/lib/view-models/') || importPath.includes('@/lib/presenters/') || importPath.includes('@/lib/display-objects/')) && !isInComment(node)) { context.report({ node, messageId: 'message', }); } }, }; }, }, // Rule 2: No state hooks in templates 'no-state-hooks-in-templates': { meta: { type: 'problem', docs: { description: 'Forbid state hooks in templates', category: 'Template Purity', }, messages: { message: 'State hooks forbidden in templates (use *PageClient.tsx) - see apps/website/lib/contracts/view-data/ViewData.ts', }, }, create(context) { return { CallExpression(node) { if (node.callee.type === 'Identifier' && ['useMemo', 'useEffect', 'useState', 'useReducer'].includes(node.callee.name) && !isInComment(node)) { context.report({ node, messageId: 'message', }); } }, }; }, }, // Rule 3: No computations in templates 'no-computations-in-templates': { meta: { type: 'problem', docs: { description: 'Forbid derived computations in templates', category: 'Template Purity', }, messages: { message: 'Derived computations forbidden in templates - see apps/website/lib/contracts/view-data/ViewData.ts', }, }, create(context) { return { CallExpression(node) { if (node.callee.type === 'MemberExpression' && ['filter', 'sort', 'reduce'].includes(node.callee.property.name) && !isInComment(node)) { context.report({ node, messageId: 'message', }); } }, }; }, }, // Rule 4: No restricted imports in templates 'no-restricted-imports-in-templates': { meta: { type: 'problem', docs: { description: 'Forbid restricted imports in templates', category: 'Template Purity', }, messages: { message: 'Templates cannot import from page-queries, services, api, di, or contracts - see apps/website/lib/contracts/view-data/ViewData.ts', }, }, create(context) { const restrictedPaths = [ '@/lib/page-queries/', '@/lib/services/', '@/lib/api/', '@/lib/di/', '@/lib/contracts/', ]; return { ImportDeclaration(node) { const importPath = node.source.value; if (restrictedPaths.some(path => importPath.includes(path)) && !isInComment(node)) { context.report({ node, messageId: 'message', }); } }, }; }, }, // Rule 5: Invalid template signature 'no-invalid-template-signature': { meta: { type: 'problem', docs: { description: 'Enforce correct template component signature', category: 'Template Purity', }, messages: { message: 'Template component must accept *ViewData type as first parameter - see apps/website/lib/contracts/view-data/ViewData.ts', }, }, create(context) { return { FunctionDeclaration(node) { if (node.params.length === 0 || !node.params[0].typeAnnotation || !node.params[0].typeAnnotation.typeAnnotation.type.includes('ViewData')) { context.report({ node, messageId: 'message', }); } }, }; }, }, // Rule 6: No template helper exports 'no-template-helper-exports': { meta: { type: 'problem', docs: { description: 'Forbid helper function exports in templates', category: 'Template Purity', }, messages: { message: 'Templates must not export helper functions - see apps/website/lib/contracts/view-data/ViewData.ts', }, }, create(context) { return { ExportNamedDeclaration(node) { if (node.declaration && (node.declaration.type === 'FunctionDeclaration' || node.declaration.type === 'VariableDeclaration')) { context.report({ node, messageId: 'message', }); } }, }; }, }, // Rule 7: Invalid template filename 'invalid-template-filename': { meta: { type: 'problem', docs: { description: 'Enforce correct template filename', category: 'Template Purity', }, messages: { message: 'Template files must end with Template.tsx - see apps/website/lib/contracts/view-data/ViewData.ts', }, }, create(context) { const filename = context.getFilename(); if (filename.includes('/templates/') && !filename.endsWith('Template.tsx')) { // Report at the top of the file context.report({ loc: { line: 1, column: 0 }, messageId: 'message', }); } return {}; }, }, }; // Helper functions function isInComment(node) { return false; }