react-doctor 0.5.7 → 0.5.8-dev.229ea2e
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 +1545 -368
- package/dist/index.d.ts +15 -1
- package/dist/index.js +960 -173
- package/dist/lsp.js +961 -173
- 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]="6c903b87-8faf-5702-b078-03465e326385")}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";
|
|
@@ -26,7 +26,7 @@ import tty from "node:tty";
|
|
|
26
26
|
import { codeFrameColumns } from "@babel/code-frame";
|
|
27
27
|
import Conf from "conf";
|
|
28
28
|
import basePrompts from "prompts";
|
|
29
|
-
import { SKILL_MANIFEST_FILE, detectInstalledSkillAgents, getSkillAgentConfig, getSkillAgentTypes, installSkillsFromSource } from "agent-install";
|
|
29
|
+
import { SKILL_MANIFEST_FILE, detectInstalledSkillAgents, getSkillAgentConfig, getSkillAgentTypes, installSkillsFromSource, isSkillAgentType } from "agent-install";
|
|
30
30
|
import { generateCode, loadFile, writeFile } from "magicast";
|
|
31
31
|
import { getConfigFromVariableDeclaration, getDefaultExportOptions } from "magicast/helpers";
|
|
32
32
|
//#region \0rolldown/runtime.js
|
|
@@ -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,8 @@ 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 SPAWN_ARGS_MAX_LENGTH_CHARS = 24e3;
|
|
36866
|
+
const PER_WORKER_MEM_BUDGET_BYTES = 1024 * 1024 * 1024;
|
|
36765
36867
|
const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
|
|
36766
36868
|
const ADOPTABLE_LINT_CONFIG_FILENAMES = [".oxlintrc.json", ".eslintrc.json"];
|
|
36767
36869
|
const OXLINT_NODE_REQUIREMENT = "^20.19.0 || >=22.12.0";
|
|
@@ -36838,7 +36940,16 @@ const CANONICAL_DISCORD_URL = "https://react.doctor/discord";
|
|
|
36838
36940
|
const SKILL_NAME = "react-doctor";
|
|
36839
36941
|
const OXLINT_OUTPUT_MAX_BYTES = 50 * 1024 * 1024;
|
|
36840
36942
|
const OXLINT_SPAWN_TIMEOUT_MS = 6e4;
|
|
36943
|
+
const NODE_COMPILE_CACHE_DIR_NAME = "node-compile-cache";
|
|
36944
|
+
const DEAD_CODE_WORKER_TIMEOUT_MS = 12e4;
|
|
36945
|
+
const OXLINT_SPLIT_TOTAL_BUDGET_MS = 18e4;
|
|
36946
|
+
const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
|
|
36947
|
+
const LINT_PHASE_TIMEOUT_MS = 3e5;
|
|
36948
|
+
const SCAN_TOTAL_DEADLINE_MS = 9e5;
|
|
36841
36949
|
const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
|
|
36950
|
+
const DEAD_CODE_TIMEOUT_CEILING_MS = 6e5;
|
|
36951
|
+
const DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS = 3e4;
|
|
36952
|
+
const DEAD_CODE_OVERLAP_PARSE_SHARE = .4;
|
|
36842
36953
|
const RECOMMENDED_PNPM_MINIMUM_RELEASE_AGE_MINUTES = 10080;
|
|
36843
36954
|
const REACT_SERVER_DOM_PACKAGES = [
|
|
36844
36955
|
"react-server-dom-webpack",
|
|
@@ -36873,9 +36984,13 @@ const CONFIG_CACHE_TTL_MS = 300 * 1e3;
|
|
|
36873
36984
|
const SOCKET_FREE_PURL_API_BASE = "https://firewall-api.socket.dev/purl";
|
|
36874
36985
|
const SOCKET_PACKAGE_PAGE_BASE = "https://socket.dev/npm/package";
|
|
36875
36986
|
const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
|
|
36987
|
+
const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
|
|
36988
|
+
const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
|
|
36876
36989
|
const SUPPLY_CHAIN_PLUGIN = "socket";
|
|
36877
36990
|
const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
|
|
36878
36991
|
const SUPPLY_CHAIN_CATEGORY = "Security";
|
|
36992
|
+
const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
|
|
36993
|
+
const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
|
|
36879
36994
|
const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
|
|
36880
36995
|
const TSCONFIG_FILENAME = "tsconfig.json";
|
|
36881
36996
|
const isRelativeExtendsValue = (extendsValue) => extendsValue.startsWith("./") || extendsValue.startsWith("../") || Path.isAbsolute(extendsValue);
|
|
@@ -37565,7 +37680,10 @@ for (const [legacyRuleKey, nativeRuleKey] of Object.entries(LEGACY_RULE_KEY_TO_N
|
|
|
37565
37680
|
NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.set(nativeRuleKey, aliases);
|
|
37566
37681
|
}
|
|
37567
37682
|
const getLegacyRuleKeysForNative = (ruleKey) => NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(ruleKey) ?? [];
|
|
37568
|
-
const canonicalizeRuleKey = (ruleKey) =>
|
|
37683
|
+
const canonicalizeRuleKey = (ruleKey) => {
|
|
37684
|
+
const nativeRuleKey = LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey];
|
|
37685
|
+
return typeof nativeRuleKey === "string" ? nativeRuleKey : ruleKey;
|
|
37686
|
+
};
|
|
37569
37687
|
const isReactDoctorShortIdOf = (bareRuleKey, qualifiedRuleKey) => !bareRuleKey.includes("/") && qualifiedRuleKey === `react-doctor/${bareRuleKey}`;
|
|
37570
37688
|
const isSameRuleKey = (candidateRuleKey, targetRuleKey) => {
|
|
37571
37689
|
const canonicalCandidate = canonicalizeRuleKey(candidateRuleKey);
|
|
@@ -38227,6 +38345,11 @@ var OxlintBatchExceeded = class extends TaggedErrorClass()("OxlintBatchExceeded"
|
|
|
38227
38345
|
}
|
|
38228
38346
|
}
|
|
38229
38347
|
};
|
|
38348
|
+
var ScanDeadlineExceeded = class extends TaggedErrorClass()("ScanDeadlineExceeded", { detail: String$1 }) {
|
|
38349
|
+
get message() {
|
|
38350
|
+
return `Scan exceeded its overall time budget: ${this.detail}`;
|
|
38351
|
+
}
|
|
38352
|
+
};
|
|
38230
38353
|
var OxlintSpawnFailed = class extends TaggedErrorClass()("OxlintSpawnFailed", { cause: Unknown }) {
|
|
38231
38354
|
get message() {
|
|
38232
38355
|
return `Failed to run oxlint: ${pretty(fail$6(this.cause))}`;
|
|
@@ -38290,6 +38413,7 @@ var GitBaseBranchInvalid = class extends TaggedErrorClass()("GitBaseBranchInvali
|
|
|
38290
38413
|
const ReactDoctorErrorReason = Union([
|
|
38291
38414
|
OxlintUnavailable,
|
|
38292
38415
|
OxlintBatchExceeded,
|
|
38416
|
+
ScanDeadlineExceeded,
|
|
38293
38417
|
OxlintSpawnFailed,
|
|
38294
38418
|
OxlintOutputUnparseable,
|
|
38295
38419
|
ConfigParseFailed,
|
|
@@ -38362,15 +38486,105 @@ const layerOtlp = unwrap$3(gen(function* () {
|
|
|
38362
38486
|
}).pipe(provide$2(layer$9));
|
|
38363
38487
|
}).pipe(orDie));
|
|
38364
38488
|
/**
|
|
38365
|
-
*
|
|
38366
|
-
* `
|
|
38367
|
-
|
|
38489
|
+
* Read a positive-millisecond timeout from an env var, falling back to
|
|
38490
|
+
* `defaultMs` when the var is unset, non-finite, or not strictly positive.
|
|
38491
|
+
*/
|
|
38492
|
+
const readPositiveEnvMs = (envVarName, defaultMs) => {
|
|
38493
|
+
const rawValue = process.env[envVarName];
|
|
38494
|
+
if (rawValue === void 0) return defaultMs;
|
|
38495
|
+
const parsedValue = Number(rawValue);
|
|
38496
|
+
if (!Number.isFinite(parsedValue) || parsedValue <= 0) return defaultMs;
|
|
38497
|
+
return parsedValue;
|
|
38498
|
+
};
|
|
38499
|
+
const CGROUP_V2_MEMORY_MAX_PATH = "/sys/fs/cgroup/memory.max";
|
|
38500
|
+
const CGROUP_V1_MEMORY_LIMIT_PATH = "/sys/fs/cgroup/memory/memory.limit_in_bytes";
|
|
38501
|
+
const CGROUP_UNLIMITED_SENTINEL_BYTES = Number.MAX_SAFE_INTEGER;
|
|
38502
|
+
/**
|
|
38503
|
+
* Parses one raw cgroup memory-limit file value into a positive byte count, or
|
|
38504
|
+
* `undefined` when it represents "no limit" (the v2 `"max"` literal, an empty
|
|
38505
|
+
* read, a non-positive / non-finite value, or v1's near-2^63 unlimited
|
|
38506
|
+
* sentinel). Pure and exported so the classification is unit-testable without
|
|
38507
|
+
* touching the filesystem.
|
|
38508
|
+
*/
|
|
38509
|
+
const parseCgroupMemoryLimitBytes = (raw) => {
|
|
38510
|
+
if (raw === void 0) return void 0;
|
|
38511
|
+
const trimmed = raw.trim();
|
|
38512
|
+
if (trimmed === "" || trimmed === "max") return void 0;
|
|
38513
|
+
const parsed = Number(trimmed);
|
|
38514
|
+
if (!Number.isFinite(parsed) || parsed <= 0 || parsed >= CGROUP_UNLIMITED_SENTINEL_BYTES) return;
|
|
38515
|
+
return parsed;
|
|
38516
|
+
};
|
|
38517
|
+
const CGROUP_MEMORY_LIMIT_PATHS = [CGROUP_V2_MEMORY_MAX_PATH, CGROUP_V1_MEMORY_LIMIT_PATH];
|
|
38518
|
+
/**
|
|
38519
|
+
* Reads this process's cgroup memory limit in bytes from the first candidate
|
|
38520
|
+
* path that yields a real limit, or `undefined` when none does — no cgroup, no
|
|
38521
|
+
* limit, or the files are unreadable (e.g. macOS / Windows dev machines).
|
|
38522
|
+
* `os.totalmem()` reports the HOST total and ignores cgroup memory limits, so a
|
|
38523
|
+
* memory-constrained container over-reports total memory; `resolveAutoScan-
|
|
38524
|
+
* Concurrency` takes `min(totalmem, this)` to honor the limit.
|
|
38525
|
+
*
|
|
38526
|
+
* The cgroup v2 read is the mount-root `memory.max`, which IS the container's
|
|
38527
|
+
* limit under the standard cgroup-namespace setup CI runners use (the
|
|
38528
|
+
* container's own cgroup is the root of its namespaced view). A process in a
|
|
38529
|
+
* non-namespaced nested/delegated cgroup whose root reads `"max"` is not
|
|
38530
|
+
* detected here and falls back to the host total; the EAGAIN/ENOMEM serial
|
|
38531
|
+
* replay in `spawnLintBatches` remains the runtime backstop for that case.
|
|
38532
|
+
*
|
|
38533
|
+
* `candidatePaths` is injectable so tests exercise the v2-wins-over-v1
|
|
38534
|
+
* precedence, the skip-unreadable fallback, and the all-missing case without a
|
|
38535
|
+
* real `/sys/fs/cgroup`.
|
|
38536
|
+
*/
|
|
38537
|
+
const readCgroupMemoryLimitBytes = (candidatePaths = CGROUP_MEMORY_LIMIT_PATHS) => {
|
|
38538
|
+
for (const limitPath of candidatePaths) {
|
|
38539
|
+
let raw;
|
|
38540
|
+
try {
|
|
38541
|
+
raw = fs.readFileSync(limitPath, "utf8");
|
|
38542
|
+
} catch {
|
|
38543
|
+
continue;
|
|
38544
|
+
}
|
|
38545
|
+
const limitBytes = parseCgroupMemoryLimitBytes(raw);
|
|
38546
|
+
if (limitBytes !== void 0) return limitBytes;
|
|
38547
|
+
}
|
|
38548
|
+
};
|
|
38549
|
+
/**
|
|
38550
|
+
* Clamps a requested lint worker count to `[MIN_SCAN_CONCURRENCY,
|
|
38551
|
+
* HARD_MAX_SCAN_CONCURRENCY]` as a finite integer. This is the explicit-pin and
|
|
38552
|
+
* spawn-boundary clamp — the memory-and-core-budgeted auto count comes from
|
|
38553
|
+
* `resolveAutoScanConcurrency`. Out-of-range or non-finite requests degrade to
|
|
38368
38554
|
* `MIN_SCAN_CONCURRENCY` rather than oversubscribing or running zero workers.
|
|
38369
38555
|
*/
|
|
38370
38556
|
const resolveScanConcurrency = (requested) => {
|
|
38371
|
-
|
|
38372
|
-
|
|
38373
|
-
|
|
38557
|
+
if (!Number.isFinite(requested) || requested < 1) return 1;
|
|
38558
|
+
return Math.min(Math.floor(requested), 32);
|
|
38559
|
+
};
|
|
38560
|
+
const readSystemFacts = () => ({
|
|
38561
|
+
availableCores: os.availableParallelism(),
|
|
38562
|
+
totalMemoryBytes: os.totalmem(),
|
|
38563
|
+
cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
|
|
38564
|
+
});
|
|
38565
|
+
/**
|
|
38566
|
+
* Auto lint-worker count: the smaller of the (cgroup-CPU-aware) core count and
|
|
38567
|
+
* the number of `PER_WORKER_MEM_BUDGET_BYTES` workers that fit in available
|
|
38568
|
+
* memory, then clamped to `[MIN, HARD_MAX]` by `resolveScanConcurrency`.
|
|
38569
|
+
*
|
|
38570
|
+
* `os.availableParallelism()` already respects cgroup CPU quotas, so the core
|
|
38571
|
+
* term needs no help. Available memory is `os.totalmem()` floored by the cgroup
|
|
38572
|
+
* memory limit — `os.freemem()` is deliberately NOT used: it excludes
|
|
38573
|
+
* reclaimable page cache and reads near-zero on macOS / cache-heavy Linux, which
|
|
38574
|
+
* would collapse the auto path to a single worker. `os.totalmem()` reports the
|
|
38575
|
+
* host total even inside a container, so the cgroup limit (read directly,
|
|
38576
|
+
* because Node doesn't fold it into `totalmem()`) is the real ceiling there.
|
|
38577
|
+
*
|
|
38578
|
+
* `facts` is injectable so tests exercise core-bound, memory-bound, cgroup-
|
|
38579
|
+
* limited, and ceiling cases without mocking `os` or the filesystem.
|
|
38580
|
+
*/
|
|
38581
|
+
const resolveAutoScanConcurrency = (facts = readSystemFacts()) => {
|
|
38582
|
+
const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
|
|
38583
|
+
const memoryBoundedWorkers = Math.floor(availableMemoryBytes / PER_WORKER_MEM_BUDGET_BYTES);
|
|
38584
|
+
return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
|
|
38585
|
+
};
|
|
38586
|
+
const resolveLintBatchOrdering = () => {
|
|
38587
|
+
return process.env["REACT_DOCTOR_LINT_BATCH_ORDERING"]?.trim().toLowerCase() === "cost" ? "cost" : "arrival";
|
|
38374
38588
|
};
|
|
38375
38589
|
/**
|
|
38376
38590
|
* Per-batch oxlint wall-clock budget. Reads from the env var on
|
|
@@ -38378,11 +38592,38 @@ const resolveScanConcurrency = (requested) => {
|
|
|
38378
38592
|
* microVMs without recompiling react-doctor. Tests override via
|
|
38379
38593
|
* `Layer.succeed(OxlintSpawnTimeoutMs, ...)`.
|
|
38380
38594
|
*/
|
|
38381
|
-
var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => {
|
|
38382
|
-
|
|
38383
|
-
|
|
38595
|
+
var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS", OXLINT_SPAWN_TIMEOUT_MS) }) {};
|
|
38596
|
+
/**
|
|
38597
|
+
* Effect-side cap on the lint phase. The env var lets CI / eval runners
|
|
38598
|
+
* raise the phase budget for slow large repos without recompiling.
|
|
38599
|
+
* Tests override via `Layer.succeed(LintPhaseTimeoutMs, ...)`.
|
|
38600
|
+
*/
|
|
38601
|
+
var LintPhaseTimeoutMs = class extends Reference("react-doctor/LintPhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_LINT_PHASE_TIMEOUT_MS", LINT_PHASE_TIMEOUT_MS) }) {};
|
|
38602
|
+
/**
|
|
38603
|
+
* Effect-side cap on the dead-code phase, sitting above the in-worker
|
|
38604
|
+
* timeout as a runtime-independent backstop. The env var raises it for
|
|
38605
|
+
* type-heavy projects; tests override via
|
|
38606
|
+
* `Layer.succeed(DeadCodePhaseTimeoutMs, ...)`.
|
|
38607
|
+
*/
|
|
38608
|
+
var DeadCodePhaseTimeoutMs = class extends Reference("react-doctor/DeadCodePhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_DEAD_CODE_PHASE_TIMEOUT_MS", DEAD_CODE_PHASE_TIMEOUT_MS) }) {};
|
|
38609
|
+
/**
|
|
38610
|
+
* Overall scan deadline backstop, bounding everything the per-phase
|
|
38611
|
+
* timeouts don't (wedged git / IO). The env var raises it for very
|
|
38612
|
+
* large repos; tests override via `Layer.succeed(ScanDeadlineMs, ...)`.
|
|
38613
|
+
*/
|
|
38614
|
+
var ScanDeadlineMs = class extends Reference("react-doctor/ScanDeadlineMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_SCAN_DEADLINE_MS", SCAN_TOTAL_DEADLINE_MS) }) {};
|
|
38615
|
+
/**
|
|
38616
|
+
* Wall-clock budget for the supply-chain check when it runs on a background
|
|
38617
|
+
* fiber overlapping the lint pass. Reads from the env var on startup so the
|
|
38618
|
+
* eval harness can raise the budget under sandbox microVMs (slower network)
|
|
38619
|
+
* without recompiling react-doctor. Tests override via
|
|
38620
|
+
* `Layer.succeed(SupplyChainOverlapTimeoutMs, ...)`.
|
|
38621
|
+
*/
|
|
38622
|
+
var SupplyChainOverlapTimeoutMs = class extends Reference("react-doctor/SupplyChainOverlapTimeoutMs", { defaultValue: () => {
|
|
38623
|
+
const raw = process.env["REACT_DOCTOR_SUPPLY_CHAIN_TIMEOUT_MS"];
|
|
38624
|
+
if (raw === void 0) return SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS;
|
|
38384
38625
|
const parsed = Number(raw);
|
|
38385
|
-
if (!Number.isFinite(parsed) || parsed <= 0) return
|
|
38626
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS;
|
|
38386
38627
|
return parsed;
|
|
38387
38628
|
} }) {};
|
|
38388
38629
|
/**
|
|
@@ -38393,31 +38634,93 @@ var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTime
|
|
|
38393
38634
|
*/
|
|
38394
38635
|
var OxlintOutputMaxBytes = class extends Reference("react-doctor/OxlintOutputMaxBytes", { defaultValue: () => OXLINT_OUTPUT_MAX_BYTES }) {};
|
|
38395
38636
|
/**
|
|
38396
|
-
* Number of oxlint subprocesses the lint pass runs in parallel. Defaults
|
|
38397
|
-
*
|
|
38398
|
-
* the box
|
|
38399
|
-
*
|
|
38400
|
-
*
|
|
38401
|
-
*
|
|
38402
|
-
*
|
|
38403
|
-
*
|
|
38404
|
-
*
|
|
38405
|
-
*
|
|
38637
|
+
* Number of oxlint subprocesses the lint pass runs in parallel. Defaults to a
|
|
38638
|
+
* memory-and-core-budgeted auto count (`resolveAutoScanConcurrency`) so large
|
|
38639
|
+
* repos scan fast out of the box without OOMing the native binding on a
|
|
38640
|
+
* high-core / low-memory box; `spawnLintBatches` transparently falls back to a
|
|
38641
|
+
* single worker if a parallel run still exhausts system resources. The CLI's
|
|
38642
|
+
* `--no-parallel` flag forces serial via `Layer.succeed`; the
|
|
38643
|
+
* `REACT_DOCTOR_PARALLEL` env var seeds the default for programmatic / CI
|
|
38644
|
+
* callers that never touch the flag — parallelism is opt-OUT, so only the
|
|
38645
|
+
* explicit serial values pin one worker:
|
|
38646
|
+
*
|
|
38647
|
+
* - unset / `auto` / `true` / `on` → memory-and-core-budgeted auto count
|
|
38406
38648
|
* - `0` / `false` / `off` → `1` (serial)
|
|
38407
38649
|
* - a positive integer → that many workers (clamped)
|
|
38408
|
-
* - any other value →
|
|
38650
|
+
* - any other value → memory-and-core-budgeted auto count
|
|
38409
38651
|
*
|
|
38410
38652
|
* The resolved value is always within
|
|
38411
|
-
* `[MIN_SCAN_CONCURRENCY,
|
|
38653
|
+
* `[MIN_SCAN_CONCURRENCY, HARD_MAX_SCAN_CONCURRENCY]`.
|
|
38412
38654
|
*/
|
|
38413
38655
|
var OxlintConcurrency = class extends Reference("react-doctor/OxlintConcurrency", { defaultValue: () => {
|
|
38414
38656
|
const raw = process.env["REACT_DOCTOR_PARALLEL"];
|
|
38415
|
-
if (raw === void 0) return
|
|
38657
|
+
if (raw === void 0) return resolveAutoScanConcurrency();
|
|
38416
38658
|
const normalized = raw.trim().toLowerCase();
|
|
38417
38659
|
if (normalized === "0" || normalized === "false" || normalized === "off") return 1;
|
|
38418
38660
|
const parsed = Number.parseInt(normalized, 10);
|
|
38419
38661
|
if (Number.isInteger(parsed) && parsed > 0) return resolveScanConcurrency(parsed);
|
|
38420
|
-
return
|
|
38662
|
+
return resolveAutoScanConcurrency();
|
|
38663
|
+
} }) {};
|
|
38664
|
+
/**
|
|
38665
|
+
* Three-state control for overlapping the dead-code pass with the lint pass —
|
|
38666
|
+
* forking dead-code as a child fiber that runs DURING lint instead of strictly
|
|
38667
|
+
* after it.
|
|
38668
|
+
*
|
|
38669
|
+
* - `"auto"` (default) / `"off"` → strictly SEQUENTIAL: dead-code runs after
|
|
38670
|
+
* lint with the full core budget. Both deslop's parse pool and the oxlint
|
|
38671
|
+
* pool are CPU-bound and each size themselves to all cores, so overlapping
|
|
38672
|
+
* them only oversubscribes (~2x the cores) and starves the parse pass past
|
|
38673
|
+
* its timeout — for no wall-clock win, since there are no spare cores to
|
|
38674
|
+
* absorb the second pass. Sequential is both faster per-phase and safe.
|
|
38675
|
+
* - `"on"` → force the overlap anyway. The orchestrator then SPLITS the core
|
|
38676
|
+
* budget (`DEAD_CODE_OVERLAP_PARSE_SHARE`): deslop's parse pool is capped
|
|
38677
|
+
* and lint shrinks to the remainder, so the two sum to the cores instead of
|
|
38678
|
+
* doubling them, and the dead-code timeout scales up for the reduced share.
|
|
38679
|
+
*
|
|
38680
|
+
* Seeded from `REACT_DOCTOR_DEAD_CODE_OVERLAP` so operators get a redeploy-free
|
|
38681
|
+
* switch; tests pin it via `Layer.succeed(DeadCodeOverlap, ...)`.
|
|
38682
|
+
*/
|
|
38683
|
+
var DeadCodeOverlap = class extends Reference("react-doctor/DeadCodeOverlap", { defaultValue: () => {
|
|
38684
|
+
const raw = process.env["REACT_DOCTOR_DEAD_CODE_OVERLAP"]?.trim().toLowerCase();
|
|
38685
|
+
if (raw === "on" || raw === "true" || raw === "1") return "on";
|
|
38686
|
+
if (raw === "off" || raw === "false" || raw === "0") return "off";
|
|
38687
|
+
return "auto";
|
|
38688
|
+
} }) {};
|
|
38689
|
+
/**
|
|
38690
|
+
* How the full-scan lint pass orders its file batches. `"arrival"` (the
|
|
38691
|
+
* default) keeps `git ls-files` discovery order. `"cost"` opts into LPT (feed
|
|
38692
|
+
* the largest files first); set `REACT_DOCTOR_LINT_BATCH_ORDERING=cost`. NOTE:
|
|
38693
|
+
* `cost` is OFF by default because the current sort-desc-then-chunk-100 packs
|
|
38694
|
+
* the heaviest files into one wave-1 batch — on size-skewed repos that mega-
|
|
38695
|
+
* batch is a straggler (and can trip the per-batch timeout + split), measurably
|
|
38696
|
+
* regressing the common full-scan case. LPT needs the heavy files SPREAD across
|
|
38697
|
+
* batches before `cost` earns the default. Tests override via
|
|
38698
|
+
* `Layer.succeed(LintBatchOrdering, ...)`. Diff / staged scans never reach this
|
|
38699
|
+
* — they pass user-scoped `includePaths` that skip discovery and stay in
|
|
38700
|
+
* arrival order; only the full-scan branch reads it.
|
|
38701
|
+
*/
|
|
38702
|
+
var LintBatchOrdering = class extends Reference("react-doctor/LintBatchOrdering", { defaultValue: resolveLintBatchOrdering }) {};
|
|
38703
|
+
const CACHE_DISABLED_VALUES$1 = new Set(["1", "true"]);
|
|
38704
|
+
/**
|
|
38705
|
+
* Whether the per-file lint cache (`runners/oxlint/file-lint-cache.ts`) is
|
|
38706
|
+
* active. Defaults ON — repeat scans re-lint only the files whose content
|
|
38707
|
+
* changed, and correctness is guaranteed byte-identical to a cold scan by the
|
|
38708
|
+
* always-fresh cross-file sidecar. Opt-OUT, two knobs (matching the whole-repo
|
|
38709
|
+
* scan cache's `REACT_DOCTOR_NO_CACHE`):
|
|
38710
|
+
*
|
|
38711
|
+
* - `REACT_DOCTOR_NO_CACHE` — the global off-switch; disables BOTH the
|
|
38712
|
+
* whole-repo scan cache and this per-file cache.
|
|
38713
|
+
* - `REACT_DOCTOR_NO_FILE_CACHE` — granular: bust only the per-file cache
|
|
38714
|
+
* while keeping the whole-repo short-circuit.
|
|
38715
|
+
*
|
|
38716
|
+
* Tests override via `Layer.succeed(PerFileLintCacheEnabled, false)`.
|
|
38717
|
+
*/
|
|
38718
|
+
var PerFileLintCacheEnabled = class extends Reference("react-doctor/PerFileLintCacheEnabled", { defaultValue: () => {
|
|
38719
|
+
const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
|
|
38720
|
+
const noFileCache = process.env["REACT_DOCTOR_NO_FILE_CACHE"]?.toLowerCase() ?? "";
|
|
38721
|
+
if (CACHE_DISABLED_VALUES$1.has(noCache)) return false;
|
|
38722
|
+
if (CACHE_DISABLED_VALUES$1.has(noFileCache)) return false;
|
|
38723
|
+
return true;
|
|
38421
38724
|
} }) {};
|
|
38422
38725
|
const DIAGNOSTIC_SURFACES = [
|
|
38423
38726
|
"cli",
|
|
@@ -38466,6 +38769,12 @@ const BOOLEAN_FIELD_NAMES = [
|
|
|
38466
38769
|
"adoptExistingLintConfig"
|
|
38467
38770
|
];
|
|
38468
38771
|
const STRING_FIELD_NAMES = ["rootDir"];
|
|
38772
|
+
const STRING_ARRAY_FIELD_NAMES = [
|
|
38773
|
+
"projects",
|
|
38774
|
+
"textComponents",
|
|
38775
|
+
"rawTextWrapperComponents",
|
|
38776
|
+
"serverAuthFunctionNames"
|
|
38777
|
+
];
|
|
38469
38778
|
const SURFACE_CONTROL_FIELD_NAMES = [
|
|
38470
38779
|
"includeTags",
|
|
38471
38780
|
"excludeTags",
|
|
@@ -38567,6 +38876,7 @@ const validateConfigTypes = (config) => {
|
|
|
38567
38876
|
const validated = { ...config };
|
|
38568
38877
|
for (const fieldName of BOOLEAN_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => coerceMaybeBooleanString(fieldName, value));
|
|
38569
38878
|
for (const fieldName of STRING_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateString(fieldName, value));
|
|
38879
|
+
for (const fieldName of STRING_ARRAY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateStringArrayField(fieldName, value));
|
|
38570
38880
|
applyFieldValidator(config, validated, "surfaces", validateSurfacesField);
|
|
38571
38881
|
for (const fieldName of SEVERITY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateSeverityMap(fieldName, value, fieldName === "categories"));
|
|
38572
38882
|
applyFieldValidator(config, validated, "plugins", (value) => validateStringArrayField("plugins", value));
|
|
@@ -38851,6 +39161,8 @@ const assignFixGroups = (diagnostics) => {
|
|
|
38851
39161
|
};
|
|
38852
39162
|
});
|
|
38853
39163
|
};
|
|
39164
|
+
const compareStrings = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
39165
|
+
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
39166
|
const getDirectDependencyNames = (packageJson) => new Set([...Object.keys(packageJson.dependencies ?? {}), ...Object.keys(packageJson.devDependencies ?? {})]);
|
|
38855
39167
|
const buildExpoCheckContext = (rootDirectory, expoVersion) => {
|
|
38856
39168
|
const packageJson = readPackageJson$1(Path.join(rootDirectory, "package.json"));
|
|
@@ -39357,10 +39669,15 @@ const buildHardeningDiagnostic = (input) => ({
|
|
|
39357
39669
|
column: input.column ?? 0,
|
|
39358
39670
|
category: "Security"
|
|
39359
39671
|
});
|
|
39360
|
-
const checkPnpmHardening = (
|
|
39361
|
-
if (!isPnpmManagedProject(
|
|
39362
|
-
const workspacePath = Path.join(
|
|
39363
|
-
const
|
|
39672
|
+
const checkPnpmHardening = (scanDirectory) => {
|
|
39673
|
+
if (!isPnpmManagedProject(scanDirectory)) return [];
|
|
39674
|
+
const workspacePath = Path.join(scanDirectory, PNPM_WORKSPACE_FILE);
|
|
39675
|
+
const hasWorkspaceFile = isFile(workspacePath);
|
|
39676
|
+
if (!hasWorkspaceFile) {
|
|
39677
|
+
const monorepoRoot = findMonorepoRoot(scanDirectory);
|
|
39678
|
+
if (monorepoRoot !== null && isFile(Path.join(monorepoRoot, PNPM_WORKSPACE_FILE))) return [];
|
|
39679
|
+
}
|
|
39680
|
+
const settings = parseHardeningSettings(hasWorkspaceFile ? NFS.readFileSync(workspacePath, "utf-8") : "");
|
|
39364
39681
|
const diagnostics = [];
|
|
39365
39682
|
if (settings.minimumReleaseAge === null) diagnostics.push(buildHardeningDiagnostic({
|
|
39366
39683
|
message: "pnpm-workspace.yaml is missing `minimumReleaseAge` — newly published versions can ship malware that gets caught and unpublished within hours",
|
|
@@ -40161,6 +40478,22 @@ process.stdin.on("end", () => {
|
|
|
40161
40478
|
...(workerInput.ignorePatterns.length > 0
|
|
40162
40479
|
? { ignorePatterns: workerInput.ignorePatterns }
|
|
40163
40480
|
: {}),
|
|
40481
|
+
// We consume only deslop's GRAPH-based findings (unusedFiles, unusedExports,
|
|
40482
|
+
// unusedDependencies, circularDependencies). Everything else deslop can compute
|
|
40483
|
+
// is pure wasted work for us, and it's the bulk of the runtime:
|
|
40484
|
+
// - semantic: a full TS Program for unusedTypes/enum/class-members/
|
|
40485
|
+
// misclassifiedDependencies (~37-45% of the phase).
|
|
40486
|
+
// - reportCodeQuality: the duplicate-block, complexity, feature-flag,
|
|
40487
|
+
// TypeScript-smell, private-type-leak and re-export-cycle detectors. These
|
|
40488
|
+
// are the single most expensive pass — duplicate-block detection alone was
|
|
40489
|
+
// ~83s of a ~130s Sentry scan — so skipping them is an ~8.5x dead-code
|
|
40490
|
+
// speedup on a large repo.
|
|
40491
|
+
// Both are provably safe: the consumed graph findings are computed by their own
|
|
40492
|
+
// detectors, independent of these passes (confirmed byte-identical on
|
|
40493
|
+
// excalidraw + mui-material + sentry). tsConfigPath stays — the module resolver
|
|
40494
|
+
// needs it for path-alias resolution in the import graph.
|
|
40495
|
+
semantic: { enabled: false },
|
|
40496
|
+
reportCodeQuality: false,
|
|
40164
40497
|
};
|
|
40165
40498
|
const result = await analyze(defineConfig(config));
|
|
40166
40499
|
emit({ ok: true, result: normalizeResult(result) });
|
|
@@ -40290,7 +40623,11 @@ const createDeadCodeWorker = (input) => {
|
|
|
40290
40623
|
"pipe",
|
|
40291
40624
|
"pipe"
|
|
40292
40625
|
],
|
|
40293
|
-
windowsHide: true
|
|
40626
|
+
windowsHide: true,
|
|
40627
|
+
env: input.parseConcurrency === void 0 ? process.env : {
|
|
40628
|
+
...process.env,
|
|
40629
|
+
DESLOP_PARSE_CONCURRENCY: String(input.parseConcurrency)
|
|
40630
|
+
}
|
|
40294
40631
|
});
|
|
40295
40632
|
const stdoutChunks = [];
|
|
40296
40633
|
const stderrChunks = [];
|
|
@@ -40335,28 +40672,25 @@ const createDeadCodeWorker = (input) => {
|
|
|
40335
40672
|
}
|
|
40336
40673
|
};
|
|
40337
40674
|
};
|
|
40338
|
-
const runDeadCodeWorkerWithTimeout = (handle, timeoutMs) => new Promise((resolve, reject) => {
|
|
40675
|
+
const runDeadCodeWorkerWithTimeout = (handle, timeoutMs, abortSignal) => new Promise((resolve, reject) => {
|
|
40339
40676
|
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) => {
|
|
40677
|
+
const settle = (finish) => {
|
|
40348
40678
|
if (didSettle) return;
|
|
40349
40679
|
didSettle = true;
|
|
40350
40680
|
clearTimeout(timeoutHandle);
|
|
40681
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
40351
40682
|
handle.terminate?.();
|
|
40352
|
-
|
|
40353
|
-
}
|
|
40354
|
-
|
|
40355
|
-
|
|
40356
|
-
|
|
40357
|
-
|
|
40358
|
-
|
|
40359
|
-
|
|
40683
|
+
finish();
|
|
40684
|
+
};
|
|
40685
|
+
const onAbort = () => settle(() => reject(/* @__PURE__ */ new Error("Dead-code worker aborted.")));
|
|
40686
|
+
const timeoutHandle = setTimeout(() => settle(() => reject(/* @__PURE__ */ new Error(`Dead-code worker timed out after ${timeoutMs / MILLISECONDS_PER_SECOND}s.`))), timeoutMs);
|
|
40687
|
+
timeoutHandle.unref?.();
|
|
40688
|
+
if (abortSignal?.aborted) {
|
|
40689
|
+
onAbort();
|
|
40690
|
+
return;
|
|
40691
|
+
}
|
|
40692
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
40693
|
+
handle.result.then((value) => settle(() => resolve(value)), (error) => settle(() => reject(error)));
|
|
40360
40694
|
});
|
|
40361
40695
|
const checkDeadCode = async (options) => {
|
|
40362
40696
|
const rootDirectory = toCanonicalPath(options.rootDirectory);
|
|
@@ -40368,8 +40702,9 @@ const checkDeadCode = async (options) => {
|
|
|
40368
40702
|
entryPatterns,
|
|
40369
40703
|
tsConfigPath: resolveTsConfigPath(rootDirectory),
|
|
40370
40704
|
ignorePatterns,
|
|
40371
|
-
deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js")
|
|
40372
|
-
|
|
40705
|
+
deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
|
|
40706
|
+
parseConcurrency: options.parseConcurrency
|
|
40707
|
+
}), options.workerTimeoutMs ?? 12e4, options.abortSignal));
|
|
40373
40708
|
const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
|
|
40374
40709
|
const diagnostics = [];
|
|
40375
40710
|
for (const unusedFile of result.unusedFiles) diagnostics.push({
|
|
@@ -40467,7 +40802,37 @@ const isDiagnosticOnSurface = (diagnostic, surface, config) => {
|
|
|
40467
40802
|
return true;
|
|
40468
40803
|
};
|
|
40469
40804
|
const filterDiagnosticsForSurface = (diagnostics, surface, config) => diagnostics.filter((diagnostic) => isDiagnosticOnSurface(diagnostic, surface, config));
|
|
40470
|
-
|
|
40805
|
+
/**
|
|
40806
|
+
* Budget for the dead-code phase, scaled to the work. deslop's graph build is
|
|
40807
|
+
* CPU-bound and roughly linear in file count, so a fixed 120s cap is too tight
|
|
40808
|
+
* for a large repo (where the pass legitimately runs that long) and is then
|
|
40809
|
+
* tipped over by any concurrent load — silently dropping every dead-code
|
|
40810
|
+
* finding. Scaling the budget with file count (and inversely with the core
|
|
40811
|
+
* share when overlapped) lets the pass complete, while the ceiling still
|
|
40812
|
+
* reclaims a genuinely wedged worker. Returns the in-worker SIGKILL deadline
|
|
40813
|
+
* and the Effect-side phase backstop that sits a margin above it.
|
|
40814
|
+
*/
|
|
40815
|
+
const resolveDeadCodeTimeout = (input) => {
|
|
40816
|
+
const coreShareFactor = Math.max(1, input.fullConcurrency / Math.max(1, input.deadCodeConcurrency));
|
|
40817
|
+
const workerTimeoutMs = Math.min(DEAD_CODE_TIMEOUT_CEILING_MS, Math.max(DEAD_CODE_WORKER_TIMEOUT_MS, Math.ceil(input.sourceFileCount * 30 * coreShareFactor)));
|
|
40818
|
+
return {
|
|
40819
|
+
workerTimeoutMs,
|
|
40820
|
+
phaseTimeoutMs: workerTimeoutMs + DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS
|
|
40821
|
+
};
|
|
40822
|
+
};
|
|
40823
|
+
const collectSizedSourceFiles = (rootDirectory, relativePaths) => {
|
|
40824
|
+
const entries = [];
|
|
40825
|
+
for (const relativePath of relativePaths) {
|
|
40826
|
+
const absolutePath = Path.resolve(rootDirectory, relativePath);
|
|
40827
|
+
const sizeBytes = statSourceFileSize(absolutePath);
|
|
40828
|
+
if (isLargeMinifiedFile(absolutePath, sizeBytes)) continue;
|
|
40829
|
+
entries.push({
|
|
40830
|
+
path: relativePath,
|
|
40831
|
+
sizeBytes: sizeBytes ?? 0
|
|
40832
|
+
});
|
|
40833
|
+
}
|
|
40834
|
+
return entries;
|
|
40835
|
+
};
|
|
40471
40836
|
const listSourceFilesViaGit = (rootDirectory) => {
|
|
40472
40837
|
const result = spawnSync("git", [
|
|
40473
40838
|
"ls-files",
|
|
@@ -40500,7 +40865,8 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
|
|
|
40500
40865
|
}
|
|
40501
40866
|
return filePaths;
|
|
40502
40867
|
};
|
|
40503
|
-
const
|
|
40868
|
+
const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory));
|
|
40869
|
+
const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
|
|
40504
40870
|
const resolveLintIncludePaths = (rootDirectory, userConfig, project) => {
|
|
40505
40871
|
if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
|
|
40506
40872
|
const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
|
|
@@ -40543,9 +40909,12 @@ var Config = class Config extends Service()("react-doctor/Config") {
|
|
|
40543
40909
|
var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
|
|
40544
40910
|
static layerNode = succeed$3(DeadCode, DeadCode.of({ run: (input) => unwrap(fn("DeadCode.run")(function* () {
|
|
40545
40911
|
return yield* tryPromise({
|
|
40546
|
-
try: () => checkDeadCode({
|
|
40912
|
+
try: (signal) => checkDeadCode({
|
|
40547
40913
|
rootDirectory: input.rootDirectory,
|
|
40548
|
-
userConfig: input.userConfig
|
|
40914
|
+
userConfig: input.userConfig,
|
|
40915
|
+
parseConcurrency: input.parseConcurrency,
|
|
40916
|
+
workerTimeoutMs: input.workerTimeoutMs,
|
|
40917
|
+
abortSignal: signal
|
|
40549
40918
|
}),
|
|
40550
40919
|
catch: (cause) => new ReactDoctorError({ reason: new DeadCodeAnalysisFailed({ cause }) })
|
|
40551
40920
|
}).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)));
|
|
@@ -40736,43 +41105,46 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40736
41105
|
* reason: GitInvocationFailed })` so the rest of the codebase
|
|
40737
41106
|
* sees a single failure channel.
|
|
40738
41107
|
*/
|
|
40739
|
-
const runCommand = (input) =>
|
|
40740
|
-
const
|
|
40741
|
-
cwd: input.directory,
|
|
40742
|
-
env: input.env,
|
|
40743
|
-
extendEnv: true
|
|
40744
|
-
}));
|
|
40745
|
-
const maxStdoutBytes = input.maxStdoutBytes;
|
|
40746
|
-
const stdoutByteCount = yield* make$13(0);
|
|
40747
|
-
const [stdout, stderr, status] = yield* all([
|
|
40748
|
-
mkString(decodeText(maxStdoutBytes === void 0 ? handle.stdout : handle.stdout.pipe(tap((chunk) => updateAndGet(stdoutByteCount, (total) => total + chunk.length).pipe(flatMap$2((total) => total > maxStdoutBytes ? fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
|
|
40749
|
-
args: [...input.args],
|
|
40750
|
-
directory: input.directory,
|
|
40751
|
-
cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
|
|
40752
|
-
}) })) : void_)))))),
|
|
40753
|
-
mkString(decodeText(handle.stderr)),
|
|
40754
|
-
handle.exitCode
|
|
40755
|
-
], { concurrency: 3 });
|
|
40756
|
-
return {
|
|
40757
|
-
status,
|
|
40758
|
-
stdout,
|
|
40759
|
-
stderr
|
|
40760
|
-
};
|
|
40761
|
-
})).pipe(catchTag$1("PlatformError", (cause) => {
|
|
40762
|
-
if (input.command !== "git") return succeed$2({
|
|
41108
|
+
const runCommand = (input) => {
|
|
41109
|
+
const foldSpawnFailure = (cause) => input.command !== "git" ? succeed$2({
|
|
40763
41110
|
status: 127,
|
|
40764
41111
|
stdout: "",
|
|
40765
41112
|
stderr: String(cause)
|
|
40766
|
-
})
|
|
40767
|
-
return new ReactDoctorError({ reason: new GitInvocationFailed({
|
|
41113
|
+
}) : fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
|
|
40768
41114
|
args: [...input.args],
|
|
40769
41115
|
directory: input.directory,
|
|
40770
41116
|
cause
|
|
40771
|
-
}) });
|
|
40772
|
-
|
|
40773
|
-
|
|
40774
|
-
|
|
40775
|
-
|
|
41117
|
+
}) }));
|
|
41118
|
+
return scoped(gen(function* () {
|
|
41119
|
+
if (!isDirectory(input.directory)) return yield* foldSpawnFailure(`spawn ENOTDIR (cwd is not a directory: ${input.directory})`);
|
|
41120
|
+
const argvLengthChars = input.command.length + 1 + input.args.reduce((total, arg) => total + arg.length + 1, 0);
|
|
41121
|
+
if (argvLengthChars > 24e3) return yield* foldSpawnFailure(`spawn ENAMETOOLONG (${argvLengthChars} argv chars exceed ${SPAWN_ARGS_MAX_LENGTH_CHARS})`);
|
|
41122
|
+
const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
|
|
41123
|
+
cwd: input.directory,
|
|
41124
|
+
env: input.env,
|
|
41125
|
+
extendEnv: true
|
|
41126
|
+
}));
|
|
41127
|
+
const maxStdoutBytes = input.maxStdoutBytes;
|
|
41128
|
+
const stdoutByteCount = yield* make$13(0);
|
|
41129
|
+
const [stdout, stderr, status] = yield* all([
|
|
41130
|
+
mkString(decodeText(maxStdoutBytes === void 0 ? handle.stdout : handle.stdout.pipe(tap((chunk) => updateAndGet(stdoutByteCount, (total) => total + chunk.length).pipe(flatMap$2((total) => total > maxStdoutBytes ? fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
|
|
41131
|
+
args: [...input.args],
|
|
41132
|
+
directory: input.directory,
|
|
41133
|
+
cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
|
|
41134
|
+
}) })) : void_)))))),
|
|
41135
|
+
mkString(decodeText(handle.stderr)),
|
|
41136
|
+
handle.exitCode
|
|
41137
|
+
], { concurrency: 3 });
|
|
41138
|
+
return {
|
|
41139
|
+
status,
|
|
41140
|
+
stdout,
|
|
41141
|
+
stderr
|
|
41142
|
+
};
|
|
41143
|
+
})).pipe(catchTag$1("PlatformError", foldSpawnFailure), withSpan("git.exec", { attributes: {
|
|
41144
|
+
"git.command": input.command,
|
|
41145
|
+
"git.subcommand": input.args[0] ?? ""
|
|
41146
|
+
} }));
|
|
41147
|
+
};
|
|
40776
41148
|
const runGit = (directory, args) => runCommand({
|
|
40777
41149
|
command: "git",
|
|
40778
41150
|
args,
|
|
@@ -40805,7 +41177,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40805
41177
|
"rev-parse",
|
|
40806
41178
|
"--verify",
|
|
40807
41179
|
branch
|
|
40808
|
-
]).pipe(map$3((result) => result.status === 0));
|
|
41180
|
+
]).pipe(map$3((result) => result.status === 0), catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(false) : fail$4(error)));
|
|
40809
41181
|
const headSha = (directory) => runGit(directory, ["rev-parse", "HEAD"]).pipe(map$3((result) => result.status === 0 ? trimOrNull(result.stdout) : null));
|
|
40810
41182
|
const mergeBase = (input) => isSafeGitRevision(input.ref) ? runGit(input.directory, [
|
|
40811
41183
|
"merge-base",
|
|
@@ -41019,7 +41391,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
41019
41391
|
]);
|
|
41020
41392
|
if (result.status !== 0) return null;
|
|
41021
41393
|
return parseChangedLineRanges(result.stdout);
|
|
41022
|
-
}).pipe(withSpan("Git.changedLineRanges"))
|
|
41394
|
+
}).pipe(catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(null) : fail$4(error)), withSpan("Git.changedLineRanges"))
|
|
41023
41395
|
});
|
|
41024
41396
|
})).pipe(provide$2(layer$3.pipe(provide$2(mergeAll$1(layer$2, layer$1)))));
|
|
41025
41397
|
/**
|
|
@@ -41258,6 +41630,14 @@ const neutralizeDisableDirectives = async (rootDirectory, includePaths) => {
|
|
|
41258
41630
|
process.removeListener("exit", onExit);
|
|
41259
41631
|
};
|
|
41260
41632
|
};
|
|
41633
|
+
const ROOT_DIRECTORY_PLACEHOLDER = "<root>";
|
|
41634
|
+
const normalizeConfigForHash = (config) => {
|
|
41635
|
+
const clone = JSON.parse(JSON.stringify(config));
|
|
41636
|
+
if (clone?.settings?.["react-doctor"]) clone.settings["react-doctor"].rootDirectory = ROOT_DIRECTORY_PLACEHOLDER;
|
|
41637
|
+
if (Array.isArray(clone?.jsPlugins)) clone.jsPlugins = clone.jsPlugins.map((_, index) => `<plugin:${index}>`);
|
|
41638
|
+
return clone;
|
|
41639
|
+
};
|
|
41640
|
+
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
41641
|
/**
|
|
41262
41642
|
* Loads a plugin module via the local require resolver and extracts
|
|
41263
41643
|
* `(name, ruleNames)` from either `module.exports.meta + rules` or
|
|
@@ -41412,8 +41792,8 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
|
|
|
41412
41792
|
}
|
|
41413
41793
|
return enabled;
|
|
41414
41794
|
};
|
|
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);
|
|
41795
|
+
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection }) => {
|
|
41796
|
+
const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
|
|
41417
41797
|
const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
|
|
41418
41798
|
const jsPlugins = [];
|
|
41419
41799
|
if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
|
|
@@ -41422,6 +41802,8 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
41422
41802
|
for (const registryEntry of REACT_DOCTOR_RULES) {
|
|
41423
41803
|
const rule = reactDoctorPlugin.rules[registryEntry.id];
|
|
41424
41804
|
if (!rule) continue;
|
|
41805
|
+
if (ruleSelection === "cacheable" && CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
|
|
41806
|
+
if (ruleSelection === "sidecar" && !CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
|
|
41425
41807
|
if (rule.scan !== void 0) continue;
|
|
41426
41808
|
if (customRulesOnly && registryEntry.originallyExternal) continue;
|
|
41427
41809
|
if (rule.framework !== "global" && !rule.requires) continue;
|
|
@@ -41436,7 +41818,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
41436
41818
|
enabledReactDoctorRules[registryEntry.key] = severity;
|
|
41437
41819
|
}
|
|
41438
41820
|
const userPluginRules = {};
|
|
41439
|
-
for (const userPlugin of userPlugins) {
|
|
41821
|
+
if (ruleSelection !== "sidecar") for (const userPlugin of userPlugins) {
|
|
41440
41822
|
Object.assign(userPluginRules, buildUserPluginRules(userPlugin, severityControls));
|
|
41441
41823
|
jsPlugins.push(userPlugin.entry);
|
|
41442
41824
|
}
|
|
@@ -41466,6 +41848,100 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
41466
41848
|
}
|
|
41467
41849
|
};
|
|
41468
41850
|
};
|
|
41851
|
+
const atomicWriteJson = (filePath, value) => {
|
|
41852
|
+
try {
|
|
41853
|
+
NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
|
|
41854
|
+
const temporaryPath = `${filePath}.${process.pid}.tmp`;
|
|
41855
|
+
NFS.writeFileSync(temporaryPath, JSON.stringify(value));
|
|
41856
|
+
NFS.renameSync(temporaryPath, filePath);
|
|
41857
|
+
} catch {
|
|
41858
|
+
return;
|
|
41859
|
+
}
|
|
41860
|
+
};
|
|
41861
|
+
const failOpenReadJson = (filePath, fallback) => {
|
|
41862
|
+
try {
|
|
41863
|
+
return JSON.parse(NFS.readFileSync(filePath, "utf8"));
|
|
41864
|
+
} catch {
|
|
41865
|
+
return fallback;
|
|
41866
|
+
}
|
|
41867
|
+
};
|
|
41868
|
+
const validateDiagnostic = decodeUnknownSync(Diagnostic);
|
|
41869
|
+
const decodeFileDiagnostics = (raw) => {
|
|
41870
|
+
if (!Array.isArray(raw)) return null;
|
|
41871
|
+
try {
|
|
41872
|
+
for (const entry of raw) validateDiagnostic(entry);
|
|
41873
|
+
return raw;
|
|
41874
|
+
} catch {
|
|
41875
|
+
return null;
|
|
41876
|
+
}
|
|
41877
|
+
};
|
|
41878
|
+
const emptyCache = () => ({
|
|
41879
|
+
version: 1,
|
|
41880
|
+
rulesets: {}
|
|
41881
|
+
});
|
|
41882
|
+
const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
|
|
41883
|
+
const entries = /* @__PURE__ */ new Map();
|
|
41884
|
+
const persisted = failOpenReadJson(cacheFilePath, emptyCache());
|
|
41885
|
+
if (persisted.version !== 1 || !isRecord$2(persisted.rulesets)) return entries;
|
|
41886
|
+
const bucket = persisted.rulesets[rulesetHash];
|
|
41887
|
+
if (!isRecord$2(bucket) || !isRecord$2(bucket.files)) return entries;
|
|
41888
|
+
for (const [fileKey, rawDiagnostics] of Object.entries(bucket.files)) {
|
|
41889
|
+
const decoded = decodeFileDiagnostics(rawDiagnostics);
|
|
41890
|
+
if (decoded !== null) entries.set(fileKey, decoded);
|
|
41891
|
+
}
|
|
41892
|
+
return entries;
|
|
41893
|
+
};
|
|
41894
|
+
const createFileLintCache = (cacheDirectory, rulesetHash) => {
|
|
41895
|
+
const cacheFilePath = Path.join(cacheDirectory, FILE_LINT_CACHE_FILENAME);
|
|
41896
|
+
const entries = loadRulesetEntries(cacheFilePath, rulesetHash);
|
|
41897
|
+
return {
|
|
41898
|
+
lookup: (fileKey) => entries.get(fileKey) ?? null,
|
|
41899
|
+
store: (fileKey, diagnostics) => {
|
|
41900
|
+
entries.delete(fileKey);
|
|
41901
|
+
entries.set(fileKey, diagnostics);
|
|
41902
|
+
},
|
|
41903
|
+
persist: () => {
|
|
41904
|
+
const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
|
|
41905
|
+
const rulesets = onDisk.version === 1 && isRecord$2(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
|
|
41906
|
+
const existingBucket = rulesets[rulesetHash];
|
|
41907
|
+
const existingFiles = isRecord$2(existingBucket) && isRecord$2(existingBucket.files) ? existingBucket.files : {};
|
|
41908
|
+
const ourFiles = {};
|
|
41909
|
+
for (const [fileKey, diagnostics] of entries) ourFiles[fileKey] = diagnostics;
|
|
41910
|
+
const cappedEntries = Object.entries({
|
|
41911
|
+
...existingFiles,
|
|
41912
|
+
...ourFiles
|
|
41913
|
+
}).slice(-FILE_LINT_CACHE_MAX_FILE_COUNT);
|
|
41914
|
+
rulesets[rulesetHash] = {
|
|
41915
|
+
updatedAtMs: Date.now(),
|
|
41916
|
+
files: Object.fromEntries(cappedEntries)
|
|
41917
|
+
};
|
|
41918
|
+
const keptHashes = Object.entries(rulesets).sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8).map(([hash]) => hash);
|
|
41919
|
+
const prunedRulesets = {};
|
|
41920
|
+
for (const hash of keptHashes) prunedRulesets[hash] = rulesets[hash];
|
|
41921
|
+
atomicWriteJson(cacheFilePath, {
|
|
41922
|
+
version: 1,
|
|
41923
|
+
rulesets: prunedRulesets
|
|
41924
|
+
});
|
|
41925
|
+
}
|
|
41926
|
+
};
|
|
41927
|
+
};
|
|
41928
|
+
const bundledRequire$2 = createRequire(import.meta.url);
|
|
41929
|
+
const TOOLCHAIN_PACKAGE_SPECIFIERS$1 = [
|
|
41930
|
+
"oxlint/package.json",
|
|
41931
|
+
"oxlint-plugin-react-doctor/package.json",
|
|
41932
|
+
"eslint-plugin-react-hooks/package.json"
|
|
41933
|
+
];
|
|
41934
|
+
const resolveOxlintToolchainVersions = () => {
|
|
41935
|
+
const versions = [`node=${process.version}`];
|
|
41936
|
+
for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS$1) try {
|
|
41937
|
+
const packageJson = bundledRequire$2(specifier);
|
|
41938
|
+
const version = typeof packageJson.version === "string" ? packageJson.version : "unknown";
|
|
41939
|
+
versions.push(`${specifier}=${version}`);
|
|
41940
|
+
} catch {
|
|
41941
|
+
versions.push(`${specifier}=missing`);
|
|
41942
|
+
}
|
|
41943
|
+
return versions;
|
|
41944
|
+
};
|
|
41469
41945
|
const esmRequire = createRequire(import.meta.url);
|
|
41470
41946
|
const resolveOxlintBinary = () => {
|
|
41471
41947
|
const oxlintMainPath = esmRequire.resolve("oxlint");
|
|
@@ -42147,15 +42623,19 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
|
|
|
42147
42623
|
};
|
|
42148
42624
|
});
|
|
42149
42625
|
};
|
|
42150
|
-
const
|
|
42151
|
-
const
|
|
42152
|
-
for (const [name, value] of Object.entries(
|
|
42626
|
+
const buildOxlintChildEnv = (sourceEnv) => {
|
|
42627
|
+
const childEnv = {};
|
|
42628
|
+
for (const [name, value] of Object.entries(sourceEnv)) {
|
|
42153
42629
|
if (name === "NODE_OPTIONS" || name === "NODE_DEBUG") continue;
|
|
42154
42630
|
if (name.startsWith("npm_config_")) continue;
|
|
42155
|
-
|
|
42631
|
+
childEnv[name] = value;
|
|
42156
42632
|
}
|
|
42157
|
-
|
|
42158
|
-
|
|
42633
|
+
const isCompileCacheDisabled = Boolean(sourceEnv.NODE_DISABLE_COMPILE_CACHE);
|
|
42634
|
+
const isCompileCacheAlreadySet = childEnv.NODE_COMPILE_CACHE !== void 0;
|
|
42635
|
+
if (!isCompileCacheDisabled && !isCompileCacheAlreadySet) childEnv.NODE_COMPILE_CACHE = Path.join(os.tmpdir(), NODE_COMPILE_CACHE_DIR_NAME);
|
|
42636
|
+
return childEnv;
|
|
42637
|
+
};
|
|
42638
|
+
const SANITIZED_ENV = buildOxlintChildEnv(process.env);
|
|
42159
42639
|
/**
|
|
42160
42640
|
* Spawn one oxlint subprocess with hard ceilings on wall time and
|
|
42161
42641
|
* output size. Returns stdout on success; raises a tagged
|
|
@@ -42172,7 +42652,11 @@ const SANITIZED_ENV = (() => {
|
|
|
42172
42652
|
* The first three are splittable (the caller's binary-split retry
|
|
42173
42653
|
* shrinks the batch and re-spawns); the fourth isn't.
|
|
42174
42654
|
*/
|
|
42175
|
-
const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLINT_SPAWN_TIMEOUT_MS, outputMaxBytes = OXLINT_OUTPUT_MAX_BYTES) => new Promise((resolve, reject) => {
|
|
42655
|
+
const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLINT_SPAWN_TIMEOUT_MS, outputMaxBytes = OXLINT_OUTPUT_MAX_BYTES, abortSignal) => new Promise((resolve, reject) => {
|
|
42656
|
+
if (abortSignal?.aborted) {
|
|
42657
|
+
reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: "lint phase aborted" }) }));
|
|
42658
|
+
return;
|
|
42659
|
+
}
|
|
42176
42660
|
const child = spawn(nodeBinaryPath, args, {
|
|
42177
42661
|
cwd: rootDirectory,
|
|
42178
42662
|
env: SANITIZED_ENV,
|
|
@@ -42182,7 +42666,14 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
|
|
|
42182
42666
|
"pipe"
|
|
42183
42667
|
]
|
|
42184
42668
|
});
|
|
42669
|
+
const onAbort = () => {
|
|
42670
|
+
child.kill("SIGKILL");
|
|
42671
|
+
reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: "lint phase aborted" }) }));
|
|
42672
|
+
};
|
|
42673
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
42674
|
+
const clearAbortListener = () => abortSignal?.removeEventListener("abort", onAbort);
|
|
42185
42675
|
const timeoutHandle = setTimeout(() => {
|
|
42676
|
+
clearAbortListener();
|
|
42186
42677
|
child.kill("SIGKILL");
|
|
42187
42678
|
reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
|
|
42188
42679
|
kind: "timeout",
|
|
@@ -42217,10 +42708,12 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
|
|
|
42217
42708
|
});
|
|
42218
42709
|
child.on("error", (error) => {
|
|
42219
42710
|
clearTimeout(timeoutHandle);
|
|
42711
|
+
clearAbortListener();
|
|
42220
42712
|
reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: error }) }));
|
|
42221
42713
|
});
|
|
42222
42714
|
child.on("close", (_code, signal) => {
|
|
42223
42715
|
clearTimeout(timeoutHandle);
|
|
42716
|
+
clearAbortListener();
|
|
42224
42717
|
if (didKillForSize) {
|
|
42225
42718
|
reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
|
|
42226
42719
|
kind: "output-too-large",
|
|
@@ -42287,26 +42780,28 @@ const isParallelismRelatedSpawnError = (error) => {
|
|
|
42287
42780
|
* loop with a slimmer config in that case.
|
|
42288
42781
|
*/
|
|
42289
42782
|
const spawnLintBatches = async (input) => {
|
|
42290
|
-
const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes } = input;
|
|
42783
|
+
const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 8, signal } = input;
|
|
42291
42784
|
const requestedConcurrency = resolveScanConcurrency(input.concurrency ?? 1);
|
|
42292
42785
|
const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
|
|
42293
42786
|
const runBatchPass = async (concurrency) => {
|
|
42294
42787
|
const allDiagnostics = [];
|
|
42295
42788
|
const droppedFiles = [];
|
|
42296
42789
|
let firstDropReason = null;
|
|
42297
|
-
const
|
|
42790
|
+
const splitDeadlineMs = Date.now() + splitTotalBudgetMs;
|
|
42791
|
+
const spawnLintBatch = async (batch, depth) => {
|
|
42298
42792
|
const batchArgs = [...baseArgs, ...batch];
|
|
42299
42793
|
try {
|
|
42300
|
-
return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes), project, rootDirectory);
|
|
42794
|
+
return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes, signal), project, rootDirectory);
|
|
42301
42795
|
} catch (error) {
|
|
42302
42796
|
if (!isSplittableReactDoctorError(error)) throw error;
|
|
42303
|
-
|
|
42797
|
+
const splitBudgetExhausted = Date.now() >= splitDeadlineMs || depth >= splitMaxDepth;
|
|
42798
|
+
if (batch.length <= 1 || splitBudgetExhausted) {
|
|
42304
42799
|
droppedFiles.push(...batch);
|
|
42305
|
-
if (firstDropReason === null) firstDropReason = error.message;
|
|
42800
|
+
if (firstDropReason === null) firstDropReason = splitBudgetExhausted && batch.length > 1 ? `${error.message} (split budget exhausted after ${splitMaxDepth} levels / ${splitTotalBudgetMs / MILLISECONDS_PER_SECOND}s)` : error.message;
|
|
42306
42801
|
return [];
|
|
42307
42802
|
}
|
|
42308
42803
|
const splitIndex = Math.ceil(batch.length / 2);
|
|
42309
|
-
return [...await spawnLintBatch(batch.slice(0, splitIndex)), ...await spawnLintBatch(batch.slice(splitIndex))];
|
|
42804
|
+
return [...await spawnLintBatch(batch.slice(0, splitIndex), depth + 1), ...await spawnLintBatch(batch.slice(splitIndex), depth + 1)];
|
|
42310
42805
|
}
|
|
42311
42806
|
};
|
|
42312
42807
|
let startedFileCount = 0;
|
|
@@ -42323,7 +42818,7 @@ const spawnLintBatches = async (input) => {
|
|
|
42323
42818
|
try {
|
|
42324
42819
|
const batchResults = await mapWithConcurrency(fileBatches, concurrency, async (batch) => {
|
|
42325
42820
|
startedFileCount += batch.length;
|
|
42326
|
-
const batchDiagnostics = await spawnLintBatch(batch);
|
|
42821
|
+
const batchDiagnostics = await spawnLintBatch(batch, 0);
|
|
42327
42822
|
scannedFileCount += batch.length;
|
|
42328
42823
|
if (onFileProgress) {
|
|
42329
42824
|
displayedFileCount = Math.min(Math.max(displayedFileCount, scannedFileCount), totalFileCount);
|
|
@@ -42384,6 +42879,22 @@ const validateRuleRegistration = () => {
|
|
|
42384
42879
|
].filter((entry) => entry !== null).join("; ");
|
|
42385
42880
|
console.warn(`[react-doctor] rule-registration drift: ${detail}`);
|
|
42386
42881
|
};
|
|
42882
|
+
const hashFileContents = (filePath) => {
|
|
42883
|
+
try {
|
|
42884
|
+
return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
|
|
42885
|
+
} catch {
|
|
42886
|
+
return null;
|
|
42887
|
+
}
|
|
42888
|
+
};
|
|
42889
|
+
const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
|
|
42890
|
+
const resolveReactDoctorCacheDir = (projectDirectory) => {
|
|
42891
|
+
const cacheDirOverride = process.env["REACT_DOCTOR_CACHE_DIR"]?.trim();
|
|
42892
|
+
if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
|
|
42893
|
+
const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
|
|
42894
|
+
if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
|
|
42895
|
+
return Path.join(os.tmpdir(), "react-doctor-cache", projectCacheSubdir(projectDirectory));
|
|
42896
|
+
};
|
|
42897
|
+
const sortSourceFilesByCost = (entries) => [...entries].sort((left, right) => right.sizeBytes - left.sizeBytes).map((entry) => entry.path);
|
|
42387
42898
|
/**
|
|
42388
42899
|
* Atomically (re)writes the generated oxlintrc.json. Used twice in
|
|
42389
42900
|
* the runner: once for the primary scan, once for the
|
|
@@ -42442,7 +42953,7 @@ const reactHooksJsPluginDropNote = (error) => {
|
|
|
42442
42953
|
* 6. always restore disable directives + clean up the temp dir
|
|
42443
42954
|
*/
|
|
42444
42955
|
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;
|
|
42956
|
+
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
42957
|
const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
|
|
42447
42958
|
const severityControls = buildRuleSeverityControls(userConfig);
|
|
42448
42959
|
validateRuleRegistration();
|
|
@@ -42459,30 +42970,156 @@ const runOxlint = async (options) => {
|
|
|
42459
42970
|
serverAuthFunctionNames,
|
|
42460
42971
|
severityControls,
|
|
42461
42972
|
userPlugins,
|
|
42462
|
-
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
|
|
42973
|
+
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin,
|
|
42974
|
+
ruleSelection: overrides.ruleSelection
|
|
42463
42975
|
});
|
|
42464
42976
|
const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
|
|
42465
42977
|
const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
|
|
42466
42978
|
const configPath = Path.join(configDirectory, "oxlintrc.json");
|
|
42467
42979
|
try {
|
|
42468
|
-
const
|
|
42469
|
-
|
|
42470
|
-
|
|
42471
|
-
configPath,
|
|
42472
|
-
"--format",
|
|
42473
|
-
"json"
|
|
42474
|
-
];
|
|
42980
|
+
const oxlintBinary = resolveOxlintBinary();
|
|
42981
|
+
const sharedArgs = [];
|
|
42982
|
+
let tsconfigContent = null;
|
|
42475
42983
|
if (project.hasTypeScript) {
|
|
42476
42984
|
const tsconfigRelativePath = resolveTsConfigRelativePath(rootDirectory);
|
|
42477
|
-
if (tsconfigRelativePath)
|
|
42985
|
+
if (tsconfigRelativePath) {
|
|
42986
|
+
sharedArgs.push("--tsconfig", tsconfigRelativePath);
|
|
42987
|
+
try {
|
|
42988
|
+
tsconfigContent = NFS.readFileSync(Path.resolve(rootDirectory, tsconfigRelativePath), "utf8");
|
|
42989
|
+
} catch {
|
|
42990
|
+
tsconfigContent = null;
|
|
42991
|
+
}
|
|
42992
|
+
}
|
|
42478
42993
|
}
|
|
42479
42994
|
const combinedPatterns = collectIgnorePatterns(rootDirectory);
|
|
42480
42995
|
if (combinedPatterns.length > 0) {
|
|
42481
42996
|
const combinedIgnorePath = Path.join(configDirectory, "combined.ignore");
|
|
42482
42997
|
NFS.writeFileSync(combinedIgnorePath, `${combinedPatterns.join("\n")}\n`);
|
|
42483
|
-
|
|
42998
|
+
sharedArgs.push("--ignore-path", combinedIgnorePath);
|
|
42484
42999
|
}
|
|
42485
|
-
const
|
|
43000
|
+
const makeBaseArgs = (oxlintConfigPath) => [
|
|
43001
|
+
oxlintBinary,
|
|
43002
|
+
"-c",
|
|
43003
|
+
oxlintConfigPath,
|
|
43004
|
+
"--format",
|
|
43005
|
+
"json",
|
|
43006
|
+
...sharedArgs
|
|
43007
|
+
];
|
|
43008
|
+
const discoverScanFiles = () => lintBatchOrdering === "cost" ? sortSourceFilesByCost(listSourceFilesWithSize(rootDirectory)) : listSourceFiles(rootDirectory);
|
|
43009
|
+
const candidateFiles = includePaths !== void 0 ? includePaths : discoverScanFiles();
|
|
43010
|
+
const runConfigOverFiles = async (buildConfigForPass, configFileName, files, fileProgress) => {
|
|
43011
|
+
if (files.length === 0) return {
|
|
43012
|
+
diagnostics: [],
|
|
43013
|
+
didDropReactHooksJsPlugin: false,
|
|
43014
|
+
hadPartialFailure: false
|
|
43015
|
+
};
|
|
43016
|
+
let hadPartialFailure = false;
|
|
43017
|
+
const reportPartialFailure = (reason) => {
|
|
43018
|
+
hadPartialFailure = true;
|
|
43019
|
+
onPartialFailure?.(reason);
|
|
43020
|
+
};
|
|
43021
|
+
const passConfigPath = Path.join(configDirectory, configFileName);
|
|
43022
|
+
const passBaseArgs = makeBaseArgs(passConfigPath);
|
|
43023
|
+
const passFileBatches = batchIncludePaths(passBaseArgs, files);
|
|
43024
|
+
const spawnPass = () => spawnLintBatches({
|
|
43025
|
+
baseArgs: passBaseArgs,
|
|
43026
|
+
fileBatches: passFileBatches,
|
|
43027
|
+
rootDirectory,
|
|
43028
|
+
nodeBinaryPath,
|
|
43029
|
+
project,
|
|
43030
|
+
onPartialFailure: reportPartialFailure,
|
|
43031
|
+
onFileProgress: fileProgress,
|
|
43032
|
+
spawnTimeoutMs,
|
|
43033
|
+
outputMaxBytes,
|
|
43034
|
+
concurrency: options.concurrency,
|
|
43035
|
+
signal: options.signal
|
|
43036
|
+
});
|
|
43037
|
+
writeOxlintConfig(passConfigPath, buildConfigForPass({}));
|
|
43038
|
+
try {
|
|
43039
|
+
return {
|
|
43040
|
+
diagnostics: await spawnPass(),
|
|
43041
|
+
didDropReactHooksJsPlugin: false,
|
|
43042
|
+
hadPartialFailure
|
|
43043
|
+
};
|
|
43044
|
+
} catch (error) {
|
|
43045
|
+
const reactHooksJsDropNote = reactHooksJsPluginDropNote(error);
|
|
43046
|
+
if (reactHooksJsDropNote === null) throw error;
|
|
43047
|
+
writeOxlintConfig(passConfigPath, buildConfigForPass({ disableReactHooksJsPlugin: true }));
|
|
43048
|
+
const diagnostics = await spawnPass();
|
|
43049
|
+
reportPartialFailure(reactHooksJsDropNote);
|
|
43050
|
+
return {
|
|
43051
|
+
diagnostics,
|
|
43052
|
+
didDropReactHooksJsPlugin: true,
|
|
43053
|
+
hadPartialFailure
|
|
43054
|
+
};
|
|
43055
|
+
}
|
|
43056
|
+
};
|
|
43057
|
+
if (perFileLintCacheEnabled && respectInlineDisables && !project.hasReactCompiler && extendsPaths.length === 0 && userPlugins.length === 0) {
|
|
43058
|
+
const rulesetHash = computeRulesetHash({
|
|
43059
|
+
config: buildConfig({
|
|
43060
|
+
extendsPaths: [],
|
|
43061
|
+
ruleSelection: "cacheable"
|
|
43062
|
+
}),
|
|
43063
|
+
toolchainVersions: resolveOxlintToolchainVersions(),
|
|
43064
|
+
ignorePatterns: combinedPatterns,
|
|
43065
|
+
tsconfigContent
|
|
43066
|
+
});
|
|
43067
|
+
const cache = createFileLintCache(resolveReactDoctorCacheDir(rootDirectory), rulesetHash);
|
|
43068
|
+
const cacheKeyByFile = /* @__PURE__ */ new Map();
|
|
43069
|
+
const missFiles = [];
|
|
43070
|
+
const replayedDiagnostics = [];
|
|
43071
|
+
for (const candidateFile of candidateFiles) {
|
|
43072
|
+
const contentHash = hashFileContents(Path.resolve(rootDirectory, candidateFile));
|
|
43073
|
+
if (contentHash === null) {
|
|
43074
|
+
missFiles.push(candidateFile);
|
|
43075
|
+
continue;
|
|
43076
|
+
}
|
|
43077
|
+
const cacheKey = `${candidateFile.replaceAll("\\", "/")}${contentHash}`;
|
|
43078
|
+
cacheKeyByFile.set(candidateFile, cacheKey);
|
|
43079
|
+
const cachedDiagnostics = cache.lookup(cacheKey);
|
|
43080
|
+
if (cachedDiagnostics === null) missFiles.push(candidateFile);
|
|
43081
|
+
else replayedDiagnostics.push(...cachedDiagnostics);
|
|
43082
|
+
}
|
|
43083
|
+
const cacheHitFileCount = candidateFiles.length - missFiles.length;
|
|
43084
|
+
const cacheableResult = await runConfigOverFiles((overrides) => buildConfig({
|
|
43085
|
+
extendsPaths: [],
|
|
43086
|
+
ruleSelection: "cacheable",
|
|
43087
|
+
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
|
|
43088
|
+
}), "oxlintrc.cacheable.json", missFiles, void 0);
|
|
43089
|
+
const sidecarResult = await runConfigOverFiles(() => buildConfig({
|
|
43090
|
+
extendsPaths: [],
|
|
43091
|
+
ruleSelection: "sidecar"
|
|
43092
|
+
}), "oxlintrc.sidecar.json", candidateFiles, options.onFileProgress);
|
|
43093
|
+
onCacheStats?.(cacheHitFileCount, candidateFiles.length);
|
|
43094
|
+
const missFileByNormalizedPath = /* @__PURE__ */ new Map();
|
|
43095
|
+
for (const missFile of missFiles) missFileByNormalizedPath.set(missFile.replaceAll("\\", "/"), missFile);
|
|
43096
|
+
const freshDiagnosticsByFile = /* @__PURE__ */ new Map();
|
|
43097
|
+
let isAttributionSound = true;
|
|
43098
|
+
for (const diagnostic of cacheableResult.diagnostics) {
|
|
43099
|
+
const missFile = missFileByNormalizedPath.get(diagnostic.filePath);
|
|
43100
|
+
if (missFile === void 0) {
|
|
43101
|
+
isAttributionSound = false;
|
|
43102
|
+
break;
|
|
43103
|
+
}
|
|
43104
|
+
const fileDiagnostics = freshDiagnosticsByFile.get(missFile) ?? [];
|
|
43105
|
+
fileDiagnostics.push(diagnostic);
|
|
43106
|
+
freshDiagnosticsByFile.set(missFile, fileDiagnostics);
|
|
43107
|
+
}
|
|
43108
|
+
if (!cacheableResult.didDropReactHooksJsPlugin && !cacheableResult.hadPartialFailure && isAttributionSound) {
|
|
43109
|
+
for (const missFile of missFiles) {
|
|
43110
|
+
const cacheKey = cacheKeyByFile.get(missFile);
|
|
43111
|
+
if (cacheKey !== void 0) cache.store(cacheKey, freshDiagnosticsByFile.get(missFile) ?? []);
|
|
43112
|
+
}
|
|
43113
|
+
cache.persist();
|
|
43114
|
+
}
|
|
43115
|
+
return dedupeDiagnostics([
|
|
43116
|
+
...replayedDiagnostics,
|
|
43117
|
+
...cacheableResult.diagnostics,
|
|
43118
|
+
...sidecarResult.diagnostics
|
|
43119
|
+
]);
|
|
43120
|
+
}
|
|
43121
|
+
const baseArgs = makeBaseArgs(configPath);
|
|
43122
|
+
const fileBatches = batchIncludePaths(baseArgs, candidateFiles);
|
|
42486
43123
|
const runBatches = () => spawnLintBatches({
|
|
42487
43124
|
baseArgs,
|
|
42488
43125
|
fileBatches,
|
|
@@ -42493,7 +43130,8 @@ const runOxlint = async (options) => {
|
|
|
42493
43130
|
onFileProgress: options.onFileProgress,
|
|
42494
43131
|
spawnTimeoutMs,
|
|
42495
43132
|
outputMaxBytes,
|
|
42496
|
-
concurrency: options.concurrency
|
|
43133
|
+
concurrency: options.concurrency,
|
|
43134
|
+
signal: options.signal
|
|
42497
43135
|
});
|
|
42498
43136
|
writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
|
|
42499
43137
|
try {
|
|
@@ -42572,9 +43210,11 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
|
|
|
42572
43210
|
const spawnTimeoutMs = yield* OxlintSpawnTimeoutMs;
|
|
42573
43211
|
const outputMaxBytes = yield* OxlintOutputMaxBytes;
|
|
42574
43212
|
const concurrency = yield* OxlintConcurrency;
|
|
43213
|
+
const lintBatchOrdering = yield* LintBatchOrdering;
|
|
43214
|
+
const perFileLintCacheEnabled = yield* PerFileLintCacheEnabled;
|
|
42575
43215
|
const collectedFailures = [];
|
|
42576
43216
|
const diagnostics = yield* tryPromise({
|
|
42577
|
-
try: () => runOxlint({
|
|
43217
|
+
try: (signal) => runOxlint({
|
|
42578
43218
|
rootDirectory: input.rootDirectory,
|
|
42579
43219
|
project: input.project,
|
|
42580
43220
|
includePaths: input.includePaths ? [...input.includePaths] : void 0,
|
|
@@ -42589,9 +43229,13 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
|
|
|
42589
43229
|
collectedFailures.push(reason);
|
|
42590
43230
|
},
|
|
42591
43231
|
onFileProgress: input.onFileProgress,
|
|
43232
|
+
perFileLintCacheEnabled,
|
|
43233
|
+
onCacheStats: input.onCacheStats,
|
|
42592
43234
|
spawnTimeoutMs,
|
|
42593
43235
|
outputMaxBytes,
|
|
42594
|
-
concurrency
|
|
43236
|
+
concurrency,
|
|
43237
|
+
signal,
|
|
43238
|
+
lintBatchOrdering
|
|
42595
43239
|
}),
|
|
42596
43240
|
catch: ensureReactDoctorError
|
|
42597
43241
|
});
|
|
@@ -42983,14 +43627,49 @@ const parseArtifactFromBody = (body) => {
|
|
|
42983
43627
|
}
|
|
42984
43628
|
return null;
|
|
42985
43629
|
};
|
|
42986
|
-
const
|
|
43630
|
+
const isSupplyChainCacheDisabled = () => {
|
|
43631
|
+
const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
|
|
43632
|
+
return noCache === "1" || noCache === "true";
|
|
43633
|
+
};
|
|
43634
|
+
const supplyChainCacheFile = (cacheDirectory, dependency) => {
|
|
43635
|
+
const purlHash = crypto.createHash("sha256").update(toPurl(dependency)).digest("hex").slice(0, 16);
|
|
43636
|
+
return Path.join(cacheDirectory, SUPPLY_CHAIN_CACHE_SUBDIR, `${purlHash}.json`);
|
|
43637
|
+
};
|
|
43638
|
+
const readCachedSocketBody = (cacheFile) => {
|
|
43639
|
+
try {
|
|
43640
|
+
const entry = JSON.parse(NFS.readFileSync(cacheFile, "utf-8"));
|
|
43641
|
+
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;
|
|
43642
|
+
} catch {}
|
|
43643
|
+
return null;
|
|
43644
|
+
};
|
|
43645
|
+
const writeCachedSocketBody = (cacheFile, body) => {
|
|
43646
|
+
try {
|
|
43647
|
+
NFS.mkdirSync(Path.dirname(cacheFile), { recursive: true });
|
|
43648
|
+
NFS.writeFileSync(cacheFile, JSON.stringify({
|
|
43649
|
+
fetchedAtMs: Date.now(),
|
|
43650
|
+
body
|
|
43651
|
+
}));
|
|
43652
|
+
} catch {}
|
|
43653
|
+
};
|
|
43654
|
+
const fetchSocketArtifact = (dependency, cacheDirectory) => tryPromise(async (signal) => {
|
|
43655
|
+
const cacheFile = cacheDirectory === null ? null : supplyChainCacheFile(cacheDirectory, dependency);
|
|
43656
|
+
if (cacheFile !== null) {
|
|
43657
|
+
const cachedBody = readCachedSocketBody(cacheFile);
|
|
43658
|
+
if (cachedBody !== null) {
|
|
43659
|
+
const cachedArtifact = parseArtifactFromBody(cachedBody);
|
|
43660
|
+
if (cachedArtifact !== null) return cachedArtifact;
|
|
43661
|
+
}
|
|
43662
|
+
}
|
|
42987
43663
|
const requestUrl = `${SOCKET_FREE_PURL_API_BASE}/${encodeURIComponent(toPurl(dependency))}`;
|
|
42988
43664
|
const response = await fetch(requestUrl, {
|
|
42989
43665
|
headers: { "User-Agent": SOCKET_FREE_USER_AGENT },
|
|
42990
43666
|
signal
|
|
42991
43667
|
});
|
|
42992
43668
|
if (!response.ok) return null;
|
|
42993
|
-
|
|
43669
|
+
const body = await response.text();
|
|
43670
|
+
const artifact = parseArtifactFromBody(body);
|
|
43671
|
+
if (artifact !== null && cacheFile !== null) writeCachedSocketBody(cacheFile, body);
|
|
43672
|
+
return artifact;
|
|
42994
43673
|
}).pipe(timeout(FETCH_TIMEOUT_MS), orElseSucceed(() => null), tap$1((artifact) => {
|
|
42995
43674
|
const scoreAttributes = {};
|
|
42996
43675
|
if (artifact !== null) {
|
|
@@ -43095,7 +43774,8 @@ const checkSupplyChain = (input) => gen(function* () {
|
|
|
43095
43774
|
const packageJsonPath = Path.join(input.rootDirectory, "package.json");
|
|
43096
43775
|
const dependencies = collectDependenciesToScore(readPackageJson$1(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies);
|
|
43097
43776
|
if (dependencies.length === 0) return [];
|
|
43098
|
-
const
|
|
43777
|
+
const cacheDirectory = isSupplyChainCacheDisabled() ? null : resolveReactDoctorCacheDir(input.rootDirectory);
|
|
43778
|
+
const artifacts = yield* forEach$1(dependencies, (dependency) => fetchSocketArtifact(dependency, cacheDirectory), { concurrency: 8 }).pipe(timeoutOption(input.totalTimeoutMs ?? 9e4), map$3((maybeArtifacts) => getOrElse$1(maybeArtifacts, () => [])));
|
|
43099
43779
|
const diagnostics = [];
|
|
43100
43780
|
for (let index = 0; index < dependencies.length; index += 1) {
|
|
43101
43781
|
const artifact = artifacts[index];
|
|
@@ -43120,6 +43800,10 @@ const checkSupplyChain = (input) => gen(function* () {
|
|
|
43120
43800
|
* The underlying `checkSupplyChain` Effect is total/fail-open — per-package
|
|
43121
43801
|
* timeouts and network failures recover to "skip" — so the stream never
|
|
43122
43802
|
* fails, mirroring `DeadCode`'s stream shape so the two compose the same way.
|
|
43803
|
+
* The orchestrator (`run-inspect.ts`) consumes this stream on a background
|
|
43804
|
+
* fiber whose network time overlaps the lint pass, joined under a generous
|
|
43805
|
+
* wall-clock budget; a budget expiry is the same fail-open outcome as a Socket
|
|
43806
|
+
* outage.
|
|
43123
43807
|
*/
|
|
43124
43808
|
var SupplyChain = class SupplyChain extends Service()("react-doctor/SupplyChain") {
|
|
43125
43809
|
static layerNode = succeed$3(SupplyChain, SupplyChain.of({ run: (input) => unwrap(checkSupplyChain(input).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), withSpan("SupplyChain.run"))) }));
|
|
@@ -43178,18 +43862,42 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
|
|
|
43178
43862
|
*
|
|
43179
43863
|
* Phases:
|
|
43180
43864
|
*
|
|
43181
|
-
* 1. Config.resolve(directory) → Project.discover → Git metadata
|
|
43865
|
+
* 1. Config.resolve(directory) → Project.discover → Git metadata.
|
|
43866
|
+
* The GitHub viewer-permission lookup is forked onto a background
|
|
43867
|
+
* fiber here and joined late (it feeds score metadata, not
|
|
43868
|
+
* diagnostics).
|
|
43182
43869
|
* 2. beforeLint hook (e.g. CLI renders the project-detection block)
|
|
43183
43870
|
* 3. environment checks (reduced-motion + pnpm hardening +
|
|
43184
|
-
* expo/react-native + security scan)
|
|
43185
|
-
* 4.
|
|
43186
|
-
*
|
|
43187
|
-
*
|
|
43188
|
-
*
|
|
43189
|
-
*
|
|
43190
|
-
*
|
|
43191
|
-
*
|
|
43192
|
-
*
|
|
43871
|
+
* expo/react-native + security scan), collected synchronously
|
|
43872
|
+
* 4. The supply-chain check (Socket.dev) is forked onto a background
|
|
43873
|
+
* fiber so its ~100% network-bound time overlaps the ~100%
|
|
43874
|
+
* CPU/subprocess-bound lint pass below, collapsing two serial
|
|
43875
|
+
* phases into roughly `max(supplyChain, lint)`. It is capped by
|
|
43876
|
+
* `SupplyChainOverlapTimeoutMs` (measured from fork) so a hung
|
|
43877
|
+
* socket can't drag out its join; on timeout it fails open to no
|
|
43878
|
+
* diagnostics — the same outcome class as a Socket outage.
|
|
43879
|
+
* 5. Linter.run runs; DeadCode.run runs concurrently (forked child
|
|
43880
|
+
* fiber) ONLY when the memory gate has headroom to run the 8 GB
|
|
43881
|
+
* dead-code child alongside the oxlint workers — or when overlap is
|
|
43882
|
+
* forced via REACT_DOCTOR_DEAD_CODE_OVERLAP. Otherwise dead-code
|
|
43883
|
+
* runs sequentially after lint, exactly as it did pre-overlap. The
|
|
43884
|
+
* fiber is joined (or interrupted, SIGKILLing its worker, on lint
|
|
43885
|
+
* failure) before diagnostics are concatenated. The afterLint hook
|
|
43886
|
+
* fires between lint and dead-code. Progress spinner labels AND the
|
|
43887
|
+
* final diagnostic / score order stay independent of execution
|
|
43888
|
+
* order, so terminal output is identical either way; supply-chain
|
|
43889
|
+
* rides alongside without a spinner.
|
|
43890
|
+
* 6. Join the supply-chain fiber, then assemble the diagnostics in a
|
|
43891
|
+
* FIXED order (env, supply-chain, lint, dead-code) so the output is
|
|
43892
|
+
* byte-identical regardless of which fiber settled first. The
|
|
43893
|
+
* viewer-permission fiber is joined later, during score-metadata
|
|
43894
|
+
* assembly (it feeds score metadata, not diagnostics). The per-element
|
|
43895
|
+
* `Reporter.emit` side-channel now interleaves supply-chain with lint
|
|
43896
|
+
* emits, so capture-order assertions must target the deterministic
|
|
43897
|
+
* concat below, not emit order (production `Reporter.layerNoop` makes
|
|
43898
|
+
* emit a no-op).
|
|
43899
|
+
* 7. Reporter.finalize
|
|
43900
|
+
* 8. Score.compute against the surface-filtered diagnostic set
|
|
43193
43901
|
*
|
|
43194
43902
|
* The orchestrator owns spinner lifecycle via `Progress`; callers
|
|
43195
43903
|
* choose `Progress.layerOra(...)` for CLI feedback or
|
|
@@ -43247,10 +43955,21 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
43247
43955
|
ignoredTags: input.ignoredTags
|
|
43248
43956
|
})
|
|
43249
43957
|
])));
|
|
43250
|
-
const
|
|
43958
|
+
const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
|
|
43959
|
+
const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
|
|
43960
|
+
const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
|
|
43251
43961
|
rootDirectory: scanDirectory,
|
|
43252
43962
|
userConfig: resolvedConfig.config
|
|
43253
|
-
})))
|
|
43963
|
+
}))).pipe(map$3((diagnostics) => ({
|
|
43964
|
+
diagnostics,
|
|
43965
|
+
timedOut: false
|
|
43966
|
+
})), timeout(supplyChainOverlapTimeout), orElseSucceed(() => ({
|
|
43967
|
+
diagnostics: [],
|
|
43968
|
+
timedOut: true
|
|
43969
|
+
}))) : succeed$2({
|
|
43970
|
+
diagnostics: [],
|
|
43971
|
+
timedOut: false
|
|
43972
|
+
}));
|
|
43254
43973
|
const lintFailure = yield* make$13({
|
|
43255
43974
|
didFail: false,
|
|
43256
43975
|
reason: null,
|
|
@@ -43261,12 +43980,49 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
43261
43980
|
didFail: false,
|
|
43262
43981
|
reason: null
|
|
43263
43982
|
});
|
|
43264
|
-
const scanConcurrency = yield* OxlintConcurrency;
|
|
43983
|
+
const scanConcurrency = resolveScanConcurrency(yield* OxlintConcurrency);
|
|
43984
|
+
const lintPhaseTimeoutMs = yield* LintPhaseTimeoutMs;
|
|
43985
|
+
const deadCodePhaseTimeoutMs = yield* DeadCodePhaseTimeoutMs;
|
|
43986
|
+
const resolveDeadCodePhaseTimeoutMs = (scaledPhaseTimeoutMs) => deadCodePhaseTimeoutMs === 15e4 ? scaledPhaseTimeoutMs : deadCodePhaseTimeoutMs;
|
|
43265
43987
|
const workerCountSuffix = scanConcurrency > 1 ? ` ${highlighter.dim(`[~${scanConcurrency} workers]`)}` : "";
|
|
43988
|
+
const shouldRunDeadCode = input.runDeadCode && !isDiffMode && (showWarnings || deadCodeMaySurfaceWhenWarningsHidden(resolvedConfig.config));
|
|
43989
|
+
const deadCodeOverlapMode = yield* DeadCodeOverlap;
|
|
43990
|
+
const shouldOverlapDeadCode = shouldRunDeadCode && deadCodeOverlapMode === "on";
|
|
43991
|
+
const deadCodeParseConcurrency = shouldOverlapDeadCode ? Math.max(1, Math.floor(scanConcurrency * DEAD_CODE_OVERLAP_PARSE_SHARE)) : void 0;
|
|
43992
|
+
const lintConcurrency = deadCodeParseConcurrency === void 0 ? scanConcurrency : Math.max(1, scanConcurrency - deadCodeParseConcurrency);
|
|
43993
|
+
const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
|
|
43994
|
+
rootDirectory: scanDirectory,
|
|
43995
|
+
userConfig: resolvedConfig.config,
|
|
43996
|
+
parseConcurrency: deadCodeParseConcurrency,
|
|
43997
|
+
workerTimeoutMs: deadCodeTimeout.workerTimeoutMs
|
|
43998
|
+
}).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
|
|
43999
|
+
yield* set(deadCodeFailure, {
|
|
44000
|
+
didFail: true,
|
|
44001
|
+
reason: error.message
|
|
44002
|
+
});
|
|
44003
|
+
return empty$4;
|
|
44004
|
+
})))))).pipe(timeoutOption(deadCodeTimeout.phaseTimeoutMs), flatMap$2(match$3({
|
|
44005
|
+
onNone: () => set(deadCodeFailure, {
|
|
44006
|
+
didFail: true,
|
|
44007
|
+
reason: `Dead-code analysis exceeded ${Math.round(deadCodeTimeout.phaseTimeoutMs / MILLISECONDS_PER_SECOND)}s and was skipped.`
|
|
44008
|
+
}).pipe(as([])),
|
|
44009
|
+
onSome: succeed$2
|
|
44010
|
+
})));
|
|
44011
|
+
const overlapDeadCodeTimeout = resolveDeadCodeTimeout({
|
|
44012
|
+
sourceFileCount: project.sourceFileCount,
|
|
44013
|
+
deadCodeConcurrency: deadCodeParseConcurrency ?? scanConcurrency,
|
|
44014
|
+
fullConcurrency: scanConcurrency
|
|
44015
|
+
});
|
|
44016
|
+
const deadCodeFiber = shouldOverlapDeadCode ? yield* forkChild(buildCollectDeadCode({
|
|
44017
|
+
workerTimeoutMs: overlapDeadCodeTimeout.workerTimeoutMs,
|
|
44018
|
+
phaseTimeoutMs: resolveDeadCodePhaseTimeoutMs(overlapDeadCodeTimeout.phaseTimeoutMs)
|
|
44019
|
+
})) : null;
|
|
43266
44020
|
const scanProgress = yield* progressService.start("Scanning...");
|
|
43267
44021
|
const scanStartTime = Date.now();
|
|
43268
44022
|
let lastReportedTotalFileCount = 0;
|
|
43269
|
-
|
|
44023
|
+
let lintCacheHitFileCount = null;
|
|
44024
|
+
let lintCacheTotalFileCount = null;
|
|
44025
|
+
const baseLintStream = linterService.run({
|
|
43270
44026
|
rootDirectory: scanDirectory,
|
|
43271
44027
|
project,
|
|
43272
44028
|
includePaths: lintIncludePaths ?? void 0,
|
|
@@ -43280,6 +44036,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
43280
44036
|
onFileProgress: (scannedFileCount, totalFileCount) => {
|
|
43281
44037
|
lastReportedTotalFileCount = totalFileCount;
|
|
43282
44038
|
runSync(scanProgress.update(`Scanning files (${scannedFileCount}/${totalFileCount})${workerCountSuffix}...`));
|
|
44039
|
+
},
|
|
44040
|
+
onCacheStats: (cacheHitFileCount, totalConsideredFileCount) => {
|
|
44041
|
+
lintCacheHitFileCount = cacheHitFileCount;
|
|
44042
|
+
lintCacheTotalFileCount = totalConsideredFileCount;
|
|
43283
44043
|
}
|
|
43284
44044
|
}).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
|
|
43285
44045
|
yield* set(lintFailure, {
|
|
@@ -43289,36 +44049,54 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
43289
44049
|
reasonKind: error.reason._tag === "OxlintUnavailable" ? error.reason.kind : null
|
|
43290
44050
|
});
|
|
43291
44051
|
return empty$4;
|
|
43292
|
-
}))))
|
|
44052
|
+
}))));
|
|
44053
|
+
const lintCollected = yield* runCollect(applyPerElementPipeline(shouldOverlapDeadCode ? baseLintStream.pipe(provideService(OxlintConcurrency, lintConcurrency)) : baseLintStream)).pipe(timeoutOption(lintPhaseTimeoutMs), flatMap$2(match$3({
|
|
44054
|
+
onNone: () => set(lintFailure, {
|
|
44055
|
+
didFail: true,
|
|
44056
|
+
reason: `Lint analysis exceeded ${lintPhaseTimeoutMs / MILLISECONDS_PER_SECOND}s and was skipped.`,
|
|
44057
|
+
reasonTag: "OxlintBatchExceeded",
|
|
44058
|
+
reasonKind: null
|
|
44059
|
+
}).pipe(as([])),
|
|
44060
|
+
onSome: succeed$2
|
|
44061
|
+
})));
|
|
43293
44062
|
const lintFailureState = yield* get$2(lintFailure);
|
|
43294
44063
|
yield* afterLint(lintFailureState.didFail);
|
|
43295
44064
|
if (lintFailureState.didFail) yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
|
|
43296
44065
|
const totalFileCount = lastReportedTotalFileCount || (lintIncludePaths?.length ?? project.sourceFileCount);
|
|
43297
44066
|
const scannedFilesLabel = `${totalFileCount} ${totalFileCount === 1 ? "file" : "files"}`;
|
|
43298
|
-
|
|
43299
|
-
|
|
43300
|
-
|
|
43301
|
-
|
|
43302
|
-
|
|
43303
|
-
|
|
43304
|
-
|
|
43305
|
-
|
|
44067
|
+
let deadCodeCollected = [];
|
|
44068
|
+
if (lintFailureState.didFail) {
|
|
44069
|
+
if (deadCodeFiber !== null) yield* interrupt(deadCodeFiber);
|
|
44070
|
+
} else if (shouldRunDeadCode) {
|
|
44071
|
+
yield* scanProgress.update(`Scanned ${scannedFilesLabel}, analyzing dead code...`);
|
|
44072
|
+
const sequentialDeadCodeTimeout = resolveDeadCodeTimeout({
|
|
44073
|
+
sourceFileCount: totalFileCount,
|
|
44074
|
+
deadCodeConcurrency: scanConcurrency,
|
|
44075
|
+
fullConcurrency: scanConcurrency
|
|
43306
44076
|
});
|
|
43307
|
-
|
|
43308
|
-
|
|
43309
|
-
|
|
44077
|
+
deadCodeCollected = deadCodeFiber !== null ? yield* join(deadCodeFiber) : yield* buildCollectDeadCode({
|
|
44078
|
+
workerTimeoutMs: sequentialDeadCodeTimeout.workerTimeoutMs,
|
|
44079
|
+
phaseTimeoutMs: resolveDeadCodePhaseTimeoutMs(sequentialDeadCodeTimeout.phaseTimeoutMs)
|
|
44080
|
+
});
|
|
44081
|
+
}
|
|
44082
|
+
const deadCodeFailureState = lintFailureState.didFail ? {
|
|
44083
|
+
didFail: false,
|
|
44084
|
+
reason: null
|
|
44085
|
+
} : yield* get$2(deadCodeFailure);
|
|
43310
44086
|
const scanElapsedMilliseconds = Date.now() - scanStartTime;
|
|
43311
44087
|
const scanElapsedSeconds = (scanElapsedMilliseconds / MILLISECONDS_PER_SECOND).toFixed(1);
|
|
43312
44088
|
if (!lintFailureState.didFail) if (deadCodeFailureState.didFail) yield* scanProgress.fail(DEAD_CODE_FAIL_TEXT);
|
|
43313
44089
|
else if (input.suppressScanSummary) yield* scanProgress.stop();
|
|
43314
44090
|
else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
|
|
44091
|
+
const supplyChainResult = yield* join(supplyChainFiber);
|
|
44092
|
+
const supplyChainCollected = supplyChainResult.diagnostics;
|
|
43315
44093
|
yield* reporterService.finalize;
|
|
43316
|
-
const finalDiagnostics = assignFixGroups([
|
|
44094
|
+
const finalDiagnostics = sortDiagnosticsStable(assignFixGroups([
|
|
43317
44095
|
...envCollected,
|
|
43318
44096
|
...supplyChainCollected,
|
|
43319
44097
|
...lintCollected,
|
|
43320
44098
|
...deadCodeCollected
|
|
43321
|
-
]);
|
|
44099
|
+
]));
|
|
43322
44100
|
const githubViewerPermission = yield* join(githubViewerPermissionFiber);
|
|
43323
44101
|
const scoreMetadata = {
|
|
43324
44102
|
...repo !== null ? { repo } : {},
|
|
@@ -43354,9 +44132,14 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
43354
44132
|
lintPartialFailures,
|
|
43355
44133
|
didDeadCodeFail: deadCodeFailureState.didFail,
|
|
43356
44134
|
deadCodeFailureReason: deadCodeFailureState.reason,
|
|
44135
|
+
deadCodeOverlapped: shouldOverlapDeadCode,
|
|
43357
44136
|
scannedFileCount: totalFileCount,
|
|
43358
44137
|
scannedFilePaths,
|
|
43359
|
-
scanElapsedMilliseconds
|
|
44138
|
+
scanElapsedMilliseconds,
|
|
44139
|
+
scanConcurrency,
|
|
44140
|
+
supplyChainOverlapTimedOut: supplyChainResult.timedOut,
|
|
44141
|
+
lintCacheHitFileCount,
|
|
44142
|
+
lintCacheTotalFileCount
|
|
43360
44143
|
};
|
|
43361
44144
|
}).pipe(withSpan("runInspect", { attributes: {
|
|
43362
44145
|
"inspect.directory": input.directory,
|
|
@@ -43364,7 +44147,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
43364
44147
|
"inspect.runDeadCode": input.runDeadCode,
|
|
43365
44148
|
"inspect.isCi": input.isCi,
|
|
43366
44149
|
"inspect.scoreSurface": input.scoreSurface ?? "score"
|
|
43367
|
-
} }));
|
|
44150
|
+
} }), (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
44151
|
const parseNodeVersion = (versionString) => {
|
|
43369
44152
|
const [major = 0, minor = 0, patch = 0] = versionString.replace(/^v/, "").trim().split(".").map(Number);
|
|
43370
44153
|
return {
|
|
@@ -44070,6 +44853,7 @@ const NANOSECONDS_PER_SECOND = 1000000000n;
|
|
|
44070
44853
|
const METRIC = {
|
|
44071
44854
|
cliInvoked: "cli.invoked",
|
|
44072
44855
|
cliError: "cli.error",
|
|
44856
|
+
cliEnvironmentError: "cli.env_error",
|
|
44073
44857
|
projectDetected: "project.detected",
|
|
44074
44858
|
projectPathSelected: "project.path_selected",
|
|
44075
44859
|
projectConfigSelected: "project.config_selected",
|
|
@@ -44142,7 +44926,7 @@ const makeNoopConsole = () => ({
|
|
|
44142
44926
|
});
|
|
44143
44927
|
//#endregion
|
|
44144
44928
|
//#region src/cli/utils/version.ts
|
|
44145
|
-
const VERSION = "0.5.
|
|
44929
|
+
const VERSION = "0.5.8-dev.229ea2e";
|
|
44146
44930
|
//#endregion
|
|
44147
44931
|
//#region src/cli/utils/json-mode.ts
|
|
44148
44932
|
let context = null;
|
|
@@ -44295,7 +45079,8 @@ const buildRunContext = () => {
|
|
|
44295
45079
|
terminalKind: detectTerminalKind(),
|
|
44296
45080
|
jsonMode: isJsonModeActive(),
|
|
44297
45081
|
debug: isDebugFlagEnabled(),
|
|
44298
|
-
invokedVia: detectInvokedVia()
|
|
45082
|
+
invokedVia: detectInvokedVia(),
|
|
45083
|
+
lintBatchOrdering: resolveLintBatchOrdering()
|
|
44299
45084
|
};
|
|
44300
45085
|
};
|
|
44301
45086
|
//#endregion
|
|
@@ -44368,7 +45153,8 @@ const buildSentryScope = (runContext = buildRunContext()) => {
|
|
|
44368
45153
|
jsonMode: runContext.jsonMode,
|
|
44369
45154
|
debug: runContext.debug,
|
|
44370
45155
|
invokedVia: runContext.invokedVia,
|
|
44371
|
-
nodeMajor: runContext.nodeMajor
|
|
45156
|
+
nodeMajor: runContext.nodeMajor,
|
|
45157
|
+
lintBatchOrdering: runContext.lintBatchOrdering
|
|
44372
45158
|
};
|
|
44373
45159
|
const contexts = { run: { ...runContext } };
|
|
44374
45160
|
const projectInfo = getSentryProjectInfo();
|
|
@@ -44504,13 +45290,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
|
|
|
44504
45290
|
* uploads source-map artifacts under, so stack frames symbolicate. Honors the
|
|
44505
45291
|
* standard `SENTRY_RELEASE` override.
|
|
44506
45292
|
*/
|
|
44507
|
-
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.
|
|
45293
|
+
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.8-dev.229ea2e`;
|
|
44508
45294
|
/**
|
|
44509
45295
|
* Deployment environment shown in Sentry's environment filter. Defaults to
|
|
44510
45296
|
* `production` for tagged releases and `development` for dev/unbuilt versions,
|
|
44511
45297
|
* overridable via the standard `SENTRY_ENVIRONMENT` env var.
|
|
44512
45298
|
*/
|
|
44513
|
-
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.
|
|
45299
|
+
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.8-dev.229ea2e") ? "development" : "production");
|
|
44514
45300
|
/**
|
|
44515
45301
|
* Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
|
|
44516
45302
|
* (set to `0` to disable tracing) and falls back to
|
|
@@ -44713,7 +45499,7 @@ const externalSpanFrom = (sentrySpan) => {
|
|
|
44713
45499
|
* in-memory tracer — identical to the prior default behavior.
|
|
44714
45500
|
*/
|
|
44715
45501
|
const applyObservability = (program, rootSentrySpan) => {
|
|
44716
|
-
if (isOtlpExportConfigured()) return (rootSentrySpan ? program.pipe(provideService(ParentSpan, externalSpanFrom(rootSentrySpan))) : program).pipe(provide(layerOtlp));
|
|
45502
|
+
if (isOtlpExportConfigured()) return (rootSentrySpan ? program.pipe(provideService$2(ParentSpan, externalSpanFrom(rootSentrySpan))) : program).pipe(provide(layerOtlp));
|
|
44717
45503
|
if (rootSentrySpan) return program.pipe(withTracer(makeSentryTracer(rootSentrySpan)));
|
|
44718
45504
|
return program.pipe(provide(layerOtlp));
|
|
44719
45505
|
};
|
|
@@ -48186,6 +48972,9 @@ const buildOutcomeAttributes = (input) => {
|
|
|
48186
48972
|
"migration.largestRuleBucketSites": largestRuleBucket ? largestRuleBucket.siteCount : null,
|
|
48187
48973
|
"migration.largestRuleBucketRule": largestRuleBucket ? largestRuleBucket.ruleKey : null,
|
|
48188
48974
|
scannedFileCount: result.scannedFileCount ?? null,
|
|
48975
|
+
lintCacheHitFiles: result.lintCacheHitFileCount ?? null,
|
|
48976
|
+
lintCacheTotalFiles: result.lintCacheTotalFileCount ?? null,
|
|
48977
|
+
lintCacheHitRatio: result.lintCacheTotalFileCount != null && result.lintCacheTotalFileCount > 0 ? (result.lintCacheHitFileCount ?? 0) / result.lintCacheTotalFileCount : null,
|
|
48189
48978
|
elapsedMs: result.elapsedMilliseconds,
|
|
48190
48979
|
scanPhaseMs: result.scanElapsedMilliseconds ?? null,
|
|
48191
48980
|
score: result.score ? result.score.score : null,
|
|
@@ -48195,7 +48984,10 @@ const buildOutcomeAttributes = (input) => {
|
|
|
48195
48984
|
didLintFail: input.didLintFail ?? null,
|
|
48196
48985
|
lintFailureReasonKind: input.lintFailureReasonKind ?? null,
|
|
48197
48986
|
lintPartialFailureCount: input.lintPartialFailureCount ?? null,
|
|
48198
|
-
|
|
48987
|
+
lintDroppedFileCount: input.lintDroppedFileCount ?? null,
|
|
48988
|
+
didDeadCodeFail: input.didDeadCodeFail ?? null,
|
|
48989
|
+
supplyChainOverlapTimedOut: input.supplyChainOverlapTimedOut ?? null,
|
|
48990
|
+
deadCodeOverlapped: input.deadCodeOverlapped ?? null
|
|
48199
48991
|
};
|
|
48200
48992
|
for (const [category, count] of countByCategory) attributes[`diag.category.${toCategoryKey(category)}`] = count;
|
|
48201
48993
|
if (result.baselineDelta) {
|
|
@@ -48264,6 +49056,32 @@ const recordRunEvent = (rootSpan, input) => {
|
|
|
48264
49056
|
} catch {}
|
|
48265
49057
|
};
|
|
48266
49058
|
//#endregion
|
|
49059
|
+
//#region src/cli/utils/resolve-worker-telemetry.ts
|
|
49060
|
+
/**
|
|
49061
|
+
* Projects the resolved lint worker count into the `(workerCount, parallel)`
|
|
49062
|
+
* telemetry pair. `resolvedWorkerCount` is the count the scan actually fanned
|
|
49063
|
+
* out to (`InspectOutput.scanConcurrency`); `pinnedConcurrency` is the caller's
|
|
49064
|
+
* `inspect({ concurrency })` pin, used as the fallback when no scan resolved a
|
|
49065
|
+
* count (the pre-scan failure path, or a cache entry persisted before the
|
|
49066
|
+
* resolved count was tracked). `parallel` is derived from the count — NOT from
|
|
49067
|
+
* whether a count was pinned — so the common auto path (no pin) still reports
|
|
49068
|
+
* parallelism correctly instead of always reading `false`.
|
|
49069
|
+
*/
|
|
49070
|
+
const resolveWorkerTelemetry = (resolvedWorkerCount, pinnedConcurrency) => {
|
|
49071
|
+
const workerCount = resolvedWorkerCount ?? pinnedConcurrency;
|
|
49072
|
+
return {
|
|
49073
|
+
workerCount,
|
|
49074
|
+
parallel: workerCount !== void 0 && workerCount > 1
|
|
49075
|
+
};
|
|
49076
|
+
};
|
|
49077
|
+
//#endregion
|
|
49078
|
+
//#region src/cli/utils/count-dropped-lint-files.ts
|
|
49079
|
+
const DROPPED_FILES_MESSAGE_PATTERN = /^(\d+) file\(s\) failed to lint and were skipped/;
|
|
49080
|
+
const countDroppedLintFiles = (lintPartialFailures) => lintPartialFailures.reduce((total, message) => {
|
|
49081
|
+
const match = DROPPED_FILES_MESSAGE_PATTERN.exec(message);
|
|
49082
|
+
return match ? total + Number(match[1]) : total;
|
|
49083
|
+
}, 0);
|
|
49084
|
+
//#endregion
|
|
48267
49085
|
//#region src/cli/utils/path-format.ts
|
|
48268
49086
|
const toForwardSlashes = (filePath) => filePath.replaceAll("\\", "/");
|
|
48269
49087
|
//#endregion
|
|
@@ -48880,25 +49698,192 @@ const canAnimateOnboarding = (stream = process.stdout) => {
|
|
|
48880
49698
|
return !isGitHookEnvironment() && !isCiEnvironment();
|
|
48881
49699
|
};
|
|
48882
49700
|
//#endregion
|
|
48883
|
-
//#region src/cli/utils/
|
|
48884
|
-
const
|
|
48885
|
-
const
|
|
49701
|
+
//#region src/cli/utils/now-iso.ts
|
|
49702
|
+
const nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
49703
|
+
const ONBOARDING_EVENT = "onboarding";
|
|
49704
|
+
const CI_PITCH_EVENT = "ci-pitch";
|
|
49705
|
+
const ACTION_UPGRADE_EVENT = "action-upgrade-v2";
|
|
49706
|
+
const SETUP_HINT_EVENT = "setup-hint";
|
|
49707
|
+
const HANDOFF_TARGET_PREFERENCE_ID = "handoff-target";
|
|
49708
|
+
const INSTALL_AGENTS_PREFERENCE_ID = "install-agents";
|
|
49709
|
+
const foldLegacyDecisions = (projects, legacy, eventId) => {
|
|
49710
|
+
for (const [hash, record] of Object.entries(legacy ?? {})) {
|
|
49711
|
+
const existing = projects[hash] ?? { rootDirectory: record.rootDirectory ?? "" };
|
|
49712
|
+
projects[hash] = {
|
|
49713
|
+
...existing,
|
|
49714
|
+
events: {
|
|
49715
|
+
...existing.events,
|
|
49716
|
+
[eventId]: {
|
|
49717
|
+
firedAt: record.at ?? nowIso(),
|
|
49718
|
+
version: 1,
|
|
49719
|
+
...record.outcome ? { outcome: record.outcome } : {}
|
|
49720
|
+
}
|
|
49721
|
+
}
|
|
49722
|
+
};
|
|
49723
|
+
}
|
|
49724
|
+
};
|
|
49725
|
+
const migrateCliState = (state) => {
|
|
49726
|
+
if (state.schemaVersion === 2) return state;
|
|
49727
|
+
const projects = {};
|
|
49728
|
+
for (const [hash, record] of Object.entries(state.projects ?? {})) {
|
|
49729
|
+
const carried = {
|
|
49730
|
+
rootDirectory: record.rootDirectory,
|
|
49731
|
+
...record.events ? { events: record.events } : {},
|
|
49732
|
+
...record.migrations ? { migrations: record.migrations } : {}
|
|
49733
|
+
};
|
|
49734
|
+
projects[hash] = record.setupPrompt === false ? {
|
|
49735
|
+
...carried,
|
|
49736
|
+
events: {
|
|
49737
|
+
...carried.events,
|
|
49738
|
+
[SETUP_HINT_EVENT]: {
|
|
49739
|
+
firedAt: nowIso(),
|
|
49740
|
+
version: 1,
|
|
49741
|
+
outcome: "declined"
|
|
49742
|
+
}
|
|
49743
|
+
}
|
|
49744
|
+
} : carried;
|
|
49745
|
+
}
|
|
49746
|
+
foldLegacyDecisions(projects, state.ciPrompts, CI_PITCH_EVENT);
|
|
49747
|
+
foldLegacyDecisions(projects, state.actionUpgrades, ACTION_UPGRADE_EVENT);
|
|
49748
|
+
return {
|
|
49749
|
+
schemaVersion: 2,
|
|
49750
|
+
global: typeof state.onboardedAt === "string" ? { events: { [ONBOARDING_EVENT]: {
|
|
49751
|
+
firedAt: state.onboardedAt,
|
|
49752
|
+
version: 1
|
|
49753
|
+
} } } : {},
|
|
49754
|
+
projects
|
|
49755
|
+
};
|
|
49756
|
+
};
|
|
49757
|
+
const resolveConfigDir = (options) => options.cwd ?? (process.env["REACT_DOCTOR_CONFIG_DIR"] || void 0);
|
|
49758
|
+
const openStore = (options = {}) => new Conf({
|
|
48886
49759
|
projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
|
|
48887
|
-
cwd: options
|
|
49760
|
+
cwd: resolveConfigDir(options)
|
|
48888
49761
|
});
|
|
48889
|
-
const
|
|
49762
|
+
const openMigratedStore = (options) => {
|
|
49763
|
+
const store = openStore(options);
|
|
49764
|
+
if (store.store.schemaVersion !== 2) store.store = migrateCliState(store.store);
|
|
49765
|
+
return store;
|
|
49766
|
+
};
|
|
49767
|
+
const readCliState = (select, fallback, options = {}) => {
|
|
48890
49768
|
try {
|
|
48891
|
-
return
|
|
49769
|
+
return select(openMigratedStore(options).store);
|
|
48892
49770
|
} catch {
|
|
49771
|
+
return fallback;
|
|
49772
|
+
}
|
|
49773
|
+
};
|
|
49774
|
+
const updateCliState = (update, options = {}) => {
|
|
49775
|
+
try {
|
|
49776
|
+
const store = openMigratedStore(options);
|
|
49777
|
+
store.store = update(store.store);
|
|
48893
49778
|
return true;
|
|
49779
|
+
} catch {
|
|
49780
|
+
return false;
|
|
48894
49781
|
}
|
|
48895
49782
|
};
|
|
49783
|
+
//#endregion
|
|
49784
|
+
//#region src/cli/utils/hash-project-root.ts
|
|
49785
|
+
const hashProjectRoot = (projectRoot) => createHash("sha256").update(Path.resolve(projectRoot)).digest("hex");
|
|
49786
|
+
//#endregion
|
|
49787
|
+
//#region src/cli/utils/cli-lifecycle.ts
|
|
49788
|
+
const versionOf = (item) => item.version ?? 1;
|
|
49789
|
+
const selectScope = (state, scoped, projectRoot) => scoped.scope === "global" ? state.global : projectRoot === void 0 ? void 0 : state.projects?.[hashProjectRoot(projectRoot)];
|
|
49790
|
+
const updateScope = (state, scoped, projectRoot, updateScopeState) => {
|
|
49791
|
+
if (scoped.scope === "global") return {
|
|
49792
|
+
...state,
|
|
49793
|
+
global: updateScopeState(state.global ?? {})
|
|
49794
|
+
};
|
|
49795
|
+
if (projectRoot === void 0) return state;
|
|
49796
|
+
const projectKey = hashProjectRoot(projectRoot);
|
|
49797
|
+
const base = state.projects?.[projectKey] ?? { rootDirectory: Path.resolve(projectRoot) };
|
|
49798
|
+
return {
|
|
49799
|
+
...state,
|
|
49800
|
+
projects: {
|
|
49801
|
+
...state.projects,
|
|
49802
|
+
[projectKey]: {
|
|
49803
|
+
...base,
|
|
49804
|
+
...updateScopeState(base)
|
|
49805
|
+
}
|
|
49806
|
+
}
|
|
49807
|
+
};
|
|
49808
|
+
};
|
|
49809
|
+
const isGatePending = (gate, target = {}, options = {}) => {
|
|
49810
|
+
if (gate.scope === "project" && target.projectRoot === void 0) return false;
|
|
49811
|
+
return readCliState((state) => {
|
|
49812
|
+
const record = selectScope(state, gate, target.projectRoot)?.events?.[gate.id];
|
|
49813
|
+
return !record || record.version < versionOf(gate);
|
|
49814
|
+
}, gate.fireWhenUnknown ?? false, options);
|
|
49815
|
+
};
|
|
49816
|
+
const recordGate = (gate, target = {}, options = {}) => updateCliState((state) => updateScope(state, gate, target.projectRoot, (scope) => ({
|
|
49817
|
+
...scope,
|
|
49818
|
+
events: {
|
|
49819
|
+
...scope.events,
|
|
49820
|
+
[gate.id]: {
|
|
49821
|
+
firedAt: nowIso(),
|
|
49822
|
+
version: versionOf(gate),
|
|
49823
|
+
...target.outcome ? { outcome: target.outcome } : {}
|
|
49824
|
+
}
|
|
49825
|
+
}
|
|
49826
|
+
})), options);
|
|
49827
|
+
const readPreference = (preference, target = {}, options = {}) => readCliState((state) => selectScope(state, preference, target.projectRoot)?.preferences?.[preference.id] ?? null, null, options);
|
|
49828
|
+
const writePreference = (preference, value, target = {}, options = {}) => updateCliState((state) => updateScope(state, preference, target.projectRoot, (scope) => ({
|
|
49829
|
+
...scope,
|
|
49830
|
+
preferences: {
|
|
49831
|
+
...scope.preferences,
|
|
49832
|
+
[preference.id]: value
|
|
49833
|
+
}
|
|
49834
|
+
})), options);
|
|
49835
|
+
const isMigrationPending = (migration, target = {}, options = {}) => {
|
|
49836
|
+
if (migration.scope === "project" && target.projectRoot === void 0) return false;
|
|
49837
|
+
return readCliState((state) => {
|
|
49838
|
+
const record = selectScope(state, migration, target.projectRoot)?.migrations?.[migration.id];
|
|
49839
|
+
return !record || record.version < versionOf(migration);
|
|
49840
|
+
}, false, options);
|
|
49841
|
+
};
|
|
49842
|
+
const recordMigration = (migration, projectRoot, record, options) => updateCliState((state) => updateScope(state, migration, projectRoot, (scope) => ({
|
|
49843
|
+
...scope,
|
|
49844
|
+
migrations: {
|
|
49845
|
+
...scope.migrations,
|
|
49846
|
+
[migration.id]: record
|
|
49847
|
+
}
|
|
49848
|
+
})), options);
|
|
49849
|
+
const runMigrations = async (migrations, target = {}, options = {}) => {
|
|
49850
|
+
const results = [];
|
|
49851
|
+
for (const migration of migrations) {
|
|
49852
|
+
if (!isMigrationPending(migration, target, options)) {
|
|
49853
|
+
results.push({
|
|
49854
|
+
id: migration.id,
|
|
49855
|
+
ran: false,
|
|
49856
|
+
applied: true
|
|
49857
|
+
});
|
|
49858
|
+
continue;
|
|
49859
|
+
}
|
|
49860
|
+
let applied = false;
|
|
49861
|
+
try {
|
|
49862
|
+
applied = await migration.run({ projectRoot: target.projectRoot });
|
|
49863
|
+
} catch {
|
|
49864
|
+
applied = false;
|
|
49865
|
+
}
|
|
49866
|
+
if (applied) recordMigration(migration, target.projectRoot, {
|
|
49867
|
+
ranAt: nowIso(),
|
|
49868
|
+
version: versionOf(migration)
|
|
49869
|
+
}, options);
|
|
49870
|
+
results.push({
|
|
49871
|
+
id: migration.id,
|
|
49872
|
+
ran: true,
|
|
49873
|
+
applied
|
|
49874
|
+
});
|
|
49875
|
+
}
|
|
49876
|
+
return results;
|
|
49877
|
+
};
|
|
49878
|
+
//#endregion
|
|
49879
|
+
//#region src/cli/utils/onboarding-state.ts
|
|
49880
|
+
const ONBOARDING_GATE = {
|
|
49881
|
+
id: ONBOARDING_EVENT,
|
|
49882
|
+
scope: "global"
|
|
49883
|
+
};
|
|
49884
|
+
const hasCompletedOnboarding = (options = {}) => !isGatePending(ONBOARDING_GATE, {}, options);
|
|
48896
49885
|
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 {}
|
|
49886
|
+
if (isGatePending(ONBOARDING_GATE, {}, options)) recordGate(ONBOARDING_GATE, {}, options);
|
|
48902
49887
|
};
|
|
48903
49888
|
//#endregion
|
|
48904
49889
|
//#region src/cli/utils/render-project-detection.ts
|
|
@@ -49384,7 +50369,7 @@ const readPackageJson = (projectRoot) => {
|
|
|
49384
50369
|
return null;
|
|
49385
50370
|
}
|
|
49386
50371
|
};
|
|
49387
|
-
const writeJsonFile
|
|
50372
|
+
const writeJsonFile = (filePath, value) => {
|
|
49388
50373
|
NFS.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
49389
50374
|
};
|
|
49390
50375
|
const packageHasDependency = (projectRoot, dependencyName) => {
|
|
@@ -49473,12 +50458,12 @@ const resolveCacheFilePath = (projectDirectory) => {
|
|
|
49473
50458
|
const readPersistedCache = (cacheFilePath) => {
|
|
49474
50459
|
try {
|
|
49475
50460
|
const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
|
|
49476
|
-
if (!isRecord(parsed) || parsed.version !==
|
|
49477
|
-
version:
|
|
50461
|
+
if (!isRecord(parsed) || parsed.version !== 2) return {
|
|
50462
|
+
version: 2,
|
|
49478
50463
|
entries: []
|
|
49479
50464
|
};
|
|
49480
50465
|
if (!Array.isArray(parsed.entries)) return {
|
|
49481
|
-
version:
|
|
50466
|
+
version: 2,
|
|
49482
50467
|
entries: []
|
|
49483
50468
|
};
|
|
49484
50469
|
const entries = [];
|
|
@@ -49488,12 +50473,12 @@ const readPersistedCache = (cacheFilePath) => {
|
|
|
49488
50473
|
entries.push(entry);
|
|
49489
50474
|
}
|
|
49490
50475
|
return {
|
|
49491
|
-
version:
|
|
50476
|
+
version: 2,
|
|
49492
50477
|
entries
|
|
49493
50478
|
};
|
|
49494
50479
|
} catch {
|
|
49495
50480
|
return {
|
|
49496
|
-
version:
|
|
50481
|
+
version: 2,
|
|
49497
50482
|
entries: []
|
|
49498
50483
|
};
|
|
49499
50484
|
}
|
|
@@ -49546,7 +50531,7 @@ const buildScanResultCacheKey = (input) => {
|
|
|
49546
50531
|
if (headSha === null) return null;
|
|
49547
50532
|
if (stringifyStableJson(input.userConfig) === null) return null;
|
|
49548
50533
|
const cacheKeyJson = stringifyStableJson({
|
|
49549
|
-
schemaVersion:
|
|
50534
|
+
schemaVersion: 2,
|
|
49550
50535
|
projectIdentity: resolveProjectIdentity(input.projectDirectory),
|
|
49551
50536
|
headSha,
|
|
49552
50537
|
reactDoctorVersion: input.version,
|
|
@@ -49566,6 +50551,7 @@ const buildScanResultCacheKey = (input) => {
|
|
|
49566
50551
|
adoptExistingLintConfig: input.options.adoptExistingLintConfig,
|
|
49567
50552
|
ignoredTags: [...input.options.ignoredTags].sort(),
|
|
49568
50553
|
concurrency: input.options.concurrency,
|
|
50554
|
+
lintBatchOrdering: resolveLintBatchOrdering(),
|
|
49569
50555
|
baselineRef: input.options.baseline?.ref,
|
|
49570
50556
|
changedLineRanges: input.options.changedLineRanges ?? void 0,
|
|
49571
50557
|
noScore: input.options.noScore,
|
|
@@ -49583,7 +50569,7 @@ const createScanResultCache = (projectDirectory) => {
|
|
|
49583
50569
|
for (const entry of persistedCache.entries) entries.set(entry.key, entry);
|
|
49584
50570
|
const persist = () => {
|
|
49585
50571
|
writePersistedCache(cacheFilePath, {
|
|
49586
|
-
version:
|
|
50572
|
+
version: 2,
|
|
49587
50573
|
entries: [...entries.values()].sort((firstEntry, secondEntry) => secondEntry.createdAtMs - firstEntry.createdAtMs).slice(0, 20)
|
|
49588
50574
|
});
|
|
49589
50575
|
};
|
|
@@ -49599,7 +50585,7 @@ const createScanResultCache = (projectDirectory) => {
|
|
|
49599
50585
|
}
|
|
49600
50586
|
};
|
|
49601
50587
|
};
|
|
49602
|
-
const shouldStoreScanPayload = (payload) => !payload.didLintFail && !payload.didDeadCodeFail && payload.lintPartialFailures.length === 0;
|
|
50588
|
+
const shouldStoreScanPayload = (payload) => !payload.didLintFail && !payload.didDeadCodeFail && payload.lintPartialFailures.length === 0 && !payload.supplyChainOverlapTimedOut;
|
|
49603
50589
|
//#endregion
|
|
49604
50590
|
//#region src/inspect.ts
|
|
49605
50591
|
const silentConsole = makeNoopConsole();
|
|
@@ -49663,21 +50649,24 @@ const deriveScope = (options) => {
|
|
|
49663
50649
|
if (options.changedLineRanges !== null) return "lines";
|
|
49664
50650
|
return options.includePaths.length > 0 ? "files" : "full";
|
|
49665
50651
|
};
|
|
49666
|
-
const buildRunEventConfig = (options, userConfig, hasCustomConfig) =>
|
|
49667
|
-
|
|
49668
|
-
|
|
49669
|
-
|
|
49670
|
-
|
|
49671
|
-
|
|
49672
|
-
|
|
49673
|
-
|
|
49674
|
-
|
|
49675
|
-
|
|
49676
|
-
|
|
49677
|
-
|
|
49678
|
-
|
|
49679
|
-
|
|
49680
|
-
|
|
50652
|
+
const buildRunEventConfig = (options, userConfig, hasCustomConfig, resolvedWorkerCount) => {
|
|
50653
|
+
const { workerCount, parallel } = resolveWorkerTelemetry(resolvedWorkerCount, options.concurrency);
|
|
50654
|
+
return {
|
|
50655
|
+
scope: deriveScope(options),
|
|
50656
|
+
parallel,
|
|
50657
|
+
workerCount,
|
|
50658
|
+
lint: options.lint,
|
|
50659
|
+
deadCode: options.deadCode,
|
|
50660
|
+
scoreOnly: options.scoreOnly,
|
|
50661
|
+
noScore: options.noScore,
|
|
50662
|
+
respectInlineDisables: options.respectInlineDisables,
|
|
50663
|
+
showWarnings: options.warnings,
|
|
50664
|
+
usedOutputDir: options.outputDirectory !== null,
|
|
50665
|
+
ignoredTagCount: options.ignoredTags.size,
|
|
50666
|
+
hasCustomConfig,
|
|
50667
|
+
userConfig
|
|
50668
|
+
};
|
|
50669
|
+
};
|
|
49681
50670
|
const inspect = async (directory, inputOptions = {}) => {
|
|
49682
50671
|
const startTime = performance$1.now();
|
|
49683
50672
|
const isConcurrentScan = inputOptions.concurrentScan === true;
|
|
@@ -49769,7 +50758,7 @@ const runBaselineComparison = async (params) => {
|
|
|
49769
50758
|
resolveLocalGithubViewerPermission: false,
|
|
49770
50759
|
suppressScanSummary: true,
|
|
49771
50760
|
supplyChainManifestChanged: params.options.supplyChainManifestChanged
|
|
49772
|
-
}, {}).pipe(provide(baseLayers), provideService(Console, silentConsole))));
|
|
50761
|
+
}, {}).pipe(provide(baseLayers), provideService$2(Console, silentConsole))));
|
|
49773
50762
|
if (baseOutput.didLintFail) return null;
|
|
49774
50763
|
const delta = computeDiagnosticDelta({
|
|
49775
50764
|
headDiagnostics: params.headDiagnostics,
|
|
@@ -49854,7 +50843,8 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
|
|
|
49854
50843
|
runId: getRunId(),
|
|
49855
50844
|
resolveLocalGithubViewerPermission: !options.noScore,
|
|
49856
50845
|
suppressScanSummary: options.suppressRendering,
|
|
49857
|
-
supplyChainManifestChanged: options.supplyChainManifestChanged
|
|
50846
|
+
supplyChainManifestChanged: options.supplyChainManifestChanged,
|
|
50847
|
+
concurrentScan: options.concurrentScan
|
|
49858
50848
|
}, { beforeLint: (projectInfo, lintIncludePaths) => gen(function* () {
|
|
49859
50849
|
recordSentryProjectContext(projectInfo, rootSentrySpan, { concurrentScan: options.concurrentScan });
|
|
49860
50850
|
recordCount(METRIC.projectDetected, 1);
|
|
@@ -49868,7 +50858,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
|
|
|
49868
50858
|
lintSourceFileCount
|
|
49869
50859
|
});
|
|
49870
50860
|
}) });
|
|
49871
|
-
const output = await runPromise(restoreLegacyThrow(applyObservability(options.silent ? program.pipe(provide(layers), provideService(Console, silentConsole)) : program.pipe(provide(layers)), rootSentrySpan)));
|
|
50861
|
+
const output = await runPromise(restoreLegacyThrow(applyObservability(options.silent ? program.pipe(provide(layers), provideService$2(Console, silentConsole)) : program.pipe(provide(layers)), rootSentrySpan)));
|
|
49872
50862
|
const didLintFail = lintBindingMissing || output.didLintFail;
|
|
49873
50863
|
const lintFailureReason = lintBindingMissing ? `oxlint native binding not found for Node ${process.version}; expected one matching ${OXLINT_NODE_REQUIREMENT}` : output.lintFailureReason;
|
|
49874
50864
|
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 +50895,15 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
|
|
|
49905
50895
|
lintPartialFailures: output.lintPartialFailures,
|
|
49906
50896
|
didDeadCodeFail: output.didDeadCodeFail,
|
|
49907
50897
|
deadCodeFailureReason: output.deadCodeFailureReason,
|
|
50898
|
+
deadCodeOverlapped: output.deadCodeOverlapped,
|
|
49908
50899
|
directory: output.resolvedDirectory,
|
|
49909
50900
|
scannedFileCount: output.scannedFileCount,
|
|
49910
50901
|
scannedFilePaths: output.scannedFilePaths,
|
|
49911
50902
|
scanElapsedMilliseconds: output.scanElapsedMilliseconds,
|
|
50903
|
+
scanConcurrency: output.scanConcurrency,
|
|
49912
50904
|
baselineDelta,
|
|
49913
|
-
lintFailureReasonKind: lintBindingMissing ? "native-binding-missing" : output.lintFailureReasonKind
|
|
50905
|
+
lintFailureReasonKind: lintBindingMissing ? "native-binding-missing" : output.lintFailureReasonKind,
|
|
50906
|
+
supplyChainOverlapTimedOut: output.supplyChainOverlapTimedOut
|
|
49914
50907
|
};
|
|
49915
50908
|
if (cacheKey !== null && scanResultCache !== null && shouldStoreScanPayload(payload)) scanResultCache.store(cacheKey, payload);
|
|
49916
50909
|
const result = await renderAndRecordScan({
|
|
@@ -49921,12 +50914,14 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
|
|
|
49921
50914
|
startTime,
|
|
49922
50915
|
rootSentrySpan,
|
|
49923
50916
|
scanMode: baselineDelta ? "baseline" : isDiffMode ? "diff" : "full",
|
|
49924
|
-
baselineDegraded
|
|
50917
|
+
baselineDegraded,
|
|
50918
|
+
lintCacheHitFileCount: output.lintCacheHitFileCount,
|
|
50919
|
+
lintCacheTotalFileCount: output.lintCacheTotalFileCount
|
|
49925
50920
|
});
|
|
49926
50921
|
recordOnboardingCompletion(options);
|
|
49927
50922
|
return result;
|
|
49928
50923
|
};
|
|
49929
|
-
const runMaybeSilent = (effect, silent) => silent ? effect.pipe(provideService(Console, silentConsole)) : effect;
|
|
50924
|
+
const runMaybeSilent = (effect, silent) => silent ? effect.pipe(provideService$2(Console, silentConsole)) : effect;
|
|
49930
50925
|
const renderCachedProjectDetection = async (input) => {
|
|
49931
50926
|
if (input.options.scoreOnly || input.options.suppressRendering) return;
|
|
49932
50927
|
await runPromise(runMaybeSilent(printProjectDetection({
|
|
@@ -49954,14 +50949,17 @@ const renderAndRecordScan = async (input) => {
|
|
|
49954
50949
|
scannedFileCount: input.payload.scannedFileCount,
|
|
49955
50950
|
scannedFilePaths: input.payload.scannedFilePaths,
|
|
49956
50951
|
scanElapsedMilliseconds: input.payload.scanElapsedMilliseconds,
|
|
50952
|
+
lintCacheHitFileCount: input.lintCacheHitFileCount ?? null,
|
|
50953
|
+
lintCacheTotalFileCount: input.lintCacheTotalFileCount ?? null,
|
|
49957
50954
|
baselineDelta: input.payload.baselineDelta
|
|
49958
50955
|
}), input.options.silent));
|
|
50956
|
+
const { workerCount: resolvedWorkerCount, parallel } = resolveWorkerTelemetry(input.payload.scanConcurrency, input.options.concurrency);
|
|
49959
50957
|
recordScanMetrics({
|
|
49960
50958
|
result,
|
|
49961
50959
|
mode: input.scanMode,
|
|
49962
50960
|
baselineDegraded: input.baselineDegraded,
|
|
49963
|
-
parallel
|
|
49964
|
-
workerCount:
|
|
50961
|
+
parallel,
|
|
50962
|
+
workerCount: resolvedWorkerCount,
|
|
49965
50963
|
lint: input.options.lint,
|
|
49966
50964
|
deadCode: input.options.deadCode,
|
|
49967
50965
|
scoreOnly: input.options.scoreOnly,
|
|
@@ -49971,19 +50969,22 @@ const renderAndRecordScan = async (input) => {
|
|
|
49971
50969
|
didDeadCodeFail: input.payload.didDeadCodeFail
|
|
49972
50970
|
});
|
|
49973
50971
|
recordRunEvent(input.rootSentrySpan, {
|
|
49974
|
-
...buildRunEventConfig(input.options, input.userConfig, input.hasCustomConfig),
|
|
50972
|
+
...buildRunEventConfig(input.options, input.userConfig, input.hasCustomConfig, resolvedWorkerCount),
|
|
49975
50973
|
result,
|
|
49976
50974
|
mode: input.scanMode,
|
|
49977
50975
|
gateExempt: input.baselineDegraded,
|
|
49978
50976
|
didLintFail: input.payload.didLintFail,
|
|
49979
50977
|
lintFailureReasonKind: input.payload.lintFailureReasonKind,
|
|
49980
50978
|
lintPartialFailureCount: input.payload.lintPartialFailures.length,
|
|
49981
|
-
|
|
50979
|
+
lintDroppedFileCount: countDroppedLintFiles(input.payload.lintPartialFailures),
|
|
50980
|
+
didDeadCodeFail: input.payload.didDeadCodeFail,
|
|
50981
|
+
supplyChainOverlapTimedOut: input.payload.supplyChainOverlapTimedOut,
|
|
50982
|
+
deadCodeOverlapped: input.payload.deadCodeOverlapped
|
|
49982
50983
|
});
|
|
49983
50984
|
return result;
|
|
49984
50985
|
};
|
|
49985
50986
|
const finalizeAndRender = (input) => gen(function* () {
|
|
49986
|
-
const { options, elapsedMilliseconds, diagnostics, score, project, userConfig, didLintFail, lintFailureReason, lintPartialFailures, didDeadCodeFail, deadCodeFailureReason, directory, scannedFileCount, scannedFilePaths, scanElapsedMilliseconds, baselineDelta } = input;
|
|
50987
|
+
const { options, elapsedMilliseconds, diagnostics, score, project, userConfig, didLintFail, lintFailureReason, lintPartialFailures, didDeadCodeFail, deadCodeFailureReason, directory, scannedFileCount, scannedFilePaths, scanElapsedMilliseconds, lintCacheHitFileCount, lintCacheTotalFileCount, baselineDelta } = input;
|
|
49987
50988
|
const { skippedChecks, skippedCheckReasons } = buildSkippedChecks({
|
|
49988
50989
|
didLintFail,
|
|
49989
50990
|
lintFailureReason,
|
|
@@ -50003,6 +51004,10 @@ const finalizeAndRender = (input) => gen(function* () {
|
|
|
50003
51004
|
scannedFileCount,
|
|
50004
51005
|
scannedFilePaths,
|
|
50005
51006
|
scanElapsedMilliseconds,
|
|
51007
|
+
...lintCacheTotalFileCount !== null ? {
|
|
51008
|
+
lintCacheHitFileCount,
|
|
51009
|
+
lintCacheTotalFileCount
|
|
51010
|
+
} : {},
|
|
50006
51011
|
...baselineDelta ? { baselineDelta } : {}
|
|
50007
51012
|
});
|
|
50008
51013
|
if (options.suppressRendering) return buildResult();
|
|
@@ -50084,7 +51089,8 @@ const getStagedSourceFiles = async (directory) => {
|
|
|
50084
51089
|
return [...await runPromise(gen(function* () {
|
|
50085
51090
|
return yield* (yield* StagedFiles).discoverSourceFiles(directory);
|
|
50086
51091
|
}).pipe(provide(stagedFilesLayer)))];
|
|
50087
|
-
} catch {
|
|
51092
|
+
} catch (error) {
|
|
51093
|
+
cliLogger.warn(`Failed to discover staged files: ${error instanceof Error ? error.message : String(error)}`);
|
|
50088
51094
|
return [];
|
|
50089
51095
|
}
|
|
50090
51096
|
};
|
|
@@ -50103,6 +51109,35 @@ const materializeStagedFiles = async (directory, stagedFiles, tempDirectory) =>
|
|
|
50103
51109
|
};
|
|
50104
51110
|
};
|
|
50105
51111
|
//#endregion
|
|
51112
|
+
//#region src/cli/utils/is-environment-error.ts
|
|
51113
|
+
const isNodeSystemError = (error) => error instanceof Error && typeof error.code === "string";
|
|
51114
|
+
const ENVIRONMENT_ERROR_CODES = new Set([
|
|
51115
|
+
"ENOSPC",
|
|
51116
|
+
"EIO",
|
|
51117
|
+
"EROFS",
|
|
51118
|
+
"EACCES",
|
|
51119
|
+
"EPERM",
|
|
51120
|
+
"ENOTDIR"
|
|
51121
|
+
]);
|
|
51122
|
+
const isEnvironmentError = (error) => {
|
|
51123
|
+
if (!isNodeSystemError(error)) return false;
|
|
51124
|
+
if (error.code === "ENOENT") return error.syscall?.startsWith("spawn") ?? false;
|
|
51125
|
+
return error.code !== void 0 && ENVIRONMENT_ERROR_CODES.has(error.code);
|
|
51126
|
+
};
|
|
51127
|
+
const formatEnvironmentError = (error) => {
|
|
51128
|
+
if (!isNodeSystemError(error)) return error instanceof Error ? error.message : String(error);
|
|
51129
|
+
switch (error.code) {
|
|
51130
|
+
case "ENOSPC": return "No space left on device. Free up disk space and try again.";
|
|
51131
|
+
case "EIO": return "I/O error: the filesystem or disk may be failing. Check your system logs.";
|
|
51132
|
+
case "EROFS": return "Read-only filesystem: cannot write to this location.";
|
|
51133
|
+
case "EACCES":
|
|
51134
|
+
case "EPERM": return error.path ? `Permission denied accessing ${error.path}. Check file permissions and try again.` : "Permission denied. Check file permissions and try again.";
|
|
51135
|
+
case "ENOTDIR": return error.path ? `A file exists at ${error.path} or one of its parent paths where a directory was expected.` : "A file exists where a directory was expected.";
|
|
51136
|
+
case "ENOENT": return "Required command not found. Ensure the tool (e.g. git) is installed and on your PATH.";
|
|
51137
|
+
default: return error.message;
|
|
51138
|
+
}
|
|
51139
|
+
};
|
|
51140
|
+
//#endregion
|
|
50106
51141
|
//#region src/cli/utils/handle-error.ts
|
|
50107
51142
|
const OTLP_ENDPOINT_ENVIRONMENT_VARIABLE = "REACT_DOCTOR_OTLP_ENDPOINT";
|
|
50108
51143
|
const OTLP_AUTH_HEADER_ENVIRONMENT_VARIABLE = "REACT_DOCTOR_OTLP_AUTH_HEADER";
|
|
@@ -50185,15 +51220,19 @@ const handleError = (error, options = {}) => {
|
|
|
50185
51220
|
process.exitCode = 1;
|
|
50186
51221
|
};
|
|
50187
51222
|
/**
|
|
50188
|
-
* Renderer for expected, user-actionable failures — a bad `--diff` value
|
|
50189
|
-
* a base branch that isn't fetched
|
|
50190
|
-
*
|
|
50191
|
-
*
|
|
51223
|
+
* Renderer for expected, user-actionable failures — a bad `--diff` value,
|
|
51224
|
+
* a base branch that isn't fetched, or environment errors like disk-full or
|
|
51225
|
+
* permission-denied. Prints just the (already human-readable) message — no
|
|
51226
|
+
* "Something went wrong", prefilled issue, Discord link, or Sentry reference
|
|
51227
|
+
* — because there is no bug to report.
|
|
50192
51228
|
*/
|
|
50193
51229
|
const handleUserError = (error, options = {}) => {
|
|
51230
|
+
const isEnvError = isEnvironmentError(error);
|
|
51231
|
+
if (isEnvError) recordCount(METRIC.cliEnvironmentError, 1, { code: error.code ?? "unknown" });
|
|
51232
|
+
const message = isEnvError ? formatEnvironmentError(error) : formatErrorForReport(error);
|
|
50194
51233
|
runSync(gen(function* () {
|
|
50195
51234
|
yield* error$1("");
|
|
50196
|
-
yield* error$1(highlighter.error(
|
|
51235
|
+
yield* error$1(highlighter.error(message));
|
|
50197
51236
|
yield* error$1("");
|
|
50198
51237
|
}));
|
|
50199
51238
|
if (options.shouldExit !== false) process.exit(1);
|
|
@@ -50208,7 +51247,7 @@ const handleUserError = (error, options = {}) => {
|
|
|
50208
51247
|
* `handleUserError` (a plain message — no "Something went wrong", prefilled
|
|
50209
51248
|
* issue, Discord link, or Sentry reference), since there is no bug to report.
|
|
50210
51249
|
*
|
|
50211
|
-
*
|
|
51250
|
+
* Four distinct shapes reach the CLI's catch blocks:
|
|
50212
51251
|
*
|
|
50213
51252
|
* - **Project-discovery failures** (`NoReactDependencyError`,
|
|
50214
51253
|
* `ProjectNotFoundError`, `PackageJsonNotFoundError`, `NotADirectoryError`,
|
|
@@ -50221,12 +51260,19 @@ const handleUserError = (error, options = {}) => {
|
|
|
50221
51260
|
* `--project` name.
|
|
50222
51261
|
* - **Bad `--diff` input** (`GitBaseBranchInvalid` / `GitBaseBranchMissing`)
|
|
50223
51262
|
* stays the tagged `ReactDoctorError`, so dispatch on the reason `_tag`.
|
|
51263
|
+
* - **Environment failures** (`ENOSPC`, `EIO`, `EROFS`, `EACCES`, `EPERM`,
|
|
51264
|
+
* `ENOTDIR`, plus a `spawn`-scoped `ENOENT` for a missing binary) — disk
|
|
51265
|
+
* full / failing / read-only, permission denied, or a path blocked by a
|
|
51266
|
+
* file. React Doctor cannot fix the user's environment; exit cleanly with an
|
|
51267
|
+
* actionable message instead of crashing. See `is-environment-error.ts` for
|
|
51268
|
+
* why the set stays narrow (codes that usually mean our bug keep reaching
|
|
51269
|
+
* Sentry).
|
|
50224
51270
|
*
|
|
50225
51271
|
* This composes the existing core narrowers rather than introducing a new
|
|
50226
51272
|
* error-shape helper (AGENTS.md): it encodes CLI-layer reporting policy, not
|
|
50227
51273
|
* knowledge of the `ReactDoctorError` shape.
|
|
50228
51274
|
*/
|
|
50229
|
-
const isExpectedUserError = (error) => error instanceof CliInputError || isProjectDiscoveryError(error) || isReactDoctorError(error) && (error.reason._tag === "GitBaseBranchInvalid" || error.reason._tag === "GitBaseBranchMissing");
|
|
51275
|
+
const isExpectedUserError = (error) => error instanceof CliInputError || isProjectDiscoveryError(error) || isEnvironmentError(error) || isReactDoctorError(error) && (error.reason._tag === "GitBaseBranchInvalid" || error.reason._tag === "GitBaseBranchMissing");
|
|
50230
51276
|
//#endregion
|
|
50231
51277
|
//#region src/cli/utils/build-handoff-payload.ts
|
|
50232
51278
|
const buildHandoffPayload = (input) => {
|
|
@@ -50307,6 +51353,20 @@ const detectAvailableAgents = async () => {
|
|
|
50307
51353
|
const detected = new Set([...detectPathAvailableAgents(), ...await detectInstalledSkillAgents()]);
|
|
50308
51354
|
return getSkillAgentTypes().filter((agent) => agent !== "universal" && detected.has(agent));
|
|
50309
51355
|
};
|
|
51356
|
+
const DEFAULT_INSTALL_AGENTS = [
|
|
51357
|
+
"claude-code",
|
|
51358
|
+
"cursor",
|
|
51359
|
+
"codex",
|
|
51360
|
+
"opencode"
|
|
51361
|
+
];
|
|
51362
|
+
const computeDefaultSelectedAgents = (detectedAgents, rememberedAgents) => {
|
|
51363
|
+
const detected = new Set(detectedAgents);
|
|
51364
|
+
const remembered = rememberedAgents.filter((agent) => detected.has(agent));
|
|
51365
|
+
if (remembered.length > 0) return remembered;
|
|
51366
|
+
const defaults = DEFAULT_INSTALL_AGENTS.filter((agent) => detected.has(agent));
|
|
51367
|
+
if (defaults.length > 0) return defaults;
|
|
51368
|
+
return detectedAgents.length === 1 ? [...detectedAgents] : [];
|
|
51369
|
+
};
|
|
50310
51370
|
//#endregion
|
|
50311
51371
|
//#region src/cli/utils/install-doctor-script.ts
|
|
50312
51372
|
const DOCTOR_SCRIPT_NAME = "doctor";
|
|
@@ -50388,7 +51448,7 @@ const installDoctorScript = (options) => {
|
|
|
50388
51448
|
};
|
|
50389
51449
|
})();
|
|
50390
51450
|
const scriptStatus = scriptTarget.status;
|
|
50391
|
-
if (scriptStatus === "created") writeJsonFile
|
|
51451
|
+
if (scriptStatus === "created") writeJsonFile(packageJsonPath, {
|
|
50392
51452
|
...packageJson,
|
|
50393
51453
|
scripts: {
|
|
50394
51454
|
...isRecord$1(scripts) ? scripts : {},
|
|
@@ -50542,54 +51602,35 @@ const upgradeReactDoctorWorkflowInPlace = (projectRoot) => {
|
|
|
50542
51602
|
}
|
|
50543
51603
|
};
|
|
50544
51604
|
//#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
51605
|
//#region src/cli/utils/action-upgrade-prompt.ts
|
|
50583
|
-
const
|
|
50584
|
-
|
|
50585
|
-
|
|
50586
|
-
|
|
51606
|
+
const ACTION_UPGRADE_GATE = {
|
|
51607
|
+
id: ACTION_UPGRADE_EVENT,
|
|
51608
|
+
scope: "project"
|
|
51609
|
+
};
|
|
51610
|
+
const hasHandledActionUpgrade = (projectRoot, options = {}) => !isGatePending(ACTION_UPGRADE_GATE, { projectRoot }, options);
|
|
51611
|
+
const recordActionUpgradeDecision = (projectRoot, outcome, options = {}) => recordGate(ACTION_UPGRADE_GATE, {
|
|
51612
|
+
projectRoot,
|
|
51613
|
+
outcome
|
|
51614
|
+
}, options);
|
|
50587
51615
|
//#endregion
|
|
50588
51616
|
//#region src/cli/utils/ci-prompt-decision.ts
|
|
50589
|
-
const
|
|
50590
|
-
|
|
50591
|
-
|
|
50592
|
-
|
|
51617
|
+
const CI_PITCH_GATE = {
|
|
51618
|
+
id: CI_PITCH_EVENT,
|
|
51619
|
+
scope: "project"
|
|
51620
|
+
};
|
|
51621
|
+
const hasHandledCiPrompt = (projectRoot, options = {}) => !isGatePending(CI_PITCH_GATE, { projectRoot }, options);
|
|
51622
|
+
const recordCiPromptDecision = (projectRoot, outcome, options = {}) => recordGate(CI_PITCH_GATE, {
|
|
51623
|
+
projectRoot,
|
|
51624
|
+
outcome
|
|
51625
|
+
}, options);
|
|
51626
|
+
//#endregion
|
|
51627
|
+
//#region src/cli/utils/handoff-target-preference.ts
|
|
51628
|
+
const HANDOFF_TARGET_PREFERENCE = {
|
|
51629
|
+
id: HANDOFF_TARGET_PREFERENCE_ID,
|
|
51630
|
+
scope: "global"
|
|
51631
|
+
};
|
|
51632
|
+
const readHandoffTarget = (options = {}) => readPreference(HANDOFF_TARGET_PREFERENCE, {}, options);
|
|
51633
|
+
const rememberHandoffTarget = (target, options = {}) => writePreference(HANDOFF_TARGET_PREFERENCE, target, {}, options);
|
|
50593
51634
|
//#endregion
|
|
50594
51635
|
//#region src/cli/utils/open-url.ts
|
|
50595
51636
|
const resolveOpenCommand = (url) => {
|
|
@@ -50755,39 +51796,80 @@ const DEFAULT_PR_TITLE = "Add React Doctor to GitHub Actions";
|
|
|
50755
51796
|
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
51797
|
|
|
50757
51798
|
Docs: https://www.react.doctor/ci`;
|
|
50758
|
-
const findUniqueBranchName = async (cwd) => {
|
|
50759
|
-
if (!(await
|
|
51799
|
+
const findUniqueBranchName = async (cwd, run) => {
|
|
51800
|
+
if (!(await run("git", [
|
|
50760
51801
|
"rev-parse",
|
|
50761
51802
|
"--verify",
|
|
50762
51803
|
NEW_BRANCH_PREFIX
|
|
50763
51804
|
], cwd)).success) return NEW_BRANCH_PREFIX;
|
|
50764
51805
|
return `${NEW_BRANCH_PREFIX}-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace(/[-:T]/g, "")}`;
|
|
50765
51806
|
};
|
|
51807
|
+
const findExistingSetupPullRequest = async (cwd, run) => {
|
|
51808
|
+
const prList = await run("gh", [
|
|
51809
|
+
"pr",
|
|
51810
|
+
"list",
|
|
51811
|
+
"--state",
|
|
51812
|
+
"open",
|
|
51813
|
+
"--json",
|
|
51814
|
+
"headRefName,url",
|
|
51815
|
+
"--limit",
|
|
51816
|
+
String(100)
|
|
51817
|
+
], cwd);
|
|
51818
|
+
if (!prList.success) return null;
|
|
51819
|
+
try {
|
|
51820
|
+
return JSON.parse(prList.stdout).find((pullRequest) => (pullRequest.headRefName ?? "").startsWith(NEW_BRANCH_PREFIX)) ?? null;
|
|
51821
|
+
} catch {
|
|
51822
|
+
return null;
|
|
51823
|
+
}
|
|
51824
|
+
};
|
|
51825
|
+
const hasUnrelatedTrackedChanges = async (cwd, workflowRelative, run) => {
|
|
51826
|
+
const statusProbe = await run("git", [
|
|
51827
|
+
"status",
|
|
51828
|
+
"--porcelain",
|
|
51829
|
+
"--",
|
|
51830
|
+
".",
|
|
51831
|
+
`:!${workflowRelative}`
|
|
51832
|
+
], cwd);
|
|
51833
|
+
if (!statusProbe.success) return true;
|
|
51834
|
+
return statusProbe.stdout.split(/\r?\n/).filter(Boolean).some((statusLine) => !statusLine.startsWith("??"));
|
|
51835
|
+
};
|
|
50766
51836
|
const openWorkflowPullRequest = async (params) => {
|
|
50767
51837
|
const workflowPath = Path.resolve(params.workflowPath);
|
|
50768
51838
|
const commitMessage = params.commitMessage ?? DEFAULT_COMMIT_MESSAGE;
|
|
50769
51839
|
const prTitle = params.prTitle ?? DEFAULT_PR_TITLE;
|
|
50770
51840
|
const prBody = params.prBody ?? DEFAULT_PR_BODY;
|
|
50771
|
-
const
|
|
51841
|
+
const run = params.run ?? runCommand;
|
|
51842
|
+
const checkCommandAvailable = params.checkCommandAvailable ?? isCommandAvailable;
|
|
51843
|
+
const repoRootProbe = await run("git", ["rev-parse", "--show-toplevel"], Path.dirname(workflowPath));
|
|
50772
51844
|
if (!repoRootProbe.success) return {
|
|
50773
51845
|
status: "not-attempted",
|
|
50774
51846
|
reason: "not-a-git-repo"
|
|
50775
51847
|
};
|
|
50776
51848
|
const cwd = repoRootProbe.stdout;
|
|
50777
|
-
|
|
51849
|
+
const workflowRelative = toForwardSlashes(Path.relative(cwd, workflowPath));
|
|
51850
|
+
if (!checkCommandAvailable("gh")) return {
|
|
50778
51851
|
status: "not-attempted",
|
|
50779
51852
|
reason: "gh-not-installed"
|
|
50780
51853
|
};
|
|
50781
|
-
if (!(await
|
|
51854
|
+
if (!(await run("gh", ["auth", "status"], cwd)).success) return {
|
|
50782
51855
|
status: "not-attempted",
|
|
50783
51856
|
reason: "gh-not-authenticated"
|
|
50784
51857
|
};
|
|
50785
|
-
const
|
|
51858
|
+
const existingSetupPullRequest = await findExistingSetupPullRequest(cwd, run);
|
|
51859
|
+
if (existingSetupPullRequest) return {
|
|
51860
|
+
status: "pr-exists",
|
|
51861
|
+
url: existingSetupPullRequest.url ?? ""
|
|
51862
|
+
};
|
|
51863
|
+
if (await hasUnrelatedTrackedChanges(cwd, workflowRelative, run)) return {
|
|
51864
|
+
status: "not-attempted",
|
|
51865
|
+
reason: "working-tree-dirty"
|
|
51866
|
+
};
|
|
51867
|
+
const defaultBranch = params.baseBranch ?? await detectDefaultBranch(cwd, run);
|
|
50786
51868
|
if (!defaultBranch) return {
|
|
50787
51869
|
status: "not-attempted",
|
|
50788
51870
|
reason: "no-default-branch"
|
|
50789
51871
|
};
|
|
50790
|
-
const previousBranchProbe = await
|
|
51872
|
+
const previousBranchProbe = await run("git", [
|
|
50791
51873
|
"rev-parse",
|
|
50792
51874
|
"--abbrev-ref",
|
|
50793
51875
|
"HEAD"
|
|
@@ -50797,13 +51879,13 @@ const openWorkflowPullRequest = async (params) => {
|
|
|
50797
51879
|
reason: "detached-head"
|
|
50798
51880
|
};
|
|
50799
51881
|
const previousBranch = previousBranchProbe.stdout;
|
|
50800
|
-
await
|
|
51882
|
+
await run("git", [
|
|
50801
51883
|
"fetch",
|
|
50802
51884
|
"origin",
|
|
50803
51885
|
defaultBranch
|
|
50804
51886
|
], cwd);
|
|
50805
|
-
const newBranch = await findUniqueBranchName(cwd);
|
|
50806
|
-
if (!(await
|
|
51887
|
+
const newBranch = await findUniqueBranchName(cwd, run);
|
|
51888
|
+
if (!(await run("git", [
|
|
50807
51889
|
"checkout",
|
|
50808
51890
|
"-b",
|
|
50809
51891
|
newBranch,
|
|
@@ -50813,17 +51895,17 @@ const openWorkflowPullRequest = async (params) => {
|
|
|
50813
51895
|
reason: "checkout-failed"
|
|
50814
51896
|
};
|
|
50815
51897
|
const restoreToPreviousBranch = async (deleteNewBranch) => {
|
|
50816
|
-
await
|
|
50817
|
-
if (deleteNewBranch) await
|
|
51898
|
+
await run("git", ["checkout", previousBranch], cwd);
|
|
51899
|
+
if (deleteNewBranch) await run("git", [
|
|
50818
51900
|
"branch",
|
|
50819
51901
|
"-D",
|
|
50820
51902
|
newBranch
|
|
50821
51903
|
], cwd);
|
|
50822
51904
|
};
|
|
50823
|
-
if (!(await
|
|
51905
|
+
if (!(await run("git", [
|
|
50824
51906
|
"add",
|
|
50825
51907
|
"--",
|
|
50826
|
-
|
|
51908
|
+
workflowRelative
|
|
50827
51909
|
], cwd)).success) {
|
|
50828
51910
|
await restoreToPreviousBranch(true);
|
|
50829
51911
|
return {
|
|
@@ -50831,7 +51913,7 @@ const openWorkflowPullRequest = async (params) => {
|
|
|
50831
51913
|
reason: "git-add-failed"
|
|
50832
51914
|
};
|
|
50833
51915
|
}
|
|
50834
|
-
if (!(await
|
|
51916
|
+
if (!(await run("git", [
|
|
50835
51917
|
"commit",
|
|
50836
51918
|
"-m",
|
|
50837
51919
|
commitMessage
|
|
@@ -50842,7 +51924,7 @@ const openWorkflowPullRequest = async (params) => {
|
|
|
50842
51924
|
reason: "git-commit-failed"
|
|
50843
51925
|
};
|
|
50844
51926
|
}
|
|
50845
|
-
if (!(await
|
|
51927
|
+
if (!(await run("git", [
|
|
50846
51928
|
"push",
|
|
50847
51929
|
"-u",
|
|
50848
51930
|
"origin",
|
|
@@ -50854,7 +51936,7 @@ const openWorkflowPullRequest = async (params) => {
|
|
|
50854
51936
|
reason: "git-push-failed"
|
|
50855
51937
|
};
|
|
50856
51938
|
}
|
|
50857
|
-
const prCreate = await
|
|
51939
|
+
const prCreate = await run("gh", [
|
|
50858
51940
|
"pr",
|
|
50859
51941
|
"create",
|
|
50860
51942
|
"--title",
|
|
@@ -50878,12 +51960,13 @@ const openWorkflowPullRequest = async (params) => {
|
|
|
50878
51960
|
};
|
|
50879
51961
|
const stageWorkflowFile = async (params) => {
|
|
50880
51962
|
const workflowPath = Path.resolve(params.workflowPath);
|
|
50881
|
-
const
|
|
51963
|
+
const run = params.run ?? runCommand;
|
|
51964
|
+
const repoRootProbe = await run("git", ["rev-parse", "--show-toplevel"], Path.dirname(workflowPath));
|
|
50882
51965
|
if (!repoRootProbe.success) return false;
|
|
50883
|
-
return (await
|
|
51966
|
+
return (await run("git", [
|
|
50884
51967
|
"add",
|
|
50885
51968
|
"--",
|
|
50886
|
-
Path.relative(repoRootProbe.stdout, workflowPath)
|
|
51969
|
+
toForwardSlashes(Path.relative(repoRootProbe.stdout, workflowPath))
|
|
50887
51970
|
], repoRootProbe.stdout)).success;
|
|
50888
51971
|
};
|
|
50889
51972
|
//#endregion
|
|
@@ -50924,6 +52007,7 @@ const setUpGitHubActions = async (options) => {
|
|
|
50924
52007
|
baseBranch: defaultBranch
|
|
50925
52008
|
});
|
|
50926
52009
|
if (pullRequestResult.status === "pr-opened") pullRequestSpinner.succeed(`Opened pull request for review: ${highlighter.info(pullRequestResult.url)}`);
|
|
52010
|
+
else if (pullRequestResult.status === "pr-exists") pullRequestSpinner.succeed(`A React Doctor setup pull request is already open: ${highlighter.info(pullRequestResult.url)}`);
|
|
50927
52011
|
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
52012
|
else {
|
|
50929
52013
|
pullRequestSpinner.stop();
|
|
@@ -50935,6 +52019,19 @@ const setUpGitHubActions = async (options) => {
|
|
|
50935
52019
|
return didCreateWorkflow;
|
|
50936
52020
|
};
|
|
50937
52021
|
//#endregion
|
|
52022
|
+
//#region src/cli/utils/install-agents-preference.ts
|
|
52023
|
+
const INSTALL_AGENTS_PREFERENCE = {
|
|
52024
|
+
id: INSTALL_AGENTS_PREFERENCE_ID,
|
|
52025
|
+
scope: "global"
|
|
52026
|
+
};
|
|
52027
|
+
const PREFERENCE_SEPARATOR = ",";
|
|
52028
|
+
const readInstallAgents = (options = {}) => {
|
|
52029
|
+
const stored = readPreference(INSTALL_AGENTS_PREFERENCE, {}, options);
|
|
52030
|
+
if (stored === null) return [];
|
|
52031
|
+
return stored.split(PREFERENCE_SEPARATOR).map((entry) => entry.trim()).filter((entry) => isSkillAgentType(entry));
|
|
52032
|
+
};
|
|
52033
|
+
const rememberInstallAgents = (agents, options = {}) => writePreference(INSTALL_AGENTS_PREFERENCE, agents.join(PREFERENCE_SEPARATOR), {}, options);
|
|
52034
|
+
//#endregion
|
|
50938
52035
|
//#region src/cli/utils/install-agent-hooks.ts
|
|
50939
52036
|
const CLAUDE_AGENT = "claude-code";
|
|
50940
52037
|
const CURSOR_AGENT = "cursor";
|
|
@@ -50945,20 +52042,34 @@ const CURSOR_HOOKS_RELATIVE_PATH = ".cursor/hooks.json";
|
|
|
50945
52042
|
const CURSOR_HOOK_RELATIVE_PATH = ".cursor/hooks/react-doctor.sh";
|
|
50946
52043
|
const CURSOR_HOOK_MATCHER = "Write|Edit|MultiEdit|ApplyPatch";
|
|
50947
52044
|
const CURSOR_HOOKS_SCHEMA_VERSION = 1;
|
|
50948
|
-
const JSON_INDENT_SPACES$1 = 2;
|
|
50949
52045
|
const isSupportedAgent = (agent) => agent === CLAUDE_AGENT || agent === CURSOR_AGENT;
|
|
50950
52046
|
const readJsonFile = (filePath, fallback) => {
|
|
50951
52047
|
if (!NFS.existsSync(filePath)) return fallback;
|
|
50952
52048
|
const content = NFS.readFileSync(filePath, "utf8").trim();
|
|
50953
52049
|
if (content.length === 0) return fallback;
|
|
50954
|
-
|
|
52050
|
+
try {
|
|
52051
|
+
return JSON.parse(content);
|
|
52052
|
+
} catch (error) {
|
|
52053
|
+
if (error instanceof SyntaxError) throw new CliInputError(`Could not parse ${filePath}: the file contains invalid JSON. Fix the syntax errors in this file and re-run the install command.`);
|
|
52054
|
+
throw error;
|
|
52055
|
+
}
|
|
50955
52056
|
};
|
|
50956
|
-
const
|
|
50957
|
-
|
|
50958
|
-
|
|
52057
|
+
const ensureDirectoryExists = (directoryPath) => {
|
|
52058
|
+
try {
|
|
52059
|
+
NFS.mkdirSync(directoryPath, { recursive: true });
|
|
52060
|
+
} catch (error) {
|
|
52061
|
+
const code = error.code;
|
|
52062
|
+
if (code === "EACCES" || code === "EPERM") throw new CliInputError(`Could not create directory ${directoryPath}: permission denied. Ensure you have write permissions for this location and re-run the install command.`);
|
|
52063
|
+
if (code === "ENOTDIR" || code === "EEXIST") throw new CliInputError(`Could not create directory ${directoryPath}: a file exists at this path or one of its parent paths. Remove the conflicting file and re-run the install command.`);
|
|
52064
|
+
throw error;
|
|
52065
|
+
}
|
|
52066
|
+
};
|
|
52067
|
+
const writeJsonFileWithDirectoryCheck = (filePath, value) => {
|
|
52068
|
+
ensureDirectoryExists(Path.dirname(filePath));
|
|
52069
|
+
writeJsonFile(filePath, value);
|
|
50959
52070
|
};
|
|
50960
52071
|
const writeHookScript = (filePath) => {
|
|
50961
|
-
|
|
52072
|
+
ensureDirectoryExists(Path.dirname(filePath));
|
|
50962
52073
|
NFS.writeFileSync(filePath, buildAgentHookScript());
|
|
50963
52074
|
NFS.chmodSync(filePath, 493);
|
|
50964
52075
|
};
|
|
@@ -50974,7 +52085,7 @@ const installClaudeHook = (projectRoot) => {
|
|
|
50974
52085
|
command: CLAUDE_HOOK_COMMAND
|
|
50975
52086
|
}] });
|
|
50976
52087
|
hooks.PostToolBatch = postToolBatchHooks;
|
|
50977
|
-
|
|
52088
|
+
writeJsonFileWithDirectoryCheck(settingsPath, {
|
|
50978
52089
|
...settings,
|
|
50979
52090
|
hooks
|
|
50980
52091
|
});
|
|
@@ -50994,7 +52105,7 @@ const installCursorHook = (projectRoot) => {
|
|
|
50994
52105
|
timeout: 120
|
|
50995
52106
|
});
|
|
50996
52107
|
hooks.postToolUse = postToolUseHooks;
|
|
50997
|
-
|
|
52108
|
+
writeJsonFileWithDirectoryCheck(configPath, {
|
|
50998
52109
|
...config,
|
|
50999
52110
|
version: config.version ?? CURSOR_HOOKS_SCHEMA_VERSION,
|
|
51000
52111
|
hooks
|
|
@@ -51230,7 +52341,7 @@ const installPackageJsonHook = (options, strategy) => {
|
|
|
51230
52341
|
parent = cloned;
|
|
51231
52342
|
}
|
|
51232
52343
|
parent[leafKey] = strategy.leafShape === "array" ? appendArrayCommand(parent[leafKey]) : appendStringCommand(parent[leafKey]);
|
|
51233
|
-
writeJsonFile
|
|
52344
|
+
writeJsonFile(packageJsonPath, nextPackageJson);
|
|
51234
52345
|
removeLegacyManagedRunner(options.projectRoot);
|
|
51235
52346
|
return {
|
|
51236
52347
|
hookPath: packageJsonPath,
|
|
@@ -51813,6 +52924,9 @@ const runInstallReactDoctor = async (options = {}) => {
|
|
|
51813
52924
|
const shouldUpgradeWorkflow = canUpgradeWorkflow && (Boolean(options.yes) || upgradePromptOutcome === "yes");
|
|
51814
52925
|
if (upgradePromptOutcome === "no" && !options.dryRun) recordActionUpgradeDecision(projectRoot, "declined");
|
|
51815
52926
|
if ((ciPromptOutcome === "yes" || ciPromptOutcome === "no") && !options.dryRun) recordCiPromptDecision(projectRoot, ciPromptOutcome === "yes" ? "accepted" : "declined");
|
|
52927
|
+
const rememberedAgents = options.lastSelectedAgents ?? readInstallAgents();
|
|
52928
|
+
const defaultSelectedAgents = computeDefaultSelectedAgents(detectedAgents, rememberedAgents);
|
|
52929
|
+
const usedRememberedAgents = rememberedAgents.some((agent) => detectedAgents.includes(agent));
|
|
51816
52930
|
const selectedAgents = skipPrompts ? detectedAgents : (await prompt({
|
|
51817
52931
|
type: "multiselect",
|
|
51818
52932
|
name: "agents",
|
|
@@ -51820,12 +52934,13 @@ const runInstallReactDoctor = async (options = {}) => {
|
|
|
51820
52934
|
choices: detectedAgents.map((agent) => ({
|
|
51821
52935
|
title: getSkillAgentConfig(agent).displayName,
|
|
51822
52936
|
value: agent,
|
|
51823
|
-
selected:
|
|
52937
|
+
selected: defaultSelectedAgents.includes(agent)
|
|
51824
52938
|
})),
|
|
51825
52939
|
instructions: false,
|
|
51826
52940
|
min: 1
|
|
51827
52941
|
}, promptOptions)).agents ?? [];
|
|
51828
52942
|
if (selectedAgents.length === 0) return;
|
|
52943
|
+
if (!skipPrompts && !options.dryRun) rememberInstallAgents(selectedAgents);
|
|
51829
52944
|
let dependencyResult;
|
|
51830
52945
|
if (!options.dryRun) {
|
|
51831
52946
|
await installReactDoctorSkillStep(sourceDir, selectedAgents, projectRoot);
|
|
@@ -51884,6 +52999,8 @@ const runInstallReactDoctor = async (options = {}) => {
|
|
|
51884
52999
|
}
|
|
51885
53000
|
recordCount(METRIC.installCompleted, 1, {
|
|
51886
53001
|
agentsCount: selectedAgents.length,
|
|
53002
|
+
agentsDetected: detectedAgents.length,
|
|
53003
|
+
usedRememberedAgents,
|
|
51887
53004
|
gitHook: shouldInstallGitHook,
|
|
51888
53005
|
agentHooks: shouldInstallAgentHooks,
|
|
51889
53006
|
workflow: didInstallWorkflow,
|
|
@@ -52024,7 +53141,12 @@ const upgradeGitHubActionsWorkflow = async (workflow) => {
|
|
|
52024
53141
|
prBody: UPGRADE_PR_BODY
|
|
52025
53142
|
});
|
|
52026
53143
|
if (pullRequestResult.status === "pr-opened") upgradeSpinner.succeed(`Opened pull request for review: ${highlighter.info(pullRequestResult.url)}`);
|
|
52027
|
-
else if (pullRequestResult.status === "
|
|
53144
|
+
else if (pullRequestResult.status === "pr-exists") {
|
|
53145
|
+
try {
|
|
53146
|
+
NFS.writeFileSync(workflow.workflowPath, workflow.content);
|
|
53147
|
+
} catch {}
|
|
53148
|
+
upgradeSpinner.succeed(`A React Doctor pull request is already open: ${highlighter.info(pullRequestResult.url)}`);
|
|
53149
|
+
} 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
53150
|
else {
|
|
52029
53151
|
upgradeSpinner.stop();
|
|
52030
53152
|
try {
|
|
@@ -52077,29 +53199,34 @@ const handoffToAgent = async (input) => {
|
|
|
52077
53199
|
outcome: "ci-suppressed",
|
|
52078
53200
|
diagnosticsCount: input.diagnostics.length
|
|
52079
53201
|
});
|
|
53202
|
+
const choices = [
|
|
53203
|
+
...(await detectLaunchableAgents()).map((agentId) => ({
|
|
53204
|
+
title: getSkillAgentConfig(agentId).displayName,
|
|
53205
|
+
description: `Open ${CLI_AGENT_BINARIES[agentId]} here with the top issues as a prompt`,
|
|
53206
|
+
value: agentId
|
|
53207
|
+
})),
|
|
53208
|
+
{
|
|
53209
|
+
title: "Copy prompt to clipboard",
|
|
53210
|
+
description: "Paste into any agent or chat",
|
|
53211
|
+
value: CLIPBOARD_CHOICE
|
|
53212
|
+
},
|
|
53213
|
+
{
|
|
53214
|
+
title: "Skip",
|
|
53215
|
+
description: "Don't hand off",
|
|
53216
|
+
value: SKIP_CHOICE
|
|
53217
|
+
}
|
|
53218
|
+
];
|
|
53219
|
+
const rememberedTarget = readHandoffTarget();
|
|
53220
|
+
const rememberedChoiceIndex = choices.findIndex((choice) => choice.value === rememberedTarget);
|
|
53221
|
+
const initial = rememberedChoiceIndex >= 0 ? rememberedChoiceIndex : 0;
|
|
52080
53222
|
const { handoffTarget } = await prompts({
|
|
52081
53223
|
type: "select",
|
|
52082
53224
|
name: "handoffTarget",
|
|
52083
53225
|
message: "What would you like to do next?",
|
|
52084
|
-
choices
|
|
52085
|
-
|
|
52086
|
-
title: getSkillAgentConfig(agentId).displayName,
|
|
52087
|
-
description: `Open ${CLI_AGENT_BINARIES[agentId]} here with the top issues as a prompt`,
|
|
52088
|
-
value: agentId
|
|
52089
|
-
})),
|
|
52090
|
-
{
|
|
52091
|
-
title: "Copy prompt to clipboard",
|
|
52092
|
-
description: "Paste into any agent or chat",
|
|
52093
|
-
value: CLIPBOARD_CHOICE
|
|
52094
|
-
},
|
|
52095
|
-
{
|
|
52096
|
-
title: "Skip",
|
|
52097
|
-
description: "Don't hand off",
|
|
52098
|
-
value: SKIP_CHOICE
|
|
52099
|
-
}
|
|
52100
|
-
],
|
|
52101
|
-
initial: 0
|
|
53226
|
+
choices,
|
|
53227
|
+
initial
|
|
52102
53228
|
}, { onCancel: () => true });
|
|
53229
|
+
if (handoffTarget !== void 0) rememberHandoffTarget(handoffTarget);
|
|
52103
53230
|
let handoffOutcome = "launch";
|
|
52104
53231
|
if (handoffTarget === void 0) handoffOutcome = "cancel";
|
|
52105
53232
|
else if (handoffTarget === SKIP_CHOICE) handoffOutcome = "skip";
|
|
@@ -52107,7 +53234,9 @@ const handoffToAgent = async (input) => {
|
|
|
52107
53234
|
recordCount(METRIC.agentHandoff, 1, {
|
|
52108
53235
|
outcome: handoffOutcome,
|
|
52109
53236
|
agent: handoffOutcome === "launch" ? handoffTarget : void 0,
|
|
52110
|
-
diagnosticsCount: input.diagnostics.length
|
|
53237
|
+
diagnosticsCount: input.diagnostics.length,
|
|
53238
|
+
defaultRemembered: rememberedChoiceIndex >= 0,
|
|
53239
|
+
keptDefault: handoffTarget === choices[initial].value
|
|
52111
53240
|
});
|
|
52112
53241
|
if (handoffTarget === void 0 || handoffTarget === SKIP_CHOICE) return;
|
|
52113
53242
|
const payload = buildHandoffPayload({
|
|
@@ -52138,6 +53267,47 @@ const handoffToAgent = async (input) => {
|
|
|
52138
53267
|
}
|
|
52139
53268
|
};
|
|
52140
53269
|
//#endregion
|
|
53270
|
+
//#region src/cli/utils/migrate-action-pin.ts
|
|
53271
|
+
const WORKFLOWS_DIRECTORY = Path.join(".github", "workflows");
|
|
53272
|
+
const RECOMMENDED_ACTION_REF = "v2";
|
|
53273
|
+
const MUTABLE_ACTION_REF = /(uses:\s*[\w.-]+\/react-doctor@)(?:main|master)\b/g;
|
|
53274
|
+
const isWorkflowFile = (fileName) => /\.ya?ml$/.test(fileName);
|
|
53275
|
+
/**
|
|
53276
|
+
* Rewrites mutable `@main` / `@master` React Doctor GitHub Action references in
|
|
53277
|
+
* the repo's `.github/workflows/*.yml` to the recommended floating major
|
|
53278
|
+
* (`@v2`) — a supply-chain hardening (#299) that also moves the workflow onto
|
|
53279
|
+
* the current (install- and scan-cached) action release. Pinned tags / SHAs are
|
|
53280
|
+
* deliberate and left untouched. Returns the absolute paths of the workflow
|
|
53281
|
+
* files it rewrote — empty when there's nothing to migrate.
|
|
53282
|
+
*/
|
|
53283
|
+
const migrateActionPin = (projectRoot) => {
|
|
53284
|
+
const workflowsDirectory = Path.join(projectRoot, WORKFLOWS_DIRECTORY);
|
|
53285
|
+
let entries;
|
|
53286
|
+
try {
|
|
53287
|
+
entries = NFS.readdirSync(workflowsDirectory, { withFileTypes: true });
|
|
53288
|
+
} catch {
|
|
53289
|
+
return [];
|
|
53290
|
+
}
|
|
53291
|
+
const rewrittenFiles = [];
|
|
53292
|
+
for (const entry of entries) {
|
|
53293
|
+
if (!entry.isFile() || !isWorkflowFile(entry.name)) continue;
|
|
53294
|
+
const workflowPath = Path.join(workflowsDirectory, entry.name);
|
|
53295
|
+
let contents;
|
|
53296
|
+
try {
|
|
53297
|
+
contents = NFS.readFileSync(workflowPath, "utf-8");
|
|
53298
|
+
} catch {
|
|
53299
|
+
continue;
|
|
53300
|
+
}
|
|
53301
|
+
const updated = contents.replace(MUTABLE_ACTION_REF, `$1${RECOMMENDED_ACTION_REF}`);
|
|
53302
|
+
if (updated === contents) continue;
|
|
53303
|
+
try {
|
|
53304
|
+
NFS.writeFileSync(workflowPath, updated);
|
|
53305
|
+
rewrittenFiles.push(workflowPath);
|
|
53306
|
+
} catch {}
|
|
53307
|
+
}
|
|
53308
|
+
return rewrittenFiles;
|
|
53309
|
+
};
|
|
53310
|
+
//#endregion
|
|
52141
53311
|
//#region src/cli/utils/read-object-file.ts
|
|
52142
53312
|
/**
|
|
52143
53313
|
* Reads a JSON / JSONC file as a plain object, or `null` when it is missing,
|
|
@@ -52202,6 +53372,35 @@ export default ${serializeTsObjectLiteral(config)} satisfies ReactDoctorConfig;
|
|
|
52202
53372
|
NFS.rmSync(legacy.legacyFilePath, { force: true });
|
|
52203
53373
|
return targetPath;
|
|
52204
53374
|
};
|
|
53375
|
+
const PROJECT_MIGRATIONS = [{
|
|
53376
|
+
id: "config-json-to-ts",
|
|
53377
|
+
scope: "project",
|
|
53378
|
+
run: ({ projectRoot }) => {
|
|
53379
|
+
if (projectRoot === void 0) return false;
|
|
53380
|
+
const legacyConfig = findLegacyConfig(projectRoot);
|
|
53381
|
+
if (!legacyConfig) return false;
|
|
53382
|
+
const migratedPath = migrateLegacyConfig(legacyConfig);
|
|
53383
|
+
if (!migratedPath) return false;
|
|
53384
|
+
cliLogger.success("Migrated react-doctor.config.json → doctor.config.ts");
|
|
53385
|
+
cliLogger.dim(` Your settings were preserved. Review ${toRelativePath(migratedPath, projectRoot)} and commit it.`);
|
|
53386
|
+
cliLogger.break();
|
|
53387
|
+
return true;
|
|
53388
|
+
}
|
|
53389
|
+
}, {
|
|
53390
|
+
id: "action-pin-main-to-v2",
|
|
53391
|
+
scope: "project",
|
|
53392
|
+
run: ({ projectRoot }) => {
|
|
53393
|
+
if (projectRoot === void 0) return false;
|
|
53394
|
+
const rewrittenFiles = migrateActionPin(projectRoot);
|
|
53395
|
+
if (rewrittenFiles.length === 0) return false;
|
|
53396
|
+
const relativeFiles = rewrittenFiles.map((file) => toRelativePath(file, projectRoot)).join(", ");
|
|
53397
|
+
cliLogger.success(`Pinned the React Doctor action to @v2 in ${relativeFiles}`);
|
|
53398
|
+
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.");
|
|
53399
|
+
cliLogger.break();
|
|
53400
|
+
return true;
|
|
53401
|
+
}
|
|
53402
|
+
}];
|
|
53403
|
+
const runProjectMigrations = (projectRoot, options = {}) => runMigrations(PROJECT_MIGRATIONS, { projectRoot }, options);
|
|
52205
53404
|
//#endregion
|
|
52206
53405
|
//#region src/cli/utils/print-branded-header.ts
|
|
52207
53406
|
/**
|
|
@@ -52421,36 +53620,16 @@ const printMultiProjectSummary = (input) => gen(function* () {
|
|
|
52421
53620
|
});
|
|
52422
53621
|
//#endregion
|
|
52423
53622
|
//#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
|
-
};
|
|
53623
|
+
const SETUP_HINT_GATE = {
|
|
53624
|
+
id: SETUP_HINT_EVENT,
|
|
53625
|
+
scope: "project",
|
|
53626
|
+
fireWhenUnknown: true
|
|
53627
|
+
};
|
|
53628
|
+
const hasDisabledSetupPrompt = (projectRoot, options = {}) => !isGatePending(SETUP_HINT_GATE, { projectRoot }, options);
|
|
53629
|
+
const disableSetupPrompt = (projectRoot, options = {}) => recordGate(SETUP_HINT_GATE, {
|
|
53630
|
+
projectRoot,
|
|
53631
|
+
outcome: "declined"
|
|
53632
|
+
}, options);
|
|
52454
53633
|
const resolveInstallSetupProjectRoot = (options) => {
|
|
52455
53634
|
if (options.scanDirectories.length === 0) return findNearestPackageDirectory(options.scanRoot) ?? options.scanRoot;
|
|
52456
53635
|
const packageDirectories = /* @__PURE__ */ new Set();
|
|
@@ -52601,7 +53780,7 @@ const warnDeprecatedDiff = (flags, userConfig) => {
|
|
|
52601
53780
|
};
|
|
52602
53781
|
const warnDiffUnavailable = (requested, isQuiet) => {
|
|
52603
53782
|
if (isQuiet) return;
|
|
52604
|
-
if (typeof requested.base === "string") cliLogger.warn(`Could not compute diff against "${requested.base}" (
|
|
53783
|
+
if (typeof requested.base === "string") cliLogger.warn(`Could not compute diff against "${requested.base}" (git unavailable, ref not found, or merge-base failed). Running full scan.`);
|
|
52605
53784
|
else cliLogger.warn("No feature branch or uncommitted changes detected. Running full scan.");
|
|
52606
53785
|
cliLogger.break();
|
|
52607
53786
|
};
|
|
@@ -52978,15 +54157,9 @@ const buildChangedFilesDiffInfo = (changedFiles) => ({
|
|
|
52978
54157
|
* are left untouched — the loader still reads the legacy file as a deprecated
|
|
52979
54158
|
* fallback and warns — so a scan never mutates the repo unattended.
|
|
52980
54159
|
*/
|
|
52981
|
-
const maybeMigrateLegacyConfig = (requestedDirectory, { isQuiet, isStaged }) => {
|
|
54160
|
+
const maybeMigrateLegacyConfig = async (requestedDirectory, { isQuiet, isStaged }) => {
|
|
52982
54161
|
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();
|
|
54162
|
+
await runProjectMigrations(requestedDirectory);
|
|
52990
54163
|
};
|
|
52991
54164
|
const inspectAction = async (directory, flags) => {
|
|
52992
54165
|
const isScoreOnly = Boolean(flags.score);
|
|
@@ -53001,7 +54174,7 @@ const inspectAction = async (directory, flags) => {
|
|
|
53001
54174
|
recordCount(METRIC.cliInvoked, 1, { command: "inspect" });
|
|
53002
54175
|
try {
|
|
53003
54176
|
validateModeFlags(flags);
|
|
53004
|
-
maybeMigrateLegacyConfig(requestedDirectory, {
|
|
54177
|
+
await maybeMigrateLegacyConfig(requestedDirectory, {
|
|
53005
54178
|
isQuiet,
|
|
53006
54179
|
isStaged: Boolean(flags.staged)
|
|
53007
54180
|
});
|
|
@@ -53298,6 +54471,10 @@ const installAction = async (options, command) => {
|
|
|
53298
54471
|
projectRoot: options.cwd ?? process.cwd()
|
|
53299
54472
|
});
|
|
53300
54473
|
} catch (error) {
|
|
54474
|
+
if (isExpectedUserError(error)) {
|
|
54475
|
+
handleUserError(error);
|
|
54476
|
+
return;
|
|
54477
|
+
}
|
|
53301
54478
|
handleError(error, { sentryEventId: await reportErrorToSentry(error) });
|
|
53302
54479
|
}
|
|
53303
54480
|
};
|
|
@@ -54305,4 +55482,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
|
|
|
54305
55482
|
export {};
|
|
54306
55483
|
|
|
54307
55484
|
//# sourceMappingURL=cli.js.map
|
|
54308
|
-
//# debugId=
|
|
55485
|
+
//# debugId=6c903b87-8faf-5702-b078-03465e326385
|