@avi2dg/checks 0.36.0 → 0.38.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/CHANGELOG.md +16 -0
- package/README.md +33 -20
- package/dependency-cruiser.config.js +92 -60
- package/dist/presets/dependency-cruiser.d.ts +15 -0
- package/dist/presets/dependency-cruiser.js +126 -0
- package/dist/presets/knip.d.ts +6 -0
- package/dist/presets/knip.js +15 -0
- package/dist/presets/oxlint.d.ts +33 -0
- package/dist/presets/oxlint.js +145 -0
- package/docs/configs/dependency-rules.md +28 -31
- package/docs/configs/effect-rules.md +47 -23
- package/docs/configs/native-settings.md +65 -8
- package/docs/configs/typescript-rules.md +7 -7
- package/docs/design.md +25 -8
- package/docs/gates/checks-effect-scope.md +63 -0
- package/docs/gates/checks-exports.md +5 -5
- package/docs/gates/checks-imports.md +64 -0
- package/docs/gates/checks-lint.md +1 -1
- package/docs/gates/checks-mutation-baseline.md +99 -0
- package/docs/gates/checks-mutation-compare.md +5 -2
- package/docs/gates/checks-mutation.md +2 -2
- package/docs/gates/checks-unused.md +4 -4
- package/oxlintrc.json +49 -10
- package/package.json +29 -7
- package/src/complexity/exports.ts +1 -1
- package/src/complexity/knip.ts +1 -1
- package/src/core/gates.ts +4 -0
- package/src/dependencies/imports.ts +94 -0
- package/src/dependencies/kit-defaults.ts +3 -0
- package/src/quality/effect-scope.ts +116 -0
- package/src/quality/jsonc-patch.ts +209 -0
- package/src/quality/presets/dependency-cruiser.ts +148 -0
- package/src/quality/presets/oxlint.ts +165 -0
- package/src/testing/mutation-baseline.ts +211 -0
- package/src/quality/presets/effect.oxlint.json +0 -11
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// src/quality/presets/oxlint.ts
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
var SIZE_RULES = ["max-lines", "max-lines-per-function", "max-statements", "readability/cognitive-complexity", "max-depth"];
|
|
4
|
+
var SOURCE_LIMITS = {
|
|
5
|
+
"max-lines": 400,
|
|
6
|
+
"max-lines-per-function": 100,
|
|
7
|
+
"max-statements": 30,
|
|
8
|
+
"readability/cognitive-complexity": 15,
|
|
9
|
+
"max-depth": 4
|
|
10
|
+
};
|
|
11
|
+
var TEST_LIMITS = {
|
|
12
|
+
"max-lines": 600,
|
|
13
|
+
"max-lines-per-function": "off",
|
|
14
|
+
"max-statements": 50,
|
|
15
|
+
"readability/cognitive-complexity": 15,
|
|
16
|
+
"max-depth": 4
|
|
17
|
+
};
|
|
18
|
+
var SOURCES = ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"];
|
|
19
|
+
var TESTS = ["tests/**", "**/*.test.ts", "**/*.test.tsx"];
|
|
20
|
+
var PLUGINS = ["typescript", "oxc", "eslint", "import"];
|
|
21
|
+
var EFFECT_ONLY_PLUGINS = ["node", "promise", "unicorn"];
|
|
22
|
+
var EFFECT_PLUGINS = [...PLUGINS, ...EFFECT_ONLY_PLUGINS];
|
|
23
|
+
var JS_PLUGINS = ["dist/effect-channel/index.js", "dist/readability/index.js", "dist/data-shape/index.js"];
|
|
24
|
+
var EFFECT_RULES = {
|
|
25
|
+
"node/no-sync": "error",
|
|
26
|
+
"oxc/no-async-await": "error",
|
|
27
|
+
"promise/avoid-new": "error",
|
|
28
|
+
"unicorn/no-process-exit": "error",
|
|
29
|
+
"effect-channel/no-throw": "error",
|
|
30
|
+
"effect-channel/no-try-catch": "error"
|
|
31
|
+
};
|
|
32
|
+
var LINE_RULES = new Set(["max-lines", "max-lines-per-function"]);
|
|
33
|
+
var REFUSED_EFFECT = "@avi2dg/checks/oxlint: set effect to true, false, or { files, excludeFiles } with at least one glob in files";
|
|
34
|
+
function packagePath(path) {
|
|
35
|
+
return fileURLToPath(new URL(`../../${path}`, import.meta.url));
|
|
36
|
+
}
|
|
37
|
+
var BASE = {
|
|
38
|
+
plugins: [...PLUGINS],
|
|
39
|
+
jsPlugins: JS_PLUGINS.map(packagePath),
|
|
40
|
+
categories: { correctness: "error", suspicious: "error" },
|
|
41
|
+
rules: {
|
|
42
|
+
"effect-channel/no-error-channel-escape": "error",
|
|
43
|
+
"typescript/no-explicit-any": "error",
|
|
44
|
+
"typescript/ban-ts-comment": ["error", { "ts-expect-error": true }],
|
|
45
|
+
"typescript/no-inferrable-types": "error",
|
|
46
|
+
"typescript/explicit-module-boundary-types": "error",
|
|
47
|
+
"typescript/switch-exhaustiveness-check": "error",
|
|
48
|
+
"typescript/prefer-readonly": "error",
|
|
49
|
+
"typescript/no-unnecessary-condition": ["error", { allowConstantLoopConditions: true }],
|
|
50
|
+
"typescript/no-unnecessary-type-parameters": "error",
|
|
51
|
+
"typescript/use-unknown-in-catch-callback-variable": "error",
|
|
52
|
+
"eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_", ignoreRestSiblings: true }],
|
|
53
|
+
"typescript/no-unsafe-type-assertion": "error",
|
|
54
|
+
"typescript/no-non-null-assertion": "error",
|
|
55
|
+
"typescript/no-deprecated": "error",
|
|
56
|
+
"typescript/consistent-return": "off"
|
|
57
|
+
},
|
|
58
|
+
overrides: [
|
|
59
|
+
{
|
|
60
|
+
files: ["**/*.ts", "**/*.tsx"],
|
|
61
|
+
rules: { "data-shape/readonly-collection-param": "error" }
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
files: ["**/*.ts", "**/*.tsx"],
|
|
65
|
+
excludeFiles: ["tests/**"],
|
|
66
|
+
rules: {
|
|
67
|
+
"data-shape/schema-twin": "error",
|
|
68
|
+
"typescript/no-unsafe-assignment": "error",
|
|
69
|
+
"typescript/no-unsafe-member-access": "error",
|
|
70
|
+
"typescript/no-unsafe-argument": "error",
|
|
71
|
+
"typescript/no-unsafe-return": "error",
|
|
72
|
+
"typescript/no-unsafe-call": "error"
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
files: ["**/*.astro"],
|
|
77
|
+
rules: { "readability/thin-astro": "error", "import/no-unassigned-import": "off" }
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
};
|
|
81
|
+
var base = BASE;
|
|
82
|
+
function sizeSetting(rule, limit) {
|
|
83
|
+
if (limit === "off")
|
|
84
|
+
return "off";
|
|
85
|
+
return LINE_RULES.has(rule) ? ["error", { max: limit, skipBlankLines: false, skipComments: false }] : ["error", { max: limit }];
|
|
86
|
+
}
|
|
87
|
+
function sizeBudget(files, limits, excludeFiles = []) {
|
|
88
|
+
const rules = Object.fromEntries(SIZE_RULES.map((rule) => [rule, sizeSetting(rule, limits[rule])]));
|
|
89
|
+
return { files: [...files], excludeFiles: [...excludeFiles], rules };
|
|
90
|
+
}
|
|
91
|
+
function effectRules(files, excludeFiles = []) {
|
|
92
|
+
return { files: [...files], excludeFiles: [...excludeFiles], plugins: [...EFFECT_PLUGINS], rules: { ...EFFECT_RULES } };
|
|
93
|
+
}
|
|
94
|
+
function isGlobList(value) {
|
|
95
|
+
return Array.isArray(value) && value.every((glob) => typeof glob === "string" && glob !== "");
|
|
96
|
+
}
|
|
97
|
+
function effectOverrides(effect) {
|
|
98
|
+
if (effect === true)
|
|
99
|
+
return [effectRules(SOURCES, TESTS)];
|
|
100
|
+
if (effect === false)
|
|
101
|
+
return [];
|
|
102
|
+
if (typeof effect !== "object" || effect === null)
|
|
103
|
+
throw new TypeError(REFUSED_EFFECT);
|
|
104
|
+
const files = "files" in effect ? effect.files : undefined;
|
|
105
|
+
const excludeFiles = "excludeFiles" in effect ? effect.excludeFiles : [];
|
|
106
|
+
if (!isGlobList(excludeFiles))
|
|
107
|
+
throw new TypeError(REFUSED_EFFECT);
|
|
108
|
+
if (files === undefined)
|
|
109
|
+
return [effectRules(SOURCES, [...TESTS, ...excludeFiles])];
|
|
110
|
+
if (!isGlobList(files) || files.length === 0)
|
|
111
|
+
throw new TypeError(REFUSED_EFFECT);
|
|
112
|
+
return [effectRules(files, excludeFiles)];
|
|
113
|
+
}
|
|
114
|
+
function withRulePlugins(override) {
|
|
115
|
+
const rules = Object.keys(override.rules ?? {});
|
|
116
|
+
const namesEffectOnlyRule = rules.some((rule) => EFFECT_ONLY_PLUGINS.some((plugin) => rule.startsWith(`${plugin}/`)));
|
|
117
|
+
if (!namesEffectOnlyRule)
|
|
118
|
+
return override;
|
|
119
|
+
return { ...override, plugins: [...new Set([...EFFECT_PLUGINS, ...override.plugins ?? []])] };
|
|
120
|
+
}
|
|
121
|
+
function defineConfig({ effect, rules, overrides = [], options, ...rest }) {
|
|
122
|
+
return {
|
|
123
|
+
...BASE,
|
|
124
|
+
...rest,
|
|
125
|
+
options: { typeAware: true, ...options },
|
|
126
|
+
rules: { ...BASE.rules, ...rules },
|
|
127
|
+
overrides: [
|
|
128
|
+
...BASE.overrides,
|
|
129
|
+
sizeBudget(SOURCES, SOURCE_LIMITS, TESTS),
|
|
130
|
+
sizeBudget(TESTS, TEST_LIMITS),
|
|
131
|
+
...effectOverrides(effect),
|
|
132
|
+
...overrides.map(withRulePlugins)
|
|
133
|
+
]
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
export {
|
|
137
|
+
SOURCES,
|
|
138
|
+
SOURCE_LIMITS,
|
|
139
|
+
TESTS,
|
|
140
|
+
TEST_LIMITS,
|
|
141
|
+
base,
|
|
142
|
+
defineConfig,
|
|
143
|
+
effectRules,
|
|
144
|
+
sizeBudget
|
|
145
|
+
};
|
|
@@ -4,34 +4,40 @@ audience: consumers
|
|
|
4
4
|
---
|
|
5
5
|
# The dependency rules
|
|
6
6
|
|
|
7
|
-
The
|
|
7
|
+
The kit's dependency-cruiser rules hold a repository's imports, and a repository adds its own boundaries on top.
|
|
8
8
|
|
|
9
9
|
## Base rules
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
`defineConfig` from `@avi2dg/checks/dependency-cruiser` returns these rules, with or without a config of the repository's own:
|
|
12
12
|
|
|
13
13
|
- `no-circular`
|
|
14
|
-
- `no-orphans`
|
|
14
|
+
- `no-orphans`, which a repository that depends on `astro` gets only by listing its own, as [checks-imports](../gates/checks-imports.md) explains
|
|
15
15
|
- `not-to-dev-dep`, which refuses shipped source importing a dev-only package, and a package listed in `peerDependencies` too is not dev-only
|
|
16
16
|
- `no-non-package-json`, which refuses an import of an installed package that the nearest `package.json` does not declare, and counts a peer that `peerDependenciesMeta` marks optional as declared
|
|
17
17
|
- `not-to-unresolvable`, which refuses a specifier nothing installed answers
|
|
18
18
|
- `no-deep-imports`, which refuses a subpath the package's exports map does not publish
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
A test file, a config file and every path under `tests/` may import a dev-only package.
|
|
21
|
+
|
|
22
|
+
The rules parse with swc, so the cruise needs `@swc/core` installed, and without it the cruise silently skips every `.ts` file.
|
|
21
23
|
|
|
22
24
|
`no-deep-imports` judges the import specifier, never the file it resolves to.
|
|
23
|
-
The
|
|
25
|
+
The rules honour `exports` maps, so a subpath the map publishes resolves and passes, and a subpath it omits fails to resolve and is reported.
|
|
24
26
|
A package without an `exports` map publishes every file.
|
|
25
27
|
A bare import always passes, whatever file its entry lives in.
|
|
26
|
-
A repository that sets its own `options.enhancedResolveOptions` replaces the
|
|
28
|
+
A repository that sets its own `options.enhancedResolveOptions` replaces the kit's, and restates `exportsFields` and `conditionNames` in it.
|
|
29
|
+
|
|
30
|
+
## A repository's own config
|
|
27
31
|
|
|
28
|
-
|
|
32
|
+
A repository writes `dependency-cruiser.config.ts` when the defaults do not fit:
|
|
29
33
|
|
|
30
|
-
|
|
34
|
+
```ts
|
|
35
|
+
import { defineConfig } from "@avi2dg/checks/dependency-cruiser";
|
|
31
36
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
export default defineConfig({
|
|
38
|
+
// A launcher script loads the bin by path, so nothing imports it.
|
|
39
|
+
orphans: ["^src/bin[.]ts$"],
|
|
40
|
+
devOnly: ["^(?:evals|tests)/"],
|
|
35
41
|
forbidden: [
|
|
36
42
|
{
|
|
37
43
|
name: "ui-cannot-reach-server",
|
|
@@ -40,33 +46,24 @@ module.exports = {
|
|
|
40
46
|
to: { path: "^src/server" },
|
|
41
47
|
},
|
|
42
48
|
],
|
|
43
|
-
}
|
|
49
|
+
options: { exclude: { path: ["^dist/"] } },
|
|
50
|
+
});
|
|
44
51
|
```
|
|
45
52
|
|
|
46
|
-
|
|
47
|
-
That is how an entry point stops being an orphan: redeclare `no-orphans` with the entry added to its `pathNot`.
|
|
48
|
-
A repository with an import boundary writes its rule directly under `forbidden`.
|
|
53
|
+
The builder takes dependency-cruiser's own config without `extends`, plus two keys of its own:
|
|
49
54
|
|
|
50
|
-
|
|
55
|
+
- `devOnly` lists the paths that may import a dev dependency, and replaces the default `["^tests/"]`.
|
|
56
|
+
- `orphans` lists the entry points `no-orphans` passes over.
|
|
51
57
|
|
|
52
|
-
`
|
|
58
|
+
A rule under `forbidden` that takes a kit rule's name replaces that rule whole, and any other rule is added after the kit's.
|
|
59
|
+
`options.exclude` joins the kit's `^repos/`, which keeps the cruise out of the libraries `checks-vendor` links, and every other option replaces the kit's of the same name.
|
|
60
|
+
`tsc` refuses an `extends` key and a severity dependency-cruiser does not know.
|
|
53
61
|
|
|
54
|
-
|
|
55
|
-
"lint:deps": "depcruise --config .dependency-cruiser.cjs src"
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
CI runs it beside the other checks:
|
|
62
|
+
## Running it
|
|
59
63
|
|
|
60
|
-
|
|
61
|
-
jobs:
|
|
62
|
-
lint:
|
|
63
|
-
steps:
|
|
64
|
-
- uses: actions/checkout@v5
|
|
65
|
-
- uses: oven-sh/setup-bun@v2
|
|
66
|
-
- run: bun install --frozen-lockfile
|
|
67
|
-
- run: bun run lint:deps
|
|
68
|
-
```
|
|
64
|
+
`checks-lint` runs [checks-imports](../gates/checks-imports.md), which cruises every tracked TypeScript file against `dependency-cruiser.config.ts`, or against the kit's defaults when the repository holds no config.
|
|
69
65
|
|
|
70
66
|
## Related topics
|
|
71
67
|
|
|
68
|
+
- [checks-imports](../gates/checks-imports.md)
|
|
72
69
|
- [Why it is shaped this way](../design.md)
|
|
@@ -8,38 +8,59 @@ The oxlint base and Effect language service check paths a repository writes with
|
|
|
8
8
|
|
|
9
9
|
## Oxlint override
|
|
10
10
|
|
|
11
|
-
The
|
|
12
|
-
|
|
11
|
+
The kit's oxlint `base` loads `effect-channel/no-error-channel-escape` across the tree.
|
|
12
|
+
`effect` in `oxlint.config.ts` names the paths that hold the six Effect rules, and has no default:
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
| `effect` | The Effect rules hold |
|
|
15
|
+
| --- | --- |
|
|
16
|
+
| `true` | every source file outside the tests |
|
|
17
|
+
| `false` | no file |
|
|
18
|
+
| `{ excludeFiles }` | every source file outside the tests, less these globs |
|
|
19
|
+
| `{ files, excludeFiles }` | these globs, less `excludeFiles`, with at least one glob in `files` |
|
|
20
|
+
|
|
21
|
+
`files` and `excludeFiles` read as they do in an oxlint override.
|
|
22
|
+
Every source file is `SOURCES`, the `.ts`, `.tsx`, `.mts` and `.cts` files, and the tests are `TESTS`, the files under `tests/` and each `*.test.ts` or `*.test.tsx`.
|
|
23
|
+
A comment beside an excluded glob says why that path stays outside Effect:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { defineConfig } from "@avi2dg/checks/oxlint";
|
|
27
|
+
|
|
28
|
+
export default defineConfig({
|
|
29
|
+
effect: {
|
|
30
|
+
excludeFiles: [
|
|
31
|
+
// Launchd runs the renamer, and it cannot import Effect v4.
|
|
32
|
+
"home/.config/renamer/**",
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
A config that leaves `effect` out, sets it to a string or gives `files` no glob fails `tsc`, and oxlint refuses it again when it loads the file.
|
|
39
|
+
`effectRules(files, excludeFiles)` from the same module returns the override itself:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
15
42
|
{
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"effect-channel/no-throw": "error",
|
|
28
|
-
"effect-channel/no-try-catch": "error"
|
|
29
|
-
}
|
|
30
|
-
}]
|
|
43
|
+
files: ["src/**/*.ts"],
|
|
44
|
+
excludeFiles: ["src/host/*.ts"],
|
|
45
|
+
plugins: ["typescript", "oxc", "eslint", "import", "node", "promise", "unicorn"],
|
|
46
|
+
rules: {
|
|
47
|
+
"node/no-sync": "error",
|
|
48
|
+
"oxc/no-async-await": "error",
|
|
49
|
+
"promise/avoid-new": "error",
|
|
50
|
+
"unicorn/no-process-exit": "error",
|
|
51
|
+
"effect-channel/no-throw": "error",
|
|
52
|
+
"effect-channel/no-try-catch": "error"
|
|
53
|
+
}
|
|
31
54
|
}
|
|
32
55
|
```
|
|
33
56
|
|
|
34
|
-
The kit ships the rule block in `src/quality/presets/effect.oxlint.json` for copying into the override.
|
|
35
|
-
Each config in an oxlint `extends` chain sets `plugins` explicitly, because an omitted list enables defaults across the chain.
|
|
36
57
|
`unicorn/no-process-exit` does not check a shebang script, so a bin can use `eslint/no-restricted-properties` for `process.exit`.
|
|
37
58
|
|
|
38
59
|
## Language service
|
|
39
60
|
|
|
40
61
|
The repository's `tsconfig.json` holds its Effect override under `compilerOptions.plugins`.
|
|
41
|
-
|
|
42
|
-
The
|
|
62
|
+
[checks-effect-scope](../gates/checks-effect-scope.md) writes that override from `oxlint.config.ts`, so it includes and excludes the same paths as oxlint.
|
|
63
|
+
The override takes its `options` from the severity values the kit ships in `src/quality/presets/effect.language-service.json`.
|
|
43
64
|
The preset turns these diagnostics to errors:
|
|
44
65
|
|
|
45
66
|
- `nodeBuiltinImport` refuses an import of a Node built-in module that has an Effect counterpart.
|
|
@@ -50,9 +71,12 @@ The preset turns these diagnostics to errors:
|
|
|
50
71
|
- `processEnvInEffect` refuses a read of `process.env` inside an Effect generator.
|
|
51
72
|
|
|
52
73
|
Effect's `Config` reads the environment in place of `process.env`.
|
|
53
|
-
The kit's `tsconfig.effect.json`
|
|
74
|
+
The kit's `tsconfig.effect.json` holds the shared language service diagnostics.
|
|
75
|
+
A plugin entry in the repository's `tsconfig.json` stands in for the kit's, so `checks-effect-scope` writes those severities into it on every run.
|
|
76
|
+
The kit owns the severities of its own keys, and a repository adds keys of its own but does not change the kit's.
|
|
54
77
|
|
|
55
78
|
## Related topics
|
|
56
79
|
|
|
57
80
|
- [The TypeScript rules](typescript-rules.md)
|
|
58
81
|
- [Native settings](native-settings.md)
|
|
82
|
+
- [checks-effect-scope](../gates/checks-effect-scope.md)
|
|
@@ -11,15 +11,15 @@ A consuming repository puts each setting in the file its tool reads, and no mani
|
|
|
11
11
|
| File | Setting | Reader |
|
|
12
12
|
| --- | --- | --- |
|
|
13
13
|
| `.github/workflows/*.yml` | Pull request commands, scheduled commands, runner labels | GitHub Actions and `checks-ci-wiring` |
|
|
14
|
-
|
|
|
14
|
+
| `oxlint.config.ts` | Effect paths and their exemptions, ignore patterns, and any rule or budget the repository sets over the kit's defaults | oxlint and `checks-effect-scope` |
|
|
15
15
|
| `oxlint-suppressions.json` | The existing violations oxlint suppresses, per file and rule | oxlint and `checks-suppressions-ratchet` |
|
|
16
16
|
| `exports-baseline.json` | The existing unused exports and types, per file, kind and name | `checks-exports` |
|
|
17
17
|
| `.jscpd.json` | The `path` and `ignore` globs of the files repetition is measured in | jscpd and `checks-repetition` |
|
|
18
|
-
| `knip.config.ts` | The `entry` globs Knip traces unreferenced files from
|
|
19
|
-
| `
|
|
18
|
+
| `knip.config.ts` | The `entry` globs Knip traces unreferenced files from | Knip, `checks-unused` and `checks-exports` |
|
|
19
|
+
| `dependency-cruiser.config.ts` | Entry points nothing imports, paths that may import dev dependencies, and import boundaries, when the kit's defaults do not fit | `checks-imports` |
|
|
20
|
+
| `tsconfig.json` | Compiler options, and the Effect language service plugin whose `overrides` and kit severities `checks-effect-scope` writes, keeping every other key | TypeScript and Effect language service |
|
|
20
21
|
| `package.json` | `scripts` with the `checks-vendor` arguments in `prepare`, `author` and `contributors` | Bun, `checks-commit-identity` and `checks-vendor` |
|
|
21
22
|
| `bunfig.toml` | Test discovery and quarantine exclusion | Bun and `checks-test-layout` |
|
|
22
|
-
| `.dependency-cruiser.cjs` | Import rules | dependency-cruiser |
|
|
23
23
|
| `stryker.conf.mjs` | Mutation settings | Stryker |
|
|
24
24
|
| Each page under `docs/` | Diátaxis mode in `kind` front matter, and `audience: consumers` on a page that speaks to a consuming repository | `checks-docs` |
|
|
25
25
|
|
|
@@ -27,16 +27,66 @@ Every page under `docs/` names its mode in `kind` front matter, whatever directo
|
|
|
27
27
|
A page whose front matter sets `audience: consumers` names commands that a consuming repository runs.
|
|
28
28
|
`checks-docs` skips the `bun run` commands on such a page, and resolves those in every other living doc or agent file as [Paths, links and commands](../gates/checks-docs.md#paths-links-and-commands) says.
|
|
29
29
|
|
|
30
|
+
## The config builders
|
|
31
|
+
|
|
32
|
+
The kit ships a `defineConfig` for oxlint, Knip and dependency-cruiser.
|
|
33
|
+
Each takes the tool's own config type, less the keys the kit owns, and returns the whole config with a default for every kit rule.
|
|
34
|
+
A key only the repository knows has no default, so `tsc` refuses a config that leaves it out.
|
|
35
|
+
The builder throws when the tool loads such a config anyway.
|
|
36
|
+
|
|
37
|
+
| Builder | Required | The kit owns |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| `@avi2dg/checks/oxlint` | `effect` | `extends`, `plugins`, `jsPlugins` and `categories` |
|
|
40
|
+
| `@avi2dg/checks/knip` | `entry`, where `[]` means the package scripts and tests name every entry | `include` |
|
|
41
|
+
| `@avi2dg/checks/dependency-cruiser` | nothing | `extends` |
|
|
42
|
+
|
|
43
|
+
A repository whose sources are Effect programs writes `oxlint.config.ts`:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { defineConfig } from "@avi2dg/checks/oxlint";
|
|
47
|
+
|
|
48
|
+
export default defineConfig({
|
|
49
|
+
effect: true,
|
|
50
|
+
ignorePatterns: ["src/crawler-extract.js"],
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`ignorePatterns`, `rules`, `overrides`, `settings`, `env`, `globals` and `options` mean what they mean in oxlint's own docs.
|
|
55
|
+
The kit's rules come first, and the repository's `rules` and `overrides` land after them.
|
|
56
|
+
An override that sets a `node/`, `promise/` or `unicorn/` rule gets the Effect override's `plugins`, since the kit loads those plugins only there.
|
|
57
|
+
[The Effect rules](effect-rules.md) lists what `effect` accepts.
|
|
58
|
+
`defineConfig` sets `options.typeAware`, so a plain `oxlint` runs the type-aware rules.
|
|
59
|
+
The module also exports the blocks the config is built from: `base`, `sizeBudget`, `effectRules`, `SOURCE_LIMITS`, `TEST_LIMITS`, `SOURCES` and `TESTS`.
|
|
60
|
+
|
|
61
|
+
`knip.config.ts` names the files nothing imports:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { defineConfig } from "@avi2dg/checks/knip";
|
|
65
|
+
|
|
66
|
+
export default defineConfig({ entry: ["src/index.ts"] });
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`dependency-cruiser.config.ts` is optional, since `checks-imports` cruises against the kit's defaults without one:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { defineConfig } from "@avi2dg/checks/dependency-cruiser";
|
|
73
|
+
|
|
74
|
+
export default defineConfig({ devOnly: ["^(?:evals|tests)/"] });
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
[The dependency rules](dependency-rules.md) lists its keys.
|
|
78
|
+
`tsconfig.json` keeps the three config files in its program, so `bun run typecheck` holds them to the builders' types.
|
|
79
|
+
|
|
30
80
|
## Size limits
|
|
31
81
|
|
|
32
|
-
The size rules are oxlint's own rules at `error
|
|
82
|
+
The size rules are oxlint's own rules at `error`, which every `oxlint.config.ts` built with `defineConfig` sets over the whole tree, so `bun run lint` fails on any file over them.
|
|
33
83
|
A repository records its existing violations with `oxlint --suppress-all`, which writes them to `oxlint-suppressions.json`.
|
|
34
84
|
`checks-suppressions-ratchet` refuses any count in that file that rises.
|
|
35
|
-
|
|
85
|
+
`SOURCE_LIMITS` holds the limits for sources and `TEST_LIMITS` the limits for tests:
|
|
36
86
|
|
|
37
|
-
<!-- generated size-limits: bun run build writes it from
|
|
87
|
+
<!-- generated size-limits: bun run build writes it from SOURCE_LIMITS, TEST_LIMITS, SOURCES and TESTS in src/quality/presets/oxlint.ts and scripts/doc-blocks.ts -->
|
|
38
88
|
|
|
39
|
-
| Limits | oxlint rule |
|
|
89
|
+
| Limits | oxlint rule | `**/*.ts`, `**/*.tsx`, `**/*.mts`, `**/*.cts` outside the tests | `tests/**`, `**/*.test.ts`, `**/*.test.tsx` |
|
|
40
90
|
| --- | --- | --- | --- |
|
|
41
91
|
| The most lines a file may hold, blank and comment lines counted | `max-lines` | 400 | 600 |
|
|
42
92
|
| The most lines a function may span, blank and comment lines counted | `max-lines-per-function` | 100 | off |
|
|
@@ -46,9 +96,16 @@ The kit's own `.oxlintrc.json` sets these limits for each size override, and a r
|
|
|
46
96
|
|
|
47
97
|
<!-- end generated size-limits -->
|
|
48
98
|
|
|
99
|
+
A file a repository holds to another limit gets an ordinary override in `oxlint.config.ts`:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
overrides: [{ files: ["src/lib/server.ts"], rules: { "max-lines": ["error", { max: 600 }] } }],
|
|
103
|
+
```
|
|
104
|
+
|
|
49
105
|
## Related topics
|
|
50
106
|
|
|
51
107
|
- [The Effect rules](effect-rules.md)
|
|
108
|
+
- [The dependency rules](dependency-rules.md)
|
|
52
109
|
- [checks-ci-wiring](../gates/checks-ci-wiring.md)
|
|
53
110
|
- [checks-suppressions-ratchet](../gates/checks-suppressions-ratchet.md)
|
|
54
111
|
- [checks-docs](../gates/checks-docs.md)
|
|
@@ -4,11 +4,11 @@ audience: consumers
|
|
|
4
4
|
---
|
|
5
5
|
# The TypeScript rules
|
|
6
6
|
|
|
7
|
-
The oxlint base
|
|
7
|
+
The kit's oxlint base and the tsconfig fragment hold a repository's TypeScript to a set of rules, and some of them need type information.
|
|
8
8
|
|
|
9
9
|
## Syntax rules
|
|
10
10
|
|
|
11
|
-
`
|
|
11
|
+
`base` from `@avi2dg/checks/oxlint`, which every config its `defineConfig` returns starts from, turns on the `correctness` and `suspicious` categories as errors, and these rules on top of them:
|
|
12
12
|
|
|
13
13
|
- `typescript/no-explicit-any` refuses an `any` type written out.
|
|
14
14
|
- `typescript/ban-ts-comment` refuses `@ts-ignore`, `@ts-expect-error` and `@ts-nocheck`.
|
|
@@ -22,8 +22,8 @@ It turns on `effect-channel/no-error-channel-escape` as well, and [The Effect ru
|
|
|
22
22
|
|
|
23
23
|
## Type-aware rules
|
|
24
24
|
|
|
25
|
-
These rules read the types, so they run only
|
|
26
|
-
|
|
25
|
+
These rules read the types, so they run only with `oxlint-tsgolint` installed and type-aware linting on.
|
|
26
|
+
`defineConfig` turns it on through `options.typeAware`, so a plain `oxlint` runs them.
|
|
27
27
|
|
|
28
28
|
- `typescript/switch-exhaustiveness-check` refuses a `switch` over a union that leaves a member without a case.
|
|
29
29
|
- `typescript/prefer-readonly` refuses a private member that nothing reassigns and that is not `readonly`.
|
|
@@ -45,7 +45,7 @@ The base loads the kit's `data-shape` plugin from `dist/` with one rule for ever
|
|
|
45
45
|
|
|
46
46
|
## Astro rules
|
|
47
47
|
|
|
48
|
-
An override in `
|
|
48
|
+
An override in `base` turns on one rule of the kit's `readability` plugin in each `.astro` file:
|
|
49
49
|
|
|
50
50
|
- `readability/thin-astro` refuses a statement in the frontmatter or a script block that is neither an import, a re-export from another module, a type or interface declaration, nor a variable read from `Astro.props`.
|
|
51
51
|
- A default inside an `Astro.props` destructuring passes only when it is a literal, a `-` or `+` on a numeric literal, or a name read from `Astro.props` earlier, in the same pattern or a statement before it, so `const { title = "Home", heading = title } = Astro.props;` passes and a call or `await` in a default is refused.
|
|
@@ -55,7 +55,7 @@ An override in `oxlintrc.json` turns on one rule of the kit's `readability` plug
|
|
|
55
55
|
|
|
56
56
|
## Rules outside tests
|
|
57
57
|
|
|
58
|
-
An override in `
|
|
58
|
+
An override in `base` turns on these type-aware rules in each `.ts` and `.tsx` file outside `tests/`:
|
|
59
59
|
|
|
60
60
|
- `typescript/no-unsafe-assignment` refuses assigning an `any` value to a variable, a property or a destructured name.
|
|
61
61
|
- `typescript/no-unsafe-member-access` refuses reading a member of an `any` value.
|
|
@@ -63,7 +63,7 @@ An override in `oxlintrc.json` turns on these type-aware rules in each `.ts` and
|
|
|
63
63
|
- `typescript/no-unsafe-return` refuses returning an `any` value from a function, unless the function returns `unknown`.
|
|
64
64
|
- `typescript/no-unsafe-call` refuses calling an `any` value.
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
Every config `defineConfig` returns carries the override, and a file under `tests/` answers to none of the five.
|
|
67
67
|
|
|
68
68
|
## Compiler options
|
|
69
69
|
|
package/docs/design.md
CHANGED
|
@@ -8,14 +8,25 @@ Each section names the limit and what the obvious alternative would break.
|
|
|
8
8
|
|
|
9
9
|
## Each tool reads its own config
|
|
10
10
|
|
|
11
|
-
A repository keeps each setting in the file its tool reads, such as
|
|
11
|
+
A repository keeps each setting in the file its tool reads, such as `oxlint.config.ts`, `tsconfig.json` or `.jscpd.json`.
|
|
12
12
|
A developer then changes a rule where the tool reads it, and the tool's own docs describe that file.
|
|
13
|
-
|
|
13
|
+
|
|
14
|
+
oxlint, Knip and dependency-cruiser each read a TypeScript config, so the kit ships a `defineConfig` for each over the tool's own config type.
|
|
15
|
+
A repository writes the shape that tool's docs describe, and adds to the kit's rules through the tool's own keys.
|
|
16
|
+
Each builder sets a default for every kit rule, and every glob in it covers the whole tree, so no unlisted directory escapes a rule.
|
|
17
|
+
A rule set copied by hand would miss whatever the copy left out, and would fail on a path the copy forgot.
|
|
18
|
+
A fact only the repository knows has no default, `effect` for oxlint and `entry` for Knip.
|
|
19
|
+
Defaulting `effect` on fails a repository whose code is deliberately outside Effect, and defaulting it off leaves an Effect repository unchecked, so its type is required.
|
|
20
|
+
Neither oxlint nor Knip typechecks the config it loads, so each builder also throws when it gets a config without that fact.
|
|
21
|
+
Both tools read a config's default export synchronously, so a throw is the one refusal open to the builder.
|
|
22
|
+
|
|
23
|
+
The Effect paths sit in `oxlint.config.ts` alone.
|
|
24
|
+
`tsconfig.json` cannot import a TypeScript module, so `checks-effect-scope` writes the Effect language service override in `tsconfig.json` from them, and fails `checks-lint` when the two differ.
|
|
14
25
|
|
|
15
26
|
oxlint does not pass `plugins` down an `extends` chain.
|
|
16
27
|
A config in the chain that sets no `plugins` gets oxlint's default plugins, and the base's `categories` then turn on their rules across the tree.
|
|
17
|
-
So
|
|
18
|
-
|
|
28
|
+
So `defineConfig` returns the whole root config with `plugins` set, and a repository adds its own keys rather than extending a kit object.
|
|
29
|
+
oxlint reads a relative `jsPlugins` path against the consumer's config, so `base` names each plugin bundle by its absolute path in the installed kit.
|
|
19
30
|
`.gitignore` keeps oxlint out of `node_modules/`, because oxlint still walks the installed package when only `ignorePatterns` names it.
|
|
20
31
|
|
|
21
32
|
The base turns `data-shape/readonly-collection-param` on for every TypeScript file and `data-shape/schema-twin` on for production files only.
|
|
@@ -29,9 +40,9 @@ An empty `--path-ignore-patterns` flag overrides the copied `[test] pathIgnorePa
|
|
|
29
40
|
Stryker 10 does not resolve `extends` in a JSON config.
|
|
30
41
|
So the Stryker preset is a JavaScript module that a repository's `stryker.conf.mjs` spreads, and a key set after the spread wins.
|
|
31
42
|
|
|
32
|
-
The dependency-cruiser
|
|
43
|
+
The dependency-cruiser rules parse with swc, because TypeScript 7, which is tsgo, has no compiler API that dependency-cruiser can use.
|
|
33
44
|
Without `@swc/core` installed, the cruise skips every `.ts` file without a warning, so the kit's own suite asserts that its TypeScript is cruised.
|
|
34
|
-
The
|
|
45
|
+
The rules count `bun` as a built-in module.
|
|
35
46
|
Only `@types/bun` resolves it, and that package is a dev dependency, so every runtime `bun` import would otherwise read as dev only.
|
|
36
47
|
|
|
37
48
|
## The shared configs close gaps in the types
|
|
@@ -39,7 +50,7 @@ Only `@types/bun` resolves it, and that package is a dev dependency, so every ru
|
|
|
39
50
|
The shared configs refuse four places where a value's type says less than the value does.
|
|
40
51
|
Each sits in a file a consumer already extends or copies.
|
|
41
52
|
|
|
42
|
-
The
|
|
53
|
+
The oxlint `base` turns on the five `typescript/no-unsafe-*` rules in an override for `.ts` and `.tsx` files outside `tests/`, so every config `defineConfig` returns carries them.
|
|
43
54
|
`typescript/no-explicit-any` and `strict` already refuse an `any` someone writes, so the `any` left is one nobody wrote.
|
|
44
55
|
`Array.isArray` narrows an `unknown` to `any[]`, `JSON.parse` returns `any`, `Object.entries` lists `any` values from an `object`, and a defaulted parameter in a generator passed to `Effect.fnUntraced` is typed `any`.
|
|
45
56
|
Tests stay out because bun:test types its asymmetric matchers, such as `expect.arrayContaining`, as returning `any`.
|
|
@@ -86,7 +97,8 @@ A path read without the resolver, such as `node_modules/@avi2dg/checks/scripts/t
|
|
|
86
97
|
npm adds `package.json`, `README.md` and `LICENSE` whatever `files` says.
|
|
87
98
|
`bun pm pack` builds the same tarball the registry serves, and the consumer e2e test installs that tarball.
|
|
88
99
|
|
|
89
|
-
Each oxlint plugin ships compiled under `dist/`, because Node refuses to strip types from a `.ts` file under `node_modules`.
|
|
100
|
+
Each oxlint plugin and each config builder ships compiled under `dist/`, because Node refuses to strip types from a `.ts` file under `node_modules`.
|
|
101
|
+
oxlint loads `oxlint.config.ts` under Node, so the builders ship as JavaScript beside a `.d.ts` that `tsc` reads.
|
|
90
102
|
`@oxlint/plugins` ships no RuleTester, so each `effect-channel`, `readability` and `data-shape` rule is proven red and green against an installed consumer in `tests/e2e/consumer.test.ts`, except `readability/thin-astro`, which `tests/e2e/astro-consumer.test.ts` proves against a consumer tree linked to the checkout.
|
|
91
103
|
`dist/` is committed, with the doc templates in `dist/templates/`, and so is `CHANGELOG.md`, which the same build writes.
|
|
92
104
|
No `prepack` or `prepublishOnly` script rebuilds them, so a publish ships the committed files.
|
|
@@ -112,6 +124,11 @@ The gates run one at a time and pass their output straight through, so each repo
|
|
|
112
124
|
`checks-lint` picks the gates that apply from the tracked files, and a repository cannot select gates.
|
|
113
125
|
A TypeScript gate runs as soon as the repository tracks TypeScript source, and `checks-lint-coverage` and `checks-unused` also run on tracked Astro source.
|
|
114
126
|
|
|
127
|
+
dependency-cruiser runs as the `checks-imports` gate rather than as a script each repository adds to `lint`, so no repository can leave it out.
|
|
128
|
+
A repository with no config of its own is cruised against the kit's defaults, since every rule has one.
|
|
129
|
+
The gate cruises the tracked TypeScript, so a build output or a linked library outside git never reaches the cruise.
|
|
130
|
+
It runs dependency-cruiser under Bun, which loads a TypeScript config wherever it sits, the kit's defaults under `node_modules` among them.
|
|
131
|
+
|
|
115
132
|
GitHub authors the pull request merge commit it builds as `GitHub <noreply@github.com>`, and `checks-commit-identity` refuses that author.
|
|
116
133
|
So `checks-lint` ends a pull request's range at the event's head commit, and the merge commit is never in it.
|
|
117
134
|
A `lint` that calls `checks-commit-identity HEAD` directly checks out `github.event.pull_request.head.sha` rather than the default merge ref.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: reference
|
|
3
|
+
audience: consumers
|
|
4
|
+
---
|
|
5
|
+
# checks-effect-scope
|
|
6
|
+
|
|
7
|
+
`checks-effect-scope` writes the Effect paths of `oxlint.config.ts` and the kit's severities into the Effect language service plugin in `tsconfig.json`.
|
|
8
|
+
As a gate it fails when either differs from what it would write.
|
|
9
|
+
|
|
10
|
+
## What it checks
|
|
11
|
+
|
|
12
|
+
It loads `oxlint.config.ts` and takes the `files` and `excludeFiles` of the override holding the six Effect rules, the one the `effect` key of `defineConfig` builds.
|
|
13
|
+
Each such override becomes one entry under `overrides` of the `@effect/language-service` plugin in `tsconfig.json`, with `files` as its `include`, `excludeFiles` as its `exclude`, and the severities in `src/quality/presets/effect.language-service.json` as its `options`.
|
|
14
|
+
The `overrides` of that plugin and the kit's keys in its `diagnosticSeverity` belong to it, and every other key of `tsconfig.json` stays as the repository wrote it.
|
|
15
|
+
With `effect: false` the plugin holds no `overrides`.
|
|
16
|
+
With `--check` it fails when the override or the kit's severities in `tsconfig.json` differ from what it would write, and writes nothing.
|
|
17
|
+
|
|
18
|
+
## What it reads
|
|
19
|
+
|
|
20
|
+
It reads `oxlint.config.ts` and `tsconfig.json` at the repository root.
|
|
21
|
+
It reads `tsconfig.json` as TypeScript does, so a comment or a trailing comma parses, and refuses one that does not parse as a JSONC object.
|
|
22
|
+
It rewrites only the `overrides` value of the `@effect/language-service` plugin and the kit's keys in its `diagnosticSeverity`, as JSON with two spaces of indent.
|
|
23
|
+
When that value is missing it adds the member, and the plugin, `plugins` or `compilerOptions` holding it, after the last member of each.
|
|
24
|
+
Each key of the `diagnosticSeverity` in the kit's `tsconfig.effect.json` gets the kit's value, and a key the repository added stays.
|
|
25
|
+
A key a release drops from `tsconfig.effect.json` stays in the repository's entry until the repository removes it.
|
|
26
|
+
It refuses a `compilerOptions` or `plugins` set to `null`, or a `diagnosticSeverity` that is not an object, which would leave no place for what it writes.
|
|
27
|
+
With `effect: false` it deletes the `overrides` member and its comma, and leaves the comments beside it.
|
|
28
|
+
Every other byte of `tsconfig.json` stays as the repository wrote it.
|
|
29
|
+
A repository without `tsconfig.json` has no language service to hold the paths, so it passes.
|
|
30
|
+
|
|
31
|
+
## Arguments
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
checks-effect-scope
|
|
35
|
+
checks-effect-scope --check
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
With no argument it writes `tsconfig.json`.
|
|
39
|
+
With `--check` it only compares.
|
|
40
|
+
|
|
41
|
+
## Exit codes
|
|
42
|
+
|
|
43
|
+
| Code | When |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| 0 | `tsconfig.json` holds the Effect paths and the kit's severities, or was written to hold them |
|
|
46
|
+
| 1 | with `--check`, `tsconfig.json` holds other Effect paths or other severities for the kit's keys |
|
|
47
|
+
| 2 | `oxlint.config.ts` does not load, or `tsconfig.json` does not parse, sets `compilerOptions` or `plugins` to `null`, or holds a `diagnosticSeverity` that is not an object |
|
|
48
|
+
|
|
49
|
+
## Sample output
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
effect-scope: the @effect/language-service entry in tsconfig.json differs from the Effect paths of oxlint.config.ts or the kit's severities; run checks-effect-scope to rewrite them
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## When it runs
|
|
56
|
+
|
|
57
|
+
A repository runs `checks-effect-scope` from its `build` script, so CI's `git diff --exit-code` after the build fails on a stale `tsconfig.json` too.
|
|
58
|
+
`checks-lint` runs it with `--check` when the repository tracks `oxlint.config.ts`.
|
|
59
|
+
|
|
60
|
+
## Related topics
|
|
61
|
+
|
|
62
|
+
- [The Effect rules](../configs/effect-rules.md)
|
|
63
|
+
- [checks-lint](checks-lint.md)
|