mirror of
https://github.com/millionco/react-doctor.git
synced 2026-09-14 20:00:24 +08:00
aa61efae28
* refactor: collapse @react-doctor/types and @react-doctor/project-info into core Eliminates two private workspace packages whose only role was to namespace shared TS types and project-discovery helpers — neither was a real consumer boundary, and the indirection cost a `workspace:*` edge per leaf package without saving any code. **Layout** - `packages/types/` → `packages/core/src/types/` (10 files, pure TS types) - `packages/project-info/` → `packages/core/src/project-info/` (27 files, discoverProject + framework detection + monorepo helpers) - `packages/types/src/react-native-dependency-names.ts` → `packages/oxlint-plugin-react-doctor/src/react-native-dependency-names.ts` (the canonical RN-aware-package list lives next to its heaviest consumer — the rule gate — and is re-exported from core to avoid the rule-plugin ↔ core dependency cycle the previous types package was specifically designed to break). The two packages are deleted from the workspace; every consumer (`react-doctor`, `api`, `oxlint-plugin-react-doctor`, `actions/review`, +130 source/test files) now imports from `@react-doctor/core` directly. **Errors cleanup** Dropped the legacy plain-class `ReactDoctorError` and `isReactDoctorError` from `project-info`. The four narrow errors (`ProjectNotFoundError`, `NoReactDependencyError`, `PackageJsonNotFoundError`, `AmbiguousProjectError`) now extend `Error` directly — the only valid hierarchy is the tagged `Schema.TaggedErrorClass` `ReactDoctorError` from `core/errors.ts`, which has been the canonical one since the Effect v4 refactor. Added `isProjectDiscoveryError` as a narrow type guard for the four leaves. The discovery errors are thrown synchronously BEFORE the Effect runtime takes over (at `diagnose()` / CLI entry points); inside the runtime, the Project service's `translateProjectInfoError` converts each one to its tagged-reason equivalent. **Schema vs type name collisions** `schemas.ts` exports `Diagnostic` / `JsonReport` / `JsonReportMode` / … as Schema.Class values, while `types/` exports the same names as TS types. The core barrel re-exports the TS-type versions (broad-strokes API) and exposes the Schema classes via a new `@react-doctor/core/schemas` subpath export for the two callers that need them (`Reporter` service, smoke validator, schema tests). **Validation** - `pnpm typecheck` ✓ - `pnpm lint` ✓ - `pnpm format` ✓ - `pnpm test` ✓ (1449 passing, 3 skipped — no regressions) - `pnpm smoke:json-report` ✓ Co-authored-by: Cursor <cursoragent@cursor.com> * fix(core): keep RN-dependency constants out of types barrel to avoid pulling oxlint plugin into discovery Bugbot caught (medium severity): re-exporting `isReactNativeDependencyName` from `oxlint-plugin-react-doctor` via the core types barrel meant every consumer of `@react-doctor/core` (including `discoverProject` / `discoverReactSubprojects`) would load the entire 286-rule plugin at module-init. Adds a tiny standalone leaf at `core/src/project-info/internal-rn-dependency-names.ts` (zero imports, just the constants the workspace-discovery gate needs) and switches `is-package-json-react-native-aware.ts` to use it. The types barrel no longer re-exports the plugin-side constants — rule consumers import them directly from `oxlint-plugin-react-doctor`. The duplication is intentional and documented at both files; the two lists are short enough that a future regression test in the plugin can observe the union. Validated: typecheck + test (1449 pass) + lint + format all green. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
73 lines
3.1 KiB
TypeScript
73 lines
3.1 KiB
TypeScript
import { describe, expect, it } from "vite-plus/test";
|
|
import { parseReactMajor } from "@react-doctor/core";
|
|
|
|
describe("parseReactMajor", () => {
|
|
it("extracts the major from caret/tilde/exact ranges", () => {
|
|
expect(parseReactMajor("^19.0.0")).toBe(19);
|
|
expect(parseReactMajor("~18.3.1")).toBe(18);
|
|
expect(parseReactMajor("17.0.2")).toBe(17);
|
|
expect(parseReactMajor("19")).toBe(19);
|
|
expect(parseReactMajor("19.x")).toBe(19);
|
|
expect(parseReactMajor("v19.0.0")).toBe(19);
|
|
});
|
|
|
|
it("uses the lower bound on multi-comparator ranges", () => {
|
|
expect(parseReactMajor(">=18 <20")).toBe(18);
|
|
expect(parseReactMajor(">=18.3.1 <19")).toBe(18);
|
|
expect(parseReactMajor("18 || 19")).toBe(18);
|
|
expect(parseReactMajor("^18.0.0 || ^19.0.0")).toBe(18);
|
|
expect(parseReactMajor(">=17.0.0 <20.0.0")).toBe(17);
|
|
});
|
|
|
|
it("returns null for upper-bound-only ranges", () => {
|
|
expect(parseReactMajor("<19")).toBeNull();
|
|
expect(parseReactMajor("<=18")).toBeNull();
|
|
expect(parseReactMajor("< 19")).toBeNull();
|
|
expect(parseReactMajor("<20 || >=17")).toBeNull();
|
|
});
|
|
|
|
it("returns null for tags, unresolvable protocols, and missing/empty input", () => {
|
|
expect(parseReactMajor(null)).toBeNull();
|
|
expect(parseReactMajor(undefined)).toBeNull();
|
|
expect(parseReactMajor("")).toBeNull();
|
|
expect(parseReactMajor(" ")).toBeNull();
|
|
expect(parseReactMajor("latest")).toBeNull();
|
|
expect(parseReactMajor("next")).toBeNull();
|
|
expect(parseReactMajor("workspace:*")).toBeNull();
|
|
expect(parseReactMajor("github:facebook/react#1234567890")).toBeNull();
|
|
expect(parseReactMajor("file:../react-19-local.tgz")).toBeNull();
|
|
expect(parseReactMajor("npm:@scope/react19-fork@latest")).toBeNull();
|
|
expect(parseReactMajor("*")).toBeNull();
|
|
});
|
|
|
|
it("ignores leading whitespace and prefixes", () => {
|
|
expect(parseReactMajor(" ^19.0.0 ")).toBe(19);
|
|
expect(parseReactMajor("npm:react@^19")).toBe(19);
|
|
expect(parseReactMajor("npm:@vendor/react@~18.2.0")).toBe(18);
|
|
expect(parseReactMajor(" v18.3.1 ")).toBe(18);
|
|
});
|
|
|
|
it("returns null for non-lower-bound comparators", () => {
|
|
expect(parseReactMajor(">18")).toBeNull();
|
|
expect(parseReactMajor(">18 <20")).toBeNull();
|
|
expect(parseReactMajor("!=19")).toBeNull();
|
|
});
|
|
|
|
it("returns null for React experimental / canary builds (0.0.0-...)", () => {
|
|
// React ships experimental and canary builds as `0.0.0-...` so
|
|
// the dependency graph stays semver-safe. The first-integer scan
|
|
// would land on `0` and misclassify them as older than any supported
|
|
// React major; we reject 0 → null so callers can treat them as
|
|
// unknown.
|
|
expect(parseReactMajor("0.0.0-experimental-abc123")).toBeNull();
|
|
expect(parseReactMajor("0.0.0-canary-1a2b3c4d-20251230")).toBeNull();
|
|
expect(parseReactMajor("^0.0.0-experimental")).toBeNull();
|
|
});
|
|
|
|
it("still reads pre-release tags on real majors", () => {
|
|
expect(parseReactMajor("19.0.0-rc.1")).toBe(19);
|
|
expect(parseReactMajor("19.0.0-canary-abc123-20251230")).toBe(19);
|
|
expect(parseReactMajor("^19.0.0-rc.1")).toBe(19);
|
|
});
|
|
});
|