react-doctor 0.0.34 → 0.0.35
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 +81 -9
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +79 -8
- package/dist/index.js.map +1 -1
- package/dist/react-doctor-plugin.d.ts.map +1 -1
- package/dist/react-doctor-plugin.js +568 -17
- package/dist/react-doctor-plugin.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
//#region src/types.d.ts
|
|
2
2
|
type FailOnLevel = "error" | "warning" | "none";
|
|
3
|
-
type Framework = "nextjs" | "vite" | "cra" | "remix" | "gatsby" | "expo" | "react-native" | "unknown";
|
|
3
|
+
type Framework = "nextjs" | "vite" | "cra" | "remix" | "gatsby" | "expo" | "react-native" | "tanstack-start" | "unknown";
|
|
4
4
|
interface ProjectInfo {
|
|
5
5
|
rootDirectory: string;
|
|
6
6
|
projectName: string;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/utils/get-diff-files.ts","../src/index.ts"],"mappings":";KAAY,WAAA;AAAA,KAEA,SAAA;AAAA,
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/utils/get-diff-files.ts","../src/index.ts"],"mappings":";KAAY,WAAA;AAAA,KAEA,SAAA;AAAA,UAWK,WAAA;EACf,aAAA;EACA,WAAA;EACA,YAAA;EACA,SAAA,EAAW,SAAA;EACX,aAAA;EACA,gBAAA;EACA,eAAA;AAAA;AAAA,UAiCe,UAAA;EACf,QAAA;EACA,MAAA;EACA,IAAA;EACA,QAAA;EACA,OAAA;EACA,IAAA;EACA,IAAA;EACA,MAAA;EACA,QAAA;EACA,MAAA;AAAA;AAAA,UA4Be,WAAA;EACf,KAAA;EACA,KAAA;AAAA;AAAA,UAmBe,QAAA;EACf,aAAA;EACA,UAAA;EACA,YAAA;EACA,gBAAA;AAAA;AAAA,UA2Ce,uBAAA;EACf,KAAA;EACA,KAAA;AAAA;AAAA,UAGe,iBAAA;EACf,MAAA,GAAS,uBAAA;EACT,IAAA;EACA,QAAA;EACA,OAAA;EACA,IAAA;EACA,MAAA,GAAS,WAAA;EACT,eAAA;EACA,KAAA;EACA,cAAA;AAAA;;;cC9FW,WAAA,GAAe,SAAA,UAAmB,kBAAA,cAA8B,QAAA;AAAA,cAiBhE,iBAAA,GAAqB,SAAA;;;UClFjB,eAAA;EACf,IAAA;EACA,QAAA;EACA,YAAA;AAAA;AAAA,UAGe,cAAA;EACf,WAAA,EAAa,UAAA;EACb,KAAA,EAAO,WAAA;EACP,OAAA,EAAS,WAAA;EACT,mBAAA;AAAA;AAAA,cAGW,QAAA,GACX,SAAA,UACA,OAAA,GAAS,eAAA,KACR,OAAA,CAAQ,cAAA"}
|
package/dist/index.js
CHANGED
|
@@ -382,9 +382,11 @@ const EXPO_APP_CONFIG_FILENAMES = [
|
|
|
382
382
|
"app.config.js",
|
|
383
383
|
"app.config.ts"
|
|
384
384
|
];
|
|
385
|
-
const
|
|
385
|
+
const REACT_COMPILER_PACKAGE_REFERENCE_PATTERN = /babel-plugin-react-compiler|react-compiler-runtime|eslint-plugin-react-compiler|["']react-compiler["']/;
|
|
386
|
+
const REACT_COMPILER_ENABLED_FLAG_PATTERN = /["']?reactCompiler["']?\s*:\s*true\b/;
|
|
386
387
|
const FRAMEWORK_PACKAGES = {
|
|
387
388
|
next: "nextjs",
|
|
389
|
+
"@tanstack/react-start": "tanstack-start",
|
|
388
390
|
vite: "vite",
|
|
389
391
|
"react-scripts": "cra",
|
|
390
392
|
"@remix-run/react": "remix",
|
|
@@ -630,12 +632,12 @@ const hasCompilerPackage = (packageJson) => {
|
|
|
630
632
|
const allDependencies = collectAllDependencies(packageJson);
|
|
631
633
|
return Object.keys(allDependencies).some((packageName) => REACT_COMPILER_PACKAGES.has(packageName));
|
|
632
634
|
};
|
|
633
|
-
const
|
|
635
|
+
const hasCompilerInConfigFile = (filePath) => {
|
|
634
636
|
if (!isFile(filePath)) return false;
|
|
635
637
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
636
|
-
return
|
|
638
|
+
return REACT_COMPILER_ENABLED_FLAG_PATTERN.test(content) || REACT_COMPILER_PACKAGE_REFERENCE_PATTERN.test(content);
|
|
637
639
|
};
|
|
638
|
-
const hasCompilerInConfigFiles = (directory, filenames) => filenames.some((filename) =>
|
|
640
|
+
const hasCompilerInConfigFiles = (directory, filenames) => filenames.some((filename) => hasCompilerInConfigFile(path.join(directory, filename)));
|
|
639
641
|
const detectReactCompiler = (directory, packageJson) => {
|
|
640
642
|
if (hasCompilerPackage(packageJson)) return true;
|
|
641
643
|
if (hasCompilerInConfigFiles(directory, NEXT_CONFIG_FILENAMES)) return true;
|
|
@@ -917,6 +919,22 @@ const REACT_NATIVE_RULES = {
|
|
|
917
919
|
"react-doctor/rn-prefer-reanimated": "warn",
|
|
918
920
|
"react-doctor/rn-no-single-element-style-array": "warn"
|
|
919
921
|
};
|
|
922
|
+
const TANSTACK_START_RULES = {
|
|
923
|
+
"react-doctor/tanstack-start-route-property-order": "error",
|
|
924
|
+
"react-doctor/tanstack-start-no-direct-fetch-in-loader": "warn",
|
|
925
|
+
"react-doctor/tanstack-start-server-fn-validate-input": "warn",
|
|
926
|
+
"react-doctor/tanstack-start-no-useeffect-fetch": "warn",
|
|
927
|
+
"react-doctor/tanstack-start-missing-head-content": "warn",
|
|
928
|
+
"react-doctor/tanstack-start-no-anchor-element": "warn",
|
|
929
|
+
"react-doctor/tanstack-start-server-fn-method-order": "error",
|
|
930
|
+
"react-doctor/tanstack-start-no-navigate-in-render": "warn",
|
|
931
|
+
"react-doctor/tanstack-start-no-dynamic-server-fn-import": "error",
|
|
932
|
+
"react-doctor/tanstack-start-no-use-server-in-handler": "error",
|
|
933
|
+
"react-doctor/tanstack-start-no-secrets-in-loader": "error",
|
|
934
|
+
"react-doctor/tanstack-start-get-mutation": "warn",
|
|
935
|
+
"react-doctor/tanstack-start-redirect-in-try-catch": "warn",
|
|
936
|
+
"react-doctor/tanstack-start-loader-parallel-fetch": "warn"
|
|
937
|
+
};
|
|
920
938
|
const REACT_COMPILER_RULES = {
|
|
921
939
|
"react-hooks-js/set-state-in-render": "error",
|
|
922
940
|
"react-hooks-js/immutability": "error",
|
|
@@ -1006,12 +1024,14 @@ const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler, customRul
|
|
|
1006
1024
|
"react-doctor/rendering-animate-svg-wrapper": "warn",
|
|
1007
1025
|
"react-doctor/no-inline-prop-on-memo-component": "warn",
|
|
1008
1026
|
"react-doctor/rendering-hydration-no-flicker": "warn",
|
|
1027
|
+
"react-doctor/rendering-script-defer-async": "warn",
|
|
1009
1028
|
"react-doctor/no-transition-all": "warn",
|
|
1010
1029
|
"react-doctor/no-global-css-variable-animation": "error",
|
|
1011
1030
|
"react-doctor/no-large-animated-blur": "warn",
|
|
1012
1031
|
"react-doctor/no-scale-from-zero": "warn",
|
|
1013
1032
|
"react-doctor/no-permanent-will-change": "warn",
|
|
1014
1033
|
"react-doctor/no-secrets-in-client-code": "error",
|
|
1034
|
+
"react-doctor/js-flatmap-filter": "warn",
|
|
1015
1035
|
"react-doctor/no-barrel-import": "warn",
|
|
1016
1036
|
"react-doctor/no-full-lodash-import": "warn",
|
|
1017
1037
|
"react-doctor/no-moment": "warn",
|
|
@@ -1024,9 +1044,16 @@ const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler, customRul
|
|
|
1024
1044
|
"react-doctor/server-auth-actions": "error",
|
|
1025
1045
|
"react-doctor/server-after-nonblocking": "warn",
|
|
1026
1046
|
"react-doctor/client-passive-event-listeners": "warn",
|
|
1047
|
+
"react-doctor/query-stable-query-client": "error",
|
|
1048
|
+
"react-doctor/query-no-rest-destructuring": "warn",
|
|
1049
|
+
"react-doctor/query-no-void-query-fn": "warn",
|
|
1050
|
+
"react-doctor/query-no-query-in-effect": "warn",
|
|
1051
|
+
"react-doctor/query-mutation-missing-invalidation": "warn",
|
|
1052
|
+
"react-doctor/query-no-usequery-for-mutation": "warn",
|
|
1027
1053
|
"react-doctor/async-parallel": "warn",
|
|
1028
1054
|
...framework === "nextjs" ? NEXTJS_RULES : {},
|
|
1029
|
-
...framework === "expo" || framework === "react-native" ? REACT_NATIVE_RULES : {}
|
|
1055
|
+
...framework === "expo" || framework === "react-native" ? REACT_NATIVE_RULES : {},
|
|
1056
|
+
...framework === "tanstack-start" ? TANSTACK_START_RULES : {}
|
|
1030
1057
|
}
|
|
1031
1058
|
});
|
|
1032
1059
|
|
|
@@ -1103,6 +1130,7 @@ const RULE_CATEGORY_MAP = {
|
|
|
1103
1130
|
"react-doctor/rendering-animate-svg-wrapper": "Performance",
|
|
1104
1131
|
"react-doctor/rendering-usetransition-loading": "Performance",
|
|
1105
1132
|
"react-doctor/rendering-hydration-no-flicker": "Performance",
|
|
1133
|
+
"react-doctor/rendering-script-defer-async": "Performance",
|
|
1106
1134
|
"react-doctor/no-transition-all": "Performance",
|
|
1107
1135
|
"react-doctor/no-global-css-variable-animation": "Performance",
|
|
1108
1136
|
"react-doctor/no-large-animated-blur": "Performance",
|
|
@@ -1137,6 +1165,13 @@ const RULE_CATEGORY_MAP = {
|
|
|
1137
1165
|
"react-doctor/server-auth-actions": "Server",
|
|
1138
1166
|
"react-doctor/server-after-nonblocking": "Server",
|
|
1139
1167
|
"react-doctor/client-passive-event-listeners": "Performance",
|
|
1168
|
+
"react-doctor/query-stable-query-client": "TanStack Query",
|
|
1169
|
+
"react-doctor/query-no-rest-destructuring": "TanStack Query",
|
|
1170
|
+
"react-doctor/query-no-void-query-fn": "TanStack Query",
|
|
1171
|
+
"react-doctor/query-no-query-in-effect": "TanStack Query",
|
|
1172
|
+
"react-doctor/query-mutation-missing-invalidation": "TanStack Query",
|
|
1173
|
+
"react-doctor/query-no-usequery-for-mutation": "TanStack Query",
|
|
1174
|
+
"react-doctor/js-flatmap-filter": "Performance",
|
|
1140
1175
|
"react-doctor/async-parallel": "Performance",
|
|
1141
1176
|
"react-doctor/rn-no-raw-text": "React Native",
|
|
1142
1177
|
"react-doctor/rn-no-deprecated-modules": "React Native",
|
|
@@ -1145,7 +1180,21 @@ const RULE_CATEGORY_MAP = {
|
|
|
1145
1180
|
"react-doctor/rn-no-inline-flatlist-renderitem": "React Native",
|
|
1146
1181
|
"react-doctor/rn-no-legacy-shadow-styles": "React Native",
|
|
1147
1182
|
"react-doctor/rn-prefer-reanimated": "React Native",
|
|
1148
|
-
"react-doctor/rn-no-single-element-style-array": "React Native"
|
|
1183
|
+
"react-doctor/rn-no-single-element-style-array": "React Native",
|
|
1184
|
+
"react-doctor/tanstack-start-route-property-order": "TanStack Start",
|
|
1185
|
+
"react-doctor/tanstack-start-no-direct-fetch-in-loader": "TanStack Start",
|
|
1186
|
+
"react-doctor/tanstack-start-server-fn-validate-input": "TanStack Start",
|
|
1187
|
+
"react-doctor/tanstack-start-no-useeffect-fetch": "TanStack Start",
|
|
1188
|
+
"react-doctor/tanstack-start-missing-head-content": "TanStack Start",
|
|
1189
|
+
"react-doctor/tanstack-start-no-anchor-element": "TanStack Start",
|
|
1190
|
+
"react-doctor/tanstack-start-server-fn-method-order": "TanStack Start",
|
|
1191
|
+
"react-doctor/tanstack-start-no-navigate-in-render": "TanStack Start",
|
|
1192
|
+
"react-doctor/tanstack-start-no-dynamic-server-fn-import": "TanStack Start",
|
|
1193
|
+
"react-doctor/tanstack-start-no-use-server-in-handler": "TanStack Start",
|
|
1194
|
+
"react-doctor/tanstack-start-no-secrets-in-loader": "Security",
|
|
1195
|
+
"react-doctor/tanstack-start-get-mutation": "Security",
|
|
1196
|
+
"react-doctor/tanstack-start-redirect-in-try-catch": "TanStack Start",
|
|
1197
|
+
"react-doctor/tanstack-start-loader-parallel-fetch": "Performance"
|
|
1149
1198
|
};
|
|
1150
1199
|
const RULE_HELP_MAP = {
|
|
1151
1200
|
"no-derived-state-effect": "For derived state, compute inline: `const x = fn(dep)`. For state resets on prop change, use a key prop: `<Component key={prop} />`. See https://react.dev/learn/you-might-not-need-an-effect",
|
|
@@ -1167,6 +1216,7 @@ const RULE_HELP_MAP = {
|
|
|
1167
1216
|
"rendering-animate-svg-wrapper": "Wrap the SVG: `<motion.div animate={...}><svg>...</svg></motion.div>`",
|
|
1168
1217
|
"rendering-usetransition-loading": "Replace with `const [isPending, startTransition] = useTransition()` — avoids a re-render for the loading state",
|
|
1169
1218
|
"rendering-hydration-no-flicker": "Use `useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)` or add `suppressHydrationWarning` to the element",
|
|
1219
|
+
"rendering-script-defer-async": "Add `defer` for DOM-dependent scripts or `async` for independent ones (analytics). In Next.js, use `<Script strategy=\"afterInteractive\" />` instead",
|
|
1170
1220
|
"no-transition-all": "List specific properties: `transition: \"opacity 200ms, transform 200ms\"` — or in Tailwind use `transition-colors`, `transition-opacity`, or `transition-transform`",
|
|
1171
1221
|
"no-global-css-variable-animation": "Set the variable on the nearest element instead of a parent, or use `@property` with `inherits: false` to prevent cascade. Better yet, use targeted `element.style.transform` updates",
|
|
1172
1222
|
"no-large-animated-blur": "Keep blur radius under 10px, or apply blur to a smaller element. Large blurs multiply GPU memory usage with layer size",
|
|
@@ -1188,7 +1238,7 @@ const RULE_HELP_MAP = {
|
|
|
1188
1238
|
"nextjs-no-use-search-params-without-suspense": "Wrap the component using useSearchParams: `<Suspense fallback={<Skeleton />}><SearchComponent /></Suspense>`",
|
|
1189
1239
|
"nextjs-no-client-fetch-for-server-data": "Remove 'use client' and fetch directly in the Server Component — no API round-trip, secrets stay on server",
|
|
1190
1240
|
"nextjs-missing-metadata": "Add `export const metadata = { title: '...', description: '...' }` or `export async function generateMetadata()`",
|
|
1191
|
-
"nextjs-no-client-side-redirect": "
|
|
1241
|
+
"nextjs-no-client-side-redirect": "Avoid redirects inside useEffect. Use an event handler, middleware, or server-side redirect (App Router: redirect() from next/navigation; Pages Router: getServerSideProps redirect)",
|
|
1192
1242
|
"nextjs-no-redirect-in-try-catch": "Move the redirect/notFound call outside the try block, or add `unstable_rethrow(error)` in the catch",
|
|
1193
1243
|
"nextjs-image-missing-sizes": "Add sizes for responsive behavior: `sizes=\"(max-width: 768px) 100vw, 50vw\"` matching your layout breakpoints",
|
|
1194
1244
|
"nextjs-no-native-script": "`import Script from \"next/script\"` — use `strategy=\"afterInteractive\"` for analytics or `\"lazyOnload\"` for widgets",
|
|
@@ -1201,6 +1251,13 @@ const RULE_HELP_MAP = {
|
|
|
1201
1251
|
"server-auth-actions": "Add `const session = await auth()` at the top and throw/redirect if unauthorized before any data access",
|
|
1202
1252
|
"server-after-nonblocking": "`import { after } from 'next/server'` then wrap: `after(() => analytics.track(...))` — response isn't blocked",
|
|
1203
1253
|
"client-passive-event-listeners": "Add `{ passive: true }` as the third argument: `addEventListener('scroll', handler, { passive: true })`",
|
|
1254
|
+
"query-stable-query-client": "Move `new QueryClient()` to module scope or wrap in `useState(() => new QueryClient())` — recreating it on every render resets the entire cache",
|
|
1255
|
+
"query-no-rest-destructuring": "Destructure only the fields you need: `const { data, isLoading } = useQuery(...)` — rest destructuring subscribes to all fields and causes extra re-renders",
|
|
1256
|
+
"query-no-void-query-fn": "queryFn must return a value for the cache. Use the `enabled` option to conditionally disable the query instead of returning undefined",
|
|
1257
|
+
"query-no-query-in-effect": "React Query manages refetching automatically via queryKey dependencies and the `enabled` option — manual refetch() in useEffect is usually unnecessary",
|
|
1258
|
+
"query-mutation-missing-invalidation": "Add `onSuccess: () => queryClient.invalidateQueries({ queryKey: ['...'] })` so cached data stays in sync after the mutation",
|
|
1259
|
+
"query-no-usequery-for-mutation": "Use `useMutation()` for POST/PUT/DELETE — it provides onSuccess/onError callbacks, doesn't auto-refetch, and correctly models write operations",
|
|
1260
|
+
"js-flatmap-filter": "Use `.flatMap(item => condition ? [value] : [])` — transforms and filters in a single pass instead of creating an intermediate array",
|
|
1204
1261
|
"async-parallel": "Use `const [a, b] = await Promise.all([fetchA(), fetchB()])` to run independent operations concurrently",
|
|
1205
1262
|
"rn-no-raw-text": "Wrap text in a `<Text>` component: `<Text>{value}</Text>` — raw strings outside `<Text>` crash on React Native",
|
|
1206
1263
|
"rn-no-deprecated-modules": "Import from the community package instead — deprecated modules were removed from the react-native core",
|
|
@@ -1209,7 +1266,21 @@ const RULE_HELP_MAP = {
|
|
|
1209
1266
|
"rn-no-inline-flatlist-renderitem": "Extract renderItem to a named function or wrap in useCallback to avoid re-creating on every render",
|
|
1210
1267
|
"rn-no-legacy-shadow-styles": "Use `boxShadow` for cross-platform shadows on the new architecture instead of platform-specific shadow properties",
|
|
1211
1268
|
"rn-prefer-reanimated": "Use `import Animated from 'react-native-reanimated'` — animations run on the UI thread instead of the JS thread",
|
|
1212
|
-
"rn-no-single-element-style-array": "Use `style={value}` instead of `style={[value]}` — single-element arrays add unnecessary allocation"
|
|
1269
|
+
"rn-no-single-element-style-array": "Use `style={value}` instead of `style={[value]}` — single-element arrays add unnecessary allocation",
|
|
1270
|
+
"tanstack-start-route-property-order": "Follow the order: params/validateSearch → loaderDeps → context → beforeLoad → loader → head. See https://tanstack.com/router/latest/docs/eslint/create-route-property-order",
|
|
1271
|
+
"tanstack-start-no-direct-fetch-in-loader": "Use `createServerFn()` from @tanstack/react-start — provides type-safe RPC, input validation, and proper server/client code splitting",
|
|
1272
|
+
"tanstack-start-server-fn-validate-input": "Add `.inputValidator(schema)` before `.handler()` — data crosses a network boundary and must be validated at runtime",
|
|
1273
|
+
"tanstack-start-no-useeffect-fetch": "Fetch data in the route `loader` instead — the router coordinates loading before rendering to avoid waterfalls",
|
|
1274
|
+
"tanstack-start-missing-head-content": "Add `<HeadContent />` inside `<head>` in your __root route — without it, route `head()` meta tags are silently dropped",
|
|
1275
|
+
"tanstack-start-no-anchor-element": "`import { Link } from '@tanstack/react-router'` — enables type-safe routes, preloading via `preload=\"intent\"`, and client-side navigation",
|
|
1276
|
+
"tanstack-start-server-fn-method-order": "Chain methods in order: .middleware() → .inputValidator() → .client() → .server() → .handler() — types depend on this sequence",
|
|
1277
|
+
"tanstack-start-no-navigate-in-render": "Use `throw redirect({ to: '/path' })` in `beforeLoad` or `loader` instead — navigate() during render causes hydration issues",
|
|
1278
|
+
"tanstack-start-no-dynamic-server-fn-import": "Use `import { myFn } from '~/utils/my.functions'` — the bundler replaces server code with RPC stubs only for static imports",
|
|
1279
|
+
"tanstack-start-no-use-server-in-handler": "TanStack Start handles server boundaries automatically via the Vite plugin — \"use server\" inside createServerFn causes compilation errors",
|
|
1280
|
+
"tanstack-start-no-secrets-in-loader": "Loaders are isomorphic (run on both server and client). Wrap secret access in `createServerFn()` so it stays server-only",
|
|
1281
|
+
"tanstack-start-get-mutation": "Use `createServerFn({ method: 'POST' })` for data modifications — GET requests can be triggered by prefetching and are vulnerable to CSRF",
|
|
1282
|
+
"tanstack-start-redirect-in-try-catch": "TanStack Router's `redirect()` and `notFound()` throw special errors caught by the router. Move them outside the try block or re-throw in the catch",
|
|
1283
|
+
"tanstack-start-loader-parallel-fetch": "Use `const [a, b] = await Promise.all([fetchA(), fetchB()])` to avoid request waterfalls in route loaders"
|
|
1213
1284
|
};
|
|
1214
1285
|
const FILEPATH_WITH_LOCATION_PATTERN = /\S+\.\w+:\d+:\d+[\s\S]*$/;
|
|
1215
1286
|
const REACT_COMPILER_MESSAGE = "React Compiler can't optimize this code";
|