react-doctor 0.0.30 → 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.
- package/dist/cli.js +198 -30
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +181 -22
- package/dist/index.js.map +1 -1
- package/dist/react-doctor-plugin.js +23 -8
- package/dist/react-doctor-plugin.js.map +1 -1
- package/package.json +1 -1
|
@@ -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",
|
|
@@ -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
|
|
864
|
+
const describeClientSideNavigation = (node) => {
|
|
857
865
|
if (node.type === "CallExpression" && node.callee?.type === "MemberExpression") {
|
|
858
|
-
|
|
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
|
|
864
|
-
if (objectName === "location" && propertyName === "href") return
|
|
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
|
|
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
|
-
|
|
883
|
+
const navigationDescription = describeClientSideNavigation(child);
|
|
884
|
+
if (navigationDescription) context.report({
|
|
874
885
|
node: child,
|
|
875
|
-
message:
|
|
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 && (
|
|
1276
|
+
if (elementName && isTextHandlingComponent(elementName)) return;
|
|
1262
1277
|
for (const child of node.children ?? []) {
|
|
1263
1278
|
if (!isRawTextContent(child)) continue;
|
|
1264
1279
|
context.report({
|