@rdjperron/lint 0.3.0 → 0.5.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 +179 -63
- package/dist/config-CD2iCM6L.mjs +346 -0
- package/dist/config-CD2iCM6L.mjs.map +1 -0
- package/dist/config.d.mts +109 -11
- package/dist/config.mjs +2 -218
- package/dist/index.mjs +44 -10
- package/dist/index.mjs.map +1 -1
- package/dist/verify.d.mts +1 -92
- package/dist/verify.mjs +124 -256
- package/dist/verify.mjs.map +1 -1
- package/fixtures/README.md +17 -24
- package/package.json +26 -17
- package/dist/config.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -3,44 +3,38 @@
|
|
|
3
3
|
One Oxlint JS plugin, `rdjperron`, with twenty rules against low-evidence code, the config data a
|
|
4
4
|
project spreads into its own `vite.config.ts`, and the check that proves every rule still fires.
|
|
5
5
|
|
|
6
|
-
| Entry
|
|
7
|
-
|
|
|
8
|
-
| `@rdjperron/lint`
|
|
9
|
-
| `@rdjperron/lint/config`
|
|
10
|
-
|
|
|
11
|
-
| `@rdjperron/lint/fixtures/*` | One deliberately violating file per rule, the input the verify command lints. |
|
|
6
|
+
| Entry | What it gives you |
|
|
7
|
+
| ------------------------ | ---------------------------------------------------------------------------------- |
|
|
8
|
+
| `@rdjperron/lint` | The plugin. One `jsPlugins` entry, one prefix, twenty rules. |
|
|
9
|
+
| `@rdjperron/lint/config` | `recommended`, the override builders, and the `fmt`, `ignore` and `staged` blocks. |
|
|
10
|
+
| `rdjperron-lint-verify` | The command that proves every rule still fires. No config, no arguments. |
|
|
12
11
|
|
|
13
12
|
Seventeen of the rules are a copy of [dmmulroy/anti-slop](https://github.com/dmmulroy/anti-slop),
|
|
14
|
-
owned and edited here (`src/anti-slop/VENDORED.md` in
|
|
13
|
+
owned and edited here (`src/anti-slop/VENDORED.md` in this repository records the commit). The
|
|
15
14
|
other three are written here.
|
|
16
15
|
|
|
17
|
-
|
|
16
|
+
Measured on vite-plus 0.3.0, 0.3.1, 0.3.2 and 0.3.3, which is oxlint 1.79.0 through 1.83.0
|
|
17
|
+
(2026-09-19). All twenty rules fired on all four.
|
|
18
18
|
|
|
19
19
|
## Install
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
npm install --save-dev --save-exact @rdjperron/lint
|
|
22
|
+
npm install --save-dev --save-exact @rdjperron/lint
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
and not the bundled oxlint version:
|
|
25
|
+
That is the whole installation. `@oxlint/plugins`, which the rules import `defineRule` and the AST
|
|
26
|
+
types from, is an ordinary dependency here and arrives with the package; there is no version of it
|
|
27
|
+
for you to pin or to move.
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Pin `@oxlint/plugins` exactly to that number. The peer range here is a caret, so an exact pin
|
|
36
|
-
anywhere in the same major satisfies it. Move the pin every time `vp` moves; `rdjperron-lint-verify`
|
|
37
|
-
fails when the two drift apart.
|
|
29
|
+
`vite-plus` is an optional peer, `>=0.3.0`, so a project on plain oxlint can still load the plugin.
|
|
30
|
+
The floor is `react()`, not the rules: oxlint 1.77.0, which vite-plus 0.2.9 declares, has no
|
|
31
|
+
`react/immutability`, `react/purity` or `react/refs`, and refuses to build a config that names them.
|
|
38
32
|
|
|
39
33
|
## Use it
|
|
40
34
|
|
|
41
35
|
```ts
|
|
42
36
|
// vite.config.ts
|
|
43
|
-
import {
|
|
37
|
+
import { fmt, ignore, react, recommended, staged, tests } from "@rdjperron/lint/config";
|
|
44
38
|
import { defineConfig } from "vite-plus";
|
|
45
39
|
|
|
46
40
|
export default defineConfig({
|
|
@@ -48,22 +42,21 @@ export default defineConfig({
|
|
|
48
42
|
fmt: { ...fmt, ignorePatterns: ignore },
|
|
49
43
|
lint: {
|
|
50
44
|
extends: [recommended],
|
|
51
|
-
plugins: ["typescript", "unicorn", "oxc", "import", "promise", "node"],
|
|
52
|
-
categories: { correctness: "error", suspicious: "warn", perf: "warn" },
|
|
53
45
|
env: { builtin: true, node: true },
|
|
54
46
|
overrides: [
|
|
55
47
|
...react(["app/**"], { useEffectAllowedIn: "hooks/use-mount-effect.ts" }),
|
|
56
48
|
...tests(["**/*.test.ts", "**/*.test.tsx"]),
|
|
57
|
-
...fixtures(),
|
|
58
49
|
],
|
|
59
|
-
options: {
|
|
50
|
+
options: { typeCheck: true },
|
|
60
51
|
ignorePatterns: ignore,
|
|
61
52
|
},
|
|
62
53
|
});
|
|
63
54
|
```
|
|
64
55
|
|
|
65
|
-
`jsPlugins`
|
|
66
|
-
|
|
56
|
+
`jsPlugins`, `plugins`, `categories` and `options.typeAware` need no entry of their own:
|
|
57
|
+
`recommended` carries all four, and they come through `extends`. `options` merges key by key, so
|
|
58
|
+
writing `options: { typeCheck: false }` keeps the inherited `typeAware`. Registering the plugin by
|
|
59
|
+
hand works too, in either form, because it declares its own `meta.name`:
|
|
67
60
|
|
|
68
61
|
```ts
|
|
69
62
|
jsPlugins: ["@rdjperron/lint"];
|
|
@@ -80,57 +73,116 @@ Measured on vite-plus 0.3.1 and oxlint 1.81.0 (2026-09-17), with a base config o
|
|
|
80
73
|
| `jsPlugins`, `rules`, `overrides`, `plugins`, `categories`, `options` | yes, and the consumer's own entries win |
|
|
81
74
|
| `env`, `settings`, `ignorePatterns` | no, the consumer writes its own |
|
|
82
75
|
|
|
83
|
-
So `recommended` carries
|
|
84
|
-
|
|
85
|
-
`ignorePatterns`, not a field you inherit.
|
|
76
|
+
So `recommended` carries everything on the inherited side: the plugin entry, the rules, the plugin
|
|
77
|
+
set, the categories and `options.typeAware`. What is left to you is `env` and `ignorePatterns`;
|
|
78
|
+
`ignore` is an array you spread into `ignorePatterns`, not a field you inherit.
|
|
79
|
+
|
|
80
|
+
`options` is the one field that could read either way, so it was measured on its own (2026-09-19):
|
|
81
|
+
it merges key by key. A base carrying `{ typeAware: true }` and a consumer writing
|
|
82
|
+
`{ typeCheck: false }` leaves `typeAware` on. Only naming `typeAware` yourself overrides it.
|
|
86
83
|
|
|
87
|
-
Two more things oxlint does quietly. An override that
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
84
|
+
Two more things oxlint does quietly. An override that names a rule of a plugin the config does not
|
|
85
|
+
enable has to add that plugin in its own `plugins`, or the entry is dropped with no error; a plugin
|
|
86
|
+
already in the top-level set needs no repeating, and an override's `plugins` adds to that set rather
|
|
87
|
+
than replacing it (measured on vite-plus 0.3.1 and oxlint 1.81.0, 2026-09-19). And a relative plugin
|
|
88
|
+
specifier inside an `extends` object is refused; a package name is what works.
|
|
91
89
|
|
|
92
90
|
### The config exports
|
|
93
91
|
|
|
94
|
-
| Export
|
|
95
|
-
|
|
|
96
|
-
| `plugin`
|
|
97
|
-
| `RULE_IDS`, `RuleId`
|
|
98
|
-
| `recommended`
|
|
99
|
-
| `react(files, options?)`
|
|
100
|
-
| `i18n(files, options?)`
|
|
101
|
-
| `expo(files)`
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
92
|
+
| Export | What it is |
|
|
93
|
+
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
94
|
+
| `plugin` | `{ name: "rdjperron", specifier: "@rdjperron/lint" }`. |
|
|
95
|
+
| `RULE_IDS`, `RuleId` | The twenty prefixed ids, as a readonly tuple and as a type. |
|
|
96
|
+
| `recommended` | The config object for `lint.extends`: the plugin entry, the rules below, the built-in choices, the plugin set, the categories and `options.typeAware`. |
|
|
97
|
+
| `react(files, options?)` | The hooks rules, the compiler's rules and the globals. `options.env` is the environment those files run in, `{ browser: true }` by default, so a React Native consumer names its own. `options.useEffectAllowedIn` names the one file allowed to call `useEffect`, puts its name in every finding, and switches both `no-use-effect` and `react/exhaustive-deps` off for it. |
|
|
98
|
+
| `i18n(files, options?)` | `rdjperron/no-hardcoded-jsx-text` at error. `options.pattern` sets the word pattern. |
|
|
99
|
+
| `expo(files, options?)` | The React Native facts: `static-public-env` at error, `typescript/no-require-imports` relaxed for asset extensions, `unicorn/no-array-sort` off. `options.assetExts` adds your `metro.config.js` extensions, `options.reactFiles` adds the two `react/*` rules an Expo app needs. |
|
|
100
|
+
| `restrictedImports(…)` | One `no-restricted-imports` override per file set, expanded from one declaration. See below. |
|
|
101
|
+
| `tests(files)` | `any`, `!`, `no-runtime-typeof` and the safety-comment rule off, with the reason on each. |
|
|
102
|
+
| `decoders(files, options?)` | `no-runtime-typeof` and `no-unknown-parameters` at error everywhere, each hinting at the files in `files`, and `no-runtime-typeof` off inside them. `options.hint` replaces the generated sentence. |
|
|
103
|
+
| `fmt` | `printWidth` 100, double quotes, semicolons, two spaces. |
|
|
104
|
+
| `ignore` | Dependencies, build output, the AI agent directories and the Vitest JSON report. |
|
|
105
|
+
| `staged` | `vp check --fix` on JS and TS sources. |
|
|
107
106
|
|
|
108
107
|
`recommended` puts every rule at `error` except `no-hardcoded-jsx-text` and `static-public-env`,
|
|
109
108
|
which belong to a translated app and to an Expo app and are left to `i18n()` and `expo()`. The
|
|
110
109
|
measurements behind those severities are in the comments beside them.
|
|
111
110
|
|
|
111
|
+
### `restrictedImports`, one declaration for every file set
|
|
112
|
+
|
|
113
|
+
Two overrides matching one file do not merge. The later one replaces the rule's whole entry, so
|
|
114
|
+
every file set needs `no-restricted-imports` written out in full. A repo with ten file sets
|
|
115
|
+
restates the same list ten times, and a new ban has to be added to all ten. Forget one and that
|
|
116
|
+
file set silently keeps importing what the others may not.
|
|
117
|
+
|
|
118
|
+
Here each entry is named once and each scope says which scope it starts `from`, what it `add`s and
|
|
119
|
+
what it `drop`s. Nothing is matched by glob and nothing is guessed: one override comes out per
|
|
120
|
+
scope, in the order you declared them, so you declare broad to narrow.
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
const RN_TEXT = { name: "react-native", importNames: ["Text"], message: "Text goes through <T role>." };
|
|
124
|
+
const RN_ALERT = { name: "react-native", importNames: ["Alert"], message: "lib/alert.ts answers on both targets." };
|
|
125
|
+
const SCHEMA = { group: ["**/convex/schema"], message: "convex/validators.ts is what the client reads." };
|
|
126
|
+
|
|
127
|
+
...restrictedImports({
|
|
128
|
+
paths: { RN_TEXT, RN_ALERT },
|
|
129
|
+
patterns: { SCHEMA },
|
|
130
|
+
scopes: {
|
|
131
|
+
all: { files: ["**"], use: ["RN_TEXT", "RN_ALERT", "SCHEMA"] },
|
|
132
|
+
kit: { files: ["components/ui/**"], from: "all", drop: ["RN_TEXT"] },
|
|
133
|
+
},
|
|
134
|
+
}),
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
That returns two overrides. The first bans all three everywhere, the second bans `RN_ALERT` and
|
|
138
|
+
`SCHEMA` under `components/ui/`, which is where `Text` is wrapped. A ban added to `paths` and to
|
|
139
|
+
`all` reaches every scope that descends from it, and `components/ui/` keeps its one exemption.
|
|
140
|
+
|
|
141
|
+
The broadest scope is an override on `files: ["**"]` rather than a top-level rule. Measured on
|
|
142
|
+
vite-plus 0.3.1 and oxlint 1.81.0, 2026-09-19: the two behave the same, files matched by no other
|
|
143
|
+
override included, so the whole list reads in one place.
|
|
144
|
+
|
|
145
|
+
Severity is `error`, always. Every message is yours.
|
|
146
|
+
|
|
147
|
+
TypeScript rejects four mistakes on the spot:
|
|
148
|
+
|
|
149
|
+
- a name in `use`, `add` or `drop` that is no key of `paths` or `patterns`,
|
|
150
|
+
- a `from` naming a scope that does not exist,
|
|
151
|
+
- `use` and `from` on the same scope,
|
|
152
|
+
- a scope with neither.
|
|
153
|
+
|
|
154
|
+
Three more depend on the order scopes resolve in, so they throw when the config loads, naming the
|
|
155
|
+
scope and the entry:
|
|
156
|
+
|
|
157
|
+
- a `from` naming a scope declared after this one,
|
|
158
|
+
- a `drop` of a name the parent does not restrict,
|
|
159
|
+
- an `add` of a name the parent already restricts.
|
|
160
|
+
|
|
161
|
+
The last two exist because both are a claim about the parent that is false, and a config that
|
|
162
|
+
states something false about its parent is one rename away from lifting a ban.
|
|
163
|
+
|
|
112
164
|
## Prove the rules still fire
|
|
113
165
|
|
|
114
166
|
```bash
|
|
115
167
|
npx rdjperron-lint-verify
|
|
116
168
|
```
|
|
117
169
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
every rule fired on the file named after it
|
|
121
|
-
|
|
122
|
-
like "no violations". Nothing else notices.
|
|
170
|
+
No configuration and no arguments. A rule can go silently dead after a linter upgrade, and "reports
|
|
171
|
+
nothing" looks exactly like "no violations", so nothing else in a repo notices. This asserts a
|
|
172
|
+
positive: every rule fired, each on the file named after it. It prints a table and exits non-zero on
|
|
173
|
+
the first rule that stayed quiet.
|
|
123
174
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
175
|
+
The fixtures ship in the package, one deliberately violating `fixtures/<rule>.fixture.ts(x)` per
|
|
176
|
+
rule. The command copies them into a throwaway project under your system temp directory, beside a
|
|
177
|
+
generated config that puts every rule at error, with your `node_modules` symlinked in, and removes
|
|
178
|
+
the whole directory afterwards whether the run passed or failed. Your own config, your ignore
|
|
179
|
+
patterns and your severities take no part.
|
|
128
180
|
|
|
129
|
-
|
|
130
|
-
|
|
181
|
+
The linter it runs is the oxlint your local `vite-plus` depends on, so the run that matters is the
|
|
182
|
+
one right after you move `vite-plus`, and in CI.
|
|
131
183
|
|
|
132
|
-
|
|
133
|
-
|
|
184
|
+
One thing not to do: do not delete the fixtures. They are the only thing that catches a rule going
|
|
185
|
+
dead.
|
|
134
186
|
|
|
135
187
|
## The rules
|
|
136
188
|
|
|
@@ -167,12 +219,76 @@ scope it off; the messages, the options and the cases are in the rule and its te
|
|
|
167
219
|
- `rdjperron/no-hardcoded-jsx-text` takes `{ pattern?: string }`, a regex source compiled with `u`. The
|
|
168
220
|
default `\p{L}{3,}` says three letters in a row is a word; digits, separators and bullets are
|
|
169
221
|
not. It looks at JSX text and at string literals placed as children, never at attribute values.
|
|
170
|
-
- `rdjperron/no-runtime-typeof` takes `{ allowInTypeGuards?: boolean }`. `recommended`
|
|
171
|
-
`typeof` inside a type predicate is the decode the rule asks for, named and
|
|
222
|
+
- `rdjperron/no-runtime-typeof` takes `{ allowInTypeGuards?: boolean, hint?: string }`. `recommended`
|
|
223
|
+
turns the first on: a `typeof` inside a type predicate is the decode the rule asks for, named and
|
|
224
|
+
reusable. The hint is appended to every finding; `decoders(files)` sets it.
|
|
225
|
+
- `rdjperron/no-unknown-parameters` takes `{ hint?: string }`, appended to every finding, so a project
|
|
226
|
+
can name the modules where the parse belongs. `decoders(files)` sets it.
|
|
172
227
|
- `rdjperron/static-public-env` has no options.
|
|
173
228
|
|
|
229
|
+
## Name the file that holds the fix
|
|
230
|
+
|
|
231
|
+
A rule tells a reader what is wrong. When the fix is the same every time, the finding can tell them
|
|
232
|
+
where to go as well, which is what `useEffectAllowedIn` and `decoders(files)` are for: they put a
|
|
233
|
+
sentence naming the sanctioned file in every finding of the rule that will send someone there.
|
|
234
|
+
`decoders(["lib/api-error.ts", "convex/lib/json.ts"])` reads:
|
|
235
|
+
|
|
236
|
+
```
|
|
237
|
+
Parameter `body` leaves input unparsed. Accept a named domain type; run the expected schema or
|
|
238
|
+
parser at the I/O boundary before calling this function. Decode the value in lib/api-error.ts or
|
|
239
|
+
convex/lib/json.ts, then call that decoder from here.
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
The same sentence lands on `no-runtime-typeof`, and inside those files that rule is off, because a
|
|
243
|
+
`typeof` there is the parse rather than a narrowing in place of one.
|
|
244
|
+
|
|
174
245
|
## Severities are measurements
|
|
175
246
|
|
|
176
247
|
Land a new rule at `warn`, fix or scope what fires, then promote it. When a rule fires, fix the
|
|
177
248
|
code or add a narrowly scoped override with the measurement that justifies it. Never weaken the
|
|
178
249
|
rule, add a cast, or rename a symbol to dodge the check.
|
|
250
|
+
|
|
251
|
+
## Moving from 0.4
|
|
252
|
+
|
|
253
|
+
Nothing is removed. One builder changed what it does, and three are new.
|
|
254
|
+
|
|
255
|
+
`expo(files)` used to set `static-public-env` and nothing else. It now also puts
|
|
256
|
+
`typescript/no-require-imports` at error with the asset extensions allowed, and turns
|
|
257
|
+
`unicorn/no-array-sort` off. If your config carries either by hand, delete it, and pass
|
|
258
|
+
`assetExts` for what your `metro.config.js` adds. An asset `require()` is now allowed only in the
|
|
259
|
+
files you hand to `expo()`. If you allowed it everywhere before, a `require()` of an asset outside
|
|
260
|
+
those files reports.
|
|
261
|
+
|
|
262
|
+
| You write by hand today | 0.5.0 gives you |
|
|
263
|
+
| ------------------------------------------------------------------------ | --------------------------------------------------- |
|
|
264
|
+
| The same `no-restricted-imports` list in every override, minus one entry | `restrictedImports({ paths, patterns, scopes })` |
|
|
265
|
+
| `no-runtime-typeof` off for your decoder modules | `decoders(files)`, which also names them in the fix |
|
|
266
|
+
| `react/capitalized-calls` off, `react/style-prop-object` for `StatusBar` | `expo(files, { reactFiles })` |
|
|
267
|
+
| `react/exhaustive-deps` off for the `useEffectAllowedIn` file | `react()` does it |
|
|
268
|
+
|
|
269
|
+
Measured on rummy, 2026-09-19: `vite.config.ts` went from 447 lines to 366. 132 planted findings
|
|
270
|
+
across ten import scopes came out identical, position for position, before and after. The one
|
|
271
|
+
difference is the asset rule, which moved from warn to error and stopped allowing assets outside
|
|
272
|
+
the phone's files.
|
|
273
|
+
|
|
274
|
+
## Moving from 0.3
|
|
275
|
+
|
|
276
|
+
0.4.0 moved the rules' health out of the consumer. Everything below is something you delete, and
|
|
277
|
+
nothing replaces it.
|
|
278
|
+
|
|
279
|
+
| Delete | Why it is gone |
|
|
280
|
+
| ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
281
|
+
| `@oxlint/plugins` from your `package.json` | An ordinary dependency here now. Nothing left to pin, and nothing to move on a `vp` upgrade. |
|
|
282
|
+
| `plugins: [...]` from `lint` | `recommended` carries the set its rules need. Keep yours only if you add a plugin of your own. |
|
|
283
|
+
| `categories: {...}` from `lint` | `recommended` carries `correctness: "error"`, `suspicious: "warn"`, `perf: "warn"`. |
|
|
284
|
+
| `typeAware: true` from `lint.options` | `recommended` carries it, and `options` merges key by key, so your `typeCheck` still applies. |
|
|
285
|
+
| `fixtures()` from your imports and your overrides | The verify command writes its own config now. The builder no longer exists. |
|
|
286
|
+
| Any comment about keeping `scripts/.lint-fixtures` un-ignored | The check runs under your system temp directory, where nothing is ignored. |
|
|
287
|
+
| Your `verify:lint-fixtures` script, if it passed arguments | `rdjperron-lint-verify` takes none. |
|
|
288
|
+
|
|
289
|
+
Two exports also went: `@rdjperron/lint/verify` (`verifySuites`, `packageSuite`,
|
|
290
|
+
`verifyPluginApiPin`) and `@rdjperron/lint/fixtures/*`. The fixtures still ship, the bin reads them
|
|
291
|
+
itself, and a repo with a second plugin to check now runs that plugin's own command beside this one.
|
|
292
|
+
|
|
293
|
+
New in 0.4.0: `oxc/no-accumulating-spread` at error, measured at zero hits on three repos. If it
|
|
294
|
+
fires on yours, that reducer is quadratic.
|