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 CHANGED
@@ -5,17 +5,19 @@
5
5
  [![npm version](https://img.shields.io/npm/v/eslint-plugin-import-boundaries)](https://www.npmjs.com/package/eslint-plugin-import-boundaries)
6
6
  [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](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**: Resolves to logical, readable filepaths (no `../../../../../../` chains)
17
- - **Architectural Boundaries**: Enforce clean architecture, hexagonal architecture, or any non-nested boundary pattern (nested boundaries planned but not yet implemented)
18
- - **Auto-fixable**: Most violations are automatically 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
- ## Roadmap
357
+ ## Examples
320
358
 
321
- - **Nested Boundaries**: Support for nested boundaries where sub-boundaries can have broader allow patterns than their parents (e.g., `@ports` nested in `@application` can import from `@infrastructure` even though `@application` cannot). This is required for proper hexagonal architecture support. See [Nested Boundaries Design](./NESTED_BOUNDARIES_DESIGN.md) for the design document.
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: matchingBoundaries.length > 0 ? matchingBoundaries.sort((a, b) => b.absDir.length - a.absDir.length)[0] : null
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 = boundaries.find((b) => isInsideDir(b.absDir, targetAbs)) ?? null;
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 = boundaries.find((b) => isInsideDir(b.absDir, targetAbs)) ?? null;
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
- "main": "./eslint-plugin-import-boundaries.js",
7
- "types": "./eslint-plugin-import-boundaries.d.ts",
8
- "exports": {
9
- ".": {
10
- "import": "./eslint-plugin-import-boundaries.js",
11
- "types": "./eslint-plugin-import-boundaries.d.ts"
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
- "author": "ClassicalMoser",
34
- "license": "ISC",
35
- "repository": {
36
- "type": "git",
37
- "url": "git+https://github.com/ClassicalMoser/eslint-plugin-import-boundaries.git"
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
- "homepage": "https://github.com/ClassicalMoser/eslint-plugin-import-boundaries#readme",
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",