@santi020k/eslint-config-basic 3.1.0 → 3.2.0

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.
@@ -175,6 +175,13 @@ const FACTORY_ALIAS_REPLACEMENTS = /* @__PURE__ */ new Set([
175
175
  "astroConfig",
176
176
  "gitignore"
177
177
  ]);
178
+ const PARAMETER_PROPERTY_MODIFIERS = /* @__PURE__ */ new Set([
179
+ "override",
180
+ "private",
181
+ "protected",
182
+ "public",
183
+ "readonly"
184
+ ]);
178
185
  const dependencyFields = [
179
186
  "dependencies",
180
187
  "devDependencies",
@@ -323,16 +330,121 @@ const getEnclosingDelimiter = (content, offset) => {
323
330
  }
324
331
  return null;
325
332
  };
333
+ const findClosingDelimiter = (content, start, opener, closer) => {
334
+ let depth = 0;
335
+ for (let index = start; index < content.length; index++) {
336
+ if (content[index] === opener) depth++;
337
+ else if (content[index] === closer && --depth === 0) return index + 1;
338
+ }
339
+ return content.length;
340
+ };
341
+ const getShadowedRanges = (content, name) => {
342
+ const masked = maskNonCode(content);
343
+ const importRanges = getImportRanges(content);
344
+ let searchable = masked;
345
+ for (const range of importRanges.toReversed()) {
346
+ searchable = `${searchable.slice(0, range.start)}${" ".repeat(range.end - range.start)}${searchable.slice(range.end)}`;
347
+ }
348
+ const getArrowRange = (start, arrowIndex) => {
349
+ const relativeBodyStart = searchable.slice(arrowIndex + 2).search(/\S/);
350
+ const bodyStart = relativeBodyStart === -1 ? searchable.length : arrowIndex + 2 + relativeBodyStart;
351
+ const bodyDelimiter = searchable[bodyStart];
352
+ let bodyCloser;
353
+ if (bodyDelimiter === "(") bodyCloser = ")";
354
+ else if (bodyDelimiter === "[") bodyCloser = "]";
355
+ if (bodyDelimiter === "{") {
356
+ return { end: findClosingDelimiter(searchable, bodyStart, "{", "}"), start };
357
+ }
358
+ if (bodyCloser) {
359
+ return { end: findClosingDelimiter(searchable, bodyStart, bodyDelimiter, bodyCloser), start };
360
+ }
361
+ const lineEnd = searchable.indexOf("\n", arrowIndex + 2);
362
+ return { end: lineEnd === -1 ? searchable.length : lineEnd, start };
363
+ };
364
+ const unparenthesizedArrowPattern = new RegExp(`\\b${name}\\b\\s*=>`, "gm");
365
+ const ranges = [...searchable.matchAll(unparenthesizedArrowPattern)].map((match) => getArrowRange(match.index, searchable.indexOf("=>", match.index)));
366
+ const splitAtTopLevel = (value, separator) => {
367
+ const parts = [];
368
+ const closers = { "(": ")", "[": "]", "{": "}" };
369
+ const expectedClosers = [];
370
+ let partStart = 0;
371
+ for (let index = 0; index < value.length; index++) {
372
+ const character = value[index];
373
+ const closer = closers[character];
374
+ if (closer) expectedClosers.push(closer);
375
+ else if (character === expectedClosers.at(-1)) expectedClosers.pop();
376
+ else if (character === separator && expectedClosers.length === 0) {
377
+ parts.push(value.slice(partStart, index));
378
+ partStart = index + 1;
379
+ }
380
+ }
381
+ parts.push(value.slice(partStart));
382
+ return parts;
383
+ };
384
+ const hasBindingName = (value) => {
385
+ let binding = splitAtTopLevel(value, "=")[0].trim();
386
+ if (binding.startsWith("...")) binding = binding.slice(3).trim();
387
+ binding = splitAtTopLevel(binding, ":")[0].trim();
388
+ const bindingParts = binding.split(/\s+/);
389
+ while (PARAMETER_PROPERTY_MODIFIERS.has(bindingParts[0])) bindingParts.shift();
390
+ binding = bindingParts.join(" ");
391
+ if (binding.startsWith("{") && binding.endsWith("}")) {
392
+ return splitAtTopLevel(binding.slice(1, -1), ",").some((property) => {
393
+ const propertyParts = splitAtTopLevel(property, ":");
394
+ return hasBindingName(propertyParts.length > 1 ? propertyParts.slice(1).join(":") : property);
395
+ });
396
+ }
397
+ if (binding.startsWith("[") && binding.endsWith("]")) {
398
+ return splitAtTopLevel(binding.slice(1, -1), ",").some(hasBindingName);
399
+ }
400
+ return binding.replace(/\?$/, "") === name;
401
+ };
402
+ const hasParameterBinding = (parameters) => splitAtTopLevel(parameters, ",").some(hasBindingName);
403
+ const parameterOwnerPattern = /(?:\bfunction\b[^()]*|\bcatch|\b(?:constructor|[a-zA-Z_$][\w$]*))\s*$/;
404
+ for (let parameterStart = 0; parameterStart < searchable.length; parameterStart++) {
405
+ if (searchable[parameterStart] !== "(") continue;
406
+ const parameterEnd = findClosingDelimiter(searchable, parameterStart, "(", ")");
407
+ if (searchable[parameterEnd - 1] !== ")") continue;
408
+ const parameters = searchable.slice(parameterStart + 1, parameterEnd - 1);
409
+ if (!hasParameterBinding(parameters)) continue;
410
+ const relativeBodyStart = searchable.slice(parameterEnd).search(/\S/);
411
+ const bodyStart = relativeBodyStart === -1 ? searchable.length : parameterEnd + relativeBodyStart;
412
+ if (searchable.startsWith("=>", bodyStart)) {
413
+ ranges.push(getArrowRange(parameterStart, bodyStart));
414
+ continue;
415
+ }
416
+ if (searchable[bodyStart] !== "{") continue;
417
+ const ownerMatch = parameterOwnerPattern.exec(searchable.slice(0, parameterStart));
418
+ if (!ownerMatch) continue;
419
+ ranges.push({
420
+ end: findClosingDelimiter(searchable, bodyStart, "{", "}"),
421
+ start: ownerMatch.index
422
+ });
423
+ }
424
+ const blockBindingPattern = new RegExp(`\\b(?:const|let|var|function|class)\\s+${name}\\b`, "g");
425
+ for (const match of searchable.matchAll(blockBindingPattern)) {
426
+ const blockStart = searchable.lastIndexOf("{", match.index);
427
+ if (blockStart !== -1) {
428
+ ranges.push({
429
+ end: findClosingDelimiter(searchable, blockStart, "{", "}"),
430
+ start: blockStart
431
+ });
432
+ }
433
+ }
434
+ return ranges;
435
+ };
326
436
  const replaceBindingReferences = (content, from, to, invokeReferences = false) => {
327
437
  const importRanges = getImportRanges(content);
328
438
  const masked = maskNonCode(content);
329
439
  const identifierPattern = new RegExp(`\\b${from}\\b`, "g");
330
440
  const matches = [...masked.matchAll(identifierPattern)];
441
+ const shadowedRanges = getShadowedRanges(content, from);
331
442
  let updated = content;
332
443
  for (const match of matches.reverse()) {
333
444
  const offset = match.index;
334
445
  const isImportSpecifier = importRanges.some((range) => offset >= range.start && offset < range.end);
335
- if (isImportSpecifier) continue;
446
+ const isShadowed = shadowedRanges.some((range) => offset >= range.start && offset < range.end);
447
+ if (isImportSpecifier || isShadowed) continue;
336
448
  const precedingContent = masked.slice(0, offset).trimEnd();
337
449
  const preceding = precedingContent.at(-1);
338
450
  const following = masked.slice(offset + from.length);
@@ -420,17 +532,235 @@ const hasCodePropertySeparator = (codeContent, match) => {
420
532
  const matchIndex = match.index ?? -1;
421
533
  return matchIndex >= 0 && separatorOffset >= 0 && codeContent[matchIndex + separatorOffset] === ":";
422
534
  };
535
+ const replaceCodeProperty = (content, property, replacement, matches) => {
536
+ const masked = maskNonCode(content, true);
537
+ const pattern = new RegExp(`\\b${property}\\b(?=\\s*:)`, "g");
538
+ const propertyMatches = matches ?? [...masked.matchAll(pattern)];
539
+ let updated = content;
540
+ for (const match of propertyMatches.toReversed()) {
541
+ const { index } = match;
542
+ if (index === void 0) continue;
543
+ updated = `${updated.slice(0, index)}${replacement}${updated.slice(index + property.length)}`;
544
+ }
545
+ return updated;
546
+ };
547
+ const toFeatureName = (value) => {
548
+ const trimmed = value.trim();
549
+ const enumMatch = /^(?:Extension|Format|Library|Testing|Tool)\.([a-zA-Z0-9_$]+)$/.exec(trimmed);
550
+ const stringMatch = /^(['"])([^'"]+)\1$/.exec(trimmed);
551
+ const raw = enumMatch?.[1] ?? stringMatch?.[2];
552
+ return raw ? toKebabCase(raw) : null;
553
+ };
554
+ const getOwningObjectStart = (structure, position) => {
555
+ const objectStarts = [];
556
+ for (let index = 0; index < position; index++) {
557
+ if (structure[index] === "{") objectStarts.push(index);
558
+ if (structure[index] === "}") objectStarts.pop();
559
+ }
560
+ return objectStarts.at(-1);
561
+ };
562
+ const getDefineConfigOptionOwners = (searchable) => {
563
+ const owners = new Set(
564
+ [...searchable.matchAll(/\bdefineConfig\s*\(\s*\{/g)].map((match) => match.index + match[0].lastIndexOf("{"))
565
+ );
566
+ const optionNames = new Set(
567
+ [...searchable.matchAll(/\bdefineConfig\s*\(\s*([a-zA-Z_$][\w$]*)\b/g)].map((match) => match[1])
568
+ );
569
+ for (const optionName of optionNames) {
570
+ const bindingPattern = new RegExp(`\\b(?:const|let|var)\\s+${optionName}\\s*=\\s*\\{`, "g");
571
+ for (const match of searchable.matchAll(bindingPattern)) {
572
+ owners.add(match.index + match[0].lastIndexOf("{"));
573
+ }
574
+ }
575
+ return owners;
576
+ };
577
+ const getDirectChildObjectStarts = (structure, containerStart) => {
578
+ const children = [];
579
+ const objectStarts = [containerStart];
580
+ for (let index = containerStart + 1; index < structure.length && objectStarts.length > 0; index++) {
581
+ if (structure[index] === "{") {
582
+ if (objectStarts.at(-1) === containerStart) children.push(index);
583
+ objectStarts.push(index);
584
+ } else if (structure[index] === "}") {
585
+ objectStarts.pop();
586
+ }
587
+ }
588
+ return children;
589
+ };
590
+ const getOwnedObjectPropertyStarts = (searchable, structure, property, owners) => {
591
+ const pattern = new RegExp(`\\b${property}\\b\\s*:\\s*\\{`, "g");
592
+ return [...searchable.matchAll(pattern)].flatMap((match) => {
593
+ const owner = getOwningObjectStart(structure, match.index);
594
+ const objectStart = match.index + match[0].lastIndexOf("{");
595
+ return owner !== void 0 && owners.has(owner) && hasCodePropertySeparator(structure, match) && structure[objectStart] === "{" ? [objectStart] : [];
596
+ });
597
+ };
598
+ const getBasicOptionOwners = (searchable, structure) => {
599
+ const owners = getDefineConfigOptionOwners(searchable);
600
+ for (const projectDefaultsOwner of getOwnedObjectPropertyStarts(
601
+ searchable,
602
+ structure,
603
+ "projectDefaults",
604
+ owners
605
+ )) {
606
+ owners.add(projectDefaultsOwner);
607
+ }
608
+ const visitedProjectContainers = /* @__PURE__ */ new Set();
609
+ let projectContainers = getOwnedObjectPropertyStarts(searchable, structure, "projects", owners);
610
+ while (projectContainers.length > 0) {
611
+ for (const container of projectContainers) {
612
+ visitedProjectContainers.add(container);
613
+ for (const projectOwner of getDirectChildObjectStarts(structure, container)) {
614
+ owners.add(projectOwner);
615
+ }
616
+ }
617
+ projectContainers = getOwnedObjectPropertyStarts(searchable, structure, "projects", owners).filter((container) => !visitedProjectContainers.has(container));
618
+ }
619
+ return owners;
620
+ };
621
+ const getOwnedPropertyMatches = (searchable, structure, property, owners) => {
622
+ const pattern = new RegExp(`\\b${property}\\b\\s*:`, "g");
623
+ return [...searchable.matchAll(pattern)].flatMap((match) => {
624
+ const owner = getOwningObjectStart(structure, match.index);
625
+ return owner !== void 0 && owners.has(owner) && hasCodePropertySeparator(structure, match) ? [match] : [];
626
+ });
627
+ };
628
+ const modernizeLiteralFeatureArrays = (content) => {
629
+ const searchable = maskNonCode(content, true);
630
+ const structure = maskNonCode(content);
631
+ const optionOwners = getBasicOptionOwners(searchable, structure);
632
+ const selections = [...searchable.matchAll(
633
+ /\b(?:extensions|formats|libraries|testing|tools)\b\s*:\s*\[([\s\S]*?)\]\s*,?/g
634
+ )].flatMap((match) => {
635
+ if (/\/[/*]/.test(content.slice(match.index, match.index + match[0].length))) return [];
636
+ const features2 = match[1].split(",").filter(Boolean).map(toFeatureName);
637
+ const owner = getOwningObjectStart(structure, match.index);
638
+ if (owner === void 0 || !optionOwners.has(owner) || !hasCodePropertySeparator(structure, match) || features2.length === 0 || features2.some((feature) => feature === null)) return [];
639
+ return [{
640
+ end: match.index + match[0].length,
641
+ features: features2,
642
+ owner,
643
+ start: match.index
644
+ }];
645
+ });
646
+ if (selections.length === 0) return { changed: false, content, features: [] };
647
+ const features = [...new Set(selections.flatMap((selection) => selection.features))];
648
+ const selectionsByOwner = /* @__PURE__ */ new Map();
649
+ for (const selection of selections) {
650
+ const ownedSelections = selectionsByOwner.get(selection.owner);
651
+ if (ownedSelections) ownedSelections.push(selection);
652
+ else selectionsByOwner.set(selection.owner, [selection]);
653
+ }
654
+ const existingFeaturesByOwner = /* @__PURE__ */ new Map();
655
+ for (const match of searchable.matchAll(/\bfeatures\b\s*:\s*\{/g)) {
656
+ const owner = getOwningObjectStart(structure, match.index);
657
+ if (owner !== void 0 && !existingFeaturesByOwner.has(owner)) {
658
+ existingFeaturesByOwner.set(owner, match);
659
+ }
660
+ }
661
+ const edits = [];
662
+ for (const [owner, ownedSelections] of selectionsByOwner) {
663
+ const ownedFeatures = [...new Set(ownedSelections.flatMap((selection) => selection.features))];
664
+ const first = ownedSelections[0];
665
+ const lineStart = content.lastIndexOf("\n", first.start) + 1;
666
+ const indent = /^\s*/.exec(content.slice(lineStart, first.start))?.[0] ?? "";
667
+ const existingFeatures = existingFeaturesByOwner.get(owner);
668
+ const featureBlock = existingFeatures ? `
669
+ ${ownedFeatures.map((feature) => `${indent} ${JSON.stringify(feature)}: true,`).join("\n")}` : [
670
+ "features: {",
671
+ ...ownedFeatures.map((feature) => `${indent} ${JSON.stringify(feature)}: true,`),
672
+ `${indent}},`
673
+ ].join("\n");
674
+ for (const selection of ownedSelections) {
675
+ edits.push({
676
+ end: selection.end,
677
+ replacement: !existingFeatures && selection === first ? featureBlock : "",
678
+ start: selection.start
679
+ });
680
+ }
681
+ if (existingFeatures) {
682
+ const insertionIndex = (existingFeatures.index ?? 0) + existingFeatures[0].length;
683
+ edits.push({
684
+ end: insertionIndex,
685
+ replacement: featureBlock,
686
+ start: insertionIndex
687
+ });
688
+ }
689
+ }
690
+ let updated = content;
691
+ for (const edit of edits.sort((a, b) => b.start - a.start)) {
692
+ updated = `${updated.slice(0, edit.start)}${edit.replacement}${updated.slice(edit.end)}`;
693
+ }
694
+ return { changed: updated !== content, content: updated, features };
695
+ };
696
+ const modernizeConfigOptions = (content) => {
697
+ const changes = [];
698
+ let updated = content;
699
+ const searchable = maskNonCode(updated, true);
700
+ const structure = maskNonCode(updated);
701
+ const basicOptionOwners = getBasicOptionOwners(searchable, structure);
702
+ const featureOwners = new Set(
703
+ getOwnedPropertyMatches(searchable, structure, "features", basicOptionOwners).flatMap((match) => {
704
+ if (match.index === void 0) return [];
705
+ const owner = getOwningObjectStart(structure, match.index);
706
+ return owner === void 0 ? [] : [owner];
707
+ })
708
+ );
709
+ const integrationMatches = getOwnedPropertyMatches(searchable, structure, "integrations", basicOptionOwners).filter((match) => {
710
+ if (match.index === void 0) return false;
711
+ const owner = getOwningObjectStart(structure, match.index);
712
+ return owner !== void 0 && !featureOwners.has(owner);
713
+ });
714
+ if (integrationMatches.length > 0) {
715
+ updated = replaceCodeProperty(updated, "integrations", "features", integrationMatches);
716
+ changes.push("Replaced the deprecated integrations map with features.");
717
+ }
718
+ const updatedSearchable = maskNonCode(updated, true);
719
+ const updatedStructure = maskNonCode(updated);
720
+ const optionOwners = getDefineConfigOptionOwners(updatedSearchable);
721
+ const rootMatches = getOwnedPropertyMatches(updatedSearchable, updatedStructure, "root", optionOwners);
722
+ const rootAliasMatches = /* @__PURE__ */ new Map([
723
+ ["detectRootDir", getOwnedPropertyMatches(updatedSearchable, updatedStructure, "detectRootDir", optionOwners)],
724
+ ["tsconfigRootDir", getOwnedPropertyMatches(updatedSearchable, updatedStructure, "tsconfigRootDir", optionOwners)]
725
+ ]);
726
+ const rootAliases = [...rootAliasMatches].filter(([, matches]) => matches.length > 0).map(([alias]) => alias);
727
+ if (rootMatches.length === 0 && rootAliases.length === 1) {
728
+ const alias = rootAliases[0];
729
+ updated = replaceCodeProperty(updated, alias, "root", rootAliasMatches.get(alias));
730
+ changes.push(`Replaced ${alias} with the v3 root option.`);
731
+ } else if (rootAliases.length > 0) {
732
+ changes.push(
733
+ `Manual action required: consolidate ${rootAliases.join(" and ")} into root after confirming they resolve to the same directory.`
734
+ );
735
+ }
736
+ const featureMigration = modernizeLiteralFeatureArrays(updated);
737
+ if (featureMigration.changed) {
738
+ updated = featureMigration.content;
739
+ changes.push(`Replaced literal category arrays with the v3 features map (${featureMigration.features.join(", ")}).`);
740
+ }
741
+ if (maskNonCode(updated, true).includes("better-tailwindcss/no-unknown-classes")) {
742
+ changes.push(
743
+ 'Manual action required: replace the raw better-tailwindcss/no-unknown-classes override with projects["<scope>"].tailwind.noUnknownClasses.'
744
+ );
745
+ }
746
+ return { changes, content: updated };
747
+ };
423
748
  const getExplicitConfigFeaturePackages = (content) => {
424
749
  const packages = /* @__PURE__ */ new Set();
425
750
  const codeContent = maskNonCode(content);
426
751
  const searchableContent = maskNonCode(content, true);
752
+ const optionOwners = getBasicOptionOwners(searchableContent, codeContent);
753
+ const isBasicOption = (match) => {
754
+ const owner = getOwningObjectStart(codeContent, match.index ?? -1);
755
+ return owner !== void 0 && optionOwners.has(owner) && hasCodePropertySeparator(codeContent, match);
756
+ };
427
757
  for (const category of Object.keys(FEATURE_PACKAGES)) {
428
758
  const categorySelections = searchableContent.matchAll(
429
759
  // eslint-disable-next-line security/detect-non-literal-regexp -- category names come from the internal package registry
430
760
  new RegExp(`(?:\\b${category}\\b|['"]${category}['"])\\s*:\\s*\\[([\\s\\S]*?)\\]`, "g")
431
761
  );
432
762
  for (const selection of categorySelections) {
433
- if (hasCodePropertySeparator(codeContent, selection) && selection[1].trim()) {
763
+ if (isBasicOption(selection) && selection[1].trim()) {
434
764
  packages.add(FEATURE_PACKAGES[category]);
435
765
  break;
436
766
  }
@@ -438,7 +768,7 @@ const getExplicitConfigFeaturePackages = (content) => {
438
768
  }
439
769
  const featureSelections = /(?:\b(?:features|integrations)\b|['"](?:features|integrations)['"])\s*:\s*\{([\s\S]*?)\}/g;
440
770
  for (const selection of searchableContent.matchAll(featureSelections)) {
441
- if (!hasCodePropertySeparator(codeContent, selection)) continue;
771
+ if (!isBasicOption(selection)) continue;
442
772
  const enabledFeaturePattern = /(?:['"]([^'"]+)['"]|([a-zA-Z_$][\w$-]*))\s*:\s*true\b/g;
443
773
  for (const match of selection[1].matchAll(enabledFeaturePattern)) {
444
774
  const featureName = (match[1] || match[2]).toLowerCase();
@@ -448,7 +778,7 @@ const getExplicitConfigFeaturePackages = (content) => {
448
778
  }
449
779
  const enumPresetPattern = /(?:\bpreset\b|['"]preset['"])\s*:\s*Preset\.(All|App|CI|Library|Monorepo)/g;
450
780
  for (const match of searchableContent.matchAll(enumPresetPattern)) {
451
- if (!hasCodePropertySeparator(codeContent, match)) continue;
781
+ if (!isBasicOption(match)) continue;
452
782
  const presetName = match[1].toLowerCase();
453
783
  for (const packageName of PRESET_FEATURE_PACKAGES[presetName] ?? []) {
454
784
  packages.add(packageName);
@@ -456,31 +786,40 @@ const getExplicitConfigFeaturePackages = (content) => {
456
786
  }
457
787
  const stringPresetPattern = /(?:\bpreset\b|['"]preset['"])\s*:\s*(['"`])(all|app|ci|library|monorepo)\1/g;
458
788
  for (const match of searchableContent.matchAll(stringPresetPattern)) {
459
- if (!hasCodePropertySeparator(codeContent, match)) continue;
789
+ if (!isBasicOption(match)) continue;
460
790
  for (const packageName of PRESET_FEATURE_PACKAGES[match[2]] ?? []) {
461
791
  packages.add(packageName);
462
792
  }
463
793
  }
464
794
  const strictPattern = /(?:\bstrict\b|['"]strict['"])\s*:\s*(['"`])pedantic\1/g;
465
795
  for (const match of searchableContent.matchAll(strictPattern)) {
466
- if (hasCodePropertySeparator(codeContent, match)) {
796
+ if (isBasicOption(match)) {
467
797
  packages.add(FEATURE_PACKAGES.extensions);
468
798
  break;
469
799
  }
470
800
  }
471
801
  return [...packages];
472
802
  };
803
+ const replacePackageSpecifiers = (content, from, to) => content.replace(
804
+ // eslint-disable-next-line security/detect-unsafe-regex -- bounded module specifiers cannot cross quote characters
805
+ /(\b(?:from|import|export)\s*(?:\(\s*)?|\brequire\s*\(\s*)(['"])([^'"]+)\2/g,
806
+ (statement, prefix, quote, specifier) => specifier === from ? `${prefix}${quote}${to}${quote}` : statement
807
+ );
473
808
  const migrateConfigToV3 = (content, mode) => {
474
809
  const changes = [];
475
810
  const featurePackages = new Set(getExplicitConfigFeaturePackages(content));
476
811
  let migrated = content;
477
- if (migrated.includes(LITE_PACKAGE)) {
478
- migrated = migrated.replaceAll(LITE_PACKAGE, BASIC_PACKAGE);
812
+ const liteMigrated = replacePackageSpecifiers(migrated, LITE_PACKAGE, BASIC_PACKAGE);
813
+ if (liteMigrated !== migrated) {
814
+ migrated = liteMigrated;
479
815
  changes.push("Replaced the Lite compatibility import with Basic.");
480
816
  }
481
- if (mode === "full" && migrated.includes(BASIC_PACKAGE)) {
482
- migrated = migrated.replaceAll(BASIC_PACKAGE, FULL_PACKAGE);
483
- changes.push("Switched the root config import to the full bundle.");
817
+ if (mode === "full") {
818
+ const fullMigrated = replacePackageSpecifiers(migrated, BASIC_PACKAGE, FULL_PACKAGE);
819
+ if (fullMigrated !== migrated) {
820
+ migrated = fullMigrated;
821
+ changes.push("Switched the root config import to the full bundle.");
822
+ }
484
823
  }
485
824
  if (mode === "lean") {
486
825
  const split = splitBasicIntegrationImports(migrated);
@@ -506,13 +845,21 @@ const migrateConfigToV3 = (content, mode) => {
506
845
  migrated = remixMigrated;
507
846
  changes.push('Moved frameworks.remix to frameworks["react-router"].');
508
847
  }
509
- if (migrated.includes(REMIX_PACKAGE)) {
510
- migrated = migrated.replaceAll(REMIX_PACKAGE, FRAMEWORK_PACKAGES["react-router"]);
848
+ const reactRouterMigrated = replacePackageSpecifiers(
849
+ migrated,
850
+ REMIX_PACKAGE,
851
+ FRAMEWORK_PACKAGES["react-router"]
852
+ );
853
+ if (reactRouterMigrated !== migrated) {
854
+ migrated = reactRouterMigrated;
511
855
  changes.push("Replaced the Remix package import with React Router.");
512
856
  }
513
857
  if (/\bloadModule\b/.test(migrated)) {
514
858
  changes.push("Manual action required: replace loadModule usage with createModuleLoader(resolver).");
515
859
  }
860
+ const modernized = modernizeConfigOptions(migrated);
861
+ migrated = modernized.content;
862
+ changes.push(...modernized.changes);
516
863
  return {
517
864
  changes,
518
865
  content: migrated,
@@ -525,13 +872,18 @@ const shouldUseFullBundle = (configContent, packageJson, forceFull) => {
525
872
  if (configContent) {
526
873
  const codeContent = maskNonCode(configContent);
527
874
  const searchableContent = maskNonCode(configContent, true);
875
+ const optionOwners = getDefineConfigOptionOwners(searchableContent);
876
+ const isRootOption = (match) => {
877
+ const owner = getOwningObjectStart(codeContent, match.index ?? -1);
878
+ return owner !== void 0 && optionOwners.has(owner) && hasCodePropertySeparator(codeContent, match);
879
+ };
528
880
  const enumPresetPattern = /(?:\bpreset\b|['"]preset['"])\s*:\s*Preset\.All/g;
529
881
  for (const match of searchableContent.matchAll(enumPresetPattern)) {
530
- if (hasCodePropertySeparator(codeContent, match)) return true;
882
+ if (isRootOption(match)) return true;
531
883
  }
532
884
  const stringPresetPattern = /(?:\bpreset\b|['"]preset['"])\s*:\s*(['"`])all\1/g;
533
885
  for (const match of searchableContent.matchAll(stringPresetPattern)) {
534
- if (hasCodePropertySeparator(codeContent, match)) return true;
886
+ if (isRootOption(match)) return true;
535
887
  }
536
888
  }
537
889
  return getDeclaredConfigPackages(packageJson).has(FULL_PACKAGE);
@@ -0,0 +1,31 @@
1
+ import { EslintConfigOptions } from '@santi020k/eslint-config-core';
2
+
3
+ interface ExplainPresetOptions {
4
+ compatibility?: boolean;
5
+ file?: string;
6
+ json?: boolean;
7
+ output?: string;
8
+ }
9
+ declare const createPresetReport: (cwd: string, presetName: string, file?: string) => Promise<{
10
+ added: Record<string, unknown>;
11
+ changed: Record<string, {
12
+ from: unknown;
13
+ to: unknown;
14
+ }>;
15
+ file: string;
16
+ groups: {
17
+ [k: string]: string[];
18
+ };
19
+ missingPackages: string[];
20
+ preset: string;
21
+ presetOptions: Partial<EslintConfigOptions>;
22
+ removed: Record<string, unknown>;
23
+ totals: {
24
+ added: number;
25
+ changed: number;
26
+ removed: number;
27
+ };
28
+ }>;
29
+ declare const handleExplainPreset: (cwd: string | undefined, preset: string, options?: ExplainPresetOptions) => Promise<void>;
30
+
31
+ export { type ExplainPresetOptions, createPresetReport, handleExplainPreset };