@antelopejs/tooling-configs 0.2.1 → 0.2.3
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/README.md +26 -5
- package/dist/bin/api-diff.d.mts +1 -0
- package/dist/bin/api-diff.mjs +360 -0
- package/dist/bin/check-size.mjs +2 -2
- package/dist/bin/checks.mjs +8 -5
- package/dist/bin/comment-proof.d.mts +1 -0
- package/dist/bin/comment-proof.mjs +43 -0
- package/dist/bin/ratchet.mjs +14 -7
- package/dist/bin/sync.mjs +138 -20
- package/dist/{ceilings--pJmhSde.mjs → ceilings-BSw9p4YB.mjs} +5 -1
- package/dist/files-jKlueTMd.mjs +298 -0
- package/dist/oxc/fmt.mjs +1 -1
- package/dist/oxc/lint.mjs +4 -3
- package/dist/{package-B1aipTbn.mjs → package-Ckd7e4tA.mjs} +4 -0
- package/dist/{ratchet-B2-4XxAl.mjs → ratchet-CNbpU42F.mjs} +18 -12
- package/dist/renames-BGO4dWJ8.mjs +59 -0
- package/dist/{shared-NC0T8ykm.mjs → shared-CsXIMK3I.mjs} +4 -2
- package/dist/{size-DJ7YSry_.mjs → size-BdYmv57u.mjs} +4 -3
- package/dist/typescript-aLCrMi2y.mjs +120 -0
- package/package.json +4 -3
- package/dist/files-Bocv-qA8.mjs +0 -103
package/dist/bin/sync.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as formatCeilings, t as CEILINGS_FILE } from "../ceilings
|
|
3
|
-
import {
|
|
4
|
-
import { a as repositoryTools, i as parseManifest, n as managedPackage, o as toolingVersions, r as packageManager, t as execCommand } from "../package-
|
|
5
|
-
import { t as measureBaseline } from "../ratchet-
|
|
2
|
+
import { a as formatCeilings, o as isRetired, s as readCeilings, t as CEILINGS_FILE } from "../ceilings-BSw9p4YB.mjs";
|
|
3
|
+
import { i as repositoryRoot, u as runCli } from "../files-jKlueTMd.mjs";
|
|
4
|
+
import { a as repositoryTools, i as parseManifest, n as managedPackage, o as toolingVersions, r as packageManager, t as execCommand } from "../package-Ckd7e4tA.mjs";
|
|
5
|
+
import { t as measureBaseline } from "../ratchet-CNbpU42F.mjs";
|
|
6
6
|
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { dirname, join, resolve } from "node:path";
|
|
8
|
+
import { execFileSync } from "node:child_process";
|
|
8
9
|
import { parseArgs } from "node:util";
|
|
9
10
|
import { isMap, isScalar, isSeq, parseDocument } from "yaml";
|
|
10
11
|
//#region src/guardrails/agents-block.ts
|
|
@@ -12,7 +13,11 @@ const AGENTS_BLOCK_START = "<!-- antelopejs-guardrails:start -->";
|
|
|
12
13
|
const AGENTS_BLOCK_END = "<!-- antelopejs-guardrails:end -->";
|
|
13
14
|
const COMMENTS = `### Comments
|
|
14
15
|
|
|
15
|
-
Say it in code. A constraint belongs in a type, a lint rule, or a test. A public item's contract belongs in TSDoc (\`/** */\`) on that item. The only other comments are lint directives that name their rules, \`@ts-expect-error <reason>\`, \`SAFETY: <reason>\` above a type assertion, \`/*!\` license headers, and bundler or formatter directives
|
|
16
|
+
Say it in code. A constraint belongs in a type, a lint rule, or a test. A public item's contract belongs in TSDoc (\`/** */\`) on that item. The only other comments are lint directives that name their rules, \`@ts-expect-error <reason>\`, \`SAFETY: <reason>\` above a type assertion, \`/*!\` license headers, and bundler or formatter directives.
|
|
17
|
+
|
|
18
|
+
Before suppressing a type rule, try inference or a type derived from its owner with \`Parameters\`, \`ReturnType\`, or indexed access. A private alias can still appear in a published declaration. Keep a suppression only after checking the published contract and the unchecked boundary it protects.
|
|
19
|
+
|
|
20
|
+
Check each directive reason against the current line and its callers. Returning a value is not the same as awaiting and discarding it. A reason must describe what the code does now.`;
|
|
16
21
|
const TESTS = `### Tests
|
|
17
22
|
|
|
18
23
|
Hold every test you write or keep to this checklist:
|
|
@@ -20,6 +25,15 @@ Hold every test you write or keep to this checklist:
|
|
|
20
25
|
- Name the wrong implementation this test fails on. A test with no answer is deleted, not kept.
|
|
21
26
|
- No test reads the file it tests. Drive the behaviour through the public interface.
|
|
22
27
|
- An invariant over many files is a lint rule, not a test.`;
|
|
28
|
+
const CLEANUP_PROOF = `### Cleanup proof
|
|
29
|
+
|
|
30
|
+
A behaviour-preserving cleanup PR shows \`antelopejs-api-diff <base-ref>\` passing. Published declarations and built runtime exports stay unchanged. A comment-only PR shows \`antelopejs-comment-proof <base-ref>\` passing instead of a verifier. Both compare committed HEAD.
|
|
31
|
+
|
|
32
|
+
Copy counts from the proof's base and HEAD table. Never type counts by hand. Keep the command and the committed head beside the copied output. Directive lines are not individual suppressed rules.
|
|
33
|
+
|
|
34
|
+
Pass \`--probe <file.cjs>\` to api-diff when changing receivers, positional calls, or modules that no published entry reaches. The same captured script runs against both worktree roots, supplied as \`argv[2]\`, and prints JSON observations. Matching metadata alone does not prove those behaviours.
|
|
35
|
+
|
|
36
|
+
An entry-only rule exempts only its own module. Add a second module entry rather than exempting another importer. A no-upward rule must reject peers in the same tier as well as higher tiers. Probe the rank decision with an import, not just a reading of the configuration.`;
|
|
23
37
|
const SHARED_RULES = [
|
|
24
38
|
["Formatting", "oxfmt"],
|
|
25
39
|
["Lint findings are errors; no rule warns", "oxlint preset"],
|
|
@@ -28,14 +42,20 @@ const SHARED_RULES = [
|
|
|
28
42
|
["Tests read no files outside fixtures, helpers, locales", "`antelopejs/no-fs-read-in-tests`"],
|
|
29
43
|
["No assertion that restates a constant", "`antelopejs/no-constant-value-assertion`"],
|
|
30
44
|
["No test whose only check is \"is a function\"", "`antelopejs/no-typeof-function-only-test`"],
|
|
31
|
-
["Params ≤ 5, function ≤ 120 lines, file ≤ 500 lines, depth ≤ 4, complexity ≤ 20", "`eslint/max-*`, `eslint/complexity`"]
|
|
45
|
+
["Params ≤ 5, function ≤ 120 lines, file ≤ 500 lines, depth ≤ 4, complexity ≤ 20", "`eslint/max-*`, `eslint/complexity`"],
|
|
46
|
+
["Cleanup preserves declarations and built runtime export metadata", "`antelopejs-api-diff <base-ref>`"],
|
|
47
|
+
["Comment-only changes preserve JavaScript emit", "`antelopejs-comment-proof <base-ref>`"],
|
|
48
|
+
["PR-body counts come from tracked code files", "api-diff and comment-proof tables"],
|
|
49
|
+
["Receiver, positional and unpublished-module behaviour", "`antelopejs-api-diff <base-ref> --probe <file.cjs>`"],
|
|
50
|
+
["Type suppressions and directive reasons are justified", "review; no tool"],
|
|
51
|
+
["No-upward rules reject same-tier peers", "review and import probes"]
|
|
32
52
|
];
|
|
33
53
|
const FULL_RULES = [
|
|
34
54
|
["Files ≤ 20,000 bytes, tests included", "`antelopejs-check-size` (pre-commit)"],
|
|
35
|
-
["Existing debt only shrinks", "`antelopejs-ratchet`"],
|
|
36
55
|
["Types check", "`tsc` (pre-push)"],
|
|
37
56
|
["No dead files, exports, or dependencies", "knip"],
|
|
38
57
|
["Managed files match the pack", "`antelopejs-sync --check`"],
|
|
58
|
+
["Entry-only rules exempt no tracked outsider", "`antelopejs-sync --check`"],
|
|
39
59
|
["A test names the wrong implementation it catches", "review; no tool"]
|
|
40
60
|
];
|
|
41
61
|
/** Pads each column the way oxfmt does, so formatting AGENTS.md never reports sync drift. */
|
|
@@ -62,7 +82,7 @@ function fullBody(context) {
|
|
|
62
82
|
return [
|
|
63
83
|
"## Guardrails",
|
|
64
84
|
"",
|
|
65
|
-
|
|
85
|
+
`Finish every change with \`pnpm run checks\`. It runs typecheck, lint, format, knip, tests, size, ${context.ratchet ? "ratchet, " : ""}and sync, and prints only what failed. The change is done when it prints no failure.`,
|
|
66
86
|
"",
|
|
67
87
|
...pointers(context).flatMap((line) => [line, ""]),
|
|
68
88
|
"The tools enforce most rules, listed in the table at the end. The sections below hold the rules no tool checks.",
|
|
@@ -71,11 +91,19 @@ function fullBody(context) {
|
|
|
71
91
|
"",
|
|
72
92
|
TESTS,
|
|
73
93
|
"",
|
|
74
|
-
|
|
94
|
+
CLEANUP_PROOF,
|
|
75
95
|
"",
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
96
|
+
...context.ratchet ? [
|
|
97
|
+
"### Debt",
|
|
98
|
+
"",
|
|
99
|
+
"`guardrails.ratchet.json` records the lint findings and oversize files this repository already had. Its numbers only go down. After you remove debt, run `pnpm run ratchet:update` and commit the lowered file. A rule listed there is off for `oxlint`. The ratchet counts it instead, and the pre-commit hook rejects it on every line you add.",
|
|
100
|
+
""
|
|
101
|
+
] : [],
|
|
102
|
+
ruleTable([
|
|
103
|
+
...SHARED_RULES,
|
|
104
|
+
...FULL_RULES,
|
|
105
|
+
...context.ratchet ? [["Existing debt only shrinks", "`antelopejs-ratchet`"]] : []
|
|
106
|
+
])
|
|
79
107
|
];
|
|
80
108
|
}
|
|
81
109
|
function templateBody(context) {
|
|
@@ -89,6 +117,8 @@ function templateBody(context) {
|
|
|
89
117
|
"",
|
|
90
118
|
TESTS,
|
|
91
119
|
"",
|
|
120
|
+
CLEANUP_PROOF,
|
|
121
|
+
"",
|
|
92
122
|
ruleTable(SHARED_RULES)
|
|
93
123
|
];
|
|
94
124
|
}
|
|
@@ -140,7 +170,7 @@ function presetConfig(current, preset, tools) {
|
|
|
140
170
|
const importLine = preset.content.split("\n").filter((line) => line.includes(preset.specifier)).join("\n").replace(preset.specifier, target);
|
|
141
171
|
throw new Error(`${preset.path} does not import its preset. Add \`${importLine}\` and extend that preset, then rerun antelopejs-sync. The file was not replaced.`);
|
|
142
172
|
}
|
|
143
|
-
function lefthook(current, manager, tools) {
|
|
173
|
+
function lefthook(current, manager, tools, ratchet = true) {
|
|
144
174
|
const exec = execCommand(manager);
|
|
145
175
|
const document = parseDocument(current ?? "{}\n");
|
|
146
176
|
if (document.errors.length) throw new Error(`lefthook.yml: ${document.errors.map(({ message }) => message).join("; ")}`);
|
|
@@ -176,6 +206,11 @@ function lefthook(current, manager, tools) {
|
|
|
176
206
|
key
|
|
177
207
|
], document.createNode(command));
|
|
178
208
|
}
|
|
209
|
+
if (!ratchet) document.deleteIn([
|
|
210
|
+
"pre-commit",
|
|
211
|
+
"commands",
|
|
212
|
+
"antelopejs-ratchet"
|
|
213
|
+
]);
|
|
179
214
|
return document.toString({ lineWidth: 0 });
|
|
180
215
|
}
|
|
181
216
|
const SETUP_SCRIPT = `#!/usr/bin/env bash
|
|
@@ -239,8 +274,79 @@ function hasChecksWorkflow(root) {
|
|
|
239
274
|
return false;
|
|
240
275
|
}
|
|
241
276
|
//#endregion
|
|
277
|
+
//#region src/guardrails/entry-only.ts
|
|
278
|
+
const LOAD_RULES = `
|
|
279
|
+
const advisories = [];
|
|
280
|
+
const rules = [];
|
|
281
|
+
const SUPPORTED = { from: ["pathNot"], to: ["path", "pathNot"] };
|
|
282
|
+
function patterns(value) {
|
|
283
|
+
if (value === undefined) return [];
|
|
284
|
+
const list = Array.isArray(value) ? value : [value];
|
|
285
|
+
if (list.some(pattern => typeof pattern !== "string")) return null;
|
|
286
|
+
try { list.forEach(pattern => new RegExp(pattern)); } catch { return null; }
|
|
287
|
+
return list;
|
|
288
|
+
}
|
|
289
|
+
function problem(rule) {
|
|
290
|
+
if (!rule.from || typeof rule.from !== "object" || !rule.to || typeof rule.to !== "object")
|
|
291
|
+
return "Warning: entry-only rule " + rule.name + " has no from and to selectors, so its exemptions cannot be checked.";
|
|
292
|
+
const unsupported = [
|
|
293
|
+
...(rule.severity === "error" ? [] : ["severity " + String(rule.severity)]),
|
|
294
|
+
...Object.keys(rule.from).filter(key => !SUPPORTED.from.includes(key)).map(key => "from." + key),
|
|
295
|
+
...Object.keys(rule.to).filter(key => !SUPPORTED.to.includes(key)).map(key => "to." + key),
|
|
296
|
+
];
|
|
297
|
+
if (unsupported.length)
|
|
298
|
+
return "Note: entry-only rule " + rule.name + " skipped, because its selectors are not supported (" + unsupported.join(", ") + "); only error rules built from from.pathNot and to.path or to.pathNot patterns are checked.";
|
|
299
|
+
if (rule.to.path === undefined)
|
|
300
|
+
return "Warning: entry-only rule " + rule.name + " has no to.path, so there is no module to exempt against.";
|
|
301
|
+
if (patterns(rule.from.pathNot) === null || !patterns(rule.to.path)?.length || patterns(rule.to.pathNot) === null)
|
|
302
|
+
return "Warning: entry-only rule " + rule.name + " has selectors that are not valid regular expression strings, so its exemptions cannot be checked.";
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
const forbidden = require(process.cwd()+"/.dependency-cruiser.cjs").forbidden;
|
|
307
|
+
if (!Array.isArray(forbidden)) advisories.push("Warning: .dependency-cruiser.cjs has no forbidden rule array, so entry-only exemptions are not checked.");
|
|
308
|
+
else for (const rule of forbidden) {
|
|
309
|
+
if (!rule || typeof rule.name !== "string" || !rule.name.endsWith("-entry-only")) continue;
|
|
310
|
+
const advisory = problem(rule);
|
|
311
|
+
if (advisory === undefined) rules.push({ name: rule.name, exempt: patterns(rule.from.pathNot), domain: patterns(rule.to.path) });
|
|
312
|
+
else advisories.push(advisory);
|
|
313
|
+
}
|
|
314
|
+
} catch (error) {
|
|
315
|
+
advisories.push("Warning: .dependency-cruiser.cjs could not be loaded, so entry-only exemptions are not checked (" + String(error && error.message) + ").");
|
|
316
|
+
}
|
|
317
|
+
process.stdout.write(JSON.stringify({ rules, advisories }));
|
|
318
|
+
`;
|
|
319
|
+
function matcher(patterns) {
|
|
320
|
+
return patterns.map((pattern) => new RegExp(pattern));
|
|
321
|
+
}
|
|
322
|
+
/** Rejects tracked importers exempted from another module's entry-only boundary. A rule it cannot interpret yields a printed note or warning in `advisories` and never a failure. */
|
|
323
|
+
function checkEntryOnlyConfiguration(root) {
|
|
324
|
+
if (!existsSync(join(root, ".dependency-cruiser.cjs"))) return {
|
|
325
|
+
errors: [],
|
|
326
|
+
advisories: []
|
|
327
|
+
};
|
|
328
|
+
const { rules, advisories } = JSON.parse(execFileSync(process.execPath, ["-e", LOAD_RULES], {
|
|
329
|
+
cwd: root,
|
|
330
|
+
encoding: "utf8",
|
|
331
|
+
timeout: 1e4
|
|
332
|
+
}));
|
|
333
|
+
const paths = execFileSync("git", ["ls-files", "-z"], {
|
|
334
|
+
cwd: root,
|
|
335
|
+
encoding: "utf8"
|
|
336
|
+
}).split("\0").filter(Boolean);
|
|
337
|
+
return {
|
|
338
|
+
errors: rules.flatMap((rule) => {
|
|
339
|
+
const domain = matcher(rule.domain);
|
|
340
|
+
const exemption = matcher(rule.exempt);
|
|
341
|
+
return paths.filter((path) => exemption.some((pattern) => pattern.test(path)) && !domain.some((pattern) => pattern.test(path))).map((path) => `${rule.name} exempts ${path} outside its module; give the module a second entry instead`);
|
|
342
|
+
}),
|
|
343
|
+
advisories
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
//#endregion
|
|
242
347
|
//#region src/guardrails/sync.ts
|
|
243
348
|
const LEGACY_COMMENT_INSTRUCTION = "- Explain non-obvious constraints and decisions in comments; avoid restating the code. Document public contracts with TSDoc.";
|
|
349
|
+
const RETIRED_RATCHET = `${CEILINGS_FILE} has no debt left. Run ratchet:update to delete it, then run antelopejs-sync.`;
|
|
244
350
|
function managedAgents(current = "", context) {
|
|
245
351
|
const start = current.indexOf(AGENTS_BLOCK_START);
|
|
246
352
|
const end = current.indexOf(AGENTS_BLOCK_END);
|
|
@@ -273,7 +379,7 @@ const MANAGED_ARTIFACTS = [
|
|
|
273
379
|
path: "lefthook.yml",
|
|
274
380
|
profiles: ["full"],
|
|
275
381
|
desired: (current, context) => ({
|
|
276
|
-
content: lefthook(current, context.manager, context.tools),
|
|
382
|
+
content: lefthook(current, context.manager, context.tools, context.ratchet),
|
|
277
383
|
notes: []
|
|
278
384
|
})
|
|
279
385
|
},
|
|
@@ -333,6 +439,7 @@ function planSync(current, modes, context) {
|
|
|
333
439
|
});
|
|
334
440
|
for (const artifact of MANAGED_ARTIFACTS) {
|
|
335
441
|
if (!artifact.profiles.includes(context.profile)) continue;
|
|
442
|
+
if (artifact.path === "guardrails.ratchet.json" && !context.ratchet) continue;
|
|
336
443
|
try {
|
|
337
444
|
const { content, notes } = artifact.desired(current.get(artifact.path), context);
|
|
338
445
|
const changed = current.get(artifact.path) !== content || artifact.mode !== void 0 && (artifact.mode & 73) !== 0 && ((modes.get(artifact.path) ?? 0) & 73) === 0;
|
|
@@ -407,6 +514,7 @@ function repositoryContext(root, current, profile) {
|
|
|
407
514
|
const desiredLint = current.get(lint.path) ?? lint.content;
|
|
408
515
|
return {
|
|
409
516
|
profile,
|
|
517
|
+
ratchet: current.has(CEILINGS_FILE),
|
|
410
518
|
manager: packageManager(manifest),
|
|
411
519
|
versions: toolingVersions(),
|
|
412
520
|
tools: repositoryTools(manifest),
|
|
@@ -427,6 +535,13 @@ function writeArtifacts(root, artifacts) {
|
|
|
427
535
|
if (artifact.mode !== void 0) chmodSync(path, artifact.mode);
|
|
428
536
|
}
|
|
429
537
|
}
|
|
538
|
+
function repositoryNotices(root, context) {
|
|
539
|
+
const notices = [];
|
|
540
|
+
if (context.profile === "full" && !hasChecksWorkflow(root)) notices.push(MISSING_CHECKS_WORKFLOW);
|
|
541
|
+
if (context.ratchet && isRetired(readCeilings(root))) notices.push(RETIRED_RATCHET);
|
|
542
|
+
return notices;
|
|
543
|
+
}
|
|
544
|
+
/** Measures debt only on first adoption, before sync has written the sync:check script; retired repositories stay retired. */
|
|
430
545
|
function syncRepository(root, options = {}) {
|
|
431
546
|
const profile = options.profile ?? "full";
|
|
432
547
|
const { current, modes } = repositorySnapshot(root);
|
|
@@ -434,15 +549,16 @@ function syncRepository(root, options = {}) {
|
|
|
434
549
|
const context = repositoryContext(root, current, profile);
|
|
435
550
|
let plan = planSync(current, modes, context);
|
|
436
551
|
const errors = plan.filter((artifact) => artifact.kind === "error").map(({ message }) => message);
|
|
552
|
+
const entryOnly = checkEntryOnlyConfiguration(root);
|
|
553
|
+
errors.push(...entryOnly.errors);
|
|
437
554
|
const notes = plan.flatMap((artifact) => artifact.kind === "migrated" ? artifact.notes : []);
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
else notes.push(MISSING_CHECKS_WORKFLOW);
|
|
441
|
-
}
|
|
442
|
-
if (!errors.length && profile === "full" && !options.check && !current.has("guardrails.ratchet.json")) {
|
|
555
|
+
(options.check ? errors : notes).push(...repositoryNotices(root, context));
|
|
556
|
+
if (!errors.length && profile === "full" && !options.check && !current.has("guardrails.ratchet.json") && manifest.scripts?.["sync:check"] === void 0) {
|
|
443
557
|
const lint = PRESET_CONFIGS[0];
|
|
444
558
|
const desired = presetConfig(current.get(lint.path), lint, context.tools);
|
|
445
|
-
|
|
559
|
+
const baseline = measureBaseline(root, desired === current.get(lint.path) ? void 0 : desired);
|
|
560
|
+
context.ratchet = !isRetired(baseline);
|
|
561
|
+
context.baseline = formatCeilings(baseline);
|
|
446
562
|
plan = planSync(current, modes, context);
|
|
447
563
|
}
|
|
448
564
|
const writes = plan.filter((artifact) => artifact.kind === "write" || artifact.kind === "migrated");
|
|
@@ -455,6 +571,7 @@ function syncRepository(root, options = {}) {
|
|
|
455
571
|
paths: errors.length ? [] : writes.map(({ path }) => path),
|
|
456
572
|
errors,
|
|
457
573
|
notes,
|
|
574
|
+
advisories: entryOnly.advisories,
|
|
458
575
|
dependenciesAdded
|
|
459
576
|
};
|
|
460
577
|
}
|
|
@@ -474,6 +591,7 @@ runCli(() => {
|
|
|
474
591
|
profile: values.profile
|
|
475
592
|
});
|
|
476
593
|
for (const note of result.notes) process.stdout.write(`Note: ${values.check || result.errors.length ? "Planned migration. " : ""}${note}\n`);
|
|
594
|
+
for (const advisory of result.advisories) process.stdout.write(`${advisory}\n`);
|
|
477
595
|
for (const error of result.errors) process.stderr.write(`${error}\n`);
|
|
478
596
|
if (result.errors.length) return 1;
|
|
479
597
|
for (const path of result.paths) process.stdout.write(`${values.check ? "Drift" : "Updated"}: ${path}${values.check ? "; run antelopejs-sync" : ""}\n`);
|
|
@@ -14,6 +14,10 @@ const EMPTY_CEILINGS = {
|
|
|
14
14
|
rules: {},
|
|
15
15
|
oversizeFiles: {}
|
|
16
16
|
};
|
|
17
|
+
/** A repository retires its ratchet once no lint or file debt remains. */
|
|
18
|
+
function isRetired(ceilings) {
|
|
19
|
+
return Object.values(ceilings.rules).every((count) => count === 0) && Object.keys(ceilings.oversizeFiles).length === 0;
|
|
20
|
+
}
|
|
17
21
|
/** Finds the nearest git directory or worktree marker, falling back to cwd outside git. */
|
|
18
22
|
function findGitRoot(cwd = process.cwd()) {
|
|
19
23
|
let directory = resolve(cwd);
|
|
@@ -61,4 +65,4 @@ function formatCeilings(ceilings) {
|
|
|
61
65
|
return `${JSON.stringify(sorted, null, 2)}\n`;
|
|
62
66
|
}
|
|
63
67
|
//#endregion
|
|
64
|
-
export { formatCeilings as a, findGitRoot as i, MAX_FILE_BYTES as n,
|
|
68
|
+
export { formatCeilings as a, findGitRoot as i, MAX_FILE_BYTES as n, isRetired as o, MEASURE_ENV as r, readCeilings as s, CEILINGS_FILE as t };
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import "./ceilings-BSw9p4YB.mjs";
|
|
2
|
+
import { n as IGNORE_PATTERNS, t as AGENT_IGNORE_PATTERNS } from "./shared-CsXIMK3I.mjs";
|
|
3
|
+
import { closeSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readSync, rmSync, symlinkSync } from "node:fs";
|
|
4
|
+
import { dirname, join, matchesGlob, relative, resolve } from "node:path";
|
|
5
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
//#region src/guardrails/cli.ts
|
|
8
|
+
function runCli(main) {
|
|
9
|
+
try {
|
|
10
|
+
process.exitCode = main();
|
|
11
|
+
} catch (error) {
|
|
12
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
13
|
+
process.exitCode = 1;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/guardrails/revisions.ts
|
|
18
|
+
function trackedManifests(root) {
|
|
19
|
+
return execFileSync("git", ["ls-files", "-z"], {
|
|
20
|
+
cwd: root,
|
|
21
|
+
encoding: "utf8"
|
|
22
|
+
}).split("\0").filter((file) => file === "package.json" || file.endsWith("/package.json"));
|
|
23
|
+
}
|
|
24
|
+
const DIRECTORY_LINK = process.platform === "win32" ? "junction" : "dir";
|
|
25
|
+
function linkDependencies(root, worktree) {
|
|
26
|
+
const directories = /* @__PURE__ */ new Set([".", ...trackedManifests(root).map(dirname)]);
|
|
27
|
+
for (const directory of directories) {
|
|
28
|
+
const source = join(root, directory, "node_modules");
|
|
29
|
+
const target = join(worktree, directory, "node_modules");
|
|
30
|
+
if (existsSync(source) && existsSync(dirname(target)) && !existsSync(target)) symlinkSync(source, target, DIRECTORY_LINK);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Compares isolated committed revisions and removes both worktrees even when compilation fails. */
|
|
34
|
+
function compareRevisions(root, baseRef, label, emit) {
|
|
35
|
+
const temporary = mkdtempSync(join(tmpdir(), "antelope-proof-"));
|
|
36
|
+
const worktrees = [];
|
|
37
|
+
const results = [];
|
|
38
|
+
try {
|
|
39
|
+
for (const [side, ref] of [["base", baseRef], ["head", "HEAD"]]) {
|
|
40
|
+
const revision = execFileSync("git", [
|
|
41
|
+
"rev-parse",
|
|
42
|
+
"--verify",
|
|
43
|
+
"--end-of-options",
|
|
44
|
+
`${ref}^{commit}`
|
|
45
|
+
], {
|
|
46
|
+
cwd: root,
|
|
47
|
+
encoding: "utf8",
|
|
48
|
+
stdio: [
|
|
49
|
+
"ignore",
|
|
50
|
+
"pipe",
|
|
51
|
+
"pipe"
|
|
52
|
+
]
|
|
53
|
+
}).trim();
|
|
54
|
+
const worktree = join(temporary, "worktrees", side);
|
|
55
|
+
execFileSync("git", [
|
|
56
|
+
"worktree",
|
|
57
|
+
"add",
|
|
58
|
+
"--quiet",
|
|
59
|
+
"--detach",
|
|
60
|
+
worktree,
|
|
61
|
+
revision
|
|
62
|
+
], {
|
|
63
|
+
cwd: root,
|
|
64
|
+
stdio: [
|
|
65
|
+
"ignore",
|
|
66
|
+
"pipe",
|
|
67
|
+
"pipe"
|
|
68
|
+
]
|
|
69
|
+
});
|
|
70
|
+
worktrees.push(worktree);
|
|
71
|
+
linkDependencies(root, worktree);
|
|
72
|
+
const output = join(temporary, side);
|
|
73
|
+
mkdirSync(output, { recursive: true });
|
|
74
|
+
results.push(emit(worktree, output, join(temporary, "emit", side)));
|
|
75
|
+
}
|
|
76
|
+
const diff = spawnSync("git", [
|
|
77
|
+
"diff",
|
|
78
|
+
"--no-index",
|
|
79
|
+
"--no-color",
|
|
80
|
+
"--no-renames",
|
|
81
|
+
"--no-ext-diff",
|
|
82
|
+
"--no-textconv",
|
|
83
|
+
"--",
|
|
84
|
+
"base",
|
|
85
|
+
"head"
|
|
86
|
+
], {
|
|
87
|
+
cwd: temporary,
|
|
88
|
+
encoding: "utf8",
|
|
89
|
+
maxBuffer: 67108864
|
|
90
|
+
});
|
|
91
|
+
if (diff.error) throw diff.error;
|
|
92
|
+
if (diff.status !== 0 && diff.status !== 1) throw new Error(diff.stderr || "Could not compare revision outputs.");
|
|
93
|
+
const packages = new Set(results.flatMap((result) => result.packages)).size;
|
|
94
|
+
return {
|
|
95
|
+
different: diff.status === 1,
|
|
96
|
+
output: diff.status === 1 ? `${label}: differences against ${baseRef} (committed HEAD).\n${diff.stdout}` : `${label}: ${packages} packages, ${results[1].entries} entries unchanged against ${baseRef} (committed HEAD).\n`
|
|
97
|
+
};
|
|
98
|
+
} finally {
|
|
99
|
+
for (const worktree of worktrees.reverse()) execFileSync("git", [
|
|
100
|
+
"worktree",
|
|
101
|
+
"remove",
|
|
102
|
+
"--force",
|
|
103
|
+
worktree
|
|
104
|
+
], {
|
|
105
|
+
cwd: root,
|
|
106
|
+
stdio: "pipe"
|
|
107
|
+
});
|
|
108
|
+
rmSync(temporary, {
|
|
109
|
+
recursive: true,
|
|
110
|
+
force: true
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const COUNTS = [
|
|
115
|
+
["oxlint-disable / eslint-disable directive lines", /^\s*(?:\/\/|\/\*|\*)\s*(?:oxlint|eslint)-disable\b/u],
|
|
116
|
+
["SAFETY: lines", /^\s*(?:\/\/|\/\*|\*)\s*SAFETY:/u],
|
|
117
|
+
["@ts-expect-error lines", /^\s*(?:\/\/|\/\*|\*)\s*@ts-expect-error\b/u]
|
|
118
|
+
];
|
|
119
|
+
const CODE_PATHSPECS = [
|
|
120
|
+
"*.ts",
|
|
121
|
+
"*.tsx",
|
|
122
|
+
"*.mts",
|
|
123
|
+
"*.cts",
|
|
124
|
+
"*.js",
|
|
125
|
+
"*.jsx",
|
|
126
|
+
"*.mjs",
|
|
127
|
+
"*.cjs",
|
|
128
|
+
"*.vue"
|
|
129
|
+
];
|
|
130
|
+
function countsAt(root, ref) {
|
|
131
|
+
const result = spawnSync("git", [
|
|
132
|
+
"grep",
|
|
133
|
+
"-h",
|
|
134
|
+
"-I",
|
|
135
|
+
"-E",
|
|
136
|
+
"-e",
|
|
137
|
+
"(oxlint|eslint)-disable|SAFETY:|@ts-expect-error",
|
|
138
|
+
ref,
|
|
139
|
+
"--",
|
|
140
|
+
...CODE_PATHSPECS
|
|
141
|
+
], {
|
|
142
|
+
cwd: root,
|
|
143
|
+
encoding: "utf8",
|
|
144
|
+
maxBuffer: 67108864
|
|
145
|
+
});
|
|
146
|
+
if (result.error) throw result.error;
|
|
147
|
+
if (result.status !== 0 && result.status !== 1) throw new Error(result.stderr);
|
|
148
|
+
const lines = result.stdout.split("\n");
|
|
149
|
+
return COUNTS.map(([, pattern]) => lines.filter((line) => pattern.test(line)).length);
|
|
150
|
+
}
|
|
151
|
+
/** Counts directive, `SAFETY:` and `@ts-expect-error` lines in every tracked code file at the base and at committed HEAD, so a PR body copies them instead of typing them. */
|
|
152
|
+
function revisionCounts(root, baseRef) {
|
|
153
|
+
const base = countsAt(root, baseRef);
|
|
154
|
+
const head = countsAt(root, "HEAD");
|
|
155
|
+
return `\nCounts in tracked code files (${baseRef} and committed HEAD).\n| Lines | base | HEAD |\n| --- | ---: | ---: |\n${COUNTS.map(([label], index) => `| ${label} | ${base[index]} | ${head[index]} |`).join("\n")}\n`;
|
|
156
|
+
}
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region src/guardrails/files.ts
|
|
159
|
+
const SIZE_EXCLUSIONS = [
|
|
160
|
+
{
|
|
161
|
+
kind: "generated",
|
|
162
|
+
patterns: [
|
|
163
|
+
...IGNORE_PATTERNS,
|
|
164
|
+
...AGENT_IGNORE_PATTERNS,
|
|
165
|
+
"**/*.generated.*",
|
|
166
|
+
"**/generated/**",
|
|
167
|
+
"**/__snapshots__/**"
|
|
168
|
+
]
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
kind: "fixtures",
|
|
172
|
+
patterns: ["**/fixtures/**", "**/__fixtures__/**"]
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
kind: "data",
|
|
176
|
+
patterns: [
|
|
177
|
+
"**/locales/**/*.json",
|
|
178
|
+
"**/i18n/**/*.json",
|
|
179
|
+
"**/blocks.json",
|
|
180
|
+
"**/schema.json",
|
|
181
|
+
"**/evals/**/cases.json"
|
|
182
|
+
]
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
kind: "lockfiles",
|
|
186
|
+
patterns: [
|
|
187
|
+
"**/{pnpm-lock.yaml,package-lock.json,npm-shrinkwrap.json,yarn.lock,bun.lock,bun.lockb,Cargo.lock,poetry.lock,uv.lock,Gemfile.lock,composer.lock,go.sum}",
|
|
188
|
+
"**/*.lock",
|
|
189
|
+
"**/*.lockb"
|
|
190
|
+
]
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
kind: "documentation",
|
|
194
|
+
patterns: ["**/CHANGELOG.md", "**/*.{md,mdx,markdown,MD}"]
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
kind: "assets",
|
|
198
|
+
patterns: ["**/*.{png,jpg,jpeg,gif,webp,avif,ico,svg,bmp,tiff,pdf,mp3,wav,ogg,flac,mp4,webm,mov,avi,woff,woff2,ttf,otf,eot,zip,gz,tgz,bz2,xz,7z,tar,rar,wasm,bin,exe,dll,so,dylib,db,sqlite,sqlite3,icns}"]
|
|
199
|
+
}
|
|
200
|
+
];
|
|
201
|
+
function gitFiles(root) {
|
|
202
|
+
return [...new Set(execFileSync("git", [
|
|
203
|
+
"ls-files",
|
|
204
|
+
"--cached",
|
|
205
|
+
"--others",
|
|
206
|
+
"--exclude-standard",
|
|
207
|
+
"-z"
|
|
208
|
+
], {
|
|
209
|
+
cwd: root,
|
|
210
|
+
encoding: "utf8"
|
|
211
|
+
}).split("\0").filter(Boolean))];
|
|
212
|
+
}
|
|
213
|
+
function repositoryRoot(cwd = process.cwd()) {
|
|
214
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
215
|
+
cwd,
|
|
216
|
+
encoding: "utf8"
|
|
217
|
+
}).trim();
|
|
218
|
+
}
|
|
219
|
+
/** Resolves a user-supplied revision to a commit id, so it can never read as an option. */
|
|
220
|
+
function resolveRevision(root, revision) {
|
|
221
|
+
return execFileSync("git", [
|
|
222
|
+
"rev-parse",
|
|
223
|
+
"--verify",
|
|
224
|
+
"--end-of-options",
|
|
225
|
+
`${revision}^{commit}`
|
|
226
|
+
], {
|
|
227
|
+
cwd: root,
|
|
228
|
+
encoding: "utf8",
|
|
229
|
+
stdio: [
|
|
230
|
+
"ignore",
|
|
231
|
+
"pipe",
|
|
232
|
+
"pipe"
|
|
233
|
+
]
|
|
234
|
+
}).trim();
|
|
235
|
+
}
|
|
236
|
+
function repositoryPath(root, file) {
|
|
237
|
+
return relative(root, resolve(root, file)).split("\\").join("/");
|
|
238
|
+
}
|
|
239
|
+
function fileBytes(root, file, staged = false) {
|
|
240
|
+
if (staged) {
|
|
241
|
+
const result = spawnSync("git", [
|
|
242
|
+
"cat-file",
|
|
243
|
+
"-s",
|
|
244
|
+
`:${file}`
|
|
245
|
+
], {
|
|
246
|
+
cwd: root,
|
|
247
|
+
encoding: "utf8"
|
|
248
|
+
});
|
|
249
|
+
if (result.error) throw result.error;
|
|
250
|
+
return result.status === 0 ? Number(result.stdout.trim()) : void 0;
|
|
251
|
+
}
|
|
252
|
+
const path = join(root, file);
|
|
253
|
+
if (!existsSync(path)) return void 0;
|
|
254
|
+
const stat = lstatSync(path);
|
|
255
|
+
return stat.isFile() ? stat.size : void 0;
|
|
256
|
+
}
|
|
257
|
+
/** A package root is the directory of a tracked manifest outside every `src` directory; its top-level `build` and `output` are generated output. */
|
|
258
|
+
function packageOutputPatterns(root) {
|
|
259
|
+
return trackedManifests(root).flatMap((manifest) => {
|
|
260
|
+
const directory = dirname(manifest);
|
|
261
|
+
if (directory === "." || directory.split("/").includes("src")) return [];
|
|
262
|
+
return [`${directory}/build/**`, `${directory}/output/**`];
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
/** `path.matchesGlob` never lets `*` or `**` cross a segment starting with a dot, so each path is also tried with those dots removed. */
|
|
266
|
+
function isExcluded(file, packageOutputs) {
|
|
267
|
+
const undotted = file.replace(/(^|\/)\./gu, "$1");
|
|
268
|
+
const matches = (pattern) => matchesGlob(file, pattern) || matchesGlob(undotted, pattern);
|
|
269
|
+
return SIZE_EXCLUSIONS.some(({ patterns }) => patterns.some(matches)) || packageOutputs.some(matches);
|
|
270
|
+
}
|
|
271
|
+
function isBinary(root, file, staged) {
|
|
272
|
+
if (staged) return execFileSync("git", ["show", `:${file}`], {
|
|
273
|
+
cwd: root,
|
|
274
|
+
maxBuffer: fileBytes(root, file, true)
|
|
275
|
+
}).subarray(0, 8e3).includes(0);
|
|
276
|
+
const descriptor = openSync(join(root, file), "r");
|
|
277
|
+
try {
|
|
278
|
+
const prefix = Buffer.alloc(8e3);
|
|
279
|
+
const length = readSync(descriptor, prefix, 0, prefix.length, 0);
|
|
280
|
+
return prefix.subarray(0, length).includes(0);
|
|
281
|
+
} finally {
|
|
282
|
+
closeSync(descriptor);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function authoredSizes(root, files = gitFiles(root), staged = false) {
|
|
286
|
+
const sizes = /* @__PURE__ */ new Map();
|
|
287
|
+
const packageOutputs = packageOutputPatterns(root);
|
|
288
|
+
for (const input of files) {
|
|
289
|
+
const file = repositoryPath(root, input);
|
|
290
|
+
if (isExcluded(file, packageOutputs)) continue;
|
|
291
|
+
const size = fileBytes(root, file, staged);
|
|
292
|
+
if (size !== void 0 && size > 2e4 && isBinary(root, file, staged)) continue;
|
|
293
|
+
if (size !== void 0) sizes.set(file, size);
|
|
294
|
+
}
|
|
295
|
+
return sizes;
|
|
296
|
+
}
|
|
297
|
+
//#endregion
|
|
298
|
+
export { resolveRevision as a, revisionCounts as c, repositoryRoot as i, trackedManifests as l, gitFiles as n, DIRECTORY_LINK as o, repositoryPath as r, compareRevisions as s, authoredSizes as t, runCli as u };
|
package/dist/oxc/fmt.mjs
CHANGED
package/dist/oxc/lint.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { i as findGitRoot
|
|
1
|
+
import { i as findGitRoot } from "../ceilings-BSw9p4YB.mjs";
|
|
2
|
+
import { t as readCarriedCeilings } from "../renames-BGO4dWJ8.mjs";
|
|
2
3
|
import { n as TEST_FILE_GLOBS, t as ASSERTION_FUNCTION_NAMES } from "../shared-WC0z3y9F.mjs";
|
|
3
|
-
import { n as IGNORE_PATTERNS, t as AGENT_IGNORE_PATTERNS } from "../shared-
|
|
4
|
+
import { n as IGNORE_PATTERNS, t as AGENT_IGNORE_PATTERNS } from "../shared-CsXIMK3I.mjs";
|
|
4
5
|
import { defineConfig } from "oxlint";
|
|
5
6
|
//#region src/oxc/lint.ts
|
|
6
7
|
/**
|
|
@@ -179,7 +180,7 @@ function importSortingPreset() {
|
|
|
179
180
|
/** Overrides run after rules, including test-specific rules; measurement bypasses all ceilings. */
|
|
180
181
|
function ratchetOverrides() {
|
|
181
182
|
if (process.env["ANTELOPEJS_RATCHET_MEASURE"] !== void 0) return [];
|
|
182
|
-
const ceilings =
|
|
183
|
+
const ceilings = readCarriedCeilings(findGitRoot());
|
|
183
184
|
const rules = Object.fromEntries(Object.keys(ceilings.rules).map((rule) => [rule, "off"]));
|
|
184
185
|
return [...Object.keys(ceilings.oversizeFiles).map((path) => ({
|
|
185
186
|
files: [path.replaceAll(/([*?[\]{}])/gu, "\\$1")],
|
|
@@ -101,6 +101,10 @@ function managedPackage(current, versions, options = {}) {
|
|
|
101
101
|
"ratchet:update": `${tools.commands.ratchet} --update`,
|
|
102
102
|
"sync:check": `${tools.commands.sync} --check`
|
|
103
103
|
});
|
|
104
|
+
if (options.ratchet === false) {
|
|
105
|
+
delete scripts.ratchet;
|
|
106
|
+
delete scripts["ratchet:update"];
|
|
107
|
+
}
|
|
104
108
|
for (const [name, command] of Object.entries(DEFAULT_SCRIPTS)) scripts[name] ??= name === "typecheck" && options.nuxt ? "nuxi typecheck" : command;
|
|
105
109
|
const dependencies = manifest.devDependencies ??= {};
|
|
106
110
|
dependencies.lefthook ??= versions.lefthook;
|