website refactor
This commit is contained in:
215
apps/website/eslint-rules/template-purity-rules.js
Normal file
215
apps/website/eslint-rules/template-purity-rules.js
Normal file
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* ESLint rules for Template Purity Guardrails
|
||||
*
|
||||
* Enforces pure template components without business logic
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
// Rule 1: No ViewModels/DisplayObjects in templates
|
||||
'no-view-models-in-templates': {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Forbid ViewModels/DisplayObjects imports in templates',
|
||||
category: 'Template Purity',
|
||||
},
|
||||
messages: {
|
||||
message: 'ViewModels or DisplayObjects import forbidden in templates',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
const importPath = node.source.value;
|
||||
if ((importPath.includes('@/lib/view-models/') ||
|
||||
importPath.includes('@/lib/presenters/') ||
|
||||
importPath.includes('@/lib/display-objects/')) &&
|
||||
!isInComment(node)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'message',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// Rule 2: No state hooks in templates
|
||||
'no-state-hooks-in-templates': {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Forbid state hooks in templates',
|
||||
category: 'Template Purity',
|
||||
},
|
||||
messages: {
|
||||
message: 'State hooks forbidden in templates (use *PageClient.tsx)',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
CallExpression(node) {
|
||||
if (node.callee.type === 'Identifier' &&
|
||||
['useMemo', 'useEffect', 'useState', 'useReducer'].includes(node.callee.name) &&
|
||||
!isInComment(node)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'message',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// Rule 3: No computations in templates
|
||||
'no-computations-in-templates': {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Forbid derived computations in templates',
|
||||
category: 'Template Purity',
|
||||
},
|
||||
messages: {
|
||||
message: 'Derived computations forbidden in templates',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
CallExpression(node) {
|
||||
if (node.callee.type === 'MemberExpression' &&
|
||||
['filter', 'sort', 'reduce'].includes(node.callee.property.name) &&
|
||||
!isInComment(node)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'message',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// Rule 4: No restricted imports in templates
|
||||
'no-restricted-imports-in-templates': {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Forbid restricted imports in templates',
|
||||
category: 'Template Purity',
|
||||
},
|
||||
messages: {
|
||||
message: 'Templates cannot import from page-queries, services, api, di, or contracts',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const restrictedPaths = [
|
||||
'@/lib/page-queries/',
|
||||
'@/lib/services/',
|
||||
'@/lib/api/',
|
||||
'@/lib/di/',
|
||||
'@/lib/contracts/',
|
||||
];
|
||||
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
const importPath = node.source.value;
|
||||
if (restrictedPaths.some(path => importPath.includes(path)) &&
|
||||
!isInComment(node)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'message',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// Rule 5: Invalid template signature
|
||||
'no-invalid-template-signature': {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Enforce correct template component signature',
|
||||
category: 'Template Purity',
|
||||
},
|
||||
messages: {
|
||||
message: 'Template component must accept *ViewData type as first parameter',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
FunctionDeclaration(node) {
|
||||
if (node.params.length === 0 ||
|
||||
!node.params[0].typeAnnotation ||
|
||||
!node.params[0].typeAnnotation.typeAnnotation.type.includes('ViewData')) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'message',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// Rule 6: No template helper exports
|
||||
'no-template-helper-exports': {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Forbid helper function exports in templates',
|
||||
category: 'Template Purity',
|
||||
},
|
||||
messages: {
|
||||
message: 'Templates must not export helper functions',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
ExportNamedDeclaration(node) {
|
||||
if (node.declaration &&
|
||||
(node.declaration.type === 'FunctionDeclaration' ||
|
||||
node.declaration.type === 'VariableDeclaration')) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'message',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// Rule 7: Invalid template filename
|
||||
'invalid-template-filename': {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Enforce correct template filename',
|
||||
category: 'Template Purity',
|
||||
},
|
||||
messages: {
|
||||
message: 'Template files must end with Template.tsx',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const filename = context.getFilename();
|
||||
if (filename.includes('/templates/') && !filename.endsWith('Template.tsx')) {
|
||||
// Report at the top of the file
|
||||
context.report({
|
||||
loc: { line: 1, column: 0 },
|
||||
messageId: 'message',
|
||||
});
|
||||
}
|
||||
return {};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Helper functions
|
||||
function isInComment(node) {
|
||||
return false;
|
||||
}
|
||||
Reference in New Issue
Block a user