eslint-plugin-import-boundaries 0.1.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/LICENSE +15 -0
- package/README.md +299 -0
- package/eslint-plugin-import-boundaries.js +462 -0
- package/package.json +69 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025, ClassicalMoser
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# eslint-plugin-import-boundaries
|
|
2
|
+
|
|
3
|
+
> Enforce architectural boundaries with deterministic import paths.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/eslint-plugin-import-boundaries)
|
|
6
|
+
[](https://opensource.org/licenses/ISC)
|
|
7
|
+
|
|
8
|
+
**Note: This is a beta release developed for a personal project. It is not yet stable and may have breaking changes.**
|
|
9
|
+
|
|
10
|
+
An 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
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- **Deterministic**: One correct path for every import
|
|
15
|
+
- **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 ready)
|
|
18
|
+
- **Auto-fixable**: Most violations are automatically fixable
|
|
19
|
+
- **Zero I/O**: Pure path math and AST analysis - fast even on large codebases
|
|
20
|
+
- **Type-aware**: Different rules for type-only imports vs value imports
|
|
21
|
+
- **Circular Dependency Prevention**: Blocks ancestor barrel imports
|
|
22
|
+
- **Configurable**: Works with any architectural pattern
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install --save-dev eslint-plugin-import-boundaries
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```javascript
|
|
31
|
+
// eslint.config.js
|
|
32
|
+
import importBoundaries from 'eslint-plugin-import-boundaries';
|
|
33
|
+
|
|
34
|
+
export default {
|
|
35
|
+
plugins: {
|
|
36
|
+
'import-boundaries': importBoundaries,
|
|
37
|
+
},
|
|
38
|
+
rules: {
|
|
39
|
+
'import-boundaries/enforce': [
|
|
40
|
+
'error',
|
|
41
|
+
{
|
|
42
|
+
rootDir: 'src',
|
|
43
|
+
boundaries: [
|
|
44
|
+
{ dir: 'domain/entities', alias: '@entities' },
|
|
45
|
+
{ dir: 'domain/queries', alias: '@queries' },
|
|
46
|
+
{ dir: 'domain/events', alias: '@events' },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## What Problem Does This Solve?
|
|
55
|
+
|
|
56
|
+
Most projects have inconsistent import patterns:
|
|
57
|
+
|
|
58
|
+
- Sometimes `@entities`, sometimes `../entities`, sometimes `../../domain/entities`
|
|
59
|
+
- No clear rules for when to use alias vs relative
|
|
60
|
+
- Import formatting discussions waste time in code reviews
|
|
61
|
+
- Long relative paths like `../../../../../../utils` are hard to read
|
|
62
|
+
- Absolute paths might not fit your architecture (`src/domain/entities/army/unit/weapon/sword`)
|
|
63
|
+
- Directories aren't explicit about their exports (no barrel files)
|
|
64
|
+
- Architectural boundaries are violated without enforcement
|
|
65
|
+
- Circular dependencies sneak in
|
|
66
|
+
- Refactoring is risky because import paths are ambiguous
|
|
67
|
+
|
|
68
|
+
This rule provides deterministic import paths with architectural boundary enforcement - one correct answer for every import, eliminating debates and making refactoring safer.
|
|
69
|
+
|
|
70
|
+
## Core Rules
|
|
71
|
+
|
|
72
|
+
### 1. Cross-Boundary Imports → Alias
|
|
73
|
+
|
|
74
|
+
When importing from a different boundary, always use the boundary alias (no subpaths):
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
// ✅ CORRECT
|
|
78
|
+
import { Entity } from '@entities';
|
|
79
|
+
import { Query } from '@queries';
|
|
80
|
+
|
|
81
|
+
// ❌ WRONG
|
|
82
|
+
import { Entity } from '@entities/army'; // Subpath not allowed
|
|
83
|
+
import { Entity } from '../entities'; // Relative not allowed
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 2. Same-Boundary Imports → Relative (when close)
|
|
87
|
+
|
|
88
|
+
When importing within the same boundary, use relative paths for close imports:
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
// Same directory (sibling)
|
|
92
|
+
import { helper } from './helper'; // ✅
|
|
93
|
+
|
|
94
|
+
// Parent's sibling (cousin, max one ../)
|
|
95
|
+
import { utils } from '../utils'; // ✅
|
|
96
|
+
|
|
97
|
+
// Top-level or distant → Use alias
|
|
98
|
+
import { something } from '@queries/topLevel'; // ✅
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 3. Architectural Boundary Enforcement
|
|
102
|
+
|
|
103
|
+
Prevent violations of your architecture:
|
|
104
|
+
|
|
105
|
+
```javascript
|
|
106
|
+
{
|
|
107
|
+
dir: 'domain/entities',
|
|
108
|
+
alias: '@entities',
|
|
109
|
+
allowImportsFrom: ['@events'], // Only allow imports from @events
|
|
110
|
+
denyImportsFrom: ['@queries'], // Deny imports from @queries
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// ✅ ALLOWED: @entities can import from @events
|
|
116
|
+
import { Event } from '@events';
|
|
117
|
+
|
|
118
|
+
// ❌ VIOLATION: @entities cannot import from @queries
|
|
119
|
+
import { Query } from '@queries';
|
|
120
|
+
// Error: Cannot import from '@queries' to '@entities': Import not allowed
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 4. Type-Only Imports
|
|
124
|
+
|
|
125
|
+
Different rules for types vs values (types don't create runtime dependencies):
|
|
126
|
+
|
|
127
|
+
```javascript
|
|
128
|
+
{
|
|
129
|
+
dir: 'domain/entities',
|
|
130
|
+
alias: '@entities',
|
|
131
|
+
allowImportsFrom: ['@events'], // Value imports
|
|
132
|
+
allowTypeImportsFrom: ['@events', '@queries'], // Type imports (more permissive)
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
// ✅ ALLOWED: Type import from @queries
|
|
138
|
+
import type { QueryResult } from '@queries';
|
|
139
|
+
|
|
140
|
+
// ❌ VIOLATION: Value import from @queries
|
|
141
|
+
import { executeQuery } from '@queries';
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### 5. Ancestor Barrel Prevention
|
|
145
|
+
|
|
146
|
+
Prevents circular dependencies by blocking ancestor barrel imports:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
// ❌ FORBIDDEN: Would create circular dependency
|
|
150
|
+
import { something } from '@queries'; // When inside @queries boundary
|
|
151
|
+
// Error: Cannot import from ancestor barrel '@queries'.
|
|
152
|
+
// This would create a circular dependency.
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Configuration
|
|
156
|
+
|
|
157
|
+
### Basic Configuration
|
|
158
|
+
|
|
159
|
+
```javascript
|
|
160
|
+
{
|
|
161
|
+
rootDir: 'src', // Root directory (default: 'src')
|
|
162
|
+
crossBoundaryStyle: 'alias', // 'alias' | 'absolute' (default: 'alias')
|
|
163
|
+
defaultSeverity: 'error', // 'error' | 'warn' (default: 'error')
|
|
164
|
+
allowUnknownBoundaries: false, // Allow imports outside boundaries (default: false)
|
|
165
|
+
skipBoundaryRulesForTestFiles: true, // Skip boundary rules for tests (default: true)
|
|
166
|
+
boundaries: [ // Required: Array of boundary definitions
|
|
167
|
+
{
|
|
168
|
+
dir: 'domain/entities', // Required: Relative directory path
|
|
169
|
+
alias: '@entities', // Required: Import alias
|
|
170
|
+
severity: 'error', // Optional: Override default severity
|
|
171
|
+
allowImportsFrom: ['@events'], // Optional: Allowed boundaries (value imports)
|
|
172
|
+
denyImportsFrom: ['@queries'], // Optional: Denied boundaries
|
|
173
|
+
allowTypeImportsFrom: ['@events', '@queries'], // Optional: Allowed for types
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Test Files Configuration
|
|
180
|
+
|
|
181
|
+
Use ESLint's file matching for test files:
|
|
182
|
+
|
|
183
|
+
```javascript
|
|
184
|
+
export default [
|
|
185
|
+
{
|
|
186
|
+
files: ['src/**/*.ts'],
|
|
187
|
+
rules: {
|
|
188
|
+
'import-boundaries/enforce': [
|
|
189
|
+
'error',
|
|
190
|
+
{
|
|
191
|
+
/* config */
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
files: ['**/*.test.{ts,js}', '**/*.spec.{ts,js}'],
|
|
198
|
+
rules: {
|
|
199
|
+
'import-boundaries/enforce': [
|
|
200
|
+
'error',
|
|
201
|
+
{
|
|
202
|
+
skipBoundaryRulesForTestFiles: true, // Tests can import from any boundary
|
|
203
|
+
// ... rest of config
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
];
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## How It Works
|
|
212
|
+
|
|
213
|
+
The rule uses pure path math - no file I/O, just deterministic algorithms:
|
|
214
|
+
|
|
215
|
+
1. **Boundary Detection**: Determines which boundary a file belongs to
|
|
216
|
+
2. **Path Calculation**: Calculates the correct import path using the "first differing segment" algorithm
|
|
217
|
+
3. **Boundary Rules**: Checks allow/deny rules for cross-boundary imports
|
|
218
|
+
4. **Type Detection**: Distinguishes type-only imports from value imports
|
|
219
|
+
|
|
220
|
+
### Barrel Files as Module Interface
|
|
221
|
+
|
|
222
|
+
The rule assumes barrel files (`index.ts`) are the module interface for each directory. This means:
|
|
223
|
+
|
|
224
|
+
- `./dir` imports from `dir/index.ts` (the barrel)
|
|
225
|
+
- You cannot bypass the barrel: `./dir/file` is not allowed
|
|
226
|
+
- This enforces a clear public API for each module
|
|
227
|
+
|
|
228
|
+
This barrel file assumption enables zero I/O: because we know every directory has a barrel file, we can determine correct paths using pure path math - no file system access needed. This makes the rule fast, reliable, and deterministic.
|
|
229
|
+
|
|
230
|
+
The rule is barrel-agnostic - it enforces the path pattern (must go through the barrel), not what the barrel exports. Whether you use selective exports (`export { A, B } from './module'`) or universal exports (`export * from './module'`) is your choice based on your codebase needs.
|
|
231
|
+
|
|
232
|
+
**Scale considerations**:
|
|
233
|
+
|
|
234
|
+
- **Small projects**: If you have boundaries worth enforcing, you have enough structure for barrel files. For truly tiny projects (single file), this rule may be overkill.
|
|
235
|
+
- **Large projects**: The pattern works at any scale. Performance depends on what you export (use selective exports in large apps), not the pattern itself. The rule's zero I/O approach means it stays fast even in massive codebases.
|
|
236
|
+
- **Monorepos**: Works across packages, but requires manual configuration. You'd configure boundaries that span packages (e.g., `{ dir: 'packages/pkg-a/src/domain', alias: '@pkg-a/domain' }`). Each package maintains its own barrel structure, and the rule enforces boundaries between them based on your configuration.
|
|
237
|
+
|
|
238
|
+
## What Gets Skipped
|
|
239
|
+
|
|
240
|
+
The rule automatically skips checking for:
|
|
241
|
+
|
|
242
|
+
- External packages (`node_modules`, npm packages)
|
|
243
|
+
- Imports that don't match any boundary alias and aren't relative paths
|
|
244
|
+
|
|
245
|
+
Only internal imports (relative paths, boundary aliases, and absolute paths within `rootDir`) are checked.
|
|
246
|
+
|
|
247
|
+
## Error Messages
|
|
248
|
+
|
|
249
|
+
Clear, actionable error messages:
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
Expected '@entities' but got '@entities/army'
|
|
253
|
+
Expected './sibling' but got '@queries/sibling'
|
|
254
|
+
Expected '../cousin' but got '@queries/nested/cousin'
|
|
255
|
+
Cannot import from '@queries' to '@entities': Import not allowed
|
|
256
|
+
Cannot import from ancestor barrel '@queries'. This would create a circular dependency.
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Comparison with Other Plugins
|
|
260
|
+
|
|
261
|
+
### Simple Path Enforcers
|
|
262
|
+
|
|
263
|
+
Plugins like `eslint-plugin-no-relative-import-paths` and `eslint-plugin-absolute-imports` only enforce "use absolute paths everywhere" or "use relative paths everywhere." They don't handle:
|
|
264
|
+
|
|
265
|
+
- Deterministic alias vs relative (when to use which)
|
|
266
|
+
- Architectural boundaries
|
|
267
|
+
- Allow/deny rules between boundaries
|
|
268
|
+
- Type-only import handling
|
|
269
|
+
- Circular dependency prevention
|
|
270
|
+
- Barrel file enforcement
|
|
271
|
+
|
|
272
|
+
### Architectural Boundary Plugins
|
|
273
|
+
|
|
274
|
+
`eslint-plugin-boundaries` does enforce architectural boundaries, but uses a different approach:
|
|
275
|
+
|
|
276
|
+
- Pattern-based element matching (more complex configuration)
|
|
277
|
+
- File I/O for resolution (slower, requires file system access)
|
|
278
|
+
- Different rule structure
|
|
279
|
+
|
|
280
|
+
This plugin uses a different approach:
|
|
281
|
+
|
|
282
|
+
- Simple, deterministic path rules (pure path math, zero I/O)
|
|
283
|
+
- Architectural boundary enforcement
|
|
284
|
+
- Type-aware rules
|
|
285
|
+
- Fast and auto-fixable
|
|
286
|
+
|
|
287
|
+
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.
|
|
288
|
+
|
|
289
|
+
## Roadmap
|
|
290
|
+
|
|
291
|
+
- **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.
|
|
292
|
+
|
|
293
|
+
## License
|
|
294
|
+
|
|
295
|
+
ISC
|
|
296
|
+
|
|
297
|
+
## Contributing
|
|
298
|
+
|
|
299
|
+
Contributions welcome! Please open an issue or PR.
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
|
|
4
|
+
//#region eslint-plugin-import-boundaries/pathUtils.ts
|
|
5
|
+
/**
|
|
6
|
+
* Path utility functions for the boundary-alias-vs-relative ESLint rule.
|
|
7
|
+
* Pure path math - no file I/O.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Check if a path is inside a directory.
|
|
11
|
+
* Uses path.relative() which is more reliable than string comparison.
|
|
12
|
+
*
|
|
13
|
+
* @param absDir - Absolute directory path
|
|
14
|
+
* @param absPath - Absolute file or directory path to check
|
|
15
|
+
* @returns true if absPath is inside absDir (or is absDir itself)
|
|
16
|
+
*
|
|
17
|
+
* Examples:
|
|
18
|
+
* - isInsideDir('/a/b', '/a/b/file.ts') => true
|
|
19
|
+
* - isInsideDir('/a/b', '/a/b/c/file.ts') => true
|
|
20
|
+
* - isInsideDir('/a/b', '/a/file.ts') => false (../file.ts)
|
|
21
|
+
* - isInsideDir('/a/b', '/a/b') => true (empty relative path)
|
|
22
|
+
*/
|
|
23
|
+
function isInsideDir(absDir, absPath) {
|
|
24
|
+
const rel = path.relative(absDir, absPath);
|
|
25
|
+
if (rel === "") return true;
|
|
26
|
+
return !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region eslint-plugin-import-boundaries/boundaryDetection.ts
|
|
31
|
+
/**
|
|
32
|
+
* Check if an import specifier is using an alias subpath (e.g., '@entities/army').
|
|
33
|
+
* Subpaths should be converted to the base alias (e.g., '@entities').
|
|
34
|
+
*
|
|
35
|
+
* @param spec - Import specifier to check
|
|
36
|
+
* @param boundaries - Array of resolved boundaries
|
|
37
|
+
* @returns Object indicating if it's a subpath and which base alias it uses
|
|
38
|
+
*
|
|
39
|
+
* Examples:
|
|
40
|
+
* - checkAliasSubpath('@entities/army', boundaries) => { isSubpath: true, baseAlias: '@entities' }
|
|
41
|
+
* - checkAliasSubpath('@entities', boundaries) => { isSubpath: false }
|
|
42
|
+
*/
|
|
43
|
+
function checkAliasSubpath(spec, boundaries) {
|
|
44
|
+
for (const b of boundaries) if (spec.startsWith(`${b.alias}/`)) return {
|
|
45
|
+
isSubpath: true,
|
|
46
|
+
baseAlias: b.alias
|
|
47
|
+
};
|
|
48
|
+
return { isSubpath: false };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Get metadata about the current file being linted.
|
|
52
|
+
* Results are cached per file to avoid recomputation.
|
|
53
|
+
*
|
|
54
|
+
* @param filename - Absolute filename from ESLint context
|
|
55
|
+
* @param boundaries - Array of resolved boundaries
|
|
56
|
+
* @returns FileData with directory and boundary information, or { isValid: false } if file path is invalid
|
|
57
|
+
*/
|
|
58
|
+
function getFileData(filename, boundaries) {
|
|
59
|
+
if (!path.isAbsolute(filename)) return { isValid: false };
|
|
60
|
+
const fileDir = path.dirname(filename);
|
|
61
|
+
const matchingBoundaries = boundaries.filter((b) => isInsideDir(b.absDir, filename));
|
|
62
|
+
return {
|
|
63
|
+
isValid: true,
|
|
64
|
+
fileDir,
|
|
65
|
+
fileBoundary: matchingBoundaries.length > 0 ? matchingBoundaries.sort((a, b) => b.absDir.length - a.absDir.length)[0] : null
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region eslint-plugin-import-boundaries/boundaryRules.ts
|
|
71
|
+
/**
|
|
72
|
+
* Check if an import from fileBoundary to targetBoundary is allowed.
|
|
73
|
+
* Returns violation info if not allowed, null if allowed.
|
|
74
|
+
*
|
|
75
|
+
* Semantics:
|
|
76
|
+
* - If both allowImportsFrom and denyImportsFrom are specified, they work as:
|
|
77
|
+
* - allowImportsFrom: explicit allow list (overrides deny for those items)
|
|
78
|
+
* - denyImportsFrom: explicit deny list (overrides default allow for those items)
|
|
79
|
+
* - If only allowImportsFrom: only those boundaries are allowed (deny-all by default)
|
|
80
|
+
* - If only denyImportsFrom: all boundaries allowed except those (allow-all by default)
|
|
81
|
+
* - If neither: deny-all by default (strictest)
|
|
82
|
+
* - allowTypeImportsFrom: For type-only imports, this overrides allowImportsFrom (allows types from more boundaries)
|
|
83
|
+
*/
|
|
84
|
+
function checkBoundaryRules(fileBoundary, targetBoundary, allBoundaries, isTypeOnly = false) {
|
|
85
|
+
if (fileBoundary === targetBoundary) return null;
|
|
86
|
+
if (isTypeOnly && fileBoundary.allowTypeImportsFrom?.includes(targetBoundary.alias)) return null;
|
|
87
|
+
const hasAllowList = fileBoundary.allowImportsFrom && fileBoundary.allowImportsFrom.length > 0;
|
|
88
|
+
const hasDenyList = fileBoundary.denyImportsFrom && fileBoundary.denyImportsFrom.length > 0;
|
|
89
|
+
if (hasAllowList && fileBoundary.allowImportsFrom.includes(targetBoundary.alias)) return null;
|
|
90
|
+
if (hasDenyList && fileBoundary.denyImportsFrom.includes(targetBoundary.alias)) return { reason: `Boundary '${fileBoundary.alias}' explicitly denies imports from '${targetBoundary.alias}'` };
|
|
91
|
+
if (hasAllowList && !hasDenyList) return { reason: `Cross-boundary import from '${targetBoundary.alias}' to '${fileBoundary.alias}' is not allowed. Add '${targetBoundary.alias}' to 'allowImportsFrom' if this import is intentional.` };
|
|
92
|
+
if (hasDenyList && !hasAllowList) return null;
|
|
93
|
+
return { reason: `Cross-boundary import from '${targetBoundary.alias}' to '${fileBoundary.alias}' is not allowed. Add '${targetBoundary.alias}' to 'allowImportsFrom' if this import is intentional.` };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region eslint-plugin-import-boundaries/fixer.ts
|
|
98
|
+
/**
|
|
99
|
+
* Create a fixer function to replace an import path.
|
|
100
|
+
* Handles different import node types: ImportDeclaration, ImportExpression, require().
|
|
101
|
+
*
|
|
102
|
+
* @param node - AST node for the import
|
|
103
|
+
* @param newPath - New import path to use
|
|
104
|
+
* @returns Fixer function, or null if node type is unsupported
|
|
105
|
+
*/
|
|
106
|
+
function createFixer(node, newPath) {
|
|
107
|
+
return (fixer) => {
|
|
108
|
+
if ("source" in node && node.source) return fixer.replaceText(node.source, `'${newPath}'`);
|
|
109
|
+
if ("arguments" in node && Array.isArray(node.arguments) && node.arguments[0]) return fixer.replaceText(node.arguments[0], `'${newPath}'`);
|
|
110
|
+
return null;
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region eslint-plugin-import-boundaries/relationshipDetection.ts
|
|
116
|
+
/**
|
|
117
|
+
* Resolve the target path from an import specifier.
|
|
118
|
+
*/
|
|
119
|
+
function resolveTargetPath(rawSpec, fileDir, boundaries, rootDir, cwd) {
|
|
120
|
+
let targetAbs;
|
|
121
|
+
let targetDir;
|
|
122
|
+
if (rawSpec.startsWith("@")) {
|
|
123
|
+
const boundary = boundaries.find((b) => rawSpec === b.alias || rawSpec.startsWith(`${b.alias}/`));
|
|
124
|
+
if (boundary) {
|
|
125
|
+
const subpath = rawSpec.slice(boundary.alias.length + 1);
|
|
126
|
+
if (subpath && !subpath.endsWith(".ts")) {
|
|
127
|
+
targetDir = path.resolve(boundary.absDir, subpath);
|
|
128
|
+
targetAbs = path.join(targetDir, "index.ts");
|
|
129
|
+
} else if (subpath) {
|
|
130
|
+
targetAbs = path.resolve(boundary.absDir, subpath);
|
|
131
|
+
targetDir = path.dirname(targetAbs);
|
|
132
|
+
} else {
|
|
133
|
+
targetAbs = path.join(boundary.absDir, "index.ts");
|
|
134
|
+
targetDir = boundary.absDir;
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
targetAbs = "";
|
|
138
|
+
targetDir = "";
|
|
139
|
+
}
|
|
140
|
+
} else if (rawSpec.startsWith(".")) if (!rawSpec.endsWith(".ts")) {
|
|
141
|
+
targetDir = path.resolve(fileDir, rawSpec);
|
|
142
|
+
targetAbs = path.join(targetDir, "index.ts");
|
|
143
|
+
} else {
|
|
144
|
+
targetAbs = path.resolve(fileDir, rawSpec);
|
|
145
|
+
targetDir = path.dirname(targetAbs);
|
|
146
|
+
}
|
|
147
|
+
else if (rawSpec.startsWith(rootDir)) if (!rawSpec.endsWith(".ts")) {
|
|
148
|
+
targetDir = path.resolve(cwd, rawSpec);
|
|
149
|
+
targetAbs = path.join(targetDir, "index.ts");
|
|
150
|
+
} else {
|
|
151
|
+
targetAbs = path.resolve(cwd, rawSpec);
|
|
152
|
+
targetDir = path.dirname(targetAbs);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
targetAbs = "";
|
|
156
|
+
targetDir = "";
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
targetAbs,
|
|
160
|
+
targetDir
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Calculate the correct import path using the simplified algorithm.
|
|
165
|
+
*/
|
|
166
|
+
function calculateCorrectImportPath(rawSpec, fileDir, fileBoundary, boundaries, rootDir, cwd, crossBoundaryStyle = "alias") {
|
|
167
|
+
const { targetAbs, targetDir } = resolveTargetPath(rawSpec, fileDir, boundaries, rootDir, cwd);
|
|
168
|
+
const targetBoundary = boundaries.find((b) => isInsideDir(b.absDir, targetAbs)) ?? null;
|
|
169
|
+
if (!fileBoundary || targetBoundary !== fileBoundary) {
|
|
170
|
+
if (targetBoundary) {
|
|
171
|
+
if (crossBoundaryStyle === "absolute") return path.join(rootDir, targetBoundary.dir).replace(/\\/g, "/");
|
|
172
|
+
return targetBoundary.alias;
|
|
173
|
+
}
|
|
174
|
+
return "UNKNOWN_BOUNDARY";
|
|
175
|
+
}
|
|
176
|
+
if (rawSpec === fileBoundary.alias) return null;
|
|
177
|
+
const targetRelativeToBoundary = path.relative(fileBoundary.absDir, targetDir);
|
|
178
|
+
const fileRelativeToBoundary = path.relative(fileBoundary.absDir, fileDir);
|
|
179
|
+
const targetParts = targetRelativeToBoundary === "" || targetRelativeToBoundary === "." ? [] : targetRelativeToBoundary.split(path.sep).filter((p) => p && p !== ".");
|
|
180
|
+
const fileParts = fileRelativeToBoundary === "" || fileRelativeToBoundary === "." ? [] : fileRelativeToBoundary.split(path.sep).filter((p) => p && p !== ".");
|
|
181
|
+
if (targetParts.length === 0) {
|
|
182
|
+
const targetBasename = path.basename(targetAbs, ".ts");
|
|
183
|
+
if (targetBasename !== "index") return `${fileBoundary.alias}/${targetBasename}`;
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
let firstDifferingIndex = 0;
|
|
187
|
+
while (firstDifferingIndex < targetParts.length && firstDifferingIndex < fileParts.length && targetParts[firstDifferingIndex] === fileParts[firstDifferingIndex]) firstDifferingIndex++;
|
|
188
|
+
if (firstDifferingIndex >= targetParts.length && firstDifferingIndex >= fileParts.length) {
|
|
189
|
+
const targetBasename = path.basename(targetAbs, ".ts");
|
|
190
|
+
if (targetBasename !== "index") return `./${targetBasename}`;
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
const firstDifferingSegment = targetParts[firstDifferingIndex];
|
|
194
|
+
if (!firstDifferingSegment) return null;
|
|
195
|
+
if (firstDifferingIndex === fileParts.length) return `./${firstDifferingSegment}`;
|
|
196
|
+
if (firstDifferingIndex === fileParts.length - 1) {
|
|
197
|
+
if (!(firstDifferingIndex === 0)) return `../${firstDifferingSegment}`;
|
|
198
|
+
}
|
|
199
|
+
return `${fileBoundary.alias}/${firstDifferingSegment}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
//#endregion
|
|
203
|
+
//#region eslint-plugin-import-boundaries/importHandler.ts
|
|
204
|
+
/**
|
|
205
|
+
* Main handler for all import statements.
|
|
206
|
+
* Validates import paths against boundary rules and enforces correct path format.
|
|
207
|
+
*
|
|
208
|
+
* @returns true if a violation was reported, false otherwise
|
|
209
|
+
*/
|
|
210
|
+
function handleImport(options) {
|
|
211
|
+
const { node, rawSpec, fileDir, fileBoundary, boundaries, rootDir, cwd, context, crossBoundaryStyle = "alias", defaultSeverity, allowUnknownBoundaries = false, isTypeOnly = false, skipBoundaryRules = false } = options;
|
|
212
|
+
const isRelative = rawSpec.startsWith(".");
|
|
213
|
+
const matchesBoundaryAlias = boundaries.some((b) => rawSpec === b.alias || rawSpec.startsWith(`${b.alias}/`));
|
|
214
|
+
const isAbsoluteInRoot = rawSpec.startsWith(rootDir) || rawSpec.startsWith(`/${rootDir}`);
|
|
215
|
+
if (!isRelative && !matchesBoundaryAlias && !isAbsoluteInRoot) return false;
|
|
216
|
+
if (crossBoundaryStyle === "alias") {
|
|
217
|
+
const aliasSubpathCheck = checkAliasSubpath(rawSpec, boundaries);
|
|
218
|
+
if (aliasSubpathCheck.isSubpath) {
|
|
219
|
+
const targetBoundary$1 = boundaries.find((b) => b.alias === aliasSubpathCheck.baseAlias);
|
|
220
|
+
if (targetBoundary$1 && fileBoundary && targetBoundary$1 !== fileBoundary) {
|
|
221
|
+
const expectedPath = targetBoundary$1.alias;
|
|
222
|
+
const severity$1 = fileBoundary.severity || defaultSeverity;
|
|
223
|
+
const reportOptions$1 = {
|
|
224
|
+
node,
|
|
225
|
+
messageId: "incorrectImportPath",
|
|
226
|
+
data: {
|
|
227
|
+
expectedPath,
|
|
228
|
+
actualPath: rawSpec
|
|
229
|
+
},
|
|
230
|
+
fix: createFixer(node, expectedPath),
|
|
231
|
+
...severity$1 && { severity: severity$1 === "warn" ? 1 : 2 }
|
|
232
|
+
};
|
|
233
|
+
context.report(reportOptions$1);
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const { targetAbs } = resolveTargetPath(rawSpec, fileDir, boundaries, rootDir, cwd);
|
|
239
|
+
const targetBoundary = boundaries.find((b) => isInsideDir(b.absDir, targetAbs)) ?? null;
|
|
240
|
+
if (!skipBoundaryRules && fileBoundary && targetBoundary && fileBoundary !== targetBoundary) {
|
|
241
|
+
const violation = checkBoundaryRules(fileBoundary, targetBoundary, boundaries, isTypeOnly);
|
|
242
|
+
if (violation) {
|
|
243
|
+
const severity$1 = fileBoundary.severity || defaultSeverity;
|
|
244
|
+
const reportOptions$1 = {
|
|
245
|
+
node,
|
|
246
|
+
messageId: "boundaryViolation",
|
|
247
|
+
data: {
|
|
248
|
+
from: fileBoundary.alias,
|
|
249
|
+
to: targetBoundary.alias,
|
|
250
|
+
reason: violation.reason
|
|
251
|
+
},
|
|
252
|
+
...severity$1 && { severity: severity$1 === "warn" ? 1 : 2 }
|
|
253
|
+
};
|
|
254
|
+
context.report(reportOptions$1);
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const correctPath = calculateCorrectImportPath(rawSpec, fileDir, fileBoundary, boundaries, rootDir, cwd, crossBoundaryStyle);
|
|
259
|
+
if (!correctPath) {
|
|
260
|
+
if (fileBoundary && rawSpec === fileBoundary.alias) {
|
|
261
|
+
const severity$1 = fileBoundary.severity || defaultSeverity;
|
|
262
|
+
const reportOptions$1 = {
|
|
263
|
+
node,
|
|
264
|
+
messageId: "ancestorBarrelImport",
|
|
265
|
+
data: { alias: fileBoundary.alias },
|
|
266
|
+
...severity$1 && { severity: severity$1 === "warn" ? 1 : 2 }
|
|
267
|
+
};
|
|
268
|
+
context.report(reportOptions$1);
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
if (correctPath === "UNKNOWN_BOUNDARY") {
|
|
274
|
+
if (!allowUnknownBoundaries) {
|
|
275
|
+
const reportOptions$1 = {
|
|
276
|
+
node,
|
|
277
|
+
messageId: "unknownBoundaryImport",
|
|
278
|
+
data: { path: rawSpec },
|
|
279
|
+
...defaultSeverity && { severity: defaultSeverity === "warn" ? 1 : 2 }
|
|
280
|
+
};
|
|
281
|
+
context.report(reportOptions$1);
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
if (rawSpec === correctPath) return false;
|
|
287
|
+
const severity = fileBoundary?.severity || defaultSeverity;
|
|
288
|
+
const reportOptions = {
|
|
289
|
+
node,
|
|
290
|
+
messageId: "incorrectImportPath",
|
|
291
|
+
data: {
|
|
292
|
+
expectedPath: correctPath,
|
|
293
|
+
actualPath: rawSpec
|
|
294
|
+
},
|
|
295
|
+
fix: createFixer(node, correctPath),
|
|
296
|
+
...severity && { severity: severity === "warn" ? 1 : 2 }
|
|
297
|
+
};
|
|
298
|
+
context.report(reportOptions);
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
//#endregion
|
|
303
|
+
//#region eslint-plugin-import-boundaries/index.ts
|
|
304
|
+
const rule = {
|
|
305
|
+
meta: {
|
|
306
|
+
type: "problem",
|
|
307
|
+
fixable: "code",
|
|
308
|
+
docs: {
|
|
309
|
+
description: "Enforces architectural boundaries with deterministic import path rules: cross-boundary uses alias without subpath, siblings use relative, boundary-root and top-level paths use alias, cousins use relative (max one ../).",
|
|
310
|
+
recommended: false
|
|
311
|
+
},
|
|
312
|
+
schema: [{
|
|
313
|
+
type: "object",
|
|
314
|
+
properties: {
|
|
315
|
+
rootDir: { type: "string" },
|
|
316
|
+
boundaries: {
|
|
317
|
+
type: "array",
|
|
318
|
+
items: {
|
|
319
|
+
type: "object",
|
|
320
|
+
properties: {
|
|
321
|
+
dir: { type: "string" },
|
|
322
|
+
alias: { type: "string" },
|
|
323
|
+
allowImportsFrom: {
|
|
324
|
+
type: "array",
|
|
325
|
+
items: { type: "string" }
|
|
326
|
+
},
|
|
327
|
+
denyImportsFrom: {
|
|
328
|
+
type: "array",
|
|
329
|
+
items: { type: "string" }
|
|
330
|
+
},
|
|
331
|
+
allowTypeImportsFrom: {
|
|
332
|
+
type: "array",
|
|
333
|
+
items: { type: "string" }
|
|
334
|
+
},
|
|
335
|
+
nestedPathFormat: {
|
|
336
|
+
type: "string",
|
|
337
|
+
enum: [
|
|
338
|
+
"alias",
|
|
339
|
+
"relative",
|
|
340
|
+
"inherit"
|
|
341
|
+
]
|
|
342
|
+
},
|
|
343
|
+
severity: {
|
|
344
|
+
type: "string",
|
|
345
|
+
enum: ["error", "warn"]
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
required: ["dir", "alias"]
|
|
349
|
+
},
|
|
350
|
+
minItems: 1
|
|
351
|
+
},
|
|
352
|
+
crossBoundaryStyle: {
|
|
353
|
+
type: "string",
|
|
354
|
+
enum: ["alias", "absolute"],
|
|
355
|
+
default: "alias"
|
|
356
|
+
},
|
|
357
|
+
defaultSeverity: {
|
|
358
|
+
type: "string",
|
|
359
|
+
enum: ["error", "warn"]
|
|
360
|
+
},
|
|
361
|
+
allowUnknownBoundaries: {
|
|
362
|
+
type: "boolean",
|
|
363
|
+
default: false
|
|
364
|
+
},
|
|
365
|
+
skipBoundaryRulesForTestFiles: {
|
|
366
|
+
type: "boolean",
|
|
367
|
+
default: true
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
required: ["boundaries"]
|
|
371
|
+
}],
|
|
372
|
+
messages: {
|
|
373
|
+
incorrectImportPath: "Expected '{{expectedPath}}' but got '{{actualPath}}'.",
|
|
374
|
+
ancestorBarrelImport: "Cannot import from ancestor barrel '{{alias}}'. This would create a circular dependency. Import from the specific file or directory instead.",
|
|
375
|
+
unknownBoundaryImport: "Cannot import from '{{path}}' - path is outside all configured boundaries. Add this path to boundaries configuration or set 'allowUnknownBoundaries: true'.",
|
|
376
|
+
boundaryViolation: "Cannot import from '{{to}}' to '{{from}}': {{reason}}"
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
create(context) {
|
|
380
|
+
if (!context.options || context.options.length === 0) throw new Error("boundary-alias-vs-relative requires boundaries configuration");
|
|
381
|
+
const { rootDir = "src", boundaries, crossBoundaryStyle = "alias", defaultSeverity, allowUnknownBoundaries = false, skipBoundaryRulesForTestFiles = true } = context.options[0];
|
|
382
|
+
const cwd = context.getCwd?.() ?? process.cwd();
|
|
383
|
+
const resolvedBoundaries = boundaries.map((b) => ({
|
|
384
|
+
dir: b.dir,
|
|
385
|
+
alias: b.alias,
|
|
386
|
+
absDir: path.resolve(cwd, rootDir, b.dir),
|
|
387
|
+
allowImportsFrom: b.allowImportsFrom,
|
|
388
|
+
denyImportsFrom: b.denyImportsFrom,
|
|
389
|
+
allowTypeImportsFrom: b.allowTypeImportsFrom,
|
|
390
|
+
nestedPathFormat: b.nestedPathFormat,
|
|
391
|
+
severity: b.severity
|
|
392
|
+
}));
|
|
393
|
+
let cachedFileData = null;
|
|
394
|
+
/**
|
|
395
|
+
* Get metadata about the current file being linted.
|
|
396
|
+
* Results are cached per file to avoid recomputation.
|
|
397
|
+
*
|
|
398
|
+
* @returns FileData with directory and boundary information, or { isValid: false } if file path is invalid
|
|
399
|
+
*/
|
|
400
|
+
function getFileDataCached() {
|
|
401
|
+
if (cachedFileData) return cachedFileData;
|
|
402
|
+
cachedFileData = getFileData(context.filename ?? context.getFilename?.() ?? "<unknown>", resolvedBoundaries);
|
|
403
|
+
return cachedFileData;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Wrapper function that prepares file data and calls the main import handler.
|
|
407
|
+
*
|
|
408
|
+
* @param node - AST node for the import (ImportDeclaration, ImportExpression, or CallExpression)
|
|
409
|
+
* @param rawSpec - Raw import specifier string (e.g., '@entities', './file', '../parent')
|
|
410
|
+
* @param isTypeOnly - Whether this is a type-only import (TypeScript)
|
|
411
|
+
*/
|
|
412
|
+
function handleImportStatement(node, rawSpec, isTypeOnly = false) {
|
|
413
|
+
const fileData = getFileDataCached();
|
|
414
|
+
if (!fileData.isValid) return;
|
|
415
|
+
const { fileDir, fileBoundary } = fileData;
|
|
416
|
+
if (!fileDir) return;
|
|
417
|
+
handleImport({
|
|
418
|
+
node,
|
|
419
|
+
rawSpec,
|
|
420
|
+
fileDir,
|
|
421
|
+
fileBoundary: fileBoundary ?? null,
|
|
422
|
+
boundaries: resolvedBoundaries,
|
|
423
|
+
rootDir,
|
|
424
|
+
cwd,
|
|
425
|
+
context,
|
|
426
|
+
crossBoundaryStyle,
|
|
427
|
+
defaultSeverity,
|
|
428
|
+
allowUnknownBoundaries,
|
|
429
|
+
isTypeOnly,
|
|
430
|
+
skipBoundaryRules: skipBoundaryRulesForTestFiles
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
Program() {
|
|
435
|
+
cachedFileData = null;
|
|
436
|
+
},
|
|
437
|
+
ImportDeclaration(node) {
|
|
438
|
+
const spec = node.source?.value;
|
|
439
|
+
if (typeof spec === "string") handleImportStatement(node, spec, node.importKind === "type");
|
|
440
|
+
},
|
|
441
|
+
ImportExpression(node) {
|
|
442
|
+
const arg = node.source;
|
|
443
|
+
if (arg?.type === "Literal" && typeof arg.value === "string") handleImportStatement(node, arg.value, false);
|
|
444
|
+
},
|
|
445
|
+
CallExpression(node) {
|
|
446
|
+
if (node.callee.type === "Identifier" && node.callee.name === "require" && node.arguments.length === 1 && node.arguments[0]?.type === "Literal" && typeof node.arguments[0].value === "string") handleImportStatement(node, node.arguments[0].value, false);
|
|
447
|
+
},
|
|
448
|
+
ExportNamedDeclaration(node) {
|
|
449
|
+
const spec = node.source?.value;
|
|
450
|
+
if (typeof spec === "string") handleImportStatement(node, spec, node.exportKind === "type");
|
|
451
|
+
},
|
|
452
|
+
ExportAllDeclaration(node) {
|
|
453
|
+
const spec = node.source?.value;
|
|
454
|
+
if (typeof spec === "string") handleImportStatement(node, spec, node.exportKind === "type");
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
var eslint_plugin_import_boundaries_default = { rules: { enforce: rule } };
|
|
460
|
+
|
|
461
|
+
//#endregion
|
|
462
|
+
export { eslint_plugin_import_boundaries_default as default };
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "eslint-plugin-import-boundaries",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Enforce architectural boundaries with deterministic import paths",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./eslint-plugin-import-boundaries.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./eslint-plugin-import-boundaries.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"eslint-plugin-import-boundaries.js",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"eslint",
|
|
19
|
+
"eslint-plugin",
|
|
20
|
+
"eslint-rule",
|
|
21
|
+
"import",
|
|
22
|
+
"boundaries",
|
|
23
|
+
"architecture",
|
|
24
|
+
"clean-architecture",
|
|
25
|
+
"hexagonal-architecture",
|
|
26
|
+
"barrel-files",
|
|
27
|
+
"deterministic",
|
|
28
|
+
"import-paths"
|
|
29
|
+
],
|
|
30
|
+
"author": "ClassicalMoser",
|
|
31
|
+
"license": "ISC",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/ClassicalMoser/eslint-plugin-import-boundaries.git"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/ClassicalMoser/eslint-plugin-import-boundaries/issues"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/ClassicalMoser/eslint-plugin-import-boundaries#readme",
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18.0.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"eslint": ">=9.0.0"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsdown --config tsdown.config.ts",
|
|
48
|
+
"test": "vitest run",
|
|
49
|
+
"test:watch": "vitest watch",
|
|
50
|
+
"test:coverage": "vitest run --coverage",
|
|
51
|
+
"typecheck": "tsc --noEmit",
|
|
52
|
+
"lint": "eslint .",
|
|
53
|
+
"lint:fix": "eslint . --fix",
|
|
54
|
+
"format": "prettier --write .",
|
|
55
|
+
"prepublishOnly": "npm run build && npm run test && npm run typecheck"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@antfu/eslint-config": "^6.4.2",
|
|
59
|
+
"@rolldown/binding-darwin-x64": "1.0.0-beta.53",
|
|
60
|
+
"@types/node": "^24.10.1",
|
|
61
|
+
"@vitest/coverage-v8": "^4.0.15",
|
|
62
|
+
"eslint": "^9.39.1",
|
|
63
|
+
"eslint-config-prettier": "^10.1.8",
|
|
64
|
+
"prettier": "^3.7.4",
|
|
65
|
+
"tsdown": "0.17.0",
|
|
66
|
+
"typescript": "^5.9.3",
|
|
67
|
+
"vitest": "4.0.15"
|
|
68
|
+
}
|
|
69
|
+
}
|