@rdjperron/lint 0.3.0 → 0.6.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 +329 -64
- 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/repo.d.mts +71 -0
- package/dist/repo.mjs +357 -0
- package/dist/repo.mjs.map +1 -0
- 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 +30 -17
- package/dist/config.mjs.map +0 -1
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
//#region src/config.ts
|
|
2
|
+
/**
|
|
3
|
+
* The plugin entry. The alias is `rdjperron`, which is also the prefix in every
|
|
4
|
+
* diagnostic and every disable comment, so a consumer that renames it renames
|
|
5
|
+
* its own rule keys too.
|
|
6
|
+
*/
|
|
7
|
+
const plugin = {
|
|
8
|
+
name: "rdjperron",
|
|
9
|
+
specifier: "@rdjperron/lint"
|
|
10
|
+
};
|
|
11
|
+
/** Every rule the plugin registers, prefixed. The verify bin puts all of them at error. */
|
|
12
|
+
const RULE_IDS = [
|
|
13
|
+
"rdjperron/no-array-filter-map",
|
|
14
|
+
"rdjperron/no-chained-type-assertions",
|
|
15
|
+
"rdjperron/no-conditional-empty-object-spread",
|
|
16
|
+
"rdjperron/no-hardcoded-jsx-text",
|
|
17
|
+
"rdjperron/no-known-value-widening",
|
|
18
|
+
"rdjperron/no-module-mocking",
|
|
19
|
+
"rdjperron/no-object-parameters",
|
|
20
|
+
"rdjperron/no-reduce-accumulator-copy",
|
|
21
|
+
"rdjperron/no-reflect-apply",
|
|
22
|
+
"rdjperron/no-reflect-get",
|
|
23
|
+
"rdjperron/no-runtime-typeof",
|
|
24
|
+
"rdjperron/no-shape-in-symbol-names",
|
|
25
|
+
"rdjperron/no-unknown-parameters",
|
|
26
|
+
"rdjperron/no-unknown-returns",
|
|
27
|
+
"rdjperron/no-unknown-type-aliases",
|
|
28
|
+
"rdjperron/no-unsafe-dictionary-type",
|
|
29
|
+
"rdjperron/no-use-effect",
|
|
30
|
+
"rdjperron/no-widen-then-assert",
|
|
31
|
+
"rdjperron/require-safety-comment-for-type-assertion",
|
|
32
|
+
"rdjperron/static-public-env"
|
|
33
|
+
];
|
|
34
|
+
/**
|
|
35
|
+
* The base config, for `lint: { extends: [recommended] }`.
|
|
36
|
+
*
|
|
37
|
+
* Measured through `extends` on vite-plus 0.3.1 / oxlint 1.81.0 (2026-09-17):
|
|
38
|
+
* `jsPlugins`, `rules`, `overrides`, `plugins`, `categories` and `options` are
|
|
39
|
+
* inherited by the consumer's config, and the consumer's own entries win.
|
|
40
|
+
* `env`, `settings` and `ignorePatterns` are not inherited, which is why this
|
|
41
|
+
* object carries none of them and the consumer keeps writing its own.
|
|
42
|
+
*/
|
|
43
|
+
const recommended = {
|
|
44
|
+
jsPlugins: [plugin],
|
|
45
|
+
plugins: [
|
|
46
|
+
"typescript",
|
|
47
|
+
"unicorn",
|
|
48
|
+
"oxc",
|
|
49
|
+
"import",
|
|
50
|
+
"promise",
|
|
51
|
+
"node"
|
|
52
|
+
],
|
|
53
|
+
categories: {
|
|
54
|
+
correctness: "error",
|
|
55
|
+
suspicious: "warn",
|
|
56
|
+
perf: "warn"
|
|
57
|
+
},
|
|
58
|
+
options: { typeAware: true },
|
|
59
|
+
rules: {
|
|
60
|
+
"rdjperron/no-array-filter-map": "error",
|
|
61
|
+
"rdjperron/no-chained-type-assertions": "error",
|
|
62
|
+
"rdjperron/no-conditional-empty-object-spread": "error",
|
|
63
|
+
"rdjperron/no-known-value-widening": "error",
|
|
64
|
+
"rdjperron/no-module-mocking": "error",
|
|
65
|
+
"rdjperron/no-object-parameters": "error",
|
|
66
|
+
"rdjperron/no-reduce-accumulator-copy": "error",
|
|
67
|
+
"rdjperron/no-reflect-apply": "error",
|
|
68
|
+
"rdjperron/no-reflect-get": "error",
|
|
69
|
+
"rdjperron/no-runtime-typeof": ["error", { allowInTypeGuards: true }],
|
|
70
|
+
"rdjperron/no-shape-in-symbol-names": "error",
|
|
71
|
+
"rdjperron/no-unknown-parameters": "error",
|
|
72
|
+
"rdjperron/no-unknown-returns": "error",
|
|
73
|
+
"rdjperron/no-unknown-type-aliases": "error",
|
|
74
|
+
"rdjperron/no-unsafe-dictionary-type": "error",
|
|
75
|
+
"rdjperron/no-widen-then-assert": "error",
|
|
76
|
+
"rdjperron/require-safety-comment-for-type-assertion": "error",
|
|
77
|
+
"rdjperron/no-use-effect": "error",
|
|
78
|
+
"oxc/no-accumulating-spread": "error",
|
|
79
|
+
"typescript/no-floating-promises": "error",
|
|
80
|
+
"typescript/no-misused-promises": "error",
|
|
81
|
+
"typescript/await-thenable": "error",
|
|
82
|
+
"typescript/no-unnecessary-type-assertion": "error",
|
|
83
|
+
"typescript/no-misused-spread": "error",
|
|
84
|
+
"typescript/require-array-sort-compare": "error",
|
|
85
|
+
"typescript/no-explicit-any": "warn",
|
|
86
|
+
"prefer-const": ["error", { destructuring: "all" }],
|
|
87
|
+
"no-unused-vars": ["error", {
|
|
88
|
+
argsIgnorePattern: "^_",
|
|
89
|
+
varsIgnorePattern: "^_"
|
|
90
|
+
}],
|
|
91
|
+
"no-underscore-dangle": "off",
|
|
92
|
+
eqeqeq: [
|
|
93
|
+
"error",
|
|
94
|
+
"always",
|
|
95
|
+
{ null: "ignore" }
|
|
96
|
+
],
|
|
97
|
+
"typescript/no-unsafe-type-assertion": "off",
|
|
98
|
+
"no-await-in-loop": "off",
|
|
99
|
+
"unicorn/consistent-function-scoping": "off",
|
|
100
|
+
"promise/always-return": "off"
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* The React file set: the globals, the hooks rules, and the compiler's own
|
|
105
|
+
* judgement. A consumer appends its own entries after these, because an override
|
|
106
|
+
* that changes a `react/*` rule must repeat `plugins: ["react"]` or oxlint
|
|
107
|
+
* silently drops the entry.
|
|
108
|
+
*/
|
|
109
|
+
function react(files, options = {}) {
|
|
110
|
+
const { env = { browser: true }, useEffectAllowedIn } = options;
|
|
111
|
+
const rules = {
|
|
112
|
+
"react/rules-of-hooks": "error",
|
|
113
|
+
"react/exhaustive-deps": "warn",
|
|
114
|
+
"react/react-in-jsx-scope": "off",
|
|
115
|
+
"react/immutability": "error",
|
|
116
|
+
"react/refs": "error",
|
|
117
|
+
"react/purity": "error",
|
|
118
|
+
"react/only-export-components": "error"
|
|
119
|
+
};
|
|
120
|
+
const overrides = [{
|
|
121
|
+
files,
|
|
122
|
+
plugins: ["react"],
|
|
123
|
+
env,
|
|
124
|
+
rules
|
|
125
|
+
}];
|
|
126
|
+
if (useEffectAllowedIn !== void 0) {
|
|
127
|
+
rules["rdjperron/no-use-effect"] = ["error", { hint: `The one sanctioned wrapper is ${useEffectAllowedIn}.` }];
|
|
128
|
+
overrides.push({
|
|
129
|
+
files: [useEffectAllowedIn],
|
|
130
|
+
plugins: ["react"],
|
|
131
|
+
rules: {
|
|
132
|
+
"rdjperron/no-use-effect": "off",
|
|
133
|
+
"react/exhaustive-deps": "off"
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return overrides;
|
|
138
|
+
}
|
|
139
|
+
/** Files whose JSX text belongs in a catalogue rather than in the source. */
|
|
140
|
+
function i18n(files, options = {}) {
|
|
141
|
+
const { pattern } = options;
|
|
142
|
+
return [{
|
|
143
|
+
files,
|
|
144
|
+
rules: { "rdjperron/no-hardcoded-jsx-text": pattern === void 0 ? "error" : ["error", { pattern }] }
|
|
145
|
+
}];
|
|
146
|
+
}
|
|
147
|
+
/** Every extension Metro treats as an asset, the list eslint-config-expo 10 allowed. */
|
|
148
|
+
const ASSET_EXTENSIONS = [
|
|
149
|
+
"aac",
|
|
150
|
+
"aiff",
|
|
151
|
+
"avif",
|
|
152
|
+
"bmp",
|
|
153
|
+
"caf",
|
|
154
|
+
"db",
|
|
155
|
+
"gif",
|
|
156
|
+
"heic",
|
|
157
|
+
"html",
|
|
158
|
+
"jpeg",
|
|
159
|
+
"jpg",
|
|
160
|
+
"json",
|
|
161
|
+
"m4a",
|
|
162
|
+
"m4v",
|
|
163
|
+
"mov",
|
|
164
|
+
"mp3",
|
|
165
|
+
"mp4",
|
|
166
|
+
"mpeg",
|
|
167
|
+
"mpg",
|
|
168
|
+
"otf",
|
|
169
|
+
"pdf",
|
|
170
|
+
"png",
|
|
171
|
+
"psd",
|
|
172
|
+
"svg",
|
|
173
|
+
"ttf",
|
|
174
|
+
"wav",
|
|
175
|
+
"webm",
|
|
176
|
+
"webp",
|
|
177
|
+
"xml",
|
|
178
|
+
"yaml",
|
|
179
|
+
"yml",
|
|
180
|
+
"zip"
|
|
181
|
+
];
|
|
182
|
+
/**
|
|
183
|
+
* The React Native side: what Expo's bundler inlines, what Metro bundles, and what
|
|
184
|
+
* Hermes can be trusted to run.
|
|
185
|
+
*
|
|
186
|
+
* `typescript/no-require-imports` is relaxed here rather than switched on: the
|
|
187
|
+
* repo-wide ban is the consumer's to write, and this is the one file set where
|
|
188
|
+
* `require()` of an asset is how that asset enters the bundle.
|
|
189
|
+
*/
|
|
190
|
+
function expo(files, options = {}) {
|
|
191
|
+
const { assetExts = [], reactFiles } = options;
|
|
192
|
+
const overrides = [{
|
|
193
|
+
files,
|
|
194
|
+
rules: {
|
|
195
|
+
"rdjperron/static-public-env": "error",
|
|
196
|
+
"typescript/no-require-imports": ["error", { allow: [`\\.(${[.../* @__PURE__ */ new Set([...ASSET_EXTENSIONS, ...assetExts])].toSorted().join("|")})$`] }],
|
|
197
|
+
"unicorn/no-array-sort": "off"
|
|
198
|
+
}
|
|
199
|
+
}];
|
|
200
|
+
if (reactFiles !== void 0) overrides.push({
|
|
201
|
+
files: reactFiles,
|
|
202
|
+
plugins: ["react"],
|
|
203
|
+
rules: {
|
|
204
|
+
"react/capitalized-calls": "off",
|
|
205
|
+
"react/style-prop-object": ["error", { allow: ["StatusBar"] }]
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
return overrides;
|
|
209
|
+
}
|
|
210
|
+
/** Test files: four rules whose premise a test does not hold. */
|
|
211
|
+
function tests(files) {
|
|
212
|
+
return [{
|
|
213
|
+
files,
|
|
214
|
+
rules: {
|
|
215
|
+
"typescript/no-explicit-any": "off",
|
|
216
|
+
"typescript/no-non-null-assertion": "off",
|
|
217
|
+
"rdjperron/no-runtime-typeof": "off",
|
|
218
|
+
"rdjperron/require-safety-comment-for-type-assertion": "off"
|
|
219
|
+
}
|
|
220
|
+
}];
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* `no-restricted-imports` for every file set at once. Two overrides matching one
|
|
224
|
+
* file do not merge, so each scope needs its whole list written out; a consumer
|
|
225
|
+
* doing that by hand restates the same list per scope and lifts a ban by
|
|
226
|
+
* forgetting one. Here each scope says which scope it starts `from` and what it
|
|
227
|
+
* `add`s and `drop`s, and this expands them.
|
|
228
|
+
*
|
|
229
|
+
* One override per scope comes out in declaration order, so the consumer declares
|
|
230
|
+
* broad to narrow. The broadest scope is an ordinary override on `files: ["**"]`,
|
|
231
|
+
* which behaves exactly like the top-level rule (measured on vite-plus 0.3.1 and
|
|
232
|
+
* oxlint 1.81.0, 2026-09-19).
|
|
233
|
+
*
|
|
234
|
+
* TypeScript rejects a name that is no key of `paths` or `patterns`, a `from`
|
|
235
|
+
* naming no scope at all, and `use` beside `from`. A `from` naming a scope
|
|
236
|
+
* declared later, a `drop` of a name the parent does not restrict and an `add` of
|
|
237
|
+
* one it already does throw here, because resolution order is what decides them.
|
|
238
|
+
*/
|
|
239
|
+
function restrictedImports(config) {
|
|
240
|
+
const { paths, patterns, scopes } = config;
|
|
241
|
+
const known = /* @__PURE__ */ new Set([...Object.keys(paths), ...Object.keys(patterns)]);
|
|
242
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
243
|
+
const overrides = [];
|
|
244
|
+
const entries = Object.entries(scopes);
|
|
245
|
+
for (const [scope, spec] of entries) {
|
|
246
|
+
const named = spec.use ?? [...spec.add ?? [], ...spec.drop ?? []];
|
|
247
|
+
for (const name of named) if (!known.has(name)) throw new Error(`restrictedImports: scope "${scope}" names "${name}", which is neither a path nor a pattern.`);
|
|
248
|
+
let chosen;
|
|
249
|
+
if (spec.use !== void 0) chosen = new Set(spec.use);
|
|
250
|
+
else {
|
|
251
|
+
const parent = resolved.get(spec.from);
|
|
252
|
+
if (parent === void 0) throw new Error(`restrictedImports: scope "${scope}" starts from "${spec.from}", which is not a scope declared before it.`);
|
|
253
|
+
chosen = new Set(parent);
|
|
254
|
+
for (const name of spec.drop ?? []) if (!chosen.delete(name)) throw new Error(`restrictedImports: scope "${scope}" drops "${name}", which "${spec.from}" does not restrict.`);
|
|
255
|
+
for (const name of spec.add ?? []) {
|
|
256
|
+
if (chosen.has(name)) throw new Error(`restrictedImports: scope "${scope}" adds "${name}", which "${spec.from}" already restricts.`);
|
|
257
|
+
chosen.add(name);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
resolved.set(scope, chosen);
|
|
261
|
+
overrides.push({
|
|
262
|
+
files: spec.files,
|
|
263
|
+
rules: { "no-restricted-imports": ["error", {
|
|
264
|
+
paths: chosenFrom(paths, chosen),
|
|
265
|
+
patterns: chosenFrom(patterns, chosen)
|
|
266
|
+
}] }
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
return overrides;
|
|
270
|
+
}
|
|
271
|
+
/** In the order the entry was declared in, so two scopes' lists read side by side. */
|
|
272
|
+
function chosenFrom(table, chosen) {
|
|
273
|
+
return Object.entries(table).flatMap(([name, entry]) => chosen.has(name) ? [entry] : []);
|
|
274
|
+
}
|
|
275
|
+
/** `a`, `a or b`, `a, b or c`. */
|
|
276
|
+
function orList(names) {
|
|
277
|
+
const last = names.at(-1) ?? "";
|
|
278
|
+
return names.length > 1 ? `${names.slice(0, -1).join(", ")} or ${last}` : last;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* The decoder modules, the one place per kind of outside value where the parse
|
|
282
|
+
* belongs. Every file gets `no-runtime-typeof` and `no-unknown-parameters` at
|
|
283
|
+
* error carrying a hint that names these files, because moving the check into one
|
|
284
|
+
* of them is the fix in both cases, and `no-runtime-typeof` is off inside them.
|
|
285
|
+
*
|
|
286
|
+
* `no-unknown-parameters` and `no-unsafe-dictionary-type` stay on in the decoders.
|
|
287
|
+
* A decoder that needs either off says so in its own override with its own
|
|
288
|
+
* measured count, and one switch for the whole set would cover the files that
|
|
289
|
+
* never needed it.
|
|
290
|
+
*
|
|
291
|
+
* Place it before `tests()`, which switches `no-runtime-typeof` off for test files.
|
|
292
|
+
*/
|
|
293
|
+
function decoders(files, options = {}) {
|
|
294
|
+
const { hint = `Decode the value in ${orList(files)}, then call that decoder from here.` } = options;
|
|
295
|
+
return [{
|
|
296
|
+
files: ["**"],
|
|
297
|
+
rules: {
|
|
298
|
+
"rdjperron/no-runtime-typeof": ["error", {
|
|
299
|
+
allowInTypeGuards: true,
|
|
300
|
+
hint
|
|
301
|
+
}],
|
|
302
|
+
"rdjperron/no-unknown-parameters": ["error", { hint }]
|
|
303
|
+
}
|
|
304
|
+
}, {
|
|
305
|
+
files,
|
|
306
|
+
rules: { "rdjperron/no-runtime-typeof": "off" }
|
|
307
|
+
}];
|
|
308
|
+
}
|
|
309
|
+
/** The formatting both consumers settled on. Spread it and add what is yours. */
|
|
310
|
+
const fmt = {
|
|
311
|
+
printWidth: 100,
|
|
312
|
+
singleQuote: false,
|
|
313
|
+
semi: true,
|
|
314
|
+
tabWidth: 2,
|
|
315
|
+
useTabs: false
|
|
316
|
+
};
|
|
317
|
+
/**
|
|
318
|
+
* Paths no linter and no formatter should walk: build output, dependencies, the
|
|
319
|
+
* directories AI coding agents scribble in, and the Vitest JSON report. Spread
|
|
320
|
+
* it into `ignorePatterns` and append the repo's own.
|
|
321
|
+
*/
|
|
322
|
+
const ignore = [
|
|
323
|
+
"node_modules/**",
|
|
324
|
+
"**/node_modules/**",
|
|
325
|
+
"dist/**",
|
|
326
|
+
"**/dist/**",
|
|
327
|
+
...[
|
|
328
|
+
".agents",
|
|
329
|
+
".claude",
|
|
330
|
+
".codex",
|
|
331
|
+
".continue",
|
|
332
|
+
".cursor",
|
|
333
|
+
".gemini",
|
|
334
|
+
".opencode",
|
|
335
|
+
".pi",
|
|
336
|
+
".roo",
|
|
337
|
+
".windsurf"
|
|
338
|
+
].flatMap((dir) => [`${dir}/**`, `**/${dir}/**`]),
|
|
339
|
+
"**/test-results.json"
|
|
340
|
+
];
|
|
341
|
+
/** The pre-commit entry: `vp check --fix` is defined for JS and TS sources, nothing else. */
|
|
342
|
+
const staged = { "*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}": "vp check --fix" };
|
|
343
|
+
//#endregion
|
|
344
|
+
export { i18n as a, react as c, staged as d, tests as f, fmt as i, recommended as l, decoders as n, ignore as o, expo as r, plugin as s, RULE_IDS as t, restrictedImports as u };
|
|
345
|
+
|
|
346
|
+
//# sourceMappingURL=config-CD2iCM6L.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-CD2iCM6L.mjs","names":[],"sources":["../src/config.ts"],"sourcesContent":["/**\n * Config data for a consumer's `vite.config.ts` (or `.oxlintrc.ts`).\n *\n * The types below are declared locally rather than imported from `oxlint`:\n * `oxlint` is never a dependency of this package (`vp migrate` strips one, and a\n * direct dependency installs a second linter beside the Vite+ IDE stub), and a\n * consumer that installs only `vite-plus` has no guaranteed path to oxlint's own\n * `.d.ts`. They are structurally compatible with `OxlintConfig` and\n * `OxlintOverride`, checked by a consumer-shaped config in\n * `tests/consumer-config.ts`: arrays are mutable (oxlint's are), and `plugins` is\n * oxlint's own literal union, because a bare `string[]` is not assignable to it.\n */\n\n/** A severity as oxlint spells it in a config; the numeric forms are not used here. */\ntype Severity = \"off\" | \"warn\" | \"error\";\n\n/** A rule entry: a severity, or a severity followed by the rule's own options. */\ntype RuleEntry = Severity | [Severity, ...unknown[]];\n\ninterface Rules {\n [ruleId: string]: RuleEntry;\n}\n\n/** oxlint's built-in plugins, the values its `plugins` field accepts. */\ntype BuiltinPlugin =\n | \"eslint\"\n | \"import\"\n | \"jest\"\n | \"jsdoc\"\n | \"jsx-a11y\"\n | \"nextjs\"\n | \"node\"\n | \"oxc\"\n | \"promise\"\n | \"react\"\n | \"react-perf\"\n | \"typescript\"\n | \"unicorn\"\n | \"vitest\"\n | \"vue\";\n\n/** oxlint's rule categories, the keys its `categories` field accepts. */\ntype Category =\n | \"correctness\"\n | \"nursery\"\n | \"pedantic\"\n | \"perf\"\n | \"restriction\"\n | \"style\"\n | \"suspicious\";\n\ninterface JsPlugin {\n name: string;\n specifier: string;\n}\n\nexport interface Override {\n files: string[];\n excludeFiles?: string[];\n plugins?: BuiltinPlugin[];\n env?: { [name: string]: boolean };\n jsPlugins?: JsPlugin[];\n rules?: Rules;\n}\n\nexport interface Config {\n jsPlugins?: JsPlugin[];\n plugins?: BuiltinPlugin[];\n categories?: { [category in Category]?: Severity };\n rules?: Rules;\n overrides?: Override[];\n options?: { typeAware?: boolean; typeCheck?: boolean };\n}\n\n/**\n * The plugin entry. The alias is `rdjperron`, which is also the prefix in every\n * diagnostic and every disable comment, so a consumer that renames it renames\n * its own rule keys too.\n */\nexport const plugin: JsPlugin = { name: \"rdjperron\", specifier: \"@rdjperron/lint\" };\n\n/** Every rule the plugin registers, prefixed. The verify bin puts all of them at error. */\nexport const RULE_IDS = [\n \"rdjperron/no-array-filter-map\",\n \"rdjperron/no-chained-type-assertions\",\n \"rdjperron/no-conditional-empty-object-spread\",\n \"rdjperron/no-hardcoded-jsx-text\",\n \"rdjperron/no-known-value-widening\",\n \"rdjperron/no-module-mocking\",\n \"rdjperron/no-object-parameters\",\n \"rdjperron/no-reduce-accumulator-copy\",\n \"rdjperron/no-reflect-apply\",\n \"rdjperron/no-reflect-get\",\n \"rdjperron/no-runtime-typeof\",\n \"rdjperron/no-shape-in-symbol-names\",\n \"rdjperron/no-unknown-parameters\",\n \"rdjperron/no-unknown-returns\",\n \"rdjperron/no-unknown-type-aliases\",\n \"rdjperron/no-unsafe-dictionary-type\",\n \"rdjperron/no-use-effect\",\n \"rdjperron/no-widen-then-assert\",\n \"rdjperron/require-safety-comment-for-type-assertion\",\n \"rdjperron/static-public-env\",\n] as const;\n\nexport type RuleId = (typeof RULE_IDS)[number];\n\n/**\n * The base config, for `lint: { extends: [recommended] }`.\n *\n * Measured through `extends` on vite-plus 0.3.1 / oxlint 1.81.0 (2026-09-17):\n * `jsPlugins`, `rules`, `overrides`, `plugins`, `categories` and `options` are\n * inherited by the consumer's config, and the consumer's own entries win.\n * `env`, `settings` and `ignorePatterns` are not inherited, which is why this\n * object carries none of them and the consumer keeps writing its own.\n */\nexport const recommended: Config = {\n jsPlugins: [plugin],\n // Naming any plugin replaces oxlint's default set, so this is the whole set the\n // rules below need: `promise/*` and `typescript/*` are silently dropped without it.\n plugins: [\"typescript\", \"unicorn\", \"oxc\", \"import\", \"promise\", \"node\"],\n categories: {\n correctness: \"error\",\n suspicious: \"warn\",\n perf: \"warn\",\n },\n // `options` merges key by key through `extends`, so a consumer writing\n // `options: { typeCheck: false }` still gets `typeAware` (measured on vite-plus\n // 0.3.1 / oxlint 1.81.0, 2026-09-19). The type-aware rules below need it on.\n options: { typeAware: true },\n rules: {\n // The anti-slop rules. Measured on stack-lab 2026-08-20 and 2026-09-13, and\n // on rummy 2026-09-16 (vite-plus 0.3.1, oxlint 1.81.0): on real hand-written\n // code they fire only on the patterns they exist to reject, so a hit is the\n // finding. Land a new one at `warn` in the consumer, fix or scope what\n // fires, then let this default take over.\n \"rdjperron/no-array-filter-map\": \"error\",\n \"rdjperron/no-chained-type-assertions\": \"error\",\n \"rdjperron/no-conditional-empty-object-spread\": \"error\",\n \"rdjperron/no-known-value-widening\": \"error\",\n \"rdjperron/no-module-mocking\": \"error\",\n \"rdjperron/no-object-parameters\": \"error\",\n \"rdjperron/no-reduce-accumulator-copy\": \"error\",\n \"rdjperron/no-reflect-apply\": \"error\",\n \"rdjperron/no-reflect-get\": \"error\",\n // A `typeof` inside a type predicate is the decode the rule asks for, named\n // and reusable, and the pack already exempts a predicate's subject from\n // `no-unknown-parameters` (rummy, 2026-09-16).\n \"rdjperron/no-runtime-typeof\": [\"error\", { allowInTypeGuards: true }],\n \"rdjperron/no-shape-in-symbol-names\": \"error\",\n \"rdjperron/no-unknown-parameters\": \"error\",\n \"rdjperron/no-unknown-returns\": \"error\",\n \"rdjperron/no-unknown-type-aliases\": \"error\",\n \"rdjperron/no-unsafe-dictionary-type\": \"error\",\n \"rdjperron/no-widen-then-assert\": \"error\",\n \"rdjperron/require-safety-comment-for-type-assertion\": \"error\",\n // Derived state, event handlers and a reactive query cover every case; the\n // one sanctioned wrapper is named through `react(files, { useEffectAllowedIn })`.\n \"rdjperron/no-use-effect\": \"error\",\n\n // `no-hardcoded-jsx-text` and `static-public-env` are absent on purpose:\n // they belong to a translated app and to an Expo app. `i18n()` and `expo()`\n // switch them on for the files that want them.\n\n // Quadratic by construction, and the sibling of `no-reduce-accumulator-copy`,\n // which is already at error. `categories.perf` alone only warns. Measured\n // 2026-09-19 (vite-plus 0.3.1, oxlint 1.81.0): zero hits on this repo's 32\n // files, on rummy's 330 and on stack-lab's 57.\n \"oxc/no-accumulating-spread\": \"error\",\n\n // Type-aware rules, which `options.typeAware` above switches on.\n \"typescript/no-floating-promises\": \"error\",\n \"typescript/no-misused-promises\": \"error\",\n \"typescript/await-thenable\": \"error\",\n // Measured on rummy 2026-09-16 before the cleanup: 5, 5 and 1 hits, zero after.\n \"typescript/no-unnecessary-type-assertion\": \"error\",\n \"typescript/no-misused-spread\": \"error\",\n \"typescript/require-array-sort-compare\": \"error\",\n \"typescript/no-explicit-any\": \"warn\",\n\n \"prefer-const\": [\"error\", { destructuring: \"all\" }],\n \"no-unused-vars\": [\"error\", { argsIgnorePattern: \"^_\", varsIgnorePattern: \"^_\" }],\n // oxlint 1.77 moved this into `suspicious`, where it contradicts the `^_`\n // convention above. The prefix wins.\n \"no-underscore-dangle\": \"off\",\n eqeqeq: [\"error\", \"always\", { null: \"ignore\" }],\n\n // Deliberate off-switches, held by both consumers since the 2026-08-20 setup.\n \"typescript/no-unsafe-type-assertion\": \"off\",\n \"no-await-in-loop\": \"off\",\n \"unicorn/consistent-function-scoping\": \"off\",\n \"promise/always-return\": \"off\",\n },\n};\n\nexport interface ReactOptions {\n /**\n * The globals these files run with, as oxlint's `env`, defaulting to\n * `{ browser: true }`. A React Native app has no DOM: oxlint ships no\n * `react-native` environment, and `{ \"shared-node-browser\": true }` is the\n * closest one, the timers and the console both runtimes agree on.\n */\n env?: { [name: string]: boolean };\n /**\n * The one file allowed to call `useEffect`, as a path. Its name goes into the\n * `no-use-effect` hint for every React file, and the rule is off for it.\n */\n useEffectAllowedIn?: string;\n}\n\n/**\n * The React file set: the globals, the hooks rules, and the compiler's own\n * judgement. A consumer appends its own entries after these, because an override\n * that changes a `react/*` rule must repeat `plugins: [\"react\"]` or oxlint\n * silently drops the entry.\n */\nexport function react(files: string[], options: ReactOptions = {}): Override[] {\n const { env = { browser: true }, useEffectAllowedIn } = options;\n const rules: Rules = {\n \"react/rules-of-hooks\": \"error\",\n \"react/exhaustive-deps\": \"warn\",\n // React 19 and the automatic JSX runtime: nothing has to be in scope.\n \"react/react-in-jsx-scope\": \"off\",\n // The React Compiler's own rules: a component it silently skips is a\n // component that never memoizes.\n \"react/immutability\": \"error\",\n \"react/refs\": \"error\",\n \"react/purity\": \"error\",\n // A module that exports only components refreshes in place; any other\n // export re-runs every importer.\n \"react/only-export-components\": \"error\",\n };\n const overrides: Override[] = [{ files, plugins: [\"react\"], env, rules }];\n\n if (useEffectAllowedIn !== undefined) {\n rules[\"rdjperron/no-use-effect\"] = [\n \"error\",\n { hint: `The one sanctioned wrapper is ${useEffectAllowedIn}.` },\n ];\n overrides.push({\n // The sanctioned wrapper exists precisely so every other file can be clean.\n files: [useEffectAllowedIn],\n plugins: [\"react\"],\n rules: {\n \"rdjperron/no-use-effect\": \"off\",\n // The empty dependency array is this wrapper's contract, not an omission.\n \"react/exhaustive-deps\": \"off\",\n },\n });\n }\n\n return overrides;\n}\n\nexport interface I18nOptions {\n /**\n * The word pattern, a regex source compiled with `u`. The rule's default,\n * `\\p{L}{3,}`, reads three letters in a row as a word.\n */\n pattern?: string;\n}\n\n/** Files whose JSX text belongs in a catalogue rather than in the source. */\nexport function i18n(files: string[], options: I18nOptions = {}): Override[] {\n const { pattern } = options;\n return [\n {\n files,\n rules: {\n \"rdjperron/no-hardcoded-jsx-text\": pattern === undefined ? \"error\" : [\"error\", { pattern }],\n },\n },\n ];\n}\n\n/** Every extension Metro treats as an asset, the list eslint-config-expo 10 allowed. */\nconst ASSET_EXTENSIONS = [\n \"aac\",\n \"aiff\",\n \"avif\",\n \"bmp\",\n \"caf\",\n \"db\",\n \"gif\",\n \"heic\",\n \"html\",\n \"jpeg\",\n \"jpg\",\n \"json\",\n \"m4a\",\n \"m4v\",\n \"mov\",\n \"mp3\",\n \"mp4\",\n \"mpeg\",\n \"mpg\",\n \"otf\",\n \"pdf\",\n \"png\",\n \"psd\",\n \"svg\",\n \"ttf\",\n \"wav\",\n \"webm\",\n \"webp\",\n \"xml\",\n \"yaml\",\n \"yml\",\n \"zip\",\n];\n\nexport interface ExpoOptions {\n /** The extensions this app's `metro.config.js` adds to `resolver.assetExts`, without the dot. */\n assetExts?: string[];\n /**\n * The files React renders, if the app has any. They get the two `react/*` rules\n * an Expo app needs, on their own override because `react` is not in the\n * inherited plugin set.\n */\n reactFiles?: string[];\n}\n\n/**\n * The React Native side: what Expo's bundler inlines, what Metro bundles, and what\n * Hermes can be trusted to run.\n *\n * `typescript/no-require-imports` is relaxed here rather than switched on: the\n * repo-wide ban is the consumer's to write, and this is the one file set where\n * `require()` of an asset is how that asset enters the bundle.\n */\nexport function expo(files: string[], options: ExpoOptions = {}): Override[] {\n const { assetExts = [], reactFiles } = options;\n const extensions = [...new Set([...ASSET_EXTENSIONS, ...assetExts])].toSorted();\n const overrides: Override[] = [\n {\n files,\n rules: {\n \"rdjperron/static-public-env\": \"error\",\n // Measured on rummy's 330 files 2026-09-19 (vite-plus 0.3.1, oxlint 1.81.0):\n // zero hits, in a run that reported a non-asset `require()` from a probe file.\n \"typescript/no-require-imports\": [\"error\", { allow: [`\\\\.(${extensions.join(\"|\")})$`] }],\n // Hermes has no iterator helpers and its `toSorted` is unverified, so the copy\n // this rule asks for is not one the phone can be trusted to run.\n \"unicorn/no-array-sort\": \"off\",\n },\n },\n ];\n\n if (reactFiles !== undefined) {\n overrides.push({\n files: reactFiles,\n plugins: [\"react\"],\n rules: {\n // `Gesture.Pan()` reads as a component call to this rule; the React Compiler\n // compiles that component itself and is the judge.\n \"react/capitalized-calls\": \"off\",\n // expo-status-bar's `style` prop is a string by API design.\n \"react/style-prop-object\": [\"error\", { allow: [\"StatusBar\"] }],\n },\n });\n }\n\n return overrides;\n}\n\n/** Test files: four rules whose premise a test does not hold. */\nexport function tests(files: string[]): Override[] {\n return [\n {\n files,\n rules: {\n // A fixture is allowed to be the wrong type on purpose.\n \"typescript/no-explicit-any\": \"off\",\n \"typescript/no-non-null-assertion\": \"off\",\n // `expect(typeof x).toBe(\"function\")` reads as an unparsed boundary, but\n // asserting the shape of your own public API is the point of the assertion.\n \"rdjperron/no-runtime-typeof\": \"off\",\n // A justification on every assertion in a test is noise (53 measured on\n // rummy, 2026-09-16).\n \"rdjperron/require-safety-comment-for-type-assertion\": \"off\",\n },\n },\n ];\n}\n\n/** One banned module or named import, as `no-restricted-imports` spells a path. */\nexport interface RestrictedPath {\n name: string;\n importNames?: string[];\n allowImportNames?: string[];\n message: string;\n}\n\n/** One banned glob group, as `no-restricted-imports` spells a pattern. */\nexport interface RestrictedPattern {\n group: string[];\n message: string;\n importNames?: string[];\n allowImportNames?: string[];\n caseSensitive?: boolean;\n}\n\n/**\n * One file set and the restrictions it carries: either the whole list by name, or\n * another scope's list plus and minus named entries.\n */\nexport type RestrictionScope<Name extends string, Parent extends string> =\n | { files: string[]; use: Name[]; from?: never; add?: never; drop?: never }\n | { files: string[]; use?: never; from: Parent; add?: Name[]; drop?: Name[] };\n\n/**\n * `no-restricted-imports` for every file set at once. Two overrides matching one\n * file do not merge, so each scope needs its whole list written out; a consumer\n * doing that by hand restates the same list per scope and lifts a ban by\n * forgetting one. Here each scope says which scope it starts `from` and what it\n * `add`s and `drop`s, and this expands them.\n *\n * One override per scope comes out in declaration order, so the consumer declares\n * broad to narrow. The broadest scope is an ordinary override on `files: [\"**\"]`,\n * which behaves exactly like the top-level rule (measured on vite-plus 0.3.1 and\n * oxlint 1.81.0, 2026-09-19).\n *\n * TypeScript rejects a name that is no key of `paths` or `patterns`, a `from`\n * naming no scope at all, and `use` beside `from`. A `from` naming a scope\n * declared later, a `drop` of a name the parent does not restrict and an `add` of\n * one it already does throw here, because resolution order is what decides them.\n */\nexport function restrictedImports<\n Paths extends { [name: string]: RestrictedPath },\n Patterns extends { [name: string]: RestrictedPattern },\n Scopes extends {\n [scope in keyof Scopes]: RestrictionScope<\n Extract<keyof Paths | keyof Patterns, string>,\n Extract<keyof Scopes, string>\n >;\n },\n>(config: { paths: Paths; patterns: Patterns; scopes: Scopes }): Override[] {\n const { paths, patterns, scopes } = config;\n const known = new Set([...Object.keys(paths), ...Object.keys(patterns)]);\n const resolved = new Map<string, Set<string>>();\n const overrides: Override[] = [];\n\n const entries: [string, RestrictionScope<string, string>][] = Object.entries(scopes);\n for (const [scope, spec] of entries) {\n const named = spec.use ?? [...(spec.add ?? []), ...(spec.drop ?? [])];\n for (const name of named) {\n if (!known.has(name)) {\n throw new Error(\n `restrictedImports: scope \"${scope}\" names \"${name}\", which is neither a path nor a pattern.`,\n );\n }\n }\n\n let chosen: Set<string>;\n if (spec.use !== undefined) {\n chosen = new Set(spec.use);\n } else {\n const parent = resolved.get(spec.from);\n if (parent === undefined) {\n throw new Error(\n `restrictedImports: scope \"${scope}\" starts from \"${spec.from}\", which is not a scope declared before it.`,\n );\n }\n chosen = new Set(parent);\n for (const name of spec.drop ?? []) {\n if (!chosen.delete(name)) {\n throw new Error(\n `restrictedImports: scope \"${scope}\" drops \"${name}\", which \"${spec.from}\" does not restrict.`,\n );\n }\n }\n for (const name of spec.add ?? []) {\n if (chosen.has(name)) {\n throw new Error(\n `restrictedImports: scope \"${scope}\" adds \"${name}\", which \"${spec.from}\" already restricts.`,\n );\n }\n chosen.add(name);\n }\n }\n\n resolved.set(scope, chosen);\n overrides.push({\n files: spec.files,\n rules: {\n \"no-restricted-imports\": [\n \"error\",\n { paths: chosenFrom(paths, chosen), patterns: chosenFrom(patterns, chosen) },\n ],\n },\n });\n }\n\n return overrides;\n}\n\n/** In the order the entry was declared in, so two scopes' lists read side by side. */\nfunction chosenFrom<Entry>(table: { [name: string]: Entry }, chosen: ReadonlySet<string>): Entry[] {\n return Object.entries(table).flatMap(([name, entry]) => (chosen.has(name) ? [entry] : []));\n}\n\nexport interface DecodersOptions {\n /**\n * The sentence appended to both findings. The default names the files in\n * `files`, which is what a reader has to know to act on either message.\n */\n hint?: string;\n}\n\n/** `a`, `a or b`, `a, b or c`. */\nfunction orList(names: string[]): string {\n const last = names.at(-1) ?? \"\";\n return names.length > 1 ? `${names.slice(0, -1).join(\", \")} or ${last}` : last;\n}\n\n/**\n * The decoder modules, the one place per kind of outside value where the parse\n * belongs. Every file gets `no-runtime-typeof` and `no-unknown-parameters` at\n * error carrying a hint that names these files, because moving the check into one\n * of them is the fix in both cases, and `no-runtime-typeof` is off inside them.\n *\n * `no-unknown-parameters` and `no-unsafe-dictionary-type` stay on in the decoders.\n * A decoder that needs either off says so in its own override with its own\n * measured count, and one switch for the whole set would cover the files that\n * never needed it.\n *\n * Place it before `tests()`, which switches `no-runtime-typeof` off for test files.\n */\nexport function decoders(files: string[], options: DecodersOptions = {}): Override[] {\n const { hint = `Decode the value in ${orList(files)}, then call that decoder from here.` } =\n options;\n return [\n {\n files: [\"**\"],\n rules: {\n // A later override replaces a rule's whole entry, options included (measured\n // on vite-plus 0.3.1 / oxlint 1.81.0, 2026-09-19), so `allowInTypeGuards` is\n // restated rather than inherited from `recommended`.\n \"rdjperron/no-runtime-typeof\": [\"error\", { allowInTypeGuards: true, hint }],\n \"rdjperron/no-unknown-parameters\": [\"error\", { hint }],\n },\n },\n {\n // Inside a decoder a `typeof` is the parse, not a narrowing in place of one.\n files,\n rules: { \"rdjperron/no-runtime-typeof\": \"off\" },\n },\n ];\n}\n\n/** The formatting both consumers settled on. Spread it and add what is yours. */\nexport const fmt = {\n printWidth: 100,\n singleQuote: false,\n semi: true,\n tabWidth: 2,\n useTabs: false,\n};\n\n/**\n * Paths no linter and no formatter should walk: build output, dependencies, the\n * directories AI coding agents scribble in, and the Vitest JSON report. Spread\n * it into `ignorePatterns` and append the repo's own.\n */\nexport const ignore: string[] = [\n \"node_modules/**\",\n \"**/node_modules/**\",\n \"dist/**\",\n \"**/dist/**\",\n ...[\n \".agents\",\n \".claude\",\n \".codex\",\n \".continue\",\n \".cursor\",\n \".gemini\",\n \".opencode\",\n \".pi\",\n \".roo\",\n \".windsurf\",\n ].flatMap((dir) => [`${dir}/**`, `**/${dir}/**`]),\n \"**/test-results.json\",\n];\n\n/** The pre-commit entry: `vp check --fix` is defined for JS and TS sources, nothing else. */\nexport const staged = {\n \"*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}\": \"vp check --fix\",\n};\n"],"mappings":";;;;;;AA+EA,MAAa,SAAmB;CAAE,MAAM;CAAa,WAAW;AAAkB;;AAGlF,MAAa,WAAW;CACtB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;;;;;;AAaA,MAAa,cAAsB;CACjC,WAAW,CAAC,MAAM;CAGlB,SAAS;EAAC;EAAc;EAAW;EAAO;EAAU;EAAW;CAAM;CACrE,YAAY;EACV,aAAa;EACb,YAAY;EACZ,MAAM;CACR;CAIA,SAAS,EAAE,WAAW,KAAK;CAC3B,OAAO;EAML,iCAAiC;EACjC,wCAAwC;EACxC,gDAAgD;EAChD,qCAAqC;EACrC,+BAA+B;EAC/B,kCAAkC;EAClC,wCAAwC;EACxC,8BAA8B;EAC9B,4BAA4B;EAI5B,+BAA+B,CAAC,SAAS,EAAE,mBAAmB,KAAK,CAAC;EACpE,sCAAsC;EACtC,mCAAmC;EACnC,gCAAgC;EAChC,qCAAqC;EACrC,uCAAuC;EACvC,kCAAkC;EAClC,uDAAuD;EAGvD,2BAA2B;EAU3B,8BAA8B;EAG9B,mCAAmC;EACnC,kCAAkC;EAClC,6BAA6B;EAE7B,4CAA4C;EAC5C,gCAAgC;EAChC,yCAAyC;EACzC,8BAA8B;EAE9B,gBAAgB,CAAC,SAAS,EAAE,eAAe,MAAM,CAAC;EAClD,kBAAkB,CAAC,SAAS;GAAE,mBAAmB;GAAM,mBAAmB;EAAK,CAAC;EAGhF,wBAAwB;EACxB,QAAQ;GAAC;GAAS;GAAU,EAAE,MAAM,SAAS;EAAC;EAG9C,uCAAuC;EACvC,oBAAoB;EACpB,uCAAuC;EACvC,yBAAyB;CAC3B;AACF;;;;;;;AAuBA,SAAgB,MAAM,OAAiB,UAAwB,CAAC,GAAe;CAC7E,MAAM,EAAE,MAAM,EAAE,SAAS,KAAK,GAAG,uBAAuB;CACxD,MAAM,QAAe;EACnB,wBAAwB;EACxB,yBAAyB;EAEzB,4BAA4B;EAG5B,sBAAsB;EACtB,cAAc;EACd,gBAAgB;EAGhB,gCAAgC;CAClC;CACA,MAAM,YAAwB,CAAC;EAAE;EAAO,SAAS,CAAC,OAAO;EAAG;EAAK;CAAM,CAAC;CAExE,IAAI,uBAAuB,KAAA,GAAW;EACpC,MAAM,6BAA6B,CACjC,SACA,EAAE,MAAM,iCAAiC,mBAAmB,GAAG,CACjE;EACA,UAAU,KAAK;GAEb,OAAO,CAAC,kBAAkB;GAC1B,SAAS,CAAC,OAAO;GACjB,OAAO;IACL,2BAA2B;IAE3B,yBAAyB;GAC3B;EACF,CAAC;CACH;CAEA,OAAO;AACT;;AAWA,SAAgB,KAAK,OAAiB,UAAuB,CAAC,GAAe;CAC3E,MAAM,EAAE,YAAY;CACpB,OAAO,CACL;EACE;EACA,OAAO,EACL,mCAAmC,YAAY,KAAA,IAAY,UAAU,CAAC,SAAS,EAAE,QAAQ,CAAC,EAC5F;CACF,CACF;AACF;;AAGA,MAAM,mBAAmB;CACvB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;;;;;AAqBA,SAAgB,KAAK,OAAiB,UAAuB,CAAC,GAAe;CAC3E,MAAM,EAAE,YAAY,CAAC,GAAG,eAAe;CAEvC,MAAM,YAAwB,CAC5B;EACE;EACA,OAAO;GACL,+BAA+B;GAG/B,iCAAiC,CAAC,SAAS,EAAE,OAAO,CAAC,OARxC,CAAC,mBAAG,IAAI,IAAI,CAAC,GAAG,kBAAkB,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,SAQM,CAAC,CAAC,KAAK,GAAG,EAAE,GAAG,EAAE,CAAC;GAGvF,yBAAyB;EAC3B;CACF,CACF;CAEA,IAAI,eAAe,KAAA,GACjB,UAAU,KAAK;EACb,OAAO;EACP,SAAS,CAAC,OAAO;EACjB,OAAO;GAGL,2BAA2B;GAE3B,2BAA2B,CAAC,SAAS,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC;EAC/D;CACF,CAAC;CAGH,OAAO;AACT;;AAGA,SAAgB,MAAM,OAA6B;CACjD,OAAO,CACL;EACE;EACA,OAAO;GAEL,8BAA8B;GAC9B,oCAAoC;GAGpC,+BAA+B;GAG/B,uDAAuD;EACzD;CACF,CACF;AACF;;;;;;;;;;;;;;;;;;AA4CA,SAAgB,kBASd,QAA0E;CAC1E,MAAM,EAAE,OAAO,UAAU,WAAW;CACpC,MAAM,wBAAQ,IAAI,IAAI,CAAC,GAAG,OAAO,KAAK,KAAK,GAAG,GAAG,OAAO,KAAK,QAAQ,CAAC,CAAC;CACvE,MAAM,2BAAW,IAAI,IAAyB;CAC9C,MAAM,YAAwB,CAAC;CAE/B,MAAM,UAAwD,OAAO,QAAQ,MAAM;CACnF,KAAK,MAAM,CAAC,OAAO,SAAS,SAAS;EACnC,MAAM,QAAQ,KAAK,OAAO,CAAC,GAAI,KAAK,OAAO,CAAC,GAAI,GAAI,KAAK,QAAQ,CAAC,CAAE;EACpE,KAAK,MAAM,QAAQ,OACjB,IAAI,CAAC,MAAM,IAAI,IAAI,GACjB,MAAM,IAAI,MACR,6BAA6B,MAAM,WAAW,KAAK,0CACrD;EAIJ,IAAI;EACJ,IAAI,KAAK,QAAQ,KAAA,GACf,SAAS,IAAI,IAAI,KAAK,GAAG;OACpB;GACL,MAAM,SAAS,SAAS,IAAI,KAAK,IAAI;GACrC,IAAI,WAAW,KAAA,GACb,MAAM,IAAI,MACR,6BAA6B,MAAM,iBAAiB,KAAK,KAAK,4CAChE;GAEF,SAAS,IAAI,IAAI,MAAM;GACvB,KAAK,MAAM,QAAQ,KAAK,QAAQ,CAAC,GAC/B,IAAI,CAAC,OAAO,OAAO,IAAI,GACrB,MAAM,IAAI,MACR,6BAA6B,MAAM,WAAW,KAAK,YAAY,KAAK,KAAK,qBAC3E;GAGJ,KAAK,MAAM,QAAQ,KAAK,OAAO,CAAC,GAAG;IACjC,IAAI,OAAO,IAAI,IAAI,GACjB,MAAM,IAAI,MACR,6BAA6B,MAAM,UAAU,KAAK,YAAY,KAAK,KAAK,qBAC1E;IAEF,OAAO,IAAI,IAAI;GACjB;EACF;EAEA,SAAS,IAAI,OAAO,MAAM;EAC1B,UAAU,KAAK;GACb,OAAO,KAAK;GACZ,OAAO,EACL,yBAAyB,CACvB,SACA;IAAE,OAAO,WAAW,OAAO,MAAM;IAAG,UAAU,WAAW,UAAU,MAAM;GAAE,CAC7E,EACF;EACF,CAAC;CACH;CAEA,OAAO;AACT;;AAGA,SAAS,WAAkB,OAAkC,QAAsC;CACjG,OAAO,OAAO,QAAQ,KAAK,CAAC,CAAC,SAAS,CAAC,MAAM,WAAY,OAAO,IAAI,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC,CAAE;AAC3F;;AAWA,SAAS,OAAO,OAAyB;CACvC,MAAM,OAAO,MAAM,GAAG,EAAE,KAAK;CAC7B,OAAO,MAAM,SAAS,IAAI,GAAG,MAAM,MAAM,GAAG,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE,MAAM,SAAS;AAC5E;;;;;;;;;;;;;;AAeA,SAAgB,SAAS,OAAiB,UAA2B,CAAC,GAAe;CACnF,MAAM,EAAE,OAAO,uBAAuB,OAAO,KAAK,EAAE,yCAClD;CACF,OAAO,CACL;EACE,OAAO,CAAC,IAAI;EACZ,OAAO;GAIL,+BAA+B,CAAC,SAAS;IAAE,mBAAmB;IAAM;GAAK,CAAC;GAC1E,mCAAmC,CAAC,SAAS,EAAE,KAAK,CAAC;EACvD;CACF,GACA;EAEE;EACA,OAAO,EAAE,+BAA+B,MAAM;CAChD,CACF;AACF;;AAGA,MAAa,MAAM;CACjB,YAAY;CACZ,aAAa;CACb,MAAM;CACN,UAAU;CACV,SAAS;AACX;;;;;;AAOA,MAAa,SAAmB;CAC9B;CACA;CACA;CACA;CACA,GAAG;EACD;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF,CAAC,CAAC,SAAS,QAAQ,CAAC,GAAG,IAAI,MAAM,MAAM,IAAI,IAAI,CAAC;CAChD;AACF;;AAGA,MAAa,SAAS,EACpB,qCAAqC,iBACvC"}
|
package/dist/config.d.mts
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* The types below are declared locally rather than imported from `oxlint`:
|
|
6
6
|
* `oxlint` is never a dependency of this package (`vp migrate` strips one, and a
|
|
7
7
|
* direct dependency installs a second linter beside the Vite+ IDE stub), and a
|
|
8
|
-
* consumer that installs only `vite-plus`
|
|
9
|
-
*
|
|
10
|
-
* `
|
|
11
|
-
* `tests/config.
|
|
8
|
+
* consumer that installs only `vite-plus` has no guaranteed path to oxlint's own
|
|
9
|
+
* `.d.ts`. They are structurally compatible with `OxlintConfig` and
|
|
10
|
+
* `OxlintOverride`, checked by a consumer-shaped config in
|
|
11
|
+
* `tests/consumer-config.ts`: arrays are mutable (oxlint's are), and `plugins` is
|
|
12
12
|
* oxlint's own literal union, because a bare `string[]` is not assignable to it.
|
|
13
13
|
*/
|
|
14
14
|
/** A severity as oxlint spells it in a config; the numeric forms are not used here. */
|
|
@@ -20,6 +20,8 @@ interface Rules {
|
|
|
20
20
|
}
|
|
21
21
|
/** oxlint's built-in plugins, the values its `plugins` field accepts. */
|
|
22
22
|
type BuiltinPlugin = "eslint" | "import" | "jest" | "jsdoc" | "jsx-a11y" | "nextjs" | "node" | "oxc" | "promise" | "react" | "react-perf" | "typescript" | "unicorn" | "vitest" | "vue";
|
|
23
|
+
/** oxlint's rule categories, the keys its `categories` field accepts. */
|
|
24
|
+
type Category = "correctness" | "nursery" | "pedantic" | "perf" | "restriction" | "style" | "suspicious";
|
|
23
25
|
interface JsPlugin {
|
|
24
26
|
name: string;
|
|
25
27
|
specifier: string;
|
|
@@ -36,8 +38,14 @@ export interface Override {
|
|
|
36
38
|
}
|
|
37
39
|
export interface Config {
|
|
38
40
|
jsPlugins?: JsPlugin[];
|
|
41
|
+
plugins?: BuiltinPlugin[];
|
|
42
|
+
categories?: { [category in Category]?: Severity; };
|
|
39
43
|
rules?: Rules;
|
|
40
44
|
overrides?: Override[];
|
|
45
|
+
options?: {
|
|
46
|
+
typeAware?: boolean;
|
|
47
|
+
typeCheck?: boolean;
|
|
48
|
+
};
|
|
41
49
|
}
|
|
42
50
|
/**
|
|
43
51
|
* The plugin entry. The alias is `rdjperron`, which is also the prefix in every
|
|
@@ -45,7 +53,7 @@ export interface Config {
|
|
|
45
53
|
* its own rule keys too.
|
|
46
54
|
*/
|
|
47
55
|
export declare const plugin: JsPlugin;
|
|
48
|
-
/** Every rule the plugin registers, prefixed.
|
|
56
|
+
/** Every rule the plugin registers, prefixed. The verify bin puts all of them at error. */
|
|
49
57
|
export declare const RULE_IDS: readonly ["rdjperron/no-array-filter-map", "rdjperron/no-chained-type-assertions", "rdjperron/no-conditional-empty-object-spread", "rdjperron/no-hardcoded-jsx-text", "rdjperron/no-known-value-widening", "rdjperron/no-module-mocking", "rdjperron/no-object-parameters", "rdjperron/no-reduce-accumulator-copy", "rdjperron/no-reflect-apply", "rdjperron/no-reflect-get", "rdjperron/no-runtime-typeof", "rdjperron/no-shape-in-symbol-names", "rdjperron/no-unknown-parameters", "rdjperron/no-unknown-returns", "rdjperron/no-unknown-type-aliases", "rdjperron/no-unsafe-dictionary-type", "rdjperron/no-use-effect", "rdjperron/no-widen-then-assert", "rdjperron/require-safety-comment-for-type-assertion", "rdjperron/static-public-env"];
|
|
50
58
|
export type RuleId = (typeof RULE_IDS)[number];
|
|
51
59
|
/**
|
|
@@ -90,16 +98,106 @@ export interface I18nOptions {
|
|
|
90
98
|
}
|
|
91
99
|
/** Files whose JSX text belongs in a catalogue rather than in the source. */
|
|
92
100
|
export declare function i18n(files: string[], options?: I18nOptions): Override[];
|
|
93
|
-
|
|
94
|
-
|
|
101
|
+
export interface ExpoOptions {
|
|
102
|
+
/** The extensions this app's `metro.config.js` adds to `resolver.assetExts`, without the dot. */
|
|
103
|
+
assetExts?: string[];
|
|
104
|
+
/**
|
|
105
|
+
* The files React renders, if the app has any. They get the two `react/*` rules
|
|
106
|
+
* an Expo app needs, on their own override because `react` is not in the
|
|
107
|
+
* inherited plugin set.
|
|
108
|
+
*/
|
|
109
|
+
reactFiles?: string[];
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The React Native side: what Expo's bundler inlines, what Metro bundles, and what
|
|
113
|
+
* Hermes can be trusted to run.
|
|
114
|
+
*
|
|
115
|
+
* `typescript/no-require-imports` is relaxed here rather than switched on: the
|
|
116
|
+
* repo-wide ban is the consumer's to write, and this is the one file set where
|
|
117
|
+
* `require()` of an asset is how that asset enters the bundle.
|
|
118
|
+
*/
|
|
119
|
+
export declare function expo(files: string[], options?: ExpoOptions): Override[];
|
|
95
120
|
/** Test files: four rules whose premise a test does not hold. */
|
|
96
121
|
export declare function tests(files: string[]): Override[];
|
|
122
|
+
/** One banned module or named import, as `no-restricted-imports` spells a path. */
|
|
123
|
+
export interface RestrictedPath {
|
|
124
|
+
name: string;
|
|
125
|
+
importNames?: string[];
|
|
126
|
+
allowImportNames?: string[];
|
|
127
|
+
message: string;
|
|
128
|
+
}
|
|
129
|
+
/** One banned glob group, as `no-restricted-imports` spells a pattern. */
|
|
130
|
+
export interface RestrictedPattern {
|
|
131
|
+
group: string[];
|
|
132
|
+
message: string;
|
|
133
|
+
importNames?: string[];
|
|
134
|
+
allowImportNames?: string[];
|
|
135
|
+
caseSensitive?: boolean;
|
|
136
|
+
}
|
|
97
137
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
|
|
138
|
+
* One file set and the restrictions it carries: either the whole list by name, or
|
|
139
|
+
* another scope's list plus and minus named entries.
|
|
140
|
+
*/
|
|
141
|
+
export type RestrictionScope<Name extends string, Parent extends string> = {
|
|
142
|
+
files: string[];
|
|
143
|
+
use: Name[];
|
|
144
|
+
from?: never;
|
|
145
|
+
add?: never;
|
|
146
|
+
drop?: never;
|
|
147
|
+
} | {
|
|
148
|
+
files: string[];
|
|
149
|
+
use?: never;
|
|
150
|
+
from: Parent;
|
|
151
|
+
add?: Name[];
|
|
152
|
+
drop?: Name[];
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* `no-restricted-imports` for every file set at once. Two overrides matching one
|
|
156
|
+
* file do not merge, so each scope needs its whole list written out; a consumer
|
|
157
|
+
* doing that by hand restates the same list per scope and lifts a ban by
|
|
158
|
+
* forgetting one. Here each scope says which scope it starts `from` and what it
|
|
159
|
+
* `add`s and `drop`s, and this expands them.
|
|
160
|
+
*
|
|
161
|
+
* One override per scope comes out in declaration order, so the consumer declares
|
|
162
|
+
* broad to narrow. The broadest scope is an ordinary override on `files: ["**"]`,
|
|
163
|
+
* which behaves exactly like the top-level rule (measured on vite-plus 0.3.1 and
|
|
164
|
+
* oxlint 1.81.0, 2026-09-19).
|
|
165
|
+
*
|
|
166
|
+
* TypeScript rejects a name that is no key of `paths` or `patterns`, a `from`
|
|
167
|
+
* naming no scope at all, and `use` beside `from`. A `from` naming a scope
|
|
168
|
+
* declared later, a `drop` of a name the parent does not restrict and an `add` of
|
|
169
|
+
* one it already does throw here, because resolution order is what decides them.
|
|
170
|
+
*/
|
|
171
|
+
export declare function restrictedImports<Paths extends {
|
|
172
|
+
[name: string]: RestrictedPath;
|
|
173
|
+
}, Patterns extends {
|
|
174
|
+
[name: string]: RestrictedPattern;
|
|
175
|
+
}, Scopes extends { [scope in keyof Scopes]: RestrictionScope<Extract<keyof Paths | keyof Patterns, string>, Extract<keyof Scopes, string>>; }>(config: {
|
|
176
|
+
paths: Paths;
|
|
177
|
+
patterns: Patterns;
|
|
178
|
+
scopes: Scopes;
|
|
179
|
+
}): Override[];
|
|
180
|
+
export interface DecodersOptions {
|
|
181
|
+
/**
|
|
182
|
+
* The sentence appended to both findings. The default names the files in
|
|
183
|
+
* `files`, which is what a reader has to know to act on either message.
|
|
184
|
+
*/
|
|
185
|
+
hint?: string;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* The decoder modules, the one place per kind of outside value where the parse
|
|
189
|
+
* belongs. Every file gets `no-runtime-typeof` and `no-unknown-parameters` at
|
|
190
|
+
* error carrying a hint that names these files, because moving the check into one
|
|
191
|
+
* of them is the fix in both cases, and `no-runtime-typeof` is off inside them.
|
|
192
|
+
*
|
|
193
|
+
* `no-unknown-parameters` and `no-unsafe-dictionary-type` stay on in the decoders.
|
|
194
|
+
* A decoder that needs either off says so in its own override with its own
|
|
195
|
+
* measured count, and one switch for the whole set would cover the files that
|
|
196
|
+
* never needed it.
|
|
197
|
+
*
|
|
198
|
+
* Place it before `tests()`, which switches `no-runtime-typeof` off for test files.
|
|
101
199
|
*/
|
|
102
|
-
export declare function
|
|
200
|
+
export declare function decoders(files: string[], options?: DecodersOptions): Override[];
|
|
103
201
|
/** The formatting both consumers settled on. Spread it and add what is yours. */
|
|
104
202
|
export declare const fmt: {
|
|
105
203
|
printWidth: number;
|