116 lines
4.0 KiB
JavaScript
116 lines
4.0 KiB
JavaScript
/**
|
|
* 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',
|
|
});
|
|
}
|
|
}
|
|
};
|
|
},
|
|
};
|