@alveolus/arch 0.2.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 +11 -0
- package/dist/bin.mjs +4 -2
- package/dist/bin.mjs.map +1 -1
- package/dist/{cli-P5PwH9OE.mjs → docs-DcFgskuN.mjs} +214 -43
- package/dist/docs-DcFgskuN.mjs.map +1 -0
- package/dist/index.d.mts +53 -12
- 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 +108 -0
- package/docs/guide/getting-started.md +286 -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 +185 -0
- package/docs/rules/layers/no-driving-shortcut.md +119 -0
- package/docs/rules/layers/no-impure-domain.md +191 -0
- package/docs/rules/layers/no-outward-import.md +186 -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 +114 -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 +201 -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-P5PwH9OE.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
|
});
|
|
@@ -35,8 +40,14 @@ Seventeen rules, each with a page that says what it reports, why, how to fix it
|
|
|
35
40
|
cannot see. A baseline for existing projects, `error` / `warn` / `info` levels, disable comments
|
|
36
41
|
with a reason, JSON and SARIF output for the pull request.
|
|
37
42
|
|
|
43
|
+
The documentation is installed with the package, for you and for a coding agent:
|
|
44
|
+
`npx alveolus explain layers/no-impure-domain` prints a rule, `npx alveolus explain aggregates` a
|
|
45
|
+
building block, and `npx alveolus init` writes the configuration and the instructions that tell
|
|
46
|
+
Claude Code, Cursor or Codex to read them there.
|
|
47
|
+
|
|
38
48
|
- [Rules](https://alveolus.dev/rules/)
|
|
39
49
|
- [Getting started](https://alveolus.dev/guide/getting-started)
|
|
40
50
|
- [Adopt it on an existing project](https://alveolus.dev/guide/existing-project)
|
|
51
|
+
- [Coding agents](https://alveolus.dev/guide/agents)
|
|
41
52
|
|
|
42
53
|
Node.js 24 or later. MIT.
|
package/dist/bin.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { r as Cli, t as Docs } from "./docs-DcFgskuN.mjs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
3
4
|
//#region src/bin.ts
|
|
4
|
-
const
|
|
5
|
+
const docs = new Docs(fileURLToPath(new URL("../docs/", import.meta.url)));
|
|
6
|
+
const cli = new Cli(process.stdout, process.stderr, process.cwd(), docs, process.stdout.isTTY);
|
|
5
7
|
process.exitCode = await cli.run(process.argv.slice(2));
|
|
6
8
|
//#endregion
|
|
7
9
|
export {};
|
package/dist/bin.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bin.mjs","names":[],"sources":["../src/bin.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { Cli } from \"./cli/index.ts\";\n\nconst cli = new Cli(process.stdout, process.stderr, process.cwd(), process.stdout.isTTY);\nprocess.exitCode = await cli.run(process.argv.slice(2));\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"bin.mjs","names":[],"sources":["../src/bin.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { fileURLToPath } from \"node:url\";\n\nimport { Cli } from \"./cli/index.ts\";\nimport { Docs } from \"./docs/index.ts\";\n\nconst docs = new Docs(fileURLToPath(new URL(\"../docs/\", import.meta.url)));\nconst cli = new Cli(process.stdout, process.stderr, process.cwd(), docs, process.stdout.isTTY);\nprocess.exitCode = await cli.run(process.argv.slice(2));\n"],"mappings":";;;;AAMA,MAAM,OAAO,IAAI,KAAK,cAAc,IAAI,IAAI,YAAY,YAAY,GAAG,CAAC,CAAC;AACzE,MAAM,MAAM,IAAI,IAAI,QAAQ,QAAQ,QAAQ,QAAQ,QAAQ,IAAI,GAAG,MAAM,QAAQ,OAAO,KAAK;AAC7F,QAAQ,WAAW,MAAM,IAAI,IAAI,QAAQ,KAAK,MAAM,CAAC,CAAC"}
|
|
@@ -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";
|
|
@@ -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
|
});
|
|
@@ -1997,7 +1976,9 @@ var Report = class {
|
|
|
1997
1976
|
text() {
|
|
1998
1977
|
const blocks = [];
|
|
1999
1978
|
for (const [file, violations] of this.byFile()) blocks.push(this.block(file, violations));
|
|
2000
|
-
|
|
1979
|
+
const parts = [...blocks, this.summary()];
|
|
1980
|
+
if (blocks.length > 0) parts.push(this.colors.dim("Why, and how to fix it: npx alveolus explain <rule>"));
|
|
1981
|
+
return `${parts.join("\n\n")}\n`;
|
|
2001
1982
|
}
|
|
2002
1983
|
json() {
|
|
2003
1984
|
const { baselined, files, stale, suppressed, violations } = this.input;
|
|
@@ -2116,7 +2097,7 @@ var Config = class {
|
|
|
2116
2097
|
this.applicationDependencies = this.domainDependencies.with(new AllowedPackages(config.applicationDependencies));
|
|
2117
2098
|
this.ignored = [...testFiles, ...config.ignore ?? []];
|
|
2118
2099
|
this.extraFolders = config.layout?.extraFolders ?? {};
|
|
2119
|
-
this.contextMap =
|
|
2100
|
+
this.contextMap = this.validContextMap(config.contextMap, Object.keys(config.boundedContexts));
|
|
2120
2101
|
this.rules = config.rules;
|
|
2121
2102
|
const sharedKernel = config.sharedKernel ?? "shared-kernel";
|
|
2122
2103
|
this.contextFolders = [...this.classifiedContexts(config.subdomains ?? {}, config.boundedContexts), {
|
|
@@ -2150,10 +2131,16 @@ var Config = class {
|
|
|
2150
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.`);
|
|
2151
2132
|
return folders;
|
|
2152
2133
|
}
|
|
2153
|
-
validContextMap(
|
|
2134
|
+
validContextMap(relations, contexts) {
|
|
2135
|
+
const upstreams = {};
|
|
2136
|
+
for (const [name, { consumes }] of Object.entries(relations)) upstreams[name] = consumes;
|
|
2154
2137
|
const map = new ContextMap(upstreams);
|
|
2155
|
-
const unknown = map.contexts.filter((name) => !contexts.includes(name));
|
|
2138
|
+
const unknown = [.../* @__PURE__ */ new Set([...map.contexts, ...map.consumed])].filter((name) => !contexts.includes(name));
|
|
2156
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.`);
|
|
2157
2144
|
const cycle = map.cycle();
|
|
2158
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.`);
|
|
2159
2146
|
return map;
|
|
@@ -2180,7 +2167,7 @@ const schema = z.strictObject({
|
|
|
2180
2167
|
applicationDependencies: packageDependencies.exactOptional(),
|
|
2181
2168
|
boundedContexts: z.record(z.string(), z.string()),
|
|
2182
2169
|
compositionRoot: z.string().exactOptional(),
|
|
2183
|
-
contextMap: z.record(z.string(), z.array(z.string()))
|
|
2170
|
+
contextMap: z.record(z.string(), z.strictObject({ consumes: z.array(z.string()) })),
|
|
2184
2171
|
domainDependencies: packageDependencies.exactOptional(),
|
|
2185
2172
|
ignore: z.array(z.string()).exactOptional(),
|
|
2186
2173
|
layout: z.strictObject({ extraFolders: z.strictObject({
|
|
@@ -2773,18 +2760,92 @@ var TsMorphImporter = class TsMorphImporter extends Importer {
|
|
|
2773
2760
|
return offset !== "" && !offset.startsWith("..") && !isAbsolute(offset) && !offset.split(/[\\/]/).includes("node_modules");
|
|
2774
2761
|
}
|
|
2775
2762
|
};
|
|
2763
|
+
const scaffolds = [
|
|
2764
|
+
{
|
|
2765
|
+
content: `import { defineConfig } from "@alveolus/arch";
|
|
2766
|
+
|
|
2767
|
+
export default defineConfig({
|
|
2768
|
+
boundedContexts: {},
|
|
2769
|
+
contextMap: {},
|
|
2770
|
+
root: "src",
|
|
2771
|
+
});
|
|
2772
|
+
`,
|
|
2773
|
+
path: "alveolus.config.ts"
|
|
2774
|
+
},
|
|
2775
|
+
{
|
|
2776
|
+
content: `---
|
|
2777
|
+
name: alveolus
|
|
2778
|
+
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.
|
|
2779
|
+
---
|
|
2780
|
+
|
|
2781
|
+
# Alveolus
|
|
2782
|
+
|
|
2783
|
+
The documentation is installed with the package: read it with \`npx alveolus explain <topic>\`, not on the web. \`npx alveolus explain\` lists every topic.
|
|
2784
|
+
|
|
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\`.
|
|
2786
|
+
2. Where a file goes and what each layer may import: \`npx alveolus explain project-layout\`.
|
|
2787
|
+
3. After each change, run \`npx alveolus arch check\`.
|
|
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.
|
|
2789
|
+
`,
|
|
2790
|
+
path: ".claude/skills/alveolus/SKILL.md"
|
|
2791
|
+
},
|
|
2792
|
+
{
|
|
2793
|
+
content: `## Alveolus
|
|
2794
|
+
|
|
2795
|
+
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\`.
|
|
2796
|
+
`,
|
|
2797
|
+
marker: "npx alveolus explain",
|
|
2798
|
+
path: "AGENTS.md"
|
|
2799
|
+
}
|
|
2800
|
+
];
|
|
2801
|
+
//#endregion
|
|
2802
|
+
//#region src/init/init.ts
|
|
2803
|
+
var Init = class {
|
|
2804
|
+
projectDir;
|
|
2805
|
+
constructor(projectDir) {
|
|
2806
|
+
this.projectDir = projectDir;
|
|
2807
|
+
}
|
|
2808
|
+
run() {
|
|
2809
|
+
const written = [];
|
|
2810
|
+
for (const scaffold of scaffolds) written.push({
|
|
2811
|
+
outcome: this.write(scaffold),
|
|
2812
|
+
path: scaffold.path
|
|
2813
|
+
});
|
|
2814
|
+
return written;
|
|
2815
|
+
}
|
|
2816
|
+
get hint() {
|
|
2817
|
+
const claude = join(this.projectDir, "CLAUDE.md");
|
|
2818
|
+
if (!existsSync(claude)) return;
|
|
2819
|
+
return "CLAUDE.md exists: Claude Code reads it instead of AGENTS.md, so add a line with @AGENTS.md to it.";
|
|
2820
|
+
}
|
|
2821
|
+
write(scaffold) {
|
|
2822
|
+
const path = join(this.projectDir, scaffold.path);
|
|
2823
|
+
if (!existsSync(path)) {
|
|
2824
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
2825
|
+
writeFileSync(path, scaffold.content);
|
|
2826
|
+
return "created";
|
|
2827
|
+
}
|
|
2828
|
+
if (scaffold.marker === void 0) return "kept";
|
|
2829
|
+
const existing = readFileSync(path, "utf8");
|
|
2830
|
+
if (existing.includes(scaffold.marker)) return "kept";
|
|
2831
|
+
writeFileSync(path, `${existing.trimEnd()}\n\n${scaffold.content}`);
|
|
2832
|
+
return "appended";
|
|
2833
|
+
}
|
|
2834
|
+
};
|
|
2776
2835
|
//#endregion
|
|
2777
2836
|
//#region src/cli/cli.ts
|
|
2778
2837
|
var Cli = class {
|
|
2779
2838
|
stdout;
|
|
2780
2839
|
stderr;
|
|
2781
2840
|
cwd;
|
|
2841
|
+
docs;
|
|
2782
2842
|
colored;
|
|
2783
2843
|
exitCode = 0;
|
|
2784
|
-
constructor(stdout, stderr, cwd, colored = false) {
|
|
2844
|
+
constructor(stdout, stderr, cwd, docs, colored = false) {
|
|
2785
2845
|
this.stdout = stdout;
|
|
2786
2846
|
this.stderr = stderr;
|
|
2787
2847
|
this.cwd = cwd;
|
|
2848
|
+
this.docs = docs;
|
|
2788
2849
|
this.colored = colored;
|
|
2789
2850
|
}
|
|
2790
2851
|
async run(args) {
|
|
@@ -2804,8 +2865,30 @@ var Cli = class {
|
|
|
2804
2865
|
const arch = program.command("arch").description("Check the architecture of a Domain-Driven Design project");
|
|
2805
2866
|
this.withOptions(arch.command("check").description("Report the violations that are not in the baseline")).action((options) => this.check(options));
|
|
2806
2867
|
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));
|
|
2868
|
+
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));
|
|
2869
|
+
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));
|
|
2807
2870
|
return program;
|
|
2808
2871
|
}
|
|
2872
|
+
explain(topic) {
|
|
2873
|
+
if (topic === void 0) {
|
|
2874
|
+
this.stdout.write(`${this.docs.topics().join("\n")}\n`);
|
|
2875
|
+
return;
|
|
2876
|
+
}
|
|
2877
|
+
const match = this.docs.find(topic);
|
|
2878
|
+
if ("page" in match) {
|
|
2879
|
+
this.stdout.write(match.page.text());
|
|
2880
|
+
return;
|
|
2881
|
+
}
|
|
2882
|
+
const list = match.candidates.length === 0 ? "alveolus explain lists the topics." : `Did you mean ${match.candidates.join(", ")}?`;
|
|
2883
|
+
this.stderr.write(`No page for ${topic}: ${list}\n`);
|
|
2884
|
+
this.exitCode = 1;
|
|
2885
|
+
}
|
|
2886
|
+
init(options) {
|
|
2887
|
+
const init = new Init(resolve(this.cwd, options.project));
|
|
2888
|
+
for (const { outcome, path } of init.run()) this.stdout.write(`${outcome} ${path}\n`);
|
|
2889
|
+
this.stdout.write(`Name your bounded contexts in ${ConfigLoader.fileName}, then run alveolus arch check.\n`);
|
|
2890
|
+
if (init.hint !== void 0) this.stderr.write(`${init.hint}\n`);
|
|
2891
|
+
}
|
|
2809
2892
|
withOptions(command) {
|
|
2810
2893
|
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([
|
|
2811
2894
|
"text",
|
|
@@ -2869,6 +2952,94 @@ var Cli = class {
|
|
|
2869
2952
|
}
|
|
2870
2953
|
};
|
|
2871
2954
|
//#endregion
|
|
2872
|
-
|
|
2955
|
+
//#region src/docs/page.ts
|
|
2956
|
+
const html = [
|
|
2957
|
+
{
|
|
2958
|
+
pattern: /<dt>/g,
|
|
2959
|
+
text: "- "
|
|
2960
|
+
},
|
|
2961
|
+
{
|
|
2962
|
+
pattern: /<\/dt>\s*<dd>/g,
|
|
2963
|
+
text: ": "
|
|
2964
|
+
},
|
|
2965
|
+
{
|
|
2966
|
+
pattern: /<[^>\n]+>/g,
|
|
2967
|
+
text: ""
|
|
2968
|
+
},
|
|
2969
|
+
{
|
|
2970
|
+
pattern: /^\t+- /gm,
|
|
2971
|
+
text: "- "
|
|
2972
|
+
},
|
|
2973
|
+
{
|
|
2974
|
+
pattern: /</g,
|
|
2975
|
+
text: "<"
|
|
2976
|
+
},
|
|
2977
|
+
{
|
|
2978
|
+
pattern: />/g,
|
|
2979
|
+
text: ">"
|
|
2980
|
+
},
|
|
2981
|
+
{
|
|
2982
|
+
pattern: /&/g,
|
|
2983
|
+
text: "&"
|
|
2984
|
+
}
|
|
2985
|
+
];
|
|
2986
|
+
const frontmatter = /^---\n([\s\S]*?)\n---\n/;
|
|
2987
|
+
var Page = class {
|
|
2988
|
+
topic;
|
|
2989
|
+
source;
|
|
2990
|
+
constructor(topic, source) {
|
|
2991
|
+
this.topic = topic;
|
|
2992
|
+
this.source = source;
|
|
2993
|
+
}
|
|
2994
|
+
get description() {
|
|
2995
|
+
const header = frontmatter.exec(this.source)?.[1] ?? "";
|
|
2996
|
+
return /^description: "?(.*?)"?$/m.exec(header)?.[1] ?? "";
|
|
2997
|
+
}
|
|
2998
|
+
text() {
|
|
2999
|
+
let text = this.source.replace(frontmatter, "");
|
|
3000
|
+
for (const { pattern, text: replacement } of html) text = text.replace(pattern, replacement);
|
|
3001
|
+
return `${text.replace(/\n{3,}/g, "\n\n").trim()}\n`;
|
|
3002
|
+
}
|
|
3003
|
+
};
|
|
3004
|
+
//#endregion
|
|
3005
|
+
//#region src/docs/docs.ts
|
|
3006
|
+
var Docs = class Docs {
|
|
3007
|
+
dir;
|
|
3008
|
+
static sections = [
|
|
3009
|
+
"guide",
|
|
3010
|
+
"integrations",
|
|
3011
|
+
"core",
|
|
3012
|
+
"rules"
|
|
3013
|
+
];
|
|
3014
|
+
constructor(dir) {
|
|
3015
|
+
this.dir = dir;
|
|
3016
|
+
}
|
|
3017
|
+
topics() {
|
|
3018
|
+
const topics = [];
|
|
3019
|
+
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)));
|
|
3020
|
+
return topics.sort();
|
|
3021
|
+
}
|
|
3022
|
+
find(name) {
|
|
3023
|
+
const wanted = name.replace(/\.md$/, "").replace(/\/$/, "").replace(/\\/g, "/");
|
|
3024
|
+
const candidates = this.topics().filter((topic) => topic === wanted || topic.endsWith(`/${wanted}`));
|
|
3025
|
+
const exact = candidates.find((topic) => topic === wanted);
|
|
3026
|
+
if (exact !== void 0) return { page: this.read(exact) };
|
|
3027
|
+
if (candidates.length === 1 && candidates[0] !== void 0) return { page: this.read(candidates[0]) };
|
|
3028
|
+
return { candidates };
|
|
3029
|
+
}
|
|
3030
|
+
read(topic) {
|
|
3031
|
+
const path = join(this.dir, ...topic.split("/"));
|
|
3032
|
+
try {
|
|
3033
|
+
return new Page(topic, readFileSync(`${path}.md`, "utf8"));
|
|
3034
|
+
} catch {
|
|
3035
|
+
return new Page(topic, readFileSync(join(path, "index.md"), "utf8"));
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
topicOf(file) {
|
|
3039
|
+
return file.split(sep).join("/").replace(/\.md$/, "").replace(/\/index$/, "");
|
|
3040
|
+
}
|
|
3041
|
+
};
|
|
3042
|
+
//#endregion
|
|
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 };
|
|
2873
3044
|
|
|
2874
|
-
//# sourceMappingURL=
|
|
3045
|
+
//# sourceMappingURL=docs-DcFgskuN.mjs.map
|