react-doctor 0.0.30 → 0.0.32

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/utils/get-diff-files.ts","../src/index.ts"],"mappings":";KAAY,WAAA;AAAA,KAEA,SAAA;AAAA,UAUK,WAAA;EACf,aAAA;EACA,WAAA;EACA,YAAA;EACA,SAAA,EAAW,SAAA;EACX,aAAA;EACA,gBAAA;EACA,eAAA;AAAA;AAAA,UAiCe,UAAA;EACf,QAAA;EACA,MAAA;EACA,IAAA;EACA,QAAA;EACA,OAAA;EACA,IAAA;EACA,IAAA;EACA,MAAA;EACA,QAAA;EACA,MAAA;AAAA;AAAA,UA4Be,WAAA;EACf,KAAA;EACA,KAAA;AAAA;AAAA,UAyBe,QAAA;EACf,aAAA;EACA,UAAA;EACA,YAAA;EACA,gBAAA;AAAA;AAAA,UA2Ce,uBAAA;EACf,KAAA;EACA,KAAA;AAAA;AAAA,UAGe,iBAAA;EACf,MAAA,GAAS,uBAAA;EACT,IAAA;EACA,QAAA;EACA,OAAA;EACA,IAAA;EACA,MAAA,GAAS,WAAA;AAAA;;;cChGE,WAAA,GAAe,SAAA,UAAmB,kBAAA,cAA8B,QAAA;AAAA,cAiBhE,iBAAA,GAAqB,SAAA;;;UCnFjB,eAAA;EACf,IAAA;EACA,QAAA;EACA,YAAA;AAAA;AAAA,UAGe,cAAA;EACf,WAAA,EAAa,UAAA;EACb,KAAA,EAAO,WAAA;EACP,OAAA,EAAS,WAAA;EACT,mBAAA;AAAA;AAAA,cAGW,QAAA,GACX,SAAA,UACA,OAAA,GAAS,eAAA,KACR,OAAA,CAAQ,cAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/utils/get-diff-files.ts","../src/index.ts"],"mappings":";KAAY,WAAA;AAAA,KAEA,SAAA;AAAA,UAUK,WAAA;EACf,aAAA;EACA,WAAA;EACA,YAAA;EACA,SAAA,EAAW,SAAA;EACX,aAAA;EACA,gBAAA;EACA,eAAA;AAAA;AAAA,UAiCe,UAAA;EACf,QAAA;EACA,MAAA;EACA,IAAA;EACA,QAAA;EACA,OAAA;EACA,IAAA;EACA,IAAA;EACA,MAAA;EACA,QAAA;EACA,MAAA;AAAA;AAAA,UA4Be,WAAA;EACf,KAAA;EACA,KAAA;AAAA;AAAA,UAyBe,QAAA;EACf,aAAA;EACA,UAAA;EACA,YAAA;EACA,gBAAA;AAAA;AAAA,UA2Ce,uBAAA;EACf,KAAA;EACA,KAAA;AAAA;AAAA,UAGe,iBAAA;EACf,MAAA,GAAS,uBAAA;EACT,IAAA;EACA,QAAA;EACA,OAAA;EACA,IAAA;EACA,MAAA,GAAS,WAAA;AAAA;;;cChGE,WAAA,GAAe,SAAA,UAAmB,kBAAA,cAA8B,QAAA;AAAA,cAiBhE,iBAAA,GAAqB,SAAA;;;UClFjB,eAAA;EACf,IAAA;EACA,QAAA;EACA,YAAA;AAAA;AAAA,UAGe,cAAA;EACf,WAAA,EAAa,UAAA;EACb,KAAA,EAAO,WAAA;EACP,OAAA,EAAS,WAAA;EACT,mBAAA;AAAA;AAAA,cAGW,QAAA,GACX,SAAA,UACA,OAAA,GAAS,eAAA,KACR,OAAA,CAAQ,cAAA"}
package/dist/index.js CHANGED
@@ -25,6 +25,12 @@ const WARNING_RULE_PENALTY = .75;
25
25
  const ERROR_ESTIMATED_FIX_RATE = .85;
26
26
  const WARNING_ESTIMATED_FIX_RATE = .8;
27
27
  const MAX_KNIP_RETRIES = 5;
28
+ const IGNORED_DIRECTORIES = new Set([
29
+ "node_modules",
30
+ "dist",
31
+ "build",
32
+ "coverage"
33
+ ]);
28
34
  const AMI_WEBSITE_URL = "https://ami.dev";
29
35
  const AMI_INSTALL_URL = `${AMI_WEBSITE_URL}/install.sh`;
30
36
 
@@ -230,17 +236,31 @@ const compileGlobPattern = (pattern) => {
230
236
  return new RegExp(regexSource);
231
237
  };
232
238
 
239
+ //#endregion
240
+ //#region src/utils/is-ignored-file.ts
241
+ const toRelativePath = (filePath, rootDirectory) => {
242
+ const normalizedFilePath = filePath.replace(/\\/g, "/");
243
+ const normalizedRoot = rootDirectory.replace(/\\/g, "/").replace(/\/$/, "") + "/";
244
+ if (normalizedFilePath.startsWith(normalizedRoot)) return normalizedFilePath.slice(normalizedRoot.length);
245
+ return normalizedFilePath.replace(/^\.\//, "");
246
+ };
247
+ const compileIgnoredFilePatterns = (userConfig) => Array.isArray(userConfig?.ignore?.files) ? userConfig.ignore.files.map(compileGlobPattern) : [];
248
+ const isFileIgnoredByPatterns = (filePath, rootDirectory, patterns) => {
249
+ if (patterns.length === 0) return false;
250
+ const relativePath = toRelativePath(filePath, rootDirectory);
251
+ return patterns.some((pattern) => pattern.test(relativePath));
252
+ };
253
+
233
254
  //#endregion
234
255
  //#region src/utils/filter-diagnostics.ts
235
- const filterIgnoredDiagnostics = (diagnostics, config) => {
256
+ const filterIgnoredDiagnostics = (diagnostics, config, rootDirectory) => {
236
257
  const ignoredRules = new Set(Array.isArray(config.ignore?.rules) ? config.ignore.rules : []);
237
- const ignoredFilePatterns = Array.isArray(config.ignore?.files) ? config.ignore.files.map(compileGlobPattern) : [];
258
+ const ignoredFilePatterns = compileIgnoredFilePatterns(config);
238
259
  if (ignoredRules.size === 0 && ignoredFilePatterns.length === 0) return diagnostics;
239
260
  return diagnostics.filter((diagnostic) => {
240
261
  const ruleIdentifier = `${diagnostic.plugin}/${diagnostic.rule}`;
241
262
  if (ignoredRules.has(ruleIdentifier)) return false;
242
- const normalizedPath = diagnostic.filePath.replace(/\\/g, "/").replace(/^\.\//, "");
243
- if (ignoredFilePatterns.some((pattern) => pattern.test(normalizedPath))) return false;
263
+ if (isFileIgnoredByPatterns(diagnostic.filePath, rootDirectory, ignoredFilePatterns)) return false;
244
264
  return true;
245
265
  });
246
266
  };
@@ -295,7 +315,7 @@ const combineDiagnostics = (lintDiagnostics, deadCodeDiagnostics, directory, isD
295
315
  ...deadCodeDiagnostics,
296
316
  ...isDiffMode ? [] : checkReducedMotion(directory)
297
317
  ];
298
- return filterInlineSuppressions(userConfig ? filterIgnoredDiagnostics(merged, userConfig) : merged, directory);
318
+ return filterInlineSuppressions(userConfig ? filterIgnoredDiagnostics(merged, userConfig, directory) : merged, directory);
299
319
  };
300
320
 
301
321
  //#endregion
@@ -363,12 +383,6 @@ const FRAMEWORK_PACKAGES = {
363
383
  expo: "expo",
364
384
  "react-native": "react-native"
365
385
  };
366
- const IGNORED_DIRECTORIES = new Set([
367
- "node_modules",
368
- "dist",
369
- "build",
370
- "coverage"
371
- ]);
372
386
  const countSourceFilesViaFilesystem = (rootDirectory) => {
373
387
  let count = 0;
374
388
  const stack = [rootDirectory];
@@ -410,23 +424,115 @@ const detectFramework = (dependencies) => {
410
424
  return "unknown";
411
425
  };
412
426
  const isCatalogReference = (version) => version.startsWith("catalog:");
427
+ const extractCatalogName = (version) => {
428
+ if (!isCatalogReference(version)) return null;
429
+ const name = version.slice(8).trim();
430
+ return name.length > 0 ? name : null;
431
+ };
413
432
  const resolveVersionFromCatalog = (catalog, packageName) => {
414
433
  const version = catalog[packageName];
415
434
  if (typeof version === "string" && !isCatalogReference(version)) return version;
416
435
  return null;
417
436
  };
418
- const resolveCatalogVersion = (packageJson, packageName) => {
437
+ const parsePnpmWorkspaceCatalogs = (rootDirectory) => {
438
+ const workspacePath = path.join(rootDirectory, "pnpm-workspace.yaml");
439
+ if (!isFile(workspacePath)) return {
440
+ defaultCatalog: {},
441
+ namedCatalogs: {}
442
+ };
443
+ const content = fs.readFileSync(workspacePath, "utf-8");
444
+ const defaultCatalog = {};
445
+ const namedCatalogs = {};
446
+ let currentSection = "none";
447
+ let currentCatalogName = "";
448
+ for (const line of content.split("\n")) {
449
+ const trimmed = line.trim();
450
+ if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
451
+ const indentLevel = line.search(/\S/);
452
+ if (indentLevel === 0 && trimmed === "catalog:") {
453
+ currentSection = "catalog";
454
+ continue;
455
+ }
456
+ if (indentLevel === 0 && trimmed === "catalogs:") {
457
+ currentSection = "catalogs";
458
+ continue;
459
+ }
460
+ if (indentLevel === 0) {
461
+ currentSection = "none";
462
+ continue;
463
+ }
464
+ if (currentSection === "catalog" && indentLevel > 0) {
465
+ const colonIndex = trimmed.indexOf(":");
466
+ if (colonIndex > 0) {
467
+ const key = trimmed.slice(0, colonIndex).trim().replace(/["']/g, "");
468
+ const value = trimmed.slice(colonIndex + 1).trim().replace(/["']/g, "");
469
+ if (key && value) defaultCatalog[key] = value;
470
+ }
471
+ continue;
472
+ }
473
+ if (currentSection === "catalogs" && indentLevel > 0) {
474
+ if (trimmed.endsWith(":") && !trimmed.includes(" ")) {
475
+ currentCatalogName = trimmed.slice(0, -1).replace(/["']/g, "");
476
+ currentSection = "named-catalog";
477
+ namedCatalogs[currentCatalogName] = {};
478
+ continue;
479
+ }
480
+ }
481
+ if (currentSection === "named-catalog" && indentLevel > 0) {
482
+ if (indentLevel <= 2 && trimmed.endsWith(":") && !trimmed.includes(" ")) {
483
+ currentCatalogName = trimmed.slice(0, -1).replace(/["']/g, "");
484
+ namedCatalogs[currentCatalogName] = {};
485
+ continue;
486
+ }
487
+ const colonIndex = trimmed.indexOf(":");
488
+ if (colonIndex > 0 && currentCatalogName) {
489
+ const key = trimmed.slice(0, colonIndex).trim().replace(/["']/g, "");
490
+ const value = trimmed.slice(colonIndex + 1).trim().replace(/["']/g, "");
491
+ if (key && value) namedCatalogs[currentCatalogName][key] = value;
492
+ }
493
+ }
494
+ }
495
+ return {
496
+ defaultCatalog,
497
+ namedCatalogs
498
+ };
499
+ };
500
+ const resolveCatalogVersionFromCollection = (catalogs, packageName, catalogReference) => {
501
+ if (catalogReference) {
502
+ const namedCatalog = catalogs.namedCatalogs[catalogReference];
503
+ if (namedCatalog?.[packageName]) return namedCatalog[packageName];
504
+ }
505
+ if (catalogs.defaultCatalog[packageName]) return catalogs.defaultCatalog[packageName];
506
+ for (const namedCatalog of Object.values(catalogs.namedCatalogs)) if (namedCatalog[packageName]) return namedCatalog[packageName];
507
+ return null;
508
+ };
509
+ const resolveCatalogVersion = (packageJson, packageName, rootDirectory) => {
510
+ const rawVersion = collectAllDependencies(packageJson)[packageName];
511
+ const catalogName = rawVersion ? extractCatalogName(rawVersion) : null;
419
512
  const raw = packageJson;
420
513
  if (isPlainObject(raw.catalog)) {
421
514
  const version = resolveVersionFromCatalog(raw.catalog, packageName);
422
515
  if (version) return version;
423
516
  }
424
517
  if (isPlainObject(raw.catalogs)) {
518
+ if (catalogName && isPlainObject(raw.catalogs[catalogName])) {
519
+ const version = resolveVersionFromCatalog(raw.catalogs[catalogName], packageName);
520
+ if (version) return version;
521
+ }
425
522
  for (const catalogEntries of Object.values(raw.catalogs)) if (isPlainObject(catalogEntries)) {
426
523
  const version = resolveVersionFromCatalog(catalogEntries, packageName);
427
524
  if (version) return version;
428
525
  }
429
526
  }
527
+ const workspaces = packageJson.workspaces;
528
+ if (workspaces && !Array.isArray(workspaces) && isPlainObject(workspaces.catalog)) {
529
+ const version = resolveVersionFromCatalog(workspaces.catalog, packageName);
530
+ if (version) return version;
531
+ }
532
+ if (rootDirectory) {
533
+ const pnpmVersion = resolveCatalogVersionFromCollection(parsePnpmWorkspaceCatalogs(rootDirectory), packageName, catalogName);
534
+ if (pnpmVersion) return pnpmVersion;
535
+ }
430
536
  return null;
431
537
  };
432
538
  const extractDependencyInfo = (packageJson) => {
@@ -487,7 +593,7 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
487
593
  };
488
594
  const rootPackageJson = readPackageJson(monorepoPackageJsonPath);
489
595
  const rootInfo = extractDependencyInfo(rootPackageJson);
490
- const catalogVersion = resolveCatalogVersion(rootPackageJson, "react");
596
+ const catalogVersion = resolveCatalogVersion(rootPackageJson, "react", monorepoRoot);
491
597
  const workspaceInfo = findReactInWorkspaces(monorepoRoot, rootPackageJson);
492
598
  return {
493
599
  reactVersion: rootInfo.reactVersion ?? catalogVersion ?? workspaceInfo.reactVersion,
@@ -542,7 +648,14 @@ const discoverProject = (directory) => {
542
648
  if (!isFile(packageJsonPath)) throw new Error(`No package.json found in ${directory}`);
543
649
  const packageJson = readPackageJson(packageJsonPath);
544
650
  let { reactVersion, framework } = extractDependencyInfo(packageJson);
545
- if (!reactVersion) reactVersion = resolveCatalogVersion(packageJson, "react");
651
+ if (!reactVersion) reactVersion = resolveCatalogVersion(packageJson, "react", directory);
652
+ if (!reactVersion) {
653
+ const monorepoRoot = findMonorepoRoot(directory);
654
+ if (monorepoRoot) {
655
+ const monorepoPackageJsonPath = path.join(monorepoRoot, "package.json");
656
+ if (isFile(monorepoPackageJsonPath)) reactVersion = resolveCatalogVersion(readPackageJson(monorepoPackageJsonPath), "react", monorepoRoot);
657
+ }
658
+ }
546
659
  if (!reactVersion || framework === "unknown") {
547
660
  const workspaceInfo = findReactInWorkspaces(directory, packageJson);
548
661
  if (!reactVersion && workspaceInfo.reactVersion) reactVersion = workspaceInfo.reactVersion;
@@ -593,6 +706,49 @@ const loadConfig = (rootDirectory) => {
593
706
  return null;
594
707
  };
595
708
 
709
+ //#endregion
710
+ //#region src/utils/resolve-lint-include-paths.ts
711
+ const listSourceFilesViaGit = (rootDirectory) => {
712
+ const result = spawnSync("git", [
713
+ "ls-files",
714
+ "--cached",
715
+ "--others",
716
+ "--exclude-standard"
717
+ ], {
718
+ cwd: rootDirectory,
719
+ encoding: "utf-8",
720
+ maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
721
+ });
722
+ if (result.error || result.status !== 0) return null;
723
+ return result.stdout.split("\n").filter((filePath) => filePath.length > 0 && SOURCE_FILE_PATTERN.test(filePath));
724
+ };
725
+ const listSourceFilesViaFilesystem = (rootDirectory) => {
726
+ const filePaths = [];
727
+ const stack = [rootDirectory];
728
+ while (stack.length > 0) {
729
+ const currentDirectory = stack.pop();
730
+ const entries = fs.readdirSync(currentDirectory, { withFileTypes: true });
731
+ for (const entry of entries) {
732
+ const absolutePath = path.join(currentDirectory, entry.name);
733
+ if (entry.isDirectory()) {
734
+ if (!entry.name.startsWith(".") && !IGNORED_DIRECTORIES.has(entry.name)) stack.push(absolutePath);
735
+ continue;
736
+ }
737
+ if (entry.isFile() && SOURCE_FILE_PATTERN.test(entry.name)) filePaths.push(path.relative(rootDirectory, absolutePath).replace(/\\/g, "/"));
738
+ }
739
+ }
740
+ return filePaths;
741
+ };
742
+ const listSourceFiles = (rootDirectory) => listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory);
743
+ const resolveLintIncludePaths = (rootDirectory, userConfig) => {
744
+ if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
745
+ const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
746
+ return listSourceFiles(rootDirectory).filter((filePath) => {
747
+ if (!JSX_FILE_PATTERN.test(filePath)) return false;
748
+ return !isFileIgnoredByPatterns(filePath, rootDirectory, ignoredPatterns);
749
+ });
750
+ };
751
+
596
752
  //#endregion
597
753
  //#region src/utils/run-knip.ts
598
754
  const KNIP_CATEGORY_MAP = {
@@ -851,24 +1007,27 @@ const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler }) => ({
851
1007
 
852
1008
  //#endregion
853
1009
  //#region src/utils/neutralize-disable-directives.ts
854
- const findFilesWithDisableDirectives = (rootDirectory) => {
855
- const result = spawnSync("git", [
1010
+ const findFilesWithDisableDirectives = (rootDirectory, includePaths) => {
1011
+ const grepArgs = [
856
1012
  "grep",
857
1013
  "-l",
858
1014
  "--untracked",
859
1015
  "-E",
860
1016
  "(eslint|oxlint)-disable"
861
- ], {
1017
+ ];
1018
+ if (includePaths && includePaths.length > 0) grepArgs.push("--", ...includePaths);
1019
+ const result = spawnSync("git", grepArgs, {
862
1020
  cwd: rootDirectory,
863
1021
  encoding: "utf-8",
864
1022
  maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
865
1023
  });
866
1024
  if (result.error || result.status === null) return [];
1025
+ if (result.status !== 0 && result.stdout.trim().length === 0) return [];
867
1026
  return result.stdout.split("\n").filter((filePath) => filePath.length > 0 && SOURCE_FILE_PATTERN.test(filePath));
868
1027
  };
869
1028
  const neutralizeContent = (content) => content.replaceAll("eslint-disable", "eslint_disable").replaceAll("oxlint-disable", "oxlint_disable");
870
- const neutralizeDisableDirectives = (rootDirectory) => {
871
- const filePaths = findFilesWithDisableDirectives(rootDirectory);
1029
+ const neutralizeDisableDirectives = (rootDirectory, includePaths) => {
1030
+ const filePaths = findFilesWithDisableDirectives(rootDirectory, includePaths);
872
1031
  const originalContents = /* @__PURE__ */ new Map();
873
1032
  for (const relativePath of filePaths) {
874
1033
  const absolutePath = path.join(rootDirectory, relativePath);
@@ -1144,7 +1303,7 @@ const runOxlint = async (rootDirectory, hasTypeScript, framework, hasReactCompil
1144
1303
  framework,
1145
1304
  hasReactCompiler
1146
1305
  });
1147
- const restoreDisableDirectives = neutralizeDisableDirectives(rootDirectory);
1306
+ const restoreDisableDirectives = neutralizeDisableDirectives(rootDirectory, includePaths);
1148
1307
  try {
1149
1308
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
1150
1309
  const baseArgs = [
@@ -1260,9 +1419,9 @@ const diagnose = async (directory, options = {}) => {
1260
1419
  const effectiveLint = options.lint ?? userConfig?.lint ?? true;
1261
1420
  const effectiveDeadCode = options.deadCode ?? userConfig?.deadCode ?? true;
1262
1421
  if (!projectInfo.reactVersion) throw new Error("No React dependency found in package.json");
1263
- const jsxIncludePaths = computeJsxIncludePaths(includePaths);
1422
+ const lintIncludePaths = computeJsxIncludePaths(includePaths) ?? resolveLintIncludePaths(resolvedDirectory, userConfig);
1264
1423
  const emptyDiagnostics = [];
1265
- const lintPromise = effectiveLint ? runOxlint(resolvedDirectory, projectInfo.hasTypeScript, projectInfo.framework, projectInfo.hasReactCompiler, jsxIncludePaths).catch((error) => {
1424
+ const lintPromise = effectiveLint ? runOxlint(resolvedDirectory, projectInfo.hasTypeScript, projectInfo.framework, projectInfo.hasReactCompiler, lintIncludePaths).catch((error) => {
1266
1425
  console.error("Lint failed:", error);
1267
1426
  return emptyDiagnostics;
1268
1427
  }) : Promise.resolve(emptyDiagnostics);