website refactor

This commit is contained in:
2026-01-14 23:46:04 +01:00
parent c1a86348d7
commit 4a2d7d15a5
294 changed files with 5637 additions and 3418 deletions

View File

@@ -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',
});
}
},
};