react-doctor 0.0.28 → 0.0.29

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/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  //#region src/types.d.ts
2
- type Framework = "nextjs" | "vite" | "cra" | "remix" | "gatsby" | "unknown";
2
+ type FailOnLevel = "error" | "warning" | "none";
3
+ type Framework = "nextjs" | "vite" | "cra" | "remix" | "gatsby" | "expo" | "react-native" | "unknown";
3
4
  interface ProjectInfo {
4
5
  rootDirectory: string;
5
6
  projectName: string;
@@ -41,6 +42,7 @@ interface ReactDoctorConfig {
41
42
  deadCode?: boolean;
42
43
  verbose?: boolean;
43
44
  diff?: boolean | string;
45
+ failOn?: FailOnLevel;
44
46
  }
45
47
  //#endregion
46
48
  //#region src/utils/get-diff-files.d.ts
@@ -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,SAAA;AAAA,UAEK,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;AAAA;;;cCrFW,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;;;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"}
package/dist/index.js CHANGED
@@ -194,6 +194,7 @@ const combineDiagnostics = (lintDiagnostics, deadCodeDiagnostics, directory, isD
194
194
  //#region src/utils/find-monorepo-root.ts
195
195
  const isMonorepoRoot = (directory) => {
196
196
  if (fs.existsSync(path.join(directory, "pnpm-workspace.yaml"))) return true;
197
+ if (fs.existsSync(path.join(directory, "nx.json"))) return true;
197
198
  const packageJsonPath = path.join(directory, "package.json");
198
199
  if (!fs.existsSync(packageJsonPath)) return false;
199
200
  const packageJson = readPackageJson(packageJsonPath);
@@ -246,9 +247,33 @@ const FRAMEWORK_PACKAGES = {
246
247
  vite: "vite",
247
248
  "react-scripts": "cra",
248
249
  "@remix-run/react": "remix",
249
- gatsby: "gatsby"
250
+ gatsby: "gatsby",
251
+ expo: "expo",
252
+ "react-native": "react-native"
250
253
  };
251
- const countSourceFiles = (rootDirectory) => {
254
+ const IGNORED_DIRECTORIES = new Set([
255
+ "node_modules",
256
+ "dist",
257
+ "build",
258
+ "coverage"
259
+ ]);
260
+ const countSourceFilesViaFilesystem = (rootDirectory) => {
261
+ let count = 0;
262
+ const stack = [rootDirectory];
263
+ while (stack.length > 0) {
264
+ const currentDirectory = stack.pop();
265
+ const entries = fs.readdirSync(currentDirectory, { withFileTypes: true });
266
+ for (const entry of entries) {
267
+ if (entry.isDirectory()) {
268
+ if (!entry.name.startsWith(".") && !IGNORED_DIRECTORIES.has(entry.name)) stack.push(path.join(currentDirectory, entry.name));
269
+ continue;
270
+ }
271
+ if (entry.isFile() && SOURCE_FILE_PATTERN.test(entry.name)) count++;
272
+ }
273
+ }
274
+ return count;
275
+ };
276
+ const countSourceFilesViaGit = (rootDirectory) => {
252
277
  const result = spawnSync("git", [
253
278
  "ls-files",
254
279
  "--cached",
@@ -259,9 +284,10 @@ const countSourceFiles = (rootDirectory) => {
259
284
  encoding: "utf-8",
260
285
  maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
261
286
  });
262
- if (result.error || result.status !== 0) return 0;
287
+ if (result.error || result.status !== 0) return null;
263
288
  return result.stdout.split("\n").filter((filePath) => filePath.length > 0 && SOURCE_FILE_PATTERN.test(filePath)).length;
264
289
  };
290
+ const countSourceFiles = (rootDirectory) => countSourceFilesViaGit(rootDirectory) ?? countSourceFilesViaFilesystem(rootDirectory);
265
291
  const collectAllDependencies = (packageJson) => ({
266
292
  ...packageJson.peerDependencies,
267
293
  ...packageJson.dependencies,
@@ -412,14 +438,10 @@ const loadConfig = (rootDirectory) => {
412
438
  if (fs.existsSync(configFilePath)) try {
413
439
  const fileContent = fs.readFileSync(configFilePath, "utf-8");
414
440
  const parsed = JSON.parse(fileContent);
415
- if (!isPlainObject(parsed)) {
416
- console.warn(`Warning: ${CONFIG_FILENAME} must be a JSON object, ignoring.`);
417
- return null;
418
- }
419
- return parsed;
441
+ if (isPlainObject(parsed)) return parsed;
442
+ console.warn(`Warning: ${CONFIG_FILENAME} must be a JSON object, ignoring.`);
420
443
  } catch (error) {
421
444
  console.warn(`Warning: Failed to parse ${CONFIG_FILENAME}: ${error instanceof Error ? error.message : String(error)}`);
422
- return null;
423
445
  }
424
446
  const packageJsonPath = path.join(rootDirectory, "package.json");
425
447
  if (fs.existsSync(packageJsonPath)) try {
@@ -490,11 +512,15 @@ const CONFIG_LOADING_ERROR_PATTERN = /Error loading .*\/([a-z-]+)\.config\./;
490
512
  const extractFailedPluginName = (error) => {
491
513
  return String(error).match(CONFIG_LOADING_ERROR_PATTERN)?.[1] ?? null;
492
514
  };
515
+ const TSCONFIG_FILENAMES = ["tsconfig.base.json", "tsconfig.json"];
516
+ const resolveTsConfigFile = (directory) => TSCONFIG_FILENAMES.find((filename) => fs.existsSync(path.join(directory, filename)));
493
517
  const runKnipWithOptions = async (knipCwd, workspaceName) => {
518
+ const tsConfigFile = resolveTsConfigFile(knipCwd);
494
519
  const options = await silenced(() => createOptions({
495
520
  cwd: knipCwd,
496
521
  isShowProgress: false,
497
- ...workspaceName ? { workspace: workspaceName } : {}
522
+ ...workspaceName ? { workspace: workspaceName } : {},
523
+ ...tsConfigFile ? { tsConfigFile } : {}
498
524
  }));
499
525
  const parsedConfig = options.parsedConfig;
500
526
  for (let attempt = 0; attempt <= MAX_KNIP_RETRIES; attempt++) try {
@@ -566,6 +592,16 @@ const NEXTJS_RULES = {
566
592
  "react-doctor/nextjs-no-head-import": "error",
567
593
  "react-doctor/nextjs-no-side-effect-in-get-handler": "error"
568
594
  };
595
+ const REACT_NATIVE_RULES = {
596
+ "react-doctor/rn-no-raw-text": "error",
597
+ "react-doctor/rn-no-deprecated-modules": "error",
598
+ "react-doctor/rn-no-legacy-expo-packages": "warn",
599
+ "react-doctor/rn-no-dimensions-get": "warn",
600
+ "react-doctor/rn-no-inline-flatlist-renderitem": "warn",
601
+ "react-doctor/rn-no-legacy-shadow-styles": "warn",
602
+ "react-doctor/rn-prefer-reanimated": "warn",
603
+ "react-doctor/rn-no-single-element-style-array": "warn"
604
+ };
569
605
  const REACT_COMPILER_RULES = {
570
606
  "react-hooks-js/set-state-in-render": "error",
571
607
  "react-hooks-js/immutability": "error",
@@ -669,7 +705,8 @@ const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler }) => ({
669
705
  "react-doctor/server-after-nonblocking": "warn",
670
706
  "react-doctor/client-passive-event-listeners": "warn",
671
707
  "react-doctor/async-parallel": "warn",
672
- ...framework === "nextjs" ? NEXTJS_RULES : {}
708
+ ...framework === "nextjs" ? NEXTJS_RULES : {},
709
+ ...framework === "expo" || framework === "react-native" ? REACT_NATIVE_RULES : {}
673
710
  }
674
711
  });
675
712
 
@@ -777,7 +814,15 @@ const RULE_CATEGORY_MAP = {
777
814
  "react-doctor/server-auth-actions": "Server",
778
815
  "react-doctor/server-after-nonblocking": "Server",
779
816
  "react-doctor/client-passive-event-listeners": "Performance",
780
- "react-doctor/async-parallel": "Performance"
817
+ "react-doctor/async-parallel": "Performance",
818
+ "react-doctor/rn-no-raw-text": "React Native",
819
+ "react-doctor/rn-no-deprecated-modules": "React Native",
820
+ "react-doctor/rn-no-legacy-expo-packages": "React Native",
821
+ "react-doctor/rn-no-dimensions-get": "React Native",
822
+ "react-doctor/rn-no-inline-flatlist-renderitem": "React Native",
823
+ "react-doctor/rn-no-legacy-shadow-styles": "React Native",
824
+ "react-doctor/rn-prefer-reanimated": "React Native",
825
+ "react-doctor/rn-no-single-element-style-array": "React Native"
781
826
  };
782
827
  const RULE_HELP_MAP = {
783
828
  "no-derived-state-effect": "For derived state, compute inline: `const x = fn(dep)`. For state resets on prop change, use a key prop: `<Component key={prop} />`",
@@ -833,7 +878,15 @@ const RULE_HELP_MAP = {
833
878
  "server-auth-actions": "Add `const session = await auth()` at the top and throw/redirect if unauthorized before any data access",
834
879
  "server-after-nonblocking": "`import { after } from 'next/server'` then wrap: `after(() => analytics.track(...))` — response isn't blocked",
835
880
  "client-passive-event-listeners": "Add `{ passive: true }` as the third argument: `addEventListener('scroll', handler, { passive: true })`",
836
- "async-parallel": "Use `const [a, b] = await Promise.all([fetchA(), fetchB()])` to run independent operations concurrently"
881
+ "async-parallel": "Use `const [a, b] = await Promise.all([fetchA(), fetchB()])` to run independent operations concurrently",
882
+ "rn-no-raw-text": "Wrap text in a `<Text>` component: `<Text>{value}</Text>` — raw strings outside `<Text>` crash on React Native",
883
+ "rn-no-deprecated-modules": "Import from the community package instead — deprecated modules were removed from the react-native core",
884
+ "rn-no-legacy-expo-packages": "Migrate to the recommended replacement package — legacy Expo packages are no longer maintained",
885
+ "rn-no-dimensions-get": "Use `const { width, height } = useWindowDimensions()` — it updates reactively on rotation and resize",
886
+ "rn-no-inline-flatlist-renderitem": "Extract renderItem to a named function or wrap in useCallback to avoid re-creating on every render",
887
+ "rn-no-legacy-shadow-styles": "Use `boxShadow` for cross-platform shadows on the new architecture instead of platform-specific shadow properties",
888
+ "rn-prefer-reanimated": "Use `import Animated from 'react-native-reanimated'` — animations run on the UI thread instead of the JS thread",
889
+ "rn-no-single-element-style-array": "Use `style={value}` instead of `style={[value]}` — single-element arrays add unnecessary allocation"
837
890
  };
838
891
  const FILEPATH_WITH_LOCATION_PATTERN = /\S+\.\w+:\d+:\d+[\s\S]*$/;
839
892
  const REACT_COMPILER_MESSAGE = "React Compiler can't optimize this code";