Skip to content

Commit 8bb54c9

Browse files
authored
refactor: split src/utils/ into src/core/{config,detection,diagnostics,scoring,runners}/ + src/cli/ (#224)
* refactor: split src/utils/ (68 flat files) into src/core/{config,detection,diagnostics,scoring,runners}/ + src/cli/ Final big organizational refactor in the series (#218 plugin/, #220 cli/, #221 scan/, #222 drop browser-poc, #223 dead-code). Carves the 68-file src/utils/ dumping ground into purpose-built directories. New layout: src/cli/ presentation (26 files) + prompts.ts, highlighter.ts, spinner.ts, logger.ts + colorize-by-score.ts, format-error-chain.ts + indent-multiline-text.ts, wrap-indented-text.ts + build-hidden-diagnostics-summary.ts + should-auto-select-current-choice.ts, should-select-all-choices.ts + detect-agents.ts, to-display-name.ts + (existing cli/ files: index.ts, render-*.ts, etc.) src/core/ core logic (7 files at root + 5 subdirs) build-json-report.ts shared JSON reporting (CLI + public API) build-json-report-error.ts shared JSON error envelope group-by.ts, is-file.ts generic primitives is-plain-object.ts, to-relative-path.ts read-file-lines-node.ts config/ (11) config loading, root-dir resolution, ignore patterns, glob matching, gitattributes parsing, validation detection/ (6) project discovery, react/tailwind major-version parsing, package.json reading, monorepo root detection diagnostics/ (12) combine/filter/suppress/merge, jsx-opener-span lookup, disable-directive walks scoring/ (6) local + remote score calc, score breakdown, reduced-motion check, proxy fetch runners/ (13) oxlint + knip + their config builders, include-path computation, Node-version compat, diff-files src/plugin/ (unchanged from #218) scan.ts, index.ts, errors.ts, types.ts, constants.ts, oxlint-config.ts, eslint-plugin.ts, install-skill.ts top-level Driven by a one-shot codemod that performed the moves via `git mv` (so all 68 file renames preserve blame/log history) and rewrote every relative import in the package + tests (98 importer files touched) to the new paths. One non-mechanical fix needed after the move: src/core/runners/run-oxlint.ts resolved its built plugin via `../../dist/react-doctor-plugin.js` -- with 1 extra level of nesting that needs to be `../../../dist/react-doctor-plugin.js`. One test fixed: tests/regressions/scan-resilience.test.ts read a source file by hardcoded path (`src/utils/calculate-score-locally.ts`) to assert no `fetch(` reference -- updated to the new location (`src/core/scoring/calculate-score-locally.ts`). Verification: - 128 files changed, +237 / -222 (almost entirely import-path edits) - typecheck/lint/format clean - 734/734 tests pass - build produces identical dist/cli.js, dist/index.js, etc. - No public API change - src/utils/ no longer exists * refactor: move orchestrators into tiers + rename scan -> inspect (#225) * refactor: move install-skill, scan, oxlint-config out of src/ root into their tiers Top-level src/ was still holding three orchestrator-grade files that logically belong inside the existing tier directories. Move them so the src/ root only contains public-API entry points and cross-cutting types. Moves (via git mv, history preserved): src/install-skill.ts -> src/cli/install-skill.ts CLI subcommand (already imported 6 things from cli/) src/scan.ts -> src/core/scan.ts Orchestrator (lives with the logic it calls) src/oxlint-config.ts -> src/core/runners/oxlint-config.ts Sibling of run-oxlint.ts (config + runner) After: top-level src/ contains only the entry points and shared types: src/ ├── cli/ presentation ├── core/ logic ├── plugin/ rules ├── constants.ts ├── errors.ts ├── eslint-plugin.ts public-API entry ├── index.ts public-API entry ├── knip.d.ts type declaration └── types.ts Imports rewritten via a small codemod (11 files): - 3 moved files' internal imports recomputed for new depth - 4 src/ consumers of the moved files updated - 4 test files updated Verification: - 11 files changed, +39/-39 (all import path edits) - typecheck/lint/format clean - 734/734 tests pass - build produces identical dist/* output - run-oxlint.ts's relative path to dist/react-doctor-plugin.js (../../../) is unchanged -- run-oxlint.ts itself didn't move Stacks on top of #224 (utils->core reorg). Once #224 merges, this PR's base auto-rebases to main. * refactor: rename scan -> inspect (function, file, types, test) "scan" was a misnomer -- the function does much more than scan files: it runs lint via oxlint, runs dead-code via knip, calculates a score, merges/filters diagnostics, and renders CLI output. "inspect" describes the role accurately and matches v2's chosen name. Renames: src/core/scan.ts -> src/core/inspect.ts (git mv) tests/scan.test.ts -> tests/inspect.test.ts (git mv) scan() function -> inspect() function runScan() -> runInspect() mergeScanOptions() -> mergeInspectOptions() ResolvedScanOptions -> ResolvedInspectOptions ScanResult -> InspectResult (src/types.ts) ScanOptions -> InspectOptions (src/types.ts) describe("scan", ...) -> describe("inspect", ...) (test) Importers updated: src/cli/index.ts 4 sites src/core/build-json-report.ts 3 sites tests/inspect.test.ts 5 sites tests/regressions/cli-and-output.test.ts 4 sites tests/build-json-report.test.ts 2 sites Kept as English prose (not identifiers): - "full scan", "scan output", "scan only files", "scan banner" in comments, CLI option descriptions, and prompt text -- these describe the action of scanning the codebase, not references to the function. - constants.ts JSX_OPENER_SCAN_MAX_LINES (about lexical JSX scanning, unrelated to inspect()). Verification: - 7 files changed, +54/-54 (all renames) - typecheck/lint/format clean - 734/734 tests pass - No public API change (scan/ScanResult/ScanOptions weren't exported from src/index.ts -- the public API still calls the underlying function diagnose() which is unchanged) Stacks on #225 (which moved scan.ts to src/core/). Once #225 merges, this PR's base auto-rebases to main. * fix: restore help-text example paths corrupted by the import codemod The utils->core import-path codemod's regex matched `from '...'` substrings inside string literals -- specifically the EXAMPLE code shown to users in oxlint diagnostic help text. Three messages in HELP_TEXT_MAP had their example paths mangled into the codemod's target form (`../../utils/...`) even though they were supposed to illustrate paths in the USER'S code, not paths internal to this package. Restored to the original example paths: no-barrel-import: `import { Button } from '../../utils/components/Button.js'` -> `import { Button } from './components/Button'` no-dynamic-import-path: `import('../../utils/feature/heavy.js')` -> `import('./feature/heavy.js')` nextjs-no-css-link: `import styles from '../../utils/Button.module.css.js'` -> `import styles from './Button.module.css'` Verified no other string literals were corrupted (grepped for `["'`]../../(utils|core|cli)/` across src/ -- all remaining hits are legitimate plugin/utils imports from the per-rule split in #218, which is its own unrelated `utils/` directory). Closes the Bugbot finding on PR #224. * refactor: move format-error-chain / logger / highlighter to core/ (fix layering) Addresses Bugbot's "Core modules depend backwards on CLI layer" finding on PR #224. Several core/ files were importing from cli/, undermining the layered structure the refactor was introducing: core/build-json-report-error.ts -> cli/format-error-chain core/runners/extract-failed-plugin-name -> cli/format-error-chain core/config/load-config -> cli/logger core/config/read-ignore-file -> cli/logger core/config/resolve-config-root-dir -> cli/logger The 3 utilities in question are all pure abstractions (no CLI-specific behavior beyond using console.* for output), so they belong in core/ where both core AND cli consumers can import them without inversion. Moves (git mv, history preserved): cli/format-error-chain.ts -> core/format-error-chain.ts (Error.cause walker) cli/logger.ts -> core/logger.ts (silent-mode logger) cli/highlighter.ts -> core/highlighter.ts (picocolors wrapper) 22 files changed, +29/-29 -- all relative-import path rewrites driven by a small codemod with a strict `^import ... from "..."` regex (line-anchored to avoid the help-text-literal corruption bug from the previous codemod, which triggered the bigger Bugbot follow-up). After this: - core/ no longer imports from cli/ for utilities -- only core/inspect.ts still does (legitimately: it's the CLI orchestrator that calls render-*.ts + resolve-oxlint-node prompt + spinner) - cli/ files import the moved utilities via `../core/<name>.js` Verification: - typecheck/lint/format clean - 734/734 tests pass - `rg "from .+cli/" src/core` shows only inspect.ts's legitimate imports of render-*, spinner, resolve-oxlint-node (all of which are CLI presentation/interaction, not utilities)
1 parent ef2b7fe commit 8bb54c9

131 files changed

Lines changed: 302 additions & 287 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/react-doctor/src/utils/build-hidden-diagnostics-summary.ts renamed to packages/react-doctor/src/cli/build-hidden-diagnostics-summary.ts

File renamed without changes.

packages/react-doctor/src/utils/colorize-by-score.ts renamed to packages/react-doctor/src/cli/colorize-by-score.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { SCORE_GOOD_THRESHOLD, SCORE_OK_THRESHOLD } from "../constants.js";
2-
import { highlighter } from "./highlighter.js";
2+
import { highlighter } from "../core/highlighter.js";
33

44
export const colorizeByScore = (text: string, score: number): string => {
55
if (score >= SCORE_GOOD_THRESHOLD) return highlighter.success(text);
File renamed without changes.

packages/react-doctor/src/cli/find-owning-project.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import path from "node:path";
2-
import { discoverReactSubprojects, listWorkspacePackages } from "../utils/discover-project.js";
2+
import {
3+
discoverReactSubprojects,
4+
listWorkspacePackages,
5+
} from "../core/detection/discover-project.js";
36

47
export const findOwningProjectDirectory = (rootDirectory: string, filePath: string): string => {
58
const absoluteFile = path.isAbsolute(filePath) ? filePath : path.resolve(rootDirectory, filePath);

packages/react-doctor/src/cli/handle-error.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { CANONICAL_GITHUB_URL } from "../constants.js";
22
import type { HandleErrorOptions } from "../types.js";
3-
import { formatErrorChain } from "../utils/format-error-chain.js";
4-
import { logger } from "../utils/logger.js";
3+
import { formatErrorChain } from "../core/format-error-chain.js";
4+
import { logger } from "../core/logger.js";
55

66
const DEFAULT_HANDLE_ERROR_OPTIONS: HandleErrorOptions = {
77
shouldExit: true,

packages/react-doctor/src/utils/indent-multiline-text.ts renamed to packages/react-doctor/src/cli/indent-multiline-text.ts

File renamed without changes.

packages/react-doctor/src/cli/index.ts

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,27 @@ import path from "node:path";
44
import { performance } from "node:perf_hooks";
55
import { Command } from "commander";
66
import { CANONICAL_GITHUB_URL } from "../constants.js";
7-
import { runInstallSkill } from "../install-skill.js";
8-
import { scan } from "../scan.js";
7+
import { runInstallSkill } from "./install-skill.js";
8+
import { inspect } from "../core/inspect.js";
99
import type {
1010
Diagnostic,
1111
DiffInfo,
1212
FailOnLevel,
1313
JsonReport,
1414
JsonReportMode,
1515
ReactDoctorConfig,
16-
ScanOptions,
17-
ScanResult,
16+
InspectOptions,
17+
InspectResult,
1818
} from "../types.js";
19-
import { buildJsonReport } from "../utils/build-json-report.js";
20-
import { buildJsonReportError } from "../utils/build-json-report-error.js";
21-
import { filterSourceFiles, getDiffInfo } from "../utils/get-diff-files.js";
22-
import { highlighter } from "../utils/highlighter.js";
23-
import { loadConfigWithSource } from "../utils/load-config.js";
24-
import { resolveConfigRootDir } from "../utils/resolve-config-root-dir.js";
25-
import { logger, setLoggerSilent } from "../utils/logger.js";
26-
import { prompts } from "../utils/prompts.js";
27-
import { toRelativePath } from "../utils/to-relative-path.js";
19+
import { buildJsonReport } from "../core/build-json-report.js";
20+
import { buildJsonReportError } from "../core/build-json-report-error.js";
21+
import { filterSourceFiles, getDiffInfo } from "../core/runners/get-diff-files.js";
22+
import { highlighter } from "../core/highlighter.js";
23+
import { loadConfigWithSource } from "../core/config/load-config.js";
24+
import { resolveConfigRootDir } from "../core/config/resolve-config-root-dir.js";
25+
import { logger, setLoggerSilent } from "../core/logger.js";
26+
import { prompts } from "./prompts.js";
27+
import { toRelativePath } from "../core/to-relative-path.js";
2828
import { encodeAnnotationProperty, encodeAnnotationMessage } from "./annotation-encoding.js";
2929
import { findOwningProjectDirectory } from "./find-owning-project.js";
3030
import { getStagedSourceFiles, materializeStagedFiles } from "./get-staged-files.js";
@@ -161,11 +161,11 @@ const isCiEnvironment = (): boolean =>
161161
CI_ENVIRONMENT_VARIABLES.some((envVariable) => Boolean(process.env[envVariable])) ||
162162
process.env.CI === "true";
163163

164-
const resolveCliScanOptions = (
164+
const resolveCliInspectOptions = (
165165
flags: CliFlags,
166166
userConfig: ReactDoctorConfig | null,
167167
programInstance: Command,
168-
): ScanOptions => {
168+
): InspectOptions => {
169169
const isCliOverride = (optionName: string) =>
170170
programInstance.getOptionValueSource(optionName) === "cli";
171171

@@ -264,7 +264,7 @@ const resolveDiffMode = async (
264264
interface ExplainContext {
265265
resolvedDirectory: string;
266266
userConfig: ReactDoctorConfig | null;
267-
scanOptions: ScanOptions;
267+
scanOptions: InspectOptions;
268268
projectFlag: string | undefined;
269269
}
270270

@@ -275,7 +275,7 @@ const runExplain = async (fileLineArgument: string, context: ExplainContext): Pr
275275
const { filePath, line } = parseFileLineArgument(fileLineArgument);
276276
const targetDirectory = await resolveExplainTargetDirectory(filePath, context);
277277

278-
const scanResult = await scan(targetDirectory, {
278+
const scanResult = await inspect(targetDirectory, {
279279
...context.scanOptions,
280280
silent: true,
281281
offline: true,
@@ -449,7 +449,7 @@ const program = new Command()
449449
await runExplain(explainArgument, {
450450
resolvedDirectory,
451451
userConfig,
452-
scanOptions: resolveCliScanOptions(flags, userConfig, program),
452+
scanOptions: resolveCliInspectOptions(flags, userConfig, program),
453453
projectFlag: flags.project,
454454
});
455455
return;
@@ -460,7 +460,7 @@ const program = new Command()
460460
logger.break();
461461
}
462462

463-
const scanOptions = resolveCliScanOptions(flags, userConfig, program);
463+
const scanOptions = resolveCliInspectOptions(flags, userConfig, program);
464464
const shouldSkipPrompts =
465465
flags.yes ||
466466
flags.full ||
@@ -506,7 +506,7 @@ const program = new Command()
506506
const snapshot = materializeStagedFiles(resolvedDirectory, stagedFiles, tempDirectory);
507507
cleanupSnapshot = snapshot.cleanup;
508508

509-
const scanResult = await scan(snapshot.tempDirectory, {
509+
const scanResult = await inspect(snapshot.tempDirectory, {
510510
...scanOptions,
511511
includePaths: snapshot.stagedFiles,
512512
configOverride: userConfig,
@@ -520,7 +520,7 @@ const program = new Command()
520520
}));
521521

522522
if (isJsonMode) {
523-
const remappedScanResult: ScanResult = {
523+
const remappedInspectResult: InspectResult = {
524524
...scanResult,
525525
diagnostics: remappedDiagnostics,
526526
project: {
@@ -534,7 +534,7 @@ const program = new Command()
534534
directory: resolvedDirectory,
535535
mode: "staged",
536536
diff: null,
537-
scans: [{ directory: resolvedDirectory, result: remappedScanResult }],
537+
scans: [{ directory: resolvedDirectory, result: remappedInspectResult }],
538538
totalElapsedMilliseconds: performance.now() - jsonStartTime,
539539
}),
540540
);
@@ -594,7 +594,7 @@ const program = new Command()
594594
}
595595

596596
const allDiagnostics: Diagnostic[] = [];
597-
const completedScans: Array<{ directory: string; result: ScanResult }> = [];
597+
const completedScans: Array<{ directory: string; result: InspectResult }> = [];
598598

599599
for (const projectDirectory of projectDirectories) {
600600
let includePaths: string[] | undefined;
@@ -625,7 +625,7 @@ const program = new Command()
625625
logger.dim(`Scanning ${projectDirectory}...`);
626626
logger.break();
627627
}
628-
const scanResult = await scan(projectDirectory, {
628+
const scanResult = await inspect(projectDirectory, {
629629
...scanOptions,
630630
includePaths,
631631
configOverride: userConfig,

packages/react-doctor/src/install-skill.ts renamed to packages/react-doctor/src/cli/install-skill.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import { existsSync } from "node:fs";
22
import path from "node:path";
33
import { fileURLToPath } from "node:url";
44
import { installSkillsFromSource, SKILL_MANIFEST_FILE, type SkillAgentType } from "agent-install";
5-
import { SKILL_NAME } from "./constants.js";
6-
import { detectAvailableAgents } from "./utils/detect-agents.js";
7-
import { highlighter } from "./utils/highlighter.js";
8-
import { logger } from "./utils/logger.js";
9-
import { prompts } from "./utils/prompts.js";
10-
import { spinner } from "./utils/spinner.js";
11-
import { toDisplayName } from "./utils/to-display-name.js";
5+
import { SKILL_NAME } from "../constants.js";
6+
import { detectAvailableAgents } from "./detect-agents.js";
7+
import { highlighter } from "../core/highlighter.js";
8+
import { logger } from "../core/logger.js";
9+
import { prompts } from "./prompts.js";
10+
import { spinner } from "./spinner.js";
11+
import { toDisplayName } from "./to-display-name.js";
1212

1313
interface InstallSkillOptions {
1414
yes?: boolean;

packages/react-doctor/src/utils/prompts.ts renamed to packages/react-doctor/src/cli/prompts.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createRequire } from "node:module";
22
import basePrompts, { type PromptObject, type Answers } from "prompts";
33
import type { PromptMultiselectContext } from "../types.js";
4-
import { logger } from "./logger.js";
4+
import { logger } from "../core/logger.js";
55
import { shouldAutoSelectCurrentChoice } from "./should-auto-select-current-choice.js";
66
import { shouldSelectAllChoices } from "./should-select-all-choices.js";
77

packages/react-doctor/src/cli/render-diagnostics.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import {
66
RULE_NAME_COLUMN_WIDTH_CHARS,
77
} from "../constants.js";
88
import type { Diagnostic } from "../types.js";
9-
import { buildHiddenDiagnosticsSummary } from "../utils/build-hidden-diagnostics-summary.js";
10-
import { groupBy } from "../utils/group-by.js";
11-
import { highlighter } from "../utils/highlighter.js";
12-
import { indentMultilineText } from "../utils/indent-multiline-text.js";
13-
import { logger } from "../utils/logger.js";
14-
import { toRelativePath } from "../utils/to-relative-path.js";
15-
import { wrapIndentedText } from "../utils/wrap-indented-text.js";
9+
import { buildHiddenDiagnosticsSummary } from "./build-hidden-diagnostics-summary.js";
10+
import { groupBy } from "../core/group-by.js";
11+
import { highlighter } from "../core/highlighter.js";
12+
import { indentMultilineText } from "./indent-multiline-text.js";
13+
import { logger } from "../core/logger.js";
14+
import { toRelativePath } from "../core/to-relative-path.js";
15+
import { wrapIndentedText } from "./wrap-indented-text.js";
1616

1717
const SEVERITY_ORDER: Record<Diagnostic["severity"], number> = {
1818
error: 0,

0 commit comments

Comments
 (0)