website refactor

This commit is contained in:
2026-01-12 13:02:36 +01:00
parent ce558ad51b
commit e3e451d959
52 changed files with 6398 additions and 2 deletions

View File

@@ -0,0 +1,95 @@
{
"extends": ["next/core-web-vitals"],
"ignorePatterns": [
"lib/types/generated/**",
"**/*.test.ts",
"**/*.test.tsx",
"eslint-rules/**"
],
"overrides": [
{
"files": ["lib/presenters/*.ts", "lib/presenters/*.tsx"],
"rules": {
"presenter-contract": "error",
"filename-presenter-match": "error"
}
},
{
"files": ["app/**/*.tsx"],
"rules": {
"template-no-direct-mutations": "error",
"template-no-side-effects": "error",
"template-no-async-render": "error",
"template-no-external-state": "error",
"template-no-global-objects": "error",
"template-no-mutation-props": "error",
"template-no-unsafe-html": "error"
}
},
{
"files": ["app/**/page.tsx", "app/**/layout.tsx"],
"rules": {
"rsc-no-container-manager": "error",
"rsc-no-page-data-fetcher": "error",
"rsc-no-view-models": "error",
"rsc-no-presenters": "error",
"rsc-no-intl": "error",
"rsc-no-sorting-filtering": "error",
"rsc-no-display-objects": "error",
"rsc-no-unsafe-services": "error",
"rsc-no-di": "error",
"rsc-no-local-helpers": "error",
"rsc-no-object-construction": "error",
"rsc-no-container-manager-calls": "error"
}
},
{
"files": ["lib/display-objects/**/*.ts", "lib/display-objects/**/*.tsx"],
"rules": {
"display-no-domain-models": "error",
"display-no-business-logic": "error",
"model-no-domain-in-display": "error"
}
},
{
"files": ["lib/page-queries/**/*.ts"],
"rules": {
"page-query-no-null-returns": "error",
"page-query-filename": "error",
"page-query-contract": "error",
"page-query-execute": "error",
"page-query-return-type": "error"
}
},
{
"files": ["lib/services/**/*.ts"],
"rules": {
"services-must-be-marked": "error",
"services-no-external-api": "error",
"services-must-be-pure": "error",
"filename-service-match": "error"
}
},
{
"files": ["app/**/*.tsx"],
"rules": {
"client-only-no-server-code": "error",
"client-only-must-have-directive": "error"
}
},
{
"files": ["lib/write-boundaries/**/*.ts"],
"rules": {
"write-boundary-no-direct-mutations": "error",
"write-boundary-must-use-repository": "error"
}
},
{
"files": ["lib/domain/**/*.ts", "lib/models/**/*.ts"],
"rules": {
"model-no-display-in-domain": "error"
}
}
],
"rulesdir": ["./eslint-rules"]
}

View File

@@ -7,7 +7,8 @@
"ignorePatterns": [
"lib/types/generated/**",
"**/*.test.ts",
"**/*.test.tsx"
"**/*.test.tsx",
"eslint-rules/**"
],
"overrides": [
{
@@ -25,13 +26,119 @@
"import/no-default-export": "off",
"no-restricted-syntax": "off"
}
},
{
"files": [
"lib/presenters/*.ts",
"lib/presenters/*.tsx"
],
"rules": {
"gridpilot-rules/presenter-contract": "error",
"gridpilot-rules/filename-presenter-match": "error"
}
},
{
"files": [
"app/**/*.tsx"
],
"rules": {
"gridpilot-rules/template-no-direct-mutations": "error",
"gridpilot-rules/template-no-side-effects": "error",
"gridpilot-rules/template-no-async-render": "error",
"gridpilot-rules/template-no-external-state": "error",
"gridpilot-rules/template-no-global-objects": "error",
"gridpilot-rules/template-no-mutation-props": "error",
"gridpilot-rules/template-no-unsafe-html": "error"
}
},
{
"files": [
"app/**/page.tsx",
"app/**/layout.tsx"
],
"rules": {
"gridpilot-rules/rsc-no-container-manager": "error",
"gridpilot-rules/rsc-no-page-data-fetcher": "error",
"gridpilot-rules/rsc-no-view-models": "error",
"gridpilot-rules/rsc-no-presenters": "error",
"gridpilot-rules/rsc-no-intl": "error",
"gridpilot-rules/rsc-no-sorting-filtering": "error",
"gridpilot-rules/rsc-no-display-objects": "error",
"gridpilot-rules/rsc-no-unsafe-services": "error",
"gridpilot-rules/rsc-no-di": "error",
"gridpilot-rules/rsc-no-local-helpers": "error",
"gridpilot-rules/rsc-no-object-construction": "error",
"gridpilot-rules/rsc-no-container-manager-calls": "error"
}
},
{
"files": [
"lib/display-objects/**/*.ts",
"lib/display-objects/**/*.tsx"
],
"rules": {
"gridpilot-rules/display-no-domain-models": "error",
"gridpilot-rules/display-no-business-logic": "error",
"gridpilot-rules/model-no-domain-in-display": "error"
}
},
{
"files": [
"lib/page-queries/**/*.ts"
],
"rules": {
"gridpilot-rules/page-query-no-null-returns": "error",
"gridpilot-rules/page-query-filename": "error",
"gridpilot-rules/page-query-contract": "error",
"gridpilot-rules/page-query-execute": "error",
"gridpilot-rules/page-query-return-type": "error"
}
},
{
"files": [
"lib/services/**/*.ts"
],
"rules": {
"gridpilot-rules/services-must-be-marked": "error",
"gridpilot-rules/services-no-external-api": "error",
"gridpilot-rules/services-must-be-pure": "error",
"gridpilot-rules/filename-service-match": "error"
}
},
{
"files": [
"app/**/*.tsx"
],
"rules": {
"gridpilot-rules/client-only-no-server-code": "error",
"gridpilot-rules/client-only-must-have-directive": "error"
}
},
{
"files": [
"lib/write-boundaries/**/*.ts"
],
"rules": {
"gridpilot-rules/write-boundary-no-direct-mutations": "error",
"gridpilot-rules/write-boundary-must-use-repository": "error"
}
},
{
"files": [
"lib/domain/**/*.ts",
"lib/models/**/*.ts"
],
"rules": {
"gridpilot-rules/model-no-display-in-domain": "error"
}
}
],
"plugins": [
"boundaries",
"import",
"@typescript-eslint",
"unused-imports"
"unused-imports",
"gridpilot-rules"
],
"root": true,
"rules": {

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,94 @@
/**
* ESLint rules for Client-Only Guardrails
*
* Enforces client-side only boundaries
*/
module.exports = {
// Rule 1: No server-side code in client-only files
'no-server-code-in-client-only': {
meta: {
type: 'problem',
docs: {
description: 'Forbid server-side code in client-only files',
category: 'Client-Only',
},
messages: {
message: 'Client-only files cannot contain server-side code',
},
},
create(context) {
return {
Program(node) {
const filename = context.getFilename();
if (filename.includes('/app/') &&
filename.endsWith('.tsx') &&
!filename.endsWith('page.tsx') &&
!filename.endsWith('layout.tsx')) {
const sourceCode = context.getSourceCode();
const text = sourceCode.getText();
// Check for server-side patterns
const serverPatterns = [
/getServerSideProps/,
/cookies\(\)/,
/headers\(\)/,
/next\/headers/,
];
for (const pattern of serverPatterns) {
if (pattern.test(text)) {
context.report({
loc: { line: 1, column: 0 },
messageId: 'message',
});
break;
}
}
}
},
};
},
},
// Rule 2: Client-only files must have 'use client' directive
'client-only-must-have-directive': {
meta: {
type: 'problem',
docs: {
description: 'Enforce use client directive',
category: 'Client-Only',
},
messages: {
message: 'Client-only files must have "use client" directive at the top',
},
},
create(context) {
return {
Program(node) {
const filename = context.getFilename();
if (filename.includes('/app/') &&
filename.endsWith('.tsx') &&
!filename.endsWith('page.tsx') &&
!filename.endsWith('layout.tsx')) {
const sourceCode = context.getSourceCode();
const firstComment = sourceCode.getAllComments()[0];
const hasDirective = firstComment &&
firstComment.type === 'Line' &&
firstComment.value.trim() === '"use client"';
if (!hasDirective) {
context.report({
loc: { line: 1, column: 0 },
messageId: 'message',
});
}
}
},
};
},
},
};

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,87 @@
/**
* ESLint rules for Display Object Guardrails
*
* Enforces display object boundaries and purity
*/
module.exports = {
// Rule 1: No IO in display objects
'no-io-in-display-objects': {
meta: {
type: 'problem',
docs: {
description: 'Forbid IO imports in display objects',
category: 'Display Objects',
},
messages: {
message: 'DisplayObjects cannot import from api, services, page-queries, or view-models',
},
},
create(context) {
const forbiddenPaths = [
'@/lib/api/',
'@/lib/services/',
'@/lib/page-queries/',
'@/lib/view-models/',
'@/lib/presenters/',
];
return {
ImportDeclaration(node) {
const importPath = node.source.value;
if (forbiddenPaths.some(path => importPath.includes(path)) &&
!isInComment(node)) {
context.report({
node,
messageId: 'message',
});
}
},
};
},
},
// Rule 2: No non-class display exports
'no-non-class-display-exports': {
meta: {
type: 'problem',
docs: {
description: 'Forbid non-class exports in display objects',
category: 'Display Objects',
},
messages: {
message: 'Display Objects must be class-based and export only classes',
},
},
create(context) {
return {
ExportNamedDeclaration(node) {
if (node.declaration &&
(node.declaration.type === 'FunctionDeclaration' ||
(node.declaration.type === 'VariableDeclaration' &&
!node.declaration.declarations.some(d => d.init && d.init.type === 'ClassExpression')))) {
context.report({
node,
messageId: 'message',
});
}
},
ExportDefaultDeclaration(node) {
if (node.declaration &&
node.declaration.type !== 'ClassDeclaration' &&
node.declaration.type !== 'ClassExpression') {
context.report({
node,
messageId: 'message',
});
}
},
};
},
},
};
// Helper functions
function isInComment(node) {
return false;
}

View File

@@ -0,0 +1,34 @@
/**
* Filename rule: Presenter filename must match class name
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce presenter filename matches class name',
category: 'Filename',
},
messages: {
message: 'Presenter filename must match class name (e.g., FooPresenter.ts contains class FooPresenter)',
},
},
create(context) {
return {
ClassDeclaration(node) {
const filename = context.getFilename();
if (filename.includes('/lib/presenters/') && filename.endsWith('.ts')) {
const expectedClassName = filename.split('/').pop().replace('.ts', '');
const actualClassName = node.id?.name;
if (actualClassName && actualClassName !== expectedClassName) {
context.report({
node,
messageId: 'message',
});
}
}
},
};
},
};

View File

@@ -0,0 +1,71 @@
/**
* ESLint rules for Filename Rules
*
* Enforces correct file naming conventions
*/
module.exports = {
// Rule 1: Presenter filename must match class name
'presenter-filename-must-match-class': {
meta: {
type: 'problem',
docs: {
description: 'Enforce presenter filename matches class name',
category: 'Filename',
},
messages: {
message: 'Presenter filename must match class name (e.g., FooPresenter.ts contains class FooPresenter)',
},
},
create(context) {
return {
ClassDeclaration(node) {
const filename = context.getFilename();
if (filename.includes('/lib/presenters/') && filename.endsWith('.ts')) {
const expectedClassName = filename.split('/').pop().replace('.ts', '');
const actualClassName = node.id?.name;
if (actualClassName && actualClassName !== expectedClassName) {
context.report({
node,
messageId: 'message',
});
}
}
},
};
},
},
// Rule 2: Service filename must match function name
'service-filename-must-match-function': {
meta: {
type: 'problem',
docs: {
description: 'Enforce service filename matches function name',
category: 'Filename',
},
messages: {
message: 'Service filename must match function name (e.g., getUser.ts contains function getUser)',
},
},
create(context) {
return {
FunctionDeclaration(node) {
const filename = context.getFilename();
if (filename.includes('/lib/services/') && filename.endsWith('.ts')) {
const expectedFunctionName = filename.split('/').pop().replace('.ts', '');
const actualFunctionName = node.id?.name;
if (actualFunctionName && actualFunctionName !== expectedFunctionName) {
context.report({
node,
messageId: 'message',
});
}
}
},
};
},
},
};

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,183 @@
/**
* GridPilot ESLint Rules Plugin
*
* Comprehensive architectural guardrails converted to ESLint rules
*
* Usage in .eslintrc.json:
* {
* "plugins": ["gridpilot-rules"],
* "rules": {
* "gridpilot-rules/presenter-contract": "error",
* "gridpilot-rules/rsc-no-container-manager": "error",
* ...
* }
* }
*/
const presenterContract = require('./presenter-contract');
const rscBoundaryRules = require('./rsc-boundary-rules');
const templatePurityRules = require('./template-purity-rules');
const displayObjectRules = require('./display-object-rules');
const pageQueryRules = require('./page-query-rules');
const servicesRules = require('./services-rules');
const clientOnlyRules = require('./client-only-rules');
const writeBoundaryRules = require('./write-boundary-rules');
const modelTaxonomyRules = require('./model-taxonomy-rules');
const filenameRules = require('./filename-rules');
module.exports = {
rules: {
// Presenter Contract
'presenter-contract': presenterContract,
// RSC Boundary Rules
'rsc-no-container-manager': rscBoundaryRules['no-container-manager-in-server'],
'rsc-no-page-data-fetcher': rscBoundaryRules['no-page-data-fetcher-fetch-in-server'],
'rsc-no-view-models': rscBoundaryRules['no-view-models-in-server'],
'rsc-no-presenters': rscBoundaryRules['no-presenters-in-server'],
'rsc-no-intl': rscBoundaryRules['no-intl-in-presentation'],
'rsc-no-sorting-filtering': rscBoundaryRules['no-sorting-filtering-in-server'],
'rsc-no-display-objects': rscBoundaryRules['no-display-objects-in-server'],
'rsc-no-unsafe-services': rscBoundaryRules['no-unsafe-services-in-server'],
'rsc-no-di': rscBoundaryRules['no-di-in-server'],
'rsc-no-local-helpers': rscBoundaryRules['no-local-helpers-in-server'],
'rsc-no-object-construction': rscBoundaryRules['no-object-construction-in-server'],
'rsc-no-container-manager-calls': rscBoundaryRules['no-container-manager-calls-in-server'],
// Template Purity Rules
'template-no-direct-mutations': templatePurityRules['no-view-models-in-templates'],
'template-no-side-effects': templatePurityRules['no-state-hooks-in-templates'],
'template-no-async-render': templatePurityRules['no-computations-in-templates'],
'template-no-external-state': templatePurityRules['no-restricted-imports-in-templates'],
'template-no-global-objects': templatePurityRules['no-invalid-template-signature'],
'template-no-mutation-props': templatePurityRules['no-template-helper-exports'],
'template-no-unsafe-html': templatePurityRules['invalid-template-filename'],
// Display Object Rules
'display-no-domain-models': displayObjectRules['no-io-in-display-objects'],
'display-no-business-logic': displayObjectRules['no-non-class-display-exports'],
// Page Query Rules
'page-query-no-null-returns': pageQueryRules['no-null-returns-in-page-queries'],
'page-query-filename': pageQueryRules['invalid-page-query-filename'],
'page-query-contract': pageQueryRules['pagequery-must-implement-contract'],
'page-query-execute': pageQueryRules['pagequery-must-have-execute'],
'page-query-return-type': pageQueryRules['pagequery-execute-return-type'],
// Services Rules
'services-must-be-marked': servicesRules['services-must-be-marked'],
'services-no-external-api': servicesRules['no-external-api-in-services'],
'services-must-be-pure': servicesRules['services-must-be-pure'],
// Client-Only Rules
'client-only-no-server-code': clientOnlyRules['no-server-code-in-client-only'],
'client-only-must-have-directive': clientOnlyRules['client-only-must-have-directive'],
// Write Boundary Rules
'write-boundary-no-direct-mutations': writeBoundaryRules['no-direct-mutations-in-write-boundaries'],
'write-boundary-must-use-repository': writeBoundaryRules['write-boundaries-must-use-repository'],
// Model Taxonomy Rules
'model-no-domain-in-display': modelTaxonomyRules['no-domain-models-in-display-objects'],
'model-no-display-in-domain': modelTaxonomyRules['no-display-objects-in-domain-models'],
// Filename Rules
'filename-presenter-match': filenameRules['presenter-filename-must-match-class'],
'filename-service-match': filenameRules['service-filename-must-match-function'],
},
// Configurations for different use cases
configs: {
// Recommended: All rules enabled
recommended: {
plugins: ['gridpilot-rules'],
rules: {
// Presenter
'gridpilot-rules/presenter-contract': 'error',
// RSC Boundary
'gridpilot-rules/rsc-no-container-manager': 'error',
'gridpilot-rules/rsc-no-page-data-fetcher': 'error',
'gridpilot-rules/rsc-no-view-models': 'error',
'gridpilot-rules/rsc-no-presenters': 'error',
'gridpilot-rules/rsc-no-intl': 'error',
'gridpilot-rules/rsc-no-sorting-filtering': 'error',
'gridpilot-rules/rsc-no-display-objects': 'error',
'gridpilot-rules/rsc-no-unsafe-services': 'error',
'gridpilot-rules/rsc-no-di': 'error',
'gridpilot-rules/rsc-no-local-helpers': 'error',
'gridpilot-rules/rsc-no-object-construction': 'error',
'gridpilot-rules/rsc-no-container-manager-calls': 'error',
// Template Purity
'gridpilot-rules/template-no-direct-mutations': 'error',
'gridpilot-rules/template-no-side-effects': 'error',
'gridpilot-rules/template-no-async-render': 'error',
'gridpilot-rules/template-no-external-state': 'error',
'gridpilot-rules/template-no-global-objects': 'error',
'gridpilot-rules/template-no-mutation-props': 'error',
'gridpilot-rules/template-no-unsafe-html': 'error',
// Display Objects
'gridpilot-rules/display-no-domain-models': 'error',
'gridpilot-rules/display-no-business-logic': 'error',
// Page Queries
'gridpilot-rules/page-query-no-null-returns': 'error',
'gridpilot-rules/page-query-filename': 'error',
'gridpilot-rules/page-query-contract': 'error',
'gridpilot-rules/page-query-execute': 'error',
'gridpilot-rules/page-query-return-type': 'error',
// Services
'gridpilot-rules/services-must-be-marked': 'error',
'gridpilot-rules/services-no-external-api': 'error',
'gridpilot-rules/services-must-be-pure': 'error',
// Client-Only
'gridpilot-rules/client-only-no-server-code': 'error',
'gridpilot-rules/client-only-must-have-directive': 'error',
// Write Boundaries
'gridpilot-rules/write-boundary-no-direct-mutations': 'error',
'gridpilot-rules/write-boundary-must-use-repository': 'error',
// Model Taxonomy
'gridpilot-rules/model-no-domain-in-display': 'error',
'gridpilot-rules/model-no-display-in-domain': 'error',
// Filename
'gridpilot-rules/filename-presenter-match': 'error',
'gridpilot-rules/filename-service-match': 'error',
},
},
// Presenter-only: Just the presenter rules
presenters: {
plugins: ['gridpilot-rules'],
rules: {
'gridpilot-rules/presenter-contract': 'error',
'gridpilot-rules/filename-presenter-match': 'error',
},
},
// RSC-only: Just RSC boundary rules
rsc: {
plugins: ['gridpilot-rules'],
rules: {
'gridpilot-rules/rsc-no-container-manager': 'error',
'gridpilot-rules/rsc-no-page-data-fetcher': 'error',
'gridpilot-rules/rsc-no-view-models': 'error',
'gridpilot-rules/rsc-no-presenters': 'error',
'gridpilot-rules/rsc-no-intl': 'error',
'gridpilot-rules/rsc-no-sorting-filtering': 'error',
'gridpilot-rules/rsc-no-display-objects': 'error',
'gridpilot-rules/rsc-no-unsafe-services': 'error',
'gridpilot-rules/rsc-no-di': 'error',
'gridpilot-rules/rsc-no-local-helpers': 'error',
'gridpilot-rules/rsc-no-object-construction': 'error',
'gridpilot-rules/rsc-no-container-manager-calls': 'error',
},
},
},
};

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,77 @@
/**
* ESLint rules for Model Taxonomy Guardrails
*
* Enforces model classification boundaries
*/
module.exports = {
// Rule 1: No domain models in display objects
'no-domain-models-in-display-objects': {
meta: {
type: 'problem',
docs: {
description: 'Forbid domain models in display objects',
category: 'Model Taxonomy',
},
messages: {
message: 'Display objects cannot contain domain models',
},
},
create(context) {
return {
ImportDeclaration(node) {
const filename = context.getFilename();
if (filename.includes('/lib/display-objects/')) {
const importPath = node.source.value;
// Check for domain model imports
if (importPath.includes('@/lib/domain/') ||
importPath.includes('@/lib/models/') ||
importPath.includes('Entity') ||
importPath.includes('Aggregate')) {
context.report({
node,
messageId: 'message',
});
}
}
},
};
},
},
// Rule 2: No display objects in domain models
'no-display-objects-in-domain-models': {
meta: {
type: 'problem',
docs: {
description: 'Forbid display objects in domain models',
category: 'Model Taxonomy',
},
messages: {
message: 'Domain models cannot contain display objects',
},
},
create(context) {
return {
ImportDeclaration(node) {
const filename = context.getFilename();
if (filename.includes('/lib/domain/') ||
filename.includes('/lib/models/')) {
const importPath = node.source.value;
// Check for display object imports
if (importPath.includes('@/lib/display-objects/') ||
importPath.includes('Component') ||
importPath.includes('View')) {
context.report({
node,
messageId: 'message',
});
}
}
},
};
},
},
};

View File

@@ -0,0 +1,13 @@
{
"name": "eslint-plugin-gridpilot-rules",
"version": "1.0.0",
"main": "index.js",
"type": "commonjs",
"description": "",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,167 @@
/**
* ESLint rules for Page Query Guardrails
*
* Enforces page query contracts and boundaries
*/
module.exports = {
// Rule 1: No null returns in page queries
'no-null-returns-in-page-queries': {
meta: {
type: 'problem',
docs: {
description: 'Forbid null returns in page queries',
category: 'Page Query',
},
messages: {
message: 'PageQueries must return PageQueryResult union, not null',
},
},
create(context) {
return {
ReturnStatement(node) {
if (node.argument &&
node.argument.type === 'Literal' &&
node.argument.value === null &&
!isInComment(node)) {
context.report({
node,
messageId: 'message',
});
}
},
};
},
},
// Rule 2: Invalid page query filename
'invalid-page-query-filename': {
meta: {
type: 'problem',
docs: {
description: 'Enforce correct page query filename',
category: 'Page Query',
},
messages: {
message: 'PageQuery files must end with PageQuery.ts',
},
},
create(context) {
const filename = context.getFilename();
if (filename.includes('/page-queries/') && !filename.endsWith('PageQuery.ts')) {
context.report({
loc: { line: 1, column: 0 },
messageId: 'message',
});
}
return {};
},
},
// Rule 3: PageQuery must implement contract
'pagequery-must-implement-contract': {
meta: {
type: 'problem',
docs: {
description: 'Enforce PageQuery interface implementation',
category: 'Page Query',
},
messages: {
message: 'PageQuery class must implement PageQuery<TPageDto, TParams> interface',
},
},
create(context) {
return {
ClassDeclaration(node) {
const className = node.id?.name;
if (className && className.endsWith('PageQuery')) {
if (!node.implements ||
!node.implements.some(impl =>
impl.expression.type === 'GenericIdentifier' &&
impl.expression.name === 'PageQuery')) {
context.report({
node,
messageId: 'message',
});
}
}
},
};
},
},
// Rule 4: PageQuery must have execute method
'pagequery-must-have-execute': {
meta: {
type: 'problem',
docs: {
description: 'Enforce PageQuery execute method',
category: 'Page Query',
},
messages: {
message: 'PageQuery class must have execute(params) method',
},
},
create(context) {
return {
ClassDeclaration(node) {
const className = node.id?.name;
if (className && className.endsWith('PageQuery')) {
const hasExecute = node.body.body.some(member =>
member.type === 'MethodDefinition' &&
member.key.type === 'Identifier' &&
member.key.name === 'execute'
);
if (!hasExecute) {
context.report({
node,
messageId: 'message',
});
}
}
},
};
},
},
// Rule 5: PageQuery execute return type
'pagequery-execute-return-type': {
meta: {
type: 'problem',
docs: {
description: 'Enforce PageQuery execute return type',
category: 'Page Query',
},
messages: {
message: 'PageQuery execute() must return Promise<PageQueryResult<TPageDto>>',
},
},
create(context) {
return {
MethodDefinition(node) {
if (node.key.type === 'Identifier' &&
node.key.name === 'execute' &&
node.value.type === 'FunctionExpression') {
const returnType = node.value.returnType;
if (!returnType ||
!returnType.typeAnnotation ||
!returnType.typeAnnotation.typeName ||
returnType.typeAnnotation.typeName.name !== 'Promise') {
context.report({
node,
messageId: 'message',
});
}
}
},
};
},
},
};
// Helper functions
function isInComment(node) {
return false;
}

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,370 @@
/**
* ESLint rules for RSC Boundary Guardrails
*
* Enforces server-side code boundaries in Next.js app directory
*/
module.exports = {
// Rule 1: No ContainerManager in server code
'no-container-manager-in-server': {
meta: {
type: 'problem',
docs: {
description: 'Forbid ContainerManager usage in server code',
category: 'RSC Boundary',
},
messages: {
message: 'ContainerManager usage forbidden in server code',
},
},
create(context) {
return {
Identifier(node) {
if (node.name === 'ContainerManager' && !isInComment(node)) {
context.report({
node,
messageId: 'message',
});
}
},
};
},
},
// Rule 2: No PageDataFetcher.fetch() in server code
'no-page-data-fetcher-fetch-in-server': {
meta: {
type: 'problem',
docs: {
description: 'Forbid PageDataFetcher.fetch() in server code',
category: 'RSC Boundary',
},
messages: {
message: 'PageDataFetcher.fetch() forbidden in server code',
},
},
create(context) {
return {
CallExpression(node) {
if (node.callee.type === 'MemberExpression' &&
node.callee.object.name === 'PageDataFetcher' &&
node.callee.property.name === 'fetch' &&
!isInComment(node)) {
context.report({
node,
messageId: 'message',
});
}
},
};
},
},
// Rule 3: No ViewModels/ViewModels imports in server code
'no-view-models-in-server': {
meta: {
type: 'problem',
docs: {
description: 'Forbid ViewModels/Presenters imports in server code',
category: 'RSC Boundary',
},
messages: {
message: 'ViewModels or Presenters import forbidden in server code',
},
},
create(context) {
return {
ImportDeclaration(node) {
const importPath = node.source.value;
if ((importPath.includes('@/lib/view-models/') ||
importPath.includes('@/lib/presenters/')) &&
!isInComment(node)) {
context.report({
node,
messageId: 'message',
});
}
},
};
},
},
// Rule 4: No Presenter imports in server code
'no-presenters-in-server': {
meta: {
type: 'problem',
docs: {
description: 'Forbid Presenter imports in server code',
category: 'RSC Boundary',
},
messages: {
message: 'Presenter import forbidden in server code',
},
},
create(context) {
return {
ImportDeclaration(node) {
const importPath = node.source.value;
if ((importPath.includes('@/lib/presenters/') ||
importPath.includes('@/lib/presenters/')) &&
!isInComment(node)) {
context.report({
node,
messageId: 'message',
});
}
},
};
},
},
// Rule 5: No Intl usage in presentation paths
'no-intl-in-presentation': {
meta: {
type: 'problem',
docs: {
description: 'Forbid Intl.* or toLocale* usage in presentation paths',
category: 'RSC Boundary',
},
messages: {
message: 'Intl.* or toLocale* usage forbidden in presentation paths',
},
},
create(context) {
return {
MemberExpression(node) {
if ((node.object.name === 'Intl' ||
(node.property && node.property.name && node.property.name.startsWith('toLocale'))) &&
!isInComment(node)) {
context.report({
node,
messageId: 'message',
});
}
},
};
},
},
// Rule 6: No sorting/filtering/reduce in server code
'no-sorting-filtering-in-server': {
meta: {
type: 'problem',
docs: {
description: 'Forbid sorting/filtering/reduce operations in server code',
category: 'RSC Boundary',
},
messages: {
message: 'Sorting/filtering/reduce operations forbidden in server code',
},
},
create(context) {
return {
CallExpression(node) {
if (node.callee.type === 'MemberExpression' &&
['sort', 'filter', 'reduce'].includes(node.callee.property.name) &&
!isInComment(node) &&
!node.loc.start.line.toString().includes('null check')) {
context.report({
node,
messageId: 'message',
});
}
},
};
},
},
// Rule 7: No DisplayObjects imports in server code
'no-display-objects-in-server': {
meta: {
type: 'problem',
docs: {
description: 'Forbid DisplayObjects imports in server code',
category: 'RSC Boundary',
},
messages: {
message: 'DisplayObjects import forbidden in server code',
},
},
create(context) {
return {
ImportDeclaration(node) {
const importPath = node.source.value;
if (importPath.includes('@/lib/display-objects/') &&
!isInComment(node)) {
context.report({
node,
messageId: 'message',
});
}
},
};
},
},
// Rule 8: No unsafe services imports in server code
'no-unsafe-services-in-server': {
meta: {
type: 'problem',
docs: {
description: 'Forbid unsafe services imports in server code',
category: 'RSC Boundary',
},
messages: {
message: 'Services import must be explicitly marked as server-safe',
},
},
create(context) {
return {
ImportDeclaration(node) {
const importPath = node.source.value;
if (importPath.includes('@/lib/services/') &&
!isInComment(node) &&
!isMarkedServerSafe(context.getSourceCode().getText(node))) {
context.report({
node,
messageId: 'message',
});
}
},
};
},
},
// Rule 9: No DI imports in server code
'no-di-in-server': {
meta: {
type: 'problem',
docs: {
description: 'Forbid DI imports in server code',
category: 'RSC Boundary',
},
messages: {
message: 'DI import forbidden in server code',
},
},
create(context) {
return {
ImportDeclaration(node) {
const importPath = node.source.value;
if (importPath.includes('@/lib/di/') &&
!isInComment(node)) {
context.report({
node,
messageId: 'message',
});
}
},
};
},
},
// Rule 10: No local helpers in server code
'no-local-helpers-in-server': {
meta: {
type: 'problem',
docs: {
description: 'Forbid local helper functions in server code',
category: 'RSC Boundary',
},
messages: {
message: 'Local helper functions forbidden (only assert*/invariant* allowed)',
},
},
create(context) {
return {
FunctionDeclaration(node) {
if (!node.id.name.startsWith('assert') &&
!node.id.name.startsWith('invariant') &&
!isInComment(node)) {
context.report({
node,
messageId: 'message',
});
}
},
VariableDeclarator(node) {
if (node.init &&
(node.init.type === 'FunctionExpression' || node.init.type === 'ArrowFunctionExpression') &&
!node.id.name.startsWith('assert') &&
!node.id.name.startsWith('invariant') &&
!isInComment(node)) {
context.report({
node,
messageId: 'message',
});
}
},
};
},
},
// Rule 11: No object construction in server code
'no-object-construction-in-server': {
meta: {
type: 'problem',
docs: {
description: 'Forbid object construction with new in server code',
category: 'RSC Boundary',
},
messages: {
message: 'Object construction with new forbidden (use PageQueries)',
},
},
create(context) {
return {
NewExpression(node) {
if (node.callee.type === 'Identifier' &&
/^[A-Z]/.test(node.callee.name) &&
!isInComment(node)) {
context.report({
node,
messageId: 'message',
});
}
},
};
},
},
// Rule 12: No ContainerManager calls in server code
'no-container-manager-calls-in-server': {
meta: {
type: 'problem',
docs: {
description: 'Forbid ContainerManager calls in server code',
category: 'RSC Boundary',
},
messages: {
message: 'ContainerManager calls forbidden in server code',
},
},
create(context) {
return {
CallExpression(node) {
if (node.callee.type === 'MemberExpression' &&
node.callee.object.name === 'ContainerManager' &&
(node.callee.property.name === 'getInstance' ||
node.callee.property.name === 'getContainer') &&
!isInComment(node)) {
context.report({
node,
messageId: 'message',
});
}
},
};
},
},
};
// Helper functions
function isInComment(node) {
// This is a simplified check - in practice you'd need to check the actual comment
return false;
}
function isMarkedServerSafe(text) {
return text.includes('// @server-safe') || text.includes('/* @server-safe */');
}

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,156 @@
/**
* ESLint rules for Services Guardrails
*
* Enforces service contracts and boundaries
*/
module.exports = {
// Rule 1: Services must be marked with @server-safe or @client-only
'services-must-be-marked': {
meta: {
type: 'problem',
docs: {
description: 'Enforce service safety marking',
category: 'Services',
},
messages: {
message: 'Services must be explicitly marked with @server-safe or @client-only comment',
},
},
create(context) {
return {
Program(node) {
const filename = context.getFilename();
if (filename.includes('/lib/services/') && filename.endsWith('.ts')) {
const sourceCode = context.getSourceCode();
const text = sourceCode.getText();
const hasServerSafe = text.includes('@server-safe');
const hasClientOnly = text.includes('@client-only');
if (!hasServerSafe && !hasClientOnly) {
context.report({
loc: { line: 1, column: 0 },
messageId: 'message',
});
}
}
},
};
},
},
// Rule 2: No external API calls in services
'no-external-api-in-services': {
meta: {
type: 'problem',
docs: {
description: 'Forbid external API calls in services',
category: 'Services',
},
messages: {
message: 'External API calls must be in adapters, not services',
},
},
create(context) {
return {
CallExpression(node) {
const filename = context.getFilename();
if (filename.includes('/lib/services/')) {
// Check for fetch, axios, or other HTTP calls
if (node.callee.type === 'Identifier' &&
['fetch', 'axios'].includes(node.callee.name) &&
!isInComment(node)) {
context.report({
node,
messageId: 'message',
});
}
// Check for external API URLs
if (node.arguments.length > 0) {
const firstArg = node.arguments[0];
if (firstArg.type === 'Literal' &&
typeof firstArg.value === 'string' &&
(firstArg.value.startsWith('http') ||
firstArg.value.includes('api.') ||
firstArg.value.includes('.com'))) {
context.report({
node,
messageId: 'message',
});
}
}
}
},
};
},
},
// Rule 3: Services must be pure functions
'services-must-be-pure': {
meta: {
type: 'problem',
docs: {
description: 'Enforce service purity',
category: 'Services',
},
messages: {
message: 'Services must be pure functions, no side effects allowed',
},
},
create(context) {
return {
CallExpression(node) {
const filename = context.getFilename();
if (filename.includes('/lib/services/')) {
// Check for common side effects
if (node.callee.type === 'MemberExpression') {
const object = node.callee.object;
const property = node.callee.property;
// DOM manipulation
if (object.type === 'Identifier' &&
['document', 'window'].includes(object.name)) {
context.report({
node,
messageId: 'message',
});
}
// State mutation
if (property.type === 'Identifier' &&
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].includes(property.name)) {
context.report({
node,
messageId: 'message',
});
}
}
// Direct assignment to external state
if (node.type === 'AssignmentExpression' &&
node.left.type === 'MemberExpression' &&
node.left.object.type === 'Identifier' &&
!isInFunctionScope(node)) {
context.report({
node,
messageId: 'message',
});
}
}
},
};
},
},
};
// Helper functions
function isInComment(node) {
return false;
}
function isInFunctionScope(node) {
// Simplified check
return false;
}

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View 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;
}

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,125 @@
/**
* ESLint rule to enforce Presenter contract
*
* Enforces that classes ending with "Presenter" must:
* 1. Implement Presenter<TInput, TOutput> interface
* 2. Have a present(input) method
* 3. Have 'use client' directive
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce Presenter contract implementation',
category: 'Best Practices',
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',
},
},
create(context) {
const sourceCode = context.getSourceCode();
let hasUseClient = false;
let presenterClassNode = null;
let hasPresentMethod = false;
let hasImplements = false;
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;
}
}
// 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;
}
}
},
// 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) {
context.report({
node: presenterClassNode,
messageId: 'missingImplements',
});
}
if (!hasPresentMethod) {
context.report({
node: presenterClassNode,
messageId: 'missingPresentMethod',
});
}
if (!hasUseClient) {
context.report({
node: presenterClassNode,
messageId: 'missingUseClient',
});
}
},
};
},
};

View File

@@ -0,0 +1,99 @@
/**
* ESLint rules for Write Boundary Guardrails
*
* Enforces write operation boundaries
*/
module.exports = {
// Rule 1: No direct mutations in write boundaries
'no-direct-mutations-in-write-boundaries': {
meta: {
type: 'problem',
docs: {
description: 'Forbid direct mutations in write boundaries',
category: 'Write Boundary',
},
messages: {
message: 'Write boundaries must use mutation functions, not direct mutations',
},
},
create(context) {
return {
AssignmentExpression(node) {
const filename = context.getFilename();
if (filename.includes('/lib/write-boundaries/')) {
// Check for direct property assignment
if (node.left.type === 'MemberExpression' &&
!isInMutationFunction(node)) {
context.report({
node,
messageId: 'message',
});
}
}
},
UpdateExpression(node) {
const filename = context.getFilename();
if (filename.includes('/lib/write-boundaries/') &&
!isInMutationFunction(node)) {
context.report({
node,
messageId: 'message',
});
}
},
};
},
},
// Rule 2: Write boundaries must use repository pattern
'write-boundaries-must-use-repository': {
meta: {
type: 'problem',
docs: {
description: 'Enforce repository pattern in write boundaries',
category: 'Write Boundary',
},
messages: {
message: 'Write boundaries must use repository pattern for data access',
},
},
create(context) {
return {
CallExpression(node) {
const filename = context.getFilename();
if (filename.includes('/lib/write-boundaries/')) {
// Check for direct database access
if (node.callee.type === 'MemberExpression' &&
node.callee.object.type === 'Identifier' &&
['db', 'prisma', 'knex'].includes(node.callee.object.name) &&
!isInComment(node)) {
context.report({
node,
messageId: 'message',
});
}
}
},
};
},
},
};
// Helper functions
function isInMutationFunction(node) {
// Check if node is inside a mutation function
let current = node;
while (current) {
if (current.type === 'FunctionDeclaration' || current.type === 'FunctionExpression') {
const name = current.id?.name || '';
return name.includes('mutate') || name.includes('update') || name.includes('create');
}
current = current.parent;
}
return false;
}
function isInComment(node) {
return false;
}

View File

@@ -7,6 +7,8 @@
"build": "next build",
"start": "next start",
"lint": "eslint . --ext .ts,.tsx --max-warnings 0",
"lint:presenters": "eslint lib/presenters/*.ts",
"lint:all": "eslint .",
"type-check": "npx tsc --noEmit",
"clean": "rm -rf .next"
},
@@ -38,6 +40,7 @@
"eslint-config-next": "15.5.7",
"eslint-import-resolver-typescript": "2.7.1",
"eslint-plugin-boundaries": "^5.3.1",
"eslint-plugin-gridpilot-rules": "file:eslint-rules",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-unused-imports": "^3.0.0",
"typescript": "^5.6.0"