@antelopejs/tooling-configs 0.0.6 → 0.0.8
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
|
@@ -156,6 +156,10 @@ Import order is not part of it: `perfectionist/sort-imports` owns that on the li
|
|
|
156
156
|
Enabling oxfmt's `sortImports` as well would have two tools sorting the same imports
|
|
157
157
|
differently, each undoing the other on every save.
|
|
158
158
|
|
|
159
|
+
The preset also leaves `CHANGELOG.md` alone. changelogen writes it during a release and
|
|
160
|
+
the release commit lands unformatted, so checking it would turn the default branch red
|
|
161
|
+
after every release with a breaking change.
|
|
162
|
+
|
|
159
163
|
Extra ignores are **added** to the shared ones rather than replacing them, so a repository
|
|
160
164
|
adopting oxfmt in stages can park a directory without losing the rest:
|
|
161
165
|
|
|
@@ -222,6 +226,38 @@ The front end is left out of `project` rather than ignored, so a repository that
|
|
|
222
226
|
`frontend-vue/` analysed can add it under Knip's `workspaces` without the preset
|
|
223
227
|
overriding the choice.
|
|
224
228
|
|
|
229
|
+
## Interface ranges
|
|
230
|
+
|
|
231
|
+
`@antelopejs/core` keeps one copy of each `@antelopejs/interface-*` package at startup, the
|
|
232
|
+
one its implementing module installed, and refuses to start when a loaded module declares a
|
|
233
|
+
range that copy falls outside. An exact pin (`0.1.7`) or a 0.x caret (`^0.1.2` stops at
|
|
234
|
+
`0.2.0`) therefore breaks a module's startup at the next interface release, with no change to
|
|
235
|
+
the module.
|
|
236
|
+
|
|
237
|
+
`antelopejs-check-interface-ranges` fails on such a range in `dependencies` or
|
|
238
|
+
`optionalDependencies`, in every `package.json` git tracks in the repository (playgrounds and
|
|
239
|
+
front-end layers included), whatever directory it runs from:
|
|
240
|
+
|
|
241
|
+
| The package… | must declare |
|
|
242
|
+
| ------------------------------------------------- | -------------------------------------------- |
|
|
243
|
+
| consumes the interface | `>=<floor> <1.0.0` (`<next major>` from 1.0) |
|
|
244
|
+
| implements it (listed in `antelopeJs.implements`) | `>=<floor> <0.<minor + 1>.0` while it is 0.x |
|
|
245
|
+
|
|
246
|
+
An implementer is the opposite case on purpose: it must never be handed a breaking minor it
|
|
247
|
+
does not implement. When the implemented interface sits in the same repository, its version is
|
|
248
|
+
the floor to use. `workspace:`, `file:` and `link:` specs are left alone.
|
|
249
|
+
|
|
250
|
+
```jsonc
|
|
251
|
+
// package.json
|
|
252
|
+
{
|
|
253
|
+
"scripts": {
|
|
254
|
+
"lint": "oxlint && antelopejs-check-interface-ranges",
|
|
255
|
+
},
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Pass paths to check only those manifests.
|
|
260
|
+
|
|
225
261
|
## Vendored anti-slop
|
|
226
262
|
|
|
227
263
|
`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/dist/oxc/fmt.mjs
CHANGED
|
@@ -17,6 +17,12 @@ const ANTELOPE_STYLE = {
|
|
|
17
17
|
trailingComma: "all"
|
|
18
18
|
};
|
|
19
19
|
/**
|
|
20
|
+
* Files that release tooling writes and commits without formatting them.
|
|
21
|
+
* changelogen puts two spaces after the ⚠️ of a breaking change, so the first
|
|
22
|
+
* release with one leaves the default branch failing `oxfmt --check`.
|
|
23
|
+
*/
|
|
24
|
+
const GENERATED_IGNORE_PATTERNS = ["**/CHANGELOG.md"];
|
|
25
|
+
/**
|
|
20
26
|
* The shared AntelopeJS oxfmt preset.
|
|
21
27
|
*
|
|
22
28
|
* Import order is deliberately absent: `perfectionist/sort-imports` owns it on
|
|
@@ -44,6 +50,7 @@ function antelopeFmtPreset(options = {}) {
|
|
|
44
50
|
ignorePatterns: [
|
|
45
51
|
...IGNORE_PATTERNS,
|
|
46
52
|
...AGENT_IGNORE_PATTERNS,
|
|
53
|
+
...GENERATED_IGNORE_PATTERNS,
|
|
47
54
|
...ignorePatterns
|
|
48
55
|
]
|
|
49
56
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antelopejs/tooling-configs",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
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
|
},
|