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
ordersto import fromshared-uibut not the other way around - we might want to allow
ordersto import fromproductsbut not the other way around - we might want to allow all libraries to import the
shared-uicomponents, 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 package.json of your orders library and assign the tags type:feature and scope:orders to it.
{12 collapsed lines
"name": "@react-monorepo/orders", "version": "0.0.1", "main": "./src/index.ts", "types": "./src/index.ts", "exports": { ".": { "types": "./src/index.ts", "import": "./src/index.ts", "default": "./src/index.ts" }, "./package.json": "./package.json" }, "nx": { "tags": [ "type:feature", "scope:orders" ] }}Then go to the package.json of your products library and assign the tags type:feature and scope:products to it.
{12 collapsed lines
"name": "@react-monorepo/products", "version": "0.0.1", "main": "./src/index.ts", "types": "./src/index.ts", "exports": { ".": { "types": "./src/index.ts", "import": "./src/index.ts", "default": "./src/index.ts" }, "./package.json": "./package.json" }, "nx": { "tags": [ "type:feature", "scope:products" ] }}Finally, go to the package.json of the shared-ui library and assign the tags type:ui and scope:shared to it.
{12 collapsed lines
"name": "@react-monorepo/ui", "version": "0.0.1", "main": "./src/index.ts", "types": "./src/index.ts", "exports": { ".": { "types": "./src/index.ts", "import": "./src/index.ts", "default": "./src/index.ts" }, "./package.json": "./package.json" }, "nx": { "tags": [ "type:ui", "scope:shared" ] }}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:featureshould be able to import fromtype:featureandtype:uitype:uishould only be able to import fromtype:uiscope:ordersshould be able to import fromscope:orders,scope:sharedandscope:productsscope:productsshould be able to import fromscope:productsandscope: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.tsx file and import the Orders component from the orders project:
import styles from './products.module.css';
// This import is not allowed 👇import { Orders } from '@react-monorepo/orders';
export function Products() { return ( <div className={styles['container']}> <h1>Welcome to Products!</h1> <p>This is a change. 👋</p> </div> );}
export default Products;If you lint your workspace you’ll get an error now:
nx run-many -t lint ✔ nx run @react-monorepo/orders:lint [existing outputs match the cache, left as is] ✔ nx run @react-monorepo/react-store:lint [existing outputs match the cache, left as is] ✔ nx run @react-monorepo/inventory:lint [existing outputs match the cache, left as is] ✔ nx run @react-monorepo/ui:lint [existing outputs match the cache, left as is] ✔ nx run inventory-e2e:lint [existing outputs match the cache, left as is] ✔ nx run react-store-e2e:lint (877ms)
——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— ✖ nx run @react-monorepo/products:lint > eslint .
/home/tutorial/libs/products/src/lib/products.tsx 3:1 error A project tagged with "scope:products" can only depend on libs tagged with "scope:products", "scope:shared" @nx/enforce-module-boundaries 3:10 warning 'Orders' is defined but never used @typescript-eslint/no-unused-vars
✖ 2 problems (1 error, 1 warning)
———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— NX Ran target lint for 7 projects (1s)
✔ 6/7 succeeded [5 read from cache]
✖ 1/7 targets failed, including the following:
- nx run @react-monorepo/products:lintIf 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