Tag in Multiple Dimensions
The example listed in Enforce Module Boundaries shows using a single dimension: scope
. It's the most commonly used one. But you can find other dimensions useful. You can define which projects contain components, state management code, and features, so you, for instance, can disallow projects containing dumb UI components to depend on state management code. You can define which projects are experimental and which are stable, so stable applications cannot depend on experimental projects etc. You can define which projects have server-side code and which have client-side code to make sure your node app doesn't bundle in your frontend framework.
Let's consider our previous three scopes - scope:client
. scope:admin
, scope:shared
. By using just a single dimension, our client-e2e
application would be able to import client
application or client-feature-main
. This is likely not something we want to allow as it's using framework that our E2E project doesn't have.
Let's add another dimension - type
. Some of our projects are applications, some are UI features and some are just plain helper libraries. Let's define three new tags: type:app
, type:feature
, type:ui
and type:util
.
Our project configurations might now look like this:
1// project "client"
2{
3 // ... more project configuration here
4
5 "tags": ["scope:client", "type:app"]
6}
7
8// project "client-e2e"
9{
10 // ... more project configuration here
11
12 "tags": ["scope:client", "type:app"],
13 "implicitDependencies": ["client"]
14}
15
16// project "admin"
17{
18 // ... more project configuration here
19
20 "tags": ["scope:admin", "type:app"]
21}
22
23// project "admin-e2e"
24{
25 // ... more project configuration here
26
27 "tags": ["scope:admin", "type:app"],
28 "implicitDependencies": ["admin"]
29}
30
31// project "client-feature-main"
32{
33 // ... more project configuration here
34
35 "tags": ["scope:client", "type:feature"]
36},
37
38// project "admin-feature-permissions"
39{
40 // ... more project configuration here
41
42 "tags": ["scope:admin", "type:feature"]
43}
44
45// project "components-shared"
46{
47 // ... more project configuration here
48
49 "tags": ["scope:shared", "type:ui"]
50}
51
52// project "utils"
53{
54 // ... more project configuration here
55
56 "tags": ["scope:shared", "type:util"]
57}
58
We can now restrict projects within the same group to depend on each other based on the type:
app
can only depend onfeature
,ui
orutil
, but not other appsfeature
cannot depend on app or another featureui
can only depend on otherui
- everyone can depend on
util
includingutil
itself
1{
2 // ... more ESLint config here
3
4 // @nx/enforce-module-boundaries should already exist at the top-level of your config
5 "@nx/enforce-module-boundaries": [
6 "error",
7 {
8 "allow": [],
9 // update depConstraints based on your tags
10 "depConstraints": [
11 {
12 "sourceTag": "scope:shared",
13 "onlyDependOnLibsWithTags": ["scope:shared"]
14 },
15 {
16 "sourceTag": "scope:admin",
17 "onlyDependOnLibsWithTags": ["scope:shared", "scope:admin"]
18 },
19 {
20 "sourceTag": "scope:client",
21 "onlyDependOnLibsWithTags": ["scope:shared", "scope:client"]
22 },
23 {
24 "sourceTag": "type:app",
25 "onlyDependOnLibsWithTags": ["type:feature", "type:ui", "type:util"]
26 },
27 {
28 "sourceTag": "type:feature",
29 "onlyDependOnLibsWithTags": ["type:ui", "type:util"]
30 },
31 {
32 "sourceTag": "type:ui",
33 "onlyDependOnLibsWithTags": ["type:ui", "type:util"]
34 },
35 {
36 "sourceTag": "type:util",
37 "onlyDependOnLibsWithTags": ["type:util"]
38 }
39 ]
40 }
41 ]
42
43 // ... more ESLint config here
44}
45
There are no limits to the number of tags, but as you add more tags the complexity of your dependency constraints rises exponentially. It's always good to draw a diagram and carefully plan the boundaries.
Matching multiple source tags
Matching just a single source tag is sometimes not enough for solving complex restrictions. To avoid creating ad-hoc tags that are only meant for specific constraints, you can also combine multiple tags with allSourceTags
. Each tag in the array must be matched for a constraint to be applied:
1{
2 // ... more ESLint config here
3
4 // @nx/enforce-module-boundaries should already exist at the top-level of your config
5 "@nx/enforce-module-boundaries": [
6 "error",
7 {
8 "allow": [],
9 // update depConstraints based on your tags
10 "depConstraints": [
11 {
12 // this constraint applies to all "admin" projects
13 "sourceTag": "scope:admin",
14 "onlyDependOnLibsWithTags": ["scope:shared", "scope:admin"]
15 },
16 {
17 "sourceTag": "type:ui",
18 "onlyDependOnLibsWithTags": ["type:ui", "type:util"]
19 },
20 {
21 // we don't want our admin ui components to depend on anything except utilities,
22 // and we also want to ban router imports
23 "allSourceTags": ["scope:admin", "type:ui"],
24 "onlyDependOnLibsWithTags": ["type:util"],
25 "bannedExternalImports": ["*router*"]
26 }
27 ]
28 }
29 ]
30
31 // ... more ESLint config here
32}
33