Skip to content
Open
26 changes: 24 additions & 2 deletions docs/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,31 @@ These options have sensible defaults and are typically only customized for speci

#### `barrelFileName` {#barrelfilename}

- **Type**: `string`
- **Type**: `string | string[]`
- **Default**: `'index.ts'`
- **Description**: Name of the barrel file that exports public APIs from a module.
- **Description**: Name of the barrel file that exports public APIs from a module. Supports glob patterns and arrays to allow multiple entry files per module.

**Glob wildcards:**
- `*` matches zero or more characters
- `?` matches exactly one character

**Examples:**

```typescript
// Single filename (default behavior)
barrelFileName: 'index.ts'

// Glob pattern for multiple entry files
barrelFileName: 'index.*.ts'

// Explicit list of entry files
barrelFileName: ['index.ts', 'index.routing.ts', 'index.state.ts']

// Combine exact names and glob patterns
barrelFileName: ['index.ts', 'index.*.ts']
```

> **Use case**: In Angular monorepos, a single barrel file can cause inefficient tree shaking. By splitting entry files (e.g., `index.ts`, `index.routing.ts`, `index.state.ts`), you can keep module boundaries enforced while allowing multiple public access points.

#### `ignoreFileExtensions` {#ignorefileextensions}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ function accessesBarrelFileForBarrelModules(fileInfo: FileInfo) {
return false;
}

return fileInfo.moduleInfo.barrelPath === fileInfo.path;
return fileInfo.moduleInfo.isBarrelFile(fileInfo.path);
}

function isExcludedRootModule(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
import { describe, expect, it } from 'vitest';
import { hasEncapsulationViolations } from '../has-encapsulation-violations';
import { toFsPath } from '../../file-info/fs-path';
import { testInit } from '../../test/test-init';
import { tsConfig } from '../../test/fixtures/ts-config';
import { sheriffConfig } from '../../test/project-configurator';

describe('encapsulation with multiple barrel files', () => {
it('should allow imports of all barrel files when using an array', () => {
const projectInfo = testInit('src/main.ts', {
'tsconfig.json': tsConfig(),
'sheriff.config.ts': sheriffConfig({
barrelFileName: ['index.ts', 'index.routing.ts'],
depRules: {},
}),
src: {
'main.ts': [
'./app/orders/index',
'./app/orders/index.routing',
],
app: {
orders: {
'index.ts': ['./order-list.component'],
'index.routing.ts': ['./order-routes'],
'order-list.component.ts': [],
'order-routes.ts': [],
},
},
},
});

const violations = hasEncapsulationViolations(
toFsPath('/project/src/main.ts'),
projectInfo,
);
expect(Object.keys(violations)).toEqual([]);
});

it('should block deep imports even with multiple barrel files', () => {
const projectInfo = testInit('src/main.ts', {
'tsconfig.json': tsConfig(),
'sheriff.config.ts': sheriffConfig({
barrelFileName: ['index.ts', 'index.routing.ts'],
depRules: {},
}),
src: {
'main.ts': [
'./app/orders/index',
'./app/orders/order-list.component',
],
app: {
orders: {
'index.ts': ['./order-list.component'],
'index.routing.ts': ['./order-routes'],
'order-list.component.ts': [],
'order-routes.ts': [],
},
},
},
});

const violations = hasEncapsulationViolations(
toFsPath('/project/src/main.ts'),
projectInfo,
);
expect(Object.keys(violations)).toEqual([
'./app/orders/order-list.component',
]);
});

it('should allow importing barrel files of nested modules', () => {
const projectInfo = testInit('src/main.ts', {
'tsconfig.json': tsConfig(),
'sheriff.config.ts': sheriffConfig({
barrelFileName: ['index.ts'],
depRules: {},
}),
src: {
'main.ts': ['./app/orders/internal/index'],
app: {
orders: {
'index.ts': ['./order-list.component'],
'order-list.component.ts': [],
internal: {
'index.ts': ['./details'],
'details.ts': [],
},
},
},
},
});

const violations = hasEncapsulationViolations(
toFsPath('/project/src/main.ts'),
projectInfo,
);
expect(Object.keys(violations)).toEqual([]);
});

it('should block deep imports into nested modules', () => {
const projectInfo = testInit('src/main.ts', {
'tsconfig.json': tsConfig(),
'sheriff.config.ts': sheriffConfig({
barrelFileName: ['index.ts'],
depRules: {},
}),
src: {
'main.ts': ['./app/orders/internal/details'],
app: {
orders: {
'index.ts': ['./order-list.component'],
'order-list.component.ts': [],
internal: {
'index.ts': ['./details'],
'details.ts': [],
},
},
},
},
});

const violations = hasEncapsulationViolations(
toFsPath('/project/src/main.ts'),
projectInfo,
);
expect(Object.keys(violations)).toEqual([
'./app/orders/internal/details',
]);
});

it('should allow imports of barrel files matching a glob pattern', () => {
const projectInfo = testInit('src/main.ts', {
'tsconfig.json': tsConfig(),
'sheriff.config.ts': sheriffConfig({
barrelFileName: ['index.ts', 'index.*.ts'],
depRules: {},
}),
src: {
'main.ts': [
'./app/orders/index',
'./app/orders/index.state',
'./app/orders/index.routing',
],
app: {
orders: {
'index.ts': ['./order-list.component'],
'index.state.ts': ['./state'],
'index.routing.ts': ['./order-routes'],
'order-list.component.ts': [],
'state.ts': [],
'order-routes.ts': [],
},
},
},
});

const violations = hasEncapsulationViolations(
toFsPath('/project/src/main.ts'),
projectInfo,
);
expect(Object.keys(violations)).toEqual([]);
});

it('should block deep imports when using glob barrel pattern', () => {
const projectInfo = testInit('src/main.ts', {
'tsconfig.json': tsConfig(),
'sheriff.config.ts': sheriffConfig({
barrelFileName: ['index.ts', 'index.*.ts'],
depRules: {},
}),
src: {
'main.ts': [
'./app/orders/index',
'./app/orders/state',
],
app: {
orders: {
'index.ts': ['./order-list.component'],
'index.state.ts': ['./state'],
'order-list.component.ts': [],
'state.ts': [],
},
},
},
});

const violations = hasEncapsulationViolations(
toFsPath('/project/src/main.ts'),
projectInfo,
);
expect(Object.keys(violations)).toEqual(['./app/orders/state']);
});

it('should allow barrel imports of nested modules with glob patterns', () => {
const projectInfo = testInit('src/main.ts', {
'tsconfig.json': tsConfig(),
'sheriff.config.ts': sheriffConfig({
barrelFileName: ['index.ts', 'index.*.ts'],
depRules: {},
}),
src: {
'main.ts': ['./app/orders/feature/index.routing'],
app: {
orders: {
'index.ts': ['./order-list.component'],
'index.routing.ts': ['./order-routes'],
'order-list.component.ts': [],
'order-routes.ts': [],
feature: {
'index.routing.ts': ['./feature-routes'],
'feature-routes.ts': [],
},
},
},
},
});

const violations = hasEncapsulationViolations(
toFsPath('/project/src/main.ts'),
projectInfo,
);
expect(Object.keys(violations)).toEqual([]);
});

it('should block non-barrel imports into nested modules with glob patterns', () => {
const projectInfo = testInit('src/main.ts', {
'tsconfig.json': tsConfig(),
'sheriff.config.ts': sheriffConfig({
barrelFileName: ['index.ts', 'index.*.ts'],
depRules: {},
}),
src: {
'main.ts': ['./app/orders/feature/feature-routes'],
app: {
orders: {
'index.ts': ['./order-list.component'],
'index.routing.ts': ['./order-routes'],
'order-list.component.ts': [],
'order-routes.ts': [],
feature: {
'index.routing.ts': ['./feature-routes'],
'feature-routes.ts': [],
},
},
},
},
});

const violations = hasEncapsulationViolations(
toFsPath('/project/src/main.ts'),
projectInfo,
);
expect(Object.keys(violations)).toEqual([
'./app/orders/feature/feature-routes',
]);
});

it('should work across multiple modules with glob barrel files', () => {
const projectInfo = testInit('src/main.ts', {
'tsconfig.json': tsConfig(),
'sheriff.config.ts': sheriffConfig({
barrelFileName: ['index.ts', 'index.*.ts'],
depRules: {},
}),
src: {
'main.ts': [
'./app/orders/index',
'./app/orders/index.routing',
'./app/customers/index',
],
app: {
orders: {
'index.ts': ['./order-list.component'],
'index.routing.ts': ['./order-routes'],
'order-list.component.ts': [],
'order-routes.ts': [],
},
customers: {
'index.ts': ['./customer.component'],
'customer.component.ts': [],
},
},
},
});

for (const [filename, expectedViolations] of [
['src/main.ts', []],
['src/app/orders/index.ts', []],
['src/app/orders/index.routing.ts', []],
['src/app/customers/index.ts', []],
] as [string, string[]][]) {
const violations = hasEncapsulationViolations(
toFsPath(`/project/${filename}`),
projectInfo,
);
expect(
Object.keys(violations),
`encapsulation check failed for ${filename}`,
).toEqual(expectedViolations);
}
});
});
3 changes: 3 additions & 0 deletions packages/core/src/lib/config/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type Configuration = Required<
| 'showWarningOnBarrelCollision'
| 'encapsulatedFolderNameForBarrelLess'
| 'entryPoints'
| 'barrelFileName'
>
> & {
// dependency rules will skip if `isConfigFileMissing` is true
Expand All @@ -18,4 +19,6 @@ export type Configuration = Required<
entryPoints?: Record<string, string>;
// ignoreFileExtensions is always present (either user-specified or default)
ignoreFileExtensions: string[];
// barrelFileName is always normalized to an array internally
barrelFileName: string[];
};
2 changes: 1 addition & 1 deletion packages/core/src/lib/config/default-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const defaultConfig: Configuration = {
log: false,
entryFile: '',
isConfigFileMissing: false,
barrelFileName: 'index.ts',
barrelFileName: ['index.ts'],
entryPoints: undefined,
ignoreFileExtensions: defaultIgnoreFileExtensions,
};
9 changes: 9 additions & 0 deletions packages/core/src/lib/config/parse-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,15 @@ export const parseConfig = (configFile: FsPath): Configuration => {
}
const mergedConfig = { ...defaultConfig, ...rest };

const barrelFileName = getBarrelFileName(mergedConfig.barrelFileName);

const ignoreFileExtensions = getIgnoreFileExtensions(
mergedConfig.ignoreFileExtensions,
);

return {
...mergedConfig,
barrelFileName,
ignoreFileExtensions,
};
};
Expand All @@ -82,3 +85,9 @@ function getIgnoreFileExtensions(
: ignoreFileExtensions;
return Array.from(new Set(extensions.map((ext) => ext.toLowerCase())));
}

function getBarrelFileName(
barrelFileName: string | string[],
): string[] {
return Array.isArray(barrelFileName) ? barrelFileName : [barrelFileName];
}
Loading
Loading