@useinsider/eslint-config 3.0.2 → 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.
Files changed (60) hide show
  1. package/README.md +376 -64
  2. package/dist/builders/configFiles.d.ts +13 -0
  3. package/dist/builders/oxlint.d.ts +51 -0
  4. package/dist/builders/oxlintOwnership.d.ts +43 -0
  5. package/dist/builders/scopeBlocks.d.ts +6 -0
  6. package/dist/cli.cjs +1028 -86
  7. package/dist/cli.cjs.map +1 -1
  8. package/dist/configs/dependencies.d.ts +3 -0
  9. package/dist/{javascript-VBXqjVi8.cjs → javascript-DH8-N38w.cjs} +5 -5
  10. package/dist/{javascript-VBXqjVi8.cjs.map → javascript-DH8-N38w.cjs.map} +1 -1
  11. package/dist/{javascript-dom-5jRL_yKP.cjs → javascript-dom-CvtQ6_9W.cjs} +4 -4
  12. package/dist/{javascript-dom-5jRL_yKP.cjs.map → javascript-dom-CvtQ6_9W.cjs.map} +1 -1
  13. package/dist/{javascript-node-C1AU_4X2.cjs → javascript-node-BNsy3loY.cjs} +4 -4
  14. package/dist/{javascript-node-C1AU_4X2.cjs.map → javascript-node-BNsy3loY.cjs.map} +1 -1
  15. package/dist/{jest-CHzjzBtf.cjs → jest-4Es9Ggaa.cjs} +3 -3
  16. package/dist/{jest-CHzjzBtf.cjs.map → jest-4Es9Ggaa.cjs.map} +1 -1
  17. package/dist/main.cjs +763 -78
  18. package/dist/main.cjs.map +1 -1
  19. package/dist/main.d.ts +9 -2
  20. package/dist/main.js +746 -61
  21. package/dist/main.js.map +1 -1
  22. package/dist/oxlintOwnership-7hpdB3d1.cjs +199 -0
  23. package/dist/oxlintOwnership-7hpdB3d1.cjs.map +1 -0
  24. package/dist/oxlintOwnership-Cx0hgA_J.js +127 -0
  25. package/dist/oxlintOwnership-Cx0hgA_J.js.map +1 -0
  26. package/dist/removePluginsFromConfigs-D0oSUj2C.cjs.map +1 -1
  27. package/dist/removePluginsFromConfigs-DzqX2Ju2.js.map +1 -1
  28. package/dist/{stylistic-Dio0AM2E.cjs → stylistic-CoP3MT1Y.cjs} +4 -4
  29. package/dist/{stylistic-Dio0AM2E.cjs.map → stylistic-CoP3MT1Y.cjs.map} +1 -1
  30. package/dist/{typescript-C1icmOxY.cjs → typescript-D3VeLnpn.cjs} +7 -7
  31. package/dist/{typescript-C1icmOxY.cjs.map → typescript-D3VeLnpn.cjs.map} +1 -1
  32. package/dist/{typescript-dom-RUJn2WCZ.cjs → typescript-dom-iB_xBTAD.cjs} +4 -4
  33. package/dist/{typescript-dom-RUJn2WCZ.cjs.map → typescript-dom-iB_xBTAD.cjs.map} +1 -1
  34. package/dist/{typescript-node-CBEzcc56.cjs → typescript-node-Cq6qhj53.cjs} +4 -4
  35. package/dist/{typescript-node-CBEzcc56.cjs.map → typescript-node-Cq6qhj53.cjs.map} +1 -1
  36. package/dist/utils/ensureConfigStructure.d.ts +15 -6
  37. package/dist/utils/ensureOxlintConfig.d.ts +10 -0
  38. package/dist/utils/layoutDiagnosis.d.ts +63 -0
  39. package/dist/utils/offerMigration.d.ts +42 -0
  40. package/dist/utils/silenceRules.d.ts +9 -0
  41. package/dist/{vitest-BomI8gtu.cjs → vitest-BlGQswK2.cjs} +3 -3
  42. package/dist/{vitest-BomI8gtu.cjs.map → vitest-BlGQswK2.cjs.map} +1 -1
  43. package/dist/{vue2-azKwRgaJ.cjs → vue2-Da1i_oVn.cjs} +8 -8
  44. package/dist/{vue2-azKwRgaJ.cjs.map → vue2-Da1i_oVn.cjs.map} +1 -1
  45. package/dist/{vue2-options-api-ma3J-bvM.cjs → vue2-options-api-BSdrUm8H.cjs} +8 -8
  46. package/dist/{vue2-options-api-ma3J-bvM.cjs.map → vue2-options-api-BSdrUm8H.cjs.map} +1 -1
  47. package/dist/{vue2-typescript-CZSHzp7I.cjs → vue2-typescript-dXLRSGNP.cjs} +10 -10
  48. package/dist/{vue2-typescript-CZSHzp7I.cjs.map → vue2-typescript-dXLRSGNP.cjs.map} +1 -1
  49. package/dist/{vue2-typescript-options-api-B8OzZlje.cjs → vue2-typescript-options-api-LM7U5dWq.cjs} +10 -10
  50. package/dist/{vue2-typescript-options-api-B8OzZlje.cjs.map → vue2-typescript-options-api-LM7U5dWq.cjs.map} +1 -1
  51. package/dist/{vue3-Df5UQ1Ys.cjs → vue3-621EM2Ii.cjs} +8 -8
  52. package/dist/{vue3-Df5UQ1Ys.cjs.map → vue3-621EM2Ii.cjs.map} +1 -1
  53. package/dist/{vue3-options-api-Bx7yYHza.cjs → vue3-options-api-CC9_fAiq.cjs} +8 -8
  54. package/dist/{vue3-options-api-Bx7yYHza.cjs.map → vue3-options-api-CC9_fAiq.cjs.map} +1 -1
  55. package/dist/{vue3-typescript-C0O1JZlk.cjs → vue3-typescript-DONe-IKz.cjs} +10 -10
  56. package/dist/{vue3-typescript-C0O1JZlk.cjs.map → vue3-typescript-DONe-IKz.cjs.map} +1 -1
  57. package/dist/{vue3-typescript-options-api-cPal4O-K.cjs → vue3-typescript-options-api-D4pqpDFs.cjs} +10 -10
  58. package/dist/{vue3-typescript-options-api-cPal4O-K.cjs.map → vue3-typescript-options-api-D4pqpDFs.cjs.map} +1 -1
  59. package/package.json +8 -5
  60. package/dist/rolldown-runtime-VH7oDXx4.cjs +0 -28
package/dist/main.js CHANGED
@@ -1,12 +1,14 @@
1
+ import { a as isVueUnsafeRule, i as isRuleDisabled, n as VUE_LIKE_GLOBS, o as toOxlintPluginName, r as isOxlintOwned, s as toOxlintRuleName, t as OXLINT_CATEGORIES } from "./oxlintOwnership-Cx0hgA_J.js";
1
2
  import { t as merge } from "./merge-R5ZHqMsV.js";
2
3
  import { t as mergeAll } from "./mergeAllConfig-B2rSXCdn.js";
3
4
  import { defineConfig } from "eslint/config";
4
5
  import globals from "globals";
5
6
  import { existsSync, readFileSync, realpathSync } from "node:fs";
6
7
  import { createRequire } from "node:module";
7
- import { dirname, resolve } from "node:path";
8
+ import { basename, dirname, join, resolve } from "node:path";
8
9
  import { fileURLToPath } from "node:url";
9
10
  import { AST_NODE_TYPES } from "@typescript-eslint/types";
11
+ import { spawnSync } from "node:child_process";
10
12
  import { installPackage } from "@antfu/install-pkg";
11
13
  import semver from "semver";
12
14
  import { warn } from "node:console";
@@ -29,6 +31,33 @@ var configFileRules = {
29
31
  "import-x/no-extraneous-dependencies": "off",
30
32
  "import-x/prefer-default-export": "off"
31
33
  };
34
+ var LINT_TOPOLOGY_NAME = "insider/lint-topology";
35
+ /**
36
+ * The three files `useInsider()` requires a consumer to own. They are always linted with
37
+ * {@link buildLintTopologyBlocks}, independently of the `configFiles` option.
38
+ */
39
+ var LINT_TOPOLOGY_GLOBS = [
40
+ "**/insider.lint.{ts,mts,cts,js,mjs,cjs}",
41
+ "**/eslint.config.{ts,mts,cts,js,mjs,cjs}",
42
+ "**/oxlint.config.{ts,mts,cts,js,mjs,cjs}"
43
+ ];
44
+ var lintTopologyRules = {
45
+ ...configFileRules,
46
+ "import-x/extensions": "off"
47
+ };
48
+ /**
49
+ * Relaxations the mandated three-file layout needs in order to lint clean. Emitted unconditionally
50
+ * because the layout exists whether or not the consumer passes `configFiles`.
51
+ * @example
52
+ * buildLintTopologyBlocks()[0].rules['import-x/extensions']; // 'off'
53
+ */
54
+ function buildLintTopologyBlocks() {
55
+ return [{
56
+ name: LINT_TOPOLOGY_NAME,
57
+ files: [...LINT_TOPOLOGY_GLOBS],
58
+ rules: { ...lintTopologyRules }
59
+ }];
60
+ }
32
61
  function mergeGlobs(userGlobs) {
33
62
  const seen = /* @__PURE__ */ new Set();
34
63
  const merged = [];
@@ -58,17 +87,459 @@ function buildConfigFilesBlocks(value) {
58
87
  function buildJqueryBlocks(entries) {
59
88
  if (!entries || entries.length === 0) return [];
60
89
  return entries.map((entry) => ({
61
- files: entry.files,
90
+ files: [...entry.files],
62
91
  languageOptions: { globals: { ...globals.jquery } }
63
92
  }));
64
93
  }
65
94
  //#endregion
66
- //#region src/utils/ensureConfigStructure.ts
67
- var CONFIG_FILE_REGEX = /eslint\.config\.[mc]?[jt]s(?:$|\?)/;
95
+ //#region src/builders/oxlint.ts
96
+ var OXLINT_SUPPRESSION_NAME = "insider/oxlint-suppression";
97
+ var OXLINT_SFC_EXCEPTION_NAME = "insider/oxlint-suppression-sfc-exception";
98
+ var UNUSED_DIRECTIVE_BLOCK_NAME = "insider/unused-disable-directives";
99
+ var UNUSED_DIRECTIVE_SEVERITY = "warn";
100
+ var OXLINT_DIRECTIVE_OPTIONS = {
101
+ respectEslintDisableDirectives: false,
102
+ reportUnusedDisableDirectives: UNUSED_DIRECTIVE_SEVERITY
103
+ };
104
+ /**
105
+ * The always-on ESLint block that makes a stale `eslint-disable` visible. ESLint 9 defaults this to
106
+ * `warn`, but the split relies on it, so it is pinned rather than inherited.
107
+ * @example
108
+ * buildUnusedDirectiveBlocks()[0].linterOptions; // { reportUnusedDisableDirectives: 'warn' }
109
+ */
110
+ function buildUnusedDirectiveBlocks() {
111
+ return [{
112
+ name: UNUSED_DIRECTIVE_BLOCK_NAME,
113
+ linterOptions: { reportUnusedDisableDirectives: UNUSED_DIRECTIVE_SEVERITY }
114
+ }];
115
+ }
116
+ function ownedRuleNames(rules) {
117
+ return Object.keys(rules).filter((name) => isOxlintOwned(name) && !isRuleDisabled(rules[name]));
118
+ }
119
+ function translatableRuleNames(rules) {
120
+ return Object.keys(rules).filter(isOxlintOwned);
121
+ }
122
+ function toOffRecord(ruleNames) {
123
+ return Object.fromEntries(ruleNames.map((name) => [name, "off"]));
124
+ }
125
+ function suppressionFor(block) {
126
+ if (!block.rules) return [];
127
+ const owned = ownedRuleNames(block.rules);
128
+ if (owned.length === 0) return [];
129
+ const files = block.files ? { files: [...block.files] } : {};
130
+ const blockIgnores = block.ignores ? [...block.ignores] : [];
131
+ const vueSafe = owned.filter((name) => !isVueUnsafeRule(name));
132
+ const vueUnsafe = owned.filter((name) => isVueUnsafeRule(name));
133
+ const suppressions = [];
134
+ if (vueSafe.length > 0) suppressions.push({
135
+ name: OXLINT_SUPPRESSION_NAME,
136
+ ...files,
137
+ ...blockIgnores.length > 0 ? { ignores: blockIgnores } : {},
138
+ rules: toOffRecord(vueSafe)
139
+ });
140
+ if (vueUnsafe.length > 0) suppressions.push({
141
+ name: OXLINT_SFC_EXCEPTION_NAME,
142
+ ...files,
143
+ ignores: [...blockIgnores, ...VUE_LIKE_GLOBS],
144
+ rules: toOffRecord(vueUnsafe)
145
+ });
146
+ return suppressions;
147
+ }
148
+ /**
149
+ * ESLint blocks that turn off every rule the emitted oxlint config takes over. Rules oxlint cannot
150
+ * check inside an SFC are suppressed everywhere except `.vue`/`.svelte`/`.astro`, so a Vue consumer
151
+ * keeps them.
152
+ * @example
153
+ * buildOxlintSuppressionBlocks([{ files: ['src/**\/*.ts'], rules: { 'no-var': 'error' } }]);
154
+ * // [{ files: ['src/**\/*.ts'], rules: { 'no-var': 'off' } }]
155
+ */
156
+ function buildOxlintSuppressionBlocks(blocks) {
157
+ return blocks.flatMap(suppressionFor);
158
+ }
159
+ function translateRules(rules, plugins) {
160
+ return translatableRuleNames(rules).reduce((accumulator, name) => {
161
+ plugins.add(toOxlintPluginName(name));
162
+ accumulator[toOxlintRuleName(name)] = rules[name];
163
+ return accumulator;
164
+ }, {});
165
+ }
166
+ function anchorOxlintGlob(glob) {
167
+ return glob.startsWith("**") || glob.startsWith("./") || glob.startsWith("/") ? glob : `./${glob}`;
168
+ }
169
+ function anchorGlobs(globs) {
170
+ return globs.map(anchorOxlintGlob);
171
+ }
172
+ function anchorOxlintIgnorePattern(pattern) {
173
+ const negated = pattern.startsWith("!");
174
+ const body = negated ? pattern.slice(1) : pattern;
175
+ const anchored = body.startsWith("/") || body.startsWith("**") ? body : `/${body}`;
176
+ return negated ? `!${anchored}` : anchored;
177
+ }
178
+ function overrideFor(block, plugins) {
179
+ if (!block.files || !block.rules) return [];
180
+ const translated = translateRules(block.rules, plugins);
181
+ if (Object.keys(translated).length === 0) return [];
182
+ return [{
183
+ files: anchorGlobs(block.files),
184
+ ...block.ignores && block.ignores.length > 0 ? { excludeFiles: anchorGlobs(block.ignores) } : {},
185
+ rules: translated
186
+ }];
187
+ }
188
+ /**
189
+ * The oxlint half of the split, derived from the same emitted ESLint blocks the suppression layer
190
+ * reads. Every oxlint category is pinned off so the config enables exactly the preset's own rules
191
+ * and nothing oxlint would otherwise turn on by default.
192
+ * @example
193
+ * buildOxlintConfig([{ files: ['src/**\/*.ts'], rules: { 'no-var': 'error' } }], ['dist']);
194
+ * // { plugins: ['eslint'], categories: { correctness: 'off', ... }, ignorePatterns: ['/dist'], ... }
195
+ */
196
+ function buildOxlintConfig(blocks, ignores) {
197
+ const plugins = /* @__PURE__ */ new Set(["eslint"]);
198
+ const overrides = blocks.flatMap((block) => overrideFor(block, plugins));
199
+ const globalRules = blocks.filter((block) => !block.files && block.rules).reduce((accumulator, block) => ({
200
+ ...accumulator,
201
+ ...translateRules(block.rules, plugins)
202
+ }), {});
203
+ return {
204
+ plugins: [...plugins].sort((left, right) => left.localeCompare(right)),
205
+ categories: Object.fromEntries(OXLINT_CATEGORIES.map((name) => [name, "off"])),
206
+ ...ignores && ignores.length > 0 ? { ignorePatterns: ignores.map(anchorOxlintIgnorePattern) } : {},
207
+ options: { ...OXLINT_DIRECTIVE_OPTIONS },
208
+ rules: globalRules,
209
+ overrides
210
+ };
211
+ }
212
+ //#endregion
213
+ //#region src/utils/layoutDiagnosis.ts
214
+ var MODULE_EXTENSIONS = [
215
+ "ts",
216
+ "mts",
217
+ "cts",
218
+ "js",
219
+ "mjs",
220
+ "cjs"
221
+ ];
222
+ var LINT_MODULE_FILENAMES = MODULE_EXTENSIONS.map((ext) => `insider.lint.${ext}`);
223
+ var ESLINT_CONFIG_FILENAMES = MODULE_EXTENSIONS.map((ext) => `eslint.config.${ext}`);
224
+ var OXLINT_CONFIG_FILENAMES = ["oxlint.config.ts", "oxlint.config.mts"];
68
225
  var OPT_OUT_ENV = "INSIDER_SKIP_CONFIG_VALIDATION";
69
- var FILE_URL_PREFIX = "file://";
70
- var TARGET_CALLEE_NAME = "useInsider";
226
+ var MIGRATE_COMMAND = "npx @useinsider/eslint-config migrate";
227
+ var SCAFFOLD_COMMAND = "npx @useinsider/eslint-config";
71
228
  var INSIDER_IMPORT_SOURCE = "@useinsider/eslint-config";
229
+ var TARGET_CALLEE_NAME = "useInsider";
230
+ var ESLINT_RENDERER_NAME = "useInsiderESLint";
231
+ var OXC_RENDERER_NAME = "useInsiderOXC";
232
+ var RENDERER_EXPORT = `export const ${`{ ${ESLINT_RENDERER_NAME}, ${OXC_RENDERER_NAME} }`} = ${TARGET_CALLEE_NAME}({ ... })`;
233
+ var EXPECTED_LAYOUT = [
234
+ "Expected layout (three sibling files):",
235
+ "",
236
+ " // insider.lint.ts",
237
+ ` import { ${TARGET_CALLEE_NAME} } from '${INSIDER_IMPORT_SOURCE}';`,
238
+ "",
239
+ ` ${RENDERER_EXPORT};`,
240
+ "",
241
+ " // eslint.config.ts",
242
+ ` import { ${ESLINT_RENDERER_NAME} } from './insider.lint.ts';`,
243
+ "",
244
+ ` export default ${ESLINT_RENDERER_NAME}();`,
245
+ "",
246
+ " // oxlint.config.ts",
247
+ ` import { ${OXC_RENDERER_NAME} } from './insider.lint.ts';`,
248
+ "",
249
+ ` export default await ${OXC_RENDERER_NAME}();`
250
+ ].join("\n");
251
+ function findSibling(directory, filenames, fileExists) {
252
+ return filenames.find((filename) => fileExists(join(directory, filename))) ?? null;
253
+ }
254
+ var lazyRequire$1 = createRequire(import.meta.url);
255
+ function parseProgram(source) {
256
+ try {
257
+ const { parse } = lazyRequire$1("@typescript-eslint/parser");
258
+ return parse(source, {
259
+ sourceType: "module",
260
+ range: true,
261
+ loc: true
262
+ });
263
+ } catch {
264
+ return null;
265
+ }
266
+ }
267
+ function collectUseInsiderAliases(program) {
268
+ const aliases = /* @__PURE__ */ new Set();
269
+ program.body.forEach((node) => {
270
+ if (node.type !== AST_NODE_TYPES.ImportDeclaration) return;
271
+ node.specifiers.forEach((specifier) => {
272
+ if (specifier.type === AST_NODE_TYPES.ImportSpecifier && specifier.imported.type === AST_NODE_TYPES.Identifier && specifier.imported.name === "useInsider") aliases.add(specifier.local.name);
273
+ });
274
+ });
275
+ return aliases;
276
+ }
277
+ function callsAny(node, aliases) {
278
+ if (!node || typeof node !== "object") return false;
279
+ if (Array.isArray(node)) return node.some((child) => callsAny(child, aliases));
280
+ const candidate = node;
281
+ if (candidate.type === AST_NODE_TYPES.CallExpression) {
282
+ const { callee } = candidate;
283
+ if (callee.type === AST_NODE_TYPES.Identifier && aliases.has(callee.name)) return true;
284
+ }
285
+ return Object.entries(node).some(([key, value]) => key !== "parent" && callsAny(value, aliases));
286
+ }
287
+ /**
288
+ * Whether a flat-config source is still the removed v3 shape: it imports `useInsider` and calls it
289
+ * itself, instead of importing a renderer from a sibling `insider.lint.*` module. Both the plain
290
+ * `export default useInsider({...})` form and the spread form are recognized.
291
+ * @example
292
+ * isV3EslintConfigSource("import { useInsider } from '@useinsider/eslint-config';\nexport default useInsider({});");
293
+ * // true
294
+ */
295
+ function isV3EslintConfigSource(source) {
296
+ const program = parseProgram(source);
297
+ if (!program) return false;
298
+ const aliases = collectUseInsiderAliases(program);
299
+ if (aliases.size === 0) return false;
300
+ return program.body.some((node) => callsAny(node, aliases));
301
+ }
302
+ function classify(lintModule, eslintConfig, oxlintConfig, eslintConfigIsV3) {
303
+ if (lintModule && eslintConfig && oxlintConfig) return "complete";
304
+ if (!lintModule && !eslintConfig && !oxlintConfig) return "noConfig";
305
+ if (!lintModule && eslintConfig && eslintConfigIsV3) return "unmigratedV3";
306
+ return "partial";
307
+ }
308
+ /**
309
+ * What of the three-file layout is on disk in a directory, and which of the four guidance branches
310
+ * applies. `unmigratedV3` is the only branch the in-lint migration offer acts on, because it is the
311
+ * only one {@link import('../cli/migrate').runMigrate} can actually rewrite.
312
+ * @example
313
+ * diagnoseLayout('/repo').kind; // 'unmigratedV3' when eslint.config.js still calls useInsider()
314
+ */
315
+ function diagnoseLayout(directory, options = {}) {
316
+ const fileExists = options.fileExists ?? existsSync;
317
+ const readSource = options.readSource ?? ((path) => readFileSync(path, "utf8"));
318
+ const lintModule = findSibling(directory, LINT_MODULE_FILENAMES, fileExists);
319
+ const eslintConfig = findSibling(directory, ESLINT_CONFIG_FILENAMES, fileExists);
320
+ const oxlintConfig = findSibling(directory, OXLINT_CONFIG_FILENAMES, fileExists);
321
+ let eslintConfigIsV3 = false;
322
+ if (eslintConfig && !lintModule) try {
323
+ eslintConfigIsV3 = isV3EslintConfigSource(readSource(join(directory, eslintConfig)));
324
+ } catch {
325
+ eslintConfigIsV3 = false;
326
+ }
327
+ const kind = classify(lintModule, eslintConfig, oxlintConfig, eslintConfigIsV3);
328
+ return {
329
+ kind,
330
+ directory,
331
+ lintModule,
332
+ eslintConfig,
333
+ oxlintConfig,
334
+ missing: [
335
+ lintModule ? null : "insider.lint.ts",
336
+ eslintConfig ? null : "eslint.config.ts",
337
+ oxlintConfig ? null : "oxlint.config.ts"
338
+ ].filter((name) => name !== null),
339
+ autoMigratable: kind === "unmigratedV3"
340
+ };
341
+ }
342
+ /**
343
+ * Import specifier for the consumer's lint module, naming the extension that is actually on disk so
344
+ * the suggested snippet resolves as printed.
345
+ * @example
346
+ * lintModuleSpecifier('/repo'); // './insider.lint.mts' when that is the file present
347
+ */
348
+ function lintModuleSpecifier(directory, fileExists = existsSync) {
349
+ return `./${findSibling(directory, LINT_MODULE_FILENAMES, fileExists) ?? "insider.lint.ts"}`;
350
+ }
351
+ function inventoryLines(diagnosis) {
352
+ const v3Note = diagnosis.kind === "unmigratedV3" ? " (still calls `useInsider()` itself)" : "";
353
+ return [
354
+ diagnosis.lintModule ? ` present: ${diagnosis.lintModule}` : " MISSING: insider.lint.{ts,mts}",
355
+ diagnosis.eslintConfig ? ` present: ${diagnosis.eslintConfig}${v3Note}` : " MISSING: eslint.config.{ts,mts}",
356
+ diagnosis.oxlintConfig ? ` present: ${diagnosis.oxlintConfig}` : ` MISSING: ${OXLINT_CONFIG_FILENAMES.join(" or ")}`
357
+ ];
358
+ }
359
+ function guidanceBody(diagnosis) {
360
+ if (diagnosis.kind === "unmigratedV3") return [
361
+ "`useInsider()` no longer returns a flat-config array, so the config it is called from has",
362
+ "to become a module both linters can import. The migration does that rewrite for you:",
363
+ "",
364
+ ` ${MIGRATE_COMMAND}`
365
+ ];
366
+ if (diagnosis.kind === "noConfig") return [
367
+ "There is no linter configuration here yet. Scaffold the three-file layout:",
368
+ "",
369
+ ` ${SCAFFOLD_COMMAND}`
370
+ ];
371
+ return [
372
+ `The layout is incomplete — missing ${diagnosis.missing.join(", ")}.`,
373
+ "Add the missing file, or let the migration regenerate the whole layout:",
374
+ "",
375
+ ` ${MIGRATE_COMMAND}`
376
+ ];
377
+ }
378
+ function detailLines(detail) {
379
+ if (!detail || detail.length === 0) return [];
380
+ return ["", ...detail];
381
+ }
382
+ /**
383
+ * The consumer-facing failure text: a site-specific headline, then the branch that matches what is
384
+ * on disk, then the layout dump, then the bypass. The command to run always comes before the dump,
385
+ * because a consumer who reads only the first screen still has to be told about `migrate`.
386
+ * @example
387
+ * formatLayoutGuidance({ headline: 'no `insider.lint.*` module found in /repo.', diagnosis });
388
+ */
389
+ function formatLayoutGuidance(input) {
390
+ return [
391
+ `@useinsider/eslint-config: ${input.headline}`,
392
+ "",
393
+ ...inventoryLines(input.diagnosis),
394
+ "",
395
+ ...guidanceBody(input.diagnosis),
396
+ ...detailLines(input.detail),
397
+ "",
398
+ EXPECTED_LAYOUT,
399
+ "",
400
+ `Emergency bypass: set ${OPT_OUT_ENV}=true (do not commit).`
401
+ ].join("\n");
402
+ }
403
+ //#endregion
404
+ //#region src/utils/offerMigration.ts
405
+ var PACKAGE_NAME = "@useinsider/eslint-config";
406
+ var CLI_RELATIVE_PATH = "dist/cli.cjs";
407
+ var NO_PROMPT_ENV = "INSIDER_NO_PROMPT";
408
+ var IN_FLIGHT_ENV = "INSIDER_MIGRATION_IN_PROGRESS";
409
+ var MIGRATE_FROM_LINT_ARGS = ["migrate", "--from-lint"];
410
+ var BINARY_EXTENSIONS = /\.[mc]?js$/u;
411
+ function binaryName(argument) {
412
+ return basename(argument).replace(BINARY_EXTENSIONS, "");
413
+ }
414
+ /**
415
+ * Which linter is hosting the current process. oxlint evaluates `oxlint.config.ts` inside the Node
416
+ * process that runs its own `bin/oxlint` wrapper, so `useInsider()` genuinely executes under both
417
+ * binaries and the two have to be told apart before anything is written to stdout.
418
+ * @example
419
+ * detectLintHost(['node', '/app/node_modules/oxlint/bin/oxlint', '.']); // 'oxlint'
420
+ */
421
+ function detectLintHost(argv = process.argv) {
422
+ const names = argv.slice(1).map(binaryName);
423
+ if (names.includes("oxlint")) return "oxlint";
424
+ if (names.includes("eslint")) return "eslint";
425
+ return "unknown";
426
+ }
427
+ function defaultInteractive() {
428
+ return Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
429
+ }
430
+ function skipReasonFor(env, argv, interactive) {
431
+ if (env[IN_FLIGHT_ENV] === "true") return "already-offered";
432
+ if (env.VITEST || env.NODE_ENV === "test") return "running-under-test";
433
+ if (env["INSIDER_SKIP_CONFIG_VALIDATION"] === "true" || env[NO_PROMPT_ENV] === "true") return "suppressed";
434
+ if (env.CI) return "running-in-ci";
435
+ if (detectLintHost(argv) === "oxlint") return "host-is-oxlint";
436
+ if (!interactive) return "no-tty";
437
+ return null;
438
+ }
439
+ function manifestNameAt(directory) {
440
+ const manifestPath = join(directory, "package.json");
441
+ if (!existsSync(manifestPath)) return null;
442
+ try {
443
+ return JSON.parse(readFileSync(manifestPath, "utf8")).name ?? null;
444
+ } catch {
445
+ return null;
446
+ }
447
+ }
448
+ function packageRootFrom(startDir) {
449
+ let current = startDir;
450
+ for (let depth = 0; depth < 10; depth += 1) {
451
+ if (manifestNameAt(current) === PACKAGE_NAME) return current;
452
+ const parent = dirname(current);
453
+ if (parent === current) return null;
454
+ current = parent;
455
+ }
456
+ return null;
457
+ }
458
+ /**
459
+ * Absolute path to this package's own built CLI, or `null` when it is not on disk. Resolved by
460
+ * walking up from `startDir` — this module's own directory by default — rather than through
461
+ * `require.resolve`, so it works from `dist/` and from `src/` alike.
462
+ * @example
463
+ * resolveCliEntry(); // '/app/node_modules/@useinsider/eslint-config/dist/cli.cjs'
464
+ */
465
+ function resolveCliEntry(startDir) {
466
+ const root = packageRootFrom(startDir ?? dirname(fileURLToPath(import.meta.url)));
467
+ if (!root) return null;
468
+ const entry = resolve(root, CLI_RELATIVE_PATH);
469
+ return existsSync(entry) ? entry : null;
470
+ }
471
+ function defaultRunCli(entry, args, cwd, env) {
472
+ const result = spawnSync(process.execPath, [entry, ...args], {
473
+ cwd,
474
+ env,
475
+ stdio: "inherit"
476
+ });
477
+ if (result.error) return 1;
478
+ return result.status ?? 1;
479
+ }
480
+ function defaultWarn(message) {
481
+ console.error(message);
482
+ }
483
+ function outdatedNotice(diagnosis) {
484
+ const config = diagnosis.eslintConfig ?? "eslint.config.*";
485
+ return [
486
+ `@useinsider/eslint-config: ${config} still uses the v3 single-file layout.`,
487
+ ` ${join(diagnosis.directory, config)} calls \`useInsider()\` itself, and`,
488
+ ` ${diagnosis.missing.join(" and ")} ${diagnosis.missing.length === 1 ? "is" : "are"} missing.`,
489
+ "",
490
+ "Starting the guided migration. It shows every change before applying it.",
491
+ `Skip it with ${NO_PROMPT_ENV}=true.`,
492
+ ""
493
+ ].join("\n");
494
+ }
495
+ var offerAttempted = false;
496
+ function withDefaults(deps) {
497
+ return {
498
+ env: deps.env ?? process.env,
499
+ argv: deps.argv ?? process.argv,
500
+ interactive: deps.interactive ?? defaultInteractive(),
501
+ resolveCliEntry: deps.resolveCliEntry ?? resolveCliEntry,
502
+ runCli: deps.runCli ?? defaultRunCli,
503
+ warn: deps.warn ?? defaultWarn
504
+ };
505
+ }
506
+ /**
507
+ * Offers the guided migration for an outdated layout and runs it when the consumer accepts. Declines
508
+ * silently — returning a skip reason instead of prompting — whenever writing to stdout would be
509
+ * wrong: under oxlint, in CI, without a TTY, or when already inside a migration.
510
+ * @example
511
+ * offerMigration(diagnoseLayout('/repo')).accepted; // true after the consumer confirmed
512
+ */
513
+ function offerMigration(diagnosis, deps = {}) {
514
+ const resolved = withDefaults(deps);
515
+ if (offerAttempted) return {
516
+ offered: false,
517
+ skipReason: "already-offered"
518
+ };
519
+ const skipReason = skipReasonFor(resolved.env, resolved.argv, resolved.interactive);
520
+ if (skipReason) return {
521
+ offered: false,
522
+ skipReason
523
+ };
524
+ const entry = resolved.resolveCliEntry();
525
+ if (!entry) return {
526
+ offered: false,
527
+ skipReason: "cli-not-found"
528
+ };
529
+ offerAttempted = true;
530
+ resolved.warn(outdatedNotice(diagnosis));
531
+ return {
532
+ offered: true,
533
+ accepted: resolved.runCli(entry, MIGRATE_FROM_LINT_ARGS, diagnosis.directory, {
534
+ ...resolved.env,
535
+ [IN_FLIGHT_ENV]: "true"
536
+ }) === 0
537
+ };
538
+ }
539
+ //#endregion
540
+ //#region src/utils/ensureConfigStructure.ts
541
+ var ANCHOR_FILE_REGEX = /(?:insider\.lint|eslint\.config)\.[mc]?[jt]s(?:$|\?)/;
542
+ var FILE_URL_PREFIX = "file://";
72
543
  var bypassWarnEmitted = false;
73
544
  function computeSelfModulePath() {
74
545
  const raw = fileURLToPath(import.meta.url);
@@ -83,7 +554,7 @@ function isPathSpecifier(source) {
83
554
  return source.startsWith(".") || source.startsWith("/");
84
555
  }
85
556
  function defaultIsInsiderImportSource(source, configPath) {
86
- if (source === INSIDER_IMPORT_SOURCE) return true;
557
+ if (source === "@useinsider/eslint-config") return true;
87
558
  if (!configPath || !isPathSpecifier(source)) return false;
88
559
  try {
89
560
  const absolute = resolve(dirname(configPath), source);
@@ -98,24 +569,24 @@ function locOf(node) {
98
569
  column: node.loc.start.column
99
570
  };
100
571
  }
101
- function validateCallExpression(callExpr) {
572
+ function validateCallExpression(callExpr, expectedCallee) {
102
573
  const { callee } = callExpr;
103
- if (callee.type !== AST_NODE_TYPES.Identifier || callee.name !== TARGET_CALLEE_NAME) return [{
574
+ if (callee.type !== AST_NODE_TYPES.Identifier || callee.name !== expectedCallee) return [{
104
575
  ...locOf(callExpr),
105
- reason: "Default export must call `useInsider(...)` directly."
576
+ reason: `Default export must call \`${expectedCallee}(...)\` directly.`
106
577
  }];
107
578
  return [];
108
579
  }
109
- function validateExportedCall(callee, fallbackLoc) {
580
+ function validateExportedCall(callee, fallbackLoc, expectedCallee) {
110
581
  let target = callee;
111
582
  if (target.type === AST_NODE_TYPES.AwaitExpression) target = target.argument;
112
- if (target.type === AST_NODE_TYPES.CallExpression) return validateCallExpression(target);
583
+ if (target.type === AST_NODE_TYPES.CallExpression) return validateCallExpression(target, expectedCallee);
113
584
  if (target.type === AST_NODE_TYPES.ArrayExpression) return [{
114
585
  ...locOf(target),
115
- reason: "Default export must not wrap `useInsider(...)` in an array. Use the typed options object instead."
586
+ reason: `Default export must not wrap \`${expectedCallee}(...)\` in an array.`
116
587
  }];
117
588
  if (target.type === AST_NODE_TYPES.LogicalExpression || target.type === AST_NODE_TYPES.ConditionalExpression) {
118
- const reason = ["Default export must not gate `useInsider(...)` behind a logical/conditional expression.", "Use the typed options object instead."].join(" ");
589
+ const reason = [`Default export must not gate \`${expectedCallee}(...)\` behind a logical/conditional expression.`, "Use the typed options object instead."].join(" ");
119
590
  return [{
120
591
  ...locOf(target),
121
592
  reason
@@ -123,7 +594,7 @@ function validateExportedCall(callee, fallbackLoc) {
123
594
  }
124
595
  return [{
125
596
  ...fallbackLoc,
126
- reason: "Default export must be a direct call to `useInsider(...)`."
597
+ reason: `Default export must be a direct call to \`${expectedCallee}(...)\`.`
127
598
  }];
128
599
  }
129
600
  function collectInsiderBindings(node, bindings, isInsiderSource) {
@@ -144,7 +615,7 @@ function classifyExpressionStatement(node, insiderBindings) {
144
615
  }
145
616
  };
146
617
  }
147
- function classifyStatement(node, exportSeen, insiderBindings) {
618
+ function classifyStatement(node, exportSeen, insiderBindings, expectedCallee) {
148
619
  if (node.type === AST_NODE_TYPES.ImportDeclaration || node.type === AST_NODE_TYPES.VariableDeclaration) return { type: "ignore" };
149
620
  if (node.type === AST_NODE_TYPES.ExpressionStatement) return classifyExpressionStatement(node, insiderBindings);
150
621
  if (node.type === AST_NODE_TYPES.ExportDefaultDeclaration) {
@@ -158,13 +629,13 @@ function classifyStatement(node, exportSeen, insiderBindings) {
158
629
  const declaration = node.declaration;
159
630
  return {
160
631
  type: "export",
161
- violations: validateExportedCall(declaration, locOf(node))
632
+ violations: validateExportedCall(declaration, locOf(node), expectedCallee)
162
633
  };
163
634
  }
164
635
  const reason = [
165
636
  `Disallowed top-level statement \`${node.type}\`.`,
166
637
  "Only imports, variable declarations, helper calls from `@useinsider/eslint-config`,",
167
- "and one `export default useInsider(...)` are allowed."
638
+ `and one \`export default ${expectedCallee}()\` are allowed.`
168
639
  ].join(" ");
169
640
  return {
170
641
  type: "disallowed",
@@ -179,18 +650,112 @@ function validateProgramStructure(program, configPath = "", options = {}) {
179
650
  const insiderBindings = /* @__PURE__ */ new Set();
180
651
  const resolver = options.isInsiderImportSource ?? defaultIsInsiderImportSource;
181
652
  const isInsiderSource = (source) => resolver(source, configPath);
653
+ const expectedCallee = options.expectedCallee ?? "useInsiderESLint";
182
654
  let exportSeen = false;
183
655
  program.body.forEach((node) => {
184
656
  if (node.type === AST_NODE_TYPES.ImportDeclaration) collectInsiderBindings(node, insiderBindings, isInsiderSource);
185
- const result = classifyStatement(node, exportSeen, insiderBindings);
657
+ const result = classifyStatement(node, exportSeen, insiderBindings, expectedCallee);
186
658
  if (result.type === "ignore") return;
187
659
  if (result.type === "export") {
188
660
  exportSeen = true;
189
661
  violations.push(...result.violations);
190
662
  return;
191
663
  }
192
- if (result.type === "duplicateExport") {
193
- violations.push(result.violation);
664
+ violations.push(result.violation);
665
+ });
666
+ if (!exportSeen) violations.push({
667
+ line: 1,
668
+ column: 0,
669
+ reason: `Missing \`export default ${options.expectedCallee ?? "useInsiderESLint"}()\`.`
670
+ });
671
+ return violations;
672
+ }
673
+ function validateRendererPattern(pattern) {
674
+ const bound = /* @__PURE__ */ new Set();
675
+ const violations = [];
676
+ pattern.properties.forEach((property) => {
677
+ if (property.type !== AST_NODE_TYPES.Property || property.computed || !property.shorthand) {
678
+ violations.push({
679
+ ...locOf(property),
680
+ reason: `Destructure \`${ESLINT_RENDERER_NAME}\` and \`${OXC_RENDERER_NAME}\` by name, without renaming or spreading.`
681
+ });
682
+ return;
683
+ }
684
+ bound.add(property.key.name);
685
+ });
686
+ [ESLINT_RENDERER_NAME, OXC_RENDERER_NAME].forEach((name) => {
687
+ if (!bound.has(name)) violations.push({
688
+ ...locOf(pattern),
689
+ reason: `Missing \`${name}\` in the destructured \`${TARGET_CALLEE_NAME}(...)\` result.`
690
+ });
691
+ });
692
+ return violations;
693
+ }
694
+ function validateRendererExport(node) {
695
+ const { declaration } = node;
696
+ if (!declaration || declaration.type !== AST_NODE_TYPES.VariableDeclaration || declaration.kind !== "const") return [{
697
+ ...locOf(node),
698
+ reason: `Renderer export must be \`${RENDERER_EXPORT};\`.`
699
+ }];
700
+ if (declaration.declarations.length !== 1) return [{
701
+ ...locOf(declaration),
702
+ reason: "Renderer export must declare exactly one binding."
703
+ }];
704
+ const [declarator] = declaration.declarations;
705
+ if (declarator.id.type !== AST_NODE_TYPES.ObjectPattern) return [{
706
+ ...locOf(declarator.id),
707
+ reason: `Renderer export must destructure \`${ESLINT_RENDERER_NAME}\` and \`${OXC_RENDERER_NAME}\`.`
708
+ }];
709
+ const patternViolations = validateRendererPattern(declarator.id);
710
+ const { init } = declarator;
711
+ if (!init) return [...patternViolations, {
712
+ ...locOf(declarator),
713
+ reason: `Renderer export must be initialized by a direct \`${TARGET_CALLEE_NAME}(...)\` call.`
714
+ }];
715
+ return [...patternViolations, ...validateExportedCall(init, locOf(declarator), TARGET_CALLEE_NAME)];
716
+ }
717
+ function classifyLintModuleStatement(node, exportSeen, insiderBindings) {
718
+ if (node.type === AST_NODE_TYPES.ImportDeclaration || node.type === AST_NODE_TYPES.VariableDeclaration) return { type: "ignore" };
719
+ if (node.type === AST_NODE_TYPES.ExpressionStatement) return classifyExpressionStatement(node, insiderBindings);
720
+ if (node.type === AST_NODE_TYPES.ExportNamedDeclaration) {
721
+ if (exportSeen) return {
722
+ type: "duplicateExport",
723
+ violation: {
724
+ ...locOf(node),
725
+ reason: "Only one export is allowed."
726
+ }
727
+ };
728
+ return {
729
+ type: "export",
730
+ violations: validateRendererExport(node)
731
+ };
732
+ }
733
+ const reason = [
734
+ `Disallowed top-level statement \`${node.type}\`.`,
735
+ "Only imports, variable declarations, helper calls from `@useinsider/eslint-config`,",
736
+ `and one \`${RENDERER_EXPORT}\` are allowed.`
737
+ ].join(" ");
738
+ return {
739
+ type: "disallowed",
740
+ violation: {
741
+ ...locOf(node),
742
+ reason
743
+ }
744
+ };
745
+ }
746
+ function validateLintModuleStructure(program, configPath = "", options = {}) {
747
+ const violations = [];
748
+ const insiderBindings = /* @__PURE__ */ new Set();
749
+ const resolver = options.isInsiderImportSource ?? defaultIsInsiderImportSource;
750
+ const isInsiderSource = (source) => resolver(source, configPath);
751
+ let exportSeen = false;
752
+ program.body.forEach((node) => {
753
+ if (node.type === AST_NODE_TYPES.ImportDeclaration) collectInsiderBindings(node, insiderBindings, isInsiderSource);
754
+ const result = classifyLintModuleStatement(node, exportSeen, insiderBindings);
755
+ if (result.type === "ignore") return;
756
+ if (result.type === "export") {
757
+ exportSeen = true;
758
+ violations.push(...result.violations);
194
759
  return;
195
760
  }
196
761
  violations.push(result.violation);
@@ -198,7 +763,7 @@ function validateProgramStructure(program, configPath = "", options = {}) {
198
763
  if (!exportSeen) violations.push({
199
764
  line: 1,
200
765
  column: 0,
201
- reason: "Missing `export default useInsider(...)`."
766
+ reason: `Missing \`${RENDERER_EXPORT};\`.`
202
767
  });
203
768
  return violations;
204
769
  }
@@ -222,7 +787,7 @@ function extractMatchesFromStack(frames) {
222
787
  const matches = /* @__PURE__ */ new Set();
223
788
  frames.forEach((frame) => {
224
789
  const name = normalizeFrameFilename(frame.getFileName());
225
- if (name && CONFIG_FILE_REGEX.test(name)) matches.add(name);
790
+ if (name && ANCHOR_FILE_REGEX.test(name)) matches.add(name);
226
791
  });
227
792
  return Array.from(matches);
228
793
  }
@@ -235,13 +800,16 @@ function argvCandidateAt(argv, index) {
235
800
  function extractConfigFromArgv(argv) {
236
801
  for (let i = 0; i < argv.length; i += 1) {
237
802
  const candidate = argvCandidateAt(argv, i);
238
- if (candidate && CONFIG_FILE_REGEX.test(candidate)) return candidate;
803
+ if (candidate && ANCHOR_FILE_REGEX.test(candidate)) return candidate;
239
804
  }
240
805
  return null;
241
806
  }
807
+ var LINT_MODULE_REGEX = /insider\.lint\.[mc]?[jt]s(?:$|\?)/;
242
808
  function getConsumerConfigPath(frames = readStackFrames(), argv = process.argv) {
243
809
  const stackMatches = extractMatchesFromStack(frames);
244
810
  if (stackMatches.length === 1) return stackMatches[0];
811
+ const lintModules = stackMatches.filter((match) => LINT_MODULE_REGEX.test(match));
812
+ if (lintModules.length === 1) return lintModules[0];
245
813
  const fromArgv = extractConfigFromArgv(argv);
246
814
  if (fromArgv) return fromArgv;
247
815
  return null;
@@ -251,11 +819,12 @@ function captureConsumerConfigDir(configPath) {
251
819
  consumerConfigDir = configPath === null ? null : dirname(resolve(configPath));
252
820
  }
253
821
  /**
254
- * Directory holding the consumer's `eslint.config.*`, captured by {@link ensureConfigStructure}.
255
- * This is the anchor `tsconfigPaths` entries are written against; it falls back to `process.cwd()`
256
- * when the config file could not be located.
822
+ * Directory holding the consumer's `insider.lint.*` module and its sibling linter configs, captured
823
+ * by {@link ensureConfigStructure}. This is the anchor `tsconfigPaths` entries are written against,
824
+ * and where {@link import('./ensureOxlintConfig').ensureOxlintConfig} looks for the oxlint config;
825
+ * it falls back to `process.cwd()` when the module could not be located.
257
826
  * @example
258
- * // consumer config at /repo/eslint.config.mjs, `eslint` launched from /repo/app
827
+ * // consumer module at /repo/insider.lint.ts, `eslint` launched from /repo/app
259
828
  * getConsumerConfigDir(); // '/repo'
260
829
  */
261
830
  function getConsumerConfigDir() {
@@ -266,12 +835,12 @@ function formatReport(configPath, violations) {
266
835
  const lines = violations.map((violation) => ` - [${violation.line}:${violation.column}] ${violation.reason}`);
267
836
  const footer = [
268
837
  "",
269
- "Allowed structure:",
270
- " - import statements",
271
- " - const/let/var declarations",
272
- " - exactly one `export default useInsider({ ... })`",
838
+ "Alongside the export, only import statements, const/let/var declarations and helper calls",
839
+ "imported from `@useinsider/eslint-config` are allowed.",
840
+ "",
841
+ EXPECTED_LAYOUT,
273
842
  "",
274
- "Emergency bypass: set INSIDER_SKIP_CONFIG_VALIDATION=true (do not commit)."
843
+ `Emergency bypass: set ${OPT_OUT_ENV}=true (do not commit).`
275
844
  ].join("\n");
276
845
  return `${header}\n${lines.join("\n")}\n${footer}`;
277
846
  }
@@ -312,21 +881,57 @@ function parseConsumerSource(configPath, source) {
312
881
  return fail(`@useinsider/eslint-config: failed to parse consumer config at ${configPath}: ${message}`);
313
882
  }
314
883
  }
315
- function runValidation(configPath) {
316
- if (!configPath) fail(["@useinsider/eslint-config: unable to locate consumer `eslint.config.*` file for structure validation.", "If this is an unexpected environment, set INSIDER_SKIP_CONFIG_VALIDATION=true (do not commit)."].join("\n"));
317
- const violations = validateProgramStructure(parseConsumerSource(configPath, readConsumerSource(configPath)), configPath);
884
+ function resolveSibling(directory, filenames) {
885
+ const found = filenames.find((filename) => existsSync(resolve(directory, filename)));
886
+ return found ? resolve(directory, found) : null;
887
+ }
888
+ function validateFile(filePath, validator, options) {
889
+ const violations = validator(parseConsumerSource(filePath, readConsumerSource(filePath)), filePath, options);
318
890
  if (violations.length === 0) return;
319
- fail(formatReport(configPath, violations));
891
+ fail(formatReport(filePath, violations));
320
892
  }
321
- function ensureConfigStructure(overridePath) {
893
+ var MIGRATED_EXIT_NOTICE = [
894
+ "@useinsider/eslint-config: migration finished.",
895
+ "",
896
+ "Re-run your lint command. This process already loaded the old configuration, so continuing now",
897
+ "would lint against a config that no longer exists on disk."
898
+ ].join("\n");
899
+ var DECLINED_DETAIL = ["The guided migration was not completed, so nothing was written."];
900
+ function failWithGuidance(headline, diagnosis, detail) {
901
+ return fail(formatLayoutGuidance({
902
+ headline,
903
+ diagnosis,
904
+ detail
905
+ }));
906
+ }
907
+ function handleOutdatedLayout(headline, diagnosis, offer) {
908
+ if (!diagnosis.autoMigratable) return failWithGuidance(headline, diagnosis);
909
+ const outcome = offer(diagnosis);
910
+ if (!outcome.offered) return failWithGuidance(headline, diagnosis);
911
+ if (!outcome.accepted) return failWithGuidance(headline, diagnosis, DECLINED_DETAIL);
912
+ return fail(MIGRATED_EXIT_NOTICE);
913
+ }
914
+ function runValidation(anchorPath, deps = {}) {
915
+ const diagnose = deps.diagnose ?? ((directory) => diagnoseLayout(directory));
916
+ const offer = deps.offer ?? offerMigration;
917
+ if (!anchorPath) handleOutdatedLayout("unable to locate the consumer `insider.lint.*` module for structure validation.", diagnose(process.cwd()), offer);
918
+ const directory = dirname(resolve(anchorPath));
919
+ const lintModulePath = resolveSibling(directory, LINT_MODULE_FILENAMES);
920
+ if (!lintModulePath) handleOutdatedLayout(`no \`insider.lint.*\` module found in ${directory}.`, diagnose(directory), offer);
921
+ validateFile(lintModulePath, validateLintModuleStructure, {});
922
+ const eslintConfigPath = resolveSibling(directory, ESLINT_CONFIG_FILENAMES);
923
+ if (!eslintConfigPath) failWithGuidance(`no \`eslint.config.*\` file found in ${directory}.`, diagnose(directory), [`\`${TARGET_CALLEE_NAME}()\` returns renderers, not a flat-config array, so ESLint still`, `needs a config file whose default export is \`${ESLINT_RENDERER_NAME}()\`.`]);
924
+ validateFile(eslintConfigPath, validateProgramStructure, { expectedCallee: ESLINT_RENDERER_NAME });
925
+ }
926
+ function ensureConfigStructure(overridePath, deps = {}) {
322
927
  const configPath = overridePath === void 0 ? getConsumerConfigPath() : overridePath;
323
928
  captureConsumerConfigDir(configPath);
324
- if (process.env[OPT_OUT_ENV] === "true") {
929
+ if (process.env["INSIDER_SKIP_CONFIG_VALIDATION"] === "true") {
325
930
  emitBypassWarning();
326
931
  return;
327
932
  }
328
933
  try {
329
- runValidation(configPath);
934
+ runValidation(configPath, deps);
330
935
  } catch (error) {
331
936
  if (error instanceof ConfigStructureExit) return;
332
937
  throw error;
@@ -450,6 +1055,14 @@ function clonePreset(preset) {
450
1055
  function isScopeName(key) {
451
1056
  return Object.keys(scopeMap).includes(key);
452
1057
  }
1058
+ /**
1059
+ * Every scope name `useInsider()` accepts, for error messages that have to spell the options out.
1060
+ * @example
1061
+ * scopeNames().includes('typescriptNode'); // true
1062
+ */
1063
+ function scopeNames() {
1064
+ return Object.keys(scopeMap);
1065
+ }
453
1066
  function applyCommonEntryFields(block, entry) {
454
1067
  if (entry.ignores && entry.ignores.length > 0) block.ignores = [...entry.ignores];
455
1068
  if (entry.globals) {
@@ -465,7 +1078,7 @@ async function buildScopeBlocks(scope, entries) {
465
1078
  const preset = (await scopeMap[scope]()).default;
466
1079
  return entries.map((entry) => {
467
1080
  const block = clonePreset(preset);
468
- block.files = entry.files;
1081
+ block.files = [...entry.files];
469
1082
  if (tsScopes.has(scope)) {
470
1083
  const { tsconfigPaths } = entry;
471
1084
  if (tsconfigPaths) applyTypescriptOverrides(block, { tsconfigPaths });
@@ -499,7 +1112,7 @@ async function buildStrictBlocks(value) {
499
1112
  const { default: strictConfig } = await import("./strict-BHw_cKrv.js");
500
1113
  const clone = { ...strictConfig };
501
1114
  if (clone.rules) clone.rules = { ...clone.rules };
502
- if (isFileScoped) clone.files = value;
1115
+ if (isFileScoped) clone.files = [...value];
503
1116
  else delete clone.files;
504
1117
  return [clone];
505
1118
  }
@@ -508,7 +1121,11 @@ async function buildStrictBlocks(value) {
508
1121
  var turnOff = false;
509
1122
  var scanned = false;
510
1123
  var silencedDependencies = /* @__PURE__ */ new Set();
511
- var fixedDependencies = ["eslint", "globals"];
1124
+ var fixedDependencies = [
1125
+ "eslint",
1126
+ "globals",
1127
+ "oxlint"
1128
+ ];
512
1129
  var require = createRequire(import.meta.url);
513
1130
  function getPackageJSONPath(currentDir) {
514
1131
  const fileLocation = resolve(currentDir, "package.json");
@@ -564,6 +1181,42 @@ function silenceDependencyWarning(input) {
564
1181
  input.forEach((dependency) => silencedDependencies.add(String(dependency)));
565
1182
  }
566
1183
  //#endregion
1184
+ //#region src/utils/ensureOxlintConfig.ts
1185
+ function formatMessage(directory) {
1186
+ return formatLayoutGuidance({
1187
+ headline: `no oxlint config found in ${directory}.`,
1188
+ diagnosis: diagnoseLayout(directory),
1189
+ detail: [
1190
+ "useInsiderESLint() turns off every rule oxlint owns, so running ESLint alone would silently",
1191
+ "drop those checks. Create the sibling oxlint config:",
1192
+ "",
1193
+ " // oxlint.config.ts",
1194
+ ` import { useInsiderOXC } from '${lintModuleSpecifier(directory)}';`,
1195
+ "",
1196
+ " export default await useInsiderOXC();",
1197
+ "",
1198
+ "and run both linters:",
1199
+ "",
1200
+ " \"lint\": \"oxlint --disable-nested-config && eslint .\"",
1201
+ "",
1202
+ `oxlint discovers only ${OXLINT_CONFIG_FILENAMES.join(" and ")} — any other extension is`,
1203
+ "ignored without a word."
1204
+ ]
1205
+ });
1206
+ }
1207
+ /**
1208
+ * Fails when the consumer enabled the ESLint-side oxlint suppression without a sibling oxlint
1209
+ * config to run the suppressed rules. Resolved against the directory
1210
+ * {@link getConsumerConfigDir} captured during `useInsider()`.
1211
+ * @example
1212
+ * ensureOxlintConfig(); // throws unless oxlint.config.{ts,mts} sits beside insider.lint.*
1213
+ */
1214
+ function ensureOxlintConfig(directory = getConsumerConfigDir()) {
1215
+ if (process.env["INSIDER_SKIP_CONFIG_VALIDATION"] === "true") return;
1216
+ if (OXLINT_CONFIG_FILENAMES.some((filename) => existsSync(join(directory, filename)))) return;
1217
+ throw new Error(formatMessage(directory));
1218
+ }
1219
+ //#endregion
567
1220
  //#region src/utils/silenceRules.ts
568
1221
  var globallySilencedRules = /* @__PURE__ */ new Set();
569
1222
  /**
@@ -583,14 +1236,25 @@ function silenceRule(config) {
583
1236
  const [, ...restConfig] = Array.isArray(config) ? config : [];
584
1237
  return ["warn", ...restConfig];
585
1238
  }
1239
+ /**
1240
+ * Reports a silenced rule name that no emitted block declares, which is how a typo in
1241
+ * `silenceRules` stays visible. The caller passes the union of every block's rule keys: a single
1242
+ * block is not evidence, because the emission is many narrow blocks and a correctly spelled rule is
1243
+ * absent from nearly all of them.
1244
+ * @example
1245
+ * warnUnknownSilencedRules(['no-console'], ['no-unknown-rule']); // warns once
1246
+ */
1247
+ function warnUnknownSilencedRules(knownRules, silencedRules) {
1248
+ const known = new Set(knownRules);
1249
+ new Set(silencedRules).forEach((rule) => {
1250
+ if (known.has(rule)) return;
1251
+ warn(`Unknown rule "${rule}". Please remove it from the silenced rules list.`);
1252
+ });
1253
+ }
586
1254
  function filterSilencedRules(rules, silencedRules) {
587
1255
  let targetSilencedRules = silencedRules || globallySilencedRules;
588
1256
  if (Array.isArray(targetSilencedRules)) targetSilencedRules = new Set(targetSilencedRules);
589
1257
  if (!rules || !targetSilencedRules.size) return rules;
590
- if (silencedRules) targetSilencedRules.forEach((rule) => {
591
- if (rules[rule]) return;
592
- warn(`Unknown rule "${rule}". Please remove it from the silenced rules list.`);
593
- });
594
1258
  return Object.entries(rules).reduce((newRules, [key, value]) => {
595
1259
  if (value) newRules[key] = targetSilencedRules.has(key) && !isDisabled(value) ? silenceRule(value) : value;
596
1260
  return newRules;
@@ -613,6 +1277,11 @@ function assertNoRemovedKeys(config) {
613
1277
  const details = used.map((key) => ` - \`${key}\`: ${removedKeys.get(key)}`).join("\n");
614
1278
  throw new Error(`@useinsider/eslint-config: removed option(s) passed to useInsider():\n${details}`);
615
1279
  }
1280
+ function assertNoUnknownKeys(config) {
1281
+ const unknown = Object.keys(config).filter((key) => !reservedKeys.has(key) && !removedKeys.has(key) && !isScopeName(key));
1282
+ if (unknown.length === 0) return;
1283
+ throw new Error(`@useinsider/eslint-config: unknown option(s) passed to useInsider(): ${unknown.join(", ")}.\n Scopes: ${scopeNames().join(", ")}\n Other options: ${[...reservedKeys].join(", ")}`);
1284
+ }
616
1285
  function isProcessableScopeKey(key) {
617
1286
  return !reservedKeys.has(key) && isScopeName(key);
618
1287
  }
@@ -626,6 +1295,7 @@ async function collectScopeBlocks(config) {
626
1295
  }, Promise.resolve([]));
627
1296
  }
628
1297
  function applySilenceRules(blocks, perCallSilence) {
1298
+ if (perCallSilence) warnUnknownSilencedRules(blocks.flatMap((block) => Object.keys(block.rules ?? {})), perCallSilence);
629
1299
  blocks.forEach((block) => {
630
1300
  if (!block.rules) return;
631
1301
  block.rules = perCallSilence ? filterSilencedRules(block.rules, perCallSilence) : filterSilencedRules(block.rules);
@@ -635,28 +1305,43 @@ function buildIgnoresBlocks(ignores) {
635
1305
  if (!ignores || ignores.length === 0) return [];
636
1306
  return [{ ignores: [...ignores] }];
637
1307
  }
638
- async function useInsider(config) {
639
- assertNoRemovedKeys(config);
640
- ensureConfigStructure();
641
- await ensureDependencies();
642
- const ignoresBlocks = buildIgnoresBlocks(config.ignores);
1308
+ async function buildBaseBlocks(config) {
643
1309
  const scopeBlocks = await collectScopeBlocks(config);
644
- const jqueryBlocks = buildJqueryBlocks(config.jquery);
645
- const configFilesBlocks = buildConfigFilesBlocks(config.configFiles);
646
1310
  const strictBlocks = await buildStrictBlocks(config.strict);
647
- const spellCheckerBlocks = buildSpellCheckerBlocks(config.spellChecker);
648
- const blocks = [
649
- ...ignoresBlocks,
1311
+ return [
1312
+ ...buildIgnoresBlocks(config.ignores),
1313
+ ...buildUnusedDirectiveBlocks(),
650
1314
  ...scopeBlocks,
651
- ...jqueryBlocks,
652
- ...configFilesBlocks,
1315
+ ...buildJqueryBlocks(config.jquery),
1316
+ ...buildConfigFilesBlocks(config.configFiles),
653
1317
  ...strictBlocks,
654
- ...spellCheckerBlocks
1318
+ ...buildLintTopologyBlocks(),
1319
+ ...buildSpellCheckerBlocks(config.spellChecker)
655
1320
  ];
1321
+ }
1322
+ async function renderEslint(config) {
1323
+ await ensureDependencies();
1324
+ ensureOxlintConfig();
1325
+ const baseBlocks = await buildBaseBlocks(config);
1326
+ const blocks = [...baseBlocks, ...buildOxlintSuppressionBlocks(baseBlocks)];
656
1327
  applySilenceRules(blocks, config.silenceRules);
657
1328
  return defineConfig(blocks);
658
1329
  }
1330
+ async function renderOxlint(config) {
1331
+ const baseBlocks = await buildBaseBlocks(config);
1332
+ applySilenceRules(baseBlocks, config.silenceRules);
1333
+ return buildOxlintConfig(baseBlocks, config.ignores);
1334
+ }
1335
+ function useInsider(config) {
1336
+ assertNoRemovedKeys(config);
1337
+ assertNoUnknownKeys(config);
1338
+ ensureConfigStructure();
1339
+ return {
1340
+ useInsiderESLint: () => renderEslint(config),
1341
+ useInsiderOXC: () => renderOxlint(config)
1342
+ };
1343
+ }
659
1344
  //#endregion
660
- export { ensureConfigStructure, merge, mergeAll, silenceDependencyWarning, silenceRules, useInsider };
1345
+ export { ensureConfigStructure, ensureOxlintConfig, merge, mergeAll, silenceDependencyWarning, silenceRules, useInsider };
661
1346
 
662
1347
  //# sourceMappingURL=main.js.map