@avi2dg/checks 0.56.0 → 0.57.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/src/core/lint.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  import { Config, Console, Effect, FileSystem, Option, Path, Schema } from "effect";
3
3
  import { ChildProcess, ChildProcessSpawner } from "effect/process";
4
- import { EVERY_REPOSITORY, KIT_GATES, type KitGate } from "./gates.ts";
4
+ import { EVERY_REPOSITORY, NIGHT_GATES, PR_GATES, WHOLE_TREE, type KitGate } from "./gates.ts";
5
5
  import { changedPaths, defaultBranch, git } from "./git.ts";
6
6
  import { runMain, Usage } from "./main.ts";
7
7
  import { FAST_TREE_GATES, selectScope } from "./scope.ts";
@@ -22,7 +22,8 @@ class GatesUndecided extends Schema.TaggedError<GatesUndecided>()("GatesUndecide
22
22
  }) {}
23
23
 
24
24
  const NAME = "checks-lint";
25
- const USAGE = "usage: lint.ts [--full | --desk] [<base-ref> <head-ref>]";
25
+ const USAGE = "usage: lint.ts [--full | --desk] [<base-ref> <head-ref>] | --night";
26
+ const NIGHT = "--night";
26
27
  const PULL_REQUEST_EVENT = "pull_request";
27
28
  const SHALLOW_HINT = "a CI checkout needs actions/checkout fetch-depth: 0";
28
29
 
@@ -159,11 +160,16 @@ function outcomeOf(exitCode: number): Outcome {
159
160
  return exitCode === 1 ? "violated" : "undecided";
160
161
  }
161
162
 
162
- const runGate = Effect.fn("runGate")(function* (gate: KitGate, range: Range) {
163
+ const runGate = Effect.fn("runGate")(function* (gate: KitGate, range: Range, wholeTree: boolean) {
163
164
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
164
165
  const script = (yield* Path.Path).join(import.meta.dir, "..", gate.vector, gate.file);
165
166
  const program = gate.file.endsWith(".sh") ? "sh" : process.execPath;
166
- const args = gate.reads === "range" ? [script, ...range.refs] : [script, ...(gate.args ?? [])];
167
+ const args =
168
+ gate.reads === "range"
169
+ ? wholeTree
170
+ ? [script, WHOLE_TREE, ...range.refs]
171
+ : [script, ...range.refs]
172
+ : [script, ...(gate.args ?? [])];
167
173
  const extra = gate.env === undefined ? {} : { env: gate.env, extendEnv: true };
168
174
  const exitCode = yield* spawner
169
175
  .exitCode(ChildProcess.make(program, args, { stdin: "ignore", stdout: "inherit", stderr: "inherit", ...extra }))
@@ -175,6 +181,11 @@ const runGate = Effect.fn("runGate")(function* (gate: KitGate, range: Range) {
175
181
  return { gate, outcome: outcomeOf(exitCode) };
176
182
  });
177
183
 
184
+ const wholeTreeRange = Effect.fn("wholeTreeRange")(function* () {
185
+ const head = yield* commitOf("HEAD");
186
+ return { refs: [head], source: "the whole tree at HEAD" } satisfies Range;
187
+ });
188
+
178
189
  const scopedGates = Effect.fn("scopedGates")(function* (range: Range, root: string) {
179
190
  if (range.refs.length === 1) return { bins: undefined, detail: "the full run, since a lone tip scopes nothing" };
180
191
  const [base, head] = range.refs;
@@ -187,39 +198,62 @@ const scopedGates = Effect.fn("scopedGates")(function* (range: Range, root: stri
187
198
  return { bins: scope.gates, detail: "" };
188
199
  });
189
200
 
201
+ const pullRequestSelection = Effect.fn("pullRequestSelection")(function* (
202
+ split: { readonly mode: LintMode; readonly rest: readonly string[] },
203
+ range: Range,
204
+ root: string,
205
+ applicable: readonly KitGate[],
206
+ ) {
207
+ if (split.mode === "desk") {
208
+ return {
209
+ bins: applicable.filter((gate) => FAST_TREE_GATES.includes(gate.bin)).map((gate) => gate.bin),
210
+ detail: "the desk run",
211
+ };
212
+ }
213
+ if (split.mode === "full") {
214
+ return { bins: applicable.map((gate) => gate.bin), detail: "the full run (--full)" };
215
+ }
216
+ return yield* scopedGates(range, root);
217
+ });
218
+
219
+ function selectionDetail(night: boolean, mode: LintMode, scopeDetail: string, count: number): string | undefined {
220
+ if (night) return undefined;
221
+ if (mode === "scoped" && scopeDetail === "") return `${count} gate(s) for this change`;
222
+ return scopeDetail === "" ? undefined : scopeDetail;
223
+ }
224
+
190
225
  const lint = Effect.gen(function* () {
191
- const split = splitMode(process.argv.slice(2));
226
+ const raw = process.argv.slice(2);
227
+ const night = raw.includes(NIGHT);
228
+ if (night && raw.length > 1) return yield* new Usage({ message: USAGE });
229
+ const split = night ? { mode: "full" as const, rest: [] as readonly string[] } : splitMode(raw);
192
230
  if (split === undefined) return yield* new Usage({ message: USAGE });
193
231
  const root = (yield* git(["rev-parse", "--show-toplevel"])).trim();
194
- const range = yield* resolveRange(split.rest);
232
+ const range = night ? yield* wholeTreeRange() : yield* resolveRange(split.rest);
195
233
  yield* Console.log(`${NAME}: ${describe(range)} from ${range.source}`);
196
- const applicable = yield* Effect.filter(KIT_GATES, (gate) =>
234
+ const applicable = yield* Effect.filter(night ? NIGHT_GATES : PR_GATES, (gate) =>
197
235
  gate.appliesTo === EVERY_REPOSITORY
198
236
  ? Effect.succeed(true)
199
237
  : git(["ls-files", "--", ...gate.appliesTo.pathspecs], root).pipe(Effect.map((files) => files.trim() !== "")),
200
238
  );
201
- const scoped = split.mode === "desk"
202
- ? {
203
- bins: applicable.filter((gate) => FAST_TREE_GATES.includes(gate.bin)).map((gate) => gate.bin),
204
- detail: "the desk run",
205
- }
206
- : split.mode === "full"
207
- ? { bins: applicable.map((gate) => gate.bin), detail: "the full run (--full)" }
208
- : yield* scopedGates(range, root);
239
+ const scoped = night
240
+ ? { bins: applicable.map((gate) => gate.bin) as readonly string[] | undefined, detail: "" }
241
+ : yield* pullRequestSelection(split, range, root, applicable);
209
242
  const bins = scoped.bins;
210
243
  const gates = bins === undefined ? applicable : applicable.filter((gate) => bins.includes(gate.bin));
211
- const detail = split.mode === "scoped" && bins !== undefined ? `${gates.length} gate(s) for this change` : scoped.detail;
212
- yield* Console.log(`${NAME}: ${detail}`);
213
- const verdicts = yield* Effect.forEach(gates, (gate) => runGate(gate, range));
244
+ const detail = selectionDetail(night, split.mode, scoped.detail, gates.length);
245
+ if (detail !== undefined) yield* Console.log(`${NAME}: ${detail}`);
246
+ const verdicts = yield* Effect.forEach(gates, (gate) => runGate(gate, range, night));
214
247
  const failed = verdicts.filter((verdict) => verdict.outcome !== "passed");
215
248
  if (failed.length === 0) {
216
249
  yield* Console.log(`${NAME}: ${gates.length} gate(s) pass`);
217
250
  return true;
218
251
  }
219
- const summary = `${failed.length} of ${gates.length} gate(s) failed: ${failed.map((verdict) => verdict.gate.bin).join(", ")}`;
252
+ const names = failed.map((verdict) => verdict.gate.bin).join(", ");
220
253
  if (failed.every((verdict) => verdict.outcome === "undecided")) {
221
- return yield* new GatesUndecided({ message: summary });
254
+ return yield* new GatesUndecided({ message: `${failed.length} of ${gates.length} gate(s) undecided: ${names}` });
222
255
  }
256
+ const summary = `${failed.length} of ${gates.length} gate(s) failed: ${names}`;
223
257
  yield* Console.error(`${NAME}: ${summary}`);
224
258
  return false;
225
259
  });
package/src/core/main.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { BunRuntime, BunServices } from "@effect/platform-bun";
2
2
  import { Cause, Console, Effect, Exit, Schema } from "effect";
3
3
 
4
- const PASSED = 0;
5
- const VIOLATED = 1;
6
- const UNDECIDED = 2;
4
+ export const PASSED = 0;
5
+ export const VIOLATED = 1;
6
+ export const UNDECIDED = 2;
7
7
  const INTERRUPTED = 130;
8
8
 
9
9
  export class Usage extends Schema.TaggedError<Usage>()("Usage", {
@@ -19,12 +19,16 @@ export function runMain<E extends { readonly message: string }>(
19
19
  name: string,
20
20
  passes: Effect.Effect<boolean, E, BunServices.BunServices>,
21
21
  ): void {
22
- const program = passes.pipe(
23
- Effect.map((passed) => (passed ? PASSED : VIOLATED)),
24
- Effect.catch((failure) => Console.error(`${name}: ${failure.message}`).pipe(Effect.as(UNDECIDED))),
25
- Effect.provide(BunServices.layer),
22
+ runExitMain(
23
+ passes.pipe(
24
+ Effect.map((passed) => (passed ? PASSED : VIOLATED)),
25
+ Effect.catch((failure) => Console.error(`${name}: ${failure.message}`).pipe(Effect.as(UNDECIDED))),
26
+ ),
26
27
  );
27
- BunRuntime.runMain(program, { teardown: (exit, onExit) => onExit(statusOf(exit)) });
28
+ }
29
+
30
+ export function runExitMain(exits: Effect.Effect<number, never, BunServices.BunServices>): void {
31
+ BunRuntime.runMain(exits.pipe(Effect.provide(BunServices.layer)), { teardown: (exit, onExit) => onExit(statusOf(exit)) });
28
32
  }
29
33
 
30
34
  export function runTreeGate<Outcome extends { readonly violations: readonly unknown[] }>(
@@ -19,7 +19,8 @@ export type PreviousNight = { readonly url: string; readonly record: NightRecord
19
19
  export type Finding =
20
20
  | { readonly kind: "mutation"; readonly file: string; readonly before: Score; readonly after: Score; readonly since: string }
21
21
  | { readonly kind: "test"; readonly file: string; readonly name: string }
22
- | { readonly kind: "crash"; readonly file: string };
22
+ | { readonly kind: "crash"; readonly file: string }
23
+ | { readonly kind: "gates"; readonly file: string };
23
24
 
24
25
  function scoreOf(file: ReportFile): Score {
25
26
  let detected = 0;
@@ -45,6 +46,7 @@ function fell(before: Score, after: Score): boolean {
45
46
  export function keyOf(finding: Finding): string {
46
47
  if (finding.kind === "mutation") return `mutation:${finding.file}`;
47
48
  if (finding.kind === "test") return `test:${finding.file}${NAME_SEPARATOR}${finding.name}`;
49
+ if (finding.kind === "gates") return `gates:${finding.file}`;
48
50
  return `test:${finding.file}`;
49
51
  }
50
52
 
@@ -65,8 +67,23 @@ function testFindings(reports: readonly TestReport[]): readonly Finding[] {
65
67
  return [...new Map(failing.map((finding) => [keyOf(finding), finding])).values()];
66
68
  }
67
69
 
68
- export function findingsOf(previous: Option.Option<PreviousNight>, tonight: Tonight): readonly Finding[] {
69
- return [...scoreFindings(previous, tonight.scores), ...testFindings(tonight.tests)].sort((one, other) => keyOf(one).localeCompare(keyOf(other)));
70
+ export function findingsOf(previous: Option.Option<PreviousNight>, tonight: Tonight, gates: readonly Finding[] = []): readonly Finding[] {
71
+ return [...scoreFindings(previous, tonight.scores), ...testFindings(tonight.tests), ...gates].sort((one, other) =>
72
+ keyOf(one).localeCompare(keyOf(other)),
73
+ );
74
+ }
75
+
76
+ const FAILED_GATES = /of \d+ gate\(s\) failed: (.+)/;
77
+ const PASSED_GATES = /gate\(s\) pass/;
78
+
79
+ export function gatesOf(log: string): readonly Finding[] {
80
+ const names = log
81
+ .split("\n")
82
+ .flatMap((line) => FAILED_GATES.exec(line)?.[1]?.split(",") ?? [])
83
+ .map((name) => name.trim())
84
+ .filter((name) => name !== "");
85
+ if (names.length > 0) return [...new Map(names.map((file): [string, Finding] => [file, { kind: "gates", file }])).values()];
86
+ return PASSED_GATES.test(log) ? [] : [{ kind: "gates", file: "checks-lint" }];
70
87
  }
71
88
 
72
89
  export function percent(score: Score): string {
@@ -5,11 +5,11 @@ import { DEFAULT_BRANCH } from "../core/gates.ts";
5
5
  import { runMain, Usage } from "../core/main.ts";
6
6
  import { gh, type GitHubFailure, gitHubJson, REPOSITORY } from "../delivery/github.ts";
7
7
  import { parseReport as parseMutationReport } from "./mutation-compare.ts";
8
- import { type Finding, findingsOf, keyOf, NightRecordSchema, type NightRecord, percent, type PreviousNight, scoresOf, type TestReport } from "./night-run-findings.ts";
8
+ import { type Finding, findingsOf, gatesOf, keyOf, NightRecordSchema, type NightRecord, percent, type PreviousNight, scoresOf, type TestReport } from "./night-run-findings.ts";
9
9
  import { parseReport as parseTestReport } from "./test-report.ts";
10
10
 
11
11
  const NAME = "checks-night-run";
12
- const USAGE = `usage: ${NAME} [--mutation <mutation-report>] [--tests <junit-report>]...`;
12
+ const USAGE = `usage: ${NAME} [--mutation <mutation-report>] [--tests <junit-report>]... [--gates <lint-log>]`;
13
13
  export const LABEL = "night-run";
14
14
  const LABEL_COLOR = "5319e7";
15
15
  const LABEL_DESCRIPTION = "A regression a night run on main found";
@@ -23,19 +23,22 @@ class NightRunFailure extends Schema.TaggedError<NightRunFailure>()("NightRunFai
23
23
  message: Schema.String,
24
24
  }) {}
25
25
 
26
- export type Options = { readonly mutation: Option.Option<string>; readonly tests: readonly string[] };
26
+ export type Options = { readonly mutation: Option.Option<string>; readonly tests: readonly string[]; readonly gates: Option.Option<string> };
27
27
 
28
28
  export const parseArgs = Effect.fnUntraced(function* (argv: readonly string[]): Effect.fn.Return<Options, Usage> {
29
29
  const mutation: string[] = [];
30
30
  const tests: string[] = [];
31
+ const gates: string[] = [];
31
32
  for (let at = 0; at < argv.length; at += 2) {
32
33
  const [flag, value] = [argv[at], argv[at + 1]];
33
- const into = flag === "--mutation" ? mutation : flag === "--tests" ? tests : undefined;
34
+ const into = flag === "--mutation" ? mutation : flag === "--tests" ? tests : flag === "--gates" ? gates : undefined;
34
35
  if (into === undefined || value === undefined || value.startsWith("--")) return yield* new Usage({ message: USAGE });
35
36
  into.push(value);
36
37
  }
37
- if (mutation.length > 1 || mutation.length + tests.length === 0) return yield* new Usage({ message: USAGE });
38
- return { mutation: Option.fromNullishOr(mutation[0]), tests };
38
+ if (mutation.length > 1 || gates.length > 1 || mutation.length + tests.length + gates.length === 0) {
39
+ return yield* new Usage({ message: USAGE });
40
+ }
41
+ return { mutation: Option.fromNullishOr(mutation[0]), tests, gates: Option.fromNullishOr(gates[0]) };
39
42
  });
40
43
 
41
44
  export type OpenIssue = { readonly number: number; readonly key: string };
@@ -102,6 +105,7 @@ const openIssues = Effect.gen(function* () {
102
105
  function fullTitleOf(finding: Finding): string {
103
106
  if (finding.kind === "mutation") return `Night run: mutation score fell in ${finding.file}`;
104
107
  if (finding.kind === "test") return `Night run: ${finding.file} > ${finding.name} fails`;
108
+ if (finding.kind === "gates") return `Night run: nightly gate ${finding.file} failed`;
105
109
  return `Night run: ${finding.file} crashed its test worker`;
106
110
  }
107
111
 
@@ -125,6 +129,7 @@ export function findingText(finding: Finding, run: string): string {
125
129
  ].join("\n");
126
130
  }
127
131
  if (finding.kind === "test") return `The night run ${run} failed \`${finding.name}\` in \`${finding.file}\`.`;
132
+ if (finding.kind === "gates") return `The night run ${run} failed nightly gate \`${finding.file}\`.`;
128
133
  return `The night run ${run} lost every result of \`${finding.file}\`, because the worker running it crashed.`;
129
134
  }
130
135
 
@@ -240,8 +245,9 @@ const nightRun = Effect.gen(function* () {
240
245
  const actions = yield* actionsRun;
241
246
  const scores = Option.isSome(options.mutation) ? Option.some(yield* tonightScores(options.mutation.value)) : Option.none<NightRecord["scores"]>();
242
247
  const tests: TestReport[] = yield* Effect.forEach(options.tests, testReport);
248
+ const gates: readonly Finding[] = Option.isSome(options.gates) ? gatesOf(yield* readText(options.gates.value)) : [];
243
249
  const previous = Option.isSome(scores) ? yield* previousNight(actions) : Option.none<PreviousNight>();
244
- const findings = findingsOf(previous, { scores: Option.getOrElse(scores, () => ({})), tests });
250
+ const findings = findingsOf(previous, { scores: Option.getOrElse(scores, () => ({})), tests }, gates);
245
251
  const filing: Filing = actions.ref === `refs/heads/${DEFAULT_BRANCH}` ? { onMain: true, filed: yield* fileFindings(findings, actions.run) } : { onMain: false, ref: actions.ref };
246
252
  if (filing.onMain && Option.isSome(scores)) yield* writeRecord({ scores: scores.value });
247
253
  yield* Console.log(report(actions.run, Option.map(scores, (tonight) => Object.keys(tonight).length), previous, findings, filing));
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env bun
2
2
  import { Console, Effect, Schema } from "effect";
3
3
  import { commitOf, git, isShallowBoundary, refArgs, writtenAt } from "../core/git.ts";
4
- import { runMain } from "../core/main.ts";
4
+ import { WHOLE_TREE } from "../core/gates.ts";
5
+ import { Usage, runMain } from "../core/main.ts";
5
6
  import { TEST_FILE } from "./test-layout.ts";
6
7
 
7
8
  export const QUARANTINE = "tests/quarantine/";
@@ -12,7 +13,7 @@ export class QuarantineError extends Schema.TaggedError<QuarantineError>()("Quar
12
13
  message: Schema.String,
13
14
  }) {}
14
15
 
15
- const USAGE = "usage: quarantine-clock.ts <ref> | <base-ref> <head-ref>";
16
+ const USAGE = "usage: quarantine-clock.ts <ref> | <base-ref> <head-ref> | --whole-tree <head-ref>";
16
17
 
17
18
  export type Entry =
18
19
  | { readonly kind: "entered"; readonly sha: string; readonly at: number; readonly day: string }
@@ -133,6 +134,13 @@ export const runHead = Effect.fn("runHead")(function* (root: string, head: strin
133
134
 
134
135
  const clock = Effect.gen(function* () {
135
136
  const { first, second } = yield* refArgs(process.argv.slice(2), USAGE);
137
+ if (first === WHOLE_TREE) {
138
+ if (second === undefined) return yield* new Usage({ message: USAGE });
139
+ const root = (yield* git(["rev-parse", "--show-toplevel"])).trim();
140
+ const result = yield* runHead(root, yield* commitOf(second));
141
+ yield* Console.log(report(result));
142
+ return result.overdue.length === 0;
143
+ }
136
144
  if (second !== undefined) yield* commitOf(first);
137
145
  const head = yield* commitOf(second ?? first);
138
146
  const root = (yield* git(["rev-parse", "--show-toplevel"])).trim();
@@ -1,104 +0,0 @@
1
- ---
2
- kind: reference
3
- audience: consumers
4
- ---
5
- # checks-exports
6
-
7
- `checks-exports` is the gate that refuses an exported value or type nothing imports.
8
-
9
- ## What it checks
10
-
11
- It runs Knip with the repository's own configuration and names each exported value and type no file imports.
12
- It reads only the exports and types issue types, so an unreferenced file or an unused dependency never fails it.
13
- It asks Knip for those issue types itself, so a configuration that narrows `include` or excludes them still has its exports judged.
14
- A configuration that turns the exports or types rules off silences those symbols, so keep both rules on.
15
- It holds each reported symbol against `exports-baseline.json`, and fails on any symbol the baseline does not hold.
16
- It fails on any baseline entry the range adds unless Knip reports that same symbol unused at the base, so the baseline records debt that already existed and never debt the range creates.
17
- It fails on any baseline entry Knip no longer reports, so the baseline only shrinks.
18
- It fails when the repository tracks no TypeScript source, since an empty scan would pass without judging anything.
19
- It fails when the repository holds no Knip configuration, since Knip's default entries cannot tell a dead export from a public one.
20
-
21
- ## What it reads
22
-
23
- It runs Knip over the working tree, so an uncommitted file is judged like a committed one.
24
- It reads the repository's Knip configuration, which names the entries.
25
- It reads `exports-baseline.json` in the repository root, which holds each accepted unused export as a file, kind and name triple.
26
- It reads `exports-baseline.json` again at the base of the range, where a commit without the file counts as empty.
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 builder loads there too.
29
- The repository writes `knip.config.ts` with the kit's builder, which requires `entry`, the files nothing imports:
30
-
31
- ```ts
32
- import { defineConfig } from "@avi2dg/checks/knip";
33
-
34
- export default defineConfig({ entry: ["src/index.ts"] });
35
- ```
36
-
37
- The builder sets `include` to unreferenced files alone, and adds `tests/**/*.test.ts`, `dependency-cruiser.config.ts` and `oxlint.config.ts` to the entries.
38
- The gate resolves the Knip binary from the installed kit, so a consumer installs nothing beyond the kit.
39
-
40
- ## Arguments
41
-
42
- ```sh
43
- checks-exports <base-ref> <head-ref>
44
- checks-exports <ref>
45
- checks-exports --write
46
- ```
47
-
48
- With two arguments the base is where the head branched off, at their merge-base.
49
- When the checked-out commit is a merge of the head ref and one other commit, as a pull request's default checkout is, the base is that other commit instead.
50
- So under `checks-lint` a pull request's merge checkout is judged against the base branch tip it merges, and a symbol that branch already baselined does not count as added.
51
- With one the base is that commit's parent, or the empty tree for a repository's first commit, where nothing counts as already unused.
52
- With `--write` it records every unused export and type Knip reports into `exports-baseline.json`, the way `oxlint --suppress-all` seeds `oxlint-suppressions.json`.
53
-
54
- ## Seeding a baseline
55
-
56
- A repository adopting the gate runs `checks-exports --write` on a branch that changes no source and commits the file it writes.
57
- The gate passes that range, since Knip reports every seeded symbol unused at the base too.
58
- A seed written after the range adds a dead export, or after it removes the last import of one, fails on that symbol.
59
-
60
- ## Exit codes
61
-
62
- | Code | When |
63
- | --- | --- |
64
- | 0 | no reported symbol is unlisted, no baseline entry is added or stale, or `--write` recorded the baseline |
65
- | 1 | a reported symbol is unlisted, a baseline entry is added or stale or the repository holds no Knip configuration |
66
- | 2 | a ref does not resolve, the repository tracks no TypeScript source, Knip cannot run, or a report or baseline does not parse |
67
-
68
- ## Sample output
69
-
70
- ```
71
- exports: 2 unused export(s) not in exports-baseline.json:
72
- src/used.ts: unusedExport (export)
73
- src/used.ts: UnusedOptions (type)
74
- ```
75
-
76
- A baseline entry the range adds fails when Knip does not report its symbol unused at the base:
77
-
78
- ```
79
- exports: 1 exports-baseline.json export(s) the range adds that its base did not leave unused, remove the export instead:
80
- src/used.ts: unusedExport (export)
81
- ```
82
-
83
- A passing run with a held baseline counts what it holds:
84
-
85
- ```
86
- exports: 2 unused export(s) in exports-baseline.json, and no new ones
87
- ```
88
-
89
- A passing run with an empty baseline says so:
90
-
91
- ```
92
- exports: no unused exports or types
93
- ```
94
-
95
- ## When it runs
96
-
97
- `checks-lint` runs it over each pull request's range when the repository tracks a `.ts` or `.tsx` file.
98
- Such a repository names its entries in a Knip configuration.
99
-
100
- ## Related topics
101
-
102
- - [checks-lint](checks-lint.md)
103
- - [checks-suppressions-ratchet](checks-suppressions-ratchet.md)
104
- - [checks-unused](checks-unused.md)
@@ -1,64 +0,0 @@
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 in the full run when the repository tracks a `.ts` or `.tsx` file, so a repository with no config of its own still has its imports judged, and never in the desk run.
60
-
61
- ## Related topics
62
-
63
- - [The dependency rules](../configs/dependency-rules.md)
64
- - [checks-lint](checks-lint.md)
@@ -1,66 +0,0 @@
1
- ---
2
- kind: reference
3
- audience: consumers
4
- ---
5
- # checks-unused
6
-
7
- `checks-unused` is the gate that refuses a TypeScript or Astro file no entry point reaches.
8
-
9
- ## What it checks
10
-
11
- It runs Knip with the repository's own configuration and names each `.ts`, `.tsx` or `.astro` file no entry reaches.
12
- It reads only the files issue type, so an unused export or dependency never fails it.
13
- It asks Knip for that issue type itself, so a configuration that narrows `include`, excludes files or turns the files rule off still has its files judged.
14
- It fails when the repository tracks no `.ts`, `.tsx` or `.astro` file, since an empty scan would pass without judging anything.
15
- It fails when the repository holds no Knip configuration, since Knip's default entries cannot tell a dead file from an entry point.
16
-
17
- ## What it reads
18
-
19
- It reads the working tree, so an uncommitted file is judged like a committed one.
20
- It reads the repository's Knip configuration, which names the entries.
21
- The repository writes `knip.config.ts` with the kit's builder, which requires `entry`, the files nothing imports:
22
-
23
- ```ts
24
- import { defineConfig } from "@avi2dg/checks/knip";
25
-
26
- export default defineConfig({ entry: ["src/index.ts"] });
27
- ```
28
-
29
- The builder sets `include` to unreferenced files alone, and adds `tests/**/*.test.ts`, `dependency-cruiser.config.ts` and `oxlint.config.ts` to the entries.
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
- The gate resolves the Knip binary from the installed kit, so a consumer installs nothing beyond the kit.
32
-
33
- ## Arguments
34
-
35
- It takes none.
36
-
37
- ## Exit codes
38
-
39
- | Code | When |
40
- | --- | --- |
41
- | 0 | no tracked file is unreferenced |
42
- | 1 | a tracked file is unreferenced or the repository holds no Knip configuration |
43
- | 2 | the repository tracks no TypeScript or Astro source, Knip cannot run, or its configuration does not parse |
44
-
45
- ## Sample output
46
-
47
- ```
48
- unused: 1 unreferenced file(s):
49
- src/planted-dead.ts
50
- ```
51
-
52
- A passing run counts the files it judged:
53
-
54
- ```
55
- unused: no unreferenced files among 110 tracked .ts/.tsx/.astro file(s)
56
- ```
57
-
58
- ## When it runs
59
-
60
- `checks-lint` runs it in the full run when the repository tracks a `.ts`, `.tsx` or `.astro` file, and never in the desk run.
61
- Such a repository names its entries in a Knip configuration.
62
-
63
- ## Related topics
64
-
65
- - [checks-lint](checks-lint.md)
66
- - [checks-repetition](checks-repetition.md)