website refactor
This commit is contained in:
@@ -1,125 +1,80 @@
|
||||
/**
|
||||
* ESLint rule to enforce Presenter contract
|
||||
* ESLint rule to forbid raw HTML and className in templates
|
||||
*
|
||||
* Enforces that classes ending with "Presenter" must:
|
||||
* 1. Implement Presenter<TInput, TOutput> interface
|
||||
* 2. Have a present(input) method
|
||||
* 3. Have 'use client' directive
|
||||
* Templates must use proper reusable UI components instead of low level html and css.
|
||||
* To avoid workarounds using `Box` with tailwind classes, the `className` property is forbidden.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Enforce Presenter contract implementation',
|
||||
category: 'Best Practices',
|
||||
description: 'Forbid raw HTML and className in templates',
|
||||
category: 'Architecture',
|
||||
recommended: true,
|
||||
},
|
||||
fixable: null,
|
||||
schema: [],
|
||||
messages: {
|
||||
missingImplements: 'Presenter class must implement Presenter<TInput, TOutput> interface',
|
||||
missingPresentMethod: 'Presenter class must have present(input) method',
|
||||
missingUseClient: 'Presenter must have \'use client\' directive at top-level',
|
||||
noRawHtml: 'Raw HTML tags are forbidden in templates. Use UI components from ui/ instead.',
|
||||
noClassName: 'The className property is forbidden in templates. Use proper component props for styling.',
|
||||
noStyle: 'The style property is forbidden in templates. Use proper component props for styling.',
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.getSourceCode();
|
||||
let hasUseClient = false;
|
||||
let presenterClassNode = null;
|
||||
let hasPresentMethod = false;
|
||||
let hasImplements = false;
|
||||
const filename = context.getFilename();
|
||||
const isInTemplates = filename.includes('/templates/');
|
||||
|
||||
if (!isInTemplates) return {};
|
||||
|
||||
return {
|
||||
// Check for 'use client' directive
|
||||
Program(node) {
|
||||
// Check comments at the top
|
||||
const comments = sourceCode.getAllComments();
|
||||
if (comments.length > 0) {
|
||||
const firstComment = comments[0];
|
||||
if (firstComment.type === 'Line' && firstComment.value.trim() === 'use client') {
|
||||
hasUseClient = true;
|
||||
} else if (firstComment.type === 'Block' && firstComment.value.includes('use client')) {
|
||||
hasUseClient = true;
|
||||
}
|
||||
JSXOpeningElement(node) {
|
||||
let tagName = '';
|
||||
if (node.name.type === 'JSXIdentifier') {
|
||||
tagName = node.name.name;
|
||||
} else if (node.name.type === 'JSXMemberExpression') {
|
||||
tagName = node.name.property.name;
|
||||
}
|
||||
|
||||
// Also check for 'use client' string literal as first statement
|
||||
if (node.body.length > 0) {
|
||||
const firstStmt = node.body[0];
|
||||
if (firstStmt &&
|
||||
firstStmt.type === 'ExpressionStatement' &&
|
||||
firstStmt.expression.type === 'Literal' &&
|
||||
firstStmt.expression.value === 'use client') {
|
||||
hasUseClient = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
if (!tagName) return;
|
||||
|
||||
// Find Presenter classes
|
||||
ClassDeclaration(node) {
|
||||
const className = node.id?.name;
|
||||
|
||||
// Check if this is a Presenter class
|
||||
if (className && className.endsWith('Presenter')) {
|
||||
presenterClassNode = node;
|
||||
|
||||
// Check if it implements any interface
|
||||
if (node.implements && node.implements.length > 0) {
|
||||
for (const impl of node.implements) {
|
||||
// Handle GenericTypeAnnotation for Presenter<TInput, TOutput>
|
||||
if (impl.expression.type === 'TSInstantiationExpression') {
|
||||
const expr = impl.expression.expression;
|
||||
if (expr.type === 'Identifier' && expr.name === 'Presenter') {
|
||||
hasImplements = true;
|
||||
}
|
||||
} else if (impl.expression.type === 'Identifier') {
|
||||
// Handle simple Presenter (without generics)
|
||||
if (impl.expression.name === 'Presenter') {
|
||||
hasImplements = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Check for present method in classes
|
||||
MethodDefinition(node) {
|
||||
if (presenterClassNode &&
|
||||
node.key.type === 'Identifier' &&
|
||||
node.key.name === 'present' &&
|
||||
node.parent === presenterClassNode) {
|
||||
hasPresentMethod = true;
|
||||
}
|
||||
},
|
||||
|
||||
// Report violations at the end
|
||||
'Program:exit'() {
|
||||
if (!presenterClassNode) return;
|
||||
|
||||
if (!hasImplements) {
|
||||
// 1. Forbid raw HTML tags (lowercase)
|
||||
if (tagName[0] === tagName[0].toLowerCase()) {
|
||||
context.report({
|
||||
node: presenterClassNode,
|
||||
messageId: 'missingImplements',
|
||||
node,
|
||||
messageId: 'noRawHtml',
|
||||
});
|
||||
}
|
||||
|
||||
if (!hasPresentMethod) {
|
||||
// 2. Forbid className property
|
||||
const classNameAttr = node.attributes.find(
|
||||
attr => attr.type === 'JSXAttribute' &&
|
||||
attr.name.type === 'JSXIdentifier' &&
|
||||
attr.name.name === 'className'
|
||||
);
|
||||
|
||||
if (classNameAttr) {
|
||||
context.report({
|
||||
node: presenterClassNode,
|
||||
messageId: 'missingPresentMethod',
|
||||
node: classNameAttr,
|
||||
messageId: 'noClassName',
|
||||
});
|
||||
}
|
||||
|
||||
if (!hasUseClient) {
|
||||
// 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: presenterClassNode,
|
||||
messageId: 'missingUseClient',
|
||||
node: styleAttr,
|
||||
messageId: 'noStyle',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user