@alveolus/arch 0.1.0-alpha.0 → 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.
@@ -0,0 +1,2810 @@
1
+ import { Command, CommanderError, Option } from "commander";
2
+ import { existsSync } from "node:fs";
3
+ import { basename, dirname, isAbsolute, join, matchesGlob, normalize, relative, resolve, sep } from "node:path";
4
+ import { readFile, writeFile } from "node:fs/promises";
5
+ import { createHash } from "node:crypto";
6
+ import picocolors from "picocolors";
7
+ import { createJiti } from "jiti";
8
+ import { z } from "zod";
9
+ import { ModuleDeclarationKind, Node, Project, Scope, SyntaxKind, VariableDeclarationKind, ts } from "ts-morph";
10
+ //#region src/check/baseline.ts
11
+ var Baseline = class Baseline {
12
+ entries;
13
+ static fileName = "alveolus.baseline.json";
14
+ constructor(entries) {
15
+ this.entries = entries;
16
+ }
17
+ static of(violations) {
18
+ return new Baseline(violations.map(({ file, fingerprint, rule, symbol }) => ({
19
+ file,
20
+ fingerprint,
21
+ rule,
22
+ symbol
23
+ })));
24
+ }
25
+ static async load(path) {
26
+ if (!existsSync(path)) return new Baseline([]);
27
+ const content = JSON.parse(await readFile(path, "utf8"));
28
+ return new Baseline(content.violations ?? []);
29
+ }
30
+ get size() {
31
+ return this.entries.length;
32
+ }
33
+ get outdatedEntries() {
34
+ return this.entries.filter((entry) => entry.fingerprint === void 0).length;
35
+ }
36
+ newViolations(violations) {
37
+ return this.match(violations).fresh;
38
+ }
39
+ staleEntries(violations) {
40
+ return this.match(violations).stale;
41
+ }
42
+ match(violations) {
43
+ const remaining = /* @__PURE__ */ new Map();
44
+ for (const entry of this.entries) remaining.set(this.keyOf(entry), (remaining.get(this.keyOf(entry)) ?? 0) + 1);
45
+ const fresh = [];
46
+ for (const violation of violations) {
47
+ const key = this.keyOf(violation);
48
+ const count = remaining.get(key) ?? 0;
49
+ if (count > 0) remaining.set(key, count - 1);
50
+ else fresh.push(violation);
51
+ }
52
+ let stale = 0;
53
+ for (const count of remaining.values()) stale += count;
54
+ return {
55
+ fresh,
56
+ stale
57
+ };
58
+ }
59
+ async save(path) {
60
+ const violations = [...this.entries].sort((left, right) => this.keyOf(left).localeCompare(this.keyOf(right)));
61
+ await writeFile(path, `${JSON.stringify({ violations }, null, " ")}\n`);
62
+ }
63
+ keyOf(entry) {
64
+ return [
65
+ entry.rule,
66
+ entry.file,
67
+ entry.symbol,
68
+ entry.fingerprint ?? ""
69
+ ].join("\n");
70
+ }
71
+ };
72
+ //#endregion
73
+ //#region src/architecture/allowed-packages.ts
74
+ var AllowedPackages = class AllowedPackages {
75
+ packages;
76
+ constructor(packages = {}) {
77
+ this.packages = packages;
78
+ }
79
+ has(packageName) {
80
+ return Object.hasOwn(this.packages, packageName);
81
+ }
82
+ forbiddenNames(packageName, names) {
83
+ const allowed = this.packages[packageName];
84
+ if (allowed === void 0) return names;
85
+ if (allowed === true) return [];
86
+ return names.filter((name) => !allowed.includes(name));
87
+ }
88
+ allowedNames(packageName) {
89
+ const allowed = this.packages[packageName];
90
+ if (allowed === void 0 || allowed === true) return [];
91
+ return allowed;
92
+ }
93
+ with(other) {
94
+ const merged = { ...this.packages };
95
+ for (const [packageName, allowed] of Object.entries(other.packages)) {
96
+ const existing = merged[packageName];
97
+ if (existing === void 0) merged[packageName] = allowed;
98
+ else if (existing === true || allowed === true) merged[packageName] = true;
99
+ else merged[packageName] = [...existing, ...allowed];
100
+ }
101
+ return new AllowedPackages(merged);
102
+ }
103
+ };
104
+ //#endregion
105
+ //#region src/model/classes/class-declaration.ts
106
+ var ClassDeclaration = class {
107
+ name;
108
+ line;
109
+ isAbstract;
110
+ type;
111
+ heritage;
112
+ implemented;
113
+ members;
114
+ constructor(props) {
115
+ this.name = props.name;
116
+ this.line = props.line;
117
+ this.isAbstract = props.isAbstract;
118
+ this.type = props.type;
119
+ this.heritage = props.heritage;
120
+ this.implemented = props.implemented;
121
+ this.members = props.members;
122
+ }
123
+ get heldMembers() {
124
+ return this.members.filter((member) => member.kind === "field" || member.kind === "constructor parameter");
125
+ }
126
+ get publicSurface() {
127
+ return this.members.filter((member) => member.isPublicInstance && member.kind !== "setter" && this.isDeclared(member));
128
+ }
129
+ get typeArguments() {
130
+ return this.heritage?.typeArguments ?? [];
131
+ }
132
+ get extendsByName() {
133
+ return this.heritage === void 0 || this.heritage.isByName;
134
+ }
135
+ get isStaticOnly() {
136
+ const members = this.members.filter((member) => this.isDeclared(member));
137
+ return members.length > 0 && members.every((member) => member.isStatic);
138
+ }
139
+ isDeclared(member) {
140
+ return member.kind !== "constructor parameter" || member.isParameterProperty;
141
+ }
142
+ };
143
+ //#endregion
144
+ //#region src/model/classes/class-type.ts
145
+ var ClassType = class {
146
+ name;
147
+ declaredIn;
148
+ lineage;
149
+ constructor(name, declaredIn, lineage) {
150
+ this.name = name;
151
+ this.declaredIn = declaredIn;
152
+ this.lineage = lineage;
153
+ }
154
+ get key() {
155
+ return `${this.declaredIn ?? ""}#${this.name}`;
156
+ }
157
+ get isInstalled() {
158
+ return (this.declaredIn ?? "").split(/[\\/]/).includes("node_modules");
159
+ }
160
+ };
161
+ //#endregion
162
+ //#region src/model/classes/member.ts
163
+ var Member = class {
164
+ name;
165
+ line;
166
+ kind;
167
+ visibility;
168
+ isStatic;
169
+ isParameterProperty;
170
+ isCallable;
171
+ valueTypes;
172
+ parameterTypes;
173
+ returns;
174
+ constructor(props) {
175
+ this.name = props.name;
176
+ this.line = props.line;
177
+ this.kind = props.kind;
178
+ this.visibility = props.visibility;
179
+ this.isStatic = props.isStatic;
180
+ this.isParameterProperty = props.isParameterProperty;
181
+ this.isCallable = props.isCallable;
182
+ this.valueTypes = props.valueTypes;
183
+ this.parameterTypes = props.parameterTypes;
184
+ this.returns = props.returns;
185
+ }
186
+ get isPublicInstance() {
187
+ return this.visibility === "public" && !this.isStatic;
188
+ }
189
+ get isField() {
190
+ return this.kind === "field" || this.isParameterProperty;
191
+ }
192
+ };
193
+ //#endregion
194
+ //#region src/model/dependencies/dependency.ts
195
+ var Dependency = class {
196
+ line;
197
+ form;
198
+ specifier;
199
+ names;
200
+ target;
201
+ constructor(line, form, specifier, names, target) {
202
+ this.line = line;
203
+ this.form = form;
204
+ this.specifier = specifier;
205
+ this.names = names;
206
+ this.target = target;
207
+ }
208
+ get label() {
209
+ return this.names.length === 0 ? this.specifier : this.names.join(", ");
210
+ }
211
+ isOnPackage(name) {
212
+ return this.target.kind === "package" && this.target.name === name;
213
+ }
214
+ };
215
+ //#endregion
216
+ //#region src/model/dependencies/global-use.ts
217
+ var GlobalUse = class {
218
+ line;
219
+ name;
220
+ origin;
221
+ effect;
222
+ constructor(line, name, origin, effect) {
223
+ this.line = line;
224
+ this.name = name;
225
+ this.origin = origin;
226
+ this.effect = effect;
227
+ }
228
+ };
229
+ //#endregion
230
+ //#region src/model/project.ts
231
+ var Project$1 = class {
232
+ directory;
233
+ files;
234
+ filesByPath;
235
+ constructor(directory, files) {
236
+ this.directory = directory;
237
+ this.files = files;
238
+ this.filesByPath = new Map(files.map((file) => [file.path, file]));
239
+ }
240
+ file(path) {
241
+ return this.filesByPath.get(path);
242
+ }
243
+ classOf(type) {
244
+ if (type.declaredIn === void 0) return;
245
+ return this.file(type.declaredIn)?.classNamed(type.name);
246
+ }
247
+ relativePath(path) {
248
+ return relative(this.directory, path).split(sep).join("/");
249
+ }
250
+ };
251
+ //#endregion
252
+ //#region src/model/source-file.ts
253
+ var SourceFile = class {
254
+ path;
255
+ dependencies;
256
+ classes;
257
+ statements;
258
+ throws;
259
+ globals;
260
+ disables;
261
+ lines;
262
+ constructor(props) {
263
+ this.path = props.path;
264
+ this.dependencies = props.dependencies;
265
+ this.classes = props.classes;
266
+ this.statements = props.statements;
267
+ this.throws = props.throws;
268
+ this.globals = props.globals;
269
+ this.disables = props.disables;
270
+ this.lines = props.text.split(/\r?\n/);
271
+ }
272
+ lineText(line) {
273
+ return this.lines[line - 1] ?? "";
274
+ }
275
+ disableAbove(line) {
276
+ return this.disables.find((comment) => comment.target === line);
277
+ }
278
+ classNamed(name) {
279
+ return this.classes.find((candidate) => candidate.name === name);
280
+ }
281
+ };
282
+ //#endregion
283
+ //#region src/model/statements/disable-comment.ts
284
+ var DisableComment = class {
285
+ line;
286
+ text;
287
+ constructor(line, text) {
288
+ this.line = line;
289
+ this.text = text;
290
+ }
291
+ get target() {
292
+ return this.line + 1;
293
+ }
294
+ };
295
+ //#endregion
296
+ //#region src/model/statements/throw.ts
297
+ var Throw = class {
298
+ line;
299
+ form;
300
+ constructor(line, form) {
301
+ this.line = line;
302
+ this.form = form;
303
+ }
304
+ };
305
+ //#endregion
306
+ //#region src/model/statements/top-level-statement.ts
307
+ var TopLevelStatement = class {
308
+ kind;
309
+ name;
310
+ line;
311
+ constructor(kind, name, line) {
312
+ this.kind = kind;
313
+ this.name = name;
314
+ this.line = line;
315
+ }
316
+ };
317
+ //#endregion
318
+ //#region src/conventions/building-blocks.ts
319
+ const buildingBlockPlaces = [
320
+ {
321
+ folder: "aggregates",
322
+ kind: "AggregateRoot",
323
+ layer: "domain",
324
+ suffix: ".aggregate.ts"
325
+ },
326
+ {
327
+ folder: "entities",
328
+ kind: "Entity",
329
+ layer: "domain",
330
+ suffix: ".entity.ts"
331
+ },
332
+ {
333
+ folder: "value-objects",
334
+ kind: "Identifier",
335
+ layer: "domain",
336
+ suffix: ".identifier.ts"
337
+ },
338
+ {
339
+ folder: "value-objects",
340
+ kind: "ValueObject",
341
+ layer: "domain",
342
+ suffix: ".value-object.ts"
343
+ },
344
+ {
345
+ folder: "events",
346
+ kind: "DomainEvent",
347
+ layer: "domain",
348
+ suffix: ".event.ts"
349
+ },
350
+ {
351
+ folder: "errors",
352
+ kind: "DomainError",
353
+ layer: "domain",
354
+ suffix: ".error.ts"
355
+ },
356
+ {
357
+ folder: "services",
358
+ kind: "DomainService",
359
+ layer: "domain",
360
+ suffix: ".service.ts"
361
+ },
362
+ {
363
+ folder: "commands",
364
+ kind: "CommandHandler",
365
+ layer: "application",
366
+ suffix: ".command.ts"
367
+ },
368
+ {
369
+ folder: "queries",
370
+ kind: "QueryHandler",
371
+ layer: "application",
372
+ suffix: ".query.ts"
373
+ },
374
+ {
375
+ folder: "translators",
376
+ kind: "EventTranslator",
377
+ layer: "application",
378
+ suffix: ".translator.ts"
379
+ },
380
+ {
381
+ concreteOnly: true,
382
+ folder: "adapters",
383
+ kind: "Port",
384
+ layer: "driven",
385
+ suffix: ".adapter.ts"
386
+ },
387
+ {
388
+ folder: "repositories",
389
+ kind: "CommandRepository",
390
+ layer: "domain",
391
+ suffix: ".repository.ts"
392
+ },
393
+ {
394
+ folder: "repositories",
395
+ kind: "QueryRepository",
396
+ layer: "domain",
397
+ suffix: ".repository.ts"
398
+ },
399
+ {
400
+ folder: "ports",
401
+ kind: "Port",
402
+ layer: "domain",
403
+ suffix: ".port.ts"
404
+ }
405
+ ];
406
+ const markerPlaces = [{
407
+ folder: "adapters",
408
+ layer: "driven",
409
+ marker: "AntiCorruptionLayer",
410
+ suffix: ".adapter.ts"
411
+ }, {
412
+ layer: "driving",
413
+ marker: "OpenHostService"
414
+ }];
415
+ const extraDomainFolders = ["views"];
416
+ //#endregion
417
+ //#region src/conventions/core-api.ts
418
+ const corePackageName = "@alveolus/core";
419
+ const coreKinds = [
420
+ "AggregateRoot",
421
+ "Entity",
422
+ "ValueObject",
423
+ "Identifier",
424
+ "DomainEvent",
425
+ "DomainError",
426
+ "DomainService",
427
+ "Port",
428
+ "CommandRepository",
429
+ "QueryRepository",
430
+ "CommandHandler",
431
+ "QueryHandler",
432
+ "EventTranslator",
433
+ "EventPublisher",
434
+ "Outbox",
435
+ "UnitOfWork"
436
+ ];
437
+ const coreMarkers = ["OpenHostService", "AntiCorruptionLayer"];
438
+ const coreDomainSymbols = [
439
+ "AggregateRoot",
440
+ "AnyAggregateRoot",
441
+ "AnyDomainError",
442
+ "AnyDomainEvent",
443
+ "AnyEntity",
444
+ "AnyIdentifier",
445
+ "AnySnapshot",
446
+ "AnyValueObject",
447
+ "Clock",
448
+ "CommandRepository",
449
+ "DomainError",
450
+ "DomainEvent",
451
+ "DomainEventProps",
452
+ "DomainService",
453
+ "Entity",
454
+ "Err",
455
+ "IdGenerator",
456
+ "Identifier",
457
+ "IdentifierValue",
458
+ "Ok",
459
+ "Port",
460
+ "QueryRepository",
461
+ "Result",
462
+ "SnapshotValue",
463
+ "ValueObject",
464
+ "View",
465
+ "andThen",
466
+ "combine",
467
+ "err",
468
+ "map",
469
+ "mapErr",
470
+ "ok"
471
+ ];
472
+ const corePublishedLanguageSymbols = [
473
+ "AnyIntegrationEvent",
474
+ "IntegrationEvent",
475
+ "JsonValue",
476
+ "PublishedLanguage"
477
+ ];
478
+ //#endregion
479
+ //#region src/conventions/layers.ts
480
+ const layers = [
481
+ "domain",
482
+ "application",
483
+ "published-language",
484
+ "driven",
485
+ "driving"
486
+ ];
487
+ const layerImports = {
488
+ application: [
489
+ "domain",
490
+ "application",
491
+ "published-language"
492
+ ],
493
+ driven: [
494
+ "domain",
495
+ "application",
496
+ "published-language",
497
+ "driven"
498
+ ],
499
+ driving: [
500
+ "domain",
501
+ "application",
502
+ "published-language",
503
+ "driving"
504
+ ],
505
+ "published-language": ["published-language"]
506
+ };
507
+ //#endregion
508
+ //#region src/architecture/paths/place.ts
509
+ var Place = class {
510
+ layer;
511
+ folder;
512
+ suffix;
513
+ constructor(spec) {
514
+ this.layer = spec.layer;
515
+ this.folder = spec.folder;
516
+ this.suffix = spec.suffix;
517
+ }
518
+ fits(location) {
519
+ if (!location.isInBoundedContext && !location.isInSharedKernel) return false;
520
+ if (location.layer !== this.layer) return false;
521
+ if (this.folder !== void 0 && location.folder !== this.folder) return false;
522
+ return this.suffix === void 0 || location.fileName.endsWith(this.suffix);
523
+ }
524
+ };
525
+ //#endregion
526
+ //#region src/architecture/building-blocks.ts
527
+ var BuildingBlocks = class {
528
+ core;
529
+ constructor(core) {
530
+ this.core = core;
531
+ }
532
+ placeOf(declaration) {
533
+ const kinds = this.core.kindsOf(declaration.type);
534
+ for (const entry of buildingBlockPlaces) if (kinds.includes(entry.kind) && !(entry.concreteOnly === true && declaration.isAbstract)) return new Place(entry);
535
+ }
536
+ markedPlacesOf(declaration) {
537
+ const markers = this.core.markersOf(declaration.implemented);
538
+ const places = [];
539
+ for (const entry of markerPlaces) if (markers.includes(entry.marker)) places.push({
540
+ marker: entry.marker,
541
+ place: new Place(entry)
542
+ });
543
+ return places;
544
+ }
545
+ foldersOf(layer) {
546
+ const folders = [];
547
+ for (const entry of buildingBlockPlaces) if (entry.layer === layer && entry.folder !== void 0 && !folders.includes(entry.folder)) folders.push(entry.folder);
548
+ if (layer === "domain") folders.push(...extraDomainFolders);
549
+ return folders;
550
+ }
551
+ };
552
+ //#endregion
553
+ //#region src/architecture/core-api.ts
554
+ var CoreApi = class {
555
+ get packageName() {
556
+ return corePackageName;
557
+ }
558
+ kindsOf(type) {
559
+ const kinds = [];
560
+ for (const ancestor of type.lineage) if (this.isCore(ancestor) && this.isKind(ancestor.name)) kinds.push(ancestor.name);
561
+ return kinds;
562
+ }
563
+ markersOf(implemented) {
564
+ const markers = [];
565
+ for (const type of implemented) if (this.isCore(type) && this.isMarker(type.name)) markers.push(type.name);
566
+ return markers;
567
+ }
568
+ isResult(shape) {
569
+ if (shape.alias !== void 0 && shape.alias.name === "Result" && this.isCore(shape.alias)) return true;
570
+ return shape.union.every((member) => (member.name === "Ok" || member.name === "Err") && this.isCore(member));
571
+ }
572
+ isDomainSymbol(name) {
573
+ return coreDomainSymbols.includes(name);
574
+ }
575
+ isPublishedLanguageSymbol(name) {
576
+ return corePublishedLanguageSymbols.includes(name);
577
+ }
578
+ isKind(name) {
579
+ return coreKinds.includes(name);
580
+ }
581
+ isMarker(name) {
582
+ return coreMarkers.includes(name);
583
+ }
584
+ isCore(type) {
585
+ return type.packageName === corePackageName;
586
+ }
587
+ };
588
+ //#endregion
589
+ //#region src/architecture/layers.ts
590
+ var Layers = class {
591
+ isLayer(name) {
592
+ return layers.includes(name);
593
+ }
594
+ importableFrom(layer) {
595
+ return layerImports[layer];
596
+ }
597
+ };
598
+ //#endregion
599
+ //#region src/architecture/paths/layer-shape.ts
600
+ var LayerShape = class {
601
+ blocks;
602
+ extraFolders;
603
+ constructor(blocks, extraFolders) {
604
+ this.blocks = blocks;
605
+ this.extraFolders = extraFolders;
606
+ }
607
+ issueWith(location) {
608
+ const layer = location.layer;
609
+ const folders = location.foldersInLayer;
610
+ if (layer === "domain" || layer === "application") return this.kindFolderIssue(layer, folders);
611
+ if (layer === "published-language" && folders.length > 0) return { kind: "nestedPublishedLanguage" };
612
+ if (layer === "driven" && folders.length < 2) return { kind: "drivenWithoutTechnology" };
613
+ if (layer === "driven" && folders.length > 2) return { kind: "drivenTooDeep" };
614
+ if (layer === "driving" && folders.length === 0) return { kind: "drivingWithoutTechnology" };
615
+ }
616
+ kindFolderIssue(layer, folders) {
617
+ const expected = [...this.blocks.foldersOf(layer), ...this.extraFolders[layer] ?? []];
618
+ const example = `${layer}/${expected[0]}/`;
619
+ if (folders.length === 0) return {
620
+ example,
621
+ kind: "directlyInLayer",
622
+ layer
623
+ };
624
+ if (folders.length > 1) return {
625
+ example,
626
+ kind: "tooDeep",
627
+ layer
628
+ };
629
+ const [folder] = folders;
630
+ if (folder !== void 0 && !expected.includes(folder)) return {
631
+ folder,
632
+ folders: expected,
633
+ kind: "unknownFolder",
634
+ layer
635
+ };
636
+ }
637
+ };
638
+ //#endregion
639
+ //#region src/architecture/paths/location.ts
640
+ var Location = class {
641
+ area;
642
+ context;
643
+ layer;
644
+ folder;
645
+ foldersInLayer;
646
+ fileName;
647
+ isCompositionRoot;
648
+ unseen;
649
+ constructor(props) {
650
+ this.area = props.area;
651
+ this.context = props.context;
652
+ this.layer = props.layer;
653
+ this.folder = props.folder;
654
+ this.foldersInLayer = props.foldersInLayer ?? [];
655
+ this.fileName = props.fileName;
656
+ this.isCompositionRoot = props.isCompositionRoot ?? false;
657
+ this.unseen = props.unseen;
658
+ }
659
+ get isInBoundedContext() {
660
+ return this.area === "context";
661
+ }
662
+ get isInSharedKernel() {
663
+ return this.area === "shared-kernel";
664
+ }
665
+ get isAtRoot() {
666
+ return this.area === "root";
667
+ }
668
+ get isOutside() {
669
+ return this.area === "outside";
670
+ }
671
+ get isInLayer() {
672
+ return this.layer !== void 0;
673
+ }
674
+ isSameContextAs(other) {
675
+ return this.context !== void 0 && this.context === other.context;
676
+ }
677
+ isOtherBoundedContextThan(other) {
678
+ return this.isInBoundedContext && !this.isSameContextAs(other);
679
+ }
680
+ };
681
+ //#endregion
682
+ //#region src/architecture/paths/layout.ts
683
+ var Layout = class {
684
+ settings;
685
+ constructor(settings) {
686
+ this.settings = settings;
687
+ }
688
+ locate(path) {
689
+ const file = resolve(path);
690
+ const fileName = basename(file);
691
+ const folder = this.contextFolderOf(file);
692
+ if (folder === void 0) return new Location({
693
+ area: dirname(file) === this.settings.rootDir ? "root" : "outside",
694
+ fileName
695
+ });
696
+ const area = folder.isSharedKernel ? "shared-kernel" : "context";
697
+ const context = folder.name;
698
+ const directories = this.directoriesBetween(folder.dir, file);
699
+ if (directories.length === 0) return new Location({
700
+ area,
701
+ context,
702
+ fileName,
703
+ isCompositionRoot: this.isCompositionRoot(fileName)
704
+ });
705
+ const layerIndex = this.layerIndexOf(directories, folder.isSharedKernel);
706
+ if (layerIndex === void 0) return new Location({
707
+ area,
708
+ context,
709
+ fileName,
710
+ isCompositionRoot: folder.isSharedKernel && directories.length === 1 && this.isCompositionRoot(fileName)
711
+ });
712
+ const layer = directories[layerIndex];
713
+ if (layer === void 0 || !this.isLayer(layer)) return new Location({
714
+ area,
715
+ context,
716
+ fileName
717
+ });
718
+ const foldersInLayer = directories.slice(layerIndex + 1);
719
+ const subfolder = foldersInLayer.at(-1);
720
+ return new Location({
721
+ area,
722
+ context,
723
+ fileName,
724
+ foldersInLayer,
725
+ layer,
726
+ ...subfolder === void 0 ? {} : { folder: subfolder }
727
+ });
728
+ }
729
+ layerIndexOf(directories, isSharedKernel) {
730
+ if (this.isLayer(directories[0] ?? "")) return 0;
731
+ if (isSharedKernel && this.isLayer(directories[1] ?? "")) return 1;
732
+ }
733
+ isLayer(name) {
734
+ return layers.includes(name);
735
+ }
736
+ isCompositionRoot(fileName) {
737
+ return matchesGlob(fileName, this.settings.compositionRoot);
738
+ }
739
+ contextFolderOf(path) {
740
+ return this.settings.contextFolders.filter((folder) => this.isInside(folder.dir, path)).sort((left, right) => right.dir.length - left.dir.length)[0];
741
+ }
742
+ directoriesBetween(dir, path) {
743
+ const between = relative(dir, dirname(path));
744
+ return between === "" ? [] : between.split(sep);
745
+ }
746
+ isInside(dir, path) {
747
+ const offset = relative(dir, path);
748
+ return offset !== "" && !offset.startsWith("..") && !isAbsolute(offset);
749
+ }
750
+ };
751
+ //#endregion
752
+ //#region src/architecture/architecture.ts
753
+ var Architecture = class {
754
+ project;
755
+ settings;
756
+ core = new CoreApi();
757
+ layers = new Layers();
758
+ blocks = new BuildingBlocks(this.core);
759
+ shape;
760
+ layout;
761
+ locations = /* @__PURE__ */ new Map();
762
+ constructor(project, settings) {
763
+ this.project = project;
764
+ this.settings = settings;
765
+ this.layout = new Layout(settings);
766
+ this.shape = new LayerShape(this.blocks, settings.extraFolders);
767
+ }
768
+ get files() {
769
+ return this.project.files;
770
+ }
771
+ get domainDependencies() {
772
+ return this.settings.domainDependencies;
773
+ }
774
+ get applicationDependencies() {
775
+ return this.settings.applicationDependencies;
776
+ }
777
+ get contextMap() {
778
+ return this.settings.contextMap;
779
+ }
780
+ locationOf(file) {
781
+ return this.locate(file.path);
782
+ }
783
+ locationOfTarget(target) {
784
+ if (target.visibility !== "analysed") return new Location({
785
+ area: "outside",
786
+ fileName: basename(target.path),
787
+ unseen: target.visibility
788
+ });
789
+ return this.locate(target.path);
790
+ }
791
+ shapeIssueOf(location) {
792
+ return this.shape.issueWith(location);
793
+ }
794
+ relativePath(path) {
795
+ return this.project.relativePath(path);
796
+ }
797
+ kindsOf(subject) {
798
+ return this.core.kindsOf(this.typeOf(subject));
799
+ }
800
+ kindOf(subject) {
801
+ return this.kindsOf(subject)[0];
802
+ }
803
+ is(subject, kind) {
804
+ return this.kindsOf(subject).includes(kind);
805
+ }
806
+ isBuildingBlock(subject) {
807
+ return this.kindsOf(subject).length > 0;
808
+ }
809
+ implementsMarker(declaration, marker) {
810
+ return this.core.markersOf(declaration.implemented).includes(marker);
811
+ }
812
+ returnsResult(member) {
813
+ return member.returns !== void 0 && this.core.isResult(member.returns);
814
+ }
815
+ classOf(type) {
816
+ return this.project.classOf(type);
817
+ }
818
+ locate(path) {
819
+ let location = this.locations.get(path);
820
+ if (location === void 0) {
821
+ location = this.layout.locate(path);
822
+ this.locations.set(path, location);
823
+ }
824
+ return location;
825
+ }
826
+ typeOf(subject) {
827
+ return subject instanceof ClassDeclaration ? subject.type : subject;
828
+ }
829
+ };
830
+ //#endregion
831
+ //#region src/architecture/context-map.ts
832
+ var ContextMap = class ContextMap {
833
+ upstreams;
834
+ constructor(upstreams) {
835
+ this.upstreams = upstreams;
836
+ }
837
+ static ofEdges(edges) {
838
+ const upstreams = {};
839
+ for (const { from, to } of edges) {
840
+ const known = upstreams[from] ?? [];
841
+ if (!known.includes(to)) upstreams[from] = [...known, to];
842
+ }
843
+ return new ContextMap(upstreams);
844
+ }
845
+ get contexts() {
846
+ const names = new Set(Object.keys(this.upstreams));
847
+ for (const targets of Object.values(this.upstreams)) for (const target of targets) names.add(target);
848
+ return [...names];
849
+ }
850
+ allows(from, to) {
851
+ return (this.upstreams[from] ?? []).includes(to);
852
+ }
853
+ cycle() {
854
+ const visiting = /* @__PURE__ */ new Set();
855
+ const done = /* @__PURE__ */ new Set();
856
+ for (const start of this.contexts) {
857
+ const found = this.cycleFrom(start, [], visiting, done);
858
+ if (found !== void 0) return found;
859
+ }
860
+ }
861
+ cycleThrough(from, to) {
862
+ return this.reaches(to, from, /* @__PURE__ */ new Set());
863
+ }
864
+ cycleFrom(context, path, visiting, done) {
865
+ if (done.has(context)) return;
866
+ if (visiting.has(context)) return [...path.slice(path.indexOf(context)), context];
867
+ visiting.add(context);
868
+ for (const upstream of this.upstreams[context] ?? []) {
869
+ const found = this.cycleFrom(upstream, [...path, context], visiting, done);
870
+ if (found !== void 0) return found;
871
+ }
872
+ visiting.delete(context);
873
+ done.add(context);
874
+ }
875
+ reaches(from, target, seen) {
876
+ if (from === target) return true;
877
+ if (seen.has(from)) return false;
878
+ seen.add(from);
879
+ return (this.upstreams[from] ?? []).some((upstream) => this.reaches(upstream, target, seen));
880
+ }
881
+ };
882
+ //#endregion
883
+ //#region src/rules/framework/wording.ts
884
+ var Wording = class {
885
+ target(target, architecture) {
886
+ return `${architecture.relativePath(target.path)} (${this.location(architecture.locationOfTarget(target))})`;
887
+ }
888
+ location(location) {
889
+ if (location.unseen === "ignored") return "ignored by the analysis";
890
+ if (location.unseen === "unresolved") return "not resolved by the analysis";
891
+ if (location.isOutside) return "outside the declared bounded contexts and shared kernel";
892
+ if (location.isAtRoot) return "at the root";
893
+ const role = location.isCompositionRoot ? "composition root" : location.layer ?? "outside the layers";
894
+ return `${location.context} ${role}`;
895
+ }
896
+ place(place) {
897
+ return place.folder === void 0 ? `${place.layer}/` : `${place.layer}/${place.folder}/*${place.suffix ?? ".ts"}`;
898
+ }
899
+ };
900
+ //#endregion
901
+ //#region src/rules/framework/rule.ts
902
+ var Rule = class {
903
+ wording = new Wording();
904
+ finding(file, line, symbol, messageId, data = {}) {
905
+ return {
906
+ data,
907
+ file,
908
+ line,
909
+ messageId,
910
+ symbol
911
+ };
912
+ }
913
+ };
914
+ //#endregion
915
+ //#region src/rules/framework/class-rule.ts
916
+ var ClassRule = class extends Rule {
917
+ check(architecture) {
918
+ const findings = [];
919
+ for (const file of architecture.files) for (const codeClass of file.classes) findings.push(...this.findingsFor(codeClass, file, architecture));
920
+ return findings;
921
+ }
922
+ };
923
+ //#endregion
924
+ //#region src/rules/framework/import-rule.ts
925
+ var ImportRule = class extends Rule {
926
+ check(architecture) {
927
+ const findings = [];
928
+ for (const file of architecture.files) {
929
+ if (!this.appliesTo(file, architecture)) continue;
930
+ for (const dependency of file.dependencies) {
931
+ const finding = this.findingFor(dependency, file, architecture);
932
+ if (finding !== void 0) findings.push(finding);
933
+ }
934
+ }
935
+ return findings;
936
+ }
937
+ };
938
+ //#endregion
939
+ //#region src/rules/framework/injection-rule.ts
940
+ var InjectionRule = class extends ClassRule {
941
+ findingsFor(codeClass, file, architecture) {
942
+ if (!architecture.is(codeClass, this.holder)) return [];
943
+ const findings = [];
944
+ for (const member of codeClass.heldMembers) for (const type of member.valueTypes) if (!this.accepts(type, architecture)) {
945
+ const data = {
946
+ class: codeClass.name,
947
+ kind: this.describeKind(type, architecture),
948
+ type: type.name
949
+ };
950
+ findings.push(this.finding(file, member.line, `${codeClass.name}.${member.name}`, "foreign", data));
951
+ }
952
+ return findings;
953
+ }
954
+ accepts(type, architecture) {
955
+ if (this.forbidden.some((kind) => architecture.is(type, kind))) return false;
956
+ if (this.allowed.some((kind) => architecture.is(type, kind))) return true;
957
+ return type.isInstalled && !architecture.isBuildingBlock(type);
958
+ }
959
+ describeKind(type, architecture) {
960
+ const kind = architecture.kindOf(type);
961
+ if (kind === void 0) return "a class that extends no building block";
962
+ return `${/^[AEIO]/.test(kind) ? "an" : "a"} ${kind}`;
963
+ }
964
+ };
965
+ //#endregion
966
+ //#region src/rules/layers/no-driving-shortcut.rule.ts
967
+ const shortcuts = [
968
+ "CommandRepository",
969
+ "QueryRepository",
970
+ "Port",
971
+ "AggregateRoot",
972
+ "Entity",
973
+ "DomainService"
974
+ ];
975
+ var NoDrivingShortcutRule = class extends ImportRule {
976
+ meta = {
977
+ description: "A driving adapter importing a port, a repository, an aggregate, an entity or a domain service instead of calling a handler.",
978
+ id: "layers/no-driving-shortcut",
979
+ messages: { shortcut: "Imports {name}, {kind}: a driving adapter calls the command and query handlers, never the ports, repositories or aggregates of the domain." }
980
+ };
981
+ appliesTo(file, architecture) {
982
+ return architecture.locationOf(file).layer === "driving";
983
+ }
984
+ findingFor(dependency, file, architecture) {
985
+ if (dependency.target.kind !== "file") return;
986
+ const target = architecture.project.file(dependency.target.path);
987
+ if (target === void 0) return;
988
+ for (const codeClass of this.importedClasses(dependency, target)) {
989
+ const kind = this.shortcutKindOf(codeClass, architecture);
990
+ if (kind !== void 0) {
991
+ const article = /^[AEIO]/.test(kind) ? "an" : "a";
992
+ return this.finding(file, dependency.line, codeClass.name, "shortcut", {
993
+ kind: `${article} ${kind}`,
994
+ name: codeClass.name
995
+ });
996
+ }
997
+ }
998
+ }
999
+ importedClasses(dependency, target) {
1000
+ if (dependency.names.includes("*")) return [...target.classes];
1001
+ const classes = [];
1002
+ for (const name of dependency.names) {
1003
+ const codeClass = target.classNamed(name);
1004
+ if (codeClass !== void 0) classes.push(codeClass);
1005
+ }
1006
+ return classes;
1007
+ }
1008
+ shortcutKindOf(codeClass, architecture) {
1009
+ return architecture.kindsOf(codeClass).find((kind) => shortcuts.includes(kind));
1010
+ }
1011
+ };
1012
+ //#endregion
1013
+ //#region src/rules/layers/no-impure-domain.rule.ts
1014
+ var NoImpureDomainRule = class extends ImportRule {
1015
+ meta = {
1016
+ description: "The domain importing a framework, a database, another layer or a package not allowed, or using the host, the clock or randomness.",
1017
+ id: "layers/no-impure-domain",
1018
+ messages: {
1019
+ clock: "The domain reads the system clock with {name}: receive the time from the Clock port.",
1020
+ coreSymbol: "The domain imports {names} from @alveolus/core: only domain building blocks and Result are allowed.",
1021
+ hostGlobal: "The domain uses {name}, a global of the host: reach it through a port.",
1022
+ otherLayer: "The domain imports {target}: it may only import the domain.",
1023
+ packageName: "The domain imports {names} from {package}: domainDependencies only allows {allowed}.",
1024
+ randomness: "The domain draws a random value with {name}: receive it from a port, such as IdGenerator.",
1025
+ undeclaredPackage: "The domain imports {package}: add it to domainDependencies if the domain really needs it."
1026
+ }
1027
+ };
1028
+ check(architecture) {
1029
+ const findings = super.check(architecture);
1030
+ for (const file of architecture.files) if (this.appliesTo(file, architecture)) findings.push(...this.impureGlobals(file));
1031
+ return findings;
1032
+ }
1033
+ appliesTo(file, architecture) {
1034
+ return architecture.locationOf(file).layer === "domain";
1035
+ }
1036
+ findingFor(dependency, file, architecture) {
1037
+ const { line, label } = dependency;
1038
+ const target = dependency.target;
1039
+ if (target.kind === "file") {
1040
+ if (this.isDomainReachable(architecture.locationOfTarget(target), architecture.locationOf(file))) return;
1041
+ return this.finding(file, line, label, "otherLayer", { target: this.wording.target(target, architecture) });
1042
+ }
1043
+ if (target.name === architecture.core.packageName) {
1044
+ const forbidden = dependency.names.filter((name) => !architecture.core.isDomainSymbol(name));
1045
+ return forbidden.length === 0 ? void 0 : this.finding(file, line, label, "coreSymbol", { names: forbidden.join(", ") });
1046
+ }
1047
+ const allowed = architecture.domainDependencies;
1048
+ if (!allowed.has(target.name)) return this.finding(file, line, label, "undeclaredPackage", { package: target.name });
1049
+ const forbidden = allowed.forbiddenNames(target.name, dependency.names);
1050
+ if (forbidden.length === 0) return;
1051
+ return this.finding(file, line, label, "packageName", {
1052
+ allowed: allowed.allowedNames(target.name).join(", "),
1053
+ names: forbidden.join(", "),
1054
+ package: target.name
1055
+ });
1056
+ }
1057
+ impureGlobals(file) {
1058
+ const findings = [];
1059
+ for (const use of file.globals) {
1060
+ const messageId = this.messageFor(use);
1061
+ if (messageId !== void 0) findings.push(this.finding(file, use.line, use.name, messageId, { name: use.name }));
1062
+ }
1063
+ return findings;
1064
+ }
1065
+ messageFor(use) {
1066
+ if (use.origin === "host") return "hostGlobal";
1067
+ return use.effect;
1068
+ }
1069
+ isDomainReachable(to, from) {
1070
+ return to.layer === "domain" && (to.isSameContextAs(from) || to.isInSharedKernel);
1071
+ }
1072
+ };
1073
+ //#endregion
1074
+ //#region src/rules/layers/no-outward-import.rule.ts
1075
+ var NoOutwardImportRule = class extends ImportRule {
1076
+ meta = {
1077
+ description: "A dependency pointing away from the domain, a file outside the layers or in the wrong folder of its layer.",
1078
+ id: "layers/no-outward-import",
1079
+ messages: {
1080
+ compositionRoots: "{context} has {count} composition roots ({names}): keep one, and move the rest into the layers.",
1081
+ directlyInLayer: "The file sits directly in {layer}/: put it in the folder of its kind, such as {example}.",
1082
+ drivenTooDeep: "The file is nested too deep: driven/ holds driven/<technology>/<folder>/, such as driven/pg/adapters/.",
1083
+ drivenWithoutTechnology: "The file is not under a technology: driven/ holds driven/<technology>/<folder>/, such as driven/pg/adapters/.",
1084
+ drivingWithoutTechnology: "The file is not under a technology: driving/ holds driving/<technology>/, such as driving/http/.",
1085
+ nestedPublishedLanguage: "The file is nested in published-language/: the published language holds its files directly.",
1086
+ outsideContexts: "The file is outside the bounded contexts and the shared kernel declared in alveolus.config.ts: move it, or add it to ignore.",
1087
+ outsideLayers: "The file is outside the layers: move it to domain/, application/, published-language/, driven/ or driving/.",
1088
+ outwardLayer: "The {layer} layer imports {target}: it may only import {allowed}.",
1089
+ packageName: "The application imports {names} from {package}: applicationDependencies only allows {allowed}.",
1090
+ publishedLanguageCore: "The published language imports {names} from @alveolus/core: only published-language types are allowed.",
1091
+ rootImport: "Files at the root import composition roots only, not {target}.",
1092
+ tooDeep: "The file is nested too deep: {layer}/ holds one folder per kind, such as {example}.",
1093
+ undeclaredPackage: "The application imports {package}: add it to applicationDependencies if the application really needs it.",
1094
+ unknownFolder: "{layer}/{folder}/ is no folder of the {layer}: use {folders}.",
1095
+ wiring: "Imports {target}: only the composition root wires the layers."
1096
+ }
1097
+ };
1098
+ check(architecture) {
1099
+ return [
1100
+ ...this.misplacedFiles(architecture),
1101
+ ...this.extraCompositionRoots(architecture),
1102
+ ...super.check(architecture)
1103
+ ];
1104
+ }
1105
+ appliesTo(file, architecture) {
1106
+ const location = architecture.locationOf(file);
1107
+ return location.isAtRoot || location.isCompositionRoot || location.isInLayer && location.layer !== "domain";
1108
+ }
1109
+ findingFor(dependency, file, architecture) {
1110
+ const from = architecture.locationOf(file);
1111
+ const target = dependency.target;
1112
+ if (target.kind === "file") {
1113
+ const problem = this.layerProblem(from, architecture.locationOfTarget(target), architecture);
1114
+ if (problem === void 0) return;
1115
+ return this.finding(file, dependency.line, dependency.label, problem.messageId, {
1116
+ ...problem.data,
1117
+ target: this.wording.target(target, architecture)
1118
+ });
1119
+ }
1120
+ if (from.layer === "application") return this.applicationPackageFinding(dependency, target.name, file, architecture);
1121
+ if (from.layer === "published-language" && target.name === architecture.core.packageName) {
1122
+ const forbidden = dependency.names.filter((name) => !architecture.core.isPublishedLanguageSymbol(name));
1123
+ return forbidden.length === 0 ? void 0 : this.finding(file, dependency.line, dependency.label, "publishedLanguageCore", { names: forbidden.join(", ") });
1124
+ }
1125
+ }
1126
+ misplacedFiles(architecture) {
1127
+ const findings = [];
1128
+ for (const file of architecture.files) {
1129
+ const location = architecture.locationOf(file);
1130
+ if (location.isOutside) findings.push(this.finding(file, 1, location.fileName, "outsideContexts"));
1131
+ else if (!location.isAtRoot && !location.isInLayer && !location.isCompositionRoot) findings.push(this.finding(file, 1, location.fileName, "outsideLayers"));
1132
+ else if (location.isInLayer) {
1133
+ const issue = architecture.shapeIssueOf(location);
1134
+ if (issue !== void 0) findings.push(this.finding(file, 1, location.fileName, issue.kind, this.shapeData(issue)));
1135
+ }
1136
+ }
1137
+ return findings;
1138
+ }
1139
+ shapeData(issue) {
1140
+ if (issue.kind === "unknownFolder") return {
1141
+ folder: issue.folder,
1142
+ folders: issue.folders.map((name) => `${name}/`).join(", "),
1143
+ layer: issue.layer
1144
+ };
1145
+ if (issue.kind === "directlyInLayer" || issue.kind === "tooDeep") return {
1146
+ example: issue.example,
1147
+ layer: issue.layer
1148
+ };
1149
+ return {};
1150
+ }
1151
+ extraCompositionRoots(architecture) {
1152
+ const byFolder = /* @__PURE__ */ new Map();
1153
+ for (const file of architecture.files) if (architecture.locationOf(file).isCompositionRoot) {
1154
+ const folder = dirname(file.path);
1155
+ byFolder.set(folder, [...byFolder.get(folder) ?? [], file]);
1156
+ }
1157
+ const findings = [];
1158
+ for (const roots of byFolder.values()) {
1159
+ if (roots.length < 2) continue;
1160
+ const names = roots.map((root) => basename(root.path)).sort();
1161
+ for (const root of roots) {
1162
+ const location = architecture.locationOf(root);
1163
+ const data = {
1164
+ context: location.context ?? "",
1165
+ count: roots.length,
1166
+ names: names.join(", ")
1167
+ };
1168
+ findings.push(this.finding(root, 1, location.fileName, "compositionRoots", data));
1169
+ }
1170
+ }
1171
+ return findings;
1172
+ }
1173
+ layerProblem(from, to, architecture) {
1174
+ if (from.isAtRoot) return to.isInBoundedContext && !to.isCompositionRoot ? {
1175
+ data: {},
1176
+ messageId: "rootImport"
1177
+ } : void 0;
1178
+ if (from.isCompositionRoot || to.isOtherBoundedContextThan(from)) return;
1179
+ if (to.isCompositionRoot) return {
1180
+ data: {},
1181
+ messageId: "wiring"
1182
+ };
1183
+ const layer = from.layer;
1184
+ if (layer === void 0 || layer === "domain") return;
1185
+ const allowed = architecture.layers.importableFrom(layer);
1186
+ if (to.layer !== void 0 && allowed.includes(to.layer)) return;
1187
+ return {
1188
+ data: {
1189
+ allowed: allowed.join(", "),
1190
+ layer
1191
+ },
1192
+ messageId: "outwardLayer"
1193
+ };
1194
+ }
1195
+ applicationPackageFinding(dependency, packageName, file, architecture) {
1196
+ if (packageName === architecture.core.packageName) return;
1197
+ const { line, label } = dependency;
1198
+ const allowed = architecture.applicationDependencies;
1199
+ if (!allowed.has(packageName)) return this.finding(file, line, label, "undeclaredPackage", { package: packageName });
1200
+ const forbidden = allowed.forbiddenNames(packageName, dependency.names);
1201
+ if (forbidden.length === 0) return;
1202
+ return this.finding(file, line, label, "packageName", {
1203
+ allowed: allowed.allowedNames(packageName).join(", "),
1204
+ names: forbidden.join(", "),
1205
+ package: packageName
1206
+ });
1207
+ }
1208
+ };
1209
+ //#endregion
1210
+ //#region src/rules/layers/no-portless-adapter.rule.ts
1211
+ var NoPortlessAdapterRule = class extends ClassRule {
1212
+ meta = {
1213
+ description: "A driven adapter that extends no port, a port declared outside the domain.",
1214
+ id: "layers/no-portless-adapter",
1215
+ messages: {
1216
+ portless: "{class} is a driven adapter but extends no Port: extend the port it implements.",
1217
+ portOutsideDomain: "The port {class} is declared outside the domain: move it to domain/ports/ or domain/repositories/."
1218
+ }
1219
+ };
1220
+ findingsFor(codeClass, file, architecture) {
1221
+ const location = architecture.locationOf(file);
1222
+ const isPort = architecture.is(codeClass, "Port");
1223
+ if (this.isAdaptersFolder(location) && !isPort) return [this.finding(file, codeClass.line, codeClass.name, "portless", { class: codeClass.name })];
1224
+ if (isPort && codeClass.isAbstract && !this.isPortsFolder(location) && !this.isAdaptersFolder(location)) return [this.finding(file, codeClass.line, codeClass.name, "portOutsideDomain", { class: codeClass.name })];
1225
+ return [];
1226
+ }
1227
+ isAdaptersFolder(location) {
1228
+ return location.layer === "driven" && location.folder === "adapters";
1229
+ }
1230
+ isPortsFolder(location) {
1231
+ return location.layer === "domain" && (location.folder === "ports" || location.folder === "repositories");
1232
+ }
1233
+ };
1234
+ //#endregion
1235
+ //#region src/rules/strategic/no-cross-context-import.rule.ts
1236
+ var NoCrossContextImportRule = class extends ImportRule {
1237
+ meta = {
1238
+ description: "An import from another bounded context that is not its open host service, a composition root that re-exports.",
1239
+ id: "strategic/no-cross-context-import",
1240
+ messages: {
1241
+ notOpenHostService: "Imports {target}: only an OpenHostService of another bounded context may be imported.",
1242
+ outsideAntiCorruptionLayer: "Uses the open host service of {context} outside an AntiCorruptionLayer: translate it in an anti-corruption layer.",
1243
+ publishedLanguage: "Imports the published language of {context}: redeclare the fields you read in your own published-language/.",
1244
+ reexport: "The composition root re-exports {names}: it exports its own module only, so that no other context reaches through it.",
1245
+ sharedKernel: "The shared kernel imports no bounded context, but imports {target}."
1246
+ }
1247
+ };
1248
+ appliesTo(file, architecture) {
1249
+ const location = architecture.locationOf(file);
1250
+ return location.isInBoundedContext || location.isInSharedKernel;
1251
+ }
1252
+ findingFor(dependency, file, architecture) {
1253
+ const from = architecture.locationOf(file);
1254
+ if (from.isCompositionRoot && dependency.form === "re-export") return this.finding(file, dependency.line, dependency.label, "reexport", { names: dependency.label });
1255
+ const target = dependency.target;
1256
+ if (target.kind !== "file") return;
1257
+ const to = architecture.locationOfTarget(target);
1258
+ if (!to.isOtherBoundedContextThan(from)) return;
1259
+ if (from.isInSharedKernel) return this.finding(file, dependency.line, dependency.label, "sharedKernel", { target: this.wording.target(target, architecture) });
1260
+ if (from.isCompositionRoot && to.isCompositionRoot) return;
1261
+ if (to.layer === "published-language") return this.finding(file, dependency.line, dependency.label, "publishedLanguage", { context: to.context ?? "" });
1262
+ if (!this.importsOnlyOpenHostServices(dependency, target.path, architecture)) return this.finding(file, dependency.line, dependency.label, "notOpenHostService", { target: this.wording.target(target, architecture) });
1263
+ if (!from.isCompositionRoot && !this.declaresAntiCorruptionLayer(file, architecture)) return this.finding(file, dependency.line, dependency.label, "outsideAntiCorruptionLayer", { context: to.context ?? "" });
1264
+ }
1265
+ importsOnlyOpenHostServices(dependency, path, architecture) {
1266
+ const target = architecture.project.file(path);
1267
+ if (target === void 0 || dependency.names.length === 0) return false;
1268
+ return dependency.names.every((name) => {
1269
+ const declaration = target.classNamed(name);
1270
+ return declaration !== void 0 && architecture.implementsMarker(declaration, "OpenHostService");
1271
+ });
1272
+ }
1273
+ declaresAntiCorruptionLayer(file, architecture) {
1274
+ return file.classes.some((codeClass) => architecture.implementsMarker(codeClass, "AntiCorruptionLayer"));
1275
+ }
1276
+ };
1277
+ //#endregion
1278
+ //#region src/rules/strategic/no-fat-shared-kernel.rule.ts
1279
+ const owned = [
1280
+ "AggregateRoot",
1281
+ "Entity",
1282
+ "DomainEvent",
1283
+ "DomainService",
1284
+ "CommandRepository",
1285
+ "QueryRepository",
1286
+ "CommandHandler",
1287
+ "QueryHandler",
1288
+ "EventTranslator"
1289
+ ];
1290
+ var NoFatSharedKernelRule = class extends ClassRule {
1291
+ meta = {
1292
+ description: "An aggregate, an entity, an event, a domain service, a repository or a handler in the shared kernel.",
1293
+ id: "strategic/no-fat-shared-kernel",
1294
+ messages: { owned: "{class} is {kind} in the shared kernel: it belongs to one bounded context; the shared kernel holds value objects, ports and their adapters." }
1295
+ };
1296
+ findingsFor(codeClass, file, architecture) {
1297
+ if (!architecture.locationOf(file).isInSharedKernel) return [];
1298
+ const kind = architecture.kindsOf(codeClass).find((candidate) => owned.includes(candidate));
1299
+ if (kind === void 0) return [];
1300
+ const article = /^[AEIO]/.test(kind) ? "an" : "a";
1301
+ return [this.finding(file, codeClass.line, codeClass.name, "owned", {
1302
+ class: codeClass.name,
1303
+ kind: `${article} ${kind}`
1304
+ })];
1305
+ }
1306
+ };
1307
+ //#endregion
1308
+ //#region src/rules/strategic/no-leaky-host-service.rule.ts
1309
+ var NoLeakyHostServiceRule = class extends ClassRule {
1310
+ meta = {
1311
+ description: "An open host service that exposes a class of its context instead of the published language.",
1312
+ id: "strategic/no-leaky-host-service",
1313
+ messages: { exposes: "{member} exposes {type}, {kind}{owner}: an open host service speaks the published language." }
1314
+ };
1315
+ findingsFor(codeClass, file, architecture) {
1316
+ if (!architecture.implementsMarker(codeClass, "OpenHostService")) return [];
1317
+ const findings = /* @__PURE__ */ new Map();
1318
+ for (const member of codeClass.publicSurface) {
1319
+ const symbol = `${codeClass.name}.${member.name}`;
1320
+ for (const type of [...member.parameterTypes, ...member.valueTypes]) {
1321
+ const key = `${symbol}:${type.name}`;
1322
+ if (this.leaks(type, architecture) && !findings.has(key)) findings.set(key, this.finding(file, member.line, symbol, "exposes", this.describe(symbol, type, architecture)));
1323
+ }
1324
+ }
1325
+ return [...findings.values()];
1326
+ }
1327
+ leaks(type, architecture) {
1328
+ if (type.isInstalled) return false;
1329
+ const declaringFile = type.declaredIn === void 0 ? void 0 : architecture.project.file(type.declaredIn);
1330
+ return declaringFile === void 0 || !architecture.locationOf(declaringFile).isInSharedKernel;
1331
+ }
1332
+ describe(symbol, type, architecture) {
1333
+ const kind = architecture.kindOf(type) ?? "class";
1334
+ const article = /^[AEIO]/.test(kind) ? "an" : "a";
1335
+ const declaringFile = type.declaredIn === void 0 ? void 0 : architecture.project.file(type.declaredIn);
1336
+ const context = declaringFile === void 0 ? void 0 : architecture.locationOf(declaringFile).context;
1337
+ return {
1338
+ kind: `${article} ${kind}`,
1339
+ member: symbol,
1340
+ owner: context === void 0 ? "" : ` of ${context}`,
1341
+ type: type.name
1342
+ };
1343
+ }
1344
+ };
1345
+ //#endregion
1346
+ //#region src/rules/strategic/no-unmapped-context.rule.ts
1347
+ var NoUnmappedContextRule = class extends Rule {
1348
+ meta = {
1349
+ description: "A bounded context consuming one the context map does not allow, or two contexts that depend on each other.",
1350
+ id: "strategic/no-unmapped-context",
1351
+ messages: {
1352
+ cycle: "{from} consumes {to}, which consumes {from} back: two contexts that depend on each other can no longer change alone; declare a contextMap and reverse one dependency.",
1353
+ unmapped: "{from} consumes {to}, which the context map does not allow: add {to} to contextMap.{from}, or reverse the dependency."
1354
+ }
1355
+ };
1356
+ check(architecture) {
1357
+ const consumptions = this.consumptionsIn(architecture);
1358
+ const declared = architecture.contextMap;
1359
+ const findings = [];
1360
+ if (declared !== void 0) {
1361
+ for (const consumption of consumptions) if (!declared.allows(consumption.from, consumption.to)) findings.push(this.consumptionFinding(consumption, "unmapped"));
1362
+ return findings;
1363
+ }
1364
+ const observed = ContextMap.ofEdges(consumptions);
1365
+ for (const consumption of consumptions) if (observed.cycleThrough(consumption.from, consumption.to)) findings.push(this.consumptionFinding(consumption, "cycle"));
1366
+ return findings;
1367
+ }
1368
+ consumptionsIn(architecture) {
1369
+ const consumptions = [];
1370
+ for (const file of architecture.files) {
1371
+ const from = architecture.locationOf(file);
1372
+ if (!from.isInBoundedContext || from.context === void 0) continue;
1373
+ for (const dependency of file.dependencies) {
1374
+ if (dependency.target.kind !== "file") continue;
1375
+ const to = architecture.locationOfTarget(dependency.target);
1376
+ if (to.isOtherBoundedContextThan(from) && to.context !== void 0) consumptions.push({
1377
+ dependency,
1378
+ file,
1379
+ from: from.context,
1380
+ to: to.context
1381
+ });
1382
+ }
1383
+ }
1384
+ return consumptions;
1385
+ }
1386
+ consumptionFinding(consumption, messageId) {
1387
+ const { dependency, file, from, to } = consumption;
1388
+ return this.finding(file, dependency.line, dependency.label, messageId, {
1389
+ from,
1390
+ to
1391
+ });
1392
+ }
1393
+ };
1394
+ //#endregion
1395
+ //#region src/rules/tactical/aggregates/no-aggregate-reference.rule.ts
1396
+ const holders = [
1397
+ "Entity",
1398
+ "ValueObject",
1399
+ "DomainEvent"
1400
+ ];
1401
+ var NoAggregateReferenceRule = class extends Rule {
1402
+ meta = {
1403
+ description: "An aggregate holding another aggregate instead of its identifier, an entity held by two aggregates.",
1404
+ id: "tactical/no-aggregate-reference",
1405
+ messages: {
1406
+ aggregateInTypeArgument: "{class} holds the aggregate {type} in its {parameter}: reference it by its identifier instead.",
1407
+ heldAggregate: "{member} holds the aggregate {type}: reference it by its identifier instead.",
1408
+ sharedEntity: "{member} holds the entity {entity}, which {others} {verb} too: an entity belongs to one aggregate."
1409
+ }
1410
+ };
1411
+ check(architecture) {
1412
+ const findings = [];
1413
+ for (const file of architecture.files) for (const codeClass of file.classes) findings.push(...this.heldAggregates(codeClass, file, architecture));
1414
+ findings.push(...this.sharedEntities(architecture));
1415
+ return findings;
1416
+ }
1417
+ heldAggregates(codeClass, file, architecture) {
1418
+ if (!holders.some((kind) => architecture.is(codeClass, kind))) return [];
1419
+ const findings = [];
1420
+ for (const member of codeClass.heldMembers) for (const type of member.valueTypes) if (architecture.is(type, "AggregateRoot")) {
1421
+ const symbol = `${codeClass.name}.${member.name}`;
1422
+ findings.push(this.finding(file, member.line, symbol, "heldAggregate", {
1423
+ member: symbol,
1424
+ type: type.name
1425
+ }));
1426
+ }
1427
+ for (const argument of codeClass.typeArguments) for (const type of argument.types) if (architecture.is(type, "AggregateRoot")) {
1428
+ const data = {
1429
+ class: codeClass.name,
1430
+ parameter: argument.parameter,
1431
+ type: type.name
1432
+ };
1433
+ findings.push(this.finding(file, argument.line, `${codeClass.name}.${argument.parameter}`, "aggregateInTypeArgument", data));
1434
+ }
1435
+ return findings;
1436
+ }
1437
+ sharedEntities(architecture) {
1438
+ const holdings = this.holdingsIn(architecture);
1439
+ const findings = [];
1440
+ for (const holding of holdings) {
1441
+ const others = this.otherOwners(holding, holdings);
1442
+ if (others.length > 0) {
1443
+ const symbol = `${holding.aggregate.name}.${holding.through.name}`;
1444
+ const data = {
1445
+ entity: holding.entity.name,
1446
+ member: symbol,
1447
+ others: others.join(" and "),
1448
+ verb: others.length === 1 ? "holds" : "hold"
1449
+ };
1450
+ findings.push(this.finding(holding.file, holding.through.line, symbol, "sharedEntity", data));
1451
+ }
1452
+ }
1453
+ return findings;
1454
+ }
1455
+ otherOwners(holding, holdings) {
1456
+ const others = /* @__PURE__ */ new Set();
1457
+ for (const other of holdings) if (other.aggregate !== holding.aggregate && other.entity.key === holding.entity.key) others.add(other.aggregate.name);
1458
+ return [...others].sort();
1459
+ }
1460
+ holdingsIn(architecture) {
1461
+ const holdings = [];
1462
+ for (const file of architecture.files) for (const aggregate of file.classes) {
1463
+ if (!architecture.is(aggregate, "AggregateRoot")) continue;
1464
+ const visited = /* @__PURE__ */ new Set();
1465
+ for (const through of this.heldBy(aggregate)) for (const type of through.types) for (const entity of this.entitiesReachedFrom(type, architecture, visited)) holdings.push({
1466
+ aggregate,
1467
+ entity,
1468
+ file,
1469
+ through
1470
+ });
1471
+ }
1472
+ return holdings;
1473
+ }
1474
+ entitiesReachedFrom(type, architecture, visited) {
1475
+ if (architecture.is(type, "AggregateRoot") || visited.has(type.key)) return [];
1476
+ visited.add(type.key);
1477
+ const isEntity = architecture.is(type, "Entity");
1478
+ const reached = isEntity ? [type] : [];
1479
+ const declaration = architecture.classOf(type);
1480
+ if (declaration === void 0 || !(isEntity || architecture.is(type, "ValueObject"))) return reached;
1481
+ for (const held of this.heldBy(declaration)) for (const inner of held.types) reached.push(...this.entitiesReachedFrom(inner, architecture, visited));
1482
+ return reached;
1483
+ }
1484
+ heldBy(codeClass) {
1485
+ const held = codeClass.heldMembers.map((member) => ({
1486
+ line: member.line,
1487
+ name: member.name,
1488
+ types: member.valueTypes
1489
+ }));
1490
+ for (const argument of codeClass.typeArguments) held.push({
1491
+ line: argument.line,
1492
+ name: argument.parameter,
1493
+ types: argument.types
1494
+ });
1495
+ return held;
1496
+ }
1497
+ };
1498
+ //#endregion
1499
+ //#region src/rules/tactical/building-blocks/no-loose-code.rule.ts
1500
+ const guarded$2 = /* @__PURE__ */ new Set(["domain", "application"]);
1501
+ const adapters = /* @__PURE__ */ new Set(["driven", "driving"]);
1502
+ const expectedBlocks = {
1503
+ application: "CommandHandler, QueryHandler or EventTranslator",
1504
+ domain: "AggregateRoot, Entity, ValueObject, Identifier, DomainEvent, DomainError, DomainService or a Port"
1505
+ };
1506
+ const statementMessages = {
1507
+ "class expression": "classExpression",
1508
+ "computed constant": "computedConstant",
1509
+ enum: "enum",
1510
+ function: "function",
1511
+ "mutable variable": "mutableVariable",
1512
+ namespace: "namespace",
1513
+ statement: "statement"
1514
+ };
1515
+ var NoLooseCodeRule = class extends Rule {
1516
+ meta = {
1517
+ description: "Code outside a building block in the domain or the application, outside a class in an adapter layer, anything but the module class in a composition root.",
1518
+ id: "tactical/no-loose-code",
1519
+ messages: {
1520
+ adapterDeclaration: "The {kind} {name} has no place in an adapter layer: adapters are classes; make it a method of the adapter, or a mapper class of its own.",
1521
+ adapterStatement: "A statement runs when the module loads: an adapter layer holds classes, which the composition root wires.",
1522
+ classExpression: "{name} is a class expression: declare it as a class that extends a building block.",
1523
+ compositionRootDeclaration: "The {kind} {name} has no place in a composition root: it holds its module class only.",
1524
+ compositionRootStatement: "A statement runs when the module loads: the composition root holds its module class only.",
1525
+ computedConstant: "The constant {name} is computed when the module loads: keep top-level constants to plain data.",
1526
+ enum: "The enum {name} has no place here: use a union of literal types, or a ValueObject when it has behaviour.",
1527
+ extendsExpression: "{name} extends an expression: extend a class by its name, so that what it is stays readable.",
1528
+ function: "The function {name} floats outside any class: make it a method of a value object or of a DomainService.",
1529
+ mutableVariable: "{name} is module state: keep state in aggregates, not in modules.",
1530
+ namespace: "The namespace {name} groups loose code: make it a method of a value object or of a DomainService.",
1531
+ plainClass: "{name} extends no building block: extend {blocks}.",
1532
+ statement: "A statement runs when the module loads: move it into a method.",
1533
+ staticOnly: "{name} only has static members: a class of functions is no building block; make them methods of the value object they work on, or of a DomainService."
1534
+ }
1535
+ };
1536
+ check(architecture) {
1537
+ const findings = [];
1538
+ for (const file of architecture.files) {
1539
+ const location = architecture.locationOf(file);
1540
+ if (guarded$2.has(location.layer)) {
1541
+ findings.push(...this.looseClasses(file, location.layer ?? "domain", architecture));
1542
+ findings.push(...this.looseStatements(file));
1543
+ } else if (location.isCompositionRoot && location.isInBoundedContext) findings.push(...this.compositionRootStatements(file));
1544
+ else if (adapters.has(location.layer)) findings.push(...this.adapterStatements(file));
1545
+ }
1546
+ return findings;
1547
+ }
1548
+ looseClasses(file, layer, architecture) {
1549
+ const findings = [];
1550
+ for (const codeClass of file.classes) {
1551
+ const messageId = this.classMessage(codeClass, architecture);
1552
+ if (messageId !== void 0) findings.push(this.finding(file, codeClass.line, codeClass.name, messageId, {
1553
+ blocks: expectedBlocks[layer] ?? "",
1554
+ name: codeClass.name
1555
+ }));
1556
+ }
1557
+ return findings;
1558
+ }
1559
+ classMessage(codeClass, architecture) {
1560
+ if (!codeClass.extendsByName) return "extendsExpression";
1561
+ if (!architecture.isBuildingBlock(codeClass)) return "plainClass";
1562
+ return codeClass.isStaticOnly ? "staticOnly" : void 0;
1563
+ }
1564
+ looseStatements(file) {
1565
+ return file.statements.map((statement) => this.statementFinding(file, statement, statementMessages[statement.kind]));
1566
+ }
1567
+ compositionRootStatements(file) {
1568
+ return file.statements.map((statement) => this.statementFinding(file, statement, statement.kind === "statement" ? "compositionRootStatement" : "compositionRootDeclaration"));
1569
+ }
1570
+ adapterStatements(file) {
1571
+ return file.statements.map((statement) => this.statementFinding(file, statement, statement.kind === "statement" ? "adapterStatement" : "adapterDeclaration"));
1572
+ }
1573
+ statementFinding(file, statement, messageId) {
1574
+ return this.finding(file, statement.line, statement.name, messageId, {
1575
+ kind: statement.kind,
1576
+ name: statement.name
1577
+ });
1578
+ }
1579
+ };
1580
+ //#endregion
1581
+ //#region src/rules/tactical/building-blocks/no-misplaced-class.rule.ts
1582
+ const markerWording = {
1583
+ AntiCorruptionLayer: "driven/<technology>/adapters/*.adapter.ts, as an adapter of a port",
1584
+ OpenHostService: "driving/<technology>/"
1585
+ };
1586
+ var NoMisplacedClassRule = class extends Rule {
1587
+ meta = {
1588
+ description: "A class in the wrong folder or file, two classes in one file.",
1589
+ id: "tactical/no-misplaced-class",
1590
+ messages: {
1591
+ misplacedKind: "{name} belongs in {place}.",
1592
+ misplacedMarker: "{name} implements {marker}: it belongs in {place}.",
1593
+ sharedFile: "{name} shares its file with {first}: one class per file."
1594
+ }
1595
+ };
1596
+ check(architecture) {
1597
+ const findings = [];
1598
+ for (const file of architecture.files) {
1599
+ findings.push(...this.extraClasses(file));
1600
+ const [first] = file.classes;
1601
+ const finding = first === void 0 ? void 0 : this.misplacement(first, file, architecture.locationOf(file), architecture);
1602
+ if (finding !== void 0) findings.push(finding);
1603
+ }
1604
+ return findings;
1605
+ }
1606
+ extraClasses(file) {
1607
+ const [first, ...others] = file.classes;
1608
+ const findings = [];
1609
+ for (const codeClass of others) findings.push(this.finding(file, codeClass.line, codeClass.name, "sharedFile", {
1610
+ first: first?.name ?? "",
1611
+ name: codeClass.name
1612
+ }));
1613
+ return findings;
1614
+ }
1615
+ misplacement(codeClass, file, location, architecture) {
1616
+ for (const { marker, place } of architecture.blocks.markedPlacesOf(codeClass)) if (!place.fits(location)) return this.finding(file, codeClass.line, codeClass.name, "misplacedMarker", {
1617
+ marker,
1618
+ name: codeClass.name,
1619
+ place: markerWording[marker]
1620
+ });
1621
+ const place = architecture.blocks.placeOf(codeClass);
1622
+ if (place === void 0 || place.fits(location)) return;
1623
+ return this.finding(file, codeClass.line, codeClass.name, "misplacedKind", {
1624
+ name: codeClass.name,
1625
+ place: this.wording.place(place)
1626
+ });
1627
+ }
1628
+ };
1629
+ //#endregion
1630
+ //#region src/rules/tactical/command-handlers/no-foreign-command-dependency.rule.ts
1631
+ var NoForeignCommandDependencyRule = class extends InjectionRule {
1632
+ meta = {
1633
+ description: "A command handler receiving a query repository, an event publisher, another handler or a plain class.",
1634
+ id: "tactical/no-foreign-command-dependency",
1635
+ messages: { foreign: "The CommandHandler {class} receives {type}, {kind}: a command handler receives command repositories, ports, event translators, domain services and value objects; events leave through the outbox, never a publisher." }
1636
+ };
1637
+ holder = "CommandHandler";
1638
+ forbidden = ["QueryRepository", "EventPublisher"];
1639
+ allowed = [
1640
+ "CommandRepository",
1641
+ "Port",
1642
+ "EventTranslator",
1643
+ "DomainService",
1644
+ "ValueObject",
1645
+ "Identifier"
1646
+ ];
1647
+ };
1648
+ //#endregion
1649
+ //#region src/rules/tactical/domain-errors/no-thrown-failure.rule.ts
1650
+ const exempt = /* @__PURE__ */ new Set([
1651
+ "equals",
1652
+ "toSnapshot",
1653
+ "toString",
1654
+ "toJSON",
1655
+ "valueOf"
1656
+ ]);
1657
+ const guarded$1 = /* @__PURE__ */ new Set(["domain", "application"]);
1658
+ var NoThrownFailureRule = class extends Rule {
1659
+ meta = {
1660
+ description: "A business failure thrown instead of returned, an entity method without a Result, a setter.",
1661
+ id: "tactical/no-thrown-failure",
1662
+ messages: {
1663
+ noResult: "{member} must return a Result: expose reads as getters and return business failures as values.",
1664
+ rejected: "A failure is rejected: return it in a Result instead.",
1665
+ setter: "{member} is a setter: change the state through a business method that returns a Result.",
1666
+ thrown: "A failure is thrown: return it in a Result instead."
1667
+ }
1668
+ };
1669
+ check(architecture) {
1670
+ const findings = [];
1671
+ for (const file of architecture.files) {
1672
+ findings.push(...this.operationsWithoutResult(file, architecture));
1673
+ if (guarded$1.has(architecture.locationOf(file).layer)) findings.push(...this.raisedFailures(file));
1674
+ }
1675
+ return findings;
1676
+ }
1677
+ operationsWithoutResult(file, architecture) {
1678
+ const findings = [];
1679
+ for (const codeClass of file.classes) {
1680
+ if (!architecture.is(codeClass, "Entity")) continue;
1681
+ for (const member of codeClass.members) {
1682
+ const messageId = this.messageFor(member, architecture);
1683
+ if (messageId !== void 0) {
1684
+ const symbol = `${codeClass.name}.${member.name}`;
1685
+ findings.push(this.finding(file, member.line, symbol, messageId, { member: symbol }));
1686
+ }
1687
+ }
1688
+ }
1689
+ return findings;
1690
+ }
1691
+ messageFor(member, architecture) {
1692
+ if (!member.isPublicInstance || exempt.has(member.name) || member.name.startsWith("[")) return;
1693
+ if (member.kind === "setter") return "setter";
1694
+ return (member.kind === "method" || member.kind === "field" && member.isCallable) && !architecture.returnsResult(member) ? "noResult" : void 0;
1695
+ }
1696
+ raisedFailures(file) {
1697
+ const findings = [];
1698
+ for (const thrown of file.throws) findings.push(this.finding(file, thrown.line, thrown.form, thrown.form === "throw" ? "thrown" : "rejected"));
1699
+ return findings;
1700
+ }
1701
+ };
1702
+ //#endregion
1703
+ //#region src/rules/tactical/domain-services/no-stateful-service.rule.ts
1704
+ var NoStatefulServiceRule = class extends InjectionRule {
1705
+ meta = {
1706
+ description: "A domain service holding a port, a repository or another service.",
1707
+ id: "tactical/no-stateful-service",
1708
+ messages: { foreign: "The DomainService {class} holds {type}, {kind}: a domain service holds configuration only; the command handler passes it what it needs." }
1709
+ };
1710
+ holder = "DomainService";
1711
+ forbidden = [];
1712
+ allowed = ["ValueObject", "Identifier"];
1713
+ };
1714
+ //#endregion
1715
+ //#region src/rules/tactical/entities/no-public-field.rule.ts
1716
+ const guarded = [
1717
+ "Entity",
1718
+ "ValueObject",
1719
+ "Identifier"
1720
+ ];
1721
+ var NoPublicFieldRule = class extends ClassRule {
1722
+ meta = {
1723
+ description: "A public field on an aggregate, an entity, a value object or an identifier.",
1724
+ id: "tactical/no-public-field",
1725
+ messages: { publicField: "{member} is a public field: keep the state private, and expose what callers need through a getter." }
1726
+ };
1727
+ findingsFor(codeClass, file, architecture) {
1728
+ if (!guarded.some((kind) => architecture.is(codeClass, kind))) return [];
1729
+ const findings = [];
1730
+ for (const member of codeClass.members) if (member.isField && member.visibility === "public" && !member.isStatic) {
1731
+ const symbol = `${codeClass.name}.${member.name}`;
1732
+ findings.push(this.finding(file, member.line, symbol, "publicField", { member: symbol }));
1733
+ }
1734
+ return findings;
1735
+ }
1736
+ };
1737
+ //#endregion
1738
+ //#region src/rules/tactical/query-handlers/no-foreign-query-dependency.rule.ts
1739
+ var NoForeignQueryDependencyRule = class extends InjectionRule {
1740
+ meta = {
1741
+ description: "A query handler receiving what writes or changes state.",
1742
+ id: "tactical/no-foreign-query-dependency",
1743
+ messages: { foreign: "The QueryHandler {class} receives {type}, {kind}: a query handler receives query repositories, ports that do not write, and value objects." }
1744
+ };
1745
+ holder = "QueryHandler";
1746
+ forbidden = [
1747
+ "CommandRepository",
1748
+ "Outbox",
1749
+ "UnitOfWork",
1750
+ "EventPublisher"
1751
+ ];
1752
+ allowed = [
1753
+ "QueryRepository",
1754
+ "Port",
1755
+ "ValueObject",
1756
+ "Identifier"
1757
+ ];
1758
+ };
1759
+ //#endregion
1760
+ //#region src/rules/tooling/disable-directive.ts
1761
+ var DisableDirective = class {
1762
+ rule;
1763
+ reason;
1764
+ constructor(text) {
1765
+ const match = /^([a-z]+\/[a-z-]+)?\s*(?::\s*(.+?))?\s*$/.exec(text);
1766
+ this.rule = match?.[1];
1767
+ this.reason = match?.[2];
1768
+ }
1769
+ get isComplete() {
1770
+ return this.rule !== void 0 && this.reason !== void 0;
1771
+ }
1772
+ };
1773
+ //#endregion
1774
+ //#region src/rules/tooling/no-loose-disable.rule.ts
1775
+ var NoLooseDisableRule = class extends Rule {
1776
+ knownRules;
1777
+ meta = {
1778
+ description: "A disable comment that names no known rule, gives no reason, or disables nothing.",
1779
+ id: "tooling/no-loose-disable",
1780
+ messages: {
1781
+ noReason: "The disable comment gives no reason: write `// alveolus-disable-next-line {rule}: <why this line keeps its violation>`.",
1782
+ noRule: "The disable comment names no rule: write `// alveolus-disable-next-line <rule-id>: <reason>`.",
1783
+ unknownRule: "The disable comment names {rule}, which is no rule: check the id on the rules page.",
1784
+ unused: "The disable comment disables nothing: the line below breaks {rule} no more; remove the comment."
1785
+ }
1786
+ };
1787
+ constructor(knownRules) {
1788
+ super();
1789
+ this.knownRules = knownRules;
1790
+ }
1791
+ check(architecture) {
1792
+ const findings = [];
1793
+ for (const file of architecture.files) for (const comment of file.disables) {
1794
+ const finding = this.malformed(file, comment);
1795
+ if (finding !== void 0) findings.push(finding);
1796
+ }
1797
+ return findings;
1798
+ }
1799
+ unused(file, comment) {
1800
+ const directive = new DisableDirective(comment.text);
1801
+ return this.finding(file, comment.line, "alveolus-disable-next-line", "unused", { rule: directive.rule ?? "" });
1802
+ }
1803
+ malformed(file, comment) {
1804
+ const directive = new DisableDirective(comment.text);
1805
+ if (directive.rule === void 0) return this.finding(file, comment.line, "alveolus-disable-next-line", "noRule");
1806
+ if (!this.knownRules.includes(directive.rule)) return this.finding(file, comment.line, "alveolus-disable-next-line", "unknownRule", { rule: directive.rule });
1807
+ if (directive.reason === void 0) return this.finding(file, comment.line, "alveolus-disable-next-line", "noReason", { rule: directive.rule });
1808
+ }
1809
+ };
1810
+ //#endregion
1811
+ //#region src/rules/registry.ts
1812
+ const ruleIds = [
1813
+ "strategic/no-cross-context-import",
1814
+ "strategic/no-leaky-host-service",
1815
+ "strategic/no-unmapped-context",
1816
+ "strategic/no-fat-shared-kernel",
1817
+ "layers/no-impure-domain",
1818
+ "layers/no-outward-import",
1819
+ "layers/no-portless-adapter",
1820
+ "layers/no-driving-shortcut",
1821
+ "tactical/no-aggregate-reference",
1822
+ "tactical/no-public-field",
1823
+ "tactical/no-foreign-command-dependency",
1824
+ "tactical/no-foreign-query-dependency",
1825
+ "tactical/no-stateful-service",
1826
+ "tactical/no-thrown-failure",
1827
+ "tactical/no-misplaced-class",
1828
+ "tactical/no-loose-code",
1829
+ "tooling/no-loose-disable"
1830
+ ];
1831
+ var RuleRegistry = class {
1832
+ rules = [
1833
+ new NoCrossContextImportRule(),
1834
+ new NoLeakyHostServiceRule(),
1835
+ new NoUnmappedContextRule(),
1836
+ new NoFatSharedKernelRule(),
1837
+ new NoImpureDomainRule(),
1838
+ new NoOutwardImportRule(),
1839
+ new NoPortlessAdapterRule(),
1840
+ new NoDrivingShortcutRule(),
1841
+ new NoAggregateReferenceRule(),
1842
+ new NoPublicFieldRule(),
1843
+ new NoForeignCommandDependencyRule(),
1844
+ new NoForeignQueryDependencyRule(),
1845
+ new NoStatefulServiceRule(),
1846
+ new NoThrownFailureRule(),
1847
+ new NoMisplacedClassRule(),
1848
+ new NoLooseCodeRule(),
1849
+ new NoLooseDisableRule(ruleIds)
1850
+ ];
1851
+ get looseDisable() {
1852
+ return this.rules.find((rule) => rule instanceof NoLooseDisableRule);
1853
+ }
1854
+ get ids() {
1855
+ return this.rules.map((rule) => rule.meta.id);
1856
+ }
1857
+ };
1858
+ //#endregion
1859
+ //#region src/check/fingerprint.ts
1860
+ const length = 8;
1861
+ var Fingerprint = class {
1862
+ of(text) {
1863
+ const normalized = text.trim().replace(/\s+/g, " ");
1864
+ return createHash("sha1").update(normalized).digest("hex").slice(0, length);
1865
+ }
1866
+ };
1867
+ //#endregion
1868
+ //#region src/check/checker.ts
1869
+ var Checker = class {
1870
+ importer;
1871
+ rules;
1872
+ fingerprint = new Fingerprint();
1873
+ constructor(importer, rules) {
1874
+ this.importer = importer;
1875
+ this.rules = rules;
1876
+ }
1877
+ check(settings) {
1878
+ const project = this.importer.read(settings);
1879
+ if (project.files.length === 0) throw new Error(`No file to analyse under ${settings.rootDir}: check root and ignore in alveolus.config.ts, and include in your tsconfig.`);
1880
+ if (!this.importer.resolves("@alveolus/core", settings)) throw new Error(`${corePackageName} cannot be imported from ${settings.rootDir}: install it, or map it in the paths of your tsconfig. Without it, no building block can be recognised.`);
1881
+ const architecture = new Architecture(project, settings);
1882
+ const outcome = {
1883
+ files: project.files.length,
1884
+ suppressed: [],
1885
+ violations: []
1886
+ };
1887
+ const used = /* @__PURE__ */ new Set();
1888
+ for (const rule of this.rules) {
1889
+ const severity = settings.severityOf(rule.meta.id);
1890
+ if (severity !== "off") this.collect(rule, severity, architecture, outcome, used);
1891
+ }
1892
+ this.collectUnusedDisables(architecture, settings, outcome, used);
1893
+ outcome.violations.sort((left, right) => this.compare(left, right));
1894
+ return outcome;
1895
+ }
1896
+ run(rule, architecture) {
1897
+ return rule.check(architecture).map((finding) => this.violationOf(rule, "error", finding, architecture));
1898
+ }
1899
+ collect(rule, severity, architecture, outcome, used) {
1900
+ for (const finding of rule.check(architecture)) {
1901
+ const violation = this.violationOf(rule, severity, finding, architecture);
1902
+ const comment = finding.file.disableAbove(finding.line);
1903
+ const directive = comment === void 0 ? void 0 : new DisableDirective(comment.text);
1904
+ if (comment !== void 0 && directive?.isComplete === true && directive.rule === rule.meta.id) {
1905
+ used.add(`${finding.file.path}:${comment.line}`);
1906
+ outcome.suppressed.push({
1907
+ reason: directive.reason ?? "",
1908
+ violation
1909
+ });
1910
+ } else outcome.violations.push(violation);
1911
+ }
1912
+ }
1913
+ collectUnusedDisables(architecture, settings, outcome, used) {
1914
+ const rule = this.rules.find((candidate) => candidate instanceof NoLooseDisableRule);
1915
+ if (!(rule instanceof NoLooseDisableRule)) return;
1916
+ const severity = settings.severityOf(rule.meta.id);
1917
+ if (severity === "off") return;
1918
+ for (const file of architecture.files) for (const comment of file.disables) if (new DisableDirective(comment.text).isComplete && !used.has(`${file.path}:${comment.line}`)) outcome.violations.push(this.violationOf(rule, severity, rule.unused(file, comment), architecture));
1919
+ }
1920
+ violationOf(rule, severity, finding, architecture) {
1921
+ return {
1922
+ file: architecture.relativePath(finding.file.path),
1923
+ fingerprint: this.fingerprint.of(finding.file.lineText(finding.line)),
1924
+ line: finding.line,
1925
+ message: this.messageOf(rule, finding),
1926
+ rule: rule.meta.id,
1927
+ severity,
1928
+ symbol: finding.symbol
1929
+ };
1930
+ }
1931
+ messageOf(rule, finding) {
1932
+ const template = rule.meta.messages[finding.messageId];
1933
+ if (template === void 0) throw new Error(`${rule.meta.id} has no message "${finding.messageId}".`);
1934
+ return template.replace(/\{(\w+)\}/g, (placeholder, key) => this.fill(rule, finding, key, placeholder));
1935
+ }
1936
+ fill(rule, finding, key, placeholder) {
1937
+ const value = finding.data[key];
1938
+ if (value === void 0) throw new Error(`${rule.meta.id}: the message "${finding.messageId}" needs ${placeholder}, which the finding does not give.`);
1939
+ return String(value);
1940
+ }
1941
+ compare(left, right) {
1942
+ return left.file.localeCompare(right.file) || left.line - right.line || left.rule.localeCompare(right.rule);
1943
+ }
1944
+ };
1945
+ //#endregion
1946
+ //#region src/check/report.ts
1947
+ const docsUrl = "https://alveolus.dev/";
1948
+ const sarifLevels = {
1949
+ error: "error",
1950
+ info: "note",
1951
+ warn: "warning"
1952
+ };
1953
+ var Report = class {
1954
+ input;
1955
+ colors;
1956
+ constructor(input, colored = false) {
1957
+ this.input = input;
1958
+ this.colors = picocolors.createColors(colored);
1959
+ }
1960
+ text() {
1961
+ const blocks = [];
1962
+ for (const [file, violations] of this.byFile()) blocks.push(this.block(file, violations));
1963
+ return `${[...blocks, this.summary()].join("\n\n")}\n`;
1964
+ }
1965
+ json() {
1966
+ const { baselined, files, stale, suppressed, violations } = this.input;
1967
+ const disabled = suppressed.map(({ reason, violation }) => ({
1968
+ ...violation,
1969
+ reason
1970
+ }));
1971
+ return `${JSON.stringify({
1972
+ baselined,
1973
+ files,
1974
+ stale,
1975
+ suppressed: disabled,
1976
+ violations
1977
+ }, null, " ")}\n`;
1978
+ }
1979
+ sarif(rules) {
1980
+ const driver = {
1981
+ informationUri: docsUrl,
1982
+ name: "alveolus",
1983
+ rules: rules.map((rule) => ({
1984
+ helpUri: `${docsUrl}rules/${rule.id}`,
1985
+ id: rule.id,
1986
+ shortDescription: { text: rule.description }
1987
+ }))
1988
+ };
1989
+ const log = {
1990
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
1991
+ runs: [{
1992
+ results: this.input.violations.map((violation) => ({
1993
+ level: sarifLevels[violation.severity],
1994
+ locations: [{ physicalLocation: {
1995
+ artifactLocation: {
1996
+ uri: violation.file,
1997
+ uriBaseId: "%SRCROOT%"
1998
+ },
1999
+ region: { startLine: violation.line }
2000
+ } }],
2001
+ message: { text: violation.message },
2002
+ partialFingerprints: { "alveolus/v1": violation.fingerprint },
2003
+ ruleId: violation.rule
2004
+ })),
2005
+ tool: { driver }
2006
+ }],
2007
+ version: "2.1.0"
2008
+ };
2009
+ return `${JSON.stringify(log, null, " ")}\n`;
2010
+ }
2011
+ byFile() {
2012
+ const byFile = /* @__PURE__ */ new Map();
2013
+ for (const violation of this.input.violations) byFile.set(violation.file, [...byFile.get(violation.file) ?? [], violation]);
2014
+ return byFile;
2015
+ }
2016
+ block(file, violations) {
2017
+ const lines = violations.map((violation) => ` ${violation.line} ${this.severity(violation.severity)} ${violation.rule}: ${violation.message}`);
2018
+ return [this.colors.bold(file), ...lines].join("\n");
2019
+ }
2020
+ severity(severity) {
2021
+ const { dim, red, yellow } = this.colors;
2022
+ if (severity === "error") return red("error");
2023
+ if (severity === "warn") return yellow("warn");
2024
+ return dim("info");
2025
+ }
2026
+ summary() {
2027
+ const { dim, green } = this.colors;
2028
+ const counted = this.counts();
2029
+ const head = counted.length === 0 ? green("No violation") : counted.join(", ");
2030
+ const parts = [];
2031
+ if (this.input.baselined > 0) parts.push(`${this.input.baselined} in the baseline`);
2032
+ if (this.input.stale > 0) parts.push(`${this.input.stale} fixed`);
2033
+ if (this.input.suppressed.length > 0) parts.push(`${this.input.suppressed.length} disabled`);
2034
+ const where = ` in ${this.input.files} file${this.input.files === 1 ? "" : "s"}`;
2035
+ return parts.length === 0 ? `${head}${where}` : `${head}${where}${dim(` (${parts.join(", ")})`)}`;
2036
+ }
2037
+ counts() {
2038
+ const { dim, red, yellow } = this.colors;
2039
+ const errors = this.input.violations.filter((violation) => violation.severity === "error").length;
2040
+ const warnings = this.input.violations.filter((violation) => violation.severity === "warn").length;
2041
+ const infos = this.input.violations.filter((violation) => violation.severity === "info").length;
2042
+ const counted = [];
2043
+ if (errors > 0) counted.push(red(`${errors} error${errors === 1 ? "" : "s"}`));
2044
+ if (warnings > 0) counted.push(yellow(`${warnings} warning${warnings === 1 ? "" : "s"}`));
2045
+ if (infos > 0) counted.push(dim(`${infos} info${infos === 1 ? "" : "s"}`));
2046
+ return counted;
2047
+ }
2048
+ };
2049
+ //#endregion
2050
+ //#region src/config/config.ts
2051
+ const testFiles = [
2052
+ "**/*.spec.ts",
2053
+ "**/*.test.ts",
2054
+ "**/*.e2e-spec.ts",
2055
+ "**/*.fixture.ts",
2056
+ "**/*.fixtures.ts",
2057
+ "**/*.stories.ts",
2058
+ "**/__tests__/**",
2059
+ "**/__mocks__/**"
2060
+ ];
2061
+ var Config = class {
2062
+ projectDir;
2063
+ rootDir;
2064
+ tsConfigPath;
2065
+ compositionRoot;
2066
+ domainDependencies;
2067
+ applicationDependencies;
2068
+ contextFolders;
2069
+ extraFolders;
2070
+ contextMap;
2071
+ ignored;
2072
+ rules;
2073
+ constructor(config, projectDir) {
2074
+ this.projectDir = resolve(projectDir);
2075
+ this.rootDir = resolve(projectDir, config.root);
2076
+ this.tsConfigPath = resolve(projectDir, config.tsconfig ?? "tsconfig.json");
2077
+ this.compositionRoot = config.compositionRoot ?? "*.module.ts";
2078
+ this.domainDependencies = new AllowedPackages(config.domainDependencies);
2079
+ this.applicationDependencies = this.domainDependencies.with(new AllowedPackages(config.applicationDependencies));
2080
+ this.ignored = [...testFiles, ...config.ignore ?? []];
2081
+ this.extraFolders = config.layout?.extraFolders ?? {};
2082
+ this.contextMap = config.contextMap === void 0 ? void 0 : this.validContextMap(config.contextMap, Object.keys(config.boundedContexts));
2083
+ this.rules = config.rules;
2084
+ const sharedKernel = config.sharedKernel ?? "shared-kernel";
2085
+ this.contextFolders = [...Object.entries(config.boundedContexts).map(([name, folder]) => ({
2086
+ dir: resolve(this.rootDir, folder),
2087
+ isSharedKernel: false,
2088
+ name
2089
+ })), {
2090
+ dir: resolve(this.rootDir, sharedKernel),
2091
+ isSharedKernel: true,
2092
+ name: "shared kernel"
2093
+ }];
2094
+ }
2095
+ validContextMap(upstreams, contexts) {
2096
+ const map = new ContextMap(upstreams);
2097
+ const unknown = map.contexts.filter((name) => !contexts.includes(name));
2098
+ if (unknown.length > 0) throw new Error(`contextMap names ${unknown.join(", ")}, which boundedContexts does not declare.`);
2099
+ const cycle = map.cycle();
2100
+ if (cycle !== void 0) throw new Error(`contextMap has a cycle: ${cycle.join(" → ")}. Two contexts that depend on each other can no longer change alone: reverse one dependency.`);
2101
+ return map;
2102
+ }
2103
+ isIgnored(path) {
2104
+ const projectPath = relative(this.projectDir, path).split(sep).join("/");
2105
+ return this.ignored.some((glob) => matchesGlob(projectPath, glob));
2106
+ }
2107
+ severityOf(rule) {
2108
+ return this.rules?.[rule] ?? "error";
2109
+ }
2110
+ };
2111
+ //#endregion
2112
+ //#region src/config/config-loader.ts
2113
+ const packageDependencies = z.record(z.string(), z.union([z.literal(true), z.array(z.string())]));
2114
+ const ruleSetting = z.enum([
2115
+ "error",
2116
+ "warn",
2117
+ "info",
2118
+ "off"
2119
+ ]);
2120
+ const schema = z.strictObject({
2121
+ applicationDependencies: packageDependencies.exactOptional(),
2122
+ boundedContexts: z.record(z.string(), z.string()),
2123
+ compositionRoot: z.string().exactOptional(),
2124
+ contextMap: z.record(z.string(), z.array(z.string())).exactOptional(),
2125
+ domainDependencies: packageDependencies.exactOptional(),
2126
+ ignore: z.array(z.string()).exactOptional(),
2127
+ layout: z.strictObject({ extraFolders: z.strictObject({
2128
+ application: z.array(z.string()).exactOptional(),
2129
+ domain: z.array(z.string()).exactOptional()
2130
+ }).exactOptional() }).exactOptional(),
2131
+ root: z.string(),
2132
+ rules: z.strictObject(Object.fromEntries(new RuleRegistry().ids.map((id) => [id, ruleSetting.exactOptional()]))).exactOptional(),
2133
+ sharedKernel: z.string().exactOptional(),
2134
+ tsconfig: z.string().exactOptional()
2135
+ });
2136
+ var ConfigLoader = class ConfigLoader {
2137
+ static fileName = "alveolus.config.ts";
2138
+ async load(projectDir, configFile = ConfigLoader.fileName) {
2139
+ const path = resolve(projectDir, configFile);
2140
+ if (!existsSync(path)) throw new Error(`No configuration found at ${path}.`);
2141
+ const exported = await createJiti(import.meta.url).import(path, { default: true });
2142
+ return new Config(this.validate(exported, path), projectDir);
2143
+ }
2144
+ validate(exported, path) {
2145
+ const parsed = schema.safeParse(exported);
2146
+ if (!parsed.success) throw new Error(`Invalid configuration in ${path}:\n${z.prettifyError(parsed.error)}`);
2147
+ return parsed.data;
2148
+ }
2149
+ };
2150
+ //#endregion
2151
+ //#region src/importer/importer.ts
2152
+ var Importer = class {};
2153
+ //#endregion
2154
+ //#region src/importer/ts-morph/package-names.ts
2155
+ var PackageNames = class {
2156
+ fileSystem;
2157
+ cache = /* @__PURE__ */ new Map();
2158
+ constructor(fileSystem) {
2159
+ this.fileSystem = fileSystem;
2160
+ }
2161
+ ofFile(path) {
2162
+ return this.ofDirectory(dirname(path));
2163
+ }
2164
+ ofDirectory(directory) {
2165
+ if (this.cache.has(directory)) return this.cache.get(directory);
2166
+ const name = this.readManifest(directory) ?? this.ofParent(directory);
2167
+ this.cache.set(directory, name);
2168
+ return name;
2169
+ }
2170
+ ofParent(directory) {
2171
+ const parent = dirname(directory);
2172
+ return parent === directory ? void 0 : this.ofDirectory(parent);
2173
+ }
2174
+ readManifest(directory) {
2175
+ const manifest = join(directory, "package.json");
2176
+ if (!this.fileSystem.fileExistsSync(manifest)) return;
2177
+ const content = JSON.parse(this.fileSystem.readFileSync(manifest));
2178
+ return typeof content.name === "string" ? content.name : void 0;
2179
+ }
2180
+ };
2181
+ //#endregion
2182
+ //#region src/importer/ts-morph/readers/class-reader.ts
2183
+ var ClassReader = class {
2184
+ types;
2185
+ constructor(types) {
2186
+ this.types = types;
2187
+ }
2188
+ read(declaration) {
2189
+ return new ClassDeclaration({
2190
+ heritage: this.heritageOf(declaration),
2191
+ implemented: declaration.getImplements().map((clause) => this.types.namedTypeOf(clause.getType())),
2192
+ isAbstract: declaration.isAbstract(),
2193
+ line: declaration.getStartLineNumber(),
2194
+ members: declaration.getMembers().flatMap((member) => this.membersOf(member)),
2195
+ name: declaration.getName() ?? "default",
2196
+ type: this.types.classTypeOf(declaration.getType())
2197
+ });
2198
+ }
2199
+ membersOf(member) {
2200
+ if (Node.isConstructorDeclaration(member)) return this.constructorParametersOf(member);
2201
+ if (Node.isPropertyDeclaration(member)) {
2202
+ const type = member.getType();
2203
+ const [signature] = type.getCallSignatures();
2204
+ return [this.member(member, "field", {
2205
+ isCallable: signature !== void 0,
2206
+ returns: signature === void 0 ? void 0 : this.types.returnShapeOf(signature.getReturnType()),
2207
+ valueTypes: this.types.classTypesIn(type)
2208
+ })];
2209
+ }
2210
+ if (Node.isMethodDeclaration(member)) {
2211
+ const returnType = member.getReturnType();
2212
+ const parameterTypes = member.getParameters().flatMap((parameter) => this.types.classTypesIn(parameter.getType()));
2213
+ return [this.member(member, "method", {
2214
+ parameterTypes,
2215
+ returns: this.types.returnShapeOf(returnType),
2216
+ valueTypes: this.types.classTypesIn(returnType)
2217
+ })];
2218
+ }
2219
+ if (Node.isGetAccessorDeclaration(member)) {
2220
+ const returnType = member.getReturnType();
2221
+ return [this.member(member, "getter", {
2222
+ returns: this.types.returnShapeOf(returnType),
2223
+ valueTypes: this.types.classTypesIn(returnType)
2224
+ })];
2225
+ }
2226
+ if (Node.isSetAccessorDeclaration(member)) return [this.member(member, "setter", {})];
2227
+ return [];
2228
+ }
2229
+ member(member, kind, types) {
2230
+ const name = Node.hasName(member) ? member.getName() : "";
2231
+ const isStatic = Node.isStaticable(member) && member.isStatic();
2232
+ const scope = Node.isScoped(member) ? member.getScope() : Scope.Public;
2233
+ return new Member({
2234
+ isCallable: types.isCallable ?? false,
2235
+ isParameterProperty: false,
2236
+ isStatic,
2237
+ kind,
2238
+ line: member.getStartLineNumber(),
2239
+ name,
2240
+ parameterTypes: types.parameterTypes ?? [],
2241
+ returns: types.returns,
2242
+ valueTypes: types.valueTypes ?? [],
2243
+ visibility: name.startsWith("#") ? "private" : this.visibilityOf(scope)
2244
+ });
2245
+ }
2246
+ constructorParametersOf(declaration) {
2247
+ return declaration.getParameters().map((parameter) => this.constructorParameter(parameter));
2248
+ }
2249
+ constructorParameter(parameter) {
2250
+ return new Member({
2251
+ isCallable: false,
2252
+ isParameterProperty: parameter.isParameterProperty(),
2253
+ isStatic: false,
2254
+ kind: "constructor parameter",
2255
+ line: parameter.getStartLineNumber(),
2256
+ name: parameter.getName(),
2257
+ parameterTypes: [],
2258
+ returns: void 0,
2259
+ valueTypes: this.types.classTypesIn(parameter.getType()),
2260
+ visibility: this.visibilityOf(parameter.getScope() ?? Scope.Public)
2261
+ });
2262
+ }
2263
+ heritageOf(declaration) {
2264
+ const clause = declaration.getExtends();
2265
+ if (clause === void 0) return;
2266
+ const parameters = declaration.getBaseClass()?.getTypeParameters() ?? [];
2267
+ const typeArguments = clause.getTypeArguments().map((argument, index) => ({
2268
+ line: argument.getStartLineNumber(),
2269
+ parameter: parameters[index]?.getName() ?? argument.getText(),
2270
+ types: this.types.classTypesIn(argument.getType())
2271
+ }));
2272
+ return {
2273
+ isByName: this.namesAClass(clause.getExpression()),
2274
+ typeArguments
2275
+ };
2276
+ }
2277
+ namesAClass(expression) {
2278
+ if (!Node.isIdentifier(expression) && !Node.isPropertyAccessExpression(expression)) return false;
2279
+ const symbol = expression.getSymbol();
2280
+ const declarations = (symbol?.getAliasedSymbol() ?? symbol)?.getDeclarations() ?? [];
2281
+ return declarations.length === 0 || declarations.some((candidate) => Node.isClassDeclaration(candidate));
2282
+ }
2283
+ visibilityOf(scope) {
2284
+ if (scope === Scope.Private) return "private";
2285
+ if (scope === Scope.Protected) return "protected";
2286
+ return "public";
2287
+ }
2288
+ };
2289
+ //#endregion
2290
+ //#region src/importer/ts-morph/readers/dependency-reader.ts
2291
+ const everything = ["*"];
2292
+ var DependencyReader = class {
2293
+ project;
2294
+ scope;
2295
+ constructor(project, scope) {
2296
+ this.project = project;
2297
+ this.scope = scope;
2298
+ }
2299
+ read(file, globals) {
2300
+ const dependencies = [];
2301
+ for (const reference of this.referencesOf(file)) dependencies.push(new Dependency(reference.line, reference.form, reference.specifier ?? reference.text, reference.names, this.targetOf(reference, file)));
2302
+ for (const global of globals) if (global.origin === "project") dependencies.push(new Dependency(global.line, "global", global.declaredIn, [global.name], this.fileTarget(normalize(global.declaredIn))));
2303
+ return dependencies.sort((left, right) => left.line - right.line);
2304
+ }
2305
+ referencesOf(file) {
2306
+ const references = [];
2307
+ for (const declaration of file.getImportDeclarations()) references.push(this.fromDeclaration(declaration, "import"));
2308
+ for (const declaration of file.getExportDeclarations()) if (declaration.hasModuleSpecifier()) references.push(this.fromDeclaration(declaration, "re-export"));
2309
+ for (const declaration of file.getDescendantsOfKind(SyntaxKind.ImportEqualsDeclaration)) references.push(...this.fromImportEquals(declaration));
2310
+ for (const node of file.getDescendantsOfKind(SyntaxKind.ImportType)) references.push(this.fromImportType(node));
2311
+ for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) references.push(...this.fromCall(call));
2312
+ return references;
2313
+ }
2314
+ fromDeclaration(declaration, form) {
2315
+ const specifier = declaration.getModuleSpecifierValue() ?? "";
2316
+ return {
2317
+ form,
2318
+ line: declaration.getStartLineNumber(),
2319
+ names: this.declaredNames(declaration),
2320
+ specifier,
2321
+ text: specifier
2322
+ };
2323
+ }
2324
+ declaredNames(declaration) {
2325
+ if (declaration.isKind(SyntaxKind.ExportDeclaration)) return declaration.isNamespaceExport() ? [...everything] : declaration.getNamedExports().map((named) => named.getName());
2326
+ const names = declaration.getNamedImports().map((named) => named.getName());
2327
+ if (declaration.getDefaultImport() !== void 0) names.unshift("default");
2328
+ if (declaration.getNamespaceImport() !== void 0) names.unshift("*");
2329
+ return names;
2330
+ }
2331
+ fromImportEquals(declaration) {
2332
+ const reference = declaration.getModuleReference();
2333
+ if (!reference.isKind(SyntaxKind.ExternalModuleReference)) return [];
2334
+ return [this.reference(declaration, "require", reference.getExpressionOrThrow(), everything)];
2335
+ }
2336
+ fromImportType(node) {
2337
+ const argument = node.getArgument();
2338
+ const literal = argument.isKind(SyntaxKind.LiteralType) ? argument.getLiteral() : argument;
2339
+ const qualifier = node.getQualifier();
2340
+ const names = qualifier === void 0 ? everything : [qualifier.getText().split(".")[0] ?? "*"];
2341
+ return this.reference(node, "inline type", literal, names);
2342
+ }
2343
+ fromCall(call) {
2344
+ const callee = call.getExpression();
2345
+ const [argument] = call.getArguments();
2346
+ if (argument === void 0) return [];
2347
+ if (callee.isKind(SyntaxKind.ImportKeyword)) return [this.reference(call, "dynamic import", argument, everything)];
2348
+ if (callee.isKind(SyntaxKind.Identifier) && callee.getText() === "require") return [this.reference(call, "require", argument, everything)];
2349
+ return [];
2350
+ }
2351
+ reference(node, form, specifierNode, names) {
2352
+ const isLiteral = specifierNode.isKind(SyntaxKind.StringLiteral) || specifierNode.isKind(SyntaxKind.NoSubstitutionTemplateLiteral);
2353
+ return {
2354
+ form,
2355
+ line: node.getStartLineNumber(),
2356
+ names,
2357
+ specifier: isLiteral ? specifierNode.getLiteralValue() : void 0,
2358
+ text: specifierNode.getText()
2359
+ };
2360
+ }
2361
+ targetOf(reference, file) {
2362
+ const directory = file.getDirectoryPath();
2363
+ if (reference.specifier === void 0) return {
2364
+ kind: "file",
2365
+ path: normalize(join(directory, reference.text)),
2366
+ visibility: "unresolved"
2367
+ };
2368
+ const resolved = this.resolve(reference.specifier, file);
2369
+ if (resolved !== void 0 && this.isInsideProject(resolved)) return this.fileTarget(resolved);
2370
+ if (reference.specifier.startsWith(".")) return {
2371
+ kind: "file",
2372
+ path: normalize(join(directory, reference.specifier)),
2373
+ visibility: "unresolved"
2374
+ };
2375
+ return {
2376
+ kind: "package",
2377
+ name: this.packageNameOf(reference.specifier)
2378
+ };
2379
+ }
2380
+ fileTarget(path) {
2381
+ return {
2382
+ kind: "file",
2383
+ path,
2384
+ visibility: this.scope.isIgnored(path) ? "ignored" : "analysed"
2385
+ };
2386
+ }
2387
+ resolve(specifier, file) {
2388
+ const resolved = ts.resolveModuleName(specifier, file.getFilePath(), this.project.getCompilerOptions(), this.project.getModuleResolutionHost()).resolvedModule?.resolvedFileName;
2389
+ return resolved === void 0 ? void 0 : normalize(resolved);
2390
+ }
2391
+ packageNameOf(specifier) {
2392
+ const segments = specifier.split("/");
2393
+ const length = specifier.startsWith("@") ? 2 : 1;
2394
+ return segments.slice(0, length).join("/");
2395
+ }
2396
+ isInsideProject(path) {
2397
+ const offset = relative(this.scope.projectDir, path);
2398
+ return offset !== "" && !offset.startsWith("..") && !isAbsolute(offset) && !offset.split(/[\\/]/).includes("node_modules");
2399
+ }
2400
+ };
2401
+ //#endregion
2402
+ //#region src/importer/ts-morph/readers/disable-reader.ts
2403
+ const marker = /^\s*\/\/\s*alveolus-disable-next-line\b\s*(.*?)\s*$/;
2404
+ var DisableReader = class {
2405
+ read(file) {
2406
+ const comments = [];
2407
+ const lines = file.getFullText().split(/\r?\n/);
2408
+ for (const [index, line] of lines.entries()) {
2409
+ const text = marker.exec(line)?.[1];
2410
+ if (text !== void 0) comments.push(new DisableComment(index + 1, text));
2411
+ }
2412
+ return comments;
2413
+ }
2414
+ };
2415
+ //#endregion
2416
+ //#region src/importer/ts-morph/readers/global-reader.ts
2417
+ const ecmaScriptLibrary = /^lib\.(es|decorators).*\.d\.ts$/;
2418
+ var GlobalReader = class {
2419
+ projectDir;
2420
+ constructor(projectDir) {
2421
+ this.projectDir = projectDir;
2422
+ }
2423
+ read(file) {
2424
+ const references = [];
2425
+ for (const identifier of file.getDescendantsOfKind(SyntaxKind.Identifier)) {
2426
+ const declaringFile = this.globalDeclaringFileOf(identifier);
2427
+ if (declaringFile === void 0 || declaringFile === file) continue;
2428
+ const origin = this.originOf(declaringFile.getFilePath());
2429
+ const effect = origin === "ecmascript" ? this.effectOf(identifier) : void 0;
2430
+ references.push({
2431
+ declaredIn: declaringFile.getFilePath(),
2432
+ effect: effect?.effect,
2433
+ line: identifier.getStartLineNumber(),
2434
+ name: effect?.name ?? identifier.getText(),
2435
+ origin
2436
+ });
2437
+ }
2438
+ return references;
2439
+ }
2440
+ globalDeclaringFileOf(identifier) {
2441
+ const symbol = identifier.getSymbol();
2442
+ const declaration = symbol?.getValueDeclaration() ?? symbol?.getDeclarations()[0];
2443
+ if (declaration === void 0 || !this.isGlobalScope(declaration)) return;
2444
+ return declaration.getSourceFile();
2445
+ }
2446
+ isGlobalScope(declaration) {
2447
+ const container = (Node.isVariableDeclaration(declaration) ? declaration.getVariableStatement() : declaration)?.getParent();
2448
+ if (Node.isSourceFile(container)) return !ts.isExternalModule(container.compilerNode);
2449
+ const module = container?.getParent();
2450
+ return Node.isModuleBlock(container) && Node.isModuleDeclaration(module) && module.getDeclarationKind() === ModuleDeclarationKind.Global;
2451
+ }
2452
+ originOf(path) {
2453
+ if (this.isInsideProject(path)) return "project";
2454
+ return ecmaScriptLibrary.test(basename(path)) ? "ecmascript" : "host";
2455
+ }
2456
+ effectOf(identifier) {
2457
+ const name = identifier.getText();
2458
+ const parent = identifier.getParent();
2459
+ if (name === "Date") {
2460
+ if (Node.isNewExpression(parent) && parent.getExpression() === identifier && parent.getArguments().length === 0) return {
2461
+ effect: "clock",
2462
+ name: "new Date()"
2463
+ };
2464
+ if (Node.isCallExpression(parent) && parent.getExpression() === identifier) return {
2465
+ effect: "clock",
2466
+ name: "Date()"
2467
+ };
2468
+ if (this.isMemberAccess(parent, identifier, "now")) return {
2469
+ effect: "clock",
2470
+ name: "Date.now"
2471
+ };
2472
+ }
2473
+ if (name === "Math" && this.isMemberAccess(parent, identifier, "random")) return {
2474
+ effect: "randomness",
2475
+ name: "Math.random"
2476
+ };
2477
+ }
2478
+ isMemberAccess(parent, object, member) {
2479
+ return Node.isPropertyAccessExpression(parent) && parent.getExpression() === object && parent.getName() === member;
2480
+ }
2481
+ isInsideProject(path) {
2482
+ const offset = relative(this.projectDir, path);
2483
+ return offset !== "" && !offset.startsWith("..") && !isAbsolute(offset) && !offset.split(/[\\/]/).includes("node_modules");
2484
+ }
2485
+ };
2486
+ //#endregion
2487
+ //#region src/importer/ts-morph/readers/statement-reader.ts
2488
+ const declarative = /* @__PURE__ */ new Set([
2489
+ SyntaxKind.ImportDeclaration,
2490
+ SyntaxKind.ImportEqualsDeclaration,
2491
+ SyntaxKind.ExportDeclaration,
2492
+ SyntaxKind.ClassDeclaration,
2493
+ SyntaxKind.InterfaceDeclaration,
2494
+ SyntaxKind.TypeAliasDeclaration,
2495
+ SyntaxKind.EmptyStatement
2496
+ ]);
2497
+ const literals = /* @__PURE__ */ new Set([
2498
+ SyntaxKind.StringLiteral,
2499
+ SyntaxKind.NumericLiteral,
2500
+ SyntaxKind.BigIntLiteral,
2501
+ SyntaxKind.NoSubstitutionTemplateLiteral,
2502
+ SyntaxKind.RegularExpressionLiteral,
2503
+ SyntaxKind.TrueKeyword,
2504
+ SyntaxKind.FalseKeyword,
2505
+ SyntaxKind.NullKeyword
2506
+ ]);
2507
+ var StatementReader = class {
2508
+ read(file) {
2509
+ return file.getStatements().flatMap((statement) => this.declarationsOf(statement));
2510
+ }
2511
+ declarationsOf(statement) {
2512
+ if (declarative.has(statement.getKind())) return [];
2513
+ const line = statement.getStartLineNumber();
2514
+ if (Node.isFunctionDeclaration(statement)) return [new TopLevelStatement("function", statement.getName() ?? "default", line)];
2515
+ if (Node.isEnumDeclaration(statement)) return [new TopLevelStatement("enum", statement.getName(), line)];
2516
+ if (Node.isModuleDeclaration(statement)) return statement.getDeclarationKind() === ModuleDeclarationKind.Global ? [] : [new TopLevelStatement("namespace", statement.getName(), line)];
2517
+ if (Node.isVariableStatement(statement)) return this.variablesOf(statement);
2518
+ if (Node.isExportAssignment(statement)) return this.isData(statement.getExpression()) ? [] : [new TopLevelStatement("computed constant", "default", line)];
2519
+ return [new TopLevelStatement("statement", statement.getKindName(), line)];
2520
+ }
2521
+ variablesOf(statement) {
2522
+ const isConstant = statement.getDeclarationKind() === VariableDeclarationKind.Const;
2523
+ const declarations = [];
2524
+ for (const variable of statement.getDeclarations()) {
2525
+ const kind = isConstant ? this.constantKindOf(variable) : "mutable variable";
2526
+ if (kind !== void 0) declarations.push(new TopLevelStatement(kind, variable.getName(), variable.getStartLineNumber()));
2527
+ }
2528
+ return declarations;
2529
+ }
2530
+ constantKindOf(variable) {
2531
+ const initializer = variable.getInitializer();
2532
+ if (initializer === void 0) return;
2533
+ const value = this.unwrapped(initializer);
2534
+ if (Node.isClassExpression(value)) return "class expression";
2535
+ if (value.getType().getCallSignatures().length > 0) return "function";
2536
+ return this.isData(initializer) ? void 0 : "computed constant";
2537
+ }
2538
+ isData(expression) {
2539
+ const value = this.unwrapped(expression);
2540
+ if (literals.has(value.getKind())) return true;
2541
+ if (Node.isPrefixUnaryExpression(value)) return this.isData(value.getOperand());
2542
+ if (Node.isBinaryExpression(value)) return this.isData(value.getLeft()) && this.isData(value.getRight());
2543
+ if (Node.isTemplateExpression(value)) return value.getTemplateSpans().every((span) => this.isData(span.getExpression()));
2544
+ if (Node.isArrayLiteralExpression(value)) return value.getElements().every((element) => this.isData(Node.isSpreadElement(element) ? element.getExpression() : element));
2545
+ if (Node.isObjectLiteralExpression(value)) return value.getProperties().every((property) => this.isDataProperty(property));
2546
+ if (Node.isIdentifier(value) || Node.isPropertyAccessExpression(value)) return value.getType().getCallSignatures().length === 0;
2547
+ return false;
2548
+ }
2549
+ isDataProperty(property) {
2550
+ if (Node.isPropertyAssignment(property)) {
2551
+ const initializer = property.getInitializer();
2552
+ return initializer !== void 0 && this.isData(initializer);
2553
+ }
2554
+ if (Node.isShorthandPropertyAssignment(property)) return property.getNameNode().getType().getCallSignatures().length === 0;
2555
+ if (Node.isSpreadAssignment(property)) return this.isData(property.getExpression());
2556
+ return false;
2557
+ }
2558
+ unwrapped(expression) {
2559
+ if (Node.isParenthesizedExpression(expression) || Node.isAsExpression(expression) || Node.isSatisfiesExpression(expression) || Node.isNonNullExpression(expression)) return this.unwrapped(expression.getExpression());
2560
+ if (Node.isTypeAssertion(expression)) return this.unwrapped(expression.getExpression());
2561
+ return expression;
2562
+ }
2563
+ };
2564
+ //#endregion
2565
+ //#region src/importer/ts-morph/readers/throw-reader.ts
2566
+ var ThrowReader = class {
2567
+ read(file) {
2568
+ const throws = [];
2569
+ for (const statement of file.getDescendantsOfKind(SyntaxKind.ThrowStatement)) throws.push(new Throw(statement.getStartLineNumber(), "throw"));
2570
+ for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) if (call.getExpression().getText() === "Promise.reject") throws.push(new Throw(call.getStartLineNumber(), "Promise.reject"));
2571
+ return throws.sort((left, right) => left.line - right.line);
2572
+ }
2573
+ };
2574
+ //#endregion
2575
+ //#region src/importer/ts-morph/readers/type-reader.ts
2576
+ var TypeReader = class {
2577
+ packageNames;
2578
+ constructor(packageNames) {
2579
+ this.packageNames = packageNames;
2580
+ }
2581
+ classTypesIn(type) {
2582
+ const found = [];
2583
+ this.collectClassTypes(type, found, /* @__PURE__ */ new Set());
2584
+ return found;
2585
+ }
2586
+ classTypeOf(type) {
2587
+ const symbol = type.getSymbol();
2588
+ const declaredIn = symbol?.getDeclarations()[0]?.getSourceFile().getFilePath();
2589
+ return new ClassType(symbol?.getName() ?? type.getText(), declaredIn === void 0 ? void 0 : normalize(declaredIn), this.lineageOf(type));
2590
+ }
2591
+ namedTypeOf(type) {
2592
+ const symbol = type.getSymbol() ?? type.getAliasSymbol();
2593
+ return symbol === void 0 ? {
2594
+ name: type.getText(),
2595
+ packageName: void 0
2596
+ } : this.namedSymbol(symbol);
2597
+ }
2598
+ returnShapeOf(type) {
2599
+ if (type.getSymbol()?.getName() === "Promise") {
2600
+ const [inner] = type.getTypeArguments();
2601
+ if (inner !== void 0) return this.returnShapeOf(inner);
2602
+ }
2603
+ const alias = type.getAliasSymbol();
2604
+ const members = type.isUnion() ? type.getUnionTypes() : [type];
2605
+ return {
2606
+ alias: alias === void 0 ? void 0 : this.namedSymbol(alias),
2607
+ union: members.map((member) => this.namedTypeOf(member))
2608
+ };
2609
+ }
2610
+ lineageOf(type) {
2611
+ const classType = type.getTargetType() ?? type;
2612
+ const symbol = classType.getSymbol();
2613
+ if (symbol === void 0 || !classType.isClass()) return [];
2614
+ const lineage = [this.namedSymbol(symbol)];
2615
+ for (const base of classType.getBaseTypes()) lineage.push(...this.lineageOf(base));
2616
+ return lineage;
2617
+ }
2618
+ namedSymbol(symbol) {
2619
+ const declaration = symbol.getDeclarations()[0];
2620
+ const packageName = declaration === void 0 ? void 0 : this.packageNames.ofFile(declaration.getSourceFile().getFilePath());
2621
+ return {
2622
+ name: symbol.getName(),
2623
+ packageName
2624
+ };
2625
+ }
2626
+ collectClassTypes(type, found, seen) {
2627
+ if (seen.has(type.compilerType)) return;
2628
+ seen.add(type.compilerType);
2629
+ if (type.isUnion()) {
2630
+ for (const member of type.getUnionTypes()) this.collectClassTypes(member, found, seen);
2631
+ return;
2632
+ }
2633
+ if (type.isIntersection()) {
2634
+ for (const member of type.getIntersectionTypes()) this.collectClassTypes(member, found, seen);
2635
+ return;
2636
+ }
2637
+ if (type.isClass()) found.push(this.classTypeOf(type));
2638
+ for (const inner of this.innerTypesOf(type)) this.collectClassTypes(inner, found, seen);
2639
+ }
2640
+ innerTypesOf(type) {
2641
+ const inner = [
2642
+ ...type.getTypeArguments(),
2643
+ ...type.getAliasTypeArguments(),
2644
+ ...type.getTupleElements()
2645
+ ];
2646
+ for (const signature of type.getCallSignatures()) inner.push(signature.getReturnType());
2647
+ if (this.isDeclaredByProject(type)) for (const property of type.getProperties()) {
2648
+ const declaration = property.getDeclarations()[0];
2649
+ if (declaration !== void 0) inner.push(declaration.getType());
2650
+ }
2651
+ return inner;
2652
+ }
2653
+ isDeclaredByProject(type) {
2654
+ if (!type.isObject() || type.isClass()) return false;
2655
+ const declaration = (type.getSymbol() ?? type.getAliasSymbol())?.getDeclarations()[0];
2656
+ if (declaration === void 0) return false;
2657
+ const file = declaration.getSourceFile();
2658
+ return !file.isInNodeModules() && !file.isDeclarationFile();
2659
+ }
2660
+ };
2661
+ //#endregion
2662
+ //#region src/importer/ts-morph/ts-morph-importer.ts
2663
+ var TsMorphImporter = class TsMorphImporter extends Importer {
2664
+ sources;
2665
+ classes;
2666
+ statements = new StatementReader();
2667
+ disables = new DisableReader();
2668
+ throws = new ThrowReader();
2669
+ constructor(sources) {
2670
+ super();
2671
+ this.sources = sources;
2672
+ this.classes = new ClassReader(new TypeReader(new PackageNames(sources.getFileSystem())));
2673
+ }
2674
+ static fromTsConfig(tsConfigFilePath) {
2675
+ if (!existsSync(tsConfigFilePath)) throw new Error(`No TypeScript configuration found at ${tsConfigFilePath}: pass --tsconfig, or set tsconfig in alveolus.config.ts.`);
2676
+ return new TsMorphImporter(new Project({ tsConfigFilePath }));
2677
+ }
2678
+ resolves(packageName, scope) {
2679
+ const from = join(scope.rootDir, "index.ts");
2680
+ return ts.resolveModuleName(packageName, from, this.sources.getCompilerOptions(), this.sources.getModuleResolutionHost()).resolvedModule !== void 0;
2681
+ }
2682
+ read(scope) {
2683
+ const dependencies = new DependencyReader(this.sources, scope);
2684
+ const globals = new GlobalReader(scope.projectDir);
2685
+ const files = [];
2686
+ for (const file of this.sources.getSourceFiles()) if (this.isInside(scope.rootDir, file.getFilePath()) && !scope.isIgnored(file.getFilePath())) files.push(this.readFile(file, dependencies, globals));
2687
+ return new Project$1(scope.projectDir, files);
2688
+ }
2689
+ readFile(file, dependencies, globals) {
2690
+ const globalReferences = globals.read(file);
2691
+ return new SourceFile({
2692
+ classes: file.getClasses().map((declaration) => this.classes.read(declaration)),
2693
+ dependencies: dependencies.read(file, globalReferences),
2694
+ disables: this.disables.read(file),
2695
+ globals: this.globalUsesOf(globalReferences),
2696
+ path: normalize(file.getFilePath()),
2697
+ statements: this.statements.read(file),
2698
+ text: file.getFullText(),
2699
+ throws: this.throws.read(file)
2700
+ });
2701
+ }
2702
+ globalUsesOf(references) {
2703
+ const uses = [];
2704
+ for (const reference of references) if (reference.origin !== "project") uses.push(new GlobalUse(reference.line, reference.name, reference.origin, reference.effect));
2705
+ return uses;
2706
+ }
2707
+ isInside(directory, path) {
2708
+ const offset = relative(directory, path);
2709
+ return offset !== "" && !offset.startsWith("..") && !isAbsolute(offset) && !offset.split(/[\\/]/).includes("node_modules");
2710
+ }
2711
+ };
2712
+ //#endregion
2713
+ //#region src/cli/cli.ts
2714
+ var Cli = class {
2715
+ stdout;
2716
+ stderr;
2717
+ cwd;
2718
+ colored;
2719
+ exitCode = 0;
2720
+ constructor(stdout, stderr, cwd, colored = false) {
2721
+ this.stdout = stdout;
2722
+ this.stderr = stderr;
2723
+ this.cwd = cwd;
2724
+ this.colored = colored;
2725
+ }
2726
+ async run(args) {
2727
+ this.exitCode = 0;
2728
+ try {
2729
+ await this.program().parseAsync([...args], { from: "user" });
2730
+ } catch (error) {
2731
+ return this.fail(error);
2732
+ }
2733
+ return this.exitCode;
2734
+ }
2735
+ program() {
2736
+ const program = new Command("alveolus").exitOverride().configureOutput({
2737
+ writeErr: (text) => this.stderr.write(text),
2738
+ writeOut: (text) => this.stdout.write(text)
2739
+ });
2740
+ const arch = program.command("arch").description("Check the architecture of a Domain-Driven Design project");
2741
+ this.withOptions(arch.command("check").description("Report the violations that are not in the baseline")).action((options) => this.check(options));
2742
+ this.withOptions(arch.command("baseline").description(`Write the current violations to ${Baseline.fileName}`)).option("--allow-growth", "write the baseline even when it holds more entries than before").action((options) => this.baseline(options));
2743
+ return program;
2744
+ }
2745
+ withOptions(command) {
2746
+ return command.option("--project <dir>", "project directory", ".").option("--config <file>", "configuration file", ConfigLoader.fileName).option("--tsconfig <file>", "TypeScript configuration, tsconfig.json or the one set in the configuration file").addOption(new Option("--format <format>", "how violations are printed").choices([
2747
+ "text",
2748
+ "json",
2749
+ "sarif"
2750
+ ]).default("text"));
2751
+ }
2752
+ async check(options) {
2753
+ const { config, registry, outcome } = await this.analyze(options);
2754
+ const baseline = await Baseline.load(join(config.projectDir, Baseline.fileName));
2755
+ const fresh = baseline.newViolations(outcome.violations);
2756
+ const stale = baseline.staleEntries(outcome.violations);
2757
+ if (baseline.outdatedEntries > 0) this.warnOutdated(baseline.outdatedEntries);
2758
+ if (stale > 0) this.stderr.write(`${stale} ${stale === 1 ? "entry" : "entries"} of the baseline match nothing any more: run alveolus arch baseline to drop ${stale === 1 ? "it" : "them"}.\n`);
2759
+ const report = new Report({
2760
+ baselined: outcome.violations.length - fresh.length,
2761
+ files: outcome.files,
2762
+ stale,
2763
+ suppressed: outcome.suppressed,
2764
+ violations: fresh
2765
+ }, this.colored);
2766
+ this.stdout.write(this.render(report, options.format, registry));
2767
+ this.exitCode = fresh.some((violation) => violation.severity === "error") ? 1 : 0;
2768
+ }
2769
+ render(report, format, registry) {
2770
+ if (format === "json") return report.json();
2771
+ if (format === "sarif") return report.sarif(registry.rules.map((rule) => rule.meta));
2772
+ return report.text();
2773
+ }
2774
+ warnOutdated(count) {
2775
+ const entries = count === 1 ? "1 entry of the baseline has no fingerprint and matches" : `${count} entries of the baseline have no fingerprint and match`;
2776
+ this.stderr.write(`${entries} nothing: run alveolus arch baseline to write ${Baseline.fileName} again.\n`);
2777
+ }
2778
+ async baseline(options) {
2779
+ const { config, outcome } = await this.analyze(options);
2780
+ const path = join(config.projectDir, Baseline.fileName);
2781
+ const previous = (await Baseline.load(path)).size;
2782
+ if (existsSync(path) && outcome.violations.length > previous && options.allowGrowth !== true) {
2783
+ this.stderr.write(`The baseline would grow from ${previous} to ${outcome.violations.length} entries: fix the new violations, or pass --allow-growth.\n`);
2784
+ this.exitCode = 1;
2785
+ return;
2786
+ }
2787
+ await Baseline.of(outcome.violations).save(path);
2788
+ this.stdout.write(`${outcome.violations.length} violations written to ${Baseline.fileName}\n`);
2789
+ }
2790
+ async analyze(options) {
2791
+ const projectDir = resolve(this.cwd, options.project);
2792
+ const config = await new ConfigLoader().load(projectDir, options.config);
2793
+ const tsConfigPath = options.tsconfig === void 0 ? config.tsConfigPath : resolve(config.projectDir, options.tsconfig);
2794
+ const registry = new RuleRegistry();
2795
+ return {
2796
+ config,
2797
+ outcome: new Checker(TsMorphImporter.fromTsConfig(tsConfigPath), registry.rules).check(config),
2798
+ registry
2799
+ };
2800
+ }
2801
+ fail(error) {
2802
+ if (error instanceof CommanderError) return error.exitCode === 0 ? 0 : 2;
2803
+ this.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
2804
+ return 2;
2805
+ }
2806
+ };
2807
+ //#endregion
2808
+ export { Config as a, RuleRegistry as c, AllowedPackages as d, Baseline as f, ConfigLoader as i, Rule as l, TsMorphImporter as n, Report as o, Importer as r, Checker as s, Cli as t, Architecture as u };
2809
+
2810
+ //# sourceMappingURL=cli-CwPCGjDg.mjs.map