react-doctor 0.5.8-dev.80e3093 → 0.5.8-dev.8232e96
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 +575 -410
- package/dist/index.d.ts +12 -0
- package/dist/index.js +265 -227
- package/dist/lsp.js +287 -250
- package/package.json +5 -5
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="56ca0cbd-9f70-5a50-8496-43abdf078431")}catch(e){}}();
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import * as NodeChildProcess from "node:child_process";
|
|
5
5
|
import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
|
|
@@ -22465,6 +22465,15 @@ var JsonReportV1 = class extends Class("JsonReportV1")({
|
|
|
22465
22465
|
ok: Boolean$1,
|
|
22466
22466
|
directory: String$1,
|
|
22467
22467
|
mode: JsonReportMode,
|
|
22468
|
+
/**
|
|
22469
|
+
* True when a `changed` (compare) run was intended but the base couldn't be
|
|
22470
|
+
* resolved — a shallow CI checkout with no merge base, or a failed base/head
|
|
22471
|
+
* lint. The report then lists every finding in the changed files (mode
|
|
22472
|
+
* downgrades to `diff`, the `baseline` block is dropped, the CI gate is
|
|
22473
|
+
* skipped) rather than only the ones the change introduced. Absent on a
|
|
22474
|
+
* successful `baseline` run (`schemaVersion: 2`) and on non-compare scopes.
|
|
22475
|
+
*/
|
|
22476
|
+
baselineDegraded: optional(Boolean$1),
|
|
22468
22477
|
diff: NullOr(JsonReportDiffInfo),
|
|
22469
22478
|
projects: ArraySchema(JsonReportProjectEntry),
|
|
22470
22479
|
diagnostics: ArraySchema(Diagnostic),
|
|
@@ -35948,12 +35957,31 @@ const GENERATED_BUNDLE_FILE_PATTERN = /\.(iife|umd|global|min)\.m?js$/i;
|
|
|
35948
35957
|
const MINIFIED_SNIFF_BYTES = 65536;
|
|
35949
35958
|
const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
|
|
35950
35959
|
const IGNORED_DIRECTORIES = new Set([
|
|
35960
|
+
".angular",
|
|
35961
|
+
".astro",
|
|
35962
|
+
".cache",
|
|
35963
|
+
".contentlayer",
|
|
35964
|
+
".direnv",
|
|
35965
|
+
".docusaurus",
|
|
35966
|
+
".expo",
|
|
35967
|
+
".firebase",
|
|
35951
35968
|
".git",
|
|
35969
|
+
".gradle",
|
|
35970
|
+
".hg",
|
|
35952
35971
|
".next",
|
|
35953
35972
|
".nuxt",
|
|
35973
|
+
".nx",
|
|
35954
35974
|
".output",
|
|
35975
|
+
".parcel-cache",
|
|
35976
|
+
".serverless",
|
|
35955
35977
|
".svelte-kit",
|
|
35978
|
+
".svn",
|
|
35979
|
+
".terraform",
|
|
35956
35980
|
".turbo",
|
|
35981
|
+
".venv",
|
|
35982
|
+
".vercel",
|
|
35983
|
+
".wrangler",
|
|
35984
|
+
".yarn",
|
|
35957
35985
|
"build",
|
|
35958
35986
|
"coverage",
|
|
35959
35987
|
"dist",
|
|
@@ -35961,6 +35989,152 @@ const IGNORED_DIRECTORIES = new Set([
|
|
|
35961
35989
|
"out",
|
|
35962
35990
|
"storybook-static"
|
|
35963
35991
|
]);
|
|
35992
|
+
const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
|
|
35993
|
+
const MILLISECONDS_PER_SECOND = 1e3;
|
|
35994
|
+
const SCORE_API_URL = "https://www.react.doctor/api/score";
|
|
35995
|
+
const ENTERPRISE_CONTACT_URL = "https://react.doctor/enterprise";
|
|
35996
|
+
const SHARE_BASE_URL = "https://react.doctor/share";
|
|
35997
|
+
const CI_URL = "https://react.doctor/ci";
|
|
35998
|
+
const GITHUB_ACTIONS_SETUP_URL = "https://www.react.doctor/docs/ci-and-prs/github-actions-setup";
|
|
35999
|
+
const DOCS_URL = "https://react.doctor/docs";
|
|
36000
|
+
const DOCS_RULES_BASE_URL = `${DOCS_URL}/rules`;
|
|
36001
|
+
const FETCH_TIMEOUT_MS = 1e4;
|
|
36002
|
+
const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
|
|
36003
|
+
const SPAWN_ARGS_MAX_LENGTH_CHARS = 24e3;
|
|
36004
|
+
const SPAWN_ARGS_MAX_LENGTH_CHARS_POSIX = 15e5;
|
|
36005
|
+
const SPAWN_ARGS_MAX_LENGTH_CHARS_DARWIN = 8e5;
|
|
36006
|
+
const NODE_VERSION_PROBE_TIMEOUT_MS = 5e3;
|
|
36007
|
+
const PER_WORKER_MEM_BUDGET_BYTES = 1024 * 1024 * 1024;
|
|
36008
|
+
const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
|
|
36009
|
+
const ADOPTABLE_LINT_CONFIG_FILENAMES = [".oxlintrc.json", ".eslintrc.json"];
|
|
36010
|
+
const OXLINT_NODE_REQUIREMENT = "^20.19.0 || >=22.12.0";
|
|
36011
|
+
const GIT_SHOW_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
36012
|
+
const ES_TARGET_YEAR_BY_NAME = {
|
|
36013
|
+
es3: 1999,
|
|
36014
|
+
es5: 2009,
|
|
36015
|
+
es6: 2015,
|
|
36016
|
+
es2015: 2015,
|
|
36017
|
+
es2016: 2016,
|
|
36018
|
+
es2017: 2017,
|
|
36019
|
+
es2018: 2018,
|
|
36020
|
+
es2019: 2019,
|
|
36021
|
+
es2020: 2020,
|
|
36022
|
+
es2021: 2021,
|
|
36023
|
+
es2022: 2022,
|
|
36024
|
+
es2023: 2023,
|
|
36025
|
+
es2024: 2024,
|
|
36026
|
+
es2025: 2025,
|
|
36027
|
+
esnext: 9999
|
|
36028
|
+
};
|
|
36029
|
+
/**
|
|
36030
|
+
* tsconfig filenames probed when resolving a project's TypeScript
|
|
36031
|
+
* compiler options — the root config first, then a monorepo base config.
|
|
36032
|
+
*/
|
|
36033
|
+
const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
|
|
36034
|
+
/**
|
|
36035
|
+
* Project-config files that `StagedFiles.materialize` copies into
|
|
36036
|
+
* the temp directory alongside staged sources so oxlint resolves
|
|
36037
|
+
* `tsconfig` / `package.json` / lint configs the same way it would
|
|
36038
|
+
* in the working tree. Hoisted out of the staged-files helper so
|
|
36039
|
+
* the constant lives next to the rest of the IO budget knobs.
|
|
36040
|
+
*/
|
|
36041
|
+
const STAGED_FILES_PROJECT_CONFIG_FILENAMES = [
|
|
36042
|
+
"tsconfig.json",
|
|
36043
|
+
"tsconfig.base.json",
|
|
36044
|
+
"package.json",
|
|
36045
|
+
"doctor.config.ts",
|
|
36046
|
+
"doctor.config.mts",
|
|
36047
|
+
"doctor.config.cts",
|
|
36048
|
+
"doctor.config.js",
|
|
36049
|
+
"doctor.config.mjs",
|
|
36050
|
+
"doctor.config.cjs",
|
|
36051
|
+
"doctor.config.json",
|
|
36052
|
+
"doctor.config.jsonc",
|
|
36053
|
+
"oxlint.json",
|
|
36054
|
+
".oxlintrc.json"
|
|
36055
|
+
];
|
|
36056
|
+
const CONFIG_FINGERPRINT_FILENAMES = [
|
|
36057
|
+
"doctor.config.ts",
|
|
36058
|
+
"doctor.config.mts",
|
|
36059
|
+
"doctor.config.cts",
|
|
36060
|
+
"doctor.config.js",
|
|
36061
|
+
"doctor.config.mjs",
|
|
36062
|
+
"doctor.config.cjs",
|
|
36063
|
+
"doctor.config.json",
|
|
36064
|
+
"doctor.config.jsonc",
|
|
36065
|
+
"react-doctor.config.json",
|
|
36066
|
+
"package.json",
|
|
36067
|
+
"pnpm-workspace.yaml",
|
|
36068
|
+
"tsconfig.json",
|
|
36069
|
+
"tsconfig.base.json",
|
|
36070
|
+
".oxlintrc.json",
|
|
36071
|
+
".eslintrc.json",
|
|
36072
|
+
"pnpm-lock.yaml",
|
|
36073
|
+
"package-lock.json",
|
|
36074
|
+
"yarn.lock",
|
|
36075
|
+
"bun.lock",
|
|
36076
|
+
"bun.lockb",
|
|
36077
|
+
".gitignore"
|
|
36078
|
+
];
|
|
36079
|
+
const CANONICAL_GITHUB_URL = "https://github.com/millionco/react-doctor";
|
|
36080
|
+
const CANONICAL_DISCORD_URL = "https://react.doctor/discord";
|
|
36081
|
+
const SKILL_NAME = "react-doctor";
|
|
36082
|
+
const OXLINT_OUTPUT_MAX_BYTES = 50 * 1024 * 1024;
|
|
36083
|
+
const OXLINT_SPAWN_TIMEOUT_MS = 6e4;
|
|
36084
|
+
const NODE_COMPILE_CACHE_DIR_NAME = "node-compile-cache";
|
|
36085
|
+
const DEAD_CODE_WORKER_TIMEOUT_MS = 12e4;
|
|
36086
|
+
const OXLINT_SPLIT_TOTAL_BUDGET_MS = 18e4;
|
|
36087
|
+
const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
|
|
36088
|
+
const LINT_PHASE_TIMEOUT_MS = 3e5;
|
|
36089
|
+
const SCAN_TOTAL_DEADLINE_MS = 9e5;
|
|
36090
|
+
const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
|
|
36091
|
+
const DEAD_CODE_WORKER_MEM_BUDGET_BYTES = 2 * 1024 * 1024 * 1024;
|
|
36092
|
+
const DEAD_CODE_TIMEOUT_CEILING_MS = 6e5;
|
|
36093
|
+
const DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS = 3e4;
|
|
36094
|
+
const DEAD_CODE_OVERLAP_PARSE_SHARE = .4;
|
|
36095
|
+
const RECOMMENDED_PNPM_MINIMUM_RELEASE_AGE_MINUTES = 10080;
|
|
36096
|
+
const REACT_SERVER_DOM_PACKAGES = [
|
|
36097
|
+
"react-server-dom-webpack",
|
|
36098
|
+
"react-server-dom-parcel",
|
|
36099
|
+
"react-server-dom-turbopack"
|
|
36100
|
+
];
|
|
36101
|
+
const REACT_BLOG_RSC_ADVISORY_URL = "https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components";
|
|
36102
|
+
const VERCEL_NEXTJS_SECURITY_RELEASE_URL = "https://vercel.com/changelog/next-js-may-2026-security-release";
|
|
36103
|
+
const DIAGNOSTIC_CATEGORY_BUCKETS = [
|
|
36104
|
+
"Security",
|
|
36105
|
+
"Bugs",
|
|
36106
|
+
"Performance",
|
|
36107
|
+
"Accessibility",
|
|
36108
|
+
"Maintainability"
|
|
36109
|
+
];
|
|
36110
|
+
const APP_ONLY_RULE_KEYS = new Set([
|
|
36111
|
+
"react-hooks-js/static-components",
|
|
36112
|
+
"react-doctor/no-render-prop-children",
|
|
36113
|
+
"react-doctor/prefer-explicit-variants"
|
|
36114
|
+
]);
|
|
36115
|
+
const COMPILER_CLEANUP_BUCKET = "compiler-cleanup";
|
|
36116
|
+
const COMPILER_CLEANUP_RULE_KEYS = new Set(["react-doctor/react-compiler-no-manual-memoization"]);
|
|
36117
|
+
const ROOT_CAUSE_GROUPABLE_RULE_KEYS = new Set([
|
|
36118
|
+
"react-doctor/no-derived-state",
|
|
36119
|
+
"react-doctor/no-derived-state-effect",
|
|
36120
|
+
"react-doctor/no-derived-useState",
|
|
36121
|
+
"react-doctor/no-adjust-state-on-prop-change",
|
|
36122
|
+
"react-doctor/no-reset-all-state-on-prop-change"
|
|
36123
|
+
]);
|
|
36124
|
+
const MAX_GLOB_PATTERN_LENGTH_CHARS = 1024;
|
|
36125
|
+
const CONFIG_CACHE_TTL_MS = 300 * 1e3;
|
|
36126
|
+
const SOCKET_FREE_PURL_API_BASE = "https://firewall-api.socket.dev/purl";
|
|
36127
|
+
const SOCKET_PACKAGE_PAGE_BASE = "https://socket.dev/npm/package";
|
|
36128
|
+
const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
|
|
36129
|
+
const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
|
|
36130
|
+
const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
|
|
36131
|
+
const SUPPLY_CHAIN_PLUGIN = "socket";
|
|
36132
|
+
const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
|
|
36133
|
+
const SUPPLY_CHAIN_CATEGORY = "Security";
|
|
36134
|
+
const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
|
|
36135
|
+
const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
|
|
36136
|
+
const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
|
|
36137
|
+
const hasIgnoredPathSegment = (relativePath) => relativePath.split(/[/\\]/).slice(0, -1).some((segment) => IGNORED_DIRECTORIES.has(segment));
|
|
35964
36138
|
const isLintableSourceFile = (filePath) => SOURCE_FILE_PATTERN.test(filePath) && !GENERATED_BUNDLE_FILE_PATTERN.test(filePath);
|
|
35965
36139
|
const isMinifiedSource = (absolutePath) => {
|
|
35966
36140
|
let fileDescriptor;
|
|
@@ -36014,23 +36188,38 @@ const readDirectoryEntries = (directoryPath) => {
|
|
|
36014
36188
|
throw error;
|
|
36015
36189
|
}
|
|
36016
36190
|
};
|
|
36017
|
-
|
|
36018
|
-
let count = 0;
|
|
36191
|
+
function* walkSourceTreeFiles(rootDirectory) {
|
|
36019
36192
|
const stack = [rootDirectory];
|
|
36020
36193
|
while (stack.length > 0) {
|
|
36021
36194
|
const currentDirectory = stack.pop();
|
|
36022
|
-
|
|
36023
|
-
for (const entry of
|
|
36195
|
+
if (currentDirectory === void 0) continue;
|
|
36196
|
+
for (const entry of readDirectoryEntries(currentDirectory)) {
|
|
36197
|
+
const absolutePath = Path.join(currentDirectory, entry.name);
|
|
36024
36198
|
if (entry.isDirectory()) {
|
|
36025
|
-
if (!
|
|
36199
|
+
if (!IGNORED_DIRECTORIES.has(entry.name)) stack.push(absolutePath);
|
|
36026
36200
|
continue;
|
|
36027
36201
|
}
|
|
36028
|
-
if (entry.isFile()
|
|
36202
|
+
if (entry.isFile()) yield {
|
|
36203
|
+
absolutePath,
|
|
36204
|
+
name: entry.name
|
|
36205
|
+
};
|
|
36029
36206
|
}
|
|
36030
36207
|
}
|
|
36031
|
-
|
|
36208
|
+
}
|
|
36209
|
+
const collectSizedSourceFiles = (rootDirectory, relativePaths) => {
|
|
36210
|
+
const entries = [];
|
|
36211
|
+
for (const relativePath of relativePaths) {
|
|
36212
|
+
const absolutePath = Path.resolve(rootDirectory, relativePath);
|
|
36213
|
+
const sizeBytes = statSourceFileSize(absolutePath);
|
|
36214
|
+
if (isLargeMinifiedFile(absolutePath, sizeBytes)) continue;
|
|
36215
|
+
entries.push({
|
|
36216
|
+
path: relativePath,
|
|
36217
|
+
sizeBytes: sizeBytes ?? 0
|
|
36218
|
+
});
|
|
36219
|
+
}
|
|
36220
|
+
return entries;
|
|
36032
36221
|
};
|
|
36033
|
-
const
|
|
36222
|
+
const listSourceFilesViaGit = (rootDirectory) => {
|
|
36034
36223
|
const result = spawnSync("git", [
|
|
36035
36224
|
"ls-files",
|
|
36036
36225
|
"-z",
|
|
@@ -36043,9 +36232,16 @@ const countSourceFilesViaGit = (rootDirectory) => {
|
|
|
36043
36232
|
maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
|
|
36044
36233
|
});
|
|
36045
36234
|
if (result.error || result.status !== 0) return null;
|
|
36046
|
-
return result.stdout.split("\0").filter((filePath) => filePath.length > 0 && isLintableSourceFile(filePath) && !
|
|
36235
|
+
return result.stdout.split("\0").filter((filePath) => filePath.length > 0 && isLintableSourceFile(filePath) && !hasIgnoredPathSegment(filePath));
|
|
36236
|
+
};
|
|
36237
|
+
const listSourceFilesViaFilesystem = (rootDirectory) => {
|
|
36238
|
+
const filePaths = [];
|
|
36239
|
+
for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) if (isLintableSourceFile(name)) filePaths.push(Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/"));
|
|
36240
|
+
return filePaths;
|
|
36047
36241
|
};
|
|
36048
|
-
const
|
|
36242
|
+
const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
|
|
36243
|
+
const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
|
|
36244
|
+
const countSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).length;
|
|
36049
36245
|
const cachedPackageJsons = /* @__PURE__ */ new Map();
|
|
36050
36246
|
const readPackageJsonUncached = (packageJsonPath) => {
|
|
36051
36247
|
try {
|
|
@@ -36851,151 +37047,6 @@ const resolveEffectiveReactMajor = (reactVersion, packageJson) => {
|
|
|
36851
37047
|
if (peerFloor === null) return hasUpperBoundOnlyPeerRange(peerReactRange) ? null : installedReactMajor;
|
|
36852
37048
|
return installedReactMajor !== null ? Math.min(installedReactMajor, peerFloor) : peerFloor;
|
|
36853
37049
|
};
|
|
36854
|
-
const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
|
|
36855
|
-
const MILLISECONDS_PER_SECOND = 1e3;
|
|
36856
|
-
const SCORE_API_URL = "https://www.react.doctor/api/score";
|
|
36857
|
-
const ENTERPRISE_CONTACT_URL = "https://react.doctor/enterprise";
|
|
36858
|
-
const SHARE_BASE_URL = "https://react.doctor/share";
|
|
36859
|
-
const CI_URL = "https://react.doctor/ci";
|
|
36860
|
-
const GITHUB_ACTIONS_SETUP_URL = "https://www.react.doctor/docs/ci-and-prs/github-actions-setup";
|
|
36861
|
-
const DOCS_URL = "https://react.doctor/docs";
|
|
36862
|
-
const DOCS_RULES_BASE_URL = `${DOCS_URL}/rules`;
|
|
36863
|
-
const FETCH_TIMEOUT_MS = 1e4;
|
|
36864
|
-
const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
|
|
36865
|
-
const SPAWN_ARGS_MAX_LENGTH_CHARS = 24e3;
|
|
36866
|
-
const SPAWN_ARGS_MAX_LENGTH_CHARS_POSIX = 15e5;
|
|
36867
|
-
const SPAWN_ARGS_MAX_LENGTH_CHARS_DARWIN = 8e5;
|
|
36868
|
-
const NODE_VERSION_PROBE_TIMEOUT_MS = 5e3;
|
|
36869
|
-
const PER_WORKER_MEM_BUDGET_BYTES = 1024 * 1024 * 1024;
|
|
36870
|
-
const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
|
|
36871
|
-
const ADOPTABLE_LINT_CONFIG_FILENAMES = [".oxlintrc.json", ".eslintrc.json"];
|
|
36872
|
-
const OXLINT_NODE_REQUIREMENT = "^20.19.0 || >=22.12.0";
|
|
36873
|
-
const GIT_SHOW_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
36874
|
-
const ES_TARGET_YEAR_BY_NAME = {
|
|
36875
|
-
es3: 1999,
|
|
36876
|
-
es5: 2009,
|
|
36877
|
-
es6: 2015,
|
|
36878
|
-
es2015: 2015,
|
|
36879
|
-
es2016: 2016,
|
|
36880
|
-
es2017: 2017,
|
|
36881
|
-
es2018: 2018,
|
|
36882
|
-
es2019: 2019,
|
|
36883
|
-
es2020: 2020,
|
|
36884
|
-
es2021: 2021,
|
|
36885
|
-
es2022: 2022,
|
|
36886
|
-
es2023: 2023,
|
|
36887
|
-
es2024: 2024,
|
|
36888
|
-
es2025: 2025,
|
|
36889
|
-
esnext: 9999
|
|
36890
|
-
};
|
|
36891
|
-
/**
|
|
36892
|
-
* tsconfig filenames probed when resolving a project's TypeScript
|
|
36893
|
-
* compiler options — the root config first, then a monorepo base config.
|
|
36894
|
-
*/
|
|
36895
|
-
const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
|
|
36896
|
-
/**
|
|
36897
|
-
* Project-config files that `StagedFiles.materialize` copies into
|
|
36898
|
-
* the temp directory alongside staged sources so oxlint resolves
|
|
36899
|
-
* `tsconfig` / `package.json` / lint configs the same way it would
|
|
36900
|
-
* in the working tree. Hoisted out of the staged-files helper so
|
|
36901
|
-
* the constant lives next to the rest of the IO budget knobs.
|
|
36902
|
-
*/
|
|
36903
|
-
const STAGED_FILES_PROJECT_CONFIG_FILENAMES = [
|
|
36904
|
-
"tsconfig.json",
|
|
36905
|
-
"tsconfig.base.json",
|
|
36906
|
-
"package.json",
|
|
36907
|
-
"doctor.config.ts",
|
|
36908
|
-
"doctor.config.mts",
|
|
36909
|
-
"doctor.config.cts",
|
|
36910
|
-
"doctor.config.js",
|
|
36911
|
-
"doctor.config.mjs",
|
|
36912
|
-
"doctor.config.cjs",
|
|
36913
|
-
"doctor.config.json",
|
|
36914
|
-
"doctor.config.jsonc",
|
|
36915
|
-
"oxlint.json",
|
|
36916
|
-
".oxlintrc.json"
|
|
36917
|
-
];
|
|
36918
|
-
const CONFIG_FINGERPRINT_FILENAMES = [
|
|
36919
|
-
"doctor.config.ts",
|
|
36920
|
-
"doctor.config.mts",
|
|
36921
|
-
"doctor.config.cts",
|
|
36922
|
-
"doctor.config.js",
|
|
36923
|
-
"doctor.config.mjs",
|
|
36924
|
-
"doctor.config.cjs",
|
|
36925
|
-
"doctor.config.json",
|
|
36926
|
-
"doctor.config.jsonc",
|
|
36927
|
-
"react-doctor.config.json",
|
|
36928
|
-
"package.json",
|
|
36929
|
-
"pnpm-workspace.yaml",
|
|
36930
|
-
"tsconfig.json",
|
|
36931
|
-
"tsconfig.base.json",
|
|
36932
|
-
".oxlintrc.json",
|
|
36933
|
-
".eslintrc.json",
|
|
36934
|
-
"pnpm-lock.yaml",
|
|
36935
|
-
"package-lock.json",
|
|
36936
|
-
"yarn.lock",
|
|
36937
|
-
"bun.lock",
|
|
36938
|
-
"bun.lockb",
|
|
36939
|
-
".gitignore"
|
|
36940
|
-
];
|
|
36941
|
-
const CANONICAL_GITHUB_URL = "https://github.com/millionco/react-doctor";
|
|
36942
|
-
const CANONICAL_DISCORD_URL = "https://react.doctor/discord";
|
|
36943
|
-
const SKILL_NAME = "react-doctor";
|
|
36944
|
-
const OXLINT_OUTPUT_MAX_BYTES = 50 * 1024 * 1024;
|
|
36945
|
-
const OXLINT_SPAWN_TIMEOUT_MS = 6e4;
|
|
36946
|
-
const NODE_COMPILE_CACHE_DIR_NAME = "node-compile-cache";
|
|
36947
|
-
const DEAD_CODE_WORKER_TIMEOUT_MS = 12e4;
|
|
36948
|
-
const OXLINT_SPLIT_TOTAL_BUDGET_MS = 18e4;
|
|
36949
|
-
const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
|
|
36950
|
-
const LINT_PHASE_TIMEOUT_MS = 3e5;
|
|
36951
|
-
const SCAN_TOTAL_DEADLINE_MS = 9e5;
|
|
36952
|
-
const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
|
|
36953
|
-
const DEAD_CODE_WORKER_MEM_BUDGET_BYTES = 2 * 1024 * 1024 * 1024;
|
|
36954
|
-
const DEAD_CODE_TIMEOUT_CEILING_MS = 6e5;
|
|
36955
|
-
const DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS = 3e4;
|
|
36956
|
-
const DEAD_CODE_OVERLAP_PARSE_SHARE = .4;
|
|
36957
|
-
const RECOMMENDED_PNPM_MINIMUM_RELEASE_AGE_MINUTES = 10080;
|
|
36958
|
-
const REACT_SERVER_DOM_PACKAGES = [
|
|
36959
|
-
"react-server-dom-webpack",
|
|
36960
|
-
"react-server-dom-parcel",
|
|
36961
|
-
"react-server-dom-turbopack"
|
|
36962
|
-
];
|
|
36963
|
-
const REACT_BLOG_RSC_ADVISORY_URL = "https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components";
|
|
36964
|
-
const VERCEL_NEXTJS_SECURITY_RELEASE_URL = "https://vercel.com/changelog/next-js-may-2026-security-release";
|
|
36965
|
-
const DIAGNOSTIC_CATEGORY_BUCKETS = [
|
|
36966
|
-
"Security",
|
|
36967
|
-
"Bugs",
|
|
36968
|
-
"Performance",
|
|
36969
|
-
"Accessibility",
|
|
36970
|
-
"Maintainability"
|
|
36971
|
-
];
|
|
36972
|
-
const APP_ONLY_RULE_KEYS = new Set([
|
|
36973
|
-
"react-hooks-js/static-components",
|
|
36974
|
-
"react-doctor/no-render-prop-children",
|
|
36975
|
-
"react-doctor/prefer-explicit-variants"
|
|
36976
|
-
]);
|
|
36977
|
-
const COMPILER_CLEANUP_BUCKET = "compiler-cleanup";
|
|
36978
|
-
const COMPILER_CLEANUP_RULE_KEYS = new Set(["react-doctor/react-compiler-no-manual-memoization"]);
|
|
36979
|
-
const ROOT_CAUSE_GROUPABLE_RULE_KEYS = new Set([
|
|
36980
|
-
"react-doctor/no-derived-state",
|
|
36981
|
-
"react-doctor/no-derived-state-effect",
|
|
36982
|
-
"react-doctor/no-derived-useState",
|
|
36983
|
-
"react-doctor/no-adjust-state-on-prop-change",
|
|
36984
|
-
"react-doctor/no-reset-all-state-on-prop-change"
|
|
36985
|
-
]);
|
|
36986
|
-
const MAX_GLOB_PATTERN_LENGTH_CHARS = 1024;
|
|
36987
|
-
const CONFIG_CACHE_TTL_MS = 300 * 1e3;
|
|
36988
|
-
const SOCKET_FREE_PURL_API_BASE = "https://firewall-api.socket.dev/purl";
|
|
36989
|
-
const SOCKET_PACKAGE_PAGE_BASE = "https://socket.dev/npm/package";
|
|
36990
|
-
const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
|
|
36991
|
-
const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
|
|
36992
|
-
const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
|
|
36993
|
-
const SUPPLY_CHAIN_PLUGIN = "socket";
|
|
36994
|
-
const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
|
|
36995
|
-
const SUPPLY_CHAIN_CATEGORY = "Security";
|
|
36996
|
-
const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
|
|
36997
|
-
const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
|
|
36998
|
-
const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
|
|
36999
37050
|
const TSCONFIG_FILENAME = "tsconfig.json";
|
|
37000
37051
|
const isRelativeExtendsValue = (extendsValue) => extendsValue.startsWith("./") || extendsValue.startsWith("../") || Path.isAbsolute(extendsValue);
|
|
37001
37052
|
const ensureJsonExtension = (filePath) => Path.extname(filePath) === "" ? `${filePath}.json` : filePath;
|
|
@@ -39975,14 +40026,17 @@ const checkReactServerComponentsAdvisory = (scanDirectory, project) => {
|
|
|
39975
40026
|
return diagnostics;
|
|
39976
40027
|
};
|
|
39977
40028
|
const REDUCED_MOTION_GREP_PATTERN = "prefers-reduced-motion|useReducedMotion|MotionConfig|reducedMotion";
|
|
39978
|
-
const
|
|
39979
|
-
|
|
39980
|
-
"
|
|
39981
|
-
"
|
|
39982
|
-
"
|
|
39983
|
-
"
|
|
39984
|
-
"
|
|
39985
|
-
|
|
40029
|
+
const REDUCED_MOTION_PATTERN = new RegExp(REDUCED_MOTION_GREP_PATTERN);
|
|
40030
|
+
const REDUCED_MOTION_FILE_EXTENSIONS = new Set([
|
|
40031
|
+
".ts",
|
|
40032
|
+
".tsx",
|
|
40033
|
+
".js",
|
|
40034
|
+
".jsx",
|
|
40035
|
+
".css",
|
|
40036
|
+
".scss"
|
|
40037
|
+
]);
|
|
40038
|
+
const REDUCED_MOTION_FILE_GLOBS = [...REDUCED_MOTION_FILE_EXTENSIONS].map((extension) => `*${extension}`);
|
|
40039
|
+
const GIT_GREP_NO_MATCH_STATUS = 1;
|
|
39986
40040
|
const MISSING_REDUCED_MOTION_DIAGNOSTIC = {
|
|
39987
40041
|
filePath: "package.json",
|
|
39988
40042
|
plugin: "react-doctor",
|
|
@@ -39994,6 +40048,19 @@ const MISSING_REDUCED_MOTION_DIAGNOSTIC = {
|
|
|
39994
40048
|
column: 0,
|
|
39995
40049
|
category: "Accessibility"
|
|
39996
40050
|
};
|
|
40051
|
+
const hasReducedMotionHandlingViaFilesystem = (rootDirectory) => {
|
|
40052
|
+
for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) {
|
|
40053
|
+
if (!REDUCED_MOTION_FILE_EXTENSIONS.has(Path.extname(name))) continue;
|
|
40054
|
+
let content;
|
|
40055
|
+
try {
|
|
40056
|
+
content = NFS.readFileSync(absolutePath, "utf-8");
|
|
40057
|
+
} catch {
|
|
40058
|
+
continue;
|
|
40059
|
+
}
|
|
40060
|
+
if (REDUCED_MOTION_PATTERN.test(content)) return true;
|
|
40061
|
+
}
|
|
40062
|
+
return false;
|
|
40063
|
+
};
|
|
39997
40064
|
const checkReducedMotion = (rootDirectory) => {
|
|
39998
40065
|
const packageJsonPath = Path.join(rootDirectory, "package.json");
|
|
39999
40066
|
if (!isFile(packageJsonPath)) return [];
|
|
@@ -40012,22 +40079,21 @@ const checkReducedMotion = (rootDirectory) => {
|
|
|
40012
40079
|
const result = spawnSync("git", [
|
|
40013
40080
|
"grep",
|
|
40014
40081
|
"--untracked",
|
|
40015
|
-
"-
|
|
40016
|
-
"-E",
|
|
40082
|
+
"-lE",
|
|
40017
40083
|
REDUCED_MOTION_GREP_PATTERN,
|
|
40018
40084
|
"--",
|
|
40019
40085
|
...REDUCED_MOTION_FILE_GLOBS
|
|
40020
40086
|
], {
|
|
40021
40087
|
cwd: rootDirectory,
|
|
40088
|
+
encoding: "utf-8",
|
|
40022
40089
|
stdio: [
|
|
40023
40090
|
"ignore",
|
|
40024
40091
|
"pipe",
|
|
40025
40092
|
"pipe"
|
|
40026
40093
|
]
|
|
40027
40094
|
});
|
|
40028
|
-
if (result.error) return [MISSING_REDUCED_MOTION_DIAGNOSTIC];
|
|
40029
|
-
|
|
40030
|
-
return [MISSING_REDUCED_MOTION_DIAGNOSTIC];
|
|
40095
|
+
if (!(!result.error && (result.status === 0 || result.status === GIT_GREP_NO_MATCH_STATUS))) return hasReducedMotionHandlingViaFilesystem(rootDirectory) ? [] : [MISSING_REDUCED_MOTION_DIAGNOSTIC];
|
|
40096
|
+
return result.status === 0 && result.stdout.split("\n").some((filePath) => filePath.length > 0 && !hasIgnoredPathSegment(filePath)) ? [] : [MISSING_REDUCED_MOTION_DIAGNOSTIC];
|
|
40031
40097
|
};
|
|
40032
40098
|
const buildSecurityScanDiagnostic = (finding, entry, relativePath) => ({
|
|
40033
40099
|
filePath: relativePath,
|
|
@@ -40411,7 +40477,7 @@ const collectIgnorePatterns = (rootDirectory) => {
|
|
|
40411
40477
|
cachedPatternsByRoot.set(rootDirectory, patterns);
|
|
40412
40478
|
return patterns;
|
|
40413
40479
|
};
|
|
40414
|
-
const isRecord$
|
|
40480
|
+
const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
40415
40481
|
const KNIP_JSON_FILENAME = "knip.json";
|
|
40416
40482
|
const readJsonFileSafe = (filePath) => {
|
|
40417
40483
|
let rawContents;
|
|
@@ -40428,10 +40494,10 @@ const readJsonFileSafe = (filePath) => {
|
|
|
40428
40494
|
};
|
|
40429
40495
|
const readKnipConfig = (rootDirectory) => {
|
|
40430
40496
|
const knipJson = readJsonFileSafe(path.join(rootDirectory, KNIP_JSON_FILENAME));
|
|
40431
|
-
if (isRecord$
|
|
40497
|
+
if (isRecord$1(knipJson)) return knipJson;
|
|
40432
40498
|
const packageJson = readJsonFileSafe(path.join(rootDirectory, "package.json"));
|
|
40433
|
-
const packageKnipConfig = isRecord$
|
|
40434
|
-
return isRecord$
|
|
40499
|
+
const packageKnipConfig = isRecord$1(packageJson) ? packageJson.knip : null;
|
|
40500
|
+
return isRecord$1(packageKnipConfig) ? packageKnipConfig : null;
|
|
40435
40501
|
};
|
|
40436
40502
|
const normalizePatternList = (value) => {
|
|
40437
40503
|
if (typeof value === "string" && value.length > 0) return [value];
|
|
@@ -40443,10 +40509,10 @@ const prefixWorkspacePatterns = (workspacePattern, patterns) => {
|
|
|
40443
40509
|
return patterns.map((pattern) => pattern.startsWith("!") ? `!${normalizedWorkspacePattern}/${pattern.slice(1)}` : `${normalizedWorkspacePattern}/${pattern}`);
|
|
40444
40510
|
};
|
|
40445
40511
|
const collectKnipWorkspacePatterns = (workspaces, settingName) => {
|
|
40446
|
-
if (!isRecord$
|
|
40512
|
+
if (!isRecord$1(workspaces)) return [];
|
|
40447
40513
|
const patterns = [];
|
|
40448
40514
|
for (const [workspacePattern, workspaceConfig] of Object.entries(workspaces)) {
|
|
40449
|
-
if (!isRecord$
|
|
40515
|
+
if (!isRecord$1(workspaceConfig)) continue;
|
|
40450
40516
|
patterns.push(...prefixWorkspacePatterns(workspacePattern, normalizePatternList(workspaceConfig[settingName])));
|
|
40451
40517
|
}
|
|
40452
40518
|
return patterns;
|
|
@@ -40669,7 +40735,7 @@ const parseUnusedFiles = (value) => {
|
|
|
40669
40735
|
const values = parseArray(value, "unusedFiles");
|
|
40670
40736
|
const unusedFiles = [];
|
|
40671
40737
|
for (const [index, entry] of values.entries()) {
|
|
40672
|
-
if (!isRecord$
|
|
40738
|
+
if (!isRecord$1(entry)) throw new Error(`Dead-code worker returned invalid unusedFiles[${index}].`);
|
|
40673
40739
|
unusedFiles.push({ path: parseString(entry.path, `unusedFiles[${index}].path`) });
|
|
40674
40740
|
}
|
|
40675
40741
|
return unusedFiles;
|
|
@@ -40678,7 +40744,7 @@ const parseUnusedExports = (value) => {
|
|
|
40678
40744
|
const values = parseArray(value, "unusedExports");
|
|
40679
40745
|
const unusedExports = [];
|
|
40680
40746
|
for (const [index, entry] of values.entries()) {
|
|
40681
|
-
if (!isRecord$
|
|
40747
|
+
if (!isRecord$1(entry)) throw new Error(`Dead-code worker returned invalid unusedExports[${index}].`);
|
|
40682
40748
|
unusedExports.push({
|
|
40683
40749
|
path: parseString(entry.path, `unusedExports[${index}].path`),
|
|
40684
40750
|
name: parseString(entry.name, `unusedExports[${index}].name`),
|
|
@@ -40693,7 +40759,7 @@ const parseUnusedDependencies = (value) => {
|
|
|
40693
40759
|
const values = parseArray(value, "unusedDependencies");
|
|
40694
40760
|
const unusedDependencies = [];
|
|
40695
40761
|
for (const [index, entry] of values.entries()) {
|
|
40696
|
-
if (!isRecord$
|
|
40762
|
+
if (!isRecord$1(entry)) throw new Error(`Dead-code worker returned invalid unusedDependencies[${index}].`);
|
|
40697
40763
|
unusedDependencies.push({
|
|
40698
40764
|
name: parseString(entry.name, `unusedDependencies[${index}].name`),
|
|
40699
40765
|
isDevDependency: parseBoolean$1(entry.isDevDependency, `unusedDependencies[${index}].isDevDependency`)
|
|
@@ -40705,13 +40771,13 @@ const parseCircularDependencies = (value) => {
|
|
|
40705
40771
|
const values = parseArray(value, "circularDependencies");
|
|
40706
40772
|
const circularDependencies = [];
|
|
40707
40773
|
for (const [index, entry] of values.entries()) {
|
|
40708
|
-
if (!isRecord$
|
|
40774
|
+
if (!isRecord$1(entry)) throw new Error(`Dead-code worker returned invalid circularDependencies[${index}].`);
|
|
40709
40775
|
circularDependencies.push({ files: parseStringArray(entry.files, `circularDependencies[${index}].files`) });
|
|
40710
40776
|
}
|
|
40711
40777
|
return circularDependencies;
|
|
40712
40778
|
};
|
|
40713
40779
|
const parseDeadCodeWorkerResult = (value) => {
|
|
40714
|
-
if (!isRecord$
|
|
40780
|
+
if (!isRecord$1(value)) throw new Error("Dead-code worker returned an invalid result.");
|
|
40715
40781
|
return {
|
|
40716
40782
|
unusedFiles: parseUnusedFiles(value.unusedFiles),
|
|
40717
40783
|
unusedExports: parseUnusedExports(value.unusedExports),
|
|
@@ -40720,7 +40786,7 @@ const parseDeadCodeWorkerResult = (value) => {
|
|
|
40720
40786
|
};
|
|
40721
40787
|
};
|
|
40722
40788
|
const parseDeadCodeWorkerError = (value) => {
|
|
40723
|
-
if (!isRecord$
|
|
40789
|
+
if (!isRecord$1(value) || typeof value.message !== "string") return { message: "Dead-code worker failed." };
|
|
40724
40790
|
return {
|
|
40725
40791
|
...typeof value.name === "string" ? { name: value.name } : {},
|
|
40726
40792
|
message: value.message,
|
|
@@ -40728,7 +40794,7 @@ const parseDeadCodeWorkerError = (value) => {
|
|
|
40728
40794
|
};
|
|
40729
40795
|
};
|
|
40730
40796
|
const parseDeadCodeWorkerMessage = (value) => {
|
|
40731
|
-
if (!isRecord$
|
|
40797
|
+
if (!isRecord$1(value)) throw new Error("Dead-code worker returned an invalid message.");
|
|
40732
40798
|
if (value.ok === true) return {
|
|
40733
40799
|
ok: true,
|
|
40734
40800
|
result: value.result
|
|
@@ -40939,6 +41005,7 @@ const isDiagnosticOnSurface = (diagnostic, surface, config) => {
|
|
|
40939
41005
|
return true;
|
|
40940
41006
|
};
|
|
40941
41007
|
const filterDiagnosticsForSurface = (diagnostics, surface, config) => diagnostics.filter((diagnostic) => isDiagnosticOnSurface(diagnostic, surface, config));
|
|
41008
|
+
const remainingDeadlineBudgetMs = (deadlineEpochMs) => Math.max(deadlineEpochMs - Date.now(), 0);
|
|
40942
41009
|
/**
|
|
40943
41010
|
* Budget for the dead-code phase, scaled to the work. deslop's graph build is
|
|
40944
41011
|
* CPU-bound and roughly linear in file count, so a fixed 120s cap is too tight
|
|
@@ -40957,53 +41024,6 @@ const resolveDeadCodeTimeout = (input) => {
|
|
|
40957
41024
|
phaseTimeoutMs: workerTimeoutMs + DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS
|
|
40958
41025
|
};
|
|
40959
41026
|
};
|
|
40960
|
-
const collectSizedSourceFiles = (rootDirectory, relativePaths) => {
|
|
40961
|
-
const entries = [];
|
|
40962
|
-
for (const relativePath of relativePaths) {
|
|
40963
|
-
const absolutePath = Path.resolve(rootDirectory, relativePath);
|
|
40964
|
-
const sizeBytes = statSourceFileSize(absolutePath);
|
|
40965
|
-
if (isLargeMinifiedFile(absolutePath, sizeBytes)) continue;
|
|
40966
|
-
entries.push({
|
|
40967
|
-
path: relativePath,
|
|
40968
|
-
sizeBytes: sizeBytes ?? 0
|
|
40969
|
-
});
|
|
40970
|
-
}
|
|
40971
|
-
return entries;
|
|
40972
|
-
};
|
|
40973
|
-
const listSourceFilesViaGit = (rootDirectory) => {
|
|
40974
|
-
const result = spawnSync("git", [
|
|
40975
|
-
"ls-files",
|
|
40976
|
-
"-z",
|
|
40977
|
-
"--cached",
|
|
40978
|
-
"--others",
|
|
40979
|
-
"--exclude-standard"
|
|
40980
|
-
], {
|
|
40981
|
-
cwd: rootDirectory,
|
|
40982
|
-
encoding: "utf-8",
|
|
40983
|
-
maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
|
|
40984
|
-
});
|
|
40985
|
-
if (result.error || result.status !== 0) return null;
|
|
40986
|
-
return result.stdout.split("\0").filter((filePath) => filePath.length > 0 && isLintableSourceFile(filePath));
|
|
40987
|
-
};
|
|
40988
|
-
const listSourceFilesViaFilesystem = (rootDirectory) => {
|
|
40989
|
-
const filePaths = [];
|
|
40990
|
-
const stack = [rootDirectory];
|
|
40991
|
-
while (stack.length > 0) {
|
|
40992
|
-
const currentDirectory = stack.pop();
|
|
40993
|
-
const entries = readDirectoryEntries(currentDirectory);
|
|
40994
|
-
for (const entry of entries) {
|
|
40995
|
-
const absolutePath = Path.join(currentDirectory, entry.name);
|
|
40996
|
-
if (entry.isDirectory()) {
|
|
40997
|
-
if (!entry.name.startsWith(".") && !IGNORED_DIRECTORIES.has(entry.name)) stack.push(absolutePath);
|
|
40998
|
-
continue;
|
|
40999
|
-
}
|
|
41000
|
-
if (entry.isFile() && isLintableSourceFile(entry.name)) filePaths.push(Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/"));
|
|
41001
|
-
}
|
|
41002
|
-
}
|
|
41003
|
-
return filePaths;
|
|
41004
|
-
};
|
|
41005
|
-
const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory));
|
|
41006
|
-
const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
|
|
41007
41027
|
const resolveLintIncludePaths = (rootDirectory, userConfig, project) => {
|
|
41008
41028
|
if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
|
|
41009
41029
|
const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
|
|
@@ -41700,12 +41720,12 @@ const findFilesWithDisableDirectivesViaGit = async (rootDirectory, includePaths)
|
|
|
41700
41720
|
return null;
|
|
41701
41721
|
}
|
|
41702
41722
|
if (grepResult === null) return null;
|
|
41703
|
-
return grepResult.stdout.split("\n").filter((filePath) => filePath.length > 0 && isLintableSourceFile(filePath));
|
|
41723
|
+
return grepResult.stdout.split("\n").filter((filePath) => filePath.length > 0 && isLintableSourceFile(filePath) && !hasIgnoredPathSegment(filePath));
|
|
41704
41724
|
};
|
|
41705
41725
|
const findFilesWithDisableDirectivesViaFilesystem = (rootDirectory, includePaths) => {
|
|
41706
41726
|
const matches = [];
|
|
41707
41727
|
const checkFile = (relativePath) => {
|
|
41708
|
-
if (!isLintableSourceFile(relativePath)) return;
|
|
41728
|
+
if (!isLintableSourceFile(relativePath) || hasIgnoredPathSegment(relativePath)) return;
|
|
41709
41729
|
const absolutePath = Path.join(rootDirectory, relativePath);
|
|
41710
41730
|
let content;
|
|
41711
41731
|
try {
|
|
@@ -41719,22 +41739,7 @@ const findFilesWithDisableDirectivesViaFilesystem = (rootDirectory, includePaths
|
|
|
41719
41739
|
for (const candidate of includePaths) checkFile(candidate);
|
|
41720
41740
|
return matches;
|
|
41721
41741
|
}
|
|
41722
|
-
const
|
|
41723
|
-
while (stack.length > 0) {
|
|
41724
|
-
const current = stack.pop();
|
|
41725
|
-
if (current === void 0) continue;
|
|
41726
|
-
const entries = readDirectoryEntries(current);
|
|
41727
|
-
for (const entry of entries) {
|
|
41728
|
-
if (entry.isDirectory()) {
|
|
41729
|
-
if (entry.name.startsWith(".") || IGNORED_DIRECTORIES.has(entry.name)) continue;
|
|
41730
|
-
stack.push(Path.join(current, entry.name));
|
|
41731
|
-
continue;
|
|
41732
|
-
}
|
|
41733
|
-
if (!entry.isFile()) continue;
|
|
41734
|
-
const absolute = Path.join(current, entry.name);
|
|
41735
|
-
checkFile(Path.relative(rootDirectory, absolute));
|
|
41736
|
-
}
|
|
41737
|
-
}
|
|
41742
|
+
for (const { absolutePath } of walkSourceTreeFiles(rootDirectory)) checkFile(Path.relative(rootDirectory, absolutePath));
|
|
41738
41743
|
return matches;
|
|
41739
41744
|
};
|
|
41740
41745
|
const findFilesWithDisableDirectives = async (rootDirectory, includePaths) => await findFilesWithDisableDirectivesViaGit(rootDirectory, includePaths) ?? findFilesWithDisableDirectivesViaFilesystem(rootDirectory, includePaths);
|
|
@@ -41951,12 +41956,12 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
|
|
|
41951
41956
|
if (customRulesOnly && registryEntry.originallyExternal) continue;
|
|
41952
41957
|
if (rule.framework !== "global" && !rule.requires) continue;
|
|
41953
41958
|
if (!shouldEnableRule(rule.requires, rule.tags, capabilities, ignoredTags, rule.disabledBy)) continue;
|
|
41954
|
-
const
|
|
41959
|
+
const explicitRuleOverride = resolveRuleSeverityOverride({ ruleKey: registryEntry.key }, severityControls);
|
|
41960
|
+
if (rule.defaultEnabled === false && explicitRuleOverride === void 0) continue;
|
|
41961
|
+
const severity = resolveRuleSeverityOverride({
|
|
41955
41962
|
ruleKey: registryEntry.key,
|
|
41956
41963
|
category: rule.category
|
|
41957
|
-
}, severityControls);
|
|
41958
|
-
if (rule.defaultEnabled === false && explicitSeverity === void 0) continue;
|
|
41959
|
-
const severity = explicitSeverity ?? resolveCompilerCleanupBucketSeverity(registryEntry.key, severityControls) ?? rule.severity;
|
|
41964
|
+
}, severityControls) ?? resolveCompilerCleanupBucketSeverity(registryEntry.key, severityControls) ?? rule.severity;
|
|
41960
41965
|
if (severity === "off") continue;
|
|
41961
41966
|
enabledReactDoctorRules[registryEntry.key] = severity;
|
|
41962
41967
|
}
|
|
@@ -42025,9 +42030,9 @@ const emptyCache = () => ({
|
|
|
42025
42030
|
const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
|
|
42026
42031
|
const entries = /* @__PURE__ */ new Map();
|
|
42027
42032
|
const persisted = failOpenReadJson(cacheFilePath, emptyCache());
|
|
42028
|
-
if (persisted.version !== 1 || !isRecord$
|
|
42033
|
+
if (persisted.version !== 1 || !isRecord$1(persisted.rulesets)) return entries;
|
|
42029
42034
|
const bucket = persisted.rulesets[rulesetHash];
|
|
42030
|
-
if (!isRecord$
|
|
42035
|
+
if (!isRecord$1(bucket) || !isRecord$1(bucket.files)) return entries;
|
|
42031
42036
|
for (const [fileKey, rawDiagnostics] of Object.entries(bucket.files)) {
|
|
42032
42037
|
const decoded = decodeFileDiagnostics(rawDiagnostics);
|
|
42033
42038
|
if (decoded !== null) entries.set(fileKey, decoded);
|
|
@@ -42045,9 +42050,9 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
|
|
|
42045
42050
|
},
|
|
42046
42051
|
persist: () => {
|
|
42047
42052
|
const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
|
|
42048
|
-
const rulesets = onDisk.version === 1 && isRecord$
|
|
42053
|
+
const rulesets = onDisk.version === 1 && isRecord$1(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
|
|
42049
42054
|
const existingBucket = rulesets[rulesetHash];
|
|
42050
|
-
const existingFiles = isRecord$
|
|
42055
|
+
const existingFiles = isRecord$1(existingBucket) && isRecord$1(existingBucket.files) ? existingBucket.files : {};
|
|
42051
42056
|
const ourFiles = {};
|
|
42052
42057
|
for (const [fileKey, diagnostics] of entries) ourFiles[fileKey] = diagnostics;
|
|
42053
42058
|
const cappedEntries = Object.entries({
|
|
@@ -42060,7 +42065,7 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
|
|
|
42060
42065
|
};
|
|
42061
42066
|
atomicWriteJson(cacheFilePath, {
|
|
42062
42067
|
version: 1,
|
|
42063
|
-
rulesets: Object.fromEntries(Object.entries(rulesets).filter(([, bucket]) => isRecord$
|
|
42068
|
+
rulesets: Object.fromEntries(Object.entries(rulesets).filter(([, bucket]) => isRecord$1(bucket) && typeof bucket.updatedAtMs === "number").sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8))
|
|
42064
42069
|
});
|
|
42065
42070
|
}
|
|
42066
42071
|
};
|
|
@@ -42938,21 +42943,33 @@ const isParallelismRelatedSpawnError = (error) => {
|
|
|
42938
42943
|
* loop with a slimmer config in that case.
|
|
42939
42944
|
*/
|
|
42940
42945
|
const spawnLintBatches = async (input) => {
|
|
42941
|
-
const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 8, signal } = input;
|
|
42946
|
+
const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 8, signal, deadlineEpochMs } = input;
|
|
42942
42947
|
const requestedConcurrency = resolveScanConcurrency(input.concurrency ?? 1);
|
|
42943
42948
|
const totalFileCount = fileBatches.reduce((sum, batch) => sum + batch.length, 0);
|
|
42944
42949
|
const runBatchPass = async (concurrency) => {
|
|
42945
42950
|
const allDiagnostics = [];
|
|
42946
42951
|
const droppedFiles = [];
|
|
42952
|
+
const deadlineSkippedFiles = [];
|
|
42953
|
+
const isPastDeadline = () => deadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(deadlineEpochMs) === 0;
|
|
42947
42954
|
let firstDropReason = null;
|
|
42948
|
-
const spawnLintBatch = async (batch, depth,
|
|
42955
|
+
const spawnLintBatch = async (batch, depth, batchState) => {
|
|
42956
|
+
if (isPastDeadline()) {
|
|
42957
|
+
deadlineSkippedFiles.push(...batch);
|
|
42958
|
+
batchState.deadlineSkippedFileCount += batch.length;
|
|
42959
|
+
return [];
|
|
42960
|
+
}
|
|
42949
42961
|
const batchArgs = [...baseArgs, ...batch];
|
|
42950
42962
|
try {
|
|
42951
42963
|
return parseOxlintOutput(await spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes, signal), project, rootDirectory);
|
|
42952
42964
|
} catch (error) {
|
|
42953
42965
|
if (!isSplittableReactDoctorError(error)) throw error;
|
|
42954
|
-
|
|
42955
|
-
|
|
42966
|
+
if (isPastDeadline()) {
|
|
42967
|
+
deadlineSkippedFiles.push(...batch);
|
|
42968
|
+
batchState.deadlineSkippedFileCount += batch.length;
|
|
42969
|
+
return [];
|
|
42970
|
+
}
|
|
42971
|
+
batchState.deadlineMs ??= Date.now() + splitTotalBudgetMs;
|
|
42972
|
+
const isBudgetElapsed = Date.now() >= batchState.deadlineMs;
|
|
42956
42973
|
const isDepthCapReached = depth >= splitMaxDepth;
|
|
42957
42974
|
if (batch.length <= 1 || isBudgetElapsed || isDepthCapReached) {
|
|
42958
42975
|
droppedFiles.push(...batch);
|
|
@@ -42965,7 +42982,7 @@ const spawnLintBatches = async (input) => {
|
|
|
42965
42982
|
return [];
|
|
42966
42983
|
}
|
|
42967
42984
|
const splitIndex = Math.ceil(batch.length / 2);
|
|
42968
|
-
return [...await spawnLintBatch(batch.slice(0, splitIndex), depth + 1,
|
|
42985
|
+
return [...await spawnLintBatch(batch.slice(0, splitIndex), depth + 1, batchState), ...await spawnLintBatch(batch.slice(splitIndex), depth + 1, batchState)];
|
|
42969
42986
|
}
|
|
42970
42987
|
};
|
|
42971
42988
|
let startedFileCount = 0;
|
|
@@ -42981,9 +42998,17 @@ const spawnLintBatches = async (input) => {
|
|
|
42981
42998
|
progressTimer?.unref?.();
|
|
42982
42999
|
try {
|
|
42983
43000
|
const batchResults = await mapWithConcurrency(fileBatches, concurrency, async (batch) => {
|
|
43001
|
+
if (isPastDeadline()) {
|
|
43002
|
+
deadlineSkippedFiles.push(...batch);
|
|
43003
|
+
return [];
|
|
43004
|
+
}
|
|
42984
43005
|
startedFileCount += batch.length;
|
|
42985
|
-
const
|
|
42986
|
-
|
|
43006
|
+
const batchState = {
|
|
43007
|
+
deadlineMs: null,
|
|
43008
|
+
deadlineSkippedFileCount: 0
|
|
43009
|
+
};
|
|
43010
|
+
const batchDiagnostics = await spawnLintBatch(batch, 0, batchState);
|
|
43011
|
+
scannedFileCount += batch.length - batchState.deadlineSkippedFileCount;
|
|
42987
43012
|
if (onFileProgress) {
|
|
42988
43013
|
displayedFileCount = Math.min(Math.max(displayedFileCount, scannedFileCount), totalFileCount);
|
|
42989
43014
|
onFileProgress(displayedFileCount, totalFileCount);
|
|
@@ -42994,12 +43019,13 @@ const spawnLintBatches = async (input) => {
|
|
|
42994
43019
|
} finally {
|
|
42995
43020
|
if (progressTimer !== null) clearInterval(progressTimer);
|
|
42996
43021
|
}
|
|
42997
|
-
|
|
42998
|
-
|
|
42999
|
-
|
|
43000
|
-
|
|
43001
|
-
|
|
43002
|
-
}
|
|
43022
|
+
const reportSkippedFiles = (skippedFiles, buildMessage) => {
|
|
43023
|
+
if (skippedFiles.length === 0 || !onPartialFailure) return;
|
|
43024
|
+
onPartialFailure(buildMessage(`${skippedFiles.slice(0, 3).join(", ")}${skippedFiles.length > 3 ? `, +${skippedFiles.length - 3} more` : ""}`));
|
|
43025
|
+
};
|
|
43026
|
+
const reasonHint = firstDropReason ? ` — first failure: ${firstDropReason}` : "";
|
|
43027
|
+
reportSkippedFiles(droppedFiles, (fileListPreview) => `${droppedFiles.length} file(s) failed to lint and were skipped (${fileListPreview})${reasonHint}`);
|
|
43028
|
+
reportSkippedFiles(deadlineSkippedFiles, (fileListPreview) => `${deadlineSkippedFiles.length} file(s) skipped — max scan duration reached before they were linted (${fileListPreview})`);
|
|
43003
43029
|
return allDiagnostics;
|
|
43004
43030
|
};
|
|
43005
43031
|
let diagnostics;
|
|
@@ -43203,7 +43229,8 @@ const runOxlint = async (options) => {
|
|
|
43203
43229
|
spawnTimeoutMs,
|
|
43204
43230
|
outputMaxBytes,
|
|
43205
43231
|
concurrency: options.concurrency,
|
|
43206
|
-
signal: options.signal
|
|
43232
|
+
signal: options.signal,
|
|
43233
|
+
deadlineEpochMs: options.deadlineEpochMs
|
|
43207
43234
|
});
|
|
43208
43235
|
writeOxlintConfig(passConfigPath, buildConfigForPass({}));
|
|
43209
43236
|
try {
|
|
@@ -43302,7 +43329,8 @@ const runOxlint = async (options) => {
|
|
|
43302
43329
|
spawnTimeoutMs,
|
|
43303
43330
|
outputMaxBytes,
|
|
43304
43331
|
concurrency: options.concurrency,
|
|
43305
|
-
signal: options.signal
|
|
43332
|
+
signal: options.signal,
|
|
43333
|
+
deadlineEpochMs: options.deadlineEpochMs
|
|
43306
43334
|
});
|
|
43307
43335
|
writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
|
|
43308
43336
|
try {
|
|
@@ -43406,7 +43434,8 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
|
|
|
43406
43434
|
outputMaxBytes,
|
|
43407
43435
|
concurrency,
|
|
43408
43436
|
signal,
|
|
43409
|
-
lintBatchOrdering
|
|
43437
|
+
lintBatchOrdering,
|
|
43438
|
+
deadlineEpochMs: input.deadlineEpochMs
|
|
43410
43439
|
}),
|
|
43411
43440
|
catch: ensureReactDoctorError
|
|
43412
43441
|
});
|
|
@@ -44166,6 +44195,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
44166
44195
|
const deadCodePhaseTimeoutMs = yield* DeadCodePhaseTimeoutMs;
|
|
44167
44196
|
const resolveDeadCodePhaseTimeoutMs = (scaledPhaseTimeoutMs) => deadCodePhaseTimeoutMs === 15e4 ? scaledPhaseTimeoutMs : deadCodePhaseTimeoutMs;
|
|
44168
44197
|
const workerCountSuffix = scanConcurrency > 1 ? ` ${highlighter.dim(`[~${scanConcurrency} workers]`)}` : "";
|
|
44198
|
+
const capToDeadline = (phaseTimeoutMs) => input.deadlineEpochMs === void 0 ? phaseTimeoutMs : Math.min(phaseTimeoutMs, remainingDeadlineBudgetMs(input.deadlineEpochMs));
|
|
44169
44199
|
const shouldRunDeadCode = input.runDeadCode && !isDiffMode && (showWarnings || deadCodeMaySurfaceWhenWarningsHidden(resolvedConfig.config));
|
|
44170
44200
|
const deadCodeOverlapMode = yield* DeadCodeOverlap;
|
|
44171
44201
|
const shouldOverlapDeadCode = shouldRunDeadCode && deadCodeOverlapMode === "on";
|
|
@@ -44195,7 +44225,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
44195
44225
|
});
|
|
44196
44226
|
const deadCodeFiber = shouldOverlapDeadCode ? yield* forkChild(buildCollectDeadCode({
|
|
44197
44227
|
workerTimeoutMs: overlapDeadCodeTimeout.workerTimeoutMs,
|
|
44198
|
-
phaseTimeoutMs: resolveDeadCodePhaseTimeoutMs(overlapDeadCodeTimeout.phaseTimeoutMs)
|
|
44228
|
+
phaseTimeoutMs: capToDeadline(resolveDeadCodePhaseTimeoutMs(overlapDeadCodeTimeout.phaseTimeoutMs))
|
|
44199
44229
|
})) : null;
|
|
44200
44230
|
const scanProgress = yield* progressService.start("Scanning...");
|
|
44201
44231
|
const scanStartTime = Date.now();
|
|
@@ -44220,7 +44250,8 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
44220
44250
|
onCacheStats: (cacheHitFileCount, totalConsideredFileCount) => {
|
|
44221
44251
|
lintCacheHitFileCount = cacheHitFileCount;
|
|
44222
44252
|
lintCacheTotalFileCount = totalConsideredFileCount;
|
|
44223
|
-
}
|
|
44253
|
+
},
|
|
44254
|
+
deadlineEpochMs: input.deadlineEpochMs
|
|
44224
44255
|
}).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
|
|
44225
44256
|
yield* set(lintFailure, {
|
|
44226
44257
|
didFail: true,
|
|
@@ -44247,7 +44278,13 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
44247
44278
|
let deadCodeCollected = [];
|
|
44248
44279
|
if (lintFailureState.didFail) {
|
|
44249
44280
|
if (deadCodeFiber !== null) yield* interrupt(deadCodeFiber);
|
|
44250
|
-
} else if (shouldRunDeadCode) {
|
|
44281
|
+
} else if (shouldRunDeadCode) if (input.deadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(input.deadlineEpochMs) === 0) {
|
|
44282
|
+
if (deadCodeFiber !== null) yield* interrupt(deadCodeFiber);
|
|
44283
|
+
yield* set(deadCodeFailure, {
|
|
44284
|
+
didFail: true,
|
|
44285
|
+
reason: "Dead-code analysis skipped — max scan duration reached."
|
|
44286
|
+
});
|
|
44287
|
+
} else {
|
|
44251
44288
|
yield* scanProgress.update(`Scanned ${scannedFilesLabel}, analyzing dead code...`);
|
|
44252
44289
|
const sequentialDeadCodeTimeout = resolveDeadCodeTimeout({
|
|
44253
44290
|
sourceFileCount: totalFileCount,
|
|
@@ -44256,7 +44293,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
44256
44293
|
});
|
|
44257
44294
|
deadCodeCollected = deadCodeFiber !== null ? yield* join(deadCodeFiber) : yield* buildCollectDeadCode({
|
|
44258
44295
|
workerTimeoutMs: sequentialDeadCodeTimeout.workerTimeoutMs,
|
|
44259
|
-
phaseTimeoutMs: resolveDeadCodePhaseTimeoutMs(sequentialDeadCodeTimeout.phaseTimeoutMs)
|
|
44296
|
+
phaseTimeoutMs: capToDeadline(resolveDeadCodePhaseTimeoutMs(sequentialDeadCodeTimeout.phaseTimeoutMs))
|
|
44260
44297
|
});
|
|
44261
44298
|
}
|
|
44262
44299
|
const deadCodeFailureState = lintFailureState.didFail ? {
|
|
@@ -44294,7 +44331,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
44294
44331
|
};
|
|
44295
44332
|
const scoreSurface = input.scoreSurface ?? "score";
|
|
44296
44333
|
const scoreDiagnostics = filterDiagnosticsForSurface([...finalDiagnostics], scoreSurface, resolvedConfig.config);
|
|
44297
|
-
const score = lintFailureState.didFail ? null : yield* scoreService.compute({
|
|
44334
|
+
const score = lintFailureState.didFail || deadCodeFailureState.didFail ? null : yield* scoreService.compute({
|
|
44298
44335
|
diagnostics: scoreDiagnostics,
|
|
44299
44336
|
isCi: input.isCi,
|
|
44300
44337
|
metadata: scoreMetadata
|
|
@@ -44687,6 +44724,7 @@ const buildJsonReport = (input) => {
|
|
|
44687
44724
|
return {
|
|
44688
44725
|
schemaVersion: 1,
|
|
44689
44726
|
mode: input.mode,
|
|
44727
|
+
...input.baselineDegraded ? { baselineDegraded: true } : {},
|
|
44690
44728
|
...shared
|
|
44691
44729
|
};
|
|
44692
44730
|
};
|
|
@@ -45194,7 +45232,7 @@ const makeNoopConsole = () => ({
|
|
|
45194
45232
|
});
|
|
45195
45233
|
//#endregion
|
|
45196
45234
|
//#region src/cli/utils/version.ts
|
|
45197
|
-
const VERSION = "0.5.8-dev.
|
|
45235
|
+
const VERSION = "0.5.8-dev.8232e96";
|
|
45198
45236
|
//#endregion
|
|
45199
45237
|
//#region src/cli/utils/json-mode.ts
|
|
45200
45238
|
let context = null;
|
|
@@ -45563,13 +45601,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
|
|
|
45563
45601
|
* uploads source-map artifacts under, so stack frames symbolicate. Honors the
|
|
45564
45602
|
* standard `SENTRY_RELEASE` override.
|
|
45565
45603
|
*/
|
|
45566
|
-
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.8-dev.
|
|
45604
|
+
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.8-dev.8232e96`;
|
|
45567
45605
|
/**
|
|
45568
45606
|
* Deployment environment shown in Sentry's environment filter. Defaults to
|
|
45569
45607
|
* `production` for tagged releases and `development` for dev/unbuilt versions,
|
|
45570
45608
|
* overridable via the standard `SENTRY_ENVIRONMENT` env var.
|
|
45571
45609
|
*/
|
|
45572
|
-
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.8-dev.
|
|
45610
|
+
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.8-dev.8232e96") ? "development" : "production");
|
|
45573
45611
|
/**
|
|
45574
45612
|
* Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
|
|
45575
45613
|
* (set to `0` to disable tracing) and falls back to
|
|
@@ -45777,7 +45815,7 @@ const runGit = (projectRoot, args) => {
|
|
|
45777
45815
|
}
|
|
45778
45816
|
};
|
|
45779
45817
|
const resolveGitPath = (baseDirectory, value) => Path.isAbsolute(value) ? value : Path.resolve(baseDirectory, value);
|
|
45780
|
-
const isRecord
|
|
45818
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
45781
45819
|
const getPackageJsonPath = (projectRoot) => Path.join(projectRoot, PACKAGE_JSON_FILE_NAME);
|
|
45782
45820
|
const readPackageJson = (projectRoot) => {
|
|
45783
45821
|
try {
|
|
@@ -45791,25 +45829,25 @@ const writeJsonFile = (filePath, value) => {
|
|
|
45791
45829
|
};
|
|
45792
45830
|
const packageHasDependency = (projectRoot, dependencyName) => {
|
|
45793
45831
|
const packageJson = readPackageJson(projectRoot);
|
|
45794
|
-
if (!isRecord
|
|
45832
|
+
if (!isRecord(packageJson)) return false;
|
|
45795
45833
|
return [
|
|
45796
45834
|
"dependencies",
|
|
45797
45835
|
"devDependencies",
|
|
45798
45836
|
"optionalDependencies"
|
|
45799
45837
|
].some((fieldName) => {
|
|
45800
45838
|
const dependencies = packageJson[fieldName];
|
|
45801
|
-
return isRecord
|
|
45839
|
+
return isRecord(dependencies) && typeof dependencies[dependencyName] === "string";
|
|
45802
45840
|
});
|
|
45803
45841
|
};
|
|
45804
45842
|
const packageHasRecordKey = (projectRoot, key) => {
|
|
45805
45843
|
const packageJson = readPackageJson(projectRoot);
|
|
45806
|
-
return isRecord
|
|
45844
|
+
return isRecord(packageJson) && isRecord(packageJson[key]);
|
|
45807
45845
|
};
|
|
45808
45846
|
const packageHasNestedRecordKey = (projectRoot, key, nestedKey) => {
|
|
45809
45847
|
const packageJson = readPackageJson(projectRoot);
|
|
45810
|
-
if (!isRecord
|
|
45848
|
+
if (!isRecord(packageJson)) return false;
|
|
45811
45849
|
const value = packageJson[key];
|
|
45812
|
-
return isRecord
|
|
45850
|
+
return isRecord(value) && isRecord(value[nestedKey]);
|
|
45813
45851
|
};
|
|
45814
45852
|
const ensureTrailingNewline = (content) => content.endsWith("\n") ? content : `${content}\n`;
|
|
45815
45853
|
//#endregion
|
|
@@ -48849,31 +48887,31 @@ const findNearestPackageDirectory = (startDirectory, stopDirectory) => {
|
|
|
48849
48887
|
};
|
|
48850
48888
|
const hasDoctorScript = (projectRoot) => {
|
|
48851
48889
|
const packageJson = readPackageJson(findNearestPackageDirectory(projectRoot) ?? projectRoot);
|
|
48852
|
-
if (!isRecord
|
|
48890
|
+
if (!isRecord(packageJson)) return false;
|
|
48853
48891
|
const scripts = packageJson.scripts;
|
|
48854
|
-
if (!isRecord
|
|
48892
|
+
if (!isRecord(scripts)) return false;
|
|
48855
48893
|
return isReactDoctorScriptCommand(scripts[DOCTOR_SCRIPT_NAME]) || isReactDoctorScriptCommand(scripts[FALLBACK_DOCTOR_SCRIPT_NAME]);
|
|
48856
48894
|
};
|
|
48857
48895
|
const hasDoctorDependency = (packageJson) => DEPENDENCY_FIELD_NAMES.some((fieldName) => {
|
|
48858
48896
|
const dependencies = packageJson[fieldName];
|
|
48859
|
-
return isRecord
|
|
48897
|
+
return isRecord(dependencies) && Object.hasOwn(dependencies, "react-doctor");
|
|
48860
48898
|
});
|
|
48861
48899
|
const installDoctorScript = (options) => {
|
|
48862
48900
|
const packageDirectory = findNearestPackageDirectory(options.projectRoot) ?? options.projectRoot;
|
|
48863
48901
|
const packageJsonPath = getPackageJsonPath(packageDirectory);
|
|
48864
48902
|
const packageJson = readPackageJson(packageDirectory);
|
|
48865
|
-
if (!isRecord
|
|
48903
|
+
if (!isRecord(packageJson)) return {
|
|
48866
48904
|
packageJsonPath,
|
|
48867
48905
|
scriptStatus: "skipped",
|
|
48868
48906
|
scriptReason: "missing-or-invalid-package-json"
|
|
48869
48907
|
};
|
|
48870
48908
|
const scripts = packageJson.scripts;
|
|
48871
48909
|
const scriptTarget = (() => {
|
|
48872
|
-
if (scripts !== void 0 && !isRecord
|
|
48910
|
+
if (scripts !== void 0 && !isRecord(scripts)) return {
|
|
48873
48911
|
status: "skipped",
|
|
48874
48912
|
reason: "invalid-scripts"
|
|
48875
48913
|
};
|
|
48876
|
-
const scriptRecord = isRecord
|
|
48914
|
+
const scriptRecord = isRecord(scripts) ? scripts : {};
|
|
48877
48915
|
if (isReactDoctorScriptCommand(scriptRecord[DOCTOR_SCRIPT_NAME])) return {
|
|
48878
48916
|
scriptName: DOCTOR_SCRIPT_NAME,
|
|
48879
48917
|
status: "existing"
|
|
@@ -48907,7 +48945,7 @@ const installDoctorScript = (options) => {
|
|
|
48907
48945
|
if (scriptStatus === "created") writeJsonFile(packageJsonPath, {
|
|
48908
48946
|
...packageJson,
|
|
48909
48947
|
scripts: {
|
|
48910
|
-
...isRecord
|
|
48948
|
+
...isRecord(scripts) ? scripts : {},
|
|
48911
48949
|
[scriptTarget.scriptName ?? DOCTOR_SCRIPT_NAME]: DOCTOR_SCRIPT_COMMAND
|
|
48912
48950
|
}
|
|
48913
48951
|
});
|
|
@@ -55991,7 +56029,10 @@ jobs:
|
|
|
55991
56029
|
react-doctor:
|
|
55992
56030
|
runs-on: ubuntu-latest
|
|
55993
56031
|
steps:
|
|
56032
|
+
# fetch-depth: 0 gives React Doctor the full git history it needs to find the merge base with the target branch. Without it a shallow checkout has no merge base, so PR runs can't compare against the base and fall back to reporting every issue in the changed files (pre-existing ones included) instead of only the ones the PR introduced.
|
|
55994
56033
|
- uses: actions/checkout@v5
|
|
56034
|
+
with:
|
|
56035
|
+
fetch-depth: 0
|
|
55995
56036
|
|
|
55996
56037
|
- uses: millionco/react-doctor@${actionRef}
|
|
55997
56038
|
# Advisory by default: React Doctor reports findings on every PR — a
|
|
@@ -56289,7 +56330,10 @@ jobs:
|
|
|
56289
56330
|
react-doctor:
|
|
56290
56331
|
runs-on: ubuntu-latest
|
|
56291
56332
|
steps:
|
|
56333
|
+
# fetch-depth: 0 gives React Doctor the full git history it needs to find the merge base with the target branch. Without it a shallow checkout has no merge base, so PR runs can't compare against the base and fall back to reporting every issue in the changed files (pre-existing ones included) instead of only the ones the PR introduced.
|
|
56292
56334
|
- uses: actions/checkout@v5
|
|
56335
|
+
with:
|
|
56336
|
+
fetch-depth: 0
|
|
56293
56337
|
|
|
56294
56338
|
- uses: millionco/react-doctor@${actionRef}
|
|
56295
56339
|
${WITH_INDENT}with:
|
|
@@ -56332,12 +56376,8 @@ const parseGate$1 = (content) => {
|
|
|
56332
56376
|
};
|
|
56333
56377
|
};
|
|
56334
56378
|
const surgicalApplyGate = (content, gate) => {
|
|
56335
|
-
|
|
56336
|
-
|
|
56337
|
-
doc = import_dist.parseDocument(content);
|
|
56338
|
-
} catch {
|
|
56339
|
-
return null;
|
|
56340
|
-
}
|
|
56379
|
+
const doc = import_dist.parseDocument(content);
|
|
56380
|
+
if (doc.errors.length > 0) return null;
|
|
56341
56381
|
const located = findReactDoctorStep(doc);
|
|
56342
56382
|
if (located === null) return null;
|
|
56343
56383
|
let withMap = located.withMap;
|
|
@@ -56745,23 +56785,20 @@ const promptForGate = async (provider, currentGate, prompt) => {
|
|
|
56745
56785
|
const openCiPullRequest = async (params) => {
|
|
56746
56786
|
const pullRequestSpinner = spinner("Opening a pull request for review...").start();
|
|
56747
56787
|
const result = await openWorkflowPullRequest(params);
|
|
56748
|
-
if (result.status === "pr-opened") {
|
|
56749
|
-
|
|
56750
|
-
|
|
56751
|
-
|
|
56752
|
-
|
|
56753
|
-
|
|
56754
|
-
|
|
56755
|
-
|
|
56756
|
-
|
|
56757
|
-
|
|
56758
|
-
return "pr";
|
|
56788
|
+
if (result.status === "pr-opened") pullRequestSpinner.succeed(`Opened pull request for review: ${highlighter.info(result.url)}`);
|
|
56789
|
+
else if (result.status === "pr-exists") pullRequestSpinner.succeed(`A React Doctor pull request is already open: ${highlighter.info(result.url)}`);
|
|
56790
|
+
else if (result.status === "branch-pushed") pullRequestSpinner.warn(`Pushed ${highlighter.bold(result.branch)} but couldn't open a PR. Open one with: gh pr create --head ${result.branch}`);
|
|
56791
|
+
else {
|
|
56792
|
+
pullRequestSpinner.stop();
|
|
56793
|
+
const didStage = await stageWorkflowFile({
|
|
56794
|
+
workflowPath: params.workflowPath,
|
|
56795
|
+
run: params.run
|
|
56796
|
+
});
|
|
56797
|
+
cliLogger.dim(didStage ? " Staged the change. Commit it to apply." : " Review and commit the change to apply it.");
|
|
56759
56798
|
}
|
|
56760
|
-
|
|
56761
|
-
const didStage = await stageWorkflowFile({ workflowPath: params.workflowPath });
|
|
56762
|
-
cliLogger.dim(didStage ? " Staged the change. Commit it to apply." : " Review and commit the change to apply it.");
|
|
56763
|
-
return "tree";
|
|
56799
|
+
return result.status;
|
|
56764
56800
|
};
|
|
56801
|
+
const pullRequestMode = (status) => status === "not-attempted" ? "tree" : "pr";
|
|
56765
56802
|
const runCiInstall = async (options = {}) => {
|
|
56766
56803
|
const projectRoot = resolveProjectRoot$1(options);
|
|
56767
56804
|
const prompt = options.prompt ?? prompts;
|
|
@@ -56800,10 +56837,12 @@ const runCiInstall = async (options = {}) => {
|
|
|
56800
56837
|
}
|
|
56801
56838
|
scaffoldSpinner.succeed(`Added ${provider.fileLabel}.`);
|
|
56802
56839
|
let mode = "tree";
|
|
56803
|
-
if (provider.supportsPullRequest && options.pr) mode = await openCiPullRequest({
|
|
56840
|
+
if (provider.supportsPullRequest && options.pr) mode = pullRequestMode(await openCiPullRequest({
|
|
56804
56841
|
workflowPath: result.path,
|
|
56805
|
-
baseBranch: defaultBranch
|
|
56806
|
-
|
|
56842
|
+
baseBranch: defaultBranch,
|
|
56843
|
+
run: options.run,
|
|
56844
|
+
checkCommandAvailable: options.checkCommandAvailable
|
|
56845
|
+
}));
|
|
56807
56846
|
else cliLogger.dim(" Review and commit it to start scanning every pull request.");
|
|
56808
56847
|
printGateSummary(provider, gate);
|
|
56809
56848
|
cliLogger.log(` Learn more: ${highlighter.info(setupDocsUrl(provider))}`);
|
|
@@ -56849,11 +56888,28 @@ const runCiUpgrade = async (options = {}) => {
|
|
|
56849
56888
|
process.exitCode = 1;
|
|
56850
56889
|
return;
|
|
56851
56890
|
}
|
|
56852
|
-
|
|
56891
|
+
const status = await openCiPullRequest({
|
|
56853
56892
|
workflowPath: workflow.path,
|
|
56854
56893
|
baseBranch: await detectDefaultBranch(projectRoot, run) ?? "main",
|
|
56855
|
-
...upgradeCopy
|
|
56894
|
+
...upgradeCopy,
|
|
56895
|
+
run: options.run,
|
|
56896
|
+
checkCommandAvailable: options.checkCommandAvailable
|
|
56856
56897
|
});
|
|
56898
|
+
if (status === "pr-exists") {
|
|
56899
|
+
try {
|
|
56900
|
+
NFS.writeFileSync(workflow.path, workflow.content);
|
|
56901
|
+
} catch {
|
|
56902
|
+
cliLogger.dim(" The upgrade edit is still in your working tree — review and commit it.");
|
|
56903
|
+
return;
|
|
56904
|
+
}
|
|
56905
|
+
cliLogger.dim(` Merge or close that PR, then re-run ${highlighter.info("react-doctor ci upgrade")} if the workflow still needs it.`);
|
|
56906
|
+
recordCount(METRIC.ciUpgraded, 1, {
|
|
56907
|
+
provider: provider.id,
|
|
56908
|
+
mode: "pr-exists"
|
|
56909
|
+
});
|
|
56910
|
+
return;
|
|
56911
|
+
}
|
|
56912
|
+
mode = pullRequestMode(status);
|
|
56857
56913
|
} else {
|
|
56858
56914
|
const upgradeSpinner = spinner("Upgrading workflow to @v2...").start();
|
|
56859
56915
|
try {
|
|
@@ -56951,7 +57007,6 @@ const runCiConfig = async (options = {}) => {
|
|
|
56951
57007
|
};
|
|
56952
57008
|
//#endregion
|
|
56953
57009
|
//#region src/cli/utils/is-environment-error.ts
|
|
56954
|
-
const isNodeSystemError = (error) => error instanceof Error && typeof error.code === "string";
|
|
56955
57010
|
const ENVIRONMENT_ERROR_CODES = new Set([
|
|
56956
57011
|
"ENOSPC",
|
|
56957
57012
|
"EIO",
|
|
@@ -56961,12 +57016,12 @@ const ENVIRONMENT_ERROR_CODES = new Set([
|
|
|
56961
57016
|
"ENOTDIR"
|
|
56962
57017
|
]);
|
|
56963
57018
|
const isEnvironmentError = (error) => {
|
|
56964
|
-
if (!
|
|
57019
|
+
if (!isErrnoException(error) || typeof error.code !== "string") return false;
|
|
56965
57020
|
if (error.code === "ENOENT") return error.syscall?.startsWith("spawn") ?? false;
|
|
56966
|
-
return
|
|
57021
|
+
return ENVIRONMENT_ERROR_CODES.has(error.code);
|
|
56967
57022
|
};
|
|
56968
57023
|
const formatEnvironmentError = (error) => {
|
|
56969
|
-
if (!
|
|
57024
|
+
if (!isErrnoException(error)) return messageFromUnknown(error);
|
|
56970
57025
|
switch (error.code) {
|
|
56971
57026
|
case "ENOSPC": return "No space left on device. Free up disk space and try again.";
|
|
56972
57027
|
case "EIO": return "I/O error: the filesystem or disk may be failing. Check your system logs.";
|
|
@@ -57069,7 +57124,7 @@ const handleError = (error, options = {}) => {
|
|
|
57069
57124
|
*/
|
|
57070
57125
|
const handleUserError = (error, options = {}) => {
|
|
57071
57126
|
const isEnvError = isEnvironmentError(error);
|
|
57072
|
-
if (isEnvError) recordCount(METRIC.cliEnvironmentError, 1, { code: error.code ?? "unknown" });
|
|
57127
|
+
if (isEnvError) recordCount(METRIC.cliEnvironmentError, 1, { code: (isErrnoException(error) ? error.code : void 0) ?? "unknown" });
|
|
57073
57128
|
const message = isEnvError ? formatEnvironmentError(error) : formatErrorForReport(error);
|
|
57074
57129
|
runSync(gen(function* () {
|
|
57075
57130
|
yield* error$1("");
|
|
@@ -57583,7 +57638,7 @@ const recordScanMetrics = (input) => {
|
|
|
57583
57638
|
});
|
|
57584
57639
|
if (result.diagnostics.length === 0 && !hasSkippedChecks) recordCount(METRIC.scanClean, 1, { mode: input.mode });
|
|
57585
57640
|
if (result.score) recordDistribution(METRIC.scanScore, result.score.score, { attributes: { mode: input.mode } });
|
|
57586
|
-
else if (!input.noScore && !input.didLintFail) recordCount(METRIC.scoreUnavailable, 1, { mode: input.mode });
|
|
57641
|
+
else if (!input.noScore && !input.didLintFail && !input.didDeadCodeFail) recordCount(METRIC.scoreUnavailable, 1, { mode: input.mode });
|
|
57587
57642
|
if (input.baselineDegraded) recordCount(METRIC.baselineDegraded, 1);
|
|
57588
57643
|
if (input.didLintFail) recordCount(METRIC.lintFailed, 1, { reasonKind: input.lintFailureReasonKind });
|
|
57589
57644
|
if (input.didDeadCodeFail) recordCount(METRIC.deadCodeFailed, 1);
|
|
@@ -57772,6 +57827,7 @@ const buildOutcomeAttributes = (input) => {
|
|
|
57772
57827
|
failureReasonKind: input.lintFailureReasonKind ?? null,
|
|
57773
57828
|
partialFailureCount: input.lintPartialFailureCount ?? null,
|
|
57774
57829
|
droppedFileCount: input.lintDroppedFileCount ?? null,
|
|
57830
|
+
deadlineSkippedFileCount: input.lintDeadlineSkippedFileCount ?? null,
|
|
57775
57831
|
cacheHitFiles: result.lintCacheHitFileCount ?? null,
|
|
57776
57832
|
cacheTotalFiles: result.lintCacheTotalFileCount ?? null,
|
|
57777
57833
|
cacheHitRatio: result.lintCacheTotalFileCount != null && result.lintCacheTotalFileCount > 0 ? (result.lintCacheHitFileCount ?? 0) / result.lintCacheTotalFileCount : null
|
|
@@ -57819,6 +57875,7 @@ const buildScanAttributes = (input) => {
|
|
|
57819
57875
|
scope: input.scope,
|
|
57820
57876
|
parallel: input.parallel,
|
|
57821
57877
|
workerCount: input.workerCount ?? null,
|
|
57878
|
+
maxDurationMs: input.maxDurationMs,
|
|
57822
57879
|
lint: input.lint,
|
|
57823
57880
|
deadCode: input.deadCode,
|
|
57824
57881
|
scoreOnly: input.scoreOnly,
|
|
@@ -57884,13 +57941,20 @@ const resolveWorkerTelemetry = (resolvedWorkerCount, pinnedConcurrency) => {
|
|
|
57884
57941
|
};
|
|
57885
57942
|
};
|
|
57886
57943
|
//#endregion
|
|
57887
|
-
//#region src/cli/utils/
|
|
57888
|
-
const
|
|
57889
|
-
const
|
|
57890
|
-
const match = DROPPED_FILES_MESSAGE_PATTERN.exec(message);
|
|
57944
|
+
//#region src/cli/utils/sum-file-counts-matching.ts
|
|
57945
|
+
const sumFileCountsMatching = (messages, countPattern) => messages.reduce((total, message) => {
|
|
57946
|
+
const match = countPattern.exec(message);
|
|
57891
57947
|
return match ? total + Number(match[1]) : total;
|
|
57892
57948
|
}, 0);
|
|
57893
57949
|
//#endregion
|
|
57950
|
+
//#region src/cli/utils/count-deadline-skipped-files.ts
|
|
57951
|
+
const DEADLINE_SKIPPED_MESSAGE_PATTERN = /^(\d+) file\(s\) skipped — max scan duration reached/;
|
|
57952
|
+
const countDeadlineSkippedFiles = (lintPartialFailures) => sumFileCountsMatching(lintPartialFailures, DEADLINE_SKIPPED_MESSAGE_PATTERN);
|
|
57953
|
+
//#endregion
|
|
57954
|
+
//#region src/cli/utils/count-dropped-lint-files.ts
|
|
57955
|
+
const DROPPED_FILES_MESSAGE_PATTERN = /^(\d+) file\(s\) failed to lint and were skipped/;
|
|
57956
|
+
const countDroppedLintFiles = (lintPartialFailures) => sumFileCountsMatching(lintPartialFailures, DROPPED_FILES_MESSAGE_PATTERN);
|
|
57957
|
+
//#endregion
|
|
57894
57958
|
//#region src/cli/utils/materialize-baseline-files.ts
|
|
57895
57959
|
/**
|
|
57896
57960
|
* Materializes the `ref` version of `files` into `tempDirectory` (via
|
|
@@ -58529,7 +58593,7 @@ const foldLegacyDecisions = (projects, legacy, eventId) => {
|
|
|
58529
58593
|
}
|
|
58530
58594
|
};
|
|
58531
58595
|
const migrateCliState = (state) => {
|
|
58532
|
-
if (state.schemaVersion === 2) return state;
|
|
58596
|
+
if (typeof state.schemaVersion === "number" && state.schemaVersion >= 2) return state;
|
|
58533
58597
|
const projects = {};
|
|
58534
58598
|
for (const [hash, record] of Object.entries(state.projects ?? {})) {
|
|
58535
58599
|
const carried = {
|
|
@@ -58567,7 +58631,9 @@ const openStore = (options = {}) => new Conf({
|
|
|
58567
58631
|
});
|
|
58568
58632
|
const openMigratedStore = (options) => {
|
|
58569
58633
|
const store = openStore(options);
|
|
58570
|
-
|
|
58634
|
+
const state = store.store;
|
|
58635
|
+
const migrated = migrateCliState(state);
|
|
58636
|
+
if (migrated !== state) store.store = migrated;
|
|
58571
58637
|
return store;
|
|
58572
58638
|
};
|
|
58573
58639
|
const readCliState = (select, fallback, options = {}) => {
|
|
@@ -59062,7 +59128,6 @@ const TOOLCHAIN_PACKAGE_SPECIFIERS = [
|
|
|
59062
59128
|
"eslint-plugin-react-hooks/package.json"
|
|
59063
59129
|
];
|
|
59064
59130
|
const bundledRequire = createRequire(import.meta.url);
|
|
59065
|
-
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
59066
59131
|
const normalizeForStableJson = (value) => {
|
|
59067
59132
|
if (value === null) return null;
|
|
59068
59133
|
if (value === void 0) return void 0;
|
|
@@ -59075,10 +59140,10 @@ const normalizeForStableJson = (value) => {
|
|
|
59075
59140
|
});
|
|
59076
59141
|
const prototype = Object.getPrototypeOf(value);
|
|
59077
59142
|
if (prototype !== Object.prototype && prototype !== null) throw new Error("Unsupported cache key object");
|
|
59078
|
-
|
|
59143
|
+
if (!isRecord(value)) throw new Error("Unsupported cache key object");
|
|
59079
59144
|
const normalizedRecord = {};
|
|
59080
|
-
for (const key of Object.keys(
|
|
59081
|
-
const normalized = normalizeForStableJson(
|
|
59145
|
+
for (const key of Object.keys(value).sort()) {
|
|
59146
|
+
const normalized = normalizeForStableJson(value[key]);
|
|
59082
59147
|
if (normalized !== void 0) normalizedRecord[key] = normalized;
|
|
59083
59148
|
}
|
|
59084
59149
|
return normalizedRecord;
|
|
@@ -59296,6 +59361,7 @@ const mergeInspectOptions = (inputOptions, userConfig) => ({
|
|
|
59296
59361
|
suppressRendering: inputOptions.suppressRendering ?? false,
|
|
59297
59362
|
concurrentScan: inputOptions.concurrentScan ?? false,
|
|
59298
59363
|
concurrency: inputOptions.concurrency,
|
|
59364
|
+
maxDurationMs: inputOptions.maxDurationMs ?? null,
|
|
59299
59365
|
baseline: inputOptions.baseline ?? null,
|
|
59300
59366
|
changedLineRanges: inputOptions.changedLineRanges ?? null,
|
|
59301
59367
|
supplyChainManifestChanged: inputOptions.supplyChainManifestChanged ?? false
|
|
@@ -59311,6 +59377,7 @@ const buildRunEventConfig = (options, userConfig, hasCustomConfig, resolvedWorke
|
|
|
59311
59377
|
scope: deriveScope(options),
|
|
59312
59378
|
parallel,
|
|
59313
59379
|
workerCount,
|
|
59380
|
+
maxDurationMs: options.maxDurationMs,
|
|
59314
59381
|
lint: options.lint,
|
|
59315
59382
|
deadCode: options.deadCode,
|
|
59316
59383
|
scoreOnly: options.scoreOnly,
|
|
@@ -59325,6 +59392,7 @@ const buildRunEventConfig = (options, userConfig, hasCustomConfig, resolvedWorke
|
|
|
59325
59392
|
};
|
|
59326
59393
|
const inspect = async (directory, inputOptions = {}) => {
|
|
59327
59394
|
const startTime = performance$1.now();
|
|
59395
|
+
const deadlineEpochMs = inputOptions.deadlineEpochMs ?? (inputOptions.maxDurationMs != null ? Date.now() + inputOptions.maxDurationMs : null);
|
|
59328
59396
|
const isConcurrentScan = inputOptions.concurrentScan === true;
|
|
59329
59397
|
if (!isConcurrentScan) resetSentryRunState();
|
|
59330
59398
|
const hasConfigOverride = inputOptions.configOverride !== void 0;
|
|
@@ -59348,7 +59416,7 @@ const inspect = async (directory, inputOptions = {}) => {
|
|
|
59348
59416
|
try {
|
|
59349
59417
|
const result = await withSentryRunSpan(async (rootSentrySpan) => {
|
|
59350
59418
|
try {
|
|
59351
|
-
return await runInspectWithRuntime(scanDirectory, options, userConfig, hasConfigOverride, configSourceDirectory, startTime, rootSentrySpan);
|
|
59419
|
+
return await runInspectWithRuntime(scanDirectory, options, userConfig, hasConfigOverride, configSourceDirectory, startTime, deadlineEpochMs, rootSentrySpan);
|
|
59352
59420
|
} catch (error) {
|
|
59353
59421
|
recordRunEvent(rootSentrySpan, {
|
|
59354
59422
|
...buildRunEventConfig(options, userConfig, userConfig !== null),
|
|
@@ -59364,6 +59432,7 @@ const inspect = async (directory, inputOptions = {}) => {
|
|
|
59364
59432
|
if (ownsSpinnerSilence) setSpinnerSilent(wasSpinnerSilent);
|
|
59365
59433
|
}
|
|
59366
59434
|
};
|
|
59435
|
+
const countIncompleteLintFiles = (lintPartialFailures) => countDroppedLintFiles(lintPartialFailures) + countDeadlineSkippedFiles(lintPartialFailures);
|
|
59367
59436
|
/**
|
|
59368
59437
|
* Runs a second, lint-only scan over the changed files as they existed at the
|
|
59369
59438
|
* baseline ref (materialized into a temp tree with head's config) and diffs it
|
|
@@ -59390,7 +59459,7 @@ const runBaselineComparison = async (params) => {
|
|
|
59390
59459
|
directory: snapshot.tempDirectory,
|
|
59391
59460
|
hasConfigOverride: true,
|
|
59392
59461
|
userConfig: params.userConfig,
|
|
59393
|
-
configSourceDirectory:
|
|
59462
|
+
configSourceDirectory: params.configSourceDirectory,
|
|
59394
59463
|
projectInfoOverride: params.headProjectInfo,
|
|
59395
59464
|
shouldSkipLint: !params.options.lint || !params.resolvedNodeBinaryPath,
|
|
59396
59465
|
shouldRunDeadCode: false,
|
|
@@ -59413,9 +59482,10 @@ const runBaselineComparison = async (params) => {
|
|
|
59413
59482
|
runId: getRunId(),
|
|
59414
59483
|
resolveLocalGithubViewerPermission: false,
|
|
59415
59484
|
suppressScanSummary: true,
|
|
59416
|
-
supplyChainManifestChanged: params.options.supplyChainManifestChanged
|
|
59485
|
+
supplyChainManifestChanged: params.options.supplyChainManifestChanged,
|
|
59486
|
+
deadlineEpochMs: params.deadlineEpochMs ?? void 0
|
|
59417
59487
|
}, {}).pipe(provide(baseLayers), provideService$2(Console, silentConsole))));
|
|
59418
|
-
if (baseOutput.didLintFail) return null;
|
|
59488
|
+
if (baseOutput.didLintFail || countIncompleteLintFiles(baseOutput.lintPartialFailures) > 0) return null;
|
|
59419
59489
|
const delta = computeDiagnosticDelta({
|
|
59420
59490
|
headDiagnostics: params.headDiagnostics,
|
|
59421
59491
|
baseDiagnostics: baseOutput.diagnostics,
|
|
@@ -59434,7 +59504,7 @@ const runBaselineComparison = async (params) => {
|
|
|
59434
59504
|
snapshot.cleanup();
|
|
59435
59505
|
}
|
|
59436
59506
|
};
|
|
59437
|
-
const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOverride, configSourceDirectory, startTime, rootSentrySpan) => {
|
|
59507
|
+
const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOverride, configSourceDirectory, startTime, deadlineEpochMs, rootSentrySpan) => {
|
|
59438
59508
|
const isDiffMode = options.includePaths.length > 0;
|
|
59439
59509
|
const resolvedNodeBinaryPath = await resolveOxlintNode(options.lint, options.scoreOnly || options.silent);
|
|
59440
59510
|
const lintBindingMissing = options.lint && !resolvedNodeBinaryPath;
|
|
@@ -59500,7 +59570,8 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
|
|
|
59500
59570
|
resolveLocalGithubViewerPermission: !options.noScore,
|
|
59501
59571
|
suppressScanSummary: options.suppressRendering,
|
|
59502
59572
|
supplyChainManifestChanged: options.supplyChainManifestChanged,
|
|
59503
|
-
concurrentScan: options.concurrentScan
|
|
59573
|
+
concurrentScan: options.concurrentScan,
|
|
59574
|
+
deadlineEpochMs: deadlineEpochMs ?? void 0
|
|
59504
59575
|
}, { beforeLint: (projectInfo, lintIncludePaths) => gen(function* () {
|
|
59505
59576
|
recordSentryProjectContext(projectInfo, rootSentrySpan, { concurrentScan: options.concurrentScan });
|
|
59506
59577
|
recordCount(METRIC.projectDetected, 1);
|
|
@@ -59521,15 +59592,17 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
|
|
|
59521
59592
|
else runConsole(error$1(highlighter.error(lintFailureReason)));
|
|
59522
59593
|
let inspectDiagnostics = output.diagnostics;
|
|
59523
59594
|
let baselineDelta;
|
|
59524
|
-
if (options.baseline && isDiffMode && !didLintFail) {
|
|
59595
|
+
if (options.baseline && isDiffMode && !didLintFail && countIncompleteLintFiles(output.lintPartialFailures) === 0) {
|
|
59525
59596
|
const comparison = await runBaselineComparison({
|
|
59526
59597
|
directory,
|
|
59527
59598
|
options,
|
|
59528
59599
|
userConfig,
|
|
59600
|
+
configSourceDirectory,
|
|
59529
59601
|
headProjectInfo: output.project,
|
|
59530
59602
|
headDiagnostics: output.diagnostics,
|
|
59531
59603
|
resolvedNodeBinaryPath,
|
|
59532
|
-
baselineRef: options.baseline.ref
|
|
59604
|
+
baselineRef: options.baseline.ref,
|
|
59605
|
+
deadlineEpochMs
|
|
59533
59606
|
});
|
|
59534
59607
|
if (comparison) {
|
|
59535
59608
|
inspectDiagnostics = comparison.displayDiagnostics;
|
|
@@ -59563,7 +59636,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
|
|
|
59563
59636
|
securityScanFailed: output.securityScanFailed,
|
|
59564
59637
|
suppressedRuleCounts: output.suppressedRuleCounts
|
|
59565
59638
|
};
|
|
59566
|
-
if (cacheKey !== null && scanResultCache !== null && shouldStoreScanPayload(payload)) scanResultCache.store(cacheKey, payload);
|
|
59639
|
+
if (cacheKey !== null && scanResultCache !== null && shouldStoreScanPayload(payload) && !baselineDegraded) scanResultCache.store(cacheKey, payload);
|
|
59567
59640
|
const result = await renderAndRecordScan({
|
|
59568
59641
|
payload,
|
|
59569
59642
|
options,
|
|
@@ -59637,6 +59710,7 @@ const renderAndRecordScan = async (input) => {
|
|
|
59637
59710
|
lintFailureReasonKind: input.payload.lintFailureReasonKind,
|
|
59638
59711
|
lintPartialFailureCount: input.payload.lintPartialFailures.length,
|
|
59639
59712
|
lintDroppedFileCount: countDroppedLintFiles(input.payload.lintPartialFailures),
|
|
59713
|
+
lintDeadlineSkippedFileCount: countDeadlineSkippedFiles(input.payload.lintPartialFailures),
|
|
59640
59714
|
didDeadCodeFail: input.payload.didDeadCodeFail,
|
|
59641
59715
|
supplyChainOverlapTimedOut: input.payload.supplyChainOverlapTimedOut,
|
|
59642
59716
|
securityScanFailed: input.payload.securityScanFailed,
|
|
@@ -59752,7 +59826,7 @@ const getStagedSourceFiles = async (directory) => {
|
|
|
59752
59826
|
return yield* (yield* StagedFiles).discoverSourceFiles(directory);
|
|
59753
59827
|
}).pipe(provide(stagedFilesLayer)))];
|
|
59754
59828
|
} catch (error) {
|
|
59755
|
-
cliLogger.warn(`Failed to discover staged files: ${
|
|
59829
|
+
cliLogger.warn(`Failed to discover staged files: ${messageFromUnknown(error)}`);
|
|
59756
59830
|
return [];
|
|
59757
59831
|
}
|
|
59758
59832
|
};
|
|
@@ -59771,6 +59845,9 @@ const materializeStagedFiles = async (directory, stagedFiles, tempDirectory) =>
|
|
|
59771
59845
|
};
|
|
59772
59846
|
};
|
|
59773
59847
|
//#endregion
|
|
59848
|
+
//#region src/cli/utils/is-share-opted-out.ts
|
|
59849
|
+
const isShareOptedOut = (scans, flagNoScore) => Boolean(flagNoScore) || scans.some((scan) => Boolean(scan.config?.noScore) || scan.config?.share === false);
|
|
59850
|
+
//#endregion
|
|
59774
59851
|
//#region src/cli/utils/build-handoff-payload.ts
|
|
59775
59852
|
const buildHandoffPayload = (input) => {
|
|
59776
59853
|
const topGroups = buildSortedRuleGroups(input.diagnostics).slice(0, 3);
|
|
@@ -60053,6 +60130,8 @@ const CURSOR_HOOK_RELATIVE_PATH = ".cursor/hooks/react-doctor.mjs";
|
|
|
60053
60130
|
const CURSOR_HOOK_COMMAND = "node .cursor/hooks/react-doctor.mjs";
|
|
60054
60131
|
const CURSOR_HOOK_MATCHER = "Write|Edit|MultiEdit|ApplyPatch";
|
|
60055
60132
|
const CURSOR_HOOKS_SCHEMA_VERSION = 1;
|
|
60133
|
+
const LEGACY_HOOK_SCRIPT_PATHS = [".claude/hooks/react-doctor.sh", ".cursor/hooks/react-doctor.sh"];
|
|
60134
|
+
const isLegacyHookCommand = (command) => typeof command === "string" && LEGACY_HOOK_SCRIPT_PATHS.some((legacyPath) => command.includes(legacyPath));
|
|
60056
60135
|
const isSupportedAgent = (agent) => agent === CLAUDE_AGENT || agent === CURSOR_AGENT;
|
|
60057
60136
|
const readJsonFile = (filePath, fallback) => {
|
|
60058
60137
|
if (!NFS.existsSync(filePath)) return fallback;
|
|
@@ -60065,11 +60144,24 @@ const readJsonFile = (filePath, fallback) => {
|
|
|
60065
60144
|
throw error;
|
|
60066
60145
|
}
|
|
60067
60146
|
};
|
|
60147
|
+
const readJsonFileSafely = (filePath, fallback) => {
|
|
60148
|
+
try {
|
|
60149
|
+
return readJsonFile(filePath, fallback);
|
|
60150
|
+
} catch {
|
|
60151
|
+
return fallback;
|
|
60152
|
+
}
|
|
60153
|
+
};
|
|
60154
|
+
const findAgentsWithLegacyShellHooks = (projectRoot) => {
|
|
60155
|
+
const agents = [];
|
|
60156
|
+
if ((readJsonFileSafely(Path.join(projectRoot, CLAUDE_SETTINGS_RELATIVE_PATH), {}).hooks?.PostToolBatch ?? []).some((group) => (group.hooks ?? []).some((hook) => isLegacyHookCommand(hook.command)))) agents.push(CLAUDE_AGENT);
|
|
60157
|
+
if ((readJsonFileSafely(Path.join(projectRoot, CURSOR_HOOKS_RELATIVE_PATH), {}).hooks?.postToolUse ?? []).some((handler) => isLegacyHookCommand(handler.command))) agents.push(CURSOR_AGENT);
|
|
60158
|
+
return agents;
|
|
60159
|
+
};
|
|
60068
60160
|
const ensureDirectoryExists = (directoryPath) => {
|
|
60069
60161
|
try {
|
|
60070
60162
|
NFS.mkdirSync(directoryPath, { recursive: true });
|
|
60071
60163
|
} catch (error) {
|
|
60072
|
-
const code = error.code;
|
|
60164
|
+
const code = isErrnoException(error) ? error.code : void 0;
|
|
60073
60165
|
if (code === "EACCES" || code === "EPERM") throw new CliInputError(`Could not create directory ${directoryPath}: permission denied. Ensure you have write permissions for this location and re-run the install command.`);
|
|
60074
60166
|
if (code === "ENOTDIR" || code === "EEXIST") throw new CliInputError(`Could not create directory ${directoryPath}: a file exists at this path or one of its parent paths. Remove the conflicting file and re-run the install command.`);
|
|
60075
60167
|
throw error;
|
|
@@ -60082,6 +60174,9 @@ const writeJsonFileWithDirectoryCheck = (filePath, value) => {
|
|
|
60082
60174
|
const writeHookScript = (filePath) => {
|
|
60083
60175
|
ensureDirectoryExists(Path.dirname(filePath));
|
|
60084
60176
|
NFS.writeFileSync(filePath, buildAgentHookScript());
|
|
60177
|
+
try {
|
|
60178
|
+
NFS.rmSync(filePath.replace(/\.mjs$/, ".sh"), { force: true });
|
|
60179
|
+
} catch {}
|
|
60085
60180
|
};
|
|
60086
60181
|
const hasClaudeHookCommand = (groups) => groups.some((group) => (group.hooks ?? []).some((hook) => hook.command === CLAUDE_HOOK_COMMAND));
|
|
60087
60182
|
const installClaudeHook = (projectRoot) => {
|
|
@@ -60089,7 +60184,15 @@ const installClaudeHook = (projectRoot) => {
|
|
|
60089
60184
|
const hookPath = Path.join(projectRoot, CLAUDE_HOOK_RELATIVE_PATH);
|
|
60090
60185
|
const settings = readJsonFile(settingsPath, {});
|
|
60091
60186
|
const hooks = { ...settings.hooks ?? {} };
|
|
60092
|
-
const postToolBatchHooks =
|
|
60187
|
+
const postToolBatchHooks = (hooks.PostToolBatch ?? []).flatMap((group) => {
|
|
60188
|
+
const groupHooks = group.hooks ?? [];
|
|
60189
|
+
const keptHooks = groupHooks.filter((hook) => !isLegacyHookCommand(hook.command));
|
|
60190
|
+
if (keptHooks.length === groupHooks.length) return [group];
|
|
60191
|
+
return keptHooks.length > 0 ? [{
|
|
60192
|
+
...group,
|
|
60193
|
+
hooks: keptHooks
|
|
60194
|
+
}] : [];
|
|
60195
|
+
});
|
|
60093
60196
|
if (!hasClaudeHookCommand(postToolBatchHooks)) postToolBatchHooks.push({ hooks: [{
|
|
60094
60197
|
type: "command",
|
|
60095
60198
|
command: CLAUDE_HOOK_COMMAND
|
|
@@ -60108,7 +60211,7 @@ const installCursorHook = (projectRoot) => {
|
|
|
60108
60211
|
const hookPath = Path.join(projectRoot, CURSOR_HOOK_RELATIVE_PATH);
|
|
60109
60212
|
const config = readJsonFile(configPath, {});
|
|
60110
60213
|
const hooks = { ...config.hooks ?? {} };
|
|
60111
|
-
const postToolUseHooks =
|
|
60214
|
+
const postToolUseHooks = (hooks.postToolUse ?? []).filter((handler) => !isLegacyHookCommand(handler.command));
|
|
60112
60215
|
if (!hasCursorHookCommand(postToolUseHooks)) postToolUseHooks.push({
|
|
60113
60216
|
command: CURSOR_HOOK_COMMAND,
|
|
60114
60217
|
matcher: CURSOR_HOOK_MATCHER,
|
|
@@ -60124,7 +60227,7 @@ const installCursorHook = (projectRoot) => {
|
|
|
60124
60227
|
return [configPath, hookPath];
|
|
60125
60228
|
};
|
|
60126
60229
|
const buildAgentHookScript = () => [
|
|
60127
|
-
"import { readFileSync, writeFileSync, unlinkSync } from 'node:fs';",
|
|
60230
|
+
"import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs';",
|
|
60128
60231
|
"import { tmpdir } from 'node:os';",
|
|
60129
60232
|
"import { join, dirname } from 'node:path';",
|
|
60130
60233
|
"import { fileURLToPath } from 'node:url';",
|
|
@@ -60133,11 +60236,14 @@ const buildAgentHookScript = () => [
|
|
|
60133
60236
|
"const __filename = fileURLToPath(import.meta.url);",
|
|
60134
60237
|
"const __dirname = dirname(__filename);",
|
|
60135
60238
|
"",
|
|
60239
|
+
"// --verbose scans on large diffs can exceed spawnSync's 1 MiB default.",
|
|
60240
|
+
"const SPAWN_MAX_BUFFER_BYTES = 16 * 1024 * 1024;",
|
|
60241
|
+
"",
|
|
60136
60242
|
"const EDIT_TOOL_NAMES = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'ApplyPatch']);",
|
|
60137
60243
|
"",
|
|
60138
|
-
"const
|
|
60244
|
+
"const readFileOrEmpty = (source) => {",
|
|
60139
60245
|
" try {",
|
|
60140
|
-
" return readFileSync(
|
|
60246
|
+
" return readFileSync(source, 'utf8');",
|
|
60141
60247
|
" } catch {",
|
|
60142
60248
|
" return '';",
|
|
60143
60249
|
" }",
|
|
@@ -60157,18 +60263,25 @@ const buildAgentHookScript = () => [
|
|
|
60157
60263
|
" // Each candidate is a single shell command string (not an args array):",
|
|
60158
60264
|
" // `shell: true` is required to run the Windows `.cmd` shims, and an args",
|
|
60159
60265
|
" // array with `shell: true` trips Node's DEP0190. A missing command exits",
|
|
60160
|
-
" // 127 via
|
|
60161
|
-
" //
|
|
60266
|
+
" // 127 via a POSIX shell (no ENOENT error) and 9009 via cmd.exe, so fall",
|
|
60267
|
+
" // through on those. The local bin is probed with existsSync (its `./`",
|
|
60268
|
+
" // prefix form is not runnable by cmd.exe at all). With no runner found,",
|
|
60269
|
+
" // exit 0 silently — stdout is parsed as the hook's JSON.",
|
|
60270
|
+
" const localBin = process.platform === 'win32'",
|
|
60271
|
+
" ? 'node_modules\\\\.bin\\\\react-doctor.cmd'",
|
|
60272
|
+
" : './node_modules/.bin/react-doctor';",
|
|
60162
60273
|
" const commands = [",
|
|
60163
|
-
"
|
|
60274
|
+
" ...(existsSync(localBin)",
|
|
60275
|
+
" ? [localBin + ' --verbose --scope changed --blocking warning --no-score']",
|
|
60276
|
+
" : []),",
|
|
60164
60277
|
" 'react-doctor --verbose --scope changed --blocking warning --no-score',",
|
|
60165
60278
|
" 'pnpm dlx react-doctor@latest --verbose --scope changed --blocking warning --no-score',",
|
|
60166
60279
|
" 'npx --yes react-doctor@latest --verbose --scope changed --blocking warning --no-score',",
|
|
60167
60280
|
" ];",
|
|
60168
60281
|
"",
|
|
60169
60282
|
" for (const command of commands) {",
|
|
60170
|
-
" const result = spawnSync(command, { encoding: 'utf8', shell: true });",
|
|
60171
|
-
" if (result.error?.code === 'ENOENT' || result.status === 127) continue;",
|
|
60283
|
+
" const result = spawnSync(command, { encoding: 'utf8', shell: true, maxBuffer: SPAWN_MAX_BUFFER_BYTES });",
|
|
60284
|
+
" if (result.error?.code === 'ENOENT' || result.status === 127 || result.status === 9009) continue;",
|
|
60172
60285
|
" try {",
|
|
60173
60286
|
" writeFileSync(outputPath, (result.stdout || '') + (result.stderr || ''));",
|
|
60174
60287
|
" } catch {}",
|
|
@@ -60187,8 +60300,7 @@ const buildAgentHookScript = () => [
|
|
|
60187
60300
|
"const main = () => {",
|
|
60188
60301
|
" let input;",
|
|
60189
60302
|
" try {",
|
|
60190
|
-
"
|
|
60191
|
-
" input = JSON.parse(stdinContent || '{}');",
|
|
60303
|
+
" input = JSON.parse(readFileOrEmpty(0) || '{}');",
|
|
60192
60304
|
" } catch {",
|
|
60193
60305
|
" input = {};",
|
|
60194
60306
|
" }",
|
|
@@ -60212,7 +60324,9 @@ const buildAgentHookScript = () => [
|
|
|
60212
60324
|
" process.exit(0);",
|
|
60213
60325
|
" }",
|
|
60214
60326
|
"",
|
|
60215
|
-
"
|
|
60327
|
+
" // The write above is best-effort (unwritable tmpdir), so the read is too",
|
|
60328
|
+
" // — a hook must never crash the agent loop with a stack trace.",
|
|
60329
|
+
" const scanOutput = readFileOrEmpty(outputPath).trim();",
|
|
60216
60330
|
" cleanup(outputPath);",
|
|
60217
60331
|
"",
|
|
60218
60332
|
" if (!scanOutput) {",
|
|
@@ -60253,7 +60367,9 @@ const installReactDoctorAgentHooks = (options) => {
|
|
|
60253
60367
|
//#region src/cli/utils/install-git-hook-file.ts
|
|
60254
60368
|
const REACT_DOCTOR_BLOCK_START = "# react-doctor hook start";
|
|
60255
60369
|
const REACT_DOCTOR_BLOCK_END = "# react-doctor hook end";
|
|
60256
|
-
const
|
|
60370
|
+
const REACT_DOCTOR_BLOCK_PATTERN_SOURCE = `(?:${REACT_DOCTOR_BLOCK_START}[\\s\\S]*?${REACT_DOCTOR_BLOCK_END}\\n?|# react-doctor hook launcher start[\\s\\S]*?# react-doctor hook launcher end\\n?)`;
|
|
60371
|
+
const REACT_DOCTOR_BLOCK_PATTERN = new RegExp(REACT_DOCTOR_BLOCK_PATTERN_SOURCE);
|
|
60372
|
+
const ALL_REACT_DOCTOR_BLOCKS_PATTERN = new RegExp(REACT_DOCTOR_BLOCK_PATTERN_SOURCE, "g");
|
|
60257
60373
|
const SHEBANG = "#!/bin/sh";
|
|
60258
60374
|
const SHEBANG_PREFIX = "#!";
|
|
60259
60375
|
const LOCAL_REACT_DOCTOR_BIN = "./node_modules/.bin/react-doctor";
|
|
@@ -60297,7 +60413,16 @@ const buildReactDoctorHookBlock = () => [
|
|
|
60297
60413
|
].join("\n");
|
|
60298
60414
|
const mergeHookContent = (existingContent) => {
|
|
60299
60415
|
const hookBlock = `${buildReactDoctorHookBlock()}\n`;
|
|
60300
|
-
if (REACT_DOCTOR_BLOCK_PATTERN.test(existingContent))
|
|
60416
|
+
if (REACT_DOCTOR_BLOCK_PATTERN.test(existingContent)) {
|
|
60417
|
+
let isFirstManagedBlock = true;
|
|
60418
|
+
return ensureTrailingNewline(existingContent.replace(ALL_REACT_DOCTOR_BLOCKS_PATTERN, () => {
|
|
60419
|
+
if (isFirstManagedBlock) {
|
|
60420
|
+
isFirstManagedBlock = false;
|
|
60421
|
+
return hookBlock;
|
|
60422
|
+
}
|
|
60423
|
+
return "";
|
|
60424
|
+
}));
|
|
60425
|
+
}
|
|
60301
60426
|
if (existingContent.length === 0) return `${SHEBANG}\n\n${hookBlock}`;
|
|
60302
60427
|
const normalizedExistingContent = ensureTrailingNewline(existingContent);
|
|
60303
60428
|
if (normalizedExistingContent.startsWith(SHEBANG_PREFIX)) {
|
|
@@ -60350,13 +60475,13 @@ const installPackageJsonHook = (options, strategy) => {
|
|
|
60350
60475
|
const packageJsonPath = getPackageJsonPath(options.projectRoot);
|
|
60351
60476
|
const didHookExist = NFS.existsSync(packageJsonPath);
|
|
60352
60477
|
const packageJson = readPackageJson(options.projectRoot);
|
|
60353
|
-
const nextPackageJson = isRecord
|
|
60478
|
+
const nextPackageJson = isRecord(packageJson) ? { ...packageJson } : {};
|
|
60354
60479
|
const parentKeys = strategy.path.slice(0, -1);
|
|
60355
60480
|
const leafKey = strategy.path[strategy.path.length - 1];
|
|
60356
60481
|
let parent = nextPackageJson;
|
|
60357
60482
|
for (const key of parentKeys) {
|
|
60358
60483
|
const existing = parent[key];
|
|
60359
|
-
const cloned = isRecord
|
|
60484
|
+
const cloned = isRecord(existing) ? { ...existing } : {};
|
|
60360
60485
|
parent[key] = cloned;
|
|
60361
60486
|
parent = cloned;
|
|
60362
60487
|
}
|
|
@@ -60527,7 +60652,7 @@ const isHuskyProject = (projectRoot) => NFS.existsSync(Path.join(projectRoot, ".
|
|
|
60527
60652
|
const isVitePlusProject = (projectRoot) => packageHasDependency(projectRoot, "vite-plus");
|
|
60528
60653
|
const isSimpleGitHooksProject = (projectRoot) => {
|
|
60529
60654
|
const packageJson = readPackageJson(projectRoot);
|
|
60530
|
-
return isRecord
|
|
60655
|
+
return isRecord(packageJson) && isRecord(packageJson["simple-git-hooks"]) || packageHasDependency(projectRoot, "simple-git-hooks") || NFS.existsSync(Path.join(projectRoot, ".simple-git-hooks.cjs"));
|
|
60531
60656
|
};
|
|
60532
60657
|
const getLefthookConfigPath = (projectRoot) => {
|
|
60533
60658
|
for (const fileName of LEFTHOOK_CONFIG_FILES) {
|
|
@@ -60690,7 +60815,7 @@ const detectPackageManager = (projectRoot) => {
|
|
|
60690
60815
|
let currentDirectory = Path.resolve(projectRoot);
|
|
60691
60816
|
while (true) {
|
|
60692
60817
|
const packageJson = readPackageJson(currentDirectory);
|
|
60693
|
-
if (isRecord
|
|
60818
|
+
if (isRecord(packageJson) && typeof packageJson.packageManager === "string") {
|
|
60694
60819
|
const packageManagerName = packageJson.packageManager.split("@")[0];
|
|
60695
60820
|
if (packageManagerName === "pnpm" || packageManagerName === "yarn" || packageManagerName === "bun" || packageManagerName === "npm") return packageManagerName;
|
|
60696
60821
|
}
|
|
@@ -60766,12 +60891,12 @@ const isSupplyChainTrustError = (error) => {
|
|
|
60766
60891
|
const formatInstallCommand = (input) => [input.command, ...input.args].join(" ");
|
|
60767
60892
|
const installReactDoctorDependency = async (options) => {
|
|
60768
60893
|
const packageJson = readPackageJson(options.projectRoot);
|
|
60769
|
-
if (!isRecord
|
|
60894
|
+
if (!isRecord(packageJson)) return {
|
|
60770
60895
|
dependencyStatus: "skipped",
|
|
60771
60896
|
dependencyReason: "missing-or-invalid-package-json"
|
|
60772
60897
|
};
|
|
60773
60898
|
if (hasDoctorDependency(packageJson)) return { dependencyStatus: "existing" };
|
|
60774
|
-
if (packageJson.devDependencies !== void 0 && !isRecord
|
|
60899
|
+
if (packageJson.devDependencies !== void 0 && !isRecord(packageJson.devDependencies)) return {
|
|
60775
60900
|
dependencyStatus: "skipped",
|
|
60776
60901
|
dependencyReason: "invalid-dev-dependencies"
|
|
60777
60902
|
};
|
|
@@ -61310,7 +61435,7 @@ const handoffToAgent = async (input) => {
|
|
|
61310
61435
|
//#region src/cli/utils/migrate-action-pin.ts
|
|
61311
61436
|
const WORKFLOWS_DIRECTORY = Path.join(".github", "workflows");
|
|
61312
61437
|
const RECOMMENDED_ACTION_REF = "v2";
|
|
61313
|
-
const MUTABLE_ACTION_REF = /(uses:\s*
|
|
61438
|
+
const MUTABLE_ACTION_REF = /(uses:\s*millionco\/react-doctor@)(?:main|master)\b/gi;
|
|
61314
61439
|
const isWorkflowFile = (fileName) => /\.ya?ml$/.test(fileName);
|
|
61315
61440
|
/**
|
|
61316
61441
|
* Rewrites mutable `@main` / `@master` React Doctor GitHub Action references in
|
|
@@ -61412,34 +61537,54 @@ export default ${serializeTsObjectLiteral(config)} satisfies ReactDoctorConfig;
|
|
|
61412
61537
|
NFS.rmSync(legacy.legacyFilePath, { force: true });
|
|
61413
61538
|
return targetPath;
|
|
61414
61539
|
};
|
|
61415
|
-
const PROJECT_MIGRATIONS = [
|
|
61416
|
-
|
|
61417
|
-
|
|
61418
|
-
|
|
61419
|
-
|
|
61420
|
-
|
|
61421
|
-
|
|
61422
|
-
|
|
61423
|
-
|
|
61424
|
-
|
|
61425
|
-
|
|
61426
|
-
|
|
61427
|
-
|
|
61428
|
-
|
|
61429
|
-
}
|
|
61430
|
-
|
|
61431
|
-
|
|
61432
|
-
|
|
61433
|
-
|
|
61434
|
-
|
|
61435
|
-
|
|
61436
|
-
|
|
61437
|
-
|
|
61438
|
-
|
|
61439
|
-
|
|
61440
|
-
|
|
61540
|
+
const PROJECT_MIGRATIONS = [
|
|
61541
|
+
{
|
|
61542
|
+
id: "config-json-to-ts",
|
|
61543
|
+
scope: "project",
|
|
61544
|
+
run: ({ projectRoot }) => {
|
|
61545
|
+
if (projectRoot === void 0) return false;
|
|
61546
|
+
const legacyConfig = findLegacyConfig(projectRoot);
|
|
61547
|
+
if (!legacyConfig) return false;
|
|
61548
|
+
const migratedPath = migrateLegacyConfig(legacyConfig);
|
|
61549
|
+
if (!migratedPath) return false;
|
|
61550
|
+
cliLogger.success("Migrated react-doctor.config.json → doctor.config.ts");
|
|
61551
|
+
cliLogger.dim(` Your settings were preserved. Review ${toRelativePath(migratedPath, projectRoot)} and commit it.`);
|
|
61552
|
+
cliLogger.break();
|
|
61553
|
+
return true;
|
|
61554
|
+
}
|
|
61555
|
+
},
|
|
61556
|
+
{
|
|
61557
|
+
id: "action-pin-main-to-v2",
|
|
61558
|
+
scope: "project",
|
|
61559
|
+
run: ({ projectRoot }) => {
|
|
61560
|
+
if (projectRoot === void 0) return false;
|
|
61561
|
+
const rewrittenFiles = migrateActionPin(projectRoot);
|
|
61562
|
+
if (rewrittenFiles.length === 0) return false;
|
|
61563
|
+
const relativeFiles = rewrittenFiles.map((file) => toRelativePath(file, projectRoot)).join(", ");
|
|
61564
|
+
cliLogger.success(`Pinned the React Doctor action to @v2 in ${relativeFiles}`);
|
|
61565
|
+
cliLogger.dim(" An unpinned @main reference runs whatever the action's HEAD points to (a supply-chain risk). Review and commit the change — or revert it if you intentionally track main.");
|
|
61566
|
+
cliLogger.break();
|
|
61567
|
+
return true;
|
|
61568
|
+
}
|
|
61569
|
+
},
|
|
61570
|
+
{
|
|
61571
|
+
id: "agent-hooks-sh-to-mjs",
|
|
61572
|
+
scope: "project",
|
|
61573
|
+
run: ({ projectRoot }) => {
|
|
61574
|
+
if (projectRoot === void 0) return false;
|
|
61575
|
+
const agents = findAgentsWithLegacyShellHooks(projectRoot);
|
|
61576
|
+
if (agents.length === 0) return false;
|
|
61577
|
+
installReactDoctorAgentHooks({
|
|
61578
|
+
projectRoot,
|
|
61579
|
+
agents
|
|
61580
|
+
});
|
|
61581
|
+
cliLogger.success(`Upgraded the legacy react-doctor.sh agent hook to the Node hook (${agents.join(", ")})`);
|
|
61582
|
+
cliLogger.dim(" The shell hook can't run on Windows and would double-scan next to the current hook. Review and commit the change.");
|
|
61583
|
+
cliLogger.break();
|
|
61584
|
+
return true;
|
|
61585
|
+
}
|
|
61441
61586
|
}
|
|
61442
|
-
|
|
61587
|
+
];
|
|
61443
61588
|
const runProjectMigrations = (projectRoot, options = {}) => runMigrations(PROJECT_MIGRATIONS, { projectRoot }, options);
|
|
61444
61589
|
//#endregion
|
|
61445
61590
|
//#region src/cli/utils/render-welcome.ts
|
|
@@ -61499,7 +61644,7 @@ const readChangedFilesFrom = (filePath) => {
|
|
|
61499
61644
|
try {
|
|
61500
61645
|
raw = NFS.readFileSync(filePath, "utf8");
|
|
61501
61646
|
} catch (error) {
|
|
61502
|
-
const errorCode = error
|
|
61647
|
+
const errorCode = isErrnoException(error) ? error.code : void 0;
|
|
61503
61648
|
throw new CliInputError(`Could not read the --changed-files-from file "${filePath}"${errorCode ? ` (${errorCode})` : ""}. Pass a path to a readable text file that lists changed files, one per line.`);
|
|
61504
61649
|
}
|
|
61505
61650
|
const uniqueFiles = /* @__PURE__ */ new Set();
|
|
@@ -61642,6 +61787,17 @@ const printAgentInstallHint = (writeLine = defaultWriteLine) => {
|
|
|
61642
61787
|
for (const line of AGENT_INSTALL_HINT_LINES) writeLine(line);
|
|
61643
61788
|
};
|
|
61644
61789
|
//#endregion
|
|
61790
|
+
//#region src/cli/utils/resolve-max-duration-flag.ts
|
|
61791
|
+
const resolveMaxDurationFlag = (maxDuration) => {
|
|
61792
|
+
if (maxDuration === void 0) return void 0;
|
|
61793
|
+
const seconds = Number(maxDuration);
|
|
61794
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
61795
|
+
cliLogger.warn(`Invalid --max-duration "${maxDuration}". Expected a positive number of seconds; running without a budget.`);
|
|
61796
|
+
return;
|
|
61797
|
+
}
|
|
61798
|
+
return seconds * MILLISECONDS_PER_SECOND;
|
|
61799
|
+
};
|
|
61800
|
+
//#endregion
|
|
61645
61801
|
//#region src/cli/utils/resolve-parallel-flag.ts
|
|
61646
61802
|
/**
|
|
61647
61803
|
* Maps the `--no-parallel` flag to an `InspectOptions.concurrency` value.
|
|
@@ -61655,14 +61811,12 @@ const resolveParallelFlag = (parallel) => parallel === false ? 1 : void 0;
|
|
|
61655
61811
|
//#region src/cli/utils/resolve-cli-inspect-options.ts
|
|
61656
61812
|
/**
|
|
61657
61813
|
* Translates CLI flags into the `InspectOptions` contract `inspect()`
|
|
61658
|
-
* accepts. Flag-
|
|
61659
|
-
* `
|
|
61660
|
-
*
|
|
61661
|
-
*
|
|
61662
|
-
*
|
|
61663
|
-
*
|
|
61664
|
-
* so this resolver can apply the one flag-and-config rule that flags own
|
|
61665
|
-
* (`--score false` wins, otherwise inherit `userConfig.noScore`).
|
|
61814
|
+
* accepts. Flag-derived fields only (`scoreOnly`, `noScore`, `silent`,
|
|
61815
|
+
* `isCi`): every `userConfig` fallback — including `noScore` — lives in
|
|
61816
|
+
* `inspect()`'s merge layer, which sees each project's module-merged
|
|
61817
|
+
* config. The plain boolean knobs (`lint`, `deadCode`, `verbose`) pass
|
|
61818
|
+
* through unchanged for the same reason. This resolver reads
|
|
61819
|
+
* `userConfig` only to decide the `--blocking warning` gate.
|
|
61666
61820
|
*/
|
|
61667
61821
|
const resolveCliInspectOptions = (flags, userConfig) => {
|
|
61668
61822
|
const wantsWarningGate = pickBlockingLevel(flags, userConfig) === "warning";
|
|
@@ -61674,10 +61828,11 @@ const resolveCliInspectOptions = (flags, userConfig) => {
|
|
|
61674
61828
|
respectInlineDisables: flags.respectInlineDisables === false ? false : void 0,
|
|
61675
61829
|
warnings: wantsWarningGate ? true : flags.warnings,
|
|
61676
61830
|
scoreOnly: flags.score === true,
|
|
61677
|
-
noScore: flags.score === false || flags.telemetry === false
|
|
61831
|
+
noScore: flags.score === false || flags.telemetry === false ? true : void 0,
|
|
61678
61832
|
isCi: isCiEnvironment(),
|
|
61679
61833
|
silent: Boolean(flags.json),
|
|
61680
61834
|
concurrency: resolveParallelFlag(flags.parallel),
|
|
61835
|
+
maxDurationMs: resolveMaxDurationFlag(flags.maxDuration),
|
|
61681
61836
|
categoryFilters: resolveCliCategories(flags.category)
|
|
61682
61837
|
};
|
|
61683
61838
|
};
|
|
@@ -62012,7 +62167,8 @@ const finalizeScans = (input) => {
|
|
|
62012
62167
|
diff: input.diff,
|
|
62013
62168
|
scans: jsonCompletedScans,
|
|
62014
62169
|
totalElapsedMilliseconds: performance$1.now() - input.startTime,
|
|
62015
|
-
baseline
|
|
62170
|
+
baseline,
|
|
62171
|
+
baselineDegraded
|
|
62016
62172
|
}));
|
|
62017
62173
|
}
|
|
62018
62174
|
if (input.isScoreOnly || baselineDegraded) return;
|
|
@@ -62082,6 +62238,7 @@ const inspectAction = async (directory, flags) => {
|
|
|
62082
62238
|
if (!isQuiet) if (!flags.verbose && canAnimateOnboarding(process.stdout)) await runPromise(playWelcomeScene({ speedMultiplier: !isOnboardingForced() && hasCompletedOnboarding() ? 2 : 1 }));
|
|
62083
62239
|
else runSync(printBrandedHeader);
|
|
62084
62240
|
const scanOptions = resolveCliInspectOptions(flags, userConfig);
|
|
62241
|
+
const scanDeadlineEpochMs = scanOptions.maxDurationMs !== void 0 ? Date.now() + scanOptions.maxDurationMs : void 0;
|
|
62085
62242
|
const categoryFilters = new Set(scanOptions.categoryFilters ?? []);
|
|
62086
62243
|
const skipPrompts = shouldSkipPrompts({
|
|
62087
62244
|
yes: flags.yes,
|
|
@@ -62127,8 +62284,10 @@ const inspectAction = async (directory, flags) => {
|
|
|
62127
62284
|
try {
|
|
62128
62285
|
const scanResult = await inspect(snapshot.tempDirectory, {
|
|
62129
62286
|
...scanOptions,
|
|
62287
|
+
deadlineEpochMs: scanDeadlineEpochMs,
|
|
62130
62288
|
includePaths: snapshot.stagedFiles,
|
|
62131
62289
|
configOverride: userConfig,
|
|
62290
|
+
configSourceDirectory: scanTarget.configSourceDirectory ?? void 0,
|
|
62132
62291
|
changedLineRanges: stagedLineRanges ?? void 0
|
|
62133
62292
|
});
|
|
62134
62293
|
const remappedDiagnostics = scanResult.diagnostics.map((diagnostic) => ({
|
|
@@ -62228,6 +62387,7 @@ const inspectAction = async (directory, flags) => {
|
|
|
62228
62387
|
if (!isQuiet && !isMultiProject) cliLogger.dim(" ");
|
|
62229
62388
|
const scanResult = await inspect(scanDirectory, {
|
|
62230
62389
|
...scanOptions,
|
|
62390
|
+
deadlineEpochMs: scanDeadlineEpochMs,
|
|
62231
62391
|
includePaths,
|
|
62232
62392
|
configOverride: projectConfig,
|
|
62233
62393
|
configSourceDirectory: projectConfigSourceDirectory ?? void 0,
|
|
@@ -62268,7 +62428,7 @@ const inspectAction = async (directory, flags) => {
|
|
|
62268
62428
|
completedScans.push(scanOutcome);
|
|
62269
62429
|
}
|
|
62270
62430
|
if (!isQuiet && isMultiProject && completedScans.length > 0) {
|
|
62271
|
-
const shouldShowShareLink = !scanOptions.noScore
|
|
62431
|
+
const shouldShowShareLink = !isShareOptedOut(completedScans, scanOptions.noScore) && !scanOptions.isCi;
|
|
62272
62432
|
await runPromise(printMultiProjectSummary({
|
|
62273
62433
|
completedScans,
|
|
62274
62434
|
categoryFilters,
|
|
@@ -62602,6 +62762,10 @@ const resolveEffectiveRuleSeverity = (config, entry) => {
|
|
|
62602
62762
|
source: "rule"
|
|
62603
62763
|
};
|
|
62604
62764
|
}
|
|
62765
|
+
if (!entry.defaultEnabled) return {
|
|
62766
|
+
value: "off",
|
|
62767
|
+
source: "default"
|
|
62768
|
+
};
|
|
62605
62769
|
const categoryOverride = config?.categories?.[entry.category];
|
|
62606
62770
|
if (categoryOverride !== void 0) return {
|
|
62607
62771
|
value: categoryOverride,
|
|
@@ -63089,7 +63253,7 @@ const printDebugTrace = () => {
|
|
|
63089
63253
|
//#endregion
|
|
63090
63254
|
//#region src/cli/utils/removed-cli-flags.ts
|
|
63091
63255
|
const REMOVED_FLAGS = new Map([
|
|
63092
|
-
["--full", "use `--
|
|
63256
|
+
["--full", "use `--scope full` to force a full scan"],
|
|
63093
63257
|
["--explain", "use the `why <file>:<line>` command"],
|
|
63094
63258
|
["--why", "use the `why <file>:<line>` command"],
|
|
63095
63259
|
["--pr-comment", "the GitHub Action posts the PR comment for you; remove this flag"]
|
|
@@ -63139,6 +63303,7 @@ const ROOT_FLAG_SPEC = {
|
|
|
63139
63303
|
"--changed-files-from",
|
|
63140
63304
|
"--blocking",
|
|
63141
63305
|
"--json-out",
|
|
63306
|
+
"--max-duration",
|
|
63142
63307
|
"--output-dir",
|
|
63143
63308
|
"--fail-on",
|
|
63144
63309
|
"--project",
|
|
@@ -63375,7 +63540,7 @@ ${highlighter.dim("Learn more:")}
|
|
|
63375
63540
|
${highlighter.info(CI_URL)}
|
|
63376
63541
|
`;
|
|
63377
63542
|
const collectCategoryOption = (value, previousValues) => [...previousValues ?? [], value];
|
|
63378
|
-
const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--debug", "force a Sentry trace and print its id at the end (paste it into a bug report)").option("--output-dir <dir>", "directory for the full diagnostics dump (default: a temp folder)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("--json-out <path>", "with --json, write the report to a file instead of stdout").option("-y, --yes", "skip prompts, scan all workspace projects").option("--no-parallel", "lint serially with one worker (default: parallel across CPU cores; set the worker count with REACT_DOCTOR_PARALLEL)").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple); overrides the `projects` config field").option("--scope <value>", "how much to scan/report: full (default), files, changed (only new issues vs base), or lines (only changed lines)").option("--base <ref>", "base git ref for files/changed/lines scope (auto-detected when omitted)").addOption(new Option("--diff [base]", "[deprecated] alias for --scope changed (pass `false` to force a full scan)").hideHelp()).addOption(new Option("--changed-files-from <file>", "scan source files listed in a newline-delimited changed-files file").hideHelp()).option("--no-score", "skip the score API, the share URL, and crash reporting").addOption(new Option("--category <category>", "only show diagnostics in a category (repeatable; e.g. Security)").argParser(collectCategoryOption)).option("--no-telemetry", "alias for --no-score (skip the score API, share URL, and crash reporting)").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").addOption(new Option("--fail-on <level>", "[deprecated] alias for --blocking <level>").hideHelp()).option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").option("--warnings", "show warning-severity diagnostics (default)").option("--no-warnings", "hide warning-severity diagnostics (errors only)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderRootHelpEpilog);
|
|
63543
|
+
const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--debug", "force a Sentry trace and print its id at the end (paste it into a bug report)").option("--output-dir <dir>", "directory for the full diagnostics dump (default: a temp folder)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("--json-out <path>", "with --json, write the report to a file instead of stdout").option("-y, --yes", "skip prompts, scan all workspace projects").option("--no-parallel", "lint serially with one worker (default: parallel across CPU cores; set the worker count with REACT_DOCTOR_PARALLEL)").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple); overrides the `projects` config field").option("--scope <value>", "how much to scan/report: full (default), files, changed (only new issues vs base), or lines (only changed lines)").option("--base <ref>", "base git ref for files/changed/lines scope (auto-detected when omitted)").addOption(new Option("--diff [base]", "[deprecated] alias for --scope changed (pass `false` to force a full scan)").hideHelp()).addOption(new Option("--changed-files-from <file>", "scan source files listed in a newline-delimited changed-files file").hideHelp()).option("--no-score", "skip the score API, the share URL, and crash reporting").addOption(new Option("--category <category>", "only show diagnostics in a category (repeatable; e.g. Security)").argParser(collectCategoryOption)).option("--no-telemetry", "alias for --no-score (skip the score API, share URL, and crash reporting)").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--max-duration <seconds>", "scan time budget for the whole run, shared across workspace projects: past it, remaining lint batches and dead-code are skipped and partial results are reported (skipped files are listed in the JSON report)").option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").addOption(new Option("--fail-on <level>", "[deprecated] alias for --blocking <level>").hideHelp()).option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").option("--warnings", "show warning-severity diagnostics (default)").option("--no-warnings", "hide warning-severity diagnostics (errors only)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderRootHelpEpilog);
|
|
63379
63544
|
program.action(inspectAction);
|
|
63380
63545
|
program.command("why <location>").description("Explain why a rule fired (or why a suppression didn't apply) at a file:line").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple)").option("-c, --cwd <cwd>", "working directory", process.cwd()).option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").action((location, options) => whyAction(location, options));
|
|
63381
63546
|
program.command("install").alias("setup").description("Install the react-doctor skill into your coding agents and optional git hook").option("-y, --yes", "skip prompts, install for all detected agents").option("--dry-run", "show what would be installed without writing files").option("--agent-hooks", "install native non-blocking agent hooks for Claude Code and Cursor").option("-c, --cwd <cwd>", "working directory", process.cwd()).option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderInstallHelpEpilog).action(installAction);
|
|
@@ -63425,4 +63590,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
|
|
|
63425
63590
|
export {};
|
|
63426
63591
|
|
|
63427
63592
|
//# sourceMappingURL=cli.js.map
|
|
63428
|
-
//# debugId=
|
|
63593
|
+
//# debugId=56ca0cbd-9f70-5a50-8496-43abdf078431
|