@useinsider/eslint-config 4.0.0-beta.1 → 4.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,8 +27,8 @@ export const { useInsiderESLint, useInsiderOXC } = useInsider({
27
27
  });
28
28
  ```
29
29
 
30
- ```js
31
- // eslint.config.mjs
30
+ ```ts
31
+ // eslint.config.ts
32
32
  import { useInsiderESLint } from './insider.lint.ts';
33
33
 
34
34
  export default useInsiderESLint();
@@ -52,6 +52,13 @@ Three constraints this layout is not free to break:
52
52
  - **The `./insider.lint.ts` import keeps its real extension.** Neither
53
53
  ESLint's loader nor oxlint's applies TypeScript's `.js` → `.ts` specifier
54
54
  rewriting, so `'./insider.lint.js'` does not resolve.
55
+ - **The ESLint config needs a TypeScript extension too** — `eslint.config.ts`
56
+ or `eslint.config.mts`. ESLint routes those through jiti, which resolves the
57
+ TypeScript lint module on every supported Node version. A `.js`/`.mjs`/`.cjs`
58
+ config is loaded by Node directly, so importing `./insider.lint.ts` from one
59
+ works only where Node's type stripping is unflagged (Node >= 22.18) and dies
60
+ with `ERR_UNKNOWN_FILE_EXTENSION` on Node 20. Both CLI commands therefore
61
+ rename an existing JavaScript flat config and delete the original.
55
62
  - **The oxlint config must be `oxlint.config.ts` or `oxlint.config.mts`.**
56
63
  oxlint silently ignores `oxlint.config.{cts,js,mjs,cjs}` and falls through
57
64
  to the nearest ancestor config. In a package with no `"type": "module"`,
@@ -59,7 +66,11 @@ Three constraints this layout is not free to break:
59
66
  - **A TypeScript project needs `allowImportingTsExtensions: true`** in the
60
67
  `tsconfig.json` that covers the layout files, or `tsc` rejects the import
61
68
  with `TS5097`. Both CLI flows offer to set it for you, following `extends`
62
- first so a shared base that already carries it is left alone.
69
+ first so a shared base that already carries it is left alone. TypeScript only
70
+ accepts the flag on a project with `noEmit`, `emitDeclarationOnly` or
71
+ `rewriteRelativeImportExtensions` — on an emitting project it fails with
72
+ `TS5096` — so a project that emits is never edited; the CLI explains what to
73
+ turn on instead.
63
74
  - **`oxlint` runs with `--disable-nested-config`.** The emitted config carries
64
75
  options oxlint accepts only in a root config, and a discovered nested config
65
76
  carrying them aborts the whole run. Disabling nested discovery also makes
@@ -83,7 +94,9 @@ is a sign your scope coverage is incomplete — describe the extra files inside
83
94
  - [Strict and spell-checker knobs](#strict-and-spell-checker-knobs)
84
95
  - [API reference](#api-reference)
85
96
  - [Unused disable directives](#unused-disable-directives)
97
+ - [Lint scripts](#lint-scripts)
86
98
  - [Migrating to v4](#migrating-to-v4)
99
+ - [The guided migration offer](#the-guided-migration-offer)
87
100
  - [Migrating from v1.x](#migrating-from-v1x)
88
101
  - [First lint after install](#first-lint-after-install)
89
102
  - [Troubleshooting](#troubleshooting)
@@ -105,11 +118,14 @@ npx @useinsider/eslint-config
105
118
  The CLI walks an interactive prompt (project type, language, framework, test
106
119
  runner, jQuery globals, tsconfig layout), installs every required linter and
107
120
  plugin pinned to the versions this package was built against, writes the three
108
- layout files at the project root, sets
109
- `"lint": "oxlint --disable-nested-config && eslint ."` in your `package.json`,
110
- and offers to add `allowImportingTsExtensions: true` to your `tsconfig.json`.
111
- That last edit is surgical — comments, key order and indentation survive — and
112
- declining it prints the exact snippet to paste.
121
+ layout files at the project root, wires oxlint into the script that actually
122
+ invokes ESLint (see [Lint scripts](#lint-scripts)), and offers to add
123
+ `allowImportingTsExtensions: true` to the tsconfig that really governs those
124
+ files. In a solution-style project that is the referenced `tsconfig.node.json`,
125
+ not the root — and if an applicable config already sets the flag, or no tsconfig
126
+ type-checks those files, you are not asked at all. That edit is surgical —
127
+ comments, key order and indentation survive — and declining it prints the exact
128
+ snippet to paste.
113
129
 
114
130
  To migrate an existing v1 `eslint.config.{js,mjs,cjs}` or a legacy
115
131
  `.eslintrc.{js,cjs}`:
@@ -432,8 +448,51 @@ silenceRules(['no-magic-numbers']);
432
448
  export const { useInsiderESLint, useInsiderOXC } = useInsider({ /* ... */ });
433
449
  ```
434
450
 
451
+ ## Lint scripts
452
+
453
+ Both linters have to run, and oxlint has to run *first* — the ESLint side
454
+ turns off every rule oxlint owns, so ESLint alone silently drops those checks.
455
+ The canonical script is:
456
+
457
+ ```json
458
+ { "scripts": { "lint": "oxlint --disable-nested-config && eslint ." } }
459
+ ```
460
+
461
+ The CLI never replaces a script it did not author. It finds the **one** script
462
+ that really invokes the `eslint` binary and **prepends** oxlint to it, leaving
463
+ the original command verbatim inside the new value. A composed `lint` that fans
464
+ out to a type-check and a stylelint pass is therefore untouched:
465
+
466
+ ```diff
467
+ "lint": "run-p lint:type lint:es lint:style",
468
+ "lint:es": "npm run lint:es-ci -- --cache",
469
+ - "lint:es-ci": "NODE_OPTIONS=--max-old-space-size=6144 eslint .",
470
+ + "lint:es-ci": "oxlint --disable-nested-config && NODE_OPTIONS=--max-old-space-size=6144 eslint .",
471
+ "lint:style": "npm run lint:style-ci -- --cache",
472
+ "lint:type": "run-p lint:type:app lint:type:test lint:type:node",
473
+ ```
474
+
475
+ Prepending rather than appending is deliberate: a `--` passthrough
476
+ (`npm run lint:es-ci -- --cache`) lands on the *last* command, so `--cache`
477
+ still reaches ESLint. A script whose name ends in a `--fix` invocation gets
478
+ `oxlint --disable-nested-config --fix`. Every rewrite is printed with its
479
+ previous value.
480
+
481
+ Exactly one script is rewritten. When several entry points run ESLint —
482
+ `lint`, `lint:fix`, `lint:ci` — prepending to all of them would run oxlint once
483
+ per entry point over the same tree, so the CLI picks the entry point `lint`
484
+ designates (itself, or the leaf it delegates to) and reports the rest with the
485
+ prefix to add if you want oxlint on them too.
486
+
487
+ If no script invokes `eslint` and a `lint` script already exists, nothing is
488
+ written — the conflict is reported with the command to add by hand.
489
+
435
490
  ## Migrating to v4
436
491
 
492
+ v4 requires **Node.js 24.19+** (`engines.node` was `>=20` through v3). Check
493
+ your CI images and any repo pinning an older Node before upgrading: npm and
494
+ pnpm warn with `EBADENGINE`, and fail outright under `engine-strict`.
495
+
437
496
  v4 splits the lint run across ESLint and oxlint. Run the CLI to do all of
438
497
  this automatically:
439
498
 
@@ -441,6 +500,37 @@ this automatically:
441
500
  npx @useinsider/eslint-config migrate
442
501
  ```
443
502
 
503
+ ### The guided migration offer
504
+
505
+ You do not have to know that command exists. Running lint against a v3 config
506
+ under v4 detects the outdated structure and, in an interactive terminal, offers
507
+ to run the migration there and then:
508
+
509
+ ```
510
+ @useinsider/eslint-config: eslint.config.js still uses the v3 single-file layout.
511
+ /repo/eslint.config.js calls `useInsider()` itself, and
512
+ insider.lint.ts and oxlint.config.ts are missing.
513
+
514
+ Starting the guided migration. It shows every change before applying it.
515
+ Skip it with INSIDER_NO_PROMPT=true.
516
+
517
+ ◆ eslint.config.js still uses the v3 single-file layout. Migrate it now?
518
+ │ ● Yes / ○ No
519
+ ```
520
+
521
+ Accepting runs the normal `migrate` flow — same summary, same confirmations —
522
+ and then exits non-zero asking you to re-run lint, because this process already
523
+ loaded the configuration that was just rewritten.
524
+
525
+ The offer is suppressed, and the failure message leads with the `migrate`
526
+ command instead, whenever prompting would be wrong: under oxlint (whose stdout
527
+ is its report), in CI, without a TTY, with `INSIDER_NO_PROMPT=true`, or with
528
+ `INSIDER_SKIP_CONFIG_VALIDATION=true`.
529
+
530
+ The directive rewrite is reported as its own step, with the file count shown
531
+ *before* anything is written, so you can take it as a separate commit or
532
+ decline it and run `migrate` again later.
533
+
444
534
  What changes:
445
535
 
446
536
  - **`eslint-disable` no longer suppresses an oxlint-owned rule. This is the
@@ -509,7 +599,8 @@ What changes:
509
599
  nothing here applies TypeScript's `.js` → `.ts` rewriting — and `tsc` fails
510
600
  with `TS5097` otherwise. The CLI prompts to add it and resolves the full
511
601
  `extends` chain first, so a shared base that already sets it is left alone;
512
- declining prints the snippet to paste.
602
+ declining prints the snippet to paste. A project that emits is never edited —
603
+ TypeScript rejects the flag there with `TS5096`.
513
604
  - **`@typescript-eslint` type-aware rules, `sonarjs`, `@stylistic`, `@cspell`,
514
605
  `jsdoc/check-tag-names` and `no-inner-declarations` stay with ESLint.** Two
515
606
  of those are permanent, for different reasons:
@@ -44,5 +44,11 @@ declare const scopeMap: {
44
44
  };
45
45
  export type ScopeName = keyof typeof scopeMap;
46
46
  export declare function isScopeName(key: string): key is ScopeName;
47
+ /**
48
+ * Every scope name `useInsider()` accepts, for error messages that have to spell the options out.
49
+ * @example
50
+ * scopeNames().includes('typescriptNode'); // true
51
+ */
52
+ export declare function scopeNames(): ScopeName[];
47
53
  export declare function buildScopeBlocks(scope: ScopeName, entries: readonly ScopeEntry[]): Promise<Linter.Config[]>;
48
54
  export {};