react-doctor 0.6.2 → 0.6.3-dev.20d81f6
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 +346 -71
- package/dist/index.d.ts +8 -0
- package/dist/index.js +247 -57
- package/dist/lsp.js +248 -58
- package/package.json +6 -6
package/dist/lsp.js
CHANGED
|
@@ -15,6 +15,7 @@ import { fileURLToPath } from "node:url";
|
|
|
15
15
|
import { createJiti } from "jiti";
|
|
16
16
|
import * as Crypto from "node:crypto";
|
|
17
17
|
import crypto, { createHash } from "node:crypto";
|
|
18
|
+
import { ANALYZED_MANIFEST_FILENAMES, DEFAULT_EXTENSIONS } from "deslop-js/analyzed-inputs";
|
|
18
19
|
import { gzipSync } from "node:zlib";
|
|
19
20
|
import { CodeActionKind, CodeActionTriggerKind, DidChangeWatchedFilesNotification, DocumentDiagnosticReportKind, FileChangeType, TextDocumentSyncKind, TextDocuments, createConnection } from "vscode-languageserver/node.js";
|
|
20
21
|
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
@@ -32986,6 +32987,8 @@ const SOCKET_PACKAGE_PAGE_BASE = "https://socket.dev/npm/package";
|
|
|
32986
32987
|
const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
|
|
32987
32988
|
const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
|
|
32988
32989
|
const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
|
|
32990
|
+
const CORE_PACKAGE_VERSION = "0.6.3";
|
|
32991
|
+
const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
|
|
32989
32992
|
const SUPPLY_CHAIN_PLUGIN = "socket";
|
|
32990
32993
|
const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
|
|
32991
32994
|
const SUPPLY_CHAIN_CATEGORY = "Security";
|
|
@@ -35512,7 +35515,7 @@ const resolveAutoScanConcurrency = (facts = readSystemFacts$1()) => {
|
|
|
35512
35515
|
return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
|
|
35513
35516
|
};
|
|
35514
35517
|
const resolveLintBatchOrdering = () => {
|
|
35515
|
-
return process.env["REACT_DOCTOR_LINT_BATCH_ORDERING"]?.trim().toLowerCase() === "
|
|
35518
|
+
return process.env["REACT_DOCTOR_LINT_BATCH_ORDERING"]?.trim().toLowerCase() === "arrival" ? "arrival" : "cost";
|
|
35516
35519
|
};
|
|
35517
35520
|
/**
|
|
35518
35521
|
* Per-batch oxlint wall-clock budget. Reads from the env var on
|
|
@@ -35609,17 +35612,19 @@ var DeadCodeOverlap = class extends Reference("react-doctor/DeadCodeOverlap", {
|
|
|
35609
35612
|
return "auto";
|
|
35610
35613
|
} }) {};
|
|
35611
35614
|
/**
|
|
35612
|
-
* How the full-scan lint pass
|
|
35613
|
-
*
|
|
35614
|
-
*
|
|
35615
|
-
*
|
|
35616
|
-
* the
|
|
35617
|
-
*
|
|
35618
|
-
*
|
|
35619
|
-
*
|
|
35620
|
-
* `
|
|
35621
|
-
*
|
|
35622
|
-
*
|
|
35615
|
+
* How the full-scan lint pass plans its file batches. `"cost"` (the default)
|
|
35616
|
+
* builds size-balanced LPT batches (`planLintBatches`): the same mandatory
|
|
35617
|
+
* batch count as greedy chunking (`ceil(files / 100)`), but every batch gets
|
|
35618
|
+
* an even share of files AND bytes, so no 100-file chunk is a straggler while
|
|
35619
|
+
* the remainder-batch worker idles — and the heavy files are SPREAD across
|
|
35620
|
+
* batches, the precondition the old sort-desc-then-chunk-100 `cost` mode
|
|
35621
|
+
* lacked (it packed the heaviest files into one wave-1 straggler batch,
|
|
35622
|
+
* measurably regressing size-skewed repos, which is why it never earned the
|
|
35623
|
+
* default). `"arrival"` (`REACT_DOCTOR_LINT_BATCH_ORDERING=arrival`) is the
|
|
35624
|
+
* rollback hatch to plain greedy 100-file chunking in discovery order. Tests
|
|
35625
|
+
* override via `Layer.succeed(LintBatchOrdering, ...)`. Diff / staged scans
|
|
35626
|
+
* never reach this — they pass user-scoped `includePaths` that skip discovery
|
|
35627
|
+
* and stay in arrival order; only the full-scan branch reads it.
|
|
35623
35628
|
*/
|
|
35624
35629
|
var LintBatchOrdering = class extends Reference("react-doctor/LintBatchOrdering", { defaultValue: resolveLintBatchOrdering }) {};
|
|
35625
35630
|
const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
|
|
@@ -35644,6 +35649,25 @@ var PerFileLintCacheEnabled = class extends Reference("react-doctor/PerFileLintC
|
|
|
35644
35649
|
if (CACHE_DISABLED_VALUES.has(noFileCache)) return false;
|
|
35645
35650
|
return true;
|
|
35646
35651
|
} }) {};
|
|
35652
|
+
/**
|
|
35653
|
+
* Whether the whole-project dead-code result cache
|
|
35654
|
+
* (`dead-code/dead-code-result-cache.ts`) is active. Defaults ON — a rescan
|
|
35655
|
+
* whose inputs (source tree, manifests, configs, analyzer version) are
|
|
35656
|
+
* unchanged replays the stored diagnostics instead of re-running the
|
|
35657
|
+
* analysis worker. Opt-OUT, two knobs (matching the per-file lint cache):
|
|
35658
|
+
*
|
|
35659
|
+
* - `REACT_DOCTOR_NO_CACHE` — the global off-switch.
|
|
35660
|
+
* - `REACT_DOCTOR_NO_DEAD_CODE_CACHE` — granular: bust only this cache.
|
|
35661
|
+
*
|
|
35662
|
+
* Tests override via `Layer.succeed(DeadCodeResultCacheEnabled, false)`.
|
|
35663
|
+
*/
|
|
35664
|
+
var DeadCodeResultCacheEnabled = class extends Reference("react-doctor/DeadCodeResultCacheEnabled", { defaultValue: () => {
|
|
35665
|
+
const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
|
|
35666
|
+
const noDeadCodeCache = process.env["REACT_DOCTOR_NO_DEAD_CODE_CACHE"]?.toLowerCase() ?? "";
|
|
35667
|
+
if (CACHE_DISABLED_VALUES.has(noCache)) return false;
|
|
35668
|
+
if (CACHE_DISABLED_VALUES.has(noDeadCodeCache)) return false;
|
|
35669
|
+
return true;
|
|
35670
|
+
} }) {};
|
|
35647
35671
|
const DIAGNOSTIC_SURFACES = [
|
|
35648
35672
|
"cli",
|
|
35649
35673
|
"prComment",
|
|
@@ -36928,6 +36952,7 @@ function* collectSecurityScanFiles(rootDirectory) {
|
|
|
36928
36952
|
isGeneratedBundleByName: classification.isGeneratedBundleByName
|
|
36929
36953
|
});
|
|
36930
36954
|
}
|
|
36955
|
+
yield null;
|
|
36931
36956
|
}
|
|
36932
36957
|
for (const candidates of [
|
|
36933
36958
|
priorityCandidates,
|
|
@@ -37053,9 +37078,18 @@ const checkSecurityScanCooperative = async (rootDirectory, options = {}) => {
|
|
|
37053
37078
|
const session = createSecurityScanSession(rootDirectory, options);
|
|
37054
37079
|
if (session === null) return [];
|
|
37055
37080
|
let sliceStartedAt = performance.now();
|
|
37056
|
-
for (const file of collectSecurityScanFiles(rootDirectory))
|
|
37057
|
-
|
|
37058
|
-
|
|
37081
|
+
for (const file of collectSecurityScanFiles(rootDirectory)) {
|
|
37082
|
+
if (file === null) {
|
|
37083
|
+
if (performance.now() - sliceStartedAt >= 12) {
|
|
37084
|
+
await yieldToEventLoop();
|
|
37085
|
+
sliceStartedAt = performance.now();
|
|
37086
|
+
}
|
|
37087
|
+
continue;
|
|
37088
|
+
}
|
|
37089
|
+
for (const _ruleStep of session.scanFileByRule(file)) if (performance.now() - sliceStartedAt >= 12) {
|
|
37090
|
+
await yieldToEventLoop();
|
|
37091
|
+
sliceStartedAt = performance.now();
|
|
37092
|
+
}
|
|
37059
37093
|
}
|
|
37060
37094
|
return session.diagnostics;
|
|
37061
37095
|
};
|
|
@@ -37274,6 +37308,80 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
|
|
|
37274
37308
|
return [...seen].filter((pattern) => pattern.length > 0);
|
|
37275
37309
|
};
|
|
37276
37310
|
const collectDeadCodeEntryPatterns = (rootDirectory) => [...new Set(collectKnipPatterns(rootDirectory, "entry"))].filter((pattern) => pattern.length > 0);
|
|
37311
|
+
const atomicWriteJson = (filePath, value) => {
|
|
37312
|
+
try {
|
|
37313
|
+
NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
|
|
37314
|
+
const temporaryPath = `${filePath}.${process.pid}.tmp`;
|
|
37315
|
+
NFS.writeFileSync(temporaryPath, JSON.stringify(value));
|
|
37316
|
+
NFS.renameSync(temporaryPath, filePath);
|
|
37317
|
+
} catch {
|
|
37318
|
+
return;
|
|
37319
|
+
}
|
|
37320
|
+
};
|
|
37321
|
+
const failOpenReadJson = (filePath, fallback) => {
|
|
37322
|
+
try {
|
|
37323
|
+
return JSON.parse(NFS.readFileSync(filePath, "utf8"));
|
|
37324
|
+
} catch {
|
|
37325
|
+
return fallback;
|
|
37326
|
+
}
|
|
37327
|
+
};
|
|
37328
|
+
const ANALYZED_FILE_EXTENSIONS = new Set(DEFAULT_EXTENSIONS);
|
|
37329
|
+
const CORE_SIDE_MANIFEST_NAMES = ["knip.json", "deno.lock"];
|
|
37330
|
+
const ANALYZED_MANIFEST_NAMES = new Set([...ANALYZED_MANIFEST_FILENAMES, ...CORE_SIDE_MANIFEST_NAMES]);
|
|
37331
|
+
const isTsConfigLikeFile = (fileName) => (fileName.startsWith("tsconfig") || fileName.startsWith("jsconfig")) && fileName.endsWith(".json");
|
|
37332
|
+
const isFingerprintedFile = (fileName) => ANALYZED_FILE_EXTENSIONS.has(Path.extname(fileName).toLowerCase()) || ANALYZED_MANIFEST_NAMES.has(fileName) || isTsConfigLikeFile(fileName);
|
|
37333
|
+
const collectAnalyzedFileFingerprints = (rootDirectory) => {
|
|
37334
|
+
const fingerprints = [];
|
|
37335
|
+
for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) {
|
|
37336
|
+
if (!isFingerprintedFile(name)) continue;
|
|
37337
|
+
try {
|
|
37338
|
+
const fileStat = NFS.statSync(absolutePath);
|
|
37339
|
+
const relativePath = Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/");
|
|
37340
|
+
fingerprints.push(`${relativePath}:${fileStat.mtimeMs}:${fileStat.size}`);
|
|
37341
|
+
} catch {}
|
|
37342
|
+
}
|
|
37343
|
+
return fingerprints.sort();
|
|
37344
|
+
};
|
|
37345
|
+
const bundledRequire$2 = createRequire(import.meta.url);
|
|
37346
|
+
const resolveDeslopVersion = () => {
|
|
37347
|
+
try {
|
|
37348
|
+
const packageJson = JSON.parse(NFS.readFileSync(bundledRequire$2.resolve("deslop-js/package.json"), "utf8"));
|
|
37349
|
+
return isRecord(packageJson) && typeof packageJson.version === "string" ? packageJson.version : "unknown";
|
|
37350
|
+
} catch {
|
|
37351
|
+
return "unknown";
|
|
37352
|
+
}
|
|
37353
|
+
};
|
|
37354
|
+
const computeDeadCodeCacheKey = (input) => crypto.createHash("sha1").update(JSON.stringify({
|
|
37355
|
+
schemaVersion: 1,
|
|
37356
|
+
coreVersion: input.coreVersion,
|
|
37357
|
+
deslopVersion: resolveDeslopVersion(),
|
|
37358
|
+
deslopJsModuleSpecifier: input.deslopJsModuleSpecifier,
|
|
37359
|
+
entryPatterns: input.entryPatterns,
|
|
37360
|
+
ignorePatterns: input.ignorePatterns,
|
|
37361
|
+
tsConfigFile: input.tsConfigPath === void 0 ? null : Path.relative(input.rootDirectory, input.tsConfigPath).replace(/\\/g, "/"),
|
|
37362
|
+
files: collectAnalyzedFileFingerprints(input.rootDirectory)
|
|
37363
|
+
})).digest("hex");
|
|
37364
|
+
const validateDiagnostic$1 = decodeUnknownSync(Diagnostic);
|
|
37365
|
+
const decodeCachedDiagnostics = (raw) => {
|
|
37366
|
+
try {
|
|
37367
|
+
for (const entry of raw) validateDiagnostic$1(entry);
|
|
37368
|
+
return raw;
|
|
37369
|
+
} catch {
|
|
37370
|
+
return null;
|
|
37371
|
+
}
|
|
37372
|
+
};
|
|
37373
|
+
const lookupDeadCodeResultCache = (cacheDirectory, cacheKey) => {
|
|
37374
|
+
const persisted = failOpenReadJson(Path.join(cacheDirectory, DEAD_CODE_CACHE_FILENAME), null);
|
|
37375
|
+
if (persisted === null || !isRecord(persisted) || persisted.version !== 1 || persisted.key !== cacheKey || !Array.isArray(persisted.diagnostics)) return null;
|
|
37376
|
+
return decodeCachedDiagnostics(persisted.diagnostics);
|
|
37377
|
+
};
|
|
37378
|
+
const storeDeadCodeResultCache = (cacheDirectory, cacheKey, diagnostics) => {
|
|
37379
|
+
atomicWriteJson(Path.join(cacheDirectory, DEAD_CODE_CACHE_FILENAME), {
|
|
37380
|
+
version: 1,
|
|
37381
|
+
key: cacheKey,
|
|
37382
|
+
diagnostics
|
|
37383
|
+
});
|
|
37384
|
+
};
|
|
37277
37385
|
const readSystemFacts = () => ({
|
|
37278
37386
|
availableCores: os.availableParallelism(),
|
|
37279
37387
|
totalMemoryBytes: os.totalmem(),
|
|
@@ -37342,6 +37450,21 @@ const withDeadCodeWorkerSlot = async (task, abortSignal) => {
|
|
|
37342
37450
|
releaseSlot();
|
|
37343
37451
|
}
|
|
37344
37452
|
};
|
|
37453
|
+
const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
|
|
37454
|
+
const resolveReactDoctorCacheDir = (projectDirectory) => {
|
|
37455
|
+
const cacheDirOverride = process.env["REACT_DOCTOR_CACHE_DIR"]?.trim();
|
|
37456
|
+
if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
|
|
37457
|
+
const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
|
|
37458
|
+
if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
|
|
37459
|
+
return Path.join(os.tmpdir(), `react-doctor-cache-${resolveUserCacheScope()}`, projectCacheSubdir(projectDirectory));
|
|
37460
|
+
};
|
|
37461
|
+
const resolveUserCacheScope = () => {
|
|
37462
|
+
try {
|
|
37463
|
+
return String(process.getuid?.() ?? os.userInfo().username);
|
|
37464
|
+
} catch {
|
|
37465
|
+
return "shared";
|
|
37466
|
+
}
|
|
37467
|
+
};
|
|
37345
37468
|
/**
|
|
37346
37469
|
* Resolves a path to its canonical, symlink-free form, falling back to
|
|
37347
37470
|
* the input when it cannot be realpath'd (broken symlink, permission
|
|
@@ -37637,13 +37760,28 @@ const checkDeadCode = async (options) => {
|
|
|
37637
37760
|
if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
|
|
37638
37761
|
const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
|
|
37639
37762
|
const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
|
|
37763
|
+
const tsConfigPath = resolveTsConfigPath(rootDirectory);
|
|
37764
|
+
const deslopJsModuleSpecifier = options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js");
|
|
37765
|
+
const cacheKey = options.cacheEnabled ? computeDeadCodeCacheKey({
|
|
37766
|
+
rootDirectory,
|
|
37767
|
+
entryPatterns,
|
|
37768
|
+
ignorePatterns,
|
|
37769
|
+
tsConfigPath,
|
|
37770
|
+
deslopJsModuleSpecifier,
|
|
37771
|
+
coreVersion: CORE_PACKAGE_VERSION
|
|
37772
|
+
}) : null;
|
|
37773
|
+
if (cacheKey !== null) {
|
|
37774
|
+
const cachedDiagnostics = lookupDeadCodeResultCache(resolveReactDoctorCacheDir(rootDirectory), cacheKey);
|
|
37775
|
+
options.onCacheOutcome?.(cachedDiagnostics !== null);
|
|
37776
|
+
if (cachedDiagnostics !== null) return [...cachedDiagnostics];
|
|
37777
|
+
}
|
|
37640
37778
|
const spawnAndRun = () => {
|
|
37641
37779
|
return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
|
|
37642
37780
|
rootDirectory,
|
|
37643
37781
|
entryPatterns,
|
|
37644
|
-
tsConfigPath
|
|
37782
|
+
tsConfigPath,
|
|
37645
37783
|
ignorePatterns,
|
|
37646
|
-
deslopJsModuleSpecifier
|
|
37784
|
+
deslopJsModuleSpecifier,
|
|
37647
37785
|
parseConcurrency: options.parseConcurrency
|
|
37648
37786
|
}), options.workerTimeoutMs ?? 12e4, options.abortSignal);
|
|
37649
37787
|
};
|
|
@@ -37704,6 +37842,7 @@ const checkDeadCode = async (options) => {
|
|
|
37704
37842
|
category: DEAD_CODE_CATEGORY
|
|
37705
37843
|
});
|
|
37706
37844
|
}
|
|
37845
|
+
if (cacheKey !== null) storeDeadCodeResultCache(resolveReactDoctorCacheDir(rootDirectory), cacheKey, diagnostics);
|
|
37707
37846
|
return diagnostics;
|
|
37708
37847
|
};
|
|
37709
37848
|
const DEAD_CODE_RULE_KEY_PREFIX = `${DEAD_CODE_PLUGIN}/`;
|
|
@@ -37806,12 +37945,15 @@ var Config = class Config extends Service()("react-doctor/Config") {
|
|
|
37806
37945
|
*/
|
|
37807
37946
|
var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
|
|
37808
37947
|
static layerNode = succeed$3(DeadCode, DeadCode.of({ run: (input) => unwrap(fn("DeadCode.run")(function* () {
|
|
37948
|
+
const cacheEnabled = yield* DeadCodeResultCacheEnabled;
|
|
37809
37949
|
return yield* tryPromise({
|
|
37810
37950
|
try: (signal) => checkDeadCode({
|
|
37811
37951
|
rootDirectory: input.rootDirectory,
|
|
37812
37952
|
parseConcurrency: input.parseConcurrency,
|
|
37813
37953
|
workerTimeoutMs: input.workerTimeoutMs,
|
|
37814
|
-
abortSignal: signal
|
|
37954
|
+
abortSignal: signal,
|
|
37955
|
+
cacheEnabled,
|
|
37956
|
+
onCacheOutcome: input.onCacheOutcome
|
|
37815
37957
|
}),
|
|
37816
37958
|
catch: (cause) => new ReactDoctorError({ reason: new DeadCodeAnalysisFailed({ cause }) })
|
|
37817
37959
|
}).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)));
|
|
@@ -38737,23 +38879,6 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
38737
38879
|
}
|
|
38738
38880
|
};
|
|
38739
38881
|
};
|
|
38740
|
-
const atomicWriteJson = (filePath, value) => {
|
|
38741
|
-
try {
|
|
38742
|
-
NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
|
|
38743
|
-
const temporaryPath = `${filePath}.${process.pid}.tmp`;
|
|
38744
|
-
NFS.writeFileSync(temporaryPath, JSON.stringify(value));
|
|
38745
|
-
NFS.renameSync(temporaryPath, filePath);
|
|
38746
|
-
} catch {
|
|
38747
|
-
return;
|
|
38748
|
-
}
|
|
38749
|
-
};
|
|
38750
|
-
const failOpenReadJson = (filePath, fallback) => {
|
|
38751
|
-
try {
|
|
38752
|
-
return JSON.parse(NFS.readFileSync(filePath, "utf8"));
|
|
38753
|
-
} catch {
|
|
38754
|
-
return fallback;
|
|
38755
|
-
}
|
|
38756
|
-
};
|
|
38757
38882
|
const validateDiagnostic = decodeUnknownSync(Diagnostic);
|
|
38758
38883
|
const decodeFileDiagnostics = (raw) => {
|
|
38759
38884
|
if (!Array.isArray(raw)) return null;
|
|
@@ -39817,22 +39942,70 @@ const hashFileContents = (filePath) => {
|
|
|
39817
39942
|
return null;
|
|
39818
39943
|
}
|
|
39819
39944
|
};
|
|
39820
|
-
|
|
39821
|
-
|
|
39822
|
-
|
|
39823
|
-
|
|
39824
|
-
|
|
39825
|
-
|
|
39826
|
-
|
|
39827
|
-
|
|
39828
|
-
|
|
39829
|
-
|
|
39830
|
-
|
|
39831
|
-
|
|
39832
|
-
|
|
39945
|
+
/**
|
|
39946
|
+
* Balanced LPT (longest-processing-time-first) lint batch planner. Where
|
|
39947
|
+
* `batchIncludePaths` greedily fills 100-file batches and leaves a small
|
|
39948
|
+
* remainder batch (617 files → 6×100 + 17, so the parallel pool's wall clock
|
|
39949
|
+
* is set by whichever 100-file chunk happened to collect the most work while
|
|
39950
|
+
* the remainder's worker idles), this planner keeps the SAME mandatory batch
|
|
39951
|
+
* count — `ceil(files / OXLINT_MAX_FILES_PER_BATCH)`, the JS-plugin
|
|
39952
|
+
* perf-cliff / native-binding SIGABRT guard — and assigns files largest-first,
|
|
39953
|
+
* each to the least-loaded batch by cumulative byte size. Every batch ends up
|
|
39954
|
+
* with an even share of files AND bytes, so no batch is a straggler; heavy
|
|
39955
|
+
* files are SPREAD across batches (the documented precondition for `cost`
|
|
39956
|
+
* ordering to beat `arrival`) instead of concentrated in one.
|
|
39957
|
+
*
|
|
39958
|
+
* Deliberately NOT worker-count-aware: splitting into extra batches to feed
|
|
39959
|
+
* idle workers was measured (619-file corpus, 10 cores) to REGRESS the lint
|
|
39960
|
+
* wall — each additional concurrent subprocess pays a contended cold start
|
|
39961
|
+
* (node boot + plugin import + native dlopen, ~180 ms uncontended and 2-3×
|
|
39962
|
+
* that in a full wave) that outweighs the smaller per-batch file share.
|
|
39963
|
+
*
|
|
39964
|
+
* Every batch also respects the spawn-args char budget; a file that fits no
|
|
39965
|
+
* existing batch opens a new one, mirroring `batchIncludePaths`. Within a
|
|
39966
|
+
* batch, files return in arrival order, and the assignment is fully
|
|
39967
|
+
* deterministic (ties broken by lowest batch index), so a plan is
|
|
39968
|
+
* reproducible for a fixed (files, sizes) input. Batch composition cannot
|
|
39969
|
+
* change diagnostics — rules that look beyond one file (`CROSS_FILE_RULE_IDS`)
|
|
39970
|
+
* read the OTHER files via the filesystem, never via batch-mates — so the
|
|
39971
|
+
* plan only moves wall-clock time.
|
|
39972
|
+
*
|
|
39973
|
+
* O(files × batches); the linear least-loaded scan is fine at real-world
|
|
39974
|
+
* scale (a 3,500-file monorepo is 36 batches ≈ 126k comparisons).
|
|
39975
|
+
*/
|
|
39976
|
+
const planLintBatches = ({ baseArgs, files, sizeByFile }) => {
|
|
39977
|
+
if (files.length === 0) return [];
|
|
39978
|
+
const batchCount = Math.ceil(files.length / 100);
|
|
39979
|
+
const maxFilesPerBatch = Math.ceil(files.length / batchCount);
|
|
39980
|
+
const baseArgsLengthChars = estimateArgsLength(baseArgs);
|
|
39981
|
+
const descendingSizeIndexes = [...files.keys()].sort((leftIndex, rightIndex) => (sizeByFile.get(files[rightIndex]) ?? 0) - (sizeByFile.get(files[leftIndex]) ?? 0));
|
|
39982
|
+
const batches = Array.from({ length: batchCount }, () => ({
|
|
39983
|
+
fileIndexes: [],
|
|
39984
|
+
totalSizeBytes: 0,
|
|
39985
|
+
argsLengthChars: baseArgsLengthChars
|
|
39986
|
+
}));
|
|
39987
|
+
for (const fileIndex of descendingSizeIndexes) {
|
|
39988
|
+
const entryLengthChars = files[fileIndex].length + 1;
|
|
39989
|
+
let targetBatch = null;
|
|
39990
|
+
for (const batch of batches) {
|
|
39991
|
+
if (batch.fileIndexes.length >= maxFilesPerBatch) continue;
|
|
39992
|
+
if (batch.fileIndexes.length > 0 && batch.argsLengthChars + entryLengthChars > 24e3) continue;
|
|
39993
|
+
if (targetBatch === null || batch.totalSizeBytes < targetBatch.totalSizeBytes || batch.totalSizeBytes === targetBatch.totalSizeBytes && batch.fileIndexes.length < targetBatch.fileIndexes.length) targetBatch = batch;
|
|
39994
|
+
}
|
|
39995
|
+
if (targetBatch === null) {
|
|
39996
|
+
targetBatch = {
|
|
39997
|
+
fileIndexes: [],
|
|
39998
|
+
totalSizeBytes: 0,
|
|
39999
|
+
argsLengthChars: baseArgsLengthChars
|
|
40000
|
+
};
|
|
40001
|
+
batches.push(targetBatch);
|
|
40002
|
+
}
|
|
40003
|
+
targetBatch.fileIndexes.push(fileIndex);
|
|
40004
|
+
targetBatch.totalSizeBytes += sizeByFile.get(files[fileIndex]) ?? 0;
|
|
40005
|
+
targetBatch.argsLengthChars += entryLengthChars;
|
|
39833
40006
|
}
|
|
40007
|
+
return batches.filter((batch) => batch.fileIndexes.length > 0).map((batch) => batch.fileIndexes.sort((leftIndex, rightIndex) => leftIndex - rightIndex).map((fileIndex) => files[fileIndex]));
|
|
39834
40008
|
};
|
|
39835
|
-
const sortSourceFilesByCost = (entries) => [...entries].sort((left, right) => right.sizeBytes - left.sizeBytes).map((entry) => entry.path);
|
|
39836
40009
|
/**
|
|
39837
40010
|
* Atomically (re)writes the generated oxlintrc.json. Used twice in
|
|
39838
40011
|
* the runner: once for the primary scan, once for the
|
|
@@ -39891,7 +40064,7 @@ const reactHooksJsPluginDropNote = (error) => {
|
|
|
39891
40064
|
* 6. always restore disable directives + clean up the temp dir
|
|
39892
40065
|
*/
|
|
39893
40066
|
const runOxlint = async (options) => {
|
|
39894
|
-
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 = "
|
|
40067
|
+
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;
|
|
39895
40068
|
const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
|
|
39896
40069
|
const severityControls = buildRuleSeverityControls(userConfig);
|
|
39897
40070
|
validateRuleRegistration();
|
|
@@ -39943,8 +40116,15 @@ const runOxlint = async (options) => {
|
|
|
39943
40116
|
"json",
|
|
39944
40117
|
...sharedArgs
|
|
39945
40118
|
];
|
|
39946
|
-
|
|
39947
|
-
const
|
|
40119
|
+
await yieldToEventLoop();
|
|
40120
|
+
const sizedScanFiles = includePaths === void 0 ? listSourceFilesWithSize(rootDirectory) : null;
|
|
40121
|
+
const candidateFiles = includePaths !== void 0 ? includePaths : (sizedScanFiles ?? []).map((entry) => entry.path);
|
|
40122
|
+
const sizeByFile = sizedScanFiles !== null && lintBatchOrdering === "cost" ? new Map(sizedScanFiles.map((entry) => [entry.path, entry.sizeBytes])) : null;
|
|
40123
|
+
const buildFileBatches = (passBaseArgs, passFiles) => sizeByFile !== null ? planLintBatches({
|
|
40124
|
+
baseArgs: passBaseArgs,
|
|
40125
|
+
files: passFiles,
|
|
40126
|
+
sizeByFile
|
|
40127
|
+
}) : batchIncludePaths(passBaseArgs, passFiles);
|
|
39948
40128
|
const runConfigOverFiles = async (buildConfigForPass, configFileName, files, fileProgress) => {
|
|
39949
40129
|
if (files.length === 0) return {
|
|
39950
40130
|
diagnostics: [],
|
|
@@ -39958,7 +40138,7 @@ const runOxlint = async (options) => {
|
|
|
39958
40138
|
};
|
|
39959
40139
|
const passConfigPath = Path.join(configDirectory, configFileName);
|
|
39960
40140
|
const passBaseArgs = makeBaseArgs(passConfigPath);
|
|
39961
|
-
const passFileBatches =
|
|
40141
|
+
const passFileBatches = buildFileBatches(passBaseArgs, files);
|
|
39962
40142
|
const spawnPass = () => spawnLintBatches({
|
|
39963
40143
|
baseArgs: passBaseArgs,
|
|
39964
40144
|
fileBatches: passFileBatches,
|
|
@@ -40007,6 +40187,7 @@ const runOxlint = async (options) => {
|
|
|
40007
40187
|
const cacheKeyByFile = /* @__PURE__ */ new Map();
|
|
40008
40188
|
const missFiles = [];
|
|
40009
40189
|
const replayedDiagnostics = [];
|
|
40190
|
+
let hashSliceStartedAt = performance.now();
|
|
40010
40191
|
for (const candidateFile of candidateFiles) {
|
|
40011
40192
|
const contentHash = hashFileContents(Path.resolve(rootDirectory, candidateFile));
|
|
40012
40193
|
if (contentHash === null) {
|
|
@@ -40018,6 +40199,10 @@ const runOxlint = async (options) => {
|
|
|
40018
40199
|
const cachedDiagnostics = cache.lookup(cacheKey);
|
|
40019
40200
|
if (cachedDiagnostics === null) missFiles.push(candidateFile);
|
|
40020
40201
|
else replayedDiagnostics.push(...cachedDiagnostics);
|
|
40202
|
+
if (performance.now() - hashSliceStartedAt >= 12) {
|
|
40203
|
+
await yieldToEventLoop();
|
|
40204
|
+
hashSliceStartedAt = performance.now();
|
|
40205
|
+
}
|
|
40021
40206
|
}
|
|
40022
40207
|
const cacheHitFileCount = candidateFiles.length - missFiles.length;
|
|
40023
40208
|
const fullResult = await runConfigOverFiles((overrides) => buildConfig({
|
|
@@ -40060,7 +40245,7 @@ const runOxlint = async (options) => {
|
|
|
40060
40245
|
]);
|
|
40061
40246
|
}
|
|
40062
40247
|
const baseArgs = makeBaseArgs(configPath);
|
|
40063
|
-
const fileBatches =
|
|
40248
|
+
const fileBatches = buildFileBatches(baseArgs, candidateFiles);
|
|
40064
40249
|
const runBatches = () => spawnLintBatches({
|
|
40065
40250
|
baseArgs,
|
|
40066
40251
|
fileBatches,
|
|
@@ -40947,7 +41132,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
40947
41132
|
const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
|
|
40948
41133
|
rootDirectory: scanDirectory,
|
|
40949
41134
|
parseConcurrency: deadCodeParseConcurrency,
|
|
40950
|
-
workerTimeoutMs: deadCodeTimeout.workerTimeoutMs
|
|
41135
|
+
workerTimeoutMs: deadCodeTimeout.workerTimeoutMs,
|
|
41136
|
+
onCacheOutcome: (didHitCache) => {
|
|
41137
|
+
deadCodeCacheHit = didHitCache;
|
|
41138
|
+
}
|
|
40951
41139
|
}).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
|
|
40952
41140
|
yield* set(deadCodeFailure, {
|
|
40953
41141
|
didFail: true,
|
|
@@ -40975,6 +41163,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
40975
41163
|
let lastReportedTotalFileCount = 0;
|
|
40976
41164
|
let lintCacheHitFileCount = null;
|
|
40977
41165
|
let lintCacheTotalFileCount = null;
|
|
41166
|
+
let deadCodeCacheHit = null;
|
|
40978
41167
|
const baseLintStream = linterService.run({
|
|
40979
41168
|
rootDirectory: scanDirectory,
|
|
40980
41169
|
project,
|
|
@@ -41104,6 +41293,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
41104
41293
|
securityScanFailed,
|
|
41105
41294
|
lintCacheHitFileCount,
|
|
41106
41295
|
lintCacheTotalFileCount,
|
|
41296
|
+
deadCodeCacheHit: lintFailureState.didFail ? null : deadCodeCacheHit,
|
|
41107
41297
|
suppressedRuleCounts: transform.summarizeSuppressions()
|
|
41108
41298
|
};
|
|
41109
41299
|
}).pipe(withSpan("runInspect", { attributes: {
|
|
@@ -41433,7 +41623,7 @@ const computeConfigFingerprint = (projectDirectory, version) => {
|
|
|
41433
41623
|
/** Display name used in client-facing messages and progress titles. */
|
|
41434
41624
|
const SERVER_DISPLAY_NAME = "React Doctor";
|
|
41435
41625
|
/** Server version reported in `serverInfo`; injected at build, `dev` from source. */
|
|
41436
|
-
const SERVER_VERSION = "0.6.
|
|
41626
|
+
const SERVER_VERSION = "0.6.3";
|
|
41437
41627
|
/** `Diagnostic.source` shown next to every published diagnostic. */
|
|
41438
41628
|
const DIAGNOSTIC_SOURCE = "react-doctor";
|
|
41439
41629
|
/**
|
|
@@ -43483,5 +43673,5 @@ const startLanguageServer = () => {
|
|
|
43483
43673
|
};
|
|
43484
43674
|
//#endregion
|
|
43485
43675
|
export { startLanguageServer };
|
|
43486
|
-
!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]="
|
|
43487
|
-
//# debugId=
|
|
43676
|
+
!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]="440ae812-d58d-5654-b3ad-1087b4aa2060")}catch(e){}}();
|
|
43677
|
+
//# debugId=440ae812-d58d-5654-b3ad-1087b4aa2060
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-doctor",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3-dev.20d81f6",
|
|
4
4
|
"description": "Your agent writes bad React. This catches it",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"accessibility",
|
|
@@ -65,8 +65,8 @@
|
|
|
65
65
|
"vscode-languageserver-textdocument": "^1.0.12",
|
|
66
66
|
"vscode-uri": "^3.1.0",
|
|
67
67
|
"yaml": "^2.9.0",
|
|
68
|
-
"deslop-js": "0.6.
|
|
69
|
-
"oxlint-plugin-react-doctor": "0.6.
|
|
68
|
+
"deslop-js": "0.6.3",
|
|
69
|
+
"oxlint-plugin-react-doctor": "0.6.3-dev.20d81f6"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@types/babel__code-frame": "^7.27.0",
|
|
@@ -74,9 +74,9 @@
|
|
|
74
74
|
"@xterm/headless": "^6.0.0",
|
|
75
75
|
"commander": "^14.0.3",
|
|
76
76
|
"ora": "^9.4.0",
|
|
77
|
-
"@react-doctor/api": "0.6.
|
|
78
|
-
"@react-doctor/core": "0.6.
|
|
79
|
-
"@react-doctor/language-server": "0.6.
|
|
77
|
+
"@react-doctor/api": "0.6.3",
|
|
78
|
+
"@react-doctor/core": "0.6.3",
|
|
79
|
+
"@react-doctor/language-server": "0.6.3"
|
|
80
80
|
},
|
|
81
81
|
"engines": {
|
|
82
82
|
"node": "^20.19.0 || >=22.13.0"
|