eslint-plugin-import-boundaries 0.1.2 → 0.2.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 +44 -6
- package/eslint-plugin-import-boundaries.js +19 -6
- package/package.json +30 -27
package/README.md
CHANGED
|
@@ -5,17 +5,19 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/eslint-plugin-import-boundaries)
|
|
6
6
|
[](https://opensource.org/licenses/ISC)
|
|
7
7
|
|
|
8
|
-
**Note: This is a beta release developed for a personal project. It is not yet stable and may have breaking changes.**
|
|
8
|
+
**Note: This is a beta 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
|
|
|
12
|
+
**Important:** This rule expects an index or barrel file at every directory level. This barrel file represents the external accessibility of the module as a boundary interface, enabling zero I/O path resolution.
|
|
13
|
+
|
|
12
14
|
## Features
|
|
13
15
|
|
|
14
16
|
- **Deterministic**: One correct path for every import
|
|
15
17
|
- **Explicit Exports**: Ensures every directory is explicit about what it exports (via barrel files)
|
|
16
|
-
- **Readable Paths**:
|
|
17
|
-
- **Architectural Boundaries**: Enforce clean architecture, hexagonal architecture, or any
|
|
18
|
-
- **Auto-fixable**:
|
|
18
|
+
- **Readable Paths**: Always resolves to the most readable filepath (no `../../../../../../` chains)
|
|
19
|
+
- **Architectural Boundaries**: Enforce clean architecture, hexagonal architecture, feature-sliced design, or any other boundary pattern (including nested boundaries)
|
|
20
|
+
- **Auto-fixable**: Legal import paths are auto-fixable and will always converge to the correct import string.
|
|
19
21
|
- **Zero I/O**: Pure path math and AST analysis - fast even on large codebases
|
|
20
22
|
- **Type-aware**: Different rules for type-only imports vs value imports
|
|
21
23
|
- **Circular Dependency Prevention**: Blocks ancestor barrel imports
|
|
@@ -128,6 +130,41 @@ import { Database } from "@infrastructure";
|
|
|
128
130
|
// Error: Cannot import from '@infrastructure' to '@application': Import not allowed
|
|
129
131
|
```
|
|
130
132
|
|
|
133
|
+
#### Nested Boundaries
|
|
134
|
+
|
|
135
|
+
Boundaries can be nested, and each boundary must explicitly declare its import rules:
|
|
136
|
+
|
|
137
|
+
```javascript
|
|
138
|
+
{
|
|
139
|
+
boundaries: [
|
|
140
|
+
{
|
|
141
|
+
dir: 'application',
|
|
142
|
+
alias: '@application',
|
|
143
|
+
allowImportsFrom: ['@domain'],
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
dir: 'application/ports',
|
|
147
|
+
alias: '@ports',
|
|
148
|
+
allowImportsFrom: ['@infrastructure', '@domain'], // Can import from infrastructure even though parent cannot
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
dir: 'interface',
|
|
152
|
+
alias: '@interface',
|
|
153
|
+
allowImportsFrom: ['@application', '@public-use-cases'],
|
|
154
|
+
denyImportsFrom: ['@use-cases'], // Can allow parent and specific child, but deny intermediate boundary
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**Key behaviors:**
|
|
161
|
+
|
|
162
|
+
- Each boundary must explicitly declare its rules (no inheritance)
|
|
163
|
+
- Files in boundaries without rules resolve to their nearest ancestor with rules (or are rejected if no ancestor has rules)
|
|
164
|
+
- Rules work the same regardless of nesting depth (flat rule checking)
|
|
165
|
+
- You can selectively allow/deny specific nested boundaries
|
|
166
|
+
- A boundary is considered to have rules if it defines `allowImportsFrom`, `denyImportsFrom`, or `allowTypeImportsFrom` (even if empty arrays)
|
|
167
|
+
|
|
131
168
|
### 4. Type-Only Imports
|
|
132
169
|
|
|
133
170
|
Different rules for types vs values (types don't create runtime dependencies):
|
|
@@ -178,6 +215,7 @@ import { something } from "@application"; // When inside @application boundary
|
|
|
178
215
|
dir: 'domain', // Required: Relative directory path
|
|
179
216
|
alias: '@domain', // Required when crossBoundaryStyle is 'alias', optional when 'absolute'
|
|
180
217
|
denyImportsFrom: ['@application', '@infrastructure', '@interface', '@composition'], // Domain is pure
|
|
218
|
+
severity: 'error', // Optional: 'error' | 'warn' (overrides defaultSeverity for this boundary)
|
|
181
219
|
},
|
|
182
220
|
{
|
|
183
221
|
dir: 'application',
|
|
@@ -316,9 +354,9 @@ This plugin uses a different approach:
|
|
|
316
354
|
|
|
317
355
|
By assuming barrel files at every directory, this plugin can determine correct paths using pure path math - no file system access needed. This makes it faster and more reliable. The barrel file pattern also enforces clear module interfaces (you must go through the barrel), which is good architecture. Because paths are deterministic, there's no debugging overhead - you always know exactly where a module comes from.
|
|
318
356
|
|
|
319
|
-
##
|
|
357
|
+
## Examples
|
|
320
358
|
|
|
321
|
-
|
|
359
|
+
See [Hexagonal Architecture Defaults](./HEXAGONAL_DEFAULTS.md) for a complete example configuration for hexagonal architecture (ports and adapters) projects.
|
|
322
360
|
|
|
323
361
|
## License
|
|
324
362
|
|
|
@@ -87,6 +87,21 @@ function checkAliasSubpath(spec, boundaries) {
|
|
|
87
87
|
return { isSubpath: false };
|
|
88
88
|
}
|
|
89
89
|
/**
|
|
90
|
+
* Resolve a file to the nearest boundary that has rules specified.
|
|
91
|
+
* If no boundaries with rules are found, returns null.
|
|
92
|
+
*
|
|
93
|
+
* @param filename - Absolute filename
|
|
94
|
+
* @param boundaries - Array of all boundaries
|
|
95
|
+
* @returns The nearest boundary with rules, or null if none found
|
|
96
|
+
*/
|
|
97
|
+
function resolveToSpecifiedBoundary(filename, boundaries) {
|
|
98
|
+
const specifiedBoundaries = boundaries.filter((b) => isInsideDir(b.absDir, filename)).filter((b) => b.allowImportsFrom !== void 0 || b.denyImportsFrom !== void 0 || b.allowTypeImportsFrom !== void 0);
|
|
99
|
+
if (specifiedBoundaries.length > 0) return specifiedBoundaries.sort((a, b) => b.absDir.length - a.absDir.length)[0];
|
|
100
|
+
const ancestors = boundaries.filter((b) => b.allowImportsFrom !== void 0 || b.denyImportsFrom !== void 0 || b.allowTypeImportsFrom !== void 0).filter((b) => isInsideDir(b.absDir, filename));
|
|
101
|
+
if (ancestors.length > 0) return ancestors.sort((a, b) => b.absDir.length - a.absDir.length)[0];
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
90
105
|
* Get metadata about the current file being linted.
|
|
91
106
|
* Results are cached per file to avoid recomputation.
|
|
92
107
|
*
|
|
@@ -96,12 +111,10 @@ function checkAliasSubpath(spec, boundaries) {
|
|
|
96
111
|
*/
|
|
97
112
|
function getFileData(filename, boundaries) {
|
|
98
113
|
if (!path.isAbsolute(filename)) return { isValid: false };
|
|
99
|
-
const fileDir = path.dirname(filename);
|
|
100
|
-
const matchingBoundaries = boundaries.filter((b) => isInsideDir(b.absDir, filename));
|
|
101
114
|
return {
|
|
102
115
|
isValid: true,
|
|
103
|
-
fileDir,
|
|
104
|
-
fileBoundary:
|
|
116
|
+
fileDir: path.dirname(filename),
|
|
117
|
+
fileBoundary: resolveToSpecifiedBoundary(filename, boundaries)
|
|
105
118
|
};
|
|
106
119
|
}
|
|
107
120
|
|
|
@@ -273,7 +286,7 @@ function calculateCorrectImportPath(rawSpec, fileDir, fileBoundary, boundaries,
|
|
|
273
286
|
".cjs"
|
|
274
287
|
]) {
|
|
275
288
|
const { targetAbs, targetDir } = resolveTargetPath(rawSpec, fileDir, boundaries, rootDir, cwd, barrelFileName, fileExtensions);
|
|
276
|
-
const targetBoundary =
|
|
289
|
+
const targetBoundary = resolveToSpecifiedBoundary(targetAbs, boundaries);
|
|
277
290
|
if (!fileBoundary || targetBoundary !== fileBoundary) {
|
|
278
291
|
if (targetBoundary) {
|
|
279
292
|
if (crossBoundaryStyle === "absolute") return path.join(rootDir, targetBoundary.dir).replace(/\\/g, "/");
|
|
@@ -358,7 +371,7 @@ function handleImport(options) {
|
|
|
358
371
|
}
|
|
359
372
|
}
|
|
360
373
|
}
|
|
361
|
-
const targetBoundary =
|
|
374
|
+
const targetBoundary = resolveToSpecifiedBoundary(targetAbs, boundaries);
|
|
362
375
|
if (!skipBoundaryRules && fileBoundary && targetBoundary && fileBoundary !== targetBoundary) {
|
|
363
376
|
const violation = checkBoundaryRules(fileBoundary, targetBoundary, boundaries, isTypeOnly);
|
|
364
377
|
if (violation) {
|
package/package.json
CHANGED
|
@@ -1,22 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-import-boundaries",
|
|
3
|
-
"version": "0.1.2",
|
|
4
|
-
"description": "Enforce architectural boundaries with deterministic import paths",
|
|
5
3
|
"type": "module",
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
4
|
+
"version": "0.2.0",
|
|
5
|
+
"description": "Enforce architectural boundaries with deterministic import paths",
|
|
6
|
+
"author": "ClassicalMoser",
|
|
7
|
+
"license": "ISC",
|
|
8
|
+
"homepage": "https://github.com/ClassicalMoser/eslint-plugin-import-boundaries#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/ClassicalMoser/eslint-plugin-import-boundaries.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/ClassicalMoser/eslint-plugin-import-boundaries/issues"
|
|
13
15
|
},
|
|
14
|
-
"files": [
|
|
15
|
-
"eslint-plugin-import-boundaries.js",
|
|
16
|
-
"eslint-plugin-import-boundaries.d.ts",
|
|
17
|
-
"README.md",
|
|
18
|
-
"LICENSE"
|
|
19
|
-
],
|
|
20
16
|
"keywords": [
|
|
21
17
|
"eslint",
|
|
22
18
|
"eslint-plugin",
|
|
@@ -30,22 +26,23 @@
|
|
|
30
26
|
"deterministic",
|
|
31
27
|
"import-paths"
|
|
32
28
|
],
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
},
|
|
39
|
-
"bugs": {
|
|
40
|
-
"url": "https://github.com/ClassicalMoser/eslint-plugin-import-boundaries/issues"
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./eslint-plugin-import-boundaries.d.ts",
|
|
32
|
+
"import": "./eslint-plugin-import-boundaries.js"
|
|
33
|
+
}
|
|
41
34
|
},
|
|
42
|
-
"
|
|
35
|
+
"main": "./eslint-plugin-import-boundaries.js",
|
|
36
|
+
"types": "./eslint-plugin-import-boundaries.d.ts",
|
|
37
|
+
"files": [
|
|
38
|
+
"LICENSE",
|
|
39
|
+
"README.md",
|
|
40
|
+
"eslint-plugin-import-boundaries.d.ts",
|
|
41
|
+
"eslint-plugin-import-boundaries.js"
|
|
42
|
+
],
|
|
43
43
|
"engines": {
|
|
44
44
|
"node": ">=18.0.0"
|
|
45
45
|
},
|
|
46
|
-
"peerDependencies": {
|
|
47
|
-
"eslint": ">=9.0.0"
|
|
48
|
-
},
|
|
49
46
|
"scripts": {
|
|
50
47
|
"build": "tsdown --config tsdown.config.ts",
|
|
51
48
|
"test": "vitest run",
|
|
@@ -55,8 +52,14 @@
|
|
|
55
52
|
"lint": "eslint .",
|
|
56
53
|
"lint:fix": "eslint . --fix",
|
|
57
54
|
"format": "prettier --write .",
|
|
55
|
+
"format:check": "prettier --check .",
|
|
56
|
+
"validate:fix": "pnpm run lint:fix && pnpm run format && pnpm run typecheck",
|
|
57
|
+
"validate:check": "pnpm run lint && pnpm run format:check && pnpm run typecheck",
|
|
58
58
|
"prepublishOnly": "npm run build && npm run test && npm run typecheck"
|
|
59
59
|
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"eslint": ">=9.0.0"
|
|
62
|
+
},
|
|
60
63
|
"devDependencies": {
|
|
61
64
|
"@antfu/eslint-config": "^6.4.2",
|
|
62
65
|
"@rolldown/binding-darwin-x64": "1.0.0-beta.53",
|