react-doctor 0.5.7 → 0.5.8-dev.f4e8e4b
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/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="6a2c86b6-c3a8-5e50-8e7b-daef840df200")}catch(e){}}();
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import * as NodeChildProcess from "node:child_process";
|
|
5
5
|
import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
|
|
@@ -9,7 +9,7 @@ import * as NFS from "node:fs";
|
|
|
9
9
|
import fs, { mkdtempSync, rmSync } from "node:fs";
|
|
10
10
|
import process$1 from "node:process";
|
|
11
11
|
import * as ts from "typescript";
|
|
12
|
-
import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
|
|
12
|
+
import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
|
|
13
13
|
import * as OS from "node:os";
|
|
14
14
|
import os, { tmpdir } from "node:os";
|
|
15
15
|
import { parseJSON5 } from "confbox";
|
|
@@ -10277,7 +10277,7 @@ const provideContext$1 = /* @__PURE__ */ dual(2, (self, context) => {
|
|
|
10277
10277
|
return updateContext$1(self, merge$3(context));
|
|
10278
10278
|
});
|
|
10279
10279
|
/** @internal */
|
|
10280
|
-
const provideService$
|
|
10280
|
+
const provideService$3 = function() {
|
|
10281
10281
|
if (arguments.length === 1) return dual(2, (self, impl) => provideServiceImpl(self, arguments[0], impl));
|
|
10282
10282
|
return dual(3, (self, service, impl) => provideServiceImpl(self, service, impl)).apply(this, arguments);
|
|
10283
10283
|
};
|
|
@@ -10508,7 +10508,7 @@ const constScopeEmpty = { _tag: "Empty" };
|
|
|
10508
10508
|
/** @internal */
|
|
10509
10509
|
const scope = scopeTag;
|
|
10510
10510
|
/** @internal */
|
|
10511
|
-
const provideScope = /* @__PURE__ */ provideService$
|
|
10511
|
+
const provideScope = /* @__PURE__ */ provideService$3(scopeTag);
|
|
10512
10512
|
/** @internal */
|
|
10513
10513
|
const scoped$1 = (self) => withFiber$1((fiber) => {
|
|
10514
10514
|
const prev = fiber.context;
|
|
@@ -10949,9 +10949,9 @@ const makeLatchUnsafe = (open) => new Latch(open ?? false);
|
|
|
10949
10949
|
/** @internal */
|
|
10950
10950
|
const makeLatch = (open) => sync$2(() => makeLatchUnsafe(open));
|
|
10951
10951
|
/** @internal */
|
|
10952
|
-
const withTracer$1 = /* @__PURE__ */ dual(2, (effect, tracer) => provideService$
|
|
10952
|
+
const withTracer$1 = /* @__PURE__ */ dual(2, (effect, tracer) => provideService$3(effect, Tracer, tracer));
|
|
10953
10953
|
/** @internal */
|
|
10954
|
-
const withTracerEnabled$1 = /* @__PURE__ */ provideService$
|
|
10954
|
+
const withTracerEnabled$1 = /* @__PURE__ */ provideService$3(TracerEnabled);
|
|
10955
10955
|
const bigint0 = /* @__PURE__ */ BigInt(0);
|
|
10956
10956
|
const NoopSpanProto = {
|
|
10957
10957
|
_tag: "Span",
|
|
@@ -11032,7 +11032,7 @@ const useSpan$1 = (name, ...args) => {
|
|
|
11032
11032
|
}));
|
|
11033
11033
|
});
|
|
11034
11034
|
};
|
|
11035
|
-
const provideParentSpan = /* @__PURE__ */ provideService$
|
|
11035
|
+
const provideParentSpan = /* @__PURE__ */ provideService$3(ParentSpan);
|
|
11036
11036
|
/** @internal */
|
|
11037
11037
|
const withParentSpan$1 = function() {
|
|
11038
11038
|
const dataFirst = isEffect$1(arguments[0]);
|
|
@@ -12599,7 +12599,7 @@ var CurrentMemoMap = class extends Service()("effect/Layer/CurrentMemoMap") {
|
|
|
12599
12599
|
* @category memo map
|
|
12600
12600
|
* @since 2.0.0
|
|
12601
12601
|
*/
|
|
12602
|
-
const buildWithMemoMap = /* @__PURE__ */ dual(3, (self, memoMap, scope) => provideService$
|
|
12602
|
+
const buildWithMemoMap = /* @__PURE__ */ dual(3, (self, memoMap, scope) => provideService$3(map$4(self.build(memoMap, scope), add(CurrentMemoMap, memoMap)), CurrentMemoMap, memoMap));
|
|
12603
12603
|
/**
|
|
12604
12604
|
* Builds a layer into an `Effect` value. Any resources associated with this
|
|
12605
12605
|
* layer will be released when the specified scope is closed unless their scope
|
|
@@ -13952,7 +13952,7 @@ const provide$1 = /* @__PURE__ */ dual((args) => isEffect$1(args[0]), (self, sou
|
|
|
13952
13952
|
/** @internal */
|
|
13953
13953
|
const repeatOrElse = /* @__PURE__ */ dual(3, (self, schedule, orElse) => flatMap$4(toStepWithMetadata(schedule), (step) => {
|
|
13954
13954
|
let meta = CurrentMetadata.defaultValue();
|
|
13955
|
-
return catch_$2(forever$2(tap$2(flatMap$4(suspend$3(() => provideService$
|
|
13955
|
+
return catch_$2(forever$2(tap$2(flatMap$4(suspend$3(() => provideService$3(self, CurrentMetadata, meta)), step), (meta_) => sync$2(() => {
|
|
13956
13956
|
meta = meta_;
|
|
13957
13957
|
})), { disableYield: true }), (error) => isDone$2(error) ? succeed$5(error.value) : orElse(error, meta.attempt === 0 ? none() : some(meta)));
|
|
13958
13958
|
}));
|
|
@@ -13960,7 +13960,7 @@ const repeatOrElse = /* @__PURE__ */ dual(3, (self, schedule, orElse) => flatMap
|
|
|
13960
13960
|
const retryOrElse = /* @__PURE__ */ dual(3, (self, policy, orElse) => flatMap$4(toStepWithMetadata(policy), (step) => {
|
|
13961
13961
|
let meta = CurrentMetadata.defaultValue();
|
|
13962
13962
|
let lastError;
|
|
13963
|
-
const loop = catch_$2(suspend$3(() => provideService$
|
|
13963
|
+
const loop = catch_$2(suspend$3(() => provideService$3(self, CurrentMetadata, meta)), (error) => {
|
|
13964
13964
|
lastError = error;
|
|
13965
13965
|
return flatMap$4(step(error), (meta_) => {
|
|
13966
13966
|
meta = meta_;
|
|
@@ -16076,7 +16076,7 @@ const updateContext = updateContext$1;
|
|
|
16076
16076
|
* @category Context
|
|
16077
16077
|
* @since 2.0.0
|
|
16078
16078
|
*/
|
|
16079
|
-
const provideService = provideService$
|
|
16079
|
+
const provideService$2 = provideService$3;
|
|
16080
16080
|
/**
|
|
16081
16081
|
* Scopes all resources used in this workflow to the lifetime of the workflow,
|
|
16082
16082
|
* ensuring that their finalizers are run as soon as this workflow completes
|
|
@@ -21091,6 +21091,20 @@ function decodeUnknownOption$1(schema, options) {
|
|
|
21091
21091
|
return asOption(decodeUnknownEffect(schema, options));
|
|
21092
21092
|
}
|
|
21093
21093
|
/**
|
|
21094
|
+
* Creates a synchronous decoder for `unknown` input.
|
|
21095
|
+
*
|
|
21096
|
+
* **Details**
|
|
21097
|
+
*
|
|
21098
|
+
* The returned function returns the decoded `Type` on success and throws an
|
|
21099
|
+
* `Error` with the `SchemaIssue.Issue` in its `cause` on decoding failure.
|
|
21100
|
+
*
|
|
21101
|
+
* @category decoding
|
|
21102
|
+
* @since 3.10.0
|
|
21103
|
+
*/
|
|
21104
|
+
function decodeUnknownSync$1(schema, options) {
|
|
21105
|
+
return asSync(decodeUnknownEffect(schema, options));
|
|
21106
|
+
}
|
|
21107
|
+
/**
|
|
21094
21108
|
* Creates an effectful encoder for `unknown` input.
|
|
21095
21109
|
*
|
|
21096
21110
|
* **Details**
|
|
@@ -21392,6 +21406,40 @@ function isSchemaError(u) {
|
|
|
21392
21406
|
*/
|
|
21393
21407
|
const decodeUnknownOption = decodeUnknownOption$1;
|
|
21394
21408
|
/**
|
|
21409
|
+
* Decodes an `unknown` input against a schema synchronously, returning the
|
|
21410
|
+
* decoded value or throwing an `Error` whose cause contains the schema issue.
|
|
21411
|
+
* Use this when you want to validate data at a boundary and treat a schema
|
|
21412
|
+
* mismatch as an exception. For typed input use `decodeSync`.
|
|
21413
|
+
*
|
|
21414
|
+
* **Details**
|
|
21415
|
+
*
|
|
21416
|
+
* Only service-free schemas can be decoded synchronously. For non-throwing
|
|
21417
|
+
* alternatives see `decodeUnknownOption`, `decodeUnknownExit`, or
|
|
21418
|
+
* `decodeUnknownEffect`. Options may be provided either when creating the
|
|
21419
|
+
* decoder or when applying it; application options override creation options.
|
|
21420
|
+
*
|
|
21421
|
+
* **Example** (Decoding with a transformation schema)
|
|
21422
|
+
*
|
|
21423
|
+
* ```ts
|
|
21424
|
+
* import { Schema } from "effect"
|
|
21425
|
+
*
|
|
21426
|
+
* const NumberFromString = Schema.NumberFromString
|
|
21427
|
+
*
|
|
21428
|
+
* console.log(Schema.decodeUnknownSync(NumberFromString)("42"))
|
|
21429
|
+
* // Output: 42
|
|
21430
|
+
*
|
|
21431
|
+
* Schema.decodeUnknownSync(NumberFromString)("not a number")
|
|
21432
|
+
* // throws SchemaError: NumberFromString
|
|
21433
|
+
* // └─ Encoded side transformation failure
|
|
21434
|
+
* // └─ NumberFromString
|
|
21435
|
+
* // └─ Expected a numeric string, actual "not a number"
|
|
21436
|
+
* ```
|
|
21437
|
+
*
|
|
21438
|
+
* @category decoding
|
|
21439
|
+
* @since 4.0.0
|
|
21440
|
+
*/
|
|
21441
|
+
const decodeUnknownSync = decodeUnknownSync$1;
|
|
21442
|
+
/**
|
|
21395
21443
|
* Encodes an `unknown` input against a schema synchronously, throwing a
|
|
21396
21444
|
* {@link SchemaError} on failure. Use this when you want to serialize data at a
|
|
21397
21445
|
* boundary and treat a schema mismatch as an unrecoverable error. For
|
|
@@ -28271,6 +28319,14 @@ const runWith = (self, f, onHalt) => suspend$2(() => {
|
|
|
28271
28319
|
return catchDone(flatMap$2(toTransform(self)(done$1(), scope), f), onHalt ? onHalt : succeed$2).pipe(onExit$2((exit) => close(scope, exit)));
|
|
28272
28320
|
});
|
|
28273
28321
|
/**
|
|
28322
|
+
* Provides a concrete service for a context key, removing that service
|
|
28323
|
+
* requirement from the returned channel.
|
|
28324
|
+
*
|
|
28325
|
+
* @category services
|
|
28326
|
+
* @since 2.0.0
|
|
28327
|
+
*/
|
|
28328
|
+
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))));
|
|
28329
|
+
/**
|
|
28274
28330
|
* Runs a channel and applies an effect to each output element.
|
|
28275
28331
|
*
|
|
28276
28332
|
* **Example** (Running effects for each output)
|
|
@@ -29659,6 +29715,44 @@ const splitLines = (self) => self.channel.pipe(pipeTo(splitLines$1()), fromChann
|
|
|
29659
29715
|
*/
|
|
29660
29716
|
const ensuring = /* @__PURE__ */ dual(2, (self, finalizer) => fromChannel(ensuring$1(self.channel, finalizer)));
|
|
29661
29717
|
/**
|
|
29718
|
+
* Provides the stream with a single required service, eliminating that
|
|
29719
|
+
* requirement from its environment.
|
|
29720
|
+
*
|
|
29721
|
+
* **Example** (Providing a stream service)
|
|
29722
|
+
*
|
|
29723
|
+
* ```ts
|
|
29724
|
+
* import { Console, Context, Effect, Stream } from "effect"
|
|
29725
|
+
*
|
|
29726
|
+
* class Greeter extends Context.Service<Greeter, {
|
|
29727
|
+
* greet: (name: string) => string
|
|
29728
|
+
* }>()("Greeter") {}
|
|
29729
|
+
*
|
|
29730
|
+
* const stream = Stream.fromEffect(
|
|
29731
|
+
* Effect.service(Greeter).pipe(
|
|
29732
|
+
* Effect.map((greeter) => greeter.greet("Ada"))
|
|
29733
|
+
* )
|
|
29734
|
+
* )
|
|
29735
|
+
*
|
|
29736
|
+
* const program = Effect.gen(function*() {
|
|
29737
|
+
* const collected = yield* Stream.runCollect(
|
|
29738
|
+
* stream.pipe(
|
|
29739
|
+
* Stream.provideService(Greeter, {
|
|
29740
|
+
* greet: (name) => `Hello, ${name}`
|
|
29741
|
+
* })
|
|
29742
|
+
* )
|
|
29743
|
+
* )
|
|
29744
|
+
* yield* Console.log(collected)
|
|
29745
|
+
* })
|
|
29746
|
+
*
|
|
29747
|
+
* Effect.runPromise(program)
|
|
29748
|
+
* //=> ["Hello, Ada"]
|
|
29749
|
+
* ```
|
|
29750
|
+
*
|
|
29751
|
+
* @category services
|
|
29752
|
+
* @since 2.0.0
|
|
29753
|
+
*/
|
|
29754
|
+
const provideService = /* @__PURE__ */ dual(3, (self, key, service) => fromChannel(provideService$1(self.channel, key, service)));
|
|
29755
|
+
/**
|
|
29662
29756
|
* Runs a stream with a sink and returns the sink result.
|
|
29663
29757
|
*
|
|
29664
29758
|
* **Example** (Running a stream with a sink)
|
|
@@ -33088,7 +33182,7 @@ const make$8 = /* @__PURE__ */ fnUntraced(function* (options) {
|
|
|
33088
33182
|
const runFork = runForkWith(services);
|
|
33089
33183
|
const exportInterval = max(fromInputUnsafe(options.exportInterval), zero);
|
|
33090
33184
|
let disabledUntil = void 0;
|
|
33091
|
-
const client = filterStatusOk(get$4(services, HttpClient)).pipe(transformResponse(provideService(TracerPropagationEnabled, false)), retryTransient({
|
|
33185
|
+
const client = filterStatusOk(get$4(services, HttpClient)).pipe(transformResponse(provideService$2(TracerPropagationEnabled, false)), retryTransient({
|
|
33092
33186
|
schedule: policy,
|
|
33093
33187
|
times: 3
|
|
33094
33188
|
}));
|
|
@@ -35885,15 +35979,21 @@ const isMinifiedSource = (absolutePath) => {
|
|
|
35885
35979
|
if (fileDescriptor !== void 0) NFS.closeSync(fileDescriptor);
|
|
35886
35980
|
}
|
|
35887
35981
|
};
|
|
35888
|
-
const
|
|
35889
|
-
|
|
35982
|
+
const cachedIsLargeMinifiedByPath = /* @__PURE__ */ new Map();
|
|
35983
|
+
const statSourceFileSize = (absolutePath) => {
|
|
35890
35984
|
try {
|
|
35891
|
-
|
|
35985
|
+
return NFS.statSync(absolutePath).size;
|
|
35892
35986
|
} catch {
|
|
35893
|
-
return
|
|
35987
|
+
return null;
|
|
35894
35988
|
}
|
|
35895
|
-
|
|
35896
|
-
|
|
35989
|
+
};
|
|
35990
|
+
const isLargeMinifiedFile = (absolutePath, knownSizeBytes) => {
|
|
35991
|
+
const cached = cachedIsLargeMinifiedByPath.get(absolutePath);
|
|
35992
|
+
if (cached !== void 0) return cached;
|
|
35993
|
+
const sizeBytes = knownSizeBytes === void 0 ? statSourceFileSize(absolutePath) : knownSizeBytes;
|
|
35994
|
+
const result = sizeBytes !== null && sizeBytes >= 2e4 && isMinifiedSource(absolutePath);
|
|
35995
|
+
cachedIsLargeMinifiedByPath.set(absolutePath, result);
|
|
35996
|
+
return result;
|
|
35897
35997
|
};
|
|
35898
35998
|
const isErrnoException = (error) => error instanceof Error && "code" in error;
|
|
35899
35999
|
const IGNORABLE_READDIR_ERROR_CODES = new Set([
|
|
@@ -36762,6 +36862,7 @@ const DOCS_URL = "https://react.doctor/docs";
|
|
|
36762
36862
|
const DOCS_RULES_BASE_URL = `${DOCS_URL}/rules`;
|
|
36763
36863
|
const FETCH_TIMEOUT_MS = 1e4;
|
|
36764
36864
|
const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
|
|
36865
|
+
const PER_WORKER_MEM_BUDGET_BYTES = 1024 * 1024 * 1024;
|
|
36765
36866
|
const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
|
|
36766
36867
|
const ADOPTABLE_LINT_CONFIG_FILENAMES = [".oxlintrc.json", ".eslintrc.json"];
|
|
36767
36868
|
const OXLINT_NODE_REQUIREMENT = "^20.19.0 || >=22.12.0";
|
|
@@ -36838,7 +36939,16 @@ const CANONICAL_DISCORD_URL = "https://react.doctor/discord";
|
|
|
36838
36939
|
const SKILL_NAME = "react-doctor";
|
|
36839
36940
|
const OXLINT_OUTPUT_MAX_BYTES = 50 * 1024 * 1024;
|
|
36840
36941
|
const OXLINT_SPAWN_TIMEOUT_MS = 6e4;
|
|
36942
|
+
const NODE_COMPILE_CACHE_DIR_NAME = "node-compile-cache";
|
|
36943
|
+
const DEAD_CODE_WORKER_TIMEOUT_MS = 12e4;
|
|
36944
|
+
const OXLINT_SPLIT_TOTAL_BUDGET_MS = 18e4;
|
|
36945
|
+
const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
|
|
36946
|
+
const LINT_PHASE_TIMEOUT_MS = 3e5;
|
|
36947
|
+
const SCAN_TOTAL_DEADLINE_MS = 9e5;
|
|
36841
36948
|
const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
|
|
36949
|
+
const DEAD_CODE_TIMEOUT_CEILING_MS = 6e5;
|
|
36950
|
+
const DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS = 3e4;
|
|
36951
|
+
const DEAD_CODE_OVERLAP_PARSE_SHARE = .4;
|
|
36842
36952
|
const RECOMMENDED_PNPM_MINIMUM_RELEASE_AGE_MINUTES = 10080;
|
|
36843
36953
|
const REACT_SERVER_DOM_PACKAGES = [
|
|
36844
36954
|
"react-server-dom-webpack",
|
|
@@ -36873,9 +36983,13 @@ const CONFIG_CACHE_TTL_MS = 300 * 1e3;
|
|
|
36873
36983
|
const SOCKET_FREE_PURL_API_BASE = "https://firewall-api.socket.dev/purl";
|
|
36874
36984
|
const SOCKET_PACKAGE_PAGE_BASE = "https://socket.dev/npm/package";
|
|
36875
36985
|
const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
|
|
36986
|
+
const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
|
|
36987
|
+
const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
|
|
36876
36988
|
const SUPPLY_CHAIN_PLUGIN = "socket";
|
|
36877
36989
|
const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
|
|
36878
36990
|
const SUPPLY_CHAIN_CATEGORY = "Security";
|
|
36991
|
+
const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
|
|
36992
|
+
const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
|
|
36879
36993
|
const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
|
|
36880
36994
|
const TSCONFIG_FILENAME = "tsconfig.json";
|
|
36881
36995
|
const isRelativeExtendsValue = (extendsValue) => extendsValue.startsWith("./") || extendsValue.startsWith("../") || Path.isAbsolute(extendsValue);
|
|
@@ -38227,6 +38341,11 @@ var OxlintBatchExceeded = class extends TaggedErrorClass()("OxlintBatchExceeded"
|
|
|
38227
38341
|
}
|
|
38228
38342
|
}
|
|
38229
38343
|
};
|
|
38344
|
+
var ScanDeadlineExceeded = class extends TaggedErrorClass()("ScanDeadlineExceeded", { detail: String$1 }) {
|
|
38345
|
+
get message() {
|
|
38346
|
+
return `Scan exceeded its overall time budget: ${this.detail}`;
|
|
38347
|
+
}
|
|
38348
|
+
};
|
|
38230
38349
|
var OxlintSpawnFailed = class extends TaggedErrorClass()("OxlintSpawnFailed", { cause: Unknown }) {
|
|
38231
38350
|
get message() {
|
|
38232
38351
|
return `Failed to run oxlint: ${pretty(fail$6(this.cause))}`;
|
|
@@ -38290,6 +38409,7 @@ var GitBaseBranchInvalid = class extends TaggedErrorClass()("GitBaseBranchInvali
|
|
|
38290
38409
|
const ReactDoctorErrorReason = Union([
|
|
38291
38410
|
OxlintUnavailable,
|
|
38292
38411
|
OxlintBatchExceeded,
|
|
38412
|
+
ScanDeadlineExceeded,
|
|
38293
38413
|
OxlintSpawnFailed,
|
|
38294
38414
|
OxlintOutputUnparseable,
|
|
38295
38415
|
ConfigParseFailed,
|
|
@@ -38362,15 +38482,105 @@ const layerOtlp = unwrap$3(gen(function* () {
|
|
|
38362
38482
|
}).pipe(provide$2(layer$9));
|
|
38363
38483
|
}).pipe(orDie));
|
|
38364
38484
|
/**
|
|
38365
|
-
*
|
|
38366
|
-
* `
|
|
38367
|
-
|
|
38485
|
+
* Read a positive-millisecond timeout from an env var, falling back to
|
|
38486
|
+
* `defaultMs` when the var is unset, non-finite, or not strictly positive.
|
|
38487
|
+
*/
|
|
38488
|
+
const readPositiveEnvMs = (envVarName, defaultMs) => {
|
|
38489
|
+
const rawValue = process.env[envVarName];
|
|
38490
|
+
if (rawValue === void 0) return defaultMs;
|
|
38491
|
+
const parsedValue = Number(rawValue);
|
|
38492
|
+
if (!Number.isFinite(parsedValue) || parsedValue <= 0) return defaultMs;
|
|
38493
|
+
return parsedValue;
|
|
38494
|
+
};
|
|
38495
|
+
const CGROUP_V2_MEMORY_MAX_PATH = "/sys/fs/cgroup/memory.max";
|
|
38496
|
+
const CGROUP_V1_MEMORY_LIMIT_PATH = "/sys/fs/cgroup/memory/memory.limit_in_bytes";
|
|
38497
|
+
const CGROUP_UNLIMITED_SENTINEL_BYTES = Number.MAX_SAFE_INTEGER;
|
|
38498
|
+
/**
|
|
38499
|
+
* Parses one raw cgroup memory-limit file value into a positive byte count, or
|
|
38500
|
+
* `undefined` when it represents "no limit" (the v2 `"max"` literal, an empty
|
|
38501
|
+
* read, a non-positive / non-finite value, or v1's near-2^63 unlimited
|
|
38502
|
+
* sentinel). Pure and exported so the classification is unit-testable without
|
|
38503
|
+
* touching the filesystem.
|
|
38504
|
+
*/
|
|
38505
|
+
const parseCgroupMemoryLimitBytes = (raw) => {
|
|
38506
|
+
if (raw === void 0) return void 0;
|
|
38507
|
+
const trimmed = raw.trim();
|
|
38508
|
+
if (trimmed === "" || trimmed === "max") return void 0;
|
|
38509
|
+
const parsed = Number(trimmed);
|
|
38510
|
+
if (!Number.isFinite(parsed) || parsed <= 0 || parsed >= CGROUP_UNLIMITED_SENTINEL_BYTES) return;
|
|
38511
|
+
return parsed;
|
|
38512
|
+
};
|
|
38513
|
+
const CGROUP_MEMORY_LIMIT_PATHS = [CGROUP_V2_MEMORY_MAX_PATH, CGROUP_V1_MEMORY_LIMIT_PATH];
|
|
38514
|
+
/**
|
|
38515
|
+
* Reads this process's cgroup memory limit in bytes from the first candidate
|
|
38516
|
+
* path that yields a real limit, or `undefined` when none does — no cgroup, no
|
|
38517
|
+
* limit, or the files are unreadable (e.g. macOS / Windows dev machines).
|
|
38518
|
+
* `os.totalmem()` reports the HOST total and ignores cgroup memory limits, so a
|
|
38519
|
+
* memory-constrained container over-reports total memory; `resolveAutoScan-
|
|
38520
|
+
* Concurrency` takes `min(totalmem, this)` to honor the limit.
|
|
38521
|
+
*
|
|
38522
|
+
* The cgroup v2 read is the mount-root `memory.max`, which IS the container's
|
|
38523
|
+
* limit under the standard cgroup-namespace setup CI runners use (the
|
|
38524
|
+
* container's own cgroup is the root of its namespaced view). A process in a
|
|
38525
|
+
* non-namespaced nested/delegated cgroup whose root reads `"max"` is not
|
|
38526
|
+
* detected here and falls back to the host total; the EAGAIN/ENOMEM serial
|
|
38527
|
+
* replay in `spawnLintBatches` remains the runtime backstop for that case.
|
|
38528
|
+
*
|
|
38529
|
+
* `candidatePaths` is injectable so tests exercise the v2-wins-over-v1
|
|
38530
|
+
* precedence, the skip-unreadable fallback, and the all-missing case without a
|
|
38531
|
+
* real `/sys/fs/cgroup`.
|
|
38532
|
+
*/
|
|
38533
|
+
const readCgroupMemoryLimitBytes = (candidatePaths = CGROUP_MEMORY_LIMIT_PATHS) => {
|
|
38534
|
+
for (const limitPath of candidatePaths) {
|
|
38535
|
+
let raw;
|
|
38536
|
+
try {
|
|
38537
|
+
raw = fs.readFileSync(limitPath, "utf8");
|
|
38538
|
+
} catch {
|
|
38539
|
+
continue;
|
|
38540
|
+
}
|
|
38541
|
+
const limitBytes = parseCgroupMemoryLimitBytes(raw);
|
|
38542
|
+
if (limitBytes !== void 0) return limitBytes;
|
|
38543
|
+
}
|
|
38544
|
+
};
|
|
38545
|
+
/**
|
|
38546
|
+
* Clamps a requested lint worker count to `[MIN_SCAN_CONCURRENCY,
|
|
38547
|
+
* HARD_MAX_SCAN_CONCURRENCY]` as a finite integer. This is the explicit-pin and
|
|
38548
|
+
* spawn-boundary clamp — the memory-and-core-budgeted auto count comes from
|
|
38549
|
+
* `resolveAutoScanConcurrency`. Out-of-range or non-finite requests degrade to
|
|
38368
38550
|
* `MIN_SCAN_CONCURRENCY` rather than oversubscribing or running zero workers.
|
|
38369
38551
|
*/
|
|
38370
38552
|
const resolveScanConcurrency = (requested) => {
|
|
38371
|
-
|
|
38372
|
-
|
|
38373
|
-
|
|
38553
|
+
if (!Number.isFinite(requested) || requested < 1) return 1;
|
|
38554
|
+
return Math.min(Math.floor(requested), 32);
|
|
38555
|
+
};
|
|
38556
|
+
const readSystemFacts = () => ({
|
|
38557
|
+
availableCores: os.availableParallelism(),
|
|
38558
|
+
totalMemoryBytes: os.totalmem(),
|
|
38559
|
+
cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
|
|
38560
|
+
});
|
|
38561
|
+
/**
|
|
38562
|
+
* Auto lint-worker count: the smaller of the (cgroup-CPU-aware) core count and
|
|
38563
|
+
* the number of `PER_WORKER_MEM_BUDGET_BYTES` workers that fit in available
|
|
38564
|
+
* memory, then clamped to `[MIN, HARD_MAX]` by `resolveScanConcurrency`.
|
|
38565
|
+
*
|
|
38566
|
+
* `os.availableParallelism()` already respects cgroup CPU quotas, so the core
|
|
38567
|
+
* term needs no help. Available memory is `os.totalmem()` floored by the cgroup
|
|
38568
|
+
* memory limit — `os.freemem()` is deliberately NOT used: it excludes
|
|
38569
|
+
* reclaimable page cache and reads near-zero on macOS / cache-heavy Linux, which
|
|
38570
|
+
* would collapse the auto path to a single worker. `os.totalmem()` reports the
|
|
38571
|
+
* host total even inside a container, so the cgroup limit (read directly,
|
|
38572
|
+
* because Node doesn't fold it into `totalmem()`) is the real ceiling there.
|
|
38573
|
+
*
|
|
38574
|
+
* `facts` is injectable so tests exercise core-bound, memory-bound, cgroup-
|
|
38575
|
+
* limited, and ceiling cases without mocking `os` or the filesystem.
|
|
38576
|
+
*/
|
|
38577
|
+
const resolveAutoScanConcurrency = (facts = readSystemFacts()) => {
|
|
38578
|
+
const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
|
|
38579
|
+
const memoryBoundedWorkers = Math.floor(availableMemoryBytes / PER_WORKER_MEM_BUDGET_BYTES);
|
|
38580
|
+
return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
|
|
38581
|
+
};
|
|
38582
|
+
const resolveLintBatchOrdering = () => {
|
|
38583
|
+
return process.env["REACT_DOCTOR_LINT_BATCH_ORDERING"]?.trim().toLowerCase() === "cost" ? "cost" : "arrival";
|
|
38374
38584
|
};
|
|
38375
38585
|
/**
|
|
38376
38586
|
* Per-batch oxlint wall-clock budget. Reads from the env var on
|
|
@@ -38378,11 +38588,38 @@ const resolveScanConcurrency = (requested) => {
|
|
|
38378
38588
|
* microVMs without recompiling react-doctor. Tests override via
|
|
38379
38589
|
* `Layer.succeed(OxlintSpawnTimeoutMs, ...)`.
|
|
38380
38590
|
*/
|
|
38381
|
-
var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => {
|
|
38382
|
-
|
|
38383
|
-
|
|
38591
|
+
var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS", OXLINT_SPAWN_TIMEOUT_MS) }) {};
|
|
38592
|
+
/**
|
|
38593
|
+
* Effect-side cap on the lint phase. The env var lets CI / eval runners
|
|
38594
|
+
* raise the phase budget for slow large repos without recompiling.
|
|
38595
|
+
* Tests override via `Layer.succeed(LintPhaseTimeoutMs, ...)`.
|
|
38596
|
+
*/
|
|
38597
|
+
var LintPhaseTimeoutMs = class extends Reference("react-doctor/LintPhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_LINT_PHASE_TIMEOUT_MS", LINT_PHASE_TIMEOUT_MS) }) {};
|
|
38598
|
+
/**
|
|
38599
|
+
* Effect-side cap on the dead-code phase, sitting above the in-worker
|
|
38600
|
+
* timeout as a runtime-independent backstop. The env var raises it for
|
|
38601
|
+
* type-heavy projects; tests override via
|
|
38602
|
+
* `Layer.succeed(DeadCodePhaseTimeoutMs, ...)`.
|
|
38603
|
+
*/
|
|
38604
|
+
var DeadCodePhaseTimeoutMs = class extends Reference("react-doctor/DeadCodePhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_DEAD_CODE_PHASE_TIMEOUT_MS", DEAD_CODE_PHASE_TIMEOUT_MS) }) {};
|
|
38605
|
+
/**
|
|
38606
|
+
* Overall scan deadline backstop, bounding everything the per-phase
|
|
38607
|
+
* timeouts don't (wedged git / IO). The env var raises it for very
|
|
38608
|
+
* large repos; tests override via `Layer.succeed(ScanDeadlineMs, ...)`.
|
|
38609
|
+
*/
|
|
38610
|
+
var ScanDeadlineMs = class extends Reference("react-doctor/ScanDeadlineMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_SCAN_DEADLINE_MS", SCAN_TOTAL_DEADLINE_MS) }) {};
|
|
38611
|
+
/**
|
|
38612
|
+
* Wall-clock budget for the supply-chain check when it runs on a background
|
|
38613
|
+
* fiber overlapping the lint pass. Reads from the env var on startup so the
|
|
38614
|
+
* eval harness can raise the budget under sandbox microVMs (slower network)
|
|
38615
|
+
* without recompiling react-doctor. Tests override via
|
|
38616
|
+
* `Layer.succeed(SupplyChainOverlapTimeoutMs, ...)`.
|
|
38617
|
+
*/
|
|
38618
|
+
var SupplyChainOverlapTimeoutMs = class extends Reference("react-doctor/SupplyChainOverlapTimeoutMs", { defaultValue: () => {
|
|
38619
|
+
const raw = process.env["REACT_DOCTOR_SUPPLY_CHAIN_TIMEOUT_MS"];
|
|
38620
|
+
if (raw === void 0) return SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS;
|
|
38384
38621
|
const parsed = Number(raw);
|
|
38385
|
-
if (!Number.isFinite(parsed) || parsed <= 0) return
|
|
38622
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS;
|
|
38386
38623
|
return parsed;
|
|
38387
38624
|
} }) {};
|
|
38388
38625
|
/**
|
|
@@ -38393,31 +38630,93 @@ var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTime
|
|
|
38393
38630
|
*/
|
|
38394
38631
|
var OxlintOutputMaxBytes = class extends Reference("react-doctor/OxlintOutputMaxBytes", { defaultValue: () => OXLINT_OUTPUT_MAX_BYTES }) {};
|
|
38395
38632
|
/**
|
|
38396
|
-
* Number of oxlint subprocesses the lint pass runs in parallel. Defaults
|
|
38397
|
-
*
|
|
38398
|
-
* the box
|
|
38399
|
-
*
|
|
38400
|
-
*
|
|
38401
|
-
*
|
|
38402
|
-
*
|
|
38403
|
-
*
|
|
38404
|
-
*
|
|
38405
|
-
*
|
|
38633
|
+
* Number of oxlint subprocesses the lint pass runs in parallel. Defaults to a
|
|
38634
|
+
* memory-and-core-budgeted auto count (`resolveAutoScanConcurrency`) so large
|
|
38635
|
+
* repos scan fast out of the box without OOMing the native binding on a
|
|
38636
|
+
* high-core / low-memory box; `spawnLintBatches` transparently falls back to a
|
|
38637
|
+
* single worker if a parallel run still exhausts system resources. The CLI's
|
|
38638
|
+
* `--no-parallel` flag forces serial via `Layer.succeed`; the
|
|
38639
|
+
* `REACT_DOCTOR_PARALLEL` env var seeds the default for programmatic / CI
|
|
38640
|
+
* callers that never touch the flag — parallelism is opt-OUT, so only the
|
|
38641
|
+
* explicit serial values pin one worker:
|
|
38642
|
+
*
|
|
38643
|
+
* - unset / `auto` / `true` / `on` → memory-and-core-budgeted auto count
|
|
38406
38644
|
* - `0` / `false` / `off` → `1` (serial)
|
|
38407
38645
|
* - a positive integer → that many workers (clamped)
|
|
38408
|
-
* - any other value →
|
|
38646
|
+
* - any other value → memory-and-core-budgeted auto count
|
|
38409
38647
|
*
|
|
38410
38648
|
* The resolved value is always within
|
|
38411
|
-
* `[MIN_SCAN_CONCURRENCY,
|
|
38649
|
+
* `[MIN_SCAN_CONCURRENCY, HARD_MAX_SCAN_CONCURRENCY]`.
|
|
38412
38650
|
*/
|
|
38413
38651
|
var OxlintConcurrency = class extends Reference("react-doctor/OxlintConcurrency", { defaultValue: () => {
|
|
38414
38652
|
const raw = process.env["REACT_DOCTOR_PARALLEL"];
|
|
38415
|
-
if (raw === void 0) return
|
|
38653
|
+
if (raw === void 0) return resolveAutoScanConcurrency();
|
|
38416
38654
|
const normalized = raw.trim().toLowerCase();
|
|
38417
38655
|
if (normalized === "0" || normalized === "false" || normalized === "off") return 1;
|
|
38418
38656
|
const parsed = Number.parseInt(normalized, 10);
|
|
38419
38657
|
if (Number.isInteger(parsed) && parsed > 0) return resolveScanConcurrency(parsed);
|
|
38420
|
-
return
|
|
38658
|
+
return resolveAutoScanConcurrency();
|
|
38659
|
+
} }) {};
|
|
38660
|
+
/**
|
|
38661
|
+
* Three-state control for overlapping the dead-code pass with the lint pass —
|
|
38662
|
+
* forking dead-code as a child fiber that runs DURING lint instead of strictly
|
|
38663
|
+
* after it.
|
|
38664
|
+
*
|
|
38665
|
+
* - `"auto"` (default) / `"off"` → strictly SEQUENTIAL: dead-code runs after
|
|
38666
|
+
* lint with the full core budget. Both deslop's parse pool and the oxlint
|
|
38667
|
+
* pool are CPU-bound and each size themselves to all cores, so overlapping
|
|
38668
|
+
* them only oversubscribes (~2x the cores) and starves the parse pass past
|
|
38669
|
+
* its timeout — for no wall-clock win, since there are no spare cores to
|
|
38670
|
+
* absorb the second pass. Sequential is both faster per-phase and safe.
|
|
38671
|
+
* - `"on"` → force the overlap anyway. The orchestrator then SPLITS the core
|
|
38672
|
+
* budget (`DEAD_CODE_OVERLAP_PARSE_SHARE`): deslop's parse pool is capped
|
|
38673
|
+
* and lint shrinks to the remainder, so the two sum to the cores instead of
|
|
38674
|
+
* doubling them, and the dead-code timeout scales up for the reduced share.
|
|
38675
|
+
*
|
|
38676
|
+
* Seeded from `REACT_DOCTOR_DEAD_CODE_OVERLAP` so operators get a redeploy-free
|
|
38677
|
+
* switch; tests pin it via `Layer.succeed(DeadCodeOverlap, ...)`.
|
|
38678
|
+
*/
|
|
38679
|
+
var DeadCodeOverlap = class extends Reference("react-doctor/DeadCodeOverlap", { defaultValue: () => {
|
|
38680
|
+
const raw = process.env["REACT_DOCTOR_DEAD_CODE_OVERLAP"]?.trim().toLowerCase();
|
|
38681
|
+
if (raw === "on" || raw === "true" || raw === "1") return "on";
|
|
38682
|
+
if (raw === "off" || raw === "false" || raw === "0") return "off";
|
|
38683
|
+
return "auto";
|
|
38684
|
+
} }) {};
|
|
38685
|
+
/**
|
|
38686
|
+
* How the full-scan lint pass orders its file batches. `"arrival"` (the
|
|
38687
|
+
* default) keeps `git ls-files` discovery order. `"cost"` opts into LPT (feed
|
|
38688
|
+
* the largest files first); set `REACT_DOCTOR_LINT_BATCH_ORDERING=cost`. NOTE:
|
|
38689
|
+
* `cost` is OFF by default because the current sort-desc-then-chunk-100 packs
|
|
38690
|
+
* the heaviest files into one wave-1 batch — on size-skewed repos that mega-
|
|
38691
|
+
* batch is a straggler (and can trip the per-batch timeout + split), measurably
|
|
38692
|
+
* regressing the common full-scan case. LPT needs the heavy files SPREAD across
|
|
38693
|
+
* batches before `cost` earns the default. Tests override via
|
|
38694
|
+
* `Layer.succeed(LintBatchOrdering, ...)`. Diff / staged scans never reach this
|
|
38695
|
+
* — they pass user-scoped `includePaths` that skip discovery and stay in
|
|
38696
|
+
* arrival order; only the full-scan branch reads it.
|
|
38697
|
+
*/
|
|
38698
|
+
var LintBatchOrdering = class extends Reference("react-doctor/LintBatchOrdering", { defaultValue: resolveLintBatchOrdering }) {};
|
|
38699
|
+
const CACHE_DISABLED_VALUES$1 = new Set(["1", "true"]);
|
|
38700
|
+
/**
|
|
38701
|
+
* Whether the per-file lint cache (`runners/oxlint/file-lint-cache.ts`) is
|
|
38702
|
+
* active. Defaults ON — repeat scans re-lint only the files whose content
|
|
38703
|
+
* changed, and correctness is guaranteed byte-identical to a cold scan by the
|
|
38704
|
+
* always-fresh cross-file sidecar. Opt-OUT, two knobs (matching the whole-repo
|
|
38705
|
+
* scan cache's `REACT_DOCTOR_NO_CACHE`):
|
|
38706
|
+
*
|
|
38707
|
+
* - `REACT_DOCTOR_NO_CACHE` — the global off-switch; disables BOTH the
|
|
38708
|
+
* whole-repo scan cache and this per-file cache.
|
|
38709
|
+
* - `REACT_DOCTOR_NO_FILE_CACHE` — granular: bust only the per-file cache
|
|
38710
|
+
* while keeping the whole-repo short-circuit.
|
|
38711
|
+
*
|
|
38712
|
+
* Tests override via `Layer.succeed(PerFileLintCacheEnabled, false)`.
|
|
38713
|
+
*/
|
|
38714
|
+
var PerFileLintCacheEnabled = class extends Reference("react-doctor/PerFileLintCacheEnabled", { defaultValue: () => {
|
|
38715
|
+
const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
|
|
38716
|
+
const noFileCache = process.env["REACT_DOCTOR_NO_FILE_CACHE"]?.toLowerCase() ?? "";
|
|
38717
|
+
if (CACHE_DISABLED_VALUES$1.has(noCache)) return false;
|
|
38718
|
+
if (CACHE_DISABLED_VALUES$1.has(noFileCache)) return false;
|
|
38719
|
+
return true;
|
|
38421
38720
|
} }) {};
|
|
38422
38721
|
const DIAGNOSTIC_SURFACES = [
|
|
38423
38722
|
"cli",
|
|
@@ -38851,6 +39150,8 @@ const assignFixGroups = (diagnostics) => {
|
|
|
38851
39150
|
};
|
|
38852
39151
|
});
|
|
38853
39152
|
};
|
|
39153
|
+
const compareStrings = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
39154
|
+
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));
|
|
38854
39155
|
const getDirectDependencyNames = (packageJson) => new Set([...Object.keys(packageJson.dependencies ?? {}), ...Object.keys(packageJson.devDependencies ?? {})]);
|
|
38855
39156
|
const buildExpoCheckContext = (rootDirectory, expoVersion) => {
|
|
38856
39157
|
const packageJson = readPackageJson$1(Path.join(rootDirectory, "package.json"));
|
|
@@ -39357,10 +39658,15 @@ const buildHardeningDiagnostic = (input) => ({
|
|
|
39357
39658
|
column: input.column ?? 0,
|
|
39358
39659
|
category: "Security"
|
|
39359
39660
|
});
|
|
39360
|
-
const checkPnpmHardening = (
|
|
39361
|
-
if (!isPnpmManagedProject(
|
|
39362
|
-
const workspacePath = Path.join(
|
|
39363
|
-
const
|
|
39661
|
+
const checkPnpmHardening = (scanDirectory) => {
|
|
39662
|
+
if (!isPnpmManagedProject(scanDirectory)) return [];
|
|
39663
|
+
const workspacePath = Path.join(scanDirectory, PNPM_WORKSPACE_FILE);
|
|
39664
|
+
const hasWorkspaceFile = isFile(workspacePath);
|
|
39665
|
+
if (!hasWorkspaceFile) {
|
|
39666
|
+
const monorepoRoot = findMonorepoRoot(scanDirectory);
|
|
39667
|
+
if (monorepoRoot !== null && isFile(Path.join(monorepoRoot, PNPM_WORKSPACE_FILE))) return [];
|
|
39668
|
+
}
|
|
39669
|
+
const settings = parseHardeningSettings(hasWorkspaceFile ? NFS.readFileSync(workspacePath, "utf-8") : "");
|
|
39364
39670
|
const diagnostics = [];
|
|
39365
39671
|
if (settings.minimumReleaseAge === null) diagnostics.push(buildHardeningDiagnostic({
|
|
39366
39672
|
message: "pnpm-workspace.yaml is missing `minimumReleaseAge` — newly published versions can ship malware that gets caught and unpublished within hours",
|
|
@@ -40161,6 +40467,22 @@ process.stdin.on("end", () => {
|
|
|
40161
40467
|
...(workerInput.ignorePatterns.length > 0
|
|
40162
40468
|
? { ignorePatterns: workerInput.ignorePatterns }
|
|
40163
40469
|
: {}),
|
|
40470
|
+
// We consume only deslop's GRAPH-based findings (unusedFiles, unusedExports,
|
|
40471
|
+
// unusedDependencies, circularDependencies). Everything else deslop can compute
|
|
40472
|
+
// is pure wasted work for us, and it's the bulk of the runtime:
|
|
40473
|
+
// - semantic: a full TS Program for unusedTypes/enum/class-members/
|
|
40474
|
+
// misclassifiedDependencies (~37-45% of the phase).
|
|
40475
|
+
// - reportCodeQuality: the duplicate-block, complexity, feature-flag,
|
|
40476
|
+
// TypeScript-smell, private-type-leak and re-export-cycle detectors. These
|
|
40477
|
+
// are the single most expensive pass — duplicate-block detection alone was
|
|
40478
|
+
// ~83s of a ~130s Sentry scan — so skipping them is an ~8.5x dead-code
|
|
40479
|
+
// speedup on a large repo.
|
|
40480
|
+
// Both are provably safe: the consumed graph findings are computed by their own
|
|
40481
|
+
// detectors, independent of these passes (confirmed byte-identical on
|
|
40482
|
+
// excalidraw + mui-material + sentry). tsConfigPath stays — the module resolver
|
|
40483
|
+
// needs it for path-alias resolution in the import graph.
|
|
40484
|
+
semantic: { enabled: false },
|
|
40485
|
+
reportCodeQuality: false,
|
|
40164
40486
|
};
|
|
40165
40487
|
const result = await analyze(defineConfig(config));
|
|
40166
40488
|
emit({ ok: true, result: normalizeResult(result) });
|
|
@@ -40290,7 +40612,11 @@ const createDeadCodeWorker = (input) => {
|
|
|
40290
40612
|
"pipe",
|
|
40291
40613
|
"pipe"
|
|
40292
40614
|
],
|
|
40293
|
-
windowsHide: true
|
|
40615
|
+
windowsHide: true,
|
|
40616
|
+
env: input.parseConcurrency === void 0 ? process.env : {
|
|
40617
|
+
...process.env,
|
|
40618
|
+
DESLOP_PARSE_CONCURRENCY: String(input.parseConcurrency)
|
|
40619
|
+
}
|
|
40294
40620
|
});
|
|
40295
40621
|
const stdoutChunks = [];
|
|
40296
40622
|
const stderrChunks = [];
|
|
@@ -40335,28 +40661,25 @@ const createDeadCodeWorker = (input) => {
|
|
|
40335
40661
|
}
|
|
40336
40662
|
};
|
|
40337
40663
|
};
|
|
40338
|
-
const runDeadCodeWorkerWithTimeout = (handle, timeoutMs) => new Promise((resolve, reject) => {
|
|
40664
|
+
const runDeadCodeWorkerWithTimeout = (handle, timeoutMs, abortSignal) => new Promise((resolve, reject) => {
|
|
40339
40665
|
let didSettle = false;
|
|
40340
|
-
const
|
|
40341
|
-
if (didSettle) return;
|
|
40342
|
-
didSettle = true;
|
|
40343
|
-
handle.terminate?.();
|
|
40344
|
-
reject(/* @__PURE__ */ new Error(`Dead-code worker timed out after ${timeoutMs / MILLISECONDS_PER_SECOND}s.`));
|
|
40345
|
-
}, timeoutMs);
|
|
40346
|
-
timeoutHandle.unref?.();
|
|
40347
|
-
handle.result.then((value) => {
|
|
40348
|
-
if (didSettle) return;
|
|
40349
|
-
didSettle = true;
|
|
40350
|
-
clearTimeout(timeoutHandle);
|
|
40351
|
-
handle.terminate?.();
|
|
40352
|
-
resolve(value);
|
|
40353
|
-
}, (error) => {
|
|
40666
|
+
const settle = (finish) => {
|
|
40354
40667
|
if (didSettle) return;
|
|
40355
40668
|
didSettle = true;
|
|
40356
40669
|
clearTimeout(timeoutHandle);
|
|
40670
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
40357
40671
|
handle.terminate?.();
|
|
40358
|
-
|
|
40359
|
-
}
|
|
40672
|
+
finish();
|
|
40673
|
+
};
|
|
40674
|
+
const onAbort = () => settle(() => reject(/* @__PURE__ */ new Error("Dead-code worker aborted.")));
|
|
40675
|
+
const timeoutHandle = setTimeout(() => settle(() => reject(/* @__PURE__ */ new Error(`Dead-code worker timed out after ${timeoutMs / MILLISECONDS_PER_SECOND}s.`))), timeoutMs);
|
|
40676
|
+
timeoutHandle.unref?.();
|
|
40677
|
+
if (abortSignal?.aborted) {
|
|
40678
|
+
onAbort();
|
|
40679
|
+
return;
|
|
40680
|
+
}
|
|
40681
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
40682
|
+
handle.result.then((value) => settle(() => resolve(value)), (error) => settle(() => reject(error)));
|
|
40360
40683
|
});
|
|
40361
40684
|
const checkDeadCode = async (options) => {
|
|
40362
40685
|
const rootDirectory = toCanonicalPath(options.rootDirectory);
|
|
@@ -40368,8 +40691,9 @@ const checkDeadCode = async (options) => {
|
|
|
40368
40691
|
entryPatterns,
|
|
40369
40692
|
tsConfigPath: resolveTsConfigPath(rootDirectory),
|
|
40370
40693
|
ignorePatterns,
|
|
40371
|
-
deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js")
|
|
40372
|
-
|
|
40694
|
+
deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
|
|
40695
|
+
parseConcurrency: options.parseConcurrency
|
|
40696
|
+
}), options.workerTimeoutMs ?? 12e4, options.abortSignal));
|
|
40373
40697
|
const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
|
|
40374
40698
|
const diagnostics = [];
|
|
40375
40699
|
for (const unusedFile of result.unusedFiles) diagnostics.push({
|
|
@@ -40467,7 +40791,37 @@ const isDiagnosticOnSurface = (diagnostic, surface, config) => {
|
|
|
40467
40791
|
return true;
|
|
40468
40792
|
};
|
|
40469
40793
|
const filterDiagnosticsForSurface = (diagnostics, surface, config) => diagnostics.filter((diagnostic) => isDiagnosticOnSurface(diagnostic, surface, config));
|
|
40470
|
-
|
|
40794
|
+
/**
|
|
40795
|
+
* Budget for the dead-code phase, scaled to the work. deslop's graph build is
|
|
40796
|
+
* CPU-bound and roughly linear in file count, so a fixed 120s cap is too tight
|
|
40797
|
+
* for a large repo (where the pass legitimately runs that long) and is then
|
|
40798
|
+
* tipped over by any concurrent load — silently dropping every dead-code
|
|
40799
|
+
* finding. Scaling the budget with file count (and inversely with the core
|
|
40800
|
+
* share when overlapped) lets the pass complete, while the ceiling still
|
|
40801
|
+
* reclaims a genuinely wedged worker. Returns the in-worker SIGKILL deadline
|
|
40802
|
+
* and the Effect-side phase backstop that sits a margin above it.
|
|
40803
|
+
*/
|
|
40804
|
+
const resolveDeadCodeTimeout = (input) => {
|
|
40805
|
+
const coreShareFactor = Math.max(1, input.fullConcurrency / Math.max(1, input.deadCodeConcurrency));
|
|
40806
|
+
const workerTimeoutMs = Math.min(DEAD_CODE_TIMEOUT_CEILING_MS, Math.max(DEAD_CODE_WORKER_TIMEOUT_MS, Math.ceil(input.sourceFileCount * 30 * coreShareFactor)));
|
|
40807
|
+
return {
|
|
40808
|
+
workerTimeoutMs,
|
|
40809
|
+
phaseTimeoutMs: workerTimeoutMs + DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS
|
|
40810
|
+
};
|
|
40811
|
+
};
|
|
40812
|
+
const collectSizedSourceFiles = (rootDirectory, relativePaths) => {
|
|
40813
|
+
const entries = [];
|
|
40814
|
+
for (const relativePath of relativePaths) {
|
|
40815
|
+
const absolutePath = Path.resolve(rootDirectory, relativePath);
|
|
40816
|
+
const sizeBytes = statSourceFileSize(absolutePath);
|
|
40817
|
+
if (isLargeMinifiedFile(absolutePath, sizeBytes)) continue;
|
|
40818
|
+
entries.push({
|
|
40819
|
+
path: relativePath,
|
|
40820
|
+
sizeBytes: sizeBytes ?? 0
|
|
40821
|
+
});
|
|
40822
|
+
}
|
|
40823
|
+
return entries;
|
|
40824
|
+
};
|
|
40471
40825
|
const listSourceFilesViaGit = (rootDirectory) => {
|
|
40472
40826
|
const result = spawnSync("git", [
|
|
40473
40827
|
"ls-files",
|
|
@@ -40500,7 +40854,8 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
|
|
|
40500
40854
|
}
|
|
40501
40855
|
return filePaths;
|
|
40502
40856
|
};
|
|
40503
|
-
const
|
|
40857
|
+
const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory));
|
|
40858
|
+
const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
|
|
40504
40859
|
const resolveLintIncludePaths = (rootDirectory, userConfig, project) => {
|
|
40505
40860
|
if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
|
|
40506
40861
|
const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
|
|
@@ -40543,9 +40898,12 @@ var Config = class Config extends Service()("react-doctor/Config") {
|
|
|
40543
40898
|
var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
|
|
40544
40899
|
static layerNode = succeed$3(DeadCode, DeadCode.of({ run: (input) => unwrap(fn("DeadCode.run")(function* () {
|
|
40545
40900
|
return yield* tryPromise({
|
|
40546
|
-
try: () => checkDeadCode({
|
|
40901
|
+
try: (signal) => checkDeadCode({
|
|
40547
40902
|
rootDirectory: input.rootDirectory,
|
|
40548
|
-
userConfig: input.userConfig
|
|
40903
|
+
userConfig: input.userConfig,
|
|
40904
|
+
parseConcurrency: input.parseConcurrency,
|
|
40905
|
+
workerTimeoutMs: input.workerTimeoutMs,
|
|
40906
|
+
abortSignal: signal
|
|
40549
40907
|
}),
|
|
40550
40908
|
catch: (cause) => new ReactDoctorError({ reason: new DeadCodeAnalysisFailed({ cause }) })
|
|
40551
40909
|
}).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)));
|
|
@@ -41258,6 +41616,14 @@ const neutralizeDisableDirectives = async (rootDirectory, includePaths) => {
|
|
|
41258
41616
|
process.removeListener("exit", onExit);
|
|
41259
41617
|
};
|
|
41260
41618
|
};
|
|
41619
|
+
const ROOT_DIRECTORY_PLACEHOLDER = "<root>";
|
|
41620
|
+
const normalizeConfigForHash = (config) => {
|
|
41621
|
+
const clone = JSON.parse(JSON.stringify(config));
|
|
41622
|
+
if (clone?.settings?.["react-doctor"]) clone.settings["react-doctor"].rootDirectory = ROOT_DIRECTORY_PLACEHOLDER;
|
|
41623
|
+
if (Array.isArray(clone?.jsPlugins)) clone.jsPlugins = clone.jsPlugins.map((_, index) => `<plugin:${index}>`);
|
|
41624
|
+
return clone;
|
|
41625
|
+
};
|
|
41626
|
+
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");
|
|
41261
41627
|
/**
|
|
41262
41628
|
* Loads a plugin module via the local require resolver and extracts
|
|
41263
41629
|
* `(name, ruleNames)` from either `module.exports.meta + rules` or
|
|
@@ -41412,8 +41778,8 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
|
|
|
41412
41778
|
}
|
|
41413
41779
|
return enabled;
|
|
41414
41780
|
};
|
|
41415
|
-
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false }) => {
|
|
41416
|
-
const reactHooksJsPlugin = disableReactHooksJsPlugin ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
|
|
41781
|
+
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection }) => {
|
|
41782
|
+
const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
|
|
41417
41783
|
const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
|
|
41418
41784
|
const jsPlugins = [];
|
|
41419
41785
|
if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
|
|
@@ -41422,6 +41788,8 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
41422
41788
|
for (const registryEntry of REACT_DOCTOR_RULES) {
|
|
41423
41789
|
const rule = reactDoctorPlugin.rules[registryEntry.id];
|
|
41424
41790
|
if (!rule) continue;
|
|
41791
|
+
if (ruleSelection === "cacheable" && CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
|
|
41792
|
+
if (ruleSelection === "sidecar" && !CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
|
|
41425
41793
|
if (rule.scan !== void 0) continue;
|
|
41426
41794
|
if (customRulesOnly && registryEntry.originallyExternal) continue;
|
|
41427
41795
|
if (rule.framework !== "global" && !rule.requires) continue;
|
|
@@ -41436,7 +41804,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
41436
41804
|
enabledReactDoctorRules[registryEntry.key] = severity;
|
|
41437
41805
|
}
|
|
41438
41806
|
const userPluginRules = {};
|
|
41439
|
-
for (const userPlugin of userPlugins) {
|
|
41807
|
+
if (ruleSelection !== "sidecar") for (const userPlugin of userPlugins) {
|
|
41440
41808
|
Object.assign(userPluginRules, buildUserPluginRules(userPlugin, severityControls));
|
|
41441
41809
|
jsPlugins.push(userPlugin.entry);
|
|
41442
41810
|
}
|
|
@@ -41466,6 +41834,100 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
41466
41834
|
}
|
|
41467
41835
|
};
|
|
41468
41836
|
};
|
|
41837
|
+
const atomicWriteJson = (filePath, value) => {
|
|
41838
|
+
try {
|
|
41839
|
+
NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
|
|
41840
|
+
const temporaryPath = `${filePath}.${process.pid}.tmp`;
|
|
41841
|
+
NFS.writeFileSync(temporaryPath, JSON.stringify(value));
|
|
41842
|
+
NFS.renameSync(temporaryPath, filePath);
|
|
41843
|
+
} catch {
|
|
41844
|
+
return;
|
|
41845
|
+
}
|
|
41846
|
+
};
|
|
41847
|
+
const failOpenReadJson = (filePath, fallback) => {
|
|
41848
|
+
try {
|
|
41849
|
+
return JSON.parse(NFS.readFileSync(filePath, "utf8"));
|
|
41850
|
+
} catch {
|
|
41851
|
+
return fallback;
|
|
41852
|
+
}
|
|
41853
|
+
};
|
|
41854
|
+
const validateDiagnostic = decodeUnknownSync(Diagnostic);
|
|
41855
|
+
const decodeFileDiagnostics = (raw) => {
|
|
41856
|
+
if (!Array.isArray(raw)) return null;
|
|
41857
|
+
try {
|
|
41858
|
+
for (const entry of raw) validateDiagnostic(entry);
|
|
41859
|
+
return raw;
|
|
41860
|
+
} catch {
|
|
41861
|
+
return null;
|
|
41862
|
+
}
|
|
41863
|
+
};
|
|
41864
|
+
const emptyCache = () => ({
|
|
41865
|
+
version: 1,
|
|
41866
|
+
rulesets: {}
|
|
41867
|
+
});
|
|
41868
|
+
const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
|
|
41869
|
+
const entries = /* @__PURE__ */ new Map();
|
|
41870
|
+
const persisted = failOpenReadJson(cacheFilePath, emptyCache());
|
|
41871
|
+
if (persisted.version !== 1 || !isRecord$2(persisted.rulesets)) return entries;
|
|
41872
|
+
const bucket = persisted.rulesets[rulesetHash];
|
|
41873
|
+
if (!isRecord$2(bucket) || !isRecord$2(bucket.files)) return entries;
|
|
41874
|
+
for (const [fileKey, rawDiagnostics] of Object.entries(bucket.files)) {
|
|
41875
|
+
const decoded = decodeFileDiagnostics(rawDiagnostics);
|
|
41876
|
+
if (decoded !== null) entries.set(fileKey, decoded);
|
|
41877
|
+
}
|
|
41878
|
+
return entries;
|
|
41879
|
+
};
|
|
41880
|
+
const createFileLintCache = (cacheDirectory, rulesetHash) => {
|
|
41881
|
+
const cacheFilePath = Path.join(cacheDirectory, FILE_LINT_CACHE_FILENAME);
|
|
41882
|
+
const entries = loadRulesetEntries(cacheFilePath, rulesetHash);
|
|
41883
|
+
return {
|
|
41884
|
+
lookup: (fileKey) => entries.get(fileKey) ?? null,
|
|
41885
|
+
store: (fileKey, diagnostics) => {
|
|
41886
|
+
entries.delete(fileKey);
|
|
41887
|
+
entries.set(fileKey, diagnostics);
|
|
41888
|
+
},
|
|
41889
|
+
persist: () => {
|
|
41890
|
+
const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
|
|
41891
|
+
const rulesets = onDisk.version === 1 && isRecord$2(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
|
|
41892
|
+
const existingBucket = rulesets[rulesetHash];
|
|
41893
|
+
const existingFiles = isRecord$2(existingBucket) && isRecord$2(existingBucket.files) ? existingBucket.files : {};
|
|
41894
|
+
const ourFiles = {};
|
|
41895
|
+
for (const [fileKey, diagnostics] of entries) ourFiles[fileKey] = diagnostics;
|
|
41896
|
+
const cappedEntries = Object.entries({
|
|
41897
|
+
...existingFiles,
|
|
41898
|
+
...ourFiles
|
|
41899
|
+
}).slice(-FILE_LINT_CACHE_MAX_FILE_COUNT);
|
|
41900
|
+
rulesets[rulesetHash] = {
|
|
41901
|
+
updatedAtMs: Date.now(),
|
|
41902
|
+
files: Object.fromEntries(cappedEntries)
|
|
41903
|
+
};
|
|
41904
|
+
const keptHashes = Object.entries(rulesets).sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8).map(([hash]) => hash);
|
|
41905
|
+
const prunedRulesets = {};
|
|
41906
|
+
for (const hash of keptHashes) prunedRulesets[hash] = rulesets[hash];
|
|
41907
|
+
atomicWriteJson(cacheFilePath, {
|
|
41908
|
+
version: 1,
|
|
41909
|
+
rulesets: prunedRulesets
|
|
41910
|
+
});
|
|
41911
|
+
}
|
|
41912
|
+
};
|
|
41913
|
+
};
|
|
41914
|
+
const bundledRequire$2 = createRequire(import.meta.url);
|
|
41915
|
+
const TOOLCHAIN_PACKAGE_SPECIFIERS$1 = [
|
|
41916
|
+
"oxlint/package.json",
|
|
41917
|
+
"oxlint-plugin-react-doctor/package.json",
|
|
41918
|
+
"eslint-plugin-react-hooks/package.json"
|
|
41919
|
+
];
|
|
41920
|
+
const resolveOxlintToolchainVersions = () => {
|
|
41921
|
+
const versions = [`node=${process.version}`];
|
|
41922
|
+
for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS$1) try {
|
|
41923
|
+
const packageJson = bundledRequire$2(specifier);
|
|
41924
|
+
const version = typeof packageJson.version === "string" ? packageJson.version : "unknown";
|
|
41925
|
+
versions.push(`${specifier}=${version}`);
|
|
41926
|
+
} catch {
|
|
41927
|
+
versions.push(`${specifier}=missing`);
|
|
41928
|
+
}
|
|
41929
|
+
return versions;
|
|
41930
|
+
};
|
|
41469
41931
|
const esmRequire = createRequire(import.meta.url);
|
|
41470
41932
|
const resolveOxlintBinary = () => {
|
|
41471
41933
|
const oxlintMainPath = esmRequire.resolve("oxlint");
|
|
@@ -42147,15 +42609,19 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
|
|
|
42147
42609
|
};
|
|
42148
42610
|
});
|
|
42149
42611
|
};
|
|
42150
|
-
const
|
|
42151
|
-
const
|
|
42152
|
-
for (const [name, value] of Object.entries(
|
|
42612
|
+
const buildOxlintChildEnv = (sourceEnv) => {
|
|
42613
|
+
const childEnv = {};
|
|
42614
|
+
for (const [name, value] of Object.entries(sourceEnv)) {
|
|
42153
42615
|
if (name === "NODE_OPTIONS" || name === "NODE_DEBUG") continue;
|
|
42154
42616
|
if (name.startsWith("npm_config_")) continue;
|
|
42155
|
-
|
|
42617
|
+
childEnv[name] = value;
|
|
42156
42618
|
}
|
|
42157
|
-
|
|
42158
|
-
|
|
42619
|
+
const isCompileCacheDisabled = Boolean(sourceEnv.NODE_DISABLE_COMPILE_CACHE);
|
|
42620
|
+
const isCompileCacheAlreadySet = childEnv.NODE_COMPILE_CACHE !== void 0;
|
|
42621
|
+
if (!isCompileCacheDisabled && !isCompileCacheAlreadySet) childEnv.NODE_COMPILE_CACHE = Path.join(os.tmpdir(), NODE_COMPILE_CACHE_DIR_NAME);
|
|
42622
|
+
return childEnv;
|
|
42623
|
+
};
|
|
42624
|
+
const SANITIZED_ENV = buildOxlintChildEnv(process.env);
|
|
42159
42625
|
/**
|
|
42160
42626
|
* Spawn one oxlint subprocess with hard ceilings on wall time and
|
|
42161
42627
|
* output size. Returns stdout on success; raises a tagged
|
|
@@ -42172,7 +42638,11 @@ const SANITIZED_ENV = (() => {
|
|
|
42172
42638
|
* The first three are splittable (the caller's binary-split retry
|
|
42173
42639
|
* shrinks the batch and re-spawns); the fourth isn't.
|
|
42174
42640
|
*/
|
|
42175
|
-
const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLINT_SPAWN_TIMEOUT_MS, outputMaxBytes = OXLINT_OUTPUT_MAX_BYTES) => new Promise((resolve, reject) => {
|
|
42641
|
+
const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLINT_SPAWN_TIMEOUT_MS, outputMaxBytes = OXLINT_OUTPUT_MAX_BYTES, abortSignal) => new Promise((resolve, reject) => {
|
|
42642
|
+
if (abortSignal?.aborted) {
|
|
42643
|
+
reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: "lint phase aborted" }) }));
|
|
42644
|
+
return;
|
|
42645
|
+
}
|
|
42176
42646
|
const child = spawn(nodeBinaryPath, args, {
|
|
42177
42647
|
cwd: rootDirectory,
|
|
42178
42648
|
env: SANITIZED_ENV,
|
|
@@ -42182,7 +42652,14 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
|
|
|
42182
42652
|
"pipe"
|
|
42183
42653
|
]
|
|
42184
42654
|
});
|
|
42655
|
+
const onAbort = () => {
|
|
42656
|
+
child.kill("SIGKILL");
|
|
42657
|
+
reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: "lint phase aborted" }) }));
|
|
42658
|
+
};
|
|
42659
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
42660
|
+
const clearAbortListener = () => abortSignal?.removeEventListener("abort", onAbort);
|
|
42185
42661
|
const timeoutHandle = setTimeout(() => {
|
|
42662
|
+
clearAbortListener();
|
|
42186
42663
|
child.kill("SIGKILL");
|
|
42187
42664
|
reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
|
|
42188
42665
|
kind: "timeout",
|
|
@@ -42217,10 +42694,12 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
|
|
|
42217
42694
|
});
|
|
42218
42695
|
child.on("error", (error) => {
|
|
42219
42696
|
clearTimeout(timeoutHandle);
|
|
42697
|
+
clearAbortListener();
|
|
42220
42698
|
reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: error }) }));
|
|
42221
42699
|
});
|
|
42222
42700
|
child.on("close", (_code, signal) => {
|
|
42223
42701
|
clearTimeout(timeoutHandle);
|
|
42702
|
+
clearAbortListener();
|
|
42224
42703
|
if (didKillForSize) {
|
|
42225
42704
|
reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
|
|
42226
42705
|
kind: "output-too-large",
|
|
@@ -42287,26 +42766,28 @@ const isParallelismRelatedSpawnError = (error) => {
|
|
|
42287
42766
|
* loop with a slimmer config in that case.
|
|
42288
42767
|
*/
|
|
42289
42768
|
const spawnLintBatches = async (input) => {
|
|
42290
|
-
const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes } = input;
|
|
42769
|
+
const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 8, signal } = input;
|
|
42291
42770
|
const requestedConcurrency = resolveScanConcurrency(input.concurrency ?? 1);
|
|
42292
42771
|
const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
|
|
42293
42772
|
const runBatchPass = async (concurrency) => {
|
|
42294
42773
|
const allDiagnostics = [];
|
|
42295
42774
|
const droppedFiles = [];
|
|
42296
42775
|
let firstDropReason = null;
|
|
42297
|
-
const
|
|
42776
|
+
const splitDeadlineMs = Date.now() + splitTotalBudgetMs;
|
|
42777
|
+
const spawnLintBatch = async (batch, depth) => {
|
|
42298
42778
|
const batchArgs = [...baseArgs, ...batch];
|
|
42299
42779
|
try {
|
|
42300
|
-
return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes), project, rootDirectory);
|
|
42780
|
+
return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes, signal), project, rootDirectory);
|
|
42301
42781
|
} catch (error) {
|
|
42302
42782
|
if (!isSplittableReactDoctorError(error)) throw error;
|
|
42303
|
-
|
|
42783
|
+
const splitBudgetExhausted = Date.now() >= splitDeadlineMs || depth >= splitMaxDepth;
|
|
42784
|
+
if (batch.length <= 1 || splitBudgetExhausted) {
|
|
42304
42785
|
droppedFiles.push(...batch);
|
|
42305
|
-
if (firstDropReason === null) firstDropReason = error.message;
|
|
42786
|
+
if (firstDropReason === null) firstDropReason = splitBudgetExhausted && batch.length > 1 ? `${error.message} (split budget exhausted after ${splitMaxDepth} levels / ${splitTotalBudgetMs / MILLISECONDS_PER_SECOND}s)` : error.message;
|
|
42306
42787
|
return [];
|
|
42307
42788
|
}
|
|
42308
42789
|
const splitIndex = Math.ceil(batch.length / 2);
|
|
42309
|
-
return [...await spawnLintBatch(batch.slice(0, splitIndex)), ...await spawnLintBatch(batch.slice(splitIndex))];
|
|
42790
|
+
return [...await spawnLintBatch(batch.slice(0, splitIndex), depth + 1), ...await spawnLintBatch(batch.slice(splitIndex), depth + 1)];
|
|
42310
42791
|
}
|
|
42311
42792
|
};
|
|
42312
42793
|
let startedFileCount = 0;
|
|
@@ -42323,7 +42804,7 @@ const spawnLintBatches = async (input) => {
|
|
|
42323
42804
|
try {
|
|
42324
42805
|
const batchResults = await mapWithConcurrency(fileBatches, concurrency, async (batch) => {
|
|
42325
42806
|
startedFileCount += batch.length;
|
|
42326
|
-
const batchDiagnostics = await spawnLintBatch(batch);
|
|
42807
|
+
const batchDiagnostics = await spawnLintBatch(batch, 0);
|
|
42327
42808
|
scannedFileCount += batch.length;
|
|
42328
42809
|
if (onFileProgress) {
|
|
42329
42810
|
displayedFileCount = Math.min(Math.max(displayedFileCount, scannedFileCount), totalFileCount);
|
|
@@ -42384,6 +42865,22 @@ const validateRuleRegistration = () => {
|
|
|
42384
42865
|
].filter((entry) => entry !== null).join("; ");
|
|
42385
42866
|
console.warn(`[react-doctor] rule-registration drift: ${detail}`);
|
|
42386
42867
|
};
|
|
42868
|
+
const hashFileContents = (filePath) => {
|
|
42869
|
+
try {
|
|
42870
|
+
return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
|
|
42871
|
+
} catch {
|
|
42872
|
+
return null;
|
|
42873
|
+
}
|
|
42874
|
+
};
|
|
42875
|
+
const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
|
|
42876
|
+
const resolveReactDoctorCacheDir = (projectDirectory) => {
|
|
42877
|
+
const cacheDirOverride = process.env["REACT_DOCTOR_CACHE_DIR"]?.trim();
|
|
42878
|
+
if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
|
|
42879
|
+
const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
|
|
42880
|
+
if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
|
|
42881
|
+
return Path.join(os.tmpdir(), "react-doctor-cache", projectCacheSubdir(projectDirectory));
|
|
42882
|
+
};
|
|
42883
|
+
const sortSourceFilesByCost = (entries) => [...entries].sort((left, right) => right.sizeBytes - left.sizeBytes).map((entry) => entry.path);
|
|
42387
42884
|
/**
|
|
42388
42885
|
* Atomically (re)writes the generated oxlintrc.json. Used twice in
|
|
42389
42886
|
* the runner: once for the primary scan, once for the
|
|
@@ -42442,7 +42939,7 @@ const reactHooksJsPluginDropNote = (error) => {
|
|
|
42442
42939
|
* 6. always restore disable directives + clean up the temp dir
|
|
42443
42940
|
*/
|
|
42444
42941
|
const runOxlint = async (options) => {
|
|
42445
|
-
const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure, spawnTimeoutMs, outputMaxBytes } = options;
|
|
42942
|
+
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;
|
|
42446
42943
|
const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
|
|
42447
42944
|
const severityControls = buildRuleSeverityControls(userConfig);
|
|
42448
42945
|
validateRuleRegistration();
|
|
@@ -42459,30 +42956,156 @@ const runOxlint = async (options) => {
|
|
|
42459
42956
|
serverAuthFunctionNames,
|
|
42460
42957
|
severityControls,
|
|
42461
42958
|
userPlugins,
|
|
42462
|
-
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
|
|
42959
|
+
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin,
|
|
42960
|
+
ruleSelection: overrides.ruleSelection
|
|
42463
42961
|
});
|
|
42464
42962
|
const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
|
|
42465
42963
|
const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
|
|
42466
42964
|
const configPath = Path.join(configDirectory, "oxlintrc.json");
|
|
42467
42965
|
try {
|
|
42468
|
-
const
|
|
42469
|
-
|
|
42470
|
-
|
|
42471
|
-
configPath,
|
|
42472
|
-
"--format",
|
|
42473
|
-
"json"
|
|
42474
|
-
];
|
|
42966
|
+
const oxlintBinary = resolveOxlintBinary();
|
|
42967
|
+
const sharedArgs = [];
|
|
42968
|
+
let tsconfigContent = null;
|
|
42475
42969
|
if (project.hasTypeScript) {
|
|
42476
42970
|
const tsconfigRelativePath = resolveTsConfigRelativePath(rootDirectory);
|
|
42477
|
-
if (tsconfigRelativePath)
|
|
42971
|
+
if (tsconfigRelativePath) {
|
|
42972
|
+
sharedArgs.push("--tsconfig", tsconfigRelativePath);
|
|
42973
|
+
try {
|
|
42974
|
+
tsconfigContent = NFS.readFileSync(Path.resolve(rootDirectory, tsconfigRelativePath), "utf8");
|
|
42975
|
+
} catch {
|
|
42976
|
+
tsconfigContent = null;
|
|
42977
|
+
}
|
|
42978
|
+
}
|
|
42478
42979
|
}
|
|
42479
42980
|
const combinedPatterns = collectIgnorePatterns(rootDirectory);
|
|
42480
42981
|
if (combinedPatterns.length > 0) {
|
|
42481
42982
|
const combinedIgnorePath = Path.join(configDirectory, "combined.ignore");
|
|
42482
42983
|
NFS.writeFileSync(combinedIgnorePath, `${combinedPatterns.join("\n")}\n`);
|
|
42483
|
-
|
|
42984
|
+
sharedArgs.push("--ignore-path", combinedIgnorePath);
|
|
42985
|
+
}
|
|
42986
|
+
const makeBaseArgs = (oxlintConfigPath) => [
|
|
42987
|
+
oxlintBinary,
|
|
42988
|
+
"-c",
|
|
42989
|
+
oxlintConfigPath,
|
|
42990
|
+
"--format",
|
|
42991
|
+
"json",
|
|
42992
|
+
...sharedArgs
|
|
42993
|
+
];
|
|
42994
|
+
const discoverScanFiles = () => lintBatchOrdering === "cost" ? sortSourceFilesByCost(listSourceFilesWithSize(rootDirectory)) : listSourceFiles(rootDirectory);
|
|
42995
|
+
const candidateFiles = includePaths !== void 0 ? includePaths : discoverScanFiles();
|
|
42996
|
+
const runConfigOverFiles = async (buildConfigForPass, configFileName, files, fileProgress) => {
|
|
42997
|
+
if (files.length === 0) return {
|
|
42998
|
+
diagnostics: [],
|
|
42999
|
+
didDropReactHooksJsPlugin: false,
|
|
43000
|
+
hadPartialFailure: false
|
|
43001
|
+
};
|
|
43002
|
+
let hadPartialFailure = false;
|
|
43003
|
+
const reportPartialFailure = (reason) => {
|
|
43004
|
+
hadPartialFailure = true;
|
|
43005
|
+
onPartialFailure?.(reason);
|
|
43006
|
+
};
|
|
43007
|
+
const passConfigPath = Path.join(configDirectory, configFileName);
|
|
43008
|
+
const passBaseArgs = makeBaseArgs(passConfigPath);
|
|
43009
|
+
const passFileBatches = batchIncludePaths(passBaseArgs, files);
|
|
43010
|
+
const spawnPass = () => spawnLintBatches({
|
|
43011
|
+
baseArgs: passBaseArgs,
|
|
43012
|
+
fileBatches: passFileBatches,
|
|
43013
|
+
rootDirectory,
|
|
43014
|
+
nodeBinaryPath,
|
|
43015
|
+
project,
|
|
43016
|
+
onPartialFailure: reportPartialFailure,
|
|
43017
|
+
onFileProgress: fileProgress,
|
|
43018
|
+
spawnTimeoutMs,
|
|
43019
|
+
outputMaxBytes,
|
|
43020
|
+
concurrency: options.concurrency,
|
|
43021
|
+
signal: options.signal
|
|
43022
|
+
});
|
|
43023
|
+
writeOxlintConfig(passConfigPath, buildConfigForPass({}));
|
|
43024
|
+
try {
|
|
43025
|
+
return {
|
|
43026
|
+
diagnostics: await spawnPass(),
|
|
43027
|
+
didDropReactHooksJsPlugin: false,
|
|
43028
|
+
hadPartialFailure
|
|
43029
|
+
};
|
|
43030
|
+
} catch (error) {
|
|
43031
|
+
const reactHooksJsDropNote = reactHooksJsPluginDropNote(error);
|
|
43032
|
+
if (reactHooksJsDropNote === null) throw error;
|
|
43033
|
+
writeOxlintConfig(passConfigPath, buildConfigForPass({ disableReactHooksJsPlugin: true }));
|
|
43034
|
+
const diagnostics = await spawnPass();
|
|
43035
|
+
reportPartialFailure(reactHooksJsDropNote);
|
|
43036
|
+
return {
|
|
43037
|
+
diagnostics,
|
|
43038
|
+
didDropReactHooksJsPlugin: true,
|
|
43039
|
+
hadPartialFailure
|
|
43040
|
+
};
|
|
43041
|
+
}
|
|
43042
|
+
};
|
|
43043
|
+
if (perFileLintCacheEnabled && respectInlineDisables && !project.hasReactCompiler && extendsPaths.length === 0 && userPlugins.length === 0) {
|
|
43044
|
+
const rulesetHash = computeRulesetHash({
|
|
43045
|
+
config: buildConfig({
|
|
43046
|
+
extendsPaths: [],
|
|
43047
|
+
ruleSelection: "cacheable"
|
|
43048
|
+
}),
|
|
43049
|
+
toolchainVersions: resolveOxlintToolchainVersions(),
|
|
43050
|
+
ignorePatterns: combinedPatterns,
|
|
43051
|
+
tsconfigContent
|
|
43052
|
+
});
|
|
43053
|
+
const cache = createFileLintCache(resolveReactDoctorCacheDir(rootDirectory), rulesetHash);
|
|
43054
|
+
const cacheKeyByFile = /* @__PURE__ */ new Map();
|
|
43055
|
+
const missFiles = [];
|
|
43056
|
+
const replayedDiagnostics = [];
|
|
43057
|
+
for (const candidateFile of candidateFiles) {
|
|
43058
|
+
const contentHash = hashFileContents(Path.resolve(rootDirectory, candidateFile));
|
|
43059
|
+
if (contentHash === null) {
|
|
43060
|
+
missFiles.push(candidateFile);
|
|
43061
|
+
continue;
|
|
43062
|
+
}
|
|
43063
|
+
const cacheKey = `${candidateFile.replaceAll("\\", "/")}${contentHash}`;
|
|
43064
|
+
cacheKeyByFile.set(candidateFile, cacheKey);
|
|
43065
|
+
const cachedDiagnostics = cache.lookup(cacheKey);
|
|
43066
|
+
if (cachedDiagnostics === null) missFiles.push(candidateFile);
|
|
43067
|
+
else replayedDiagnostics.push(...cachedDiagnostics);
|
|
43068
|
+
}
|
|
43069
|
+
const cacheHitFileCount = candidateFiles.length - missFiles.length;
|
|
43070
|
+
const cacheableResult = await runConfigOverFiles((overrides) => buildConfig({
|
|
43071
|
+
extendsPaths: [],
|
|
43072
|
+
ruleSelection: "cacheable",
|
|
43073
|
+
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
|
|
43074
|
+
}), "oxlintrc.cacheable.json", missFiles, void 0);
|
|
43075
|
+
const sidecarResult = await runConfigOverFiles(() => buildConfig({
|
|
43076
|
+
extendsPaths: [],
|
|
43077
|
+
ruleSelection: "sidecar"
|
|
43078
|
+
}), "oxlintrc.sidecar.json", candidateFiles, options.onFileProgress);
|
|
43079
|
+
onCacheStats?.(cacheHitFileCount, candidateFiles.length);
|
|
43080
|
+
const missFileByNormalizedPath = /* @__PURE__ */ new Map();
|
|
43081
|
+
for (const missFile of missFiles) missFileByNormalizedPath.set(missFile.replaceAll("\\", "/"), missFile);
|
|
43082
|
+
const freshDiagnosticsByFile = /* @__PURE__ */ new Map();
|
|
43083
|
+
let isAttributionSound = true;
|
|
43084
|
+
for (const diagnostic of cacheableResult.diagnostics) {
|
|
43085
|
+
const missFile = missFileByNormalizedPath.get(diagnostic.filePath);
|
|
43086
|
+
if (missFile === void 0) {
|
|
43087
|
+
isAttributionSound = false;
|
|
43088
|
+
break;
|
|
43089
|
+
}
|
|
43090
|
+
const fileDiagnostics = freshDiagnosticsByFile.get(missFile) ?? [];
|
|
43091
|
+
fileDiagnostics.push(diagnostic);
|
|
43092
|
+
freshDiagnosticsByFile.set(missFile, fileDiagnostics);
|
|
43093
|
+
}
|
|
43094
|
+
if (!cacheableResult.didDropReactHooksJsPlugin && !cacheableResult.hadPartialFailure && isAttributionSound) {
|
|
43095
|
+
for (const missFile of missFiles) {
|
|
43096
|
+
const cacheKey = cacheKeyByFile.get(missFile);
|
|
43097
|
+
if (cacheKey !== void 0) cache.store(cacheKey, freshDiagnosticsByFile.get(missFile) ?? []);
|
|
43098
|
+
}
|
|
43099
|
+
cache.persist();
|
|
43100
|
+
}
|
|
43101
|
+
return dedupeDiagnostics([
|
|
43102
|
+
...replayedDiagnostics,
|
|
43103
|
+
...cacheableResult.diagnostics,
|
|
43104
|
+
...sidecarResult.diagnostics
|
|
43105
|
+
]);
|
|
42484
43106
|
}
|
|
42485
|
-
const
|
|
43107
|
+
const baseArgs = makeBaseArgs(configPath);
|
|
43108
|
+
const fileBatches = batchIncludePaths(baseArgs, candidateFiles);
|
|
42486
43109
|
const runBatches = () => spawnLintBatches({
|
|
42487
43110
|
baseArgs,
|
|
42488
43111
|
fileBatches,
|
|
@@ -42493,7 +43116,8 @@ const runOxlint = async (options) => {
|
|
|
42493
43116
|
onFileProgress: options.onFileProgress,
|
|
42494
43117
|
spawnTimeoutMs,
|
|
42495
43118
|
outputMaxBytes,
|
|
42496
|
-
concurrency: options.concurrency
|
|
43119
|
+
concurrency: options.concurrency,
|
|
43120
|
+
signal: options.signal
|
|
42497
43121
|
});
|
|
42498
43122
|
writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
|
|
42499
43123
|
try {
|
|
@@ -42572,9 +43196,11 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
|
|
|
42572
43196
|
const spawnTimeoutMs = yield* OxlintSpawnTimeoutMs;
|
|
42573
43197
|
const outputMaxBytes = yield* OxlintOutputMaxBytes;
|
|
42574
43198
|
const concurrency = yield* OxlintConcurrency;
|
|
43199
|
+
const lintBatchOrdering = yield* LintBatchOrdering;
|
|
43200
|
+
const perFileLintCacheEnabled = yield* PerFileLintCacheEnabled;
|
|
42575
43201
|
const collectedFailures = [];
|
|
42576
43202
|
const diagnostics = yield* tryPromise({
|
|
42577
|
-
try: () => runOxlint({
|
|
43203
|
+
try: (signal) => runOxlint({
|
|
42578
43204
|
rootDirectory: input.rootDirectory,
|
|
42579
43205
|
project: input.project,
|
|
42580
43206
|
includePaths: input.includePaths ? [...input.includePaths] : void 0,
|
|
@@ -42589,9 +43215,13 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
|
|
|
42589
43215
|
collectedFailures.push(reason);
|
|
42590
43216
|
},
|
|
42591
43217
|
onFileProgress: input.onFileProgress,
|
|
43218
|
+
perFileLintCacheEnabled,
|
|
43219
|
+
onCacheStats: input.onCacheStats,
|
|
42592
43220
|
spawnTimeoutMs,
|
|
42593
43221
|
outputMaxBytes,
|
|
42594
|
-
concurrency
|
|
43222
|
+
concurrency,
|
|
43223
|
+
signal,
|
|
43224
|
+
lintBatchOrdering
|
|
42595
43225
|
}),
|
|
42596
43226
|
catch: ensureReactDoctorError
|
|
42597
43227
|
});
|
|
@@ -42983,14 +43613,49 @@ const parseArtifactFromBody = (body) => {
|
|
|
42983
43613
|
}
|
|
42984
43614
|
return null;
|
|
42985
43615
|
};
|
|
42986
|
-
const
|
|
43616
|
+
const isSupplyChainCacheDisabled = () => {
|
|
43617
|
+
const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
|
|
43618
|
+
return noCache === "1" || noCache === "true";
|
|
43619
|
+
};
|
|
43620
|
+
const supplyChainCacheFile = (cacheDirectory, dependency) => {
|
|
43621
|
+
const purlHash = crypto.createHash("sha256").update(toPurl(dependency)).digest("hex").slice(0, 16);
|
|
43622
|
+
return Path.join(cacheDirectory, SUPPLY_CHAIN_CACHE_SUBDIR, `${purlHash}.json`);
|
|
43623
|
+
};
|
|
43624
|
+
const readCachedSocketBody = (cacheFile) => {
|
|
43625
|
+
try {
|
|
43626
|
+
const entry = JSON.parse(NFS.readFileSync(cacheFile, "utf-8"));
|
|
43627
|
+
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;
|
|
43628
|
+
} catch {}
|
|
43629
|
+
return null;
|
|
43630
|
+
};
|
|
43631
|
+
const writeCachedSocketBody = (cacheFile, body) => {
|
|
43632
|
+
try {
|
|
43633
|
+
NFS.mkdirSync(Path.dirname(cacheFile), { recursive: true });
|
|
43634
|
+
NFS.writeFileSync(cacheFile, JSON.stringify({
|
|
43635
|
+
fetchedAtMs: Date.now(),
|
|
43636
|
+
body
|
|
43637
|
+
}));
|
|
43638
|
+
} catch {}
|
|
43639
|
+
};
|
|
43640
|
+
const fetchSocketArtifact = (dependency, cacheDirectory) => tryPromise(async (signal) => {
|
|
43641
|
+
const cacheFile = cacheDirectory === null ? null : supplyChainCacheFile(cacheDirectory, dependency);
|
|
43642
|
+
if (cacheFile !== null) {
|
|
43643
|
+
const cachedBody = readCachedSocketBody(cacheFile);
|
|
43644
|
+
if (cachedBody !== null) {
|
|
43645
|
+
const cachedArtifact = parseArtifactFromBody(cachedBody);
|
|
43646
|
+
if (cachedArtifact !== null) return cachedArtifact;
|
|
43647
|
+
}
|
|
43648
|
+
}
|
|
42987
43649
|
const requestUrl = `${SOCKET_FREE_PURL_API_BASE}/${encodeURIComponent(toPurl(dependency))}`;
|
|
42988
43650
|
const response = await fetch(requestUrl, {
|
|
42989
43651
|
headers: { "User-Agent": SOCKET_FREE_USER_AGENT },
|
|
42990
43652
|
signal
|
|
42991
43653
|
});
|
|
42992
43654
|
if (!response.ok) return null;
|
|
42993
|
-
|
|
43655
|
+
const body = await response.text();
|
|
43656
|
+
const artifact = parseArtifactFromBody(body);
|
|
43657
|
+
if (artifact !== null && cacheFile !== null) writeCachedSocketBody(cacheFile, body);
|
|
43658
|
+
return artifact;
|
|
42994
43659
|
}).pipe(timeout(FETCH_TIMEOUT_MS), orElseSucceed(() => null), tap$1((artifact) => {
|
|
42995
43660
|
const scoreAttributes = {};
|
|
42996
43661
|
if (artifact !== null) {
|
|
@@ -43095,7 +43760,8 @@ const checkSupplyChain = (input) => gen(function* () {
|
|
|
43095
43760
|
const packageJsonPath = Path.join(input.rootDirectory, "package.json");
|
|
43096
43761
|
const dependencies = collectDependenciesToScore(readPackageJson$1(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies);
|
|
43097
43762
|
if (dependencies.length === 0) return [];
|
|
43098
|
-
const
|
|
43763
|
+
const cacheDirectory = isSupplyChainCacheDisabled() ? null : resolveReactDoctorCacheDir(input.rootDirectory);
|
|
43764
|
+
const artifacts = yield* forEach$1(dependencies, (dependency) => fetchSocketArtifact(dependency, cacheDirectory), { concurrency: 8 }).pipe(timeoutOption(input.totalTimeoutMs ?? 9e4), map$3((maybeArtifacts) => getOrElse$1(maybeArtifacts, () => [])));
|
|
43099
43765
|
const diagnostics = [];
|
|
43100
43766
|
for (let index = 0; index < dependencies.length; index += 1) {
|
|
43101
43767
|
const artifact = artifacts[index];
|
|
@@ -43120,6 +43786,10 @@ const checkSupplyChain = (input) => gen(function* () {
|
|
|
43120
43786
|
* The underlying `checkSupplyChain` Effect is total/fail-open — per-package
|
|
43121
43787
|
* timeouts and network failures recover to "skip" — so the stream never
|
|
43122
43788
|
* fails, mirroring `DeadCode`'s stream shape so the two compose the same way.
|
|
43789
|
+
* The orchestrator (`run-inspect.ts`) consumes this stream on a background
|
|
43790
|
+
* fiber whose network time overlaps the lint pass, joined under a generous
|
|
43791
|
+
* wall-clock budget; a budget expiry is the same fail-open outcome as a Socket
|
|
43792
|
+
* outage.
|
|
43123
43793
|
*/
|
|
43124
43794
|
var SupplyChain = class SupplyChain extends Service()("react-doctor/SupplyChain") {
|
|
43125
43795
|
static layerNode = succeed$3(SupplyChain, SupplyChain.of({ run: (input) => unwrap(checkSupplyChain(input).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), withSpan("SupplyChain.run"))) }));
|
|
@@ -43178,18 +43848,42 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
|
|
|
43178
43848
|
*
|
|
43179
43849
|
* Phases:
|
|
43180
43850
|
*
|
|
43181
|
-
* 1. Config.resolve(directory) → Project.discover → Git metadata
|
|
43851
|
+
* 1. Config.resolve(directory) → Project.discover → Git metadata.
|
|
43852
|
+
* The GitHub viewer-permission lookup is forked onto a background
|
|
43853
|
+
* fiber here and joined late (it feeds score metadata, not
|
|
43854
|
+
* diagnostics).
|
|
43182
43855
|
* 2. beforeLint hook (e.g. CLI renders the project-detection block)
|
|
43183
43856
|
* 3. environment checks (reduced-motion + pnpm hardening +
|
|
43184
|
-
* expo/react-native + security scan)
|
|
43185
|
-
* 4.
|
|
43186
|
-
*
|
|
43187
|
-
*
|
|
43188
|
-
*
|
|
43189
|
-
*
|
|
43190
|
-
*
|
|
43191
|
-
*
|
|
43192
|
-
*
|
|
43857
|
+
* expo/react-native + security scan), collected synchronously
|
|
43858
|
+
* 4. The supply-chain check (Socket.dev) is forked onto a background
|
|
43859
|
+
* fiber so its ~100% network-bound time overlaps the ~100%
|
|
43860
|
+
* CPU/subprocess-bound lint pass below, collapsing two serial
|
|
43861
|
+
* phases into roughly `max(supplyChain, lint)`. It is capped by
|
|
43862
|
+
* `SupplyChainOverlapTimeoutMs` (measured from fork) so a hung
|
|
43863
|
+
* socket can't drag out its join; on timeout it fails open to no
|
|
43864
|
+
* diagnostics — the same outcome class as a Socket outage.
|
|
43865
|
+
* 5. Linter.run runs; DeadCode.run runs concurrently (forked child
|
|
43866
|
+
* fiber) ONLY when the memory gate has headroom to run the 8 GB
|
|
43867
|
+
* dead-code child alongside the oxlint workers — or when overlap is
|
|
43868
|
+
* forced via REACT_DOCTOR_DEAD_CODE_OVERLAP. Otherwise dead-code
|
|
43869
|
+
* runs sequentially after lint, exactly as it did pre-overlap. The
|
|
43870
|
+
* fiber is joined (or interrupted, SIGKILLing its worker, on lint
|
|
43871
|
+
* failure) before diagnostics are concatenated. The afterLint hook
|
|
43872
|
+
* fires between lint and dead-code. Progress spinner labels AND the
|
|
43873
|
+
* final diagnostic / score order stay independent of execution
|
|
43874
|
+
* order, so terminal output is identical either way; supply-chain
|
|
43875
|
+
* rides alongside without a spinner.
|
|
43876
|
+
* 6. Join the supply-chain fiber, then assemble the diagnostics in a
|
|
43877
|
+
* FIXED order (env, supply-chain, lint, dead-code) so the output is
|
|
43878
|
+
* byte-identical regardless of which fiber settled first. The
|
|
43879
|
+
* viewer-permission fiber is joined later, during score-metadata
|
|
43880
|
+
* assembly (it feeds score metadata, not diagnostics). The per-element
|
|
43881
|
+
* `Reporter.emit` side-channel now interleaves supply-chain with lint
|
|
43882
|
+
* emits, so capture-order assertions must target the deterministic
|
|
43883
|
+
* concat below, not emit order (production `Reporter.layerNoop` makes
|
|
43884
|
+
* emit a no-op).
|
|
43885
|
+
* 7. Reporter.finalize
|
|
43886
|
+
* 8. Score.compute against the surface-filtered diagnostic set
|
|
43193
43887
|
*
|
|
43194
43888
|
* The orchestrator owns spinner lifecycle via `Progress`; callers
|
|
43195
43889
|
* choose `Progress.layerOra(...)` for CLI feedback or
|
|
@@ -43247,10 +43941,21 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
43247
43941
|
ignoredTags: input.ignoredTags
|
|
43248
43942
|
})
|
|
43249
43943
|
])));
|
|
43250
|
-
const
|
|
43944
|
+
const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
|
|
43945
|
+
const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
|
|
43946
|
+
const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
|
|
43251
43947
|
rootDirectory: scanDirectory,
|
|
43252
43948
|
userConfig: resolvedConfig.config
|
|
43253
|
-
})))
|
|
43949
|
+
}))).pipe(map$3((diagnostics) => ({
|
|
43950
|
+
diagnostics,
|
|
43951
|
+
timedOut: false
|
|
43952
|
+
})), timeout(supplyChainOverlapTimeout), orElseSucceed(() => ({
|
|
43953
|
+
diagnostics: [],
|
|
43954
|
+
timedOut: true
|
|
43955
|
+
}))) : succeed$2({
|
|
43956
|
+
diagnostics: [],
|
|
43957
|
+
timedOut: false
|
|
43958
|
+
}));
|
|
43254
43959
|
const lintFailure = yield* make$13({
|
|
43255
43960
|
didFail: false,
|
|
43256
43961
|
reason: null,
|
|
@@ -43261,12 +43966,49 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
43261
43966
|
didFail: false,
|
|
43262
43967
|
reason: null
|
|
43263
43968
|
});
|
|
43264
|
-
const scanConcurrency = yield* OxlintConcurrency;
|
|
43969
|
+
const scanConcurrency = resolveScanConcurrency(yield* OxlintConcurrency);
|
|
43970
|
+
const lintPhaseTimeoutMs = yield* LintPhaseTimeoutMs;
|
|
43971
|
+
const deadCodePhaseTimeoutMs = yield* DeadCodePhaseTimeoutMs;
|
|
43972
|
+
const resolveDeadCodePhaseTimeoutMs = (scaledPhaseTimeoutMs) => deadCodePhaseTimeoutMs === 15e4 ? scaledPhaseTimeoutMs : deadCodePhaseTimeoutMs;
|
|
43265
43973
|
const workerCountSuffix = scanConcurrency > 1 ? ` ${highlighter.dim(`[~${scanConcurrency} workers]`)}` : "";
|
|
43974
|
+
const shouldRunDeadCode = input.runDeadCode && !isDiffMode && (showWarnings || deadCodeMaySurfaceWhenWarningsHidden(resolvedConfig.config));
|
|
43975
|
+
const deadCodeOverlapMode = yield* DeadCodeOverlap;
|
|
43976
|
+
const shouldOverlapDeadCode = shouldRunDeadCode && deadCodeOverlapMode === "on";
|
|
43977
|
+
const deadCodeParseConcurrency = shouldOverlapDeadCode ? Math.max(1, Math.floor(scanConcurrency * DEAD_CODE_OVERLAP_PARSE_SHARE)) : void 0;
|
|
43978
|
+
const lintConcurrency = deadCodeParseConcurrency === void 0 ? scanConcurrency : Math.max(1, scanConcurrency - deadCodeParseConcurrency);
|
|
43979
|
+
const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
|
|
43980
|
+
rootDirectory: scanDirectory,
|
|
43981
|
+
userConfig: resolvedConfig.config,
|
|
43982
|
+
parseConcurrency: deadCodeParseConcurrency,
|
|
43983
|
+
workerTimeoutMs: deadCodeTimeout.workerTimeoutMs
|
|
43984
|
+
}).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
|
|
43985
|
+
yield* set(deadCodeFailure, {
|
|
43986
|
+
didFail: true,
|
|
43987
|
+
reason: error.message
|
|
43988
|
+
});
|
|
43989
|
+
return empty$4;
|
|
43990
|
+
})))))).pipe(timeoutOption(deadCodeTimeout.phaseTimeoutMs), flatMap$2(match$3({
|
|
43991
|
+
onNone: () => set(deadCodeFailure, {
|
|
43992
|
+
didFail: true,
|
|
43993
|
+
reason: `Dead-code analysis exceeded ${Math.round(deadCodeTimeout.phaseTimeoutMs / MILLISECONDS_PER_SECOND)}s and was skipped.`
|
|
43994
|
+
}).pipe(as([])),
|
|
43995
|
+
onSome: succeed$2
|
|
43996
|
+
})));
|
|
43997
|
+
const overlapDeadCodeTimeout = resolveDeadCodeTimeout({
|
|
43998
|
+
sourceFileCount: project.sourceFileCount,
|
|
43999
|
+
deadCodeConcurrency: deadCodeParseConcurrency ?? scanConcurrency,
|
|
44000
|
+
fullConcurrency: scanConcurrency
|
|
44001
|
+
});
|
|
44002
|
+
const deadCodeFiber = shouldOverlapDeadCode ? yield* forkChild(buildCollectDeadCode({
|
|
44003
|
+
workerTimeoutMs: overlapDeadCodeTimeout.workerTimeoutMs,
|
|
44004
|
+
phaseTimeoutMs: resolveDeadCodePhaseTimeoutMs(overlapDeadCodeTimeout.phaseTimeoutMs)
|
|
44005
|
+
})) : null;
|
|
43266
44006
|
const scanProgress = yield* progressService.start("Scanning...");
|
|
43267
44007
|
const scanStartTime = Date.now();
|
|
43268
44008
|
let lastReportedTotalFileCount = 0;
|
|
43269
|
-
|
|
44009
|
+
let lintCacheHitFileCount = null;
|
|
44010
|
+
let lintCacheTotalFileCount = null;
|
|
44011
|
+
const baseLintStream = linterService.run({
|
|
43270
44012
|
rootDirectory: scanDirectory,
|
|
43271
44013
|
project,
|
|
43272
44014
|
includePaths: lintIncludePaths ?? void 0,
|
|
@@ -43280,6 +44022,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
43280
44022
|
onFileProgress: (scannedFileCount, totalFileCount) => {
|
|
43281
44023
|
lastReportedTotalFileCount = totalFileCount;
|
|
43282
44024
|
runSync(scanProgress.update(`Scanning files (${scannedFileCount}/${totalFileCount})${workerCountSuffix}...`));
|
|
44025
|
+
},
|
|
44026
|
+
onCacheStats: (cacheHitFileCount, totalConsideredFileCount) => {
|
|
44027
|
+
lintCacheHitFileCount = cacheHitFileCount;
|
|
44028
|
+
lintCacheTotalFileCount = totalConsideredFileCount;
|
|
43283
44029
|
}
|
|
43284
44030
|
}).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
|
|
43285
44031
|
yield* set(lintFailure, {
|
|
@@ -43289,36 +44035,54 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
43289
44035
|
reasonKind: error.reason._tag === "OxlintUnavailable" ? error.reason.kind : null
|
|
43290
44036
|
});
|
|
43291
44037
|
return empty$4;
|
|
43292
|
-
}))))
|
|
44038
|
+
}))));
|
|
44039
|
+
const lintCollected = yield* runCollect(applyPerElementPipeline(shouldOverlapDeadCode ? baseLintStream.pipe(provideService(OxlintConcurrency, lintConcurrency)) : baseLintStream)).pipe(timeoutOption(lintPhaseTimeoutMs), flatMap$2(match$3({
|
|
44040
|
+
onNone: () => set(lintFailure, {
|
|
44041
|
+
didFail: true,
|
|
44042
|
+
reason: `Lint analysis exceeded ${lintPhaseTimeoutMs / MILLISECONDS_PER_SECOND}s and was skipped.`,
|
|
44043
|
+
reasonTag: "OxlintBatchExceeded",
|
|
44044
|
+
reasonKind: null
|
|
44045
|
+
}).pipe(as([])),
|
|
44046
|
+
onSome: succeed$2
|
|
44047
|
+
})));
|
|
43293
44048
|
const lintFailureState = yield* get$2(lintFailure);
|
|
43294
44049
|
yield* afterLint(lintFailureState.didFail);
|
|
43295
44050
|
if (lintFailureState.didFail) yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
|
|
43296
44051
|
const totalFileCount = lastReportedTotalFileCount || (lintIncludePaths?.length ?? project.sourceFileCount);
|
|
43297
44052
|
const scannedFilesLabel = `${totalFileCount} ${totalFileCount === 1 ? "file" : "files"}`;
|
|
43298
|
-
|
|
43299
|
-
|
|
43300
|
-
|
|
43301
|
-
|
|
43302
|
-
|
|
43303
|
-
|
|
43304
|
-
|
|
43305
|
-
|
|
44053
|
+
let deadCodeCollected = [];
|
|
44054
|
+
if (lintFailureState.didFail) {
|
|
44055
|
+
if (deadCodeFiber !== null) yield* interrupt(deadCodeFiber);
|
|
44056
|
+
} else if (shouldRunDeadCode) {
|
|
44057
|
+
yield* scanProgress.update(`Scanned ${scannedFilesLabel}, analyzing dead code...`);
|
|
44058
|
+
const sequentialDeadCodeTimeout = resolveDeadCodeTimeout({
|
|
44059
|
+
sourceFileCount: totalFileCount,
|
|
44060
|
+
deadCodeConcurrency: scanConcurrency,
|
|
44061
|
+
fullConcurrency: scanConcurrency
|
|
43306
44062
|
});
|
|
43307
|
-
|
|
43308
|
-
|
|
43309
|
-
|
|
44063
|
+
deadCodeCollected = deadCodeFiber !== null ? yield* join(deadCodeFiber) : yield* buildCollectDeadCode({
|
|
44064
|
+
workerTimeoutMs: sequentialDeadCodeTimeout.workerTimeoutMs,
|
|
44065
|
+
phaseTimeoutMs: resolveDeadCodePhaseTimeoutMs(sequentialDeadCodeTimeout.phaseTimeoutMs)
|
|
44066
|
+
});
|
|
44067
|
+
}
|
|
44068
|
+
const deadCodeFailureState = lintFailureState.didFail ? {
|
|
44069
|
+
didFail: false,
|
|
44070
|
+
reason: null
|
|
44071
|
+
} : yield* get$2(deadCodeFailure);
|
|
43310
44072
|
const scanElapsedMilliseconds = Date.now() - scanStartTime;
|
|
43311
44073
|
const scanElapsedSeconds = (scanElapsedMilliseconds / MILLISECONDS_PER_SECOND).toFixed(1);
|
|
43312
44074
|
if (!lintFailureState.didFail) if (deadCodeFailureState.didFail) yield* scanProgress.fail(DEAD_CODE_FAIL_TEXT);
|
|
43313
44075
|
else if (input.suppressScanSummary) yield* scanProgress.stop();
|
|
43314
44076
|
else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
|
|
44077
|
+
const supplyChainResult = yield* join(supplyChainFiber);
|
|
44078
|
+
const supplyChainCollected = supplyChainResult.diagnostics;
|
|
43315
44079
|
yield* reporterService.finalize;
|
|
43316
|
-
const finalDiagnostics = assignFixGroups([
|
|
44080
|
+
const finalDiagnostics = sortDiagnosticsStable(assignFixGroups([
|
|
43317
44081
|
...envCollected,
|
|
43318
44082
|
...supplyChainCollected,
|
|
43319
44083
|
...lintCollected,
|
|
43320
44084
|
...deadCodeCollected
|
|
43321
|
-
]);
|
|
44085
|
+
]));
|
|
43322
44086
|
const githubViewerPermission = yield* join(githubViewerPermissionFiber);
|
|
43323
44087
|
const scoreMetadata = {
|
|
43324
44088
|
...repo !== null ? { repo } : {},
|
|
@@ -43354,9 +44118,14 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
43354
44118
|
lintPartialFailures,
|
|
43355
44119
|
didDeadCodeFail: deadCodeFailureState.didFail,
|
|
43356
44120
|
deadCodeFailureReason: deadCodeFailureState.reason,
|
|
44121
|
+
deadCodeOverlapped: shouldOverlapDeadCode,
|
|
43357
44122
|
scannedFileCount: totalFileCount,
|
|
43358
44123
|
scannedFilePaths,
|
|
43359
|
-
scanElapsedMilliseconds
|
|
44124
|
+
scanElapsedMilliseconds,
|
|
44125
|
+
scanConcurrency,
|
|
44126
|
+
supplyChainOverlapTimedOut: supplyChainResult.timedOut,
|
|
44127
|
+
lintCacheHitFileCount,
|
|
44128
|
+
lintCacheTotalFileCount
|
|
43360
44129
|
};
|
|
43361
44130
|
}).pipe(withSpan("runInspect", { attributes: {
|
|
43362
44131
|
"inspect.directory": input.directory,
|
|
@@ -43364,7 +44133,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
43364
44133
|
"inspect.runDeadCode": input.runDeadCode,
|
|
43365
44134
|
"inspect.isCi": input.isCi,
|
|
43366
44135
|
"inspect.scoreSurface": input.scoreSurface ?? "score"
|
|
43367
|
-
} }));
|
|
44136
|
+
} }), (scanProgram) => flatMap$2(ScanDeadlineMs, (scanDeadlineMs) => scanProgram.pipe(timeout(scanDeadlineMs), catchTag$1("TimeoutError", () => new ReactDoctorError({ reason: new ScanDeadlineExceeded({ detail: `${scanDeadlineMs / MILLISECONDS_PER_SECOND}s elapsed` }) })))));
|
|
43368
44137
|
const parseNodeVersion = (versionString) => {
|
|
43369
44138
|
const [major = 0, minor = 0, patch = 0] = versionString.replace(/^v/, "").trim().split(".").map(Number);
|
|
43370
44139
|
return {
|
|
@@ -44142,7 +44911,7 @@ const makeNoopConsole = () => ({
|
|
|
44142
44911
|
});
|
|
44143
44912
|
//#endregion
|
|
44144
44913
|
//#region src/cli/utils/version.ts
|
|
44145
|
-
const VERSION = "0.5.
|
|
44914
|
+
const VERSION = "0.5.8-dev.f4e8e4b";
|
|
44146
44915
|
//#endregion
|
|
44147
44916
|
//#region src/cli/utils/json-mode.ts
|
|
44148
44917
|
let context = null;
|
|
@@ -44295,7 +45064,8 @@ const buildRunContext = () => {
|
|
|
44295
45064
|
terminalKind: detectTerminalKind(),
|
|
44296
45065
|
jsonMode: isJsonModeActive(),
|
|
44297
45066
|
debug: isDebugFlagEnabled(),
|
|
44298
|
-
invokedVia: detectInvokedVia()
|
|
45067
|
+
invokedVia: detectInvokedVia(),
|
|
45068
|
+
lintBatchOrdering: resolveLintBatchOrdering()
|
|
44299
45069
|
};
|
|
44300
45070
|
};
|
|
44301
45071
|
//#endregion
|
|
@@ -44368,7 +45138,8 @@ const buildSentryScope = (runContext = buildRunContext()) => {
|
|
|
44368
45138
|
jsonMode: runContext.jsonMode,
|
|
44369
45139
|
debug: runContext.debug,
|
|
44370
45140
|
invokedVia: runContext.invokedVia,
|
|
44371
|
-
nodeMajor: runContext.nodeMajor
|
|
45141
|
+
nodeMajor: runContext.nodeMajor,
|
|
45142
|
+
lintBatchOrdering: runContext.lintBatchOrdering
|
|
44372
45143
|
};
|
|
44373
45144
|
const contexts = { run: { ...runContext } };
|
|
44374
45145
|
const projectInfo = getSentryProjectInfo();
|
|
@@ -44504,13 +45275,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
|
|
|
44504
45275
|
* uploads source-map artifacts under, so stack frames symbolicate. Honors the
|
|
44505
45276
|
* standard `SENTRY_RELEASE` override.
|
|
44506
45277
|
*/
|
|
44507
|
-
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.
|
|
45278
|
+
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.8-dev.f4e8e4b`;
|
|
44508
45279
|
/**
|
|
44509
45280
|
* Deployment environment shown in Sentry's environment filter. Defaults to
|
|
44510
45281
|
* `production` for tagged releases and `development` for dev/unbuilt versions,
|
|
44511
45282
|
* overridable via the standard `SENTRY_ENVIRONMENT` env var.
|
|
44512
45283
|
*/
|
|
44513
|
-
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.
|
|
45284
|
+
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.8-dev.f4e8e4b") ? "development" : "production");
|
|
44514
45285
|
/**
|
|
44515
45286
|
* Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
|
|
44516
45287
|
* (set to `0` to disable tracing) and falls back to
|
|
@@ -44713,7 +45484,7 @@ const externalSpanFrom = (sentrySpan) => {
|
|
|
44713
45484
|
* in-memory tracer — identical to the prior default behavior.
|
|
44714
45485
|
*/
|
|
44715
45486
|
const applyObservability = (program, rootSentrySpan) => {
|
|
44716
|
-
if (isOtlpExportConfigured()) return (rootSentrySpan ? program.pipe(provideService(ParentSpan, externalSpanFrom(rootSentrySpan))) : program).pipe(provide(layerOtlp));
|
|
45487
|
+
if (isOtlpExportConfigured()) return (rootSentrySpan ? program.pipe(provideService$2(ParentSpan, externalSpanFrom(rootSentrySpan))) : program).pipe(provide(layerOtlp));
|
|
44717
45488
|
if (rootSentrySpan) return program.pipe(withTracer(makeSentryTracer(rootSentrySpan)));
|
|
44718
45489
|
return program.pipe(provide(layerOtlp));
|
|
44719
45490
|
};
|
|
@@ -48186,6 +48957,9 @@ const buildOutcomeAttributes = (input) => {
|
|
|
48186
48957
|
"migration.largestRuleBucketSites": largestRuleBucket ? largestRuleBucket.siteCount : null,
|
|
48187
48958
|
"migration.largestRuleBucketRule": largestRuleBucket ? largestRuleBucket.ruleKey : null,
|
|
48188
48959
|
scannedFileCount: result.scannedFileCount ?? null,
|
|
48960
|
+
lintCacheHitFiles: result.lintCacheHitFileCount ?? null,
|
|
48961
|
+
lintCacheTotalFiles: result.lintCacheTotalFileCount ?? null,
|
|
48962
|
+
lintCacheHitRatio: result.lintCacheTotalFileCount != null && result.lintCacheTotalFileCount > 0 ? (result.lintCacheHitFileCount ?? 0) / result.lintCacheTotalFileCount : null,
|
|
48189
48963
|
elapsedMs: result.elapsedMilliseconds,
|
|
48190
48964
|
scanPhaseMs: result.scanElapsedMilliseconds ?? null,
|
|
48191
48965
|
score: result.score ? result.score.score : null,
|
|
@@ -48195,7 +48969,10 @@ const buildOutcomeAttributes = (input) => {
|
|
|
48195
48969
|
didLintFail: input.didLintFail ?? null,
|
|
48196
48970
|
lintFailureReasonKind: input.lintFailureReasonKind ?? null,
|
|
48197
48971
|
lintPartialFailureCount: input.lintPartialFailureCount ?? null,
|
|
48198
|
-
|
|
48972
|
+
lintDroppedFileCount: input.lintDroppedFileCount ?? null,
|
|
48973
|
+
didDeadCodeFail: input.didDeadCodeFail ?? null,
|
|
48974
|
+
supplyChainOverlapTimedOut: input.supplyChainOverlapTimedOut ?? null,
|
|
48975
|
+
deadCodeOverlapped: input.deadCodeOverlapped ?? null
|
|
48199
48976
|
};
|
|
48200
48977
|
for (const [category, count] of countByCategory) attributes[`diag.category.${toCategoryKey(category)}`] = count;
|
|
48201
48978
|
if (result.baselineDelta) {
|
|
@@ -48264,6 +49041,32 @@ const recordRunEvent = (rootSpan, input) => {
|
|
|
48264
49041
|
} catch {}
|
|
48265
49042
|
};
|
|
48266
49043
|
//#endregion
|
|
49044
|
+
//#region src/cli/utils/resolve-worker-telemetry.ts
|
|
49045
|
+
/**
|
|
49046
|
+
* Projects the resolved lint worker count into the `(workerCount, parallel)`
|
|
49047
|
+
* telemetry pair. `resolvedWorkerCount` is the count the scan actually fanned
|
|
49048
|
+
* out to (`InspectOutput.scanConcurrency`); `pinnedConcurrency` is the caller's
|
|
49049
|
+
* `inspect({ concurrency })` pin, used as the fallback when no scan resolved a
|
|
49050
|
+
* count (the pre-scan failure path, or a cache entry persisted before the
|
|
49051
|
+
* resolved count was tracked). `parallel` is derived from the count — NOT from
|
|
49052
|
+
* whether a count was pinned — so the common auto path (no pin) still reports
|
|
49053
|
+
* parallelism correctly instead of always reading `false`.
|
|
49054
|
+
*/
|
|
49055
|
+
const resolveWorkerTelemetry = (resolvedWorkerCount, pinnedConcurrency) => {
|
|
49056
|
+
const workerCount = resolvedWorkerCount ?? pinnedConcurrency;
|
|
49057
|
+
return {
|
|
49058
|
+
workerCount,
|
|
49059
|
+
parallel: workerCount !== void 0 && workerCount > 1
|
|
49060
|
+
};
|
|
49061
|
+
};
|
|
49062
|
+
//#endregion
|
|
49063
|
+
//#region src/cli/utils/count-dropped-lint-files.ts
|
|
49064
|
+
const DROPPED_FILES_MESSAGE_PATTERN = /^(\d+) file\(s\) failed to lint and were skipped/;
|
|
49065
|
+
const countDroppedLintFiles = (lintPartialFailures) => lintPartialFailures.reduce((total, message) => {
|
|
49066
|
+
const match = DROPPED_FILES_MESSAGE_PATTERN.exec(message);
|
|
49067
|
+
return match ? total + Number(match[1]) : total;
|
|
49068
|
+
}, 0);
|
|
49069
|
+
//#endregion
|
|
48267
49070
|
//#region src/cli/utils/path-format.ts
|
|
48268
49071
|
const toForwardSlashes = (filePath) => filePath.replaceAll("\\", "/");
|
|
48269
49072
|
//#endregion
|
|
@@ -48880,25 +49683,182 @@ const canAnimateOnboarding = (stream = process.stdout) => {
|
|
|
48880
49683
|
return !isGitHookEnvironment() && !isCiEnvironment();
|
|
48881
49684
|
};
|
|
48882
49685
|
//#endregion
|
|
48883
|
-
//#region src/cli/utils/
|
|
48884
|
-
const
|
|
48885
|
-
const
|
|
49686
|
+
//#region src/cli/utils/now-iso.ts
|
|
49687
|
+
const nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
49688
|
+
const ONBOARDING_EVENT = "onboarding";
|
|
49689
|
+
const CI_PITCH_EVENT = "ci-pitch";
|
|
49690
|
+
const ACTION_UPGRADE_EVENT = "action-upgrade-v2";
|
|
49691
|
+
const SETUP_HINT_EVENT = "setup-hint";
|
|
49692
|
+
const foldLegacyDecisions = (projects, legacy, eventId) => {
|
|
49693
|
+
for (const [hash, record] of Object.entries(legacy ?? {})) {
|
|
49694
|
+
const existing = projects[hash] ?? { rootDirectory: record.rootDirectory ?? "" };
|
|
49695
|
+
projects[hash] = {
|
|
49696
|
+
...existing,
|
|
49697
|
+
events: {
|
|
49698
|
+
...existing.events,
|
|
49699
|
+
[eventId]: {
|
|
49700
|
+
firedAt: record.at ?? nowIso(),
|
|
49701
|
+
version: 1,
|
|
49702
|
+
...record.outcome ? { outcome: record.outcome } : {}
|
|
49703
|
+
}
|
|
49704
|
+
}
|
|
49705
|
+
};
|
|
49706
|
+
}
|
|
49707
|
+
};
|
|
49708
|
+
const migrateCliState = (state) => {
|
|
49709
|
+
if (state.schemaVersion === 2) return state;
|
|
49710
|
+
const projects = {};
|
|
49711
|
+
for (const [hash, record] of Object.entries(state.projects ?? {})) {
|
|
49712
|
+
const carried = {
|
|
49713
|
+
rootDirectory: record.rootDirectory,
|
|
49714
|
+
...record.events ? { events: record.events } : {},
|
|
49715
|
+
...record.migrations ? { migrations: record.migrations } : {}
|
|
49716
|
+
};
|
|
49717
|
+
projects[hash] = record.setupPrompt === false ? {
|
|
49718
|
+
...carried,
|
|
49719
|
+
events: {
|
|
49720
|
+
...carried.events,
|
|
49721
|
+
[SETUP_HINT_EVENT]: {
|
|
49722
|
+
firedAt: nowIso(),
|
|
49723
|
+
version: 1,
|
|
49724
|
+
outcome: "declined"
|
|
49725
|
+
}
|
|
49726
|
+
}
|
|
49727
|
+
} : carried;
|
|
49728
|
+
}
|
|
49729
|
+
foldLegacyDecisions(projects, state.ciPrompts, CI_PITCH_EVENT);
|
|
49730
|
+
foldLegacyDecisions(projects, state.actionUpgrades, ACTION_UPGRADE_EVENT);
|
|
49731
|
+
return {
|
|
49732
|
+
schemaVersion: 2,
|
|
49733
|
+
global: typeof state.onboardedAt === "string" ? { events: { [ONBOARDING_EVENT]: {
|
|
49734
|
+
firedAt: state.onboardedAt,
|
|
49735
|
+
version: 1
|
|
49736
|
+
} } } : {},
|
|
49737
|
+
projects
|
|
49738
|
+
};
|
|
49739
|
+
};
|
|
49740
|
+
const resolveConfigDir = (options) => options.cwd ?? (process.env["REACT_DOCTOR_CONFIG_DIR"] || void 0);
|
|
49741
|
+
const openStore = (options = {}) => new Conf({
|
|
48886
49742
|
projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
|
|
48887
|
-
cwd: options
|
|
49743
|
+
cwd: resolveConfigDir(options)
|
|
48888
49744
|
});
|
|
48889
|
-
const
|
|
49745
|
+
const openMigratedStore = (options) => {
|
|
49746
|
+
const store = openStore(options);
|
|
49747
|
+
if (store.store.schemaVersion !== 2) store.store = migrateCliState(store.store);
|
|
49748
|
+
return store;
|
|
49749
|
+
};
|
|
49750
|
+
const readCliState = (select, fallback, options = {}) => {
|
|
48890
49751
|
try {
|
|
48891
|
-
return
|
|
49752
|
+
return select(openMigratedStore(options).store);
|
|
48892
49753
|
} catch {
|
|
49754
|
+
return fallback;
|
|
49755
|
+
}
|
|
49756
|
+
};
|
|
49757
|
+
const updateCliState = (update, options = {}) => {
|
|
49758
|
+
try {
|
|
49759
|
+
const store = openMigratedStore(options);
|
|
49760
|
+
store.store = update(store.store);
|
|
48893
49761
|
return true;
|
|
49762
|
+
} catch {
|
|
49763
|
+
return false;
|
|
49764
|
+
}
|
|
49765
|
+
};
|
|
49766
|
+
//#endregion
|
|
49767
|
+
//#region src/cli/utils/hash-project-root.ts
|
|
49768
|
+
const hashProjectRoot = (projectRoot) => createHash("sha256").update(Path.resolve(projectRoot)).digest("hex");
|
|
49769
|
+
//#endregion
|
|
49770
|
+
//#region src/cli/utils/cli-lifecycle.ts
|
|
49771
|
+
const versionOf = (item) => item.version ?? 1;
|
|
49772
|
+
const selectScope = (state, scoped, projectRoot) => scoped.scope === "global" ? state.global : projectRoot === void 0 ? void 0 : state.projects?.[hashProjectRoot(projectRoot)];
|
|
49773
|
+
const updateScope = (state, scoped, projectRoot, updateScopeState) => {
|
|
49774
|
+
if (scoped.scope === "global") return {
|
|
49775
|
+
...state,
|
|
49776
|
+
global: updateScopeState(state.global ?? {})
|
|
49777
|
+
};
|
|
49778
|
+
if (projectRoot === void 0) return state;
|
|
49779
|
+
const projectKey = hashProjectRoot(projectRoot);
|
|
49780
|
+
const base = state.projects?.[projectKey] ?? { rootDirectory: Path.resolve(projectRoot) };
|
|
49781
|
+
return {
|
|
49782
|
+
...state,
|
|
49783
|
+
projects: {
|
|
49784
|
+
...state.projects,
|
|
49785
|
+
[projectKey]: {
|
|
49786
|
+
...base,
|
|
49787
|
+
...updateScopeState(base)
|
|
49788
|
+
}
|
|
49789
|
+
}
|
|
49790
|
+
};
|
|
49791
|
+
};
|
|
49792
|
+
const isGatePending = (gate, target = {}, options = {}) => {
|
|
49793
|
+
if (gate.scope === "project" && target.projectRoot === void 0) return false;
|
|
49794
|
+
return readCliState((state) => {
|
|
49795
|
+
const record = selectScope(state, gate, target.projectRoot)?.events?.[gate.id];
|
|
49796
|
+
return !record || record.version < versionOf(gate);
|
|
49797
|
+
}, gate.fireWhenUnknown ?? false, options);
|
|
49798
|
+
};
|
|
49799
|
+
const recordGate = (gate, target = {}, options = {}) => updateCliState((state) => updateScope(state, gate, target.projectRoot, (scope) => ({
|
|
49800
|
+
...scope,
|
|
49801
|
+
events: {
|
|
49802
|
+
...scope.events,
|
|
49803
|
+
[gate.id]: {
|
|
49804
|
+
firedAt: nowIso(),
|
|
49805
|
+
version: versionOf(gate),
|
|
49806
|
+
...target.outcome ? { outcome: target.outcome } : {}
|
|
49807
|
+
}
|
|
49808
|
+
}
|
|
49809
|
+
})), options);
|
|
49810
|
+
const isMigrationPending = (migration, target = {}, options = {}) => {
|
|
49811
|
+
if (migration.scope === "project" && target.projectRoot === void 0) return false;
|
|
49812
|
+
return readCliState((state) => {
|
|
49813
|
+
const record = selectScope(state, migration, target.projectRoot)?.migrations?.[migration.id];
|
|
49814
|
+
return !record || record.version < versionOf(migration);
|
|
49815
|
+
}, false, options);
|
|
49816
|
+
};
|
|
49817
|
+
const recordMigration = (migration, projectRoot, record, options) => updateCliState((state) => updateScope(state, migration, projectRoot, (scope) => ({
|
|
49818
|
+
...scope,
|
|
49819
|
+
migrations: {
|
|
49820
|
+
...scope.migrations,
|
|
49821
|
+
[migration.id]: record
|
|
49822
|
+
}
|
|
49823
|
+
})), options);
|
|
49824
|
+
const runMigrations = async (migrations, target = {}, options = {}) => {
|
|
49825
|
+
const results = [];
|
|
49826
|
+
for (const migration of migrations) {
|
|
49827
|
+
if (!isMigrationPending(migration, target, options)) {
|
|
49828
|
+
results.push({
|
|
49829
|
+
id: migration.id,
|
|
49830
|
+
ran: false,
|
|
49831
|
+
applied: true
|
|
49832
|
+
});
|
|
49833
|
+
continue;
|
|
49834
|
+
}
|
|
49835
|
+
let applied = false;
|
|
49836
|
+
try {
|
|
49837
|
+
applied = await migration.run({ projectRoot: target.projectRoot });
|
|
49838
|
+
} catch {
|
|
49839
|
+
applied = false;
|
|
49840
|
+
}
|
|
49841
|
+
if (applied) recordMigration(migration, target.projectRoot, {
|
|
49842
|
+
ranAt: nowIso(),
|
|
49843
|
+
version: versionOf(migration)
|
|
49844
|
+
}, options);
|
|
49845
|
+
results.push({
|
|
49846
|
+
id: migration.id,
|
|
49847
|
+
ran: true,
|
|
49848
|
+
applied
|
|
49849
|
+
});
|
|
48894
49850
|
}
|
|
49851
|
+
return results;
|
|
49852
|
+
};
|
|
49853
|
+
//#endregion
|
|
49854
|
+
//#region src/cli/utils/onboarding-state.ts
|
|
49855
|
+
const ONBOARDING_GATE = {
|
|
49856
|
+
id: ONBOARDING_EVENT,
|
|
49857
|
+
scope: "global"
|
|
48895
49858
|
};
|
|
49859
|
+
const hasCompletedOnboarding = (options = {}) => !isGatePending(ONBOARDING_GATE, {}, options);
|
|
48896
49860
|
const markOnboardingComplete = (options = {}) => {
|
|
48897
|
-
|
|
48898
|
-
const store = getOnboardingStore(options);
|
|
48899
|
-
if (typeof store.get(ONBOARDED_AT_KEY) === "string") return;
|
|
48900
|
-
store.set(ONBOARDED_AT_KEY, (/* @__PURE__ */ new Date()).toISOString());
|
|
48901
|
-
} catch {}
|
|
49861
|
+
if (isGatePending(ONBOARDING_GATE, {}, options)) recordGate(ONBOARDING_GATE, {}, options);
|
|
48902
49862
|
};
|
|
48903
49863
|
//#endregion
|
|
48904
49864
|
//#region src/cli/utils/render-project-detection.ts
|
|
@@ -49473,12 +50433,12 @@ const resolveCacheFilePath = (projectDirectory) => {
|
|
|
49473
50433
|
const readPersistedCache = (cacheFilePath) => {
|
|
49474
50434
|
try {
|
|
49475
50435
|
const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
|
|
49476
|
-
if (!isRecord(parsed) || parsed.version !==
|
|
49477
|
-
version:
|
|
50436
|
+
if (!isRecord(parsed) || parsed.version !== 2) return {
|
|
50437
|
+
version: 2,
|
|
49478
50438
|
entries: []
|
|
49479
50439
|
};
|
|
49480
50440
|
if (!Array.isArray(parsed.entries)) return {
|
|
49481
|
-
version:
|
|
50441
|
+
version: 2,
|
|
49482
50442
|
entries: []
|
|
49483
50443
|
};
|
|
49484
50444
|
const entries = [];
|
|
@@ -49488,12 +50448,12 @@ const readPersistedCache = (cacheFilePath) => {
|
|
|
49488
50448
|
entries.push(entry);
|
|
49489
50449
|
}
|
|
49490
50450
|
return {
|
|
49491
|
-
version:
|
|
50451
|
+
version: 2,
|
|
49492
50452
|
entries
|
|
49493
50453
|
};
|
|
49494
50454
|
} catch {
|
|
49495
50455
|
return {
|
|
49496
|
-
version:
|
|
50456
|
+
version: 2,
|
|
49497
50457
|
entries: []
|
|
49498
50458
|
};
|
|
49499
50459
|
}
|
|
@@ -49546,7 +50506,7 @@ const buildScanResultCacheKey = (input) => {
|
|
|
49546
50506
|
if (headSha === null) return null;
|
|
49547
50507
|
if (stringifyStableJson(input.userConfig) === null) return null;
|
|
49548
50508
|
const cacheKeyJson = stringifyStableJson({
|
|
49549
|
-
schemaVersion:
|
|
50509
|
+
schemaVersion: 2,
|
|
49550
50510
|
projectIdentity: resolveProjectIdentity(input.projectDirectory),
|
|
49551
50511
|
headSha,
|
|
49552
50512
|
reactDoctorVersion: input.version,
|
|
@@ -49566,6 +50526,7 @@ const buildScanResultCacheKey = (input) => {
|
|
|
49566
50526
|
adoptExistingLintConfig: input.options.adoptExistingLintConfig,
|
|
49567
50527
|
ignoredTags: [...input.options.ignoredTags].sort(),
|
|
49568
50528
|
concurrency: input.options.concurrency,
|
|
50529
|
+
lintBatchOrdering: resolveLintBatchOrdering(),
|
|
49569
50530
|
baselineRef: input.options.baseline?.ref,
|
|
49570
50531
|
changedLineRanges: input.options.changedLineRanges ?? void 0,
|
|
49571
50532
|
noScore: input.options.noScore,
|
|
@@ -49583,7 +50544,7 @@ const createScanResultCache = (projectDirectory) => {
|
|
|
49583
50544
|
for (const entry of persistedCache.entries) entries.set(entry.key, entry);
|
|
49584
50545
|
const persist = () => {
|
|
49585
50546
|
writePersistedCache(cacheFilePath, {
|
|
49586
|
-
version:
|
|
50547
|
+
version: 2,
|
|
49587
50548
|
entries: [...entries.values()].sort((firstEntry, secondEntry) => secondEntry.createdAtMs - firstEntry.createdAtMs).slice(0, 20)
|
|
49588
50549
|
});
|
|
49589
50550
|
};
|
|
@@ -49599,7 +50560,7 @@ const createScanResultCache = (projectDirectory) => {
|
|
|
49599
50560
|
}
|
|
49600
50561
|
};
|
|
49601
50562
|
};
|
|
49602
|
-
const shouldStoreScanPayload = (payload) => !payload.didLintFail && !payload.didDeadCodeFail && payload.lintPartialFailures.length === 0;
|
|
50563
|
+
const shouldStoreScanPayload = (payload) => !payload.didLintFail && !payload.didDeadCodeFail && payload.lintPartialFailures.length === 0 && !payload.supplyChainOverlapTimedOut;
|
|
49603
50564
|
//#endregion
|
|
49604
50565
|
//#region src/inspect.ts
|
|
49605
50566
|
const silentConsole = makeNoopConsole();
|
|
@@ -49663,21 +50624,24 @@ const deriveScope = (options) => {
|
|
|
49663
50624
|
if (options.changedLineRanges !== null) return "lines";
|
|
49664
50625
|
return options.includePaths.length > 0 ? "files" : "full";
|
|
49665
50626
|
};
|
|
49666
|
-
const buildRunEventConfig = (options, userConfig, hasCustomConfig) =>
|
|
49667
|
-
|
|
49668
|
-
|
|
49669
|
-
|
|
49670
|
-
|
|
49671
|
-
|
|
49672
|
-
|
|
49673
|
-
|
|
49674
|
-
|
|
49675
|
-
|
|
49676
|
-
|
|
49677
|
-
|
|
49678
|
-
|
|
49679
|
-
|
|
49680
|
-
|
|
50627
|
+
const buildRunEventConfig = (options, userConfig, hasCustomConfig, resolvedWorkerCount) => {
|
|
50628
|
+
const { workerCount, parallel } = resolveWorkerTelemetry(resolvedWorkerCount, options.concurrency);
|
|
50629
|
+
return {
|
|
50630
|
+
scope: deriveScope(options),
|
|
50631
|
+
parallel,
|
|
50632
|
+
workerCount,
|
|
50633
|
+
lint: options.lint,
|
|
50634
|
+
deadCode: options.deadCode,
|
|
50635
|
+
scoreOnly: options.scoreOnly,
|
|
50636
|
+
noScore: options.noScore,
|
|
50637
|
+
respectInlineDisables: options.respectInlineDisables,
|
|
50638
|
+
showWarnings: options.warnings,
|
|
50639
|
+
usedOutputDir: options.outputDirectory !== null,
|
|
50640
|
+
ignoredTagCount: options.ignoredTags.size,
|
|
50641
|
+
hasCustomConfig,
|
|
50642
|
+
userConfig
|
|
50643
|
+
};
|
|
50644
|
+
};
|
|
49681
50645
|
const inspect = async (directory, inputOptions = {}) => {
|
|
49682
50646
|
const startTime = performance$1.now();
|
|
49683
50647
|
const isConcurrentScan = inputOptions.concurrentScan === true;
|
|
@@ -49769,7 +50733,7 @@ const runBaselineComparison = async (params) => {
|
|
|
49769
50733
|
resolveLocalGithubViewerPermission: false,
|
|
49770
50734
|
suppressScanSummary: true,
|
|
49771
50735
|
supplyChainManifestChanged: params.options.supplyChainManifestChanged
|
|
49772
|
-
}, {}).pipe(provide(baseLayers), provideService(Console, silentConsole))));
|
|
50736
|
+
}, {}).pipe(provide(baseLayers), provideService$2(Console, silentConsole))));
|
|
49773
50737
|
if (baseOutput.didLintFail) return null;
|
|
49774
50738
|
const delta = computeDiagnosticDelta({
|
|
49775
50739
|
headDiagnostics: params.headDiagnostics,
|
|
@@ -49854,7 +50818,8 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
|
|
|
49854
50818
|
runId: getRunId(),
|
|
49855
50819
|
resolveLocalGithubViewerPermission: !options.noScore,
|
|
49856
50820
|
suppressScanSummary: options.suppressRendering,
|
|
49857
|
-
supplyChainManifestChanged: options.supplyChainManifestChanged
|
|
50821
|
+
supplyChainManifestChanged: options.supplyChainManifestChanged,
|
|
50822
|
+
concurrentScan: options.concurrentScan
|
|
49858
50823
|
}, { beforeLint: (projectInfo, lintIncludePaths) => gen(function* () {
|
|
49859
50824
|
recordSentryProjectContext(projectInfo, rootSentrySpan, { concurrentScan: options.concurrentScan });
|
|
49860
50825
|
recordCount(METRIC.projectDetected, 1);
|
|
@@ -49868,7 +50833,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
|
|
|
49868
50833
|
lintSourceFileCount
|
|
49869
50834
|
});
|
|
49870
50835
|
}) });
|
|
49871
|
-
const output = await runPromise(restoreLegacyThrow(applyObservability(options.silent ? program.pipe(provide(layers), provideService(Console, silentConsole)) : program.pipe(provide(layers)), rootSentrySpan)));
|
|
50836
|
+
const output = await runPromise(restoreLegacyThrow(applyObservability(options.silent ? program.pipe(provide(layers), provideService$2(Console, silentConsole)) : program.pipe(provide(layers)), rootSentrySpan)));
|
|
49872
50837
|
const didLintFail = lintBindingMissing || output.didLintFail;
|
|
49873
50838
|
const lintFailureReason = lintBindingMissing ? `oxlint native binding not found for Node ${process.version}; expected one matching ${OXLINT_NODE_REQUIREMENT}` : output.lintFailureReason;
|
|
49874
50839
|
if (!options.scoreOnly && !lintBindingMissing && output.didLintFail && lintFailureReason !== null) if (output.lintFailureReasonKind === "native-binding-missing") runConsole(log(highlighter.gray(` Upgrade to Node ${OXLINT_NODE_REQUIREMENT} or run: npx -p oxlint@latest react-doctor@latest`)));
|
|
@@ -49905,12 +50870,15 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
|
|
|
49905
50870
|
lintPartialFailures: output.lintPartialFailures,
|
|
49906
50871
|
didDeadCodeFail: output.didDeadCodeFail,
|
|
49907
50872
|
deadCodeFailureReason: output.deadCodeFailureReason,
|
|
50873
|
+
deadCodeOverlapped: output.deadCodeOverlapped,
|
|
49908
50874
|
directory: output.resolvedDirectory,
|
|
49909
50875
|
scannedFileCount: output.scannedFileCount,
|
|
49910
50876
|
scannedFilePaths: output.scannedFilePaths,
|
|
49911
50877
|
scanElapsedMilliseconds: output.scanElapsedMilliseconds,
|
|
50878
|
+
scanConcurrency: output.scanConcurrency,
|
|
49912
50879
|
baselineDelta,
|
|
49913
|
-
lintFailureReasonKind: lintBindingMissing ? "native-binding-missing" : output.lintFailureReasonKind
|
|
50880
|
+
lintFailureReasonKind: lintBindingMissing ? "native-binding-missing" : output.lintFailureReasonKind,
|
|
50881
|
+
supplyChainOverlapTimedOut: output.supplyChainOverlapTimedOut
|
|
49914
50882
|
};
|
|
49915
50883
|
if (cacheKey !== null && scanResultCache !== null && shouldStoreScanPayload(payload)) scanResultCache.store(cacheKey, payload);
|
|
49916
50884
|
const result = await renderAndRecordScan({
|
|
@@ -49921,12 +50889,14 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
|
|
|
49921
50889
|
startTime,
|
|
49922
50890
|
rootSentrySpan,
|
|
49923
50891
|
scanMode: baselineDelta ? "baseline" : isDiffMode ? "diff" : "full",
|
|
49924
|
-
baselineDegraded
|
|
50892
|
+
baselineDegraded,
|
|
50893
|
+
lintCacheHitFileCount: output.lintCacheHitFileCount,
|
|
50894
|
+
lintCacheTotalFileCount: output.lintCacheTotalFileCount
|
|
49925
50895
|
});
|
|
49926
50896
|
recordOnboardingCompletion(options);
|
|
49927
50897
|
return result;
|
|
49928
50898
|
};
|
|
49929
|
-
const runMaybeSilent = (effect, silent) => silent ? effect.pipe(provideService(Console, silentConsole)) : effect;
|
|
50899
|
+
const runMaybeSilent = (effect, silent) => silent ? effect.pipe(provideService$2(Console, silentConsole)) : effect;
|
|
49930
50900
|
const renderCachedProjectDetection = async (input) => {
|
|
49931
50901
|
if (input.options.scoreOnly || input.options.suppressRendering) return;
|
|
49932
50902
|
await runPromise(runMaybeSilent(printProjectDetection({
|
|
@@ -49954,14 +50924,17 @@ const renderAndRecordScan = async (input) => {
|
|
|
49954
50924
|
scannedFileCount: input.payload.scannedFileCount,
|
|
49955
50925
|
scannedFilePaths: input.payload.scannedFilePaths,
|
|
49956
50926
|
scanElapsedMilliseconds: input.payload.scanElapsedMilliseconds,
|
|
50927
|
+
lintCacheHitFileCount: input.lintCacheHitFileCount ?? null,
|
|
50928
|
+
lintCacheTotalFileCount: input.lintCacheTotalFileCount ?? null,
|
|
49957
50929
|
baselineDelta: input.payload.baselineDelta
|
|
49958
50930
|
}), input.options.silent));
|
|
50931
|
+
const { workerCount: resolvedWorkerCount, parallel } = resolveWorkerTelemetry(input.payload.scanConcurrency, input.options.concurrency);
|
|
49959
50932
|
recordScanMetrics({
|
|
49960
50933
|
result,
|
|
49961
50934
|
mode: input.scanMode,
|
|
49962
50935
|
baselineDegraded: input.baselineDegraded,
|
|
49963
|
-
parallel
|
|
49964
|
-
workerCount:
|
|
50936
|
+
parallel,
|
|
50937
|
+
workerCount: resolvedWorkerCount,
|
|
49965
50938
|
lint: input.options.lint,
|
|
49966
50939
|
deadCode: input.options.deadCode,
|
|
49967
50940
|
scoreOnly: input.options.scoreOnly,
|
|
@@ -49971,19 +50944,22 @@ const renderAndRecordScan = async (input) => {
|
|
|
49971
50944
|
didDeadCodeFail: input.payload.didDeadCodeFail
|
|
49972
50945
|
});
|
|
49973
50946
|
recordRunEvent(input.rootSentrySpan, {
|
|
49974
|
-
...buildRunEventConfig(input.options, input.userConfig, input.hasCustomConfig),
|
|
50947
|
+
...buildRunEventConfig(input.options, input.userConfig, input.hasCustomConfig, resolvedWorkerCount),
|
|
49975
50948
|
result,
|
|
49976
50949
|
mode: input.scanMode,
|
|
49977
50950
|
gateExempt: input.baselineDegraded,
|
|
49978
50951
|
didLintFail: input.payload.didLintFail,
|
|
49979
50952
|
lintFailureReasonKind: input.payload.lintFailureReasonKind,
|
|
49980
50953
|
lintPartialFailureCount: input.payload.lintPartialFailures.length,
|
|
49981
|
-
|
|
50954
|
+
lintDroppedFileCount: countDroppedLintFiles(input.payload.lintPartialFailures),
|
|
50955
|
+
didDeadCodeFail: input.payload.didDeadCodeFail,
|
|
50956
|
+
supplyChainOverlapTimedOut: input.payload.supplyChainOverlapTimedOut,
|
|
50957
|
+
deadCodeOverlapped: input.payload.deadCodeOverlapped
|
|
49982
50958
|
});
|
|
49983
50959
|
return result;
|
|
49984
50960
|
};
|
|
49985
50961
|
const finalizeAndRender = (input) => gen(function* () {
|
|
49986
|
-
const { options, elapsedMilliseconds, diagnostics, score, project, userConfig, didLintFail, lintFailureReason, lintPartialFailures, didDeadCodeFail, deadCodeFailureReason, directory, scannedFileCount, scannedFilePaths, scanElapsedMilliseconds, baselineDelta } = input;
|
|
50962
|
+
const { options, elapsedMilliseconds, diagnostics, score, project, userConfig, didLintFail, lintFailureReason, lintPartialFailures, didDeadCodeFail, deadCodeFailureReason, directory, scannedFileCount, scannedFilePaths, scanElapsedMilliseconds, lintCacheHitFileCount, lintCacheTotalFileCount, baselineDelta } = input;
|
|
49987
50963
|
const { skippedChecks, skippedCheckReasons } = buildSkippedChecks({
|
|
49988
50964
|
didLintFail,
|
|
49989
50965
|
lintFailureReason,
|
|
@@ -50003,6 +50979,10 @@ const finalizeAndRender = (input) => gen(function* () {
|
|
|
50003
50979
|
scannedFileCount,
|
|
50004
50980
|
scannedFilePaths,
|
|
50005
50981
|
scanElapsedMilliseconds,
|
|
50982
|
+
...lintCacheTotalFileCount !== null ? {
|
|
50983
|
+
lintCacheHitFileCount,
|
|
50984
|
+
lintCacheTotalFileCount
|
|
50985
|
+
} : {},
|
|
50006
50986
|
...baselineDelta ? { baselineDelta } : {}
|
|
50007
50987
|
});
|
|
50008
50988
|
if (options.suppressRendering) return buildResult();
|
|
@@ -50542,54 +51522,27 @@ const upgradeReactDoctorWorkflowInPlace = (projectRoot) => {
|
|
|
50542
51522
|
}
|
|
50543
51523
|
};
|
|
50544
51524
|
//#endregion
|
|
50545
|
-
//#region src/cli/utils/hash-project-root.ts
|
|
50546
|
-
const hashProjectRoot = (projectRoot) => createHash("sha256").update(Path.resolve(projectRoot)).digest("hex");
|
|
50547
|
-
//#endregion
|
|
50548
|
-
//#region src/cli/utils/project-decision-store.ts
|
|
50549
|
-
const createProjectDecisionStore = (storeKey) => {
|
|
50550
|
-
const getStore = (options = {}) => new Conf({
|
|
50551
|
-
projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
|
|
50552
|
-
cwd: options.cwd
|
|
50553
|
-
});
|
|
50554
|
-
return {
|
|
50555
|
-
getConfigPath: (options = {}) => getStore(options).path,
|
|
50556
|
-
hasHandled: (projectRoot, options = {}) => {
|
|
50557
|
-
try {
|
|
50558
|
-
return Boolean(getStore(options).get(storeKey, {})[hashProjectRoot(projectRoot)]);
|
|
50559
|
-
} catch {
|
|
50560
|
-
return true;
|
|
50561
|
-
}
|
|
50562
|
-
},
|
|
50563
|
-
record: (projectRoot, outcome, options = {}) => {
|
|
50564
|
-
try {
|
|
50565
|
-
const store = getStore(options);
|
|
50566
|
-
store.set(storeKey, {
|
|
50567
|
-
...store.get(storeKey, {}),
|
|
50568
|
-
[hashProjectRoot(projectRoot)]: {
|
|
50569
|
-
rootDirectory: Path.resolve(projectRoot),
|
|
50570
|
-
outcome,
|
|
50571
|
-
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
50572
|
-
}
|
|
50573
|
-
});
|
|
50574
|
-
return true;
|
|
50575
|
-
} catch {
|
|
50576
|
-
return false;
|
|
50577
|
-
}
|
|
50578
|
-
}
|
|
50579
|
-
};
|
|
50580
|
-
};
|
|
50581
|
-
//#endregion
|
|
50582
51525
|
//#region src/cli/utils/action-upgrade-prompt.ts
|
|
50583
|
-
const
|
|
50584
|
-
|
|
50585
|
-
|
|
50586
|
-
|
|
51526
|
+
const ACTION_UPGRADE_GATE = {
|
|
51527
|
+
id: ACTION_UPGRADE_EVENT,
|
|
51528
|
+
scope: "project"
|
|
51529
|
+
};
|
|
51530
|
+
const hasHandledActionUpgrade = (projectRoot, options = {}) => !isGatePending(ACTION_UPGRADE_GATE, { projectRoot }, options);
|
|
51531
|
+
const recordActionUpgradeDecision = (projectRoot, outcome, options = {}) => recordGate(ACTION_UPGRADE_GATE, {
|
|
51532
|
+
projectRoot,
|
|
51533
|
+
outcome
|
|
51534
|
+
}, options);
|
|
50587
51535
|
//#endregion
|
|
50588
51536
|
//#region src/cli/utils/ci-prompt-decision.ts
|
|
50589
|
-
const
|
|
50590
|
-
|
|
50591
|
-
|
|
50592
|
-
|
|
51537
|
+
const CI_PITCH_GATE = {
|
|
51538
|
+
id: CI_PITCH_EVENT,
|
|
51539
|
+
scope: "project"
|
|
51540
|
+
};
|
|
51541
|
+
const hasHandledCiPrompt = (projectRoot, options = {}) => !isGatePending(CI_PITCH_GATE, { projectRoot }, options);
|
|
51542
|
+
const recordCiPromptDecision = (projectRoot, outcome, options = {}) => recordGate(CI_PITCH_GATE, {
|
|
51543
|
+
projectRoot,
|
|
51544
|
+
outcome
|
|
51545
|
+
}, options);
|
|
50593
51546
|
//#endregion
|
|
50594
51547
|
//#region src/cli/utils/open-url.ts
|
|
50595
51548
|
const resolveOpenCommand = (url) => {
|
|
@@ -50755,39 +51708,80 @@ const DEFAULT_PR_TITLE = "Add React Doctor to GitHub Actions";
|
|
|
50755
51708
|
const DEFAULT_PR_BODY = `Adds a [React Doctor](https://www.react.doctor) scan to every pull request and every push to the default branch. The workflow file is documented inline.
|
|
50756
51709
|
|
|
50757
51710
|
Docs: https://www.react.doctor/ci`;
|
|
50758
|
-
const findUniqueBranchName = async (cwd) => {
|
|
50759
|
-
if (!(await
|
|
51711
|
+
const findUniqueBranchName = async (cwd, run) => {
|
|
51712
|
+
if (!(await run("git", [
|
|
50760
51713
|
"rev-parse",
|
|
50761
51714
|
"--verify",
|
|
50762
51715
|
NEW_BRANCH_PREFIX
|
|
50763
51716
|
], cwd)).success) return NEW_BRANCH_PREFIX;
|
|
50764
51717
|
return `${NEW_BRANCH_PREFIX}-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace(/[-:T]/g, "")}`;
|
|
50765
51718
|
};
|
|
51719
|
+
const findExistingSetupPullRequest = async (cwd, run) => {
|
|
51720
|
+
const prList = await run("gh", [
|
|
51721
|
+
"pr",
|
|
51722
|
+
"list",
|
|
51723
|
+
"--state",
|
|
51724
|
+
"open",
|
|
51725
|
+
"--json",
|
|
51726
|
+
"headRefName,url",
|
|
51727
|
+
"--limit",
|
|
51728
|
+
String(100)
|
|
51729
|
+
], cwd);
|
|
51730
|
+
if (!prList.success) return null;
|
|
51731
|
+
try {
|
|
51732
|
+
return JSON.parse(prList.stdout).find((pullRequest) => (pullRequest.headRefName ?? "").startsWith(NEW_BRANCH_PREFIX)) ?? null;
|
|
51733
|
+
} catch {
|
|
51734
|
+
return null;
|
|
51735
|
+
}
|
|
51736
|
+
};
|
|
51737
|
+
const hasUnrelatedTrackedChanges = async (cwd, workflowRelative, run) => {
|
|
51738
|
+
const statusProbe = await run("git", [
|
|
51739
|
+
"status",
|
|
51740
|
+
"--porcelain",
|
|
51741
|
+
"--",
|
|
51742
|
+
".",
|
|
51743
|
+
`:!${workflowRelative}`
|
|
51744
|
+
], cwd);
|
|
51745
|
+
if (!statusProbe.success) return true;
|
|
51746
|
+
return statusProbe.stdout.split(/\r?\n/).filter(Boolean).some((statusLine) => !statusLine.startsWith("??"));
|
|
51747
|
+
};
|
|
50766
51748
|
const openWorkflowPullRequest = async (params) => {
|
|
50767
51749
|
const workflowPath = Path.resolve(params.workflowPath);
|
|
50768
51750
|
const commitMessage = params.commitMessage ?? DEFAULT_COMMIT_MESSAGE;
|
|
50769
51751
|
const prTitle = params.prTitle ?? DEFAULT_PR_TITLE;
|
|
50770
51752
|
const prBody = params.prBody ?? DEFAULT_PR_BODY;
|
|
50771
|
-
const
|
|
51753
|
+
const run = params.run ?? runCommand;
|
|
51754
|
+
const checkCommandAvailable = params.checkCommandAvailable ?? isCommandAvailable;
|
|
51755
|
+
const repoRootProbe = await run("git", ["rev-parse", "--show-toplevel"], Path.dirname(workflowPath));
|
|
50772
51756
|
if (!repoRootProbe.success) return {
|
|
50773
51757
|
status: "not-attempted",
|
|
50774
51758
|
reason: "not-a-git-repo"
|
|
50775
51759
|
};
|
|
50776
51760
|
const cwd = repoRootProbe.stdout;
|
|
50777
|
-
|
|
51761
|
+
const workflowRelative = toForwardSlashes(Path.relative(cwd, workflowPath));
|
|
51762
|
+
if (!checkCommandAvailable("gh")) return {
|
|
50778
51763
|
status: "not-attempted",
|
|
50779
51764
|
reason: "gh-not-installed"
|
|
50780
51765
|
};
|
|
50781
|
-
if (!(await
|
|
51766
|
+
if (!(await run("gh", ["auth", "status"], cwd)).success) return {
|
|
50782
51767
|
status: "not-attempted",
|
|
50783
51768
|
reason: "gh-not-authenticated"
|
|
50784
51769
|
};
|
|
50785
|
-
const
|
|
51770
|
+
const existingSetupPullRequest = await findExistingSetupPullRequest(cwd, run);
|
|
51771
|
+
if (existingSetupPullRequest) return {
|
|
51772
|
+
status: "pr-exists",
|
|
51773
|
+
url: existingSetupPullRequest.url ?? ""
|
|
51774
|
+
};
|
|
51775
|
+
if (await hasUnrelatedTrackedChanges(cwd, workflowRelative, run)) return {
|
|
51776
|
+
status: "not-attempted",
|
|
51777
|
+
reason: "working-tree-dirty"
|
|
51778
|
+
};
|
|
51779
|
+
const defaultBranch = params.baseBranch ?? await detectDefaultBranch(cwd, run);
|
|
50786
51780
|
if (!defaultBranch) return {
|
|
50787
51781
|
status: "not-attempted",
|
|
50788
51782
|
reason: "no-default-branch"
|
|
50789
51783
|
};
|
|
50790
|
-
const previousBranchProbe = await
|
|
51784
|
+
const previousBranchProbe = await run("git", [
|
|
50791
51785
|
"rev-parse",
|
|
50792
51786
|
"--abbrev-ref",
|
|
50793
51787
|
"HEAD"
|
|
@@ -50797,13 +51791,13 @@ const openWorkflowPullRequest = async (params) => {
|
|
|
50797
51791
|
reason: "detached-head"
|
|
50798
51792
|
};
|
|
50799
51793
|
const previousBranch = previousBranchProbe.stdout;
|
|
50800
|
-
await
|
|
51794
|
+
await run("git", [
|
|
50801
51795
|
"fetch",
|
|
50802
51796
|
"origin",
|
|
50803
51797
|
defaultBranch
|
|
50804
51798
|
], cwd);
|
|
50805
|
-
const newBranch = await findUniqueBranchName(cwd);
|
|
50806
|
-
if (!(await
|
|
51799
|
+
const newBranch = await findUniqueBranchName(cwd, run);
|
|
51800
|
+
if (!(await run("git", [
|
|
50807
51801
|
"checkout",
|
|
50808
51802
|
"-b",
|
|
50809
51803
|
newBranch,
|
|
@@ -50813,17 +51807,17 @@ const openWorkflowPullRequest = async (params) => {
|
|
|
50813
51807
|
reason: "checkout-failed"
|
|
50814
51808
|
};
|
|
50815
51809
|
const restoreToPreviousBranch = async (deleteNewBranch) => {
|
|
50816
|
-
await
|
|
50817
|
-
if (deleteNewBranch) await
|
|
51810
|
+
await run("git", ["checkout", previousBranch], cwd);
|
|
51811
|
+
if (deleteNewBranch) await run("git", [
|
|
50818
51812
|
"branch",
|
|
50819
51813
|
"-D",
|
|
50820
51814
|
newBranch
|
|
50821
51815
|
], cwd);
|
|
50822
51816
|
};
|
|
50823
|
-
if (!(await
|
|
51817
|
+
if (!(await run("git", [
|
|
50824
51818
|
"add",
|
|
50825
51819
|
"--",
|
|
50826
|
-
|
|
51820
|
+
workflowRelative
|
|
50827
51821
|
], cwd)).success) {
|
|
50828
51822
|
await restoreToPreviousBranch(true);
|
|
50829
51823
|
return {
|
|
@@ -50831,7 +51825,7 @@ const openWorkflowPullRequest = async (params) => {
|
|
|
50831
51825
|
reason: "git-add-failed"
|
|
50832
51826
|
};
|
|
50833
51827
|
}
|
|
50834
|
-
if (!(await
|
|
51828
|
+
if (!(await run("git", [
|
|
50835
51829
|
"commit",
|
|
50836
51830
|
"-m",
|
|
50837
51831
|
commitMessage
|
|
@@ -50842,7 +51836,7 @@ const openWorkflowPullRequest = async (params) => {
|
|
|
50842
51836
|
reason: "git-commit-failed"
|
|
50843
51837
|
};
|
|
50844
51838
|
}
|
|
50845
|
-
if (!(await
|
|
51839
|
+
if (!(await run("git", [
|
|
50846
51840
|
"push",
|
|
50847
51841
|
"-u",
|
|
50848
51842
|
"origin",
|
|
@@ -50854,7 +51848,7 @@ const openWorkflowPullRequest = async (params) => {
|
|
|
50854
51848
|
reason: "git-push-failed"
|
|
50855
51849
|
};
|
|
50856
51850
|
}
|
|
50857
|
-
const prCreate = await
|
|
51851
|
+
const prCreate = await run("gh", [
|
|
50858
51852
|
"pr",
|
|
50859
51853
|
"create",
|
|
50860
51854
|
"--title",
|
|
@@ -50878,12 +51872,13 @@ const openWorkflowPullRequest = async (params) => {
|
|
|
50878
51872
|
};
|
|
50879
51873
|
const stageWorkflowFile = async (params) => {
|
|
50880
51874
|
const workflowPath = Path.resolve(params.workflowPath);
|
|
50881
|
-
const
|
|
51875
|
+
const run = params.run ?? runCommand;
|
|
51876
|
+
const repoRootProbe = await run("git", ["rev-parse", "--show-toplevel"], Path.dirname(workflowPath));
|
|
50882
51877
|
if (!repoRootProbe.success) return false;
|
|
50883
|
-
return (await
|
|
51878
|
+
return (await run("git", [
|
|
50884
51879
|
"add",
|
|
50885
51880
|
"--",
|
|
50886
|
-
Path.relative(repoRootProbe.stdout, workflowPath)
|
|
51881
|
+
toForwardSlashes(Path.relative(repoRootProbe.stdout, workflowPath))
|
|
50887
51882
|
], repoRootProbe.stdout)).success;
|
|
50888
51883
|
};
|
|
50889
51884
|
//#endregion
|
|
@@ -50924,6 +51919,7 @@ const setUpGitHubActions = async (options) => {
|
|
|
50924
51919
|
baseBranch: defaultBranch
|
|
50925
51920
|
});
|
|
50926
51921
|
if (pullRequestResult.status === "pr-opened") pullRequestSpinner.succeed(`Opened pull request for review: ${highlighter.info(pullRequestResult.url)}`);
|
|
51922
|
+
else if (pullRequestResult.status === "pr-exists") pullRequestSpinner.succeed(`A React Doctor setup pull request is already open: ${highlighter.info(pullRequestResult.url)}`);
|
|
50927
51923
|
else if (pullRequestResult.status === "branch-pushed") pullRequestSpinner.warn(`Pushed branch ${highlighter.bold(pullRequestResult.branch)} but couldn't open a PR. Open one with: gh pr create --head ${pullRequestResult.branch}`);
|
|
50928
51924
|
else {
|
|
50929
51925
|
pullRequestSpinner.stop();
|
|
@@ -52024,7 +53020,12 @@ const upgradeGitHubActionsWorkflow = async (workflow) => {
|
|
|
52024
53020
|
prBody: UPGRADE_PR_BODY
|
|
52025
53021
|
});
|
|
52026
53022
|
if (pullRequestResult.status === "pr-opened") upgradeSpinner.succeed(`Opened pull request for review: ${highlighter.info(pullRequestResult.url)}`);
|
|
52027
|
-
else if (pullRequestResult.status === "
|
|
53023
|
+
else if (pullRequestResult.status === "pr-exists") {
|
|
53024
|
+
try {
|
|
53025
|
+
NFS.writeFileSync(workflow.workflowPath, workflow.content);
|
|
53026
|
+
} catch {}
|
|
53027
|
+
upgradeSpinner.succeed(`A React Doctor pull request is already open: ${highlighter.info(pullRequestResult.url)}`);
|
|
53028
|
+
} else if (pullRequestResult.status === "branch-pushed") upgradeSpinner.warn(`Pushed branch ${highlighter.bold(pullRequestResult.branch)} but couldn't open a PR. Open one with: gh pr create --head ${pullRequestResult.branch}`);
|
|
52028
53029
|
else {
|
|
52029
53030
|
upgradeSpinner.stop();
|
|
52030
53031
|
try {
|
|
@@ -52138,6 +53139,47 @@ const handoffToAgent = async (input) => {
|
|
|
52138
53139
|
}
|
|
52139
53140
|
};
|
|
52140
53141
|
//#endregion
|
|
53142
|
+
//#region src/cli/utils/migrate-action-pin.ts
|
|
53143
|
+
const WORKFLOWS_DIRECTORY = Path.join(".github", "workflows");
|
|
53144
|
+
const RECOMMENDED_ACTION_REF = "v2";
|
|
53145
|
+
const MUTABLE_ACTION_REF = /(uses:\s*[\w.-]+\/react-doctor@)(?:main|master)\b/g;
|
|
53146
|
+
const isWorkflowFile = (fileName) => /\.ya?ml$/.test(fileName);
|
|
53147
|
+
/**
|
|
53148
|
+
* Rewrites mutable `@main` / `@master` React Doctor GitHub Action references in
|
|
53149
|
+
* the repo's `.github/workflows/*.yml` to the recommended floating major
|
|
53150
|
+
* (`@v2`) — a supply-chain hardening (#299) that also moves the workflow onto
|
|
53151
|
+
* the current (install- and scan-cached) action release. Pinned tags / SHAs are
|
|
53152
|
+
* deliberate and left untouched. Returns the absolute paths of the workflow
|
|
53153
|
+
* files it rewrote — empty when there's nothing to migrate.
|
|
53154
|
+
*/
|
|
53155
|
+
const migrateActionPin = (projectRoot) => {
|
|
53156
|
+
const workflowsDirectory = Path.join(projectRoot, WORKFLOWS_DIRECTORY);
|
|
53157
|
+
let entries;
|
|
53158
|
+
try {
|
|
53159
|
+
entries = NFS.readdirSync(workflowsDirectory, { withFileTypes: true });
|
|
53160
|
+
} catch {
|
|
53161
|
+
return [];
|
|
53162
|
+
}
|
|
53163
|
+
const rewrittenFiles = [];
|
|
53164
|
+
for (const entry of entries) {
|
|
53165
|
+
if (!entry.isFile() || !isWorkflowFile(entry.name)) continue;
|
|
53166
|
+
const workflowPath = Path.join(workflowsDirectory, entry.name);
|
|
53167
|
+
let contents;
|
|
53168
|
+
try {
|
|
53169
|
+
contents = NFS.readFileSync(workflowPath, "utf-8");
|
|
53170
|
+
} catch {
|
|
53171
|
+
continue;
|
|
53172
|
+
}
|
|
53173
|
+
const updated = contents.replace(MUTABLE_ACTION_REF, `$1${RECOMMENDED_ACTION_REF}`);
|
|
53174
|
+
if (updated === contents) continue;
|
|
53175
|
+
try {
|
|
53176
|
+
NFS.writeFileSync(workflowPath, updated);
|
|
53177
|
+
rewrittenFiles.push(workflowPath);
|
|
53178
|
+
} catch {}
|
|
53179
|
+
}
|
|
53180
|
+
return rewrittenFiles;
|
|
53181
|
+
};
|
|
53182
|
+
//#endregion
|
|
52141
53183
|
//#region src/cli/utils/read-object-file.ts
|
|
52142
53184
|
/**
|
|
52143
53185
|
* Reads a JSON / JSONC file as a plain object, or `null` when it is missing,
|
|
@@ -52202,6 +53244,35 @@ export default ${serializeTsObjectLiteral(config)} satisfies ReactDoctorConfig;
|
|
|
52202
53244
|
NFS.rmSync(legacy.legacyFilePath, { force: true });
|
|
52203
53245
|
return targetPath;
|
|
52204
53246
|
};
|
|
53247
|
+
const PROJECT_MIGRATIONS = [{
|
|
53248
|
+
id: "config-json-to-ts",
|
|
53249
|
+
scope: "project",
|
|
53250
|
+
run: ({ projectRoot }) => {
|
|
53251
|
+
if (projectRoot === void 0) return false;
|
|
53252
|
+
const legacyConfig = findLegacyConfig(projectRoot);
|
|
53253
|
+
if (!legacyConfig) return false;
|
|
53254
|
+
const migratedPath = migrateLegacyConfig(legacyConfig);
|
|
53255
|
+
if (!migratedPath) return false;
|
|
53256
|
+
cliLogger.success("Migrated react-doctor.config.json → doctor.config.ts");
|
|
53257
|
+
cliLogger.dim(` Your settings were preserved. Review ${toRelativePath(migratedPath, projectRoot)} and commit it.`);
|
|
53258
|
+
cliLogger.break();
|
|
53259
|
+
return true;
|
|
53260
|
+
}
|
|
53261
|
+
}, {
|
|
53262
|
+
id: "action-pin-main-to-v2",
|
|
53263
|
+
scope: "project",
|
|
53264
|
+
run: ({ projectRoot }) => {
|
|
53265
|
+
if (projectRoot === void 0) return false;
|
|
53266
|
+
const rewrittenFiles = migrateActionPin(projectRoot);
|
|
53267
|
+
if (rewrittenFiles.length === 0) return false;
|
|
53268
|
+
const relativeFiles = rewrittenFiles.map((file) => toRelativePath(file, projectRoot)).join(", ");
|
|
53269
|
+
cliLogger.success(`Pinned the React Doctor action to @v2 in ${relativeFiles}`);
|
|
53270
|
+
cliLogger.dim(" An unpinned @main reference runs whatever the action's HEAD points to (a supply-chain risk). Review and commit the change — or revert it if you intentionally track main.");
|
|
53271
|
+
cliLogger.break();
|
|
53272
|
+
return true;
|
|
53273
|
+
}
|
|
53274
|
+
}];
|
|
53275
|
+
const runProjectMigrations = (projectRoot, options = {}) => runMigrations(PROJECT_MIGRATIONS, { projectRoot }, options);
|
|
52205
53276
|
//#endregion
|
|
52206
53277
|
//#region src/cli/utils/print-branded-header.ts
|
|
52207
53278
|
/**
|
|
@@ -52421,36 +53492,16 @@ const printMultiProjectSummary = (input) => gen(function* () {
|
|
|
52421
53492
|
});
|
|
52422
53493
|
//#endregion
|
|
52423
53494
|
//#region src/cli/utils/prompt-install-setup.ts
|
|
52424
|
-
const
|
|
52425
|
-
|
|
52426
|
-
|
|
52427
|
-
|
|
52428
|
-
|
|
52429
|
-
const hasDisabledSetupPrompt = (projectRoot,
|
|
52430
|
-
|
|
52431
|
-
|
|
52432
|
-
|
|
52433
|
-
|
|
52434
|
-
}
|
|
52435
|
-
};
|
|
52436
|
-
const disableSetupPrompt = (projectRoot, storeOptions = {}) => {
|
|
52437
|
-
try {
|
|
52438
|
-
const store = getSetupPromptStore(storeOptions);
|
|
52439
|
-
const projects = store.get("projects", {});
|
|
52440
|
-
const projectKey = getSetupPromptProjectKey(projectRoot);
|
|
52441
|
-
store.set("projects", {
|
|
52442
|
-
...projects,
|
|
52443
|
-
[projectKey]: {
|
|
52444
|
-
...projects[projectKey] ?? {},
|
|
52445
|
-
rootDirectory: Path.resolve(projectRoot),
|
|
52446
|
-
setupPrompt: false
|
|
52447
|
-
}
|
|
52448
|
-
});
|
|
52449
|
-
return true;
|
|
52450
|
-
} catch {
|
|
52451
|
-
return false;
|
|
52452
|
-
}
|
|
52453
|
-
};
|
|
53495
|
+
const SETUP_HINT_GATE = {
|
|
53496
|
+
id: SETUP_HINT_EVENT,
|
|
53497
|
+
scope: "project",
|
|
53498
|
+
fireWhenUnknown: true
|
|
53499
|
+
};
|
|
53500
|
+
const hasDisabledSetupPrompt = (projectRoot, options = {}) => !isGatePending(SETUP_HINT_GATE, { projectRoot }, options);
|
|
53501
|
+
const disableSetupPrompt = (projectRoot, options = {}) => recordGate(SETUP_HINT_GATE, {
|
|
53502
|
+
projectRoot,
|
|
53503
|
+
outcome: "declined"
|
|
53504
|
+
}, options);
|
|
52454
53505
|
const resolveInstallSetupProjectRoot = (options) => {
|
|
52455
53506
|
if (options.scanDirectories.length === 0) return findNearestPackageDirectory(options.scanRoot) ?? options.scanRoot;
|
|
52456
53507
|
const packageDirectories = /* @__PURE__ */ new Set();
|
|
@@ -52978,15 +54029,9 @@ const buildChangedFilesDiffInfo = (changedFiles) => ({
|
|
|
52978
54029
|
* are left untouched — the loader still reads the legacy file as a deprecated
|
|
52979
54030
|
* fallback and warns — so a scan never mutates the repo unattended.
|
|
52980
54031
|
*/
|
|
52981
|
-
const maybeMigrateLegacyConfig = (requestedDirectory, { isQuiet, isStaged }) => {
|
|
54032
|
+
const maybeMigrateLegacyConfig = async (requestedDirectory, { isQuiet, isStaged }) => {
|
|
52982
54033
|
if (!(!isQuiet && !isStaged && process.stdout.isTTY === true && !isCiOrCodingAgentEnvironment())) return;
|
|
52983
|
-
|
|
52984
|
-
if (!legacyConfig) return;
|
|
52985
|
-
const migratedPath = migrateLegacyConfig(legacyConfig);
|
|
52986
|
-
if (!migratedPath) return;
|
|
52987
|
-
cliLogger.success("Migrated react-doctor.config.json → doctor.config.ts");
|
|
52988
|
-
cliLogger.dim(` Your settings were preserved. Review ${toRelativePath(migratedPath, requestedDirectory)} and commit it.`);
|
|
52989
|
-
cliLogger.break();
|
|
54034
|
+
await runProjectMigrations(requestedDirectory);
|
|
52990
54035
|
};
|
|
52991
54036
|
const inspectAction = async (directory, flags) => {
|
|
52992
54037
|
const isScoreOnly = Boolean(flags.score);
|
|
@@ -53001,7 +54046,7 @@ const inspectAction = async (directory, flags) => {
|
|
|
53001
54046
|
recordCount(METRIC.cliInvoked, 1, { command: "inspect" });
|
|
53002
54047
|
try {
|
|
53003
54048
|
validateModeFlags(flags);
|
|
53004
|
-
maybeMigrateLegacyConfig(requestedDirectory, {
|
|
54049
|
+
await maybeMigrateLegacyConfig(requestedDirectory, {
|
|
53005
54050
|
isQuiet,
|
|
53006
54051
|
isStaged: Boolean(flags.staged)
|
|
53007
54052
|
});
|
|
@@ -54305,4 +55350,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
|
|
|
54305
55350
|
export {};
|
|
54306
55351
|
|
|
54307
55352
|
//# sourceMappingURL=cli.js.map
|
|
54308
|
-
//# debugId=
|
|
55353
|
+
//# debugId=6a2c86b6-c3a8-5e50-8e7b-daef840df200
|