diff --git a/apps/website/eslint-rules/view-data-builder-contract.js b/apps/website/eslint-rules/view-data-builder-contract.js index cd8c154e6..dc5dfcf5b 100644 --- a/apps/website/eslint-rules/view-data-builder-contract.js +++ b/apps/website/eslint-rules/view-data-builder-contract.js @@ -4,8 +4,9 @@ * View Data Builders must: * 1. Be classes named *ViewDataBuilder * 2. Have a static build() method - * 3. Accept API DTO as parameter (named 'apiDto', NOT 'pageDto') - * 4. Return View Data + * 3. Use 'satisfies ViewDataBuilder<...>' for static enforcement + * 4. Accept API DTO as parameter (named 'apiDto') + * 5. Return View Data */ module.exports = { @@ -20,7 +21,8 @@ module.exports = { schema: [], messages: { notAClass: 'View Data Builders must be classes named *ViewDataBuilder', - missingBuildMethod: 'View Data Builders must have a static build() method', + missingStaticBuild: 'View Data Builders must have a static build() method', + missingSatisfies: 'View Data Builders must use "satisfies ViewDataBuilder<...>" for static type enforcement', invalidBuildSignature: 'build() method must accept API DTO and return View Data', wrongParameterName: 'Parameter must be named "apiDto", not "pageDto" or other names', }, @@ -32,7 +34,8 @@ module.exports = { if (!isInViewDataBuilders) return {}; - let hasBuildMethod = false; + let hasStaticBuild = false; + let hasSatisfies = false; let hasCorrectSignature = false; let hasCorrectParameterName = false; @@ -49,28 +52,28 @@ module.exports = { } // Check for static build method - const buildMethod = node.body.body.find(member => + const staticBuild = node.body.body.find(member => member.type === 'MethodDefinition' && member.key.type === 'Identifier' && member.key.name === 'build' && member.static === true ); - if (buildMethod) { - hasBuildMethod = true; + if (staticBuild) { + hasStaticBuild = true; // Check signature - should have at least one parameter - if (buildMethod.value && - buildMethod.value.params && - buildMethod.value.params.length > 0) { + if (staticBuild.value && + staticBuild.value.params && + staticBuild.value.params.length > 0) { hasCorrectSignature = true; // Check parameter name - const firstParam = buildMethod.value.params[0]; + const firstParam = staticBuild.value.params[0]; if (firstParam.type === 'Identifier' && firstParam.name === 'apiDto') { hasCorrectParameterName = true; - } else if (firstParam.type === 'Identifier' && firstParam.name === 'pageDto') { - // Report specific error for pageDto + } else if (firstParam.type === 'Identifier' && (firstParam.name === 'pageDto' || firstParam.name === 'dto')) { + // Report specific error for wrong names context.report({ node: firstParam, messageId: 'wrongParameterName', @@ -80,23 +83,35 @@ module.exports = { } }, + // Check for satisfies expression + TSSatisfiesExpression(node) { + if (node.typeAnnotation && + node.typeAnnotation.type === 'TSTypeReference' && + node.typeAnnotation.typeName.name === 'ViewDataBuilder') { + hasSatisfies = true; + } + }, + 'Program:exit'() { - if (!hasBuildMethod) { + if (!hasStaticBuild) { context.report({ node: context.getSourceCode().ast, - messageId: 'missingBuildMethod', + messageId: 'missingStaticBuild', }); - } else if (!hasCorrectSignature) { + } + + if (!hasSatisfies) { + context.report({ + node: context.getSourceCode().ast, + messageId: 'missingSatisfies', + }); + } + + if (hasStaticBuild && !hasCorrectSignature) { context.report({ node: context.getSourceCode().ast, messageId: 'invalidBuildSignature', }); - } else if (!hasCorrectParameterName) { - // Only report if not already reported for pageDto - context.report({ - node: context.getSourceCode().ast, - messageId: 'wrongParameterName', - }); } }, }; diff --git a/apps/website/eslint-rules/view-data-builder-implements.js b/apps/website/eslint-rules/view-data-builder-implements.js index 86d38f66d..ee6d07e99 100644 --- a/apps/website/eslint-rules/view-data-builder-implements.js +++ b/apps/website/eslint-rules/view-data-builder-implements.js @@ -3,8 +3,9 @@ * * View Data Builders in lib/builders/view-data/ must: * 1. Be classes named *ViewDataBuilder - * 2. Implement the ViewDataBuilder interface - * 3. Have a static build() method + * 2. Have a static build() method + * + * Note: 'implements' is deprecated in favor of 'satisfies' checked in view-data-builder-contract.js */ module.exports = { @@ -19,7 +20,6 @@ module.exports = { schema: [], messages: { notAClass: 'View Data Builders must be classes named *ViewDataBuilder', - missingImplements: 'View Data Builders must implement ViewDataBuilder interface', missingBuildMethod: 'View Data Builders must have a static build() method', }, }, @@ -30,7 +30,6 @@ module.exports = { if (!isInViewDataBuilders) return {}; - let hasImplements = false; let hasBuildMethod = false; return { @@ -45,24 +44,6 @@ module.exports = { }); } - // Check if class implements ViewDataBuilder interface - if (node.implements && node.implements.length > 0) { - for (const impl of node.implements) { - // Handle GenericTypeAnnotation for ViewDataBuilder - if (impl.expression.type === 'TSInstantiationExpression') { - const expr = impl.expression.expression; - if (expr.type === 'Identifier' && expr.name === 'ViewDataBuilder') { - hasImplements = true; - } - } else if (impl.expression.type === 'Identifier') { - // Handle simple ViewDataBuilder (without generics) - if (impl.expression.name === 'ViewDataBuilder') { - hasImplements = true; - } - } - } - } - // Check for static build method const buildMethod = node.body.body.find(member => member.type === 'MethodDefinition' && @@ -77,13 +58,6 @@ module.exports = { }, 'Program:exit'() { - if (!hasImplements) { - context.report({ - node: context.getSourceCode().ast, - messageId: 'missingImplements', - }); - } - if (!hasBuildMethod) { context.report({ node: context.getSourceCode().ast, diff --git a/apps/website/eslint-rules/view-model-builder-contract.js b/apps/website/eslint-rules/view-model-builder-contract.js index cd93e4767..57af31e7f 100644 --- a/apps/website/eslint-rules/view-model-builder-contract.js +++ b/apps/website/eslint-rules/view-model-builder-contract.js @@ -1,11 +1,12 @@ /** * ESLint rule to enforce View Model Builder contract - * + * * View Model Builders must: * 1. Be classes named *ViewModelBuilder * 2. Have a static build() method - * 3. Accept View Data as parameter - * 4. Return View Model + * 3. Use 'satisfies ViewModelBuilder<...>' for static enforcement + * 4. Accept View Data as parameter + * 5. Return View Model */ module.exports = { @@ -20,7 +21,8 @@ module.exports = { schema: [], messages: { notAClass: 'View Model Builders must be classes named *ViewModelBuilder', - missingBuildMethod: 'View Model Builders must have a static build() method', + missingStaticBuild: 'View Model Builders must have a static build() method', + missingSatisfies: 'View Model Builders must use "satisfies ViewModelBuilder<...>" for static type enforcement', invalidBuildSignature: 'build() method must accept View Data and return View Model', }, }, @@ -31,7 +33,8 @@ module.exports = { if (!isInViewModelBuilders) return {}; - let hasBuildMethod = false; + let hasStaticBuild = false; + let hasSatisfies = false; let hasCorrectSignature = false; return { @@ -47,32 +50,50 @@ module.exports = { } // Check for static build method - const buildMethod = node.body.body.find(member => + const staticBuild = node.body.body.find(member => member.type === 'MethodDefinition' && member.key.type === 'Identifier' && member.key.name === 'build' && member.static === true ); - if (buildMethod) { - hasBuildMethod = true; + if (staticBuild) { + hasStaticBuild = true; // Check signature - should have at least one parameter - if (buildMethod.value && - buildMethod.value.params && - buildMethod.value.params.length > 0) { + if (staticBuild.value && + staticBuild.value.params && + staticBuild.value.params.length > 0) { hasCorrectSignature = true; } } }, + // Check for satisfies expression + TSSatisfiesExpression(node) { + if (node.typeAnnotation && + node.typeAnnotation.type === 'TSTypeReference' && + node.typeAnnotation.typeName.name === 'ViewModelBuilder') { + hasSatisfies = true; + } + }, + 'Program:exit'() { - if (!hasBuildMethod) { + if (!hasStaticBuild) { context.report({ node: context.getSourceCode().ast, - messageId: 'missingBuildMethod', + messageId: 'missingStaticBuild', }); - } else if (!hasCorrectSignature) { + } + + if (!hasSatisfies) { + context.report({ + node: context.getSourceCode().ast, + messageId: 'missingSatisfies', + }); + } + + if (hasStaticBuild && !hasCorrectSignature) { context.report({ node: context.getSourceCode().ast, messageId: 'invalidBuildSignature',