@alveolus/arch 0.3.0 → 0.4.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 +5 -0
- package/dist/bin.mjs +1 -1
- package/dist/{docs-DsQHpTtV.mjs → docs-DcFgskuN.mjs} +26 -40
- package/dist/docs-DcFgskuN.mjs.map +1 -0
- package/dist/index.d.mts +10 -11
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/docs/guide/existing-project.md +6 -3
- package/docs/guide/getting-started.md +14 -3
- package/docs/rules/index.md +2 -0
- package/docs/rules/layers/no-impure-domain.md +2 -0
- package/docs/rules/layers/no-outward-import.md +2 -0
- package/docs/rules/strategic/no-unmapped-context.md +25 -22
- package/docs/rules/tactical/no-foreign-query-dependency.md +98 -3
- package/package.json +1 -1
- package/dist/docs-DsQHpTtV.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -14,6 +14,11 @@ import { defineConfig } from "@alveolus/arch";
|
|
|
14
14
|
|
|
15
15
|
export default defineConfig({
|
|
16
16
|
boundedContexts: { catalog: "catalog", notifications: "notifications", ordering: "ordering" },
|
|
17
|
+
contextMap: {
|
|
18
|
+
catalog: { consumes: [] },
|
|
19
|
+
notifications: { consumes: ["ordering"] },
|
|
20
|
+
ordering: { consumes: ["catalog"] },
|
|
21
|
+
},
|
|
17
22
|
root: "src",
|
|
18
23
|
subdomains: { core: ["catalog", "ordering"], generic: ["notifications"] },
|
|
19
24
|
});
|
package/dist/bin.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { r as Cli, t as Docs } from "./docs-
|
|
2
|
+
import { r as Cli, t as Docs } from "./docs-DcFgskuN.mjs";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
//#region src/bin.ts
|
|
5
5
|
const docs = new Docs(fileURLToPath(new URL("../docs/", import.meta.url)));
|
|
@@ -839,24 +839,22 @@ var Architecture = class {
|
|
|
839
839
|
};
|
|
840
840
|
//#endregion
|
|
841
841
|
//#region src/architecture/context-map.ts
|
|
842
|
-
var ContextMap = class
|
|
842
|
+
var ContextMap = class {
|
|
843
843
|
upstreams;
|
|
844
844
|
constructor(upstreams) {
|
|
845
845
|
this.upstreams = upstreams;
|
|
846
846
|
}
|
|
847
|
-
static ofEdges(edges) {
|
|
848
|
-
const upstreams = {};
|
|
849
|
-
for (const { from, to } of edges) {
|
|
850
|
-
const known = upstreams[from] ?? [];
|
|
851
|
-
if (!known.includes(to)) upstreams[from] = [...known, to];
|
|
852
|
-
}
|
|
853
|
-
return new ContextMap(upstreams);
|
|
854
|
-
}
|
|
855
847
|
get contexts() {
|
|
856
|
-
|
|
848
|
+
return Object.keys(this.upstreams);
|
|
849
|
+
}
|
|
850
|
+
get consumed() {
|
|
851
|
+
const names = /* @__PURE__ */ new Set();
|
|
857
852
|
for (const targets of Object.values(this.upstreams)) for (const target of targets) names.add(target);
|
|
858
853
|
return [...names];
|
|
859
854
|
}
|
|
855
|
+
selfConsumers() {
|
|
856
|
+
return this.contexts.filter((context) => this.allows(context, context));
|
|
857
|
+
}
|
|
860
858
|
allows(from, to) {
|
|
861
859
|
return (this.upstreams[from] ?? []).includes(to);
|
|
862
860
|
}
|
|
@@ -868,9 +866,6 @@ var ContextMap = class ContextMap {
|
|
|
868
866
|
if (found !== void 0) return found;
|
|
869
867
|
}
|
|
870
868
|
}
|
|
871
|
-
cycleThrough(from, to) {
|
|
872
|
-
return this.reaches(to, from, /* @__PURE__ */ new Set());
|
|
873
|
-
}
|
|
874
869
|
cycleFrom(context, path, visiting, done) {
|
|
875
870
|
if (done.has(context)) return;
|
|
876
871
|
if (visiting.has(context)) return [...path.slice(path.indexOf(context)), context];
|
|
@@ -882,12 +877,6 @@ var ContextMap = class ContextMap {
|
|
|
882
877
|
visiting.delete(context);
|
|
883
878
|
done.add(context);
|
|
884
879
|
}
|
|
885
|
-
reaches(from, target, seen) {
|
|
886
|
-
if (from === target) return true;
|
|
887
|
-
if (seen.has(from)) return false;
|
|
888
|
-
seen.add(from);
|
|
889
|
-
return (this.upstreams[from] ?? []).some((upstream) => this.reaches(upstream, target, seen));
|
|
890
|
-
}
|
|
891
880
|
};
|
|
892
881
|
//#endregion
|
|
893
882
|
//#region src/rules/framework/wording.ts
|
|
@@ -1374,23 +1363,13 @@ var NoLeakyHostServiceRule = class extends ClassRule {
|
|
|
1374
1363
|
var NoUnmappedContextRule = class extends Rule {
|
|
1375
1364
|
meta = {
|
|
1376
1365
|
contexts: "every",
|
|
1377
|
-
description: "A bounded context consuming one the context map does not allow
|
|
1366
|
+
description: "A bounded context consuming one the context map does not allow.",
|
|
1378
1367
|
id: "strategic/no-unmapped-context",
|
|
1379
|
-
messages: {
|
|
1380
|
-
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.",
|
|
1381
|
-
unmapped: "{from} consumes {to}, which the context map does not allow: add {to} to contextMap.{from}, or reverse the dependency."
|
|
1382
|
-
}
|
|
1368
|
+
messages: { unmapped: "{from} consumes {to}, which the context map does not allow: reverse the dependency, or if {from} really is downstream of {to}, add {to} to contextMap.{from}.consumes." }
|
|
1383
1369
|
};
|
|
1384
1370
|
check(architecture) {
|
|
1385
|
-
const consumptions = this.consumptionsIn(architecture);
|
|
1386
|
-
const declared = architecture.contextMap;
|
|
1387
1371
|
const findings = [];
|
|
1388
|
-
|
|
1389
|
-
for (const consumption of consumptions) if (!declared.allows(consumption.from, consumption.to)) findings.push(this.consumptionFinding(consumption, "unmapped"));
|
|
1390
|
-
return findings;
|
|
1391
|
-
}
|
|
1392
|
-
const observed = ContextMap.ofEdges(consumptions);
|
|
1393
|
-
for (const consumption of consumptions) if (observed.cycleThrough(consumption.from, consumption.to)) findings.push(this.consumptionFinding(consumption, "cycle"));
|
|
1372
|
+
for (const consumption of this.consumptionsIn(architecture)) if (!architecture.contextMap.allows(consumption.from, consumption.to)) findings.push(this.consumptionFinding(consumption));
|
|
1394
1373
|
return findings;
|
|
1395
1374
|
}
|
|
1396
1375
|
consumptionsIn(architecture) {
|
|
@@ -1411,9 +1390,9 @@ var NoUnmappedContextRule = class extends Rule {
|
|
|
1411
1390
|
}
|
|
1412
1391
|
return consumptions;
|
|
1413
1392
|
}
|
|
1414
|
-
consumptionFinding(consumption
|
|
1393
|
+
consumptionFinding(consumption) {
|
|
1415
1394
|
const { dependency, file, from, to } = consumption;
|
|
1416
|
-
return this.finding(file, dependency.line, dependency.label,
|
|
1395
|
+
return this.finding(file, dependency.line, dependency.label, "unmapped", {
|
|
1417
1396
|
from,
|
|
1418
1397
|
to
|
|
1419
1398
|
});
|
|
@@ -2118,7 +2097,7 @@ var Config = class {
|
|
|
2118
2097
|
this.applicationDependencies = this.domainDependencies.with(new AllowedPackages(config.applicationDependencies));
|
|
2119
2098
|
this.ignored = [...testFiles, ...config.ignore ?? []];
|
|
2120
2099
|
this.extraFolders = config.layout?.extraFolders ?? {};
|
|
2121
|
-
this.contextMap =
|
|
2100
|
+
this.contextMap = this.validContextMap(config.contextMap, Object.keys(config.boundedContexts));
|
|
2122
2101
|
this.rules = config.rules;
|
|
2123
2102
|
const sharedKernel = config.sharedKernel ?? "shared-kernel";
|
|
2124
2103
|
this.contextFolders = [...this.classifiedContexts(config.subdomains ?? {}, config.boundedContexts), {
|
|
@@ -2152,10 +2131,16 @@ var Config = class {
|
|
|
2152
2131
|
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
2132
|
return folders;
|
|
2154
2133
|
}
|
|
2155
|
-
validContextMap(
|
|
2134
|
+
validContextMap(relations, contexts) {
|
|
2135
|
+
const upstreams = {};
|
|
2136
|
+
for (const [name, { consumes }] of Object.entries(relations)) upstreams[name] = consumes;
|
|
2156
2137
|
const map = new ContextMap(upstreams);
|
|
2157
|
-
const unknown = map.contexts.filter((name) => !contexts.includes(name));
|
|
2138
|
+
const unknown = [.../* @__PURE__ */ new Set([...map.contexts, ...map.consumed])].filter((name) => !contexts.includes(name));
|
|
2158
2139
|
if (unknown.length > 0) throw new Error(`contextMap names ${unknown.join(", ")}, which boundedContexts does not declare.`);
|
|
2140
|
+
const unlisted = contexts.filter((name) => !map.contexts.includes(name));
|
|
2141
|
+
if (unlisted.length > 0) throw new Error(`contextMap does not list ${unlisted.join(", ")}: every bounded context lists the contexts it consumes, consumes: [] when none.`);
|
|
2142
|
+
const selfConsumers = map.selfConsumers();
|
|
2143
|
+
if (selfConsumers.length > 0) throw new Error(`contextMap lists ${selfConsumers.join(", ")} as consuming itself: a context consumes other contexts only.`);
|
|
2159
2144
|
const cycle = map.cycle();
|
|
2160
2145
|
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.`);
|
|
2161
2146
|
return map;
|
|
@@ -2182,7 +2167,7 @@ const schema = z.strictObject({
|
|
|
2182
2167
|
applicationDependencies: packageDependencies.exactOptional(),
|
|
2183
2168
|
boundedContexts: z.record(z.string(), z.string()),
|
|
2184
2169
|
compositionRoot: z.string().exactOptional(),
|
|
2185
|
-
contextMap: z.record(z.string(), z.array(z.string()))
|
|
2170
|
+
contextMap: z.record(z.string(), z.strictObject({ consumes: z.array(z.string()) })),
|
|
2186
2171
|
domainDependencies: packageDependencies.exactOptional(),
|
|
2187
2172
|
ignore: z.array(z.string()).exactOptional(),
|
|
2188
2173
|
layout: z.strictObject({ extraFolders: z.strictObject({
|
|
@@ -2781,6 +2766,7 @@ const scaffolds = [
|
|
|
2781
2766
|
|
|
2782
2767
|
export default defineConfig({
|
|
2783
2768
|
boundedContexts: {},
|
|
2769
|
+
contextMap: {},
|
|
2784
2770
|
root: "src",
|
|
2785
2771
|
});
|
|
2786
2772
|
`,
|
|
@@ -2799,7 +2785,7 @@ The documentation is installed with the package: read it with \`npx alveolus exp
|
|
|
2799
2785
|
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
2786
|
2. Where a file goes and what each layer may import: \`npx alveolus explain project-layout\`.
|
|
2801
2787
|
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.
|
|
2788
|
+
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, add a context to \`contextMap\` or edit \`alveolus.baseline.json\` by hand without asking.
|
|
2803
2789
|
`,
|
|
2804
2790
|
path: ".claude/skills/alveolus/SKILL.md"
|
|
2805
2791
|
},
|
|
@@ -3056,4 +3042,4 @@ var Docs = class Docs {
|
|
|
3056
3042
|
//#endregion
|
|
3057
3043
|
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 };
|
|
3058
3044
|
|
|
3059
|
-
//# sourceMappingURL=docs-
|
|
3045
|
+
//# sourceMappingURL=docs-DcFgskuN.mjs.map
|