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/README.md +23 -0
- package/dist/cli.js +121 -29
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +66 -13
- package/dist/index.js.map +1 -1
- package/dist/react-doctor-plugin.js +8 -0
- package/dist/react-doctor-plugin.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
//#region src/types.d.ts
|
|
2
|
-
type
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
|
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 (
|
|
416
|
-
|
|
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";
|