@alveolus/arch 0.1.0-alpha.0 → 0.2.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.
@@ -1,1229 +0,0 @@
1
- import { Command, CommanderError } from "commander";
2
- import { basename, dirname, isAbsolute, join, matchesGlob, relative, resolve, sep } from "node:path";
3
- import { Project, Scope, SyntaxKind } from "ts-morph";
4
- import { existsSync } from "node:fs";
5
- import { readFile, writeFile } from "node:fs/promises";
6
- import { createJiti } from "jiti";
7
- import { z } from "zod";
8
- import picocolors from "picocolors";
9
- //#region src/analysis/code-analyzer.ts
10
- var CodeAnalyzer = class {};
11
- //#endregion
12
- //#region src/codebase/code-class.ts
13
- var CodeClass = class {
14
- name;
15
- line;
16
- isAbstract;
17
- constructorParameters;
18
- fields;
19
- publicMethods;
20
- kinds;
21
- markers;
22
- constructor(props) {
23
- this.name = props.name;
24
- this.line = props.line;
25
- this.isAbstract = props.isAbstract;
26
- this.kinds = props.kinds;
27
- this.markers = props.markers;
28
- this.constructorParameters = props.constructorParameters;
29
- this.fields = props.fields;
30
- this.publicMethods = props.publicMethods;
31
- }
32
- is(kind) {
33
- return this.kinds.includes(kind);
34
- }
35
- implements(marker) {
36
- return this.markers.includes(marker);
37
- }
38
- get extendsBuildingBlock() {
39
- return this.kinds.length > 0;
40
- }
41
- get hasMarker() {
42
- return this.markers.length > 0;
43
- }
44
- };
45
- //#endregion
46
- //#region src/codebase/code-file.ts
47
- var CodeFile = class {
48
- path;
49
- location;
50
- imports;
51
- classes;
52
- declarations;
53
- domainErrorThrows;
54
- constructor(props) {
55
- this.path = props.path;
56
- this.location = props.location;
57
- this.imports = props.imports;
58
- this.classes = props.classes;
59
- this.declarations = props.declarations;
60
- this.domainErrorThrows = props.domainErrorThrows;
61
- }
62
- declaresOpenHostService(name) {
63
- return this.classes.some((codeClass) => codeClass.name === name && codeClass.implements("OpenHostService"));
64
- }
65
- get declaresAntiCorruptionLayer() {
66
- return this.classes.some((codeClass) => codeClass.implements("AntiCorruptionLayer"));
67
- }
68
- };
69
- //#endregion
70
- //#region src/codebase/codebase.ts
71
- var Codebase = class {
72
- files;
73
- config;
74
- filesByPath;
75
- constructor(files, config) {
76
- this.files = files;
77
- this.config = config;
78
- this.filesByPath = new Map(files.map((file) => [file.path, file]));
79
- }
80
- file(path) {
81
- return this.filesByPath.get(path);
82
- }
83
- relativePath(path) {
84
- return relative(this.config.projectDir, path).split(sep).join("/");
85
- }
86
- };
87
- //#endregion
88
- //#region src/codebase/core-api.ts
89
- var CoreApi = class CoreApi {
90
- static packageName = "@alveolus/core";
91
- static domainSymbols = /* @__PURE__ */ new Set([
92
- "AggregateRoot",
93
- "AnyAggregateRoot",
94
- "AnyDomainError",
95
- "AnyDomainEvent",
96
- "AnyEntity",
97
- "AnyIdentifier",
98
- "AnySnapshot",
99
- "AnyValueObject",
100
- "Clock",
101
- "CommandRepository",
102
- "DomainError",
103
- "DomainEvent",
104
- "DomainEventProps",
105
- "DomainService",
106
- "Entity",
107
- "Err",
108
- "IdGenerator",
109
- "Identifier",
110
- "IdentifierValue",
111
- "Ok",
112
- "Port",
113
- "QueryRepository",
114
- "Result",
115
- "SnapshotValue",
116
- "ValueObject",
117
- "View",
118
- "andThen",
119
- "combine",
120
- "err",
121
- "map",
122
- "mapErr",
123
- "ok"
124
- ]);
125
- static publishedLanguageSymbols = /* @__PURE__ */ new Set([
126
- "AnyIntegrationEvent",
127
- "IntegrationEvent",
128
- "JsonValue",
129
- "PublishedLanguage"
130
- ]);
131
- static markers = /* @__PURE__ */ new Set(["OpenHostService", "AntiCorruptionLayer"]);
132
- static kinds = /* @__PURE__ */ new Set([
133
- "AggregateRoot",
134
- "Entity",
135
- "ValueObject",
136
- "Identifier",
137
- "DomainEvent",
138
- "DomainError",
139
- "DomainService",
140
- "Port",
141
- "CommandRepository",
142
- "QueryRepository",
143
- "CommandHandler",
144
- "QueryHandler",
145
- "EventTranslator",
146
- "EventPublisher",
147
- "Outbox",
148
- "UnitOfWork"
149
- ]);
150
- static isDomainSymbol(name) {
151
- return CoreApi.domainSymbols.has(name);
152
- }
153
- static isPublishedLanguageSymbol(name) {
154
- return CoreApi.publishedLanguageSymbols.has(name);
155
- }
156
- static isKind(name) {
157
- return CoreApi.kinds.has(name);
158
- }
159
- static isMarker(name) {
160
- return CoreApi.markers.has(name);
161
- }
162
- };
163
- //#endregion
164
- //#region src/codebase/declaration.ts
165
- var Declaration = class {
166
- kind;
167
- name;
168
- line;
169
- constructor(kind, name, line) {
170
- this.kind = kind;
171
- this.name = name;
172
- this.line = line;
173
- }
174
- };
175
- //#endregion
176
- //#region src/codebase/import.ts
177
- var Import = class {
178
- line;
179
- specifier;
180
- names;
181
- target;
182
- constructor(line, specifier, names, target) {
183
- this.line = line;
184
- this.specifier = specifier;
185
- this.names = names;
186
- this.target = target;
187
- }
188
- get label() {
189
- return this.names.length === 0 ? this.specifier : this.names.join(", ");
190
- }
191
- isFromPackage(name) {
192
- return this.target.kind === "package" && this.target.name === name;
193
- }
194
- };
195
- //#endregion
196
- //#region src/codebase/location.ts
197
- var Location = class {
198
- area;
199
- context;
200
- layer;
201
- folder;
202
- fileName;
203
- isCompositionRoot;
204
- constructor(props) {
205
- this.area = props.area;
206
- this.context = props.context;
207
- this.layer = props.layer;
208
- this.folder = props.folder;
209
- this.fileName = props.fileName;
210
- this.isCompositionRoot = props.isCompositionRoot ?? false;
211
- }
212
- get isInBoundedContext() {
213
- return this.area === "context";
214
- }
215
- get isInSharedKernel() {
216
- return this.area === "shared-kernel";
217
- }
218
- get isAtRoot() {
219
- return this.area === "root";
220
- }
221
- get isOutside() {
222
- return this.area === "outside";
223
- }
224
- get isInLayer() {
225
- return this.layer !== void 0;
226
- }
227
- isSameContextAs(other) {
228
- return this.context !== void 0 && this.context === other.context;
229
- }
230
- isOtherBoundedContextThan(other) {
231
- return this.isInBoundedContext && !this.isSameContextAs(other);
232
- }
233
- describe() {
234
- if (this.isOutside) return "outside the declared bounded contexts and shared kernel";
235
- if (this.isAtRoot) return "at the root";
236
- const role = this.isCompositionRoot ? "composition root" : this.layer ?? "outside the layers";
237
- return `${this.context} ${role}`;
238
- }
239
- };
240
- //#endregion
241
- //#region src/codebase/layout.ts
242
- const layers = /* @__PURE__ */ new Set([
243
- "domain",
244
- "application",
245
- "published-language",
246
- "driven",
247
- "driving"
248
- ]);
249
- var Layout = class {
250
- config;
251
- constructor(config) {
252
- this.config = config;
253
- }
254
- locate(path) {
255
- const fileName = basename(path);
256
- const folder = this.contextFolderOf(path);
257
- if (folder === void 0) return new Location({
258
- area: dirname(path) === this.config.rootDir ? "root" : "outside",
259
- fileName
260
- });
261
- const area = folder.isSharedKernel ? "shared-kernel" : "context";
262
- const context = folder.name;
263
- const directories = this.directoriesBetween(folder.dir, path);
264
- if (directories.length === 0) return new Location({
265
- area,
266
- context,
267
- fileName,
268
- isCompositionRoot: this.isCompositionRoot(fileName)
269
- });
270
- const layerIndex = directories.findIndex((directory) => layers.has(directory));
271
- if (layerIndex === -1) return new Location({
272
- area,
273
- context,
274
- fileName,
275
- isCompositionRoot: folder.isSharedKernel && directories.length === 1 && this.isCompositionRoot(fileName)
276
- });
277
- const layer = directories[layerIndex];
278
- const subfolder = directories.length - 1 > layerIndex ? directories.at(-1) : void 0;
279
- return new Location({
280
- area,
281
- context,
282
- fileName,
283
- layer,
284
- ...subfolder === void 0 ? {} : { folder: subfolder }
285
- });
286
- }
287
- isCompositionRoot(fileName) {
288
- return matchesGlob(fileName, this.config.compositionRoot);
289
- }
290
- contextFolderOf(path) {
291
- return this.config.contextFolders.filter((folder) => this.isInside(folder.dir, path)).sort((left, right) => right.dir.length - left.dir.length)[0];
292
- }
293
- directoriesBetween(dir, path) {
294
- const between = relative(dir, dirname(path));
295
- return between === "" ? [] : between.split(sep);
296
- }
297
- isInside(dir, path) {
298
- const offset = relative(dir, path);
299
- return offset !== "" && !offset.startsWith("..") && !isAbsolute(offset);
300
- }
301
- };
302
- //#endregion
303
- //#region src/codebase/method.ts
304
- var Method = class {
305
- name;
306
- line;
307
- returnsResult;
308
- constructor(name, line, returnsResult) {
309
- this.name = name;
310
- this.line = line;
311
- this.returnsResult = returnsResult;
312
- }
313
- };
314
- //#endregion
315
- //#region src/codebase/type-usage.ts
316
- var TypeUsage = class {
317
- member;
318
- line;
319
- typeName;
320
- kinds;
321
- constructor(member, line, typeName, kinds) {
322
- this.member = member;
323
- this.line = line;
324
- this.typeName = typeName;
325
- this.kinds = kinds;
326
- }
327
- is(kind) {
328
- return this.kinds.includes(kind);
329
- }
330
- };
331
- //#endregion
332
- //#region src/analysis/class-reader.ts
333
- var ClassReader = class {
334
- types;
335
- constructor(types) {
336
- this.types = types;
337
- }
338
- read(declaration) {
339
- const parameters = declaration.getConstructors()[0]?.getParameters() ?? [];
340
- const parameterProperties = parameters.filter((parameter) => parameter.isParameterProperty());
341
- return new CodeClass({
342
- constructorParameters: parameters.flatMap((parameter) => this.usagesOf(parameter)),
343
- fields: [...declaration.getProperties(), ...parameterProperties].flatMap((member) => this.usagesOf(member)),
344
- isAbstract: declaration.isAbstract(),
345
- kinds: this.types.kindsOf(declaration.getType()),
346
- line: declaration.getStartLineNumber(),
347
- markers: this.markersOf(declaration),
348
- name: declaration.getName() ?? "default",
349
- publicMethods: this.publicMethodsOf(declaration)
350
- });
351
- }
352
- markersOf(declaration) {
353
- const markers = [];
354
- for (const clause of declaration.getImplements()) {
355
- const marker = this.types.markerOf(clause.getType());
356
- if (marker !== void 0) markers.push(marker);
357
- }
358
- return markers;
359
- }
360
- publicMethodsOf(declaration) {
361
- return declaration.getMethods().filter((method) => !method.isStatic() && method.getScope() === Scope.Public).map((method) => new Method(method.getName(), method.getStartLineNumber(), this.types.isResult(method.getReturnType())));
362
- }
363
- usagesOf(member) {
364
- return this.types.classTypesIn(member.getType()).map((type) => new TypeUsage(member.getName(), member.getStartLineNumber(), type.getSymbol()?.getName() ?? type.getText(), this.types.kindsOf(type)));
365
- }
366
- };
367
- //#endregion
368
- //#region src/analysis/package-names.ts
369
- var PackageNames = class {
370
- fileSystem;
371
- cache = /* @__PURE__ */ new Map();
372
- constructor(fileSystem) {
373
- this.fileSystem = fileSystem;
374
- }
375
- ofFile(path) {
376
- return this.ofDirectory(dirname(path));
377
- }
378
- ofDirectory(directory) {
379
- if (this.cache.has(directory)) return this.cache.get(directory);
380
- const name = this.readManifest(directory) ?? this.ofParent(directory);
381
- this.cache.set(directory, name);
382
- return name;
383
- }
384
- ofParent(directory) {
385
- const parent = dirname(directory);
386
- return parent === directory ? void 0 : this.ofDirectory(parent);
387
- }
388
- readManifest(directory) {
389
- const manifest = join(directory, "package.json");
390
- if (!this.fileSystem.fileExistsSync(manifest)) return;
391
- const content = JSON.parse(this.fileSystem.readFileSync(manifest));
392
- return typeof content.name === "string" ? content.name : void 0;
393
- }
394
- };
395
- //#endregion
396
- //#region src/analysis/type-inspector.ts
397
- const containers = /* @__PURE__ */ new Set([
398
- "Array",
399
- "ReadonlyArray",
400
- "Map",
401
- "ReadonlyMap",
402
- "Set",
403
- "ReadonlySet",
404
- "Readonly",
405
- "Promise"
406
- ]);
407
- var TypeInspector = class {
408
- packageNames;
409
- constructor(packageNames) {
410
- this.packageNames = packageNames;
411
- }
412
- kindsOf(type) {
413
- const classType = type.getTargetType() ?? type;
414
- const symbol = classType.getSymbol();
415
- if (symbol === void 0 || !classType.isClass()) return [];
416
- const kinds = [];
417
- const name = symbol.getName();
418
- if (CoreApi.isKind(name) && this.isCore(symbol)) kinds.push(name);
419
- for (const base of classType.getBaseTypes()) kinds.push(...this.kindsOf(base));
420
- return kinds;
421
- }
422
- markerOf(type) {
423
- const symbol = type.getSymbol();
424
- if (symbol === void 0) return;
425
- const name = symbol.getName();
426
- return CoreApi.isMarker(name) && this.isCore(symbol) ? name : void 0;
427
- }
428
- isResult(type) {
429
- const alias = type.getAliasSymbol();
430
- if (alias !== void 0 && alias.getName() === "Result" && this.isCore(alias)) return true;
431
- return (type.isUnion() ? type.getUnionTypes() : [type]).every((member) => this.isOkOrErr(member));
432
- }
433
- classTypesIn(type) {
434
- if (type.isUnion()) return type.getUnionTypes().flatMap((member) => this.classTypesIn(member));
435
- if (type.isIntersection()) return type.getIntersectionTypes().flatMap((member) => this.classTypesIn(member));
436
- const element = type.getArrayElementType();
437
- if (element !== void 0) return this.classTypesIn(element);
438
- if (this.isContainer(type)) return [...type.getTypeArguments(), ...type.getAliasTypeArguments()].flatMap((argument) => this.classTypesIn(argument));
439
- return type.isClass() ? [type] : [];
440
- }
441
- isContainer(type) {
442
- const name = type.getSymbol()?.getName() ?? type.getAliasSymbol()?.getName();
443
- return name !== void 0 && containers.has(name);
444
- }
445
- isOkOrErr(type) {
446
- const symbol = type.getSymbol();
447
- if (symbol === void 0) return false;
448
- const name = symbol.getName();
449
- return (name === "Ok" || name === "Err") && this.isCore(symbol);
450
- }
451
- isCore(symbol) {
452
- const declaration = symbol.getDeclarations()[0];
453
- if (declaration === void 0) return false;
454
- return this.packageNames.ofFile(declaration.getSourceFile().getFilePath()) === CoreApi.packageName;
455
- }
456
- };
457
- //#endregion
458
- //#region src/analysis/ts-morph-analyzer.ts
459
- var TsMorphAnalyzer = class TsMorphAnalyzer extends CodeAnalyzer {
460
- project;
461
- types;
462
- classes;
463
- constructor(project) {
464
- super();
465
- this.project = project;
466
- this.types = new TypeInspector(new PackageNames(project.getFileSystem()));
467
- this.classes = new ClassReader(this.types);
468
- }
469
- static fromTsConfig(projectDir) {
470
- return new TsMorphAnalyzer(new Project({ tsConfigFilePath: join(projectDir, "tsconfig.json") }));
471
- }
472
- analyze(config) {
473
- const layout = new Layout(config);
474
- return new Codebase(this.project.getSourceFiles().filter((file) => this.isInside(config.rootDir, file.getFilePath()) && !config.isIgnored(file.getFilePath())).map((file) => this.read(file, config, layout)), config);
475
- }
476
- read(file, config, layout) {
477
- const path = file.getFilePath();
478
- const imports = this.moduleDeclarationsOf(file).map((declaration) => this.readImport(declaration, config, layout));
479
- return new CodeFile({
480
- classes: file.getClasses().map((declaration) => this.classes.read(declaration)),
481
- declarations: this.declarationsOf(file),
482
- domainErrorThrows: this.domainErrorThrowsOf(file),
483
- imports,
484
- location: layout.locate(path),
485
- path
486
- });
487
- }
488
- declarationsOf(file) {
489
- const functions = file.getFunctions().map((declaration) => new Declaration("function", declaration.getName() ?? "default", declaration.getStartLineNumber()));
490
- const functionConstants = file.getVariableDeclarations().filter((declaration) => declaration.getInitializerIfKind(SyntaxKind.ArrowFunction) !== void 0 || declaration.getInitializerIfKind(SyntaxKind.FunctionExpression) !== void 0).map((declaration) => new Declaration("function", declaration.getName(), declaration.getStartLineNumber()));
491
- const enums = file.getEnums().map((declaration) => new Declaration("enum", declaration.getName(), declaration.getStartLineNumber()));
492
- return [
493
- ...functions,
494
- ...functionConstants,
495
- ...enums
496
- ];
497
- }
498
- moduleDeclarationsOf(file) {
499
- const reexports = file.getExportDeclarations().filter((declaration) => declaration.hasModuleSpecifier());
500
- return [...file.getImportDeclarations(), ...reexports];
501
- }
502
- readImport(declaration, config, layout) {
503
- const specifier = declaration.getModuleSpecifierValue() ?? "";
504
- return new Import(declaration.getStartLineNumber(), specifier, this.importedNames(declaration), this.targetOf(declaration, specifier, config, layout));
505
- }
506
- importedNames(declaration) {
507
- if (declaration.isKind(SyntaxKind.ExportDeclaration)) return declaration.isNamespaceExport() ? ["*"] : declaration.getNamedExports().map((named) => named.getName());
508
- const names = declaration.getNamedImports().map((named) => named.getName());
509
- if (declaration.getDefaultImport() !== void 0) names.unshift("default");
510
- if (declaration.getNamespaceImport() !== void 0) names.unshift("*");
511
- return names;
512
- }
513
- targetOf(declaration, specifier, config, layout) {
514
- const resolved = declaration.getModuleSpecifierSourceFile()?.getFilePath();
515
- if (resolved !== void 0 && this.isInside(config.projectDir, resolved)) return {
516
- kind: "file",
517
- location: layout.locate(resolved),
518
- path: resolved
519
- };
520
- if (specifier.startsWith(".")) {
521
- const path = join(declaration.getSourceFile().getDirectoryPath(), specifier);
522
- return {
523
- kind: "file",
524
- location: layout.locate(path),
525
- path
526
- };
527
- }
528
- return {
529
- kind: "package",
530
- name: this.packageNameOf(specifier)
531
- };
532
- }
533
- packageNameOf(specifier) {
534
- const segments = specifier.split("/");
535
- const length = specifier.startsWith("@") ? 2 : 1;
536
- return segments.slice(0, length).join("/");
537
- }
538
- domainErrorThrowsOf(file) {
539
- return file.getDescendantsOfKind(SyntaxKind.ThrowStatement).filter((statement) => this.types.kindsOf(statement.getExpression().getType()).includes("DomainError")).map((statement) => statement.getStartLineNumber());
540
- }
541
- isInside(directory, path) {
542
- const offset = relative(directory, path);
543
- return offset !== "" && !offset.startsWith("..") && !isAbsolute(offset) && !offset.split(/[\\/]/).includes("node_modules");
544
- }
545
- };
546
- //#endregion
547
- //#region src/baseline/baseline.ts
548
- var Baseline = class Baseline {
549
- entries;
550
- static fileName = "alveolus.baseline.json";
551
- constructor(entries) {
552
- this.entries = entries;
553
- }
554
- static of(violations) {
555
- return new Baseline(violations.map(({ file, rule, symbol }) => ({
556
- file,
557
- rule,
558
- symbol
559
- })));
560
- }
561
- static async load(path) {
562
- if (!existsSync(path)) return new Baseline([]);
563
- const content = JSON.parse(await readFile(path, "utf8"));
564
- return new Baseline(content.violations ?? []);
565
- }
566
- newViolations(violations) {
567
- const remaining = /* @__PURE__ */ new Map();
568
- for (const entry of this.entries) remaining.set(this.keyOf(entry), (remaining.get(this.keyOf(entry)) ?? 0) + 1);
569
- const fresh = [];
570
- for (const violation of violations) {
571
- const key = this.keyOf(violation);
572
- const count = remaining.get(key) ?? 0;
573
- if (count > 0) remaining.set(key, count - 1);
574
- else fresh.push(violation);
575
- }
576
- return fresh;
577
- }
578
- async save(path) {
579
- const violations = [...this.entries].sort((left, right) => this.keyOf(left).localeCompare(this.keyOf(right)));
580
- await writeFile(path, `${JSON.stringify({ violations }, null, " ")}\n`);
581
- }
582
- keyOf(entry) {
583
- return [
584
- entry.rule,
585
- entry.file,
586
- entry.symbol
587
- ].join("\n");
588
- }
589
- };
590
- //#endregion
591
- //#region src/checker/arch-checker.ts
592
- var ArchChecker = class {
593
- analyzer;
594
- rules;
595
- constructor(analyzer, rules) {
596
- this.analyzer = analyzer;
597
- this.rules = rules;
598
- }
599
- check(config) {
600
- const codebase = this.analyzer.analyze(config);
601
- return this.rules.filter((rule) => config.isEnabled(rule.id)).flatMap((rule) => rule.check(codebase)).sort((left, right) => this.compare(left, right));
602
- }
603
- compare(left, right) {
604
- return left.file.localeCompare(right.file) || left.line - right.line || left.rule.localeCompare(right.rule);
605
- }
606
- };
607
- //#endregion
608
- //#region src/config/config.ts
609
- const testFiles = [
610
- "**/*.spec.ts",
611
- "**/*.test.ts",
612
- "**/__tests__/**"
613
- ];
614
- var Config = class {
615
- projectDir;
616
- rootDir;
617
- compositionRoot;
618
- domainDependencies;
619
- contextFolders;
620
- ignored;
621
- rules;
622
- constructor(config, projectDir) {
623
- this.projectDir = resolve(projectDir);
624
- this.rootDir = resolve(projectDir, config.root);
625
- this.compositionRoot = config.compositionRoot ?? "*.module.ts";
626
- this.domainDependencies = config.domainDependencies ?? [];
627
- this.ignored = [...testFiles, ...config.ignore ?? []];
628
- this.rules = config.rules;
629
- const sharedKernel = config.sharedKernel ?? "shared-kernel";
630
- this.contextFolders = [...Object.entries(config.boundedContexts).map(([name, folder]) => ({
631
- dir: resolve(this.rootDir, folder),
632
- isSharedKernel: false,
633
- name
634
- })), {
635
- dir: resolve(this.rootDir, sharedKernel),
636
- isSharedKernel: true,
637
- name: "shared kernel"
638
- }];
639
- }
640
- isIgnored(path) {
641
- const projectPath = relative(this.projectDir, path).split(sep).join("/");
642
- return this.ignored.some((glob) => matchesGlob(projectPath, glob));
643
- }
644
- isEnabled(rule) {
645
- return this.rules?.[rule] !== "off";
646
- }
647
- };
648
- //#endregion
649
- //#region src/config/config-loader.ts
650
- const ruleSetting = z.enum(["error", "off"]);
651
- const schema = z.strictObject({
652
- boundedContexts: z.record(z.string(), z.string()),
653
- compositionRoot: z.string().exactOptional(),
654
- domainDependencies: z.array(z.string()).exactOptional(),
655
- ignore: z.array(z.string()).exactOptional(),
656
- root: z.string(),
657
- rules: z.strictObject({
658
- "bc-isolation": ruleSetting.exactOptional(),
659
- "building-blocks-only": ruleSetting.exactOptional(),
660
- "command-query-separation": ruleSetting.exactOptional(),
661
- "domain-purity": ruleSetting.exactOptional(),
662
- "driven-adapters-extend-port": ruleSetting.exactOptional(),
663
- "errors-as-values": ruleSetting.exactOptional(),
664
- "layer-direction": ruleSetting.exactOptional(),
665
- placement: ruleSetting.exactOptional(),
666
- "reference-by-identity": ruleSetting.exactOptional()
667
- }).exactOptional(),
668
- sharedKernel: z.string().exactOptional()
669
- });
670
- var ConfigLoader = class ConfigLoader {
671
- static fileName = "alveolus.config.ts";
672
- async load(projectDir, configFile = ConfigLoader.fileName) {
673
- const path = resolve(projectDir, configFile);
674
- if (!existsSync(path)) throw new Error(`No configuration found at ${path}.`);
675
- const exported = await createJiti(import.meta.url).import(path, { default: true });
676
- return new Config(this.validate(exported, path), projectDir);
677
- }
678
- validate(exported, path) {
679
- const parsed = schema.safeParse(exported);
680
- if (!parsed.success) throw new Error(`Invalid configuration in ${path}:\n${z.prettifyError(parsed.error)}`);
681
- return parsed.data;
682
- }
683
- };
684
- //#endregion
685
- //#region src/report/report.ts
686
- var Report = class {
687
- violations;
688
- baselined;
689
- colors;
690
- constructor(violations, baselined, colored = false) {
691
- this.violations = violations;
692
- this.baselined = baselined;
693
- this.colors = picocolors.createColors(colored);
694
- }
695
- text() {
696
- return `${[...this.violations.map((violation) => this.block(violation)), this.summary()].join("\n\n")}\n`;
697
- }
698
- json() {
699
- return `${JSON.stringify({
700
- baselined: this.baselined,
701
- violations: this.violations
702
- }, null, " ")}\n`;
703
- }
704
- block(violation) {
705
- const { bold, red } = this.colors;
706
- return `${bold(`${violation.file}:${violation.line}`)}\n ${red(violation.rule)}: ${violation.message}`;
707
- }
708
- summary() {
709
- const { dim, green, red } = this.colors;
710
- const count = this.violations.length;
711
- const baselined = this.baselined > 0 ? dim(` (${this.baselined} in the baseline)`) : "";
712
- if (count === 0) return `${green("No violation")}${baselined}`;
713
- return `${red(`${count} violation${count === 1 ? "" : "s"}`)}${baselined}`;
714
- }
715
- };
716
- //#endregion
717
- //#region src/rules/rule.ts
718
- var Rule = class {
719
- violation(codebase, file, problem) {
720
- return {
721
- file: codebase.relativePath(file.path),
722
- line: problem.line,
723
- message: problem.message,
724
- rule: this.id,
725
- symbol: problem.symbol
726
- };
727
- }
728
- };
729
- //#endregion
730
- //#region src/rules/import-rule.ts
731
- var ImportRule = class extends Rule {
732
- check(codebase) {
733
- const violations = [];
734
- for (const file of codebase.files) {
735
- if (!this.appliesTo(file)) continue;
736
- for (const imported of file.imports) {
737
- const message = this.problemWith(imported, file, codebase);
738
- if (message !== void 0) violations.push(this.violation(codebase, file, {
739
- line: imported.line,
740
- message,
741
- symbol: imported.label
742
- }));
743
- }
744
- }
745
- return violations;
746
- }
747
- describeTarget(imported, codebase) {
748
- if (imported.target.kind === "package") return imported.target.name;
749
- return `${codebase.relativePath(imported.target.path)} (${imported.target.location.describe()})`;
750
- }
751
- };
752
- //#endregion
753
- //#region src/rules/bc-isolation.rule.ts
754
- var BcIsolationRule = class extends ImportRule {
755
- id = "bc-isolation";
756
- appliesTo(file) {
757
- return file.location.isInBoundedContext || file.location.isInSharedKernel;
758
- }
759
- problemWith(imported, file, codebase) {
760
- if (imported.target.kind !== "file") return;
761
- const from = file.location;
762
- const to = imported.target.location;
763
- if (!to.isOtherBoundedContextThan(from)) return;
764
- if (from.isInSharedKernel) return `The shared kernel imports no bounded context, but imports ${this.describeTarget(imported, codebase)}.`;
765
- if (from.isCompositionRoot && to.isCompositionRoot) return;
766
- if (to.layer === "published-language") return `Imports the published language of ${to.context}: redeclare the fields you read in your own published-language/.`;
767
- if (!this.importsOnlyOpenHostServices(imported, codebase)) return `Imports ${this.describeTarget(imported, codebase)}: only an OpenHostService of another bounded context may be imported.`;
768
- if (!from.isCompositionRoot && !file.declaresAntiCorruptionLayer) return `Uses the open host service of ${to.context} outside an AntiCorruptionLayer: translate it in an anti-corruption layer.`;
769
- }
770
- importsOnlyOpenHostServices(imported, codebase) {
771
- if (imported.target.kind !== "file" || imported.names.length === 0) return false;
772
- const target = codebase.file(imported.target.path);
773
- return target !== void 0 && imported.names.every((name) => target.declaresOpenHostService(name));
774
- }
775
- };
776
- //#endregion
777
- //#region src/rules/building-blocks-only.rule.ts
778
- const guarded = /* @__PURE__ */ new Set(["domain", "application"]);
779
- const blocksOf = {
780
- application: "CommandHandler, QueryHandler or EventTranslator",
781
- domain: "AggregateRoot, Entity, ValueObject, Identifier, DomainEvent, DomainError, DomainService or a Port"
782
- };
783
- var BuildingBlocksOnlyRule = class extends Rule {
784
- id = "building-blocks-only";
785
- check(codebase) {
786
- const violations = [];
787
- for (const file of codebase.files) {
788
- if (!guarded.has(file.location.layer)) continue;
789
- for (const problem of [...this.plainClasses(file), ...this.freeDeclarations(file)]) violations.push(this.violation(codebase, file, problem));
790
- }
791
- return violations;
792
- }
793
- plainClasses(file) {
794
- const blocks = blocksOf[file.location.layer ?? ""];
795
- return file.classes.filter((codeClass) => !codeClass.extendsBuildingBlock).map((codeClass) => ({
796
- line: codeClass.line,
797
- message: `${codeClass.name} extends no building block: extend ${blocks}.`,
798
- symbol: codeClass.name
799
- }));
800
- }
801
- freeDeclarations(file) {
802
- return file.declarations.map((declaration) => {
803
- if (declaration.kind === "enum") return {
804
- line: declaration.line,
805
- message: `The enum ${declaration.name} has no place here: use a union of literal types, or a ValueObject when it has behaviour.`,
806
- symbol: declaration.name
807
- };
808
- return {
809
- line: declaration.line,
810
- message: `The function ${declaration.name} floats outside any class: make it a method of a value object or of a DomainService.`,
811
- symbol: declaration.name
812
- };
813
- });
814
- }
815
- };
816
- //#endregion
817
- //#region src/rules/class-rule.ts
818
- var ClassRule = class extends Rule {
819
- check(codebase) {
820
- const violations = [];
821
- for (const file of codebase.files) for (const codeClass of file.classes) for (const problem of this.problemsWith(codeClass, file)) violations.push(this.violation(codebase, file, problem));
822
- return violations;
823
- }
824
- };
825
- //#endregion
826
- //#region src/rules/command-query-separation.rule.ts
827
- const separations = [{
828
- forbidden: ["QueryRepository"],
829
- handler: "CommandHandler"
830
- }, {
831
- forbidden: [
832
- "CommandRepository",
833
- "Outbox",
834
- "UnitOfWork",
835
- "EventPublisher"
836
- ],
837
- handler: "QueryHandler"
838
- }];
839
- var CommandQuerySeparationRule = class extends ClassRule {
840
- id = "command-query-separation";
841
- problemsWith(codeClass) {
842
- const problems = [];
843
- for (const { handler, forbidden } of separations) {
844
- if (!codeClass.is(handler)) continue;
845
- for (const parameter of codeClass.constructorParameters) {
846
- const kind = forbidden.find((candidate) => parameter.is(candidate));
847
- if (kind !== void 0) problems.push({
848
- line: parameter.line,
849
- message: `The ${handler} ${codeClass.name} receives ${parameter.typeName}, a ${kind}: keep commands and queries apart.`,
850
- symbol: `${codeClass.name}.${parameter.member}`
851
- });
852
- }
853
- }
854
- return problems;
855
- }
856
- };
857
- //#endregion
858
- //#region src/rules/domain-purity.rule.ts
859
- var DomainPurityRule = class extends ImportRule {
860
- id = "domain-purity";
861
- appliesTo(file) {
862
- return file.location.layer === "domain";
863
- }
864
- problemWith(imported, file, codebase) {
865
- const target = imported.target;
866
- if (target.kind === "file") {
867
- if (this.isDomainReachableFrom(target.location, file.location)) return;
868
- return `The domain imports ${this.describeTarget(imported, codebase)}: it may only import the domain.`;
869
- }
870
- if (target.name === CoreApi.packageName) {
871
- const forbidden = imported.names.filter((name) => !CoreApi.isDomainSymbol(name));
872
- if (forbidden.length === 0) return;
873
- return `The domain imports ${forbidden.join(", ")} from ${CoreApi.packageName}: only domain building blocks and Result are allowed.`;
874
- }
875
- if (codebase.config.domainDependencies.includes(target.name)) return;
876
- return `The domain imports ${target.name}: add it to domainDependencies if the domain really needs it.`;
877
- }
878
- isDomainReachableFrom(to, from) {
879
- return to.layer === "domain" && (to.isSameContextAs(from) || to.isInSharedKernel);
880
- }
881
- };
882
- //#endregion
883
- //#region src/rules/driven-adapters-extend-port.rule.ts
884
- var DrivenAdaptersExtendPortRule = class extends ClassRule {
885
- id = "driven-adapters-extend-port";
886
- problemsWith(codeClass, file) {
887
- const location = file.location;
888
- if (this.isAdaptersFolder(location) && !codeClass.is("Port")) return [{
889
- line: codeClass.line,
890
- message: `${codeClass.name} is a driven adapter but extends no Port: extend the port it implements.`,
891
- symbol: codeClass.name
892
- }];
893
- if (codeClass.is("Port") && codeClass.isAbstract && !this.isPortsFolder(location) && !this.isAdaptersFolder(location)) return [{
894
- line: codeClass.line,
895
- message: `The port ${codeClass.name} is declared outside the domain: move it to domain/ports/ or domain/repositories/.`,
896
- symbol: codeClass.name
897
- }];
898
- return [];
899
- }
900
- isAdaptersFolder(location) {
901
- return location.layer === "driven" && location.folder === "adapters";
902
- }
903
- isPortsFolder(location) {
904
- return location.layer === "domain" && (location.folder === "ports" || location.folder === "repositories");
905
- }
906
- };
907
- //#endregion
908
- //#region src/rules/errors-as-values.rule.ts
909
- const exempt = /* @__PURE__ */ new Set(["equals", "toSnapshot"]);
910
- var ErrorsAsValuesRule = class extends Rule {
911
- id = "errors-as-values";
912
- check(codebase) {
913
- const violations = [];
914
- for (const file of codebase.files) for (const problem of [...this.methodsWithoutResult(file), ...this.thrownErrors(file)]) violations.push(this.violation(codebase, file, problem));
915
- return violations;
916
- }
917
- methodsWithoutResult(file) {
918
- const problems = [];
919
- for (const codeClass of file.classes.filter((candidate) => candidate.is("Entity"))) for (const method of codeClass.publicMethods) if (!exempt.has(method.name) && !method.returnsResult) problems.push({
920
- line: method.line,
921
- message: `${codeClass.name}.${method.name} must return a Result: expose reads as getters and return business failures as values.`,
922
- symbol: `${codeClass.name}.${method.name}`
923
- });
924
- return problems;
925
- }
926
- thrownErrors(file) {
927
- return file.domainErrorThrows.map((line) => ({
928
- line,
929
- message: "A DomainError is thrown: return it in a Result instead.",
930
- symbol: "throw"
931
- }));
932
- }
933
- };
934
- //#endregion
935
- //#region src/rules/layer-direction.rule.ts
936
- const allowedLayers = {
937
- application: [
938
- "domain",
939
- "application",
940
- "published-language"
941
- ],
942
- driven: [
943
- "domain",
944
- "application",
945
- "published-language",
946
- "driven"
947
- ],
948
- driving: [
949
- "domain",
950
- "application",
951
- "published-language",
952
- "driving"
953
- ],
954
- "published-language": ["published-language"]
955
- };
956
- var LayerDirectionRule = class extends ImportRule {
957
- id = "layer-direction";
958
- check(codebase) {
959
- return [...this.filesOutsideLayers(codebase), ...super.check(codebase)];
960
- }
961
- appliesTo(file) {
962
- const location = file.location;
963
- return location.isAtRoot || location.isCompositionRoot || location.isInLayer && location.layer !== "domain";
964
- }
965
- problemWith(imported, file, codebase) {
966
- if (imported.target.kind === "file") return this.fileProblem(file.location, imported.target.location, this.describeTarget(imported, codebase));
967
- return this.packageProblem(file.location, imported, codebase.config);
968
- }
969
- filesOutsideLayers(codebase) {
970
- const violations = [];
971
- for (const file of codebase.files) {
972
- const location = file.location;
973
- if (location.isOutside) violations.push(this.violation(codebase, file, {
974
- line: 1,
975
- message: "The file is outside the bounded contexts and the shared kernel declared in alveolus.config.ts: move it, or add it to ignore.",
976
- symbol: location.fileName
977
- }));
978
- else if (!location.isAtRoot && !location.isInLayer && !location.isCompositionRoot) violations.push(this.violation(codebase, file, {
979
- line: 1,
980
- message: "The file is outside the layers: move it to domain/, application/, published-language/, driven/ or driving/.",
981
- symbol: location.fileName
982
- }));
983
- }
984
- return violations;
985
- }
986
- fileProblem(from, to, target) {
987
- if (from.isAtRoot) return to.isInBoundedContext && !to.isCompositionRoot ? `Files at the root import composition roots only, not ${target}.` : void 0;
988
- if (from.isCompositionRoot || to.isOtherBoundedContextThan(from)) return;
989
- if (to.isCompositionRoot) return `Imports ${target}: only the composition root wires the layers.`;
990
- const layer = from.layer;
991
- if (layer === void 0 || layer === "domain") return;
992
- const allowed = allowedLayers[layer];
993
- if (to.layer !== void 0 && allowed.includes(to.layer)) return;
994
- return `The ${layer} layer imports ${target}: it may only import ${allowed.join(", ")}.`;
995
- }
996
- packageProblem(from, imported, config) {
997
- if (from.layer === "application") return this.applicationPackageProblem(imported, config);
998
- if (from.layer === "published-language" && imported.isFromPackage(CoreApi.packageName)) {
999
- const forbidden = imported.names.filter((name) => !CoreApi.isPublishedLanguageSymbol(name));
1000
- return forbidden.length === 0 ? void 0 : `The published language imports ${forbidden.join(", ")} from ${CoreApi.packageName}: only published-language types are allowed.`;
1001
- }
1002
- }
1003
- applicationPackageProblem(imported, config) {
1004
- if (imported.target.kind !== "package" || imported.isFromPackage(CoreApi.packageName)) return;
1005
- if (imported.isFromPackage("@nestjs/common")) {
1006
- const forbidden = imported.names.filter((name) => name !== "Injectable");
1007
- return forbidden.length === 0 ? void 0 : `The application imports ${forbidden.join(", ")} from @nestjs/common: only Injectable is allowed.`;
1008
- }
1009
- if (config.domainDependencies.includes(imported.target.name)) return;
1010
- return `The application imports ${imported.target.name}: only ${CoreApi.packageName}, Injectable from @nestjs/common and domainDependencies are allowed.`;
1011
- }
1012
- };
1013
- //#endregion
1014
- //#region src/rules/place.ts
1015
- var Place = class {
1016
- layer;
1017
- folder;
1018
- suffix;
1019
- constructor(layer, folder, suffix) {
1020
- this.layer = layer;
1021
- this.folder = folder;
1022
- this.suffix = suffix;
1023
- }
1024
- fits(location) {
1025
- if (!location.isInBoundedContext && !location.isInSharedKernel) return false;
1026
- if (location.layer !== this.layer) return false;
1027
- if (this.folder !== void 0 && location.folder !== this.folder) return false;
1028
- return this.suffix === void 0 || location.fileName.endsWith(this.suffix);
1029
- }
1030
- describe() {
1031
- return this.folder === void 0 ? `${this.layer}/` : `${this.layer}/${this.folder}/*${this.suffix ?? ".ts"}`;
1032
- }
1033
- };
1034
- //#endregion
1035
- //#region src/rules/placement.rule.ts
1036
- const repositories = new Place("domain", "repositories", ".repository.ts");
1037
- const kindPlaces = [
1038
- {
1039
- kind: "AggregateRoot",
1040
- place: new Place("domain", "aggregates", ".aggregate.ts")
1041
- },
1042
- {
1043
- kind: "Entity",
1044
- place: new Place("domain", "entities", ".entity.ts")
1045
- },
1046
- {
1047
- kind: "Identifier",
1048
- place: new Place("domain", "value-objects", ".identifier.ts")
1049
- },
1050
- {
1051
- kind: "ValueObject",
1052
- place: new Place("domain", "value-objects", ".value-object.ts")
1053
- },
1054
- {
1055
- kind: "DomainEvent",
1056
- place: new Place("domain", "events", ".event.ts")
1057
- },
1058
- {
1059
- kind: "DomainError",
1060
- place: new Place("domain", "errors", ".error.ts")
1061
- },
1062
- {
1063
- kind: "DomainService",
1064
- place: new Place("domain", "services", ".service.ts")
1065
- },
1066
- {
1067
- kind: "CommandHandler",
1068
- place: new Place("application", "commands", ".command.ts")
1069
- },
1070
- {
1071
- kind: "QueryHandler",
1072
- place: new Place("application", "queries", ".query.ts")
1073
- },
1074
- {
1075
- kind: "EventTranslator",
1076
- place: new Place("application", "translators", ".translator.ts")
1077
- },
1078
- {
1079
- concreteOnly: true,
1080
- kind: "Port",
1081
- place: new Place("driven", "adapters", ".adapter.ts")
1082
- },
1083
- {
1084
- kind: "CommandRepository",
1085
- place: repositories
1086
- },
1087
- {
1088
- kind: "QueryRepository",
1089
- place: repositories
1090
- },
1091
- {
1092
- kind: "Port",
1093
- place: new Place("domain", "ports", ".port.ts")
1094
- }
1095
- ];
1096
- const markedPlace = new Place("driving");
1097
- var PlacementRule = class extends Rule {
1098
- id = "placement";
1099
- check(codebase) {
1100
- const violations = [];
1101
- for (const file of codebase.files) for (const problem of [...this.extraClasses(file), ...this.misplacedClasses(file)]) violations.push(this.violation(codebase, file, problem));
1102
- return violations;
1103
- }
1104
- extraClasses(file) {
1105
- const [first, ...others] = file.classes;
1106
- return others.map((codeClass) => ({
1107
- line: codeClass.line,
1108
- message: `${codeClass.name} shares its file with ${first?.name}: one class per file.`,
1109
- symbol: codeClass.name
1110
- }));
1111
- }
1112
- misplacedClasses(file) {
1113
- const problems = [];
1114
- for (const codeClass of file.classes) {
1115
- const place = this.placeOf(codeClass);
1116
- if (place !== void 0 && !place.fits(file.location)) problems.push({
1117
- line: codeClass.line,
1118
- message: `${codeClass.name} belongs in ${place.describe()}.`,
1119
- symbol: codeClass.name
1120
- });
1121
- }
1122
- return problems;
1123
- }
1124
- placeOf(codeClass) {
1125
- const match = kindPlaces.find((entry) => codeClass.is(entry.kind) && !(entry.concreteOnly === true && codeClass.isAbstract));
1126
- if (match !== void 0) return match.place;
1127
- return codeClass.hasMarker ? markedPlace : void 0;
1128
- }
1129
- };
1130
- //#endregion
1131
- //#region src/rules/reference-by-identity.rule.ts
1132
- var ReferenceByIdentityRule = class extends ClassRule {
1133
- id = "reference-by-identity";
1134
- problemsWith(codeClass) {
1135
- if (!codeClass.is("Entity")) return [];
1136
- return this.membersOf(codeClass).filter((member) => member.is("AggregateRoot")).map((member) => ({
1137
- line: member.line,
1138
- message: `${codeClass.name}.${member.member} holds the aggregate ${member.typeName}: reference it by its identifier instead.`,
1139
- symbol: `${codeClass.name}.${member.member}`
1140
- }));
1141
- }
1142
- membersOf(codeClass) {
1143
- const byName = /* @__PURE__ */ new Map();
1144
- for (const member of [...codeClass.fields, ...codeClass.constructorParameters]) byName.set(`${member.member}:${member.typeName}`, member);
1145
- return [...byName.values()];
1146
- }
1147
- };
1148
- //#endregion
1149
- //#region src/rules/rules.ts
1150
- var Rules = class {
1151
- static all() {
1152
- return [
1153
- new BcIsolationRule(),
1154
- new DomainPurityRule(),
1155
- new LayerDirectionRule(),
1156
- new DrivenAdaptersExtendPortRule(),
1157
- new ReferenceByIdentityRule(),
1158
- new CommandQuerySeparationRule(),
1159
- new ErrorsAsValuesRule(),
1160
- new PlacementRule(),
1161
- new BuildingBlocksOnlyRule()
1162
- ];
1163
- }
1164
- };
1165
- //#endregion
1166
- //#region src/cli/cli.ts
1167
- var Cli = class {
1168
- stdout;
1169
- stderr;
1170
- cwd;
1171
- colored;
1172
- exitCode = 0;
1173
- constructor(stdout, stderr, cwd, colored = false) {
1174
- this.stdout = stdout;
1175
- this.stderr = stderr;
1176
- this.cwd = cwd;
1177
- this.colored = colored;
1178
- }
1179
- async run(args) {
1180
- try {
1181
- await this.program().parseAsync([...args], { from: "user" });
1182
- } catch (error) {
1183
- return this.fail(error);
1184
- }
1185
- return this.exitCode;
1186
- }
1187
- program() {
1188
- const program = new Command("alveolus").exitOverride().configureOutput({
1189
- writeErr: (text) => this.stderr.write(text),
1190
- writeOut: (text) => this.stdout.write(text)
1191
- });
1192
- const arch = program.command("arch").description("Check the architecture of a Domain-Driven Design project");
1193
- this.withOptions(arch.command("check").description("Report the violations that are not in the baseline")).action((options) => this.check(options));
1194
- this.withOptions(arch.command("baseline").description(`Write the current violations to ${Baseline.fileName}`)).action((options) => this.baseline(options));
1195
- return program;
1196
- }
1197
- withOptions(command) {
1198
- return command.option("--project <dir>", "project directory", ".").option("--config <file>", "configuration file", ConfigLoader.fileName).option("--format <format>", "text or json", "text");
1199
- }
1200
- async check(options) {
1201
- const { config, violations } = await this.analyze(options);
1202
- const fresh = (await Baseline.load(join(config.projectDir, Baseline.fileName))).newViolations(violations);
1203
- const report = new Report(fresh, violations.length - fresh.length, this.colored);
1204
- this.stdout.write(options.format === "json" ? report.json() : report.text());
1205
- this.exitCode = fresh.length === 0 ? 0 : 1;
1206
- }
1207
- async baseline(options) {
1208
- const { config, violations } = await this.analyze(options);
1209
- await Baseline.of(violations).save(join(config.projectDir, Baseline.fileName));
1210
- this.stdout.write(`${violations.length} violations written to ${Baseline.fileName}\n`);
1211
- }
1212
- async analyze(options) {
1213
- const projectDir = resolve(this.cwd, options.project);
1214
- const config = await new ConfigLoader().load(projectDir, options.config);
1215
- return {
1216
- config,
1217
- violations: new ArchChecker(TsMorphAnalyzer.fromTsConfig(config.projectDir), Rules.all()).check(config)
1218
- };
1219
- }
1220
- fail(error) {
1221
- if (error instanceof CommanderError) return error.exitCode === 0 ? 0 : 2;
1222
- this.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
1223
- return 2;
1224
- }
1225
- };
1226
- //#endregion
1227
- export { ConfigLoader as a, Baseline as c, Report as i, TsMorphAnalyzer as l, Rules as n, Config as o, Rule as r, ArchChecker as s, Cli as t, CodeAnalyzer as u };
1228
-
1229
- //# sourceMappingURL=cli-CW1zAnrP.mjs.map