react-doctor 0.0.29 → 0.0.31

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.
@@ -252,6 +252,14 @@ const BLUR_VALUE_PATTERN = /blur\((\d+(?:\.\d+)?)px\)/;
252
252
  const ANIMATION_CALLBACK_NAMES = new Set(["requestAnimationFrame", "setInterval"]);
253
253
  const RAW_TEXT_PREVIEW_MAX_CHARS = 30;
254
254
  const REACT_NATIVE_TEXT_COMPONENTS = new Set(["Text", "TextInput"]);
255
+ const REACT_NATIVE_TEXT_COMPONENT_SUFFIXES = new Set([
256
+ "Text",
257
+ "Title",
258
+ "Label",
259
+ "Heading",
260
+ "Caption",
261
+ "Subtitle"
262
+ ]);
255
263
  const DEPRECATED_RN_MODULE_REPLACEMENTS = {
256
264
  AsyncStorage: "@react-native-async-storage/async-storage",
257
265
  Picker: "@react-native-picker/picker",
@@ -273,7 +281,7 @@ const DEPRECATED_RN_MODULE_REPLACEMENTS = {
273
281
  const LEGACY_EXPO_PACKAGE_REPLACEMENTS = {
274
282
  "expo-av": "expo-audio for audio and expo-video for video",
275
283
  "expo-permissions": "the permissions API in each module (e.g. Camera.requestPermissionsAsync())",
276
- "@expo/vector-icons": "expo-image with sf: source URIs"
284
+ "@expo/vector-icons": "expo-symbols or expo-image (see https://docs.expo.dev/versions/latest/sdk/symbols/)"
277
285
  };
278
286
  const REACT_NATIVE_LIST_COMPONENTS = new Set([
279
287
  "FlatList",
@@ -853,26 +861,29 @@ const nextjsMissingMetadata = { create: (context) => ({ Program(programNode) {
853
861
  message: "Page without metadata or generateMetadata export — hurts SEO"
854
862
  });
855
863
  } }) };
856
- const isClientSideRedirect = (node) => {
864
+ const describeClientSideNavigation = (node) => {
857
865
  if (node.type === "CallExpression" && node.callee?.type === "MemberExpression") {
858
- if ((node.callee.object?.type === "Identifier" ? node.callee.object.name : null) === "router" && (isMemberProperty(node.callee, "push") || isMemberProperty(node.callee, "replace"))) return true;
866
+ const objectName = node.callee.object?.type === "Identifier" ? node.callee.object.name : null;
867
+ const methodName = node.callee.property?.type === "Identifier" ? node.callee.property.name : null;
868
+ if (objectName === "router" && (methodName === "push" || methodName === "replace")) return `router.${methodName}() in useEffect — use redirect() from next/navigation or handle navigation in an event handler`;
859
869
  }
860
870
  if (node.type === "AssignmentExpression" && node.left?.type === "MemberExpression") {
861
871
  const objectName = node.left.object?.type === "Identifier" ? node.left.object.name : null;
862
872
  const propertyName = node.left.property?.type === "Identifier" ? node.left.property.name : null;
863
- if (objectName === "window" && propertyName === "location") return true;
864
- if (objectName === "location" && propertyName === "href") return true;
873
+ if (objectName === "window" && propertyName === "location") return "window.location assignment in useEffect — use redirect() from next/navigation or handle in middleware instead";
874
+ if (objectName === "location" && propertyName === "href") return "location.href assignment in useEffect — use redirect() from next/navigation or handle in middleware instead";
865
875
  }
866
- return false;
876
+ return null;
867
877
  };
868
878
  const nextjsNoClientSideRedirect = { create: (context) => ({ CallExpression(node) {
869
879
  if (!isHookCall(node, EFFECT_HOOK_NAMES)) return;
870
880
  const callback = getEffectCallback(node);
871
881
  if (!callback) return;
872
882
  walkAst(callback, (child) => {
873
- if (isClientSideRedirect(child)) context.report({
883
+ const navigationDescription = describeClientSideNavigation(child);
884
+ if (navigationDescription) context.report({
874
885
  node: child,
875
- message: "Client-side redirect in useEffect — use redirect() in a server component or middleware instead"
886
+ message: navigationDescription
876
887
  });
877
888
  });
878
889
  } }) };
@@ -1249,6 +1260,10 @@ const getRawTextDescription = (child) => {
1249
1260
  }
1250
1261
  return "text content";
1251
1262
  };
1263
+ const isTextHandlingComponent = (elementName) => {
1264
+ if (REACT_NATIVE_TEXT_COMPONENTS.has(elementName)) return true;
1265
+ return [...REACT_NATIVE_TEXT_COMPONENT_SUFFIXES].some((suffix) => elementName.endsWith(suffix));
1266
+ };
1252
1267
  const rnNoRawText = { create: (context) => {
1253
1268
  let isDomComponentFile = false;
1254
1269
  return {
@@ -1258,7 +1273,7 @@ const rnNoRawText = { create: (context) => {
1258
1273
  JSXElement(node) {
1259
1274
  if (isDomComponentFile) return;
1260
1275
  const elementName = resolveJsxElementName(node.openingElement);
1261
- if (elementName && REACT_NATIVE_TEXT_COMPONENTS.has(elementName)) return;
1276
+ if (elementName && isTextHandlingComponent(elementName)) return;
1262
1277
  for (const child of node.children ?? []) {
1263
1278
  if (!isRawTextContent(child)) continue;
1264
1279
  context.report({