Skip to content

Commit 1bcdf56

Browse files
committed
refactor(core): normalize workspace package graph
1 parent ce55017 commit 1bcdf56

44 files changed

Lines changed: 2899 additions & 539 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"oxlint-plugin-react-doctor": patch
3+
---
4+
5+
Add a lightweight contracts entry for shared framework and capability metadata.

packages/core/src/check-reduced-motion.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as fs from "node:fs";
22
import * as path from "node:path";
3-
import { MOTION_LIBRARY_PACKAGES } from "oxlint-plugin-react-doctor";
3+
import { MOTION_LIBRARY_PACKAGES } from "oxlint-plugin-react-doctor/contracts";
44
import ts from "typescript";
55
import type { Diagnostic } from "./types/index.js";
66
import { getTypescriptScriptKind } from "./utils/get-typescript-script-kind.js";

packages/core/src/check-security-scan.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type { Diagnostic, ProjectInfo } from "./types/index.js";
99
import { isPathGitIgnored } from "./utils/is-path-git-ignored.js";
1010
import { shouldEnableRuleByDefaultStatus } from "./utils/should-enable-rule-by-default-status.js";
1111
import { yieldToEventLoop } from "./utils/yield-to-event-loop.js";
12-
import type { Capability } from "oxlint-plugin-react-doctor";
12+
import type { Capability } from "oxlint-plugin-react-doctor/contracts";
1313

1414
export interface CheckSecurityScanOptions {
1515
readonly project?: ProjectInfo;
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import * as path from "node:path";
2+
import type { Capability } from "oxlint-plugin-react-doctor/contracts";
3+
import { LATEST_SUPPORTED_MOBX_MAJOR } from "../constants.js";
4+
import type { ProjectInfo } from "../types/index.js";
5+
import { getCapabilities } from "./capabilities.js";
6+
import {
7+
MOBX_REACT_LITE_PACKAGE_NAME,
8+
MOBX_REACT_OBSERVER_PACKAGE_NAME,
9+
MOBX_REACT_PACKAGE_NAME,
10+
MOBX_STATE_TREE_PACKAGE_NAME,
11+
REACT_ROUTER_DEPENDENCY_NAMES,
12+
REACT_THREE_FIBER_DEPENDENCY_NAMES,
13+
REACT_THREE_FIBER_ECOSYSTEM_DEPENDENCY_NAMES,
14+
TANSTACK_REACT_QUERY_PACKAGE_NAMES,
15+
} from "./capability-dependency-names.js";
16+
import { detectPreES2023Target } from "./detect-pre-es2023-target.js";
17+
import { REACT_SECTIONS, TAILWIND_ZOD_SECTIONS } from "./dependencies.js";
18+
import {
19+
detectNextjsStaticExport,
20+
detectReactCompiler,
21+
detectReactCompilerLintPlugin,
22+
} from "./detectors.js";
23+
import { findPreferredDependency } from "./find-preferred-dependency.js";
24+
import { isFile } from "./fs-utils.js";
25+
import { hasI18nDependency } from "./has-i18n-dependency.js";
26+
import type {
27+
PackageGraph,
28+
PackageGraphDependencyDeclaration,
29+
PackageGraphPackage,
30+
} from "./package-graph.js";
31+
import { isPackageJsonReactNativeAware, isPackageJsonReanimatedAware } from "./rn-metadata.js";
32+
import { isPackageJsonSsrAware } from "./ssr-metadata.js";
33+
import {
34+
getDependencyMajorWithinSupportedRange,
35+
getLowestDependencyMajor,
36+
parseReactMajor,
37+
parseThreeRelease,
38+
resolveEffectiveReactMajor,
39+
} from "./version.js";
40+
41+
const getDependencyVersion = (
42+
packageGraph: PackageGraph,
43+
packageNode: PackageGraphPackage,
44+
dependencyName: string,
45+
sections?: ReadonlyArray<PackageGraphDependencyDeclaration["section"]>,
46+
): string | null => {
47+
const dependencyDeclaration = packageGraph.getDependency(
48+
packageNode.directory,
49+
dependencyName,
50+
sections,
51+
);
52+
if (dependencyDeclaration === null) return null;
53+
if (dependencyDeclaration.workspaceTargetPackageDirectory === null) {
54+
return dependencyDeclaration.resolvedSpecifier;
55+
}
56+
const workspaceTarget = packageGraph.packages.find(
57+
(candidatePackage) =>
58+
candidatePackage.directory === dependencyDeclaration.workspaceTargetPackageDirectory,
59+
);
60+
return workspaceTarget?.version ?? dependencyDeclaration.resolvedSpecifier;
61+
};
62+
63+
export const buildPackageCapabilities = (
64+
packageGraph: PackageGraph,
65+
packageNode: PackageGraphPackage,
66+
): ReadonlySet<Capability> => {
67+
const reactVersion = getDependencyVersion(packageGraph, packageNode, "react", REACT_SECTIONS);
68+
const tailwindVersion = getDependencyVersion(
69+
packageGraph,
70+
packageNode,
71+
"tailwindcss",
72+
TAILWIND_ZOD_SECTIONS,
73+
);
74+
const zodVersion = getDependencyVersion(packageGraph, packageNode, "zod", TAILWIND_ZOD_SECTIONS);
75+
const mobxVersion = getDependencyVersion(packageGraph, packageNode, "mobx");
76+
const mobxReactVersion = getDependencyVersion(packageGraph, packageNode, MOBX_REACT_PACKAGE_NAME);
77+
const mobxReactLiteVersion = getDependencyVersion(
78+
packageGraph,
79+
packageNode,
80+
MOBX_REACT_LITE_PACKAGE_NAME,
81+
);
82+
const zustandVersion = getDependencyVersion(packageGraph, packageNode, "zustand");
83+
const findPackageDependency = (dependencyNames: ReadonlyArray<string>) =>
84+
findPreferredDependency({
85+
dependencyNames,
86+
getValue: (dependencyName) =>
87+
getDependencyVersion(packageGraph, packageNode, dependencyName, REACT_SECTIONS),
88+
});
89+
const tanstackQuery = findPackageDependency(TANSTACK_REACT_QUERY_PACKAGE_NAMES);
90+
const reactRouter = findPackageDependency(REACT_ROUTER_DEPENDENCY_NAMES);
91+
const reactThreeFiber = findPackageDependency(REACT_THREE_FIBER_DEPENDENCY_NAMES);
92+
const preactVersion = getDependencyVersion(packageGraph, packageNode, "preact", REACT_SECTIONS);
93+
const remotionVersion = getDependencyVersion(packageGraph, packageNode, "remotion");
94+
const threeVersion = getDependencyVersion(packageGraph, packageNode, "three");
95+
const valtioVersion = getDependencyVersion(packageGraph, packageNode, "valtio");
96+
const styledComponentsVersion = getDependencyVersion(
97+
packageGraph,
98+
packageNode,
99+
"styled-components",
100+
REACT_SECTIONS,
101+
);
102+
const hasReactNativePackage =
103+
packageNode.dependencyInfo.framework === "expo" ||
104+
packageNode.dependencyInfo.framework === "react-native" ||
105+
isPackageJsonReactNativeAware(packageNode.manifest);
106+
const expoVersion = hasReactNativePackage
107+
? getDependencyVersion(packageGraph, packageNode, "expo")
108+
: null;
109+
const shopifyFlashListVersion = hasReactNativePackage
110+
? getDependencyVersion(packageGraph, packageNode, "@shopify/flash-list")
111+
: null;
112+
const hasReanimated = hasReactNativePackage && isPackageJsonReanimatedAware(packageNode.manifest);
113+
const reanimatedVersion = hasReanimated
114+
? getDependencyVersion(packageGraph, packageNode, "react-native-reanimated")
115+
: null;
116+
const nextjsVersion =
117+
packageNode.dependencyInfo.framework === "nextjs"
118+
? getDependencyVersion(packageGraph, packageNode, "next")
119+
: null;
120+
const hasTypeScript = isFile(path.join(packageNode.directory, "tsconfig.json"));
121+
const hasReactThreeFiber = REACT_THREE_FIBER_ECOSYSTEM_DEPENDENCY_NAMES.some(
122+
(dependencyName) => getDependencyVersion(packageGraph, packageNode, dependencyName) !== null,
123+
);
124+
const projectInfo: ProjectInfo = {
125+
rootDirectory: packageNode.directory,
126+
projectName: packageNode.name ?? packageNode.directory,
127+
reactVersion,
128+
reactMajorVersion: resolveEffectiveReactMajor(reactVersion, packageNode.manifest),
129+
tailwindVersion,
130+
zodVersion,
131+
zodMajorVersion: zodVersion === null ? null : getLowestDependencyMajor(zodVersion),
132+
mobxVersion,
133+
mobxMajorVersion:
134+
mobxVersion === null
135+
? null
136+
: getDependencyMajorWithinSupportedRange(mobxVersion, LATEST_SUPPORTED_MOBX_MAJOR),
137+
hasMobxReact: mobxReactVersion !== null,
138+
mobxReactVersion,
139+
hasMobxReactLite: mobxReactLiteVersion !== null,
140+
mobxReactLiteVersion,
141+
hasMobxStateTree:
142+
getDependencyVersion(packageGraph, packageNode, MOBX_STATE_TREE_PACKAGE_NAME) !== null,
143+
hasMobxReactObserver:
144+
getDependencyVersion(packageGraph, packageNode, MOBX_REACT_OBSERVER_PACKAGE_NAME) !== null,
145+
zustandVersion,
146+
zustandMajorVersion: zustandVersion === null ? null : getLowestDependencyMajor(zustandVersion),
147+
framework: packageNode.dependencyInfo.framework,
148+
hasTypeScript,
149+
hasReactCompiler: detectReactCompiler(packageNode.directory, packageNode.manifest),
150+
hasReactCompilerLintPlugin: detectReactCompilerLintPlugin(
151+
packageNode.directory,
152+
packageNode.manifest,
153+
),
154+
hasTanStackQuery: tanstackQuery !== null,
155+
hasI18nLibrary: hasI18nDependency(packageNode.manifest),
156+
tanstackQueryVersion: tanstackQuery?.value ?? null,
157+
styledComponentsVersion,
158+
valtioVersion,
159+
valtioMajorVersion: valtioVersion === null ? null : getLowestDependencyMajor(valtioVersion),
160+
hasRemotion: remotionVersion !== null,
161+
remotionVersion,
162+
remotionMajorVersion:
163+
remotionVersion === null ? null : getLowestDependencyMajor(remotionVersion),
164+
hasThree: threeVersion !== null || hasReactThreeFiber,
165+
threeVersion,
166+
threeRelease: parseThreeRelease(threeVersion),
167+
hasReactThreeFiber,
168+
reactThreeFiberVersion: reactThreeFiber?.value ?? null,
169+
reactThreeFiberMajorVersion:
170+
reactThreeFiber === null ? null : getLowestDependencyMajor(reactThreeFiber.value),
171+
hasSsrDependency: isPackageJsonSsrAware(packageNode.manifest),
172+
preactVersion,
173+
preactMajorVersion: parseReactMajor(preactVersion),
174+
hasReactNativeWorkspace: hasReactNativePackage,
175+
nextjsVersion,
176+
nextjsMajorVersion: nextjsVersion === null ? null : getLowestDependencyMajor(nextjsVersion),
177+
reactRouterVersion: reactRouter?.value ?? null,
178+
hasReactRouterFramework:
179+
getDependencyVersion(packageGraph, packageNode, "@react-router/dev") !== null,
180+
expoVersion,
181+
shopifyFlashListVersion,
182+
shopifyFlashListMajorVersion:
183+
shopifyFlashListVersion === null ? null : getLowestDependencyMajor(shopifyFlashListVersion),
184+
hasReanimated,
185+
reanimatedVersion,
186+
isPreES2023Target: hasTypeScript && detectPreES2023Target(packageNode.directory),
187+
isStaticExport:
188+
packageNode.dependencyInfo.framework === "nextjs" &&
189+
detectNextjsStaticExport(packageNode.directory),
190+
sourceFileCount: 0,
191+
};
192+
return getCapabilities(projectInfo);
193+
};

packages/core/src/project-info/capabilities.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as path from "node:path";
2-
import type { Capability } from "oxlint-plugin-react-doctor";
2+
import type { Capability } from "oxlint-plugin-react-doctor/contracts";
33
import type { Framework, ProjectInfo } from "../types/index.js";
44
import {
55
EARLIEST_GATED_MOBX_MAJOR,
@@ -37,6 +37,8 @@ import {
3737
parseTailwindMajorMinor,
3838
} from "./version.js";
3939
import { detectTargetBlankOpenerProtection } from "./detect-target-blank-opener-protection.js";
40+
import { findNearestAncestorPackageJson } from "./find-nearest-ancestor-package-json.js";
41+
import { isFile } from "./fs-utils.js";
4042
import { readPackageJson } from "./package-json.js";
4143

4244
// SPA / mobile frameworks with no server-side form handler at all —
@@ -351,9 +353,13 @@ export const getCapabilities = (project: ProjectInfo): ReadonlySet<Capability> =
351353
const cached = capabilitiesByProject.get(project);
352354
if (cached !== undefined) return cached;
353355
const capabilities = new Set(buildCapabilities(project));
354-
const packageJson = readPackageJson(path.join(project.rootDirectory, "package.json"));
356+
const packageJsonPath = path.join(project.rootDirectory, "package.json");
357+
const capabilityRootDirectory = isFile(packageJsonPath)
358+
? project.rootDirectory
359+
: (findNearestAncestorPackageJson(project.rootDirectory) ?? project.rootDirectory);
360+
const packageJson = readPackageJson(path.join(capabilityRootDirectory, "package.json"));
355361
const targetBlankOpenerProtection = detectTargetBlankOpenerProtection(
356-
project.rootDirectory,
362+
capabilityRootDirectory,
357363
packageJson,
358364
);
359365
if (targetBlankOpenerProtection !== undefined) {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export const MOBX_REACT_PACKAGE_NAME = "mobx-react";
2+
export const MOBX_REACT_LITE_PACKAGE_NAME = "mobx-react-lite";
3+
export const MOBX_STATE_TREE_PACKAGE_NAME = "mobx-state-tree";
4+
export const MOBX_REACT_OBSERVER_PACKAGE_NAME = "mobx-react-observer";
5+
export const REANIMATED_DEPENDENCY_NAME = "react-native-reanimated";
6+
export const REACT_THREE_FIBER_DEPENDENCY_NAMES = [
7+
"@react-three/fiber",
8+
"react-three-fiber",
9+
] as const;
10+
export const REACT_THREE_FIBER_ECOSYSTEM_DEPENDENCY_NAMES = [
11+
...REACT_THREE_FIBER_DEPENDENCY_NAMES,
12+
"@react-three/drei",
13+
] as const;
14+
export const THREE_DEPENDENCY_NAMES = [
15+
...REACT_THREE_FIBER_ECOSYSTEM_DEPENDENCY_NAMES,
16+
"three",
17+
] as const;
18+
export const REACT_ROUTER_DEPENDENCY_NAMES = [
19+
"@react-router/dev",
20+
"react-router-dom",
21+
"react-router",
22+
] as const;
23+
export const TANSTACK_REACT_QUERY_PACKAGE_NAMES = ["@tanstack/react-query", "react-query"] as const;

0 commit comments

Comments
 (0)