Sheriff enforces module boundaries and dependency rules in TypeScript.
It is easy to use and has zero dependencies. The only peer dependency is TypeScript itself.
Some examples are located in ./test-projects/.
- 1. Installation & Setup
- 2. Video Introduction
- 3. Module Boundaries
- 4. Dependency Rules
- 5. Integrating Sheriff into large Projects
- 6. Planned Features
npm install -D @softarc/sheriff-core @softarc/eslint-plugin-sheriff
In your eslintrc.json, insert the rules:
{
"files": ["*.ts"],
"extends": ["plugin:@softarc/sheriff/default"]
}
Show Example for Angular (CLI)
{
"root": true,
"ignorePatterns": ["**/*"],
"overrides": [
// existing rules...
{
"files": ["*.ts"],
"extends": ["plugin:@softarc/sheriff/default"]
}
]
}
Show Example for Angular (NX)
{
"root": true,
"ignorePatterns": ["**/*"],
"plugins": ["@nrwl/nx"],
"overrides": [
// existing rules...
{
"files": ["*.ts"],
"extends": ["plugin:@softarc/sheriff/default"]
}
]
}
Every directory with an index.ts is a module. The index.ts exports those files that should be accessible from the outside.
In the screenshot below, you see an index.ts, which exposes the holidays-facade.service.ts, but encapsulates the internal.service.ts.
Every file outside of that directory (module) now gets a linting error when it imports the internal.service.ts.
Sheriff allows the configuration of access rules between modules.
For that, there has to be a sheriff.config.ts in the project's root folder. The config assigns tags to every directory that represents a module, i.e. it contains an index.ts.
The dependency rules operate on these tags.
The following snippet shows a configuration where four directories are assigned to a domain and to a module type:
import { SheriffConfig } from '@softarc/sheriff-core';
export const sheriffConfig: SheriffConfig = {
version: 1,
tagging: {
'src/app/holidays/feature': ['domain:holidays', 'type:feature'],
'src/app/holidays/data': ['domain:holidays', 'type:data'],
'src/app/customers/feature': ['domain:customers', 'type:feature'],
'src/app/customers/data': ['domain:customers', 'type:data'],
},
depRules: {},
};
If modules of the same domains can access each other and if a module of type feature can access type data but not the other way around, the depRules
in the config would have these values.
import { SheriffConfig } from '@softarc/sheriff-core';
export const sheriffConfig: SheriffConfig = {
version: 1,
tagging: {
'src/app/holidays/feature': ['domain:holidays', 'type:feature'],
'src/app/holidays/data': ['domain:holidays', 'type:data'],
'src/app/customers/feature': ['domain:customers', 'type:feature'],
'src/app/customers/data': ['domain:customers', 'type:data'],
},
depRules: {
'domain:holidays': ['domain:holidays', 'shared'],
'domain:customers': ['domain:customers', 'shared'],
'type:feature': 'type:data',
},
};
If those roles are broken, a linting error is raised:
The configuration can be simplified by nesting the paths. Multiple levels are allowed.
import { SheriffConfig } from '@softarc/sheriff-core';
export const sheriffConfig: SheriffConfig = {
version: 1,
tagging: {
'src/app': {
holidays: {
feature: ['domain:holidays', 'type:feature'],
data: ['domain:holidays', 'type:data'],
},
customers: {
feature: ['domain:customers', 'type:feature'],
data: ['domain:customers', 'type:data'],
},
},
},
depRules: {
'domain:holidays': ['domain:holidays', 'shared'],
'domain:customers': ['domain:customers', 'shared'],
'type:feature': 'type:data',
},
};
For repeating patterns, one can also use placeholders with the syntax <name>
:
import { SheriffConfig } from '@softarc/sheriff-core';
export const sheriffConfig: SheriffConfig = {
version: 1,
tagging: {
'src/app': {
holidays: {
'<type>': ['domain:holidays', 'type:<type>'],
},
customers: {
'<type>': ['domain:customers', 'type:<type>'],
},
},
},
depRules: {
'domain:holidays': ['domain:holidays', 'shared'],
'domain:customers': ['domain:customers', 'shared'],
'type:feature': 'type:data',
},
};
Since placeholders are allowed on all levels, we could have the following improved version:
import { SheriffConfig } from '@softarc/sheriff-core';
export const sheriffConfig: SheriffConfig = {
version: 1,
tagging: {
'src/app/<domain>/<type>': ['domain:<domain>', 'type:<type>'],
},
depRules: {
'domain:holidays': ['domain:holidays', 'shared'],
'domain:customers': ['domain:customers', 'shared'],
'type:feature': 'type:data',
},
};
The values of the dependency rules can also be implemented as functions. The names of the tags can include wildcards.
So an optimised version would look like this:
import { SheriffConfig } from '@softarc/sheriff-core';
export const sheriffConfig: SheriffConfig = {
version: 1,
tagging: {
'src/app/<domain>/<type>': ['domain:<domain>', 'type:<type>'],
},
depRules: {
'domain:*': [({ from, to }) => from === to, 'shared'],
'type:feature': 'type:data',
},
};
or
import { sameTag, SheriffConfig } from '@softarc/sheriff-core';
export const sheriffConfig: SheriffConfig = {
version: 1,
tagging: {
'src/app/<domain>/<type>': ['domain:<domain>', 'type:<type>'],
},
depRules: {
'domain:*': [sameTag, 'shared'],
'type:feature': 'type:data',
},
};
It is usually not possible to modularise an existing codebase at once. Instead, we have to integrate Sheriff incrementally.
The recommended approach is to pick one directory and set it as a module. Let's call that module shared. All files from the outside have to import now from the module's index.ts.
Once a single module does exist, Sheriff automatically assigned the root module to the rest. If files from shared need to import from root, an index.ts in root is required as well.
We can disable the deep import checks for the root module by setting excludeRoot
in sheriff.config.ts to true.
Example:
export const config: SheriffConfig = {
excludeRoot: true,
tagging: {
'src/shared': 'shared',
},
depRules: {
'*': anyTag,
},
};
Note, that dependency rules are also disabled.
- Once shared is complete, create a new module and do the same.
- You can wait to configure the dependency rules at the end or together with the modules incrementally.
For feature requests, please add an issue at https://github.com/softarc-consulting/sheriff.
- Editor
- Print modules with their tags
- Testing Dependency Rules
- Angular Schematic
- Feature Shell: It shouldn't be necessary to create a feature subdirectory for a domain, since feature has access to everything
- Dependency rules for node_modules
- CLI as alternative to eslint
- Find cyclic dependencies
- Find unused files
- TestCoverage 100%
- UI for Configuration
- Migration from Nx (automatic)
- Cache