react-doctor 0.2.10 → 0.2.11-dev.d0f5206
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/{cli-logger-BRBUS1pE.js → cli-logger-Df45H6Lw.js} +221 -130
- package/dist/cli.js +27 -14
- package/dist/index.d.ts +17 -8
- package/dist/index.js +225 -133
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -21,7 +21,6 @@ import * as Option from "effect/Option";
|
|
|
21
21
|
import * as Ref from "effect/Ref";
|
|
22
22
|
import * as Stream from "effect/Stream";
|
|
23
23
|
import * as Cache from "effect/Cache";
|
|
24
|
-
import { Worker } from "node:worker_threads";
|
|
25
24
|
import * as NodeChildProcessSpawner from "@effect/platform-node-shared/NodeChildProcessSpawner";
|
|
26
25
|
import * as NodeFileSystem from "@effect/platform-node-shared/NodeFileSystem";
|
|
27
26
|
import * as NodePath from "@effect/platform-node-shared/NodePath";
|
|
@@ -2724,6 +2723,21 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
|
|
|
2724
2723
|
framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
|
|
2725
2724
|
};
|
|
2726
2725
|
};
|
|
2726
|
+
const someWorkspacePackageJson = (rootDirectory, rootPackageJson, predicate) => {
|
|
2727
|
+
if (predicate(rootPackageJson)) return true;
|
|
2728
|
+
const patterns = getWorkspacePatterns(rootDirectory, rootPackageJson);
|
|
2729
|
+
if (patterns.length === 0) return false;
|
|
2730
|
+
const visitedDirectories = /* @__PURE__ */ new Set();
|
|
2731
|
+
for (const pattern of patterns) {
|
|
2732
|
+
const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
|
|
2733
|
+
for (const workspaceDirectory of directories) {
|
|
2734
|
+
if (visitedDirectories.has(workspaceDirectory)) continue;
|
|
2735
|
+
visitedDirectories.add(workspaceDirectory);
|
|
2736
|
+
if (predicate(readPackageJson(path.join(workspaceDirectory, "package.json")))) return true;
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
return false;
|
|
2740
|
+
};
|
|
2727
2741
|
const NAMES = new Set([
|
|
2728
2742
|
"react-native",
|
|
2729
2743
|
"react-native-tvos",
|
|
@@ -2754,27 +2768,13 @@ const isPackageJsonReactNativeAware = (packageJson) => {
|
|
|
2754
2768
|
if (containsAnyReactNativeDependency(packageJson.optionalDependencies)) return true;
|
|
2755
2769
|
return false;
|
|
2756
2770
|
};
|
|
2757
|
-
const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) =>
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
if (patterns.length === 0) return false;
|
|
2761
|
-
const visitedDirectories = /* @__PURE__ */ new Set();
|
|
2762
|
-
for (const pattern of patterns) {
|
|
2763
|
-
const directories = resolveWorkspaceDirectories(rootDirectory, pattern);
|
|
2764
|
-
for (const workspaceDirectory of directories) {
|
|
2765
|
-
if (visitedDirectories.has(workspaceDirectory)) continue;
|
|
2766
|
-
visitedDirectories.add(workspaceDirectory);
|
|
2767
|
-
if (isPackageJsonReactNativeAware(readPackageJson(path.join(workspaceDirectory, "package.json")))) return true;
|
|
2768
|
-
}
|
|
2769
|
-
}
|
|
2770
|
-
return false;
|
|
2771
|
-
};
|
|
2772
|
-
const hasPreact = (packageJson) => {
|
|
2773
|
-
return "preact" in {
|
|
2771
|
+
const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
|
|
2772
|
+
const getPreactVersion = (packageJson) => {
|
|
2773
|
+
return {
|
|
2774
2774
|
...packageJson.peerDependencies,
|
|
2775
2775
|
...packageJson.dependencies,
|
|
2776
2776
|
...packageJson.devDependencies
|
|
2777
|
-
};
|
|
2777
|
+
}.preact ?? null;
|
|
2778
2778
|
};
|
|
2779
2779
|
const TANSTACK_QUERY_PACKAGES = new Set([
|
|
2780
2780
|
"@tanstack/react-query",
|
|
@@ -2789,6 +2789,16 @@ const hasTanStackQuery = (packageJson) => {
|
|
|
2789
2789
|
};
|
|
2790
2790
|
return Object.keys(allDependencies).some((packageName) => TANSTACK_QUERY_PACKAGES.has(packageName));
|
|
2791
2791
|
};
|
|
2792
|
+
const REANIMATED_DEPENDENCY_NAME = "react-native-reanimated";
|
|
2793
|
+
const isPackageJsonReanimatedAware = (packageJson) => {
|
|
2794
|
+
const allDependencies = {
|
|
2795
|
+
...packageJson.peerDependencies,
|
|
2796
|
+
...packageJson.dependencies,
|
|
2797
|
+
...packageJson.devDependencies,
|
|
2798
|
+
...packageJson.optionalDependencies
|
|
2799
|
+
};
|
|
2800
|
+
return Object.hasOwn(allDependencies, REANIMATED_DEPENDENCY_NAME);
|
|
2801
|
+
};
|
|
2792
2802
|
const hasUpperBoundOnlyPeerRange = (range) => {
|
|
2793
2803
|
if (typeof range !== "string") return false;
|
|
2794
2804
|
const normalizedRange = normalizeDependencyVersion(range);
|
|
@@ -2875,12 +2885,22 @@ const listManifestWorkspacePackages = (rootDirectory) => {
|
|
|
2875
2885
|
const nxPatterns = patterns.length > 0 ? [] : getNxWorkspaceDirectories(rootDirectory);
|
|
2876
2886
|
return toReactWorkspacePackages((patterns.length > 0 ? patterns : nxPatterns).flatMap((pattern) => resolveWorkspaceDirectories(rootDirectory, pattern)));
|
|
2877
2887
|
};
|
|
2888
|
+
const NON_PROJECT_DIRECTORIES = new Set([
|
|
2889
|
+
"AppData",
|
|
2890
|
+
"Application Data",
|
|
2891
|
+
"Library"
|
|
2892
|
+
]);
|
|
2893
|
+
const MAX_SCAN_DEPTH = 6;
|
|
2878
2894
|
const discoverReactSubprojectsByFilesystem = (rootDirectory) => {
|
|
2879
2895
|
const packages = [];
|
|
2880
|
-
const pendingDirectories = [
|
|
2896
|
+
const pendingDirectories = [{
|
|
2897
|
+
directory: rootDirectory,
|
|
2898
|
+
depth: 0
|
|
2899
|
+
}];
|
|
2881
2900
|
while (pendingDirectories.length > 0) {
|
|
2882
|
-
const
|
|
2883
|
-
if (!
|
|
2901
|
+
const current = pendingDirectories.pop();
|
|
2902
|
+
if (!current) continue;
|
|
2903
|
+
const { directory: currentDirectory, depth } = current;
|
|
2884
2904
|
const packageJsonPath = path.join(currentDirectory, "package.json");
|
|
2885
2905
|
if (isFile(packageJsonPath)) {
|
|
2886
2906
|
const packageJson = readPackageJson(packageJsonPath);
|
|
@@ -2892,10 +2912,14 @@ const discoverReactSubprojectsByFilesystem = (rootDirectory) => {
|
|
|
2892
2912
|
});
|
|
2893
2913
|
}
|
|
2894
2914
|
}
|
|
2915
|
+
if (depth >= MAX_SCAN_DEPTH) continue;
|
|
2895
2916
|
const entries = readDirectoryEntries(currentDirectory).toSorted((firstEntry, secondEntry) => firstEntry.name.localeCompare(secondEntry.name));
|
|
2896
2917
|
for (const entry of entries) {
|
|
2897
|
-
if (!entry.isDirectory() || entry.name.startsWith(".") || IGNORED_DIRECTORIES.has(entry.name)) continue;
|
|
2898
|
-
pendingDirectories.push(
|
|
2918
|
+
if (!entry.isDirectory() || entry.name.startsWith(".") || IGNORED_DIRECTORIES.has(entry.name) || NON_PROJECT_DIRECTORIES.has(entry.name)) continue;
|
|
2919
|
+
pendingDirectories.push({
|
|
2920
|
+
directory: path.join(currentDirectory, entry.name),
|
|
2921
|
+
depth: depth + 1
|
|
2922
|
+
});
|
|
2899
2923
|
}
|
|
2900
2924
|
}
|
|
2901
2925
|
return packages;
|
|
@@ -2966,6 +2990,8 @@ const discoverProject = (directory) => {
|
|
|
2966
2990
|
const hasTypeScript = fs.existsSync(path.join(directory, "tsconfig.json"));
|
|
2967
2991
|
const sourceFileCount = countSourceFiles(directory);
|
|
2968
2992
|
const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || hasReactNativeWorkspaceAnywhere(directory, packageJson);
|
|
2993
|
+
const hasReanimated = hasReactNativeWorkspace && someWorkspacePackageJson(directory, packageJson, isPackageJsonReanimatedAware);
|
|
2994
|
+
const preactVersion = getPreactVersion(packageJson);
|
|
2969
2995
|
const projectInfo = {
|
|
2970
2996
|
rootDirectory: directory,
|
|
2971
2997
|
projectName,
|
|
@@ -2976,13 +3002,16 @@ const discoverProject = (directory) => {
|
|
|
2976
3002
|
hasTypeScript,
|
|
2977
3003
|
hasReactCompiler: detectReactCompiler(directory, packageJson),
|
|
2978
3004
|
hasTanStackQuery: hasTanStackQuery(packageJson),
|
|
2979
|
-
|
|
3005
|
+
preactVersion,
|
|
3006
|
+
preactMajorVersion: parseReactMajor(preactVersion),
|
|
2980
3007
|
hasReactNativeWorkspace,
|
|
3008
|
+
hasReanimated,
|
|
2981
3009
|
sourceFileCount
|
|
2982
3010
|
};
|
|
2983
3011
|
cachedProjectInfos.set(directory, projectInfo);
|
|
2984
3012
|
return projectInfo;
|
|
2985
3013
|
};
|
|
3014
|
+
const isAnalyzableProject = (project) => project.reactVersion !== null || project.preactVersion !== null;
|
|
2986
3015
|
const MAJOR_MINOR_PATTERN = /(\d{1,4})\.(\d{1,4})/;
|
|
2987
3016
|
const MAJOR_ONLY_PATTERN = /(\d{1,4})/;
|
|
2988
3017
|
const UPPER_BOUND_COMPARATOR_PATTERN = /<=?\s{0,8}\d{1,4}(?:\.\d{1,4}){0,2}(?:-[^\s,|]+)?/g;
|
|
@@ -3948,17 +3977,26 @@ const layerOtlp = Layer.unwrap(Effect.gen(function* () {
|
|
|
3948
3977
|
headers
|
|
3949
3978
|
}).pipe(Layer.provide(FetchHttpClient.layer));
|
|
3950
3979
|
}).pipe(Effect.orDie));
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3980
|
+
/**
|
|
3981
|
+
* Per-batch oxlint wall-clock budget. Reads from the env var on
|
|
3982
|
+
* startup so the eval harness can raise the budget under sandbox
|
|
3983
|
+
* microVMs without recompiling react-doctor. Tests override via
|
|
3984
|
+
* `Layer.succeed(OxlintSpawnTimeoutMs, ...)`.
|
|
3985
|
+
*/
|
|
3986
|
+
var OxlintSpawnTimeoutMs = class extends Context.Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => {
|
|
3954
3987
|
const raw = process.env["REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS"];
|
|
3955
3988
|
if (raw === void 0) return OXLINT_SPAWN_TIMEOUT_MS;
|
|
3956
3989
|
const parsed = Number(raw);
|
|
3957
3990
|
if (!Number.isFinite(parsed) || parsed <= 0) return OXLINT_SPAWN_TIMEOUT_MS;
|
|
3958
3991
|
return parsed;
|
|
3959
|
-
} });
|
|
3960
|
-
|
|
3961
|
-
|
|
3992
|
+
} }) {};
|
|
3993
|
+
/**
|
|
3994
|
+
* Hard cap on combined stdout+stderr bytes per oxlint batch. The
|
|
3995
|
+
* subprocess gets SIGKILL'd if it produces more; the recovery path
|
|
3996
|
+
* suggests narrowing the scan with --diff. Override via Layer in
|
|
3997
|
+
* tests that exercise the cap behavior.
|
|
3998
|
+
*/
|
|
3999
|
+
var OxlintOutputMaxBytes = class extends Context.Reference("react-doctor/OxlintOutputMaxBytes", { defaultValue: () => OXLINT_OUTPUT_MAX_BYTES }) {};
|
|
3962
4000
|
const DIAGNOSTIC_SURFACES = [
|
|
3963
4001
|
"cli",
|
|
3964
4002
|
"prComment",
|
|
@@ -4570,47 +4608,57 @@ const collectIgnorePatterns = (rootDirectory) => {
|
|
|
4570
4608
|
};
|
|
4571
4609
|
const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
|
|
4572
4610
|
const DEAD_CODE_WORKER_SCRIPT = `
|
|
4573
|
-
const
|
|
4611
|
+
const inputChunks = [];
|
|
4612
|
+
process.stdin.on("data", (chunk) => inputChunks.push(chunk));
|
|
4613
|
+
process.stdin.on("end", () => {
|
|
4614
|
+
const workerInput = JSON.parse(Buffer.concat(inputChunks).toString("utf8"));
|
|
4574
4615
|
|
|
4575
|
-
const normalizeResult = (result) => ({
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
});
|
|
4616
|
+
const normalizeResult = (result) => ({
|
|
4617
|
+
unusedFiles: result.unusedFiles.map((unusedFile) => ({
|
|
4618
|
+
path: unusedFile.path,
|
|
4619
|
+
})),
|
|
4620
|
+
unusedExports: result.unusedExports.map((unusedExport) => ({
|
|
4621
|
+
path: unusedExport.path,
|
|
4622
|
+
name: unusedExport.name,
|
|
4623
|
+
line: unusedExport.line,
|
|
4624
|
+
column: unusedExport.column,
|
|
4625
|
+
isTypeOnly: unusedExport.isTypeOnly,
|
|
4626
|
+
})),
|
|
4627
|
+
unusedDependencies: result.unusedDependencies.map((unusedDependency) => ({
|
|
4628
|
+
name: unusedDependency.name,
|
|
4629
|
+
isDevDependency: unusedDependency.isDevDependency,
|
|
4630
|
+
})),
|
|
4631
|
+
circularDependencies: result.circularDependencies.map((cycle) => ({
|
|
4632
|
+
files: cycle.files,
|
|
4633
|
+
})),
|
|
4634
|
+
});
|
|
4594
4635
|
|
|
4595
|
-
const serializeError = (error) =>
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4636
|
+
const serializeError = (error) =>
|
|
4637
|
+
error instanceof Error
|
|
4638
|
+
? { name: error.name, message: error.message, stack: error.stack }
|
|
4639
|
+
: { message: String(error) };
|
|
4599
4640
|
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
}
|
|
4641
|
+
const emit = (message) => {
|
|
4642
|
+
process.stdout.write(JSON.stringify(message), () => process.exit(0));
|
|
4643
|
+
};
|
|
4644
|
+
|
|
4645
|
+
(async () => {
|
|
4646
|
+
try {
|
|
4647
|
+
const { analyze, defineConfig } = await import(workerInput.deslopJsModuleSpecifier);
|
|
4648
|
+
const config = {
|
|
4649
|
+
rootDir: workerInput.rootDirectory,
|
|
4650
|
+
...(workerInput.tsConfigPath ? { tsConfigPath: workerInput.tsConfigPath } : {}),
|
|
4651
|
+
...(workerInput.ignorePatterns.length > 0
|
|
4652
|
+
? { ignorePatterns: workerInput.ignorePatterns }
|
|
4653
|
+
: {}),
|
|
4654
|
+
};
|
|
4655
|
+
const result = await analyze(defineConfig(config));
|
|
4656
|
+
emit({ ok: true, result: normalizeResult(result) });
|
|
4657
|
+
} catch (error) {
|
|
4658
|
+
emit({ ok: false, error: serializeError(error) });
|
|
4659
|
+
}
|
|
4660
|
+
})();
|
|
4661
|
+
});
|
|
4614
4662
|
`;
|
|
4615
4663
|
const resolveTsConfigPath = (rootDirectory) => {
|
|
4616
4664
|
for (const filename of TSCONFIG_FILENAMES$1) {
|
|
@@ -4733,43 +4781,54 @@ const buildDeadCodeWorkerError = (workerError) => {
|
|
|
4733
4781
|
return error;
|
|
4734
4782
|
};
|
|
4735
4783
|
const createDeadCodeWorker = (input) => {
|
|
4736
|
-
const
|
|
4737
|
-
|
|
4738
|
-
|
|
4784
|
+
const child = spawn(process.execPath, ["-e", DEAD_CODE_WORKER_SCRIPT], {
|
|
4785
|
+
stdio: [
|
|
4786
|
+
"pipe",
|
|
4787
|
+
"pipe",
|
|
4788
|
+
"pipe"
|
|
4789
|
+
],
|
|
4790
|
+
windowsHide: true
|
|
4739
4791
|
});
|
|
4792
|
+
const stdoutChunks = [];
|
|
4793
|
+
const stderrChunks = [];
|
|
4794
|
+
child.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
|
|
4795
|
+
child.stderr.on("data", (chunk) => stderrChunks.push(chunk));
|
|
4740
4796
|
let didSettle = false;
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4797
|
+
const result = new Promise((resolve, reject) => {
|
|
4798
|
+
const settle = (callback) => {
|
|
4799
|
+
if (didSettle) return;
|
|
4800
|
+
didSettle = true;
|
|
4801
|
+
callback();
|
|
4802
|
+
};
|
|
4803
|
+
child.once("error", (error) => {
|
|
4804
|
+
settle(() => reject(error));
|
|
4805
|
+
});
|
|
4806
|
+
child.once("close", (exitCode) => {
|
|
4807
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf8").trim();
|
|
4808
|
+
if (stdout.length === 0) {
|
|
4809
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
|
|
4810
|
+
settle(() => reject(/* @__PURE__ */ new Error(`Dead-code worker exited with code ${exitCode ?? "null"}${stderr ? `: ${stderr}` : ""}.`)));
|
|
4811
|
+
return;
|
|
4812
|
+
}
|
|
4813
|
+
try {
|
|
4814
|
+
const parsedMessage = parseDeadCodeWorkerMessage(JSON.parse(stdout));
|
|
4815
|
+
if (parsedMessage.ok) {
|
|
4816
|
+
settle(() => resolve(parsedMessage.result));
|
|
4817
|
+
return;
|
|
4759
4818
|
}
|
|
4760
|
-
|
|
4761
|
-
|
|
4819
|
+
settle(() => reject(buildDeadCodeWorkerError(parsedMessage.error)));
|
|
4820
|
+
} catch (error) {
|
|
4762
4821
|
settle(() => reject(error));
|
|
4763
|
-
}
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4822
|
+
}
|
|
4823
|
+
});
|
|
4824
|
+
});
|
|
4825
|
+
child.stdin.on("error", () => {});
|
|
4826
|
+
child.stdin.end(JSON.stringify(input));
|
|
4827
|
+
return {
|
|
4828
|
+
result,
|
|
4769
4829
|
terminate: () => {
|
|
4770
4830
|
didSettle = true;
|
|
4771
|
-
|
|
4772
|
-
return worker.terminate();
|
|
4831
|
+
child.kill("SIGKILL");
|
|
4773
4832
|
}
|
|
4774
4833
|
};
|
|
4775
4834
|
};
|
|
@@ -5011,8 +5070,15 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
|
|
|
5011
5070
|
env: input.env,
|
|
5012
5071
|
extendEnv: true
|
|
5013
5072
|
}));
|
|
5073
|
+
const maxStdoutBytes = input.maxStdoutBytes;
|
|
5074
|
+
const stdoutByteCount = yield* Ref.make(0);
|
|
5075
|
+
const stdoutStream = maxStdoutBytes === void 0 ? handle.stdout : handle.stdout.pipe(Stream.tap((chunk) => Ref.updateAndGet(stdoutByteCount, (total) => total + chunk.length).pipe(Effect.flatMap((total) => total > maxStdoutBytes ? Effect.fail(new ReactDoctorError({ reason: new GitInvocationFailed({
|
|
5076
|
+
args: [...input.args],
|
|
5077
|
+
directory: input.directory,
|
|
5078
|
+
cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
|
|
5079
|
+
}) })) : Effect.void))));
|
|
5014
5080
|
const [stdout, stderr, status] = yield* Effect.all([
|
|
5015
|
-
Stream.mkString(Stream.decodeText(
|
|
5081
|
+
Stream.mkString(Stream.decodeText(stdoutStream)),
|
|
5016
5082
|
Stream.mkString(Stream.decodeText(handle.stderr)),
|
|
5017
5083
|
handle.exitCode
|
|
5018
5084
|
], { concurrency: 3 });
|
|
@@ -5174,7 +5240,12 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
|
|
|
5174
5240
|
if (result.status !== 0) return [];
|
|
5175
5241
|
return splitNullSeparated(result.stdout);
|
|
5176
5242
|
})),
|
|
5177
|
-
showStagedContent: (directory, relativePath) =>
|
|
5243
|
+
showStagedContent: (directory, relativePath, options) => runCommand({
|
|
5244
|
+
command: "git",
|
|
5245
|
+
args: ["show", `:${relativePath}`],
|
|
5246
|
+
directory,
|
|
5247
|
+
maxStdoutBytes: options?.maxBufferBytes
|
|
5248
|
+
}).pipe(Effect.map((result) => result.status === 0 ? result.stdout : null)),
|
|
5178
5249
|
grep: (input) => Effect.gen(function* () {
|
|
5179
5250
|
const args = ["grep"];
|
|
5180
5251
|
if (input.listMatchingFiles ?? true) args.push("-l");
|
|
@@ -5182,7 +5253,12 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
|
|
|
5182
5253
|
if (input.extendedRegexp ?? false) args.push("-E");
|
|
5183
5254
|
args.push(input.pattern);
|
|
5184
5255
|
if (input.includePaths && input.includePaths.length > 0) args.push("--", ...input.includePaths);
|
|
5185
|
-
const result = yield*
|
|
5256
|
+
const result = yield* runCommand({
|
|
5257
|
+
command: "git",
|
|
5258
|
+
args,
|
|
5259
|
+
directory: input.directory,
|
|
5260
|
+
maxStdoutBytes: input.maxBufferBytes
|
|
5261
|
+
});
|
|
5186
5262
|
if (result.status === 128) return null;
|
|
5187
5263
|
return {
|
|
5188
5264
|
status: result.status,
|
|
@@ -5430,7 +5506,8 @@ const buildCapabilities = (project) => {
|
|
|
5430
5506
|
if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
|
|
5431
5507
|
const reactMajor = project.reactMajorVersion;
|
|
5432
5508
|
if (reactMajor !== null) {
|
|
5433
|
-
|
|
5509
|
+
const cappedReactMajor = Math.min(reactMajor, 30);
|
|
5510
|
+
for (let major = 17; major <= cappedReactMajor; major++) capabilities.add(`react:${major}`);
|
|
5434
5511
|
if (reactMajor >= 19) {
|
|
5435
5512
|
if (isReactAtLeast(parseReactMajorMinor(project.reactVersion), {
|
|
5436
5513
|
major: 19,
|
|
@@ -5448,8 +5525,13 @@ const buildCapabilities = (project) => {
|
|
|
5448
5525
|
if (project.hasReactCompiler) capabilities.add("react-compiler");
|
|
5449
5526
|
if (project.hasTanStackQuery) capabilities.add("tanstack-query");
|
|
5450
5527
|
if (project.hasTypeScript) capabilities.add("typescript");
|
|
5451
|
-
if (project.
|
|
5528
|
+
if (project.preactVersion !== null) {
|
|
5452
5529
|
capabilities.add("preact");
|
|
5530
|
+
const preactMajor = project.preactMajorVersion;
|
|
5531
|
+
if (preactMajor !== null) {
|
|
5532
|
+
const cappedPreactMajor = Math.min(preactMajor, 20);
|
|
5533
|
+
for (let major = 10; major <= cappedPreactMajor; major++) capabilities.add(`preact:${major}`);
|
|
5534
|
+
}
|
|
5453
5535
|
if (project.reactVersion === null) capabilities.add("pure-preact");
|
|
5454
5536
|
}
|
|
5455
5537
|
return capabilities;
|
|
@@ -5706,6 +5788,13 @@ const buildNoSecretsRecommendation = (project, fallbackRecommendation) => {
|
|
|
5706
5788
|
if (!publicEnvPrefix) return fallbackRecommendation;
|
|
5707
5789
|
return `Move secrets to server-only code. In ${formatFrameworkName(project.framework)}, only \`${publicEnvPrefix}\` env vars are exposed to the browser, and they must not contain secrets`;
|
|
5708
5790
|
};
|
|
5791
|
+
const REANIMATED_SHARED_VALUE_HINT = "If this is a Reanimated shared value, prefer its React Compiler-compatible `.get()` / `.set()` accessors over `.value` — https://docs.swmansion.com/react-native-reanimated/docs/core/useSharedValue/#react-compiler-support";
|
|
5792
|
+
const appendReanimatedSharedValueHint = (help, rule, project) => {
|
|
5793
|
+
if (rule !== "immutability") return help;
|
|
5794
|
+
if (!project.hasReanimated) return help;
|
|
5795
|
+
if (!help) return REANIMATED_SHARED_VALUE_HINT;
|
|
5796
|
+
return `${help}\n\n${REANIMATED_SHARED_VALUE_HINT}`;
|
|
5797
|
+
};
|
|
5709
5798
|
const REACT_MODULE_SOURCE = "react";
|
|
5710
5799
|
const REQUIRE_IDENTIFIER = "require";
|
|
5711
5800
|
const USE_IDENTIFIER = "use";
|
|
@@ -6029,7 +6118,7 @@ const getRuleCategory = (ruleName) => reactDoctorPlugin.rules[ruleName]?.categor
|
|
|
6029
6118
|
const cleanDiagnosticMessage = (message, help, plugin, rule, project) => {
|
|
6030
6119
|
if (plugin === "react-hooks-js") return {
|
|
6031
6120
|
message: REACT_COMPILER_MESSAGE,
|
|
6032
|
-
help: message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || help
|
|
6121
|
+
help: appendReanimatedSharedValueHint(message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || help, rule, project)
|
|
6033
6122
|
};
|
|
6034
6123
|
return {
|
|
6035
6124
|
message: message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || message,
|
|
@@ -6098,13 +6187,6 @@ const SANITIZED_ENV = (() => {
|
|
|
6098
6187
|
}
|
|
6099
6188
|
return sanitized;
|
|
6100
6189
|
})();
|
|
6101
|
-
const OXLINT_SPAWN_TIMEOUT_MS$1 = (() => {
|
|
6102
|
-
const raw = process.env["REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS"];
|
|
6103
|
-
if (raw === void 0) return OXLINT_SPAWN_TIMEOUT_MS;
|
|
6104
|
-
const parsed = Number(raw);
|
|
6105
|
-
if (!Number.isFinite(parsed) || parsed <= 0) return OXLINT_SPAWN_TIMEOUT_MS;
|
|
6106
|
-
return parsed;
|
|
6107
|
-
})();
|
|
6108
6190
|
/**
|
|
6109
6191
|
* Spawn one oxlint subprocess with hard ceilings on wall time and
|
|
6110
6192
|
* output size. Returns stdout on success; raises a tagged
|
|
@@ -6121,7 +6203,7 @@ const OXLINT_SPAWN_TIMEOUT_MS$1 = (() => {
|
|
|
6121
6203
|
* The first three are splittable (the caller's binary-split retry
|
|
6122
6204
|
* shrinks the batch and re-spawns); the fourth isn't.
|
|
6123
6205
|
*/
|
|
6124
|
-
const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolve, reject) => {
|
|
6206
|
+
const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLINT_SPAWN_TIMEOUT_MS, outputMaxBytes = OXLINT_OUTPUT_MAX_BYTES) => new Promise((resolve, reject) => {
|
|
6125
6207
|
const child = spawn(nodeBinaryPath, args, {
|
|
6126
6208
|
cwd: rootDirectory,
|
|
6127
6209
|
env: SANITIZED_ENV
|
|
@@ -6130,9 +6212,9 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
|
|
|
6130
6212
|
child.kill("SIGKILL");
|
|
6131
6213
|
reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
|
|
6132
6214
|
kind: "timeout",
|
|
6133
|
-
detail: `${
|
|
6215
|
+
detail: `${spawnTimeoutMs / 1e3}s budget exceeded`
|
|
6134
6216
|
}) }));
|
|
6135
|
-
},
|
|
6217
|
+
}, spawnTimeoutMs);
|
|
6136
6218
|
timeoutHandle.unref?.();
|
|
6137
6219
|
const stdoutBuffers = [];
|
|
6138
6220
|
const stderrBuffers = [];
|
|
@@ -6142,7 +6224,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
|
|
|
6142
6224
|
const killIfTooLarge = (incomingBytes, isStdout) => {
|
|
6143
6225
|
if (isStdout) stdoutByteCount += incomingBytes;
|
|
6144
6226
|
else stderrByteCount += incomingBytes;
|
|
6145
|
-
if (stdoutByteCount + stderrByteCount >
|
|
6227
|
+
if (stdoutByteCount + stderrByteCount > outputMaxBytes && !didKillForSize) {
|
|
6146
6228
|
didKillForSize = true;
|
|
6147
6229
|
child.kill("SIGKILL");
|
|
6148
6230
|
return true;
|
|
@@ -6168,7 +6250,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
|
|
|
6168
6250
|
if (didKillForSize) {
|
|
6169
6251
|
reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
|
|
6170
6252
|
kind: "output-too-large",
|
|
6171
|
-
detail: `exceeded ${
|
|
6253
|
+
detail: `exceeded ${outputMaxBytes} bytes — scan a smaller subset with --diff or --staged`
|
|
6172
6254
|
}) }));
|
|
6173
6255
|
return;
|
|
6174
6256
|
}
|
|
@@ -6209,7 +6291,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath) => new Promise((resolv
|
|
|
6209
6291
|
* with a slimmer config in that case.
|
|
6210
6292
|
*/
|
|
6211
6293
|
const spawnLintBatches = async (input) => {
|
|
6212
|
-
const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress } = input;
|
|
6294
|
+
const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes } = input;
|
|
6213
6295
|
const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
|
|
6214
6296
|
const allDiagnostics = [];
|
|
6215
6297
|
const droppedFiles = [];
|
|
@@ -6217,7 +6299,7 @@ const spawnLintBatches = async (input) => {
|
|
|
6217
6299
|
const spawnLintBatch = async (batch) => {
|
|
6218
6300
|
const batchArgs = [...baseArgs, ...batch];
|
|
6219
6301
|
try {
|
|
6220
|
-
return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath), project, rootDirectory);
|
|
6302
|
+
return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes), project, rootDirectory);
|
|
6221
6303
|
} catch (error) {
|
|
6222
6304
|
if (!isSplittableReactDoctorError(error)) throw error;
|
|
6223
6305
|
if (batch.length <= 1) {
|
|
@@ -6320,13 +6402,11 @@ const writeOxlintConfig = (configPath, configToWrite) => {
|
|
|
6320
6402
|
* 6. always restore disable directives + clean up the temp dir
|
|
6321
6403
|
*/
|
|
6322
6404
|
const runOxlint = async (options) => {
|
|
6323
|
-
const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure } = options;
|
|
6405
|
+
const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure, spawnTimeoutMs, outputMaxBytes } = options;
|
|
6324
6406
|
const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
|
|
6325
6407
|
const severityControls = buildRuleSeverityControls(userConfig);
|
|
6326
6408
|
validateRuleRegistration();
|
|
6327
6409
|
if (includePaths !== void 0 && includePaths.length === 0) return [];
|
|
6328
|
-
const configDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
|
|
6329
|
-
const configPath = path.join(configDirectory, "oxlintrc.json");
|
|
6330
6410
|
const pluginPath = resolvePluginPath();
|
|
6331
6411
|
const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
|
|
6332
6412
|
const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
|
|
@@ -6341,6 +6421,8 @@ const runOxlint = async (options) => {
|
|
|
6341
6421
|
userPlugins
|
|
6342
6422
|
});
|
|
6343
6423
|
const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
|
|
6424
|
+
const configDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
|
|
6425
|
+
const configPath = path.join(configDirectory, "oxlintrc.json");
|
|
6344
6426
|
try {
|
|
6345
6427
|
const baseArgs = [
|
|
6346
6428
|
resolveOxlintBinary(),
|
|
@@ -6367,7 +6449,9 @@ const runOxlint = async (options) => {
|
|
|
6367
6449
|
nodeBinaryPath,
|
|
6368
6450
|
project,
|
|
6369
6451
|
onPartialFailure,
|
|
6370
|
-
onFileProgress: options.onFileProgress
|
|
6452
|
+
onFileProgress: options.onFileProgress,
|
|
6453
|
+
spawnTimeoutMs,
|
|
6454
|
+
outputMaxBytes
|
|
6371
6455
|
});
|
|
6372
6456
|
writeOxlintConfig(configPath, buildConfig(extendsPaths));
|
|
6373
6457
|
try {
|
|
@@ -6433,6 +6517,8 @@ var Linter = class Linter extends Context.Service()("react-doctor/Linter") {
|
|
|
6433
6517
|
*/
|
|
6434
6518
|
static layerOxlint = Layer.succeed(Linter, Linter.of({ run: (input) => Stream.unwrap(Effect.fn("Linter.run")(function* () {
|
|
6435
6519
|
const partialFailures = yield* LintPartialFailures;
|
|
6520
|
+
const spawnTimeoutMs = yield* OxlintSpawnTimeoutMs;
|
|
6521
|
+
const outputMaxBytes = yield* OxlintOutputMaxBytes;
|
|
6436
6522
|
const collectedFailures = [];
|
|
6437
6523
|
const diagnostics = yield* Effect.tryPromise({
|
|
6438
6524
|
try: () => runOxlint({
|
|
@@ -6449,7 +6535,9 @@ var Linter = class Linter extends Context.Service()("react-doctor/Linter") {
|
|
|
6449
6535
|
onPartialFailure: (reason) => {
|
|
6450
6536
|
collectedFailures.push(reason);
|
|
6451
6537
|
},
|
|
6452
|
-
onFileProgress: input.onFileProgress
|
|
6538
|
+
onFileProgress: input.onFileProgress,
|
|
6539
|
+
spawnTimeoutMs,
|
|
6540
|
+
outputMaxBytes
|
|
6453
6541
|
}),
|
|
6454
6542
|
catch: ensureReactDoctorError
|
|
6455
6543
|
});
|
|
@@ -6747,7 +6835,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
6747
6835
|
const resolvedConfig = yield* configService.resolve(input.directory);
|
|
6748
6836
|
const scanDirectory = resolvedConfig.resolvedDirectory;
|
|
6749
6837
|
const project = yield* projectService.discover(scanDirectory);
|
|
6750
|
-
if (project
|
|
6838
|
+
if (!isAnalyzableProject(project)) return yield* new ReactDoctorError({ reason: new NoReactDependency({ directory: scanDirectory }) });
|
|
6751
6839
|
const [repo, sha, defaultBranch] = yield* Effect.all([
|
|
6752
6840
|
gitService.githubRepo(scanDirectory).pipe(Effect.orElseSucceed(() => null)),
|
|
6753
6841
|
gitService.headSha(scanDirectory).pipe(Effect.orElseSucceed(() => null)),
|
|
@@ -6775,7 +6863,8 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
6775
6863
|
const lintFailure = yield* Ref.make({
|
|
6776
6864
|
didFail: false,
|
|
6777
6865
|
reason: null,
|
|
6778
|
-
reasonTag: null
|
|
6866
|
+
reasonTag: null,
|
|
6867
|
+
reasonKind: null
|
|
6779
6868
|
});
|
|
6780
6869
|
const deadCodeFailure = yield* Ref.make({
|
|
6781
6870
|
didFail: false,
|
|
@@ -6797,13 +6886,14 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
6797
6886
|
configSourceDirectory: resolvedConfig.configSourceDirectory ?? void 0,
|
|
6798
6887
|
onFileProgress: (scannedFileCount, totalFileCount) => {
|
|
6799
6888
|
lastReportedTotalFileCount = totalFileCount;
|
|
6800
|
-
Effect.runSync(scanProgress.update(`Scanning (${scannedFileCount}/${totalFileCount})...`));
|
|
6889
|
+
Effect.runSync(scanProgress.update(`Scanning files (${scannedFileCount}/${totalFileCount})...`));
|
|
6801
6890
|
}
|
|
6802
6891
|
}).pipe(Stream.catchTag("ReactDoctorError", (error) => Stream.unwrap(Effect.gen(function* () {
|
|
6803
6892
|
yield* Ref.set(lintFailure, {
|
|
6804
6893
|
didFail: true,
|
|
6805
6894
|
reason: error.message,
|
|
6806
|
-
reasonTag: error.reason._tag
|
|
6895
|
+
reasonTag: error.reason._tag,
|
|
6896
|
+
reasonKind: error.reason._tag === "OxlintUnavailable" ? error.reason.kind : null
|
|
6807
6897
|
});
|
|
6808
6898
|
return Stream.empty;
|
|
6809
6899
|
}))));
|
|
@@ -6863,6 +6953,7 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
|
|
|
6863
6953
|
didLintFail: lintFailureState.didFail,
|
|
6864
6954
|
lintFailureReason: lintFailureState.reason,
|
|
6865
6955
|
lintFailureReasonTag: lintFailureState.reasonTag,
|
|
6956
|
+
lintFailureReasonKind: lintFailureState.reasonKind,
|
|
6866
6957
|
lintPartialFailures,
|
|
6867
6958
|
didDeadCodeFail: deadCodeFailureState.didFail,
|
|
6868
6959
|
deadCodeFailureReason: deadCodeFailureState.reason
|
|
@@ -7297,11 +7388,12 @@ const buildInspectProgram = (scanTarget, options, configOverride) => {
|
|
|
7297
7388
|
const outputToDiagnoseResult = (output, elapsedMilliseconds) => {
|
|
7298
7389
|
if (output.didLintFail && output.lintFailureReason !== null) console.error("Lint failed:", output.lintFailureReason);
|
|
7299
7390
|
const skippedChecks = [];
|
|
7391
|
+
if (output.didLintFail) skippedChecks.push("lint");
|
|
7392
|
+
if (output.didDeadCodeFail) skippedChecks.push("dead-code");
|
|
7300
7393
|
const skippedCheckReasons = {};
|
|
7301
|
-
if (output.
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
}
|
|
7394
|
+
if (output.didLintFail && output.lintFailureReason !== null) skippedCheckReasons.lint = output.lintFailureReason;
|
|
7395
|
+
else if (output.lintPartialFailures.length > 0) skippedCheckReasons["lint:partial"] = output.lintPartialFailures.join("; ");
|
|
7396
|
+
if (output.didDeadCodeFail && output.deadCodeFailureReason !== null) skippedCheckReasons["dead-code"] = output.deadCodeFailureReason;
|
|
7305
7397
|
return {
|
|
7306
7398
|
diagnostics: [...output.diagnostics],
|
|
7307
7399
|
score: output.score,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-doctor",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11-dev.d0f5206",
|
|
4
4
|
"description": "Diagnose and fix React codebases for security, performance, correctness, accessibility, bundle-size, and architecture issues",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"accessibility",
|
|
@@ -58,14 +58,14 @@
|
|
|
58
58
|
"oxlint": "^1.66.0",
|
|
59
59
|
"prompts": "^2.4.2",
|
|
60
60
|
"typescript": ">=5.0.4 <7",
|
|
61
|
-
"oxlint-plugin-react-doctor": "0.2.
|
|
61
|
+
"oxlint-plugin-react-doctor": "0.2.11-dev.d0f5206"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@types/prompts": "^2.4.9",
|
|
65
65
|
"commander": "^14.0.3",
|
|
66
66
|
"ora": "^9.4.0",
|
|
67
|
-
"@react-doctor/api": "0.2.
|
|
68
|
-
"@react-doctor/core": "0.2.
|
|
67
|
+
"@react-doctor/api": "0.2.11",
|
|
68
|
+
"@react-doctor/core": "0.2.11"
|
|
69
69
|
},
|
|
70
70
|
"engines": {
|
|
71
71
|
"node": "^20.19.0 || >=22.12.0"
|