@antelopejs/tooling-configs 0.0.6 → 0.0.7
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
CHANGED
|
@@ -222,6 +222,38 @@ The front end is left out of `project` rather than ignored, so a repository that
|
|
|
222
222
|
`frontend-vue/` analysed can add it under Knip's `workspaces` without the preset
|
|
223
223
|
overriding the choice.
|
|
224
224
|
|
|
225
|
+
## Interface ranges
|
|
226
|
+
|
|
227
|
+
`@antelopejs/core` keeps one copy of each `@antelopejs/interface-*` package at startup, the
|
|
228
|
+
one its implementing module installed, and refuses to start when a loaded module declares a
|
|
229
|
+
range that copy falls outside. An exact pin (`0.1.7`) or a 0.x caret (`^0.1.2` stops at
|
|
230
|
+
`0.2.0`) therefore breaks a module's startup at the next interface release, with no change to
|
|
231
|
+
the module.
|
|
232
|
+
|
|
233
|
+
`antelopejs-check-interface-ranges` fails on such a range in `dependencies` or
|
|
234
|
+
`optionalDependencies`, in every `package.json` git tracks in the repository (playgrounds and
|
|
235
|
+
front-end layers included), whatever directory it runs from:
|
|
236
|
+
|
|
237
|
+
| The package… | must declare |
|
|
238
|
+
| ------------------------------------------------- | -------------------------------------------- |
|
|
239
|
+
| consumes the interface | `>=<floor> <1.0.0` (`<next major>` from 1.0) |
|
|
240
|
+
| implements it (listed in `antelopeJs.implements`) | `>=<floor> <0.<minor + 1>.0` while it is 0.x |
|
|
241
|
+
|
|
242
|
+
An implementer is the opposite case on purpose: it must never be handed a breaking minor it
|
|
243
|
+
does not implement. When the implemented interface sits in the same repository, its version is
|
|
244
|
+
the floor to use. `workspace:`, `file:` and `link:` specs are left alone.
|
|
245
|
+
|
|
246
|
+
```jsonc
|
|
247
|
+
// package.json
|
|
248
|
+
{
|
|
249
|
+
"scripts": {
|
|
250
|
+
"lint": "oxlint && antelopejs-check-interface-ranges",
|
|
251
|
+
},
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Pass paths to check only those manifests.
|
|
256
|
+
|
|
225
257
|
## Vendored anti-slop
|
|
226
258
|
|
|
227
259
|
`src/oxc/anti-slop/` is a copy of upstream, kept byte-identical so it can be diffed
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { join, relative } from "node:path";
|
|
5
|
+
//#region src/interface-ranges.ts
|
|
6
|
+
/**
|
|
7
|
+
* At startup `@antelopejs/core` keeps one copy of each interface package, the
|
|
8
|
+
* one its implementing module installed, and refuses to start when any loaded
|
|
9
|
+
* module declares a range that copy falls outside. An exact pin (`0.1.7`) or a
|
|
10
|
+
* 0.x caret (`^0.1.2` means `<0.2.0`) therefore stops a module from starting
|
|
11
|
+
* as soon as the implementer resolves the next interface release, with no
|
|
12
|
+
* change to the module itself.
|
|
13
|
+
*
|
|
14
|
+
* A consumer accepts every release from its floor up to the next breaking one
|
|
15
|
+
* (`>=0.1.7 <1.0.0`). The module that implements an interface is the opposite
|
|
16
|
+
* case: it must never be handed a breaking minor it does not implement, so it
|
|
17
|
+
* caps the range below the next minor (`>=0.3.0 <0.4.0`) while the interface
|
|
18
|
+
* is 0.x.
|
|
19
|
+
*/
|
|
20
|
+
const INTERFACE_PACKAGE_PREFIX = "@antelopejs/interface-";
|
|
21
|
+
const LOCAL_SPEC_PREFIXES = [
|
|
22
|
+
"workspace:",
|
|
23
|
+
"file:",
|
|
24
|
+
"link:"
|
|
25
|
+
];
|
|
26
|
+
const CHECKED_FIELDS = ["dependencies", "optionalDependencies"];
|
|
27
|
+
const BOUNDED_RANGE = /^>=(\d+)\.(\d+)\.(\d+) <(\d+)\.(\d+)\.(\d+)$/;
|
|
28
|
+
const SINGLE_VERSION = /^[\^~=]?v?(\d+)\.(\d+)\.(\d+)$/;
|
|
29
|
+
const formatVersion = ([major, minor, patch]) => `${major}.${minor}.${patch}`;
|
|
30
|
+
const sameVersion = (a, b) => a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
|
|
31
|
+
/** The first release a module declaring `floor` must not be handed. */
|
|
32
|
+
function rangeCeiling(floor, role) {
|
|
33
|
+
const [major, minor] = floor;
|
|
34
|
+
if (major > 0) return [
|
|
35
|
+
major + 1,
|
|
36
|
+
0,
|
|
37
|
+
0
|
|
38
|
+
];
|
|
39
|
+
return role === "implementer" ? [
|
|
40
|
+
0,
|
|
41
|
+
minor + 1,
|
|
42
|
+
0
|
|
43
|
+
] : [
|
|
44
|
+
1,
|
|
45
|
+
0,
|
|
46
|
+
0
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
function expectedRange(floor, role) {
|
|
50
|
+
return `>=${formatVersion(floor)} <${formatVersion(rangeCeiling(floor, role))}`;
|
|
51
|
+
}
|
|
52
|
+
function toVersion(match, offset) {
|
|
53
|
+
return [
|
|
54
|
+
Number(match[offset]),
|
|
55
|
+
Number(match[offset + 1]),
|
|
56
|
+
Number(match[offset + 2])
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
/** The lowest release `spec` accepts, when it is a shape this check reads. */
|
|
60
|
+
function readFloor(spec) {
|
|
61
|
+
const bounded = BOUNDED_RANGE.exec(spec);
|
|
62
|
+
if (bounded) return toVersion(bounded, 1);
|
|
63
|
+
const single = SINGLE_VERSION.exec(spec);
|
|
64
|
+
return single ? toVersion(single, 1) : void 0;
|
|
65
|
+
}
|
|
66
|
+
function compareVersions(a, b) {
|
|
67
|
+
return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
|
|
68
|
+
}
|
|
69
|
+
function isAcceptedRange(spec, role, local) {
|
|
70
|
+
const bounded = BOUNDED_RANGE.exec(spec);
|
|
71
|
+
if (!bounded) return false;
|
|
72
|
+
const floor = toVersion(bounded, 1);
|
|
73
|
+
const ceiling = toVersion(bounded, 4);
|
|
74
|
+
if (!sameVersion(ceiling, rangeCeiling(floor, role))) return false;
|
|
75
|
+
return local === void 0 || compareVersions(local, floor) >= 0 && compareVersions(local, ceiling) < 0;
|
|
76
|
+
}
|
|
77
|
+
function isLocalSpec(spec) {
|
|
78
|
+
return LOCAL_SPEC_PREFIXES.some((prefix) => spec.startsWith(prefix));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* `localVersions` holds the versions of the packages that sit in the same
|
|
82
|
+
* repository. An implemented interface is usually one of them, and its
|
|
83
|
+
* version, rather than the declared floor, decides the minor to stop below.
|
|
84
|
+
*/
|
|
85
|
+
function checkInterfaceRanges(manifest, localVersions = /* @__PURE__ */ new Map()) {
|
|
86
|
+
const implemented = new Set(manifest.antelopeJs?.implements ?? []);
|
|
87
|
+
const violations = [];
|
|
88
|
+
for (const field of CHECKED_FIELDS) for (const [name, spec] of Object.entries(manifest[field] ?? {})) {
|
|
89
|
+
if (!name.startsWith(INTERFACE_PACKAGE_PREFIX) || isLocalSpec(spec)) continue;
|
|
90
|
+
const role = implemented.has(name) ? "implementer" : "consumer";
|
|
91
|
+
const localVersion = localVersions.get(name);
|
|
92
|
+
const local = role === "implementer" && localVersion !== void 0 ? readFloor(localVersion) : void 0;
|
|
93
|
+
if (isAcceptedRange(spec, role, local)) continue;
|
|
94
|
+
const floor = local ?? readFloor(spec);
|
|
95
|
+
violations.push({
|
|
96
|
+
field,
|
|
97
|
+
name,
|
|
98
|
+
spec,
|
|
99
|
+
role,
|
|
100
|
+
expected: floor ? expectedRange(floor, role) : void 0
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return violations;
|
|
104
|
+
}
|
|
105
|
+
function describeViolation(violation) {
|
|
106
|
+
const { field, name, spec, role, expected } = violation;
|
|
107
|
+
const fix = expected ? `use "${expected}"` : role === "implementer" ? "declare it as >=<floor> <next minor>" : "declare it as >=<floor> <1.0.0";
|
|
108
|
+
return `${field}["${name}"] is "${spec}": ${role === "implementer" ? "this package implements it, so it must stop below the next minor" : "it must accept every release up to the next breaking one"}; ${fix}`;
|
|
109
|
+
}
|
|
110
|
+
//#endregion
|
|
111
|
+
//#region src/bin/check-interface-ranges.ts
|
|
112
|
+
function trackedManifests() {
|
|
113
|
+
const git = (args) => execFileSync("git", args, { encoding: "utf8" }).trim();
|
|
114
|
+
const root = git(["rev-parse", "--show-toplevel"]);
|
|
115
|
+
return git([
|
|
116
|
+
"-C",
|
|
117
|
+
root,
|
|
118
|
+
"ls-files",
|
|
119
|
+
"--",
|
|
120
|
+
"package.json",
|
|
121
|
+
"**/package.json"
|
|
122
|
+
]).split("\n").filter((line) => line.length > 0).map((file) => relative(process.cwd(), join(root, file)));
|
|
123
|
+
}
|
|
124
|
+
function main() {
|
|
125
|
+
const manifests = (process.argv.length > 2 ? process.argv.slice(2) : trackedManifests()).map((path) => ({
|
|
126
|
+
path,
|
|
127
|
+
manifest: JSON.parse(readFileSync(path, "utf8"))
|
|
128
|
+
}));
|
|
129
|
+
const localVersions = /* @__PURE__ */ new Map();
|
|
130
|
+
for (const { manifest } of manifests) if (manifest.name && manifest.version) localVersions.set(manifest.name, manifest.version);
|
|
131
|
+
let failures = 0;
|
|
132
|
+
for (const { path, manifest } of manifests) for (const violation of checkInterfaceRanges(manifest, localVersions)) {
|
|
133
|
+
console.error(`${path}: ${describeViolation(violation)}`);
|
|
134
|
+
failures += 1;
|
|
135
|
+
}
|
|
136
|
+
if (failures > 0) {
|
|
137
|
+
console.error(`\n${failures} interface range(s) would stop a module from starting at the next interface release.`);
|
|
138
|
+
return 1;
|
|
139
|
+
}
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
process.exitCode = main();
|
|
143
|
+
//#endregion
|
|
144
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antelopejs/tooling-configs",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "Shared tooling presets for AntelopeJS projects: oxlint (incl. vendored anti-slop) and Knip",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"antelopejs",
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
"type": "git",
|
|
15
15
|
"url": "git+https://github.com/AntelopeJS/tooling-configs.git"
|
|
16
16
|
},
|
|
17
|
+
"bin": {
|
|
18
|
+
"antelopejs-check-interface-ranges": "./dist/bin/check-interface-ranges.mjs"
|
|
19
|
+
},
|
|
17
20
|
"files": [
|
|
18
21
|
"dist"
|
|
19
22
|
],
|
|
@@ -37,7 +40,7 @@
|
|
|
37
40
|
"prepack": "pnpm run build",
|
|
38
41
|
"release": "pnpm run lint && pnpm run prepack && release-it",
|
|
39
42
|
"typecheck": "tsc --noEmit",
|
|
40
|
-
"test": "node --test \"src/oxc/anti-slop/rules/*.test.ts\"",
|
|
43
|
+
"test": "node --test \"src/oxc/anti-slop/rules/*.test.ts\" \"src/*.test.ts\"",
|
|
41
44
|
"checks": "pnpm run typecheck && pnpm run lint && pnpm run format:check && pnpm run test",
|
|
42
45
|
"format:check": "oxfmt --check ."
|
|
43
46
|
},
|