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