@aoelite/refpact-ts 0.0.0-stage → 0.1.2
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/LICENSE +21 -0
- package/README.md +38 -2
- package/dist/baseline.d.ts +26 -0
- package/dist/baseline.js +65 -0
- package/dist/core/index.d.ts +79 -0
- package/dist/core/index.js +90 -0
- package/dist/core/model.d.ts +340 -0
- package/dist/core/model.js +1 -0
- package/dist/core/results.d.ts +237 -0
- package/dist/core/results.js +1 -0
- package/dist/core/wasm.d.ts +7 -0
- package/dist/core/wasm.js +67 -0
- package/dist/custom.d.ts +129 -0
- package/dist/custom.js +217 -0
- package/dist/filters.d.ts +73 -0
- package/dist/filters.js +39 -0
- package/dist/frontend/checks.d.ts +15 -0
- package/dist/frontend/checks.js +12 -0
- package/dist/frontend/classic.d.ts +3 -0
- package/dist/frontend/classic.js +105 -0
- package/dist/frontend/compiler.d.ts +93 -0
- package/dist/frontend/compiler.js +116 -0
- package/dist/frontend/components.d.ts +29 -0
- package/dist/frontend/components.js +111 -0
- package/dist/frontend/declarations.d.ts +45 -0
- package/dist/frontend/declarations.js +158 -0
- package/dist/frontend/dependencies.d.ts +53 -0
- package/dist/frontend/dependencies.js +337 -0
- package/dist/frontend/imports.d.ts +149 -0
- package/dist/frontend/imports.js +765 -0
- package/dist/frontend/native.d.ts +3 -0
- package/dist/frontend/native.js +165 -0
- package/dist/frontend/paths.d.ts +10 -0
- package/dist/frontend/paths.js +18 -0
- package/dist/frontend/project.d.ts +118 -0
- package/dist/frontend/project.js +775 -0
- package/dist/group.d.ts +74 -0
- package/dist/group.js +106 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +27 -0
- package/dist/items.d.ts +133 -0
- package/dist/items.js +179 -0
- package/dist/layers.d.ts +112 -0
- package/dist/layers.js +215 -0
- package/dist/onion.d.ts +78 -0
- package/dist/onion.js +96 -0
- package/dist/patterns.d.ts +14 -0
- package/dist/patterns.js +15 -0
- package/dist/rules.d.ts +408 -0
- package/dist/rules.js +655 -0
- package/package.json +52 -4
- package/wasm/refpact_core.wasm +0 -0
package/dist/group.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** Groups: several rules that are checked together and report every failure. */
|
|
2
|
+
import type { Baseline } from "./baseline.ts";
|
|
3
|
+
import type { Project } from "./frontend/project.ts";
|
|
4
|
+
import type { Failure, ImportRule } from "./rules.ts";
|
|
5
|
+
/**
|
|
6
|
+
* Rules that are checked together, so that one check reports every rule that fails, not only
|
|
7
|
+
* the first. {@link rules}, {@link layers} and {@link onionArchitecture} give sets of rules,
|
|
8
|
+
* and a class of your own can be one: say which rules it stands for, and it is checked as
|
|
9
|
+
* they are.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* class NoDatabase extends RuleSet {
|
|
13
|
+
* readonly name = "no part imports the database";
|
|
14
|
+
* private readonly parts: readonly string[];
|
|
15
|
+
*
|
|
16
|
+
* constructor(parts: readonly string[]) {
|
|
17
|
+
* super();
|
|
18
|
+
* this.parts = parts;
|
|
19
|
+
* }
|
|
20
|
+
*
|
|
21
|
+
* rules(): ImportRule[] {
|
|
22
|
+
* return this.parts.map((part) =>
|
|
23
|
+
* rule(`${part} does not import the database`).files(anyOf(`src/${part}/**`)).mustNotImport(anyOf("src/db/**")),
|
|
24
|
+
* );
|
|
25
|
+
* }
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* new NoDatabase(["ui", "domain"]).check(app);
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare abstract class RuleSet {
|
|
32
|
+
/** The name of the set. It names the failure of a set without rules. */
|
|
33
|
+
abstract readonly name: string;
|
|
34
|
+
/**
|
|
35
|
+
* The rules of the set. Throw an {@link ArchitectureError} of type `malformed` when the
|
|
36
|
+
* set cannot fail as written.
|
|
37
|
+
*/
|
|
38
|
+
abstract rules(): ImportRule[];
|
|
39
|
+
/**
|
|
40
|
+
* Checks all rules and returns the failure, or `undefined` when all pass. The failure is
|
|
41
|
+
* that of one rule, or `several` in the order of the rules. A set without rules cannot
|
|
42
|
+
* fail, so it is `malformed`.
|
|
43
|
+
*/
|
|
44
|
+
verify(project: Project): Failure | undefined;
|
|
45
|
+
/** Throws an {@link ArchitectureError} when a rule is violated or cannot be evaluated. The report has every rule that fails. */
|
|
46
|
+
check(project: Project): void;
|
|
47
|
+
/** Like {@link RuleSet.verify}, with a baseline. See {@link ImportRule.verifyWithBaseline}. */
|
|
48
|
+
verifyWithBaseline(project: Project, baseline: Baseline): Failure | undefined;
|
|
49
|
+
/** Like {@link RuleSet.check}, with a baseline. See {@link ImportRule.checkWithBaseline}. */
|
|
50
|
+
checkWithBaseline(project: Project, baseline: Baseline): void;
|
|
51
|
+
private all;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Rules that are checked together: one check reports every rule that fails, not only the
|
|
55
|
+
* first.
|
|
56
|
+
*
|
|
57
|
+
* ```ts
|
|
58
|
+
* rules(
|
|
59
|
+
* "the platform rules",
|
|
60
|
+
* rule("files are read in the platform only").files(allExcept("src/platform/**")).mustNotImport(anyOf(pkg("node:fs"))),
|
|
61
|
+
* rule("the platform forms no cycle").files(anyOf("src/platform/**")).mustBeAcyclic(),
|
|
62
|
+
* ).check(app);
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export declare class Rules extends RuleSet {
|
|
66
|
+
readonly name: string;
|
|
67
|
+
private readonly list;
|
|
68
|
+
/** @internal */
|
|
69
|
+
constructor(name: string, list: readonly ImportRule[]);
|
|
70
|
+
/** The rules of the group. */
|
|
71
|
+
rules(): ImportRule[];
|
|
72
|
+
}
|
|
73
|
+
/** Groups rules, so that one check reports every rule that fails. */
|
|
74
|
+
export declare function rules(name: string, ...list: ImportRule[]): Rules;
|
package/dist/group.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { ArchitectureError } from "./rules.js";
|
|
2
|
+
/**
|
|
3
|
+
* Rules that are checked together, so that one check reports every rule that fails, not only
|
|
4
|
+
* the first. {@link rules}, {@link layers} and {@link onionArchitecture} give sets of rules,
|
|
5
|
+
* and a class of your own can be one: say which rules it stands for, and it is checked as
|
|
6
|
+
* they are.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* class NoDatabase extends RuleSet {
|
|
10
|
+
* readonly name = "no part imports the database";
|
|
11
|
+
* private readonly parts: readonly string[];
|
|
12
|
+
*
|
|
13
|
+
* constructor(parts: readonly string[]) {
|
|
14
|
+
* super();
|
|
15
|
+
* this.parts = parts;
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* rules(): ImportRule[] {
|
|
19
|
+
* return this.parts.map((part) =>
|
|
20
|
+
* rule(`${part} does not import the database`).files(anyOf(`src/${part}/**`)).mustNotImport(anyOf("src/db/**")),
|
|
21
|
+
* );
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
*
|
|
25
|
+
* new NoDatabase(["ui", "domain"]).check(app);
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export class RuleSet {
|
|
29
|
+
/**
|
|
30
|
+
* Checks all rules and returns the failure, or `undefined` when all pass. The failure is
|
|
31
|
+
* that of one rule, or `several` in the order of the rules. A set without rules cannot
|
|
32
|
+
* fail, so it is `malformed`.
|
|
33
|
+
*/
|
|
34
|
+
verify(project) {
|
|
35
|
+
return this.all((made) => made.verify(project));
|
|
36
|
+
}
|
|
37
|
+
/** Throws an {@link ArchitectureError} when a rule is violated or cannot be evaluated. The report has every rule that fails. */
|
|
38
|
+
check(project) {
|
|
39
|
+
const failure = this.verify(project);
|
|
40
|
+
if (failure !== undefined) {
|
|
41
|
+
throw new ArchitectureError(failure);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Like {@link RuleSet.verify}, with a baseline. See {@link ImportRule.verifyWithBaseline}. */
|
|
45
|
+
verifyWithBaseline(project, baseline) {
|
|
46
|
+
return this.all((made) => made.verifyWithBaseline(project, baseline));
|
|
47
|
+
}
|
|
48
|
+
/** Like {@link RuleSet.check}, with a baseline. See {@link ImportRule.checkWithBaseline}. */
|
|
49
|
+
checkWithBaseline(project, baseline) {
|
|
50
|
+
const failure = this.verifyWithBaseline(project, baseline);
|
|
51
|
+
if (failure !== undefined) {
|
|
52
|
+
throw new ArchitectureError(failure);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
all(verify) {
|
|
56
|
+
let list;
|
|
57
|
+
try {
|
|
58
|
+
list = this.rules();
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (error instanceof ArchitectureError) {
|
|
62
|
+
return error.failure;
|
|
63
|
+
}
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
if (list.length === 0) {
|
|
67
|
+
const message = `rule \`${this.name}\` cannot be checked:\n the group has no rules, so it cannot fail`;
|
|
68
|
+
return { type: "malformed", rule: this.name, message };
|
|
69
|
+
}
|
|
70
|
+
const failures = list.flatMap((made) => verify(made) ?? []);
|
|
71
|
+
if (failures.length <= 1) {
|
|
72
|
+
return failures[0];
|
|
73
|
+
}
|
|
74
|
+
return { type: "several", failures, message: failures.map((failure) => failure.message).join("\n") };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Rules that are checked together: one check reports every rule that fails, not only the
|
|
79
|
+
* first.
|
|
80
|
+
*
|
|
81
|
+
* ```ts
|
|
82
|
+
* rules(
|
|
83
|
+
* "the platform rules",
|
|
84
|
+
* rule("files are read in the platform only").files(allExcept("src/platform/**")).mustNotImport(anyOf(pkg("node:fs"))),
|
|
85
|
+
* rule("the platform forms no cycle").files(anyOf("src/platform/**")).mustBeAcyclic(),
|
|
86
|
+
* ).check(app);
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export class Rules extends RuleSet {
|
|
90
|
+
name;
|
|
91
|
+
list;
|
|
92
|
+
/** @internal */
|
|
93
|
+
constructor(name, list) {
|
|
94
|
+
super();
|
|
95
|
+
this.name = name;
|
|
96
|
+
this.list = list;
|
|
97
|
+
}
|
|
98
|
+
/** The rules of the group. */
|
|
99
|
+
rules() {
|
|
100
|
+
return [...this.list];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** Groups rules, so that one check reports every rule that fails. */
|
|
104
|
+
export function rules(name, ...list) {
|
|
105
|
+
return new Rules(name, list);
|
|
106
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Architecture tests for TypeScript 6 and 7 projects: rules about what files may import, checked
|
|
3
|
+
* as ordinary tests.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { allExcept, anyOf, loadProject, pkg, rule } from "@aoelite/refpact-ts";
|
|
7
|
+
*
|
|
8
|
+
* const app = loadProject("tsconfig.json");
|
|
9
|
+
*
|
|
10
|
+
* rule("the user interface does not import the database")
|
|
11
|
+
* .files(anyOf("src/ui/**"))
|
|
12
|
+
* .mustNotImport(anyOf("src/db/**", pkg("pg")))
|
|
13
|
+
* .check(app);
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export { Baseline, baseline } from "./baseline.ts";
|
|
17
|
+
export { Graph, ItemScope, Scope } from "./custom.ts";
|
|
18
|
+
export type { ImportEdge, SeenImport, SeenMarker } from "./custom.ts";
|
|
19
|
+
export { compilerVersion } from "./frontend/compiler.ts";
|
|
20
|
+
export type { Filters } from "./filters.ts";
|
|
21
|
+
export { Rules, RuleSet, rules } from "./group.ts";
|
|
22
|
+
export { ItemSelection } from "./items.ts";
|
|
23
|
+
export type { ItemKind, Modifier, Visibility } from "./items.ts";
|
|
24
|
+
export { Layers, layers } from "./layers.ts";
|
|
25
|
+
export { loadProject, Project } from "./frontend/project.ts";
|
|
26
|
+
export { Onion, onionArchitecture } from "./onion.ts";
|
|
27
|
+
export type { LoadOptions } from "./frontend/project.ts";
|
|
28
|
+
export type { ComponentExtension } from "./frontend/components.ts";
|
|
29
|
+
export { DEPENDENCY_KINDS } from "./frontend/dependencies.ts";
|
|
30
|
+
export type { DependencyKind } from "./frontend/dependencies.ts";
|
|
31
|
+
export { all, allExcept, AnyOf, anyOf, ArchitectureError, Files, globalApi, ImportRule, KINDS, MARKERS, NamedRule, PackageTarget, pkg, rule, ScopedRule, Targets, unverified, } from "./rules.ts";
|
|
32
|
+
export type { CheckPatterns, Failure, Kind, Marker, Target } from "./rules.ts";
|
|
33
|
+
export type { Detail, DiagramFormat, DiagramLayer, DiagramNodes, End, ErrorCode, FactSet, Role, RuleError, StaleEntry, Step, Violation } from "./core/index.ts";
|
|
34
|
+
/** The language-neutral core: the rule model and its evaluation. */
|
|
35
|
+
export * as core from "./core/index.ts";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Architecture tests for TypeScript 6 and 7 projects: rules about what files may import, checked
|
|
3
|
+
* as ordinary tests.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { allExcept, anyOf, loadProject, pkg, rule } from "@aoelite/refpact-ts";
|
|
7
|
+
*
|
|
8
|
+
* const app = loadProject("tsconfig.json");
|
|
9
|
+
*
|
|
10
|
+
* rule("the user interface does not import the database")
|
|
11
|
+
* .files(anyOf("src/ui/**"))
|
|
12
|
+
* .mustNotImport(anyOf("src/db/**", pkg("pg")))
|
|
13
|
+
* .check(app);
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export { Baseline, baseline } from "./baseline.js";
|
|
17
|
+
export { Graph, ItemScope, Scope } from "./custom.js";
|
|
18
|
+
export { compilerVersion } from "./frontend/compiler.js";
|
|
19
|
+
export { Rules, RuleSet, rules } from "./group.js";
|
|
20
|
+
export { ItemSelection } from "./items.js";
|
|
21
|
+
export { Layers, layers } from "./layers.js";
|
|
22
|
+
export { loadProject, Project } from "./frontend/project.js";
|
|
23
|
+
export { Onion, onionArchitecture } from "./onion.js";
|
|
24
|
+
export { DEPENDENCY_KINDS } from "./frontend/dependencies.js";
|
|
25
|
+
export { all, allExcept, AnyOf, anyOf, ArchitectureError, Files, globalApi, ImportRule, KINDS, MARKERS, NamedRule, PackageTarget, pkg, rule, ScopedRule, Targets, unverified, } from "./rules.js";
|
|
26
|
+
/** The language-neutral core: the rule model and its evaluation. */
|
|
27
|
+
export * as core from "./core/index.js";
|
package/dist/items.d.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/** Rules about items: where they are, what they are named, what they extend or implement, and the types in their signatures. */
|
|
2
|
+
import type { CustomConstraint, RawPattern, Related, Rule as CoreRule, Violation } from "./core/index.ts";
|
|
3
|
+
import type { ItemScope, ListNames } from "./custom.ts";
|
|
4
|
+
import type { ItemKind, Modifier, Visibility } from "./frontend/declarations.ts";
|
|
5
|
+
import type { AnyOf, CheckPatterns, Files, Finder, ImportRule, Target } from "./rules.ts";
|
|
6
|
+
export type { ItemKind, Modifier, Visibility };
|
|
7
|
+
/** What an item selection needs from the rules, which import it. @internal */
|
|
8
|
+
export interface Maker {
|
|
9
|
+
readonly rule: (rule: CoreRule, finder?: Finder) => ImportRule;
|
|
10
|
+
readonly custom: (patterns: CheckPatterns | undefined) => {
|
|
11
|
+
constraint: CustomConstraint;
|
|
12
|
+
names: ListNames;
|
|
13
|
+
};
|
|
14
|
+
readonly subjects: (files: Files) => CoreRule["subjects"];
|
|
15
|
+
readonly file: (pattern: string) => RawPattern;
|
|
16
|
+
readonly target: (target: Target) => RawPattern;
|
|
17
|
+
}
|
|
18
|
+
interface Filters {
|
|
19
|
+
readonly named?: readonly string[];
|
|
20
|
+
readonly kinds?: readonly string[];
|
|
21
|
+
readonly visibilities?: readonly string[];
|
|
22
|
+
readonly modifiers?: readonly string[];
|
|
23
|
+
readonly related: readonly Related[];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Items of some files, narrowed by filters. Each filter narrows the selection; the values
|
|
27
|
+
* that one filter takes form a union. End with a `must..` method.
|
|
28
|
+
*
|
|
29
|
+
* ```ts
|
|
30
|
+
* rule("services live in the services folder")
|
|
31
|
+
* .items(all())
|
|
32
|
+
* .named("*Service")
|
|
33
|
+
* .ofKind("class")
|
|
34
|
+
* .mustResideIn(anyOf("src/services/**"))
|
|
35
|
+
* .check(app);
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare class ItemSelection {
|
|
39
|
+
private readonly name;
|
|
40
|
+
private readonly files;
|
|
41
|
+
private readonly filters;
|
|
42
|
+
private readonly maker;
|
|
43
|
+
/** @internal */
|
|
44
|
+
constructor(name: string, files: Files, maker: Maker, filters?: Filters);
|
|
45
|
+
/** Only items whose name one of the globs matches. `*` matches any run of characters: `*Service`. */
|
|
46
|
+
named(...globs: string[]): ItemSelection;
|
|
47
|
+
/** Only items of these kinds. */
|
|
48
|
+
ofKind(...kinds: ItemKind[]): ItemSelection;
|
|
49
|
+
/** Only items with one of these visibilities. */
|
|
50
|
+
withVisibility(...visibilities: Visibility[]): ItemSelection;
|
|
51
|
+
/**
|
|
52
|
+
* Only items with one of these modifiers: `withModifier("async")` selects the `async`
|
|
53
|
+
* functions, and the variables that hold an `async` arrow function or function expression.
|
|
54
|
+
*/
|
|
55
|
+
withModifier(...modifiers: Modifier[]): ItemSelection;
|
|
56
|
+
/** Only classes and interfaces that extend one of the targets. A target that nothing extends is an error. */
|
|
57
|
+
extending(targets: AnyOf): ItemSelection;
|
|
58
|
+
/** Only classes that implement one of the targets. */
|
|
59
|
+
implementing(targets: AnyOf): ItemSelection;
|
|
60
|
+
/** Only classes with a decorator that is one of the targets. */
|
|
61
|
+
decoratedWith(targets: AnyOf): ItemSelection;
|
|
62
|
+
/**
|
|
63
|
+
* Only classes and interfaces that extend or implement one of the targets, directly or
|
|
64
|
+
* through other types of the project: when `Order extends Entity` and `Entity implements
|
|
65
|
+
* Saved`, `Order` is assignable to `Saved`. An item is not assignable to itself.
|
|
66
|
+
*/
|
|
67
|
+
assignableTo(targets: AnyOf): ItemSelection;
|
|
68
|
+
/**
|
|
69
|
+
* Only functions with one of the targets in their return type: `(): Promise<Order>` returns
|
|
70
|
+
* the type `Order`. A variable that holds an arrow function or a function expression
|
|
71
|
+
* counts as a function. Only the written type counts, not an inferred one.
|
|
72
|
+
*/
|
|
73
|
+
returning(targets: AnyOf): ItemSelection;
|
|
74
|
+
/** Only functions with one of the targets in the type of a parameter. `this` does not count. */
|
|
75
|
+
taking(targets: AnyOf): ItemSelection;
|
|
76
|
+
/** Only variables with one of the targets in their written type: `const pool: Pool = ..`. */
|
|
77
|
+
ofType(targets: AnyOf): ItemSelection;
|
|
78
|
+
/** No item may be selected: "no exported item in `src/internal`". */
|
|
79
|
+
mustNotExist(): ImportRule;
|
|
80
|
+
/** Each item must be declared in one of these files. */
|
|
81
|
+
mustResideIn(files: AnyOf<string>): ImportRule;
|
|
82
|
+
/** The name of each item must match one of the globs. */
|
|
83
|
+
mustBeNamed(...globs: string[]): ImportRule;
|
|
84
|
+
/** Each item must extend one of the targets. */
|
|
85
|
+
mustExtend(targets: AnyOf): ImportRule;
|
|
86
|
+
/**
|
|
87
|
+
* Each item must have at most this number of lines, from the line of its name to the line
|
|
88
|
+
* on which its declaration ends. The decorators and the comments above the name do not
|
|
89
|
+
* count.
|
|
90
|
+
*
|
|
91
|
+
* ```ts
|
|
92
|
+
* rule("no function is longer than 60 lines").items(anyOf("src/**")).ofKind("function").mustNotExceedLines(60).check(app);
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
mustNotExceedLines(lines: number): ImportRule;
|
|
96
|
+
/** Each item must implement one of the targets. */
|
|
97
|
+
mustImplement(targets: AnyOf): ImportRule;
|
|
98
|
+
/** Each item must extend or implement one of the targets, directly or through other types. */
|
|
99
|
+
mustBeAssignableTo(targets: AnyOf): ImportRule;
|
|
100
|
+
/**
|
|
101
|
+
* Each item must have one of these modifiers.
|
|
102
|
+
*
|
|
103
|
+
* ```ts
|
|
104
|
+
* rule("the exports are documented").items(anyOf("src/**")).withVisibility("exported").mustHaveModifier("documented").check(app);
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
mustHaveModifier(...modifiers: Modifier[]): ImportRule;
|
|
108
|
+
/** Each item must have a decorator that is one of the targets. */
|
|
109
|
+
mustBeDecoratedWith(targets: AnyOf): ImportRule;
|
|
110
|
+
/**
|
|
111
|
+
* The items must pass a check of your own: a function that gets the {@link ItemScope} of
|
|
112
|
+
* the rule and returns the violations. The scope has the items that the filters above
|
|
113
|
+
* select, and all that the scope of a rule about files has. The check names the lists of
|
|
114
|
+
* patterns that it needs: see {@link CheckPatterns}.
|
|
115
|
+
*
|
|
116
|
+
* ```ts
|
|
117
|
+
* rule("a subclass is exported")
|
|
118
|
+
* .items(anyOf("src/**"))
|
|
119
|
+
* .ofKind("class")
|
|
120
|
+
* .mustSatisfy((scope) =>
|
|
121
|
+
* scope.items
|
|
122
|
+
* .filter((item) => item.visibility === "local" && item.relations.some((tie) => tie.relation === "extends"))
|
|
123
|
+
* .map((item) => scope.violationOfItem(item, `declares ${item.name}, which extends a class and is not exported`)),
|
|
124
|
+
* )
|
|
125
|
+
* .check(app);
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
mustSatisfy(check: (scope: ItemScope) => readonly Violation[], patterns?: CheckPatterns): ImportRule;
|
|
129
|
+
private related;
|
|
130
|
+
private relate;
|
|
131
|
+
private with;
|
|
132
|
+
private rule;
|
|
133
|
+
}
|
package/dist/items.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Items of some files, narrowed by filters. Each filter narrows the selection; the values
|
|
3
|
+
* that one filter takes form a union. End with a `must..` method.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* rule("services live in the services folder")
|
|
7
|
+
* .items(all())
|
|
8
|
+
* .named("*Service")
|
|
9
|
+
* .ofKind("class")
|
|
10
|
+
* .mustResideIn(anyOf("src/services/**"))
|
|
11
|
+
* .check(app);
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export class ItemSelection {
|
|
15
|
+
name;
|
|
16
|
+
files;
|
|
17
|
+
filters;
|
|
18
|
+
maker;
|
|
19
|
+
/** @internal */
|
|
20
|
+
constructor(name, files, maker, filters = { related: [] }) {
|
|
21
|
+
this.name = name;
|
|
22
|
+
this.files = files;
|
|
23
|
+
this.maker = maker;
|
|
24
|
+
this.filters = filters;
|
|
25
|
+
}
|
|
26
|
+
/** Only items whose name one of the globs matches. `*` matches any run of characters: `*Service`. */
|
|
27
|
+
named(...globs) {
|
|
28
|
+
return this.with({ named: [...(this.filters.named ?? []), ...globs] });
|
|
29
|
+
}
|
|
30
|
+
/** Only items of these kinds. */
|
|
31
|
+
ofKind(...kinds) {
|
|
32
|
+
return this.with({ kinds: [...(this.filters.kinds ?? []), ...kinds] });
|
|
33
|
+
}
|
|
34
|
+
/** Only items with one of these visibilities. */
|
|
35
|
+
withVisibility(...visibilities) {
|
|
36
|
+
return this.with({ visibilities: [...(this.filters.visibilities ?? []), ...visibilities] });
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Only items with one of these modifiers: `withModifier("async")` selects the `async`
|
|
40
|
+
* functions, and the variables that hold an `async` arrow function or function expression.
|
|
41
|
+
*/
|
|
42
|
+
withModifier(...modifiers) {
|
|
43
|
+
return this.with({ modifiers: [...(this.filters.modifiers ?? []), ...modifiers] });
|
|
44
|
+
}
|
|
45
|
+
/** Only classes and interfaces that extend one of the targets. A target that nothing extends is an error. */
|
|
46
|
+
extending(targets) {
|
|
47
|
+
return this.related("extends", targets);
|
|
48
|
+
}
|
|
49
|
+
/** Only classes that implement one of the targets. */
|
|
50
|
+
implementing(targets) {
|
|
51
|
+
return this.related("implements", targets);
|
|
52
|
+
}
|
|
53
|
+
/** Only classes with a decorator that is one of the targets. */
|
|
54
|
+
decoratedWith(targets) {
|
|
55
|
+
return this.related("decorator", targets);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Only classes and interfaces that extend or implement one of the targets, directly or
|
|
59
|
+
* through other types of the project: when `Order extends Entity` and `Entity implements
|
|
60
|
+
* Saved`, `Order` is assignable to `Saved`. An item is not assignable to itself.
|
|
61
|
+
*/
|
|
62
|
+
assignableTo(targets) {
|
|
63
|
+
return this.related("assignable-to", targets);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Only functions with one of the targets in their return type: `(): Promise<Order>` returns
|
|
67
|
+
* the type `Order`. A variable that holds an arrow function or a function expression
|
|
68
|
+
* counts as a function. Only the written type counts, not an inferred one.
|
|
69
|
+
*/
|
|
70
|
+
returning(targets) {
|
|
71
|
+
return this.related("returns", targets);
|
|
72
|
+
}
|
|
73
|
+
/** Only functions with one of the targets in the type of a parameter. `this` does not count. */
|
|
74
|
+
taking(targets) {
|
|
75
|
+
return this.related("takes", targets);
|
|
76
|
+
}
|
|
77
|
+
/** Only variables with one of the targets in their written type: `const pool: Pool = ..`. */
|
|
78
|
+
ofType(targets) {
|
|
79
|
+
return this.related("has-type", targets);
|
|
80
|
+
}
|
|
81
|
+
/** No item may be selected: "no exported item in `src/internal`". */
|
|
82
|
+
mustNotExist() {
|
|
83
|
+
return this.rule({ type: "not-exist" });
|
|
84
|
+
}
|
|
85
|
+
/** Each item must be declared in one of these files. */
|
|
86
|
+
mustResideIn(files) {
|
|
87
|
+
return this.rule({ type: "reside-in", places: files.patterns.map(this.maker.file) });
|
|
88
|
+
}
|
|
89
|
+
/** The name of each item must match one of the globs. */
|
|
90
|
+
mustBeNamed(...globs) {
|
|
91
|
+
return this.rule({ type: "be-named", names: globs });
|
|
92
|
+
}
|
|
93
|
+
/** Each item must extend one of the targets. */
|
|
94
|
+
mustExtend(targets) {
|
|
95
|
+
return this.rule({ type: "have", ...this.relate("extends", targets) });
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Each item must have at most this number of lines, from the line of its name to the line
|
|
99
|
+
* on which its declaration ends. The decorators and the comments above the name do not
|
|
100
|
+
* count.
|
|
101
|
+
*
|
|
102
|
+
* ```ts
|
|
103
|
+
* rule("no function is longer than 60 lines").items(anyOf("src/**")).ofKind("function").mustNotExceedLines(60).check(app);
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
mustNotExceedLines(lines) {
|
|
107
|
+
return this.rule({ type: "not-exceed-lines", lines });
|
|
108
|
+
}
|
|
109
|
+
/** Each item must implement one of the targets. */
|
|
110
|
+
mustImplement(targets) {
|
|
111
|
+
return this.rule({ type: "have", ...this.relate("implements", targets) });
|
|
112
|
+
}
|
|
113
|
+
/** Each item must extend or implement one of the targets, directly or through other types. */
|
|
114
|
+
mustBeAssignableTo(targets) {
|
|
115
|
+
return this.rule({ type: "have", ...this.relate("assignable-to", targets) });
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Each item must have one of these modifiers.
|
|
119
|
+
*
|
|
120
|
+
* ```ts
|
|
121
|
+
* rule("the exports are documented").items(anyOf("src/**")).withVisibility("exported").mustHaveModifier("documented").check(app);
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
mustHaveModifier(...modifiers) {
|
|
125
|
+
return this.rule({ type: "have-modifier", modifiers });
|
|
126
|
+
}
|
|
127
|
+
/** Each item must have a decorator that is one of the targets. */
|
|
128
|
+
mustBeDecoratedWith(targets) {
|
|
129
|
+
return this.rule({ type: "have", ...this.relate("decorator", targets) });
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The items must pass a check of your own: a function that gets the {@link ItemScope} of
|
|
133
|
+
* the rule and returns the violations. The scope has the items that the filters above
|
|
134
|
+
* select, and all that the scope of a rule about files has. The check names the lists of
|
|
135
|
+
* patterns that it needs: see {@link CheckPatterns}.
|
|
136
|
+
*
|
|
137
|
+
* ```ts
|
|
138
|
+
* rule("a subclass is exported")
|
|
139
|
+
* .items(anyOf("src/**"))
|
|
140
|
+
* .ofKind("class")
|
|
141
|
+
* .mustSatisfy((scope) =>
|
|
142
|
+
* scope.items
|
|
143
|
+
* .filter((item) => item.visibility === "local" && item.relations.some((tie) => tie.relation === "extends"))
|
|
144
|
+
* .map((item) => scope.violationOfItem(item, `declares ${item.name}, which extends a class and is not exported`)),
|
|
145
|
+
* )
|
|
146
|
+
* .check(app);
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
mustSatisfy(check, patterns) {
|
|
150
|
+
const { constraint, names } = this.maker.custom(patterns);
|
|
151
|
+
return this.rule(constraint, { names, find: (scope) => check(scope) });
|
|
152
|
+
}
|
|
153
|
+
related(relation, targets) {
|
|
154
|
+
return this.with({ related: [...this.filters.related, this.relate(relation, targets)] });
|
|
155
|
+
}
|
|
156
|
+
relate(relation, targets) {
|
|
157
|
+
return { relation, anyOf: targets.patterns.map(this.maker.target) };
|
|
158
|
+
}
|
|
159
|
+
with(change) {
|
|
160
|
+
return new ItemSelection(this.name, this.files, this.maker, { ...this.filters, ...change });
|
|
161
|
+
}
|
|
162
|
+
rule(must, finder) {
|
|
163
|
+
const { named, kinds, visibilities, modifiers, related } = this.filters;
|
|
164
|
+
const rule = {
|
|
165
|
+
name: this.name,
|
|
166
|
+
subjects: this.maker.subjects(this.files),
|
|
167
|
+
constraint: {
|
|
168
|
+
type: "items",
|
|
169
|
+
...(named !== undefined && { named }),
|
|
170
|
+
...(kinds !== undefined && { kinds }),
|
|
171
|
+
...(visibilities !== undefined && { visibilities }),
|
|
172
|
+
...(modifiers !== undefined && { modifiers }),
|
|
173
|
+
related,
|
|
174
|
+
must,
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
return this.maker.rule(rule, finder);
|
|
178
|
+
}
|
|
179
|
+
}
|
package/dist/layers.d.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { Filter, Filters } from "./filters.ts";
|
|
2
|
+
import { RuleSet } from "./group.ts";
|
|
3
|
+
import type { DiagramFormat } from "./core/index.ts";
|
|
4
|
+
import type { Project } from "./frontend/project.ts";
|
|
5
|
+
import { AnyOf, Files } from "./rules.ts";
|
|
6
|
+
import type { ImportRule } from "./rules.ts";
|
|
7
|
+
interface Settings {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly list: readonly {
|
|
10
|
+
readonly name: string;
|
|
11
|
+
readonly files: readonly string[];
|
|
12
|
+
}[];
|
|
13
|
+
/** The filters of every rule of the layers. */
|
|
14
|
+
readonly filters: readonly Filter[];
|
|
15
|
+
readonly strict: boolean;
|
|
16
|
+
/** For a layer, the only layers it may import. */
|
|
17
|
+
readonly mayOnlyImport: readonly (readonly [string, readonly string[]])[];
|
|
18
|
+
/** For a layer, the only layers that may import it. */
|
|
19
|
+
readonly mayOnlyBeImportedBy: readonly (readonly [string, readonly string[]])[];
|
|
20
|
+
/** The files that must each be in a layer. */
|
|
21
|
+
readonly covered: AnyOf<string> | Files | undefined;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Layers from the top to the bottom. A layer may import the layers below it. It must not
|
|
25
|
+
* import a layer above it.
|
|
26
|
+
*
|
|
27
|
+
* ```ts
|
|
28
|
+
* layers("the layers of the shop")
|
|
29
|
+
* .layer("ui", "src/ui/**")
|
|
30
|
+
* .layer("domain", "src/domain/**")
|
|
31
|
+
* .layer("db", "src/db/**")
|
|
32
|
+
* .mayOnlyBeImportedBy("db", "domain")
|
|
33
|
+
* .everyFileInALayer(allExcept("src/main.ts"))
|
|
34
|
+
* .check(app);
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* The layers have the filters of a rule ({@link Filters}), such as `ignoringTypeImports()`,
|
|
38
|
+
* `ignoringImport(..)` and `asWarning()`. A filter applies to every rule of the layers and to
|
|
39
|
+
* their diagram.
|
|
40
|
+
*/
|
|
41
|
+
export interface Layers extends Filters<Layers> {
|
|
42
|
+
}
|
|
43
|
+
export declare class Layers extends RuleSet {
|
|
44
|
+
private readonly settings;
|
|
45
|
+
/** @internal */
|
|
46
|
+
constructor(settings: Settings);
|
|
47
|
+
/** The name of the layers. */
|
|
48
|
+
get name(): string;
|
|
49
|
+
/** Adds a layer below the layers that are there. */
|
|
50
|
+
layer(name: string, ...files: string[]): Layers;
|
|
51
|
+
/** @internal The layers with one more filter. */
|
|
52
|
+
filtered(filter: Filter): Layers;
|
|
53
|
+
/** Makes the layers strict: a layer may import only the layer right below it, not the layers further down. */
|
|
54
|
+
strict(): Layers;
|
|
55
|
+
/**
|
|
56
|
+
* The layer may import only these layers. Each of them must be below it, and right below
|
|
57
|
+
* it when the layers are {@link Layers.strict | strict}. No layers forbids the layer to
|
|
58
|
+
* import any other layer.
|
|
59
|
+
*/
|
|
60
|
+
mayOnlyImport(layer: string, ...layers: string[]): Layers;
|
|
61
|
+
/**
|
|
62
|
+
* Only these layers may import the layer. Each of them must be above it, and right above
|
|
63
|
+
* it when the layers are {@link Layers.strict | strict}. No layers forbids every other layer
|
|
64
|
+
* to import it.
|
|
65
|
+
*
|
|
66
|
+
* This is about the layers only. To keep every other file away as well, add a rule with
|
|
67
|
+
* {@link ScopedRule.mustOnlyBeImportedBy}.
|
|
68
|
+
*/
|
|
69
|
+
mayOnlyBeImportedBy(layer: string, ...layers: string[]): Layers;
|
|
70
|
+
/**
|
|
71
|
+
* Each of these files must be in a layer. A new file then cannot stay out of the
|
|
72
|
+
* architecture unseen. Leave out the files that belong to no layer, such as the entry
|
|
73
|
+
* point: `allExcept("src/main.ts")`.
|
|
74
|
+
*/
|
|
75
|
+
everyFileInALayer(files: AnyOf<string> | Files): Layers;
|
|
76
|
+
/**
|
|
77
|
+
* The rules of the layers: one `mustNotImport` rule for each layer that must not import
|
|
78
|
+
* some other layers, and the rule of {@link Layers.everyFileInALayer} when it is given.
|
|
79
|
+
*
|
|
80
|
+
* The rule of a layer has the name of the layers and the name of the layer. It is
|
|
81
|
+
* `<layers>: <layer> does not import the layers above it` when only the order of the
|
|
82
|
+
* layers applies to the layer, and `<layers>: <layer> imports only the layers it may
|
|
83
|
+
* import` when more applies.
|
|
84
|
+
*
|
|
85
|
+
* Throws an {@link ArchitectureError} of type `malformed` when the layers cannot fail as
|
|
86
|
+
* written or name a layer that does not exist.
|
|
87
|
+
*/
|
|
88
|
+
rules(): ImportRule[];
|
|
89
|
+
/** The rule with the filters of the layers. A rule that looks at no import gets only the filters that are not about imports. */
|
|
90
|
+
private withFilters;
|
|
91
|
+
/**
|
|
92
|
+
* A diagram of the layers, in Mermaid (the default) or DOT, from the top to the bottom. Each
|
|
93
|
+
* layer is a node, and an edge is an import between two layers. An import that the layers
|
|
94
|
+
* forbid is red. A file is in the first layer that matches it, and a file in no layer is
|
|
95
|
+
* not drawn. The files of {@link Layers.everyFileInALayer} are not checked.
|
|
96
|
+
*
|
|
97
|
+
* Throws an {@link ArchitectureError}: `malformed` as {@link Layers.rules} does, or
|
|
98
|
+
* `invalid` when a pattern of a layer matches no file.
|
|
99
|
+
*
|
|
100
|
+
* ```ts
|
|
101
|
+
* const text = layers("the layers").layer("ui", "src/ui/**").layer("db", "src/db/**").diagram(app);
|
|
102
|
+
* writeFileSync("docs/layers.md", `\`\`\`mermaid\n${text}\`\`\`\n`);
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
diagram(project: Project, format?: DiagramFormat): string;
|
|
106
|
+
/** For each layer, by position from the top, the layers it may import; or every problem of the layers. */
|
|
107
|
+
private allowed;
|
|
108
|
+
private with;
|
|
109
|
+
}
|
|
110
|
+
/** Starts a set of layers. Add the layers from the top to the bottom. */
|
|
111
|
+
export declare function layers(name: string): Layers;
|
|
112
|
+
export {};
|