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