/** * ESLint rule to enforce ViewModel and Builder architectural boundaries * * Rules: * 1. ViewModels/Builders MUST NOT contain the word "DTO" in identifiers * 2. ViewModels/Builders MUST NOT define inline DTO interfaces * 3. ViewModels/Builders MUST NOT import from DTO paths (except generated types in Builders) * 4. ViewModels MUST NOT define ViewData interfaces */ module.exports = { meta: { type: 'problem', docs: { description: 'Enforce ViewModel and Builder architectural boundaries', category: 'Architecture', recommended: true, }, fixable: null, schema: [], messages: { noDtoInViewModel: 'ViewModels and Builders must not use the word "DTO" in identifiers. DTOs belong to the API/Service layer. Use plain properties or ViewData types.', noDtoImport: 'ViewModels must not import from DTO paths. DTOs belong to lib/types/generated/. Import from lib/view-data/ or use plain properties instead.', noViewDataDefinition: 'ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.', noInlineDtoDefinition: 'DTOs must not be defined inline. Use generated types from lib/types/generated/ and import them.', }, }, create(context) { const filename = context.getFilename(); const isInViewModels = filename.includes('/lib/view-models/'); const isInBuilders = filename.includes('/lib/builders/'); if (!isInViewModels && !isInBuilders) return {}; return { // Check for "DTO" in any identifier Identifier(node) { const name = node.name.toUpperCase(); if (name === 'DTO' || name.endsWith('DTO')) { // Exception: Allow DTO in type references in Builders (for satisfies/input) if (isInBuilders && (node.parent.type === 'TSTypeReference' || node.parent.type === 'TSQualifiedName')) { return; } context.report({ node, messageId: 'noDtoInViewModel', }); } }, // Check for imports from DTO paths ImportDeclaration(node) { const importPath = node.source.value; // ViewModels are never allowed to import DTOs if (isInViewModels && ( importPath.includes('/lib/types/generated/') || importPath.includes('/lib/dtos/') || importPath.includes('/lib/api/') || importPath.includes('/lib/services/') )) { context.report({ node, messageId: 'noDtoImport', }); } }, // Check for ViewData definitions in ViewModels TSInterfaceDeclaration(node) { if (isInViewModels && node.id && node.id.name && node.id.name.endsWith('ViewData')) { context.report({ node, messageId: 'noViewDataDefinition', }); } // Check for inline DTO definitions in both ViewModels and Builders if (node.id && node.id.name && node.id.name.toUpperCase().includes('DTO')) { context.report({ node, messageId: 'noInlineDtoDefinition', }); } }, TSTypeAliasDeclaration(node) { if (isInViewModels && node.id && node.id.name && node.id.name.endsWith('ViewData')) { context.report({ node, messageId: 'noViewDataDefinition', }); } // Check for inline DTO definitions if (node.id && node.id.name && node.id.name.toUpperCase().includes('DTO')) { context.report({ node, messageId: 'noInlineDtoDefinition', }); } }, }; }, };