react-doctor 0.6.3 → 0.7.0-dev.6b21b70
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.js +1049 -289
- package/dist/index.d.ts +66 -1
- package/dist/index.js +672 -82
- package/dist/lsp.js +671 -86
- package/package.json +6 -6
package/dist/lsp.js
CHANGED
|
@@ -8,13 +8,14 @@ import path from "node:path";
|
|
|
8
8
|
import * as NodeChildProcess from "node:child_process";
|
|
9
9
|
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
10
10
|
import * as ts from "typescript";
|
|
11
|
-
import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
|
|
11
|
+
import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, collectCrossFileDependencyProbes, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
|
|
12
12
|
import { parseJSON5 } from "confbox";
|
|
13
13
|
import * as NodeUrl from "node:url";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
import { createJiti } from "jiti";
|
|
16
16
|
import * as Crypto from "node:crypto";
|
|
17
17
|
import crypto, { createHash } from "node:crypto";
|
|
18
|
+
import { ANALYZED_MANIFEST_FILENAMES, DEFAULT_EXTENSIONS } from "deslop-js/analyzed-inputs";
|
|
18
19
|
import { gzipSync } from "node:zlib";
|
|
19
20
|
import { CodeActionKind, CodeActionTriggerKind, DidChangeWatchedFilesNotification, DocumentDiagnosticReportKind, FileChangeType, TextDocumentSyncKind, TextDocuments, createConnection } from "vscode-languageserver/node.js";
|
|
20
21
|
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
@@ -19409,6 +19410,16 @@ var JsonReportV1 = class extends Class("JsonReportV1")({
|
|
|
19409
19410
|
* successful `baseline` run (`schemaVersion: 2`) and on non-compare scopes.
|
|
19410
19411
|
*/
|
|
19411
19412
|
baselineDegraded: optional(Boolean$1),
|
|
19413
|
+
/**
|
|
19414
|
+
* Whether any scanned project resolved a React-compatible runtime (React
|
|
19415
|
+
* or Preact). `false` means every React-runtime rule family was gated off,
|
|
19416
|
+
* so an empty `diagnostics` array is vacuous — NOT the same as a clean
|
|
19417
|
+
* React scan. Consumers gating on the report (CI, verifiers, hooks) should
|
|
19418
|
+
* treat `reactDetected === false` as "wrong scan target", not "all clear".
|
|
19419
|
+
* Absent when nothing was scanned (`projects` is empty), on error reports,
|
|
19420
|
+
* and on reports from older CLI versions.
|
|
19421
|
+
*/
|
|
19422
|
+
reactDetected: optional(Boolean$1),
|
|
19412
19423
|
diff: NullOr(JsonReportDiffInfo),
|
|
19413
19424
|
projects: ArraySchema(JsonReportProjectEntry),
|
|
19414
19425
|
diagnostics: ArraySchema(Diagnostic),
|
|
@@ -19434,6 +19445,8 @@ var JsonReportV2 = class extends Class("JsonReportV2")({
|
|
|
19434
19445
|
ok: Boolean$1,
|
|
19435
19446
|
directory: String$1,
|
|
19436
19447
|
mode: JsonReportMode,
|
|
19448
|
+
/** See `JsonReportV1.reactDetected`. */
|
|
19449
|
+
reactDetected: optional(Boolean$1),
|
|
19437
19450
|
diff: NullOr(JsonReportDiffInfo),
|
|
19438
19451
|
baseline: JsonReportBaseline,
|
|
19439
19452
|
projects: ArraySchema(JsonReportProjectEntry),
|
|
@@ -32986,10 +32999,15 @@ const SOCKET_PACKAGE_PAGE_BASE = "https://socket.dev/npm/package";
|
|
|
32986
32999
|
const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
|
|
32987
33000
|
const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
|
|
32988
33001
|
const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
|
|
33002
|
+
const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
|
|
33003
|
+
const CORE_PACKAGE_VERSION = "0.7.0";
|
|
33004
|
+
const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
|
|
33005
|
+
const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
|
|
32989
33006
|
const SUPPLY_CHAIN_PLUGIN = "socket";
|
|
32990
33007
|
const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
|
|
32991
33008
|
const SUPPLY_CHAIN_CATEGORY = "Security";
|
|
32992
33009
|
const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
|
|
33010
|
+
const SUPPLY_CHAIN_CACHE_TTL_MS = 864e5;
|
|
32993
33011
|
const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
|
|
32994
33012
|
const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
|
|
32995
33013
|
const hasIgnoredPathSegment = (relativePath) => relativePath.split(/[/\\]/).slice(0, -1).some((segment) => IGNORED_DIRECTORIES.has(segment));
|
|
@@ -35646,6 +35664,48 @@ var PerFileLintCacheEnabled = class extends Reference("react-doctor/PerFileLintC
|
|
|
35646
35664
|
if (CACHE_DISABLED_VALUES.has(noFileCache)) return false;
|
|
35647
35665
|
return true;
|
|
35648
35666
|
} }) {};
|
|
35667
|
+
/**
|
|
35668
|
+
* Whether the sidecar lint cache (`runners/oxlint/sidecar-lint-cache.ts`) is
|
|
35669
|
+
* active. Defaults ON — warm rescans replay the cross-file rules' cached
|
|
35670
|
+
* diagnostics for every file whose dependency fingerprint (the probe set the
|
|
35671
|
+
* plugin's collectors recorded) still matches the tree, and re-lint only the
|
|
35672
|
+
* rest. Only reachable when the per-file lint cache itself is on (the
|
|
35673
|
+
* sidecar covers its cache hits), so `REACT_DOCTOR_NO_CACHE` /
|
|
35674
|
+
* `REACT_DOCTOR_NO_FILE_CACHE` implicitly disable it too. Opt-OUT knobs:
|
|
35675
|
+
*
|
|
35676
|
+
* - `REACT_DOCTOR_NO_CACHE` — the global off-switch.
|
|
35677
|
+
* - `REACT_DOCTOR_NO_SIDECAR_CACHE` — granular rollback hatch: keep the
|
|
35678
|
+
* per-file cache but run the always-fresh sidecar over every hit
|
|
35679
|
+
* (the pre-cache behavior).
|
|
35680
|
+
*
|
|
35681
|
+
* Tests override via `Layer.succeed(SidecarLintCacheEnabled, false)`.
|
|
35682
|
+
*/
|
|
35683
|
+
var SidecarLintCacheEnabled = class extends Reference("react-doctor/SidecarLintCacheEnabled", { defaultValue: () => {
|
|
35684
|
+
const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
|
|
35685
|
+
const noSidecarCache = process.env["REACT_DOCTOR_NO_SIDECAR_CACHE"]?.toLowerCase() ?? "";
|
|
35686
|
+
if (CACHE_DISABLED_VALUES.has(noCache)) return false;
|
|
35687
|
+
if (CACHE_DISABLED_VALUES.has(noSidecarCache)) return false;
|
|
35688
|
+
return true;
|
|
35689
|
+
} }) {};
|
|
35690
|
+
/**
|
|
35691
|
+
* Whether the whole-project dead-code result cache
|
|
35692
|
+
* (`dead-code/dead-code-result-cache.ts`) is active. Defaults ON — a rescan
|
|
35693
|
+
* whose inputs (source tree, manifests, configs, analyzer version) are
|
|
35694
|
+
* unchanged replays the stored diagnostics instead of re-running the
|
|
35695
|
+
* analysis worker. Opt-OUT, two knobs (matching the per-file lint cache):
|
|
35696
|
+
*
|
|
35697
|
+
* - `REACT_DOCTOR_NO_CACHE` — the global off-switch.
|
|
35698
|
+
* - `REACT_DOCTOR_NO_DEAD_CODE_CACHE` — granular: bust only this cache.
|
|
35699
|
+
*
|
|
35700
|
+
* Tests override via `Layer.succeed(DeadCodeResultCacheEnabled, false)`.
|
|
35701
|
+
*/
|
|
35702
|
+
var DeadCodeResultCacheEnabled = class extends Reference("react-doctor/DeadCodeResultCacheEnabled", { defaultValue: () => {
|
|
35703
|
+
const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
|
|
35704
|
+
const noDeadCodeCache = process.env["REACT_DOCTOR_NO_DEAD_CODE_CACHE"]?.toLowerCase() ?? "";
|
|
35705
|
+
if (CACHE_DISABLED_VALUES.has(noCache)) return false;
|
|
35706
|
+
if (CACHE_DISABLED_VALUES.has(noDeadCodeCache)) return false;
|
|
35707
|
+
return true;
|
|
35708
|
+
} }) {};
|
|
35649
35709
|
const DIAGNOSTIC_SURFACES = [
|
|
35650
35710
|
"cli",
|
|
35651
35711
|
"prComment",
|
|
@@ -36947,10 +37007,17 @@ function* collectSecurityScanFiles(rootDirectory) {
|
|
|
36947
37007
|
}
|
|
36948
37008
|
}
|
|
36949
37009
|
}
|
|
37010
|
+
/**
|
|
37011
|
+
* Whether project discovery resolved a React-compatible runtime (React or
|
|
37012
|
+
* Preact, which ships the same hooks + JSX model). This is the same
|
|
37013
|
+
* predicate that grants the `react` capability, so `false` means every
|
|
37014
|
+
* React-runtime rule family was gated off for the project.
|
|
37015
|
+
*/
|
|
37016
|
+
const hasReactRuntime = (project) => project.reactVersion !== null || project.preactVersion !== null;
|
|
36950
37017
|
const buildCapabilities = (project) => {
|
|
36951
37018
|
const capabilities = /* @__PURE__ */ new Set();
|
|
36952
37019
|
capabilities.add(project.framework);
|
|
36953
|
-
if (project
|
|
37020
|
+
if (hasReactRuntime(project)) capabilities.add("react");
|
|
36954
37021
|
if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
|
|
36955
37022
|
if (project.expoVersion !== null) capabilities.add("expo");
|
|
36956
37023
|
if (project.nextjsMajorVersion !== null && project.nextjsMajorVersion >= 15) capabilities.add("nextjs:15");
|
|
@@ -37286,6 +37353,146 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
|
|
|
37286
37353
|
return [...seen].filter((pattern) => pattern.length > 0);
|
|
37287
37354
|
};
|
|
37288
37355
|
const collectDeadCodeEntryPatterns = (rootDirectory) => [...new Set(collectKnipPatterns(rootDirectory, "entry"))].filter((pattern) => pattern.length > 0);
|
|
37356
|
+
const atomicWriteJson = (filePath, value) => {
|
|
37357
|
+
try {
|
|
37358
|
+
NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
|
|
37359
|
+
const temporaryPath = `${filePath}.${process.pid}.tmp`;
|
|
37360
|
+
NFS.writeFileSync(temporaryPath, JSON.stringify(value));
|
|
37361
|
+
NFS.renameSync(temporaryPath, filePath);
|
|
37362
|
+
} catch {
|
|
37363
|
+
return;
|
|
37364
|
+
}
|
|
37365
|
+
};
|
|
37366
|
+
const failOpenReadJson = (filePath, fallback) => {
|
|
37367
|
+
try {
|
|
37368
|
+
return JSON.parse(NFS.readFileSync(filePath, "utf8"));
|
|
37369
|
+
} catch {
|
|
37370
|
+
return fallback;
|
|
37371
|
+
}
|
|
37372
|
+
};
|
|
37373
|
+
const hashFileContents = (filePath) => {
|
|
37374
|
+
try {
|
|
37375
|
+
return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
|
|
37376
|
+
} catch {
|
|
37377
|
+
return null;
|
|
37378
|
+
}
|
|
37379
|
+
};
|
|
37380
|
+
const ANALYZED_FILE_EXTENSIONS = new Set(DEFAULT_EXTENSIONS);
|
|
37381
|
+
const CORE_SIDE_MANIFEST_NAMES = ["knip.json", "deno.lock"];
|
|
37382
|
+
const ANALYZED_MANIFEST_NAMES = new Set([...ANALYZED_MANIFEST_FILENAMES, ...CORE_SIDE_MANIFEST_NAMES]);
|
|
37383
|
+
const isTsConfigLikeFile = (fileName) => (fileName.startsWith("tsconfig") || fileName.startsWith("jsconfig")) && fileName.endsWith(".json");
|
|
37384
|
+
const isFingerprintedFile = (fileName) => ANALYZED_FILE_EXTENSIONS.has(Path.extname(fileName).toLowerCase()) || ANALYZED_MANIFEST_NAMES.has(fileName) || isTsConfigLikeFile(fileName);
|
|
37385
|
+
/**
|
|
37386
|
+
* Stat snapshot of every file the analysis reads, keyed by root-relative
|
|
37387
|
+
* `/`-separated path. Taken BEFORE the (long) analysis so a stored result is
|
|
37388
|
+
* verified against the tree it started from.
|
|
37389
|
+
*/
|
|
37390
|
+
const collectAnalyzedFileStats = (rootDirectory) => {
|
|
37391
|
+
const statByRelativePath = /* @__PURE__ */ new Map();
|
|
37392
|
+
for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) {
|
|
37393
|
+
if (!isFingerprintedFile(name)) continue;
|
|
37394
|
+
try {
|
|
37395
|
+
const fileStat = NFS.statSync(absolutePath);
|
|
37396
|
+
const relativePath = Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/");
|
|
37397
|
+
statByRelativePath.set(relativePath, {
|
|
37398
|
+
mtimeMs: fileStat.mtimeMs,
|
|
37399
|
+
size: fileStat.size
|
|
37400
|
+
});
|
|
37401
|
+
} catch {}
|
|
37402
|
+
}
|
|
37403
|
+
return statByRelativePath;
|
|
37404
|
+
};
|
|
37405
|
+
const bundledRequire$2 = createRequire(import.meta.url);
|
|
37406
|
+
const resolveDeslopVersion = () => {
|
|
37407
|
+
try {
|
|
37408
|
+
const packageJson = JSON.parse(NFS.readFileSync(bundledRequire$2.resolve("deslop-js/package.json"), "utf8"));
|
|
37409
|
+
return isRecord(packageJson) && typeof packageJson.version === "string" ? packageJson.version : "unknown";
|
|
37410
|
+
} catch {
|
|
37411
|
+
return "unknown";
|
|
37412
|
+
}
|
|
37413
|
+
};
|
|
37414
|
+
const computeDeadCodeCacheKey = (input) => crypto.createHash("sha1").update(JSON.stringify({
|
|
37415
|
+
schemaVersion: 2,
|
|
37416
|
+
coreVersion: input.coreVersion,
|
|
37417
|
+
deslopVersion: resolveDeslopVersion(),
|
|
37418
|
+
deslopJsModuleSpecifier: input.deslopJsModuleSpecifier,
|
|
37419
|
+
entryPatterns: input.entryPatterns,
|
|
37420
|
+
ignorePatterns: input.ignorePatterns,
|
|
37421
|
+
tsConfigFile: input.tsConfigPath === void 0 ? null : Path.relative(input.rootDirectory, input.tsConfigPath).replace(/\\/g, "/")
|
|
37422
|
+
})).digest("hex");
|
|
37423
|
+
const validateDiagnostic$2 = decodeUnknownSync(Diagnostic);
|
|
37424
|
+
const decodeCachedDiagnostics = (raw) => {
|
|
37425
|
+
try {
|
|
37426
|
+
for (const entry of raw) validateDiagnostic$2(entry);
|
|
37427
|
+
return raw;
|
|
37428
|
+
} catch {
|
|
37429
|
+
return null;
|
|
37430
|
+
}
|
|
37431
|
+
};
|
|
37432
|
+
const isPersistedFileIdentity = (value) => Array.isArray(value) && value.length === 3 && typeof value[0] === "number" && typeof value[1] === "number" && typeof value[2] === "string";
|
|
37433
|
+
const lookupDeadCodeResultCache = (input) => {
|
|
37434
|
+
const cacheFilePath = Path.join(input.cacheDirectory, DEAD_CODE_CACHE_FILENAME);
|
|
37435
|
+
const persisted = failOpenReadJson(cacheFilePath, null);
|
|
37436
|
+
if (persisted === null || !isRecord(persisted) || persisted.version !== 2 || persisted.key !== input.cacheKey || !isRecord(persisted.files) || !Array.isArray(persisted.diagnostics)) return null;
|
|
37437
|
+
const storedFileEntries = Object.entries(persisted.files);
|
|
37438
|
+
if (storedFileEntries.length !== input.currentFileStats.size) return null;
|
|
37439
|
+
const repairedFiles = {};
|
|
37440
|
+
let repairedCount = 0;
|
|
37441
|
+
for (const [relativePath, storedIdentity] of storedFileEntries) {
|
|
37442
|
+
if (!isPersistedFileIdentity(storedIdentity)) return null;
|
|
37443
|
+
const currentStat = input.currentFileStats.get(relativePath);
|
|
37444
|
+
if (currentStat === void 0) return null;
|
|
37445
|
+
const [storedMtimeMs, storedSize, storedContentHash] = storedIdentity;
|
|
37446
|
+
if (currentStat.mtimeMs === storedMtimeMs && currentStat.size === storedSize) {
|
|
37447
|
+
repairedFiles[relativePath] = storedIdentity;
|
|
37448
|
+
continue;
|
|
37449
|
+
}
|
|
37450
|
+
if (currentStat.size !== storedSize) return null;
|
|
37451
|
+
const currentContentHash = hashFileContents(Path.join(input.rootDirectory, relativePath));
|
|
37452
|
+
if (currentContentHash === null || currentContentHash !== storedContentHash) return null;
|
|
37453
|
+
repairedFiles[relativePath] = [
|
|
37454
|
+
currentStat.mtimeMs,
|
|
37455
|
+
currentStat.size,
|
|
37456
|
+
storedContentHash
|
|
37457
|
+
];
|
|
37458
|
+
repairedCount += 1;
|
|
37459
|
+
}
|
|
37460
|
+
const diagnostics = decodeCachedDiagnostics(persisted.diagnostics);
|
|
37461
|
+
if (diagnostics === null) return null;
|
|
37462
|
+
if (repairedCount > 0) atomicWriteJson(cacheFilePath, {
|
|
37463
|
+
version: 2,
|
|
37464
|
+
key: input.cacheKey,
|
|
37465
|
+
files: repairedFiles,
|
|
37466
|
+
diagnostics: persisted.diagnostics
|
|
37467
|
+
});
|
|
37468
|
+
return diagnostics;
|
|
37469
|
+
};
|
|
37470
|
+
const storeDeadCodeResultCache = (input) => {
|
|
37471
|
+
const persistedFiles = {};
|
|
37472
|
+
for (const [relativePath, snapshotStat] of input.snapshotFileStats) {
|
|
37473
|
+
const absolutePath = Path.join(input.rootDirectory, relativePath);
|
|
37474
|
+
const contentHash = hashFileContents(absolutePath);
|
|
37475
|
+
if (contentHash === null) return;
|
|
37476
|
+
let currentStat;
|
|
37477
|
+
try {
|
|
37478
|
+
currentStat = NFS.statSync(absolutePath);
|
|
37479
|
+
} catch {
|
|
37480
|
+
return;
|
|
37481
|
+
}
|
|
37482
|
+
if (currentStat.mtimeMs !== snapshotStat.mtimeMs || currentStat.size !== snapshotStat.size) return;
|
|
37483
|
+
persistedFiles[relativePath] = [
|
|
37484
|
+
snapshotStat.mtimeMs,
|
|
37485
|
+
snapshotStat.size,
|
|
37486
|
+
contentHash
|
|
37487
|
+
];
|
|
37488
|
+
}
|
|
37489
|
+
atomicWriteJson(Path.join(input.cacheDirectory, DEAD_CODE_CACHE_FILENAME), {
|
|
37490
|
+
version: 2,
|
|
37491
|
+
key: input.cacheKey,
|
|
37492
|
+
files: persistedFiles,
|
|
37493
|
+
diagnostics: input.diagnostics
|
|
37494
|
+
});
|
|
37495
|
+
};
|
|
37289
37496
|
const readSystemFacts = () => ({
|
|
37290
37497
|
availableCores: os.availableParallelism(),
|
|
37291
37498
|
totalMemoryBytes: os.totalmem(),
|
|
@@ -37354,6 +37561,21 @@ const withDeadCodeWorkerSlot = async (task, abortSignal) => {
|
|
|
37354
37561
|
releaseSlot();
|
|
37355
37562
|
}
|
|
37356
37563
|
};
|
|
37564
|
+
const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
|
|
37565
|
+
const resolveReactDoctorCacheDir = (projectDirectory) => {
|
|
37566
|
+
const cacheDirOverride = process.env["REACT_DOCTOR_CACHE_DIR"]?.trim();
|
|
37567
|
+
if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
|
|
37568
|
+
const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
|
|
37569
|
+
if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
|
|
37570
|
+
return Path.join(os.tmpdir(), `react-doctor-cache-${resolveUserCacheScope()}`, projectCacheSubdir(projectDirectory));
|
|
37571
|
+
};
|
|
37572
|
+
const resolveUserCacheScope = () => {
|
|
37573
|
+
try {
|
|
37574
|
+
return String(process.getuid?.() ?? os.userInfo().username);
|
|
37575
|
+
} catch {
|
|
37576
|
+
return "shared";
|
|
37577
|
+
}
|
|
37578
|
+
};
|
|
37357
37579
|
/**
|
|
37358
37580
|
* Resolves a path to its canonical, symlink-free form, falling back to
|
|
37359
37581
|
* the input when it cannot be realpath'd (broken symlink, permission
|
|
@@ -37407,6 +37629,14 @@ process.stdin.on("end", () => {
|
|
|
37407
37629
|
circularDependencies: result.circularDependencies.map((cycle) => ({
|
|
37408
37630
|
files: cycle.files,
|
|
37409
37631
|
})),
|
|
37632
|
+
...(result.incrementalCacheStats
|
|
37633
|
+
? {
|
|
37634
|
+
summaryCacheStats: {
|
|
37635
|
+
hits: result.incrementalCacheStats.summaryHits,
|
|
37636
|
+
misses: result.incrementalCacheStats.summaryMisses,
|
|
37637
|
+
},
|
|
37638
|
+
}
|
|
37639
|
+
: {}),
|
|
37410
37640
|
});
|
|
37411
37641
|
|
|
37412
37642
|
const serializeError = (error) =>
|
|
@@ -37430,6 +37660,9 @@ process.stdin.on("end", () => {
|
|
|
37430
37660
|
...(workerInput.ignorePatterns.length > 0
|
|
37431
37661
|
? { ignorePatterns: workerInput.ignorePatterns }
|
|
37432
37662
|
: {}),
|
|
37663
|
+
...(workerInput.incrementalCachePath
|
|
37664
|
+
? { incrementalCachePath: workerInput.incrementalCachePath }
|
|
37665
|
+
: {}),
|
|
37433
37666
|
// We consume only deslop's GRAPH-based findings (unusedFiles, unusedExports,
|
|
37434
37667
|
// unusedDependencies, circularDependencies). Everything else deslop can compute
|
|
37435
37668
|
// is pure wasted work for us, and it's the bulk of the runtime:
|
|
@@ -37440,12 +37673,18 @@ process.stdin.on("end", () => {
|
|
|
37440
37673
|
// are the single most expensive pass — duplicate-block detection alone was
|
|
37441
37674
|
// ~83s of a ~130s Sentry scan — so skipping them is an ~8.5x dead-code
|
|
37442
37675
|
// speedup on a large repo.
|
|
37443
|
-
//
|
|
37676
|
+
// - reportRedundancy: the DRY-pattern detectors (duplicate types/constants,
|
|
37677
|
+
// simplifiable functions, identity wrappers, …) — ~120 ms of discarded
|
|
37678
|
+
// output per scan, and skipping them lets the incremental cache drop the
|
|
37679
|
+
// DRY-pattern summary fields (the largest slice of the cache file).
|
|
37680
|
+
// All are provably safe: the consumed graph findings are computed by their own
|
|
37444
37681
|
// detectors, independent of these passes (confirmed byte-identical on
|
|
37445
|
-
// excalidraw + mui-material + sentry
|
|
37446
|
-
//
|
|
37682
|
+
// excalidraw + mui-material + sentry; re-verified on sentry after the
|
|
37683
|
+
// reportRedundancy flip). tsConfigPath stays — the module resolver needs it
|
|
37684
|
+
// for path-alias resolution in the import graph.
|
|
37447
37685
|
semantic: { enabled: false },
|
|
37448
37686
|
reportCodeQuality: false,
|
|
37687
|
+
reportRedundancy: false,
|
|
37449
37688
|
};
|
|
37450
37689
|
const result = await analyze(defineConfig(config));
|
|
37451
37690
|
emit({ ok: true, result: normalizeResult(result) });
|
|
@@ -37529,13 +37768,23 @@ const parseCircularDependencies = (value) => {
|
|
|
37529
37768
|
}
|
|
37530
37769
|
return circularDependencies;
|
|
37531
37770
|
};
|
|
37771
|
+
const parseSummaryCacheStats = (value) => {
|
|
37772
|
+
if (!isRecord(value)) return void 0;
|
|
37773
|
+
if (typeof value.hits !== "number" || typeof value.misses !== "number") return void 0;
|
|
37774
|
+
return {
|
|
37775
|
+
hits: value.hits,
|
|
37776
|
+
misses: value.misses
|
|
37777
|
+
};
|
|
37778
|
+
};
|
|
37532
37779
|
const parseDeadCodeWorkerResult = (value) => {
|
|
37533
37780
|
if (!isRecord(value)) throw new Error("Dead-code worker returned an invalid result.");
|
|
37781
|
+
const summaryCacheStats = parseSummaryCacheStats(value.summaryCacheStats);
|
|
37534
37782
|
return {
|
|
37535
37783
|
unusedFiles: parseUnusedFiles(value.unusedFiles),
|
|
37536
37784
|
unusedExports: parseUnusedExports(value.unusedExports),
|
|
37537
37785
|
unusedDependencies: parseUnusedDependencies(value.unusedDependencies),
|
|
37538
|
-
circularDependencies: parseCircularDependencies(value.circularDependencies)
|
|
37786
|
+
circularDependencies: parseCircularDependencies(value.circularDependencies),
|
|
37787
|
+
...summaryCacheStats ? { summaryCacheStats } : {}
|
|
37539
37788
|
};
|
|
37540
37789
|
};
|
|
37541
37790
|
const parseDeadCodeWorkerError = (value) => {
|
|
@@ -37649,17 +37898,41 @@ const checkDeadCode = async (options) => {
|
|
|
37649
37898
|
if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
|
|
37650
37899
|
const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
|
|
37651
37900
|
const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
|
|
37901
|
+
const tsConfigPath = resolveTsConfigPath(rootDirectory);
|
|
37902
|
+
const deslopJsModuleSpecifier = options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js");
|
|
37903
|
+
const fileStatsSnapshot = options.cacheEnabled ? collectAnalyzedFileStats(rootDirectory) : null;
|
|
37904
|
+
const cacheKey = fileStatsSnapshot === null ? null : computeDeadCodeCacheKey({
|
|
37905
|
+
rootDirectory,
|
|
37906
|
+
entryPatterns,
|
|
37907
|
+
ignorePatterns,
|
|
37908
|
+
tsConfigPath,
|
|
37909
|
+
deslopJsModuleSpecifier,
|
|
37910
|
+
coreVersion: CORE_PACKAGE_VERSION
|
|
37911
|
+
});
|
|
37912
|
+
if (cacheKey !== null && fileStatsSnapshot !== null) {
|
|
37913
|
+
const cachedDiagnostics = lookupDeadCodeResultCache({
|
|
37914
|
+
cacheDirectory: resolveReactDoctorCacheDir(rootDirectory),
|
|
37915
|
+
cacheKey,
|
|
37916
|
+
rootDirectory,
|
|
37917
|
+
currentFileStats: fileStatsSnapshot
|
|
37918
|
+
});
|
|
37919
|
+
options.onCacheOutcome?.(cachedDiagnostics !== null);
|
|
37920
|
+
if (cachedDiagnostics !== null) return [...cachedDiagnostics];
|
|
37921
|
+
}
|
|
37922
|
+
const incrementalCachePath = options.cacheEnabled === true ? Path.join(resolveReactDoctorCacheDir(rootDirectory), DEAD_CODE_SUMMARY_CACHE_FILENAME) : void 0;
|
|
37652
37923
|
const spawnAndRun = () => {
|
|
37653
37924
|
return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
|
|
37654
37925
|
rootDirectory,
|
|
37655
37926
|
entryPatterns,
|
|
37656
|
-
tsConfigPath
|
|
37927
|
+
tsConfigPath,
|
|
37657
37928
|
ignorePatterns,
|
|
37658
|
-
deslopJsModuleSpecifier
|
|
37659
|
-
parseConcurrency: options.parseConcurrency
|
|
37929
|
+
deslopJsModuleSpecifier,
|
|
37930
|
+
parseConcurrency: options.parseConcurrency,
|
|
37931
|
+
incrementalCachePath
|
|
37660
37932
|
}), options.workerTimeoutMs ?? 12e4, options.abortSignal);
|
|
37661
37933
|
};
|
|
37662
37934
|
const result = parseDeadCodeWorkerResult(options.createWorker === void 0 ? await withDeadCodeWorkerSlot(spawnAndRun, options.abortSignal) : await spawnAndRun());
|
|
37935
|
+
if (result.summaryCacheStats !== void 0) options.onSummaryCacheStats?.(result.summaryCacheStats);
|
|
37663
37936
|
const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
|
|
37664
37937
|
const diagnostics = [];
|
|
37665
37938
|
for (const unusedFile of result.unusedFiles) diagnostics.push({
|
|
@@ -37716,6 +37989,13 @@ const checkDeadCode = async (options) => {
|
|
|
37716
37989
|
category: DEAD_CODE_CATEGORY
|
|
37717
37990
|
});
|
|
37718
37991
|
}
|
|
37992
|
+
if (cacheKey !== null && fileStatsSnapshot !== null) storeDeadCodeResultCache({
|
|
37993
|
+
cacheDirectory: resolveReactDoctorCacheDir(rootDirectory),
|
|
37994
|
+
cacheKey,
|
|
37995
|
+
rootDirectory,
|
|
37996
|
+
snapshotFileStats: fileStatsSnapshot,
|
|
37997
|
+
diagnostics
|
|
37998
|
+
});
|
|
37719
37999
|
return diagnostics;
|
|
37720
38000
|
};
|
|
37721
38001
|
const DEAD_CODE_RULE_KEY_PREFIX = `${DEAD_CODE_PLUGIN}/`;
|
|
@@ -37818,12 +38098,16 @@ var Config = class Config extends Service()("react-doctor/Config") {
|
|
|
37818
38098
|
*/
|
|
37819
38099
|
var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
|
|
37820
38100
|
static layerNode = succeed$3(DeadCode, DeadCode.of({ run: (input) => unwrap(fn("DeadCode.run")(function* () {
|
|
38101
|
+
const cacheEnabled = yield* DeadCodeResultCacheEnabled;
|
|
37821
38102
|
return yield* tryPromise({
|
|
37822
38103
|
try: (signal) => checkDeadCode({
|
|
37823
38104
|
rootDirectory: input.rootDirectory,
|
|
37824
38105
|
parseConcurrency: input.parseConcurrency,
|
|
37825
38106
|
workerTimeoutMs: input.workerTimeoutMs,
|
|
37826
|
-
abortSignal: signal
|
|
38107
|
+
abortSignal: signal,
|
|
38108
|
+
cacheEnabled,
|
|
38109
|
+
onCacheOutcome: input.onCacheOutcome,
|
|
38110
|
+
onSummaryCacheStats: input.onSummaryCacheStats
|
|
37827
38111
|
}),
|
|
37828
38112
|
catch: (cause) => new ReactDoctorError({ reason: new DeadCodeAnalysisFailed({ cause }) })
|
|
37829
38113
|
}).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)));
|
|
@@ -38693,7 +38977,7 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
|
|
|
38693
38977
|
}
|
|
38694
38978
|
return enabled;
|
|
38695
38979
|
};
|
|
38696
|
-
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection }) => {
|
|
38980
|
+
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection, sidecarRuleIdFilter }) => {
|
|
38697
38981
|
const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
|
|
38698
38982
|
const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
|
|
38699
38983
|
const jsPlugins = [];
|
|
@@ -38705,6 +38989,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
38705
38989
|
if (!rule) continue;
|
|
38706
38990
|
if (ruleSelection === "cacheable" && CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
|
|
38707
38991
|
if (ruleSelection === "sidecar" && !CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
|
|
38992
|
+
if (ruleSelection === "sidecar" && sidecarRuleIdFilter !== void 0 && !sidecarRuleIdFilter.has(registryEntry.id)) continue;
|
|
38708
38993
|
if (rule.scan !== void 0) continue;
|
|
38709
38994
|
if (customRulesOnly && registryEntry.originallyExternal) continue;
|
|
38710
38995
|
if (rule.framework !== "global" && !rule.requires) continue;
|
|
@@ -38749,33 +39034,145 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
38749
39034
|
}
|
|
38750
39035
|
};
|
|
38751
39036
|
};
|
|
38752
|
-
const
|
|
39037
|
+
const validateDiagnostic$1 = decodeUnknownSync(Diagnostic);
|
|
39038
|
+
const decodeFileDiagnostics = (raw) => {
|
|
39039
|
+
if (!Array.isArray(raw)) return null;
|
|
38753
39040
|
try {
|
|
38754
|
-
|
|
38755
|
-
|
|
38756
|
-
NFS.writeFileSync(temporaryPath, JSON.stringify(value));
|
|
38757
|
-
NFS.renameSync(temporaryPath, filePath);
|
|
39041
|
+
for (const entry of raw) validateDiagnostic$1(entry);
|
|
39042
|
+
return raw;
|
|
38758
39043
|
} catch {
|
|
38759
|
-
return;
|
|
39044
|
+
return null;
|
|
38760
39045
|
}
|
|
38761
39046
|
};
|
|
38762
|
-
const
|
|
38763
|
-
|
|
38764
|
-
|
|
38765
|
-
|
|
38766
|
-
|
|
39047
|
+
const emptyCache$1 = () => ({
|
|
39048
|
+
version: 1,
|
|
39049
|
+
rulesets: {}
|
|
39050
|
+
});
|
|
39051
|
+
const loadRulesetEntries$1 = (cacheFilePath, rulesetHash) => {
|
|
39052
|
+
const entries = /* @__PURE__ */ new Map();
|
|
39053
|
+
const persisted = failOpenReadJson(cacheFilePath, emptyCache$1());
|
|
39054
|
+
if (persisted.version !== 1 || !isRecord(persisted.rulesets)) return entries;
|
|
39055
|
+
const bucket = persisted.rulesets[rulesetHash];
|
|
39056
|
+
if (!isRecord(bucket) || !isRecord(bucket.files)) return entries;
|
|
39057
|
+
for (const [fileKey, rawDiagnostics] of Object.entries(bucket.files)) {
|
|
39058
|
+
const decoded = decodeFileDiagnostics(rawDiagnostics);
|
|
39059
|
+
if (decoded !== null) entries.set(fileKey, decoded);
|
|
38767
39060
|
}
|
|
39061
|
+
return entries;
|
|
39062
|
+
};
|
|
39063
|
+
const createFileLintCache = (cacheDirectory, rulesetHash) => {
|
|
39064
|
+
const cacheFilePath = Path.join(cacheDirectory, FILE_LINT_CACHE_FILENAME);
|
|
39065
|
+
const entries = loadRulesetEntries$1(cacheFilePath, rulesetHash);
|
|
39066
|
+
return {
|
|
39067
|
+
lookup: (fileKey) => entries.get(fileKey) ?? null,
|
|
39068
|
+
store: (fileKey, diagnostics) => {
|
|
39069
|
+
entries.delete(fileKey);
|
|
39070
|
+
entries.set(fileKey, diagnostics);
|
|
39071
|
+
},
|
|
39072
|
+
persist: () => {
|
|
39073
|
+
const onDisk = failOpenReadJson(cacheFilePath, emptyCache$1());
|
|
39074
|
+
const rulesets = onDisk.version === 1 && isRecord(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
|
|
39075
|
+
const existingBucket = rulesets[rulesetHash];
|
|
39076
|
+
const existingFiles = isRecord(existingBucket) && isRecord(existingBucket.files) ? existingBucket.files : {};
|
|
39077
|
+
const ourFiles = {};
|
|
39078
|
+
for (const [fileKey, diagnostics] of entries) ourFiles[fileKey] = diagnostics;
|
|
39079
|
+
const cappedEntries = Object.entries({
|
|
39080
|
+
...existingFiles,
|
|
39081
|
+
...ourFiles
|
|
39082
|
+
}).slice(-FILE_LINT_CACHE_MAX_FILE_COUNT);
|
|
39083
|
+
rulesets[rulesetHash] = {
|
|
39084
|
+
updatedAtMs: Date.now(),
|
|
39085
|
+
files: Object.fromEntries(cappedEntries)
|
|
39086
|
+
};
|
|
39087
|
+
atomicWriteJson(cacheFilePath, {
|
|
39088
|
+
version: 1,
|
|
39089
|
+
rulesets: Object.fromEntries(Object.entries(rulesets).filter(([, bucket]) => isRecord(bucket) && typeof bucket.updatedAtMs === "number").sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8))
|
|
39090
|
+
});
|
|
39091
|
+
}
|
|
39092
|
+
};
|
|
39093
|
+
};
|
|
39094
|
+
const ABSENT_CONTENT_ANSWER = "absent";
|
|
39095
|
+
/**
|
|
39096
|
+
* Answers dependency probes for the sidecar lint cache — used both when
|
|
39097
|
+
* STORING an entry (turn the collector's probe paths into answers) and when
|
|
39098
|
+
* VERIFYING one (re-answer each stored probe against the current tree).
|
|
39099
|
+
* Store and verify must agree exactly, so both go through this one resolver.
|
|
39100
|
+
*
|
|
39101
|
+
* - `content` — SHA-1 of the file's bytes, `"absent"` when unreadable
|
|
39102
|
+
* (missing, a directory, permission failure). Source files reuse the
|
|
39103
|
+
* hashes the cache-partition loop already computed
|
|
39104
|
+
* (`contentHashByRelativePath`); everything else (package.json,
|
|
39105
|
+
* tsconfig, ignored files) is hashed on demand.
|
|
39106
|
+
* - `exists` — the existence class a module-resolution candidate probe
|
|
39107
|
+
* sees: `"file"`, `"dir"`, or `"none"` (missing or an exotic entry —
|
|
39108
|
+
* `statSync` follows symlinks, matching the resolvers).
|
|
39109
|
+
*
|
|
39110
|
+
* Answers are memoized per scan: the filesystem is frozen for the run, and
|
|
39111
|
+
* hot dependency paths (a barrel imported by hundreds of files, the shared
|
|
39112
|
+
* tsconfig chain) repeat across many files' probe sets.
|
|
39113
|
+
*/
|
|
39114
|
+
const createSidecarProbeAnswerResolver = (input) => {
|
|
39115
|
+
const contentAnswerByPath = /* @__PURE__ */ new Map();
|
|
39116
|
+
const existsAnswerByPath = /* @__PURE__ */ new Map();
|
|
39117
|
+
const toRelativePath = (absolutePath) => Path.relative(input.rootDirectory, absolutePath).replaceAll("\\", "/");
|
|
39118
|
+
const contentAnswer = (relativePath) => {
|
|
39119
|
+
const memoized = contentAnswerByPath.get(relativePath);
|
|
39120
|
+
if (memoized !== void 0) return memoized;
|
|
39121
|
+
const answer = input.contentHashByRelativePath.get(relativePath) ?? hashFileContents(Path.resolve(input.rootDirectory, relativePath)) ?? ABSENT_CONTENT_ANSWER;
|
|
39122
|
+
contentAnswerByPath.set(relativePath, answer);
|
|
39123
|
+
return answer;
|
|
39124
|
+
};
|
|
39125
|
+
const existsAnswer = (relativePath) => {
|
|
39126
|
+
const memoized = existsAnswerByPath.get(relativePath);
|
|
39127
|
+
if (memoized !== void 0) return memoized;
|
|
39128
|
+
let answer;
|
|
39129
|
+
try {
|
|
39130
|
+
const stat = NFS.statSync(Path.resolve(input.rootDirectory, relativePath));
|
|
39131
|
+
answer = stat.isFile() ? "file" : stat.isDirectory() ? "dir" : "none";
|
|
39132
|
+
} catch {
|
|
39133
|
+
answer = "none";
|
|
39134
|
+
}
|
|
39135
|
+
existsAnswerByPath.set(relativePath, answer);
|
|
39136
|
+
return answer;
|
|
39137
|
+
};
|
|
39138
|
+
return {
|
|
39139
|
+
answerFor: (kind, relativePath) => kind === "content" ? contentAnswer(relativePath) : existsAnswer(relativePath),
|
|
39140
|
+
toRelativePath
|
|
39141
|
+
};
|
|
38768
39142
|
};
|
|
38769
39143
|
const validateDiagnostic = decodeUnknownSync(Diagnostic);
|
|
38770
|
-
const
|
|
38771
|
-
|
|
39144
|
+
const isProbeKind = (value) => value === "content" || value === "exists";
|
|
39145
|
+
const decodeSidecarFileEntry = (raw) => {
|
|
39146
|
+
if (!isRecord(raw) || !Array.isArray(raw.probes) || !Array.isArray(raw.diagnostics)) return null;
|
|
39147
|
+
const probes = [];
|
|
39148
|
+
for (const rawProbe of raw.probes) {
|
|
39149
|
+
if (!Array.isArray(rawProbe) || rawProbe.length !== 3) return null;
|
|
39150
|
+
const [kind, probePath, answer] = rawProbe;
|
|
39151
|
+
if (!isProbeKind(kind) || typeof probePath !== "string" || typeof answer !== "string") return null;
|
|
39152
|
+
probes.push({
|
|
39153
|
+
kind,
|
|
39154
|
+
path: probePath,
|
|
39155
|
+
answer
|
|
39156
|
+
});
|
|
39157
|
+
}
|
|
38772
39158
|
try {
|
|
38773
|
-
for (const entry of raw) validateDiagnostic(entry);
|
|
38774
|
-
return raw;
|
|
39159
|
+
for (const entry of raw.diagnostics) validateDiagnostic(entry);
|
|
38775
39160
|
} catch {
|
|
38776
39161
|
return null;
|
|
38777
39162
|
}
|
|
39163
|
+
return {
|
|
39164
|
+
probes,
|
|
39165
|
+
diagnostics: raw.diagnostics
|
|
39166
|
+
};
|
|
38778
39167
|
};
|
|
39168
|
+
const encodeSidecarFileEntry = (entry) => ({
|
|
39169
|
+
probes: entry.probes.map((probe) => [
|
|
39170
|
+
probe.kind,
|
|
39171
|
+
probe.path,
|
|
39172
|
+
probe.answer
|
|
39173
|
+
]),
|
|
39174
|
+
diagnostics: entry.diagnostics
|
|
39175
|
+
});
|
|
38779
39176
|
const emptyCache = () => ({
|
|
38780
39177
|
version: 1,
|
|
38781
39178
|
rulesets: {}
|
|
@@ -38786,20 +39183,20 @@ const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
|
|
|
38786
39183
|
if (persisted.version !== 1 || !isRecord(persisted.rulesets)) return entries;
|
|
38787
39184
|
const bucket = persisted.rulesets[rulesetHash];
|
|
38788
39185
|
if (!isRecord(bucket) || !isRecord(bucket.files)) return entries;
|
|
38789
|
-
for (const [fileKey,
|
|
38790
|
-
const decoded =
|
|
39186
|
+
for (const [fileKey, rawEntry] of Object.entries(bucket.files)) {
|
|
39187
|
+
const decoded = decodeSidecarFileEntry(rawEntry);
|
|
38791
39188
|
if (decoded !== null) entries.set(fileKey, decoded);
|
|
38792
39189
|
}
|
|
38793
39190
|
return entries;
|
|
38794
39191
|
};
|
|
38795
|
-
const
|
|
38796
|
-
const cacheFilePath = Path.join(cacheDirectory,
|
|
39192
|
+
const createSidecarLintCache = (cacheDirectory, rulesetHash) => {
|
|
39193
|
+
const cacheFilePath = Path.join(cacheDirectory, SIDECAR_LINT_CACHE_FILENAME);
|
|
38797
39194
|
const entries = loadRulesetEntries(cacheFilePath, rulesetHash);
|
|
38798
39195
|
return {
|
|
38799
39196
|
lookup: (fileKey) => entries.get(fileKey) ?? null,
|
|
38800
|
-
store: (fileKey,
|
|
39197
|
+
store: (fileKey, entry) => {
|
|
38801
39198
|
entries.delete(fileKey);
|
|
38802
|
-
entries.set(fileKey,
|
|
39199
|
+
entries.set(fileKey, entry);
|
|
38803
39200
|
},
|
|
38804
39201
|
persist: () => {
|
|
38805
39202
|
const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
|
|
@@ -38807,7 +39204,7 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
|
|
|
38807
39204
|
const existingBucket = rulesets[rulesetHash];
|
|
38808
39205
|
const existingFiles = isRecord(existingBucket) && isRecord(existingBucket.files) ? existingBucket.files : {};
|
|
38809
39206
|
const ourFiles = {};
|
|
38810
|
-
for (const [fileKey,
|
|
39207
|
+
for (const [fileKey, entry] of entries) ourFiles[fileKey] = encodeSidecarFileEntry(entry);
|
|
38811
39208
|
const cappedEntries = Object.entries({
|
|
38812
39209
|
...existingFiles,
|
|
38813
39210
|
...ourFiles
|
|
@@ -39822,13 +40219,6 @@ const validateRuleRegistration = () => {
|
|
|
39822
40219
|
].filter((entry) => entry !== null).join("; ");
|
|
39823
40220
|
console.warn(`[react-doctor] rule-registration drift: ${detail}`);
|
|
39824
40221
|
};
|
|
39825
|
-
const hashFileContents = (filePath) => {
|
|
39826
|
-
try {
|
|
39827
|
-
return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
|
|
39828
|
-
} catch {
|
|
39829
|
-
return null;
|
|
39830
|
-
}
|
|
39831
|
-
};
|
|
39832
40222
|
/**
|
|
39833
40223
|
* Balanced LPT (longest-processing-time-first) lint batch planner. Where
|
|
39834
40224
|
* `batchIncludePaths` greedily fills 100-file batches and leaves a small
|
|
@@ -39893,21 +40283,6 @@ const planLintBatches = ({ baseArgs, files, sizeByFile }) => {
|
|
|
39893
40283
|
}
|
|
39894
40284
|
return batches.filter((batch) => batch.fileIndexes.length > 0).map((batch) => batch.fileIndexes.sort((leftIndex, rightIndex) => leftIndex - rightIndex).map((fileIndex) => files[fileIndex]));
|
|
39895
40285
|
};
|
|
39896
|
-
const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
|
|
39897
|
-
const resolveReactDoctorCacheDir = (projectDirectory) => {
|
|
39898
|
-
const cacheDirOverride = process.env["REACT_DOCTOR_CACHE_DIR"]?.trim();
|
|
39899
|
-
if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
|
|
39900
|
-
const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
|
|
39901
|
-
if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
|
|
39902
|
-
return Path.join(os.tmpdir(), `react-doctor-cache-${resolveUserCacheScope()}`, projectCacheSubdir(projectDirectory));
|
|
39903
|
-
};
|
|
39904
|
-
const resolveUserCacheScope = () => {
|
|
39905
|
-
try {
|
|
39906
|
-
return String(process.getuid?.() ?? os.userInfo().username);
|
|
39907
|
-
} catch {
|
|
39908
|
-
return "shared";
|
|
39909
|
-
}
|
|
39910
|
-
};
|
|
39911
40286
|
/**
|
|
39912
40287
|
* Atomically (re)writes the generated oxlintrc.json. Used twice in
|
|
39913
40288
|
* the runner: once for the primary scan, once for the
|
|
@@ -39925,6 +40300,106 @@ const writeOxlintConfig = (configPath, configToWrite) => {
|
|
|
39925
40300
|
NFS.closeSync(fileHandle);
|
|
39926
40301
|
}
|
|
39927
40302
|
};
|
|
40303
|
+
/**
|
|
40304
|
+
* Attributes diagnostics back to the file that produced them by the
|
|
40305
|
+
* normalized path oxlint echoes. Returns `null` when ANY diagnostic can't be
|
|
40306
|
+
* attributed — the path forms don't align, so the caller must skip its cache
|
|
40307
|
+
* store rather than risk caching a wrong empty result for a file that
|
|
40308
|
+
* actually had diagnostics.
|
|
40309
|
+
*/
|
|
40310
|
+
const attributeDiagnosticsToFiles = (diagnostics, files) => {
|
|
40311
|
+
const fileByNormalizedPath = /* @__PURE__ */ new Map();
|
|
40312
|
+
for (const file of files) fileByNormalizedPath.set(file.replaceAll("\\", "/"), file);
|
|
40313
|
+
const diagnosticsByFile = /* @__PURE__ */ new Map();
|
|
40314
|
+
for (const diagnostic of diagnostics) {
|
|
40315
|
+
const file = fileByNormalizedPath.get(diagnostic.filePath);
|
|
40316
|
+
if (file === void 0) return null;
|
|
40317
|
+
const fileDiagnostics = diagnosticsByFile.get(file) ?? [];
|
|
40318
|
+
fileDiagnostics.push(diagnostic);
|
|
40319
|
+
diagnosticsByFile.set(file, fileDiagnostics);
|
|
40320
|
+
}
|
|
40321
|
+
return diagnosticsByFile;
|
|
40322
|
+
};
|
|
40323
|
+
/**
|
|
40324
|
+
* Collects each file's cross-file dependency probes (via the plugin's
|
|
40325
|
+
* collectors) with their current answers. Fail-open per file: an unreadable /
|
|
40326
|
+
* unparseable / collector-failing file maps to `null` (no entry stored — it
|
|
40327
|
+
* re-lints next scan). The filesystem is frozen for the scan, so this can run
|
|
40328
|
+
* CONCURRENTLY with an oxlint child-process await — the caller starts it
|
|
40329
|
+
* unawaited before a lint pass and awaits it at store time, hiding the
|
|
40330
|
+
* in-process parse + resolution cost under the subprocess wait. The loop
|
|
40331
|
+
* yields on the cooperative time budget so the child's stdout handling (and
|
|
40332
|
+
* sibling fibers) keep flowing; each per-file collection is synchronous, so
|
|
40333
|
+
* the module-level probe recorder never sees interleaved use. Never rejects.
|
|
40334
|
+
*/
|
|
40335
|
+
const collectSidecarProbesForFiles = async (input) => {
|
|
40336
|
+
const buildProbes = (file) => {
|
|
40337
|
+
const absoluteFilePath = Path.resolve(input.rootDirectory, file);
|
|
40338
|
+
let trace;
|
|
40339
|
+
try {
|
|
40340
|
+
trace = collectCrossFileDependencyProbes({
|
|
40341
|
+
absoluteFilePath,
|
|
40342
|
+
sourceText: NFS.readFileSync(absoluteFilePath, "utf8"),
|
|
40343
|
+
ruleIds: input.boundedSidecarRuleIds
|
|
40344
|
+
});
|
|
40345
|
+
} catch {
|
|
40346
|
+
return null;
|
|
40347
|
+
}
|
|
40348
|
+
if (trace === null) return null;
|
|
40349
|
+
const probes = [];
|
|
40350
|
+
for (const contentPath of trace.contentPaths) {
|
|
40351
|
+
const relativePath = input.probeAnswers.toRelativePath(contentPath);
|
|
40352
|
+
probes.push({
|
|
40353
|
+
kind: "content",
|
|
40354
|
+
path: relativePath,
|
|
40355
|
+
answer: input.probeAnswers.answerFor("content", relativePath)
|
|
40356
|
+
});
|
|
40357
|
+
}
|
|
40358
|
+
for (const existencePath of trace.existencePaths) {
|
|
40359
|
+
const relativePath = input.probeAnswers.toRelativePath(existencePath);
|
|
40360
|
+
probes.push({
|
|
40361
|
+
kind: "exists",
|
|
40362
|
+
path: relativePath,
|
|
40363
|
+
answer: input.probeAnswers.answerFor("exists", relativePath)
|
|
40364
|
+
});
|
|
40365
|
+
}
|
|
40366
|
+
return probes;
|
|
40367
|
+
};
|
|
40368
|
+
const probesByFile = /* @__PURE__ */ new Map();
|
|
40369
|
+
let collectSliceStartedAt = performance.now();
|
|
40370
|
+
for (const file of input.files) {
|
|
40371
|
+
probesByFile.set(file, buildProbes(file));
|
|
40372
|
+
if (performance.now() - collectSliceStartedAt >= 12) {
|
|
40373
|
+
await yieldToEventLoop();
|
|
40374
|
+
collectSliceStartedAt = performance.now();
|
|
40375
|
+
}
|
|
40376
|
+
}
|
|
40377
|
+
return probesByFile;
|
|
40378
|
+
};
|
|
40379
|
+
/**
|
|
40380
|
+
* Stores each freshly-linted file's probe set + cross-file diagnostics so
|
|
40381
|
+
* the next scan can replay instead of re-linting.
|
|
40382
|
+
*/
|
|
40383
|
+
const storeSidecarEntries = (input) => {
|
|
40384
|
+
let didStoreAnyEntry = false;
|
|
40385
|
+
for (const pass of input.storablePasses) {
|
|
40386
|
+
if (!pass.isTrusted) continue;
|
|
40387
|
+
const diagnosticsByFile = attributeDiagnosticsToFiles(pass.diagnostics, pass.files);
|
|
40388
|
+
if (diagnosticsByFile === null) continue;
|
|
40389
|
+
for (const file of pass.files) {
|
|
40390
|
+
const cacheKey = input.cacheKeyByFile.get(file);
|
|
40391
|
+
if (cacheKey === void 0) continue;
|
|
40392
|
+
const probes = pass.probesByFile.get(file);
|
|
40393
|
+
if (probes === void 0 || probes === null) continue;
|
|
40394
|
+
input.sidecarCache.store(cacheKey, {
|
|
40395
|
+
probes,
|
|
40396
|
+
diagnostics: diagnosticsByFile.get(file) ?? []
|
|
40397
|
+
});
|
|
40398
|
+
didStoreAnyEntry = true;
|
|
40399
|
+
}
|
|
40400
|
+
}
|
|
40401
|
+
if (didStoreAnyEntry) input.sidecarCache.persist();
|
|
40402
|
+
};
|
|
39928
40403
|
const REACT_HOOKS_JS_DROP_PREFIX = "React Compiler rules (react-hooks-js/*) skipped — eslint-plugin-react-hooks failed to load in this environment";
|
|
39929
40404
|
/**
|
|
39930
40405
|
* Detects an oxlint config-load crash caused by the optional
|
|
@@ -39966,7 +40441,7 @@ const reactHooksJsPluginDropNote = (error) => {
|
|
|
39966
40441
|
* 6. always restore disable directives + clean up the temp dir
|
|
39967
40442
|
*/
|
|
39968
40443
|
const runOxlint = async (options) => {
|
|
39969
|
-
const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure, perFileLintCacheEnabled = false, onCacheStats, spawnTimeoutMs, outputMaxBytes, lintBatchOrdering = "cost" } = options;
|
|
40444
|
+
const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure, perFileLintCacheEnabled = false, sidecarLintCacheEnabled = false, onCacheStats, onSidecarStats, spawnTimeoutMs, outputMaxBytes, lintBatchOrdering = "cost" } = options;
|
|
39970
40445
|
const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
|
|
39971
40446
|
const severityControls = buildRuleSeverityControls(userConfig);
|
|
39972
40447
|
validateRuleRegistration();
|
|
@@ -39984,7 +40459,8 @@ const runOxlint = async (options) => {
|
|
|
39984
40459
|
severityControls,
|
|
39985
40460
|
userPlugins,
|
|
39986
40461
|
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin,
|
|
39987
|
-
ruleSelection: overrides.ruleSelection
|
|
40462
|
+
ruleSelection: overrides.ruleSelection,
|
|
40463
|
+
sidecarRuleIdFilter: overrides.sidecarRuleIdFilter
|
|
39988
40464
|
});
|
|
39989
40465
|
const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
|
|
39990
40466
|
const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
|
|
@@ -40087,15 +40563,18 @@ const runOxlint = async (options) => {
|
|
|
40087
40563
|
});
|
|
40088
40564
|
const cache = createFileLintCache(resolveReactDoctorCacheDir(rootDirectory), rulesetHash);
|
|
40089
40565
|
const cacheKeyByFile = /* @__PURE__ */ new Map();
|
|
40566
|
+
const contentHashByRelativePath = /* @__PURE__ */ new Map();
|
|
40090
40567
|
const missFiles = [];
|
|
40091
40568
|
const replayedDiagnostics = [];
|
|
40092
40569
|
let hashSliceStartedAt = performance.now();
|
|
40093
40570
|
for (const candidateFile of candidateFiles) {
|
|
40094
|
-
const
|
|
40571
|
+
const absoluteCandidatePath = Path.resolve(rootDirectory, candidateFile);
|
|
40572
|
+
const contentHash = hashFileContents(absoluteCandidatePath);
|
|
40095
40573
|
if (contentHash === null) {
|
|
40096
40574
|
missFiles.push(candidateFile);
|
|
40097
40575
|
continue;
|
|
40098
40576
|
}
|
|
40577
|
+
contentHashByRelativePath.set(Path.relative(rootDirectory, absoluteCandidatePath).replaceAll("\\", "/"), contentHash);
|
|
40099
40578
|
const cacheKey = `${candidateFile.replaceAll("\\", "/")}\u0000${contentHash}`;
|
|
40100
40579
|
cacheKeyByFile.set(candidateFile, cacheKey);
|
|
40101
40580
|
const cachedDiagnostics = cache.lookup(cacheKey);
|
|
@@ -40107,43 +40586,111 @@ const runOxlint = async (options) => {
|
|
|
40107
40586
|
}
|
|
40108
40587
|
}
|
|
40109
40588
|
const cacheHitFileCount = candidateFiles.length - missFiles.length;
|
|
40589
|
+
const enabledSidecarRuleIds = Object.keys(buildConfig({
|
|
40590
|
+
extendsPaths: [],
|
|
40591
|
+
ruleSelection: "sidecar"
|
|
40592
|
+
}).rules).map((ruleKey) => ruleKey.slice(ruleKey.lastIndexOf("/") + 1)).filter((ruleId) => CROSS_FILE_RULE_IDS.has(ruleId));
|
|
40593
|
+
const boundedSidecarRuleIds = enabledSidecarRuleIds.filter((ruleId) => CROSS_FILE_DEPENDENCY_COLLECTORS.has(ruleId));
|
|
40594
|
+
const boundedSidecarRuleIdSet = new Set(boundedSidecarRuleIds);
|
|
40595
|
+
const unboundedSidecarRuleIds = enabledSidecarRuleIds.filter((ruleId) => !CROSS_FILE_DEPENDENCY_COLLECTORS.has(ruleId));
|
|
40596
|
+
const useSidecarCache = sidecarLintCacheEnabled && boundedSidecarRuleIds.length > 0;
|
|
40597
|
+
const sidecarCache = useSidecarCache ? createSidecarLintCache(resolveReactDoctorCacheDir(rootDirectory), computeRulesetHash({
|
|
40598
|
+
config: buildConfig({
|
|
40599
|
+
extendsPaths: [],
|
|
40600
|
+
ruleSelection: "sidecar",
|
|
40601
|
+
sidecarRuleIdFilter: boundedSidecarRuleIdSet
|
|
40602
|
+
}),
|
|
40603
|
+
toolchainVersions: resolveOxlintToolchainVersions(nodeBinaryPath),
|
|
40604
|
+
ignorePatterns: combinedPatterns,
|
|
40605
|
+
tsconfigContent
|
|
40606
|
+
})) : null;
|
|
40607
|
+
const probeAnswers = createSidecarProbeAnswerResolver({
|
|
40608
|
+
rootDirectory,
|
|
40609
|
+
contentHashByRelativePath
|
|
40610
|
+
});
|
|
40611
|
+
const missProbesTask = sidecarCache === null ? null : collectSidecarProbesForFiles({
|
|
40612
|
+
files: missFiles,
|
|
40613
|
+
rootDirectory,
|
|
40614
|
+
boundedSidecarRuleIds,
|
|
40615
|
+
probeAnswers
|
|
40616
|
+
});
|
|
40110
40617
|
const fullResult = await runConfigOverFiles((overrides) => buildConfig({
|
|
40111
40618
|
extendsPaths: [],
|
|
40112
40619
|
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
|
|
40113
40620
|
}), "oxlintrc.full.json", missFiles, options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(scannedFileCount, candidateFiles.length)));
|
|
40114
40621
|
const missFileSet = new Set(missFiles);
|
|
40115
|
-
const
|
|
40622
|
+
const hitFiles = candidateFiles.filter((candidateFile) => !missFileSet.has(candidateFile));
|
|
40623
|
+
const sidecarReplayedDiagnostics = [];
|
|
40624
|
+
const sidecarLintFiles = [];
|
|
40625
|
+
if (sidecarCache === null) sidecarLintFiles.push(...hitFiles);
|
|
40626
|
+
else {
|
|
40627
|
+
let verifySliceStartedAt = performance.now();
|
|
40628
|
+
for (const hitFile of hitFiles) {
|
|
40629
|
+
const cacheKey = cacheKeyByFile.get(hitFile);
|
|
40630
|
+
const entry = cacheKey === void 0 ? null : sidecarCache.lookup(cacheKey);
|
|
40631
|
+
if (entry !== null && entry.probes.every((probe) => probeAnswers.answerFor(probe.kind, probe.path) === probe.answer)) sidecarReplayedDiagnostics.push(...entry.diagnostics);
|
|
40632
|
+
else sidecarLintFiles.push(hitFile);
|
|
40633
|
+
if (performance.now() - verifySliceStartedAt >= 12) {
|
|
40634
|
+
await yieldToEventLoop();
|
|
40635
|
+
verifySliceStartedAt = performance.now();
|
|
40636
|
+
}
|
|
40637
|
+
}
|
|
40638
|
+
}
|
|
40639
|
+
const sidecarProbesTask = sidecarCache === null ? null : collectSidecarProbesForFiles({
|
|
40640
|
+
files: sidecarLintFiles,
|
|
40641
|
+
rootDirectory,
|
|
40642
|
+
boundedSidecarRuleIds,
|
|
40643
|
+
probeAnswers
|
|
40644
|
+
});
|
|
40645
|
+
const sidecarReplayedFileCount = hitFiles.length - sidecarLintFiles.length;
|
|
40646
|
+
if (sidecarReplayedFileCount > 0) options.onFileProgress?.(missFiles.length + sidecarReplayedFileCount, candidateFiles.length);
|
|
40647
|
+
const boundedSidecarResult = await runConfigOverFiles(() => buildConfig({
|
|
40116
40648
|
extendsPaths: [],
|
|
40117
|
-
ruleSelection: "sidecar"
|
|
40118
|
-
|
|
40649
|
+
ruleSelection: "sidecar",
|
|
40650
|
+
...useSidecarCache ? { sidecarRuleIdFilter: boundedSidecarRuleIdSet } : {}
|
|
40651
|
+
}), "oxlintrc.sidecar.json", sidecarLintFiles, options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(missFiles.length + sidecarReplayedFileCount + scannedFileCount, candidateFiles.length)));
|
|
40652
|
+
const unboundedSidecarResult = useSidecarCache && unboundedSidecarRuleIds.length > 0 ? await runConfigOverFiles(() => buildConfig({
|
|
40653
|
+
extendsPaths: [],
|
|
40654
|
+
ruleSelection: "sidecar",
|
|
40655
|
+
sidecarRuleIdFilter: new Set(unboundedSidecarRuleIds)
|
|
40656
|
+
}), "oxlintrc.sidecar-unbounded.json", hitFiles, void 0) : {
|
|
40657
|
+
diagnostics: [],
|
|
40658
|
+
didDropReactHooksJsPlugin: false,
|
|
40659
|
+
hadPartialFailure: false
|
|
40660
|
+
};
|
|
40119
40661
|
const freshCacheableDiagnostics = [];
|
|
40120
40662
|
for (const diagnostic of fullResult.diagnostics) if (!CROSS_FILE_RULE_IDS.has(diagnostic.rule)) freshCacheableDiagnostics.push(diagnostic);
|
|
40121
40663
|
onCacheStats?.(cacheHitFileCount, candidateFiles.length);
|
|
40122
|
-
|
|
40123
|
-
|
|
40124
|
-
|
|
40125
|
-
let isAttributionSound = true;
|
|
40126
|
-
for (const diagnostic of freshCacheableDiagnostics) {
|
|
40127
|
-
const missFile = missFileByNormalizedPath.get(diagnostic.filePath);
|
|
40128
|
-
if (missFile === void 0) {
|
|
40129
|
-
isAttributionSound = false;
|
|
40130
|
-
break;
|
|
40131
|
-
}
|
|
40132
|
-
const fileDiagnostics = freshDiagnosticsByFile.get(missFile) ?? [];
|
|
40133
|
-
fileDiagnostics.push(diagnostic);
|
|
40134
|
-
freshDiagnosticsByFile.set(missFile, fileDiagnostics);
|
|
40135
|
-
}
|
|
40136
|
-
if (!fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure && isAttributionSound) {
|
|
40664
|
+
if (sidecarCache !== null) onSidecarStats?.(hitFiles.length - sidecarLintFiles.length, hitFiles.length);
|
|
40665
|
+
const freshDiagnosticsByFile = attributeDiagnosticsToFiles(freshCacheableDiagnostics, missFiles);
|
|
40666
|
+
if (!fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure && freshDiagnosticsByFile !== null) {
|
|
40137
40667
|
for (const missFile of missFiles) {
|
|
40138
40668
|
const cacheKey = cacheKeyByFile.get(missFile);
|
|
40139
40669
|
if (cacheKey !== void 0) cache.store(cacheKey, freshDiagnosticsByFile.get(missFile) ?? []);
|
|
40140
40670
|
}
|
|
40141
40671
|
cache.persist();
|
|
40142
40672
|
}
|
|
40673
|
+
if (sidecarCache !== null && missProbesTask !== null && sidecarProbesTask !== null) storeSidecarEntries({
|
|
40674
|
+
sidecarCache,
|
|
40675
|
+
cacheKeyByFile,
|
|
40676
|
+
storablePasses: [{
|
|
40677
|
+
files: sidecarLintFiles,
|
|
40678
|
+
diagnostics: boundedSidecarResult.diagnostics,
|
|
40679
|
+
isTrusted: !boundedSidecarResult.hadPartialFailure,
|
|
40680
|
+
probesByFile: await sidecarProbesTask
|
|
40681
|
+
}, {
|
|
40682
|
+
files: missFiles,
|
|
40683
|
+
diagnostics: fullResult.diagnostics.filter((diagnostic) => boundedSidecarRuleIdSet.has(diagnostic.rule)),
|
|
40684
|
+
isTrusted: !fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure,
|
|
40685
|
+
probesByFile: await missProbesTask
|
|
40686
|
+
}]
|
|
40687
|
+
});
|
|
40143
40688
|
return dedupeDiagnostics([
|
|
40144
40689
|
...replayedDiagnostics,
|
|
40145
40690
|
...fullResult.diagnostics,
|
|
40146
|
-
...
|
|
40691
|
+
...sidecarReplayedDiagnostics,
|
|
40692
|
+
...boundedSidecarResult.diagnostics,
|
|
40693
|
+
...unboundedSidecarResult.diagnostics
|
|
40147
40694
|
]);
|
|
40148
40695
|
}
|
|
40149
40696
|
const baseArgs = makeBaseArgs(configPath);
|
|
@@ -40241,6 +40788,7 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
|
|
|
40241
40788
|
const concurrency = yield* OxlintConcurrency;
|
|
40242
40789
|
const lintBatchOrdering = yield* LintBatchOrdering;
|
|
40243
40790
|
const perFileLintCacheEnabled = yield* PerFileLintCacheEnabled;
|
|
40791
|
+
const sidecarLintCacheEnabled = yield* SidecarLintCacheEnabled;
|
|
40244
40792
|
const collectedFailures = [];
|
|
40245
40793
|
const diagnostics = yield* tryPromise({
|
|
40246
40794
|
try: (signal) => runOxlint({
|
|
@@ -40259,7 +40807,9 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
|
|
|
40259
40807
|
},
|
|
40260
40808
|
onFileProgress: input.onFileProgress,
|
|
40261
40809
|
perFileLintCacheEnabled,
|
|
40810
|
+
sidecarLintCacheEnabled,
|
|
40262
40811
|
onCacheStats: input.onCacheStats,
|
|
40812
|
+
onSidecarStats: input.onSidecarStats,
|
|
40263
40813
|
spawnTimeoutMs,
|
|
40264
40814
|
outputMaxBytes,
|
|
40265
40815
|
concurrency,
|
|
@@ -40681,6 +41231,20 @@ const writeCachedSocketBody = (cacheFile, body) => {
|
|
|
40681
41231
|
}));
|
|
40682
41232
|
} catch {}
|
|
40683
41233
|
};
|
|
41234
|
+
const pruneExpiredSocketCache = (cacheDirectory) => {
|
|
41235
|
+
try {
|
|
41236
|
+
const supplyChainCacheDirectory = Path.join(cacheDirectory, SUPPLY_CHAIN_CACHE_SUBDIR);
|
|
41237
|
+
const expiryThresholdMs = Date.now() - SUPPLY_CHAIN_CACHE_TTL_MS;
|
|
41238
|
+
for (const entryName of NFS.readdirSync(supplyChainCacheDirectory)) {
|
|
41239
|
+
const entryPath = Path.join(supplyChainCacheDirectory, entryName);
|
|
41240
|
+
try {
|
|
41241
|
+
if (NFS.statSync(entryPath).mtimeMs < expiryThresholdMs) NFS.rmSync(entryPath);
|
|
41242
|
+
} catch {
|
|
41243
|
+
continue;
|
|
41244
|
+
}
|
|
41245
|
+
}
|
|
41246
|
+
} catch {}
|
|
41247
|
+
};
|
|
40684
41248
|
const fetchSocketArtifact = (dependency, cacheDirectory) => tryPromise(async (signal) => {
|
|
40685
41249
|
const cacheFile = cacheDirectory === null ? null : supplyChainCacheFile(cacheDirectory, dependency);
|
|
40686
41250
|
if (cacheFile !== null) {
|
|
@@ -40808,6 +41372,7 @@ const checkSupplyChain = (input) => gen(function* () {
|
|
|
40808
41372
|
const dependencies = collectDependenciesToScore(readPackageJson(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies);
|
|
40809
41373
|
if (dependencies.length === 0) return [];
|
|
40810
41374
|
const cacheDirectory = isSupplyChainCacheDisabled() ? null : resolveReactDoctorCacheDir(input.rootDirectory);
|
|
41375
|
+
if (cacheDirectory !== null) pruneExpiredSocketCache(cacheDirectory);
|
|
40811
41376
|
const artifacts = yield* forEach$1(dependencies, (dependency) => fetchSocketArtifact(dependency, cacheDirectory), { concurrency: 8 }).pipe(timeoutOption(input.totalTimeoutMs ?? 9e4), map$3((maybeArtifacts) => getOrElse$1(maybeArtifacts, () => [])));
|
|
40812
41377
|
const diagnostics = [];
|
|
40813
41378
|
for (let index = 0; index < dependencies.length; index += 1) {
|
|
@@ -41034,7 +41599,14 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
41034
41599
|
const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
|
|
41035
41600
|
rootDirectory: scanDirectory,
|
|
41036
41601
|
parseConcurrency: deadCodeParseConcurrency,
|
|
41037
|
-
workerTimeoutMs: deadCodeTimeout.workerTimeoutMs
|
|
41602
|
+
workerTimeoutMs: deadCodeTimeout.workerTimeoutMs,
|
|
41603
|
+
onCacheOutcome: (didHitCache) => {
|
|
41604
|
+
deadCodeCacheHit = didHitCache;
|
|
41605
|
+
},
|
|
41606
|
+
onSummaryCacheStats: (stats) => {
|
|
41607
|
+
deadCodeSummaryCacheHits = stats.hits;
|
|
41608
|
+
deadCodeSummaryCacheMisses = stats.misses;
|
|
41609
|
+
}
|
|
41038
41610
|
}).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
|
|
41039
41611
|
yield* set(deadCodeFailure, {
|
|
41040
41612
|
didFail: true,
|
|
@@ -41062,6 +41634,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
41062
41634
|
let lastReportedTotalFileCount = 0;
|
|
41063
41635
|
let lintCacheHitFileCount = null;
|
|
41064
41636
|
let lintCacheTotalFileCount = null;
|
|
41637
|
+
let lintSidecarReplayedFileCount = null;
|
|
41638
|
+
let lintSidecarTotalFileCount = null;
|
|
41639
|
+
let deadCodeCacheHit = null;
|
|
41640
|
+
let deadCodeSummaryCacheHits = null;
|
|
41641
|
+
let deadCodeSummaryCacheMisses = null;
|
|
41065
41642
|
const baseLintStream = linterService.run({
|
|
41066
41643
|
rootDirectory: scanDirectory,
|
|
41067
41644
|
project,
|
|
@@ -41081,6 +41658,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
41081
41658
|
lintCacheHitFileCount = cacheHitFileCount;
|
|
41082
41659
|
lintCacheTotalFileCount = totalConsideredFileCount;
|
|
41083
41660
|
},
|
|
41661
|
+
onSidecarStats: (sidecarReplayedFileCount, sidecarConsideredFileCount) => {
|
|
41662
|
+
lintSidecarReplayedFileCount = sidecarReplayedFileCount;
|
|
41663
|
+
lintSidecarTotalFileCount = sidecarConsideredFileCount;
|
|
41664
|
+
},
|
|
41084
41665
|
deadlineEpochMs: input.deadlineEpochMs
|
|
41085
41666
|
}).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
|
|
41086
41667
|
yield* set(lintFailure, {
|
|
@@ -41191,6 +41772,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
41191
41772
|
securityScanFailed,
|
|
41192
41773
|
lintCacheHitFileCount,
|
|
41193
41774
|
lintCacheTotalFileCount,
|
|
41775
|
+
lintSidecarReplayedFileCount,
|
|
41776
|
+
lintSidecarTotalFileCount,
|
|
41777
|
+
deadCodeCacheHit: lintFailureState.didFail ? null : deadCodeCacheHit,
|
|
41778
|
+
deadCodeSummaryCacheHits: lintFailureState.didFail ? null : deadCodeSummaryCacheHits,
|
|
41779
|
+
deadCodeSummaryCacheMisses: lintFailureState.didFail ? null : deadCodeSummaryCacheMisses,
|
|
41194
41780
|
suppressedRuleCounts: transform.summarizeSuppressions()
|
|
41195
41781
|
};
|
|
41196
41782
|
}).pipe(withSpan("runInspect", { attributes: {
|
|
@@ -41503,11 +42089,9 @@ const computeConfigFingerprint = (projectDirectory, version) => {
|
|
|
41503
42089
|
const parts = [`v=${version}`];
|
|
41504
42090
|
let directory = projectDirectory;
|
|
41505
42091
|
for (;;) {
|
|
41506
|
-
for (const filename of CONFIG_FINGERPRINT_FILENAMES)
|
|
41507
|
-
const
|
|
41508
|
-
parts.push(`${directory}/${filename}=${
|
|
41509
|
-
} catch {
|
|
41510
|
-
continue;
|
|
42092
|
+
for (const filename of CONFIG_FINGERPRINT_FILENAMES) {
|
|
42093
|
+
const contentHash = hashFileContents(path.join(directory, filename));
|
|
42094
|
+
if (contentHash !== null) parts.push(`${directory}/${filename}=${contentHash}`);
|
|
41511
42095
|
}
|
|
41512
42096
|
const parent = path.dirname(directory);
|
|
41513
42097
|
if (parent === directory) break;
|
|
@@ -41520,7 +42104,7 @@ const computeConfigFingerprint = (projectDirectory, version) => {
|
|
|
41520
42104
|
/** Display name used in client-facing messages and progress titles. */
|
|
41521
42105
|
const SERVER_DISPLAY_NAME = "React Doctor";
|
|
41522
42106
|
/** Server version reported in `serverInfo`; injected at build, `dev` from source. */
|
|
41523
|
-
const SERVER_VERSION = "0.
|
|
42107
|
+
const SERVER_VERSION = "0.7.0";
|
|
41524
42108
|
/** `Diagnostic.source` shown next to every published diagnostic. */
|
|
41525
42109
|
const DIAGNOSTIC_SOURCE = "react-doctor";
|
|
41526
42110
|
/**
|
|
@@ -43227,6 +43811,7 @@ const METRIC = {
|
|
|
43227
43811
|
scanScore: "scan.score",
|
|
43228
43812
|
scanClean: "scan.clean",
|
|
43229
43813
|
scanCheckSkipped: "scan.check_skipped",
|
|
43814
|
+
scanNoReactDetected: "scan.no_react_detected",
|
|
43230
43815
|
baselineDegraded: "baseline.degraded",
|
|
43231
43816
|
ruleFired: "rule.fired",
|
|
43232
43817
|
ruleDisabled: "rule.disabled",
|
|
@@ -43570,5 +44155,5 @@ const startLanguageServer = () => {
|
|
|
43570
44155
|
};
|
|
43571
44156
|
//#endregion
|
|
43572
44157
|
export { startLanguageServer };
|
|
43573
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
43574
|
-
//# debugId=
|
|
44158
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="1c7dd926-433e-5edb-a6d7-2bb0a9f6df24")}catch(e){}}();
|
|
44159
|
+
//# debugId=1c7dd926-433e-5edb-a6d7-2bb0a9f6df24
|