Dependency Rules
Introduction
Dependency rules determine which modules can access each other. Since managing dependencies on a per-module basis doesn't scale well, Sheriff utilizes tags to group modules together. Dependency rules are then defined based on these tags.
Each tag specifies a list of other tags it can access. To maintain clarity, it’s best practice to categorize tags into two groups: one for defining the module's domain/scope and another for defining the module's type.
For instance, if an application includes a customer domain and a holiday domain, these would define the domain tags.
A domain has different modules distinguished by type tags. For example, one module might contain smart components, another might have dumb components, and another might handle logic.
Domain tags could be domain:customer
and domain:holiday
. Type tags could be type:feature
(for smart components),
type:ui
(for dumb components), or type:data
(for logic).
In this case, each module has both a domain tag and a type tag. For example, a module containing smart components for
customers would have domain:customer
and type:feature
. A module in the same domain but containing UI
components would have domain:customer
and type:ui
.
Dependency rules specify that a module tagged with domain:customer
can only access modules with the same domain tag.
Additionally, a module tagged with type:feature
can access modules tagged with type:ui
and type:data
.
When a module containing smart components needs to access a dumb component, Sheriff retrieves the tags for both modules. It then checks each tag of the smart component module to determine if it is permitted to access the corresponding tags of the dumb component module.
Since both modules share the same domain, access is allowed based on the domain tag. Additionally, because "type: feature" is allowed to access "type:ui", all tags are cleared, and access is granted.
Conversely, if a dumb component tries to access a smart component, access would be denied based on the type tag.
If a smart component from the customer domain tries to access a dumb component from another domain, access would be denied due to the domain tag.
Automatic Tagging
Sheriff automatically detects modules and assigns the noTag
tag to them.
It assigns all files that aren't part of a module to the root
module. The root module gets the root
tag.
It's essential to set up the dependency rules. Specifically, the root
tag (i.e., the root module) needs to access all modules tagged with noTag
.
The depRules
property in the sheriff.config.ts file defines the dependency rules. This property is an object literal where each key represents the tag of a module that wants to access another module. Its value specifies the tags it can access.
Initially, all modules can access each other, meaning that every noTag
module can access other noTag
modules.
The initial configuration from the CLI includes this setup.
Here’s an example configuration in sheriff.config.ts
:
import { SheriffConfig } from '@softarc/sheriff-core';
export const sheriffConfig: SheriffConfig = {
depRules: {
root: 'noTag',
noTag: ['noTag', 'root'],
},
};
That is also the recommendation for existing projects because it allows an incremental integration of Sheriff.
For green-field projects, the manual tagging is the better option.
To disable automatic tagging, set autoTagging
to false
:
import { SheriffConfig } from '@softarc/sheriff-core';
export const sheriffConfig: SheriffConfig = {
autoTagging: false,
modules: {
// see below...
},
};
The root
Tag
Given the following project structure:
src/app ├── main.ts ├── app.config.ts ├── app.component.ts ├── holidays │ ├── data │ │ ├── index.ts │ │ ├── internal.service.ts │ │ └── holidays-data.service.ts │ ├── feature │ │ ├── index.ts │ │ └── holidays-facade.service.ts │── core │ ├── header.component.ts │ ├── footer.component.ts
The directories src/app/holidays/data
and src/app/holidays/feature
are barrel modules. All other files are part of the root module, which is automatically tagged with root
by Sheriff
This tagging of the root
module cannot be changed. With disabled barrel-less mode (enableBarrelLess: false
), which is the default, no module can access the root module.
The property excludeRoot
can disable this behavior. By default. The best option, though, is to enable barrel-less mode which makes root
a barrel-less module.
Manual Tagging
The modules
property in the sheriff.config.ts
defines barrel-less modules but also assigns tags to modules, regardless if barrel or barrel-less.
The keys of modules
represent the module directories, and the corresponding values are the tags assigned to those modules.
The following snippet demonstrates a configuration where four directories are assigned both a domain and a module type:
import { SheriffConfig } from '@softarc/sheriff-core';
export const sheriffConfig: SheriffConfig = {
modules: {
'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: {},
};
By using domain:_
and type:_
define two dimension for the whole project. The following rules should apply
- A module can only depend on other modules of the same domain.
- A module tagged as
type:feature
can depend ontype:data
, but the reverse is not allowed. - The
root
module can depend on modules tagged astype:feature
. Since the root module only has theroot
tag, there is no need to include domain tags.
import { SheriffConfig } from '@softarc/sheriff-core';
export const sheriffConfig: SheriffConfig = {
modules: {
'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'], // Rule 1
'domain:customers': ['domain:customers'], // Rule 1
'type:feature': 'type:data', // Rule 2
root: 'type:feature', // Rule 3
},
};
If these rules are violated, a linting error will be triggered:
If only the modules within the director "holidays" should get tags, and the other modules should be auto-tagged, i.e. noTag
, the configuration would look like this:
import { SheriffConfig } from '@softarc/sheriff-core';
export const sheriffConfig: SheriffConfig = {
modules: {
'src/app/holidays/feature': ['domain:holidays', 'type:feature'],
'src/app/holidays/data': ['domain:holidays', 'type:data'],
},
depRules: {
'domain:holidays': ['domain:holidays', 'noTag'],
'type:feature': ['type:data', 'noTag'],
root: ['type:feature', 'noTag'],
noTag: ['noTag', 'root'],
},
};
Note: This setup allows any module from domain:holidays
to depend on modules within the customers
directory, but the reverse is not permitted.
Nested Paths
Nested paths simplify the configuration. Multiple levels are allowed.
import { SheriffConfig } from '@softarc/sheriff-core';
export const sheriffConfig: SheriffConfig = {
modules: {
'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'],
'domain:customers': ['domain:customers'],
'type:feature': 'type:data',
root: ['type:feature'],
},
};
Placeholders
Placeholders help with repeating patterns. They have the syntax <name>
, where name
is the placeholder name.
import { SheriffConfig } from '@softarc/sheriff-core';
export const sheriffConfig: SheriffConfig = {
modules: {
'src/app': {
holidays: {
'<type>': ['domain:holidays', 'type:<type>'],
},
customers: {
'<type>': ['domain:customers', 'type:<type>'],
},
},
},
depRules: {
'domain:holidays': ['domain:holidays'],
'domain:customers': ['domain:customers'],
'type:feature': 'type:data',
root: ['type:feature'],
},
};
Placeholders are available on all levels. The configuration could therefore further be improved.
import { SheriffConfig } from '@softarc/sheriff-core';
export const sheriffConfig: SheriffConfig = {
modules: {
'src/app/<domain>/<type>': ['domain:<domain>', 'type:<type>'],
},
depRules: {
'domain:holidays': ['domain:holidays'],
'domain:customers': ['domain:customers'],
'type:feature': 'type:data',
root: ['type:feature'],
},
};
depRules
Functions & Wildcards
depRules
allows functions instead of static values. The names of the tags can include wildcards:
import { SheriffConfig } from '@softarc/sheriff-core';
export const sheriffConfig: SheriffConfig = {
modules: {
'src/app/<domain>/<type>': ['domain:<domain>', 'type:<type>'],
},
depRules: {
'domain:*': ({ from, to }) => from === to,
'type:feature': 'type:data',
root: ['type:feature'],
},
};
or use sameTag
, which is a pre-defined function.
import { sameTag, SheriffConfig } from '@softarc/sheriff-core';
export const sheriffConfig: SheriffConfig = {
modules: {
'src/app/<domain>/<type>': ['domain:<domain>', 'type:<type>'],
},
depRules: {
'domain:*': [sameTag, 'shared'],
'type:feature': 'type:data',
root: ['type:feature'],
},
};