@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 +100 -9
- package/dist/builders/scopeBlocks.d.ts +6 -0
- package/dist/cli.cjs +551 -166
- package/dist/cli.cjs.map +1 -1
- package/dist/main.cjs +390 -84
- package/dist/main.cjs.map +1 -1
- package/dist/main.js +391 -85
- package/dist/main.js.map +1 -1
- package/dist/utils/ensureConfigStructure.d.ts +8 -4
- package/dist/utils/ensureOxlintConfig.d.ts +2 -1
- package/dist/utils/layoutDiagnosis.d.ts +63 -0
- package/dist/utils/offerMigration.d.ts +42 -0
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -5,9 +5,10 @@ import { defineConfig } from "eslint/config";
|
|
|
5
5
|
import globals from "globals";
|
|
6
6
|
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
7
7
|
import { createRequire } from "node:module";
|
|
8
|
-
import { dirname, join, resolve } from "node:path";
|
|
8
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { AST_NODE_TYPES } from "@typescript-eslint/types";
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
11
12
|
import { installPackage } from "@antfu/install-pkg";
|
|
12
13
|
import semver from "semver";
|
|
13
14
|
import { warn } from "node:console";
|
|
@@ -209,14 +210,7 @@ function buildOxlintConfig(blocks, ignores) {
|
|
|
209
210
|
};
|
|
210
211
|
}
|
|
211
212
|
//#endregion
|
|
212
|
-
//#region src/utils/
|
|
213
|
-
var ANCHOR_FILE_REGEX = /(?:insider\.lint|eslint\.config)\.[mc]?[jt]s(?:$|\?)/;
|
|
214
|
-
var OPT_OUT_ENV$1 = "INSIDER_SKIP_CONFIG_VALIDATION";
|
|
215
|
-
var FILE_URL_PREFIX = "file://";
|
|
216
|
-
var TARGET_CALLEE_NAME = "useInsider";
|
|
217
|
-
var ESLINT_RENDERER_NAME = "useInsiderESLint";
|
|
218
|
-
var OXC_RENDERER_NAME = "useInsiderOXC";
|
|
219
|
-
var INSIDER_IMPORT_SOURCE = "@useinsider/eslint-config";
|
|
213
|
+
//#region src/utils/layoutDiagnosis.ts
|
|
220
214
|
var MODULE_EXTENSIONS = [
|
|
221
215
|
"ts",
|
|
222
216
|
"mts",
|
|
@@ -225,9 +219,327 @@ var MODULE_EXTENSIONS = [
|
|
|
225
219
|
"mjs",
|
|
226
220
|
"cjs"
|
|
227
221
|
];
|
|
228
|
-
var RENDERER_EXPORT = `export const ${`{ ${ESLINT_RENDERER_NAME}, ${OXC_RENDERER_NAME} }`} = ${TARGET_CALLEE_NAME}({ ... })`;
|
|
229
222
|
var LINT_MODULE_FILENAMES = MODULE_EXTENSIONS.map((ext) => `insider.lint.${ext}`);
|
|
230
223
|
var ESLINT_CONFIG_FILENAMES = MODULE_EXTENSIONS.map((ext) => `eslint.config.${ext}`);
|
|
224
|
+
var OXLINT_CONFIG_FILENAMES = ["oxlint.config.ts", "oxlint.config.mts"];
|
|
225
|
+
var OPT_OUT_ENV = "INSIDER_SKIP_CONFIG_VALIDATION";
|
|
226
|
+
var MIGRATE_COMMAND = "npx @useinsider/eslint-config migrate";
|
|
227
|
+
var SCAFFOLD_COMMAND = "npx @useinsider/eslint-config";
|
|
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://";
|
|
231
543
|
var bypassWarnEmitted = false;
|
|
232
544
|
function computeSelfModulePath() {
|
|
233
545
|
const raw = fileURLToPath(import.meta.url);
|
|
@@ -242,7 +554,7 @@ function isPathSpecifier(source) {
|
|
|
242
554
|
return source.startsWith(".") || source.startsWith("/");
|
|
243
555
|
}
|
|
244
556
|
function defaultIsInsiderImportSource(source, configPath) {
|
|
245
|
-
if (source ===
|
|
557
|
+
if (source === "@useinsider/eslint-config") return true;
|
|
246
558
|
if (!configPath || !isPathSpecifier(source)) return false;
|
|
247
559
|
try {
|
|
248
560
|
const absolute = resolve(dirname(configPath), source);
|
|
@@ -338,7 +650,7 @@ function validateProgramStructure(program, configPath = "", options = {}) {
|
|
|
338
650
|
const insiderBindings = /* @__PURE__ */ new Set();
|
|
339
651
|
const resolver = options.isInsiderImportSource ?? defaultIsInsiderImportSource;
|
|
340
652
|
const isInsiderSource = (source) => resolver(source, configPath);
|
|
341
|
-
const expectedCallee = options.expectedCallee ??
|
|
653
|
+
const expectedCallee = options.expectedCallee ?? "useInsiderESLint";
|
|
342
654
|
let exportSeen = false;
|
|
343
655
|
program.body.forEach((node) => {
|
|
344
656
|
if (node.type === AST_NODE_TYPES.ImportDeclaration) collectInsiderBindings(node, insiderBindings, isInsiderSource);
|
|
@@ -354,7 +666,7 @@ function validateProgramStructure(program, configPath = "", options = {}) {
|
|
|
354
666
|
if (!exportSeen) violations.push({
|
|
355
667
|
line: 1,
|
|
356
668
|
column: 0,
|
|
357
|
-
reason: `Missing \`export default ${options.expectedCallee ??
|
|
669
|
+
reason: `Missing \`export default ${options.expectedCallee ?? "useInsiderESLint"}()\`.`
|
|
358
670
|
});
|
|
359
671
|
return violations;
|
|
360
672
|
}
|
|
@@ -518,24 +830,6 @@ function captureConsumerConfigDir(configPath) {
|
|
|
518
830
|
function getConsumerConfigDir() {
|
|
519
831
|
return consumerConfigDir ?? process.cwd();
|
|
520
832
|
}
|
|
521
|
-
var EXPECTED_LAYOUT = [
|
|
522
|
-
"Expected layout (three sibling files):",
|
|
523
|
-
"",
|
|
524
|
-
" // insider.lint.ts",
|
|
525
|
-
" import { useInsider } from '@useinsider/eslint-config';",
|
|
526
|
-
"",
|
|
527
|
-
` ${RENDERER_EXPORT};`,
|
|
528
|
-
"",
|
|
529
|
-
" // eslint.config.ts",
|
|
530
|
-
` import { ${ESLINT_RENDERER_NAME} } from './insider.lint.ts';`,
|
|
531
|
-
"",
|
|
532
|
-
` export default ${ESLINT_RENDERER_NAME}();`,
|
|
533
|
-
"",
|
|
534
|
-
" // oxlint.config.ts",
|
|
535
|
-
` import { ${OXC_RENDERER_NAME} } from './insider.lint.ts';`,
|
|
536
|
-
"",
|
|
537
|
-
` export default await ${OXC_RENDERER_NAME}();`
|
|
538
|
-
].join("\n");
|
|
539
833
|
function formatReport(configPath, violations) {
|
|
540
834
|
const header = ["@useinsider/eslint-config: protected configuration structure violated in:", ` ${configPath}`].join("\n");
|
|
541
835
|
const lines = violations.map((violation) => ` - [${violation.line}:${violation.column}] ${violation.reason}`);
|
|
@@ -546,7 +840,7 @@ function formatReport(configPath, violations) {
|
|
|
546
840
|
"",
|
|
547
841
|
EXPECTED_LAYOUT,
|
|
548
842
|
"",
|
|
549
|
-
`Emergency bypass: set ${OPT_OUT_ENV
|
|
843
|
+
`Emergency bypass: set ${OPT_OUT_ENV}=true (do not commit).`
|
|
550
844
|
].join("\n");
|
|
551
845
|
return `${header}\n${lines.join("\n")}\n${footer}`;
|
|
552
846
|
}
|
|
@@ -596,46 +890,48 @@ function validateFile(filePath, validator, options) {
|
|
|
596
890
|
if (violations.length === 0) return;
|
|
597
891
|
fail(formatReport(filePath, violations));
|
|
598
892
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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);
|
|
607
918
|
const directory = dirname(resolve(anchorPath));
|
|
608
919
|
const lintModulePath = resolveSibling(directory, LINT_MODULE_FILENAMES);
|
|
609
|
-
if (!lintModulePath)
|
|
610
|
-
`@useinsider/eslint-config: no \`insider.lint.*\` module found in ${directory}.`,
|
|
611
|
-
"",
|
|
612
|
-
`\`${TARGET_CALLEE_NAME}()\` now returns the two renderers instead of a flat-config array,`,
|
|
613
|
-
"so the config it is called from must be a module both linters can import.",
|
|
614
|
-
"",
|
|
615
|
-
EXPECTED_LAYOUT,
|
|
616
|
-
"",
|
|
617
|
-
`Emergency bypass: set ${OPT_OUT_ENV$1}=true (do not commit).`
|
|
618
|
-
].join("\n"));
|
|
920
|
+
if (!lintModulePath) handleOutdatedLayout(`no \`insider.lint.*\` module found in ${directory}.`, diagnose(directory), offer);
|
|
619
921
|
validateFile(lintModulePath, validateLintModuleStructure, {});
|
|
620
922
|
const eslintConfigPath = resolveSibling(directory, ESLINT_CONFIG_FILENAMES);
|
|
621
|
-
if (!eslintConfigPath)
|
|
622
|
-
`@useinsider/eslint-config: no \`eslint.config.*\` file found in ${directory}.`,
|
|
623
|
-
"",
|
|
624
|
-
EXPECTED_LAYOUT,
|
|
625
|
-
"",
|
|
626
|
-
`Emergency bypass: set ${OPT_OUT_ENV$1}=true (do not commit).`
|
|
627
|
-
].join("\n"));
|
|
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}()\`.`]);
|
|
628
924
|
validateFile(eslintConfigPath, validateProgramStructure, { expectedCallee: ESLINT_RENDERER_NAME });
|
|
629
925
|
}
|
|
630
|
-
function ensureConfigStructure(overridePath) {
|
|
926
|
+
function ensureConfigStructure(overridePath, deps = {}) {
|
|
631
927
|
const configPath = overridePath === void 0 ? getConsumerConfigPath() : overridePath;
|
|
632
928
|
captureConsumerConfigDir(configPath);
|
|
633
|
-
if (process.env[
|
|
929
|
+
if (process.env["INSIDER_SKIP_CONFIG_VALIDATION"] === "true") {
|
|
634
930
|
emitBypassWarning();
|
|
635
931
|
return;
|
|
636
932
|
}
|
|
637
933
|
try {
|
|
638
|
-
runValidation(configPath);
|
|
934
|
+
runValidation(configPath, deps);
|
|
639
935
|
} catch (error) {
|
|
640
936
|
if (error instanceof ConfigStructureExit) return;
|
|
641
937
|
throw error;
|
|
@@ -759,6 +1055,14 @@ function clonePreset(preset) {
|
|
|
759
1055
|
function isScopeName(key) {
|
|
760
1056
|
return Object.keys(scopeMap).includes(key);
|
|
761
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
|
+
}
|
|
762
1066
|
function applyCommonEntryFields(block, entry) {
|
|
763
1067
|
if (entry.ignores && entry.ignores.length > 0) block.ignores = [...entry.ignores];
|
|
764
1068
|
if (entry.globals) {
|
|
@@ -878,31 +1182,27 @@ function silenceDependencyWarning(input) {
|
|
|
878
1182
|
}
|
|
879
1183
|
//#endregion
|
|
880
1184
|
//#region src/utils/ensureOxlintConfig.ts
|
|
881
|
-
var OPT_OUT_ENV = "INSIDER_SKIP_CONFIG_VALIDATION";
|
|
882
|
-
var OXLINT_CONFIG_FILENAMES = ["oxlint.config.ts", "oxlint.config.mts"];
|
|
883
|
-
function lintModuleSpecifier(directory) {
|
|
884
|
-
return `./${LINT_MODULE_FILENAMES.find((filename) => existsSync(join(directory, filename))) ?? "insider.lint.ts"}`;
|
|
885
|
-
}
|
|
886
1185
|
function formatMessage(directory) {
|
|
887
|
-
return
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
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
|
+
});
|
|
906
1206
|
}
|
|
907
1207
|
/**
|
|
908
1208
|
* Fails when the consumer enabled the ESLint-side oxlint suppression without a sibling oxlint
|
|
@@ -912,7 +1212,7 @@ function formatMessage(directory) {
|
|
|
912
1212
|
* ensureOxlintConfig(); // throws unless oxlint.config.{ts,mts} sits beside insider.lint.*
|
|
913
1213
|
*/
|
|
914
1214
|
function ensureOxlintConfig(directory = getConsumerConfigDir()) {
|
|
915
|
-
if (process.env[
|
|
1215
|
+
if (process.env["INSIDER_SKIP_CONFIG_VALIDATION"] === "true") return;
|
|
916
1216
|
if (OXLINT_CONFIG_FILENAMES.some((filename) => existsSync(join(directory, filename)))) return;
|
|
917
1217
|
throw new Error(formatMessage(directory));
|
|
918
1218
|
}
|
|
@@ -977,6 +1277,11 @@ function assertNoRemovedKeys(config) {
|
|
|
977
1277
|
const details = used.map((key) => ` - \`${key}\`: ${removedKeys.get(key)}`).join("\n");
|
|
978
1278
|
throw new Error(`@useinsider/eslint-config: removed option(s) passed to useInsider():\n${details}`);
|
|
979
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
|
+
}
|
|
980
1285
|
function isProcessableScopeKey(key) {
|
|
981
1286
|
return !reservedKeys.has(key) && isScopeName(key);
|
|
982
1287
|
}
|
|
@@ -1029,6 +1334,7 @@ async function renderOxlint(config) {
|
|
|
1029
1334
|
}
|
|
1030
1335
|
function useInsider(config) {
|
|
1031
1336
|
assertNoRemovedKeys(config);
|
|
1337
|
+
assertNoUnknownKeys(config);
|
|
1032
1338
|
ensureConfigStructure();
|
|
1033
1339
|
return {
|
|
1034
1340
|
useInsiderESLint: () => renderEslint(config),
|