@zgeoff/chex 0.1.8 → 0.1.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zgeoff/chex",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Run each shared docs and repo check as one subcommand, with the lefthook pre-push baseline that calls them",
5
5
  "keywords": [
6
6
  "checks",
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/bun": "1.4.4",
44
- "@zgeoff/rig": "0.1.5",
44
+ "@zgeoff/rig": "0.1.6",
45
45
  "@zgeoff/std": "0.4.2",
46
46
  "@zgeoff/testing": "0.2.1",
47
47
  "tiny-invariant": "1.3.3",
@@ -9,6 +9,8 @@ export function collectStaleAgeGateExcludes(
9
9
  ): DependencyGateFinding[] {
10
10
  const removalDates = collectRemovalDates(bunfig);
11
11
 
12
+ // Renovate bumps a first-party package as soon as it publishes, so its exclude stays for good and
13
+ // carries no removal date.
12
14
  return parseAgeGate(bunfig)
13
15
  .excludes.filter((name) => !firstPartyScopes.some((scope) => name.startsWith(scope)))
14
16
  .flatMap((name) => {
@@ -1,5 +1,5 @@
1
- // The limits match the max-test-timeout rule in @zgeoff/oxlint-config, which CI cannot import: the
2
- // workflow downloads this file and the modules it imports one by one.
1
+ // The limits match the max-test-timeout rule in @zgeoff/oxlint-config, which neither chex nor the
2
+ // copy that repo-sync puts in each repo can import.
3
3
  const standardLaneLimits: readonly (readonly [string, number])[] = [
4
4
  ['browser', 10_000],
5
5
  ['e2e', 15_000],
@@ -0,0 +1,31 @@
1
+ import { findMember } from './find-member.ts';
2
+
3
+ export type ParsedRigLaneLimits =
4
+ | { readonly kind: 'parsed'; readonly limits: ReadonlyMap<string, number> }
5
+ | { readonly kind: 'refused'; readonly reason: string };
6
+
7
+ // chex has no dependency on rig, and the copy that repo-sync puts in each repo imports no package,
8
+ // so this reads the rig config's default export field by field. That export holds every standard
9
+ // lane too, each with its limit in ms.
10
+ export function parseRigLaneLimits(rigConfig: unknown): ParsedRigLaneLimits {
11
+ const lanes = findMember(rigConfig, 'lanes');
12
+
13
+ if (!Array.isArray(lanes)) {
14
+ return { kind: 'refused', reason: 'its default export has no list of lanes' };
15
+ }
16
+
17
+ const limits = new Map<string, number>();
18
+
19
+ for (const [index, lane] of lanes.entries()) {
20
+ const name = findMember(lane, 'name');
21
+ const limitMs = findMember(lane, 'limitMs');
22
+
23
+ if (typeof name !== 'string' || typeof limitMs !== 'number') {
24
+ return { kind: 'refused', reason: `lane ${index} lacks a name or a limitMs` };
25
+ }
26
+
27
+ limits.set(name, limitMs);
28
+ }
29
+
30
+ return { kind: 'parsed', limits };
31
+ }
@@ -1,5 +1,7 @@
1
- import { basename, dirname, posix } from 'node:path';
1
+ import { basename, dirname, join, posix } from 'node:path';
2
+ import { findMember } from './find-member.ts';
2
3
  import { parseLaneLimits } from './parse-lane-limits.ts';
4
+ import { parseRigLaneLimits } from './parse-rig-lane-limits.ts';
3
5
  import type { ShellWord } from './parse-shell-words.ts';
4
6
  import { parseShellWords } from './parse-shell-words.ts';
5
7
  import type { RunProgram } from './run-program.ts';
@@ -9,6 +11,7 @@ export interface TestTimeoutsCheckOptions {
9
11
  readonly cwd: string;
10
12
  readonly env: Readonly<Record<string, string | undefined>>;
11
13
  readonly runProgram: RunProgram;
14
+ readonly importModule: (absolutePath: string) => Promise<unknown>;
12
15
  }
13
16
 
14
17
  export interface TestTimeoutFinding {
@@ -19,16 +22,12 @@ export interface TestTimeoutFinding {
19
22
  }
20
23
 
21
24
  // The check reads the files as HEAD holds them, so a push checks what it sends.
22
- export function runTestTimeoutsCheck(
25
+ export async function runTestTimeoutsCheck(
23
26
  options: Readonly<TestTimeoutsCheckOptions>,
24
- ): TestTimeoutFinding[] {
27
+ ): Promise<TestTimeoutFinding[]> {
25
28
  const paths = runGit(options, ['ls-tree', '-r', '-z', '--name-only', 'HEAD']).split('\0');
26
29
 
27
- const configText = paths.includes(oxlintConfigPath)
28
- ? runGit(options, ['show', `HEAD:${oxlintConfigPath}`])
29
- : null;
30
-
31
- const limits = parseLaneLimits(configText);
30
+ const limits = await readLaneLimits(options, paths);
32
31
 
33
32
  const appRoots = new Set(
34
33
  paths.filter((path) => basename(path) === 'package.json').map((path) => dirname(path)),
@@ -54,7 +53,6 @@ export function runTestTimeoutsCheck(
54
53
  }
55
54
 
56
55
  const configPattern = /^\.?oxlintrc(?:\.[\w-]+)?\.jsonc?$/u;
57
- const oxlintConfigPath = '.oxlintrc.json';
58
56
 
59
57
  const checkedFiles = new Set(['package.json', 'bunfig.toml']);
60
58
 
@@ -68,6 +66,51 @@ function runGit(options: Readonly<TestTimeoutsCheckOptions>, args: readonly stri
68
66
  return result.stdout;
69
67
  }
70
68
 
69
+ const rigConfigPath = 'rig.config.ts';
70
+ const oxlintConfigPath = '.oxlintrc.json';
71
+
72
+ // oxlint-config refuses the lanes option of its timeout rule in a repo with a rig.config.ts, so
73
+ // that file alone sets the lanes there.
74
+ async function readLaneLimits(
75
+ options: Readonly<TestTimeoutsCheckOptions>,
76
+ paths: readonly string[],
77
+ ): Promise<ReadonlyMap<string, number>> {
78
+ if (!paths.includes(rigConfigPath)) {
79
+ const configText = paths.includes(oxlintConfigPath)
80
+ ? runGit(options, ['show', `HEAD:${oxlintConfigPath}`])
81
+ : null;
82
+
83
+ return parseLaneLimits(configText);
84
+ }
85
+
86
+ // Only an import resolves the rig config's lanes, and an import reads the working tree, so the
87
+ // check runs only while the working tree holds HEAD's copy.
88
+ const diff = options.runProgram(['git', 'diff', '--quiet', 'HEAD', '--', rigConfigPath], {
89
+ cwd: options.cwd,
90
+ env: options.env,
91
+ });
92
+
93
+ if (diff.exitCode === 1) {
94
+ throw new Error(
95
+ `${rigConfigPath} in the working tree differs from HEAD; commit or stash the change, because the check reads the lane limits from HEAD`,
96
+ );
97
+ }
98
+
99
+ if (diff.exitCode !== 0) {
100
+ throw new Error(`git diff --quiet HEAD -- ${rigConfigPath} failed: ${diff.stderr.trim()}`);
101
+ }
102
+
103
+ const loaded = await options.importModule(join(options.cwd, rigConfigPath));
104
+
105
+ const parsed = parseRigLaneLimits(findMember(loaded, 'default'));
106
+
107
+ if (parsed.kind === 'refused') {
108
+ throw new Error(`cannot read the lanes of ${rigConfigPath}: ${parsed.reason}`);
109
+ }
110
+
111
+ return parsed.limits;
112
+ }
113
+
71
114
  // A setting that turns a timeout rule off or down to a warning escapes the limits as a disable
72
115
  // comment does, so it needs the same ticket, in a comment on its line or the lines above it.
73
116
  function collectConfigFindings(path: string, text: string): TestTimeoutFinding[] {
@@ -288,12 +331,6 @@ function collectScriptFindings(
288
331
  });
289
332
  }
290
333
 
291
- function findMember(value: unknown, key: string): unknown {
292
- return typeof value === 'object' && value !== null && Object.hasOwn(value, key)
293
- ? Reflect.get(value, key)
294
- : undefined;
295
- }
296
-
297
334
  interface LaneLimit {
298
335
  readonly lane: string;
299
336
  readonly limitMs: number;
@@ -475,9 +512,14 @@ const ticketPattern = /\b[A-Z][A-Z0-9]*-\d+\b/u;
475
512
  const floorPattern = /\bfloor:[\s*]*[^\s*]/u;
476
513
 
477
514
  // Returns the exit code: 0 when clean, 1 with a finding, and 2 when the check cannot run.
478
- export function runTestTimeoutsCommand(): number {
515
+ export async function runTestTimeoutsCommand(): Promise<number> {
479
516
  try {
480
- const findings = runTestTimeoutsCheck({ cwd: process.cwd(), env: process.env, runProgram });
517
+ const findings = await runTestTimeoutsCheck({
518
+ cwd: process.cwd(),
519
+ env: process.env,
520
+ runProgram,
521
+ importModule: (path) => import(path),
522
+ });
481
523
 
482
524
  for (const finding of findings) {
483
525
  process.stdout.write(`${finding.path}: ${finding.message}\n`);
@@ -494,5 +536,5 @@ export function runTestTimeoutsCommand(): number {
494
536
  }
495
537
 
496
538
  if (import.meta.main) {
497
- process.exitCode = runTestTimeoutsCommand();
539
+ process.exitCode = await runTestTimeoutsCommand();
498
540
  }