/** * ESLint rule to ban usage of generic UI primitives in components * * Generic primitives like Box and Surface should only be used in the ui/ layer * to build semantic UI elements. Components should use those semantic elements. * * Rationale: * - Encourages use of semantic UI components * - Maintains architectural boundaries * - Improves consistency across the application */ module.exports = { meta: { type: 'problem', docs: { description: 'Ban usage of generic UI primitives in components', category: 'Architecture', recommended: true, }, fixable: null, schema: [], messages: { noGenericPrimitive: 'Generic UI primitive or wrapper "{{name}}" is not allowed in components. Primitives (Box, Surface) and generic wrappers (Layout, Container) are internal to the UI layer. Use semantic UI elements from @/ui instead (e.g., Card, Section, Table, Stack, Grid). If a semantic element is missing, create one in apps/website/ui/ using primitives.', noPrimitiveExport: 'Do not re-export primitives from the UI layer. Primitives should remain internal to apps/website/ui/primitives/.', noClassName: 'The "className" prop is not allowed in components. Styling must be encapsulated within UI elements in apps/website/ui/. Use semantic UI elements or create a new one if you need custom styling.', }, }, create(context) { const filename = context.getFilename(); // Only run for files under /components/ (and *.ts/*.tsx) const isComponent = filename.includes('/components/') && (filename.endsWith('.ts') || filename.endsWith('.tsx')); // Check for re-exports in the UI layer (excluding primitives themselves) const isUiLayer = filename.includes('/ui/') && !filename.includes('/ui/primitives/'); return { ImportDeclaration(node) { if (!isComponent) return; const importPath = node.source.value; // Check if it's an import from the UI primitives layer const isPrimitiveImport = importPath.includes('/ui/primitives') || importPath.startsWith('@/ui/primitives') || // Legacy direct paths importPath.endsWith('/ui/Box') || importPath.endsWith('/ui/Surface') || importPath.endsWith('/ui/Layout') || importPath.endsWith('/ui/Container') || importPath === '@/ui/Box' || importPath === '@/ui/Surface' || importPath === '@/ui/Layout' || importPath === '@/ui/Container'; if (isPrimitiveImport) { node.specifiers.forEach(specifier => { let importedName = ''; if (specifier.type === 'ImportSpecifier') { importedName = specifier.imported.name; } else if (specifier.type === 'ImportDefaultSpecifier') { importedName = specifier.local.name; } context.report({ node: specifier, messageId: 'noGenericPrimitive', data: { name: importedName }, }); }); } }, JSXAttribute(node) { if (!isComponent) return; if (node.name.name === 'className') { context.report({ node, messageId: 'noClassName', }); } }, ExportNamedDeclaration(node) { if (!isUiLayer) return; if (!node.source) return; const exportPath = node.source.value; if (exportPath.includes('/primitives/') || exportPath.startsWith('./primitives/')) { context.report({ node, messageId: 'noPrimitiveExport', }); } }, ExportAllDeclaration(node) { if (!isUiLayer) return; const exportPath = node.source.value; if (exportPath.includes('/primitives/') || exportPath.startsWith('./primitives/')) { context.report({ node, messageId: 'noPrimitiveExport', }); } } }; }, };