Repository navigation
Expand file tree
/
Copy patheslint.config.js
More file actions
596 lines (564 loc) · 28.8 KB
/
Copy patheslint.config.js
File metadata and controls
596 lines (564 loc) · 28.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
// Copyright (c) 2026 PixieBrix, Inc.
// Licensed under PolyForm Shield 1.0.0 — see LICENSE.
// ESLint complements Biome here: Biome owns formatting and the common-error
// recommended set; ESLint runs only the rules Biome does not have (unicorn's
// modern-API hints) plus this project's custom rules in `eslint-rules/`.
import path from "node:path";
import js from "@eslint/js";
import eslintComments from "@eslint-community/eslint-plugin-eslint-comments";
import importX from "eslint-plugin-import-x";
import jest from "eslint-plugin-jest";
import reactHooks from "eslint-plugin-react-hooks";
import unicorn from "eslint-plugin-unicorn";
import globals from "globals";
import tseslint from "typescript-eslint";
import localPlugin from "./eslint-rules/index.js";
// `import-x/no-restricted-paths` resolves `from` against basePath (cwd) but
// uses `except` patterns raw. We resolve them ourselves so the contract works
// from any cwd — and so minimatch doesn't have to traverse dot-prefixed
// segments like `.claude/worktrees/` (its default `dot: false` blocks `**`
// from crossing those).
const here = import.meta.dirname;
const rulePath = (suffix) => path.join(here, "src/rules", suffix);
export default tseslint.config(
{
ignores: [
"dist/**",
"node_modules/**",
"src/**/*.generated.*",
"eslint-rules/**",
"eslint.config.js",
"stylelint.config.js",
"jest.config.cjs",
],
},
js.configs.recommended,
...tseslint.configs.strictTypeChecked,
...tseslint.configs.stylisticTypeChecked,
unicorn.configs.recommended,
{
languageOptions: {
ecmaVersion: 2024,
sourceType: "module",
globals: {
...globals.browser,
...globals.node,
...globals.webextensions,
},
parserOptions: {
project: "./tsconfig.eslint.json",
tsconfigRootDir: import.meta.dirname,
},
},
plugins: {
"agent-browser-shield": localPlugin,
"@eslint-community/eslint-comments": eslintComments,
"import-x": importX,
},
settings: {
"import-x/resolver-next": [
importX.createNodeResolver({
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
}),
],
},
rules: {
"agent-browser-shield/rule-id-matches-filename": "error",
// Discourage silencing the linter. `no-unused-disable` removes stale
// disable comments so they don't accumulate as code changes around
// them; `no-restricted-disable` blocks suppression of the high-value
// type-safety and hooks rules outright — the right fix is the code,
// not a `// eslint-disable-next-line`.
"@eslint-community/eslint-comments/no-unused-disable": "error",
"@eslint-community/eslint-comments/no-restricted-disable": [
"error",
"@typescript-eslint/no-explicit-any",
"@typescript-eslint/no-floating-promises",
"@typescript-eslint/no-misused-promises",
"@typescript-eslint/no-non-null-assertion",
"@typescript-eslint/no-unsafe-argument",
"@typescript-eslint/no-unsafe-assignment",
"@typescript-eslint/no-unsafe-call",
"@typescript-eslint/no-unsafe-member-access",
"@typescript-eslint/no-unsafe-return",
"@typescript-eslint/switch-exhaustiveness-check",
"react-hooks/rules-of-hooks",
"react-hooks/exhaustive-deps",
],
// Already on via `strictTypeChecked`, but pinned explicitly so the
// intent is visible: every `switch` over a union (RuleId, message
// discriminants, etc.) must handle every case. Agents extending those
// unions get a compile-time nudge to update the switches.
"@typescript-eslint/switch-exhaustiveness-check": [
"error",
{ considerDefaultExhaustiveForUnions: true },
],
// Import hygiene — TS handles existence/named-export checking, so
// import-x here only owns structural issues TS can't see: cycles,
// duplicate import statements, self-imports, useless path segments,
// and inconsistent type-specifier style.
"import-x/no-cycle": ["error", { maxDepth: 10, ignoreExternal: true }],
"import-x/no-self-import": "error",
"import-x/no-duplicates": "error",
"import-x/no-useless-path-segments": "error",
"import-x/consistent-type-specifier-style": ["error", "prefer-top-level"],
// The module-boundary contract between src/rules and src/lib lives in
// dedicated config blocks below (search for `no-restricted-paths`) —
// scoping via `files`/`ignores` is cleaner than encoding the same
// negation inside the zone definitions.
// Disabled — overlaps with Biome's recommended set or its formatter.
"unicorn/no-useless-undefined": "off",
"unicorn/no-array-callback-reference": "off",
"unicorn/no-nested-ternary": "off",
// Preserve conventional JSDoc stars and Jest environment docblocks;
// comment layout belongs to the formatter.
"unicorn/no-asterisk-prefix-in-documentation-comments": "off",
"unicorn/prefer-combined-guards": "error",
"unicorn/prefer-ternary": "error",
"unicorn/prefer-logical-operator-over-ternary": "error",
// Renamed from `prevent-abbreviations` in unicorn v68 (same options).
"unicorn/name-replacements": [
"error",
{
replacements: {
// Idiomatic in JS/TS — kept as-is.
props: false,
params: false,
param: false,
ref: false,
refs: false,
args: false,
db: false,
// Node/build conventions. `env` is required by `NodeJS.ProcessEnv`;
// `src` is the standard `fs.cp` filter callback parameter name.
env: false,
src: false,
// Common file/module naming convention (`dom-utils.ts`, etc.).
utils: false,
util: false,
// Added to unicorn's defaults in v68; opted out to preserve prior
// behavior. `proto` is our prototype-capture shorthand in DOM
// patching; `app`/`application(s)` are used interchangeably in
// rule-application naming.
proto: false,
app: false,
application: false,
applications: false,
// Loop counters and map/filter callback params. The rule is
// not scope-aware, so these are allowed globally.
i: false,
j: false,
k: false,
x: false,
y: false,
},
},
],
// Disabled — too opinionated for this codebase.
"unicorn/no-null": "off",
"unicorn/no-array-reduce": "off",
"unicorn/no-array-for-each": "off",
"unicorn/prefer-module": "off",
"unicorn/import-style": "off",
"unicorn/no-keyword-prefix": "off",
"unicorn/switch-case-braces": "off",
// Keep but downgrade: useful guidance, but the codebase has many
// existing `for…of` loops we don't want to mass-rewrite right now.
"unicorn/no-for-loop": "warn",
// Filenames are kebab-case across the project, except React component
// files which follow PascalCase by ecosystem convention. v66 added
// directory-name checking (`checkDirectories`), which flags the Jest
// `__tests__` / `__test-mocks__` convention we deliberately keep — so
// restrict the rule to filenames only.
"unicorn/filename-case": [
"error",
{
cases: { kebabCase: true, pascalCase: true },
checkDirectories: false,
},
],
// eslint-plugin-unicorn 66 turned on a batch of new recommended rules.
// The cleanly autofixable ones are fixed in-tree and stay errors; the
// three below are turned off because their autofix is wrong (or
// redundant) for this codebase, and the rest are temporarily downgraded
// to warnings so the bump lands without a mass manual rewrite. Ratcheting
// them back to errors is tracked in #279.
//
// Off — autofix corrupts source or is redundant here:
// comment-content rewrites prose and real paths in comments
// (`.github/…` → `.GitHub/…`, `*.yaml` → `*.YAML`, `Vue` → `Vue.js`).
// prefer-https rewrites schema.org itemtype URIs and intentional
// `http://` test fixtures, changing their meaning.
// require-css-escape double-escapes values already run through this
// project's `escapeAttributeValue` helper and wraps numeric
// interpolations (`CSS.escape(i)`, which expects a string); the
// dynamic selector values it targets are already escaped via that
// helper, so the rule is redundant noise on the constant cases.
"unicorn/comment-content": "off",
"unicorn/prefer-https": "off",
"unicorn/require-css-escape": "off",
// Off — fires only on legitimate, unavoidable `this`-outside-a-class:
// native-prototype monkeypatches (`Element.prototype.attachShadow =
// function (this: Element) {…}`), `defineProperty` get/set accessors,
// page-world `executeScript` entry fns (`this: Window`), and
// webext-messaging handlers — none rewritable as class methods. Every
// hit would need a disable, so the rule is pure noise here. See #279.
"unicorn/no-this-outside-of-class": "off",
// Off — fire only on intentional patterns in this repo, so they have no
// path to `error`; disabled rather than left as standing warning noise
// (investigated under #279):
// no-computed-property-existence-check flags any `obj[dynamicKey]` in a
// boolean position, but our hits are value reads (`!RULE_DEFAULTS[id]`,
// `if (states[rule.id])`, `targetMask[i]`), not existence checks —
// `Object.hasOwn` would change meaning, and the rule has no option to
// narrow scope.
// prefer-await flags the fire-and-forget `void promise.then/.catch()`
// idiom (our `no-floating-promises` convention) in void fns, React
// effects, event-listener/`setTimeout` callbacks, and classic-script
// top-level — none of which can cleanly take `await`.
// no-top-level-side-effects's only hit is a deliberate, documented
// module-load `registerMethods({…})`.
"unicorn/no-computed-property-existence-check": "off",
"unicorn/prefer-await": "off",
"unicorn/no-top-level-side-effects": "off",
// max-nested-calls — the call-depth cap doesn't fit the two places we
// nest deeply on purpose: zod schema builders
// (`z.record(z.string(), z.union([z.string(), z.number()]))`) and
// fast-check generators (`fc.array(fc.tuple(fc.nat(…), …))`). Both
// are idiomatic and read better nested than split into temporaries.
"unicorn/max-nested-calls": "off",
// Off — blocked by the test runtime, not the codebase. The suggested
// `Uint8Array.fromBase64()`/`#toBase64()` are present in our Chrome
// target (133+; manifest requires 148) and in Bun (the build script),
// but Node 24 — which runs Jest/jsdom in CI — only exposes them behind
// the experimental `--js-base-64` flag, so they're absent under test.
// The 8 sites include two security-relevant decode paths
// (`encoded-payload-redact.ts`, `encoded-fixture.ts`) plus their
// fixtures; rewriting them would either break the suite or force an
// atob/btoa-based polyfill into jsdom — which would then exercise the
// shim, not Chrome's native impl, masking any prod divergence on a
// decode path. Not worth it for a stylistic rule. Revisit once Node
// ships these unflagged. See #279.
"unicorn/prefer-uint8array-base64": "off",
// Enforced (recommended `error`). Dynamically-built arrays pass an
// explicit comparator; the only sites carrying a scoped `eslint-disable`
// are where the rule can't be satisfied cleanly:
// require-array-sort-compare — compile-time-constant catalog lists
// (`RULE_IDS`, `Object.keys(RULE_DEFAULTS)`, …) sorted only to
// compare order-independently in tests; non-constant arrays
// (readdir results, computed diffs) get a `localeCompare` comparator.
// no-incorrect-query-selector — a deliberate `querySelectorAll("#id")`
// used to assert the element count.
// prefer-number-coercion — `Number()` replaces `parseInt(x, 10)` /
// `parseFloat(x)` only where the input is a bare numeric token (a
// `\d`-anchored regex capture, `String(i)`); the two scoped
// disables in `hidden-text-strip.ts` are where `parseFloat`'s
// leading-numeric parse is load-bearing: unit-suffixed CSS
// durations (`"0.5s"`) and a possibly-`""` computed `opacity`,
// both of which `Number()` would mis-coerce.
"unicorn/prefer-number-coercion": "error",
// Off — the rule's `:scope` rewrite is unsound for this codebase's
// dominant receiver type. It assumes the query receiver is an Element,
// where `el.querySelectorAll("foo")` and `el.querySelectorAll(":scope
// foo")` are equivalent. But our shadow-piercing scanners take
// `root: ParentNode` and run against `Document`, `ShadowRoot`, and
// `DocumentFragment` receivers at runtime — where `:scope` is NOT
// equivalent: it matches nothing on a ShadowRoot/DocumentFragment
// (so `:scope *`/`:scope noscript` silently return empty) and excludes
// `<html>` on a Document. Mechanically applying the suggestion would
// silently disable defenses inside shadow DOM (noscript-strip,
// hidden-text-strip, unicode-invisibles-strip, trust-badge-annotate,
// closed-shadow-root-annotate, …). The Element-receiver sites where
// `:scope` *is* safe are all single-compound selectors with no
// descendant combinator, so there's no correctness upside there either.
// The rule can't narrow to Element receivers or combinator selectors,
// so enabling it means breaking shadow-DOM scans or scoped-disabling
// ~10+ correct sites. Not worth it. See #279.
"unicorn/prefer-scoped-selector": "off",
// Off — not a clean ratchet (see #279). The rule treats a `switch`
// inside a loop as a "nested loop", so ~9 sites are idiomatic
// switch-case `break`s (required syntax, not loop control) that it
// would force into pointless function extractions. The genuinely
// improvable production sites — guard inversions, `.some()`, and small
// helper extractions — were cleaned up directly, but the rule itself
// can't be narrowed to exclude `switch` or the remaining test/mock
// sites, so leaving it on would be all churn for no further upside.
"unicorn/no-break-in-nested-loop": "off",
// eslint-plugin-unicorn 72 promoted two more rules to recommended.
// prefer-simple-condition-first was ratcheted in-tree (the six sites were
// reordered so the cheap operand short-circuits first) and stays at its
// recommended `error`. prefer-dom-node-html-methods is turned off:
// Off — the rule rewrites `.innerHTML` *reads* to `element.getHTML()`,
// but for a plain read the two are equivalent (both serialize the
// light-DOM descendants and exclude shadow content); getHTML()'s only
// added value is its shadow-root serialization options, which the fix
// doesn't pass. So there's no correctness or behavior upside. Worse,
// getHTML() is a recent DOM API absent from jsdom, and 12 of the 13
// hits are jsdom tests asserting on serialized output — rewriting them
// would throw `getHTML is not a function` at runtime (same test-runtime
// block as prefer-uint8array-base64). The lone production read
// (serializePageTree in irrelevant-sections-redact.ts) could switch,
// but only to be inconsistent with every test for no gain. Not worth it.
"unicorn/prefer-dom-node-html-methods": "off",
// eslint-plugin-unicorn 68 turned on another batch of recommended rules
// (and renamed `prevent-abbreviations` → `name-replacements`, handled
// above). The cleanly autofixable ones (prefer-boolean-return,
// prefer-continue, prefer-else-if, prefer-hoisting-branch-code,
// prefer-promise-with-resolvers) are fixed in-tree and stay at their
// recommended `error`. The five below fire only on intentional patterns,
// false positives, or established conventions, so they have no clean path
// to `error`; disabled rather than left as warning noise, investigated
// under #279:
// consistent-boolean-name flags ~130 boolean bindings without an
// is/has/… prefix. ~23 are genuine plain-variable wins
// (`enabled` → `isEnabled`, `hidden` → `isHidden`), but the rule also
// enforces the prefix on boolean-returning FUNCTIONS and offers no
// option to check variables only. That collides with this codebase's
// deliberate predicate-verb naming (`matchesDenylist`,
// `containsInjection`, `looksLikeNewsletterModal`, `passesLuhn`,
// `overlapsAny`) — ~35 helpers whose current names read better than
// any `is`/`has` form, and which `prefixes`-allowlisting every verb
// stem would only neuter the rule to allow. It also flags
// SCREAMING_CASE `*_ENABLED_DEFAULT` constants, where the convention
// doesn't map. Net: not worth enabling as shipped.
// no-top-level-assignment-in-function flags our module-singleton
// lazy-init idiom — a top-level `let` (unsubscribe handle,
// `listenerAttached` guard) assigned from inside the init/teardown
// function that owns it. Rewriting to avoid the pattern is the
// opposite of clearer here.
// no-non-function-verb-prefix fires on test spies named after the DOM
// method they wrap (`getAttributeSpy`, `setTimeoutSpy`, `setHtmlCount`)
// and the `removeEntirely` rule-option flag — all intentional.
// no-nonstandard-builtin-properties flags `Symbol.dispose`, which is
// standard as of explicit resource management (TS 5.2 / ES2024) and
// present in our Chrome/Node targets — the rule's builtin list is just
// stale.
// no-unreadable-for-of-expression fires on idiomatic function-call /
// `Object.values(…)` / `?? []` iterables in `for…of` headers
// (`for (const x of Object.values(fn(id)))`), which read fine; hoisting
// them to temporaries would be pure churn.
"unicorn/consistent-boolean-name": "off",
"unicorn/no-top-level-assignment-in-function": "off",
"unicorn/no-non-function-verb-prefix": "off",
"unicorn/no-nonstandard-builtin-properties": "off",
"unicorn/no-unreadable-for-of-expression": "off",
// eslint-plugin-unicorn 70 turned on four more recommended rules. All
// four were resolvable cleanly in-tree, so none needs an override — they
// stay at their recommended `error`:
// prefer-dom-node-replace-children — autofixed the `el.innerHTML = ""`
// DOM-reset idiom (test beforeEach teardown, ~150 sites) to
// `el.replaceChildren()`.
// prefer-simplified-conditions — autofixed two De Morgan negations
// (`!(a && !b)` → `!a || b`) in rule-engine/schema-trust-sanitize.
// prefer-toggle-attribute — the one hit was an if/else setAttribute("",
// "")/removeAttribute pair on `checked`; rewritten as
// `toggleAttribute("checked", element.checked)`.
// no-unnecessary-array-flat-map — the one hit was a
// `flatMap(x => cond ? [v] : [])` filter-map fusion; rewritten as
// `.map().filter(x => x !== undefined).map()`, which the inferred
// type predicate (TS 6) narrows without a non-null assertion.
// eslint-plugin-unicorn 76 promoted six more rules to recommended
// `error`. All were resolvable in-tree, so none needs an override — they
// stay at their recommended `error`:
// prefer-combined-guards — merged consecutive early-exit guards
// (`if (a) continue; if (b) continue;` → `if (a || b) continue;`, and
// the `return` equivalents) across the DOM scanners.
// prefer-ternary — collapsed if/else that return two values into a
// single ternary. Where the autofix produced a boolean-literal
// ternary (`cond ? true/false : x`) it was hand-rewritten to the
// logical-operator form the next rule wants; the guard-clause helpers
// it left as mixed if+ternary (the env-var `resolveDefault`s,
// `newsletter-modal-hide`) were restructured by hand so nothing reads
// worse than the original guards.
// prefer-logical-operator-over-ternary — the boolean-literal ternaries
// above became `a || b` / `!a && b` (`x ? true : y` → `x || y`,
// `x ? false : y` → `!x && y`), keeping the boolean return type.
// prefer-continue / prefer-early-return — inverted the last wrapping
// `if` in a loop/function into an early `continue`/`return` so the
// body dedents; the non-autofixable sites were done by hand.
// prefer-dom-node-replace-children — the one hit was a test fixture
// doing `body.replaceChildren(); body.append(node)`, collapsed to a
// single `replaceChildren(node)`. Production carrier-scrubbing blanks
// attributes/textContent rather than emptying-and-refilling, so the
// rule doesn't fire there.
// no-global-object-property-assignment stays at its recommended `error`
// for production code; it's disabled only for tests (which legitimately
// assign globals to set up mocks) in the test-files block below.
// Allow underscore-prefixed unused parameters (e.g. `_root` for the
// Rule#apply signature when a rule ignores the argument).
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
// Pure style — Biome's formatter and the codebase don't care about
// `T[]` vs `Array<T>` or the syntax used for non-null assertions.
"@typescript-eslint/array-type": "off",
"@typescript-eslint/non-nullable-type-assertion-style": "off",
// Allow `${number}` in template literals — common for debug strings
// and codegen output, and TS already guarantees the operand is a number.
"@typescript-eslint/restrict-template-expressions": [
"error",
{ allowNumber: true },
],
},
},
{
files: [
"src/**/__tests__/**/*.{ts,tsx}",
// `__test-mocks__` is test infrastructure (loaded via Jest setupFiles);
// it gets the same relaxations — same grouping tsconfig.test.json uses.
"src/__test-mocks__/**/*.{ts,tsx}",
],
plugins: { jest },
rules: {
// Explicit fixture arguments describe the scenario even when every
// current test passes the same value.
"unicorn/no-unnecessary-parameters": "off",
// Tests legitimately use literal nulls and many small `for` loops.
"unicorn/no-useless-undefined": "off",
"unicorn/consistent-function-scoping": "off",
// Tests assign onto global objects to install mocks / stubs; the rule
// stays an error everywhere else.
"unicorn/no-global-object-property-assignment": "off",
// Tests reach for `globalThis.*` deliberately — overriding globals for
// mocks (`globalThis.MutationObserver = …`) and dispatching events /
// reading computed style against the jsdom global. Enforced in prod.
"unicorn/no-unnecessary-global-this": "off",
// Jest matchers and mock patterns routinely pass methods by reference.
"@typescript-eslint/unbound-method": "off",
// Catch common test mistakes — focused/disabled tests left in,
// assertions that don't run, expects in unexpected places.
"jest/no-focused-tests": "error",
"jest/no-disabled-tests": "error",
"jest/valid-expect": "error",
"jest/no-standalone-expect": "error",
"jest/no-identical-title": "error",
"jest/valid-title": "error",
// `expect*` recognizes in-file helpers like `expectHidden(el)` that
// wrap one or more real `expect()` calls — without this, every rule
// test that delegates to such a helper trips the rule.
"jest/expect-expect": [
"error",
{ assertFunctionNames: ["expect", "expect*"] },
],
// Off: the codebase relies on TS discriminated-union narrowing
// expect(result.ok).toBe(false);
// if (!result.ok) { expect(result.error).toMatch(/.../); }
// The rule flags any `expect` inside an `if`, which is too coarse for
// this pattern. Both branches of an `it.each` matrix also legitimately
// assert different things.
"jest/no-conditional-expect": "off",
},
},
{
files: ["build.ts", "scripts/**/*.{ts,js,mjs,cjs}"],
languageOptions: {
globals: { ...globals.node },
},
rules: {
"unicorn/no-process-exit": "off",
},
},
{
// Rules of Hooks + exhaustive-deps catch stale-closure and conditional-
// hook bugs that TypeScript can't see. Scoped to the React surfaces
// (popup/options + components under lib/).
files: ["src/**/*.tsx"],
plugins: { "react-hooks": reactHooks },
rules: {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "error",
},
},
{
// Module-boundary contract: each rule file is a leaf. It can depend on
// shared helpers in ../lib, its own ./types contract, and codegen output
// (*.generated.ts) — but not on another rule's implementation. The only
// place allowed to enumerate the catalog is rules/index.ts (which is
// excluded from this block via `ignores`), where the RuleId union and
// RULES array are derived from a single tuple.
//
// `except` entries are absolute globs (via rulePath) for the reasons in
// the helper's docstring at the top of this file.
files: ["src/rules/*.ts"],
ignores: [
"src/rules/index.ts",
"src/rules/types.ts",
"src/rules/*.generated.ts",
],
rules: {
"import-x/no-restricted-paths": [
"error",
{
zones: [
{
target: "./src/rules",
from: "./src/rules/*.ts",
// Brace expansions force is-glob to return true; the only file
// matched by each pattern is the literal one named.
except: [rulePath("types.{ts,tsx}"), rulePath("*.generated.ts")],
message:
"Rules must not import sibling rules — only ./types and ./*.generated.ts. Shared helpers belong in ../lib; the catalog lives in ./index.ts.",
},
],
},
],
},
},
{
// lib/ owns the runtime engine and shared helpers. It depends on the
// rule catalog (rules/index), the contract (rules/types), the
// generated tables, and the hand-edited rule-metadata table — never on
// a specific rule implementation. Reaching into one rule from lib
// quietly couples a helper to that rule's internals and makes the next
// rule that needs the same hook copy-paste instead of factor.
files: ["src/lib/**/*.ts", "src/lib/**/*.tsx"],
ignores: ["src/lib/**/__tests__/**"],
rules: {
"import-x/no-restricted-paths": [
"error",
{
zones: [
{
target: "./src/lib",
from: "./src/rules/*.ts",
except: [
rulePath("{index,types,rule-metadata}.ts"),
rulePath("*.generated.ts"),
],
message:
"lib/ may only depend on rules/{index,types,rule-metadata,*.generated} — not on a specific rule file.",
},
],
},
],
},
},
{
// `data-abs-*` attributes are the extension's DOM marker namespace —
// every one the engine or any rule stamps on a node has to be declared
// and exported from `src/lib/dom-markers.ts` so collisions surface at
// compile time and new rules have one canonical naming reference.
// Allowed only in the registry file itself.
ignores: ["src/lib/dom-markers.ts"],
rules: {
"no-restricted-syntax": [
"error",
{
selector: "Literal[value=/^data-abs-/]",
message:
"data-abs-* attribute names must be declared in src/lib/dom-markers.ts and imported — no inline string literals.",
},
{
selector: "TemplateElement[value.raw=/^data-abs-/]",
message:
"data-abs-* attribute names must be declared in src/lib/dom-markers.ts and imported — no inline template literals.",
},
],
},
},
);