react-doctor 0.5.6 → 0.5.7-dev.0b4f4f4
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 +2032 -630
- package/dist/index.d.ts +38 -4
- package/dist/index.js +1219 -323
- package/dist/lsp.js +1238 -347
- package/package.json +6 -6
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="6a2c86b6-c3a8-5e50-8e7b-daef840df200")}catch(e){}}();
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import * as NodeChildProcess from "node:child_process";
|
|
5
5
|
import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
|
|
@@ -9,12 +9,12 @@ 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";
|
|
16
16
|
import * as NodeUrl from "node:url";
|
|
17
|
-
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
18
18
|
import { createJiti } from "jiti";
|
|
19
19
|
import * as Crypto from "node:crypto";
|
|
20
20
|
import crypto, { createHash, randomUUID } from "node:crypto";
|
|
@@ -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
|
|
@@ -22358,7 +22406,8 @@ var Diagnostic = class extends Class("Diagnostic")({
|
|
|
22358
22406
|
category: String$1,
|
|
22359
22407
|
fileContext: optional(Literals(["test", "story"])),
|
|
22360
22408
|
suppressionHint: optional(String$1),
|
|
22361
|
-
relatedLocations: optional(ArraySchema(DiagnosticRelatedLocation))
|
|
22409
|
+
relatedLocations: optional(ArraySchema(DiagnosticRelatedLocation)),
|
|
22410
|
+
fixGroupId: optional(String$1)
|
|
22362
22411
|
}) {};
|
|
22363
22412
|
const JsonReportMode = Literals([
|
|
22364
22413
|
"full",
|
|
@@ -22400,6 +22449,7 @@ var JsonReportProjectEntry = class extends Class("JsonReportProjectEntry")({
|
|
|
22400
22449
|
score: Unknown,
|
|
22401
22450
|
skippedChecks: ArraySchema(String$1),
|
|
22402
22451
|
skippedCheckReasons: optional(Record$1(String$1, String$1)),
|
|
22452
|
+
scannedFileCount: optional(Number$1),
|
|
22403
22453
|
elapsedMilliseconds: Number$1
|
|
22404
22454
|
}) {};
|
|
22405
22455
|
/**
|
|
@@ -28269,6 +28319,14 @@ const runWith = (self, f, onHalt) => suspend$2(() => {
|
|
|
28269
28319
|
return catchDone(flatMap$2(toTransform(self)(done$1(), scope), f), onHalt ? onHalt : succeed$2).pipe(onExit$2((exit) => close(scope, exit)));
|
|
28270
28320
|
});
|
|
28271
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
|
+
/**
|
|
28272
28330
|
* Runs a channel and applies an effect to each output element.
|
|
28273
28331
|
*
|
|
28274
28332
|
* **Example** (Running effects for each output)
|
|
@@ -29657,6 +29715,44 @@ const splitLines = (self) => self.channel.pipe(pipeTo(splitLines$1()), fromChann
|
|
|
29657
29715
|
*/
|
|
29658
29716
|
const ensuring = /* @__PURE__ */ dual(2, (self, finalizer) => fromChannel(ensuring$1(self.channel, finalizer)));
|
|
29659
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
|
+
/**
|
|
29660
29756
|
* Runs a stream with a sink and returns the sink result.
|
|
29661
29757
|
*
|
|
29662
29758
|
* **Example** (Running a stream with a sink)
|
|
@@ -33086,7 +33182,7 @@ const make$8 = /* @__PURE__ */ fnUntraced(function* (options) {
|
|
|
33086
33182
|
const runFork = runForkWith(services);
|
|
33087
33183
|
const exportInterval = max(fromInputUnsafe(options.exportInterval), zero);
|
|
33088
33184
|
let disabledUntil = void 0;
|
|
33089
|
-
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({
|
|
33090
33186
|
schedule: policy,
|
|
33091
33187
|
times: 3
|
|
33092
33188
|
}));
|
|
@@ -35883,16 +35979,23 @@ const isMinifiedSource = (absolutePath) => {
|
|
|
35883
35979
|
if (fileDescriptor !== void 0) NFS.closeSync(fileDescriptor);
|
|
35884
35980
|
}
|
|
35885
35981
|
};
|
|
35886
|
-
const
|
|
35887
|
-
|
|
35982
|
+
const cachedIsLargeMinifiedByPath = /* @__PURE__ */ new Map();
|
|
35983
|
+
const statSourceFileSize = (absolutePath) => {
|
|
35888
35984
|
try {
|
|
35889
|
-
|
|
35985
|
+
return NFS.statSync(absolutePath).size;
|
|
35890
35986
|
} catch {
|
|
35891
|
-
return
|
|
35987
|
+
return null;
|
|
35892
35988
|
}
|
|
35893
|
-
if (sizeBytes < 2e4) return false;
|
|
35894
|
-
return isMinifiedSource(absolutePath);
|
|
35895
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;
|
|
35997
|
+
};
|
|
35998
|
+
const isErrnoException = (error) => error instanceof Error && "code" in error;
|
|
35896
35999
|
const IGNORABLE_READDIR_ERROR_CODES = new Set([
|
|
35897
36000
|
"EACCES",
|
|
35898
36001
|
"EPERM",
|
|
@@ -35902,11 +36005,7 @@ const IGNORABLE_READDIR_ERROR_CODES = new Set([
|
|
|
35902
36005
|
"ELOOP",
|
|
35903
36006
|
"ENAMETOOLONG"
|
|
35904
36007
|
]);
|
|
35905
|
-
const isIgnorableReaddirError = (error) =>
|
|
35906
|
-
if (typeof error !== "object" || error === null) return false;
|
|
35907
|
-
const errorCode = error.code;
|
|
35908
|
-
return typeof errorCode === "string" && IGNORABLE_READDIR_ERROR_CODES.has(errorCode);
|
|
35909
|
-
};
|
|
36008
|
+
const isIgnorableReaddirError = (error) => isErrnoException(error) && typeof error.code === "string" && IGNORABLE_READDIR_ERROR_CODES.has(error.code);
|
|
35910
36009
|
const readDirectoryEntries = (directoryPath) => {
|
|
35911
36010
|
try {
|
|
35912
36011
|
return NFS.readdirSync(directoryPath, { withFileTypes: true });
|
|
@@ -35953,7 +36052,7 @@ const readPackageJsonUncached = (packageJsonPath) => {
|
|
|
35953
36052
|
return JSON.parse(NFS.readFileSync(packageJsonPath, "utf-8"));
|
|
35954
36053
|
} catch (error) {
|
|
35955
36054
|
if (error instanceof SyntaxError) return {};
|
|
35956
|
-
if (error
|
|
36055
|
+
if (isErrnoException(error)) {
|
|
35957
36056
|
const { code } = error;
|
|
35958
36057
|
if (code === "EISDIR" || code === "EACCES" || code === "EPERM" || code === "ENOENT") return {};
|
|
35959
36058
|
}
|
|
@@ -36678,17 +36777,13 @@ const isPackageJsonReactNativeAware = (packageJson) => {
|
|
|
36678
36777
|
return false;
|
|
36679
36778
|
};
|
|
36680
36779
|
const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
|
|
36681
|
-
const
|
|
36682
|
-
const spec = packageJson.dependencies?.
|
|
36780
|
+
const getDependencySpec = (packageJson, packageName) => {
|
|
36781
|
+
const spec = packageJson.dependencies?.[packageName] ?? packageJson.devDependencies?.[packageName] ?? packageJson.peerDependencies?.[packageName] ?? packageJson.optionalDependencies?.[packageName];
|
|
36683
36782
|
return typeof spec === "string" ? spec : null;
|
|
36684
36783
|
};
|
|
36685
|
-
const findExpoVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson,
|
|
36784
|
+
const findExpoVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "expo"));
|
|
36686
36785
|
const SHOPIFY_FLASH_LIST_PACKAGE_NAME = "@shopify/flash-list";
|
|
36687
|
-
const
|
|
36688
|
-
const spec = packageJson.dependencies?.["@shopify/flash-list"] ?? packageJson.devDependencies?.["@shopify/flash-list"] ?? packageJson.peerDependencies?.["@shopify/flash-list"] ?? packageJson.optionalDependencies?.["@shopify/flash-list"];
|
|
36689
|
-
return typeof spec === "string" ? spec : null;
|
|
36690
|
-
};
|
|
36691
|
-
const findShopifyFlashListVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, getShopifyFlashListDependencySpec);
|
|
36786
|
+
const findShopifyFlashListVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, SHOPIFY_FLASH_LIST_PACKAGE_NAME));
|
|
36692
36787
|
const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson, packageName, version }) => {
|
|
36693
36788
|
if (version === null || !isCatalogReference(version)) return version;
|
|
36694
36789
|
const catalogName = extractCatalogName(version);
|
|
@@ -36700,11 +36795,7 @@ const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson,
|
|
|
36700
36795
|
if (!isFile(monorepoPackageJsonPath)) return version;
|
|
36701
36796
|
return resolveCatalogVersion(readPackageJson$1(monorepoPackageJsonPath), packageName, monorepoRoot, catalogName) ?? version;
|
|
36702
36797
|
};
|
|
36703
|
-
const
|
|
36704
|
-
const spec = packageJson.dependencies?.next ?? packageJson.devDependencies?.next ?? packageJson.peerDependencies?.next ?? packageJson.optionalDependencies?.next;
|
|
36705
|
-
return typeof spec === "string" ? spec : null;
|
|
36706
|
-
};
|
|
36707
|
-
const findNextjsVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, getNextjsDependencySpec);
|
|
36798
|
+
const findNextjsVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "next"));
|
|
36708
36799
|
const getPreactVersion = (packageJson) => {
|
|
36709
36800
|
return {
|
|
36710
36801
|
...packageJson.peerDependencies,
|
|
@@ -36771,6 +36862,7 @@ const DOCS_URL = "https://react.doctor/docs";
|
|
|
36771
36862
|
const DOCS_RULES_BASE_URL = `${DOCS_URL}/rules`;
|
|
36772
36863
|
const FETCH_TIMEOUT_MS = 1e4;
|
|
36773
36864
|
const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
|
|
36865
|
+
const PER_WORKER_MEM_BUDGET_BYTES = 1024 * 1024 * 1024;
|
|
36774
36866
|
const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
|
|
36775
36867
|
const ADOPTABLE_LINT_CONFIG_FILENAMES = [".oxlintrc.json", ".eslintrc.json"];
|
|
36776
36868
|
const OXLINT_NODE_REQUIREMENT = "^20.19.0 || >=22.12.0";
|
|
@@ -36793,6 +36885,11 @@ const ES_TARGET_YEAR_BY_NAME = {
|
|
|
36793
36885
|
esnext: 9999
|
|
36794
36886
|
};
|
|
36795
36887
|
/**
|
|
36888
|
+
* tsconfig filenames probed when resolving a project's TypeScript
|
|
36889
|
+
* compiler options — the root config first, then a monorepo base config.
|
|
36890
|
+
*/
|
|
36891
|
+
const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
|
|
36892
|
+
/**
|
|
36796
36893
|
* Project-config files that `StagedFiles.materialize` copies into
|
|
36797
36894
|
* the temp directory alongside staged sources so oxlint resolves
|
|
36798
36895
|
* `tsconfig` / `package.json` / lint configs the same way it would
|
|
@@ -36842,7 +36939,16 @@ const CANONICAL_DISCORD_URL = "https://react.doctor/discord";
|
|
|
36842
36939
|
const SKILL_NAME = "react-doctor";
|
|
36843
36940
|
const OXLINT_OUTPUT_MAX_BYTES = 50 * 1024 * 1024;
|
|
36844
36941
|
const OXLINT_SPAWN_TIMEOUT_MS = 6e4;
|
|
36942
|
+
const NODE_COMPILE_CACHE_DIR_NAME = "node-compile-cache";
|
|
36943
|
+
const DEAD_CODE_WORKER_TIMEOUT_MS = 12e4;
|
|
36944
|
+
const OXLINT_SPLIT_TOTAL_BUDGET_MS = 18e4;
|
|
36945
|
+
const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
|
|
36946
|
+
const LINT_PHASE_TIMEOUT_MS = 3e5;
|
|
36947
|
+
const SCAN_TOTAL_DEADLINE_MS = 9e5;
|
|
36845
36948
|
const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
|
|
36949
|
+
const DEAD_CODE_TIMEOUT_CEILING_MS = 6e5;
|
|
36950
|
+
const DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS = 3e4;
|
|
36951
|
+
const DEAD_CODE_OVERLAP_PARSE_SHARE = .4;
|
|
36846
36952
|
const RECOMMENDED_PNPM_MINIMUM_RELEASE_AGE_MINUTES = 10080;
|
|
36847
36953
|
const REACT_SERVER_DOM_PACKAGES = [
|
|
36848
36954
|
"react-server-dom-webpack",
|
|
@@ -36865,14 +36971,25 @@ const APP_ONLY_RULE_KEYS = new Set([
|
|
|
36865
36971
|
]);
|
|
36866
36972
|
const COMPILER_CLEANUP_BUCKET = "compiler-cleanup";
|
|
36867
36973
|
const COMPILER_CLEANUP_RULE_KEYS = new Set(["react-doctor/react-compiler-no-manual-memoization"]);
|
|
36974
|
+
const ROOT_CAUSE_GROUPABLE_RULE_KEYS = new Set([
|
|
36975
|
+
"react-doctor/no-derived-state",
|
|
36976
|
+
"react-doctor/no-derived-state-effect",
|
|
36977
|
+
"react-doctor/no-derived-useState",
|
|
36978
|
+
"react-doctor/no-adjust-state-on-prop-change",
|
|
36979
|
+
"react-doctor/no-reset-all-state-on-prop-change"
|
|
36980
|
+
]);
|
|
36868
36981
|
const MAX_GLOB_PATTERN_LENGTH_CHARS = 1024;
|
|
36869
36982
|
const CONFIG_CACHE_TTL_MS = 300 * 1e3;
|
|
36870
36983
|
const SOCKET_FREE_PURL_API_BASE = "https://firewall-api.socket.dev/purl";
|
|
36871
36984
|
const SOCKET_PACKAGE_PAGE_BASE = "https://socket.dev/npm/package";
|
|
36872
36985
|
const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
|
|
36986
|
+
const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
|
|
36987
|
+
const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
|
|
36873
36988
|
const SUPPLY_CHAIN_PLUGIN = "socket";
|
|
36874
36989
|
const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
|
|
36875
36990
|
const SUPPLY_CHAIN_CATEGORY = "Security";
|
|
36991
|
+
const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
|
|
36992
|
+
const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
|
|
36876
36993
|
const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
|
|
36877
36994
|
const TSCONFIG_FILENAME = "tsconfig.json";
|
|
36878
36995
|
const isRelativeExtendsValue = (extendsValue) => extendsValue.startsWith("./") || extendsValue.startsWith("../") || Path.isAbsolute(extendsValue);
|
|
@@ -37314,6 +37431,7 @@ const isTailwindAtLeast = (detected, required) => {
|
|
|
37314
37431
|
if (detected.major !== required.major) return detected.major > required.major;
|
|
37315
37432
|
return detected.minor >= required.minor;
|
|
37316
37433
|
};
|
|
37434
|
+
const messageFromUnknown = (error) => error instanceof Error ? error.message : String(error);
|
|
37317
37435
|
var InvalidGlobPatternError = class extends Error {
|
|
37318
37436
|
pattern;
|
|
37319
37437
|
reason;
|
|
@@ -37342,7 +37460,7 @@ const compileGlobPattern = (rawPattern) => {
|
|
|
37342
37460
|
try {
|
|
37343
37461
|
return import_picomatch.default.makeRe(normalizeGlobPattern(rawPattern), PICOMATCH_OPTIONS);
|
|
37344
37462
|
} catch (caughtError) {
|
|
37345
|
-
throw new InvalidGlobPatternError(rawPattern,
|
|
37463
|
+
throw new InvalidGlobPatternError(rawPattern, messageFromUnknown(caughtError));
|
|
37346
37464
|
}
|
|
37347
37465
|
};
|
|
37348
37466
|
const compileGlobPatternsLenient = (patterns, onInvalid) => {
|
|
@@ -37438,115 +37556,6 @@ const buildRuleSeverityControls = (config) => {
|
|
|
37438
37556
|
...config.buckets !== void 0 ? { buckets: config.buckets } : {}
|
|
37439
37557
|
};
|
|
37440
37558
|
};
|
|
37441
|
-
const JSX_OPENER_TAG_PATTERN = /<[A-Za-z][\w.]*/g;
|
|
37442
|
-
const JSX_TAG_NAME_FOLLOW = /[A-Za-z]/;
|
|
37443
|
-
const isOpenerMatchInsideLineComment = (line, openerCharIndex) => {
|
|
37444
|
-
let stringDelimiter = null;
|
|
37445
|
-
for (let charIndex = 0; charIndex < openerCharIndex; charIndex++) {
|
|
37446
|
-
const character = line[charIndex];
|
|
37447
|
-
if (stringDelimiter !== null) {
|
|
37448
|
-
if (character === "\\") {
|
|
37449
|
-
charIndex++;
|
|
37450
|
-
continue;
|
|
37451
|
-
}
|
|
37452
|
-
if (character === stringDelimiter) stringDelimiter = null;
|
|
37453
|
-
continue;
|
|
37454
|
-
}
|
|
37455
|
-
if (character === "\"" || character === "'" || character === "`") {
|
|
37456
|
-
stringDelimiter = character;
|
|
37457
|
-
continue;
|
|
37458
|
-
}
|
|
37459
|
-
if (character === "/" && line[charIndex + 1] === "/") return true;
|
|
37460
|
-
}
|
|
37461
|
-
return false;
|
|
37462
|
-
};
|
|
37463
|
-
const findOpenerTagOnLine = (line) => {
|
|
37464
|
-
for (const match of line.matchAll(JSX_OPENER_TAG_PATTERN)) {
|
|
37465
|
-
if (match.index === void 0) continue;
|
|
37466
|
-
if (!isOpenerMatchInsideLineComment(line, match.index)) return { startCharIndex: match.index + match[0].length };
|
|
37467
|
-
}
|
|
37468
|
-
return null;
|
|
37469
|
-
};
|
|
37470
|
-
const findJsxOpenerSpan = (lines, openerLineIndex) => {
|
|
37471
|
-
const openerLine = lines[openerLineIndex];
|
|
37472
|
-
if (openerLine === void 0) return null;
|
|
37473
|
-
const opener = findOpenerTagOnLine(openerLine);
|
|
37474
|
-
if (!opener) return null;
|
|
37475
|
-
const lookaheadLimit = Math.min(lines.length, openerLineIndex + 32);
|
|
37476
|
-
let braceDepth = 0;
|
|
37477
|
-
let innerAngleDepth = 0;
|
|
37478
|
-
let stringDelimiter = null;
|
|
37479
|
-
for (let lineIndex = openerLineIndex; lineIndex < lookaheadLimit; lineIndex++) {
|
|
37480
|
-
const currentLine = lines[lineIndex];
|
|
37481
|
-
const startCharForLine = lineIndex === openerLineIndex ? opener.startCharIndex : 0;
|
|
37482
|
-
for (let charIndex = startCharForLine; charIndex < currentLine.length; charIndex++) {
|
|
37483
|
-
const character = currentLine[charIndex];
|
|
37484
|
-
if (stringDelimiter !== null) {
|
|
37485
|
-
if (character === "\\") {
|
|
37486
|
-
charIndex++;
|
|
37487
|
-
continue;
|
|
37488
|
-
}
|
|
37489
|
-
if (character === stringDelimiter) stringDelimiter = null;
|
|
37490
|
-
continue;
|
|
37491
|
-
}
|
|
37492
|
-
if (character === "\"" || character === "'" || character === "`") {
|
|
37493
|
-
stringDelimiter = character;
|
|
37494
|
-
continue;
|
|
37495
|
-
}
|
|
37496
|
-
if (character === "{") {
|
|
37497
|
-
braceDepth++;
|
|
37498
|
-
continue;
|
|
37499
|
-
}
|
|
37500
|
-
if (character === "}") {
|
|
37501
|
-
braceDepth--;
|
|
37502
|
-
continue;
|
|
37503
|
-
}
|
|
37504
|
-
if (braceDepth !== 0) continue;
|
|
37505
|
-
if (character === "<") {
|
|
37506
|
-
const followCharacter = currentLine[charIndex + 1];
|
|
37507
|
-
if (followCharacter !== void 0 && JSX_TAG_NAME_FOLLOW.test(followCharacter)) innerAngleDepth++;
|
|
37508
|
-
continue;
|
|
37509
|
-
}
|
|
37510
|
-
if (character !== ">") continue;
|
|
37511
|
-
const previousCharacter = currentLine[charIndex - 1];
|
|
37512
|
-
const nextCharacter = currentLine[charIndex + 1];
|
|
37513
|
-
if (previousCharacter === "=" || nextCharacter === "=") continue;
|
|
37514
|
-
if (innerAngleDepth > 0) {
|
|
37515
|
-
innerAngleDepth--;
|
|
37516
|
-
continue;
|
|
37517
|
-
}
|
|
37518
|
-
return lineIndex;
|
|
37519
|
-
}
|
|
37520
|
-
}
|
|
37521
|
-
return null;
|
|
37522
|
-
};
|
|
37523
|
-
const findEnclosingMultilineJsxOpenerStart = (lines, diagnosticLineIndex) => {
|
|
37524
|
-
for (let candidateIndex = diagnosticLineIndex - 1; candidateIndex >= 0 && diagnosticLineIndex - candidateIndex <= 32; candidateIndex--) {
|
|
37525
|
-
const openerCloseIndex = findJsxOpenerSpan(lines, candidateIndex);
|
|
37526
|
-
if (openerCloseIndex !== null && openerCloseIndex >= diagnosticLineIndex) return candidateIndex;
|
|
37527
|
-
}
|
|
37528
|
-
return null;
|
|
37529
|
-
};
|
|
37530
|
-
const DISABLE_NEXT_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-next-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
|
|
37531
|
-
const findStackedDisableCommentsAbove = (lines, anchorIndex) => {
|
|
37532
|
-
const collected = [];
|
|
37533
|
-
let isStillInChain = true;
|
|
37534
|
-
for (let candidateIndex = anchorIndex - 1; candidateIndex >= 0 && anchorIndex - candidateIndex <= 10; candidateIndex--) {
|
|
37535
|
-
const candidateLine = lines[candidateIndex];
|
|
37536
|
-
if (candidateLine === void 0) break;
|
|
37537
|
-
const match = candidateLine.match(DISABLE_NEXT_LINE_PATTERN);
|
|
37538
|
-
if (match) {
|
|
37539
|
-
collected.push({
|
|
37540
|
-
commentLineIndex: candidateIndex,
|
|
37541
|
-
ruleList: match[1],
|
|
37542
|
-
isInChain: isStillInChain
|
|
37543
|
-
});
|
|
37544
|
-
continue;
|
|
37545
|
-
}
|
|
37546
|
-
isStillInChain = false;
|
|
37547
|
-
}
|
|
37548
|
-
return collected;
|
|
37549
|
-
};
|
|
37550
37559
|
const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
|
|
37551
37560
|
"effect/no-adjust-state-on-prop-change": "react-doctor/no-adjust-state-on-prop-change",
|
|
37552
37561
|
"effect/no-chain-state-updates": "react-doctor/no-chain-state-updates",
|
|
@@ -37671,7 +37680,13 @@ for (const [legacyRuleKey, nativeRuleKey] of Object.entries(LEGACY_RULE_KEY_TO_N
|
|
|
37671
37680
|
}
|
|
37672
37681
|
const getLegacyRuleKeysForNative = (ruleKey) => NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(ruleKey) ?? [];
|
|
37673
37682
|
const canonicalizeRuleKey = (ruleKey) => LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey] ?? ruleKey;
|
|
37674
|
-
const
|
|
37683
|
+
const isReactDoctorShortIdOf = (bareRuleKey, qualifiedRuleKey) => !bareRuleKey.includes("/") && qualifiedRuleKey === `react-doctor/${bareRuleKey}`;
|
|
37684
|
+
const isSameRuleKey = (candidateRuleKey, targetRuleKey) => {
|
|
37685
|
+
const canonicalCandidate = canonicalizeRuleKey(candidateRuleKey);
|
|
37686
|
+
const canonicalTarget = canonicalizeRuleKey(targetRuleKey);
|
|
37687
|
+
if (canonicalCandidate === canonicalTarget) return true;
|
|
37688
|
+
return isReactDoctorShortIdOf(canonicalCandidate, canonicalTarget) || isReactDoctorShortIdOf(canonicalTarget, canonicalCandidate);
|
|
37689
|
+
};
|
|
37675
37690
|
const getEquivalentRuleKeys = (ruleKey) => {
|
|
37676
37691
|
const nativeRuleKey = canonicalizeRuleKey(ruleKey);
|
|
37677
37692
|
return [nativeRuleKey, ...getLegacyRuleKeysForNative(nativeRuleKey)];
|
|
@@ -37681,12 +37696,182 @@ const stripDescriptionTail = (ruleList) => {
|
|
|
37681
37696
|
if (!descriptionMatch || descriptionMatch.index === void 0) return ruleList;
|
|
37682
37697
|
return ruleList.slice(0, descriptionMatch.index);
|
|
37683
37698
|
};
|
|
37684
|
-
const
|
|
37699
|
+
const tokenizeRuleList = (ruleList) => {
|
|
37685
37700
|
const trimmed = ruleList?.trim();
|
|
37686
|
-
if (!trimmed) return
|
|
37701
|
+
if (!trimmed) return [];
|
|
37687
37702
|
const ruleSection = stripDescriptionTail(trimmed).trim();
|
|
37688
|
-
if (!ruleSection) return
|
|
37689
|
-
return ruleSection.split(/[,\s]+/).
|
|
37703
|
+
if (!ruleSection) return [];
|
|
37704
|
+
return ruleSection.split(/[,\s]+/).map((token) => token.trim()).filter(Boolean);
|
|
37705
|
+
};
|
|
37706
|
+
const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
|
|
37707
|
+
const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
|
|
37708
|
+
const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
|
|
37709
|
+
const buildHint = (tool, token, ruleId) => `oxlint matches plugin rules only by their full name, so \`${token}\` in your ${tool}-disable comment does not silence \`${ruleId}\` — change it to \`${ruleId}\`.`;
|
|
37710
|
+
const tokenMisnamesRule = (token, ruleId) => token !== ruleId && isSameRuleKey(token, ruleId);
|
|
37711
|
+
const detectInlineNearMiss = (lines, diagnosticLineIndex, ruleId) => {
|
|
37712
|
+
const candidates = [{
|
|
37713
|
+
line: lines[diagnosticLineIndex],
|
|
37714
|
+
requiredScope: "line"
|
|
37715
|
+
}, {
|
|
37716
|
+
line: lines[diagnosticLineIndex - 1],
|
|
37717
|
+
requiredScope: "next-line"
|
|
37718
|
+
}];
|
|
37719
|
+
for (const { line, requiredScope } of candidates) {
|
|
37720
|
+
const match = line?.match(FOREIGN_INLINE_DISABLE_PATTERN);
|
|
37721
|
+
if (!match) continue;
|
|
37722
|
+
const [, tool, scope, ruleList] = match;
|
|
37723
|
+
if (scope !== requiredScope) continue;
|
|
37724
|
+
const tokens = tokenizeRuleList(ruleList);
|
|
37725
|
+
if (tokens.includes(ruleId)) continue;
|
|
37726
|
+
for (const token of tokens) if (tokenMisnamesRule(token, ruleId)) return buildHint(tool, token, ruleId);
|
|
37727
|
+
}
|
|
37728
|
+
return null;
|
|
37729
|
+
};
|
|
37730
|
+
const detectBlockNearMiss = (lines, diagnosticLineIndex, ruleId) => {
|
|
37731
|
+
let openMisname = null;
|
|
37732
|
+
const lastLineIndex = Math.min(diagnosticLineIndex, lines.length - 1);
|
|
37733
|
+
for (let lineIndex = 0; lineIndex <= lastLineIndex; lineIndex++) {
|
|
37734
|
+
const line = lines[lineIndex];
|
|
37735
|
+
if (line === void 0 || !line.includes("-disable") && !line.includes("-enable")) continue;
|
|
37736
|
+
const disableMatch = line.match(FOREIGN_BLOCK_DISABLE_PATTERN);
|
|
37737
|
+
if (disableMatch) {
|
|
37738
|
+
const [, tool, ruleList] = disableMatch;
|
|
37739
|
+
const tokens = tokenizeRuleList(ruleList);
|
|
37740
|
+
if (tokens.includes(ruleId)) openMisname = null;
|
|
37741
|
+
else {
|
|
37742
|
+
const misnamed = tokens.find((token) => tokenMisnamesRule(token, ruleId));
|
|
37743
|
+
if (misnamed) openMisname = {
|
|
37744
|
+
tool,
|
|
37745
|
+
token: misnamed
|
|
37746
|
+
};
|
|
37747
|
+
}
|
|
37748
|
+
continue;
|
|
37749
|
+
}
|
|
37750
|
+
const enableMatch = line.match(FOREIGN_BLOCK_ENABLE_PATTERN);
|
|
37751
|
+
if (enableMatch) {
|
|
37752
|
+
const enabledRules = tokenizeRuleList(enableMatch[1]);
|
|
37753
|
+
if (enabledRules.length === 0 || enabledRules.some((rule) => isSameRuleKey(rule, ruleId))) openMisname = null;
|
|
37754
|
+
}
|
|
37755
|
+
}
|
|
37756
|
+
return openMisname ? buildHint(openMisname.tool, openMisname.token, ruleId) : null;
|
|
37757
|
+
};
|
|
37758
|
+
const detectForeignDisableNearMiss = (lines, diagnosticLineIndex, ruleId) => {
|
|
37759
|
+
if (!ruleId.startsWith("react-doctor/")) return null;
|
|
37760
|
+
return detectInlineNearMiss(lines, diagnosticLineIndex, ruleId) ?? detectBlockNearMiss(lines, diagnosticLineIndex, ruleId);
|
|
37761
|
+
};
|
|
37762
|
+
const JSX_OPENER_TAG_PATTERN = /<[A-Za-z][\w.]*/g;
|
|
37763
|
+
const JSX_TAG_NAME_FOLLOW = /[A-Za-z]/;
|
|
37764
|
+
const isOpenerMatchInsideLineComment = (line, openerCharIndex) => {
|
|
37765
|
+
let stringDelimiter = null;
|
|
37766
|
+
for (let charIndex = 0; charIndex < openerCharIndex; charIndex++) {
|
|
37767
|
+
const character = line[charIndex];
|
|
37768
|
+
if (stringDelimiter !== null) {
|
|
37769
|
+
if (character === "\\") {
|
|
37770
|
+
charIndex++;
|
|
37771
|
+
continue;
|
|
37772
|
+
}
|
|
37773
|
+
if (character === stringDelimiter) stringDelimiter = null;
|
|
37774
|
+
continue;
|
|
37775
|
+
}
|
|
37776
|
+
if (character === "\"" || character === "'" || character === "`") {
|
|
37777
|
+
stringDelimiter = character;
|
|
37778
|
+
continue;
|
|
37779
|
+
}
|
|
37780
|
+
if (character === "/" && line[charIndex + 1] === "/") return true;
|
|
37781
|
+
}
|
|
37782
|
+
return false;
|
|
37783
|
+
};
|
|
37784
|
+
const findOpenerTagOnLine = (line) => {
|
|
37785
|
+
for (const match of line.matchAll(JSX_OPENER_TAG_PATTERN)) {
|
|
37786
|
+
if (match.index === void 0) continue;
|
|
37787
|
+
if (!isOpenerMatchInsideLineComment(line, match.index)) return { startCharIndex: match.index + match[0].length };
|
|
37788
|
+
}
|
|
37789
|
+
return null;
|
|
37790
|
+
};
|
|
37791
|
+
const findJsxOpenerSpan = (lines, openerLineIndex) => {
|
|
37792
|
+
const openerLine = lines[openerLineIndex];
|
|
37793
|
+
if (openerLine === void 0) return null;
|
|
37794
|
+
const opener = findOpenerTagOnLine(openerLine);
|
|
37795
|
+
if (!opener) return null;
|
|
37796
|
+
const lookaheadLimit = Math.min(lines.length, openerLineIndex + 32);
|
|
37797
|
+
let braceDepth = 0;
|
|
37798
|
+
let innerAngleDepth = 0;
|
|
37799
|
+
let stringDelimiter = null;
|
|
37800
|
+
for (let lineIndex = openerLineIndex; lineIndex < lookaheadLimit; lineIndex++) {
|
|
37801
|
+
const currentLine = lines[lineIndex];
|
|
37802
|
+
const startCharForLine = lineIndex === openerLineIndex ? opener.startCharIndex : 0;
|
|
37803
|
+
for (let charIndex = startCharForLine; charIndex < currentLine.length; charIndex++) {
|
|
37804
|
+
const character = currentLine[charIndex];
|
|
37805
|
+
if (stringDelimiter !== null) {
|
|
37806
|
+
if (character === "\\") {
|
|
37807
|
+
charIndex++;
|
|
37808
|
+
continue;
|
|
37809
|
+
}
|
|
37810
|
+
if (character === stringDelimiter) stringDelimiter = null;
|
|
37811
|
+
continue;
|
|
37812
|
+
}
|
|
37813
|
+
if (character === "\"" || character === "'" || character === "`") {
|
|
37814
|
+
stringDelimiter = character;
|
|
37815
|
+
continue;
|
|
37816
|
+
}
|
|
37817
|
+
if (character === "{") {
|
|
37818
|
+
braceDepth++;
|
|
37819
|
+
continue;
|
|
37820
|
+
}
|
|
37821
|
+
if (character === "}") {
|
|
37822
|
+
braceDepth--;
|
|
37823
|
+
continue;
|
|
37824
|
+
}
|
|
37825
|
+
if (braceDepth !== 0) continue;
|
|
37826
|
+
if (character === "<") {
|
|
37827
|
+
const followCharacter = currentLine[charIndex + 1];
|
|
37828
|
+
if (followCharacter !== void 0 && JSX_TAG_NAME_FOLLOW.test(followCharacter)) innerAngleDepth++;
|
|
37829
|
+
continue;
|
|
37830
|
+
}
|
|
37831
|
+
if (character !== ">") continue;
|
|
37832
|
+
const previousCharacter = currentLine[charIndex - 1];
|
|
37833
|
+
const nextCharacter = currentLine[charIndex + 1];
|
|
37834
|
+
if (previousCharacter === "=" || nextCharacter === "=") continue;
|
|
37835
|
+
if (innerAngleDepth > 0) {
|
|
37836
|
+
innerAngleDepth--;
|
|
37837
|
+
continue;
|
|
37838
|
+
}
|
|
37839
|
+
return lineIndex;
|
|
37840
|
+
}
|
|
37841
|
+
}
|
|
37842
|
+
return null;
|
|
37843
|
+
};
|
|
37844
|
+
const findEnclosingMultilineJsxOpenerStart = (lines, diagnosticLineIndex) => {
|
|
37845
|
+
for (let candidateIndex = diagnosticLineIndex - 1; candidateIndex >= 0 && diagnosticLineIndex - candidateIndex <= 32; candidateIndex--) {
|
|
37846
|
+
const openerCloseIndex = findJsxOpenerSpan(lines, candidateIndex);
|
|
37847
|
+
if (openerCloseIndex !== null && openerCloseIndex >= diagnosticLineIndex) return candidateIndex;
|
|
37848
|
+
}
|
|
37849
|
+
return null;
|
|
37850
|
+
};
|
|
37851
|
+
const DISABLE_NEXT_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-next-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
|
|
37852
|
+
const findStackedDisableCommentsAbove = (lines, anchorIndex) => {
|
|
37853
|
+
const collected = [];
|
|
37854
|
+
let isStillInChain = true;
|
|
37855
|
+
for (let candidateIndex = anchorIndex - 1; candidateIndex >= 0 && anchorIndex - candidateIndex <= 10; candidateIndex--) {
|
|
37856
|
+
const candidateLine = lines[candidateIndex];
|
|
37857
|
+
if (candidateLine === void 0) break;
|
|
37858
|
+
const match = candidateLine.match(DISABLE_NEXT_LINE_PATTERN);
|
|
37859
|
+
if (match) {
|
|
37860
|
+
collected.push({
|
|
37861
|
+
commentLineIndex: candidateIndex,
|
|
37862
|
+
ruleList: match[1],
|
|
37863
|
+
isInChain: isStillInChain
|
|
37864
|
+
});
|
|
37865
|
+
continue;
|
|
37866
|
+
}
|
|
37867
|
+
isStillInChain = false;
|
|
37868
|
+
}
|
|
37869
|
+
return collected;
|
|
37870
|
+
};
|
|
37871
|
+
const isRuleListedInComment = (ruleList, ruleId) => {
|
|
37872
|
+
const tokens = tokenizeRuleList(ruleList);
|
|
37873
|
+
if (tokens.length === 0) return true;
|
|
37874
|
+
return tokens.some((token) => isSameRuleKey(token, ruleId));
|
|
37690
37875
|
};
|
|
37691
37876
|
const DISABLE_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
|
|
37692
37877
|
const formatLineGap = (gapLineCount) => `${gapLineCount} line${gapLineCount === 1 ? "" : "s"}`;
|
|
@@ -37730,7 +37915,7 @@ const evaluateSuppression = (lines, diagnosticLineIndex, ruleId) => {
|
|
|
37730
37915
|
};
|
|
37731
37916
|
return {
|
|
37732
37917
|
isSuppressed: false,
|
|
37733
|
-
nearMissHint: classifyFromComments([directComments, openerComments], diagnosticLineIndex, ruleId)
|
|
37918
|
+
nearMissHint: classifyFromComments([directComments, openerComments], diagnosticLineIndex, ruleId) ?? detectForeignDisableNearMiss(lines, diagnosticLineIndex, ruleId)
|
|
37734
37919
|
};
|
|
37735
37920
|
};
|
|
37736
37921
|
/**
|
|
@@ -38156,6 +38341,11 @@ var OxlintBatchExceeded = class extends TaggedErrorClass()("OxlintBatchExceeded"
|
|
|
38156
38341
|
}
|
|
38157
38342
|
}
|
|
38158
38343
|
};
|
|
38344
|
+
var ScanDeadlineExceeded = class extends TaggedErrorClass()("ScanDeadlineExceeded", { detail: String$1 }) {
|
|
38345
|
+
get message() {
|
|
38346
|
+
return `Scan exceeded its overall time budget: ${this.detail}`;
|
|
38347
|
+
}
|
|
38348
|
+
};
|
|
38159
38349
|
var OxlintSpawnFailed = class extends TaggedErrorClass()("OxlintSpawnFailed", { cause: Unknown }) {
|
|
38160
38350
|
get message() {
|
|
38161
38351
|
return `Failed to run oxlint: ${pretty(fail$6(this.cause))}`;
|
|
@@ -38219,6 +38409,7 @@ var GitBaseBranchInvalid = class extends TaggedErrorClass()("GitBaseBranchInvali
|
|
|
38219
38409
|
const ReactDoctorErrorReason = Union([
|
|
38220
38410
|
OxlintUnavailable,
|
|
38221
38411
|
OxlintBatchExceeded,
|
|
38412
|
+
ScanDeadlineExceeded,
|
|
38222
38413
|
OxlintSpawnFailed,
|
|
38223
38414
|
OxlintOutputUnparseable,
|
|
38224
38415
|
ConfigParseFailed,
|
|
@@ -38291,15 +38482,105 @@ const layerOtlp = unwrap$3(gen(function* () {
|
|
|
38291
38482
|
}).pipe(provide$2(layer$9));
|
|
38292
38483
|
}).pipe(orDie));
|
|
38293
38484
|
/**
|
|
38294
|
-
*
|
|
38295
|
-
* `
|
|
38296
|
-
|
|
38485
|
+
* Read a positive-millisecond timeout from an env var, falling back to
|
|
38486
|
+
* `defaultMs` when the var is unset, non-finite, or not strictly positive.
|
|
38487
|
+
*/
|
|
38488
|
+
const readPositiveEnvMs = (envVarName, defaultMs) => {
|
|
38489
|
+
const rawValue = process.env[envVarName];
|
|
38490
|
+
if (rawValue === void 0) return defaultMs;
|
|
38491
|
+
const parsedValue = Number(rawValue);
|
|
38492
|
+
if (!Number.isFinite(parsedValue) || parsedValue <= 0) return defaultMs;
|
|
38493
|
+
return parsedValue;
|
|
38494
|
+
};
|
|
38495
|
+
const CGROUP_V2_MEMORY_MAX_PATH = "/sys/fs/cgroup/memory.max";
|
|
38496
|
+
const CGROUP_V1_MEMORY_LIMIT_PATH = "/sys/fs/cgroup/memory/memory.limit_in_bytes";
|
|
38497
|
+
const CGROUP_UNLIMITED_SENTINEL_BYTES = Number.MAX_SAFE_INTEGER;
|
|
38498
|
+
/**
|
|
38499
|
+
* Parses one raw cgroup memory-limit file value into a positive byte count, or
|
|
38500
|
+
* `undefined` when it represents "no limit" (the v2 `"max"` literal, an empty
|
|
38501
|
+
* read, a non-positive / non-finite value, or v1's near-2^63 unlimited
|
|
38502
|
+
* sentinel). Pure and exported so the classification is unit-testable without
|
|
38503
|
+
* touching the filesystem.
|
|
38504
|
+
*/
|
|
38505
|
+
const parseCgroupMemoryLimitBytes = (raw) => {
|
|
38506
|
+
if (raw === void 0) return void 0;
|
|
38507
|
+
const trimmed = raw.trim();
|
|
38508
|
+
if (trimmed === "" || trimmed === "max") return void 0;
|
|
38509
|
+
const parsed = Number(trimmed);
|
|
38510
|
+
if (!Number.isFinite(parsed) || parsed <= 0 || parsed >= CGROUP_UNLIMITED_SENTINEL_BYTES) return;
|
|
38511
|
+
return parsed;
|
|
38512
|
+
};
|
|
38513
|
+
const CGROUP_MEMORY_LIMIT_PATHS = [CGROUP_V2_MEMORY_MAX_PATH, CGROUP_V1_MEMORY_LIMIT_PATH];
|
|
38514
|
+
/**
|
|
38515
|
+
* Reads this process's cgroup memory limit in bytes from the first candidate
|
|
38516
|
+
* path that yields a real limit, or `undefined` when none does — no cgroup, no
|
|
38517
|
+
* limit, or the files are unreadable (e.g. macOS / Windows dev machines).
|
|
38518
|
+
* `os.totalmem()` reports the HOST total and ignores cgroup memory limits, so a
|
|
38519
|
+
* memory-constrained container over-reports total memory; `resolveAutoScan-
|
|
38520
|
+
* Concurrency` takes `min(totalmem, this)` to honor the limit.
|
|
38521
|
+
*
|
|
38522
|
+
* The cgroup v2 read is the mount-root `memory.max`, which IS the container's
|
|
38523
|
+
* limit under the standard cgroup-namespace setup CI runners use (the
|
|
38524
|
+
* container's own cgroup is the root of its namespaced view). A process in a
|
|
38525
|
+
* non-namespaced nested/delegated cgroup whose root reads `"max"` is not
|
|
38526
|
+
* detected here and falls back to the host total; the EAGAIN/ENOMEM serial
|
|
38527
|
+
* replay in `spawnLintBatches` remains the runtime backstop for that case.
|
|
38528
|
+
*
|
|
38529
|
+
* `candidatePaths` is injectable so tests exercise the v2-wins-over-v1
|
|
38530
|
+
* precedence, the skip-unreadable fallback, and the all-missing case without a
|
|
38531
|
+
* real `/sys/fs/cgroup`.
|
|
38532
|
+
*/
|
|
38533
|
+
const readCgroupMemoryLimitBytes = (candidatePaths = CGROUP_MEMORY_LIMIT_PATHS) => {
|
|
38534
|
+
for (const limitPath of candidatePaths) {
|
|
38535
|
+
let raw;
|
|
38536
|
+
try {
|
|
38537
|
+
raw = fs.readFileSync(limitPath, "utf8");
|
|
38538
|
+
} catch {
|
|
38539
|
+
continue;
|
|
38540
|
+
}
|
|
38541
|
+
const limitBytes = parseCgroupMemoryLimitBytes(raw);
|
|
38542
|
+
if (limitBytes !== void 0) return limitBytes;
|
|
38543
|
+
}
|
|
38544
|
+
};
|
|
38545
|
+
/**
|
|
38546
|
+
* Clamps a requested lint worker count to `[MIN_SCAN_CONCURRENCY,
|
|
38547
|
+
* HARD_MAX_SCAN_CONCURRENCY]` as a finite integer. This is the explicit-pin and
|
|
38548
|
+
* spawn-boundary clamp — the memory-and-core-budgeted auto count comes from
|
|
38549
|
+
* `resolveAutoScanConcurrency`. Out-of-range or non-finite requests degrade to
|
|
38297
38550
|
* `MIN_SCAN_CONCURRENCY` rather than oversubscribing or running zero workers.
|
|
38298
38551
|
*/
|
|
38299
38552
|
const resolveScanConcurrency = (requested) => {
|
|
38300
|
-
|
|
38301
|
-
|
|
38302
|
-
|
|
38553
|
+
if (!Number.isFinite(requested) || requested < 1) return 1;
|
|
38554
|
+
return Math.min(Math.floor(requested), 32);
|
|
38555
|
+
};
|
|
38556
|
+
const readSystemFacts = () => ({
|
|
38557
|
+
availableCores: os.availableParallelism(),
|
|
38558
|
+
totalMemoryBytes: os.totalmem(),
|
|
38559
|
+
cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
|
|
38560
|
+
});
|
|
38561
|
+
/**
|
|
38562
|
+
* Auto lint-worker count: the smaller of the (cgroup-CPU-aware) core count and
|
|
38563
|
+
* the number of `PER_WORKER_MEM_BUDGET_BYTES` workers that fit in available
|
|
38564
|
+
* memory, then clamped to `[MIN, HARD_MAX]` by `resolveScanConcurrency`.
|
|
38565
|
+
*
|
|
38566
|
+
* `os.availableParallelism()` already respects cgroup CPU quotas, so the core
|
|
38567
|
+
* term needs no help. Available memory is `os.totalmem()` floored by the cgroup
|
|
38568
|
+
* memory limit — `os.freemem()` is deliberately NOT used: it excludes
|
|
38569
|
+
* reclaimable page cache and reads near-zero on macOS / cache-heavy Linux, which
|
|
38570
|
+
* would collapse the auto path to a single worker. `os.totalmem()` reports the
|
|
38571
|
+
* host total even inside a container, so the cgroup limit (read directly,
|
|
38572
|
+
* because Node doesn't fold it into `totalmem()`) is the real ceiling there.
|
|
38573
|
+
*
|
|
38574
|
+
* `facts` is injectable so tests exercise core-bound, memory-bound, cgroup-
|
|
38575
|
+
* limited, and ceiling cases without mocking `os` or the filesystem.
|
|
38576
|
+
*/
|
|
38577
|
+
const resolveAutoScanConcurrency = (facts = readSystemFacts()) => {
|
|
38578
|
+
const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
|
|
38579
|
+
const memoryBoundedWorkers = Math.floor(availableMemoryBytes / PER_WORKER_MEM_BUDGET_BYTES);
|
|
38580
|
+
return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
|
|
38581
|
+
};
|
|
38582
|
+
const resolveLintBatchOrdering = () => {
|
|
38583
|
+
return process.env["REACT_DOCTOR_LINT_BATCH_ORDERING"]?.trim().toLowerCase() === "cost" ? "cost" : "arrival";
|
|
38303
38584
|
};
|
|
38304
38585
|
/**
|
|
38305
38586
|
* Per-batch oxlint wall-clock budget. Reads from the env var on
|
|
@@ -38307,11 +38588,38 @@ const resolveScanConcurrency = (requested) => {
|
|
|
38307
38588
|
* microVMs without recompiling react-doctor. Tests override via
|
|
38308
38589
|
* `Layer.succeed(OxlintSpawnTimeoutMs, ...)`.
|
|
38309
38590
|
*/
|
|
38310
|
-
var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => {
|
|
38311
|
-
|
|
38312
|
-
|
|
38591
|
+
var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS", OXLINT_SPAWN_TIMEOUT_MS) }) {};
|
|
38592
|
+
/**
|
|
38593
|
+
* Effect-side cap on the lint phase. The env var lets CI / eval runners
|
|
38594
|
+
* raise the phase budget for slow large repos without recompiling.
|
|
38595
|
+
* Tests override via `Layer.succeed(LintPhaseTimeoutMs, ...)`.
|
|
38596
|
+
*/
|
|
38597
|
+
var LintPhaseTimeoutMs = class extends Reference("react-doctor/LintPhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_LINT_PHASE_TIMEOUT_MS", LINT_PHASE_TIMEOUT_MS) }) {};
|
|
38598
|
+
/**
|
|
38599
|
+
* Effect-side cap on the dead-code phase, sitting above the in-worker
|
|
38600
|
+
* timeout as a runtime-independent backstop. The env var raises it for
|
|
38601
|
+
* type-heavy projects; tests override via
|
|
38602
|
+
* `Layer.succeed(DeadCodePhaseTimeoutMs, ...)`.
|
|
38603
|
+
*/
|
|
38604
|
+
var DeadCodePhaseTimeoutMs = class extends Reference("react-doctor/DeadCodePhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_DEAD_CODE_PHASE_TIMEOUT_MS", DEAD_CODE_PHASE_TIMEOUT_MS) }) {};
|
|
38605
|
+
/**
|
|
38606
|
+
* Overall scan deadline backstop, bounding everything the per-phase
|
|
38607
|
+
* timeouts don't (wedged git / IO). The env var raises it for very
|
|
38608
|
+
* large repos; tests override via `Layer.succeed(ScanDeadlineMs, ...)`.
|
|
38609
|
+
*/
|
|
38610
|
+
var ScanDeadlineMs = class extends Reference("react-doctor/ScanDeadlineMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_SCAN_DEADLINE_MS", SCAN_TOTAL_DEADLINE_MS) }) {};
|
|
38611
|
+
/**
|
|
38612
|
+
* Wall-clock budget for the supply-chain check when it runs on a background
|
|
38613
|
+
* fiber overlapping the lint pass. Reads from the env var on startup so the
|
|
38614
|
+
* eval harness can raise the budget under sandbox microVMs (slower network)
|
|
38615
|
+
* without recompiling react-doctor. Tests override via
|
|
38616
|
+
* `Layer.succeed(SupplyChainOverlapTimeoutMs, ...)`.
|
|
38617
|
+
*/
|
|
38618
|
+
var SupplyChainOverlapTimeoutMs = class extends Reference("react-doctor/SupplyChainOverlapTimeoutMs", { defaultValue: () => {
|
|
38619
|
+
const raw = process.env["REACT_DOCTOR_SUPPLY_CHAIN_TIMEOUT_MS"];
|
|
38620
|
+
if (raw === void 0) return SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS;
|
|
38313
38621
|
const parsed = Number(raw);
|
|
38314
|
-
if (!Number.isFinite(parsed) || parsed <= 0) return
|
|
38622
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS;
|
|
38315
38623
|
return parsed;
|
|
38316
38624
|
} }) {};
|
|
38317
38625
|
/**
|
|
@@ -38322,31 +38630,93 @@ var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTime
|
|
|
38322
38630
|
*/
|
|
38323
38631
|
var OxlintOutputMaxBytes = class extends Reference("react-doctor/OxlintOutputMaxBytes", { defaultValue: () => OXLINT_OUTPUT_MAX_BYTES }) {};
|
|
38324
38632
|
/**
|
|
38325
|
-
* Number of oxlint subprocesses the lint pass runs in parallel. Defaults
|
|
38326
|
-
*
|
|
38327
|
-
* the box
|
|
38328
|
-
*
|
|
38329
|
-
*
|
|
38330
|
-
*
|
|
38331
|
-
*
|
|
38332
|
-
*
|
|
38333
|
-
*
|
|
38334
|
-
*
|
|
38633
|
+
* Number of oxlint subprocesses the lint pass runs in parallel. Defaults to a
|
|
38634
|
+
* memory-and-core-budgeted auto count (`resolveAutoScanConcurrency`) so large
|
|
38635
|
+
* repos scan fast out of the box without OOMing the native binding on a
|
|
38636
|
+
* high-core / low-memory box; `spawnLintBatches` transparently falls back to a
|
|
38637
|
+
* single worker if a parallel run still exhausts system resources. The CLI's
|
|
38638
|
+
* `--no-parallel` flag forces serial via `Layer.succeed`; the
|
|
38639
|
+
* `REACT_DOCTOR_PARALLEL` env var seeds the default for programmatic / CI
|
|
38640
|
+
* callers that never touch the flag — parallelism is opt-OUT, so only the
|
|
38641
|
+
* explicit serial values pin one worker:
|
|
38642
|
+
*
|
|
38643
|
+
* - unset / `auto` / `true` / `on` → memory-and-core-budgeted auto count
|
|
38335
38644
|
* - `0` / `false` / `off` → `1` (serial)
|
|
38336
38645
|
* - a positive integer → that many workers (clamped)
|
|
38337
|
-
* - any other value →
|
|
38646
|
+
* - any other value → memory-and-core-budgeted auto count
|
|
38338
38647
|
*
|
|
38339
38648
|
* The resolved value is always within
|
|
38340
|
-
* `[MIN_SCAN_CONCURRENCY,
|
|
38649
|
+
* `[MIN_SCAN_CONCURRENCY, HARD_MAX_SCAN_CONCURRENCY]`.
|
|
38341
38650
|
*/
|
|
38342
38651
|
var OxlintConcurrency = class extends Reference("react-doctor/OxlintConcurrency", { defaultValue: () => {
|
|
38343
38652
|
const raw = process.env["REACT_DOCTOR_PARALLEL"];
|
|
38344
|
-
if (raw === void 0) return
|
|
38653
|
+
if (raw === void 0) return resolveAutoScanConcurrency();
|
|
38345
38654
|
const normalized = raw.trim().toLowerCase();
|
|
38346
38655
|
if (normalized === "0" || normalized === "false" || normalized === "off") return 1;
|
|
38347
38656
|
const parsed = Number.parseInt(normalized, 10);
|
|
38348
38657
|
if (Number.isInteger(parsed) && parsed > 0) return resolveScanConcurrency(parsed);
|
|
38349
|
-
return
|
|
38658
|
+
return resolveAutoScanConcurrency();
|
|
38659
|
+
} }) {};
|
|
38660
|
+
/**
|
|
38661
|
+
* Three-state control for overlapping the dead-code pass with the lint pass —
|
|
38662
|
+
* forking dead-code as a child fiber that runs DURING lint instead of strictly
|
|
38663
|
+
* after it.
|
|
38664
|
+
*
|
|
38665
|
+
* - `"auto"` (default) / `"off"` → strictly SEQUENTIAL: dead-code runs after
|
|
38666
|
+
* lint with the full core budget. Both deslop's parse pool and the oxlint
|
|
38667
|
+
* pool are CPU-bound and each size themselves to all cores, so overlapping
|
|
38668
|
+
* them only oversubscribes (~2x the cores) and starves the parse pass past
|
|
38669
|
+
* its timeout — for no wall-clock win, since there are no spare cores to
|
|
38670
|
+
* absorb the second pass. Sequential is both faster per-phase and safe.
|
|
38671
|
+
* - `"on"` → force the overlap anyway. The orchestrator then SPLITS the core
|
|
38672
|
+
* budget (`DEAD_CODE_OVERLAP_PARSE_SHARE`): deslop's parse pool is capped
|
|
38673
|
+
* and lint shrinks to the remainder, so the two sum to the cores instead of
|
|
38674
|
+
* doubling them, and the dead-code timeout scales up for the reduced share.
|
|
38675
|
+
*
|
|
38676
|
+
* Seeded from `REACT_DOCTOR_DEAD_CODE_OVERLAP` so operators get a redeploy-free
|
|
38677
|
+
* switch; tests pin it via `Layer.succeed(DeadCodeOverlap, ...)`.
|
|
38678
|
+
*/
|
|
38679
|
+
var DeadCodeOverlap = class extends Reference("react-doctor/DeadCodeOverlap", { defaultValue: () => {
|
|
38680
|
+
const raw = process.env["REACT_DOCTOR_DEAD_CODE_OVERLAP"]?.trim().toLowerCase();
|
|
38681
|
+
if (raw === "on" || raw === "true" || raw === "1") return "on";
|
|
38682
|
+
if (raw === "off" || raw === "false" || raw === "0") return "off";
|
|
38683
|
+
return "auto";
|
|
38684
|
+
} }) {};
|
|
38685
|
+
/**
|
|
38686
|
+
* How the full-scan lint pass orders its file batches. `"arrival"` (the
|
|
38687
|
+
* default) keeps `git ls-files` discovery order. `"cost"` opts into LPT (feed
|
|
38688
|
+
* the largest files first); set `REACT_DOCTOR_LINT_BATCH_ORDERING=cost`. NOTE:
|
|
38689
|
+
* `cost` is OFF by default because the current sort-desc-then-chunk-100 packs
|
|
38690
|
+
* the heaviest files into one wave-1 batch — on size-skewed repos that mega-
|
|
38691
|
+
* batch is a straggler (and can trip the per-batch timeout + split), measurably
|
|
38692
|
+
* regressing the common full-scan case. LPT needs the heavy files SPREAD across
|
|
38693
|
+
* batches before `cost` earns the default. Tests override via
|
|
38694
|
+
* `Layer.succeed(LintBatchOrdering, ...)`. Diff / staged scans never reach this
|
|
38695
|
+
* — they pass user-scoped `includePaths` that skip discovery and stay in
|
|
38696
|
+
* arrival order; only the full-scan branch reads it.
|
|
38697
|
+
*/
|
|
38698
|
+
var LintBatchOrdering = class extends Reference("react-doctor/LintBatchOrdering", { defaultValue: resolveLintBatchOrdering }) {};
|
|
38699
|
+
const CACHE_DISABLED_VALUES$1 = new Set(["1", "true"]);
|
|
38700
|
+
/**
|
|
38701
|
+
* Whether the per-file lint cache (`runners/oxlint/file-lint-cache.ts`) is
|
|
38702
|
+
* active. Defaults ON — repeat scans re-lint only the files whose content
|
|
38703
|
+
* changed, and correctness is guaranteed byte-identical to a cold scan by the
|
|
38704
|
+
* always-fresh cross-file sidecar. Opt-OUT, two knobs (matching the whole-repo
|
|
38705
|
+
* scan cache's `REACT_DOCTOR_NO_CACHE`):
|
|
38706
|
+
*
|
|
38707
|
+
* - `REACT_DOCTOR_NO_CACHE` — the global off-switch; disables BOTH the
|
|
38708
|
+
* whole-repo scan cache and this per-file cache.
|
|
38709
|
+
* - `REACT_DOCTOR_NO_FILE_CACHE` — granular: bust only the per-file cache
|
|
38710
|
+
* while keeping the whole-repo short-circuit.
|
|
38711
|
+
*
|
|
38712
|
+
* Tests override via `Layer.succeed(PerFileLintCacheEnabled, false)`.
|
|
38713
|
+
*/
|
|
38714
|
+
var PerFileLintCacheEnabled = class extends Reference("react-doctor/PerFileLintCacheEnabled", { defaultValue: () => {
|
|
38715
|
+
const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
|
|
38716
|
+
const noFileCache = process.env["REACT_DOCTOR_NO_FILE_CACHE"]?.toLowerCase() ?? "";
|
|
38717
|
+
if (CACHE_DISABLED_VALUES$1.has(noCache)) return false;
|
|
38718
|
+
if (CACHE_DISABLED_VALUES$1.has(noFileCache)) return false;
|
|
38719
|
+
return true;
|
|
38350
38720
|
} }) {};
|
|
38351
38721
|
const DIAGNOSTIC_SURFACES = [
|
|
38352
38722
|
"cli",
|
|
@@ -38520,7 +38890,6 @@ const PACKAGE_JSON_FILENAME = "package.json";
|
|
|
38520
38890
|
const PACKAGE_JSON_CONFIG_KEY$1 = "reactDoctor";
|
|
38521
38891
|
const LEGACY_CONFIG_FILENAME = "react-doctor.config.json";
|
|
38522
38892
|
const jiti = createJiti(import.meta.url);
|
|
38523
|
-
const formatError = (error) => error instanceof Error ? error.message : String(error);
|
|
38524
38893
|
const importDefaultExport = async (jitiInstance, filePath) => {
|
|
38525
38894
|
const imported = await jitiInstance.import(filePath);
|
|
38526
38895
|
return imported?.default ?? imported;
|
|
@@ -38552,7 +38921,7 @@ const loadModuleConfig = async (filePath) => {
|
|
|
38552
38921
|
try {
|
|
38553
38922
|
return await importDefaultExport(aliasJiti, filePath);
|
|
38554
38923
|
} catch (retryError) {
|
|
38555
|
-
throw new Error(`${
|
|
38924
|
+
throw new Error(`${messageFromUnknown(error)} (retry with ${SELF_PACKAGE_IMPORT_SPECIFIER} aliased to the running react-doctor package also failed: ${messageFromUnknown(retryError)})`, { cause: retryError });
|
|
38556
38925
|
}
|
|
38557
38926
|
}
|
|
38558
38927
|
};
|
|
@@ -38601,7 +38970,7 @@ const loadLegacyConfig = (directory) => {
|
|
|
38601
38970
|
}
|
|
38602
38971
|
warn(`${LEGACY_CONFIG_FILENAME} must contain an object, ignoring.`);
|
|
38603
38972
|
} catch (error) {
|
|
38604
|
-
warn(`Failed to load ${LEGACY_CONFIG_FILENAME}: ${
|
|
38973
|
+
warn(`Failed to load ${LEGACY_CONFIG_FILENAME}: ${messageFromUnknown(error)}`);
|
|
38605
38974
|
}
|
|
38606
38975
|
return {
|
|
38607
38976
|
status: "invalid",
|
|
@@ -38628,7 +38997,7 @@ const loadConfigFromDirectory = async (directory) => {
|
|
|
38628
38997
|
warn(`${CONFIG_BASENAME}.${extension} must export an object, ignoring.`);
|
|
38629
38998
|
sawBrokenConfigFile = true;
|
|
38630
38999
|
} catch (error) {
|
|
38631
|
-
warn(`Failed to load ${CONFIG_BASENAME}.${extension}: ${
|
|
39000
|
+
warn(`Failed to load ${CONFIG_BASENAME}.${extension}: ${messageFromUnknown(error)}`);
|
|
38632
39001
|
sawBrokenConfigFile = true;
|
|
38633
39002
|
}
|
|
38634
39003
|
}
|
|
@@ -38758,6 +39127,31 @@ const resolveScanTarget = async (requestedDirectory, options = {}) => {
|
|
|
38758
39127
|
didRedirectViaRootDir: redirectedDirectory !== null
|
|
38759
39128
|
};
|
|
38760
39129
|
};
|
|
39130
|
+
const buildFixGroupId = (diagnostic) => createHash("sha1").update(JSON.stringify([
|
|
39131
|
+
diagnostic.filePath,
|
|
39132
|
+
`${diagnostic.plugin}/${diagnostic.rule}`,
|
|
39133
|
+
diagnostic.message
|
|
39134
|
+
])).digest("hex").slice(0, 16);
|
|
39135
|
+
const isGroupableRule = (diagnostic) => ROOT_CAUSE_GROUPABLE_RULE_KEYS.has(`${diagnostic.plugin}/${diagnostic.rule}`);
|
|
39136
|
+
const assignFixGroups = (diagnostics) => {
|
|
39137
|
+
const siteCountByGroupId = /* @__PURE__ */ new Map();
|
|
39138
|
+
for (const diagnostic of diagnostics) {
|
|
39139
|
+
if (!isGroupableRule(diagnostic)) continue;
|
|
39140
|
+
const groupId = buildFixGroupId(diagnostic);
|
|
39141
|
+
siteCountByGroupId.set(groupId, (siteCountByGroupId.get(groupId) ?? 0) + 1);
|
|
39142
|
+
}
|
|
39143
|
+
return diagnostics.map((diagnostic) => {
|
|
39144
|
+
if (!isGroupableRule(diagnostic)) return diagnostic;
|
|
39145
|
+
const groupId = buildFixGroupId(diagnostic);
|
|
39146
|
+
if ((siteCountByGroupId.get(groupId) ?? 0) < 2) return diagnostic;
|
|
39147
|
+
return {
|
|
39148
|
+
...diagnostic,
|
|
39149
|
+
fixGroupId: groupId
|
|
39150
|
+
};
|
|
39151
|
+
});
|
|
39152
|
+
};
|
|
39153
|
+
const compareStrings = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
39154
|
+
const sortDiagnosticsStable = (diagnostics) => [...diagnostics].sort((left, right) => compareStrings(left.filePath, right.filePath) || left.line - right.line || left.column - right.column || compareStrings(left.plugin, right.plugin) || compareStrings(left.rule, right.rule) || compareStrings(left.severity, right.severity) || compareStrings(left.message, right.message));
|
|
38761
39155
|
const getDirectDependencyNames = (packageJson) => new Set([...Object.keys(packageJson.dependencies ?? {}), ...Object.keys(packageJson.devDependencies ?? {})]);
|
|
38762
39156
|
const buildExpoCheckContext = (rootDirectory, expoVersion) => {
|
|
38763
39157
|
const packageJson = readPackageJson$1(Path.join(rootDirectory, "package.json"));
|
|
@@ -39264,10 +39658,15 @@ const buildHardeningDiagnostic = (input) => ({
|
|
|
39264
39658
|
column: input.column ?? 0,
|
|
39265
39659
|
category: "Security"
|
|
39266
39660
|
});
|
|
39267
|
-
const checkPnpmHardening = (
|
|
39268
|
-
if (!isPnpmManagedProject(
|
|
39269
|
-
const workspacePath = Path.join(
|
|
39270
|
-
const
|
|
39661
|
+
const checkPnpmHardening = (scanDirectory) => {
|
|
39662
|
+
if (!isPnpmManagedProject(scanDirectory)) return [];
|
|
39663
|
+
const workspacePath = Path.join(scanDirectory, PNPM_WORKSPACE_FILE);
|
|
39664
|
+
const hasWorkspaceFile = isFile(workspacePath);
|
|
39665
|
+
if (!hasWorkspaceFile) {
|
|
39666
|
+
const monorepoRoot = findMonorepoRoot(scanDirectory);
|
|
39667
|
+
if (monorepoRoot !== null && isFile(Path.join(monorepoRoot, PNPM_WORKSPACE_FILE))) return [];
|
|
39668
|
+
}
|
|
39669
|
+
const settings = parseHardeningSettings(hasWorkspaceFile ? NFS.readFileSync(workspacePath, "utf-8") : "");
|
|
39271
39670
|
const diagnostics = [];
|
|
39272
39671
|
if (settings.minimumReleaseAge === null) diagnostics.push(buildHardeningDiagnostic({
|
|
39273
39672
|
message: "pnpm-workspace.yaml is missing `minimumReleaseAge` — newly published versions can ship malware that gets caught and unpublished within hours",
|
|
@@ -39903,7 +40302,7 @@ const readIgnoreFile = (filePath) => {
|
|
|
39903
40302
|
try {
|
|
39904
40303
|
content = NFS.readFileSync(filePath, "utf-8");
|
|
39905
40304
|
} catch (error) {
|
|
39906
|
-
const errnoCode = error
|
|
40305
|
+
const errnoCode = isErrnoException(error) ? error.code : void 0;
|
|
39907
40306
|
if (errnoCode && errnoCode !== "ENOENT") runSync(warn$1(`Could not read ignore file ${filePath}: ${errnoCode}`));
|
|
39908
40307
|
return [];
|
|
39909
40308
|
}
|
|
@@ -39941,8 +40340,8 @@ const collectIgnorePatterns = (rootDirectory) => {
|
|
|
39941
40340
|
cachedPatternsByRoot.set(rootDirectory, patterns);
|
|
39942
40341
|
return patterns;
|
|
39943
40342
|
};
|
|
40343
|
+
const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
39944
40344
|
const KNIP_JSON_FILENAME = "knip.json";
|
|
39945
|
-
const isRecord$1$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
39946
40345
|
const readJsonFileSafe = (filePath) => {
|
|
39947
40346
|
let rawContents;
|
|
39948
40347
|
try {
|
|
@@ -39958,10 +40357,10 @@ const readJsonFileSafe = (filePath) => {
|
|
|
39958
40357
|
};
|
|
39959
40358
|
const readKnipConfig = (rootDirectory) => {
|
|
39960
40359
|
const knipJson = readJsonFileSafe(path.join(rootDirectory, KNIP_JSON_FILENAME));
|
|
39961
|
-
if (isRecord$
|
|
40360
|
+
if (isRecord$2(knipJson)) return knipJson;
|
|
39962
40361
|
const packageJson = readJsonFileSafe(path.join(rootDirectory, "package.json"));
|
|
39963
|
-
const packageKnipConfig = isRecord$
|
|
39964
|
-
return isRecord$
|
|
40362
|
+
const packageKnipConfig = isRecord$2(packageJson) ? packageJson.knip : null;
|
|
40363
|
+
return isRecord$2(packageKnipConfig) ? packageKnipConfig : null;
|
|
39965
40364
|
};
|
|
39966
40365
|
const normalizePatternList = (value) => {
|
|
39967
40366
|
if (typeof value === "string" && value.length > 0) return [value];
|
|
@@ -39973,10 +40372,10 @@ const prefixWorkspacePatterns = (workspacePattern, patterns) => {
|
|
|
39973
40372
|
return patterns.map((pattern) => pattern.startsWith("!") ? `!${normalizedWorkspacePattern}/${pattern.slice(1)}` : `${normalizedWorkspacePattern}/${pattern}`);
|
|
39974
40373
|
};
|
|
39975
40374
|
const collectKnipWorkspacePatterns = (workspaces, settingName) => {
|
|
39976
|
-
if (!isRecord$
|
|
40375
|
+
if (!isRecord$2(workspaces)) return [];
|
|
39977
40376
|
const patterns = [];
|
|
39978
40377
|
for (const [workspacePattern, workspaceConfig] of Object.entries(workspaces)) {
|
|
39979
|
-
if (!isRecord$
|
|
40378
|
+
if (!isRecord$2(workspaceConfig)) continue;
|
|
39980
40379
|
patterns.push(...prefixWorkspacePatterns(workspacePattern, normalizePatternList(workspaceConfig[settingName])));
|
|
39981
40380
|
}
|
|
39982
40381
|
return patterns;
|
|
@@ -39986,12 +40385,11 @@ const collectKnipPatterns = (rootDirectory, settingName) => {
|
|
|
39986
40385
|
if (!config) return [];
|
|
39987
40386
|
return [...normalizePatternList(config[settingName]), ...collectKnipWorkspacePatterns(config.workspaces, settingName)];
|
|
39988
40387
|
};
|
|
39989
|
-
const collectDeadCodeIgnorePatterns = (rootDirectory
|
|
40388
|
+
const collectDeadCodeIgnorePatterns = (rootDirectory) => {
|
|
39990
40389
|
const seen = /* @__PURE__ */ new Set();
|
|
39991
40390
|
const sources = [
|
|
39992
40391
|
readIgnoreFile(path.join(rootDirectory, ".gitignore")),
|
|
39993
40392
|
collectIgnorePatterns(rootDirectory),
|
|
39994
|
-
userConfig?.ignore?.files ?? [],
|
|
39995
40393
|
collectKnipPatterns(rootDirectory, "ignore")
|
|
39996
40394
|
];
|
|
39997
40395
|
for (const source of sources) for (const pattern of source) seen.add(pattern);
|
|
@@ -40022,8 +40420,6 @@ const toCanonicalPath = (filePath) => {
|
|
|
40022
40420
|
};
|
|
40023
40421
|
const DEAD_CODE_PLUGIN = "deslop";
|
|
40024
40422
|
const DEAD_CODE_CATEGORY = "Maintainability";
|
|
40025
|
-
const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
|
|
40026
|
-
const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
40027
40423
|
const DEAD_CODE_WORKER_SCRIPT = `
|
|
40028
40424
|
const inputChunks = [];
|
|
40029
40425
|
process.stdin.on("data", (chunk) => inputChunks.push(chunk));
|
|
@@ -40071,6 +40467,22 @@ process.stdin.on("end", () => {
|
|
|
40071
40467
|
...(workerInput.ignorePatterns.length > 0
|
|
40072
40468
|
? { ignorePatterns: workerInput.ignorePatterns }
|
|
40073
40469
|
: {}),
|
|
40470
|
+
// We consume only deslop's GRAPH-based findings (unusedFiles, unusedExports,
|
|
40471
|
+
// unusedDependencies, circularDependencies). Everything else deslop can compute
|
|
40472
|
+
// is pure wasted work for us, and it's the bulk of the runtime:
|
|
40473
|
+
// - semantic: a full TS Program for unusedTypes/enum/class-members/
|
|
40474
|
+
// misclassifiedDependencies (~37-45% of the phase).
|
|
40475
|
+
// - reportCodeQuality: the duplicate-block, complexity, feature-flag,
|
|
40476
|
+
// TypeScript-smell, private-type-leak and re-export-cycle detectors. These
|
|
40477
|
+
// are the single most expensive pass — duplicate-block detection alone was
|
|
40478
|
+
// ~83s of a ~130s Sentry scan — so skipping them is an ~8.5x dead-code
|
|
40479
|
+
// speedup on a large repo.
|
|
40480
|
+
// Both are provably safe: the consumed graph findings are computed by their own
|
|
40481
|
+
// detectors, independent of these passes (confirmed byte-identical on
|
|
40482
|
+
// excalidraw + mui-material + sentry). tsConfigPath stays — the module resolver
|
|
40483
|
+
// needs it for path-alias resolution in the import graph.
|
|
40484
|
+
semantic: { enabled: false },
|
|
40485
|
+
reportCodeQuality: false,
|
|
40074
40486
|
};
|
|
40075
40487
|
const result = await analyze(defineConfig(config));
|
|
40076
40488
|
emit({ ok: true, result: normalizeResult(result) });
|
|
@@ -40081,7 +40493,7 @@ process.stdin.on("end", () => {
|
|
|
40081
40493
|
});
|
|
40082
40494
|
`;
|
|
40083
40495
|
const resolveTsConfigPath = (rootDirectory) => {
|
|
40084
|
-
for (const filename of TSCONFIG_FILENAMES
|
|
40496
|
+
for (const filename of TSCONFIG_FILENAMES) {
|
|
40085
40497
|
const candidate = Path.join(rootDirectory, filename);
|
|
40086
40498
|
if (NFS.existsSync(candidate)) return candidate;
|
|
40087
40499
|
}
|
|
@@ -40200,7 +40612,11 @@ const createDeadCodeWorker = (input) => {
|
|
|
40200
40612
|
"pipe",
|
|
40201
40613
|
"pipe"
|
|
40202
40614
|
],
|
|
40203
|
-
windowsHide: true
|
|
40615
|
+
windowsHide: true,
|
|
40616
|
+
env: input.parseConcurrency === void 0 ? process.env : {
|
|
40617
|
+
...process.env,
|
|
40618
|
+
DESLOP_PARSE_CONCURRENCY: String(input.parseConcurrency)
|
|
40619
|
+
}
|
|
40204
40620
|
});
|
|
40205
40621
|
const stdoutChunks = [];
|
|
40206
40622
|
const stderrChunks = [];
|
|
@@ -40245,42 +40661,39 @@ const createDeadCodeWorker = (input) => {
|
|
|
40245
40661
|
}
|
|
40246
40662
|
};
|
|
40247
40663
|
};
|
|
40248
|
-
const runDeadCodeWorkerWithTimeout = (handle, timeoutMs) => new Promise((resolve, reject) => {
|
|
40664
|
+
const runDeadCodeWorkerWithTimeout = (handle, timeoutMs, abortSignal) => new Promise((resolve, reject) => {
|
|
40249
40665
|
let didSettle = false;
|
|
40250
|
-
const
|
|
40251
|
-
if (didSettle) return;
|
|
40252
|
-
didSettle = true;
|
|
40253
|
-
handle.terminate?.();
|
|
40254
|
-
reject(/* @__PURE__ */ new Error(`Dead-code worker timed out after ${timeoutMs / MILLISECONDS_PER_SECOND}s.`));
|
|
40255
|
-
}, timeoutMs);
|
|
40256
|
-
timeoutHandle.unref?.();
|
|
40257
|
-
handle.result.then((value) => {
|
|
40666
|
+
const settle = (finish) => {
|
|
40258
40667
|
if (didSettle) return;
|
|
40259
40668
|
didSettle = true;
|
|
40260
40669
|
clearTimeout(timeoutHandle);
|
|
40670
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
40261
40671
|
handle.terminate?.();
|
|
40262
|
-
|
|
40263
|
-
}
|
|
40264
|
-
|
|
40265
|
-
|
|
40266
|
-
|
|
40267
|
-
|
|
40268
|
-
|
|
40269
|
-
|
|
40672
|
+
finish();
|
|
40673
|
+
};
|
|
40674
|
+
const onAbort = () => settle(() => reject(/* @__PURE__ */ new Error("Dead-code worker aborted.")));
|
|
40675
|
+
const timeoutHandle = setTimeout(() => settle(() => reject(/* @__PURE__ */ new Error(`Dead-code worker timed out after ${timeoutMs / MILLISECONDS_PER_SECOND}s.`))), timeoutMs);
|
|
40676
|
+
timeoutHandle.unref?.();
|
|
40677
|
+
if (abortSignal?.aborted) {
|
|
40678
|
+
onAbort();
|
|
40679
|
+
return;
|
|
40680
|
+
}
|
|
40681
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
40682
|
+
handle.result.then((value) => settle(() => resolve(value)), (error) => settle(() => reject(error)));
|
|
40270
40683
|
});
|
|
40271
40684
|
const checkDeadCode = async (options) => {
|
|
40272
|
-
const { userConfig } = options;
|
|
40273
40685
|
const rootDirectory = toCanonicalPath(options.rootDirectory);
|
|
40274
40686
|
if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
|
|
40275
40687
|
const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
|
|
40276
|
-
const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory
|
|
40688
|
+
const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
|
|
40277
40689
|
const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
|
|
40278
40690
|
rootDirectory,
|
|
40279
40691
|
entryPatterns,
|
|
40280
40692
|
tsConfigPath: resolveTsConfigPath(rootDirectory),
|
|
40281
40693
|
ignorePatterns,
|
|
40282
|
-
deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js")
|
|
40283
|
-
|
|
40694
|
+
deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
|
|
40695
|
+
parseConcurrency: options.parseConcurrency
|
|
40696
|
+
}), options.workerTimeoutMs ?? 12e4, options.abortSignal));
|
|
40284
40697
|
const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
|
|
40285
40698
|
const diagnostics = [];
|
|
40286
40699
|
for (const unusedFile of result.unusedFiles) diagnostics.push({
|
|
@@ -40378,7 +40791,37 @@ const isDiagnosticOnSurface = (diagnostic, surface, config) => {
|
|
|
40378
40791
|
return true;
|
|
40379
40792
|
};
|
|
40380
40793
|
const filterDiagnosticsForSurface = (diagnostics, surface, config) => diagnostics.filter((diagnostic) => isDiagnosticOnSurface(diagnostic, surface, config));
|
|
40381
|
-
|
|
40794
|
+
/**
|
|
40795
|
+
* Budget for the dead-code phase, scaled to the work. deslop's graph build is
|
|
40796
|
+
* CPU-bound and roughly linear in file count, so a fixed 120s cap is too tight
|
|
40797
|
+
* for a large repo (where the pass legitimately runs that long) and is then
|
|
40798
|
+
* tipped over by any concurrent load — silently dropping every dead-code
|
|
40799
|
+
* finding. Scaling the budget with file count (and inversely with the core
|
|
40800
|
+
* share when overlapped) lets the pass complete, while the ceiling still
|
|
40801
|
+
* reclaims a genuinely wedged worker. Returns the in-worker SIGKILL deadline
|
|
40802
|
+
* and the Effect-side phase backstop that sits a margin above it.
|
|
40803
|
+
*/
|
|
40804
|
+
const resolveDeadCodeTimeout = (input) => {
|
|
40805
|
+
const coreShareFactor = Math.max(1, input.fullConcurrency / Math.max(1, input.deadCodeConcurrency));
|
|
40806
|
+
const workerTimeoutMs = Math.min(DEAD_CODE_TIMEOUT_CEILING_MS, Math.max(DEAD_CODE_WORKER_TIMEOUT_MS, Math.ceil(input.sourceFileCount * 30 * coreShareFactor)));
|
|
40807
|
+
return {
|
|
40808
|
+
workerTimeoutMs,
|
|
40809
|
+
phaseTimeoutMs: workerTimeoutMs + DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS
|
|
40810
|
+
};
|
|
40811
|
+
};
|
|
40812
|
+
const collectSizedSourceFiles = (rootDirectory, relativePaths) => {
|
|
40813
|
+
const entries = [];
|
|
40814
|
+
for (const relativePath of relativePaths) {
|
|
40815
|
+
const absolutePath = Path.resolve(rootDirectory, relativePath);
|
|
40816
|
+
const sizeBytes = statSourceFileSize(absolutePath);
|
|
40817
|
+
if (isLargeMinifiedFile(absolutePath, sizeBytes)) continue;
|
|
40818
|
+
entries.push({
|
|
40819
|
+
path: relativePath,
|
|
40820
|
+
sizeBytes: sizeBytes ?? 0
|
|
40821
|
+
});
|
|
40822
|
+
}
|
|
40823
|
+
return entries;
|
|
40824
|
+
};
|
|
40382
40825
|
const listSourceFilesViaGit = (rootDirectory) => {
|
|
40383
40826
|
const result = spawnSync("git", [
|
|
40384
40827
|
"ls-files",
|
|
@@ -40411,7 +40854,8 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
|
|
|
40411
40854
|
}
|
|
40412
40855
|
return filePaths;
|
|
40413
40856
|
};
|
|
40414
|
-
const
|
|
40857
|
+
const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory));
|
|
40858
|
+
const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
|
|
40415
40859
|
const resolveLintIncludePaths = (rootDirectory, userConfig, project) => {
|
|
40416
40860
|
if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
|
|
40417
40861
|
const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
|
|
@@ -40454,24 +40898,25 @@ var Config = class Config extends Service()("react-doctor/Config") {
|
|
|
40454
40898
|
var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
|
|
40455
40899
|
static layerNode = succeed$3(DeadCode, DeadCode.of({ run: (input) => unwrap(fn("DeadCode.run")(function* () {
|
|
40456
40900
|
return yield* tryPromise({
|
|
40457
|
-
try: () => checkDeadCode({
|
|
40901
|
+
try: (signal) => checkDeadCode({
|
|
40458
40902
|
rootDirectory: input.rootDirectory,
|
|
40459
|
-
userConfig: input.userConfig
|
|
40903
|
+
userConfig: input.userConfig,
|
|
40904
|
+
parseConcurrency: input.parseConcurrency,
|
|
40905
|
+
workerTimeoutMs: input.workerTimeoutMs,
|
|
40906
|
+
abortSignal: signal
|
|
40460
40907
|
}),
|
|
40461
40908
|
catch: (cause) => new ReactDoctorError({ reason: new DeadCodeAnalysisFailed({ cause }) })
|
|
40462
40909
|
}).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)));
|
|
40463
40910
|
})()) }));
|
|
40464
40911
|
static layerOf = (diagnostics) => succeed$3(DeadCode, DeadCode.of({ run: () => fromIterable$1(diagnostics) }));
|
|
40465
40912
|
};
|
|
40466
|
-
const createNodeReadFileLinesSync = (rootDirectory) => {
|
|
40467
|
-
|
|
40468
|
-
|
|
40469
|
-
|
|
40470
|
-
|
|
40471
|
-
|
|
40472
|
-
|
|
40473
|
-
}
|
|
40474
|
-
};
|
|
40913
|
+
const createNodeReadFileLinesSync = (rootDirectory) => (filePath) => {
|
|
40914
|
+
const absolutePath = Path.isAbsolute(filePath) ? filePath : Path.join(rootDirectory, filePath);
|
|
40915
|
+
try {
|
|
40916
|
+
return NFS.readFileSync(absolutePath, "utf-8").split("\n");
|
|
40917
|
+
} catch {
|
|
40918
|
+
return null;
|
|
40919
|
+
}
|
|
40475
40920
|
};
|
|
40476
40921
|
var Files = class Files extends Service()("react-doctor/Files") {
|
|
40477
40922
|
static layerNode = succeed$3(Files, Files.of({
|
|
@@ -40682,7 +41127,10 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40682
41127
|
directory: input.directory,
|
|
40683
41128
|
cause
|
|
40684
41129
|
}) });
|
|
40685
|
-
})
|
|
41130
|
+
}), withSpan("git.exec", { attributes: {
|
|
41131
|
+
"git.command": input.command,
|
|
41132
|
+
"git.subcommand": input.args[0] ?? ""
|
|
41133
|
+
} }));
|
|
40686
41134
|
const runGit = (directory, args) => runCommand({
|
|
40687
41135
|
command: "git",
|
|
40688
41136
|
args,
|
|
@@ -40710,7 +41158,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40710
41158
|
]);
|
|
40711
41159
|
if (candidates.status !== 0) return null;
|
|
40712
41160
|
return trimOrNull(candidates.stdout.split("\n")[0] ?? "");
|
|
40713
|
-
});
|
|
41161
|
+
}).pipe(withSpan("Git.defaultBranch"));
|
|
40714
41162
|
const branchExists = (directory, branch) => runGit(directory, [
|
|
40715
41163
|
"rev-parse",
|
|
40716
41164
|
"--verify",
|
|
@@ -40757,7 +41205,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40757
41205
|
const result = resultOption.value;
|
|
40758
41206
|
if (result.status !== 0) return null;
|
|
40759
41207
|
return parseGithubViewerPermission(result.stdout);
|
|
40760
|
-
}).pipe(catch_$1(() => succeed$2(null)));
|
|
41208
|
+
}).pipe(catch_$1(() => succeed$2(null)), withSpan("Git.githubViewerPermission"));
|
|
40761
41209
|
/**
|
|
40762
41210
|
* Resolves a `--diff A..B` / `A...B` commit range into a changed-file
|
|
40763
41211
|
* selection. Each endpoint is validated with `isSafeGitRevision`
|
|
@@ -40871,7 +41319,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40871
41319
|
changedFiles: splitNullSeparated(diff.stdout),
|
|
40872
41320
|
isCurrentChanges: false
|
|
40873
41321
|
};
|
|
40874
|
-
}),
|
|
41322
|
+
}).pipe(withSpan("Git.diffSelection")),
|
|
40875
41323
|
stagedFilePaths: (directory) => runGit(directory, [
|
|
40876
41324
|
"diff",
|
|
40877
41325
|
"--cached",
|
|
@@ -40913,7 +41361,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40913
41361
|
status: result.status,
|
|
40914
41362
|
stdout: result.stdout
|
|
40915
41363
|
};
|
|
40916
|
-
}),
|
|
41364
|
+
}).pipe(withSpan("Git.grep")),
|
|
40917
41365
|
changedLineRanges: ({ directory, baseRef, cached, files }) => gen(function* () {
|
|
40918
41366
|
if (files.length === 0) return [];
|
|
40919
41367
|
if (baseRef !== void 0 && !isSafeGitRevision(baseRef)) return null;
|
|
@@ -40929,7 +41377,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40929
41377
|
]);
|
|
40930
41378
|
if (result.status !== 0) return null;
|
|
40931
41379
|
return parseChangedLineRanges(result.stdout);
|
|
40932
|
-
})
|
|
41380
|
+
}).pipe(withSpan("Git.changedLineRanges"))
|
|
40933
41381
|
});
|
|
40934
41382
|
})).pipe(provide$2(layer$3.pipe(provide$2(mergeAll$1(layer$2, layer$1)))));
|
|
40935
41383
|
/**
|
|
@@ -41144,7 +41592,7 @@ const neutralizeDisableDirectives = async (rootDirectory, includePaths) => {
|
|
|
41144
41592
|
for (const [absolutePath, originalContent] of originalContents) try {
|
|
41145
41593
|
NFS.writeFileSync(absolutePath, originalContent);
|
|
41146
41594
|
} catch (error) {
|
|
41147
|
-
process.stderr.write(`[react-doctor] Failed to restore inline disable directives in ${absolutePath}: ${
|
|
41595
|
+
process.stderr.write(`[react-doctor] Failed to restore inline disable directives in ${absolutePath}: ${messageFromUnknown(error)}\n[react-doctor] Run: git checkout -- ${absolutePath}\n`);
|
|
41148
41596
|
}
|
|
41149
41597
|
};
|
|
41150
41598
|
const onExit = () => restore();
|
|
@@ -41168,6 +41616,14 @@ const neutralizeDisableDirectives = async (rootDirectory, includePaths) => {
|
|
|
41168
41616
|
process.removeListener("exit", onExit);
|
|
41169
41617
|
};
|
|
41170
41618
|
};
|
|
41619
|
+
const ROOT_DIRECTORY_PLACEHOLDER = "<root>";
|
|
41620
|
+
const normalizeConfigForHash = (config) => {
|
|
41621
|
+
const clone = JSON.parse(JSON.stringify(config));
|
|
41622
|
+
if (clone?.settings?.["react-doctor"]) clone.settings["react-doctor"].rootDirectory = ROOT_DIRECTORY_PLACEHOLDER;
|
|
41623
|
+
if (Array.isArray(clone?.jsPlugins)) clone.jsPlugins = clone.jsPlugins.map((_, index) => `<plugin:${index}>`);
|
|
41624
|
+
return clone;
|
|
41625
|
+
};
|
|
41626
|
+
const computeRulesetHash = (input) => crypto.createHash("sha1").update(JSON.stringify(normalizeConfigForHash(input.config))).update("\0").update([...input.toolchainVersions].join("\0")).update("\0").update([...input.ignorePatterns].join("\n")).update(" ").update(input.tsconfigContent ?? "").digest("hex");
|
|
41171
41627
|
/**
|
|
41172
41628
|
* Loads a plugin module via the local require resolver and extracts
|
|
41173
41629
|
* `(name, ruleNames)` from either `module.exports.meta + rules` or
|
|
@@ -41250,7 +41706,7 @@ const resolveUserPlugin = (spec, configSourceDirectory) => {
|
|
|
41250
41706
|
try {
|
|
41251
41707
|
resolvedSpecifier = isRelative ? Path.resolve(configSourceDirectory, spec) : candidateRequire.resolve(spec);
|
|
41252
41708
|
} catch (error) {
|
|
41253
|
-
warnConfigIssue(`config.plugins entry "${spec}" could not be resolved from ${configSourceDirectory}: ${
|
|
41709
|
+
warnConfigIssue(`config.plugins entry "${spec}" could not be resolved from ${configSourceDirectory}: ${messageFromUnknown(error)}`);
|
|
41254
41710
|
return null;
|
|
41255
41711
|
}
|
|
41256
41712
|
const { name, ruleNames } = readPluginShape(resolvedSpecifier, (target) => candidateRequire(target));
|
|
@@ -41322,8 +41778,8 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
|
|
|
41322
41778
|
}
|
|
41323
41779
|
return enabled;
|
|
41324
41780
|
};
|
|
41325
|
-
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [] }) => {
|
|
41326
|
-
const reactHooksJsPlugin = resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
|
|
41781
|
+
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection }) => {
|
|
41782
|
+
const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
|
|
41327
41783
|
const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
|
|
41328
41784
|
const jsPlugins = [];
|
|
41329
41785
|
if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
|
|
@@ -41332,6 +41788,8 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
41332
41788
|
for (const registryEntry of REACT_DOCTOR_RULES) {
|
|
41333
41789
|
const rule = reactDoctorPlugin.rules[registryEntry.id];
|
|
41334
41790
|
if (!rule) continue;
|
|
41791
|
+
if (ruleSelection === "cacheable" && CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
|
|
41792
|
+
if (ruleSelection === "sidecar" && !CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
|
|
41335
41793
|
if (rule.scan !== void 0) continue;
|
|
41336
41794
|
if (customRulesOnly && registryEntry.originallyExternal) continue;
|
|
41337
41795
|
if (rule.framework !== "global" && !rule.requires) continue;
|
|
@@ -41346,7 +41804,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
41346
41804
|
enabledReactDoctorRules[registryEntry.key] = severity;
|
|
41347
41805
|
}
|
|
41348
41806
|
const userPluginRules = {};
|
|
41349
|
-
for (const userPlugin of userPlugins) {
|
|
41807
|
+
if (ruleSelection !== "sidecar") for (const userPlugin of userPlugins) {
|
|
41350
41808
|
Object.assign(userPluginRules, buildUserPluginRules(userPlugin, severityControls));
|
|
41351
41809
|
jsPlugins.push(userPlugin.entry);
|
|
41352
41810
|
}
|
|
@@ -41376,6 +41834,100 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
41376
41834
|
}
|
|
41377
41835
|
};
|
|
41378
41836
|
};
|
|
41837
|
+
const atomicWriteJson = (filePath, value) => {
|
|
41838
|
+
try {
|
|
41839
|
+
NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
|
|
41840
|
+
const temporaryPath = `${filePath}.${process.pid}.tmp`;
|
|
41841
|
+
NFS.writeFileSync(temporaryPath, JSON.stringify(value));
|
|
41842
|
+
NFS.renameSync(temporaryPath, filePath);
|
|
41843
|
+
} catch {
|
|
41844
|
+
return;
|
|
41845
|
+
}
|
|
41846
|
+
};
|
|
41847
|
+
const failOpenReadJson = (filePath, fallback) => {
|
|
41848
|
+
try {
|
|
41849
|
+
return JSON.parse(NFS.readFileSync(filePath, "utf8"));
|
|
41850
|
+
} catch {
|
|
41851
|
+
return fallback;
|
|
41852
|
+
}
|
|
41853
|
+
};
|
|
41854
|
+
const validateDiagnostic = decodeUnknownSync(Diagnostic);
|
|
41855
|
+
const decodeFileDiagnostics = (raw) => {
|
|
41856
|
+
if (!Array.isArray(raw)) return null;
|
|
41857
|
+
try {
|
|
41858
|
+
for (const entry of raw) validateDiagnostic(entry);
|
|
41859
|
+
return raw;
|
|
41860
|
+
} catch {
|
|
41861
|
+
return null;
|
|
41862
|
+
}
|
|
41863
|
+
};
|
|
41864
|
+
const emptyCache = () => ({
|
|
41865
|
+
version: 1,
|
|
41866
|
+
rulesets: {}
|
|
41867
|
+
});
|
|
41868
|
+
const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
|
|
41869
|
+
const entries = /* @__PURE__ */ new Map();
|
|
41870
|
+
const persisted = failOpenReadJson(cacheFilePath, emptyCache());
|
|
41871
|
+
if (persisted.version !== 1 || !isRecord$2(persisted.rulesets)) return entries;
|
|
41872
|
+
const bucket = persisted.rulesets[rulesetHash];
|
|
41873
|
+
if (!isRecord$2(bucket) || !isRecord$2(bucket.files)) return entries;
|
|
41874
|
+
for (const [fileKey, rawDiagnostics] of Object.entries(bucket.files)) {
|
|
41875
|
+
const decoded = decodeFileDiagnostics(rawDiagnostics);
|
|
41876
|
+
if (decoded !== null) entries.set(fileKey, decoded);
|
|
41877
|
+
}
|
|
41878
|
+
return entries;
|
|
41879
|
+
};
|
|
41880
|
+
const createFileLintCache = (cacheDirectory, rulesetHash) => {
|
|
41881
|
+
const cacheFilePath = Path.join(cacheDirectory, FILE_LINT_CACHE_FILENAME);
|
|
41882
|
+
const entries = loadRulesetEntries(cacheFilePath, rulesetHash);
|
|
41883
|
+
return {
|
|
41884
|
+
lookup: (fileKey) => entries.get(fileKey) ?? null,
|
|
41885
|
+
store: (fileKey, diagnostics) => {
|
|
41886
|
+
entries.delete(fileKey);
|
|
41887
|
+
entries.set(fileKey, diagnostics);
|
|
41888
|
+
},
|
|
41889
|
+
persist: () => {
|
|
41890
|
+
const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
|
|
41891
|
+
const rulesets = onDisk.version === 1 && isRecord$2(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
|
|
41892
|
+
const existingBucket = rulesets[rulesetHash];
|
|
41893
|
+
const existingFiles = isRecord$2(existingBucket) && isRecord$2(existingBucket.files) ? existingBucket.files : {};
|
|
41894
|
+
const ourFiles = {};
|
|
41895
|
+
for (const [fileKey, diagnostics] of entries) ourFiles[fileKey] = diagnostics;
|
|
41896
|
+
const cappedEntries = Object.entries({
|
|
41897
|
+
...existingFiles,
|
|
41898
|
+
...ourFiles
|
|
41899
|
+
}).slice(-FILE_LINT_CACHE_MAX_FILE_COUNT);
|
|
41900
|
+
rulesets[rulesetHash] = {
|
|
41901
|
+
updatedAtMs: Date.now(),
|
|
41902
|
+
files: Object.fromEntries(cappedEntries)
|
|
41903
|
+
};
|
|
41904
|
+
const keptHashes = Object.entries(rulesets).sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8).map(([hash]) => hash);
|
|
41905
|
+
const prunedRulesets = {};
|
|
41906
|
+
for (const hash of keptHashes) prunedRulesets[hash] = rulesets[hash];
|
|
41907
|
+
atomicWriteJson(cacheFilePath, {
|
|
41908
|
+
version: 1,
|
|
41909
|
+
rulesets: prunedRulesets
|
|
41910
|
+
});
|
|
41911
|
+
}
|
|
41912
|
+
};
|
|
41913
|
+
};
|
|
41914
|
+
const bundledRequire$2 = createRequire(import.meta.url);
|
|
41915
|
+
const TOOLCHAIN_PACKAGE_SPECIFIERS$1 = [
|
|
41916
|
+
"oxlint/package.json",
|
|
41917
|
+
"oxlint-plugin-react-doctor/package.json",
|
|
41918
|
+
"eslint-plugin-react-hooks/package.json"
|
|
41919
|
+
];
|
|
41920
|
+
const resolveOxlintToolchainVersions = () => {
|
|
41921
|
+
const versions = [`node=${process.version}`];
|
|
41922
|
+
for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS$1) try {
|
|
41923
|
+
const packageJson = bundledRequire$2(specifier);
|
|
41924
|
+
const version = typeof packageJson.version === "string" ? packageJson.version : "unknown";
|
|
41925
|
+
versions.push(`${specifier}=${version}`);
|
|
41926
|
+
} catch {
|
|
41927
|
+
versions.push(`${specifier}=missing`);
|
|
41928
|
+
}
|
|
41929
|
+
return versions;
|
|
41930
|
+
};
|
|
41379
41931
|
const esmRequire = createRequire(import.meta.url);
|
|
41380
41932
|
const resolveOxlintBinary = () => {
|
|
41381
41933
|
const oxlintMainPath = esmRequire.resolve("oxlint");
|
|
@@ -41383,7 +41935,6 @@ const resolveOxlintBinary = () => {
|
|
|
41383
41935
|
return Path.join(oxlintPackageDirectory, "bin", "oxlint");
|
|
41384
41936
|
};
|
|
41385
41937
|
const resolvePluginPath = () => esmRequire.resolve("oxlint-plugin-react-doctor");
|
|
41386
|
-
const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
|
|
41387
41938
|
const resolveTsConfigRelativePath = (rootDirectory) => {
|
|
41388
41939
|
for (const filename of TSCONFIG_FILENAMES) if (NFS.existsSync(Path.join(rootDirectory, filename))) return `./${filename}`;
|
|
41389
41940
|
return null;
|
|
@@ -41755,7 +42306,7 @@ const scopeContainsNonImportBinding = (node, scopeNode, identifierName) => {
|
|
|
41755
42306
|
const isIdentifierShadowedByLocalBinding = (identifier, sourceFile) => {
|
|
41756
42307
|
let currentNode = identifier.parent;
|
|
41757
42308
|
while (currentNode) {
|
|
41758
|
-
if (
|
|
42309
|
+
if (isScopeBoundary(currentNode)) {
|
|
41759
42310
|
if (scopeContainsNonImportBinding(currentNode, currentNode, identifier.text)) return true;
|
|
41760
42311
|
}
|
|
41761
42312
|
if (currentNode === sourceFile) return false;
|
|
@@ -41846,11 +42397,10 @@ const findResolutionInScope = (scopeNode, identifierName, reactImportBindings, s
|
|
|
41846
42397
|
});
|
|
41847
42398
|
return resolution;
|
|
41848
42399
|
};
|
|
41849
|
-
const isScopeNode = isScopeBoundary;
|
|
41850
42400
|
const resolveIdentifierBinding = (identifier, reactImportBindings, sourceFile, visitedDeclarations = /* @__PURE__ */ new Set()) => {
|
|
41851
42401
|
let currentNode = identifier.parent;
|
|
41852
42402
|
while (currentNode) {
|
|
41853
|
-
if (
|
|
42403
|
+
if (isScopeBoundary(currentNode)) {
|
|
41854
42404
|
const resolution = findResolutionInScope(currentNode, identifier.text, reactImportBindings, sourceFile, visitedDeclarations);
|
|
41855
42405
|
if (resolution) return resolution;
|
|
41856
42406
|
}
|
|
@@ -42020,9 +42570,9 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
|
|
|
42020
42570
|
try {
|
|
42021
42571
|
parsed = JSON.parse(sanitizedStdout);
|
|
42022
42572
|
} catch {
|
|
42023
|
-
throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0,
|
|
42573
|
+
throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
|
|
42024
42574
|
}
|
|
42025
|
-
if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0,
|
|
42575
|
+
if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
|
|
42026
42576
|
const minifiedFileCache = /* @__PURE__ */ new Map();
|
|
42027
42577
|
const isMinifiedDiagnosticFile = (filename) => {
|
|
42028
42578
|
const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
|
|
@@ -42059,15 +42609,19 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
|
|
|
42059
42609
|
};
|
|
42060
42610
|
});
|
|
42061
42611
|
};
|
|
42062
|
-
const
|
|
42063
|
-
const
|
|
42064
|
-
for (const [name, value] of Object.entries(
|
|
42612
|
+
const buildOxlintChildEnv = (sourceEnv) => {
|
|
42613
|
+
const childEnv = {};
|
|
42614
|
+
for (const [name, value] of Object.entries(sourceEnv)) {
|
|
42065
42615
|
if (name === "NODE_OPTIONS" || name === "NODE_DEBUG") continue;
|
|
42066
42616
|
if (name.startsWith("npm_config_")) continue;
|
|
42067
|
-
|
|
42617
|
+
childEnv[name] = value;
|
|
42068
42618
|
}
|
|
42069
|
-
|
|
42070
|
-
|
|
42619
|
+
const isCompileCacheDisabled = Boolean(sourceEnv.NODE_DISABLE_COMPILE_CACHE);
|
|
42620
|
+
const isCompileCacheAlreadySet = childEnv.NODE_COMPILE_CACHE !== void 0;
|
|
42621
|
+
if (!isCompileCacheDisabled && !isCompileCacheAlreadySet) childEnv.NODE_COMPILE_CACHE = Path.join(os.tmpdir(), NODE_COMPILE_CACHE_DIR_NAME);
|
|
42622
|
+
return childEnv;
|
|
42623
|
+
};
|
|
42624
|
+
const SANITIZED_ENV = buildOxlintChildEnv(process.env);
|
|
42071
42625
|
/**
|
|
42072
42626
|
* Spawn one oxlint subprocess with hard ceilings on wall time and
|
|
42073
42627
|
* output size. Returns stdout on success; raises a tagged
|
|
@@ -42084,7 +42638,11 @@ const SANITIZED_ENV = (() => {
|
|
|
42084
42638
|
* The first three are splittable (the caller's binary-split retry
|
|
42085
42639
|
* shrinks the batch and re-spawns); the fourth isn't.
|
|
42086
42640
|
*/
|
|
42087
|
-
const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLINT_SPAWN_TIMEOUT_MS, outputMaxBytes = OXLINT_OUTPUT_MAX_BYTES) => new Promise((resolve, reject) => {
|
|
42641
|
+
const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLINT_SPAWN_TIMEOUT_MS, outputMaxBytes = OXLINT_OUTPUT_MAX_BYTES, abortSignal) => new Promise((resolve, reject) => {
|
|
42642
|
+
if (abortSignal?.aborted) {
|
|
42643
|
+
reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: "lint phase aborted" }) }));
|
|
42644
|
+
return;
|
|
42645
|
+
}
|
|
42088
42646
|
const child = spawn(nodeBinaryPath, args, {
|
|
42089
42647
|
cwd: rootDirectory,
|
|
42090
42648
|
env: SANITIZED_ENV,
|
|
@@ -42094,11 +42652,18 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
|
|
|
42094
42652
|
"pipe"
|
|
42095
42653
|
]
|
|
42096
42654
|
});
|
|
42655
|
+
const onAbort = () => {
|
|
42656
|
+
child.kill("SIGKILL");
|
|
42657
|
+
reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: "lint phase aborted" }) }));
|
|
42658
|
+
};
|
|
42659
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
42660
|
+
const clearAbortListener = () => abortSignal?.removeEventListener("abort", onAbort);
|
|
42097
42661
|
const timeoutHandle = setTimeout(() => {
|
|
42662
|
+
clearAbortListener();
|
|
42098
42663
|
child.kill("SIGKILL");
|
|
42099
42664
|
reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
|
|
42100
42665
|
kind: "timeout",
|
|
42101
|
-
detail: `${spawnTimeoutMs /
|
|
42666
|
+
detail: `${spawnTimeoutMs / MILLISECONDS_PER_SECOND}s budget exceeded`
|
|
42102
42667
|
}) }));
|
|
42103
42668
|
}, spawnTimeoutMs);
|
|
42104
42669
|
timeoutHandle.unref?.();
|
|
@@ -42129,10 +42694,12 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
|
|
|
42129
42694
|
});
|
|
42130
42695
|
child.on("error", (error) => {
|
|
42131
42696
|
clearTimeout(timeoutHandle);
|
|
42697
|
+
clearAbortListener();
|
|
42132
42698
|
reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: error }) }));
|
|
42133
42699
|
});
|
|
42134
42700
|
child.on("close", (_code, signal) => {
|
|
42135
42701
|
clearTimeout(timeoutHandle);
|
|
42702
|
+
clearAbortListener();
|
|
42136
42703
|
if (didKillForSize) {
|
|
42137
42704
|
reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
|
|
42138
42705
|
kind: "output-too-large",
|
|
@@ -42199,26 +42766,28 @@ const isParallelismRelatedSpawnError = (error) => {
|
|
|
42199
42766
|
* loop with a slimmer config in that case.
|
|
42200
42767
|
*/
|
|
42201
42768
|
const spawnLintBatches = async (input) => {
|
|
42202
|
-
const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes } = input;
|
|
42769
|
+
const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 8, signal } = input;
|
|
42203
42770
|
const requestedConcurrency = resolveScanConcurrency(input.concurrency ?? 1);
|
|
42204
42771
|
const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
|
|
42205
42772
|
const runBatchPass = async (concurrency) => {
|
|
42206
42773
|
const allDiagnostics = [];
|
|
42207
42774
|
const droppedFiles = [];
|
|
42208
42775
|
let firstDropReason = null;
|
|
42209
|
-
const
|
|
42776
|
+
const splitDeadlineMs = Date.now() + splitTotalBudgetMs;
|
|
42777
|
+
const spawnLintBatch = async (batch, depth) => {
|
|
42210
42778
|
const batchArgs = [...baseArgs, ...batch];
|
|
42211
42779
|
try {
|
|
42212
|
-
return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes), project, rootDirectory);
|
|
42780
|
+
return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes, signal), project, rootDirectory);
|
|
42213
42781
|
} catch (error) {
|
|
42214
42782
|
if (!isSplittableReactDoctorError(error)) throw error;
|
|
42215
|
-
|
|
42783
|
+
const splitBudgetExhausted = Date.now() >= splitDeadlineMs || depth >= splitMaxDepth;
|
|
42784
|
+
if (batch.length <= 1 || splitBudgetExhausted) {
|
|
42216
42785
|
droppedFiles.push(...batch);
|
|
42217
|
-
if (firstDropReason === null) firstDropReason = error.message;
|
|
42786
|
+
if (firstDropReason === null) firstDropReason = splitBudgetExhausted && batch.length > 1 ? `${error.message} (split budget exhausted after ${splitMaxDepth} levels / ${splitTotalBudgetMs / MILLISECONDS_PER_SECOND}s)` : error.message;
|
|
42218
42787
|
return [];
|
|
42219
42788
|
}
|
|
42220
42789
|
const splitIndex = Math.ceil(batch.length / 2);
|
|
42221
|
-
return [...await spawnLintBatch(batch.slice(0, splitIndex)), ...await spawnLintBatch(batch.slice(splitIndex))];
|
|
42790
|
+
return [...await spawnLintBatch(batch.slice(0, splitIndex), depth + 1), ...await spawnLintBatch(batch.slice(splitIndex), depth + 1)];
|
|
42222
42791
|
}
|
|
42223
42792
|
};
|
|
42224
42793
|
let startedFileCount = 0;
|
|
@@ -42235,7 +42804,7 @@ const spawnLintBatches = async (input) => {
|
|
|
42235
42804
|
try {
|
|
42236
42805
|
const batchResults = await mapWithConcurrency(fileBatches, concurrency, async (batch) => {
|
|
42237
42806
|
startedFileCount += batch.length;
|
|
42238
|
-
const batchDiagnostics = await spawnLintBatch(batch);
|
|
42807
|
+
const batchDiagnostics = await spawnLintBatch(batch, 0);
|
|
42239
42808
|
scannedFileCount += batch.length;
|
|
42240
42809
|
if (onFileProgress) {
|
|
42241
42810
|
displayedFileCount = Math.min(Math.max(displayedFileCount, scannedFileCount), totalFileCount);
|
|
@@ -42296,6 +42865,22 @@ const validateRuleRegistration = () => {
|
|
|
42296
42865
|
].filter((entry) => entry !== null).join("; ");
|
|
42297
42866
|
console.warn(`[react-doctor] rule-registration drift: ${detail}`);
|
|
42298
42867
|
};
|
|
42868
|
+
const hashFileContents = (filePath) => {
|
|
42869
|
+
try {
|
|
42870
|
+
return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
|
|
42871
|
+
} catch {
|
|
42872
|
+
return null;
|
|
42873
|
+
}
|
|
42874
|
+
};
|
|
42875
|
+
const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
|
|
42876
|
+
const resolveReactDoctorCacheDir = (projectDirectory) => {
|
|
42877
|
+
const cacheDirOverride = process.env["REACT_DOCTOR_CACHE_DIR"]?.trim();
|
|
42878
|
+
if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
|
|
42879
|
+
const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
|
|
42880
|
+
if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
|
|
42881
|
+
return Path.join(os.tmpdir(), "react-doctor-cache", projectCacheSubdir(projectDirectory));
|
|
42882
|
+
};
|
|
42883
|
+
const sortSourceFilesByCost = (entries) => [...entries].sort((left, right) => right.sizeBytes - left.sizeBytes).map((entry) => entry.path);
|
|
42299
42884
|
/**
|
|
42300
42885
|
* Atomically (re)writes the generated oxlintrc.json. Used twice in
|
|
42301
42886
|
* the runner: once for the primary scan, once for the
|
|
@@ -42313,6 +42898,28 @@ const writeOxlintConfig = (configPath, configToWrite) => {
|
|
|
42313
42898
|
NFS.closeSync(fileHandle);
|
|
42314
42899
|
}
|
|
42315
42900
|
};
|
|
42901
|
+
const REACT_HOOKS_JS_DROP_PREFIX = "React Compiler rules (react-hooks-js/*) skipped — eslint-plugin-react-hooks failed to load in this environment";
|
|
42902
|
+
/**
|
|
42903
|
+
* Detects an oxlint config-load crash caused by the optional
|
|
42904
|
+
* `react-hooks-js` (eslint-plugin-react-hooks) React Compiler plugin and
|
|
42905
|
+
* builds the partial-failure note for it; returns `null` when the failure
|
|
42906
|
+
* was anything else.
|
|
42907
|
+
*
|
|
42908
|
+
* oxlint prints a framed error to stdout (not stderr) and exits non-zero
|
|
42909
|
+
* when a `jsPlugins` entry can't be imported; that non-JSON stdout
|
|
42910
|
+
* surfaces as `OxlintOutputUnparseable`. Because oxlint fails the WHOLE
|
|
42911
|
+
* config load on it, leaving the plugin in would drop every curated
|
|
42912
|
+
* react-doctor diagnostic too — so the caller retries with the plugin
|
|
42913
|
+
* stripped (issue #833). Both markers sit at the start of oxlint's
|
|
42914
|
+
* message, so they survive the `preview` slice even for deep pnpm paths.
|
|
42915
|
+
*/
|
|
42916
|
+
const reactHooksJsPluginDropNote = (error) => {
|
|
42917
|
+
if (!(error instanceof ReactDoctorError) || error.reason._tag !== "OxlintOutputUnparseable") return null;
|
|
42918
|
+
const { preview } = error.reason;
|
|
42919
|
+
if (!preview.includes("Failed to load JS plugin") || !preview.includes("eslint-plugin-react-hooks")) return null;
|
|
42920
|
+
const underlyingReason = preview.match(/Error:[^\n]*/)?.[0]?.trim();
|
|
42921
|
+
return `${REACT_HOOKS_JS_DROP_PREFIX}${underlyingReason ? `: ${underlyingReason}` : ""}. Other rules ran normally.`;
|
|
42922
|
+
};
|
|
42316
42923
|
/**
|
|
42317
42924
|
* The oxlint runner. Composed of three pieces in `runners/oxlint/`:
|
|
42318
42925
|
*
|
|
@@ -42332,7 +42939,7 @@ const writeOxlintConfig = (configPath, configToWrite) => {
|
|
|
42332
42939
|
* 6. always restore disable directives + clean up the temp dir
|
|
42333
42940
|
*/
|
|
42334
42941
|
const runOxlint = async (options) => {
|
|
42335
|
-
const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure, spawnTimeoutMs, outputMaxBytes } = options;
|
|
42942
|
+
const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure, perFileLintCacheEnabled = false, onCacheStats, spawnTimeoutMs, outputMaxBytes, lintBatchOrdering = "arrival" } = options;
|
|
42336
42943
|
const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
|
|
42337
42944
|
const severityControls = buildRuleSeverityControls(userConfig);
|
|
42338
42945
|
validateRuleRegistration();
|
|
@@ -42340,38 +42947,165 @@ const runOxlint = async (options) => {
|
|
|
42340
42947
|
const pluginPath = resolvePluginPath();
|
|
42341
42948
|
const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
|
|
42342
42949
|
const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
|
|
42343
|
-
const buildConfig = (
|
|
42950
|
+
const buildConfig = (overrides) => createOxlintConfig({
|
|
42344
42951
|
pluginPath,
|
|
42345
42952
|
project,
|
|
42346
42953
|
customRulesOnly,
|
|
42347
|
-
extendsPaths:
|
|
42954
|
+
extendsPaths: overrides.extendsPaths,
|
|
42348
42955
|
ignoredTags,
|
|
42349
42956
|
serverAuthFunctionNames,
|
|
42350
42957
|
severityControls,
|
|
42351
|
-
userPlugins
|
|
42958
|
+
userPlugins,
|
|
42959
|
+
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin,
|
|
42960
|
+
ruleSelection: overrides.ruleSelection
|
|
42352
42961
|
});
|
|
42353
42962
|
const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
|
|
42354
42963
|
const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
|
|
42355
42964
|
const configPath = Path.join(configDirectory, "oxlintrc.json");
|
|
42356
42965
|
try {
|
|
42357
|
-
const
|
|
42358
|
-
|
|
42359
|
-
|
|
42360
|
-
configPath,
|
|
42361
|
-
"--format",
|
|
42362
|
-
"json"
|
|
42363
|
-
];
|
|
42966
|
+
const oxlintBinary = resolveOxlintBinary();
|
|
42967
|
+
const sharedArgs = [];
|
|
42968
|
+
let tsconfigContent = null;
|
|
42364
42969
|
if (project.hasTypeScript) {
|
|
42365
42970
|
const tsconfigRelativePath = resolveTsConfigRelativePath(rootDirectory);
|
|
42366
|
-
if (tsconfigRelativePath)
|
|
42971
|
+
if (tsconfigRelativePath) {
|
|
42972
|
+
sharedArgs.push("--tsconfig", tsconfigRelativePath);
|
|
42973
|
+
try {
|
|
42974
|
+
tsconfigContent = NFS.readFileSync(Path.resolve(rootDirectory, tsconfigRelativePath), "utf8");
|
|
42975
|
+
} catch {
|
|
42976
|
+
tsconfigContent = null;
|
|
42977
|
+
}
|
|
42978
|
+
}
|
|
42367
42979
|
}
|
|
42368
42980
|
const combinedPatterns = collectIgnorePatterns(rootDirectory);
|
|
42369
42981
|
if (combinedPatterns.length > 0) {
|
|
42370
42982
|
const combinedIgnorePath = Path.join(configDirectory, "combined.ignore");
|
|
42371
42983
|
NFS.writeFileSync(combinedIgnorePath, `${combinedPatterns.join("\n")}\n`);
|
|
42372
|
-
|
|
42984
|
+
sharedArgs.push("--ignore-path", combinedIgnorePath);
|
|
42373
42985
|
}
|
|
42374
|
-
const
|
|
42986
|
+
const makeBaseArgs = (oxlintConfigPath) => [
|
|
42987
|
+
oxlintBinary,
|
|
42988
|
+
"-c",
|
|
42989
|
+
oxlintConfigPath,
|
|
42990
|
+
"--format",
|
|
42991
|
+
"json",
|
|
42992
|
+
...sharedArgs
|
|
42993
|
+
];
|
|
42994
|
+
const discoverScanFiles = () => lintBatchOrdering === "cost" ? sortSourceFilesByCost(listSourceFilesWithSize(rootDirectory)) : listSourceFiles(rootDirectory);
|
|
42995
|
+
const candidateFiles = includePaths !== void 0 ? includePaths : discoverScanFiles();
|
|
42996
|
+
const runConfigOverFiles = async (buildConfigForPass, configFileName, files, fileProgress) => {
|
|
42997
|
+
if (files.length === 0) return {
|
|
42998
|
+
diagnostics: [],
|
|
42999
|
+
didDropReactHooksJsPlugin: false,
|
|
43000
|
+
hadPartialFailure: false
|
|
43001
|
+
};
|
|
43002
|
+
let hadPartialFailure = false;
|
|
43003
|
+
const reportPartialFailure = (reason) => {
|
|
43004
|
+
hadPartialFailure = true;
|
|
43005
|
+
onPartialFailure?.(reason);
|
|
43006
|
+
};
|
|
43007
|
+
const passConfigPath = Path.join(configDirectory, configFileName);
|
|
43008
|
+
const passBaseArgs = makeBaseArgs(passConfigPath);
|
|
43009
|
+
const passFileBatches = batchIncludePaths(passBaseArgs, files);
|
|
43010
|
+
const spawnPass = () => spawnLintBatches({
|
|
43011
|
+
baseArgs: passBaseArgs,
|
|
43012
|
+
fileBatches: passFileBatches,
|
|
43013
|
+
rootDirectory,
|
|
43014
|
+
nodeBinaryPath,
|
|
43015
|
+
project,
|
|
43016
|
+
onPartialFailure: reportPartialFailure,
|
|
43017
|
+
onFileProgress: fileProgress,
|
|
43018
|
+
spawnTimeoutMs,
|
|
43019
|
+
outputMaxBytes,
|
|
43020
|
+
concurrency: options.concurrency,
|
|
43021
|
+
signal: options.signal
|
|
43022
|
+
});
|
|
43023
|
+
writeOxlintConfig(passConfigPath, buildConfigForPass({}));
|
|
43024
|
+
try {
|
|
43025
|
+
return {
|
|
43026
|
+
diagnostics: await spawnPass(),
|
|
43027
|
+
didDropReactHooksJsPlugin: false,
|
|
43028
|
+
hadPartialFailure
|
|
43029
|
+
};
|
|
43030
|
+
} catch (error) {
|
|
43031
|
+
const reactHooksJsDropNote = reactHooksJsPluginDropNote(error);
|
|
43032
|
+
if (reactHooksJsDropNote === null) throw error;
|
|
43033
|
+
writeOxlintConfig(passConfigPath, buildConfigForPass({ disableReactHooksJsPlugin: true }));
|
|
43034
|
+
const diagnostics = await spawnPass();
|
|
43035
|
+
reportPartialFailure(reactHooksJsDropNote);
|
|
43036
|
+
return {
|
|
43037
|
+
diagnostics,
|
|
43038
|
+
didDropReactHooksJsPlugin: true,
|
|
43039
|
+
hadPartialFailure
|
|
43040
|
+
};
|
|
43041
|
+
}
|
|
43042
|
+
};
|
|
43043
|
+
if (perFileLintCacheEnabled && respectInlineDisables && !project.hasReactCompiler && extendsPaths.length === 0 && userPlugins.length === 0) {
|
|
43044
|
+
const rulesetHash = computeRulesetHash({
|
|
43045
|
+
config: buildConfig({
|
|
43046
|
+
extendsPaths: [],
|
|
43047
|
+
ruleSelection: "cacheable"
|
|
43048
|
+
}),
|
|
43049
|
+
toolchainVersions: resolveOxlintToolchainVersions(),
|
|
43050
|
+
ignorePatterns: combinedPatterns,
|
|
43051
|
+
tsconfigContent
|
|
43052
|
+
});
|
|
43053
|
+
const cache = createFileLintCache(resolveReactDoctorCacheDir(rootDirectory), rulesetHash);
|
|
43054
|
+
const cacheKeyByFile = /* @__PURE__ */ new Map();
|
|
43055
|
+
const missFiles = [];
|
|
43056
|
+
const replayedDiagnostics = [];
|
|
43057
|
+
for (const candidateFile of candidateFiles) {
|
|
43058
|
+
const contentHash = hashFileContents(Path.resolve(rootDirectory, candidateFile));
|
|
43059
|
+
if (contentHash === null) {
|
|
43060
|
+
missFiles.push(candidateFile);
|
|
43061
|
+
continue;
|
|
43062
|
+
}
|
|
43063
|
+
const cacheKey = `${candidateFile.replaceAll("\\", "/")}${contentHash}`;
|
|
43064
|
+
cacheKeyByFile.set(candidateFile, cacheKey);
|
|
43065
|
+
const cachedDiagnostics = cache.lookup(cacheKey);
|
|
43066
|
+
if (cachedDiagnostics === null) missFiles.push(candidateFile);
|
|
43067
|
+
else replayedDiagnostics.push(...cachedDiagnostics);
|
|
43068
|
+
}
|
|
43069
|
+
const cacheHitFileCount = candidateFiles.length - missFiles.length;
|
|
43070
|
+
const cacheableResult = await runConfigOverFiles((overrides) => buildConfig({
|
|
43071
|
+
extendsPaths: [],
|
|
43072
|
+
ruleSelection: "cacheable",
|
|
43073
|
+
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
|
|
43074
|
+
}), "oxlintrc.cacheable.json", missFiles, void 0);
|
|
43075
|
+
const sidecarResult = await runConfigOverFiles(() => buildConfig({
|
|
43076
|
+
extendsPaths: [],
|
|
43077
|
+
ruleSelection: "sidecar"
|
|
43078
|
+
}), "oxlintrc.sidecar.json", candidateFiles, options.onFileProgress);
|
|
43079
|
+
onCacheStats?.(cacheHitFileCount, candidateFiles.length);
|
|
43080
|
+
const missFileByNormalizedPath = /* @__PURE__ */ new Map();
|
|
43081
|
+
for (const missFile of missFiles) missFileByNormalizedPath.set(missFile.replaceAll("\\", "/"), missFile);
|
|
43082
|
+
const freshDiagnosticsByFile = /* @__PURE__ */ new Map();
|
|
43083
|
+
let isAttributionSound = true;
|
|
43084
|
+
for (const diagnostic of cacheableResult.diagnostics) {
|
|
43085
|
+
const missFile = missFileByNormalizedPath.get(diagnostic.filePath);
|
|
43086
|
+
if (missFile === void 0) {
|
|
43087
|
+
isAttributionSound = false;
|
|
43088
|
+
break;
|
|
43089
|
+
}
|
|
43090
|
+
const fileDiagnostics = freshDiagnosticsByFile.get(missFile) ?? [];
|
|
43091
|
+
fileDiagnostics.push(diagnostic);
|
|
43092
|
+
freshDiagnosticsByFile.set(missFile, fileDiagnostics);
|
|
43093
|
+
}
|
|
43094
|
+
if (!cacheableResult.didDropReactHooksJsPlugin && !cacheableResult.hadPartialFailure && isAttributionSound) {
|
|
43095
|
+
for (const missFile of missFiles) {
|
|
43096
|
+
const cacheKey = cacheKeyByFile.get(missFile);
|
|
43097
|
+
if (cacheKey !== void 0) cache.store(cacheKey, freshDiagnosticsByFile.get(missFile) ?? []);
|
|
43098
|
+
}
|
|
43099
|
+
cache.persist();
|
|
43100
|
+
}
|
|
43101
|
+
return dedupeDiagnostics([
|
|
43102
|
+
...replayedDiagnostics,
|
|
43103
|
+
...cacheableResult.diagnostics,
|
|
43104
|
+
...sidecarResult.diagnostics
|
|
43105
|
+
]);
|
|
43106
|
+
}
|
|
43107
|
+
const baseArgs = makeBaseArgs(configPath);
|
|
43108
|
+
const fileBatches = batchIncludePaths(baseArgs, candidateFiles);
|
|
42375
43109
|
const runBatches = () => spawnLintBatches({
|
|
42376
43110
|
baseArgs,
|
|
42377
43111
|
fileBatches,
|
|
@@ -42382,14 +43116,25 @@ const runOxlint = async (options) => {
|
|
|
42382
43116
|
onFileProgress: options.onFileProgress,
|
|
42383
43117
|
spawnTimeoutMs,
|
|
42384
43118
|
outputMaxBytes,
|
|
42385
|
-
concurrency: options.concurrency
|
|
43119
|
+
concurrency: options.concurrency,
|
|
43120
|
+
signal: options.signal
|
|
42386
43121
|
});
|
|
42387
|
-
writeOxlintConfig(configPath, buildConfig(extendsPaths));
|
|
43122
|
+
writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
|
|
42388
43123
|
try {
|
|
42389
43124
|
return await runBatches();
|
|
42390
43125
|
} catch (error) {
|
|
43126
|
+
const reactHooksJsDropNote = reactHooksJsPluginDropNote(error);
|
|
43127
|
+
if (reactHooksJsDropNote !== null) {
|
|
43128
|
+
writeOxlintConfig(configPath, buildConfig({
|
|
43129
|
+
extendsPaths,
|
|
43130
|
+
disableReactHooksJsPlugin: true
|
|
43131
|
+
}));
|
|
43132
|
+
const diagnostics = await runBatches();
|
|
43133
|
+
onPartialFailure?.(reactHooksJsDropNote);
|
|
43134
|
+
return diagnostics;
|
|
43135
|
+
}
|
|
42391
43136
|
if (extendsPaths.length === 0) throw error;
|
|
42392
|
-
writeOxlintConfig(configPath, buildConfig([]));
|
|
43137
|
+
writeOxlintConfig(configPath, buildConfig({ extendsPaths: [] }));
|
|
42393
43138
|
return await runBatches();
|
|
42394
43139
|
}
|
|
42395
43140
|
} finally {
|
|
@@ -42451,9 +43196,11 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
|
|
|
42451
43196
|
const spawnTimeoutMs = yield* OxlintSpawnTimeoutMs;
|
|
42452
43197
|
const outputMaxBytes = yield* OxlintOutputMaxBytes;
|
|
42453
43198
|
const concurrency = yield* OxlintConcurrency;
|
|
43199
|
+
const lintBatchOrdering = yield* LintBatchOrdering;
|
|
43200
|
+
const perFileLintCacheEnabled = yield* PerFileLintCacheEnabled;
|
|
42454
43201
|
const collectedFailures = [];
|
|
42455
43202
|
const diagnostics = yield* tryPromise({
|
|
42456
|
-
try: () => runOxlint({
|
|
43203
|
+
try: (signal) => runOxlint({
|
|
42457
43204
|
rootDirectory: input.rootDirectory,
|
|
42458
43205
|
project: input.project,
|
|
42459
43206
|
includePaths: input.includePaths ? [...input.includePaths] : void 0,
|
|
@@ -42468,9 +43215,13 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
|
|
|
42468
43215
|
collectedFailures.push(reason);
|
|
42469
43216
|
},
|
|
42470
43217
|
onFileProgress: input.onFileProgress,
|
|
43218
|
+
perFileLintCacheEnabled,
|
|
43219
|
+
onCacheStats: input.onCacheStats,
|
|
42471
43220
|
spawnTimeoutMs,
|
|
42472
43221
|
outputMaxBytes,
|
|
42473
|
-
concurrency
|
|
43222
|
+
concurrency,
|
|
43223
|
+
signal,
|
|
43224
|
+
lintBatchOrdering
|
|
42474
43225
|
}),
|
|
42475
43226
|
catch: ensureReactDoctorError
|
|
42476
43227
|
});
|
|
@@ -42862,14 +43613,49 @@ const parseArtifactFromBody = (body) => {
|
|
|
42862
43613
|
}
|
|
42863
43614
|
return null;
|
|
42864
43615
|
};
|
|
42865
|
-
const
|
|
43616
|
+
const isSupplyChainCacheDisabled = () => {
|
|
43617
|
+
const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
|
|
43618
|
+
return noCache === "1" || noCache === "true";
|
|
43619
|
+
};
|
|
43620
|
+
const supplyChainCacheFile = (cacheDirectory, dependency) => {
|
|
43621
|
+
const purlHash = crypto.createHash("sha256").update(toPurl(dependency)).digest("hex").slice(0, 16);
|
|
43622
|
+
return Path.join(cacheDirectory, SUPPLY_CHAIN_CACHE_SUBDIR, `${purlHash}.json`);
|
|
43623
|
+
};
|
|
43624
|
+
const readCachedSocketBody = (cacheFile) => {
|
|
43625
|
+
try {
|
|
43626
|
+
const entry = JSON.parse(NFS.readFileSync(cacheFile, "utf-8"));
|
|
43627
|
+
if (typeof entry === "object" && entry !== null && "fetchedAtMs" in entry && "body" in entry && typeof entry.fetchedAtMs === "number" && typeof entry.body === "string" && Date.now() - entry.fetchedAtMs <= 864e5) return entry.body;
|
|
43628
|
+
} catch {}
|
|
43629
|
+
return null;
|
|
43630
|
+
};
|
|
43631
|
+
const writeCachedSocketBody = (cacheFile, body) => {
|
|
43632
|
+
try {
|
|
43633
|
+
NFS.mkdirSync(Path.dirname(cacheFile), { recursive: true });
|
|
43634
|
+
NFS.writeFileSync(cacheFile, JSON.stringify({
|
|
43635
|
+
fetchedAtMs: Date.now(),
|
|
43636
|
+
body
|
|
43637
|
+
}));
|
|
43638
|
+
} catch {}
|
|
43639
|
+
};
|
|
43640
|
+
const fetchSocketArtifact = (dependency, cacheDirectory) => tryPromise(async (signal) => {
|
|
43641
|
+
const cacheFile = cacheDirectory === null ? null : supplyChainCacheFile(cacheDirectory, dependency);
|
|
43642
|
+
if (cacheFile !== null) {
|
|
43643
|
+
const cachedBody = readCachedSocketBody(cacheFile);
|
|
43644
|
+
if (cachedBody !== null) {
|
|
43645
|
+
const cachedArtifact = parseArtifactFromBody(cachedBody);
|
|
43646
|
+
if (cachedArtifact !== null) return cachedArtifact;
|
|
43647
|
+
}
|
|
43648
|
+
}
|
|
42866
43649
|
const requestUrl = `${SOCKET_FREE_PURL_API_BASE}/${encodeURIComponent(toPurl(dependency))}`;
|
|
42867
43650
|
const response = await fetch(requestUrl, {
|
|
42868
43651
|
headers: { "User-Agent": SOCKET_FREE_USER_AGENT },
|
|
42869
43652
|
signal
|
|
42870
43653
|
});
|
|
42871
43654
|
if (!response.ok) return null;
|
|
42872
|
-
|
|
43655
|
+
const body = await response.text();
|
|
43656
|
+
const artifact = parseArtifactFromBody(body);
|
|
43657
|
+
if (artifact !== null && cacheFile !== null) writeCachedSocketBody(cacheFile, body);
|
|
43658
|
+
return artifact;
|
|
42873
43659
|
}).pipe(timeout(FETCH_TIMEOUT_MS), orElseSucceed(() => null), tap$1((artifact) => {
|
|
42874
43660
|
const scoreAttributes = {};
|
|
42875
43661
|
if (artifact !== null) {
|
|
@@ -42974,7 +43760,8 @@ const checkSupplyChain = (input) => gen(function* () {
|
|
|
42974
43760
|
const packageJsonPath = Path.join(input.rootDirectory, "package.json");
|
|
42975
43761
|
const dependencies = collectDependenciesToScore(readPackageJson$1(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies);
|
|
42976
43762
|
if (dependencies.length === 0) return [];
|
|
42977
|
-
const
|
|
43763
|
+
const cacheDirectory = isSupplyChainCacheDisabled() ? null : resolveReactDoctorCacheDir(input.rootDirectory);
|
|
43764
|
+
const artifacts = yield* forEach$1(dependencies, (dependency) => fetchSocketArtifact(dependency, cacheDirectory), { concurrency: 8 }).pipe(timeoutOption(input.totalTimeoutMs ?? 9e4), map$3((maybeArtifacts) => getOrElse$1(maybeArtifacts, () => [])));
|
|
42978
43765
|
const diagnostics = [];
|
|
42979
43766
|
for (let index = 0; index < dependencies.length; index += 1) {
|
|
42980
43767
|
const artifact = artifacts[index];
|
|
@@ -42999,6 +43786,10 @@ const checkSupplyChain = (input) => gen(function* () {
|
|
|
42999
43786
|
* The underlying `checkSupplyChain` Effect is total/fail-open — per-package
|
|
43000
43787
|
* timeouts and network failures recover to "skip" — so the stream never
|
|
43001
43788
|
* fails, mirroring `DeadCode`'s stream shape so the two compose the same way.
|
|
43789
|
+
* The orchestrator (`run-inspect.ts`) consumes this stream on a background
|
|
43790
|
+
* fiber whose network time overlaps the lint pass, joined under a generous
|
|
43791
|
+
* wall-clock budget; a budget expiry is the same fail-open outcome as a Socket
|
|
43792
|
+
* outage.
|
|
43002
43793
|
*/
|
|
43003
43794
|
var SupplyChain = class SupplyChain extends Service()("react-doctor/SupplyChain") {
|
|
43004
43795
|
static layerNode = succeed$3(SupplyChain, SupplyChain.of({ run: (input) => unwrap(checkSupplyChain(input).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), withSpan("SupplyChain.run"))) }));
|
|
@@ -43057,18 +43848,42 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
|
|
|
43057
43848
|
*
|
|
43058
43849
|
* Phases:
|
|
43059
43850
|
*
|
|
43060
|
-
* 1. Config.resolve(directory) → Project.discover → Git metadata
|
|
43851
|
+
* 1. Config.resolve(directory) → Project.discover → Git metadata.
|
|
43852
|
+
* The GitHub viewer-permission lookup is forked onto a background
|
|
43853
|
+
* fiber here and joined late (it feeds score metadata, not
|
|
43854
|
+
* diagnostics).
|
|
43061
43855
|
* 2. beforeLint hook (e.g. CLI renders the project-detection block)
|
|
43062
43856
|
* 3. environment checks (reduced-motion + pnpm hardening +
|
|
43063
|
-
* expo/react-native + security scan)
|
|
43064
|
-
* 4.
|
|
43065
|
-
*
|
|
43066
|
-
*
|
|
43067
|
-
*
|
|
43068
|
-
*
|
|
43069
|
-
*
|
|
43070
|
-
*
|
|
43071
|
-
*
|
|
43857
|
+
* expo/react-native + security scan), collected synchronously
|
|
43858
|
+
* 4. The supply-chain check (Socket.dev) is forked onto a background
|
|
43859
|
+
* fiber so its ~100% network-bound time overlaps the ~100%
|
|
43860
|
+
* CPU/subprocess-bound lint pass below, collapsing two serial
|
|
43861
|
+
* phases into roughly `max(supplyChain, lint)`. It is capped by
|
|
43862
|
+
* `SupplyChainOverlapTimeoutMs` (measured from fork) so a hung
|
|
43863
|
+
* socket can't drag out its join; on timeout it fails open to no
|
|
43864
|
+
* diagnostics — the same outcome class as a Socket outage.
|
|
43865
|
+
* 5. Linter.run runs; DeadCode.run runs concurrently (forked child
|
|
43866
|
+
* fiber) ONLY when the memory gate has headroom to run the 8 GB
|
|
43867
|
+
* dead-code child alongside the oxlint workers — or when overlap is
|
|
43868
|
+
* forced via REACT_DOCTOR_DEAD_CODE_OVERLAP. Otherwise dead-code
|
|
43869
|
+
* runs sequentially after lint, exactly as it did pre-overlap. The
|
|
43870
|
+
* fiber is joined (or interrupted, SIGKILLing its worker, on lint
|
|
43871
|
+
* failure) before diagnostics are concatenated. The afterLint hook
|
|
43872
|
+
* fires between lint and dead-code. Progress spinner labels AND the
|
|
43873
|
+
* final diagnostic / score order stay independent of execution
|
|
43874
|
+
* order, so terminal output is identical either way; supply-chain
|
|
43875
|
+
* rides alongside without a spinner.
|
|
43876
|
+
* 6. Join the supply-chain fiber, then assemble the diagnostics in a
|
|
43877
|
+
* FIXED order (env, supply-chain, lint, dead-code) so the output is
|
|
43878
|
+
* byte-identical regardless of which fiber settled first. The
|
|
43879
|
+
* viewer-permission fiber is joined later, during score-metadata
|
|
43880
|
+
* assembly (it feeds score metadata, not diagnostics). The per-element
|
|
43881
|
+
* `Reporter.emit` side-channel now interleaves supply-chain with lint
|
|
43882
|
+
* emits, so capture-order assertions must target the deterministic
|
|
43883
|
+
* concat below, not emit order (production `Reporter.layerNoop` makes
|
|
43884
|
+
* emit a no-op).
|
|
43885
|
+
* 7. Reporter.finalize
|
|
43886
|
+
* 8. Score.compute against the surface-filtered diagnostic set
|
|
43072
43887
|
*
|
|
43073
43888
|
* The orchestrator owns spinner lifecycle via `Progress`; callers
|
|
43074
43889
|
* choose `Progress.layerOra(...)` for CLI feedback or
|
|
@@ -43126,10 +43941,21 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
43126
43941
|
ignoredTags: input.ignoredTags
|
|
43127
43942
|
})
|
|
43128
43943
|
])));
|
|
43129
|
-
const
|
|
43944
|
+
const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
|
|
43945
|
+
const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
|
|
43946
|
+
const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
|
|
43130
43947
|
rootDirectory: scanDirectory,
|
|
43131
43948
|
userConfig: resolvedConfig.config
|
|
43132
|
-
})))
|
|
43949
|
+
}))).pipe(map$3((diagnostics) => ({
|
|
43950
|
+
diagnostics,
|
|
43951
|
+
timedOut: false
|
|
43952
|
+
})), timeout(supplyChainOverlapTimeout), orElseSucceed(() => ({
|
|
43953
|
+
diagnostics: [],
|
|
43954
|
+
timedOut: true
|
|
43955
|
+
}))) : succeed$2({
|
|
43956
|
+
diagnostics: [],
|
|
43957
|
+
timedOut: false
|
|
43958
|
+
}));
|
|
43133
43959
|
const lintFailure = yield* make$13({
|
|
43134
43960
|
didFail: false,
|
|
43135
43961
|
reason: null,
|
|
@@ -43140,12 +43966,49 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
43140
43966
|
didFail: false,
|
|
43141
43967
|
reason: null
|
|
43142
43968
|
});
|
|
43143
|
-
const scanConcurrency = yield* OxlintConcurrency;
|
|
43969
|
+
const scanConcurrency = resolveScanConcurrency(yield* OxlintConcurrency);
|
|
43970
|
+
const lintPhaseTimeoutMs = yield* LintPhaseTimeoutMs;
|
|
43971
|
+
const deadCodePhaseTimeoutMs = yield* DeadCodePhaseTimeoutMs;
|
|
43972
|
+
const resolveDeadCodePhaseTimeoutMs = (scaledPhaseTimeoutMs) => deadCodePhaseTimeoutMs === 15e4 ? scaledPhaseTimeoutMs : deadCodePhaseTimeoutMs;
|
|
43144
43973
|
const workerCountSuffix = scanConcurrency > 1 ? ` ${highlighter.dim(`[~${scanConcurrency} workers]`)}` : "";
|
|
43974
|
+
const shouldRunDeadCode = input.runDeadCode && !isDiffMode && (showWarnings || deadCodeMaySurfaceWhenWarningsHidden(resolvedConfig.config));
|
|
43975
|
+
const deadCodeOverlapMode = yield* DeadCodeOverlap;
|
|
43976
|
+
const shouldOverlapDeadCode = shouldRunDeadCode && deadCodeOverlapMode === "on";
|
|
43977
|
+
const deadCodeParseConcurrency = shouldOverlapDeadCode ? Math.max(1, Math.floor(scanConcurrency * DEAD_CODE_OVERLAP_PARSE_SHARE)) : void 0;
|
|
43978
|
+
const lintConcurrency = deadCodeParseConcurrency === void 0 ? scanConcurrency : Math.max(1, scanConcurrency - deadCodeParseConcurrency);
|
|
43979
|
+
const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
|
|
43980
|
+
rootDirectory: scanDirectory,
|
|
43981
|
+
userConfig: resolvedConfig.config,
|
|
43982
|
+
parseConcurrency: deadCodeParseConcurrency,
|
|
43983
|
+
workerTimeoutMs: deadCodeTimeout.workerTimeoutMs
|
|
43984
|
+
}).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
|
|
43985
|
+
yield* set(deadCodeFailure, {
|
|
43986
|
+
didFail: true,
|
|
43987
|
+
reason: error.message
|
|
43988
|
+
});
|
|
43989
|
+
return empty$4;
|
|
43990
|
+
})))))).pipe(timeoutOption(deadCodeTimeout.phaseTimeoutMs), flatMap$2(match$3({
|
|
43991
|
+
onNone: () => set(deadCodeFailure, {
|
|
43992
|
+
didFail: true,
|
|
43993
|
+
reason: `Dead-code analysis exceeded ${Math.round(deadCodeTimeout.phaseTimeoutMs / MILLISECONDS_PER_SECOND)}s and was skipped.`
|
|
43994
|
+
}).pipe(as([])),
|
|
43995
|
+
onSome: succeed$2
|
|
43996
|
+
})));
|
|
43997
|
+
const overlapDeadCodeTimeout = resolveDeadCodeTimeout({
|
|
43998
|
+
sourceFileCount: project.sourceFileCount,
|
|
43999
|
+
deadCodeConcurrency: deadCodeParseConcurrency ?? scanConcurrency,
|
|
44000
|
+
fullConcurrency: scanConcurrency
|
|
44001
|
+
});
|
|
44002
|
+
const deadCodeFiber = shouldOverlapDeadCode ? yield* forkChild(buildCollectDeadCode({
|
|
44003
|
+
workerTimeoutMs: overlapDeadCodeTimeout.workerTimeoutMs,
|
|
44004
|
+
phaseTimeoutMs: resolveDeadCodePhaseTimeoutMs(overlapDeadCodeTimeout.phaseTimeoutMs)
|
|
44005
|
+
})) : null;
|
|
43145
44006
|
const scanProgress = yield* progressService.start("Scanning...");
|
|
43146
44007
|
const scanStartTime = Date.now();
|
|
43147
44008
|
let lastReportedTotalFileCount = 0;
|
|
43148
|
-
|
|
44009
|
+
let lintCacheHitFileCount = null;
|
|
44010
|
+
let lintCacheTotalFileCount = null;
|
|
44011
|
+
const baseLintStream = linterService.run({
|
|
43149
44012
|
rootDirectory: scanDirectory,
|
|
43150
44013
|
project,
|
|
43151
44014
|
includePaths: lintIncludePaths ?? void 0,
|
|
@@ -43159,6 +44022,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
43159
44022
|
onFileProgress: (scannedFileCount, totalFileCount) => {
|
|
43160
44023
|
lastReportedTotalFileCount = totalFileCount;
|
|
43161
44024
|
runSync(scanProgress.update(`Scanning files (${scannedFileCount}/${totalFileCount})${workerCountSuffix}...`));
|
|
44025
|
+
},
|
|
44026
|
+
onCacheStats: (cacheHitFileCount, totalConsideredFileCount) => {
|
|
44027
|
+
lintCacheHitFileCount = cacheHitFileCount;
|
|
44028
|
+
lintCacheTotalFileCount = totalConsideredFileCount;
|
|
43162
44029
|
}
|
|
43163
44030
|
}).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
|
|
43164
44031
|
yield* set(lintFailure, {
|
|
@@ -43168,36 +44035,54 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
43168
44035
|
reasonKind: error.reason._tag === "OxlintUnavailable" ? error.reason.kind : null
|
|
43169
44036
|
});
|
|
43170
44037
|
return empty$4;
|
|
43171
|
-
}))))
|
|
44038
|
+
}))));
|
|
44039
|
+
const lintCollected = yield* runCollect(applyPerElementPipeline(shouldOverlapDeadCode ? baseLintStream.pipe(provideService(OxlintConcurrency, lintConcurrency)) : baseLintStream)).pipe(timeoutOption(lintPhaseTimeoutMs), flatMap$2(match$3({
|
|
44040
|
+
onNone: () => set(lintFailure, {
|
|
44041
|
+
didFail: true,
|
|
44042
|
+
reason: `Lint analysis exceeded ${lintPhaseTimeoutMs / MILLISECONDS_PER_SECOND}s and was skipped.`,
|
|
44043
|
+
reasonTag: "OxlintBatchExceeded",
|
|
44044
|
+
reasonKind: null
|
|
44045
|
+
}).pipe(as([])),
|
|
44046
|
+
onSome: succeed$2
|
|
44047
|
+
})));
|
|
43172
44048
|
const lintFailureState = yield* get$2(lintFailure);
|
|
43173
44049
|
yield* afterLint(lintFailureState.didFail);
|
|
43174
44050
|
if (lintFailureState.didFail) yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
|
|
43175
44051
|
const totalFileCount = lastReportedTotalFileCount || (lintIncludePaths?.length ?? project.sourceFileCount);
|
|
43176
44052
|
const scannedFilesLabel = `${totalFileCount} ${totalFileCount === 1 ? "file" : "files"}`;
|
|
43177
|
-
|
|
43178
|
-
|
|
43179
|
-
|
|
43180
|
-
|
|
43181
|
-
|
|
43182
|
-
|
|
43183
|
-
|
|
43184
|
-
|
|
44053
|
+
let deadCodeCollected = [];
|
|
44054
|
+
if (lintFailureState.didFail) {
|
|
44055
|
+
if (deadCodeFiber !== null) yield* interrupt(deadCodeFiber);
|
|
44056
|
+
} else if (shouldRunDeadCode) {
|
|
44057
|
+
yield* scanProgress.update(`Scanned ${scannedFilesLabel}, analyzing dead code...`);
|
|
44058
|
+
const sequentialDeadCodeTimeout = resolveDeadCodeTimeout({
|
|
44059
|
+
sourceFileCount: totalFileCount,
|
|
44060
|
+
deadCodeConcurrency: scanConcurrency,
|
|
44061
|
+
fullConcurrency: scanConcurrency
|
|
43185
44062
|
});
|
|
43186
|
-
|
|
43187
|
-
|
|
43188
|
-
|
|
44063
|
+
deadCodeCollected = deadCodeFiber !== null ? yield* join(deadCodeFiber) : yield* buildCollectDeadCode({
|
|
44064
|
+
workerTimeoutMs: sequentialDeadCodeTimeout.workerTimeoutMs,
|
|
44065
|
+
phaseTimeoutMs: resolveDeadCodePhaseTimeoutMs(sequentialDeadCodeTimeout.phaseTimeoutMs)
|
|
44066
|
+
});
|
|
44067
|
+
}
|
|
44068
|
+
const deadCodeFailureState = lintFailureState.didFail ? {
|
|
44069
|
+
didFail: false,
|
|
44070
|
+
reason: null
|
|
44071
|
+
} : yield* get$2(deadCodeFailure);
|
|
43189
44072
|
const scanElapsedMilliseconds = Date.now() - scanStartTime;
|
|
43190
|
-
const scanElapsedSeconds = (scanElapsedMilliseconds /
|
|
44073
|
+
const scanElapsedSeconds = (scanElapsedMilliseconds / MILLISECONDS_PER_SECOND).toFixed(1);
|
|
43191
44074
|
if (!lintFailureState.didFail) if (deadCodeFailureState.didFail) yield* scanProgress.fail(DEAD_CODE_FAIL_TEXT);
|
|
43192
44075
|
else if (input.suppressScanSummary) yield* scanProgress.stop();
|
|
43193
44076
|
else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
|
|
44077
|
+
const supplyChainResult = yield* join(supplyChainFiber);
|
|
44078
|
+
const supplyChainCollected = supplyChainResult.diagnostics;
|
|
43194
44079
|
yield* reporterService.finalize;
|
|
43195
|
-
const finalDiagnostics = [
|
|
44080
|
+
const finalDiagnostics = sortDiagnosticsStable(assignFixGroups([
|
|
43196
44081
|
...envCollected,
|
|
43197
44082
|
...supplyChainCollected,
|
|
43198
44083
|
...lintCollected,
|
|
43199
44084
|
...deadCodeCollected
|
|
43200
|
-
];
|
|
44085
|
+
]));
|
|
43201
44086
|
const githubViewerPermission = yield* join(githubViewerPermissionFiber);
|
|
43202
44087
|
const scoreMetadata = {
|
|
43203
44088
|
...repo !== null ? { repo } : {},
|
|
@@ -43233,9 +44118,14 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
43233
44118
|
lintPartialFailures,
|
|
43234
44119
|
didDeadCodeFail: deadCodeFailureState.didFail,
|
|
43235
44120
|
deadCodeFailureReason: deadCodeFailureState.reason,
|
|
44121
|
+
deadCodeOverlapped: shouldOverlapDeadCode,
|
|
43236
44122
|
scannedFileCount: totalFileCount,
|
|
43237
44123
|
scannedFilePaths,
|
|
43238
|
-
scanElapsedMilliseconds
|
|
44124
|
+
scanElapsedMilliseconds,
|
|
44125
|
+
scanConcurrency,
|
|
44126
|
+
supplyChainOverlapTimedOut: supplyChainResult.timedOut,
|
|
44127
|
+
lintCacheHitFileCount,
|
|
44128
|
+
lintCacheTotalFileCount
|
|
43239
44129
|
};
|
|
43240
44130
|
}).pipe(withSpan("runInspect", { attributes: {
|
|
43241
44131
|
"inspect.directory": input.directory,
|
|
@@ -43243,7 +44133,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
43243
44133
|
"inspect.runDeadCode": input.runDeadCode,
|
|
43244
44134
|
"inspect.isCi": input.isCi,
|
|
43245
44135
|
"inspect.scoreSurface": input.scoreSurface ?? "score"
|
|
43246
|
-
} }));
|
|
44136
|
+
} }), (scanProgram) => flatMap$2(ScanDeadlineMs, (scanDeadlineMs) => scanProgram.pipe(timeout(scanDeadlineMs), catchTag$1("TimeoutError", () => new ReactDoctorError({ reason: new ScanDeadlineExceeded({ detail: `${scanDeadlineMs / MILLISECONDS_PER_SECOND}s elapsed` }) })))));
|
|
43247
44137
|
const parseNodeVersion = (versionString) => {
|
|
43248
44138
|
const [major = 0, minor = 0, patch = 0] = versionString.replace(/^v/, "").trim().split(".").map(Number);
|
|
43249
44139
|
return {
|
|
@@ -43424,7 +44314,7 @@ var StagedFiles = class StagedFiles extends Service()("react-doctor/StagedFiles"
|
|
|
43424
44314
|
static layerNode = effect(StagedFiles, gen(function* () {
|
|
43425
44315
|
const git = yield* Git;
|
|
43426
44316
|
return StagedFiles.of({
|
|
43427
|
-
discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(map$3((entries) => entries.filter(isLintableSourceFile))),
|
|
44317
|
+
discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(map$3((entries) => entries.filter(isLintableSourceFile)), withSpan("StagedFiles.discoverSourceFiles")),
|
|
43428
44318
|
materialize: ({ directory, stagedFiles, tempDirectory }) => materializeSourceTree({
|
|
43429
44319
|
directory,
|
|
43430
44320
|
files: stagedFiles,
|
|
@@ -43434,7 +44324,7 @@ var StagedFiles = class StagedFiles extends Service()("react-doctor/StagedFiles"
|
|
|
43434
44324
|
tempDirectory: tree.tempDirectory,
|
|
43435
44325
|
stagedFiles: tree.materializedFiles,
|
|
43436
44326
|
cleanup: tree.cleanup
|
|
43437
|
-
})))
|
|
44327
|
+
})), withSpan("StagedFiles.materialize"))
|
|
43438
44328
|
});
|
|
43439
44329
|
}));
|
|
43440
44330
|
/**
|
|
@@ -43567,6 +44457,7 @@ const buildJsonReport = (input) => {
|
|
|
43567
44457
|
score: result.score,
|
|
43568
44458
|
skippedChecks: result.skippedChecks,
|
|
43569
44459
|
...result.skippedCheckReasons ? { skippedCheckReasons: result.skippedCheckReasons } : {},
|
|
44460
|
+
...typeof result.scannedFileCount === "number" ? { scannedFileCount: result.scannedFileCount } : {},
|
|
43570
44461
|
elapsedMilliseconds: result.elapsedMilliseconds
|
|
43571
44462
|
}));
|
|
43572
44463
|
const flattenedDiagnostics = projects.flatMap((entry) => entry.diagnostics);
|
|
@@ -43841,7 +44732,7 @@ const FALSY_CI_FLAG_VALUES = new Set([
|
|
|
43841
44732
|
"false"
|
|
43842
44733
|
]);
|
|
43843
44734
|
const isCiFlagSet = (value) => value !== void 0 && !FALSY_CI_FLAG_VALUES.has(value.toLowerCase());
|
|
43844
|
-
const isCiEnvironment = () => CI_ENVIRONMENT_VARIABLES.some((environmentVariable) => Boolean(
|
|
44735
|
+
const isCiEnvironment = (env = process.env) => CI_ENVIRONMENT_VARIABLES.some((environmentVariable) => Boolean(env[environmentVariable])) || isCiFlagSet(env.CI);
|
|
43845
44736
|
const detectCiProvider = () => {
|
|
43846
44737
|
for (const [environmentVariable, provider] of CI_PROVIDER_BY_ENVIRONMENT_VARIABLE) if (process.env[environmentVariable]) return provider;
|
|
43847
44738
|
return isCiFlagSet(process.env.CI) ? "unknown" : null;
|
|
@@ -43866,6 +44757,53 @@ const detectCodingAgent = () => {
|
|
|
43866
44757
|
const isCodingAgentEnvironment = () => detectCodingAgent() !== null;
|
|
43867
44758
|
const isCiOrCodingAgentEnvironment = () => isCiEnvironment() || isCodingAgentEnvironment();
|
|
43868
44759
|
//#endregion
|
|
44760
|
+
//#region src/cli/utils/detect-terminal-kind.ts
|
|
44761
|
+
const TERMINAL_BY_TERM_PROGRAM = [
|
|
44762
|
+
["vscode", "vscode"],
|
|
44763
|
+
["iTerm.app", "iterm"],
|
|
44764
|
+
["Apple_Terminal", "apple-terminal"],
|
|
44765
|
+
["WezTerm", "wezterm"],
|
|
44766
|
+
["ghostty", "ghostty"],
|
|
44767
|
+
["Hyper", "hyper"],
|
|
44768
|
+
["Tabby", "tabby"],
|
|
44769
|
+
["rio", "rio"]
|
|
44770
|
+
];
|
|
44771
|
+
/**
|
|
44772
|
+
* Best-effort label for the terminal emulator / editor hosting the CLI,
|
|
44773
|
+
* derived from terminal-identity env vars. Recorded as the `terminalKind` run
|
|
44774
|
+
* tag so we can see where React Doctor is actually run (nvim, VS Code, iTerm,
|
|
44775
|
+
* …) — the split Sentry can't otherwise see. Low-cardinality and free of any
|
|
44776
|
+
* username/path/secret, so it's safe as a tag. Editor terminals (nvim/vim)
|
|
44777
|
+
* win over the outer emulator because that's the surface a user is reading in;
|
|
44778
|
+
* "ci" marks a run with no interactive terminal; "unknown" when nothing matches.
|
|
44779
|
+
*/
|
|
44780
|
+
const detectTerminalKind = (env = process.env) => {
|
|
44781
|
+
if (env.NVIM) return "neovim";
|
|
44782
|
+
if (env.VIM_TERMINAL) return "vim";
|
|
44783
|
+
const termProgram = env.TERM_PROGRAM;
|
|
44784
|
+
if (termProgram) {
|
|
44785
|
+
for (const [marker, label] of TERMINAL_BY_TERM_PROGRAM) if (termProgram === marker) return label;
|
|
44786
|
+
}
|
|
44787
|
+
if (env.KITTY_WINDOW_ID || env.TERM === "xterm-kitty") return "kitty";
|
|
44788
|
+
if (env.WT_SESSION) return "windows-terminal";
|
|
44789
|
+
if (env.ALACRITTY_WINDOW_ID || env.TERM === "alacritty") return "alacritty";
|
|
44790
|
+
if (env.VTE_VERSION) return "vte";
|
|
44791
|
+
if (env.TMUX) return "tmux";
|
|
44792
|
+
if (isCiEnvironment(env)) return "ci";
|
|
44793
|
+
return "unknown";
|
|
44794
|
+
};
|
|
44795
|
+
//#endregion
|
|
44796
|
+
//#region src/cli/utils/is-debug-flag.ts
|
|
44797
|
+
/**
|
|
44798
|
+
* Whether the user passed `--debug` (surface the run's Sentry trace id, and
|
|
44799
|
+
* force performance tracing on so there's a trace to surface). Read straight
|
|
44800
|
+
* from argv rather than Commander's parsed flags because `initializeSentry()`
|
|
44801
|
+
* runs before Commander parses — the same reason `shouldEnableSentry()` reads
|
|
44802
|
+
* `--no-score` from argv. Sharing this one reader keeps the init-time sampling
|
|
44803
|
+
* override and the end-of-run print in agreement.
|
|
44804
|
+
*/
|
|
44805
|
+
const isDebugFlagEnabled = (argv = process.argv) => argv.includes("--debug");
|
|
44806
|
+
//#endregion
|
|
43869
44807
|
//#region src/cli/utils/is-git-hook-environment.ts
|
|
43870
44808
|
const isGitHookEnvironment = () => Boolean(process.env.GIT_DIR);
|
|
43871
44809
|
//#endregion
|
|
@@ -43888,6 +44826,7 @@ const NON_INTERACTIVE_ENVIRONMENT_VARIABLES = [
|
|
|
43888
44826
|
const isNonInteractiveEnvironment = () => NON_INTERACTIVE_ENVIRONMENT_VARIABLES.some((envVariable) => Boolean(process.env[envVariable])) || isCodingAgentEnvironment();
|
|
43889
44827
|
//#endregion
|
|
43890
44828
|
//#region src/cli/utils/constants.ts
|
|
44829
|
+
const REACT_DOCTOR_CONFIG_PROJECT_NAME = "react-doctor";
|
|
43891
44830
|
const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
|
|
43892
44831
|
const BASELINE_FILES_TEMP_DIR_PREFIX = "react-doctor-baseline-";
|
|
43893
44832
|
const GH_DEFAULT_BRANCH_PROBE_TIMEOUT_MS = 5e3;
|
|
@@ -43972,7 +44911,7 @@ const makeNoopConsole = () => ({
|
|
|
43972
44911
|
});
|
|
43973
44912
|
//#endregion
|
|
43974
44913
|
//#region src/cli/utils/version.ts
|
|
43975
|
-
const VERSION = "0.5.
|
|
44914
|
+
const VERSION = "0.5.7-dev.0b4f4f4";
|
|
43976
44915
|
//#endregion
|
|
43977
44916
|
//#region src/cli/utils/json-mode.ts
|
|
43978
44917
|
let context = null;
|
|
@@ -44122,8 +45061,11 @@ const buildRunContext = () => {
|
|
|
44122
45061
|
viaAction: isOfficialGithubAction(),
|
|
44123
45062
|
codingAgent: detectCodingAgent(),
|
|
44124
45063
|
interactive: !isNonInteractiveEnvironment(),
|
|
45064
|
+
terminalKind: detectTerminalKind(),
|
|
44125
45065
|
jsonMode: isJsonModeActive(),
|
|
44126
|
-
|
|
45066
|
+
debug: isDebugFlagEnabled(),
|
|
45067
|
+
invokedVia: detectInvokedVia(),
|
|
45068
|
+
lintBatchOrdering: resolveLintBatchOrdering()
|
|
44127
45069
|
};
|
|
44128
45070
|
};
|
|
44129
45071
|
//#endregion
|
|
@@ -44192,9 +45134,12 @@ const buildSentryScope = (runContext = buildRunContext()) => {
|
|
|
44192
45134
|
viaAction: runContext.viaAction,
|
|
44193
45135
|
codingAgent: runContext.codingAgent,
|
|
44194
45136
|
interactive: runContext.interactive,
|
|
45137
|
+
terminalKind: runContext.terminalKind,
|
|
44195
45138
|
jsonMode: runContext.jsonMode,
|
|
45139
|
+
debug: runContext.debug,
|
|
44196
45140
|
invokedVia: runContext.invokedVia,
|
|
44197
|
-
nodeMajor: runContext.nodeMajor
|
|
45141
|
+
nodeMajor: runContext.nodeMajor,
|
|
45142
|
+
lintBatchOrdering: runContext.lintBatchOrdering
|
|
44198
45143
|
};
|
|
44199
45144
|
const contexts = { run: { ...runContext } };
|
|
44200
45145
|
const projectInfo = getSentryProjectInfo();
|
|
@@ -44330,13 +45275,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
|
|
|
44330
45275
|
* uploads source-map artifacts under, so stack frames symbolicate. Honors the
|
|
44331
45276
|
* standard `SENTRY_RELEASE` override.
|
|
44332
45277
|
*/
|
|
44333
|
-
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.
|
|
45278
|
+
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.7-dev.0b4f4f4`;
|
|
44334
45279
|
/**
|
|
44335
45280
|
* Deployment environment shown in Sentry's environment filter. Defaults to
|
|
44336
45281
|
* `production` for tagged releases and `development` for dev/unbuilt versions,
|
|
44337
45282
|
* overridable via the standard `SENTRY_ENVIRONMENT` env var.
|
|
44338
45283
|
*/
|
|
44339
|
-
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.
|
|
45284
|
+
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.7-dev.0b4f4f4") ? "development" : "production");
|
|
44340
45285
|
/**
|
|
44341
45286
|
* Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
|
|
44342
45287
|
* (set to `0` to disable tracing) and falls back to
|
|
@@ -44400,7 +45345,7 @@ const flushSentry = async () => {
|
|
|
44400
45345
|
const initializeSentry = () => {
|
|
44401
45346
|
if (isInitialized || !shouldEnableSentry()) return;
|
|
44402
45347
|
isInitialized = true;
|
|
44403
|
-
resolvedTracesSampleRate = resolveTracesSampleRate();
|
|
45348
|
+
resolvedTracesSampleRate = isDebugFlagEnabled() ? 1 : resolveTracesSampleRate();
|
|
44404
45349
|
const { tags, contexts } = buildSentryScope();
|
|
44405
45350
|
Sentry.init({
|
|
44406
45351
|
dsn: process.env.SENTRY_DSN || "https://f253d570240a59b8dbd77b7a548ef133@o4510226365743104.ingest.us.sentry.io/4511487817809920",
|
|
@@ -44539,7 +45484,7 @@ const externalSpanFrom = (sentrySpan) => {
|
|
|
44539
45484
|
* in-memory tracer — identical to the prior default behavior.
|
|
44540
45485
|
*/
|
|
44541
45486
|
const applyObservability = (program, rootSentrySpan) => {
|
|
44542
|
-
if (isOtlpExportConfigured()) return (rootSentrySpan ? program.pipe(provideService(ParentSpan, externalSpanFrom(rootSentrySpan))) : program).pipe(provide(layerOtlp));
|
|
45487
|
+
if (isOtlpExportConfigured()) return (rootSentrySpan ? program.pipe(provideService$2(ParentSpan, externalSpanFrom(rootSentrySpan))) : program).pipe(provide(layerOtlp));
|
|
44543
45488
|
if (rootSentrySpan) return program.pipe(withTracer(makeSentryTracer(rootSentrySpan)));
|
|
44544
45489
|
return program.pipe(provide(layerOtlp));
|
|
44545
45490
|
};
|
|
@@ -47616,6 +48561,11 @@ const setActiveRunTrace = (trace) => {
|
|
|
47616
48561
|
activeRunTrace = trace;
|
|
47617
48562
|
};
|
|
47618
48563
|
const getActiveRunTrace = () => activeRunTrace;
|
|
48564
|
+
let lastRunTraceId = null;
|
|
48565
|
+
const recordRunTraceId = (traceId) => {
|
|
48566
|
+
lastRunTraceId = traceId;
|
|
48567
|
+
};
|
|
48568
|
+
const getLastRunTraceId = () => lastRunTraceId;
|
|
47619
48569
|
//#endregion
|
|
47620
48570
|
//#region src/cli/utils/to-span-attributes.ts
|
|
47621
48571
|
/**
|
|
@@ -47678,14 +48628,13 @@ const withSentryRunSpan = (run, options = {}) => {
|
|
|
47678
48628
|
op: "cli.inspect",
|
|
47679
48629
|
attributes: toSpanAttributes(tags)
|
|
47680
48630
|
}, (rootSpan) => {
|
|
47681
|
-
|
|
47682
|
-
|
|
47683
|
-
|
|
47684
|
-
|
|
47685
|
-
|
|
47686
|
-
|
|
47687
|
-
|
|
47688
|
-
}
|
|
48631
|
+
const spanContext = rootSpan.spanContext();
|
|
48632
|
+
recordRunTraceId(spanContext.traceId);
|
|
48633
|
+
if (options.concurrentScan !== true) setActiveRunTrace({
|
|
48634
|
+
traceId: spanContext.traceId,
|
|
48635
|
+
spanId: spanContext.spanId,
|
|
48636
|
+
sampled: (spanContext.traceFlags & 1) === 1
|
|
48637
|
+
});
|
|
47689
48638
|
return run(rootSpan);
|
|
47690
48639
|
});
|
|
47691
48640
|
};
|
|
@@ -47825,6 +48774,42 @@ const recordScanMetrics = (input) => {
|
|
|
47825
48774
|
});
|
|
47826
48775
|
};
|
|
47827
48776
|
//#endregion
|
|
48777
|
+
//#region src/cli/utils/diagnostic-grouping.ts
|
|
48778
|
+
const buildRulePriorityMap = (scores) => {
|
|
48779
|
+
const rulePriority = /* @__PURE__ */ new Map();
|
|
48780
|
+
for (const score of scores) {
|
|
48781
|
+
if (!score?.rules) continue;
|
|
48782
|
+
for (const [ruleKey, info] of Object.entries(score.rules)) if (typeof info.priority === "number") rulePriority.set(ruleKey, info.priority);
|
|
48783
|
+
}
|
|
48784
|
+
return rulePriority;
|
|
48785
|
+
};
|
|
48786
|
+
const compareByRulePriority = (ruleKeyA, ruleKeyB, rulePriority) => {
|
|
48787
|
+
const priorityA = rulePriority?.get(ruleKeyA);
|
|
48788
|
+
const priorityB = rulePriority?.get(ruleKeyB);
|
|
48789
|
+
if (priorityA === void 0 && priorityB === void 0) return 0;
|
|
48790
|
+
if (priorityA === void 0) return 1;
|
|
48791
|
+
if (priorityB === void 0) return -1;
|
|
48792
|
+
return priorityB - priorityA;
|
|
48793
|
+
};
|
|
48794
|
+
const sortRuleGroupsByImportance = (diagnosticGroups, rulePriority) => diagnosticGroups.toSorted(([ruleKeyA], [ruleKeyB]) => compareByRulePriority(ruleKeyA, ruleKeyB, rulePriority));
|
|
48795
|
+
const buildSortedRuleGroups = (diagnostics, rulePriority) => sortRuleGroupsByImportance([...groupBy([...diagnostics], (diagnostic) => `${diagnostic.plugin}/${diagnostic.rule}`)], rulePriority);
|
|
48796
|
+
const getSharedFixSiteCount = (diagnostics) => {
|
|
48797
|
+
if (diagnostics.length < 2) return 0;
|
|
48798
|
+
const firstFixGroupId = diagnostics[0]?.fixGroupId;
|
|
48799
|
+
if (!firstFixGroupId) return 0;
|
|
48800
|
+
return diagnostics.every((diagnostic) => diagnostic.fixGroupId === firstFixGroupId) ? diagnostics.length : 0;
|
|
48801
|
+
};
|
|
48802
|
+
const FIX_RECIPE_DIRECTIVE_LABEL = "Curl with no cache & follow the canonical fix and false positive check recipe before fixing";
|
|
48803
|
+
const formatFixRecipeLine = (diagnostic) => hasPublishedFixRecipe(diagnostic) ? `${FIX_RECIPE_DIRECTIVE_LABEL}: ${buildRuleDocsUrl(diagnostic.plugin, diagnostic.rule)}` : null;
|
|
48804
|
+
const formatLearnMoreLine = (diagnostic) => hasPublishedFixRecipe(diagnostic) ? `Learn more: ${buildRuleDocsUrl(diagnostic.plugin, diagnostic.rule)}` : null;
|
|
48805
|
+
const buildRuleBlastRadii = (diagnostics) => buildSortedRuleGroups(diagnostics).map(([ruleKey, ruleDiagnostics]) => ({
|
|
48806
|
+
ruleKey,
|
|
48807
|
+
title: ruleDiagnostics[0].title ?? ruleKey,
|
|
48808
|
+
siteCount: ruleDiagnostics.length,
|
|
48809
|
+
fileCount: new Set(ruleDiagnostics.map((diagnostic) => diagnostic.filePath)).size
|
|
48810
|
+
})).toSorted((left, right) => right.fileCount - left.fileCount);
|
|
48811
|
+
const findMigrationScaleBuckets = (diagnostics) => buildRuleBlastRadii(diagnostics).filter((bucket) => bucket.fileCount >= 40);
|
|
48812
|
+
//#endregion
|
|
47828
48813
|
//#region src/cli/utils/cli-logger.ts
|
|
47829
48814
|
/**
|
|
47830
48815
|
* Thin synchronous façade over Effect's `Console` module. Used by
|
|
@@ -47941,12 +48926,17 @@ const buildOutcomeAttributes = (input) => {
|
|
|
47941
48926
|
topRule = rule;
|
|
47942
48927
|
topRuleCount = count;
|
|
47943
48928
|
}
|
|
48929
|
+
const largestRuleBucket = buildRuleBlastRadii(result.diagnostics)[0] ?? null;
|
|
47944
48930
|
let diagnosticsInTestFiles = 0;
|
|
47945
48931
|
let diagnosticsInStoryFiles = 0;
|
|
48932
|
+
const findingsPerFixGroup = /* @__PURE__ */ new Map();
|
|
47946
48933
|
for (const diagnostic of result.diagnostics) {
|
|
47947
48934
|
if (diagnostic.fileContext === "test") diagnosticsInTestFiles += 1;
|
|
47948
48935
|
if (diagnostic.fileContext === "story") diagnosticsInStoryFiles += 1;
|
|
48936
|
+
if (diagnostic.fixGroupId) findingsPerFixGroup.set(diagnostic.fixGroupId, (findingsPerFixGroup.get(diagnostic.fixGroupId) ?? 0) + 1);
|
|
47949
48937
|
}
|
|
48938
|
+
let fixGroupedFindings = 0;
|
|
48939
|
+
for (const count of findingsPerFixGroup.values()) fixGroupedFindings += count;
|
|
47950
48940
|
const attributes = {
|
|
47951
48941
|
outcome,
|
|
47952
48942
|
exitCode: wouldBlock ? 1 : 0,
|
|
@@ -47960,8 +48950,16 @@ const buildOutcomeAttributes = (input) => {
|
|
|
47960
48950
|
diagnosticsInTestFiles,
|
|
47961
48951
|
diagnosticsInStoryFiles,
|
|
47962
48952
|
distinctRulesFired: countByRule.size,
|
|
48953
|
+
"diag.fixGroups": findingsPerFixGroup.size,
|
|
48954
|
+
"diag.fixGroupedFindings": fixGroupedFindings,
|
|
47963
48955
|
topRule,
|
|
48956
|
+
"migration.largestRuleBucketFiles": largestRuleBucket ? largestRuleBucket.fileCount : null,
|
|
48957
|
+
"migration.largestRuleBucketSites": largestRuleBucket ? largestRuleBucket.siteCount : null,
|
|
48958
|
+
"migration.largestRuleBucketRule": largestRuleBucket ? largestRuleBucket.ruleKey : null,
|
|
47964
48959
|
scannedFileCount: result.scannedFileCount ?? null,
|
|
48960
|
+
lintCacheHitFiles: result.lintCacheHitFileCount ?? null,
|
|
48961
|
+
lintCacheTotalFiles: result.lintCacheTotalFileCount ?? null,
|
|
48962
|
+
lintCacheHitRatio: result.lintCacheTotalFileCount != null && result.lintCacheTotalFileCount > 0 ? (result.lintCacheHitFileCount ?? 0) / result.lintCacheTotalFileCount : null,
|
|
47965
48963
|
elapsedMs: result.elapsedMilliseconds,
|
|
47966
48964
|
scanPhaseMs: result.scanElapsedMilliseconds ?? null,
|
|
47967
48965
|
score: result.score ? result.score.score : null,
|
|
@@ -47971,7 +48969,10 @@ const buildOutcomeAttributes = (input) => {
|
|
|
47971
48969
|
didLintFail: input.didLintFail ?? null,
|
|
47972
48970
|
lintFailureReasonKind: input.lintFailureReasonKind ?? null,
|
|
47973
48971
|
lintPartialFailureCount: input.lintPartialFailureCount ?? null,
|
|
47974
|
-
|
|
48972
|
+
lintDroppedFileCount: input.lintDroppedFileCount ?? null,
|
|
48973
|
+
didDeadCodeFail: input.didDeadCodeFail ?? null,
|
|
48974
|
+
supplyChainOverlapTimedOut: input.supplyChainOverlapTimedOut ?? null,
|
|
48975
|
+
deadCodeOverlapped: input.deadCodeOverlapped ?? null
|
|
47975
48976
|
};
|
|
47976
48977
|
for (const [category, count] of countByCategory) attributes[`diag.category.${toCategoryKey(category)}`] = count;
|
|
47977
48978
|
if (result.baselineDelta) {
|
|
@@ -48040,6 +49041,32 @@ const recordRunEvent = (rootSpan, input) => {
|
|
|
48040
49041
|
} catch {}
|
|
48041
49042
|
};
|
|
48042
49043
|
//#endregion
|
|
49044
|
+
//#region src/cli/utils/resolve-worker-telemetry.ts
|
|
49045
|
+
/**
|
|
49046
|
+
* Projects the resolved lint worker count into the `(workerCount, parallel)`
|
|
49047
|
+
* telemetry pair. `resolvedWorkerCount` is the count the scan actually fanned
|
|
49048
|
+
* out to (`InspectOutput.scanConcurrency`); `pinnedConcurrency` is the caller's
|
|
49049
|
+
* `inspect({ concurrency })` pin, used as the fallback when no scan resolved a
|
|
49050
|
+
* count (the pre-scan failure path, or a cache entry persisted before the
|
|
49051
|
+
* resolved count was tracked). `parallel` is derived from the count — NOT from
|
|
49052
|
+
* whether a count was pinned — so the common auto path (no pin) still reports
|
|
49053
|
+
* parallelism correctly instead of always reading `false`.
|
|
49054
|
+
*/
|
|
49055
|
+
const resolveWorkerTelemetry = (resolvedWorkerCount, pinnedConcurrency) => {
|
|
49056
|
+
const workerCount = resolvedWorkerCount ?? pinnedConcurrency;
|
|
49057
|
+
return {
|
|
49058
|
+
workerCount,
|
|
49059
|
+
parallel: workerCount !== void 0 && workerCount > 1
|
|
49060
|
+
};
|
|
49061
|
+
};
|
|
49062
|
+
//#endregion
|
|
49063
|
+
//#region src/cli/utils/count-dropped-lint-files.ts
|
|
49064
|
+
const DROPPED_FILES_MESSAGE_PATTERN = /^(\d+) file\(s\) failed to lint and were skipped/;
|
|
49065
|
+
const countDroppedLintFiles = (lintPartialFailures) => lintPartialFailures.reduce((total, message) => {
|
|
49066
|
+
const match = DROPPED_FILES_MESSAGE_PATTERN.exec(message);
|
|
49067
|
+
return match ? total + Number(match[1]) : total;
|
|
49068
|
+
}, 0);
|
|
49069
|
+
//#endregion
|
|
48043
49070
|
//#region src/cli/utils/path-format.ts
|
|
48044
49071
|
const toForwardSlashes = (filePath) => filePath.replaceAll("\\", "/");
|
|
48045
49072
|
//#endregion
|
|
@@ -48118,9 +49145,10 @@ const AGENT_GUIDANCE_LINES = [
|
|
|
48118
49145
|
"Investigate deeply where relevant: race conditions, security-sensitive flows, state propagation, multi-file refactors, and downstream dependency chains.",
|
|
48119
49146
|
"Ignore pure style preferences, theoretical issues without real impact, missing features, and unrelated pre-existing code.",
|
|
48120
49147
|
"Start with high-confidence fixes that preserve behavior. Leave low-confidence or product-dependent changes as notes.",
|
|
48121
|
-
"Run `npx react-doctor@latest --verbose --
|
|
49148
|
+
"Run `npx react-doctor@latest --verbose --scope changed` before and after changes, plus relevant tests after each focused batch.",
|
|
48122
49149
|
"When available, spawn subagents or isolated worktrees for independent rule families, then review and merge only the best safe fixes.",
|
|
48123
49150
|
"Split unrelated, broad, or behavior-changing work into separate PRs/branches instead of one large cleanup.",
|
|
49151
|
+
"When one rule spans dozens of files (a migration-scale change), fix a representative sample first, confirm the recipe holds, and get the code owner's sign-off before changing the rest. Don't mass-fix a broad pattern in one unreviewed pass.",
|
|
48124
49152
|
"For confirmed issues that cannot be fixed now, create GitHub issues with the rule, file/line, confidence, impact, and proposed fix.",
|
|
48125
49153
|
"If a fix needs an API, UX, or architecture decision, stop and ask before editing."
|
|
48126
49154
|
];
|
|
@@ -48130,29 +49158,6 @@ const printAgentGuidance = () => gen(function* () {
|
|
|
48130
49158
|
yield* log("");
|
|
48131
49159
|
});
|
|
48132
49160
|
//#endregion
|
|
48133
|
-
//#region src/cli/utils/diagnostic-grouping.ts
|
|
48134
|
-
const buildRulePriorityMap = (scores) => {
|
|
48135
|
-
const rulePriority = /* @__PURE__ */ new Map();
|
|
48136
|
-
for (const score of scores) {
|
|
48137
|
-
if (!score?.rules) continue;
|
|
48138
|
-
for (const [ruleKey, info] of Object.entries(score.rules)) if (typeof info.priority === "number") rulePriority.set(ruleKey, info.priority);
|
|
48139
|
-
}
|
|
48140
|
-
return rulePriority;
|
|
48141
|
-
};
|
|
48142
|
-
const compareByRulePriority = (ruleKeyA, ruleKeyB, rulePriority) => {
|
|
48143
|
-
const priorityA = rulePriority?.get(ruleKeyA);
|
|
48144
|
-
const priorityB = rulePriority?.get(ruleKeyB);
|
|
48145
|
-
if (priorityA === void 0 && priorityB === void 0) return 0;
|
|
48146
|
-
if (priorityA === void 0) return 1;
|
|
48147
|
-
if (priorityB === void 0) return -1;
|
|
48148
|
-
return priorityB - priorityA;
|
|
48149
|
-
};
|
|
48150
|
-
const sortRuleGroupsByImportance = (diagnosticGroups, rulePriority) => diagnosticGroups.toSorted(([ruleKeyA], [ruleKeyB]) => compareByRulePriority(ruleKeyA, ruleKeyB, rulePriority));
|
|
48151
|
-
const buildSortedRuleGroups = (diagnostics, rulePriority) => sortRuleGroupsByImportance([...groupBy([...diagnostics], (diagnostic) => `${diagnostic.plugin}/${diagnostic.rule}`)], rulePriority);
|
|
48152
|
-
const FIX_RECIPE_DIRECTIVE_LABEL = "Curl with no cache & follow the canonical fix and false positive check recipe before fixing";
|
|
48153
|
-
const formatFixRecipeLine = (diagnostic) => hasPublishedFixRecipe(diagnostic) ? `${FIX_RECIPE_DIRECTIVE_LABEL}: ${buildRuleDocsUrl(diagnostic.plugin, diagnostic.rule)}` : null;
|
|
48154
|
-
const formatLearnMoreLine = (diagnostic) => hasPublishedFixRecipe(diagnostic) ? `Learn more: ${buildRuleDocsUrl(diagnostic.plugin, diagnostic.rule)}` : null;
|
|
48155
|
-
//#endregion
|
|
48156
49161
|
//#region src/cli/utils/box-text.ts
|
|
48157
49162
|
const ESCAPE = String.fromCharCode(27);
|
|
48158
49163
|
const ANSI_ESCAPE_PATTERN = new RegExp(`${ESCAPE}\\[[0-9;]*m`, "g");
|
|
@@ -48193,6 +49198,15 @@ const boxText = (content, innerWidth) => {
|
|
|
48193
49198
|
].join("\n");
|
|
48194
49199
|
};
|
|
48195
49200
|
//#endregion
|
|
49201
|
+
//#region src/cli/utils/resolve-absolute-path.ts
|
|
49202
|
+
/**
|
|
49203
|
+
* Resolves a diagnostic's `filePath` (relative to its project root, or
|
|
49204
|
+
* already absolute) to an absolute path. Shared by the code-frame reader and
|
|
49205
|
+
* the terminal hyperlink builder so both turn a relative path into the same
|
|
49206
|
+
* on-disk location.
|
|
49207
|
+
*/
|
|
49208
|
+
const resolveAbsolutePath = (filePath, rootDirectory) => Path.isAbsolute(filePath) ? filePath : Path.resolve(rootDirectory || ".", filePath);
|
|
49209
|
+
//#endregion
|
|
48196
49210
|
//#region src/cli/utils/build-code-frame.ts
|
|
48197
49211
|
/**
|
|
48198
49212
|
* Renders a syntax-highlighted source excerpt around a diagnostic site
|
|
@@ -48203,7 +49217,7 @@ const boxText = (content, innerWidth) => {
|
|
|
48203
49217
|
*/
|
|
48204
49218
|
const buildCodeFrame = (input) => {
|
|
48205
49219
|
if (input.line <= 0) return null;
|
|
48206
|
-
const absolutePath =
|
|
49220
|
+
const absolutePath = resolveAbsolutePath(input.filePath, input.rootDirectory);
|
|
48207
49221
|
let source;
|
|
48208
49222
|
try {
|
|
48209
49223
|
source = NFS.readFileSync(absolutePath, "utf8");
|
|
@@ -48243,6 +49257,16 @@ const resolveMeasureWidth = (reservedColumns = 0) => resolveClampedWidth({
|
|
|
48243
49257
|
const DIVIDER_INDENT = " ";
|
|
48244
49258
|
const buildSectionDivider = () => highlighter.dim(`${DIVIDER_INDENT}${"─".repeat(resolveMeasureWidth(2))}`);
|
|
48245
49259
|
//#endregion
|
|
49260
|
+
//#region src/cli/utils/format-hyperlink.ts
|
|
49261
|
+
const OSC = "\x1B]";
|
|
49262
|
+
const ST = "\x1B\\";
|
|
49263
|
+
/**
|
|
49264
|
+
* Wraps `text` in an OSC 8 hyperlink pointing at `uri`. The visible characters
|
|
49265
|
+
* are exactly `text`; the link is carried in escape sequences a capable
|
|
49266
|
+
* terminal turns into a click target.
|
|
49267
|
+
*/
|
|
49268
|
+
const formatHyperlink = (text, uri) => `${OSC}8;;${uri}${ST}${text}${OSC}8;;${ST}`;
|
|
49269
|
+
//#endregion
|
|
48246
49270
|
//#region src/cli/utils/indent-multiline-text.ts
|
|
48247
49271
|
const indentMultilineText = (text, linePrefix) => text.split("\n").map((lineText) => `${linePrefix}${lineText}`).join("\n");
|
|
48248
49272
|
//#endregion
|
|
@@ -48396,17 +49420,23 @@ const clusterNearbyDiagnostics = (diagnostics) => {
|
|
|
48396
49420
|
}
|
|
48397
49421
|
return clusters;
|
|
48398
49422
|
};
|
|
48399
|
-
const
|
|
49423
|
+
const formatClusterLocationText = (cluster) => {
|
|
49424
|
+
const { filePath } = cluster.diagnostics[0];
|
|
49425
|
+
if (cluster.startLine <= 0) return filePath;
|
|
49426
|
+
if (cluster.endLine > cluster.startLine) return `${filePath}:${cluster.startLine}-${cluster.endLine}`;
|
|
49427
|
+
return `${filePath}:${cluster.startLine}`;
|
|
49428
|
+
};
|
|
49429
|
+
const formatClusterLocation = (cluster, resolveSourceRoot, hyperlinks) => {
|
|
48400
49430
|
const lead = cluster.diagnostics[0];
|
|
48401
49431
|
const contextTag = formatFileContextTag(lead);
|
|
48402
|
-
|
|
48403
|
-
if (
|
|
48404
|
-
return `${lead.filePath
|
|
49432
|
+
const location = formatClusterLocationText(cluster);
|
|
49433
|
+
if (!hyperlinks) return `${location}${contextTag}`;
|
|
49434
|
+
return `${formatHyperlink(location, pathToFileURL(resolveAbsolutePath(lead.filePath, resolveSourceRoot(lead))).href)}${contextTag}`;
|
|
48405
49435
|
};
|
|
48406
|
-
const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame) => {
|
|
49436
|
+
const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame, hyperlinks) => {
|
|
48407
49437
|
const lead = cluster.diagnostics[0];
|
|
48408
49438
|
const isMultiSite = cluster.diagnostics.length > 1;
|
|
48409
|
-
const lines = ["", highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${formatClusterLocation(cluster)}`)];
|
|
49439
|
+
const lines = ["", highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${formatClusterLocation(cluster, resolveSourceRoot, hyperlinks)}`)];
|
|
48410
49440
|
const codeFrame = renderCodeFrame ? buildCodeFrame({
|
|
48411
49441
|
filePath: lead.filePath,
|
|
48412
49442
|
line: cluster.startLine,
|
|
@@ -48425,7 +49455,7 @@ const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame
|
|
|
48425
49455
|
}
|
|
48426
49456
|
return lines;
|
|
48427
49457
|
};
|
|
48428
|
-
const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, renderEverySite, isAgentEnvironment) => {
|
|
49458
|
+
const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, renderEverySite, isAgentEnvironment, hyperlinks) => {
|
|
48429
49459
|
const representative = pickRepresentativeDiagnostic(ruleDiagnostics);
|
|
48430
49460
|
const { severity } = representative;
|
|
48431
49461
|
const trailingBadge = formatTrailingSiteBadge(ruleDiagnostics.length);
|
|
@@ -48439,13 +49469,15 @@ const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, rende
|
|
|
48439
49469
|
const impactMessages = isCollapsedWarningGroup ? [...new Set(ruleDiagnostics.map((diagnostic) => diagnostic.message))] : [representative.message];
|
|
48440
49470
|
for (const impactMessage of impactMessages) for (const explanationLine of wrapTextToWidth(impactMessage, resolveMeasureWidth(4), { breakLongWords: false })) lines.push(`${TOP_ERROR_DETAIL_INDENT}${explanationLine}`);
|
|
48441
49471
|
if (representative.help) for (const fixLine of wrapTextToWidth(`→ ${representative.help}`, resolveMeasureWidth(4), { breakLongWords: false })) lines.push(highlighter.dim(`${TOP_ERROR_DETAIL_INDENT}${fixLine}`));
|
|
49472
|
+
const sharedFixSiteCount = getSharedFixSiteCount(ruleDiagnostics);
|
|
49473
|
+
if (sharedFixSiteCount > 0) lines.push(highlighter.dim(`${TOP_ERROR_DETAIL_INDENT}↳ One fix clears all ${sharedFixSiteCount} findings.`));
|
|
48442
49474
|
if (renderEverySite && isAgentEnvironment) {
|
|
48443
49475
|
const fixRecipeLine = formatFixRecipeLine(representative);
|
|
48444
49476
|
if (fixRecipeLine) lines.push(highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${fixRecipeLine}`));
|
|
48445
49477
|
}
|
|
48446
49478
|
const renderCodeFrame = severity === "error";
|
|
48447
49479
|
const sites = renderEverySite ? ruleDiagnostics : [representative];
|
|
48448
|
-
if (!(isCollapsedWarningGroup && representative.help.includes(representative.filePath))) for (const cluster of clusterNearbyDiagnostics(sites)) lines.push(...buildDiagnosticClusterLines(cluster, resolveSourceRoot, renderCodeFrame));
|
|
49480
|
+
if (!(isCollapsedWarningGroup && representative.help.includes(representative.filePath))) for (const cluster of clusterNearbyDiagnostics(sites)) lines.push(...buildDiagnosticClusterLines(cluster, resolveSourceRoot, renderCodeFrame, hyperlinks));
|
|
48449
49481
|
return lines;
|
|
48450
49482
|
};
|
|
48451
49483
|
const selectErrorRuleGroups = (diagnostics, rulePriority) => buildSortedRuleGroups(diagnostics.filter((diagnostic) => diagnostic.severity === "error"), rulePriority);
|
|
@@ -48457,8 +49489,21 @@ const buildOverflowSummaryLine = (diagnostics, rulePriority) => {
|
|
|
48457
49489
|
const command = highlighter.bold(highlighter.info("npx react-doctor@latest --verbose"));
|
|
48458
49490
|
return ` ${highlighter.dim("Run")} ${command} ${highlighter.dim("to list every error and warning")}`;
|
|
48459
49491
|
};
|
|
49492
|
+
const formatMigrationBucketLine = (bucket) => `${TOP_ERROR_DETAIL_INDENT}${bucket.title} ${highlighter.gray(`×${bucket.siteCount} across ${bucket.fileCount} files`)}`;
|
|
49493
|
+
const buildMigrationScaleAdvisoryLines = (diagnostics) => {
|
|
49494
|
+
const buckets = findMigrationScaleBuckets(diagnostics);
|
|
49495
|
+
if (buckets.length === 0) return [];
|
|
49496
|
+
const shownBuckets = buckets.slice(0, 3);
|
|
49497
|
+
const lines = [` ${highlighter.warn("⚠")} ${highlighter.bold("Migration-scale change")}${highlighter.dim(": sample before you sweep")}`, ...shownBuckets.map(formatMigrationBucketLine)];
|
|
49498
|
+
const remainingBuckets = buckets.length - shownBuckets.length;
|
|
49499
|
+
if (remainingBuckets > 0) lines.push(highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}+${remainingBuckets} more ${remainingBuckets === 1 ? "rule" : "rules"} at this scale`));
|
|
49500
|
+
for (const guidanceLine of wrapTextToWidth("Fixing all of them at once is hard to review and prone to subtle mistakes across the whole repo. Fix a representative few first and confirm the recipe holds. Then get the code owner's sign-off before changing the rest.", resolveMeasureWidth(4), { breakLongWords: false })) lines.push(highlighter.dim(`${TOP_ERROR_DETAIL_INDENT}${guidanceLine}`));
|
|
49501
|
+
const command = highlighter.info("npx react-doctor@latest <path>");
|
|
49502
|
+
lines.push(`${TOP_ERROR_DETAIL_INDENT}${highlighter.dim("Scope it down one area at a time:")} ${command}`);
|
|
49503
|
+
return lines;
|
|
49504
|
+
};
|
|
48460
49505
|
const getTopErrorRuleKeys = (diagnostics, limit, rulePriority) => new Set(selectTopErrorRuleGroups(diagnostics, limit, rulePriority).map(([ruleKey]) => ruleKey));
|
|
48461
|
-
const buildTopErrorsSection = (diagnostics, resolveSourceRoot, rulePriority) => {
|
|
49506
|
+
const buildTopErrorsSection = (diagnostics, resolveSourceRoot, hyperlinks, rulePriority) => {
|
|
48462
49507
|
const topRuleGroups = selectErrorRuleGroups(diagnostics, rulePriority).slice(0, 3);
|
|
48463
49508
|
if (topRuleGroups.length === 0) return {
|
|
48464
49509
|
lines: [],
|
|
@@ -48468,7 +49513,7 @@ const buildTopErrorsSection = (diagnostics, resolveSourceRoot, rulePriority) =>
|
|
|
48468
49513
|
const blockOffsets = [];
|
|
48469
49514
|
for (const [ruleKey, ruleDiagnostics] of topRuleGroups) {
|
|
48470
49515
|
blockOffsets.push(lines.length);
|
|
48471
|
-
lines.push(...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, false, false));
|
|
49516
|
+
lines.push(...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, false, false, hyperlinks));
|
|
48472
49517
|
lines.push("");
|
|
48473
49518
|
}
|
|
48474
49519
|
return {
|
|
@@ -48506,24 +49551,24 @@ const buildOverviewHeaderLines = (diagnostics) => {
|
|
|
48506
49551
|
* single Effect.forEach over Console.log so failures or fiber
|
|
48507
49552
|
* interruption produce predictable partial output.
|
|
48508
49553
|
*/
|
|
48509
|
-
const printDiagnostics = (diagnostics, isVerbose, sourceRoot, rulePriority, isAgentEnvironment = false, onboarding = {}) => gen(function* () {
|
|
49554
|
+
const printDiagnostics = (diagnostics, isVerbose, sourceRoot, rulePriority, isAgentEnvironment = false, onboarding = {}, hyperlinks = false) => gen(function* () {
|
|
48510
49555
|
const sectionPause = onboarding.sectionPause ?? void_;
|
|
48511
49556
|
const animateCountUp = onboarding.animateCountUp ?? false;
|
|
48512
49557
|
const resolveSourceRoot = typeof sourceRoot === "function" ? sourceRoot : () => sourceRoot;
|
|
48513
49558
|
let detailLines;
|
|
48514
49559
|
let topErrorBlockOffsets = [];
|
|
48515
49560
|
if (!isVerbose) {
|
|
48516
|
-
const topErrors = buildTopErrorsSection(diagnostics, resolveSourceRoot, rulePriority);
|
|
49561
|
+
const topErrors = buildTopErrorsSection(diagnostics, resolveSourceRoot, hyperlinks, rulePriority);
|
|
48517
49562
|
detailLines = topErrors.lines;
|
|
48518
49563
|
topErrorBlockOffsets = topErrors.blockOffsets;
|
|
48519
49564
|
} else detailLines = buildSortedRuleGroups(diagnostics, rulePriority).flatMap(([ruleKey, ruleDiagnostics]) => {
|
|
48520
|
-
return [...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, true, isAgentEnvironment), ""];
|
|
49565
|
+
return [...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, true, isAgentEnvironment, hyperlinks), ""];
|
|
48521
49566
|
});
|
|
48522
49567
|
const overflowLine = isVerbose ? void 0 : buildOverflowSummaryLine(diagnostics, rulePriority);
|
|
48523
49568
|
const categoryTallies = buildCategoryDiagnosticGroups(diagnostics, rulePriority).map(buildCategoryTally);
|
|
48524
49569
|
const categoryLines = buildCategoryTallyLines(categoryTallies);
|
|
48525
49570
|
const overviewDividerLines = detailLines.length > 0 && categoryLines.length > 0 ? [buildSectionDivider()] : [];
|
|
48526
|
-
const { lines, sectionStarts } = joinSections(detailLines, overviewDividerLines, buildOverviewHeaderLines(diagnostics), categoryLines, overflowLine ? [overflowLine] : []);
|
|
49571
|
+
const { lines, sectionStarts } = joinSections(detailLines, overviewDividerLines, buildOverviewHeaderLines(diagnostics), categoryLines, overflowLine ? [overflowLine] : [], buildMigrationScaleAdvisoryLines(diagnostics));
|
|
48527
49572
|
const [detailStart, , , categoryStart] = sectionStarts;
|
|
48528
49573
|
const pauseBeforeLineIndices = detailStart == null ? /* @__PURE__ */ new Set() : new Set(topErrorBlockOffsets.map((offset) => detailStart + offset));
|
|
48529
49574
|
let lineIndex = 0;
|
|
@@ -48578,6 +49623,48 @@ const computeProjectedScore = async (topErrorSource, rescoreSource, currentScore
|
|
|
48578
49623
|
//#endregion
|
|
48579
49624
|
//#region src/cli/utils/filter-diagnostics-by-categories.ts
|
|
48580
49625
|
const filterDiagnosticsByCategories = (diagnostics, categories) => categories.size === 0 ? [...diagnostics] : diagnostics.filter((diagnostic) => categories.has(diagnostic.category));
|
|
49626
|
+
//#endregion
|
|
49627
|
+
//#region src/cli/utils/supports-hyperlinks.ts
|
|
49628
|
+
const HYPERLINK_CAPABLE_TERM_PROGRAMS = new Set([
|
|
49629
|
+
"iTerm.app",
|
|
49630
|
+
"WezTerm",
|
|
49631
|
+
"vscode",
|
|
49632
|
+
"Hyper",
|
|
49633
|
+
"ghostty",
|
|
49634
|
+
"Tabby",
|
|
49635
|
+
"rio"
|
|
49636
|
+
]);
|
|
49637
|
+
const parseVteVersion = (raw) => {
|
|
49638
|
+
const parsed = Number.parseInt(raw ?? "", 10);
|
|
49639
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
49640
|
+
};
|
|
49641
|
+
/**
|
|
49642
|
+
* Whether `stream` is a terminal that renders OSC 8 hyperlinks. Auto-detected
|
|
49643
|
+
* from terminal-identity env vars; the de-facto `FORCE_HYPERLINK` env var
|
|
49644
|
+
* overrides detection (`FORCE_HYPERLINK=0`/`false` forces off, any other value
|
|
49645
|
+
* forces on), mirroring how the ecosystem's terminal libraries gate the same
|
|
49646
|
+
* feature. Off for non-TTYs, `TERM=dumb`, and CI (whose log viewers render the
|
|
49647
|
+
* raw escape rather than a link). Unknown terminals default to off.
|
|
49648
|
+
*/
|
|
49649
|
+
const supportsHyperlinks = (stream = process.stdout, env = process.env) => {
|
|
49650
|
+
const forced = env.FORCE_HYPERLINK;
|
|
49651
|
+
if (forced !== void 0 && forced !== "") return forced !== "0" && forced.toLowerCase() !== "false";
|
|
49652
|
+
if (stream.isTTY !== true) return false;
|
|
49653
|
+
if (env.TERM === "dumb") return false;
|
|
49654
|
+
if (isCiEnvironment(env)) return false;
|
|
49655
|
+
if (env.WT_SESSION) return true;
|
|
49656
|
+
if (env.KITTY_WINDOW_ID || env.TERM === "xterm-kitty") return true;
|
|
49657
|
+
if (parseVteVersion(env.VTE_VERSION) >= 5e3) return true;
|
|
49658
|
+
return Boolean(env.TERM_PROGRAM && HYPERLINK_CAPABLE_TERM_PROGRAMS.has(env.TERM_PROGRAM));
|
|
49659
|
+
};
|
|
49660
|
+
//#endregion
|
|
49661
|
+
//#region src/cli/utils/should-render-hyperlinks.ts
|
|
49662
|
+
/**
|
|
49663
|
+
* Whether to emit OSC 8 clickable `file:line` locations for this run: a
|
|
49664
|
+
* hyperlink-capable terminal AND not a coding agent (whose output parsers
|
|
49665
|
+
* would choke on the escape sequences).
|
|
49666
|
+
*/
|
|
49667
|
+
const shouldRenderHyperlinks = (stream = process.stdout) => supportsHyperlinks(stream) && !isCodingAgentEnvironment();
|
|
48581
49668
|
const FORCE_ONBOARDING_ENV_VAR = "REACT_DOCTOR_FORCE_ONBOARDING";
|
|
48582
49669
|
const FALSY_FLAG_VALUES = new Set([
|
|
48583
49670
|
"",
|
|
@@ -48596,26 +49683,182 @@ const canAnimateOnboarding = (stream = process.stdout) => {
|
|
|
48596
49683
|
return !isGitHookEnvironment() && !isCiEnvironment();
|
|
48597
49684
|
};
|
|
48598
49685
|
//#endregion
|
|
48599
|
-
//#region src/cli/utils/
|
|
48600
|
-
const
|
|
48601
|
-
const
|
|
48602
|
-
const
|
|
48603
|
-
|
|
48604
|
-
|
|
49686
|
+
//#region src/cli/utils/now-iso.ts
|
|
49687
|
+
const nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
49688
|
+
const ONBOARDING_EVENT = "onboarding";
|
|
49689
|
+
const CI_PITCH_EVENT = "ci-pitch";
|
|
49690
|
+
const ACTION_UPGRADE_EVENT = "action-upgrade-v2";
|
|
49691
|
+
const SETUP_HINT_EVENT = "setup-hint";
|
|
49692
|
+
const foldLegacyDecisions = (projects, legacy, eventId) => {
|
|
49693
|
+
for (const [hash, record] of Object.entries(legacy ?? {})) {
|
|
49694
|
+
const existing = projects[hash] ?? { rootDirectory: record.rootDirectory ?? "" };
|
|
49695
|
+
projects[hash] = {
|
|
49696
|
+
...existing,
|
|
49697
|
+
events: {
|
|
49698
|
+
...existing.events,
|
|
49699
|
+
[eventId]: {
|
|
49700
|
+
firedAt: record.at ?? nowIso(),
|
|
49701
|
+
version: 1,
|
|
49702
|
+
...record.outcome ? { outcome: record.outcome } : {}
|
|
49703
|
+
}
|
|
49704
|
+
}
|
|
49705
|
+
};
|
|
49706
|
+
}
|
|
49707
|
+
};
|
|
49708
|
+
const migrateCliState = (state) => {
|
|
49709
|
+
if (state.schemaVersion === 2) return state;
|
|
49710
|
+
const projects = {};
|
|
49711
|
+
for (const [hash, record] of Object.entries(state.projects ?? {})) {
|
|
49712
|
+
const carried = {
|
|
49713
|
+
rootDirectory: record.rootDirectory,
|
|
49714
|
+
...record.events ? { events: record.events } : {},
|
|
49715
|
+
...record.migrations ? { migrations: record.migrations } : {}
|
|
49716
|
+
};
|
|
49717
|
+
projects[hash] = record.setupPrompt === false ? {
|
|
49718
|
+
...carried,
|
|
49719
|
+
events: {
|
|
49720
|
+
...carried.events,
|
|
49721
|
+
[SETUP_HINT_EVENT]: {
|
|
49722
|
+
firedAt: nowIso(),
|
|
49723
|
+
version: 1,
|
|
49724
|
+
outcome: "declined"
|
|
49725
|
+
}
|
|
49726
|
+
}
|
|
49727
|
+
} : carried;
|
|
49728
|
+
}
|
|
49729
|
+
foldLegacyDecisions(projects, state.ciPrompts, CI_PITCH_EVENT);
|
|
49730
|
+
foldLegacyDecisions(projects, state.actionUpgrades, ACTION_UPGRADE_EVENT);
|
|
49731
|
+
return {
|
|
49732
|
+
schemaVersion: 2,
|
|
49733
|
+
global: typeof state.onboardedAt === "string" ? { events: { [ONBOARDING_EVENT]: {
|
|
49734
|
+
firedAt: state.onboardedAt,
|
|
49735
|
+
version: 1
|
|
49736
|
+
} } } : {},
|
|
49737
|
+
projects
|
|
49738
|
+
};
|
|
49739
|
+
};
|
|
49740
|
+
const resolveConfigDir = (options) => options.cwd ?? (process.env["REACT_DOCTOR_CONFIG_DIR"] || void 0);
|
|
49741
|
+
const openStore = (options = {}) => new Conf({
|
|
49742
|
+
projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
|
|
49743
|
+
cwd: resolveConfigDir(options)
|
|
48605
49744
|
});
|
|
48606
|
-
const
|
|
49745
|
+
const openMigratedStore = (options) => {
|
|
49746
|
+
const store = openStore(options);
|
|
49747
|
+
if (store.store.schemaVersion !== 2) store.store = migrateCliState(store.store);
|
|
49748
|
+
return store;
|
|
49749
|
+
};
|
|
49750
|
+
const readCliState = (select, fallback, options = {}) => {
|
|
48607
49751
|
try {
|
|
48608
|
-
return
|
|
49752
|
+
return select(openMigratedStore(options).store);
|
|
48609
49753
|
} catch {
|
|
49754
|
+
return fallback;
|
|
49755
|
+
}
|
|
49756
|
+
};
|
|
49757
|
+
const updateCliState = (update, options = {}) => {
|
|
49758
|
+
try {
|
|
49759
|
+
const store = openMigratedStore(options);
|
|
49760
|
+
store.store = update(store.store);
|
|
48610
49761
|
return true;
|
|
49762
|
+
} catch {
|
|
49763
|
+
return false;
|
|
48611
49764
|
}
|
|
48612
49765
|
};
|
|
49766
|
+
//#endregion
|
|
49767
|
+
//#region src/cli/utils/hash-project-root.ts
|
|
49768
|
+
const hashProjectRoot = (projectRoot) => createHash("sha256").update(Path.resolve(projectRoot)).digest("hex");
|
|
49769
|
+
//#endregion
|
|
49770
|
+
//#region src/cli/utils/cli-lifecycle.ts
|
|
49771
|
+
const versionOf = (item) => item.version ?? 1;
|
|
49772
|
+
const selectScope = (state, scoped, projectRoot) => scoped.scope === "global" ? state.global : projectRoot === void 0 ? void 0 : state.projects?.[hashProjectRoot(projectRoot)];
|
|
49773
|
+
const updateScope = (state, scoped, projectRoot, updateScopeState) => {
|
|
49774
|
+
if (scoped.scope === "global") return {
|
|
49775
|
+
...state,
|
|
49776
|
+
global: updateScopeState(state.global ?? {})
|
|
49777
|
+
};
|
|
49778
|
+
if (projectRoot === void 0) return state;
|
|
49779
|
+
const projectKey = hashProjectRoot(projectRoot);
|
|
49780
|
+
const base = state.projects?.[projectKey] ?? { rootDirectory: Path.resolve(projectRoot) };
|
|
49781
|
+
return {
|
|
49782
|
+
...state,
|
|
49783
|
+
projects: {
|
|
49784
|
+
...state.projects,
|
|
49785
|
+
[projectKey]: {
|
|
49786
|
+
...base,
|
|
49787
|
+
...updateScopeState(base)
|
|
49788
|
+
}
|
|
49789
|
+
}
|
|
49790
|
+
};
|
|
49791
|
+
};
|
|
49792
|
+
const isGatePending = (gate, target = {}, options = {}) => {
|
|
49793
|
+
if (gate.scope === "project" && target.projectRoot === void 0) return false;
|
|
49794
|
+
return readCliState((state) => {
|
|
49795
|
+
const record = selectScope(state, gate, target.projectRoot)?.events?.[gate.id];
|
|
49796
|
+
return !record || record.version < versionOf(gate);
|
|
49797
|
+
}, gate.fireWhenUnknown ?? false, options);
|
|
49798
|
+
};
|
|
49799
|
+
const recordGate = (gate, target = {}, options = {}) => updateCliState((state) => updateScope(state, gate, target.projectRoot, (scope) => ({
|
|
49800
|
+
...scope,
|
|
49801
|
+
events: {
|
|
49802
|
+
...scope.events,
|
|
49803
|
+
[gate.id]: {
|
|
49804
|
+
firedAt: nowIso(),
|
|
49805
|
+
version: versionOf(gate),
|
|
49806
|
+
...target.outcome ? { outcome: target.outcome } : {}
|
|
49807
|
+
}
|
|
49808
|
+
}
|
|
49809
|
+
})), options);
|
|
49810
|
+
const isMigrationPending = (migration, target = {}, options = {}) => {
|
|
49811
|
+
if (migration.scope === "project" && target.projectRoot === void 0) return false;
|
|
49812
|
+
return readCliState((state) => {
|
|
49813
|
+
const record = selectScope(state, migration, target.projectRoot)?.migrations?.[migration.id];
|
|
49814
|
+
return !record || record.version < versionOf(migration);
|
|
49815
|
+
}, false, options);
|
|
49816
|
+
};
|
|
49817
|
+
const recordMigration = (migration, projectRoot, record, options) => updateCliState((state) => updateScope(state, migration, projectRoot, (scope) => ({
|
|
49818
|
+
...scope,
|
|
49819
|
+
migrations: {
|
|
49820
|
+
...scope.migrations,
|
|
49821
|
+
[migration.id]: record
|
|
49822
|
+
}
|
|
49823
|
+
})), options);
|
|
49824
|
+
const runMigrations = async (migrations, target = {}, options = {}) => {
|
|
49825
|
+
const results = [];
|
|
49826
|
+
for (const migration of migrations) {
|
|
49827
|
+
if (!isMigrationPending(migration, target, options)) {
|
|
49828
|
+
results.push({
|
|
49829
|
+
id: migration.id,
|
|
49830
|
+
ran: false,
|
|
49831
|
+
applied: true
|
|
49832
|
+
});
|
|
49833
|
+
continue;
|
|
49834
|
+
}
|
|
49835
|
+
let applied = false;
|
|
49836
|
+
try {
|
|
49837
|
+
applied = await migration.run({ projectRoot: target.projectRoot });
|
|
49838
|
+
} catch {
|
|
49839
|
+
applied = false;
|
|
49840
|
+
}
|
|
49841
|
+
if (applied) recordMigration(migration, target.projectRoot, {
|
|
49842
|
+
ranAt: nowIso(),
|
|
49843
|
+
version: versionOf(migration)
|
|
49844
|
+
}, options);
|
|
49845
|
+
results.push({
|
|
49846
|
+
id: migration.id,
|
|
49847
|
+
ran: true,
|
|
49848
|
+
applied
|
|
49849
|
+
});
|
|
49850
|
+
}
|
|
49851
|
+
return results;
|
|
49852
|
+
};
|
|
49853
|
+
//#endregion
|
|
49854
|
+
//#region src/cli/utils/onboarding-state.ts
|
|
49855
|
+
const ONBOARDING_GATE = {
|
|
49856
|
+
id: ONBOARDING_EVENT,
|
|
49857
|
+
scope: "global"
|
|
49858
|
+
};
|
|
49859
|
+
const hasCompletedOnboarding = (options = {}) => !isGatePending(ONBOARDING_GATE, {}, options);
|
|
48613
49860
|
const markOnboardingComplete = (options = {}) => {
|
|
48614
|
-
|
|
48615
|
-
const store = getOnboardingStore(options);
|
|
48616
|
-
if (typeof store.get(ONBOARDED_AT_KEY) === "string") return;
|
|
48617
|
-
store.set(ONBOARDED_AT_KEY, (/* @__PURE__ */ new Date()).toISOString());
|
|
48618
|
-
} catch {}
|
|
49861
|
+
if (isGatePending(ONBOARDING_GATE, {}, options)) recordGate(ONBOARDING_GATE, {}, options);
|
|
48619
49862
|
};
|
|
48620
49863
|
//#endregion
|
|
48621
49864
|
//#region src/cli/utils/render-project-detection.ts
|
|
@@ -49056,6 +50299,78 @@ const resolveCliCategories = (categoryFlag) => {
|
|
|
49056
50299
|
return resolvedCategories.length > 0 ? resolvedCategories : void 0;
|
|
49057
50300
|
};
|
|
49058
50301
|
//#endregion
|
|
50302
|
+
//#region src/cli/utils/git-hook-shared.ts
|
|
50303
|
+
const HOOK_FILE_NAME = "pre-commit";
|
|
50304
|
+
const HOOK_RELATIVE_PATH = "hooks/pre-commit";
|
|
50305
|
+
const LEGACY_HOOK_RUNNER_RELATIVE_PATH = ".react-doctor/hooks/pre-commit";
|
|
50306
|
+
const HUSKY_HOOKS_PATH = ".husky";
|
|
50307
|
+
const VITE_PLUS_HOOKS_PATH = ".vite-hooks";
|
|
50308
|
+
const LEFTHOOK_CONFIG_FILES = ["lefthook.yml", "lefthook.yaml"];
|
|
50309
|
+
const PRE_COMMIT_CONFIG_FILE = ".pre-commit-config.yaml";
|
|
50310
|
+
const OVERCOMMIT_CONFIG_FILE = ".overcommit.yml";
|
|
50311
|
+
const REACT_DOCTOR_COMMAND = "react-doctor --staged --blocking warning";
|
|
50312
|
+
const NON_BLOCKING_REACT_DOCTOR_COMMAND = [
|
|
50313
|
+
"react_doctor_output=$(mktemp \"${TMPDIR:-/tmp}/react-doctor-hook.XXXXXX\");",
|
|
50314
|
+
`if ${REACT_DOCTOR_COMMAND} > "$react_doctor_output" 2>&1; then`,
|
|
50315
|
+
"rm -f \"$react_doctor_output\";",
|
|
50316
|
+
"else",
|
|
50317
|
+
"rm -f \"$react_doctor_output\";",
|
|
50318
|
+
`printf "%s\\n" "React Doctor found staged regressions." "Run ${REACT_DOCTOR_COMMAND} to inspect." "Want them fixed? Ask your agent to run that command and resolve the findings." >&2;`,
|
|
50319
|
+
"fi"
|
|
50320
|
+
].join(" ");
|
|
50321
|
+
const PACKAGE_JSON_FILE_NAME = "package.json";
|
|
50322
|
+
const runGit = (projectRoot, args) => {
|
|
50323
|
+
try {
|
|
50324
|
+
return execFileSync("git", [...args], {
|
|
50325
|
+
cwd: projectRoot,
|
|
50326
|
+
encoding: "utf8",
|
|
50327
|
+
stdio: [
|
|
50328
|
+
"ignore",
|
|
50329
|
+
"pipe",
|
|
50330
|
+
"ignore"
|
|
50331
|
+
]
|
|
50332
|
+
}).trim();
|
|
50333
|
+
} catch {
|
|
50334
|
+
return null;
|
|
50335
|
+
}
|
|
50336
|
+
};
|
|
50337
|
+
const resolveGitPath = (baseDirectory, value) => Path.isAbsolute(value) ? value : Path.resolve(baseDirectory, value);
|
|
50338
|
+
const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
50339
|
+
const getPackageJsonPath = (projectRoot) => Path.join(projectRoot, PACKAGE_JSON_FILE_NAME);
|
|
50340
|
+
const readPackageJson = (projectRoot) => {
|
|
50341
|
+
try {
|
|
50342
|
+
return JSON.parse(NFS.readFileSync(getPackageJsonPath(projectRoot), "utf8"));
|
|
50343
|
+
} catch {
|
|
50344
|
+
return null;
|
|
50345
|
+
}
|
|
50346
|
+
};
|
|
50347
|
+
const writeJsonFile$1 = (filePath, value) => {
|
|
50348
|
+
NFS.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
50349
|
+
};
|
|
50350
|
+
const packageHasDependency = (projectRoot, dependencyName) => {
|
|
50351
|
+
const packageJson = readPackageJson(projectRoot);
|
|
50352
|
+
if (!isRecord$1(packageJson)) return false;
|
|
50353
|
+
return [
|
|
50354
|
+
"dependencies",
|
|
50355
|
+
"devDependencies",
|
|
50356
|
+
"optionalDependencies"
|
|
50357
|
+
].some((fieldName) => {
|
|
50358
|
+
const dependencies = packageJson[fieldName];
|
|
50359
|
+
return isRecord$1(dependencies) && typeof dependencies[dependencyName] === "string";
|
|
50360
|
+
});
|
|
50361
|
+
};
|
|
50362
|
+
const packageHasRecordKey = (projectRoot, key) => {
|
|
50363
|
+
const packageJson = readPackageJson(projectRoot);
|
|
50364
|
+
return isRecord$1(packageJson) && isRecord$1(packageJson[key]);
|
|
50365
|
+
};
|
|
50366
|
+
const packageHasNestedRecordKey = (projectRoot, key, nestedKey) => {
|
|
50367
|
+
const packageJson = readPackageJson(projectRoot);
|
|
50368
|
+
if (!isRecord$1(packageJson)) return false;
|
|
50369
|
+
const value = packageJson[key];
|
|
50370
|
+
return isRecord$1(value) && isRecord$1(value[nestedKey]);
|
|
50371
|
+
};
|
|
50372
|
+
const ensureTrailingNewline = (content) => content.endsWith("\n") ? content : `${content}\n`;
|
|
50373
|
+
//#endregion
|
|
49059
50374
|
//#region src/cli/utils/scan-result-cache.ts
|
|
49060
50375
|
const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
|
|
49061
50376
|
const TOOLCHAIN_PACKAGE_SPECIFIERS = [
|
|
@@ -49066,7 +50381,7 @@ const TOOLCHAIN_PACKAGE_SPECIFIERS = [
|
|
|
49066
50381
|
"eslint-plugin-react-hooks/package.json"
|
|
49067
50382
|
];
|
|
49068
50383
|
const bundledRequire = createRequire(import.meta.url);
|
|
49069
|
-
const isRecord
|
|
50384
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
49070
50385
|
const normalizeForStableJson = (value) => {
|
|
49071
50386
|
if (value === null) return null;
|
|
49072
50387
|
if (value === void 0) return void 0;
|
|
@@ -49095,24 +50410,9 @@ const stringifyStableJson = (value) => {
|
|
|
49095
50410
|
}
|
|
49096
50411
|
};
|
|
49097
50412
|
const hashString = (value) => crypto.createHash("sha1").update(value).digest("hex");
|
|
49098
|
-
const
|
|
49099
|
-
try {
|
|
49100
|
-
return execFileSync("git", [...args], {
|
|
49101
|
-
cwd: directory,
|
|
49102
|
-
encoding: "utf8",
|
|
49103
|
-
stdio: [
|
|
49104
|
-
"ignore",
|
|
49105
|
-
"pipe",
|
|
49106
|
-
"ignore"
|
|
49107
|
-
]
|
|
49108
|
-
}).trim();
|
|
49109
|
-
} catch {
|
|
49110
|
-
return null;
|
|
49111
|
-
}
|
|
49112
|
-
};
|
|
49113
|
-
const readHeadSha = (projectDirectory) => runGit$1(projectDirectory, ["rev-parse", "HEAD"]);
|
|
50413
|
+
const readHeadSha = (projectDirectory) => runGit(projectDirectory, ["rev-parse", "HEAD"]);
|
|
49114
50414
|
const isWorktreeClean = (projectDirectory) => {
|
|
49115
|
-
const status = runGit
|
|
50415
|
+
const status = runGit(projectDirectory, [
|
|
49116
50416
|
"status",
|
|
49117
50417
|
"--porcelain=v1",
|
|
49118
50418
|
"--untracked-files=normal"
|
|
@@ -49120,7 +50420,7 @@ const isWorktreeClean = (projectDirectory) => {
|
|
|
49120
50420
|
return status !== null && status.length === 0;
|
|
49121
50421
|
};
|
|
49122
50422
|
const hasHiddenTrackedFileState = (projectDirectory) => {
|
|
49123
|
-
const output = runGit
|
|
50423
|
+
const output = runGit(projectDirectory, ["ls-files", "-v"]);
|
|
49124
50424
|
if (output === null) return true;
|
|
49125
50425
|
return output.split("\n").some((line) => line.length > 0 && line[0] !== "H");
|
|
49126
50426
|
};
|
|
@@ -49133,27 +50433,27 @@ const resolveCacheFilePath = (projectDirectory) => {
|
|
|
49133
50433
|
const readPersistedCache = (cacheFilePath) => {
|
|
49134
50434
|
try {
|
|
49135
50435
|
const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
|
|
49136
|
-
if (!isRecord
|
|
49137
|
-
version:
|
|
50436
|
+
if (!isRecord(parsed) || parsed.version !== 2) return {
|
|
50437
|
+
version: 2,
|
|
49138
50438
|
entries: []
|
|
49139
50439
|
};
|
|
49140
50440
|
if (!Array.isArray(parsed.entries)) return {
|
|
49141
|
-
version:
|
|
50441
|
+
version: 2,
|
|
49142
50442
|
entries: []
|
|
49143
50443
|
};
|
|
49144
50444
|
const entries = [];
|
|
49145
50445
|
for (const entry of parsed.entries) {
|
|
49146
|
-
if (!isRecord
|
|
49147
|
-
if (!isRecord
|
|
50446
|
+
if (!isRecord(entry) || typeof entry.key !== "string" || typeof entry.createdAtMs !== "number") continue;
|
|
50447
|
+
if (!isRecord(entry.payload) || !Array.isArray(entry.payload.diagnostics)) continue;
|
|
49148
50448
|
entries.push(entry);
|
|
49149
50449
|
}
|
|
49150
50450
|
return {
|
|
49151
|
-
version:
|
|
50451
|
+
version: 2,
|
|
49152
50452
|
entries
|
|
49153
50453
|
};
|
|
49154
50454
|
} catch {
|
|
49155
50455
|
return {
|
|
49156
|
-
version:
|
|
50456
|
+
version: 2,
|
|
49157
50457
|
entries: []
|
|
49158
50458
|
};
|
|
49159
50459
|
}
|
|
@@ -49206,7 +50506,7 @@ const buildScanResultCacheKey = (input) => {
|
|
|
49206
50506
|
if (headSha === null) return null;
|
|
49207
50507
|
if (stringifyStableJson(input.userConfig) === null) return null;
|
|
49208
50508
|
const cacheKeyJson = stringifyStableJson({
|
|
49209
|
-
schemaVersion:
|
|
50509
|
+
schemaVersion: 2,
|
|
49210
50510
|
projectIdentity: resolveProjectIdentity(input.projectDirectory),
|
|
49211
50511
|
headSha,
|
|
49212
50512
|
reactDoctorVersion: input.version,
|
|
@@ -49226,6 +50526,7 @@ const buildScanResultCacheKey = (input) => {
|
|
|
49226
50526
|
adoptExistingLintConfig: input.options.adoptExistingLintConfig,
|
|
49227
50527
|
ignoredTags: [...input.options.ignoredTags].sort(),
|
|
49228
50528
|
concurrency: input.options.concurrency,
|
|
50529
|
+
lintBatchOrdering: resolveLintBatchOrdering(),
|
|
49229
50530
|
baselineRef: input.options.baseline?.ref,
|
|
49230
50531
|
changedLineRanges: input.options.changedLineRanges ?? void 0,
|
|
49231
50532
|
noScore: input.options.noScore,
|
|
@@ -49243,7 +50544,7 @@ const createScanResultCache = (projectDirectory) => {
|
|
|
49243
50544
|
for (const entry of persistedCache.entries) entries.set(entry.key, entry);
|
|
49244
50545
|
const persist = () => {
|
|
49245
50546
|
writePersistedCache(cacheFilePath, {
|
|
49246
|
-
version:
|
|
50547
|
+
version: 2,
|
|
49247
50548
|
entries: [...entries.values()].sort((firstEntry, secondEntry) => secondEntry.createdAtMs - firstEntry.createdAtMs).slice(0, 20)
|
|
49248
50549
|
});
|
|
49249
50550
|
};
|
|
@@ -49259,7 +50560,7 @@ const createScanResultCache = (projectDirectory) => {
|
|
|
49259
50560
|
}
|
|
49260
50561
|
};
|
|
49261
50562
|
};
|
|
49262
|
-
const shouldStoreScanPayload = (payload) => !payload.didLintFail && !payload.didDeadCodeFail && payload.lintPartialFailures.length === 0;
|
|
50563
|
+
const shouldStoreScanPayload = (payload) => !payload.didLintFail && !payload.didDeadCodeFail && payload.lintPartialFailures.length === 0 && !payload.supplyChainOverlapTimedOut;
|
|
49263
50564
|
//#endregion
|
|
49264
50565
|
//#region src/inspect.ts
|
|
49265
50566
|
const silentConsole = makeNoopConsole();
|
|
@@ -49323,21 +50624,24 @@ const deriveScope = (options) => {
|
|
|
49323
50624
|
if (options.changedLineRanges !== null) return "lines";
|
|
49324
50625
|
return options.includePaths.length > 0 ? "files" : "full";
|
|
49325
50626
|
};
|
|
49326
|
-
const buildRunEventConfig = (options, userConfig, hasCustomConfig) =>
|
|
49327
|
-
|
|
49328
|
-
|
|
49329
|
-
|
|
49330
|
-
|
|
49331
|
-
|
|
49332
|
-
|
|
49333
|
-
|
|
49334
|
-
|
|
49335
|
-
|
|
49336
|
-
|
|
49337
|
-
|
|
49338
|
-
|
|
49339
|
-
|
|
49340
|
-
|
|
50627
|
+
const buildRunEventConfig = (options, userConfig, hasCustomConfig, resolvedWorkerCount) => {
|
|
50628
|
+
const { workerCount, parallel } = resolveWorkerTelemetry(resolvedWorkerCount, options.concurrency);
|
|
50629
|
+
return {
|
|
50630
|
+
scope: deriveScope(options),
|
|
50631
|
+
parallel,
|
|
50632
|
+
workerCount,
|
|
50633
|
+
lint: options.lint,
|
|
50634
|
+
deadCode: options.deadCode,
|
|
50635
|
+
scoreOnly: options.scoreOnly,
|
|
50636
|
+
noScore: options.noScore,
|
|
50637
|
+
respectInlineDisables: options.respectInlineDisables,
|
|
50638
|
+
showWarnings: options.warnings,
|
|
50639
|
+
usedOutputDir: options.outputDirectory !== null,
|
|
50640
|
+
ignoredTagCount: options.ignoredTags.size,
|
|
50641
|
+
hasCustomConfig,
|
|
50642
|
+
userConfig
|
|
50643
|
+
};
|
|
50644
|
+
};
|
|
49341
50645
|
const inspect = async (directory, inputOptions = {}) => {
|
|
49342
50646
|
const startTime = performance$1.now();
|
|
49343
50647
|
const isConcurrentScan = inputOptions.concurrentScan === true;
|
|
@@ -49429,7 +50733,7 @@ const runBaselineComparison = async (params) => {
|
|
|
49429
50733
|
resolveLocalGithubViewerPermission: false,
|
|
49430
50734
|
suppressScanSummary: true,
|
|
49431
50735
|
supplyChainManifestChanged: params.options.supplyChainManifestChanged
|
|
49432
|
-
}, {}).pipe(provide(baseLayers), provideService(Console, silentConsole))));
|
|
50736
|
+
}, {}).pipe(provide(baseLayers), provideService$2(Console, silentConsole))));
|
|
49433
50737
|
if (baseOutput.didLintFail) return null;
|
|
49434
50738
|
const delta = computeDiagnosticDelta({
|
|
49435
50739
|
headDiagnostics: params.headDiagnostics,
|
|
@@ -49514,7 +50818,8 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
|
|
|
49514
50818
|
runId: getRunId(),
|
|
49515
50819
|
resolveLocalGithubViewerPermission: !options.noScore,
|
|
49516
50820
|
suppressScanSummary: options.suppressRendering,
|
|
49517
|
-
supplyChainManifestChanged: options.supplyChainManifestChanged
|
|
50821
|
+
supplyChainManifestChanged: options.supplyChainManifestChanged,
|
|
50822
|
+
concurrentScan: options.concurrentScan
|
|
49518
50823
|
}, { beforeLint: (projectInfo, lintIncludePaths) => gen(function* () {
|
|
49519
50824
|
recordSentryProjectContext(projectInfo, rootSentrySpan, { concurrentScan: options.concurrentScan });
|
|
49520
50825
|
recordCount(METRIC.projectDetected, 1);
|
|
@@ -49528,7 +50833,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
|
|
|
49528
50833
|
lintSourceFileCount
|
|
49529
50834
|
});
|
|
49530
50835
|
}) });
|
|
49531
|
-
const output = await runPromise(restoreLegacyThrow(applyObservability(options.silent ? program.pipe(provide(layers), provideService(Console, silentConsole)) : program.pipe(provide(layers)), rootSentrySpan)));
|
|
50836
|
+
const output = await runPromise(restoreLegacyThrow(applyObservability(options.silent ? program.pipe(provide(layers), provideService$2(Console, silentConsole)) : program.pipe(provide(layers)), rootSentrySpan)));
|
|
49532
50837
|
const didLintFail = lintBindingMissing || output.didLintFail;
|
|
49533
50838
|
const lintFailureReason = lintBindingMissing ? `oxlint native binding not found for Node ${process.version}; expected one matching ${OXLINT_NODE_REQUIREMENT}` : output.lintFailureReason;
|
|
49534
50839
|
if (!options.scoreOnly && !lintBindingMissing && output.didLintFail && lintFailureReason !== null) if (output.lintFailureReasonKind === "native-binding-missing") runConsole(log(highlighter.gray(` Upgrade to Node ${OXLINT_NODE_REQUIREMENT} or run: npx -p oxlint@latest react-doctor@latest`)));
|
|
@@ -49565,12 +50870,15 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
|
|
|
49565
50870
|
lintPartialFailures: output.lintPartialFailures,
|
|
49566
50871
|
didDeadCodeFail: output.didDeadCodeFail,
|
|
49567
50872
|
deadCodeFailureReason: output.deadCodeFailureReason,
|
|
50873
|
+
deadCodeOverlapped: output.deadCodeOverlapped,
|
|
49568
50874
|
directory: output.resolvedDirectory,
|
|
49569
50875
|
scannedFileCount: output.scannedFileCount,
|
|
49570
50876
|
scannedFilePaths: output.scannedFilePaths,
|
|
49571
50877
|
scanElapsedMilliseconds: output.scanElapsedMilliseconds,
|
|
50878
|
+
scanConcurrency: output.scanConcurrency,
|
|
49572
50879
|
baselineDelta,
|
|
49573
|
-
lintFailureReasonKind: lintBindingMissing ? "native-binding-missing" : output.lintFailureReasonKind
|
|
50880
|
+
lintFailureReasonKind: lintBindingMissing ? "native-binding-missing" : output.lintFailureReasonKind,
|
|
50881
|
+
supplyChainOverlapTimedOut: output.supplyChainOverlapTimedOut
|
|
49574
50882
|
};
|
|
49575
50883
|
if (cacheKey !== null && scanResultCache !== null && shouldStoreScanPayload(payload)) scanResultCache.store(cacheKey, payload);
|
|
49576
50884
|
const result = await renderAndRecordScan({
|
|
@@ -49581,12 +50889,14 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
|
|
|
49581
50889
|
startTime,
|
|
49582
50890
|
rootSentrySpan,
|
|
49583
50891
|
scanMode: baselineDelta ? "baseline" : isDiffMode ? "diff" : "full",
|
|
49584
|
-
baselineDegraded
|
|
50892
|
+
baselineDegraded,
|
|
50893
|
+
lintCacheHitFileCount: output.lintCacheHitFileCount,
|
|
50894
|
+
lintCacheTotalFileCount: output.lintCacheTotalFileCount
|
|
49585
50895
|
});
|
|
49586
50896
|
recordOnboardingCompletion(options);
|
|
49587
50897
|
return result;
|
|
49588
50898
|
};
|
|
49589
|
-
const runMaybeSilent = (effect, silent) => silent ? effect.pipe(provideService(Console, silentConsole)) : effect;
|
|
50899
|
+
const runMaybeSilent = (effect, silent) => silent ? effect.pipe(provideService$2(Console, silentConsole)) : effect;
|
|
49590
50900
|
const renderCachedProjectDetection = async (input) => {
|
|
49591
50901
|
if (input.options.scoreOnly || input.options.suppressRendering) return;
|
|
49592
50902
|
await runPromise(runMaybeSilent(printProjectDetection({
|
|
@@ -49614,14 +50924,17 @@ const renderAndRecordScan = async (input) => {
|
|
|
49614
50924
|
scannedFileCount: input.payload.scannedFileCount,
|
|
49615
50925
|
scannedFilePaths: input.payload.scannedFilePaths,
|
|
49616
50926
|
scanElapsedMilliseconds: input.payload.scanElapsedMilliseconds,
|
|
50927
|
+
lintCacheHitFileCount: input.lintCacheHitFileCount ?? null,
|
|
50928
|
+
lintCacheTotalFileCount: input.lintCacheTotalFileCount ?? null,
|
|
49617
50929
|
baselineDelta: input.payload.baselineDelta
|
|
49618
50930
|
}), input.options.silent));
|
|
50931
|
+
const { workerCount: resolvedWorkerCount, parallel } = resolveWorkerTelemetry(input.payload.scanConcurrency, input.options.concurrency);
|
|
49619
50932
|
recordScanMetrics({
|
|
49620
50933
|
result,
|
|
49621
50934
|
mode: input.scanMode,
|
|
49622
50935
|
baselineDegraded: input.baselineDegraded,
|
|
49623
|
-
parallel
|
|
49624
|
-
workerCount:
|
|
50936
|
+
parallel,
|
|
50937
|
+
workerCount: resolvedWorkerCount,
|
|
49625
50938
|
lint: input.options.lint,
|
|
49626
50939
|
deadCode: input.options.deadCode,
|
|
49627
50940
|
scoreOnly: input.options.scoreOnly,
|
|
@@ -49631,19 +50944,22 @@ const renderAndRecordScan = async (input) => {
|
|
|
49631
50944
|
didDeadCodeFail: input.payload.didDeadCodeFail
|
|
49632
50945
|
});
|
|
49633
50946
|
recordRunEvent(input.rootSentrySpan, {
|
|
49634
|
-
...buildRunEventConfig(input.options, input.userConfig, input.hasCustomConfig),
|
|
50947
|
+
...buildRunEventConfig(input.options, input.userConfig, input.hasCustomConfig, resolvedWorkerCount),
|
|
49635
50948
|
result,
|
|
49636
50949
|
mode: input.scanMode,
|
|
49637
50950
|
gateExempt: input.baselineDegraded,
|
|
49638
50951
|
didLintFail: input.payload.didLintFail,
|
|
49639
50952
|
lintFailureReasonKind: input.payload.lintFailureReasonKind,
|
|
49640
50953
|
lintPartialFailureCount: input.payload.lintPartialFailures.length,
|
|
49641
|
-
|
|
50954
|
+
lintDroppedFileCount: countDroppedLintFiles(input.payload.lintPartialFailures),
|
|
50955
|
+
didDeadCodeFail: input.payload.didDeadCodeFail,
|
|
50956
|
+
supplyChainOverlapTimedOut: input.payload.supplyChainOverlapTimedOut,
|
|
50957
|
+
deadCodeOverlapped: input.payload.deadCodeOverlapped
|
|
49642
50958
|
});
|
|
49643
50959
|
return result;
|
|
49644
50960
|
};
|
|
49645
50961
|
const finalizeAndRender = (input) => gen(function* () {
|
|
49646
|
-
const { options, elapsedMilliseconds, diagnostics, score, project, userConfig, didLintFail, lintFailureReason, lintPartialFailures, didDeadCodeFail, deadCodeFailureReason, directory, scannedFileCount, scannedFilePaths, scanElapsedMilliseconds, baselineDelta } = input;
|
|
50962
|
+
const { options, elapsedMilliseconds, diagnostics, score, project, userConfig, didLintFail, lintFailureReason, lintPartialFailures, didDeadCodeFail, deadCodeFailureReason, directory, scannedFileCount, scannedFilePaths, scanElapsedMilliseconds, lintCacheHitFileCount, lintCacheTotalFileCount, baselineDelta } = input;
|
|
49647
50963
|
const { skippedChecks, skippedCheckReasons } = buildSkippedChecks({
|
|
49648
50964
|
didLintFail,
|
|
49649
50965
|
lintFailureReason,
|
|
@@ -49663,6 +50979,10 @@ const finalizeAndRender = (input) => gen(function* () {
|
|
|
49663
50979
|
scannedFileCount,
|
|
49664
50980
|
scannedFilePaths,
|
|
49665
50981
|
scanElapsedMilliseconds,
|
|
50982
|
+
...lintCacheTotalFileCount !== null ? {
|
|
50983
|
+
lintCacheHitFileCount,
|
|
50984
|
+
lintCacheTotalFileCount
|
|
50985
|
+
} : {},
|
|
49666
50986
|
...baselineDelta ? { baselineDelta } : {}
|
|
49667
50987
|
});
|
|
49668
50988
|
if (options.suppressRendering) return buildResult();
|
|
@@ -49676,6 +50996,7 @@ const finalizeAndRender = (input) => gen(function* () {
|
|
|
49676
50996
|
}
|
|
49677
50997
|
const animateRender = !options.silent && !options.verbose && canAnimateOnboarding(process.stdout);
|
|
49678
50998
|
const pause = onboardingSectionPause(animateRender);
|
|
50999
|
+
const useHyperlinks = shouldRenderHyperlinks(process.stdout);
|
|
49679
51000
|
const demotedDiagnosticCount = diagnostics.length - surfaceDiagnostics.length;
|
|
49680
51001
|
const lintSourceFileCount = options.includePaths.length > 0 ? options.includePaths.length : project.sourceFileCount;
|
|
49681
51002
|
if (printedDiagnostics.length === 0) {
|
|
@@ -49701,7 +51022,7 @@ const finalizeAndRender = (input) => gen(function* () {
|
|
|
49701
51022
|
yield* printDiagnostics([...printedDiagnostics], options.verbose, directory, buildRulePriorityMap([score]), isCodingAgentEnvironment(), {
|
|
49702
51023
|
sectionPause: pause,
|
|
49703
51024
|
animateCountUp: animateRender
|
|
49704
|
-
});
|
|
51025
|
+
}, useHyperlinks);
|
|
49705
51026
|
if (options.isNonInteractiveEnvironment && options.outputSurface !== "prComment") yield* printAgentGuidance();
|
|
49706
51027
|
if (options.categoryFilters.size === 0 && demotedDiagnosticCount > 0) {
|
|
49707
51028
|
yield* log(highlighter.gray(` ${demotedDiagnosticCount} demoted from the ${options.outputSurface} surface (e.g. design cleanup) — run \`npx react-doctor@latest .\` locally for the full list.`));
|
|
@@ -49890,24 +51211,18 @@ const isExpectedUserError = (error) => error instanceof CliInputError || isProje
|
|
|
49890
51211
|
//#region src/cli/utils/build-handoff-payload.ts
|
|
49891
51212
|
const buildHandoffPayload = (input) => {
|
|
49892
51213
|
const topGroups = buildSortedRuleGroups(input.diagnostics).slice(0, 3);
|
|
51214
|
+
const migrationScaleBuckets = new Map(findMigrationScaleBuckets(input.diagnostics).map((bucket) => [bucket.ruleKey, bucket]));
|
|
49893
51215
|
let outputDirectory = null;
|
|
49894
51216
|
try {
|
|
49895
51217
|
outputDirectory = writeDiagnosticsDirectory([...input.diagnostics], input.outputDirectory);
|
|
49896
51218
|
} catch {}
|
|
49897
|
-
const lines = [
|
|
49898
|
-
"Before fixing anything, first ask me whether I'd like to add React Doctor to CI so every pull request is scanned automatically. Explain that:",
|
|
49899
|
-
`- It's used by teams at ${CI_TRUST_COMPANIES}.`,
|
|
49900
|
-
"- I don't have to fix everything at once; CI keeps new PRs clean while I incrementally pay down the existing issues.",
|
|
49901
|
-
`- Setup guide: ${CI_URL}`,
|
|
49902
|
-
"If I say yes, run `npx react-doctor install` (or add the GitHub Actions workflow). Either way, then continue with the fixes below.",
|
|
49903
|
-
"",
|
|
49904
|
-
`Fix the top ${topGroups.length} React Doctor ${topGroups.length === 1 ? "issue" : "issues"} in ${input.projectName} on this pass — leave the rest for a follow-up.`,
|
|
49905
|
-
""
|
|
49906
|
-
];
|
|
51219
|
+
const lines = [`Fix the top ${topGroups.length} React Doctor ${topGroups.length === 1 ? "issue" : "issues"} in ${input.projectName} on this pass — leave the rest for a follow-up.`, ""];
|
|
49907
51220
|
topGroups.forEach(([ruleKey, ruleDiagnostics], index) => {
|
|
49908
51221
|
const representative = ruleDiagnostics[0];
|
|
49909
51222
|
const severityLabel = representative.severity === "error" ? "ERROR" : "WARN";
|
|
49910
|
-
|
|
51223
|
+
const sharedFixSiteCount = getSharedFixSiteCount(ruleDiagnostics);
|
|
51224
|
+
const countBadge = sharedFixSiteCount > 0 ? `one fix · ${sharedFixSiteCount} sites` : `×${ruleDiagnostics.length}`;
|
|
51225
|
+
lines.push(`${index + 1}. ${severityLabel} ${representative.category}: ${representative.title ?? ruleKey} (${countBadge})`, ` ${representative.message}`);
|
|
49911
51226
|
const fixRecipeLine = formatFixRecipeLine(representative);
|
|
49912
51227
|
if (fixRecipeLine) lines.push(` ${fixRecipeLine}`);
|
|
49913
51228
|
const uniqueFiles = [...new Set(ruleDiagnostics.map((diagnostic) => diagnostic.filePath))];
|
|
@@ -49917,10 +51232,19 @@ const buildHandoffPayload = (input) => {
|
|
|
49917
51232
|
}
|
|
49918
51233
|
const remainingFiles = uniqueFiles.length - 3;
|
|
49919
51234
|
if (remainingFiles > 0) lines.push(` - +${remainingFiles} more files`);
|
|
51235
|
+
const migrationBucket = migrationScaleBuckets.get(ruleKey);
|
|
51236
|
+
if (migrationBucket) lines.push(` Migration-scale (${migrationBucket.fileCount} files): fix a representative sample, confirm the recipe holds, and get the code owner's sign-off before changing the rest in one pass.`);
|
|
49920
51237
|
});
|
|
49921
51238
|
lines.push("");
|
|
49922
51239
|
if (outputDirectory) lines.push(`Full results for all ${input.diagnostics.length} issues (diagnostics.json + a .txt per rule): ${outputDirectory}`, "");
|
|
49923
|
-
lines.push("Read each file and fix the root cause — don't suppress or silence the rule.", "", "Verify against the real thing, don't assume: confirm each change matches the canonical fix recipe you fetched for that rule, then re-run `npx react-doctor@latest --verbose` and check the issue is actually gone against the real tool before moving on.", "", "Teach me as you go: for every issue you touch, explain it in plain language (no jargon) — what the problem is, why it's a problem, and how serious it is in human terms. Describe the real-world impact and severity concretely (e.g. \"this crashes the page for users on Safari\" vs. \"this is a minor cleanup with no user impact\") so I understand why it matters, not just what changed.", ""
|
|
51240
|
+
lines.push("Read each file and fix the root cause — don't suppress or silence the rule.", "", "Findings that share a `fixGroupId` (in diagnostics.json) are one root cause — a single fix clears all of them, so treat each `fixGroupId` as ONE task, not one per site.", "", "Verify against the real thing, don't assume: confirm each change matches the canonical fix recipe you fetched for that rule, then re-run `npx react-doctor@latest --verbose` and check the issue is actually gone against the real tool before moving on.", "", "Teach me as you go: for every issue you touch, explain it in plain language (no jargon) — what the problem is, why it's a problem, and how serious it is in human terms. Describe the real-world impact and severity concretely (e.g. \"this crashes the page for users on Safari\" vs. \"this is a minor cleanup with no user impact\") so I understand why it matters, not just what changed.", "");
|
|
51241
|
+
const shownRuleKeys = new Set(topGroups.map(([ruleKey]) => ruleKey));
|
|
51242
|
+
const deferredMigrationBuckets = [...migrationScaleBuckets.values()].filter((bucket) => !shownRuleKeys.has(bucket.ruleKey));
|
|
51243
|
+
if (deferredMigrationBuckets.length > 0) {
|
|
51244
|
+
const ruleSummaries = deferredMigrationBuckets.map((bucket) => `${bucket.title} (${bucket.fileCount} files)`).join(", ");
|
|
51245
|
+
lines.push(`Some of the rest are migration-scale (span dozens of files): ${ruleSummaries}. For each, fix a representative sample, confirm the recipe holds, and get the code owner's sign-off before changing the rest in one pass.`, "");
|
|
51246
|
+
}
|
|
51247
|
+
lines.push("Then work through the rest from the full results above.");
|
|
49924
51248
|
return lines.join("\n");
|
|
49925
51249
|
};
|
|
49926
51250
|
//#endregion
|
|
@@ -49964,78 +51288,6 @@ const detectAvailableAgents = async () => {
|
|
|
49964
51288
|
return getSkillAgentTypes().filter((agent) => agent !== "universal" && detected.has(agent));
|
|
49965
51289
|
};
|
|
49966
51290
|
//#endregion
|
|
49967
|
-
//#region src/cli/utils/git-hook-shared.ts
|
|
49968
|
-
const HOOK_FILE_NAME = "pre-commit";
|
|
49969
|
-
const HOOK_RELATIVE_PATH = "hooks/pre-commit";
|
|
49970
|
-
const LEGACY_HOOK_RUNNER_RELATIVE_PATH = ".react-doctor/hooks/pre-commit";
|
|
49971
|
-
const HUSKY_HOOKS_PATH = ".husky";
|
|
49972
|
-
const VITE_PLUS_HOOKS_PATH = ".vite-hooks";
|
|
49973
|
-
const LEFTHOOK_CONFIG_FILES = ["lefthook.yml", "lefthook.yaml"];
|
|
49974
|
-
const PRE_COMMIT_CONFIG_FILE = ".pre-commit-config.yaml";
|
|
49975
|
-
const OVERCOMMIT_CONFIG_FILE = ".overcommit.yml";
|
|
49976
|
-
const REACT_DOCTOR_COMMAND = "react-doctor --staged --blocking warning";
|
|
49977
|
-
const NON_BLOCKING_REACT_DOCTOR_COMMAND = [
|
|
49978
|
-
"react_doctor_output=$(mktemp \"${TMPDIR:-/tmp}/react-doctor-hook.XXXXXX\");",
|
|
49979
|
-
`if ${REACT_DOCTOR_COMMAND} > "$react_doctor_output" 2>&1; then`,
|
|
49980
|
-
"rm -f \"$react_doctor_output\";",
|
|
49981
|
-
"else",
|
|
49982
|
-
"rm -f \"$react_doctor_output\";",
|
|
49983
|
-
`printf "%s\\n" "React Doctor found staged regressions." "Run ${REACT_DOCTOR_COMMAND} to inspect." "Want them fixed? Ask your agent to run that command and resolve the findings." >&2;`,
|
|
49984
|
-
"fi"
|
|
49985
|
-
].join(" ");
|
|
49986
|
-
const PACKAGE_JSON_FILE_NAME = "package.json";
|
|
49987
|
-
const runGit = (projectRoot, args) => {
|
|
49988
|
-
try {
|
|
49989
|
-
return execFileSync("git", [...args], {
|
|
49990
|
-
cwd: projectRoot,
|
|
49991
|
-
encoding: "utf8",
|
|
49992
|
-
stdio: [
|
|
49993
|
-
"ignore",
|
|
49994
|
-
"pipe",
|
|
49995
|
-
"ignore"
|
|
49996
|
-
]
|
|
49997
|
-
}).trim();
|
|
49998
|
-
} catch {
|
|
49999
|
-
return null;
|
|
50000
|
-
}
|
|
50001
|
-
};
|
|
50002
|
-
const resolveGitPath = (baseDirectory, value) => Path.isAbsolute(value) ? value : Path.resolve(baseDirectory, value);
|
|
50003
|
-
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
50004
|
-
const getPackageJsonPath = (projectRoot) => Path.join(projectRoot, PACKAGE_JSON_FILE_NAME);
|
|
50005
|
-
const readPackageJson = (projectRoot) => {
|
|
50006
|
-
try {
|
|
50007
|
-
return JSON.parse(NFS.readFileSync(getPackageJsonPath(projectRoot), "utf8"));
|
|
50008
|
-
} catch {
|
|
50009
|
-
return null;
|
|
50010
|
-
}
|
|
50011
|
-
};
|
|
50012
|
-
const writeJsonFile$1 = (filePath, value) => {
|
|
50013
|
-
NFS.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
50014
|
-
};
|
|
50015
|
-
const packageHasDependency = (projectRoot, dependencyName) => {
|
|
50016
|
-
const packageJson = readPackageJson(projectRoot);
|
|
50017
|
-
if (!isRecord(packageJson)) return false;
|
|
50018
|
-
return [
|
|
50019
|
-
"dependencies",
|
|
50020
|
-
"devDependencies",
|
|
50021
|
-
"optionalDependencies"
|
|
50022
|
-
].some((fieldName) => {
|
|
50023
|
-
const dependencies = packageJson[fieldName];
|
|
50024
|
-
return isRecord(dependencies) && typeof dependencies[dependencyName] === "string";
|
|
50025
|
-
});
|
|
50026
|
-
};
|
|
50027
|
-
const packageHasRecordKey = (projectRoot, key) => {
|
|
50028
|
-
const packageJson = readPackageJson(projectRoot);
|
|
50029
|
-
return isRecord(packageJson) && isRecord(packageJson[key]);
|
|
50030
|
-
};
|
|
50031
|
-
const packageHasNestedRecordKey = (projectRoot, key, nestedKey) => {
|
|
50032
|
-
const packageJson = readPackageJson(projectRoot);
|
|
50033
|
-
if (!isRecord(packageJson)) return false;
|
|
50034
|
-
const value = packageJson[key];
|
|
50035
|
-
return isRecord(value) && isRecord(value[nestedKey]);
|
|
50036
|
-
};
|
|
50037
|
-
const ensureTrailingNewline = (content) => content.endsWith("\n") ? content : `${content}\n`;
|
|
50038
|
-
//#endregion
|
|
50039
51291
|
//#region src/cli/utils/install-doctor-script.ts
|
|
50040
51292
|
const DOCTOR_SCRIPT_NAME = "doctor";
|
|
50041
51293
|
const FALLBACK_DOCTOR_SCRIPT_NAME = "react-doctor";
|
|
@@ -50061,31 +51313,31 @@ const findNearestPackageDirectory = (startDirectory, stopDirectory) => {
|
|
|
50061
51313
|
};
|
|
50062
51314
|
const hasDoctorScript = (projectRoot) => {
|
|
50063
51315
|
const packageJson = readPackageJson(findNearestPackageDirectory(projectRoot) ?? projectRoot);
|
|
50064
|
-
if (!isRecord(packageJson)) return false;
|
|
51316
|
+
if (!isRecord$1(packageJson)) return false;
|
|
50065
51317
|
const scripts = packageJson.scripts;
|
|
50066
|
-
if (!isRecord(scripts)) return false;
|
|
51318
|
+
if (!isRecord$1(scripts)) return false;
|
|
50067
51319
|
return isReactDoctorScriptCommand(scripts[DOCTOR_SCRIPT_NAME]) || isReactDoctorScriptCommand(scripts[FALLBACK_DOCTOR_SCRIPT_NAME]);
|
|
50068
51320
|
};
|
|
50069
51321
|
const hasDoctorDependency = (packageJson) => DEPENDENCY_FIELD_NAMES.some((fieldName) => {
|
|
50070
51322
|
const dependencies = packageJson[fieldName];
|
|
50071
|
-
return isRecord(dependencies) && Object.hasOwn(dependencies, "react-doctor");
|
|
51323
|
+
return isRecord$1(dependencies) && Object.hasOwn(dependencies, "react-doctor");
|
|
50072
51324
|
});
|
|
50073
51325
|
const installDoctorScript = (options) => {
|
|
50074
51326
|
const packageDirectory = findNearestPackageDirectory(options.projectRoot) ?? options.projectRoot;
|
|
50075
51327
|
const packageJsonPath = getPackageJsonPath(packageDirectory);
|
|
50076
51328
|
const packageJson = readPackageJson(packageDirectory);
|
|
50077
|
-
if (!isRecord(packageJson)) return {
|
|
51329
|
+
if (!isRecord$1(packageJson)) return {
|
|
50078
51330
|
packageJsonPath,
|
|
50079
51331
|
scriptStatus: "skipped",
|
|
50080
51332
|
scriptReason: "missing-or-invalid-package-json"
|
|
50081
51333
|
};
|
|
50082
51334
|
const scripts = packageJson.scripts;
|
|
50083
51335
|
const scriptTarget = (() => {
|
|
50084
|
-
if (scripts !== void 0 && !isRecord(scripts)) return {
|
|
51336
|
+
if (scripts !== void 0 && !isRecord$1(scripts)) return {
|
|
50085
51337
|
status: "skipped",
|
|
50086
51338
|
reason: "invalid-scripts"
|
|
50087
51339
|
};
|
|
50088
|
-
const scriptRecord = isRecord(scripts) ? scripts : {};
|
|
51340
|
+
const scriptRecord = isRecord$1(scripts) ? scripts : {};
|
|
50089
51341
|
if (isReactDoctorScriptCommand(scriptRecord[DOCTOR_SCRIPT_NAME])) return {
|
|
50090
51342
|
scriptName: DOCTOR_SCRIPT_NAME,
|
|
50091
51343
|
status: "existing"
|
|
@@ -50119,7 +51371,7 @@ const installDoctorScript = (options) => {
|
|
|
50119
51371
|
if (scriptStatus === "created") writeJsonFile$1(packageJsonPath, {
|
|
50120
51372
|
...packageJson,
|
|
50121
51373
|
scripts: {
|
|
50122
|
-
...isRecord(scripts) ? scripts : {},
|
|
51374
|
+
...isRecord$1(scripts) ? scripts : {},
|
|
50123
51375
|
[scriptTarget.scriptName ?? DOCTOR_SCRIPT_NAME]: DOCTOR_SCRIPT_COMMAND
|
|
50124
51376
|
}
|
|
50125
51377
|
});
|
|
@@ -50270,40 +51522,27 @@ const upgradeReactDoctorWorkflowInPlace = (projectRoot) => {
|
|
|
50270
51522
|
}
|
|
50271
51523
|
};
|
|
50272
51524
|
//#endregion
|
|
50273
|
-
//#region src/cli/utils/hash-project-root.ts
|
|
50274
|
-
const hashProjectRoot = (projectRoot) => createHash("sha256").update(Path.resolve(projectRoot)).digest("hex");
|
|
50275
|
-
//#endregion
|
|
50276
51525
|
//#region src/cli/utils/action-upgrade-prompt.ts
|
|
50277
|
-
const
|
|
50278
|
-
|
|
50279
|
-
|
|
50280
|
-
|
|
50281
|
-
});
|
|
50282
|
-
const
|
|
50283
|
-
|
|
50284
|
-
|
|
50285
|
-
|
|
50286
|
-
|
|
50287
|
-
|
|
50288
|
-
|
|
50289
|
-
|
|
50290
|
-
|
|
50291
|
-
|
|
50292
|
-
|
|
50293
|
-
|
|
50294
|
-
|
|
50295
|
-
|
|
50296
|
-
|
|
50297
|
-
rootDirectory: Path.resolve(projectRoot),
|
|
50298
|
-
outcome,
|
|
50299
|
-
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
50300
|
-
}
|
|
50301
|
-
});
|
|
50302
|
-
return true;
|
|
50303
|
-
} catch {
|
|
50304
|
-
return false;
|
|
50305
|
-
}
|
|
50306
|
-
};
|
|
51526
|
+
const ACTION_UPGRADE_GATE = {
|
|
51527
|
+
id: ACTION_UPGRADE_EVENT,
|
|
51528
|
+
scope: "project"
|
|
51529
|
+
};
|
|
51530
|
+
const hasHandledActionUpgrade = (projectRoot, options = {}) => !isGatePending(ACTION_UPGRADE_GATE, { projectRoot }, options);
|
|
51531
|
+
const recordActionUpgradeDecision = (projectRoot, outcome, options = {}) => recordGate(ACTION_UPGRADE_GATE, {
|
|
51532
|
+
projectRoot,
|
|
51533
|
+
outcome
|
|
51534
|
+
}, options);
|
|
51535
|
+
//#endregion
|
|
51536
|
+
//#region src/cli/utils/ci-prompt-decision.ts
|
|
51537
|
+
const CI_PITCH_GATE = {
|
|
51538
|
+
id: CI_PITCH_EVENT,
|
|
51539
|
+
scope: "project"
|
|
51540
|
+
};
|
|
51541
|
+
const hasHandledCiPrompt = (projectRoot, options = {}) => !isGatePending(CI_PITCH_GATE, { projectRoot }, options);
|
|
51542
|
+
const recordCiPromptDecision = (projectRoot, outcome, options = {}) => recordGate(CI_PITCH_GATE, {
|
|
51543
|
+
projectRoot,
|
|
51544
|
+
outcome
|
|
51545
|
+
}, options);
|
|
50307
51546
|
//#endregion
|
|
50308
51547
|
//#region src/cli/utils/open-url.ts
|
|
50309
51548
|
const resolveOpenCommand = (url) => {
|
|
@@ -50469,39 +51708,80 @@ const DEFAULT_PR_TITLE = "Add React Doctor to GitHub Actions";
|
|
|
50469
51708
|
const DEFAULT_PR_BODY = `Adds a [React Doctor](https://www.react.doctor) scan to every pull request and every push to the default branch. The workflow file is documented inline.
|
|
50470
51709
|
|
|
50471
51710
|
Docs: https://www.react.doctor/ci`;
|
|
50472
|
-
const findUniqueBranchName = async (cwd) => {
|
|
50473
|
-
if (!(await
|
|
51711
|
+
const findUniqueBranchName = async (cwd, run) => {
|
|
51712
|
+
if (!(await run("git", [
|
|
50474
51713
|
"rev-parse",
|
|
50475
51714
|
"--verify",
|
|
50476
51715
|
NEW_BRANCH_PREFIX
|
|
50477
51716
|
], cwd)).success) return NEW_BRANCH_PREFIX;
|
|
50478
51717
|
return `${NEW_BRANCH_PREFIX}-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace(/[-:T]/g, "")}`;
|
|
50479
51718
|
};
|
|
51719
|
+
const findExistingSetupPullRequest = async (cwd, run) => {
|
|
51720
|
+
const prList = await run("gh", [
|
|
51721
|
+
"pr",
|
|
51722
|
+
"list",
|
|
51723
|
+
"--state",
|
|
51724
|
+
"open",
|
|
51725
|
+
"--json",
|
|
51726
|
+
"headRefName,url",
|
|
51727
|
+
"--limit",
|
|
51728
|
+
String(100)
|
|
51729
|
+
], cwd);
|
|
51730
|
+
if (!prList.success) return null;
|
|
51731
|
+
try {
|
|
51732
|
+
return JSON.parse(prList.stdout).find((pullRequest) => (pullRequest.headRefName ?? "").startsWith(NEW_BRANCH_PREFIX)) ?? null;
|
|
51733
|
+
} catch {
|
|
51734
|
+
return null;
|
|
51735
|
+
}
|
|
51736
|
+
};
|
|
51737
|
+
const hasUnrelatedTrackedChanges = async (cwd, workflowRelative, run) => {
|
|
51738
|
+
const statusProbe = await run("git", [
|
|
51739
|
+
"status",
|
|
51740
|
+
"--porcelain",
|
|
51741
|
+
"--",
|
|
51742
|
+
".",
|
|
51743
|
+
`:!${workflowRelative}`
|
|
51744
|
+
], cwd);
|
|
51745
|
+
if (!statusProbe.success) return true;
|
|
51746
|
+
return statusProbe.stdout.split(/\r?\n/).filter(Boolean).some((statusLine) => !statusLine.startsWith("??"));
|
|
51747
|
+
};
|
|
50480
51748
|
const openWorkflowPullRequest = async (params) => {
|
|
50481
51749
|
const workflowPath = Path.resolve(params.workflowPath);
|
|
50482
51750
|
const commitMessage = params.commitMessage ?? DEFAULT_COMMIT_MESSAGE;
|
|
50483
51751
|
const prTitle = params.prTitle ?? DEFAULT_PR_TITLE;
|
|
50484
51752
|
const prBody = params.prBody ?? DEFAULT_PR_BODY;
|
|
50485
|
-
const
|
|
51753
|
+
const run = params.run ?? runCommand;
|
|
51754
|
+
const checkCommandAvailable = params.checkCommandAvailable ?? isCommandAvailable;
|
|
51755
|
+
const repoRootProbe = await run("git", ["rev-parse", "--show-toplevel"], Path.dirname(workflowPath));
|
|
50486
51756
|
if (!repoRootProbe.success) return {
|
|
50487
51757
|
status: "not-attempted",
|
|
50488
51758
|
reason: "not-a-git-repo"
|
|
50489
51759
|
};
|
|
50490
51760
|
const cwd = repoRootProbe.stdout;
|
|
50491
|
-
|
|
51761
|
+
const workflowRelative = toForwardSlashes(Path.relative(cwd, workflowPath));
|
|
51762
|
+
if (!checkCommandAvailable("gh")) return {
|
|
50492
51763
|
status: "not-attempted",
|
|
50493
51764
|
reason: "gh-not-installed"
|
|
50494
51765
|
};
|
|
50495
|
-
if (!(await
|
|
51766
|
+
if (!(await run("gh", ["auth", "status"], cwd)).success) return {
|
|
50496
51767
|
status: "not-attempted",
|
|
50497
51768
|
reason: "gh-not-authenticated"
|
|
50498
51769
|
};
|
|
50499
|
-
const
|
|
51770
|
+
const existingSetupPullRequest = await findExistingSetupPullRequest(cwd, run);
|
|
51771
|
+
if (existingSetupPullRequest) return {
|
|
51772
|
+
status: "pr-exists",
|
|
51773
|
+
url: existingSetupPullRequest.url ?? ""
|
|
51774
|
+
};
|
|
51775
|
+
if (await hasUnrelatedTrackedChanges(cwd, workflowRelative, run)) return {
|
|
51776
|
+
status: "not-attempted",
|
|
51777
|
+
reason: "working-tree-dirty"
|
|
51778
|
+
};
|
|
51779
|
+
const defaultBranch = params.baseBranch ?? await detectDefaultBranch(cwd, run);
|
|
50500
51780
|
if (!defaultBranch) return {
|
|
50501
51781
|
status: "not-attempted",
|
|
50502
51782
|
reason: "no-default-branch"
|
|
50503
51783
|
};
|
|
50504
|
-
const previousBranchProbe = await
|
|
51784
|
+
const previousBranchProbe = await run("git", [
|
|
50505
51785
|
"rev-parse",
|
|
50506
51786
|
"--abbrev-ref",
|
|
50507
51787
|
"HEAD"
|
|
@@ -50511,13 +51791,13 @@ const openWorkflowPullRequest = async (params) => {
|
|
|
50511
51791
|
reason: "detached-head"
|
|
50512
51792
|
};
|
|
50513
51793
|
const previousBranch = previousBranchProbe.stdout;
|
|
50514
|
-
await
|
|
51794
|
+
await run("git", [
|
|
50515
51795
|
"fetch",
|
|
50516
51796
|
"origin",
|
|
50517
51797
|
defaultBranch
|
|
50518
51798
|
], cwd);
|
|
50519
|
-
const newBranch = await findUniqueBranchName(cwd);
|
|
50520
|
-
if (!(await
|
|
51799
|
+
const newBranch = await findUniqueBranchName(cwd, run);
|
|
51800
|
+
if (!(await run("git", [
|
|
50521
51801
|
"checkout",
|
|
50522
51802
|
"-b",
|
|
50523
51803
|
newBranch,
|
|
@@ -50527,17 +51807,17 @@ const openWorkflowPullRequest = async (params) => {
|
|
|
50527
51807
|
reason: "checkout-failed"
|
|
50528
51808
|
};
|
|
50529
51809
|
const restoreToPreviousBranch = async (deleteNewBranch) => {
|
|
50530
|
-
await
|
|
50531
|
-
if (deleteNewBranch) await
|
|
51810
|
+
await run("git", ["checkout", previousBranch], cwd);
|
|
51811
|
+
if (deleteNewBranch) await run("git", [
|
|
50532
51812
|
"branch",
|
|
50533
51813
|
"-D",
|
|
50534
51814
|
newBranch
|
|
50535
51815
|
], cwd);
|
|
50536
51816
|
};
|
|
50537
|
-
if (!(await
|
|
51817
|
+
if (!(await run("git", [
|
|
50538
51818
|
"add",
|
|
50539
51819
|
"--",
|
|
50540
|
-
|
|
51820
|
+
workflowRelative
|
|
50541
51821
|
], cwd)).success) {
|
|
50542
51822
|
await restoreToPreviousBranch(true);
|
|
50543
51823
|
return {
|
|
@@ -50545,7 +51825,7 @@ const openWorkflowPullRequest = async (params) => {
|
|
|
50545
51825
|
reason: "git-add-failed"
|
|
50546
51826
|
};
|
|
50547
51827
|
}
|
|
50548
|
-
if (!(await
|
|
51828
|
+
if (!(await run("git", [
|
|
50549
51829
|
"commit",
|
|
50550
51830
|
"-m",
|
|
50551
51831
|
commitMessage
|
|
@@ -50556,7 +51836,7 @@ const openWorkflowPullRequest = async (params) => {
|
|
|
50556
51836
|
reason: "git-commit-failed"
|
|
50557
51837
|
};
|
|
50558
51838
|
}
|
|
50559
|
-
if (!(await
|
|
51839
|
+
if (!(await run("git", [
|
|
50560
51840
|
"push",
|
|
50561
51841
|
"-u",
|
|
50562
51842
|
"origin",
|
|
@@ -50568,7 +51848,7 @@ const openWorkflowPullRequest = async (params) => {
|
|
|
50568
51848
|
reason: "git-push-failed"
|
|
50569
51849
|
};
|
|
50570
51850
|
}
|
|
50571
|
-
const prCreate = await
|
|
51851
|
+
const prCreate = await run("gh", [
|
|
50572
51852
|
"pr",
|
|
50573
51853
|
"create",
|
|
50574
51854
|
"--title",
|
|
@@ -50592,12 +51872,13 @@ const openWorkflowPullRequest = async (params) => {
|
|
|
50592
51872
|
};
|
|
50593
51873
|
const stageWorkflowFile = async (params) => {
|
|
50594
51874
|
const workflowPath = Path.resolve(params.workflowPath);
|
|
50595
|
-
const
|
|
51875
|
+
const run = params.run ?? runCommand;
|
|
51876
|
+
const repoRootProbe = await run("git", ["rev-parse", "--show-toplevel"], Path.dirname(workflowPath));
|
|
50596
51877
|
if (!repoRootProbe.success) return false;
|
|
50597
|
-
return (await
|
|
51878
|
+
return (await run("git", [
|
|
50598
51879
|
"add",
|
|
50599
51880
|
"--",
|
|
50600
|
-
Path.relative(repoRootProbe.stdout, workflowPath)
|
|
51881
|
+
toForwardSlashes(Path.relative(repoRootProbe.stdout, workflowPath))
|
|
50601
51882
|
], repoRootProbe.stdout)).success;
|
|
50602
51883
|
};
|
|
50603
51884
|
//#endregion
|
|
@@ -50638,6 +51919,7 @@ const setUpGitHubActions = async (options) => {
|
|
|
50638
51919
|
baseBranch: defaultBranch
|
|
50639
51920
|
});
|
|
50640
51921
|
if (pullRequestResult.status === "pr-opened") pullRequestSpinner.succeed(`Opened pull request for review: ${highlighter.info(pullRequestResult.url)}`);
|
|
51922
|
+
else if (pullRequestResult.status === "pr-exists") pullRequestSpinner.succeed(`A React Doctor setup pull request is already open: ${highlighter.info(pullRequestResult.url)}`);
|
|
50641
51923
|
else if (pullRequestResult.status === "branch-pushed") pullRequestSpinner.warn(`Pushed branch ${highlighter.bold(pullRequestResult.branch)} but couldn't open a PR. Open one with: gh pr create --head ${pullRequestResult.branch}`);
|
|
50642
51924
|
else {
|
|
50643
51925
|
pullRequestSpinner.stop();
|
|
@@ -50760,22 +52042,22 @@ const buildAgentHookScript = () => [
|
|
|
50760
52042
|
"",
|
|
50761
52043
|
"run_react_doctor() {",
|
|
50762
52044
|
" if [ -x ./node_modules/.bin/react-doctor ]; then",
|
|
50763
|
-
" ./node_modules/.bin/react-doctor --verbose --
|
|
52045
|
+
" ./node_modules/.bin/react-doctor --verbose --scope changed --blocking warning --no-score",
|
|
50764
52046
|
" return",
|
|
50765
52047
|
" fi",
|
|
50766
52048
|
"",
|
|
50767
52049
|
" if command -v react-doctor >/dev/null 2>&1; then",
|
|
50768
|
-
" react-doctor --verbose --
|
|
52050
|
+
" react-doctor --verbose --scope changed --blocking warning --no-score",
|
|
50769
52051
|
" return",
|
|
50770
52052
|
" fi",
|
|
50771
52053
|
"",
|
|
50772
52054
|
" if command -v pnpm >/dev/null 2>&1; then",
|
|
50773
|
-
" pnpm dlx react-doctor@latest --verbose --
|
|
52055
|
+
" pnpm dlx react-doctor@latest --verbose --scope changed --blocking warning --no-score",
|
|
50774
52056
|
" return",
|
|
50775
52057
|
" fi",
|
|
50776
52058
|
"",
|
|
50777
52059
|
" if command -v npx >/dev/null 2>&1; then",
|
|
50778
|
-
" npx --yes react-doctor@latest --verbose --
|
|
52060
|
+
" npx --yes react-doctor@latest --verbose --scope changed --blocking warning --no-score",
|
|
50779
52061
|
" return",
|
|
50780
52062
|
" fi",
|
|
50781
52063
|
"",
|
|
@@ -50933,13 +52215,13 @@ const installPackageJsonHook = (options, strategy) => {
|
|
|
50933
52215
|
const packageJsonPath = getPackageJsonPath(options.projectRoot);
|
|
50934
52216
|
const didHookExist = NFS.existsSync(packageJsonPath);
|
|
50935
52217
|
const packageJson = readPackageJson(options.projectRoot);
|
|
50936
|
-
const nextPackageJson = isRecord(packageJson) ? { ...packageJson } : {};
|
|
52218
|
+
const nextPackageJson = isRecord$1(packageJson) ? { ...packageJson } : {};
|
|
50937
52219
|
const parentKeys = strategy.path.slice(0, -1);
|
|
50938
52220
|
const leafKey = strategy.path[strategy.path.length - 1];
|
|
50939
52221
|
let parent = nextPackageJson;
|
|
50940
52222
|
for (const key of parentKeys) {
|
|
50941
52223
|
const existing = parent[key];
|
|
50942
|
-
const cloned = isRecord(existing) ? { ...existing } : {};
|
|
52224
|
+
const cloned = isRecord$1(existing) ? { ...existing } : {};
|
|
50943
52225
|
parent[key] = cloned;
|
|
50944
52226
|
parent = cloned;
|
|
50945
52227
|
}
|
|
@@ -51110,7 +52392,7 @@ const isHuskyProject = (projectRoot) => NFS.existsSync(Path.join(projectRoot, ".
|
|
|
51110
52392
|
const isVitePlusProject = (projectRoot) => packageHasDependency(projectRoot, "vite-plus");
|
|
51111
52393
|
const isSimpleGitHooksProject = (projectRoot) => {
|
|
51112
52394
|
const packageJson = readPackageJson(projectRoot);
|
|
51113
|
-
return isRecord(packageJson) && isRecord(packageJson["simple-git-hooks"]) || packageHasDependency(projectRoot, "simple-git-hooks") || NFS.existsSync(Path.join(projectRoot, ".simple-git-hooks.cjs"));
|
|
52395
|
+
return isRecord$1(packageJson) && isRecord$1(packageJson["simple-git-hooks"]) || packageHasDependency(projectRoot, "simple-git-hooks") || NFS.existsSync(Path.join(projectRoot, ".simple-git-hooks.cjs"));
|
|
51114
52396
|
};
|
|
51115
52397
|
const getLefthookConfigPath = (projectRoot) => {
|
|
51116
52398
|
for (const fileName of LEFTHOOK_CONFIG_FILES) {
|
|
@@ -51276,7 +52558,7 @@ const detectPackageManager = (projectRoot) => {
|
|
|
51276
52558
|
let currentDirectory = Path.resolve(projectRoot);
|
|
51277
52559
|
while (true) {
|
|
51278
52560
|
const packageJson = readPackageJson(currentDirectory);
|
|
51279
|
-
if (isRecord(packageJson) && typeof packageJson.packageManager === "string") {
|
|
52561
|
+
if (isRecord$1(packageJson) && typeof packageJson.packageManager === "string") {
|
|
51280
52562
|
const packageManagerName = packageJson.packageManager.split("@")[0];
|
|
51281
52563
|
if (packageManagerName === "pnpm" || packageManagerName === "yarn" || packageManagerName === "bun" || packageManagerName === "npm") return packageManagerName;
|
|
51282
52564
|
}
|
|
@@ -51352,12 +52634,12 @@ const isSupplyChainTrustError = (error) => {
|
|
|
51352
52634
|
const formatInstallCommand = (input) => [input.command, ...input.args].join(" ");
|
|
51353
52635
|
const installReactDoctorDependency = async (options) => {
|
|
51354
52636
|
const packageJson = readPackageJson(options.projectRoot);
|
|
51355
|
-
if (!isRecord(packageJson)) return {
|
|
52637
|
+
if (!isRecord$1(packageJson)) return {
|
|
51356
52638
|
dependencyStatus: "skipped",
|
|
51357
52639
|
dependencyReason: "missing-or-invalid-package-json"
|
|
51358
52640
|
};
|
|
51359
52641
|
if (hasDoctorDependency(packageJson)) return { dependencyStatus: "existing" };
|
|
51360
|
-
if (packageJson.devDependencies !== void 0 && !isRecord(packageJson.devDependencies)) return {
|
|
52642
|
+
if (packageJson.devDependencies !== void 0 && !isRecord$1(packageJson.devDependencies)) return {
|
|
51361
52643
|
dependencyStatus: "skipped",
|
|
51362
52644
|
dependencyReason: "invalid-dev-dependencies"
|
|
51363
52645
|
};
|
|
@@ -51521,10 +52803,12 @@ const runInstallReactDoctor = async (options = {}) => {
|
|
|
51521
52803
|
const existingWorkflow = readReactDoctorWorkflow(projectRoot);
|
|
51522
52804
|
const canInstallWorkflow = !NFS.existsSync(workflowTargetPath);
|
|
51523
52805
|
const canUpgradeWorkflow = existingWorkflow !== null && workflowUsesV1Action(existingWorkflow.content) && !hasHandledActionUpgrade(projectRoot);
|
|
51524
|
-
const
|
|
52806
|
+
const ciPromptOutcome = canInstallWorkflow && !options.yes && !skipPrompts && !hasHandledCiPrompt(projectRoot) ? await askAddToGitHubActions(prompt) : null;
|
|
52807
|
+
const shouldInstallWorkflow = canInstallWorkflow && (Boolean(options.yes) || ciPromptOutcome === "yes");
|
|
51525
52808
|
const upgradePromptOutcome = canUpgradeWorkflow && !options.yes && !skipPrompts ? await askUpgradeActionVersion(prompt) : null;
|
|
51526
52809
|
const shouldUpgradeWorkflow = canUpgradeWorkflow && (Boolean(options.yes) || upgradePromptOutcome === "yes");
|
|
51527
52810
|
if (upgradePromptOutcome === "no" && !options.dryRun) recordActionUpgradeDecision(projectRoot, "declined");
|
|
52811
|
+
if ((ciPromptOutcome === "yes" || ciPromptOutcome === "no") && !options.dryRun) recordCiPromptDecision(projectRoot, ciPromptOutcome === "yes" ? "accepted" : "declined");
|
|
51528
52812
|
const selectedAgents = skipPrompts ? detectedAgents : (await prompt({
|
|
51529
52813
|
type: "multiselect",
|
|
51530
52814
|
name: "agents",
|
|
@@ -51736,7 +53020,12 @@ const upgradeGitHubActionsWorkflow = async (workflow) => {
|
|
|
51736
53020
|
prBody: UPGRADE_PR_BODY
|
|
51737
53021
|
});
|
|
51738
53022
|
if (pullRequestResult.status === "pr-opened") upgradeSpinner.succeed(`Opened pull request for review: ${highlighter.info(pullRequestResult.url)}`);
|
|
51739
|
-
else if (pullRequestResult.status === "
|
|
53023
|
+
else if (pullRequestResult.status === "pr-exists") {
|
|
53024
|
+
try {
|
|
53025
|
+
NFS.writeFileSync(workflow.workflowPath, workflow.content);
|
|
53026
|
+
} catch {}
|
|
53027
|
+
upgradeSpinner.succeed(`A React Doctor pull request is already open: ${highlighter.info(pullRequestResult.url)}`);
|
|
53028
|
+
} else if (pullRequestResult.status === "branch-pushed") upgradeSpinner.warn(`Pushed branch ${highlighter.bold(pullRequestResult.branch)} but couldn't open a PR. Open one with: gh pr create --head ${pullRequestResult.branch}`);
|
|
51740
53029
|
else {
|
|
51741
53030
|
upgradeSpinner.stop();
|
|
51742
53031
|
try {
|
|
@@ -51771,18 +53060,24 @@ const handoffToAgent = async (input) => {
|
|
|
51771
53060
|
if (!input.interactive || input.diagnostics.length === 0) return;
|
|
51772
53061
|
cliLogger.break();
|
|
51773
53062
|
const projectRootForCi = findNearestPackageDirectory(input.rootDirectory) ?? input.rootDirectory;
|
|
51774
|
-
|
|
53063
|
+
const isGitHubActionsConfigured = isReactDoctorWorkflowInstalled(projectRootForCi);
|
|
53064
|
+
if (!isGitHubActionsConfigured && !hasHandledCiPrompt(projectRootForCi)) {
|
|
51775
53065
|
const ciOutcome = await askAddToGitHubActions();
|
|
51776
53066
|
recordCount(METRIC.agentHandoff, 1, {
|
|
51777
53067
|
outcome: `ci-${ciOutcome}`,
|
|
51778
53068
|
diagnosticsCount: input.diagnostics.length
|
|
51779
53069
|
});
|
|
51780
53070
|
if (ciOutcome === "cancel") return;
|
|
53071
|
+
recordCiPromptDecision(projectRootForCi, ciOutcome === "yes" ? "accepted" : "declined");
|
|
51781
53072
|
if (ciOutcome === "yes") {
|
|
51782
53073
|
await setUpGitHubActions({ rootDirectory: input.rootDirectory });
|
|
51783
53074
|
cliLogger.break();
|
|
51784
53075
|
}
|
|
51785
|
-
} else await maybeOfferActionUpgrade(projectRootForCi);
|
|
53076
|
+
} else if (isGitHubActionsConfigured) await maybeOfferActionUpgrade(projectRootForCi);
|
|
53077
|
+
else recordCount(METRIC.agentHandoff, 1, {
|
|
53078
|
+
outcome: "ci-suppressed",
|
|
53079
|
+
diagnosticsCount: input.diagnostics.length
|
|
53080
|
+
});
|
|
51786
53081
|
const { handoffTarget } = await prompts({
|
|
51787
53082
|
type: "select",
|
|
51788
53083
|
name: "handoffTarget",
|
|
@@ -51844,6 +53139,47 @@ const handoffToAgent = async (input) => {
|
|
|
51844
53139
|
}
|
|
51845
53140
|
};
|
|
51846
53141
|
//#endregion
|
|
53142
|
+
//#region src/cli/utils/migrate-action-pin.ts
|
|
53143
|
+
const WORKFLOWS_DIRECTORY = Path.join(".github", "workflows");
|
|
53144
|
+
const RECOMMENDED_ACTION_REF = "v2";
|
|
53145
|
+
const MUTABLE_ACTION_REF = /(uses:\s*[\w.-]+\/react-doctor@)(?:main|master)\b/g;
|
|
53146
|
+
const isWorkflowFile = (fileName) => /\.ya?ml$/.test(fileName);
|
|
53147
|
+
/**
|
|
53148
|
+
* Rewrites mutable `@main` / `@master` React Doctor GitHub Action references in
|
|
53149
|
+
* the repo's `.github/workflows/*.yml` to the recommended floating major
|
|
53150
|
+
* (`@v2`) — a supply-chain hardening (#299) that also moves the workflow onto
|
|
53151
|
+
* the current (install- and scan-cached) action release. Pinned tags / SHAs are
|
|
53152
|
+
* deliberate and left untouched. Returns the absolute paths of the workflow
|
|
53153
|
+
* files it rewrote — empty when there's nothing to migrate.
|
|
53154
|
+
*/
|
|
53155
|
+
const migrateActionPin = (projectRoot) => {
|
|
53156
|
+
const workflowsDirectory = Path.join(projectRoot, WORKFLOWS_DIRECTORY);
|
|
53157
|
+
let entries;
|
|
53158
|
+
try {
|
|
53159
|
+
entries = NFS.readdirSync(workflowsDirectory, { withFileTypes: true });
|
|
53160
|
+
} catch {
|
|
53161
|
+
return [];
|
|
53162
|
+
}
|
|
53163
|
+
const rewrittenFiles = [];
|
|
53164
|
+
for (const entry of entries) {
|
|
53165
|
+
if (!entry.isFile() || !isWorkflowFile(entry.name)) continue;
|
|
53166
|
+
const workflowPath = Path.join(workflowsDirectory, entry.name);
|
|
53167
|
+
let contents;
|
|
53168
|
+
try {
|
|
53169
|
+
contents = NFS.readFileSync(workflowPath, "utf-8");
|
|
53170
|
+
} catch {
|
|
53171
|
+
continue;
|
|
53172
|
+
}
|
|
53173
|
+
const updated = contents.replace(MUTABLE_ACTION_REF, `$1${RECOMMENDED_ACTION_REF}`);
|
|
53174
|
+
if (updated === contents) continue;
|
|
53175
|
+
try {
|
|
53176
|
+
NFS.writeFileSync(workflowPath, updated);
|
|
53177
|
+
rewrittenFiles.push(workflowPath);
|
|
53178
|
+
} catch {}
|
|
53179
|
+
}
|
|
53180
|
+
return rewrittenFiles;
|
|
53181
|
+
};
|
|
53182
|
+
//#endregion
|
|
51847
53183
|
//#region src/cli/utils/read-object-file.ts
|
|
51848
53184
|
/**
|
|
51849
53185
|
* Reads a JSON / JSONC file as a plain object, or `null` when it is missing,
|
|
@@ -51908,6 +53244,35 @@ export default ${serializeTsObjectLiteral(config)} satisfies ReactDoctorConfig;
|
|
|
51908
53244
|
NFS.rmSync(legacy.legacyFilePath, { force: true });
|
|
51909
53245
|
return targetPath;
|
|
51910
53246
|
};
|
|
53247
|
+
const PROJECT_MIGRATIONS = [{
|
|
53248
|
+
id: "config-json-to-ts",
|
|
53249
|
+
scope: "project",
|
|
53250
|
+
run: ({ projectRoot }) => {
|
|
53251
|
+
if (projectRoot === void 0) return false;
|
|
53252
|
+
const legacyConfig = findLegacyConfig(projectRoot);
|
|
53253
|
+
if (!legacyConfig) return false;
|
|
53254
|
+
const migratedPath = migrateLegacyConfig(legacyConfig);
|
|
53255
|
+
if (!migratedPath) return false;
|
|
53256
|
+
cliLogger.success("Migrated react-doctor.config.json → doctor.config.ts");
|
|
53257
|
+
cliLogger.dim(` Your settings were preserved. Review ${toRelativePath(migratedPath, projectRoot)} and commit it.`);
|
|
53258
|
+
cliLogger.break();
|
|
53259
|
+
return true;
|
|
53260
|
+
}
|
|
53261
|
+
}, {
|
|
53262
|
+
id: "action-pin-main-to-v2",
|
|
53263
|
+
scope: "project",
|
|
53264
|
+
run: ({ projectRoot }) => {
|
|
53265
|
+
if (projectRoot === void 0) return false;
|
|
53266
|
+
const rewrittenFiles = migrateActionPin(projectRoot);
|
|
53267
|
+
if (rewrittenFiles.length === 0) return false;
|
|
53268
|
+
const relativeFiles = rewrittenFiles.map((file) => toRelativePath(file, projectRoot)).join(", ");
|
|
53269
|
+
cliLogger.success(`Pinned the React Doctor action to @v2 in ${relativeFiles}`);
|
|
53270
|
+
cliLogger.dim(" An unpinned @main reference runs whatever the action's HEAD points to (a supply-chain risk). Review and commit the change — or revert it if you intentionally track main.");
|
|
53271
|
+
cliLogger.break();
|
|
53272
|
+
return true;
|
|
53273
|
+
}
|
|
53274
|
+
}];
|
|
53275
|
+
const runProjectMigrations = (projectRoot, options = {}) => runMigrations(PROJECT_MIGRATIONS, { projectRoot }, options);
|
|
51911
53276
|
//#endregion
|
|
51912
53277
|
//#region src/cli/utils/print-branded-header.ts
|
|
51913
53278
|
/**
|
|
@@ -52005,6 +53370,7 @@ const reportErrorToSentry = async (error) => {
|
|
|
52005
53370
|
sampled: runTrace.sampled,
|
|
52006
53371
|
sampleRand: Math.random()
|
|
52007
53372
|
});
|
|
53373
|
+
recordRunTraceId(scope.getPropagationContext().traceId);
|
|
52008
53374
|
return Sentry.captureException(error);
|
|
52009
53375
|
});
|
|
52010
53376
|
await Sentry.flush(SENTRY_FLUSH_TIMEOUT_MS);
|
|
@@ -52088,7 +53454,7 @@ const printMultiProjectSummary = (input) => gen(function* () {
|
|
|
52088
53454
|
yield* log(`${highlighter.success("✔")} Scanned ${totalScannedFileCount} ${totalScannedFileCount === 1 ? "file" : "files"} in ${formatElapsedTime(totalElapsedMilliseconds)}`);
|
|
52089
53455
|
if (displayDiagnostics.length > 0) {
|
|
52090
53456
|
yield* log("");
|
|
52091
|
-
yield* printDiagnostics(displayDiagnostics, verbose, resolveDiagnosticSourceRoot, buildRulePriorityMap(completedScans.map((scan) => scan.result.score)), isCodingAgentEnvironment(), { animateCountUp: animateRender });
|
|
53457
|
+
yield* printDiagnostics(displayDiagnostics, verbose, resolveDiagnosticSourceRoot, buildRulePriorityMap(completedScans.map((scan) => scan.result.score)), isCodingAgentEnvironment(), { animateCountUp: animateRender }, shouldRenderHyperlinks(process.stdout));
|
|
52092
53458
|
}
|
|
52093
53459
|
const lowestScoredScan = findLowestScoredScan(completedScans);
|
|
52094
53460
|
const aggregateScore = lowestScoredScan?.result.score ?? null;
|
|
@@ -52126,19 +53492,16 @@ const printMultiProjectSummary = (input) => gen(function* () {
|
|
|
52126
53492
|
});
|
|
52127
53493
|
//#endregion
|
|
52128
53494
|
//#region src/cli/utils/prompt-install-setup.ts
|
|
52129
|
-
const
|
|
52130
|
-
|
|
52131
|
-
|
|
52132
|
-
|
|
52133
|
-
}
|
|
52134
|
-
const
|
|
52135
|
-
const
|
|
52136
|
-
|
|
52137
|
-
|
|
52138
|
-
|
|
52139
|
-
return false;
|
|
52140
|
-
}
|
|
52141
|
-
};
|
|
53495
|
+
const SETUP_HINT_GATE = {
|
|
53496
|
+
id: SETUP_HINT_EVENT,
|
|
53497
|
+
scope: "project",
|
|
53498
|
+
fireWhenUnknown: true
|
|
53499
|
+
};
|
|
53500
|
+
const hasDisabledSetupPrompt = (projectRoot, options = {}) => !isGatePending(SETUP_HINT_GATE, { projectRoot }, options);
|
|
53501
|
+
const disableSetupPrompt = (projectRoot, options = {}) => recordGate(SETUP_HINT_GATE, {
|
|
53502
|
+
projectRoot,
|
|
53503
|
+
outcome: "declined"
|
|
53504
|
+
}, options);
|
|
52142
53505
|
const resolveInstallSetupProjectRoot = (options) => {
|
|
52143
53506
|
if (options.scanDirectories.length === 0) return findNearestPackageDirectory(options.scanRoot) ?? options.scanRoot;
|
|
52144
53507
|
const packageDirectories = /* @__PURE__ */ new Set();
|
|
@@ -52545,6 +53908,14 @@ const runExplain = async (fileLineArgument, context) => {
|
|
|
52545
53908
|
const colorizedRule = colorizeRuleByDiagnostic(ruleIdentifier, diagnostic.severity);
|
|
52546
53909
|
const severityLabel = colorizeRuleByDiagnostic(diagnostic.severity, diagnostic.severity);
|
|
52547
53910
|
cliLogger.log(`${severitySymbol} ${colorizedRule} ${highlighter.dim(`(${severityLabel})`)} — ${diagnostic.message}`);
|
|
53911
|
+
const codeFrame = buildCodeFrame({
|
|
53912
|
+
filePath: diagnostic.filePath,
|
|
53913
|
+
line: diagnostic.line,
|
|
53914
|
+
column: diagnostic.column,
|
|
53915
|
+
endLine: diagnostic.endLine,
|
|
53916
|
+
rootDirectory: targetDirectory
|
|
53917
|
+
});
|
|
53918
|
+
if (codeFrame) cliLogger.log(indentMultilineText(codeFrame, " "));
|
|
52548
53919
|
if (diagnostic.category) cliLogger.dim(` Category: ${diagnostic.category}`);
|
|
52549
53920
|
if (diagnostic.help) cliLogger.dim(` ${diagnostic.help}`);
|
|
52550
53921
|
cliLogger.dim(` If this needs follow-up or looks like a false positive, open: ${buildDiagnosticIssueUrl({
|
|
@@ -52594,6 +53965,10 @@ const validateModeFlags = (flags) => {
|
|
|
52594
53965
|
if (flags.staged && (flags.scope === "full" || flags.scope === "changed")) throw new CliInputError(`Cannot combine --staged with --scope ${flags.scope}; use --scope files or --scope lines, or drop --scope.`);
|
|
52595
53966
|
if (flags.score && flags.json) throw new CliInputError("Cannot combine --score and --json; pick one output mode.");
|
|
52596
53967
|
if (flags.score && flags.telemetry === false) throw new CliInputError("Cannot combine --score with --no-telemetry; --score prints the score that --no-telemetry disables.");
|
|
53968
|
+
if (flags.debug && (flags.score === false || flags.telemetry === false)) {
|
|
53969
|
+
const disablingFlag = flags.score === false ? "--no-score" : "--no-telemetry";
|
|
53970
|
+
throw new CliInputError(`Cannot combine --debug with ${disablingFlag}; ${disablingFlag} disables the Sentry reporting --debug needs to capture a trace.`);
|
|
53971
|
+
}
|
|
52597
53972
|
};
|
|
52598
53973
|
//#endregion
|
|
52599
53974
|
//#region src/cli/commands/inspect.ts
|
|
@@ -52654,15 +54029,9 @@ const buildChangedFilesDiffInfo = (changedFiles) => ({
|
|
|
52654
54029
|
* are left untouched — the loader still reads the legacy file as a deprecated
|
|
52655
54030
|
* fallback and warns — so a scan never mutates the repo unattended.
|
|
52656
54031
|
*/
|
|
52657
|
-
const maybeMigrateLegacyConfig = (requestedDirectory, { isQuiet, isStaged }) => {
|
|
54032
|
+
const maybeMigrateLegacyConfig = async (requestedDirectory, { isQuiet, isStaged }) => {
|
|
52658
54033
|
if (!(!isQuiet && !isStaged && process.stdout.isTTY === true && !isCiOrCodingAgentEnvironment())) return;
|
|
52659
|
-
|
|
52660
|
-
if (!legacyConfig) return;
|
|
52661
|
-
const migratedPath = migrateLegacyConfig(legacyConfig);
|
|
52662
|
-
if (!migratedPath) return;
|
|
52663
|
-
cliLogger.success("Migrated react-doctor.config.json → doctor.config.ts");
|
|
52664
|
-
cliLogger.dim(` Your settings were preserved. Review ${toRelativePath(migratedPath, requestedDirectory)} and commit it.`);
|
|
52665
|
-
cliLogger.break();
|
|
54034
|
+
await runProjectMigrations(requestedDirectory);
|
|
52666
54035
|
};
|
|
52667
54036
|
const inspectAction = async (directory, flags) => {
|
|
52668
54037
|
const isScoreOnly = Boolean(flags.score);
|
|
@@ -52677,7 +54046,7 @@ const inspectAction = async (directory, flags) => {
|
|
|
52677
54046
|
recordCount(METRIC.cliInvoked, 1, { command: "inspect" });
|
|
52678
54047
|
try {
|
|
52679
54048
|
validateModeFlags(flags);
|
|
52680
|
-
maybeMigrateLegacyConfig(requestedDirectory, {
|
|
54049
|
+
await maybeMigrateLegacyConfig(requestedDirectory, {
|
|
52681
54050
|
isQuiet,
|
|
52682
54051
|
isStaged: Boolean(flags.staged)
|
|
52683
54052
|
});
|
|
@@ -52941,11 +54310,13 @@ const inspectAction = async (directory, flags) => {
|
|
|
52941
54310
|
})) {
|
|
52942
54311
|
printAgentInstallHint();
|
|
52943
54312
|
recordCount(METRIC.agentInstallHintShown, 1);
|
|
54313
|
+
disableSetupPrompt(setupProjectRoot);
|
|
52944
54314
|
}
|
|
52945
54315
|
}
|
|
52946
54316
|
} catch (error) {
|
|
52947
54317
|
const isUserError = isExpectedUserError(error);
|
|
52948
54318
|
const sentryEventId = isUserError ? void 0 : await reportErrorToSentry(error);
|
|
54319
|
+
if (isDebugFlagEnabled()) await flushSentry();
|
|
52949
54320
|
if (isJsonMode) {
|
|
52950
54321
|
writeJsonErrorReport(error, sentryEventId);
|
|
52951
54322
|
process.exitCode = 1;
|
|
@@ -53668,6 +55039,33 @@ const normalizeHelpInvocation = (argv, knownCommands) => {
|
|
|
53668
55039
|
return [...nodeArguments, "--help"];
|
|
53669
55040
|
};
|
|
53670
55041
|
//#endregion
|
|
55042
|
+
//#region src/cli/utils/print-debug-trace.ts
|
|
55043
|
+
/**
|
|
55044
|
+
* The `--debug` end-of-run line, pure so it's testable without the Sentry SDK.
|
|
55045
|
+
* Mirrors the crash-reference phrasing in `handle-error.ts` ("mention this when
|
|
55046
|
+
* reporting") so users learn one habit for both paths. A `null` trace says why,
|
|
55047
|
+
* so `--debug` never silently does nothing.
|
|
55048
|
+
*/
|
|
55049
|
+
const buildDebugTraceMessage = (traceId) => traceId === null ? "Sentry trace unavailable for this run (no trace was recorded)." : `Sentry trace (mention this when reporting): ${traceId}`;
|
|
55050
|
+
/**
|
|
55051
|
+
* Prints the run's Sentry trace id to stderr at the end of a `--debug` run, so
|
|
55052
|
+
* maintainers can pull the full trace from a pasted id. Runs from the process
|
|
55053
|
+
* `exit` handler, so it's the last line on both the success path and the error
|
|
55054
|
+
* funnels (which `process.exit()` before the promise chain could resume).
|
|
55055
|
+
*
|
|
55056
|
+
* Writes straight to `process.stderr` (not `Console`) for three reasons: the
|
|
55057
|
+
* exit handler is synchronous, JSON mode patches the global console to no-ops —
|
|
55058
|
+
* a diagnostic the user explicitly asked for must survive that — and stderr
|
|
55059
|
+
* keeps `--json` / `--score` stdout machine-clean. The write is wrapped because
|
|
55060
|
+
* a diagnostic must never throw out of an exit handler.
|
|
55061
|
+
*/
|
|
55062
|
+
const printDebugTrace = () => {
|
|
55063
|
+
if (!Sentry.isInitialized()) return;
|
|
55064
|
+
try {
|
|
55065
|
+
process.stderr.write(`${highlighter.dim(buildDebugTraceMessage(getLastRunTraceId()))}\n`);
|
|
55066
|
+
} catch {}
|
|
55067
|
+
};
|
|
55068
|
+
//#endregion
|
|
53671
55069
|
//#region src/cli/utils/removed-cli-flags.ts
|
|
53672
55070
|
const REMOVED_FLAGS = new Map([
|
|
53673
55071
|
["--full", "use `--diff false` to force a full scan"],
|
|
@@ -53694,6 +55092,7 @@ const ROOT_FLAG_SPEC = {
|
|
|
53694
55092
|
longOptionsWithoutValues: new Set([
|
|
53695
55093
|
"--color",
|
|
53696
55094
|
"--dead-code",
|
|
55095
|
+
"--debug",
|
|
53697
55096
|
"--help",
|
|
53698
55097
|
"--json",
|
|
53699
55098
|
"--json-compact",
|
|
@@ -53861,6 +55260,9 @@ const stripUnknownCliFlags = (argv) => {
|
|
|
53861
55260
|
initializeSentry();
|
|
53862
55261
|
process.on("SIGINT", exitGracefully);
|
|
53863
55262
|
process.on("SIGTERM", exitGracefully);
|
|
55263
|
+
process.on("exit", () => {
|
|
55264
|
+
if (isDebugFlagEnabled()) printDebugTrace();
|
|
55265
|
+
});
|
|
53864
55266
|
unrefStdin();
|
|
53865
55267
|
guardStdin();
|
|
53866
55268
|
const formatExampleLines = (examples) => {
|
|
@@ -53872,7 +55274,7 @@ ${highlighter.dim("Examples:")}
|
|
|
53872
55274
|
${formatExampleLines([
|
|
53873
55275
|
["react-doctor", "scan the current project"],
|
|
53874
55276
|
["react-doctor ./apps/web", "scan a specific directory"],
|
|
53875
|
-
["react-doctor --
|
|
55277
|
+
["react-doctor --scope changed --base main", "scan only new issues vs. main"],
|
|
53876
55278
|
["react-doctor --project modules/a,modules/b", "score each module separately (names or paths)"],
|
|
53877
55279
|
["react-doctor --staged", "scan staged files (pre-commit hook)"],
|
|
53878
55280
|
["react-doctor --category Security", "show only one diagnostic category"],
|
|
@@ -53905,7 +55307,7 @@ ${highlighter.dim("Learn more:")}
|
|
|
53905
55307
|
${highlighter.info(CANONICAL_GITHUB_URL)}
|
|
53906
55308
|
`;
|
|
53907
55309
|
const collectCategoryOption = (value, previousValues) => [...previousValues ?? [], value];
|
|
53908
|
-
const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--output-dir <dir>", "directory for the full diagnostics dump (default: a temp folder)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("-y, --yes", "skip prompts, scan all workspace projects").option("--no-parallel", "lint serially with one worker (default: parallel across CPU cores; set the worker count with REACT_DOCTOR_PARALLEL)").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple); overrides the `projects` config field").option("--scope <value>", "how much to scan/report: full (default), files, changed (only new issues vs base), or lines (only changed lines)").option("--base <ref>", "base git ref for files/changed/lines scope (auto-detected when omitted)").addOption(new Option("--diff [base]", "[deprecated] alias for --scope changed (pass `false` to force a full scan)").hideHelp()).addOption(new Option("--changed-files-from <file>", "scan source files listed in a newline-delimited changed-files file").hideHelp()).option("--no-score", "skip the score API, the share URL, and crash reporting").addOption(new Option("--category <category>", "only show diagnostics in a category (repeatable; e.g. Security)").argParser(collectCategoryOption)).option("--no-telemetry", "alias for --no-score (skip the score API, share URL, and crash reporting)").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").addOption(new Option("--fail-on <level>", "[deprecated] alias for --blocking <level>").hideHelp()).option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").option("--warnings", "show warning-severity diagnostics (default)").option("--no-warnings", "hide warning-severity diagnostics (errors only)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderRootHelpEpilog);
|
|
55310
|
+
const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--debug", "force a Sentry trace and print its id at the end (paste it into a bug report)").option("--output-dir <dir>", "directory for the full diagnostics dump (default: a temp folder)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("-y, --yes", "skip prompts, scan all workspace projects").option("--no-parallel", "lint serially with one worker (default: parallel across CPU cores; set the worker count with REACT_DOCTOR_PARALLEL)").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple); overrides the `projects` config field").option("--scope <value>", "how much to scan/report: full (default), files, changed (only new issues vs base), or lines (only changed lines)").option("--base <ref>", "base git ref for files/changed/lines scope (auto-detected when omitted)").addOption(new Option("--diff [base]", "[deprecated] alias for --scope changed (pass `false` to force a full scan)").hideHelp()).addOption(new Option("--changed-files-from <file>", "scan source files listed in a newline-delimited changed-files file").hideHelp()).option("--no-score", "skip the score API, the share URL, and crash reporting").addOption(new Option("--category <category>", "only show diagnostics in a category (repeatable; e.g. Security)").argParser(collectCategoryOption)).option("--no-telemetry", "alias for --no-score (skip the score API, share URL, and crash reporting)").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").addOption(new Option("--fail-on <level>", "[deprecated] alias for --blocking <level>").hideHelp()).option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").option("--warnings", "show warning-severity diagnostics (default)").option("--no-warnings", "hide warning-severity diagnostics (errors only)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderRootHelpEpilog);
|
|
53909
55311
|
program.action(inspectAction);
|
|
53910
55312
|
program.command("why <location>").description("Explain why a rule fired (or why a suppression didn't apply) at a file:line").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple)").option("-c, --cwd <cwd>", "working directory", process.cwd()).option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").action((location, options) => whyAction(location, options));
|
|
53911
55313
|
program.command("install").alias("setup").description("Install the react-doctor skill into your coding agents and optional git hook").option("-y, --yes", "skip prompts, install for all detected agents").option("--dry-run", "show what would be installed without writing files").option("--agent-hooks", "install native non-blocking agent hooks for Claude Code and Cursor").option("-c, --cwd <cwd>", "working directory", process.cwd()).option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderInstallHelpEpilog).action(installAction);
|
|
@@ -53948,4 +55350,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
|
|
|
53948
55350
|
export {};
|
|
53949
55351
|
|
|
53950
55352
|
//# sourceMappingURL=cli.js.map
|
|
53951
|
-
//# debugId=
|
|
55353
|
+
//# debugId=6a2c86b6-c3a8-5e50-8e7b-daef840df200
|