Monorepo Setup
This guide shows how to configure ESLint Plugin Boundaries in monorepo environments, where you have multiple packages within a single repository.
Understanding External vs Local Dependencies
By default, the plugin categorizes dependencies as:
- External: npm packages in
node_modulesand unresolvable imports - Local: all other resolved file paths within your project
This is relevant because element-types rule only applies to local dependencies, and external rule only to external dependencies.
In a monorepo, you might want different behavior:
- Treat inter-package dependencies as external - Apply boundary rules only within each package
- Treat inter-package dependencies as local - Apply boundary rules across all packages
- Mix both approaches - Some packages as external, others as local
The boundaries/flag-as-external setting gives you full control over this categorization.
Eslint Execution Context
It is also important the path where ESlint is executed from, as the files patterns in the config should be relative to that path (usually the monorepo root, but can also be a package directory if eslint is run from there, such as in an nx workspace with eslint scripts defined per package).
Remember: Even when splitting the eslint configuration per-package, the files property is relative to where ESLint is executed (usually the repository root), not to the config file location.
All examples in this page assume eslint is executed from the monorepo root, so all files patterns are relative to that path, but you can easily adapt them if eslint is run from each package directory by changing the files patterns accordingly:
- If ESLint runs from monorepo root:
files: ["packages/app/**/*.ts"]settings: { "boundaries/root-path": resolve(import.meta.dirname, "packages/app") }
- If running from package dir:
files: ["**/*.ts"]settings: { "boundaries/root-path": resolve(import.meta.dirname) }
Pattern matching and rootPath
Element descriptor patterns are relative to rootPath setting. The matching behavior varies by mode:
-
fileandfoldermodes: Patterns are evaluated right-to-left (from the end of the path). This means patterns like*.model.tsorservices/*will match files regardless of their location withinrootPath, making the relativity less critical. -
fullmode: Patterns must match the complete path relative torootPath. For files outsiderootPath, patterns must match absolute paths since those files retain their absolute path for matching.
For detailed examples of how each mode works, see the Elements documentation.
Scenario 1: Inter-package Dependencies as External (Package Isolation)
Use case: You want to apply boundary rules only within each package. Inter-package imports should be treated like external dependencies.
Approach: Use outsideRootPath to flag imports outside the package directory as external.
Configuration
import { resolve } from "node:path";
export default [{
// Apply to all files in this package
files: ["packages/app/**/*.ts"],
settings: {
// Set root to the specific package directory
"boundaries/root-path": resolve(import.meta.dirname, "packages/app"),
// Imports outside this package are external
"boundaries/flag-as-external": {
outsideRootPath: true
},
"boundaries/elements": [
{
type: "component",
pattern: "src/components/**/*.ts"
},
{
type: "service",
pattern: "src/services/**/*.ts"
}
]
},
rules: {
// These rules only apply within the "app" package
"boundaries/element-types": ["error", {
default: "disallow",
rules: [
{
from: "component",
allow: ["component", "service"]
}
]
}]
}
}];
Behavior
// packages/app/src/components/UserList.ts
// ✅ Local dependency - boundary rules apply
import { Button } from './Button';
// ✅ Local dependency - boundary rules apply
import { UserService } from '../services/UserService';
// ⚠️ External dependency - boundary rules DON'T apply
import { formatDate } from '@monorepo/shared';
// ⚠️ External dependency - boundary rules DON'T apply
import { lodash } from 'lodash';
Scenario 2: Inter-package Dependencies as External (Shared rules for all packages)
Use case: Similar to Scenario 1, but you want to share the same boundary rules configuration across all packages in the monorepo.
Approach: Use customSourcePatterns to flag specific import patterns as external.
Configuration
import { resolve } from "node:path";
export default [{
// Apply to all packages
files: ["packages/*/src/**/*.ts"],
settings: {
"boundaries/root-path": resolve(import.meta.dirname),
// Organization packages are external
"boundaries/flag-as-external": {
customSourcePatterns: ["@myorg/*"]
},
"boundaries/elements": [
{
type: "component",
pattern: "packages/*/src/components/**/*.ts",
capture: ["package"]
},
{
type: "service",
pattern: "packages/*/src/services/**/*.ts",
capture: ["package"]
}
]
},
rules: {
"boundaries/element-types": ["error", {
default: "disallow",
rules: [
{
from: "component",
allow: ["component", "service"]
}
]
}]
}
}];
Behavior
// packages/app/src/components/UserList.ts
// ✅ Local dependency - boundary rules apply
import { Button } from './Button';
// ✅ Local dependency - boundary rules apply
import { UserService } from '../../services/UserService';
// ⚠️ External dependency - matches @myorg/* pattern
import { formatDate } from '@myorg/shared';
// ⚠️ External dependency - npm package
import { map } from 'lodash';
You can still use the boundaries/external rule to enforce constraints on inter-package dependencies treated as external. For example, you can prevent certain packages from importing others by defining rules based on the external import patterns.
Scenario 3: Inter-package Dependencies as Local (Monorepo-wide Rules)
Use case: You want to apply granular boundary rules across all packages in the monorepo. Inter-package imports should be treated as local dependencies.
Approach: Keep default settings (or explicitly set outsideRootPath: false) and define elements that span multiple packages.
Configuration
import { resolve } from "node:path";
export default [{
// Apply to all packages
files: ["packages/*/src/**/*.ts"],
settings: {
"boundaries/root-path": resolve(import.meta.dirname),
"boundaries/elements": [
{
type: "component",
pattern: "packages/*/src/components/**/*.ts",
capture: ["package"]
},
{
type: "service",
pattern: "packages/*/src/services/**/*.ts",
capture: ["package"]
}
]
},
rules: {
"boundaries/element-types": ["error", {
default: "disallow",
rules: [
{
from: "component",
allow: ["component", "service"]
}
]
}]
}
}];
Behavior
// packages/app/src/components/button/Button.ts
// ✅ Local dependency - allowed by rules (component → component)
import { Text } from './components/Text';
// ✅ Local dependency - allowed by rules (component → service)
import { ButtonService } from '@monorepo/shared/services/FooService';
// ❌ Local dependency - BLOCKED by rules (not explicitly allowed)
import { InternalUtil } from '@monorepo/shared/internal-utils';
// ⚠️ External dependency - boundary rules DON'T apply
import { map } from 'lodash';
With outsideRootPath: false, imports from packages/shared are categorized as local dependencies. This allows the plugin to:
- Match them against your element patterns
- Apply boundary rules between packages
- Enforce architectural constraints across your monorepo
Combining Approaches
You can combine multiple configurations with different files patterns in the same flat config file, enabling both package-level and monorepo-level rules.
For example, you could combine Scenario 3, defining rules that consider inter-package dependencies as local, and also Scenario 1, defining package-isolated rules.
You could even have different configurations considering inter-package dependencies as external to add global constraints using the external rule, while also having configurations treating them as local for granular control using the element-types rule for packages that are allowed to interact.
Common Patterns
Pattern: Selectively External Packages
Flag specific packages as external while keeping others local:
export default [{
files: ["packages/**/*.ts"],
settings: {
"boundaries/flag-as-external": {
customSourcePatterns: [
"@myorg/design-system", // Specific package
"@myorg/vendor-*" // All vendor packages
]
}
}
}];
Pattern: Different Rules for Tests
Apply different boundary rules to test files:
export default [
// Production code: strict boundaries
{
files: ["packages/*/src/**/*.ts"],
settings: {
"boundaries/elements": [
{ type: "component", pattern: "packages/*/src/components/**/*.ts" },
{ type: "service", pattern: "packages/*/src/services/**/*.ts" }
]
},
rules: {
"boundaries/element-types": ["error", {
default: "disallow",
rules: [
{ from: "component", allow: ["component", "service"] }
]
}]
}
},
// Test files: relaxed boundaries
{
files: ["packages/*/test/**/*.ts", "packages/**/*.spec.ts"],
settings: {
"boundaries/elements": [
{ type: "test", pattern: "**/*.ts" }
]
},
rules: {
"boundaries/element-types": ["error", {
default: "allow" // Tests can import anything
}]
}
}
];
Troubleshooting
Problem: Inter-package Imports Not Categorized Correctly
Check:
boundaries/root-pathis set correctly- Patterns in
customSourcePatternsuse correct glob syntax - Enable debug logging to see how imports are categorized
- Check the
filespatterns is coherent with where ESLint is executed from
Problem: False Positives with node_modules
If packages are symlinked or hoisted by package managers:
{
settings: {
"boundaries/flag-as-external": {
inNodeModules: false // Disable node_modules check
}
}
}
Then use customSourcePatterns for more precise control.