@useinsider/eslint-config 4.0.0-beta.2 → 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/dist/cli.cjs CHANGED
@@ -741,6 +741,22 @@ async function installWithRetry(opts) {
741
741
  }
742
742
  }
743
743
  //#endregion
744
+ //#region src/cli/detectIndent.ts
745
+ var DEFAULT_INDENT = " ";
746
+ /**
747
+ * The indentation a JSON source already uses, as the string `JSON.stringify` takes for its third
748
+ * argument. A width would be wrong: `JSON.stringify` emits that many *spaces*, so a tab-indented
749
+ * manifest comes back re-indented with spaces and every line of it shows up in the diff.
750
+ * @example
751
+ * detectIndent('{\n\t"name": "app"\n}'); // '\t'
752
+ * @example
753
+ * detectIndent('{"name":"app"}', ' '); // ' '
754
+ */
755
+ function detectIndent(raw, fallback = DEFAULT_INDENT) {
756
+ const match = /\n([\t ]+)\S/u.exec(raw);
757
+ return match ? match[1] : fallback;
758
+ }
759
+ //#endregion
744
760
  //#region src/cli/lintLayout.ts
745
761
  var OXLINT_COMMAND = "oxlint --disable-nested-config";
746
762
  var OXLINT_LINT_SCRIPT = `${OXLINT_COMMAND} && eslint .`;
@@ -842,14 +858,7 @@ function renderLintLayoutFiles(layout) {
842
858
  source: renderOxlintConfigFile(layout.lintModule)
843
859
  }];
844
860
  }
845
- var DEFAULT_INDENT = 2;
846
- function detectIndent$1(raw) {
847
- const firstBreak = raw.indexOf("\n");
848
- if (firstBreak === -1) return DEFAULT_INDENT;
849
- const rest = raw.slice(firstBreak + 1);
850
- const width = rest.length - rest.trimStart().length;
851
- return width > 0 ? width : DEFAULT_INDENT;
852
- }
861
+ var DEFAULT_MANIFEST_INDENT = " ";
853
862
  var SEGMENT_SEPARATOR = /&&|\|\||[;|]/u;
854
863
  var ASSIGNMENT = /^[A-Za-z_]\w*=/u;
855
864
  var WRAPPER_COMMANDS = /* @__PURE__ */ new Set([
@@ -1003,7 +1012,7 @@ function setLintScript(rawManifest) {
1003
1012
  parsed.scripts[change.script] = change.next;
1004
1013
  });
1005
1014
  return {
1006
- manifest: `${JSON.stringify(parsed, null, detectIndent$1(rawManifest))}\n`,
1015
+ manifest: `${JSON.stringify(parsed, null, detectIndent(rawManifest, DEFAULT_MANIFEST_INDENT))}\n`,
1007
1016
  changed: true,
1008
1017
  changes,
1009
1018
  alreadyRunsOxlint,
@@ -1785,6 +1794,18 @@ var ALL_PRESET_DEPS = [
1785
1794
  ...Object.values(SCOPE_TO_DEPS)
1786
1795
  ];
1787
1796
  var PRESET_DEPENDENCY_KEYS = new Set(ALL_PRESET_DEPS.flatMap((record) => Object.keys(record)));
1797
+ var MIGRATABLE_SCOPES = new Set(Object.keys(SCOPE_TO_DEPS));
1798
+ /**
1799
+ * Whether a `useInsider()` option key is a scope this migration can derive dependencies for. The
1800
+ * four `*OptionsApi` scopes are deliberately absent from the registry, so a config using one is
1801
+ * carried over untouched while its dependencies are reported as unresolved.
1802
+ * @example
1803
+ * isMigratableScope('vue3Typescript'); // true
1804
+ * isMigratableScope('vue3OptionsApi'); // false
1805
+ */
1806
+ function isMigratableScope(key) {
1807
+ return MIGRATABLE_SCOPES.has(key);
1808
+ }
1788
1809
  function isEslintRelatedDependency(name) {
1789
1810
  if (SAFELIST.has(name)) return false;
1790
1811
  if (EXACT_PATTERNS.has(name)) return true;
@@ -1899,11 +1920,6 @@ function buildInstallList(scopes, manifest) {
1899
1920
  var readOwnEslintConfigManifest = readOwnEslintConfigManifest$1;
1900
1921
  //#endregion
1901
1922
  //#region src/cli/prunePackageJson.ts
1902
- function detectIndent(raw) {
1903
- const match = /\n([ \t]+)"/u.exec(raw);
1904
- if (!match) return " ";
1905
- return match[1];
1906
- }
1907
1923
  function serializeManifest(manifest, indent) {
1908
1924
  return `${JSON.stringify(manifest, null, indent)}\n`;
1909
1925
  }
@@ -1952,7 +1968,6 @@ var TS_EXTENSIONS_SNIPPET = [
1952
1968
  " }",
1953
1969
  "}"
1954
1970
  ].join("\n");
1955
- var DEFAULT_INDENT_UNIT = " ";
1956
1971
  function acceptsTsExtensionsFlag(options) {
1957
1972
  return options.noEmit === true || options.emitDeclarationOnly === true || options.rewriteRelativeImportExtensions === true;
1958
1973
  }
@@ -2049,10 +2064,6 @@ function indentAt(raw, position) {
2049
2064
  const prefix = raw.slice(lineStart, position);
2050
2065
  return prefix.trim().length === 0 ? prefix : "";
2051
2066
  }
2052
- function detectIndentUnit(raw) {
2053
- const match = /\n([\t ]+)\S/u.exec(raw);
2054
- return match ? match[1] : DEFAULT_INDENT_UNIT;
2055
- }
2056
2067
  function splice(raw, start, end, text) {
2057
2068
  return raw.slice(0, start) + text + raw.slice(end);
2058
2069
  }
@@ -2062,14 +2073,14 @@ function setExistingFlag(raw, source, property) {
2062
2073
  function insertIntoCompilerOptions(raw, source, compilerOptions, object) {
2063
2074
  const openBrace = object.getStart(source) + 1;
2064
2075
  const [first] = object.properties;
2065
- if (first) return splice(raw, openBrace, openBrace, `\n${indentAt(raw, first.getStart(source)) || detectIndentUnit(raw).repeat(2)}"${TS_EXTENSIONS_FLAG}": true,`);
2076
+ if (first) return splice(raw, openBrace, openBrace, `\n${indentAt(raw, first.getStart(source)) || detectIndent(raw).repeat(2)}"${TS_EXTENSIONS_FLAG}": true,`);
2066
2077
  const outer = indentAt(raw, compilerOptions.getStart(source));
2067
- const unit = detectIndentUnit(raw);
2078
+ const unit = detectIndent(raw);
2068
2079
  return splice(raw, openBrace, object.getEnd() - 1, `\n${outer}${unit}"${TS_EXTENSIONS_FLAG}": true\n${outer}`);
2069
2080
  }
2070
2081
  function insertCompilerOptions(raw, source, root) {
2071
2082
  const openBrace = root.getStart(source) + 1;
2072
- const unit = detectIndentUnit(raw);
2083
+ const unit = detectIndent(raw);
2073
2084
  const [first] = root.properties;
2074
2085
  const indent = first ? indentAt(raw, first.getStart(source)) || unit : unit;
2075
2086
  const block = `\n${indent}"compilerOptions": {\n${indent}${unit}"${TS_EXTENSIONS_FLAG}": true\n${indent}}`;
@@ -2354,35 +2365,75 @@ function collectFiles(directory, accumulator, visited) {
2354
2365
  if (entry.isFile() && isLintedFile(full)) accumulator.push(full);
2355
2366
  });
2356
2367
  }
2368
+ function readSource(filePath) {
2369
+ try {
2370
+ return (0, node_fs.readFileSync)(filePath, "utf8");
2371
+ } catch {
2372
+ return null;
2373
+ }
2374
+ }
2357
2375
  /**
2358
2376
  * Every file whose `eslint-disable` comments would be re-pointed, and the rewritten source, without
2359
2377
  * touching disk. The directive rewrite is the largest diff a migration produces, so the count is
2360
- * reported — and confirmed — before anything is written.
2378
+ * reported — and confirmed — before anything is written. Files that could not be read come back in
2379
+ * `skipped` instead of aborting the walk.
2361
2380
  * @example
2362
- * previewDisableDirectives('/repo').length; // 131
2381
+ * previewDisableDirectives('/repo').candidates.length; // 131
2363
2382
  */
2364
2383
  function previewDisableDirectives(cwd) {
2365
2384
  const files = [];
2385
+ const candidates = [];
2386
+ const skipped = [];
2366
2387
  collectFiles(cwd, files, /* @__PURE__ */ new Set());
2367
- return files.reduce((candidates, filePath) => {
2368
- const result = rewriteDirectives((0, node_fs.readFileSync)(filePath, "utf8"), (0, node_path.extname)(filePath));
2369
- if (!result.changed) return candidates;
2370
- return [...candidates, {
2388
+ files.forEach((filePath) => {
2389
+ const source = readSource(filePath);
2390
+ if (source === null) {
2391
+ skipped.push(filePath);
2392
+ return;
2393
+ }
2394
+ const result = rewriteDirectives(source, (0, node_path.extname)(filePath));
2395
+ if (result.changed) candidates.push({
2371
2396
  path: filePath,
2372
2397
  source: result.source
2373
- }];
2374
- }, []);
2398
+ });
2399
+ });
2400
+ return {
2401
+ candidates,
2402
+ skipped
2403
+ };
2404
+ }
2405
+ /**
2406
+ * What the consumer is told about the files the rewrite could not touch: the paths, then why it
2407
+ * matters and how to finish the job. Nothing else in the migration is affected, and saying so is the
2408
+ * point — the layout was already written by the time the walk ran.
2409
+ * @example
2410
+ * formatUntouchedDirectiveFiles(['/repo/src/vendor.ts']); // '/repo/src/vendor.ts\n\nThese could not…'
2411
+ */
2412
+ function formatUntouchedDirectiveFiles(skipped) {
2413
+ return `${skipped.join("\n")}\n\nThese could not be read or written, so their \`eslint-disable\` comments still name rules oxlint now owns and ESLint will report each one as an unused directive. Everything else was migrated. Fix the access and re-run \`npx @useinsider/eslint-config migrate\` to pick them up.`;
2375
2414
  }
2376
2415
  /**
2377
2416
  * Applies {@link rewriteDirectives} across a consumer tree, skipping build output and dependencies.
2417
+ * A candidate whose write fails comes back in `skipped`, so one failed write costs its own
2418
+ * directives rather than every later file's.
2378
2419
  * @example
2379
2420
  * rewriteDisableDirectives('/repo').rewritten; // ['/repo/src/logger.ts']
2380
2421
  */
2381
- function rewriteDisableDirectives(cwd, candidates = previewDisableDirectives(cwd)) {
2422
+ function rewriteDisableDirectives(cwd, candidates = previewDisableDirectives(cwd).candidates) {
2423
+ const rewritten = [];
2424
+ const skipped = [];
2382
2425
  candidates.forEach((candidate) => {
2383
- (0, node_fs.writeFileSync)(candidate.path, candidate.source, "utf8");
2426
+ try {
2427
+ (0, node_fs.writeFileSync)(candidate.path, candidate.source, "utf8");
2428
+ rewritten.push(candidate.path);
2429
+ } catch {
2430
+ skipped.push(candidate.path);
2431
+ }
2384
2432
  });
2385
- return { rewritten: candidates.map((candidate) => candidate.path) };
2433
+ return {
2434
+ rewritten,
2435
+ skipped
2436
+ };
2386
2437
  }
2387
2438
  //#endregion
2388
2439
  //#region src/cli/migrate/stringEntry.ts
@@ -2411,7 +2462,7 @@ function leadingCommentTextFor(sourceFile, node) {
2411
2462
  }
2412
2463
  //#endregion
2413
2464
  //#region src/cli/migrate/eslintrc/parse.ts
2414
- function lineOf$1(sourceFile, node) {
2465
+ function lineOf$2(sourceFile, node) {
2415
2466
  const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
2416
2467
  return line + 1;
2417
2468
  }
@@ -2705,7 +2756,7 @@ function readSharedBlockFields(input) {
2705
2756
  };
2706
2757
  }
2707
2758
  function readOverride(object, context, elementNode) {
2708
- const line = lineOf$1(context.sourceFile, object);
2759
+ const line = lineOf$2(context.sourceFile, object);
2709
2760
  const filesProperty = findProperty$1(object, "files");
2710
2761
  const files = filesProperty ? readStringOrEntries(filesProperty.initializer, context.sourceFile) : [];
2711
2762
  if (files.length === 0) {
@@ -2762,7 +2813,7 @@ function findExportObject(sourceFile) {
2762
2813
  return result;
2763
2814
  }
2764
2815
  function buildRoot(object, context) {
2765
- const line = lineOf$1(context.sourceFile, object);
2816
+ const line = lineOf$2(context.sourceFile, object);
2766
2817
  const shared = readSharedBlockFields({
2767
2818
  object,
2768
2819
  line,
@@ -3392,7 +3443,7 @@ var knownPresets = /* @__PURE__ */ new Set([
3392
3443
  "dom",
3393
3444
  "node"
3394
3445
  ]);
3395
- function lineOf(sourceFile, node) {
3446
+ function lineOf$1(sourceFile, node) {
3396
3447
  const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
3397
3448
  return line + 1;
3398
3449
  }
@@ -3695,7 +3746,7 @@ function classifyPresets(presetList, line, warnings) {
3695
3746
  };
3696
3747
  }
3697
3748
  function parseUseInsiderCall(callExpression, sourceFile, warnings) {
3698
- const line = lineOf(sourceFile, callExpression);
3749
+ const line = lineOf$1(sourceFile, callExpression);
3699
3750
  const [firstArgument] = callExpression.arguments;
3700
3751
  if (!firstArgument || !typescript.isObjectLiteralExpression(firstArgument)) {
3701
3752
  warnings.push({
@@ -3795,7 +3846,7 @@ function tryClassifyStandaloneGlobals(objectLiteral, keys, line, sourceFile) {
3795
3846
  };
3796
3847
  }
3797
3848
  function classifyObjectElement(objectLiteral, sourceFile, warnings) {
3798
- const line = lineOf(sourceFile, objectLiteral);
3849
+ const line = lineOf$1(sourceFile, objectLiteral);
3799
3850
  const keys = objectLiteral.properties.map(propertyKeyName).filter((name) => name !== null);
3800
3851
  const ignoresBlock = tryClassifyIgnoresBlock(objectLiteral, keys, line, sourceFile);
3801
3852
  if (ignoresBlock) return ignoresBlock;
@@ -3832,7 +3883,7 @@ function classifyArrayElement(element, sourceFile, warnings) {
3832
3883
  if (leadingComment !== void 0) parsed.leadingComment = leadingComment;
3833
3884
  return parsed;
3834
3885
  }
3835
- const line = lineOf(sourceFile, element);
3886
+ const line = lineOf$1(sourceFile, element);
3836
3887
  warnings.push({
3837
3888
  kind: "unrecognized-block",
3838
3889
  line,
@@ -3849,18 +3900,19 @@ function classifyArrayElement(element, sourceFile, warnings) {
3849
3900
  function isDefineConfigCall(expression) {
3850
3901
  return typescript.isCallExpression(expression) && typescript.isIdentifier(expression.expression) && expression.expression.text === "defineConfig";
3851
3902
  }
3852
- function findDefaultExportArray(sourceFile) {
3853
- const exportExpressions = sourceFile.statements.filter(typescript.isExportAssignment).map((statement) => unwrapTypeCasts(statement.expression));
3854
- const directArray = exportExpressions.find(typescript.isArrayLiteralExpression);
3855
- if (directArray) return directArray;
3856
- const defineConfigCall = exportExpressions.find(isDefineConfigCall);
3857
- if (!defineConfigCall) return null;
3858
- const [first] = defineConfigCall.arguments;
3903
+ function defineConfigArray(exportExpressions) {
3904
+ const [first] = exportExpressions.find(isDefineConfigCall)?.arguments ?? [];
3859
3905
  if (!first) return null;
3860
3906
  const unwrappedFirst = unwrapTypeCasts(first);
3861
3907
  if (typescript.isArrayLiteralExpression(unwrappedFirst)) return unwrappedFirst;
3862
3908
  return null;
3863
3909
  }
3910
+ function findDefaultExportElements(sourceFile) {
3911
+ const exportExpressions = sourceFile.statements.filter(typescript.isExportAssignment).map((statement) => unwrapTypeCasts(statement.expression));
3912
+ const array = exportExpressions.find(typescript.isArrayLiteralExpression) ?? defineConfigArray(exportExpressions);
3913
+ if (array) return [...array.elements];
3914
+ return exportExpressions;
3915
+ }
3864
3916
  function checkDefineConfigWrap(sourceFile) {
3865
3917
  return sourceFile.statements.some((statement) => {
3866
3918
  if (!typescript.isExportAssignment(statement)) return false;
@@ -3899,7 +3951,7 @@ function classifyInsiderImport(statement, sourceFile, flags) {
3899
3951
  return;
3900
3952
  }
3901
3953
  flags.hasUseInsider = true;
3902
- const line = lineOf(sourceFile, statement);
3954
+ const line = lineOf$1(sourceFile, statement);
3903
3955
  flags.preserved.push({
3904
3956
  raw: statement.getText(sourceFile),
3905
3957
  moduleSpecifier: "@useinsider/eslint-config"
@@ -3944,7 +3996,7 @@ function collectSilenceRulesCalls(sourceFile) {
3944
3996
  if (!rules) return null;
3945
3997
  const leadingComment = leadingCommentTextFor(sourceFile, statement);
3946
3998
  const call = {
3947
- line: lineOf(sourceFile, statement),
3999
+ line: lineOf$1(sourceFile, statement),
3948
4000
  rules
3949
4001
  };
3950
4002
  if (leadingComment !== void 0) call.leadingComment = leadingComment;
@@ -3957,8 +4009,7 @@ function parseV1Config(source, filePath) {
3957
4009
  const imports = inspectImports(sourceFile);
3958
4010
  warnings.push(...imports.warnings);
3959
4011
  const silenceRulesCalls = collectSilenceRulesCalls(sourceFile);
3960
- const arrayLiteral = findDefaultExportArray(sourceFile);
3961
- const arrayElements = arrayLiteral ? arrayLiteral.elements.map((element) => classifyArrayElement(element, sourceFile, warnings)).filter((entry) => entry !== null) : [];
4012
+ const arrayElements = findDefaultExportElements(sourceFile).map((element) => classifyArrayElement(element, sourceFile, warnings)).filter((entry) => entry !== null);
3962
4013
  return {
3963
4014
  hasUseInsiderImport: imports.hasUseInsider,
3964
4015
  hasSilenceRulesImport: imports.hasSilenceRules,
@@ -3970,6 +4021,220 @@ function parseV1Config(source, filePath) {
3970
4021
  };
3971
4022
  }
3972
4023
  //#endregion
4024
+ //#region src/cli/migrate/rehouseV3.ts
4025
+ var INSIDER_MODULE = "@useinsider/eslint-config";
4026
+ var COMPOSER_NAME = "useInsider";
4027
+ var DEFINE_CONFIG_NAME = "defineConfig";
4028
+ var SILENCE_RULES_NAME = "silenceRules";
4029
+ var RENDERER_EXPORT = "export const { useInsiderESLint, useInsiderOXC } = ";
4030
+ var RESERVED_KEYS = /* @__PURE__ */ new Set([
4031
+ "ignores",
4032
+ "jquery",
4033
+ "strict",
4034
+ "spellChecker",
4035
+ "silenceRules",
4036
+ "configFiles"
4037
+ ]);
4038
+ function isInsiderImport(statement) {
4039
+ const { moduleSpecifier } = statement;
4040
+ return typescript.isStringLiteral(moduleSpecifier) && moduleSpecifier.text === INSIDER_MODULE;
4041
+ }
4042
+ function composerAliases(sourceFile) {
4043
+ const aliases = /* @__PURE__ */ new Set();
4044
+ sourceFile.statements.forEach((statement) => {
4045
+ if (!typescript.isImportDeclaration(statement) || !isInsiderImport(statement)) return;
4046
+ const bindings = statement.importClause?.namedBindings;
4047
+ if (!bindings || !typescript.isNamedImports(bindings)) return;
4048
+ bindings.elements.forEach((element) => {
4049
+ if ((element.propertyName?.text ?? element.name.text) === COMPOSER_NAME) aliases.add(element.name.text);
4050
+ });
4051
+ });
4052
+ return aliases;
4053
+ }
4054
+ function isCallOf(node, names) {
4055
+ return typescript.isCallExpression(node) && typescript.isIdentifier(node.expression) && names.has(node.expression.text);
4056
+ }
4057
+ function collectCalls(sourceFile, names) {
4058
+ const found = [];
4059
+ const visit = (node) => {
4060
+ if (isCallOf(node, names)) found.push(node);
4061
+ typescript.forEachChild(node, visit);
4062
+ };
4063
+ visit(sourceFile);
4064
+ return found;
4065
+ }
4066
+ function peel(expression, state) {
4067
+ if (typescript.isParenthesizedExpression(expression) || typescript.isSpreadElement(expression)) return peel(expression.expression, state);
4068
+ if (typescript.isAsExpression(expression) || typescript.isSatisfiesExpression(expression)) {
4069
+ state.droppedAnnotation = true;
4070
+ return peel(expression.expression, state);
4071
+ }
4072
+ if (typescript.isAwaitExpression(expression)) {
4073
+ state.awaited = true;
4074
+ return peel(expression.expression, state);
4075
+ }
4076
+ return expression;
4077
+ }
4078
+ function peelArray(expression, state) {
4079
+ if (!typescript.isArrayLiteralExpression(expression) || expression.elements.length === 0) return expression;
4080
+ state.arrayWrapped = true;
4081
+ state.siblingElements = expression.elements.length - 1;
4082
+ return peel(expression.elements[0], state);
4083
+ }
4084
+ function unwrapDefaultExport(expression, state) {
4085
+ const peeled = peel(expression, state);
4086
+ if (isCallOf(peeled, /* @__PURE__ */ new Set([DEFINE_CONFIG_NAME]))) {
4087
+ state.definedConfig = true;
4088
+ return peeled;
4089
+ }
4090
+ return peelArray(peeled, state);
4091
+ }
4092
+ function argumentObject(call) {
4093
+ const [first] = call.arguments;
4094
+ if (!first || !typescript.isObjectLiteralExpression(first)) return null;
4095
+ return first;
4096
+ }
4097
+ function propertyKey(property) {
4098
+ const { name } = property;
4099
+ if (name === void 0) return null;
4100
+ if (typescript.isIdentifier(name) || typescript.isStringLiteral(name)) return name.text;
4101
+ return null;
4102
+ }
4103
+ function unreadableReason(argument, keysRead) {
4104
+ if (!argument) return "is not an object literal";
4105
+ if (argument.properties.some(typescript.isSpreadAssignment)) return "spreads another object";
4106
+ return keysRead < argument.properties.length ? "has a property whose key is not a literal name" : null;
4107
+ }
4108
+ function readArgument(call) {
4109
+ const argument = argumentObject(call);
4110
+ const optionKeys = (argument?.properties ?? []).map(propertyKey).filter((key) => key !== null);
4111
+ const candidates = optionKeys.filter((key) => !RESERVED_KEYS.has(key));
4112
+ return {
4113
+ optionKeys,
4114
+ scopes: candidates.filter(isMigratableScope),
4115
+ unregisteredKeys: candidates.filter((key) => !isMigratableScope(key)),
4116
+ unreadable: unreadableReason(argument, optionKeys.length)
4117
+ };
4118
+ }
4119
+ function refuse(reason) {
4120
+ return {
4121
+ kind: "refused",
4122
+ reason
4123
+ };
4124
+ }
4125
+ function multipleCallsReason(name) {
4126
+ return `${name} calls ${COMPOSER_NAME}() more than once. v4 has exactly one call — in the lint module — and it returns the two renderers instead of a config array, so the results cannot be concatenated. Merge them into a single call, one entry per scope key, then re-run the migration.`;
4127
+ }
4128
+ function mixedArrayReason(name, siblings) {
4129
+ return `${name}'s default export combines ${COMPOSER_NAME}() with ${siblings} other flat-config ${siblings === 1 ? "block" : "blocks"}, which have nowhere to go in a lint module. Fold them into the call first — directory ignores into \`ignores\`, per-file rule overrides into \`configFiles\` — then re-run the migration.`;
4130
+ }
4131
+ function indirectCallReason(name) {
4132
+ return `${name} calls ${COMPOSER_NAME}(), but its default export is not that call. The migration re-houses the call itself, so make it the default export (\`export default ${COMPOSER_NAME}({ ... })\`) and re-run the migration.`;
4133
+ }
4134
+ function defineConfigReason(name) {
4135
+ return `${name} wraps ${COMPOSER_NAME}() in ${DEFINE_CONFIG_NAME}(). v4's lint module exports the two renderers rather than a flat-config array, so there is nothing left for ${DEFINE_CONFIG_NAME}() to validate — drop the wrapper and re-run the migration.`;
4136
+ }
4137
+ function presetReason(name) {
4138
+ return `${name} still passes the v1 \`preset\` option, and this release could not read it — the preset list has to be an array of string literals. Restore the literal list and re-run the migration, or convert the call to the scope-keyed form by hand.`;
4139
+ }
4140
+ function annotationWarning(line) {
4141
+ return {
4142
+ kind: "dropped-type-annotation",
4143
+ line,
4144
+ message: `The type annotation on the default export was dropped — ${COMPOSER_NAME}() now returns \`{ useInsiderESLint, useInsiderOXC }\` rather than a config array. Delete the type import if nothing else uses it.`
4145
+ };
4146
+ }
4147
+ function unregisteredScopeWarning(key, line) {
4148
+ return {
4149
+ kind: "unregistered-scope",
4150
+ line,
4151
+ message: `\`${key}\` has no dependency manifest in the migration, so nothing was added to the install list for it. The key itself was carried over untouched — check its plugins are installed.`
4152
+ };
4153
+ }
4154
+ function unreadableArgumentWarning(line, reason) {
4155
+ return {
4156
+ kind: "unrecognized-block",
4157
+ line,
4158
+ message: `The ${COMPOSER_NAME}() argument ${reason}, so its scope list could not be read in full. The call was carried over untouched; the dependency list may be incomplete.`
4159
+ };
4160
+ }
4161
+ function lineOf(sourceFile, node) {
4162
+ return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
4163
+ }
4164
+ function collectWarnings(line, reading, state) {
4165
+ const warnings = [];
4166
+ if (state.droppedAnnotation) warnings.push(annotationWarning(line));
4167
+ if (reading.unreadable) warnings.push(unreadableArgumentWarning(line, reading.unreadable));
4168
+ reading.unregisteredKeys.forEach((key) => {
4169
+ warnings.push(unregisteredScopeWarning(key, line));
4170
+ });
4171
+ return warnings;
4172
+ }
4173
+ function spliceExport(input) {
4174
+ const start = input.statement.getStart(input.sourceFile);
4175
+ const prefix = input.source.slice(0, start);
4176
+ const suffix = input.source.slice(input.statement.getEnd());
4177
+ const rehoused = `${prefix}${RENDERER_EXPORT}${input.callText};${suffix}`;
4178
+ return rehoused.endsWith("\n") ? rehoused : `${rehoused}\n`;
4179
+ }
4180
+ function defaultExports(sourceFile) {
4181
+ return sourceFile.statements.filter(typescript.isExportAssignment).filter((statement) => statement.isExportEquals !== true);
4182
+ }
4183
+ function emptyState() {
4184
+ return {
4185
+ awaited: false,
4186
+ arrayWrapped: false,
4187
+ droppedAnnotation: false,
4188
+ siblingElements: 0,
4189
+ definedConfig: false
4190
+ };
4191
+ }
4192
+ /**
4193
+ * Re-houses a v3 single-file config as a v4 lint module. Nothing about the configuration itself
4194
+ * changes between the two majors — only where the call lives — so the source is spliced rather than
4195
+ * re-rendered: every import, `silenceRules(...)` call, comment and blank line outside the export
4196
+ * statement survives byte-for-byte.
4197
+ * @example
4198
+ * rehouseV3Config("import { useInsider } from '@useinsider/eslint-config';\nexport default useInsider({});", 'eslint.config.js');
4199
+ * // { kind: 'rehoused', rehoused: { lintModule: '…export const { useInsiderESLint, useInsiderOXC } = useInsider({});\n', … } }
4200
+ */
4201
+ function rehouseV3Config(source, filePath) {
4202
+ const name = (0, node_path.basename)(filePath);
4203
+ const sourceFile = typescript.createSourceFile(filePath, source, typescript.ScriptTarget.Latest, true);
4204
+ const aliases = composerAliases(sourceFile);
4205
+ const calls = collectCalls(sourceFile, aliases);
4206
+ const exportStatements = defaultExports(sourceFile);
4207
+ if (calls.length === 0) return { kind: "not-v3" };
4208
+ if (exportStatements.length !== 1) return refuse(indirectCallReason(name));
4209
+ if (calls.length > 1) return refuse(multipleCallsReason(name));
4210
+ const [statement] = exportStatements;
4211
+ const state = emptyState();
4212
+ const unwrapped = unwrapDefaultExport(statement.expression, state);
4213
+ if (state.definedConfig) return refuse(defineConfigReason(name));
4214
+ if (state.siblingElements > 0) return refuse(mixedArrayReason(name, state.siblingElements));
4215
+ if (!isCallOf(unwrapped, aliases)) return refuse(indirectCallReason(name));
4216
+ const reading = readArgument(unwrapped);
4217
+ if (reading.optionKeys.includes("preset")) return refuse(presetReason(name));
4218
+ return {
4219
+ kind: "rehoused",
4220
+ rehoused: {
4221
+ lintModule: spliceExport({
4222
+ source,
4223
+ sourceFile,
4224
+ statement,
4225
+ callText: unwrapped.getText(sourceFile)
4226
+ }),
4227
+ scopes: reading.scopes,
4228
+ optionKeys: reading.optionKeys,
4229
+ unregisteredKeys: reading.unregisteredKeys,
4230
+ silenceRulesCallCount: collectCalls(sourceFile, /* @__PURE__ */ new Set([SILENCE_RULES_NAME])).length,
4231
+ droppedAwait: state.awaited,
4232
+ droppedArrayWrapper: state.arrayWrapped,
4233
+ warnings: collectWarnings(lineOf(sourceFile, unwrapped), reading, state)
4234
+ }
4235
+ };
4236
+ }
4237
+ //#endregion
3973
4238
  //#region src/cli/migrate/pruneUnusedImports.ts
3974
4239
  var INSIDER_MODULE_SPECIFIER = "@useinsider/eslint-config";
3975
4240
  function readModuleSpecifier(statement) {
@@ -4800,6 +5065,22 @@ function describeStrict(plan) {
4800
5065
  if (plan.config.strictGlobs.length > 0) return `Strict scopes: 1 (${plan.config.strictGlobs.length} globs)`;
4801
5066
  return "Strict scopes: 0";
4802
5067
  }
5068
+ function describeRehousedKeys(label, keys) {
5069
+ return keys.length === 0 ? `${label}: (none)` : `${label}: ${keys.join(", ")}`;
5070
+ }
5071
+ function rehouseSummary(rehoused) {
5072
+ const lines = [
5073
+ "Source: v3 single-file eslint.config.*",
5074
+ "Re-housed as the lint module: only the export statement changed, every other line is byte-for-byte.",
5075
+ describeRehousedKeys("Options carried over", rehoused.optionKeys),
5076
+ describeRehousedKeys("Scopes with dependencies", rehoused.scopes),
5077
+ `silenceRules() calls kept: ${rehoused.silenceRulesCallCount}`
5078
+ ];
5079
+ if (rehoused.droppedAwait) lines.push("Dropped the `await` on the call — useInsider() is synchronous in v4.");
5080
+ if (rehoused.droppedArrayWrapper) lines.push("Dropped the array wrapper around the call — the lint module exports the renderers.");
5081
+ lines.push(`Warnings: ${rehoused.warnings.length}`);
5082
+ return lines.join("\n");
5083
+ }
4803
5084
  function summary(plan, format) {
4804
5085
  const lines = [];
4805
5086
  if (format === "eslintrc") lines.push("Source: legacy .eslintrc");
@@ -4812,13 +5093,47 @@ function summary(plan, format) {
4812
5093
  lines.push(`Warnings: ${plan.warnings.length}`);
4813
5094
  return lines.join("\n");
4814
5095
  }
4815
- function planFromSource(source, filePath, format) {
4816
- if (format === "flat") {
4817
- const parseResult = parseV1Config(source, filePath);
4818
- return isV1Shape(parseResult) ? transformToV2(parseResult) : null;
5096
+ function sourceModelFrom(resolved) {
5097
+ if (resolved.format !== "flat") {
5098
+ const parseResult = parseEslintrc(resolved.source, resolved.targetPath);
5099
+ return parseResult.found ? {
5100
+ ok: true,
5101
+ model: {
5102
+ kind: "plan",
5103
+ plan: transformEslintrcToV2(parseResult)
5104
+ }
5105
+ } : {
5106
+ ok: false,
5107
+ reason: null
5108
+ };
4819
5109
  }
4820
- const parseResult = parseEslintrc(source, filePath);
4821
- return parseResult.found ? transformEslintrcToV2(parseResult) : null;
5110
+ const parseResult = parseV1Config(resolved.source, resolved.targetPath);
5111
+ if (isV1Shape(parseResult)) return {
5112
+ ok: true,
5113
+ model: {
5114
+ kind: "plan",
5115
+ plan: transformToV2(parseResult)
5116
+ }
5117
+ };
5118
+ const outcome = rehouseV3Config(resolved.source, resolved.targetPath);
5119
+ if (outcome.kind === "rehoused") return {
5120
+ ok: true,
5121
+ model: {
5122
+ kind: "rehouse",
5123
+ rehoused: outcome.rehoused
5124
+ }
5125
+ };
5126
+ return {
5127
+ ok: false,
5128
+ reason: outcome.kind === "refused" ? outcome.reason : null
5129
+ };
5130
+ }
5131
+ function scopesOf(model) {
5132
+ if (model.kind === "plan") return new Set(model.plan.config.scopes.keys());
5133
+ return new Set(model.rehoused.scopes);
5134
+ }
5135
+ function warningsOf(model) {
5136
+ return model.kind === "plan" ? model.plan.warnings : model.rehoused.warnings;
4822
5137
  }
4823
5138
  function targetWritePath(cwd, sourcePath) {
4824
5139
  const directory = (0, node_path.dirname)(sourcePath) || cwd;
@@ -4903,9 +5218,11 @@ function emitInstallNextSteps(clack, depSummary, cwd) {
4903
5218
  if (message === null) return;
4904
5219
  clack.note(message, "Next steps");
4905
5220
  }
4906
- function emitSummary(clack, plan, format) {
4907
- if (clack.note) clack.note(summary(plan, format), "Migration summary");
4908
- if (plan.warnings.length > 0 && clack.note) clack.note(formatWarnings(plan.warnings), "Warnings");
5221
+ function emitSummary(clack, model, format) {
5222
+ if (!clack.note) return;
5223
+ clack.note(model.kind === "plan" ? summary(model.plan, format) : rehouseSummary(model.rehoused), "Migration summary");
5224
+ const warnings = warningsOf(model);
5225
+ if (warnings.length > 0) clack.note(formatWarnings(warnings), "Warnings");
4909
5226
  }
4910
5227
  function writeAndAnnounce(clack, targetPath, writePath, rendered, format) {
4911
5228
  const result = format === "eslintrc" ? writeMigrationLegacy(targetPath, writePath, rendered) : writeMigration(targetPath, rendered);
@@ -4955,17 +5272,24 @@ function resolveSource(input, clack) {
4955
5272
  }
4956
5273
  };
4957
5274
  }
5275
+ function renderModel(model, eslintIgnore) {
5276
+ if (model.kind === "rehouse") return model.rehoused.lintModule;
5277
+ if (eslintIgnore && eslintIgnore.patterns.length > 0) mergeIgnorePatterns(model.plan, eslintIgnore.patterns);
5278
+ return renderV2(model.plan);
5279
+ }
4958
5280
  function prepareMigration(resolved, input) {
4959
- const plan = planFromSource(resolved.source, resolved.targetPath, resolved.format);
4960
- if (!plan) return null;
5281
+ const outcome = sourceModelFrom(resolved);
5282
+ if (!outcome.ok) return outcome;
4961
5283
  const writePath = targetWritePath(input.cwd, resolved.targetPath);
4962
5284
  const eslintIgnore = findEslintIgnore(resolved.targetPath);
4963
- if (eslintIgnore && eslintIgnore.patterns.length > 0) mergeIgnorePatterns(plan, eslintIgnore.patterns);
4964
5285
  return {
4965
- plan,
4966
- rendered: renderV2(plan),
4967
- writePath,
4968
- eslintIgnore
5286
+ ok: true,
5287
+ prepared: {
5288
+ model: outcome.model,
5289
+ rendered: renderModel(outcome.model, eslintIgnore),
5290
+ writePath,
5291
+ eslintIgnore
5292
+ }
4969
5293
  };
4970
5294
  }
4971
5295
  async function runConfirmations(clack, input, targetPath, writePath, previous) {
@@ -4995,7 +5319,7 @@ function tryReadConsumerManifest(path) {
4995
5319
  return null;
4996
5320
  }
4997
5321
  }
4998
- function computeDepSummary(plan, writePath, overrides) {
5322
+ function computeDepSummary(model, writePath, overrides) {
4999
5323
  const pkgPath = consumerPackageJsonPath(writePath);
5000
5324
  const manifest = tryReadConsumerManifest(pkgPath);
5001
5325
  if (!manifest) return {
@@ -5008,7 +5332,7 @@ function computeDepSummary(plan, writePath, overrides) {
5008
5332
  };
5009
5333
  const pruneOutcome = pruneEslintDependencies(manifest);
5010
5334
  const ownManifest = (overrides?.readEslintConfigManifest ?? readOwnEslintConfigManifest)();
5011
- const install = buildInstallList(new Set(plan.config.scopes.keys()), ownManifest);
5335
+ const install = buildInstallList(scopesOf(model), ownManifest);
5012
5336
  return {
5013
5337
  removed: pruneOutcome.removed,
5014
5338
  install,
@@ -5063,7 +5387,7 @@ async function applyDepChanges(clack, depSummary, input) {
5063
5387
  }
5064
5388
  function performWriteAndCleanup(clack, resolved, prepared) {
5065
5389
  const outcome = writeAndAnnounce(clack, resolved.targetPath, prepared.writePath, prepared.rendered, resolved.format);
5066
- if (prepared.eslintIgnore) safeUnlink(prepared.eslintIgnore.path, clack, ".eslintignore");
5390
+ if (prepared.eslintIgnore && prepared.model.kind === "plan") safeUnlink(prepared.eslintIgnore.path, clack, ".eslintignore");
5067
5391
  outcome.deletedSourceCandidates.forEach((path) => {
5068
5392
  safeUnlink(path, clack, "superseded config");
5069
5393
  });
@@ -5089,8 +5413,13 @@ function applyLintScript$1(clack, directory) {
5089
5413
  if (result.changed) (0, node_fs.writeFileSync)(manifestPath, result.manifest, "utf8");
5090
5414
  noteLintScriptOutcome$1(clack, result);
5091
5415
  }
5416
+ function noteUntouchedDirectiveFiles(clack, skipped) {
5417
+ if (skipped.length === 0 || !clack.note) return;
5418
+ clack.note(formatUntouchedDirectiveFiles(skipped), `${skipped.length} file(s) left untouched`);
5419
+ }
5092
5420
  async function rewriteDirectivesWithNote(clack, directory, autoConfirm) {
5093
- const candidates = previewDisableDirectives(directory);
5421
+ const { candidates, skipped } = previewDisableDirectives(directory);
5422
+ noteUntouchedDirectiveFiles(clack, skipped);
5094
5423
  if (candidates.length === 0) return;
5095
5424
  const count = candidates.length;
5096
5425
  if (clack.note) clack.note(`${count} file(s) carry an \`eslint-disable\` naming a rule oxlint now owns. Left as they are, ESLint reports each one as an unused directive.
@@ -5106,8 +5435,9 @@ This is a separate, source-wide diff — review it on its own commit.`, "Disable
5106
5435
  return;
5107
5436
  }
5108
5437
  }
5109
- const { rewritten } = rewriteDisableDirectives(directory, candidates);
5438
+ const { rewritten, skipped: unwritten } = rewriteDisableDirectives(directory, candidates);
5110
5439
  if (clack.note) clack.note(`Re-pointed directives in ${rewritten.length} file(s).`, "Disable directives");
5440
+ noteUntouchedDirectiveFiles(clack, unwritten);
5111
5441
  }
5112
5442
  function loadPreviousMigrate(clack, cwd) {
5113
5443
  const restored = loadSession({ cwd });
@@ -5116,7 +5446,12 @@ function loadPreviousMigrate(clack, cwd) {
5116
5446
  return previousMigrate;
5117
5447
  }
5118
5448
  function emitNothingToMigrate(clack, format) {
5119
- clack.outro(format === "eslintrc" ? "No legacy `.eslintrc` shape found — nothing to migrate." : "No v1 useInsider patterns found — nothing to migrate.");
5449
+ clack.outro(format === "eslintrc" ? "No legacy `.eslintrc` shape found — nothing to migrate." : "No v1 or v3 useInsider patterns found — nothing to migrate.");
5450
+ }
5451
+ function noteUnfoldedEslintIgnore(clack, prepared) {
5452
+ const patterns = prepared.eslintIgnore?.patterns ?? [];
5453
+ if (!clack.note || prepared.model.kind !== "rehouse" || patterns.length === 0) return;
5454
+ clack.note(`${prepared.eslintIgnore?.path} still holds ${patterns.length} pattern(s). Flat config does not read it, and the v3 config was carried over verbatim, so they were not folded into \`ignores\`. Move the ones you still need into the \`ignores\` array and delete the file.`, ".eslintignore left alone");
5120
5455
  }
5121
5456
  function persistMigrateSession(input, prepared, resolved) {
5122
5457
  const overwriteExisting = supersededFlatConfigs(resolved.targetPath, prepared.writePath).length > 0;
@@ -5151,14 +5486,20 @@ async function runMigrate(input, clackOverride) {
5151
5486
  return declinedExitCode;
5152
5487
  }
5153
5488
  const previousMigrate = loadPreviousMigrate(clack, input.cwd);
5154
- const prepared = prepareMigration(resolved, input);
5155
- if (!prepared) {
5156
- emitNothingToMigrate(clack, resolved.format);
5157
- return declinedExitCode;
5489
+ const outcome = prepareMigration(resolved, input);
5490
+ if (!outcome.ok) {
5491
+ if (outcome.reason === null) {
5492
+ emitNothingToMigrate(clack, resolved.format);
5493
+ return declinedExitCode;
5494
+ }
5495
+ clack.cancel(outcome.reason);
5496
+ return 1;
5158
5497
  }
5159
- emitSummary(clack, prepared.plan, resolved.format);
5498
+ const { prepared } = outcome;
5499
+ emitSummary(clack, prepared.model, resolved.format);
5500
+ noteUnfoldedEslintIgnore(clack, prepared);
5160
5501
  noteConfigRename(clack, resolved.targetPath, prepared.writePath);
5161
- const depSummary = computeDepSummary(prepared.plan, prepared.writePath, input.overrides);
5502
+ const depSummary = computeDepSummary(prepared.model, prepared.writePath, input.overrides);
5162
5503
  if (!await runConfirmations(clack, input, resolved.targetPath, prepared.writePath, previousMigrate)) return declinedExitCode;
5163
5504
  persistMigrateSession(input, prepared, resolved);
5164
5505
  performWriteAndCleanup(clack, resolved, prepared);