| name | module-boundaries |
| description | Nx tags and the ESLint depConstraints that enforce them. Use when creating a library, choosing or registering a scope: tag, or fixing an @nx/enforce-module-boundaries lint error. A tag with no depConstraints entry is unconstrained, which silently defeats the rules. |
Skill: Module Boundaries โ ESLint + Nx Tags
This project enforces dependency rules between libraries using @nx/enforce-module-boundaries.
Every time you create a new library or application, you must register it in both project.json and eslint.base.config.mjs.
How it works โ the full chain
project.json eslint.base.config.mjs
โโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ
"tags": [ sourceTag: 'scope:profile'
"scope:profile" โโโบ onlyDependOnLibsWithTags: [...]
]
Nx reads the tags array from each project.json and uses them to resolve the sourceTag in the ESLint config. If a project with tag scope:profile tries to import from a project not listed in its onlyDependOnLibsWithTags, ESLint throws an error.
Tag naming convention
| Project type | Tag pattern | Example |
|---|
| Application | scope:<app-name> | scope:demo-app |
| Feature library | scope:<feature-name> | scope:profile |
| Shared utility library | scope:<name>-lib | scope:entity-lib, scope:ui-lib |
| Environment | scope:environment-lib | scope:environment-lib |
The scope: prefix is mandatory. It is what connects the project.json tag to the sourceTag in the ESLint config.
Current projects and their tags
| Project | Path | project.json tag | Can depend on |
|---|
| demo-app | apps/demo-app | scope:demo-app | everything below |
| environment | libs/environment | scope:environment-lib | (nothing) |
| entity | libs/shared/entity | scope:entity-lib | (nothing) |
| theme | libs/theme | scope:theme-lib | (nothing) |
| analytics | libs/shared/analytics | scope:analytics-lib | (nothing) |
| analytics-firebase | libs/shared/analytics-firebase | scope:analytics-firebase-lib | analytics-lib, environment-lib |
| translation | libs/shared/translation | scope:translate | entity-lib, environment-lib |
| ui | libs/shared/ui | scope:ui-lib | analytics-lib, entity-lib, environment-lib, theme-lib, translate |
| profile | libs/profile | scope:profile | entity-lib, environment-lib |
| settings | libs/settings | scope:settings | entity-lib, environment-lib, theme-lib, translate, ui-lib |
Dependency graph (allowed imports)
demo-app โ the only project that may compose everything
โโโ @libs/entity (scope:entity-lib)
โโโ @libs/ui (scope:ui-lib)
โโโ @libs/theme (scope:theme-lib)
โโโ @libs/translation (scope:translate)
โโโ @libs/environment (scope:environment-lib)
โโโ @libs/analytics (scope:analytics-lib)
โโโ @libs/analytics-firebase (scope:analytics-firebase-lib)
โโโ @libs/profile (scope:profile)
โโโ @libs/settings (scope:settings)
profile
โโโ @libs/entity (scope:entity-lib)
โโโ @libs/environment (scope:environment-lib)
settings
โโโ @libs/entity (scope:entity-lib)
โโโ @libs/environment (scope:environment-lib)
โโโ @libs/theme (scope:theme-lib)
โโโ @libs/translation (scope:translate)
โโโ @libs/ui (scope:ui-lib)
shared/ui
โโโ @libs/analytics (scope:analytics-lib)
โโโ @libs/entity (scope:entity-lib)
โโโ @libs/environment (scope:environment-lib)
โโโ @libs/theme (scope:theme-lib)
โโโ @libs/translation (scope:translate)
shared/analytics-firebase
โโโ @libs/analytics (scope:analytics-lib)
โโโ @libs/environment (scope:environment-lib)
shared/translation
โโโ @libs/entity (scope:entity-lib)
โโโ @libs/environment (scope:environment-lib)
shared/entity โ (no dependencies)
shared/analytics โ (no dependencies)
environment โ (no dependencies)
theme โ (no dependencies)
Imports NOT allowed (ESLint will error):
profile importing from @libs/settings, or settings from @libs/profile โ
features never see each other. They compose through the app.
- Any library except the app importing
@libs/analytics-firebase โ picking a
vendor is a composition decision, made once in app.config.ts. Libraries depend
on @libs/analytics, the contract, and never learn what is behind it.
shared/entity, theme, analytics or environment importing anything
shared/translation importing from @libs/profile
There is deliberately no shared state library. Feature state lives in a
signalStore inside its own feature lib (see the ngrx-state skill), so a
cross-cutting @libs/store would only invite duplicated sources of truth.
Step-by-step: registering a new library
1. Set the tag in project.json
{
"name": "<feature-name>",
"tags": ["scope:<feature-name>"],
...
}
2. Add the sourceTag entry in eslint.base.config.mjs
{
sourceTag: 'scope:<feature-name>',
onlyDependOnLibsWithTags: [
'scope:entity-lib',
'scope:environment-lib',
],
},
3. Add the inverse rule for any project that needs to import the new lib
If demo-app needs to import your new library, add scope:<feature-name> to its onlyDependOnLibsWithTags:
{
sourceTag: 'scope:demo-app',
onlyDependOnLibsWithTags: [
'scope:entity-lib',
'scope:ui-lib',
'scope:environment-lib',
'scope:profile',
'scope:<feature-name>',
],
},
Complete example โ adding libs/orders
Step 1 โ libs/orders/project.json:
{
"name": "orders",
"tags": ["scope:orders"],
...
}
Step 2 โ tsconfig.base.json:
"@libs/orders": ["libs/orders/src/index.ts"]
Step 3 โ eslint.base.config.mjs:
depConstraints: [
{
sourceTag: 'scope:orders',
onlyDependOnLibsWithTags: ['scope:entity-lib', 'scope:environment-lib'],
},
{
sourceTag: 'scope:demo-app',
onlyDependOnLibsWithTags: [
'scope:entity-lib',
'scope:ui-lib',
'scope:environment-lib',
'scope:profile',
'scope:orders',
],
},
];
Verifying the rules
Run lint across all projects to validate:
npx nx run-many --target=lint --all
If you import from a lib that is not allowed, you will see:
A project tagged with "scope:profile" can only depend on libs tagged with
"scope:entity-lib", "scope:environment-lib"
Checklist when creating a new library