@antelopejs/tooling-configs 0.0.8 → 0.2.0

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
@@ -1,41 +1,55 @@
1
1
  # @antelopejs/tooling-configs
2
2
 
3
3
  <div align="center">
4
- <a href="https://www.npmjs.com/package/@antelopejs/tooling-configs"><img alt="NPM version" src="https://img.shields.io/npm/v/@antelopejs/tooling-configs.svg?style=for-the-badge&labelColor=000000"></a>
5
- <a href="./LICENSE"><img alt="License" src="https://img.shields.io/npm/l/@antelopejs/tooling-configs.svg?style=for-the-badge&labelColor=000000"></a>
4
+ <a href="https://www.npmjs.com/package/@antelopejs/tooling-configs"><img alt="npm" src="https://img.shields.io/npm/v/@antelopejs/tooling-configs?style=for-the-badge&labelColor=000000&color=000000"></a>
5
+ <a href="./LICENSE"><img alt="License" src="https://img.shields.io/npm/l/@antelopejs/tooling-configs?style=for-the-badge&labelColor=000000&color=000000"></a>
6
6
  <a href="https://discord.gg/sjK28QHrA7"><img src="https://img.shields.io/badge/Discord-18181B?logo=discord&style=for-the-badge&color=000000" alt="Discord"></a>
7
- <a href="https://antelopejs.com"><img src="https://img.shields.io/badge/Docs-18181B?style=for-the-badge&color=000000" alt="Documentation"></a>
8
7
  </div>
9
8
 
10
- Shared tooling presets for AntelopeJS repositories: [oxlint](https://oxc.rs) (including
11
- the vendored [anti-slop](https://github.com/dmmulroy/anti-slop) rules), [oxfmt](https://oxc.rs)
12
- and [Knip](https://knip.dev).
13
-
14
- Together they replace Biome outright — lint, format and import order — and Prettier in the
15
- Nuxt layers, since oxfmt formats `.vue` and sorts Tailwind classes natively. The front end
16
- keeps ESLint for linting, from a shared config here.
17
-
18
- ## Requirements
19
-
20
- Node **24** (or at least 22.18). oxlint reads `oxlint.config.ts` and loads JS plugins by
21
- executing TypeScript, which earlier runtimes cannot do.
9
+ The AntelopeJS guardrail pack: shared lint, format and dead-code presets, the commands that install and enforce them in a repository, and a check for the version ranges of interface dependencies.
10
+
11
+ - [oxlint](https://oxc.rs) and [oxfmt](https://oxc.rs) replace Biome: lint, format and Tailwind class sorting, `.vue` included.
12
+ - ESLint stays for the Nuxt front ends, from a shared config, until oxlint lints `.vue` files.
13
+ - [Knip](https://knip.dev) learns the entry points of an AntelopeJS module.
14
+ - Every preset is a function: options adjust it without copying it.
15
+ - `antelopejs-sync` writes the configs, hooks, scripts and agent instructions into a repository, and `antelopejs-sync --check` fails CI when they drift.
16
+ - Every lint rule is an error or off. Existing debt is recorded per repository and may only shrink.
17
+
18
+ ## What's in the box
19
+
20
+ | Export | Import | What it gives |
21
+ | ------------------ | ------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
22
+ | `./oxc/lint` | `import { antelopePreset } from "@antelopejs/tooling-configs/oxc/lint"` | The oxlint preset |
23
+ | `./oxc/fmt` | `import { antelopeFmtPreset } from "@antelopejs/tooling-configs/oxc/fmt"` | The oxfmt preset (house style) |
24
+ | `./eslint` | `import { antelopeNuxtConfig } from "@antelopejs/tooling-configs/eslint"` | ESLint for Nuxt layers |
25
+ | `./knip` | `import { antelopeKnipConfig } from "@antelopejs/tooling-configs/knip"` | Knip for AntelopeJS modules |
26
+ | `./oxc/anti-slop` | Loaded by the oxlint preset as a JS plugin | The vendored [anti-slop](https://github.com/dmmulroy/anti-slop) rules |
27
+ | `./oxc/guardrails` | Loaded by the oxlint preset as a JS plugin | The `antelopejs` comment and test-quality rules |
28
+ | bin | `antelopejs-sync` | Writes the guardrail pack into a repository; `--check` reports drift |
29
+ | bin | `antelopejs-checks` | Runs every check and prints only the failures |
30
+ | bin | `antelopejs-ratchet` | Fails when recorded debt grows; `--update` lowers the record |
31
+ | bin | `antelopejs-check-size` | Fails on authored files over 20,000 bytes and on blanket disable directives |
32
+ | bin | `antelopejs-check-interface-ranges` | Fails on interface ranges that break module startup |
22
33
 
23
34
  ## Install
24
35
 
36
+ Node.js 24, or at least 22.18: oxlint reads `oxlint.config.ts` and loads JS plugins by executing TypeScript.
37
+
25
38
  ```bash
26
- pnpm add -D oxlint@1.81.0 oxfmt @antelopejs/tooling-configs
39
+ pnpm add -D @antelopejs/tooling-configs oxlint@1.81.0 oxfmt
27
40
  ```
28
41
 
29
- Add `eslint-plugin-perfectionist` too unless you turn import sorting off — it is an
30
- optional peer dependency because it pulls ESLint and typescript-eslint along with it:
42
+ The tools are optional peer dependencies, so add only the ones a repository uses:
31
43
 
32
- ```bash
33
- pnpm add -D eslint-plugin-perfectionist
34
- ```
44
+ | Tool | Packages to add |
45
+ | ----------------------- | -------------------------------------------------------------------------------- |
46
+ | oxlint | `oxlint@1.81.0`, plus `oxlint-tsgolint` for the type-aware rules (on by default) |
47
+ | oxfmt | `oxfmt@^0.66.0` |
48
+ | ESLint | `@nuxt/eslint-config` |
49
+ | Knip | `knip@>=6` |
50
+ | Import sorting (opt-in) | `eslint-plugin-perfectionist` |
35
51
 
36
- Pin `oxlint` to the exact version this package depends on for `@oxlint/plugins`
37
- (currently **1.81.0**). The JS plugin API is still alpha and the two packages must move
38
- together.
52
+ Pin `oxlint` to the exact version this package uses for `@oxlint/plugins` (currently **1.81.0**): the JS plugin API is still alpha and the two must move together.
39
53
 
40
54
  ## oxlint
41
55
 
@@ -46,7 +60,7 @@ import { antelopePreset } from "@antelopejs/tooling-configs/oxc/lint";
46
60
 
47
61
  export default defineConfig({
48
62
  extends: [antelopePreset()],
49
- // Only read from the root config, never from a preset.
63
+ // oxlint reads this field only from the root config, never from a preset.
50
64
  options: { typeAware: true },
51
65
  });
52
66
  ```
@@ -61,50 +75,54 @@ export default defineConfig({
61
75
  }
62
76
  ```
63
77
 
64
- Type-aware rules (`typescript/no-floating-promises`) also need the tsgolint binary:
78
+ ### What the preset turns on
65
79
 
66
- ```bash
67
- pnpm add -D oxlint-tsgolint
68
- ```
80
+ | Group | Rules | Default |
81
+ | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
82
+ | oxc | the `correctness` category, with the `eslint`, `typescript`, `node`, `oxc`, `import` and `promise` plugins; `no-unused-vars` (`_` prefix allowed), `eqeqeq` (`== null` allowed), `radix`, `prefer-regex-literals` | error |
83
+ | Imports | `import/no-cycle`, `import/no-self-import`, `import/no-duplicates` | error |
84
+ | Type-aware | `typescript/no-floating-promises` and eight more type-aware rules | error |
85
+ | Complexity | `max-params` 5, `max-lines-per-function` 120, `max-lines` 500, `max-depth` 4, `complexity` 20 | error |
86
+ | Anti-slop | all 15 vendored rules | error |
87
+ | Guardrails | `antelopejs/no-comments`; in test files `antelopejs/no-fs-read-in-tests`, `antelopejs/no-constant-value-assertion`, `antelopejs/no-typeof-function-only-test` and `antelopejs/require-assertion` | error |
88
+ | Import sorting | `perfectionist/sort-imports`, autofixed by `oxlint --fix` | off |
69
89
 
70
- ### What the preset turns on
90
+ No rule warns. oxlint reports `correctness` as warnings by default, and warnings never fail the command, so the preset raises it to error. A repository with existing findings records them in `guardrails.ratchet.json` instead of lowering a severity (see [Guardrail pack](#guardrail-pack)).
91
+
92
+ The guardrail rules:
93
+
94
+ | Rule | Fails on |
95
+ | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
96
+ | `antelopejs/no-comments` | `//` and `/* */` comments. Allowed: TSDoc `/** */`, `SAFETY: <reason>` (one block, or a run of `//` lines), `/*!` license headers, `@ts-expect-error <reason>`, oxlint/eslint directives that name their rules, `/// <reference />`, `#__PURE__`/`@__PURE__`, `@vite-ignore` and webpack magic comments, `prettier-ignore`/`oxfmt-ignore` |
97
+ | `antelopejs/no-fs-read-in-tests` | `readFileSync` or `readdirSync` from `fs` in a test file. Files under `fixtures/`, `helpers/` and `locales/` may read |
98
+ | `antelopejs/require-assertion` | a test with no assertion. Counts: calls named `expect*` or `assert*`, `sinon.assert.*`, supertest `.expect(...)`, and should-style `x.should…` or `should(x)` |
99
+ | `antelopejs/no-constant-value-assertion` | `expect(X).toBe(literal)` where the root of `X` is an UPPER_SNAKE constant (`MAX_ITEMS`, `NAMES.if`, not `process.env.NODE_ENV`); also `.toEqual`, `.to.equal`, `.to.eq`, `.eql` and synonyms |
100
+ | `antelopejs/no-typeof-function-only-test` | a test whose only assertions are `toBeTypeOf("function")`, `to.be.a("function")` or equivalents |
101
+
102
+ Test files are `*.test.*`, `*.spec.*`, and files under `test/`, `tests/` and `__tests__/`, whatever the framework. The preset keeps four framework-neutral Jest-plugin rules: `no-disabled-tests`, `no-focused-tests`, `valid-describe-callback` and `valid-title`.
71
103
 
72
- | Group | Contents |
73
- | -------------- | ----------------------------------------------------------------------------------------------------------- |
74
- | oxc defaults | the `correctness` category, plus the `eslint`, `typescript`, `node`, `oxc`, `import` and `promise` plugins |
75
- | imports | `import/no-cycle`, `import/no-self-import`, `import/no-duplicates` as errors |
76
- | | (a repository with existing cycles starts with `importCycles: "warn"`) |
77
- | import sorting | `perfectionist/sort-imports`, autofixed by `oxlint --fix` |
78
- | complexity | `max-params` 5, `max-lines-per-function` 120, `max-lines` 500, `max-depth` 4, `complexity` 20 — as warnings |
79
- | anti-slop | all 15 generic rules, as warnings |
80
- | type-aware | `typescript/no-floating-promises` as an error |
104
+ Import sorting is off because it moves value imports across side-effect imports, and in an AntelopeJS module `import "./components"` is what registers the decorated classes. Turn it on only where import order does not matter.
81
105
 
82
106
  ### Options
83
107
 
84
108
  ```ts
85
109
  antelopePreset({
86
- antiSlop: "error", // "warn" (default) | "error" | false
87
- importCycles: "warn", // "error" (default) | "warn" | false
110
+ antiSlop: false, // true (default)
111
+ guardrails: false, // true (default)
112
+ importCycles: false, // true (default)
88
113
  complexity: { maxParams: 8 }, // override thresholds, or false
89
- importSorting: false, // leave imports alone
90
- typeAware: false, // no tsgolint in this repository
114
+ importSorting: true, // false (default); needs eslint-plugin-perfectionist
115
+ typeAware: false, // true (default); false when the repository has no oxlint-tsgolint
91
116
  });
92
117
  ```
93
118
 
94
- `correctness` is raised to error deliberately. oxlint reports it as warnings, and warnings
95
- do not fail the command — a preset that left it alone could never fail a pipeline, while
96
- Biome was failing the build on the same rules.
119
+ ### Add ignore patterns
97
120
 
98
- `antiSlop` and `complexity` default to warnings on purpose: a repository adopting them
99
- has findings to work through, and a red pipeline on day one teaches the team to ignore
100
- the pipeline. Raise them to `"error"` once the backlog is clear.
101
-
102
- ### Adding your own ignore patterns
103
-
104
- oxlint does not merge `ignorePatterns` across `extends`. A root config that declares its
105
- own **replaces** the preset's, so spread them:
121
+ oxlint does not merge `ignorePatterns` across `extends`: a root config that declares its own replaces the preset's. Spread the shared ones:
106
122
 
107
123
  ```ts
124
+ // oxlint.config.ts
125
+ import { defineConfig } from "oxlint";
108
126
  import {
109
127
  ANTELOPE_IGNORE_PATTERNS,
110
128
  antelopePreset,
@@ -116,6 +134,50 @@ export default defineConfig({
116
134
  });
117
135
  ```
118
136
 
137
+ ## Guardrail pack
138
+
139
+ Run `antelopejs-sync` at the repository root, then install dependencies. It is idempotent: a second run changes nothing.
140
+
141
+ ```bash
142
+ pnpm add -D @antelopejs/tooling-configs oxlint@1.81.0 oxlint-tsgolint oxfmt
143
+ pnpm exec antelopejs-sync
144
+ pnpm install
145
+ ```
146
+
147
+ | File | What `sync` does |
148
+ | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
149
+ | `oxlint.config.ts`, `oxfmt.config.ts`, `knip.config.ts` | Writes a config that extends the preset, unless the file already imports it |
150
+ | `lefthook.yml` | Pre-commit on staged files: oxfmt, oxlint, size. Pre-push: typecheck |
151
+ | `package.json` | `prepare` installs lefthook. Sets `checks`, `size`, `ratchet`, `ratchet:update`, `sync`, `sync:check`; adds missing `typecheck`, `lint`, `format`, `format:check`, `knip`; adds `lefthook` and `knip` |
152
+ | `.nvmrc` | `24` |
153
+ | `.agents/setup` | Orb setup: switches to the `.nvmrc` Node, activates `packageManager` through corepack, installs with the frozen lockfile |
154
+ | `AGENTS.md` | Writes the managed block between `<!-- antelopejs-guardrails:start -->` and `:end -->`; text outside the markers stays |
155
+ | `CLAUDE.md` | `@AGENTS.md` |
156
+ | `guardrails.ratchet.json` | Records the current findings, once, when the file is missing |
157
+
158
+ `--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
+
160
+ `pnpm run checks` runs `typecheck`, `lint`, `format:check`, `knip`, `test`, `size`, `ratchet` and `sync:check`, and prints only the output of the steps that failed.
161
+
162
+ ### Ratchet
163
+
164
+ `guardrails.ratchet.json` holds the debt a repository had when it adopted the pack:
165
+
166
+ ```json
167
+ {
168
+ "rules": { "antelopejs/no-comments": 22, "eslint/max-depth": 1 },
169
+ "oversizeFiles": { "src/legacy.ts": 31250 }
170
+ }
171
+ ```
172
+
173
+ - A rule listed under `rules` is off for `oxlint`, so editors, hooks and `lint` stay green. `antelopejs-ratchet` counts it with every rule on, and fails when a count rises. It prints the findings in the files the change touched.
174
+ - 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
+ - `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
+
177
+ ### Size
178
+
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. Pass paths to check only those files, as the pre-commit hook does. It 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.
180
+
119
181
  ## oxfmt
120
182
 
121
183
  ```ts
@@ -135,55 +197,23 @@ export default antelopeFmtPreset();
135
197
  }
136
198
  ```
137
199
 
138
- ### The house style
139
-
140
- | Option | Value |
141
- | ---------------------- | ---------- |
142
- | `printWidth` | 80 |
143
- | `tabWidth` / `useTabs` | 2 / spaces |
144
- | `semi` | true |
145
- | `singleQuote` | false |
146
- | `trailingComma` | `"all"` |
147
-
148
- Every field is set explicitly, including those that already match oxfmt's defaults.
149
- Repositories carry `.editorconfig` files that contradict each other — five declare tabs,
150
- two declare spaces, which is why the codebases drifted apart in the first place — and
151
- oxfmt falls back to `.editorconfig` for any field a config leaves unset. Stating all of
152
- them is what makes the style identical everywhere. Fix the repository's `.editorconfig`
153
- to match anyway, so editors agree with the formatter.
154
-
155
- Import order is not part of it: `perfectionist/sort-imports` owns that on the lint side.
156
- Enabling oxfmt's `sortImports` as well would have two tools sorting the same imports
157
- differently, each undoing the other on every save.
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.
200
+ The house style sets every field, so `.editorconfig` files cannot override it: `printWidth` 80, 2 spaces, semicolons, double quotes, `trailingComma: "all"`. Fix the repository's `.editorconfig` to match anyway, so editors agree with the formatter.
162
201
 
163
- Extra ignores are **added** to the shared ones rather than replacing them, so a repository
164
- adopting oxfmt in stages can park a directory without losing the rest:
165
-
166
- ```ts
167
- export default antelopeFmtPreset({ ignorePatterns: ["frontend-vue/**"] });
168
- ```
169
-
170
- In a front end, pass the theme stylesheet to replace `prettier-plugin-tailwindcss`:
202
+ The preset leaves import order to the linter and ignores `CHANGELOG.md`, which changelogen writes unformatted on every release. Other options of oxfmt pass through. Two are specific:
171
203
 
172
204
  ```ts
205
+ // oxfmt.config.ts
173
206
  export default antelopeFmtPreset({
174
- tailwindStylesheet: "./assets/css/main.css",
207
+ ignorePatterns: ["frontend-vue/**"], // added to the shared ignores, not replacing them
208
+ tailwindStylesheet: "./assets/css/main.css", // sorts Tailwind classes, replacing prettier-plugin-tailwindcss
175
209
  });
176
210
  ```
177
211
 
178
- ### Replacing Biome
212
+ ### Replace Biome
179
213
 
180
- Per repository: delete `biome.json` and the `@biomejs/biome` dependency, point `lint`,
181
- `lint:fix` and `format` at oxlint and oxfmt, and port anything the Biome config carried
182
- that the preset does not (`noRestrictedImports` guards become `import/no-cycle`). Reformat
183
- in its own commit and record it in `.git-blame-ignore-revs`, which GitHub honours, so the
184
- pass does not bury the history.
214
+ Delete `biome.json` and the `@biomejs/biome` dependency, point `lint`, `lint:fix` and `format` at oxlint and oxfmt, and port what the Biome config carried that the preset does not. Reformat in its own commit and list it in `.git-blame-ignore-revs`, which GitHub honours.
185
215
 
186
- ## ESLint (front ends)
216
+ ## ESLint (Nuxt front ends)
187
217
 
188
218
  oxlint does not lint `.vue` files yet, so the front end stays on ESLint:
189
219
 
@@ -194,15 +224,7 @@ import { antelopeNuxtConfig } from "@antelopejs/tooling-configs/eslint";
194
224
  export default antelopeNuxtConfig({ dirs: ["./playground"] });
195
225
  ```
196
226
 
197
- `@nuxt/eslint` supplies the Vue and TypeScript rules; the preset only records where
198
- AntelopeJS layers differ from it — single-word page components, `any` and `{}` as
199
- load-bearing types in the interface layer, `_`-prefixed unused bindings, and import order
200
- left to the formatter.
201
-
202
- Formatting rules are deliberately absent, `vue/max-attributes-per-line` included: oxfmt
203
- formats `.vue`, and a lint rule that also reflows markup would fight it on every save.
204
- Declare `@nuxt/eslint-config` explicitly — nine layers use it today through
205
- `@nuxt/eslint` without declaring it.
227
+ `@nuxt/eslint-config` supplies the Vue and TypeScript rules. The preset only records where AntelopeJS layers differ: single-word page components, `any` and `{}` allowed, `_`-prefixed unused bindings, and import order left alone. It sets no formatting rule, since oxfmt formats `.vue`. Every rule the resolved config sets to warn becomes an error, so no rule warns there either. Pass `rules` to layer your own on top, and declare `@nuxt/eslint-config` in the front end's `package.json`.
206
228
 
207
229
  ## Knip
208
230
 
@@ -213,39 +235,25 @@ import { antelopeKnipConfig } from "@antelopejs/tooling-configs/knip";
213
235
  export default antelopeKnipConfig();
214
236
  ```
215
237
 
216
- The defaults declare what Knip cannot infer for an AntelopeJS module: the interface
217
- subpaths as public API, and `src/test/**` plus a root `antelope.test.ts` as entry points,
218
- since `ajs module test` is not a runner Knip has a plugin for. Without them, Knip reports
219
- the whole test suite as dead code.
238
+ The defaults declare what Knip cannot infer for an AntelopeJS module:
220
239
 
221
- `src/index.ts` is not among them on purpose: Knip already maps the `main`/`bin` of
222
- package.json (`dist/index.js`) back onto `src/index.ts`, ahead of the `dist/**` ignore, so
223
- repeating it only earns a "redundant pattern" hint.
240
+ - entry points: `src/interfaces/**/*.ts` (public API), `src/test/**` and `antelope.test.ts` (run by `ajs module test`), and `scripts/**`;
241
+ - ignored: `dist/**`, `playground/**`, `.antelope/**`, and the `@antelopejs/*` dependencies, which the runtime loads by name.
224
242
 
225
- The front end is left out of `project` rather than ignored, so a repository that wants
226
- `frontend-vue/` analysed can add it under Knip's `workspaces` without the preset
227
- overriding the choice.
243
+ `entry`, `project`, `ignore`, `ignoreDependencies` and `ignoreBinaries` options are appended to the defaults. `frontend-vue/` stays out of `project`, so a repository can opt it in through Knip's `workspaces`.
228
244
 
229
245
  ## Interface ranges
230
246
 
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.
247
+ `@antelopejs/core` keeps one copy of each `@antelopejs/interface-*` package, the one its implementing module installed, and refuses to start when a module declares a range that copy falls outside. An exact pin (`0.1.7`) or a 0.x caret (`^0.1.2` stops at `0.2.0`) therefore breaks startup at the next interface release.
236
248
 
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:
249
+ `antelopejs-check-interface-ranges` fails on such a range in `dependencies` or `optionalDependencies`, in every `package.json` git tracks in the repository, from any directory:
240
250
 
241
251
  | The package… | must declare |
242
252
  | ------------------------------------------------- | -------------------------------------------- |
243
253
  | consumes the interface | `>=<floor> <1.0.0` (`<next major>` from 1.0) |
244
254
  | implements it (listed in `antelopeJs.implements`) | `>=<floor> <0.<minor + 1>.0` while it is 0.x |
245
255
 
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.
256
+ An implementer must never be handed a breaking minor it does not implement. `workspace:`, `file:` and `link:` specs are skipped. Pass paths to check only those manifests.
249
257
 
250
258
  ```jsonc
251
259
  // package.json
@@ -256,12 +264,23 @@ the floor to use. `workspace:`, `file:` and `link:` specs are left alone.
256
264
  }
257
265
  ```
258
266
 
259
- Pass paths to check only those manifests.
260
-
261
267
  ## Vendored anti-slop
262
268
 
263
- `src/oxc/anti-slop/` is a copy of upstream, kept byte-identical so it can be diffed
264
- against new releases. See `src/oxc/anti-slop/VENDORED.md` for the commit it came from and
265
- any local edits. It is excluded from this repository's own formatting and linting.
269
+ `src/oxc/anti-slop/` is a byte-identical copy of upstream, so it can be diffed against new releases; `src/oxc/anti-slop/VENDORED.md` records the commit and any local edit. This repository does not format or lint it. Upstream is MIT (`src/oxc/anti-slop/LICENSE`).
270
+
271
+ ## Documentation guardrail
272
+
273
+ The [documentation action](./docs/documentation-checks.md) checks local Markdown
274
+ links and explicitly labeled compatibility tables. Consumers can also opt in
275
+ complete TypeScript examples and reference-page export coverage. Pin
276
+ `AntelopeJS/tooling-configs/actions/docs` to a full commit SHA; no npm release is
277
+ required. The guide includes CI and local commands, annotation syntax, and the
278
+ checks that remain the author's responsibility.
279
+
280
+ ## Contributing
281
+
282
+ See [CONTRIBUTING.md](./CONTRIBUTING.md). `pnpm install`, then `pnpm checks` runs the typecheck, lint, format check, tests, size check and ratchet.
283
+
284
+ ## License
266
285
 
267
- Upstream is MIT (`src/oxc/anti-slop/LICENSE`); the rest of this repository is Apache-2.0.
286
+ [Apache-2.0](./LICENSE), except the vendored anti-slop rules (MIT).
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { execFileSync } from "node:child_process";
3
2
  import { readFileSync } from "node:fs";
4
3
  import { join, relative } from "node:path";
4
+ import { execFileSync } from "node:child_process";
5
5
  //#region src/interface-ranges.ts
6
6
  /**
7
7
  * At startup `@antelopejs/core` keeps one copy of each interface package, the
@@ -18,11 +18,13 @@ import { join, relative } from "node:path";
18
18
  * is 0.x.
19
19
  */
20
20
  const INTERFACE_PACKAGE_PREFIX = "@antelopejs/interface-";
21
+ /** A module's own interface linked from the workspace is the copy it builds against, not a release it resolves. */
21
22
  const LOCAL_SPEC_PREFIXES = [
22
23
  "workspace:",
23
24
  "file:",
24
25
  "link:"
25
26
  ];
27
+ /** The fields `@antelopejs/core` checks at startup. */
26
28
  const CHECKED_FIELDS = ["dependencies", "optionalDependencies"];
27
29
  const BOUNDED_RANGE = /^>=(\d+)\.(\d+)\.(\d+) <(\d+)\.(\d+)\.(\d+)$/;
28
30
  const SINGLE_VERSION = /^[\^~=]?v?(\d+)\.(\d+)\.(\d+)$/;
@@ -66,6 +68,7 @@ function readFloor(spec) {
66
68
  function compareVersions(a, b) {
67
69
  return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
68
70
  }
71
+ /** A range is accepted when it is bounded for its role and admits the interface this package builds against. */
69
72
  function isAcceptedRange(spec, role, local) {
70
73
  const bounded = BOUNDED_RANGE.exec(spec);
71
74
  if (!bounded) return false;
@@ -109,6 +112,12 @@ function describeViolation(violation) {
109
112
  }
110
113
  //#endregion
111
114
  //#region src/bin/check-interface-ranges.ts
115
+ /**
116
+ * Every manifest in the repository by default, whatever directory the lint
117
+ * runs from: playgrounds and front-end layers are loaded by the runtime too, a
118
+ * playground is where a stale interface range first broke a startup, and an
119
+ * implemented interface usually sits in a sibling package.
120
+ */
112
121
  function trackedManifests() {
113
122
  const git = (args) => execFileSync("git", args, { encoding: "utf8" }).trim();
114
123
  const root = git(["rev-parse", "--show-toplevel"]);
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ import { a as repositoryRoot, i as repositoryPath, o as runCli, t as authoredSizes } from "../files-BSl0U8lT.mjs";
3
+ import { t as checkSize } from "../size-DRCgYCLA.mjs";
4
+ import { existsSync, readFileSync } from "node:fs";
5
+ import { join, resolve } from "node:path";
6
+ import { spawnSync } from "node:child_process";
7
+ import { parseArgs } from "node:util";
8
+ //#region src/guardrails/directives.ts
9
+ const CODE_FILE = /\.(?:[cm]?[jt]sx?|vue)$/u;
10
+ const BLANKET_DISABLE = /(?:\/\*|\/\/)\s*((?:eslint|oxlint)-disable(?:-line|-next-line)?)\s*(?:--[^\n]*?)?(?:\*\/|$)/gmu;
11
+ function fileText(root, file, staged) {
12
+ if (!staged) {
13
+ const path = join(root, file);
14
+ return existsSync(path) ? readFileSync(path, "utf8") : "";
15
+ }
16
+ const result = spawnSync("git", ["show", `:${file}`], {
17
+ cwd: root,
18
+ encoding: "utf8",
19
+ maxBuffer: 67108864
20
+ });
21
+ if (result.error) throw result.error;
22
+ return result.status === 0 ? result.stdout : "";
23
+ }
24
+ /** Reports disable directives without a rule list: oxlint honours them before any rule runs, so they also silence no-comments. */
25
+ function checkBlanketDisables(root, files, staged = false) {
26
+ const failures = [];
27
+ for (const file of authoredSizes(root, files, staged).keys()) {
28
+ if (!CODE_FILE.test(file)) continue;
29
+ const text = fileText(root, file, staged);
30
+ for (const match of text.matchAll(BLANKET_DISABLE)) {
31
+ const line = text.slice(0, match.index).split("\n").length;
32
+ failures.push(`${file}:${line}: \`${match[1]}\` without a rule list disables every rule, the comment ban included. Name the rule(s) and give a reason, for example: oxlint-disable-next-line no-debugger -- debugging this operation.`);
33
+ }
34
+ }
35
+ return failures;
36
+ }
37
+ //#endregion
38
+ //#region src/bin/check-size.ts
39
+ runCli(() => {
40
+ const root = repositoryRoot();
41
+ const { values, positionals } = parseArgs({
42
+ options: { staged: { type: "boolean" } },
43
+ allowPositionals: true
44
+ });
45
+ const files = positionals.length ? positionals.map((file) => repositoryPath(root, resolve(file))) : void 0;
46
+ const failures = [...checkSize(root, files, values.staged), ...checkBlanketDisables(root, files, values.staged)];
47
+ for (const failure of failures) process.stderr.write(`${failure}\n`);
48
+ return failures.length ? 1 : 0;
49
+ });
50
+ //#endregion
51
+ export {};
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ import { a as repositoryRoot, o as runCli } from "../files-BSl0U8lT.mjs";
3
+ import { i as parseManifest, r as packageManager } from "../package-B1aipTbn.mjs";
4
+ import { readFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { spawnSync } from "node:child_process";
7
+ //#region src/guardrails/checks.ts
8
+ const CHECK_STEPS = [
9
+ "typecheck",
10
+ "lint",
11
+ "format:check",
12
+ "knip",
13
+ "test",
14
+ "size",
15
+ "ratchet",
16
+ "sync:check"
17
+ ];
18
+ function runChecks(root) {
19
+ const manifest = parseManifest(readFileSync(join(root, "package.json"), "utf8"));
20
+ const manager = packageManager(manifest);
21
+ const failures = [];
22
+ for (const step of CHECK_STEPS) {
23
+ if (!manifest.scripts?.[step]) {
24
+ failures.push(`[${step}] Missing package.json script "${step}". Run antelopejs-sync.\n`);
25
+ continue;
26
+ }
27
+ const result = spawnSync(manager, ["run", step], {
28
+ cwd: root,
29
+ encoding: "utf8",
30
+ env: {
31
+ ...process.env,
32
+ CI: "1"
33
+ },
34
+ maxBuffer: 67108864
35
+ });
36
+ if (result.status !== 0) failures.push(`[${step}]\n${result.stdout ?? ""}${result.stderr ?? ""}${result.error ? `${result.error.message}\n` : ""}${result.signal ? `Terminated by ${result.signal}\n` : ""}`);
37
+ }
38
+ return {
39
+ failed: failures.length,
40
+ output: `${failures.join("\n")}Checks: ${CHECK_STEPS.length - failures.length} passed, ${failures.length} failed.\n`
41
+ };
42
+ }
43
+ //#endregion
44
+ //#region src/bin/checks.ts
45
+ runCli(() => {
46
+ const result = runChecks(repositoryRoot());
47
+ process.stdout.write(result.output);
48
+ return result.failed ? 1 : 0;
49
+ });
50
+ //#endregion
51
+ export {};
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+ import { o as readCeilings } from "../ceilings--pJmhSde.mjs";
3
+ import { a as repositoryRoot, i as repositoryPath, o as runCli } from "../files-BSl0U8lT.mjs";
4
+ import { n as ratchet, r as lintDiagnostics } from "../ratchet-DNsVZH9s.mjs";
5
+ import { resolve } from "node:path";
6
+ import { execFileSync } from "node:child_process";
7
+ import { parseArgs } from "node:util";
8
+ //#region src/guardrails/staged-ratchet.ts
9
+ function stagedRatchet(root, files) {
10
+ const targets = files ?? execFileSync("git", [
11
+ "diff",
12
+ "--cached",
13
+ "--name-only",
14
+ "--diff-filter=ACMR",
15
+ "-z"
16
+ ], {
17
+ cwd: root,
18
+ encoding: "utf8"
19
+ }).split("\0").filter(Boolean);
20
+ if (!targets.length) return [];
21
+ const additions = /* @__PURE__ */ new Map();
22
+ for (const file of targets) {
23
+ const diff = execFileSync("git", [
24
+ "diff",
25
+ "--cached",
26
+ "--no-ext-diff",
27
+ "--no-color",
28
+ "--unified=0",
29
+ "--",
30
+ file
31
+ ], {
32
+ cwd: root,
33
+ encoding: "utf8"
34
+ });
35
+ additions.set(file, Array.from(diff.matchAll(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/gm), ([, line, count]) => ({
36
+ start: Number(line),
37
+ end: Number(line) + Number(count ?? 1) - 1
38
+ })));
39
+ }
40
+ const ceilings = readCeilings(root);
41
+ return lintDiagnostics(root, void 0, targets).filter(({ rule, file, line }) => Object.hasOwn(ceilings.rules, rule) && line !== void 0 && additions.get(file)?.some(({ start, end }) => line >= start && line <= end)).map((diagnostic) => `${diagnostic.location}: ${diagnostic.message} [${diagnostic.rule}]\n Fix: ${diagnostic.help ?? "Resolve this finding on the added line; the ceiling covers only legacy findings on untouched lines."}`);
42
+ }
43
+ //#endregion
44
+ //#region src/bin/ratchet.ts
45
+ runCli(() => {
46
+ const { values, positionals } = parseArgs({
47
+ options: {
48
+ update: { type: "boolean" },
49
+ staged: { type: "boolean" }
50
+ },
51
+ allowPositionals: true
52
+ });
53
+ const root = repositoryRoot();
54
+ if (values.update && values.staged) throw new Error("--staged cannot be combined with --update.");
55
+ if (positionals.length && !values.staged) throw new Error("File arguments require --staged.");
56
+ const result = values.staged ? {
57
+ failures: stagedRatchet(root, positionals.length ? positionals.map((file) => repositoryPath(root, resolve(file))) : void 0),
58
+ lowered: []
59
+ } : ratchet(root, values.update);
60
+ for (const message of result.lowered) process.stdout.write(`Lowered ${message}\n`);
61
+ for (const message of result.failures) process.stderr.write(`${message}\n`);
62
+ if (!result.failures.length) process.stdout.write("Ratchet: within ceilings.\n");
63
+ return result.failures.length ? 1 : 0;
64
+ });
65
+ //#endregion
66
+ export {};
@@ -0,0 +1 @@
1
+ export {}