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/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
!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]="
|
|
2
|
+
!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]="b55ef8b2-8e9d-512b-81de-a01f6ce3e60e")}catch(e){}}();
|
|
3
3
|
import { r as __toESM$1, t as __commonJSMin$1 } from "./chunk-N93fKeF6.js";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import * as NFS from "node:fs";
|
|
@@ -9,7 +9,7 @@ import path from "node:path";
|
|
|
9
9
|
import * as NodeChildProcess from "node:child_process";
|
|
10
10
|
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
11
11
|
import * as ts from "typescript";
|
|
12
|
-
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";
|
|
12
|
+
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";
|
|
13
13
|
import * as OS from "node:os";
|
|
14
14
|
import os from "node:os";
|
|
15
15
|
import { parseJSON5 } from "confbox";
|
|
@@ -18,6 +18,7 @@ import { fileURLToPath } from "node:url";
|
|
|
18
18
|
import { createJiti } from "jiti";
|
|
19
19
|
import * as Crypto from "node:crypto";
|
|
20
20
|
import crypto, { createHash } from "node:crypto";
|
|
21
|
+
import { ANALYZED_MANIFEST_FILENAMES, DEFAULT_EXTENSIONS } from "deslop-js/analyzed-inputs";
|
|
21
22
|
import { gzipSync } from "node:zlib";
|
|
22
23
|
//#region ../../node_modules/.pnpm/effect@4.0.0-beta.70/node_modules/effect/dist/Pipeable.js
|
|
23
24
|
/**
|
|
@@ -32927,6 +32928,10 @@ const SOCKET_PACKAGE_PAGE_BASE = "https://socket.dev/npm/package";
|
|
|
32927
32928
|
const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
|
|
32928
32929
|
const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
|
|
32929
32930
|
const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
|
|
32931
|
+
const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
|
|
32932
|
+
const CORE_PACKAGE_VERSION = "0.6.3";
|
|
32933
|
+
const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
|
|
32934
|
+
const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
|
|
32930
32935
|
const SUPPLY_CHAIN_PLUGIN = "socket";
|
|
32931
32936
|
const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
|
|
32932
32937
|
const SUPPLY_CHAIN_CATEGORY = "Security";
|
|
@@ -35613,6 +35618,48 @@ var PerFileLintCacheEnabled = class extends Reference("react-doctor/PerFileLintC
|
|
|
35613
35618
|
if (CACHE_DISABLED_VALUES.has(noFileCache)) return false;
|
|
35614
35619
|
return true;
|
|
35615
35620
|
} }) {};
|
|
35621
|
+
/**
|
|
35622
|
+
* Whether the sidecar lint cache (`runners/oxlint/sidecar-lint-cache.ts`) is
|
|
35623
|
+
* active. Defaults ON — warm rescans replay the cross-file rules' cached
|
|
35624
|
+
* diagnostics for every file whose dependency fingerprint (the probe set the
|
|
35625
|
+
* plugin's collectors recorded) still matches the tree, and re-lint only the
|
|
35626
|
+
* rest. Only reachable when the per-file lint cache itself is on (the
|
|
35627
|
+
* sidecar covers its cache hits), so `REACT_DOCTOR_NO_CACHE` /
|
|
35628
|
+
* `REACT_DOCTOR_NO_FILE_CACHE` implicitly disable it too. Opt-OUT knobs:
|
|
35629
|
+
*
|
|
35630
|
+
* - `REACT_DOCTOR_NO_CACHE` — the global off-switch.
|
|
35631
|
+
* - `REACT_DOCTOR_NO_SIDECAR_CACHE` — granular rollback hatch: keep the
|
|
35632
|
+
* per-file cache but run the always-fresh sidecar over every hit
|
|
35633
|
+
* (the pre-cache behavior).
|
|
35634
|
+
*
|
|
35635
|
+
* Tests override via `Layer.succeed(SidecarLintCacheEnabled, false)`.
|
|
35636
|
+
*/
|
|
35637
|
+
var SidecarLintCacheEnabled = class extends Reference("react-doctor/SidecarLintCacheEnabled", { defaultValue: () => {
|
|
35638
|
+
const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
|
|
35639
|
+
const noSidecarCache = process.env["REACT_DOCTOR_NO_SIDECAR_CACHE"]?.toLowerCase() ?? "";
|
|
35640
|
+
if (CACHE_DISABLED_VALUES.has(noCache)) return false;
|
|
35641
|
+
if (CACHE_DISABLED_VALUES.has(noSidecarCache)) return false;
|
|
35642
|
+
return true;
|
|
35643
|
+
} }) {};
|
|
35644
|
+
/**
|
|
35645
|
+
* Whether the whole-project dead-code result cache
|
|
35646
|
+
* (`dead-code/dead-code-result-cache.ts`) is active. Defaults ON — a rescan
|
|
35647
|
+
* whose inputs (source tree, manifests, configs, analyzer version) are
|
|
35648
|
+
* unchanged replays the stored diagnostics instead of re-running the
|
|
35649
|
+
* analysis worker. Opt-OUT, two knobs (matching the per-file lint cache):
|
|
35650
|
+
*
|
|
35651
|
+
* - `REACT_DOCTOR_NO_CACHE` — the global off-switch.
|
|
35652
|
+
* - `REACT_DOCTOR_NO_DEAD_CODE_CACHE` — granular: bust only this cache.
|
|
35653
|
+
*
|
|
35654
|
+
* Tests override via `Layer.succeed(DeadCodeResultCacheEnabled, false)`.
|
|
35655
|
+
*/
|
|
35656
|
+
var DeadCodeResultCacheEnabled = class extends Reference("react-doctor/DeadCodeResultCacheEnabled", { defaultValue: () => {
|
|
35657
|
+
const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
|
|
35658
|
+
const noDeadCodeCache = process.env["REACT_DOCTOR_NO_DEAD_CODE_CACHE"]?.toLowerCase() ?? "";
|
|
35659
|
+
if (CACHE_DISABLED_VALUES.has(noCache)) return false;
|
|
35660
|
+
if (CACHE_DISABLED_VALUES.has(noDeadCodeCache)) return false;
|
|
35661
|
+
return true;
|
|
35662
|
+
} }) {};
|
|
35616
35663
|
const DIAGNOSTIC_SURFACES = [
|
|
35617
35664
|
"cli",
|
|
35618
35665
|
"prComment",
|
|
@@ -37301,6 +37348,80 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
|
|
|
37301
37348
|
return [...seen].filter((pattern) => pattern.length > 0);
|
|
37302
37349
|
};
|
|
37303
37350
|
const collectDeadCodeEntryPatterns = (rootDirectory) => [...new Set(collectKnipPatterns(rootDirectory, "entry"))].filter((pattern) => pattern.length > 0);
|
|
37351
|
+
const atomicWriteJson = (filePath, value) => {
|
|
37352
|
+
try {
|
|
37353
|
+
NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
|
|
37354
|
+
const temporaryPath = `${filePath}.${process.pid}.tmp`;
|
|
37355
|
+
NFS.writeFileSync(temporaryPath, JSON.stringify(value));
|
|
37356
|
+
NFS.renameSync(temporaryPath, filePath);
|
|
37357
|
+
} catch {
|
|
37358
|
+
return;
|
|
37359
|
+
}
|
|
37360
|
+
};
|
|
37361
|
+
const failOpenReadJson = (filePath, fallback) => {
|
|
37362
|
+
try {
|
|
37363
|
+
return JSON.parse(NFS.readFileSync(filePath, "utf8"));
|
|
37364
|
+
} catch {
|
|
37365
|
+
return fallback;
|
|
37366
|
+
}
|
|
37367
|
+
};
|
|
37368
|
+
const ANALYZED_FILE_EXTENSIONS = new Set(DEFAULT_EXTENSIONS);
|
|
37369
|
+
const CORE_SIDE_MANIFEST_NAMES = ["knip.json", "deno.lock"];
|
|
37370
|
+
const ANALYZED_MANIFEST_NAMES = new Set([...ANALYZED_MANIFEST_FILENAMES, ...CORE_SIDE_MANIFEST_NAMES]);
|
|
37371
|
+
const isTsConfigLikeFile = (fileName) => (fileName.startsWith("tsconfig") || fileName.startsWith("jsconfig")) && fileName.endsWith(".json");
|
|
37372
|
+
const isFingerprintedFile = (fileName) => ANALYZED_FILE_EXTENSIONS.has(Path.extname(fileName).toLowerCase()) || ANALYZED_MANIFEST_NAMES.has(fileName) || isTsConfigLikeFile(fileName);
|
|
37373
|
+
const collectAnalyzedFileFingerprints = (rootDirectory) => {
|
|
37374
|
+
const fingerprints = [];
|
|
37375
|
+
for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) {
|
|
37376
|
+
if (!isFingerprintedFile(name)) continue;
|
|
37377
|
+
try {
|
|
37378
|
+
const fileStat = NFS.statSync(absolutePath);
|
|
37379
|
+
const relativePath = Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/");
|
|
37380
|
+
fingerprints.push(`${relativePath}:${fileStat.mtimeMs}:${fileStat.size}`);
|
|
37381
|
+
} catch {}
|
|
37382
|
+
}
|
|
37383
|
+
return fingerprints.sort();
|
|
37384
|
+
};
|
|
37385
|
+
const bundledRequire$2 = createRequire(import.meta.url);
|
|
37386
|
+
const resolveDeslopVersion = () => {
|
|
37387
|
+
try {
|
|
37388
|
+
const packageJson = JSON.parse(NFS.readFileSync(bundledRequire$2.resolve("deslop-js/package.json"), "utf8"));
|
|
37389
|
+
return isRecord(packageJson) && typeof packageJson.version === "string" ? packageJson.version : "unknown";
|
|
37390
|
+
} catch {
|
|
37391
|
+
return "unknown";
|
|
37392
|
+
}
|
|
37393
|
+
};
|
|
37394
|
+
const computeDeadCodeCacheKey = (input) => crypto.createHash("sha1").update(JSON.stringify({
|
|
37395
|
+
schemaVersion: 1,
|
|
37396
|
+
coreVersion: input.coreVersion,
|
|
37397
|
+
deslopVersion: resolveDeslopVersion(),
|
|
37398
|
+
deslopJsModuleSpecifier: input.deslopJsModuleSpecifier,
|
|
37399
|
+
entryPatterns: input.entryPatterns,
|
|
37400
|
+
ignorePatterns: input.ignorePatterns,
|
|
37401
|
+
tsConfigFile: input.tsConfigPath === void 0 ? null : Path.relative(input.rootDirectory, input.tsConfigPath).replace(/\\/g, "/"),
|
|
37402
|
+
files: collectAnalyzedFileFingerprints(input.rootDirectory)
|
|
37403
|
+
})).digest("hex");
|
|
37404
|
+
const validateDiagnostic$2 = decodeUnknownSync(Diagnostic);
|
|
37405
|
+
const decodeCachedDiagnostics = (raw) => {
|
|
37406
|
+
try {
|
|
37407
|
+
for (const entry of raw) validateDiagnostic$2(entry);
|
|
37408
|
+
return raw;
|
|
37409
|
+
} catch {
|
|
37410
|
+
return null;
|
|
37411
|
+
}
|
|
37412
|
+
};
|
|
37413
|
+
const lookupDeadCodeResultCache = (cacheDirectory, cacheKey) => {
|
|
37414
|
+
const persisted = failOpenReadJson(Path.join(cacheDirectory, DEAD_CODE_CACHE_FILENAME), null);
|
|
37415
|
+
if (persisted === null || !isRecord(persisted) || persisted.version !== 1 || persisted.key !== cacheKey || !Array.isArray(persisted.diagnostics)) return null;
|
|
37416
|
+
return decodeCachedDiagnostics(persisted.diagnostics);
|
|
37417
|
+
};
|
|
37418
|
+
const storeDeadCodeResultCache = (cacheDirectory, cacheKey, diagnostics) => {
|
|
37419
|
+
atomicWriteJson(Path.join(cacheDirectory, DEAD_CODE_CACHE_FILENAME), {
|
|
37420
|
+
version: 1,
|
|
37421
|
+
key: cacheKey,
|
|
37422
|
+
diagnostics
|
|
37423
|
+
});
|
|
37424
|
+
};
|
|
37304
37425
|
const readSystemFacts = () => ({
|
|
37305
37426
|
availableCores: os.availableParallelism(),
|
|
37306
37427
|
totalMemoryBytes: os.totalmem(),
|
|
@@ -37369,6 +37490,21 @@ const withDeadCodeWorkerSlot = async (task, abortSignal) => {
|
|
|
37369
37490
|
releaseSlot();
|
|
37370
37491
|
}
|
|
37371
37492
|
};
|
|
37493
|
+
const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
|
|
37494
|
+
const resolveReactDoctorCacheDir = (projectDirectory) => {
|
|
37495
|
+
const cacheDirOverride = process.env["REACT_DOCTOR_CACHE_DIR"]?.trim();
|
|
37496
|
+
if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
|
|
37497
|
+
const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
|
|
37498
|
+
if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
|
|
37499
|
+
return Path.join(os.tmpdir(), `react-doctor-cache-${resolveUserCacheScope()}`, projectCacheSubdir(projectDirectory));
|
|
37500
|
+
};
|
|
37501
|
+
const resolveUserCacheScope = () => {
|
|
37502
|
+
try {
|
|
37503
|
+
return String(process.getuid?.() ?? os.userInfo().username);
|
|
37504
|
+
} catch {
|
|
37505
|
+
return "shared";
|
|
37506
|
+
}
|
|
37507
|
+
};
|
|
37372
37508
|
/**
|
|
37373
37509
|
* Resolves a path to its canonical, symlink-free form, falling back to
|
|
37374
37510
|
* the input when it cannot be realpath'd (broken symlink, permission
|
|
@@ -37422,6 +37558,14 @@ process.stdin.on("end", () => {
|
|
|
37422
37558
|
circularDependencies: result.circularDependencies.map((cycle) => ({
|
|
37423
37559
|
files: cycle.files,
|
|
37424
37560
|
})),
|
|
37561
|
+
...(result.incrementalCacheStats
|
|
37562
|
+
? {
|
|
37563
|
+
summaryCacheStats: {
|
|
37564
|
+
hits: result.incrementalCacheStats.summaryHits,
|
|
37565
|
+
misses: result.incrementalCacheStats.summaryMisses,
|
|
37566
|
+
},
|
|
37567
|
+
}
|
|
37568
|
+
: {}),
|
|
37425
37569
|
});
|
|
37426
37570
|
|
|
37427
37571
|
const serializeError = (error) =>
|
|
@@ -37445,6 +37589,9 @@ process.stdin.on("end", () => {
|
|
|
37445
37589
|
...(workerInput.ignorePatterns.length > 0
|
|
37446
37590
|
? { ignorePatterns: workerInput.ignorePatterns }
|
|
37447
37591
|
: {}),
|
|
37592
|
+
...(workerInput.incrementalCachePath
|
|
37593
|
+
? { incrementalCachePath: workerInput.incrementalCachePath }
|
|
37594
|
+
: {}),
|
|
37448
37595
|
// We consume only deslop's GRAPH-based findings (unusedFiles, unusedExports,
|
|
37449
37596
|
// unusedDependencies, circularDependencies). Everything else deslop can compute
|
|
37450
37597
|
// is pure wasted work for us, and it's the bulk of the runtime:
|
|
@@ -37455,12 +37602,18 @@ process.stdin.on("end", () => {
|
|
|
37455
37602
|
// are the single most expensive pass — duplicate-block detection alone was
|
|
37456
37603
|
// ~83s of a ~130s Sentry scan — so skipping them is an ~8.5x dead-code
|
|
37457
37604
|
// speedup on a large repo.
|
|
37458
|
-
//
|
|
37605
|
+
// - reportRedundancy: the DRY-pattern detectors (duplicate types/constants,
|
|
37606
|
+
// simplifiable functions, identity wrappers, …) — ~120 ms of discarded
|
|
37607
|
+
// output per scan, and skipping them lets the incremental cache drop the
|
|
37608
|
+
// DRY-pattern summary fields (the largest slice of the cache file).
|
|
37609
|
+
// All are provably safe: the consumed graph findings are computed by their own
|
|
37459
37610
|
// detectors, independent of these passes (confirmed byte-identical on
|
|
37460
|
-
// excalidraw + mui-material + sentry
|
|
37461
|
-
//
|
|
37611
|
+
// excalidraw + mui-material + sentry; re-verified on sentry after the
|
|
37612
|
+
// reportRedundancy flip). tsConfigPath stays — the module resolver needs it
|
|
37613
|
+
// for path-alias resolution in the import graph.
|
|
37462
37614
|
semantic: { enabled: false },
|
|
37463
37615
|
reportCodeQuality: false,
|
|
37616
|
+
reportRedundancy: false,
|
|
37464
37617
|
};
|
|
37465
37618
|
const result = await analyze(defineConfig(config));
|
|
37466
37619
|
emit({ ok: true, result: normalizeResult(result) });
|
|
@@ -37544,13 +37697,23 @@ const parseCircularDependencies = (value) => {
|
|
|
37544
37697
|
}
|
|
37545
37698
|
return circularDependencies;
|
|
37546
37699
|
};
|
|
37700
|
+
const parseSummaryCacheStats = (value) => {
|
|
37701
|
+
if (!isRecord(value)) return void 0;
|
|
37702
|
+
if (typeof value.hits !== "number" || typeof value.misses !== "number") return void 0;
|
|
37703
|
+
return {
|
|
37704
|
+
hits: value.hits,
|
|
37705
|
+
misses: value.misses
|
|
37706
|
+
};
|
|
37707
|
+
};
|
|
37547
37708
|
const parseDeadCodeWorkerResult = (value) => {
|
|
37548
37709
|
if (!isRecord(value)) throw new Error("Dead-code worker returned an invalid result.");
|
|
37710
|
+
const summaryCacheStats = parseSummaryCacheStats(value.summaryCacheStats);
|
|
37549
37711
|
return {
|
|
37550
37712
|
unusedFiles: parseUnusedFiles(value.unusedFiles),
|
|
37551
37713
|
unusedExports: parseUnusedExports(value.unusedExports),
|
|
37552
37714
|
unusedDependencies: parseUnusedDependencies(value.unusedDependencies),
|
|
37553
|
-
circularDependencies: parseCircularDependencies(value.circularDependencies)
|
|
37715
|
+
circularDependencies: parseCircularDependencies(value.circularDependencies),
|
|
37716
|
+
...summaryCacheStats ? { summaryCacheStats } : {}
|
|
37554
37717
|
};
|
|
37555
37718
|
};
|
|
37556
37719
|
const parseDeadCodeWorkerError = (value) => {
|
|
@@ -37664,17 +37827,35 @@ const checkDeadCode = async (options) => {
|
|
|
37664
37827
|
if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
|
|
37665
37828
|
const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
|
|
37666
37829
|
const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
|
|
37830
|
+
const tsConfigPath = resolveTsConfigPath(rootDirectory);
|
|
37831
|
+
const deslopJsModuleSpecifier = options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js");
|
|
37832
|
+
const cacheKey = options.cacheEnabled ? computeDeadCodeCacheKey({
|
|
37833
|
+
rootDirectory,
|
|
37834
|
+
entryPatterns,
|
|
37835
|
+
ignorePatterns,
|
|
37836
|
+
tsConfigPath,
|
|
37837
|
+
deslopJsModuleSpecifier,
|
|
37838
|
+
coreVersion: CORE_PACKAGE_VERSION
|
|
37839
|
+
}) : null;
|
|
37840
|
+
if (cacheKey !== null) {
|
|
37841
|
+
const cachedDiagnostics = lookupDeadCodeResultCache(resolveReactDoctorCacheDir(rootDirectory), cacheKey);
|
|
37842
|
+
options.onCacheOutcome?.(cachedDiagnostics !== null);
|
|
37843
|
+
if (cachedDiagnostics !== null) return [...cachedDiagnostics];
|
|
37844
|
+
}
|
|
37845
|
+
const incrementalCachePath = options.cacheEnabled === true ? Path.join(resolveReactDoctorCacheDir(rootDirectory), DEAD_CODE_SUMMARY_CACHE_FILENAME) : void 0;
|
|
37667
37846
|
const spawnAndRun = () => {
|
|
37668
37847
|
return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
|
|
37669
37848
|
rootDirectory,
|
|
37670
37849
|
entryPatterns,
|
|
37671
|
-
tsConfigPath
|
|
37850
|
+
tsConfigPath,
|
|
37672
37851
|
ignorePatterns,
|
|
37673
|
-
deslopJsModuleSpecifier
|
|
37674
|
-
parseConcurrency: options.parseConcurrency
|
|
37852
|
+
deslopJsModuleSpecifier,
|
|
37853
|
+
parseConcurrency: options.parseConcurrency,
|
|
37854
|
+
incrementalCachePath
|
|
37675
37855
|
}), options.workerTimeoutMs ?? 12e4, options.abortSignal);
|
|
37676
37856
|
};
|
|
37677
37857
|
const result = parseDeadCodeWorkerResult(options.createWorker === void 0 ? await withDeadCodeWorkerSlot(spawnAndRun, options.abortSignal) : await spawnAndRun());
|
|
37858
|
+
if (result.summaryCacheStats !== void 0) options.onSummaryCacheStats?.(result.summaryCacheStats);
|
|
37678
37859
|
const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
|
|
37679
37860
|
const diagnostics = [];
|
|
37680
37861
|
for (const unusedFile of result.unusedFiles) diagnostics.push({
|
|
@@ -37731,6 +37912,7 @@ const checkDeadCode = async (options) => {
|
|
|
37731
37912
|
category: DEAD_CODE_CATEGORY
|
|
37732
37913
|
});
|
|
37733
37914
|
}
|
|
37915
|
+
if (cacheKey !== null) storeDeadCodeResultCache(resolveReactDoctorCacheDir(rootDirectory), cacheKey, diagnostics);
|
|
37734
37916
|
return diagnostics;
|
|
37735
37917
|
};
|
|
37736
37918
|
const DEAD_CODE_RULE_KEY_PREFIX = `${DEAD_CODE_PLUGIN}/`;
|
|
@@ -37833,12 +38015,16 @@ var Config = class Config extends Service()("react-doctor/Config") {
|
|
|
37833
38015
|
*/
|
|
37834
38016
|
var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
|
|
37835
38017
|
static layerNode = succeed$3(DeadCode, DeadCode.of({ run: (input) => unwrap(fn("DeadCode.run")(function* () {
|
|
38018
|
+
const cacheEnabled = yield* DeadCodeResultCacheEnabled;
|
|
37836
38019
|
return yield* tryPromise({
|
|
37837
38020
|
try: (signal) => checkDeadCode({
|
|
37838
38021
|
rootDirectory: input.rootDirectory,
|
|
37839
38022
|
parseConcurrency: input.parseConcurrency,
|
|
37840
38023
|
workerTimeoutMs: input.workerTimeoutMs,
|
|
37841
|
-
abortSignal: signal
|
|
38024
|
+
abortSignal: signal,
|
|
38025
|
+
cacheEnabled,
|
|
38026
|
+
onCacheOutcome: input.onCacheOutcome,
|
|
38027
|
+
onSummaryCacheStats: input.onSummaryCacheStats
|
|
37842
38028
|
}),
|
|
37843
38029
|
catch: (cause) => new ReactDoctorError({ reason: new DeadCodeAnalysisFailed({ cause }) })
|
|
37844
38030
|
}).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)));
|
|
@@ -38708,7 +38894,7 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
|
|
|
38708
38894
|
}
|
|
38709
38895
|
return enabled;
|
|
38710
38896
|
};
|
|
38711
|
-
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection }) => {
|
|
38897
|
+
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection, sidecarRuleIdFilter }) => {
|
|
38712
38898
|
const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
|
|
38713
38899
|
const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
|
|
38714
38900
|
const jsPlugins = [];
|
|
@@ -38720,6 +38906,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
38720
38906
|
if (!rule) continue;
|
|
38721
38907
|
if (ruleSelection === "cacheable" && CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
|
|
38722
38908
|
if (ruleSelection === "sidecar" && !CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
|
|
38909
|
+
if (ruleSelection === "sidecar" && sidecarRuleIdFilter !== void 0 && !sidecarRuleIdFilter.has(registryEntry.id)) continue;
|
|
38723
38910
|
if (rule.scan !== void 0) continue;
|
|
38724
38911
|
if (customRulesOnly && registryEntry.originallyExternal) continue;
|
|
38725
38912
|
if (rule.framework !== "global" && !rule.requires) continue;
|
|
@@ -38764,33 +38951,152 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
38764
38951
|
}
|
|
38765
38952
|
};
|
|
38766
38953
|
};
|
|
38767
|
-
const
|
|
38954
|
+
const validateDiagnostic$1 = decodeUnknownSync(Diagnostic);
|
|
38955
|
+
const decodeFileDiagnostics = (raw) => {
|
|
38956
|
+
if (!Array.isArray(raw)) return null;
|
|
38768
38957
|
try {
|
|
38769
|
-
|
|
38770
|
-
|
|
38771
|
-
NFS.writeFileSync(temporaryPath, JSON.stringify(value));
|
|
38772
|
-
NFS.renameSync(temporaryPath, filePath);
|
|
38958
|
+
for (const entry of raw) validateDiagnostic$1(entry);
|
|
38959
|
+
return raw;
|
|
38773
38960
|
} catch {
|
|
38774
|
-
return;
|
|
38961
|
+
return null;
|
|
38775
38962
|
}
|
|
38776
38963
|
};
|
|
38777
|
-
const
|
|
38964
|
+
const emptyCache$1 = () => ({
|
|
38965
|
+
version: 1,
|
|
38966
|
+
rulesets: {}
|
|
38967
|
+
});
|
|
38968
|
+
const loadRulesetEntries$1 = (cacheFilePath, rulesetHash) => {
|
|
38969
|
+
const entries = /* @__PURE__ */ new Map();
|
|
38970
|
+
const persisted = failOpenReadJson(cacheFilePath, emptyCache$1());
|
|
38971
|
+
if (persisted.version !== 1 || !isRecord(persisted.rulesets)) return entries;
|
|
38972
|
+
const bucket = persisted.rulesets[rulesetHash];
|
|
38973
|
+
if (!isRecord(bucket) || !isRecord(bucket.files)) return entries;
|
|
38974
|
+
for (const [fileKey, rawDiagnostics] of Object.entries(bucket.files)) {
|
|
38975
|
+
const decoded = decodeFileDiagnostics(rawDiagnostics);
|
|
38976
|
+
if (decoded !== null) entries.set(fileKey, decoded);
|
|
38977
|
+
}
|
|
38978
|
+
return entries;
|
|
38979
|
+
};
|
|
38980
|
+
const createFileLintCache = (cacheDirectory, rulesetHash) => {
|
|
38981
|
+
const cacheFilePath = Path.join(cacheDirectory, FILE_LINT_CACHE_FILENAME);
|
|
38982
|
+
const entries = loadRulesetEntries$1(cacheFilePath, rulesetHash);
|
|
38983
|
+
return {
|
|
38984
|
+
lookup: (fileKey) => entries.get(fileKey) ?? null,
|
|
38985
|
+
store: (fileKey, diagnostics) => {
|
|
38986
|
+
entries.delete(fileKey);
|
|
38987
|
+
entries.set(fileKey, diagnostics);
|
|
38988
|
+
},
|
|
38989
|
+
persist: () => {
|
|
38990
|
+
const onDisk = failOpenReadJson(cacheFilePath, emptyCache$1());
|
|
38991
|
+
const rulesets = onDisk.version === 1 && isRecord(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
|
|
38992
|
+
const existingBucket = rulesets[rulesetHash];
|
|
38993
|
+
const existingFiles = isRecord(existingBucket) && isRecord(existingBucket.files) ? existingBucket.files : {};
|
|
38994
|
+
const ourFiles = {};
|
|
38995
|
+
for (const [fileKey, diagnostics] of entries) ourFiles[fileKey] = diagnostics;
|
|
38996
|
+
const cappedEntries = Object.entries({
|
|
38997
|
+
...existingFiles,
|
|
38998
|
+
...ourFiles
|
|
38999
|
+
}).slice(-FILE_LINT_CACHE_MAX_FILE_COUNT);
|
|
39000
|
+
rulesets[rulesetHash] = {
|
|
39001
|
+
updatedAtMs: Date.now(),
|
|
39002
|
+
files: Object.fromEntries(cappedEntries)
|
|
39003
|
+
};
|
|
39004
|
+
atomicWriteJson(cacheFilePath, {
|
|
39005
|
+
version: 1,
|
|
39006
|
+
rulesets: Object.fromEntries(Object.entries(rulesets).filter(([, bucket]) => isRecord(bucket) && typeof bucket.updatedAtMs === "number").sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8))
|
|
39007
|
+
});
|
|
39008
|
+
}
|
|
39009
|
+
};
|
|
39010
|
+
};
|
|
39011
|
+
const hashFileContents = (filePath) => {
|
|
38778
39012
|
try {
|
|
38779
|
-
return
|
|
39013
|
+
return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
|
|
38780
39014
|
} catch {
|
|
38781
|
-
return
|
|
39015
|
+
return null;
|
|
38782
39016
|
}
|
|
38783
39017
|
};
|
|
39018
|
+
const ABSENT_CONTENT_ANSWER = "absent";
|
|
39019
|
+
/**
|
|
39020
|
+
* Answers dependency probes for the sidecar lint cache — used both when
|
|
39021
|
+
* STORING an entry (turn the collector's probe paths into answers) and when
|
|
39022
|
+
* VERIFYING one (re-answer each stored probe against the current tree).
|
|
39023
|
+
* Store and verify must agree exactly, so both go through this one resolver.
|
|
39024
|
+
*
|
|
39025
|
+
* - `content` — SHA-1 of the file's bytes, `"absent"` when unreadable
|
|
39026
|
+
* (missing, a directory, permission failure). Source files reuse the
|
|
39027
|
+
* hashes the cache-partition loop already computed
|
|
39028
|
+
* (`contentHashByRelativePath`); everything else (package.json,
|
|
39029
|
+
* tsconfig, ignored files) is hashed on demand.
|
|
39030
|
+
* - `exists` — the existence class a module-resolution candidate probe
|
|
39031
|
+
* sees: `"file"`, `"dir"`, or `"none"` (missing or an exotic entry —
|
|
39032
|
+
* `statSync` follows symlinks, matching the resolvers).
|
|
39033
|
+
*
|
|
39034
|
+
* Answers are memoized per scan: the filesystem is frozen for the run, and
|
|
39035
|
+
* hot dependency paths (a barrel imported by hundreds of files, the shared
|
|
39036
|
+
* tsconfig chain) repeat across many files' probe sets.
|
|
39037
|
+
*/
|
|
39038
|
+
const createSidecarProbeAnswerResolver = (input) => {
|
|
39039
|
+
const contentAnswerByPath = /* @__PURE__ */ new Map();
|
|
39040
|
+
const existsAnswerByPath = /* @__PURE__ */ new Map();
|
|
39041
|
+
const toRelativePath = (absolutePath) => Path.relative(input.rootDirectory, absolutePath).replaceAll("\\", "/");
|
|
39042
|
+
const contentAnswer = (relativePath) => {
|
|
39043
|
+
const memoized = contentAnswerByPath.get(relativePath);
|
|
39044
|
+
if (memoized !== void 0) return memoized;
|
|
39045
|
+
const answer = input.contentHashByRelativePath.get(relativePath) ?? hashFileContents(Path.resolve(input.rootDirectory, relativePath)) ?? ABSENT_CONTENT_ANSWER;
|
|
39046
|
+
contentAnswerByPath.set(relativePath, answer);
|
|
39047
|
+
return answer;
|
|
39048
|
+
};
|
|
39049
|
+
const existsAnswer = (relativePath) => {
|
|
39050
|
+
const memoized = existsAnswerByPath.get(relativePath);
|
|
39051
|
+
if (memoized !== void 0) return memoized;
|
|
39052
|
+
let answer;
|
|
39053
|
+
try {
|
|
39054
|
+
const stat = NFS.statSync(Path.resolve(input.rootDirectory, relativePath));
|
|
39055
|
+
answer = stat.isFile() ? "file" : stat.isDirectory() ? "dir" : "none";
|
|
39056
|
+
} catch {
|
|
39057
|
+
answer = "none";
|
|
39058
|
+
}
|
|
39059
|
+
existsAnswerByPath.set(relativePath, answer);
|
|
39060
|
+
return answer;
|
|
39061
|
+
};
|
|
39062
|
+
return {
|
|
39063
|
+
answerFor: (kind, relativePath) => kind === "content" ? contentAnswer(relativePath) : existsAnswer(relativePath),
|
|
39064
|
+
toRelativePath
|
|
39065
|
+
};
|
|
39066
|
+
};
|
|
38784
39067
|
const validateDiagnostic = decodeUnknownSync(Diagnostic);
|
|
38785
|
-
const
|
|
38786
|
-
|
|
39068
|
+
const isProbeKind = (value) => value === "content" || value === "exists";
|
|
39069
|
+
const decodeSidecarFileEntry = (raw) => {
|
|
39070
|
+
if (!isRecord(raw) || !Array.isArray(raw.probes) || !Array.isArray(raw.diagnostics)) return null;
|
|
39071
|
+
const probes = [];
|
|
39072
|
+
for (const rawProbe of raw.probes) {
|
|
39073
|
+
if (!Array.isArray(rawProbe) || rawProbe.length !== 3) return null;
|
|
39074
|
+
const [kind, probePath, answer] = rawProbe;
|
|
39075
|
+
if (!isProbeKind(kind) || typeof probePath !== "string" || typeof answer !== "string") return null;
|
|
39076
|
+
probes.push({
|
|
39077
|
+
kind,
|
|
39078
|
+
path: probePath,
|
|
39079
|
+
answer
|
|
39080
|
+
});
|
|
39081
|
+
}
|
|
38787
39082
|
try {
|
|
38788
|
-
for (const entry of raw) validateDiagnostic(entry);
|
|
38789
|
-
return raw;
|
|
39083
|
+
for (const entry of raw.diagnostics) validateDiagnostic(entry);
|
|
38790
39084
|
} catch {
|
|
38791
39085
|
return null;
|
|
38792
39086
|
}
|
|
39087
|
+
return {
|
|
39088
|
+
probes,
|
|
39089
|
+
diagnostics: raw.diagnostics
|
|
39090
|
+
};
|
|
38793
39091
|
};
|
|
39092
|
+
const encodeSidecarFileEntry = (entry) => ({
|
|
39093
|
+
probes: entry.probes.map((probe) => [
|
|
39094
|
+
probe.kind,
|
|
39095
|
+
probe.path,
|
|
39096
|
+
probe.answer
|
|
39097
|
+
]),
|
|
39098
|
+
diagnostics: entry.diagnostics
|
|
39099
|
+
});
|
|
38794
39100
|
const emptyCache = () => ({
|
|
38795
39101
|
version: 1,
|
|
38796
39102
|
rulesets: {}
|
|
@@ -38801,20 +39107,20 @@ const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
|
|
|
38801
39107
|
if (persisted.version !== 1 || !isRecord(persisted.rulesets)) return entries;
|
|
38802
39108
|
const bucket = persisted.rulesets[rulesetHash];
|
|
38803
39109
|
if (!isRecord(bucket) || !isRecord(bucket.files)) return entries;
|
|
38804
|
-
for (const [fileKey,
|
|
38805
|
-
const decoded =
|
|
39110
|
+
for (const [fileKey, rawEntry] of Object.entries(bucket.files)) {
|
|
39111
|
+
const decoded = decodeSidecarFileEntry(rawEntry);
|
|
38806
39112
|
if (decoded !== null) entries.set(fileKey, decoded);
|
|
38807
39113
|
}
|
|
38808
39114
|
return entries;
|
|
38809
39115
|
};
|
|
38810
|
-
const
|
|
38811
|
-
const cacheFilePath = Path.join(cacheDirectory,
|
|
39116
|
+
const createSidecarLintCache = (cacheDirectory, rulesetHash) => {
|
|
39117
|
+
const cacheFilePath = Path.join(cacheDirectory, SIDECAR_LINT_CACHE_FILENAME);
|
|
38812
39118
|
const entries = loadRulesetEntries(cacheFilePath, rulesetHash);
|
|
38813
39119
|
return {
|
|
38814
39120
|
lookup: (fileKey) => entries.get(fileKey) ?? null,
|
|
38815
|
-
store: (fileKey,
|
|
39121
|
+
store: (fileKey, entry) => {
|
|
38816
39122
|
entries.delete(fileKey);
|
|
38817
|
-
entries.set(fileKey,
|
|
39123
|
+
entries.set(fileKey, entry);
|
|
38818
39124
|
},
|
|
38819
39125
|
persist: () => {
|
|
38820
39126
|
const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
|
|
@@ -38822,7 +39128,7 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
|
|
|
38822
39128
|
const existingBucket = rulesets[rulesetHash];
|
|
38823
39129
|
const existingFiles = isRecord(existingBucket) && isRecord(existingBucket.files) ? existingBucket.files : {};
|
|
38824
39130
|
const ourFiles = {};
|
|
38825
|
-
for (const [fileKey,
|
|
39131
|
+
for (const [fileKey, entry] of entries) ourFiles[fileKey] = encodeSidecarFileEntry(entry);
|
|
38826
39132
|
const cappedEntries = Object.entries({
|
|
38827
39133
|
...existingFiles,
|
|
38828
39134
|
...ourFiles
|
|
@@ -39837,13 +40143,6 @@ const validateRuleRegistration = () => {
|
|
|
39837
40143
|
].filter((entry) => entry !== null).join("; ");
|
|
39838
40144
|
console.warn(`[react-doctor] rule-registration drift: ${detail}`);
|
|
39839
40145
|
};
|
|
39840
|
-
const hashFileContents = (filePath) => {
|
|
39841
|
-
try {
|
|
39842
|
-
return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
|
|
39843
|
-
} catch {
|
|
39844
|
-
return null;
|
|
39845
|
-
}
|
|
39846
|
-
};
|
|
39847
40146
|
/**
|
|
39848
40147
|
* Balanced LPT (longest-processing-time-first) lint batch planner. Where
|
|
39849
40148
|
* `batchIncludePaths` greedily fills 100-file batches and leaves a small
|
|
@@ -39908,21 +40207,6 @@ const planLintBatches = ({ baseArgs, files, sizeByFile }) => {
|
|
|
39908
40207
|
}
|
|
39909
40208
|
return batches.filter((batch) => batch.fileIndexes.length > 0).map((batch) => batch.fileIndexes.sort((leftIndex, rightIndex) => leftIndex - rightIndex).map((fileIndex) => files[fileIndex]));
|
|
39910
40209
|
};
|
|
39911
|
-
const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
|
|
39912
|
-
const resolveReactDoctorCacheDir = (projectDirectory) => {
|
|
39913
|
-
const cacheDirOverride = process.env["REACT_DOCTOR_CACHE_DIR"]?.trim();
|
|
39914
|
-
if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
|
|
39915
|
-
const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
|
|
39916
|
-
if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
|
|
39917
|
-
return Path.join(os.tmpdir(), `react-doctor-cache-${resolveUserCacheScope()}`, projectCacheSubdir(projectDirectory));
|
|
39918
|
-
};
|
|
39919
|
-
const resolveUserCacheScope = () => {
|
|
39920
|
-
try {
|
|
39921
|
-
return String(process.getuid?.() ?? os.userInfo().username);
|
|
39922
|
-
} catch {
|
|
39923
|
-
return "shared";
|
|
39924
|
-
}
|
|
39925
|
-
};
|
|
39926
40210
|
/**
|
|
39927
40211
|
* Atomically (re)writes the generated oxlintrc.json. Used twice in
|
|
39928
40212
|
* the runner: once for the primary scan, once for the
|
|
@@ -39940,6 +40224,106 @@ const writeOxlintConfig = (configPath, configToWrite) => {
|
|
|
39940
40224
|
NFS.closeSync(fileHandle);
|
|
39941
40225
|
}
|
|
39942
40226
|
};
|
|
40227
|
+
/**
|
|
40228
|
+
* Attributes diagnostics back to the file that produced them by the
|
|
40229
|
+
* normalized path oxlint echoes. Returns `null` when ANY diagnostic can't be
|
|
40230
|
+
* attributed — the path forms don't align, so the caller must skip its cache
|
|
40231
|
+
* store rather than risk caching a wrong empty result for a file that
|
|
40232
|
+
* actually had diagnostics.
|
|
40233
|
+
*/
|
|
40234
|
+
const attributeDiagnosticsToFiles = (diagnostics, files) => {
|
|
40235
|
+
const fileByNormalizedPath = /* @__PURE__ */ new Map();
|
|
40236
|
+
for (const file of files) fileByNormalizedPath.set(file.replaceAll("\\", "/"), file);
|
|
40237
|
+
const diagnosticsByFile = /* @__PURE__ */ new Map();
|
|
40238
|
+
for (const diagnostic of diagnostics) {
|
|
40239
|
+
const file = fileByNormalizedPath.get(diagnostic.filePath);
|
|
40240
|
+
if (file === void 0) return null;
|
|
40241
|
+
const fileDiagnostics = diagnosticsByFile.get(file) ?? [];
|
|
40242
|
+
fileDiagnostics.push(diagnostic);
|
|
40243
|
+
diagnosticsByFile.set(file, fileDiagnostics);
|
|
40244
|
+
}
|
|
40245
|
+
return diagnosticsByFile;
|
|
40246
|
+
};
|
|
40247
|
+
/**
|
|
40248
|
+
* Collects each file's cross-file dependency probes (via the plugin's
|
|
40249
|
+
* collectors) with their current answers. Fail-open per file: an unreadable /
|
|
40250
|
+
* unparseable / collector-failing file maps to `null` (no entry stored — it
|
|
40251
|
+
* re-lints next scan). The filesystem is frozen for the scan, so this can run
|
|
40252
|
+
* CONCURRENTLY with an oxlint child-process await — the caller starts it
|
|
40253
|
+
* unawaited before a lint pass and awaits it at store time, hiding the
|
|
40254
|
+
* in-process parse + resolution cost under the subprocess wait. The loop
|
|
40255
|
+
* yields on the cooperative time budget so the child's stdout handling (and
|
|
40256
|
+
* sibling fibers) keep flowing; each per-file collection is synchronous, so
|
|
40257
|
+
* the module-level probe recorder never sees interleaved use. Never rejects.
|
|
40258
|
+
*/
|
|
40259
|
+
const collectSidecarProbesForFiles = async (input) => {
|
|
40260
|
+
const buildProbes = (file) => {
|
|
40261
|
+
const absoluteFilePath = Path.resolve(input.rootDirectory, file);
|
|
40262
|
+
let trace;
|
|
40263
|
+
try {
|
|
40264
|
+
trace = collectCrossFileDependencyProbes({
|
|
40265
|
+
absoluteFilePath,
|
|
40266
|
+
sourceText: NFS.readFileSync(absoluteFilePath, "utf8"),
|
|
40267
|
+
ruleIds: input.boundedSidecarRuleIds
|
|
40268
|
+
});
|
|
40269
|
+
} catch {
|
|
40270
|
+
return null;
|
|
40271
|
+
}
|
|
40272
|
+
if (trace === null) return null;
|
|
40273
|
+
const probes = [];
|
|
40274
|
+
for (const contentPath of trace.contentPaths) {
|
|
40275
|
+
const relativePath = input.probeAnswers.toRelativePath(contentPath);
|
|
40276
|
+
probes.push({
|
|
40277
|
+
kind: "content",
|
|
40278
|
+
path: relativePath,
|
|
40279
|
+
answer: input.probeAnswers.answerFor("content", relativePath)
|
|
40280
|
+
});
|
|
40281
|
+
}
|
|
40282
|
+
for (const existencePath of trace.existencePaths) {
|
|
40283
|
+
const relativePath = input.probeAnswers.toRelativePath(existencePath);
|
|
40284
|
+
probes.push({
|
|
40285
|
+
kind: "exists",
|
|
40286
|
+
path: relativePath,
|
|
40287
|
+
answer: input.probeAnswers.answerFor("exists", relativePath)
|
|
40288
|
+
});
|
|
40289
|
+
}
|
|
40290
|
+
return probes;
|
|
40291
|
+
};
|
|
40292
|
+
const probesByFile = /* @__PURE__ */ new Map();
|
|
40293
|
+
let collectSliceStartedAt = performance.now();
|
|
40294
|
+
for (const file of input.files) {
|
|
40295
|
+
probesByFile.set(file, buildProbes(file));
|
|
40296
|
+
if (performance.now() - collectSliceStartedAt >= 12) {
|
|
40297
|
+
await yieldToEventLoop();
|
|
40298
|
+
collectSliceStartedAt = performance.now();
|
|
40299
|
+
}
|
|
40300
|
+
}
|
|
40301
|
+
return probesByFile;
|
|
40302
|
+
};
|
|
40303
|
+
/**
|
|
40304
|
+
* Stores each freshly-linted file's probe set + cross-file diagnostics so
|
|
40305
|
+
* the next scan can replay instead of re-linting.
|
|
40306
|
+
*/
|
|
40307
|
+
const storeSidecarEntries = (input) => {
|
|
40308
|
+
let didStoreAnyEntry = false;
|
|
40309
|
+
for (const pass of input.storablePasses) {
|
|
40310
|
+
if (!pass.isTrusted) continue;
|
|
40311
|
+
const diagnosticsByFile = attributeDiagnosticsToFiles(pass.diagnostics, pass.files);
|
|
40312
|
+
if (diagnosticsByFile === null) continue;
|
|
40313
|
+
for (const file of pass.files) {
|
|
40314
|
+
const cacheKey = input.cacheKeyByFile.get(file);
|
|
40315
|
+
if (cacheKey === void 0) continue;
|
|
40316
|
+
const probes = pass.probesByFile.get(file);
|
|
40317
|
+
if (probes === void 0 || probes === null) continue;
|
|
40318
|
+
input.sidecarCache.store(cacheKey, {
|
|
40319
|
+
probes,
|
|
40320
|
+
diagnostics: diagnosticsByFile.get(file) ?? []
|
|
40321
|
+
});
|
|
40322
|
+
didStoreAnyEntry = true;
|
|
40323
|
+
}
|
|
40324
|
+
}
|
|
40325
|
+
if (didStoreAnyEntry) input.sidecarCache.persist();
|
|
40326
|
+
};
|
|
39943
40327
|
const REACT_HOOKS_JS_DROP_PREFIX = "React Compiler rules (react-hooks-js/*) skipped — eslint-plugin-react-hooks failed to load in this environment";
|
|
39944
40328
|
/**
|
|
39945
40329
|
* Detects an oxlint config-load crash caused by the optional
|
|
@@ -39981,7 +40365,7 @@ const reactHooksJsPluginDropNote = (error) => {
|
|
|
39981
40365
|
* 6. always restore disable directives + clean up the temp dir
|
|
39982
40366
|
*/
|
|
39983
40367
|
const runOxlint = async (options) => {
|
|
39984
|
-
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;
|
|
40368
|
+
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;
|
|
39985
40369
|
const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
|
|
39986
40370
|
const severityControls = buildRuleSeverityControls(userConfig);
|
|
39987
40371
|
validateRuleRegistration();
|
|
@@ -39999,7 +40383,8 @@ const runOxlint = async (options) => {
|
|
|
39999
40383
|
severityControls,
|
|
40000
40384
|
userPlugins,
|
|
40001
40385
|
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin,
|
|
40002
|
-
ruleSelection: overrides.ruleSelection
|
|
40386
|
+
ruleSelection: overrides.ruleSelection,
|
|
40387
|
+
sidecarRuleIdFilter: overrides.sidecarRuleIdFilter
|
|
40003
40388
|
});
|
|
40004
40389
|
const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
|
|
40005
40390
|
const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
|
|
@@ -40102,15 +40487,18 @@ const runOxlint = async (options) => {
|
|
|
40102
40487
|
});
|
|
40103
40488
|
const cache = createFileLintCache(resolveReactDoctorCacheDir(rootDirectory), rulesetHash);
|
|
40104
40489
|
const cacheKeyByFile = /* @__PURE__ */ new Map();
|
|
40490
|
+
const contentHashByRelativePath = /* @__PURE__ */ new Map();
|
|
40105
40491
|
const missFiles = [];
|
|
40106
40492
|
const replayedDiagnostics = [];
|
|
40107
40493
|
let hashSliceStartedAt = performance.now();
|
|
40108
40494
|
for (const candidateFile of candidateFiles) {
|
|
40109
|
-
const
|
|
40495
|
+
const absoluteCandidatePath = Path.resolve(rootDirectory, candidateFile);
|
|
40496
|
+
const contentHash = hashFileContents(absoluteCandidatePath);
|
|
40110
40497
|
if (contentHash === null) {
|
|
40111
40498
|
missFiles.push(candidateFile);
|
|
40112
40499
|
continue;
|
|
40113
40500
|
}
|
|
40501
|
+
contentHashByRelativePath.set(Path.relative(rootDirectory, absoluteCandidatePath).replaceAll("\\", "/"), contentHash);
|
|
40114
40502
|
const cacheKey = `${candidateFile.replaceAll("\\", "/")}\u0000${contentHash}`;
|
|
40115
40503
|
cacheKeyByFile.set(candidateFile, cacheKey);
|
|
40116
40504
|
const cachedDiagnostics = cache.lookup(cacheKey);
|
|
@@ -40122,43 +40510,111 @@ const runOxlint = async (options) => {
|
|
|
40122
40510
|
}
|
|
40123
40511
|
}
|
|
40124
40512
|
const cacheHitFileCount = candidateFiles.length - missFiles.length;
|
|
40513
|
+
const enabledSidecarRuleIds = Object.keys(buildConfig({
|
|
40514
|
+
extendsPaths: [],
|
|
40515
|
+
ruleSelection: "sidecar"
|
|
40516
|
+
}).rules).map((ruleKey) => ruleKey.slice(ruleKey.lastIndexOf("/") + 1)).filter((ruleId) => CROSS_FILE_RULE_IDS.has(ruleId));
|
|
40517
|
+
const boundedSidecarRuleIds = enabledSidecarRuleIds.filter((ruleId) => CROSS_FILE_DEPENDENCY_COLLECTORS.has(ruleId));
|
|
40518
|
+
const boundedSidecarRuleIdSet = new Set(boundedSidecarRuleIds);
|
|
40519
|
+
const unboundedSidecarRuleIds = enabledSidecarRuleIds.filter((ruleId) => !CROSS_FILE_DEPENDENCY_COLLECTORS.has(ruleId));
|
|
40520
|
+
const useSidecarCache = sidecarLintCacheEnabled && boundedSidecarRuleIds.length > 0;
|
|
40521
|
+
const sidecarCache = useSidecarCache ? createSidecarLintCache(resolveReactDoctorCacheDir(rootDirectory), computeRulesetHash({
|
|
40522
|
+
config: buildConfig({
|
|
40523
|
+
extendsPaths: [],
|
|
40524
|
+
ruleSelection: "sidecar",
|
|
40525
|
+
sidecarRuleIdFilter: boundedSidecarRuleIdSet
|
|
40526
|
+
}),
|
|
40527
|
+
toolchainVersions: resolveOxlintToolchainVersions(nodeBinaryPath),
|
|
40528
|
+
ignorePatterns: combinedPatterns,
|
|
40529
|
+
tsconfigContent
|
|
40530
|
+
})) : null;
|
|
40531
|
+
const probeAnswers = createSidecarProbeAnswerResolver({
|
|
40532
|
+
rootDirectory,
|
|
40533
|
+
contentHashByRelativePath
|
|
40534
|
+
});
|
|
40535
|
+
const missProbesTask = sidecarCache === null ? null : collectSidecarProbesForFiles({
|
|
40536
|
+
files: missFiles,
|
|
40537
|
+
rootDirectory,
|
|
40538
|
+
boundedSidecarRuleIds,
|
|
40539
|
+
probeAnswers
|
|
40540
|
+
});
|
|
40125
40541
|
const fullResult = await runConfigOverFiles((overrides) => buildConfig({
|
|
40126
40542
|
extendsPaths: [],
|
|
40127
40543
|
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
|
|
40128
40544
|
}), "oxlintrc.full.json", missFiles, options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(scannedFileCount, candidateFiles.length)));
|
|
40129
40545
|
const missFileSet = new Set(missFiles);
|
|
40130
|
-
const
|
|
40546
|
+
const hitFiles = candidateFiles.filter((candidateFile) => !missFileSet.has(candidateFile));
|
|
40547
|
+
const sidecarReplayedDiagnostics = [];
|
|
40548
|
+
const sidecarLintFiles = [];
|
|
40549
|
+
if (sidecarCache === null) sidecarLintFiles.push(...hitFiles);
|
|
40550
|
+
else {
|
|
40551
|
+
let verifySliceStartedAt = performance.now();
|
|
40552
|
+
for (const hitFile of hitFiles) {
|
|
40553
|
+
const cacheKey = cacheKeyByFile.get(hitFile);
|
|
40554
|
+
const entry = cacheKey === void 0 ? null : sidecarCache.lookup(cacheKey);
|
|
40555
|
+
if (entry !== null && entry.probes.every((probe) => probeAnswers.answerFor(probe.kind, probe.path) === probe.answer)) sidecarReplayedDiagnostics.push(...entry.diagnostics);
|
|
40556
|
+
else sidecarLintFiles.push(hitFile);
|
|
40557
|
+
if (performance.now() - verifySliceStartedAt >= 12) {
|
|
40558
|
+
await yieldToEventLoop();
|
|
40559
|
+
verifySliceStartedAt = performance.now();
|
|
40560
|
+
}
|
|
40561
|
+
}
|
|
40562
|
+
}
|
|
40563
|
+
const sidecarProbesTask = sidecarCache === null ? null : collectSidecarProbesForFiles({
|
|
40564
|
+
files: sidecarLintFiles,
|
|
40565
|
+
rootDirectory,
|
|
40566
|
+
boundedSidecarRuleIds,
|
|
40567
|
+
probeAnswers
|
|
40568
|
+
});
|
|
40569
|
+
const sidecarReplayedFileCount = hitFiles.length - sidecarLintFiles.length;
|
|
40570
|
+
if (sidecarReplayedFileCount > 0) options.onFileProgress?.(missFiles.length + sidecarReplayedFileCount, candidateFiles.length);
|
|
40571
|
+
const boundedSidecarResult = await runConfigOverFiles(() => buildConfig({
|
|
40131
40572
|
extendsPaths: [],
|
|
40132
|
-
ruleSelection: "sidecar"
|
|
40133
|
-
|
|
40573
|
+
ruleSelection: "sidecar",
|
|
40574
|
+
...useSidecarCache ? { sidecarRuleIdFilter: boundedSidecarRuleIdSet } : {}
|
|
40575
|
+
}), "oxlintrc.sidecar.json", sidecarLintFiles, options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(missFiles.length + sidecarReplayedFileCount + scannedFileCount, candidateFiles.length)));
|
|
40576
|
+
const unboundedSidecarResult = useSidecarCache && unboundedSidecarRuleIds.length > 0 ? await runConfigOverFiles(() => buildConfig({
|
|
40577
|
+
extendsPaths: [],
|
|
40578
|
+
ruleSelection: "sidecar",
|
|
40579
|
+
sidecarRuleIdFilter: new Set(unboundedSidecarRuleIds)
|
|
40580
|
+
}), "oxlintrc.sidecar-unbounded.json", hitFiles, void 0) : {
|
|
40581
|
+
diagnostics: [],
|
|
40582
|
+
didDropReactHooksJsPlugin: false,
|
|
40583
|
+
hadPartialFailure: false
|
|
40584
|
+
};
|
|
40134
40585
|
const freshCacheableDiagnostics = [];
|
|
40135
40586
|
for (const diagnostic of fullResult.diagnostics) if (!CROSS_FILE_RULE_IDS.has(diagnostic.rule)) freshCacheableDiagnostics.push(diagnostic);
|
|
40136
40587
|
onCacheStats?.(cacheHitFileCount, candidateFiles.length);
|
|
40137
|
-
|
|
40138
|
-
|
|
40139
|
-
|
|
40140
|
-
let isAttributionSound = true;
|
|
40141
|
-
for (const diagnostic of freshCacheableDiagnostics) {
|
|
40142
|
-
const missFile = missFileByNormalizedPath.get(diagnostic.filePath);
|
|
40143
|
-
if (missFile === void 0) {
|
|
40144
|
-
isAttributionSound = false;
|
|
40145
|
-
break;
|
|
40146
|
-
}
|
|
40147
|
-
const fileDiagnostics = freshDiagnosticsByFile.get(missFile) ?? [];
|
|
40148
|
-
fileDiagnostics.push(diagnostic);
|
|
40149
|
-
freshDiagnosticsByFile.set(missFile, fileDiagnostics);
|
|
40150
|
-
}
|
|
40151
|
-
if (!fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure && isAttributionSound) {
|
|
40588
|
+
if (sidecarCache !== null) onSidecarStats?.(hitFiles.length - sidecarLintFiles.length, hitFiles.length);
|
|
40589
|
+
const freshDiagnosticsByFile = attributeDiagnosticsToFiles(freshCacheableDiagnostics, missFiles);
|
|
40590
|
+
if (!fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure && freshDiagnosticsByFile !== null) {
|
|
40152
40591
|
for (const missFile of missFiles) {
|
|
40153
40592
|
const cacheKey = cacheKeyByFile.get(missFile);
|
|
40154
40593
|
if (cacheKey !== void 0) cache.store(cacheKey, freshDiagnosticsByFile.get(missFile) ?? []);
|
|
40155
40594
|
}
|
|
40156
40595
|
cache.persist();
|
|
40157
40596
|
}
|
|
40597
|
+
if (sidecarCache !== null && missProbesTask !== null && sidecarProbesTask !== null) storeSidecarEntries({
|
|
40598
|
+
sidecarCache,
|
|
40599
|
+
cacheKeyByFile,
|
|
40600
|
+
storablePasses: [{
|
|
40601
|
+
files: sidecarLintFiles,
|
|
40602
|
+
diagnostics: boundedSidecarResult.diagnostics,
|
|
40603
|
+
isTrusted: !boundedSidecarResult.hadPartialFailure,
|
|
40604
|
+
probesByFile: await sidecarProbesTask
|
|
40605
|
+
}, {
|
|
40606
|
+
files: missFiles,
|
|
40607
|
+
diagnostics: fullResult.diagnostics.filter((diagnostic) => boundedSidecarRuleIdSet.has(diagnostic.rule)),
|
|
40608
|
+
isTrusted: !fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure,
|
|
40609
|
+
probesByFile: await missProbesTask
|
|
40610
|
+
}]
|
|
40611
|
+
});
|
|
40158
40612
|
return dedupeDiagnostics([
|
|
40159
40613
|
...replayedDiagnostics,
|
|
40160
40614
|
...fullResult.diagnostics,
|
|
40161
|
-
...
|
|
40615
|
+
...sidecarReplayedDiagnostics,
|
|
40616
|
+
...boundedSidecarResult.diagnostics,
|
|
40617
|
+
...unboundedSidecarResult.diagnostics
|
|
40162
40618
|
]);
|
|
40163
40619
|
}
|
|
40164
40620
|
const baseArgs = makeBaseArgs(configPath);
|
|
@@ -40256,6 +40712,7 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
|
|
|
40256
40712
|
const concurrency = yield* OxlintConcurrency;
|
|
40257
40713
|
const lintBatchOrdering = yield* LintBatchOrdering;
|
|
40258
40714
|
const perFileLintCacheEnabled = yield* PerFileLintCacheEnabled;
|
|
40715
|
+
const sidecarLintCacheEnabled = yield* SidecarLintCacheEnabled;
|
|
40259
40716
|
const collectedFailures = [];
|
|
40260
40717
|
const diagnostics = yield* tryPromise({
|
|
40261
40718
|
try: (signal) => runOxlint({
|
|
@@ -40274,7 +40731,9 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
|
|
|
40274
40731
|
},
|
|
40275
40732
|
onFileProgress: input.onFileProgress,
|
|
40276
40733
|
perFileLintCacheEnabled,
|
|
40734
|
+
sidecarLintCacheEnabled,
|
|
40277
40735
|
onCacheStats: input.onCacheStats,
|
|
40736
|
+
onSidecarStats: input.onSidecarStats,
|
|
40278
40737
|
spawnTimeoutMs,
|
|
40279
40738
|
outputMaxBytes,
|
|
40280
40739
|
concurrency,
|
|
@@ -41049,7 +41508,14 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
41049
41508
|
const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
|
|
41050
41509
|
rootDirectory: scanDirectory,
|
|
41051
41510
|
parseConcurrency: deadCodeParseConcurrency,
|
|
41052
|
-
workerTimeoutMs: deadCodeTimeout.workerTimeoutMs
|
|
41511
|
+
workerTimeoutMs: deadCodeTimeout.workerTimeoutMs,
|
|
41512
|
+
onCacheOutcome: (didHitCache) => {
|
|
41513
|
+
deadCodeCacheHit = didHitCache;
|
|
41514
|
+
},
|
|
41515
|
+
onSummaryCacheStats: (stats) => {
|
|
41516
|
+
deadCodeSummaryCacheHits = stats.hits;
|
|
41517
|
+
deadCodeSummaryCacheMisses = stats.misses;
|
|
41518
|
+
}
|
|
41053
41519
|
}).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
|
|
41054
41520
|
yield* set(deadCodeFailure, {
|
|
41055
41521
|
didFail: true,
|
|
@@ -41077,6 +41543,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
41077
41543
|
let lastReportedTotalFileCount = 0;
|
|
41078
41544
|
let lintCacheHitFileCount = null;
|
|
41079
41545
|
let lintCacheTotalFileCount = null;
|
|
41546
|
+
let lintSidecarReplayedFileCount = null;
|
|
41547
|
+
let lintSidecarTotalFileCount = null;
|
|
41548
|
+
let deadCodeCacheHit = null;
|
|
41549
|
+
let deadCodeSummaryCacheHits = null;
|
|
41550
|
+
let deadCodeSummaryCacheMisses = null;
|
|
41080
41551
|
const baseLintStream = linterService.run({
|
|
41081
41552
|
rootDirectory: scanDirectory,
|
|
41082
41553
|
project,
|
|
@@ -41096,6 +41567,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
41096
41567
|
lintCacheHitFileCount = cacheHitFileCount;
|
|
41097
41568
|
lintCacheTotalFileCount = totalConsideredFileCount;
|
|
41098
41569
|
},
|
|
41570
|
+
onSidecarStats: (sidecarReplayedFileCount, sidecarConsideredFileCount) => {
|
|
41571
|
+
lintSidecarReplayedFileCount = sidecarReplayedFileCount;
|
|
41572
|
+
lintSidecarTotalFileCount = sidecarConsideredFileCount;
|
|
41573
|
+
},
|
|
41099
41574
|
deadlineEpochMs: input.deadlineEpochMs
|
|
41100
41575
|
}).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
|
|
41101
41576
|
yield* set(lintFailure, {
|
|
@@ -41206,6 +41681,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
41206
41681
|
securityScanFailed,
|
|
41207
41682
|
lintCacheHitFileCount,
|
|
41208
41683
|
lintCacheTotalFileCount,
|
|
41684
|
+
lintSidecarReplayedFileCount,
|
|
41685
|
+
lintSidecarTotalFileCount,
|
|
41686
|
+
deadCodeCacheHit: lintFailureState.didFail ? null : deadCodeCacheHit,
|
|
41687
|
+
deadCodeSummaryCacheHits: lintFailureState.didFail ? null : deadCodeSummaryCacheHits,
|
|
41688
|
+
deadCodeSummaryCacheMisses: lintFailureState.didFail ? null : deadCodeSummaryCacheMisses,
|
|
41209
41689
|
suppressedRuleCounts: transform.summarizeSuppressions()
|
|
41210
41690
|
};
|
|
41211
41691
|
}).pipe(withSpan("runInspect", { attributes: {
|
|
@@ -41846,4 +42326,4 @@ const toJsonReport = (result, options) => buildJsonReport({
|
|
|
41846
42326
|
export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
|
|
41847
42327
|
|
|
41848
42328
|
//# sourceMappingURL=index.js.map
|
|
41849
|
-
//# debugId=
|
|
42329
|
+
//# debugId=b55ef8b2-8e9d-512b-81de-a01f6ce3e60e
|