@j2blasco/ts-boundaries
Version:
A tool to generate ESLint boundaries configuration from TypeScript boundary definitions
414 lines (376 loc) • 12 kB
text/typescript
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'] }");
});
});
});