react-doctor 0.5.7 → 0.5.8-dev.0c19858
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 +1759 -441
- package/dist/index.d.ts +15 -1
- package/dist/index.js +1082 -196
- package/dist/lsp.js +1083 -196
- 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]="1bdeba5a-5ce9-5f4f-b9b0-df7abd4c9c0b")}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
|
|
@@ -9041,7 +9041,7 @@ const composePassthrough = /* @__PURE__ */ dual(2, (left, right) => (input) => {
|
|
|
9041
9041
|
* @since 2.0.0
|
|
9042
9042
|
*/
|
|
9043
9043
|
const Scheduler = /* @__PURE__ */ Reference("effect/Scheduler", { defaultValue: () => new MixedScheduler() });
|
|
9044
|
-
const setImmediate = "setImmediate" in globalThis ? (f) => {
|
|
9044
|
+
const setImmediate$1 = "setImmediate" in globalThis ? (f) => {
|
|
9045
9045
|
const timer = globalThis.setImmediate(f);
|
|
9046
9046
|
return () => globalThis.clearImmediate(timer);
|
|
9047
9047
|
} : (f) => {
|
|
@@ -9085,7 +9085,7 @@ var PriorityBuckets = class {
|
|
|
9085
9085
|
var MixedScheduler = class {
|
|
9086
9086
|
executionMode;
|
|
9087
9087
|
setImmediate;
|
|
9088
|
-
constructor(executionMode = "async", setImmediateFn = setImmediate) {
|
|
9088
|
+
constructor(executionMode = "async", setImmediateFn = setImmediate$1) {
|
|
9089
9089
|
this.executionMode = executionMode;
|
|
9090
9090
|
this.setImmediate = setImmediateFn;
|
|
9091
9091
|
}
|
|
@@ -9110,7 +9110,7 @@ var MixedSchedulerDispatcher = class {
|
|
|
9110
9110
|
tasks = /* @__PURE__ */ new PriorityBuckets();
|
|
9111
9111
|
running = void 0;
|
|
9112
9112
|
setImmediate;
|
|
9113
|
-
constructor(setImmediateFn = setImmediate) {
|
|
9113
|
+
constructor(setImmediateFn = setImmediate$1) {
|
|
9114
9114
|
this.setImmediate = setImmediateFn;
|
|
9115
9115
|
}
|
|
9116
9116
|
/**
|
|
@@ -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,17 @@ 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_WORKER_MEM_BUDGET_BYTES = 2 * 1024 * 1024 * 1024;
|
|
36951
|
+
const DEAD_CODE_TIMEOUT_CEILING_MS = 6e5;
|
|
36952
|
+
const DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS = 3e4;
|
|
36953
|
+
const DEAD_CODE_OVERLAP_PARSE_SHARE = .4;
|
|
36842
36954
|
const RECOMMENDED_PNPM_MINIMUM_RELEASE_AGE_MINUTES = 10080;
|
|
36843
36955
|
const REACT_SERVER_DOM_PACKAGES = [
|
|
36844
36956
|
"react-server-dom-webpack",
|
|
@@ -36873,9 +36985,13 @@ const CONFIG_CACHE_TTL_MS = 300 * 1e3;
|
|
|
36873
36985
|
const SOCKET_FREE_PURL_API_BASE = "https://firewall-api.socket.dev/purl";
|
|
36874
36986
|
const SOCKET_PACKAGE_PAGE_BASE = "https://socket.dev/npm/package";
|
|
36875
36987
|
const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
|
|
36988
|
+
const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
|
|
36989
|
+
const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
|
|
36876
36990
|
const SUPPLY_CHAIN_PLUGIN = "socket";
|
|
36877
36991
|
const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
|
|
36878
36992
|
const SUPPLY_CHAIN_CATEGORY = "Security";
|
|
36993
|
+
const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
|
|
36994
|
+
const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
|
|
36879
36995
|
const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
|
|
36880
36996
|
const TSCONFIG_FILENAME = "tsconfig.json";
|
|
36881
36997
|
const isRelativeExtendsValue = (extendsValue) => extendsValue.startsWith("./") || extendsValue.startsWith("../") || Path.isAbsolute(extendsValue);
|
|
@@ -37565,7 +37681,10 @@ for (const [legacyRuleKey, nativeRuleKey] of Object.entries(LEGACY_RULE_KEY_TO_N
|
|
|
37565
37681
|
NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.set(nativeRuleKey, aliases);
|
|
37566
37682
|
}
|
|
37567
37683
|
const getLegacyRuleKeysForNative = (ruleKey) => NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(ruleKey) ?? [];
|
|
37568
|
-
const canonicalizeRuleKey = (ruleKey) =>
|
|
37684
|
+
const canonicalizeRuleKey = (ruleKey) => {
|
|
37685
|
+
const nativeRuleKey = LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey];
|
|
37686
|
+
return typeof nativeRuleKey === "string" ? nativeRuleKey : ruleKey;
|
|
37687
|
+
};
|
|
37569
37688
|
const isReactDoctorShortIdOf = (bareRuleKey, qualifiedRuleKey) => !bareRuleKey.includes("/") && qualifiedRuleKey === `react-doctor/${bareRuleKey}`;
|
|
37570
37689
|
const isSameRuleKey = (candidateRuleKey, targetRuleKey) => {
|
|
37571
37690
|
const canonicalCandidate = canonicalizeRuleKey(candidateRuleKey);
|
|
@@ -38227,6 +38346,11 @@ var OxlintBatchExceeded = class extends TaggedErrorClass()("OxlintBatchExceeded"
|
|
|
38227
38346
|
}
|
|
38228
38347
|
}
|
|
38229
38348
|
};
|
|
38349
|
+
var ScanDeadlineExceeded = class extends TaggedErrorClass()("ScanDeadlineExceeded", { detail: String$1 }) {
|
|
38350
|
+
get message() {
|
|
38351
|
+
return `Scan exceeded its overall time budget: ${this.detail}`;
|
|
38352
|
+
}
|
|
38353
|
+
};
|
|
38230
38354
|
var OxlintSpawnFailed = class extends TaggedErrorClass()("OxlintSpawnFailed", { cause: Unknown }) {
|
|
38231
38355
|
get message() {
|
|
38232
38356
|
return `Failed to run oxlint: ${pretty(fail$6(this.cause))}`;
|
|
@@ -38290,6 +38414,7 @@ var GitBaseBranchInvalid = class extends TaggedErrorClass()("GitBaseBranchInvali
|
|
|
38290
38414
|
const ReactDoctorErrorReason = Union([
|
|
38291
38415
|
OxlintUnavailable,
|
|
38292
38416
|
OxlintBatchExceeded,
|
|
38417
|
+
ScanDeadlineExceeded,
|
|
38293
38418
|
OxlintSpawnFailed,
|
|
38294
38419
|
OxlintOutputUnparseable,
|
|
38295
38420
|
ConfigParseFailed,
|
|
@@ -38362,15 +38487,105 @@ const layerOtlp = unwrap$3(gen(function* () {
|
|
|
38362
38487
|
}).pipe(provide$2(layer$9));
|
|
38363
38488
|
}).pipe(orDie));
|
|
38364
38489
|
/**
|
|
38365
|
-
*
|
|
38366
|
-
* `
|
|
38367
|
-
|
|
38490
|
+
* Read a positive-millisecond timeout from an env var, falling back to
|
|
38491
|
+
* `defaultMs` when the var is unset, non-finite, or not strictly positive.
|
|
38492
|
+
*/
|
|
38493
|
+
const readPositiveEnvMs = (envVarName, defaultMs) => {
|
|
38494
|
+
const rawValue = process.env[envVarName];
|
|
38495
|
+
if (rawValue === void 0) return defaultMs;
|
|
38496
|
+
const parsedValue = Number(rawValue);
|
|
38497
|
+
if (!Number.isFinite(parsedValue) || parsedValue <= 0) return defaultMs;
|
|
38498
|
+
return parsedValue;
|
|
38499
|
+
};
|
|
38500
|
+
const CGROUP_V2_MEMORY_MAX_PATH = "/sys/fs/cgroup/memory.max";
|
|
38501
|
+
const CGROUP_V1_MEMORY_LIMIT_PATH = "/sys/fs/cgroup/memory/memory.limit_in_bytes";
|
|
38502
|
+
const CGROUP_UNLIMITED_SENTINEL_BYTES = Number.MAX_SAFE_INTEGER;
|
|
38503
|
+
/**
|
|
38504
|
+
* Parses one raw cgroup memory-limit file value into a positive byte count, or
|
|
38505
|
+
* `undefined` when it represents "no limit" (the v2 `"max"` literal, an empty
|
|
38506
|
+
* read, a non-positive / non-finite value, or v1's near-2^63 unlimited
|
|
38507
|
+
* sentinel). Pure and exported so the classification is unit-testable without
|
|
38508
|
+
* touching the filesystem.
|
|
38509
|
+
*/
|
|
38510
|
+
const parseCgroupMemoryLimitBytes = (raw) => {
|
|
38511
|
+
if (raw === void 0) return void 0;
|
|
38512
|
+
const trimmed = raw.trim();
|
|
38513
|
+
if (trimmed === "" || trimmed === "max") return void 0;
|
|
38514
|
+
const parsed = Number(trimmed);
|
|
38515
|
+
if (!Number.isFinite(parsed) || parsed <= 0 || parsed >= CGROUP_UNLIMITED_SENTINEL_BYTES) return;
|
|
38516
|
+
return parsed;
|
|
38517
|
+
};
|
|
38518
|
+
const CGROUP_MEMORY_LIMIT_PATHS = [CGROUP_V2_MEMORY_MAX_PATH, CGROUP_V1_MEMORY_LIMIT_PATH];
|
|
38519
|
+
/**
|
|
38520
|
+
* Reads this process's cgroup memory limit in bytes from the first candidate
|
|
38521
|
+
* path that yields a real limit, or `undefined` when none does — no cgroup, no
|
|
38522
|
+
* limit, or the files are unreadable (e.g. macOS / Windows dev machines).
|
|
38523
|
+
* `os.totalmem()` reports the HOST total and ignores cgroup memory limits, so a
|
|
38524
|
+
* memory-constrained container over-reports total memory; `resolveAutoScan-
|
|
38525
|
+
* Concurrency` takes `min(totalmem, this)` to honor the limit.
|
|
38526
|
+
*
|
|
38527
|
+
* The cgroup v2 read is the mount-root `memory.max`, which IS the container's
|
|
38528
|
+
* limit under the standard cgroup-namespace setup CI runners use (the
|
|
38529
|
+
* container's own cgroup is the root of its namespaced view). A process in a
|
|
38530
|
+
* non-namespaced nested/delegated cgroup whose root reads `"max"` is not
|
|
38531
|
+
* detected here and falls back to the host total; the EAGAIN/ENOMEM serial
|
|
38532
|
+
* replay in `spawnLintBatches` remains the runtime backstop for that case.
|
|
38533
|
+
*
|
|
38534
|
+
* `candidatePaths` is injectable so tests exercise the v2-wins-over-v1
|
|
38535
|
+
* precedence, the skip-unreadable fallback, and the all-missing case without a
|
|
38536
|
+
* real `/sys/fs/cgroup`.
|
|
38537
|
+
*/
|
|
38538
|
+
const readCgroupMemoryLimitBytes = (candidatePaths = CGROUP_MEMORY_LIMIT_PATHS) => {
|
|
38539
|
+
for (const limitPath of candidatePaths) {
|
|
38540
|
+
let raw;
|
|
38541
|
+
try {
|
|
38542
|
+
raw = fs.readFileSync(limitPath, "utf8");
|
|
38543
|
+
} catch {
|
|
38544
|
+
continue;
|
|
38545
|
+
}
|
|
38546
|
+
const limitBytes = parseCgroupMemoryLimitBytes(raw);
|
|
38547
|
+
if (limitBytes !== void 0) return limitBytes;
|
|
38548
|
+
}
|
|
38549
|
+
};
|
|
38550
|
+
/**
|
|
38551
|
+
* Clamps a requested lint worker count to `[MIN_SCAN_CONCURRENCY,
|
|
38552
|
+
* HARD_MAX_SCAN_CONCURRENCY]` as a finite integer. This is the explicit-pin and
|
|
38553
|
+
* spawn-boundary clamp — the memory-and-core-budgeted auto count comes from
|
|
38554
|
+
* `resolveAutoScanConcurrency`. Out-of-range or non-finite requests degrade to
|
|
38368
38555
|
* `MIN_SCAN_CONCURRENCY` rather than oversubscribing or running zero workers.
|
|
38369
38556
|
*/
|
|
38370
38557
|
const resolveScanConcurrency = (requested) => {
|
|
38371
|
-
|
|
38372
|
-
|
|
38373
|
-
|
|
38558
|
+
if (!Number.isFinite(requested) || requested < 1) return 1;
|
|
38559
|
+
return Math.min(Math.floor(requested), 32);
|
|
38560
|
+
};
|
|
38561
|
+
const readSystemFacts$1 = () => ({
|
|
38562
|
+
availableCores: os.availableParallelism(),
|
|
38563
|
+
totalMemoryBytes: os.totalmem(),
|
|
38564
|
+
cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
|
|
38565
|
+
});
|
|
38566
|
+
/**
|
|
38567
|
+
* Auto lint-worker count: the smaller of the (cgroup-CPU-aware) core count and
|
|
38568
|
+
* the number of `PER_WORKER_MEM_BUDGET_BYTES` workers that fit in available
|
|
38569
|
+
* memory, then clamped to `[MIN, HARD_MAX]` by `resolveScanConcurrency`.
|
|
38570
|
+
*
|
|
38571
|
+
* `os.availableParallelism()` already respects cgroup CPU quotas, so the core
|
|
38572
|
+
* term needs no help. Available memory is `os.totalmem()` floored by the cgroup
|
|
38573
|
+
* memory limit — `os.freemem()` is deliberately NOT used: it excludes
|
|
38574
|
+
* reclaimable page cache and reads near-zero on macOS / cache-heavy Linux, which
|
|
38575
|
+
* would collapse the auto path to a single worker. `os.totalmem()` reports the
|
|
38576
|
+
* host total even inside a container, so the cgroup limit (read directly,
|
|
38577
|
+
* because Node doesn't fold it into `totalmem()`) is the real ceiling there.
|
|
38578
|
+
*
|
|
38579
|
+
* `facts` is injectable so tests exercise core-bound, memory-bound, cgroup-
|
|
38580
|
+
* limited, and ceiling cases without mocking `os` or the filesystem.
|
|
38581
|
+
*/
|
|
38582
|
+
const resolveAutoScanConcurrency = (facts = readSystemFacts$1()) => {
|
|
38583
|
+
const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
|
|
38584
|
+
const memoryBoundedWorkers = Math.floor(availableMemoryBytes / PER_WORKER_MEM_BUDGET_BYTES);
|
|
38585
|
+
return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
|
|
38586
|
+
};
|
|
38587
|
+
const resolveLintBatchOrdering = () => {
|
|
38588
|
+
return process.env["REACT_DOCTOR_LINT_BATCH_ORDERING"]?.trim().toLowerCase() === "cost" ? "cost" : "arrival";
|
|
38374
38589
|
};
|
|
38375
38590
|
/**
|
|
38376
38591
|
* Per-batch oxlint wall-clock budget. Reads from the env var on
|
|
@@ -38378,11 +38593,38 @@ const resolveScanConcurrency = (requested) => {
|
|
|
38378
38593
|
* microVMs without recompiling react-doctor. Tests override via
|
|
38379
38594
|
* `Layer.succeed(OxlintSpawnTimeoutMs, ...)`.
|
|
38380
38595
|
*/
|
|
38381
|
-
var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => {
|
|
38382
|
-
|
|
38383
|
-
|
|
38596
|
+
var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS", OXLINT_SPAWN_TIMEOUT_MS) }) {};
|
|
38597
|
+
/**
|
|
38598
|
+
* Effect-side cap on the lint phase. The env var lets CI / eval runners
|
|
38599
|
+
* raise the phase budget for slow large repos without recompiling.
|
|
38600
|
+
* Tests override via `Layer.succeed(LintPhaseTimeoutMs, ...)`.
|
|
38601
|
+
*/
|
|
38602
|
+
var LintPhaseTimeoutMs = class extends Reference("react-doctor/LintPhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_LINT_PHASE_TIMEOUT_MS", LINT_PHASE_TIMEOUT_MS) }) {};
|
|
38603
|
+
/**
|
|
38604
|
+
* Effect-side cap on the dead-code phase, sitting above the in-worker
|
|
38605
|
+
* timeout as a runtime-independent backstop. The env var raises it for
|
|
38606
|
+
* type-heavy projects; tests override via
|
|
38607
|
+
* `Layer.succeed(DeadCodePhaseTimeoutMs, ...)`.
|
|
38608
|
+
*/
|
|
38609
|
+
var DeadCodePhaseTimeoutMs = class extends Reference("react-doctor/DeadCodePhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_DEAD_CODE_PHASE_TIMEOUT_MS", DEAD_CODE_PHASE_TIMEOUT_MS) }) {};
|
|
38610
|
+
/**
|
|
38611
|
+
* Overall scan deadline backstop, bounding everything the per-phase
|
|
38612
|
+
* timeouts don't (wedged git / IO). The env var raises it for very
|
|
38613
|
+
* large repos; tests override via `Layer.succeed(ScanDeadlineMs, ...)`.
|
|
38614
|
+
*/
|
|
38615
|
+
var ScanDeadlineMs = class extends Reference("react-doctor/ScanDeadlineMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_SCAN_DEADLINE_MS", SCAN_TOTAL_DEADLINE_MS) }) {};
|
|
38616
|
+
/**
|
|
38617
|
+
* Wall-clock budget for the supply-chain check when it runs on a background
|
|
38618
|
+
* fiber overlapping the lint pass. Reads from the env var on startup so the
|
|
38619
|
+
* eval harness can raise the budget under sandbox microVMs (slower network)
|
|
38620
|
+
* without recompiling react-doctor. Tests override via
|
|
38621
|
+
* `Layer.succeed(SupplyChainOverlapTimeoutMs, ...)`.
|
|
38622
|
+
*/
|
|
38623
|
+
var SupplyChainOverlapTimeoutMs = class extends Reference("react-doctor/SupplyChainOverlapTimeoutMs", { defaultValue: () => {
|
|
38624
|
+
const raw = process.env["REACT_DOCTOR_SUPPLY_CHAIN_TIMEOUT_MS"];
|
|
38625
|
+
if (raw === void 0) return SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS;
|
|
38384
38626
|
const parsed = Number(raw);
|
|
38385
|
-
if (!Number.isFinite(parsed) || parsed <= 0) return
|
|
38627
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS;
|
|
38386
38628
|
return parsed;
|
|
38387
38629
|
} }) {};
|
|
38388
38630
|
/**
|
|
@@ -38393,31 +38635,93 @@ var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTime
|
|
|
38393
38635
|
*/
|
|
38394
38636
|
var OxlintOutputMaxBytes = class extends Reference("react-doctor/OxlintOutputMaxBytes", { defaultValue: () => OXLINT_OUTPUT_MAX_BYTES }) {};
|
|
38395
38637
|
/**
|
|
38396
|
-
* Number of oxlint subprocesses the lint pass runs in parallel. Defaults
|
|
38397
|
-
*
|
|
38398
|
-
* the box
|
|
38399
|
-
*
|
|
38400
|
-
*
|
|
38401
|
-
*
|
|
38402
|
-
*
|
|
38403
|
-
*
|
|
38404
|
-
*
|
|
38405
|
-
*
|
|
38638
|
+
* Number of oxlint subprocesses the lint pass runs in parallel. Defaults to a
|
|
38639
|
+
* memory-and-core-budgeted auto count (`resolveAutoScanConcurrency`) so large
|
|
38640
|
+
* repos scan fast out of the box without OOMing the native binding on a
|
|
38641
|
+
* high-core / low-memory box; `spawnLintBatches` transparently falls back to a
|
|
38642
|
+
* single worker if a parallel run still exhausts system resources. The CLI's
|
|
38643
|
+
* `--no-parallel` flag forces serial via `Layer.succeed`; the
|
|
38644
|
+
* `REACT_DOCTOR_PARALLEL` env var seeds the default for programmatic / CI
|
|
38645
|
+
* callers that never touch the flag — parallelism is opt-OUT, so only the
|
|
38646
|
+
* explicit serial values pin one worker:
|
|
38647
|
+
*
|
|
38648
|
+
* - unset / `auto` / `true` / `on` → memory-and-core-budgeted auto count
|
|
38406
38649
|
* - `0` / `false` / `off` → `1` (serial)
|
|
38407
38650
|
* - a positive integer → that many workers (clamped)
|
|
38408
|
-
* - any other value →
|
|
38651
|
+
* - any other value → memory-and-core-budgeted auto count
|
|
38409
38652
|
*
|
|
38410
38653
|
* The resolved value is always within
|
|
38411
|
-
* `[MIN_SCAN_CONCURRENCY,
|
|
38654
|
+
* `[MIN_SCAN_CONCURRENCY, HARD_MAX_SCAN_CONCURRENCY]`.
|
|
38412
38655
|
*/
|
|
38413
38656
|
var OxlintConcurrency = class extends Reference("react-doctor/OxlintConcurrency", { defaultValue: () => {
|
|
38414
38657
|
const raw = process.env["REACT_DOCTOR_PARALLEL"];
|
|
38415
|
-
if (raw === void 0) return
|
|
38658
|
+
if (raw === void 0) return resolveAutoScanConcurrency();
|
|
38416
38659
|
const normalized = raw.trim().toLowerCase();
|
|
38417
38660
|
if (normalized === "0" || normalized === "false" || normalized === "off") return 1;
|
|
38418
38661
|
const parsed = Number.parseInt(normalized, 10);
|
|
38419
38662
|
if (Number.isInteger(parsed) && parsed > 0) return resolveScanConcurrency(parsed);
|
|
38420
|
-
return
|
|
38663
|
+
return resolveAutoScanConcurrency();
|
|
38664
|
+
} }) {};
|
|
38665
|
+
/**
|
|
38666
|
+
* Three-state control for overlapping the dead-code pass with the lint pass —
|
|
38667
|
+
* forking dead-code as a child fiber that runs DURING lint instead of strictly
|
|
38668
|
+
* after it.
|
|
38669
|
+
*
|
|
38670
|
+
* - `"auto"` (default) / `"off"` → strictly SEQUENTIAL: dead-code runs after
|
|
38671
|
+
* lint with the full core budget. Both deslop's parse pool and the oxlint
|
|
38672
|
+
* pool are CPU-bound and each size themselves to all cores, so overlapping
|
|
38673
|
+
* them only oversubscribes (~2x the cores) and starves the parse pass past
|
|
38674
|
+
* its timeout — for no wall-clock win, since there are no spare cores to
|
|
38675
|
+
* absorb the second pass. Sequential is both faster per-phase and safe.
|
|
38676
|
+
* - `"on"` → force the overlap anyway. The orchestrator then SPLITS the core
|
|
38677
|
+
* budget (`DEAD_CODE_OVERLAP_PARSE_SHARE`): deslop's parse pool is capped
|
|
38678
|
+
* and lint shrinks to the remainder, so the two sum to the cores instead of
|
|
38679
|
+
* doubling them, and the dead-code timeout scales up for the reduced share.
|
|
38680
|
+
*
|
|
38681
|
+
* Seeded from `REACT_DOCTOR_DEAD_CODE_OVERLAP` so operators get a redeploy-free
|
|
38682
|
+
* switch; tests pin it via `Layer.succeed(DeadCodeOverlap, ...)`.
|
|
38683
|
+
*/
|
|
38684
|
+
var DeadCodeOverlap = class extends Reference("react-doctor/DeadCodeOverlap", { defaultValue: () => {
|
|
38685
|
+
const raw = process.env["REACT_DOCTOR_DEAD_CODE_OVERLAP"]?.trim().toLowerCase();
|
|
38686
|
+
if (raw === "on" || raw === "true" || raw === "1") return "on";
|
|
38687
|
+
if (raw === "off" || raw === "false" || raw === "0") return "off";
|
|
38688
|
+
return "auto";
|
|
38689
|
+
} }) {};
|
|
38690
|
+
/**
|
|
38691
|
+
* How the full-scan lint pass orders its file batches. `"arrival"` (the
|
|
38692
|
+
* default) keeps `git ls-files` discovery order. `"cost"` opts into LPT (feed
|
|
38693
|
+
* the largest files first); set `REACT_DOCTOR_LINT_BATCH_ORDERING=cost`. NOTE:
|
|
38694
|
+
* `cost` is OFF by default because the current sort-desc-then-chunk-100 packs
|
|
38695
|
+
* the heaviest files into one wave-1 batch — on size-skewed repos that mega-
|
|
38696
|
+
* batch is a straggler (and can trip the per-batch timeout + split), measurably
|
|
38697
|
+
* regressing the common full-scan case. LPT needs the heavy files SPREAD across
|
|
38698
|
+
* batches before `cost` earns the default. Tests override via
|
|
38699
|
+
* `Layer.succeed(LintBatchOrdering, ...)`. Diff / staged scans never reach this
|
|
38700
|
+
* — they pass user-scoped `includePaths` that skip discovery and stay in
|
|
38701
|
+
* arrival order; only the full-scan branch reads it.
|
|
38702
|
+
*/
|
|
38703
|
+
var LintBatchOrdering = class extends Reference("react-doctor/LintBatchOrdering", { defaultValue: resolveLintBatchOrdering }) {};
|
|
38704
|
+
const CACHE_DISABLED_VALUES$1 = new Set(["1", "true"]);
|
|
38705
|
+
/**
|
|
38706
|
+
* Whether the per-file lint cache (`runners/oxlint/file-lint-cache.ts`) is
|
|
38707
|
+
* active. Defaults ON — repeat scans re-lint only the files whose content
|
|
38708
|
+
* changed, and correctness is guaranteed byte-identical to a cold scan by the
|
|
38709
|
+
* always-fresh cross-file sidecar. Opt-OUT, two knobs (matching the whole-repo
|
|
38710
|
+
* scan cache's `REACT_DOCTOR_NO_CACHE`):
|
|
38711
|
+
*
|
|
38712
|
+
* - `REACT_DOCTOR_NO_CACHE` — the global off-switch; disables BOTH the
|
|
38713
|
+
* whole-repo scan cache and this per-file cache.
|
|
38714
|
+
* - `REACT_DOCTOR_NO_FILE_CACHE` — granular: bust only the per-file cache
|
|
38715
|
+
* while keeping the whole-repo short-circuit.
|
|
38716
|
+
*
|
|
38717
|
+
* Tests override via `Layer.succeed(PerFileLintCacheEnabled, false)`.
|
|
38718
|
+
*/
|
|
38719
|
+
var PerFileLintCacheEnabled = class extends Reference("react-doctor/PerFileLintCacheEnabled", { defaultValue: () => {
|
|
38720
|
+
const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
|
|
38721
|
+
const noFileCache = process.env["REACT_DOCTOR_NO_FILE_CACHE"]?.toLowerCase() ?? "";
|
|
38722
|
+
if (CACHE_DISABLED_VALUES$1.has(noCache)) return false;
|
|
38723
|
+
if (CACHE_DISABLED_VALUES$1.has(noFileCache)) return false;
|
|
38724
|
+
return true;
|
|
38421
38725
|
} }) {};
|
|
38422
38726
|
const DIAGNOSTIC_SURFACES = [
|
|
38423
38727
|
"cli",
|
|
@@ -38466,6 +38770,12 @@ const BOOLEAN_FIELD_NAMES = [
|
|
|
38466
38770
|
"adoptExistingLintConfig"
|
|
38467
38771
|
];
|
|
38468
38772
|
const STRING_FIELD_NAMES = ["rootDir"];
|
|
38773
|
+
const STRING_ARRAY_FIELD_NAMES = [
|
|
38774
|
+
"projects",
|
|
38775
|
+
"textComponents",
|
|
38776
|
+
"rawTextWrapperComponents",
|
|
38777
|
+
"serverAuthFunctionNames"
|
|
38778
|
+
];
|
|
38469
38779
|
const SURFACE_CONTROL_FIELD_NAMES = [
|
|
38470
38780
|
"includeTags",
|
|
38471
38781
|
"excludeTags",
|
|
@@ -38567,6 +38877,7 @@ const validateConfigTypes = (config) => {
|
|
|
38567
38877
|
const validated = { ...config };
|
|
38568
38878
|
for (const fieldName of BOOLEAN_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => coerceMaybeBooleanString(fieldName, value));
|
|
38569
38879
|
for (const fieldName of STRING_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateString(fieldName, value));
|
|
38880
|
+
for (const fieldName of STRING_ARRAY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateStringArrayField(fieldName, value));
|
|
38570
38881
|
applyFieldValidator(config, validated, "surfaces", validateSurfacesField);
|
|
38571
38882
|
for (const fieldName of SEVERITY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateSeverityMap(fieldName, value, fieldName === "categories"));
|
|
38572
38883
|
applyFieldValidator(config, validated, "plugins", (value) => validateStringArrayField("plugins", value));
|
|
@@ -38851,6 +39162,8 @@ const assignFixGroups = (diagnostics) => {
|
|
|
38851
39162
|
};
|
|
38852
39163
|
});
|
|
38853
39164
|
};
|
|
39165
|
+
const compareStrings = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
39166
|
+
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
39167
|
const getDirectDependencyNames = (packageJson) => new Set([...Object.keys(packageJson.dependencies ?? {}), ...Object.keys(packageJson.devDependencies ?? {})]);
|
|
38855
39168
|
const buildExpoCheckContext = (rootDirectory, expoVersion) => {
|
|
38856
39169
|
const packageJson = readPackageJson$1(Path.join(rootDirectory, "package.json"));
|
|
@@ -39357,10 +39670,15 @@ const buildHardeningDiagnostic = (input) => ({
|
|
|
39357
39670
|
column: input.column ?? 0,
|
|
39358
39671
|
category: "Security"
|
|
39359
39672
|
});
|
|
39360
|
-
const checkPnpmHardening = (
|
|
39361
|
-
if (!isPnpmManagedProject(
|
|
39362
|
-
const workspacePath = Path.join(
|
|
39363
|
-
const
|
|
39673
|
+
const checkPnpmHardening = (scanDirectory) => {
|
|
39674
|
+
if (!isPnpmManagedProject(scanDirectory)) return [];
|
|
39675
|
+
const workspacePath = Path.join(scanDirectory, PNPM_WORKSPACE_FILE);
|
|
39676
|
+
const hasWorkspaceFile = isFile(workspacePath);
|
|
39677
|
+
if (!hasWorkspaceFile) {
|
|
39678
|
+
const monorepoRoot = findMonorepoRoot(scanDirectory);
|
|
39679
|
+
if (monorepoRoot !== null && isFile(Path.join(monorepoRoot, PNPM_WORKSPACE_FILE))) return [];
|
|
39680
|
+
}
|
|
39681
|
+
const settings = parseHardeningSettings(hasWorkspaceFile ? NFS.readFileSync(workspacePath, "utf-8") : "");
|
|
39364
39682
|
const diagnostics = [];
|
|
39365
39683
|
if (settings.minimumReleaseAge === null) diagnostics.push(buildHardeningDiagnostic({
|
|
39366
39684
|
message: "pnpm-workspace.yaml is missing `minimumReleaseAge` — newly published versions can ship malware that gets caught and unpublished within hours",
|
|
@@ -39822,7 +40140,10 @@ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy)
|
|
|
39822
40140
|
}
|
|
39823
40141
|
return true;
|
|
39824
40142
|
};
|
|
39825
|
-
const
|
|
40143
|
+
const yieldToEventLoop = () => new Promise((resolve) => {
|
|
40144
|
+
setImmediate(resolve);
|
|
40145
|
+
});
|
|
40146
|
+
const createSecurityScanSession = (rootDirectory, options) => {
|
|
39826
40147
|
const capabilities = options.project ? buildCapabilities(options.project) : /* @__PURE__ */ new Set();
|
|
39827
40148
|
const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
|
|
39828
40149
|
const enabledScanRules = REACT_DOCTOR_RULES.flatMap((entry) => {
|
|
@@ -39837,7 +40158,7 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
|
|
|
39837
40158
|
committedFilesOnly: rule.committedFilesOnly === true
|
|
39838
40159
|
}];
|
|
39839
40160
|
});
|
|
39840
|
-
if (enabledScanRules.length === 0) return
|
|
40161
|
+
if (enabledScanRules.length === 0) return null;
|
|
39841
40162
|
const diagnostics = [];
|
|
39842
40163
|
const seen = /* @__PURE__ */ new Set();
|
|
39843
40164
|
const gitIgnoredCache = /* @__PURE__ */ new Map();
|
|
@@ -39849,15 +40170,34 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
|
|
|
39849
40170
|
}
|
|
39850
40171
|
return status === true;
|
|
39851
40172
|
};
|
|
39852
|
-
|
|
39853
|
-
|
|
39854
|
-
|
|
39855
|
-
|
|
39856
|
-
|
|
39857
|
-
|
|
39858
|
-
|
|
40173
|
+
const scanFile = (file) => {
|
|
40174
|
+
for (const { entry, scan, committedFilesOnly } of enabledScanRules) for (const finding of scan(file)) {
|
|
40175
|
+
if (committedFilesOnly && isFileGitIgnored(file)) continue;
|
|
40176
|
+
const diagnostic = buildSecurityScanDiagnostic(finding, entry, file.relativePath);
|
|
40177
|
+
const key = `${diagnostic.rule}:${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.message}`;
|
|
40178
|
+
if (seen.has(key)) continue;
|
|
40179
|
+
seen.add(key);
|
|
40180
|
+
diagnostics.push(diagnostic);
|
|
40181
|
+
}
|
|
40182
|
+
};
|
|
40183
|
+
return {
|
|
40184
|
+
scanFile,
|
|
40185
|
+
diagnostics
|
|
40186
|
+
};
|
|
40187
|
+
};
|
|
40188
|
+
const checkSecurityScanCooperative = async (rootDirectory, options = {}) => {
|
|
40189
|
+
const session = createSecurityScanSession(rootDirectory, options);
|
|
40190
|
+
if (session === null) return [];
|
|
40191
|
+
let filesSinceYield = 0;
|
|
40192
|
+
for (const file of collectSecurityScanFiles(rootDirectory)) {
|
|
40193
|
+
session.scanFile(file);
|
|
40194
|
+
filesSinceYield += 1;
|
|
40195
|
+
if (filesSinceYield >= 16) {
|
|
40196
|
+
filesSinceYield = 0;
|
|
40197
|
+
await yieldToEventLoop();
|
|
40198
|
+
}
|
|
39859
40199
|
}
|
|
39860
|
-
return diagnostics;
|
|
40200
|
+
return session.diagnostics;
|
|
39861
40201
|
};
|
|
39862
40202
|
var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
39863
40203
|
let p = process || {}, argv = p.argv || [], env = p.env || {};
|
|
@@ -40090,6 +40430,74 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
|
|
|
40090
40430
|
return [...seen].filter((pattern) => pattern.length > 0);
|
|
40091
40431
|
};
|
|
40092
40432
|
const collectDeadCodeEntryPatterns = (rootDirectory) => [...new Set(collectKnipPatterns(rootDirectory, "entry"))].filter((pattern) => pattern.length > 0);
|
|
40433
|
+
const readSystemFacts = () => ({
|
|
40434
|
+
availableCores: os.availableParallelism(),
|
|
40435
|
+
totalMemoryBytes: os.totalmem(),
|
|
40436
|
+
cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
|
|
40437
|
+
});
|
|
40438
|
+
/**
|
|
40439
|
+
* How many real deslop dead-code child processes may run at once, across the
|
|
40440
|
+
* concurrent per-project `runInspect` fibers of one CLI run. The cap is the
|
|
40441
|
+
* smaller of the core count and the number of `DEAD_CODE_WORKER_MEM_BUDGET_BYTES`
|
|
40442
|
+
* workers that fit in available memory, floored at 1.
|
|
40443
|
+
*
|
|
40444
|
+
* On a roomy dev box / CI runner this resolves high enough that every
|
|
40445
|
+
* concurrently-scanned project still spawns its own worker (no serialization vs
|
|
40446
|
+
* the prior uncapped behavior); on a memory-constrained runner it collapses
|
|
40447
|
+
* toward 1, so the `withDeadCodeWorkerSlot` semaphore serializes the spawns
|
|
40448
|
+
* instead of oversubscribing memory with N simultaneous children — the global
|
|
40449
|
+
* cap the per-project spawn path lacked.
|
|
40450
|
+
*
|
|
40451
|
+
* Mirrors `resolveAutoScanConcurrency` (lint), but budgets memory per the
|
|
40452
|
+
* heavier dead-code worker. `facts` is injectable for tests.
|
|
40453
|
+
*/
|
|
40454
|
+
const resolveDeadCodeConcurrency = (facts = readSystemFacts()) => {
|
|
40455
|
+
const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
|
|
40456
|
+
const memoryBoundedWorkers = Math.floor(availableMemoryBytes / DEAD_CODE_WORKER_MEM_BUDGET_BYTES);
|
|
40457
|
+
return Math.max(1, Math.min(facts.availableCores, memoryBoundedWorkers));
|
|
40458
|
+
};
|
|
40459
|
+
let availableSlots = -1;
|
|
40460
|
+
const waiters = [];
|
|
40461
|
+
const releaseSlot = () => {
|
|
40462
|
+
const nextWaiter = waiters.shift();
|
|
40463
|
+
if (nextWaiter !== void 0) nextWaiter();
|
|
40464
|
+
else availableSlots += 1;
|
|
40465
|
+
};
|
|
40466
|
+
/**
|
|
40467
|
+
* Runs `task` once a dead-code worker slot is free, releasing the slot when the
|
|
40468
|
+
* task settles (success or failure). With a high cap (roomy machine) every
|
|
40469
|
+
* caller proceeds immediately; with a low cap (constrained runner) callers
|
|
40470
|
+
* queue and run as slots free.
|
|
40471
|
+
*
|
|
40472
|
+
* `abortSignal` short-circuits the WAIT: if it's already aborted, or fires while
|
|
40473
|
+
* this caller is queued, the call rejects without acquiring a slot or running
|
|
40474
|
+
* `task` — so a cancelled scan (e.g. lint failed) doesn't sit in the queue and
|
|
40475
|
+
* then spawn a child only to tear it down. A queued caller that aborts removes
|
|
40476
|
+
* its own waiter so a later release never hands a slot to a dead request.
|
|
40477
|
+
*/
|
|
40478
|
+
const withDeadCodeWorkerSlot = async (task, abortSignal) => {
|
|
40479
|
+
if (abortSignal?.aborted) throw new Error("Dead-code worker aborted.");
|
|
40480
|
+
if (availableSlots < 0) availableSlots = resolveDeadCodeConcurrency();
|
|
40481
|
+
if (availableSlots > 0) availableSlots -= 1;
|
|
40482
|
+
else await new Promise((resolve, reject) => {
|
|
40483
|
+
const waiter = () => {
|
|
40484
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
40485
|
+
resolve();
|
|
40486
|
+
};
|
|
40487
|
+
const onAbort = () => {
|
|
40488
|
+
const queuedIndex = waiters.indexOf(waiter);
|
|
40489
|
+
if (queuedIndex !== -1) waiters.splice(queuedIndex, 1);
|
|
40490
|
+
reject(/* @__PURE__ */ new Error("Dead-code worker aborted."));
|
|
40491
|
+
};
|
|
40492
|
+
waiters.push(waiter);
|
|
40493
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
40494
|
+
});
|
|
40495
|
+
try {
|
|
40496
|
+
return await task();
|
|
40497
|
+
} finally {
|
|
40498
|
+
releaseSlot();
|
|
40499
|
+
}
|
|
40500
|
+
};
|
|
40093
40501
|
/**
|
|
40094
40502
|
* Resolves a path to its canonical, symlink-free form, falling back to
|
|
40095
40503
|
* the input when it cannot be realpath'd (broken symlink, permission
|
|
@@ -40161,6 +40569,22 @@ process.stdin.on("end", () => {
|
|
|
40161
40569
|
...(workerInput.ignorePatterns.length > 0
|
|
40162
40570
|
? { ignorePatterns: workerInput.ignorePatterns }
|
|
40163
40571
|
: {}),
|
|
40572
|
+
// We consume only deslop's GRAPH-based findings (unusedFiles, unusedExports,
|
|
40573
|
+
// unusedDependencies, circularDependencies). Everything else deslop can compute
|
|
40574
|
+
// is pure wasted work for us, and it's the bulk of the runtime:
|
|
40575
|
+
// - semantic: a full TS Program for unusedTypes/enum/class-members/
|
|
40576
|
+
// misclassifiedDependencies (~37-45% of the phase).
|
|
40577
|
+
// - reportCodeQuality: the duplicate-block, complexity, feature-flag,
|
|
40578
|
+
// TypeScript-smell, private-type-leak and re-export-cycle detectors. These
|
|
40579
|
+
// are the single most expensive pass — duplicate-block detection alone was
|
|
40580
|
+
// ~83s of a ~130s Sentry scan — so skipping them is an ~8.5x dead-code
|
|
40581
|
+
// speedup on a large repo.
|
|
40582
|
+
// Both are provably safe: the consumed graph findings are computed by their own
|
|
40583
|
+
// detectors, independent of these passes (confirmed byte-identical on
|
|
40584
|
+
// excalidraw + mui-material + sentry). tsConfigPath stays — the module resolver
|
|
40585
|
+
// needs it for path-alias resolution in the import graph.
|
|
40586
|
+
semantic: { enabled: false },
|
|
40587
|
+
reportCodeQuality: false,
|
|
40164
40588
|
};
|
|
40165
40589
|
const result = await analyze(defineConfig(config));
|
|
40166
40590
|
emit({ ok: true, result: normalizeResult(result) });
|
|
@@ -40290,7 +40714,11 @@ const createDeadCodeWorker = (input) => {
|
|
|
40290
40714
|
"pipe",
|
|
40291
40715
|
"pipe"
|
|
40292
40716
|
],
|
|
40293
|
-
windowsHide: true
|
|
40717
|
+
windowsHide: true,
|
|
40718
|
+
env: input.parseConcurrency === void 0 ? process.env : {
|
|
40719
|
+
...process.env,
|
|
40720
|
+
DESLOP_PARSE_CONCURRENCY: String(input.parseConcurrency)
|
|
40721
|
+
}
|
|
40294
40722
|
});
|
|
40295
40723
|
const stdoutChunks = [];
|
|
40296
40724
|
const stderrChunks = [];
|
|
@@ -40335,41 +40763,42 @@ const createDeadCodeWorker = (input) => {
|
|
|
40335
40763
|
}
|
|
40336
40764
|
};
|
|
40337
40765
|
};
|
|
40338
|
-
const runDeadCodeWorkerWithTimeout = (handle, timeoutMs) => new Promise((resolve, reject) => {
|
|
40766
|
+
const runDeadCodeWorkerWithTimeout = (handle, timeoutMs, abortSignal) => new Promise((resolve, reject) => {
|
|
40339
40767
|
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) => {
|
|
40768
|
+
const settle = (finish) => {
|
|
40348
40769
|
if (didSettle) return;
|
|
40349
40770
|
didSettle = true;
|
|
40350
40771
|
clearTimeout(timeoutHandle);
|
|
40772
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
40351
40773
|
handle.terminate?.();
|
|
40352
|
-
|
|
40353
|
-
}
|
|
40354
|
-
|
|
40355
|
-
|
|
40356
|
-
|
|
40357
|
-
|
|
40358
|
-
|
|
40359
|
-
|
|
40774
|
+
finish();
|
|
40775
|
+
};
|
|
40776
|
+
const onAbort = () => settle(() => reject(/* @__PURE__ */ new Error("Dead-code worker aborted.")));
|
|
40777
|
+
const timeoutHandle = setTimeout(() => settle(() => reject(/* @__PURE__ */ new Error(`Dead-code worker timed out after ${timeoutMs / MILLISECONDS_PER_SECOND}s.`))), timeoutMs);
|
|
40778
|
+
timeoutHandle.unref?.();
|
|
40779
|
+
if (abortSignal?.aborted) {
|
|
40780
|
+
onAbort();
|
|
40781
|
+
return;
|
|
40782
|
+
}
|
|
40783
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
40784
|
+
handle.result.then((value) => settle(() => resolve(value)), (error) => settle(() => reject(error)));
|
|
40360
40785
|
});
|
|
40361
40786
|
const checkDeadCode = async (options) => {
|
|
40362
40787
|
const rootDirectory = toCanonicalPath(options.rootDirectory);
|
|
40363
40788
|
if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
|
|
40364
40789
|
const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
|
|
40365
40790
|
const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
|
|
40366
|
-
const
|
|
40367
|
-
|
|
40368
|
-
|
|
40369
|
-
|
|
40370
|
-
|
|
40371
|
-
|
|
40372
|
-
|
|
40791
|
+
const spawnAndRun = () => {
|
|
40792
|
+
return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
|
|
40793
|
+
rootDirectory,
|
|
40794
|
+
entryPatterns,
|
|
40795
|
+
tsConfigPath: resolveTsConfigPath(rootDirectory),
|
|
40796
|
+
ignorePatterns,
|
|
40797
|
+
deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
|
|
40798
|
+
parseConcurrency: options.parseConcurrency
|
|
40799
|
+
}), options.workerTimeoutMs ?? 12e4, options.abortSignal);
|
|
40800
|
+
};
|
|
40801
|
+
const result = parseDeadCodeWorkerResult(options.createWorker === void 0 ? await withDeadCodeWorkerSlot(spawnAndRun, options.abortSignal) : await spawnAndRun());
|
|
40373
40802
|
const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
|
|
40374
40803
|
const diagnostics = [];
|
|
40375
40804
|
for (const unusedFile of result.unusedFiles) diagnostics.push({
|
|
@@ -40467,7 +40896,37 @@ const isDiagnosticOnSurface = (diagnostic, surface, config) => {
|
|
|
40467
40896
|
return true;
|
|
40468
40897
|
};
|
|
40469
40898
|
const filterDiagnosticsForSurface = (diagnostics, surface, config) => diagnostics.filter((diagnostic) => isDiagnosticOnSurface(diagnostic, surface, config));
|
|
40470
|
-
|
|
40899
|
+
/**
|
|
40900
|
+
* Budget for the dead-code phase, scaled to the work. deslop's graph build is
|
|
40901
|
+
* CPU-bound and roughly linear in file count, so a fixed 120s cap is too tight
|
|
40902
|
+
* for a large repo (where the pass legitimately runs that long) and is then
|
|
40903
|
+
* tipped over by any concurrent load — silently dropping every dead-code
|
|
40904
|
+
* finding. Scaling the budget with file count (and inversely with the core
|
|
40905
|
+
* share when overlapped) lets the pass complete, while the ceiling still
|
|
40906
|
+
* reclaims a genuinely wedged worker. Returns the in-worker SIGKILL deadline
|
|
40907
|
+
* and the Effect-side phase backstop that sits a margin above it.
|
|
40908
|
+
*/
|
|
40909
|
+
const resolveDeadCodeTimeout = (input) => {
|
|
40910
|
+
const coreShareFactor = Math.max(1, input.fullConcurrency / Math.max(1, input.deadCodeConcurrency));
|
|
40911
|
+
const workerTimeoutMs = Math.min(DEAD_CODE_TIMEOUT_CEILING_MS, Math.max(DEAD_CODE_WORKER_TIMEOUT_MS, Math.ceil(input.sourceFileCount * 30 * coreShareFactor)));
|
|
40912
|
+
return {
|
|
40913
|
+
workerTimeoutMs,
|
|
40914
|
+
phaseTimeoutMs: workerTimeoutMs + DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS
|
|
40915
|
+
};
|
|
40916
|
+
};
|
|
40917
|
+
const collectSizedSourceFiles = (rootDirectory, relativePaths) => {
|
|
40918
|
+
const entries = [];
|
|
40919
|
+
for (const relativePath of relativePaths) {
|
|
40920
|
+
const absolutePath = Path.resolve(rootDirectory, relativePath);
|
|
40921
|
+
const sizeBytes = statSourceFileSize(absolutePath);
|
|
40922
|
+
if (isLargeMinifiedFile(absolutePath, sizeBytes)) continue;
|
|
40923
|
+
entries.push({
|
|
40924
|
+
path: relativePath,
|
|
40925
|
+
sizeBytes: sizeBytes ?? 0
|
|
40926
|
+
});
|
|
40927
|
+
}
|
|
40928
|
+
return entries;
|
|
40929
|
+
};
|
|
40471
40930
|
const listSourceFilesViaGit = (rootDirectory) => {
|
|
40472
40931
|
const result = spawnSync("git", [
|
|
40473
40932
|
"ls-files",
|
|
@@ -40500,7 +40959,8 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
|
|
|
40500
40959
|
}
|
|
40501
40960
|
return filePaths;
|
|
40502
40961
|
};
|
|
40503
|
-
const
|
|
40962
|
+
const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory));
|
|
40963
|
+
const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
|
|
40504
40964
|
const resolveLintIncludePaths = (rootDirectory, userConfig, project) => {
|
|
40505
40965
|
if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
|
|
40506
40966
|
const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
|
|
@@ -40543,9 +41003,12 @@ var Config = class Config extends Service()("react-doctor/Config") {
|
|
|
40543
41003
|
var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
|
|
40544
41004
|
static layerNode = succeed$3(DeadCode, DeadCode.of({ run: (input) => unwrap(fn("DeadCode.run")(function* () {
|
|
40545
41005
|
return yield* tryPromise({
|
|
40546
|
-
try: () => checkDeadCode({
|
|
41006
|
+
try: (signal) => checkDeadCode({
|
|
40547
41007
|
rootDirectory: input.rootDirectory,
|
|
40548
|
-
userConfig: input.userConfig
|
|
41008
|
+
userConfig: input.userConfig,
|
|
41009
|
+
parseConcurrency: input.parseConcurrency,
|
|
41010
|
+
workerTimeoutMs: input.workerTimeoutMs,
|
|
41011
|
+
abortSignal: signal
|
|
40549
41012
|
}),
|
|
40550
41013
|
catch: (cause) => new ReactDoctorError({ reason: new DeadCodeAnalysisFailed({ cause }) })
|
|
40551
41014
|
}).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)));
|
|
@@ -40736,43 +41199,46 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40736
41199
|
* reason: GitInvocationFailed })` so the rest of the codebase
|
|
40737
41200
|
* sees a single failure channel.
|
|
40738
41201
|
*/
|
|
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({
|
|
41202
|
+
const runCommand = (input) => {
|
|
41203
|
+
const foldSpawnFailure = (cause) => input.command !== "git" ? succeed$2({
|
|
40763
41204
|
status: 127,
|
|
40764
41205
|
stdout: "",
|
|
40765
41206
|
stderr: String(cause)
|
|
40766
|
-
})
|
|
40767
|
-
return new ReactDoctorError({ reason: new GitInvocationFailed({
|
|
41207
|
+
}) : fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
|
|
40768
41208
|
args: [...input.args],
|
|
40769
41209
|
directory: input.directory,
|
|
40770
41210
|
cause
|
|
40771
|
-
}) });
|
|
40772
|
-
|
|
40773
|
-
|
|
40774
|
-
|
|
40775
|
-
|
|
41211
|
+
}) }));
|
|
41212
|
+
return scoped(gen(function* () {
|
|
41213
|
+
if (!isDirectory(input.directory)) return yield* foldSpawnFailure(`spawn ENOTDIR (cwd is not a directory: ${input.directory})`);
|
|
41214
|
+
const argvLengthChars = input.command.length + 1 + input.args.reduce((total, arg) => total + arg.length + 1, 0);
|
|
41215
|
+
if (argvLengthChars > 24e3) return yield* foldSpawnFailure(`spawn ENAMETOOLONG (${argvLengthChars} argv chars exceed ${SPAWN_ARGS_MAX_LENGTH_CHARS})`);
|
|
41216
|
+
const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
|
|
41217
|
+
cwd: input.directory,
|
|
41218
|
+
env: input.env,
|
|
41219
|
+
extendEnv: true
|
|
41220
|
+
}));
|
|
41221
|
+
const maxStdoutBytes = input.maxStdoutBytes;
|
|
41222
|
+
const stdoutByteCount = yield* make$13(0);
|
|
41223
|
+
const [stdout, stderr, status] = yield* all([
|
|
41224
|
+
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({
|
|
41225
|
+
args: [...input.args],
|
|
41226
|
+
directory: input.directory,
|
|
41227
|
+
cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
|
|
41228
|
+
}) })) : void_)))))),
|
|
41229
|
+
mkString(decodeText(handle.stderr)),
|
|
41230
|
+
handle.exitCode
|
|
41231
|
+
], { concurrency: 3 });
|
|
41232
|
+
return {
|
|
41233
|
+
status,
|
|
41234
|
+
stdout,
|
|
41235
|
+
stderr
|
|
41236
|
+
};
|
|
41237
|
+
})).pipe(catchTag$1("PlatformError", foldSpawnFailure), withSpan("git.exec", { attributes: {
|
|
41238
|
+
"git.command": input.command,
|
|
41239
|
+
"git.subcommand": input.args[0] ?? ""
|
|
41240
|
+
} }));
|
|
41241
|
+
};
|
|
40776
41242
|
const runGit = (directory, args) => runCommand({
|
|
40777
41243
|
command: "git",
|
|
40778
41244
|
args,
|
|
@@ -40805,7 +41271,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40805
41271
|
"rev-parse",
|
|
40806
41272
|
"--verify",
|
|
40807
41273
|
branch
|
|
40808
|
-
]).pipe(map$3((result) => result.status === 0));
|
|
41274
|
+
]).pipe(map$3((result) => result.status === 0), catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(false) : fail$4(error)));
|
|
40809
41275
|
const headSha = (directory) => runGit(directory, ["rev-parse", "HEAD"]).pipe(map$3((result) => result.status === 0 ? trimOrNull(result.stdout) : null));
|
|
40810
41276
|
const mergeBase = (input) => isSafeGitRevision(input.ref) ? runGit(input.directory, [
|
|
40811
41277
|
"merge-base",
|
|
@@ -41019,7 +41485,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
41019
41485
|
]);
|
|
41020
41486
|
if (result.status !== 0) return null;
|
|
41021
41487
|
return parseChangedLineRanges(result.stdout);
|
|
41022
|
-
}).pipe(withSpan("Git.changedLineRanges"))
|
|
41488
|
+
}).pipe(catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(null) : fail$4(error)), withSpan("Git.changedLineRanges"))
|
|
41023
41489
|
});
|
|
41024
41490
|
})).pipe(provide$2(layer$3.pipe(provide$2(mergeAll$1(layer$2, layer$1)))));
|
|
41025
41491
|
/**
|
|
@@ -41258,6 +41724,14 @@ const neutralizeDisableDirectives = async (rootDirectory, includePaths) => {
|
|
|
41258
41724
|
process.removeListener("exit", onExit);
|
|
41259
41725
|
};
|
|
41260
41726
|
};
|
|
41727
|
+
const ROOT_DIRECTORY_PLACEHOLDER = "<root>";
|
|
41728
|
+
const normalizeConfigForHash = (config) => {
|
|
41729
|
+
const clone = JSON.parse(JSON.stringify(config));
|
|
41730
|
+
if (clone?.settings?.["react-doctor"]) clone.settings["react-doctor"].rootDirectory = ROOT_DIRECTORY_PLACEHOLDER;
|
|
41731
|
+
if (Array.isArray(clone?.jsPlugins)) clone.jsPlugins = clone.jsPlugins.map((_, index) => `<plugin:${index}>`);
|
|
41732
|
+
return clone;
|
|
41733
|
+
};
|
|
41734
|
+
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
41735
|
/**
|
|
41262
41736
|
* Loads a plugin module via the local require resolver and extracts
|
|
41263
41737
|
* `(name, ruleNames)` from either `module.exports.meta + rules` or
|
|
@@ -41412,8 +41886,8 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
|
|
|
41412
41886
|
}
|
|
41413
41887
|
return enabled;
|
|
41414
41888
|
};
|
|
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);
|
|
41889
|
+
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection }) => {
|
|
41890
|
+
const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
|
|
41417
41891
|
const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
|
|
41418
41892
|
const jsPlugins = [];
|
|
41419
41893
|
if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
|
|
@@ -41422,6 +41896,8 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
41422
41896
|
for (const registryEntry of REACT_DOCTOR_RULES) {
|
|
41423
41897
|
const rule = reactDoctorPlugin.rules[registryEntry.id];
|
|
41424
41898
|
if (!rule) continue;
|
|
41899
|
+
if (ruleSelection === "cacheable" && CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
|
|
41900
|
+
if (ruleSelection === "sidecar" && !CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
|
|
41425
41901
|
if (rule.scan !== void 0) continue;
|
|
41426
41902
|
if (customRulesOnly && registryEntry.originallyExternal) continue;
|
|
41427
41903
|
if (rule.framework !== "global" && !rule.requires) continue;
|
|
@@ -41436,7 +41912,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
41436
41912
|
enabledReactDoctorRules[registryEntry.key] = severity;
|
|
41437
41913
|
}
|
|
41438
41914
|
const userPluginRules = {};
|
|
41439
|
-
for (const userPlugin of userPlugins) {
|
|
41915
|
+
if (ruleSelection !== "sidecar") for (const userPlugin of userPlugins) {
|
|
41440
41916
|
Object.assign(userPluginRules, buildUserPluginRules(userPlugin, severityControls));
|
|
41441
41917
|
jsPlugins.push(userPlugin.entry);
|
|
41442
41918
|
}
|
|
@@ -41466,6 +41942,100 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
41466
41942
|
}
|
|
41467
41943
|
};
|
|
41468
41944
|
};
|
|
41945
|
+
const atomicWriteJson = (filePath, value) => {
|
|
41946
|
+
try {
|
|
41947
|
+
NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
|
|
41948
|
+
const temporaryPath = `${filePath}.${process.pid}.tmp`;
|
|
41949
|
+
NFS.writeFileSync(temporaryPath, JSON.stringify(value));
|
|
41950
|
+
NFS.renameSync(temporaryPath, filePath);
|
|
41951
|
+
} catch {
|
|
41952
|
+
return;
|
|
41953
|
+
}
|
|
41954
|
+
};
|
|
41955
|
+
const failOpenReadJson = (filePath, fallback) => {
|
|
41956
|
+
try {
|
|
41957
|
+
return JSON.parse(NFS.readFileSync(filePath, "utf8"));
|
|
41958
|
+
} catch {
|
|
41959
|
+
return fallback;
|
|
41960
|
+
}
|
|
41961
|
+
};
|
|
41962
|
+
const validateDiagnostic = decodeUnknownSync(Diagnostic);
|
|
41963
|
+
const decodeFileDiagnostics = (raw) => {
|
|
41964
|
+
if (!Array.isArray(raw)) return null;
|
|
41965
|
+
try {
|
|
41966
|
+
for (const entry of raw) validateDiagnostic(entry);
|
|
41967
|
+
return raw;
|
|
41968
|
+
} catch {
|
|
41969
|
+
return null;
|
|
41970
|
+
}
|
|
41971
|
+
};
|
|
41972
|
+
const emptyCache = () => ({
|
|
41973
|
+
version: 1,
|
|
41974
|
+
rulesets: {}
|
|
41975
|
+
});
|
|
41976
|
+
const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
|
|
41977
|
+
const entries = /* @__PURE__ */ new Map();
|
|
41978
|
+
const persisted = failOpenReadJson(cacheFilePath, emptyCache());
|
|
41979
|
+
if (persisted.version !== 1 || !isRecord$2(persisted.rulesets)) return entries;
|
|
41980
|
+
const bucket = persisted.rulesets[rulesetHash];
|
|
41981
|
+
if (!isRecord$2(bucket) || !isRecord$2(bucket.files)) return entries;
|
|
41982
|
+
for (const [fileKey, rawDiagnostics] of Object.entries(bucket.files)) {
|
|
41983
|
+
const decoded = decodeFileDiagnostics(rawDiagnostics);
|
|
41984
|
+
if (decoded !== null) entries.set(fileKey, decoded);
|
|
41985
|
+
}
|
|
41986
|
+
return entries;
|
|
41987
|
+
};
|
|
41988
|
+
const createFileLintCache = (cacheDirectory, rulesetHash) => {
|
|
41989
|
+
const cacheFilePath = Path.join(cacheDirectory, FILE_LINT_CACHE_FILENAME);
|
|
41990
|
+
const entries = loadRulesetEntries(cacheFilePath, rulesetHash);
|
|
41991
|
+
return {
|
|
41992
|
+
lookup: (fileKey) => entries.get(fileKey) ?? null,
|
|
41993
|
+
store: (fileKey, diagnostics) => {
|
|
41994
|
+
entries.delete(fileKey);
|
|
41995
|
+
entries.set(fileKey, diagnostics);
|
|
41996
|
+
},
|
|
41997
|
+
persist: () => {
|
|
41998
|
+
const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
|
|
41999
|
+
const rulesets = onDisk.version === 1 && isRecord$2(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
|
|
42000
|
+
const existingBucket = rulesets[rulesetHash];
|
|
42001
|
+
const existingFiles = isRecord$2(existingBucket) && isRecord$2(existingBucket.files) ? existingBucket.files : {};
|
|
42002
|
+
const ourFiles = {};
|
|
42003
|
+
for (const [fileKey, diagnostics] of entries) ourFiles[fileKey] = diagnostics;
|
|
42004
|
+
const cappedEntries = Object.entries({
|
|
42005
|
+
...existingFiles,
|
|
42006
|
+
...ourFiles
|
|
42007
|
+
}).slice(-FILE_LINT_CACHE_MAX_FILE_COUNT);
|
|
42008
|
+
rulesets[rulesetHash] = {
|
|
42009
|
+
updatedAtMs: Date.now(),
|
|
42010
|
+
files: Object.fromEntries(cappedEntries)
|
|
42011
|
+
};
|
|
42012
|
+
const keptHashes = Object.entries(rulesets).sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8).map(([hash]) => hash);
|
|
42013
|
+
const prunedRulesets = {};
|
|
42014
|
+
for (const hash of keptHashes) prunedRulesets[hash] = rulesets[hash];
|
|
42015
|
+
atomicWriteJson(cacheFilePath, {
|
|
42016
|
+
version: 1,
|
|
42017
|
+
rulesets: prunedRulesets
|
|
42018
|
+
});
|
|
42019
|
+
}
|
|
42020
|
+
};
|
|
42021
|
+
};
|
|
42022
|
+
const bundledRequire$2 = createRequire(import.meta.url);
|
|
42023
|
+
const TOOLCHAIN_PACKAGE_SPECIFIERS$1 = [
|
|
42024
|
+
"oxlint/package.json",
|
|
42025
|
+
"oxlint-plugin-react-doctor/package.json",
|
|
42026
|
+
"eslint-plugin-react-hooks/package.json"
|
|
42027
|
+
];
|
|
42028
|
+
const resolveOxlintToolchainVersions = () => {
|
|
42029
|
+
const versions = [`node=${process.version}`];
|
|
42030
|
+
for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS$1) try {
|
|
42031
|
+
const packageJson = bundledRequire$2(specifier);
|
|
42032
|
+
const version = typeof packageJson.version === "string" ? packageJson.version : "unknown";
|
|
42033
|
+
versions.push(`${specifier}=${version}`);
|
|
42034
|
+
} catch {
|
|
42035
|
+
versions.push(`${specifier}=missing`);
|
|
42036
|
+
}
|
|
42037
|
+
return versions;
|
|
42038
|
+
};
|
|
41469
42039
|
const esmRequire = createRequire(import.meta.url);
|
|
41470
42040
|
const resolveOxlintBinary = () => {
|
|
41471
42041
|
const oxlintMainPath = esmRequire.resolve("oxlint");
|
|
@@ -42147,15 +42717,19 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
|
|
|
42147
42717
|
};
|
|
42148
42718
|
});
|
|
42149
42719
|
};
|
|
42150
|
-
const
|
|
42151
|
-
const
|
|
42152
|
-
for (const [name, value] of Object.entries(
|
|
42720
|
+
const buildOxlintChildEnv = (sourceEnv) => {
|
|
42721
|
+
const childEnv = {};
|
|
42722
|
+
for (const [name, value] of Object.entries(sourceEnv)) {
|
|
42153
42723
|
if (name === "NODE_OPTIONS" || name === "NODE_DEBUG") continue;
|
|
42154
42724
|
if (name.startsWith("npm_config_")) continue;
|
|
42155
|
-
|
|
42725
|
+
childEnv[name] = value;
|
|
42156
42726
|
}
|
|
42157
|
-
|
|
42158
|
-
|
|
42727
|
+
const isCompileCacheDisabled = Boolean(sourceEnv.NODE_DISABLE_COMPILE_CACHE);
|
|
42728
|
+
const isCompileCacheAlreadySet = childEnv.NODE_COMPILE_CACHE !== void 0;
|
|
42729
|
+
if (!isCompileCacheDisabled && !isCompileCacheAlreadySet) childEnv.NODE_COMPILE_CACHE = Path.join(os.tmpdir(), NODE_COMPILE_CACHE_DIR_NAME);
|
|
42730
|
+
return childEnv;
|
|
42731
|
+
};
|
|
42732
|
+
const SANITIZED_ENV = buildOxlintChildEnv(process.env);
|
|
42159
42733
|
/**
|
|
42160
42734
|
* Spawn one oxlint subprocess with hard ceilings on wall time and
|
|
42161
42735
|
* output size. Returns stdout on success; raises a tagged
|
|
@@ -42172,7 +42746,11 @@ const SANITIZED_ENV = (() => {
|
|
|
42172
42746
|
* The first three are splittable (the caller's binary-split retry
|
|
42173
42747
|
* shrinks the batch and re-spawns); the fourth isn't.
|
|
42174
42748
|
*/
|
|
42175
|
-
const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLINT_SPAWN_TIMEOUT_MS, outputMaxBytes = OXLINT_OUTPUT_MAX_BYTES) => new Promise((resolve, reject) => {
|
|
42749
|
+
const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLINT_SPAWN_TIMEOUT_MS, outputMaxBytes = OXLINT_OUTPUT_MAX_BYTES, abortSignal) => new Promise((resolve, reject) => {
|
|
42750
|
+
if (abortSignal?.aborted) {
|
|
42751
|
+
reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: "lint phase aborted" }) }));
|
|
42752
|
+
return;
|
|
42753
|
+
}
|
|
42176
42754
|
const child = spawn(nodeBinaryPath, args, {
|
|
42177
42755
|
cwd: rootDirectory,
|
|
42178
42756
|
env: SANITIZED_ENV,
|
|
@@ -42182,7 +42760,14 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
|
|
|
42182
42760
|
"pipe"
|
|
42183
42761
|
]
|
|
42184
42762
|
});
|
|
42763
|
+
const onAbort = () => {
|
|
42764
|
+
child.kill("SIGKILL");
|
|
42765
|
+
reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: "lint phase aborted" }) }));
|
|
42766
|
+
};
|
|
42767
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
42768
|
+
const clearAbortListener = () => abortSignal?.removeEventListener("abort", onAbort);
|
|
42185
42769
|
const timeoutHandle = setTimeout(() => {
|
|
42770
|
+
clearAbortListener();
|
|
42186
42771
|
child.kill("SIGKILL");
|
|
42187
42772
|
reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
|
|
42188
42773
|
kind: "timeout",
|
|
@@ -42217,10 +42802,12 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
|
|
|
42217
42802
|
});
|
|
42218
42803
|
child.on("error", (error) => {
|
|
42219
42804
|
clearTimeout(timeoutHandle);
|
|
42805
|
+
clearAbortListener();
|
|
42220
42806
|
reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: error }) }));
|
|
42221
42807
|
});
|
|
42222
42808
|
child.on("close", (_code, signal) => {
|
|
42223
42809
|
clearTimeout(timeoutHandle);
|
|
42810
|
+
clearAbortListener();
|
|
42224
42811
|
if (didKillForSize) {
|
|
42225
42812
|
reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
|
|
42226
42813
|
kind: "output-too-large",
|
|
@@ -42287,26 +42874,28 @@ const isParallelismRelatedSpawnError = (error) => {
|
|
|
42287
42874
|
* loop with a slimmer config in that case.
|
|
42288
42875
|
*/
|
|
42289
42876
|
const spawnLintBatches = async (input) => {
|
|
42290
|
-
const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes } = input;
|
|
42877
|
+
const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 8, signal } = input;
|
|
42291
42878
|
const requestedConcurrency = resolveScanConcurrency(input.concurrency ?? 1);
|
|
42292
42879
|
const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
|
|
42293
42880
|
const runBatchPass = async (concurrency) => {
|
|
42294
42881
|
const allDiagnostics = [];
|
|
42295
42882
|
const droppedFiles = [];
|
|
42296
42883
|
let firstDropReason = null;
|
|
42297
|
-
const
|
|
42884
|
+
const splitDeadlineMs = Date.now() + splitTotalBudgetMs;
|
|
42885
|
+
const spawnLintBatch = async (batch, depth) => {
|
|
42298
42886
|
const batchArgs = [...baseArgs, ...batch];
|
|
42299
42887
|
try {
|
|
42300
|
-
return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes), project, rootDirectory);
|
|
42888
|
+
return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes, signal), project, rootDirectory);
|
|
42301
42889
|
} catch (error) {
|
|
42302
42890
|
if (!isSplittableReactDoctorError(error)) throw error;
|
|
42303
|
-
|
|
42891
|
+
const splitBudgetExhausted = Date.now() >= splitDeadlineMs || depth >= splitMaxDepth;
|
|
42892
|
+
if (batch.length <= 1 || splitBudgetExhausted) {
|
|
42304
42893
|
droppedFiles.push(...batch);
|
|
42305
|
-
if (firstDropReason === null) firstDropReason = error.message;
|
|
42894
|
+
if (firstDropReason === null) firstDropReason = splitBudgetExhausted && batch.length > 1 ? `${error.message} (split budget exhausted after ${splitMaxDepth} levels / ${splitTotalBudgetMs / MILLISECONDS_PER_SECOND}s)` : error.message;
|
|
42306
42895
|
return [];
|
|
42307
42896
|
}
|
|
42308
42897
|
const splitIndex = Math.ceil(batch.length / 2);
|
|
42309
|
-
return [...await spawnLintBatch(batch.slice(0, splitIndex)), ...await spawnLintBatch(batch.slice(splitIndex))];
|
|
42898
|
+
return [...await spawnLintBatch(batch.slice(0, splitIndex), depth + 1), ...await spawnLintBatch(batch.slice(splitIndex), depth + 1)];
|
|
42310
42899
|
}
|
|
42311
42900
|
};
|
|
42312
42901
|
let startedFileCount = 0;
|
|
@@ -42323,7 +42912,7 @@ const spawnLintBatches = async (input) => {
|
|
|
42323
42912
|
try {
|
|
42324
42913
|
const batchResults = await mapWithConcurrency(fileBatches, concurrency, async (batch) => {
|
|
42325
42914
|
startedFileCount += batch.length;
|
|
42326
|
-
const batchDiagnostics = await spawnLintBatch(batch);
|
|
42915
|
+
const batchDiagnostics = await spawnLintBatch(batch, 0);
|
|
42327
42916
|
scannedFileCount += batch.length;
|
|
42328
42917
|
if (onFileProgress) {
|
|
42329
42918
|
displayedFileCount = Math.min(Math.max(displayedFileCount, scannedFileCount), totalFileCount);
|
|
@@ -42384,6 +42973,22 @@ const validateRuleRegistration = () => {
|
|
|
42384
42973
|
].filter((entry) => entry !== null).join("; ");
|
|
42385
42974
|
console.warn(`[react-doctor] rule-registration drift: ${detail}`);
|
|
42386
42975
|
};
|
|
42976
|
+
const hashFileContents = (filePath) => {
|
|
42977
|
+
try {
|
|
42978
|
+
return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
|
|
42979
|
+
} catch {
|
|
42980
|
+
return null;
|
|
42981
|
+
}
|
|
42982
|
+
};
|
|
42983
|
+
const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
|
|
42984
|
+
const resolveReactDoctorCacheDir = (projectDirectory) => {
|
|
42985
|
+
const cacheDirOverride = process.env["REACT_DOCTOR_CACHE_DIR"]?.trim();
|
|
42986
|
+
if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
|
|
42987
|
+
const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
|
|
42988
|
+
if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
|
|
42989
|
+
return Path.join(os.tmpdir(), "react-doctor-cache", projectCacheSubdir(projectDirectory));
|
|
42990
|
+
};
|
|
42991
|
+
const sortSourceFilesByCost = (entries) => [...entries].sort((left, right) => right.sizeBytes - left.sizeBytes).map((entry) => entry.path);
|
|
42387
42992
|
/**
|
|
42388
42993
|
* Atomically (re)writes the generated oxlintrc.json. Used twice in
|
|
42389
42994
|
* the runner: once for the primary scan, once for the
|
|
@@ -42442,7 +43047,7 @@ const reactHooksJsPluginDropNote = (error) => {
|
|
|
42442
43047
|
* 6. always restore disable directives + clean up the temp dir
|
|
42443
43048
|
*/
|
|
42444
43049
|
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;
|
|
43050
|
+
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
43051
|
const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
|
|
42447
43052
|
const severityControls = buildRuleSeverityControls(userConfig);
|
|
42448
43053
|
validateRuleRegistration();
|
|
@@ -42459,30 +43064,156 @@ const runOxlint = async (options) => {
|
|
|
42459
43064
|
serverAuthFunctionNames,
|
|
42460
43065
|
severityControls,
|
|
42461
43066
|
userPlugins,
|
|
42462
|
-
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
|
|
43067
|
+
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin,
|
|
43068
|
+
ruleSelection: overrides.ruleSelection
|
|
42463
43069
|
});
|
|
42464
43070
|
const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
|
|
42465
43071
|
const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
|
|
42466
43072
|
const configPath = Path.join(configDirectory, "oxlintrc.json");
|
|
42467
43073
|
try {
|
|
42468
|
-
const
|
|
42469
|
-
|
|
42470
|
-
|
|
42471
|
-
configPath,
|
|
42472
|
-
"--format",
|
|
42473
|
-
"json"
|
|
42474
|
-
];
|
|
43074
|
+
const oxlintBinary = resolveOxlintBinary();
|
|
43075
|
+
const sharedArgs = [];
|
|
43076
|
+
let tsconfigContent = null;
|
|
42475
43077
|
if (project.hasTypeScript) {
|
|
42476
43078
|
const tsconfigRelativePath = resolveTsConfigRelativePath(rootDirectory);
|
|
42477
|
-
if (tsconfigRelativePath)
|
|
43079
|
+
if (tsconfigRelativePath) {
|
|
43080
|
+
sharedArgs.push("--tsconfig", tsconfigRelativePath);
|
|
43081
|
+
try {
|
|
43082
|
+
tsconfigContent = NFS.readFileSync(Path.resolve(rootDirectory, tsconfigRelativePath), "utf8");
|
|
43083
|
+
} catch {
|
|
43084
|
+
tsconfigContent = null;
|
|
43085
|
+
}
|
|
43086
|
+
}
|
|
42478
43087
|
}
|
|
42479
43088
|
const combinedPatterns = collectIgnorePatterns(rootDirectory);
|
|
42480
43089
|
if (combinedPatterns.length > 0) {
|
|
42481
43090
|
const combinedIgnorePath = Path.join(configDirectory, "combined.ignore");
|
|
42482
43091
|
NFS.writeFileSync(combinedIgnorePath, `${combinedPatterns.join("\n")}\n`);
|
|
42483
|
-
|
|
43092
|
+
sharedArgs.push("--ignore-path", combinedIgnorePath);
|
|
42484
43093
|
}
|
|
42485
|
-
const
|
|
43094
|
+
const makeBaseArgs = (oxlintConfigPath) => [
|
|
43095
|
+
oxlintBinary,
|
|
43096
|
+
"-c",
|
|
43097
|
+
oxlintConfigPath,
|
|
43098
|
+
"--format",
|
|
43099
|
+
"json",
|
|
43100
|
+
...sharedArgs
|
|
43101
|
+
];
|
|
43102
|
+
const discoverScanFiles = () => lintBatchOrdering === "cost" ? sortSourceFilesByCost(listSourceFilesWithSize(rootDirectory)) : listSourceFiles(rootDirectory);
|
|
43103
|
+
const candidateFiles = includePaths !== void 0 ? includePaths : discoverScanFiles();
|
|
43104
|
+
const runConfigOverFiles = async (buildConfigForPass, configFileName, files, fileProgress) => {
|
|
43105
|
+
if (files.length === 0) return {
|
|
43106
|
+
diagnostics: [],
|
|
43107
|
+
didDropReactHooksJsPlugin: false,
|
|
43108
|
+
hadPartialFailure: false
|
|
43109
|
+
};
|
|
43110
|
+
let hadPartialFailure = false;
|
|
43111
|
+
const reportPartialFailure = (reason) => {
|
|
43112
|
+
hadPartialFailure = true;
|
|
43113
|
+
onPartialFailure?.(reason);
|
|
43114
|
+
};
|
|
43115
|
+
const passConfigPath = Path.join(configDirectory, configFileName);
|
|
43116
|
+
const passBaseArgs = makeBaseArgs(passConfigPath);
|
|
43117
|
+
const passFileBatches = batchIncludePaths(passBaseArgs, files);
|
|
43118
|
+
const spawnPass = () => spawnLintBatches({
|
|
43119
|
+
baseArgs: passBaseArgs,
|
|
43120
|
+
fileBatches: passFileBatches,
|
|
43121
|
+
rootDirectory,
|
|
43122
|
+
nodeBinaryPath,
|
|
43123
|
+
project,
|
|
43124
|
+
onPartialFailure: reportPartialFailure,
|
|
43125
|
+
onFileProgress: fileProgress,
|
|
43126
|
+
spawnTimeoutMs,
|
|
43127
|
+
outputMaxBytes,
|
|
43128
|
+
concurrency: options.concurrency,
|
|
43129
|
+
signal: options.signal
|
|
43130
|
+
});
|
|
43131
|
+
writeOxlintConfig(passConfigPath, buildConfigForPass({}));
|
|
43132
|
+
try {
|
|
43133
|
+
return {
|
|
43134
|
+
diagnostics: await spawnPass(),
|
|
43135
|
+
didDropReactHooksJsPlugin: false,
|
|
43136
|
+
hadPartialFailure
|
|
43137
|
+
};
|
|
43138
|
+
} catch (error) {
|
|
43139
|
+
const reactHooksJsDropNote = reactHooksJsPluginDropNote(error);
|
|
43140
|
+
if (reactHooksJsDropNote === null) throw error;
|
|
43141
|
+
writeOxlintConfig(passConfigPath, buildConfigForPass({ disableReactHooksJsPlugin: true }));
|
|
43142
|
+
const diagnostics = await spawnPass();
|
|
43143
|
+
reportPartialFailure(reactHooksJsDropNote);
|
|
43144
|
+
return {
|
|
43145
|
+
diagnostics,
|
|
43146
|
+
didDropReactHooksJsPlugin: true,
|
|
43147
|
+
hadPartialFailure
|
|
43148
|
+
};
|
|
43149
|
+
}
|
|
43150
|
+
};
|
|
43151
|
+
if (perFileLintCacheEnabled && respectInlineDisables && !project.hasReactCompiler && extendsPaths.length === 0 && userPlugins.length === 0) {
|
|
43152
|
+
const rulesetHash = computeRulesetHash({
|
|
43153
|
+
config: buildConfig({
|
|
43154
|
+
extendsPaths: [],
|
|
43155
|
+
ruleSelection: "cacheable"
|
|
43156
|
+
}),
|
|
43157
|
+
toolchainVersions: resolveOxlintToolchainVersions(),
|
|
43158
|
+
ignorePatterns: combinedPatterns,
|
|
43159
|
+
tsconfigContent
|
|
43160
|
+
});
|
|
43161
|
+
const cache = createFileLintCache(resolveReactDoctorCacheDir(rootDirectory), rulesetHash);
|
|
43162
|
+
const cacheKeyByFile = /* @__PURE__ */ new Map();
|
|
43163
|
+
const missFiles = [];
|
|
43164
|
+
const replayedDiagnostics = [];
|
|
43165
|
+
for (const candidateFile of candidateFiles) {
|
|
43166
|
+
const contentHash = hashFileContents(Path.resolve(rootDirectory, candidateFile));
|
|
43167
|
+
if (contentHash === null) {
|
|
43168
|
+
missFiles.push(candidateFile);
|
|
43169
|
+
continue;
|
|
43170
|
+
}
|
|
43171
|
+
const cacheKey = `${candidateFile.replaceAll("\\", "/")}${contentHash}`;
|
|
43172
|
+
cacheKeyByFile.set(candidateFile, cacheKey);
|
|
43173
|
+
const cachedDiagnostics = cache.lookup(cacheKey);
|
|
43174
|
+
if (cachedDiagnostics === null) missFiles.push(candidateFile);
|
|
43175
|
+
else replayedDiagnostics.push(...cachedDiagnostics);
|
|
43176
|
+
}
|
|
43177
|
+
const cacheHitFileCount = candidateFiles.length - missFiles.length;
|
|
43178
|
+
const cacheableResult = await runConfigOverFiles((overrides) => buildConfig({
|
|
43179
|
+
extendsPaths: [],
|
|
43180
|
+
ruleSelection: "cacheable",
|
|
43181
|
+
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
|
|
43182
|
+
}), "oxlintrc.cacheable.json", missFiles, void 0);
|
|
43183
|
+
const sidecarResult = await runConfigOverFiles(() => buildConfig({
|
|
43184
|
+
extendsPaths: [],
|
|
43185
|
+
ruleSelection: "sidecar"
|
|
43186
|
+
}), "oxlintrc.sidecar.json", candidateFiles, options.onFileProgress);
|
|
43187
|
+
onCacheStats?.(cacheHitFileCount, candidateFiles.length);
|
|
43188
|
+
const missFileByNormalizedPath = /* @__PURE__ */ new Map();
|
|
43189
|
+
for (const missFile of missFiles) missFileByNormalizedPath.set(missFile.replaceAll("\\", "/"), missFile);
|
|
43190
|
+
const freshDiagnosticsByFile = /* @__PURE__ */ new Map();
|
|
43191
|
+
let isAttributionSound = true;
|
|
43192
|
+
for (const diagnostic of cacheableResult.diagnostics) {
|
|
43193
|
+
const missFile = missFileByNormalizedPath.get(diagnostic.filePath);
|
|
43194
|
+
if (missFile === void 0) {
|
|
43195
|
+
isAttributionSound = false;
|
|
43196
|
+
break;
|
|
43197
|
+
}
|
|
43198
|
+
const fileDiagnostics = freshDiagnosticsByFile.get(missFile) ?? [];
|
|
43199
|
+
fileDiagnostics.push(diagnostic);
|
|
43200
|
+
freshDiagnosticsByFile.set(missFile, fileDiagnostics);
|
|
43201
|
+
}
|
|
43202
|
+
if (!cacheableResult.didDropReactHooksJsPlugin && !cacheableResult.hadPartialFailure && isAttributionSound) {
|
|
43203
|
+
for (const missFile of missFiles) {
|
|
43204
|
+
const cacheKey = cacheKeyByFile.get(missFile);
|
|
43205
|
+
if (cacheKey !== void 0) cache.store(cacheKey, freshDiagnosticsByFile.get(missFile) ?? []);
|
|
43206
|
+
}
|
|
43207
|
+
cache.persist();
|
|
43208
|
+
}
|
|
43209
|
+
return dedupeDiagnostics([
|
|
43210
|
+
...replayedDiagnostics,
|
|
43211
|
+
...cacheableResult.diagnostics,
|
|
43212
|
+
...sidecarResult.diagnostics
|
|
43213
|
+
]);
|
|
43214
|
+
}
|
|
43215
|
+
const baseArgs = makeBaseArgs(configPath);
|
|
43216
|
+
const fileBatches = batchIncludePaths(baseArgs, candidateFiles);
|
|
42486
43217
|
const runBatches = () => spawnLintBatches({
|
|
42487
43218
|
baseArgs,
|
|
42488
43219
|
fileBatches,
|
|
@@ -42493,7 +43224,8 @@ const runOxlint = async (options) => {
|
|
|
42493
43224
|
onFileProgress: options.onFileProgress,
|
|
42494
43225
|
spawnTimeoutMs,
|
|
42495
43226
|
outputMaxBytes,
|
|
42496
|
-
concurrency: options.concurrency
|
|
43227
|
+
concurrency: options.concurrency,
|
|
43228
|
+
signal: options.signal
|
|
42497
43229
|
});
|
|
42498
43230
|
writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
|
|
42499
43231
|
try {
|
|
@@ -42572,9 +43304,11 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
|
|
|
42572
43304
|
const spawnTimeoutMs = yield* OxlintSpawnTimeoutMs;
|
|
42573
43305
|
const outputMaxBytes = yield* OxlintOutputMaxBytes;
|
|
42574
43306
|
const concurrency = yield* OxlintConcurrency;
|
|
43307
|
+
const lintBatchOrdering = yield* LintBatchOrdering;
|
|
43308
|
+
const perFileLintCacheEnabled = yield* PerFileLintCacheEnabled;
|
|
42575
43309
|
const collectedFailures = [];
|
|
42576
43310
|
const diagnostics = yield* tryPromise({
|
|
42577
|
-
try: () => runOxlint({
|
|
43311
|
+
try: (signal) => runOxlint({
|
|
42578
43312
|
rootDirectory: input.rootDirectory,
|
|
42579
43313
|
project: input.project,
|
|
42580
43314
|
includePaths: input.includePaths ? [...input.includePaths] : void 0,
|
|
@@ -42589,9 +43323,13 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
|
|
|
42589
43323
|
collectedFailures.push(reason);
|
|
42590
43324
|
},
|
|
42591
43325
|
onFileProgress: input.onFileProgress,
|
|
43326
|
+
perFileLintCacheEnabled,
|
|
43327
|
+
onCacheStats: input.onCacheStats,
|
|
42592
43328
|
spawnTimeoutMs,
|
|
42593
43329
|
outputMaxBytes,
|
|
42594
|
-
concurrency
|
|
43330
|
+
concurrency,
|
|
43331
|
+
signal,
|
|
43332
|
+
lintBatchOrdering
|
|
42595
43333
|
}),
|
|
42596
43334
|
catch: ensureReactDoctorError
|
|
42597
43335
|
});
|
|
@@ -42983,14 +43721,49 @@ const parseArtifactFromBody = (body) => {
|
|
|
42983
43721
|
}
|
|
42984
43722
|
return null;
|
|
42985
43723
|
};
|
|
42986
|
-
const
|
|
43724
|
+
const isSupplyChainCacheDisabled = () => {
|
|
43725
|
+
const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
|
|
43726
|
+
return noCache === "1" || noCache === "true";
|
|
43727
|
+
};
|
|
43728
|
+
const supplyChainCacheFile = (cacheDirectory, dependency) => {
|
|
43729
|
+
const purlHash = crypto.createHash("sha256").update(toPurl(dependency)).digest("hex").slice(0, 16);
|
|
43730
|
+
return Path.join(cacheDirectory, SUPPLY_CHAIN_CACHE_SUBDIR, `${purlHash}.json`);
|
|
43731
|
+
};
|
|
43732
|
+
const readCachedSocketBody = (cacheFile) => {
|
|
43733
|
+
try {
|
|
43734
|
+
const entry = JSON.parse(NFS.readFileSync(cacheFile, "utf-8"));
|
|
43735
|
+
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;
|
|
43736
|
+
} catch {}
|
|
43737
|
+
return null;
|
|
43738
|
+
};
|
|
43739
|
+
const writeCachedSocketBody = (cacheFile, body) => {
|
|
43740
|
+
try {
|
|
43741
|
+
NFS.mkdirSync(Path.dirname(cacheFile), { recursive: true });
|
|
43742
|
+
NFS.writeFileSync(cacheFile, JSON.stringify({
|
|
43743
|
+
fetchedAtMs: Date.now(),
|
|
43744
|
+
body
|
|
43745
|
+
}));
|
|
43746
|
+
} catch {}
|
|
43747
|
+
};
|
|
43748
|
+
const fetchSocketArtifact = (dependency, cacheDirectory) => tryPromise(async (signal) => {
|
|
43749
|
+
const cacheFile = cacheDirectory === null ? null : supplyChainCacheFile(cacheDirectory, dependency);
|
|
43750
|
+
if (cacheFile !== null) {
|
|
43751
|
+
const cachedBody = readCachedSocketBody(cacheFile);
|
|
43752
|
+
if (cachedBody !== null) {
|
|
43753
|
+
const cachedArtifact = parseArtifactFromBody(cachedBody);
|
|
43754
|
+
if (cachedArtifact !== null) return cachedArtifact;
|
|
43755
|
+
}
|
|
43756
|
+
}
|
|
42987
43757
|
const requestUrl = `${SOCKET_FREE_PURL_API_BASE}/${encodeURIComponent(toPurl(dependency))}`;
|
|
42988
43758
|
const response = await fetch(requestUrl, {
|
|
42989
43759
|
headers: { "User-Agent": SOCKET_FREE_USER_AGENT },
|
|
42990
43760
|
signal
|
|
42991
43761
|
});
|
|
42992
43762
|
if (!response.ok) return null;
|
|
42993
|
-
|
|
43763
|
+
const body = await response.text();
|
|
43764
|
+
const artifact = parseArtifactFromBody(body);
|
|
43765
|
+
if (artifact !== null && cacheFile !== null) writeCachedSocketBody(cacheFile, body);
|
|
43766
|
+
return artifact;
|
|
42994
43767
|
}).pipe(timeout(FETCH_TIMEOUT_MS), orElseSucceed(() => null), tap$1((artifact) => {
|
|
42995
43768
|
const scoreAttributes = {};
|
|
42996
43769
|
if (artifact !== null) {
|
|
@@ -43095,7 +43868,8 @@ const checkSupplyChain = (input) => gen(function* () {
|
|
|
43095
43868
|
const packageJsonPath = Path.join(input.rootDirectory, "package.json");
|
|
43096
43869
|
const dependencies = collectDependenciesToScore(readPackageJson$1(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies);
|
|
43097
43870
|
if (dependencies.length === 0) return [];
|
|
43098
|
-
const
|
|
43871
|
+
const cacheDirectory = isSupplyChainCacheDisabled() ? null : resolveReactDoctorCacheDir(input.rootDirectory);
|
|
43872
|
+
const artifacts = yield* forEach$1(dependencies, (dependency) => fetchSocketArtifact(dependency, cacheDirectory), { concurrency: 8 }).pipe(timeoutOption(input.totalTimeoutMs ?? 9e4), map$3((maybeArtifacts) => getOrElse$1(maybeArtifacts, () => [])));
|
|
43099
43873
|
const diagnostics = [];
|
|
43100
43874
|
for (let index = 0; index < dependencies.length; index += 1) {
|
|
43101
43875
|
const artifact = artifacts[index];
|
|
@@ -43120,6 +43894,10 @@ const checkSupplyChain = (input) => gen(function* () {
|
|
|
43120
43894
|
* The underlying `checkSupplyChain` Effect is total/fail-open — per-package
|
|
43121
43895
|
* timeouts and network failures recover to "skip" — so the stream never
|
|
43122
43896
|
* fails, mirroring `DeadCode`'s stream shape so the two compose the same way.
|
|
43897
|
+
* The orchestrator (`run-inspect.ts`) consumes this stream on a background
|
|
43898
|
+
* fiber whose network time overlaps the lint pass, joined under a generous
|
|
43899
|
+
* wall-clock budget; a budget expiry is the same fail-open outcome as a Socket
|
|
43900
|
+
* outage.
|
|
43123
43901
|
*/
|
|
43124
43902
|
var SupplyChain = class SupplyChain extends Service()("react-doctor/SupplyChain") {
|
|
43125
43903
|
static layerNode = succeed$3(SupplyChain, SupplyChain.of({ run: (input) => unwrap(checkSupplyChain(input).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), withSpan("SupplyChain.run"))) }));
|
|
@@ -43178,18 +43956,45 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
|
|
|
43178
43956
|
*
|
|
43179
43957
|
* Phases:
|
|
43180
43958
|
*
|
|
43181
|
-
* 1. Config.resolve(directory) → Project.discover → Git metadata
|
|
43959
|
+
* 1. Config.resolve(directory) → Project.discover → Git metadata.
|
|
43960
|
+
* The GitHub viewer-permission lookup is forked onto a background
|
|
43961
|
+
* fiber here and joined late (it feeds score metadata, not
|
|
43962
|
+
* diagnostics).
|
|
43182
43963
|
* 2. beforeLint hook (e.g. CLI renders the project-detection block)
|
|
43183
43964
|
* 3. environment checks (reduced-motion + pnpm hardening +
|
|
43184
|
-
* expo/react-native
|
|
43185
|
-
*
|
|
43186
|
-
*
|
|
43187
|
-
*
|
|
43188
|
-
*
|
|
43189
|
-
* fiber
|
|
43190
|
-
*
|
|
43191
|
-
*
|
|
43192
|
-
*
|
|
43965
|
+
* expo/react-native), collected synchronously. The heavier
|
|
43966
|
+
* content-regex security scan is forked instead (like supply-chain
|
|
43967
|
+
* below) and joined before the concat, so its CPU overlaps lint
|
|
43968
|
+
* rather than blocking the event loop before it.
|
|
43969
|
+
* 4. The supply-chain check (Socket.dev) is forked onto a background
|
|
43970
|
+
* fiber so its ~100% network-bound time overlaps the ~100%
|
|
43971
|
+
* CPU/subprocess-bound lint pass below, collapsing two serial
|
|
43972
|
+
* phases into roughly `max(supplyChain, lint)`. It is capped by
|
|
43973
|
+
* `SupplyChainOverlapTimeoutMs` (measured from fork) so a hung
|
|
43974
|
+
* socket can't drag out its join; on timeout it fails open to no
|
|
43975
|
+
* diagnostics — the same outcome class as a Socket outage.
|
|
43976
|
+
* 5. Linter.run runs; DeadCode.run runs concurrently (forked child
|
|
43977
|
+
* fiber) ONLY when the memory gate has headroom to run the 8 GB
|
|
43978
|
+
* dead-code child alongside the oxlint workers — or when overlap is
|
|
43979
|
+
* forced via REACT_DOCTOR_DEAD_CODE_OVERLAP. Otherwise dead-code
|
|
43980
|
+
* runs sequentially after lint, exactly as it did pre-overlap. The
|
|
43981
|
+
* fiber is joined (or interrupted, SIGKILLing its worker, on lint
|
|
43982
|
+
* failure) before diagnostics are concatenated. The afterLint hook
|
|
43983
|
+
* fires between lint and dead-code. Progress spinner labels AND the
|
|
43984
|
+
* final diagnostic / score order stay independent of execution
|
|
43985
|
+
* order, so terminal output is identical either way; supply-chain
|
|
43986
|
+
* rides alongside without a spinner.
|
|
43987
|
+
* 6. Join the supply-chain fiber, then assemble the diagnostics in a
|
|
43988
|
+
* FIXED order (env, security-scan, supply-chain, lint, dead-code) so the output is
|
|
43989
|
+
* byte-identical regardless of which fiber settled first. The
|
|
43990
|
+
* viewer-permission fiber is joined later, during score-metadata
|
|
43991
|
+
* assembly (it feeds score metadata, not diagnostics). The per-element
|
|
43992
|
+
* `Reporter.emit` side-channel now interleaves supply-chain with lint
|
|
43993
|
+
* emits, so capture-order assertions must target the deterministic
|
|
43994
|
+
* concat below, not emit order (production `Reporter.layerNoop` makes
|
|
43995
|
+
* emit a no-op).
|
|
43996
|
+
* 7. Reporter.finalize
|
|
43997
|
+
* 8. Score.compute against the surface-filtered diagnostic set
|
|
43193
43998
|
*
|
|
43194
43999
|
* The orchestrator owns spinner lifecycle via `Progress`; callers
|
|
43195
44000
|
* choose `Progress.layerOra(...)` for CLI feedback or
|
|
@@ -43241,16 +44046,27 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
43241
44046
|
...checkPnpmHardening(scanDirectory),
|
|
43242
44047
|
...checkReactServerComponentsAdvisory(scanDirectory, project),
|
|
43243
44048
|
...checkExpoProject(scanDirectory, project),
|
|
43244
|
-
...checkReactNativeProject(scanDirectory, project)
|
|
43245
|
-
...checkSecurityScan(scanDirectory, {
|
|
43246
|
-
project,
|
|
43247
|
-
ignoredTags: input.ignoredTags
|
|
43248
|
-
})
|
|
44049
|
+
...checkReactNativeProject(scanDirectory, project)
|
|
43249
44050
|
])));
|
|
43250
|
-
const
|
|
44051
|
+
const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(promise(() => checkSecurityScanCooperative(scanDirectory, {
|
|
44052
|
+
project,
|
|
44053
|
+
ignoredTags: input.ignoredTags
|
|
44054
|
+
})).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)))))).pipe(withSpan("SecurityScan.run")));
|
|
44055
|
+
const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
|
|
44056
|
+
const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
|
|
44057
|
+
const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
|
|
43251
44058
|
rootDirectory: scanDirectory,
|
|
43252
44059
|
userConfig: resolvedConfig.config
|
|
43253
|
-
})))
|
|
44060
|
+
}))).pipe(map$3((diagnostics) => ({
|
|
44061
|
+
diagnostics,
|
|
44062
|
+
timedOut: false
|
|
44063
|
+
})), timeout(supplyChainOverlapTimeout), orElseSucceed(() => ({
|
|
44064
|
+
diagnostics: [],
|
|
44065
|
+
timedOut: true
|
|
44066
|
+
}))) : succeed$2({
|
|
44067
|
+
diagnostics: [],
|
|
44068
|
+
timedOut: false
|
|
44069
|
+
}));
|
|
43254
44070
|
const lintFailure = yield* make$13({
|
|
43255
44071
|
didFail: false,
|
|
43256
44072
|
reason: null,
|
|
@@ -43261,12 +44077,49 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
43261
44077
|
didFail: false,
|
|
43262
44078
|
reason: null
|
|
43263
44079
|
});
|
|
43264
|
-
const scanConcurrency = yield* OxlintConcurrency;
|
|
44080
|
+
const scanConcurrency = resolveScanConcurrency(yield* OxlintConcurrency);
|
|
44081
|
+
const lintPhaseTimeoutMs = yield* LintPhaseTimeoutMs;
|
|
44082
|
+
const deadCodePhaseTimeoutMs = yield* DeadCodePhaseTimeoutMs;
|
|
44083
|
+
const resolveDeadCodePhaseTimeoutMs = (scaledPhaseTimeoutMs) => deadCodePhaseTimeoutMs === 15e4 ? scaledPhaseTimeoutMs : deadCodePhaseTimeoutMs;
|
|
43265
44084
|
const workerCountSuffix = scanConcurrency > 1 ? ` ${highlighter.dim(`[~${scanConcurrency} workers]`)}` : "";
|
|
44085
|
+
const shouldRunDeadCode = input.runDeadCode && !isDiffMode && (showWarnings || deadCodeMaySurfaceWhenWarningsHidden(resolvedConfig.config));
|
|
44086
|
+
const deadCodeOverlapMode = yield* DeadCodeOverlap;
|
|
44087
|
+
const shouldOverlapDeadCode = shouldRunDeadCode && deadCodeOverlapMode === "on";
|
|
44088
|
+
const deadCodeParseConcurrency = shouldOverlapDeadCode ? Math.max(1, Math.floor(scanConcurrency * DEAD_CODE_OVERLAP_PARSE_SHARE)) : void 0;
|
|
44089
|
+
const lintConcurrency = deadCodeParseConcurrency === void 0 ? scanConcurrency : Math.max(1, scanConcurrency - deadCodeParseConcurrency);
|
|
44090
|
+
const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
|
|
44091
|
+
rootDirectory: scanDirectory,
|
|
44092
|
+
userConfig: resolvedConfig.config,
|
|
44093
|
+
parseConcurrency: deadCodeParseConcurrency,
|
|
44094
|
+
workerTimeoutMs: deadCodeTimeout.workerTimeoutMs
|
|
44095
|
+
}).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
|
|
44096
|
+
yield* set(deadCodeFailure, {
|
|
44097
|
+
didFail: true,
|
|
44098
|
+
reason: error.message
|
|
44099
|
+
});
|
|
44100
|
+
return empty$4;
|
|
44101
|
+
})))))).pipe(timeoutOption(deadCodeTimeout.phaseTimeoutMs), flatMap$2(match$3({
|
|
44102
|
+
onNone: () => set(deadCodeFailure, {
|
|
44103
|
+
didFail: true,
|
|
44104
|
+
reason: `Dead-code analysis exceeded ${Math.round(deadCodeTimeout.phaseTimeoutMs / MILLISECONDS_PER_SECOND)}s and was skipped.`
|
|
44105
|
+
}).pipe(as([])),
|
|
44106
|
+
onSome: succeed$2
|
|
44107
|
+
})));
|
|
44108
|
+
const overlapDeadCodeTimeout = resolveDeadCodeTimeout({
|
|
44109
|
+
sourceFileCount: project.sourceFileCount,
|
|
44110
|
+
deadCodeConcurrency: deadCodeParseConcurrency ?? scanConcurrency,
|
|
44111
|
+
fullConcurrency: scanConcurrency
|
|
44112
|
+
});
|
|
44113
|
+
const deadCodeFiber = shouldOverlapDeadCode ? yield* forkChild(buildCollectDeadCode({
|
|
44114
|
+
workerTimeoutMs: overlapDeadCodeTimeout.workerTimeoutMs,
|
|
44115
|
+
phaseTimeoutMs: resolveDeadCodePhaseTimeoutMs(overlapDeadCodeTimeout.phaseTimeoutMs)
|
|
44116
|
+
})) : null;
|
|
43266
44117
|
const scanProgress = yield* progressService.start("Scanning...");
|
|
43267
44118
|
const scanStartTime = Date.now();
|
|
43268
44119
|
let lastReportedTotalFileCount = 0;
|
|
43269
|
-
|
|
44120
|
+
let lintCacheHitFileCount = null;
|
|
44121
|
+
let lintCacheTotalFileCount = null;
|
|
44122
|
+
const baseLintStream = linterService.run({
|
|
43270
44123
|
rootDirectory: scanDirectory,
|
|
43271
44124
|
project,
|
|
43272
44125
|
includePaths: lintIncludePaths ?? void 0,
|
|
@@ -43280,6 +44133,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
43280
44133
|
onFileProgress: (scannedFileCount, totalFileCount) => {
|
|
43281
44134
|
lastReportedTotalFileCount = totalFileCount;
|
|
43282
44135
|
runSync(scanProgress.update(`Scanning files (${scannedFileCount}/${totalFileCount})${workerCountSuffix}...`));
|
|
44136
|
+
},
|
|
44137
|
+
onCacheStats: (cacheHitFileCount, totalConsideredFileCount) => {
|
|
44138
|
+
lintCacheHitFileCount = cacheHitFileCount;
|
|
44139
|
+
lintCacheTotalFileCount = totalConsideredFileCount;
|
|
43283
44140
|
}
|
|
43284
44141
|
}).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
|
|
43285
44142
|
yield* set(lintFailure, {
|
|
@@ -43289,36 +44146,56 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
43289
44146
|
reasonKind: error.reason._tag === "OxlintUnavailable" ? error.reason.kind : null
|
|
43290
44147
|
});
|
|
43291
44148
|
return empty$4;
|
|
43292
|
-
}))))
|
|
44149
|
+
}))));
|
|
44150
|
+
const lintCollected = yield* runCollect(applyPerElementPipeline(shouldOverlapDeadCode ? baseLintStream.pipe(provideService(OxlintConcurrency, lintConcurrency)) : baseLintStream)).pipe(timeoutOption(lintPhaseTimeoutMs), flatMap$2(match$3({
|
|
44151
|
+
onNone: () => set(lintFailure, {
|
|
44152
|
+
didFail: true,
|
|
44153
|
+
reason: `Lint analysis exceeded ${lintPhaseTimeoutMs / MILLISECONDS_PER_SECOND}s and was skipped.`,
|
|
44154
|
+
reasonTag: "OxlintBatchExceeded",
|
|
44155
|
+
reasonKind: null
|
|
44156
|
+
}).pipe(as([])),
|
|
44157
|
+
onSome: succeed$2
|
|
44158
|
+
})));
|
|
43293
44159
|
const lintFailureState = yield* get$2(lintFailure);
|
|
43294
44160
|
yield* afterLint(lintFailureState.didFail);
|
|
43295
44161
|
if (lintFailureState.didFail) yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
|
|
43296
44162
|
const totalFileCount = lastReportedTotalFileCount || (lintIncludePaths?.length ?? project.sourceFileCount);
|
|
43297
44163
|
const scannedFilesLabel = `${totalFileCount} ${totalFileCount === 1 ? "file" : "files"}`;
|
|
43298
|
-
|
|
43299
|
-
|
|
43300
|
-
|
|
43301
|
-
|
|
43302
|
-
|
|
43303
|
-
|
|
43304
|
-
|
|
43305
|
-
|
|
44164
|
+
let deadCodeCollected = [];
|
|
44165
|
+
if (lintFailureState.didFail) {
|
|
44166
|
+
if (deadCodeFiber !== null) yield* interrupt(deadCodeFiber);
|
|
44167
|
+
} else if (shouldRunDeadCode) {
|
|
44168
|
+
yield* scanProgress.update(`Scanned ${scannedFilesLabel}, analyzing dead code...`);
|
|
44169
|
+
const sequentialDeadCodeTimeout = resolveDeadCodeTimeout({
|
|
44170
|
+
sourceFileCount: totalFileCount,
|
|
44171
|
+
deadCodeConcurrency: scanConcurrency,
|
|
44172
|
+
fullConcurrency: scanConcurrency
|
|
43306
44173
|
});
|
|
43307
|
-
|
|
43308
|
-
|
|
43309
|
-
|
|
44174
|
+
deadCodeCollected = deadCodeFiber !== null ? yield* join(deadCodeFiber) : yield* buildCollectDeadCode({
|
|
44175
|
+
workerTimeoutMs: sequentialDeadCodeTimeout.workerTimeoutMs,
|
|
44176
|
+
phaseTimeoutMs: resolveDeadCodePhaseTimeoutMs(sequentialDeadCodeTimeout.phaseTimeoutMs)
|
|
44177
|
+
});
|
|
44178
|
+
}
|
|
44179
|
+
const deadCodeFailureState = lintFailureState.didFail ? {
|
|
44180
|
+
didFail: false,
|
|
44181
|
+
reason: null
|
|
44182
|
+
} : yield* get$2(deadCodeFailure);
|
|
43310
44183
|
const scanElapsedMilliseconds = Date.now() - scanStartTime;
|
|
43311
44184
|
const scanElapsedSeconds = (scanElapsedMilliseconds / MILLISECONDS_PER_SECOND).toFixed(1);
|
|
43312
44185
|
if (!lintFailureState.didFail) if (deadCodeFailureState.didFail) yield* scanProgress.fail(DEAD_CODE_FAIL_TEXT);
|
|
43313
44186
|
else if (input.suppressScanSummary) yield* scanProgress.stop();
|
|
43314
44187
|
else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
|
|
44188
|
+
const supplyChainResult = yield* join(supplyChainFiber);
|
|
44189
|
+
const supplyChainCollected = supplyChainResult.diagnostics;
|
|
44190
|
+
const securityScanCollected = yield* join(securityScanFiber);
|
|
43315
44191
|
yield* reporterService.finalize;
|
|
43316
|
-
const finalDiagnostics = assignFixGroups([
|
|
44192
|
+
const finalDiagnostics = sortDiagnosticsStable(assignFixGroups([
|
|
43317
44193
|
...envCollected,
|
|
44194
|
+
...securityScanCollected,
|
|
43318
44195
|
...supplyChainCollected,
|
|
43319
44196
|
...lintCollected,
|
|
43320
44197
|
...deadCodeCollected
|
|
43321
|
-
]);
|
|
44198
|
+
]));
|
|
43322
44199
|
const githubViewerPermission = yield* join(githubViewerPermissionFiber);
|
|
43323
44200
|
const scoreMetadata = {
|
|
43324
44201
|
...repo !== null ? { repo } : {},
|
|
@@ -43354,9 +44231,14 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
43354
44231
|
lintPartialFailures,
|
|
43355
44232
|
didDeadCodeFail: deadCodeFailureState.didFail,
|
|
43356
44233
|
deadCodeFailureReason: deadCodeFailureState.reason,
|
|
44234
|
+
deadCodeOverlapped: shouldOverlapDeadCode,
|
|
43357
44235
|
scannedFileCount: totalFileCount,
|
|
43358
44236
|
scannedFilePaths,
|
|
43359
|
-
scanElapsedMilliseconds
|
|
44237
|
+
scanElapsedMilliseconds,
|
|
44238
|
+
scanConcurrency,
|
|
44239
|
+
supplyChainOverlapTimedOut: supplyChainResult.timedOut,
|
|
44240
|
+
lintCacheHitFileCount,
|
|
44241
|
+
lintCacheTotalFileCount
|
|
43360
44242
|
};
|
|
43361
44243
|
}).pipe(withSpan("runInspect", { attributes: {
|
|
43362
44244
|
"inspect.directory": input.directory,
|
|
@@ -43364,7 +44246,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
43364
44246
|
"inspect.runDeadCode": input.runDeadCode,
|
|
43365
44247
|
"inspect.isCi": input.isCi,
|
|
43366
44248
|
"inspect.scoreSurface": input.scoreSurface ?? "score"
|
|
43367
|
-
} }));
|
|
44249
|
+
} }), (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
44250
|
const parseNodeVersion = (versionString) => {
|
|
43369
44251
|
const [major = 0, minor = 0, patch = 0] = versionString.replace(/^v/, "").trim().split(".").map(Number);
|
|
43370
44252
|
return {
|
|
@@ -44070,6 +44952,7 @@ const NANOSECONDS_PER_SECOND = 1000000000n;
|
|
|
44070
44952
|
const METRIC = {
|
|
44071
44953
|
cliInvoked: "cli.invoked",
|
|
44072
44954
|
cliError: "cli.error",
|
|
44955
|
+
cliEnvironmentError: "cli.env_error",
|
|
44073
44956
|
projectDetected: "project.detected",
|
|
44074
44957
|
projectPathSelected: "project.path_selected",
|
|
44075
44958
|
projectConfigSelected: "project.config_selected",
|
|
@@ -44142,7 +45025,7 @@ const makeNoopConsole = () => ({
|
|
|
44142
45025
|
});
|
|
44143
45026
|
//#endregion
|
|
44144
45027
|
//#region src/cli/utils/version.ts
|
|
44145
|
-
const VERSION = "0.5.
|
|
45028
|
+
const VERSION = "0.5.8-dev.0c19858";
|
|
44146
45029
|
//#endregion
|
|
44147
45030
|
//#region src/cli/utils/json-mode.ts
|
|
44148
45031
|
let context = null;
|
|
@@ -44295,7 +45178,8 @@ const buildRunContext = () => {
|
|
|
44295
45178
|
terminalKind: detectTerminalKind(),
|
|
44296
45179
|
jsonMode: isJsonModeActive(),
|
|
44297
45180
|
debug: isDebugFlagEnabled(),
|
|
44298
|
-
invokedVia: detectInvokedVia()
|
|
45181
|
+
invokedVia: detectInvokedVia(),
|
|
45182
|
+
lintBatchOrdering: resolveLintBatchOrdering()
|
|
44299
45183
|
};
|
|
44300
45184
|
};
|
|
44301
45185
|
//#endregion
|
|
@@ -44368,7 +45252,8 @@ const buildSentryScope = (runContext = buildRunContext()) => {
|
|
|
44368
45252
|
jsonMode: runContext.jsonMode,
|
|
44369
45253
|
debug: runContext.debug,
|
|
44370
45254
|
invokedVia: runContext.invokedVia,
|
|
44371
|
-
nodeMajor: runContext.nodeMajor
|
|
45255
|
+
nodeMajor: runContext.nodeMajor,
|
|
45256
|
+
lintBatchOrdering: runContext.lintBatchOrdering
|
|
44372
45257
|
};
|
|
44373
45258
|
const contexts = { run: { ...runContext } };
|
|
44374
45259
|
const projectInfo = getSentryProjectInfo();
|
|
@@ -44504,13 +45389,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
|
|
|
44504
45389
|
* uploads source-map artifacts under, so stack frames symbolicate. Honors the
|
|
44505
45390
|
* standard `SENTRY_RELEASE` override.
|
|
44506
45391
|
*/
|
|
44507
|
-
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.
|
|
45392
|
+
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.8-dev.0c19858`;
|
|
44508
45393
|
/**
|
|
44509
45394
|
* Deployment environment shown in Sentry's environment filter. Defaults to
|
|
44510
45395
|
* `production` for tagged releases and `development` for dev/unbuilt versions,
|
|
44511
45396
|
* overridable via the standard `SENTRY_ENVIRONMENT` env var.
|
|
44512
45397
|
*/
|
|
44513
|
-
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.
|
|
45398
|
+
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.8-dev.0c19858") ? "development" : "production");
|
|
44514
45399
|
/**
|
|
44515
45400
|
* Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
|
|
44516
45401
|
* (set to `0` to disable tracing) and falls back to
|
|
@@ -44713,7 +45598,7 @@ const externalSpanFrom = (sentrySpan) => {
|
|
|
44713
45598
|
* in-memory tracer — identical to the prior default behavior.
|
|
44714
45599
|
*/
|
|
44715
45600
|
const applyObservability = (program, rootSentrySpan) => {
|
|
44716
|
-
if (isOtlpExportConfigured()) return (rootSentrySpan ? program.pipe(provideService(ParentSpan, externalSpanFrom(rootSentrySpan))) : program).pipe(provide(layerOtlp));
|
|
45601
|
+
if (isOtlpExportConfigured()) return (rootSentrySpan ? program.pipe(provideService$2(ParentSpan, externalSpanFrom(rootSentrySpan))) : program).pipe(provide(layerOtlp));
|
|
44717
45602
|
if (rootSentrySpan) return program.pipe(withTracer(makeSentryTracer(rootSentrySpan)));
|
|
44718
45603
|
return program.pipe(provide(layerOtlp));
|
|
44719
45604
|
};
|
|
@@ -48106,6 +48991,25 @@ const shouldBlockCi = (diagnostics, blockingLevel) => {
|
|
|
48106
48991
|
*/
|
|
48107
48992
|
const toCategoryKey = (category) => category.toLowerCase().replace(/[^a-z0-9]+/g, "_");
|
|
48108
48993
|
//#endregion
|
|
48994
|
+
//#region src/cli/utils/with-namespace.ts
|
|
48995
|
+
/**
|
|
48996
|
+
* Prefixes every key in a flat attribute group with `namespace.`, so a logical
|
|
48997
|
+
* group (scan config, diagnostics rollup, score, …) carries one dotted
|
|
48998
|
+
* namespace applied in a single place rather than hand-spelled into each key —
|
|
48999
|
+
* which is how the wide event drifted into a flat, half-namespaced soup. The
|
|
49000
|
+
* dotted prefix is what makes the attributes tree up in Sentry's attribute
|
|
49001
|
+
* browser and stay group-/filter-/aggregate-able in the Spans dataset.
|
|
49002
|
+
*
|
|
49003
|
+
* Value types are preserved verbatim (numbers stay numbers so Sentry infers a
|
|
49004
|
+
* numeric attribute and `p75(...)`/`avg(...)` work; `null` is kept so
|
|
49005
|
+
* `toSpanAttributes` can drop absent signals rather than coerce them).
|
|
49006
|
+
*/
|
|
49007
|
+
const withNamespace = (namespace, attributes) => {
|
|
49008
|
+
const namespaced = {};
|
|
49009
|
+
for (const [key, value] of Object.entries(attributes)) namespaced[`${namespace}.${key}`] = value;
|
|
49010
|
+
return namespaced;
|
|
49011
|
+
};
|
|
49012
|
+
//#endregion
|
|
48109
49013
|
//#region src/cli/utils/build-run-event.ts
|
|
48110
49014
|
const readEnvBoolean = (name) => {
|
|
48111
49015
|
const value = process.env[name];
|
|
@@ -48127,12 +49031,12 @@ const buildOutcomeAttributes = (input) => {
|
|
|
48127
49031
|
if (input.result === void 0) {
|
|
48128
49032
|
const error = input.error;
|
|
48129
49033
|
const known = isReactDoctorError(error);
|
|
48130
|
-
return {
|
|
48131
|
-
|
|
49034
|
+
return withNamespace("outcome", {
|
|
49035
|
+
status: "error",
|
|
48132
49036
|
exitCode: 1,
|
|
48133
49037
|
knownError: known,
|
|
48134
49038
|
errorTag: known ? error.reason._tag : error instanceof Error ? error.name : null
|
|
48135
|
-
};
|
|
49039
|
+
});
|
|
48136
49040
|
}
|
|
48137
49041
|
const result = input.result;
|
|
48138
49042
|
const summary = summarizeDiagnostics(result.diagnostics);
|
|
@@ -48166,60 +49070,82 @@ const buildOutcomeAttributes = (input) => {
|
|
|
48166
49070
|
}
|
|
48167
49071
|
let fixGroupedFindings = 0;
|
|
48168
49072
|
for (const count of findingsPerFixGroup.values()) fixGroupedFindings += count;
|
|
49073
|
+
const categoryRollup = {};
|
|
49074
|
+
for (const [category, count] of countByCategory) categoryRollup[`category.${toCategoryKey(category)}`] = count;
|
|
48169
49075
|
const attributes = {
|
|
48170
|
-
outcome,
|
|
48171
|
-
|
|
48172
|
-
|
|
48173
|
-
|
|
48174
|
-
|
|
48175
|
-
|
|
48176
|
-
|
|
48177
|
-
|
|
48178
|
-
|
|
48179
|
-
|
|
48180
|
-
|
|
48181
|
-
|
|
48182
|
-
|
|
48183
|
-
|
|
48184
|
-
|
|
48185
|
-
|
|
48186
|
-
|
|
48187
|
-
|
|
48188
|
-
|
|
48189
|
-
|
|
48190
|
-
|
|
48191
|
-
score
|
|
48192
|
-
|
|
48193
|
-
|
|
48194
|
-
|
|
48195
|
-
|
|
48196
|
-
|
|
48197
|
-
|
|
48198
|
-
|
|
48199
|
-
|
|
48200
|
-
|
|
48201
|
-
|
|
48202
|
-
|
|
48203
|
-
|
|
48204
|
-
|
|
48205
|
-
|
|
48206
|
-
|
|
49076
|
+
...withNamespace("outcome", {
|
|
49077
|
+
status: outcome,
|
|
49078
|
+
exitCode: wouldBlock ? 1 : 0,
|
|
49079
|
+
wouldBlock,
|
|
49080
|
+
blocking: blockingLevel,
|
|
49081
|
+
clean: isClean,
|
|
49082
|
+
skippedChecks: result.skippedChecks.length
|
|
49083
|
+
}),
|
|
49084
|
+
...withNamespace("diag", {
|
|
49085
|
+
total: summary.totalDiagnosticCount,
|
|
49086
|
+
errors: summary.errorCount,
|
|
49087
|
+
warnings: summary.warningCount,
|
|
49088
|
+
affectedFiles: summary.affectedFileCount,
|
|
49089
|
+
inTestFiles: diagnosticsInTestFiles,
|
|
49090
|
+
inStoryFiles: diagnosticsInStoryFiles,
|
|
49091
|
+
distinctRules: countByRule.size,
|
|
49092
|
+
topRule,
|
|
49093
|
+
fixGroups: findingsPerFixGroup.size,
|
|
49094
|
+
fixGroupedFindings,
|
|
49095
|
+
...categoryRollup
|
|
49096
|
+
}),
|
|
49097
|
+
...withNamespace("score", {
|
|
49098
|
+
value: result.score ? result.score.score : null,
|
|
49099
|
+
label: result.score ? result.score.label : null,
|
|
49100
|
+
available: result.score !== null
|
|
49101
|
+
}),
|
|
49102
|
+
...withNamespace("lint", {
|
|
49103
|
+
failed: input.didLintFail ?? null,
|
|
49104
|
+
failureReasonKind: input.lintFailureReasonKind ?? null,
|
|
49105
|
+
partialFailureCount: input.lintPartialFailureCount ?? null,
|
|
49106
|
+
droppedFileCount: input.lintDroppedFileCount ?? null,
|
|
49107
|
+
cacheHitFiles: result.lintCacheHitFileCount ?? null,
|
|
49108
|
+
cacheTotalFiles: result.lintCacheTotalFileCount ?? null,
|
|
49109
|
+
cacheHitRatio: result.lintCacheTotalFileCount != null && result.lintCacheTotalFileCount > 0 ? (result.lintCacheHitFileCount ?? 0) / result.lintCacheTotalFileCount : null
|
|
49110
|
+
}),
|
|
49111
|
+
...withNamespace("deadCode", {
|
|
49112
|
+
failed: input.didDeadCodeFail ?? null,
|
|
49113
|
+
overlapped: input.deadCodeOverlapped ?? null
|
|
49114
|
+
}),
|
|
49115
|
+
...withNamespace("supplyChain", { overlapTimedOut: input.supplyChainOverlapTimedOut ?? null }),
|
|
49116
|
+
...withNamespace("timing", {
|
|
49117
|
+
elapsedMs: result.elapsedMilliseconds,
|
|
49118
|
+
scanMs: result.scanElapsedMilliseconds ?? null
|
|
49119
|
+
}),
|
|
49120
|
+
...withNamespace("migration", {
|
|
49121
|
+
largestRuleBucketFiles: largestRuleBucket ? largestRuleBucket.fileCount : null,
|
|
49122
|
+
largestRuleBucketSites: largestRuleBucket ? largestRuleBucket.siteCount : null,
|
|
49123
|
+
largestRuleBucketRule: largestRuleBucket ? largestRuleBucket.ruleKey : null
|
|
49124
|
+
})
|
|
49125
|
+
};
|
|
49126
|
+
if (result.baselineDelta) Object.assign(attributes, withNamespace("baseline", {
|
|
49127
|
+
new: summary.totalDiagnosticCount,
|
|
49128
|
+
fixed: result.baselineDelta.fixedCount,
|
|
49129
|
+
baseTotal: result.baselineDelta.baseTotalCount,
|
|
49130
|
+
degraded: false
|
|
49131
|
+
}));
|
|
49132
|
+
else if (input.gateExempt) attributes["baseline.degraded"] = true;
|
|
48207
49133
|
return attributes;
|
|
48208
49134
|
};
|
|
48209
|
-
const
|
|
49135
|
+
const buildActionAttributes = () => {
|
|
48210
49136
|
const { githubActorAssociation } = resolveGithubActionsScoreMetadata();
|
|
48211
|
-
return {
|
|
49137
|
+
return withNamespace("action", {
|
|
48212
49138
|
actorAssociation: githubActorAssociation ?? null,
|
|
48213
49139
|
runnerOs: detectRunnerOs(),
|
|
48214
49140
|
comment: readEnvBoolean(ACTION_INPUT_ENVIRONMENT_VARIABLES.comment),
|
|
48215
49141
|
reviewComments: readEnvBoolean(ACTION_INPUT_ENVIRONMENT_VARIABLES.reviewComments),
|
|
48216
49142
|
versionPin: resolveVersionPin(process.env[ACTION_INPUT_ENVIRONMENT_VARIABLES.version])
|
|
48217
|
-
};
|
|
49143
|
+
});
|
|
48218
49144
|
};
|
|
48219
|
-
const
|
|
49145
|
+
const buildScanAttributes = (input) => {
|
|
48220
49146
|
const ruleOverrides = input.userConfig?.rules ?? {};
|
|
48221
49147
|
const ruleKeys = Object.keys(ruleOverrides);
|
|
48222
|
-
return {
|
|
49148
|
+
return withNamespace("scan", {
|
|
48223
49149
|
mode: input.mode,
|
|
48224
49150
|
scope: input.scope,
|
|
48225
49151
|
parallel: input.parallel,
|
|
@@ -48234,12 +49160,18 @@ const buildConfigAttributes = (input) => {
|
|
|
48234
49160
|
ignoredTagCount: input.ignoredTagCount,
|
|
48235
49161
|
hasCustomConfig: input.hasCustomConfig,
|
|
48236
49162
|
rulesConfigured: ruleKeys.length,
|
|
48237
|
-
rulesDisabled: ruleKeys.filter((key) => ruleOverrides[key] === "off").length
|
|
48238
|
-
|
|
49163
|
+
rulesDisabled: ruleKeys.filter((key) => ruleOverrides[key] === "off").length,
|
|
49164
|
+
fileCount: input.result?.scannedFileCount ?? null
|
|
49165
|
+
});
|
|
48239
49166
|
};
|
|
48240
49167
|
/**
|
|
48241
|
-
* Projects a scan into the
|
|
48242
|
-
* per-scan "wide event".
|
|
49168
|
+
* Projects a scan into the namespaced attribute set for its root span — the
|
|
49169
|
+
* canonical per-scan "wide event". Every attribute carries a dotted namespace
|
|
49170
|
+
* that groups it by concept (`scan.*` config, `action.*` CI knobs, `outcome.*`
|
|
49171
|
+
* verdict, `diag.*` findings, `score.*`, `lint.*`, `deadCode.*`,
|
|
49172
|
+
* `supplyChain.*`, `timing.*`, `migration.*`, `baseline.*`) so the attributes
|
|
49173
|
+
* tree up in Sentry's attribute browser and stay filter-/group-/aggregate-able
|
|
49174
|
+
* in the Spans dataset. Pure and exported so the projection (outcome
|
|
48243
49175
|
* precedence, rule/category rollups, CI knobs, config shape) is unit-testable
|
|
48244
49176
|
* without a live Sentry client. `null` values are dropped so absent signals
|
|
48245
49177
|
* never become misleading `"null"` attributes. The run + project base context
|
|
@@ -48248,8 +49180,8 @@ const buildConfigAttributes = (input) => {
|
|
|
48248
49180
|
* those don't carry.
|
|
48249
49181
|
*/
|
|
48250
49182
|
const buildRunEventAttributes = (input) => toSpanAttributes({
|
|
48251
|
-
...
|
|
48252
|
-
...
|
|
49183
|
+
...buildScanAttributes(input),
|
|
49184
|
+
...buildActionAttributes(),
|
|
48253
49185
|
...buildOutcomeAttributes(input)
|
|
48254
49186
|
});
|
|
48255
49187
|
/**
|
|
@@ -48264,6 +49196,32 @@ const recordRunEvent = (rootSpan, input) => {
|
|
|
48264
49196
|
} catch {}
|
|
48265
49197
|
};
|
|
48266
49198
|
//#endregion
|
|
49199
|
+
//#region src/cli/utils/resolve-worker-telemetry.ts
|
|
49200
|
+
/**
|
|
49201
|
+
* Projects the resolved lint worker count into the `(workerCount, parallel)`
|
|
49202
|
+
* telemetry pair. `resolvedWorkerCount` is the count the scan actually fanned
|
|
49203
|
+
* out to (`InspectOutput.scanConcurrency`); `pinnedConcurrency` is the caller's
|
|
49204
|
+
* `inspect({ concurrency })` pin, used as the fallback when no scan resolved a
|
|
49205
|
+
* count (the pre-scan failure path, or a cache entry persisted before the
|
|
49206
|
+
* resolved count was tracked). `parallel` is derived from the count — NOT from
|
|
49207
|
+
* whether a count was pinned — so the common auto path (no pin) still reports
|
|
49208
|
+
* parallelism correctly instead of always reading `false`.
|
|
49209
|
+
*/
|
|
49210
|
+
const resolveWorkerTelemetry = (resolvedWorkerCount, pinnedConcurrency) => {
|
|
49211
|
+
const workerCount = resolvedWorkerCount ?? pinnedConcurrency;
|
|
49212
|
+
return {
|
|
49213
|
+
workerCount,
|
|
49214
|
+
parallel: workerCount !== void 0 && workerCount > 1
|
|
49215
|
+
};
|
|
49216
|
+
};
|
|
49217
|
+
//#endregion
|
|
49218
|
+
//#region src/cli/utils/count-dropped-lint-files.ts
|
|
49219
|
+
const DROPPED_FILES_MESSAGE_PATTERN = /^(\d+) file\(s\) failed to lint and were skipped/;
|
|
49220
|
+
const countDroppedLintFiles = (lintPartialFailures) => lintPartialFailures.reduce((total, message) => {
|
|
49221
|
+
const match = DROPPED_FILES_MESSAGE_PATTERN.exec(message);
|
|
49222
|
+
return match ? total + Number(match[1]) : total;
|
|
49223
|
+
}, 0);
|
|
49224
|
+
//#endregion
|
|
48267
49225
|
//#region src/cli/utils/path-format.ts
|
|
48268
49226
|
const toForwardSlashes = (filePath) => filePath.replaceAll("\\", "/");
|
|
48269
49227
|
//#endregion
|
|
@@ -48880,25 +49838,192 @@ const canAnimateOnboarding = (stream = process.stdout) => {
|
|
|
48880
49838
|
return !isGitHookEnvironment() && !isCiEnvironment();
|
|
48881
49839
|
};
|
|
48882
49840
|
//#endregion
|
|
48883
|
-
//#region src/cli/utils/
|
|
48884
|
-
const
|
|
48885
|
-
const
|
|
49841
|
+
//#region src/cli/utils/now-iso.ts
|
|
49842
|
+
const nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
49843
|
+
const ONBOARDING_EVENT = "onboarding";
|
|
49844
|
+
const CI_PITCH_EVENT = "ci-pitch";
|
|
49845
|
+
const ACTION_UPGRADE_EVENT = "action-upgrade-v2";
|
|
49846
|
+
const SETUP_HINT_EVENT = "setup-hint";
|
|
49847
|
+
const HANDOFF_TARGET_PREFERENCE_ID = "handoff-target";
|
|
49848
|
+
const INSTALL_AGENTS_PREFERENCE_ID = "install-agents";
|
|
49849
|
+
const foldLegacyDecisions = (projects, legacy, eventId) => {
|
|
49850
|
+
for (const [hash, record] of Object.entries(legacy ?? {})) {
|
|
49851
|
+
const existing = projects[hash] ?? { rootDirectory: record.rootDirectory ?? "" };
|
|
49852
|
+
projects[hash] = {
|
|
49853
|
+
...existing,
|
|
49854
|
+
events: {
|
|
49855
|
+
...existing.events,
|
|
49856
|
+
[eventId]: {
|
|
49857
|
+
firedAt: record.at ?? nowIso(),
|
|
49858
|
+
version: 1,
|
|
49859
|
+
...record.outcome ? { outcome: record.outcome } : {}
|
|
49860
|
+
}
|
|
49861
|
+
}
|
|
49862
|
+
};
|
|
49863
|
+
}
|
|
49864
|
+
};
|
|
49865
|
+
const migrateCliState = (state) => {
|
|
49866
|
+
if (state.schemaVersion === 2) return state;
|
|
49867
|
+
const projects = {};
|
|
49868
|
+
for (const [hash, record] of Object.entries(state.projects ?? {})) {
|
|
49869
|
+
const carried = {
|
|
49870
|
+
rootDirectory: record.rootDirectory,
|
|
49871
|
+
...record.events ? { events: record.events } : {},
|
|
49872
|
+
...record.migrations ? { migrations: record.migrations } : {}
|
|
49873
|
+
};
|
|
49874
|
+
projects[hash] = record.setupPrompt === false ? {
|
|
49875
|
+
...carried,
|
|
49876
|
+
events: {
|
|
49877
|
+
...carried.events,
|
|
49878
|
+
[SETUP_HINT_EVENT]: {
|
|
49879
|
+
firedAt: nowIso(),
|
|
49880
|
+
version: 1,
|
|
49881
|
+
outcome: "declined"
|
|
49882
|
+
}
|
|
49883
|
+
}
|
|
49884
|
+
} : carried;
|
|
49885
|
+
}
|
|
49886
|
+
foldLegacyDecisions(projects, state.ciPrompts, CI_PITCH_EVENT);
|
|
49887
|
+
foldLegacyDecisions(projects, state.actionUpgrades, ACTION_UPGRADE_EVENT);
|
|
49888
|
+
return {
|
|
49889
|
+
schemaVersion: 2,
|
|
49890
|
+
global: typeof state.onboardedAt === "string" ? { events: { [ONBOARDING_EVENT]: {
|
|
49891
|
+
firedAt: state.onboardedAt,
|
|
49892
|
+
version: 1
|
|
49893
|
+
} } } : {},
|
|
49894
|
+
projects
|
|
49895
|
+
};
|
|
49896
|
+
};
|
|
49897
|
+
const resolveConfigDir = (options) => options.cwd ?? (process.env["REACT_DOCTOR_CONFIG_DIR"] || void 0);
|
|
49898
|
+
const openStore = (options = {}) => new Conf({
|
|
48886
49899
|
projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
|
|
48887
|
-
cwd: options
|
|
49900
|
+
cwd: resolveConfigDir(options)
|
|
48888
49901
|
});
|
|
48889
|
-
const
|
|
49902
|
+
const openMigratedStore = (options) => {
|
|
49903
|
+
const store = openStore(options);
|
|
49904
|
+
if (store.store.schemaVersion !== 2) store.store = migrateCliState(store.store);
|
|
49905
|
+
return store;
|
|
49906
|
+
};
|
|
49907
|
+
const readCliState = (select, fallback, options = {}) => {
|
|
48890
49908
|
try {
|
|
48891
|
-
return
|
|
49909
|
+
return select(openMigratedStore(options).store);
|
|
48892
49910
|
} catch {
|
|
49911
|
+
return fallback;
|
|
49912
|
+
}
|
|
49913
|
+
};
|
|
49914
|
+
const updateCliState = (update, options = {}) => {
|
|
49915
|
+
try {
|
|
49916
|
+
const store = openMigratedStore(options);
|
|
49917
|
+
store.store = update(store.store);
|
|
48893
49918
|
return true;
|
|
49919
|
+
} catch {
|
|
49920
|
+
return false;
|
|
48894
49921
|
}
|
|
48895
49922
|
};
|
|
49923
|
+
//#endregion
|
|
49924
|
+
//#region src/cli/utils/hash-project-root.ts
|
|
49925
|
+
const hashProjectRoot = (projectRoot) => createHash("sha256").update(Path.resolve(projectRoot)).digest("hex");
|
|
49926
|
+
//#endregion
|
|
49927
|
+
//#region src/cli/utils/cli-lifecycle.ts
|
|
49928
|
+
const versionOf = (item) => item.version ?? 1;
|
|
49929
|
+
const selectScope = (state, scoped, projectRoot) => scoped.scope === "global" ? state.global : projectRoot === void 0 ? void 0 : state.projects?.[hashProjectRoot(projectRoot)];
|
|
49930
|
+
const updateScope = (state, scoped, projectRoot, updateScopeState) => {
|
|
49931
|
+
if (scoped.scope === "global") return {
|
|
49932
|
+
...state,
|
|
49933
|
+
global: updateScopeState(state.global ?? {})
|
|
49934
|
+
};
|
|
49935
|
+
if (projectRoot === void 0) return state;
|
|
49936
|
+
const projectKey = hashProjectRoot(projectRoot);
|
|
49937
|
+
const base = state.projects?.[projectKey] ?? { rootDirectory: Path.resolve(projectRoot) };
|
|
49938
|
+
return {
|
|
49939
|
+
...state,
|
|
49940
|
+
projects: {
|
|
49941
|
+
...state.projects,
|
|
49942
|
+
[projectKey]: {
|
|
49943
|
+
...base,
|
|
49944
|
+
...updateScopeState(base)
|
|
49945
|
+
}
|
|
49946
|
+
}
|
|
49947
|
+
};
|
|
49948
|
+
};
|
|
49949
|
+
const isGatePending = (gate, target = {}, options = {}) => {
|
|
49950
|
+
if (gate.scope === "project" && target.projectRoot === void 0) return false;
|
|
49951
|
+
return readCliState((state) => {
|
|
49952
|
+
const record = selectScope(state, gate, target.projectRoot)?.events?.[gate.id];
|
|
49953
|
+
return !record || record.version < versionOf(gate);
|
|
49954
|
+
}, gate.fireWhenUnknown ?? false, options);
|
|
49955
|
+
};
|
|
49956
|
+
const recordGate = (gate, target = {}, options = {}) => updateCliState((state) => updateScope(state, gate, target.projectRoot, (scope) => ({
|
|
49957
|
+
...scope,
|
|
49958
|
+
events: {
|
|
49959
|
+
...scope.events,
|
|
49960
|
+
[gate.id]: {
|
|
49961
|
+
firedAt: nowIso(),
|
|
49962
|
+
version: versionOf(gate),
|
|
49963
|
+
...target.outcome ? { outcome: target.outcome } : {}
|
|
49964
|
+
}
|
|
49965
|
+
}
|
|
49966
|
+
})), options);
|
|
49967
|
+
const readPreference = (preference, target = {}, options = {}) => readCliState((state) => selectScope(state, preference, target.projectRoot)?.preferences?.[preference.id] ?? null, null, options);
|
|
49968
|
+
const writePreference = (preference, value, target = {}, options = {}) => updateCliState((state) => updateScope(state, preference, target.projectRoot, (scope) => ({
|
|
49969
|
+
...scope,
|
|
49970
|
+
preferences: {
|
|
49971
|
+
...scope.preferences,
|
|
49972
|
+
[preference.id]: value
|
|
49973
|
+
}
|
|
49974
|
+
})), options);
|
|
49975
|
+
const isMigrationPending = (migration, target = {}, options = {}) => {
|
|
49976
|
+
if (migration.scope === "project" && target.projectRoot === void 0) return false;
|
|
49977
|
+
return readCliState((state) => {
|
|
49978
|
+
const record = selectScope(state, migration, target.projectRoot)?.migrations?.[migration.id];
|
|
49979
|
+
return !record || record.version < versionOf(migration);
|
|
49980
|
+
}, false, options);
|
|
49981
|
+
};
|
|
49982
|
+
const recordMigration = (migration, projectRoot, record, options) => updateCliState((state) => updateScope(state, migration, projectRoot, (scope) => ({
|
|
49983
|
+
...scope,
|
|
49984
|
+
migrations: {
|
|
49985
|
+
...scope.migrations,
|
|
49986
|
+
[migration.id]: record
|
|
49987
|
+
}
|
|
49988
|
+
})), options);
|
|
49989
|
+
const runMigrations = async (migrations, target = {}, options = {}) => {
|
|
49990
|
+
const results = [];
|
|
49991
|
+
for (const migration of migrations) {
|
|
49992
|
+
if (!isMigrationPending(migration, target, options)) {
|
|
49993
|
+
results.push({
|
|
49994
|
+
id: migration.id,
|
|
49995
|
+
ran: false,
|
|
49996
|
+
applied: true
|
|
49997
|
+
});
|
|
49998
|
+
continue;
|
|
49999
|
+
}
|
|
50000
|
+
let applied = false;
|
|
50001
|
+
try {
|
|
50002
|
+
applied = await migration.run({ projectRoot: target.projectRoot });
|
|
50003
|
+
} catch {
|
|
50004
|
+
applied = false;
|
|
50005
|
+
}
|
|
50006
|
+
if (applied) recordMigration(migration, target.projectRoot, {
|
|
50007
|
+
ranAt: nowIso(),
|
|
50008
|
+
version: versionOf(migration)
|
|
50009
|
+
}, options);
|
|
50010
|
+
results.push({
|
|
50011
|
+
id: migration.id,
|
|
50012
|
+
ran: true,
|
|
50013
|
+
applied
|
|
50014
|
+
});
|
|
50015
|
+
}
|
|
50016
|
+
return results;
|
|
50017
|
+
};
|
|
50018
|
+
//#endregion
|
|
50019
|
+
//#region src/cli/utils/onboarding-state.ts
|
|
50020
|
+
const ONBOARDING_GATE = {
|
|
50021
|
+
id: ONBOARDING_EVENT,
|
|
50022
|
+
scope: "global"
|
|
50023
|
+
};
|
|
50024
|
+
const hasCompletedOnboarding = (options = {}) => !isGatePending(ONBOARDING_GATE, {}, options);
|
|
48896
50025
|
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 {}
|
|
50026
|
+
if (isGatePending(ONBOARDING_GATE, {}, options)) recordGate(ONBOARDING_GATE, {}, options);
|
|
48902
50027
|
};
|
|
48903
50028
|
//#endregion
|
|
48904
50029
|
//#region src/cli/utils/render-project-detection.ts
|
|
@@ -49384,7 +50509,7 @@ const readPackageJson = (projectRoot) => {
|
|
|
49384
50509
|
return null;
|
|
49385
50510
|
}
|
|
49386
50511
|
};
|
|
49387
|
-
const writeJsonFile
|
|
50512
|
+
const writeJsonFile = (filePath, value) => {
|
|
49388
50513
|
NFS.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
49389
50514
|
};
|
|
49390
50515
|
const packageHasDependency = (projectRoot, dependencyName) => {
|
|
@@ -49473,12 +50598,12 @@ const resolveCacheFilePath = (projectDirectory) => {
|
|
|
49473
50598
|
const readPersistedCache = (cacheFilePath) => {
|
|
49474
50599
|
try {
|
|
49475
50600
|
const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
|
|
49476
|
-
if (!isRecord(parsed) || parsed.version !==
|
|
49477
|
-
version:
|
|
50601
|
+
if (!isRecord(parsed) || parsed.version !== 2) return {
|
|
50602
|
+
version: 2,
|
|
49478
50603
|
entries: []
|
|
49479
50604
|
};
|
|
49480
50605
|
if (!Array.isArray(parsed.entries)) return {
|
|
49481
|
-
version:
|
|
50606
|
+
version: 2,
|
|
49482
50607
|
entries: []
|
|
49483
50608
|
};
|
|
49484
50609
|
const entries = [];
|
|
@@ -49488,12 +50613,12 @@ const readPersistedCache = (cacheFilePath) => {
|
|
|
49488
50613
|
entries.push(entry);
|
|
49489
50614
|
}
|
|
49490
50615
|
return {
|
|
49491
|
-
version:
|
|
50616
|
+
version: 2,
|
|
49492
50617
|
entries
|
|
49493
50618
|
};
|
|
49494
50619
|
} catch {
|
|
49495
50620
|
return {
|
|
49496
|
-
version:
|
|
50621
|
+
version: 2,
|
|
49497
50622
|
entries: []
|
|
49498
50623
|
};
|
|
49499
50624
|
}
|
|
@@ -49546,7 +50671,7 @@ const buildScanResultCacheKey = (input) => {
|
|
|
49546
50671
|
if (headSha === null) return null;
|
|
49547
50672
|
if (stringifyStableJson(input.userConfig) === null) return null;
|
|
49548
50673
|
const cacheKeyJson = stringifyStableJson({
|
|
49549
|
-
schemaVersion:
|
|
50674
|
+
schemaVersion: 2,
|
|
49550
50675
|
projectIdentity: resolveProjectIdentity(input.projectDirectory),
|
|
49551
50676
|
headSha,
|
|
49552
50677
|
reactDoctorVersion: input.version,
|
|
@@ -49566,6 +50691,7 @@ const buildScanResultCacheKey = (input) => {
|
|
|
49566
50691
|
adoptExistingLintConfig: input.options.adoptExistingLintConfig,
|
|
49567
50692
|
ignoredTags: [...input.options.ignoredTags].sort(),
|
|
49568
50693
|
concurrency: input.options.concurrency,
|
|
50694
|
+
lintBatchOrdering: resolveLintBatchOrdering(),
|
|
49569
50695
|
baselineRef: input.options.baseline?.ref,
|
|
49570
50696
|
changedLineRanges: input.options.changedLineRanges ?? void 0,
|
|
49571
50697
|
noScore: input.options.noScore,
|
|
@@ -49583,7 +50709,7 @@ const createScanResultCache = (projectDirectory) => {
|
|
|
49583
50709
|
for (const entry of persistedCache.entries) entries.set(entry.key, entry);
|
|
49584
50710
|
const persist = () => {
|
|
49585
50711
|
writePersistedCache(cacheFilePath, {
|
|
49586
|
-
version:
|
|
50712
|
+
version: 2,
|
|
49587
50713
|
entries: [...entries.values()].sort((firstEntry, secondEntry) => secondEntry.createdAtMs - firstEntry.createdAtMs).slice(0, 20)
|
|
49588
50714
|
});
|
|
49589
50715
|
};
|
|
@@ -49599,7 +50725,7 @@ const createScanResultCache = (projectDirectory) => {
|
|
|
49599
50725
|
}
|
|
49600
50726
|
};
|
|
49601
50727
|
};
|
|
49602
|
-
const shouldStoreScanPayload = (payload) => !payload.didLintFail && !payload.didDeadCodeFail && payload.lintPartialFailures.length === 0;
|
|
50728
|
+
const shouldStoreScanPayload = (payload) => !payload.didLintFail && !payload.didDeadCodeFail && payload.lintPartialFailures.length === 0 && !payload.supplyChainOverlapTimedOut;
|
|
49603
50729
|
//#endregion
|
|
49604
50730
|
//#region src/inspect.ts
|
|
49605
50731
|
const silentConsole = makeNoopConsole();
|
|
@@ -49663,21 +50789,24 @@ const deriveScope = (options) => {
|
|
|
49663
50789
|
if (options.changedLineRanges !== null) return "lines";
|
|
49664
50790
|
return options.includePaths.length > 0 ? "files" : "full";
|
|
49665
50791
|
};
|
|
49666
|
-
const buildRunEventConfig = (options, userConfig, hasCustomConfig) =>
|
|
49667
|
-
|
|
49668
|
-
|
|
49669
|
-
|
|
49670
|
-
|
|
49671
|
-
|
|
49672
|
-
|
|
49673
|
-
|
|
49674
|
-
|
|
49675
|
-
|
|
49676
|
-
|
|
49677
|
-
|
|
49678
|
-
|
|
49679
|
-
|
|
49680
|
-
|
|
50792
|
+
const buildRunEventConfig = (options, userConfig, hasCustomConfig, resolvedWorkerCount) => {
|
|
50793
|
+
const { workerCount, parallel } = resolveWorkerTelemetry(resolvedWorkerCount, options.concurrency);
|
|
50794
|
+
return {
|
|
50795
|
+
scope: deriveScope(options),
|
|
50796
|
+
parallel,
|
|
50797
|
+
workerCount,
|
|
50798
|
+
lint: options.lint,
|
|
50799
|
+
deadCode: options.deadCode,
|
|
50800
|
+
scoreOnly: options.scoreOnly,
|
|
50801
|
+
noScore: options.noScore,
|
|
50802
|
+
respectInlineDisables: options.respectInlineDisables,
|
|
50803
|
+
showWarnings: options.warnings,
|
|
50804
|
+
usedOutputDir: options.outputDirectory !== null,
|
|
50805
|
+
ignoredTagCount: options.ignoredTags.size,
|
|
50806
|
+
hasCustomConfig,
|
|
50807
|
+
userConfig
|
|
50808
|
+
};
|
|
50809
|
+
};
|
|
49681
50810
|
const inspect = async (directory, inputOptions = {}) => {
|
|
49682
50811
|
const startTime = performance$1.now();
|
|
49683
50812
|
const isConcurrentScan = inputOptions.concurrentScan === true;
|
|
@@ -49769,7 +50898,7 @@ const runBaselineComparison = async (params) => {
|
|
|
49769
50898
|
resolveLocalGithubViewerPermission: false,
|
|
49770
50899
|
suppressScanSummary: true,
|
|
49771
50900
|
supplyChainManifestChanged: params.options.supplyChainManifestChanged
|
|
49772
|
-
}, {}).pipe(provide(baseLayers), provideService(Console, silentConsole))));
|
|
50901
|
+
}, {}).pipe(provide(baseLayers), provideService$2(Console, silentConsole))));
|
|
49773
50902
|
if (baseOutput.didLintFail) return null;
|
|
49774
50903
|
const delta = computeDiagnosticDelta({
|
|
49775
50904
|
headDiagnostics: params.headDiagnostics,
|
|
@@ -49854,7 +50983,8 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
|
|
|
49854
50983
|
runId: getRunId(),
|
|
49855
50984
|
resolveLocalGithubViewerPermission: !options.noScore,
|
|
49856
50985
|
suppressScanSummary: options.suppressRendering,
|
|
49857
|
-
supplyChainManifestChanged: options.supplyChainManifestChanged
|
|
50986
|
+
supplyChainManifestChanged: options.supplyChainManifestChanged,
|
|
50987
|
+
concurrentScan: options.concurrentScan
|
|
49858
50988
|
}, { beforeLint: (projectInfo, lintIncludePaths) => gen(function* () {
|
|
49859
50989
|
recordSentryProjectContext(projectInfo, rootSentrySpan, { concurrentScan: options.concurrentScan });
|
|
49860
50990
|
recordCount(METRIC.projectDetected, 1);
|
|
@@ -49868,7 +50998,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
|
|
|
49868
50998
|
lintSourceFileCount
|
|
49869
50999
|
});
|
|
49870
51000
|
}) });
|
|
49871
|
-
const output = await runPromise(restoreLegacyThrow(applyObservability(options.silent ? program.pipe(provide(layers), provideService(Console, silentConsole)) : program.pipe(provide(layers)), rootSentrySpan)));
|
|
51001
|
+
const output = await runPromise(restoreLegacyThrow(applyObservability(options.silent ? program.pipe(provide(layers), provideService$2(Console, silentConsole)) : program.pipe(provide(layers)), rootSentrySpan)));
|
|
49872
51002
|
const didLintFail = lintBindingMissing || output.didLintFail;
|
|
49873
51003
|
const lintFailureReason = lintBindingMissing ? `oxlint native binding not found for Node ${process.version}; expected one matching ${OXLINT_NODE_REQUIREMENT}` : output.lintFailureReason;
|
|
49874
51004
|
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 +51035,15 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
|
|
|
49905
51035
|
lintPartialFailures: output.lintPartialFailures,
|
|
49906
51036
|
didDeadCodeFail: output.didDeadCodeFail,
|
|
49907
51037
|
deadCodeFailureReason: output.deadCodeFailureReason,
|
|
51038
|
+
deadCodeOverlapped: output.deadCodeOverlapped,
|
|
49908
51039
|
directory: output.resolvedDirectory,
|
|
49909
51040
|
scannedFileCount: output.scannedFileCount,
|
|
49910
51041
|
scannedFilePaths: output.scannedFilePaths,
|
|
49911
51042
|
scanElapsedMilliseconds: output.scanElapsedMilliseconds,
|
|
51043
|
+
scanConcurrency: output.scanConcurrency,
|
|
49912
51044
|
baselineDelta,
|
|
49913
|
-
lintFailureReasonKind: lintBindingMissing ? "native-binding-missing" : output.lintFailureReasonKind
|
|
51045
|
+
lintFailureReasonKind: lintBindingMissing ? "native-binding-missing" : output.lintFailureReasonKind,
|
|
51046
|
+
supplyChainOverlapTimedOut: output.supplyChainOverlapTimedOut
|
|
49914
51047
|
};
|
|
49915
51048
|
if (cacheKey !== null && scanResultCache !== null && shouldStoreScanPayload(payload)) scanResultCache.store(cacheKey, payload);
|
|
49916
51049
|
const result = await renderAndRecordScan({
|
|
@@ -49921,12 +51054,14 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
|
|
|
49921
51054
|
startTime,
|
|
49922
51055
|
rootSentrySpan,
|
|
49923
51056
|
scanMode: baselineDelta ? "baseline" : isDiffMode ? "diff" : "full",
|
|
49924
|
-
baselineDegraded
|
|
51057
|
+
baselineDegraded,
|
|
51058
|
+
lintCacheHitFileCount: output.lintCacheHitFileCount,
|
|
51059
|
+
lintCacheTotalFileCount: output.lintCacheTotalFileCount
|
|
49925
51060
|
});
|
|
49926
51061
|
recordOnboardingCompletion(options);
|
|
49927
51062
|
return result;
|
|
49928
51063
|
};
|
|
49929
|
-
const runMaybeSilent = (effect, silent) => silent ? effect.pipe(provideService(Console, silentConsole)) : effect;
|
|
51064
|
+
const runMaybeSilent = (effect, silent) => silent ? effect.pipe(provideService$2(Console, silentConsole)) : effect;
|
|
49930
51065
|
const renderCachedProjectDetection = async (input) => {
|
|
49931
51066
|
if (input.options.scoreOnly || input.options.suppressRendering) return;
|
|
49932
51067
|
await runPromise(runMaybeSilent(printProjectDetection({
|
|
@@ -49954,14 +51089,17 @@ const renderAndRecordScan = async (input) => {
|
|
|
49954
51089
|
scannedFileCount: input.payload.scannedFileCount,
|
|
49955
51090
|
scannedFilePaths: input.payload.scannedFilePaths,
|
|
49956
51091
|
scanElapsedMilliseconds: input.payload.scanElapsedMilliseconds,
|
|
51092
|
+
lintCacheHitFileCount: input.lintCacheHitFileCount ?? null,
|
|
51093
|
+
lintCacheTotalFileCount: input.lintCacheTotalFileCount ?? null,
|
|
49957
51094
|
baselineDelta: input.payload.baselineDelta
|
|
49958
51095
|
}), input.options.silent));
|
|
51096
|
+
const { workerCount: resolvedWorkerCount, parallel } = resolveWorkerTelemetry(input.payload.scanConcurrency, input.options.concurrency);
|
|
49959
51097
|
recordScanMetrics({
|
|
49960
51098
|
result,
|
|
49961
51099
|
mode: input.scanMode,
|
|
49962
51100
|
baselineDegraded: input.baselineDegraded,
|
|
49963
|
-
parallel
|
|
49964
|
-
workerCount:
|
|
51101
|
+
parallel,
|
|
51102
|
+
workerCount: resolvedWorkerCount,
|
|
49965
51103
|
lint: input.options.lint,
|
|
49966
51104
|
deadCode: input.options.deadCode,
|
|
49967
51105
|
scoreOnly: input.options.scoreOnly,
|
|
@@ -49971,19 +51109,22 @@ const renderAndRecordScan = async (input) => {
|
|
|
49971
51109
|
didDeadCodeFail: input.payload.didDeadCodeFail
|
|
49972
51110
|
});
|
|
49973
51111
|
recordRunEvent(input.rootSentrySpan, {
|
|
49974
|
-
...buildRunEventConfig(input.options, input.userConfig, input.hasCustomConfig),
|
|
51112
|
+
...buildRunEventConfig(input.options, input.userConfig, input.hasCustomConfig, resolvedWorkerCount),
|
|
49975
51113
|
result,
|
|
49976
51114
|
mode: input.scanMode,
|
|
49977
51115
|
gateExempt: input.baselineDegraded,
|
|
49978
51116
|
didLintFail: input.payload.didLintFail,
|
|
49979
51117
|
lintFailureReasonKind: input.payload.lintFailureReasonKind,
|
|
49980
51118
|
lintPartialFailureCount: input.payload.lintPartialFailures.length,
|
|
49981
|
-
|
|
51119
|
+
lintDroppedFileCount: countDroppedLintFiles(input.payload.lintPartialFailures),
|
|
51120
|
+
didDeadCodeFail: input.payload.didDeadCodeFail,
|
|
51121
|
+
supplyChainOverlapTimedOut: input.payload.supplyChainOverlapTimedOut,
|
|
51122
|
+
deadCodeOverlapped: input.payload.deadCodeOverlapped
|
|
49982
51123
|
});
|
|
49983
51124
|
return result;
|
|
49984
51125
|
};
|
|
49985
51126
|
const finalizeAndRender = (input) => gen(function* () {
|
|
49986
|
-
const { options, elapsedMilliseconds, diagnostics, score, project, userConfig, didLintFail, lintFailureReason, lintPartialFailures, didDeadCodeFail, deadCodeFailureReason, directory, scannedFileCount, scannedFilePaths, scanElapsedMilliseconds, baselineDelta } = input;
|
|
51127
|
+
const { options, elapsedMilliseconds, diagnostics, score, project, userConfig, didLintFail, lintFailureReason, lintPartialFailures, didDeadCodeFail, deadCodeFailureReason, directory, scannedFileCount, scannedFilePaths, scanElapsedMilliseconds, lintCacheHitFileCount, lintCacheTotalFileCount, baselineDelta } = input;
|
|
49987
51128
|
const { skippedChecks, skippedCheckReasons } = buildSkippedChecks({
|
|
49988
51129
|
didLintFail,
|
|
49989
51130
|
lintFailureReason,
|
|
@@ -50003,6 +51144,10 @@ const finalizeAndRender = (input) => gen(function* () {
|
|
|
50003
51144
|
scannedFileCount,
|
|
50004
51145
|
scannedFilePaths,
|
|
50005
51146
|
scanElapsedMilliseconds,
|
|
51147
|
+
...lintCacheTotalFileCount !== null ? {
|
|
51148
|
+
lintCacheHitFileCount,
|
|
51149
|
+
lintCacheTotalFileCount
|
|
51150
|
+
} : {},
|
|
50006
51151
|
...baselineDelta ? { baselineDelta } : {}
|
|
50007
51152
|
});
|
|
50008
51153
|
if (options.suppressRendering) return buildResult();
|
|
@@ -50084,7 +51229,8 @@ const getStagedSourceFiles = async (directory) => {
|
|
|
50084
51229
|
return [...await runPromise(gen(function* () {
|
|
50085
51230
|
return yield* (yield* StagedFiles).discoverSourceFiles(directory);
|
|
50086
51231
|
}).pipe(provide(stagedFilesLayer)))];
|
|
50087
|
-
} catch {
|
|
51232
|
+
} catch (error) {
|
|
51233
|
+
cliLogger.warn(`Failed to discover staged files: ${error instanceof Error ? error.message : String(error)}`);
|
|
50088
51234
|
return [];
|
|
50089
51235
|
}
|
|
50090
51236
|
};
|
|
@@ -50103,6 +51249,35 @@ const materializeStagedFiles = async (directory, stagedFiles, tempDirectory) =>
|
|
|
50103
51249
|
};
|
|
50104
51250
|
};
|
|
50105
51251
|
//#endregion
|
|
51252
|
+
//#region src/cli/utils/is-environment-error.ts
|
|
51253
|
+
const isNodeSystemError = (error) => error instanceof Error && typeof error.code === "string";
|
|
51254
|
+
const ENVIRONMENT_ERROR_CODES = new Set([
|
|
51255
|
+
"ENOSPC",
|
|
51256
|
+
"EIO",
|
|
51257
|
+
"EROFS",
|
|
51258
|
+
"EACCES",
|
|
51259
|
+
"EPERM",
|
|
51260
|
+
"ENOTDIR"
|
|
51261
|
+
]);
|
|
51262
|
+
const isEnvironmentError = (error) => {
|
|
51263
|
+
if (!isNodeSystemError(error)) return false;
|
|
51264
|
+
if (error.code === "ENOENT") return error.syscall?.startsWith("spawn") ?? false;
|
|
51265
|
+
return error.code !== void 0 && ENVIRONMENT_ERROR_CODES.has(error.code);
|
|
51266
|
+
};
|
|
51267
|
+
const formatEnvironmentError = (error) => {
|
|
51268
|
+
if (!isNodeSystemError(error)) return error instanceof Error ? error.message : String(error);
|
|
51269
|
+
switch (error.code) {
|
|
51270
|
+
case "ENOSPC": return "No space left on device. Free up disk space and try again.";
|
|
51271
|
+
case "EIO": return "I/O error: the filesystem or disk may be failing. Check your system logs.";
|
|
51272
|
+
case "EROFS": return "Read-only filesystem: cannot write to this location.";
|
|
51273
|
+
case "EACCES":
|
|
51274
|
+
case "EPERM": return error.path ? `Permission denied accessing ${error.path}. Check file permissions and try again.` : "Permission denied. Check file permissions and try again.";
|
|
51275
|
+
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.";
|
|
51276
|
+
case "ENOENT": return "Required command not found. Ensure the tool (e.g. git) is installed and on your PATH.";
|
|
51277
|
+
default: return error.message;
|
|
51278
|
+
}
|
|
51279
|
+
};
|
|
51280
|
+
//#endregion
|
|
50106
51281
|
//#region src/cli/utils/handle-error.ts
|
|
50107
51282
|
const OTLP_ENDPOINT_ENVIRONMENT_VARIABLE = "REACT_DOCTOR_OTLP_ENDPOINT";
|
|
50108
51283
|
const OTLP_AUTH_HEADER_ENVIRONMENT_VARIABLE = "REACT_DOCTOR_OTLP_AUTH_HEADER";
|
|
@@ -50185,15 +51360,19 @@ const handleError = (error, options = {}) => {
|
|
|
50185
51360
|
process.exitCode = 1;
|
|
50186
51361
|
};
|
|
50187
51362
|
/**
|
|
50188
|
-
* Renderer for expected, user-actionable failures — a bad `--diff` value
|
|
50189
|
-
* a base branch that isn't fetched
|
|
50190
|
-
*
|
|
50191
|
-
*
|
|
51363
|
+
* Renderer for expected, user-actionable failures — a bad `--diff` value,
|
|
51364
|
+
* a base branch that isn't fetched, or environment errors like disk-full or
|
|
51365
|
+
* permission-denied. Prints just the (already human-readable) message — no
|
|
51366
|
+
* "Something went wrong", prefilled issue, Discord link, or Sentry reference
|
|
51367
|
+
* — because there is no bug to report.
|
|
50192
51368
|
*/
|
|
50193
51369
|
const handleUserError = (error, options = {}) => {
|
|
51370
|
+
const isEnvError = isEnvironmentError(error);
|
|
51371
|
+
if (isEnvError) recordCount(METRIC.cliEnvironmentError, 1, { code: error.code ?? "unknown" });
|
|
51372
|
+
const message = isEnvError ? formatEnvironmentError(error) : formatErrorForReport(error);
|
|
50194
51373
|
runSync(gen(function* () {
|
|
50195
51374
|
yield* error$1("");
|
|
50196
|
-
yield* error$1(highlighter.error(
|
|
51375
|
+
yield* error$1(highlighter.error(message));
|
|
50197
51376
|
yield* error$1("");
|
|
50198
51377
|
}));
|
|
50199
51378
|
if (options.shouldExit !== false) process.exit(1);
|
|
@@ -50208,7 +51387,7 @@ const handleUserError = (error, options = {}) => {
|
|
|
50208
51387
|
* `handleUserError` (a plain message — no "Something went wrong", prefilled
|
|
50209
51388
|
* issue, Discord link, or Sentry reference), since there is no bug to report.
|
|
50210
51389
|
*
|
|
50211
|
-
*
|
|
51390
|
+
* Four distinct shapes reach the CLI's catch blocks:
|
|
50212
51391
|
*
|
|
50213
51392
|
* - **Project-discovery failures** (`NoReactDependencyError`,
|
|
50214
51393
|
* `ProjectNotFoundError`, `PackageJsonNotFoundError`, `NotADirectoryError`,
|
|
@@ -50221,12 +51400,19 @@ const handleUserError = (error, options = {}) => {
|
|
|
50221
51400
|
* `--project` name.
|
|
50222
51401
|
* - **Bad `--diff` input** (`GitBaseBranchInvalid` / `GitBaseBranchMissing`)
|
|
50223
51402
|
* stays the tagged `ReactDoctorError`, so dispatch on the reason `_tag`.
|
|
51403
|
+
* - **Environment failures** (`ENOSPC`, `EIO`, `EROFS`, `EACCES`, `EPERM`,
|
|
51404
|
+
* `ENOTDIR`, plus a `spawn`-scoped `ENOENT` for a missing binary) — disk
|
|
51405
|
+
* full / failing / read-only, permission denied, or a path blocked by a
|
|
51406
|
+
* file. React Doctor cannot fix the user's environment; exit cleanly with an
|
|
51407
|
+
* actionable message instead of crashing. See `is-environment-error.ts` for
|
|
51408
|
+
* why the set stays narrow (codes that usually mean our bug keep reaching
|
|
51409
|
+
* Sentry).
|
|
50224
51410
|
*
|
|
50225
51411
|
* This composes the existing core narrowers rather than introducing a new
|
|
50226
51412
|
* error-shape helper (AGENTS.md): it encodes CLI-layer reporting policy, not
|
|
50227
51413
|
* knowledge of the `ReactDoctorError` shape.
|
|
50228
51414
|
*/
|
|
50229
|
-
const isExpectedUserError = (error) => error instanceof CliInputError || isProjectDiscoveryError(error) || isReactDoctorError(error) && (error.reason._tag === "GitBaseBranchInvalid" || error.reason._tag === "GitBaseBranchMissing");
|
|
51415
|
+
const isExpectedUserError = (error) => error instanceof CliInputError || isProjectDiscoveryError(error) || isEnvironmentError(error) || isReactDoctorError(error) && (error.reason._tag === "GitBaseBranchInvalid" || error.reason._tag === "GitBaseBranchMissing");
|
|
50230
51416
|
//#endregion
|
|
50231
51417
|
//#region src/cli/utils/build-handoff-payload.ts
|
|
50232
51418
|
const buildHandoffPayload = (input) => {
|
|
@@ -50307,6 +51493,20 @@ const detectAvailableAgents = async () => {
|
|
|
50307
51493
|
const detected = new Set([...detectPathAvailableAgents(), ...await detectInstalledSkillAgents()]);
|
|
50308
51494
|
return getSkillAgentTypes().filter((agent) => agent !== "universal" && detected.has(agent));
|
|
50309
51495
|
};
|
|
51496
|
+
const DEFAULT_INSTALL_AGENTS = [
|
|
51497
|
+
"claude-code",
|
|
51498
|
+
"cursor",
|
|
51499
|
+
"codex",
|
|
51500
|
+
"opencode"
|
|
51501
|
+
];
|
|
51502
|
+
const computeDefaultSelectedAgents = (detectedAgents, rememberedAgents) => {
|
|
51503
|
+
const detected = new Set(detectedAgents);
|
|
51504
|
+
const remembered = rememberedAgents.filter((agent) => detected.has(agent));
|
|
51505
|
+
if (remembered.length > 0) return remembered;
|
|
51506
|
+
const defaults = DEFAULT_INSTALL_AGENTS.filter((agent) => detected.has(agent));
|
|
51507
|
+
if (defaults.length > 0) return defaults;
|
|
51508
|
+
return detectedAgents.length === 1 ? [...detectedAgents] : [];
|
|
51509
|
+
};
|
|
50310
51510
|
//#endregion
|
|
50311
51511
|
//#region src/cli/utils/install-doctor-script.ts
|
|
50312
51512
|
const DOCTOR_SCRIPT_NAME = "doctor";
|
|
@@ -50388,7 +51588,7 @@ const installDoctorScript = (options) => {
|
|
|
50388
51588
|
};
|
|
50389
51589
|
})();
|
|
50390
51590
|
const scriptStatus = scriptTarget.status;
|
|
50391
|
-
if (scriptStatus === "created") writeJsonFile
|
|
51591
|
+
if (scriptStatus === "created") writeJsonFile(packageJsonPath, {
|
|
50392
51592
|
...packageJson,
|
|
50393
51593
|
scripts: {
|
|
50394
51594
|
...isRecord$1(scripts) ? scripts : {},
|
|
@@ -50542,54 +51742,35 @@ const upgradeReactDoctorWorkflowInPlace = (projectRoot) => {
|
|
|
50542
51742
|
}
|
|
50543
51743
|
};
|
|
50544
51744
|
//#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
51745
|
//#region src/cli/utils/action-upgrade-prompt.ts
|
|
50583
|
-
const
|
|
50584
|
-
|
|
50585
|
-
|
|
50586
|
-
|
|
51746
|
+
const ACTION_UPGRADE_GATE = {
|
|
51747
|
+
id: ACTION_UPGRADE_EVENT,
|
|
51748
|
+
scope: "project"
|
|
51749
|
+
};
|
|
51750
|
+
const hasHandledActionUpgrade = (projectRoot, options = {}) => !isGatePending(ACTION_UPGRADE_GATE, { projectRoot }, options);
|
|
51751
|
+
const recordActionUpgradeDecision = (projectRoot, outcome, options = {}) => recordGate(ACTION_UPGRADE_GATE, {
|
|
51752
|
+
projectRoot,
|
|
51753
|
+
outcome
|
|
51754
|
+
}, options);
|
|
50587
51755
|
//#endregion
|
|
50588
51756
|
//#region src/cli/utils/ci-prompt-decision.ts
|
|
50589
|
-
const
|
|
50590
|
-
|
|
50591
|
-
|
|
50592
|
-
|
|
51757
|
+
const CI_PITCH_GATE = {
|
|
51758
|
+
id: CI_PITCH_EVENT,
|
|
51759
|
+
scope: "project"
|
|
51760
|
+
};
|
|
51761
|
+
const hasHandledCiPrompt = (projectRoot, options = {}) => !isGatePending(CI_PITCH_GATE, { projectRoot }, options);
|
|
51762
|
+
const recordCiPromptDecision = (projectRoot, outcome, options = {}) => recordGate(CI_PITCH_GATE, {
|
|
51763
|
+
projectRoot,
|
|
51764
|
+
outcome
|
|
51765
|
+
}, options);
|
|
51766
|
+
//#endregion
|
|
51767
|
+
//#region src/cli/utils/handoff-target-preference.ts
|
|
51768
|
+
const HANDOFF_TARGET_PREFERENCE = {
|
|
51769
|
+
id: HANDOFF_TARGET_PREFERENCE_ID,
|
|
51770
|
+
scope: "global"
|
|
51771
|
+
};
|
|
51772
|
+
const readHandoffTarget = (options = {}) => readPreference(HANDOFF_TARGET_PREFERENCE, {}, options);
|
|
51773
|
+
const rememberHandoffTarget = (target, options = {}) => writePreference(HANDOFF_TARGET_PREFERENCE, target, {}, options);
|
|
50593
51774
|
//#endregion
|
|
50594
51775
|
//#region src/cli/utils/open-url.ts
|
|
50595
51776
|
const resolveOpenCommand = (url) => {
|
|
@@ -50755,39 +51936,80 @@ const DEFAULT_PR_TITLE = "Add React Doctor to GitHub Actions";
|
|
|
50755
51936
|
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
51937
|
|
|
50757
51938
|
Docs: https://www.react.doctor/ci`;
|
|
50758
|
-
const findUniqueBranchName = async (cwd) => {
|
|
50759
|
-
if (!(await
|
|
51939
|
+
const findUniqueBranchName = async (cwd, run) => {
|
|
51940
|
+
if (!(await run("git", [
|
|
50760
51941
|
"rev-parse",
|
|
50761
51942
|
"--verify",
|
|
50762
51943
|
NEW_BRANCH_PREFIX
|
|
50763
51944
|
], cwd)).success) return NEW_BRANCH_PREFIX;
|
|
50764
51945
|
return `${NEW_BRANCH_PREFIX}-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace(/[-:T]/g, "")}`;
|
|
50765
51946
|
};
|
|
51947
|
+
const findExistingSetupPullRequest = async (cwd, run) => {
|
|
51948
|
+
const prList = await run("gh", [
|
|
51949
|
+
"pr",
|
|
51950
|
+
"list",
|
|
51951
|
+
"--state",
|
|
51952
|
+
"open",
|
|
51953
|
+
"--json",
|
|
51954
|
+
"headRefName,url",
|
|
51955
|
+
"--limit",
|
|
51956
|
+
String(100)
|
|
51957
|
+
], cwd);
|
|
51958
|
+
if (!prList.success) return null;
|
|
51959
|
+
try {
|
|
51960
|
+
return JSON.parse(prList.stdout).find((pullRequest) => (pullRequest.headRefName ?? "").startsWith(NEW_BRANCH_PREFIX)) ?? null;
|
|
51961
|
+
} catch {
|
|
51962
|
+
return null;
|
|
51963
|
+
}
|
|
51964
|
+
};
|
|
51965
|
+
const hasUnrelatedTrackedChanges = async (cwd, workflowRelative, run) => {
|
|
51966
|
+
const statusProbe = await run("git", [
|
|
51967
|
+
"status",
|
|
51968
|
+
"--porcelain",
|
|
51969
|
+
"--",
|
|
51970
|
+
".",
|
|
51971
|
+
`:!${workflowRelative}`
|
|
51972
|
+
], cwd);
|
|
51973
|
+
if (!statusProbe.success) return true;
|
|
51974
|
+
return statusProbe.stdout.split(/\r?\n/).filter(Boolean).some((statusLine) => !statusLine.startsWith("??"));
|
|
51975
|
+
};
|
|
50766
51976
|
const openWorkflowPullRequest = async (params) => {
|
|
50767
51977
|
const workflowPath = Path.resolve(params.workflowPath);
|
|
50768
51978
|
const commitMessage = params.commitMessage ?? DEFAULT_COMMIT_MESSAGE;
|
|
50769
51979
|
const prTitle = params.prTitle ?? DEFAULT_PR_TITLE;
|
|
50770
51980
|
const prBody = params.prBody ?? DEFAULT_PR_BODY;
|
|
50771
|
-
const
|
|
51981
|
+
const run = params.run ?? runCommand;
|
|
51982
|
+
const checkCommandAvailable = params.checkCommandAvailable ?? isCommandAvailable;
|
|
51983
|
+
const repoRootProbe = await run("git", ["rev-parse", "--show-toplevel"], Path.dirname(workflowPath));
|
|
50772
51984
|
if (!repoRootProbe.success) return {
|
|
50773
51985
|
status: "not-attempted",
|
|
50774
51986
|
reason: "not-a-git-repo"
|
|
50775
51987
|
};
|
|
50776
51988
|
const cwd = repoRootProbe.stdout;
|
|
50777
|
-
|
|
51989
|
+
const workflowRelative = toForwardSlashes(Path.relative(cwd, workflowPath));
|
|
51990
|
+
if (!checkCommandAvailable("gh")) return {
|
|
50778
51991
|
status: "not-attempted",
|
|
50779
51992
|
reason: "gh-not-installed"
|
|
50780
51993
|
};
|
|
50781
|
-
if (!(await
|
|
51994
|
+
if (!(await run("gh", ["auth", "status"], cwd)).success) return {
|
|
50782
51995
|
status: "not-attempted",
|
|
50783
51996
|
reason: "gh-not-authenticated"
|
|
50784
51997
|
};
|
|
50785
|
-
const
|
|
51998
|
+
const existingSetupPullRequest = await findExistingSetupPullRequest(cwd, run);
|
|
51999
|
+
if (existingSetupPullRequest) return {
|
|
52000
|
+
status: "pr-exists",
|
|
52001
|
+
url: existingSetupPullRequest.url ?? ""
|
|
52002
|
+
};
|
|
52003
|
+
if (await hasUnrelatedTrackedChanges(cwd, workflowRelative, run)) return {
|
|
52004
|
+
status: "not-attempted",
|
|
52005
|
+
reason: "working-tree-dirty"
|
|
52006
|
+
};
|
|
52007
|
+
const defaultBranch = params.baseBranch ?? await detectDefaultBranch(cwd, run);
|
|
50786
52008
|
if (!defaultBranch) return {
|
|
50787
52009
|
status: "not-attempted",
|
|
50788
52010
|
reason: "no-default-branch"
|
|
50789
52011
|
};
|
|
50790
|
-
const previousBranchProbe = await
|
|
52012
|
+
const previousBranchProbe = await run("git", [
|
|
50791
52013
|
"rev-parse",
|
|
50792
52014
|
"--abbrev-ref",
|
|
50793
52015
|
"HEAD"
|
|
@@ -50797,13 +52019,13 @@ const openWorkflowPullRequest = async (params) => {
|
|
|
50797
52019
|
reason: "detached-head"
|
|
50798
52020
|
};
|
|
50799
52021
|
const previousBranch = previousBranchProbe.stdout;
|
|
50800
|
-
await
|
|
52022
|
+
await run("git", [
|
|
50801
52023
|
"fetch",
|
|
50802
52024
|
"origin",
|
|
50803
52025
|
defaultBranch
|
|
50804
52026
|
], cwd);
|
|
50805
|
-
const newBranch = await findUniqueBranchName(cwd);
|
|
50806
|
-
if (!(await
|
|
52027
|
+
const newBranch = await findUniqueBranchName(cwd, run);
|
|
52028
|
+
if (!(await run("git", [
|
|
50807
52029
|
"checkout",
|
|
50808
52030
|
"-b",
|
|
50809
52031
|
newBranch,
|
|
@@ -50813,17 +52035,17 @@ const openWorkflowPullRequest = async (params) => {
|
|
|
50813
52035
|
reason: "checkout-failed"
|
|
50814
52036
|
};
|
|
50815
52037
|
const restoreToPreviousBranch = async (deleteNewBranch) => {
|
|
50816
|
-
await
|
|
50817
|
-
if (deleteNewBranch) await
|
|
52038
|
+
await run("git", ["checkout", previousBranch], cwd);
|
|
52039
|
+
if (deleteNewBranch) await run("git", [
|
|
50818
52040
|
"branch",
|
|
50819
52041
|
"-D",
|
|
50820
52042
|
newBranch
|
|
50821
52043
|
], cwd);
|
|
50822
52044
|
};
|
|
50823
|
-
if (!(await
|
|
52045
|
+
if (!(await run("git", [
|
|
50824
52046
|
"add",
|
|
50825
52047
|
"--",
|
|
50826
|
-
|
|
52048
|
+
workflowRelative
|
|
50827
52049
|
], cwd)).success) {
|
|
50828
52050
|
await restoreToPreviousBranch(true);
|
|
50829
52051
|
return {
|
|
@@ -50831,7 +52053,7 @@ const openWorkflowPullRequest = async (params) => {
|
|
|
50831
52053
|
reason: "git-add-failed"
|
|
50832
52054
|
};
|
|
50833
52055
|
}
|
|
50834
|
-
if (!(await
|
|
52056
|
+
if (!(await run("git", [
|
|
50835
52057
|
"commit",
|
|
50836
52058
|
"-m",
|
|
50837
52059
|
commitMessage
|
|
@@ -50842,7 +52064,7 @@ const openWorkflowPullRequest = async (params) => {
|
|
|
50842
52064
|
reason: "git-commit-failed"
|
|
50843
52065
|
};
|
|
50844
52066
|
}
|
|
50845
|
-
if (!(await
|
|
52067
|
+
if (!(await run("git", [
|
|
50846
52068
|
"push",
|
|
50847
52069
|
"-u",
|
|
50848
52070
|
"origin",
|
|
@@ -50854,7 +52076,7 @@ const openWorkflowPullRequest = async (params) => {
|
|
|
50854
52076
|
reason: "git-push-failed"
|
|
50855
52077
|
};
|
|
50856
52078
|
}
|
|
50857
|
-
const prCreate = await
|
|
52079
|
+
const prCreate = await run("gh", [
|
|
50858
52080
|
"pr",
|
|
50859
52081
|
"create",
|
|
50860
52082
|
"--title",
|
|
@@ -50878,12 +52100,13 @@ const openWorkflowPullRequest = async (params) => {
|
|
|
50878
52100
|
};
|
|
50879
52101
|
const stageWorkflowFile = async (params) => {
|
|
50880
52102
|
const workflowPath = Path.resolve(params.workflowPath);
|
|
50881
|
-
const
|
|
52103
|
+
const run = params.run ?? runCommand;
|
|
52104
|
+
const repoRootProbe = await run("git", ["rev-parse", "--show-toplevel"], Path.dirname(workflowPath));
|
|
50882
52105
|
if (!repoRootProbe.success) return false;
|
|
50883
|
-
return (await
|
|
52106
|
+
return (await run("git", [
|
|
50884
52107
|
"add",
|
|
50885
52108
|
"--",
|
|
50886
|
-
Path.relative(repoRootProbe.stdout, workflowPath)
|
|
52109
|
+
toForwardSlashes(Path.relative(repoRootProbe.stdout, workflowPath))
|
|
50887
52110
|
], repoRootProbe.stdout)).success;
|
|
50888
52111
|
};
|
|
50889
52112
|
//#endregion
|
|
@@ -50924,6 +52147,7 @@ const setUpGitHubActions = async (options) => {
|
|
|
50924
52147
|
baseBranch: defaultBranch
|
|
50925
52148
|
});
|
|
50926
52149
|
if (pullRequestResult.status === "pr-opened") pullRequestSpinner.succeed(`Opened pull request for review: ${highlighter.info(pullRequestResult.url)}`);
|
|
52150
|
+
else if (pullRequestResult.status === "pr-exists") pullRequestSpinner.succeed(`A React Doctor setup pull request is already open: ${highlighter.info(pullRequestResult.url)}`);
|
|
50927
52151
|
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
52152
|
else {
|
|
50929
52153
|
pullRequestSpinner.stop();
|
|
@@ -50935,6 +52159,19 @@ const setUpGitHubActions = async (options) => {
|
|
|
50935
52159
|
return didCreateWorkflow;
|
|
50936
52160
|
};
|
|
50937
52161
|
//#endregion
|
|
52162
|
+
//#region src/cli/utils/install-agents-preference.ts
|
|
52163
|
+
const INSTALL_AGENTS_PREFERENCE = {
|
|
52164
|
+
id: INSTALL_AGENTS_PREFERENCE_ID,
|
|
52165
|
+
scope: "global"
|
|
52166
|
+
};
|
|
52167
|
+
const PREFERENCE_SEPARATOR = ",";
|
|
52168
|
+
const readInstallAgents = (options = {}) => {
|
|
52169
|
+
const stored = readPreference(INSTALL_AGENTS_PREFERENCE, {}, options);
|
|
52170
|
+
if (stored === null) return [];
|
|
52171
|
+
return stored.split(PREFERENCE_SEPARATOR).map((entry) => entry.trim()).filter((entry) => isSkillAgentType(entry));
|
|
52172
|
+
};
|
|
52173
|
+
const rememberInstallAgents = (agents, options = {}) => writePreference(INSTALL_AGENTS_PREFERENCE, agents.join(PREFERENCE_SEPARATOR), {}, options);
|
|
52174
|
+
//#endregion
|
|
50938
52175
|
//#region src/cli/utils/install-agent-hooks.ts
|
|
50939
52176
|
const CLAUDE_AGENT = "claude-code";
|
|
50940
52177
|
const CURSOR_AGENT = "cursor";
|
|
@@ -50945,20 +52182,34 @@ const CURSOR_HOOKS_RELATIVE_PATH = ".cursor/hooks.json";
|
|
|
50945
52182
|
const CURSOR_HOOK_RELATIVE_PATH = ".cursor/hooks/react-doctor.sh";
|
|
50946
52183
|
const CURSOR_HOOK_MATCHER = "Write|Edit|MultiEdit|ApplyPatch";
|
|
50947
52184
|
const CURSOR_HOOKS_SCHEMA_VERSION = 1;
|
|
50948
|
-
const JSON_INDENT_SPACES$1 = 2;
|
|
50949
52185
|
const isSupportedAgent = (agent) => agent === CLAUDE_AGENT || agent === CURSOR_AGENT;
|
|
50950
52186
|
const readJsonFile = (filePath, fallback) => {
|
|
50951
52187
|
if (!NFS.existsSync(filePath)) return fallback;
|
|
50952
52188
|
const content = NFS.readFileSync(filePath, "utf8").trim();
|
|
50953
52189
|
if (content.length === 0) return fallback;
|
|
50954
|
-
|
|
52190
|
+
try {
|
|
52191
|
+
return JSON.parse(content);
|
|
52192
|
+
} catch (error) {
|
|
52193
|
+
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.`);
|
|
52194
|
+
throw error;
|
|
52195
|
+
}
|
|
50955
52196
|
};
|
|
50956
|
-
const
|
|
50957
|
-
|
|
50958
|
-
|
|
52197
|
+
const ensureDirectoryExists = (directoryPath) => {
|
|
52198
|
+
try {
|
|
52199
|
+
NFS.mkdirSync(directoryPath, { recursive: true });
|
|
52200
|
+
} catch (error) {
|
|
52201
|
+
const code = error.code;
|
|
52202
|
+
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.`);
|
|
52203
|
+
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.`);
|
|
52204
|
+
throw error;
|
|
52205
|
+
}
|
|
52206
|
+
};
|
|
52207
|
+
const writeJsonFileWithDirectoryCheck = (filePath, value) => {
|
|
52208
|
+
ensureDirectoryExists(Path.dirname(filePath));
|
|
52209
|
+
writeJsonFile(filePath, value);
|
|
50959
52210
|
};
|
|
50960
52211
|
const writeHookScript = (filePath) => {
|
|
50961
|
-
|
|
52212
|
+
ensureDirectoryExists(Path.dirname(filePath));
|
|
50962
52213
|
NFS.writeFileSync(filePath, buildAgentHookScript());
|
|
50963
52214
|
NFS.chmodSync(filePath, 493);
|
|
50964
52215
|
};
|
|
@@ -50974,7 +52225,7 @@ const installClaudeHook = (projectRoot) => {
|
|
|
50974
52225
|
command: CLAUDE_HOOK_COMMAND
|
|
50975
52226
|
}] });
|
|
50976
52227
|
hooks.PostToolBatch = postToolBatchHooks;
|
|
50977
|
-
|
|
52228
|
+
writeJsonFileWithDirectoryCheck(settingsPath, {
|
|
50978
52229
|
...settings,
|
|
50979
52230
|
hooks
|
|
50980
52231
|
});
|
|
@@ -50994,7 +52245,7 @@ const installCursorHook = (projectRoot) => {
|
|
|
50994
52245
|
timeout: 120
|
|
50995
52246
|
});
|
|
50996
52247
|
hooks.postToolUse = postToolUseHooks;
|
|
50997
|
-
|
|
52248
|
+
writeJsonFileWithDirectoryCheck(configPath, {
|
|
50998
52249
|
...config,
|
|
50999
52250
|
version: config.version ?? CURSOR_HOOKS_SCHEMA_VERSION,
|
|
51000
52251
|
hooks
|
|
@@ -51230,7 +52481,7 @@ const installPackageJsonHook = (options, strategy) => {
|
|
|
51230
52481
|
parent = cloned;
|
|
51231
52482
|
}
|
|
51232
52483
|
parent[leafKey] = strategy.leafShape === "array" ? appendArrayCommand(parent[leafKey]) : appendStringCommand(parent[leafKey]);
|
|
51233
|
-
writeJsonFile
|
|
52484
|
+
writeJsonFile(packageJsonPath, nextPackageJson);
|
|
51234
52485
|
removeLegacyManagedRunner(options.projectRoot);
|
|
51235
52486
|
return {
|
|
51236
52487
|
hookPath: packageJsonPath,
|
|
@@ -51813,6 +53064,9 @@ const runInstallReactDoctor = async (options = {}) => {
|
|
|
51813
53064
|
const shouldUpgradeWorkflow = canUpgradeWorkflow && (Boolean(options.yes) || upgradePromptOutcome === "yes");
|
|
51814
53065
|
if (upgradePromptOutcome === "no" && !options.dryRun) recordActionUpgradeDecision(projectRoot, "declined");
|
|
51815
53066
|
if ((ciPromptOutcome === "yes" || ciPromptOutcome === "no") && !options.dryRun) recordCiPromptDecision(projectRoot, ciPromptOutcome === "yes" ? "accepted" : "declined");
|
|
53067
|
+
const rememberedAgents = options.lastSelectedAgents ?? readInstallAgents();
|
|
53068
|
+
const defaultSelectedAgents = computeDefaultSelectedAgents(detectedAgents, rememberedAgents);
|
|
53069
|
+
const usedRememberedAgents = rememberedAgents.some((agent) => detectedAgents.includes(agent));
|
|
51816
53070
|
const selectedAgents = skipPrompts ? detectedAgents : (await prompt({
|
|
51817
53071
|
type: "multiselect",
|
|
51818
53072
|
name: "agents",
|
|
@@ -51820,12 +53074,13 @@ const runInstallReactDoctor = async (options = {}) => {
|
|
|
51820
53074
|
choices: detectedAgents.map((agent) => ({
|
|
51821
53075
|
title: getSkillAgentConfig(agent).displayName,
|
|
51822
53076
|
value: agent,
|
|
51823
|
-
selected:
|
|
53077
|
+
selected: defaultSelectedAgents.includes(agent)
|
|
51824
53078
|
})),
|
|
51825
53079
|
instructions: false,
|
|
51826
53080
|
min: 1
|
|
51827
53081
|
}, promptOptions)).agents ?? [];
|
|
51828
53082
|
if (selectedAgents.length === 0) return;
|
|
53083
|
+
if (!skipPrompts && !options.dryRun) rememberInstallAgents(selectedAgents);
|
|
51829
53084
|
let dependencyResult;
|
|
51830
53085
|
if (!options.dryRun) {
|
|
51831
53086
|
await installReactDoctorSkillStep(sourceDir, selectedAgents, projectRoot);
|
|
@@ -51884,6 +53139,8 @@ const runInstallReactDoctor = async (options = {}) => {
|
|
|
51884
53139
|
}
|
|
51885
53140
|
recordCount(METRIC.installCompleted, 1, {
|
|
51886
53141
|
agentsCount: selectedAgents.length,
|
|
53142
|
+
agentsDetected: detectedAgents.length,
|
|
53143
|
+
usedRememberedAgents,
|
|
51887
53144
|
gitHook: shouldInstallGitHook,
|
|
51888
53145
|
agentHooks: shouldInstallAgentHooks,
|
|
51889
53146
|
workflow: didInstallWorkflow,
|
|
@@ -52024,7 +53281,12 @@ const upgradeGitHubActionsWorkflow = async (workflow) => {
|
|
|
52024
53281
|
prBody: UPGRADE_PR_BODY
|
|
52025
53282
|
});
|
|
52026
53283
|
if (pullRequestResult.status === "pr-opened") upgradeSpinner.succeed(`Opened pull request for review: ${highlighter.info(pullRequestResult.url)}`);
|
|
52027
|
-
else if (pullRequestResult.status === "
|
|
53284
|
+
else if (pullRequestResult.status === "pr-exists") {
|
|
53285
|
+
try {
|
|
53286
|
+
NFS.writeFileSync(workflow.workflowPath, workflow.content);
|
|
53287
|
+
} catch {}
|
|
53288
|
+
upgradeSpinner.succeed(`A React Doctor pull request is already open: ${highlighter.info(pullRequestResult.url)}`);
|
|
53289
|
+
} 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
53290
|
else {
|
|
52029
53291
|
upgradeSpinner.stop();
|
|
52030
53292
|
try {
|
|
@@ -52077,29 +53339,34 @@ const handoffToAgent = async (input) => {
|
|
|
52077
53339
|
outcome: "ci-suppressed",
|
|
52078
53340
|
diagnosticsCount: input.diagnostics.length
|
|
52079
53341
|
});
|
|
53342
|
+
const choices = [
|
|
53343
|
+
...(await detectLaunchableAgents()).map((agentId) => ({
|
|
53344
|
+
title: getSkillAgentConfig(agentId).displayName,
|
|
53345
|
+
description: `Open ${CLI_AGENT_BINARIES[agentId]} here with the top issues as a prompt`,
|
|
53346
|
+
value: agentId
|
|
53347
|
+
})),
|
|
53348
|
+
{
|
|
53349
|
+
title: "Copy prompt to clipboard",
|
|
53350
|
+
description: "Paste into any agent or chat",
|
|
53351
|
+
value: CLIPBOARD_CHOICE
|
|
53352
|
+
},
|
|
53353
|
+
{
|
|
53354
|
+
title: "Skip",
|
|
53355
|
+
description: "Don't hand off",
|
|
53356
|
+
value: SKIP_CHOICE
|
|
53357
|
+
}
|
|
53358
|
+
];
|
|
53359
|
+
const rememberedTarget = readHandoffTarget();
|
|
53360
|
+
const rememberedChoiceIndex = choices.findIndex((choice) => choice.value === rememberedTarget);
|
|
53361
|
+
const initial = rememberedChoiceIndex >= 0 ? rememberedChoiceIndex : 0;
|
|
52080
53362
|
const { handoffTarget } = await prompts({
|
|
52081
53363
|
type: "select",
|
|
52082
53364
|
name: "handoffTarget",
|
|
52083
53365
|
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
|
|
53366
|
+
choices,
|
|
53367
|
+
initial
|
|
52102
53368
|
}, { onCancel: () => true });
|
|
53369
|
+
if (handoffTarget !== void 0) rememberHandoffTarget(handoffTarget);
|
|
52103
53370
|
let handoffOutcome = "launch";
|
|
52104
53371
|
if (handoffTarget === void 0) handoffOutcome = "cancel";
|
|
52105
53372
|
else if (handoffTarget === SKIP_CHOICE) handoffOutcome = "skip";
|
|
@@ -52107,7 +53374,9 @@ const handoffToAgent = async (input) => {
|
|
|
52107
53374
|
recordCount(METRIC.agentHandoff, 1, {
|
|
52108
53375
|
outcome: handoffOutcome,
|
|
52109
53376
|
agent: handoffOutcome === "launch" ? handoffTarget : void 0,
|
|
52110
|
-
diagnosticsCount: input.diagnostics.length
|
|
53377
|
+
diagnosticsCount: input.diagnostics.length,
|
|
53378
|
+
defaultRemembered: rememberedChoiceIndex >= 0,
|
|
53379
|
+
keptDefault: handoffTarget === choices[initial].value
|
|
52111
53380
|
});
|
|
52112
53381
|
if (handoffTarget === void 0 || handoffTarget === SKIP_CHOICE) return;
|
|
52113
53382
|
const payload = buildHandoffPayload({
|
|
@@ -52138,6 +53407,47 @@ const handoffToAgent = async (input) => {
|
|
|
52138
53407
|
}
|
|
52139
53408
|
};
|
|
52140
53409
|
//#endregion
|
|
53410
|
+
//#region src/cli/utils/migrate-action-pin.ts
|
|
53411
|
+
const WORKFLOWS_DIRECTORY = Path.join(".github", "workflows");
|
|
53412
|
+
const RECOMMENDED_ACTION_REF = "v2";
|
|
53413
|
+
const MUTABLE_ACTION_REF = /(uses:\s*[\w.-]+\/react-doctor@)(?:main|master)\b/g;
|
|
53414
|
+
const isWorkflowFile = (fileName) => /\.ya?ml$/.test(fileName);
|
|
53415
|
+
/**
|
|
53416
|
+
* Rewrites mutable `@main` / `@master` React Doctor GitHub Action references in
|
|
53417
|
+
* the repo's `.github/workflows/*.yml` to the recommended floating major
|
|
53418
|
+
* (`@v2`) — a supply-chain hardening (#299) that also moves the workflow onto
|
|
53419
|
+
* the current (install- and scan-cached) action release. Pinned tags / SHAs are
|
|
53420
|
+
* deliberate and left untouched. Returns the absolute paths of the workflow
|
|
53421
|
+
* files it rewrote — empty when there's nothing to migrate.
|
|
53422
|
+
*/
|
|
53423
|
+
const migrateActionPin = (projectRoot) => {
|
|
53424
|
+
const workflowsDirectory = Path.join(projectRoot, WORKFLOWS_DIRECTORY);
|
|
53425
|
+
let entries;
|
|
53426
|
+
try {
|
|
53427
|
+
entries = NFS.readdirSync(workflowsDirectory, { withFileTypes: true });
|
|
53428
|
+
} catch {
|
|
53429
|
+
return [];
|
|
53430
|
+
}
|
|
53431
|
+
const rewrittenFiles = [];
|
|
53432
|
+
for (const entry of entries) {
|
|
53433
|
+
if (!entry.isFile() || !isWorkflowFile(entry.name)) continue;
|
|
53434
|
+
const workflowPath = Path.join(workflowsDirectory, entry.name);
|
|
53435
|
+
let contents;
|
|
53436
|
+
try {
|
|
53437
|
+
contents = NFS.readFileSync(workflowPath, "utf-8");
|
|
53438
|
+
} catch {
|
|
53439
|
+
continue;
|
|
53440
|
+
}
|
|
53441
|
+
const updated = contents.replace(MUTABLE_ACTION_REF, `$1${RECOMMENDED_ACTION_REF}`);
|
|
53442
|
+
if (updated === contents) continue;
|
|
53443
|
+
try {
|
|
53444
|
+
NFS.writeFileSync(workflowPath, updated);
|
|
53445
|
+
rewrittenFiles.push(workflowPath);
|
|
53446
|
+
} catch {}
|
|
53447
|
+
}
|
|
53448
|
+
return rewrittenFiles;
|
|
53449
|
+
};
|
|
53450
|
+
//#endregion
|
|
52141
53451
|
//#region src/cli/utils/read-object-file.ts
|
|
52142
53452
|
/**
|
|
52143
53453
|
* Reads a JSON / JSONC file as a plain object, or `null` when it is missing,
|
|
@@ -52202,6 +53512,35 @@ export default ${serializeTsObjectLiteral(config)} satisfies ReactDoctorConfig;
|
|
|
52202
53512
|
NFS.rmSync(legacy.legacyFilePath, { force: true });
|
|
52203
53513
|
return targetPath;
|
|
52204
53514
|
};
|
|
53515
|
+
const PROJECT_MIGRATIONS = [{
|
|
53516
|
+
id: "config-json-to-ts",
|
|
53517
|
+
scope: "project",
|
|
53518
|
+
run: ({ projectRoot }) => {
|
|
53519
|
+
if (projectRoot === void 0) return false;
|
|
53520
|
+
const legacyConfig = findLegacyConfig(projectRoot);
|
|
53521
|
+
if (!legacyConfig) return false;
|
|
53522
|
+
const migratedPath = migrateLegacyConfig(legacyConfig);
|
|
53523
|
+
if (!migratedPath) return false;
|
|
53524
|
+
cliLogger.success("Migrated react-doctor.config.json → doctor.config.ts");
|
|
53525
|
+
cliLogger.dim(` Your settings were preserved. Review ${toRelativePath(migratedPath, projectRoot)} and commit it.`);
|
|
53526
|
+
cliLogger.break();
|
|
53527
|
+
return true;
|
|
53528
|
+
}
|
|
53529
|
+
}, {
|
|
53530
|
+
id: "action-pin-main-to-v2",
|
|
53531
|
+
scope: "project",
|
|
53532
|
+
run: ({ projectRoot }) => {
|
|
53533
|
+
if (projectRoot === void 0) return false;
|
|
53534
|
+
const rewrittenFiles = migrateActionPin(projectRoot);
|
|
53535
|
+
if (rewrittenFiles.length === 0) return false;
|
|
53536
|
+
const relativeFiles = rewrittenFiles.map((file) => toRelativePath(file, projectRoot)).join(", ");
|
|
53537
|
+
cliLogger.success(`Pinned the React Doctor action to @v2 in ${relativeFiles}`);
|
|
53538
|
+
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.");
|
|
53539
|
+
cliLogger.break();
|
|
53540
|
+
return true;
|
|
53541
|
+
}
|
|
53542
|
+
}];
|
|
53543
|
+
const runProjectMigrations = (projectRoot, options = {}) => runMigrations(PROJECT_MIGRATIONS, { projectRoot }, options);
|
|
52205
53544
|
//#endregion
|
|
52206
53545
|
//#region src/cli/utils/print-branded-header.ts
|
|
52207
53546
|
/**
|
|
@@ -52421,36 +53760,16 @@ const printMultiProjectSummary = (input) => gen(function* () {
|
|
|
52421
53760
|
});
|
|
52422
53761
|
//#endregion
|
|
52423
53762
|
//#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
|
-
};
|
|
53763
|
+
const SETUP_HINT_GATE = {
|
|
53764
|
+
id: SETUP_HINT_EVENT,
|
|
53765
|
+
scope: "project",
|
|
53766
|
+
fireWhenUnknown: true
|
|
53767
|
+
};
|
|
53768
|
+
const hasDisabledSetupPrompt = (projectRoot, options = {}) => !isGatePending(SETUP_HINT_GATE, { projectRoot }, options);
|
|
53769
|
+
const disableSetupPrompt = (projectRoot, options = {}) => recordGate(SETUP_HINT_GATE, {
|
|
53770
|
+
projectRoot,
|
|
53771
|
+
outcome: "declined"
|
|
53772
|
+
}, options);
|
|
52454
53773
|
const resolveInstallSetupProjectRoot = (options) => {
|
|
52455
53774
|
if (options.scanDirectories.length === 0) return findNearestPackageDirectory(options.scanRoot) ?? options.scanRoot;
|
|
52456
53775
|
const packageDirectories = /* @__PURE__ */ new Set();
|
|
@@ -52601,7 +53920,7 @@ const warnDeprecatedDiff = (flags, userConfig) => {
|
|
|
52601
53920
|
};
|
|
52602
53921
|
const warnDiffUnavailable = (requested, isQuiet) => {
|
|
52603
53922
|
if (isQuiet) return;
|
|
52604
|
-
if (typeof requested.base === "string") cliLogger.warn(`Could not compute diff against "${requested.base}" (
|
|
53923
|
+
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
53924
|
else cliLogger.warn("No feature branch or uncommitted changes detected. Running full scan.");
|
|
52606
53925
|
cliLogger.break();
|
|
52607
53926
|
};
|
|
@@ -52798,6 +54117,7 @@ const resolveRequestedProjects = (requestedNames, workspacePackages, rootDirecto
|
|
|
52798
54117
|
return requestedNames.map((requestedName) => {
|
|
52799
54118
|
const matched = workspacePackages.find((workspacePackage) => workspacePackage.name === requestedName || Path.basename(workspacePackage.directory) === requestedName);
|
|
52800
54119
|
if (matched) return matched.directory;
|
|
54120
|
+
if (Path.basename(rootDirectory) === requestedName) return rootDirectory;
|
|
52801
54121
|
const candidateDirectory = Path.resolve(rootDirectory, requestedName);
|
|
52802
54122
|
if (isDirectory(candidateDirectory)) {
|
|
52803
54123
|
recordCount(METRIC.projectPathSelected);
|
|
@@ -52978,15 +54298,9 @@ const buildChangedFilesDiffInfo = (changedFiles) => ({
|
|
|
52978
54298
|
* are left untouched — the loader still reads the legacy file as a deprecated
|
|
52979
54299
|
* fallback and warns — so a scan never mutates the repo unattended.
|
|
52980
54300
|
*/
|
|
52981
|
-
const maybeMigrateLegacyConfig = (requestedDirectory, { isQuiet, isStaged }) => {
|
|
54301
|
+
const maybeMigrateLegacyConfig = async (requestedDirectory, { isQuiet, isStaged }) => {
|
|
52982
54302
|
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();
|
|
54303
|
+
await runProjectMigrations(requestedDirectory);
|
|
52990
54304
|
};
|
|
52991
54305
|
const inspectAction = async (directory, flags) => {
|
|
52992
54306
|
const isScoreOnly = Boolean(flags.score);
|
|
@@ -53001,7 +54315,7 @@ const inspectAction = async (directory, flags) => {
|
|
|
53001
54315
|
recordCount(METRIC.cliInvoked, 1, { command: "inspect" });
|
|
53002
54316
|
try {
|
|
53003
54317
|
validateModeFlags(flags);
|
|
53004
|
-
maybeMigrateLegacyConfig(requestedDirectory, {
|
|
54318
|
+
await maybeMigrateLegacyConfig(requestedDirectory, {
|
|
53005
54319
|
isQuiet,
|
|
53006
54320
|
isStaged: Boolean(flags.staged)
|
|
53007
54321
|
});
|
|
@@ -53298,6 +54612,10 @@ const installAction = async (options, command) => {
|
|
|
53298
54612
|
projectRoot: options.cwd ?? process.cwd()
|
|
53299
54613
|
});
|
|
53300
54614
|
} catch (error) {
|
|
54615
|
+
if (isExpectedUserError(error)) {
|
|
54616
|
+
handleUserError(error);
|
|
54617
|
+
return;
|
|
54618
|
+
}
|
|
53301
54619
|
handleError(error, { sentryEventId: await reportErrorToSentry(error) });
|
|
53302
54620
|
}
|
|
53303
54621
|
};
|
|
@@ -54305,4 +55623,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
|
|
|
54305
55623
|
export {};
|
|
54306
55624
|
|
|
54307
55625
|
//# sourceMappingURL=cli.js.map
|
|
54308
|
-
//# debugId=
|
|
55626
|
+
//# debugId=1bdeba5a-5ce9-5f4f-b9b0-df7abd4c9c0b
|