@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.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,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 ===
|
|
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 ??
|
|
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 ??
|
|
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
|
|
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
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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)
|
|
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)
|
|
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[
|
|
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
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
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[
|
|
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),
|