website refactor
This commit is contained in:
@@ -1,29 +1,31 @@
|
||||
/**
|
||||
* ESLint rule to suggest proper component classification
|
||||
* ESLint rule to suggest proper component classification and enforce UI component usage
|
||||
*
|
||||
* Architecture:
|
||||
* - app/ - Pages and layouts only (no business logic)
|
||||
* - components/ - App-level components (can be stateful, can use hooks)
|
||||
* - ui/ - Pure, reusable UI elements (stateless, no hooks)
|
||||
* - hooks/ - Shared stateful logic
|
||||
*
|
||||
* This rule provides SUGGESTIONS, not errors, for component placement.
|
||||
* This rule enforces that components use proper UI elements instead of raw HTML and className.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Suggest proper component classification',
|
||||
description: 'Enforce proper component classification and UI component usage',
|
||||
category: 'Architecture',
|
||||
recommended: false,
|
||||
recommended: true,
|
||||
},
|
||||
fixable: 'code',
|
||||
fixable: null,
|
||||
schema: [],
|
||||
messages: {
|
||||
uiShouldBePure: 'This appears to be a pure UI element. Consider moving to ui/ for maximum reusability.',
|
||||
componentShouldBeInComponents: 'This component uses state/hooks. Consider moving to components/.',
|
||||
pureComponentInComponents: 'Pure component in components/. Consider moving to ui/ for better reusability.',
|
||||
noRawHtml: 'Raw HTML tags are forbidden in components and pages. Use UI components from ui/ instead.',
|
||||
noClassName: 'The className property is forbidden in components and pages. Use proper component props for styling.',
|
||||
noStyle: 'The style property is forbidden in components and pages. Use proper component props for styling.',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -33,10 +35,12 @@ module.exports = {
|
||||
const isInComponents = filename.includes('/components/');
|
||||
const isInApp = filename.includes('/app/');
|
||||
|
||||
if (!isInUi && !isInComponents) return {};
|
||||
if (!isInUi && !isInComponents && !isInApp) return {};
|
||||
|
||||
return {
|
||||
Program(node) {
|
||||
if (isInApp) return; // Don't check classification for app/ files
|
||||
|
||||
const sourceCode = context.getSourceCode();
|
||||
const text = sourceCode.getText();
|
||||
|
||||
@@ -63,18 +67,64 @@ module.exports = {
|
||||
messageId: 'pureComponentInComponents',
|
||||
});
|
||||
}
|
||||
|
||||
if (isInComponents && !hasState && !hasEffects && !hasContext) {
|
||||
// Check if it's mostly just rendering props
|
||||
const hasManyProps = /\{\s*\.\.\.props\s*\}/.test(text) ||
|
||||
/\{\s*props\./.test(text);
|
||||
|
||||
if (hasManyProps && hasNoLogic) {
|
||||
context.report({
|
||||
loc: { line: 1, column: 0 },
|
||||
messageId: 'uiShouldBePure',
|
||||
});
|
||||
},
|
||||
|
||||
JSXOpeningElement(node) {
|
||||
if (isInUi) return; // Allow raw HTML and className in ui/
|
||||
|
||||
let tagName = '';
|
||||
if (node.name.type === 'JSXIdentifier') {
|
||||
tagName = node.name.name;
|
||||
} else if (node.name.type === 'JSXMemberExpression') {
|
||||
tagName = node.name.property.name;
|
||||
}
|
||||
|
||||
if (!tagName) return;
|
||||
|
||||
// 1. Forbid raw HTML tags (lowercase)
|
||||
if (tagName[0] === tagName[0].toLowerCase()) {
|
||||
// Special case for html and body in RootLayout
|
||||
if (isInApp && (tagName === 'html' || tagName === 'body' || tagName === 'head' || tagName === 'meta' || tagName === 'link' || tagName === 'script')) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'noRawHtml',
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Forbid className property
|
||||
const classNameAttr = node.attributes.find(
|
||||
attr => attr.type === 'JSXAttribute' &&
|
||||
attr.name.type === 'JSXIdentifier' &&
|
||||
attr.name.name === 'className'
|
||||
);
|
||||
|
||||
if (classNameAttr) {
|
||||
// Special case for html and body in RootLayout
|
||||
if (isInApp && (tagName === 'html' || tagName === 'body')) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.report({
|
||||
node: classNameAttr,
|
||||
messageId: 'noClassName',
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Forbid style property
|
||||
const styleAttr = node.attributes.find(
|
||||
attr => attr.type === 'JSXAttribute' &&
|
||||
attr.name.type === 'JSXIdentifier' &&
|
||||
attr.name.name === 'style'
|
||||
);
|
||||
|
||||
if (styleAttr) {
|
||||
context.report({
|
||||
node: styleAttr,
|
||||
messageId: 'noStyle',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user