@antelopejs/tooling-configs 0.2.1 → 0.2.2
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 +25 -4
- package/dist/bin/api-diff.d.mts +1 -0
- package/dist/bin/api-diff.mjs +165 -0
- package/dist/bin/check-size.mjs +2 -2
- package/dist/bin/checks.mjs +7 -5
- package/dist/bin/comment-proof.d.mts +1 -0
- package/dist/bin/comment-proof.mjs +39 -0
- package/dist/bin/ratchet.mjs +6 -4
- package/dist/bin/sync.mjs +47 -19
- package/dist/{ceilings--pJmhSde.mjs → ceilings-BSw9p4YB.mjs} +5 -1
- package/dist/{files-Bocv-qA8.mjs → files-DAD5OtfV.mjs} +28 -2
- package/dist/oxc/lint.mjs +1 -1
- package/dist/{package-B1aipTbn.mjs → package-Ckd7e4tA.mjs} +4 -0
- package/dist/{ratchet-B2-4XxAl.mjs → ratchet-B2_QUucT.mjs} +14 -9
- package/dist/{size-DJ7YSry_.mjs → size-D7JJp_W4.mjs} +2 -2
- package/dist/typescript-CHOlkIXa.mjs +202 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -29,6 +29,8 @@ The AntelopeJS guardrail pack: shared lint, format and dead-code presets, the co
|
|
|
29
29
|
| bin | `antelopejs-checks` | Runs every check and prints only the failures |
|
|
30
30
|
| bin | `antelopejs-ratchet` | Fails when recorded debt grows; `--update` lowers the record |
|
|
31
31
|
| bin | `antelopejs-check-size` | Fails on authored files over 20,000 bytes and on blanket disable directives |
|
|
32
|
+
| bin | `antelopejs-api-diff <base-ref>` | Compares published declaration bundles against committed HEAD |
|
|
33
|
+
| bin | `antelopejs-comment-proof <base-ref>` | Compares comment-free JavaScript emit against committed HEAD |
|
|
32
34
|
| bin | `antelopejs-check-interface-ranges` | Fails on interface ranges that break module startup |
|
|
33
35
|
|
|
34
36
|
## Install
|
|
@@ -153,11 +155,11 @@ pnpm install
|
|
|
153
155
|
| `.agents/setup` | Orb setup: switches to the `.nvmrc` Node, activates `packageManager` through corepack, installs with the frozen lockfile |
|
|
154
156
|
| `AGENTS.md` | Writes the managed block between `<!-- antelopejs-guardrails:start -->` and `:end -->`; text outside the markers stays |
|
|
155
157
|
| `CLAUDE.md` | `@AGENTS.md` |
|
|
156
|
-
| `guardrails.ratchet.json` | Records
|
|
158
|
+
| `guardrails.ratchet.json` | Records non-empty debt on first adoption, before `sync:check` exists |
|
|
157
159
|
|
|
158
160
|
`--profile template` writes only the three preset configs and a user-facing `AGENTS.md` block, for template repositories. `antelopejs-sync --check` writes nothing and exits 1 on any drift; `pnpm run checks` runs it.
|
|
159
161
|
|
|
160
|
-
`pnpm run checks` runs `typecheck`, `lint`, `format:check`, `knip`, `test`, `size
|
|
162
|
+
`pnpm run checks` runs `typecheck`, `lint`, `format:check`, `knip`, `test`, `size` and `sync:check`, plus `ratchet` when `guardrails.ratchet.json` exists. It prints only the output of the steps that failed. Sync adds the ratchet scripts and pre-commit hook only while that file exists.
|
|
161
163
|
|
|
162
164
|
### Ratchet
|
|
163
165
|
|
|
@@ -174,9 +176,28 @@ pnpm install
|
|
|
174
176
|
- A file listed under `oversizeFiles` may exceed 20,000 bytes and `max-lines`, up to its recorded size. Any other file over either limit fails.
|
|
175
177
|
- `antelopejs-ratchet --update` lowers each number to the current count and removes what reached zero. It never raises a number or adds an entry. Commit the lowered file.
|
|
176
178
|
|
|
179
|
+
When every rule count is zero and `oversizeFiles` is empty, the ratchet is retired. `antelopejs-ratchet --update` deletes the file and asks you to run `antelopejs-sync`. Sync then removes `ratchet`, `ratchet:update`, the ratchet hook and the Debt instructions. Commit those changes together. An empty baseline on first adoption creates no file. Later syncs never recreate it because the `sync:check` script marks adoption.
|
|
180
|
+
|
|
181
|
+
`antelopejs-sync --check` rejects an existing retired file and tells you to run `ratchet:update`. Plain sync reports the same note but leaves deletion to the update command.
|
|
182
|
+
|
|
177
183
|
### Size
|
|
178
184
|
|
|
179
|
-
`antelopejs-check-size` fails on an authored file over 20,000 bytes, tests included. Generated code, fixtures, lockfiles, `CHANGELOG.md`, Markdown and binary assets are excluded.
|
|
185
|
+
`antelopejs-check-size` fails on an authored file over 20,000 bytes, tests included. Generated code, fixtures, lockfiles, `CHANGELOG.md`, Markdown and binary assets are excluded. Data JSON under `locales/` or `i18n/`, `blocks.json`, `schema.json`, and `evals/**/cases.json` are excluded too. Files over the limit with a NUL byte in their first 8,000 bytes are binary even without a known extension. Staged checks inspect the index blob. Excluded files are also removed from recorded size allowances on ratchet update.
|
|
186
|
+
|
|
187
|
+
Pass paths to check only those files, as the pre-commit hook does. The command also fails on an `oxlint-disable` or `eslint-disable` directive (line, next-line or file form) that names no rule: oxlint applies such a directive before any rule runs, so `no-comments` cannot report it.
|
|
188
|
+
|
|
189
|
+
### Cleanup proof
|
|
190
|
+
|
|
191
|
+
A behaviour-preserving cleanup PR shows `antelopejs-api-diff <base-ref>` passing. It checks each tracked, non-private package with a `tsconfig.json`, using that package's installed TypeScript. It emits declarations without comments, resolves published entries from `exports` (preferring `types`), `main`, `types`/`typings` and `typesVersions`, and bundles each entry's local declaration closure. External packages stay external. Stable top-level ordering ignores declaration moves between internal files while preserving overload order. Compiler errors fail the proof.
|
|
192
|
+
|
|
193
|
+
A comment-only PR shows `antelopejs-comment-proof <base-ref>` passing instead of a verifier. It includes private packages, ignores type diagnostics and compares comment-free JavaScript emit byte for byte. Both commands exclude test sources, fail on added or removed outputs, and print unified diffs for changes.
|
|
194
|
+
|
|
195
|
+
Both commands compare **committed HEAD**, not staged or working-tree edits. They check out the base and HEAD in temporary detached worktrees, reuse the current checkout's root and package `node_modules`, and remove the worktrees afterwards. Install the consumer's dependencies before running them.
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
pnpm exec antelopejs-api-diff origin/main
|
|
199
|
+
pnpm exec antelopejs-comment-proof origin/main
|
|
200
|
+
```
|
|
180
201
|
|
|
181
202
|
## oxfmt
|
|
182
203
|
|
|
@@ -279,7 +300,7 @@ checks that remain the author's responsibility.
|
|
|
279
300
|
|
|
280
301
|
## Contributing
|
|
281
302
|
|
|
282
|
-
See [CONTRIBUTING.md](./CONTRIBUTING.md). `pnpm install`, then `pnpm checks` runs the typecheck, lint, format check, tests, size
|
|
303
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md). `pnpm install`, then `pnpm checks` runs the typecheck, lint, format check, Knip, tests, size and sync checks. Repositories with recorded debt also run the ratchet.
|
|
283
304
|
|
|
284
305
|
## License
|
|
285
306
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { a as runCli, i as repositoryRoot } from "../files-DAD5OtfV.mjs";
|
|
3
|
+
import { i as compareRevisions, n as isTestSource, r as packagesAt, t as emitPackage } from "../typescript-CHOlkIXa.mjs";
|
|
4
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
6
|
+
import { generateDtsBundle } from "dts-bundle-generator";
|
|
7
|
+
//#region src/guardrails/api-diff.ts
|
|
8
|
+
function entrySegment(value) {
|
|
9
|
+
return encodeURIComponent(value).replaceAll(".", "%2E").replaceAll("*", "%2A");
|
|
10
|
+
}
|
|
11
|
+
function entryPattern(value) {
|
|
12
|
+
return value.split("*").map(entrySegment).join("*");
|
|
13
|
+
}
|
|
14
|
+
function exportTargets(target, entry = "exports/root") {
|
|
15
|
+
if (typeof target === "string") return [{
|
|
16
|
+
entry,
|
|
17
|
+
target
|
|
18
|
+
}];
|
|
19
|
+
if (target == null) return [];
|
|
20
|
+
if (Array.isArray(target)) return target.flatMap((value, index) => exportTargets(value, `${entry}/fallback/${index}`));
|
|
21
|
+
if (target.types !== void 0) return exportTargets(target.types, entry);
|
|
22
|
+
return Object.entries(target).flatMap(([key, value]) => {
|
|
23
|
+
return exportTargets(value, key.startsWith(".") ? key === "." ? "exports/root" : `exports/subpath/${entryPattern(key.slice(2))}` : `${entry}/condition/${entrySegment(key)}`);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function declarationTarget(target) {
|
|
27
|
+
return target.replace(/(?<!\.d)\.(mjs|cjs|js|mts|cts|tsx?|jsx)$/u, (extension) => {
|
|
28
|
+
if (extension === ".mjs" || extension === ".mts") return ".d.mts";
|
|
29
|
+
if (extension === ".cjs" || extension === ".cts") return ".d.cts";
|
|
30
|
+
return ".d.ts";
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function entryPatterns(manifest) {
|
|
34
|
+
const targets = exportTargets(manifest.exports);
|
|
35
|
+
for (const key of [
|
|
36
|
+
"main",
|
|
37
|
+
"types",
|
|
38
|
+
"typings"
|
|
39
|
+
]) {
|
|
40
|
+
const target = manifest[key];
|
|
41
|
+
if (target !== void 0) targets.push({
|
|
42
|
+
entry: key,
|
|
43
|
+
target
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
for (const [version, mappings] of Object.entries(manifest.typesVersions ?? {})) for (const [entry, paths] of Object.entries(mappings)) for (const [index, target] of paths.entries()) targets.push({
|
|
47
|
+
entry: `typesVersions/${entrySegment(version)}/${entryPattern(entry)}/${index}`,
|
|
48
|
+
target
|
|
49
|
+
});
|
|
50
|
+
return targets.map(({ entry, target }) => ({
|
|
51
|
+
entry,
|
|
52
|
+
target: declarationTarget(target)
|
|
53
|
+
})).filter(({ target }) => /\.d\.[cm]?ts$/u.test(target) || target.includes("*"));
|
|
54
|
+
}
|
|
55
|
+
/** Export stars are substitutions rather than path globs and can span directory separators. */
|
|
56
|
+
function targetPattern(target) {
|
|
57
|
+
return new RegExp(`^${target.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")).join("(.*)")}$`, "u");
|
|
58
|
+
}
|
|
59
|
+
function publishedEntries(directory, manifest, emitted, scratch) {
|
|
60
|
+
const outputRoot = resolve(directory, emitted.options.outDir ?? ".");
|
|
61
|
+
const files = emitted.files.map((file) => relative(scratch, file).split("\\").join("/"));
|
|
62
|
+
const entries = [];
|
|
63
|
+
for (const { entry, target } of entryPatterns(manifest)) {
|
|
64
|
+
if (isTestSource(outputRoot, resolve(directory, target))) continue;
|
|
65
|
+
const path = relative(outputRoot, resolve(directory, target)).split("\\").join("/");
|
|
66
|
+
const pattern = targetPattern(path);
|
|
67
|
+
const matches = files.filter((file) => pattern.test(file));
|
|
68
|
+
if (!matches.length && !path.includes("*")) throw new Error(`${directory}: published target ${target} has no emitted declaration.`);
|
|
69
|
+
for (const file of matches) {
|
|
70
|
+
const captures = pattern.exec(file)?.slice(1) ?? [];
|
|
71
|
+
let index = 0;
|
|
72
|
+
const name = entry.includes("*") ? entry.replaceAll("*", () => entrySegment(captures[index++] ?? captures[0] ?? "")) : `${entry}${captures.length ? `/file/${entrySegment(file)}` : ""}`;
|
|
73
|
+
entries.push({
|
|
74
|
+
entry: `${name}.d.ts`,
|
|
75
|
+
file
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return entries.sort((a, b) => a.entry.localeCompare(b.entry));
|
|
80
|
+
}
|
|
81
|
+
function statementName(compiler, statement, source) {
|
|
82
|
+
if (compiler.isVariableStatement(statement)) return statement.declarationList.declarations[0]?.name.getText(source) ?? "";
|
|
83
|
+
if (compiler.isFunctionDeclaration(statement) || compiler.isClassDeclaration(statement) || compiler.isInterfaceDeclaration(statement) || compiler.isTypeAliasDeclaration(statement) || compiler.isEnumDeclaration(statement) || compiler.isModuleDeclaration(statement) || compiler.isImportEqualsDeclaration(statement)) return statement.name?.getText(source) ?? "default";
|
|
84
|
+
return statement.getText(source);
|
|
85
|
+
}
|
|
86
|
+
function normalizeBundle(compiler, text) {
|
|
87
|
+
const source = compiler.createSourceFile("bundle.d.ts", text, compiler.ScriptTarget.Latest, true);
|
|
88
|
+
const statements = [...source.statements].sort((a, b) => statementName(compiler, a, source).localeCompare(statementName(compiler, b, source)));
|
|
89
|
+
const normalized = compiler.factory.updateSourceFile(source, statements);
|
|
90
|
+
return compiler.createPrinter({
|
|
91
|
+
removeComments: true,
|
|
92
|
+
newLine: compiler.NewLineKind.LineFeed
|
|
93
|
+
}).printFile(normalized);
|
|
94
|
+
}
|
|
95
|
+
function bundleDependencies(directory, scratch) {
|
|
96
|
+
let current = directory;
|
|
97
|
+
while (!existsSync(join(current, "node_modules"))) {
|
|
98
|
+
const parent = dirname(current);
|
|
99
|
+
if (parent === current) return;
|
|
100
|
+
current = parent;
|
|
101
|
+
}
|
|
102
|
+
symlinkSync(join(current, "node_modules"), join(scratch, "node_modules"), "dir");
|
|
103
|
+
}
|
|
104
|
+
/** dts-bundle-generator 9 remaps a .d.mts/.d.cts root twice; a source-named copy preserves its declaration text. */
|
|
105
|
+
function generatorEntry(file) {
|
|
106
|
+
const alias = file.replace(/\.d\.([cm]ts)$/u, ".$1");
|
|
107
|
+
if (alias !== file) copyFileSync(file, alias);
|
|
108
|
+
return alias;
|
|
109
|
+
}
|
|
110
|
+
function bundlePackage(directory, output, scratch, manifest) {
|
|
111
|
+
const emitted = emitPackage(directory, scratch, true);
|
|
112
|
+
const entries = publishedEntries(directory, manifest, emitted, scratch);
|
|
113
|
+
if (!entries.length) return 0;
|
|
114
|
+
bundleDependencies(directory, scratch);
|
|
115
|
+
writeFileSync(join(scratch, "package.json"), readFileSync(join(directory, "package.json")));
|
|
116
|
+
const config = join(scratch, "tsconfig.json");
|
|
117
|
+
writeFileSync(config, JSON.stringify({
|
|
118
|
+
extends: join(directory, "tsconfig.json"),
|
|
119
|
+
compilerOptions: {
|
|
120
|
+
composite: false,
|
|
121
|
+
incremental: false,
|
|
122
|
+
noEmit: false,
|
|
123
|
+
rootDir: scratch
|
|
124
|
+
},
|
|
125
|
+
files: entries.map(({ file }) => join(scratch, file)),
|
|
126
|
+
include: []
|
|
127
|
+
}));
|
|
128
|
+
const bundles = generateDtsBundle(entries.map(({ file }) => ({
|
|
129
|
+
filePath: generatorEntry(join(scratch, file)),
|
|
130
|
+
output: {
|
|
131
|
+
exportReferencedTypes: false,
|
|
132
|
+
noBanner: true
|
|
133
|
+
},
|
|
134
|
+
libraries: { inlinedLibraries: [] }
|
|
135
|
+
})), { preferredConfigPath: config });
|
|
136
|
+
for (const [index, { entry }] of entries.entries()) {
|
|
137
|
+
const path = join(output, entry);
|
|
138
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
139
|
+
writeFileSync(path, normalizeBundle(emitted.compiler, bundles[index]));
|
|
140
|
+
}
|
|
141
|
+
return entries.length;
|
|
142
|
+
}
|
|
143
|
+
/** Proves published declarations match between a base commit and committed HEAD. */
|
|
144
|
+
function apiDiff(root, baseRef) {
|
|
145
|
+
return compareRevisions(root, baseRef, "API", (worktree, output, scratch) => {
|
|
146
|
+
const packages = packagesAt(worktree, true);
|
|
147
|
+
let entries = 0;
|
|
148
|
+
for (const { directory, manifest } of packages) entries += bundlePackage(join(worktree, directory), join(output, directory === "." ? "_root" : directory), join(scratch, directory === "." ? "_root" : directory), manifest);
|
|
149
|
+
return {
|
|
150
|
+
packages: packages.map(({ directory }) => directory),
|
|
151
|
+
entries
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
//#endregion
|
|
156
|
+
//#region src/bin/api-diff.ts
|
|
157
|
+
runCli(() => {
|
|
158
|
+
const args = process.argv.slice(2);
|
|
159
|
+
if (args.length !== 1) throw new Error("Usage: antelopejs-api-diff <base-ref>");
|
|
160
|
+
const result = apiDiff(repositoryRoot(), args[0]);
|
|
161
|
+
process.stdout.write(result.output);
|
|
162
|
+
return result.different ? 1 : 0;
|
|
163
|
+
});
|
|
164
|
+
//#endregion
|
|
165
|
+
export {};
|
package/dist/bin/check-size.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as
|
|
3
|
-
import { t as checkSize } from "../size-
|
|
2
|
+
import { a as runCli, i as repositoryRoot, r as repositoryPath, t as authoredSizes } from "../files-DAD5OtfV.mjs";
|
|
3
|
+
import { t as checkSize } from "../size-D7JJp_W4.mjs";
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
5
|
import { join, resolve } from "node:path";
|
|
6
6
|
import { spawnSync } from "node:child_process";
|
package/dist/bin/checks.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import "../ceilings-BSw9p4YB.mjs";
|
|
3
|
+
import { a as runCli, i as repositoryRoot } from "../files-DAD5OtfV.mjs";
|
|
4
|
+
import { i as parseManifest, r as packageManager } from "../package-Ckd7e4tA.mjs";
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
6
|
import { join } from "node:path";
|
|
6
7
|
import { spawnSync } from "node:child_process";
|
|
7
8
|
//#region src/guardrails/checks.ts
|
|
@@ -19,7 +20,8 @@ function runChecks(root) {
|
|
|
19
20
|
const manifest = parseManifest(readFileSync(join(root, "package.json"), "utf8"));
|
|
20
21
|
const manager = packageManager(manifest);
|
|
21
22
|
const failures = [];
|
|
22
|
-
|
|
23
|
+
const steps = CHECK_STEPS.filter((step) => step !== "ratchet" || existsSync(join(root, "guardrails.ratchet.json")));
|
|
24
|
+
for (const step of steps) {
|
|
23
25
|
if (!manifest.scripts?.[step]) {
|
|
24
26
|
failures.push(`[${step}] Missing package.json script "${step}". Run antelopejs-sync.\n`);
|
|
25
27
|
continue;
|
|
@@ -37,7 +39,7 @@ function runChecks(root) {
|
|
|
37
39
|
}
|
|
38
40
|
return {
|
|
39
41
|
failed: failures.length,
|
|
40
|
-
output: `${failures.join("\n")}Checks: ${
|
|
42
|
+
output: `${failures.join("\n")}Checks: ${steps.length - failures.length} passed, ${failures.length} failed.\n`
|
|
41
43
|
};
|
|
42
44
|
}
|
|
43
45
|
//#endregion
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { a as runCli, i as repositoryRoot } from "../files-DAD5OtfV.mjs";
|
|
3
|
+
import { i as compareRevisions, r as packagesAt, t as emitPackage } from "../typescript-CHOlkIXa.mjs";
|
|
4
|
+
import { copyFileSync, mkdirSync } from "node:fs";
|
|
5
|
+
import { dirname, join, relative } from "node:path";
|
|
6
|
+
//#region src/guardrails/comment-proof.ts
|
|
7
|
+
/** Proves comment-free JavaScript emit matches, including private packages and excluding tests. */
|
|
8
|
+
function commentProof(root, baseRef) {
|
|
9
|
+
return compareRevisions(root, baseRef, "Comment proof", (worktree, output, scratch) => {
|
|
10
|
+
const packages = packagesAt(worktree, false);
|
|
11
|
+
let entries = 0;
|
|
12
|
+
for (const { directory } of packages) {
|
|
13
|
+
const key = directory === "." ? "_root" : directory;
|
|
14
|
+
const emittedRoot = join(scratch, key);
|
|
15
|
+
const emitted = emitPackage(join(worktree, directory), emittedRoot, false);
|
|
16
|
+
for (const file of emitted.files) {
|
|
17
|
+
const path = join(output, key, relative(emittedRoot, file));
|
|
18
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
19
|
+
copyFileSync(file, path);
|
|
20
|
+
entries++;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
packages: packages.map(({ directory }) => directory),
|
|
25
|
+
entries
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/bin/comment-proof.ts
|
|
31
|
+
runCli(() => {
|
|
32
|
+
const args = process.argv.slice(2);
|
|
33
|
+
if (args.length !== 1) throw new Error("Usage: antelopejs-comment-proof <base-ref>");
|
|
34
|
+
const result = commentProof(repositoryRoot(), args[0]);
|
|
35
|
+
process.stdout.write(result.output);
|
|
36
|
+
return result.different ? 1 : 0;
|
|
37
|
+
});
|
|
38
|
+
//#endregion
|
|
39
|
+
export {};
|
package/dist/bin/ratchet.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { a as
|
|
4
|
-
import { n as ratchet, r as lintDiagnostics } from "../ratchet-
|
|
2
|
+
import { s as readCeilings, t as CEILINGS_FILE } from "../ceilings-BSw9p4YB.mjs";
|
|
3
|
+
import { a as runCli, i as repositoryRoot, r as repositoryPath } from "../files-DAD5OtfV.mjs";
|
|
4
|
+
import { n as ratchet, r as lintDiagnostics } from "../ratchet-B2_QUucT.mjs";
|
|
5
5
|
import { resolve } from "node:path";
|
|
6
6
|
import { execFileSync } from "node:child_process";
|
|
7
7
|
import { parseArgs } from "node:util";
|
|
@@ -55,8 +55,10 @@ runCli(() => {
|
|
|
55
55
|
if (positionals.length && !values.staged) throw new Error("File arguments require --staged.");
|
|
56
56
|
const result = values.staged ? {
|
|
57
57
|
failures: stagedRatchet(root, positionals.length ? positionals.map((file) => repositoryPath(root, resolve(file))) : void 0),
|
|
58
|
-
lowered: []
|
|
58
|
+
lowered: [],
|
|
59
|
+
retired: false
|
|
59
60
|
} : ratchet(root, values.update);
|
|
61
|
+
if (result.retired) process.stdout.write(`Deleted ${CEILINGS_FILE}: no debt left. Run antelopejs-sync.\n`);
|
|
60
62
|
for (const message of result.lowered) process.stdout.write(`Lowered ${message}\n`);
|
|
61
63
|
for (const message of result.failures) process.stderr.write(`${message}\n`);
|
|
62
64
|
if (!result.failures.length) process.stdout.write("Ratchet: within ceilings.\n");
|
package/dist/bin/sync.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as formatCeilings, t as CEILINGS_FILE } from "../ceilings
|
|
3
|
-
import { a as
|
|
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 { a as runCli, i as repositoryRoot } from "../files-DAD5OtfV.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-B2_QUucT.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
8
|
import { parseArgs } from "node:util";
|
|
@@ -20,6 +20,9 @@ Hold every test you write or keep to this checklist:
|
|
|
20
20
|
- Name the wrong implementation this test fails on. A test with no answer is deleted, not kept.
|
|
21
21
|
- No test reads the file it tests. Drive the behaviour through the public interface.
|
|
22
22
|
- An invariant over many files is a lint rule, not a test.`;
|
|
23
|
+
const CLEANUP_PROOF = `### Cleanup proof
|
|
24
|
+
|
|
25
|
+
A behaviour-preserving cleanup PR shows \`antelopejs-api-diff <base-ref>\` passing. Published declarations stay unchanged. A comment-only PR shows \`antelopejs-comment-proof <base-ref>\` passing instead of a verifier. Both compare committed HEAD.`;
|
|
23
26
|
const SHARED_RULES = [
|
|
24
27
|
["Formatting", "oxfmt"],
|
|
25
28
|
["Lint findings are errors; no rule warns", "oxlint preset"],
|
|
@@ -28,11 +31,12 @@ const SHARED_RULES = [
|
|
|
28
31
|
["Tests read no files outside fixtures, helpers, locales", "`antelopejs/no-fs-read-in-tests`"],
|
|
29
32
|
["No assertion that restates a constant", "`antelopejs/no-constant-value-assertion`"],
|
|
30
33
|
["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`"]
|
|
34
|
+
["Params ≤ 5, function ≤ 120 lines, file ≤ 500 lines, depth ≤ 4, complexity ≤ 20", "`eslint/max-*`, `eslint/complexity`"],
|
|
35
|
+
["Cleanup preserves published declarations", "`antelopejs-api-diff <base-ref>`"],
|
|
36
|
+
["Comment-only changes preserve JavaScript emit", "`antelopejs-comment-proof <base-ref>`"]
|
|
32
37
|
];
|
|
33
38
|
const FULL_RULES = [
|
|
34
39
|
["Files ≤ 20,000 bytes, tests included", "`antelopejs-check-size` (pre-commit)"],
|
|
35
|
-
["Existing debt only shrinks", "`antelopejs-ratchet`"],
|
|
36
40
|
["Types check", "`tsc` (pre-push)"],
|
|
37
41
|
["No dead files, exports, or dependencies", "knip"],
|
|
38
42
|
["Managed files match the pack", "`antelopejs-sync --check`"],
|
|
@@ -62,7 +66,7 @@ function fullBody(context) {
|
|
|
62
66
|
return [
|
|
63
67
|
"## Guardrails",
|
|
64
68
|
"",
|
|
65
|
-
|
|
69
|
+
`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
70
|
"",
|
|
67
71
|
...pointers(context).flatMap((line) => [line, ""]),
|
|
68
72
|
"The tools enforce most rules, listed in the table at the end. The sections below hold the rules no tool checks.",
|
|
@@ -71,11 +75,19 @@ function fullBody(context) {
|
|
|
71
75
|
"",
|
|
72
76
|
TESTS,
|
|
73
77
|
"",
|
|
74
|
-
|
|
75
|
-
"",
|
|
76
|
-
"`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.",
|
|
78
|
+
CLEANUP_PROOF,
|
|
77
79
|
"",
|
|
78
|
-
|
|
80
|
+
...context.ratchet ? [
|
|
81
|
+
"### Debt",
|
|
82
|
+
"",
|
|
83
|
+
"`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.",
|
|
84
|
+
""
|
|
85
|
+
] : [],
|
|
86
|
+
ruleTable([
|
|
87
|
+
...SHARED_RULES,
|
|
88
|
+
...FULL_RULES,
|
|
89
|
+
...context.ratchet ? [["Existing debt only shrinks", "`antelopejs-ratchet`"]] : []
|
|
90
|
+
])
|
|
79
91
|
];
|
|
80
92
|
}
|
|
81
93
|
function templateBody(context) {
|
|
@@ -89,6 +101,8 @@ function templateBody(context) {
|
|
|
89
101
|
"",
|
|
90
102
|
TESTS,
|
|
91
103
|
"",
|
|
104
|
+
CLEANUP_PROOF,
|
|
105
|
+
"",
|
|
92
106
|
ruleTable(SHARED_RULES)
|
|
93
107
|
];
|
|
94
108
|
}
|
|
@@ -140,7 +154,7 @@ function presetConfig(current, preset, tools) {
|
|
|
140
154
|
const importLine = preset.content.split("\n").filter((line) => line.includes(preset.specifier)).join("\n").replace(preset.specifier, target);
|
|
141
155
|
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
156
|
}
|
|
143
|
-
function lefthook(current, manager, tools) {
|
|
157
|
+
function lefthook(current, manager, tools, ratchet = true) {
|
|
144
158
|
const exec = execCommand(manager);
|
|
145
159
|
const document = parseDocument(current ?? "{}\n");
|
|
146
160
|
if (document.errors.length) throw new Error(`lefthook.yml: ${document.errors.map(({ message }) => message).join("; ")}`);
|
|
@@ -176,6 +190,11 @@ function lefthook(current, manager, tools) {
|
|
|
176
190
|
key
|
|
177
191
|
], document.createNode(command));
|
|
178
192
|
}
|
|
193
|
+
if (!ratchet) document.deleteIn([
|
|
194
|
+
"pre-commit",
|
|
195
|
+
"commands",
|
|
196
|
+
"antelopejs-ratchet"
|
|
197
|
+
]);
|
|
179
198
|
return document.toString({ lineWidth: 0 });
|
|
180
199
|
}
|
|
181
200
|
const SETUP_SCRIPT = `#!/usr/bin/env bash
|
|
@@ -241,6 +260,7 @@ function hasChecksWorkflow(root) {
|
|
|
241
260
|
//#endregion
|
|
242
261
|
//#region src/guardrails/sync.ts
|
|
243
262
|
const LEGACY_COMMENT_INSTRUCTION = "- Explain non-obvious constraints and decisions in comments; avoid restating the code. Document public contracts with TSDoc.";
|
|
263
|
+
const RETIRED_RATCHET = `${CEILINGS_FILE} has no debt left. Run ratchet:update to delete it, then run antelopejs-sync.`;
|
|
244
264
|
function managedAgents(current = "", context) {
|
|
245
265
|
const start = current.indexOf(AGENTS_BLOCK_START);
|
|
246
266
|
const end = current.indexOf(AGENTS_BLOCK_END);
|
|
@@ -273,7 +293,7 @@ const MANAGED_ARTIFACTS = [
|
|
|
273
293
|
path: "lefthook.yml",
|
|
274
294
|
profiles: ["full"],
|
|
275
295
|
desired: (current, context) => ({
|
|
276
|
-
content: lefthook(current, context.manager, context.tools),
|
|
296
|
+
content: lefthook(current, context.manager, context.tools, context.ratchet),
|
|
277
297
|
notes: []
|
|
278
298
|
})
|
|
279
299
|
},
|
|
@@ -333,6 +353,7 @@ function planSync(current, modes, context) {
|
|
|
333
353
|
});
|
|
334
354
|
for (const artifact of MANAGED_ARTIFACTS) {
|
|
335
355
|
if (!artifact.profiles.includes(context.profile)) continue;
|
|
356
|
+
if (artifact.path === "guardrails.ratchet.json" && !context.ratchet) continue;
|
|
336
357
|
try {
|
|
337
358
|
const { content, notes } = artifact.desired(current.get(artifact.path), context);
|
|
338
359
|
const changed = current.get(artifact.path) !== content || artifact.mode !== void 0 && (artifact.mode & 73) !== 0 && ((modes.get(artifact.path) ?? 0) & 73) === 0;
|
|
@@ -407,6 +428,7 @@ function repositoryContext(root, current, profile) {
|
|
|
407
428
|
const desiredLint = current.get(lint.path) ?? lint.content;
|
|
408
429
|
return {
|
|
409
430
|
profile,
|
|
431
|
+
ratchet: current.has(CEILINGS_FILE),
|
|
410
432
|
manager: packageManager(manifest),
|
|
411
433
|
versions: toolingVersions(),
|
|
412
434
|
tools: repositoryTools(manifest),
|
|
@@ -427,6 +449,13 @@ function writeArtifacts(root, artifacts) {
|
|
|
427
449
|
if (artifact.mode !== void 0) chmodSync(path, artifact.mode);
|
|
428
450
|
}
|
|
429
451
|
}
|
|
452
|
+
function repositoryNotices(root, context) {
|
|
453
|
+
const notices = [];
|
|
454
|
+
if (context.profile === "full" && !hasChecksWorkflow(root)) notices.push(MISSING_CHECKS_WORKFLOW);
|
|
455
|
+
if (context.ratchet && isRetired(readCeilings(root))) notices.push(RETIRED_RATCHET);
|
|
456
|
+
return notices;
|
|
457
|
+
}
|
|
458
|
+
/** Measures debt only on first adoption, before sync has written the sync:check script; retired repositories stay retired. */
|
|
430
459
|
function syncRepository(root, options = {}) {
|
|
431
460
|
const profile = options.profile ?? "full";
|
|
432
461
|
const { current, modes } = repositorySnapshot(root);
|
|
@@ -435,14 +464,13 @@ function syncRepository(root, options = {}) {
|
|
|
435
464
|
let plan = planSync(current, modes, context);
|
|
436
465
|
const errors = plan.filter((artifact) => artifact.kind === "error").map(({ message }) => message);
|
|
437
466
|
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")) {
|
|
467
|
+
(options.check ? errors : notes).push(...repositoryNotices(root, context));
|
|
468
|
+
if (!errors.length && profile === "full" && !options.check && !current.has("guardrails.ratchet.json") && manifest.scripts?.["sync:check"] === void 0) {
|
|
443
469
|
const lint = PRESET_CONFIGS[0];
|
|
444
470
|
const desired = presetConfig(current.get(lint.path), lint, context.tools);
|
|
445
|
-
|
|
471
|
+
const baseline = measureBaseline(root, desired === current.get(lint.path) ? void 0 : desired);
|
|
472
|
+
context.ratchet = !isRetired(baseline);
|
|
473
|
+
context.baseline = formatCeilings(baseline);
|
|
446
474
|
plan = planSync(current, modes, context);
|
|
447
475
|
}
|
|
448
476
|
const writes = plan.filter((artifact) => artifact.kind === "write" || artifact.kind === "migrated");
|
|
@@ -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 };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import "./ceilings-BSw9p4YB.mjs";
|
|
1
2
|
import { n as IGNORE_PATTERNS, t as AGENT_IGNORE_PATTERNS } from "./shared-NC0T8ykm.mjs";
|
|
2
|
-
import { existsSync, lstatSync } from "node:fs";
|
|
3
|
+
import { closeSync, existsSync, lstatSync, openSync, readSync } from "node:fs";
|
|
3
4
|
import { join, matchesGlob, relative, resolve } from "node:path";
|
|
4
5
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
5
6
|
//#region src/guardrails/cli.ts
|
|
@@ -28,6 +29,16 @@ const SIZE_EXCLUSIONS = [
|
|
|
28
29
|
kind: "fixtures",
|
|
29
30
|
patterns: ["**/fixtures/**", "**/__fixtures__/**"]
|
|
30
31
|
},
|
|
32
|
+
{
|
|
33
|
+
kind: "data",
|
|
34
|
+
patterns: [
|
|
35
|
+
"**/locales/**/*.json",
|
|
36
|
+
"**/i18n/**/*.json",
|
|
37
|
+
"**/blocks.json",
|
|
38
|
+
"**/schema.json",
|
|
39
|
+
"**/evals/**/cases.json"
|
|
40
|
+
]
|
|
41
|
+
},
|
|
31
42
|
{
|
|
32
43
|
kind: "lockfiles",
|
|
33
44
|
patterns: [
|
|
@@ -89,15 +100,30 @@ function isExcluded(file) {
|
|
|
89
100
|
const undotted = file.replace(/(^|\/)\./gu, "$1");
|
|
90
101
|
return SIZE_EXCLUSIONS.some(({ patterns }) => patterns.some((pattern) => matchesGlob(file, pattern) || matchesGlob(undotted, pattern)));
|
|
91
102
|
}
|
|
103
|
+
function isBinary(root, file, staged) {
|
|
104
|
+
if (staged) return execFileSync("git", ["show", `:${file}`], {
|
|
105
|
+
cwd: root,
|
|
106
|
+
maxBuffer: fileBytes(root, file, true)
|
|
107
|
+
}).subarray(0, 8e3).includes(0);
|
|
108
|
+
const descriptor = openSync(join(root, file), "r");
|
|
109
|
+
try {
|
|
110
|
+
const prefix = Buffer.alloc(8e3);
|
|
111
|
+
const length = readSync(descriptor, prefix, 0, prefix.length, 0);
|
|
112
|
+
return prefix.subarray(0, length).includes(0);
|
|
113
|
+
} finally {
|
|
114
|
+
closeSync(descriptor);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
92
117
|
function authoredSizes(root, files = gitFiles(root), staged = false) {
|
|
93
118
|
const sizes = /* @__PURE__ */ new Map();
|
|
94
119
|
for (const input of files) {
|
|
95
120
|
const file = repositoryPath(root, input);
|
|
96
121
|
if (isExcluded(file)) continue;
|
|
97
122
|
const size = fileBytes(root, file, staged);
|
|
123
|
+
if (size !== void 0 && size > 2e4 && isBinary(root, file, staged)) continue;
|
|
98
124
|
if (size !== void 0) sizes.set(file, size);
|
|
99
125
|
}
|
|
100
126
|
return sizes;
|
|
101
127
|
}
|
|
102
128
|
//#endregion
|
|
103
|
-
export {
|
|
129
|
+
export { runCli as a, repositoryRoot as i, gitFiles as n, repositoryPath as r, authoredSizes as t };
|
package/dist/oxc/lint.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as findGitRoot,
|
|
1
|
+
import { i as findGitRoot, s as readCeilings } from "../ceilings-BSw9p4YB.mjs";
|
|
2
2
|
import { n as TEST_FILE_GLOBS, t as ASSERTION_FUNCTION_NAMES } from "../shared-WC0z3y9F.mjs";
|
|
3
3
|
import { n as IGNORE_PATTERNS, t as AGENT_IGNORE_PATTERNS } from "../shared-NC0T8ykm.mjs";
|
|
4
4
|
import { defineConfig } from "oxlint";
|
|
@@ -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;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { a as formatCeilings, n as MAX_FILE_BYTES, o as
|
|
2
|
-
import {
|
|
3
|
-
import { n as sizeFailure } from "./size-
|
|
1
|
+
import { a as formatCeilings, n as MAX_FILE_BYTES, o as isRetired, r as MEASURE_ENV, s as readCeilings, t as CEILINGS_FILE } from "./ceilings-BSw9p4YB.mjs";
|
|
2
|
+
import { n as gitFiles, r as repositoryPath, t as authoredSizes } from "./files-DAD5OtfV.mjs";
|
|
3
|
+
import { n as sizeFailure } from "./size-D7JJp_W4.mjs";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
|
-
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { existsSync, mkdtempSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { basename, dirname, join } from "node:path";
|
|
7
7
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
8
8
|
//#region src/guardrails/oxlint.ts
|
|
@@ -77,11 +77,12 @@ function lintDiagnostics(root, config, files) {
|
|
|
77
77
|
function baseline(root, diagnostics) {
|
|
78
78
|
const rules = {};
|
|
79
79
|
const oversizeFiles = {};
|
|
80
|
+
const sizes = authoredSizes(root);
|
|
80
81
|
for (const diagnostic of diagnostics) if (diagnostic.rule === "eslint/max-lines") {
|
|
81
|
-
const size =
|
|
82
|
+
const size = sizes.get(diagnostic.file);
|
|
82
83
|
if (size !== void 0) oversizeFiles[diagnostic.file] = size;
|
|
83
84
|
} else rules[diagnostic.rule] = (rules[diagnostic.rule] ?? 0) + 1;
|
|
84
|
-
for (const [file, size] of
|
|
85
|
+
for (const [file, size] of sizes) if (size > 2e4) oversizeFiles[file] = size;
|
|
85
86
|
return {
|
|
86
87
|
rules,
|
|
87
88
|
oversizeFiles
|
|
@@ -154,18 +155,22 @@ function ratchet(root, update = false) {
|
|
|
154
155
|
if (shown.length < matches.length) failures.push(` ...and ${matches.length - shown.length} recorded in files this change does not touch.`);
|
|
155
156
|
}
|
|
156
157
|
for (const [file, size] of Object.entries(current.oversizeFiles)) if (ceilings.oversizeFiles[file] === void 0) failures.push(size > 2e4 ? sizeFailure(file, size, MAX_FILE_BYTES) : `${file}: ${size} bytes and an eslint/max-lines diagnostic; this file has no oversize allowance. Split by responsibility into sibling modules, or split tests by behaviour under test, one file per describe.`);
|
|
158
|
+
const sizes = authoredSizes(root, Object.keys(ceilings.oversizeFiles));
|
|
157
159
|
for (const [file, limit] of Object.entries(ceilings.oversizeFiles)) {
|
|
158
|
-
const size =
|
|
160
|
+
const size = sizes.get(file);
|
|
159
161
|
if (size !== void 0 && size > limit) failures.push(sizeFailure(file, size, limit));
|
|
160
162
|
}
|
|
161
163
|
const lowered = update ? lowerCeilings(ceilings, current) : [];
|
|
164
|
+
const retired = update && isRetired(ceilings);
|
|
162
165
|
if (update) {
|
|
163
166
|
const path = join(root, CEILINGS_FILE);
|
|
164
|
-
|
|
167
|
+
if (retired) unlinkSync(path);
|
|
168
|
+
else writeFileSync(path, formatCeilings(ceilings));
|
|
165
169
|
}
|
|
166
170
|
return {
|
|
167
171
|
failures,
|
|
168
|
-
lowered
|
|
172
|
+
lowered,
|
|
173
|
+
retired
|
|
169
174
|
};
|
|
170
175
|
}
|
|
171
176
|
//#endregion
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as authoredSizes } from "./files-
|
|
1
|
+
import { s as readCeilings } from "./ceilings-BSw9p4YB.mjs";
|
|
2
|
+
import { t as authoredSizes } from "./files-DAD5OtfV.mjs";
|
|
3
3
|
//#region src/guardrails/size.ts
|
|
4
4
|
function sizeFailure(file, size, limit) {
|
|
5
5
|
return `${file}: ${size} bytes, ${size - limit} bytes over the ${limit}-byte ceiling. Split by responsibility into sibling modules, one cohesive group of exports each; keep the original path re-exporting only if it is a public entry point. Split a test file by behaviour under test, one file per describe. Do not minify, strip whitespace, or move code under an excluded path to pass.`;
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { n as TEST_FILE_GLOBS } from "./shared-WC0z3y9F.mjs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join, matchesGlob, relative, sep } from "node:path";
|
|
5
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
//#region src/guardrails/revisions.ts
|
|
8
|
+
function trackedManifests(root) {
|
|
9
|
+
return execFileSync("git", ["ls-files", "-z"], {
|
|
10
|
+
cwd: root,
|
|
11
|
+
encoding: "utf8"
|
|
12
|
+
}).split("\0").filter((file) => file === "package.json" || file.endsWith("/package.json"));
|
|
13
|
+
}
|
|
14
|
+
function linkDependencies(root, worktree) {
|
|
15
|
+
const directories = /* @__PURE__ */ new Set([".", ...trackedManifests(root).map(dirname)]);
|
|
16
|
+
for (const directory of directories) {
|
|
17
|
+
const source = join(root, directory, "node_modules");
|
|
18
|
+
const target = join(worktree, directory, "node_modules");
|
|
19
|
+
if (existsSync(source) && existsSync(dirname(target)) && !existsSync(target)) symlinkSync(source, target, "dir");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Compares isolated committed revisions and removes both worktrees even when compilation fails. */
|
|
23
|
+
function compareRevisions(root, baseRef, label, emit) {
|
|
24
|
+
const temporary = mkdtempSync(join(tmpdir(), "antelope-proof-"));
|
|
25
|
+
const worktrees = [];
|
|
26
|
+
const results = [];
|
|
27
|
+
try {
|
|
28
|
+
for (const [side, ref] of [["base", baseRef], ["head", "HEAD"]]) {
|
|
29
|
+
const revision = execFileSync("git", [
|
|
30
|
+
"rev-parse",
|
|
31
|
+
"--verify",
|
|
32
|
+
"--end-of-options",
|
|
33
|
+
`${ref}^{commit}`
|
|
34
|
+
], {
|
|
35
|
+
cwd: root,
|
|
36
|
+
encoding: "utf8",
|
|
37
|
+
stdio: [
|
|
38
|
+
"ignore",
|
|
39
|
+
"pipe",
|
|
40
|
+
"pipe"
|
|
41
|
+
]
|
|
42
|
+
}).trim();
|
|
43
|
+
const worktree = join(temporary, "worktrees", side);
|
|
44
|
+
execFileSync("git", [
|
|
45
|
+
"worktree",
|
|
46
|
+
"add",
|
|
47
|
+
"--quiet",
|
|
48
|
+
"--detach",
|
|
49
|
+
worktree,
|
|
50
|
+
revision
|
|
51
|
+
], {
|
|
52
|
+
cwd: root,
|
|
53
|
+
stdio: [
|
|
54
|
+
"ignore",
|
|
55
|
+
"pipe",
|
|
56
|
+
"pipe"
|
|
57
|
+
]
|
|
58
|
+
});
|
|
59
|
+
worktrees.push(worktree);
|
|
60
|
+
linkDependencies(root, worktree);
|
|
61
|
+
const output = join(temporary, side);
|
|
62
|
+
mkdirSync(output, { recursive: true });
|
|
63
|
+
results.push(emit(worktree, output, join(temporary, "emit", side)));
|
|
64
|
+
}
|
|
65
|
+
const diff = spawnSync("git", [
|
|
66
|
+
"diff",
|
|
67
|
+
"--no-index",
|
|
68
|
+
"--no-color",
|
|
69
|
+
"--no-renames",
|
|
70
|
+
"--no-ext-diff",
|
|
71
|
+
"--no-textconv",
|
|
72
|
+
"--",
|
|
73
|
+
"base",
|
|
74
|
+
"head"
|
|
75
|
+
], {
|
|
76
|
+
cwd: temporary,
|
|
77
|
+
encoding: "utf8",
|
|
78
|
+
maxBuffer: 67108864
|
|
79
|
+
});
|
|
80
|
+
if (diff.error) throw diff.error;
|
|
81
|
+
if (diff.status !== 0 && diff.status !== 1) throw new Error(diff.stderr || "Could not compare revision outputs.");
|
|
82
|
+
const packages = new Set(results.flatMap((result) => result.packages)).size;
|
|
83
|
+
return {
|
|
84
|
+
different: diff.status === 1,
|
|
85
|
+
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`
|
|
86
|
+
};
|
|
87
|
+
} finally {
|
|
88
|
+
for (const worktree of worktrees.reverse()) execFileSync("git", [
|
|
89
|
+
"worktree",
|
|
90
|
+
"remove",
|
|
91
|
+
"--force",
|
|
92
|
+
worktree
|
|
93
|
+
], {
|
|
94
|
+
cwd: root,
|
|
95
|
+
stdio: "pipe"
|
|
96
|
+
});
|
|
97
|
+
rmSync(temporary, {
|
|
98
|
+
recursive: true,
|
|
99
|
+
force: true
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region src/guardrails/typescript.ts
|
|
105
|
+
function packagesAt(root, publishedOnly) {
|
|
106
|
+
return trackedManifests(root).flatMap((file) => {
|
|
107
|
+
const directory = dirname(file);
|
|
108
|
+
if (!existsSync(join(root, directory, "tsconfig.json"))) return [];
|
|
109
|
+
const manifest = JSON.parse(readFileSync(join(root, file), "utf8"));
|
|
110
|
+
if (publishedOnly && manifest.private === true) return [];
|
|
111
|
+
return [{
|
|
112
|
+
directory,
|
|
113
|
+
manifest
|
|
114
|
+
}];
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/** Loads the consumer's compiler, including a package-local installation in a workspace. */
|
|
118
|
+
function loadTypeScript(directory) {
|
|
119
|
+
return createRequire(join(directory, "package.json"))("typescript");
|
|
120
|
+
}
|
|
121
|
+
function isTestSource(root, file) {
|
|
122
|
+
const path = relative(root, file).split("\\").join("/");
|
|
123
|
+
return TEST_FILE_GLOBS.some((pattern) => matchesGlob(path, pattern));
|
|
124
|
+
}
|
|
125
|
+
function failDiagnostics(compiler, diagnostics) {
|
|
126
|
+
const errors = diagnostics.filter((item) => item.category === compiler.DiagnosticCategory.Error);
|
|
127
|
+
if (errors.length) throw new Error(compiler.formatDiagnostics(errors, {
|
|
128
|
+
getCanonicalFileName: (file) => file,
|
|
129
|
+
getCurrentDirectory: () => process.cwd(),
|
|
130
|
+
getNewLine: () => "\n"
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
/** Declaration inputs are part of the local closure but TypeScript does not emit copies of them. */
|
|
134
|
+
function copyDeclarations(program, directory, output) {
|
|
135
|
+
const sources = program.getSourceFiles().filter((source) => !program.isSourceFileDefaultLibrary(source) && !program.isSourceFileFromExternalLibrary(source) && !relative(directory, source.fileName).startsWith(`..${sep}`) && !isTestSource(directory, source.fileName));
|
|
136
|
+
const authored = sources.filter((source) => !source.isDeclarationFile);
|
|
137
|
+
let root = dirname((authored[0] ?? sources[0])?.fileName ?? join(directory, "index.ts"));
|
|
138
|
+
for (const source of authored.length ? authored : sources) while (relative(root, source.fileName).startsWith(`..${sep}`)) root = dirname(root);
|
|
139
|
+
root = program.getCompilerOptions().rootDir ?? root;
|
|
140
|
+
const files = [];
|
|
141
|
+
for (const source of sources) {
|
|
142
|
+
if (!source.isDeclarationFile) continue;
|
|
143
|
+
const path = relative(root, source.fileName);
|
|
144
|
+
if (path.startsWith(`..${sep}`)) continue;
|
|
145
|
+
const target = join(output, path);
|
|
146
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
147
|
+
writeFileSync(target, source.text);
|
|
148
|
+
files.push(target);
|
|
149
|
+
}
|
|
150
|
+
return files;
|
|
151
|
+
}
|
|
152
|
+
/** Emits with the package's compiler and config; test roots and outputs never enter either proof. */
|
|
153
|
+
function emitPackage(directory, output, declarations) {
|
|
154
|
+
const compiler = loadTypeScript(directory);
|
|
155
|
+
const configPath = join(directory, "tsconfig.json");
|
|
156
|
+
const config = compiler.readConfigFile(configPath, compiler.sys.readFile);
|
|
157
|
+
if (config.error) failDiagnostics(compiler, [config.error]);
|
|
158
|
+
const parsed = compiler.parseJsonConfigFileContent(config.config, compiler.sys, directory);
|
|
159
|
+
failDiagnostics(compiler, parsed.errors);
|
|
160
|
+
const options = {
|
|
161
|
+
...parsed.options,
|
|
162
|
+
rootDir: parsed.options.rootDir ?? (parsed.options.outDir ? void 0 : directory),
|
|
163
|
+
outDir: output,
|
|
164
|
+
declarationDir: declarations ? output : void 0,
|
|
165
|
+
declaration: declarations,
|
|
166
|
+
emitDeclarationOnly: declarations,
|
|
167
|
+
removeComments: true,
|
|
168
|
+
declarationMap: false,
|
|
169
|
+
sourceMap: false,
|
|
170
|
+
inlineSourceMap: false,
|
|
171
|
+
noEmit: false,
|
|
172
|
+
noEmitOnError: false,
|
|
173
|
+
composite: false,
|
|
174
|
+
incremental: false,
|
|
175
|
+
tsBuildInfoFile: void 0,
|
|
176
|
+
outFile: void 0,
|
|
177
|
+
allowImportingTsExtensions: declarations ? parsed.options.allowImportingTsExtensions : false
|
|
178
|
+
};
|
|
179
|
+
const program = compiler.createProgram({
|
|
180
|
+
rootNames: parsed.fileNames.filter((file) => !isTestSource(directory, file)),
|
|
181
|
+
options,
|
|
182
|
+
projectReferences: parsed.projectReferences
|
|
183
|
+
});
|
|
184
|
+
if (declarations) failDiagnostics(compiler, compiler.getPreEmitDiagnostics(program));
|
|
185
|
+
const files = [];
|
|
186
|
+
const result = program.emit(void 0, (file, text, _bom, _onError, sources) => {
|
|
187
|
+
if (sources?.some((source) => isTestSource(directory, source.fileName))) return;
|
|
188
|
+
if (declarations ? !/\.d\.[cm]?ts$/.test(file) : !/\.[cm]?jsx?$/.test(file)) return;
|
|
189
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
190
|
+
writeFileSync(file, text);
|
|
191
|
+
files.push(file);
|
|
192
|
+
});
|
|
193
|
+
if (declarations) failDiagnostics(compiler, result.diagnostics);
|
|
194
|
+
if (declarations) files.push(...copyDeclarations(program, directory, output));
|
|
195
|
+
return {
|
|
196
|
+
compiler,
|
|
197
|
+
files,
|
|
198
|
+
options: parsed.options
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
//#endregion
|
|
202
|
+
export { compareRevisions as i, isTestSource as n, packagesAt as r, emitPackage as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antelopejs/tooling-configs",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Shared tooling presets for AntelopeJS repositories: oxlint (with the vendored anti-slop rules), oxfmt, ESLint for Nuxt layers, Knip, and an interface range check",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"antelopejs",
|
|
@@ -21,9 +21,11 @@
|
|
|
21
21
|
"url": "git+https://github.com/AntelopeJS/tooling-configs.git"
|
|
22
22
|
},
|
|
23
23
|
"bin": {
|
|
24
|
+
"antelopejs-api-diff": "./dist/bin/api-diff.mjs",
|
|
24
25
|
"antelopejs-check-interface-ranges": "./dist/bin/check-interface-ranges.mjs",
|
|
25
26
|
"antelopejs-check-size": "./dist/bin/check-size.mjs",
|
|
26
27
|
"antelopejs-checks": "./dist/bin/checks.mjs",
|
|
28
|
+
"antelopejs-comment-proof": "./dist/bin/comment-proof.mjs",
|
|
27
29
|
"antelopejs-ratchet": "./dist/bin/ratchet.mjs",
|
|
28
30
|
"antelopejs-sync": "./dist/bin/sync.mjs"
|
|
29
31
|
},
|
|
@@ -55,15 +57,14 @@
|
|
|
55
57
|
"checks": "node src/bin/checks.ts",
|
|
56
58
|
"format:check": "oxfmt --check .",
|
|
57
59
|
"size": "node src/bin/check-size.ts",
|
|
58
|
-
"ratchet": "node src/bin/ratchet.ts",
|
|
59
60
|
"prepare": "if git rev-parse --is-inside-work-tree >/dev/null 2>&1 && command -v lefthook >/dev/null 2>&1; then lefthook install; fi",
|
|
60
61
|
"sync": "node src/bin/sync.ts",
|
|
61
|
-
"ratchet:update": "node src/bin/ratchet.ts --update",
|
|
62
62
|
"sync:check": "node src/bin/sync.ts --check",
|
|
63
63
|
"knip": "knip"
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
66
|
"@oxlint/plugins": "1.81.0",
|
|
67
|
+
"dts-bundle-generator": "^9.5.1",
|
|
67
68
|
"yaml": "^2.9.1"
|
|
68
69
|
},
|
|
69
70
|
"devDependencies": {
|