Imposing Constraints with Module Boundary Rules
Once you modularize your codebase you want to make sure that the libs are not coupled to each other in an uncontrolled way. Here are some examples of how we might want to guard our small demo workspace:
- we might want to allow
orders
to import fromshared-ui
but not the other way around - we might want to allow
orders
to import fromproducts
but not the other way around - we might want to allow all libraries to import the
shared-ui
components, but not the other way around
When building these kinds of constraints you usually have two dimensions:
- type of project: what is the type of your library. Example: “feature” library, “utility” library, “data-access” library, “ui” library
- scope (domain) of the project: what domain area is covered by the project. Example: “orders”, “products”, “shared” … this really depends on the type of product you’re developing
Nx comes with a generic mechanism that allows you to assign “tags” to projects. “tags” are arbitrary strings you can assign to a project that can be used later when defining boundaries between projects. For example, go to the project.json
of your orders
library and assign the tags type:feature
and scope:orders
to it.
{4 collapsed lines
"name": "orders", "$schema": "../../node_modules/nx/schemas/project-schema.json", "sourceRoot": "libs/orders/src", "projectType": "library", "tags": ["type:feature", "scope:orders"],13 collapsed lines
"// targets": "to see all targets run: nx show project orders --web", "targets": { "test": { "executor": "@nx/jest:jest", "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], "options": { "jestConfig": "libs/orders/jest.config.ts" } }, "lint": { "executor": "@nx/eslint:lint" } }}
Then go to the project.json
of your products
library and assign the tags type:feature
and scope:products
to it.
{4 collapsed lines
"name": "products", "$schema": "../../node_modules/nx/schemas/project-schema.json", "sourceRoot": "libs/products/src", "projectType": "library", "tags": ["type:feature", "scope:products"],13 collapsed lines
"// targets": "to see all targets run: nx show project products --web", "targets": { "test": { "executor": "@nx/jest:jest", "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], "options": { "jestConfig": "libs/products/jest.config.ts" } }, "lint": { "executor": "@nx/eslint:lint" } }}
Finally, go to the project.json
of the shared-ui
library and assign the tags type:ui
and scope:shared
to it.
{4 collapsed lines
"name": "ui", "$schema": "../../../node_modules/nx/schemas/project-schema.json", "sourceRoot": "libs/shared/ui/src", "projectType": "library", "tags": ["type:ui", "scope:shared"],13 collapsed lines
"// targets": "to see all targets run: nx show project ui --web", "targets": { "test": { "executor": "@nx/jest:jest", "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], "options": { "jestConfig": "libs/shared/ui/jest.config.ts" } }, "lint": { "executor": "@nx/eslint:lint" } }}
Notice how we assign scope:shared
to our UI library because it is intended to be used throughout the workspace.
Next, let’s come up with a set of rules based on these tags:
type:feature
should be able to import fromtype:feature
andtype:ui
type:ui
should only be able to import fromtype:ui
scope:orders
should be able to import fromscope:orders
,scope:shared
andscope:products
scope:products
should be able to import fromscope:products
andscope:shared
To enforce the rules, Nx ships with a custom ESLint rule. Open the eslint.config.mjs
at the root of the workspace and add the following depConstraints
in the @nx/enforce-module-boundaries
rule configuration:
2 collapsed lines
import nx from '@nx/eslint-plugin';
export default [6 collapsed lines
...nx.configs['flat/base'], ...nx.configs['flat/typescript'], ...nx.configs['flat/javascript'], { ignores: ['**/dist'], }, { files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], rules: { '@nx/enforce-module-boundaries': [ 'error', { enforceBuildableLibDependency: true, allow: ['^.*/eslint(\\.base)?\\.config\\.[cm]?js$'], depConstraints: [ { sourceTag: "type:feature", onlyDependOnLibsWithTags: ["type:feature", "type:ui"] }, { sourceTag: "type:ui", onlyDependOnLibsWithTags: ["type:ui"] }, { sourceTag: "scope:orders", onlyDependOnLibsWithTags: [ "scope:orders", "scope:products", "scope:shared" ] }, { sourceTag: "scope:products", onlyDependOnLibsWithTags: ["scope:products", "scope:shared"] }, { sourceTag: "scope:shared", onlyDependOnLibsWithTags: ["scope:shared"] }, { sourceTag: '*', onlyDependOnLibsWithTags: ['*'], }, ], }, ], }, },14 collapsed lines
{ files: [ '**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts', '**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs', ], // Override or add rules here rules: {}, },];
To test it, go to your /libs/products/src/lib/products/products.component.ts
file and import the Orders
component from the orders
project:
If you lint your workspace you’ll get an error now:
nx run-many -t lint
Running target lint for 7 projects✖ nx run products:lint Linting "products"...
/home/tutorial/libs/products/src/lib/products/products.component.ts 5:1 error A project tagged with "scope:products" can only depend on libs tagged with "scope:products", "scope:shared" @nx/enforce-module-boundaries 5:10 warning 'OrdersComponent' is defined but never used @typescript-eslint/no-unused-vars
✖ 2 problems (1 error, 1 warning)
Lint warnings found in the listed files.
Lint errors found in the listed files.
✔ nx run orders:lint (996ms)✔ nx run angular-store:lint (1s)✔ nx run angular-store-e2e:lint (581ms)✔ nx run inventory-e2e:lint (588ms)✔ nx run inventory:lint (836ms)✔ nx run shared-ui:lint (753ms)
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
NX Ran target lint for 7 projects (2s)
✔ 6/7 succeeded [0 read from cache]
✖ 1/7 targets failed, including the following: - nx run products:lint
If you have the ESLint plugin installed in your IDE you should also immediately see an error.
Learn more about how to enforce module boundaries.
- Stubbing git
- Installing dependencies