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
|
@@ -169,6 +169,56 @@ const SECRET_FALSE_POSITIVE_SUFFIXES = new Set([
|
|
|
169
169
|
"schema",
|
|
170
170
|
"constant"
|
|
171
171
|
]);
|
|
172
|
+
const TANSTACK_ROUTE_FILE_PATTERN = /\/routes\//;
|
|
173
|
+
const TANSTACK_ROOT_ROUTE_FILE_PATTERN = /__root\.(tsx?|jsx?)$/;
|
|
174
|
+
const TANSTACK_ROUTE_PROPERTY_ORDER = [
|
|
175
|
+
"params",
|
|
176
|
+
"validateSearch",
|
|
177
|
+
"loaderDeps",
|
|
178
|
+
"search.middlewares",
|
|
179
|
+
"ssr",
|
|
180
|
+
"context",
|
|
181
|
+
"beforeLoad",
|
|
182
|
+
"loader",
|
|
183
|
+
"onEnter",
|
|
184
|
+
"onStay",
|
|
185
|
+
"onLeave",
|
|
186
|
+
"head",
|
|
187
|
+
"scripts",
|
|
188
|
+
"headers",
|
|
189
|
+
"remountDeps"
|
|
190
|
+
];
|
|
191
|
+
const TANSTACK_ROUTE_CREATION_FUNCTIONS = new Set([
|
|
192
|
+
"createFileRoute",
|
|
193
|
+
"createRoute",
|
|
194
|
+
"createRootRoute",
|
|
195
|
+
"createRootRouteWithContext"
|
|
196
|
+
]);
|
|
197
|
+
const TANSTACK_SERVER_FN_NAMES = new Set(["createServerFn"]);
|
|
198
|
+
const TANSTACK_MIDDLEWARE_METHOD_ORDER = [
|
|
199
|
+
"middleware",
|
|
200
|
+
"inputValidator",
|
|
201
|
+
"client",
|
|
202
|
+
"server",
|
|
203
|
+
"handler"
|
|
204
|
+
];
|
|
205
|
+
const TANSTACK_REDIRECT_FUNCTIONS = new Set(["redirect", "notFound"]);
|
|
206
|
+
const TANSTACK_SERVER_FN_FILE_PATTERN = /\.functions(\.[jt]sx?)?$/;
|
|
207
|
+
const SEQUENTIAL_AWAIT_THRESHOLD_FOR_LOADER = 2;
|
|
208
|
+
const TANSTACK_QUERY_HOOKS = new Set([
|
|
209
|
+
"useQuery",
|
|
210
|
+
"useInfiniteQuery",
|
|
211
|
+
"useSuspenseQuery",
|
|
212
|
+
"useSuspenseInfiniteQuery"
|
|
213
|
+
]);
|
|
214
|
+
const TANSTACK_MUTATION_HOOKS = new Set(["useMutation"]);
|
|
215
|
+
const TANSTACK_QUERY_CLIENT_CLASS = "QueryClient";
|
|
216
|
+
const STABLE_HOOK_WRAPPERS = new Set([
|
|
217
|
+
"useState",
|
|
218
|
+
"useMemo",
|
|
219
|
+
"useRef"
|
|
220
|
+
]);
|
|
221
|
+
const SCRIPT_LOADING_ATTRIBUTES = new Set(["defer", "async"]);
|
|
172
222
|
const TRIVIAL_INITIALIZER_NAMES = new Set([
|
|
173
223
|
"Boolean",
|
|
174
224
|
"String",
|
|
@@ -355,7 +405,7 @@ const isSimpleExpression = (node) => {
|
|
|
355
405
|
case "TemplateLiteral": return true;
|
|
356
406
|
case "BinaryExpression": return isSimpleExpression(node.left) && isSimpleExpression(node.right);
|
|
357
407
|
case "UnaryExpression": return isSimpleExpression(node.argument);
|
|
358
|
-
case "MemberExpression": return !node.computed;
|
|
408
|
+
case "MemberExpression": return !node.computed && isSimpleExpression(node.object);
|
|
359
409
|
case "ConditionalExpression": return isSimpleExpression(node.test) && isSimpleExpression(node.consequent) && isSimpleExpression(node.alternate);
|
|
360
410
|
default: return false;
|
|
361
411
|
}
|
|
@@ -673,6 +723,10 @@ const jsCombineIterations = { create: (context) => ({ CallExpression(node) {
|
|
|
673
723
|
if (innerCall?.type !== "CallExpression" || innerCall.callee?.type !== "MemberExpression" || innerCall.callee.property?.type !== "Identifier") return;
|
|
674
724
|
const innerMethod = innerCall.callee.property.name;
|
|
675
725
|
if (!CHAINABLE_ITERATION_METHODS.has(innerMethod)) return;
|
|
726
|
+
if (innerMethod === "map" && outerMethod === "filter") {
|
|
727
|
+
const filterArgument = node.arguments?.[0];
|
|
728
|
+
if (filterArgument?.type === "Identifier" && filterArgument.name === "Boolean" || filterArgument?.type === "ArrowFunctionExpression" && filterArgument.params?.length === 1 && filterArgument.body?.type === "Identifier" && filterArgument.params[0]?.type === "Identifier" && filterArgument.body.name === filterArgument.params[0].name) return;
|
|
729
|
+
}
|
|
676
730
|
context.report({
|
|
677
731
|
node,
|
|
678
732
|
message: `.${innerMethod}().${outerMethod}() iterates the array twice — combine into a single loop with .reduce() or for...of`
|
|
@@ -798,6 +852,21 @@ const reportIfIndependent = (statements, context) => {
|
|
|
798
852
|
message: `${statements.length} sequential await statements that appear independent — use Promise.all() for parallel execution`
|
|
799
853
|
});
|
|
800
854
|
};
|
|
855
|
+
const jsFlatmapFilter = { create: (context) => ({ CallExpression(node) {
|
|
856
|
+
if (node.callee?.type !== "MemberExpression" || node.callee.property?.type !== "Identifier") return;
|
|
857
|
+
if (node.callee.property.name !== "filter") return;
|
|
858
|
+
const filterArgument = node.arguments?.[0];
|
|
859
|
+
if (!filterArgument) return;
|
|
860
|
+
const isIdentityArrow = filterArgument.type === "ArrowFunctionExpression" && filterArgument.params?.length === 1 && filterArgument.body?.type === "Identifier" && filterArgument.params[0]?.type === "Identifier" && filterArgument.body.name === filterArgument.params[0].name;
|
|
861
|
+
if (!(filterArgument.type === "Identifier" && filterArgument.name === "Boolean" || isIdentityArrow)) return;
|
|
862
|
+
const innerCall = node.callee.object;
|
|
863
|
+
if (innerCall?.type !== "CallExpression" || innerCall.callee?.type !== "MemberExpression" || innerCall.callee.property?.type !== "Identifier") return;
|
|
864
|
+
if (innerCall.callee.property.name !== "map") return;
|
|
865
|
+
context.report({
|
|
866
|
+
node,
|
|
867
|
+
message: ".map().filter(Boolean) iterates twice — use .flatMap() to transform and filter in a single pass"
|
|
868
|
+
});
|
|
869
|
+
} }) };
|
|
801
870
|
|
|
802
871
|
//#endregion
|
|
803
872
|
//#region src/plugin/rules/nextjs.ts
|
|
@@ -888,32 +957,37 @@ const nextjsMissingMetadata = { create: (context) => ({ Program(programNode) {
|
|
|
888
957
|
message: "Page without metadata or generateMetadata export — hurts SEO"
|
|
889
958
|
});
|
|
890
959
|
} }) };
|
|
891
|
-
const describeClientSideNavigation = (node) => {
|
|
960
|
+
const describeClientSideNavigation = (node, isPagesRouterFile) => {
|
|
961
|
+
const redirectGuidance = isPagesRouterFile ? "handle navigation in an event handler, getServerSideProps redirect, or middleware" : "use redirect() from next/navigation or handle navigation in an event handler";
|
|
892
962
|
if (node.type === "CallExpression" && node.callee?.type === "MemberExpression") {
|
|
893
963
|
const objectName = node.callee.object?.type === "Identifier" ? node.callee.object.name : null;
|
|
894
964
|
const methodName = node.callee.property?.type === "Identifier" ? node.callee.property.name : null;
|
|
895
|
-
if (objectName === "router" && (methodName === "push" || methodName === "replace")) return `router.${methodName}() in useEffect —
|
|
965
|
+
if (objectName === "router" && (methodName === "push" || methodName === "replace")) return `router.${methodName}() in useEffect — ${redirectGuidance}`;
|
|
896
966
|
}
|
|
897
967
|
if (node.type === "AssignmentExpression" && node.left?.type === "MemberExpression") {
|
|
898
968
|
const objectName = node.left.object?.type === "Identifier" ? node.left.object.name : null;
|
|
899
969
|
const propertyName = node.left.property?.type === "Identifier" ? node.left.property.name : null;
|
|
900
|
-
if (objectName === "window" && propertyName === "location") return
|
|
901
|
-
if (objectName === "location" && propertyName === "href") return
|
|
970
|
+
if (objectName === "window" && propertyName === "location") return `window.location assignment in useEffect — ${redirectGuidance}`;
|
|
971
|
+
if (objectName === "location" && propertyName === "href") return `location.href assignment in useEffect — ${redirectGuidance}`;
|
|
902
972
|
}
|
|
903
973
|
return null;
|
|
904
974
|
};
|
|
905
|
-
const nextjsNoClientSideRedirect = { create: (context) =>
|
|
906
|
-
|
|
907
|
-
const
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
const
|
|
911
|
-
if (
|
|
912
|
-
|
|
913
|
-
|
|
975
|
+
const nextjsNoClientSideRedirect = { create: (context) => {
|
|
976
|
+
const filename = context.getFilename?.() ?? "";
|
|
977
|
+
const isPagesRouterFile = PAGES_DIRECTORY_PATTERN.test(filename);
|
|
978
|
+
return { CallExpression(node) {
|
|
979
|
+
if (!isHookCall(node, EFFECT_HOOK_NAMES)) return;
|
|
980
|
+
const callback = getEffectCallback(node);
|
|
981
|
+
if (!callback) return;
|
|
982
|
+
walkAst(callback, (child) => {
|
|
983
|
+
const navigationDescription = describeClientSideNavigation(child, isPagesRouterFile);
|
|
984
|
+
if (navigationDescription) context.report({
|
|
985
|
+
node: child,
|
|
986
|
+
message: navigationDescription
|
|
987
|
+
});
|
|
914
988
|
});
|
|
915
|
-
}
|
|
916
|
-
} }
|
|
989
|
+
} };
|
|
990
|
+
} };
|
|
917
991
|
const nextjsNoRedirectInTryCatch = { create: (context) => {
|
|
918
992
|
let tryCatchDepth = 0;
|
|
919
993
|
return {
|
|
@@ -1260,6 +1334,19 @@ const renderingHydrationNoFlicker = { create: (context) => ({ CallExpression(nod
|
|
|
1260
1334
|
message: "useEffect(setState, []) on mount causes a flash — consider useSyncExternalStore or suppressHydrationWarning"
|
|
1261
1335
|
});
|
|
1262
1336
|
} }) };
|
|
1337
|
+
const renderingScriptDeferAsync = { create: (context) => ({ JSXOpeningElement(node) {
|
|
1338
|
+
if (node.name?.type !== "JSXIdentifier" || node.name.name !== "script") return;
|
|
1339
|
+
const attributes = node.attributes ?? [];
|
|
1340
|
+
if (!attributes.some((attr) => attr.type === "JSXAttribute" && attr.name?.type === "JSXIdentifier" && attr.name.name === "src")) return;
|
|
1341
|
+
const typeAttribute = attributes.find((attr) => attr.type === "JSXAttribute" && attr.name?.type === "JSXIdentifier" && attr.name.name === "type");
|
|
1342
|
+
const typeValue = typeAttribute?.value?.type === "Literal" ? typeAttribute.value.value : null;
|
|
1343
|
+
if (typeof typeValue === "string" && !EXECUTABLE_SCRIPT_TYPES.has(typeValue)) return;
|
|
1344
|
+
if (typeValue === "module") return;
|
|
1345
|
+
if (!attributes.some((attr) => attr.type === "JSXAttribute" && attr.name?.type === "JSXIdentifier" && SCRIPT_LOADING_ATTRIBUTES.has(attr.name.name))) context.report({
|
|
1346
|
+
node,
|
|
1347
|
+
message: "<script src> without defer or async — blocks HTML parsing and delays First Contentful Paint. Add defer for DOM-dependent scripts or async for independent ones"
|
|
1348
|
+
});
|
|
1349
|
+
} }) };
|
|
1263
1350
|
|
|
1264
1351
|
//#endregion
|
|
1265
1352
|
//#region src/plugin/rules/react-native.ts
|
|
@@ -1424,6 +1511,119 @@ const rnNoSingleElementStyleArray = { create: (context) => ({ JSXAttribute(node)
|
|
|
1424
1511
|
});
|
|
1425
1512
|
} }) };
|
|
1426
1513
|
|
|
1514
|
+
//#endregion
|
|
1515
|
+
//#region src/plugin/rules/tanstack-query.ts
|
|
1516
|
+
const queryStableQueryClient = { create: (context) => {
|
|
1517
|
+
let componentDepth = 0;
|
|
1518
|
+
let stableHookDepth = 0;
|
|
1519
|
+
return {
|
|
1520
|
+
FunctionDeclaration(node) {
|
|
1521
|
+
if (node.id?.name && UPPERCASE_PATTERN.test(node.id.name)) componentDepth++;
|
|
1522
|
+
},
|
|
1523
|
+
"FunctionDeclaration:exit"(node) {
|
|
1524
|
+
if (node.id?.name && UPPERCASE_PATTERN.test(node.id.name)) componentDepth--;
|
|
1525
|
+
},
|
|
1526
|
+
VariableDeclarator(node) {
|
|
1527
|
+
if (node.id?.type === "Identifier" && UPPERCASE_PATTERN.test(node.id.name) && (node.init?.type === "ArrowFunctionExpression" || node.init?.type === "FunctionExpression")) componentDepth++;
|
|
1528
|
+
},
|
|
1529
|
+
"VariableDeclarator:exit"(node) {
|
|
1530
|
+
if (node.id?.type === "Identifier" && UPPERCASE_PATTERN.test(node.id.name) && (node.init?.type === "ArrowFunctionExpression" || node.init?.type === "FunctionExpression")) componentDepth--;
|
|
1531
|
+
},
|
|
1532
|
+
CallExpression(node) {
|
|
1533
|
+
if (node.callee?.type === "Identifier" && STABLE_HOOK_WRAPPERS.has(node.callee.name)) stableHookDepth++;
|
|
1534
|
+
},
|
|
1535
|
+
"CallExpression:exit"(node) {
|
|
1536
|
+
if (node.callee?.type === "Identifier" && STABLE_HOOK_WRAPPERS.has(node.callee.name)) stableHookDepth--;
|
|
1537
|
+
},
|
|
1538
|
+
NewExpression(node) {
|
|
1539
|
+
if (componentDepth <= 0) return;
|
|
1540
|
+
if (stableHookDepth > 0) return;
|
|
1541
|
+
if (node.callee?.type !== "Identifier" || node.callee.name !== TANSTACK_QUERY_CLIENT_CLASS) return;
|
|
1542
|
+
context.report({
|
|
1543
|
+
node,
|
|
1544
|
+
message: "new QueryClient() inside a component — creates a new cache on every render. Move to module scope or wrap in useState(() => new QueryClient())"
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
};
|
|
1548
|
+
} };
|
|
1549
|
+
const queryNoRestDestructuring = { create: (context) => ({ VariableDeclarator(node) {
|
|
1550
|
+
if (node.id?.type !== "ObjectPattern") return;
|
|
1551
|
+
if (!node.init || node.init.type !== "CallExpression") return;
|
|
1552
|
+
const calleeName = node.init.callee?.type === "Identifier" ? node.init.callee.name : null;
|
|
1553
|
+
if (!calleeName || !TANSTACK_QUERY_HOOKS.has(calleeName)) return;
|
|
1554
|
+
if (node.id.properties?.some((property) => property.type === "RestElement")) context.report({
|
|
1555
|
+
node: node.id,
|
|
1556
|
+
message: `Rest destructuring on ${calleeName}() result — subscribes to all fields and causes unnecessary re-renders. Destructure only the fields you need`
|
|
1557
|
+
});
|
|
1558
|
+
} }) };
|
|
1559
|
+
const queryNoVoidQueryFn = { create: (context) => ({ CallExpression(node) {
|
|
1560
|
+
const calleeName = node.callee?.type === "Identifier" ? node.callee.name : null;
|
|
1561
|
+
if (!calleeName || !TANSTACK_QUERY_HOOKS.has(calleeName)) return;
|
|
1562
|
+
const optionsArgument = node.arguments?.[0];
|
|
1563
|
+
if (!optionsArgument || optionsArgument.type !== "ObjectExpression") return;
|
|
1564
|
+
const queryFnProperty = optionsArgument.properties?.find((property) => property.type === "Property" && property.key?.type === "Identifier" && property.key.name === "queryFn");
|
|
1565
|
+
if (!queryFnProperty?.value) return;
|
|
1566
|
+
const queryFnValue = queryFnProperty.value;
|
|
1567
|
+
if (queryFnValue.type === "ArrowFunctionExpression" && queryFnValue.body?.type !== "BlockStatement") return;
|
|
1568
|
+
if (queryFnValue.type === "ArrowFunctionExpression" || queryFnValue.type === "FunctionExpression") {
|
|
1569
|
+
const body = queryFnValue.body;
|
|
1570
|
+
if (body?.type !== "BlockStatement") return;
|
|
1571
|
+
if ((body.body ?? []).length === 0) context.report({
|
|
1572
|
+
node: queryFnProperty,
|
|
1573
|
+
message: "Empty queryFn — query functions must return a value. Use the enabled option to conditionally disable the query instead"
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
} }) };
|
|
1577
|
+
const queryNoQueryInEffect = { create: (context) => ({ CallExpression(node) {
|
|
1578
|
+
if (!isHookCall(node, EFFECT_HOOK_NAMES)) return;
|
|
1579
|
+
const callback = getEffectCallback(node);
|
|
1580
|
+
if (!callback) return;
|
|
1581
|
+
walkAst(callback, (child) => {
|
|
1582
|
+
if (child.type !== "CallExpression") return;
|
|
1583
|
+
if ((child.callee?.type === "Identifier" ? child.callee.name : null) === "refetch") context.report({
|
|
1584
|
+
node: child,
|
|
1585
|
+
message: "refetch() inside useEffect — React Query manages refetching automatically. Use queryKey dependencies or the enabled option instead"
|
|
1586
|
+
});
|
|
1587
|
+
});
|
|
1588
|
+
} }) };
|
|
1589
|
+
const queryMutationMissingInvalidation = { create: (context) => ({ CallExpression(node) {
|
|
1590
|
+
const calleeName = node.callee?.type === "Identifier" ? node.callee.name : null;
|
|
1591
|
+
if (!calleeName || !TANSTACK_MUTATION_HOOKS.has(calleeName)) return;
|
|
1592
|
+
const optionsArgument = node.arguments?.[0];
|
|
1593
|
+
if (!optionsArgument || optionsArgument.type !== "ObjectExpression") return;
|
|
1594
|
+
if (!optionsArgument.properties?.some((property) => property.type === "Property" && property.key?.type === "Identifier" && property.key.name === "mutationFn")) return;
|
|
1595
|
+
let hasInvalidation = false;
|
|
1596
|
+
walkAst(optionsArgument, (child) => {
|
|
1597
|
+
if (hasInvalidation) return;
|
|
1598
|
+
if (child.type === "CallExpression" && child.callee?.type === "MemberExpression" && child.callee.property?.type === "Identifier" && child.callee.property.name === "invalidateQueries") hasInvalidation = true;
|
|
1599
|
+
});
|
|
1600
|
+
if (!hasInvalidation) context.report({
|
|
1601
|
+
node,
|
|
1602
|
+
message: "useMutation without invalidateQueries — stale data may remain cached after the mutation. Add onSuccess with queryClient.invalidateQueries()"
|
|
1603
|
+
});
|
|
1604
|
+
} }) };
|
|
1605
|
+
const queryNoUseQueryForMutation = { create: (context) => ({ CallExpression(node) {
|
|
1606
|
+
const calleeName = node.callee?.type === "Identifier" ? node.callee.name : null;
|
|
1607
|
+
if (!calleeName || !TANSTACK_QUERY_HOOKS.has(calleeName)) return;
|
|
1608
|
+
const optionsArgument = node.arguments?.[0];
|
|
1609
|
+
if (!optionsArgument || optionsArgument.type !== "ObjectExpression") return;
|
|
1610
|
+
const queryFnProperty = optionsArgument.properties?.find((property) => property.type === "Property" && property.key?.type === "Identifier" && property.key.name === "queryFn");
|
|
1611
|
+
if (!queryFnProperty?.value) return;
|
|
1612
|
+
let hasMutatingFetch = false;
|
|
1613
|
+
walkAst(queryFnProperty.value, (child) => {
|
|
1614
|
+
if (hasMutatingFetch) return;
|
|
1615
|
+
if (child.type !== "CallExpression") return;
|
|
1616
|
+
if (child.callee?.type !== "Identifier" || child.callee.name !== "fetch") return;
|
|
1617
|
+
const optionsArg = child.arguments?.[1];
|
|
1618
|
+
if (!optionsArg || optionsArg.type !== "ObjectExpression") return;
|
|
1619
|
+
if (optionsArg.properties?.find((property) => property.type === "Property" && property.key?.type === "Identifier" && property.key.name === "method" && property.value?.type === "Literal" && typeof property.value.value === "string" && MUTATING_HTTP_METHODS.has(property.value.value.toUpperCase()))) hasMutatingFetch = true;
|
|
1620
|
+
});
|
|
1621
|
+
if (hasMutatingFetch) context.report({
|
|
1622
|
+
node,
|
|
1623
|
+
message: `${calleeName}() with a mutating fetch (POST/PUT/DELETE) — use useMutation() instead, which provides onSuccess/onError callbacks and doesn't auto-refetch`
|
|
1624
|
+
});
|
|
1625
|
+
} }) };
|
|
1626
|
+
|
|
1427
1627
|
//#endregion
|
|
1428
1628
|
//#region src/plugin/rules/security.ts
|
|
1429
1629
|
const noEval = { create: (context) => ({
|
|
@@ -1522,6 +1722,335 @@ const serverAfterNonblocking = { create: (context) => {
|
|
|
1522
1722
|
};
|
|
1523
1723
|
} };
|
|
1524
1724
|
|
|
1725
|
+
//#endregion
|
|
1726
|
+
//#region src/plugin/rules/tanstack-start.ts
|
|
1727
|
+
const getRouteOptionsObject = (node) => {
|
|
1728
|
+
if (node.type !== "CallExpression") return null;
|
|
1729
|
+
const callee = node.callee;
|
|
1730
|
+
if (callee?.type === "CallExpression" && callee.callee?.type === "Identifier") {
|
|
1731
|
+
if (!TANSTACK_ROUTE_CREATION_FUNCTIONS.has(callee.callee.name)) return null;
|
|
1732
|
+
const optionsArgument = node.arguments?.[0];
|
|
1733
|
+
if (optionsArgument?.type === "ObjectExpression") return optionsArgument;
|
|
1734
|
+
return null;
|
|
1735
|
+
}
|
|
1736
|
+
if (callee?.type === "Identifier") {
|
|
1737
|
+
if (!TANSTACK_ROUTE_CREATION_FUNCTIONS.has(callee.name)) return null;
|
|
1738
|
+
const optionsArgument = node.arguments?.[0];
|
|
1739
|
+
if (optionsArgument?.type === "ObjectExpression") return optionsArgument;
|
|
1740
|
+
return null;
|
|
1741
|
+
}
|
|
1742
|
+
return null;
|
|
1743
|
+
};
|
|
1744
|
+
const getPropertyKeyName = (property) => {
|
|
1745
|
+
if (property.type !== "Property" && property.type !== "MethodDefinition") return null;
|
|
1746
|
+
if (property.key?.type === "Identifier") return property.key.name;
|
|
1747
|
+
if (property.key?.type === "Literal") return String(property.key.value);
|
|
1748
|
+
return null;
|
|
1749
|
+
};
|
|
1750
|
+
const walkServerFnChain = (outerNode) => {
|
|
1751
|
+
const result = {
|
|
1752
|
+
isServerFnChain: false,
|
|
1753
|
+
specifiedMethod: null,
|
|
1754
|
+
hasInputValidator: false
|
|
1755
|
+
};
|
|
1756
|
+
let currentNode = outerNode.callee?.object;
|
|
1757
|
+
while (currentNode?.type === "CallExpression") {
|
|
1758
|
+
const calleeName = getCalleeName(currentNode);
|
|
1759
|
+
if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) {
|
|
1760
|
+
result.isServerFnChain = true;
|
|
1761
|
+
const optionsArgument = currentNode.arguments?.[0];
|
|
1762
|
+
if (optionsArgument?.type === "ObjectExpression") {
|
|
1763
|
+
for (const property of optionsArgument.properties ?? []) if (property.key?.type === "Identifier" && property.key.name === "method" && property.value?.type === "Literal" && typeof property.value.value === "string") result.specifiedMethod = property.value.value;
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
if (calleeName === "inputValidator") result.hasInputValidator = true;
|
|
1767
|
+
if (currentNode.callee?.type === "MemberExpression") currentNode = currentNode.callee.object;
|
|
1768
|
+
else break;
|
|
1769
|
+
}
|
|
1770
|
+
return result;
|
|
1771
|
+
};
|
|
1772
|
+
const tanstackStartRoutePropertyOrder = { create: (context) => ({ CallExpression(node) {
|
|
1773
|
+
const optionsObject = getRouteOptionsObject(node);
|
|
1774
|
+
if (!optionsObject) return;
|
|
1775
|
+
const properties = optionsObject.properties ?? [];
|
|
1776
|
+
const orderedPropertyNames = [];
|
|
1777
|
+
for (const property of properties) {
|
|
1778
|
+
const propertyName = getPropertyKeyName(property);
|
|
1779
|
+
if (propertyName !== null) orderedPropertyNames.push(propertyName);
|
|
1780
|
+
}
|
|
1781
|
+
const sensitiveProperties = orderedPropertyNames.filter((propertyName) => TANSTACK_ROUTE_PROPERTY_ORDER.includes(propertyName));
|
|
1782
|
+
let lastIndex = -1;
|
|
1783
|
+
for (const propertyName of sensitiveProperties) {
|
|
1784
|
+
const currentIndex = TANSTACK_ROUTE_PROPERTY_ORDER.indexOf(propertyName);
|
|
1785
|
+
if (currentIndex < lastIndex) {
|
|
1786
|
+
const expectedBefore = TANSTACK_ROUTE_PROPERTY_ORDER[lastIndex];
|
|
1787
|
+
context.report({
|
|
1788
|
+
node: optionsObject,
|
|
1789
|
+
message: `Route property "${propertyName}" must come before "${expectedBefore}" — wrong order breaks TypeScript type inference`
|
|
1790
|
+
});
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
lastIndex = currentIndex;
|
|
1794
|
+
}
|
|
1795
|
+
} }) };
|
|
1796
|
+
const tanstackStartNoDirectFetchInLoader = { create: (context) => ({ CallExpression(node) {
|
|
1797
|
+
const optionsObject = getRouteOptionsObject(node);
|
|
1798
|
+
if (!optionsObject) return;
|
|
1799
|
+
const properties = optionsObject.properties ?? [];
|
|
1800
|
+
for (const property of properties) {
|
|
1801
|
+
if (getPropertyKeyName(property) !== "loader") continue;
|
|
1802
|
+
walkAst(property.value ?? property, (child) => {
|
|
1803
|
+
if (child.type !== "CallExpression") return;
|
|
1804
|
+
if (child.callee?.type === "Identifier" && child.callee.name === "fetch") context.report({
|
|
1805
|
+
node: child,
|
|
1806
|
+
message: "Direct fetch() in route loader — use createServerFn() for type-safe server logic with automatic RPC"
|
|
1807
|
+
});
|
|
1808
|
+
});
|
|
1809
|
+
}
|
|
1810
|
+
} }) };
|
|
1811
|
+
const tanstackStartServerFnValidateInput = { create: (context) => ({ CallExpression(node) {
|
|
1812
|
+
if (node.callee?.type !== "MemberExpression") return;
|
|
1813
|
+
if (node.callee.property?.type !== "Identifier") return;
|
|
1814
|
+
if (node.callee.property.name !== "handler") return;
|
|
1815
|
+
const chainInfo = walkServerFnChain(node);
|
|
1816
|
+
if (!chainInfo.isServerFnChain) return;
|
|
1817
|
+
const handlerFunction = node.arguments?.[0];
|
|
1818
|
+
if (!handlerFunction) return;
|
|
1819
|
+
let accessesData = false;
|
|
1820
|
+
walkAst(handlerFunction, (child) => {
|
|
1821
|
+
if (child.type === "MemberExpression" && child.property?.type === "Identifier" && child.property.name === "data") accessesData = true;
|
|
1822
|
+
if (child.type === "ObjectPattern" && child.properties?.some((property) => property.key?.type === "Identifier" && property.key.name === "data")) accessesData = true;
|
|
1823
|
+
});
|
|
1824
|
+
if (accessesData && !chainInfo.hasInputValidator) context.report({
|
|
1825
|
+
node,
|
|
1826
|
+
message: "Server function handler accesses data without inputValidator() — validate inputs crossing the network boundary"
|
|
1827
|
+
});
|
|
1828
|
+
} }) };
|
|
1829
|
+
const tanstackStartNoUseEffectFetch = { create: (context) => ({ CallExpression(node) {
|
|
1830
|
+
const filename = context.getFilename?.() ?? "";
|
|
1831
|
+
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
1832
|
+
if (node.callee?.type !== "Identifier") return;
|
|
1833
|
+
if (node.callee.name !== "useEffect" && node.callee.name !== "useLayoutEffect") return;
|
|
1834
|
+
const callback = node.arguments?.[0];
|
|
1835
|
+
if (!callback) return;
|
|
1836
|
+
let hasFetchCall = false;
|
|
1837
|
+
walkAst(callback, (child) => {
|
|
1838
|
+
if (hasFetchCall) return;
|
|
1839
|
+
if (child.type === "CallExpression" && child.callee?.type === "Identifier" && child.callee.name === "fetch") hasFetchCall = true;
|
|
1840
|
+
});
|
|
1841
|
+
if (hasFetchCall) context.report({
|
|
1842
|
+
node,
|
|
1843
|
+
message: "fetch() inside useEffect in a route file — use the route loader or createServerFn() instead"
|
|
1844
|
+
});
|
|
1845
|
+
} }) };
|
|
1846
|
+
const tanstackStartMissingHeadContent = { create: (context) => {
|
|
1847
|
+
let hasHeadContentElement = false;
|
|
1848
|
+
return {
|
|
1849
|
+
JSXOpeningElement(node) {
|
|
1850
|
+
const filename = context.getFilename?.() ?? "";
|
|
1851
|
+
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
1852
|
+
if (node.name?.type === "JSXIdentifier" && node.name.name === "HeadContent") hasHeadContentElement = true;
|
|
1853
|
+
},
|
|
1854
|
+
"Program:exit"(programNode) {
|
|
1855
|
+
const filename = context.getFilename?.() ?? "";
|
|
1856
|
+
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
1857
|
+
if (!hasHeadContentElement) context.report({
|
|
1858
|
+
node: programNode,
|
|
1859
|
+
message: "Root route (__root) without <HeadContent /> — route head() meta tags won't render"
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
};
|
|
1863
|
+
} };
|
|
1864
|
+
const tanstackStartNoAnchorElement = { create: (context) => ({ JSXOpeningElement(node) {
|
|
1865
|
+
const filename = context.getFilename?.() ?? "";
|
|
1866
|
+
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
1867
|
+
if (node.name?.type !== "JSXIdentifier" || node.name.name !== "a") return;
|
|
1868
|
+
const hrefAttribute = (node.attributes ?? []).find((attribute) => attribute.type === "JSXAttribute" && attribute.name?.type === "JSXIdentifier" && attribute.name.name === "href");
|
|
1869
|
+
if (!hrefAttribute?.value) return;
|
|
1870
|
+
let hrefValue = null;
|
|
1871
|
+
if (hrefAttribute.value.type === "Literal") hrefValue = hrefAttribute.value.value;
|
|
1872
|
+
else if (hrefAttribute.value.type === "JSXExpressionContainer" && hrefAttribute.value.expression?.type === "Literal") hrefValue = hrefAttribute.value.expression.value;
|
|
1873
|
+
if (typeof hrefValue === "string" && hrefValue.startsWith("/")) context.report({
|
|
1874
|
+
node,
|
|
1875
|
+
message: "Use <Link> from @tanstack/react-router instead of <a> for internal navigation — enables type-safe routing and preloading"
|
|
1876
|
+
});
|
|
1877
|
+
} }) };
|
|
1878
|
+
const tanstackStartServerFnMethodOrder = { create: (context) => ({ CallExpression(node) {
|
|
1879
|
+
if (node.callee?.type !== "MemberExpression") return;
|
|
1880
|
+
const methodNames = [];
|
|
1881
|
+
let currentNode = node;
|
|
1882
|
+
while (currentNode?.type === "CallExpression" && currentNode.callee?.type === "MemberExpression") {
|
|
1883
|
+
const methodName = currentNode.callee.property?.type === "Identifier" ? currentNode.callee.property.name : null;
|
|
1884
|
+
if (methodName) methodNames.unshift(methodName);
|
|
1885
|
+
currentNode = currentNode.callee.object;
|
|
1886
|
+
}
|
|
1887
|
+
if (currentNode?.type === "CallExpression" && currentNode.callee?.type === "Identifier") {
|
|
1888
|
+
if (!TANSTACK_SERVER_FN_NAMES.has(currentNode.callee.name)) return;
|
|
1889
|
+
} else return;
|
|
1890
|
+
const ownMethodName = node.callee.property?.type === "Identifier" ? node.callee.property.name : null;
|
|
1891
|
+
if (methodNames[methodNames.length - 1] !== ownMethodName) return;
|
|
1892
|
+
const orderSensitiveMethods = methodNames.filter((name) => TANSTACK_MIDDLEWARE_METHOD_ORDER.includes(name));
|
|
1893
|
+
let lastIndex = -1;
|
|
1894
|
+
for (const methodName of orderSensitiveMethods) {
|
|
1895
|
+
const currentIndex = TANSTACK_MIDDLEWARE_METHOD_ORDER.indexOf(methodName);
|
|
1896
|
+
if (currentIndex < lastIndex) {
|
|
1897
|
+
const expectedBefore = TANSTACK_MIDDLEWARE_METHOD_ORDER[lastIndex];
|
|
1898
|
+
context.report({
|
|
1899
|
+
node,
|
|
1900
|
+
message: `Server function method .${methodName}() must come before .${expectedBefore}() — wrong order breaks type inference`
|
|
1901
|
+
});
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
lastIndex = currentIndex;
|
|
1905
|
+
}
|
|
1906
|
+
} }) };
|
|
1907
|
+
const tanstackStartNoNavigateInRender = { create: (context) => {
|
|
1908
|
+
let effectDepth = 0;
|
|
1909
|
+
let eventHandlerDepth = 0;
|
|
1910
|
+
return {
|
|
1911
|
+
CallExpression(node) {
|
|
1912
|
+
const filename = context.getFilename?.() ?? "";
|
|
1913
|
+
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
1914
|
+
if (node.callee?.type === "Identifier" && (node.callee.name === "useEffect" || node.callee.name === "useLayoutEffect")) effectDepth++;
|
|
1915
|
+
if (effectDepth > 0 || eventHandlerDepth > 0) return;
|
|
1916
|
+
if (node.callee?.type === "Identifier" && node.callee.name === "navigate" && node.arguments?.length > 0) context.report({
|
|
1917
|
+
node,
|
|
1918
|
+
message: "navigate() called during render — use redirect() in beforeLoad/loader for route-level redirects"
|
|
1919
|
+
});
|
|
1920
|
+
},
|
|
1921
|
+
"CallExpression:exit"(node) {
|
|
1922
|
+
const filename = context.getFilename?.() ?? "";
|
|
1923
|
+
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
1924
|
+
if (node.callee?.type === "Identifier" && (node.callee.name === "useEffect" || node.callee.name === "useLayoutEffect")) effectDepth--;
|
|
1925
|
+
},
|
|
1926
|
+
JSXAttribute(node) {
|
|
1927
|
+
const filename = context.getFilename?.() ?? "";
|
|
1928
|
+
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
1929
|
+
if (node.name?.type === "JSXIdentifier" && node.name.name.startsWith("on") && UPPERCASE_PATTERN.test(node.name.name.charAt(2))) eventHandlerDepth++;
|
|
1930
|
+
},
|
|
1931
|
+
"JSXAttribute:exit"(node) {
|
|
1932
|
+
const filename = context.getFilename?.() ?? "";
|
|
1933
|
+
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
1934
|
+
if (node.name?.type === "JSXIdentifier" && node.name.name.startsWith("on") && UPPERCASE_PATTERN.test(node.name.name.charAt(2))) eventHandlerDepth--;
|
|
1935
|
+
}
|
|
1936
|
+
};
|
|
1937
|
+
} };
|
|
1938
|
+
const tanstackStartNoDynamicServerFnImport = { create: (context) => ({ ImportExpression(node) {
|
|
1939
|
+
const source = node.source;
|
|
1940
|
+
if (!source) return;
|
|
1941
|
+
let importPath = null;
|
|
1942
|
+
if (source.type === "Literal" && typeof source.value === "string") importPath = source.value;
|
|
1943
|
+
else if (source.type === "TemplateLiteral" && source.quasis?.length === 1) importPath = source.quasis[0].value?.raw ?? null;
|
|
1944
|
+
if (importPath && TANSTACK_SERVER_FN_FILE_PATTERN.test(importPath)) context.report({
|
|
1945
|
+
node,
|
|
1946
|
+
message: "Dynamic import of server functions file — use static imports so the bundler can replace server code with RPC stubs"
|
|
1947
|
+
});
|
|
1948
|
+
} }) };
|
|
1949
|
+
const tanstackStartNoUseServerInHandler = { create: (context) => ({ CallExpression(node) {
|
|
1950
|
+
if (node.callee?.type !== "MemberExpression") return;
|
|
1951
|
+
if (node.callee.property?.type !== "Identifier" || node.callee.property.name !== "handler") return;
|
|
1952
|
+
const handlerFunction = node.arguments?.[0];
|
|
1953
|
+
if (!handlerFunction || handlerFunction.type !== "ArrowFunctionExpression" && handlerFunction.type !== "FunctionExpression") return;
|
|
1954
|
+
const body = handlerFunction.body;
|
|
1955
|
+
if (body?.type !== "BlockStatement") return;
|
|
1956
|
+
if (body.body?.some((statement) => statement.type === "ExpressionStatement" && (statement.directive === "use server" || statement.expression?.type === "Literal" && statement.expression.value === "use server"))) context.report({
|
|
1957
|
+
node: handlerFunction,
|
|
1958
|
+
message: "\"use server\" inside createServerFn handler — TanStack Start handles this automatically, remove the directive"
|
|
1959
|
+
});
|
|
1960
|
+
} }) };
|
|
1961
|
+
const tanstackStartNoSecretsInLoader = { create: (context) => ({ CallExpression(node) {
|
|
1962
|
+
const optionsObject = getRouteOptionsObject(node);
|
|
1963
|
+
if (!optionsObject) return;
|
|
1964
|
+
const properties = optionsObject.properties ?? [];
|
|
1965
|
+
for (const property of properties) {
|
|
1966
|
+
const keyName = getPropertyKeyName(property);
|
|
1967
|
+
if (keyName !== "loader" && keyName !== "beforeLoad") continue;
|
|
1968
|
+
walkAst(property.value ?? property, (child) => {
|
|
1969
|
+
if (child.type !== "MemberExpression") return;
|
|
1970
|
+
if (child.object?.type === "MemberExpression" && child.object.object?.type === "Identifier" && child.object.object.name === "process" && child.object.property?.type === "Identifier" && child.object.property.name === "env") {
|
|
1971
|
+
const envVarName = child.property?.type === "Identifier" ? child.property.name : null;
|
|
1972
|
+
if (envVarName && !envVarName.startsWith("VITE_")) context.report({
|
|
1973
|
+
node: child,
|
|
1974
|
+
message: `process.env.${envVarName} in ${keyName} — loaders are isomorphic and may leak secrets to the client. Move to a createServerFn()`
|
|
1975
|
+
});
|
|
1976
|
+
}
|
|
1977
|
+
});
|
|
1978
|
+
}
|
|
1979
|
+
} }) };
|
|
1980
|
+
const tanstackStartGetMutation = { create: (context) => ({ CallExpression(node) {
|
|
1981
|
+
if (node.callee?.type !== "MemberExpression") return;
|
|
1982
|
+
if (node.callee.property?.type !== "Identifier" || node.callee.property.name !== "handler") return;
|
|
1983
|
+
const chainInfo = walkServerFnChain(node);
|
|
1984
|
+
if (!chainInfo.isServerFnChain) return;
|
|
1985
|
+
if (chainInfo.specifiedMethod && MUTATING_HTTP_METHODS.has(chainInfo.specifiedMethod.toUpperCase())) return;
|
|
1986
|
+
const handlerFunction = node.arguments?.[0];
|
|
1987
|
+
if (!handlerFunction) return;
|
|
1988
|
+
const sideEffect = findSideEffect(handlerFunction);
|
|
1989
|
+
if (sideEffect) context.report({
|
|
1990
|
+
node,
|
|
1991
|
+
message: `GET server function has side effects (${sideEffect}) — use createServerFn({ method: 'POST' }) for mutations`
|
|
1992
|
+
});
|
|
1993
|
+
} }) };
|
|
1994
|
+
const tanstackStartRedirectInTryCatch = { create: (context) => {
|
|
1995
|
+
let tryBlockDepth = 0;
|
|
1996
|
+
let catchClauseDepth = 0;
|
|
1997
|
+
return {
|
|
1998
|
+
TryStatement() {
|
|
1999
|
+
tryBlockDepth++;
|
|
2000
|
+
},
|
|
2001
|
+
"TryStatement:exit"() {
|
|
2002
|
+
tryBlockDepth--;
|
|
2003
|
+
},
|
|
2004
|
+
CatchClause() {
|
|
2005
|
+
catchClauseDepth++;
|
|
2006
|
+
},
|
|
2007
|
+
"CatchClause:exit"() {
|
|
2008
|
+
catchClauseDepth--;
|
|
2009
|
+
},
|
|
2010
|
+
ThrowStatement(node) {
|
|
2011
|
+
if (tryBlockDepth === 0) return;
|
|
2012
|
+
if (catchClauseDepth > 0) return;
|
|
2013
|
+
const argument = node.argument;
|
|
2014
|
+
if (argument?.type !== "CallExpression") return;
|
|
2015
|
+
if (argument.callee?.type !== "Identifier") return;
|
|
2016
|
+
if (!TANSTACK_REDIRECT_FUNCTIONS.has(argument.callee.name)) return;
|
|
2017
|
+
context.report({
|
|
2018
|
+
node,
|
|
2019
|
+
message: `throw ${argument.callee.name}() inside try block — the router catches this internally. Move it outside the try block or re-throw in the catch`
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
};
|
|
2023
|
+
} };
|
|
2024
|
+
const hasTopLevelAwait = (statement) => {
|
|
2025
|
+
if (statement.type === "VariableDeclaration") return statement.declarations?.some((declarator) => declarator.init?.type === "AwaitExpression");
|
|
2026
|
+
if (statement.type === "ExpressionStatement") return statement.expression?.type === "AwaitExpression" || statement.expression?.type === "AssignmentExpression" && statement.expression.right?.type === "AwaitExpression";
|
|
2027
|
+
if (statement.type === "ReturnStatement") return statement.argument?.type === "AwaitExpression";
|
|
2028
|
+
return false;
|
|
2029
|
+
};
|
|
2030
|
+
const tanstackStartLoaderParallelFetch = { create: (context) => ({ CallExpression(node) {
|
|
2031
|
+
const optionsObject = getRouteOptionsObject(node);
|
|
2032
|
+
if (!optionsObject) return;
|
|
2033
|
+
const properties = optionsObject.properties ?? [];
|
|
2034
|
+
for (const property of properties) {
|
|
2035
|
+
if (getPropertyKeyName(property) !== "loader") continue;
|
|
2036
|
+
const loaderValue = property.value;
|
|
2037
|
+
if (!loaderValue || loaderValue.type !== "ArrowFunctionExpression" && loaderValue.type !== "FunctionExpression") continue;
|
|
2038
|
+
const functionBody = loaderValue.body;
|
|
2039
|
+
if (!functionBody || functionBody.type !== "BlockStatement") continue;
|
|
2040
|
+
let sequentialAwaitCount = 0;
|
|
2041
|
+
for (const statement of functionBody.body ?? []) {
|
|
2042
|
+
if (hasTopLevelAwait(statement)) sequentialAwaitCount++;
|
|
2043
|
+
if (sequentialAwaitCount >= SEQUENTIAL_AWAIT_THRESHOLD_FOR_LOADER) {
|
|
2044
|
+
context.report({
|
|
2045
|
+
node: property,
|
|
2046
|
+
message: "Multiple sequential awaits in loader — use Promise.all() to fetch data in parallel and avoid waterfalls"
|
|
2047
|
+
});
|
|
2048
|
+
break;
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
} }) };
|
|
2053
|
+
|
|
1525
2054
|
//#endregion
|
|
1526
2055
|
//#region src/plugin/rules/state-and-effects.ts
|
|
1527
2056
|
const noDerivedStateEffect = { create: (context) => ({ CallExpression(node) {
|
|
@@ -1700,6 +2229,7 @@ const plugin = {
|
|
|
1700
2229
|
"rendering-animate-svg-wrapper": renderingAnimateSvgWrapper,
|
|
1701
2230
|
"no-inline-prop-on-memo-component": noInlinePropOnMemoComponent,
|
|
1702
2231
|
"rendering-hydration-no-flicker": renderingHydrationNoFlicker,
|
|
2232
|
+
"rendering-script-defer-async": renderingScriptDeferAsync,
|
|
1703
2233
|
"no-transition-all": noTransitionAll,
|
|
1704
2234
|
"no-global-css-variable-animation": noGlobalCssVariableAnimation,
|
|
1705
2235
|
"no-large-animated-blur": noLargeAnimatedBlur,
|
|
@@ -1744,6 +2274,7 @@ const plugin = {
|
|
|
1744
2274
|
"js-index-maps": jsIndexMaps,
|
|
1745
2275
|
"js-cache-storage": jsCacheStorage,
|
|
1746
2276
|
"js-early-exit": jsEarlyExit,
|
|
2277
|
+
"js-flatmap-filter": jsFlatmapFilter,
|
|
1747
2278
|
"async-parallel": asyncParallel,
|
|
1748
2279
|
"rn-no-raw-text": rnNoRawText,
|
|
1749
2280
|
"rn-no-deprecated-modules": rnNoDeprecatedModules,
|
|
@@ -1752,7 +2283,27 @@ const plugin = {
|
|
|
1752
2283
|
"rn-no-inline-flatlist-renderitem": rnNoInlineFlatlistRenderitem,
|
|
1753
2284
|
"rn-no-legacy-shadow-styles": rnNoLegacyShadowStyles,
|
|
1754
2285
|
"rn-prefer-reanimated": rnPreferReanimated,
|
|
1755
|
-
"rn-no-single-element-style-array": rnNoSingleElementStyleArray
|
|
2286
|
+
"rn-no-single-element-style-array": rnNoSingleElementStyleArray,
|
|
2287
|
+
"tanstack-start-route-property-order": tanstackStartRoutePropertyOrder,
|
|
2288
|
+
"tanstack-start-no-direct-fetch-in-loader": tanstackStartNoDirectFetchInLoader,
|
|
2289
|
+
"tanstack-start-server-fn-validate-input": tanstackStartServerFnValidateInput,
|
|
2290
|
+
"tanstack-start-no-useeffect-fetch": tanstackStartNoUseEffectFetch,
|
|
2291
|
+
"tanstack-start-missing-head-content": tanstackStartMissingHeadContent,
|
|
2292
|
+
"tanstack-start-no-anchor-element": tanstackStartNoAnchorElement,
|
|
2293
|
+
"tanstack-start-server-fn-method-order": tanstackStartServerFnMethodOrder,
|
|
2294
|
+
"tanstack-start-no-navigate-in-render": tanstackStartNoNavigateInRender,
|
|
2295
|
+
"tanstack-start-no-dynamic-server-fn-import": tanstackStartNoDynamicServerFnImport,
|
|
2296
|
+
"tanstack-start-no-use-server-in-handler": tanstackStartNoUseServerInHandler,
|
|
2297
|
+
"tanstack-start-no-secrets-in-loader": tanstackStartNoSecretsInLoader,
|
|
2298
|
+
"tanstack-start-get-mutation": tanstackStartGetMutation,
|
|
2299
|
+
"tanstack-start-redirect-in-try-catch": tanstackStartRedirectInTryCatch,
|
|
2300
|
+
"tanstack-start-loader-parallel-fetch": tanstackStartLoaderParallelFetch,
|
|
2301
|
+
"query-stable-query-client": queryStableQueryClient,
|
|
2302
|
+
"query-no-rest-destructuring": queryNoRestDestructuring,
|
|
2303
|
+
"query-no-void-query-fn": queryNoVoidQueryFn,
|
|
2304
|
+
"query-no-query-in-effect": queryNoQueryInEffect,
|
|
2305
|
+
"query-mutation-missing-invalidation": queryMutationMissingInvalidation,
|
|
2306
|
+
"query-no-usequery-for-mutation": queryNoUseQueryForMutation
|
|
1756
2307
|
}
|
|
1757
2308
|
};
|
|
1758
2309
|
|