react-doctor 0.6.3-dev.ce49250 → 0.6.3-dev.ced746f
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 +842 -346
- package/dist/index.d.ts +8 -0
- package/dist/index.js +487 -63
- package/dist/lsp.js +490 -68
- package/package.json +3 -3
package/dist/cli.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]="1722becf-6e91-56ae-afc9-1e1410647bc3")}catch(e){}}();
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import * as NodeChildProcess from "node:child_process";
|
|
5
5
|
import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
|
|
@@ -9,7 +9,7 @@ import * as NFS from "node:fs";
|
|
|
9
9
|
import fs, { mkdtempSync, rmSync } from "node:fs";
|
|
10
10
|
import process$1 from "node: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, { tmpdir } from "node:os";
|
|
15
15
|
import { parseJSON5 } from "confbox";
|
|
@@ -36129,6 +36129,7 @@ const SOCKET_PACKAGE_PAGE_BASE = "https://socket.dev/npm/package";
|
|
|
36129
36129
|
const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
|
|
36130
36130
|
const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
|
|
36131
36131
|
const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
|
|
36132
|
+
const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
|
|
36132
36133
|
const CORE_PACKAGE_VERSION = "0.6.3";
|
|
36133
36134
|
const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
|
|
36134
36135
|
const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
|
|
@@ -36136,6 +36137,7 @@ const SUPPLY_CHAIN_PLUGIN = "socket";
|
|
|
36136
36137
|
const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
|
|
36137
36138
|
const SUPPLY_CHAIN_CATEGORY = "Security";
|
|
36138
36139
|
const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
|
|
36140
|
+
const SUPPLY_CHAIN_CACHE_TTL_MS = 864e5;
|
|
36139
36141
|
const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
|
|
36140
36142
|
const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
|
|
36141
36143
|
const hasIgnoredPathSegment = (relativePath) => relativePath.split(/[/\\]/).slice(0, -1).some((segment) => IGNORED_DIRECTORIES.has(segment));
|
|
@@ -38817,6 +38819,29 @@ var PerFileLintCacheEnabled = class extends Reference("react-doctor/PerFileLintC
|
|
|
38817
38819
|
return true;
|
|
38818
38820
|
} }) {};
|
|
38819
38821
|
/**
|
|
38822
|
+
* Whether the sidecar lint cache (`runners/oxlint/sidecar-lint-cache.ts`) is
|
|
38823
|
+
* active. Defaults ON — warm rescans replay the cross-file rules' cached
|
|
38824
|
+
* diagnostics for every file whose dependency fingerprint (the probe set the
|
|
38825
|
+
* plugin's collectors recorded) still matches the tree, and re-lint only the
|
|
38826
|
+
* rest. Only reachable when the per-file lint cache itself is on (the
|
|
38827
|
+
* sidecar covers its cache hits), so `REACT_DOCTOR_NO_CACHE` /
|
|
38828
|
+
* `REACT_DOCTOR_NO_FILE_CACHE` implicitly disable it too. Opt-OUT knobs:
|
|
38829
|
+
*
|
|
38830
|
+
* - `REACT_DOCTOR_NO_CACHE` — the global off-switch.
|
|
38831
|
+
* - `REACT_DOCTOR_NO_SIDECAR_CACHE` — granular rollback hatch: keep the
|
|
38832
|
+
* per-file cache but run the always-fresh sidecar over every hit
|
|
38833
|
+
* (the pre-cache behavior).
|
|
38834
|
+
*
|
|
38835
|
+
* Tests override via `Layer.succeed(SidecarLintCacheEnabled, false)`.
|
|
38836
|
+
*/
|
|
38837
|
+
var SidecarLintCacheEnabled = class extends Reference("react-doctor/SidecarLintCacheEnabled", { defaultValue: () => {
|
|
38838
|
+
const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
|
|
38839
|
+
const noSidecarCache = process.env["REACT_DOCTOR_NO_SIDECAR_CACHE"]?.toLowerCase() ?? "";
|
|
38840
|
+
if (CACHE_DISABLED_VALUES$1.has(noCache)) return false;
|
|
38841
|
+
if (CACHE_DISABLED_VALUES$1.has(noSidecarCache)) return false;
|
|
38842
|
+
return true;
|
|
38843
|
+
} }) {};
|
|
38844
|
+
/**
|
|
38820
38845
|
* Whether the whole-project dead-code result cache
|
|
38821
38846
|
* (`dead-code/dead-code-result-cache.ts`) is active. Defaults ON — a rescan
|
|
38822
38847
|
* whose inputs (source tree, manifests, configs, analyzer version) are
|
|
@@ -40584,22 +40609,37 @@ const failOpenReadJson = (filePath, fallback) => {
|
|
|
40584
40609
|
return fallback;
|
|
40585
40610
|
}
|
|
40586
40611
|
};
|
|
40612
|
+
const hashFileContents = (filePath) => {
|
|
40613
|
+
try {
|
|
40614
|
+
return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
|
|
40615
|
+
} catch {
|
|
40616
|
+
return null;
|
|
40617
|
+
}
|
|
40618
|
+
};
|
|
40587
40619
|
const ANALYZED_FILE_EXTENSIONS = new Set(DEFAULT_EXTENSIONS);
|
|
40588
40620
|
const CORE_SIDE_MANIFEST_NAMES = ["knip.json", "deno.lock"];
|
|
40589
40621
|
const ANALYZED_MANIFEST_NAMES = new Set([...ANALYZED_MANIFEST_FILENAMES, ...CORE_SIDE_MANIFEST_NAMES]);
|
|
40590
40622
|
const isTsConfigLikeFile = (fileName) => (fileName.startsWith("tsconfig") || fileName.startsWith("jsconfig")) && fileName.endsWith(".json");
|
|
40591
40623
|
const isFingerprintedFile = (fileName) => ANALYZED_FILE_EXTENSIONS.has(Path.extname(fileName).toLowerCase()) || ANALYZED_MANIFEST_NAMES.has(fileName) || isTsConfigLikeFile(fileName);
|
|
40592
|
-
|
|
40593
|
-
|
|
40624
|
+
/**
|
|
40625
|
+
* Stat snapshot of every file the analysis reads, keyed by root-relative
|
|
40626
|
+
* `/`-separated path. Taken BEFORE the (long) analysis so a stored result is
|
|
40627
|
+
* verified against the tree it started from.
|
|
40628
|
+
*/
|
|
40629
|
+
const collectAnalyzedFileStats = (rootDirectory) => {
|
|
40630
|
+
const statByRelativePath = /* @__PURE__ */ new Map();
|
|
40594
40631
|
for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) {
|
|
40595
40632
|
if (!isFingerprintedFile(name)) continue;
|
|
40596
40633
|
try {
|
|
40597
40634
|
const fileStat = NFS.statSync(absolutePath);
|
|
40598
40635
|
const relativePath = Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/");
|
|
40599
|
-
|
|
40636
|
+
statByRelativePath.set(relativePath, {
|
|
40637
|
+
mtimeMs: fileStat.mtimeMs,
|
|
40638
|
+
size: fileStat.size
|
|
40639
|
+
});
|
|
40600
40640
|
} catch {}
|
|
40601
40641
|
}
|
|
40602
|
-
return
|
|
40642
|
+
return statByRelativePath;
|
|
40603
40643
|
};
|
|
40604
40644
|
const bundledRequire$2 = createRequire(import.meta.url);
|
|
40605
40645
|
const resolveDeslopVersion = () => {
|
|
@@ -40611,34 +40651,85 @@ const resolveDeslopVersion = () => {
|
|
|
40611
40651
|
}
|
|
40612
40652
|
};
|
|
40613
40653
|
const computeDeadCodeCacheKey = (input) => crypto.createHash("sha1").update(JSON.stringify({
|
|
40614
|
-
schemaVersion:
|
|
40654
|
+
schemaVersion: 2,
|
|
40615
40655
|
coreVersion: input.coreVersion,
|
|
40616
40656
|
deslopVersion: resolveDeslopVersion(),
|
|
40617
40657
|
deslopJsModuleSpecifier: input.deslopJsModuleSpecifier,
|
|
40618
40658
|
entryPatterns: input.entryPatterns,
|
|
40619
40659
|
ignorePatterns: input.ignorePatterns,
|
|
40620
|
-
tsConfigFile: input.tsConfigPath === void 0 ? null : Path.relative(input.rootDirectory, input.tsConfigPath).replace(/\\/g, "/")
|
|
40621
|
-
files: collectAnalyzedFileFingerprints(input.rootDirectory)
|
|
40660
|
+
tsConfigFile: input.tsConfigPath === void 0 ? null : Path.relative(input.rootDirectory, input.tsConfigPath).replace(/\\/g, "/")
|
|
40622
40661
|
})).digest("hex");
|
|
40623
|
-
const validateDiagnostic$
|
|
40662
|
+
const validateDiagnostic$2 = decodeUnknownSync(Diagnostic);
|
|
40624
40663
|
const decodeCachedDiagnostics = (raw) => {
|
|
40625
40664
|
try {
|
|
40626
|
-
for (const entry of raw) validateDiagnostic$
|
|
40665
|
+
for (const entry of raw) validateDiagnostic$2(entry);
|
|
40627
40666
|
return raw;
|
|
40628
40667
|
} catch {
|
|
40629
40668
|
return null;
|
|
40630
40669
|
}
|
|
40631
40670
|
};
|
|
40632
|
-
const
|
|
40633
|
-
|
|
40634
|
-
|
|
40635
|
-
|
|
40671
|
+
const isPersistedFileIdentity = (value) => Array.isArray(value) && value.length === 3 && typeof value[0] === "number" && typeof value[1] === "number" && typeof value[2] === "string";
|
|
40672
|
+
const lookupDeadCodeResultCache = (input) => {
|
|
40673
|
+
const cacheFilePath = Path.join(input.cacheDirectory, DEAD_CODE_CACHE_FILENAME);
|
|
40674
|
+
const persisted = failOpenReadJson(cacheFilePath, null);
|
|
40675
|
+
if (persisted === null || !isRecord$1(persisted) || persisted.version !== 2 || persisted.key !== input.cacheKey || !isRecord$1(persisted.files) || !Array.isArray(persisted.diagnostics)) return null;
|
|
40676
|
+
const storedFileEntries = Object.entries(persisted.files);
|
|
40677
|
+
if (storedFileEntries.length !== input.currentFileStats.size) return null;
|
|
40678
|
+
const repairedFiles = {};
|
|
40679
|
+
let repairedCount = 0;
|
|
40680
|
+
for (const [relativePath, storedIdentity] of storedFileEntries) {
|
|
40681
|
+
if (!isPersistedFileIdentity(storedIdentity)) return null;
|
|
40682
|
+
const currentStat = input.currentFileStats.get(relativePath);
|
|
40683
|
+
if (currentStat === void 0) return null;
|
|
40684
|
+
const [storedMtimeMs, storedSize, storedContentHash] = storedIdentity;
|
|
40685
|
+
if (currentStat.mtimeMs === storedMtimeMs && currentStat.size === storedSize) {
|
|
40686
|
+
repairedFiles[relativePath] = storedIdentity;
|
|
40687
|
+
continue;
|
|
40688
|
+
}
|
|
40689
|
+
if (currentStat.size !== storedSize) return null;
|
|
40690
|
+
const currentContentHash = hashFileContents(Path.join(input.rootDirectory, relativePath));
|
|
40691
|
+
if (currentContentHash === null || currentContentHash !== storedContentHash) return null;
|
|
40692
|
+
repairedFiles[relativePath] = [
|
|
40693
|
+
currentStat.mtimeMs,
|
|
40694
|
+
currentStat.size,
|
|
40695
|
+
storedContentHash
|
|
40696
|
+
];
|
|
40697
|
+
repairedCount += 1;
|
|
40698
|
+
}
|
|
40699
|
+
const diagnostics = decodeCachedDiagnostics(persisted.diagnostics);
|
|
40700
|
+
if (diagnostics === null) return null;
|
|
40701
|
+
if (repairedCount > 0) atomicWriteJson(cacheFilePath, {
|
|
40702
|
+
version: 2,
|
|
40703
|
+
key: input.cacheKey,
|
|
40704
|
+
files: repairedFiles,
|
|
40705
|
+
diagnostics: persisted.diagnostics
|
|
40706
|
+
});
|
|
40707
|
+
return diagnostics;
|
|
40636
40708
|
};
|
|
40637
|
-
const storeDeadCodeResultCache = (
|
|
40638
|
-
|
|
40639
|
-
|
|
40640
|
-
|
|
40641
|
-
|
|
40709
|
+
const storeDeadCodeResultCache = (input) => {
|
|
40710
|
+
const persistedFiles = {};
|
|
40711
|
+
for (const [relativePath, snapshotStat] of input.snapshotFileStats) {
|
|
40712
|
+
const absolutePath = Path.join(input.rootDirectory, relativePath);
|
|
40713
|
+
const contentHash = hashFileContents(absolutePath);
|
|
40714
|
+
if (contentHash === null) return;
|
|
40715
|
+
let currentStat;
|
|
40716
|
+
try {
|
|
40717
|
+
currentStat = NFS.statSync(absolutePath);
|
|
40718
|
+
} catch {
|
|
40719
|
+
return;
|
|
40720
|
+
}
|
|
40721
|
+
if (currentStat.mtimeMs !== snapshotStat.mtimeMs || currentStat.size !== snapshotStat.size) return;
|
|
40722
|
+
persistedFiles[relativePath] = [
|
|
40723
|
+
snapshotStat.mtimeMs,
|
|
40724
|
+
snapshotStat.size,
|
|
40725
|
+
contentHash
|
|
40726
|
+
];
|
|
40727
|
+
}
|
|
40728
|
+
atomicWriteJson(Path.join(input.cacheDirectory, DEAD_CODE_CACHE_FILENAME), {
|
|
40729
|
+
version: 2,
|
|
40730
|
+
key: input.cacheKey,
|
|
40731
|
+
files: persistedFiles,
|
|
40732
|
+
diagnostics: input.diagnostics
|
|
40642
40733
|
});
|
|
40643
40734
|
};
|
|
40644
40735
|
const readSystemFacts = () => ({
|
|
@@ -41048,16 +41139,22 @@ const checkDeadCode = async (options) => {
|
|
|
41048
41139
|
const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
|
|
41049
41140
|
const tsConfigPath = resolveTsConfigPath(rootDirectory);
|
|
41050
41141
|
const deslopJsModuleSpecifier = options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js");
|
|
41051
|
-
const
|
|
41142
|
+
const fileStatsSnapshot = options.cacheEnabled ? collectAnalyzedFileStats(rootDirectory) : null;
|
|
41143
|
+
const cacheKey = fileStatsSnapshot === null ? null : computeDeadCodeCacheKey({
|
|
41052
41144
|
rootDirectory,
|
|
41053
41145
|
entryPatterns,
|
|
41054
41146
|
ignorePatterns,
|
|
41055
41147
|
tsConfigPath,
|
|
41056
41148
|
deslopJsModuleSpecifier,
|
|
41057
41149
|
coreVersion: CORE_PACKAGE_VERSION
|
|
41058
|
-
})
|
|
41059
|
-
if (cacheKey !== null) {
|
|
41060
|
-
const cachedDiagnostics = lookupDeadCodeResultCache(
|
|
41150
|
+
});
|
|
41151
|
+
if (cacheKey !== null && fileStatsSnapshot !== null) {
|
|
41152
|
+
const cachedDiagnostics = lookupDeadCodeResultCache({
|
|
41153
|
+
cacheDirectory: resolveReactDoctorCacheDir(rootDirectory),
|
|
41154
|
+
cacheKey,
|
|
41155
|
+
rootDirectory,
|
|
41156
|
+
currentFileStats: fileStatsSnapshot
|
|
41157
|
+
});
|
|
41061
41158
|
options.onCacheOutcome?.(cachedDiagnostics !== null);
|
|
41062
41159
|
if (cachedDiagnostics !== null) return [...cachedDiagnostics];
|
|
41063
41160
|
}
|
|
@@ -41131,7 +41228,13 @@ const checkDeadCode = async (options) => {
|
|
|
41131
41228
|
category: DEAD_CODE_CATEGORY
|
|
41132
41229
|
});
|
|
41133
41230
|
}
|
|
41134
|
-
if (cacheKey !== null) storeDeadCodeResultCache(
|
|
41231
|
+
if (cacheKey !== null && fileStatsSnapshot !== null) storeDeadCodeResultCache({
|
|
41232
|
+
cacheDirectory: resolveReactDoctorCacheDir(rootDirectory),
|
|
41233
|
+
cacheKey,
|
|
41234
|
+
rootDirectory,
|
|
41235
|
+
snapshotFileStats: fileStatsSnapshot,
|
|
41236
|
+
diagnostics
|
|
41237
|
+
});
|
|
41135
41238
|
return diagnostics;
|
|
41136
41239
|
};
|
|
41137
41240
|
const DEAD_CODE_RULE_KEY_PREFIX = `${DEAD_CODE_PLUGIN}/`;
|
|
@@ -42113,7 +42216,7 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
|
|
|
42113
42216
|
}
|
|
42114
42217
|
return enabled;
|
|
42115
42218
|
};
|
|
42116
|
-
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection }) => {
|
|
42219
|
+
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection, sidecarRuleIdFilter }) => {
|
|
42117
42220
|
const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
|
|
42118
42221
|
const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
|
|
42119
42222
|
const jsPlugins = [];
|
|
@@ -42125,6 +42228,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
42125
42228
|
if (!rule) continue;
|
|
42126
42229
|
if (ruleSelection === "cacheable" && CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
|
|
42127
42230
|
if (ruleSelection === "sidecar" && !CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
|
|
42231
|
+
if (ruleSelection === "sidecar" && sidecarRuleIdFilter !== void 0 && !sidecarRuleIdFilter.has(registryEntry.id)) continue;
|
|
42128
42232
|
if (rule.scan !== void 0) continue;
|
|
42129
42233
|
if (customRulesOnly && registryEntry.originallyExternal) continue;
|
|
42130
42234
|
if (rule.framework !== "global" && !rule.requires) continue;
|
|
@@ -42169,23 +42273,23 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
42169
42273
|
}
|
|
42170
42274
|
};
|
|
42171
42275
|
};
|
|
42172
|
-
const validateDiagnostic = decodeUnknownSync(Diagnostic);
|
|
42276
|
+
const validateDiagnostic$1 = decodeUnknownSync(Diagnostic);
|
|
42173
42277
|
const decodeFileDiagnostics = (raw) => {
|
|
42174
42278
|
if (!Array.isArray(raw)) return null;
|
|
42175
42279
|
try {
|
|
42176
|
-
for (const entry of raw) validateDiagnostic(entry);
|
|
42280
|
+
for (const entry of raw) validateDiagnostic$1(entry);
|
|
42177
42281
|
return raw;
|
|
42178
42282
|
} catch {
|
|
42179
42283
|
return null;
|
|
42180
42284
|
}
|
|
42181
42285
|
};
|
|
42182
|
-
const emptyCache = () => ({
|
|
42286
|
+
const emptyCache$1 = () => ({
|
|
42183
42287
|
version: 1,
|
|
42184
42288
|
rulesets: {}
|
|
42185
42289
|
});
|
|
42186
|
-
const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
|
|
42290
|
+
const loadRulesetEntries$1 = (cacheFilePath, rulesetHash) => {
|
|
42187
42291
|
const entries = /* @__PURE__ */ new Map();
|
|
42188
|
-
const persisted = failOpenReadJson(cacheFilePath, emptyCache());
|
|
42292
|
+
const persisted = failOpenReadJson(cacheFilePath, emptyCache$1());
|
|
42189
42293
|
if (persisted.version !== 1 || !isRecord$1(persisted.rulesets)) return entries;
|
|
42190
42294
|
const bucket = persisted.rulesets[rulesetHash];
|
|
42191
42295
|
if (!isRecord$1(bucket) || !isRecord$1(bucket.files)) return entries;
|
|
@@ -42197,7 +42301,7 @@ const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
|
|
|
42197
42301
|
};
|
|
42198
42302
|
const createFileLintCache = (cacheDirectory, rulesetHash) => {
|
|
42199
42303
|
const cacheFilePath = Path.join(cacheDirectory, FILE_LINT_CACHE_FILENAME);
|
|
42200
|
-
const entries = loadRulesetEntries(cacheFilePath, rulesetHash);
|
|
42304
|
+
const entries = loadRulesetEntries$1(cacheFilePath, rulesetHash);
|
|
42201
42305
|
return {
|
|
42202
42306
|
lookup: (fileKey) => entries.get(fileKey) ?? null,
|
|
42203
42307
|
store: (fileKey, diagnostics) => {
|
|
@@ -42205,7 +42309,7 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
|
|
|
42205
42309
|
entries.set(fileKey, diagnostics);
|
|
42206
42310
|
},
|
|
42207
42311
|
persist: () => {
|
|
42208
|
-
const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
|
|
42312
|
+
const onDisk = failOpenReadJson(cacheFilePath, emptyCache$1());
|
|
42209
42313
|
const rulesets = onDisk.version === 1 && isRecord$1(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
|
|
42210
42314
|
const existingBucket = rulesets[rulesetHash];
|
|
42211
42315
|
const existingFiles = isRecord$1(existingBucket) && isRecord$1(existingBucket.files) ? existingBucket.files : {};
|
|
@@ -42226,6 +42330,135 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
|
|
|
42226
42330
|
}
|
|
42227
42331
|
};
|
|
42228
42332
|
};
|
|
42333
|
+
const ABSENT_CONTENT_ANSWER = "absent";
|
|
42334
|
+
/**
|
|
42335
|
+
* Answers dependency probes for the sidecar lint cache — used both when
|
|
42336
|
+
* STORING an entry (turn the collector's probe paths into answers) and when
|
|
42337
|
+
* VERIFYING one (re-answer each stored probe against the current tree).
|
|
42338
|
+
* Store and verify must agree exactly, so both go through this one resolver.
|
|
42339
|
+
*
|
|
42340
|
+
* - `content` — SHA-1 of the file's bytes, `"absent"` when unreadable
|
|
42341
|
+
* (missing, a directory, permission failure). Source files reuse the
|
|
42342
|
+
* hashes the cache-partition loop already computed
|
|
42343
|
+
* (`contentHashByRelativePath`); everything else (package.json,
|
|
42344
|
+
* tsconfig, ignored files) is hashed on demand.
|
|
42345
|
+
* - `exists` — the existence class a module-resolution candidate probe
|
|
42346
|
+
* sees: `"file"`, `"dir"`, or `"none"` (missing or an exotic entry —
|
|
42347
|
+
* `statSync` follows symlinks, matching the resolvers).
|
|
42348
|
+
*
|
|
42349
|
+
* Answers are memoized per scan: the filesystem is frozen for the run, and
|
|
42350
|
+
* hot dependency paths (a barrel imported by hundreds of files, the shared
|
|
42351
|
+
* tsconfig chain) repeat across many files' probe sets.
|
|
42352
|
+
*/
|
|
42353
|
+
const createSidecarProbeAnswerResolver = (input) => {
|
|
42354
|
+
const contentAnswerByPath = /* @__PURE__ */ new Map();
|
|
42355
|
+
const existsAnswerByPath = /* @__PURE__ */ new Map();
|
|
42356
|
+
const toRelativePath = (absolutePath) => Path.relative(input.rootDirectory, absolutePath).replaceAll("\\", "/");
|
|
42357
|
+
const contentAnswer = (relativePath) => {
|
|
42358
|
+
const memoized = contentAnswerByPath.get(relativePath);
|
|
42359
|
+
if (memoized !== void 0) return memoized;
|
|
42360
|
+
const answer = input.contentHashByRelativePath.get(relativePath) ?? hashFileContents(Path.resolve(input.rootDirectory, relativePath)) ?? ABSENT_CONTENT_ANSWER;
|
|
42361
|
+
contentAnswerByPath.set(relativePath, answer);
|
|
42362
|
+
return answer;
|
|
42363
|
+
};
|
|
42364
|
+
const existsAnswer = (relativePath) => {
|
|
42365
|
+
const memoized = existsAnswerByPath.get(relativePath);
|
|
42366
|
+
if (memoized !== void 0) return memoized;
|
|
42367
|
+
let answer;
|
|
42368
|
+
try {
|
|
42369
|
+
const stat = NFS.statSync(Path.resolve(input.rootDirectory, relativePath));
|
|
42370
|
+
answer = stat.isFile() ? "file" : stat.isDirectory() ? "dir" : "none";
|
|
42371
|
+
} catch {
|
|
42372
|
+
answer = "none";
|
|
42373
|
+
}
|
|
42374
|
+
existsAnswerByPath.set(relativePath, answer);
|
|
42375
|
+
return answer;
|
|
42376
|
+
};
|
|
42377
|
+
return {
|
|
42378
|
+
answerFor: (kind, relativePath) => kind === "content" ? contentAnswer(relativePath) : existsAnswer(relativePath),
|
|
42379
|
+
toRelativePath
|
|
42380
|
+
};
|
|
42381
|
+
};
|
|
42382
|
+
const validateDiagnostic = decodeUnknownSync(Diagnostic);
|
|
42383
|
+
const isProbeKind = (value) => value === "content" || value === "exists";
|
|
42384
|
+
const decodeSidecarFileEntry = (raw) => {
|
|
42385
|
+
if (!isRecord$1(raw) || !Array.isArray(raw.probes) || !Array.isArray(raw.diagnostics)) return null;
|
|
42386
|
+
const probes = [];
|
|
42387
|
+
for (const rawProbe of raw.probes) {
|
|
42388
|
+
if (!Array.isArray(rawProbe) || rawProbe.length !== 3) return null;
|
|
42389
|
+
const [kind, probePath, answer] = rawProbe;
|
|
42390
|
+
if (!isProbeKind(kind) || typeof probePath !== "string" || typeof answer !== "string") return null;
|
|
42391
|
+
probes.push({
|
|
42392
|
+
kind,
|
|
42393
|
+
path: probePath,
|
|
42394
|
+
answer
|
|
42395
|
+
});
|
|
42396
|
+
}
|
|
42397
|
+
try {
|
|
42398
|
+
for (const entry of raw.diagnostics) validateDiagnostic(entry);
|
|
42399
|
+
} catch {
|
|
42400
|
+
return null;
|
|
42401
|
+
}
|
|
42402
|
+
return {
|
|
42403
|
+
probes,
|
|
42404
|
+
diagnostics: raw.diagnostics
|
|
42405
|
+
};
|
|
42406
|
+
};
|
|
42407
|
+
const encodeSidecarFileEntry = (entry) => ({
|
|
42408
|
+
probes: entry.probes.map((probe) => [
|
|
42409
|
+
probe.kind,
|
|
42410
|
+
probe.path,
|
|
42411
|
+
probe.answer
|
|
42412
|
+
]),
|
|
42413
|
+
diagnostics: entry.diagnostics
|
|
42414
|
+
});
|
|
42415
|
+
const emptyCache = () => ({
|
|
42416
|
+
version: 1,
|
|
42417
|
+
rulesets: {}
|
|
42418
|
+
});
|
|
42419
|
+
const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
|
|
42420
|
+
const entries = /* @__PURE__ */ new Map();
|
|
42421
|
+
const persisted = failOpenReadJson(cacheFilePath, emptyCache());
|
|
42422
|
+
if (persisted.version !== 1 || !isRecord$1(persisted.rulesets)) return entries;
|
|
42423
|
+
const bucket = persisted.rulesets[rulesetHash];
|
|
42424
|
+
if (!isRecord$1(bucket) || !isRecord$1(bucket.files)) return entries;
|
|
42425
|
+
for (const [fileKey, rawEntry] of Object.entries(bucket.files)) {
|
|
42426
|
+
const decoded = decodeSidecarFileEntry(rawEntry);
|
|
42427
|
+
if (decoded !== null) entries.set(fileKey, decoded);
|
|
42428
|
+
}
|
|
42429
|
+
return entries;
|
|
42430
|
+
};
|
|
42431
|
+
const createSidecarLintCache = (cacheDirectory, rulesetHash) => {
|
|
42432
|
+
const cacheFilePath = Path.join(cacheDirectory, SIDECAR_LINT_CACHE_FILENAME);
|
|
42433
|
+
const entries = loadRulesetEntries(cacheFilePath, rulesetHash);
|
|
42434
|
+
return {
|
|
42435
|
+
lookup: (fileKey) => entries.get(fileKey) ?? null,
|
|
42436
|
+
store: (fileKey, entry) => {
|
|
42437
|
+
entries.delete(fileKey);
|
|
42438
|
+
entries.set(fileKey, entry);
|
|
42439
|
+
},
|
|
42440
|
+
persist: () => {
|
|
42441
|
+
const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
|
|
42442
|
+
const rulesets = onDisk.version === 1 && isRecord$1(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
|
|
42443
|
+
const existingBucket = rulesets[rulesetHash];
|
|
42444
|
+
const existingFiles = isRecord$1(existingBucket) && isRecord$1(existingBucket.files) ? existingBucket.files : {};
|
|
42445
|
+
const ourFiles = {};
|
|
42446
|
+
for (const [fileKey, entry] of entries) ourFiles[fileKey] = encodeSidecarFileEntry(entry);
|
|
42447
|
+
const cappedEntries = Object.entries({
|
|
42448
|
+
...existingFiles,
|
|
42449
|
+
...ourFiles
|
|
42450
|
+
}).slice(-FILE_LINT_CACHE_MAX_FILE_COUNT);
|
|
42451
|
+
rulesets[rulesetHash] = {
|
|
42452
|
+
updatedAtMs: Date.now(),
|
|
42453
|
+
files: Object.fromEntries(cappedEntries)
|
|
42454
|
+
};
|
|
42455
|
+
atomicWriteJson(cacheFilePath, {
|
|
42456
|
+
version: 1,
|
|
42457
|
+
rulesets: Object.fromEntries(Object.entries(rulesets).filter(([, bucket]) => isRecord$1(bucket) && typeof bucket.updatedAtMs === "number").sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8))
|
|
42458
|
+
});
|
|
42459
|
+
}
|
|
42460
|
+
};
|
|
42461
|
+
};
|
|
42229
42462
|
const bundledRequire$3 = createRequire(import.meta.url);
|
|
42230
42463
|
const childNodeVersionByBinaryPath = /* @__PURE__ */ new Map();
|
|
42231
42464
|
const resolveChildNodeVersion = (nodeBinaryPath) => {
|
|
@@ -43225,13 +43458,6 @@ const validateRuleRegistration = () => {
|
|
|
43225
43458
|
].filter((entry) => entry !== null).join("; ");
|
|
43226
43459
|
console.warn(`[react-doctor] rule-registration drift: ${detail}`);
|
|
43227
43460
|
};
|
|
43228
|
-
const hashFileContents = (filePath) => {
|
|
43229
|
-
try {
|
|
43230
|
-
return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
|
|
43231
|
-
} catch {
|
|
43232
|
-
return null;
|
|
43233
|
-
}
|
|
43234
|
-
};
|
|
43235
43461
|
/**
|
|
43236
43462
|
* Balanced LPT (longest-processing-time-first) lint batch planner. Where
|
|
43237
43463
|
* `batchIncludePaths` greedily fills 100-file batches and leaves a small
|
|
@@ -43313,6 +43539,106 @@ const writeOxlintConfig = (configPath, configToWrite) => {
|
|
|
43313
43539
|
NFS.closeSync(fileHandle);
|
|
43314
43540
|
}
|
|
43315
43541
|
};
|
|
43542
|
+
/**
|
|
43543
|
+
* Attributes diagnostics back to the file that produced them by the
|
|
43544
|
+
* normalized path oxlint echoes. Returns `null` when ANY diagnostic can't be
|
|
43545
|
+
* attributed — the path forms don't align, so the caller must skip its cache
|
|
43546
|
+
* store rather than risk caching a wrong empty result for a file that
|
|
43547
|
+
* actually had diagnostics.
|
|
43548
|
+
*/
|
|
43549
|
+
const attributeDiagnosticsToFiles = (diagnostics, files) => {
|
|
43550
|
+
const fileByNormalizedPath = /* @__PURE__ */ new Map();
|
|
43551
|
+
for (const file of files) fileByNormalizedPath.set(file.replaceAll("\\", "/"), file);
|
|
43552
|
+
const diagnosticsByFile = /* @__PURE__ */ new Map();
|
|
43553
|
+
for (const diagnostic of diagnostics) {
|
|
43554
|
+
const file = fileByNormalizedPath.get(diagnostic.filePath);
|
|
43555
|
+
if (file === void 0) return null;
|
|
43556
|
+
const fileDiagnostics = diagnosticsByFile.get(file) ?? [];
|
|
43557
|
+
fileDiagnostics.push(diagnostic);
|
|
43558
|
+
diagnosticsByFile.set(file, fileDiagnostics);
|
|
43559
|
+
}
|
|
43560
|
+
return diagnosticsByFile;
|
|
43561
|
+
};
|
|
43562
|
+
/**
|
|
43563
|
+
* Collects each file's cross-file dependency probes (via the plugin's
|
|
43564
|
+
* collectors) with their current answers. Fail-open per file: an unreadable /
|
|
43565
|
+
* unparseable / collector-failing file maps to `null` (no entry stored — it
|
|
43566
|
+
* re-lints next scan). The filesystem is frozen for the scan, so this can run
|
|
43567
|
+
* CONCURRENTLY with an oxlint child-process await — the caller starts it
|
|
43568
|
+
* unawaited before a lint pass and awaits it at store time, hiding the
|
|
43569
|
+
* in-process parse + resolution cost under the subprocess wait. The loop
|
|
43570
|
+
* yields on the cooperative time budget so the child's stdout handling (and
|
|
43571
|
+
* sibling fibers) keep flowing; each per-file collection is synchronous, so
|
|
43572
|
+
* the module-level probe recorder never sees interleaved use. Never rejects.
|
|
43573
|
+
*/
|
|
43574
|
+
const collectSidecarProbesForFiles = async (input) => {
|
|
43575
|
+
const buildProbes = (file) => {
|
|
43576
|
+
const absoluteFilePath = Path.resolve(input.rootDirectory, file);
|
|
43577
|
+
let trace;
|
|
43578
|
+
try {
|
|
43579
|
+
trace = collectCrossFileDependencyProbes({
|
|
43580
|
+
absoluteFilePath,
|
|
43581
|
+
sourceText: NFS.readFileSync(absoluteFilePath, "utf8"),
|
|
43582
|
+
ruleIds: input.boundedSidecarRuleIds
|
|
43583
|
+
});
|
|
43584
|
+
} catch {
|
|
43585
|
+
return null;
|
|
43586
|
+
}
|
|
43587
|
+
if (trace === null) return null;
|
|
43588
|
+
const probes = [];
|
|
43589
|
+
for (const contentPath of trace.contentPaths) {
|
|
43590
|
+
const relativePath = input.probeAnswers.toRelativePath(contentPath);
|
|
43591
|
+
probes.push({
|
|
43592
|
+
kind: "content",
|
|
43593
|
+
path: relativePath,
|
|
43594
|
+
answer: input.probeAnswers.answerFor("content", relativePath)
|
|
43595
|
+
});
|
|
43596
|
+
}
|
|
43597
|
+
for (const existencePath of trace.existencePaths) {
|
|
43598
|
+
const relativePath = input.probeAnswers.toRelativePath(existencePath);
|
|
43599
|
+
probes.push({
|
|
43600
|
+
kind: "exists",
|
|
43601
|
+
path: relativePath,
|
|
43602
|
+
answer: input.probeAnswers.answerFor("exists", relativePath)
|
|
43603
|
+
});
|
|
43604
|
+
}
|
|
43605
|
+
return probes;
|
|
43606
|
+
};
|
|
43607
|
+
const probesByFile = /* @__PURE__ */ new Map();
|
|
43608
|
+
let collectSliceStartedAt = performance.now();
|
|
43609
|
+
for (const file of input.files) {
|
|
43610
|
+
probesByFile.set(file, buildProbes(file));
|
|
43611
|
+
if (performance.now() - collectSliceStartedAt >= 12) {
|
|
43612
|
+
await yieldToEventLoop();
|
|
43613
|
+
collectSliceStartedAt = performance.now();
|
|
43614
|
+
}
|
|
43615
|
+
}
|
|
43616
|
+
return probesByFile;
|
|
43617
|
+
};
|
|
43618
|
+
/**
|
|
43619
|
+
* Stores each freshly-linted file's probe set + cross-file diagnostics so
|
|
43620
|
+
* the next scan can replay instead of re-linting.
|
|
43621
|
+
*/
|
|
43622
|
+
const storeSidecarEntries = (input) => {
|
|
43623
|
+
let didStoreAnyEntry = false;
|
|
43624
|
+
for (const pass of input.storablePasses) {
|
|
43625
|
+
if (!pass.isTrusted) continue;
|
|
43626
|
+
const diagnosticsByFile = attributeDiagnosticsToFiles(pass.diagnostics, pass.files);
|
|
43627
|
+
if (diagnosticsByFile === null) continue;
|
|
43628
|
+
for (const file of pass.files) {
|
|
43629
|
+
const cacheKey = input.cacheKeyByFile.get(file);
|
|
43630
|
+
if (cacheKey === void 0) continue;
|
|
43631
|
+
const probes = pass.probesByFile.get(file);
|
|
43632
|
+
if (probes === void 0 || probes === null) continue;
|
|
43633
|
+
input.sidecarCache.store(cacheKey, {
|
|
43634
|
+
probes,
|
|
43635
|
+
diagnostics: diagnosticsByFile.get(file) ?? []
|
|
43636
|
+
});
|
|
43637
|
+
didStoreAnyEntry = true;
|
|
43638
|
+
}
|
|
43639
|
+
}
|
|
43640
|
+
if (didStoreAnyEntry) input.sidecarCache.persist();
|
|
43641
|
+
};
|
|
43316
43642
|
const REACT_HOOKS_JS_DROP_PREFIX = "React Compiler rules (react-hooks-js/*) skipped — eslint-plugin-react-hooks failed to load in this environment";
|
|
43317
43643
|
/**
|
|
43318
43644
|
* Detects an oxlint config-load crash caused by the optional
|
|
@@ -43354,7 +43680,7 @@ const reactHooksJsPluginDropNote = (error) => {
|
|
|
43354
43680
|
* 6. always restore disable directives + clean up the temp dir
|
|
43355
43681
|
*/
|
|
43356
43682
|
const runOxlint = async (options) => {
|
|
43357
|
-
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;
|
|
43683
|
+
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;
|
|
43358
43684
|
const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
|
|
43359
43685
|
const severityControls = buildRuleSeverityControls(userConfig);
|
|
43360
43686
|
validateRuleRegistration();
|
|
@@ -43372,7 +43698,8 @@ const runOxlint = async (options) => {
|
|
|
43372
43698
|
severityControls,
|
|
43373
43699
|
userPlugins,
|
|
43374
43700
|
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin,
|
|
43375
|
-
ruleSelection: overrides.ruleSelection
|
|
43701
|
+
ruleSelection: overrides.ruleSelection,
|
|
43702
|
+
sidecarRuleIdFilter: overrides.sidecarRuleIdFilter
|
|
43376
43703
|
});
|
|
43377
43704
|
const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
|
|
43378
43705
|
const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
|
|
@@ -43475,15 +43802,18 @@ const runOxlint = async (options) => {
|
|
|
43475
43802
|
});
|
|
43476
43803
|
const cache = createFileLintCache(resolveReactDoctorCacheDir(rootDirectory), rulesetHash);
|
|
43477
43804
|
const cacheKeyByFile = /* @__PURE__ */ new Map();
|
|
43805
|
+
const contentHashByRelativePath = /* @__PURE__ */ new Map();
|
|
43478
43806
|
const missFiles = [];
|
|
43479
43807
|
const replayedDiagnostics = [];
|
|
43480
43808
|
let hashSliceStartedAt = performance.now();
|
|
43481
43809
|
for (const candidateFile of candidateFiles) {
|
|
43482
|
-
const
|
|
43810
|
+
const absoluteCandidatePath = Path.resolve(rootDirectory, candidateFile);
|
|
43811
|
+
const contentHash = hashFileContents(absoluteCandidatePath);
|
|
43483
43812
|
if (contentHash === null) {
|
|
43484
43813
|
missFiles.push(candidateFile);
|
|
43485
43814
|
continue;
|
|
43486
43815
|
}
|
|
43816
|
+
contentHashByRelativePath.set(Path.relative(rootDirectory, absoluteCandidatePath).replaceAll("\\", "/"), contentHash);
|
|
43487
43817
|
const cacheKey = `${candidateFile.replaceAll("\\", "/")}\u0000${contentHash}`;
|
|
43488
43818
|
cacheKeyByFile.set(candidateFile, cacheKey);
|
|
43489
43819
|
const cachedDiagnostics = cache.lookup(cacheKey);
|
|
@@ -43495,43 +43825,111 @@ const runOxlint = async (options) => {
|
|
|
43495
43825
|
}
|
|
43496
43826
|
}
|
|
43497
43827
|
const cacheHitFileCount = candidateFiles.length - missFiles.length;
|
|
43828
|
+
const enabledSidecarRuleIds = Object.keys(buildConfig({
|
|
43829
|
+
extendsPaths: [],
|
|
43830
|
+
ruleSelection: "sidecar"
|
|
43831
|
+
}).rules).map((ruleKey) => ruleKey.slice(ruleKey.lastIndexOf("/") + 1)).filter((ruleId) => CROSS_FILE_RULE_IDS.has(ruleId));
|
|
43832
|
+
const boundedSidecarRuleIds = enabledSidecarRuleIds.filter((ruleId) => CROSS_FILE_DEPENDENCY_COLLECTORS.has(ruleId));
|
|
43833
|
+
const boundedSidecarRuleIdSet = new Set(boundedSidecarRuleIds);
|
|
43834
|
+
const unboundedSidecarRuleIds = enabledSidecarRuleIds.filter((ruleId) => !CROSS_FILE_DEPENDENCY_COLLECTORS.has(ruleId));
|
|
43835
|
+
const useSidecarCache = sidecarLintCacheEnabled && boundedSidecarRuleIds.length > 0;
|
|
43836
|
+
const sidecarCache = useSidecarCache ? createSidecarLintCache(resolveReactDoctorCacheDir(rootDirectory), computeRulesetHash({
|
|
43837
|
+
config: buildConfig({
|
|
43838
|
+
extendsPaths: [],
|
|
43839
|
+
ruleSelection: "sidecar",
|
|
43840
|
+
sidecarRuleIdFilter: boundedSidecarRuleIdSet
|
|
43841
|
+
}),
|
|
43842
|
+
toolchainVersions: resolveOxlintToolchainVersions(nodeBinaryPath),
|
|
43843
|
+
ignorePatterns: combinedPatterns,
|
|
43844
|
+
tsconfigContent
|
|
43845
|
+
})) : null;
|
|
43846
|
+
const probeAnswers = createSidecarProbeAnswerResolver({
|
|
43847
|
+
rootDirectory,
|
|
43848
|
+
contentHashByRelativePath
|
|
43849
|
+
});
|
|
43850
|
+
const missProbesTask = sidecarCache === null ? null : collectSidecarProbesForFiles({
|
|
43851
|
+
files: missFiles,
|
|
43852
|
+
rootDirectory,
|
|
43853
|
+
boundedSidecarRuleIds,
|
|
43854
|
+
probeAnswers
|
|
43855
|
+
});
|
|
43498
43856
|
const fullResult = await runConfigOverFiles((overrides) => buildConfig({
|
|
43499
43857
|
extendsPaths: [],
|
|
43500
43858
|
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
|
|
43501
43859
|
}), "oxlintrc.full.json", missFiles, options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(scannedFileCount, candidateFiles.length)));
|
|
43502
43860
|
const missFileSet = new Set(missFiles);
|
|
43503
|
-
const
|
|
43861
|
+
const hitFiles = candidateFiles.filter((candidateFile) => !missFileSet.has(candidateFile));
|
|
43862
|
+
const sidecarReplayedDiagnostics = [];
|
|
43863
|
+
const sidecarLintFiles = [];
|
|
43864
|
+
if (sidecarCache === null) sidecarLintFiles.push(...hitFiles);
|
|
43865
|
+
else {
|
|
43866
|
+
let verifySliceStartedAt = performance.now();
|
|
43867
|
+
for (const hitFile of hitFiles) {
|
|
43868
|
+
const cacheKey = cacheKeyByFile.get(hitFile);
|
|
43869
|
+
const entry = cacheKey === void 0 ? null : sidecarCache.lookup(cacheKey);
|
|
43870
|
+
if (entry !== null && entry.probes.every((probe) => probeAnswers.answerFor(probe.kind, probe.path) === probe.answer)) sidecarReplayedDiagnostics.push(...entry.diagnostics);
|
|
43871
|
+
else sidecarLintFiles.push(hitFile);
|
|
43872
|
+
if (performance.now() - verifySliceStartedAt >= 12) {
|
|
43873
|
+
await yieldToEventLoop();
|
|
43874
|
+
verifySliceStartedAt = performance.now();
|
|
43875
|
+
}
|
|
43876
|
+
}
|
|
43877
|
+
}
|
|
43878
|
+
const sidecarProbesTask = sidecarCache === null ? null : collectSidecarProbesForFiles({
|
|
43879
|
+
files: sidecarLintFiles,
|
|
43880
|
+
rootDirectory,
|
|
43881
|
+
boundedSidecarRuleIds,
|
|
43882
|
+
probeAnswers
|
|
43883
|
+
});
|
|
43884
|
+
const sidecarReplayedFileCount = hitFiles.length - sidecarLintFiles.length;
|
|
43885
|
+
if (sidecarReplayedFileCount > 0) options.onFileProgress?.(missFiles.length + sidecarReplayedFileCount, candidateFiles.length);
|
|
43886
|
+
const boundedSidecarResult = await runConfigOverFiles(() => buildConfig({
|
|
43504
43887
|
extendsPaths: [],
|
|
43505
|
-
ruleSelection: "sidecar"
|
|
43506
|
-
|
|
43888
|
+
ruleSelection: "sidecar",
|
|
43889
|
+
...useSidecarCache ? { sidecarRuleIdFilter: boundedSidecarRuleIdSet } : {}
|
|
43890
|
+
}), "oxlintrc.sidecar.json", sidecarLintFiles, options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(missFiles.length + sidecarReplayedFileCount + scannedFileCount, candidateFiles.length)));
|
|
43891
|
+
const unboundedSidecarResult = useSidecarCache && unboundedSidecarRuleIds.length > 0 ? await runConfigOverFiles(() => buildConfig({
|
|
43892
|
+
extendsPaths: [],
|
|
43893
|
+
ruleSelection: "sidecar",
|
|
43894
|
+
sidecarRuleIdFilter: new Set(unboundedSidecarRuleIds)
|
|
43895
|
+
}), "oxlintrc.sidecar-unbounded.json", hitFiles, void 0) : {
|
|
43896
|
+
diagnostics: [],
|
|
43897
|
+
didDropReactHooksJsPlugin: false,
|
|
43898
|
+
hadPartialFailure: false
|
|
43899
|
+
};
|
|
43507
43900
|
const freshCacheableDiagnostics = [];
|
|
43508
43901
|
for (const diagnostic of fullResult.diagnostics) if (!CROSS_FILE_RULE_IDS.has(diagnostic.rule)) freshCacheableDiagnostics.push(diagnostic);
|
|
43509
43902
|
onCacheStats?.(cacheHitFileCount, candidateFiles.length);
|
|
43510
|
-
|
|
43511
|
-
|
|
43512
|
-
|
|
43513
|
-
let isAttributionSound = true;
|
|
43514
|
-
for (const diagnostic of freshCacheableDiagnostics) {
|
|
43515
|
-
const missFile = missFileByNormalizedPath.get(diagnostic.filePath);
|
|
43516
|
-
if (missFile === void 0) {
|
|
43517
|
-
isAttributionSound = false;
|
|
43518
|
-
break;
|
|
43519
|
-
}
|
|
43520
|
-
const fileDiagnostics = freshDiagnosticsByFile.get(missFile) ?? [];
|
|
43521
|
-
fileDiagnostics.push(diagnostic);
|
|
43522
|
-
freshDiagnosticsByFile.set(missFile, fileDiagnostics);
|
|
43523
|
-
}
|
|
43524
|
-
if (!fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure && isAttributionSound) {
|
|
43903
|
+
if (sidecarCache !== null) onSidecarStats?.(hitFiles.length - sidecarLintFiles.length, hitFiles.length);
|
|
43904
|
+
const freshDiagnosticsByFile = attributeDiagnosticsToFiles(freshCacheableDiagnostics, missFiles);
|
|
43905
|
+
if (!fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure && freshDiagnosticsByFile !== null) {
|
|
43525
43906
|
for (const missFile of missFiles) {
|
|
43526
43907
|
const cacheKey = cacheKeyByFile.get(missFile);
|
|
43527
43908
|
if (cacheKey !== void 0) cache.store(cacheKey, freshDiagnosticsByFile.get(missFile) ?? []);
|
|
43528
43909
|
}
|
|
43529
43910
|
cache.persist();
|
|
43530
43911
|
}
|
|
43912
|
+
if (sidecarCache !== null && missProbesTask !== null && sidecarProbesTask !== null) storeSidecarEntries({
|
|
43913
|
+
sidecarCache,
|
|
43914
|
+
cacheKeyByFile,
|
|
43915
|
+
storablePasses: [{
|
|
43916
|
+
files: sidecarLintFiles,
|
|
43917
|
+
diagnostics: boundedSidecarResult.diagnostics,
|
|
43918
|
+
isTrusted: !boundedSidecarResult.hadPartialFailure,
|
|
43919
|
+
probesByFile: await sidecarProbesTask
|
|
43920
|
+
}, {
|
|
43921
|
+
files: missFiles,
|
|
43922
|
+
diagnostics: fullResult.diagnostics.filter((diagnostic) => boundedSidecarRuleIdSet.has(diagnostic.rule)),
|
|
43923
|
+
isTrusted: !fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure,
|
|
43924
|
+
probesByFile: await missProbesTask
|
|
43925
|
+
}]
|
|
43926
|
+
});
|
|
43531
43927
|
return dedupeDiagnostics([
|
|
43532
43928
|
...replayedDiagnostics,
|
|
43533
43929
|
...fullResult.diagnostics,
|
|
43534
|
-
...
|
|
43930
|
+
...sidecarReplayedDiagnostics,
|
|
43931
|
+
...boundedSidecarResult.diagnostics,
|
|
43932
|
+
...unboundedSidecarResult.diagnostics
|
|
43535
43933
|
]);
|
|
43536
43934
|
}
|
|
43537
43935
|
const baseArgs = makeBaseArgs(configPath);
|
|
@@ -43629,6 +44027,7 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
|
|
|
43629
44027
|
const concurrency = yield* OxlintConcurrency;
|
|
43630
44028
|
const lintBatchOrdering = yield* LintBatchOrdering;
|
|
43631
44029
|
const perFileLintCacheEnabled = yield* PerFileLintCacheEnabled;
|
|
44030
|
+
const sidecarLintCacheEnabled = yield* SidecarLintCacheEnabled;
|
|
43632
44031
|
const collectedFailures = [];
|
|
43633
44032
|
const diagnostics = yield* tryPromise({
|
|
43634
44033
|
try: (signal) => runOxlint({
|
|
@@ -43647,7 +44046,9 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
|
|
|
43647
44046
|
},
|
|
43648
44047
|
onFileProgress: input.onFileProgress,
|
|
43649
44048
|
perFileLintCacheEnabled,
|
|
44049
|
+
sidecarLintCacheEnabled,
|
|
43650
44050
|
onCacheStats: input.onCacheStats,
|
|
44051
|
+
onSidecarStats: input.onSidecarStats,
|
|
43651
44052
|
spawnTimeoutMs,
|
|
43652
44053
|
outputMaxBytes,
|
|
43653
44054
|
concurrency,
|
|
@@ -44069,6 +44470,20 @@ const writeCachedSocketBody = (cacheFile, body) => {
|
|
|
44069
44470
|
}));
|
|
44070
44471
|
} catch {}
|
|
44071
44472
|
};
|
|
44473
|
+
const pruneExpiredSocketCache = (cacheDirectory) => {
|
|
44474
|
+
try {
|
|
44475
|
+
const supplyChainCacheDirectory = Path.join(cacheDirectory, SUPPLY_CHAIN_CACHE_SUBDIR);
|
|
44476
|
+
const expiryThresholdMs = Date.now() - SUPPLY_CHAIN_CACHE_TTL_MS;
|
|
44477
|
+
for (const entryName of NFS.readdirSync(supplyChainCacheDirectory)) {
|
|
44478
|
+
const entryPath = Path.join(supplyChainCacheDirectory, entryName);
|
|
44479
|
+
try {
|
|
44480
|
+
if (NFS.statSync(entryPath).mtimeMs < expiryThresholdMs) NFS.rmSync(entryPath);
|
|
44481
|
+
} catch {
|
|
44482
|
+
continue;
|
|
44483
|
+
}
|
|
44484
|
+
}
|
|
44485
|
+
} catch {}
|
|
44486
|
+
};
|
|
44072
44487
|
const fetchSocketArtifact = (dependency, cacheDirectory) => tryPromise(async (signal) => {
|
|
44073
44488
|
const cacheFile = cacheDirectory === null ? null : supplyChainCacheFile(cacheDirectory, dependency);
|
|
44074
44489
|
if (cacheFile !== null) {
|
|
@@ -44196,6 +44611,7 @@ const checkSupplyChain = (input) => gen(function* () {
|
|
|
44196
44611
|
const dependencies = collectDependenciesToScore(readPackageJson$1(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies);
|
|
44197
44612
|
if (dependencies.length === 0) return [];
|
|
44198
44613
|
const cacheDirectory = isSupplyChainCacheDisabled() ? null : resolveReactDoctorCacheDir(input.rootDirectory);
|
|
44614
|
+
if (cacheDirectory !== null) pruneExpiredSocketCache(cacheDirectory);
|
|
44199
44615
|
const artifacts = yield* forEach$1(dependencies, (dependency) => fetchSocketArtifact(dependency, cacheDirectory), { concurrency: 8 }).pipe(timeoutOption(input.totalTimeoutMs ?? 9e4), map$3((maybeArtifacts) => getOrElse$1(maybeArtifacts, () => [])));
|
|
44200
44616
|
const diagnostics = [];
|
|
44201
44617
|
for (let index = 0; index < dependencies.length; index += 1) {
|
|
@@ -44457,6 +44873,8 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
44457
44873
|
let lastReportedTotalFileCount = 0;
|
|
44458
44874
|
let lintCacheHitFileCount = null;
|
|
44459
44875
|
let lintCacheTotalFileCount = null;
|
|
44876
|
+
let lintSidecarReplayedFileCount = null;
|
|
44877
|
+
let lintSidecarTotalFileCount = null;
|
|
44460
44878
|
let deadCodeCacheHit = null;
|
|
44461
44879
|
let deadCodeSummaryCacheHits = null;
|
|
44462
44880
|
let deadCodeSummaryCacheMisses = null;
|
|
@@ -44479,6 +44897,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
44479
44897
|
lintCacheHitFileCount = cacheHitFileCount;
|
|
44480
44898
|
lintCacheTotalFileCount = totalConsideredFileCount;
|
|
44481
44899
|
},
|
|
44900
|
+
onSidecarStats: (sidecarReplayedFileCount, sidecarConsideredFileCount) => {
|
|
44901
|
+
lintSidecarReplayedFileCount = sidecarReplayedFileCount;
|
|
44902
|
+
lintSidecarTotalFileCount = sidecarConsideredFileCount;
|
|
44903
|
+
},
|
|
44482
44904
|
deadlineEpochMs: input.deadlineEpochMs
|
|
44483
44905
|
}).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
|
|
44484
44906
|
yield* set(lintFailure, {
|
|
@@ -44589,6 +45011,8 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
44589
45011
|
securityScanFailed,
|
|
44590
45012
|
lintCacheHitFileCount,
|
|
44591
45013
|
lintCacheTotalFileCount,
|
|
45014
|
+
lintSidecarReplayedFileCount,
|
|
45015
|
+
lintSidecarTotalFileCount,
|
|
44592
45016
|
deadCodeCacheHit: lintFailureState.didFail ? null : deadCodeCacheHit,
|
|
44593
45017
|
deadCodeSummaryCacheHits: lintFailureState.didFail ? null : deadCodeSummaryCacheHits,
|
|
44594
45018
|
deadCodeSummaryCacheMisses: lintFailureState.didFail ? null : deadCodeSummaryCacheMisses,
|
|
@@ -45073,11 +45497,9 @@ const computeConfigFingerprint = (projectDirectory, version) => {
|
|
|
45073
45497
|
const parts = [`v=${version}`];
|
|
45074
45498
|
let directory = projectDirectory;
|
|
45075
45499
|
for (;;) {
|
|
45076
|
-
for (const filename of CONFIG_FINGERPRINT_FILENAMES)
|
|
45077
|
-
const
|
|
45078
|
-
parts.push(`${directory}/${filename}=${
|
|
45079
|
-
} catch {
|
|
45080
|
-
continue;
|
|
45500
|
+
for (const filename of CONFIG_FINGERPRINT_FILENAMES) {
|
|
45501
|
+
const contentHash = hashFileContents(path.join(directory, filename));
|
|
45502
|
+
if (contentHash !== null) parts.push(`${directory}/${filename}=${contentHash}`);
|
|
45081
45503
|
}
|
|
45082
45504
|
const parent = path.dirname(directory);
|
|
45083
45505
|
if (parent === directory) break;
|
|
@@ -45373,6 +45795,7 @@ const isNonInteractiveEnvironment = () => NON_INTERACTIVE_ENVIRONMENT_VARIABLES.
|
|
|
45373
45795
|
const REACT_DOCTOR_CONFIG_PROJECT_NAME = "react-doctor";
|
|
45374
45796
|
const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
|
|
45375
45797
|
const BASELINE_FILES_TEMP_DIR_PREFIX = "react-doctor-baseline-";
|
|
45798
|
+
const SCAN_RESULT_CACHE_FILENAME = "scan-cache.json";
|
|
45376
45799
|
const RUN_GIT_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
|
|
45377
45800
|
const GH_DEFAULT_BRANCH_PROBE_TIMEOUT_MS = 5e3;
|
|
45378
45801
|
const CI_TRUST_COMPANIES = "PayPal, Rippling, and Alibaba";
|
|
@@ -45464,7 +45887,7 @@ const makeNoopConsole = () => ({
|
|
|
45464
45887
|
});
|
|
45465
45888
|
//#endregion
|
|
45466
45889
|
//#region src/cli/utils/version.ts
|
|
45467
|
-
const VERSION = "0.6.3-dev.
|
|
45890
|
+
const VERSION = "0.6.3-dev.ced746f";
|
|
45468
45891
|
//#endregion
|
|
45469
45892
|
//#region src/cli/utils/json-mode.ts
|
|
45470
45893
|
let context = null;
|
|
@@ -45833,13 +46256,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
|
|
|
45833
46256
|
* uploads source-map artifacts under, so stack frames symbolicate. Honors the
|
|
45834
46257
|
* standard `SENTRY_RELEASE` override.
|
|
45835
46258
|
*/
|
|
45836
|
-
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.6.3-dev.
|
|
46259
|
+
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.6.3-dev.ced746f`;
|
|
45837
46260
|
/**
|
|
45838
46261
|
* Deployment environment shown in Sentry's environment filter. Defaults to
|
|
45839
46262
|
* `production` for tagged releases and `development` for dev/unbuilt versions,
|
|
45840
46263
|
* overridable via the standard `SENTRY_ENVIRONMENT` env var.
|
|
45841
46264
|
*/
|
|
45842
|
-
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.6.3-dev.
|
|
46265
|
+
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.6.3-dev.ced746f") ? "development" : "production");
|
|
45843
46266
|
/**
|
|
45844
46267
|
* Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
|
|
45845
46268
|
* (set to `0` to disable tracing) and falls back to
|
|
@@ -57917,6 +58340,276 @@ const buildRuleBlastRadii = (diagnostics) => buildSortedRuleGroups(diagnostics).
|
|
|
57917
58340
|
})).toSorted((left, right) => right.fileCount - left.fileCount);
|
|
57918
58341
|
const findMigrationScaleBuckets = (diagnostics) => buildRuleBlastRadii(diagnostics).filter((bucket) => bucket.fileCount >= 40);
|
|
57919
58342
|
//#endregion
|
|
58343
|
+
//#region src/cli/utils/scan-result-cache.ts
|
|
58344
|
+
const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
|
|
58345
|
+
const TOOLCHAIN_PACKAGE_SPECIFIERS = [
|
|
58346
|
+
"oxlint/package.json",
|
|
58347
|
+
"oxlint-plugin-react-doctor/package.json",
|
|
58348
|
+
"deslop-js/package.json",
|
|
58349
|
+
"eslint-plugin-react-hooks/package.json"
|
|
58350
|
+
];
|
|
58351
|
+
const bundledRequire = createRequire(import.meta.url);
|
|
58352
|
+
const normalizeForStableJson = (value) => {
|
|
58353
|
+
if (value === null) return null;
|
|
58354
|
+
if (value === void 0) return void 0;
|
|
58355
|
+
const valueType = typeof value;
|
|
58356
|
+
if (valueType === "string" || valueType === "number" || valueType === "boolean") return value;
|
|
58357
|
+
if (valueType === "bigint" || valueType === "function" || valueType === "symbol") throw new Error("Unsupported cache key value");
|
|
58358
|
+
if (Array.isArray(value)) return value.map((item) => {
|
|
58359
|
+
const normalized = normalizeForStableJson(item);
|
|
58360
|
+
return normalized === void 0 ? null : normalized;
|
|
58361
|
+
});
|
|
58362
|
+
const prototype = Object.getPrototypeOf(value);
|
|
58363
|
+
if (prototype !== Object.prototype && prototype !== null) throw new Error("Unsupported cache key object");
|
|
58364
|
+
if (!isRecord(value)) throw new Error("Unsupported cache key object");
|
|
58365
|
+
const normalizedRecord = {};
|
|
58366
|
+
for (const key of Object.keys(value).sort()) {
|
|
58367
|
+
const normalized = normalizeForStableJson(value[key]);
|
|
58368
|
+
if (normalized !== void 0) normalizedRecord[key] = normalized;
|
|
58369
|
+
}
|
|
58370
|
+
return normalizedRecord;
|
|
58371
|
+
};
|
|
58372
|
+
const stringifyStableJson = (value) => {
|
|
58373
|
+
try {
|
|
58374
|
+
return JSON.stringify(normalizeForStableJson(value));
|
|
58375
|
+
} catch {
|
|
58376
|
+
return null;
|
|
58377
|
+
}
|
|
58378
|
+
};
|
|
58379
|
+
const hashString = (value) => crypto.createHash("sha1").update(value).digest("hex");
|
|
58380
|
+
const readHeadSha = (projectDirectory) => runGit(projectDirectory, ["rev-parse", "HEAD"]);
|
|
58381
|
+
const parseWorktreeStatusRecords = (statusOutput) => {
|
|
58382
|
+
const entries = [];
|
|
58383
|
+
const tokens = statusOutput.split("\0");
|
|
58384
|
+
for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex += 1) {
|
|
58385
|
+
let record = tokens[tokenIndex];
|
|
58386
|
+
if (record.length === 0) continue;
|
|
58387
|
+
if (record.length < 3 || record[2] !== " ") {
|
|
58388
|
+
if (tokenIndex !== 0) return null;
|
|
58389
|
+
record = ` ${record}`;
|
|
58390
|
+
}
|
|
58391
|
+
if (record.length < 4 || record[2] !== " ") return null;
|
|
58392
|
+
const statusCode = record.slice(0, 2);
|
|
58393
|
+
const recordPath = record.slice(3);
|
|
58394
|
+
if (statusCode.includes("R") || statusCode.includes("C")) {
|
|
58395
|
+
tokenIndex += 1;
|
|
58396
|
+
const originPath = tokens[tokenIndex];
|
|
58397
|
+
if (originPath === void 0 || originPath.length === 0) return null;
|
|
58398
|
+
entries.push({
|
|
58399
|
+
statusCode,
|
|
58400
|
+
path: recordPath,
|
|
58401
|
+
originPath
|
|
58402
|
+
});
|
|
58403
|
+
continue;
|
|
58404
|
+
}
|
|
58405
|
+
entries.push({
|
|
58406
|
+
statusCode,
|
|
58407
|
+
path: recordPath
|
|
58408
|
+
});
|
|
58409
|
+
}
|
|
58410
|
+
return entries;
|
|
58411
|
+
};
|
|
58412
|
+
const buildDirtyPathContentFingerprint = (absolutePath, statusCode) => {
|
|
58413
|
+
let stats;
|
|
58414
|
+
try {
|
|
58415
|
+
stats = fs.lstatSync(absolutePath);
|
|
58416
|
+
} catch {
|
|
58417
|
+
return statusCode.includes("D") ? "deleted" : null;
|
|
58418
|
+
}
|
|
58419
|
+
if (stats.isSymbolicLink()) try {
|
|
58420
|
+
return `link:${fs.readlinkSync(absolutePath)}`;
|
|
58421
|
+
} catch {
|
|
58422
|
+
return null;
|
|
58423
|
+
}
|
|
58424
|
+
if (!stats.isFile()) return null;
|
|
58425
|
+
if (stats.size > 10485760) return `stat:${stats.mtimeMs}:${stats.size}`;
|
|
58426
|
+
return hashFileContents(absolutePath);
|
|
58427
|
+
};
|
|
58428
|
+
const buildWorktreeFingerprint = (projectDirectory) => {
|
|
58429
|
+
const statusOutput = runGit(projectDirectory, [
|
|
58430
|
+
"status",
|
|
58431
|
+
"--porcelain=v1",
|
|
58432
|
+
"-z",
|
|
58433
|
+
"--untracked-files=all"
|
|
58434
|
+
]);
|
|
58435
|
+
if (statusOutput === null) return null;
|
|
58436
|
+
if (statusOutput.length === 0) return [];
|
|
58437
|
+
const statusEntries = parseWorktreeStatusRecords(statusOutput);
|
|
58438
|
+
if (statusEntries === null) return null;
|
|
58439
|
+
if (statusEntries.length > 300) return null;
|
|
58440
|
+
const repositoryRoot = runGit(projectDirectory, ["rev-parse", "--show-toplevel"]);
|
|
58441
|
+
if (repositoryRoot === null) return null;
|
|
58442
|
+
const dirtyEntries = [];
|
|
58443
|
+
for (const statusEntry of statusEntries) {
|
|
58444
|
+
const contentFingerprint = buildDirtyPathContentFingerprint(path.join(repositoryRoot, statusEntry.path), statusEntry.statusCode);
|
|
58445
|
+
if (contentFingerprint === null) return null;
|
|
58446
|
+
dirtyEntries.push({
|
|
58447
|
+
...statusEntry,
|
|
58448
|
+
contentFingerprint
|
|
58449
|
+
});
|
|
58450
|
+
}
|
|
58451
|
+
return dirtyEntries.sort((firstEntry, secondEntry) => {
|
|
58452
|
+
const firstSortKey = `${firstEntry.path}${firstEntry.statusCode}`;
|
|
58453
|
+
const secondSortKey = `${secondEntry.path}${secondEntry.statusCode}`;
|
|
58454
|
+
return firstSortKey < secondSortKey ? -1 : firstSortKey > secondSortKey ? 1 : 0;
|
|
58455
|
+
});
|
|
58456
|
+
};
|
|
58457
|
+
const DOTENV_FILE_NAME_PATTERN = /^\.env(\.|$)/;
|
|
58458
|
+
const resolveDotenvFingerprint = (projectDirectory) => {
|
|
58459
|
+
try {
|
|
58460
|
+
return fs.readdirSync(projectDirectory).filter((entryName) => DOTENV_FILE_NAME_PATTERN.test(entryName)).sort().map((entryName) => `${entryName}=${hashFileContents(path.join(projectDirectory, entryName)) ?? "unreadable"}`);
|
|
58461
|
+
} catch {
|
|
58462
|
+
return [];
|
|
58463
|
+
}
|
|
58464
|
+
};
|
|
58465
|
+
const hasHiddenTrackedFileState = (projectDirectory) => {
|
|
58466
|
+
const output = runGit(projectDirectory, ["ls-files", "-v"]);
|
|
58467
|
+
if (output === null) return true;
|
|
58468
|
+
return output.split("\n").some((line) => line.length > 0 && line[0] !== "H");
|
|
58469
|
+
};
|
|
58470
|
+
const resolveCacheFilePath = (projectDirectory) => path.join(resolveReactDoctorCacheDir(projectDirectory), SCAN_RESULT_CACHE_FILENAME);
|
|
58471
|
+
const readPersistedCache = (cacheFilePath) => {
|
|
58472
|
+
try {
|
|
58473
|
+
const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
|
|
58474
|
+
if (!isRecord(parsed) || parsed.version !== 3) return {
|
|
58475
|
+
version: 3,
|
|
58476
|
+
entries: []
|
|
58477
|
+
};
|
|
58478
|
+
if (!Array.isArray(parsed.entries)) return {
|
|
58479
|
+
version: 3,
|
|
58480
|
+
entries: []
|
|
58481
|
+
};
|
|
58482
|
+
const entries = [];
|
|
58483
|
+
for (const entry of parsed.entries) {
|
|
58484
|
+
if (!isRecord(entry) || typeof entry.key !== "string" || typeof entry.createdAtMs !== "number") continue;
|
|
58485
|
+
if (!isRecord(entry.payload) || !Array.isArray(entry.payload.diagnostics)) continue;
|
|
58486
|
+
entries.push(entry);
|
|
58487
|
+
}
|
|
58488
|
+
return {
|
|
58489
|
+
version: 3,
|
|
58490
|
+
entries
|
|
58491
|
+
};
|
|
58492
|
+
} catch {
|
|
58493
|
+
return {
|
|
58494
|
+
version: 3,
|
|
58495
|
+
entries: []
|
|
58496
|
+
};
|
|
58497
|
+
}
|
|
58498
|
+
};
|
|
58499
|
+
const writePersistedCache = (cacheFilePath, cache) => {
|
|
58500
|
+
try {
|
|
58501
|
+
fs.mkdirSync(path.dirname(cacheFilePath), { recursive: true });
|
|
58502
|
+
const tempPath = `${cacheFilePath}.${process.pid}.tmp`;
|
|
58503
|
+
fs.writeFileSync(tempPath, JSON.stringify(cache));
|
|
58504
|
+
fs.renameSync(tempPath, cacheFilePath);
|
|
58505
|
+
} catch {
|
|
58506
|
+
return;
|
|
58507
|
+
}
|
|
58508
|
+
};
|
|
58509
|
+
/**
|
|
58510
|
+
* The global cache off-switch (`REACT_DOCTOR_NO_CACHE`), which disables every
|
|
58511
|
+
* cache subsystem: this whole-repo scan cache plus core's per-file lint,
|
|
58512
|
+
* sidecar, and dead-code caches (their `Context.Reference` defaults read the
|
|
58513
|
+
* same variable). Exported for the wide event's `cache.temperature`
|
|
58514
|
+
* derivation, which reports `"disabled"` instead of `"cold"` when the switch
|
|
58515
|
+
* is on. Granular knobs (`REACT_DOCTOR_NO_FILE_CACHE`, …) are deliberately
|
|
58516
|
+
* not consulted — they leave the other subsystems live.
|
|
58517
|
+
*/
|
|
58518
|
+
const isCacheGloballyDisabled = () => CACHE_DISABLED_VALUES.has(process.env.REACT_DOCTOR_NO_CACHE?.toLowerCase() ?? "");
|
|
58519
|
+
const resolveProjectIdentity = (projectDirectory) => {
|
|
58520
|
+
try {
|
|
58521
|
+
return fs.realpathSync.native(projectDirectory);
|
|
58522
|
+
} catch {
|
|
58523
|
+
return path.resolve(projectDirectory);
|
|
58524
|
+
}
|
|
58525
|
+
};
|
|
58526
|
+
const fileFingerprint = (filePath) => {
|
|
58527
|
+
try {
|
|
58528
|
+
const stat = fs.statSync(filePath);
|
|
58529
|
+
return `${filePath}:${stat.mtimeMs}:${stat.size}`;
|
|
58530
|
+
} catch {
|
|
58531
|
+
return null;
|
|
58532
|
+
}
|
|
58533
|
+
};
|
|
58534
|
+
const resolveToolchainFingerprint = (nodeBinaryPath) => {
|
|
58535
|
+
const fingerprints = [];
|
|
58536
|
+
if (nodeBinaryPath !== null) fingerprints.push(nodeBinaryPath === process.execPath ? `node=${process.version}` : `node=${fileFingerprint(nodeBinaryPath) ?? "unreadable"}`);
|
|
58537
|
+
for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS) try {
|
|
58538
|
+
const packageJson = bundledRequire(specifier);
|
|
58539
|
+
const version = typeof packageJson.version === "string" ? packageJson.version : "unknown";
|
|
58540
|
+
fingerprints.push(`${specifier}=${version}`);
|
|
58541
|
+
} catch {
|
|
58542
|
+
fingerprints.push(`${specifier}=missing`);
|
|
58543
|
+
}
|
|
58544
|
+
return fingerprints;
|
|
58545
|
+
};
|
|
58546
|
+
const buildScanResultCacheKey = (input) => {
|
|
58547
|
+
if (isCacheGloballyDisabled()) return null;
|
|
58548
|
+
if (hasHiddenTrackedFileState(input.projectDirectory)) return null;
|
|
58549
|
+
const worktreeFingerprint = buildWorktreeFingerprint(input.projectDirectory);
|
|
58550
|
+
if (worktreeFingerprint === null) return null;
|
|
58551
|
+
const headSha = readHeadSha(input.projectDirectory);
|
|
58552
|
+
if (headSha === null) return null;
|
|
58553
|
+
if (stringifyStableJson(input.userConfig) === null) return null;
|
|
58554
|
+
const cacheKeyJson = stringifyStableJson({
|
|
58555
|
+
schemaVersion: 3,
|
|
58556
|
+
projectIdentity: resolveProjectIdentity(input.projectDirectory),
|
|
58557
|
+
headSha,
|
|
58558
|
+
worktreeFingerprint,
|
|
58559
|
+
dotenvFingerprint: resolveDotenvFingerprint(input.projectDirectory),
|
|
58560
|
+
reactDoctorVersion: input.version,
|
|
58561
|
+
nodeVersion: process.version,
|
|
58562
|
+
toolchainFingerprint: resolveToolchainFingerprint(input.nodeBinaryPath),
|
|
58563
|
+
configFingerprint: computeConfigFingerprint(input.projectDirectory, input.version),
|
|
58564
|
+
hasConfigOverride: input.hasConfigOverride,
|
|
58565
|
+
configSourceDirectory: input.configSourceDirectory,
|
|
58566
|
+
userConfig: input.userConfig,
|
|
58567
|
+
engineOptions: {
|
|
58568
|
+
lint: input.options.lint,
|
|
58569
|
+
deadCode: input.options.deadCode,
|
|
58570
|
+
includePaths: [...input.options.includePaths].sort(),
|
|
58571
|
+
customRulesOnly: input.options.customRulesOnly,
|
|
58572
|
+
respectInlineDisables: input.options.respectInlineDisables,
|
|
58573
|
+
warnings: input.options.warnings,
|
|
58574
|
+
adoptExistingLintConfig: input.options.adoptExistingLintConfig,
|
|
58575
|
+
ignoredTags: [...input.options.ignoredTags].sort(),
|
|
58576
|
+
concurrency: input.options.concurrency,
|
|
58577
|
+
lintBatchOrdering: resolveLintBatchOrdering(),
|
|
58578
|
+
baselineRef: input.options.baseline?.ref,
|
|
58579
|
+
changedLineRanges: input.options.changedLineRanges ?? void 0,
|
|
58580
|
+
noScore: input.options.noScore,
|
|
58581
|
+
isCi: input.options.isCi,
|
|
58582
|
+
suppressRendering: input.options.suppressRendering,
|
|
58583
|
+
supplyChainManifestChanged: input.options.supplyChainManifestChanged
|
|
58584
|
+
}
|
|
58585
|
+
});
|
|
58586
|
+
return cacheKeyJson === null ? null : hashString(cacheKeyJson);
|
|
58587
|
+
};
|
|
58588
|
+
const createScanResultCache = (projectDirectory) => {
|
|
58589
|
+
const cacheFilePath = resolveCacheFilePath(projectDirectory);
|
|
58590
|
+
const persistedCache = readPersistedCache(cacheFilePath);
|
|
58591
|
+
const entries = /* @__PURE__ */ new Map();
|
|
58592
|
+
for (const entry of persistedCache.entries) entries.set(entry.key, entry);
|
|
58593
|
+
const persist = () => {
|
|
58594
|
+
writePersistedCache(cacheFilePath, {
|
|
58595
|
+
version: 3,
|
|
58596
|
+
entries: [...entries.values()].sort((firstEntry, secondEntry) => secondEntry.createdAtMs - firstEntry.createdAtMs).slice(0, 20)
|
|
58597
|
+
});
|
|
58598
|
+
};
|
|
58599
|
+
return {
|
|
58600
|
+
lookup: (key) => entries.get(key)?.payload ?? null,
|
|
58601
|
+
store: (key, payload) => {
|
|
58602
|
+
entries.set(key, {
|
|
58603
|
+
key,
|
|
58604
|
+
createdAtMs: Date.now(),
|
|
58605
|
+
payload
|
|
58606
|
+
});
|
|
58607
|
+
persist();
|
|
58608
|
+
}
|
|
58609
|
+
};
|
|
58610
|
+
};
|
|
58611
|
+
const shouldStoreScanPayload = (payload) => !payload.didLintFail && !payload.didDeadCodeFail && payload.lintPartialFailures.length === 0 && !payload.supplyChainOverlapTimedOut && payload.securityScanFailed !== true;
|
|
58612
|
+
//#endregion
|
|
57920
58613
|
//#region src/cli/utils/should-block-ci.ts
|
|
57921
58614
|
const shouldBlockCi = (diagnostics, blockingLevel) => {
|
|
57922
58615
|
if (blockingLevel === "none") return false;
|
|
@@ -57958,6 +58651,63 @@ const readEnvBoolean = (name) => {
|
|
|
57958
58651
|
if (value === void 0) return null;
|
|
57959
58652
|
return value.toLowerCase() === "true" || value === "1";
|
|
57960
58653
|
};
|
|
58654
|
+
const ratioOf = (numerator, denominator) => denominator != null && denominator > 0 ? (numerator ?? 0) / denominator : null;
|
|
58655
|
+
const resolveDeadCodeReuseRatio = (result) => {
|
|
58656
|
+
if (result.deadCodeCacheHit === true) return 1;
|
|
58657
|
+
const summaryTotal = (result.deadCodeSummaryCacheHits ?? 0) + (result.deadCodeSummaryCacheMisses ?? 0);
|
|
58658
|
+
if (summaryTotal > 0) return (result.deadCodeSummaryCacheHits ?? 0) / summaryTotal;
|
|
58659
|
+
return result.deadCodeCacheHit === false ? 0 : null;
|
|
58660
|
+
};
|
|
58661
|
+
/**
|
|
58662
|
+
* One queryable cache temperature per scan, derived from the whole stack:
|
|
58663
|
+
*
|
|
58664
|
+
* - `"turbo"` — whole-repo scan-result replay (the explicit
|
|
58665
|
+
* `wholeRepoCacheHit` flag from the CLI's cachedPayload
|
|
58666
|
+
* branch; no scan work ran).
|
|
58667
|
+
* - `"warm"` — any incremental reuse: per-file lint hits, sidecar
|
|
58668
|
+
* replays, a dead-code whole-result hit, or dead-code
|
|
58669
|
+
* summary-cache hits.
|
|
58670
|
+
* - `"disabled"` — zero reuse because the global `REACT_DOCTOR_NO_CACHE`
|
|
58671
|
+
* off-switch is on. Granular per-cache opt-outs
|
|
58672
|
+
* (`REACT_DOCTOR_NO_FILE_CACHE`, …) still read warm/cold,
|
|
58673
|
+
* since the other subsystems stay live.
|
|
58674
|
+
* - `"cold"` — caches on, zero reuse (first scan / everything changed).
|
|
58675
|
+
*
|
|
58676
|
+
* `cache.warmth` is the headline reuse magnitude in [0, 1]: the plain mean of
|
|
58677
|
+
* the subsystem reuse fractions known this run (lint hit ratio, sidecar
|
|
58678
|
+
* replay ratio, dead-code reuse), skipping subsystems that never consulted a
|
|
58679
|
+
* cache; `1` on turbo, dropped when nothing consulted any cache. Deliberately
|
|
58680
|
+
* unweighted — the per-subsystem dims stay the precise signal; warmth is the
|
|
58681
|
+
* p50/p90-able summary. Emitted only on the success path.
|
|
58682
|
+
*/
|
|
58683
|
+
const buildCacheAttributes = (input) => {
|
|
58684
|
+
if (input.result === void 0) return {};
|
|
58685
|
+
if (input.wholeRepoCacheHit) return withNamespace("cache", {
|
|
58686
|
+
wholeRepoHit: true,
|
|
58687
|
+
temperature: "turbo",
|
|
58688
|
+
warmth: 1
|
|
58689
|
+
});
|
|
58690
|
+
const result = input.result;
|
|
58691
|
+
const subsystemReuseRatios = [
|
|
58692
|
+
ratioOf(result.lintCacheHitFileCount, result.lintCacheTotalFileCount),
|
|
58693
|
+
ratioOf(result.lintSidecarReplayedFileCount, result.lintSidecarTotalFileCount),
|
|
58694
|
+
resolveDeadCodeReuseRatio(result)
|
|
58695
|
+
];
|
|
58696
|
+
let knownSubsystemCount = 0;
|
|
58697
|
+
let reuseRatioSum = 0;
|
|
58698
|
+
for (const reuseRatio of subsystemReuseRatios) {
|
|
58699
|
+
if (reuseRatio === null) continue;
|
|
58700
|
+
knownSubsystemCount += 1;
|
|
58701
|
+
reuseRatioSum += reuseRatio;
|
|
58702
|
+
}
|
|
58703
|
+
const warmth = knownSubsystemCount > 0 ? reuseRatioSum / knownSubsystemCount : null;
|
|
58704
|
+
const temperature = warmth !== null && warmth > 0 ? "warm" : isCacheGloballyDisabled() ? "disabled" : "cold";
|
|
58705
|
+
return withNamespace("cache", {
|
|
58706
|
+
wholeRepoHit: input.wholeRepoCacheHit ?? null,
|
|
58707
|
+
temperature,
|
|
58708
|
+
warmth
|
|
58709
|
+
});
|
|
58710
|
+
};
|
|
57961
58711
|
const resolveVersionPin = (versionInput) => {
|
|
57962
58712
|
if (versionInput === void 0 || versionInput.trim() === "") return null;
|
|
57963
58713
|
if (versionInput === "latest") return "latest";
|
|
@@ -58063,7 +58813,10 @@ const buildOutcomeAttributes = (input) => {
|
|
|
58063
58813
|
deadlineSkippedFileCount: input.lintDeadlineSkippedFileCount ?? null,
|
|
58064
58814
|
cacheHitFiles: result.lintCacheHitFileCount ?? null,
|
|
58065
58815
|
cacheTotalFiles: result.lintCacheTotalFileCount ?? null,
|
|
58066
|
-
cacheHitRatio:
|
|
58816
|
+
cacheHitRatio: ratioOf(result.lintCacheHitFileCount, result.lintCacheTotalFileCount),
|
|
58817
|
+
sidecarReplayedFiles: result.lintSidecarReplayedFileCount ?? null,
|
|
58818
|
+
sidecarTotalFiles: result.lintSidecarTotalFileCount ?? null,
|
|
58819
|
+
sidecarReplayRatio: ratioOf(result.lintSidecarReplayedFileCount, result.lintSidecarTotalFileCount)
|
|
58067
58820
|
}),
|
|
58068
58821
|
...withNamespace("deadCode", {
|
|
58069
58822
|
failed: input.didDeadCodeFail ?? null,
|
|
@@ -58130,7 +58883,7 @@ const buildScanAttributes = (input) => {
|
|
|
58130
58883
|
* Projects a scan into the namespaced attribute set for its root span — the
|
|
58131
58884
|
* canonical per-scan "wide event". Every attribute carries a dotted namespace
|
|
58132
58885
|
* that groups it by concept (`scan.*` config, `action.*` CI knobs, `outcome.*`
|
|
58133
|
-
* verdict, `diag.*` findings, `score.*`, `lint.*`, `deadCode.*`,
|
|
58886
|
+
* verdict, `diag.*` findings, `score.*`, `lint.*`, `deadCode.*`, `cache.*`,
|
|
58134
58887
|
* `supplyChain.*`, `timing.*`, `migration.*`, `baseline.*`) so the attributes
|
|
58135
58888
|
* tree up in Sentry's attribute browser and stay filter-/group-/aggregate-able
|
|
58136
58889
|
* in the Spans dataset. Pure and exported so the projection (outcome
|
|
@@ -58144,7 +58897,8 @@ const buildScanAttributes = (input) => {
|
|
|
58144
58897
|
const buildRunEventAttributes = (input) => toSpanAttributes({
|
|
58145
58898
|
...buildScanAttributes(input),
|
|
58146
58899
|
...buildActionAttributes(),
|
|
58147
|
-
...buildOutcomeAttributes(input)
|
|
58900
|
+
...buildOutcomeAttributes(input),
|
|
58901
|
+
...buildCacheAttributes(input)
|
|
58148
58902
|
});
|
|
58149
58903
|
/**
|
|
58150
58904
|
* Stamps the wide-event attributes onto the run's root span. A guarded no-op
|
|
@@ -59354,275 +60108,6 @@ const resolveCliCategories = (categoryFlag) => {
|
|
|
59354
60108
|
return resolvedCategories.length > 0 ? resolvedCategories : void 0;
|
|
59355
60109
|
};
|
|
59356
60110
|
//#endregion
|
|
59357
|
-
//#region src/cli/utils/scan-result-cache.ts
|
|
59358
|
-
const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
|
|
59359
|
-
const TOOLCHAIN_PACKAGE_SPECIFIERS = [
|
|
59360
|
-
"oxlint",
|
|
59361
|
-
"oxlint/package.json",
|
|
59362
|
-
"oxlint-plugin-react-doctor",
|
|
59363
|
-
"deslop-js/package.json",
|
|
59364
|
-
"eslint-plugin-react-hooks/package.json"
|
|
59365
|
-
];
|
|
59366
|
-
const bundledRequire = createRequire(import.meta.url);
|
|
59367
|
-
const normalizeForStableJson = (value) => {
|
|
59368
|
-
if (value === null) return null;
|
|
59369
|
-
if (value === void 0) return void 0;
|
|
59370
|
-
const valueType = typeof value;
|
|
59371
|
-
if (valueType === "string" || valueType === "number" || valueType === "boolean") return value;
|
|
59372
|
-
if (valueType === "bigint" || valueType === "function" || valueType === "symbol") throw new Error("Unsupported cache key value");
|
|
59373
|
-
if (Array.isArray(value)) return value.map((item) => {
|
|
59374
|
-
const normalized = normalizeForStableJson(item);
|
|
59375
|
-
return normalized === void 0 ? null : normalized;
|
|
59376
|
-
});
|
|
59377
|
-
const prototype = Object.getPrototypeOf(value);
|
|
59378
|
-
if (prototype !== Object.prototype && prototype !== null) throw new Error("Unsupported cache key object");
|
|
59379
|
-
if (!isRecord(value)) throw new Error("Unsupported cache key object");
|
|
59380
|
-
const normalizedRecord = {};
|
|
59381
|
-
for (const key of Object.keys(value).sort()) {
|
|
59382
|
-
const normalized = normalizeForStableJson(value[key]);
|
|
59383
|
-
if (normalized !== void 0) normalizedRecord[key] = normalized;
|
|
59384
|
-
}
|
|
59385
|
-
return normalizedRecord;
|
|
59386
|
-
};
|
|
59387
|
-
const stringifyStableJson = (value) => {
|
|
59388
|
-
try {
|
|
59389
|
-
return JSON.stringify(normalizeForStableJson(value));
|
|
59390
|
-
} catch {
|
|
59391
|
-
return null;
|
|
59392
|
-
}
|
|
59393
|
-
};
|
|
59394
|
-
const hashString = (value) => crypto.createHash("sha1").update(value).digest("hex");
|
|
59395
|
-
const readHeadSha = (projectDirectory) => runGit(projectDirectory, ["rev-parse", "HEAD"]);
|
|
59396
|
-
const parseWorktreeStatusRecords = (statusOutput) => {
|
|
59397
|
-
const entries = [];
|
|
59398
|
-
const tokens = statusOutput.split("\0");
|
|
59399
|
-
for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex += 1) {
|
|
59400
|
-
let record = tokens[tokenIndex];
|
|
59401
|
-
if (record.length === 0) continue;
|
|
59402
|
-
if (record.length < 3 || record[2] !== " ") {
|
|
59403
|
-
if (tokenIndex !== 0) return null;
|
|
59404
|
-
record = ` ${record}`;
|
|
59405
|
-
}
|
|
59406
|
-
if (record.length < 4 || record[2] !== " ") return null;
|
|
59407
|
-
const statusCode = record.slice(0, 2);
|
|
59408
|
-
const recordPath = record.slice(3);
|
|
59409
|
-
if (statusCode.includes("R") || statusCode.includes("C")) {
|
|
59410
|
-
tokenIndex += 1;
|
|
59411
|
-
const originPath = tokens[tokenIndex];
|
|
59412
|
-
if (originPath === void 0 || originPath.length === 0) return null;
|
|
59413
|
-
entries.push({
|
|
59414
|
-
statusCode,
|
|
59415
|
-
path: recordPath,
|
|
59416
|
-
originPath
|
|
59417
|
-
});
|
|
59418
|
-
continue;
|
|
59419
|
-
}
|
|
59420
|
-
entries.push({
|
|
59421
|
-
statusCode,
|
|
59422
|
-
path: recordPath
|
|
59423
|
-
});
|
|
59424
|
-
}
|
|
59425
|
-
return entries;
|
|
59426
|
-
};
|
|
59427
|
-
const buildDirtyPathContentFingerprint = (absolutePath, statusCode) => {
|
|
59428
|
-
let stats;
|
|
59429
|
-
try {
|
|
59430
|
-
stats = fs.lstatSync(absolutePath);
|
|
59431
|
-
} catch {
|
|
59432
|
-
return statusCode.includes("D") ? "deleted" : null;
|
|
59433
|
-
}
|
|
59434
|
-
if (stats.isSymbolicLink()) try {
|
|
59435
|
-
return `link:${fs.readlinkSync(absolutePath)}`;
|
|
59436
|
-
} catch {
|
|
59437
|
-
return null;
|
|
59438
|
-
}
|
|
59439
|
-
if (!stats.isFile()) return null;
|
|
59440
|
-
if (stats.size > 10485760) return `stat:${stats.mtimeMs}:${stats.size}`;
|
|
59441
|
-
return hashFileContents(absolutePath);
|
|
59442
|
-
};
|
|
59443
|
-
const buildWorktreeFingerprint = (projectDirectory) => {
|
|
59444
|
-
const statusOutput = runGit(projectDirectory, [
|
|
59445
|
-
"status",
|
|
59446
|
-
"--porcelain=v1",
|
|
59447
|
-
"-z",
|
|
59448
|
-
"--untracked-files=all"
|
|
59449
|
-
]);
|
|
59450
|
-
if (statusOutput === null) return null;
|
|
59451
|
-
if (statusOutput.length === 0) return [];
|
|
59452
|
-
const statusEntries = parseWorktreeStatusRecords(statusOutput);
|
|
59453
|
-
if (statusEntries === null) return null;
|
|
59454
|
-
if (statusEntries.length > 300) return null;
|
|
59455
|
-
const repositoryRoot = runGit(projectDirectory, ["rev-parse", "--show-toplevel"]);
|
|
59456
|
-
if (repositoryRoot === null) return null;
|
|
59457
|
-
const dirtyEntries = [];
|
|
59458
|
-
for (const statusEntry of statusEntries) {
|
|
59459
|
-
const contentFingerprint = buildDirtyPathContentFingerprint(path.join(repositoryRoot, statusEntry.path), statusEntry.statusCode);
|
|
59460
|
-
if (contentFingerprint === null) return null;
|
|
59461
|
-
dirtyEntries.push({
|
|
59462
|
-
...statusEntry,
|
|
59463
|
-
contentFingerprint
|
|
59464
|
-
});
|
|
59465
|
-
}
|
|
59466
|
-
return dirtyEntries.sort((firstEntry, secondEntry) => {
|
|
59467
|
-
const firstSortKey = `${firstEntry.path}${firstEntry.statusCode}`;
|
|
59468
|
-
const secondSortKey = `${secondEntry.path}${secondEntry.statusCode}`;
|
|
59469
|
-
return firstSortKey < secondSortKey ? -1 : firstSortKey > secondSortKey ? 1 : 0;
|
|
59470
|
-
});
|
|
59471
|
-
};
|
|
59472
|
-
const DOTENV_FILE_NAME_PATTERN = /^\.env(\.|$)/;
|
|
59473
|
-
const resolveDotenvFingerprint = (projectDirectory) => {
|
|
59474
|
-
try {
|
|
59475
|
-
return fs.readdirSync(projectDirectory).filter((entryName) => DOTENV_FILE_NAME_PATTERN.test(entryName)).sort().map((entryName) => `${entryName}=${fileFingerprint(path.join(projectDirectory, entryName)) ?? "unreadable"}`);
|
|
59476
|
-
} catch {
|
|
59477
|
-
return [];
|
|
59478
|
-
}
|
|
59479
|
-
};
|
|
59480
|
-
const hasHiddenTrackedFileState = (projectDirectory) => {
|
|
59481
|
-
const output = runGit(projectDirectory, ["ls-files", "-v"]);
|
|
59482
|
-
if (output === null) return true;
|
|
59483
|
-
return output.split("\n").some((line) => line.length > 0 && line[0] !== "H");
|
|
59484
|
-
};
|
|
59485
|
-
const resolveCacheFilePath = (projectDirectory) => {
|
|
59486
|
-
const nodeModulesDirectory = path.join(projectDirectory, "node_modules");
|
|
59487
|
-
if (fs.existsSync(nodeModulesDirectory)) return path.join(nodeModulesDirectory, ".cache", "react-doctor", "scan-cache.json");
|
|
59488
|
-
const projectHash = hashString(projectDirectory).slice(0, 16);
|
|
59489
|
-
return path.join(os.tmpdir(), "react-doctor-cache", `scan-${projectHash}.json`);
|
|
59490
|
-
};
|
|
59491
|
-
const readPersistedCache = (cacheFilePath) => {
|
|
59492
|
-
try {
|
|
59493
|
-
const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
|
|
59494
|
-
if (!isRecord(parsed) || parsed.version !== 3) return {
|
|
59495
|
-
version: 3,
|
|
59496
|
-
entries: []
|
|
59497
|
-
};
|
|
59498
|
-
if (!Array.isArray(parsed.entries)) return {
|
|
59499
|
-
version: 3,
|
|
59500
|
-
entries: []
|
|
59501
|
-
};
|
|
59502
|
-
const entries = [];
|
|
59503
|
-
for (const entry of parsed.entries) {
|
|
59504
|
-
if (!isRecord(entry) || typeof entry.key !== "string" || typeof entry.createdAtMs !== "number") continue;
|
|
59505
|
-
if (!isRecord(entry.payload) || !Array.isArray(entry.payload.diagnostics)) continue;
|
|
59506
|
-
entries.push(entry);
|
|
59507
|
-
}
|
|
59508
|
-
return {
|
|
59509
|
-
version: 3,
|
|
59510
|
-
entries
|
|
59511
|
-
};
|
|
59512
|
-
} catch {
|
|
59513
|
-
return {
|
|
59514
|
-
version: 3,
|
|
59515
|
-
entries: []
|
|
59516
|
-
};
|
|
59517
|
-
}
|
|
59518
|
-
};
|
|
59519
|
-
const writePersistedCache = (cacheFilePath, cache) => {
|
|
59520
|
-
try {
|
|
59521
|
-
fs.mkdirSync(path.dirname(cacheFilePath), { recursive: true });
|
|
59522
|
-
const tempPath = `${cacheFilePath}.${process.pid}.tmp`;
|
|
59523
|
-
fs.writeFileSync(tempPath, JSON.stringify(cache));
|
|
59524
|
-
fs.renameSync(tempPath, cacheFilePath);
|
|
59525
|
-
} catch {
|
|
59526
|
-
return;
|
|
59527
|
-
}
|
|
59528
|
-
};
|
|
59529
|
-
const isScanResultCacheDisabled = () => CACHE_DISABLED_VALUES.has(process.env.REACT_DOCTOR_NO_CACHE?.toLowerCase() ?? "");
|
|
59530
|
-
const resolveProjectIdentity = (projectDirectory) => {
|
|
59531
|
-
try {
|
|
59532
|
-
return fs.realpathSync.native(projectDirectory);
|
|
59533
|
-
} catch {
|
|
59534
|
-
return path.resolve(projectDirectory);
|
|
59535
|
-
}
|
|
59536
|
-
};
|
|
59537
|
-
const fileFingerprint = (filePath) => {
|
|
59538
|
-
try {
|
|
59539
|
-
const stat = fs.statSync(filePath);
|
|
59540
|
-
return `${filePath}:${stat.mtimeMs}:${stat.size}`;
|
|
59541
|
-
} catch {
|
|
59542
|
-
return null;
|
|
59543
|
-
}
|
|
59544
|
-
};
|
|
59545
|
-
const resolveToolchainFingerprint = (nodeBinaryPath) => {
|
|
59546
|
-
const fingerprints = [];
|
|
59547
|
-
if (nodeBinaryPath !== null) {
|
|
59548
|
-
const fingerprint = fileFingerprint(nodeBinaryPath);
|
|
59549
|
-
if (fingerprint !== null) fingerprints.push(`node=${fingerprint}`);
|
|
59550
|
-
}
|
|
59551
|
-
for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS) try {
|
|
59552
|
-
const fingerprint = fileFingerprint(bundledRequire.resolve(specifier));
|
|
59553
|
-
if (fingerprint !== null) fingerprints.push(`${specifier}=${fingerprint}`);
|
|
59554
|
-
} catch {
|
|
59555
|
-
continue;
|
|
59556
|
-
}
|
|
59557
|
-
return fingerprints;
|
|
59558
|
-
};
|
|
59559
|
-
const buildScanResultCacheKey = (input) => {
|
|
59560
|
-
if (isScanResultCacheDisabled()) return null;
|
|
59561
|
-
if (hasHiddenTrackedFileState(input.projectDirectory)) return null;
|
|
59562
|
-
const worktreeFingerprint = buildWorktreeFingerprint(input.projectDirectory);
|
|
59563
|
-
if (worktreeFingerprint === null) return null;
|
|
59564
|
-
const headSha = readHeadSha(input.projectDirectory);
|
|
59565
|
-
if (headSha === null) return null;
|
|
59566
|
-
if (stringifyStableJson(input.userConfig) === null) return null;
|
|
59567
|
-
const cacheKeyJson = stringifyStableJson({
|
|
59568
|
-
schemaVersion: 3,
|
|
59569
|
-
projectIdentity: resolveProjectIdentity(input.projectDirectory),
|
|
59570
|
-
headSha,
|
|
59571
|
-
worktreeFingerprint,
|
|
59572
|
-
dotenvFingerprint: resolveDotenvFingerprint(input.projectDirectory),
|
|
59573
|
-
reactDoctorVersion: input.version,
|
|
59574
|
-
nodeVersion: process.version,
|
|
59575
|
-
toolchainFingerprint: resolveToolchainFingerprint(input.nodeBinaryPath),
|
|
59576
|
-
configFingerprint: computeConfigFingerprint(input.projectDirectory, input.version),
|
|
59577
|
-
hasConfigOverride: input.hasConfigOverride,
|
|
59578
|
-
configSourceDirectory: input.configSourceDirectory,
|
|
59579
|
-
userConfig: input.userConfig,
|
|
59580
|
-
engineOptions: {
|
|
59581
|
-
lint: input.options.lint,
|
|
59582
|
-
deadCode: input.options.deadCode,
|
|
59583
|
-
includePaths: [...input.options.includePaths].sort(),
|
|
59584
|
-
customRulesOnly: input.options.customRulesOnly,
|
|
59585
|
-
respectInlineDisables: input.options.respectInlineDisables,
|
|
59586
|
-
warnings: input.options.warnings,
|
|
59587
|
-
adoptExistingLintConfig: input.options.adoptExistingLintConfig,
|
|
59588
|
-
ignoredTags: [...input.options.ignoredTags].sort(),
|
|
59589
|
-
concurrency: input.options.concurrency,
|
|
59590
|
-
lintBatchOrdering: resolveLintBatchOrdering(),
|
|
59591
|
-
baselineRef: input.options.baseline?.ref,
|
|
59592
|
-
changedLineRanges: input.options.changedLineRanges ?? void 0,
|
|
59593
|
-
noScore: input.options.noScore,
|
|
59594
|
-
isCi: input.options.isCi,
|
|
59595
|
-
suppressRendering: input.options.suppressRendering,
|
|
59596
|
-
supplyChainManifestChanged: input.options.supplyChainManifestChanged
|
|
59597
|
-
}
|
|
59598
|
-
});
|
|
59599
|
-
return cacheKeyJson === null ? null : hashString(cacheKeyJson);
|
|
59600
|
-
};
|
|
59601
|
-
const createScanResultCache = (projectDirectory) => {
|
|
59602
|
-
const cacheFilePath = resolveCacheFilePath(projectDirectory);
|
|
59603
|
-
const persistedCache = readPersistedCache(cacheFilePath);
|
|
59604
|
-
const entries = /* @__PURE__ */ new Map();
|
|
59605
|
-
for (const entry of persistedCache.entries) entries.set(entry.key, entry);
|
|
59606
|
-
const persist = () => {
|
|
59607
|
-
writePersistedCache(cacheFilePath, {
|
|
59608
|
-
version: 3,
|
|
59609
|
-
entries: [...entries.values()].sort((firstEntry, secondEntry) => secondEntry.createdAtMs - firstEntry.createdAtMs).slice(0, 20)
|
|
59610
|
-
});
|
|
59611
|
-
};
|
|
59612
|
-
return {
|
|
59613
|
-
lookup: (key) => entries.get(key)?.payload ?? null,
|
|
59614
|
-
store: (key, payload) => {
|
|
59615
|
-
entries.set(key, {
|
|
59616
|
-
key,
|
|
59617
|
-
createdAtMs: Date.now(),
|
|
59618
|
-
payload
|
|
59619
|
-
});
|
|
59620
|
-
persist();
|
|
59621
|
-
}
|
|
59622
|
-
};
|
|
59623
|
-
};
|
|
59624
|
-
const shouldStoreScanPayload = (payload) => !payload.didLintFail && !payload.didDeadCodeFail && payload.lintPartialFailures.length === 0 && !payload.supplyChainOverlapTimedOut && payload.securityScanFailed !== true;
|
|
59625
|
-
//#endregion
|
|
59626
60111
|
//#region src/inspect.ts
|
|
59627
60112
|
const silentConsole = makeNoopConsole();
|
|
59628
60113
|
const runConsole = (effect) => {
|
|
@@ -59799,7 +60284,7 @@ const runBaselineComparison = async (params) => {
|
|
|
59799
60284
|
suppressScanSummary: true,
|
|
59800
60285
|
supplyChainManifestChanged: params.options.supplyChainManifestChanged,
|
|
59801
60286
|
deadlineEpochMs: params.deadlineEpochMs ?? void 0
|
|
59802
|
-
}, {}).pipe(provide(baseLayers), provideService$2(Console, silentConsole))));
|
|
60287
|
+
}, {}).pipe(provide(baseLayers), provideService$2(PerFileLintCacheEnabled, false), provideService$2(SidecarLintCacheEnabled, false), provideService$2(Console, silentConsole))));
|
|
59803
60288
|
if (baseOutput.didLintFail || countIncompleteLintFiles(baseOutput.lintPartialFailures) > 0) return null;
|
|
59804
60289
|
const delta = computeDiagnosticDelta({
|
|
59805
60290
|
headDiagnostics: params.headDiagnostics,
|
|
@@ -59852,7 +60337,8 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
|
|
|
59852
60337
|
startTime,
|
|
59853
60338
|
rootSentrySpan,
|
|
59854
60339
|
scanMode: cachedPayload.baselineDelta ? "baseline" : isDiffMode ? "diff" : "full",
|
|
59855
|
-
baselineDegraded
|
|
60340
|
+
baselineDegraded,
|
|
60341
|
+
wholeRepoCacheHit: true
|
|
59856
60342
|
});
|
|
59857
60343
|
recordOnboardingCompletion(options);
|
|
59858
60344
|
return result;
|
|
@@ -59961,8 +60447,11 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
|
|
|
59961
60447
|
rootSentrySpan,
|
|
59962
60448
|
scanMode: baselineDelta ? "baseline" : isDiffMode ? "diff" : "full",
|
|
59963
60449
|
baselineDegraded,
|
|
60450
|
+
wholeRepoCacheHit: false,
|
|
59964
60451
|
lintCacheHitFileCount: output.lintCacheHitFileCount,
|
|
59965
60452
|
lintCacheTotalFileCount: output.lintCacheTotalFileCount,
|
|
60453
|
+
lintSidecarReplayedFileCount: output.lintSidecarReplayedFileCount,
|
|
60454
|
+
lintSidecarTotalFileCount: output.lintSidecarTotalFileCount,
|
|
59966
60455
|
deadCodeCacheHit: output.deadCodeCacheHit,
|
|
59967
60456
|
deadCodeSummaryCacheHits: output.deadCodeSummaryCacheHits,
|
|
59968
60457
|
deadCodeSummaryCacheMisses: output.deadCodeSummaryCacheMisses
|
|
@@ -60000,6 +60489,8 @@ const renderAndRecordScan = async (input) => {
|
|
|
60000
60489
|
scanElapsedMilliseconds: input.payload.scanElapsedMilliseconds,
|
|
60001
60490
|
lintCacheHitFileCount: input.lintCacheHitFileCount ?? null,
|
|
60002
60491
|
lintCacheTotalFileCount: input.lintCacheTotalFileCount ?? null,
|
|
60492
|
+
lintSidecarReplayedFileCount: input.lintSidecarReplayedFileCount ?? null,
|
|
60493
|
+
lintSidecarTotalFileCount: input.lintSidecarTotalFileCount ?? null,
|
|
60003
60494
|
deadCodeCacheHit: input.deadCodeCacheHit ?? null,
|
|
60004
60495
|
deadCodeSummaryCacheHits: input.deadCodeSummaryCacheHits ?? null,
|
|
60005
60496
|
deadCodeSummaryCacheMisses: input.deadCodeSummaryCacheMisses ?? null,
|
|
@@ -60027,6 +60518,7 @@ const renderAndRecordScan = async (input) => {
|
|
|
60027
60518
|
result,
|
|
60028
60519
|
mode: input.scanMode,
|
|
60029
60520
|
gateExempt: input.baselineDegraded,
|
|
60521
|
+
wholeRepoCacheHit: input.wholeRepoCacheHit,
|
|
60030
60522
|
didLintFail: input.payload.didLintFail,
|
|
60031
60523
|
lintFailureReasonKind: input.payload.lintFailureReasonKind,
|
|
60032
60524
|
lintPartialFailureCount: input.payload.lintPartialFailures.length,
|
|
@@ -60041,7 +60533,7 @@ const renderAndRecordScan = async (input) => {
|
|
|
60041
60533
|
return result;
|
|
60042
60534
|
};
|
|
60043
60535
|
const finalizeAndRender = (input) => gen(function* () {
|
|
60044
|
-
const { options, elapsedMilliseconds, diagnostics, score, project, userConfig, didLintFail, lintFailureReason, lintPartialFailures, didDeadCodeFail, deadCodeFailureReason, directory, scannedFileCount, scannedFilePaths, scanElapsedMilliseconds, lintCacheHitFileCount, lintCacheTotalFileCount, deadCodeCacheHit, deadCodeSummaryCacheHits, deadCodeSummaryCacheMisses, baselineDelta } = input;
|
|
60536
|
+
const { options, elapsedMilliseconds, diagnostics, score, project, userConfig, didLintFail, lintFailureReason, lintPartialFailures, didDeadCodeFail, deadCodeFailureReason, directory, scannedFileCount, scannedFilePaths, scanElapsedMilliseconds, lintCacheHitFileCount, lintCacheTotalFileCount, lintSidecarReplayedFileCount, lintSidecarTotalFileCount, deadCodeCacheHit, deadCodeSummaryCacheHits, deadCodeSummaryCacheMisses, baselineDelta } = input;
|
|
60045
60537
|
const { skippedChecks, skippedCheckReasons } = buildSkippedChecks({
|
|
60046
60538
|
didLintFail,
|
|
60047
60539
|
lintFailureReason,
|
|
@@ -60065,6 +60557,10 @@ const finalizeAndRender = (input) => gen(function* () {
|
|
|
60065
60557
|
lintCacheHitFileCount,
|
|
60066
60558
|
lintCacheTotalFileCount
|
|
60067
60559
|
} : {},
|
|
60560
|
+
...lintSidecarTotalFileCount !== null ? {
|
|
60561
|
+
lintSidecarReplayedFileCount,
|
|
60562
|
+
lintSidecarTotalFileCount
|
|
60563
|
+
} : {},
|
|
60068
60564
|
...deadCodeCacheHit !== null ? { deadCodeCacheHit } : {},
|
|
60069
60565
|
...deadCodeSummaryCacheHits !== null && deadCodeSummaryCacheMisses !== null ? {
|
|
60070
60566
|
deadCodeSummaryCacheHits,
|
|
@@ -63916,4 +64412,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
|
|
|
63916
64412
|
export {};
|
|
63917
64413
|
|
|
63918
64414
|
//# sourceMappingURL=cli.js.map
|
|
63919
|
-
//# debugId=
|
|
64415
|
+
//# debugId=1722becf-6e91-56ae-afc9-1e1410647bc3
|