From e3e451d95958c737d8c9324a5afc1d670aecc5f9 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Mon, 12 Jan 2026 13:02:36 +0100 Subject: [PATCH] website refactor --- apps/website/.eslintrc.custom.json | 95 +++++ apps/website/.eslintrc.json | 111 +++++- .../client-only-must-have-directive.js | 125 ++++++ .../client-only-no-server-code.js | 125 ++++++ .../website/eslint-rules/client-only-rules.js | 94 +++++ .../eslint-rules/display-no-business-logic.js | 125 ++++++ .../eslint-rules/display-no-domain-models.js | 125 ++++++ .../eslint-rules/display-object-rules.js | 87 ++++ .../eslint-rules/filename-presenter-match.js | 34 ++ apps/website/eslint-rules/filename-rules.js | 71 ++++ .../eslint-rules/filename-service-match.js | 125 ++++++ apps/website/eslint-rules/index.js | 183 +++++++++ .../model-no-display-in-domain.js | 125 ++++++ .../model-no-domain-in-display.js | 125 ++++++ .../eslint-rules/model-taxonomy-rules.js | 77 ++++ apps/website/eslint-rules/package.json | 13 + .../eslint-rules/page-query-contract.js | 125 ++++++ .../eslint-rules/page-query-execute.js | 125 ++++++ .../eslint-rules/page-query-filename.js | 125 ++++++ .../page-query-no-null-returns.js | 125 ++++++ .../eslint-rules/page-query-return-type.js | 125 ++++++ apps/website/eslint-rules/page-query-rules.js | 167 ++++++++ .../eslint-rules/presenter-contract.js | 125 ++++++ .../eslint-rules/rsc-boundary-rules.js | 370 ++++++++++++++++++ .../rsc-no-container-manager-calls.js | 125 ++++++ .../eslint-rules/rsc-no-container-manager.js | 125 ++++++ apps/website/eslint-rules/rsc-no-di.js | 125 ++++++ .../eslint-rules/rsc-no-display-objects.js | 125 ++++++ apps/website/eslint-rules/rsc-no-intl.js | 125 ++++++ .../eslint-rules/rsc-no-local-helpers.js | 125 ++++++ .../rsc-no-object-construction.js | 125 ++++++ .../eslint-rules/rsc-no-page-data-fetcher.js | 125 ++++++ .../website/eslint-rules/rsc-no-presenters.js | 125 ++++++ .../eslint-rules/rsc-no-sorting-filtering.js | 125 ++++++ .../eslint-rules/rsc-no-unsafe-services.js | 125 ++++++ .../eslint-rules/rsc-no-view-models.js | 125 ++++++ .../eslint-rules/services-must-be-marked.js | 125 ++++++ .../eslint-rules/services-must-be-pure.js | 125 ++++++ .../eslint-rules/services-no-external-api.js | 125 ++++++ apps/website/eslint-rules/services-rules.js | 156 ++++++++ .../eslint-rules/template-no-async-render.js | 125 ++++++ .../template-no-direct-mutations.js | 125 ++++++ .../template-no-external-state.js | 125 ++++++ .../template-no-global-objects.js | 125 ++++++ .../template-no-mutation-props.js | 125 ++++++ .../eslint-rules/template-no-side-effects.js | 125 ++++++ .../eslint-rules/template-no-unsafe-html.js | 125 ++++++ .../eslint-rules/template-purity-rules.js | 215 ++++++++++ .../write-boundary-must-use-repository.js | 125 ++++++ .../write-boundary-no-direct-mutations.js | 125 ++++++ .../eslint-rules/write-boundary-rules.js | 99 +++++ apps/website/package.json | 3 + 52 files changed, 6398 insertions(+), 2 deletions(-) create mode 100644 apps/website/.eslintrc.custom.json create mode 100644 apps/website/eslint-rules/client-only-must-have-directive.js create mode 100644 apps/website/eslint-rules/client-only-no-server-code.js create mode 100644 apps/website/eslint-rules/client-only-rules.js create mode 100644 apps/website/eslint-rules/display-no-business-logic.js create mode 100644 apps/website/eslint-rules/display-no-domain-models.js create mode 100644 apps/website/eslint-rules/display-object-rules.js create mode 100644 apps/website/eslint-rules/filename-presenter-match.js create mode 100644 apps/website/eslint-rules/filename-rules.js create mode 100644 apps/website/eslint-rules/filename-service-match.js create mode 100644 apps/website/eslint-rules/index.js create mode 100644 apps/website/eslint-rules/model-no-display-in-domain.js create mode 100644 apps/website/eslint-rules/model-no-domain-in-display.js create mode 100644 apps/website/eslint-rules/model-taxonomy-rules.js create mode 100644 apps/website/eslint-rules/package.json create mode 100644 apps/website/eslint-rules/page-query-contract.js create mode 100644 apps/website/eslint-rules/page-query-execute.js create mode 100644 apps/website/eslint-rules/page-query-filename.js create mode 100644 apps/website/eslint-rules/page-query-no-null-returns.js create mode 100644 apps/website/eslint-rules/page-query-return-type.js create mode 100644 apps/website/eslint-rules/page-query-rules.js create mode 100644 apps/website/eslint-rules/presenter-contract.js create mode 100644 apps/website/eslint-rules/rsc-boundary-rules.js create mode 100644 apps/website/eslint-rules/rsc-no-container-manager-calls.js create mode 100644 apps/website/eslint-rules/rsc-no-container-manager.js create mode 100644 apps/website/eslint-rules/rsc-no-di.js create mode 100644 apps/website/eslint-rules/rsc-no-display-objects.js create mode 100644 apps/website/eslint-rules/rsc-no-intl.js create mode 100644 apps/website/eslint-rules/rsc-no-local-helpers.js create mode 100644 apps/website/eslint-rules/rsc-no-object-construction.js create mode 100644 apps/website/eslint-rules/rsc-no-page-data-fetcher.js create mode 100644 apps/website/eslint-rules/rsc-no-presenters.js create mode 100644 apps/website/eslint-rules/rsc-no-sorting-filtering.js create mode 100644 apps/website/eslint-rules/rsc-no-unsafe-services.js create mode 100644 apps/website/eslint-rules/rsc-no-view-models.js create mode 100644 apps/website/eslint-rules/services-must-be-marked.js create mode 100644 apps/website/eslint-rules/services-must-be-pure.js create mode 100644 apps/website/eslint-rules/services-no-external-api.js create mode 100644 apps/website/eslint-rules/services-rules.js create mode 100644 apps/website/eslint-rules/template-no-async-render.js create mode 100644 apps/website/eslint-rules/template-no-direct-mutations.js create mode 100644 apps/website/eslint-rules/template-no-external-state.js create mode 100644 apps/website/eslint-rules/template-no-global-objects.js create mode 100644 apps/website/eslint-rules/template-no-mutation-props.js create mode 100644 apps/website/eslint-rules/template-no-side-effects.js create mode 100644 apps/website/eslint-rules/template-no-unsafe-html.js create mode 100644 apps/website/eslint-rules/template-purity-rules.js create mode 100644 apps/website/eslint-rules/write-boundary-must-use-repository.js create mode 100644 apps/website/eslint-rules/write-boundary-no-direct-mutations.js create mode 100644 apps/website/eslint-rules/write-boundary-rules.js diff --git a/apps/website/.eslintrc.custom.json b/apps/website/.eslintrc.custom.json new file mode 100644 index 000000000..71f6941e4 --- /dev/null +++ b/apps/website/.eslintrc.custom.json @@ -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"] +} \ No newline at end of file diff --git a/apps/website/.eslintrc.json b/apps/website/.eslintrc.json index e50a1a950..11965d388 100644 --- a/apps/website/.eslintrc.json +++ b/apps/website/.eslintrc.json @@ -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": { diff --git a/apps/website/eslint-rules/client-only-must-have-directive.js b/apps/website/eslint-rules/client-only-must-have-directive.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/client-only-must-have-directive.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/client-only-no-server-code.js b/apps/website/eslint-rules/client-only-no-server-code.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/client-only-no-server-code.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/client-only-rules.js b/apps/website/eslint-rules/client-only-rules.js new file mode 100644 index 000000000..15bc2a868 --- /dev/null +++ b/apps/website/eslint-rules/client-only-rules.js @@ -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', + }); + } + } + }, + }; + }, + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/display-no-business-logic.js b/apps/website/eslint-rules/display-no-business-logic.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/display-no-business-logic.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/display-no-domain-models.js b/apps/website/eslint-rules/display-no-domain-models.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/display-no-domain-models.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/display-object-rules.js b/apps/website/eslint-rules/display-object-rules.js new file mode 100644 index 000000000..9fd15698c --- /dev/null +++ b/apps/website/eslint-rules/display-object-rules.js @@ -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; +} \ No newline at end of file diff --git a/apps/website/eslint-rules/filename-presenter-match.js b/apps/website/eslint-rules/filename-presenter-match.js new file mode 100644 index 000000000..73512995c --- /dev/null +++ b/apps/website/eslint-rules/filename-presenter-match.js @@ -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', + }); + } + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/filename-rules.js b/apps/website/eslint-rules/filename-rules.js new file mode 100644 index 000000000..976acd47e --- /dev/null +++ b/apps/website/eslint-rules/filename-rules.js @@ -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', + }); + } + } + }, + }; + }, + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/filename-service-match.js b/apps/website/eslint-rules/filename-service-match.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/filename-service-match.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/index.js b/apps/website/eslint-rules/index.js new file mode 100644 index 000000000..d7e095565 --- /dev/null +++ b/apps/website/eslint-rules/index.js @@ -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', + }, + }, + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/model-no-display-in-domain.js b/apps/website/eslint-rules/model-no-display-in-domain.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/model-no-display-in-domain.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/model-no-domain-in-display.js b/apps/website/eslint-rules/model-no-domain-in-display.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/model-no-domain-in-display.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/model-taxonomy-rules.js b/apps/website/eslint-rules/model-taxonomy-rules.js new file mode 100644 index 000000000..379f04cc8 --- /dev/null +++ b/apps/website/eslint-rules/model-taxonomy-rules.js @@ -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', + }); + } + } + }, + }; + }, + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/package.json b/apps/website/eslint-rules/package.json new file mode 100644 index 000000000..06e066f0b --- /dev/null +++ b/apps/website/eslint-rules/package.json @@ -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" +} diff --git a/apps/website/eslint-rules/page-query-contract.js b/apps/website/eslint-rules/page-query-contract.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/page-query-contract.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/page-query-execute.js b/apps/website/eslint-rules/page-query-execute.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/page-query-execute.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/page-query-filename.js b/apps/website/eslint-rules/page-query-filename.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/page-query-filename.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/page-query-no-null-returns.js b/apps/website/eslint-rules/page-query-no-null-returns.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/page-query-no-null-returns.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/page-query-return-type.js b/apps/website/eslint-rules/page-query-return-type.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/page-query-return-type.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/page-query-rules.js b/apps/website/eslint-rules/page-query-rules.js new file mode 100644 index 000000000..2c8ca238d --- /dev/null +++ b/apps/website/eslint-rules/page-query-rules.js @@ -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 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>', + }, + }, + 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; +} \ No newline at end of file diff --git a/apps/website/eslint-rules/presenter-contract.js b/apps/website/eslint-rules/presenter-contract.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/presenter-contract.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/rsc-boundary-rules.js b/apps/website/eslint-rules/rsc-boundary-rules.js new file mode 100644 index 000000000..d9c083310 --- /dev/null +++ b/apps/website/eslint-rules/rsc-boundary-rules.js @@ -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 */'); +} \ No newline at end of file diff --git a/apps/website/eslint-rules/rsc-no-container-manager-calls.js b/apps/website/eslint-rules/rsc-no-container-manager-calls.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/rsc-no-container-manager-calls.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/rsc-no-container-manager.js b/apps/website/eslint-rules/rsc-no-container-manager.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/rsc-no-container-manager.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/rsc-no-di.js b/apps/website/eslint-rules/rsc-no-di.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/rsc-no-di.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/rsc-no-display-objects.js b/apps/website/eslint-rules/rsc-no-display-objects.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/rsc-no-display-objects.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/rsc-no-intl.js b/apps/website/eslint-rules/rsc-no-intl.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/rsc-no-intl.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/rsc-no-local-helpers.js b/apps/website/eslint-rules/rsc-no-local-helpers.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/rsc-no-local-helpers.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/rsc-no-object-construction.js b/apps/website/eslint-rules/rsc-no-object-construction.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/rsc-no-object-construction.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/rsc-no-page-data-fetcher.js b/apps/website/eslint-rules/rsc-no-page-data-fetcher.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/rsc-no-page-data-fetcher.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/rsc-no-presenters.js b/apps/website/eslint-rules/rsc-no-presenters.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/rsc-no-presenters.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/rsc-no-sorting-filtering.js b/apps/website/eslint-rules/rsc-no-sorting-filtering.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/rsc-no-sorting-filtering.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/rsc-no-unsafe-services.js b/apps/website/eslint-rules/rsc-no-unsafe-services.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/rsc-no-unsafe-services.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/rsc-no-view-models.js b/apps/website/eslint-rules/rsc-no-view-models.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/rsc-no-view-models.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/services-must-be-marked.js b/apps/website/eslint-rules/services-must-be-marked.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/services-must-be-marked.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/services-must-be-pure.js b/apps/website/eslint-rules/services-must-be-pure.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/services-must-be-pure.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/services-no-external-api.js b/apps/website/eslint-rules/services-no-external-api.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/services-no-external-api.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/services-rules.js b/apps/website/eslint-rules/services-rules.js new file mode 100644 index 000000000..38ef1be9f --- /dev/null +++ b/apps/website/eslint-rules/services-rules.js @@ -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; +} \ No newline at end of file diff --git a/apps/website/eslint-rules/template-no-async-render.js b/apps/website/eslint-rules/template-no-async-render.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/template-no-async-render.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/template-no-direct-mutations.js b/apps/website/eslint-rules/template-no-direct-mutations.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/template-no-direct-mutations.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/template-no-external-state.js b/apps/website/eslint-rules/template-no-external-state.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/template-no-external-state.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/template-no-global-objects.js b/apps/website/eslint-rules/template-no-global-objects.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/template-no-global-objects.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/template-no-mutation-props.js b/apps/website/eslint-rules/template-no-mutation-props.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/template-no-mutation-props.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/template-no-side-effects.js b/apps/website/eslint-rules/template-no-side-effects.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/template-no-side-effects.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/template-no-unsafe-html.js b/apps/website/eslint-rules/template-no-unsafe-html.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/template-no-unsafe-html.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/template-purity-rules.js b/apps/website/eslint-rules/template-purity-rules.js new file mode 100644 index 000000000..138f0930f --- /dev/null +++ b/apps/website/eslint-rules/template-purity-rules.js @@ -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; +} \ No newline at end of file diff --git a/apps/website/eslint-rules/write-boundary-must-use-repository.js b/apps/website/eslint-rules/write-boundary-must-use-repository.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/write-boundary-must-use-repository.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/write-boundary-no-direct-mutations.js b/apps/website/eslint-rules/write-boundary-no-direct-mutations.js new file mode 100644 index 000000000..1aec96a22 --- /dev/null +++ b/apps/website/eslint-rules/write-boundary-no-direct-mutations.js @@ -0,0 +1,125 @@ +/** + * ESLint rule to enforce Presenter contract + * + * Enforces that classes ending with "Presenter" must: + * 1. Implement Presenter 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 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 + 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', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/apps/website/eslint-rules/write-boundary-rules.js b/apps/website/eslint-rules/write-boundary-rules.js new file mode 100644 index 000000000..3211b4c5e --- /dev/null +++ b/apps/website/eslint-rules/write-boundary-rules.js @@ -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; +} \ No newline at end of file diff --git a/apps/website/package.json b/apps/website/package.json index 1b1088618..a520db2c3 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -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"