react-doctor 0.6.3-dev.8325343 → 0.6.3-dev.bdf8074
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 +936 -280
- package/dist/index.d.ts +26 -0
- package/dist/index.js +557 -77
- package/dist/lsp.js +557 -77
- package/package.json +2 -2
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";
|
|
@@ -32986,6 +32987,10 @@ const SOCKET_PACKAGE_PAGE_BASE = "https://socket.dev/npm/package";
|
|
|
32986
32987
|
const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
|
|
32987
32988
|
const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
|
|
32988
32989
|
const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
|
|
32990
|
+
const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
|
|
32991
|
+
const CORE_PACKAGE_VERSION = "0.6.3";
|
|
32992
|
+
const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
|
|
32993
|
+
const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
|
|
32989
32994
|
const SUPPLY_CHAIN_PLUGIN = "socket";
|
|
32990
32995
|
const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
|
|
32991
32996
|
const SUPPLY_CHAIN_CATEGORY = "Security";
|
|
@@ -35646,6 +35651,48 @@ var PerFileLintCacheEnabled = class extends Reference("react-doctor/PerFileLintC
|
|
|
35646
35651
|
if (CACHE_DISABLED_VALUES.has(noFileCache)) return false;
|
|
35647
35652
|
return true;
|
|
35648
35653
|
} }) {};
|
|
35654
|
+
/**
|
|
35655
|
+
* Whether the sidecar lint cache (`runners/oxlint/sidecar-lint-cache.ts`) is
|
|
35656
|
+
* active. Defaults ON — warm rescans replay the cross-file rules' cached
|
|
35657
|
+
* diagnostics for every file whose dependency fingerprint (the probe set the
|
|
35658
|
+
* plugin's collectors recorded) still matches the tree, and re-lint only the
|
|
35659
|
+
* rest. Only reachable when the per-file lint cache itself is on (the
|
|
35660
|
+
* sidecar covers its cache hits), so `REACT_DOCTOR_NO_CACHE` /
|
|
35661
|
+
* `REACT_DOCTOR_NO_FILE_CACHE` implicitly disable it too. Opt-OUT knobs:
|
|
35662
|
+
*
|
|
35663
|
+
* - `REACT_DOCTOR_NO_CACHE` — the global off-switch.
|
|
35664
|
+
* - `REACT_DOCTOR_NO_SIDECAR_CACHE` — granular rollback hatch: keep the
|
|
35665
|
+
* per-file cache but run the always-fresh sidecar over every hit
|
|
35666
|
+
* (the pre-cache behavior).
|
|
35667
|
+
*
|
|
35668
|
+
* Tests override via `Layer.succeed(SidecarLintCacheEnabled, false)`.
|
|
35669
|
+
*/
|
|
35670
|
+
var SidecarLintCacheEnabled = class extends Reference("react-doctor/SidecarLintCacheEnabled", { defaultValue: () => {
|
|
35671
|
+
const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
|
|
35672
|
+
const noSidecarCache = process.env["REACT_DOCTOR_NO_SIDECAR_CACHE"]?.toLowerCase() ?? "";
|
|
35673
|
+
if (CACHE_DISABLED_VALUES.has(noCache)) return false;
|
|
35674
|
+
if (CACHE_DISABLED_VALUES.has(noSidecarCache)) return false;
|
|
35675
|
+
return true;
|
|
35676
|
+
} }) {};
|
|
35677
|
+
/**
|
|
35678
|
+
* Whether the whole-project dead-code result cache
|
|
35679
|
+
* (`dead-code/dead-code-result-cache.ts`) is active. Defaults ON — a rescan
|
|
35680
|
+
* whose inputs (source tree, manifests, configs, analyzer version) are
|
|
35681
|
+
* unchanged replays the stored diagnostics instead of re-running the
|
|
35682
|
+
* analysis worker. Opt-OUT, two knobs (matching the per-file lint cache):
|
|
35683
|
+
*
|
|
35684
|
+
* - `REACT_DOCTOR_NO_CACHE` — the global off-switch.
|
|
35685
|
+
* - `REACT_DOCTOR_NO_DEAD_CODE_CACHE` — granular: bust only this cache.
|
|
35686
|
+
*
|
|
35687
|
+
* Tests override via `Layer.succeed(DeadCodeResultCacheEnabled, false)`.
|
|
35688
|
+
*/
|
|
35689
|
+
var DeadCodeResultCacheEnabled = class extends Reference("react-doctor/DeadCodeResultCacheEnabled", { defaultValue: () => {
|
|
35690
|
+
const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
|
|
35691
|
+
const noDeadCodeCache = process.env["REACT_DOCTOR_NO_DEAD_CODE_CACHE"]?.toLowerCase() ?? "";
|
|
35692
|
+
if (CACHE_DISABLED_VALUES.has(noCache)) return false;
|
|
35693
|
+
if (CACHE_DISABLED_VALUES.has(noDeadCodeCache)) return false;
|
|
35694
|
+
return true;
|
|
35695
|
+
} }) {};
|
|
35649
35696
|
const DIAGNOSTIC_SURFACES = [
|
|
35650
35697
|
"cli",
|
|
35651
35698
|
"prComment",
|
|
@@ -37286,6 +37333,80 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
|
|
|
37286
37333
|
return [...seen].filter((pattern) => pattern.length > 0);
|
|
37287
37334
|
};
|
|
37288
37335
|
const collectDeadCodeEntryPatterns = (rootDirectory) => [...new Set(collectKnipPatterns(rootDirectory, "entry"))].filter((pattern) => pattern.length > 0);
|
|
37336
|
+
const atomicWriteJson = (filePath, value) => {
|
|
37337
|
+
try {
|
|
37338
|
+
NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
|
|
37339
|
+
const temporaryPath = `${filePath}.${process.pid}.tmp`;
|
|
37340
|
+
NFS.writeFileSync(temporaryPath, JSON.stringify(value));
|
|
37341
|
+
NFS.renameSync(temporaryPath, filePath);
|
|
37342
|
+
} catch {
|
|
37343
|
+
return;
|
|
37344
|
+
}
|
|
37345
|
+
};
|
|
37346
|
+
const failOpenReadJson = (filePath, fallback) => {
|
|
37347
|
+
try {
|
|
37348
|
+
return JSON.parse(NFS.readFileSync(filePath, "utf8"));
|
|
37349
|
+
} catch {
|
|
37350
|
+
return fallback;
|
|
37351
|
+
}
|
|
37352
|
+
};
|
|
37353
|
+
const ANALYZED_FILE_EXTENSIONS = new Set(DEFAULT_EXTENSIONS);
|
|
37354
|
+
const CORE_SIDE_MANIFEST_NAMES = ["knip.json", "deno.lock"];
|
|
37355
|
+
const ANALYZED_MANIFEST_NAMES = new Set([...ANALYZED_MANIFEST_FILENAMES, ...CORE_SIDE_MANIFEST_NAMES]);
|
|
37356
|
+
const isTsConfigLikeFile = (fileName) => (fileName.startsWith("tsconfig") || fileName.startsWith("jsconfig")) && fileName.endsWith(".json");
|
|
37357
|
+
const isFingerprintedFile = (fileName) => ANALYZED_FILE_EXTENSIONS.has(Path.extname(fileName).toLowerCase()) || ANALYZED_MANIFEST_NAMES.has(fileName) || isTsConfigLikeFile(fileName);
|
|
37358
|
+
const collectAnalyzedFileFingerprints = (rootDirectory) => {
|
|
37359
|
+
const fingerprints = [];
|
|
37360
|
+
for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) {
|
|
37361
|
+
if (!isFingerprintedFile(name)) continue;
|
|
37362
|
+
try {
|
|
37363
|
+
const fileStat = NFS.statSync(absolutePath);
|
|
37364
|
+
const relativePath = Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/");
|
|
37365
|
+
fingerprints.push(`${relativePath}:${fileStat.mtimeMs}:${fileStat.size}`);
|
|
37366
|
+
} catch {}
|
|
37367
|
+
}
|
|
37368
|
+
return fingerprints.sort();
|
|
37369
|
+
};
|
|
37370
|
+
const bundledRequire$2 = createRequire(import.meta.url);
|
|
37371
|
+
const resolveDeslopVersion = () => {
|
|
37372
|
+
try {
|
|
37373
|
+
const packageJson = JSON.parse(NFS.readFileSync(bundledRequire$2.resolve("deslop-js/package.json"), "utf8"));
|
|
37374
|
+
return isRecord(packageJson) && typeof packageJson.version === "string" ? packageJson.version : "unknown";
|
|
37375
|
+
} catch {
|
|
37376
|
+
return "unknown";
|
|
37377
|
+
}
|
|
37378
|
+
};
|
|
37379
|
+
const computeDeadCodeCacheKey = (input) => crypto.createHash("sha1").update(JSON.stringify({
|
|
37380
|
+
schemaVersion: 1,
|
|
37381
|
+
coreVersion: input.coreVersion,
|
|
37382
|
+
deslopVersion: resolveDeslopVersion(),
|
|
37383
|
+
deslopJsModuleSpecifier: input.deslopJsModuleSpecifier,
|
|
37384
|
+
entryPatterns: input.entryPatterns,
|
|
37385
|
+
ignorePatterns: input.ignorePatterns,
|
|
37386
|
+
tsConfigFile: input.tsConfigPath === void 0 ? null : Path.relative(input.rootDirectory, input.tsConfigPath).replace(/\\/g, "/"),
|
|
37387
|
+
files: collectAnalyzedFileFingerprints(input.rootDirectory)
|
|
37388
|
+
})).digest("hex");
|
|
37389
|
+
const validateDiagnostic$2 = decodeUnknownSync(Diagnostic);
|
|
37390
|
+
const decodeCachedDiagnostics = (raw) => {
|
|
37391
|
+
try {
|
|
37392
|
+
for (const entry of raw) validateDiagnostic$2(entry);
|
|
37393
|
+
return raw;
|
|
37394
|
+
} catch {
|
|
37395
|
+
return null;
|
|
37396
|
+
}
|
|
37397
|
+
};
|
|
37398
|
+
const lookupDeadCodeResultCache = (cacheDirectory, cacheKey) => {
|
|
37399
|
+
const persisted = failOpenReadJson(Path.join(cacheDirectory, DEAD_CODE_CACHE_FILENAME), null);
|
|
37400
|
+
if (persisted === null || !isRecord(persisted) || persisted.version !== 1 || persisted.key !== cacheKey || !Array.isArray(persisted.diagnostics)) return null;
|
|
37401
|
+
return decodeCachedDiagnostics(persisted.diagnostics);
|
|
37402
|
+
};
|
|
37403
|
+
const storeDeadCodeResultCache = (cacheDirectory, cacheKey, diagnostics) => {
|
|
37404
|
+
atomicWriteJson(Path.join(cacheDirectory, DEAD_CODE_CACHE_FILENAME), {
|
|
37405
|
+
version: 1,
|
|
37406
|
+
key: cacheKey,
|
|
37407
|
+
diagnostics
|
|
37408
|
+
});
|
|
37409
|
+
};
|
|
37289
37410
|
const readSystemFacts = () => ({
|
|
37290
37411
|
availableCores: os.availableParallelism(),
|
|
37291
37412
|
totalMemoryBytes: os.totalmem(),
|
|
@@ -37354,6 +37475,21 @@ const withDeadCodeWorkerSlot = async (task, abortSignal) => {
|
|
|
37354
37475
|
releaseSlot();
|
|
37355
37476
|
}
|
|
37356
37477
|
};
|
|
37478
|
+
const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
|
|
37479
|
+
const resolveReactDoctorCacheDir = (projectDirectory) => {
|
|
37480
|
+
const cacheDirOverride = process.env["REACT_DOCTOR_CACHE_DIR"]?.trim();
|
|
37481
|
+
if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
|
|
37482
|
+
const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
|
|
37483
|
+
if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
|
|
37484
|
+
return Path.join(os.tmpdir(), `react-doctor-cache-${resolveUserCacheScope()}`, projectCacheSubdir(projectDirectory));
|
|
37485
|
+
};
|
|
37486
|
+
const resolveUserCacheScope = () => {
|
|
37487
|
+
try {
|
|
37488
|
+
return String(process.getuid?.() ?? os.userInfo().username);
|
|
37489
|
+
} catch {
|
|
37490
|
+
return "shared";
|
|
37491
|
+
}
|
|
37492
|
+
};
|
|
37357
37493
|
/**
|
|
37358
37494
|
* Resolves a path to its canonical, symlink-free form, falling back to
|
|
37359
37495
|
* the input when it cannot be realpath'd (broken symlink, permission
|
|
@@ -37407,6 +37543,14 @@ process.stdin.on("end", () => {
|
|
|
37407
37543
|
circularDependencies: result.circularDependencies.map((cycle) => ({
|
|
37408
37544
|
files: cycle.files,
|
|
37409
37545
|
})),
|
|
37546
|
+
...(result.incrementalCacheStats
|
|
37547
|
+
? {
|
|
37548
|
+
summaryCacheStats: {
|
|
37549
|
+
hits: result.incrementalCacheStats.summaryHits,
|
|
37550
|
+
misses: result.incrementalCacheStats.summaryMisses,
|
|
37551
|
+
},
|
|
37552
|
+
}
|
|
37553
|
+
: {}),
|
|
37410
37554
|
});
|
|
37411
37555
|
|
|
37412
37556
|
const serializeError = (error) =>
|
|
@@ -37430,6 +37574,9 @@ process.stdin.on("end", () => {
|
|
|
37430
37574
|
...(workerInput.ignorePatterns.length > 0
|
|
37431
37575
|
? { ignorePatterns: workerInput.ignorePatterns }
|
|
37432
37576
|
: {}),
|
|
37577
|
+
...(workerInput.incrementalCachePath
|
|
37578
|
+
? { incrementalCachePath: workerInput.incrementalCachePath }
|
|
37579
|
+
: {}),
|
|
37433
37580
|
// We consume only deslop's GRAPH-based findings (unusedFiles, unusedExports,
|
|
37434
37581
|
// unusedDependencies, circularDependencies). Everything else deslop can compute
|
|
37435
37582
|
// is pure wasted work for us, and it's the bulk of the runtime:
|
|
@@ -37440,12 +37587,18 @@ process.stdin.on("end", () => {
|
|
|
37440
37587
|
// are the single most expensive pass — duplicate-block detection alone was
|
|
37441
37588
|
// ~83s of a ~130s Sentry scan — so skipping them is an ~8.5x dead-code
|
|
37442
37589
|
// speedup on a large repo.
|
|
37443
|
-
//
|
|
37590
|
+
// - reportRedundancy: the DRY-pattern detectors (duplicate types/constants,
|
|
37591
|
+
// simplifiable functions, identity wrappers, …) — ~120 ms of discarded
|
|
37592
|
+
// output per scan, and skipping them lets the incremental cache drop the
|
|
37593
|
+
// DRY-pattern summary fields (the largest slice of the cache file).
|
|
37594
|
+
// All are provably safe: the consumed graph findings are computed by their own
|
|
37444
37595
|
// detectors, independent of these passes (confirmed byte-identical on
|
|
37445
|
-
// excalidraw + mui-material + sentry
|
|
37446
|
-
//
|
|
37596
|
+
// excalidraw + mui-material + sentry; re-verified on sentry after the
|
|
37597
|
+
// reportRedundancy flip). tsConfigPath stays — the module resolver needs it
|
|
37598
|
+
// for path-alias resolution in the import graph.
|
|
37447
37599
|
semantic: { enabled: false },
|
|
37448
37600
|
reportCodeQuality: false,
|
|
37601
|
+
reportRedundancy: false,
|
|
37449
37602
|
};
|
|
37450
37603
|
const result = await analyze(defineConfig(config));
|
|
37451
37604
|
emit({ ok: true, result: normalizeResult(result) });
|
|
@@ -37529,13 +37682,23 @@ const parseCircularDependencies = (value) => {
|
|
|
37529
37682
|
}
|
|
37530
37683
|
return circularDependencies;
|
|
37531
37684
|
};
|
|
37685
|
+
const parseSummaryCacheStats = (value) => {
|
|
37686
|
+
if (!isRecord(value)) return void 0;
|
|
37687
|
+
if (typeof value.hits !== "number" || typeof value.misses !== "number") return void 0;
|
|
37688
|
+
return {
|
|
37689
|
+
hits: value.hits,
|
|
37690
|
+
misses: value.misses
|
|
37691
|
+
};
|
|
37692
|
+
};
|
|
37532
37693
|
const parseDeadCodeWorkerResult = (value) => {
|
|
37533
37694
|
if (!isRecord(value)) throw new Error("Dead-code worker returned an invalid result.");
|
|
37695
|
+
const summaryCacheStats = parseSummaryCacheStats(value.summaryCacheStats);
|
|
37534
37696
|
return {
|
|
37535
37697
|
unusedFiles: parseUnusedFiles(value.unusedFiles),
|
|
37536
37698
|
unusedExports: parseUnusedExports(value.unusedExports),
|
|
37537
37699
|
unusedDependencies: parseUnusedDependencies(value.unusedDependencies),
|
|
37538
|
-
circularDependencies: parseCircularDependencies(value.circularDependencies)
|
|
37700
|
+
circularDependencies: parseCircularDependencies(value.circularDependencies),
|
|
37701
|
+
...summaryCacheStats ? { summaryCacheStats } : {}
|
|
37539
37702
|
};
|
|
37540
37703
|
};
|
|
37541
37704
|
const parseDeadCodeWorkerError = (value) => {
|
|
@@ -37649,17 +37812,35 @@ const checkDeadCode = async (options) => {
|
|
|
37649
37812
|
if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
|
|
37650
37813
|
const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
|
|
37651
37814
|
const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
|
|
37815
|
+
const tsConfigPath = resolveTsConfigPath(rootDirectory);
|
|
37816
|
+
const deslopJsModuleSpecifier = options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js");
|
|
37817
|
+
const cacheKey = options.cacheEnabled ? computeDeadCodeCacheKey({
|
|
37818
|
+
rootDirectory,
|
|
37819
|
+
entryPatterns,
|
|
37820
|
+
ignorePatterns,
|
|
37821
|
+
tsConfigPath,
|
|
37822
|
+
deslopJsModuleSpecifier,
|
|
37823
|
+
coreVersion: CORE_PACKAGE_VERSION
|
|
37824
|
+
}) : null;
|
|
37825
|
+
if (cacheKey !== null) {
|
|
37826
|
+
const cachedDiagnostics = lookupDeadCodeResultCache(resolveReactDoctorCacheDir(rootDirectory), cacheKey);
|
|
37827
|
+
options.onCacheOutcome?.(cachedDiagnostics !== null);
|
|
37828
|
+
if (cachedDiagnostics !== null) return [...cachedDiagnostics];
|
|
37829
|
+
}
|
|
37830
|
+
const incrementalCachePath = options.cacheEnabled === true ? Path.join(resolveReactDoctorCacheDir(rootDirectory), DEAD_CODE_SUMMARY_CACHE_FILENAME) : void 0;
|
|
37652
37831
|
const spawnAndRun = () => {
|
|
37653
37832
|
return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
|
|
37654
37833
|
rootDirectory,
|
|
37655
37834
|
entryPatterns,
|
|
37656
|
-
tsConfigPath
|
|
37835
|
+
tsConfigPath,
|
|
37657
37836
|
ignorePatterns,
|
|
37658
|
-
deslopJsModuleSpecifier
|
|
37659
|
-
parseConcurrency: options.parseConcurrency
|
|
37837
|
+
deslopJsModuleSpecifier,
|
|
37838
|
+
parseConcurrency: options.parseConcurrency,
|
|
37839
|
+
incrementalCachePath
|
|
37660
37840
|
}), options.workerTimeoutMs ?? 12e4, options.abortSignal);
|
|
37661
37841
|
};
|
|
37662
37842
|
const result = parseDeadCodeWorkerResult(options.createWorker === void 0 ? await withDeadCodeWorkerSlot(spawnAndRun, options.abortSignal) : await spawnAndRun());
|
|
37843
|
+
if (result.summaryCacheStats !== void 0) options.onSummaryCacheStats?.(result.summaryCacheStats);
|
|
37663
37844
|
const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
|
|
37664
37845
|
const diagnostics = [];
|
|
37665
37846
|
for (const unusedFile of result.unusedFiles) diagnostics.push({
|
|
@@ -37716,6 +37897,7 @@ const checkDeadCode = async (options) => {
|
|
|
37716
37897
|
category: DEAD_CODE_CATEGORY
|
|
37717
37898
|
});
|
|
37718
37899
|
}
|
|
37900
|
+
if (cacheKey !== null) storeDeadCodeResultCache(resolveReactDoctorCacheDir(rootDirectory), cacheKey, diagnostics);
|
|
37719
37901
|
return diagnostics;
|
|
37720
37902
|
};
|
|
37721
37903
|
const DEAD_CODE_RULE_KEY_PREFIX = `${DEAD_CODE_PLUGIN}/`;
|
|
@@ -37818,12 +38000,16 @@ var Config = class Config extends Service()("react-doctor/Config") {
|
|
|
37818
38000
|
*/
|
|
37819
38001
|
var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
|
|
37820
38002
|
static layerNode = succeed$3(DeadCode, DeadCode.of({ run: (input) => unwrap(fn("DeadCode.run")(function* () {
|
|
38003
|
+
const cacheEnabled = yield* DeadCodeResultCacheEnabled;
|
|
37821
38004
|
return yield* tryPromise({
|
|
37822
38005
|
try: (signal) => checkDeadCode({
|
|
37823
38006
|
rootDirectory: input.rootDirectory,
|
|
37824
38007
|
parseConcurrency: input.parseConcurrency,
|
|
37825
38008
|
workerTimeoutMs: input.workerTimeoutMs,
|
|
37826
|
-
abortSignal: signal
|
|
38009
|
+
abortSignal: signal,
|
|
38010
|
+
cacheEnabled,
|
|
38011
|
+
onCacheOutcome: input.onCacheOutcome,
|
|
38012
|
+
onSummaryCacheStats: input.onSummaryCacheStats
|
|
37827
38013
|
}),
|
|
37828
38014
|
catch: (cause) => new ReactDoctorError({ reason: new DeadCodeAnalysisFailed({ cause }) })
|
|
37829
38015
|
}).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)));
|
|
@@ -38693,7 +38879,7 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
|
|
|
38693
38879
|
}
|
|
38694
38880
|
return enabled;
|
|
38695
38881
|
};
|
|
38696
|
-
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection }) => {
|
|
38882
|
+
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection, sidecarRuleIdFilter }) => {
|
|
38697
38883
|
const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
|
|
38698
38884
|
const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
|
|
38699
38885
|
const jsPlugins = [];
|
|
@@ -38705,6 +38891,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
38705
38891
|
if (!rule) continue;
|
|
38706
38892
|
if (ruleSelection === "cacheable" && CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
|
|
38707
38893
|
if (ruleSelection === "sidecar" && !CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
|
|
38894
|
+
if (ruleSelection === "sidecar" && sidecarRuleIdFilter !== void 0 && !sidecarRuleIdFilter.has(registryEntry.id)) continue;
|
|
38708
38895
|
if (rule.scan !== void 0) continue;
|
|
38709
38896
|
if (customRulesOnly && registryEntry.originallyExternal) continue;
|
|
38710
38897
|
if (rule.framework !== "global" && !rule.requires) continue;
|
|
@@ -38749,33 +38936,152 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
38749
38936
|
}
|
|
38750
38937
|
};
|
|
38751
38938
|
};
|
|
38752
|
-
const
|
|
38939
|
+
const validateDiagnostic$1 = decodeUnknownSync(Diagnostic);
|
|
38940
|
+
const decodeFileDiagnostics = (raw) => {
|
|
38941
|
+
if (!Array.isArray(raw)) return null;
|
|
38753
38942
|
try {
|
|
38754
|
-
|
|
38755
|
-
|
|
38756
|
-
NFS.writeFileSync(temporaryPath, JSON.stringify(value));
|
|
38757
|
-
NFS.renameSync(temporaryPath, filePath);
|
|
38943
|
+
for (const entry of raw) validateDiagnostic$1(entry);
|
|
38944
|
+
return raw;
|
|
38758
38945
|
} catch {
|
|
38759
|
-
return;
|
|
38946
|
+
return null;
|
|
38760
38947
|
}
|
|
38761
38948
|
};
|
|
38762
|
-
const
|
|
38949
|
+
const emptyCache$1 = () => ({
|
|
38950
|
+
version: 1,
|
|
38951
|
+
rulesets: {}
|
|
38952
|
+
});
|
|
38953
|
+
const loadRulesetEntries$1 = (cacheFilePath, rulesetHash) => {
|
|
38954
|
+
const entries = /* @__PURE__ */ new Map();
|
|
38955
|
+
const persisted = failOpenReadJson(cacheFilePath, emptyCache$1());
|
|
38956
|
+
if (persisted.version !== 1 || !isRecord(persisted.rulesets)) return entries;
|
|
38957
|
+
const bucket = persisted.rulesets[rulesetHash];
|
|
38958
|
+
if (!isRecord(bucket) || !isRecord(bucket.files)) return entries;
|
|
38959
|
+
for (const [fileKey, rawDiagnostics] of Object.entries(bucket.files)) {
|
|
38960
|
+
const decoded = decodeFileDiagnostics(rawDiagnostics);
|
|
38961
|
+
if (decoded !== null) entries.set(fileKey, decoded);
|
|
38962
|
+
}
|
|
38963
|
+
return entries;
|
|
38964
|
+
};
|
|
38965
|
+
const createFileLintCache = (cacheDirectory, rulesetHash) => {
|
|
38966
|
+
const cacheFilePath = Path.join(cacheDirectory, FILE_LINT_CACHE_FILENAME);
|
|
38967
|
+
const entries = loadRulesetEntries$1(cacheFilePath, rulesetHash);
|
|
38968
|
+
return {
|
|
38969
|
+
lookup: (fileKey) => entries.get(fileKey) ?? null,
|
|
38970
|
+
store: (fileKey, diagnostics) => {
|
|
38971
|
+
entries.delete(fileKey);
|
|
38972
|
+
entries.set(fileKey, diagnostics);
|
|
38973
|
+
},
|
|
38974
|
+
persist: () => {
|
|
38975
|
+
const onDisk = failOpenReadJson(cacheFilePath, emptyCache$1());
|
|
38976
|
+
const rulesets = onDisk.version === 1 && isRecord(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
|
|
38977
|
+
const existingBucket = rulesets[rulesetHash];
|
|
38978
|
+
const existingFiles = isRecord(existingBucket) && isRecord(existingBucket.files) ? existingBucket.files : {};
|
|
38979
|
+
const ourFiles = {};
|
|
38980
|
+
for (const [fileKey, diagnostics] of entries) ourFiles[fileKey] = diagnostics;
|
|
38981
|
+
const cappedEntries = Object.entries({
|
|
38982
|
+
...existingFiles,
|
|
38983
|
+
...ourFiles
|
|
38984
|
+
}).slice(-FILE_LINT_CACHE_MAX_FILE_COUNT);
|
|
38985
|
+
rulesets[rulesetHash] = {
|
|
38986
|
+
updatedAtMs: Date.now(),
|
|
38987
|
+
files: Object.fromEntries(cappedEntries)
|
|
38988
|
+
};
|
|
38989
|
+
atomicWriteJson(cacheFilePath, {
|
|
38990
|
+
version: 1,
|
|
38991
|
+
rulesets: Object.fromEntries(Object.entries(rulesets).filter(([, bucket]) => isRecord(bucket) && typeof bucket.updatedAtMs === "number").sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8))
|
|
38992
|
+
});
|
|
38993
|
+
}
|
|
38994
|
+
};
|
|
38995
|
+
};
|
|
38996
|
+
const hashFileContents = (filePath) => {
|
|
38763
38997
|
try {
|
|
38764
|
-
return
|
|
38998
|
+
return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
|
|
38765
38999
|
} catch {
|
|
38766
|
-
return
|
|
39000
|
+
return null;
|
|
38767
39001
|
}
|
|
38768
39002
|
};
|
|
39003
|
+
const ABSENT_CONTENT_ANSWER = "absent";
|
|
39004
|
+
/**
|
|
39005
|
+
* Answers dependency probes for the sidecar lint cache — used both when
|
|
39006
|
+
* STORING an entry (turn the collector's probe paths into answers) and when
|
|
39007
|
+
* VERIFYING one (re-answer each stored probe against the current tree).
|
|
39008
|
+
* Store and verify must agree exactly, so both go through this one resolver.
|
|
39009
|
+
*
|
|
39010
|
+
* - `content` — SHA-1 of the file's bytes, `"absent"` when unreadable
|
|
39011
|
+
* (missing, a directory, permission failure). Source files reuse the
|
|
39012
|
+
* hashes the cache-partition loop already computed
|
|
39013
|
+
* (`contentHashByRelativePath`); everything else (package.json,
|
|
39014
|
+
* tsconfig, ignored files) is hashed on demand.
|
|
39015
|
+
* - `exists` — the existence class a module-resolution candidate probe
|
|
39016
|
+
* sees: `"file"`, `"dir"`, or `"none"` (missing or an exotic entry —
|
|
39017
|
+
* `statSync` follows symlinks, matching the resolvers).
|
|
39018
|
+
*
|
|
39019
|
+
* Answers are memoized per scan: the filesystem is frozen for the run, and
|
|
39020
|
+
* hot dependency paths (a barrel imported by hundreds of files, the shared
|
|
39021
|
+
* tsconfig chain) repeat across many files' probe sets.
|
|
39022
|
+
*/
|
|
39023
|
+
const createSidecarProbeAnswerResolver = (input) => {
|
|
39024
|
+
const contentAnswerByPath = /* @__PURE__ */ new Map();
|
|
39025
|
+
const existsAnswerByPath = /* @__PURE__ */ new Map();
|
|
39026
|
+
const toRelativePath = (absolutePath) => Path.relative(input.rootDirectory, absolutePath).replaceAll("\\", "/");
|
|
39027
|
+
const contentAnswer = (relativePath) => {
|
|
39028
|
+
const memoized = contentAnswerByPath.get(relativePath);
|
|
39029
|
+
if (memoized !== void 0) return memoized;
|
|
39030
|
+
const answer = input.contentHashByRelativePath.get(relativePath) ?? hashFileContents(Path.resolve(input.rootDirectory, relativePath)) ?? ABSENT_CONTENT_ANSWER;
|
|
39031
|
+
contentAnswerByPath.set(relativePath, answer);
|
|
39032
|
+
return answer;
|
|
39033
|
+
};
|
|
39034
|
+
const existsAnswer = (relativePath) => {
|
|
39035
|
+
const memoized = existsAnswerByPath.get(relativePath);
|
|
39036
|
+
if (memoized !== void 0) return memoized;
|
|
39037
|
+
let answer;
|
|
39038
|
+
try {
|
|
39039
|
+
const stat = NFS.statSync(Path.resolve(input.rootDirectory, relativePath));
|
|
39040
|
+
answer = stat.isFile() ? "file" : stat.isDirectory() ? "dir" : "none";
|
|
39041
|
+
} catch {
|
|
39042
|
+
answer = "none";
|
|
39043
|
+
}
|
|
39044
|
+
existsAnswerByPath.set(relativePath, answer);
|
|
39045
|
+
return answer;
|
|
39046
|
+
};
|
|
39047
|
+
return {
|
|
39048
|
+
answerFor: (kind, relativePath) => kind === "content" ? contentAnswer(relativePath) : existsAnswer(relativePath),
|
|
39049
|
+
toRelativePath
|
|
39050
|
+
};
|
|
39051
|
+
};
|
|
38769
39052
|
const validateDiagnostic = decodeUnknownSync(Diagnostic);
|
|
38770
|
-
const
|
|
38771
|
-
|
|
39053
|
+
const isProbeKind = (value) => value === "content" || value === "exists";
|
|
39054
|
+
const decodeSidecarFileEntry = (raw) => {
|
|
39055
|
+
if (!isRecord(raw) || !Array.isArray(raw.probes) || !Array.isArray(raw.diagnostics)) return null;
|
|
39056
|
+
const probes = [];
|
|
39057
|
+
for (const rawProbe of raw.probes) {
|
|
39058
|
+
if (!Array.isArray(rawProbe) || rawProbe.length !== 3) return null;
|
|
39059
|
+
const [kind, probePath, answer] = rawProbe;
|
|
39060
|
+
if (!isProbeKind(kind) || typeof probePath !== "string" || typeof answer !== "string") return null;
|
|
39061
|
+
probes.push({
|
|
39062
|
+
kind,
|
|
39063
|
+
path: probePath,
|
|
39064
|
+
answer
|
|
39065
|
+
});
|
|
39066
|
+
}
|
|
38772
39067
|
try {
|
|
38773
|
-
for (const entry of raw) validateDiagnostic(entry);
|
|
38774
|
-
return raw;
|
|
39068
|
+
for (const entry of raw.diagnostics) validateDiagnostic(entry);
|
|
38775
39069
|
} catch {
|
|
38776
39070
|
return null;
|
|
38777
39071
|
}
|
|
39072
|
+
return {
|
|
39073
|
+
probes,
|
|
39074
|
+
diagnostics: raw.diagnostics
|
|
39075
|
+
};
|
|
38778
39076
|
};
|
|
39077
|
+
const encodeSidecarFileEntry = (entry) => ({
|
|
39078
|
+
probes: entry.probes.map((probe) => [
|
|
39079
|
+
probe.kind,
|
|
39080
|
+
probe.path,
|
|
39081
|
+
probe.answer
|
|
39082
|
+
]),
|
|
39083
|
+
diagnostics: entry.diagnostics
|
|
39084
|
+
});
|
|
38779
39085
|
const emptyCache = () => ({
|
|
38780
39086
|
version: 1,
|
|
38781
39087
|
rulesets: {}
|
|
@@ -38786,20 +39092,20 @@ const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
|
|
|
38786
39092
|
if (persisted.version !== 1 || !isRecord(persisted.rulesets)) return entries;
|
|
38787
39093
|
const bucket = persisted.rulesets[rulesetHash];
|
|
38788
39094
|
if (!isRecord(bucket) || !isRecord(bucket.files)) return entries;
|
|
38789
|
-
for (const [fileKey,
|
|
38790
|
-
const decoded =
|
|
39095
|
+
for (const [fileKey, rawEntry] of Object.entries(bucket.files)) {
|
|
39096
|
+
const decoded = decodeSidecarFileEntry(rawEntry);
|
|
38791
39097
|
if (decoded !== null) entries.set(fileKey, decoded);
|
|
38792
39098
|
}
|
|
38793
39099
|
return entries;
|
|
38794
39100
|
};
|
|
38795
|
-
const
|
|
38796
|
-
const cacheFilePath = Path.join(cacheDirectory,
|
|
39101
|
+
const createSidecarLintCache = (cacheDirectory, rulesetHash) => {
|
|
39102
|
+
const cacheFilePath = Path.join(cacheDirectory, SIDECAR_LINT_CACHE_FILENAME);
|
|
38797
39103
|
const entries = loadRulesetEntries(cacheFilePath, rulesetHash);
|
|
38798
39104
|
return {
|
|
38799
39105
|
lookup: (fileKey) => entries.get(fileKey) ?? null,
|
|
38800
|
-
store: (fileKey,
|
|
39106
|
+
store: (fileKey, entry) => {
|
|
38801
39107
|
entries.delete(fileKey);
|
|
38802
|
-
entries.set(fileKey,
|
|
39108
|
+
entries.set(fileKey, entry);
|
|
38803
39109
|
},
|
|
38804
39110
|
persist: () => {
|
|
38805
39111
|
const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
|
|
@@ -38807,7 +39113,7 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
|
|
|
38807
39113
|
const existingBucket = rulesets[rulesetHash];
|
|
38808
39114
|
const existingFiles = isRecord(existingBucket) && isRecord(existingBucket.files) ? existingBucket.files : {};
|
|
38809
39115
|
const ourFiles = {};
|
|
38810
|
-
for (const [fileKey,
|
|
39116
|
+
for (const [fileKey, entry] of entries) ourFiles[fileKey] = encodeSidecarFileEntry(entry);
|
|
38811
39117
|
const cappedEntries = Object.entries({
|
|
38812
39118
|
...existingFiles,
|
|
38813
39119
|
...ourFiles
|
|
@@ -39822,13 +40128,6 @@ const validateRuleRegistration = () => {
|
|
|
39822
40128
|
].filter((entry) => entry !== null).join("; ");
|
|
39823
40129
|
console.warn(`[react-doctor] rule-registration drift: ${detail}`);
|
|
39824
40130
|
};
|
|
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
40131
|
/**
|
|
39833
40132
|
* Balanced LPT (longest-processing-time-first) lint batch planner. Where
|
|
39834
40133
|
* `batchIncludePaths` greedily fills 100-file batches and leaves a small
|
|
@@ -39893,21 +40192,6 @@ const planLintBatches = ({ baseArgs, files, sizeByFile }) => {
|
|
|
39893
40192
|
}
|
|
39894
40193
|
return batches.filter((batch) => batch.fileIndexes.length > 0).map((batch) => batch.fileIndexes.sort((leftIndex, rightIndex) => leftIndex - rightIndex).map((fileIndex) => files[fileIndex]));
|
|
39895
40194
|
};
|
|
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
40195
|
/**
|
|
39912
40196
|
* Atomically (re)writes the generated oxlintrc.json. Used twice in
|
|
39913
40197
|
* the runner: once for the primary scan, once for the
|
|
@@ -39925,6 +40209,106 @@ const writeOxlintConfig = (configPath, configToWrite) => {
|
|
|
39925
40209
|
NFS.closeSync(fileHandle);
|
|
39926
40210
|
}
|
|
39927
40211
|
};
|
|
40212
|
+
/**
|
|
40213
|
+
* Attributes diagnostics back to the file that produced them by the
|
|
40214
|
+
* normalized path oxlint echoes. Returns `null` when ANY diagnostic can't be
|
|
40215
|
+
* attributed — the path forms don't align, so the caller must skip its cache
|
|
40216
|
+
* store rather than risk caching a wrong empty result for a file that
|
|
40217
|
+
* actually had diagnostics.
|
|
40218
|
+
*/
|
|
40219
|
+
const attributeDiagnosticsToFiles = (diagnostics, files) => {
|
|
40220
|
+
const fileByNormalizedPath = /* @__PURE__ */ new Map();
|
|
40221
|
+
for (const file of files) fileByNormalizedPath.set(file.replaceAll("\\", "/"), file);
|
|
40222
|
+
const diagnosticsByFile = /* @__PURE__ */ new Map();
|
|
40223
|
+
for (const diagnostic of diagnostics) {
|
|
40224
|
+
const file = fileByNormalizedPath.get(diagnostic.filePath);
|
|
40225
|
+
if (file === void 0) return null;
|
|
40226
|
+
const fileDiagnostics = diagnosticsByFile.get(file) ?? [];
|
|
40227
|
+
fileDiagnostics.push(diagnostic);
|
|
40228
|
+
diagnosticsByFile.set(file, fileDiagnostics);
|
|
40229
|
+
}
|
|
40230
|
+
return diagnosticsByFile;
|
|
40231
|
+
};
|
|
40232
|
+
/**
|
|
40233
|
+
* Collects each file's cross-file dependency probes (via the plugin's
|
|
40234
|
+
* collectors) with their current answers. Fail-open per file: an unreadable /
|
|
40235
|
+
* unparseable / collector-failing file maps to `null` (no entry stored — it
|
|
40236
|
+
* re-lints next scan). The filesystem is frozen for the scan, so this can run
|
|
40237
|
+
* CONCURRENTLY with an oxlint child-process await — the caller starts it
|
|
40238
|
+
* unawaited before a lint pass and awaits it at store time, hiding the
|
|
40239
|
+
* in-process parse + resolution cost under the subprocess wait. The loop
|
|
40240
|
+
* yields on the cooperative time budget so the child's stdout handling (and
|
|
40241
|
+
* sibling fibers) keep flowing; each per-file collection is synchronous, so
|
|
40242
|
+
* the module-level probe recorder never sees interleaved use. Never rejects.
|
|
40243
|
+
*/
|
|
40244
|
+
const collectSidecarProbesForFiles = async (input) => {
|
|
40245
|
+
const buildProbes = (file) => {
|
|
40246
|
+
const absoluteFilePath = Path.resolve(input.rootDirectory, file);
|
|
40247
|
+
let trace;
|
|
40248
|
+
try {
|
|
40249
|
+
trace = collectCrossFileDependencyProbes({
|
|
40250
|
+
absoluteFilePath,
|
|
40251
|
+
sourceText: NFS.readFileSync(absoluteFilePath, "utf8"),
|
|
40252
|
+
ruleIds: input.boundedSidecarRuleIds
|
|
40253
|
+
});
|
|
40254
|
+
} catch {
|
|
40255
|
+
return null;
|
|
40256
|
+
}
|
|
40257
|
+
if (trace === null) return null;
|
|
40258
|
+
const probes = [];
|
|
40259
|
+
for (const contentPath of trace.contentPaths) {
|
|
40260
|
+
const relativePath = input.probeAnswers.toRelativePath(contentPath);
|
|
40261
|
+
probes.push({
|
|
40262
|
+
kind: "content",
|
|
40263
|
+
path: relativePath,
|
|
40264
|
+
answer: input.probeAnswers.answerFor("content", relativePath)
|
|
40265
|
+
});
|
|
40266
|
+
}
|
|
40267
|
+
for (const existencePath of trace.existencePaths) {
|
|
40268
|
+
const relativePath = input.probeAnswers.toRelativePath(existencePath);
|
|
40269
|
+
probes.push({
|
|
40270
|
+
kind: "exists",
|
|
40271
|
+
path: relativePath,
|
|
40272
|
+
answer: input.probeAnswers.answerFor("exists", relativePath)
|
|
40273
|
+
});
|
|
40274
|
+
}
|
|
40275
|
+
return probes;
|
|
40276
|
+
};
|
|
40277
|
+
const probesByFile = /* @__PURE__ */ new Map();
|
|
40278
|
+
let collectSliceStartedAt = performance.now();
|
|
40279
|
+
for (const file of input.files) {
|
|
40280
|
+
probesByFile.set(file, buildProbes(file));
|
|
40281
|
+
if (performance.now() - collectSliceStartedAt >= 12) {
|
|
40282
|
+
await yieldToEventLoop();
|
|
40283
|
+
collectSliceStartedAt = performance.now();
|
|
40284
|
+
}
|
|
40285
|
+
}
|
|
40286
|
+
return probesByFile;
|
|
40287
|
+
};
|
|
40288
|
+
/**
|
|
40289
|
+
* Stores each freshly-linted file's probe set + cross-file diagnostics so
|
|
40290
|
+
* the next scan can replay instead of re-linting.
|
|
40291
|
+
*/
|
|
40292
|
+
const storeSidecarEntries = (input) => {
|
|
40293
|
+
let didStoreAnyEntry = false;
|
|
40294
|
+
for (const pass of input.storablePasses) {
|
|
40295
|
+
if (!pass.isTrusted) continue;
|
|
40296
|
+
const diagnosticsByFile = attributeDiagnosticsToFiles(pass.diagnostics, pass.files);
|
|
40297
|
+
if (diagnosticsByFile === null) continue;
|
|
40298
|
+
for (const file of pass.files) {
|
|
40299
|
+
const cacheKey = input.cacheKeyByFile.get(file);
|
|
40300
|
+
if (cacheKey === void 0) continue;
|
|
40301
|
+
const probes = pass.probesByFile.get(file);
|
|
40302
|
+
if (probes === void 0 || probes === null) continue;
|
|
40303
|
+
input.sidecarCache.store(cacheKey, {
|
|
40304
|
+
probes,
|
|
40305
|
+
diagnostics: diagnosticsByFile.get(file) ?? []
|
|
40306
|
+
});
|
|
40307
|
+
didStoreAnyEntry = true;
|
|
40308
|
+
}
|
|
40309
|
+
}
|
|
40310
|
+
if (didStoreAnyEntry) input.sidecarCache.persist();
|
|
40311
|
+
};
|
|
39928
40312
|
const REACT_HOOKS_JS_DROP_PREFIX = "React Compiler rules (react-hooks-js/*) skipped — eslint-plugin-react-hooks failed to load in this environment";
|
|
39929
40313
|
/**
|
|
39930
40314
|
* Detects an oxlint config-load crash caused by the optional
|
|
@@ -39966,7 +40350,7 @@ const reactHooksJsPluginDropNote = (error) => {
|
|
|
39966
40350
|
* 6. always restore disable directives + clean up the temp dir
|
|
39967
40351
|
*/
|
|
39968
40352
|
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;
|
|
40353
|
+
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
40354
|
const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
|
|
39971
40355
|
const severityControls = buildRuleSeverityControls(userConfig);
|
|
39972
40356
|
validateRuleRegistration();
|
|
@@ -39984,7 +40368,8 @@ const runOxlint = async (options) => {
|
|
|
39984
40368
|
severityControls,
|
|
39985
40369
|
userPlugins,
|
|
39986
40370
|
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin,
|
|
39987
|
-
ruleSelection: overrides.ruleSelection
|
|
40371
|
+
ruleSelection: overrides.ruleSelection,
|
|
40372
|
+
sidecarRuleIdFilter: overrides.sidecarRuleIdFilter
|
|
39988
40373
|
});
|
|
39989
40374
|
const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
|
|
39990
40375
|
const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
|
|
@@ -40087,15 +40472,18 @@ const runOxlint = async (options) => {
|
|
|
40087
40472
|
});
|
|
40088
40473
|
const cache = createFileLintCache(resolveReactDoctorCacheDir(rootDirectory), rulesetHash);
|
|
40089
40474
|
const cacheKeyByFile = /* @__PURE__ */ new Map();
|
|
40475
|
+
const contentHashByRelativePath = /* @__PURE__ */ new Map();
|
|
40090
40476
|
const missFiles = [];
|
|
40091
40477
|
const replayedDiagnostics = [];
|
|
40092
40478
|
let hashSliceStartedAt = performance.now();
|
|
40093
40479
|
for (const candidateFile of candidateFiles) {
|
|
40094
|
-
const
|
|
40480
|
+
const absoluteCandidatePath = Path.resolve(rootDirectory, candidateFile);
|
|
40481
|
+
const contentHash = hashFileContents(absoluteCandidatePath);
|
|
40095
40482
|
if (contentHash === null) {
|
|
40096
40483
|
missFiles.push(candidateFile);
|
|
40097
40484
|
continue;
|
|
40098
40485
|
}
|
|
40486
|
+
contentHashByRelativePath.set(Path.relative(rootDirectory, absoluteCandidatePath).replaceAll("\\", "/"), contentHash);
|
|
40099
40487
|
const cacheKey = `${candidateFile.replaceAll("\\", "/")}\u0000${contentHash}`;
|
|
40100
40488
|
cacheKeyByFile.set(candidateFile, cacheKey);
|
|
40101
40489
|
const cachedDiagnostics = cache.lookup(cacheKey);
|
|
@@ -40107,43 +40495,111 @@ const runOxlint = async (options) => {
|
|
|
40107
40495
|
}
|
|
40108
40496
|
}
|
|
40109
40497
|
const cacheHitFileCount = candidateFiles.length - missFiles.length;
|
|
40498
|
+
const enabledSidecarRuleIds = Object.keys(buildConfig({
|
|
40499
|
+
extendsPaths: [],
|
|
40500
|
+
ruleSelection: "sidecar"
|
|
40501
|
+
}).rules).map((ruleKey) => ruleKey.slice(ruleKey.lastIndexOf("/") + 1)).filter((ruleId) => CROSS_FILE_RULE_IDS.has(ruleId));
|
|
40502
|
+
const boundedSidecarRuleIds = enabledSidecarRuleIds.filter((ruleId) => CROSS_FILE_DEPENDENCY_COLLECTORS.has(ruleId));
|
|
40503
|
+
const boundedSidecarRuleIdSet = new Set(boundedSidecarRuleIds);
|
|
40504
|
+
const unboundedSidecarRuleIds = enabledSidecarRuleIds.filter((ruleId) => !CROSS_FILE_DEPENDENCY_COLLECTORS.has(ruleId));
|
|
40505
|
+
const useSidecarCache = sidecarLintCacheEnabled && boundedSidecarRuleIds.length > 0;
|
|
40506
|
+
const sidecarCache = useSidecarCache ? createSidecarLintCache(resolveReactDoctorCacheDir(rootDirectory), computeRulesetHash({
|
|
40507
|
+
config: buildConfig({
|
|
40508
|
+
extendsPaths: [],
|
|
40509
|
+
ruleSelection: "sidecar",
|
|
40510
|
+
sidecarRuleIdFilter: boundedSidecarRuleIdSet
|
|
40511
|
+
}),
|
|
40512
|
+
toolchainVersions: resolveOxlintToolchainVersions(nodeBinaryPath),
|
|
40513
|
+
ignorePatterns: combinedPatterns,
|
|
40514
|
+
tsconfigContent
|
|
40515
|
+
})) : null;
|
|
40516
|
+
const probeAnswers = createSidecarProbeAnswerResolver({
|
|
40517
|
+
rootDirectory,
|
|
40518
|
+
contentHashByRelativePath
|
|
40519
|
+
});
|
|
40520
|
+
const missProbesTask = sidecarCache === null ? null : collectSidecarProbesForFiles({
|
|
40521
|
+
files: missFiles,
|
|
40522
|
+
rootDirectory,
|
|
40523
|
+
boundedSidecarRuleIds,
|
|
40524
|
+
probeAnswers
|
|
40525
|
+
});
|
|
40110
40526
|
const fullResult = await runConfigOverFiles((overrides) => buildConfig({
|
|
40111
40527
|
extendsPaths: [],
|
|
40112
40528
|
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
|
|
40113
40529
|
}), "oxlintrc.full.json", missFiles, options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(scannedFileCount, candidateFiles.length)));
|
|
40114
40530
|
const missFileSet = new Set(missFiles);
|
|
40115
|
-
const
|
|
40531
|
+
const hitFiles = candidateFiles.filter((candidateFile) => !missFileSet.has(candidateFile));
|
|
40532
|
+
const sidecarReplayedDiagnostics = [];
|
|
40533
|
+
const sidecarLintFiles = [];
|
|
40534
|
+
if (sidecarCache === null) sidecarLintFiles.push(...hitFiles);
|
|
40535
|
+
else {
|
|
40536
|
+
let verifySliceStartedAt = performance.now();
|
|
40537
|
+
for (const hitFile of hitFiles) {
|
|
40538
|
+
const cacheKey = cacheKeyByFile.get(hitFile);
|
|
40539
|
+
const entry = cacheKey === void 0 ? null : sidecarCache.lookup(cacheKey);
|
|
40540
|
+
if (entry !== null && entry.probes.every((probe) => probeAnswers.answerFor(probe.kind, probe.path) === probe.answer)) sidecarReplayedDiagnostics.push(...entry.diagnostics);
|
|
40541
|
+
else sidecarLintFiles.push(hitFile);
|
|
40542
|
+
if (performance.now() - verifySliceStartedAt >= 12) {
|
|
40543
|
+
await yieldToEventLoop();
|
|
40544
|
+
verifySliceStartedAt = performance.now();
|
|
40545
|
+
}
|
|
40546
|
+
}
|
|
40547
|
+
}
|
|
40548
|
+
const sidecarProbesTask = sidecarCache === null ? null : collectSidecarProbesForFiles({
|
|
40549
|
+
files: sidecarLintFiles,
|
|
40550
|
+
rootDirectory,
|
|
40551
|
+
boundedSidecarRuleIds,
|
|
40552
|
+
probeAnswers
|
|
40553
|
+
});
|
|
40554
|
+
const sidecarReplayedFileCount = hitFiles.length - sidecarLintFiles.length;
|
|
40555
|
+
if (sidecarReplayedFileCount > 0) options.onFileProgress?.(missFiles.length + sidecarReplayedFileCount, candidateFiles.length);
|
|
40556
|
+
const boundedSidecarResult = await runConfigOverFiles(() => buildConfig({
|
|
40116
40557
|
extendsPaths: [],
|
|
40117
|
-
ruleSelection: "sidecar"
|
|
40118
|
-
|
|
40558
|
+
ruleSelection: "sidecar",
|
|
40559
|
+
...useSidecarCache ? { sidecarRuleIdFilter: boundedSidecarRuleIdSet } : {}
|
|
40560
|
+
}), "oxlintrc.sidecar.json", sidecarLintFiles, options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(missFiles.length + sidecarReplayedFileCount + scannedFileCount, candidateFiles.length)));
|
|
40561
|
+
const unboundedSidecarResult = useSidecarCache && unboundedSidecarRuleIds.length > 0 ? await runConfigOverFiles(() => buildConfig({
|
|
40562
|
+
extendsPaths: [],
|
|
40563
|
+
ruleSelection: "sidecar",
|
|
40564
|
+
sidecarRuleIdFilter: new Set(unboundedSidecarRuleIds)
|
|
40565
|
+
}), "oxlintrc.sidecar-unbounded.json", hitFiles, void 0) : {
|
|
40566
|
+
diagnostics: [],
|
|
40567
|
+
didDropReactHooksJsPlugin: false,
|
|
40568
|
+
hadPartialFailure: false
|
|
40569
|
+
};
|
|
40119
40570
|
const freshCacheableDiagnostics = [];
|
|
40120
40571
|
for (const diagnostic of fullResult.diagnostics) if (!CROSS_FILE_RULE_IDS.has(diagnostic.rule)) freshCacheableDiagnostics.push(diagnostic);
|
|
40121
40572
|
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) {
|
|
40573
|
+
if (sidecarCache !== null) onSidecarStats?.(hitFiles.length - sidecarLintFiles.length, hitFiles.length);
|
|
40574
|
+
const freshDiagnosticsByFile = attributeDiagnosticsToFiles(freshCacheableDiagnostics, missFiles);
|
|
40575
|
+
if (!fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure && freshDiagnosticsByFile !== null) {
|
|
40137
40576
|
for (const missFile of missFiles) {
|
|
40138
40577
|
const cacheKey = cacheKeyByFile.get(missFile);
|
|
40139
40578
|
if (cacheKey !== void 0) cache.store(cacheKey, freshDiagnosticsByFile.get(missFile) ?? []);
|
|
40140
40579
|
}
|
|
40141
40580
|
cache.persist();
|
|
40142
40581
|
}
|
|
40582
|
+
if (sidecarCache !== null && missProbesTask !== null && sidecarProbesTask !== null) storeSidecarEntries({
|
|
40583
|
+
sidecarCache,
|
|
40584
|
+
cacheKeyByFile,
|
|
40585
|
+
storablePasses: [{
|
|
40586
|
+
files: sidecarLintFiles,
|
|
40587
|
+
diagnostics: boundedSidecarResult.diagnostics,
|
|
40588
|
+
isTrusted: !boundedSidecarResult.hadPartialFailure,
|
|
40589
|
+
probesByFile: await sidecarProbesTask
|
|
40590
|
+
}, {
|
|
40591
|
+
files: missFiles,
|
|
40592
|
+
diagnostics: fullResult.diagnostics.filter((diagnostic) => boundedSidecarRuleIdSet.has(diagnostic.rule)),
|
|
40593
|
+
isTrusted: !fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure,
|
|
40594
|
+
probesByFile: await missProbesTask
|
|
40595
|
+
}]
|
|
40596
|
+
});
|
|
40143
40597
|
return dedupeDiagnostics([
|
|
40144
40598
|
...replayedDiagnostics,
|
|
40145
40599
|
...fullResult.diagnostics,
|
|
40146
|
-
...
|
|
40600
|
+
...sidecarReplayedDiagnostics,
|
|
40601
|
+
...boundedSidecarResult.diagnostics,
|
|
40602
|
+
...unboundedSidecarResult.diagnostics
|
|
40147
40603
|
]);
|
|
40148
40604
|
}
|
|
40149
40605
|
const baseArgs = makeBaseArgs(configPath);
|
|
@@ -40241,6 +40697,7 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
|
|
|
40241
40697
|
const concurrency = yield* OxlintConcurrency;
|
|
40242
40698
|
const lintBatchOrdering = yield* LintBatchOrdering;
|
|
40243
40699
|
const perFileLintCacheEnabled = yield* PerFileLintCacheEnabled;
|
|
40700
|
+
const sidecarLintCacheEnabled = yield* SidecarLintCacheEnabled;
|
|
40244
40701
|
const collectedFailures = [];
|
|
40245
40702
|
const diagnostics = yield* tryPromise({
|
|
40246
40703
|
try: (signal) => runOxlint({
|
|
@@ -40259,7 +40716,9 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
|
|
|
40259
40716
|
},
|
|
40260
40717
|
onFileProgress: input.onFileProgress,
|
|
40261
40718
|
perFileLintCacheEnabled,
|
|
40719
|
+
sidecarLintCacheEnabled,
|
|
40262
40720
|
onCacheStats: input.onCacheStats,
|
|
40721
|
+
onSidecarStats: input.onSidecarStats,
|
|
40263
40722
|
spawnTimeoutMs,
|
|
40264
40723
|
outputMaxBytes,
|
|
40265
40724
|
concurrency,
|
|
@@ -41034,7 +41493,14 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
41034
41493
|
const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
|
|
41035
41494
|
rootDirectory: scanDirectory,
|
|
41036
41495
|
parseConcurrency: deadCodeParseConcurrency,
|
|
41037
|
-
workerTimeoutMs: deadCodeTimeout.workerTimeoutMs
|
|
41496
|
+
workerTimeoutMs: deadCodeTimeout.workerTimeoutMs,
|
|
41497
|
+
onCacheOutcome: (didHitCache) => {
|
|
41498
|
+
deadCodeCacheHit = didHitCache;
|
|
41499
|
+
},
|
|
41500
|
+
onSummaryCacheStats: (stats) => {
|
|
41501
|
+
deadCodeSummaryCacheHits = stats.hits;
|
|
41502
|
+
deadCodeSummaryCacheMisses = stats.misses;
|
|
41503
|
+
}
|
|
41038
41504
|
}).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
|
|
41039
41505
|
yield* set(deadCodeFailure, {
|
|
41040
41506
|
didFail: true,
|
|
@@ -41062,6 +41528,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
41062
41528
|
let lastReportedTotalFileCount = 0;
|
|
41063
41529
|
let lintCacheHitFileCount = null;
|
|
41064
41530
|
let lintCacheTotalFileCount = null;
|
|
41531
|
+
let lintSidecarReplayedFileCount = null;
|
|
41532
|
+
let lintSidecarTotalFileCount = null;
|
|
41533
|
+
let deadCodeCacheHit = null;
|
|
41534
|
+
let deadCodeSummaryCacheHits = null;
|
|
41535
|
+
let deadCodeSummaryCacheMisses = null;
|
|
41065
41536
|
const baseLintStream = linterService.run({
|
|
41066
41537
|
rootDirectory: scanDirectory,
|
|
41067
41538
|
project,
|
|
@@ -41081,6 +41552,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
41081
41552
|
lintCacheHitFileCount = cacheHitFileCount;
|
|
41082
41553
|
lintCacheTotalFileCount = totalConsideredFileCount;
|
|
41083
41554
|
},
|
|
41555
|
+
onSidecarStats: (sidecarReplayedFileCount, sidecarConsideredFileCount) => {
|
|
41556
|
+
lintSidecarReplayedFileCount = sidecarReplayedFileCount;
|
|
41557
|
+
lintSidecarTotalFileCount = sidecarConsideredFileCount;
|
|
41558
|
+
},
|
|
41084
41559
|
deadlineEpochMs: input.deadlineEpochMs
|
|
41085
41560
|
}).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
|
|
41086
41561
|
yield* set(lintFailure, {
|
|
@@ -41191,6 +41666,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
41191
41666
|
securityScanFailed,
|
|
41192
41667
|
lintCacheHitFileCount,
|
|
41193
41668
|
lintCacheTotalFileCount,
|
|
41669
|
+
lintSidecarReplayedFileCount,
|
|
41670
|
+
lintSidecarTotalFileCount,
|
|
41671
|
+
deadCodeCacheHit: lintFailureState.didFail ? null : deadCodeCacheHit,
|
|
41672
|
+
deadCodeSummaryCacheHits: lintFailureState.didFail ? null : deadCodeSummaryCacheHits,
|
|
41673
|
+
deadCodeSummaryCacheMisses: lintFailureState.didFail ? null : deadCodeSummaryCacheMisses,
|
|
41194
41674
|
suppressedRuleCounts: transform.summarizeSuppressions()
|
|
41195
41675
|
};
|
|
41196
41676
|
}).pipe(withSpan("runInspect", { attributes: {
|
|
@@ -43570,5 +44050,5 @@ const startLanguageServer = () => {
|
|
|
43570
44050
|
};
|
|
43571
44051
|
//#endregion
|
|
43572
44052
|
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=
|
|
44053
|
+
!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]="17a3f533-8248-511a-859b-584197cefba6")}catch(e){}}();
|
|
44054
|
+
//# debugId=17a3f533-8248-511a-859b-584197cefba6
|