react-doctor 0.5.6 → 0.5.7-dev.2cadd3f
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 +2029 -630
- package/dist/index.d.ts +38 -4
- package/dist/index.js +1216 -323
- package/dist/lsp.js +1235 -347
- package/package.json +6 -6
package/dist/index.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]="c1d3306c-dbf2-5096-b48e-5c9bb3128102")}catch(e){}}();
|
|
3
3
|
import { r as __toESM$1, t as __commonJSMin$1 } from "./chunk-N93fKeF6.js";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import * as NFS from "node:fs";
|
|
@@ -9,7 +9,7 @@ import path from "node:path";
|
|
|
9
9
|
import * as NodeChildProcess from "node:child_process";
|
|
10
10
|
import { spawn, spawnSync } from "node:child_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 from "node:os";
|
|
15
15
|
import { parseJSON5 } from "confbox";
|
|
@@ -17,6 +17,7 @@ import * as NodeUrl from "node:url";
|
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
18
|
import { createJiti } from "jiti";
|
|
19
19
|
import * as Crypto from "node:crypto";
|
|
20
|
+
import crypto, { createHash } from "node:crypto";
|
|
20
21
|
import { gzipSync } from "node:zlib";
|
|
21
22
|
//#region ../../node_modules/.pnpm/effect@4.0.0-beta.70/node_modules/effect/dist/Pipeable.js
|
|
22
23
|
/**
|
|
@@ -7198,7 +7199,7 @@ const provideContext$1 = /* @__PURE__ */ dual(2, (self, context) => {
|
|
|
7198
7199
|
return updateContext$1(self, merge$3(context));
|
|
7199
7200
|
});
|
|
7200
7201
|
/** @internal */
|
|
7201
|
-
const provideService$
|
|
7202
|
+
const provideService$3 = function() {
|
|
7202
7203
|
if (arguments.length === 1) return dual(2, (self, impl) => provideServiceImpl(self, arguments[0], impl));
|
|
7203
7204
|
return dual(3, (self, service, impl) => provideServiceImpl(self, service, impl)).apply(this, arguments);
|
|
7204
7205
|
};
|
|
@@ -7429,7 +7430,7 @@ const constScopeEmpty = { _tag: "Empty" };
|
|
|
7429
7430
|
/** @internal */
|
|
7430
7431
|
const scope = scopeTag;
|
|
7431
7432
|
/** @internal */
|
|
7432
|
-
const provideScope = /* @__PURE__ */ provideService$
|
|
7433
|
+
const provideScope = /* @__PURE__ */ provideService$3(scopeTag);
|
|
7433
7434
|
/** @internal */
|
|
7434
7435
|
const scoped$1 = (self) => withFiber$1((fiber) => {
|
|
7435
7436
|
const prev = fiber.context;
|
|
@@ -7870,7 +7871,7 @@ const makeLatchUnsafe = (open) => new Latch(open ?? false);
|
|
|
7870
7871
|
/** @internal */
|
|
7871
7872
|
const makeLatch = (open) => sync$2(() => makeLatchUnsafe(open));
|
|
7872
7873
|
/** @internal */
|
|
7873
|
-
const withTracerEnabled$1 = /* @__PURE__ */ provideService$
|
|
7874
|
+
const withTracerEnabled$1 = /* @__PURE__ */ provideService$3(TracerEnabled);
|
|
7874
7875
|
const bigint0 = /* @__PURE__ */ BigInt(0);
|
|
7875
7876
|
const NoopSpanProto = {
|
|
7876
7877
|
_tag: "Span",
|
|
@@ -7951,7 +7952,7 @@ const useSpan$1 = (name, ...args) => {
|
|
|
7951
7952
|
}));
|
|
7952
7953
|
});
|
|
7953
7954
|
};
|
|
7954
|
-
const provideParentSpan = /* @__PURE__ */ provideService$
|
|
7955
|
+
const provideParentSpan = /* @__PURE__ */ provideService$3(ParentSpan);
|
|
7955
7956
|
/** @internal */
|
|
7956
7957
|
const withParentSpan$1 = function() {
|
|
7957
7958
|
const dataFirst = isEffect$1(arguments[0]);
|
|
@@ -9518,7 +9519,7 @@ var CurrentMemoMap = class extends Service()("effect/Layer/CurrentMemoMap") {
|
|
|
9518
9519
|
* @category memo map
|
|
9519
9520
|
* @since 2.0.0
|
|
9520
9521
|
*/
|
|
9521
|
-
const buildWithMemoMap = /* @__PURE__ */ dual(3, (self, memoMap, scope) => provideService$
|
|
9522
|
+
const buildWithMemoMap = /* @__PURE__ */ dual(3, (self, memoMap, scope) => provideService$3(map$4(self.build(memoMap, scope), add(CurrentMemoMap, memoMap)), CurrentMemoMap, memoMap));
|
|
9522
9523
|
/**
|
|
9523
9524
|
* Builds a layer into an `Effect` value. Any resources associated with this
|
|
9524
9525
|
* layer will be released when the specified scope is closed unless their scope
|
|
@@ -10871,7 +10872,7 @@ const provide$1 = /* @__PURE__ */ dual((args) => isEffect$1(args[0]), (self, sou
|
|
|
10871
10872
|
/** @internal */
|
|
10872
10873
|
const repeatOrElse = /* @__PURE__ */ dual(3, (self, schedule, orElse) => flatMap$4(toStepWithMetadata(schedule), (step) => {
|
|
10873
10874
|
let meta = CurrentMetadata.defaultValue();
|
|
10874
|
-
return catch_$2(forever$2(tap$2(flatMap$4(suspend$3(() => provideService$
|
|
10875
|
+
return catch_$2(forever$2(tap$2(flatMap$4(suspend$3(() => provideService$3(self, CurrentMetadata, meta)), step), (meta_) => sync$2(() => {
|
|
10875
10876
|
meta = meta_;
|
|
10876
10877
|
})), { disableYield: true }), (error) => isDone$2(error) ? succeed$5(error.value) : orElse(error, meta.attempt === 0 ? none() : some(meta)));
|
|
10877
10878
|
}));
|
|
@@ -10879,7 +10880,7 @@ const repeatOrElse = /* @__PURE__ */ dual(3, (self, schedule, orElse) => flatMap
|
|
|
10879
10880
|
const retryOrElse = /* @__PURE__ */ dual(3, (self, policy, orElse) => flatMap$4(toStepWithMetadata(policy), (step) => {
|
|
10880
10881
|
let meta = CurrentMetadata.defaultValue();
|
|
10881
10882
|
let lastError;
|
|
10882
|
-
const loop = catch_$2(suspend$3(() => provideService$
|
|
10883
|
+
const loop = catch_$2(suspend$3(() => provideService$3(self, CurrentMetadata, meta)), (error) => {
|
|
10883
10884
|
lastError = error;
|
|
10884
10885
|
return flatMap$4(step(error), (meta_) => {
|
|
10885
10886
|
meta = meta_;
|
|
@@ -12995,7 +12996,7 @@ const updateContext = updateContext$1;
|
|
|
12995
12996
|
* @category Context
|
|
12996
12997
|
* @since 2.0.0
|
|
12997
12998
|
*/
|
|
12998
|
-
const provideService = provideService$
|
|
12999
|
+
const provideService$2 = provideService$3;
|
|
12999
13000
|
/**
|
|
13000
13001
|
* Scopes all resources used in this workflow to the lifetime of the workflow,
|
|
13001
13002
|
* ensuring that their finalizers are run as soon as this workflow completes
|
|
@@ -17989,6 +17990,20 @@ function decodeUnknownOption$1(schema, options) {
|
|
|
17989
17990
|
return asOption(decodeUnknownEffect(schema, options));
|
|
17990
17991
|
}
|
|
17991
17992
|
/**
|
|
17993
|
+
* Creates a synchronous decoder for `unknown` input.
|
|
17994
|
+
*
|
|
17995
|
+
* **Details**
|
|
17996
|
+
*
|
|
17997
|
+
* The returned function returns the decoded `Type` on success and throws an
|
|
17998
|
+
* `Error` with the `SchemaIssue.Issue` in its `cause` on decoding failure.
|
|
17999
|
+
*
|
|
18000
|
+
* @category decoding
|
|
18001
|
+
* @since 3.10.0
|
|
18002
|
+
*/
|
|
18003
|
+
function decodeUnknownSync$1(schema, options) {
|
|
18004
|
+
return asSync(decodeUnknownEffect(schema, options));
|
|
18005
|
+
}
|
|
18006
|
+
/**
|
|
17992
18007
|
* Creates an effectful encoder for `unknown` input.
|
|
17993
18008
|
*
|
|
17994
18009
|
* **Details**
|
|
@@ -18290,6 +18305,40 @@ function isSchemaError(u) {
|
|
|
18290
18305
|
*/
|
|
18291
18306
|
const decodeUnknownOption = decodeUnknownOption$1;
|
|
18292
18307
|
/**
|
|
18308
|
+
* Decodes an `unknown` input against a schema synchronously, returning the
|
|
18309
|
+
* decoded value or throwing an `Error` whose cause contains the schema issue.
|
|
18310
|
+
* Use this when you want to validate data at a boundary and treat a schema
|
|
18311
|
+
* mismatch as an exception. For typed input use `decodeSync`.
|
|
18312
|
+
*
|
|
18313
|
+
* **Details**
|
|
18314
|
+
*
|
|
18315
|
+
* Only service-free schemas can be decoded synchronously. For non-throwing
|
|
18316
|
+
* alternatives see `decodeUnknownOption`, `decodeUnknownExit`, or
|
|
18317
|
+
* `decodeUnknownEffect`. Options may be provided either when creating the
|
|
18318
|
+
* decoder or when applying it; application options override creation options.
|
|
18319
|
+
*
|
|
18320
|
+
* **Example** (Decoding with a transformation schema)
|
|
18321
|
+
*
|
|
18322
|
+
* ```ts
|
|
18323
|
+
* import { Schema } from "effect"
|
|
18324
|
+
*
|
|
18325
|
+
* const NumberFromString = Schema.NumberFromString
|
|
18326
|
+
*
|
|
18327
|
+
* console.log(Schema.decodeUnknownSync(NumberFromString)("42"))
|
|
18328
|
+
* // Output: 42
|
|
18329
|
+
*
|
|
18330
|
+
* Schema.decodeUnknownSync(NumberFromString)("not a number")
|
|
18331
|
+
* // throws SchemaError: NumberFromString
|
|
18332
|
+
* // └─ Encoded side transformation failure
|
|
18333
|
+
* // └─ NumberFromString
|
|
18334
|
+
* // └─ Expected a numeric string, actual "not a number"
|
|
18335
|
+
* ```
|
|
18336
|
+
*
|
|
18337
|
+
* @category decoding
|
|
18338
|
+
* @since 4.0.0
|
|
18339
|
+
*/
|
|
18340
|
+
const decodeUnknownSync = decodeUnknownSync$1;
|
|
18341
|
+
/**
|
|
18293
18342
|
* Encodes an `unknown` input against a schema synchronously, throwing a
|
|
18294
18343
|
* {@link SchemaError} on failure. Use this when you want to serialize data at a
|
|
18295
18344
|
* boundary and treat a schema mismatch as an unrecoverable error. For
|
|
@@ -19256,7 +19305,8 @@ var Diagnostic = class extends Class("Diagnostic")({
|
|
|
19256
19305
|
category: String$1,
|
|
19257
19306
|
fileContext: optional(Literals(["test", "story"])),
|
|
19258
19307
|
suppressionHint: optional(String$1),
|
|
19259
|
-
relatedLocations: optional(ArraySchema(DiagnosticRelatedLocation))
|
|
19308
|
+
relatedLocations: optional(ArraySchema(DiagnosticRelatedLocation)),
|
|
19309
|
+
fixGroupId: optional(String$1)
|
|
19260
19310
|
}) {};
|
|
19261
19311
|
const JsonReportMode = Literals([
|
|
19262
19312
|
"full",
|
|
@@ -19298,6 +19348,7 @@ var JsonReportProjectEntry = class extends Class("JsonReportProjectEntry")({
|
|
|
19298
19348
|
score: Unknown,
|
|
19299
19349
|
skippedChecks: ArraySchema(String$1),
|
|
19300
19350
|
skippedCheckReasons: optional(Record$1(String$1, String$1)),
|
|
19351
|
+
scannedFileCount: optional(Number$1),
|
|
19301
19352
|
elapsedMilliseconds: Number$1
|
|
19302
19353
|
}) {};
|
|
19303
19354
|
/**
|
|
@@ -25167,6 +25218,14 @@ const runWith = (self, f, onHalt) => suspend$2(() => {
|
|
|
25167
25218
|
return catchDone(flatMap$2(toTransform(self)(done$1(), scope), f), onHalt ? onHalt : succeed$2).pipe(onExit$1((exit) => close(scope, exit)));
|
|
25168
25219
|
});
|
|
25169
25220
|
/**
|
|
25221
|
+
* Provides a concrete service for a context key, removing that service
|
|
25222
|
+
* requirement from the returned channel.
|
|
25223
|
+
*
|
|
25224
|
+
* @category services
|
|
25225
|
+
* @since 2.0.0
|
|
25226
|
+
*/
|
|
25227
|
+
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))));
|
|
25228
|
+
/**
|
|
25170
25229
|
* Runs a channel and applies an effect to each output element.
|
|
25171
25230
|
*
|
|
25172
25231
|
* **Example** (Running effects for each output)
|
|
@@ -26555,6 +26614,44 @@ const splitLines = (self) => self.channel.pipe(pipeTo(splitLines$1()), fromChann
|
|
|
26555
26614
|
*/
|
|
26556
26615
|
const ensuring = /* @__PURE__ */ dual(2, (self, finalizer) => fromChannel(ensuring$1(self.channel, finalizer)));
|
|
26557
26616
|
/**
|
|
26617
|
+
* Provides the stream with a single required service, eliminating that
|
|
26618
|
+
* requirement from its environment.
|
|
26619
|
+
*
|
|
26620
|
+
* **Example** (Providing a stream service)
|
|
26621
|
+
*
|
|
26622
|
+
* ```ts
|
|
26623
|
+
* import { Console, Context, Effect, Stream } from "effect"
|
|
26624
|
+
*
|
|
26625
|
+
* class Greeter extends Context.Service<Greeter, {
|
|
26626
|
+
* greet: (name: string) => string
|
|
26627
|
+
* }>()("Greeter") {}
|
|
26628
|
+
*
|
|
26629
|
+
* const stream = Stream.fromEffect(
|
|
26630
|
+
* Effect.service(Greeter).pipe(
|
|
26631
|
+
* Effect.map((greeter) => greeter.greet("Ada"))
|
|
26632
|
+
* )
|
|
26633
|
+
* )
|
|
26634
|
+
*
|
|
26635
|
+
* const program = Effect.gen(function*() {
|
|
26636
|
+
* const collected = yield* Stream.runCollect(
|
|
26637
|
+
* stream.pipe(
|
|
26638
|
+
* Stream.provideService(Greeter, {
|
|
26639
|
+
* greet: (name) => `Hello, ${name}`
|
|
26640
|
+
* })
|
|
26641
|
+
* )
|
|
26642
|
+
* )
|
|
26643
|
+
* yield* Console.log(collected)
|
|
26644
|
+
* })
|
|
26645
|
+
*
|
|
26646
|
+
* Effect.runPromise(program)
|
|
26647
|
+
* //=> ["Hello, Ada"]
|
|
26648
|
+
* ```
|
|
26649
|
+
*
|
|
26650
|
+
* @category services
|
|
26651
|
+
* @since 2.0.0
|
|
26652
|
+
*/
|
|
26653
|
+
const provideService = /* @__PURE__ */ dual(3, (self, key, service) => fromChannel(provideService$1(self.channel, key, service)));
|
|
26654
|
+
/**
|
|
26558
26655
|
* Runs a stream with a sink and returns the sink result.
|
|
26559
26656
|
*
|
|
26560
26657
|
* **Example** (Running a stream with a sink)
|
|
@@ -29984,7 +30081,7 @@ const make$8 = /* @__PURE__ */ fnUntraced(function* (options) {
|
|
|
29984
30081
|
const runFork = runForkWith(services);
|
|
29985
30082
|
const exportInterval = max(fromInputUnsafe(options.exportInterval), zero);
|
|
29986
30083
|
let disabledUntil = void 0;
|
|
29987
|
-
const client = filterStatusOk(get$4(services, HttpClient)).pipe(transformResponse(provideService(TracerPropagationEnabled, false)), retryTransient({
|
|
30084
|
+
const client = filterStatusOk(get$4(services, HttpClient)).pipe(transformResponse(provideService$2(TracerPropagationEnabled, false)), retryTransient({
|
|
29988
30085
|
schedule: policy,
|
|
29989
30086
|
times: 3
|
|
29990
30087
|
}));
|
|
@@ -32714,16 +32811,26 @@ const isMinifiedSource = (absolutePath) => {
|
|
|
32714
32811
|
if (fileDescriptor !== void 0) NFS.closeSync(fileDescriptor);
|
|
32715
32812
|
}
|
|
32716
32813
|
};
|
|
32717
|
-
const
|
|
32718
|
-
|
|
32814
|
+
const cachedIsLargeMinifiedByPath = /* @__PURE__ */ new Map();
|
|
32815
|
+
const clearMinifiedFileCache = () => {
|
|
32816
|
+
cachedIsLargeMinifiedByPath.clear();
|
|
32817
|
+
};
|
|
32818
|
+
const statSourceFileSize = (absolutePath) => {
|
|
32719
32819
|
try {
|
|
32720
|
-
|
|
32820
|
+
return NFS.statSync(absolutePath).size;
|
|
32721
32821
|
} catch {
|
|
32722
|
-
return
|
|
32822
|
+
return null;
|
|
32723
32823
|
}
|
|
32724
|
-
if (sizeBytes < 2e4) return false;
|
|
32725
|
-
return isMinifiedSource(absolutePath);
|
|
32726
32824
|
};
|
|
32825
|
+
const isLargeMinifiedFile = (absolutePath, knownSizeBytes) => {
|
|
32826
|
+
const cached = cachedIsLargeMinifiedByPath.get(absolutePath);
|
|
32827
|
+
if (cached !== void 0) return cached;
|
|
32828
|
+
const sizeBytes = knownSizeBytes === void 0 ? statSourceFileSize(absolutePath) : knownSizeBytes;
|
|
32829
|
+
const result = sizeBytes !== null && sizeBytes >= 2e4 && isMinifiedSource(absolutePath);
|
|
32830
|
+
cachedIsLargeMinifiedByPath.set(absolutePath, result);
|
|
32831
|
+
return result;
|
|
32832
|
+
};
|
|
32833
|
+
const isErrnoException = (error) => error instanceof Error && "code" in error;
|
|
32727
32834
|
const IGNORABLE_READDIR_ERROR_CODES = new Set([
|
|
32728
32835
|
"EACCES",
|
|
32729
32836
|
"EPERM",
|
|
@@ -32733,11 +32840,7 @@ const IGNORABLE_READDIR_ERROR_CODES = new Set([
|
|
|
32733
32840
|
"ELOOP",
|
|
32734
32841
|
"ENAMETOOLONG"
|
|
32735
32842
|
]);
|
|
32736
|
-
const isIgnorableReaddirError = (error) =>
|
|
32737
|
-
if (typeof error !== "object" || error === null) return false;
|
|
32738
|
-
const errorCode = error.code;
|
|
32739
|
-
return typeof errorCode === "string" && IGNORABLE_READDIR_ERROR_CODES.has(errorCode);
|
|
32740
|
-
};
|
|
32843
|
+
const isIgnorableReaddirError = (error) => isErrnoException(error) && typeof error.code === "string" && IGNORABLE_READDIR_ERROR_CODES.has(error.code);
|
|
32741
32844
|
const readDirectoryEntries = (directoryPath) => {
|
|
32742
32845
|
try {
|
|
32743
32846
|
return NFS.readdirSync(directoryPath, { withFileTypes: true });
|
|
@@ -32787,7 +32890,7 @@ const readPackageJsonUncached = (packageJsonPath) => {
|
|
|
32787
32890
|
return JSON.parse(NFS.readFileSync(packageJsonPath, "utf-8"));
|
|
32788
32891
|
} catch (error) {
|
|
32789
32892
|
if (error instanceof SyntaxError) return {};
|
|
32790
|
-
if (error
|
|
32893
|
+
if (isErrnoException(error)) {
|
|
32791
32894
|
const { code } = error;
|
|
32792
32895
|
if (code === "EISDIR" || code === "EACCES" || code === "EPERM" || code === "ENOENT") return {};
|
|
32793
32896
|
}
|
|
@@ -33512,17 +33615,13 @@ const isPackageJsonReactNativeAware = (packageJson) => {
|
|
|
33512
33615
|
return false;
|
|
33513
33616
|
};
|
|
33514
33617
|
const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
|
|
33515
|
-
const
|
|
33516
|
-
const spec = packageJson.dependencies?.
|
|
33618
|
+
const getDependencySpec = (packageJson, packageName) => {
|
|
33619
|
+
const spec = packageJson.dependencies?.[packageName] ?? packageJson.devDependencies?.[packageName] ?? packageJson.peerDependencies?.[packageName] ?? packageJson.optionalDependencies?.[packageName];
|
|
33517
33620
|
return typeof spec === "string" ? spec : null;
|
|
33518
33621
|
};
|
|
33519
|
-
const findExpoVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson,
|
|
33622
|
+
const findExpoVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "expo"));
|
|
33520
33623
|
const SHOPIFY_FLASH_LIST_PACKAGE_NAME = "@shopify/flash-list";
|
|
33521
|
-
const
|
|
33522
|
-
const spec = packageJson.dependencies?.["@shopify/flash-list"] ?? packageJson.devDependencies?.["@shopify/flash-list"] ?? packageJson.peerDependencies?.["@shopify/flash-list"] ?? packageJson.optionalDependencies?.["@shopify/flash-list"];
|
|
33523
|
-
return typeof spec === "string" ? spec : null;
|
|
33524
|
-
};
|
|
33525
|
-
const findShopifyFlashListVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, getShopifyFlashListDependencySpec);
|
|
33624
|
+
const findShopifyFlashListVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, SHOPIFY_FLASH_LIST_PACKAGE_NAME));
|
|
33526
33625
|
const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson, packageName, version }) => {
|
|
33527
33626
|
if (version === null || !isCatalogReference(version)) return version;
|
|
33528
33627
|
const catalogName = extractCatalogName(version);
|
|
@@ -33534,11 +33633,7 @@ const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson,
|
|
|
33534
33633
|
if (!isFile(monorepoPackageJsonPath)) return version;
|
|
33535
33634
|
return resolveCatalogVersion(readPackageJson(monorepoPackageJsonPath), packageName, monorepoRoot, catalogName) ?? version;
|
|
33536
33635
|
};
|
|
33537
|
-
const
|
|
33538
|
-
const spec = packageJson.dependencies?.next ?? packageJson.devDependencies?.next ?? packageJson.peerDependencies?.next ?? packageJson.optionalDependencies?.next;
|
|
33539
|
-
return typeof spec === "string" ? spec : null;
|
|
33540
|
-
};
|
|
33541
|
-
const findNextjsVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, getNextjsDependencySpec);
|
|
33636
|
+
const findNextjsVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "next"));
|
|
33542
33637
|
const getPreactVersion = (packageJson) => {
|
|
33543
33638
|
return {
|
|
33544
33639
|
...packageJson.peerDependencies,
|
|
@@ -33599,6 +33694,7 @@ const MILLISECONDS_PER_SECOND = 1e3;
|
|
|
33599
33694
|
const SCORE_API_URL = "https://www.react.doctor/api/score";
|
|
33600
33695
|
const FETCH_TIMEOUT_MS = 1e4;
|
|
33601
33696
|
const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
|
|
33697
|
+
const PER_WORKER_MEM_BUDGET_BYTES = 1024 * 1024 * 1024;
|
|
33602
33698
|
const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
|
|
33603
33699
|
const ADOPTABLE_LINT_CONFIG_FILENAMES = [".oxlintrc.json", ".eslintrc.json"];
|
|
33604
33700
|
const GIT_SHOW_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
@@ -33620,6 +33716,11 @@ const ES_TARGET_YEAR_BY_NAME = {
|
|
|
33620
33716
|
esnext: 9999
|
|
33621
33717
|
};
|
|
33622
33718
|
/**
|
|
33719
|
+
* tsconfig filenames probed when resolving a project's TypeScript
|
|
33720
|
+
* compiler options — the root config first, then a monorepo base config.
|
|
33721
|
+
*/
|
|
33722
|
+
const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
|
|
33723
|
+
/**
|
|
33623
33724
|
* Project-config files that `StagedFiles.materialize` copies into
|
|
33624
33725
|
* the temp directory alongside staged sources so oxlint resolves
|
|
33625
33726
|
* `tsconfig` / `package.json` / lint configs the same way it would
|
|
@@ -33643,7 +33744,16 @@ const STAGED_FILES_PROJECT_CONFIG_FILENAMES = [
|
|
|
33643
33744
|
];
|
|
33644
33745
|
const OXLINT_OUTPUT_MAX_BYTES = 50 * 1024 * 1024;
|
|
33645
33746
|
const OXLINT_SPAWN_TIMEOUT_MS = 6e4;
|
|
33747
|
+
const NODE_COMPILE_CACHE_DIR_NAME = "node-compile-cache";
|
|
33748
|
+
const DEAD_CODE_WORKER_TIMEOUT_MS = 12e4;
|
|
33749
|
+
const OXLINT_SPLIT_TOTAL_BUDGET_MS = 18e4;
|
|
33750
|
+
const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
|
|
33751
|
+
const LINT_PHASE_TIMEOUT_MS = 3e5;
|
|
33752
|
+
const SCAN_TOTAL_DEADLINE_MS = 9e5;
|
|
33646
33753
|
const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
|
|
33754
|
+
const DEAD_CODE_TIMEOUT_CEILING_MS = 6e5;
|
|
33755
|
+
const DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS = 3e4;
|
|
33756
|
+
const DEAD_CODE_OVERLAP_PARSE_SHARE = .4;
|
|
33647
33757
|
const RECOMMENDED_PNPM_MINIMUM_RELEASE_AGE_MINUTES = 10080;
|
|
33648
33758
|
const REACT_SERVER_DOM_PACKAGES = [
|
|
33649
33759
|
"react-server-dom-webpack",
|
|
@@ -33666,14 +33776,25 @@ const APP_ONLY_RULE_KEYS = new Set([
|
|
|
33666
33776
|
]);
|
|
33667
33777
|
const COMPILER_CLEANUP_BUCKET = "compiler-cleanup";
|
|
33668
33778
|
const COMPILER_CLEANUP_RULE_KEYS = new Set(["react-doctor/react-compiler-no-manual-memoization"]);
|
|
33779
|
+
const ROOT_CAUSE_GROUPABLE_RULE_KEYS = new Set([
|
|
33780
|
+
"react-doctor/no-derived-state",
|
|
33781
|
+
"react-doctor/no-derived-state-effect",
|
|
33782
|
+
"react-doctor/no-derived-useState",
|
|
33783
|
+
"react-doctor/no-adjust-state-on-prop-change",
|
|
33784
|
+
"react-doctor/no-reset-all-state-on-prop-change"
|
|
33785
|
+
]);
|
|
33669
33786
|
const MAX_GLOB_PATTERN_LENGTH_CHARS = 1024;
|
|
33670
33787
|
const CONFIG_CACHE_TTL_MS = 300 * 1e3;
|
|
33671
33788
|
const SOCKET_FREE_PURL_API_BASE = "https://firewall-api.socket.dev/purl";
|
|
33672
33789
|
const SOCKET_PACKAGE_PAGE_BASE = "https://socket.dev/npm/package";
|
|
33673
33790
|
const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
|
|
33791
|
+
const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
|
|
33792
|
+
const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
|
|
33674
33793
|
const SUPPLY_CHAIN_PLUGIN = "socket";
|
|
33675
33794
|
const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
|
|
33676
33795
|
const SUPPLY_CHAIN_CATEGORY = "Security";
|
|
33796
|
+
const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
|
|
33797
|
+
const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
|
|
33677
33798
|
const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
|
|
33678
33799
|
const TSCONFIG_FILENAME = "tsconfig.json";
|
|
33679
33800
|
const isRelativeExtendsValue = (extendsValue) => extendsValue.startsWith("./") || extendsValue.startsWith("../") || Path.isAbsolute(extendsValue);
|
|
@@ -34118,6 +34239,7 @@ const isTailwindAtLeast = (detected, required) => {
|
|
|
34118
34239
|
if (detected.major !== required.major) return detected.major > required.major;
|
|
34119
34240
|
return detected.minor >= required.minor;
|
|
34120
34241
|
};
|
|
34242
|
+
const messageFromUnknown = (error) => error instanceof Error ? error.message : String(error);
|
|
34121
34243
|
var InvalidGlobPatternError = class extends Error {
|
|
34122
34244
|
pattern;
|
|
34123
34245
|
reason;
|
|
@@ -34146,7 +34268,7 @@ const compileGlobPattern = (rawPattern) => {
|
|
|
34146
34268
|
try {
|
|
34147
34269
|
return import_picomatch.default.makeRe(normalizeGlobPattern(rawPattern), PICOMATCH_OPTIONS);
|
|
34148
34270
|
} catch (caughtError) {
|
|
34149
|
-
throw new InvalidGlobPatternError(rawPattern,
|
|
34271
|
+
throw new InvalidGlobPatternError(rawPattern, messageFromUnknown(caughtError));
|
|
34150
34272
|
}
|
|
34151
34273
|
};
|
|
34152
34274
|
const compileGlobPatternsLenient = (patterns, onInvalid) => {
|
|
@@ -34242,115 +34364,6 @@ const buildRuleSeverityControls = (config) => {
|
|
|
34242
34364
|
...config.buckets !== void 0 ? { buckets: config.buckets } : {}
|
|
34243
34365
|
};
|
|
34244
34366
|
};
|
|
34245
|
-
const JSX_OPENER_TAG_PATTERN = /<[A-Za-z][\w.]*/g;
|
|
34246
|
-
const JSX_TAG_NAME_FOLLOW = /[A-Za-z]/;
|
|
34247
|
-
const isOpenerMatchInsideLineComment = (line, openerCharIndex) => {
|
|
34248
|
-
let stringDelimiter = null;
|
|
34249
|
-
for (let charIndex = 0; charIndex < openerCharIndex; charIndex++) {
|
|
34250
|
-
const character = line[charIndex];
|
|
34251
|
-
if (stringDelimiter !== null) {
|
|
34252
|
-
if (character === "\\") {
|
|
34253
|
-
charIndex++;
|
|
34254
|
-
continue;
|
|
34255
|
-
}
|
|
34256
|
-
if (character === stringDelimiter) stringDelimiter = null;
|
|
34257
|
-
continue;
|
|
34258
|
-
}
|
|
34259
|
-
if (character === "\"" || character === "'" || character === "`") {
|
|
34260
|
-
stringDelimiter = character;
|
|
34261
|
-
continue;
|
|
34262
|
-
}
|
|
34263
|
-
if (character === "/" && line[charIndex + 1] === "/") return true;
|
|
34264
|
-
}
|
|
34265
|
-
return false;
|
|
34266
|
-
};
|
|
34267
|
-
const findOpenerTagOnLine = (line) => {
|
|
34268
|
-
for (const match of line.matchAll(JSX_OPENER_TAG_PATTERN)) {
|
|
34269
|
-
if (match.index === void 0) continue;
|
|
34270
|
-
if (!isOpenerMatchInsideLineComment(line, match.index)) return { startCharIndex: match.index + match[0].length };
|
|
34271
|
-
}
|
|
34272
|
-
return null;
|
|
34273
|
-
};
|
|
34274
|
-
const findJsxOpenerSpan = (lines, openerLineIndex) => {
|
|
34275
|
-
const openerLine = lines[openerLineIndex];
|
|
34276
|
-
if (openerLine === void 0) return null;
|
|
34277
|
-
const opener = findOpenerTagOnLine(openerLine);
|
|
34278
|
-
if (!opener) return null;
|
|
34279
|
-
const lookaheadLimit = Math.min(lines.length, openerLineIndex + 32);
|
|
34280
|
-
let braceDepth = 0;
|
|
34281
|
-
let innerAngleDepth = 0;
|
|
34282
|
-
let stringDelimiter = null;
|
|
34283
|
-
for (let lineIndex = openerLineIndex; lineIndex < lookaheadLimit; lineIndex++) {
|
|
34284
|
-
const currentLine = lines[lineIndex];
|
|
34285
|
-
const startCharForLine = lineIndex === openerLineIndex ? opener.startCharIndex : 0;
|
|
34286
|
-
for (let charIndex = startCharForLine; charIndex < currentLine.length; charIndex++) {
|
|
34287
|
-
const character = currentLine[charIndex];
|
|
34288
|
-
if (stringDelimiter !== null) {
|
|
34289
|
-
if (character === "\\") {
|
|
34290
|
-
charIndex++;
|
|
34291
|
-
continue;
|
|
34292
|
-
}
|
|
34293
|
-
if (character === stringDelimiter) stringDelimiter = null;
|
|
34294
|
-
continue;
|
|
34295
|
-
}
|
|
34296
|
-
if (character === "\"" || character === "'" || character === "`") {
|
|
34297
|
-
stringDelimiter = character;
|
|
34298
|
-
continue;
|
|
34299
|
-
}
|
|
34300
|
-
if (character === "{") {
|
|
34301
|
-
braceDepth++;
|
|
34302
|
-
continue;
|
|
34303
|
-
}
|
|
34304
|
-
if (character === "}") {
|
|
34305
|
-
braceDepth--;
|
|
34306
|
-
continue;
|
|
34307
|
-
}
|
|
34308
|
-
if (braceDepth !== 0) continue;
|
|
34309
|
-
if (character === "<") {
|
|
34310
|
-
const followCharacter = currentLine[charIndex + 1];
|
|
34311
|
-
if (followCharacter !== void 0 && JSX_TAG_NAME_FOLLOW.test(followCharacter)) innerAngleDepth++;
|
|
34312
|
-
continue;
|
|
34313
|
-
}
|
|
34314
|
-
if (character !== ">") continue;
|
|
34315
|
-
const previousCharacter = currentLine[charIndex - 1];
|
|
34316
|
-
const nextCharacter = currentLine[charIndex + 1];
|
|
34317
|
-
if (previousCharacter === "=" || nextCharacter === "=") continue;
|
|
34318
|
-
if (innerAngleDepth > 0) {
|
|
34319
|
-
innerAngleDepth--;
|
|
34320
|
-
continue;
|
|
34321
|
-
}
|
|
34322
|
-
return lineIndex;
|
|
34323
|
-
}
|
|
34324
|
-
}
|
|
34325
|
-
return null;
|
|
34326
|
-
};
|
|
34327
|
-
const findEnclosingMultilineJsxOpenerStart = (lines, diagnosticLineIndex) => {
|
|
34328
|
-
for (let candidateIndex = diagnosticLineIndex - 1; candidateIndex >= 0 && diagnosticLineIndex - candidateIndex <= 32; candidateIndex--) {
|
|
34329
|
-
const openerCloseIndex = findJsxOpenerSpan(lines, candidateIndex);
|
|
34330
|
-
if (openerCloseIndex !== null && openerCloseIndex >= diagnosticLineIndex) return candidateIndex;
|
|
34331
|
-
}
|
|
34332
|
-
return null;
|
|
34333
|
-
};
|
|
34334
|
-
const DISABLE_NEXT_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-next-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
|
|
34335
|
-
const findStackedDisableCommentsAbove = (lines, anchorIndex) => {
|
|
34336
|
-
const collected = [];
|
|
34337
|
-
let isStillInChain = true;
|
|
34338
|
-
for (let candidateIndex = anchorIndex - 1; candidateIndex >= 0 && anchorIndex - candidateIndex <= 10; candidateIndex--) {
|
|
34339
|
-
const candidateLine = lines[candidateIndex];
|
|
34340
|
-
if (candidateLine === void 0) break;
|
|
34341
|
-
const match = candidateLine.match(DISABLE_NEXT_LINE_PATTERN);
|
|
34342
|
-
if (match) {
|
|
34343
|
-
collected.push({
|
|
34344
|
-
commentLineIndex: candidateIndex,
|
|
34345
|
-
ruleList: match[1],
|
|
34346
|
-
isInChain: isStillInChain
|
|
34347
|
-
});
|
|
34348
|
-
continue;
|
|
34349
|
-
}
|
|
34350
|
-
isStillInChain = false;
|
|
34351
|
-
}
|
|
34352
|
-
return collected;
|
|
34353
|
-
};
|
|
34354
34367
|
const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
|
|
34355
34368
|
"effect/no-adjust-state-on-prop-change": "react-doctor/no-adjust-state-on-prop-change",
|
|
34356
34369
|
"effect/no-chain-state-updates": "react-doctor/no-chain-state-updates",
|
|
@@ -34475,7 +34488,13 @@ for (const [legacyRuleKey, nativeRuleKey] of Object.entries(LEGACY_RULE_KEY_TO_N
|
|
|
34475
34488
|
}
|
|
34476
34489
|
const getLegacyRuleKeysForNative = (ruleKey) => NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(ruleKey) ?? [];
|
|
34477
34490
|
const canonicalizeRuleKey = (ruleKey) => LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey] ?? ruleKey;
|
|
34478
|
-
const
|
|
34491
|
+
const isReactDoctorShortIdOf = (bareRuleKey, qualifiedRuleKey) => !bareRuleKey.includes("/") && qualifiedRuleKey === `react-doctor/${bareRuleKey}`;
|
|
34492
|
+
const isSameRuleKey = (candidateRuleKey, targetRuleKey) => {
|
|
34493
|
+
const canonicalCandidate = canonicalizeRuleKey(candidateRuleKey);
|
|
34494
|
+
const canonicalTarget = canonicalizeRuleKey(targetRuleKey);
|
|
34495
|
+
if (canonicalCandidate === canonicalTarget) return true;
|
|
34496
|
+
return isReactDoctorShortIdOf(canonicalCandidate, canonicalTarget) || isReactDoctorShortIdOf(canonicalTarget, canonicalCandidate);
|
|
34497
|
+
};
|
|
34479
34498
|
const getEquivalentRuleKeys = (ruleKey) => {
|
|
34480
34499
|
const nativeRuleKey = canonicalizeRuleKey(ruleKey);
|
|
34481
34500
|
return [nativeRuleKey, ...getLegacyRuleKeysForNative(nativeRuleKey)];
|
|
@@ -34485,12 +34504,182 @@ const stripDescriptionTail = (ruleList) => {
|
|
|
34485
34504
|
if (!descriptionMatch || descriptionMatch.index === void 0) return ruleList;
|
|
34486
34505
|
return ruleList.slice(0, descriptionMatch.index);
|
|
34487
34506
|
};
|
|
34488
|
-
const
|
|
34507
|
+
const tokenizeRuleList = (ruleList) => {
|
|
34489
34508
|
const trimmed = ruleList?.trim();
|
|
34490
|
-
if (!trimmed) return
|
|
34509
|
+
if (!trimmed) return [];
|
|
34491
34510
|
const ruleSection = stripDescriptionTail(trimmed).trim();
|
|
34492
|
-
if (!ruleSection) return
|
|
34493
|
-
return ruleSection.split(/[,\s]+/).
|
|
34511
|
+
if (!ruleSection) return [];
|
|
34512
|
+
return ruleSection.split(/[,\s]+/).map((token) => token.trim()).filter(Boolean);
|
|
34513
|
+
};
|
|
34514
|
+
const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
|
|
34515
|
+
const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
|
|
34516
|
+
const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
|
|
34517
|
+
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}\`.`;
|
|
34518
|
+
const tokenMisnamesRule = (token, ruleId) => token !== ruleId && isSameRuleKey(token, ruleId);
|
|
34519
|
+
const detectInlineNearMiss = (lines, diagnosticLineIndex, ruleId) => {
|
|
34520
|
+
const candidates = [{
|
|
34521
|
+
line: lines[diagnosticLineIndex],
|
|
34522
|
+
requiredScope: "line"
|
|
34523
|
+
}, {
|
|
34524
|
+
line: lines[diagnosticLineIndex - 1],
|
|
34525
|
+
requiredScope: "next-line"
|
|
34526
|
+
}];
|
|
34527
|
+
for (const { line, requiredScope } of candidates) {
|
|
34528
|
+
const match = line?.match(FOREIGN_INLINE_DISABLE_PATTERN);
|
|
34529
|
+
if (!match) continue;
|
|
34530
|
+
const [, tool, scope, ruleList] = match;
|
|
34531
|
+
if (scope !== requiredScope) continue;
|
|
34532
|
+
const tokens = tokenizeRuleList(ruleList);
|
|
34533
|
+
if (tokens.includes(ruleId)) continue;
|
|
34534
|
+
for (const token of tokens) if (tokenMisnamesRule(token, ruleId)) return buildHint(tool, token, ruleId);
|
|
34535
|
+
}
|
|
34536
|
+
return null;
|
|
34537
|
+
};
|
|
34538
|
+
const detectBlockNearMiss = (lines, diagnosticLineIndex, ruleId) => {
|
|
34539
|
+
let openMisname = null;
|
|
34540
|
+
const lastLineIndex = Math.min(diagnosticLineIndex, lines.length - 1);
|
|
34541
|
+
for (let lineIndex = 0; lineIndex <= lastLineIndex; lineIndex++) {
|
|
34542
|
+
const line = lines[lineIndex];
|
|
34543
|
+
if (line === void 0 || !line.includes("-disable") && !line.includes("-enable")) continue;
|
|
34544
|
+
const disableMatch = line.match(FOREIGN_BLOCK_DISABLE_PATTERN);
|
|
34545
|
+
if (disableMatch) {
|
|
34546
|
+
const [, tool, ruleList] = disableMatch;
|
|
34547
|
+
const tokens = tokenizeRuleList(ruleList);
|
|
34548
|
+
if (tokens.includes(ruleId)) openMisname = null;
|
|
34549
|
+
else {
|
|
34550
|
+
const misnamed = tokens.find((token) => tokenMisnamesRule(token, ruleId));
|
|
34551
|
+
if (misnamed) openMisname = {
|
|
34552
|
+
tool,
|
|
34553
|
+
token: misnamed
|
|
34554
|
+
};
|
|
34555
|
+
}
|
|
34556
|
+
continue;
|
|
34557
|
+
}
|
|
34558
|
+
const enableMatch = line.match(FOREIGN_BLOCK_ENABLE_PATTERN);
|
|
34559
|
+
if (enableMatch) {
|
|
34560
|
+
const enabledRules = tokenizeRuleList(enableMatch[1]);
|
|
34561
|
+
if (enabledRules.length === 0 || enabledRules.some((rule) => isSameRuleKey(rule, ruleId))) openMisname = null;
|
|
34562
|
+
}
|
|
34563
|
+
}
|
|
34564
|
+
return openMisname ? buildHint(openMisname.tool, openMisname.token, ruleId) : null;
|
|
34565
|
+
};
|
|
34566
|
+
const detectForeignDisableNearMiss = (lines, diagnosticLineIndex, ruleId) => {
|
|
34567
|
+
if (!ruleId.startsWith("react-doctor/")) return null;
|
|
34568
|
+
return detectInlineNearMiss(lines, diagnosticLineIndex, ruleId) ?? detectBlockNearMiss(lines, diagnosticLineIndex, ruleId);
|
|
34569
|
+
};
|
|
34570
|
+
const JSX_OPENER_TAG_PATTERN = /<[A-Za-z][\w.]*/g;
|
|
34571
|
+
const JSX_TAG_NAME_FOLLOW = /[A-Za-z]/;
|
|
34572
|
+
const isOpenerMatchInsideLineComment = (line, openerCharIndex) => {
|
|
34573
|
+
let stringDelimiter = null;
|
|
34574
|
+
for (let charIndex = 0; charIndex < openerCharIndex; charIndex++) {
|
|
34575
|
+
const character = line[charIndex];
|
|
34576
|
+
if (stringDelimiter !== null) {
|
|
34577
|
+
if (character === "\\") {
|
|
34578
|
+
charIndex++;
|
|
34579
|
+
continue;
|
|
34580
|
+
}
|
|
34581
|
+
if (character === stringDelimiter) stringDelimiter = null;
|
|
34582
|
+
continue;
|
|
34583
|
+
}
|
|
34584
|
+
if (character === "\"" || character === "'" || character === "`") {
|
|
34585
|
+
stringDelimiter = character;
|
|
34586
|
+
continue;
|
|
34587
|
+
}
|
|
34588
|
+
if (character === "/" && line[charIndex + 1] === "/") return true;
|
|
34589
|
+
}
|
|
34590
|
+
return false;
|
|
34591
|
+
};
|
|
34592
|
+
const findOpenerTagOnLine = (line) => {
|
|
34593
|
+
for (const match of line.matchAll(JSX_OPENER_TAG_PATTERN)) {
|
|
34594
|
+
if (match.index === void 0) continue;
|
|
34595
|
+
if (!isOpenerMatchInsideLineComment(line, match.index)) return { startCharIndex: match.index + match[0].length };
|
|
34596
|
+
}
|
|
34597
|
+
return null;
|
|
34598
|
+
};
|
|
34599
|
+
const findJsxOpenerSpan = (lines, openerLineIndex) => {
|
|
34600
|
+
const openerLine = lines[openerLineIndex];
|
|
34601
|
+
if (openerLine === void 0) return null;
|
|
34602
|
+
const opener = findOpenerTagOnLine(openerLine);
|
|
34603
|
+
if (!opener) return null;
|
|
34604
|
+
const lookaheadLimit = Math.min(lines.length, openerLineIndex + 32);
|
|
34605
|
+
let braceDepth = 0;
|
|
34606
|
+
let innerAngleDepth = 0;
|
|
34607
|
+
let stringDelimiter = null;
|
|
34608
|
+
for (let lineIndex = openerLineIndex; lineIndex < lookaheadLimit; lineIndex++) {
|
|
34609
|
+
const currentLine = lines[lineIndex];
|
|
34610
|
+
const startCharForLine = lineIndex === openerLineIndex ? opener.startCharIndex : 0;
|
|
34611
|
+
for (let charIndex = startCharForLine; charIndex < currentLine.length; charIndex++) {
|
|
34612
|
+
const character = currentLine[charIndex];
|
|
34613
|
+
if (stringDelimiter !== null) {
|
|
34614
|
+
if (character === "\\") {
|
|
34615
|
+
charIndex++;
|
|
34616
|
+
continue;
|
|
34617
|
+
}
|
|
34618
|
+
if (character === stringDelimiter) stringDelimiter = null;
|
|
34619
|
+
continue;
|
|
34620
|
+
}
|
|
34621
|
+
if (character === "\"" || character === "'" || character === "`") {
|
|
34622
|
+
stringDelimiter = character;
|
|
34623
|
+
continue;
|
|
34624
|
+
}
|
|
34625
|
+
if (character === "{") {
|
|
34626
|
+
braceDepth++;
|
|
34627
|
+
continue;
|
|
34628
|
+
}
|
|
34629
|
+
if (character === "}") {
|
|
34630
|
+
braceDepth--;
|
|
34631
|
+
continue;
|
|
34632
|
+
}
|
|
34633
|
+
if (braceDepth !== 0) continue;
|
|
34634
|
+
if (character === "<") {
|
|
34635
|
+
const followCharacter = currentLine[charIndex + 1];
|
|
34636
|
+
if (followCharacter !== void 0 && JSX_TAG_NAME_FOLLOW.test(followCharacter)) innerAngleDepth++;
|
|
34637
|
+
continue;
|
|
34638
|
+
}
|
|
34639
|
+
if (character !== ">") continue;
|
|
34640
|
+
const previousCharacter = currentLine[charIndex - 1];
|
|
34641
|
+
const nextCharacter = currentLine[charIndex + 1];
|
|
34642
|
+
if (previousCharacter === "=" || nextCharacter === "=") continue;
|
|
34643
|
+
if (innerAngleDepth > 0) {
|
|
34644
|
+
innerAngleDepth--;
|
|
34645
|
+
continue;
|
|
34646
|
+
}
|
|
34647
|
+
return lineIndex;
|
|
34648
|
+
}
|
|
34649
|
+
}
|
|
34650
|
+
return null;
|
|
34651
|
+
};
|
|
34652
|
+
const findEnclosingMultilineJsxOpenerStart = (lines, diagnosticLineIndex) => {
|
|
34653
|
+
for (let candidateIndex = diagnosticLineIndex - 1; candidateIndex >= 0 && diagnosticLineIndex - candidateIndex <= 32; candidateIndex--) {
|
|
34654
|
+
const openerCloseIndex = findJsxOpenerSpan(lines, candidateIndex);
|
|
34655
|
+
if (openerCloseIndex !== null && openerCloseIndex >= diagnosticLineIndex) return candidateIndex;
|
|
34656
|
+
}
|
|
34657
|
+
return null;
|
|
34658
|
+
};
|
|
34659
|
+
const DISABLE_NEXT_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-next-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
|
|
34660
|
+
const findStackedDisableCommentsAbove = (lines, anchorIndex) => {
|
|
34661
|
+
const collected = [];
|
|
34662
|
+
let isStillInChain = true;
|
|
34663
|
+
for (let candidateIndex = anchorIndex - 1; candidateIndex >= 0 && anchorIndex - candidateIndex <= 10; candidateIndex--) {
|
|
34664
|
+
const candidateLine = lines[candidateIndex];
|
|
34665
|
+
if (candidateLine === void 0) break;
|
|
34666
|
+
const match = candidateLine.match(DISABLE_NEXT_LINE_PATTERN);
|
|
34667
|
+
if (match) {
|
|
34668
|
+
collected.push({
|
|
34669
|
+
commentLineIndex: candidateIndex,
|
|
34670
|
+
ruleList: match[1],
|
|
34671
|
+
isInChain: isStillInChain
|
|
34672
|
+
});
|
|
34673
|
+
continue;
|
|
34674
|
+
}
|
|
34675
|
+
isStillInChain = false;
|
|
34676
|
+
}
|
|
34677
|
+
return collected;
|
|
34678
|
+
};
|
|
34679
|
+
const isRuleListedInComment = (ruleList, ruleId) => {
|
|
34680
|
+
const tokens = tokenizeRuleList(ruleList);
|
|
34681
|
+
if (tokens.length === 0) return true;
|
|
34682
|
+
return tokens.some((token) => isSameRuleKey(token, ruleId));
|
|
34494
34683
|
};
|
|
34495
34684
|
const DISABLE_LINE_PATTERN = /(?:\/\/|\/\*)\s*react-doctor-disable-line\b(?:\s+([^\r\n]*?))?\s*(?:\*\/)?\s*\}?\s*$/;
|
|
34496
34685
|
const formatLineGap = (gapLineCount) => `${gapLineCount} line${gapLineCount === 1 ? "" : "s"}`;
|
|
@@ -34534,7 +34723,7 @@ const evaluateSuppression = (lines, diagnosticLineIndex, ruleId) => {
|
|
|
34534
34723
|
};
|
|
34535
34724
|
return {
|
|
34536
34725
|
isSuppressed: false,
|
|
34537
|
-
nearMissHint: classifyFromComments([directComments, openerComments], diagnosticLineIndex, ruleId)
|
|
34726
|
+
nearMissHint: classifyFromComments([directComments, openerComments], diagnosticLineIndex, ruleId) ?? detectForeignDisableNearMiss(lines, diagnosticLineIndex, ruleId)
|
|
34538
34727
|
};
|
|
34539
34728
|
};
|
|
34540
34729
|
/**
|
|
@@ -34964,6 +35153,11 @@ var OxlintBatchExceeded = class extends TaggedErrorClass()("OxlintBatchExceeded"
|
|
|
34964
35153
|
}
|
|
34965
35154
|
}
|
|
34966
35155
|
};
|
|
35156
|
+
var ScanDeadlineExceeded = class extends TaggedErrorClass()("ScanDeadlineExceeded", { detail: String$1 }) {
|
|
35157
|
+
get message() {
|
|
35158
|
+
return `Scan exceeded its overall time budget: ${this.detail}`;
|
|
35159
|
+
}
|
|
35160
|
+
};
|
|
34967
35161
|
var OxlintSpawnFailed = class extends TaggedErrorClass()("OxlintSpawnFailed", { cause: Unknown }) {
|
|
34968
35162
|
get message() {
|
|
34969
35163
|
return `Failed to run oxlint: ${pretty(fail$6(this.cause))}`;
|
|
@@ -35027,6 +35221,7 @@ var GitBaseBranchInvalid = class extends TaggedErrorClass()("GitBaseBranchInvali
|
|
|
35027
35221
|
const ReactDoctorErrorReason = Union([
|
|
35028
35222
|
OxlintUnavailable,
|
|
35029
35223
|
OxlintBatchExceeded,
|
|
35224
|
+
ScanDeadlineExceeded,
|
|
35030
35225
|
OxlintSpawnFailed,
|
|
35031
35226
|
OxlintOutputUnparseable,
|
|
35032
35227
|
ConfigParseFailed,
|
|
@@ -35099,15 +35294,105 @@ const layerOtlp = unwrap$3(gen(function* () {
|
|
|
35099
35294
|
}).pipe(provide$2(layer$8));
|
|
35100
35295
|
}).pipe(orDie));
|
|
35101
35296
|
/**
|
|
35102
|
-
*
|
|
35103
|
-
* `
|
|
35104
|
-
|
|
35297
|
+
* Read a positive-millisecond timeout from an env var, falling back to
|
|
35298
|
+
* `defaultMs` when the var is unset, non-finite, or not strictly positive.
|
|
35299
|
+
*/
|
|
35300
|
+
const readPositiveEnvMs = (envVarName, defaultMs) => {
|
|
35301
|
+
const rawValue = process.env[envVarName];
|
|
35302
|
+
if (rawValue === void 0) return defaultMs;
|
|
35303
|
+
const parsedValue = Number(rawValue);
|
|
35304
|
+
if (!Number.isFinite(parsedValue) || parsedValue <= 0) return defaultMs;
|
|
35305
|
+
return parsedValue;
|
|
35306
|
+
};
|
|
35307
|
+
const CGROUP_V2_MEMORY_MAX_PATH = "/sys/fs/cgroup/memory.max";
|
|
35308
|
+
const CGROUP_V1_MEMORY_LIMIT_PATH = "/sys/fs/cgroup/memory/memory.limit_in_bytes";
|
|
35309
|
+
const CGROUP_UNLIMITED_SENTINEL_BYTES = Number.MAX_SAFE_INTEGER;
|
|
35310
|
+
/**
|
|
35311
|
+
* Parses one raw cgroup memory-limit file value into a positive byte count, or
|
|
35312
|
+
* `undefined` when it represents "no limit" (the v2 `"max"` literal, an empty
|
|
35313
|
+
* read, a non-positive / non-finite value, or v1's near-2^63 unlimited
|
|
35314
|
+
* sentinel). Pure and exported so the classification is unit-testable without
|
|
35315
|
+
* touching the filesystem.
|
|
35316
|
+
*/
|
|
35317
|
+
const parseCgroupMemoryLimitBytes = (raw) => {
|
|
35318
|
+
if (raw === void 0) return void 0;
|
|
35319
|
+
const trimmed = raw.trim();
|
|
35320
|
+
if (trimmed === "" || trimmed === "max") return void 0;
|
|
35321
|
+
const parsed = Number(trimmed);
|
|
35322
|
+
if (!Number.isFinite(parsed) || parsed <= 0 || parsed >= CGROUP_UNLIMITED_SENTINEL_BYTES) return;
|
|
35323
|
+
return parsed;
|
|
35324
|
+
};
|
|
35325
|
+
const CGROUP_MEMORY_LIMIT_PATHS = [CGROUP_V2_MEMORY_MAX_PATH, CGROUP_V1_MEMORY_LIMIT_PATH];
|
|
35326
|
+
/**
|
|
35327
|
+
* Reads this process's cgroup memory limit in bytes from the first candidate
|
|
35328
|
+
* path that yields a real limit, or `undefined` when none does — no cgroup, no
|
|
35329
|
+
* limit, or the files are unreadable (e.g. macOS / Windows dev machines).
|
|
35330
|
+
* `os.totalmem()` reports the HOST total and ignores cgroup memory limits, so a
|
|
35331
|
+
* memory-constrained container over-reports total memory; `resolveAutoScan-
|
|
35332
|
+
* Concurrency` takes `min(totalmem, this)` to honor the limit.
|
|
35333
|
+
*
|
|
35334
|
+
* The cgroup v2 read is the mount-root `memory.max`, which IS the container's
|
|
35335
|
+
* limit under the standard cgroup-namespace setup CI runners use (the
|
|
35336
|
+
* container's own cgroup is the root of its namespaced view). A process in a
|
|
35337
|
+
* non-namespaced nested/delegated cgroup whose root reads `"max"` is not
|
|
35338
|
+
* detected here and falls back to the host total; the EAGAIN/ENOMEM serial
|
|
35339
|
+
* replay in `spawnLintBatches` remains the runtime backstop for that case.
|
|
35340
|
+
*
|
|
35341
|
+
* `candidatePaths` is injectable so tests exercise the v2-wins-over-v1
|
|
35342
|
+
* precedence, the skip-unreadable fallback, and the all-missing case without a
|
|
35343
|
+
* real `/sys/fs/cgroup`.
|
|
35344
|
+
*/
|
|
35345
|
+
const readCgroupMemoryLimitBytes = (candidatePaths = CGROUP_MEMORY_LIMIT_PATHS) => {
|
|
35346
|
+
for (const limitPath of candidatePaths) {
|
|
35347
|
+
let raw;
|
|
35348
|
+
try {
|
|
35349
|
+
raw = fs.readFileSync(limitPath, "utf8");
|
|
35350
|
+
} catch {
|
|
35351
|
+
continue;
|
|
35352
|
+
}
|
|
35353
|
+
const limitBytes = parseCgroupMemoryLimitBytes(raw);
|
|
35354
|
+
if (limitBytes !== void 0) return limitBytes;
|
|
35355
|
+
}
|
|
35356
|
+
};
|
|
35357
|
+
/**
|
|
35358
|
+
* Clamps a requested lint worker count to `[MIN_SCAN_CONCURRENCY,
|
|
35359
|
+
* HARD_MAX_SCAN_CONCURRENCY]` as a finite integer. This is the explicit-pin and
|
|
35360
|
+
* spawn-boundary clamp — the memory-and-core-budgeted auto count comes from
|
|
35361
|
+
* `resolveAutoScanConcurrency`. Out-of-range or non-finite requests degrade to
|
|
35105
35362
|
* `MIN_SCAN_CONCURRENCY` rather than oversubscribing or running zero workers.
|
|
35106
35363
|
*/
|
|
35107
35364
|
const resolveScanConcurrency = (requested) => {
|
|
35108
|
-
|
|
35109
|
-
|
|
35110
|
-
|
|
35365
|
+
if (!Number.isFinite(requested) || requested < 1) return 1;
|
|
35366
|
+
return Math.min(Math.floor(requested), 32);
|
|
35367
|
+
};
|
|
35368
|
+
const readSystemFacts = () => ({
|
|
35369
|
+
availableCores: os.availableParallelism(),
|
|
35370
|
+
totalMemoryBytes: os.totalmem(),
|
|
35371
|
+
cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
|
|
35372
|
+
});
|
|
35373
|
+
/**
|
|
35374
|
+
* Auto lint-worker count: the smaller of the (cgroup-CPU-aware) core count and
|
|
35375
|
+
* the number of `PER_WORKER_MEM_BUDGET_BYTES` workers that fit in available
|
|
35376
|
+
* memory, then clamped to `[MIN, HARD_MAX]` by `resolveScanConcurrency`.
|
|
35377
|
+
*
|
|
35378
|
+
* `os.availableParallelism()` already respects cgroup CPU quotas, so the core
|
|
35379
|
+
* term needs no help. Available memory is `os.totalmem()` floored by the cgroup
|
|
35380
|
+
* memory limit — `os.freemem()` is deliberately NOT used: it excludes
|
|
35381
|
+
* reclaimable page cache and reads near-zero on macOS / cache-heavy Linux, which
|
|
35382
|
+
* would collapse the auto path to a single worker. `os.totalmem()` reports the
|
|
35383
|
+
* host total even inside a container, so the cgroup limit (read directly,
|
|
35384
|
+
* because Node doesn't fold it into `totalmem()`) is the real ceiling there.
|
|
35385
|
+
*
|
|
35386
|
+
* `facts` is injectable so tests exercise core-bound, memory-bound, cgroup-
|
|
35387
|
+
* limited, and ceiling cases without mocking `os` or the filesystem.
|
|
35388
|
+
*/
|
|
35389
|
+
const resolveAutoScanConcurrency = (facts = readSystemFacts()) => {
|
|
35390
|
+
const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
|
|
35391
|
+
const memoryBoundedWorkers = Math.floor(availableMemoryBytes / PER_WORKER_MEM_BUDGET_BYTES);
|
|
35392
|
+
return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
|
|
35393
|
+
};
|
|
35394
|
+
const resolveLintBatchOrdering = () => {
|
|
35395
|
+
return process.env["REACT_DOCTOR_LINT_BATCH_ORDERING"]?.trim().toLowerCase() === "cost" ? "cost" : "arrival";
|
|
35111
35396
|
};
|
|
35112
35397
|
/**
|
|
35113
35398
|
* Per-batch oxlint wall-clock budget. Reads from the env var on
|
|
@@ -35115,11 +35400,38 @@ const resolveScanConcurrency = (requested) => {
|
|
|
35115
35400
|
* microVMs without recompiling react-doctor. Tests override via
|
|
35116
35401
|
* `Layer.succeed(OxlintSpawnTimeoutMs, ...)`.
|
|
35117
35402
|
*/
|
|
35118
|
-
var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => {
|
|
35119
|
-
|
|
35120
|
-
|
|
35403
|
+
var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS", OXLINT_SPAWN_TIMEOUT_MS) }) {};
|
|
35404
|
+
/**
|
|
35405
|
+
* Effect-side cap on the lint phase. The env var lets CI / eval runners
|
|
35406
|
+
* raise the phase budget for slow large repos without recompiling.
|
|
35407
|
+
* Tests override via `Layer.succeed(LintPhaseTimeoutMs, ...)`.
|
|
35408
|
+
*/
|
|
35409
|
+
var LintPhaseTimeoutMs = class extends Reference("react-doctor/LintPhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_LINT_PHASE_TIMEOUT_MS", LINT_PHASE_TIMEOUT_MS) }) {};
|
|
35410
|
+
/**
|
|
35411
|
+
* Effect-side cap on the dead-code phase, sitting above the in-worker
|
|
35412
|
+
* timeout as a runtime-independent backstop. The env var raises it for
|
|
35413
|
+
* type-heavy projects; tests override via
|
|
35414
|
+
* `Layer.succeed(DeadCodePhaseTimeoutMs, ...)`.
|
|
35415
|
+
*/
|
|
35416
|
+
var DeadCodePhaseTimeoutMs = class extends Reference("react-doctor/DeadCodePhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_DEAD_CODE_PHASE_TIMEOUT_MS", DEAD_CODE_PHASE_TIMEOUT_MS) }) {};
|
|
35417
|
+
/**
|
|
35418
|
+
* Overall scan deadline backstop, bounding everything the per-phase
|
|
35419
|
+
* timeouts don't (wedged git / IO). The env var raises it for very
|
|
35420
|
+
* large repos; tests override via `Layer.succeed(ScanDeadlineMs, ...)`.
|
|
35421
|
+
*/
|
|
35422
|
+
var ScanDeadlineMs = class extends Reference("react-doctor/ScanDeadlineMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_SCAN_DEADLINE_MS", SCAN_TOTAL_DEADLINE_MS) }) {};
|
|
35423
|
+
/**
|
|
35424
|
+
* Wall-clock budget for the supply-chain check when it runs on a background
|
|
35425
|
+
* fiber overlapping the lint pass. Reads from the env var on startup so the
|
|
35426
|
+
* eval harness can raise the budget under sandbox microVMs (slower network)
|
|
35427
|
+
* without recompiling react-doctor. Tests override via
|
|
35428
|
+
* `Layer.succeed(SupplyChainOverlapTimeoutMs, ...)`.
|
|
35429
|
+
*/
|
|
35430
|
+
var SupplyChainOverlapTimeoutMs = class extends Reference("react-doctor/SupplyChainOverlapTimeoutMs", { defaultValue: () => {
|
|
35431
|
+
const raw = process.env["REACT_DOCTOR_SUPPLY_CHAIN_TIMEOUT_MS"];
|
|
35432
|
+
if (raw === void 0) return SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS;
|
|
35121
35433
|
const parsed = Number(raw);
|
|
35122
|
-
if (!Number.isFinite(parsed) || parsed <= 0) return
|
|
35434
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS;
|
|
35123
35435
|
return parsed;
|
|
35124
35436
|
} }) {};
|
|
35125
35437
|
/**
|
|
@@ -35130,31 +35442,93 @@ var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTime
|
|
|
35130
35442
|
*/
|
|
35131
35443
|
var OxlintOutputMaxBytes = class extends Reference("react-doctor/OxlintOutputMaxBytes", { defaultValue: () => OXLINT_OUTPUT_MAX_BYTES }) {};
|
|
35132
35444
|
/**
|
|
35133
|
-
* Number of oxlint subprocesses the lint pass runs in parallel. Defaults
|
|
35134
|
-
*
|
|
35135
|
-
* the box
|
|
35136
|
-
*
|
|
35137
|
-
*
|
|
35138
|
-
*
|
|
35139
|
-
*
|
|
35140
|
-
*
|
|
35141
|
-
*
|
|
35142
|
-
*
|
|
35445
|
+
* Number of oxlint subprocesses the lint pass runs in parallel. Defaults to a
|
|
35446
|
+
* memory-and-core-budgeted auto count (`resolveAutoScanConcurrency`) so large
|
|
35447
|
+
* repos scan fast out of the box without OOMing the native binding on a
|
|
35448
|
+
* high-core / low-memory box; `spawnLintBatches` transparently falls back to a
|
|
35449
|
+
* single worker if a parallel run still exhausts system resources. The CLI's
|
|
35450
|
+
* `--no-parallel` flag forces serial via `Layer.succeed`; the
|
|
35451
|
+
* `REACT_DOCTOR_PARALLEL` env var seeds the default for programmatic / CI
|
|
35452
|
+
* callers that never touch the flag — parallelism is opt-OUT, so only the
|
|
35453
|
+
* explicit serial values pin one worker:
|
|
35454
|
+
*
|
|
35455
|
+
* - unset / `auto` / `true` / `on` → memory-and-core-budgeted auto count
|
|
35143
35456
|
* - `0` / `false` / `off` → `1` (serial)
|
|
35144
35457
|
* - a positive integer → that many workers (clamped)
|
|
35145
|
-
* - any other value →
|
|
35458
|
+
* - any other value → memory-and-core-budgeted auto count
|
|
35146
35459
|
*
|
|
35147
35460
|
* The resolved value is always within
|
|
35148
|
-
* `[MIN_SCAN_CONCURRENCY,
|
|
35461
|
+
* `[MIN_SCAN_CONCURRENCY, HARD_MAX_SCAN_CONCURRENCY]`.
|
|
35149
35462
|
*/
|
|
35150
35463
|
var OxlintConcurrency = class extends Reference("react-doctor/OxlintConcurrency", { defaultValue: () => {
|
|
35151
35464
|
const raw = process.env["REACT_DOCTOR_PARALLEL"];
|
|
35152
|
-
if (raw === void 0) return
|
|
35465
|
+
if (raw === void 0) return resolveAutoScanConcurrency();
|
|
35153
35466
|
const normalized = raw.trim().toLowerCase();
|
|
35154
35467
|
if (normalized === "0" || normalized === "false" || normalized === "off") return 1;
|
|
35155
35468
|
const parsed = Number.parseInt(normalized, 10);
|
|
35156
35469
|
if (Number.isInteger(parsed) && parsed > 0) return resolveScanConcurrency(parsed);
|
|
35157
|
-
return
|
|
35470
|
+
return resolveAutoScanConcurrency();
|
|
35471
|
+
} }) {};
|
|
35472
|
+
/**
|
|
35473
|
+
* Three-state control for overlapping the dead-code pass with the lint pass —
|
|
35474
|
+
* forking dead-code as a child fiber that runs DURING lint instead of strictly
|
|
35475
|
+
* after it.
|
|
35476
|
+
*
|
|
35477
|
+
* - `"auto"` (default) / `"off"` → strictly SEQUENTIAL: dead-code runs after
|
|
35478
|
+
* lint with the full core budget. Both deslop's parse pool and the oxlint
|
|
35479
|
+
* pool are CPU-bound and each size themselves to all cores, so overlapping
|
|
35480
|
+
* them only oversubscribes (~2x the cores) and starves the parse pass past
|
|
35481
|
+
* its timeout — for no wall-clock win, since there are no spare cores to
|
|
35482
|
+
* absorb the second pass. Sequential is both faster per-phase and safe.
|
|
35483
|
+
* - `"on"` → force the overlap anyway. The orchestrator then SPLITS the core
|
|
35484
|
+
* budget (`DEAD_CODE_OVERLAP_PARSE_SHARE`): deslop's parse pool is capped
|
|
35485
|
+
* and lint shrinks to the remainder, so the two sum to the cores instead of
|
|
35486
|
+
* doubling them, and the dead-code timeout scales up for the reduced share.
|
|
35487
|
+
*
|
|
35488
|
+
* Seeded from `REACT_DOCTOR_DEAD_CODE_OVERLAP` so operators get a redeploy-free
|
|
35489
|
+
* switch; tests pin it via `Layer.succeed(DeadCodeOverlap, ...)`.
|
|
35490
|
+
*/
|
|
35491
|
+
var DeadCodeOverlap = class extends Reference("react-doctor/DeadCodeOverlap", { defaultValue: () => {
|
|
35492
|
+
const raw = process.env["REACT_DOCTOR_DEAD_CODE_OVERLAP"]?.trim().toLowerCase();
|
|
35493
|
+
if (raw === "on" || raw === "true" || raw === "1") return "on";
|
|
35494
|
+
if (raw === "off" || raw === "false" || raw === "0") return "off";
|
|
35495
|
+
return "auto";
|
|
35496
|
+
} }) {};
|
|
35497
|
+
/**
|
|
35498
|
+
* How the full-scan lint pass orders its file batches. `"arrival"` (the
|
|
35499
|
+
* default) keeps `git ls-files` discovery order. `"cost"` opts into LPT (feed
|
|
35500
|
+
* the largest files first); set `REACT_DOCTOR_LINT_BATCH_ORDERING=cost`. NOTE:
|
|
35501
|
+
* `cost` is OFF by default because the current sort-desc-then-chunk-100 packs
|
|
35502
|
+
* the heaviest files into one wave-1 batch — on size-skewed repos that mega-
|
|
35503
|
+
* batch is a straggler (and can trip the per-batch timeout + split), measurably
|
|
35504
|
+
* regressing the common full-scan case. LPT needs the heavy files SPREAD across
|
|
35505
|
+
* batches before `cost` earns the default. Tests override via
|
|
35506
|
+
* `Layer.succeed(LintBatchOrdering, ...)`. Diff / staged scans never reach this
|
|
35507
|
+
* — they pass user-scoped `includePaths` that skip discovery and stay in
|
|
35508
|
+
* arrival order; only the full-scan branch reads it.
|
|
35509
|
+
*/
|
|
35510
|
+
var LintBatchOrdering = class extends Reference("react-doctor/LintBatchOrdering", { defaultValue: resolveLintBatchOrdering }) {};
|
|
35511
|
+
const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
|
|
35512
|
+
/**
|
|
35513
|
+
* Whether the per-file lint cache (`runners/oxlint/file-lint-cache.ts`) is
|
|
35514
|
+
* active. Defaults ON — repeat scans re-lint only the files whose content
|
|
35515
|
+
* changed, and correctness is guaranteed byte-identical to a cold scan by the
|
|
35516
|
+
* always-fresh cross-file sidecar. Opt-OUT, two knobs (matching the whole-repo
|
|
35517
|
+
* scan cache's `REACT_DOCTOR_NO_CACHE`):
|
|
35518
|
+
*
|
|
35519
|
+
* - `REACT_DOCTOR_NO_CACHE` — the global off-switch; disables BOTH the
|
|
35520
|
+
* whole-repo scan cache and this per-file cache.
|
|
35521
|
+
* - `REACT_DOCTOR_NO_FILE_CACHE` — granular: bust only the per-file cache
|
|
35522
|
+
* while keeping the whole-repo short-circuit.
|
|
35523
|
+
*
|
|
35524
|
+
* Tests override via `Layer.succeed(PerFileLintCacheEnabled, false)`.
|
|
35525
|
+
*/
|
|
35526
|
+
var PerFileLintCacheEnabled = class extends Reference("react-doctor/PerFileLintCacheEnabled", { defaultValue: () => {
|
|
35527
|
+
const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
|
|
35528
|
+
const noFileCache = process.env["REACT_DOCTOR_NO_FILE_CACHE"]?.toLowerCase() ?? "";
|
|
35529
|
+
if (CACHE_DISABLED_VALUES.has(noCache)) return false;
|
|
35530
|
+
if (CACHE_DISABLED_VALUES.has(noFileCache)) return false;
|
|
35531
|
+
return true;
|
|
35158
35532
|
} }) {};
|
|
35159
35533
|
const DIAGNOSTIC_SURFACES = [
|
|
35160
35534
|
"cli",
|
|
@@ -35328,7 +35702,6 @@ const PACKAGE_JSON_FILENAME = "package.json";
|
|
|
35328
35702
|
const PACKAGE_JSON_CONFIG_KEY = "reactDoctor";
|
|
35329
35703
|
const LEGACY_CONFIG_FILENAME = "react-doctor.config.json";
|
|
35330
35704
|
const jiti = createJiti(import.meta.url);
|
|
35331
|
-
const formatError = (error) => error instanceof Error ? error.message : String(error);
|
|
35332
35705
|
const importDefaultExport = async (jitiInstance, filePath) => {
|
|
35333
35706
|
const imported = await jitiInstance.import(filePath);
|
|
35334
35707
|
return imported?.default ?? imported;
|
|
@@ -35360,7 +35733,7 @@ const loadModuleConfig = async (filePath) => {
|
|
|
35360
35733
|
try {
|
|
35361
35734
|
return await importDefaultExport(aliasJiti, filePath);
|
|
35362
35735
|
} catch (retryError) {
|
|
35363
|
-
throw new Error(`${
|
|
35736
|
+
throw new Error(`${messageFromUnknown(error)} (retry with ${SELF_PACKAGE_IMPORT_SPECIFIER} aliased to the running react-doctor package also failed: ${messageFromUnknown(retryError)})`, { cause: retryError });
|
|
35364
35737
|
}
|
|
35365
35738
|
}
|
|
35366
35739
|
};
|
|
@@ -35409,7 +35782,7 @@ const loadLegacyConfig = (directory) => {
|
|
|
35409
35782
|
}
|
|
35410
35783
|
warn(`${LEGACY_CONFIG_FILENAME} must contain an object, ignoring.`);
|
|
35411
35784
|
} catch (error) {
|
|
35412
|
-
warn(`Failed to load ${LEGACY_CONFIG_FILENAME}: ${
|
|
35785
|
+
warn(`Failed to load ${LEGACY_CONFIG_FILENAME}: ${messageFromUnknown(error)}`);
|
|
35413
35786
|
}
|
|
35414
35787
|
return {
|
|
35415
35788
|
status: "invalid",
|
|
@@ -35436,7 +35809,7 @@ const loadConfigFromDirectory = async (directory) => {
|
|
|
35436
35809
|
warn(`${CONFIG_BASENAME}.${extension} must export an object, ignoring.`);
|
|
35437
35810
|
sawBrokenConfigFile = true;
|
|
35438
35811
|
} catch (error) {
|
|
35439
|
-
warn(`Failed to load ${CONFIG_BASENAME}.${extension}: ${
|
|
35812
|
+
warn(`Failed to load ${CONFIG_BASENAME}.${extension}: ${messageFromUnknown(error)}`);
|
|
35440
35813
|
sawBrokenConfigFile = true;
|
|
35441
35814
|
}
|
|
35442
35815
|
}
|
|
@@ -35538,6 +35911,31 @@ const resolveScanTarget = async (requestedDirectory, options = {}) => {
|
|
|
35538
35911
|
didRedirectViaRootDir: redirectedDirectory !== null
|
|
35539
35912
|
};
|
|
35540
35913
|
};
|
|
35914
|
+
const buildFixGroupId = (diagnostic) => createHash("sha1").update(JSON.stringify([
|
|
35915
|
+
diagnostic.filePath,
|
|
35916
|
+
`${diagnostic.plugin}/${diagnostic.rule}`,
|
|
35917
|
+
diagnostic.message
|
|
35918
|
+
])).digest("hex").slice(0, 16);
|
|
35919
|
+
const isGroupableRule = (diagnostic) => ROOT_CAUSE_GROUPABLE_RULE_KEYS.has(`${diagnostic.plugin}/${diagnostic.rule}`);
|
|
35920
|
+
const assignFixGroups = (diagnostics) => {
|
|
35921
|
+
const siteCountByGroupId = /* @__PURE__ */ new Map();
|
|
35922
|
+
for (const diagnostic of diagnostics) {
|
|
35923
|
+
if (!isGroupableRule(diagnostic)) continue;
|
|
35924
|
+
const groupId = buildFixGroupId(diagnostic);
|
|
35925
|
+
siteCountByGroupId.set(groupId, (siteCountByGroupId.get(groupId) ?? 0) + 1);
|
|
35926
|
+
}
|
|
35927
|
+
return diagnostics.map((diagnostic) => {
|
|
35928
|
+
if (!isGroupableRule(diagnostic)) return diagnostic;
|
|
35929
|
+
const groupId = buildFixGroupId(diagnostic);
|
|
35930
|
+
if ((siteCountByGroupId.get(groupId) ?? 0) < 2) return diagnostic;
|
|
35931
|
+
return {
|
|
35932
|
+
...diagnostic,
|
|
35933
|
+
fixGroupId: groupId
|
|
35934
|
+
};
|
|
35935
|
+
});
|
|
35936
|
+
};
|
|
35937
|
+
const compareStrings = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
35938
|
+
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));
|
|
35541
35939
|
const getDirectDependencyNames = (packageJson) => new Set([...Object.keys(packageJson.dependencies ?? {}), ...Object.keys(packageJson.devDependencies ?? {})]);
|
|
35542
35940
|
const buildExpoCheckContext = (rootDirectory, expoVersion) => {
|
|
35543
35941
|
const packageJson = readPackageJson(Path.join(rootDirectory, "package.json"));
|
|
@@ -36044,10 +36442,15 @@ const buildHardeningDiagnostic = (input) => ({
|
|
|
36044
36442
|
column: input.column ?? 0,
|
|
36045
36443
|
category: "Security"
|
|
36046
36444
|
});
|
|
36047
|
-
const checkPnpmHardening = (
|
|
36048
|
-
if (!isPnpmManagedProject(
|
|
36049
|
-
const workspacePath = Path.join(
|
|
36050
|
-
const
|
|
36445
|
+
const checkPnpmHardening = (scanDirectory) => {
|
|
36446
|
+
if (!isPnpmManagedProject(scanDirectory)) return [];
|
|
36447
|
+
const workspacePath = Path.join(scanDirectory, PNPM_WORKSPACE_FILE);
|
|
36448
|
+
const hasWorkspaceFile = isFile(workspacePath);
|
|
36449
|
+
if (!hasWorkspaceFile) {
|
|
36450
|
+
const monorepoRoot = findMonorepoRoot(scanDirectory);
|
|
36451
|
+
if (monorepoRoot !== null && isFile(Path.join(monorepoRoot, PNPM_WORKSPACE_FILE))) return [];
|
|
36452
|
+
}
|
|
36453
|
+
const settings = parseHardeningSettings(hasWorkspaceFile ? NFS.readFileSync(workspacePath, "utf-8") : "");
|
|
36051
36454
|
const diagnostics = [];
|
|
36052
36455
|
if (settings.minimumReleaseAge === null) diagnostics.push(buildHardeningDiagnostic({
|
|
36053
36456
|
message: "pnpm-workspace.yaml is missing `minimumReleaseAge` — newly published versions can ship malware that gets caught and unpublished within hours",
|
|
@@ -36664,7 +37067,7 @@ const readIgnoreFile = (filePath) => {
|
|
|
36664
37067
|
try {
|
|
36665
37068
|
content = NFS.readFileSync(filePath, "utf-8");
|
|
36666
37069
|
} catch (error) {
|
|
36667
|
-
const errnoCode = error
|
|
37070
|
+
const errnoCode = isErrnoException(error) ? error.code : void 0;
|
|
36668
37071
|
if (errnoCode && errnoCode !== "ENOENT") runSync(warn$1(`Could not read ignore file ${filePath}: ${errnoCode}`));
|
|
36669
37072
|
return [];
|
|
36670
37073
|
}
|
|
@@ -36705,8 +37108,8 @@ const collectIgnorePatterns = (rootDirectory) => {
|
|
|
36705
37108
|
cachedPatternsByRoot.set(rootDirectory, patterns);
|
|
36706
37109
|
return patterns;
|
|
36707
37110
|
};
|
|
37111
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
36708
37112
|
const KNIP_JSON_FILENAME = "knip.json";
|
|
36709
|
-
const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
36710
37113
|
const readJsonFileSafe = (filePath) => {
|
|
36711
37114
|
let rawContents;
|
|
36712
37115
|
try {
|
|
@@ -36722,10 +37125,10 @@ const readJsonFileSafe = (filePath) => {
|
|
|
36722
37125
|
};
|
|
36723
37126
|
const readKnipConfig = (rootDirectory) => {
|
|
36724
37127
|
const knipJson = readJsonFileSafe(path.join(rootDirectory, KNIP_JSON_FILENAME));
|
|
36725
|
-
if (isRecord
|
|
37128
|
+
if (isRecord(knipJson)) return knipJson;
|
|
36726
37129
|
const packageJson = readJsonFileSafe(path.join(rootDirectory, "package.json"));
|
|
36727
|
-
const packageKnipConfig = isRecord
|
|
36728
|
-
return isRecord
|
|
37130
|
+
const packageKnipConfig = isRecord(packageJson) ? packageJson.knip : null;
|
|
37131
|
+
return isRecord(packageKnipConfig) ? packageKnipConfig : null;
|
|
36729
37132
|
};
|
|
36730
37133
|
const normalizePatternList = (value) => {
|
|
36731
37134
|
if (typeof value === "string" && value.length > 0) return [value];
|
|
@@ -36737,10 +37140,10 @@ const prefixWorkspacePatterns = (workspacePattern, patterns) => {
|
|
|
36737
37140
|
return patterns.map((pattern) => pattern.startsWith("!") ? `!${normalizedWorkspacePattern}/${pattern.slice(1)}` : `${normalizedWorkspacePattern}/${pattern}`);
|
|
36738
37141
|
};
|
|
36739
37142
|
const collectKnipWorkspacePatterns = (workspaces, settingName) => {
|
|
36740
|
-
if (!isRecord
|
|
37143
|
+
if (!isRecord(workspaces)) return [];
|
|
36741
37144
|
const patterns = [];
|
|
36742
37145
|
for (const [workspacePattern, workspaceConfig] of Object.entries(workspaces)) {
|
|
36743
|
-
if (!isRecord
|
|
37146
|
+
if (!isRecord(workspaceConfig)) continue;
|
|
36744
37147
|
patterns.push(...prefixWorkspacePatterns(workspacePattern, normalizePatternList(workspaceConfig[settingName])));
|
|
36745
37148
|
}
|
|
36746
37149
|
return patterns;
|
|
@@ -36750,12 +37153,11 @@ const collectKnipPatterns = (rootDirectory, settingName) => {
|
|
|
36750
37153
|
if (!config) return [];
|
|
36751
37154
|
return [...normalizePatternList(config[settingName]), ...collectKnipWorkspacePatterns(config.workspaces, settingName)];
|
|
36752
37155
|
};
|
|
36753
|
-
const collectDeadCodeIgnorePatterns = (rootDirectory
|
|
37156
|
+
const collectDeadCodeIgnorePatterns = (rootDirectory) => {
|
|
36754
37157
|
const seen = /* @__PURE__ */ new Set();
|
|
36755
37158
|
const sources = [
|
|
36756
37159
|
readIgnoreFile(path.join(rootDirectory, ".gitignore")),
|
|
36757
37160
|
collectIgnorePatterns(rootDirectory),
|
|
36758
|
-
userConfig?.ignore?.files ?? [],
|
|
36759
37161
|
collectKnipPatterns(rootDirectory, "ignore")
|
|
36760
37162
|
];
|
|
36761
37163
|
for (const source of sources) for (const pattern of source) seen.add(pattern);
|
|
@@ -36786,8 +37188,6 @@ const toCanonicalPath = (filePath) => {
|
|
|
36786
37188
|
};
|
|
36787
37189
|
const DEAD_CODE_PLUGIN = "deslop";
|
|
36788
37190
|
const DEAD_CODE_CATEGORY = "Maintainability";
|
|
36789
|
-
const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
|
|
36790
|
-
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
36791
37191
|
const DEAD_CODE_WORKER_SCRIPT = `
|
|
36792
37192
|
const inputChunks = [];
|
|
36793
37193
|
process.stdin.on("data", (chunk) => inputChunks.push(chunk));
|
|
@@ -36835,6 +37235,22 @@ process.stdin.on("end", () => {
|
|
|
36835
37235
|
...(workerInput.ignorePatterns.length > 0
|
|
36836
37236
|
? { ignorePatterns: workerInput.ignorePatterns }
|
|
36837
37237
|
: {}),
|
|
37238
|
+
// We consume only deslop's GRAPH-based findings (unusedFiles, unusedExports,
|
|
37239
|
+
// unusedDependencies, circularDependencies). Everything else deslop can compute
|
|
37240
|
+
// is pure wasted work for us, and it's the bulk of the runtime:
|
|
37241
|
+
// - semantic: a full TS Program for unusedTypes/enum/class-members/
|
|
37242
|
+
// misclassifiedDependencies (~37-45% of the phase).
|
|
37243
|
+
// - reportCodeQuality: the duplicate-block, complexity, feature-flag,
|
|
37244
|
+
// TypeScript-smell, private-type-leak and re-export-cycle detectors. These
|
|
37245
|
+
// are the single most expensive pass — duplicate-block detection alone was
|
|
37246
|
+
// ~83s of a ~130s Sentry scan — so skipping them is an ~8.5x dead-code
|
|
37247
|
+
// speedup on a large repo.
|
|
37248
|
+
// Both are provably safe: the consumed graph findings are computed by their own
|
|
37249
|
+
// detectors, independent of these passes (confirmed byte-identical on
|
|
37250
|
+
// excalidraw + mui-material + sentry). tsConfigPath stays — the module resolver
|
|
37251
|
+
// needs it for path-alias resolution in the import graph.
|
|
37252
|
+
semantic: { enabled: false },
|
|
37253
|
+
reportCodeQuality: false,
|
|
36838
37254
|
};
|
|
36839
37255
|
const result = await analyze(defineConfig(config));
|
|
36840
37256
|
emit({ ok: true, result: normalizeResult(result) });
|
|
@@ -36845,7 +37261,7 @@ process.stdin.on("end", () => {
|
|
|
36845
37261
|
});
|
|
36846
37262
|
`;
|
|
36847
37263
|
const resolveTsConfigPath = (rootDirectory) => {
|
|
36848
|
-
for (const filename of TSCONFIG_FILENAMES
|
|
37264
|
+
for (const filename of TSCONFIG_FILENAMES) {
|
|
36849
37265
|
const candidate = Path.join(rootDirectory, filename);
|
|
36850
37266
|
if (NFS.existsSync(candidate)) return candidate;
|
|
36851
37267
|
}
|
|
@@ -36964,7 +37380,11 @@ const createDeadCodeWorker = (input) => {
|
|
|
36964
37380
|
"pipe",
|
|
36965
37381
|
"pipe"
|
|
36966
37382
|
],
|
|
36967
|
-
windowsHide: true
|
|
37383
|
+
windowsHide: true,
|
|
37384
|
+
env: input.parseConcurrency === void 0 ? process.env : {
|
|
37385
|
+
...process.env,
|
|
37386
|
+
DESLOP_PARSE_CONCURRENCY: String(input.parseConcurrency)
|
|
37387
|
+
}
|
|
36968
37388
|
});
|
|
36969
37389
|
const stdoutChunks = [];
|
|
36970
37390
|
const stderrChunks = [];
|
|
@@ -37009,42 +37429,39 @@ const createDeadCodeWorker = (input) => {
|
|
|
37009
37429
|
}
|
|
37010
37430
|
};
|
|
37011
37431
|
};
|
|
37012
|
-
const runDeadCodeWorkerWithTimeout = (handle, timeoutMs) => new Promise((resolve, reject) => {
|
|
37432
|
+
const runDeadCodeWorkerWithTimeout = (handle, timeoutMs, abortSignal) => new Promise((resolve, reject) => {
|
|
37013
37433
|
let didSettle = false;
|
|
37014
|
-
const
|
|
37015
|
-
if (didSettle) return;
|
|
37016
|
-
didSettle = true;
|
|
37017
|
-
handle.terminate?.();
|
|
37018
|
-
reject(/* @__PURE__ */ new Error(`Dead-code worker timed out after ${timeoutMs / MILLISECONDS_PER_SECOND}s.`));
|
|
37019
|
-
}, timeoutMs);
|
|
37020
|
-
timeoutHandle.unref?.();
|
|
37021
|
-
handle.result.then((value) => {
|
|
37022
|
-
if (didSettle) return;
|
|
37023
|
-
didSettle = true;
|
|
37024
|
-
clearTimeout(timeoutHandle);
|
|
37025
|
-
handle.terminate?.();
|
|
37026
|
-
resolve(value);
|
|
37027
|
-
}, (error) => {
|
|
37434
|
+
const settle = (finish) => {
|
|
37028
37435
|
if (didSettle) return;
|
|
37029
37436
|
didSettle = true;
|
|
37030
37437
|
clearTimeout(timeoutHandle);
|
|
37438
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
37031
37439
|
handle.terminate?.();
|
|
37032
|
-
|
|
37033
|
-
}
|
|
37440
|
+
finish();
|
|
37441
|
+
};
|
|
37442
|
+
const onAbort = () => settle(() => reject(/* @__PURE__ */ new Error("Dead-code worker aborted.")));
|
|
37443
|
+
const timeoutHandle = setTimeout(() => settle(() => reject(/* @__PURE__ */ new Error(`Dead-code worker timed out after ${timeoutMs / MILLISECONDS_PER_SECOND}s.`))), timeoutMs);
|
|
37444
|
+
timeoutHandle.unref?.();
|
|
37445
|
+
if (abortSignal?.aborted) {
|
|
37446
|
+
onAbort();
|
|
37447
|
+
return;
|
|
37448
|
+
}
|
|
37449
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
37450
|
+
handle.result.then((value) => settle(() => resolve(value)), (error) => settle(() => reject(error)));
|
|
37034
37451
|
});
|
|
37035
37452
|
const checkDeadCode = async (options) => {
|
|
37036
|
-
const { userConfig } = options;
|
|
37037
37453
|
const rootDirectory = toCanonicalPath(options.rootDirectory);
|
|
37038
37454
|
if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
|
|
37039
37455
|
const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
|
|
37040
|
-
const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory
|
|
37456
|
+
const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
|
|
37041
37457
|
const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
|
|
37042
37458
|
rootDirectory,
|
|
37043
37459
|
entryPatterns,
|
|
37044
37460
|
tsConfigPath: resolveTsConfigPath(rootDirectory),
|
|
37045
37461
|
ignorePatterns,
|
|
37046
|
-
deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js")
|
|
37047
|
-
|
|
37462
|
+
deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
|
|
37463
|
+
parseConcurrency: options.parseConcurrency
|
|
37464
|
+
}), options.workerTimeoutMs ?? 12e4, options.abortSignal));
|
|
37048
37465
|
const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
|
|
37049
37466
|
const diagnostics = [];
|
|
37050
37467
|
for (const unusedFile of result.unusedFiles) diagnostics.push({
|
|
@@ -37142,7 +37559,37 @@ const isDiagnosticOnSurface = (diagnostic, surface, config) => {
|
|
|
37142
37559
|
return true;
|
|
37143
37560
|
};
|
|
37144
37561
|
const filterDiagnosticsForSurface = (diagnostics, surface, config) => diagnostics.filter((diagnostic) => isDiagnosticOnSurface(diagnostic, surface, config));
|
|
37145
|
-
|
|
37562
|
+
/**
|
|
37563
|
+
* Budget for the dead-code phase, scaled to the work. deslop's graph build is
|
|
37564
|
+
* CPU-bound and roughly linear in file count, so a fixed 120s cap is too tight
|
|
37565
|
+
* for a large repo (where the pass legitimately runs that long) and is then
|
|
37566
|
+
* tipped over by any concurrent load — silently dropping every dead-code
|
|
37567
|
+
* finding. Scaling the budget with file count (and inversely with the core
|
|
37568
|
+
* share when overlapped) lets the pass complete, while the ceiling still
|
|
37569
|
+
* reclaims a genuinely wedged worker. Returns the in-worker SIGKILL deadline
|
|
37570
|
+
* and the Effect-side phase backstop that sits a margin above it.
|
|
37571
|
+
*/
|
|
37572
|
+
const resolveDeadCodeTimeout = (input) => {
|
|
37573
|
+
const coreShareFactor = Math.max(1, input.fullConcurrency / Math.max(1, input.deadCodeConcurrency));
|
|
37574
|
+
const workerTimeoutMs = Math.min(DEAD_CODE_TIMEOUT_CEILING_MS, Math.max(DEAD_CODE_WORKER_TIMEOUT_MS, Math.ceil(input.sourceFileCount * 30 * coreShareFactor)));
|
|
37575
|
+
return {
|
|
37576
|
+
workerTimeoutMs,
|
|
37577
|
+
phaseTimeoutMs: workerTimeoutMs + DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS
|
|
37578
|
+
};
|
|
37579
|
+
};
|
|
37580
|
+
const collectSizedSourceFiles = (rootDirectory, relativePaths) => {
|
|
37581
|
+
const entries = [];
|
|
37582
|
+
for (const relativePath of relativePaths) {
|
|
37583
|
+
const absolutePath = Path.resolve(rootDirectory, relativePath);
|
|
37584
|
+
const sizeBytes = statSourceFileSize(absolutePath);
|
|
37585
|
+
if (isLargeMinifiedFile(absolutePath, sizeBytes)) continue;
|
|
37586
|
+
entries.push({
|
|
37587
|
+
path: relativePath,
|
|
37588
|
+
sizeBytes: sizeBytes ?? 0
|
|
37589
|
+
});
|
|
37590
|
+
}
|
|
37591
|
+
return entries;
|
|
37592
|
+
};
|
|
37146
37593
|
const listSourceFilesViaGit = (rootDirectory) => {
|
|
37147
37594
|
const result = spawnSync("git", [
|
|
37148
37595
|
"ls-files",
|
|
@@ -37175,7 +37622,8 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
|
|
|
37175
37622
|
}
|
|
37176
37623
|
return filePaths;
|
|
37177
37624
|
};
|
|
37178
|
-
const
|
|
37625
|
+
const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory));
|
|
37626
|
+
const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
|
|
37179
37627
|
const resolveLintIncludePaths = (rootDirectory, userConfig, project) => {
|
|
37180
37628
|
if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
|
|
37181
37629
|
const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
|
|
@@ -37218,24 +37666,25 @@ var Config = class Config extends Service()("react-doctor/Config") {
|
|
|
37218
37666
|
var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
|
|
37219
37667
|
static layerNode = succeed$3(DeadCode, DeadCode.of({ run: (input) => unwrap(fn("DeadCode.run")(function* () {
|
|
37220
37668
|
return yield* tryPromise({
|
|
37221
|
-
try: () => checkDeadCode({
|
|
37669
|
+
try: (signal) => checkDeadCode({
|
|
37222
37670
|
rootDirectory: input.rootDirectory,
|
|
37223
|
-
userConfig: input.userConfig
|
|
37671
|
+
userConfig: input.userConfig,
|
|
37672
|
+
parseConcurrency: input.parseConcurrency,
|
|
37673
|
+
workerTimeoutMs: input.workerTimeoutMs,
|
|
37674
|
+
abortSignal: signal
|
|
37224
37675
|
}),
|
|
37225
37676
|
catch: (cause) => new ReactDoctorError({ reason: new DeadCodeAnalysisFailed({ cause }) })
|
|
37226
37677
|
}).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)));
|
|
37227
37678
|
})()) }));
|
|
37228
37679
|
static layerOf = (diagnostics) => succeed$3(DeadCode, DeadCode.of({ run: () => fromIterable$1(diagnostics) }));
|
|
37229
37680
|
};
|
|
37230
|
-
const createNodeReadFileLinesSync = (rootDirectory) => {
|
|
37231
|
-
|
|
37232
|
-
|
|
37233
|
-
|
|
37234
|
-
|
|
37235
|
-
|
|
37236
|
-
|
|
37237
|
-
}
|
|
37238
|
-
};
|
|
37681
|
+
const createNodeReadFileLinesSync = (rootDirectory) => (filePath) => {
|
|
37682
|
+
const absolutePath = Path.isAbsolute(filePath) ? filePath : Path.join(rootDirectory, filePath);
|
|
37683
|
+
try {
|
|
37684
|
+
return NFS.readFileSync(absolutePath, "utf-8").split("\n");
|
|
37685
|
+
} catch {
|
|
37686
|
+
return null;
|
|
37687
|
+
}
|
|
37239
37688
|
};
|
|
37240
37689
|
var Files = class Files extends Service()("react-doctor/Files") {
|
|
37241
37690
|
static layerNode = succeed$3(Files, Files.of({
|
|
@@ -37446,7 +37895,10 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
37446
37895
|
directory: input.directory,
|
|
37447
37896
|
cause
|
|
37448
37897
|
}) });
|
|
37449
|
-
})
|
|
37898
|
+
}), withSpan("git.exec", { attributes: {
|
|
37899
|
+
"git.command": input.command,
|
|
37900
|
+
"git.subcommand": input.args[0] ?? ""
|
|
37901
|
+
} }));
|
|
37450
37902
|
const runGit = (directory, args) => runCommand({
|
|
37451
37903
|
command: "git",
|
|
37452
37904
|
args,
|
|
@@ -37474,7 +37926,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
37474
37926
|
]);
|
|
37475
37927
|
if (candidates.status !== 0) return null;
|
|
37476
37928
|
return trimOrNull(candidates.stdout.split("\n")[0] ?? "");
|
|
37477
|
-
});
|
|
37929
|
+
}).pipe(withSpan("Git.defaultBranch"));
|
|
37478
37930
|
const branchExists = (directory, branch) => runGit(directory, [
|
|
37479
37931
|
"rev-parse",
|
|
37480
37932
|
"--verify",
|
|
@@ -37521,7 +37973,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
37521
37973
|
const result = resultOption.value;
|
|
37522
37974
|
if (result.status !== 0) return null;
|
|
37523
37975
|
return parseGithubViewerPermission(result.stdout);
|
|
37524
|
-
}).pipe(catch_$1(() => succeed$2(null)));
|
|
37976
|
+
}).pipe(catch_$1(() => succeed$2(null)), withSpan("Git.githubViewerPermission"));
|
|
37525
37977
|
/**
|
|
37526
37978
|
* Resolves a `--diff A..B` / `A...B` commit range into a changed-file
|
|
37527
37979
|
* selection. Each endpoint is validated with `isSafeGitRevision`
|
|
@@ -37635,7 +38087,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
37635
38087
|
changedFiles: splitNullSeparated(diff.stdout),
|
|
37636
38088
|
isCurrentChanges: false
|
|
37637
38089
|
};
|
|
37638
|
-
}),
|
|
38090
|
+
}).pipe(withSpan("Git.diffSelection")),
|
|
37639
38091
|
stagedFilePaths: (directory) => runGit(directory, [
|
|
37640
38092
|
"diff",
|
|
37641
38093
|
"--cached",
|
|
@@ -37677,7 +38129,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
37677
38129
|
status: result.status,
|
|
37678
38130
|
stdout: result.stdout
|
|
37679
38131
|
};
|
|
37680
|
-
}),
|
|
38132
|
+
}).pipe(withSpan("Git.grep")),
|
|
37681
38133
|
changedLineRanges: ({ directory, baseRef, cached, files }) => gen(function* () {
|
|
37682
38134
|
if (files.length === 0) return [];
|
|
37683
38135
|
if (baseRef !== void 0 && !isSafeGitRevision(baseRef)) return null;
|
|
@@ -37693,7 +38145,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
37693
38145
|
]);
|
|
37694
38146
|
if (result.status !== 0) return null;
|
|
37695
38147
|
return parseChangedLineRanges(result.stdout);
|
|
37696
|
-
})
|
|
38148
|
+
}).pipe(withSpan("Git.changedLineRanges"))
|
|
37697
38149
|
});
|
|
37698
38150
|
})).pipe(provide$2(layer$2.pipe(provide$2(mergeAll$1(layer$1, layer)))));
|
|
37699
38151
|
/**
|
|
@@ -37908,7 +38360,7 @@ const neutralizeDisableDirectives = async (rootDirectory, includePaths) => {
|
|
|
37908
38360
|
for (const [absolutePath, originalContent] of originalContents) try {
|
|
37909
38361
|
NFS.writeFileSync(absolutePath, originalContent);
|
|
37910
38362
|
} catch (error) {
|
|
37911
|
-
process.stderr.write(`[react-doctor] Failed to restore inline disable directives in ${absolutePath}: ${
|
|
38363
|
+
process.stderr.write(`[react-doctor] Failed to restore inline disable directives in ${absolutePath}: ${messageFromUnknown(error)}\n[react-doctor] Run: git checkout -- ${absolutePath}\n`);
|
|
37912
38364
|
}
|
|
37913
38365
|
};
|
|
37914
38366
|
const onExit = () => restore();
|
|
@@ -37932,6 +38384,14 @@ const neutralizeDisableDirectives = async (rootDirectory, includePaths) => {
|
|
|
37932
38384
|
process.removeListener("exit", onExit);
|
|
37933
38385
|
};
|
|
37934
38386
|
};
|
|
38387
|
+
const ROOT_DIRECTORY_PLACEHOLDER = "<root>";
|
|
38388
|
+
const normalizeConfigForHash = (config) => {
|
|
38389
|
+
const clone = JSON.parse(JSON.stringify(config));
|
|
38390
|
+
if (clone?.settings?.["react-doctor"]) clone.settings["react-doctor"].rootDirectory = ROOT_DIRECTORY_PLACEHOLDER;
|
|
38391
|
+
if (Array.isArray(clone?.jsPlugins)) clone.jsPlugins = clone.jsPlugins.map((_, index) => `<plugin:${index}>`);
|
|
38392
|
+
return clone;
|
|
38393
|
+
};
|
|
38394
|
+
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");
|
|
37935
38395
|
/**
|
|
37936
38396
|
* Loads a plugin module via the local require resolver and extracts
|
|
37937
38397
|
* `(name, ruleNames)` from either `module.exports.meta + rules` or
|
|
@@ -37958,16 +38418,16 @@ const readPluginShape = (pluginSpecifier, loadModule) => {
|
|
|
37958
38418
|
ruleNames: new Set(Object.keys(rules))
|
|
37959
38419
|
};
|
|
37960
38420
|
};
|
|
37961
|
-
const bundledRequire = createRequire(import.meta.url);
|
|
38421
|
+
const bundledRequire$1 = createRequire(import.meta.url);
|
|
37962
38422
|
const resolveReactHooksJsPlugin = (hasReactCompiler, customRulesOnly) => {
|
|
37963
38423
|
if (!hasReactCompiler || customRulesOnly) return null;
|
|
37964
38424
|
let pluginSpecifier;
|
|
37965
38425
|
try {
|
|
37966
|
-
pluginSpecifier = bundledRequire.resolve("eslint-plugin-react-hooks");
|
|
38426
|
+
pluginSpecifier = bundledRequire$1.resolve("eslint-plugin-react-hooks");
|
|
37967
38427
|
} catch {
|
|
37968
38428
|
return null;
|
|
37969
38429
|
}
|
|
37970
|
-
const { ruleNames } = readPluginShape(pluginSpecifier, (spec) => bundledRequire(spec));
|
|
38430
|
+
const { ruleNames } = readPluginShape(pluginSpecifier, (spec) => bundledRequire$1(spec));
|
|
37971
38431
|
return {
|
|
37972
38432
|
entry: {
|
|
37973
38433
|
name: "react-hooks-js",
|
|
@@ -38014,7 +38474,7 @@ const resolveUserPlugin = (spec, configSourceDirectory) => {
|
|
|
38014
38474
|
try {
|
|
38015
38475
|
resolvedSpecifier = isRelative ? Path.resolve(configSourceDirectory, spec) : candidateRequire.resolve(spec);
|
|
38016
38476
|
} catch (error) {
|
|
38017
|
-
warnConfigIssue(`config.plugins entry "${spec}" could not be resolved from ${configSourceDirectory}: ${
|
|
38477
|
+
warnConfigIssue(`config.plugins entry "${spec}" could not be resolved from ${configSourceDirectory}: ${messageFromUnknown(error)}`);
|
|
38018
38478
|
return null;
|
|
38019
38479
|
}
|
|
38020
38480
|
const { name, ruleNames } = readPluginShape(resolvedSpecifier, (target) => candidateRequire(target));
|
|
@@ -38086,8 +38546,8 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
|
|
|
38086
38546
|
}
|
|
38087
38547
|
return enabled;
|
|
38088
38548
|
};
|
|
38089
|
-
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [] }) => {
|
|
38090
|
-
const reactHooksJsPlugin = resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
|
|
38549
|
+
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection }) => {
|
|
38550
|
+
const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
|
|
38091
38551
|
const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
|
|
38092
38552
|
const jsPlugins = [];
|
|
38093
38553
|
if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
|
|
@@ -38096,6 +38556,8 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
38096
38556
|
for (const registryEntry of REACT_DOCTOR_RULES) {
|
|
38097
38557
|
const rule = reactDoctorPlugin.rules[registryEntry.id];
|
|
38098
38558
|
if (!rule) continue;
|
|
38559
|
+
if (ruleSelection === "cacheable" && CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
|
|
38560
|
+
if (ruleSelection === "sidecar" && !CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
|
|
38099
38561
|
if (rule.scan !== void 0) continue;
|
|
38100
38562
|
if (customRulesOnly && registryEntry.originallyExternal) continue;
|
|
38101
38563
|
if (rule.framework !== "global" && !rule.requires) continue;
|
|
@@ -38110,7 +38572,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
38110
38572
|
enabledReactDoctorRules[registryEntry.key] = severity;
|
|
38111
38573
|
}
|
|
38112
38574
|
const userPluginRules = {};
|
|
38113
|
-
for (const userPlugin of userPlugins) {
|
|
38575
|
+
if (ruleSelection !== "sidecar") for (const userPlugin of userPlugins) {
|
|
38114
38576
|
Object.assign(userPluginRules, buildUserPluginRules(userPlugin, severityControls));
|
|
38115
38577
|
jsPlugins.push(userPlugin.entry);
|
|
38116
38578
|
}
|
|
@@ -38140,6 +38602,100 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
38140
38602
|
}
|
|
38141
38603
|
};
|
|
38142
38604
|
};
|
|
38605
|
+
const atomicWriteJson = (filePath, value) => {
|
|
38606
|
+
try {
|
|
38607
|
+
NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
|
|
38608
|
+
const temporaryPath = `${filePath}.${process.pid}.tmp`;
|
|
38609
|
+
NFS.writeFileSync(temporaryPath, JSON.stringify(value));
|
|
38610
|
+
NFS.renameSync(temporaryPath, filePath);
|
|
38611
|
+
} catch {
|
|
38612
|
+
return;
|
|
38613
|
+
}
|
|
38614
|
+
};
|
|
38615
|
+
const failOpenReadJson = (filePath, fallback) => {
|
|
38616
|
+
try {
|
|
38617
|
+
return JSON.parse(NFS.readFileSync(filePath, "utf8"));
|
|
38618
|
+
} catch {
|
|
38619
|
+
return fallback;
|
|
38620
|
+
}
|
|
38621
|
+
};
|
|
38622
|
+
const validateDiagnostic = decodeUnknownSync(Diagnostic);
|
|
38623
|
+
const decodeFileDiagnostics = (raw) => {
|
|
38624
|
+
if (!Array.isArray(raw)) return null;
|
|
38625
|
+
try {
|
|
38626
|
+
for (const entry of raw) validateDiagnostic(entry);
|
|
38627
|
+
return raw;
|
|
38628
|
+
} catch {
|
|
38629
|
+
return null;
|
|
38630
|
+
}
|
|
38631
|
+
};
|
|
38632
|
+
const emptyCache = () => ({
|
|
38633
|
+
version: 1,
|
|
38634
|
+
rulesets: {}
|
|
38635
|
+
});
|
|
38636
|
+
const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
|
|
38637
|
+
const entries = /* @__PURE__ */ new Map();
|
|
38638
|
+
const persisted = failOpenReadJson(cacheFilePath, emptyCache());
|
|
38639
|
+
if (persisted.version !== 1 || !isRecord(persisted.rulesets)) return entries;
|
|
38640
|
+
const bucket = persisted.rulesets[rulesetHash];
|
|
38641
|
+
if (!isRecord(bucket) || !isRecord(bucket.files)) return entries;
|
|
38642
|
+
for (const [fileKey, rawDiagnostics] of Object.entries(bucket.files)) {
|
|
38643
|
+
const decoded = decodeFileDiagnostics(rawDiagnostics);
|
|
38644
|
+
if (decoded !== null) entries.set(fileKey, decoded);
|
|
38645
|
+
}
|
|
38646
|
+
return entries;
|
|
38647
|
+
};
|
|
38648
|
+
const createFileLintCache = (cacheDirectory, rulesetHash) => {
|
|
38649
|
+
const cacheFilePath = Path.join(cacheDirectory, FILE_LINT_CACHE_FILENAME);
|
|
38650
|
+
const entries = loadRulesetEntries(cacheFilePath, rulesetHash);
|
|
38651
|
+
return {
|
|
38652
|
+
lookup: (fileKey) => entries.get(fileKey) ?? null,
|
|
38653
|
+
store: (fileKey, diagnostics) => {
|
|
38654
|
+
entries.delete(fileKey);
|
|
38655
|
+
entries.set(fileKey, diagnostics);
|
|
38656
|
+
},
|
|
38657
|
+
persist: () => {
|
|
38658
|
+
const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
|
|
38659
|
+
const rulesets = onDisk.version === 1 && isRecord(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
|
|
38660
|
+
const existingBucket = rulesets[rulesetHash];
|
|
38661
|
+
const existingFiles = isRecord(existingBucket) && isRecord(existingBucket.files) ? existingBucket.files : {};
|
|
38662
|
+
const ourFiles = {};
|
|
38663
|
+
for (const [fileKey, diagnostics] of entries) ourFiles[fileKey] = diagnostics;
|
|
38664
|
+
const cappedEntries = Object.entries({
|
|
38665
|
+
...existingFiles,
|
|
38666
|
+
...ourFiles
|
|
38667
|
+
}).slice(-FILE_LINT_CACHE_MAX_FILE_COUNT);
|
|
38668
|
+
rulesets[rulesetHash] = {
|
|
38669
|
+
updatedAtMs: Date.now(),
|
|
38670
|
+
files: Object.fromEntries(cappedEntries)
|
|
38671
|
+
};
|
|
38672
|
+
const keptHashes = Object.entries(rulesets).sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8).map(([hash]) => hash);
|
|
38673
|
+
const prunedRulesets = {};
|
|
38674
|
+
for (const hash of keptHashes) prunedRulesets[hash] = rulesets[hash];
|
|
38675
|
+
atomicWriteJson(cacheFilePath, {
|
|
38676
|
+
version: 1,
|
|
38677
|
+
rulesets: prunedRulesets
|
|
38678
|
+
});
|
|
38679
|
+
}
|
|
38680
|
+
};
|
|
38681
|
+
};
|
|
38682
|
+
const bundledRequire = createRequire(import.meta.url);
|
|
38683
|
+
const TOOLCHAIN_PACKAGE_SPECIFIERS = [
|
|
38684
|
+
"oxlint/package.json",
|
|
38685
|
+
"oxlint-plugin-react-doctor/package.json",
|
|
38686
|
+
"eslint-plugin-react-hooks/package.json"
|
|
38687
|
+
];
|
|
38688
|
+
const resolveOxlintToolchainVersions = () => {
|
|
38689
|
+
const versions = [`node=${process.version}`];
|
|
38690
|
+
for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS) try {
|
|
38691
|
+
const packageJson = bundledRequire(specifier);
|
|
38692
|
+
const version = typeof packageJson.version === "string" ? packageJson.version : "unknown";
|
|
38693
|
+
versions.push(`${specifier}=${version}`);
|
|
38694
|
+
} catch {
|
|
38695
|
+
versions.push(`${specifier}=missing`);
|
|
38696
|
+
}
|
|
38697
|
+
return versions;
|
|
38698
|
+
};
|
|
38143
38699
|
const esmRequire = createRequire(import.meta.url);
|
|
38144
38700
|
const resolveOxlintBinary = () => {
|
|
38145
38701
|
const oxlintMainPath = esmRequire.resolve("oxlint");
|
|
@@ -38147,7 +38703,6 @@ const resolveOxlintBinary = () => {
|
|
|
38147
38703
|
return Path.join(oxlintPackageDirectory, "bin", "oxlint");
|
|
38148
38704
|
};
|
|
38149
38705
|
const resolvePluginPath = () => esmRequire.resolve("oxlint-plugin-react-doctor");
|
|
38150
|
-
const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
|
|
38151
38706
|
const resolveTsConfigRelativePath = (rootDirectory) => {
|
|
38152
38707
|
for (const filename of TSCONFIG_FILENAMES) if (NFS.existsSync(Path.join(rootDirectory, filename))) return `./${filename}`;
|
|
38153
38708
|
return null;
|
|
@@ -38519,7 +39074,7 @@ const scopeContainsNonImportBinding = (node, scopeNode, identifierName) => {
|
|
|
38519
39074
|
const isIdentifierShadowedByLocalBinding = (identifier, sourceFile) => {
|
|
38520
39075
|
let currentNode = identifier.parent;
|
|
38521
39076
|
while (currentNode) {
|
|
38522
|
-
if (
|
|
39077
|
+
if (isScopeBoundary(currentNode)) {
|
|
38523
39078
|
if (scopeContainsNonImportBinding(currentNode, currentNode, identifier.text)) return true;
|
|
38524
39079
|
}
|
|
38525
39080
|
if (currentNode === sourceFile) return false;
|
|
@@ -38610,11 +39165,10 @@ const findResolutionInScope = (scopeNode, identifierName, reactImportBindings, s
|
|
|
38610
39165
|
});
|
|
38611
39166
|
return resolution;
|
|
38612
39167
|
};
|
|
38613
|
-
const isScopeNode = isScopeBoundary;
|
|
38614
39168
|
const resolveIdentifierBinding = (identifier, reactImportBindings, sourceFile, visitedDeclarations = /* @__PURE__ */ new Set()) => {
|
|
38615
39169
|
let currentNode = identifier.parent;
|
|
38616
39170
|
while (currentNode) {
|
|
38617
|
-
if (
|
|
39171
|
+
if (isScopeBoundary(currentNode)) {
|
|
38618
39172
|
const resolution = findResolutionInScope(currentNode, identifier.text, reactImportBindings, sourceFile, visitedDeclarations);
|
|
38619
39173
|
if (resolution) return resolution;
|
|
38620
39174
|
}
|
|
@@ -38784,9 +39338,9 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
|
|
|
38784
39338
|
try {
|
|
38785
39339
|
parsed = JSON.parse(sanitizedStdout);
|
|
38786
39340
|
} catch {
|
|
38787
|
-
throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0,
|
|
39341
|
+
throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
|
|
38788
39342
|
}
|
|
38789
|
-
if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0,
|
|
39343
|
+
if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
|
|
38790
39344
|
const minifiedFileCache = /* @__PURE__ */ new Map();
|
|
38791
39345
|
const isMinifiedDiagnosticFile = (filename) => {
|
|
38792
39346
|
const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
|
|
@@ -38823,15 +39377,19 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
|
|
|
38823
39377
|
};
|
|
38824
39378
|
});
|
|
38825
39379
|
};
|
|
38826
|
-
const
|
|
38827
|
-
const
|
|
38828
|
-
for (const [name, value] of Object.entries(
|
|
39380
|
+
const buildOxlintChildEnv = (sourceEnv) => {
|
|
39381
|
+
const childEnv = {};
|
|
39382
|
+
for (const [name, value] of Object.entries(sourceEnv)) {
|
|
38829
39383
|
if (name === "NODE_OPTIONS" || name === "NODE_DEBUG") continue;
|
|
38830
39384
|
if (name.startsWith("npm_config_")) continue;
|
|
38831
|
-
|
|
39385
|
+
childEnv[name] = value;
|
|
38832
39386
|
}
|
|
38833
|
-
|
|
38834
|
-
|
|
39387
|
+
const isCompileCacheDisabled = Boolean(sourceEnv.NODE_DISABLE_COMPILE_CACHE);
|
|
39388
|
+
const isCompileCacheAlreadySet = childEnv.NODE_COMPILE_CACHE !== void 0;
|
|
39389
|
+
if (!isCompileCacheDisabled && !isCompileCacheAlreadySet) childEnv.NODE_COMPILE_CACHE = Path.join(os.tmpdir(), NODE_COMPILE_CACHE_DIR_NAME);
|
|
39390
|
+
return childEnv;
|
|
39391
|
+
};
|
|
39392
|
+
const SANITIZED_ENV = buildOxlintChildEnv(process.env);
|
|
38835
39393
|
/**
|
|
38836
39394
|
* Spawn one oxlint subprocess with hard ceilings on wall time and
|
|
38837
39395
|
* output size. Returns stdout on success; raises a tagged
|
|
@@ -38848,7 +39406,11 @@ const SANITIZED_ENV = (() => {
|
|
|
38848
39406
|
* The first three are splittable (the caller's binary-split retry
|
|
38849
39407
|
* shrinks the batch and re-spawns); the fourth isn't.
|
|
38850
39408
|
*/
|
|
38851
|
-
const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLINT_SPAWN_TIMEOUT_MS, outputMaxBytes = OXLINT_OUTPUT_MAX_BYTES) => new Promise((resolve, reject) => {
|
|
39409
|
+
const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLINT_SPAWN_TIMEOUT_MS, outputMaxBytes = OXLINT_OUTPUT_MAX_BYTES, abortSignal) => new Promise((resolve, reject) => {
|
|
39410
|
+
if (abortSignal?.aborted) {
|
|
39411
|
+
reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: "lint phase aborted" }) }));
|
|
39412
|
+
return;
|
|
39413
|
+
}
|
|
38852
39414
|
const child = spawn(nodeBinaryPath, args, {
|
|
38853
39415
|
cwd: rootDirectory,
|
|
38854
39416
|
env: SANITIZED_ENV,
|
|
@@ -38858,11 +39420,18 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
|
|
|
38858
39420
|
"pipe"
|
|
38859
39421
|
]
|
|
38860
39422
|
});
|
|
39423
|
+
const onAbort = () => {
|
|
39424
|
+
child.kill("SIGKILL");
|
|
39425
|
+
reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: "lint phase aborted" }) }));
|
|
39426
|
+
};
|
|
39427
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
39428
|
+
const clearAbortListener = () => abortSignal?.removeEventListener("abort", onAbort);
|
|
38861
39429
|
const timeoutHandle = setTimeout(() => {
|
|
39430
|
+
clearAbortListener();
|
|
38862
39431
|
child.kill("SIGKILL");
|
|
38863
39432
|
reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
|
|
38864
39433
|
kind: "timeout",
|
|
38865
|
-
detail: `${spawnTimeoutMs /
|
|
39434
|
+
detail: `${spawnTimeoutMs / MILLISECONDS_PER_SECOND}s budget exceeded`
|
|
38866
39435
|
}) }));
|
|
38867
39436
|
}, spawnTimeoutMs);
|
|
38868
39437
|
timeoutHandle.unref?.();
|
|
@@ -38893,10 +39462,12 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
|
|
|
38893
39462
|
});
|
|
38894
39463
|
child.on("error", (error) => {
|
|
38895
39464
|
clearTimeout(timeoutHandle);
|
|
39465
|
+
clearAbortListener();
|
|
38896
39466
|
reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: error }) }));
|
|
38897
39467
|
});
|
|
38898
39468
|
child.on("close", (_code, signal) => {
|
|
38899
39469
|
clearTimeout(timeoutHandle);
|
|
39470
|
+
clearAbortListener();
|
|
38900
39471
|
if (didKillForSize) {
|
|
38901
39472
|
reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
|
|
38902
39473
|
kind: "output-too-large",
|
|
@@ -38963,26 +39534,28 @@ const isParallelismRelatedSpawnError = (error) => {
|
|
|
38963
39534
|
* loop with a slimmer config in that case.
|
|
38964
39535
|
*/
|
|
38965
39536
|
const spawnLintBatches = async (input) => {
|
|
38966
|
-
const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes } = input;
|
|
39537
|
+
const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 8, signal } = input;
|
|
38967
39538
|
const requestedConcurrency = resolveScanConcurrency(input.concurrency ?? 1);
|
|
38968
39539
|
const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
|
|
38969
39540
|
const runBatchPass = async (concurrency) => {
|
|
38970
39541
|
const allDiagnostics = [];
|
|
38971
39542
|
const droppedFiles = [];
|
|
38972
39543
|
let firstDropReason = null;
|
|
38973
|
-
const
|
|
39544
|
+
const splitDeadlineMs = Date.now() + splitTotalBudgetMs;
|
|
39545
|
+
const spawnLintBatch = async (batch, depth) => {
|
|
38974
39546
|
const batchArgs = [...baseArgs, ...batch];
|
|
38975
39547
|
try {
|
|
38976
|
-
return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes), project, rootDirectory);
|
|
39548
|
+
return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes, signal), project, rootDirectory);
|
|
38977
39549
|
} catch (error) {
|
|
38978
39550
|
if (!isSplittableReactDoctorError(error)) throw error;
|
|
38979
|
-
|
|
39551
|
+
const splitBudgetExhausted = Date.now() >= splitDeadlineMs || depth >= splitMaxDepth;
|
|
39552
|
+
if (batch.length <= 1 || splitBudgetExhausted) {
|
|
38980
39553
|
droppedFiles.push(...batch);
|
|
38981
|
-
if (firstDropReason === null) firstDropReason = error.message;
|
|
39554
|
+
if (firstDropReason === null) firstDropReason = splitBudgetExhausted && batch.length > 1 ? `${error.message} (split budget exhausted after ${splitMaxDepth} levels / ${splitTotalBudgetMs / MILLISECONDS_PER_SECOND}s)` : error.message;
|
|
38982
39555
|
return [];
|
|
38983
39556
|
}
|
|
38984
39557
|
const splitIndex = Math.ceil(batch.length / 2);
|
|
38985
|
-
return [...await spawnLintBatch(batch.slice(0, splitIndex)), ...await spawnLintBatch(batch.slice(splitIndex))];
|
|
39558
|
+
return [...await spawnLintBatch(batch.slice(0, splitIndex), depth + 1), ...await spawnLintBatch(batch.slice(splitIndex), depth + 1)];
|
|
38986
39559
|
}
|
|
38987
39560
|
};
|
|
38988
39561
|
let startedFileCount = 0;
|
|
@@ -38999,7 +39572,7 @@ const spawnLintBatches = async (input) => {
|
|
|
38999
39572
|
try {
|
|
39000
39573
|
const batchResults = await mapWithConcurrency(fileBatches, concurrency, async (batch) => {
|
|
39001
39574
|
startedFileCount += batch.length;
|
|
39002
|
-
const batchDiagnostics = await spawnLintBatch(batch);
|
|
39575
|
+
const batchDiagnostics = await spawnLintBatch(batch, 0);
|
|
39003
39576
|
scannedFileCount += batch.length;
|
|
39004
39577
|
if (onFileProgress) {
|
|
39005
39578
|
displayedFileCount = Math.min(Math.max(displayedFileCount, scannedFileCount), totalFileCount);
|
|
@@ -39060,6 +39633,22 @@ const validateRuleRegistration = () => {
|
|
|
39060
39633
|
].filter((entry) => entry !== null).join("; ");
|
|
39061
39634
|
console.warn(`[react-doctor] rule-registration drift: ${detail}`);
|
|
39062
39635
|
};
|
|
39636
|
+
const hashFileContents = (filePath) => {
|
|
39637
|
+
try {
|
|
39638
|
+
return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
|
|
39639
|
+
} catch {
|
|
39640
|
+
return null;
|
|
39641
|
+
}
|
|
39642
|
+
};
|
|
39643
|
+
const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
|
|
39644
|
+
const resolveReactDoctorCacheDir = (projectDirectory) => {
|
|
39645
|
+
const cacheDirOverride = process.env["REACT_DOCTOR_CACHE_DIR"]?.trim();
|
|
39646
|
+
if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
|
|
39647
|
+
const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
|
|
39648
|
+
if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
|
|
39649
|
+
return Path.join(os.tmpdir(), "react-doctor-cache", projectCacheSubdir(projectDirectory));
|
|
39650
|
+
};
|
|
39651
|
+
const sortSourceFilesByCost = (entries) => [...entries].sort((left, right) => right.sizeBytes - left.sizeBytes).map((entry) => entry.path);
|
|
39063
39652
|
/**
|
|
39064
39653
|
* Atomically (re)writes the generated oxlintrc.json. Used twice in
|
|
39065
39654
|
* the runner: once for the primary scan, once for the
|
|
@@ -39077,6 +39666,28 @@ const writeOxlintConfig = (configPath, configToWrite) => {
|
|
|
39077
39666
|
NFS.closeSync(fileHandle);
|
|
39078
39667
|
}
|
|
39079
39668
|
};
|
|
39669
|
+
const REACT_HOOKS_JS_DROP_PREFIX = "React Compiler rules (react-hooks-js/*) skipped — eslint-plugin-react-hooks failed to load in this environment";
|
|
39670
|
+
/**
|
|
39671
|
+
* Detects an oxlint config-load crash caused by the optional
|
|
39672
|
+
* `react-hooks-js` (eslint-plugin-react-hooks) React Compiler plugin and
|
|
39673
|
+
* builds the partial-failure note for it; returns `null` when the failure
|
|
39674
|
+
* was anything else.
|
|
39675
|
+
*
|
|
39676
|
+
* oxlint prints a framed error to stdout (not stderr) and exits non-zero
|
|
39677
|
+
* when a `jsPlugins` entry can't be imported; that non-JSON stdout
|
|
39678
|
+
* surfaces as `OxlintOutputUnparseable`. Because oxlint fails the WHOLE
|
|
39679
|
+
* config load on it, leaving the plugin in would drop every curated
|
|
39680
|
+
* react-doctor diagnostic too — so the caller retries with the plugin
|
|
39681
|
+
* stripped (issue #833). Both markers sit at the start of oxlint's
|
|
39682
|
+
* message, so they survive the `preview` slice even for deep pnpm paths.
|
|
39683
|
+
*/
|
|
39684
|
+
const reactHooksJsPluginDropNote = (error) => {
|
|
39685
|
+
if (!(error instanceof ReactDoctorError) || error.reason._tag !== "OxlintOutputUnparseable") return null;
|
|
39686
|
+
const { preview } = error.reason;
|
|
39687
|
+
if (!preview.includes("Failed to load JS plugin") || !preview.includes("eslint-plugin-react-hooks")) return null;
|
|
39688
|
+
const underlyingReason = preview.match(/Error:[^\n]*/)?.[0]?.trim();
|
|
39689
|
+
return `${REACT_HOOKS_JS_DROP_PREFIX}${underlyingReason ? `: ${underlyingReason}` : ""}. Other rules ran normally.`;
|
|
39690
|
+
};
|
|
39080
39691
|
/**
|
|
39081
39692
|
* The oxlint runner. Composed of three pieces in `runners/oxlint/`:
|
|
39082
39693
|
*
|
|
@@ -39096,7 +39707,7 @@ const writeOxlintConfig = (configPath, configToWrite) => {
|
|
|
39096
39707
|
* 6. always restore disable directives + clean up the temp dir
|
|
39097
39708
|
*/
|
|
39098
39709
|
const runOxlint = async (options) => {
|
|
39099
|
-
const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure, spawnTimeoutMs, outputMaxBytes } = options;
|
|
39710
|
+
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;
|
|
39100
39711
|
const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
|
|
39101
39712
|
const severityControls = buildRuleSeverityControls(userConfig);
|
|
39102
39713
|
validateRuleRegistration();
|
|
@@ -39104,38 +39715,165 @@ const runOxlint = async (options) => {
|
|
|
39104
39715
|
const pluginPath = resolvePluginPath();
|
|
39105
39716
|
const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
|
|
39106
39717
|
const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
|
|
39107
|
-
const buildConfig = (
|
|
39718
|
+
const buildConfig = (overrides) => createOxlintConfig({
|
|
39108
39719
|
pluginPath,
|
|
39109
39720
|
project,
|
|
39110
39721
|
customRulesOnly,
|
|
39111
|
-
extendsPaths:
|
|
39722
|
+
extendsPaths: overrides.extendsPaths,
|
|
39112
39723
|
ignoredTags,
|
|
39113
39724
|
serverAuthFunctionNames,
|
|
39114
39725
|
severityControls,
|
|
39115
|
-
userPlugins
|
|
39726
|
+
userPlugins,
|
|
39727
|
+
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin,
|
|
39728
|
+
ruleSelection: overrides.ruleSelection
|
|
39116
39729
|
});
|
|
39117
39730
|
const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
|
|
39118
39731
|
const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
|
|
39119
39732
|
const configPath = Path.join(configDirectory, "oxlintrc.json");
|
|
39120
39733
|
try {
|
|
39121
|
-
const
|
|
39122
|
-
|
|
39123
|
-
|
|
39124
|
-
configPath,
|
|
39125
|
-
"--format",
|
|
39126
|
-
"json"
|
|
39127
|
-
];
|
|
39734
|
+
const oxlintBinary = resolveOxlintBinary();
|
|
39735
|
+
const sharedArgs = [];
|
|
39736
|
+
let tsconfigContent = null;
|
|
39128
39737
|
if (project.hasTypeScript) {
|
|
39129
39738
|
const tsconfigRelativePath = resolveTsConfigRelativePath(rootDirectory);
|
|
39130
|
-
if (tsconfigRelativePath)
|
|
39739
|
+
if (tsconfigRelativePath) {
|
|
39740
|
+
sharedArgs.push("--tsconfig", tsconfigRelativePath);
|
|
39741
|
+
try {
|
|
39742
|
+
tsconfigContent = NFS.readFileSync(Path.resolve(rootDirectory, tsconfigRelativePath), "utf8");
|
|
39743
|
+
} catch {
|
|
39744
|
+
tsconfigContent = null;
|
|
39745
|
+
}
|
|
39746
|
+
}
|
|
39131
39747
|
}
|
|
39132
39748
|
const combinedPatterns = collectIgnorePatterns(rootDirectory);
|
|
39133
39749
|
if (combinedPatterns.length > 0) {
|
|
39134
39750
|
const combinedIgnorePath = Path.join(configDirectory, "combined.ignore");
|
|
39135
39751
|
NFS.writeFileSync(combinedIgnorePath, `${combinedPatterns.join("\n")}\n`);
|
|
39136
|
-
|
|
39752
|
+
sharedArgs.push("--ignore-path", combinedIgnorePath);
|
|
39753
|
+
}
|
|
39754
|
+
const makeBaseArgs = (oxlintConfigPath) => [
|
|
39755
|
+
oxlintBinary,
|
|
39756
|
+
"-c",
|
|
39757
|
+
oxlintConfigPath,
|
|
39758
|
+
"--format",
|
|
39759
|
+
"json",
|
|
39760
|
+
...sharedArgs
|
|
39761
|
+
];
|
|
39762
|
+
const discoverScanFiles = () => lintBatchOrdering === "cost" ? sortSourceFilesByCost(listSourceFilesWithSize(rootDirectory)) : listSourceFiles(rootDirectory);
|
|
39763
|
+
const candidateFiles = includePaths !== void 0 ? includePaths : discoverScanFiles();
|
|
39764
|
+
const runConfigOverFiles = async (buildConfigForPass, configFileName, files, fileProgress) => {
|
|
39765
|
+
if (files.length === 0) return {
|
|
39766
|
+
diagnostics: [],
|
|
39767
|
+
didDropReactHooksJsPlugin: false,
|
|
39768
|
+
hadPartialFailure: false
|
|
39769
|
+
};
|
|
39770
|
+
let hadPartialFailure = false;
|
|
39771
|
+
const reportPartialFailure = (reason) => {
|
|
39772
|
+
hadPartialFailure = true;
|
|
39773
|
+
onPartialFailure?.(reason);
|
|
39774
|
+
};
|
|
39775
|
+
const passConfigPath = Path.join(configDirectory, configFileName);
|
|
39776
|
+
const passBaseArgs = makeBaseArgs(passConfigPath);
|
|
39777
|
+
const passFileBatches = batchIncludePaths(passBaseArgs, files);
|
|
39778
|
+
const spawnPass = () => spawnLintBatches({
|
|
39779
|
+
baseArgs: passBaseArgs,
|
|
39780
|
+
fileBatches: passFileBatches,
|
|
39781
|
+
rootDirectory,
|
|
39782
|
+
nodeBinaryPath,
|
|
39783
|
+
project,
|
|
39784
|
+
onPartialFailure: reportPartialFailure,
|
|
39785
|
+
onFileProgress: fileProgress,
|
|
39786
|
+
spawnTimeoutMs,
|
|
39787
|
+
outputMaxBytes,
|
|
39788
|
+
concurrency: options.concurrency,
|
|
39789
|
+
signal: options.signal
|
|
39790
|
+
});
|
|
39791
|
+
writeOxlintConfig(passConfigPath, buildConfigForPass({}));
|
|
39792
|
+
try {
|
|
39793
|
+
return {
|
|
39794
|
+
diagnostics: await spawnPass(),
|
|
39795
|
+
didDropReactHooksJsPlugin: false,
|
|
39796
|
+
hadPartialFailure
|
|
39797
|
+
};
|
|
39798
|
+
} catch (error) {
|
|
39799
|
+
const reactHooksJsDropNote = reactHooksJsPluginDropNote(error);
|
|
39800
|
+
if (reactHooksJsDropNote === null) throw error;
|
|
39801
|
+
writeOxlintConfig(passConfigPath, buildConfigForPass({ disableReactHooksJsPlugin: true }));
|
|
39802
|
+
const diagnostics = await spawnPass();
|
|
39803
|
+
reportPartialFailure(reactHooksJsDropNote);
|
|
39804
|
+
return {
|
|
39805
|
+
diagnostics,
|
|
39806
|
+
didDropReactHooksJsPlugin: true,
|
|
39807
|
+
hadPartialFailure
|
|
39808
|
+
};
|
|
39809
|
+
}
|
|
39810
|
+
};
|
|
39811
|
+
if (perFileLintCacheEnabled && respectInlineDisables && !project.hasReactCompiler && extendsPaths.length === 0 && userPlugins.length === 0) {
|
|
39812
|
+
const rulesetHash = computeRulesetHash({
|
|
39813
|
+
config: buildConfig({
|
|
39814
|
+
extendsPaths: [],
|
|
39815
|
+
ruleSelection: "cacheable"
|
|
39816
|
+
}),
|
|
39817
|
+
toolchainVersions: resolveOxlintToolchainVersions(),
|
|
39818
|
+
ignorePatterns: combinedPatterns,
|
|
39819
|
+
tsconfigContent
|
|
39820
|
+
});
|
|
39821
|
+
const cache = createFileLintCache(resolveReactDoctorCacheDir(rootDirectory), rulesetHash);
|
|
39822
|
+
const cacheKeyByFile = /* @__PURE__ */ new Map();
|
|
39823
|
+
const missFiles = [];
|
|
39824
|
+
const replayedDiagnostics = [];
|
|
39825
|
+
for (const candidateFile of candidateFiles) {
|
|
39826
|
+
const contentHash = hashFileContents(Path.resolve(rootDirectory, candidateFile));
|
|
39827
|
+
if (contentHash === null) {
|
|
39828
|
+
missFiles.push(candidateFile);
|
|
39829
|
+
continue;
|
|
39830
|
+
}
|
|
39831
|
+
const cacheKey = `${candidateFile.replaceAll("\\", "/")}${contentHash}`;
|
|
39832
|
+
cacheKeyByFile.set(candidateFile, cacheKey);
|
|
39833
|
+
const cachedDiagnostics = cache.lookup(cacheKey);
|
|
39834
|
+
if (cachedDiagnostics === null) missFiles.push(candidateFile);
|
|
39835
|
+
else replayedDiagnostics.push(...cachedDiagnostics);
|
|
39836
|
+
}
|
|
39837
|
+
const cacheHitFileCount = candidateFiles.length - missFiles.length;
|
|
39838
|
+
const cacheableResult = await runConfigOverFiles((overrides) => buildConfig({
|
|
39839
|
+
extendsPaths: [],
|
|
39840
|
+
ruleSelection: "cacheable",
|
|
39841
|
+
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
|
|
39842
|
+
}), "oxlintrc.cacheable.json", missFiles, void 0);
|
|
39843
|
+
const sidecarResult = await runConfigOverFiles(() => buildConfig({
|
|
39844
|
+
extendsPaths: [],
|
|
39845
|
+
ruleSelection: "sidecar"
|
|
39846
|
+
}), "oxlintrc.sidecar.json", candidateFiles, options.onFileProgress);
|
|
39847
|
+
onCacheStats?.(cacheHitFileCount, candidateFiles.length);
|
|
39848
|
+
const missFileByNormalizedPath = /* @__PURE__ */ new Map();
|
|
39849
|
+
for (const missFile of missFiles) missFileByNormalizedPath.set(missFile.replaceAll("\\", "/"), missFile);
|
|
39850
|
+
const freshDiagnosticsByFile = /* @__PURE__ */ new Map();
|
|
39851
|
+
let isAttributionSound = true;
|
|
39852
|
+
for (const diagnostic of cacheableResult.diagnostics) {
|
|
39853
|
+
const missFile = missFileByNormalizedPath.get(diagnostic.filePath);
|
|
39854
|
+
if (missFile === void 0) {
|
|
39855
|
+
isAttributionSound = false;
|
|
39856
|
+
break;
|
|
39857
|
+
}
|
|
39858
|
+
const fileDiagnostics = freshDiagnosticsByFile.get(missFile) ?? [];
|
|
39859
|
+
fileDiagnostics.push(diagnostic);
|
|
39860
|
+
freshDiagnosticsByFile.set(missFile, fileDiagnostics);
|
|
39861
|
+
}
|
|
39862
|
+
if (!cacheableResult.didDropReactHooksJsPlugin && !cacheableResult.hadPartialFailure && isAttributionSound) {
|
|
39863
|
+
for (const missFile of missFiles) {
|
|
39864
|
+
const cacheKey = cacheKeyByFile.get(missFile);
|
|
39865
|
+
if (cacheKey !== void 0) cache.store(cacheKey, freshDiagnosticsByFile.get(missFile) ?? []);
|
|
39866
|
+
}
|
|
39867
|
+
cache.persist();
|
|
39868
|
+
}
|
|
39869
|
+
return dedupeDiagnostics([
|
|
39870
|
+
...replayedDiagnostics,
|
|
39871
|
+
...cacheableResult.diagnostics,
|
|
39872
|
+
...sidecarResult.diagnostics
|
|
39873
|
+
]);
|
|
39137
39874
|
}
|
|
39138
|
-
const
|
|
39875
|
+
const baseArgs = makeBaseArgs(configPath);
|
|
39876
|
+
const fileBatches = batchIncludePaths(baseArgs, candidateFiles);
|
|
39139
39877
|
const runBatches = () => spawnLintBatches({
|
|
39140
39878
|
baseArgs,
|
|
39141
39879
|
fileBatches,
|
|
@@ -39146,14 +39884,25 @@ const runOxlint = async (options) => {
|
|
|
39146
39884
|
onFileProgress: options.onFileProgress,
|
|
39147
39885
|
spawnTimeoutMs,
|
|
39148
39886
|
outputMaxBytes,
|
|
39149
|
-
concurrency: options.concurrency
|
|
39887
|
+
concurrency: options.concurrency,
|
|
39888
|
+
signal: options.signal
|
|
39150
39889
|
});
|
|
39151
|
-
writeOxlintConfig(configPath, buildConfig(extendsPaths));
|
|
39890
|
+
writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
|
|
39152
39891
|
try {
|
|
39153
39892
|
return await runBatches();
|
|
39154
39893
|
} catch (error) {
|
|
39894
|
+
const reactHooksJsDropNote = reactHooksJsPluginDropNote(error);
|
|
39895
|
+
if (reactHooksJsDropNote !== null) {
|
|
39896
|
+
writeOxlintConfig(configPath, buildConfig({
|
|
39897
|
+
extendsPaths,
|
|
39898
|
+
disableReactHooksJsPlugin: true
|
|
39899
|
+
}));
|
|
39900
|
+
const diagnostics = await runBatches();
|
|
39901
|
+
onPartialFailure?.(reactHooksJsDropNote);
|
|
39902
|
+
return diagnostics;
|
|
39903
|
+
}
|
|
39155
39904
|
if (extendsPaths.length === 0) throw error;
|
|
39156
|
-
writeOxlintConfig(configPath, buildConfig([]));
|
|
39905
|
+
writeOxlintConfig(configPath, buildConfig({ extendsPaths: [] }));
|
|
39157
39906
|
return await runBatches();
|
|
39158
39907
|
}
|
|
39159
39908
|
} finally {
|
|
@@ -39215,9 +39964,11 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
|
|
|
39215
39964
|
const spawnTimeoutMs = yield* OxlintSpawnTimeoutMs;
|
|
39216
39965
|
const outputMaxBytes = yield* OxlintOutputMaxBytes;
|
|
39217
39966
|
const concurrency = yield* OxlintConcurrency;
|
|
39967
|
+
const lintBatchOrdering = yield* LintBatchOrdering;
|
|
39968
|
+
const perFileLintCacheEnabled = yield* PerFileLintCacheEnabled;
|
|
39218
39969
|
const collectedFailures = [];
|
|
39219
39970
|
const diagnostics = yield* tryPromise({
|
|
39220
|
-
try: () => runOxlint({
|
|
39971
|
+
try: (signal) => runOxlint({
|
|
39221
39972
|
rootDirectory: input.rootDirectory,
|
|
39222
39973
|
project: input.project,
|
|
39223
39974
|
includePaths: input.includePaths ? [...input.includePaths] : void 0,
|
|
@@ -39232,9 +39983,13 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
|
|
|
39232
39983
|
collectedFailures.push(reason);
|
|
39233
39984
|
},
|
|
39234
39985
|
onFileProgress: input.onFileProgress,
|
|
39986
|
+
perFileLintCacheEnabled,
|
|
39987
|
+
onCacheStats: input.onCacheStats,
|
|
39235
39988
|
spawnTimeoutMs,
|
|
39236
39989
|
outputMaxBytes,
|
|
39237
|
-
concurrency
|
|
39990
|
+
concurrency,
|
|
39991
|
+
signal,
|
|
39992
|
+
lintBatchOrdering
|
|
39238
39993
|
}),
|
|
39239
39994
|
catch: ensureReactDoctorError
|
|
39240
39995
|
});
|
|
@@ -39626,14 +40381,46 @@ const parseArtifactFromBody = (body) => {
|
|
|
39626
40381
|
}
|
|
39627
40382
|
return null;
|
|
39628
40383
|
};
|
|
39629
|
-
const
|
|
40384
|
+
const isSupplyChainCacheDisabled = () => {
|
|
40385
|
+
const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
|
|
40386
|
+
return noCache === "1" || noCache === "true";
|
|
40387
|
+
};
|
|
40388
|
+
const supplyChainCacheFile = (cacheDirectory, dependency) => {
|
|
40389
|
+
const purlHash = crypto.createHash("sha256").update(toPurl(dependency)).digest("hex").slice(0, 16);
|
|
40390
|
+
return Path.join(cacheDirectory, SUPPLY_CHAIN_CACHE_SUBDIR, `${purlHash}.json`);
|
|
40391
|
+
};
|
|
40392
|
+
const readCachedSocketBody = (cacheFile) => {
|
|
40393
|
+
try {
|
|
40394
|
+
const entry = JSON.parse(NFS.readFileSync(cacheFile, "utf-8"));
|
|
40395
|
+
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;
|
|
40396
|
+
} catch {}
|
|
40397
|
+
return null;
|
|
40398
|
+
};
|
|
40399
|
+
const writeCachedSocketBody = (cacheFile, body) => {
|
|
40400
|
+
try {
|
|
40401
|
+
NFS.mkdirSync(Path.dirname(cacheFile), { recursive: true });
|
|
40402
|
+
NFS.writeFileSync(cacheFile, JSON.stringify({
|
|
40403
|
+
fetchedAtMs: Date.now(),
|
|
40404
|
+
body
|
|
40405
|
+
}));
|
|
40406
|
+
} catch {}
|
|
40407
|
+
};
|
|
40408
|
+
const fetchSocketArtifact = (dependency, cacheDirectory) => tryPromise(async (signal) => {
|
|
40409
|
+
const cacheFile = cacheDirectory === null ? null : supplyChainCacheFile(cacheDirectory, dependency);
|
|
40410
|
+
if (cacheFile !== null) {
|
|
40411
|
+
const cachedBody = readCachedSocketBody(cacheFile);
|
|
40412
|
+
if (cachedBody !== null) return parseArtifactFromBody(cachedBody);
|
|
40413
|
+
}
|
|
39630
40414
|
const requestUrl = `${SOCKET_FREE_PURL_API_BASE}/${encodeURIComponent(toPurl(dependency))}`;
|
|
39631
40415
|
const response = await fetch(requestUrl, {
|
|
39632
40416
|
headers: { "User-Agent": SOCKET_FREE_USER_AGENT },
|
|
39633
40417
|
signal
|
|
39634
40418
|
});
|
|
39635
40419
|
if (!response.ok) return null;
|
|
39636
|
-
|
|
40420
|
+
const body = await response.text();
|
|
40421
|
+
const artifact = parseArtifactFromBody(body);
|
|
40422
|
+
if (artifact !== null && cacheFile !== null) writeCachedSocketBody(cacheFile, body);
|
|
40423
|
+
return artifact;
|
|
39637
40424
|
}).pipe(timeout(FETCH_TIMEOUT_MS), orElseSucceed(() => null), tap$1((artifact) => {
|
|
39638
40425
|
const scoreAttributes = {};
|
|
39639
40426
|
if (artifact !== null) {
|
|
@@ -39738,7 +40525,8 @@ const checkSupplyChain = (input) => gen(function* () {
|
|
|
39738
40525
|
const packageJsonPath = Path.join(input.rootDirectory, "package.json");
|
|
39739
40526
|
const dependencies = collectDependenciesToScore(readPackageJson(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies);
|
|
39740
40527
|
if (dependencies.length === 0) return [];
|
|
39741
|
-
const
|
|
40528
|
+
const cacheDirectory = isSupplyChainCacheDisabled() ? null : resolveReactDoctorCacheDir(input.rootDirectory);
|
|
40529
|
+
const artifacts = yield* forEach$1(dependencies, (dependency) => fetchSocketArtifact(dependency, cacheDirectory), { concurrency: 8 }).pipe(timeoutOption(input.totalTimeoutMs ?? 9e4), map$3((maybeArtifacts) => getOrElse$1(maybeArtifacts, () => [])));
|
|
39742
40530
|
const diagnostics = [];
|
|
39743
40531
|
for (let index = 0; index < dependencies.length; index += 1) {
|
|
39744
40532
|
const artifact = artifacts[index];
|
|
@@ -39763,6 +40551,10 @@ const checkSupplyChain = (input) => gen(function* () {
|
|
|
39763
40551
|
* The underlying `checkSupplyChain` Effect is total/fail-open — per-package
|
|
39764
40552
|
* timeouts and network failures recover to "skip" — so the stream never
|
|
39765
40553
|
* fails, mirroring `DeadCode`'s stream shape so the two compose the same way.
|
|
40554
|
+
* The orchestrator (`run-inspect.ts`) consumes this stream on a background
|
|
40555
|
+
* fiber whose network time overlaps the lint pass, joined under a generous
|
|
40556
|
+
* wall-clock budget; a budget expiry is the same fail-open outcome as a Socket
|
|
40557
|
+
* outage.
|
|
39766
40558
|
*/
|
|
39767
40559
|
var SupplyChain = class SupplyChain extends Service()("react-doctor/SupplyChain") {
|
|
39768
40560
|
static layerNode = succeed$3(SupplyChain, SupplyChain.of({ run: (input) => unwrap(checkSupplyChain(input).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), withSpan("SupplyChain.run"))) }));
|
|
@@ -39821,18 +40613,42 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
|
|
|
39821
40613
|
*
|
|
39822
40614
|
* Phases:
|
|
39823
40615
|
*
|
|
39824
|
-
* 1. Config.resolve(directory) → Project.discover → Git metadata
|
|
40616
|
+
* 1. Config.resolve(directory) → Project.discover → Git metadata.
|
|
40617
|
+
* The GitHub viewer-permission lookup is forked onto a background
|
|
40618
|
+
* fiber here and joined late (it feeds score metadata, not
|
|
40619
|
+
* diagnostics).
|
|
39825
40620
|
* 2. beforeLint hook (e.g. CLI renders the project-detection block)
|
|
39826
40621
|
* 3. environment checks (reduced-motion + pnpm hardening +
|
|
39827
|
-
* expo/react-native + security scan)
|
|
39828
|
-
* 4.
|
|
39829
|
-
*
|
|
39830
|
-
*
|
|
39831
|
-
*
|
|
39832
|
-
*
|
|
39833
|
-
*
|
|
39834
|
-
*
|
|
39835
|
-
*
|
|
40622
|
+
* expo/react-native + security scan), collected synchronously
|
|
40623
|
+
* 4. The supply-chain check (Socket.dev) is forked onto a background
|
|
40624
|
+
* fiber so its ~100% network-bound time overlaps the ~100%
|
|
40625
|
+
* CPU/subprocess-bound lint pass below, collapsing two serial
|
|
40626
|
+
* phases into roughly `max(supplyChain, lint)`. It is capped by
|
|
40627
|
+
* `SupplyChainOverlapTimeoutMs` (measured from fork) so a hung
|
|
40628
|
+
* socket can't drag out its join; on timeout it fails open to no
|
|
40629
|
+
* diagnostics — the same outcome class as a Socket outage.
|
|
40630
|
+
* 5. Linter.run runs; DeadCode.run runs concurrently (forked child
|
|
40631
|
+
* fiber) ONLY when the memory gate has headroom to run the 8 GB
|
|
40632
|
+
* dead-code child alongside the oxlint workers — or when overlap is
|
|
40633
|
+
* forced via REACT_DOCTOR_DEAD_CODE_OVERLAP. Otherwise dead-code
|
|
40634
|
+
* runs sequentially after lint, exactly as it did pre-overlap. The
|
|
40635
|
+
* fiber is joined (or interrupted, SIGKILLing its worker, on lint
|
|
40636
|
+
* failure) before diagnostics are concatenated. The afterLint hook
|
|
40637
|
+
* fires between lint and dead-code. Progress spinner labels AND the
|
|
40638
|
+
* final diagnostic / score order stay independent of execution
|
|
40639
|
+
* order, so terminal output is identical either way; supply-chain
|
|
40640
|
+
* rides alongside without a spinner.
|
|
40641
|
+
* 6. Join the supply-chain fiber, then assemble the diagnostics in a
|
|
40642
|
+
* FIXED order (env, supply-chain, lint, dead-code) so the output is
|
|
40643
|
+
* byte-identical regardless of which fiber settled first. The
|
|
40644
|
+
* viewer-permission fiber is joined later, during score-metadata
|
|
40645
|
+
* assembly (it feeds score metadata, not diagnostics). The per-element
|
|
40646
|
+
* `Reporter.emit` side-channel now interleaves supply-chain with lint
|
|
40647
|
+
* emits, so capture-order assertions must target the deterministic
|
|
40648
|
+
* concat below, not emit order (production `Reporter.layerNoop` makes
|
|
40649
|
+
* emit a no-op).
|
|
40650
|
+
* 7. Reporter.finalize
|
|
40651
|
+
* 8. Score.compute against the surface-filtered diagnostic set
|
|
39836
40652
|
*
|
|
39837
40653
|
* The orchestrator owns spinner lifecycle via `Progress`; callers
|
|
39838
40654
|
* choose `Progress.layerOra(...)` for CLI feedback or
|
|
@@ -39890,10 +40706,21 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
39890
40706
|
ignoredTags: input.ignoredTags
|
|
39891
40707
|
})
|
|
39892
40708
|
])));
|
|
39893
|
-
const
|
|
40709
|
+
const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
|
|
40710
|
+
const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
|
|
40711
|
+
const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
|
|
39894
40712
|
rootDirectory: scanDirectory,
|
|
39895
40713
|
userConfig: resolvedConfig.config
|
|
39896
|
-
})))
|
|
40714
|
+
}))).pipe(map$3((diagnostics) => ({
|
|
40715
|
+
diagnostics,
|
|
40716
|
+
timedOut: false
|
|
40717
|
+
})), timeout(supplyChainOverlapTimeout), orElseSucceed(() => ({
|
|
40718
|
+
diagnostics: [],
|
|
40719
|
+
timedOut: true
|
|
40720
|
+
}))) : succeed$2({
|
|
40721
|
+
diagnostics: [],
|
|
40722
|
+
timedOut: false
|
|
40723
|
+
}));
|
|
39897
40724
|
const lintFailure = yield* make$13({
|
|
39898
40725
|
didFail: false,
|
|
39899
40726
|
reason: null,
|
|
@@ -39904,12 +40731,49 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
39904
40731
|
didFail: false,
|
|
39905
40732
|
reason: null
|
|
39906
40733
|
});
|
|
39907
|
-
const scanConcurrency = yield* OxlintConcurrency;
|
|
40734
|
+
const scanConcurrency = resolveScanConcurrency(yield* OxlintConcurrency);
|
|
40735
|
+
const lintPhaseTimeoutMs = yield* LintPhaseTimeoutMs;
|
|
40736
|
+
const deadCodePhaseTimeoutMs = yield* DeadCodePhaseTimeoutMs;
|
|
40737
|
+
const resolveDeadCodePhaseTimeoutMs = (scaledPhaseTimeoutMs) => deadCodePhaseTimeoutMs === 15e4 ? scaledPhaseTimeoutMs : deadCodePhaseTimeoutMs;
|
|
39908
40738
|
const workerCountSuffix = scanConcurrency > 1 ? ` ${highlighter.dim(`[~${scanConcurrency} workers]`)}` : "";
|
|
40739
|
+
const shouldRunDeadCode = input.runDeadCode && !isDiffMode && (showWarnings || deadCodeMaySurfaceWhenWarningsHidden(resolvedConfig.config));
|
|
40740
|
+
const deadCodeOverlapMode = yield* DeadCodeOverlap;
|
|
40741
|
+
const shouldOverlapDeadCode = shouldRunDeadCode && deadCodeOverlapMode === "on";
|
|
40742
|
+
const deadCodeParseConcurrency = shouldOverlapDeadCode ? Math.max(1, Math.floor(scanConcurrency * DEAD_CODE_OVERLAP_PARSE_SHARE)) : void 0;
|
|
40743
|
+
const lintConcurrency = deadCodeParseConcurrency === void 0 ? scanConcurrency : Math.max(1, scanConcurrency - deadCodeParseConcurrency);
|
|
40744
|
+
const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
|
|
40745
|
+
rootDirectory: scanDirectory,
|
|
40746
|
+
userConfig: resolvedConfig.config,
|
|
40747
|
+
parseConcurrency: deadCodeParseConcurrency,
|
|
40748
|
+
workerTimeoutMs: deadCodeTimeout.workerTimeoutMs
|
|
40749
|
+
}).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
|
|
40750
|
+
yield* set(deadCodeFailure, {
|
|
40751
|
+
didFail: true,
|
|
40752
|
+
reason: error.message
|
|
40753
|
+
});
|
|
40754
|
+
return empty$4;
|
|
40755
|
+
})))))).pipe(timeoutOption(deadCodeTimeout.phaseTimeoutMs), flatMap$2(match$3({
|
|
40756
|
+
onNone: () => set(deadCodeFailure, {
|
|
40757
|
+
didFail: true,
|
|
40758
|
+
reason: `Dead-code analysis exceeded ${Math.round(deadCodeTimeout.phaseTimeoutMs / MILLISECONDS_PER_SECOND)}s and was skipped.`
|
|
40759
|
+
}).pipe(as([])),
|
|
40760
|
+
onSome: succeed$2
|
|
40761
|
+
})));
|
|
40762
|
+
const overlapDeadCodeTimeout = resolveDeadCodeTimeout({
|
|
40763
|
+
sourceFileCount: project.sourceFileCount,
|
|
40764
|
+
deadCodeConcurrency: deadCodeParseConcurrency ?? scanConcurrency,
|
|
40765
|
+
fullConcurrency: scanConcurrency
|
|
40766
|
+
});
|
|
40767
|
+
const deadCodeFiber = shouldOverlapDeadCode ? yield* forkChild(buildCollectDeadCode({
|
|
40768
|
+
workerTimeoutMs: overlapDeadCodeTimeout.workerTimeoutMs,
|
|
40769
|
+
phaseTimeoutMs: resolveDeadCodePhaseTimeoutMs(overlapDeadCodeTimeout.phaseTimeoutMs)
|
|
40770
|
+
})) : null;
|
|
39909
40771
|
const scanProgress = yield* progressService.start("Scanning...");
|
|
39910
40772
|
const scanStartTime = Date.now();
|
|
39911
40773
|
let lastReportedTotalFileCount = 0;
|
|
39912
|
-
|
|
40774
|
+
let lintCacheHitFileCount = null;
|
|
40775
|
+
let lintCacheTotalFileCount = null;
|
|
40776
|
+
const baseLintStream = linterService.run({
|
|
39913
40777
|
rootDirectory: scanDirectory,
|
|
39914
40778
|
project,
|
|
39915
40779
|
includePaths: lintIncludePaths ?? void 0,
|
|
@@ -39923,6 +40787,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
39923
40787
|
onFileProgress: (scannedFileCount, totalFileCount) => {
|
|
39924
40788
|
lastReportedTotalFileCount = totalFileCount;
|
|
39925
40789
|
runSync(scanProgress.update(`Scanning files (${scannedFileCount}/${totalFileCount})${workerCountSuffix}...`));
|
|
40790
|
+
},
|
|
40791
|
+
onCacheStats: (cacheHitFileCount, totalConsideredFileCount) => {
|
|
40792
|
+
lintCacheHitFileCount = cacheHitFileCount;
|
|
40793
|
+
lintCacheTotalFileCount = totalConsideredFileCount;
|
|
39926
40794
|
}
|
|
39927
40795
|
}).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
|
|
39928
40796
|
yield* set(lintFailure, {
|
|
@@ -39932,36 +40800,54 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
39932
40800
|
reasonKind: error.reason._tag === "OxlintUnavailable" ? error.reason.kind : null
|
|
39933
40801
|
});
|
|
39934
40802
|
return empty$4;
|
|
39935
|
-
}))))
|
|
40803
|
+
}))));
|
|
40804
|
+
const lintCollected = yield* runCollect(applyPerElementPipeline(shouldOverlapDeadCode ? baseLintStream.pipe(provideService(OxlintConcurrency, lintConcurrency)) : baseLintStream)).pipe(timeoutOption(lintPhaseTimeoutMs), flatMap$2(match$3({
|
|
40805
|
+
onNone: () => set(lintFailure, {
|
|
40806
|
+
didFail: true,
|
|
40807
|
+
reason: `Lint analysis exceeded ${lintPhaseTimeoutMs / MILLISECONDS_PER_SECOND}s and was skipped.`,
|
|
40808
|
+
reasonTag: "OxlintBatchExceeded",
|
|
40809
|
+
reasonKind: null
|
|
40810
|
+
}).pipe(as([])),
|
|
40811
|
+
onSome: succeed$2
|
|
40812
|
+
})));
|
|
39936
40813
|
const lintFailureState = yield* get$2(lintFailure);
|
|
39937
40814
|
yield* afterLint(lintFailureState.didFail);
|
|
39938
40815
|
if (lintFailureState.didFail) yield* scanProgress.fail(formatLintFailText(lintFailureState.reasonTag, process.version));
|
|
39939
40816
|
const totalFileCount = lastReportedTotalFileCount || (lintIncludePaths?.length ?? project.sourceFileCount);
|
|
39940
40817
|
const scannedFilesLabel = `${totalFileCount} ${totalFileCount === 1 ? "file" : "files"}`;
|
|
39941
|
-
|
|
39942
|
-
|
|
39943
|
-
|
|
39944
|
-
|
|
39945
|
-
|
|
39946
|
-
|
|
39947
|
-
|
|
39948
|
-
|
|
40818
|
+
let deadCodeCollected = [];
|
|
40819
|
+
if (lintFailureState.didFail) {
|
|
40820
|
+
if (deadCodeFiber !== null) yield* interrupt(deadCodeFiber);
|
|
40821
|
+
} else if (shouldRunDeadCode) {
|
|
40822
|
+
yield* scanProgress.update(`Scanned ${scannedFilesLabel}, analyzing dead code...`);
|
|
40823
|
+
const sequentialDeadCodeTimeout = resolveDeadCodeTimeout({
|
|
40824
|
+
sourceFileCount: totalFileCount,
|
|
40825
|
+
deadCodeConcurrency: scanConcurrency,
|
|
40826
|
+
fullConcurrency: scanConcurrency
|
|
39949
40827
|
});
|
|
39950
|
-
|
|
39951
|
-
|
|
39952
|
-
|
|
40828
|
+
deadCodeCollected = deadCodeFiber !== null ? yield* join(deadCodeFiber) : yield* buildCollectDeadCode({
|
|
40829
|
+
workerTimeoutMs: sequentialDeadCodeTimeout.workerTimeoutMs,
|
|
40830
|
+
phaseTimeoutMs: resolveDeadCodePhaseTimeoutMs(sequentialDeadCodeTimeout.phaseTimeoutMs)
|
|
40831
|
+
});
|
|
40832
|
+
}
|
|
40833
|
+
const deadCodeFailureState = lintFailureState.didFail ? {
|
|
40834
|
+
didFail: false,
|
|
40835
|
+
reason: null
|
|
40836
|
+
} : yield* get$2(deadCodeFailure);
|
|
39953
40837
|
const scanElapsedMilliseconds = Date.now() - scanStartTime;
|
|
39954
|
-
const scanElapsedSeconds = (scanElapsedMilliseconds /
|
|
40838
|
+
const scanElapsedSeconds = (scanElapsedMilliseconds / MILLISECONDS_PER_SECOND).toFixed(1);
|
|
39955
40839
|
if (!lintFailureState.didFail) if (deadCodeFailureState.didFail) yield* scanProgress.fail(DEAD_CODE_FAIL_TEXT);
|
|
39956
40840
|
else if (input.suppressScanSummary) yield* scanProgress.stop();
|
|
39957
40841
|
else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
|
|
40842
|
+
const supplyChainResult = yield* join(supplyChainFiber);
|
|
40843
|
+
const supplyChainCollected = supplyChainResult.diagnostics;
|
|
39958
40844
|
yield* reporterService.finalize;
|
|
39959
|
-
const finalDiagnostics = [
|
|
40845
|
+
const finalDiagnostics = sortDiagnosticsStable(assignFixGroups([
|
|
39960
40846
|
...envCollected,
|
|
39961
40847
|
...supplyChainCollected,
|
|
39962
40848
|
...lintCollected,
|
|
39963
40849
|
...deadCodeCollected
|
|
39964
|
-
];
|
|
40850
|
+
]));
|
|
39965
40851
|
const githubViewerPermission = yield* join(githubViewerPermissionFiber);
|
|
39966
40852
|
const scoreMetadata = {
|
|
39967
40853
|
...repo !== null ? { repo } : {},
|
|
@@ -39997,9 +40883,14 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
39997
40883
|
lintPartialFailures,
|
|
39998
40884
|
didDeadCodeFail: deadCodeFailureState.didFail,
|
|
39999
40885
|
deadCodeFailureReason: deadCodeFailureState.reason,
|
|
40886
|
+
deadCodeOverlapped: shouldOverlapDeadCode,
|
|
40000
40887
|
scannedFileCount: totalFileCount,
|
|
40001
40888
|
scannedFilePaths,
|
|
40002
|
-
scanElapsedMilliseconds
|
|
40889
|
+
scanElapsedMilliseconds,
|
|
40890
|
+
scanConcurrency,
|
|
40891
|
+
supplyChainOverlapTimedOut: supplyChainResult.timedOut,
|
|
40892
|
+
lintCacheHitFileCount,
|
|
40893
|
+
lintCacheTotalFileCount
|
|
40003
40894
|
};
|
|
40004
40895
|
}).pipe(withSpan("runInspect", { attributes: {
|
|
40005
40896
|
"inspect.directory": input.directory,
|
|
@@ -40007,7 +40898,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
40007
40898
|
"inspect.runDeadCode": input.runDeadCode,
|
|
40008
40899
|
"inspect.isCi": input.isCi,
|
|
40009
40900
|
"inspect.scoreSurface": input.scoreSurface ?? "score"
|
|
40010
|
-
} }));
|
|
40901
|
+
} }), (scanProgram) => flatMap$2(ScanDeadlineMs, (scanDeadlineMs) => scanProgram.pipe(timeout(scanDeadlineMs), catchTag$1("TimeoutError", () => new ReactDoctorError({ reason: new ScanDeadlineExceeded({ detail: `${scanDeadlineMs / MILLISECONDS_PER_SECOND}s elapsed` }) })))));
|
|
40011
40902
|
const parseNodeVersion = (versionString) => {
|
|
40012
40903
|
const [major = 0, minor = 0, patch = 0] = versionString.replace(/^v/, "").trim().split(".").map(Number);
|
|
40013
40904
|
return {
|
|
@@ -40165,7 +41056,7 @@ const materializeSourceTree = (input) => gen(function* () {
|
|
|
40165
41056
|
static layerNode = effect(StagedFiles, gen(function* () {
|
|
40166
41057
|
const git = yield* Git;
|
|
40167
41058
|
return StagedFiles.of({
|
|
40168
|
-
discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(map$3((entries) => entries.filter(isLintableSourceFile))),
|
|
41059
|
+
discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(map$3((entries) => entries.filter(isLintableSourceFile)), withSpan("StagedFiles.discoverSourceFiles")),
|
|
40169
41060
|
materialize: ({ directory, stagedFiles, tempDirectory }) => materializeSourceTree({
|
|
40170
41061
|
directory,
|
|
40171
41062
|
files: stagedFiles,
|
|
@@ -40175,7 +41066,7 @@ const materializeSourceTree = (input) => gen(function* () {
|
|
|
40175
41066
|
tempDirectory: tree.tempDirectory,
|
|
40176
41067
|
stagedFiles: tree.materializedFiles,
|
|
40177
41068
|
cleanup: tree.cleanup
|
|
40178
|
-
})))
|
|
41069
|
+
})), withSpan("StagedFiles.materialize"))
|
|
40179
41070
|
});
|
|
40180
41071
|
}));
|
|
40181
41072
|
/**
|
|
@@ -40307,6 +41198,7 @@ const buildJsonReport = (input) => {
|
|
|
40307
41198
|
score: result.score,
|
|
40308
41199
|
skippedChecks: result.skippedChecks,
|
|
40309
41200
|
...result.skippedCheckReasons ? { skippedCheckReasons: result.skippedCheckReasons } : {},
|
|
41201
|
+
...typeof result.scannedFileCount === "number" ? { scannedFileCount: result.scannedFileCount } : {},
|
|
40310
41202
|
elapsedMilliseconds: result.elapsedMilliseconds
|
|
40311
41203
|
}));
|
|
40312
41204
|
const flattenedDiagnostics = projects.flatMap((entry) => entry.diagnostics);
|
|
@@ -40550,6 +41442,7 @@ const clearCaches = () => {
|
|
|
40550
41442
|
clearIgnorePatternsCache();
|
|
40551
41443
|
clearPackageRoleCache();
|
|
40552
41444
|
clearAutoSuppressionCaches();
|
|
41445
|
+
clearMinifiedFileCache();
|
|
40553
41446
|
};
|
|
40554
41447
|
const toJsonReport = (result, options) => buildJsonReport({
|
|
40555
41448
|
version: options.version,
|
|
@@ -40573,4 +41466,4 @@ const toJsonReport = (result, options) => buildJsonReport({
|
|
|
40573
41466
|
export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
|
|
40574
41467
|
|
|
40575
41468
|
//# sourceMappingURL=index.js.map
|
|
40576
|
-
//# debugId=
|
|
41469
|
+
//# debugId=c1d3306c-dbf2-5096-b48e-5c9bb3128102
|