eslint-plugin-import-boundaries 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +195 -30
- package/eslint-plugin-import-boundaries.js +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/eslint-plugin-import-boundaries)
|
|
6
6
|
[](https://opensource.org/licenses/ISC)
|
|
7
7
|
|
|
8
|
-
**Note: This is
|
|
8
|
+
**Note: This is an alpha release, originally developed for a personal project. It is not yet stable and may have breaking changes.**
|
|
9
9
|
|
|
10
10
|
An opinionated ESLint rule that enforces architectural boundaries using deterministic import path rules. This rule determines when to use alias vs relative imports based on your architecture, rather than enforcing a single pattern for all imports.
|
|
11
11
|
|
|
@@ -20,6 +20,7 @@ An opinionated ESLint rule that enforces architectural boundaries using determin
|
|
|
20
20
|
- **Auto-fixable**: Legal import paths are auto-fixable and will always converge to the correct import string.
|
|
21
21
|
- **Zero I/O**: Pure path math and AST analysis - fast even on large codebases
|
|
22
22
|
- **Type-aware**: Different rules for type-only imports vs value imports
|
|
23
|
+
- **Test-ready**: Flexible configuration for test files (skip boundary rules while maintaining path format)
|
|
23
24
|
- **Circular Dependency Prevention**: Blocks ancestor barrel imports
|
|
24
25
|
- **Configurable**: Works with any architectural pattern
|
|
25
26
|
|
|
@@ -29,6 +30,8 @@ An opinionated ESLint rule that enforces architectural boundaries using determin
|
|
|
29
30
|
npm install --save-dev eslint-plugin-import-boundaries
|
|
30
31
|
```
|
|
31
32
|
|
|
33
|
+
### Using Alias Paths (Default)
|
|
34
|
+
|
|
32
35
|
```javascript
|
|
33
36
|
// eslint.config.js
|
|
34
37
|
import importBoundaries from "eslint-plugin-import-boundaries";
|
|
@@ -42,6 +45,7 @@ export default {
|
|
|
42
45
|
"error",
|
|
43
46
|
{
|
|
44
47
|
rootDir: "src",
|
|
48
|
+
crossBoundaryStyle: "alias", // Default: use alias paths
|
|
45
49
|
boundaries: [
|
|
46
50
|
{ dir: "domain", alias: "@domain" },
|
|
47
51
|
{ dir: "application", alias: "@application" },
|
|
@@ -53,6 +57,21 @@ export default {
|
|
|
53
57
|
};
|
|
54
58
|
```
|
|
55
59
|
|
|
60
|
+
**Import patterns with aliases:**
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
// Cross-boundary imports → use alias
|
|
64
|
+
import { Entity } from "@domain"; // ✅
|
|
65
|
+
import { UseCase } from "@application"; // ✅
|
|
66
|
+
|
|
67
|
+
// Same-boundary, close imports → use relative
|
|
68
|
+
import { helper } from "./helper"; // ✅ Same directory
|
|
69
|
+
import { utils } from "../utils"; // ✅ Parent's sibling
|
|
70
|
+
|
|
71
|
+
// Same-boundary, distant imports → use alias
|
|
72
|
+
import { useCase } from "@application/use-cases"; // ✅ Distant in same boundary
|
|
73
|
+
```
|
|
74
|
+
|
|
56
75
|
## What Problem Does This Solve?
|
|
57
76
|
|
|
58
77
|
Most projects suffer from inconsistent import patterns that create ambiguity and technical debt:
|
|
@@ -104,8 +123,8 @@ import { helper } from "./helper"; // ✅
|
|
|
104
123
|
// Parent's sibling (cousin, max one ../)
|
|
105
124
|
import { utils } from "../utils"; // ✅
|
|
106
125
|
|
|
107
|
-
//
|
|
108
|
-
import { useCase } from "@application/
|
|
126
|
+
// Distant within same boundary → Use alias (with subpath allowed for same-boundary imports)
|
|
127
|
+
import { useCase } from "@application/use-cases"; // ✅ Same boundary, distant location
|
|
109
128
|
```
|
|
110
129
|
|
|
111
130
|
### 3. Architectural Boundary Enforcement
|
|
@@ -132,31 +151,70 @@ import { Database } from "@infrastructure";
|
|
|
132
151
|
|
|
133
152
|
#### Nested Boundaries
|
|
134
153
|
|
|
135
|
-
Boundaries can be nested, and each boundary must explicitly declare its import rules:
|
|
154
|
+
Boundaries can be nested, and each boundary must explicitly declare its import rules. Each nested boundary has its own independent rules (no inheritance from parent boundaries):
|
|
136
155
|
|
|
137
156
|
```javascript
|
|
138
157
|
{
|
|
139
158
|
boundaries: [
|
|
140
159
|
{
|
|
141
|
-
dir: '
|
|
142
|
-
alias: '@
|
|
143
|
-
allowImportsFrom: ['@domain'],
|
|
160
|
+
dir: 'interface',
|
|
161
|
+
alias: '@interface',
|
|
162
|
+
allowImportsFrom: ['@application', '@domain'], // @interface can import from @application and @domain
|
|
163
|
+
// Implicitly denies all other boundaries (including @infrastructure, @composition, etc.)
|
|
144
164
|
},
|
|
145
165
|
{
|
|
146
|
-
dir: '
|
|
147
|
-
alias: '@
|
|
148
|
-
allowImportsFrom: ['@
|
|
166
|
+
dir: 'interface/api',
|
|
167
|
+
alias: '@api',
|
|
168
|
+
allowImportsFrom: ['@domain', '@public-use-cases'],
|
|
169
|
+
// @api (public REST API) only allows public use cases, not all of @application
|
|
170
|
+
// This demonstrates selective access within an allowed parent boundary
|
|
171
|
+
// Note: @public-use-cases and @internal-use-cases would be separate boundaries
|
|
172
|
+
// defined elsewhere in your boundaries array
|
|
173
|
+
denyImportsFrom: ['@internal-use-cases'],
|
|
149
174
|
},
|
|
150
175
|
{
|
|
151
|
-
dir: 'interface',
|
|
152
|
-
alias: '@
|
|
153
|
-
allowImportsFrom: ['@application', '@
|
|
154
|
-
|
|
176
|
+
dir: 'interface/graphql',
|
|
177
|
+
alias: '@graphql',
|
|
178
|
+
allowImportsFrom: ['@application', '@domain'],
|
|
179
|
+
// @graphql can import from all of @application (different rules than @api sibling)
|
|
180
|
+
// This shows how sibling boundaries can have different rules
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
dir: 'composition',
|
|
184
|
+
alias: '@composition',
|
|
185
|
+
allowImportsFrom: ['@domain', '@application', '@infrastructure', '@interface'],
|
|
186
|
+
// @composition can import from all boundaries (wiring layer)
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
dir: 'composition/di',
|
|
190
|
+
alias: '@di',
|
|
191
|
+
allowImportsFrom: ['@domain', '@application', '@infrastructure'],
|
|
192
|
+
// @di (dependency injection setup) doesn't need @interface
|
|
193
|
+
// This shows how nested boundaries can be more restrictive than parent
|
|
155
194
|
},
|
|
156
195
|
],
|
|
157
196
|
}
|
|
158
197
|
```
|
|
159
198
|
|
|
199
|
+
**Example behavior:**
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
// File: interface/api/user-controller.ts
|
|
203
|
+
import { Entity } from "@domain"; // ✅ Allowed: @api can import from @domain
|
|
204
|
+
import { CreateUser } from "@public-use-cases"; // ✅ Allowed: @api can import from @public-use-cases
|
|
205
|
+
import { InternalAudit } from "@internal-use-cases"; // ❌ Violation: @api explicitly denies @internal-use-cases
|
|
206
|
+
|
|
207
|
+
// File: interface/graphql/user-resolver.ts
|
|
208
|
+
import { Entity } from "@domain"; // ✅ Allowed: @graphql can import from @domain
|
|
209
|
+
import { CreateUser } from "@public-use-cases"; // ✅ Allowed: @graphql can import from any @application code
|
|
210
|
+
import { InternalAudit } from "@internal-use-cases"; // ✅ Allowed: @graphql has different rules than @api sibling
|
|
211
|
+
|
|
212
|
+
// File: composition/di/container.ts
|
|
213
|
+
import { Repository } from "@infrastructure"; // ✅ Allowed: @di can import from @infrastructure for wiring
|
|
214
|
+
import { UseCase } from "@application"; // ✅ Allowed: @di can import from @application
|
|
215
|
+
import { Controller } from "@interface"; // ❌ Violation: @di cannot import from @interface (more restrictive than parent)
|
|
216
|
+
```
|
|
217
|
+
|
|
160
218
|
**Key behaviors:**
|
|
161
219
|
|
|
162
220
|
- Each boundary has rules: explicit (via `allowImportsFrom`/`denyImportsFrom`) or implicit "deny all" (if neither is specified)
|
|
@@ -209,27 +267,23 @@ import { something } from "@application"; // When inside @application boundary
|
|
|
209
267
|
|
|
210
268
|
### Basic Configuration
|
|
211
269
|
|
|
270
|
+
Here's a complete configuration example with all boundary rules:
|
|
271
|
+
|
|
212
272
|
```javascript
|
|
213
273
|
{
|
|
214
|
-
rootDir: 'src', // Root directory (default: 'src')
|
|
215
|
-
crossBoundaryStyle: 'alias', // 'alias' | 'absolute' (default: 'alias')
|
|
216
|
-
defaultSeverity: 'error', // 'error' | 'warn' (default: 'error')
|
|
217
|
-
allowUnknownBoundaries: false, // Allow imports outside boundaries (default: false)
|
|
218
|
-
skipBoundaryRulesForTestFiles: true, // Skip boundary rules for tests (default: true)
|
|
219
|
-
barrelFileName: 'index', // Barrel file name without extension (default: 'index')
|
|
220
|
-
fileExtensions: ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'], // Extensions to recognize (default: all common JS/TS extensions)
|
|
274
|
+
rootDir: 'src', // Required: Root directory (default: 'src')
|
|
221
275
|
boundaries: [ // Required: Array of boundary definitions
|
|
222
276
|
{
|
|
223
277
|
dir: 'domain', // Required: Relative directory path
|
|
224
|
-
alias: '@domain', // Required
|
|
225
|
-
denyImportsFrom: ['@application', '@infrastructure', '@interface', '@composition'], // Domain is pure
|
|
278
|
+
alias: '@domain', // Required: Import alias (e.g., '@domain')
|
|
279
|
+
denyImportsFrom: ['@application', '@infrastructure', '@interface', '@composition'], // Domain is pure - denies all other boundaries
|
|
226
280
|
severity: 'error', // Optional: 'error' | 'warn' (overrides defaultSeverity for this boundary)
|
|
227
281
|
},
|
|
228
282
|
{
|
|
229
283
|
dir: 'application',
|
|
230
284
|
alias: '@application',
|
|
231
285
|
allowImportsFrom: ['@domain'], // Application uses domain (deny-all by default)
|
|
232
|
-
// Note: denyImportsFrom is redundant here -
|
|
286
|
+
// Note: denyImportsFrom is redundant here - anything not in allowImportsFrom is already denied
|
|
233
287
|
},
|
|
234
288
|
{
|
|
235
289
|
dir: 'infrastructure',
|
|
@@ -238,34 +292,92 @@ import { something } from "@application"; // When inside @application boundary
|
|
|
238
292
|
allowTypeImportsFrom: ['@application'], // Infrastructure implements application ports (types only)
|
|
239
293
|
},
|
|
240
294
|
],
|
|
295
|
+
// Optional configuration options (all have sensible defaults):
|
|
296
|
+
defaultSeverity: 'error', // 'error' | 'warn' (default: 'error')
|
|
297
|
+
enforceBoundaries: true, // Enforce boundary rules (default: true)
|
|
298
|
+
allowUnknownBoundaries: false, // Allow imports outside boundaries (default: false)
|
|
299
|
+
barrelFileName: 'index', // Barrel file name without extension (default: 'index')
|
|
300
|
+
fileExtensions: ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'], // Extensions to recognize (default: all common JS/TS extensions)
|
|
241
301
|
}
|
|
242
302
|
```
|
|
243
303
|
|
|
304
|
+
**Important:** The `enforceBoundaries` option applies globally to all files when set. To have different behavior for test files vs regular files, you must use ESLint's file matching (see Test Files Configuration below).
|
|
305
|
+
|
|
244
306
|
### Test Files Configuration
|
|
245
307
|
|
|
246
|
-
|
|
308
|
+
**How test exclusion works:** When `enforceBoundaries: false`, the rule skips boundary rule checking (allow/deny rules) but still enforces path format (alias vs relative). This allows test files to import from any boundary while maintaining consistent import path patterns. The default is `true` (boundary rules are enforced by default).
|
|
309
|
+
|
|
310
|
+
**Why skip boundary rules for tests?** Test files often need to:
|
|
311
|
+
|
|
312
|
+
- Import from multiple boundaries to set up test scenarios (e.g., mocking infrastructure while testing application logic)
|
|
313
|
+
- Use test helper libraries or mocks that don't fit clean architectural boundaries
|
|
314
|
+
- Access internal implementation details for thorough testing
|
|
315
|
+
- Create test fixtures that span multiple boundaries
|
|
316
|
+
|
|
317
|
+
By setting `enforceBoundaries: false` for test files, you maintain architectural boundaries in production code while giving tests the flexibility they need. Path format (alias vs relative) is still enforced, keeping import paths consistent and readable.
|
|
318
|
+
|
|
319
|
+
**Alternative approach:** You can also define separate boundaries for test directories (e.g., `test/domain`, `test/application`) with their own import rules, but this has two downsides: it discourages test collocation (tests must live in separate test directories rather than alongside source files), and it requires much more configuration overhead than most projects need. The `enforceBoundaries: false` approach is simpler and sufficient for most use cases.
|
|
320
|
+
|
|
321
|
+
**Configuration pattern:** Use ESLint's file matching to apply different configs to test files vs regular files. Define boundaries once and reuse them in both config blocks:
|
|
247
322
|
|
|
248
323
|
```javascript
|
|
324
|
+
import importBoundaries from "eslint-plugin-import-boundaries";
|
|
325
|
+
|
|
326
|
+
// Define boundaries once - shared between regular files and test files
|
|
327
|
+
const boundaries = [
|
|
328
|
+
{
|
|
329
|
+
dir: "domain",
|
|
330
|
+
alias: "@domain",
|
|
331
|
+
// No imports allowed by default
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
dir: "application",
|
|
335
|
+
alias: "@application",
|
|
336
|
+
allowImportsFrom: ["@domain"],
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
dir: "infrastructure",
|
|
340
|
+
alias: "@infrastructure",
|
|
341
|
+
allowImportsFrom: ["@domain"],
|
|
342
|
+
},
|
|
343
|
+
];
|
|
344
|
+
|
|
249
345
|
export default [
|
|
346
|
+
// Test files - skip boundary rules but keep path format enforcement
|
|
347
|
+
// Put test files first so they take precedence over regular file patterns
|
|
250
348
|
{
|
|
251
|
-
files: [
|
|
349
|
+
files: [
|
|
350
|
+
"**/*.test.{ts,js}",
|
|
351
|
+
"**/*.spec.{ts,js}",
|
|
352
|
+
"**/__tests__/**/*.{ts,js}",
|
|
353
|
+
],
|
|
252
354
|
rules: {
|
|
253
355
|
"import-boundaries/enforce": [
|
|
254
356
|
"error",
|
|
255
357
|
{
|
|
256
|
-
|
|
358
|
+
rootDir: "src",
|
|
359
|
+
enforceBoundaries: false, // Tests can import from any boundary
|
|
360
|
+
boundaries, // Same boundaries - needed for path format calculation
|
|
257
361
|
},
|
|
258
362
|
],
|
|
259
363
|
},
|
|
260
364
|
},
|
|
365
|
+
// Regular source files - enforce boundary rules
|
|
366
|
+
// Excludes test files via ignores to prevent overlap
|
|
261
367
|
{
|
|
262
|
-
files: ["
|
|
368
|
+
files: ["src/**/*.ts", "src/**/*.tsx"],
|
|
369
|
+
ignores: [
|
|
370
|
+
"**/*.test.{ts,js}",
|
|
371
|
+
"**/*.spec.{ts,js}",
|
|
372
|
+
"**/__tests__/**/*.{ts,js}",
|
|
373
|
+
],
|
|
263
374
|
rules: {
|
|
264
375
|
"import-boundaries/enforce": [
|
|
265
376
|
"error",
|
|
266
377
|
{
|
|
267
|
-
|
|
268
|
-
//
|
|
378
|
+
rootDir: "src",
|
|
379
|
+
enforceBoundaries: true, // Enforce boundary rules
|
|
380
|
+
boundaries,
|
|
269
381
|
},
|
|
270
382
|
],
|
|
271
383
|
},
|
|
@@ -273,6 +385,59 @@ export default [
|
|
|
273
385
|
];
|
|
274
386
|
```
|
|
275
387
|
|
|
388
|
+
**What gets checked in test files:**
|
|
389
|
+
|
|
390
|
+
- ✅ Path format (alias vs relative) - still enforced
|
|
391
|
+
- ✅ Barrel file imports - still enforced
|
|
392
|
+
- ✅ Ancestor barrel prevention - still enforced
|
|
393
|
+
- ❌ Boundary allow/deny rules - **skipped** (tests can import from any boundary)
|
|
394
|
+
|
|
395
|
+
**What gets checked in regular files:**
|
|
396
|
+
|
|
397
|
+
- ✅ Path format (alias vs relative)
|
|
398
|
+
- ✅ Barrel file imports
|
|
399
|
+
- ✅ Ancestor barrel prevention
|
|
400
|
+
- ✅ Boundary allow/deny rules - **enforced**
|
|
401
|
+
|
|
402
|
+
### Using Absolute Paths
|
|
403
|
+
|
|
404
|
+
By default, the rule uses alias paths (e.g., `@domain`). If your build configuration doesn't support path aliases, or you prefer absolute paths, you can use `crossBoundaryStyle: 'absolute'`:
|
|
405
|
+
|
|
406
|
+
```javascript
|
|
407
|
+
{
|
|
408
|
+
rootDir: 'src',
|
|
409
|
+
crossBoundaryStyle: 'absolute', // Use absolute paths instead of aliases
|
|
410
|
+
boundaries: [
|
|
411
|
+
{ dir: 'domain' }, // No alias required when using absolute paths
|
|
412
|
+
{ dir: 'application' },
|
|
413
|
+
{ dir: 'infrastructure' },
|
|
414
|
+
],
|
|
415
|
+
}
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
**Import patterns with absolute paths:**
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
// Cross-boundary imports → use absolute path
|
|
422
|
+
import { Entity } from "src/domain"; // ✅
|
|
423
|
+
import { UseCase } from "src/application"; // ✅
|
|
424
|
+
|
|
425
|
+
// Same-boundary, close imports → use relative (same as alias style)
|
|
426
|
+
import { helper } from "./helper"; // ✅ Same directory
|
|
427
|
+
import { utils } from "../utils"; // ✅ Parent's sibling
|
|
428
|
+
|
|
429
|
+
// Same-boundary, distant imports → use absolute path
|
|
430
|
+
import { useCase } from "src/application/use-cases"; // ✅ Distant in same boundary
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
**When to use absolute paths:**
|
|
434
|
+
|
|
435
|
+
- Your build configuration doesn't support path aliases (e.g., some bundlers or older tooling)
|
|
436
|
+
- You prefer explicit paths over aliases for clarity
|
|
437
|
+
- You're working in a codebase that already uses absolute paths
|
|
438
|
+
|
|
439
|
+
**Note:** Alias paths are recommended for readability, especially for boundaries defined at deeper directory levels (e.g., `@entities/user` vs `src/hexagonal/domain/entities/user`). However, this rule does not require them since not all build configurations support path aliases. When using `crossBoundaryStyle: 'absolute'`, the `alias` property in boundary definitions becomes optional, and the rule will use paths like `src/domain` instead of `@domain`.
|
|
440
|
+
|
|
276
441
|
## How It Works
|
|
277
442
|
|
|
278
443
|
The rule uses pure path math - no file I/O, just deterministic algorithms:
|
|
@@ -497,7 +497,7 @@ const rule = {
|
|
|
497
497
|
type: "boolean",
|
|
498
498
|
default: false
|
|
499
499
|
},
|
|
500
|
-
|
|
500
|
+
enforceBoundaries: {
|
|
501
501
|
type: "boolean",
|
|
502
502
|
default: true
|
|
503
503
|
},
|
|
@@ -529,7 +529,7 @@ const rule = {
|
|
|
529
529
|
},
|
|
530
530
|
create(context) {
|
|
531
531
|
if (!context.options || context.options.length === 0) throw new Error("boundary-alias-vs-relative requires boundaries configuration");
|
|
532
|
-
const { rootDir = "src", boundaries, crossBoundaryStyle = "alias", defaultSeverity, allowUnknownBoundaries = false,
|
|
532
|
+
const { rootDir = "src", boundaries, crossBoundaryStyle = "alias", defaultSeverity, allowUnknownBoundaries = false, enforceBoundaries = true, barrelFileName = "index", fileExtensions = [
|
|
533
533
|
".ts",
|
|
534
534
|
".tsx",
|
|
535
535
|
".js",
|
|
@@ -592,7 +592,7 @@ const rule = {
|
|
|
592
592
|
defaultSeverity,
|
|
593
593
|
allowUnknownBoundaries,
|
|
594
594
|
isTypeOnly,
|
|
595
|
-
skipBoundaryRules:
|
|
595
|
+
skipBoundaryRules: !enforceBoundaries,
|
|
596
596
|
barrelFileName,
|
|
597
597
|
fileExtensions
|
|
598
598
|
});
|
package/package.json
CHANGED