architecture-linter 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/README.md +686 -0
- package/dist/aliasResolver.d.ts +37 -0
- package/dist/aliasResolver.d.ts.map +1 -0
- package/dist/aliasResolver.js +94 -0
- package/dist/aliasResolver.js.map +1 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +437 -0
- package/dist/cli.js.map +1 -0
- package/dist/contextParser.d.ts +12 -0
- package/dist/contextParser.d.ts.map +1 -0
- package/dist/contextParser.js +67 -0
- package/dist/contextParser.js.map +1 -0
- package/dist/dependencyScanner.d.ts +17 -0
- package/dist/dependencyScanner.d.ts.map +1 -0
- package/dist/dependencyScanner.js +78 -0
- package/dist/dependencyScanner.js.map +1 -0
- package/dist/explainer.d.ts +23 -0
- package/dist/explainer.d.ts.map +1 -0
- package/dist/explainer.js +113 -0
- package/dist/explainer.js.map +1 -0
- package/dist/presets.d.ts +21 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +118 -0
- package/dist/presets.js.map +1 -0
- package/dist/ruleEngine.d.ts +22 -0
- package/dist/ruleEngine.d.ts.map +1 -0
- package/dist/ruleEngine.js +141 -0
- package/dist/ruleEngine.js.map +1 -0
- package/dist/types.d.ts +91 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
# architecture-linter
|
|
2
|
+
|
|
3
|
+
> Enforce architectural layer rules in TypeScript projects from the command line.
|
|
4
|
+
|
|
5
|
+
`architecture-linter` reads a `.context.yml` configuration file and scans your
|
|
6
|
+
TypeScript source tree for dependency violations — such as a controller importing
|
|
7
|
+
a repository directly, bypassing the service layer.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install --save-dev architecture-linter
|
|
15
|
+
npx architecture-linter init # generate .context.yml from your folder structure
|
|
16
|
+
npx architecture-linter scan # check for violations
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Expected output when a violation exists:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
Scanning project...
|
|
23
|
+
|
|
24
|
+
❌ Architecture violation detected
|
|
25
|
+
|
|
26
|
+
File: controllers/orderController.ts
|
|
27
|
+
Import: repositories/orderRepository
|
|
28
|
+
Rule: Controller cannot import Repository
|
|
29
|
+
|
|
30
|
+
── Violations by layer ──────────────────
|
|
31
|
+
controller 1 violation(s)
|
|
32
|
+
service 0 violation(s)
|
|
33
|
+
repository 0 violation(s)
|
|
34
|
+
|
|
35
|
+
Found 1 violation in 3 file(s) scanned.
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
### As a local dev dependency (recommended)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install --save-dev architecture-linter
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Add a script to your `package.json`:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"scripts": {
|
|
53
|
+
"lint:arch": "architecture-linter scan"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Global install
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npm install -g architecture-linter
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Quick setup for a new project
|
|
67
|
+
|
|
68
|
+
Run `init` to auto-generate a `.context.yml` by inspecting your folder structure.
|
|
69
|
+
The command detects common layer names (`controller`, `service`, `repository`,
|
|
70
|
+
`middleware`, etc.) from top-level and `src/` subdirectory names.
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
architecture-linter init
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Then edit the generated file to add your constraints, and run:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
architecture-linter scan
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Framework presets
|
|
85
|
+
|
|
86
|
+
Use a built-in preset to get a sensible starting configuration for popular
|
|
87
|
+
architectural patterns. Declare it with the `extends` key in `.context.yml`:
|
|
88
|
+
|
|
89
|
+
```yaml
|
|
90
|
+
extends: nestjs
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
User-defined layers and rules always take precedence over preset defaults.
|
|
94
|
+
|
|
95
|
+
| Preset | Layers |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `nestjs` | module, controller, service, repository, guard, interceptor, pipe, decorator, dto, entity |
|
|
98
|
+
| `clean-architecture` | entity, usecase, repository, infrastructure, interface |
|
|
99
|
+
| `hexagonal` | domain, port, adapter, application, infrastructure |
|
|
100
|
+
| `nextjs` | page, component, hook, lib, api, store, util |
|
|
101
|
+
|
|
102
|
+
### Extending multiple presets
|
|
103
|
+
|
|
104
|
+
```yaml
|
|
105
|
+
extends:
|
|
106
|
+
- clean-architecture
|
|
107
|
+
- nestjs
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Overriding a preset rule
|
|
111
|
+
|
|
112
|
+
```yaml
|
|
113
|
+
extends: nestjs
|
|
114
|
+
|
|
115
|
+
rules:
|
|
116
|
+
# Override the nestjs default — allow controllers to import repositories directly
|
|
117
|
+
controller:
|
|
118
|
+
cannot_import: []
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Configuration reference
|
|
124
|
+
|
|
125
|
+
Create a `.context.yml` in your project root (or pass `--context` to override).
|
|
126
|
+
When `--context` is omitted, the linter walks up the directory tree until a
|
|
127
|
+
`.context.yml` is found — just like ESLint.
|
|
128
|
+
|
|
129
|
+
```yaml
|
|
130
|
+
# Optional: extend a built-in preset
|
|
131
|
+
extends: nestjs
|
|
132
|
+
|
|
133
|
+
architecture:
|
|
134
|
+
layers:
|
|
135
|
+
- controller
|
|
136
|
+
- service
|
|
137
|
+
- repository
|
|
138
|
+
|
|
139
|
+
rules:
|
|
140
|
+
# Blacklist: this layer must NOT import from any layer in the list.
|
|
141
|
+
controller:
|
|
142
|
+
cannot_import:
|
|
143
|
+
- repository
|
|
144
|
+
|
|
145
|
+
# Whitelist: this layer may ONLY import from layers in the list.
|
|
146
|
+
service:
|
|
147
|
+
can_only_import:
|
|
148
|
+
- repository
|
|
149
|
+
|
|
150
|
+
repository:
|
|
151
|
+
cannot_import: []
|
|
152
|
+
|
|
153
|
+
# Glob patterns (project-relative) for files to skip entirely.
|
|
154
|
+
exclude:
|
|
155
|
+
- "**/*.spec.ts"
|
|
156
|
+
- "**/*.test.ts"
|
|
157
|
+
- "**/__mocks__/**"
|
|
158
|
+
|
|
159
|
+
# Manual path alias overrides (supplements tsconfig.json paths automatically).
|
|
160
|
+
aliases:
|
|
161
|
+
"@repositories": "src/repositories"
|
|
162
|
+
"@services": "src/services"
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Rule options
|
|
166
|
+
|
|
167
|
+
| Option | Type | Description |
|
|
168
|
+
|---|---|---|
|
|
169
|
+
| `cannot_import` | `string[]` | Blacklist — the layer must not import from any listed layer |
|
|
170
|
+
| `can_only_import` | `string[]` | Whitelist — the layer may only import from listed layers |
|
|
171
|
+
| `files` | `string` (glob) | Scope this rule to source files matching the pattern |
|
|
172
|
+
|
|
173
|
+
`cannot_import` and `can_only_import` are mutually exclusive. Use one per layer rule.
|
|
174
|
+
|
|
175
|
+
#### Scoping a rule to specific files
|
|
176
|
+
|
|
177
|
+
```yaml
|
|
178
|
+
rules:
|
|
179
|
+
controller:
|
|
180
|
+
files: "src/controllers/admin/**"
|
|
181
|
+
cannot_import:
|
|
182
|
+
- repository
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Path alias resolution
|
|
186
|
+
|
|
187
|
+
The linter automatically reads `compilerOptions.paths` from your `tsconfig.json`
|
|
188
|
+
and resolves aliased imports before checking rules. No extra config needed for
|
|
189
|
+
standard TypeScript path aliases.
|
|
190
|
+
|
|
191
|
+
For monorepos or non-standard setups, add manual overrides via the `aliases` key:
|
|
192
|
+
|
|
193
|
+
```yaml
|
|
194
|
+
aliases:
|
|
195
|
+
"@repositories": "src/repositories"
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Manual aliases take precedence over any `tsconfig.json` entries with the same key.
|
|
199
|
+
|
|
200
|
+
### Inline suppression with `arch-ignore`
|
|
201
|
+
|
|
202
|
+
To suppress a single violation without removing the import, add an
|
|
203
|
+
`// arch-ignore:` comment on the line immediately before the import:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
// arch-ignore: controller cannot import repository
|
|
207
|
+
import { OrderRepository } from '../repositories/orderRepository';
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
The hint must match the rule string (case-insensitive): `<sourceLayer> cannot import <targetLayer>`.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
### How layer detection works
|
|
215
|
+
|
|
216
|
+
The linter infers a file's layer from its **directory name**. Both singular and
|
|
217
|
+
plural forms are recognised (including irregular plurals such as `repository` → `repositories`).
|
|
218
|
+
|
|
219
|
+
| Path | Detected layer |
|
|
220
|
+
|---|---|
|
|
221
|
+
| `controllers/orderController.ts` | `controller` |
|
|
222
|
+
| `services/orderService.ts` | `service` |
|
|
223
|
+
| `repositories/orderRepository.ts` | `repository` |
|
|
224
|
+
| `src/controllers/admin/ctrl.ts` | `controller` |
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## CLI reference
|
|
229
|
+
|
|
230
|
+
### `scan`
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
architecture-linter scan [options]
|
|
234
|
+
|
|
235
|
+
Options:
|
|
236
|
+
-c, --context <path> Path to the .context.yml file (auto-detected if omitted)
|
|
237
|
+
-p, --project <path> Root directory of the project to scan (default: .)
|
|
238
|
+
-f, --format <format> Output format: text or json (default: text)
|
|
239
|
+
-s, --strict Report files not assigned to any layer
|
|
240
|
+
-q, --quiet Suppress the "Scanning project..." banner
|
|
241
|
+
-e, --explain Print why/impact/how-to-fix guidance per violation
|
|
242
|
+
-x, --fix Show a suggested fix for each violation
|
|
243
|
+
-w, --watch Watch for file changes and re-scan automatically
|
|
244
|
+
-h, --help Display help
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
#### `--explain` — understand each violation
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
architecture-linter scan --explain
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Adds three sections below each violation:
|
|
254
|
+
- **Why this matters** — the architectural reason this rule exists
|
|
255
|
+
- **Impact** — what goes wrong if the violation is left in place
|
|
256
|
+
- **How to fix** — a concrete recommendation
|
|
257
|
+
|
|
258
|
+
#### `--fix` — get a suggested fix
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
architecture-linter scan --fix
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Prints a short actionable message per violation, e.g.:
|
|
265
|
+
|
|
266
|
+
```
|
|
267
|
+
🔧 Suggested fix
|
|
268
|
+
Instead of importing 'repository' directly, route through an allowed
|
|
269
|
+
intermediary layer: 'service'.
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
#### `--watch` — re-scan on file changes
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
architecture-linter scan --watch
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Watches the project directory for `.ts` file changes and re-runs the scan
|
|
279
|
+
automatically. Press `Ctrl+C` to stop.
|
|
280
|
+
|
|
281
|
+
### `init`
|
|
282
|
+
|
|
283
|
+
```
|
|
284
|
+
architecture-linter init [options]
|
|
285
|
+
|
|
286
|
+
Options:
|
|
287
|
+
-p, --project <path> Root directory of the project (default: .)
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Generates a starter `.context.yml` by detecting layer names from directory
|
|
291
|
+
structure. Fails safely if a `.context.yml` already exists.
|
|
292
|
+
|
|
293
|
+
### `ci`
|
|
294
|
+
|
|
295
|
+
```
|
|
296
|
+
architecture-linter ci [options]
|
|
297
|
+
|
|
298
|
+
Options:
|
|
299
|
+
--platform <platform> CI platform to target: github (default: github)
|
|
300
|
+
-p, --project <path> Root directory of the project (default: .)
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Generates a ready-to-use CI workflow file. Currently supports GitHub Actions:
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
architecture-linter ci
|
|
307
|
+
# Creates: .github/workflows/arch-lint.yml
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Fails safely if the workflow file already exists.
|
|
311
|
+
|
|
312
|
+
### Exit codes
|
|
313
|
+
|
|
314
|
+
| Code | Meaning |
|
|
315
|
+
|---|---|
|
|
316
|
+
| `0` | No violations found |
|
|
317
|
+
| `1` | One or more violations found (or a fatal error occurred) |
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## CI integration
|
|
322
|
+
|
|
323
|
+
Run the linter on every push and pull request. The `ci` command generates this
|
|
324
|
+
for you (`architecture-linter ci`), or add the step manually:
|
|
325
|
+
|
|
326
|
+
```yaml
|
|
327
|
+
# .github/workflows/arch-lint.yml
|
|
328
|
+
name: Architecture Lint
|
|
329
|
+
|
|
330
|
+
on:
|
|
331
|
+
push:
|
|
332
|
+
branches: ["**"]
|
|
333
|
+
pull_request:
|
|
334
|
+
branches: ["**"]
|
|
335
|
+
|
|
336
|
+
jobs:
|
|
337
|
+
arch-lint:
|
|
338
|
+
runs-on: ubuntu-latest
|
|
339
|
+
steps:
|
|
340
|
+
- uses: actions/checkout@v4
|
|
341
|
+
- uses: actions/setup-node@v4
|
|
342
|
+
with:
|
|
343
|
+
node-version: "20"
|
|
344
|
+
cache: "npm"
|
|
345
|
+
- run: npm ci
|
|
346
|
+
- run: npx architecture-linter scan --strict
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## JSON output
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
architecture-linter scan --format json
|
|
355
|
+
architecture-linter scan --format json --fix --explain
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
```json
|
|
359
|
+
{
|
|
360
|
+
"filesScanned": 3,
|
|
361
|
+
"violations": [
|
|
362
|
+
{
|
|
363
|
+
"file": "controllers/orderController.ts",
|
|
364
|
+
"importPath": "repositories/orderRepository",
|
|
365
|
+
"rawSpecifier": "../repositories/orderRepository",
|
|
366
|
+
"sourceLayer": "controller",
|
|
367
|
+
"targetLayer": "repository",
|
|
368
|
+
"rule": "Controller cannot import Repository",
|
|
369
|
+
"fix": "Instead of importing 'repository' directly, route through an allowed intermediary layer: 'service'.",
|
|
370
|
+
"explanation": {
|
|
371
|
+
"why": "...",
|
|
372
|
+
"impact": "...",
|
|
373
|
+
"fix": "..."
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
],
|
|
377
|
+
"unclassifiedFiles": [],
|
|
378
|
+
"violationsByLayer": {
|
|
379
|
+
"controller": 1,
|
|
380
|
+
"service": 0,
|
|
381
|
+
"repository": 0
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
## Development
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
# Install dependencies
|
|
392
|
+
npm install
|
|
393
|
+
|
|
394
|
+
# Run directly with ts-node (no build required)
|
|
395
|
+
npx ts-node src/cli.ts scan --context examples/sample.context.yml --project examples/sample-project
|
|
396
|
+
|
|
397
|
+
# Build to dist/
|
|
398
|
+
npm run build
|
|
399
|
+
|
|
400
|
+
# Run the full test suite
|
|
401
|
+
npm test
|
|
402
|
+
|
|
403
|
+
# Run tests in watch mode
|
|
404
|
+
npm run test:watch
|
|
405
|
+
|
|
406
|
+
# Run tests with coverage report
|
|
407
|
+
npm run test:coverage
|
|
408
|
+
|
|
409
|
+
# Clean build artefacts
|
|
410
|
+
npm run clean
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## Project structure
|
|
416
|
+
|
|
417
|
+
```
|
|
418
|
+
architecture-linter/
|
|
419
|
+
├── src/
|
|
420
|
+
│ ├── cli.ts # Commander-based CLI entry point
|
|
421
|
+
│ ├── contextParser.ts # Loads and validates .context.yml; walks up directory tree
|
|
422
|
+
│ ├── dependencyScanner.ts # Walks .ts files and extracts imports via ts-morph
|
|
423
|
+
│ ├── ruleEngine.ts # Matches imports against rules; builds violations
|
|
424
|
+
│ ├── aliasResolver.ts # Resolves tsconfig.json path aliases
|
|
425
|
+
│ ├── presets.ts # Built-in framework presets
|
|
426
|
+
│ ├── explainer.ts # Why/impact/fix guidance for --explain
|
|
427
|
+
│ └── types.ts # Shared TypeScript interfaces
|
|
428
|
+
│
|
|
429
|
+
├── src/__tests__/ # Jest test suite (105 tests)
|
|
430
|
+
│
|
|
431
|
+
├── examples/
|
|
432
|
+
│ ├── sample.context.yml # Example rule configuration
|
|
433
|
+
│ ├── sample-project/ # ❌ intentional violation for demo
|
|
434
|
+
│ ├── alias-test/ # Demo of path alias resolution
|
|
435
|
+
│ └── preset-test/ # Demo of framework presets
|
|
436
|
+
│
|
|
437
|
+
├── .github/workflows/ci.yml # Runs tests on every push/PR
|
|
438
|
+
├── jest.config.js
|
|
439
|
+
├── package.json
|
|
440
|
+
├── tsconfig.json
|
|
441
|
+
└── README.md
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## How it works
|
|
447
|
+
|
|
448
|
+
1. **Parse config** — `contextParser` loads `.context.yml`, merges any preset
|
|
449
|
+
declared via `extends`, validates required fields, and walks up the directory
|
|
450
|
+
tree when no explicit path is provided.
|
|
451
|
+
2. **Scan files** — `dependencyScanner` uses `fast-glob` to find every `.ts`
|
|
452
|
+
file (respecting `exclude` patterns) and `ts-morph` to parse import
|
|
453
|
+
declarations. Path aliases are resolved via `aliasResolver` before rules are
|
|
454
|
+
applied. Each import is checked for a preceding `// arch-ignore:` comment.
|
|
455
|
+
3. **Check rules** — `ruleEngine` evaluates `cannot_import` / `can_only_import`
|
|
456
|
+
rules against each import. Violations are collected with optional fix
|
|
457
|
+
suggestions and layer-level counts.
|
|
458
|
+
4. **Report** — results are printed as human-readable text (with colour) or
|
|
459
|
+
machine-readable JSON. The process exits `0` for clean, `1` for violations.
|
|
460
|
+
3. **Apply rules** — `ruleEngine` maps each file and resolved import to an
|
|
461
|
+
architectural layer, then evaluates `cannot_import` (blacklist) and
|
|
462
|
+
`can_only_import` (whitelist) rules. Per-rule `files` glob scoping is
|
|
463
|
+
applied via `minimatch`.
|
|
464
|
+
4. **Report** — The CLI prints every violation with the file, import path, and
|
|
465
|
+
rule broken. A per-layer summary is shown at the end. `--format json` emits
|
|
466
|
+
machine-readable output.
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
## Roadmap (post-MVP)
|
|
471
|
+
|
|
472
|
+
- `--fix` flag to suggest corrected import paths
|
|
473
|
+
- SARIF output format for GitHub Advanced Security integration
|
|
474
|
+
- Watch mode (`--watch`)
|
|
475
|
+
- Support for TypeScript path alias resolution (`@app/repositories`)
|
|
476
|
+
|
|
477
|
+
---
|
|
478
|
+
|
|
479
|
+
## License
|
|
480
|
+
|
|
481
|
+
MIT
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
`architecture-linter` reads a `.context.yml` configuration file and scans your
|
|
485
|
+
TypeScript source tree for dependency violations — such as a controller importing
|
|
486
|
+
a repository directly, bypassing the service layer.
|
|
487
|
+
|
|
488
|
+
---
|
|
489
|
+
|
|
490
|
+
## Quick start
|
|
491
|
+
|
|
492
|
+
```bash
|
|
493
|
+
# 1. Install dependencies
|
|
494
|
+
npm install
|
|
495
|
+
|
|
496
|
+
# 2. Build
|
|
497
|
+
npm run build
|
|
498
|
+
|
|
499
|
+
# 3. Scan the bundled example project
|
|
500
|
+
node dist/cli.js scan \
|
|
501
|
+
--context examples/sample.context.yml \
|
|
502
|
+
--project examples/sample-project
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
Expected output:
|
|
506
|
+
|
|
507
|
+
```
|
|
508
|
+
Scanning project...
|
|
509
|
+
|
|
510
|
+
❌ Architecture violation detected
|
|
511
|
+
|
|
512
|
+
File: controllers/orderController.ts
|
|
513
|
+
Import: repositories/orderRepository
|
|
514
|
+
Rule: Controller cannot import Repository
|
|
515
|
+
|
|
516
|
+
Found 1 violation in 3 file(s) scanned.
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
---
|
|
520
|
+
|
|
521
|
+
## Installation
|
|
522
|
+
|
|
523
|
+
### As a local dev dependency
|
|
524
|
+
|
|
525
|
+
```bash
|
|
526
|
+
npm install --save-dev architecture-linter
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
Then add a script to your `package.json`:
|
|
530
|
+
|
|
531
|
+
```json
|
|
532
|
+
{
|
|
533
|
+
"scripts": {
|
|
534
|
+
"lint:arch": "architecture-linter scan"
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### Global install
|
|
540
|
+
|
|
541
|
+
```bash
|
|
542
|
+
npm install -g architecture-linter
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
---
|
|
546
|
+
|
|
547
|
+
## Configuration
|
|
548
|
+
|
|
549
|
+
Create a `.context.yml` file in your project root (or pass `--context` to point
|
|
550
|
+
to a different path).
|
|
551
|
+
|
|
552
|
+
```yaml
|
|
553
|
+
architecture:
|
|
554
|
+
layers:
|
|
555
|
+
- controller
|
|
556
|
+
- service
|
|
557
|
+
- repository
|
|
558
|
+
|
|
559
|
+
rules:
|
|
560
|
+
controller:
|
|
561
|
+
cannot_import:
|
|
562
|
+
- repository # Controllers must go through the service layer
|
|
563
|
+
|
|
564
|
+
service:
|
|
565
|
+
cannot_import: []
|
|
566
|
+
|
|
567
|
+
repository:
|
|
568
|
+
cannot_import: []
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
### How layer detection works
|
|
572
|
+
|
|
573
|
+
The linter infers a file's layer from its **directory name**. A file inside a
|
|
574
|
+
directory called `controllers/` or `controller/` is automatically assigned to
|
|
575
|
+
the `controller` layer. Both singular and plural forms are recognised.
|
|
576
|
+
|
|
577
|
+
| Path | Detected layer |
|
|
578
|
+
|---|---|
|
|
579
|
+
| `controllers/orderController.ts` | `controller` |
|
|
580
|
+
| `services/orderService.ts` | `service` |
|
|
581
|
+
| `repositories/orderRepository.ts` | `repository` |
|
|
582
|
+
|
|
583
|
+
The same logic applies to import paths: a relative import that resolves into a
|
|
584
|
+
`repositories/` directory is treated as a `repository`-layer import.
|
|
585
|
+
|
|
586
|
+
---
|
|
587
|
+
|
|
588
|
+
## CLI reference
|
|
589
|
+
|
|
590
|
+
```
|
|
591
|
+
architecture-linter scan [options]
|
|
592
|
+
|
|
593
|
+
Options:
|
|
594
|
+
-c, --context <path> Path to the .context.yml file (default: .context.yml)
|
|
595
|
+
-p, --project <path> Root directory of the project (default: .)
|
|
596
|
+
-V, --version Print version number
|
|
597
|
+
-h, --help Display help
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
### Exit codes
|
|
601
|
+
|
|
602
|
+
| Code | Meaning |
|
|
603
|
+
|---|---|
|
|
604
|
+
| `0` | No violations found |
|
|
605
|
+
| `1` | One or more violations found (or a fatal error occurred) |
|
|
606
|
+
|
|
607
|
+
This makes the tool suitable for use in CI pipelines:
|
|
608
|
+
|
|
609
|
+
```yaml
|
|
610
|
+
# .github/workflows/ci.yml (example)
|
|
611
|
+
- name: Architecture lint
|
|
612
|
+
run: npx architecture-linter scan
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
---
|
|
616
|
+
|
|
617
|
+
## Development
|
|
618
|
+
|
|
619
|
+
```bash
|
|
620
|
+
# Run directly with ts-node (no build step required)
|
|
621
|
+
npx ts-node src/cli.ts scan --context examples/sample.context.yml --project examples/sample-project
|
|
622
|
+
|
|
623
|
+
# Build to dist/
|
|
624
|
+
npm run build
|
|
625
|
+
|
|
626
|
+
# Run the compiled output
|
|
627
|
+
node dist/cli.js scan --context examples/sample.context.yml --project examples/sample-project
|
|
628
|
+
|
|
629
|
+
# Clean build artefacts
|
|
630
|
+
npm run clean
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
---
|
|
634
|
+
|
|
635
|
+
## Project structure
|
|
636
|
+
|
|
637
|
+
```
|
|
638
|
+
architecture-linter/
|
|
639
|
+
├── src/
|
|
640
|
+
│ ├── cli.ts # Commander-based CLI entry point
|
|
641
|
+
│ ├── contextParser.ts # Loads and validates .context.yml
|
|
642
|
+
│ ├── dependencyScanner.ts # Walks .ts files and extracts imports via ts-morph
|
|
643
|
+
│ ├── ruleEngine.ts # Matches imports against rules and returns violations
|
|
644
|
+
│ └── types.ts # Shared TypeScript interfaces
|
|
645
|
+
│
|
|
646
|
+
├── examples/
|
|
647
|
+
│ ├── sample.context.yml # Example rule configuration
|
|
648
|
+
│ └── sample-project/
|
|
649
|
+
│ ├── controllers/orderController.ts # ❌ contains an intentional violation
|
|
650
|
+
│ ├── services/orderService.ts
|
|
651
|
+
│ └── repositories/orderRepository.ts
|
|
652
|
+
│
|
|
653
|
+
├── package.json
|
|
654
|
+
├── tsconfig.json
|
|
655
|
+
└── README.md
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
---
|
|
659
|
+
|
|
660
|
+
## How it works
|
|
661
|
+
|
|
662
|
+
1. **Parse config** — `contextParser` loads `.context.yml` using `js-yaml` and
|
|
663
|
+
validates the required fields.
|
|
664
|
+
2. **Scan files** — `dependencyScanner` uses `fast-glob` to find every `.ts`
|
|
665
|
+
file in the project and `ts-morph` to parse its import declarations.
|
|
666
|
+
Relative imports are resolved to project-relative paths.
|
|
667
|
+
3. **Apply rules** — `ruleEngine` maps each file and each resolved import path
|
|
668
|
+
to an architectural layer, then checks the `cannot_import` rules.
|
|
669
|
+
4. **Report** — The CLI prints every violation with the offending file, the
|
|
670
|
+
resolved import path, and the rule that was broken.
|
|
671
|
+
|
|
672
|
+
---
|
|
673
|
+
|
|
674
|
+
## Roadmap (post-MVP)
|
|
675
|
+
|
|
676
|
+
- `--fix` flag to suggest corrected import paths
|
|
677
|
+
- JSON / SARIF output format for CI integration
|
|
678
|
+
- Wildcard layer patterns (`src/*/controllers/**`)
|
|
679
|
+
- Support for path alias resolution (`@app/repositories`)
|
|
680
|
+
- Watch mode
|
|
681
|
+
|
|
682
|
+
---
|
|
683
|
+
|
|
684
|
+
## License
|
|
685
|
+
|
|
686
|
+
MIT
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A resolved alias map where each key is an alias prefix (e.g. '@repositories')
|
|
3
|
+
* and each value is the absolute path to the corresponding directory on disk.
|
|
4
|
+
*/
|
|
5
|
+
export interface AliasMap {
|
|
6
|
+
[prefix: string]: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Reads tsconfig.json from the project root and extracts compilerOptions.paths
|
|
10
|
+
* entries, resolving them relative to compilerOptions.baseUrl (or the project root).
|
|
11
|
+
*
|
|
12
|
+
* Handles tsconfig files that contain // line comments (which TypeScript allows).
|
|
13
|
+
*
|
|
14
|
+
* Returns an empty map if tsconfig.json is absent or cannot be parsed.
|
|
15
|
+
*/
|
|
16
|
+
export declare function loadTsConfigAliases(projectDir: string): AliasMap;
|
|
17
|
+
/**
|
|
18
|
+
* Merges tsconfig-derived aliases with any manual aliases declared in .context.yml.
|
|
19
|
+
* Manual aliases take precedence over tsconfig aliases.
|
|
20
|
+
*
|
|
21
|
+
* @param tsconfigAliases Aliases loaded from tsconfig.json
|
|
22
|
+
* @param configAliases Aliases from the `aliases:` field in .context.yml (project-relative dirs)
|
|
23
|
+
* @param projectDir Absolute path to the project root (used to resolve configAliases)
|
|
24
|
+
*/
|
|
25
|
+
export declare function mergeAliases(tsconfigAliases: AliasMap, configAliases: Record<string, string> | undefined, projectDir: string): AliasMap;
|
|
26
|
+
/**
|
|
27
|
+
* Attempts to resolve a bare (non-relative) import specifier using the alias map.
|
|
28
|
+
*
|
|
29
|
+
* Returns a project-relative forward-slash path on success, or null if the specifier
|
|
30
|
+
* does not match any known alias (e.g. it's a node_modules package).
|
|
31
|
+
*
|
|
32
|
+
* @param specifier The raw import string (e.g. '@repositories/orderRepo')
|
|
33
|
+
* @param aliases The merged alias map
|
|
34
|
+
* @param projectDir Absolute path to the project root
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveAlias(specifier: string, aliases: AliasMap, projectDir: string): string | null;
|
|
37
|
+
//# sourceMappingURL=aliasResolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aliasResolver.d.ts","sourceRoot":"","sources":["../src/aliasResolver.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,MAAM,WAAW,QAAQ;IACvB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,QAAQ,CA0ChE;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,eAAe,EAAE,QAAQ,EACzB,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,EACjD,UAAU,EAAE,MAAM,GACjB,QAAQ,CAUV;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAC1B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,QAAQ,EACjB,UAAU,EAAE,MAAM,GACjB,MAAM,GAAG,IAAI,CASf"}
|