@alveolus/arch 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/dist/bin.mjs +4 -2
- package/dist/bin.mjs.map +1 -1
- package/dist/{cli-CwPCGjDg.mjs → docs-DsQHpTtV.mjs} +287 -38
- package/dist/docs-DsQHpTtV.mjs.map +1 -0
- package/dist/index.d.mts +90 -36
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/docs/core/application/command-handlers.md +617 -0
- package/docs/core/application/event-publishers.md +234 -0
- package/docs/core/application/event-translators.md +329 -0
- package/docs/core/application/index.md +99 -0
- package/docs/core/application/integration-events.md +277 -0
- package/docs/core/application/outbox.md +416 -0
- package/docs/core/application/query-handlers.md +292 -0
- package/docs/core/application/unit-of-work.md +352 -0
- package/docs/core/domain/aggregates.md +822 -0
- package/docs/core/domain/domain-errors.md +251 -0
- package/docs/core/domain/domain-events.md +292 -0
- package/docs/core/domain/domain-services.md +249 -0
- package/docs/core/domain/entities.md +431 -0
- package/docs/core/domain/index.md +93 -0
- package/docs/core/domain/ports.md +284 -0
- package/docs/core/domain/repositories.md +335 -0
- package/docs/core/domain/value-objects.md +425 -0
- package/docs/core/domain/views.md +265 -0
- package/docs/core/index.md +108 -0
- package/docs/core/strategic/anti-corruption-layers.md +349 -0
- package/docs/core/strategic/index.md +83 -0
- package/docs/core/strategic/open-host-services.md +287 -0
- package/docs/core/strategic/published-language.md +265 -0
- package/docs/core/utilities/result.md +413 -0
- package/docs/guide/agents.md +68 -0
- package/docs/guide/existing-project.md +105 -0
- package/docs/guide/getting-started.md +275 -0
- package/docs/guide/learning-path.md +123 -0
- package/docs/guide/project-layout.md +324 -0
- package/docs/guide/versioning.md +42 -0
- package/docs/integrations/index.md +112 -0
- package/docs/integrations/nestjs.md +169 -0
- package/docs/rules/index.md +183 -0
- package/docs/rules/layers/no-driving-shortcut.md +119 -0
- package/docs/rules/layers/no-impure-domain.md +189 -0
- package/docs/rules/layers/no-outward-import.md +184 -0
- package/docs/rules/layers/no-portless-adapter.md +123 -0
- package/docs/rules/strategic/no-cross-context-import.md +140 -0
- package/docs/rules/strategic/no-fat-shared-kernel.md +81 -0
- package/docs/rules/strategic/no-leaky-host-service.md +107 -0
- package/docs/rules/strategic/no-unmapped-context.md +111 -0
- package/docs/rules/tactical/no-aggregate-reference.md +139 -0
- package/docs/rules/tactical/no-foreign-command-dependency.md +119 -0
- package/docs/rules/tactical/no-foreign-query-dependency.md +106 -0
- package/docs/rules/tactical/no-loose-code.md +171 -0
- package/docs/rules/tactical/no-misplaced-class.md +146 -0
- package/docs/rules/tactical/no-public-field.md +113 -0
- package/docs/rules/tactical/no-stateful-service.md +102 -0
- package/docs/rules/tactical/no-thrown-failure.md +162 -0
- package/docs/rules/tooling/no-loose-disable.md +98 -0
- package/package.json +4 -3
- package/dist/cli-CwPCGjDg.mjs.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command, CommanderError, Option } from "commander";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
2
|
+
import { existsSync, globSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { basename, dirname, isAbsolute, join, matchesGlob, normalize, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { readFile, writeFile } from "node:fs/promises";
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
@@ -640,6 +640,7 @@ var LayerShape = class {
|
|
|
640
640
|
var Location = class {
|
|
641
641
|
area;
|
|
642
642
|
context;
|
|
643
|
+
subdomain;
|
|
643
644
|
layer;
|
|
644
645
|
folder;
|
|
645
646
|
foldersInLayer;
|
|
@@ -649,6 +650,7 @@ var Location = class {
|
|
|
649
650
|
constructor(props) {
|
|
650
651
|
this.area = props.area;
|
|
651
652
|
this.context = props.context;
|
|
653
|
+
this.subdomain = props.subdomain;
|
|
652
654
|
this.layer = props.layer;
|
|
653
655
|
this.folder = props.folder;
|
|
654
656
|
this.foldersInLayer = props.foldersInLayer ?? [];
|
|
@@ -659,6 +661,9 @@ var Location = class {
|
|
|
659
661
|
get isInBoundedContext() {
|
|
660
662
|
return this.area === "context";
|
|
661
663
|
}
|
|
664
|
+
get isInCoreDomain() {
|
|
665
|
+
return this.isInBoundedContext && this.subdomain === "core";
|
|
666
|
+
}
|
|
662
667
|
get isInSharedKernel() {
|
|
663
668
|
return this.area === "shared-kernel";
|
|
664
669
|
}
|
|
@@ -693,39 +698,44 @@ var Layout = class {
|
|
|
693
698
|
area: dirname(file) === this.settings.rootDir ? "root" : "outside",
|
|
694
699
|
fileName
|
|
695
700
|
});
|
|
696
|
-
const
|
|
697
|
-
const context = folder.name;
|
|
701
|
+
const inside = this.insideOf(folder);
|
|
698
702
|
const directories = this.directoriesBetween(folder.dir, file);
|
|
699
703
|
if (directories.length === 0) return new Location({
|
|
700
|
-
|
|
701
|
-
context,
|
|
704
|
+
...inside,
|
|
702
705
|
fileName,
|
|
703
706
|
isCompositionRoot: this.isCompositionRoot(fileName)
|
|
704
707
|
});
|
|
705
708
|
const layerIndex = this.layerIndexOf(directories, folder.isSharedKernel);
|
|
706
|
-
if (layerIndex === void 0)
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
709
|
+
if (layerIndex === void 0) {
|
|
710
|
+
const isFeatureRoot = folder.isSharedKernel && directories.length === 1;
|
|
711
|
+
return new Location({
|
|
712
|
+
...inside,
|
|
713
|
+
fileName,
|
|
714
|
+
isCompositionRoot: isFeatureRoot && this.isCompositionRoot(fileName)
|
|
715
|
+
});
|
|
716
|
+
}
|
|
712
717
|
const layer = directories[layerIndex];
|
|
713
718
|
if (layer === void 0 || !this.isLayer(layer)) return new Location({
|
|
714
|
-
|
|
715
|
-
context,
|
|
719
|
+
...inside,
|
|
716
720
|
fileName
|
|
717
721
|
});
|
|
718
722
|
const foldersInLayer = directories.slice(layerIndex + 1);
|
|
719
723
|
const subfolder = foldersInLayer.at(-1);
|
|
720
724
|
return new Location({
|
|
721
|
-
|
|
722
|
-
context,
|
|
725
|
+
...inside,
|
|
723
726
|
fileName,
|
|
724
727
|
foldersInLayer,
|
|
725
728
|
layer,
|
|
726
729
|
...subfolder === void 0 ? {} : { folder: subfolder }
|
|
727
730
|
});
|
|
728
731
|
}
|
|
732
|
+
insideOf(folder) {
|
|
733
|
+
return {
|
|
734
|
+
area: folder.isSharedKernel ? "shared-kernel" : "context",
|
|
735
|
+
context: folder.name,
|
|
736
|
+
...folder.subdomain === void 0 ? {} : { subdomain: folder.subdomain }
|
|
737
|
+
};
|
|
738
|
+
}
|
|
729
739
|
layerIndexOf(directories, isSharedKernel) {
|
|
730
740
|
if (this.isLayer(directories[0] ?? "")) return 0;
|
|
731
741
|
if (isSharedKernel && this.isLayer(directories[1] ?? "")) return 1;
|
|
@@ -901,6 +911,16 @@ var Wording = class {
|
|
|
901
911
|
//#region src/rules/framework/rule.ts
|
|
902
912
|
var Rule = class {
|
|
903
913
|
wording = new Wording();
|
|
914
|
+
filesOf(architecture) {
|
|
915
|
+
const files = [];
|
|
916
|
+
for (const file of architecture.files) if (this.checks(file, architecture)) files.push(file);
|
|
917
|
+
return files;
|
|
918
|
+
}
|
|
919
|
+
checks(file, architecture) {
|
|
920
|
+
if (this.meta.contexts === "every") return true;
|
|
921
|
+
const location = architecture.locationOf(file);
|
|
922
|
+
return !location.isInBoundedContext || location.isInCoreDomain;
|
|
923
|
+
}
|
|
904
924
|
finding(file, line, symbol, messageId, data = {}) {
|
|
905
925
|
return {
|
|
906
926
|
data,
|
|
@@ -916,7 +936,7 @@ var Rule = class {
|
|
|
916
936
|
var ClassRule = class extends Rule {
|
|
917
937
|
check(architecture) {
|
|
918
938
|
const findings = [];
|
|
919
|
-
for (const file of architecture
|
|
939
|
+
for (const file of this.filesOf(architecture)) for (const codeClass of file.classes) findings.push(...this.findingsFor(codeClass, file, architecture));
|
|
920
940
|
return findings;
|
|
921
941
|
}
|
|
922
942
|
};
|
|
@@ -925,7 +945,7 @@ var ClassRule = class extends Rule {
|
|
|
925
945
|
var ImportRule = class extends Rule {
|
|
926
946
|
check(architecture) {
|
|
927
947
|
const findings = [];
|
|
928
|
-
for (const file of architecture
|
|
948
|
+
for (const file of this.filesOf(architecture)) {
|
|
929
949
|
if (!this.appliesTo(file, architecture)) continue;
|
|
930
950
|
for (const dependency of file.dependencies) {
|
|
931
951
|
const finding = this.findingFor(dependency, file, architecture);
|
|
@@ -974,6 +994,7 @@ const shortcuts = [
|
|
|
974
994
|
];
|
|
975
995
|
var NoDrivingShortcutRule = class extends ImportRule {
|
|
976
996
|
meta = {
|
|
997
|
+
contexts: "core",
|
|
977
998
|
description: "A driving adapter importing a port, a repository, an aggregate, an entity or a domain service instead of calling a handler.",
|
|
978
999
|
id: "layers/no-driving-shortcut",
|
|
979
1000
|
messages: { shortcut: "Imports {name}, {kind}: a driving adapter calls the command and query handlers, never the ports, repositories or aggregates of the domain." }
|
|
@@ -1013,6 +1034,7 @@ var NoDrivingShortcutRule = class extends ImportRule {
|
|
|
1013
1034
|
//#region src/rules/layers/no-impure-domain.rule.ts
|
|
1014
1035
|
var NoImpureDomainRule = class extends ImportRule {
|
|
1015
1036
|
meta = {
|
|
1037
|
+
contexts: "core",
|
|
1016
1038
|
description: "The domain importing a framework, a database, another layer or a package not allowed, or using the host, the clock or randomness.",
|
|
1017
1039
|
id: "layers/no-impure-domain",
|
|
1018
1040
|
messages: {
|
|
@@ -1027,7 +1049,7 @@ var NoImpureDomainRule = class extends ImportRule {
|
|
|
1027
1049
|
};
|
|
1028
1050
|
check(architecture) {
|
|
1029
1051
|
const findings = super.check(architecture);
|
|
1030
|
-
for (const file of architecture
|
|
1052
|
+
for (const file of this.filesOf(architecture)) if (this.appliesTo(file, architecture)) findings.push(...this.impureGlobals(file));
|
|
1031
1053
|
return findings;
|
|
1032
1054
|
}
|
|
1033
1055
|
appliesTo(file, architecture) {
|
|
@@ -1074,6 +1096,7 @@ var NoImpureDomainRule = class extends ImportRule {
|
|
|
1074
1096
|
//#region src/rules/layers/no-outward-import.rule.ts
|
|
1075
1097
|
var NoOutwardImportRule = class extends ImportRule {
|
|
1076
1098
|
meta = {
|
|
1099
|
+
contexts: "core",
|
|
1077
1100
|
description: "A dependency pointing away from the domain, a file outside the layers or in the wrong folder of its layer.",
|
|
1078
1101
|
id: "layers/no-outward-import",
|
|
1079
1102
|
messages: {
|
|
@@ -1125,7 +1148,7 @@ var NoOutwardImportRule = class extends ImportRule {
|
|
|
1125
1148
|
}
|
|
1126
1149
|
misplacedFiles(architecture) {
|
|
1127
1150
|
const findings = [];
|
|
1128
|
-
for (const file of architecture
|
|
1151
|
+
for (const file of this.filesOf(architecture)) {
|
|
1129
1152
|
const location = architecture.locationOf(file);
|
|
1130
1153
|
if (location.isOutside) findings.push(this.finding(file, 1, location.fileName, "outsideContexts"));
|
|
1131
1154
|
else if (!location.isAtRoot && !location.isInLayer && !location.isCompositionRoot) findings.push(this.finding(file, 1, location.fileName, "outsideLayers"));
|
|
@@ -1150,7 +1173,7 @@ var NoOutwardImportRule = class extends ImportRule {
|
|
|
1150
1173
|
}
|
|
1151
1174
|
extraCompositionRoots(architecture) {
|
|
1152
1175
|
const byFolder = /* @__PURE__ */ new Map();
|
|
1153
|
-
for (const file of architecture
|
|
1176
|
+
for (const file of this.filesOf(architecture)) if (architecture.locationOf(file).isCompositionRoot) {
|
|
1154
1177
|
const folder = dirname(file.path);
|
|
1155
1178
|
byFolder.set(folder, [...byFolder.get(folder) ?? [], file]);
|
|
1156
1179
|
}
|
|
@@ -1210,6 +1233,7 @@ var NoOutwardImportRule = class extends ImportRule {
|
|
|
1210
1233
|
//#region src/rules/layers/no-portless-adapter.rule.ts
|
|
1211
1234
|
var NoPortlessAdapterRule = class extends ClassRule {
|
|
1212
1235
|
meta = {
|
|
1236
|
+
contexts: "core",
|
|
1213
1237
|
description: "A driven adapter that extends no port, a port declared outside the domain.",
|
|
1214
1238
|
id: "layers/no-portless-adapter",
|
|
1215
1239
|
messages: {
|
|
@@ -1235,11 +1259,12 @@ var NoPortlessAdapterRule = class extends ClassRule {
|
|
|
1235
1259
|
//#region src/rules/strategic/no-cross-context-import.rule.ts
|
|
1236
1260
|
var NoCrossContextImportRule = class extends ImportRule {
|
|
1237
1261
|
meta = {
|
|
1262
|
+
contexts: "every",
|
|
1238
1263
|
description: "An import from another bounded context that is not its open host service, a composition root that re-exports.",
|
|
1239
1264
|
id: "strategic/no-cross-context-import",
|
|
1240
1265
|
messages: {
|
|
1241
1266
|
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:
|
|
1267
|
+
outsideAntiCorruptionLayer: "Uses the open host service of {context} outside an AntiCorruptionLayer: a core context translates what it consumes in an anti-corruption layer.",
|
|
1243
1268
|
publishedLanguage: "Imports the published language of {context}: redeclare the fields you read in your own published-language/.",
|
|
1244
1269
|
reexport: "The composition root re-exports {names}: it exports its own module only, so that no other context reaches through it.",
|
|
1245
1270
|
sharedKernel: "The shared kernel imports no bounded context, but imports {target}."
|
|
@@ -1260,7 +1285,7 @@ var NoCrossContextImportRule = class extends ImportRule {
|
|
|
1260
1285
|
if (from.isCompositionRoot && to.isCompositionRoot) return;
|
|
1261
1286
|
if (to.layer === "published-language") return this.finding(file, dependency.line, dependency.label, "publishedLanguage", { context: to.context ?? "" });
|
|
1262
1287
|
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 ?? "" });
|
|
1288
|
+
if (from.isInCoreDomain && !from.isCompositionRoot && !this.declaresAntiCorruptionLayer(file, architecture)) return this.finding(file, dependency.line, dependency.label, "outsideAntiCorruptionLayer", { context: to.context ?? "" });
|
|
1264
1289
|
}
|
|
1265
1290
|
importsOnlyOpenHostServices(dependency, path, architecture) {
|
|
1266
1291
|
const target = architecture.project.file(path);
|
|
@@ -1289,6 +1314,7 @@ const owned = [
|
|
|
1289
1314
|
];
|
|
1290
1315
|
var NoFatSharedKernelRule = class extends ClassRule {
|
|
1291
1316
|
meta = {
|
|
1317
|
+
contexts: "every",
|
|
1292
1318
|
description: "An aggregate, an entity, an event, a domain service, a repository or a handler in the shared kernel.",
|
|
1293
1319
|
id: "strategic/no-fat-shared-kernel",
|
|
1294
1320
|
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." }
|
|
@@ -1308,6 +1334,7 @@ var NoFatSharedKernelRule = class extends ClassRule {
|
|
|
1308
1334
|
//#region src/rules/strategic/no-leaky-host-service.rule.ts
|
|
1309
1335
|
var NoLeakyHostServiceRule = class extends ClassRule {
|
|
1310
1336
|
meta = {
|
|
1337
|
+
contexts: "every",
|
|
1311
1338
|
description: "An open host service that exposes a class of its context instead of the published language.",
|
|
1312
1339
|
id: "strategic/no-leaky-host-service",
|
|
1313
1340
|
messages: { exposes: "{member} exposes {type}, {kind}{owner}: an open host service speaks the published language." }
|
|
@@ -1346,6 +1373,7 @@ var NoLeakyHostServiceRule = class extends ClassRule {
|
|
|
1346
1373
|
//#region src/rules/strategic/no-unmapped-context.rule.ts
|
|
1347
1374
|
var NoUnmappedContextRule = class extends Rule {
|
|
1348
1375
|
meta = {
|
|
1376
|
+
contexts: "every",
|
|
1349
1377
|
description: "A bounded context consuming one the context map does not allow, or two contexts that depend on each other.",
|
|
1350
1378
|
id: "strategic/no-unmapped-context",
|
|
1351
1379
|
messages: {
|
|
@@ -1367,7 +1395,7 @@ var NoUnmappedContextRule = class extends Rule {
|
|
|
1367
1395
|
}
|
|
1368
1396
|
consumptionsIn(architecture) {
|
|
1369
1397
|
const consumptions = [];
|
|
1370
|
-
for (const file of architecture
|
|
1398
|
+
for (const file of this.filesOf(architecture)) {
|
|
1371
1399
|
const from = architecture.locationOf(file);
|
|
1372
1400
|
if (!from.isInBoundedContext || from.context === void 0) continue;
|
|
1373
1401
|
for (const dependency of file.dependencies) {
|
|
@@ -1400,6 +1428,7 @@ const holders = [
|
|
|
1400
1428
|
];
|
|
1401
1429
|
var NoAggregateReferenceRule = class extends Rule {
|
|
1402
1430
|
meta = {
|
|
1431
|
+
contexts: "core",
|
|
1403
1432
|
description: "An aggregate holding another aggregate instead of its identifier, an entity held by two aggregates.",
|
|
1404
1433
|
id: "tactical/no-aggregate-reference",
|
|
1405
1434
|
messages: {
|
|
@@ -1410,7 +1439,7 @@ var NoAggregateReferenceRule = class extends Rule {
|
|
|
1410
1439
|
};
|
|
1411
1440
|
check(architecture) {
|
|
1412
1441
|
const findings = [];
|
|
1413
|
-
for (const file of architecture
|
|
1442
|
+
for (const file of this.filesOf(architecture)) for (const codeClass of file.classes) findings.push(...this.heldAggregates(codeClass, file, architecture));
|
|
1414
1443
|
findings.push(...this.sharedEntities(architecture));
|
|
1415
1444
|
return findings;
|
|
1416
1445
|
}
|
|
@@ -1459,7 +1488,7 @@ var NoAggregateReferenceRule = class extends Rule {
|
|
|
1459
1488
|
}
|
|
1460
1489
|
holdingsIn(architecture) {
|
|
1461
1490
|
const holdings = [];
|
|
1462
|
-
for (const file of architecture
|
|
1491
|
+
for (const file of this.filesOf(architecture)) for (const aggregate of file.classes) {
|
|
1463
1492
|
if (!architecture.is(aggregate, "AggregateRoot")) continue;
|
|
1464
1493
|
const visited = /* @__PURE__ */ new Set();
|
|
1465
1494
|
for (const through of this.heldBy(aggregate)) for (const type of through.types) for (const entity of this.entitiesReachedFrom(type, architecture, visited)) holdings.push({
|
|
@@ -1514,6 +1543,7 @@ const statementMessages = {
|
|
|
1514
1543
|
};
|
|
1515
1544
|
var NoLooseCodeRule = class extends Rule {
|
|
1516
1545
|
meta = {
|
|
1546
|
+
contexts: "core",
|
|
1517
1547
|
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
1548
|
id: "tactical/no-loose-code",
|
|
1519
1549
|
messages: {
|
|
@@ -1535,7 +1565,7 @@ var NoLooseCodeRule = class extends Rule {
|
|
|
1535
1565
|
};
|
|
1536
1566
|
check(architecture) {
|
|
1537
1567
|
const findings = [];
|
|
1538
|
-
for (const file of architecture
|
|
1568
|
+
for (const file of this.filesOf(architecture)) {
|
|
1539
1569
|
const location = architecture.locationOf(file);
|
|
1540
1570
|
if (guarded$2.has(location.layer)) {
|
|
1541
1571
|
findings.push(...this.looseClasses(file, location.layer ?? "domain", architecture));
|
|
@@ -1585,6 +1615,7 @@ const markerWording = {
|
|
|
1585
1615
|
};
|
|
1586
1616
|
var NoMisplacedClassRule = class extends Rule {
|
|
1587
1617
|
meta = {
|
|
1618
|
+
contexts: "core",
|
|
1588
1619
|
description: "A class in the wrong folder or file, two classes in one file.",
|
|
1589
1620
|
id: "tactical/no-misplaced-class",
|
|
1590
1621
|
messages: {
|
|
@@ -1595,7 +1626,7 @@ var NoMisplacedClassRule = class extends Rule {
|
|
|
1595
1626
|
};
|
|
1596
1627
|
check(architecture) {
|
|
1597
1628
|
const findings = [];
|
|
1598
|
-
for (const file of architecture
|
|
1629
|
+
for (const file of this.filesOf(architecture)) {
|
|
1599
1630
|
findings.push(...this.extraClasses(file));
|
|
1600
1631
|
const [first] = file.classes;
|
|
1601
1632
|
const finding = first === void 0 ? void 0 : this.misplacement(first, file, architecture.locationOf(file), architecture);
|
|
@@ -1630,6 +1661,7 @@ var NoMisplacedClassRule = class extends Rule {
|
|
|
1630
1661
|
//#region src/rules/tactical/command-handlers/no-foreign-command-dependency.rule.ts
|
|
1631
1662
|
var NoForeignCommandDependencyRule = class extends InjectionRule {
|
|
1632
1663
|
meta = {
|
|
1664
|
+
contexts: "core",
|
|
1633
1665
|
description: "A command handler receiving a query repository, an event publisher, another handler or a plain class.",
|
|
1634
1666
|
id: "tactical/no-foreign-command-dependency",
|
|
1635
1667
|
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." }
|
|
@@ -1657,6 +1689,7 @@ const exempt = /* @__PURE__ */ new Set([
|
|
|
1657
1689
|
const guarded$1 = /* @__PURE__ */ new Set(["domain", "application"]);
|
|
1658
1690
|
var NoThrownFailureRule = class extends Rule {
|
|
1659
1691
|
meta = {
|
|
1692
|
+
contexts: "core",
|
|
1660
1693
|
description: "A business failure thrown instead of returned, an entity method without a Result, a setter.",
|
|
1661
1694
|
id: "tactical/no-thrown-failure",
|
|
1662
1695
|
messages: {
|
|
@@ -1668,7 +1701,7 @@ var NoThrownFailureRule = class extends Rule {
|
|
|
1668
1701
|
};
|
|
1669
1702
|
check(architecture) {
|
|
1670
1703
|
const findings = [];
|
|
1671
|
-
for (const file of architecture
|
|
1704
|
+
for (const file of this.filesOf(architecture)) {
|
|
1672
1705
|
findings.push(...this.operationsWithoutResult(file, architecture));
|
|
1673
1706
|
if (guarded$1.has(architecture.locationOf(file).layer)) findings.push(...this.raisedFailures(file));
|
|
1674
1707
|
}
|
|
@@ -1703,6 +1736,7 @@ var NoThrownFailureRule = class extends Rule {
|
|
|
1703
1736
|
//#region src/rules/tactical/domain-services/no-stateful-service.rule.ts
|
|
1704
1737
|
var NoStatefulServiceRule = class extends InjectionRule {
|
|
1705
1738
|
meta = {
|
|
1739
|
+
contexts: "core",
|
|
1706
1740
|
description: "A domain service holding a port, a repository or another service.",
|
|
1707
1741
|
id: "tactical/no-stateful-service",
|
|
1708
1742
|
messages: { foreign: "The DomainService {class} holds {type}, {kind}: a domain service holds configuration only; the command handler passes it what it needs." }
|
|
@@ -1720,6 +1754,7 @@ const guarded = [
|
|
|
1720
1754
|
];
|
|
1721
1755
|
var NoPublicFieldRule = class extends ClassRule {
|
|
1722
1756
|
meta = {
|
|
1757
|
+
contexts: "core",
|
|
1723
1758
|
description: "A public field on an aggregate, an entity, a value object or an identifier.",
|
|
1724
1759
|
id: "tactical/no-public-field",
|
|
1725
1760
|
messages: { publicField: "{member} is a public field: keep the state private, and expose what callers need through a getter." }
|
|
@@ -1738,6 +1773,7 @@ var NoPublicFieldRule = class extends ClassRule {
|
|
|
1738
1773
|
//#region src/rules/tactical/query-handlers/no-foreign-query-dependency.rule.ts
|
|
1739
1774
|
var NoForeignQueryDependencyRule = class extends InjectionRule {
|
|
1740
1775
|
meta = {
|
|
1776
|
+
contexts: "core",
|
|
1741
1777
|
description: "A query handler receiving what writes or changes state.",
|
|
1742
1778
|
id: "tactical/no-foreign-query-dependency",
|
|
1743
1779
|
messages: { foreign: "The QueryHandler {class} receives {type}, {kind}: a query handler receives query repositories, ports that do not write, and value objects." }
|
|
@@ -1775,6 +1811,7 @@ var DisableDirective = class {
|
|
|
1775
1811
|
var NoLooseDisableRule = class extends Rule {
|
|
1776
1812
|
knownRules;
|
|
1777
1813
|
meta = {
|
|
1814
|
+
contexts: "every",
|
|
1778
1815
|
description: "A disable comment that names no known rule, gives no reason, or disables nothing.",
|
|
1779
1816
|
id: "tooling/no-loose-disable",
|
|
1780
1817
|
messages: {
|
|
@@ -1790,7 +1827,7 @@ var NoLooseDisableRule = class extends Rule {
|
|
|
1790
1827
|
}
|
|
1791
1828
|
check(architecture) {
|
|
1792
1829
|
const findings = [];
|
|
1793
|
-
for (const file of architecture
|
|
1830
|
+
for (const file of this.filesOf(architecture)) for (const comment of file.disables) {
|
|
1794
1831
|
const finding = this.malformed(file, comment);
|
|
1795
1832
|
if (finding !== void 0) findings.push(finding);
|
|
1796
1833
|
}
|
|
@@ -1960,7 +1997,9 @@ var Report = class {
|
|
|
1960
1997
|
text() {
|
|
1961
1998
|
const blocks = [];
|
|
1962
1999
|
for (const [file, violations] of this.byFile()) blocks.push(this.block(file, violations));
|
|
1963
|
-
|
|
2000
|
+
const parts = [...blocks, this.summary()];
|
|
2001
|
+
if (blocks.length > 0) parts.push(this.colors.dim("Why, and how to fix it: npx alveolus explain <rule>"));
|
|
2002
|
+
return `${parts.join("\n\n")}\n`;
|
|
1964
2003
|
}
|
|
1965
2004
|
json() {
|
|
1966
2005
|
const { baselined, files, stale, suppressed, violations } = this.input;
|
|
@@ -2082,16 +2121,37 @@ var Config = class {
|
|
|
2082
2121
|
this.contextMap = config.contextMap === void 0 ? void 0 : this.validContextMap(config.contextMap, Object.keys(config.boundedContexts));
|
|
2083
2122
|
this.rules = config.rules;
|
|
2084
2123
|
const sharedKernel = config.sharedKernel ?? "shared-kernel";
|
|
2085
|
-
this.contextFolders = [...
|
|
2086
|
-
dir: resolve(this.rootDir, folder),
|
|
2087
|
-
isSharedKernel: false,
|
|
2088
|
-
name
|
|
2089
|
-
})), {
|
|
2124
|
+
this.contextFolders = [...this.classifiedContexts(config.subdomains ?? {}, config.boundedContexts), {
|
|
2090
2125
|
dir: resolve(this.rootDir, sharedKernel),
|
|
2091
2126
|
isSharedKernel: true,
|
|
2092
2127
|
name: "shared kernel"
|
|
2093
2128
|
}];
|
|
2094
2129
|
}
|
|
2130
|
+
classifiedContexts(subdomains, boundedContexts) {
|
|
2131
|
+
const types = [
|
|
2132
|
+
"core",
|
|
2133
|
+
"supporting",
|
|
2134
|
+
"generic"
|
|
2135
|
+
];
|
|
2136
|
+
const classified = /* @__PURE__ */ new Map();
|
|
2137
|
+
const folders = [];
|
|
2138
|
+
for (const type of types) for (const name of subdomains[type] ?? []) {
|
|
2139
|
+
const folder = boundedContexts[name];
|
|
2140
|
+
if (folder === void 0) throw new Error(`subdomains names ${name}, which boundedContexts does not declare.`);
|
|
2141
|
+
const already = classified.get(name);
|
|
2142
|
+
if (already !== void 0) throw new Error(`subdomains lists ${name} as ${already} and as ${type}: a bounded context implements one subdomain.`);
|
|
2143
|
+
classified.set(name, type);
|
|
2144
|
+
folders.push({
|
|
2145
|
+
dir: resolve(this.rootDir, folder),
|
|
2146
|
+
isSharedKernel: false,
|
|
2147
|
+
name,
|
|
2148
|
+
subdomain: type
|
|
2149
|
+
});
|
|
2150
|
+
}
|
|
2151
|
+
const unclassified = Object.keys(boundedContexts).filter((name) => !classified.has(name));
|
|
2152
|
+
if (unclassified.length > 0) throw new Error(`boundedContexts declares ${unclassified.join(", ")}, which subdomains does not classify: list each context under subdomains.core, subdomains.supporting or subdomains.generic.`);
|
|
2153
|
+
return folders;
|
|
2154
|
+
}
|
|
2095
2155
|
validContextMap(upstreams, contexts) {
|
|
2096
2156
|
const map = new ContextMap(upstreams);
|
|
2097
2157
|
const unknown = map.contexts.filter((name) => !contexts.includes(name));
|
|
@@ -2117,6 +2177,7 @@ const ruleSetting = z.enum([
|
|
|
2117
2177
|
"info",
|
|
2118
2178
|
"off"
|
|
2119
2179
|
]);
|
|
2180
|
+
const contextNames = z.array(z.string()).exactOptional();
|
|
2120
2181
|
const schema = z.strictObject({
|
|
2121
2182
|
applicationDependencies: packageDependencies.exactOptional(),
|
|
2122
2183
|
boundedContexts: z.record(z.string(), z.string()),
|
|
@@ -2131,6 +2192,11 @@ const schema = z.strictObject({
|
|
|
2131
2192
|
root: z.string(),
|
|
2132
2193
|
rules: z.strictObject(Object.fromEntries(new RuleRegistry().ids.map((id) => [id, ruleSetting.exactOptional()]))).exactOptional(),
|
|
2133
2194
|
sharedKernel: z.string().exactOptional(),
|
|
2195
|
+
subdomains: z.strictObject({
|
|
2196
|
+
core: contextNames,
|
|
2197
|
+
generic: contextNames,
|
|
2198
|
+
supporting: contextNames
|
|
2199
|
+
}).exactOptional(),
|
|
2134
2200
|
tsconfig: z.string().exactOptional()
|
|
2135
2201
|
});
|
|
2136
2202
|
var ConfigLoader = class ConfigLoader {
|
|
@@ -2709,18 +2775,91 @@ var TsMorphImporter = class TsMorphImporter extends Importer {
|
|
|
2709
2775
|
return offset !== "" && !offset.startsWith("..") && !isAbsolute(offset) && !offset.split(/[\\/]/).includes("node_modules");
|
|
2710
2776
|
}
|
|
2711
2777
|
};
|
|
2778
|
+
const scaffolds = [
|
|
2779
|
+
{
|
|
2780
|
+
content: `import { defineConfig } from "@alveolus/arch";
|
|
2781
|
+
|
|
2782
|
+
export default defineConfig({
|
|
2783
|
+
boundedContexts: {},
|
|
2784
|
+
root: "src",
|
|
2785
|
+
});
|
|
2786
|
+
`,
|
|
2787
|
+
path: "alveolus.config.ts"
|
|
2788
|
+
},
|
|
2789
|
+
{
|
|
2790
|
+
content: `---
|
|
2791
|
+
name: alveolus
|
|
2792
|
+
description: Domain-Driven Design with @alveolus/core and @alveolus/arch. Use when writing or changing a class of the domain, the application or an adapter, when a class extends an Alveolus building block, or when alveolus arch check reports a violation.
|
|
2793
|
+
---
|
|
2794
|
+
|
|
2795
|
+
# Alveolus
|
|
2796
|
+
|
|
2797
|
+
The documentation is installed with the package: read it with \`npx alveolus explain <topic>\`, not on the web. \`npx alveolus explain\` lists every topic.
|
|
2798
|
+
|
|
2799
|
+
1. Before writing a class, read its building block: \`npx alveolus explain aggregates\`, \`entities\`, \`value-objects\`, \`domain-events\`, \`domain-errors\`, \`ports\`, \`repositories\`, \`command-handlers\`, \`query-handlers\`, \`result\`.
|
|
2800
|
+
2. Where a file goes and what each layer may import: \`npx alveolus explain project-layout\`.
|
|
2801
|
+
3. After each change, run \`npx alveolus arch check\`.
|
|
2802
|
+
4. On a violation, read the rule before changing the code: \`npx alveolus explain <rule>\`, with the rule id of the report, such as \`layers/no-impure-domain\`. Fix the cause: never turn a rule off, add a disable comment or edit \`alveolus.baseline.json\` by hand without asking.
|
|
2803
|
+
`,
|
|
2804
|
+
path: ".claude/skills/alveolus/SKILL.md"
|
|
2805
|
+
},
|
|
2806
|
+
{
|
|
2807
|
+
content: `## Alveolus
|
|
2808
|
+
|
|
2809
|
+
This project uses Alveolus for Domain-Driven Design: the building blocks of \`@alveolus/core\` and the architecture checks of \`@alveolus/arch\`. The documentation is installed with the package: \`npx alveolus explain\` lists the topics and \`npx alveolus explain <topic>\` prints one, so do not look for it on the web. Run \`npx alveolus arch check\` after each change, and read the rule reported with \`npx alveolus explain <rule>\` before fixing. See \`.claude/skills/alveolus/SKILL.md\`.
|
|
2810
|
+
`,
|
|
2811
|
+
marker: "npx alveolus explain",
|
|
2812
|
+
path: "AGENTS.md"
|
|
2813
|
+
}
|
|
2814
|
+
];
|
|
2815
|
+
//#endregion
|
|
2816
|
+
//#region src/init/init.ts
|
|
2817
|
+
var Init = class {
|
|
2818
|
+
projectDir;
|
|
2819
|
+
constructor(projectDir) {
|
|
2820
|
+
this.projectDir = projectDir;
|
|
2821
|
+
}
|
|
2822
|
+
run() {
|
|
2823
|
+
const written = [];
|
|
2824
|
+
for (const scaffold of scaffolds) written.push({
|
|
2825
|
+
outcome: this.write(scaffold),
|
|
2826
|
+
path: scaffold.path
|
|
2827
|
+
});
|
|
2828
|
+
return written;
|
|
2829
|
+
}
|
|
2830
|
+
get hint() {
|
|
2831
|
+
const claude = join(this.projectDir, "CLAUDE.md");
|
|
2832
|
+
if (!existsSync(claude)) return;
|
|
2833
|
+
return "CLAUDE.md exists: Claude Code reads it instead of AGENTS.md, so add a line with @AGENTS.md to it.";
|
|
2834
|
+
}
|
|
2835
|
+
write(scaffold) {
|
|
2836
|
+
const path = join(this.projectDir, scaffold.path);
|
|
2837
|
+
if (!existsSync(path)) {
|
|
2838
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
2839
|
+
writeFileSync(path, scaffold.content);
|
|
2840
|
+
return "created";
|
|
2841
|
+
}
|
|
2842
|
+
if (scaffold.marker === void 0) return "kept";
|
|
2843
|
+
const existing = readFileSync(path, "utf8");
|
|
2844
|
+
if (existing.includes(scaffold.marker)) return "kept";
|
|
2845
|
+
writeFileSync(path, `${existing.trimEnd()}\n\n${scaffold.content}`);
|
|
2846
|
+
return "appended";
|
|
2847
|
+
}
|
|
2848
|
+
};
|
|
2712
2849
|
//#endregion
|
|
2713
2850
|
//#region src/cli/cli.ts
|
|
2714
2851
|
var Cli = class {
|
|
2715
2852
|
stdout;
|
|
2716
2853
|
stderr;
|
|
2717
2854
|
cwd;
|
|
2855
|
+
docs;
|
|
2718
2856
|
colored;
|
|
2719
2857
|
exitCode = 0;
|
|
2720
|
-
constructor(stdout, stderr, cwd, colored = false) {
|
|
2858
|
+
constructor(stdout, stderr, cwd, docs, colored = false) {
|
|
2721
2859
|
this.stdout = stdout;
|
|
2722
2860
|
this.stderr = stderr;
|
|
2723
2861
|
this.cwd = cwd;
|
|
2862
|
+
this.docs = docs;
|
|
2724
2863
|
this.colored = colored;
|
|
2725
2864
|
}
|
|
2726
2865
|
async run(args) {
|
|
@@ -2740,8 +2879,30 @@ var Cli = class {
|
|
|
2740
2879
|
const arch = program.command("arch").description("Check the architecture of a Domain-Driven Design project");
|
|
2741
2880
|
this.withOptions(arch.command("check").description("Report the violations that are not in the baseline")).action((options) => this.check(options));
|
|
2742
2881
|
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));
|
|
2882
|
+
program.command("explain").description("Print a page of the documentation: a rule, a building block or a guide").argument("[topic]", "a rule id, a building block or a guide; without it, the list of topics").action((topic) => this.explain(topic));
|
|
2883
|
+
program.command("init").description(`Write ${ConfigLoader.fileName}, and the instructions that tell a coding agent to read the documentation installed with the package`).option("--project <dir>", "project directory", ".").action((options) => this.init(options));
|
|
2743
2884
|
return program;
|
|
2744
2885
|
}
|
|
2886
|
+
explain(topic) {
|
|
2887
|
+
if (topic === void 0) {
|
|
2888
|
+
this.stdout.write(`${this.docs.topics().join("\n")}\n`);
|
|
2889
|
+
return;
|
|
2890
|
+
}
|
|
2891
|
+
const match = this.docs.find(topic);
|
|
2892
|
+
if ("page" in match) {
|
|
2893
|
+
this.stdout.write(match.page.text());
|
|
2894
|
+
return;
|
|
2895
|
+
}
|
|
2896
|
+
const list = match.candidates.length === 0 ? "alveolus explain lists the topics." : `Did you mean ${match.candidates.join(", ")}?`;
|
|
2897
|
+
this.stderr.write(`No page for ${topic}: ${list}\n`);
|
|
2898
|
+
this.exitCode = 1;
|
|
2899
|
+
}
|
|
2900
|
+
init(options) {
|
|
2901
|
+
const init = new Init(resolve(this.cwd, options.project));
|
|
2902
|
+
for (const { outcome, path } of init.run()) this.stdout.write(`${outcome} ${path}\n`);
|
|
2903
|
+
this.stdout.write(`Name your bounded contexts in ${ConfigLoader.fileName}, then run alveolus arch check.\n`);
|
|
2904
|
+
if (init.hint !== void 0) this.stderr.write(`${init.hint}\n`);
|
|
2905
|
+
}
|
|
2745
2906
|
withOptions(command) {
|
|
2746
2907
|
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
2908
|
"text",
|
|
@@ -2805,6 +2966,94 @@ var Cli = class {
|
|
|
2805
2966
|
}
|
|
2806
2967
|
};
|
|
2807
2968
|
//#endregion
|
|
2808
|
-
|
|
2969
|
+
//#region src/docs/page.ts
|
|
2970
|
+
const html = [
|
|
2971
|
+
{
|
|
2972
|
+
pattern: /<dt>/g,
|
|
2973
|
+
text: "- "
|
|
2974
|
+
},
|
|
2975
|
+
{
|
|
2976
|
+
pattern: /<\/dt>\s*<dd>/g,
|
|
2977
|
+
text: ": "
|
|
2978
|
+
},
|
|
2979
|
+
{
|
|
2980
|
+
pattern: /<[^>\n]+>/g,
|
|
2981
|
+
text: ""
|
|
2982
|
+
},
|
|
2983
|
+
{
|
|
2984
|
+
pattern: /^\t+- /gm,
|
|
2985
|
+
text: "- "
|
|
2986
|
+
},
|
|
2987
|
+
{
|
|
2988
|
+
pattern: /</g,
|
|
2989
|
+
text: "<"
|
|
2990
|
+
},
|
|
2991
|
+
{
|
|
2992
|
+
pattern: />/g,
|
|
2993
|
+
text: ">"
|
|
2994
|
+
},
|
|
2995
|
+
{
|
|
2996
|
+
pattern: /&/g,
|
|
2997
|
+
text: "&"
|
|
2998
|
+
}
|
|
2999
|
+
];
|
|
3000
|
+
const frontmatter = /^---\n([\s\S]*?)\n---\n/;
|
|
3001
|
+
var Page = class {
|
|
3002
|
+
topic;
|
|
3003
|
+
source;
|
|
3004
|
+
constructor(topic, source) {
|
|
3005
|
+
this.topic = topic;
|
|
3006
|
+
this.source = source;
|
|
3007
|
+
}
|
|
3008
|
+
get description() {
|
|
3009
|
+
const header = frontmatter.exec(this.source)?.[1] ?? "";
|
|
3010
|
+
return /^description: "?(.*?)"?$/m.exec(header)?.[1] ?? "";
|
|
3011
|
+
}
|
|
3012
|
+
text() {
|
|
3013
|
+
let text = this.source.replace(frontmatter, "");
|
|
3014
|
+
for (const { pattern, text: replacement } of html) text = text.replace(pattern, replacement);
|
|
3015
|
+
return `${text.replace(/\n{3,}/g, "\n\n").trim()}\n`;
|
|
3016
|
+
}
|
|
3017
|
+
};
|
|
3018
|
+
//#endregion
|
|
3019
|
+
//#region src/docs/docs.ts
|
|
3020
|
+
var Docs = class Docs {
|
|
3021
|
+
dir;
|
|
3022
|
+
static sections = [
|
|
3023
|
+
"guide",
|
|
3024
|
+
"integrations",
|
|
3025
|
+
"core",
|
|
3026
|
+
"rules"
|
|
3027
|
+
];
|
|
3028
|
+
constructor(dir) {
|
|
3029
|
+
this.dir = dir;
|
|
3030
|
+
}
|
|
3031
|
+
topics() {
|
|
3032
|
+
const topics = [];
|
|
3033
|
+
for (const section of Docs.sections) for (const file of globSync("**/*.md", { cwd: join(this.dir, section) }).sort()) topics.push(this.topicOf(join(section, file)));
|
|
3034
|
+
return topics.sort();
|
|
3035
|
+
}
|
|
3036
|
+
find(name) {
|
|
3037
|
+
const wanted = name.replace(/\.md$/, "").replace(/\/$/, "").replace(/\\/g, "/");
|
|
3038
|
+
const candidates = this.topics().filter((topic) => topic === wanted || topic.endsWith(`/${wanted}`));
|
|
3039
|
+
const exact = candidates.find((topic) => topic === wanted);
|
|
3040
|
+
if (exact !== void 0) return { page: this.read(exact) };
|
|
3041
|
+
if (candidates.length === 1 && candidates[0] !== void 0) return { page: this.read(candidates[0]) };
|
|
3042
|
+
return { candidates };
|
|
3043
|
+
}
|
|
3044
|
+
read(topic) {
|
|
3045
|
+
const path = join(this.dir, ...topic.split("/"));
|
|
3046
|
+
try {
|
|
3047
|
+
return new Page(topic, readFileSync(`${path}.md`, "utf8"));
|
|
3048
|
+
} catch {
|
|
3049
|
+
return new Page(topic, readFileSync(join(path, "index.md"), "utf8"));
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
topicOf(file) {
|
|
3053
|
+
return file.split(sep).join("/").replace(/\.md$/, "").replace(/\/index$/, "");
|
|
3054
|
+
}
|
|
3055
|
+
};
|
|
3056
|
+
//#endregion
|
|
3057
|
+
export { TsMorphImporter as a, Config as c, RuleRegistry as d, Rule as f, Baseline as h, Init as i, Report as l, AllowedPackages as m, Page as n, Importer as o, Architecture as p, Cli as r, ConfigLoader as s, Docs as t, Checker as u };
|
|
2809
3058
|
|
|
2810
|
-
//# sourceMappingURL=
|
|
3059
|
+
//# sourceMappingURL=docs-DsQHpTtV.mjs.map
|