react-doctor 0.7.0 → 0.7.1-dev.1880b15
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 +185 -42
- package/dist/index.d.ts +51 -1
- package/dist/index.js +133 -22
- package/dist/lsp.js +129 -21
- package/package.json +6 -6
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="47338856-c658-5c78-a5e2-e871c2dafaa7")}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";
|
|
@@ -22405,6 +22405,7 @@ var Diagnostic = class extends Class("Diagnostic")({
|
|
|
22405
22405
|
endLine: optional(Number$1),
|
|
22406
22406
|
endColumn: optional(Number$1),
|
|
22407
22407
|
category: String$1,
|
|
22408
|
+
matchByOccurrence: optional(Boolean$1),
|
|
22408
22409
|
fileContext: optional(Literals(["test", "story"])),
|
|
22409
22410
|
suppressionHint: optional(String$1),
|
|
22410
22411
|
relatedLocations: optional(ArraySchema(DiagnosticRelatedLocation)),
|
|
@@ -22475,6 +22476,16 @@ var JsonReportV1 = class extends Class("JsonReportV1")({
|
|
|
22475
22476
|
* successful `baseline` run (`schemaVersion: 2`) and on non-compare scopes.
|
|
22476
22477
|
*/
|
|
22477
22478
|
baselineDegraded: optional(Boolean$1),
|
|
22479
|
+
/**
|
|
22480
|
+
* Whether any scanned project resolved a React-compatible runtime (React
|
|
22481
|
+
* or Preact). `false` means every React-runtime rule family was gated off,
|
|
22482
|
+
* so an empty `diagnostics` array is vacuous — NOT the same as a clean
|
|
22483
|
+
* React scan. Consumers gating on the report (CI, verifiers, hooks) should
|
|
22484
|
+
* treat `reactDetected === false` as "wrong scan target", not "all clear".
|
|
22485
|
+
* Absent when nothing was scanned (`projects` is empty), on error reports,
|
|
22486
|
+
* and on reports from older CLI versions.
|
|
22487
|
+
*/
|
|
22488
|
+
reactDetected: optional(Boolean$1),
|
|
22478
22489
|
diff: NullOr(JsonReportDiffInfo),
|
|
22479
22490
|
projects: ArraySchema(JsonReportProjectEntry),
|
|
22480
22491
|
diagnostics: ArraySchema(Diagnostic),
|
|
@@ -22500,6 +22511,8 @@ var JsonReportV2 = class extends Class("JsonReportV2")({
|
|
|
22500
22511
|
ok: Boolean$1,
|
|
22501
22512
|
directory: String$1,
|
|
22502
22513
|
mode: JsonReportMode,
|
|
22514
|
+
/** See `JsonReportV1.reactDetected`. */
|
|
22515
|
+
reactDetected: optional(Boolean$1),
|
|
22503
22516
|
diff: NullOr(JsonReportDiffInfo),
|
|
22504
22517
|
baseline: JsonReportBaseline,
|
|
22505
22518
|
projects: ArraySchema(JsonReportProjectEntry),
|
|
@@ -36108,6 +36121,7 @@ const DIAGNOSTIC_CATEGORY_BUCKETS = [
|
|
|
36108
36121
|
"Accessibility",
|
|
36109
36122
|
"Maintainability"
|
|
36110
36123
|
];
|
|
36124
|
+
const OCCURRENCE_MATCHED_CATEGORIES = new Set(["Accessibility"]);
|
|
36111
36125
|
const APP_ONLY_RULE_KEYS = new Set([
|
|
36112
36126
|
"react-hooks-js/static-components",
|
|
36113
36127
|
"react-doctor/no-render-prop-children",
|
|
@@ -36130,7 +36144,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
|
|
|
36130
36144
|
const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
|
|
36131
36145
|
const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
|
|
36132
36146
|
const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
|
|
36133
|
-
const CORE_PACKAGE_VERSION = "0.7.
|
|
36147
|
+
const CORE_PACKAGE_VERSION = "0.7.1";
|
|
36134
36148
|
const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
|
|
36135
36149
|
const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
|
|
36136
36150
|
const SUPPLY_CHAIN_PLUGIN = "socket";
|
|
@@ -36248,6 +36262,23 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
|
|
|
36248
36262
|
const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
|
|
36249
36263
|
const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
|
|
36250
36264
|
const countSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).length;
|
|
36265
|
+
/**
|
|
36266
|
+
* The chain of ancestor directories of `startDirectory`, nearest first, up to
|
|
36267
|
+
* and including the filesystem root. With `includeStart`, the chain begins at
|
|
36268
|
+
* `startDirectory` itself; otherwise at its parent. The one place the "walk up
|
|
36269
|
+
* the tree" traversal is expressed, so each caller adds only its own match /
|
|
36270
|
+
* stop policy instead of re-deriving the `path.dirname` loop.
|
|
36271
|
+
*/
|
|
36272
|
+
const ancestorDirectories = (startDirectory, { includeStart }) => {
|
|
36273
|
+
const directories = [];
|
|
36274
|
+
let currentDirectory = includeStart ? startDirectory : Path.dirname(startDirectory);
|
|
36275
|
+
while (true) {
|
|
36276
|
+
directories.push(currentDirectory);
|
|
36277
|
+
const parentDirectory = Path.dirname(currentDirectory);
|
|
36278
|
+
if (parentDirectory === currentDirectory) return directories;
|
|
36279
|
+
currentDirectory = parentDirectory;
|
|
36280
|
+
}
|
|
36281
|
+
};
|
|
36251
36282
|
const cachedPackageJsons = /* @__PURE__ */ new Map();
|
|
36252
36283
|
const readPackageJsonUncached = (packageJsonPath) => {
|
|
36253
36284
|
try {
|
|
@@ -36278,11 +36309,7 @@ const isMonorepoRoot = (directory) => {
|
|
|
36278
36309
|
return Array.isArray(packageJson.workspaces) || Boolean(packageJson.workspaces?.packages);
|
|
36279
36310
|
};
|
|
36280
36311
|
const findMonorepoRoot = (startDirectory) => {
|
|
36281
|
-
|
|
36282
|
-
while (currentDirectory !== Path.dirname(currentDirectory)) {
|
|
36283
|
-
if (isMonorepoRoot(currentDirectory)) return currentDirectory;
|
|
36284
|
-
currentDirectory = Path.dirname(currentDirectory);
|
|
36285
|
-
}
|
|
36312
|
+
for (const directory of ancestorDirectories(startDirectory, { includeStart: false })) if (isMonorepoRoot(directory)) return directory;
|
|
36286
36313
|
return null;
|
|
36287
36314
|
};
|
|
36288
36315
|
/**
|
|
@@ -36928,6 +36955,74 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
|
|
|
36928
36955
|
framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
|
|
36929
36956
|
};
|
|
36930
36957
|
};
|
|
36958
|
+
/**
|
|
36959
|
+
* Walk up from `startDirectory` to the nearest ancestor that owns a
|
|
36960
|
+
* `package.json`, stopping at (and including) the enclosing project boundary
|
|
36961
|
+
* — the working tree's git root or a monorepo root. Lets a scan of a package
|
|
36962
|
+
* subfolder that has no `package.json` of its own adopt the nearest enclosing
|
|
36963
|
+
* package as its project root, so it inherits that package's dependency +
|
|
36964
|
+
* framework detection instead of synthesizing an empty (React-blind) project.
|
|
36965
|
+
*
|
|
36966
|
+
* Generalizes the older monorepo-root-only lookup: the nearest ancestor is the
|
|
36967
|
+
* most specific owning package (a leaf workspace, not just the repo root), so
|
|
36968
|
+
* a subfolder of one workspace no longer borrows a sibling's capabilities.
|
|
36969
|
+
*
|
|
36970
|
+
* Returns `null` when no ancestor `package.json` exists inside the project
|
|
36971
|
+
* boundary (a loose tree of files, or a subfolder of a package-less repo).
|
|
36972
|
+
*/
|
|
36973
|
+
const findNearestAncestorPackageJson = (startDirectory) => {
|
|
36974
|
+
if (isProjectBoundary(startDirectory)) return null;
|
|
36975
|
+
for (const directory of ancestorDirectories(startDirectory, { includeStart: false })) {
|
|
36976
|
+
if (isFile(Path.join(directory, "package.json"))) return directory;
|
|
36977
|
+
if (isProjectBoundary(directory)) return null;
|
|
36978
|
+
}
|
|
36979
|
+
return null;
|
|
36980
|
+
};
|
|
36981
|
+
/**
|
|
36982
|
+
* Last-resort React detection: locate React the way Node's `node_modules`
|
|
36983
|
+
* resolution would from `directory` — check each `node_modules/react` up the
|
|
36984
|
+
* tree — but bounded to the enclosing repo so a globally installed React can't
|
|
36985
|
+
* masquerade as the project's version. Stops at (and including) the nearest
|
|
36986
|
+
* project boundary — the git root or a monorepo root — and returns the first
|
|
36987
|
+
* installed `version`.
|
|
36988
|
+
*
|
|
36989
|
+
* Makes "React is installed and importable" ⇒ "React is detected" an invariant
|
|
36990
|
+
* for packages whose only React declaration is a version-less spec
|
|
36991
|
+
* (`workspace:*`, `*`, a dist-tag) or where React lives solely in a hoisted
|
|
36992
|
+
* `node_modules` the declaration walks never reach — the profile of a component
|
|
36993
|
+
* package inside a monorepo. Matching the `node_modules/react` entry rather than
|
|
36994
|
+
* its realpath means a project whose `node_modules` is symlinked elsewhere (a
|
|
36995
|
+
* Docker volume, a shared store) still counts, while a global install outside
|
|
36996
|
+
* the repo does not.
|
|
36997
|
+
*
|
|
36998
|
+
* Returns `null` when React isn't installed within the repo or its package.json
|
|
36999
|
+
* carries no version string. A tree with no boundary marker at all bounds the
|
|
37000
|
+
* search to `directory`, so a dependency hoisted above the scanned package there
|
|
37001
|
+
* is conservatively not adopted.
|
|
37002
|
+
*/
|
|
37003
|
+
/**
|
|
37004
|
+
* The nearest enclosing project boundary (git root or monorepo root), or
|
|
37005
|
+
* `directory` itself when the scan target sits outside any repo. Bounds the
|
|
37006
|
+
* node_modules walk so a React hoisted anywhere inside the repo counts, but a
|
|
37007
|
+
* React in an ancestor above the repo — a home-directory or global
|
|
37008
|
+
* `node_modules` — can't leak in.
|
|
37009
|
+
*/
|
|
37010
|
+
const findContainmentRoot = (directory) => {
|
|
37011
|
+
for (const ancestorDirectory of ancestorDirectories(directory, { includeStart: true })) if (isProjectBoundary(ancestorDirectory)) return ancestorDirectory;
|
|
37012
|
+
return directory;
|
|
37013
|
+
};
|
|
37014
|
+
const resolveInstalledReactVersion = (directory) => {
|
|
37015
|
+
const containmentRoot = findContainmentRoot(directory);
|
|
37016
|
+
for (const ancestorDirectory of ancestorDirectories(directory, { includeStart: true })) {
|
|
37017
|
+
const reactPackageJsonPath = Path.join(ancestorDirectory, "node_modules", "react", "package.json");
|
|
37018
|
+
if (isFile(reactPackageJsonPath)) {
|
|
37019
|
+
const installedVersion = readPackageJson$1(reactPackageJsonPath).version;
|
|
37020
|
+
return typeof installedVersion === "string" ? installedVersion : null;
|
|
37021
|
+
}
|
|
37022
|
+
if (ancestorDirectory === containmentRoot) return null;
|
|
37023
|
+
}
|
|
37024
|
+
return null;
|
|
37025
|
+
};
|
|
36931
37026
|
const findInWorkspacePackageJsons = (rootDirectory, rootPackageJson, select) => {
|
|
36932
37027
|
const rootValue = select(rootPackageJson);
|
|
36933
37028
|
if (rootValue !== null) return rootValue;
|
|
@@ -37264,19 +37359,20 @@ const discoverReactSubprojects = (rootDirectory) => {
|
|
|
37264
37359
|
const cachedProjectInfos = /* @__PURE__ */ new Map();
|
|
37265
37360
|
/**
|
|
37266
37361
|
* Build a `ProjectInfo` for a directory that has no `package.json` of
|
|
37267
|
-
* its own — a
|
|
37268
|
-
* of TypeScript/JavaScript files. Dependency + framework
|
|
37269
|
-
* inherited from the enclosing
|
|
37270
|
-
*
|
|
37271
|
-
*
|
|
37272
|
-
*
|
|
37273
|
-
*
|
|
37362
|
+
* its own — a package subfolder like `repo/packages` or `app/src/features`,
|
|
37363
|
+
* or any loose tree of TypeScript/JavaScript files. Dependency + framework
|
|
37364
|
+
* detection is inherited from the nearest enclosing package (a leaf
|
|
37365
|
+
* workspace, a plain app root, or a monorepo root — whichever is closest,
|
|
37366
|
+
* bounded by the git root), so scanning a subdirectory of a React project
|
|
37367
|
+
* still gets the React capabilities; a standalone non-React directory simply
|
|
37368
|
+
* scans with the framework-agnostic rules. Throws only when the directory has
|
|
37369
|
+
* nothing to scan (no enclosing project and no source files of its own).
|
|
37274
37370
|
*/
|
|
37275
37371
|
const discoverProjectWithoutPackageJson = (directory) => {
|
|
37276
37372
|
const sourceFileCount = countSourceFiles(directory);
|
|
37277
37373
|
const hasOwnTsConfig = NFS.existsSync(Path.join(directory, "tsconfig.json"));
|
|
37278
|
-
const
|
|
37279
|
-
const enclosingProject =
|
|
37374
|
+
const enclosingProjectRoot = findNearestAncestorPackageJson(directory);
|
|
37375
|
+
const enclosingProject = enclosingProjectRoot !== null ? discoverProject(enclosingProjectRoot) : null;
|
|
37280
37376
|
if (enclosingProject !== null) return {
|
|
37281
37377
|
...enclosingProject,
|
|
37282
37378
|
rootDirectory: directory,
|
|
@@ -37380,6 +37476,7 @@ const discoverProject = (directory) => {
|
|
|
37380
37476
|
if (!reactVersion && reactDeclaration.version && !isCatalogReference(reactDeclaration.version)) reactVersion = reactDeclaration.version;
|
|
37381
37477
|
if (!tailwindVersion && tailwindDeclaration.version && !isCatalogReference(tailwindDeclaration.version)) tailwindVersion = tailwindDeclaration.version;
|
|
37382
37478
|
if (!zodVersion && zodDeclaration.version && !isCatalogReference(zodDeclaration.version)) zodVersion = zodDeclaration.version;
|
|
37479
|
+
if (!reactVersion || parseReactMajor(reactVersion) === null) reactVersion = resolveInstalledReactVersion(directory) ?? reactVersion;
|
|
37383
37480
|
const projectName = packageJson.name ?? Path.basename(directory);
|
|
37384
37481
|
const hasTypeScript = NFS.existsSync(Path.join(directory, "tsconfig.json"));
|
|
37385
37482
|
const sourceFileCount = countSourceFiles(directory);
|
|
@@ -40237,10 +40334,17 @@ function* collectSecurityScanFiles(rootDirectory) {
|
|
|
40237
40334
|
}
|
|
40238
40335
|
}
|
|
40239
40336
|
}
|
|
40337
|
+
/**
|
|
40338
|
+
* Whether project discovery resolved a React-compatible runtime (React or
|
|
40339
|
+
* Preact, which ships the same hooks + JSX model). This is the same
|
|
40340
|
+
* predicate that grants the `react` capability, so `false` means every
|
|
40341
|
+
* React-runtime rule family was gated off for the project.
|
|
40342
|
+
*/
|
|
40343
|
+
const hasReactRuntime = (project) => project.reactVersion !== null || project.preactVersion !== null;
|
|
40240
40344
|
const buildCapabilities = (project) => {
|
|
40241
40345
|
const capabilities = /* @__PURE__ */ new Set();
|
|
40242
40346
|
capabilities.add(project.framework);
|
|
40243
|
-
if (project
|
|
40347
|
+
if (hasReactRuntime(project)) capabilities.add("react");
|
|
40244
40348
|
if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
|
|
40245
40349
|
if (project.expoVersion !== null) capabilities.add("expo");
|
|
40246
40350
|
if (project.nextjsMajorVersion !== null && project.nextjsMajorVersion >= 15) capabilities.add("nextjs:15");
|
|
@@ -41786,7 +41890,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
41786
41890
|
})),
|
|
41787
41891
|
showStagedContent: (directory, relativePath, options) => runCommand({
|
|
41788
41892
|
command: "git",
|
|
41789
|
-
args: ["show",
|
|
41893
|
+
args: ["show", `:./${relativePath}`],
|
|
41790
41894
|
directory,
|
|
41791
41895
|
maxStdoutBytes: options?.maxBufferBytes
|
|
41792
41896
|
}).pipe(map$3((result) => result.status === 0 ? result.stdout : null)),
|
|
@@ -43092,6 +43196,7 @@ const parseRuleCode = (code) => {
|
|
|
43092
43196
|
};
|
|
43093
43197
|
};
|
|
43094
43198
|
const resolveDiagnosticCategory = (plugin, rule) => getRuleCategory(rule) ?? lookupOwnString(PLUGIN_CATEGORY_MAP, plugin) ?? "Bugs";
|
|
43199
|
+
const resolveMatchByOccurrence = (rule, category) => OCCURRENCE_MATCHED_CATEGORIES.has(category) || Boolean(reactDoctorPlugin.rules[rule]?.matchByOccurrence);
|
|
43095
43200
|
/**
|
|
43096
43201
|
* Maps oxlint's non-primary labels (`labels[1..]`) into related source
|
|
43097
43202
|
* locations. Editors surface these as a diagnostic's
|
|
@@ -43155,6 +43260,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
|
|
|
43155
43260
|
const normalizedFilePath = diagnostic.filename.replaceAll("\\", "/");
|
|
43156
43261
|
const primarySpan = primaryLabel?.span;
|
|
43157
43262
|
const relatedLocations = buildRelatedLocations(diagnostic.labels, normalizedFilePath);
|
|
43263
|
+
const category = resolveDiagnosticCategory(plugin, rule);
|
|
43158
43264
|
return {
|
|
43159
43265
|
filePath: normalizedFilePath,
|
|
43160
43266
|
plugin,
|
|
@@ -43170,7 +43276,8 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
|
|
|
43170
43276
|
offset: primarySpan.offset,
|
|
43171
43277
|
length: primarySpan.length
|
|
43172
43278
|
} : {},
|
|
43173
|
-
category
|
|
43279
|
+
category,
|
|
43280
|
+
...resolveMatchByOccurrence(rule, category) ? { matchByOccurrence: true } : {},
|
|
43174
43281
|
...relatedLocations.length > 0 ? { relatedLocations } : {}
|
|
43175
43282
|
};
|
|
43176
43283
|
});
|
|
@@ -45355,6 +45462,7 @@ const buildJsonReport = (input) => {
|
|
|
45355
45462
|
const worstScoredProject = findWorstScoredProject(projects);
|
|
45356
45463
|
const summary = summarizeDiagnostics(flattenedDiagnostics, worstScoredProject?.score?.score ?? null, worstScoredProject?.score?.label ?? null);
|
|
45357
45464
|
const shared = {
|
|
45465
|
+
...input.scans.length > 0 ? { reactDetected: input.scans.some((scan) => hasReactRuntime(scan.result.project)) } : {},
|
|
45358
45466
|
version: input.version,
|
|
45359
45467
|
ok: true,
|
|
45360
45468
|
directory: input.directory,
|
|
@@ -45405,7 +45513,7 @@ const buildSkippedChecks = (input) => {
|
|
|
45405
45513
|
};
|
|
45406
45514
|
const fingerprintDiagnostic = (diagnostic, lineText) => {
|
|
45407
45515
|
const ruleKey = `${diagnostic.plugin}/${diagnostic.rule}`;
|
|
45408
|
-
const snippet = lineText === null ? "" : createHash("sha1").update(lineText.trim()).digest("hex");
|
|
45516
|
+
const snippet = lineText === null || diagnostic.matchByOccurrence ? "" : createHash("sha1").update(lineText.trim()).digest("hex");
|
|
45409
45517
|
return `${diagnostic.filePath}\u0000${ruleKey}\u0000${snippet}`;
|
|
45410
45518
|
};
|
|
45411
45519
|
/**
|
|
@@ -45416,6 +45524,17 @@ const fingerprintDiagnostic = (diagnostic, lineText) => {
|
|
|
45416
45524
|
* look new, while a genuinely new occurrence (new line text, or one more of
|
|
45417
45525
|
* the same) surfaces. Identical repeated findings are matched by count.
|
|
45418
45526
|
*
|
|
45527
|
+
* Diagnostics carrying `matchByOccurrence` (resolved at diagnostic creation:
|
|
45528
|
+
* every Accessibility-category finding, plus rules opting in via their
|
|
45529
|
+
* `matchByOccurrence` metadata flag) drop the line-text snippet and match by
|
|
45530
|
+
* `(filePath, plugin/rule)` occurrence count alone. Their identity is the
|
|
45531
|
+
* flagged element, not the line's text, so editing the line (reindentation,
|
|
45532
|
+
* prettier reflow, collapsing a multi-line JSX element) doesn't reclassify a
|
|
45533
|
+
* pre-existing finding as new — while one MORE occurrence of the same rule in
|
|
45534
|
+
* the file still surfaces. Expression-level rules keep the line-text snippet:
|
|
45535
|
+
* there the flagged expression IS the finding, so changed text means new +
|
|
45536
|
+
* fixed.
|
|
45537
|
+
*
|
|
45419
45538
|
* v1 limitation: the fingerprint keys on the head-relative `filePath`, and base
|
|
45420
45539
|
* content is read at that same path. A file renamed by the change therefore has
|
|
45421
45540
|
* no base match, so its pre-existing findings are reported as new. This
|
|
@@ -45818,6 +45937,7 @@ const METRIC = {
|
|
|
45818
45937
|
scanScore: "scan.score",
|
|
45819
45938
|
scanClean: "scan.clean",
|
|
45820
45939
|
scanCheckSkipped: "scan.check_skipped",
|
|
45940
|
+
scanNoReactDetected: "scan.no_react_detected",
|
|
45821
45941
|
baselineDegraded: "baseline.degraded",
|
|
45822
45942
|
ruleFired: "rule.fired",
|
|
45823
45943
|
ruleDisabled: "rule.disabled",
|
|
@@ -45887,7 +46007,7 @@ const makeNoopConsole = () => ({
|
|
|
45887
46007
|
});
|
|
45888
46008
|
//#endregion
|
|
45889
46009
|
//#region src/cli/utils/version.ts
|
|
45890
|
-
const VERSION = "0.7.
|
|
46010
|
+
const VERSION = "0.7.1-dev.1880b15";
|
|
45891
46011
|
//#endregion
|
|
45892
46012
|
//#region src/cli/utils/json-mode.ts
|
|
45893
46013
|
let context = null;
|
|
@@ -46256,13 +46376,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
|
|
|
46256
46376
|
* uploads source-map artifacts under, so stack frames symbolicate. Honors the
|
|
46257
46377
|
* standard `SENTRY_RELEASE` override.
|
|
46258
46378
|
*/
|
|
46259
|
-
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.
|
|
46379
|
+
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.1-dev.1880b15`;
|
|
46260
46380
|
/**
|
|
46261
46381
|
* Deployment environment shown in Sentry's environment filter. Defaults to
|
|
46262
46382
|
* `production` for tagged releases and `development` for dev/unbuilt versions,
|
|
46263
46383
|
* overridable via the standard `SENTRY_ENVIRONMENT` env var.
|
|
46264
46384
|
*/
|
|
46265
|
-
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.
|
|
46385
|
+
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.1-dev.1880b15") ? "development" : "production");
|
|
46266
46386
|
/**
|
|
46267
46387
|
* Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
|
|
46268
46388
|
* (set to `0` to disable tracing) and falls back to
|
|
@@ -58106,7 +58226,7 @@ const buildRuntimeLayers = (input) => {
|
|
|
58106
58226
|
const linterLayer = input.shouldSkipLint ? Linter.layerOf([]) : Linter.layerOxlint;
|
|
58107
58227
|
const deadCodeLayer = input.shouldRunDeadCode ? DeadCode.layerNode : DeadCode.layerOf([]);
|
|
58108
58228
|
const scoreLayer = input.shouldComputeScore ? Score.layerHttp : Score.layerOf(null);
|
|
58109
|
-
const supplyChainLayer = input.
|
|
58229
|
+
const supplyChainLayer = input.shouldRunSupplyChain ? SupplyChain.layerNode : SupplyChain.layerOf([]);
|
|
58110
58230
|
const projectLayer = input.projectInfoOverride === void 0 ? Project.layerNode : Project.layerOf(input.projectInfoOverride);
|
|
58111
58231
|
const progressLayer = input.shouldShowProgressSpinners ? Progress.layerOra(buildSpinnerProgressHandle) : Progress.layerNoop;
|
|
58112
58232
|
const baseLayers = mergeAll$1(projectLayer, input.hasConfigOverride ? Config.layerOf({
|
|
@@ -58462,21 +58582,22 @@ const resolveDotenvFingerprint = (projectDirectory) => {
|
|
|
58462
58582
|
return [];
|
|
58463
58583
|
}
|
|
58464
58584
|
};
|
|
58465
|
-
const
|
|
58585
|
+
const isGitIdentityTrustworthy = (projectDirectory) => {
|
|
58466
58586
|
const output = runGit(projectDirectory, ["ls-files", "-v"]);
|
|
58467
|
-
if (output === null) return
|
|
58468
|
-
|
|
58587
|
+
if (output === null) return false;
|
|
58588
|
+
const entryLines = output.split("\n").filter((line) => line.length > 0);
|
|
58589
|
+
return entryLines.length > 0 && entryLines.every((line) => line[0] === "H");
|
|
58469
58590
|
};
|
|
58470
58591
|
const resolveCacheFilePath = (projectDirectory) => path.join(resolveReactDoctorCacheDir(projectDirectory), SCAN_RESULT_CACHE_FILENAME);
|
|
58471
58592
|
const readPersistedCache = (cacheFilePath) => {
|
|
58472
58593
|
try {
|
|
58473
58594
|
const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
|
|
58474
|
-
if (!isRecord(parsed) || parsed.version !==
|
|
58475
|
-
version:
|
|
58595
|
+
if (!isRecord(parsed) || parsed.version !== 4) return {
|
|
58596
|
+
version: 4,
|
|
58476
58597
|
entries: []
|
|
58477
58598
|
};
|
|
58478
58599
|
if (!Array.isArray(parsed.entries)) return {
|
|
58479
|
-
version:
|
|
58600
|
+
version: 4,
|
|
58480
58601
|
entries: []
|
|
58481
58602
|
};
|
|
58482
58603
|
const entries = [];
|
|
@@ -58486,12 +58607,12 @@ const readPersistedCache = (cacheFilePath) => {
|
|
|
58486
58607
|
entries.push(entry);
|
|
58487
58608
|
}
|
|
58488
58609
|
return {
|
|
58489
|
-
version:
|
|
58610
|
+
version: 4,
|
|
58490
58611
|
entries
|
|
58491
58612
|
};
|
|
58492
58613
|
} catch {
|
|
58493
58614
|
return {
|
|
58494
|
-
version:
|
|
58615
|
+
version: 4,
|
|
58495
58616
|
entries: []
|
|
58496
58617
|
};
|
|
58497
58618
|
}
|
|
@@ -58523,6 +58644,7 @@ const resolveProjectIdentity = (projectDirectory) => {
|
|
|
58523
58644
|
return path.resolve(projectDirectory);
|
|
58524
58645
|
}
|
|
58525
58646
|
};
|
|
58647
|
+
const resolveManifestContentHash = (projectDirectory) => hashFileContents(getPackageJsonPath(projectDirectory));
|
|
58526
58648
|
const fileFingerprint = (filePath) => {
|
|
58527
58649
|
try {
|
|
58528
58650
|
const stat = fs.statSync(filePath);
|
|
@@ -58545,14 +58667,14 @@ const resolveToolchainFingerprint = (nodeBinaryPath) => {
|
|
|
58545
58667
|
};
|
|
58546
58668
|
const buildScanResultCacheKey = (input) => {
|
|
58547
58669
|
if (isCacheGloballyDisabled()) return null;
|
|
58548
|
-
if (
|
|
58670
|
+
if (!isGitIdentityTrustworthy(input.projectDirectory)) return null;
|
|
58549
58671
|
const worktreeFingerprint = buildWorktreeFingerprint(input.projectDirectory);
|
|
58550
58672
|
if (worktreeFingerprint === null) return null;
|
|
58551
58673
|
const headSha = readHeadSha(input.projectDirectory);
|
|
58552
58674
|
if (headSha === null) return null;
|
|
58553
58675
|
if (stringifyStableJson(input.userConfig) === null) return null;
|
|
58554
58676
|
const cacheKeyJson = stringifyStableJson({
|
|
58555
|
-
schemaVersion:
|
|
58677
|
+
schemaVersion: 4,
|
|
58556
58678
|
projectIdentity: resolveProjectIdentity(input.projectDirectory),
|
|
58557
58679
|
headSha,
|
|
58558
58680
|
worktreeFingerprint,
|
|
@@ -58567,6 +58689,7 @@ const buildScanResultCacheKey = (input) => {
|
|
|
58567
58689
|
engineOptions: {
|
|
58568
58690
|
lint: input.options.lint,
|
|
58569
58691
|
deadCode: input.options.deadCode,
|
|
58692
|
+
supplyChain: input.options.supplyChain,
|
|
58570
58693
|
includePaths: [...input.options.includePaths].sort(),
|
|
58571
58694
|
customRulesOnly: input.options.customRulesOnly,
|
|
58572
58695
|
respectInlineDisables: input.options.respectInlineDisables,
|
|
@@ -58592,17 +58715,24 @@ const createScanResultCache = (projectDirectory) => {
|
|
|
58592
58715
|
for (const entry of persistedCache.entries) entries.set(entry.key, entry);
|
|
58593
58716
|
const persist = () => {
|
|
58594
58717
|
writePersistedCache(cacheFilePath, {
|
|
58595
|
-
version:
|
|
58718
|
+
version: 4,
|
|
58596
58719
|
entries: [...entries.values()].sort((firstEntry, secondEntry) => secondEntry.createdAtMs - firstEntry.createdAtMs).slice(0, 20)
|
|
58597
58720
|
});
|
|
58598
58721
|
};
|
|
58599
58722
|
return {
|
|
58600
|
-
lookup: (key) =>
|
|
58723
|
+
lookup: (key) => {
|
|
58724
|
+
const payload = entries.get(key)?.payload;
|
|
58725
|
+
if (payload === void 0) return null;
|
|
58726
|
+
return resolveProjectIdentity(payload.directory) === resolveProjectIdentity(projectDirectory) && (payload.manifestContentHash ?? null) === resolveManifestContentHash(projectDirectory) ? payload : null;
|
|
58727
|
+
},
|
|
58601
58728
|
store: (key, payload) => {
|
|
58602
58729
|
entries.set(key, {
|
|
58603
58730
|
key,
|
|
58604
58731
|
createdAtMs: Date.now(),
|
|
58605
|
-
payload
|
|
58732
|
+
payload: {
|
|
58733
|
+
...payload,
|
|
58734
|
+
manifestContentHash: resolveManifestContentHash(projectDirectory)
|
|
58735
|
+
}
|
|
58606
58736
|
});
|
|
58607
58737
|
persist();
|
|
58608
58738
|
}
|
|
@@ -58867,6 +58997,7 @@ const buildScanAttributes = (input) => {
|
|
|
58867
58997
|
maxDurationMs: input.maxDurationMs,
|
|
58868
58998
|
lint: input.lint,
|
|
58869
58999
|
deadCode: input.deadCode,
|
|
59000
|
+
supplyChain: input.supplyChain,
|
|
58870
59001
|
scoreOnly: input.scoreOnly,
|
|
58871
59002
|
noScore: input.noScore,
|
|
58872
59003
|
respectInlineDisables: input.respectInlineDisables,
|
|
@@ -60141,6 +60272,7 @@ const buildIgnoredTags = (userConfig) => {
|
|
|
60141
60272
|
const mergeInspectOptions = (inputOptions, userConfig) => ({
|
|
60142
60273
|
lint: inputOptions.lint ?? userConfig?.lint ?? true,
|
|
60143
60274
|
deadCode: inputOptions.deadCode ?? userConfig?.deadCode ?? true,
|
|
60275
|
+
supplyChain: inputOptions.supplyChain ?? userConfig?.supplyChain?.enabled ?? true,
|
|
60144
60276
|
verbose: inputOptions.verbose ?? userConfig?.verbose ?? false,
|
|
60145
60277
|
outputDirectory: inputOptions.outputDirectory || null,
|
|
60146
60278
|
scoreOnly: inputOptions.scoreOnly ?? false,
|
|
@@ -60180,6 +60312,7 @@ const buildRunEventConfig = (options, userConfig, hasCustomConfig, resolvedWorke
|
|
|
60180
60312
|
maxDurationMs: options.maxDurationMs,
|
|
60181
60313
|
lint: options.lint,
|
|
60182
60314
|
deadCode: options.deadCode,
|
|
60315
|
+
supplyChain: options.supplyChain,
|
|
60183
60316
|
scoreOnly: options.scoreOnly,
|
|
60184
60317
|
noScore: options.noScore,
|
|
60185
60318
|
respectInlineDisables: options.respectInlineDisables,
|
|
@@ -60263,6 +60396,7 @@ const runBaselineComparison = async (params) => {
|
|
|
60263
60396
|
projectInfoOverride: params.headProjectInfo,
|
|
60264
60397
|
shouldSkipLint: !params.options.lint || !params.resolvedNodeBinaryPath,
|
|
60265
60398
|
shouldRunDeadCode: false,
|
|
60399
|
+
shouldRunSupplyChain: params.options.supplyChain,
|
|
60266
60400
|
shouldComputeScore: false,
|
|
60267
60401
|
shouldShowProgressSpinners: false,
|
|
60268
60402
|
oxlintConcurrency: params.options.concurrency
|
|
@@ -60351,6 +60485,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
|
|
|
60351
60485
|
configSourceDirectory,
|
|
60352
60486
|
shouldSkipLint: !options.lint || lintBindingMissing,
|
|
60353
60487
|
shouldRunDeadCode: options.deadCode,
|
|
60488
|
+
shouldRunSupplyChain: options.supplyChain,
|
|
60354
60489
|
shouldComputeScore: !options.noScore,
|
|
60355
60490
|
shouldShowProgressSpinners,
|
|
60356
60491
|
oxlintConcurrency: options.concurrency
|
|
@@ -62636,8 +62771,8 @@ const resolveParallelFlag = (parallel) => parallel === false ? 1 : void 0;
|
|
|
62636
62771
|
* accepts. Flag-derived fields only (`scoreOnly`, `noScore`, `silent`,
|
|
62637
62772
|
* `isCi`): every `userConfig` fallback — including `noScore` — lives in
|
|
62638
62773
|
* `inspect()`'s merge layer, which sees each project's module-merged
|
|
62639
|
-
* config. The plain boolean knobs (`lint`, `deadCode`, `
|
|
62640
|
-
* through unchanged for the same reason. This resolver reads
|
|
62774
|
+
* config. The plain boolean knobs (`lint`, `deadCode`, `supplyChain`,
|
|
62775
|
+
* `verbose`) pass through unchanged for the same reason. This resolver reads
|
|
62641
62776
|
* `userConfig` only to decide the `--blocking warning` gate.
|
|
62642
62777
|
*/
|
|
62643
62778
|
const resolveCliInspectOptions = (flags, userConfig) => {
|
|
@@ -62645,6 +62780,7 @@ const resolveCliInspectOptions = (flags, userConfig) => {
|
|
|
62645
62780
|
return {
|
|
62646
62781
|
lint: flags.lint,
|
|
62647
62782
|
deadCode: flags.deadCode,
|
|
62783
|
+
supplyChain: flags.supplyChain,
|
|
62648
62784
|
verbose: flags.verbose,
|
|
62649
62785
|
outputDirectory: flags.outputDir,
|
|
62650
62786
|
respectInlineDisables: flags.respectInlineDisables === false ? false : void 0,
|
|
@@ -62975,6 +63111,11 @@ const finalizeScans = (input) => {
|
|
|
62975
63111
|
const baselineComputed = input.completedScans.length > 0 && input.completedScans.every((scan) => scan.result.baselineDelta !== void 0);
|
|
62976
63112
|
const baselineDegraded = input.baselineIntended && !baselineComputed;
|
|
62977
63113
|
const mode = baselineDegraded ? "diff" : input.mode;
|
|
63114
|
+
const isReactDetected = input.completedScans.some((scan) => hasReactRuntime(scan.result.project));
|
|
63115
|
+
if (input.completedScans.length > 0 && !isReactDetected) {
|
|
63116
|
+
recordCount(METRIC.scanNoReactDetected, 1);
|
|
63117
|
+
cliLogger.warn(`No React project detected at ${input.resolvedDirectory} — React rules were gated off; this is not the same as a clean scan.`);
|
|
63118
|
+
}
|
|
62978
63119
|
const jsonCompletedScans = filterCompletedScansByCategories(input.completedScans, input.categoryFilters);
|
|
62979
63120
|
if (input.isJsonMode) {
|
|
62980
63121
|
const baseline = baselineComputed && baselineDeltas.length > 0 ? {
|
|
@@ -63190,7 +63331,7 @@ const inspectAction = async (directory, flags) => {
|
|
|
63190
63331
|
const scanDirectory = projectScanTarget.resolvedDirectory;
|
|
63191
63332
|
const projectConfig = projectDirectory === resolvedDirectory ? userConfig : mergeReactDoctorConfigs(userConfig, projectScanTarget.userConfig ?? void 0);
|
|
63192
63333
|
const projectConfigSourceDirectory = projectScanTarget.userConfig?.plugins === void 0 ? scanTarget.configSourceDirectory : projectScanTarget.configSourceDirectory;
|
|
63193
|
-
const supplyChainEnabled = projectConfig?.supplyChain?.enabled !== false;
|
|
63334
|
+
const supplyChainEnabled = flags.supplyChain ?? projectConfig?.supplyChain?.enabled !== false;
|
|
63194
63335
|
let includePaths;
|
|
63195
63336
|
let supplyChainManifestChanged = false;
|
|
63196
63337
|
if (isDiffMode) {
|
|
@@ -64110,10 +64251,12 @@ const ROOT_FLAG_SPEC = {
|
|
|
64110
64251
|
"--no-parallel",
|
|
64111
64252
|
"--no-respect-inline-disables",
|
|
64112
64253
|
"--no-score",
|
|
64254
|
+
"--no-supply-chain",
|
|
64113
64255
|
"--no-telemetry",
|
|
64114
64256
|
"--no-warnings",
|
|
64115
64257
|
"--score",
|
|
64116
64258
|
"--staged",
|
|
64259
|
+
"--supply-chain",
|
|
64117
64260
|
"--verbose",
|
|
64118
64261
|
"--version",
|
|
64119
64262
|
"--warnings",
|
|
@@ -64362,7 +64505,7 @@ ${highlighter.dim("Learn more:")}
|
|
|
64362
64505
|
${highlighter.info(CI_URL)}
|
|
64363
64506
|
`;
|
|
64364
64507
|
const collectCategoryOption = (value, previousValues) => [...previousValues ?? [], value];
|
|
64365
|
-
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);
|
|
64508
|
+
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("--supply-chain", "enable the dependency supply-chain scan (default)").option("--no-supply-chain", "skip the dependency supply-chain scan (Socket.dev dependency health checks)").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);
|
|
64366
64509
|
program.action(inspectAction);
|
|
64367
64510
|
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));
|
|
64368
64511
|
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);
|
|
@@ -64412,4 +64555,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
|
|
|
64412
64555
|
export {};
|
|
64413
64556
|
|
|
64414
64557
|
//# sourceMappingURL=cli.js.map
|
|
64415
|
-
//# debugId=
|
|
64558
|
+
//# debugId=47338856-c658-5c78-a5e2-e871c2dafaa7
|
package/dist/index.d.ts
CHANGED
|
@@ -9493,6 +9493,17 @@ interface Diagnostic {
|
|
|
9493
9493
|
/** 1-indexed end column of the primary span, when derivable. */
|
|
9494
9494
|
endColumn?: number;
|
|
9495
9495
|
category: string;
|
|
9496
|
+
/**
|
|
9497
|
+
* Set when the finding's identity is the flagged element itself (a missing
|
|
9498
|
+
* attribute, a wrong element) rather than the flagged line's text — every
|
|
9499
|
+
* Accessibility-category finding, plus rules that opt in via their
|
|
9500
|
+
* `matchByOccurrence` metadata flag. `computeDiagnosticDelta` matches these
|
|
9501
|
+
* by `(filePath, plugin/rule)` occurrence count, so reformatting the flagged
|
|
9502
|
+
* line doesn't reclassify a pre-existing finding as new. Resolved at
|
|
9503
|
+
* diagnostic creation, where rule metadata is available. Absent means
|
|
9504
|
+
* line-text-sensitive matching (the default).
|
|
9505
|
+
*/
|
|
9506
|
+
matchByOccurrence?: boolean;
|
|
9496
9507
|
/**
|
|
9497
9508
|
* Set when the file never ships to users (`"test"` / `"story"`), so
|
|
9498
9509
|
* renderers can label the site instead of implying production impact.
|
|
@@ -9644,6 +9655,17 @@ interface DiagnoseResult {
|
|
|
9644
9655
|
/** See `InspectResult.skippedCheckReasons`. */
|
|
9645
9656
|
skippedCheckReasons?: Record<string, string>;
|
|
9646
9657
|
project: ProjectInfo;
|
|
9658
|
+
/**
|
|
9659
|
+
* Whether the scanned project resolved a React-compatible runtime (React
|
|
9660
|
+
* or Preact). `false` means every React-runtime rule family was gated
|
|
9661
|
+
* off, so an empty `diagnostics` array is vacuous — NOT the same as a
|
|
9662
|
+
* clean React scan. Consumers gating on the result should treat
|
|
9663
|
+
* `reactDetected === false` as "wrong scan target", not "all clear".
|
|
9664
|
+
* Mirrors `JsonReport.reactDetected`; same predicate as
|
|
9665
|
+
* `hasReactRuntime(result.project)`. Always set by `diagnose()`;
|
|
9666
|
+
* optional so hand-constructed results keep compiling.
|
|
9667
|
+
*/
|
|
9668
|
+
reactDetected?: boolean;
|
|
9647
9669
|
elapsedMilliseconds: number;
|
|
9648
9670
|
}
|
|
9649
9671
|
/**
|
|
@@ -9693,6 +9715,13 @@ interface DiagnoseProjectsResult {
|
|
|
9693
9715
|
projects: ProjectResult[];
|
|
9694
9716
|
diagnostics: Diagnostic[];
|
|
9695
9717
|
score: ScoreResult | null;
|
|
9718
|
+
/**
|
|
9719
|
+
* Whether any successfully scanned project resolved a React-compatible
|
|
9720
|
+
* runtime (React or Preact). Absent when no project scanned successfully.
|
|
9721
|
+
* See `DiagnoseResult.reactDetected` for gating guidance; per-project
|
|
9722
|
+
* detail is on each `ProjectResultOk.reactDetected`.
|
|
9723
|
+
*/
|
|
9724
|
+
reactDetected?: boolean;
|
|
9696
9725
|
elapsedMilliseconds: number;
|
|
9697
9726
|
} //#endregion
|
|
9698
9727
|
//#region src/types/handle-error.d.ts
|
|
@@ -9888,6 +9917,17 @@ interface JsonReportV1$1 {
|
|
|
9888
9917
|
ok: boolean;
|
|
9889
9918
|
directory: string;
|
|
9890
9919
|
mode: JsonReportMode;
|
|
9920
|
+
/**
|
|
9921
|
+
* Whether any scanned project resolved a React-compatible runtime (React
|
|
9922
|
+
* or Preact). `false` means every React-runtime rule family was gated off,
|
|
9923
|
+
* so an empty `diagnostics` array is vacuous — NOT the same as a clean
|
|
9924
|
+
* React scan. Consumers gating on the report (CI, verifiers, hooks) should
|
|
9925
|
+
* treat `reactDetected === false` as "wrong scan target", not "all clear"
|
|
9926
|
+
* (per project, `projects[].project.reactVersion` / `preactVersion` say
|
|
9927
|
+
* which roots were non-React). Absent when nothing was scanned (`projects`
|
|
9928
|
+
* is empty), on error reports, and on reports from older CLI versions.
|
|
9929
|
+
*/
|
|
9930
|
+
reactDetected?: boolean;
|
|
9891
9931
|
diff: JsonReportDiffInfo | null;
|
|
9892
9932
|
projects: JsonReportProjectEntry[];
|
|
9893
9933
|
/**
|
|
@@ -10172,6 +10212,16 @@ declare const summarizeDiagnostics: (diagnostics: Diagnostic[], worstScore?: num
|
|
|
10172
10212
|
declare const defineConfig: (config: ReactDoctorConfig) => ReactDoctorConfig; //#endregion
|
|
10173
10213
|
//#region src/utils/detect-ai-training-environment.d.ts
|
|
10174
10214
|
//#endregion
|
|
10215
|
+
//#region src/utils/has-react-runtime.d.ts
|
|
10216
|
+
/**
|
|
10217
|
+
* Whether project discovery resolved a React-compatible runtime (React or
|
|
10218
|
+
* Preact, which ships the same hooks + JSX model). This is the same
|
|
10219
|
+
* predicate that grants the `react` capability, so `false` means every
|
|
10220
|
+
* React-runtime rule family was gated off for the project.
|
|
10221
|
+
*/
|
|
10222
|
+
declare const hasReactRuntime: (project: ProjectInfo) => boolean; //#endregion
|
|
10223
|
+
//#region src/utils/is-errno-exception.d.ts
|
|
10224
|
+
//#endregion
|
|
10175
10225
|
//#region ../api/dist/index.d.ts
|
|
10176
10226
|
//#region src/diagnose.d.ts
|
|
10177
10227
|
interface Diagnose {
|
|
@@ -10198,5 +10248,5 @@ interface ToJsonReportOptions {
|
|
|
10198
10248
|
}
|
|
10199
10249
|
declare const toJsonReport: (result: DiagnoseResult, options: ToJsonReportOptions) => JsonReport;
|
|
10200
10250
|
//#endregion
|
|
10201
|
-
export { AmbiguousProjectError, type DiagnoseOptions, type DiagnoseProjectsInput, type DiagnoseProjectsResult, type DiagnoseResult, type Diagnostic, type DiffInfo, type JsonReport, type JsonReportDiffInfo, type JsonReportError, type JsonReportMode, type JsonReportProjectEntry, type JsonReportSummary, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, type ProjectDefinition, type ProjectInfo, ProjectNotFoundError, type ProjectResult, type ProjectResultError, type ProjectResultOk, type ReactDoctorConfig, ReactDoctorError, type ScoreResult, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
|
|
10251
|
+
export { AmbiguousProjectError, type DiagnoseOptions, type DiagnoseProjectsInput, type DiagnoseProjectsResult, type DiagnoseResult, type Diagnostic, type DiffInfo, type JsonReport, type JsonReportDiffInfo, type JsonReportError, type JsonReportMode, type JsonReportProjectEntry, type JsonReportSummary, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, type ProjectDefinition, type ProjectInfo, ProjectNotFoundError, type ProjectResult, type ProjectResultError, type ProjectResultOk, type ReactDoctorConfig, ReactDoctorError, type ScoreResult, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, hasReactRuntime, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
|
|
10202
10252
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="f00947fe-3794-5d73-a491-232b535dc962")}catch(e){}}();
|
|
3
3
|
import { r as __toESM$1, t as __commonJSMin$1 } from "./chunk-N93fKeF6.js";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import * as NFS from "node:fs";
|
|
@@ -19304,6 +19304,7 @@ var Diagnostic = class extends Class("Diagnostic")({
|
|
|
19304
19304
|
endLine: optional(Number$1),
|
|
19305
19305
|
endColumn: optional(Number$1),
|
|
19306
19306
|
category: String$1,
|
|
19307
|
+
matchByOccurrence: optional(Boolean$1),
|
|
19307
19308
|
fileContext: optional(Literals(["test", "story"])),
|
|
19308
19309
|
suppressionHint: optional(String$1),
|
|
19309
19310
|
relatedLocations: optional(ArraySchema(DiagnosticRelatedLocation)),
|
|
@@ -19374,6 +19375,16 @@ var JsonReportV1 = class extends Class("JsonReportV1")({
|
|
|
19374
19375
|
* successful `baseline` run (`schemaVersion: 2`) and on non-compare scopes.
|
|
19375
19376
|
*/
|
|
19376
19377
|
baselineDegraded: optional(Boolean$1),
|
|
19378
|
+
/**
|
|
19379
|
+
* Whether any scanned project resolved a React-compatible runtime (React
|
|
19380
|
+
* or Preact). `false` means every React-runtime rule family was gated off,
|
|
19381
|
+
* so an empty `diagnostics` array is vacuous — NOT the same as a clean
|
|
19382
|
+
* React scan. Consumers gating on the report (CI, verifiers, hooks) should
|
|
19383
|
+
* treat `reactDetected === false` as "wrong scan target", not "all clear".
|
|
19384
|
+
* Absent when nothing was scanned (`projects` is empty), on error reports,
|
|
19385
|
+
* and on reports from older CLI versions.
|
|
19386
|
+
*/
|
|
19387
|
+
reactDetected: optional(Boolean$1),
|
|
19377
19388
|
diff: NullOr(JsonReportDiffInfo),
|
|
19378
19389
|
projects: ArraySchema(JsonReportProjectEntry),
|
|
19379
19390
|
diagnostics: ArraySchema(Diagnostic),
|
|
@@ -19399,6 +19410,8 @@ var JsonReportV2 = class extends Class("JsonReportV2")({
|
|
|
19399
19410
|
ok: Boolean$1,
|
|
19400
19411
|
directory: String$1,
|
|
19401
19412
|
mode: JsonReportMode,
|
|
19413
|
+
/** See `JsonReportV1.reactDetected`. */
|
|
19414
|
+
reactDetected: optional(Boolean$1),
|
|
19402
19415
|
diff: NullOr(JsonReportDiffInfo),
|
|
19403
19416
|
baseline: JsonReportBaseline,
|
|
19404
19417
|
projects: ArraySchema(JsonReportProjectEntry),
|
|
@@ -32907,6 +32920,7 @@ const DIAGNOSTIC_CATEGORY_BUCKETS = [
|
|
|
32907
32920
|
"Accessibility",
|
|
32908
32921
|
"Maintainability"
|
|
32909
32922
|
];
|
|
32923
|
+
const OCCURRENCE_MATCHED_CATEGORIES = new Set(["Accessibility"]);
|
|
32910
32924
|
const APP_ONLY_RULE_KEYS = new Set([
|
|
32911
32925
|
"react-hooks-js/static-components",
|
|
32912
32926
|
"react-doctor/no-render-prop-children",
|
|
@@ -32929,7 +32943,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
|
|
|
32929
32943
|
const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
|
|
32930
32944
|
const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
|
|
32931
32945
|
const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
|
|
32932
|
-
const CORE_PACKAGE_VERSION = "0.7.
|
|
32946
|
+
const CORE_PACKAGE_VERSION = "0.7.1";
|
|
32933
32947
|
const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
|
|
32934
32948
|
const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
|
|
32935
32949
|
const SUPPLY_CHAIN_PLUGIN = "socket";
|
|
@@ -33050,6 +33064,23 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
|
|
|
33050
33064
|
const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
|
|
33051
33065
|
const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
|
|
33052
33066
|
const countSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).length;
|
|
33067
|
+
/**
|
|
33068
|
+
* The chain of ancestor directories of `startDirectory`, nearest first, up to
|
|
33069
|
+
* and including the filesystem root. With `includeStart`, the chain begins at
|
|
33070
|
+
* `startDirectory` itself; otherwise at its parent. The one place the "walk up
|
|
33071
|
+
* the tree" traversal is expressed, so each caller adds only its own match /
|
|
33072
|
+
* stop policy instead of re-deriving the `path.dirname` loop.
|
|
33073
|
+
*/
|
|
33074
|
+
const ancestorDirectories = (startDirectory, { includeStart }) => {
|
|
33075
|
+
const directories = [];
|
|
33076
|
+
let currentDirectory = includeStart ? startDirectory : Path.dirname(startDirectory);
|
|
33077
|
+
while (true) {
|
|
33078
|
+
directories.push(currentDirectory);
|
|
33079
|
+
const parentDirectory = Path.dirname(currentDirectory);
|
|
33080
|
+
if (parentDirectory === currentDirectory) return directories;
|
|
33081
|
+
currentDirectory = parentDirectory;
|
|
33082
|
+
}
|
|
33083
|
+
};
|
|
33053
33084
|
const cachedPackageJsons = /* @__PURE__ */ new Map();
|
|
33054
33085
|
const clearPackageJsonCache = () => {
|
|
33055
33086
|
cachedPackageJsons.clear();
|
|
@@ -33083,11 +33114,7 @@ const isMonorepoRoot = (directory) => {
|
|
|
33083
33114
|
return Array.isArray(packageJson.workspaces) || Boolean(packageJson.workspaces?.packages);
|
|
33084
33115
|
};
|
|
33085
33116
|
const findMonorepoRoot = (startDirectory) => {
|
|
33086
|
-
|
|
33087
|
-
while (currentDirectory !== Path.dirname(currentDirectory)) {
|
|
33088
|
-
if (isMonorepoRoot(currentDirectory)) return currentDirectory;
|
|
33089
|
-
currentDirectory = Path.dirname(currentDirectory);
|
|
33090
|
-
}
|
|
33117
|
+
for (const directory of ancestorDirectories(startDirectory, { includeStart: false })) if (isMonorepoRoot(directory)) return directory;
|
|
33091
33118
|
return null;
|
|
33092
33119
|
};
|
|
33093
33120
|
/**
|
|
@@ -33733,6 +33760,74 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
|
|
|
33733
33760
|
framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
|
|
33734
33761
|
};
|
|
33735
33762
|
};
|
|
33763
|
+
/**
|
|
33764
|
+
* Walk up from `startDirectory` to the nearest ancestor that owns a
|
|
33765
|
+
* `package.json`, stopping at (and including) the enclosing project boundary
|
|
33766
|
+
* — the working tree's git root or a monorepo root. Lets a scan of a package
|
|
33767
|
+
* subfolder that has no `package.json` of its own adopt the nearest enclosing
|
|
33768
|
+
* package as its project root, so it inherits that package's dependency +
|
|
33769
|
+
* framework detection instead of synthesizing an empty (React-blind) project.
|
|
33770
|
+
*
|
|
33771
|
+
* Generalizes the older monorepo-root-only lookup: the nearest ancestor is the
|
|
33772
|
+
* most specific owning package (a leaf workspace, not just the repo root), so
|
|
33773
|
+
* a subfolder of one workspace no longer borrows a sibling's capabilities.
|
|
33774
|
+
*
|
|
33775
|
+
* Returns `null` when no ancestor `package.json` exists inside the project
|
|
33776
|
+
* boundary (a loose tree of files, or a subfolder of a package-less repo).
|
|
33777
|
+
*/
|
|
33778
|
+
const findNearestAncestorPackageJson = (startDirectory) => {
|
|
33779
|
+
if (isProjectBoundary(startDirectory)) return null;
|
|
33780
|
+
for (const directory of ancestorDirectories(startDirectory, { includeStart: false })) {
|
|
33781
|
+
if (isFile(Path.join(directory, "package.json"))) return directory;
|
|
33782
|
+
if (isProjectBoundary(directory)) return null;
|
|
33783
|
+
}
|
|
33784
|
+
return null;
|
|
33785
|
+
};
|
|
33786
|
+
/**
|
|
33787
|
+
* Last-resort React detection: locate React the way Node's `node_modules`
|
|
33788
|
+
* resolution would from `directory` — check each `node_modules/react` up the
|
|
33789
|
+
* tree — but bounded to the enclosing repo so a globally installed React can't
|
|
33790
|
+
* masquerade as the project's version. Stops at (and including) the nearest
|
|
33791
|
+
* project boundary — the git root or a monorepo root — and returns the first
|
|
33792
|
+
* installed `version`.
|
|
33793
|
+
*
|
|
33794
|
+
* Makes "React is installed and importable" ⇒ "React is detected" an invariant
|
|
33795
|
+
* for packages whose only React declaration is a version-less spec
|
|
33796
|
+
* (`workspace:*`, `*`, a dist-tag) or where React lives solely in a hoisted
|
|
33797
|
+
* `node_modules` the declaration walks never reach — the profile of a component
|
|
33798
|
+
* package inside a monorepo. Matching the `node_modules/react` entry rather than
|
|
33799
|
+
* its realpath means a project whose `node_modules` is symlinked elsewhere (a
|
|
33800
|
+
* Docker volume, a shared store) still counts, while a global install outside
|
|
33801
|
+
* the repo does not.
|
|
33802
|
+
*
|
|
33803
|
+
* Returns `null` when React isn't installed within the repo or its package.json
|
|
33804
|
+
* carries no version string. A tree with no boundary marker at all bounds the
|
|
33805
|
+
* search to `directory`, so a dependency hoisted above the scanned package there
|
|
33806
|
+
* is conservatively not adopted.
|
|
33807
|
+
*/
|
|
33808
|
+
/**
|
|
33809
|
+
* The nearest enclosing project boundary (git root or monorepo root), or
|
|
33810
|
+
* `directory` itself when the scan target sits outside any repo. Bounds the
|
|
33811
|
+
* node_modules walk so a React hoisted anywhere inside the repo counts, but a
|
|
33812
|
+
* React in an ancestor above the repo — a home-directory or global
|
|
33813
|
+
* `node_modules` — can't leak in.
|
|
33814
|
+
*/
|
|
33815
|
+
const findContainmentRoot = (directory) => {
|
|
33816
|
+
for (const ancestorDirectory of ancestorDirectories(directory, { includeStart: true })) if (isProjectBoundary(ancestorDirectory)) return ancestorDirectory;
|
|
33817
|
+
return directory;
|
|
33818
|
+
};
|
|
33819
|
+
const resolveInstalledReactVersion = (directory) => {
|
|
33820
|
+
const containmentRoot = findContainmentRoot(directory);
|
|
33821
|
+
for (const ancestorDirectory of ancestorDirectories(directory, { includeStart: true })) {
|
|
33822
|
+
const reactPackageJsonPath = Path.join(ancestorDirectory, "node_modules", "react", "package.json");
|
|
33823
|
+
if (isFile(reactPackageJsonPath)) {
|
|
33824
|
+
const installedVersion = readPackageJson(reactPackageJsonPath).version;
|
|
33825
|
+
return typeof installedVersion === "string" ? installedVersion : null;
|
|
33826
|
+
}
|
|
33827
|
+
if (ancestorDirectory === containmentRoot) return null;
|
|
33828
|
+
}
|
|
33829
|
+
return null;
|
|
33830
|
+
};
|
|
33736
33831
|
const findInWorkspacePackageJsons = (rootDirectory, rootPackageJson, select) => {
|
|
33737
33832
|
const rootValue = select(rootPackageJson);
|
|
33738
33833
|
if (rootValue !== null) return rootValue;
|
|
@@ -34072,19 +34167,20 @@ const clearProjectCache = () => {
|
|
|
34072
34167
|
};
|
|
34073
34168
|
/**
|
|
34074
34169
|
* Build a `ProjectInfo` for a directory that has no `package.json` of
|
|
34075
|
-
* its own — a
|
|
34076
|
-
* of TypeScript/JavaScript files. Dependency + framework
|
|
34077
|
-
* inherited from the enclosing
|
|
34078
|
-
*
|
|
34079
|
-
*
|
|
34080
|
-
*
|
|
34081
|
-
*
|
|
34170
|
+
* its own — a package subfolder like `repo/packages` or `app/src/features`,
|
|
34171
|
+
* or any loose tree of TypeScript/JavaScript files. Dependency + framework
|
|
34172
|
+
* detection is inherited from the nearest enclosing package (a leaf
|
|
34173
|
+
* workspace, a plain app root, or a monorepo root — whichever is closest,
|
|
34174
|
+
* bounded by the git root), so scanning a subdirectory of a React project
|
|
34175
|
+
* still gets the React capabilities; a standalone non-React directory simply
|
|
34176
|
+
* scans with the framework-agnostic rules. Throws only when the directory has
|
|
34177
|
+
* nothing to scan (no enclosing project and no source files of its own).
|
|
34082
34178
|
*/
|
|
34083
34179
|
const discoverProjectWithoutPackageJson = (directory) => {
|
|
34084
34180
|
const sourceFileCount = countSourceFiles(directory);
|
|
34085
34181
|
const hasOwnTsConfig = NFS.existsSync(Path.join(directory, "tsconfig.json"));
|
|
34086
|
-
const
|
|
34087
|
-
const enclosingProject =
|
|
34182
|
+
const enclosingProjectRoot = findNearestAncestorPackageJson(directory);
|
|
34183
|
+
const enclosingProject = enclosingProjectRoot !== null ? discoverProject(enclosingProjectRoot) : null;
|
|
34088
34184
|
if (enclosingProject !== null) return {
|
|
34089
34185
|
...enclosingProject,
|
|
34090
34186
|
rootDirectory: directory,
|
|
@@ -34188,6 +34284,7 @@ const discoverProject = (directory) => {
|
|
|
34188
34284
|
if (!reactVersion && reactDeclaration.version && !isCatalogReference(reactDeclaration.version)) reactVersion = reactDeclaration.version;
|
|
34189
34285
|
if (!tailwindVersion && tailwindDeclaration.version && !isCatalogReference(tailwindDeclaration.version)) tailwindVersion = tailwindDeclaration.version;
|
|
34190
34286
|
if (!zodVersion && zodDeclaration.version && !isCatalogReference(zodDeclaration.version)) zodVersion = zodDeclaration.version;
|
|
34287
|
+
if (!reactVersion || parseReactMajor(reactVersion) === null) reactVersion = resolveInstalledReactVersion(directory) ?? reactVersion;
|
|
34191
34288
|
const projectName = packageJson.name ?? Path.basename(directory);
|
|
34192
34289
|
const hasTypeScript = NFS.existsSync(Path.join(directory, "tsconfig.json"));
|
|
34193
34290
|
const sourceFileCount = countSourceFiles(directory);
|
|
@@ -37010,10 +37107,17 @@ function* collectSecurityScanFiles(rootDirectory) {
|
|
|
37010
37107
|
}
|
|
37011
37108
|
}
|
|
37012
37109
|
}
|
|
37110
|
+
/**
|
|
37111
|
+
* Whether project discovery resolved a React-compatible runtime (React or
|
|
37112
|
+
* Preact, which ships the same hooks + JSX model). This is the same
|
|
37113
|
+
* predicate that grants the `react` capability, so `false` means every
|
|
37114
|
+
* React-runtime rule family was gated off for the project.
|
|
37115
|
+
*/
|
|
37116
|
+
const hasReactRuntime = (project) => project.reactVersion !== null || project.preactVersion !== null;
|
|
37013
37117
|
const buildCapabilities = (project) => {
|
|
37014
37118
|
const capabilities = /* @__PURE__ */ new Set();
|
|
37015
37119
|
capabilities.add(project.framework);
|
|
37016
|
-
if (project
|
|
37120
|
+
if (hasReactRuntime(project)) capabilities.add("react");
|
|
37017
37121
|
if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
|
|
37018
37122
|
if (project.expoVersion !== null) capabilities.add("expo");
|
|
37019
37123
|
if (project.nextjsMajorVersion !== null && project.nextjsMajorVersion >= 15) capabilities.add("nextjs:15");
|
|
@@ -38543,7 +38647,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
38543
38647
|
})),
|
|
38544
38648
|
showStagedContent: (directory, relativePath, options) => runCommand({
|
|
38545
38649
|
command: "git",
|
|
38546
|
-
args: ["show",
|
|
38650
|
+
args: ["show", `:./${relativePath}`],
|
|
38547
38651
|
directory,
|
|
38548
38652
|
maxStdoutBytes: options?.maxBufferBytes
|
|
38549
38653
|
}).pipe(map$3((result) => result.status === 0 ? result.stdout : null)),
|
|
@@ -39849,6 +39953,7 @@ const parseRuleCode = (code) => {
|
|
|
39849
39953
|
};
|
|
39850
39954
|
};
|
|
39851
39955
|
const resolveDiagnosticCategory = (plugin, rule) => getRuleCategory(rule) ?? lookupOwnString(PLUGIN_CATEGORY_MAP, plugin) ?? "Bugs";
|
|
39956
|
+
const resolveMatchByOccurrence = (rule, category) => OCCURRENCE_MATCHED_CATEGORIES.has(category) || Boolean(reactDoctorPlugin.rules[rule]?.matchByOccurrence);
|
|
39852
39957
|
/**
|
|
39853
39958
|
* Maps oxlint's non-primary labels (`labels[1..]`) into related source
|
|
39854
39959
|
* locations. Editors surface these as a diagnostic's
|
|
@@ -39912,6 +40017,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
|
|
|
39912
40017
|
const normalizedFilePath = diagnostic.filename.replaceAll("\\", "/");
|
|
39913
40018
|
const primarySpan = primaryLabel?.span;
|
|
39914
40019
|
const relatedLocations = buildRelatedLocations(diagnostic.labels, normalizedFilePath);
|
|
40020
|
+
const category = resolveDiagnosticCategory(plugin, rule);
|
|
39915
40021
|
return {
|
|
39916
40022
|
filePath: normalizedFilePath,
|
|
39917
40023
|
plugin,
|
|
@@ -39927,7 +40033,8 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
|
|
|
39927
40033
|
offset: primarySpan.offset,
|
|
39928
40034
|
length: primarySpan.length
|
|
39929
40035
|
} : {},
|
|
39930
|
-
category
|
|
40036
|
+
category,
|
|
40037
|
+
...resolveMatchByOccurrence(rule, category) ? { matchByOccurrence: true } : {},
|
|
39931
40038
|
...relatedLocations.length > 0 ? { relatedLocations } : {}
|
|
39932
40039
|
};
|
|
39933
40040
|
});
|
|
@@ -42088,6 +42195,7 @@ const buildJsonReport = (input) => {
|
|
|
42088
42195
|
const worstScoredProject = findWorstScoredProject(projects);
|
|
42089
42196
|
const summary = summarizeDiagnostics(flattenedDiagnostics, worstScoredProject?.score?.score ?? null, worstScoredProject?.score?.label ?? null);
|
|
42090
42197
|
const shared = {
|
|
42198
|
+
...input.scans.length > 0 ? { reactDetected: input.scans.some((scan) => hasReactRuntime(scan.result.project)) } : {},
|
|
42091
42199
|
version: input.version,
|
|
42092
42200
|
ok: true,
|
|
42093
42201
|
directory: input.directory,
|
|
@@ -42317,6 +42425,7 @@ const outputToDiagnoseResult = (output, elapsedMilliseconds) => {
|
|
|
42317
42425
|
skippedChecks,
|
|
42318
42426
|
...Object.keys(skippedCheckReasons).length > 0 ? { skippedCheckReasons } : {},
|
|
42319
42427
|
project: output.project,
|
|
42428
|
+
reactDetected: hasReactRuntime(output.project),
|
|
42320
42429
|
elapsedMilliseconds
|
|
42321
42430
|
};
|
|
42322
42431
|
};
|
|
@@ -42372,10 +42481,12 @@ const diagnoseProjectBatch = async (input) => {
|
|
|
42372
42481
|
const startTime = globalThis.performance.now();
|
|
42373
42482
|
const { projects, concurrency, config: batchConfig, ...baseOptions } = input;
|
|
42374
42483
|
const projectResults = await mapWithConcurrency(projects, concurrency ?? 4, (projectDefinition) => diagnoseProject(projectDefinition, baseOptions, batchConfig));
|
|
42484
|
+
const succeededProjects = projectResults.filter((projectResult) => projectResult.ok);
|
|
42375
42485
|
return {
|
|
42376
42486
|
projects: projectResults,
|
|
42377
|
-
diagnostics:
|
|
42487
|
+
diagnostics: succeededProjects.flatMap((projectResult) => projectResult.diagnostics),
|
|
42378
42488
|
score: findWorstScore(projectResults),
|
|
42489
|
+
...succeededProjects.length > 0 ? { reactDetected: succeededProjects.some((projectResult) => projectResult.reactDetected) } : {},
|
|
42379
42490
|
elapsedMilliseconds: globalThis.performance.now() - startTime
|
|
42380
42491
|
};
|
|
42381
42492
|
};
|
|
@@ -42410,7 +42521,7 @@ const toJsonReport = (result, options) => buildJsonReport({
|
|
|
42410
42521
|
totalElapsedMilliseconds: result.elapsedMilliseconds
|
|
42411
42522
|
});
|
|
42412
42523
|
//#endregion
|
|
42413
|
-
export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
|
|
42524
|
+
export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, hasReactRuntime, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
|
|
42414
42525
|
|
|
42415
42526
|
//# sourceMappingURL=index.js.map
|
|
42416
|
-
//# debugId=
|
|
42527
|
+
//# debugId=f00947fe-3794-5d73-a491-232b535dc962
|
package/dist/lsp.js
CHANGED
|
@@ -19333,6 +19333,7 @@ var Diagnostic = class extends Class("Diagnostic")({
|
|
|
19333
19333
|
endLine: optional(Number$1),
|
|
19334
19334
|
endColumn: optional(Number$1),
|
|
19335
19335
|
category: String$1,
|
|
19336
|
+
matchByOccurrence: optional(Boolean$1),
|
|
19336
19337
|
fileContext: optional(Literals(["test", "story"])),
|
|
19337
19338
|
suppressionHint: optional(String$1),
|
|
19338
19339
|
relatedLocations: optional(ArraySchema(DiagnosticRelatedLocation)),
|
|
@@ -19410,6 +19411,16 @@ var JsonReportV1 = class extends Class("JsonReportV1")({
|
|
|
19410
19411
|
* successful `baseline` run (`schemaVersion: 2`) and on non-compare scopes.
|
|
19411
19412
|
*/
|
|
19412
19413
|
baselineDegraded: optional(Boolean$1),
|
|
19414
|
+
/**
|
|
19415
|
+
* Whether any scanned project resolved a React-compatible runtime (React
|
|
19416
|
+
* or Preact). `false` means every React-runtime rule family was gated off,
|
|
19417
|
+
* so an empty `diagnostics` array is vacuous — NOT the same as a clean
|
|
19418
|
+
* React scan. Consumers gating on the report (CI, verifiers, hooks) should
|
|
19419
|
+
* treat `reactDetected === false` as "wrong scan target", not "all clear".
|
|
19420
|
+
* Absent when nothing was scanned (`projects` is empty), on error reports,
|
|
19421
|
+
* and on reports from older CLI versions.
|
|
19422
|
+
*/
|
|
19423
|
+
reactDetected: optional(Boolean$1),
|
|
19413
19424
|
diff: NullOr(JsonReportDiffInfo),
|
|
19414
19425
|
projects: ArraySchema(JsonReportProjectEntry),
|
|
19415
19426
|
diagnostics: ArraySchema(Diagnostic),
|
|
@@ -19435,6 +19446,8 @@ var JsonReportV2 = class extends Class("JsonReportV2")({
|
|
|
19435
19446
|
ok: Boolean$1,
|
|
19436
19447
|
directory: String$1,
|
|
19437
19448
|
mode: JsonReportMode,
|
|
19449
|
+
/** See `JsonReportV1.reactDetected`. */
|
|
19450
|
+
reactDetected: optional(Boolean$1),
|
|
19438
19451
|
diff: NullOr(JsonReportDiffInfo),
|
|
19439
19452
|
baseline: JsonReportBaseline,
|
|
19440
19453
|
projects: ArraySchema(JsonReportProjectEntry),
|
|
@@ -32966,6 +32979,7 @@ const DIAGNOSTIC_CATEGORY_BUCKETS = [
|
|
|
32966
32979
|
"Accessibility",
|
|
32967
32980
|
"Maintainability"
|
|
32968
32981
|
];
|
|
32982
|
+
const OCCURRENCE_MATCHED_CATEGORIES = new Set(["Accessibility"]);
|
|
32969
32983
|
const APP_ONLY_RULE_KEYS = new Set([
|
|
32970
32984
|
"react-hooks-js/static-components",
|
|
32971
32985
|
"react-doctor/no-render-prop-children",
|
|
@@ -32988,7 +33002,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
|
|
|
32988
33002
|
const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
|
|
32989
33003
|
const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
|
|
32990
33004
|
const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
|
|
32991
|
-
const CORE_PACKAGE_VERSION = "0.7.
|
|
33005
|
+
const CORE_PACKAGE_VERSION = "0.7.1";
|
|
32992
33006
|
const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
|
|
32993
33007
|
const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
|
|
32994
33008
|
const SUPPLY_CHAIN_PLUGIN = "socket";
|
|
@@ -33109,6 +33123,23 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
|
|
|
33109
33123
|
const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
|
|
33110
33124
|
const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
|
|
33111
33125
|
const countSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).length;
|
|
33126
|
+
/**
|
|
33127
|
+
* The chain of ancestor directories of `startDirectory`, nearest first, up to
|
|
33128
|
+
* and including the filesystem root. With `includeStart`, the chain begins at
|
|
33129
|
+
* `startDirectory` itself; otherwise at its parent. The one place the "walk up
|
|
33130
|
+
* the tree" traversal is expressed, so each caller adds only its own match /
|
|
33131
|
+
* stop policy instead of re-deriving the `path.dirname` loop.
|
|
33132
|
+
*/
|
|
33133
|
+
const ancestorDirectories = (startDirectory, { includeStart }) => {
|
|
33134
|
+
const directories = [];
|
|
33135
|
+
let currentDirectory = includeStart ? startDirectory : Path.dirname(startDirectory);
|
|
33136
|
+
while (true) {
|
|
33137
|
+
directories.push(currentDirectory);
|
|
33138
|
+
const parentDirectory = Path.dirname(currentDirectory);
|
|
33139
|
+
if (parentDirectory === currentDirectory) return directories;
|
|
33140
|
+
currentDirectory = parentDirectory;
|
|
33141
|
+
}
|
|
33142
|
+
};
|
|
33112
33143
|
const cachedPackageJsons = /* @__PURE__ */ new Map();
|
|
33113
33144
|
const clearPackageJsonCache = () => {
|
|
33114
33145
|
cachedPackageJsons.clear();
|
|
@@ -33142,11 +33173,7 @@ const isMonorepoRoot = (directory) => {
|
|
|
33142
33173
|
return Array.isArray(packageJson.workspaces) || Boolean(packageJson.workspaces?.packages);
|
|
33143
33174
|
};
|
|
33144
33175
|
const findMonorepoRoot = (startDirectory) => {
|
|
33145
|
-
|
|
33146
|
-
while (currentDirectory !== Path.dirname(currentDirectory)) {
|
|
33147
|
-
if (isMonorepoRoot(currentDirectory)) return currentDirectory;
|
|
33148
|
-
currentDirectory = Path.dirname(currentDirectory);
|
|
33149
|
-
}
|
|
33176
|
+
for (const directory of ancestorDirectories(startDirectory, { includeStart: false })) if (isMonorepoRoot(directory)) return directory;
|
|
33150
33177
|
return null;
|
|
33151
33178
|
};
|
|
33152
33179
|
/**
|
|
@@ -33792,6 +33819,74 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
|
|
|
33792
33819
|
framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
|
|
33793
33820
|
};
|
|
33794
33821
|
};
|
|
33822
|
+
/**
|
|
33823
|
+
* Walk up from `startDirectory` to the nearest ancestor that owns a
|
|
33824
|
+
* `package.json`, stopping at (and including) the enclosing project boundary
|
|
33825
|
+
* — the working tree's git root or a monorepo root. Lets a scan of a package
|
|
33826
|
+
* subfolder that has no `package.json` of its own adopt the nearest enclosing
|
|
33827
|
+
* package as its project root, so it inherits that package's dependency +
|
|
33828
|
+
* framework detection instead of synthesizing an empty (React-blind) project.
|
|
33829
|
+
*
|
|
33830
|
+
* Generalizes the older monorepo-root-only lookup: the nearest ancestor is the
|
|
33831
|
+
* most specific owning package (a leaf workspace, not just the repo root), so
|
|
33832
|
+
* a subfolder of one workspace no longer borrows a sibling's capabilities.
|
|
33833
|
+
*
|
|
33834
|
+
* Returns `null` when no ancestor `package.json` exists inside the project
|
|
33835
|
+
* boundary (a loose tree of files, or a subfolder of a package-less repo).
|
|
33836
|
+
*/
|
|
33837
|
+
const findNearestAncestorPackageJson = (startDirectory) => {
|
|
33838
|
+
if (isProjectBoundary(startDirectory)) return null;
|
|
33839
|
+
for (const directory of ancestorDirectories(startDirectory, { includeStart: false })) {
|
|
33840
|
+
if (isFile(Path.join(directory, "package.json"))) return directory;
|
|
33841
|
+
if (isProjectBoundary(directory)) return null;
|
|
33842
|
+
}
|
|
33843
|
+
return null;
|
|
33844
|
+
};
|
|
33845
|
+
/**
|
|
33846
|
+
* Last-resort React detection: locate React the way Node's `node_modules`
|
|
33847
|
+
* resolution would from `directory` — check each `node_modules/react` up the
|
|
33848
|
+
* tree — but bounded to the enclosing repo so a globally installed React can't
|
|
33849
|
+
* masquerade as the project's version. Stops at (and including) the nearest
|
|
33850
|
+
* project boundary — the git root or a monorepo root — and returns the first
|
|
33851
|
+
* installed `version`.
|
|
33852
|
+
*
|
|
33853
|
+
* Makes "React is installed and importable" ⇒ "React is detected" an invariant
|
|
33854
|
+
* for packages whose only React declaration is a version-less spec
|
|
33855
|
+
* (`workspace:*`, `*`, a dist-tag) or where React lives solely in a hoisted
|
|
33856
|
+
* `node_modules` the declaration walks never reach — the profile of a component
|
|
33857
|
+
* package inside a monorepo. Matching the `node_modules/react` entry rather than
|
|
33858
|
+
* its realpath means a project whose `node_modules` is symlinked elsewhere (a
|
|
33859
|
+
* Docker volume, a shared store) still counts, while a global install outside
|
|
33860
|
+
* the repo does not.
|
|
33861
|
+
*
|
|
33862
|
+
* Returns `null` when React isn't installed within the repo or its package.json
|
|
33863
|
+
* carries no version string. A tree with no boundary marker at all bounds the
|
|
33864
|
+
* search to `directory`, so a dependency hoisted above the scanned package there
|
|
33865
|
+
* is conservatively not adopted.
|
|
33866
|
+
*/
|
|
33867
|
+
/**
|
|
33868
|
+
* The nearest enclosing project boundary (git root or monorepo root), or
|
|
33869
|
+
* `directory` itself when the scan target sits outside any repo. Bounds the
|
|
33870
|
+
* node_modules walk so a React hoisted anywhere inside the repo counts, but a
|
|
33871
|
+
* React in an ancestor above the repo — a home-directory or global
|
|
33872
|
+
* `node_modules` — can't leak in.
|
|
33873
|
+
*/
|
|
33874
|
+
const findContainmentRoot = (directory) => {
|
|
33875
|
+
for (const ancestorDirectory of ancestorDirectories(directory, { includeStart: true })) if (isProjectBoundary(ancestorDirectory)) return ancestorDirectory;
|
|
33876
|
+
return directory;
|
|
33877
|
+
};
|
|
33878
|
+
const resolveInstalledReactVersion = (directory) => {
|
|
33879
|
+
const containmentRoot = findContainmentRoot(directory);
|
|
33880
|
+
for (const ancestorDirectory of ancestorDirectories(directory, { includeStart: true })) {
|
|
33881
|
+
const reactPackageJsonPath = Path.join(ancestorDirectory, "node_modules", "react", "package.json");
|
|
33882
|
+
if (isFile(reactPackageJsonPath)) {
|
|
33883
|
+
const installedVersion = readPackageJson(reactPackageJsonPath).version;
|
|
33884
|
+
return typeof installedVersion === "string" ? installedVersion : null;
|
|
33885
|
+
}
|
|
33886
|
+
if (ancestorDirectory === containmentRoot) return null;
|
|
33887
|
+
}
|
|
33888
|
+
return null;
|
|
33889
|
+
};
|
|
33795
33890
|
const findInWorkspacePackageJsons = (rootDirectory, rootPackageJson, select) => {
|
|
33796
33891
|
const rootValue = select(rootPackageJson);
|
|
33797
33892
|
if (rootValue !== null) return rootValue;
|
|
@@ -34131,19 +34226,20 @@ const clearProjectCache = () => {
|
|
|
34131
34226
|
};
|
|
34132
34227
|
/**
|
|
34133
34228
|
* Build a `ProjectInfo` for a directory that has no `package.json` of
|
|
34134
|
-
* its own — a
|
|
34135
|
-
* of TypeScript/JavaScript files. Dependency + framework
|
|
34136
|
-
* inherited from the enclosing
|
|
34137
|
-
*
|
|
34138
|
-
*
|
|
34139
|
-
*
|
|
34140
|
-
*
|
|
34229
|
+
* its own — a package subfolder like `repo/packages` or `app/src/features`,
|
|
34230
|
+
* or any loose tree of TypeScript/JavaScript files. Dependency + framework
|
|
34231
|
+
* detection is inherited from the nearest enclosing package (a leaf
|
|
34232
|
+
* workspace, a plain app root, or a monorepo root — whichever is closest,
|
|
34233
|
+
* bounded by the git root), so scanning a subdirectory of a React project
|
|
34234
|
+
* still gets the React capabilities; a standalone non-React directory simply
|
|
34235
|
+
* scans with the framework-agnostic rules. Throws only when the directory has
|
|
34236
|
+
* nothing to scan (no enclosing project and no source files of its own).
|
|
34141
34237
|
*/
|
|
34142
34238
|
const discoverProjectWithoutPackageJson = (directory) => {
|
|
34143
34239
|
const sourceFileCount = countSourceFiles(directory);
|
|
34144
34240
|
const hasOwnTsConfig = NFS.existsSync(Path.join(directory, "tsconfig.json"));
|
|
34145
|
-
const
|
|
34146
|
-
const enclosingProject =
|
|
34241
|
+
const enclosingProjectRoot = findNearestAncestorPackageJson(directory);
|
|
34242
|
+
const enclosingProject = enclosingProjectRoot !== null ? discoverProject(enclosingProjectRoot) : null;
|
|
34147
34243
|
if (enclosingProject !== null) return {
|
|
34148
34244
|
...enclosingProject,
|
|
34149
34245
|
rootDirectory: directory,
|
|
@@ -34247,6 +34343,7 @@ const discoverProject = (directory) => {
|
|
|
34247
34343
|
if (!reactVersion && reactDeclaration.version && !isCatalogReference(reactDeclaration.version)) reactVersion = reactDeclaration.version;
|
|
34248
34344
|
if (!tailwindVersion && tailwindDeclaration.version && !isCatalogReference(tailwindDeclaration.version)) tailwindVersion = tailwindDeclaration.version;
|
|
34249
34345
|
if (!zodVersion && zodDeclaration.version && !isCatalogReference(zodDeclaration.version)) zodVersion = zodDeclaration.version;
|
|
34346
|
+
if (!reactVersion || parseReactMajor(reactVersion) === null) reactVersion = resolveInstalledReactVersion(directory) ?? reactVersion;
|
|
34250
34347
|
const projectName = packageJson.name ?? Path.basename(directory);
|
|
34251
34348
|
const hasTypeScript = NFS.existsSync(Path.join(directory, "tsconfig.json"));
|
|
34252
34349
|
const sourceFileCount = countSourceFiles(directory);
|
|
@@ -36995,10 +37092,17 @@ function* collectSecurityScanFiles(rootDirectory) {
|
|
|
36995
37092
|
}
|
|
36996
37093
|
}
|
|
36997
37094
|
}
|
|
37095
|
+
/**
|
|
37096
|
+
* Whether project discovery resolved a React-compatible runtime (React or
|
|
37097
|
+
* Preact, which ships the same hooks + JSX model). This is the same
|
|
37098
|
+
* predicate that grants the `react` capability, so `false` means every
|
|
37099
|
+
* React-runtime rule family was gated off for the project.
|
|
37100
|
+
*/
|
|
37101
|
+
const hasReactRuntime = (project) => project.reactVersion !== null || project.preactVersion !== null;
|
|
36998
37102
|
const buildCapabilities = (project) => {
|
|
36999
37103
|
const capabilities = /* @__PURE__ */ new Set();
|
|
37000
37104
|
capabilities.add(project.framework);
|
|
37001
|
-
if (project
|
|
37105
|
+
if (hasReactRuntime(project)) capabilities.add("react");
|
|
37002
37106
|
if (project.framework === "expo" || project.framework === "react-native" || project.hasReactNativeWorkspace) capabilities.add("react-native");
|
|
37003
37107
|
if (project.expoVersion !== null) capabilities.add("expo");
|
|
37004
37108
|
if (project.nextjsMajorVersion !== null && project.nextjsMajorVersion >= 15) capabilities.add("nextjs:15");
|
|
@@ -38528,7 +38632,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
38528
38632
|
})),
|
|
38529
38633
|
showStagedContent: (directory, relativePath, options) => runCommand({
|
|
38530
38634
|
command: "git",
|
|
38531
|
-
args: ["show",
|
|
38635
|
+
args: ["show", `:./${relativePath}`],
|
|
38532
38636
|
directory,
|
|
38533
38637
|
maxStdoutBytes: options?.maxBufferBytes
|
|
38534
38638
|
}).pipe(map$3((result) => result.status === 0 ? result.stdout : null)),
|
|
@@ -39834,6 +39938,7 @@ const parseRuleCode = (code) => {
|
|
|
39834
39938
|
};
|
|
39835
39939
|
};
|
|
39836
39940
|
const resolveDiagnosticCategory = (plugin, rule) => getRuleCategory(rule) ?? lookupOwnString(PLUGIN_CATEGORY_MAP, plugin) ?? "Bugs";
|
|
39941
|
+
const resolveMatchByOccurrence = (rule, category) => OCCURRENCE_MATCHED_CATEGORIES.has(category) || Boolean(reactDoctorPlugin.rules[rule]?.matchByOccurrence);
|
|
39837
39942
|
/**
|
|
39838
39943
|
* Maps oxlint's non-primary labels (`labels[1..]`) into related source
|
|
39839
39944
|
* locations. Editors surface these as a diagnostic's
|
|
@@ -39897,6 +40002,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
|
|
|
39897
40002
|
const normalizedFilePath = diagnostic.filename.replaceAll("\\", "/");
|
|
39898
40003
|
const primarySpan = primaryLabel?.span;
|
|
39899
40004
|
const relatedLocations = buildRelatedLocations(diagnostic.labels, normalizedFilePath);
|
|
40005
|
+
const category = resolveDiagnosticCategory(plugin, rule);
|
|
39900
40006
|
return {
|
|
39901
40007
|
filePath: normalizedFilePath,
|
|
39902
40008
|
plugin,
|
|
@@ -39912,7 +40018,8 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
|
|
|
39912
40018
|
offset: primarySpan.offset,
|
|
39913
40019
|
length: primarySpan.length
|
|
39914
40020
|
} : {},
|
|
39915
|
-
category
|
|
40021
|
+
category,
|
|
40022
|
+
...resolveMatchByOccurrence(rule, category) ? { matchByOccurrence: true } : {},
|
|
39916
40023
|
...relatedLocations.length > 0 ? { relatedLocations } : {}
|
|
39917
40024
|
};
|
|
39918
40025
|
});
|
|
@@ -42085,7 +42192,7 @@ const computeConfigFingerprint = (projectDirectory, version) => {
|
|
|
42085
42192
|
/** Display name used in client-facing messages and progress titles. */
|
|
42086
42193
|
const SERVER_DISPLAY_NAME = "React Doctor";
|
|
42087
42194
|
/** Server version reported in `serverInfo`; injected at build, `dev` from source. */
|
|
42088
|
-
const SERVER_VERSION = "0.7.
|
|
42195
|
+
const SERVER_VERSION = "0.7.1";
|
|
42089
42196
|
/** `Diagnostic.source` shown next to every published diagnostic. */
|
|
42090
42197
|
const DIAGNOSTIC_SOURCE = "react-doctor";
|
|
42091
42198
|
/**
|
|
@@ -43792,6 +43899,7 @@ const METRIC = {
|
|
|
43792
43899
|
scanScore: "scan.score",
|
|
43793
43900
|
scanClean: "scan.clean",
|
|
43794
43901
|
scanCheckSkipped: "scan.check_skipped",
|
|
43902
|
+
scanNoReactDetected: "scan.no_react_detected",
|
|
43795
43903
|
baselineDegraded: "baseline.degraded",
|
|
43796
43904
|
ruleFired: "rule.fired",
|
|
43797
43905
|
ruleDisabled: "rule.disabled",
|
|
@@ -44135,5 +44243,5 @@ const startLanguageServer = () => {
|
|
|
44135
44243
|
};
|
|
44136
44244
|
//#endregion
|
|
44137
44245
|
export { startLanguageServer };
|
|
44138
|
-
!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]="
|
|
44139
|
-
//# debugId=
|
|
44246
|
+
!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]="8bc40ab2-982a-5d5d-a65c-9733ec980391")}catch(e){}}();
|
|
44247
|
+
//# debugId=8bc40ab2-982a-5d5d-a65c-9733ec980391
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-doctor",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1-dev.1880b15",
|
|
4
4
|
"description": "Your agent writes bad React. This catches it",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"accessibility",
|
|
@@ -65,8 +65,8 @@
|
|
|
65
65
|
"vscode-languageserver-textdocument": "^1.0.12",
|
|
66
66
|
"vscode-uri": "^3.1.0",
|
|
67
67
|
"yaml": "^2.9.0",
|
|
68
|
-
"deslop-js": "0.7.
|
|
69
|
-
"oxlint-plugin-react-doctor": "0.7.
|
|
68
|
+
"deslop-js": "0.7.1",
|
|
69
|
+
"oxlint-plugin-react-doctor": "0.7.1-dev.1880b15"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@types/babel__code-frame": "^7.27.0",
|
|
@@ -74,9 +74,9 @@
|
|
|
74
74
|
"@xterm/headless": "^6.0.0",
|
|
75
75
|
"commander": "^14.0.3",
|
|
76
76
|
"ora": "^9.4.0",
|
|
77
|
-
"@react-doctor/
|
|
78
|
-
"@react-doctor/core": "0.7.
|
|
79
|
-
"@react-doctor/
|
|
77
|
+
"@react-doctor/language-server": "0.7.1",
|
|
78
|
+
"@react-doctor/core": "0.7.1",
|
|
79
|
+
"@react-doctor/api": "0.7.1"
|
|
80
80
|
},
|
|
81
81
|
"engines": {
|
|
82
82
|
"node": "^20.19.0 || >=22.13.0"
|