react-doctor 0.7.1-dev.ce9dabf → 0.7.1-dev.d353dad
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 +138 -37
- package/dist/index.js +99 -16
- package/dist/lsp.js +99 -16
- package/package.json +4 -4
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";
|
|
@@ -36262,6 +36262,23 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
|
|
|
36262
36262
|
const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
|
|
36263
36263
|
const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
|
|
36264
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
|
+
};
|
|
36265
36282
|
const cachedPackageJsons = /* @__PURE__ */ new Map();
|
|
36266
36283
|
const readPackageJsonUncached = (packageJsonPath) => {
|
|
36267
36284
|
try {
|
|
@@ -36292,11 +36309,7 @@ const isMonorepoRoot = (directory) => {
|
|
|
36292
36309
|
return Array.isArray(packageJson.workspaces) || Boolean(packageJson.workspaces?.packages);
|
|
36293
36310
|
};
|
|
36294
36311
|
const findMonorepoRoot = (startDirectory) => {
|
|
36295
|
-
|
|
36296
|
-
while (currentDirectory !== Path.dirname(currentDirectory)) {
|
|
36297
|
-
if (isMonorepoRoot(currentDirectory)) return currentDirectory;
|
|
36298
|
-
currentDirectory = Path.dirname(currentDirectory);
|
|
36299
|
-
}
|
|
36312
|
+
for (const directory of ancestorDirectories(startDirectory, { includeStart: false })) if (isMonorepoRoot(directory)) return directory;
|
|
36300
36313
|
return null;
|
|
36301
36314
|
};
|
|
36302
36315
|
/**
|
|
@@ -36942,6 +36955,74 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
|
|
|
36942
36955
|
framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
|
|
36943
36956
|
};
|
|
36944
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
|
+
};
|
|
36945
37026
|
const findInWorkspacePackageJsons = (rootDirectory, rootPackageJson, select) => {
|
|
36946
37027
|
const rootValue = select(rootPackageJson);
|
|
36947
37028
|
if (rootValue !== null) return rootValue;
|
|
@@ -37278,19 +37359,20 @@ const discoverReactSubprojects = (rootDirectory) => {
|
|
|
37278
37359
|
const cachedProjectInfos = /* @__PURE__ */ new Map();
|
|
37279
37360
|
/**
|
|
37280
37361
|
* Build a `ProjectInfo` for a directory that has no `package.json` of
|
|
37281
|
-
* its own — a
|
|
37282
|
-
* of TypeScript/JavaScript files. Dependency + framework
|
|
37283
|
-
* inherited from the enclosing
|
|
37284
|
-
*
|
|
37285
|
-
*
|
|
37286
|
-
*
|
|
37287
|
-
*
|
|
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).
|
|
37288
37370
|
*/
|
|
37289
37371
|
const discoverProjectWithoutPackageJson = (directory) => {
|
|
37290
37372
|
const sourceFileCount = countSourceFiles(directory);
|
|
37291
37373
|
const hasOwnTsConfig = NFS.existsSync(Path.join(directory, "tsconfig.json"));
|
|
37292
|
-
const
|
|
37293
|
-
const enclosingProject =
|
|
37374
|
+
const enclosingProjectRoot = findNearestAncestorPackageJson(directory);
|
|
37375
|
+
const enclosingProject = enclosingProjectRoot !== null ? discoverProject(enclosingProjectRoot) : null;
|
|
37294
37376
|
if (enclosingProject !== null) return {
|
|
37295
37377
|
...enclosingProject,
|
|
37296
37378
|
rootDirectory: directory,
|
|
@@ -37394,6 +37476,7 @@ const discoverProject = (directory) => {
|
|
|
37394
37476
|
if (!reactVersion && reactDeclaration.version && !isCatalogReference(reactDeclaration.version)) reactVersion = reactDeclaration.version;
|
|
37395
37477
|
if (!tailwindVersion && tailwindDeclaration.version && !isCatalogReference(tailwindDeclaration.version)) tailwindVersion = tailwindDeclaration.version;
|
|
37396
37478
|
if (!zodVersion && zodDeclaration.version && !isCatalogReference(zodDeclaration.version)) zodVersion = zodDeclaration.version;
|
|
37479
|
+
if (!reactVersion || parseReactMajor(reactVersion) === null) reactVersion = resolveInstalledReactVersion(directory) ?? reactVersion;
|
|
37397
37480
|
const projectName = packageJson.name ?? Path.basename(directory);
|
|
37398
37481
|
const hasTypeScript = NFS.existsSync(Path.join(directory, "tsconfig.json"));
|
|
37399
37482
|
const sourceFileCount = countSourceFiles(directory);
|
|
@@ -45924,7 +46007,7 @@ const makeNoopConsole = () => ({
|
|
|
45924
46007
|
});
|
|
45925
46008
|
//#endregion
|
|
45926
46009
|
//#region src/cli/utils/version.ts
|
|
45927
|
-
const VERSION = "0.7.1-dev.
|
|
46010
|
+
const VERSION = "0.7.1-dev.d353dad";
|
|
45928
46011
|
//#endregion
|
|
45929
46012
|
//#region src/cli/utils/json-mode.ts
|
|
45930
46013
|
let context = null;
|
|
@@ -46293,13 +46376,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
|
|
|
46293
46376
|
* uploads source-map artifacts under, so stack frames symbolicate. Honors the
|
|
46294
46377
|
* standard `SENTRY_RELEASE` override.
|
|
46295
46378
|
*/
|
|
46296
|
-
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.1-dev.
|
|
46379
|
+
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.1-dev.d353dad`;
|
|
46297
46380
|
/**
|
|
46298
46381
|
* Deployment environment shown in Sentry's environment filter. Defaults to
|
|
46299
46382
|
* `production` for tagged releases and `development` for dev/unbuilt versions,
|
|
46300
46383
|
* overridable via the standard `SENTRY_ENVIRONMENT` env var.
|
|
46301
46384
|
*/
|
|
46302
|
-
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.1-dev.
|
|
46385
|
+
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.1-dev.d353dad") ? "development" : "production");
|
|
46303
46386
|
/**
|
|
46304
46387
|
* Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
|
|
46305
46388
|
* (set to `0` to disable tracing) and falls back to
|
|
@@ -58143,7 +58226,7 @@ const buildRuntimeLayers = (input) => {
|
|
|
58143
58226
|
const linterLayer = input.shouldSkipLint ? Linter.layerOf([]) : Linter.layerOxlint;
|
|
58144
58227
|
const deadCodeLayer = input.shouldRunDeadCode ? DeadCode.layerNode : DeadCode.layerOf([]);
|
|
58145
58228
|
const scoreLayer = input.shouldComputeScore ? Score.layerHttp : Score.layerOf(null);
|
|
58146
|
-
const supplyChainLayer = input.
|
|
58229
|
+
const supplyChainLayer = input.shouldRunSupplyChain ? SupplyChain.layerNode : SupplyChain.layerOf([]);
|
|
58147
58230
|
const projectLayer = input.projectInfoOverride === void 0 ? Project.layerNode : Project.layerOf(input.projectInfoOverride);
|
|
58148
58231
|
const progressLayer = input.shouldShowProgressSpinners ? Progress.layerOra(buildSpinnerProgressHandle) : Progress.layerNoop;
|
|
58149
58232
|
const baseLayers = mergeAll$1(projectLayer, input.hasConfigOverride ? Config.layerOf({
|
|
@@ -58499,21 +58582,22 @@ const resolveDotenvFingerprint = (projectDirectory) => {
|
|
|
58499
58582
|
return [];
|
|
58500
58583
|
}
|
|
58501
58584
|
};
|
|
58502
|
-
const
|
|
58585
|
+
const isGitIdentityTrustworthy = (projectDirectory) => {
|
|
58503
58586
|
const output = runGit(projectDirectory, ["ls-files", "-v"]);
|
|
58504
|
-
if (output === null) return
|
|
58505
|
-
|
|
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");
|
|
58506
58590
|
};
|
|
58507
58591
|
const resolveCacheFilePath = (projectDirectory) => path.join(resolveReactDoctorCacheDir(projectDirectory), SCAN_RESULT_CACHE_FILENAME);
|
|
58508
58592
|
const readPersistedCache = (cacheFilePath) => {
|
|
58509
58593
|
try {
|
|
58510
58594
|
const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
|
|
58511
|
-
if (!isRecord(parsed) || parsed.version !==
|
|
58512
|
-
version:
|
|
58595
|
+
if (!isRecord(parsed) || parsed.version !== 4) return {
|
|
58596
|
+
version: 4,
|
|
58513
58597
|
entries: []
|
|
58514
58598
|
};
|
|
58515
58599
|
if (!Array.isArray(parsed.entries)) return {
|
|
58516
|
-
version:
|
|
58600
|
+
version: 4,
|
|
58517
58601
|
entries: []
|
|
58518
58602
|
};
|
|
58519
58603
|
const entries = [];
|
|
@@ -58523,12 +58607,12 @@ const readPersistedCache = (cacheFilePath) => {
|
|
|
58523
58607
|
entries.push(entry);
|
|
58524
58608
|
}
|
|
58525
58609
|
return {
|
|
58526
|
-
version:
|
|
58610
|
+
version: 4,
|
|
58527
58611
|
entries
|
|
58528
58612
|
};
|
|
58529
58613
|
} catch {
|
|
58530
58614
|
return {
|
|
58531
|
-
version:
|
|
58615
|
+
version: 4,
|
|
58532
58616
|
entries: []
|
|
58533
58617
|
};
|
|
58534
58618
|
}
|
|
@@ -58560,6 +58644,7 @@ const resolveProjectIdentity = (projectDirectory) => {
|
|
|
58560
58644
|
return path.resolve(projectDirectory);
|
|
58561
58645
|
}
|
|
58562
58646
|
};
|
|
58647
|
+
const resolveManifestContentHash = (projectDirectory) => hashFileContents(getPackageJsonPath(projectDirectory));
|
|
58563
58648
|
const fileFingerprint = (filePath) => {
|
|
58564
58649
|
try {
|
|
58565
58650
|
const stat = fs.statSync(filePath);
|
|
@@ -58582,14 +58667,14 @@ const resolveToolchainFingerprint = (nodeBinaryPath) => {
|
|
|
58582
58667
|
};
|
|
58583
58668
|
const buildScanResultCacheKey = (input) => {
|
|
58584
58669
|
if (isCacheGloballyDisabled()) return null;
|
|
58585
|
-
if (
|
|
58670
|
+
if (!isGitIdentityTrustworthy(input.projectDirectory)) return null;
|
|
58586
58671
|
const worktreeFingerprint = buildWorktreeFingerprint(input.projectDirectory);
|
|
58587
58672
|
if (worktreeFingerprint === null) return null;
|
|
58588
58673
|
const headSha = readHeadSha(input.projectDirectory);
|
|
58589
58674
|
if (headSha === null) return null;
|
|
58590
58675
|
if (stringifyStableJson(input.userConfig) === null) return null;
|
|
58591
58676
|
const cacheKeyJson = stringifyStableJson({
|
|
58592
|
-
schemaVersion:
|
|
58677
|
+
schemaVersion: 4,
|
|
58593
58678
|
projectIdentity: resolveProjectIdentity(input.projectDirectory),
|
|
58594
58679
|
headSha,
|
|
58595
58680
|
worktreeFingerprint,
|
|
@@ -58604,6 +58689,7 @@ const buildScanResultCacheKey = (input) => {
|
|
|
58604
58689
|
engineOptions: {
|
|
58605
58690
|
lint: input.options.lint,
|
|
58606
58691
|
deadCode: input.options.deadCode,
|
|
58692
|
+
supplyChain: input.options.supplyChain,
|
|
58607
58693
|
includePaths: [...input.options.includePaths].sort(),
|
|
58608
58694
|
customRulesOnly: input.options.customRulesOnly,
|
|
58609
58695
|
respectInlineDisables: input.options.respectInlineDisables,
|
|
@@ -58629,17 +58715,24 @@ const createScanResultCache = (projectDirectory) => {
|
|
|
58629
58715
|
for (const entry of persistedCache.entries) entries.set(entry.key, entry);
|
|
58630
58716
|
const persist = () => {
|
|
58631
58717
|
writePersistedCache(cacheFilePath, {
|
|
58632
|
-
version:
|
|
58718
|
+
version: 4,
|
|
58633
58719
|
entries: [...entries.values()].sort((firstEntry, secondEntry) => secondEntry.createdAtMs - firstEntry.createdAtMs).slice(0, 20)
|
|
58634
58720
|
});
|
|
58635
58721
|
};
|
|
58636
58722
|
return {
|
|
58637
|
-
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
|
+
},
|
|
58638
58728
|
store: (key, payload) => {
|
|
58639
58729
|
entries.set(key, {
|
|
58640
58730
|
key,
|
|
58641
58731
|
createdAtMs: Date.now(),
|
|
58642
|
-
payload
|
|
58732
|
+
payload: {
|
|
58733
|
+
...payload,
|
|
58734
|
+
manifestContentHash: resolveManifestContentHash(projectDirectory)
|
|
58735
|
+
}
|
|
58643
58736
|
});
|
|
58644
58737
|
persist();
|
|
58645
58738
|
}
|
|
@@ -58904,6 +58997,7 @@ const buildScanAttributes = (input) => {
|
|
|
58904
58997
|
maxDurationMs: input.maxDurationMs,
|
|
58905
58998
|
lint: input.lint,
|
|
58906
58999
|
deadCode: input.deadCode,
|
|
59000
|
+
supplyChain: input.supplyChain,
|
|
58907
59001
|
scoreOnly: input.scoreOnly,
|
|
58908
59002
|
noScore: input.noScore,
|
|
58909
59003
|
respectInlineDisables: input.respectInlineDisables,
|
|
@@ -60178,6 +60272,7 @@ const buildIgnoredTags = (userConfig) => {
|
|
|
60178
60272
|
const mergeInspectOptions = (inputOptions, userConfig) => ({
|
|
60179
60273
|
lint: inputOptions.lint ?? userConfig?.lint ?? true,
|
|
60180
60274
|
deadCode: inputOptions.deadCode ?? userConfig?.deadCode ?? true,
|
|
60275
|
+
supplyChain: inputOptions.supplyChain ?? userConfig?.supplyChain?.enabled ?? true,
|
|
60181
60276
|
verbose: inputOptions.verbose ?? userConfig?.verbose ?? false,
|
|
60182
60277
|
outputDirectory: inputOptions.outputDirectory || null,
|
|
60183
60278
|
scoreOnly: inputOptions.scoreOnly ?? false,
|
|
@@ -60217,6 +60312,7 @@ const buildRunEventConfig = (options, userConfig, hasCustomConfig, resolvedWorke
|
|
|
60217
60312
|
maxDurationMs: options.maxDurationMs,
|
|
60218
60313
|
lint: options.lint,
|
|
60219
60314
|
deadCode: options.deadCode,
|
|
60315
|
+
supplyChain: options.supplyChain,
|
|
60220
60316
|
scoreOnly: options.scoreOnly,
|
|
60221
60317
|
noScore: options.noScore,
|
|
60222
60318
|
respectInlineDisables: options.respectInlineDisables,
|
|
@@ -60300,6 +60396,7 @@ const runBaselineComparison = async (params) => {
|
|
|
60300
60396
|
projectInfoOverride: params.headProjectInfo,
|
|
60301
60397
|
shouldSkipLint: !params.options.lint || !params.resolvedNodeBinaryPath,
|
|
60302
60398
|
shouldRunDeadCode: false,
|
|
60399
|
+
shouldRunSupplyChain: params.options.supplyChain,
|
|
60303
60400
|
shouldComputeScore: false,
|
|
60304
60401
|
shouldShowProgressSpinners: false,
|
|
60305
60402
|
oxlintConcurrency: params.options.concurrency
|
|
@@ -60388,6 +60485,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
|
|
|
60388
60485
|
configSourceDirectory,
|
|
60389
60486
|
shouldSkipLint: !options.lint || lintBindingMissing,
|
|
60390
60487
|
shouldRunDeadCode: options.deadCode,
|
|
60488
|
+
shouldRunSupplyChain: options.supplyChain,
|
|
60391
60489
|
shouldComputeScore: !options.noScore,
|
|
60392
60490
|
shouldShowProgressSpinners,
|
|
60393
60491
|
oxlintConcurrency: options.concurrency
|
|
@@ -62673,8 +62771,8 @@ const resolveParallelFlag = (parallel) => parallel === false ? 1 : void 0;
|
|
|
62673
62771
|
* accepts. Flag-derived fields only (`scoreOnly`, `noScore`, `silent`,
|
|
62674
62772
|
* `isCi`): every `userConfig` fallback — including `noScore` — lives in
|
|
62675
62773
|
* `inspect()`'s merge layer, which sees each project's module-merged
|
|
62676
|
-
* config. The plain boolean knobs (`lint`, `deadCode`, `
|
|
62677
|
-
* 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
|
|
62678
62776
|
* `userConfig` only to decide the `--blocking warning` gate.
|
|
62679
62777
|
*/
|
|
62680
62778
|
const resolveCliInspectOptions = (flags, userConfig) => {
|
|
@@ -62682,6 +62780,7 @@ const resolveCliInspectOptions = (flags, userConfig) => {
|
|
|
62682
62780
|
return {
|
|
62683
62781
|
lint: flags.lint,
|
|
62684
62782
|
deadCode: flags.deadCode,
|
|
62783
|
+
supplyChain: flags.supplyChain,
|
|
62685
62784
|
verbose: flags.verbose,
|
|
62686
62785
|
outputDirectory: flags.outputDir,
|
|
62687
62786
|
respectInlineDisables: flags.respectInlineDisables === false ? false : void 0,
|
|
@@ -63232,7 +63331,7 @@ const inspectAction = async (directory, flags) => {
|
|
|
63232
63331
|
const scanDirectory = projectScanTarget.resolvedDirectory;
|
|
63233
63332
|
const projectConfig = projectDirectory === resolvedDirectory ? userConfig : mergeReactDoctorConfigs(userConfig, projectScanTarget.userConfig ?? void 0);
|
|
63234
63333
|
const projectConfigSourceDirectory = projectScanTarget.userConfig?.plugins === void 0 ? scanTarget.configSourceDirectory : projectScanTarget.configSourceDirectory;
|
|
63235
|
-
const supplyChainEnabled = projectConfig?.supplyChain?.enabled !== false;
|
|
63334
|
+
const supplyChainEnabled = flags.supplyChain ?? projectConfig?.supplyChain?.enabled !== false;
|
|
63236
63335
|
let includePaths;
|
|
63237
63336
|
let supplyChainManifestChanged = false;
|
|
63238
63337
|
if (isDiffMode) {
|
|
@@ -64152,10 +64251,12 @@ const ROOT_FLAG_SPEC = {
|
|
|
64152
64251
|
"--no-parallel",
|
|
64153
64252
|
"--no-respect-inline-disables",
|
|
64154
64253
|
"--no-score",
|
|
64254
|
+
"--no-supply-chain",
|
|
64155
64255
|
"--no-telemetry",
|
|
64156
64256
|
"--no-warnings",
|
|
64157
64257
|
"--score",
|
|
64158
64258
|
"--staged",
|
|
64259
|
+
"--supply-chain",
|
|
64159
64260
|
"--verbose",
|
|
64160
64261
|
"--version",
|
|
64161
64262
|
"--warnings",
|
|
@@ -64404,7 +64505,7 @@ ${highlighter.dim("Learn more:")}
|
|
|
64404
64505
|
${highlighter.info(CI_URL)}
|
|
64405
64506
|
`;
|
|
64406
64507
|
const collectCategoryOption = (value, previousValues) => [...previousValues ?? [], value];
|
|
64407
|
-
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);
|
|
64408
64509
|
program.action(inspectAction);
|
|
64409
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));
|
|
64410
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);
|
|
@@ -64454,4 +64555,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
|
|
|
64454
64555
|
export {};
|
|
64455
64556
|
|
|
64456
64557
|
//# sourceMappingURL=cli.js.map
|
|
64457
|
-
//# debugId=
|
|
64558
|
+
//# debugId=47338856-c658-5c78-a5e2-e871c2dafaa7
|
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";
|
|
@@ -33064,6 +33064,23 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
|
|
|
33064
33064
|
const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
|
|
33065
33065
|
const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
|
|
33066
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
|
+
};
|
|
33067
33084
|
const cachedPackageJsons = /* @__PURE__ */ new Map();
|
|
33068
33085
|
const clearPackageJsonCache = () => {
|
|
33069
33086
|
cachedPackageJsons.clear();
|
|
@@ -33097,11 +33114,7 @@ const isMonorepoRoot = (directory) => {
|
|
|
33097
33114
|
return Array.isArray(packageJson.workspaces) || Boolean(packageJson.workspaces?.packages);
|
|
33098
33115
|
};
|
|
33099
33116
|
const findMonorepoRoot = (startDirectory) => {
|
|
33100
|
-
|
|
33101
|
-
while (currentDirectory !== Path.dirname(currentDirectory)) {
|
|
33102
|
-
if (isMonorepoRoot(currentDirectory)) return currentDirectory;
|
|
33103
|
-
currentDirectory = Path.dirname(currentDirectory);
|
|
33104
|
-
}
|
|
33117
|
+
for (const directory of ancestorDirectories(startDirectory, { includeStart: false })) if (isMonorepoRoot(directory)) return directory;
|
|
33105
33118
|
return null;
|
|
33106
33119
|
};
|
|
33107
33120
|
/**
|
|
@@ -33747,6 +33760,74 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
|
|
|
33747
33760
|
framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
|
|
33748
33761
|
};
|
|
33749
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
|
+
};
|
|
33750
33831
|
const findInWorkspacePackageJsons = (rootDirectory, rootPackageJson, select) => {
|
|
33751
33832
|
const rootValue = select(rootPackageJson);
|
|
33752
33833
|
if (rootValue !== null) return rootValue;
|
|
@@ -34086,19 +34167,20 @@ const clearProjectCache = () => {
|
|
|
34086
34167
|
};
|
|
34087
34168
|
/**
|
|
34088
34169
|
* Build a `ProjectInfo` for a directory that has no `package.json` of
|
|
34089
|
-
* its own — a
|
|
34090
|
-
* of TypeScript/JavaScript files. Dependency + framework
|
|
34091
|
-
* inherited from the enclosing
|
|
34092
|
-
*
|
|
34093
|
-
*
|
|
34094
|
-
*
|
|
34095
|
-
*
|
|
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).
|
|
34096
34178
|
*/
|
|
34097
34179
|
const discoverProjectWithoutPackageJson = (directory) => {
|
|
34098
34180
|
const sourceFileCount = countSourceFiles(directory);
|
|
34099
34181
|
const hasOwnTsConfig = NFS.existsSync(Path.join(directory, "tsconfig.json"));
|
|
34100
|
-
const
|
|
34101
|
-
const enclosingProject =
|
|
34182
|
+
const enclosingProjectRoot = findNearestAncestorPackageJson(directory);
|
|
34183
|
+
const enclosingProject = enclosingProjectRoot !== null ? discoverProject(enclosingProjectRoot) : null;
|
|
34102
34184
|
if (enclosingProject !== null) return {
|
|
34103
34185
|
...enclosingProject,
|
|
34104
34186
|
rootDirectory: directory,
|
|
@@ -34202,6 +34284,7 @@ const discoverProject = (directory) => {
|
|
|
34202
34284
|
if (!reactVersion && reactDeclaration.version && !isCatalogReference(reactDeclaration.version)) reactVersion = reactDeclaration.version;
|
|
34203
34285
|
if (!tailwindVersion && tailwindDeclaration.version && !isCatalogReference(tailwindDeclaration.version)) tailwindVersion = tailwindDeclaration.version;
|
|
34204
34286
|
if (!zodVersion && zodDeclaration.version && !isCatalogReference(zodDeclaration.version)) zodVersion = zodDeclaration.version;
|
|
34287
|
+
if (!reactVersion || parseReactMajor(reactVersion) === null) reactVersion = resolveInstalledReactVersion(directory) ?? reactVersion;
|
|
34205
34288
|
const projectName = packageJson.name ?? Path.basename(directory);
|
|
34206
34289
|
const hasTypeScript = NFS.existsSync(Path.join(directory, "tsconfig.json"));
|
|
34207
34290
|
const sourceFileCount = countSourceFiles(directory);
|
|
@@ -42441,4 +42524,4 @@ const toJsonReport = (result, options) => buildJsonReport({
|
|
|
42441
42524
|
export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, hasReactRuntime, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
|
|
42442
42525
|
|
|
42443
42526
|
//# sourceMappingURL=index.js.map
|
|
42444
|
-
//# debugId=
|
|
42527
|
+
//# debugId=f00947fe-3794-5d73-a491-232b535dc962
|
package/dist/lsp.js
CHANGED
|
@@ -33123,6 +33123,23 @@ const listSourceFilesViaFilesystem = (rootDirectory) => {
|
|
|
33123
33123
|
const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
|
|
33124
33124
|
const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
|
|
33125
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
|
+
};
|
|
33126
33143
|
const cachedPackageJsons = /* @__PURE__ */ new Map();
|
|
33127
33144
|
const clearPackageJsonCache = () => {
|
|
33128
33145
|
cachedPackageJsons.clear();
|
|
@@ -33156,11 +33173,7 @@ const isMonorepoRoot = (directory) => {
|
|
|
33156
33173
|
return Array.isArray(packageJson.workspaces) || Boolean(packageJson.workspaces?.packages);
|
|
33157
33174
|
};
|
|
33158
33175
|
const findMonorepoRoot = (startDirectory) => {
|
|
33159
|
-
|
|
33160
|
-
while (currentDirectory !== Path.dirname(currentDirectory)) {
|
|
33161
|
-
if (isMonorepoRoot(currentDirectory)) return currentDirectory;
|
|
33162
|
-
currentDirectory = Path.dirname(currentDirectory);
|
|
33163
|
-
}
|
|
33176
|
+
for (const directory of ancestorDirectories(startDirectory, { includeStart: false })) if (isMonorepoRoot(directory)) return directory;
|
|
33164
33177
|
return null;
|
|
33165
33178
|
};
|
|
33166
33179
|
/**
|
|
@@ -33806,6 +33819,74 @@ const findDependencyInfoFromMonorepoRoot = (directory) => {
|
|
|
33806
33819
|
framework: rootInfo.framework !== "unknown" ? rootInfo.framework : workspaceInfo.framework
|
|
33807
33820
|
};
|
|
33808
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
|
+
};
|
|
33809
33890
|
const findInWorkspacePackageJsons = (rootDirectory, rootPackageJson, select) => {
|
|
33810
33891
|
const rootValue = select(rootPackageJson);
|
|
33811
33892
|
if (rootValue !== null) return rootValue;
|
|
@@ -34145,19 +34226,20 @@ const clearProjectCache = () => {
|
|
|
34145
34226
|
};
|
|
34146
34227
|
/**
|
|
34147
34228
|
* Build a `ProjectInfo` for a directory that has no `package.json` of
|
|
34148
|
-
* its own — a
|
|
34149
|
-
* of TypeScript/JavaScript files. Dependency + framework
|
|
34150
|
-
* inherited from the enclosing
|
|
34151
|
-
*
|
|
34152
|
-
*
|
|
34153
|
-
*
|
|
34154
|
-
*
|
|
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).
|
|
34155
34237
|
*/
|
|
34156
34238
|
const discoverProjectWithoutPackageJson = (directory) => {
|
|
34157
34239
|
const sourceFileCount = countSourceFiles(directory);
|
|
34158
34240
|
const hasOwnTsConfig = NFS.existsSync(Path.join(directory, "tsconfig.json"));
|
|
34159
|
-
const
|
|
34160
|
-
const enclosingProject =
|
|
34241
|
+
const enclosingProjectRoot = findNearestAncestorPackageJson(directory);
|
|
34242
|
+
const enclosingProject = enclosingProjectRoot !== null ? discoverProject(enclosingProjectRoot) : null;
|
|
34161
34243
|
if (enclosingProject !== null) return {
|
|
34162
34244
|
...enclosingProject,
|
|
34163
34245
|
rootDirectory: directory,
|
|
@@ -34261,6 +34343,7 @@ const discoverProject = (directory) => {
|
|
|
34261
34343
|
if (!reactVersion && reactDeclaration.version && !isCatalogReference(reactDeclaration.version)) reactVersion = reactDeclaration.version;
|
|
34262
34344
|
if (!tailwindVersion && tailwindDeclaration.version && !isCatalogReference(tailwindDeclaration.version)) tailwindVersion = tailwindDeclaration.version;
|
|
34263
34345
|
if (!zodVersion && zodDeclaration.version && !isCatalogReference(zodDeclaration.version)) zodVersion = zodDeclaration.version;
|
|
34346
|
+
if (!reactVersion || parseReactMajor(reactVersion) === null) reactVersion = resolveInstalledReactVersion(directory) ?? reactVersion;
|
|
34264
34347
|
const projectName = packageJson.name ?? Path.basename(directory);
|
|
34265
34348
|
const hasTypeScript = NFS.existsSync(Path.join(directory, "tsconfig.json"));
|
|
34266
34349
|
const sourceFileCount = countSourceFiles(directory);
|
|
@@ -44160,5 +44243,5 @@ const startLanguageServer = () => {
|
|
|
44160
44243
|
};
|
|
44161
44244
|
//#endregion
|
|
44162
44245
|
export { startLanguageServer };
|
|
44163
|
-
!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]="
|
|
44164
|
-
//# 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.1-dev.
|
|
3
|
+
"version": "0.7.1-dev.d353dad",
|
|
4
4
|
"description": "Your agent writes bad React. This catches it",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"accessibility",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"vscode-uri": "^3.1.0",
|
|
67
67
|
"yaml": "^2.9.0",
|
|
68
68
|
"deslop-js": "0.7.1",
|
|
69
|
-
"oxlint-plugin-react-doctor": "0.7.1-dev.
|
|
69
|
+
"oxlint-plugin-react-doctor": "0.7.1-dev.d353dad"
|
|
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/
|
|
77
|
+
"@react-doctor/core": "0.7.1",
|
|
78
78
|
"@react-doctor/api": "0.7.1",
|
|
79
|
-
"@react-doctor/
|
|
79
|
+
"@react-doctor/language-server": "0.7.1"
|
|
80
80
|
},
|
|
81
81
|
"engines": {
|
|
82
82
|
"node": "^20.19.0 || >=22.13.0"
|