UNPKG

@j2blasco/ts-boundaries

Version:

A tool to generate ESLint boundaries configuration from TypeScript boundary definitions

414 lines (376 loc) 12 kB
import { generateESLintConfigFromData, type BoundaryData } from './generator'; import type { Boundaries } from './boundaries.types'; describe('Generator - ESLint Config Generation', () => { describe('generateESLintConfigFromData', () => { it('should generate basic config with single root boundary', () => { const mockData: BoundaryData = { elements: [ { type: 'root', pattern: 'src', folderPath: '', }, ], configs: new Map<string, Boundaries>([ [ 'root', { name: 'root', internal: [], external: ['fs', 'path', 'url'], }, ], ]), }; const result = generateESLintConfigFromData(mockData); expect(result).toContain( '// This file is auto-generated by @j2blasco/ts-boundaries', ); expect(result).toContain('// Do not edit manually!'); expect(result).toContain( "import boundaries from 'eslint-plugin-boundaries'", ); expect(result).toContain('export default ['); expect(result).toContain("{ type: 'root', pattern: 'src' }"); expect(result).toContain( "{ from: 'root', allow: ['fs', 'path', 'url'] }", ); expect(result).toContain( "'boundaries/no-private': [2, { 'allowUncles': false }]", ); }); it('should generate config with multiple boundaries and internal rules', () => { const mockData: BoundaryData = { elements: [ { type: 'root', pattern: 'src', folderPath: '', }, { type: 'features', pattern: 'src/features', folderPath: 'features', }, { type: 'auth', pattern: 'src/features/auth', folderPath: 'features/auth', }, ], configs: new Map<string, Boundaries>([ [ 'root', { name: 'root', internal: [], external: ['fs', 'path'], }, ], [ 'features', { name: 'features', internal: ['root'], external: ['axios'], }, ], [ 'auth', { name: 'auth', internal: ['features'], external: ['bcrypt'], }, ], ]), }; const result = generateESLintConfigFromData(mockData); // Check element definitions (should be sorted by depth - deepest first) expect(result).toContain( "{ type: 'auth', pattern: 'src/features/auth' }", ); expect(result).toContain("{ type: 'features', pattern: 'src/features' }"); expect(result).toContain("{ type: 'root', pattern: 'src' }"); // Check internal rules expect(result).toContain("{ from: 'features', allow: ['root'] }"); expect(result).toContain("{ from: 'auth', allow: ['features'] }"); // Check external rules expect(result).toContain("{ from: 'root', allow: ['fs', 'path'] }"); expect(result).toContain("{ from: 'features', allow: ['axios'] }"); expect(result).toContain("{ from: 'auth', allow: ['bcrypt'] }"); }); it('should handle empty boundaries configuration', () => { const mockData: BoundaryData = { elements: [], configs: new Map<string, Boundaries>(), }; const result = generateESLintConfigFromData(mockData); expect(result).toContain( '// This file is auto-generated by @j2blasco/ts-boundaries', ); // Check that elements section is empty expect(result).toContain("'boundaries/elements': ["); expect(result).toContain('export default ['); }); it('should handle boundaries with no dependencies', () => { const mockData: BoundaryData = { elements: [ { type: 'isolated', pattern: 'src/isolated', folderPath: 'isolated', }, ], configs: new Map<string, Boundaries>([ [ 'isolated', { name: 'isolated', internal: [], external: [], }, ], ]), }; const result = generateESLintConfigFromData(mockData); expect(result).toContain("{ type: 'isolated', pattern: 'src/isolated' }"); // Should not contain any allow rules for this boundary expect(result).not.toContain("from: 'isolated'"); }); it('should handle scoped packages in external dependencies', () => { const mockData: BoundaryData = { elements: [ { type: 'frontend', pattern: 'src/frontend', folderPath: 'frontend', }, ], configs: new Map<string, Boundaries>([ [ 'frontend', { name: 'frontend', internal: [], external: [ '@types/react', '@testing-library/react', 'react', 'react-dom', ], }, ], ]), }; const result = generateESLintConfigFromData(mockData); expect(result).toContain( "{ from: 'frontend', allow: ['@types/react', '@testing-library/react', 'react', 'react-dom'] }", ); }); it('should handle wildcard patterns in dependencies', () => { const mockData: BoundaryData = { elements: [ { type: 'utils', pattern: 'src/utils', folderPath: 'utils', }, ], configs: new Map<string, Boundaries>([ [ 'utils', { name: 'utils', internal: [], external: ['@types/*', 'lodash*', 'ramda'], }, ], ]), }; const result = generateESLintConfigFromData(mockData); expect(result).toContain( "{ from: 'utils', allow: ['@types/*', 'lodash*', 'ramda'] }", ); }); it('should sort elements by depth (deepest first)', () => { const mockData: BoundaryData = { elements: [ { type: 'root', pattern: 'src', folderPath: '', }, { type: 'level1', pattern: 'src/level1', folderPath: 'level1', }, { type: 'level2', pattern: 'src/level1/level2', folderPath: 'level1/level2', }, ], configs: new Map<string, Boundaries>([ [ 'root', { name: 'root', internal: [], external: ['fs'], }, ], [ 'level1', { name: 'level1', internal: [], external: ['axios'], }, ], [ 'level2', { name: 'level2', internal: [], external: ['lodash'], }, ], ]), }; const result = generateESLintConfigFromData(mockData); // Verify that all elements are present in the output expect(result).toContain( "{ type: 'level2', pattern: 'src/level1/level2' }", ); expect(result).toContain("{ type: 'level1', pattern: 'src/level1' }"); expect(result).toContain("{ type: 'root', pattern: 'src' }"); // Verify that deeper elements appear before shallower ones in the elements section const elementsSection = result.match( /'boundaries\/elements': \[([\s\S]*?)\]/, )?.[1]; expect(elementsSection).toBeTruthy(); if (elementsSection) { const level2Position = elementsSection.indexOf("'level2'"); const level1Position = elementsSection.indexOf("'level1'"); const rootPosition = elementsSection.indexOf("'root'"); // At minimum, all should be found expect(level2Position).toBeGreaterThanOrEqual(0); expect(level1Position).toBeGreaterThanOrEqual(0); expect(rootPosition).toBeGreaterThanOrEqual(0); // Level2 should come before level1 in the elements section expect(level2Position).toBeLessThan(level1Position); } }); it('should properly quote strings with special characters', () => { const mockData: BoundaryData = { elements: [ { type: 'special-chars_123', pattern: 'src/special-chars_123', folderPath: 'special-chars_123', }, ], configs: new Map<string, Boundaries>([ [ 'special-chars_123', { name: 'special-chars_123', internal: [], external: ['@scope/package', 'package-name'], }, ], ]), }; const result = generateESLintConfigFromData(mockData); expect(result).toContain("type: 'special-chars_123'"); expect(result).toContain("pattern: 'src/special-chars_123'"); expect(result).toContain("'@scope/package'"); expect(result).toContain("'package-name'"); }); it('should generate valid ESLint configuration structure', () => { const mockData: BoundaryData = { elements: [ { type: 'api', pattern: 'src/api', folderPath: 'api', }, ], configs: new Map<string, Boundaries>([ [ 'api', { name: 'api', internal: ['utils'], external: ['axios'], }, ], ]), }; const result = generateESLintConfigFromData(mockData); // Basic structure validation expect(result).toMatch(/export default \[/); expect(result).toMatch(/plugins: \{/); expect(result).toMatch(/boundaries/); expect(result).toMatch(/settings: \{/); expect(result).toMatch(/rules: \{/); expect(result).toMatch(/'boundaries\/no-private'/); expect(result).toMatch(/'boundaries\/external'/); expect(result).toMatch(/'boundaries\/element-types'/); expect(result.trim().endsWith('];')).toBe(true); // Specific rule structure validation expect(result).toMatch( /'boundaries\/no-private': \[2, \{ 'allowUncles': false \}\]/, ); expect(result).toMatch( /'boundaries\/external': \[[\s\S]*default: 'disallow'/, ); expect(result).toMatch( /'boundaries\/element-types': \[[\s\S]*default: 'disallow'/, ); }); it('should handle mixed internal and external dependencies', () => { const mockData: BoundaryData = { elements: [ { type: 'services', pattern: 'src/services', folderPath: 'services', }, { type: 'utils', pattern: 'src/utils', folderPath: 'utils', }, ], configs: new Map<string, Boundaries>([ [ 'services', { name: 'services', internal: ['utils'], external: ['axios', 'lodash'], }, ], [ 'utils', { name: 'utils', internal: [], external: ['ramda'], }, ], ]), }; const result = generateESLintConfigFromData(mockData); // Check both internal and external rules are present expect(result).toContain("{ from: 'services', allow: ['utils'] }"); expect(result).toContain( "{ from: 'services', allow: ['axios', 'lodash'] }", ); expect(result).toContain("{ from: 'utils', allow: ['ramda'] }"); }); }); });