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,22 +1,23 @@
/**
* ESLint rule to enforce UI element purity
* ESLint rule to enforce UI element purity and architectural boundaries
*
* UI elements in ui/ must be:
* - Stateless (no useState, useReducer)
* - No side effects (no useEffect)
* - Pure functions that only render based on props
* - Isolated (cannot import from outside the ui/ directory)
*
* Rationale:
* - ui/ is for reusable, pure presentation elements
* - Stateful logic belongs in components/ or hooks/
* - This ensures maximum reusability and testability
* - Isolation ensures UI elements don't depend on app-specific logic
* - Raw HTML and className are allowed in ui/ for implementation
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce UI elements are pure and stateless',
description: 'Enforce UI elements are pure and isolated',
category: 'Architecture',
recommended: true,
},
@@ -26,6 +27,7 @@ module.exports = {
noStateInUi: 'UI elements in ui/ must be stateless. Use components/ for stateful wrappers.',
noHooksInUi: 'UI elements must not use hooks. Use components/ or hooks/ for stateful logic.',
noSideEffects: 'UI elements must not have side effects. Use components/ for side effect logic.',
noExternalImports: 'UI elements in ui/ cannot import from outside the ui/ directory. Only npm packages and other UI elements are allowed.',
},
},
@@ -36,6 +38,38 @@ module.exports = {
if (!isInUi) return {};
return {
// Check for imports from outside ui/
ImportDeclaration(node) {
const importPath = node.source.value;
// Allow npm packages (don't start with . or @/)
if (!importPath.startsWith('.') && !importPath.startsWith('@/')) {
return;
}
// Check internal imports
const isInternalUiImport = importPath.startsWith('@/ui/') ||
(importPath.startsWith('.') && !importPath.includes('..'));
// Special case for relative imports that stay within ui/
let staysInUi = false;
if (importPath.startsWith('.')) {
const path = require('path');
const absoluteImportPath = path.resolve(path.dirname(filename), importPath);
const uiDir = filename.split('/ui/')[0] + '/ui';
if (absoluteImportPath.startsWith(uiDir)) {
staysInUi = true;
}
}
if (!isInternalUiImport && !staysInUi) {
context.report({
node,
messageId: 'noExternalImports',
});
}
},
// Check for 'use client' directive
ExpressionStatement(node) {
if (node.expression.type === 'Literal' &&
@@ -77,31 +111,6 @@ module.exports = {
});
}
},
// Check for class components with state
ClassDeclaration(node) {
if (node.superClass &&
node.superClass.type === 'Identifier' &&
node.superClass.name === 'Component') {
context.report({
node,
messageId: 'noStateInUi',
});
}
},
// Check for direct state assignment (rare but possible)
AssignmentExpression(node) {
if (node.left.type === 'MemberExpression' &&
node.left.property.type === 'Identifier' &&
(node.left.property.name === 'state' ||
node.left.property.name === 'setState')) {
context.report({
node,
messageId: 'noStateInUi',
});
}
},
};
},
};