architecture-linter 0.1.1 → 0.1.3

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.
Files changed (2) hide show
  1. package/README.md +777 -686
  2. package/package.json +48 -48
package/README.md CHANGED
@@ -1,686 +1,777 @@
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
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
+ ## GitHub Action
322
+
323
+ The easiest way to enforce architecture rules on every pull request no Node.js
324
+ setup needed, violations appear as inline code annotations.
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
+
342
+ - uses: cvalingam/architecture-linter@v0.1.2
343
+ with:
344
+ config: .context.yml # path to your config (default: .context.yml)
345
+ fail-on-violations: 'true' # fail the job on violations (default: true)
346
+ token: ${{ secrets.GITHUB_TOKEN }} # enables PR comment summary (optional)
347
+ ```
348
+
349
+ ### Action inputs
350
+
351
+ | Input | Default | Description |
352
+ |---|---|---|
353
+ | `config` | `.context.yml` | Path to the config file |
354
+ | `working-directory` | `.` | Root directory to scan |
355
+ | `fail-on-violations` | `true` | Fail the step when violations are found |
356
+ | `token` | `''` | GitHub token — enables PR comment with violation table |
357
+
358
+ ### Action outputs
359
+
360
+ | Output | Description |
361
+ |---|---|
362
+ | `violations` | Number of violations found (usable in subsequent steps) |
363
+
364
+ When violations are found, each one appears as a **red annotation** directly on
365
+ the diff line in the PR, and a summary comment is posted to the PR thread.
366
+
367
+ ---
368
+
369
+ ## GitHub Action
370
+
371
+ The easiest way to enforce architecture rules on every pull request — no manual
372
+ setup required. Violations are posted as inline PR annotations and an optional
373
+ PR comment summary:
374
+
375
+ ```yaml
376
+ # .github/workflows/arch-lint.yml
377
+ name: Architecture Lint
378
+
379
+ on:
380
+ pull_request:
381
+ branches: ["**"]
382
+
383
+ jobs:
384
+ arch-lint:
385
+ runs-on: ubuntu-latest
386
+ steps:
387
+ - uses: actions/checkout@v4
388
+
389
+ - name: Enforce architecture rules
390
+ uses: cvalingam/architecture-linter@v0.1.2
391
+ with:
392
+ token: ${{ secrets.GITHUB_TOKEN }} # for PR comment (optional)
393
+ ```
394
+
395
+ **Inputs**
396
+
397
+ | Input | Default | Description |
398
+ |---|---|---|
399
+ | `config` | `.context.yml` | Path to your config file |
400
+ | `working-directory` | `.` | Root of the project to scan |
401
+ | `fail-on-violations` | `true` | Fail the step when violations are found |
402
+ | `token` | `''` | `GITHUB_TOKEN` — enables PR comment summary |
403
+
404
+ **Outputs**
405
+
406
+ | Output | Description |
407
+ |---|---|
408
+ | `violations` | Number of violations found |
409
+
410
+ ---
411
+
412
+ ## CI integration (manual setup)
413
+
414
+ Run the linter on every push and pull request. The `ci` command generates this
415
+ for you (`architecture-linter ci`), or add the step manually:
416
+
417
+ ```yaml
418
+ # .github/workflows/arch-lint.yml
419
+ name: Architecture Lint
420
+
421
+ on:
422
+ push:
423
+ branches: ["**"]
424
+ pull_request:
425
+ branches: ["**"]
426
+
427
+ jobs:
428
+ arch-lint:
429
+ runs-on: ubuntu-latest
430
+ steps:
431
+ - uses: actions/checkout@v4
432
+ - uses: actions/setup-node@v4
433
+ with:
434
+ node-version: "20"
435
+ cache: "npm"
436
+ - run: npm ci
437
+ - run: npx architecture-linter scan --strict
438
+ ```
439
+
440
+ ---
441
+
442
+ ## JSON output
443
+
444
+ ```bash
445
+ architecture-linter scan --format json
446
+ architecture-linter scan --format json --fix --explain
447
+ ```
448
+
449
+ ```json
450
+ {
451
+ "filesScanned": 3,
452
+ "violations": [
453
+ {
454
+ "file": "controllers/orderController.ts",
455
+ "importPath": "repositories/orderRepository",
456
+ "rawSpecifier": "../repositories/orderRepository",
457
+ "sourceLayer": "controller",
458
+ "targetLayer": "repository",
459
+ "rule": "Controller cannot import Repository",
460
+ "fix": "Instead of importing 'repository' directly, route through an allowed intermediary layer: 'service'.",
461
+ "explanation": {
462
+ "why": "...",
463
+ "impact": "...",
464
+ "fix": "..."
465
+ }
466
+ }
467
+ ],
468
+ "unclassifiedFiles": [],
469
+ "violationsByLayer": {
470
+ "controller": 1,
471
+ "service": 0,
472
+ "repository": 0
473
+ }
474
+ }
475
+ ```
476
+
477
+ ---
478
+
479
+ ## Development
480
+
481
+ ```bash
482
+ # Install dependencies
483
+ npm install
484
+
485
+ # Run directly with ts-node (no build required)
486
+ npx ts-node src/cli.ts scan --context examples/sample.context.yml --project examples/sample-project
487
+
488
+ # Build to dist/
489
+ npm run build
490
+
491
+ # Run the full test suite
492
+ npm test
493
+
494
+ # Run tests in watch mode
495
+ npm run test:watch
496
+
497
+ # Run tests with coverage report
498
+ npm run test:coverage
499
+
500
+ # Clean build artefacts
501
+ npm run clean
502
+ ```
503
+
504
+ ---
505
+
506
+ ## Project structure
507
+
508
+ ```
509
+ architecture-linter/
510
+ ├── src/
511
+ │ ├── cli.ts # Commander-based CLI entry point
512
+ ├── contextParser.ts # Loads and validates .context.yml; walks up directory tree
513
+ ├── dependencyScanner.ts # Walks .ts files and extracts imports via ts-morph
514
+ ├── ruleEngine.ts # Matches imports against rules; builds violations
515
+ │ ├── aliasResolver.ts # Resolves tsconfig.json path aliases
516
+ │ ├── presets.ts # Built-in framework presets
517
+ │ ├── explainer.ts # Why/impact/fix guidance for --explain
518
+ │ └── types.ts # Shared TypeScript interfaces
519
+
520
+ ├── src/__tests__/ # Jest test suite (105 tests)
521
+
522
+ ├── examples/
523
+ │ ├── sample.context.yml # Example rule configuration
524
+ │ ├── sample-project/ # ❌ intentional violation for demo
525
+ │ ├── alias-test/ # Demo of path alias resolution
526
+ │ └── preset-test/ # Demo of framework presets
527
+
528
+ ├── .github/workflows/ci.yml # Runs tests on every push/PR
529
+ ├── jest.config.js
530
+ ├── package.json
531
+ ├── tsconfig.json
532
+ └── README.md
533
+ ```
534
+
535
+ ---
536
+
537
+ ## How it works
538
+
539
+ 1. **Parse config** — `contextParser` loads `.context.yml`, merges any preset
540
+ declared via `extends`, validates required fields, and walks up the directory
541
+ tree when no explicit path is provided.
542
+ 2. **Scan files** — `dependencyScanner` uses `fast-glob` to find every `.ts`
543
+ file (respecting `exclude` patterns) and `ts-morph` to parse import
544
+ declarations. Path aliases are resolved via `aliasResolver` before rules are
545
+ applied. Each import is checked for a preceding `// arch-ignore:` comment.
546
+ 3. **Check rules** — `ruleEngine` evaluates `cannot_import` / `can_only_import`
547
+ rules against each import. Violations are collected with optional fix
548
+ suggestions and layer-level counts.
549
+ 4. **Report** results are printed as human-readable text (with colour) or
550
+ machine-readable JSON. The process exits `0` for clean, `1` for violations.
551
+ 3. **Apply rules** — `ruleEngine` maps each file and resolved import to an
552
+ architectural layer, then evaluates `cannot_import` (blacklist) and
553
+ `can_only_import` (whitelist) rules. Per-rule `files` glob scoping is
554
+ applied via `minimatch`.
555
+ 4. **Report** — The CLI prints every violation with the file, import path, and
556
+ rule broken. A per-layer summary is shown at the end. `--format json` emits
557
+ machine-readable output.
558
+
559
+ ---
560
+
561
+ ## Roadmap (post-MVP)
562
+
563
+ - `--fix` flag to suggest corrected import paths
564
+ - SARIF output format for GitHub Advanced Security integration
565
+ - Watch mode (`--watch`)
566
+ - Support for TypeScript path alias resolution (`@app/repositories`)
567
+
568
+ ---
569
+
570
+ ## License
571
+
572
+ MIT
573
+
574
+
575
+ `architecture-linter` reads a `.context.yml` configuration file and scans your
576
+ TypeScript source tree for dependency violations — such as a controller importing
577
+ a repository directly, bypassing the service layer.
578
+
579
+ ---
580
+
581
+ ## Quick start
582
+
583
+ ```bash
584
+ # 1. Install dependencies
585
+ npm install
586
+
587
+ # 2. Build
588
+ npm run build
589
+
590
+ # 3. Scan the bundled example project
591
+ node dist/cli.js scan \
592
+ --context examples/sample.context.yml \
593
+ --project examples/sample-project
594
+ ```
595
+
596
+ Expected output:
597
+
598
+ ```
599
+ Scanning project...
600
+
601
+ ❌ Architecture violation detected
602
+
603
+ File: controllers/orderController.ts
604
+ Import: repositories/orderRepository
605
+ Rule: Controller cannot import Repository
606
+
607
+ Found 1 violation in 3 file(s) scanned.
608
+ ```
609
+
610
+ ---
611
+
612
+ ## Installation
613
+
614
+ ### As a local dev dependency
615
+
616
+ ```bash
617
+ npm install --save-dev architecture-linter
618
+ ```
619
+
620
+ Then add a script to your `package.json`:
621
+
622
+ ```json
623
+ {
624
+ "scripts": {
625
+ "lint:arch": "architecture-linter scan"
626
+ }
627
+ }
628
+ ```
629
+
630
+ ### Global install
631
+
632
+ ```bash
633
+ npm install -g architecture-linter
634
+ ```
635
+
636
+ ---
637
+
638
+ ## Configuration
639
+
640
+ Create a `.context.yml` file in your project root (or pass `--context` to point
641
+ to a different path).
642
+
643
+ ```yaml
644
+ architecture:
645
+ layers:
646
+ - controller
647
+ - service
648
+ - repository
649
+
650
+ rules:
651
+ controller:
652
+ cannot_import:
653
+ - repository # Controllers must go through the service layer
654
+
655
+ service:
656
+ cannot_import: []
657
+
658
+ repository:
659
+ cannot_import: []
660
+ ```
661
+
662
+ ### How layer detection works
663
+
664
+ The linter infers a file's layer from its **directory name**. A file inside a
665
+ directory called `controllers/` or `controller/` is automatically assigned to
666
+ the `controller` layer. Both singular and plural forms are recognised.
667
+
668
+ | Path | Detected layer |
669
+ |---|---|
670
+ | `controllers/orderController.ts` | `controller` |
671
+ | `services/orderService.ts` | `service` |
672
+ | `repositories/orderRepository.ts` | `repository` |
673
+
674
+ The same logic applies to import paths: a relative import that resolves into a
675
+ `repositories/` directory is treated as a `repository`-layer import.
676
+
677
+ ---
678
+
679
+ ## CLI reference
680
+
681
+ ```
682
+ architecture-linter scan [options]
683
+
684
+ Options:
685
+ -c, --context <path> Path to the .context.yml file (default: .context.yml)
686
+ -p, --project <path> Root directory of the project (default: .)
687
+ -V, --version Print version number
688
+ -h, --help Display help
689
+ ```
690
+
691
+ ### Exit codes
692
+
693
+ | Code | Meaning |
694
+ |---|---|
695
+ | `0` | No violations found |
696
+ | `1` | One or more violations found (or a fatal error occurred) |
697
+
698
+ This makes the tool suitable for use in CI pipelines:
699
+
700
+ ```yaml
701
+ # .github/workflows/ci.yml (example)
702
+ - name: Architecture lint
703
+ run: npx architecture-linter scan
704
+ ```
705
+
706
+ ---
707
+
708
+ ## Development
709
+
710
+ ```bash
711
+ # Run directly with ts-node (no build step required)
712
+ npx ts-node src/cli.ts scan --context examples/sample.context.yml --project examples/sample-project
713
+
714
+ # Build to dist/
715
+ npm run build
716
+
717
+ # Run the compiled output
718
+ node dist/cli.js scan --context examples/sample.context.yml --project examples/sample-project
719
+
720
+ # Clean build artefacts
721
+ npm run clean
722
+ ```
723
+
724
+ ---
725
+
726
+ ## Project structure
727
+
728
+ ```
729
+ architecture-linter/
730
+ ├── src/
731
+ │ ├── cli.ts # Commander-based CLI entry point
732
+ │ ├── contextParser.ts # Loads and validates .context.yml
733
+ │ ├── dependencyScanner.ts # Walks .ts files and extracts imports via ts-morph
734
+ │ ├── ruleEngine.ts # Matches imports against rules and returns violations
735
+ │ └── types.ts # Shared TypeScript interfaces
736
+
737
+ ├── examples/
738
+ │ ├── sample.context.yml # Example rule configuration
739
+ │ └── sample-project/
740
+ │ ├── controllers/orderController.ts # ❌ contains an intentional violation
741
+ │ ├── services/orderService.ts
742
+ │ └── repositories/orderRepository.ts
743
+
744
+ ├── package.json
745
+ ├── tsconfig.json
746
+ └── README.md
747
+ ```
748
+
749
+ ---
750
+
751
+ ## How it works
752
+
753
+ 1. **Parse config** — `contextParser` loads `.context.yml` using `js-yaml` and
754
+ validates the required fields.
755
+ 2. **Scan files** — `dependencyScanner` uses `fast-glob` to find every `.ts`
756
+ file in the project and `ts-morph` to parse its import declarations.
757
+ Relative imports are resolved to project-relative paths.
758
+ 3. **Apply rules** — `ruleEngine` maps each file and each resolved import path
759
+ to an architectural layer, then checks the `cannot_import` rules.
760
+ 4. **Report** — The CLI prints every violation with the offending file, the
761
+ resolved import path, and the rule that was broken.
762
+
763
+ ---
764
+
765
+ ## Roadmap (post-MVP)
766
+
767
+ - `--fix` flag to suggest corrected import paths
768
+ - JSON / SARIF output format for CI integration
769
+ - Wildcard layer patterns (`src/*/controllers/**`)
770
+ - Support for path alias resolution (`@app/repositories`)
771
+ - Watch mode
772
+
773
+ ---
774
+
775
+ ## License
776
+
777
+ MIT