@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
|
@@ -25,16 +25,16 @@ It reads the repository's Knip configuration, which names the entries.
|
|
|
25
25
|
It reads `exports-baseline.json` in the repository root, which holds each accepted unused export as a file, kind and name triple.
|
|
26
26
|
It reads `exports-baseline.json` again at the base of the range, where a commit without the file counts as empty.
|
|
27
27
|
When the range adds baseline entries, it runs Knip again on a temporary checkout of the base and removes the checkout when it ends.
|
|
28
|
-
That checkout links the repository's `node_modules`, so a configuration importing the kit's
|
|
29
|
-
|
|
28
|
+
That checkout links the repository's `node_modules`, so a configuration importing the kit's builder loads there too.
|
|
29
|
+
The repository writes `knip.config.ts` with the kit's builder, which requires `entry`, the files nothing imports:
|
|
30
30
|
|
|
31
31
|
```ts
|
|
32
|
-
import
|
|
32
|
+
import { defineConfig } from "@avi2dg/checks/knip";
|
|
33
33
|
|
|
34
|
-
export default {
|
|
34
|
+
export default defineConfig({ entry: ["src/index.ts"] });
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
The
|
|
37
|
+
The builder sets `include` to unreferenced files alone, and adds `tests/**/*.test.ts` and `dependency-cruiser.config.ts` to the entries.
|
|
38
38
|
The gate resolves the Knip binary from the installed kit, so a consumer installs nothing beyond the kit.
|
|
39
39
|
|
|
40
40
|
## Arguments
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: reference
|
|
3
|
+
audience: consumers
|
|
4
|
+
---
|
|
5
|
+
# checks-imports
|
|
6
|
+
|
|
7
|
+
`checks-imports` is the gate that holds a repository's imports to the dependency rules, with the kit's defaults when the repository writes no config.
|
|
8
|
+
|
|
9
|
+
## What it checks
|
|
10
|
+
|
|
11
|
+
It runs dependency-cruiser over every tracked `.ts`, `.tsx`, `.mts` and `.cts` file and names each violation with its rule, the importing module and the module it reaches.
|
|
12
|
+
It fails on a violation whose rule has `error` severity, and lists a `warn` or `info` violation without failing.
|
|
13
|
+
The rules are those [The dependency rules](../configs/dependency-rules.md) lists, plus each rule the repository's config adds.
|
|
14
|
+
|
|
15
|
+
In a repository whose `package.json` lists `astro` under `dependencies` or `devDependencies`, the kit's defaults leave `no-orphans` out.
|
|
16
|
+
The cruise never reads an `.astro` file, so a `.ts` module only a page imports would read as an orphan.
|
|
17
|
+
[checks-unused](checks-unused.md) already names each file no entry reaches there, and it reads `.astro` imports.
|
|
18
|
+
A repository that wants the rule back lists its own `no-orphans` under `forbidden` in `dependency-cruiser.config.ts`.
|
|
19
|
+
|
|
20
|
+
## What it reads
|
|
21
|
+
|
|
22
|
+
It reads the working tree from the repository root, and leaves out a tracked file the working tree no longer holds.
|
|
23
|
+
It cruises against the first config it finds there:
|
|
24
|
+
|
|
25
|
+
1. `dependency-cruiser.config.ts`, which calls `defineConfig` from `@avi2dg/checks/dependency-cruiser`.
|
|
26
|
+
1. A config under one of dependency-cruiser's own names, such as `.dependency-cruiser.cjs`.
|
|
27
|
+
1. The kit's defaults, which `defineConfig()` returns with no argument.
|
|
28
|
+
|
|
29
|
+
It runs dependency-cruiser from the installed kit under Bun, which loads a TypeScript config wherever it sits.
|
|
30
|
+
|
|
31
|
+
## Arguments
|
|
32
|
+
|
|
33
|
+
It takes none.
|
|
34
|
+
|
|
35
|
+
## Exit codes
|
|
36
|
+
|
|
37
|
+
| Code | When |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| 0 | no import breaks an `error` rule |
|
|
40
|
+
| 1 | an import breaks an `error` rule |
|
|
41
|
+
| 2 | the repository tracks no TypeScript, or dependency-cruiser cannot load the config or run |
|
|
42
|
+
|
|
43
|
+
## Sample output
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
imports: 2 violation(s) in 36 module(s) cruised against dependency-cruiser.config.ts
|
|
47
|
+
error not-to-dev-dep: src/index.ts → node_modules/effect/dist/index.js
|
|
48
|
+
error no-orphans: src/lonely.ts
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
A passing run counts the modules it cruised:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
imports: 225 module(s) cruised against dependency-cruiser.config.ts, no violation
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## When it runs
|
|
58
|
+
|
|
59
|
+
`checks-lint` runs it when the repository tracks a `.ts` or `.tsx` file, so a repository with no config of its own still has its imports judged.
|
|
60
|
+
|
|
61
|
+
## Related topics
|
|
62
|
+
|
|
63
|
+
- [The dependency rules](../configs/dependency-rules.md)
|
|
64
|
+
- [checks-lint](checks-lint.md)
|
|
@@ -47,7 +47,7 @@ With two arguments, the base and head override range discovery.
|
|
|
47
47
|
|
|
48
48
|
```
|
|
49
49
|
checks-lint: range 2504acf098d120e73a8ece3c96f22b934f35c6a8..10ba7d8935b73ed72624120a1542e51bd21ca7c7 from HEAD against origin/main
|
|
50
|
-
checks-lint: 1 of
|
|
50
|
+
checks-lint: 1 of 16 gate(s) failed: checks-comment-gate
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
<!-- end generated lint-sample -->
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: reference
|
|
3
|
+
audience: consumers
|
|
4
|
+
---
|
|
5
|
+
# checks-mutation-baseline
|
|
6
|
+
|
|
7
|
+
`checks-mutation-baseline` restores the newest mutation baseline that `main` published, and on a self-hosted runner it keeps each baseline it downloads so the next job copies it instead.
|
|
8
|
+
|
|
9
|
+
## What it checks
|
|
10
|
+
|
|
11
|
+
It reads the successful runs of `mutation.yml` on `main`.
|
|
12
|
+
Without `--full` it takes the newest of the 20 newest of those runs that a pull request did not start.
|
|
13
|
+
It restores that run's `mutation-baseline` artifact, and takes the run whether or not it holds one.
|
|
14
|
+
It tries no older run, so a newest run without the artifact restores nothing.
|
|
15
|
+
An artifact without `stryker-incremental.json` counts as no baseline.
|
|
16
|
+
It copies `stryker-incremental.json` to the first destination, and `mutation/mutation.json` to the second destination when the artifact holds it.
|
|
17
|
+
|
|
18
|
+
With `--full` it lists up to 50 `schedule` runs and up to 50 `workflow_dispatch` runs apart, so pushes cannot crowd the full runs out of one list.
|
|
19
|
+
It tries those runs newest first, and restores the first `mutation-baseline-full` artifact that has not expired, downloads and holds the report.
|
|
20
|
+
It tries no run after that one.
|
|
21
|
+
It takes the report from `mutation.json` at the artifact's root, or else from `mutation/mutation.json`, and copies it to the destination.
|
|
22
|
+
|
|
23
|
+
It creates the directories each destination needs.
|
|
24
|
+
|
|
25
|
+
## What it reads
|
|
26
|
+
|
|
27
|
+
It reads the runs and their artifacts through `gh`, which needs a token that reads the repository's Actions, such as `GH_TOKEN: ${{ github.token }}` with `actions: read`.
|
|
28
|
+
The `mutation-baseline` artifact holds `stryker-incremental.json` and `mutation/mutation.json`, which an upload of `reports/stryker-incremental.json` and `reports/mutation/mutation.json` writes.
|
|
29
|
+
The `mutation-baseline-full` artifact holds `mutation.json` at its root, which an upload of `reports/mutation/mutation.json` alone writes.
|
|
30
|
+
It reads `RUNNER_ENVIRONMENT` and `HOME` to place the runner cache.
|
|
31
|
+
|
|
32
|
+
## The runner cache
|
|
33
|
+
|
|
34
|
+
The cache sits under `$HOME/.cache/avi2dg-checks/mutation-baseline/`.
|
|
35
|
+
It lies outside the job's workspace and `RUNNER_TEMP`, so it outlives the job on a self-hosted runner.
|
|
36
|
+
Each entry sits at `<repository id>/<artifact name>/<artifact id>/`.
|
|
37
|
+
GitHub gives every upload a new artifact id, so an entry under a listed id is never stale.
|
|
38
|
+
On a hit the bin copies the files from the entry and downloads nothing.
|
|
39
|
+
On a miss it downloads into a staging directory beside the entry and renames it into place.
|
|
40
|
+
A job sharing the runner sees an entry whole or not at all.
|
|
41
|
+
If the entry already exists, because another job placed it or it lost a file, the bin keeps it and restores from its own download.
|
|
42
|
+
After each download it keeps the 2 highest artifact ids for that repository and artifact name, plus the entry it restores from.
|
|
43
|
+
It removes the rest.
|
|
44
|
+
A 100 MB baseline zip can unpack to about 500 MB, so a repository restoring both artifacts holds about 2 GB.
|
|
45
|
+
|
|
46
|
+
When `RUNNER_ENVIRONMENT` is `github-hosted`, it downloads into a temporary directory and writes no cache, since a hosted runner starts every job on a fresh machine.
|
|
47
|
+
When `HOME` is unset it works the same way, and prints to stderr that the runner cache is disabled.
|
|
48
|
+
|
|
49
|
+
## Arguments
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
checks-mutation-baseline <incremental-dest> [mutation-json-dest]
|
|
53
|
+
checks-mutation-baseline --full <mutation-json-dest>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`--full` restores only the report of a run that started with no state.
|
|
57
|
+
|
|
58
|
+
## Exit codes
|
|
59
|
+
|
|
60
|
+
| Code | When |
|
|
61
|
+
| --- | --- |
|
|
62
|
+
| 0 | it restored a baseline, or found none to restore |
|
|
63
|
+
| 2 | the arguments do not parse, `gh` cannot list the runs, the cache cannot be written, or another job removed the entry before it was copied |
|
|
64
|
+
|
|
65
|
+
A failed artifact lookup or download prints the `gh` error and moves on, as a missing artifact does.
|
|
66
|
+
|
|
67
|
+
## Sample output
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
mutation-baseline: restored it from the runner's cache, mutation-baseline artifact 11666313211 from run 38042411822
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
A miss prints `downloaded it into the runner's cache` instead, and a hosted runner prints `downloaded it`.
|
|
74
|
+
|
|
75
|
+
## When it runs
|
|
76
|
+
|
|
77
|
+
Only a CI step the repository writes runs it.
|
|
78
|
+
A baseline job restores the state the previous `main` run left before an incremental Stryker run:
|
|
79
|
+
|
|
80
|
+
```yaml
|
|
81
|
+
- name: Restore previous baseline state
|
|
82
|
+
env:
|
|
83
|
+
GH_TOKEN: ${{ github.token }}
|
|
84
|
+
run: bunx checks-mutation-baseline reports/stryker-incremental.json
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
A pull request's scope step restores the full report with `--full`:
|
|
88
|
+
|
|
89
|
+
```yaml
|
|
90
|
+
- name: Select mutation scope
|
|
91
|
+
env:
|
|
92
|
+
GH_TOKEN: ${{ github.token }}
|
|
93
|
+
run: bunx checks-mutation-baseline --full "$RUNNER_TEMP/baseline-full/mutation.json"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Related topics
|
|
97
|
+
|
|
98
|
+
- [checks-mutation](checks-mutation.md)
|
|
99
|
+
- [checks-mutation-compare](checks-mutation-compare.md)
|
|
@@ -4,7 +4,7 @@ audience: consumers
|
|
|
4
4
|
---
|
|
5
5
|
# checks-mutation-compare
|
|
6
6
|
|
|
7
|
-
`checks-mutation-compare`
|
|
7
|
+
`checks-mutation-compare` compares the mutants of two reports and lists each one that regresses, rather than judging an absolute score.
|
|
8
8
|
|
|
9
9
|
## What it checks
|
|
10
10
|
|
|
@@ -68,7 +68,10 @@ mutation-compare: REGRESSION (1 mutant(s))
|
|
|
68
68
|
## When it runs
|
|
69
69
|
|
|
70
70
|
Only a CI step the repository writes runs it.
|
|
71
|
-
A repository runs it with `--advisory
|
|
71
|
+
A repository runs it with `--advisory`, so the comparison stays advisory and never blocks a pull request.
|
|
72
|
+
Its lines land only in the CI log.
|
|
73
|
+
A repository that wants a reviewer to answer each regression line puts the lines in front of that reviewer with its own CI step.
|
|
74
|
+
That step can post them as a pull request comment.
|
|
72
75
|
A full sweep runs in CI and never on a laptop.
|
|
73
76
|
Start a baseline with `gh workflow run mutation` and keep its report as an artifact.
|
|
74
77
|
The shared preset refuses a full `stryker run` outside CI and names that workflow command instead, as [checks-mutation](checks-mutation.md) says.
|
|
@@ -75,9 +75,9 @@ An `--incremental` run with no report stops before instrumenting with the missin
|
|
|
75
75
|
## When it runs
|
|
76
76
|
|
|
77
77
|
A repository runs full baselines from the mutation workflow on `workflow_dispatch`.
|
|
78
|
-
A repository that picks a pull request scope from a baseline
|
|
78
|
+
A repository that picks a pull request scope from a baseline starts that workflow weekly with `gh workflow run mutation`, because a GitHub schedule can start hours late.
|
|
79
79
|
Run scoped checks locally during development.
|
|
80
|
-
|
|
80
|
+
Each baseline costs a full Stryker run.
|
|
81
81
|
|
|
82
82
|
## Running it in CI
|
|
83
83
|
|
|
@@ -18,15 +18,15 @@ It fails when the repository holds no Knip configuration, since Knip's default e
|
|
|
18
18
|
|
|
19
19
|
It reads the working tree, so an uncommitted file is judged like a committed one.
|
|
20
20
|
It reads the repository's Knip configuration, which names the entries.
|
|
21
|
-
|
|
21
|
+
The repository writes `knip.config.ts` with the kit's builder, which requires `entry`, the files nothing imports:
|
|
22
22
|
|
|
23
23
|
```ts
|
|
24
|
-
import
|
|
24
|
+
import { defineConfig } from "@avi2dg/checks/knip";
|
|
25
25
|
|
|
26
|
-
export default {
|
|
26
|
+
export default defineConfig({ entry: ["src/index.ts"] });
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
The
|
|
29
|
+
The builder sets `include` to unreferenced files alone, and adds `tests/**/*.test.ts` and `dependency-cruiser.config.ts` to the entries.
|
|
30
30
|
In a repository that depends on `astro`, Knip reads `.astro` imports, so a file only an `.astro` entry imports counts as used and an `.astro` file no entry reaches is named.
|
|
31
31
|
The gate resolves the Knip binary from the installed kit, so a consumer installs nothing beyond the kit.
|
|
32
32
|
|
package/oxlintrc.json
CHANGED
|
@@ -1,19 +1,48 @@
|
|
|
1
1
|
{
|
|
2
|
-
"plugins": [
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
"plugins": [
|
|
3
|
+
"typescript",
|
|
4
|
+
"oxc",
|
|
5
|
+
"eslint",
|
|
6
|
+
"import"
|
|
7
|
+
],
|
|
8
|
+
"jsPlugins": [
|
|
9
|
+
"./dist/effect-channel/index.js",
|
|
10
|
+
"./dist/readability/index.js",
|
|
11
|
+
"./dist/data-shape/index.js"
|
|
12
|
+
],
|
|
13
|
+
"categories": {
|
|
14
|
+
"correctness": "error",
|
|
15
|
+
"suspicious": "error"
|
|
16
|
+
},
|
|
5
17
|
"rules": {
|
|
6
18
|
"effect-channel/no-error-channel-escape": "error",
|
|
7
19
|
"typescript/no-explicit-any": "error",
|
|
8
|
-
"typescript/ban-ts-comment": [
|
|
20
|
+
"typescript/ban-ts-comment": [
|
|
21
|
+
"error",
|
|
22
|
+
{
|
|
23
|
+
"ts-expect-error": true
|
|
24
|
+
}
|
|
25
|
+
],
|
|
9
26
|
"typescript/no-inferrable-types": "error",
|
|
10
27
|
"typescript/explicit-module-boundary-types": "error",
|
|
11
28
|
"typescript/switch-exhaustiveness-check": "error",
|
|
12
29
|
"typescript/prefer-readonly": "error",
|
|
13
|
-
"typescript/no-unnecessary-condition": [
|
|
30
|
+
"typescript/no-unnecessary-condition": [
|
|
31
|
+
"error",
|
|
32
|
+
{
|
|
33
|
+
"allowConstantLoopConditions": true
|
|
34
|
+
}
|
|
35
|
+
],
|
|
14
36
|
"typescript/no-unnecessary-type-parameters": "error",
|
|
15
37
|
"typescript/use-unknown-in-catch-callback-variable": "error",
|
|
16
|
-
"eslint/no-unused-vars": [
|
|
38
|
+
"eslint/no-unused-vars": [
|
|
39
|
+
"error",
|
|
40
|
+
{
|
|
41
|
+
"argsIgnorePattern": "^_",
|
|
42
|
+
"varsIgnorePattern": "^_",
|
|
43
|
+
"ignoreRestSiblings": true
|
|
44
|
+
}
|
|
45
|
+
],
|
|
17
46
|
"typescript/no-unsafe-type-assertion": "error",
|
|
18
47
|
"typescript/no-non-null-assertion": "error",
|
|
19
48
|
"typescript/no-deprecated": "error",
|
|
@@ -21,14 +50,22 @@
|
|
|
21
50
|
},
|
|
22
51
|
"overrides": [
|
|
23
52
|
{
|
|
24
|
-
"files": [
|
|
53
|
+
"files": [
|
|
54
|
+
"**/*.ts",
|
|
55
|
+
"**/*.tsx"
|
|
56
|
+
],
|
|
25
57
|
"rules": {
|
|
26
58
|
"data-shape/readonly-collection-param": "error"
|
|
27
59
|
}
|
|
28
60
|
},
|
|
29
61
|
{
|
|
30
|
-
"files": [
|
|
31
|
-
|
|
62
|
+
"files": [
|
|
63
|
+
"**/*.ts",
|
|
64
|
+
"**/*.tsx"
|
|
65
|
+
],
|
|
66
|
+
"excludeFiles": [
|
|
67
|
+
"tests/**"
|
|
68
|
+
],
|
|
32
69
|
"rules": {
|
|
33
70
|
"data-shape/schema-twin": "error",
|
|
34
71
|
"typescript/no-unsafe-assignment": "error",
|
|
@@ -39,7 +76,9 @@
|
|
|
39
76
|
}
|
|
40
77
|
},
|
|
41
78
|
{
|
|
42
|
-
"files": [
|
|
79
|
+
"files": [
|
|
80
|
+
"**/*.astro"
|
|
81
|
+
],
|
|
43
82
|
"rules": {
|
|
44
83
|
"readability/thin-astro": "error",
|
|
45
84
|
"import/no-unassigned-import": "off"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avi2dg/checks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"description": "Deterministic checks shared across a set of TypeScript repositories",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"src/testing/mutation-scope.js",
|
|
45
45
|
"src/testing/mutation-guard-plugin.js",
|
|
46
46
|
"src/testing/mutation-compare.ts",
|
|
47
|
+
"src/testing/mutation-baseline.ts",
|
|
47
48
|
"src/testing/subsumed-tests.ts",
|
|
48
49
|
"src/delivery/ci-wiring.ts",
|
|
49
50
|
"src/delivery/shell-command.ts",
|
|
@@ -80,15 +81,21 @@
|
|
|
80
81
|
"src/docs/doc-templates.ts",
|
|
81
82
|
"src/docs/doc-names.ts",
|
|
82
83
|
"dist/templates/",
|
|
83
|
-
"src/quality/presets/effect.oxlint.json",
|
|
84
84
|
"src/quality/presets/effect.language-service.json",
|
|
85
|
+
"src/quality/presets/oxlint.ts",
|
|
86
|
+
"src/quality/presets/dependency-cruiser.ts",
|
|
87
|
+
"src/quality/effect-scope.ts",
|
|
88
|
+
"src/quality/jsonc-patch.ts",
|
|
89
|
+
"src/dependencies/imports.ts",
|
|
90
|
+
"src/dependencies/kit-defaults.ts",
|
|
85
91
|
"oxlintrc.json",
|
|
86
92
|
"stryker.preset.js",
|
|
87
93
|
"tsconfig.effect.json",
|
|
88
94
|
"ts-reset.d.ts",
|
|
89
95
|
"dist/effect-channel/index.js",
|
|
90
96
|
"dist/readability/index.js",
|
|
91
|
-
"dist/data-shape/index.js"
|
|
97
|
+
"dist/data-shape/index.js",
|
|
98
|
+
"dist/presets/"
|
|
92
99
|
],
|
|
93
100
|
"exports": {
|
|
94
101
|
"./dependency-cruiser.config.js": "./dependency-cruiser.config.js",
|
|
@@ -99,7 +106,19 @@
|
|
|
99
106
|
"./stryker.preset.js": "./stryker.preset.js",
|
|
100
107
|
"./templates/*": "./dist/templates/*",
|
|
101
108
|
"./tsconfig.effect.json": "./tsconfig.effect.json",
|
|
102
|
-
"./browser-hooks.ts": "./src/quality/browser/hooks.ts"
|
|
109
|
+
"./browser-hooks.ts": "./src/quality/browser/hooks.ts",
|
|
110
|
+
"./oxlint": {
|
|
111
|
+
"types": "./dist/presets/oxlint.d.ts",
|
|
112
|
+
"default": "./dist/presets/oxlint.js"
|
|
113
|
+
},
|
|
114
|
+
"./knip": {
|
|
115
|
+
"types": "./dist/presets/knip.d.ts",
|
|
116
|
+
"default": "./dist/presets/knip.js"
|
|
117
|
+
},
|
|
118
|
+
"./dependency-cruiser": {
|
|
119
|
+
"types": "./dist/presets/dependency-cruiser.d.ts",
|
|
120
|
+
"default": "./dist/presets/dependency-cruiser.js"
|
|
121
|
+
}
|
|
103
122
|
},
|
|
104
123
|
"bin": {
|
|
105
124
|
"checks-lint": "src/core/lint.ts",
|
|
@@ -115,6 +134,7 @@
|
|
|
115
134
|
"checks-commit-identity": "src/delivery/commit-identity.ts",
|
|
116
135
|
"checks-mutation": "src/testing/mutation.ts",
|
|
117
136
|
"checks-mutation-compare": "src/testing/mutation-compare.ts",
|
|
137
|
+
"checks-mutation-baseline": "src/testing/mutation-baseline.ts",
|
|
118
138
|
"checks-subsumed-tests": "src/testing/subsumed-tests.ts",
|
|
119
139
|
"checks-ci-wiring": "src/delivery/ci-wiring.ts",
|
|
120
140
|
"checks-comment-gate": "src/quality/comment-gate.ts",
|
|
@@ -128,12 +148,14 @@
|
|
|
128
148
|
"checks-advisories": "src/dependencies/advisories.ts",
|
|
129
149
|
"checks-secrets": "src/delivery/secrets.ts",
|
|
130
150
|
"checks-frontend-syntax": "src/quality/frontend-syntax.ts",
|
|
131
|
-
"checks-browser": "src/quality/browser/browser.ts"
|
|
151
|
+
"checks-browser": "src/quality/browser/browser.ts",
|
|
152
|
+
"checks-imports": "src/dependencies/imports.ts",
|
|
153
|
+
"checks-effect-scope": "src/quality/effect-scope.ts"
|
|
132
154
|
},
|
|
133
155
|
"scripts": {
|
|
134
156
|
"prepare": "bun src/dependencies/vendor.ts --library effect --package effect --repository https://github.com/Effect-TS/effect.git --tag 'effect@{version}' --path packages/effect/package.json",
|
|
135
|
-
"build": "bun build src/quality/effect-channel/index.ts --outdir dist/effect-channel --target node --format esm && bun build src/complexity/readability/index.ts --outdir dist/readability --target node --format esm && bun build src/quality/data-shape/index.ts --outdir dist/data-shape --target node --format esm && bun scripts/doc-templates-write.ts && bun src/delivery/changelog-write.ts && bun scripts/doc-blocks-write.ts",
|
|
136
|
-
"lint": "oxlint
|
|
157
|
+
"build": "bun build src/quality/effect-channel/index.ts --outdir dist/effect-channel --target node --format esm && bun build src/complexity/readability/index.ts --outdir dist/readability --target node --format esm && bun build src/quality/data-shape/index.ts --outdir dist/data-shape --target node --format esm && bun build src/quality/presets/oxlint.ts src/quality/presets/knip.ts src/quality/presets/dependency-cruiser.ts --outdir dist/presets --target node --format esm && tsc -p tsconfig.presets.json && bun scripts/configs-write.ts && bun src/quality/effect-scope.ts && bun scripts/doc-templates-write.ts && bun src/delivery/changelog-write.ts && bun scripts/doc-blocks-write.ts",
|
|
158
|
+
"lint": "oxlint && bun src/core/lint.ts",
|
|
137
159
|
"typecheck": "tsc --noEmit && effect-tsgo diagnostics --project tsconfig.json --format text --strict",
|
|
138
160
|
"test": "bun src/testing/test.ts",
|
|
139
161
|
"mutate": "bunx stryker run",
|
|
@@ -130,7 +130,7 @@ export function report(head: Baseline, { unlisted, added, stale }: Drift): strin
|
|
|
130
130
|
|
|
131
131
|
const scan = scanTree(NAME, INCLUDED, TYPESCRIPT_SOURCE).pipe(Effect.mapError((cause) => new ExportsError({ message: cause.message })));
|
|
132
132
|
|
|
133
|
-
// Knip loads the configuration's imports, such as the kit's knip
|
|
133
|
+
// Knip loads the configuration's imports, such as the kit's knip builder, from a node_modules the checkout lacks.
|
|
134
134
|
const unusedAt = Effect.fn("unusedAt")(
|
|
135
135
|
function* (rev: string, root: string) {
|
|
136
136
|
const files = yield* pathsAt(rev, [], root);
|
package/src/complexity/knip.ts
CHANGED
|
@@ -63,7 +63,7 @@ export const scanTree = Effect.fn("scanTree")(function* (name: string, args: rea
|
|
|
63
63
|
if (tracked.length === 0) return yield* new KnipError({ message: `no tracked ${source.content} to scan` });
|
|
64
64
|
const reported = yield* knipReport(root, args);
|
|
65
65
|
if (reported.kind === "unconfigured") {
|
|
66
|
-
yield* Console.log(`${name}: no knip configuration names entry files, so add
|
|
66
|
+
yield* Console.log(`${name}: no knip configuration names entry files, so add a knip.config.ts calling defineConfig from @avi2dg/checks/knip`);
|
|
67
67
|
}
|
|
68
68
|
return { root, tracked: tracked.length, reported };
|
|
69
69
|
});
|
package/src/core/gates.ts
CHANGED
|
@@ -36,6 +36,8 @@ export const LINTED_SOURCE: TrackedContent = { pathspecs: ["*.ts", "*.tsx", "*.a
|
|
|
36
36
|
|
|
37
37
|
const BUN_LOCKFILE: TrackedContent = { pathspecs: ["bun.lock"], content: "a bun lockfile" };
|
|
38
38
|
|
|
39
|
+
const OXLINT_CONFIG: TrackedContent = { pathspecs: ["oxlint.config.ts"], content: "an oxlint.config.ts" };
|
|
40
|
+
|
|
39
41
|
const FRONTEND_SYNTAX_DECLARATION: TrackedContent = { pathspecs: ["frontend-syntax.json"], content: "a frontend syntax declaration" };
|
|
40
42
|
|
|
41
43
|
export const KIT_GATES = [
|
|
@@ -43,6 +45,7 @@ export const KIT_GATES = [
|
|
|
43
45
|
{ bin: "checks-test-layout", vector: "testing", file: "test-layout.ts", reads: "tree", appliesTo: TYPESCRIPT_SOURCE },
|
|
44
46
|
{ bin: "checks-commit-identity", vector: "delivery", file: "commit-identity.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
|
|
45
47
|
{ bin: "checks-comment-gate", vector: "quality", file: "comment-gate.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
|
|
48
|
+
{ bin: "checks-effect-scope", vector: "quality", file: "effect-scope.ts", reads: "tree", args: ["--check"], appliesTo: OXLINT_CONFIG },
|
|
46
49
|
{ bin: "checks-frontend-syntax", vector: "quality", file: "frontend-syntax.ts", reads: "tree", appliesTo: FRONTEND_SYNTAX_DECLARATION },
|
|
47
50
|
{ bin: "checks-suppressions-ratchet", vector: "complexity", file: "suppressions-ratchet.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
|
|
48
51
|
{ bin: "checks-ci-wiring", vector: "delivery", file: "ci-wiring.ts", reads: "tree", appliesTo: EVERY_REPOSITORY },
|
|
@@ -52,5 +55,6 @@ export const KIT_GATES = [
|
|
|
52
55
|
{ bin: "checks-unused", vector: "complexity", file: "unused.ts", reads: "tree", appliesTo: LINTED_SOURCE },
|
|
53
56
|
{ bin: "checks-exports", vector: "complexity", file: "exports.ts", reads: "range", appliesTo: TYPESCRIPT_SOURCE },
|
|
54
57
|
{ bin: "checks-quarantine-clock", vector: "testing", file: "quarantine-clock.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
|
|
58
|
+
{ bin: "checks-imports", vector: "dependencies", file: "imports.ts", reads: "tree", appliesTo: TYPESCRIPT_SOURCE },
|
|
55
59
|
{ bin: "checks-advisories", vector: "dependencies", file: "advisories.ts", reads: "range", appliesTo: BUN_LOCKFILE },
|
|
56
60
|
] as const satisfies readonly KitGate[];
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { Console, Effect, FileSystem, Path, Schema } from "effect";
|
|
3
|
+
import { collect, git } from "../core/git.ts";
|
|
4
|
+
import { runMain, Usage } from "../core/main.ts";
|
|
5
|
+
|
|
6
|
+
export const PROJECT_CONFIG = "dependency-cruiser.config.ts";
|
|
7
|
+
|
|
8
|
+
export const OWN_CONFIGS = [
|
|
9
|
+
".dependency-cruiser.json",
|
|
10
|
+
".dependency-cruiser.js",
|
|
11
|
+
".dependency-cruiser.cjs",
|
|
12
|
+
".dependency-cruiser.mjs",
|
|
13
|
+
".dependency-cruiser.ts",
|
|
14
|
+
".dependency-cruiser.cts",
|
|
15
|
+
".dependency-cruiser.mts",
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
export const CRUISED = ["*.ts", "*.tsx", "*.mts", "*.cts"] as const;
|
|
19
|
+
|
|
20
|
+
const NAME = "imports";
|
|
21
|
+
const USAGE = "usage: imports.ts";
|
|
22
|
+
const KIT_DEFAULTS = "kit-defaults.ts";
|
|
23
|
+
|
|
24
|
+
class ImportsError extends Schema.TaggedError<ImportsError>()("ImportsError", {
|
|
25
|
+
message: Schema.String,
|
|
26
|
+
}) {}
|
|
27
|
+
|
|
28
|
+
const Violation = Schema.Struct({
|
|
29
|
+
from: Schema.String,
|
|
30
|
+
to: Schema.String,
|
|
31
|
+
rule: Schema.Struct({ name: Schema.String, severity: Schema.String }),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export type Violation = typeof Violation.Type;
|
|
35
|
+
|
|
36
|
+
const decodeCruise = Schema.decodeUnknownEffect(
|
|
37
|
+
Schema.fromJsonString(
|
|
38
|
+
Schema.Struct({
|
|
39
|
+
summary: Schema.Struct({ violations: Schema.Array(Violation), error: Schema.Int, totalCruised: Schema.Int }),
|
|
40
|
+
}),
|
|
41
|
+
),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
export function describeViolation({ from, to, rule }: Violation): string {
|
|
45
|
+
return from === to ? ` ${rule.severity} ${rule.name}: ${from}` : ` ${rule.severity} ${rule.name}: ${from} → ${to}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const configOf = Effect.fn("configOf")(function* (root: string) {
|
|
49
|
+
const fs = yield* FileSystem.FileSystem;
|
|
50
|
+
const path = yield* Path.Path;
|
|
51
|
+
for (const name of [PROJECT_CONFIG, ...OWN_CONFIGS]) {
|
|
52
|
+
if (yield* fs.exists(path.join(root, name))) return { file: name, shown: name };
|
|
53
|
+
}
|
|
54
|
+
return { file: path.join(import.meta.dir, KIT_DEFAULTS), shown: "the kit's defaults" };
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const depcruise = Effect.fn("depcruise")(function* () {
|
|
58
|
+
const path = yield* Path.Path;
|
|
59
|
+
const main = yield* Effect.try({
|
|
60
|
+
try: () => new URL("../../bin/dependency-cruiser.mjs", import.meta.resolve("dependency-cruiser")),
|
|
61
|
+
catch: () => new ImportsError({ message: "cannot resolve dependency-cruiser, a peer dependency of the kit" }),
|
|
62
|
+
});
|
|
63
|
+
return yield* path.fromFileUrl(main);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const trackedInTree = Effect.fn("trackedInTree")(function* (root: string) {
|
|
67
|
+
const fs = yield* FileSystem.FileSystem;
|
|
68
|
+
const path = yield* Path.Path;
|
|
69
|
+
const listed = yield* git(["ls-files", "-z", "--", ...CRUISED], root);
|
|
70
|
+
return yield* Effect.filter(listed.split("\0").filter(Boolean), (file) => fs.exists(path.join(root, file)), { concurrency: "unbounded" });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const imports = Effect.gen(function* () {
|
|
74
|
+
if (process.argv.length > 2) return yield* new Usage({ message: USAGE });
|
|
75
|
+
const root = (yield* git(["rev-parse", "--show-toplevel"])).trim();
|
|
76
|
+
const files = yield* trackedInTree(root);
|
|
77
|
+
if (files.length === 0) return yield* new ImportsError({ message: "no tracked TypeScript to cruise" });
|
|
78
|
+
const config = yield* configOf(root);
|
|
79
|
+
const run = yield* collect(process.execPath, [yield* depcruise(), "--config", config.file, "--output-type", "json", ...files], root).pipe(
|
|
80
|
+
Effect.mapError((cause) => new ImportsError({ message: `cannot run dependency-cruiser: ${cause.message}` })),
|
|
81
|
+
);
|
|
82
|
+
const { summary } = yield* decodeCruise(run.stdout).pipe(
|
|
83
|
+
Effect.mapError(() => new ImportsError({ message: `dependency-cruiser exits ${run.exitCode} with no report: ${(run.stdout + run.stderr).trim()}` })),
|
|
84
|
+
);
|
|
85
|
+
const cruised = `${summary.totalCruised} module(s) cruised against ${config.shown}`;
|
|
86
|
+
if (summary.violations.length === 0) {
|
|
87
|
+
yield* Console.log(`${NAME}: ${cruised}, no violation`);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
yield* Console.error([`${NAME}: ${summary.violations.length} violation(s) in ${cruised}`, ...summary.violations.map(describeViolation)].join("\n"));
|
|
91
|
+
return summary.error === 0;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (import.meta.main) runMain(NAME, imports);
|