@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.
Files changed (35) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +33 -20
  3. package/dependency-cruiser.config.js +92 -60
  4. package/dist/presets/dependency-cruiser.d.ts +15 -0
  5. package/dist/presets/dependency-cruiser.js +126 -0
  6. package/dist/presets/knip.d.ts +6 -0
  7. package/dist/presets/knip.js +15 -0
  8. package/dist/presets/oxlint.d.ts +33 -0
  9. package/dist/presets/oxlint.js +145 -0
  10. package/docs/configs/dependency-rules.md +28 -31
  11. package/docs/configs/effect-rules.md +47 -23
  12. package/docs/configs/native-settings.md +65 -8
  13. package/docs/configs/typescript-rules.md +7 -7
  14. package/docs/design.md +25 -8
  15. package/docs/gates/checks-effect-scope.md +63 -0
  16. package/docs/gates/checks-exports.md +5 -5
  17. package/docs/gates/checks-imports.md +64 -0
  18. package/docs/gates/checks-lint.md +1 -1
  19. package/docs/gates/checks-mutation-baseline.md +99 -0
  20. package/docs/gates/checks-mutation-compare.md +5 -2
  21. package/docs/gates/checks-mutation.md +2 -2
  22. package/docs/gates/checks-unused.md +4 -4
  23. package/oxlintrc.json +49 -10
  24. package/package.json +29 -7
  25. package/src/complexity/exports.ts +1 -1
  26. package/src/complexity/knip.ts +1 -1
  27. package/src/core/gates.ts +4 -0
  28. package/src/dependencies/imports.ts +94 -0
  29. package/src/dependencies/kit-defaults.ts +3 -0
  30. package/src/quality/effect-scope.ts +116 -0
  31. package/src/quality/jsonc-patch.ts +209 -0
  32. package/src/quality/presets/dependency-cruiser.ts +148 -0
  33. package/src/quality/presets/oxlint.ts +165 -0
  34. package/src/testing/mutation-baseline.ts +211 -0
  35. 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 base loads there too.
29
- Knip configurations do not extend a package file, so the consumer configuration imports the kit's base and spreads it:
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 base from "@avi2dg/checks/knip-base.json";
32
+ import { defineConfig } from "@avi2dg/checks/knip";
33
33
 
34
- export default { ...base, entry: ["src/index.ts", "tests/**/*.test.ts"] };
34
+ export default defineConfig({ entry: ["src/index.ts"] });
35
35
  ```
36
36
 
37
- The base in `knip-base.json` reports only unreferenced files.
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 14 gate(s) failed: checks-comment-gate
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` is the gate that fails a pull request when any mutant regresses, rather than judging an absolute score.
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` for its first month, then drops the flag so it blocks.
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 also runs that workflow on a nightly schedule.
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
- A scheduled run costs a full Stryker run.
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
- Knip configurations do not extend a package file, so the consumer configuration imports the kit's base and spreads it:
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 base from "@avi2dg/checks/knip-base.json";
24
+ import { defineConfig } from "@avi2dg/checks/knip";
25
25
 
26
- export default { ...base, entry: ["src/index.ts", "tests/**/*.test.ts"] };
26
+ export default defineConfig({ entry: ["src/index.ts"] });
27
27
  ```
28
28
 
29
- The base in `knip-base.json` reports only unreferenced files.
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": ["typescript", "oxc", "eslint", "import"],
3
- "jsPlugins": ["./dist/effect-channel/index.js", "./dist/readability/index.js", "./dist/data-shape/index.js"],
4
- "categories": { "correctness": "error", "suspicious": "error" },
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": ["error", { "ts-expect-error": true }],
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": ["error", { "allowConstantLoopConditions": true }],
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": ["error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_", "ignoreRestSiblings": true }],
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": ["**/*.ts", "**/*.tsx"],
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": ["**/*.ts", "**/*.tsx"],
31
- "excludeFiles": ["tests/**"],
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": ["**/*.astro"],
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.36.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 --type-aware && bun src/core/lint.ts && depcruise --config .dependency-cruiser.cjs .",
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-base.json, from a node_modules the checkout lacks.
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);
@@ -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 one extending the kit's knip-base.json`);
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);
@@ -0,0 +1,3 @@
1
+ import { defineConfig } from "../quality/presets/dependency-cruiser.ts";
2
+
3
+ export default defineConfig();