Imposing Constraints on the Dependency Graph
If you partition your code into well-defined cohesive units, even a small organization will end up with a dozen apps and dozens or hundreds of libs. If all of them can depend on each other freely, the chaos will ensue, and the workspace will become unmanageable.
To help with that Nx uses code analyses to make sure projects can only depend on each other's well-defined public API. It also allows you to declaratively impose constraints on how projects can depend on each other.
Tags
Nx comes with a generic mechanism for expressing constraints: tags.
First, use nx.json
to annotate your projects with tags. In this example, we will use three tags: scope:client
. scope:admin
, scope:shared
.
1{
2 "npmScope": "myorg",
3 "implicitDependencies": {
4 "package.json": "*",
5 "tsconfig.base.json": "*",
6 "nx.json": "*"
7 },
8 "projects": {
9 "client": {
10 "tags": ["scope:client"],
11 "implicitDependencies": []
12 },
13 "client-e2e": {
14 "tags": ["scope:client"],
15 "implicitDependencies": ["client"]
16 },
17 "admin": {
18 "tags": ["scope:admin"],
19 "implicitDependencies": []
20 },
21 "admin-e2e": {
22 "tags": ["scope:admin"],
23 "implicitDependencies": ["admin"]
24 },
25 "client-feature-main": {
26 "tags": ["scope:client"],
27 "implicitDependencies": []
28 },
29 "admin-feature-permissions": {
30 "tags": ["scope:admin"],
31 "implicitDependencies": []
32 },
33 "components-shared": {
34 "tags": ["scope:shared"],
35 "implicitDependencies": []
36 }
37 }
38}
Next you should update your root lint configuration:
- If you are using ESLint you should look for an existing rule entry in your root
.eslintrc.json
called"@nrwl/nx/enforce-module-boundaries"
and you should update the"depConstraints"
:
1{
2 // ... more ESLint config here
3
4 // @nrwl/nx/enforce-module-boundaries should already exist within an "overrides" block using `"files": ["*.ts", "*.tsx", "*.js", "*.jsx",]`
5 "@nrwl/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 }
25 ]
26
27 // ... more ESLint config here
28}
- If you are using TSLint you should look for an existing rule entry in your root
tslint.json
called"nx-enforce-module-boundaries"
and you should update the"depConstraints"
:
1{
2 // ... more TSLint config here
3
4 // nx-enforce-module-boundaries should already exist at the top-level of your config
5 "nx-enforce-module-boundaries": [
6 true,
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 }
25 ]
26
27 // ... more TSLint config here
28}
With these constraints in place, scope:client
projects can only depend on other scope:client
projects or on scope:shared
projects. And scope:admin
projects can only depend on other scope:admin
projects or on scope:shared
projects. So scope:client
and scope:admin
cannot depend on each other.
Projects without any tags cannot depend on any other projects. If you add the following, projects without any tags will be able to depend on any other project.
1{
2 "sourceTag": "*",
3 "onlyDependOnLibsWithTags": ["*"]
4}
If you try to violate the constrains, you will get an error:
A project tagged with "scope:admin" can only depend on projects tagged with "scoped:shared" or "scope:admin".
Exceptions
The "allow": []
are the list of imports that won't fail linting.
"allow": ['@myorg/mylib/testing']
allows importing'@myorg/mylib/testing'
."allow": ['@myorg/mylib/*']
allows importing'@myorg/mylib/a'
but not'@myorg/mylib/a/b'
."allow": ['@myorg/mylib/**']
allows importing'@myorg/mylib/a'
and'@myorg/mylib/a/b'
."allow": ['@myorg/**/testing']
allows importing'@myorg/mylib/testing'
and'@myorg/nested/lib/testing'
.
Multiple Dimensions
The example above 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.