react-doctor 0.0.33 → 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 CHANGED
@@ -414,9 +414,11 @@ const EXPO_APP_CONFIG_FILENAMES = [
414
414
  "app.config.js",
415
415
  "app.config.ts"
416
416
  ];
417
- const REACT_COMPILER_CONFIG_PATTERN = /react-compiler|reactCompiler/;
417
+ const REACT_COMPILER_PACKAGE_REFERENCE_PATTERN = /babel-plugin-react-compiler|react-compiler-runtime|eslint-plugin-react-compiler|["']react-compiler["']/;
418
+ const REACT_COMPILER_ENABLED_FLAG_PATTERN = /["']?reactCompiler["']?\s*:\s*true\b/;
418
419
  const FRAMEWORK_PACKAGES = {
419
420
  next: "nextjs",
421
+ "@tanstack/react-start": "tanstack-start",
420
422
  vite: "vite",
421
423
  "react-scripts": "cra",
422
424
  "@remix-run/react": "remix",
@@ -426,6 +428,7 @@ const FRAMEWORK_PACKAGES = {
426
428
  };
427
429
  const FRAMEWORK_DISPLAY_NAMES = {
428
430
  nextjs: "Next.js",
431
+ "tanstack-start": "TanStack Start",
429
432
  vite: "Vite",
430
433
  cra: "Create React App",
431
434
  remix: "Remix",
@@ -740,12 +743,12 @@ const hasCompilerPackage = (packageJson) => {
740
743
  const allDependencies = collectAllDependencies(packageJson);
741
744
  return Object.keys(allDependencies).some((packageName) => REACT_COMPILER_PACKAGES.has(packageName));
742
745
  };
743
- const fileContainsPattern = (filePath, pattern) => {
746
+ const hasCompilerInConfigFile = (filePath) => {
744
747
  if (!isFile(filePath)) return false;
745
748
  const content = fs.readFileSync(filePath, "utf-8");
746
- return pattern.test(content);
749
+ return REACT_COMPILER_ENABLED_FLAG_PATTERN.test(content) || REACT_COMPILER_PACKAGE_REFERENCE_PATTERN.test(content);
747
750
  };
748
- const hasCompilerInConfigFiles = (directory, filenames) => filenames.some((filename) => fileContainsPattern(path.join(directory, filename), REACT_COMPILER_CONFIG_PATTERN));
751
+ const hasCompilerInConfigFiles = (directory, filenames) => filenames.some((filename) => hasCompilerInConfigFile(path.join(directory, filename)));
749
752
  const detectReactCompiler = (directory, packageJson) => {
750
753
  if (hasCompilerPackage(packageJson)) return true;
751
754
  if (hasCompilerInConfigFiles(directory, NEXT_CONFIG_FILENAMES)) return true;
@@ -1252,6 +1255,22 @@ const REACT_NATIVE_RULES = {
1252
1255
  "react-doctor/rn-prefer-reanimated": "warn",
1253
1256
  "react-doctor/rn-no-single-element-style-array": "warn"
1254
1257
  };
1258
+ const TANSTACK_START_RULES = {
1259
+ "react-doctor/tanstack-start-route-property-order": "error",
1260
+ "react-doctor/tanstack-start-no-direct-fetch-in-loader": "warn",
1261
+ "react-doctor/tanstack-start-server-fn-validate-input": "warn",
1262
+ "react-doctor/tanstack-start-no-useeffect-fetch": "warn",
1263
+ "react-doctor/tanstack-start-missing-head-content": "warn",
1264
+ "react-doctor/tanstack-start-no-anchor-element": "warn",
1265
+ "react-doctor/tanstack-start-server-fn-method-order": "error",
1266
+ "react-doctor/tanstack-start-no-navigate-in-render": "warn",
1267
+ "react-doctor/tanstack-start-no-dynamic-server-fn-import": "error",
1268
+ "react-doctor/tanstack-start-no-use-server-in-handler": "error",
1269
+ "react-doctor/tanstack-start-no-secrets-in-loader": "error",
1270
+ "react-doctor/tanstack-start-get-mutation": "warn",
1271
+ "react-doctor/tanstack-start-redirect-in-try-catch": "warn",
1272
+ "react-doctor/tanstack-start-loader-parallel-fetch": "warn"
1273
+ };
1255
1274
  const REACT_COMPILER_RULES = {
1256
1275
  "react-hooks-js/set-state-in-render": "error",
1257
1276
  "react-hooks-js/immutability": "error",
@@ -1341,12 +1360,14 @@ const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler, customRul
1341
1360
  "react-doctor/rendering-animate-svg-wrapper": "warn",
1342
1361
  "react-doctor/no-inline-prop-on-memo-component": "warn",
1343
1362
  "react-doctor/rendering-hydration-no-flicker": "warn",
1363
+ "react-doctor/rendering-script-defer-async": "warn",
1344
1364
  "react-doctor/no-transition-all": "warn",
1345
1365
  "react-doctor/no-global-css-variable-animation": "error",
1346
1366
  "react-doctor/no-large-animated-blur": "warn",
1347
1367
  "react-doctor/no-scale-from-zero": "warn",
1348
1368
  "react-doctor/no-permanent-will-change": "warn",
1349
1369
  "react-doctor/no-secrets-in-client-code": "error",
1370
+ "react-doctor/js-flatmap-filter": "warn",
1350
1371
  "react-doctor/no-barrel-import": "warn",
1351
1372
  "react-doctor/no-full-lodash-import": "warn",
1352
1373
  "react-doctor/no-moment": "warn",
@@ -1359,9 +1380,16 @@ const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler, customRul
1359
1380
  "react-doctor/server-auth-actions": "error",
1360
1381
  "react-doctor/server-after-nonblocking": "warn",
1361
1382
  "react-doctor/client-passive-event-listeners": "warn",
1383
+ "react-doctor/query-stable-query-client": "error",
1384
+ "react-doctor/query-no-rest-destructuring": "warn",
1385
+ "react-doctor/query-no-void-query-fn": "warn",
1386
+ "react-doctor/query-no-query-in-effect": "warn",
1387
+ "react-doctor/query-mutation-missing-invalidation": "warn",
1388
+ "react-doctor/query-no-usequery-for-mutation": "warn",
1362
1389
  "react-doctor/async-parallel": "warn",
1363
1390
  ...framework === "nextjs" ? NEXTJS_RULES : {},
1364
- ...framework === "expo" || framework === "react-native" ? REACT_NATIVE_RULES : {}
1391
+ ...framework === "expo" || framework === "react-native" ? REACT_NATIVE_RULES : {},
1392
+ ...framework === "tanstack-start" ? TANSTACK_START_RULES : {}
1365
1393
  }
1366
1394
  });
1367
1395
 
@@ -1438,6 +1466,7 @@ const RULE_CATEGORY_MAP = {
1438
1466
  "react-doctor/rendering-animate-svg-wrapper": "Performance",
1439
1467
  "react-doctor/rendering-usetransition-loading": "Performance",
1440
1468
  "react-doctor/rendering-hydration-no-flicker": "Performance",
1469
+ "react-doctor/rendering-script-defer-async": "Performance",
1441
1470
  "react-doctor/no-transition-all": "Performance",
1442
1471
  "react-doctor/no-global-css-variable-animation": "Performance",
1443
1472
  "react-doctor/no-large-animated-blur": "Performance",
@@ -1472,6 +1501,13 @@ const RULE_CATEGORY_MAP = {
1472
1501
  "react-doctor/server-auth-actions": "Server",
1473
1502
  "react-doctor/server-after-nonblocking": "Server",
1474
1503
  "react-doctor/client-passive-event-listeners": "Performance",
1504
+ "react-doctor/query-stable-query-client": "TanStack Query",
1505
+ "react-doctor/query-no-rest-destructuring": "TanStack Query",
1506
+ "react-doctor/query-no-void-query-fn": "TanStack Query",
1507
+ "react-doctor/query-no-query-in-effect": "TanStack Query",
1508
+ "react-doctor/query-mutation-missing-invalidation": "TanStack Query",
1509
+ "react-doctor/query-no-usequery-for-mutation": "TanStack Query",
1510
+ "react-doctor/js-flatmap-filter": "Performance",
1475
1511
  "react-doctor/async-parallel": "Performance",
1476
1512
  "react-doctor/rn-no-raw-text": "React Native",
1477
1513
  "react-doctor/rn-no-deprecated-modules": "React Native",
@@ -1480,7 +1516,21 @@ const RULE_CATEGORY_MAP = {
1480
1516
  "react-doctor/rn-no-inline-flatlist-renderitem": "React Native",
1481
1517
  "react-doctor/rn-no-legacy-shadow-styles": "React Native",
1482
1518
  "react-doctor/rn-prefer-reanimated": "React Native",
1483
- "react-doctor/rn-no-single-element-style-array": "React Native"
1519
+ "react-doctor/rn-no-single-element-style-array": "React Native",
1520
+ "react-doctor/tanstack-start-route-property-order": "TanStack Start",
1521
+ "react-doctor/tanstack-start-no-direct-fetch-in-loader": "TanStack Start",
1522
+ "react-doctor/tanstack-start-server-fn-validate-input": "TanStack Start",
1523
+ "react-doctor/tanstack-start-no-useeffect-fetch": "TanStack Start",
1524
+ "react-doctor/tanstack-start-missing-head-content": "TanStack Start",
1525
+ "react-doctor/tanstack-start-no-anchor-element": "TanStack Start",
1526
+ "react-doctor/tanstack-start-server-fn-method-order": "TanStack Start",
1527
+ "react-doctor/tanstack-start-no-navigate-in-render": "TanStack Start",
1528
+ "react-doctor/tanstack-start-no-dynamic-server-fn-import": "TanStack Start",
1529
+ "react-doctor/tanstack-start-no-use-server-in-handler": "TanStack Start",
1530
+ "react-doctor/tanstack-start-no-secrets-in-loader": "Security",
1531
+ "react-doctor/tanstack-start-get-mutation": "Security",
1532
+ "react-doctor/tanstack-start-redirect-in-try-catch": "TanStack Start",
1533
+ "react-doctor/tanstack-start-loader-parallel-fetch": "Performance"
1484
1534
  };
1485
1535
  const RULE_HELP_MAP = {
1486
1536
  "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",
@@ -1502,6 +1552,7 @@ const RULE_HELP_MAP = {
1502
1552
  "rendering-animate-svg-wrapper": "Wrap the SVG: `<motion.div animate={...}><svg>...</svg></motion.div>`",
1503
1553
  "rendering-usetransition-loading": "Replace with `const [isPending, startTransition] = useTransition()` — avoids a re-render for the loading state",
1504
1554
  "rendering-hydration-no-flicker": "Use `useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)` or add `suppressHydrationWarning` to the element",
1555
+ "rendering-script-defer-async": "Add `defer` for DOM-dependent scripts or `async` for independent ones (analytics). In Next.js, use `<Script strategy=\"afterInteractive\" />` instead",
1505
1556
  "no-transition-all": "List specific properties: `transition: \"opacity 200ms, transform 200ms\"` — or in Tailwind use `transition-colors`, `transition-opacity`, or `transition-transform`",
1506
1557
  "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",
1507
1558
  "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",
@@ -1523,7 +1574,7 @@ const RULE_HELP_MAP = {
1523
1574
  "nextjs-no-use-search-params-without-suspense": "Wrap the component using useSearchParams: `<Suspense fallback={<Skeleton />}><SearchComponent /></Suspense>`",
1524
1575
  "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",
1525
1576
  "nextjs-missing-metadata": "Add `export const metadata = { title: '...', description: '...' }` or `export async function generateMetadata()`",
1526
- "nextjs-no-client-side-redirect": "Use `redirect('/path')` from 'next/navigation' directly (works in both server and client components), or handle in middleware",
1577
+ "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)",
1527
1578
  "nextjs-no-redirect-in-try-catch": "Move the redirect/notFound call outside the try block, or add `unstable_rethrow(error)` in the catch",
1528
1579
  "nextjs-image-missing-sizes": "Add sizes for responsive behavior: `sizes=\"(max-width: 768px) 100vw, 50vw\"` matching your layout breakpoints",
1529
1580
  "nextjs-no-native-script": "`import Script from \"next/script\"` — use `strategy=\"afterInteractive\"` for analytics or `\"lazyOnload\"` for widgets",
@@ -1536,6 +1587,13 @@ const RULE_HELP_MAP = {
1536
1587
  "server-auth-actions": "Add `const session = await auth()` at the top and throw/redirect if unauthorized before any data access",
1537
1588
  "server-after-nonblocking": "`import { after } from 'next/server'` then wrap: `after(() => analytics.track(...))` — response isn't blocked",
1538
1589
  "client-passive-event-listeners": "Add `{ passive: true }` as the third argument: `addEventListener('scroll', handler, { passive: true })`",
1590
+ "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",
1591
+ "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",
1592
+ "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",
1593
+ "query-no-query-in-effect": "React Query manages refetching automatically via queryKey dependencies and the `enabled` option — manual refetch() in useEffect is usually unnecessary",
1594
+ "query-mutation-missing-invalidation": "Add `onSuccess: () => queryClient.invalidateQueries({ queryKey: ['...'] })` so cached data stays in sync after the mutation",
1595
+ "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",
1596
+ "js-flatmap-filter": "Use `.flatMap(item => condition ? [value] : [])` — transforms and filters in a single pass instead of creating an intermediate array",
1539
1597
  "async-parallel": "Use `const [a, b] = await Promise.all([fetchA(), fetchB()])` to run independent operations concurrently",
1540
1598
  "rn-no-raw-text": "Wrap text in a `<Text>` component: `<Text>{value}</Text>` — raw strings outside `<Text>` crash on React Native",
1541
1599
  "rn-no-deprecated-modules": "Import from the community package instead — deprecated modules were removed from the react-native core",
@@ -1544,7 +1602,21 @@ const RULE_HELP_MAP = {
1544
1602
  "rn-no-inline-flatlist-renderitem": "Extract renderItem to a named function or wrap in useCallback to avoid re-creating on every render",
1545
1603
  "rn-no-legacy-shadow-styles": "Use `boxShadow` for cross-platform shadows on the new architecture instead of platform-specific shadow properties",
1546
1604
  "rn-prefer-reanimated": "Use `import Animated from 'react-native-reanimated'` — animations run on the UI thread instead of the JS thread",
1547
- "rn-no-single-element-style-array": "Use `style={value}` instead of `style={[value]}` — single-element arrays add unnecessary allocation"
1605
+ "rn-no-single-element-style-array": "Use `style={value}` instead of `style={[value]}` — single-element arrays add unnecessary allocation",
1606
+ "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",
1607
+ "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",
1608
+ "tanstack-start-server-fn-validate-input": "Add `.inputValidator(schema)` before `.handler()` — data crosses a network boundary and must be validated at runtime",
1609
+ "tanstack-start-no-useeffect-fetch": "Fetch data in the route `loader` instead — the router coordinates loading before rendering to avoid waterfalls",
1610
+ "tanstack-start-missing-head-content": "Add `<HeadContent />` inside `<head>` in your __root route — without it, route `head()` meta tags are silently dropped",
1611
+ "tanstack-start-no-anchor-element": "`import { Link } from '@tanstack/react-router'` — enables type-safe routes, preloading via `preload=\"intent\"`, and client-side navigation",
1612
+ "tanstack-start-server-fn-method-order": "Chain methods in order: .middleware() → .inputValidator() → .client() → .server() → .handler() — types depend on this sequence",
1613
+ "tanstack-start-no-navigate-in-render": "Use `throw redirect({ to: '/path' })` in `beforeLoad` or `loader` instead — navigate() during render causes hydration issues",
1614
+ "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",
1615
+ "tanstack-start-no-use-server-in-handler": "TanStack Start handles server boundaries automatically via the Vite plugin — \"use server\" inside createServerFn causes compilation errors",
1616
+ "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",
1617
+ "tanstack-start-get-mutation": "Use `createServerFn({ method: 'POST' })` for data modifications — GET requests can be triggered by prefetching and are vulnerable to CSRF",
1618
+ "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",
1619
+ "tanstack-start-loader-parallel-fetch": "Use `const [a, b] = await Promise.all([fetchA(), fetchB()])` to avoid request waterfalls in route loaders"
1548
1620
  };
1549
1621
  const FILEPATH_WITH_LOCATION_PATTERN = /\S+\.\w+:\d+:\d+[\s\S]*$/;
1550
1622
  const REACT_COMPILER_MESSAGE = "React Compiler can't optimize this code";
@@ -2280,7 +2352,7 @@ const promptProjectSelection = async (workspacePackages, rootDirectory) => {
2280
2352
 
2281
2353
  //#endregion
2282
2354
  //#region src/cli.ts
2283
- const VERSION = "0.0.33";
2355
+ const VERSION = "0.0.35";
2284
2356
  const VALID_FAIL_ON_LEVELS = new Set([
2285
2357
  "error",
2286
2358
  "warning",