@lucasacoutinho/archer 0.2.1 → 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 +144 -387
- package/dist/analyzer/Analyzer.d.ts +17 -0
- package/dist/analyzer/Analyzer.d.ts.map +1 -0
- package/dist/analyzer/Analyzer.js +15 -0
- package/dist/analyzer/Analyzer.js.map +1 -0
- package/dist/analyzer/bindings.d.ts +26 -0
- package/dist/analyzer/bindings.d.ts.map +1 -0
- package/dist/analyzer/bindings.js +61 -0
- package/dist/analyzer/bindings.js.map +1 -0
- package/dist/analyzer/errors.d.ts +34 -0
- package/dist/analyzer/errors.d.ts.map +1 -0
- package/dist/analyzer/errors.js +20 -0
- package/dist/analyzer/errors.js.map +1 -0
- package/dist/analyzer/graph-file.d.ts +13 -0
- package/dist/analyzer/graph-file.d.ts.map +1 -0
- package/dist/analyzer/graph-file.js +22 -0
- package/dist/analyzer/graph-file.js.map +1 -0
- package/dist/analyzer/paths.d.ts +12 -0
- package/dist/analyzer/paths.d.ts.map +1 -0
- package/dist/analyzer/paths.js +26 -0
- package/dist/analyzer/paths.js.map +1 -0
- package/dist/analyzer/server-exit.d.ts +19 -0
- package/dist/analyzer/server-exit.d.ts.map +1 -0
- package/dist/analyzer/server-exit.js +70 -0
- package/dist/analyzer/server-exit.js.map +1 -0
- package/dist/analyzer/source.d.ts +29 -0
- package/dist/analyzer/source.d.ts.map +1 -0
- package/dist/analyzer/source.js +58 -0
- package/dist/analyzer/source.js.map +1 -0
- package/dist/analyzer/syntax.d.ts +29 -0
- package/dist/analyzer/syntax.d.ts.map +1 -0
- package/dist/analyzer/syntax.js +226 -0
- package/dist/analyzer/syntax.js.map +1 -0
- package/dist/analyzer/targets.d.ts +26 -0
- package/dist/analyzer/targets.d.ts.map +1 -0
- package/dist/analyzer/targets.js +84 -0
- package/dist/analyzer/targets.js.map +1 -0
- package/dist/analyzer/typescript.d.ts +5 -0
- package/dist/analyzer/typescript.d.ts.map +1 -0
- package/dist/analyzer/typescript.js +221 -0
- package/dist/analyzer/typescript.js.map +1 -0
- package/dist/analyzer/workspace.d.ts +24 -0
- package/dist/analyzer/workspace.d.ts.map +1 -0
- package/dist/analyzer/workspace.js +9 -0
- package/dist/analyzer/workspace.js.map +1 -0
- package/dist/cli/explain.d.ts +3 -0
- package/dist/cli/explain.d.ts.map +1 -0
- package/dist/cli/explain.js +21 -0
- package/dist/cli/explain.js.map +1 -0
- package/dist/cli/main.d.ts +3 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +159 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/config/input.d.ts +59 -0
- package/dist/config/input.d.ts.map +1 -0
- package/dist/config/input.js +115 -0
- package/dist/config/input.js.map +1 -0
- package/dist/config/rules.d.ts +94 -0
- package/dist/config/rules.d.ts.map +1 -0
- package/dist/config/rules.js +130 -0
- package/dist/config/rules.js.map +1 -0
- package/dist/core/baseline.d.ts +16 -0
- package/dist/core/baseline.d.ts.map +1 -0
- package/dist/core/baseline.js +25 -0
- package/dist/core/baseline.js.map +1 -0
- package/dist/core/check.d.ts +16 -0
- package/dist/core/check.d.ts.map +1 -0
- package/dist/core/check.js +271 -0
- package/dist/core/check.js.map +1 -0
- package/dist/core/config.d.ts +72 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +78 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/fingerprint.d.ts +54 -0
- package/dist/core/fingerprint.d.ts.map +1 -0
- package/dist/core/fingerprint.js +182 -0
- package/dist/core/fingerprint.js.map +1 -0
- package/dist/core/keys.d.ts +10 -0
- package/dist/core/keys.d.ts.map +1 -0
- package/dist/core/keys.js +16 -0
- package/dist/core/keys.js.map +1 -0
- package/dist/core/lower.d.ts +13 -0
- package/dist/core/lower.d.ts.map +1 -0
- package/dist/core/lower.js +82 -0
- package/dist/core/lower.js.map +1 -0
- package/dist/core/model.d.ts +155 -0
- package/dist/core/model.d.ts.map +1 -0
- package/dist/core/model.js +116 -0
- package/dist/core/model.js.map +1 -0
- package/dist/core/pattern.d.ts +13 -0
- package/dist/core/pattern.d.ts.map +1 -0
- package/dist/core/pattern.js +29 -0
- package/dist/core/pattern.js.map +1 -0
- package/dist/core/reach.d.ts +10 -0
- package/dist/core/reach.d.ts.map +1 -0
- package/dist/core/reach.js +81 -0
- package/dist/core/reach.js.map +1 -0
- package/dist/core/rule.d.ts +200 -0
- package/dist/core/rule.d.ts.map +1 -0
- package/dist/core/rule.js +162 -0
- package/dist/core/rule.js.map +1 -0
- package/dist/core/selector.d.ts +7 -0
- package/dist/core/selector.d.ts.map +1 -0
- package/dist/core/selector.js +27 -0
- package/dist/core/selector.js.map +1 -0
- package/dist/core/structural.d.ts +10 -0
- package/dist/core/structural.d.ts.map +1 -0
- package/dist/core/structural.js +24 -0
- package/dist/core/structural.js.map +1 -0
- package/dist/core/text.d.ts +26 -0
- package/dist/core/text.d.ts.map +1 -0
- package/dist/core/text.js +144 -0
- package/dist/core/text.js.map +1 -0
- package/dist/effect.d.ts +19 -0
- package/dist/effect.d.ts.map +1 -0
- package/dist/effect.js +21 -0
- package/dist/effect.js.map +1 -0
- package/dist/index.d.ts +24 -44
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -52
- package/dist/index.js.map +1 -0
- package/dist/report/agent.d.ts +3 -0
- package/dist/report/agent.d.ts.map +1 -0
- package/dist/report/agent.js +35 -0
- package/dist/report/agent.js.map +1 -0
- package/dist/report/dot.d.ts +4 -0
- package/dist/report/dot.d.ts.map +1 -0
- package/dist/report/dot.js +15 -0
- package/dist/report/dot.js.map +1 -0
- package/dist/report/github.d.ts +3 -0
- package/dist/report/github.d.ts.map +1 -0
- package/dist/report/github.js +18 -0
- package/dist/report/github.js.map +1 -0
- package/dist/report/json.d.ts +3 -0
- package/dist/report/json.d.ts.map +1 -0
- package/dist/report/json.js +4 -0
- package/dist/report/json.js.map +1 -0
- package/dist/report/message.d.ts +20 -0
- package/dist/report/message.d.ts.map +1 -0
- package/dist/report/message.js +73 -0
- package/dist/report/message.js.map +1 -0
- package/dist/report/pretty.d.ts +8 -0
- package/dist/report/pretty.d.ts.map +1 -0
- package/dist/report/pretty.js +89 -0
- package/dist/report/pretty.js.map +1 -0
- package/dist/report/render.d.ts +9 -0
- package/dist/report/render.d.ts.map +1 -0
- package/dist/report/render.js +10 -0
- package/dist/report/render.js.map +1 -0
- package/dist/report/report.d.ts +44 -0
- package/dist/report/report.d.ts.map +1 -0
- package/dist/report/report.js +49 -0
- package/dist/report/report.js.map +1 -0
- package/dist/report/sarif.d.ts +3 -0
- package/dist/report/sarif.d.ts.map +1 -0
- package/dist/report/sarif.js +68 -0
- package/dist/report/sarif.js.map +1 -0
- package/dist/run.d.ts +49 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +87 -0
- package/dist/run.js.map +1 -0
- package/dist/shell/baseline-file.d.ts +20 -0
- package/dist/shell/baseline-file.d.ts.map +1 -0
- package/dist/shell/baseline-file.js +46 -0
- package/dist/shell/baseline-file.js.map +1 -0
- package/dist/shell/load-config.d.ts +19 -0
- package/dist/shell/load-config.d.ts.map +1 -0
- package/dist/shell/load-config.js +39 -0
- package/dist/shell/load-config.js.map +1 -0
- package/package.json +29 -177
- package/skills/archer/SKILL.md +127 -45
- package/skills/archer/agents/openai.yaml +2 -2
- package/skills/archer/references/rules.md +267 -54
- package/dist/bun/plugin.cjs +0 -85
- package/dist/bun/plugin.d.cts +0 -7
- package/dist/bun/plugin.d.ts +0 -7
- package/dist/bun/plugin.js +0 -11
- package/dist/bun.cjs +0 -2238
- package/dist/bun.d.cts +0 -13
- package/dist/bun.d.ts +0 -13
- package/dist/bun.js +0 -28
- package/dist/chunk-2AKMBLHG.js +0 -2165
- package/dist/chunk-67YEOTXP.js +0 -25
- package/dist/chunk-IDVLHNL2.js +0 -46
- package/dist/chunk-ZZ6TR4PO.js +0 -201
- package/dist/cli.js +0 -2643
- package/dist/index.cjs +0 -2484
- package/dist/index.d.cts +0 -45
- package/dist/jest/plugin.cjs +0 -85
- package/dist/jest/plugin.d.cts +0 -7
- package/dist/jest/plugin.d.ts +0 -7
- package/dist/jest/plugin.js +0 -11
- package/dist/jest.cjs +0 -2237
- package/dist/jest.d.cts +0 -13
- package/dist/jest.d.ts +0 -13
- package/dist/jest.js +0 -27
- package/dist/matchers-Cqnbsyxe.d.cts +0 -11
- package/dist/matchers-Cqnbsyxe.d.ts +0 -11
- package/dist/node.cjs +0 -2237
- package/dist/node.d.cts +0 -13
- package/dist/node.d.ts +0 -13
- package/dist/node.js +0 -27
- package/dist/reporters-BbAXMpuW.d.cts +0 -6
- package/dist/reporters-xh-HQneS.d.ts +0 -6
- package/dist/suite-BwnPDCvU.d.cts +0 -337
- package/dist/suite-BwnPDCvU.d.ts +0 -337
- package/dist/vitest/plugin.cjs +0 -85
- package/dist/vitest/plugin.d.cts +0 -7
- package/dist/vitest/plugin.d.ts +0 -7
- package/dist/vitest/plugin.js +0 -11
- package/dist/vitest.cjs +0 -2531
- package/dist/vitest.d.cts +0 -37
- package/dist/vitest.d.ts +0 -37
- package/dist/vitest.js +0 -139
|
@@ -1,87 +1,300 @@
|
|
|
1
1
|
# Archer rule reference
|
|
2
2
|
|
|
3
|
+
Open this when you write, read, or change a rule. Everything here is exported from `@lucasacoutinho/archer`: `defineConfig`, `r` (rule constructors), `p` (declaration predicates), `use` (call and operator matchers), and `presets`.
|
|
4
|
+
|
|
5
|
+
Contents: [Configuration fields](#configuration-fields), [Selectors](#selectors), [Rule options](#rule-options), [Dependency rules](#dependency-rules), [Cycles and resolution](#cycles-and-resolution), [Declarations and exports](#declarations-and-exports), [Calls and operators](#calls-and-operators), [Names and size](#names-and-size), [Custom rules](#custom-rules), [Declaration predicates](#declaration-predicates-p), [Use matchers](#use-matchers-use), [Presets](#presets), [Test a rule without the filesystem](#test-a-rule-without-the-filesystem).
|
|
6
|
+
|
|
7
|
+
## Configuration fields
|
|
8
|
+
|
|
9
|
+
`archer.config.ts` default-exports `defineConfig({ ... })`. Every field is optional, but the config must produce at least one rule.
|
|
10
|
+
|
|
11
|
+
| Field | Meaning |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| `root` | Analysis root, relative to the config file. Default `"."`. |
|
|
14
|
+
| `projects` | tsconfig files relative to `root`, as a non-empty list. Default `["tsconfig.json"]`. The source set is the union of their file lists, minus declaration files, JSON, and `node_modules`. A file that no listed tsconfig includes is invisible to every rule. |
|
|
15
|
+
| `layers` | Layer name to selector. Names are non-empty and must not look like array indexes (`"0"`). A layer selector cannot contain `layer:`. |
|
|
16
|
+
| `stack` | Layer names from top to bottom. Each layer may import the layers listed after it. |
|
|
17
|
+
| `allow` | Layer name to further layers it may import. |
|
|
18
|
+
| `publicApis` | Layer name to the selector of the files that other layers may import. |
|
|
19
|
+
| `rules` | `r.*` rules. |
|
|
20
|
+
| `baseline` | Baseline file relative to `root`. Default `".archer-baseline"`. `false` turns baselines off. |
|
|
21
|
+
| `failOnWarnings` | When `true`, warnings fail the check. Default `false`. |
|
|
22
|
+
|
|
23
|
+
Archer lowers `layers`, `stack`, `allow`, and `publicApis` into rules with fixed ids:
|
|
24
|
+
|
|
25
|
+
- `layer/<name>` for each constrained layer, meaning one named in `stack` or as a key of `allow`. The layer may import itself and the layers it is allowed, and nothing else that belongs to a declared layer.
|
|
26
|
+
- `public-api/<layer>` for each entry in `publicApis`. Files outside the layer may import only the entry files.
|
|
27
|
+
|
|
28
|
+
Both are `error` rules over direct imports of local source files. They ignore packages, built-ins, computed imports, and files that belong to no layer. A constrained layer may not import a declared layer that is neither in its allowed list nor itself, even when that layer is unconstrained.
|
|
29
|
+
|
|
3
30
|
## Selectors
|
|
4
31
|
|
|
5
|
-
|
|
32
|
+
Anywhere a rule takes a selector, it takes selector text, an array (union), `{ not: selector }`, or `{ all: [selectors] }` (intersection).
|
|
6
33
|
|
|
7
|
-
|
|
|
8
|
-
|
|
|
9
|
-
| `src/domain
|
|
10
|
-
| `
|
|
11
|
-
| `
|
|
12
|
-
| `
|
|
13
|
-
| `
|
|
14
|
-
| `
|
|
34
|
+
| Text | Selects |
|
|
35
|
+
| --- | --- |
|
|
36
|
+
| `src/domain` | That file or directory and everything under it. Same as `src/domain/**`. |
|
|
37
|
+
| `src/domain/*` | Direct children only. A pattern with a wildcard means exactly its segments. |
|
|
38
|
+
| `src/**/*.test.ts` | `**` as a whole segment matches any number of segments, including none. `*` matches within one segment. `?` matches one character. All match names that start with a dot. |
|
|
39
|
+
| `src/{api,web}/**` | Braces give a union and may nest. They may span segments and mix forms: `{src/**,npm:react}`, `npm:{react,zod}`. |
|
|
40
|
+
| `npm:*`, `npm:react`, `npm:@scope/name` | Any package, or that package with any subpath. A name containing `/` must be `@scope/name`. |
|
|
41
|
+
| `node:*`, `node:fs`, `node:fs/promises` | Node built-ins. `bun:*` and `bun:test` do the same for Bun. |
|
|
42
|
+
| `layer:domain` | The layer with that name. Only in rules, not in `layers` or `publicApis`. |
|
|
43
|
+
| `unresolved:*` | Every import that TypeScript could not resolve. |
|
|
44
|
+
| `computed:*` | Every import without a static specifier, such as `import(name)`. |
|
|
15
45
|
|
|
16
|
-
|
|
46
|
+
Paths are root-relative with `/` separators, and a leading `./` is dropped. Selector text is rejected when it is empty, has braces that do not balance, has a stray `,` outside braces, uses `\`, `[`, or `]`, or has an empty, `.`, or `..` segment (which covers a leading or trailing `/` and `a//b`). A prefixed form with a bad name is rejected, not read as a path.
|
|
47
|
+
|
|
48
|
+
A rule's subject selects source files only, so `npm:*` as a subject selects nothing. An object selects any target. `{ not: "src/domain" }` as an object also covers every package, built-in, unresolved import, and computed import.
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { defineConfig, r } from "@lucasacoutinho/archer"
|
|
52
|
+
|
|
53
|
+
export default defineConfig({
|
|
54
|
+
layers: { domain: "src/domain", adapters: ["src/http", "src/db"] },
|
|
55
|
+
rules: [
|
|
56
|
+
r.forbid("selectors/union", ["src/http", "src/db"], "npm:{lodash,moment}"),
|
|
57
|
+
r.forbid("selectors/not", { not: "layer:adapters" }, "node:{fs,child_process}"),
|
|
58
|
+
r.forbid("selectors/all", { all: ["src", { not: "**/*.test.ts" }] }, "unresolved:*"),
|
|
59
|
+
r.forbid("selectors/globs", "src/**/{legacy,old}-*.ts", ["computed:*", "bun:*"]),
|
|
60
|
+
],
|
|
61
|
+
})
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Rule options
|
|
65
|
+
|
|
66
|
+
Every constructor takes an id first and an options object last. The id must be unique, and it should stay stable, because findings and baseline entries use it.
|
|
67
|
+
|
|
68
|
+
| Option | Applies to | Meaning |
|
|
69
|
+
| --- | --- | --- |
|
|
70
|
+
| `reason` | every rule | Text that the report prints with each finding as `reason=`. Write why the rule exists. |
|
|
71
|
+
| `severity` | every rule | `"error"` (default), `"warning"`, or `"info"`. |
|
|
72
|
+
| `kinds` | the dependency rules and `acyclic` | Which imports count, from `"static"` (value imports and re-exports), `"type"` (type-only imports), `"require"`, `"dynamic"` (`import()`). Default: all four. |
|
|
73
|
+
| `transitive` | the dependency rules | Follow imports through other files. Default `false`. A direct rule reports one finding per import site. A transitive rule reports one finding per reached target, with the shortest chain. |
|
|
74
|
+
| `localOnly` | the dependency rules and `acyclic` | Count only imports of source files. Packages, built-ins, unresolved imports, and computed imports are skipped. Default `false`. |
|
|
75
|
+
| `folderDepth` | `acyclic` | Collapse files into their first n directory segments before looking for cycles. |
|
|
76
|
+
| `allowComputed` | `resolvable` | Accept computed imports. Default `false`. |
|
|
77
|
+
| `forbid` | `prefix`, `suffix` | Invert the rule: the name must not start or end with the value. |
|
|
78
|
+
|
|
79
|
+
Narrowing `kinds`, setting `localOnly`, or setting `allowComputed` weakens a rule. Treat each one as a policy change.
|
|
17
80
|
|
|
18
81
|
## Dependency rules
|
|
19
82
|
|
|
83
|
+
Each takes `(id, subject, object, options?)`. Incoming rules never count a file as its own dependent.
|
|
84
|
+
|
|
20
85
|
```ts
|
|
21
|
-
archer
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
86
|
+
import { defineConfig, r } from "@lucasacoutinho/archer"
|
|
87
|
+
|
|
88
|
+
export default defineConfig({
|
|
89
|
+
layers: { domain: "src/domain", application: "src/application" },
|
|
90
|
+
rules: [
|
|
91
|
+
// r.forbid: the subject must not reach the object. A computed import always violates it.
|
|
92
|
+
r.forbid("domain/no-infra", "layer:domain", "src/infrastructure"),
|
|
93
|
+
r.forbid("web/no-db-chain", "src/web", "src/db", { transitive: true }),
|
|
94
|
+
|
|
95
|
+
// r.require: the subject must reach something the object selects. A computed import never counts.
|
|
96
|
+
r.require("handlers/use-logger", "src/handlers", "src/shared/logger.ts"),
|
|
97
|
+
|
|
98
|
+
// r.onlyDependOn: the subject may reach only what the object selects.
|
|
99
|
+
// Packages and built-ins are targets too, so list the ones the layer needs.
|
|
100
|
+
r.onlyDependOn("domain/closed", "layer:domain", ["layer:domain", "npm:zod"]),
|
|
101
|
+
|
|
102
|
+
// r.forbidDependents: no source file the object selects may reach the subject.
|
|
103
|
+
r.forbidDependents("internal/not-imported-by-web", "src/internal", "src/web"),
|
|
104
|
+
|
|
105
|
+
// r.requireDependents: some source file the object selects must reach the subject.
|
|
106
|
+
r.requireDependents("plugins/registered", "src/plugins", "src/registry.ts"),
|
|
107
|
+
|
|
108
|
+
// r.onlyDependedOnBy: only source files the object selects may reach the subject.
|
|
109
|
+
r.onlyDependedOnBy("db/only-from-repositories", "src/db", "src/repositories", {
|
|
110
|
+
reason: "Everything else goes through a repository.",
|
|
111
|
+
}),
|
|
112
|
+
],
|
|
113
|
+
})
|
|
28
114
|
```
|
|
29
115
|
|
|
30
|
-
|
|
116
|
+
A `require`-style rule reports `does not satisfy the rule's requirement` once per subject file.
|
|
31
117
|
|
|
32
|
-
|
|
118
|
+
## Cycles and resolution
|
|
33
119
|
|
|
34
120
|
```ts
|
|
35
|
-
archer
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
121
|
+
import { defineConfig, r } from "@lucasacoutinho/archer"
|
|
122
|
+
|
|
123
|
+
export default defineConfig({
|
|
124
|
+
rules: [
|
|
125
|
+
// r.acyclic(id, subject, options?): no import cycle among the subject's files.
|
|
126
|
+
// Cycles through files outside the subject are not seen.
|
|
127
|
+
r.acyclic("src/acyclic", "src"),
|
|
128
|
+
|
|
129
|
+
// folderDepth collapses files to their first n directories, so the rule finds cycles between folders.
|
|
130
|
+
r.acyclic("src/folders-acyclic", "src", { folderDepth: 2, severity: "warning" }),
|
|
131
|
+
|
|
132
|
+
// r.resolvable(id, subject, options?): every import must resolve to a file, package, or built-in.
|
|
133
|
+
// Unresolved imports and computed imports are findings unless allowComputed is set.
|
|
134
|
+
r.resolvable("src/resolvable", "src"),
|
|
135
|
+
r.resolvable("loader/resolvable", "src/plugins/load.ts", {
|
|
136
|
+
allowComputed: true,
|
|
137
|
+
reason: "The loader imports plugins by name.",
|
|
138
|
+
}),
|
|
139
|
+
],
|
|
140
|
+
})
|
|
46
141
|
```
|
|
47
142
|
|
|
48
|
-
|
|
143
|
+
A finding from a folder rule has the folder as its `subject`.
|
|
49
144
|
|
|
50
|
-
##
|
|
145
|
+
## Declarations and exports
|
|
51
146
|
|
|
52
147
|
```ts
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
148
|
+
import { defineConfig, p, r } from "@lucasacoutinho/archer"
|
|
149
|
+
|
|
150
|
+
export default defineConfig({
|
|
151
|
+
rules: [
|
|
152
|
+
// r.declares(id, subject, predicate): every declaration in the subject satisfies the predicate.
|
|
153
|
+
// A file without declarations passes.
|
|
154
|
+
r.declares("entities/classes", "src/domain/entities", p.kind("class")),
|
|
155
|
+
|
|
156
|
+
// r.declaresNone(id, subject, predicate): no declaration in the subject satisfies the predicate.
|
|
157
|
+
r.declaresNone("no-enums", "src", p.kind("enum")),
|
|
158
|
+
|
|
159
|
+
// r.exportsOnly(id, subject, names): the subject exports no name outside the list.
|
|
160
|
+
r.exportsOnly("api/root", "src/index.ts", ["createClient", "Client"]),
|
|
161
|
+
],
|
|
57
162
|
})
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
A declaration finding names the declaration and points at it.
|
|
58
166
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
layers.toBeFreeOfCycles()
|
|
167
|
+
## Calls and operators
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
import { defineConfig, r, use } from "@lucasacoutinho/archer"
|
|
64
171
|
|
|
65
|
-
|
|
172
|
+
export default defineConfig({
|
|
173
|
+
rules: [
|
|
174
|
+
// r.forbidUse(id, subject, use): the subject must not contain a matching call or operator.
|
|
175
|
+
r.forbidUse("no-eval", "src", use.globals("eval")),
|
|
176
|
+
r.forbidUse("no-loose-equality", "src", use.looseEquality),
|
|
177
|
+
|
|
178
|
+
// r.requireUse(id, subject, use): every subject file must contain a match.
|
|
179
|
+
r.requireUse("handlers/audit", "src/handlers", use.imported("@acme/audit", "audit")),
|
|
180
|
+
],
|
|
181
|
+
})
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Names and size
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
import { defineConfig, r } from "@lucasacoutinho/archer"
|
|
188
|
+
|
|
189
|
+
export default defineConfig({
|
|
190
|
+
rules: [
|
|
191
|
+
// r.prefix and r.suffix test the file name with its extension, so "Service" does not match UserService.ts.
|
|
192
|
+
r.suffix("services/naming", "src/services", "Service.ts"),
|
|
193
|
+
r.prefix("hooks/naming", "src/hooks", "use", { severity: "warning" }),
|
|
194
|
+
r.prefix("no-tmp-files", "src", "tmp-", { forbid: true }),
|
|
195
|
+
|
|
196
|
+
// r.maxLines(id, subject, max): files longer than max lines. max is at least 1.
|
|
197
|
+
r.maxLines("size/files", "src", 400, { severity: "warning", reason: "A file this long is due for a split." }),
|
|
198
|
+
],
|
|
199
|
+
})
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Custom rules
|
|
203
|
+
|
|
204
|
+
`r.custom(id, subject, check, options?)` runs `check` once per subject file. The check gets `{ graph, subject }`, where `subject` is the file's root-relative path joined with `/`, and returns a list of messages. Each message is one finding. A check that throws, or returns something other than a list of strings, becomes one finding. A custom finding has no `at`.
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
import { defineConfig, r } from "@lucasacoutinho/archer"
|
|
208
|
+
|
|
209
|
+
export default defineConfig({
|
|
210
|
+
rules: [
|
|
211
|
+
r.custom(
|
|
212
|
+
"handlers/one-export",
|
|
213
|
+
"src/handlers",
|
|
214
|
+
({ graph, subject }) => {
|
|
215
|
+
const file = graph.files.find((candidate) => candidate.path.join("/") === subject)
|
|
216
|
+
return file !== undefined && file.exports.length > 1 ? [`exports ${file.exports.length} names, expected one`] : []
|
|
217
|
+
},
|
|
218
|
+
{ reason: "One handler per file keeps routing readable." },
|
|
219
|
+
),
|
|
220
|
+
],
|
|
221
|
+
})
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Prefer a built-in rule when one expresses the constraint.
|
|
225
|
+
|
|
226
|
+
## Declaration predicates (p)
|
|
227
|
+
|
|
228
|
+
For `r.declares` and `r.declaresNone`. They apply to classes, interfaces, enums, functions, type aliases, and variables. A variable statement gives one declaration per bound name.
|
|
229
|
+
|
|
230
|
+
| Predicate | True when the declaration |
|
|
231
|
+
| --- | --- |
|
|
232
|
+
| `p.kind(...kinds)` | has one of the kinds `"class"`, `"interface"`, `"enum"`, `"function"`, `"type"`, `"variable"` |
|
|
233
|
+
| `p.abstract` | is abstract |
|
|
234
|
+
| `p.exported` | is exported, including `export { a as b }` and `export default a` |
|
|
235
|
+
| `p.readonlyProperties` | is a class or interface with at least one property, all of them readonly (constructor parameter properties count) |
|
|
236
|
+
| `p.extends(name)` | extends `name`, written without type arguments |
|
|
237
|
+
| `p.implements(name)` | is a class that implements `name` |
|
|
238
|
+
| `p.decorator(name)` | is a class with the decorator `name`, written without `@` |
|
|
239
|
+
| `p.hasMethods(names, visibility?)` | declares every named method, with that visibility when given (`"public"`, `"protected"`, `"private"`) |
|
|
240
|
+
| `p.onlyPublicMethods` | has no protected or private methods |
|
|
241
|
+
| `p.all(...predicates)` | satisfies every predicate |
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
import { defineConfig, p, r } from "@lucasacoutinho/archer"
|
|
245
|
+
|
|
246
|
+
export default defineConfig({
|
|
247
|
+
rules: [
|
|
248
|
+
r.declares("services/injectable", "src/services", p.all(p.kind("class"), p.decorator("Injectable"))),
|
|
249
|
+
r.declares("errors/extend-base", "src/errors", p.all(p.kind("class"), p.extends("AppError"))),
|
|
250
|
+
r.declaresNone("no-private-methods", "src/domain", p.hasMethods(["save"], "private")),
|
|
251
|
+
r.declares("events/immutable", "src/domain/events", p.all(p.kind("class"), p.readonlyProperties)),
|
|
252
|
+
],
|
|
253
|
+
})
|
|
66
254
|
```
|
|
67
255
|
|
|
68
|
-
##
|
|
256
|
+
## Use matchers (use)
|
|
69
257
|
|
|
70
|
-
|
|
71
|
-
- Declarations: `toBeAbstract`, `toBeExported`, `toExtend`, `toImplement`, `toHaveDecorator`
|
|
72
|
-
- Members: `toHaveReadonlyProperties`, `toHaveMethods`, `toHavePublicMethods`, `toHaveProtectedMethods`, `toHavePrivateMethods`, `toOnlyHavePublicMethods`
|
|
73
|
-
- Modules: `toOnlyExport`, `toHaveResolvableDependencies`, `toBePublicApi`
|
|
74
|
-
- Source: `toUseStrictEquality`, `toUseGlobals`, `toHavePrefix`, `toHaveSuffix`, `toHaveLineCountLessThan`
|
|
75
|
-
- Imported calls: `toUseImportedSymbols('react', 'useEffect')`; aliases and namespace calls resolve to the original module export
|
|
258
|
+
For `r.forbidUse` and `r.requireUse`.
|
|
76
259
|
|
|
77
|
-
|
|
260
|
+
| Matcher | Matches |
|
|
261
|
+
| --- | --- |
|
|
262
|
+
| `use.looseEquality` | Each `==` and `!=`. |
|
|
263
|
+
| `use.globals(...names)` | Bare calls such as `eval()` or `setTimeout()` where the name resolves to no declaration in the program. Member calls such as `console.log()` are not recorded. |
|
|
264
|
+
| `use.imported(source, ...names)` | Calls of those exports of `source`, where `source` is the module specifier as written in the import. Relative specifiers differ from file to file, so this fits packages best. Namespace and default imports count: `cp.execSync()` matches `use.imported("node:child_process", "execSync")`. |
|
|
78
265
|
|
|
79
|
-
## Presets
|
|
266
|
+
## Presets
|
|
80
267
|
|
|
81
|
-
|
|
268
|
+
`presets.recommended` is an array of two rules, `archer/acyclic` (no cycles among source files) and `archer/resolvable` (every import resolves, computed imports included), both over `**`. Spread it into `rules`. To exempt one file from either rule you must write your own rules in its place. That is a policy change.
|
|
82
269
|
|
|
83
270
|
```ts
|
|
84
|
-
archer
|
|
271
|
+
import { defineConfig, presets, r } from "@lucasacoutinho/archer"
|
|
272
|
+
|
|
273
|
+
export default defineConfig({
|
|
274
|
+
rules: [...presets.recommended, r.maxLines("size/files", "src", 400, { severity: "warning" })],
|
|
275
|
+
})
|
|
85
276
|
```
|
|
86
277
|
|
|
87
|
-
|
|
278
|
+
## Test a rule without the filesystem
|
|
279
|
+
|
|
280
|
+
`check(config, graph)` evaluates a config against a graph built in memory, with no TypeScript and no files. `graphOf` takes root-relative paths and the paths they import, so it tests file-to-file rules only: no packages, no declarations, no calls. `check` throws a tagged `ConfigInvalid` or `ConfigRejected` error for an invalid config.
|
|
281
|
+
|
|
282
|
+
Only subjects are checked for emptiness. A mistyped object selector makes a `forbid` rule pass without a trace, so run a new rule against a small graph that should violate it:
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
import { check, defineConfig, graphOf, r } from "@lucasacoutinho/archer"
|
|
286
|
+
|
|
287
|
+
const config = defineConfig({
|
|
288
|
+
rules: [r.forbid("domain/no-infra", "src/domain", "src/infrastructure")],
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
const report = check(
|
|
292
|
+
config,
|
|
293
|
+
graphOf({
|
|
294
|
+
"src/domain/order.ts": ["src/infrastructure/db.ts"],
|
|
295
|
+
"src/infrastructure/db.ts": [],
|
|
296
|
+
}),
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
console.log(report.violations.map((violation) => violation.ruleId))
|
|
300
|
+
```
|
package/dist/bun/plugin.cjs
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __export = (target, all) => {
|
|
7
|
-
for (var name in all)
|
|
8
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
-
};
|
|
10
|
-
var __copyProps = (to, from, except, desc) => {
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
for (let key of __getOwnPropNames(from))
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
-
|
|
20
|
-
// src/adapters/bun/plugin.ts
|
|
21
|
-
var plugin_exports = {};
|
|
22
|
-
__export(plugin_exports, {
|
|
23
|
-
archerMatchers: () => archerMatchers
|
|
24
|
-
});
|
|
25
|
-
module.exports = __toCommonJS(plugin_exports);
|
|
26
|
-
var import_bun_test = require("bun:test");
|
|
27
|
-
|
|
28
|
-
// src/reporting/format.ts
|
|
29
|
-
function formatLocation(violation) {
|
|
30
|
-
if (violation.file === void 0) {
|
|
31
|
-
return "";
|
|
32
|
-
}
|
|
33
|
-
if (violation.line === void 0) {
|
|
34
|
-
return violation.file;
|
|
35
|
-
}
|
|
36
|
-
return `${violation.file}:${violation.line}:${violation.column ?? 1}`;
|
|
37
|
-
}
|
|
38
|
-
function formatResult(result) {
|
|
39
|
-
if (result.violations.length === 0) {
|
|
40
|
-
return `Archer checked ${result.filesAnalyzed} files and ${result.dependenciesAnalyzed} dependencies.`;
|
|
41
|
-
}
|
|
42
|
-
const violations = result.violations.map((violation) => {
|
|
43
|
-
const location = formatLocation(violation);
|
|
44
|
-
const prefix = location.length > 0 ? ` ${location}
|
|
45
|
-
` : " ";
|
|
46
|
-
const source = violation.sourceLine === void 0 ? "" : `
|
|
47
|
-
${violation.sourceLine.trim()}
|
|
48
|
-
${" ".repeat(Math.max(0, (violation.column ?? 1) - 1))}^`;
|
|
49
|
-
const chain = violation.importChain === void 0 || violation.importChain.length < 3 ? "" : `
|
|
50
|
-
Import chain: ${violation.importChain.join(" -> ")}`;
|
|
51
|
-
const reason = violation.reason === void 0 ? "" : `
|
|
52
|
-
Because: ${violation.reason}`;
|
|
53
|
-
const id = violation.ruleId === void 0 ? "" : ` [${violation.ruleId}]`;
|
|
54
|
-
return `${prefix} ${violation.severity.toUpperCase()}${id}: ${violation.message}${source}
|
|
55
|
-
Rule: ${violation.rule}${reason}${chain}`;
|
|
56
|
-
});
|
|
57
|
-
return `${result.violations.length} architecture finding${result.violations.length === 1 ? "" : "s"}:
|
|
58
|
-
|
|
59
|
-
${violations.join("\n\n")}`;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// src/testing/matchers.ts
|
|
63
|
-
function isArchitectureCheck(value) {
|
|
64
|
-
return typeof value === "object" && value !== null && "check" in value && typeof value.check === "function";
|
|
65
|
-
}
|
|
66
|
-
async function toRespectArchitecture(received) {
|
|
67
|
-
if (!isArchitectureCheck(received)) {
|
|
68
|
-
throw new TypeError(
|
|
69
|
-
"toRespectArchitecture() expects an Archer suite created by createArcher() or arch()."
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
const result = await received.check();
|
|
73
|
-
return {
|
|
74
|
-
pass: result.ok,
|
|
75
|
-
message: () => result.ok ? "Expected the architecture check to report at least one finding." : formatResult(result)
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
var archerMatchers = { toRespectArchitecture };
|
|
79
|
-
|
|
80
|
-
// src/adapters/bun/plugin.ts
|
|
81
|
-
import_bun_test.expect.extend(archerMatchers);
|
|
82
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
83
|
-
0 && (module.exports = {
|
|
84
|
-
archerMatchers
|
|
85
|
-
});
|
package/dist/bun/plugin.d.cts
DELETED
package/dist/bun/plugin.d.ts
DELETED
package/dist/bun/plugin.js
DELETED