@avi2dg/checks 0.30.0 → 0.31.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 CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  Every release of `@avi2dg/checks`, newest first, written by the release from its conventional commits.
4
4
 
5
+ ## 0.31.0
6
+
7
+ Released 2026-09-28.
8
+
9
+ ### Features
10
+
11
+ - lint .astro files in lint coverage, unused and a thin-frontmatter rule [#108](https://github.com/avi2d/checks/pull/108)
12
+
5
13
  ## 0.30.0
6
14
 
7
15
  Released 2026-09-28.
package/README.md CHANGED
@@ -118,9 +118,9 @@ The table groups the gates by vector, the part of a repository each one judges.
118
118
  | --- | --- | --- | --- |
119
119
  | complexity | [`checks-suppressions-ratchet`](docs/gates/checks-suppressions-ratchet.md) | the range | every repository |
120
120
  | complexity | [`checks-repetition`](docs/gates/checks-repetition.md) | the range | a repository tracking `*.ts` or `*.tsx` |
121
- | complexity | [`checks-unused`](docs/gates/checks-unused.md) | the working tree | a repository tracking `*.ts` or `*.tsx` |
121
+ | complexity | [`checks-unused`](docs/gates/checks-unused.md) | the working tree | a repository tracking `*.ts` or `*.tsx` or `*.astro` |
122
122
  | complexity | [`checks-exports`](docs/gates/checks-exports.md) | the range | a repository tracking `*.ts` or `*.tsx` |
123
- | quality | [`checks-lint-coverage`](docs/gates/checks-lint-coverage.md) | the working tree | a repository tracking `*.ts` or `*.tsx` |
123
+ | quality | [`checks-lint-coverage`](docs/gates/checks-lint-coverage.md) | the working tree | a repository tracking `*.ts` or `*.tsx` or `*.astro` |
124
124
  | quality | [`checks-comment-gate`](docs/gates/checks-comment-gate.md) | the range | every repository |
125
125
  | testing | [`checks-test-layout`](docs/gates/checks-test-layout.md) | the working tree | a repository tracking `*.ts` or `*.tsx` |
126
126
  | testing | [`checks-quarantine-clock`](docs/gates/checks-quarantine-clock.md) | the range | every repository |
@@ -416,11 +416,104 @@ var rule = {
416
416
  };
417
417
  var cognitive_complexity_default = rule;
418
418
 
419
+ // src/complexity/readability/thin-astro.ts
420
+ function unwrapped(expression) {
421
+ let current = expression;
422
+ while (current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSNonNullExpression" || current.type === "TSTypeAssertion") {
423
+ current = current.expression;
424
+ }
425
+ return current;
426
+ }
427
+ function isAstroProps(value) {
428
+ if (value.type !== "MemberExpression" || value.computed)
429
+ return false;
430
+ const { object, property } = value;
431
+ return object.type === "Identifier" && object.name === "Astro" && property.type === "Identifier" && property.name === "props";
432
+ }
433
+ function isPlainValue(value, bound) {
434
+ return value.type === "Literal" || value.type === "Identifier" && bound.has(value.name);
435
+ }
436
+ function readsProps(expression, bound) {
437
+ const value = unwrapped(expression);
438
+ if (value.type !== "MemberExpression")
439
+ return value.type === "Identifier" && bound.has(value.name);
440
+ if (value.computed && !isPlainValue(value.property, bound))
441
+ return false;
442
+ return isAstroProps(value) || readsProps(value.object, bound);
443
+ }
444
+ function isPlainPattern(pattern, bound) {
445
+ if (pattern === null)
446
+ return true;
447
+ if (pattern.type === "RestElement")
448
+ return isPlainPattern(pattern.argument, bound);
449
+ if (pattern.type === "AssignmentPattern")
450
+ return isPlainValue(pattern.right, bound) && isPlainPattern(pattern.left, bound);
451
+ if (pattern.type === "ArrayPattern")
452
+ return pattern.elements.every((element) => isPlainPattern(element, bound));
453
+ if (pattern.type === "Identifier")
454
+ return true;
455
+ return pattern.properties.every((property) => property.type === "RestElement" ? isPlainPattern(property.argument, bound) : (!property.computed || isPlainValue(property.key, bound)) && isPlainPattern(property.value, bound));
456
+ }
457
+ function isPropsRead(declarator, bound) {
458
+ return declarator.init !== null && readsProps(declarator.init, bound) && isPlainPattern(declarator.id, bound);
459
+ }
460
+ function boundName(pattern, found) {
461
+ if (pattern === null)
462
+ return;
463
+ if (pattern.type === "RestElement")
464
+ boundName(pattern.argument, found);
465
+ else if (pattern.type === "AssignmentPattern")
466
+ boundName(pattern.left, found);
467
+ else if (pattern.type === "ObjectPattern") {
468
+ for (const property of pattern.properties)
469
+ boundName(property.type === "Property" ? property.value : property.argument, found);
470
+ } else if (pattern.type === "ArrayPattern") {
471
+ for (const element of pattern.elements)
472
+ boundName(element, found);
473
+ } else
474
+ found.add(pattern.name);
475
+ }
476
+ function isTypeDeclaration(statement) {
477
+ const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
478
+ return declaration?.type === "TSInterfaceDeclaration" || declaration?.type === "TSTypeAliasDeclaration";
479
+ }
480
+ var rule2 = {
481
+ meta: {
482
+ type: "problem",
483
+ docs: { description: 'Disallow logic in .astro frontmatter and script blocks: only imports, props and markup, with client code loaded by a side-effect import such as import "../client.ts"' }
484
+ },
485
+ create(context) {
486
+ if (!context.filename.endsWith(".astro"))
487
+ return {};
488
+ const bound = new Set;
489
+ return {
490
+ Program(node) {
491
+ for (const statement of node.body) {
492
+ if (statement.type === "ImportDeclaration" || statement.type === "EmptyStatement" || statement.type === "ExportNamedDeclaration" && statement.source !== null || isTypeDeclaration(statement)) {
493
+ continue;
494
+ }
495
+ if (statement.type === "VariableDeclaration" && statement.declarations.length > 0 && statement.declarations.every((declarator) => isPropsRead(declarator, bound))) {
496
+ for (const declarator of statement.declarations)
497
+ boundName(declarator.id, bound);
498
+ continue;
499
+ }
500
+ context.report({
501
+ node: statement,
502
+ message: 'an .astro frontmatter or script block holds more than imports and props: move this statement into a .ts file and import it, so the .astro file holds only imports, props and markup; a script block loads client code with a side-effect import such as import "../client.ts"'
503
+ });
504
+ }
505
+ }
506
+ };
507
+ }
508
+ };
509
+ var thin_astro_default = rule2;
510
+
419
511
  // src/complexity/readability/index.ts
420
512
  var plugin = {
421
513
  meta: { name: "readability" },
422
514
  rules: {
423
- "cognitive-complexity": cognitive_complexity_default
515
+ "cognitive-complexity": cognitive_complexity_default,
516
+ "thin-astro": thin_astro_default
424
517
  }
425
518
  };
426
519
  var readability_default = plugin;
@@ -43,6 +43,16 @@ The base loads the kit's `data-shape` plugin from `dist/` with one rule for ever
43
43
  - Derive such a type from the schema with `typeof Name.Type` instead of writing both.
44
44
  - The twin rule runs on production files only, so a test that declares its own schema as an oracle stays green.
45
45
 
46
+ ## Astro rules
47
+
48
+ An override in `oxlintrc.json` turns on one rule of the kit's `readability` plugin in each `.astro` file:
49
+
50
+ - `readability/thin-astro` refuses a statement in the frontmatter or a script block that is neither an import, a re-export from another module, a type or interface declaration, nor a variable read from `Astro.props`.
51
+ - A default inside an `Astro.props` destructuring passes only when it is a literal or a name read from `Astro.props` earlier, so `const { title = "Home" } = Astro.props;` passes and a call or `await` in a default is refused.
52
+ - Move a refused statement into a `.ts` file and import it, so the `.astro` file holds only imports, props and markup.
53
+ - A script block loads client code with a side-effect import, as in `<script>import "../client.ts";</script>`, and the override turns off `import/no-unassigned-import` so that import passes.
54
+ - A dynamic route re-exports `getStaticPaths` from a `.ts` file, as in `export { getStaticPaths } from "../lib/paths.ts";`.
55
+
46
56
  ## Rules outside tests
47
57
 
48
58
  An override in `oxlintrc.json` turns on these type-aware rules in each `.ts` and `.tsx` file outside `tests/`:
package/docs/design.md CHANGED
@@ -86,7 +86,7 @@ npm adds `package.json`, `README.md` and `LICENSE` whatever `files` says.
86
86
  `bun pm pack` builds the same tarball the registry serves, and the consumer e2e test installs that tarball.
87
87
 
88
88
  Each oxlint plugin ships compiled under `dist/`, because Node refuses to strip types from a `.ts` file under `node_modules`.
89
- `@oxlint/plugins` ships no RuleTester, so each `effect-channel`, `readability` and `data-shape` rule is proven red and green against an installed consumer in `tests/e2e/consumer.test.ts`.
89
+ `@oxlint/plugins` ships no RuleTester, so each `effect-channel`, `readability` and `data-shape` rule is proven red and green against an installed consumer in `tests/e2e/consumer.test.ts`, except `readability/thin-astro`, which `tests/e2e/astro-consumer.test.ts` proves against a consumer tree linked to the checkout.
90
90
  `dist/` is committed, with the doc templates in `dist/templates/`, and so is `CHANGELOG.md`, which the same build writes.
91
91
  No `prepack` or `prepublishOnly` script rebuilds them, so a publish ships the committed files.
92
92
  CI runs `git diff --exit-code` over the whole tree after `bun run build`.
@@ -109,7 +109,7 @@ So the three packages move together at one exact version.
109
109
  A gate then behaves the same alone or through `checks-lint`, and `lint-coverage.sh` can stay a shell script.
110
110
  The gates run one at a time and pass their output straight through, so each report reads whole and in the order of the gate table.
111
111
  `checks-lint` picks the gates that apply from the tracked files, and a repository cannot select gates.
112
- A TypeScript gate runs as soon as the repository tracks TypeScript source.
112
+ A TypeScript gate runs as soon as the repository tracks TypeScript source, and `checks-lint-coverage` and `checks-unused` also run on tracked Astro source.
113
113
 
114
114
  GitHub authors the pull request merge commit it builds as `GitHub <noreply@github.com>`, and `checks-commit-identity` refuses that author.
115
115
  So `checks-lint` ends a pull request's range at the event's head commit, and the merge commit is never in it.
@@ -4,12 +4,13 @@ audience: consumers
4
4
  ---
5
5
  # checks-lint-coverage
6
6
 
7
- `checks-lint-coverage` is the gate that fails when oxlint skips a tracked TypeScript file, or when the program `tsconfig.json` builds drops the ts-reset rules, without saying so.
7
+ `checks-lint-coverage` is the gate that fails when oxlint skips a tracked TypeScript or Astro file, or when the program `tsconfig.json` builds drops the ts-reset rules, without saying so.
8
8
 
9
9
  ## What it checks
10
10
 
11
- It fails when oxlint skips a tracked `.ts` or `.tsx` file, for example through a stray `.gitignore` entry.
11
+ It fails when oxlint skips a tracked `.ts`, `.tsx` or `.astro` file, for example through a stray `.gitignore` entry.
12
12
  It compares `git ls-files` against oxlint's own file walk and names the missing files.
13
+ It lists each tracked `.astro` file the same way, since oxlint lints the frontmatter and script blocks of an `.astro` file, where a script block loads client code with a side-effect import such as `import "../client.ts";`.
13
14
 
14
15
  It fails when the program `tsconfig.json` builds leaves out the `is-array` or the `json-parse` rule of `@total-typescript/ts-reset`, which `tsconfig.effect.json` lists.
15
16
  A `tsconfig.json` that does not extend `@avi2dg/checks/tsconfig.effect.json`, or that sets both `files` and `include`, leaves both rules out.
@@ -17,9 +18,9 @@ A `tsconfig.json` that does not extend `@avi2dg/checks/tsconfig.effect.json`, or
17
18
 
18
19
  ## What it reads
19
20
 
20
- It reads the working tree: the `*.ts` and `*.tsx` files `git ls-files` lists, and the files `oxlint --debug=files` walks.
21
+ It reads the working tree: the `*.ts`, `*.tsx` and `*.astro` files `git ls-files` lists, and the files `oxlint --debug=files` walks.
21
22
  It walks without naming a path, since an explicit path bypasses the ignore files whose skips it looks for.
22
- It reads the program from `tsc --listFilesOnly -p tsconfig.json`, and passes over the program when the root holds no `tsconfig.json`.
23
+ It reads the program from `tsc --listFilesOnly -p tsconfig.json`, and passes over the program when the repository tracks no `.ts` or `.tsx` file or the root holds no `tsconfig.json`.
23
24
  oxlint and tsc must be on `PATH`, as they are under a package script.
24
25
 
25
26
  ## Arguments
@@ -30,14 +31,14 @@ It takes none.
30
31
 
31
32
  | Code | When |
32
33
  | --- | --- |
33
- | 0 | the repository tracks no `.ts` or `.tsx` file, or oxlint walks each one and the program holds both ts-reset rules or the root holds no `tsconfig.json` |
34
+ | 0 | the repository tracks no `.ts`, `.tsx` or `.astro` file, or oxlint walks each one and the program holds both ts-reset rules, the repository tracks no `.ts` or `.tsx` file or the root holds no `tsconfig.json` |
34
35
  | 1 | oxlint skips a tracked file, or the program drops a ts-reset rule |
35
36
  | 2 | oxlint cannot walk the tree, or tsc cannot list the program after oxlint walks every tracked file, as when either is not on `PATH` or a config does not parse |
36
37
 
37
38
  ## Sample output
38
39
 
39
40
  ```
40
- lint-coverage: oxlint skips 1/3 tracked .ts/.tsx files; missing:
41
+ lint-coverage: oxlint skips 1/3 tracked .ts/.tsx/.astro files; missing:
41
42
  ignored/b.ts
42
43
  lint-coverage: the program tsconfig.json builds drops the ts-reset rules: is-array json-parse
43
44
  extend @avi2dg/checks/tsconfig.effect.json, and set files or include in tsconfig.json but not both
@@ -46,13 +47,13 @@ lint-coverage: the program tsconfig.json builds drops the ts-reset rules: is-arr
46
47
  A passing run counts the files and names the rules:
47
48
 
48
49
  ```
49
- lint-coverage: 71/71 tracked .ts/.tsx files
50
+ lint-coverage: 71/71 tracked .ts/.tsx/.astro files
50
51
  lint-coverage: the program tsconfig.json builds holds the ts-reset rules is-array and json-parse
51
52
  ```
52
53
 
53
54
  ## When it runs
54
55
 
55
- `checks-lint` runs it when the repository tracks a `.ts` or `.tsx` file.
56
+ `checks-lint` runs it when the repository tracks a `.ts`, `.tsx` or `.astro` file.
56
57
 
57
58
  ## Related topics
58
59
 
@@ -10,6 +10,7 @@ audience: consumers
10
10
 
11
11
  It runs the gates under [What runs](../../README.md#what-runs), each in its own process.
12
12
  Gates requiring tracked TypeScript files begin running when the repository tracks TypeScript.
13
+ `checks-lint-coverage` and `checks-unused` also begin running when the repository tracks an `.astro` file.
13
14
  `checks-advisories` begins running when the repository tracks `bun.lock`.
14
15
  All other gates run for every repository.
15
16
 
@@ -53,7 +54,7 @@ checks-lint: 1 of 12 gate(s) failed: checks-comment-gate
53
54
  ## When it runs
54
55
 
55
56
  A repository runs it from `bun run lint` in a pull request workflow that fetches the whole git history.
56
- It leaves out the TypeScript gates while the repository tracks no TypeScript file.
57
+ It leaves out the TypeScript gates while the repository tracks no TypeScript file, except that `checks-lint-coverage` and `checks-unused` run when it tracks an `.astro` file.
57
58
 
58
59
  ## Related topics
59
60
 
@@ -4,14 +4,14 @@ audience: consumers
4
4
  ---
5
5
  # checks-unused
6
6
 
7
- `checks-unused` is the gate that refuses a TypeScript file no entry point reaches.
7
+ `checks-unused` is the gate that refuses a TypeScript or Astro file no entry point reaches.
8
8
 
9
9
  ## What it checks
10
10
 
11
- It runs Knip with the repository's own configuration and names each `.ts` or `.tsx` file no entry reaches.
11
+ It runs Knip with the repository's own configuration and names each `.ts`, `.tsx` or `.astro` file no entry reaches.
12
12
  It reads only the files issue type, so an unused export or dependency never fails it.
13
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 TypeScript source, since an empty scan would pass without judging anything.
14
+ It fails when the repository tracks no `.ts`, `.tsx` or `.astro` file, since an empty scan would pass without judging anything.
15
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
16
 
17
17
  ## What it reads
@@ -27,6 +27,7 @@ export default { ...base, entry: ["src/index.ts", "tests/**/*.test.ts"] };
27
27
  ```
28
28
 
29
29
  The base in `knip-base.json` reports only unreferenced files.
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.
30
31
  The gate resolves the Knip binary from the installed kit, so a consumer installs nothing beyond the kit.
31
32
 
32
33
  ## Arguments
@@ -39,7 +40,7 @@ It takes none.
39
40
  | --- | --- |
40
41
  | 0 | no tracked file is unreferenced |
41
42
  | 1 | a tracked file is unreferenced or the repository holds no Knip configuration |
42
- | 2 | the repository tracks no TypeScript source, Knip cannot run, or its configuration does not parse |
43
+ | 2 | the repository tracks no TypeScript or Astro source, Knip cannot run, or its configuration does not parse |
43
44
 
44
45
  ## Sample output
45
46
 
@@ -51,12 +52,12 @@ unused: 1 unreferenced file(s):
51
52
  A passing run counts the files it judged:
52
53
 
53
54
  ```
54
- unused: no unreferenced files among 110 tracked .ts/.tsx file(s)
55
+ unused: no unreferenced files among 110 tracked .ts/.tsx/.astro file(s)
55
56
  ```
56
57
 
57
58
  ## When it runs
58
59
 
59
- `checks-lint` runs it when the repository tracks a `.ts` or `.tsx` file.
60
+ `checks-lint` runs it when the repository tracks a `.ts`, `.tsx` or `.astro` file.
60
61
  Such a repository names its entries in a Knip configuration.
61
62
 
62
63
  ## Related topics
package/oxlintrc.json CHANGED
@@ -37,6 +37,13 @@
37
37
  "typescript/no-unsafe-return": "error",
38
38
  "typescript/no-unsafe-call": "error"
39
39
  }
40
+ },
41
+ {
42
+ "files": ["**/*.astro"],
43
+ "rules": {
44
+ "readability/thin-astro": "error",
45
+ "import/no-unassigned-import": "off"
46
+ }
40
47
  }
41
48
  ]
42
49
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avi2dg/checks",
3
- "version": "0.30.0",
3
+ "version": "0.31.0",
4
4
  "description": "Deterministic checks shared across a set of TypeScript repositories",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -117,7 +117,9 @@
117
117
  "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",
118
118
  "lint": "oxlint --type-aware && bun src/core/lint.ts && depcruise --config .dependency-cruiser.cjs .",
119
119
  "typecheck": "tsc --noEmit && effect-tsgo diagnostics --project tsconfig.json --format text --strict",
120
- "test": "bun src/testing/test.ts"
120
+ "test": "bun src/testing/test.ts",
121
+ "mutate": "bunx stryker run",
122
+ "mutate:incremental": "bunx stryker run --incremental"
121
123
  },
122
124
  "peerDependencies": {
123
125
  "@swc/core": "1.16.2",
@@ -131,7 +133,9 @@
131
133
  },
132
134
  "devDependencies": {
133
135
  "@effect/tsgo": "0.45.0",
136
+ "@hughescr/stryker-bun-runner": "1.4.0",
134
137
  "@oxlint/plugins": "1.83.0",
138
+ "@stryker-mutator/core": "10.0.0",
135
139
  "@swc/core": "1.16.2",
136
140
  "@types/bun": "^1.3.0",
137
141
  "dependency-cruiser": "18.4.0",
@@ -148,5 +152,9 @@
148
152
  "@effect/platform-node-shared": "4.0.0-rc.115",
149
153
  "@total-typescript/ts-reset": "0.6.1",
150
154
  "knip": "6.38.0"
155
+ },
156
+ "overrides": {
157
+ "qs": "6.16.0",
158
+ "smol-toml": "1.9.0"
151
159
  }
152
160
  }
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  import { Console, Effect, FileSystem, Path, Schema } from "effect";
3
+ import { TYPESCRIPT_SOURCE } from "../core/gates.ts";
3
4
  import { checkoutFiles, commitOf, git, pathsAt, rangeEnds, refArgs } from "../core/git.ts";
4
5
  import { runMain } from "../core/main.ts";
5
6
  import { knipReport, scanTree } from "./knip.ts";
@@ -127,7 +128,7 @@ export function report(head: Baseline, { unlisted, added, stale }: Drift): strin
127
128
  : `${NAME}: ${head.length} unused export(s) in ${BASELINE_FILE}, and no new ones`;
128
129
  }
129
130
 
130
- const scan = scanTree(NAME, INCLUDED).pipe(Effect.mapError((cause) => new ExportsError({ message: cause.message })));
131
+ const scan = scanTree(NAME, INCLUDED, TYPESCRIPT_SOURCE).pipe(Effect.mapError((cause) => new ExportsError({ message: cause.message })));
131
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
134
  const unusedAt = Effect.fn("unusedAt")(
@@ -1,4 +1,5 @@
1
1
  import { Console, Effect, FileSystem, Path, Schema } from "effect";
2
+ import type { TrackedContent } from "../core/gates.ts";
2
3
  import { collect, git } from "../core/git.ts";
3
4
 
4
5
  class KnipError extends Schema.TaggedError<KnipError>()("KnipError", {
@@ -16,8 +17,6 @@ const CONFIGS = [
16
17
  "knip.config.ts",
17
18
  ] as const;
18
19
 
19
- const TYPESCRIPT = ["*.ts", "*.tsx"];
20
-
21
20
  const hasKnipConfig = Effect.fn("hasKnipConfig")(function* (root: string) {
22
21
  const fs = yield* FileSystem.FileSystem;
23
22
  const path = yield* Path.Path;
@@ -55,13 +54,13 @@ export const knipReport = Effect.fn("knipReport")(function* (root: string, args:
55
54
  return { kind: "reported", stdout: run.stdout } as const;
56
55
  });
57
56
 
58
- export const scanTree = Effect.fn("scanTree")(function* (name: string, args: readonly string[]) {
57
+ export const scanTree = Effect.fn("scanTree")(function* (name: string, args: readonly string[], source: TrackedContent) {
59
58
  const root = (yield* git(["rev-parse", "--show-toplevel"])).trim();
60
- const tracked = (yield* git(["ls-files", "--", ...TYPESCRIPT], root))
59
+ const tracked = (yield* git(["ls-files", "--", ...source.pathspecs], root))
61
60
  .split("\n")
62
61
  .map((line) => line.trim())
63
62
  .filter((line) => line !== "");
64
- if (tracked.length === 0) return yield* new KnipError({ message: "no tracked .ts or .tsx files to scan" });
63
+ if (tracked.length === 0) return yield* new KnipError({ message: `no tracked ${source.content} to scan` });
65
64
  const reported = yield* knipReport(root, args);
66
65
  if (reported.kind === "unconfigured") {
67
66
  yield* Console.log(`${name}: no knip configuration names entry files, so add one extending the kit's knip-base.json`);
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env bun
2
2
  import { Console, Effect, Schema } from "effect";
3
+ import { LINTED_SOURCE } from "../core/gates.ts";
3
4
  import { runMain } from "../core/main.ts";
4
5
  import { scanTree } from "./knip.ts";
5
6
 
6
7
  export type Scan = { readonly tracked: number; readonly files: readonly string[] };
7
8
 
8
9
  const NAME = "unused";
9
- const JUDGED = /\.tsx?$/;
10
+ const JUDGED = /[.]tsx?$|[.]astro$/;
10
11
 
11
12
  class UnusedError extends Schema.TaggedError<UnusedError>()("UnusedError", {
12
13
  message: Schema.String,
@@ -31,12 +32,12 @@ export const filesOf = Effect.fn("filesOf")(function* (stdout: string) {
31
32
  });
32
33
 
33
34
  export function report({ tracked, files }: Scan): string {
34
- if (files.length === 0) return `${NAME}: no unreferenced files among ${tracked} tracked .ts/.tsx file(s)`;
35
+ if (files.length === 0) return `${NAME}: no unreferenced files among ${tracked} tracked .ts/.tsx/.astro file(s)`;
35
36
  return [`${NAME}: ${files.length} unreferenced file(s):`, ...files.map((file) => ` ${file}`)].join("\n");
36
37
  }
37
38
 
38
39
  const unused = Effect.gen(function* () {
39
- const { tracked, reported } = yield* scanTree(NAME, ["--files"]).pipe(
40
+ const { tracked, reported } = yield* scanTree(NAME, ["--files"], LINTED_SOURCE).pipe(
40
41
  Effect.mapError((cause) => new UnusedError({ message: cause.message })),
41
42
  );
42
43
  if (reported.kind === "unconfigured") return false;
package/src/core/gates.ts CHANGED
@@ -3,7 +3,7 @@ export type Program = {
3
3
  readonly script: string;
4
4
  };
5
5
 
6
- type TrackedContent = {
6
+ export type TrackedContent = {
7
7
  readonly pathspecs: readonly string[];
8
8
  readonly content: string;
9
9
  };
@@ -30,12 +30,14 @@ export const TEST_ENTRY_POINT: Program = { bin: "checks-test", script: "testing/
30
30
 
31
31
  export const DEFAULT_BRANCH = "main";
32
32
 
33
- const TYPESCRIPT_SOURCE: TrackedContent = { pathspecs: ["*.ts", "*.tsx"], content: "TypeScript source" };
33
+ export const TYPESCRIPT_SOURCE: TrackedContent = { pathspecs: ["*.ts", "*.tsx"], content: "TypeScript source" };
34
+
35
+ export const LINTED_SOURCE: TrackedContent = { pathspecs: ["*.ts", "*.tsx", "*.astro"], content: "TypeScript or Astro source" };
34
36
 
35
37
  const BUN_LOCKFILE: TrackedContent = { pathspecs: ["bun.lock"], content: "a bun lockfile" };
36
38
 
37
39
  export const KIT_GATES = [
38
- { bin: "checks-lint-coverage", vector: "quality", file: "lint-coverage.sh", reads: "tree", appliesTo: TYPESCRIPT_SOURCE },
40
+ { bin: "checks-lint-coverage", vector: "quality", file: "lint-coverage.sh", reads: "tree", appliesTo: LINTED_SOURCE },
39
41
  { bin: "checks-test-layout", vector: "testing", file: "test-layout.ts", reads: "tree", appliesTo: TYPESCRIPT_SOURCE },
40
42
  { bin: "checks-commit-identity", vector: "delivery", file: "commit-identity.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
41
43
  { bin: "checks-comment-gate", vector: "quality", file: "comment-gate.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
@@ -43,7 +45,7 @@ export const KIT_GATES = [
43
45
  { bin: "checks-ci-wiring", vector: "delivery", file: "ci-wiring.ts", reads: "tree", appliesTo: EVERY_REPOSITORY },
44
46
  { bin: "checks-docs", vector: "docs", file: "docs.ts", reads: "range", alsoReads: "every agent file at the head commit", appliesTo: EVERY_REPOSITORY },
45
47
  { bin: "checks-repetition", vector: "complexity", file: "repetition.ts", reads: "range", appliesTo: TYPESCRIPT_SOURCE },
46
- { bin: "checks-unused", vector: "complexity", file: "unused.ts", reads: "tree", appliesTo: TYPESCRIPT_SOURCE },
48
+ { bin: "checks-unused", vector: "complexity", file: "unused.ts", reads: "tree", appliesTo: LINTED_SOURCE },
47
49
  { bin: "checks-exports", vector: "complexity", file: "exports.ts", reads: "range", appliesTo: TYPESCRIPT_SOURCE },
48
50
  { bin: "checks-quarantine-clock", vector: "testing", file: "quarantine-clock.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
49
51
  { bin: "checks-advisories", vector: "dependencies", file: "advisories.ts", reads: "range", appliesTo: BUN_LOCKFILE },
@@ -152,4 +152,4 @@ const check = Effect.gen(function* () {
152
152
  return true;
153
153
  });
154
154
 
155
- runMain("commit-identity", check);
155
+ if (import.meta.main) runMain("commit-identity", check);
@@ -1,14 +1,14 @@
1
1
  #!/bin/sh
2
- # lint-coverage: fail when oxlint silently skips a tracked TypeScript source,
2
+ # lint-coverage: fail when oxlint silently skips a tracked TypeScript or Astro source,
3
3
  # or when tsconfig.json's program silently drops the ts-reset rules.
4
4
  set -eu
5
5
 
6
6
  tmp="$(mktemp -d)"
7
7
  trap 'rm -rf "$tmp"' EXIT INT TERM
8
8
 
9
- git ls-files -- '*.ts' '*.tsx' | LC_ALL=C sort > "$tmp/expected"
9
+ git ls-files -- '*.ts' '*.tsx' '*.astro' | LC_ALL=C sort > "$tmp/expected"
10
10
  if ! [ -s "$tmp/expected" ]; then
11
- echo "lint-coverage: no tracked .ts/.tsx files"
11
+ echo "lint-coverage: no tracked .ts/.tsx/.astro files"
12
12
  exit 0
13
13
  fi
14
14
 
@@ -18,7 +18,7 @@ if ! oxlint --debug=files > "$tmp/walk" 2> "$tmp/walk-error"; then
18
18
  cat "$tmp/walk" "$tmp/walk-error"
19
19
  exit 2
20
20
  fi
21
- grep -E '\.tsx?$' "$tmp/walk" | LC_ALL=C sort > "$tmp/walked" || true
21
+ grep -E '[.]tsx?$|[.]astro$' "$tmp/walk" | LC_ALL=C sort > "$tmp/walked" || true
22
22
 
23
23
  expected_count="$(wc -l < "$tmp/expected" | tr -d ' ')"
24
24
  walked_count="$(grep -c . "$tmp/walked" || true)"
@@ -27,11 +27,16 @@ status=0
27
27
  comm -23 "$tmp/expected" "$tmp/walked" > "$tmp/missing"
28
28
  if [ -s "$tmp/missing" ]; then
29
29
  missing_count="$(wc -l < "$tmp/missing" | tr -d ' ')"
30
- echo "lint-coverage: oxlint skips ${missing_count}/${expected_count} tracked .ts/.tsx files; missing:"
30
+ echo "lint-coverage: oxlint skips ${missing_count}/${expected_count} tracked .ts/.tsx/.astro files; missing:"
31
31
  cat "$tmp/missing"
32
32
  status=1
33
33
  else
34
- echo "lint-coverage: ${walked_count}/${expected_count} tracked .ts/.tsx files"
34
+ echo "lint-coverage: ${walked_count}/${expected_count} tracked .ts/.tsx/.astro files"
35
+ fi
36
+
37
+ if ! grep -qE '[.]tsx?$' "$tmp/expected"; then
38
+ echo "lint-coverage: no tracked .ts/.tsx files, so no program to hold the ts-reset rules"
39
+ exit "$status"
35
40
  fi
36
41
 
37
42
  if ! [ -f tsconfig.json ]; then