@avi2dg/checks 0.1.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.
@@ -0,0 +1,27 @@
1
+ #!/bin/sh
2
+ # lint-coverage: fail when oxlint silently skips a tracked TypeScript source.
3
+ set -eu
4
+
5
+ tmp="$(mktemp -d)"
6
+ trap 'rm -rf "$tmp"' EXIT INT TERM
7
+
8
+ git ls-files -- '*.ts' '*.tsx' | LC_ALL=C sort > "$tmp/expected"
9
+ if ! [ -s "$tmp/expected" ]; then
10
+ echo "lint-coverage: no tracked .ts/.tsx files"
11
+ exit 0
12
+ fi
13
+
14
+ # Explicit paths bypass ignore files, so only an unscoped walk proves coverage.
15
+ oxlint --debug=files 2>/dev/null | grep -E '\.tsx?$' | LC_ALL=C sort > "$tmp/walked" || true
16
+
17
+ expected_count="$(wc -l < "$tmp/expected" | tr -d ' ')"
18
+ walked_count="$(grep -c . "$tmp/walked" || true)"
19
+
20
+ missing="$(comm -23 "$tmp/expected" "$tmp/walked")"
21
+ if [ -n "$missing" ]; then
22
+ echo "lint-coverage: oxlint skips ${walked_count}/${expected_count} tracked .ts/.tsx files; missing:"
23
+ printf '%s\n' "$missing"
24
+ exit 1
25
+ fi
26
+
27
+ echo "lint-coverage: ${walked_count}/${expected_count} tracked .ts/.tsx files"
@@ -0,0 +1,316 @@
1
+ #!/usr/bin/env bun
2
+ import { parseSync } from "@swc/core";
3
+ import { readFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ export type Violation = {
8
+ readonly file: string;
9
+ readonly line: number | undefined;
10
+ readonly message: string;
11
+ };
12
+
13
+ const TESTS = "tests/";
14
+ const E2E = "tests/e2e/";
15
+ const HELPER_DIRS = ["tests/lib/", "tests/fixtures/"] as const;
16
+ const DATA_DIR = "tests/fixtures/";
17
+
18
+ const TEST_FILE = /(?:[.](?:test|spec)|_test)[.](tsx?)$/;
19
+ const TARGET_FILE = /^tests\/(?:[^/]+\/)*[^/]+[.]test[.]tsx?$/;
20
+ const TYPESCRIPT = /[.]tsx?$/;
21
+
22
+ const BANNED_MODULES: readonly string[] = [
23
+ "child_process",
24
+ "net",
25
+ "http",
26
+ "https",
27
+ "http2",
28
+ "tls",
29
+ "dgram",
30
+ ];
31
+ const BANNED_BUN_NAMES: readonly string[] = [
32
+ "$",
33
+ "spawn",
34
+ "spawnSync",
35
+ "connect",
36
+ "serve",
37
+ "listen",
38
+ ];
39
+ const BANNED_GLOBAL_CALLS: readonly string[] = ["fetch"];
40
+
41
+ export const REQUIRED_TEST_SCRIPT = "bun test --randomize";
42
+ export const LAYOUT_CHECK_MARK = "scripts/test-layout.ts";
43
+
44
+ function isRecord(value: unknown): value is Record<string, unknown> {
45
+ return typeof value === "object" && value !== null;
46
+ }
47
+
48
+ function startsWithAny(file: string, prefixes: readonly string[]): boolean {
49
+ return prefixes.some((prefix) => file.startsWith(prefix));
50
+ }
51
+
52
+ function targetFor(file: string): string {
53
+ const withoutLeadingDirs = file
54
+ .replace(/^tests\/(?:lib|fixtures)\//, "")
55
+ .replace(/^(?:src|lib|app)\//, "")
56
+ .replace(/(?:^|\/)__tests__\//, "/")
57
+ .replace(/^\/+/, "");
58
+ const renamed = withoutLeadingDirs.replace(TEST_FILE, ".test.$1");
59
+ return renamed.startsWith(TESTS) ? renamed : `${TESTS}${renamed}`;
60
+ }
61
+
62
+ export function placementViolations(files: readonly string[]): readonly Violation[] {
63
+ const violations: Violation[] = [];
64
+ for (const file of files) {
65
+ if (!TEST_FILE.test(file)) continue;
66
+ const target = targetFor(file);
67
+ if (startsWithAny(file, HELPER_DIRS)) {
68
+ violations.push({
69
+ file,
70
+ line: undefined,
71
+ message: `tests/lib and tests/fixtures hold helpers and data, never tests; move it to ${target}`,
72
+ });
73
+ continue;
74
+ }
75
+ if (!TARGET_FILE.test(file)) {
76
+ violations.push({
77
+ file,
78
+ line: undefined,
79
+ message: `a test file must live at tests/**/*.test.ts; move it to ${target}`,
80
+ });
81
+ }
82
+ }
83
+ return violations;
84
+ }
85
+
86
+ function lineOf(source: string, start: number): number {
87
+ const bytes = Buffer.from(source, "utf8");
88
+ const offset = Math.max(0, Math.min(bytes.length, start - 1));
89
+ let line = 1;
90
+ for (let index = 0; index < offset; index += 1) {
91
+ if (bytes[index] === 0x0a) line += 1;
92
+ }
93
+ return line;
94
+ }
95
+
96
+ function bannedModule(specifier: string): string | undefined {
97
+ const bare = specifier.startsWith("node:") ? specifier.slice("node:".length) : specifier;
98
+ return BANNED_MODULES.includes(bare) ? bare : undefined;
99
+ }
100
+
101
+ function identifierName(node: unknown): string | undefined {
102
+ if (!isRecord(node) || node["type"] !== "Identifier") return undefined;
103
+ const value = node["value"];
104
+ return typeof value === "string" ? value : undefined;
105
+ }
106
+
107
+ function stringValue(node: unknown): string | undefined {
108
+ if (!isRecord(node) || node["type"] !== "StringLiteral") return undefined;
109
+ const value = node["value"];
110
+ return typeof value === "string" ? value : undefined;
111
+ }
112
+
113
+ function spanStart(node: Record<string, unknown>): number {
114
+ const span = node["span"];
115
+ if (!isRecord(span)) return 0;
116
+ const start = span["start"];
117
+ return typeof start === "number" ? start : 0;
118
+ }
119
+
120
+ function importedNames(node: Record<string, unknown>): readonly string[] {
121
+ const specifiers = node["specifiers"];
122
+ if (!Array.isArray(specifiers)) return [];
123
+ const names: string[] = [];
124
+ for (const specifier of specifiers) {
125
+ if (!isRecord(specifier)) continue;
126
+ const name =
127
+ identifierName(specifier["imported"]) ??
128
+ identifierName(specifier["orig"]) ??
129
+ identifierName(specifier["local"]);
130
+ if (name !== undefined) names.push(name);
131
+ }
132
+ return names;
133
+ }
134
+
135
+ function outOfProcessUse(node: Record<string, unknown>): string | undefined {
136
+ const type = node["type"];
137
+
138
+ if (type === "ImportDeclaration" || type === "ExportNamedDeclaration" || type === "ExportAllDeclaration") {
139
+ const source = stringValue(node["source"]);
140
+ if (source === undefined) return undefined;
141
+ const banned = bannedModule(source);
142
+ if (banned !== undefined) return `imports node:${banned}`;
143
+ if (source === "bun") {
144
+ const name = importedNames(node).find((candidate) => BANNED_BUN_NAMES.includes(candidate));
145
+ if (name !== undefined) return `imports ${name} from bun`;
146
+ }
147
+ return undefined;
148
+ }
149
+
150
+ if (type === "MemberExpression") {
151
+ if (identifierName(node["object"]) !== "Bun") return undefined;
152
+ const property = identifierName(node["property"]);
153
+ if (property !== undefined && BANNED_BUN_NAMES.includes(property)) return `uses Bun.${property}`;
154
+ return undefined;
155
+ }
156
+
157
+ if (type === "CallExpression") {
158
+ const callee = node["callee"];
159
+ const calleeName = identifierName(callee);
160
+ if (calleeName !== undefined && BANNED_GLOBAL_CALLS.includes(calleeName)) {
161
+ return `calls ${calleeName}`;
162
+ }
163
+ const argument = Array.isArray(node["arguments"]) ? node["arguments"][0] : undefined;
164
+ const specifier = isRecord(argument) ? stringValue(argument["expression"]) : undefined;
165
+ if (specifier === undefined) return undefined;
166
+ const banned = bannedModule(specifier);
167
+ if (banned === undefined) return undefined;
168
+ if (isRecord(callee) && callee["type"] === "Import") return `imports node:${banned}`;
169
+ if (calleeName === "require") return `requires node:${banned}`;
170
+ return undefined;
171
+ }
172
+
173
+ return undefined;
174
+ }
175
+
176
+ export function isolationViolations(file: string, source: string): readonly Violation[] {
177
+ const module = parseSync(source, { syntax: "typescript", tsx: file.endsWith(".tsx"), target: "esnext" });
178
+ const violations: Violation[] = [];
179
+ const seen = new Set<unknown>();
180
+
181
+ const visit = (node: unknown): void => {
182
+ if (Array.isArray(node)) {
183
+ for (const child of node) visit(child);
184
+ return;
185
+ }
186
+ if (!isRecord(node) || seen.has(node)) return;
187
+ seen.add(node);
188
+ if (typeof node["type"] === "string") {
189
+ const use = outOfProcessUse(node);
190
+ if (use !== undefined) {
191
+ violations.push({
192
+ file,
193
+ line: lineOf(source, spanStart(node)),
194
+ message: `a test outside ${E2E} must stay in-process, and this ${use}; move it to ${E2E}${file.slice(TESTS.length)}`,
195
+ });
196
+ }
197
+ }
198
+ for (const value of Object.values(node)) visit(value);
199
+ };
200
+
201
+ visit(module);
202
+ return violations;
203
+ }
204
+
205
+ export function scriptViolations(manifest: unknown): readonly Violation[] {
206
+ const file = "package.json";
207
+ const scripts = isRecord(manifest) ? manifest["scripts"] : undefined;
208
+ const test = isRecord(scripts) ? scripts["test"] : undefined;
209
+ const lint = isRecord(scripts) ? scripts["lint"] : undefined;
210
+ const violations: Violation[] = [];
211
+ if (test !== REQUIRED_TEST_SCRIPT) {
212
+ violations.push({
213
+ file,
214
+ line: undefined,
215
+ message: `scripts.test must be exactly "${REQUIRED_TEST_SCRIPT}", found ${JSON.stringify(test ?? null)}`,
216
+ });
217
+ }
218
+ if (typeof lint !== "string" || !lint.includes(LAYOUT_CHECK_MARK)) {
219
+ violations.push({
220
+ file,
221
+ line: undefined,
222
+ message: `scripts.lint must run the layout check: add "bun ./node_modules/@avi2dg/checks/${LAYOUT_CHECK_MARK}"`,
223
+ });
224
+ }
225
+ return violations;
226
+ }
227
+
228
+ export function bunfigViolations(consumer: unknown, preset: unknown): readonly Violation[] {
229
+ const file = "bunfig.toml";
230
+ const copy = "bun has no bunfig extends, so copy node_modules/@avi2dg/checks/bunfig.toml";
231
+ if (consumer === undefined) {
232
+ return [{ file, line: undefined, message: `bunfig.toml is missing; ${copy}` }];
233
+ }
234
+ const presetTest = isRecord(preset) ? preset["test"] : undefined;
235
+ if (!isRecord(presetTest)) {
236
+ return [{ file, line: undefined, message: "the shipped bunfig preset has no [test] table" }];
237
+ }
238
+ const consumerTest = isRecord(consumer) ? consumer["test"] : undefined;
239
+ const violations: Violation[] = [];
240
+ for (const [key, value] of Object.entries(presetTest)) {
241
+ const found = isRecord(consumerTest) ? consumerTest[key] : undefined;
242
+ if (!Bun.deepEquals(found, value)) {
243
+ violations.push({
244
+ file,
245
+ line: undefined,
246
+ message: `[test].${key} must be ${JSON.stringify(value)}, found ${JSON.stringify(found ?? null)}; ${copy}`,
247
+ });
248
+ }
249
+ }
250
+ return violations;
251
+ }
252
+
253
+ async function workingTreeFiles(root: string): Promise<readonly string[]> {
254
+ const listed = Bun.spawnSync(["git", "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { cwd: root });
255
+ if (listed.exitCode !== 0) {
256
+ throw new Error(`test-layout: git ls-files failed in ${root}: ${listed.stderr.toString()}`);
257
+ }
258
+ const listedFiles = listed.stdout.toString().split("\0").filter(Boolean);
259
+ const present = await Promise.all(listedFiles.map((file) => Bun.file(join(root, file)).exists()));
260
+ return listedFiles.filter((_, index) => present[index]);
261
+ }
262
+
263
+ async function parsedToml(path: string): Promise<unknown> {
264
+ const imported: unknown = await import(path);
265
+ return isRecord(imported) ? (imported["default"] ?? imported) : imported;
266
+ }
267
+
268
+ async function readManifest(root: string): Promise<unknown> {
269
+ const text = await readFile(join(root, "package.json"), "utf8");
270
+ return JSON.parse(text) as unknown;
271
+ }
272
+
273
+ export type Result = { readonly files: number; readonly violations: readonly Violation[] };
274
+
275
+ export async function run(root: string, presetPath: string): Promise<Result> {
276
+ const files = await workingTreeFiles(root);
277
+ const violations: Violation[] = [...placementViolations(files)];
278
+
279
+ const inProcess = files.filter(
280
+ (file) =>
281
+ file.startsWith(TESTS) &&
282
+ TYPESCRIPT.test(file) &&
283
+ !file.startsWith(E2E) &&
284
+ !file.startsWith(DATA_DIR),
285
+ );
286
+ for (const file of inProcess) {
287
+ const source = await readFile(join(root, file), "utf8");
288
+ violations.push(...isolationViolations(file, source));
289
+ }
290
+
291
+ violations.push(...scriptViolations(await readManifest(root)));
292
+
293
+ const consumerBunfig = (await Bun.file(join(root, "bunfig.toml")).exists())
294
+ ? await parsedToml(join(root, "bunfig.toml"))
295
+ : undefined;
296
+ violations.push(...bunfigViolations(consumerBunfig, await parsedToml(presetPath)));
297
+
298
+ return { files: files.length, violations };
299
+ }
300
+
301
+ export function report({ files, violations }: Result): string {
302
+ if (violations.length === 0) return `test-layout: ${files} files satisfy the layout`;
303
+ const lines = violations.map(
304
+ (violation) =>
305
+ ` ${violation.file}${violation.line === undefined ? "" : `:${violation.line}`}: ${violation.message}`,
306
+ );
307
+ return [`test-layout: ${violations.length} violation(s)`, ...lines].join("\n");
308
+ }
309
+
310
+ if (import.meta.main) {
311
+ const root = process.argv[2] ?? process.cwd();
312
+ const preset = fileURLToPath(new URL("../bunfig.toml", import.meta.url));
313
+ const result = await run(root, preset);
314
+ console.log(report(result));
315
+ process.exit(result.violations.length === 0 ? 0 : 1);
316
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "erasableSyntaxOnly": true,
4
+ "plugins": [
5
+ {
6
+ "name": "@effect/language-service",
7
+ "diagnosticSeverity": {
8
+ "anyUnknownInErrorContext": "error",
9
+ "unsafeEffectTypeAssertion": "error",
10
+ "missingEffectError": "error",
11
+ "unknownInEffectCatch": "error",
12
+ "globalErrorInEffectCatch": "error",
13
+ "globalErrorInEffectFailure": "error",
14
+ "catchUnfailableEffect": "error",
15
+ "catchAllToMapError": "error",
16
+ "missingReturnYieldStar": "error",
17
+ "floatingEffect": "error",
18
+ "outdatedApi": "error"
19
+ }
20
+ }
21
+ ]
22
+ }
23
+ }