@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.
- package/CHANGELOG.md +26 -0
- package/IMPROVEMENTS.md +270 -0
- package/dist/cli-migration.js +367 -15
- package/dist/cli-preset.d.ts +31 -0
- package/dist/cli-preset.js +315 -0
- package/dist/cli.d.ts +13 -1
- package/dist/cli.js +359 -49
- package/dist/define-config-metadata.d.ts +10 -0
- package/dist/define-config-metadata.js +18 -0
- package/dist/frameworks.js +4 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.js +63 -6
- package/dist/integrations.js +3 -1
- package/dist/optional-package-errors.d.ts +3 -0
- package/dist/optional-package-errors.js +14 -0
- package/package.json +8 -7
package/dist/cli-migration.js
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (
|
|
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
|
-
|
|
478
|
-
|
|
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"
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
|
|
510
|
-
migrated
|
|
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 (
|
|
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 (
|
|
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 };
|