@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/dist/main.cjs CHANGED
@@ -10,6 +10,7 @@ let node_module = require("node:module");
10
10
  let node_path = require("node:path");
11
11
  let node_url = require("node:url");
12
12
  let _typescript_eslint_types = require("@typescript-eslint/types");
13
+ let node_child_process = require("node:child_process");
13
14
  let _antfu_install_pkg = require("@antfu/install-pkg");
14
15
  let semver = require("semver");
15
16
  semver = require_oxlintOwnership.__toESM(semver, 1);
@@ -212,14 +213,7 @@ function buildOxlintConfig(blocks, ignores) {
212
213
  };
213
214
  }
214
215
  //#endregion
215
- //#region src/utils/ensureConfigStructure.ts
216
- var ANCHOR_FILE_REGEX = /(?:insider\.lint|eslint\.config)\.[mc]?[jt]s(?:$|\?)/;
217
- var OPT_OUT_ENV$1 = "INSIDER_SKIP_CONFIG_VALIDATION";
218
- var FILE_URL_PREFIX = "file://";
219
- var TARGET_CALLEE_NAME = "useInsider";
220
- var ESLINT_RENDERER_NAME = "useInsiderESLint";
221
- var OXC_RENDERER_NAME = "useInsiderOXC";
222
- var INSIDER_IMPORT_SOURCE = "@useinsider/eslint-config";
216
+ //#region src/utils/layoutDiagnosis.ts
223
217
  var MODULE_EXTENSIONS = [
224
218
  "ts",
225
219
  "mts",
@@ -228,9 +222,327 @@ var MODULE_EXTENSIONS = [
228
222
  "mjs",
229
223
  "cjs"
230
224
  ];
231
- var RENDERER_EXPORT = `export const ${`{ ${ESLINT_RENDERER_NAME}, ${OXC_RENDERER_NAME} }`} = ${TARGET_CALLEE_NAME}({ ... })`;
232
225
  var LINT_MODULE_FILENAMES = MODULE_EXTENSIONS.map((ext) => `insider.lint.${ext}`);
233
226
  var ESLINT_CONFIG_FILENAMES = MODULE_EXTENSIONS.map((ext) => `eslint.config.${ext}`);
227
+ var OXLINT_CONFIG_FILENAMES = ["oxlint.config.ts", "oxlint.config.mts"];
228
+ var OPT_OUT_ENV = "INSIDER_SKIP_CONFIG_VALIDATION";
229
+ var MIGRATE_COMMAND = "npx @useinsider/eslint-config migrate";
230
+ var SCAFFOLD_COMMAND = "npx @useinsider/eslint-config";
231
+ var INSIDER_IMPORT_SOURCE = "@useinsider/eslint-config";
232
+ var TARGET_CALLEE_NAME = "useInsider";
233
+ var ESLINT_RENDERER_NAME = "useInsiderESLint";
234
+ var OXC_RENDERER_NAME = "useInsiderOXC";
235
+ var RENDERER_EXPORT = `export const ${`{ ${ESLINT_RENDERER_NAME}, ${OXC_RENDERER_NAME} }`} = ${TARGET_CALLEE_NAME}({ ... })`;
236
+ var EXPECTED_LAYOUT = [
237
+ "Expected layout (three sibling files):",
238
+ "",
239
+ " // insider.lint.ts",
240
+ ` import { ${TARGET_CALLEE_NAME} } from '${INSIDER_IMPORT_SOURCE}';`,
241
+ "",
242
+ ` ${RENDERER_EXPORT};`,
243
+ "",
244
+ " // eslint.config.ts",
245
+ ` import { ${ESLINT_RENDERER_NAME} } from './insider.lint.ts';`,
246
+ "",
247
+ ` export default ${ESLINT_RENDERER_NAME}();`,
248
+ "",
249
+ " // oxlint.config.ts",
250
+ ` import { ${OXC_RENDERER_NAME} } from './insider.lint.ts';`,
251
+ "",
252
+ ` export default await ${OXC_RENDERER_NAME}();`
253
+ ].join("\n");
254
+ function findSibling(directory, filenames, fileExists) {
255
+ return filenames.find((filename) => fileExists((0, node_path.join)(directory, filename))) ?? null;
256
+ }
257
+ var lazyRequire$1 = (0, node_module.createRequire)({}.url);
258
+ function parseProgram(source) {
259
+ try {
260
+ const { parse } = lazyRequire$1("@typescript-eslint/parser");
261
+ return parse(source, {
262
+ sourceType: "module",
263
+ range: true,
264
+ loc: true
265
+ });
266
+ } catch {
267
+ return null;
268
+ }
269
+ }
270
+ function collectUseInsiderAliases(program) {
271
+ const aliases = /* @__PURE__ */ new Set();
272
+ program.body.forEach((node) => {
273
+ if (node.type !== _typescript_eslint_types.AST_NODE_TYPES.ImportDeclaration) return;
274
+ node.specifiers.forEach((specifier) => {
275
+ if (specifier.type === _typescript_eslint_types.AST_NODE_TYPES.ImportSpecifier && specifier.imported.type === _typescript_eslint_types.AST_NODE_TYPES.Identifier && specifier.imported.name === "useInsider") aliases.add(specifier.local.name);
276
+ });
277
+ });
278
+ return aliases;
279
+ }
280
+ function callsAny(node, aliases) {
281
+ if (!node || typeof node !== "object") return false;
282
+ if (Array.isArray(node)) return node.some((child) => callsAny(child, aliases));
283
+ const candidate = node;
284
+ if (candidate.type === _typescript_eslint_types.AST_NODE_TYPES.CallExpression) {
285
+ const { callee } = candidate;
286
+ if (callee.type === _typescript_eslint_types.AST_NODE_TYPES.Identifier && aliases.has(callee.name)) return true;
287
+ }
288
+ return Object.entries(node).some(([key, value]) => key !== "parent" && callsAny(value, aliases));
289
+ }
290
+ /**
291
+ * Whether a flat-config source is still the removed v3 shape: it imports `useInsider` and calls it
292
+ * itself, instead of importing a renderer from a sibling `insider.lint.*` module. Both the plain
293
+ * `export default useInsider({...})` form and the spread form are recognized.
294
+ * @example
295
+ * isV3EslintConfigSource("import { useInsider } from '@useinsider/eslint-config';\nexport default useInsider({});");
296
+ * // true
297
+ */
298
+ function isV3EslintConfigSource(source) {
299
+ const program = parseProgram(source);
300
+ if (!program) return false;
301
+ const aliases = collectUseInsiderAliases(program);
302
+ if (aliases.size === 0) return false;
303
+ return program.body.some((node) => callsAny(node, aliases));
304
+ }
305
+ function classify(lintModule, eslintConfig, oxlintConfig, eslintConfigIsV3) {
306
+ if (lintModule && eslintConfig && oxlintConfig) return "complete";
307
+ if (!lintModule && !eslintConfig && !oxlintConfig) return "noConfig";
308
+ if (!lintModule && eslintConfig && eslintConfigIsV3) return "unmigratedV3";
309
+ return "partial";
310
+ }
311
+ /**
312
+ * What of the three-file layout is on disk in a directory, and which of the four guidance branches
313
+ * applies. `unmigratedV3` is the only branch the in-lint migration offer acts on, because it is the
314
+ * only one {@link import('../cli/migrate').runMigrate} can actually rewrite.
315
+ * @example
316
+ * diagnoseLayout('/repo').kind; // 'unmigratedV3' when eslint.config.js still calls useInsider()
317
+ */
318
+ function diagnoseLayout(directory, options = {}) {
319
+ const fileExists = options.fileExists ?? node_fs.existsSync;
320
+ const readSource = options.readSource ?? ((path) => (0, node_fs.readFileSync)(path, "utf8"));
321
+ const lintModule = findSibling(directory, LINT_MODULE_FILENAMES, fileExists);
322
+ const eslintConfig = findSibling(directory, ESLINT_CONFIG_FILENAMES, fileExists);
323
+ const oxlintConfig = findSibling(directory, OXLINT_CONFIG_FILENAMES, fileExists);
324
+ let eslintConfigIsV3 = false;
325
+ if (eslintConfig && !lintModule) try {
326
+ eslintConfigIsV3 = isV3EslintConfigSource(readSource((0, node_path.join)(directory, eslintConfig)));
327
+ } catch {
328
+ eslintConfigIsV3 = false;
329
+ }
330
+ const kind = classify(lintModule, eslintConfig, oxlintConfig, eslintConfigIsV3);
331
+ return {
332
+ kind,
333
+ directory,
334
+ lintModule,
335
+ eslintConfig,
336
+ oxlintConfig,
337
+ missing: [
338
+ lintModule ? null : "insider.lint.ts",
339
+ eslintConfig ? null : "eslint.config.ts",
340
+ oxlintConfig ? null : "oxlint.config.ts"
341
+ ].filter((name) => name !== null),
342
+ autoMigratable: kind === "unmigratedV3"
343
+ };
344
+ }
345
+ /**
346
+ * Import specifier for the consumer's lint module, naming the extension that is actually on disk so
347
+ * the suggested snippet resolves as printed.
348
+ * @example
349
+ * lintModuleSpecifier('/repo'); // './insider.lint.mts' when that is the file present
350
+ */
351
+ function lintModuleSpecifier(directory, fileExists = node_fs.existsSync) {
352
+ return `./${findSibling(directory, LINT_MODULE_FILENAMES, fileExists) ?? "insider.lint.ts"}`;
353
+ }
354
+ function inventoryLines(diagnosis) {
355
+ const v3Note = diagnosis.kind === "unmigratedV3" ? " (still calls `useInsider()` itself)" : "";
356
+ return [
357
+ diagnosis.lintModule ? ` present: ${diagnosis.lintModule}` : " MISSING: insider.lint.{ts,mts}",
358
+ diagnosis.eslintConfig ? ` present: ${diagnosis.eslintConfig}${v3Note}` : " MISSING: eslint.config.{ts,mts}",
359
+ diagnosis.oxlintConfig ? ` present: ${diagnosis.oxlintConfig}` : ` MISSING: ${OXLINT_CONFIG_FILENAMES.join(" or ")}`
360
+ ];
361
+ }
362
+ function guidanceBody(diagnosis) {
363
+ if (diagnosis.kind === "unmigratedV3") return [
364
+ "`useInsider()` no longer returns a flat-config array, so the config it is called from has",
365
+ "to become a module both linters can import. The migration does that rewrite for you:",
366
+ "",
367
+ ` ${MIGRATE_COMMAND}`
368
+ ];
369
+ if (diagnosis.kind === "noConfig") return [
370
+ "There is no linter configuration here yet. Scaffold the three-file layout:",
371
+ "",
372
+ ` ${SCAFFOLD_COMMAND}`
373
+ ];
374
+ return [
375
+ `The layout is incomplete — missing ${diagnosis.missing.join(", ")}.`,
376
+ "Add the missing file, or let the migration regenerate the whole layout:",
377
+ "",
378
+ ` ${MIGRATE_COMMAND}`
379
+ ];
380
+ }
381
+ function detailLines(detail) {
382
+ if (!detail || detail.length === 0) return [];
383
+ return ["", ...detail];
384
+ }
385
+ /**
386
+ * The consumer-facing failure text: a site-specific headline, then the branch that matches what is
387
+ * on disk, then the layout dump, then the bypass. The command to run always comes before the dump,
388
+ * because a consumer who reads only the first screen still has to be told about `migrate`.
389
+ * @example
390
+ * formatLayoutGuidance({ headline: 'no `insider.lint.*` module found in /repo.', diagnosis });
391
+ */
392
+ function formatLayoutGuidance(input) {
393
+ return [
394
+ `@useinsider/eslint-config: ${input.headline}`,
395
+ "",
396
+ ...inventoryLines(input.diagnosis),
397
+ "",
398
+ ...guidanceBody(input.diagnosis),
399
+ ...detailLines(input.detail),
400
+ "",
401
+ EXPECTED_LAYOUT,
402
+ "",
403
+ `Emergency bypass: set ${OPT_OUT_ENV}=true (do not commit).`
404
+ ].join("\n");
405
+ }
406
+ //#endregion
407
+ //#region src/utils/offerMigration.ts
408
+ var PACKAGE_NAME = "@useinsider/eslint-config";
409
+ var CLI_RELATIVE_PATH = "dist/cli.cjs";
410
+ var NO_PROMPT_ENV = "INSIDER_NO_PROMPT";
411
+ var IN_FLIGHT_ENV = "INSIDER_MIGRATION_IN_PROGRESS";
412
+ var MIGRATE_FROM_LINT_ARGS = ["migrate", "--from-lint"];
413
+ var BINARY_EXTENSIONS = /\.[mc]?js$/u;
414
+ function binaryName(argument) {
415
+ return (0, node_path.basename)(argument).replace(BINARY_EXTENSIONS, "");
416
+ }
417
+ /**
418
+ * Which linter is hosting the current process. oxlint evaluates `oxlint.config.ts` inside the Node
419
+ * process that runs its own `bin/oxlint` wrapper, so `useInsider()` genuinely executes under both
420
+ * binaries and the two have to be told apart before anything is written to stdout.
421
+ * @example
422
+ * detectLintHost(['node', '/app/node_modules/oxlint/bin/oxlint', '.']); // 'oxlint'
423
+ */
424
+ function detectLintHost(argv = process.argv) {
425
+ const names = argv.slice(1).map(binaryName);
426
+ if (names.includes("oxlint")) return "oxlint";
427
+ if (names.includes("eslint")) return "eslint";
428
+ return "unknown";
429
+ }
430
+ function defaultInteractive() {
431
+ return Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
432
+ }
433
+ function skipReasonFor(env, argv, interactive) {
434
+ if (env[IN_FLIGHT_ENV] === "true") return "already-offered";
435
+ if (env.VITEST || env.NODE_ENV === "test") return "running-under-test";
436
+ if (env["INSIDER_SKIP_CONFIG_VALIDATION"] === "true" || env[NO_PROMPT_ENV] === "true") return "suppressed";
437
+ if (env.CI) return "running-in-ci";
438
+ if (detectLintHost(argv) === "oxlint") return "host-is-oxlint";
439
+ if (!interactive) return "no-tty";
440
+ return null;
441
+ }
442
+ function manifestNameAt(directory) {
443
+ const manifestPath = (0, node_path.join)(directory, "package.json");
444
+ if (!(0, node_fs.existsSync)(manifestPath)) return null;
445
+ try {
446
+ return JSON.parse((0, node_fs.readFileSync)(manifestPath, "utf8")).name ?? null;
447
+ } catch {
448
+ return null;
449
+ }
450
+ }
451
+ function packageRootFrom(startDir) {
452
+ let current = startDir;
453
+ for (let depth = 0; depth < 10; depth += 1) {
454
+ if (manifestNameAt(current) === PACKAGE_NAME) return current;
455
+ const parent = (0, node_path.dirname)(current);
456
+ if (parent === current) return null;
457
+ current = parent;
458
+ }
459
+ return null;
460
+ }
461
+ /**
462
+ * Absolute path to this package's own built CLI, or `null` when it is not on disk. Resolved by
463
+ * walking up from `startDir` — this module's own directory by default — rather than through
464
+ * `require.resolve`, so it works from `dist/` and from `src/` alike.
465
+ * @example
466
+ * resolveCliEntry(); // '/app/node_modules/@useinsider/eslint-config/dist/cli.cjs'
467
+ */
468
+ function resolveCliEntry(startDir) {
469
+ const root = packageRootFrom(startDir ?? (0, node_path.dirname)((0, node_url.fileURLToPath)({}.url)));
470
+ if (!root) return null;
471
+ const entry = (0, node_path.resolve)(root, CLI_RELATIVE_PATH);
472
+ return (0, node_fs.existsSync)(entry) ? entry : null;
473
+ }
474
+ function defaultRunCli(entry, args, cwd, env) {
475
+ const result = (0, node_child_process.spawnSync)(process.execPath, [entry, ...args], {
476
+ cwd,
477
+ env,
478
+ stdio: "inherit"
479
+ });
480
+ if (result.error) return 1;
481
+ return result.status ?? 1;
482
+ }
483
+ function defaultWarn(message) {
484
+ console.error(message);
485
+ }
486
+ function outdatedNotice(diagnosis) {
487
+ const config = diagnosis.eslintConfig ?? "eslint.config.*";
488
+ return [
489
+ `@useinsider/eslint-config: ${config} still uses the v3 single-file layout.`,
490
+ ` ${(0, node_path.join)(diagnosis.directory, config)} calls \`useInsider()\` itself, and`,
491
+ ` ${diagnosis.missing.join(" and ")} ${diagnosis.missing.length === 1 ? "is" : "are"} missing.`,
492
+ "",
493
+ "Starting the guided migration. It shows every change before applying it.",
494
+ `Skip it with ${NO_PROMPT_ENV}=true.`,
495
+ ""
496
+ ].join("\n");
497
+ }
498
+ var offerAttempted = false;
499
+ function withDefaults(deps) {
500
+ return {
501
+ env: deps.env ?? process.env,
502
+ argv: deps.argv ?? process.argv,
503
+ interactive: deps.interactive ?? defaultInteractive(),
504
+ resolveCliEntry: deps.resolveCliEntry ?? resolveCliEntry,
505
+ runCli: deps.runCli ?? defaultRunCli,
506
+ warn: deps.warn ?? defaultWarn
507
+ };
508
+ }
509
+ /**
510
+ * Offers the guided migration for an outdated layout and runs it when the consumer accepts. Declines
511
+ * silently — returning a skip reason instead of prompting — whenever writing to stdout would be
512
+ * wrong: under oxlint, in CI, without a TTY, or when already inside a migration.
513
+ * @example
514
+ * offerMigration(diagnoseLayout('/repo')).accepted; // true after the consumer confirmed
515
+ */
516
+ function offerMigration(diagnosis, deps = {}) {
517
+ const resolved = withDefaults(deps);
518
+ if (offerAttempted) return {
519
+ offered: false,
520
+ skipReason: "already-offered"
521
+ };
522
+ const skipReason = skipReasonFor(resolved.env, resolved.argv, resolved.interactive);
523
+ if (skipReason) return {
524
+ offered: false,
525
+ skipReason
526
+ };
527
+ const entry = resolved.resolveCliEntry();
528
+ if (!entry) return {
529
+ offered: false,
530
+ skipReason: "cli-not-found"
531
+ };
532
+ offerAttempted = true;
533
+ resolved.warn(outdatedNotice(diagnosis));
534
+ return {
535
+ offered: true,
536
+ accepted: resolved.runCli(entry, MIGRATE_FROM_LINT_ARGS, diagnosis.directory, {
537
+ ...resolved.env,
538
+ [IN_FLIGHT_ENV]: "true"
539
+ }) === 0
540
+ };
541
+ }
542
+ //#endregion
543
+ //#region src/utils/ensureConfigStructure.ts
544
+ var ANCHOR_FILE_REGEX = /(?:insider\.lint|eslint\.config)\.[mc]?[jt]s(?:$|\?)/;
545
+ var FILE_URL_PREFIX = "file://";
234
546
  var bypassWarnEmitted = false;
235
547
  function computeSelfModulePath() {
236
548
  const raw = (0, node_url.fileURLToPath)({}.url);
@@ -245,7 +557,7 @@ function isPathSpecifier(source) {
245
557
  return source.startsWith(".") || source.startsWith("/");
246
558
  }
247
559
  function defaultIsInsiderImportSource(source, configPath) {
248
- if (source === INSIDER_IMPORT_SOURCE) return true;
560
+ if (source === "@useinsider/eslint-config") return true;
249
561
  if (!configPath || !isPathSpecifier(source)) return false;
250
562
  try {
251
563
  const absolute = (0, node_path.resolve)((0, node_path.dirname)(configPath), source);
@@ -341,7 +653,7 @@ function validateProgramStructure(program, configPath = "", options = {}) {
341
653
  const insiderBindings = /* @__PURE__ */ new Set();
342
654
  const resolver = options.isInsiderImportSource ?? defaultIsInsiderImportSource;
343
655
  const isInsiderSource = (source) => resolver(source, configPath);
344
- const expectedCallee = options.expectedCallee ?? ESLINT_RENDERER_NAME;
656
+ const expectedCallee = options.expectedCallee ?? "useInsiderESLint";
345
657
  let exportSeen = false;
346
658
  program.body.forEach((node) => {
347
659
  if (node.type === _typescript_eslint_types.AST_NODE_TYPES.ImportDeclaration) collectInsiderBindings(node, insiderBindings, isInsiderSource);
@@ -357,7 +669,7 @@ function validateProgramStructure(program, configPath = "", options = {}) {
357
669
  if (!exportSeen) violations.push({
358
670
  line: 1,
359
671
  column: 0,
360
- reason: `Missing \`export default ${options.expectedCallee ?? ESLINT_RENDERER_NAME}()\`.`
672
+ reason: `Missing \`export default ${options.expectedCallee ?? "useInsiderESLint"}()\`.`
361
673
  });
362
674
  return violations;
363
675
  }
@@ -521,24 +833,6 @@ function captureConsumerConfigDir(configPath) {
521
833
  function getConsumerConfigDir() {
522
834
  return consumerConfigDir ?? process.cwd();
523
835
  }
524
- var EXPECTED_LAYOUT = [
525
- "Expected layout (three sibling files):",
526
- "",
527
- " // insider.lint.ts",
528
- " import { useInsider } from '@useinsider/eslint-config';",
529
- "",
530
- ` ${RENDERER_EXPORT};`,
531
- "",
532
- " // eslint.config.ts",
533
- ` import { ${ESLINT_RENDERER_NAME} } from './insider.lint.ts';`,
534
- "",
535
- ` export default ${ESLINT_RENDERER_NAME}();`,
536
- "",
537
- " // oxlint.config.ts",
538
- ` import { ${OXC_RENDERER_NAME} } from './insider.lint.ts';`,
539
- "",
540
- ` export default await ${OXC_RENDERER_NAME}();`
541
- ].join("\n");
542
836
  function formatReport(configPath, violations) {
543
837
  const header = ["@useinsider/eslint-config: protected configuration structure violated in:", ` ${configPath}`].join("\n");
544
838
  const lines = violations.map((violation) => ` - [${violation.line}:${violation.column}] ${violation.reason}`);
@@ -549,7 +843,7 @@ function formatReport(configPath, violations) {
549
843
  "",
550
844
  EXPECTED_LAYOUT,
551
845
  "",
552
- `Emergency bypass: set ${OPT_OUT_ENV$1}=true (do not commit).`
846
+ `Emergency bypass: set ${OPT_OUT_ENV}=true (do not commit).`
553
847
  ].join("\n");
554
848
  return `${header}\n${lines.join("\n")}\n${footer}`;
555
849
  }
@@ -599,46 +893,48 @@ function validateFile(filePath, validator, options) {
599
893
  if (violations.length === 0) return;
600
894
  fail(formatReport(filePath, violations));
601
895
  }
602
- function runValidation(anchorPath) {
603
- if (!anchorPath) fail([
604
- "@useinsider/eslint-config: unable to locate the consumer `insider.lint.*` module for structure validation.",
605
- "",
606
- EXPECTED_LAYOUT,
607
- "",
608
- `If this is an unexpected environment, set ${OPT_OUT_ENV$1}=true (do not commit).`
609
- ].join("\n"));
896
+ var MIGRATED_EXIT_NOTICE = [
897
+ "@useinsider/eslint-config: migration finished.",
898
+ "",
899
+ "Re-run your lint command. This process already loaded the old configuration, so continuing now",
900
+ "would lint against a config that no longer exists on disk."
901
+ ].join("\n");
902
+ var DECLINED_DETAIL = ["The guided migration was not completed, so nothing was written."];
903
+ function failWithGuidance(headline, diagnosis, detail) {
904
+ return fail(formatLayoutGuidance({
905
+ headline,
906
+ diagnosis,
907
+ detail
908
+ }));
909
+ }
910
+ function handleOutdatedLayout(headline, diagnosis, offer) {
911
+ if (!diagnosis.autoMigratable) return failWithGuidance(headline, diagnosis);
912
+ const outcome = offer(diagnosis);
913
+ if (!outcome.offered) return failWithGuidance(headline, diagnosis);
914
+ if (!outcome.accepted) return failWithGuidance(headline, diagnosis, DECLINED_DETAIL);
915
+ return fail(MIGRATED_EXIT_NOTICE);
916
+ }
917
+ function runValidation(anchorPath, deps = {}) {
918
+ const diagnose = deps.diagnose ?? ((directory) => diagnoseLayout(directory));
919
+ const offer = deps.offer ?? offerMigration;
920
+ if (!anchorPath) handleOutdatedLayout("unable to locate the consumer `insider.lint.*` module for structure validation.", diagnose(process.cwd()), offer);
610
921
  const directory = (0, node_path.dirname)((0, node_path.resolve)(anchorPath));
611
922
  const lintModulePath = resolveSibling(directory, LINT_MODULE_FILENAMES);
612
- if (!lintModulePath) fail([
613
- `@useinsider/eslint-config: no \`insider.lint.*\` module found in ${directory}.`,
614
- "",
615
- `\`${TARGET_CALLEE_NAME}()\` now returns the two renderers instead of a flat-config array,`,
616
- "so the config it is called from must be a module both linters can import.",
617
- "",
618
- EXPECTED_LAYOUT,
619
- "",
620
- `Emergency bypass: set ${OPT_OUT_ENV$1}=true (do not commit).`
621
- ].join("\n"));
923
+ if (!lintModulePath) handleOutdatedLayout(`no \`insider.lint.*\` module found in ${directory}.`, diagnose(directory), offer);
622
924
  validateFile(lintModulePath, validateLintModuleStructure, {});
623
925
  const eslintConfigPath = resolveSibling(directory, ESLINT_CONFIG_FILENAMES);
624
- if (!eslintConfigPath) fail([
625
- `@useinsider/eslint-config: no \`eslint.config.*\` file found in ${directory}.`,
626
- "",
627
- EXPECTED_LAYOUT,
628
- "",
629
- `Emergency bypass: set ${OPT_OUT_ENV$1}=true (do not commit).`
630
- ].join("\n"));
926
+ 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}()\`.`]);
631
927
  validateFile(eslintConfigPath, validateProgramStructure, { expectedCallee: ESLINT_RENDERER_NAME });
632
928
  }
633
- function ensureConfigStructure(overridePath) {
929
+ function ensureConfigStructure(overridePath, deps = {}) {
634
930
  const configPath = overridePath === void 0 ? getConsumerConfigPath() : overridePath;
635
931
  captureConsumerConfigDir(configPath);
636
- if (process.env[OPT_OUT_ENV$1] === "true") {
932
+ if (process.env["INSIDER_SKIP_CONFIG_VALIDATION"] === "true") {
637
933
  emitBypassWarning();
638
934
  return;
639
935
  }
640
936
  try {
641
- runValidation(configPath);
937
+ runValidation(configPath, deps);
642
938
  } catch (error) {
643
939
  if (error instanceof ConfigStructureExit) return;
644
940
  throw error;
@@ -762,6 +1058,14 @@ function clonePreset(preset) {
762
1058
  function isScopeName(key) {
763
1059
  return Object.keys(scopeMap).includes(key);
764
1060
  }
1061
+ /**
1062
+ * Every scope name `useInsider()` accepts, for error messages that have to spell the options out.
1063
+ * @example
1064
+ * scopeNames().includes('typescriptNode'); // true
1065
+ */
1066
+ function scopeNames() {
1067
+ return Object.keys(scopeMap);
1068
+ }
765
1069
  function applyCommonEntryFields(block, entry) {
766
1070
  if (entry.ignores && entry.ignores.length > 0) block.ignores = [...entry.ignores];
767
1071
  if (entry.globals) {
@@ -881,31 +1185,27 @@ function silenceDependencyWarning(input) {
881
1185
  }
882
1186
  //#endregion
883
1187
  //#region src/utils/ensureOxlintConfig.ts
884
- var OPT_OUT_ENV = "INSIDER_SKIP_CONFIG_VALIDATION";
885
- var OXLINT_CONFIG_FILENAMES = ["oxlint.config.ts", "oxlint.config.mts"];
886
- function lintModuleSpecifier(directory) {
887
- return `./${LINT_MODULE_FILENAMES.find((filename) => (0, node_fs.existsSync)((0, node_path.join)(directory, filename))) ?? "insider.lint.ts"}`;
888
- }
889
1188
  function formatMessage(directory) {
890
- return [
891
- "@useinsider/eslint-config: no oxlint config found in",
892
- ` ${directory}`,
893
- "",
894
- "useInsiderESLint() turns off every rule oxlint owns, so running ESLint alone would silently",
895
- "drop those checks. Create the sibling oxlint config:",
896
- "",
897
- " // oxlint.config.ts",
898
- ` import { useInsiderOXC } from '${lintModuleSpecifier(directory)}';`,
899
- "",
900
- " export default await useInsiderOXC();",
901
- "",
902
- "and run both linters:",
903
- "",
904
- " \"lint\": \"oxlint --disable-nested-config && eslint .\"",
905
- "",
906
- `Expected one of: ${OXLINT_CONFIG_FILENAMES.join(", ")}.`,
907
- `Emergency bypass: set ${OPT_OUT_ENV}=true (do not commit).`
908
- ].join("\n");
1189
+ return formatLayoutGuidance({
1190
+ headline: `no oxlint config found in ${directory}.`,
1191
+ diagnosis: diagnoseLayout(directory),
1192
+ detail: [
1193
+ "useInsiderESLint() turns off every rule oxlint owns, so running ESLint alone would silently",
1194
+ "drop those checks. Create the sibling oxlint config:",
1195
+ "",
1196
+ " // oxlint.config.ts",
1197
+ ` import { useInsiderOXC } from '${lintModuleSpecifier(directory)}';`,
1198
+ "",
1199
+ " export default await useInsiderOXC();",
1200
+ "",
1201
+ "and run both linters:",
1202
+ "",
1203
+ " \"lint\": \"oxlint --disable-nested-config && eslint .\"",
1204
+ "",
1205
+ `oxlint discovers only ${OXLINT_CONFIG_FILENAMES.join(" and ")} — any other extension is`,
1206
+ "ignored without a word."
1207
+ ]
1208
+ });
909
1209
  }
910
1210
  /**
911
1211
  * Fails when the consumer enabled the ESLint-side oxlint suppression without a sibling oxlint
@@ -915,7 +1215,7 @@ function formatMessage(directory) {
915
1215
  * ensureOxlintConfig(); // throws unless oxlint.config.{ts,mts} sits beside insider.lint.*
916
1216
  */
917
1217
  function ensureOxlintConfig(directory = getConsumerConfigDir()) {
918
- if (process.env[OPT_OUT_ENV] === "true") return;
1218
+ if (process.env["INSIDER_SKIP_CONFIG_VALIDATION"] === "true") return;
919
1219
  if (OXLINT_CONFIG_FILENAMES.some((filename) => (0, node_fs.existsSync)((0, node_path.join)(directory, filename)))) return;
920
1220
  throw new Error(formatMessage(directory));
921
1221
  }
@@ -980,6 +1280,11 @@ function assertNoRemovedKeys(config) {
980
1280
  const details = used.map((key) => ` - \`${key}\`: ${removedKeys.get(key)}`).join("\n");
981
1281
  throw new Error(`@useinsider/eslint-config: removed option(s) passed to useInsider():\n${details}`);
982
1282
  }
1283
+ function assertNoUnknownKeys(config) {
1284
+ const unknown = Object.keys(config).filter((key) => !reservedKeys.has(key) && !removedKeys.has(key) && !isScopeName(key));
1285
+ if (unknown.length === 0) return;
1286
+ throw new Error(`@useinsider/eslint-config: unknown option(s) passed to useInsider(): ${unknown.join(", ")}.\n Scopes: ${scopeNames().join(", ")}\n Other options: ${[...reservedKeys].join(", ")}`);
1287
+ }
983
1288
  function isProcessableScopeKey(key) {
984
1289
  return !reservedKeys.has(key) && isScopeName(key);
985
1290
  }
@@ -1032,6 +1337,7 @@ async function renderOxlint(config) {
1032
1337
  }
1033
1338
  function useInsider(config) {
1034
1339
  assertNoRemovedKeys(config);
1340
+ assertNoUnknownKeys(config);
1035
1341
  ensureConfigStructure();
1036
1342
  return {
1037
1343
  useInsiderESLint: () => renderEslint(config),