react-doctor 0.5.7 → 0.5.8
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 +1338 -293
- package/dist/index.d.ts +15 -1
- package/dist/index.js +912 -139
- package/dist/lsp.js +912 -139
- package/package.json +6 -6
package/dist/lsp.js
CHANGED
|
@@ -8,7 +8,7 @@ import path from "node:path";
|
|
|
8
8
|
import * as NodeChildProcess from "node:child_process";
|
|
9
9
|
import { spawn, spawnSync } from "node:child_process";
|
|
10
10
|
import * as ts from "typescript";
|
|
11
|
-
import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
|
|
11
|
+
import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
|
|
12
12
|
import { parseJSON5 } from "confbox";
|
|
13
13
|
import * as NodeUrl from "node:url";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
@@ -7224,7 +7224,7 @@ const provideContext$1 = /* @__PURE__ */ dual(2, (self, context) => {
|
|
|
7224
7224
|
return updateContext$1(self, merge$3(context));
|
|
7225
7225
|
});
|
|
7226
7226
|
/** @internal */
|
|
7227
|
-
const provideService$
|
|
7227
|
+
const provideService$3 = function() {
|
|
7228
7228
|
if (arguments.length === 1) return dual(2, (self, impl) => provideServiceImpl(self, arguments[0], impl));
|
|
7229
7229
|
return dual(3, (self, service, impl) => provideServiceImpl(self, service, impl)).apply(this, arguments);
|
|
7230
7230
|
};
|
|
@@ -7445,7 +7445,7 @@ const constScopeEmpty = { _tag: "Empty" };
|
|
|
7445
7445
|
/** @internal */
|
|
7446
7446
|
const scope = scopeTag;
|
|
7447
7447
|
/** @internal */
|
|
7448
|
-
const provideScope = /* @__PURE__ */ provideService$
|
|
7448
|
+
const provideScope = /* @__PURE__ */ provideService$3(scopeTag);
|
|
7449
7449
|
/** @internal */
|
|
7450
7450
|
const scoped$1 = (self) => withFiber$1((fiber) => {
|
|
7451
7451
|
const prev = fiber.context;
|
|
@@ -7888,7 +7888,7 @@ const makeLatchUnsafe = (open) => new Latch(open ?? false);
|
|
|
7888
7888
|
/** @internal */
|
|
7889
7889
|
const makeLatch = (open) => sync$2(() => makeLatchUnsafe(open));
|
|
7890
7890
|
/** @internal */
|
|
7891
|
-
const withTracerEnabled$1 = /* @__PURE__ */ provideService$
|
|
7891
|
+
const withTracerEnabled$1 = /* @__PURE__ */ provideService$3(TracerEnabled);
|
|
7892
7892
|
const bigint0 = /* @__PURE__ */ BigInt(0);
|
|
7893
7893
|
const NoopSpanProto = {
|
|
7894
7894
|
_tag: "Span",
|
|
@@ -7969,7 +7969,7 @@ const useSpan$1 = (name, ...args) => {
|
|
|
7969
7969
|
}));
|
|
7970
7970
|
});
|
|
7971
7971
|
};
|
|
7972
|
-
const provideParentSpan = /* @__PURE__ */ provideService$
|
|
7972
|
+
const provideParentSpan = /* @__PURE__ */ provideService$3(ParentSpan);
|
|
7973
7973
|
/** @internal */
|
|
7974
7974
|
const withParentSpan$1 = function() {
|
|
7975
7975
|
const dataFirst = isEffect$1(arguments[0]);
|
|
@@ -9536,7 +9536,7 @@ var CurrentMemoMap = class extends Service()("effect/Layer/CurrentMemoMap") {
|
|
|
9536
9536
|
* @category memo map
|
|
9537
9537
|
* @since 2.0.0
|
|
9538
9538
|
*/
|
|
9539
|
-
const buildWithMemoMap = /* @__PURE__ */ dual(3, (self, memoMap, scope) => provideService$
|
|
9539
|
+
const buildWithMemoMap = /* @__PURE__ */ dual(3, (self, memoMap, scope) => provideService$3(map$4(self.build(memoMap, scope), add(CurrentMemoMap, memoMap)), CurrentMemoMap, memoMap));
|
|
9540
9540
|
/**
|
|
9541
9541
|
* Builds a layer into an `Effect` value. Any resources associated with this
|
|
9542
9542
|
* layer will be released when the specified scope is closed unless their scope
|
|
@@ -10889,7 +10889,7 @@ const provide$1 = /* @__PURE__ */ dual((args) => isEffect$1(args[0]), (self, sou
|
|
|
10889
10889
|
/** @internal */
|
|
10890
10890
|
const repeatOrElse = /* @__PURE__ */ dual(3, (self, schedule, orElse) => flatMap$4(toStepWithMetadata(schedule), (step) => {
|
|
10891
10891
|
let meta = CurrentMetadata.defaultValue();
|
|
10892
|
-
return catch_$2(forever$2(tap$2(flatMap$4(suspend$3(() => provideService$
|
|
10892
|
+
return catch_$2(forever$2(tap$2(flatMap$4(suspend$3(() => provideService$3(self, CurrentMetadata, meta)), step), (meta_) => sync$2(() => {
|
|
10893
10893
|
meta = meta_;
|
|
10894
10894
|
})), { disableYield: true }), (error) => isDone$2(error) ? succeed$5(error.value) : orElse(error, meta.attempt === 0 ? none() : some(meta)));
|
|
10895
10895
|
}));
|
|
@@ -10897,7 +10897,7 @@ const repeatOrElse = /* @__PURE__ */ dual(3, (self, schedule, orElse) => flatMap
|
|
|
10897
10897
|
const retryOrElse = /* @__PURE__ */ dual(3, (self, policy, orElse) => flatMap$4(toStepWithMetadata(policy), (step) => {
|
|
10898
10898
|
let meta = CurrentMetadata.defaultValue();
|
|
10899
10899
|
let lastError;
|
|
10900
|
-
const loop = catch_$2(suspend$3(() => provideService$
|
|
10900
|
+
const loop = catch_$2(suspend$3(() => provideService$3(self, CurrentMetadata, meta)), (error) => {
|
|
10901
10901
|
lastError = error;
|
|
10902
10902
|
return flatMap$4(step(error), (meta_) => {
|
|
10903
10903
|
meta = meta_;
|
|
@@ -12977,7 +12977,7 @@ const updateContext = updateContext$1;
|
|
|
12977
12977
|
* @category Context
|
|
12978
12978
|
* @since 2.0.0
|
|
12979
12979
|
*/
|
|
12980
|
-
const provideService = provideService$
|
|
12980
|
+
const provideService$2 = provideService$3;
|
|
12981
12981
|
/**
|
|
12982
12982
|
* Scopes all resources used in this workflow to the lifetime of the workflow,
|
|
12983
12983
|
* ensuring that their finalizers are run as soon as this workflow completes
|
|
@@ -18019,6 +18019,20 @@ function decodeUnknownOption$1(schema, options) {
|
|
|
18019
18019
|
return asOption(decodeUnknownEffect(schema, options));
|
|
18020
18020
|
}
|
|
18021
18021
|
/**
|
|
18022
|
+
* Creates a synchronous decoder for `unknown` input.
|
|
18023
|
+
*
|
|
18024
|
+
* **Details**
|
|
18025
|
+
*
|
|
18026
|
+
* The returned function returns the decoded `Type` on success and throws an
|
|
18027
|
+
* `Error` with the `SchemaIssue.Issue` in its `cause` on decoding failure.
|
|
18028
|
+
*
|
|
18029
|
+
* @category decoding
|
|
18030
|
+
* @since 3.10.0
|
|
18031
|
+
*/
|
|
18032
|
+
function decodeUnknownSync$1(schema, options) {
|
|
18033
|
+
return asSync(decodeUnknownEffect(schema, options));
|
|
18034
|
+
}
|
|
18035
|
+
/**
|
|
18022
18036
|
* Creates an effectful encoder for `unknown` input.
|
|
18023
18037
|
*
|
|
18024
18038
|
* **Details**
|
|
@@ -18320,6 +18334,40 @@ function isSchemaError(u) {
|
|
|
18320
18334
|
*/
|
|
18321
18335
|
const decodeUnknownOption = decodeUnknownOption$1;
|
|
18322
18336
|
/**
|
|
18337
|
+
* Decodes an `unknown` input against a schema synchronously, returning the
|
|
18338
|
+
* decoded value or throwing an `Error` whose cause contains the schema issue.
|
|
18339
|
+
* Use this when you want to validate data at a boundary and treat a schema
|
|
18340
|
+
* mismatch as an exception. For typed input use `decodeSync`.
|
|
18341
|
+
*
|
|
18342
|
+
* **Details**
|
|
18343
|
+
*
|
|
18344
|
+
* Only service-free schemas can be decoded synchronously. For non-throwing
|
|
18345
|
+
* alternatives see `decodeUnknownOption`, `decodeUnknownExit`, or
|
|
18346
|
+
* `decodeUnknownEffect`. Options may be provided either when creating the
|
|
18347
|
+
* decoder or when applying it; application options override creation options.
|
|
18348
|
+
*
|
|
18349
|
+
* **Example** (Decoding with a transformation schema)
|
|
18350
|
+
*
|
|
18351
|
+
* ```ts
|
|
18352
|
+
* import { Schema } from "effect"
|
|
18353
|
+
*
|
|
18354
|
+
* const NumberFromString = Schema.NumberFromString
|
|
18355
|
+
*
|
|
18356
|
+
* console.log(Schema.decodeUnknownSync(NumberFromString)("42"))
|
|
18357
|
+
* // Output: 42
|
|
18358
|
+
*
|
|
18359
|
+
* Schema.decodeUnknownSync(NumberFromString)("not a number")
|
|
18360
|
+
* // throws SchemaError: NumberFromString
|
|
18361
|
+
* // └─ Encoded side transformation failure
|
|
18362
|
+
* // └─ NumberFromString
|
|
18363
|
+
* // └─ Expected a numeric string, actual "not a number"
|
|
18364
|
+
* ```
|
|
18365
|
+
*
|
|
18366
|
+
* @category decoding
|
|
18367
|
+
* @since 4.0.0
|
|
18368
|
+
*/
|
|
18369
|
+
const decodeUnknownSync = decodeUnknownSync$1;
|
|
18370
|
+
/**
|
|
18323
18371
|
* Encodes an `unknown` input against a schema synchronously, throwing a
|
|
18324
18372
|
* {@link SchemaError} on failure. Use this when you want to serialize data at a
|
|
18325
18373
|
* boundary and treat a schema mismatch as an unrecoverable error. For
|
|
@@ -25206,6 +25254,14 @@ const runWith = (self, f, onHalt) => suspend$2(() => {
|
|
|
25206
25254
|
return catchDone(flatMap$2(toTransform(self)(done$1(), scope), f), onHalt ? onHalt : succeed$2).pipe(onExit$1((exit) => close(scope, exit)));
|
|
25207
25255
|
});
|
|
25208
25256
|
/**
|
|
25257
|
+
* Provides a concrete service for a context key, removing that service
|
|
25258
|
+
* requirement from the returned channel.
|
|
25259
|
+
*
|
|
25260
|
+
* @category services
|
|
25261
|
+
* @since 2.0.0
|
|
25262
|
+
*/
|
|
25263
|
+
const provideService$1 = /* @__PURE__ */ dual(3, (self, key, service) => fromTransform$1((upstream, scope) => map$3(provideService$2(toTransform(self)(upstream, scope), key, service), provideService$2(key, service))));
|
|
25264
|
+
/**
|
|
25209
25265
|
* Runs a channel and applies an effect to each output element.
|
|
25210
25266
|
*
|
|
25211
25267
|
* **Example** (Running effects for each output)
|
|
@@ -26594,6 +26650,44 @@ const splitLines = (self) => self.channel.pipe(pipeTo(splitLines$1()), fromChann
|
|
|
26594
26650
|
*/
|
|
26595
26651
|
const ensuring = /* @__PURE__ */ dual(2, (self, finalizer) => fromChannel(ensuring$1(self.channel, finalizer)));
|
|
26596
26652
|
/**
|
|
26653
|
+
* Provides the stream with a single required service, eliminating that
|
|
26654
|
+
* requirement from its environment.
|
|
26655
|
+
*
|
|
26656
|
+
* **Example** (Providing a stream service)
|
|
26657
|
+
*
|
|
26658
|
+
* ```ts
|
|
26659
|
+
* import { Console, Context, Effect, Stream } from "effect"
|
|
26660
|
+
*
|
|
26661
|
+
* class Greeter extends Context.Service<Greeter, {
|
|
26662
|
+
* greet: (name: string) => string
|
|
26663
|
+
* }>()("Greeter") {}
|
|
26664
|
+
*
|
|
26665
|
+
* const stream = Stream.fromEffect(
|
|
26666
|
+
* Effect.service(Greeter).pipe(
|
|
26667
|
+
* Effect.map((greeter) => greeter.greet("Ada"))
|
|
26668
|
+
* )
|
|
26669
|
+
* )
|
|
26670
|
+
*
|
|
26671
|
+
* const program = Effect.gen(function*() {
|
|
26672
|
+
* const collected = yield* Stream.runCollect(
|
|
26673
|
+
* stream.pipe(
|
|
26674
|
+
* Stream.provideService(Greeter, {
|
|
26675
|
+
* greet: (name) => `Hello, ${name}`
|
|
26676
|
+
* })
|
|
26677
|
+
* )
|
|
26678
|
+
* )
|
|
26679
|
+
* yield* Console.log(collected)
|
|
26680
|
+
* })
|
|
26681
|
+
*
|
|
26682
|
+
* Effect.runPromise(program)
|
|
26683
|
+
* //=> ["Hello, Ada"]
|
|
26684
|
+
* ```
|
|
26685
|
+
*
|
|
26686
|
+
* @category services
|
|
26687
|
+
* @since 2.0.0
|
|
26688
|
+
*/
|
|
26689
|
+
const provideService = /* @__PURE__ */ dual(3, (self, key, service) => fromChannel(provideService$1(self.channel, key, service)));
|
|
26690
|
+
/**
|
|
26597
26691
|
* Runs a stream with a sink and returns the sink result.
|
|
26598
26692
|
*
|
|
26599
26693
|
* **Example** (Running a stream with a sink)
|
|
@@ -30023,7 +30117,7 @@ const make$8 = /* @__PURE__ */ fnUntraced(function* (options) {
|
|
|
30023
30117
|
const runFork = runForkWith(services);
|
|
30024
30118
|
const exportInterval = max(fromInputUnsafe(options.exportInterval), zero);
|
|
30025
30119
|
let disabledUntil = void 0;
|
|
30026
|
-
const client = filterStatusOk(get$4(services, HttpClient)).pipe(transformResponse(provideService(TracerPropagationEnabled, false)), retryTransient({
|
|
30120
|
+
const client = filterStatusOk(get$4(services, HttpClient)).pipe(transformResponse(provideService$2(TracerPropagationEnabled, false)), retryTransient({
|
|
30027
30121
|
schedule: policy,
|
|
30028
30122
|
times: 3
|
|
30029
30123
|
}));
|
|
@@ -32753,15 +32847,24 @@ const isMinifiedSource = (absolutePath) => {
|
|
|
32753
32847
|
if (fileDescriptor !== void 0) NFS.closeSync(fileDescriptor);
|
|
32754
32848
|
}
|
|
32755
32849
|
};
|
|
32756
|
-
const
|
|
32757
|
-
|
|
32850
|
+
const cachedIsLargeMinifiedByPath = /* @__PURE__ */ new Map();
|
|
32851
|
+
const clearMinifiedFileCache = () => {
|
|
32852
|
+
cachedIsLargeMinifiedByPath.clear();
|
|
32853
|
+
};
|
|
32854
|
+
const statSourceFileSize = (absolutePath) => {
|
|
32758
32855
|
try {
|
|
32759
|
-
|
|
32856
|
+
return NFS.statSync(absolutePath).size;
|
|
32760
32857
|
} catch {
|
|
32761
|
-
return
|
|
32858
|
+
return null;
|
|
32762
32859
|
}
|
|
32763
|
-
|
|
32764
|
-
|
|
32860
|
+
};
|
|
32861
|
+
const isLargeMinifiedFile = (absolutePath, knownSizeBytes) => {
|
|
32862
|
+
const cached = cachedIsLargeMinifiedByPath.get(absolutePath);
|
|
32863
|
+
if (cached !== void 0) return cached;
|
|
32864
|
+
const sizeBytes = knownSizeBytes === void 0 ? statSourceFileSize(absolutePath) : knownSizeBytes;
|
|
32865
|
+
const result = sizeBytes !== null && sizeBytes >= 2e4 && isMinifiedSource(absolutePath);
|
|
32866
|
+
cachedIsLargeMinifiedByPath.set(absolutePath, result);
|
|
32867
|
+
return result;
|
|
32765
32868
|
};
|
|
32766
32869
|
const isErrnoException = (error) => error instanceof Error && "code" in error;
|
|
32767
32870
|
const IGNORABLE_READDIR_ERROR_CODES = new Set([
|
|
@@ -33627,6 +33730,7 @@ const MILLISECONDS_PER_SECOND = 1e3;
|
|
|
33627
33730
|
const SCORE_API_URL = "https://www.react.doctor/api/score";
|
|
33628
33731
|
const FETCH_TIMEOUT_MS = 1e4;
|
|
33629
33732
|
const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
|
|
33733
|
+
const PER_WORKER_MEM_BUDGET_BYTES = 1024 * 1024 * 1024;
|
|
33630
33734
|
const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
|
|
33631
33735
|
const ADOPTABLE_LINT_CONFIG_FILENAMES = [".oxlintrc.json", ".eslintrc.json"];
|
|
33632
33736
|
const GIT_SHOW_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
@@ -33699,7 +33803,16 @@ const CONFIG_FINGERPRINT_FILENAMES = [
|
|
|
33699
33803
|
];
|
|
33700
33804
|
const OXLINT_OUTPUT_MAX_BYTES = 50 * 1024 * 1024;
|
|
33701
33805
|
const OXLINT_SPAWN_TIMEOUT_MS = 6e4;
|
|
33806
|
+
const NODE_COMPILE_CACHE_DIR_NAME = "node-compile-cache";
|
|
33807
|
+
const DEAD_CODE_WORKER_TIMEOUT_MS = 12e4;
|
|
33808
|
+
const OXLINT_SPLIT_TOTAL_BUDGET_MS = 18e4;
|
|
33809
|
+
const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
|
|
33810
|
+
const LINT_PHASE_TIMEOUT_MS = 3e5;
|
|
33811
|
+
const SCAN_TOTAL_DEADLINE_MS = 9e5;
|
|
33702
33812
|
const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
|
|
33813
|
+
const DEAD_CODE_TIMEOUT_CEILING_MS = 6e5;
|
|
33814
|
+
const DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS = 3e4;
|
|
33815
|
+
const DEAD_CODE_OVERLAP_PARSE_SHARE = .4;
|
|
33703
33816
|
const RECOMMENDED_PNPM_MINIMUM_RELEASE_AGE_MINUTES = 10080;
|
|
33704
33817
|
const REACT_SERVER_DOM_PACKAGES = [
|
|
33705
33818
|
"react-server-dom-webpack",
|
|
@@ -33734,9 +33847,13 @@ const CONFIG_CACHE_TTL_MS = 300 * 1e3;
|
|
|
33734
33847
|
const SOCKET_FREE_PURL_API_BASE = "https://firewall-api.socket.dev/purl";
|
|
33735
33848
|
const SOCKET_PACKAGE_PAGE_BASE = "https://socket.dev/npm/package";
|
|
33736
33849
|
const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
|
|
33850
|
+
const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
|
|
33851
|
+
const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
|
|
33737
33852
|
const SUPPLY_CHAIN_PLUGIN = "socket";
|
|
33738
33853
|
const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
|
|
33739
33854
|
const SUPPLY_CHAIN_CATEGORY = "Security";
|
|
33855
|
+
const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
|
|
33856
|
+
const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
|
|
33740
33857
|
const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
|
|
33741
33858
|
const TSCONFIG_FILENAME = "tsconfig.json";
|
|
33742
33859
|
const isRelativeExtendsValue = (extendsValue) => extendsValue.startsWith("./") || extendsValue.startsWith("../") || Path.isAbsolute(extendsValue);
|
|
@@ -35091,6 +35208,11 @@ var OxlintBatchExceeded = class extends TaggedErrorClass()("OxlintBatchExceeded"
|
|
|
35091
35208
|
}
|
|
35092
35209
|
}
|
|
35093
35210
|
};
|
|
35211
|
+
var ScanDeadlineExceeded = class extends TaggedErrorClass()("ScanDeadlineExceeded", { detail: String$1 }) {
|
|
35212
|
+
get message() {
|
|
35213
|
+
return `Scan exceeded its overall time budget: ${this.detail}`;
|
|
35214
|
+
}
|
|
35215
|
+
};
|
|
35094
35216
|
var OxlintSpawnFailed = class extends TaggedErrorClass()("OxlintSpawnFailed", { cause: Unknown }) {
|
|
35095
35217
|
get message() {
|
|
35096
35218
|
return `Failed to run oxlint: ${pretty(fail$6(this.cause))}`;
|
|
@@ -35154,6 +35276,7 @@ var GitBaseBranchInvalid = class extends TaggedErrorClass()("GitBaseBranchInvali
|
|
|
35154
35276
|
const ReactDoctorErrorReason = Union([
|
|
35155
35277
|
OxlintUnavailable,
|
|
35156
35278
|
OxlintBatchExceeded,
|
|
35279
|
+
ScanDeadlineExceeded,
|
|
35157
35280
|
OxlintSpawnFailed,
|
|
35158
35281
|
OxlintOutputUnparseable,
|
|
35159
35282
|
ConfigParseFailed,
|
|
@@ -35204,15 +35327,105 @@ const layerOtlp = unwrap$3(gen(function* () {
|
|
|
35204
35327
|
}).pipe(provide$2(layer$8));
|
|
35205
35328
|
}).pipe(orDie));
|
|
35206
35329
|
/**
|
|
35207
|
-
*
|
|
35208
|
-
* `
|
|
35209
|
-
|
|
35330
|
+
* Read a positive-millisecond timeout from an env var, falling back to
|
|
35331
|
+
* `defaultMs` when the var is unset, non-finite, or not strictly positive.
|
|
35332
|
+
*/
|
|
35333
|
+
const readPositiveEnvMs = (envVarName, defaultMs) => {
|
|
35334
|
+
const rawValue = process.env[envVarName];
|
|
35335
|
+
if (rawValue === void 0) return defaultMs;
|
|
35336
|
+
const parsedValue = Number(rawValue);
|
|
35337
|
+
if (!Number.isFinite(parsedValue) || parsedValue <= 0) return defaultMs;
|
|
35338
|
+
return parsedValue;
|
|
35339
|
+
};
|
|
35340
|
+
const CGROUP_V2_MEMORY_MAX_PATH = "/sys/fs/cgroup/memory.max";
|
|
35341
|
+
const CGROUP_V1_MEMORY_LIMIT_PATH = "/sys/fs/cgroup/memory/memory.limit_in_bytes";
|
|
35342
|
+
const CGROUP_UNLIMITED_SENTINEL_BYTES = Number.MAX_SAFE_INTEGER;
|
|
35343
|
+
/**
|
|
35344
|
+
* Parses one raw cgroup memory-limit file value into a positive byte count, or
|
|
35345
|
+
* `undefined` when it represents "no limit" (the v2 `"max"` literal, an empty
|
|
35346
|
+
* read, a non-positive / non-finite value, or v1's near-2^63 unlimited
|
|
35347
|
+
* sentinel). Pure and exported so the classification is unit-testable without
|
|
35348
|
+
* touching the filesystem.
|
|
35349
|
+
*/
|
|
35350
|
+
const parseCgroupMemoryLimitBytes = (raw) => {
|
|
35351
|
+
if (raw === void 0) return void 0;
|
|
35352
|
+
const trimmed = raw.trim();
|
|
35353
|
+
if (trimmed === "" || trimmed === "max") return void 0;
|
|
35354
|
+
const parsed = Number(trimmed);
|
|
35355
|
+
if (!Number.isFinite(parsed) || parsed <= 0 || parsed >= CGROUP_UNLIMITED_SENTINEL_BYTES) return;
|
|
35356
|
+
return parsed;
|
|
35357
|
+
};
|
|
35358
|
+
const CGROUP_MEMORY_LIMIT_PATHS = [CGROUP_V2_MEMORY_MAX_PATH, CGROUP_V1_MEMORY_LIMIT_PATH];
|
|
35359
|
+
/**
|
|
35360
|
+
* Reads this process's cgroup memory limit in bytes from the first candidate
|
|
35361
|
+
* path that yields a real limit, or `undefined` when none does — no cgroup, no
|
|
35362
|
+
* limit, or the files are unreadable (e.g. macOS / Windows dev machines).
|
|
35363
|
+
* `os.totalmem()` reports the HOST total and ignores cgroup memory limits, so a
|
|
35364
|
+
* memory-constrained container over-reports total memory; `resolveAutoScan-
|
|
35365
|
+
* Concurrency` takes `min(totalmem, this)` to honor the limit.
|
|
35366
|
+
*
|
|
35367
|
+
* The cgroup v2 read is the mount-root `memory.max`, which IS the container's
|
|
35368
|
+
* limit under the standard cgroup-namespace setup CI runners use (the
|
|
35369
|
+
* container's own cgroup is the root of its namespaced view). A process in a
|
|
35370
|
+
* non-namespaced nested/delegated cgroup whose root reads `"max"` is not
|
|
35371
|
+
* detected here and falls back to the host total; the EAGAIN/ENOMEM serial
|
|
35372
|
+
* replay in `spawnLintBatches` remains the runtime backstop for that case.
|
|
35373
|
+
*
|
|
35374
|
+
* `candidatePaths` is injectable so tests exercise the v2-wins-over-v1
|
|
35375
|
+
* precedence, the skip-unreadable fallback, and the all-missing case without a
|
|
35376
|
+
* real `/sys/fs/cgroup`.
|
|
35377
|
+
*/
|
|
35378
|
+
const readCgroupMemoryLimitBytes = (candidatePaths = CGROUP_MEMORY_LIMIT_PATHS) => {
|
|
35379
|
+
for (const limitPath of candidatePaths) {
|
|
35380
|
+
let raw;
|
|
35381
|
+
try {
|
|
35382
|
+
raw = fs.readFileSync(limitPath, "utf8");
|
|
35383
|
+
} catch {
|
|
35384
|
+
continue;
|
|
35385
|
+
}
|
|
35386
|
+
const limitBytes = parseCgroupMemoryLimitBytes(raw);
|
|
35387
|
+
if (limitBytes !== void 0) return limitBytes;
|
|
35388
|
+
}
|
|
35389
|
+
};
|
|
35390
|
+
/**
|
|
35391
|
+
* Clamps a requested lint worker count to `[MIN_SCAN_CONCURRENCY,
|
|
35392
|
+
* HARD_MAX_SCAN_CONCURRENCY]` as a finite integer. This is the explicit-pin and
|
|
35393
|
+
* spawn-boundary clamp — the memory-and-core-budgeted auto count comes from
|
|
35394
|
+
* `resolveAutoScanConcurrency`. Out-of-range or non-finite requests degrade to
|
|
35210
35395
|
* `MIN_SCAN_CONCURRENCY` rather than oversubscribing or running zero workers.
|
|
35211
35396
|
*/
|
|
35212
35397
|
const resolveScanConcurrency = (requested) => {
|
|
35213
|
-
|
|
35214
|
-
|
|
35215
|
-
|
|
35398
|
+
if (!Number.isFinite(requested) || requested < 1) return 1;
|
|
35399
|
+
return Math.min(Math.floor(requested), 32);
|
|
35400
|
+
};
|
|
35401
|
+
const readSystemFacts = () => ({
|
|
35402
|
+
availableCores: os.availableParallelism(),
|
|
35403
|
+
totalMemoryBytes: os.totalmem(),
|
|
35404
|
+
cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
|
|
35405
|
+
});
|
|
35406
|
+
/**
|
|
35407
|
+
* Auto lint-worker count: the smaller of the (cgroup-CPU-aware) core count and
|
|
35408
|
+
* the number of `PER_WORKER_MEM_BUDGET_BYTES` workers that fit in available
|
|
35409
|
+
* memory, then clamped to `[MIN, HARD_MAX]` by `resolveScanConcurrency`.
|
|
35410
|
+
*
|
|
35411
|
+
* `os.availableParallelism()` already respects cgroup CPU quotas, so the core
|
|
35412
|
+
* term needs no help. Available memory is `os.totalmem()` floored by the cgroup
|
|
35413
|
+
* memory limit — `os.freemem()` is deliberately NOT used: it excludes
|
|
35414
|
+
* reclaimable page cache and reads near-zero on macOS / cache-heavy Linux, which
|
|
35415
|
+
* would collapse the auto path to a single worker. `os.totalmem()` reports the
|
|
35416
|
+
* host total even inside a container, so the cgroup limit (read directly,
|
|
35417
|
+
* because Node doesn't fold it into `totalmem()`) is the real ceiling there.
|
|
35418
|
+
*
|
|
35419
|
+
* `facts` is injectable so tests exercise core-bound, memory-bound, cgroup-
|
|
35420
|
+
* limited, and ceiling cases without mocking `os` or the filesystem.
|
|
35421
|
+
*/
|
|
35422
|
+
const resolveAutoScanConcurrency = (facts = readSystemFacts()) => {
|
|
35423
|
+
const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
|
|
35424
|
+
const memoryBoundedWorkers = Math.floor(availableMemoryBytes / PER_WORKER_MEM_BUDGET_BYTES);
|
|
35425
|
+
return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
|
|
35426
|
+
};
|
|
35427
|
+
const resolveLintBatchOrdering = () => {
|
|
35428
|
+
return process.env["REACT_DOCTOR_LINT_BATCH_ORDERING"]?.trim().toLowerCase() === "cost" ? "cost" : "arrival";
|
|
35216
35429
|
};
|
|
35217
35430
|
/**
|
|
35218
35431
|
* Per-batch oxlint wall-clock budget. Reads from the env var on
|
|
@@ -35220,11 +35433,38 @@ const resolveScanConcurrency = (requested) => {
|
|
|
35220
35433
|
* microVMs without recompiling react-doctor. Tests override via
|
|
35221
35434
|
* `Layer.succeed(OxlintSpawnTimeoutMs, ...)`.
|
|
35222
35435
|
*/
|
|
35223
|
-
var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => {
|
|
35224
|
-
|
|
35225
|
-
|
|
35436
|
+
var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS", OXLINT_SPAWN_TIMEOUT_MS) }) {};
|
|
35437
|
+
/**
|
|
35438
|
+
* Effect-side cap on the lint phase. The env var lets CI / eval runners
|
|
35439
|
+
* raise the phase budget for slow large repos without recompiling.
|
|
35440
|
+
* Tests override via `Layer.succeed(LintPhaseTimeoutMs, ...)`.
|
|
35441
|
+
*/
|
|
35442
|
+
var LintPhaseTimeoutMs = class extends Reference("react-doctor/LintPhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_LINT_PHASE_TIMEOUT_MS", LINT_PHASE_TIMEOUT_MS) }) {};
|
|
35443
|
+
/**
|
|
35444
|
+
* Effect-side cap on the dead-code phase, sitting above the in-worker
|
|
35445
|
+
* timeout as a runtime-independent backstop. The env var raises it for
|
|
35446
|
+
* type-heavy projects; tests override via
|
|
35447
|
+
* `Layer.succeed(DeadCodePhaseTimeoutMs, ...)`.
|
|
35448
|
+
*/
|
|
35449
|
+
var DeadCodePhaseTimeoutMs = class extends Reference("react-doctor/DeadCodePhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_DEAD_CODE_PHASE_TIMEOUT_MS", DEAD_CODE_PHASE_TIMEOUT_MS) }) {};
|
|
35450
|
+
/**
|
|
35451
|
+
* Overall scan deadline backstop, bounding everything the per-phase
|
|
35452
|
+
* timeouts don't (wedged git / IO). The env var raises it for very
|
|
35453
|
+
* large repos; tests override via `Layer.succeed(ScanDeadlineMs, ...)`.
|
|
35454
|
+
*/
|
|
35455
|
+
var ScanDeadlineMs = class extends Reference("react-doctor/ScanDeadlineMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_SCAN_DEADLINE_MS", SCAN_TOTAL_DEADLINE_MS) }) {};
|
|
35456
|
+
/**
|
|
35457
|
+
* Wall-clock budget for the supply-chain check when it runs on a background
|
|
35458
|
+
* fiber overlapping the lint pass. Reads from the env var on startup so the
|
|
35459
|
+
* eval harness can raise the budget under sandbox microVMs (slower network)
|
|
35460
|
+
* without recompiling react-doctor. Tests override via
|
|
35461
|
+
* `Layer.succeed(SupplyChainOverlapTimeoutMs, ...)`.
|
|
35462
|
+
*/
|
|
35463
|
+
var SupplyChainOverlapTimeoutMs = class extends Reference("react-doctor/SupplyChainOverlapTimeoutMs", { defaultValue: () => {
|
|
35464
|
+
const raw = process.env["REACT_DOCTOR_SUPPLY_CHAIN_TIMEOUT_MS"];
|
|
35465
|
+
if (raw === void 0) return SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS;
|
|
35226
35466
|
const parsed = Number(raw);
|
|
35227
|
-
if (!Number.isFinite(parsed) || parsed <= 0) return
|
|
35467
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS;
|
|
35228
35468
|
return parsed;
|
|
35229
35469
|
} }) {};
|
|
35230
35470
|
/**
|
|
@@ -35235,31 +35475,93 @@ var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTime
|
|
|
35235
35475
|
*/
|
|
35236
35476
|
var OxlintOutputMaxBytes = class extends Reference("react-doctor/OxlintOutputMaxBytes", { defaultValue: () => OXLINT_OUTPUT_MAX_BYTES }) {};
|
|
35237
35477
|
/**
|
|
35238
|
-
* Number of oxlint subprocesses the lint pass runs in parallel. Defaults
|
|
35239
|
-
*
|
|
35240
|
-
* the box
|
|
35241
|
-
*
|
|
35242
|
-
*
|
|
35243
|
-
*
|
|
35244
|
-
*
|
|
35245
|
-
*
|
|
35246
|
-
*
|
|
35247
|
-
*
|
|
35478
|
+
* Number of oxlint subprocesses the lint pass runs in parallel. Defaults to a
|
|
35479
|
+
* memory-and-core-budgeted auto count (`resolveAutoScanConcurrency`) so large
|
|
35480
|
+
* repos scan fast out of the box without OOMing the native binding on a
|
|
35481
|
+
* high-core / low-memory box; `spawnLintBatches` transparently falls back to a
|
|
35482
|
+
* single worker if a parallel run still exhausts system resources. The CLI's
|
|
35483
|
+
* `--no-parallel` flag forces serial via `Layer.succeed`; the
|
|
35484
|
+
* `REACT_DOCTOR_PARALLEL` env var seeds the default for programmatic / CI
|
|
35485
|
+
* callers that never touch the flag — parallelism is opt-OUT, so only the
|
|
35486
|
+
* explicit serial values pin one worker:
|
|
35487
|
+
*
|
|
35488
|
+
* - unset / `auto` / `true` / `on` → memory-and-core-budgeted auto count
|
|
35248
35489
|
* - `0` / `false` / `off` → `1` (serial)
|
|
35249
35490
|
* - a positive integer → that many workers (clamped)
|
|
35250
|
-
* - any other value →
|
|
35491
|
+
* - any other value → memory-and-core-budgeted auto count
|
|
35251
35492
|
*
|
|
35252
35493
|
* The resolved value is always within
|
|
35253
|
-
* `[MIN_SCAN_CONCURRENCY,
|
|
35494
|
+
* `[MIN_SCAN_CONCURRENCY, HARD_MAX_SCAN_CONCURRENCY]`.
|
|
35254
35495
|
*/
|
|
35255
35496
|
var OxlintConcurrency = class extends Reference("react-doctor/OxlintConcurrency", { defaultValue: () => {
|
|
35256
35497
|
const raw = process.env["REACT_DOCTOR_PARALLEL"];
|
|
35257
|
-
if (raw === void 0) return
|
|
35498
|
+
if (raw === void 0) return resolveAutoScanConcurrency();
|
|
35258
35499
|
const normalized = raw.trim().toLowerCase();
|
|
35259
35500
|
if (normalized === "0" || normalized === "false" || normalized === "off") return 1;
|
|
35260
35501
|
const parsed = Number.parseInt(normalized, 10);
|
|
35261
35502
|
if (Number.isInteger(parsed) && parsed > 0) return resolveScanConcurrency(parsed);
|
|
35262
|
-
return
|
|
35503
|
+
return resolveAutoScanConcurrency();
|
|
35504
|
+
} }) {};
|
|
35505
|
+
/**
|
|
35506
|
+
* Three-state control for overlapping the dead-code pass with the lint pass —
|
|
35507
|
+
* forking dead-code as a child fiber that runs DURING lint instead of strictly
|
|
35508
|
+
* after it.
|
|
35509
|
+
*
|
|
35510
|
+
* - `"auto"` (default) / `"off"` → strictly SEQUENTIAL: dead-code runs after
|
|
35511
|
+
* lint with the full core budget. Both deslop's parse pool and the oxlint
|
|
35512
|
+
* pool are CPU-bound and each size themselves to all cores, so overlapping
|
|
35513
|
+
* them only oversubscribes (~2x the cores) and starves the parse pass past
|
|
35514
|
+
* its timeout — for no wall-clock win, since there are no spare cores to
|
|
35515
|
+
* absorb the second pass. Sequential is both faster per-phase and safe.
|
|
35516
|
+
* - `"on"` → force the overlap anyway. The orchestrator then SPLITS the core
|
|
35517
|
+
* budget (`DEAD_CODE_OVERLAP_PARSE_SHARE`): deslop's parse pool is capped
|
|
35518
|
+
* and lint shrinks to the remainder, so the two sum to the cores instead of
|
|
35519
|
+
* doubling them, and the dead-code timeout scales up for the reduced share.
|
|
35520
|
+
*
|
|
35521
|
+
* Seeded from `REACT_DOCTOR_DEAD_CODE_OVERLAP` so operators get a redeploy-free
|
|
35522
|
+
* switch; tests pin it via `Layer.succeed(DeadCodeOverlap, ...)`.
|
|
35523
|
+
*/
|
|
35524
|
+
var DeadCodeOverlap = class extends Reference("react-doctor/DeadCodeOverlap", { defaultValue: () => {
|
|
35525
|
+
const raw = process.env["REACT_DOCTOR_DEAD_CODE_OVERLAP"]?.trim().toLowerCase();
|
|
35526
|
+
if (raw === "on" || raw === "true" || raw === "1") return "on";
|
|
35527
|
+
if (raw === "off" || raw === "false" || raw === "0") return "off";
|
|
35528
|
+
return "auto";
|
|
35529
|
+
} }) {};
|
|
35530
|
+
/**
|
|
35531
|
+
* How the full-scan lint pass orders its file batches. `"arrival"` (the
|
|
35532
|
+
* default) keeps `git ls-files` discovery order. `"cost"` opts into LPT (feed
|
|
35533
|
+
* the largest files first); set `REACT_DOCTOR_LINT_BATCH_ORDERING=cost`. NOTE:
|
|
35534
|
+
* `cost` is OFF by default because the current sort-desc-then-chunk-100 packs
|
|
35535
|
+
* the heaviest files into one wave-1 batch — on size-skewed repos that mega-
|
|
35536
|
+
* batch is a straggler (and can trip the per-batch timeout + split), measurably
|
|
35537
|
+
* regressing the common full-scan case. LPT needs the heavy files SPREAD across
|
|
35538
|
+
* batches before `cost` earns the default. Tests override via
|
|
35539
|
+
* `Layer.succeed(LintBatchOrdering, ...)`. Diff / staged scans never reach this
|
|
35540
|
+
* — they pass user-scoped `includePaths` that skip discovery and stay in
|
|
35541
|
+
* arrival order; only the full-scan branch reads it.
|
|
35542
|
+
*/
|
|
35543
|
+
var LintBatchOrdering = class extends Reference("react-doctor/LintBatchOrdering", { defaultValue: resolveLintBatchOrdering }) {};
|
|
35544
|
+
const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
|
|
35545
|
+
/**
|
|
35546
|
+
* Whether the per-file lint cache (`runners/oxlint/file-lint-cache.ts`) is
|
|
35547
|
+
* active. Defaults ON — repeat scans re-lint only the files whose content
|
|
35548
|
+
* changed, and correctness is guaranteed byte-identical to a cold scan by the
|
|
35549
|
+
* always-fresh cross-file sidecar. Opt-OUT, two knobs (matching the whole-repo
|
|
35550
|
+
* scan cache's `REACT_DOCTOR_NO_CACHE`):
|
|
35551
|
+
*
|
|
35552
|
+
* - `REACT_DOCTOR_NO_CACHE` — the global off-switch; disables BOTH the
|
|
35553
|
+
* whole-repo scan cache and this per-file cache.
|
|
35554
|
+
* - `REACT_DOCTOR_NO_FILE_CACHE` — granular: bust only the per-file cache
|
|
35555
|
+
* while keeping the whole-repo short-circuit.
|
|
35556
|
+
*
|
|
35557
|
+
* Tests override via `Layer.succeed(PerFileLintCacheEnabled, false)`.
|
|
35558
|
+
*/
|
|
35559
|
+
var PerFileLintCacheEnabled = class extends Reference("react-doctor/PerFileLintCacheEnabled", { defaultValue: () => {
|
|
35560
|
+
const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
|
|
35561
|
+
const noFileCache = process.env["REACT_DOCTOR_NO_FILE_CACHE"]?.toLowerCase() ?? "";
|
|
35562
|
+
if (CACHE_DISABLED_VALUES.has(noCache)) return false;
|
|
35563
|
+
if (CACHE_DISABLED_VALUES.has(noFileCache)) return false;
|
|
35564
|
+
return true;
|
|
35263
35565
|
} }) {};
|
|
35264
35566
|
const DIAGNOSTIC_SURFACES = [
|
|
35265
35567
|
"cli",
|
|
@@ -35617,6 +35919,8 @@ const assignFixGroups = (diagnostics) => {
|
|
|
35617
35919
|
};
|
|
35618
35920
|
});
|
|
35619
35921
|
};
|
|
35922
|
+
const compareStrings = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
35923
|
+
const sortDiagnosticsStable = (diagnostics) => [...diagnostics].sort((left, right) => compareStrings(left.filePath, right.filePath) || left.line - right.line || left.column - right.column || compareStrings(left.plugin, right.plugin) || compareStrings(left.rule, right.rule) || compareStrings(left.severity, right.severity) || compareStrings(left.message, right.message));
|
|
35620
35924
|
const getDirectDependencyNames = (packageJson) => new Set([...Object.keys(packageJson.dependencies ?? {}), ...Object.keys(packageJson.devDependencies ?? {})]);
|
|
35621
35925
|
const buildExpoCheckContext = (rootDirectory, expoVersion) => {
|
|
35622
35926
|
const packageJson = readPackageJson(Path.join(rootDirectory, "package.json"));
|
|
@@ -36123,10 +36427,15 @@ const buildHardeningDiagnostic = (input) => ({
|
|
|
36123
36427
|
column: input.column ?? 0,
|
|
36124
36428
|
category: "Security"
|
|
36125
36429
|
});
|
|
36126
|
-
const checkPnpmHardening = (
|
|
36127
|
-
if (!isPnpmManagedProject(
|
|
36128
|
-
const workspacePath = Path.join(
|
|
36129
|
-
const
|
|
36430
|
+
const checkPnpmHardening = (scanDirectory) => {
|
|
36431
|
+
if (!isPnpmManagedProject(scanDirectory)) return [];
|
|
36432
|
+
const workspacePath = Path.join(scanDirectory, PNPM_WORKSPACE_FILE);
|
|
36433
|
+
const hasWorkspaceFile = isFile(workspacePath);
|
|
36434
|
+
if (!hasWorkspaceFile) {
|
|
36435
|
+
const monorepoRoot = findMonorepoRoot(scanDirectory);
|
|
36436
|
+
if (monorepoRoot !== null && isFile(Path.join(monorepoRoot, PNPM_WORKSPACE_FILE))) return [];
|
|
36437
|
+
}
|
|
36438
|
+
const settings = parseHardeningSettings(hasWorkspaceFile ? NFS.readFileSync(workspacePath, "utf-8") : "");
|
|
36130
36439
|
const diagnostics = [];
|
|
36131
36440
|
if (settings.minimumReleaseAge === null) diagnostics.push(buildHardeningDiagnostic({
|
|
36132
36441
|
message: "pnpm-workspace.yaml is missing `minimumReleaseAge` — newly published versions can ship malware that gets caught and unpublished within hours",
|
|
@@ -36911,6 +37220,22 @@ process.stdin.on("end", () => {
|
|
|
36911
37220
|
...(workerInput.ignorePatterns.length > 0
|
|
36912
37221
|
? { ignorePatterns: workerInput.ignorePatterns }
|
|
36913
37222
|
: {}),
|
|
37223
|
+
// We consume only deslop's GRAPH-based findings (unusedFiles, unusedExports,
|
|
37224
|
+
// unusedDependencies, circularDependencies). Everything else deslop can compute
|
|
37225
|
+
// is pure wasted work for us, and it's the bulk of the runtime:
|
|
37226
|
+
// - semantic: a full TS Program for unusedTypes/enum/class-members/
|
|
37227
|
+
// misclassifiedDependencies (~37-45% of the phase).
|
|
37228
|
+
// - reportCodeQuality: the duplicate-block, complexity, feature-flag,
|
|
37229
|
+
// TypeScript-smell, private-type-leak and re-export-cycle detectors. These
|
|
37230
|
+
// are the single most expensive pass — duplicate-block detection alone was
|
|
37231
|
+
// ~83s of a ~130s Sentry scan — so skipping them is an ~8.5x dead-code
|
|
37232
|
+
// speedup on a large repo.
|
|
37233
|
+
// Both are provably safe: the consumed graph findings are computed by their own
|
|
37234
|
+
// detectors, independent of these passes (confirmed byte-identical on
|
|
37235
|
+
// excalidraw + mui-material + sentry). tsConfigPath stays — the module resolver
|
|
37236
|
+
// needs it for path-alias resolution in the import graph.
|
|
37237
|
+
semantic: { enabled: false },
|
|
37238
|
+
reportCodeQuality: false,
|
|
36914
37239
|
};
|
|
36915
37240
|
const result = await analyze(defineConfig(config));
|
|
36916
37241
|
emit({ ok: true, result: normalizeResult(result) });
|
|
@@ -37040,7 +37365,11 @@ const createDeadCodeWorker = (input) => {
|
|
|
37040
37365
|
"pipe",
|
|
37041
37366
|
"pipe"
|
|
37042
37367
|
],
|
|
37043
|
-
windowsHide: true
|
|
37368
|
+
windowsHide: true,
|
|
37369
|
+
env: input.parseConcurrency === void 0 ? process.env : {
|
|
37370
|
+
...process.env,
|
|
37371
|
+
DESLOP_PARSE_CONCURRENCY: String(input.parseConcurrency)
|
|
37372
|
+
}
|
|
37044
37373
|
});
|
|
37045
37374
|
const stdoutChunks = [];
|
|
37046
37375
|
const stderrChunks = [];
|
|
@@ -37085,28 +37414,25 @@ const createDeadCodeWorker = (input) => {
|
|
|
37085
37414
|
}
|
|
37086
37415
|
};
|
|
37087
37416
|
};
|
|
37088
|
-
const runDeadCodeWorkerWithTimeout = (handle, timeoutMs) => new Promise((resolve, reject) => {
|
|
37417
|
+
const runDeadCodeWorkerWithTimeout = (handle, timeoutMs, abortSignal) => new Promise((resolve, reject) => {
|
|
37089
37418
|
let didSettle = false;
|
|
37090
|
-
const
|
|
37091
|
-
if (didSettle) return;
|
|
37092
|
-
didSettle = true;
|
|
37093
|
-
handle.terminate?.();
|
|
37094
|
-
reject(/* @__PURE__ */ new Error(`Dead-code worker timed out after ${timeoutMs / MILLISECONDS_PER_SECOND}s.`));
|
|
37095
|
-
}, timeoutMs);
|
|
37096
|
-
timeoutHandle.unref?.();
|
|
37097
|
-
handle.result.then((value) => {
|
|
37419
|
+
const settle = (finish) => {
|
|
37098
37420
|
if (didSettle) return;
|
|
37099
37421
|
didSettle = true;
|
|
37100
37422
|
clearTimeout(timeoutHandle);
|
|
37423
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
37101
37424
|
handle.terminate?.();
|
|
37102
|
-
|
|
37103
|
-
}
|
|
37104
|
-
|
|
37105
|
-
|
|
37106
|
-
|
|
37107
|
-
|
|
37108
|
-
|
|
37109
|
-
|
|
37425
|
+
finish();
|
|
37426
|
+
};
|
|
37427
|
+
const onAbort = () => settle(() => reject(/* @__PURE__ */ new Error("Dead-code worker aborted.")));
|
|
37428
|
+
const timeoutHandle = setTimeout(() => settle(() => reject(/* @__PURE__ */ new Error(`Dead-code worker timed out after ${timeoutMs / MILLISECONDS_PER_SECOND}s.`))), timeoutMs);
|
|
37429
|
+
timeoutHandle.unref?.();
|
|
37430
|
+
if (abortSignal?.aborted) {
|
|
37431
|
+
onAbort();
|
|
37432
|
+
return;
|
|
37433
|
+
}
|
|
37434
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
37435
|
+
handle.result.then((value) => settle(() => resolve(value)), (error) => settle(() => reject(error)));
|
|
37110
37436
|
});
|
|
37111
37437
|
const checkDeadCode = async (options) => {
|
|
37112
37438
|
const rootDirectory = toCanonicalPath(options.rootDirectory);
|
|
@@ -37118,8 +37444,9 @@ const checkDeadCode = async (options) => {
|
|
|
37118
37444
|
entryPatterns,
|
|
37119
37445
|
tsConfigPath: resolveTsConfigPath(rootDirectory),
|
|
37120
37446
|
ignorePatterns,
|
|
37121
|
-
deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js")
|
|
37122
|
-
|
|
37447
|
+
deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
|
|
37448
|
+
parseConcurrency: options.parseConcurrency
|
|
37449
|
+
}), options.workerTimeoutMs ?? 12e4, options.abortSignal));
|
|
37123
37450
|
const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
|
|
37124
37451
|
const diagnostics = [];
|
|
37125
37452
|
for (const unusedFile of result.unusedFiles) diagnostics.push({
|
|
@@ -37217,7 +37544,37 @@ const isDiagnosticOnSurface = (diagnostic, surface, config) => {
|
|
|
37217
37544
|
return true;
|
|
37218
37545
|
};
|
|
37219
37546
|
const filterDiagnosticsForSurface = (diagnostics, surface, config) => diagnostics.filter((diagnostic) => isDiagnosticOnSurface(diagnostic, surface, config));
|
|
37220
|
-
|
|
37547
|
+
/**
|
|
37548
|
+
* Budget for the dead-code phase, scaled to the work. deslop's graph build is
|
|
37549
|
+
* CPU-bound and roughly linear in file count, so a fixed 120s cap is too tight
|
|
37550
|
+
* for a large repo (where the pass legitimately runs that long) and is then
|
|
37551
|
+
* tipped over by any concurrent load — silently dropping every dead-code
|
|
37552
|
+
* finding. Scaling the budget with file count (and inversely with the core
|
|
37553
|
+
* share when overlapped) lets the pass complete, while the ceiling still
|
|
37554
|
+
* reclaims a genuinely wedged worker. Returns the in-worker SIGKILL deadline
|
|
37555
|
+
* and the Effect-side phase backstop that sits a margin above it.
|
|
37556
|
+
*/
|
|
37557
|
+
const resolveDeadCodeTimeout = (input) => {
|
|
37558
|
+
const coreShareFactor = Math.max(1, input.fullConcurrency / Math.max(1, input.deadCodeConcurrency));
|
|
37559
|
+
const workerTimeoutMs = Math.min(DEAD_CODE_TIMEOUT_CEILING_MS, Math.max(DEAD_CODE_WORKER_TIMEOUT_MS, Math.ceil(input.sourceFileCount * 30 * coreShareFactor)));
|
|
37560
|
+
return {
|
|
37561
|
+
workerTimeoutMs,
|
|
37562
|
+
phaseTimeoutMs: workerTimeoutMs + DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS
|
|
37563
|
+
};
|
|
37564
|
+
};
|
|
37565
|
+
const collectSizedSourceFiles = (rootDirectory, relativePaths) => {
|
|
37566
|
+
const entries = [];
|
|
37567
|
+
for (const relativePath of relativePaths) {
|
|
37568
|
+
const absolutePath = Path.resolve(rootDirectory, relativePath);
|
|
37569
|
+
const sizeBytes = statSourceFileSize(absolutePath);
|
|
37570
|
+
if (isLargeMinifiedFile(absolutePath, sizeBytes)) continue;
|
|
37571
|
+
entries.push({
|
|
37572
|
+
path: relativePath,
|
|
37573
|
+
sizeBytes: sizeBytes ?? 0
|
|
37574
|
+
});
|
|
37575
|
+
}
|
|
37576
|
+
return entries;
|
|
37577
|
+
};
|
|
37221
37578
|
const listSourceFilesViaGit = (rootDirectory) => {
|
|
37222
37579
|
const result = spawnSync("git", [
|
|
37223
37580
|
"ls-files",
|
|
@@ -37250,7 +37607,8 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
|
|
|
37250
37607
|
}
|
|
37251
37608
|
return filePaths;
|
|
37252
37609
|
};
|
|
37253
|
-
const
|
|
37610
|
+
const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory));
|
|
37611
|
+
const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
|
|
37254
37612
|
const resolveLintIncludePaths = (rootDirectory, userConfig, project) => {
|
|
37255
37613
|
if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
|
|
37256
37614
|
const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
|
|
@@ -37293,9 +37651,12 @@ var Config = class Config extends Service()("react-doctor/Config") {
|
|
|
37293
37651
|
var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
|
|
37294
37652
|
static layerNode = succeed$3(DeadCode, DeadCode.of({ run: (input) => unwrap(fn("DeadCode.run")(function* () {
|
|
37295
37653
|
return yield* tryPromise({
|
|
37296
|
-
try: () => checkDeadCode({
|
|
37654
|
+
try: (signal) => checkDeadCode({
|
|
37297
37655
|
rootDirectory: input.rootDirectory,
|
|
37298
|
-
userConfig: input.userConfig
|
|
37656
|
+
userConfig: input.userConfig,
|
|
37657
|
+
parseConcurrency: input.parseConcurrency,
|
|
37658
|
+
workerTimeoutMs: input.workerTimeoutMs,
|
|
37659
|
+
abortSignal: signal
|
|
37299
37660
|
}),
|
|
37300
37661
|
catch: (cause) => new ReactDoctorError({ reason: new DeadCodeAnalysisFailed({ cause }) })
|
|
37301
37662
|
}).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)));
|
|
@@ -38008,6 +38369,14 @@ const neutralizeDisableDirectives = async (rootDirectory, includePaths) => {
|
|
|
38008
38369
|
process.removeListener("exit", onExit);
|
|
38009
38370
|
};
|
|
38010
38371
|
};
|
|
38372
|
+
const ROOT_DIRECTORY_PLACEHOLDER = "<root>";
|
|
38373
|
+
const normalizeConfigForHash = (config) => {
|
|
38374
|
+
const clone = JSON.parse(JSON.stringify(config));
|
|
38375
|
+
if (clone?.settings?.["react-doctor"]) clone.settings["react-doctor"].rootDirectory = ROOT_DIRECTORY_PLACEHOLDER;
|
|
38376
|
+
if (Array.isArray(clone?.jsPlugins)) clone.jsPlugins = clone.jsPlugins.map((_, index) => `<plugin:${index}>`);
|
|
38377
|
+
return clone;
|
|
38378
|
+
};
|
|
38379
|
+
const computeRulesetHash = (input) => crypto.createHash("sha1").update(JSON.stringify(normalizeConfigForHash(input.config))).update("\0").update([...input.toolchainVersions].join("\0")).update("\0").update([...input.ignorePatterns].join("\n")).update(" ").update(input.tsconfigContent ?? "").digest("hex");
|
|
38011
38380
|
/**
|
|
38012
38381
|
* Loads a plugin module via the local require resolver and extracts
|
|
38013
38382
|
* `(name, ruleNames)` from either `module.exports.meta + rules` or
|
|
@@ -38034,16 +38403,16 @@ const readPluginShape = (pluginSpecifier, loadModule) => {
|
|
|
38034
38403
|
ruleNames: new Set(Object.keys(rules))
|
|
38035
38404
|
};
|
|
38036
38405
|
};
|
|
38037
|
-
const bundledRequire = createRequire(import.meta.url);
|
|
38406
|
+
const bundledRequire$1 = createRequire(import.meta.url);
|
|
38038
38407
|
const resolveReactHooksJsPlugin = (hasReactCompiler, customRulesOnly) => {
|
|
38039
38408
|
if (!hasReactCompiler || customRulesOnly) return null;
|
|
38040
38409
|
let pluginSpecifier;
|
|
38041
38410
|
try {
|
|
38042
|
-
pluginSpecifier = bundledRequire.resolve("eslint-plugin-react-hooks");
|
|
38411
|
+
pluginSpecifier = bundledRequire$1.resolve("eslint-plugin-react-hooks");
|
|
38043
38412
|
} catch {
|
|
38044
38413
|
return null;
|
|
38045
38414
|
}
|
|
38046
|
-
const { ruleNames } = readPluginShape(pluginSpecifier, (spec) => bundledRequire(spec));
|
|
38415
|
+
const { ruleNames } = readPluginShape(pluginSpecifier, (spec) => bundledRequire$1(spec));
|
|
38047
38416
|
return {
|
|
38048
38417
|
entry: {
|
|
38049
38418
|
name: "react-hooks-js",
|
|
@@ -38162,8 +38531,8 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
|
|
|
38162
38531
|
}
|
|
38163
38532
|
return enabled;
|
|
38164
38533
|
};
|
|
38165
|
-
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false }) => {
|
|
38166
|
-
const reactHooksJsPlugin = disableReactHooksJsPlugin ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
|
|
38534
|
+
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection }) => {
|
|
38535
|
+
const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
|
|
38167
38536
|
const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
|
|
38168
38537
|
const jsPlugins = [];
|
|
38169
38538
|
if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
|
|
@@ -38172,6 +38541,8 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
38172
38541
|
for (const registryEntry of REACT_DOCTOR_RULES) {
|
|
38173
38542
|
const rule = reactDoctorPlugin.rules[registryEntry.id];
|
|
38174
38543
|
if (!rule) continue;
|
|
38544
|
+
if (ruleSelection === "cacheable" && CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
|
|
38545
|
+
if (ruleSelection === "sidecar" && !CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
|
|
38175
38546
|
if (rule.scan !== void 0) continue;
|
|
38176
38547
|
if (customRulesOnly && registryEntry.originallyExternal) continue;
|
|
38177
38548
|
if (rule.framework !== "global" && !rule.requires) continue;
|
|
@@ -38186,7 +38557,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
38186
38557
|
enabledReactDoctorRules[registryEntry.key] = severity;
|
|
38187
38558
|
}
|
|
38188
38559
|
const userPluginRules = {};
|
|
38189
|
-
for (const userPlugin of userPlugins) {
|
|
38560
|
+
if (ruleSelection !== "sidecar") for (const userPlugin of userPlugins) {
|
|
38190
38561
|
Object.assign(userPluginRules, buildUserPluginRules(userPlugin, severityControls));
|
|
38191
38562
|
jsPlugins.push(userPlugin.entry);
|
|
38192
38563
|
}
|
|
@@ -38216,6 +38587,100 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
38216
38587
|
}
|
|
38217
38588
|
};
|
|
38218
38589
|
};
|
|
38590
|
+
const atomicWriteJson = (filePath, value) => {
|
|
38591
|
+
try {
|
|
38592
|
+
NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
|
|
38593
|
+
const temporaryPath = `${filePath}.${process.pid}.tmp`;
|
|
38594
|
+
NFS.writeFileSync(temporaryPath, JSON.stringify(value));
|
|
38595
|
+
NFS.renameSync(temporaryPath, filePath);
|
|
38596
|
+
} catch {
|
|
38597
|
+
return;
|
|
38598
|
+
}
|
|
38599
|
+
};
|
|
38600
|
+
const failOpenReadJson = (filePath, fallback) => {
|
|
38601
|
+
try {
|
|
38602
|
+
return JSON.parse(NFS.readFileSync(filePath, "utf8"));
|
|
38603
|
+
} catch {
|
|
38604
|
+
return fallback;
|
|
38605
|
+
}
|
|
38606
|
+
};
|
|
38607
|
+
const validateDiagnostic = decodeUnknownSync(Diagnostic);
|
|
38608
|
+
const decodeFileDiagnostics = (raw) => {
|
|
38609
|
+
if (!Array.isArray(raw)) return null;
|
|
38610
|
+
try {
|
|
38611
|
+
for (const entry of raw) validateDiagnostic(entry);
|
|
38612
|
+
return raw;
|
|
38613
|
+
} catch {
|
|
38614
|
+
return null;
|
|
38615
|
+
}
|
|
38616
|
+
};
|
|
38617
|
+
const emptyCache = () => ({
|
|
38618
|
+
version: 1,
|
|
38619
|
+
rulesets: {}
|
|
38620
|
+
});
|
|
38621
|
+
const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
|
|
38622
|
+
const entries = /* @__PURE__ */ new Map();
|
|
38623
|
+
const persisted = failOpenReadJson(cacheFilePath, emptyCache());
|
|
38624
|
+
if (persisted.version !== 1 || !isRecord(persisted.rulesets)) return entries;
|
|
38625
|
+
const bucket = persisted.rulesets[rulesetHash];
|
|
38626
|
+
if (!isRecord(bucket) || !isRecord(bucket.files)) return entries;
|
|
38627
|
+
for (const [fileKey, rawDiagnostics] of Object.entries(bucket.files)) {
|
|
38628
|
+
const decoded = decodeFileDiagnostics(rawDiagnostics);
|
|
38629
|
+
if (decoded !== null) entries.set(fileKey, decoded);
|
|
38630
|
+
}
|
|
38631
|
+
return entries;
|
|
38632
|
+
};
|
|
38633
|
+
const createFileLintCache = (cacheDirectory, rulesetHash) => {
|
|
38634
|
+
const cacheFilePath = Path.join(cacheDirectory, FILE_LINT_CACHE_FILENAME);
|
|
38635
|
+
const entries = loadRulesetEntries(cacheFilePath, rulesetHash);
|
|
38636
|
+
return {
|
|
38637
|
+
lookup: (fileKey) => entries.get(fileKey) ?? null,
|
|
38638
|
+
store: (fileKey, diagnostics) => {
|
|
38639
|
+
entries.delete(fileKey);
|
|
38640
|
+
entries.set(fileKey, diagnostics);
|
|
38641
|
+
},
|
|
38642
|
+
persist: () => {
|
|
38643
|
+
const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
|
|
38644
|
+
const rulesets = onDisk.version === 1 && isRecord(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
|
|
38645
|
+
const existingBucket = rulesets[rulesetHash];
|
|
38646
|
+
const existingFiles = isRecord(existingBucket) && isRecord(existingBucket.files) ? existingBucket.files : {};
|
|
38647
|
+
const ourFiles = {};
|
|
38648
|
+
for (const [fileKey, diagnostics] of entries) ourFiles[fileKey] = diagnostics;
|
|
38649
|
+
const cappedEntries = Object.entries({
|
|
38650
|
+
...existingFiles,
|
|
38651
|
+
...ourFiles
|
|
38652
|
+
}).slice(-FILE_LINT_CACHE_MAX_FILE_COUNT);
|
|
38653
|
+
rulesets[rulesetHash] = {
|
|
38654
|
+
updatedAtMs: Date.now(),
|
|
38655
|
+
files: Object.fromEntries(cappedEntries)
|
|
38656
|
+
};
|
|
38657
|
+
const keptHashes = Object.entries(rulesets).sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8).map(([hash]) => hash);
|
|
38658
|
+
const prunedRulesets = {};
|
|
38659
|
+
for (const hash of keptHashes) prunedRulesets[hash] = rulesets[hash];
|
|
38660
|
+
atomicWriteJson(cacheFilePath, {
|
|
38661
|
+
version: 1,
|
|
38662
|
+
rulesets: prunedRulesets
|
|
38663
|
+
});
|
|
38664
|
+
}
|
|
38665
|
+
};
|
|
38666
|
+
};
|
|
38667
|
+
const bundledRequire = createRequire(import.meta.url);
|
|
38668
|
+
const TOOLCHAIN_PACKAGE_SPECIFIERS = [
|
|
38669
|
+
"oxlint/package.json",
|
|
38670
|
+
"oxlint-plugin-react-doctor/package.json",
|
|
38671
|
+
"eslint-plugin-react-hooks/package.json"
|
|
38672
|
+
];
|
|
38673
|
+
const resolveOxlintToolchainVersions = () => {
|
|
38674
|
+
const versions = [`node=${process.version}`];
|
|
38675
|
+
for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS) try {
|
|
38676
|
+
const packageJson = bundledRequire(specifier);
|
|
38677
|
+
const version = typeof packageJson.version === "string" ? packageJson.version : "unknown";
|
|
38678
|
+
versions.push(`${specifier}=${version}`);
|
|
38679
|
+
} catch {
|
|
38680
|
+
versions.push(`${specifier}=missing`);
|
|
38681
|
+
}
|
|
38682
|
+
return versions;
|
|
38683
|
+
};
|
|
38219
38684
|
const esmRequire = createRequire(import.meta.url);
|
|
38220
38685
|
const resolveOxlintBinary = () => {
|
|
38221
38686
|
const oxlintMainPath = esmRequire.resolve("oxlint");
|
|
@@ -38897,15 +39362,19 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
|
|
|
38897
39362
|
};
|
|
38898
39363
|
});
|
|
38899
39364
|
};
|
|
38900
|
-
const
|
|
38901
|
-
const
|
|
38902
|
-
for (const [name, value] of Object.entries(
|
|
39365
|
+
const buildOxlintChildEnv = (sourceEnv) => {
|
|
39366
|
+
const childEnv = {};
|
|
39367
|
+
for (const [name, value] of Object.entries(sourceEnv)) {
|
|
38903
39368
|
if (name === "NODE_OPTIONS" || name === "NODE_DEBUG") continue;
|
|
38904
39369
|
if (name.startsWith("npm_config_")) continue;
|
|
38905
|
-
|
|
39370
|
+
childEnv[name] = value;
|
|
38906
39371
|
}
|
|
38907
|
-
|
|
38908
|
-
|
|
39372
|
+
const isCompileCacheDisabled = Boolean(sourceEnv.NODE_DISABLE_COMPILE_CACHE);
|
|
39373
|
+
const isCompileCacheAlreadySet = childEnv.NODE_COMPILE_CACHE !== void 0;
|
|
39374
|
+
if (!isCompileCacheDisabled && !isCompileCacheAlreadySet) childEnv.NODE_COMPILE_CACHE = Path.join(os.tmpdir(), NODE_COMPILE_CACHE_DIR_NAME);
|
|
39375
|
+
return childEnv;
|
|
39376
|
+
};
|
|
39377
|
+
const SANITIZED_ENV = buildOxlintChildEnv(process.env);
|
|
38909
39378
|
/**
|
|
38910
39379
|
* Spawn one oxlint subprocess with hard ceilings on wall time and
|
|
38911
39380
|
* output size. Returns stdout on success; raises a tagged
|
|
@@ -38922,7 +39391,11 @@ const SANITIZED_ENV = (() => {
|
|
|
38922
39391
|
* The first three are splittable (the caller's binary-split retry
|
|
38923
39392
|
* shrinks the batch and re-spawns); the fourth isn't.
|
|
38924
39393
|
*/
|
|
38925
|
-
const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLINT_SPAWN_TIMEOUT_MS, outputMaxBytes = OXLINT_OUTPUT_MAX_BYTES) => new Promise((resolve, reject) => {
|
|
39394
|
+
const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLINT_SPAWN_TIMEOUT_MS, outputMaxBytes = OXLINT_OUTPUT_MAX_BYTES, abortSignal) => new Promise((resolve, reject) => {
|
|
39395
|
+
if (abortSignal?.aborted) {
|
|
39396
|
+
reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: "lint phase aborted" }) }));
|
|
39397
|
+
return;
|
|
39398
|
+
}
|
|
38926
39399
|
const child = spawn(nodeBinaryPath, args, {
|
|
38927
39400
|
cwd: rootDirectory,
|
|
38928
39401
|
env: SANITIZED_ENV,
|
|
@@ -38932,7 +39405,14 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
|
|
|
38932
39405
|
"pipe"
|
|
38933
39406
|
]
|
|
38934
39407
|
});
|
|
39408
|
+
const onAbort = () => {
|
|
39409
|
+
child.kill("SIGKILL");
|
|
39410
|
+
reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: "lint phase aborted" }) }));
|
|
39411
|
+
};
|
|
39412
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
39413
|
+
const clearAbortListener = () => abortSignal?.removeEventListener("abort", onAbort);
|
|
38935
39414
|
const timeoutHandle = setTimeout(() => {
|
|
39415
|
+
clearAbortListener();
|
|
38936
39416
|
child.kill("SIGKILL");
|
|
38937
39417
|
reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
|
|
38938
39418
|
kind: "timeout",
|
|
@@ -38967,10 +39447,12 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
|
|
|
38967
39447
|
});
|
|
38968
39448
|
child.on("error", (error) => {
|
|
38969
39449
|
clearTimeout(timeoutHandle);
|
|
39450
|
+
clearAbortListener();
|
|
38970
39451
|
reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: error }) }));
|
|
38971
39452
|
});
|
|
38972
39453
|
child.on("close", (_code, signal) => {
|
|
38973
39454
|
clearTimeout(timeoutHandle);
|
|
39455
|
+
clearAbortListener();
|
|
38974
39456
|
if (didKillForSize) {
|
|
38975
39457
|
reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
|
|
38976
39458
|
kind: "output-too-large",
|
|
@@ -39037,26 +39519,28 @@ const isParallelismRelatedSpawnError = (error) => {
|
|
|
39037
39519
|
* loop with a slimmer config in that case.
|
|
39038
39520
|
*/
|
|
39039
39521
|
const spawnLintBatches = async (input) => {
|
|
39040
|
-
const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes } = input;
|
|
39522
|
+
const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 8, signal } = input;
|
|
39041
39523
|
const requestedConcurrency = resolveScanConcurrency(input.concurrency ?? 1);
|
|
39042
39524
|
const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
|
|
39043
39525
|
const runBatchPass = async (concurrency) => {
|
|
39044
39526
|
const allDiagnostics = [];
|
|
39045
39527
|
const droppedFiles = [];
|
|
39046
39528
|
let firstDropReason = null;
|
|
39047
|
-
const
|
|
39529
|
+
const splitDeadlineMs = Date.now() + splitTotalBudgetMs;
|
|
39530
|
+
const spawnLintBatch = async (batch, depth) => {
|
|
39048
39531
|
const batchArgs = [...baseArgs, ...batch];
|
|
39049
39532
|
try {
|
|
39050
|
-
return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes), project, rootDirectory);
|
|
39533
|
+
return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes, signal), project, rootDirectory);
|
|
39051
39534
|
} catch (error) {
|
|
39052
39535
|
if (!isSplittableReactDoctorError(error)) throw error;
|
|
39053
|
-
|
|
39536
|
+
const splitBudgetExhausted = Date.now() >= splitDeadlineMs || depth >= splitMaxDepth;
|
|
39537
|
+
if (batch.length <= 1 || splitBudgetExhausted) {
|
|
39054
39538
|
droppedFiles.push(...batch);
|
|
39055
|
-
if (firstDropReason === null) firstDropReason = error.message;
|
|
39539
|
+
if (firstDropReason === null) firstDropReason = splitBudgetExhausted && batch.length > 1 ? `${error.message} (split budget exhausted after ${splitMaxDepth} levels / ${splitTotalBudgetMs / MILLISECONDS_PER_SECOND}s)` : error.message;
|
|
39056
39540
|
return [];
|
|
39057
39541
|
}
|
|
39058
39542
|
const splitIndex = Math.ceil(batch.length / 2);
|
|
39059
|
-
return [...await spawnLintBatch(batch.slice(0, splitIndex)), ...await spawnLintBatch(batch.slice(splitIndex))];
|
|
39543
|
+
return [...await spawnLintBatch(batch.slice(0, splitIndex), depth + 1), ...await spawnLintBatch(batch.slice(splitIndex), depth + 1)];
|
|
39060
39544
|
}
|
|
39061
39545
|
};
|
|
39062
39546
|
let startedFileCount = 0;
|
|
@@ -39073,7 +39557,7 @@ const spawnLintBatches = async (input) => {
|
|
|
39073
39557
|
try {
|
|
39074
39558
|
const batchResults = await mapWithConcurrency(fileBatches, concurrency, async (batch) => {
|
|
39075
39559
|
startedFileCount += batch.length;
|
|
39076
|
-
const batchDiagnostics = await spawnLintBatch(batch);
|
|
39560
|
+
const batchDiagnostics = await spawnLintBatch(batch, 0);
|
|
39077
39561
|
scannedFileCount += batch.length;
|
|
39078
39562
|
if (onFileProgress) {
|
|
39079
39563
|
displayedFileCount = Math.min(Math.max(displayedFileCount, scannedFileCount), totalFileCount);
|
|
@@ -39134,6 +39618,22 @@ const validateRuleRegistration = () => {
|
|
|
39134
39618
|
].filter((entry) => entry !== null).join("; ");
|
|
39135
39619
|
console.warn(`[react-doctor] rule-registration drift: ${detail}`);
|
|
39136
39620
|
};
|
|
39621
|
+
const hashFileContents = (filePath) => {
|
|
39622
|
+
try {
|
|
39623
|
+
return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
|
|
39624
|
+
} catch {
|
|
39625
|
+
return null;
|
|
39626
|
+
}
|
|
39627
|
+
};
|
|
39628
|
+
const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
|
|
39629
|
+
const resolveReactDoctorCacheDir = (projectDirectory) => {
|
|
39630
|
+
const cacheDirOverride = process.env["REACT_DOCTOR_CACHE_DIR"]?.trim();
|
|
39631
|
+
if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
|
|
39632
|
+
const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
|
|
39633
|
+
if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
|
|
39634
|
+
return Path.join(os.tmpdir(), "react-doctor-cache", projectCacheSubdir(projectDirectory));
|
|
39635
|
+
};
|
|
39636
|
+
const sortSourceFilesByCost = (entries) => [...entries].sort((left, right) => right.sizeBytes - left.sizeBytes).map((entry) => entry.path);
|
|
39137
39637
|
/**
|
|
39138
39638
|
* Atomically (re)writes the generated oxlintrc.json. Used twice in
|
|
39139
39639
|
* the runner: once for the primary scan, once for the
|
|
@@ -39192,7 +39692,7 @@ const reactHooksJsPluginDropNote = (error) => {
|
|
|
39192
39692
|
* 6. always restore disable directives + clean up the temp dir
|
|
39193
39693
|
*/
|
|
39194
39694
|
const runOxlint = async (options) => {
|
|
39195
|
-
const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure, spawnTimeoutMs, outputMaxBytes } = options;
|
|
39695
|
+
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 = "arrival" } = options;
|
|
39196
39696
|
const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
|
|
39197
39697
|
const severityControls = buildRuleSeverityControls(userConfig);
|
|
39198
39698
|
validateRuleRegistration();
|
|
@@ -39209,30 +39709,156 @@ const runOxlint = async (options) => {
|
|
|
39209
39709
|
serverAuthFunctionNames,
|
|
39210
39710
|
severityControls,
|
|
39211
39711
|
userPlugins,
|
|
39212
|
-
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
|
|
39712
|
+
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin,
|
|
39713
|
+
ruleSelection: overrides.ruleSelection
|
|
39213
39714
|
});
|
|
39214
39715
|
const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
|
|
39215
39716
|
const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
|
|
39216
39717
|
const configPath = Path.join(configDirectory, "oxlintrc.json");
|
|
39217
39718
|
try {
|
|
39218
|
-
const
|
|
39219
|
-
|
|
39220
|
-
|
|
39221
|
-
configPath,
|
|
39222
|
-
"--format",
|
|
39223
|
-
"json"
|
|
39224
|
-
];
|
|
39719
|
+
const oxlintBinary = resolveOxlintBinary();
|
|
39720
|
+
const sharedArgs = [];
|
|
39721
|
+
let tsconfigContent = null;
|
|
39225
39722
|
if (project.hasTypeScript) {
|
|
39226
39723
|
const tsconfigRelativePath = resolveTsConfigRelativePath(rootDirectory);
|
|
39227
|
-
if (tsconfigRelativePath)
|
|
39724
|
+
if (tsconfigRelativePath) {
|
|
39725
|
+
sharedArgs.push("--tsconfig", tsconfigRelativePath);
|
|
39726
|
+
try {
|
|
39727
|
+
tsconfigContent = NFS.readFileSync(Path.resolve(rootDirectory, tsconfigRelativePath), "utf8");
|
|
39728
|
+
} catch {
|
|
39729
|
+
tsconfigContent = null;
|
|
39730
|
+
}
|
|
39731
|
+
}
|
|
39228
39732
|
}
|
|
39229
39733
|
const combinedPatterns = collectIgnorePatterns(rootDirectory);
|
|
39230
39734
|
if (combinedPatterns.length > 0) {
|
|
39231
39735
|
const combinedIgnorePath = Path.join(configDirectory, "combined.ignore");
|
|
39232
39736
|
NFS.writeFileSync(combinedIgnorePath, `${combinedPatterns.join("\n")}\n`);
|
|
39233
|
-
|
|
39737
|
+
sharedArgs.push("--ignore-path", combinedIgnorePath);
|
|
39234
39738
|
}
|
|
39235
|
-
const
|
|
39739
|
+
const makeBaseArgs = (oxlintConfigPath) => [
|
|
39740
|
+
oxlintBinary,
|
|
39741
|
+
"-c",
|
|
39742
|
+
oxlintConfigPath,
|
|
39743
|
+
"--format",
|
|
39744
|
+
"json",
|
|
39745
|
+
...sharedArgs
|
|
39746
|
+
];
|
|
39747
|
+
const discoverScanFiles = () => lintBatchOrdering === "cost" ? sortSourceFilesByCost(listSourceFilesWithSize(rootDirectory)) : listSourceFiles(rootDirectory);
|
|
39748
|
+
const candidateFiles = includePaths !== void 0 ? includePaths : discoverScanFiles();
|
|
39749
|
+
const runConfigOverFiles = async (buildConfigForPass, configFileName, files, fileProgress) => {
|
|
39750
|
+
if (files.length === 0) return {
|
|
39751
|
+
diagnostics: [],
|
|
39752
|
+
didDropReactHooksJsPlugin: false,
|
|
39753
|
+
hadPartialFailure: false
|
|
39754
|
+
};
|
|
39755
|
+
let hadPartialFailure = false;
|
|
39756
|
+
const reportPartialFailure = (reason) => {
|
|
39757
|
+
hadPartialFailure = true;
|
|
39758
|
+
onPartialFailure?.(reason);
|
|
39759
|
+
};
|
|
39760
|
+
const passConfigPath = Path.join(configDirectory, configFileName);
|
|
39761
|
+
const passBaseArgs = makeBaseArgs(passConfigPath);
|
|
39762
|
+
const passFileBatches = batchIncludePaths(passBaseArgs, files);
|
|
39763
|
+
const spawnPass = () => spawnLintBatches({
|
|
39764
|
+
baseArgs: passBaseArgs,
|
|
39765
|
+
fileBatches: passFileBatches,
|
|
39766
|
+
rootDirectory,
|
|
39767
|
+
nodeBinaryPath,
|
|
39768
|
+
project,
|
|
39769
|
+
onPartialFailure: reportPartialFailure,
|
|
39770
|
+
onFileProgress: fileProgress,
|
|
39771
|
+
spawnTimeoutMs,
|
|
39772
|
+
outputMaxBytes,
|
|
39773
|
+
concurrency: options.concurrency,
|
|
39774
|
+
signal: options.signal
|
|
39775
|
+
});
|
|
39776
|
+
writeOxlintConfig(passConfigPath, buildConfigForPass({}));
|
|
39777
|
+
try {
|
|
39778
|
+
return {
|
|
39779
|
+
diagnostics: await spawnPass(),
|
|
39780
|
+
didDropReactHooksJsPlugin: false,
|
|
39781
|
+
hadPartialFailure
|
|
39782
|
+
};
|
|
39783
|
+
} catch (error) {
|
|
39784
|
+
const reactHooksJsDropNote = reactHooksJsPluginDropNote(error);
|
|
39785
|
+
if (reactHooksJsDropNote === null) throw error;
|
|
39786
|
+
writeOxlintConfig(passConfigPath, buildConfigForPass({ disableReactHooksJsPlugin: true }));
|
|
39787
|
+
const diagnostics = await spawnPass();
|
|
39788
|
+
reportPartialFailure(reactHooksJsDropNote);
|
|
39789
|
+
return {
|
|
39790
|
+
diagnostics,
|
|
39791
|
+
didDropReactHooksJsPlugin: true,
|
|
39792
|
+
hadPartialFailure
|
|
39793
|
+
};
|
|
39794
|
+
}
|
|
39795
|
+
};
|
|
39796
|
+
if (perFileLintCacheEnabled && respectInlineDisables && !project.hasReactCompiler && extendsPaths.length === 0 && userPlugins.length === 0) {
|
|
39797
|
+
const rulesetHash = computeRulesetHash({
|
|
39798
|
+
config: buildConfig({
|
|
39799
|
+
extendsPaths: [],
|
|
39800
|
+
ruleSelection: "cacheable"
|
|
39801
|
+
}),
|
|
39802
|
+
toolchainVersions: resolveOxlintToolchainVersions(),
|
|
39803
|
+
ignorePatterns: combinedPatterns,
|
|
39804
|
+
tsconfigContent
|
|
39805
|
+
});
|
|
39806
|
+
const cache = createFileLintCache(resolveReactDoctorCacheDir(rootDirectory), rulesetHash);
|
|
39807
|
+
const cacheKeyByFile = /* @__PURE__ */ new Map();
|
|
39808
|
+
const missFiles = [];
|
|
39809
|
+
const replayedDiagnostics = [];
|
|
39810
|
+
for (const candidateFile of candidateFiles) {
|
|
39811
|
+
const contentHash = hashFileContents(Path.resolve(rootDirectory, candidateFile));
|
|
39812
|
+
if (contentHash === null) {
|
|
39813
|
+
missFiles.push(candidateFile);
|
|
39814
|
+
continue;
|
|
39815
|
+
}
|
|
39816
|
+
const cacheKey = `${candidateFile.replaceAll("\\", "/")}${contentHash}`;
|
|
39817
|
+
cacheKeyByFile.set(candidateFile, cacheKey);
|
|
39818
|
+
const cachedDiagnostics = cache.lookup(cacheKey);
|
|
39819
|
+
if (cachedDiagnostics === null) missFiles.push(candidateFile);
|
|
39820
|
+
else replayedDiagnostics.push(...cachedDiagnostics);
|
|
39821
|
+
}
|
|
39822
|
+
const cacheHitFileCount = candidateFiles.length - missFiles.length;
|
|
39823
|
+
const cacheableResult = await runConfigOverFiles((overrides) => buildConfig({
|
|
39824
|
+
extendsPaths: [],
|
|
39825
|
+
ruleSelection: "cacheable",
|
|
39826
|
+
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
|
|
39827
|
+
}), "oxlintrc.cacheable.json", missFiles, void 0);
|
|
39828
|
+
const sidecarResult = await runConfigOverFiles(() => buildConfig({
|
|
39829
|
+
extendsPaths: [],
|
|
39830
|
+
ruleSelection: "sidecar"
|
|
39831
|
+
}), "oxlintrc.sidecar.json", candidateFiles, options.onFileProgress);
|
|
39832
|
+
onCacheStats?.(cacheHitFileCount, candidateFiles.length);
|
|
39833
|
+
const missFileByNormalizedPath = /* @__PURE__ */ new Map();
|
|
39834
|
+
for (const missFile of missFiles) missFileByNormalizedPath.set(missFile.replaceAll("\\", "/"), missFile);
|
|
39835
|
+
const freshDiagnosticsByFile = /* @__PURE__ */ new Map();
|
|
39836
|
+
let isAttributionSound = true;
|
|
39837
|
+
for (const diagnostic of cacheableResult.diagnostics) {
|
|
39838
|
+
const missFile = missFileByNormalizedPath.get(diagnostic.filePath);
|
|
39839
|
+
if (missFile === void 0) {
|
|
39840
|
+
isAttributionSound = false;
|
|
39841
|
+
break;
|
|
39842
|
+
}
|
|
39843
|
+
const fileDiagnostics = freshDiagnosticsByFile.get(missFile) ?? [];
|
|
39844
|
+
fileDiagnostics.push(diagnostic);
|
|
39845
|
+
freshDiagnosticsByFile.set(missFile, fileDiagnostics);
|
|
39846
|
+
}
|
|
39847
|
+
if (!cacheableResult.didDropReactHooksJsPlugin && !cacheableResult.hadPartialFailure && isAttributionSound) {
|
|
39848
|
+
for (const missFile of missFiles) {
|
|
39849
|
+
const cacheKey = cacheKeyByFile.get(missFile);
|
|
39850
|
+
if (cacheKey !== void 0) cache.store(cacheKey, freshDiagnosticsByFile.get(missFile) ?? []);
|
|
39851
|
+
}
|
|
39852
|
+
cache.persist();
|
|
39853
|
+
}
|
|
39854
|
+
return dedupeDiagnostics([
|
|
39855
|
+
...replayedDiagnostics,
|
|
39856
|
+
...cacheableResult.diagnostics,
|
|
39857
|
+
...sidecarResult.diagnostics
|
|
39858
|
+
]);
|
|
39859
|
+
}
|
|
39860
|
+
const baseArgs = makeBaseArgs(configPath);
|
|
39861
|
+
const fileBatches = batchIncludePaths(baseArgs, candidateFiles);
|
|
39236
39862
|
const runBatches = () => spawnLintBatches({
|
|
39237
39863
|
baseArgs,
|
|
39238
39864
|
fileBatches,
|
|
@@ -39243,7 +39869,8 @@ const runOxlint = async (options) => {
|
|
|
39243
39869
|
onFileProgress: options.onFileProgress,
|
|
39244
39870
|
spawnTimeoutMs,
|
|
39245
39871
|
outputMaxBytes,
|
|
39246
|
-
concurrency: options.concurrency
|
|
39872
|
+
concurrency: options.concurrency,
|
|
39873
|
+
signal: options.signal
|
|
39247
39874
|
});
|
|
39248
39875
|
writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
|
|
39249
39876
|
try {
|
|
@@ -39322,9 +39949,11 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
|
|
|
39322
39949
|
const spawnTimeoutMs = yield* OxlintSpawnTimeoutMs;
|
|
39323
39950
|
const outputMaxBytes = yield* OxlintOutputMaxBytes;
|
|
39324
39951
|
const concurrency = yield* OxlintConcurrency;
|
|
39952
|
+
const lintBatchOrdering = yield* LintBatchOrdering;
|
|
39953
|
+
const perFileLintCacheEnabled = yield* PerFileLintCacheEnabled;
|
|
39325
39954
|
const collectedFailures = [];
|
|
39326
39955
|
const diagnostics = yield* tryPromise({
|
|
39327
|
-
try: () => runOxlint({
|
|
39956
|
+
try: (signal) => runOxlint({
|
|
39328
39957
|
rootDirectory: input.rootDirectory,
|
|
39329
39958
|
project: input.project,
|
|
39330
39959
|
includePaths: input.includePaths ? [...input.includePaths] : void 0,
|
|
@@ -39339,9 +39968,13 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
|
|
|
39339
39968
|
collectedFailures.push(reason);
|
|
39340
39969
|
},
|
|
39341
39970
|
onFileProgress: input.onFileProgress,
|
|
39971
|
+
perFileLintCacheEnabled,
|
|
39972
|
+
onCacheStats: input.onCacheStats,
|
|
39342
39973
|
spawnTimeoutMs,
|
|
39343
39974
|
outputMaxBytes,
|
|
39344
|
-
concurrency
|
|
39975
|
+
concurrency,
|
|
39976
|
+
signal,
|
|
39977
|
+
lintBatchOrdering
|
|
39345
39978
|
}),
|
|
39346
39979
|
catch: ensureReactDoctorError
|
|
39347
39980
|
});
|
|
@@ -39733,14 +40366,49 @@ const parseArtifactFromBody = (body) => {
|
|
|
39733
40366
|
}
|
|
39734
40367
|
return null;
|
|
39735
40368
|
};
|
|
39736
|
-
const
|
|
40369
|
+
const isSupplyChainCacheDisabled = () => {
|
|
40370
|
+
const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
|
|
40371
|
+
return noCache === "1" || noCache === "true";
|
|
40372
|
+
};
|
|
40373
|
+
const supplyChainCacheFile = (cacheDirectory, dependency) => {
|
|
40374
|
+
const purlHash = crypto.createHash("sha256").update(toPurl(dependency)).digest("hex").slice(0, 16);
|
|
40375
|
+
return Path.join(cacheDirectory, SUPPLY_CHAIN_CACHE_SUBDIR, `${purlHash}.json`);
|
|
40376
|
+
};
|
|
40377
|
+
const readCachedSocketBody = (cacheFile) => {
|
|
40378
|
+
try {
|
|
40379
|
+
const entry = JSON.parse(NFS.readFileSync(cacheFile, "utf-8"));
|
|
40380
|
+
if (typeof entry === "object" && entry !== null && "fetchedAtMs" in entry && "body" in entry && typeof entry.fetchedAtMs === "number" && typeof entry.body === "string" && Date.now() - entry.fetchedAtMs <= 864e5) return entry.body;
|
|
40381
|
+
} catch {}
|
|
40382
|
+
return null;
|
|
40383
|
+
};
|
|
40384
|
+
const writeCachedSocketBody = (cacheFile, body) => {
|
|
40385
|
+
try {
|
|
40386
|
+
NFS.mkdirSync(Path.dirname(cacheFile), { recursive: true });
|
|
40387
|
+
NFS.writeFileSync(cacheFile, JSON.stringify({
|
|
40388
|
+
fetchedAtMs: Date.now(),
|
|
40389
|
+
body
|
|
40390
|
+
}));
|
|
40391
|
+
} catch {}
|
|
40392
|
+
};
|
|
40393
|
+
const fetchSocketArtifact = (dependency, cacheDirectory) => tryPromise(async (signal) => {
|
|
40394
|
+
const cacheFile = cacheDirectory === null ? null : supplyChainCacheFile(cacheDirectory, dependency);
|
|
40395
|
+
if (cacheFile !== null) {
|
|
40396
|
+
const cachedBody = readCachedSocketBody(cacheFile);
|
|
40397
|
+
if (cachedBody !== null) {
|
|
40398
|
+
const cachedArtifact = parseArtifactFromBody(cachedBody);
|
|
40399
|
+
if (cachedArtifact !== null) return cachedArtifact;
|
|
40400
|
+
}
|
|
40401
|
+
}
|
|
39737
40402
|
const requestUrl = `${SOCKET_FREE_PURL_API_BASE}/${encodeURIComponent(toPurl(dependency))}`;
|
|
39738
40403
|
const response = await fetch(requestUrl, {
|
|
39739
40404
|
headers: { "User-Agent": SOCKET_FREE_USER_AGENT },
|
|
39740
40405
|
signal
|
|
39741
40406
|
});
|
|
39742
40407
|
if (!response.ok) return null;
|
|
39743
|
-
|
|
40408
|
+
const body = await response.text();
|
|
40409
|
+
const artifact = parseArtifactFromBody(body);
|
|
40410
|
+
if (artifact !== null && cacheFile !== null) writeCachedSocketBody(cacheFile, body);
|
|
40411
|
+
return artifact;
|
|
39744
40412
|
}).pipe(timeout(FETCH_TIMEOUT_MS), orElseSucceed(() => null), tap$1((artifact) => {
|
|
39745
40413
|
const scoreAttributes = {};
|
|
39746
40414
|
if (artifact !== null) {
|
|
@@ -39845,7 +40513,8 @@ const checkSupplyChain = (input) => gen(function* () {
|
|
|
39845
40513
|
const packageJsonPath = Path.join(input.rootDirectory, "package.json");
|
|
39846
40514
|
const dependencies = collectDependenciesToScore(readPackageJson(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies);
|
|
39847
40515
|
if (dependencies.length === 0) return [];
|
|
39848
|
-
const
|
|
40516
|
+
const cacheDirectory = isSupplyChainCacheDisabled() ? null : resolveReactDoctorCacheDir(input.rootDirectory);
|
|
40517
|
+
const artifacts = yield* forEach$1(dependencies, (dependency) => fetchSocketArtifact(dependency, cacheDirectory), { concurrency: 8 }).pipe(timeoutOption(input.totalTimeoutMs ?? 9e4), map$3((maybeArtifacts) => getOrElse$1(maybeArtifacts, () => [])));
|
|
39849
40518
|
const diagnostics = [];
|
|
39850
40519
|
for (let index = 0; index < dependencies.length; index += 1) {
|
|
39851
40520
|
const artifact = artifacts[index];
|
|
@@ -39870,6 +40539,10 @@ const checkSupplyChain = (input) => gen(function* () {
|
|
|
39870
40539
|
* The underlying `checkSupplyChain` Effect is total/fail-open — per-package
|
|
39871
40540
|
* timeouts and network failures recover to "skip" — so the stream never
|
|
39872
40541
|
* fails, mirroring `DeadCode`'s stream shape so the two compose the same way.
|
|
40542
|
+
* The orchestrator (`run-inspect.ts`) consumes this stream on a background
|
|
40543
|
+
* fiber whose network time overlaps the lint pass, joined under a generous
|
|
40544
|
+
* wall-clock budget; a budget expiry is the same fail-open outcome as a Socket
|
|
40545
|
+
* outage.
|
|
39873
40546
|
*/
|
|
39874
40547
|
var SupplyChain = class SupplyChain extends Service()("react-doctor/SupplyChain") {
|
|
39875
40548
|
static layerNode = succeed$3(SupplyChain, SupplyChain.of({ run: (input) => unwrap(checkSupplyChain(input).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), withSpan("SupplyChain.run"))) }));
|
|
@@ -39928,18 +40601,42 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
|
|
|
39928
40601
|
*
|
|
39929
40602
|
* Phases:
|
|
39930
40603
|
*
|
|
39931
|
-
* 1. Config.resolve(directory) → Project.discover → Git metadata
|
|
40604
|
+
* 1. Config.resolve(directory) → Project.discover → Git metadata.
|
|
40605
|
+
* The GitHub viewer-permission lookup is forked onto a background
|
|
40606
|
+
* fiber here and joined late (it feeds score metadata, not
|
|
40607
|
+
* diagnostics).
|
|
39932
40608
|
* 2. beforeLint hook (e.g. CLI renders the project-detection block)
|
|
39933
40609
|
* 3. environment checks (reduced-motion + pnpm hardening +
|
|
39934
|
-
* expo/react-native + security scan)
|
|
39935
|
-
* 4.
|
|
39936
|
-
*
|
|
39937
|
-
*
|
|
39938
|
-
*
|
|
39939
|
-
*
|
|
39940
|
-
*
|
|
39941
|
-
*
|
|
39942
|
-
*
|
|
40610
|
+
* expo/react-native + security scan), collected synchronously
|
|
40611
|
+
* 4. The supply-chain check (Socket.dev) is forked onto a background
|
|
40612
|
+
* fiber so its ~100% network-bound time overlaps the ~100%
|
|
40613
|
+
* CPU/subprocess-bound lint pass below, collapsing two serial
|
|
40614
|
+
* phases into roughly `max(supplyChain, lint)`. It is capped by
|
|
40615
|
+
* `SupplyChainOverlapTimeoutMs` (measured from fork) so a hung
|
|
40616
|
+
* socket can't drag out its join; on timeout it fails open to no
|
|
40617
|
+
* diagnostics — the same outcome class as a Socket outage.
|
|
40618
|
+
* 5. Linter.run runs; DeadCode.run runs concurrently (forked child
|
|
40619
|
+
* fiber) ONLY when the memory gate has headroom to run the 8 GB
|
|
40620
|
+
* dead-code child alongside the oxlint workers — or when overlap is
|
|
40621
|
+
* forced via REACT_DOCTOR_DEAD_CODE_OVERLAP. Otherwise dead-code
|
|
40622
|
+
* runs sequentially after lint, exactly as it did pre-overlap. The
|
|
40623
|
+
* fiber is joined (or interrupted, SIGKILLing its worker, on lint
|
|
40624
|
+
* failure) before diagnostics are concatenated. The afterLint hook
|
|
40625
|
+
* fires between lint and dead-code. Progress spinner labels AND the
|
|
40626
|
+
* final diagnostic / score order stay independent of execution
|
|
40627
|
+
* order, so terminal output is identical either way; supply-chain
|
|
40628
|
+
* rides alongside without a spinner.
|
|
40629
|
+
* 6. Join the supply-chain fiber, then assemble the diagnostics in a
|
|
40630
|
+
* FIXED order (env, supply-chain, lint, dead-code) so the output is
|
|
40631
|
+
* byte-identical regardless of which fiber settled first. The
|
|
40632
|
+
* viewer-permission fiber is joined later, during score-metadata
|
|
40633
|
+
* assembly (it feeds score metadata, not diagnostics). The per-element
|
|
40634
|
+
* `Reporter.emit` side-channel now interleaves supply-chain with lint
|
|
40635
|
+
* emits, so capture-order assertions must target the deterministic
|
|
40636
|
+
* concat below, not emit order (production `Reporter.layerNoop` makes
|
|
40637
|
+
* emit a no-op).
|
|
40638
|
+
* 7. Reporter.finalize
|
|
40639
|
+
* 8. Score.compute against the surface-filtered diagnostic set
|
|
39943
40640
|
*
|
|
39944
40641
|
* The orchestrator owns spinner lifecycle via `Progress`; callers
|
|
39945
40642
|
* choose `Progress.layerOra(...)` for CLI feedback or
|
|
@@ -39997,10 +40694,21 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
39997
40694
|
ignoredTags: input.ignoredTags
|
|
39998
40695
|
})
|
|
39999
40696
|
])));
|
|
40000
|
-
const
|
|
40697
|
+
const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
|
|
40698
|
+
const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
|
|
40699
|
+
const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
|
|
40001
40700
|
rootDirectory: scanDirectory,
|
|
40002
40701
|
userConfig: resolvedConfig.config
|
|
40003
|
-
})))
|
|
40702
|
+
}))).pipe(map$3((diagnostics) => ({
|
|
40703
|
+
diagnostics,
|
|
40704
|
+
timedOut: false
|
|
40705
|
+
})), timeout(supplyChainOverlapTimeout), orElseSucceed(() => ({
|
|
40706
|
+
diagnostics: [],
|
|
40707
|
+
timedOut: true
|
|
40708
|
+
}))) : succeed$2({
|
|
40709
|
+
diagnostics: [],
|
|
40710
|
+
timedOut: false
|
|
40711
|
+
}));
|
|
40004
40712
|
const lintFailure = yield* make$13({
|
|
40005
40713
|
didFail: false,
|
|
40006
40714
|
reason: null,
|
|
@@ -40011,12 +40719,49 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
40011
40719
|
didFail: false,
|
|
40012
40720
|
reason: null
|
|
40013
40721
|
});
|
|
40014
|
-
const scanConcurrency = yield* OxlintConcurrency;
|
|
40722
|
+
const scanConcurrency = resolveScanConcurrency(yield* OxlintConcurrency);
|
|
40723
|
+
const lintPhaseTimeoutMs = yield* LintPhaseTimeoutMs;
|
|
40724
|
+
const deadCodePhaseTimeoutMs = yield* DeadCodePhaseTimeoutMs;
|
|
40725
|
+
const resolveDeadCodePhaseTimeoutMs = (scaledPhaseTimeoutMs) => deadCodePhaseTimeoutMs === 15e4 ? scaledPhaseTimeoutMs : deadCodePhaseTimeoutMs;
|
|
40015
40726
|
const workerCountSuffix = scanConcurrency > 1 ? ` ${highlighter.dim(`[~${scanConcurrency} workers]`)}` : "";
|
|
40727
|
+
const shouldRunDeadCode = input.runDeadCode && !isDiffMode && (showWarnings || deadCodeMaySurfaceWhenWarningsHidden(resolvedConfig.config));
|
|
40728
|
+
const deadCodeOverlapMode = yield* DeadCodeOverlap;
|
|
40729
|
+
const shouldOverlapDeadCode = shouldRunDeadCode && deadCodeOverlapMode === "on";
|
|
40730
|
+
const deadCodeParseConcurrency = shouldOverlapDeadCode ? Math.max(1, Math.floor(scanConcurrency * DEAD_CODE_OVERLAP_PARSE_SHARE)) : void 0;
|
|
40731
|
+
const lintConcurrency = deadCodeParseConcurrency === void 0 ? scanConcurrency : Math.max(1, scanConcurrency - deadCodeParseConcurrency);
|
|
40732
|
+
const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
|
|
40733
|
+
rootDirectory: scanDirectory,
|
|
40734
|
+
userConfig: resolvedConfig.config,
|
|
40735
|
+
parseConcurrency: deadCodeParseConcurrency,
|
|
40736
|
+
workerTimeoutMs: deadCodeTimeout.workerTimeoutMs
|
|
40737
|
+
}).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
|
|
40738
|
+
yield* set(deadCodeFailure, {
|
|
40739
|
+
didFail: true,
|
|
40740
|
+
reason: error.message
|
|
40741
|
+
});
|
|
40742
|
+
return empty$4;
|
|
40743
|
+
})))))).pipe(timeoutOption(deadCodeTimeout.phaseTimeoutMs), flatMap$2(match$3({
|
|
40744
|
+
onNone: () => set(deadCodeFailure, {
|
|
40745
|
+
didFail: true,
|
|
40746
|
+
reason: `Dead-code analysis exceeded ${Math.round(deadCodeTimeout.phaseTimeoutMs / MILLISECONDS_PER_SECOND)}s and was skipped.`
|
|
40747
|
+
}).pipe(as([])),
|
|
40748
|
+
onSome: succeed$2
|
|
40749
|
+
})));
|
|
40750
|
+
const overlapDeadCodeTimeout = resolveDeadCodeTimeout({
|
|
40751
|
+
sourceFileCount: project.sourceFileCount,
|
|
40752
|
+
deadCodeConcurrency: deadCodeParseConcurrency ?? scanConcurrency,
|
|
40753
|
+
fullConcurrency: scanConcurrency
|
|
40754
|
+
});
|
|
40755
|
+
const deadCodeFiber = shouldOverlapDeadCode ? yield* forkChild(buildCollectDeadCode({
|
|
40756
|
+
workerTimeoutMs: overlapDeadCodeTimeout.workerTimeoutMs,
|
|
40757
|
+
phaseTimeoutMs: resolveDeadCodePhaseTimeoutMs(overlapDeadCodeTimeout.phaseTimeoutMs)
|
|
40758
|
+
})) : null;
|
|
40016
40759
|
const scanProgress = yield* progressService.start("Scanning...");
|
|
40017
40760
|
const scanStartTime = Date.now();
|
|
40018
40761
|
let lastReportedTotalFileCount = 0;
|
|
40019
|
-
|
|
40762
|
+
let lintCacheHitFileCount = null;
|
|
40763
|
+
let lintCacheTotalFileCount = null;
|
|
40764
|
+
const baseLintStream = linterService.run({
|
|
40020
40765
|
rootDirectory: scanDirectory,
|
|
40021
40766
|
project,
|
|
40022
40767
|
includePaths: lintIncludePaths ?? void 0,
|
|
@@ -40030,6 +40775,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
40030
40775
|
onFileProgress: (scannedFileCount, totalFileCount) => {
|
|
40031
40776
|
lastReportedTotalFileCount = totalFileCount;
|
|
40032
40777
|
runSync(scanProgress.update(`Scanning files (${scannedFileCount}/${totalFileCount})${workerCountSuffix}...`));
|
|
40778
|
+
},
|
|
40779
|
+
onCacheStats: (cacheHitFileCount, totalConsideredFileCount) => {
|
|
40780
|
+
lintCacheHitFileCount = cacheHitFileCount;
|
|
40781
|
+
lintCacheTotalFileCount = totalConsideredFileCount;
|
|
40033
40782
|
}
|
|
40034
40783
|
}).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
|
|
40035
40784
|
yield* set(lintFailure, {
|
|
@@ -40039,36 +40788,54 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
40039
40788
|
reasonKind: error.reason._tag === "OxlintUnavailable" ? error.reason.kind : null
|
|
40040
40789
|
});
|
|
40041
40790
|
return empty$4;
|
|
40042
|
-
}))))
|
|
40791
|
+
}))));
|
|
40792
|
+
const lintCollected = yield* runCollect(applyPerElementPipeline(shouldOverlapDeadCode ? baseLintStream.pipe(provideService(OxlintConcurrency, lintConcurrency)) : baseLintStream)).pipe(timeoutOption(lintPhaseTimeoutMs), flatMap$2(match$3({
|
|
40793
|
+
onNone: () => set(lintFailure, {
|
|
40794
|
+
didFail: true,
|
|
40795
|
+
reason: `Lint analysis exceeded ${lintPhaseTimeoutMs / MILLISECONDS_PER_SECOND}s and was skipped.`,
|
|
40796
|
+
reasonTag: "OxlintBatchExceeded",
|
|
40797
|
+
reasonKind: null
|
|
40798
|
+
}).pipe(as([])),
|
|
40799
|
+
onSome: succeed$2
|
|
40800
|
+
})));
|
|
40043
40801
|
const lintFailureState = yield* get$2(lintFailure);
|
|
40044
40802
|
yield* afterLint(lintFailureState.didFail);
|
|
40045
40803
|
if (lintFailureState.didFail) yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
|
|
40046
40804
|
const totalFileCount = lastReportedTotalFileCount || (lintIncludePaths?.length ?? project.sourceFileCount);
|
|
40047
40805
|
const scannedFilesLabel = `${totalFileCount} ${totalFileCount === 1 ? "file" : "files"}`;
|
|
40048
|
-
|
|
40049
|
-
|
|
40050
|
-
|
|
40051
|
-
|
|
40052
|
-
|
|
40053
|
-
|
|
40054
|
-
|
|
40055
|
-
|
|
40806
|
+
let deadCodeCollected = [];
|
|
40807
|
+
if (lintFailureState.didFail) {
|
|
40808
|
+
if (deadCodeFiber !== null) yield* interrupt(deadCodeFiber);
|
|
40809
|
+
} else if (shouldRunDeadCode) {
|
|
40810
|
+
yield* scanProgress.update(`Scanned ${scannedFilesLabel}, analyzing dead code...`);
|
|
40811
|
+
const sequentialDeadCodeTimeout = resolveDeadCodeTimeout({
|
|
40812
|
+
sourceFileCount: totalFileCount,
|
|
40813
|
+
deadCodeConcurrency: scanConcurrency,
|
|
40814
|
+
fullConcurrency: scanConcurrency
|
|
40056
40815
|
});
|
|
40057
|
-
|
|
40058
|
-
|
|
40059
|
-
|
|
40816
|
+
deadCodeCollected = deadCodeFiber !== null ? yield* join(deadCodeFiber) : yield* buildCollectDeadCode({
|
|
40817
|
+
workerTimeoutMs: sequentialDeadCodeTimeout.workerTimeoutMs,
|
|
40818
|
+
phaseTimeoutMs: resolveDeadCodePhaseTimeoutMs(sequentialDeadCodeTimeout.phaseTimeoutMs)
|
|
40819
|
+
});
|
|
40820
|
+
}
|
|
40821
|
+
const deadCodeFailureState = lintFailureState.didFail ? {
|
|
40822
|
+
didFail: false,
|
|
40823
|
+
reason: null
|
|
40824
|
+
} : yield* get$2(deadCodeFailure);
|
|
40060
40825
|
const scanElapsedMilliseconds = Date.now() - scanStartTime;
|
|
40061
40826
|
const scanElapsedSeconds = (scanElapsedMilliseconds / MILLISECONDS_PER_SECOND).toFixed(1);
|
|
40062
40827
|
if (!lintFailureState.didFail) if (deadCodeFailureState.didFail) yield* scanProgress.fail(DEAD_CODE_FAIL_TEXT);
|
|
40063
40828
|
else if (input.suppressScanSummary) yield* scanProgress.stop();
|
|
40064
40829
|
else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
|
|
40830
|
+
const supplyChainResult = yield* join(supplyChainFiber);
|
|
40831
|
+
const supplyChainCollected = supplyChainResult.diagnostics;
|
|
40065
40832
|
yield* reporterService.finalize;
|
|
40066
|
-
const finalDiagnostics = assignFixGroups([
|
|
40833
|
+
const finalDiagnostics = sortDiagnosticsStable(assignFixGroups([
|
|
40067
40834
|
...envCollected,
|
|
40068
40835
|
...supplyChainCollected,
|
|
40069
40836
|
...lintCollected,
|
|
40070
40837
|
...deadCodeCollected
|
|
40071
|
-
]);
|
|
40838
|
+
]));
|
|
40072
40839
|
const githubViewerPermission = yield* join(githubViewerPermissionFiber);
|
|
40073
40840
|
const scoreMetadata = {
|
|
40074
40841
|
...repo !== null ? { repo } : {},
|
|
@@ -40104,9 +40871,14 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
40104
40871
|
lintPartialFailures,
|
|
40105
40872
|
didDeadCodeFail: deadCodeFailureState.didFail,
|
|
40106
40873
|
deadCodeFailureReason: deadCodeFailureState.reason,
|
|
40874
|
+
deadCodeOverlapped: shouldOverlapDeadCode,
|
|
40107
40875
|
scannedFileCount: totalFileCount,
|
|
40108
40876
|
scannedFilePaths,
|
|
40109
|
-
scanElapsedMilliseconds
|
|
40877
|
+
scanElapsedMilliseconds,
|
|
40878
|
+
scanConcurrency,
|
|
40879
|
+
supplyChainOverlapTimedOut: supplyChainResult.timedOut,
|
|
40880
|
+
lintCacheHitFileCount,
|
|
40881
|
+
lintCacheTotalFileCount
|
|
40110
40882
|
};
|
|
40111
40883
|
}).pipe(withSpan("runInspect", { attributes: {
|
|
40112
40884
|
"inspect.directory": input.directory,
|
|
@@ -40114,7 +40886,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
40114
40886
|
"inspect.runDeadCode": input.runDeadCode,
|
|
40115
40887
|
"inspect.isCi": input.isCi,
|
|
40116
40888
|
"inspect.scoreSurface": input.scoreSurface ?? "score"
|
|
40117
|
-
} }));
|
|
40889
|
+
} }), (scanProgram) => flatMap$2(ScanDeadlineMs, (scanDeadlineMs) => scanProgram.pipe(timeout(scanDeadlineMs), catchTag$1("TimeoutError", () => new ReactDoctorError({ reason: new ScanDeadlineExceeded({ detail: `${scanDeadlineMs / MILLISECONDS_PER_SECOND}s elapsed` }) })))));
|
|
40118
40890
|
const parseNodeVersion = (versionString) => {
|
|
40119
40891
|
const [major = 0, minor = 0, patch = 0] = versionString.replace(/^v/, "").trim().split(".").map(Number);
|
|
40120
40892
|
return {
|
|
@@ -40435,7 +41207,7 @@ const computeConfigFingerprint = (projectDirectory, version) => {
|
|
|
40435
41207
|
/** Display name used in client-facing messages and progress titles. */
|
|
40436
41208
|
const SERVER_DISPLAY_NAME = "React Doctor";
|
|
40437
41209
|
/** Server version reported in `serverInfo`; injected at build, `dev` from source. */
|
|
40438
|
-
const SERVER_VERSION = "0.5.
|
|
41210
|
+
const SERVER_VERSION = "0.5.8";
|
|
40439
41211
|
/** `Diagnostic.source` shown next to every published diagnostic. */
|
|
40440
41212
|
const DIAGNOSTIC_SOURCE = "react-doctor";
|
|
40441
41213
|
/**
|
|
@@ -41090,6 +41862,7 @@ const createProjectGraph = (options) => {
|
|
|
41090
41862
|
clearPackageJsonCache();
|
|
41091
41863
|
clearIgnorePatternsCache();
|
|
41092
41864
|
clearAutoSuppressionCaches();
|
|
41865
|
+
clearMinifiedFileCache();
|
|
41093
41866
|
projects = null;
|
|
41094
41867
|
}
|
|
41095
41868
|
};
|
|
@@ -42476,5 +43249,5 @@ const startLanguageServer = () => {
|
|
|
42476
43249
|
};
|
|
42477
43250
|
//#endregion
|
|
42478
43251
|
export { startLanguageServer };
|
|
42479
|
-
!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]="
|
|
42480
|
-
//# debugId=
|
|
43252
|
+
!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]="9ee72ccc-703c-588f-b7e4-c92657144721")}catch(e){}}();
|
|
43253
|
+
//# debugId=9ee72ccc-703c-588f-b7e4-c92657144721
|