@avi2dg/checks 0.18.0 → 0.19.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 +10 -0
- package/README.md +3 -1
- package/bunfig.toml +1 -1
- package/dependency-cruiser.config.js +2 -0
- package/dist/feature-rules.js +22 -5
- package/docs/configs/quality-file.md +1 -0
- package/docs/gates/checks-lint.md +2 -1
- package/docs/gates/checks-mutation-compare.md +20 -11
- package/docs/gates/checks-quarantine-clock.md +54 -0
- package/docs/gates/checks-test-layout.md +9 -3
- package/docs/gates/checks-vendor.md +97 -0
- package/package.json +9 -2
- package/quality.schema.json +56 -4
- package/scripts/gates.ts +1 -0
- package/scripts/git.ts +10 -0
- package/scripts/lint.ts +75 -35
- package/scripts/mutation-compare.ts +179 -70
- package/scripts/quality-file.ts +30 -3
- package/scripts/quarantine-clock.ts +153 -0
- package/scripts/repetition.ts +40 -19
- package/scripts/size-budget.ts +46 -20
- package/scripts/suppressions-ratchet.ts +1 -5
- package/scripts/test-layout.ts +28 -17
- package/scripts/vendor.ts +337 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { Console, Effect, FileSystem, Option, Path, Schema } from "effect";
|
|
3
|
+
import { QUALITY_FILE } from "./gates.ts";
|
|
4
|
+
import { git } from "./git.ts";
|
|
5
|
+
import { runMain, Usage } from "./main.ts";
|
|
6
|
+
import { readQuality, type Library } from "./quality-file.ts";
|
|
7
|
+
|
|
8
|
+
const NAME = "checks-vendor";
|
|
9
|
+
const USAGE = `usage: ${NAME} takes no arguments, since quality.json names the libraries`;
|
|
10
|
+
const CACHE_HOME = ".cache/avi2dg-checks";
|
|
11
|
+
const RECORD_SUFFIX = ".commit";
|
|
12
|
+
const LINKS = "repos";
|
|
13
|
+
const VERSION_TOKEN = "{version}";
|
|
14
|
+
// bun runs prepare under umask 0, so a mode left to the umask lets any local user swap the shared tree.
|
|
15
|
+
const DIRECTORY_MODE = 0o755;
|
|
16
|
+
const RECORD_MODE = 0o644;
|
|
17
|
+
|
|
18
|
+
export class VendorError extends Schema.TaggedError<VendorError>()("VendorError", {
|
|
19
|
+
message: Schema.String,
|
|
20
|
+
}) {}
|
|
21
|
+
|
|
22
|
+
export class Unreachable extends Schema.TaggedError<Unreachable>()("Unreachable", {
|
|
23
|
+
message: Schema.String,
|
|
24
|
+
}) {}
|
|
25
|
+
|
|
26
|
+
export function tagFor(template: string, version: string): string {
|
|
27
|
+
return template.replaceAll(VERSION_TOKEN, version);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const SCHEMED = /^([a-z][a-z0-9+.-]*):\/\/(?:[^/@]+@)?([^/:]+)(?::\d+)?\/(.+)$/i;
|
|
31
|
+
const SCP_LIKE = /^(?:[^@/:]+@)?([^:/]+):(.+)$/;
|
|
32
|
+
|
|
33
|
+
function urlSegments(host: string, rest: string): readonly string[] {
|
|
34
|
+
const parts = rest.split("/").filter((part) => part !== "");
|
|
35
|
+
return [host, ...parts.slice(0, -1), (parts[parts.length - 1] ?? "").replace(/\.git$/, "")];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function remoteSegments(remote: string): readonly string[] {
|
|
39
|
+
const cleaned = remote.replace(/\/+$/, "");
|
|
40
|
+
const schemed = SCHEMED.exec(cleaned);
|
|
41
|
+
if (schemed !== null) {
|
|
42
|
+
const [, , host, rest] = schemed;
|
|
43
|
+
if (host !== undefined && rest !== undefined) return urlSegments(host, rest);
|
|
44
|
+
}
|
|
45
|
+
const scp = SCP_LIKE.exec(cleaned);
|
|
46
|
+
if (scp !== null) {
|
|
47
|
+
const [, host, rest] = scp;
|
|
48
|
+
if (host !== undefined && rest !== undefined) return urlSegments(host, rest);
|
|
49
|
+
}
|
|
50
|
+
const escaped = cleaned
|
|
51
|
+
.replaceAll("_", "__")
|
|
52
|
+
.replaceAll("/", "_")
|
|
53
|
+
.replaceAll(":", "_")
|
|
54
|
+
.replace(/^\.+/, (dots) => "_".repeat(dots.length));
|
|
55
|
+
return ["local", escaped];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function listsTag(output: string, tag: string): boolean {
|
|
59
|
+
return output.split("\n").some((line) => line.split("\t")[1] === `refs/tags/${tag}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const PackageManifest = Schema.Struct({ version: Schema.NonEmptyString });
|
|
63
|
+
const decodeManifest = Schema.decodeUnknownEffect(Schema.fromJsonString(PackageManifest));
|
|
64
|
+
|
|
65
|
+
const manifestVersion = Effect.fn("manifestVersion")(function* (file: string, missing: string) {
|
|
66
|
+
const fs = yield* FileSystem.FileSystem;
|
|
67
|
+
if (!(yield* fs.exists(file))) return yield* new VendorError({ message: missing });
|
|
68
|
+
const { version } = yield* fs.readFileString(file).pipe(
|
|
69
|
+
Effect.flatMap(decodeManifest),
|
|
70
|
+
Effect.mapError((cause) => new VendorError({ message: `${file} carries no version: ${cause.message}` })),
|
|
71
|
+
);
|
|
72
|
+
return version;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const headOf = Effect.fn("headOf")(function* (dir: string) {
|
|
76
|
+
return (yield* git(["rev-parse", "HEAD"], dir).pipe(
|
|
77
|
+
Effect.mapError((cause) => new VendorError({ message: `${dir} names no commit: ${cause.message}` })),
|
|
78
|
+
)).trim();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const confirmTag = Effect.fn("confirmTag")(function* (remote: string, tag: string) {
|
|
82
|
+
const output = yield* git(["ls-remote", remote, `refs/tags/${tag}*`]).pipe(
|
|
83
|
+
Effect.mapError((cause) => new Unreachable({ message: `cannot list ${tag} on ${remote}: ${cause.message}` })),
|
|
84
|
+
);
|
|
85
|
+
if (!listsTag(output, tag)) {
|
|
86
|
+
return yield* new VendorError({ message: `${remote} holds no tag ${tag}, so the installed version points nowhere` });
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const isLink = Effect.fn("isLink")(function* (entry: string) {
|
|
91
|
+
const fs = yield* FileSystem.FileSystem;
|
|
92
|
+
return Option.isSome(yield* fs.readLink(entry).pipe(Effect.option));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
type EntryMode = {
|
|
96
|
+
readonly entry: string;
|
|
97
|
+
readonly mode: number;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const modes = Effect.fn("modes")(function* (dir: string) {
|
|
101
|
+
const fs = yield* FileSystem.FileSystem;
|
|
102
|
+
const path = yield* Path.Path;
|
|
103
|
+
const found: EntryMode[] = [];
|
|
104
|
+
const pending = [dir];
|
|
105
|
+
for (const entry of pending) {
|
|
106
|
+
if (yield* isLink(entry)) continue;
|
|
107
|
+
const info = yield* fs.stat(entry).pipe(
|
|
108
|
+
Effect.mapError((cause) => new VendorError({ message: `cannot stat ${entry}: ${cause.message}` })),
|
|
109
|
+
);
|
|
110
|
+
found.push({ entry, mode: info.mode });
|
|
111
|
+
if (info.type !== "Directory") continue;
|
|
112
|
+
const children = yield* fs.readDirectory(entry).pipe(
|
|
113
|
+
Effect.mapError((cause) => new VendorError({ message: `cannot list ${entry}: ${cause.message}` })),
|
|
114
|
+
);
|
|
115
|
+
pending.push(...children.map((child) => path.join(entry, child)));
|
|
116
|
+
}
|
|
117
|
+
return found;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const WRITE_BITS = 0o222;
|
|
121
|
+
const OWNER_WRITE = 0o200;
|
|
122
|
+
|
|
123
|
+
function clearing(dir: string): string {
|
|
124
|
+
return `clear it with \`chmod -R u+w ${dir} && rm -rf ${dir}\` and rerun ${NAME}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const freeze = Effect.fn("freeze")(function* (dir: string) {
|
|
128
|
+
const fs = yield* FileSystem.FileSystem;
|
|
129
|
+
for (const { entry, mode } of yield* modes(dir)) {
|
|
130
|
+
yield* fs.chmod(entry, mode & ~WRITE_BITS).pipe(
|
|
131
|
+
Effect.mapError((cause) => new VendorError({ message: `cannot freeze ${entry}: ${cause.message}` })),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
function discard(dir: string) {
|
|
137
|
+
return Effect.gen(function* () {
|
|
138
|
+
const fs = yield* FileSystem.FileSystem;
|
|
139
|
+
if (!(yield* fs.exists(dir))) return;
|
|
140
|
+
for (const { entry, mode } of yield* modes(dir)) yield* fs.chmod(entry, mode | OWNER_WRITE);
|
|
141
|
+
yield* fs.remove(dir, { recursive: true });
|
|
142
|
+
}).pipe(Effect.catch((cause) => Console.error(`${NAME}: cannot clear the staging tree ${dir}: ${cause.message}`)));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const checkVersion = Effect.fn("checkVersion")(function* (dir: string, library: Library, installed: string, tag: string) {
|
|
146
|
+
const path = yield* Path.Path;
|
|
147
|
+
const manifest = path.join(dir, library.path ?? "package.json");
|
|
148
|
+
const landed = yield* manifestVersion(manifest, `${manifest} is missing, so ${tag} cannot prove it holds ${installed}`);
|
|
149
|
+
if (landed !== installed) {
|
|
150
|
+
return yield* new VendorError({ message: `${tag} holds version ${landed}, not the installed ${installed}` });
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
function recordOf(dir: string): string {
|
|
155
|
+
return `${dir}${RECORD_SUFFIX}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const recorded = Effect.fn("recorded")(function* (dir: string) {
|
|
159
|
+
const fs = yield* FileSystem.FileSystem;
|
|
160
|
+
const record = recordOf(dir);
|
|
161
|
+
if (!(yield* fs.exists(record))) return Option.none<string>();
|
|
162
|
+
const text = yield* fs.readFileString(record).pipe(
|
|
163
|
+
Effect.mapError((cause) => new VendorError({ message: `cannot read ${record}: ${cause.message}` })),
|
|
164
|
+
);
|
|
165
|
+
return Option.some(text.trim());
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const verify = Effect.fn("verify")(function* (dir: string, library: Library, installed: string, tag: string) {
|
|
169
|
+
const held = yield* recorded(dir);
|
|
170
|
+
if (Option.isNone(held)) {
|
|
171
|
+
return yield* new VendorError({ message: `${recordOf(dir)} is missing, so ${dir} has no commit to hold to; ${clearing(dir)}` });
|
|
172
|
+
}
|
|
173
|
+
const record = held.value;
|
|
174
|
+
const head = yield* headOf(dir);
|
|
175
|
+
if (head !== record) {
|
|
176
|
+
return yield* new VendorError({ message: `${dir} sits on ${head}, not the recorded ${record}; ${clearing(dir)}` });
|
|
177
|
+
}
|
|
178
|
+
yield* checkVersion(dir, library, installed, tag);
|
|
179
|
+
const tampered = (yield* modes(dir)).filter(({ mode }) => (mode & WRITE_BITS) !== 0).map(({ entry }) => entry);
|
|
180
|
+
if (tampered.length > 0) {
|
|
181
|
+
const [first = dir] = tampered;
|
|
182
|
+
return yield* new VendorError({
|
|
183
|
+
message: `${dir} leaves ${tampered.length} paths writable, starting with ${first}; ${clearing(dir)}`,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
const status = yield* git(["status", "--porcelain", "--ignored"], dir).pipe(
|
|
187
|
+
Effect.mapError((cause) => new VendorError({ message: `${dir} reports no status: ${cause.message}` })),
|
|
188
|
+
);
|
|
189
|
+
if (status.trim() !== "") {
|
|
190
|
+
const [first = ""] = status.trim().split("\n");
|
|
191
|
+
return yield* new VendorError({ message: `${dir} holds writes outside the recorded commit, starting with ${first}; ${clearing(dir)}` });
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const ensureLink = Effect.fn("ensureLink")(function* (root: string, library: Library, dir: string) {
|
|
196
|
+
const fs = yield* FileSystem.FileSystem;
|
|
197
|
+
const path = yield* Path.Path;
|
|
198
|
+
const parent = path.join(root, LINKS);
|
|
199
|
+
yield* fs.makeDirectory(parent, { recursive: true, mode: DIRECTORY_MODE }).pipe(
|
|
200
|
+
Effect.mapError((cause) => new VendorError({ message: `cannot hold ${parent}: ${cause.message}` })),
|
|
201
|
+
);
|
|
202
|
+
const at = path.join(parent, library.name);
|
|
203
|
+
const current = yield* fs.readLink(at).pipe(Effect.option);
|
|
204
|
+
if (Option.isSome(current)) {
|
|
205
|
+
if (current.value === dir) return;
|
|
206
|
+
yield* fs.remove(at).pipe(
|
|
207
|
+
Effect.mapError((cause) => new VendorError({ message: `cannot move ${at} aside: ${cause.message}` })),
|
|
208
|
+
);
|
|
209
|
+
} else if (yield* fs.exists(at)) {
|
|
210
|
+
return yield* new VendorError({ message: `${at} is not a link, so ${NAME} leaves it alone; move it away and rerun` });
|
|
211
|
+
}
|
|
212
|
+
yield* fs.symlink(dir, at).pipe(
|
|
213
|
+
Effect.mapError((cause) => new VendorError({ message: `cannot link ${at} to ${dir}: ${cause.message}` })),
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const unlink = Effect.fn("unlink")(function* (root: string, library: Library) {
|
|
218
|
+
const fs = yield* FileSystem.FileSystem;
|
|
219
|
+
const path = yield* Path.Path;
|
|
220
|
+
const at = path.join(root, LINKS, library.name);
|
|
221
|
+
if (Option.isNone(yield* fs.readLink(at).pipe(Effect.option))) return;
|
|
222
|
+
yield* fs.remove(at).pipe(
|
|
223
|
+
Effect.mapError((cause) => new VendorError({ message: `cannot drop ${at}: ${cause.message}` })),
|
|
224
|
+
);
|
|
225
|
+
yield* Console.error(`${NAME}: dropped ${LINKS}/${library.name}, so readers fall back to node_modules/${library.package}`);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const stage = Effect.fn("stage")(function* (staging: string, library: Library, installed: string, tag: string, dir: string) {
|
|
229
|
+
const fs = yield* FileSystem.FileSystem;
|
|
230
|
+
yield* git(["clone", "--no-local", "--branch", tag, "--depth", "1", "--", library.repository, staging]).pipe(
|
|
231
|
+
Effect.mapError((cause) => new Unreachable({ message: `cannot clone ${tag} from ${library.repository}: ${cause.message}` })),
|
|
232
|
+
);
|
|
233
|
+
const head = yield* headOf(staging);
|
|
234
|
+
const held = yield* recorded(dir);
|
|
235
|
+
if (Option.isSome(held) && held.value !== head) {
|
|
236
|
+
return yield* new VendorError({
|
|
237
|
+
message: `${tag} on ${library.repository} lands on ${head}, not the recorded ${held.value}; delete ${recordOf(dir)} to accept the move`,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
yield* checkVersion(staging, library, installed, tag);
|
|
241
|
+
if (Option.isNone(held)) {
|
|
242
|
+
yield* fs.writeFileString(recordOf(dir), `${head}\n`, { mode: RECORD_MODE }).pipe(
|
|
243
|
+
Effect.mapError((cause) => new VendorError({ message: `cannot record the fetch beside ${dir}: ${cause.message}` })),
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
yield* freeze(staging);
|
|
247
|
+
return yield* fs.rename(staging, dir).pipe(
|
|
248
|
+
Effect.as(true),
|
|
249
|
+
Effect.catchTag("PlatformError", (cause) =>
|
|
250
|
+
Effect.flatMap(fs.exists(dir), (raced) =>
|
|
251
|
+
raced ? Effect.succeed(false) : Effect.fail(new VendorError({ message: `cannot move ${staging} into ${dir}: ${cause.message}` })),
|
|
252
|
+
),
|
|
253
|
+
),
|
|
254
|
+
);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const land = Effect.fn("land")(function* (library: Library, installed: string, tag: string, dir: string) {
|
|
258
|
+
const fs = yield* FileSystem.FileSystem;
|
|
259
|
+
const path = yield* Path.Path;
|
|
260
|
+
const parent = path.dirname(dir);
|
|
261
|
+
yield* confirmTag(library.repository, tag);
|
|
262
|
+
yield* fs.makeDirectory(parent, { recursive: true, mode: DIRECTORY_MODE }).pipe(
|
|
263
|
+
Effect.mapError((cause) => new VendorError({ message: `cannot hold ${dir}: ${cause.message}` })),
|
|
264
|
+
);
|
|
265
|
+
const staging = yield* fs.makeTempDirectory({ directory: parent, prefix: `.${path.basename(dir)}-` }).pipe(
|
|
266
|
+
Effect.mapError((cause) => new VendorError({ message: `cannot stage ${dir}: ${cause.message}` })),
|
|
267
|
+
);
|
|
268
|
+
return yield* stage(staging, library, installed, tag, dir).pipe(Effect.ensuring(discard(staging)));
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const vend = Effect.fn("vend")(function* (root: string, cache: string, library: Library) {
|
|
272
|
+
const fs = yield* FileSystem.FileSystem;
|
|
273
|
+
const path = yield* Path.Path;
|
|
274
|
+
const installed = yield* manifestVersion(
|
|
275
|
+
path.join(root, "node_modules", ...library.package.split("/"), "package.json"),
|
|
276
|
+
`node_modules/${library.package}/package.json is missing, so the installed version of ${library.package} is unknown; install first`,
|
|
277
|
+
);
|
|
278
|
+
const tag = tagFor(library.tag, installed);
|
|
279
|
+
const dir = path.join(cache, LINKS, ...remoteSegments(library.repository), tag);
|
|
280
|
+
if (!(yield* fs.exists(dir)) && (yield* land(library, installed, tag, dir))) {
|
|
281
|
+
yield* Console.error(`${NAME}: cloned ${tag} from ${library.repository} and linked ${LINKS}/${library.name}`);
|
|
282
|
+
} else {
|
|
283
|
+
yield* verify(dir, library, installed, tag);
|
|
284
|
+
yield* Console.error(`${NAME}: ${LINKS}/${library.name} still holds ${tag}, verified against its recorded commit`);
|
|
285
|
+
}
|
|
286
|
+
yield* ensureLink(root, library, dir);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const cacheRoot = Effect.fn("cacheRoot")(function* () {
|
|
290
|
+
const path = yield* Path.Path;
|
|
291
|
+
const home = process.env.HOME;
|
|
292
|
+
if (home === undefined || home === "") {
|
|
293
|
+
return yield* new VendorError({ message: "HOME is missing, so the shared cache has no root" });
|
|
294
|
+
}
|
|
295
|
+
return path.join(home, CACHE_HOME);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const main = Effect.gen(function* () {
|
|
299
|
+
const [extra] = process.argv.slice(2);
|
|
300
|
+
if (extra !== undefined) return yield* new Usage({ message: USAGE });
|
|
301
|
+
const toplevel = yield* git(["rev-parse", "--show-toplevel"]).pipe(
|
|
302
|
+
Effect.map((output) => Option.some(output.trim())),
|
|
303
|
+
Effect.catchTag("GitFailure", (failure) =>
|
|
304
|
+
Console.error(`${NAME}: ${failure.message}, so no checkout holds ${LINKS}/ and readers stay on node_modules`).pipe(
|
|
305
|
+
Effect.as(Option.none<string>()),
|
|
306
|
+
),
|
|
307
|
+
),
|
|
308
|
+
);
|
|
309
|
+
if (Option.isNone(toplevel)) return true;
|
|
310
|
+
const root = toplevel.value;
|
|
311
|
+
const { quality } = yield* readQuality(root);
|
|
312
|
+
const libraries = quality.sources?.libraries ?? [];
|
|
313
|
+
if (libraries.length === 0) {
|
|
314
|
+
yield* Console.error(`${NAME}: ${QUALITY_FILE} declares no libraries, so nothing is pinned`);
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
const cache = yield* cacheRoot();
|
|
318
|
+
let passed = true;
|
|
319
|
+
for (const library of libraries) {
|
|
320
|
+
const vended = yield* vend(root, cache, library).pipe(
|
|
321
|
+
Effect.as(true),
|
|
322
|
+
Effect.catchTags({
|
|
323
|
+
VendorError: (failure) =>
|
|
324
|
+
Console.error(`${NAME}: ${failure.message}`).pipe(Effect.andThen(unlink(root, library)), Effect.as(false)),
|
|
325
|
+
Unreachable: (failure) =>
|
|
326
|
+
Console.error(`${NAME}: ${failure.message}, so ${LINKS}/${library.name} stays unlinked until a run can fetch it`).pipe(
|
|
327
|
+
Effect.andThen(unlink(root, library)),
|
|
328
|
+
Effect.as(true),
|
|
329
|
+
),
|
|
330
|
+
}),
|
|
331
|
+
);
|
|
332
|
+
passed = passed && vended;
|
|
333
|
+
}
|
|
334
|
+
return passed;
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
if (import.meta.main) runMain(NAME, main);
|