@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.6",
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
  },