@alveolus/arch 0.2.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 +6 -0
- package/dist/bin.mjs +4 -2
- package/dist/bin.mjs.map +1 -1
- package/dist/{cli-P5PwH9OE.mjs → docs-DsQHpTtV.mjs} +190 -5
- package/dist/docs-DsQHpTtV.mjs.map +1 -0
- package/dist/index.d.mts +44 -2
- 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-P5PwH9OE.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -35,8 +35,14 @@ Seventeen rules, each with a page that says what it reports, why, how to fix it
|
|
|
35
35
|
cannot see. A baseline for existing projects, `error` / `warn` / `info` levels, disable comments
|
|
36
36
|
with a reason, JSON and SARIF output for the pull request.
|
|
37
37
|
|
|
38
|
+
The documentation is installed with the package, for you and for a coding agent:
|
|
39
|
+
`npx alveolus explain layers/no-impure-domain` prints a rule, `npx alveolus explain aggregates` a
|
|
40
|
+
building block, and `npx alveolus init` writes the configuration and the instructions that tell
|
|
41
|
+
Claude Code, Cursor or Codex to read them there.
|
|
42
|
+
|
|
38
43
|
- [Rules](https://alveolus.dev/rules/)
|
|
39
44
|
- [Getting started](https://alveolus.dev/guide/getting-started)
|
|
40
45
|
- [Adopt it on an existing project](https://alveolus.dev/guide/existing-project)
|
|
46
|
+
- [Coding agents](https://alveolus.dev/guide/agents)
|
|
41
47
|
|
|
42
48
|
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-DsQHpTtV.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";
|
|
@@ -1997,7 +1997,9 @@ var Report = class {
|
|
|
1997
1997
|
text() {
|
|
1998
1998
|
const blocks = [];
|
|
1999
1999
|
for (const [file, violations] of this.byFile()) blocks.push(this.block(file, violations));
|
|
2000
|
-
|
|
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`;
|
|
2001
2003
|
}
|
|
2002
2004
|
json() {
|
|
2003
2005
|
const { baselined, files, stale, suppressed, violations } = this.input;
|
|
@@ -2773,18 +2775,91 @@ var TsMorphImporter = class TsMorphImporter extends Importer {
|
|
|
2773
2775
|
return offset !== "" && !offset.startsWith("..") && !isAbsolute(offset) && !offset.split(/[\\/]/).includes("node_modules");
|
|
2774
2776
|
}
|
|
2775
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
|
+
};
|
|
2776
2849
|
//#endregion
|
|
2777
2850
|
//#region src/cli/cli.ts
|
|
2778
2851
|
var Cli = class {
|
|
2779
2852
|
stdout;
|
|
2780
2853
|
stderr;
|
|
2781
2854
|
cwd;
|
|
2855
|
+
docs;
|
|
2782
2856
|
colored;
|
|
2783
2857
|
exitCode = 0;
|
|
2784
|
-
constructor(stdout, stderr, cwd, colored = false) {
|
|
2858
|
+
constructor(stdout, stderr, cwd, docs, colored = false) {
|
|
2785
2859
|
this.stdout = stdout;
|
|
2786
2860
|
this.stderr = stderr;
|
|
2787
2861
|
this.cwd = cwd;
|
|
2862
|
+
this.docs = docs;
|
|
2788
2863
|
this.colored = colored;
|
|
2789
2864
|
}
|
|
2790
2865
|
async run(args) {
|
|
@@ -2804,8 +2879,30 @@ var Cli = class {
|
|
|
2804
2879
|
const arch = program.command("arch").description("Check the architecture of a Domain-Driven Design project");
|
|
2805
2880
|
this.withOptions(arch.command("check").description("Report the violations that are not in the baseline")).action((options) => this.check(options));
|
|
2806
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));
|
|
2807
2884
|
return program;
|
|
2808
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
|
+
}
|
|
2809
2906
|
withOptions(command) {
|
|
2810
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([
|
|
2811
2908
|
"text",
|
|
@@ -2869,6 +2966,94 @@ var Cli = class {
|
|
|
2869
2966
|
}
|
|
2870
2967
|
};
|
|
2871
2968
|
//#endregion
|
|
2872
|
-
|
|
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 };
|
|
2873
3058
|
|
|
2874
|
-
//# sourceMappingURL=
|
|
3059
|
+
//# sourceMappingURL=docs-DsQHpTtV.mjs.map
|