vigiles 32.1.1 → 33.1.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/dist/adapters/claude-code/run-scripts.d.ts +19 -3
- package/dist/adapters/claude-code/run-scripts.js +12 -4
- package/dist/cli-main.js +53 -9
- package/dist/core/frame.d.ts +1 -1
- package/dist/core/hook-program.js +1 -1
- package/dist/core/hook-providers.d.ts +1 -1
- package/dist/coverage-evidence.d.ts +1 -3
- package/dist/coverage-evidence.js +7 -35
- package/dist/eval-define.js +3 -3
- package/dist/eval-lock.js +2 -1
- package/dist/hook-install.d.ts +93 -4
- package/dist/hook-install.js +235 -16
- package/dist/hook-runtime.d.ts +1 -1
- package/dist/hook-runtime.js +8 -2
- package/dist/hook-state-store.d.ts +1 -1
- package/dist/hook.js +1 -1
- package/dist/load-hook.d.ts +1 -1
- package/dist/load-hook.js +1 -1
- package/dist/source-kinds.d.ts +128 -0
- package/dist/source-kinds.js +162 -0
- package/dist/test-coverage-files.js +2 -17
- package/dist/test-coverage.js +11 -8
- package/package.json +6 -3
- package/packages/eslint-config/README.md +17 -0
- package/packages/eslint-config/index.mjs +344 -0
|
@@ -34,22 +34,7 @@ exports.findUntestedSurfacesInFiles = findUntestedSurfacesInFiles;
|
|
|
34
34
|
const posix_path_js_1 = require("./posix-path.js");
|
|
35
35
|
const layout_js_1 = require("./core/layout.js");
|
|
36
36
|
const coverage_evidence_js_1 = require("./coverage-evidence.js");
|
|
37
|
-
|
|
38
|
-
// — test-coverage.ts pulls in node:fs/glob, and this twin must stay browser-safe.
|
|
39
|
-
const DEFAULT_TEST_SUFFIXES = [
|
|
40
|
-
".harness.ts",
|
|
41
|
-
".harness.mts",
|
|
42
|
-
".harness.cts",
|
|
43
|
-
".harness.js",
|
|
44
|
-
".harness.mjs",
|
|
45
|
-
".harness.cjs",
|
|
46
|
-
".eval.ts",
|
|
47
|
-
".eval.mts",
|
|
48
|
-
".eval.cts",
|
|
49
|
-
".eval.js",
|
|
50
|
-
".eval.mjs",
|
|
51
|
-
".eval.cjs",
|
|
52
|
-
];
|
|
37
|
+
const source_kinds_js_1 = require("./source-kinds.js");
|
|
53
38
|
const IGNORE_MARKER = "vigiles:ignore-test";
|
|
54
39
|
/** Mirror of the DEFAULT_IGNORE globs (root-anchored), as a key predicate. */
|
|
55
40
|
function isIgnored(key) {
|
|
@@ -190,7 +175,7 @@ function discoverTests(files) {
|
|
|
190
175
|
for (const path of Object.keys(files)) {
|
|
191
176
|
if (isIgnored(path))
|
|
192
177
|
continue;
|
|
193
|
-
if (
|
|
178
|
+
if ((0, source_kinds_js_1.isVigilesTest)((0, posix_path_js_1.basename)(path))) {
|
|
194
179
|
out.push((0, coverage_evidence_js_1.prepareTest)(path));
|
|
195
180
|
}
|
|
196
181
|
}
|
package/dist/test-coverage.js
CHANGED
|
@@ -78,6 +78,7 @@ const ts_runner_caps_js_1 = require("./ts-runner-caps.js");
|
|
|
78
78
|
const glob_1 = require("glob");
|
|
79
79
|
const glob_ignore_js_1 = require("./core/glob-ignore.js");
|
|
80
80
|
const exclude_js_1 = require("./exclude.js");
|
|
81
|
+
const source_kinds_js_1 = require("./source-kinds.js");
|
|
81
82
|
const layout_js_1 = require("./core/layout.js");
|
|
82
83
|
const coverage_evidence_js_1 = require("./coverage-evidence.js");
|
|
83
84
|
const coverage_artifact_js_1 = require("./coverage-artifact.js");
|
|
@@ -98,9 +99,6 @@ function surfaceGlobs(dir, leaf, materializeRoot) {
|
|
|
98
99
|
// ---------------------------------------------------------------------------
|
|
99
100
|
// Internals
|
|
100
101
|
// ---------------------------------------------------------------------------
|
|
101
|
-
/** Every extension Node executes directly. `.mts`/`.cts` are real (TS 4.7+) and
|
|
102
|
-
* Node 22 strips their types with no toolchain — measured, not assumed. */
|
|
103
|
-
const RUNNABLE_EXTS = "{ts,mts,cts,js,mjs,cjs}";
|
|
104
102
|
/**
|
|
105
103
|
* 🔴 `*.test.*` USED TO BE HERE, AND REMOVING IT IS THE POINT.
|
|
106
104
|
*
|
|
@@ -128,10 +126,11 @@ const RUNNABLE_EXTS = "{ts,mts,cts,js,mjs,cjs}";
|
|
|
128
126
|
* for hooks/agents and dropping them for skills — because a rule with a per-kind
|
|
129
127
|
* exception is what this file just spent a day removing.
|
|
130
128
|
*/
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
129
|
+
// Read off the table in `source-kinds.ts` — the same one `vigiles test`,
|
|
130
|
+
// `vigiles eval` and the hook/provider discovery classify by — so the patterns
|
|
131
|
+
// that credit a test and the files a hook directory refuses to compile cannot
|
|
132
|
+
// drift apart.
|
|
133
|
+
const DEFAULT_TEST_GLOBS = source_kinds_js_1.DEFAULT_TEST_GLOBS;
|
|
135
134
|
const DEFAULT_IGNORE = [
|
|
136
135
|
"node_modules/**",
|
|
137
136
|
"dist/**",
|
|
@@ -298,6 +297,10 @@ function discoverTests(basePath, globs, ignore) {
|
|
|
298
297
|
cwd: basePath,
|
|
299
298
|
ignore,
|
|
300
299
|
dot: true,
|
|
300
|
+
// `glob` defaults this to TRUE on macOS and Windows, so `a.hook.HARNESS.mjs`
|
|
301
|
+
// was a test there and unclaimed on Linux — and the classifier is
|
|
302
|
+
// case-sensitive. One answer on every OS.
|
|
303
|
+
nocase: false,
|
|
301
304
|
});
|
|
302
305
|
// Prepared ONCE per file (comment-strip + declaration parse), not once per
|
|
303
306
|
// (surface × file) pair — the matching below is quadratic by nature.
|
|
@@ -737,7 +740,7 @@ function staleRunNote(report) {
|
|
|
737
740
|
];
|
|
738
741
|
}
|
|
739
742
|
/** Names a default vitest/jest run collects — the suffixes vigiles will not use. */
|
|
740
|
-
const FOREIGN_RUNNER_SUFFIX =
|
|
743
|
+
const FOREIGN_RUNNER_SUFFIX = new RegExp(`\\.(test|spec)\\.(${source_kinds_js_1.RUNNABLE_EXTS.join("|")})$`);
|
|
741
744
|
/**
|
|
742
745
|
* The would-be colocated tests that only their NAME disqualifies.
|
|
743
746
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vigiles",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "33.1.0",
|
|
4
4
|
"description": "Audit, test and measure the harness your AI agent runs on — grade your CLAUDE.md / AGENTS.md, skills, subagents and hooks, run them against a scripted model, and measure whether they actually fire.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
@@ -67,6 +67,8 @@
|
|
|
67
67
|
"hooks",
|
|
68
68
|
"skills",
|
|
69
69
|
"!skills/**/*.spec.ts",
|
|
70
|
+
"packages/eslint-config/index.mjs",
|
|
71
|
+
"packages/eslint-config/README.md",
|
|
70
72
|
"README.md",
|
|
71
73
|
"LICENSE"
|
|
72
74
|
],
|
|
@@ -75,7 +77,7 @@
|
|
|
75
77
|
"build:core": "tsc",
|
|
76
78
|
"test": "npm run build && vitest run",
|
|
77
79
|
"coverage": "npm run build && vitest run --coverage",
|
|
78
|
-
"lint": "eslint src/",
|
|
80
|
+
"lint": "eslint src/ && node scripts/lint-ceilings.mjs",
|
|
79
81
|
"fmt": "prettier --write .",
|
|
80
82
|
"fmt:check": "prettier --check .",
|
|
81
83
|
"test:unit": "npm run build && vitest run --project unit",
|
|
@@ -112,7 +114,8 @@
|
|
|
112
114
|
"conventional-changelog-conventionalcommits": "9.3.1",
|
|
113
115
|
"eslint": "^10.1.0",
|
|
114
116
|
"eslint-import-resolver-typescript": "^4.4.5",
|
|
115
|
-
"eslint-plugin-boundaries": "^
|
|
117
|
+
"eslint-plugin-boundaries": "^7.2.0",
|
|
118
|
+
"eslint-plugin-functional": "^10.0.1",
|
|
116
119
|
"eslint-plugin-sonarjs": "^4.0.2",
|
|
117
120
|
"globals": "^17.4.0",
|
|
118
121
|
"jest": "^30.4.2",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# @vigiles/eslint-config (private)
|
|
2
|
+
|
|
3
|
+
The strict lint rules vigiles and paperlint share: size limits, functional
|
|
4
|
+
rules, no type assertions, and `layers()`, the hexagonal boundary.
|
|
5
|
+
|
|
6
|
+
**This is not part of vigiles' API.** It has no entry in vigiles' `exports`, no
|
|
7
|
+
version of its own, and it can change in any release without notice. It ships
|
|
8
|
+
inside the `vigiles` npm package only so that paperlint can import it by path:
|
|
9
|
+
|
|
10
|
+
```js
|
|
11
|
+
// PRIVATE file of vigiles, not a public API. If it moves, this import fails loudly.
|
|
12
|
+
import { layers } from "./node_modules/vigiles/packages/eslint-config/index.mjs";
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Nobody else should depend on it. The plugins it uses (eslint-plugin-boundaries,
|
|
16
|
+
eslint-plugin-functional, eslint-plugin-sonarjs, typescript-eslint) are not
|
|
17
|
+
bundled; the importing repo installs its own.
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* The strict lint rules vigiles and paperlint share.
|
|
4
|
+
*
|
|
5
|
+
* `.mjs`, not `.ts`: ESLint loads a config without a build step, and paperlint
|
|
6
|
+
* imports this file by path from `node_modules/vigiles/packages/eslint-config/`.
|
|
7
|
+
* That import is deliberate and private: vigiles' `exports` has no entry for this
|
|
8
|
+
* file, so nobody else should depend on it.
|
|
9
|
+
*
|
|
10
|
+
* The plugins are not bundled. Each repo installs eslint-plugin-boundaries,
|
|
11
|
+
* eslint-plugin-functional and eslint-plugin-sonarjs itself, and the imports
|
|
12
|
+
* below resolve to that copy.
|
|
13
|
+
*
|
|
14
|
+
* What each repo keeps for itself: its folder layout (passed to `layers`), its
|
|
15
|
+
* ceiling table for old functions (passed to `ceilings`) and its
|
|
16
|
+
* eslint-suppressions.json.
|
|
17
|
+
*/
|
|
18
|
+
import boundaries from "eslint-plugin-boundaries";
|
|
19
|
+
import functional from "eslint-plugin-functional";
|
|
20
|
+
import sonarjs from "eslint-plugin-sonarjs";
|
|
21
|
+
|
|
22
|
+
/** @typedef {import("eslint").Linter.Config} Config */
|
|
23
|
+
/** @typedef {import("eslint").Linter.RuleEntry} RuleEntry */
|
|
24
|
+
/** @typedef {{ files: string[], ignores?: string[] }} Scope */
|
|
25
|
+
|
|
26
|
+
/** One collection library: remeda. These would be a second one for the same job. */
|
|
27
|
+
export const ONE_COLLECTION_LIBRARY = ["lodash", "lodash-es", "ramda"].map(
|
|
28
|
+
(name) => ({ name, message: "Use remeda." }),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
/** `x as unknown as T` switches the type checker off. For `no-restricted-syntax`. */
|
|
32
|
+
export const AS_UNKNOWN_AS = {
|
|
33
|
+
selector:
|
|
34
|
+
"TSAsExpression > TSAsExpression[typeAnnotation.type='TSUnknownKeyword']",
|
|
35
|
+
message:
|
|
36
|
+
"`as unknown as` switches the type checker off. Convert with a function, or fix the type.",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** A function is one job; at thirty lines a second one starts to hide. */
|
|
40
|
+
const MAX_LINES = { max: 30, skipComments: true, skipBlankLines: true };
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Limits on the size of a function. These rules report one finding per function
|
|
44
|
+
* however far over the limit it is, so a suppressed finding would let the
|
|
45
|
+
* function keep growing. Old functions get a ceiling instead (see `ceilings`).
|
|
46
|
+
*/
|
|
47
|
+
export const SIZE_LIMITS = {
|
|
48
|
+
complexity: 10,
|
|
49
|
+
"sonarjs/cognitive-complexity": 10,
|
|
50
|
+
"max-depth": 3,
|
|
51
|
+
"max-params": 4,
|
|
52
|
+
"max-lines-per-function": MAX_LINES.max,
|
|
53
|
+
"max-nested-callbacks": 3,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** @typedef {keyof typeof SIZE_LIMITS} SizeRule */
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {string} rule
|
|
60
|
+
* @param {number} n
|
|
61
|
+
* @returns {RuleEntry}
|
|
62
|
+
*/
|
|
63
|
+
const sizeRule = (rule, n) =>
|
|
64
|
+
rule === "max-lines-per-function"
|
|
65
|
+
? ["error", { ...MAX_LINES, max: n }]
|
|
66
|
+
: ["error", n];
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The strict rules for TypeScript source. Needs the @typescript-eslint plugin registered.
|
|
70
|
+
*
|
|
71
|
+
* @param {string[]} files
|
|
72
|
+
* @returns {Config}
|
|
73
|
+
*/
|
|
74
|
+
export const strictTypeScript = (files) => ({
|
|
75
|
+
files,
|
|
76
|
+
plugins: { sonarjs },
|
|
77
|
+
rules: {
|
|
78
|
+
...Object.fromEntries(
|
|
79
|
+
Object.entries(SIZE_LIMITS).map(([rule, n]) => [rule, sizeRule(rule, n)]),
|
|
80
|
+
),
|
|
81
|
+
"@typescript-eslint/no-explicit-any": "error",
|
|
82
|
+
// `x as T` is a claim the checker takes on trust. Parse or narrow instead.
|
|
83
|
+
"@typescript-eslint/consistent-type-assertions": [
|
|
84
|
+
"error",
|
|
85
|
+
{ assertionStyle: "never" },
|
|
86
|
+
],
|
|
87
|
+
"@typescript-eslint/switch-exhaustiveness-check": [
|
|
88
|
+
"error",
|
|
89
|
+
{ considerDefaultExhaustiveForUnions: true },
|
|
90
|
+
],
|
|
91
|
+
// Not set here: `no-restricted-imports` and `no-restricted-syntax`. Flat config
|
|
92
|
+
// replaces a rule's options per file group, so a repo that sets either for its
|
|
93
|
+
// own reasons would silently drop ours. Merge ONE_COLLECTION_LIBRARY and
|
|
94
|
+
// AS_UNKNOWN_AS into the repo's own blocks instead.
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* A test's `describe` callback is a list of cases, not a function to split.
|
|
100
|
+
*
|
|
101
|
+
* @param {string[]} files
|
|
102
|
+
* @returns {Config}
|
|
103
|
+
*/
|
|
104
|
+
export const testSizes = (files) => ({
|
|
105
|
+
files,
|
|
106
|
+
rules: { "max-lines-per-function": sizeRule("max-lines-per-function", 60) },
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Per-file ceilings for functions written before the limits: each number is the
|
|
111
|
+
* file's measured maximum, so its functions may shrink and may not grow. Lower a
|
|
112
|
+
* number after a refactor; a file leaves the table once it is under the limits.
|
|
113
|
+
*
|
|
114
|
+
* @param {Record<string, Partial<Record<SizeRule, number>>>} table
|
|
115
|
+
* @returns {Config[]}
|
|
116
|
+
*/
|
|
117
|
+
export const ceilings = (table) =>
|
|
118
|
+
Object.entries(table).map(([file, max]) => ({
|
|
119
|
+
files: [file],
|
|
120
|
+
rules: Object.fromEntries(
|
|
121
|
+
Object.entries(max).map(([rule, n]) => [rule, sizeRule(rule, n)]),
|
|
122
|
+
),
|
|
123
|
+
}));
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* No `let`, no mutation, no loops, readonly parameters. Tests may mutate their fixtures.
|
|
127
|
+
*
|
|
128
|
+
* @param {Scope} scope
|
|
129
|
+
* @returns {Config}
|
|
130
|
+
*/
|
|
131
|
+
export const functionalCode = ({ files, ignores }) => ({
|
|
132
|
+
files,
|
|
133
|
+
ignores,
|
|
134
|
+
plugins: { functional },
|
|
135
|
+
rules: {
|
|
136
|
+
"functional/no-let": "error",
|
|
137
|
+
"functional/immutable-data": "error",
|
|
138
|
+
"functional/no-loop-statements": "error",
|
|
139
|
+
"functional/prefer-immutable-types": [
|
|
140
|
+
"error",
|
|
141
|
+
{ enforcement: "ReadonlyShallow", ignoreInferredTypes: true },
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
/** Node modules that ARE effects. Only an `io` file, a test or the root may import one. */
|
|
147
|
+
export const IO_MODULES = [
|
|
148
|
+
"fs",
|
|
149
|
+
"fs/promises",
|
|
150
|
+
"child_process",
|
|
151
|
+
"os",
|
|
152
|
+
"net",
|
|
153
|
+
"http",
|
|
154
|
+
"https",
|
|
155
|
+
"worker_threads",
|
|
156
|
+
"readline",
|
|
157
|
+
"readline/promises",
|
|
158
|
+
].flatMap((m) => [m, `node:${m}`]);
|
|
159
|
+
|
|
160
|
+
/** Libraries the domain and ports may use: types, pure path arithmetic, hashing, collections. */
|
|
161
|
+
export const PURE_LIBRARIES = [
|
|
162
|
+
"ts-essentials",
|
|
163
|
+
"remeda",
|
|
164
|
+
"path",
|
|
165
|
+
"node:path",
|
|
166
|
+
"crypto",
|
|
167
|
+
"node:crypto",
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
/** What app files may use besides: Node's pure helpers. */
|
|
171
|
+
export const APP_LIBRARIES = [
|
|
172
|
+
...PURE_LIBRARIES,
|
|
173
|
+
"util",
|
|
174
|
+
"node:util",
|
|
175
|
+
"url",
|
|
176
|
+
"node:url",
|
|
177
|
+
"module",
|
|
178
|
+
"node:module",
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
const ORIGINS = ["external", "core"];
|
|
182
|
+
/** @param {string[]} source */
|
|
183
|
+
const libraries = (source) => ({ to: { module: { origin: ORIGINS, source } } });
|
|
184
|
+
/** @param {...string} types */
|
|
185
|
+
const elements = (...types) => ({ element: { types: { anyOf: types } } });
|
|
186
|
+
const APP = { file: { categories: "app" } };
|
|
187
|
+
const ROOT = { file: { categories: "root" } };
|
|
188
|
+
|
|
189
|
+
/** Who may import whom: the domain knows only itself, an adapter never another adapter. */
|
|
190
|
+
/**
|
|
191
|
+
* @param {string[]} domainLibraries
|
|
192
|
+
* @param {string[]} appLibraries
|
|
193
|
+
*/
|
|
194
|
+
const knowledgePolicies = (domainLibraries, appLibraries) => [
|
|
195
|
+
{ from: elements("domain"), allow: { to: elements("domain") } },
|
|
196
|
+
{ from: elements("port"), allow: { to: elements("domain", "port") } },
|
|
197
|
+
{ from: APP, allow: { to: [elements("domain", "port"), APP] } },
|
|
198
|
+
{ from: elements("adapter"), allow: { to: elements("domain", "port") } },
|
|
199
|
+
{ from: ROOT, allow: { to: [{ element: { type: "*" } }, APP, ROOT] } },
|
|
200
|
+
{ from: elements("domain", "port"), allow: libraries(domainLibraries) },
|
|
201
|
+
{ from: APP, allow: libraries(appLibraries) },
|
|
202
|
+
{
|
|
203
|
+
from: [elements("adapter"), ROOT],
|
|
204
|
+
allow: { to: { module: { origin: ORIGINS } } },
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
from: { file: { categories: "test" } },
|
|
208
|
+
allow: {
|
|
209
|
+
to: [
|
|
210
|
+
{ element: { type: "*" } },
|
|
211
|
+
APP,
|
|
212
|
+
ROOT,
|
|
213
|
+
{ module: { origin: ORIGINS } },
|
|
214
|
+
],
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Effects only in io files, tests and the root. Written as "nobody, then these
|
|
221
|
+
* three": a file with no category has `categories: null`, and a `noneOf` query
|
|
222
|
+
* never matches null, so the negative form would exempt every plain file.
|
|
223
|
+
*/
|
|
224
|
+
/** @param {string} ioPattern */
|
|
225
|
+
const effectPolicies = (ioPattern) => [
|
|
226
|
+
{
|
|
227
|
+
disallow: { to: { module: { origin: "core", source: IO_MODULES } } },
|
|
228
|
+
message: `{{ dependency.source }} is I/O in a file that is not ${ioPattern}. Take a port instead, and do the I/O in an adapter's io file or the root.`,
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
from: { file: { categories: ["io", "test", "root"] } },
|
|
232
|
+
allow: { to: { module: { origin: "core", source: IO_MODULES } } },
|
|
233
|
+
},
|
|
234
|
+
];
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* @param {string} type
|
|
238
|
+
* @param {string | undefined} pattern
|
|
239
|
+
* @param {Record<string, unknown>} [extra]
|
|
240
|
+
*/
|
|
241
|
+
const element = (type, pattern, extra = {}) =>
|
|
242
|
+
pattern === undefined
|
|
243
|
+
? []
|
|
244
|
+
: [{ type, pattern, partialMatch: false, ...extra }];
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Where a repo keeps each layer. Patterns are relative to `root`.
|
|
248
|
+
*
|
|
249
|
+
* @typedef {object} Layout
|
|
250
|
+
* @property {string[]} files the files the rules apply to
|
|
251
|
+
* @property {string} root the repository directory
|
|
252
|
+
* @property {string} domain folder of the domain, e.g. "src/core"
|
|
253
|
+
* @property {string} [ports] folder of the ports, if the repo has one
|
|
254
|
+
* @property {string} adapters adapter folders, one element each, e.g. "src/adapters/*"
|
|
255
|
+
* @property {string} cliRoot the composition root: the only non-test file that may wire anything to anything
|
|
256
|
+
* @property {string} app the flat application files, e.g. "src/*.ts"
|
|
257
|
+
* @property {string} tests test files
|
|
258
|
+
* @property {string} io files allowed to do I/O, e.g. "src/adapters/*\/*.io.ts"
|
|
259
|
+
* @property {string[]} [domainLibraries] what the domain and ports may import (default PURE_LIBRARIES)
|
|
260
|
+
* @property {string[]} [appLibraries] what app files may import (default APP_LIBRARIES)
|
|
261
|
+
* @property {object} [resolver] import/resolver settings (default: the TypeScript resolver)
|
|
262
|
+
* @property {object[]} [extraElements] more boundaries elements, e.g. a folder of plain-JS modules
|
|
263
|
+
* @property {object[]} [extraAllows] more allow-policies, checked before the effects rule
|
|
264
|
+
*/
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* The hexagonal layers, as one eslint-plugin-boundaries block. Every linted file
|
|
268
|
+
* must be a declared element or category, so a new file cannot sit outside the
|
|
269
|
+
* rules.
|
|
270
|
+
*
|
|
271
|
+
* `root` must be the repository directory. Without `boundaries/root-path` the
|
|
272
|
+
* plugin matches patterns against `process.cwd()`, and lint started from any
|
|
273
|
+
* other directory classifies nothing and passes.
|
|
274
|
+
*
|
|
275
|
+
* @param {Layout} o
|
|
276
|
+
* @returns {Config}
|
|
277
|
+
*/
|
|
278
|
+
export const layers = (o) => ({
|
|
279
|
+
files: o.files,
|
|
280
|
+
// @ts-expect-error The plugin's types describe an ES module whose `default` is
|
|
281
|
+
// the plugin; Node hands a default import of this CommonJS file the plugin
|
|
282
|
+
// itself (keys meta, rules, configs). Runtime is right, the declaration is not.
|
|
283
|
+
plugins: { boundaries },
|
|
284
|
+
settings: {
|
|
285
|
+
"boundaries/root-path": o.root,
|
|
286
|
+
"import/resolver": o.resolver ?? { typescript: { alwaysTryTypes: true } },
|
|
287
|
+
"boundaries/elements": [
|
|
288
|
+
...element("port", o.ports),
|
|
289
|
+
...element("domain", o.domain),
|
|
290
|
+
...element("adapter", o.adapters, { capture: ["name"] }),
|
|
291
|
+
...(o.extraElements ?? []),
|
|
292
|
+
],
|
|
293
|
+
"boundaries/files": [
|
|
294
|
+
{ category: "root", pattern: o.cliRoot, exclusive: true },
|
|
295
|
+
{ category: "app", pattern: o.app },
|
|
296
|
+
{ category: "test", pattern: o.tests },
|
|
297
|
+
{ category: "io", pattern: o.io },
|
|
298
|
+
],
|
|
299
|
+
},
|
|
300
|
+
rules: {
|
|
301
|
+
"boundaries/no-unknown-files": "error",
|
|
302
|
+
"boundaries/no-unknown-dependencies": "error",
|
|
303
|
+
"boundaries/dependencies": [
|
|
304
|
+
"error",
|
|
305
|
+
{
|
|
306
|
+
default: "disallow",
|
|
307
|
+
checkAllOrigins: true,
|
|
308
|
+
policies: [
|
|
309
|
+
...knowledgePolicies(
|
|
310
|
+
o.domainLibraries ?? PURE_LIBRARIES,
|
|
311
|
+
o.appLibraries ?? APP_LIBRARIES,
|
|
312
|
+
),
|
|
313
|
+
...(o.extraAllows ?? []),
|
|
314
|
+
...effectPolicies(o.io),
|
|
315
|
+
],
|
|
316
|
+
},
|
|
317
|
+
],
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* `process` and `fetch` are not imports, so `layers` cannot see them. Same scope as its effects rule.
|
|
323
|
+
*
|
|
324
|
+
* @param {Scope} scope
|
|
325
|
+
* @returns {Config}
|
|
326
|
+
*/
|
|
327
|
+
export const ioGlobals = ({ files, ignores }) => ({
|
|
328
|
+
files,
|
|
329
|
+
ignores,
|
|
330
|
+
rules: {
|
|
331
|
+
"no-restricted-globals": [
|
|
332
|
+
"error",
|
|
333
|
+
{
|
|
334
|
+
name: "process",
|
|
335
|
+
message:
|
|
336
|
+
"The environment is an input: the root reads it and passes a value in.",
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
name: "fetch",
|
|
340
|
+
message: "Network access belongs in an adapter's io file.",
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
},
|
|
344
|
+
});
|