@useinsider/eslint-config 4.0.0-beta.1 → 4.0.0-beta.3
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 +122 -17
- package/dist/builders/scopeBlocks.d.ts +6 -0
- package/dist/cli.cjs +949 -223
- package/dist/cli.cjs.map +1 -1
- package/dist/main.cjs +395 -84
- package/dist/main.cjs.map +1 -1
- package/dist/main.js +396 -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.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/
|
|
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,332 @@ 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 === "complete") return [
|
|
364
|
+
`All three files are already here, so there is nothing to migrate — ${TARGET_CALLEE_NAME}() was`,
|
|
365
|
+
`reached from somewhere other than ${diagnosis.lintModule ?? LINT_MODULE_FILENAMES[0]}. Only the`,
|
|
366
|
+
"lint module may call it; the two configs import the renderers it exports."
|
|
367
|
+
];
|
|
368
|
+
if (diagnosis.kind === "unmigratedV3") return [
|
|
369
|
+
"`useInsider()` no longer returns a flat-config array, so the config it is called from has",
|
|
370
|
+
"to become a module both linters can import. The migration does that rewrite for you:",
|
|
371
|
+
"",
|
|
372
|
+
` ${MIGRATE_COMMAND}`
|
|
373
|
+
];
|
|
374
|
+
if (diagnosis.kind === "noConfig") return [
|
|
375
|
+
"There is no linter configuration here yet. Scaffold the three-file layout:",
|
|
376
|
+
"",
|
|
377
|
+
` ${SCAFFOLD_COMMAND}`
|
|
378
|
+
];
|
|
379
|
+
return [
|
|
380
|
+
`The layout is incomplete — missing ${diagnosis.missing.join(", ")}.`,
|
|
381
|
+
"Add the missing file, or let the migration regenerate the whole layout:",
|
|
382
|
+
"",
|
|
383
|
+
` ${MIGRATE_COMMAND}`
|
|
384
|
+
];
|
|
385
|
+
}
|
|
386
|
+
function detailLines(detail) {
|
|
387
|
+
if (!detail || detail.length === 0) return [];
|
|
388
|
+
return ["", ...detail];
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* The consumer-facing failure text: a site-specific headline, then the branch that matches what is
|
|
392
|
+
* on disk, then the layout dump, then the bypass. The command to run always comes before the dump,
|
|
393
|
+
* because a consumer who reads only the first screen still has to be told about `migrate`.
|
|
394
|
+
* @example
|
|
395
|
+
* formatLayoutGuidance({ headline: 'no `insider.lint.*` module found in /repo.', diagnosis });
|
|
396
|
+
*/
|
|
397
|
+
function formatLayoutGuidance(input) {
|
|
398
|
+
return [
|
|
399
|
+
`@useinsider/eslint-config: ${input.headline}`,
|
|
400
|
+
"",
|
|
401
|
+
...inventoryLines(input.diagnosis),
|
|
402
|
+
"",
|
|
403
|
+
...guidanceBody(input.diagnosis),
|
|
404
|
+
...detailLines(input.detail),
|
|
405
|
+
"",
|
|
406
|
+
EXPECTED_LAYOUT,
|
|
407
|
+
"",
|
|
408
|
+
`Emergency bypass: set ${OPT_OUT_ENV}=true (do not commit).`
|
|
409
|
+
].join("\n");
|
|
410
|
+
}
|
|
411
|
+
//#endregion
|
|
412
|
+
//#region src/utils/offerMigration.ts
|
|
413
|
+
var PACKAGE_NAME = "@useinsider/eslint-config";
|
|
414
|
+
var CLI_RELATIVE_PATH = "dist/cli.cjs";
|
|
415
|
+
var NO_PROMPT_ENV = "INSIDER_NO_PROMPT";
|
|
416
|
+
var IN_FLIGHT_ENV = "INSIDER_MIGRATION_IN_PROGRESS";
|
|
417
|
+
var MIGRATE_FROM_LINT_ARGS = ["migrate", "--from-lint"];
|
|
418
|
+
var BINARY_EXTENSIONS = /\.[mc]?js$/u;
|
|
419
|
+
function binaryName(argument) {
|
|
420
|
+
return (0, node_path.basename)(argument).replace(BINARY_EXTENSIONS, "");
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Which linter is hosting the current process. oxlint evaluates `oxlint.config.ts` inside the Node
|
|
424
|
+
* process that runs its own `bin/oxlint` wrapper, so `useInsider()` genuinely executes under both
|
|
425
|
+
* binaries and the two have to be told apart before anything is written to stdout.
|
|
426
|
+
* @example
|
|
427
|
+
* detectLintHost(['node', '/app/node_modules/oxlint/bin/oxlint', '.']); // 'oxlint'
|
|
428
|
+
*/
|
|
429
|
+
function detectLintHost(argv = process.argv) {
|
|
430
|
+
const names = argv.slice(1).map(binaryName);
|
|
431
|
+
if (names.includes("oxlint")) return "oxlint";
|
|
432
|
+
if (names.includes("eslint")) return "eslint";
|
|
433
|
+
return "unknown";
|
|
434
|
+
}
|
|
435
|
+
function defaultInteractive() {
|
|
436
|
+
return Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
|
|
437
|
+
}
|
|
438
|
+
function skipReasonFor(env, argv, interactive) {
|
|
439
|
+
if (env[IN_FLIGHT_ENV] === "true") return "already-offered";
|
|
440
|
+
if (env.VITEST || env.NODE_ENV === "test") return "running-under-test";
|
|
441
|
+
if (env["INSIDER_SKIP_CONFIG_VALIDATION"] === "true" || env[NO_PROMPT_ENV] === "true") return "suppressed";
|
|
442
|
+
if (env.CI) return "running-in-ci";
|
|
443
|
+
if (detectLintHost(argv) === "oxlint") return "host-is-oxlint";
|
|
444
|
+
if (!interactive) return "no-tty";
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
function manifestNameAt(directory) {
|
|
448
|
+
const manifestPath = (0, node_path.join)(directory, "package.json");
|
|
449
|
+
if (!(0, node_fs.existsSync)(manifestPath)) return null;
|
|
450
|
+
try {
|
|
451
|
+
return JSON.parse((0, node_fs.readFileSync)(manifestPath, "utf8")).name ?? null;
|
|
452
|
+
} catch {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
function packageRootFrom(startDir) {
|
|
457
|
+
let current = startDir;
|
|
458
|
+
for (let depth = 0; depth < 10; depth += 1) {
|
|
459
|
+
if (manifestNameAt(current) === PACKAGE_NAME) return current;
|
|
460
|
+
const parent = (0, node_path.dirname)(current);
|
|
461
|
+
if (parent === current) return null;
|
|
462
|
+
current = parent;
|
|
463
|
+
}
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Absolute path to this package's own built CLI, or `null` when it is not on disk. Resolved by
|
|
468
|
+
* walking up from `startDir` — this module's own directory by default — rather than through
|
|
469
|
+
* `require.resolve`, so it works from `dist/` and from `src/` alike.
|
|
470
|
+
* @example
|
|
471
|
+
* resolveCliEntry(); // '/app/node_modules/@useinsider/eslint-config/dist/cli.cjs'
|
|
472
|
+
*/
|
|
473
|
+
function resolveCliEntry(startDir) {
|
|
474
|
+
const root = packageRootFrom(startDir ?? (0, node_path.dirname)((0, node_url.fileURLToPath)({}.url)));
|
|
475
|
+
if (!root) return null;
|
|
476
|
+
const entry = (0, node_path.resolve)(root, CLI_RELATIVE_PATH);
|
|
477
|
+
return (0, node_fs.existsSync)(entry) ? entry : null;
|
|
478
|
+
}
|
|
479
|
+
function defaultRunCli(entry, args, cwd, env) {
|
|
480
|
+
const result = (0, node_child_process.spawnSync)(process.execPath, [entry, ...args], {
|
|
481
|
+
cwd,
|
|
482
|
+
env,
|
|
483
|
+
stdio: "inherit"
|
|
484
|
+
});
|
|
485
|
+
if (result.error) return 1;
|
|
486
|
+
return result.status ?? 1;
|
|
487
|
+
}
|
|
488
|
+
function defaultWarn(message) {
|
|
489
|
+
console.error(message);
|
|
490
|
+
}
|
|
491
|
+
function outdatedNotice(diagnosis) {
|
|
492
|
+
const config = diagnosis.eslintConfig ?? "eslint.config.*";
|
|
493
|
+
return [
|
|
494
|
+
`@useinsider/eslint-config: ${config} still uses the v3 single-file layout.`,
|
|
495
|
+
` ${(0, node_path.join)(diagnosis.directory, config)} calls \`useInsider()\` itself, and`,
|
|
496
|
+
` ${diagnosis.missing.join(" and ")} ${diagnosis.missing.length === 1 ? "is" : "are"} missing.`,
|
|
497
|
+
"",
|
|
498
|
+
"Starting the guided migration. It shows every change before applying it.",
|
|
499
|
+
`Skip it with ${NO_PROMPT_ENV}=true.`,
|
|
500
|
+
""
|
|
501
|
+
].join("\n");
|
|
502
|
+
}
|
|
503
|
+
var offerAttempted = false;
|
|
504
|
+
function withDefaults(deps) {
|
|
505
|
+
return {
|
|
506
|
+
env: deps.env ?? process.env,
|
|
507
|
+
argv: deps.argv ?? process.argv,
|
|
508
|
+
interactive: deps.interactive ?? defaultInteractive(),
|
|
509
|
+
resolveCliEntry: deps.resolveCliEntry ?? resolveCliEntry,
|
|
510
|
+
runCli: deps.runCli ?? defaultRunCli,
|
|
511
|
+
warn: deps.warn ?? defaultWarn
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Offers the guided migration for an outdated layout and runs it when the consumer accepts. Declines
|
|
516
|
+
* silently — returning a skip reason instead of prompting — whenever writing to stdout would be
|
|
517
|
+
* wrong: under oxlint, in CI, without a TTY, or when already inside a migration.
|
|
518
|
+
* @example
|
|
519
|
+
* offerMigration(diagnoseLayout('/repo')).accepted; // true after the consumer confirmed
|
|
520
|
+
*/
|
|
521
|
+
function offerMigration(diagnosis, deps = {}) {
|
|
522
|
+
const resolved = withDefaults(deps);
|
|
523
|
+
if (offerAttempted) return {
|
|
524
|
+
offered: false,
|
|
525
|
+
skipReason: "already-offered"
|
|
526
|
+
};
|
|
527
|
+
const skipReason = skipReasonFor(resolved.env, resolved.argv, resolved.interactive);
|
|
528
|
+
if (skipReason) return {
|
|
529
|
+
offered: false,
|
|
530
|
+
skipReason
|
|
531
|
+
};
|
|
532
|
+
const entry = resolved.resolveCliEntry();
|
|
533
|
+
if (!entry) return {
|
|
534
|
+
offered: false,
|
|
535
|
+
skipReason: "cli-not-found"
|
|
536
|
+
};
|
|
537
|
+
offerAttempted = true;
|
|
538
|
+
resolved.warn(outdatedNotice(diagnosis));
|
|
539
|
+
return {
|
|
540
|
+
offered: true,
|
|
541
|
+
accepted: resolved.runCli(entry, MIGRATE_FROM_LINT_ARGS, diagnosis.directory, {
|
|
542
|
+
...resolved.env,
|
|
543
|
+
[IN_FLIGHT_ENV]: "true"
|
|
544
|
+
}) === 0
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
//#endregion
|
|
548
|
+
//#region src/utils/ensureConfigStructure.ts
|
|
549
|
+
var ANCHOR_FILE_REGEX = /(?:insider\.lint|eslint\.config)\.[mc]?[jt]s(?:$|\?)/;
|
|
550
|
+
var FILE_URL_PREFIX = "file://";
|
|
234
551
|
var bypassWarnEmitted = false;
|
|
235
552
|
function computeSelfModulePath() {
|
|
236
553
|
const raw = (0, node_url.fileURLToPath)({}.url);
|
|
@@ -245,7 +562,7 @@ function isPathSpecifier(source) {
|
|
|
245
562
|
return source.startsWith(".") || source.startsWith("/");
|
|
246
563
|
}
|
|
247
564
|
function defaultIsInsiderImportSource(source, configPath) {
|
|
248
|
-
if (source ===
|
|
565
|
+
if (source === "@useinsider/eslint-config") return true;
|
|
249
566
|
if (!configPath || !isPathSpecifier(source)) return false;
|
|
250
567
|
try {
|
|
251
568
|
const absolute = (0, node_path.resolve)((0, node_path.dirname)(configPath), source);
|
|
@@ -341,7 +658,7 @@ function validateProgramStructure(program, configPath = "", options = {}) {
|
|
|
341
658
|
const insiderBindings = /* @__PURE__ */ new Set();
|
|
342
659
|
const resolver = options.isInsiderImportSource ?? defaultIsInsiderImportSource;
|
|
343
660
|
const isInsiderSource = (source) => resolver(source, configPath);
|
|
344
|
-
const expectedCallee = options.expectedCallee ??
|
|
661
|
+
const expectedCallee = options.expectedCallee ?? "useInsiderESLint";
|
|
345
662
|
let exportSeen = false;
|
|
346
663
|
program.body.forEach((node) => {
|
|
347
664
|
if (node.type === _typescript_eslint_types.AST_NODE_TYPES.ImportDeclaration) collectInsiderBindings(node, insiderBindings, isInsiderSource);
|
|
@@ -357,7 +674,7 @@ function validateProgramStructure(program, configPath = "", options = {}) {
|
|
|
357
674
|
if (!exportSeen) violations.push({
|
|
358
675
|
line: 1,
|
|
359
676
|
column: 0,
|
|
360
|
-
reason: `Missing \`export default ${options.expectedCallee ??
|
|
677
|
+
reason: `Missing \`export default ${options.expectedCallee ?? "useInsiderESLint"}()\`.`
|
|
361
678
|
});
|
|
362
679
|
return violations;
|
|
363
680
|
}
|
|
@@ -521,24 +838,6 @@ function captureConsumerConfigDir(configPath) {
|
|
|
521
838
|
function getConsumerConfigDir() {
|
|
522
839
|
return consumerConfigDir ?? process.cwd();
|
|
523
840
|
}
|
|
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
841
|
function formatReport(configPath, violations) {
|
|
543
842
|
const header = ["@useinsider/eslint-config: protected configuration structure violated in:", ` ${configPath}`].join("\n");
|
|
544
843
|
const lines = violations.map((violation) => ` - [${violation.line}:${violation.column}] ${violation.reason}`);
|
|
@@ -549,7 +848,7 @@ function formatReport(configPath, violations) {
|
|
|
549
848
|
"",
|
|
550
849
|
EXPECTED_LAYOUT,
|
|
551
850
|
"",
|
|
552
|
-
`Emergency bypass: set ${OPT_OUT_ENV
|
|
851
|
+
`Emergency bypass: set ${OPT_OUT_ENV}=true (do not commit).`
|
|
553
852
|
].join("\n");
|
|
554
853
|
return `${header}\n${lines.join("\n")}\n${footer}`;
|
|
555
854
|
}
|
|
@@ -599,46 +898,48 @@ function validateFile(filePath, validator, options) {
|
|
|
599
898
|
if (violations.length === 0) return;
|
|
600
899
|
fail(formatReport(filePath, violations));
|
|
601
900
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
901
|
+
var MIGRATED_EXIT_NOTICE = [
|
|
902
|
+
"@useinsider/eslint-config: migration finished.",
|
|
903
|
+
"",
|
|
904
|
+
"Re-run your lint command. This process already loaded the old configuration, so continuing now",
|
|
905
|
+
"would lint against a config that no longer exists on disk."
|
|
906
|
+
].join("\n");
|
|
907
|
+
var DECLINED_DETAIL = ["The guided migration was not completed, so nothing was written."];
|
|
908
|
+
function failWithGuidance(headline, diagnosis, detail) {
|
|
909
|
+
return fail(formatLayoutGuidance({
|
|
910
|
+
headline,
|
|
911
|
+
diagnosis,
|
|
912
|
+
detail
|
|
913
|
+
}));
|
|
914
|
+
}
|
|
915
|
+
function handleOutdatedLayout(headline, diagnosis, offer) {
|
|
916
|
+
if (!diagnosis.autoMigratable) return failWithGuidance(headline, diagnosis);
|
|
917
|
+
const outcome = offer(diagnosis);
|
|
918
|
+
if (!outcome.offered) return failWithGuidance(headline, diagnosis);
|
|
919
|
+
if (!outcome.accepted) return failWithGuidance(headline, diagnosis, DECLINED_DETAIL);
|
|
920
|
+
return fail(MIGRATED_EXIT_NOTICE);
|
|
921
|
+
}
|
|
922
|
+
function runValidation(anchorPath, deps = {}) {
|
|
923
|
+
const diagnose = deps.diagnose ?? ((directory) => diagnoseLayout(directory));
|
|
924
|
+
const offer = deps.offer ?? offerMigration;
|
|
925
|
+
if (!anchorPath) handleOutdatedLayout("unable to locate the consumer `insider.lint.*` module for structure validation.", diagnose(process.cwd()), offer);
|
|
610
926
|
const directory = (0, node_path.dirname)((0, node_path.resolve)(anchorPath));
|
|
611
927
|
const lintModulePath = resolveSibling(directory, LINT_MODULE_FILENAMES);
|
|
612
|
-
if (!lintModulePath)
|
|
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"));
|
|
928
|
+
if (!lintModulePath) handleOutdatedLayout(`no \`insider.lint.*\` module found in ${directory}.`, diagnose(directory), offer);
|
|
622
929
|
validateFile(lintModulePath, validateLintModuleStructure, {});
|
|
623
930
|
const eslintConfigPath = resolveSibling(directory, ESLINT_CONFIG_FILENAMES);
|
|
624
|
-
if (!eslintConfigPath)
|
|
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"));
|
|
931
|
+
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
932
|
validateFile(eslintConfigPath, validateProgramStructure, { expectedCallee: ESLINT_RENDERER_NAME });
|
|
632
933
|
}
|
|
633
|
-
function ensureConfigStructure(overridePath) {
|
|
934
|
+
function ensureConfigStructure(overridePath, deps = {}) {
|
|
634
935
|
const configPath = overridePath === void 0 ? getConsumerConfigPath() : overridePath;
|
|
635
936
|
captureConsumerConfigDir(configPath);
|
|
636
|
-
if (process.env[
|
|
937
|
+
if (process.env["INSIDER_SKIP_CONFIG_VALIDATION"] === "true") {
|
|
637
938
|
emitBypassWarning();
|
|
638
939
|
return;
|
|
639
940
|
}
|
|
640
941
|
try {
|
|
641
|
-
runValidation(configPath);
|
|
942
|
+
runValidation(configPath, deps);
|
|
642
943
|
} catch (error) {
|
|
643
944
|
if (error instanceof ConfigStructureExit) return;
|
|
644
945
|
throw error;
|
|
@@ -762,6 +1063,14 @@ function clonePreset(preset) {
|
|
|
762
1063
|
function isScopeName(key) {
|
|
763
1064
|
return Object.keys(scopeMap).includes(key);
|
|
764
1065
|
}
|
|
1066
|
+
/**
|
|
1067
|
+
* Every scope name `useInsider()` accepts, for error messages that have to spell the options out.
|
|
1068
|
+
* @example
|
|
1069
|
+
* scopeNames().includes('typescriptNode'); // true
|
|
1070
|
+
*/
|
|
1071
|
+
function scopeNames() {
|
|
1072
|
+
return Object.keys(scopeMap);
|
|
1073
|
+
}
|
|
765
1074
|
function applyCommonEntryFields(block, entry) {
|
|
766
1075
|
if (entry.ignores && entry.ignores.length > 0) block.ignores = [...entry.ignores];
|
|
767
1076
|
if (entry.globals) {
|
|
@@ -881,31 +1190,27 @@ function silenceDependencyWarning(input) {
|
|
|
881
1190
|
}
|
|
882
1191
|
//#endregion
|
|
883
1192
|
//#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
1193
|
function formatMessage(directory) {
|
|
890
|
-
return
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1194
|
+
return formatLayoutGuidance({
|
|
1195
|
+
headline: `no oxlint config found in ${directory}.`,
|
|
1196
|
+
diagnosis: diagnoseLayout(directory),
|
|
1197
|
+
detail: [
|
|
1198
|
+
"useInsiderESLint() turns off every rule oxlint owns, so running ESLint alone would silently",
|
|
1199
|
+
"drop those checks. Create the sibling oxlint config:",
|
|
1200
|
+
"",
|
|
1201
|
+
" // oxlint.config.ts",
|
|
1202
|
+
` import { useInsiderOXC } from '${lintModuleSpecifier(directory)}';`,
|
|
1203
|
+
"",
|
|
1204
|
+
" export default await useInsiderOXC();",
|
|
1205
|
+
"",
|
|
1206
|
+
"and run both linters:",
|
|
1207
|
+
"",
|
|
1208
|
+
" \"lint\": \"oxlint --disable-nested-config && eslint .\"",
|
|
1209
|
+
"",
|
|
1210
|
+
`oxlint discovers only ${OXLINT_CONFIG_FILENAMES.join(" and ")} — any other extension is`,
|
|
1211
|
+
"ignored without a word."
|
|
1212
|
+
]
|
|
1213
|
+
});
|
|
909
1214
|
}
|
|
910
1215
|
/**
|
|
911
1216
|
* Fails when the consumer enabled the ESLint-side oxlint suppression without a sibling oxlint
|
|
@@ -915,7 +1220,7 @@ function formatMessage(directory) {
|
|
|
915
1220
|
* ensureOxlintConfig(); // throws unless oxlint.config.{ts,mts} sits beside insider.lint.*
|
|
916
1221
|
*/
|
|
917
1222
|
function ensureOxlintConfig(directory = getConsumerConfigDir()) {
|
|
918
|
-
if (process.env[
|
|
1223
|
+
if (process.env["INSIDER_SKIP_CONFIG_VALIDATION"] === "true") return;
|
|
919
1224
|
if (OXLINT_CONFIG_FILENAMES.some((filename) => (0, node_fs.existsSync)((0, node_path.join)(directory, filename)))) return;
|
|
920
1225
|
throw new Error(formatMessage(directory));
|
|
921
1226
|
}
|
|
@@ -980,6 +1285,11 @@ function assertNoRemovedKeys(config) {
|
|
|
980
1285
|
const details = used.map((key) => ` - \`${key}\`: ${removedKeys.get(key)}`).join("\n");
|
|
981
1286
|
throw new Error(`@useinsider/eslint-config: removed option(s) passed to useInsider():\n${details}`);
|
|
982
1287
|
}
|
|
1288
|
+
function assertNoUnknownKeys(config) {
|
|
1289
|
+
const unknown = Object.keys(config).filter((key) => !reservedKeys.has(key) && !removedKeys.has(key) && !isScopeName(key));
|
|
1290
|
+
if (unknown.length === 0) return;
|
|
1291
|
+
throw new Error(`@useinsider/eslint-config: unknown option(s) passed to useInsider(): ${unknown.join(", ")}.\n Scopes: ${scopeNames().join(", ")}\n Other options: ${[...reservedKeys].join(", ")}`);
|
|
1292
|
+
}
|
|
983
1293
|
function isProcessableScopeKey(key) {
|
|
984
1294
|
return !reservedKeys.has(key) && isScopeName(key);
|
|
985
1295
|
}
|
|
@@ -1032,6 +1342,7 @@ async function renderOxlint(config) {
|
|
|
1032
1342
|
}
|
|
1033
1343
|
function useInsider(config) {
|
|
1034
1344
|
assertNoRemovedKeys(config);
|
|
1345
|
+
assertNoUnknownKeys(config);
|
|
1035
1346
|
ensureConfigStructure();
|
|
1036
1347
|
return {
|
|
1037
1348
|
useInsiderESLint: () => renderEslint(config),
|