diff --git a/apps/website/eslint-rules/index.js b/apps/website/eslint-rules/index.js index e526c75b1..409e554b4 100644 --- a/apps/website/eslint-rules/index.js +++ b/apps/website/eslint-rules/index.js @@ -25,11 +25,13 @@ const writeBoundaryRules = require('./write-boundary-rules'); const modelTaxonomyRules = require('./model-taxonomy-rules'); const filenameRules = require('./filename-rules'); const componentNoDataManipulation = require('./component-no-data-manipulation'); +const presenterPurity = require('./presenter-purity'); module.exports = { rules: { // Presenter Contract 'presenter-contract': presenterContract, + 'presenter-purity': presenterPurity, // RSC Boundary Rules 'rsc-no-container-manager': rscBoundaryRules['no-container-manager-in-server'], diff --git a/apps/website/eslint-rules/presenter-contract.js b/apps/website/eslint-rules/presenter-contract.js index b4a884f06..88181b938 100644 --- a/apps/website/eslint-rules/presenter-contract.js +++ b/apps/website/eslint-rules/presenter-contract.js @@ -87,10 +87,11 @@ module.exports = { // Check for present method in classes MethodDefinition(node) { - if (presenterClassNode && - node.key.type === 'Identifier' && + if (presenterClassNode && + node.key.type === 'Identifier' && node.key.name === 'present' && - node.parent === presenterClassNode) { + node.parent.type === 'ClassBody' && + node.parent.parent === presenterClassNode) { hasPresentMethod = true; } }, diff --git a/apps/website/eslint-rules/presenter-purity.js b/apps/website/eslint-rules/presenter-purity.js new file mode 100644 index 000000000..24e7bfa3e --- /dev/null +++ b/apps/website/eslint-rules/presenter-purity.js @@ -0,0 +1,53 @@ +/** + * ESLint rule: Presenters should only do pure mapping, not business logic + * + * Presenters should NOT use: + * - .filter() - filtering should happen in PageQuery + * - .sort() - sorting should happen in PageQuery + * - .reduce() - aggregation should happen in PageQuery + * - .find() - lookup should happen in PageQuery + * - .some() / .every() - checks should happen in PageQuery + */ + +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'Forbid business logic operations in presenters', + category: 'Presenter Purity', + }, + messages: { + message: 'Presenter should only do pure mapping. Move {{operation}}() to PageQuery - see apps/website/lib/contracts/presenters/Presenter.ts', + }, + }, + create(context) { + return { + CallExpression(node) { + const filename = context.getFilename(); + + // Check if this is a presenter file + if (filename.includes('/lib/presenters/') && filename.endsWith('.ts')) { + + // Check for business logic operations + if (node.callee.type === 'MemberExpression') { + const property = node.callee.property; + + if (property.type === 'Identifier') { + const forbiddenOperations = ['filter', 'sort', 'reduce', 'find', 'some', 'every', 'map']; + const operation = property.name; + + // Allow .map() - that's the core presenter operation + if (forbiddenOperations.includes(operation) && operation !== 'map') { + context.report({ + node, + messageId: 'message', + data: { operation }, + }); + } + } + } + } + }, + }; + }, +}; \ No newline at end of file