Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 35 additions & 81 deletions packages/react-doctor/src/cli/commands/inspect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,22 @@ import * as path from "node:path";
import { performance } from "node:perf_hooks";
import * as Effect from "effect/Effect";
import * as fs from "node:fs";
import { mergeReactDoctorConfigs } from "../../core/core-configuration.js";
import type { ReactDoctorConfig } from "../../core/core-configuration.js";
import { highlighter } from "../../core/core-presentation.js";
import { toRelativePath } from "../../core/core-primitives.js";
import { hasReactRuntime, resolveScanTarget } from "../../core/core-project-discovery.js";
import { buildJsonReport } from "../../core/core-reporting.js";
import type { JsonReportMode } from "../../core/core-reporting.js";
import { DEFAULT_PROJECT_SCAN_CONCURRENCY, mapWithConcurrency } from "../../core/core-runtime.js";
import type { DiffInfo, InspectResult } from "../../core/core-types.js";
import {
buildJsonReport,
DEFAULT_PROJECT_SCAN_CONCURRENCY,
getBaselineDiffPlan,
getChangedLineRanges,
getDiffInfo,
hasReactRuntime,
highlighter,
mapWithConcurrency,
mergeReactDoctorConfigs,
resolveScanTarget,
toRelativePath,
} from "@react-doctor/core";
import { inspect } from "../../inspect.js";
} from "../../core/core-version-control.js";
import { createInvocationInspect } from "../../inspect.js";
import { flushSentry } from "../../instrument.js";
import type {
DiffInfo,
InspectResult,
JsonReportMode,
ReactDoctorConfig,
} from "@react-doctor/core";
import type { RequestedScope } from "../utils/resolve-scope.js";
import { cliLogger as logger } from "../utils/cli-logger.js";
import { METRIC, STAGED_FILES_TEMP_DIR_PREFIX } from "../utils/constants.js";
Expand Down Expand Up @@ -64,13 +59,8 @@ import type { CliInspectOptions } from "../utils/resolve-cli-inspect-options.js"
import { finalizeScope, resolveScope, warnDeprecatedDiff } from "../utils/resolve-scope.js";
import { resolveMergeBaseRef } from "../utils/materialize-baseline-files.js";
import { resolveBlockingLevel } from "../utils/resolve-blocking-level.js";
import {
resolveProjectChangedLineRanges,
resolveProjectDiffIncludePaths,
} from "../utils/resolve-project-diff-include-paths.js";
import { resolveProjectSourceFilePaths } from "../utils/resolve-project-source-file-paths.js";
import { resolveProjectChangedLineRanges } from "../utils/resolve-project-diff-include-paths.js";
import { runExplain } from "../utils/run-explain.js";
import { projectManifestChanged } from "../utils/project-manifest-changed.js";
import { filterScansForSurface } from "../utils/filter-scans-for-surface.js";
import { selectProjects } from "../utils/select-projects.js";
import { resolveProjectRelativeDirectory } from "../utils/resolve-project-relative-directory.js";
Expand All @@ -83,6 +73,7 @@ import { validateIncludeUntrackedScope, validateModeFlags } from "../utils/valid
import { VERSION } from "../utils/version.js";
import { findStagedSnapshotDivergences } from "../utils/find-staged-snapshot-divergences.js";
import { CliInputError } from "../utils/cli-input-error.js";
import { buildProjectScanPlan } from "../utils/build-project-scan-plan.js";

interface CompletedScan {
directory: string;
Expand Down Expand Up @@ -359,6 +350,7 @@ export const inspectAction = async (
}

const scanOptions: CliInspectOptions = resolveCliInspectOptions(flags, userConfig);
const inspectProject = createInvocationInspect(scanOptions.concurrency);
// One `--max-duration` budget per invocation, shared by every project of a
// workspace scan: fix the absolute deadline once here and hand it to each
// project's `inspect()` (rather than restarting the budget per project).
Expand Down Expand Up @@ -425,7 +417,7 @@ export const inspectAction = async (
logger.break();
}
try {
const scanResult = await inspect(snapshot.tempDirectory, {
const scanResult = await inspectProject(snapshot.tempDirectory, {
...scanOptions,
deadlineEpochMs: scanDeadlineEpochMs,
includePaths: snapshot.stagedFiles,
Expand Down Expand Up @@ -641,67 +633,29 @@ export const inspectAction = async (
// diff change shouldn't pull a project into the scan (nothing to report).
const supplyChainEnabled = flags.supplyChain ?? projectConfig?.supplyChain?.enabled !== false;

let includePaths: string[] | undefined;
let supplyChainManifestChanged = false;
const projectBaselineBaseFiles =
baselineDiffPlan === null
? null
: resolveProjectSourceFilePaths(
resolvedDirectory,
scanDirectory,
baselineDiffPlan.baseFiles,
);
const projectBaselineHeadFiles =
baselineDiffPlan === null
? null
: resolveProjectSourceFilePaths(
resolvedDirectory,
scanDirectory,
baselineDiffPlan.headFiles,
);
if (isDiffMode) {
const changedSourceFiles =
diffInfo === null
? []
: resolveProjectDiffIncludePaths(resolvedDirectory, scanDirectory, diffInfo);
// A PR that edits this project's package.json should still have its
// dependencies scored, even with no changed source files — dependency
// health is a manifest property, not a per-file one.
supplyChainManifestChanged =
supplyChainEnabled &&
diffInfo !== null &&
projectManifestChanged(resolvedDirectory, scanDirectory, diffInfo);
const hasBaselineOnlyFiles = (projectBaselineBaseFiles?.length ?? 0) > 0;
if (
changedSourceFiles.length === 0 &&
!supplyChainManifestChanged &&
!hasBaselineOnlyFiles
) {
if (!isQuiet) {
logger.dim(`No changed source files in ${scanDirectory}, skipping.`);
logger.break();
}
return null;
}
// A changed package.json enters the scan as an include so the run
// stays in diff mode (lint ignores it — it's not a source file) while
// the supply-chain pass runs. Including it also makes the baseline pass
// materialize the base manifest, so the delta filters out pre-existing
// low-score dependencies instead of reporting them as newly introduced.
includePaths = [...changedSourceFiles];
if (includePaths.length === 0 && hasBaselineOnlyFiles) {
includePaths.push(...(projectBaselineBaseFiles ?? []));
const projectScanPlan = buildProjectScanPlan({
rootDirectory: resolvedDirectory,
projectDirectory: scanDirectory,
baselineDiffPlan,
diffInfo,
isDiffMode,
supplyChainEnabled,
});
if (projectScanPlan.shouldSkipProject) {
if (!isQuiet) {
logger.dim(`No changed source files in ${scanDirectory}, skipping.`);
logger.break();
}
if (supplyChainManifestChanged) includePaths.push("package.json");
return null;
}

if (!isQuiet && !isMultiProject) {
logger.dim(" ");
}
const scanResult = await inspect(scanDirectory, {
const scanResult = await inspectProject(scanDirectory, {
...scanOptions,
deadlineEpochMs: scanDeadlineEpochMs,
includePaths,
includePaths: projectScanPlan.includePaths,
configOverride: projectConfig,
configSourceDirectory: projectConfigSourceDirectory ?? undefined,
suppressRendering: isMultiProject,
Expand All @@ -710,19 +664,19 @@ export const inspectAction = async (
concurrentScan: isMultiProject,
baseline:
baselineRef !== null &&
projectBaselineBaseFiles !== null &&
projectBaselineHeadFiles !== null
projectScanPlan.projectBaselineBaseFiles !== null &&
projectScanPlan.projectBaselineHeadFiles !== null
? {
ref: baselineRef,
baseFiles: projectBaselineBaseFiles,
headFiles: projectBaselineHeadFiles,
baseFiles: projectScanPlan.projectBaselineBaseFiles,
headFiles: projectScanPlan.projectBaselineHeadFiles,
}
: undefined,
changedLineRanges:
scope === "lines" && changedLineRanges !== null
? resolveProjectChangedLineRanges(resolvedDirectory, scanDirectory, changedLineRanges)
: undefined,
supplyChainManifestChanged,
supplyChainManifestChanged: projectScanPlan.supplyChainManifestChanged,
});
if (!isQuiet && !isMultiProject) {
logger.break();
Expand Down
39 changes: 27 additions & 12 deletions packages/react-doctor/src/cli/ink/run-scan-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,25 @@ import path from "node:path";
import { performance } from "node:perf_hooks";
import { render } from "ink";
import * as Effect from "effect/Effect";
import { mergeReactDoctorConfigs } from "../../core/core-configuration.js";
import type { ReactDoctorConfig } from "../../core/core-configuration.js";
import { highlighter } from "../../core/core-presentation.js";
import { resolveScanTarget } from "../../core/core-project-discovery.js";
import {
DEFAULT_PROJECT_SCAN_CONCURRENCY,
highlighter,
mapWithConcurrency,
mergeReactDoctorConfigs,
Reporter,
resolveScanTarget,
} from "@react-doctor/core";
} from "../../core/core-runtime.js";
import type {
BlockingLevel,
Diagnostic,
InspectResult,
ReactDoctorConfig,
ResolvedScanTarget,
ScoreResult,
WorkspacePackage,
} from "@react-doctor/core";
import { inspect } from "../../inspect.js";
import type { ReactDoctorInspectOptions } from "../../inspect.js";
} from "../../core/core-types.js";
import { createInvocationInspect } from "../../inspect.js";
import type { ReactDoctorInspectOptions } from "../../inspect-options.js";
import { buildNoScoreMessage } from "../utils/build-no-score-message.js";
import { computeProjectedScore } from "../utils/compute-score-projection.js";
import { countUniqueScannedFiles } from "../utils/count-unique-scanned-files.js";
Expand Down Expand Up @@ -379,11 +379,12 @@ const runSingleProjectScan = async (
projectDirectory: string,
input: RunScanAppInput,
blockingLevel: BlockingLevel,
inspectProject: ReturnType<typeof createInvocationInspect>,
): Promise<RunScanAppResult> => {
const projectScan = await resolveProjectScan(rootScanTarget, projectDirectory);
const presentation = resolveScanPresentation(input, [projectScan]);
return runMountedScan(projectScan.directory, presentation, blockingLevel, async (context) => {
const result = await inspect(projectScan.directory, {
const result = await inspectProject(projectScan.directory, {
...resolveTuiInspectOptions(input, projectScan.config),
isCi: isCiEnvironment(),
configOverride: projectScan.config,
Expand Down Expand Up @@ -421,6 +422,7 @@ const runMultiProjectScan = async (
directories: ReadonlyArray<string>,
input: RunScanAppInput,
blockingLevel: BlockingLevel,
inspectProject: ReturnType<typeof createInvocationInspect>,
): Promise<RunScanAppResult> => {
const rootDirectory = rootScanTarget.resolvedDirectory;
const projectScans = await mapWithConcurrency(
Expand All @@ -437,7 +439,7 @@ const runMultiProjectScan = async (
projectScans,
DEFAULT_PROJECT_SCAN_CONCURRENCY,
async (projectScan) => {
const result = await inspect(projectScan.directory, {
const result = await inspectProject(projectScan.directory, {
...resolveTuiInspectOptions(input, projectScan.config),
isCi: isCiEnvironment(),
configOverride: projectScan.config,
Expand Down Expand Up @@ -514,6 +516,7 @@ export const runScanApp = async (input: RunScanAppInput): Promise<RunScanAppResu
share: input.share ?? scanTarget.userConfig?.share ?? true,
};
const selectedDirectories = await resolveSelectedDirectories(rootDirectory, resolvedInput);
const inspectProject = createInvocationInspect(input.options?.concurrency);
const blockingLevel = resolveBlockingLevel(
{ blocking: resolvedInput.blocking },
scanTarget.userConfig,
Expand All @@ -523,7 +526,19 @@ export const runScanApp = async (input: RunScanAppInput): Promise<RunScanAppResu
return { shouldFail: false };
}
if (selectedDirectories.length === 1) {
return runSingleProjectScan(scanTarget, selectedDirectories[0], resolvedInput, blockingLevel);
return runSingleProjectScan(
scanTarget,
selectedDirectories[0],
resolvedInput,
blockingLevel,
inspectProject,
);
}
return runMultiProjectScan(scanTarget, selectedDirectories, resolvedInput, blockingLevel);
return runMultiProjectScan(
scanTarget,
selectedDirectories,
resolvedInput,
blockingLevel,
inspectProject,
);
};
6 changes: 5 additions & 1 deletion packages/react-doctor/src/cli/utils/anonymize-text.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { isPlainObject, redactSensitiveText, scrubSensitivePaths } from "@react-doctor/core";
import {
isPlainObject,
redactSensitiveText,
scrubSensitivePaths,
} from "../../core/core-primitives.js";

/**
* Free-text fields can carry both a home-directory path (the OS username) and a
Expand Down
57 changes: 57 additions & 0 deletions packages/react-doctor/src/cli/utils/build-inspect-result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { Diagnostic, InspectResult, ProjectInfo, ScoreResult } from "../../core/core-types.js";

export interface BuildInspectResultInput {
readonly diagnostics: ReadonlyArray<Diagnostic>;
readonly score: ScoreResult | null;
readonly skippedChecks: string[];
readonly skippedCheckReasons: Record<string, string>;
readonly project: ProjectInfo;
readonly elapsedMilliseconds: number;
readonly scannedFileCount: number;
readonly scannedFilePaths: ReadonlyArray<string>;
readonly analyzedFiles: ReadonlyArray<string>;
readonly scanElapsedMilliseconds: number;
readonly lintCacheHitFileCount: number | null;
readonly lintCacheTotalFileCount: number | null;
readonly lintSidecarReplayedFileCount: number | null;
readonly lintSidecarTotalFileCount: number | null;
readonly deadCodeCacheHit: boolean | null;
readonly deadCodeSummaryCacheHits: number | null;
readonly deadCodeSummaryCacheMisses: number | null;
readonly baselineDelta: InspectResult["baselineDelta"];
}

export const buildInspectResult = (input: BuildInspectResultInput): InspectResult => ({
diagnostics: [...input.diagnostics],
score: input.score,
skippedChecks: input.skippedChecks,
...(Object.keys(input.skippedCheckReasons).length > 0
? { skippedCheckReasons: input.skippedCheckReasons }
: {}),
project: input.project,
elapsedMilliseconds: input.elapsedMilliseconds,
scannedFileCount: input.scannedFileCount,
scannedFilePaths: input.scannedFilePaths,
analyzedFiles: input.analyzedFiles,
scanElapsedMilliseconds: input.scanElapsedMilliseconds,
...(input.lintCacheTotalFileCount !== null
? {
lintCacheHitFileCount: input.lintCacheHitFileCount,
lintCacheTotalFileCount: input.lintCacheTotalFileCount,
}
: {}),
...(input.lintSidecarTotalFileCount !== null
? {
lintSidecarReplayedFileCount: input.lintSidecarReplayedFileCount,
lintSidecarTotalFileCount: input.lintSidecarTotalFileCount,
}
: {}),
...(input.deadCodeCacheHit !== null ? { deadCodeCacheHit: input.deadCodeCacheHit } : {}),
...(input.deadCodeSummaryCacheHits !== null && input.deadCodeSummaryCacheMisses !== null
? {
deadCodeSummaryCacheHits: input.deadCodeSummaryCacheHits,
deadCodeSummaryCacheMisses: input.deadCodeSummaryCacheMisses,
}
: {}),
...(input.baselineDelta ? { baselineDelta: input.baselineDelta } : {}),
});
Loading
Loading