From 248e12d18654f16ae22a3f0dbfe857bc66c9d723 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 17 Sep 2025 14:39:58 +0000
Subject: [PATCH 1/6] Initial plan
From c59ff0c8b8372ccf906353b586952de017e5b8ec Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 17 Sep 2025 14:47:56 +0000
Subject: [PATCH 2/6] Initial analysis and plan for check-parent-suspense
ESLint rule
Co-authored-by: manudeli <61593290+manudeli@users.noreply.github.com>
---
.../tsconfig.tsbuildinfo | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/examples/vite-react-18-suspense-prerender-siblings-problem/tsconfig.tsbuildinfo b/examples/vite-react-18-suspense-prerender-siblings-problem/tsconfig.tsbuildinfo
index 654de112c..832ae3958 100644
--- a/examples/vite-react-18-suspense-prerender-siblings-problem/tsconfig.tsbuildinfo
+++ b/examples/vite-react-18-suspense-prerender-siblings-problem/tsconfig.tsbuildinfo
@@ -1 +1 @@
-{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./vite.config.ts"],"version":"5.8.3"}
\ No newline at end of file
+{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./vite.config.ts"],"version":"5.8.3"}
\ No newline at end of file
From a587446aaebd318490410d255e186d878dc9110c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 17 Sep 2025 14:57:09 +0000
Subject: [PATCH 3/6] Implement check-parent-suspense ESLint rule
Co-authored-by: manudeli <61593290+manudeli@users.noreply.github.com>
---
configs/eslint-config/index.js | 5 +
configs/eslint-config/package.json | 4 +
configs/eslint-config/plugin.js | 13 +
.../rules/check-parent-suspense.js | 197 +++++++++++++
configs/eslint-config/rules/index.js | 5 +
.../tests/rules/check-parent-suspense.test.js | 264 ++++++++++++++++++
.../src/App.tsx | 1 +
pnpm-lock.yaml | 164 +++++++++--
8 files changed, 628 insertions(+), 25 deletions(-)
create mode 100644 configs/eslint-config/plugin.js
create mode 100644 configs/eslint-config/rules/check-parent-suspense.js
create mode 100644 configs/eslint-config/rules/index.js
create mode 100644 configs/eslint-config/tests/rules/check-parent-suspense.test.js
diff --git a/configs/eslint-config/index.js b/configs/eslint-config/index.js
index 7e663c8f7..7aa64efb1 100644
--- a/configs/eslint-config/index.js
+++ b/configs/eslint-config/index.js
@@ -9,6 +9,7 @@ import cspellConfigs from '@cspell/eslint-plugin/configs'
import vitest from '@vitest/eslint-plugin'
import jestDom from 'eslint-plugin-jest-dom'
import * as mdx from 'eslint-plugin-mdx'
+import suspensivePlugin from './plugin.js'
const ignores = ['**/.next/**', '**/build/**', '**/coverage/**', '**/dist/**']
@@ -123,11 +124,15 @@ export const suspensiveReactTypeScriptConfig = tseslint.config(
JSX: true,
},
},
+ plugins: {
+ '@suspensive': suspensivePlugin,
+ },
rules: {
'react-hooks/react-compiler': 'warn',
'@eslint-react/no-use-context': 'off',
'@eslint-react/no-forward-ref': 'off',
'@eslint-react/no-context-provider': 'off',
+ '@suspensive/check-parent-suspense': 'error',
},
settings: {
react: {
diff --git a/configs/eslint-config/package.json b/configs/eslint-config/package.json
index d9a0a37b9..f90c1b93c 100644
--- a/configs/eslint-config/package.json
+++ b/configs/eslint-config/package.json
@@ -23,5 +23,9 @@
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react-hooks": "6.0.0-rc.1",
"typescript-eslint": "^8.31.0"
+ },
+ "devDependencies": {
+ "@babel/eslint-parser": "^7.28.4",
+ "@babel/preset-react": "^7.27.1"
}
}
diff --git a/configs/eslint-config/plugin.js b/configs/eslint-config/plugin.js
new file mode 100644
index 000000000..5af8395e5
--- /dev/null
+++ b/configs/eslint-config/plugin.js
@@ -0,0 +1,13 @@
+import rules from './rules/index.js'
+
+export default {
+ rules,
+ configs: {
+ recommended: {
+ plugins: ['@suspensive'],
+ rules: {
+ '@suspensive/check-parent-suspense': 'error',
+ },
+ },
+ },
+}
\ No newline at end of file
diff --git a/configs/eslint-config/rules/check-parent-suspense.js b/configs/eslint-config/rules/check-parent-suspense.js
new file mode 100644
index 000000000..ecb9ac988
--- /dev/null
+++ b/configs/eslint-config/rules/check-parent-suspense.js
@@ -0,0 +1,197 @@
+/**
+ * @fileoverview Rule to check if components using Suspense-related APIs are wrapped in a Suspense boundary
+ * @author Suspensive Team
+ */
+
+/** @type {import('eslint').Rule.RuleModule} */
+export default {
+ meta: {
+ type: 'problem',
+ docs: {
+ description: 'ensure components using Suspense-related APIs are wrapped in a Suspense boundary',
+ category: 'Possible Errors',
+ recommended: true,
+ },
+ fixable: null,
+ schema: [
+ {
+ type: 'object',
+ properties: {
+ suspenseHooks: {
+ type: 'array',
+ items: { type: 'string' },
+ default: [
+ 'useSuspenseQuery',
+ 'useSuspenseInfiniteQuery',
+ 'useSuspenseQueries'
+ ]
+ },
+ suspenseComponents: {
+ type: 'array',
+ items: { type: 'string' },
+ default: [
+ 'SuspenseQuery',
+ 'SuspenseInfiniteQuery',
+ 'SuspenseQueries'
+ ]
+ },
+ suspenseWrappers: {
+ type: 'array',
+ items: { type: 'string' },
+ default: [
+ 'Suspense'
+ ]
+ }
+ },
+ additionalProperties: false,
+ }
+ ],
+ messages: {
+ missingSuspenseWrapper: 'Component using "{{name}}" must be wrapped in a Suspense boundary.',
+ missingLazySuspenseWrapper: 'Lazy component "{{name}}" must be wrapped in a Suspense boundary.',
+ },
+ },
+
+ create(context) {
+ const options = context.options[0] || {}
+ const suspenseHooks = new Set(options.suspenseHooks || [
+ 'useSuspenseQuery',
+ 'useSuspenseInfiniteQuery',
+ 'useSuspenseQueries'
+ ])
+ const suspenseComponents = new Set(options.suspenseComponents || [
+ 'SuspenseQuery',
+ 'SuspenseInfiniteQuery',
+ 'SuspenseQueries'
+ ])
+ const suspenseWrappers = new Set(options.suspenseWrappers || ['Suspense'])
+
+ /**
+ * Get JSX element name from JSX identifier
+ */
+ function getJSXElementName(nameNode) {
+ if (nameNode.type === 'JSXIdentifier') {
+ return nameNode.name
+ }
+ if (nameNode.type === 'JSXMemberExpression') {
+ return `${getJSXElementName(nameNode.object)}.${nameNode.property.name}`
+ }
+ return null
+ }
+
+ /**
+ * Check if a node is inside a Suspense boundary
+ */
+ function isInsideSuspenseBoundary(node) {
+ let parent = node.parent
+ while (parent) {
+ if (parent.type === 'JSXElement' && parent.openingElement && parent.openingElement.name) {
+ const elementName = getJSXElementName(parent.openingElement.name)
+ if (suspenseWrappers.has(elementName)) {
+ return true
+ }
+ }
+ parent = parent.parent
+ }
+ return false
+ }
+
+ /**
+ * Check if a call expression is a suspense hook
+ */
+ function isSuspenseHook(node) {
+ if (node.type !== 'CallExpression') return false
+
+ let name = null
+ if (node.callee.type === 'Identifier') {
+ name = node.callee.name
+ } else if (node.callee.type === 'MemberExpression' &&
+ node.callee.property.type === 'Identifier') {
+ name = node.callee.property.name
+ }
+
+ return name && suspenseHooks.has(name)
+ }
+
+ /**
+ * Check if a JSX element is a lazy component
+ */
+ function isLazyComponent(node) {
+ if (node.type !== 'JSXElement') return false
+
+ const elementName = getJSXElementName(node.openingElement.name)
+ if (!elementName) return false
+
+ // Check if the component was created with lazy()
+ const scope = context.sourceCode.getScope(node)
+ let currentScope = scope
+ while (currentScope) {
+ const variable = currentScope.set.get(elementName)
+ if (variable && variable.defs.length > 0) {
+ const def = variable.defs[0]
+ if (def.node.type === 'VariableDeclarator' && def.node.init) {
+ return isLazyCall(def.node.init)
+ }
+ }
+ currentScope = currentScope.upper
+ }
+
+ return false
+ }
+
+ /**
+ * Check if a call expression is a lazy() call
+ */
+ function isLazyCall(node) {
+ if (node.type !== 'CallExpression') return false
+
+ return (node.callee.type === 'Identifier' && node.callee.name === 'lazy') ||
+ (node.callee.type === 'MemberExpression' &&
+ node.callee.property.type === 'Identifier' &&
+ node.callee.property.name === 'lazy')
+ }
+
+ return {
+ // Check for suspense hooks
+ 'CallExpression': function(node) {
+ if (isSuspenseHook(node) && !isInsideSuspenseBoundary(node)) {
+ let name = null
+ if (node.callee.type === 'Identifier') {
+ name = node.callee.name
+ } else if (node.callee.type === 'MemberExpression') {
+ name = node.callee.property.name
+ }
+
+ context.report({
+ node: node.callee,
+ messageId: 'missingSuspenseWrapper',
+ data: { name }
+ })
+ }
+ },
+
+ // Check suspense components and lazy components
+ 'JSXElement': function(node) {
+ const elementName = getJSXElementName(node.openingElement.name)
+
+ // Check if this is a suspense component that needs wrapping
+ if (elementName && suspenseComponents.has(elementName) && !isInsideSuspenseBoundary(node)) {
+ context.report({
+ node: node.openingElement.name,
+ messageId: 'missingSuspenseWrapper',
+ data: { name: elementName }
+ })
+ }
+
+ // Check if this is a lazy component that needs wrapping
+ if (isLazyComponent(node) && !isInsideSuspenseBoundary(node)) {
+ context.report({
+ node: node.openingElement.name,
+ messageId: 'missingLazySuspenseWrapper',
+ data: { name: elementName }
+ })
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/configs/eslint-config/rules/index.js b/configs/eslint-config/rules/index.js
new file mode 100644
index 000000000..8d69c9370
--- /dev/null
+++ b/configs/eslint-config/rules/index.js
@@ -0,0 +1,5 @@
+import checkParentSuspense from './check-parent-suspense.js'
+
+export default {
+ 'check-parent-suspense': checkParentSuspense,
+}
\ No newline at end of file
diff --git a/configs/eslint-config/tests/rules/check-parent-suspense.test.js b/configs/eslint-config/tests/rules/check-parent-suspense.test.js
new file mode 100644
index 000000000..21dad2251
--- /dev/null
+++ b/configs/eslint-config/tests/rules/check-parent-suspense.test.js
@@ -0,0 +1,264 @@
+/**
+ * @fileoverview Tests for check-parent-suspense rule
+ * @author Suspensive Team
+ */
+
+import { RuleTester } from 'eslint'
+import babelEslintParser from '@babel/eslint-parser'
+import rule from '../../rules/check-parent-suspense.js'
+
+const ruleTester = new RuleTester({
+ languageOptions: {
+ parser: babelEslintParser,
+ ecmaVersion: 2020,
+ sourceType: 'module',
+ parserOptions: {
+ requireConfigFile: false,
+ babelOptions: {
+ presets: ['@babel/preset-react'],
+ },
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+ },
+})
+
+ruleTester.run('check-parent-suspense', rule, {
+ valid: [
+ // Valid: useSuspenseQuery wrapped in Suspense in same component
+ {
+ code: `
+ function Component() {
+ return (
+ Loading...}>
+
+ {(() => {
+ const query = useSuspenseQuery()
+ return
{query.data}
+ })()}
+
+
+ )
+ }
+ `,
+ },
+
+ // Valid: SuspenseQuery wrapped in Suspense
+ {
+ code: `
+ function Component() {
+ return (
+ Loading...}>
+
+ {(data) => {data}
}
+
+
+ )
+ }
+ `,
+ },
+
+ // Valid: lazy component wrapped in Suspense
+ {
+ code: `
+ const LazyComponent = lazy(() => import('./Component'))
+
+ function App() {
+ return (
+ Loading...}>
+
+
+ )
+ }
+ `,
+ },
+
+ // Valid: nested Suspense boundaries
+ {
+ code: `
+ function Component() {
+ return (
+ Outer loading...}>
+
+ Inner loading...
}>
+
+ {(data) => {data}
}
+
+
+
+
+ )
+ }
+ `,
+ },
+
+ // Valid: non-suspense hooks
+ {
+ code: `
+ function Component() {
+ const query = useQuery()
+ return
{query.data}
+ }
+ `,
+ },
+
+ // Valid: regular components
+ {
+ code: `
+ function Component() {
+ return Hello World
+ }
+ `,
+ },
+ ],
+
+ invalid: [
+ // Invalid: useSuspenseQuery not wrapped in Suspense
+ {
+ code: `
+ function Component() {
+ const query = useSuspenseQuery()
+ return {query.data}
+ }
+ `,
+ errors: [
+ {
+ messageId: 'missingSuspenseWrapper',
+ data: { name: 'useSuspenseQuery' },
+ },
+ ],
+ },
+
+ // Invalid: useSuspenseInfiniteQuery not wrapped
+ {
+ code: `
+ function Component() {
+ const query = useSuspenseInfiniteQuery()
+ return {query.data}
+ }
+ `,
+ errors: [
+ {
+ messageId: 'missingSuspenseWrapper',
+ data: { name: 'useSuspenseInfiniteQuery' },
+ },
+ ],
+ },
+
+ // Invalid: useSuspenseQueries not wrapped
+ {
+ code: `
+ function Component() {
+ const queries = useSuspenseQueries()
+ return {queries[0].data}
+ }
+ `,
+ errors: [
+ {
+ messageId: 'missingSuspenseWrapper',
+ data: { name: 'useSuspenseQueries' },
+ },
+ ],
+ },
+
+ // Invalid: SuspenseQuery not wrapped in Suspense
+ {
+ code: `
+ function Component() {
+ return (
+
+ {(data) => {data}
}
+
+ )
+ }
+ `,
+ errors: [
+ {
+ messageId: 'missingSuspenseWrapper',
+ data: { name: 'SuspenseQuery' },
+ },
+ ],
+ },
+
+ // Invalid: SuspenseInfiniteQuery not wrapped
+ {
+ code: `
+ function Component() {
+ return (
+
+ {(data) => {data}
}
+
+ )
+ }
+ `,
+ errors: [
+ {
+ messageId: 'missingSuspenseWrapper',
+ data: { name: 'SuspenseInfiniteQuery' },
+ },
+ ],
+ },
+
+ // Invalid: lazy component not wrapped in Suspense
+ {
+ code: `
+ const LazyComponent = lazy(() => import('./Component'))
+
+ function App() {
+ return
+ }
+ `,
+ errors: [
+ {
+ messageId: 'missingLazySuspenseWrapper',
+ data: { name: 'LazyComponent' },
+ },
+ ],
+ },
+
+ // Invalid: multiple violations
+ {
+ code: `
+ function Component() {
+ const query = useSuspenseQuery()
+ return (
+
+
+ {(data) => {data}
}
+
+
+ )
+ }
+ `,
+ errors: [
+ {
+ messageId: 'missingSuspenseWrapper',
+ data: { name: 'useSuspenseQuery' },
+ },
+ {
+ messageId: 'missingSuspenseWrapper',
+ data: { name: 'SuspenseQuery' },
+ },
+ ],
+ },
+
+ // Invalid: member expression usage
+ {
+ code: `
+ function Component() {
+ const query = TanStackQuery.useSuspenseQuery()
+ return {query.data}
+ }
+ `,
+ errors: [
+ {
+ messageId: 'missingSuspenseWrapper',
+ data: { name: 'useSuspenseQuery' },
+ },
+ ],
+ },
+ ],
+})
+
+console.log('All tests passed!')
\ No newline at end of file
diff --git a/examples/vite-react-18-suspense-prerender-siblings-problem/src/App.tsx b/examples/vite-react-18-suspense-prerender-siblings-problem/src/App.tsx
index 2fbee7ce5..44499a77a 100644
--- a/examples/vite-react-18-suspense-prerender-siblings-problem/src/App.tsx
+++ b/examples/vite-react-18-suspense-prerender-siblings-problem/src/App.tsx
@@ -41,6 +41,7 @@ const Slow = () => {
const Suspend = ({ i }: { i: number }) => {
console.log({ Suspend: `before Suspend${i + 1}` })
+ // eslint-disable-next-line @suspensive/check-parent-suspense
useSuspenseQuery(query.dummy(i))
console.log({ Suspend: `after Suspend${i + 1}` })
return {i}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7189a4308..4f07a3020 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -231,6 +231,13 @@ importers:
typescript-eslint:
specifier: ^8.31.0
version: 8.31.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)
+ devDependencies:
+ '@babel/eslint-parser':
+ specifier: ^7.28.4
+ version: 7.28.4(@babel/core@7.27.1)(eslint@9.28.0(jiti@2.4.2))
+ '@babel/preset-react':
+ specifier: ^7.27.1
+ version: 7.27.1(@babel/core@7.27.1)
configs/tsconfig: {}
@@ -418,7 +425,7 @@ importers:
version: 29.7.0(@types/node@22.14.1)(ts-node@10.9.2(@types/node@22.14.1)(typescript@5.8.3))
jest-expo:
specifier: catalog:react19
- version: 52.0.2(@babel/core@7.27.1)(expo@52.0.24(@babel/core@7.27.1)(@babel/preset-env@7.26.0(@babel/core@7.27.1))(@expo/metro-runtime@4.0.0(react-native@0.76.6(@babel/core@7.27.1)(@babel/preset-env@7.26.0(@babel/core@7.27.1))(@types/react@19.1.6)(react@19.1.0)))(graphql@16.9.0)(react-native-webview@13.12.5(react-native@0.76.6(@babel/core@7.27.1)(@babel/preset-env@7.26.0(@babel/core@7.27.1))(@types/react@19.1.6)(react@19.1.0))(react@19.1.0))(react-native@0.76.6(@babel/core@7.27.1)(@babel/preset-env@7.26.0(@babel/core@7.27.1))(@types/react@19.1.6)(react@19.1.0))(react@19.1.0))(jest@29.7.0(@types/node@22.14.1)(ts-node@10.9.2(@types/node@22.14.1)(typescript@5.8.3)))(react-dom@19.1.0(react@19.1.0))(react-native@0.76.6(@babel/core@7.27.1)(@babel/preset-env@7.26.0(@babel/core@7.27.1))(@types/react@19.1.6)(react@19.1.0))(react@19.1.0)(webpack@5.96.1)
+ version: 52.0.2(@babel/core@7.27.1)(expo@52.0.24(@babel/core@7.27.1)(@babel/preset-env@7.26.0(@babel/core@7.27.1))(@expo/metro-runtime@4.0.0(react-native@0.76.6(@babel/core@7.27.1)(@babel/preset-env@7.26.0(@babel/core@7.27.1))(@types/react@19.1.6)(react@19.1.0)))(graphql@16.9.0)(react-native-webview@13.12.5(react-native@0.76.6(@babel/core@7.27.1)(@babel/preset-env@7.26.0(@babel/core@7.27.1))(@types/react@19.1.6)(react@19.1.0))(react@19.1.0))(react-native@0.76.6(@babel/core@7.27.1)(@babel/preset-env@7.26.0(@babel/core@7.27.1))(@types/react@19.1.6)(react@19.1.0))(react@19.1.0))(jest@29.7.0(@types/node@22.14.1))(react-dom@19.1.0(react@19.1.0))(react-native@0.76.6(@babel/core@7.27.1)(@babel/preset-env@7.26.0(@babel/core@7.27.1))(@types/react@19.1.6)(react@19.1.0))(react@19.1.0)(webpack@5.96.1)
react-test-renderer:
specifier: 19.0.0
version: 19.0.0(react@19.1.0)
@@ -655,7 +662,7 @@ importers:
version: 29.7.0(@types/node@22.14.1)(ts-node@10.9.2(@types/node@22.14.1)(typescript@5.8.3))
jest-expo:
specifier: catalog:react19
- version: 52.0.2(@babel/core@7.27.1)(expo@52.0.24(@babel/core@7.27.1)(@babel/preset-env@7.26.0(@babel/core@7.27.1))(@expo/metro-runtime@4.0.0(react-native@0.76.6(@babel/core@7.27.1)(@babel/preset-env@7.26.0(@babel/core@7.27.1))(@types/react@19.1.6)(react@19.1.0)))(graphql@16.9.0)(react-native-webview@13.12.5(react-native@0.76.6(@babel/core@7.27.1)(@babel/preset-env@7.26.0(@babel/core@7.27.1))(@types/react@19.1.6)(react@19.1.0))(react@19.1.0))(react-native@0.76.6(@babel/core@7.27.1)(@babel/preset-env@7.26.0(@babel/core@7.27.1))(@types/react@19.1.6)(react@19.1.0))(react@19.1.0))(jest@29.7.0(@types/node@22.14.1)(ts-node@10.9.2(@types/node@22.14.1)(typescript@5.8.3)))(react-dom@19.1.0(react@19.1.0))(react-native@0.76.6(@babel/core@7.27.1)(@babel/preset-env@7.26.0(@babel/core@7.27.1))(@types/react@19.1.6)(react@19.1.0))(react@19.1.0)(webpack@5.96.1)
+ version: 52.0.2(@babel/core@7.27.1)(expo@52.0.24(@babel/core@7.27.1)(@babel/preset-env@7.26.0(@babel/core@7.27.1))(@expo/metro-runtime@4.0.0(react-native@0.76.6(@babel/core@7.27.1)(@babel/preset-env@7.26.0(@babel/core@7.27.1))(@types/react@19.1.6)(react@19.1.0)))(graphql@16.9.0)(react-native-webview@13.12.5(react-native@0.76.6(@babel/core@7.27.1)(@babel/preset-env@7.26.0(@babel/core@7.27.1))(@types/react@19.1.6)(react@19.1.0))(react@19.1.0))(react-native@0.76.6(@babel/core@7.27.1)(@babel/preset-env@7.26.0(@babel/core@7.27.1))(@types/react@19.1.6)(react@19.1.0))(react@19.1.0))(jest@29.7.0(@types/node@22.14.1))(react-dom@19.1.0(react@19.1.0))(react-native@0.76.6(@babel/core@7.27.1)(@babel/preset-env@7.26.0(@babel/core@7.27.1))(@types/react@19.1.6)(react@19.1.0))(react@19.1.0)(webpack@5.96.1)
react:
specifier: catalog:react19
version: 19.1.0
@@ -806,6 +813,13 @@ packages:
resolution: {integrity: sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==}
engines: {node: '>=6.9.0'}
+ '@babel/eslint-parser@7.28.4':
+ resolution: {integrity: sha512-Aa+yDiH87980jR6zvRfFuCR1+dLb00vBydhTL+zI992Rz/wQhSvuxjmOOuJOgO3XmakO6RykRGD2S1mq1AtgHA==}
+ engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0}
+ peerDependencies:
+ '@babel/core': ^7.11.0
+ eslint: ^7.5.0 || ^8.0.0 || ^9.0.0
+
'@babel/generator@7.27.1':
resolution: {integrity: sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==}
engines: {node: '>=6.9.0'}
@@ -818,6 +832,10 @@ packages:
resolution: {integrity: sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==}
engines: {node: '>=6.9.0'}
+ '@babel/helper-annotate-as-pure@7.27.3':
+ resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==}
+ engines: {node: '>=6.9.0'}
+
'@babel/helper-compilation-targets@7.27.1':
resolution: {integrity: sha512-2YaDd/Rd9E598B5+WIc8wJPmWETiiJXFYVE60oX8FDohv7rAUU3CQj+A1MgeEmcsk2+dQuEjIe/GDvig0SqL4g==}
engines: {node: '>=6.9.0'}
@@ -843,10 +861,6 @@ packages:
resolution: {integrity: sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==}
engines: {node: '>=6.9.0'}
- '@babel/helper-module-imports@7.25.9':
- resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==}
- engines: {node: '>=6.9.0'}
-
'@babel/helper-module-imports@7.27.1':
resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==}
engines: {node: '>=6.9.0'}
@@ -871,6 +885,10 @@ packages:
resolution: {integrity: sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==}
engines: {node: '>=6.9.0'}
+ '@babel/helper-plugin-utils@7.27.1':
+ resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==}
+ engines: {node: '>=6.9.0'}
+
'@babel/helper-remap-async-to-generator@7.25.9':
resolution: {integrity: sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==}
engines: {node: '>=6.9.0'}
@@ -907,10 +925,6 @@ packages:
resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
engines: {node: '>=6.9.0'}
- '@babel/helper-validator-option@7.25.9':
- resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==}
- engines: {node: '>=6.9.0'}
-
'@babel/helper-validator-option@7.27.1':
resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
engines: {node: '>=6.9.0'}
@@ -1083,6 +1097,12 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
+ '@babel/plugin-syntax-jsx@7.27.1':
+ resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
'@babel/plugin-syntax-logical-assignment-operators@7.10.4':
resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==}
peerDependencies:
@@ -1383,12 +1403,24 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
+ '@babel/plugin-transform-react-display-name@7.28.0':
+ resolution: {integrity: sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
'@babel/plugin-transform-react-jsx-development@7.25.9':
resolution: {integrity: sha512-9mj6rm7XVYs4mdLIpbZnHOYdpW42uoiBCTVowg7sP1thUOiANgMb4UtpRivR0pp5iL+ocvUv7X4mZgFRpJEzGw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
+ '@babel/plugin-transform-react-jsx-development@7.27.1':
+ resolution: {integrity: sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
'@babel/plugin-transform-react-jsx-self@7.25.9':
resolution: {integrity: sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==}
engines: {node: '>=6.9.0'}
@@ -1407,12 +1439,24 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
+ '@babel/plugin-transform-react-jsx@7.27.1':
+ resolution: {integrity: sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
'@babel/plugin-transform-react-pure-annotations@7.25.9':
resolution: {integrity: sha512-KQ/Takk3T8Qzj5TppkS1be588lkbTp5uj7w6a0LeQaTMSckU/wK0oJ/pih+T690tkgI5jfmg2TqDJvd41Sj1Cg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
+ '@babel/plugin-transform-react-pure-annotations@7.27.1':
+ resolution: {integrity: sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
'@babel/plugin-transform-regenerator@7.25.9':
resolution: {integrity: sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==}
engines: {node: '>=6.9.0'}
@@ -1520,6 +1564,12 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
+ '@babel/preset-react@7.27.1':
+ resolution: {integrity: sha512-oJHWh2gLhU9dW9HHr42q0cI0/iHHXTLGe39qvpAZZzagHy0MzYLCnCVV0symeRvzmjHyVU7mw2K06E6u/JwbhA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
'@babel/preset-typescript@7.26.0':
resolution: {integrity: sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg==}
engines: {node: '>=6.9.0'}
@@ -2790,6 +2840,9 @@ packages:
next: ^13.0.0 || ^14.0.0 || ^15.0.0
react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
+ '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1':
+ resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==}
+
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@@ -5816,6 +5869,10 @@ packages:
resolution: {integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ eslint-visitor-keys@2.1.0:
+ resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==}
+ engines: {node: '>=10'}
+
eslint-visitor-keys@3.4.3:
resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -11065,6 +11122,14 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@babel/eslint-parser@7.28.4(@babel/core@7.27.1)(eslint@9.28.0(jiti@2.4.2))':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1
+ eslint: 9.28.0(jiti@2.4.2)
+ eslint-visitor-keys: 2.1.0
+ semver: 6.3.1
+
'@babel/generator@7.27.1':
dependencies:
'@babel/parser': 7.27.1
@@ -11085,6 +11150,10 @@ snapshots:
dependencies:
'@babel/types': 7.28.2
+ '@babel/helper-annotate-as-pure@7.27.3':
+ dependencies:
+ '@babel/types': 7.28.2
+
'@babel/helper-compilation-targets@7.27.1':
dependencies:
'@babel/compat-data': 7.27.1
@@ -11131,13 +11200,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@babel/helper-module-imports@7.25.9':
- dependencies:
- '@babel/traverse': 7.27.1
- '@babel/types': 7.28.2
- transitivePeerDependencies:
- - supports-color
-
'@babel/helper-module-imports@7.27.1':
dependencies:
'@babel/traverse': 7.27.1
@@ -11148,7 +11210,7 @@ snapshots:
'@babel/helper-module-transforms@7.26.0(@babel/core@7.27.1)':
dependencies:
'@babel/core': 7.27.1
- '@babel/helper-module-imports': 7.25.9
+ '@babel/helper-module-imports': 7.27.1
'@babel/helper-validator-identifier': 7.25.9
'@babel/traverse': 7.25.9
transitivePeerDependencies:
@@ -11169,6 +11231,8 @@ snapshots:
'@babel/helper-plugin-utils@7.25.9': {}
+ '@babel/helper-plugin-utils@7.27.1': {}
+
'@babel/helper-remap-async-to-generator@7.25.9(@babel/core@7.27.1)':
dependencies:
'@babel/core': 7.27.1
@@ -11209,8 +11273,6 @@ snapshots:
'@babel/helper-validator-identifier@7.27.1': {}
- '@babel/helper-validator-option@7.25.9': {}
-
'@babel/helper-validator-option@7.27.1': {}
'@babel/helper-wrap-function@7.25.9':
@@ -11386,6 +11448,11 @@ snapshots:
'@babel/core': 7.27.1
'@babel/helper-plugin-utils': 7.25.9
+ '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.27.1
+
'@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.27.1)':
dependencies:
'@babel/core': 7.27.1
@@ -11708,6 +11775,11 @@ snapshots:
'@babel/core': 7.27.1
'@babel/helper-plugin-utils': 7.25.9
+ '@babel/plugin-transform-react-display-name@7.28.0(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.27.1
+
'@babel/plugin-transform-react-jsx-development@7.25.9(@babel/core@7.27.1)':
dependencies:
'@babel/core': 7.27.1
@@ -11715,6 +11787,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.27.1)
+ transitivePeerDependencies:
+ - supports-color
+
'@babel/plugin-transform-react-jsx-self@7.25.9(@babel/core@7.27.1)':
dependencies:
'@babel/core': 7.27.1
@@ -11736,12 +11815,29 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-annotate-as-pure': 7.27.3
+ '@babel/helper-module-imports': 7.27.1
+ '@babel/helper-plugin-utils': 7.27.1
+ '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.1)
+ '@babel/types': 7.28.2
+ transitivePeerDependencies:
+ - supports-color
+
'@babel/plugin-transform-react-pure-annotations@7.25.9(@babel/core@7.27.1)':
dependencies:
'@babel/core': 7.27.1
'@babel/helper-annotate-as-pure': 7.25.9
'@babel/helper-plugin-utils': 7.25.9
+ '@babel/plugin-transform-react-pure-annotations@7.27.1(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-annotate-as-pure': 7.27.3
+ '@babel/helper-plugin-utils': 7.27.1
+
'@babel/plugin-transform-regenerator@7.25.9(@babel/core@7.27.1)':
dependencies:
'@babel/core': 7.27.1
@@ -11912,7 +12008,7 @@ snapshots:
dependencies:
'@babel/core': 7.27.1
'@babel/helper-plugin-utils': 7.25.9
- '@babel/helper-validator-option': 7.25.9
+ '@babel/helper-validator-option': 7.27.1
'@babel/plugin-transform-flow-strip-types': 7.25.9(@babel/core@7.27.1)
'@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.27.1)':
@@ -11934,11 +12030,23 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@babel/preset-react@7.27.1(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.27.1
+ '@babel/helper-validator-option': 7.27.1
+ '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.27.1)
+ '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-transform-react-pure-annotations': 7.27.1(@babel/core@7.27.1)
+ transitivePeerDependencies:
+ - supports-color
+
'@babel/preset-typescript@7.26.0(@babel/core@7.27.1)':
dependencies:
'@babel/core': 7.27.1
'@babel/helper-plugin-utils': 7.25.9
- '@babel/helper-validator-option': 7.25.9
+ '@babel/helper-validator-option': 7.27.1
'@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.27.1)
'@babel/plugin-transform-modules-commonjs': 7.25.9(@babel/core@7.27.1)
'@babel/plugin-transform-typescript': 7.25.9(@babel/core@7.27.1)
@@ -13710,6 +13818,10 @@ snapshots:
react: 19.1.0
third-party-capital: 1.0.20
+ '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1':
+ dependencies:
+ eslint-scope: 5.1.1
+
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
@@ -17255,6 +17367,8 @@ snapshots:
esrecurse: 4.3.0
estraverse: 5.3.0
+ eslint-visitor-keys@2.1.0: {}
+
eslint-visitor-keys@3.4.3: {}
eslint-visitor-keys@4.2.0: {}
@@ -18712,7 +18826,7 @@ snapshots:
jest-mock: 29.7.0
jest-util: 29.7.0
- jest-expo@52.0.2(@babel/core@7.27.1)(expo@52.0.24(@babel/core@7.27.1)(@babel/preset-env@7.26.0(@babel/core@7.27.1))(@expo/metro-runtime@4.0.0(react-native@0.76.6(@babel/core@7.27.1)(@babel/preset-env@7.26.0(@babel/core@7.27.1))(@types/react@19.1.6)(react@19.1.0)))(graphql@16.9.0)(react-native-webview@13.12.5(react-native@0.76.6(@babel/core@7.27.1)(@babel/preset-env@7.26.0(@babel/core@7.27.1))(@types/react@19.1.6)(react@19.1.0))(react@19.1.0))(react-native@0.76.6(@babel/core@7.27.1)(@babel/preset-env@7.26.0(@babel/core@7.27.1))(@types/react@19.1.6)(react@19.1.0))(react@19.1.0))(jest@29.7.0(@types/node@22.14.1)(ts-node@10.9.2(@types/node@22.14.1)(typescript@5.8.3)))(react-dom@19.1.0(react@19.1.0))(react-native@0.76.6(@babel/core@7.27.1)(@babel/preset-env@7.26.0(@babel/core@7.27.1))(@types/react@19.1.6)(react@19.1.0))(react@19.1.0)(webpack@5.96.1):
+ jest-expo@52.0.2(@babel/core@7.27.1)(expo@52.0.24(@babel/core@7.27.1)(@babel/preset-env@7.26.0(@babel/core@7.27.1))(@expo/metro-runtime@4.0.0(react-native@0.76.6(@babel/core@7.27.1)(@babel/preset-env@7.26.0(@babel/core@7.27.1))(@types/react@19.1.6)(react@19.1.0)))(graphql@16.9.0)(react-native-webview@13.12.5(react-native@0.76.6(@babel/core@7.27.1)(@babel/preset-env@7.26.0(@babel/core@7.27.1))(@types/react@19.1.6)(react@19.1.0))(react@19.1.0))(react-native@0.76.6(@babel/core@7.27.1)(@babel/preset-env@7.26.0(@babel/core@7.27.1))(@types/react@19.1.6)(react@19.1.0))(react@19.1.0))(jest@29.7.0(@types/node@22.14.1))(react-dom@19.1.0(react@19.1.0))(react-native@0.76.6(@babel/core@7.27.1)(@babel/preset-env@7.26.0(@babel/core@7.27.1))(@types/react@19.1.6)(react@19.1.0))(react@19.1.0)(webpack@5.96.1):
dependencies:
'@expo/config': 10.0.7
'@expo/json-file': 9.0.0
@@ -18725,7 +18839,7 @@ snapshots:
jest-environment-jsdom: 29.7.0
jest-snapshot: 29.7.0
jest-watch-select-projects: 2.0.0
- jest-watch-typeahead: 2.2.1(jest@29.7.0(@types/node@22.14.1)(ts-node@10.9.2(@types/node@22.14.1)(typescript@5.8.3)))
+ jest-watch-typeahead: 2.2.1(jest@29.7.0(@types/node@22.14.1))
json5: 2.2.3
lodash: 4.17.21
react-native: 0.76.6(@babel/core@7.27.1)(@babel/preset-env@7.26.0(@babel/core@7.27.1))(@types/react@19.1.6)(react@19.1.0)
@@ -18920,7 +19034,7 @@ snapshots:
chalk: 3.0.0
prompts: 2.4.2
- jest-watch-typeahead@2.2.1(jest@29.7.0(@types/node@22.14.1)(ts-node@10.9.2(@types/node@22.14.1)(typescript@5.8.3))):
+ jest-watch-typeahead@2.2.1(jest@29.7.0(@types/node@22.14.1)):
dependencies:
ansi-escapes: 6.2.1
chalk: 4.1.2
From 9e7afaa839656becdb31bd39de38937c144215d3 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 17 Sep 2025 15:09:47 +0000
Subject: [PATCH 4/6] Complete check-parent-suspense ESLint rule implementation
with documentation
Co-authored-by: manudeli <61593290+manudeli@users.noreply.github.com>
---
.../docs/check-parent-suspense.md | 132 ++++++++++++++++++
.../src/app/page.tsx | 1 +
.../src/app/react/zodSearchParams/page.tsx | 1 +
.../src/SuspenseInfiniteQuery.test-d.tsx | 1 +
.../src/SuspenseInfiniteQuery.tsx | 7 +-
.../src/SuspenseQueries.test-d.tsx | 1 +
.../react-query-4/src/SuspenseQueries.tsx | 7 +-
.../src/SuspenseQuery.test-d.tsx | 1 +
packages/react-query-4/src/SuspenseQuery.tsx | 7 +-
.../src/SuspenseInfiniteQuery.test-d.tsx | 1 +
.../src/SuspenseInfiniteQuery.tsx | 7 +-
.../src/SuspenseQueries.test-d.tsx | 1 +
.../react-query-5/src/SuspenseQueries.tsx | 7 +-
.../src/SuspenseQuery.test-d.tsx | 1 +
packages/react-query-5/src/SuspenseQuery.tsx | 7 +-
packages/react/src/lazy.spec.tsx | 1 +
16 files changed, 177 insertions(+), 6 deletions(-)
create mode 100644 configs/eslint-config/docs/check-parent-suspense.md
diff --git a/configs/eslint-config/docs/check-parent-suspense.md b/configs/eslint-config/docs/check-parent-suspense.md
new file mode 100644
index 000000000..779e70145
--- /dev/null
+++ b/configs/eslint-config/docs/check-parent-suspense.md
@@ -0,0 +1,132 @@
+# @suspensive/check-parent-suspense
+
+Ensures that components using Suspense-related APIs are wrapped in a Suspense boundary.
+
+## Rule Details
+
+This rule checks that components using Suspense-related hooks and components are properly wrapped in a `` component boundary within the same component scope.
+
+### Suspense-related APIs checked:
+
+**Hooks:**
+- `useSuspenseQuery`
+- `useSuspenseInfiniteQuery`
+- `useSuspenseQueries`
+
+**Components:**
+- `SuspenseQuery`
+- `SuspenseInfiniteQuery`
+- `SuspenseQueries`
+
+**Lazy components:**
+- Components created with `lazy()` function
+
+## Examples
+
+### ❌ Incorrect
+
+```tsx
+// Hook without Suspense wrapper
+function MyComponent() {
+ const data = useSuspenseQuery(queryOptions)
+ return {data}
+}
+
+// Component without Suspense wrapper
+function MyComponent() {
+ return (
+
+ {(data) => {data}
}
+
+ )
+}
+
+// Lazy component without Suspense wrapper
+const LazyComponent = lazy(() => import('./Component'))
+
+function MyApp() {
+ return
+}
+```
+
+### ✅ Correct
+
+```tsx
+// Hook with Suspense wrapper
+function MyComponent() {
+ return (
+ Loading...}>
+
+ {(() => {
+ const data = useSuspenseQuery(queryOptions)
+ return
{data}
+ })()}
+
+
+ )
+}
+
+// Component with Suspense wrapper
+function MyComponent() {
+ return (
+ Loading...}>
+
+ {(data) => {data}
}
+
+
+ )
+}
+
+// Lazy component with Suspense wrapper
+const LazyComponent = lazy(() => import('./Component'))
+
+function MyApp() {
+ return (
+ Loading...}>
+
+
+ )
+}
+```
+
+## Options
+
+The rule accepts an options object with the following properties:
+
+- `suspenseHooks` (array): List of hook names to check. Default: `['useSuspenseQuery', 'useSuspenseInfiniteQuery', 'useSuspenseQueries']`
+- `suspenseComponents` (array): List of component names to check. Default: `['SuspenseQuery', 'SuspenseInfiniteQuery', 'SuspenseQueries']`
+- `suspenseWrappers` (array): List of valid Suspense wrapper component names. Default: `['Suspense']`
+
+### Example configuration
+
+```json
+{
+ "rules": {
+ "@suspensive/check-parent-suspense": [
+ "error",
+ {
+ "suspenseHooks": ["useSuspenseQuery", "useCustomSuspenseHook"],
+ "suspenseComponents": ["SuspenseQuery", "CustomSuspenseComponent"],
+ "suspenseWrappers": ["Suspense", "CustomSuspense"]
+ }
+ ]
+ }
+}
+```
+
+## When Not To Use It
+
+This rule enforces Suspense boundaries within the same component scope. In some patterns, components using Suspense APIs may be rendered within Suspense boundaries defined in parent components. If you're using such patterns and want to allow them, you can disable the rule for specific lines:
+
+```tsx
+const MyComponent = () => {
+ // eslint-disable-next-line @suspensive/check-parent-suspense
+ const data = useSuspenseQuery(queryOptions)
+ return {data}
+}
+```
+
+## Related Rules
+
+- React's built-in Suspense documentation: https://react.dev/reference/react/Suspense
+- @tanstack/react-query Suspense documentation
\ No newline at end of file
diff --git a/examples/next-streaming-react-query/src/app/page.tsx b/examples/next-streaming-react-query/src/app/page.tsx
index 5f1ab59f3..7ff189690 100644
--- a/examples/next-streaming-react-query/src/app/page.tsx
+++ b/examples/next-streaming-react-query/src/app/page.tsx
@@ -8,6 +8,7 @@ import type { ReactNode } from 'react'
import { query } from '~/query'
const Text = ({ ms }: { ms: number }) => {
+ // eslint-disable-next-line @suspensive/check-parent-suspense
const { data } = useSuspenseQuery(query.text(ms))
return result: {data}
}
diff --git a/examples/visualization/src/app/react/zodSearchParams/page.tsx b/examples/visualization/src/app/react/zodSearchParams/page.tsx
index 998c93a8f..1337124e4 100644
--- a/examples/visualization/src/app/react/zodSearchParams/page.tsx
+++ b/examples/visualization/src/app/react/zodSearchParams/page.tsx
@@ -32,6 +32,7 @@ export default ErrorBoundary.with(
Suspense.with({ fallback: }, () => {
const searchParams = useSearchParams()
const { id } = searchParamsSchema.parse(Object.fromEntries(searchParams.entries()))
+ // eslint-disable-next-line @suspensive/check-parent-suspense
const userQuery = useSuspenseQuery({
queryKey: ['users', id] as const,
queryFn: ({ queryKey: [, userId] }) => delay(200).then(() => ({ id: userId, name: 'John' })),
diff --git a/packages/react-query-4/src/SuspenseInfiniteQuery.test-d.tsx b/packages/react-query-4/src/SuspenseInfiniteQuery.test-d.tsx
index aab59bee4..3ce54ac1b 100644
--- a/packages/react-query-4/src/SuspenseInfiniteQuery.test-d.tsx
+++ b/packages/react-query-4/src/SuspenseInfiniteQuery.test-d.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable @suspensive/check-parent-suspense */
import { type InfiniteData, type UseSuspenseInfiniteQueryResult, infiniteQueryOptions } from '@tanstack/react-query'
import type { ReactNode } from 'react'
import { describe, expectTypeOf, it } from 'vitest'
diff --git a/packages/react-query-4/src/SuspenseInfiniteQuery.tsx b/packages/react-query-4/src/SuspenseInfiniteQuery.tsx
index 6542b1ad5..90c0c5ed3 100644
--- a/packages/react-query-4/src/SuspenseInfiniteQuery.tsx
+++ b/packages/react-query-4/src/SuspenseInfiniteQuery.tsx
@@ -27,4 +27,9 @@ export const SuspenseInfiniteQuery = <
...options
}: UseSuspenseInfiniteQueryOptions & {
children: (query: UseSuspenseInfiniteQueryResult) => ReactNode
-}) => <>{children(useSuspenseInfiniteQuery(options))}>
+}) => (
+ <>
+ {/* eslint-disable-next-line @suspensive/check-parent-suspense */}
+ {children(useSuspenseInfiniteQuery(options))}
+ >
+)
diff --git a/packages/react-query-4/src/SuspenseQueries.test-d.tsx b/packages/react-query-4/src/SuspenseQueries.test-d.tsx
index cc71bcfbe..293aaeecc 100644
--- a/packages/react-query-4/src/SuspenseQueries.test-d.tsx
+++ b/packages/react-query-4/src/SuspenseQueries.test-d.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable @suspensive/check-parent-suspense */
import { type UseSuspenseQueryResult, queryOptions } from '@tanstack/react-query'
import type { ReactNode } from 'react'
import { describe, expectTypeOf, it } from 'vitest'
diff --git a/packages/react-query-4/src/SuspenseQueries.tsx b/packages/react-query-4/src/SuspenseQueries.tsx
index 57f9b4def..dbd5b0620 100644
--- a/packages/react-query-4/src/SuspenseQueries.tsx
+++ b/packages/react-query-4/src/SuspenseQueries.tsx
@@ -23,5 +23,10 @@ export function SuspenseQueries({
queries: readonly [...SuspenseQueriesOptions]
children: (queries: SuspenseQueriesResults) => ReactNode
}) {
- return <>{children(useSuspenseQueries({ queries }))}>
+ return (
+ <>
+ {/* eslint-disable-next-line @suspensive/check-parent-suspense */}
+ {children(useSuspenseQueries({ queries }))}
+ >
+ )
}
diff --git a/packages/react-query-4/src/SuspenseQuery.test-d.tsx b/packages/react-query-4/src/SuspenseQuery.test-d.tsx
index 9cabe02a3..7bb732ce3 100644
--- a/packages/react-query-4/src/SuspenseQuery.test-d.tsx
+++ b/packages/react-query-4/src/SuspenseQuery.test-d.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable @suspensive/check-parent-suspense */
import { type UseSuspenseQueryResult, queryOptions } from '@tanstack/react-query'
import type { ReactNode } from 'react'
import { describe, expectTypeOf, it } from 'vitest'
diff --git a/packages/react-query-4/src/SuspenseQuery.tsx b/packages/react-query-4/src/SuspenseQuery.tsx
index 0b710f9e2..8405808f7 100644
--- a/packages/react-query-4/src/SuspenseQuery.tsx
+++ b/packages/react-query-4/src/SuspenseQuery.tsx
@@ -32,4 +32,9 @@ export const SuspenseQuery = <
...options
}: UseSuspenseQueryOptions & {
children: (queryResult: UseSuspenseQueryResult) => ReactNode
-}) => <>{children(useSuspenseQuery(options))}>
+}) => (
+ <>
+ {/* eslint-disable-next-line @suspensive/check-parent-suspense */}
+ {children(useSuspenseQuery(options))}
+ >
+)
diff --git a/packages/react-query-5/src/SuspenseInfiniteQuery.test-d.tsx b/packages/react-query-5/src/SuspenseInfiniteQuery.test-d.tsx
index 1ba2b4ae8..98d8afdeb 100644
--- a/packages/react-query-5/src/SuspenseInfiniteQuery.test-d.tsx
+++ b/packages/react-query-5/src/SuspenseInfiniteQuery.test-d.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable @suspensive/check-parent-suspense */
import type { InfiniteData, UseSuspenseInfiniteQueryResult } from '@tanstack/react-query'
import type { ReactNode } from 'react'
import { describe, expectTypeOf, it } from 'vitest'
diff --git a/packages/react-query-5/src/SuspenseInfiniteQuery.tsx b/packages/react-query-5/src/SuspenseInfiniteQuery.tsx
index 84ad5aecf..bb55b5f97 100644
--- a/packages/react-query-5/src/SuspenseInfiniteQuery.tsx
+++ b/packages/react-query-5/src/SuspenseInfiniteQuery.tsx
@@ -35,4 +35,9 @@ export const SuspenseInfiniteQuery = <
...options
}: UseSuspenseInfiniteQueryOptions & {
children: (query: UseSuspenseInfiniteQueryResult) => ReactNode
-}) => <>{children(useSuspenseInfiniteQuery(options))}>
+}) => (
+ <>
+ {/* eslint-disable-next-line @suspensive/check-parent-suspense */}
+ {children(useSuspenseInfiniteQuery(options))}
+ >
+)
diff --git a/packages/react-query-5/src/SuspenseQueries.test-d.tsx b/packages/react-query-5/src/SuspenseQueries.test-d.tsx
index ea9f15040..4004518d9 100644
--- a/packages/react-query-5/src/SuspenseQueries.test-d.tsx
+++ b/packages/react-query-5/src/SuspenseQueries.test-d.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable @suspensive/check-parent-suspense */
import type { UseSuspenseQueryResult } from '@tanstack/react-query'
import type { ReactNode } from 'react'
import { describe, expectTypeOf, it } from 'vitest'
diff --git a/packages/react-query-5/src/SuspenseQueries.tsx b/packages/react-query-5/src/SuspenseQueries.tsx
index 10007df3c..d853e684f 100644
--- a/packages/react-query-5/src/SuspenseQueries.tsx
+++ b/packages/react-query-5/src/SuspenseQueries.tsx
@@ -25,5 +25,10 @@ export function SuspenseQueries ReactNode
combine?: (result: SuspenseQueriesResults) => TCombinedResult
}) {
- return <>{children(useSuspenseQueries({ queries, combine }))}>
+ return (
+ <>
+ {/* eslint-disable-next-line @suspensive/check-parent-suspense */}
+ {children(useSuspenseQueries({ queries, combine }))}
+ >
+ )
}
diff --git a/packages/react-query-5/src/SuspenseQuery.test-d.tsx b/packages/react-query-5/src/SuspenseQuery.test-d.tsx
index 1fca85a3e..1d90a940b 100644
--- a/packages/react-query-5/src/SuspenseQuery.test-d.tsx
+++ b/packages/react-query-5/src/SuspenseQuery.test-d.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable @suspensive/check-parent-suspense */
import type { UseSuspenseQueryResult } from '@tanstack/react-query'
import type { ReactNode } from 'react'
import { describe, expectTypeOf, it } from 'vitest'
diff --git a/packages/react-query-5/src/SuspenseQuery.tsx b/packages/react-query-5/src/SuspenseQuery.tsx
index 842c145f4..6533abfad 100644
--- a/packages/react-query-5/src/SuspenseQuery.tsx
+++ b/packages/react-query-5/src/SuspenseQuery.tsx
@@ -32,4 +32,9 @@ export const SuspenseQuery = <
...options
}: UseSuspenseQueryOptions & {
children: (queryResult: UseSuspenseQueryResult) => ReactNode
-}) => <>{children(useSuspenseQuery(options))}>
+}) => (
+ <>
+ {/* eslint-disable-next-line @suspensive/check-parent-suspense */}
+ {children(useSuspenseQuery(options))}
+ >
+)
diff --git a/packages/react/src/lazy.spec.tsx b/packages/react/src/lazy.spec.tsx
index 30bdd8ed6..cee78c765 100644
--- a/packages/react/src/lazy.spec.tsx
+++ b/packages/react/src/lazy.spec.tsx
@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
+/* eslint-disable @suspensive/check-parent-suspense */
import { act, render, screen } from '@testing-library/react'
import { type ComponentType, Suspense, useState } from 'react'
import { afterEach, describe, expect, it, vi } from 'vitest'
From fa1a51c3f1b97f231bf1116ea24bd741f115502a Mon Sep 17 00:00:00 2001
From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com>
Date: Wed, 24 Sep 2025 15:26:35 +0000
Subject: [PATCH 5/6] [autofix.ci] apply automated fixes
---
.../docs/check-parent-suspense.md | 23 +++--
configs/eslint-config/plugin.js | 2 +-
.../rules/check-parent-suspense.js | 87 ++++++++-----------
configs/eslint-config/rules/index.js | 2 +-
.../tests/rules/check-parent-suspense.test.js | 26 +++---
5 files changed, 62 insertions(+), 78 deletions(-)
diff --git a/configs/eslint-config/docs/check-parent-suspense.md b/configs/eslint-config/docs/check-parent-suspense.md
index 779e70145..a1b87b7fb 100644
--- a/configs/eslint-config/docs/check-parent-suspense.md
+++ b/configs/eslint-config/docs/check-parent-suspense.md
@@ -9,16 +9,19 @@ This rule checks that components using Suspense-related hooks and components are
### Suspense-related APIs checked:
**Hooks:**
-- `useSuspenseQuery`
+
+- `useSuspenseQuery`
- `useSuspenseInfiniteQuery`
- `useSuspenseQueries`
**Components:**
+
- `SuspenseQuery`
-- `SuspenseInfiniteQuery`
+- `SuspenseInfiniteQuery`
- `SuspenseQueries`
**Lazy components:**
+
- Components created with `lazy()` function
## Examples
@@ -32,13 +35,9 @@ function MyComponent() {
return {data}
}
-// Component without Suspense wrapper
+// Component without Suspense wrapper
function MyComponent() {
- return (
-
- {(data) => {data}
}
-
- )
+ return {(data) => {data}
}
}
// Lazy component without Suspense wrapper
@@ -70,9 +69,7 @@ function MyComponent() {
function MyComponent() {
return (
Loading...}>
-
- {(data) => {data}
}
-
+ {(data) => {data}
}
)
}
@@ -94,7 +91,7 @@ function MyApp() {
The rule accepts an options object with the following properties:
- `suspenseHooks` (array): List of hook names to check. Default: `['useSuspenseQuery', 'useSuspenseInfiniteQuery', 'useSuspenseQueries']`
-- `suspenseComponents` (array): List of component names to check. Default: `['SuspenseQuery', 'SuspenseInfiniteQuery', 'SuspenseQueries']`
+- `suspenseComponents` (array): List of component names to check. Default: `['SuspenseQuery', 'SuspenseInfiniteQuery', 'SuspenseQueries']`
- `suspenseWrappers` (array): List of valid Suspense wrapper component names. Default: `['Suspense']`
### Example configuration
@@ -129,4 +126,4 @@ const MyComponent = () => {
## Related Rules
- React's built-in Suspense documentation: https://react.dev/reference/react/Suspense
-- @tanstack/react-query Suspense documentation
\ No newline at end of file
+- @tanstack/react-query Suspense documentation
diff --git a/configs/eslint-config/plugin.js b/configs/eslint-config/plugin.js
index 5af8395e5..44329e55f 100644
--- a/configs/eslint-config/plugin.js
+++ b/configs/eslint-config/plugin.js
@@ -10,4 +10,4 @@ export default {
},
},
},
-}
\ No newline at end of file
+}
diff --git a/configs/eslint-config/rules/check-parent-suspense.js b/configs/eslint-config/rules/check-parent-suspense.js
index ecb9ac988..8646c77b4 100644
--- a/configs/eslint-config/rules/check-parent-suspense.js
+++ b/configs/eslint-config/rules/check-parent-suspense.js
@@ -20,31 +20,21 @@ export default {
suspenseHooks: {
type: 'array',
items: { type: 'string' },
- default: [
- 'useSuspenseQuery',
- 'useSuspenseInfiniteQuery',
- 'useSuspenseQueries'
- ]
+ default: ['useSuspenseQuery', 'useSuspenseInfiniteQuery', 'useSuspenseQueries'],
},
suspenseComponents: {
type: 'array',
items: { type: 'string' },
- default: [
- 'SuspenseQuery',
- 'SuspenseInfiniteQuery',
- 'SuspenseQueries'
- ]
+ default: ['SuspenseQuery', 'SuspenseInfiniteQuery', 'SuspenseQueries'],
},
suspenseWrappers: {
type: 'array',
items: { type: 'string' },
- default: [
- 'Suspense'
- ]
- }
+ default: ['Suspense'],
+ },
},
additionalProperties: false,
- }
+ },
],
messages: {
missingSuspenseWrapper: 'Component using "{{name}}" must be wrapped in a Suspense boundary.',
@@ -54,18 +44,14 @@ export default {
create(context) {
const options = context.options[0] || {}
- const suspenseHooks = new Set(options.suspenseHooks || [
- 'useSuspenseQuery',
- 'useSuspenseInfiniteQuery',
- 'useSuspenseQueries'
- ])
- const suspenseComponents = new Set(options.suspenseComponents || [
- 'SuspenseQuery',
- 'SuspenseInfiniteQuery',
- 'SuspenseQueries'
- ])
+ const suspenseHooks = new Set(
+ options.suspenseHooks || ['useSuspenseQuery', 'useSuspenseInfiniteQuery', 'useSuspenseQueries']
+ )
+ const suspenseComponents = new Set(
+ options.suspenseComponents || ['SuspenseQuery', 'SuspenseInfiniteQuery', 'SuspenseQueries']
+ )
const suspenseWrappers = new Set(options.suspenseWrappers || ['Suspense'])
-
+
/**
* Get JSX element name from JSX identifier
*/
@@ -101,15 +87,14 @@ export default {
*/
function isSuspenseHook(node) {
if (node.type !== 'CallExpression') return false
-
+
let name = null
if (node.callee.type === 'Identifier') {
name = node.callee.name
- } else if (node.callee.type === 'MemberExpression' &&
- node.callee.property.type === 'Identifier') {
+ } else if (node.callee.type === 'MemberExpression' && node.callee.property.type === 'Identifier') {
name = node.callee.property.name
}
-
+
return name && suspenseHooks.has(name)
}
@@ -118,10 +103,10 @@ export default {
*/
function isLazyComponent(node) {
if (node.type !== 'JSXElement') return false
-
+
const elementName = getJSXElementName(node.openingElement.name)
if (!elementName) return false
-
+
// Check if the component was created with lazy()
const scope = context.sourceCode.getScope(node)
let currentScope = scope
@@ -135,7 +120,7 @@ export default {
}
currentScope = currentScope.upper
}
-
+
return false
}
@@ -144,16 +129,18 @@ export default {
*/
function isLazyCall(node) {
if (node.type !== 'CallExpression') return false
-
- return (node.callee.type === 'Identifier' && node.callee.name === 'lazy') ||
- (node.callee.type === 'MemberExpression' &&
- node.callee.property.type === 'Identifier' &&
- node.callee.property.name === 'lazy')
+
+ return (
+ (node.callee.type === 'Identifier' && node.callee.name === 'lazy') ||
+ (node.callee.type === 'MemberExpression' &&
+ node.callee.property.type === 'Identifier' &&
+ node.callee.property.name === 'lazy')
+ )
}
return {
// Check for suspense hooks
- 'CallExpression': function(node) {
+ CallExpression: function (node) {
if (isSuspenseHook(node) && !isInsideSuspenseBoundary(node)) {
let name = null
if (node.callee.type === 'Identifier') {
@@ -161,37 +148,37 @@ export default {
} else if (node.callee.type === 'MemberExpression') {
name = node.callee.property.name
}
-
+
context.report({
node: node.callee,
messageId: 'missingSuspenseWrapper',
- data: { name }
+ data: { name },
})
}
},
-
+
// Check suspense components and lazy components
- 'JSXElement': function(node) {
+ JSXElement: function (node) {
const elementName = getJSXElementName(node.openingElement.name)
-
+
// Check if this is a suspense component that needs wrapping
if (elementName && suspenseComponents.has(elementName) && !isInsideSuspenseBoundary(node)) {
context.report({
node: node.openingElement.name,
messageId: 'missingSuspenseWrapper',
- data: { name: elementName }
+ data: { name: elementName },
})
}
-
+
// Check if this is a lazy component that needs wrapping
if (isLazyComponent(node) && !isInsideSuspenseBoundary(node)) {
context.report({
node: node.openingElement.name,
messageId: 'missingLazySuspenseWrapper',
- data: { name: elementName }
+ data: { name: elementName },
})
}
- }
+ },
}
- }
-}
\ No newline at end of file
+ },
+}
diff --git a/configs/eslint-config/rules/index.js b/configs/eslint-config/rules/index.js
index 8d69c9370..98ce79bef 100644
--- a/configs/eslint-config/rules/index.js
+++ b/configs/eslint-config/rules/index.js
@@ -2,4 +2,4 @@ import checkParentSuspense from './check-parent-suspense.js'
export default {
'check-parent-suspense': checkParentSuspense,
-}
\ No newline at end of file
+}
diff --git a/configs/eslint-config/tests/rules/check-parent-suspense.test.js b/configs/eslint-config/tests/rules/check-parent-suspense.test.js
index 21dad2251..cc14fc751 100644
--- a/configs/eslint-config/tests/rules/check-parent-suspense.test.js
+++ b/configs/eslint-config/tests/rules/check-parent-suspense.test.js
@@ -43,7 +43,7 @@ ruleTester.run('check-parent-suspense', rule, {
}
`,
},
-
+
// Valid: SuspenseQuery wrapped in Suspense
{
code: `
@@ -58,7 +58,7 @@ ruleTester.run('check-parent-suspense', rule, {
}
`,
},
-
+
// Valid: lazy component wrapped in Suspense
{
code: `
@@ -73,7 +73,7 @@ ruleTester.run('check-parent-suspense', rule, {
}
`,
},
-
+
// Valid: nested Suspense boundaries
{
code: `
@@ -92,7 +92,7 @@ ruleTester.run('check-parent-suspense', rule, {
}
`,
},
-
+
// Valid: non-suspense hooks
{
code: `
@@ -102,7 +102,7 @@ ruleTester.run('check-parent-suspense', rule, {
}
`,
},
-
+
// Valid: regular components
{
code: `
@@ -129,7 +129,7 @@ ruleTester.run('check-parent-suspense', rule, {
},
],
},
-
+
// Invalid: useSuspenseInfiniteQuery not wrapped
{
code: `
@@ -145,7 +145,7 @@ ruleTester.run('check-parent-suspense', rule, {
},
],
},
-
+
// Invalid: useSuspenseQueries not wrapped
{
code: `
@@ -161,7 +161,7 @@ ruleTester.run('check-parent-suspense', rule, {
},
],
},
-
+
// Invalid: SuspenseQuery not wrapped in Suspense
{
code: `
@@ -180,7 +180,7 @@ ruleTester.run('check-parent-suspense', rule, {
},
],
},
-
+
// Invalid: SuspenseInfiniteQuery not wrapped
{
code: `
@@ -199,7 +199,7 @@ ruleTester.run('check-parent-suspense', rule, {
},
],
},
-
+
// Invalid: lazy component not wrapped in Suspense
{
code: `
@@ -216,7 +216,7 @@ ruleTester.run('check-parent-suspense', rule, {
},
],
},
-
+
// Invalid: multiple violations
{
code: `
@@ -242,7 +242,7 @@ ruleTester.run('check-parent-suspense', rule, {
},
],
},
-
+
// Invalid: member expression usage
{
code: `
@@ -261,4 +261,4 @@ ruleTester.run('check-parent-suspense', rule, {
],
})
-console.log('All tests passed!')
\ No newline at end of file
+console.log('All tests passed!')
From 214626958741210f1498eec406ee8b7d6e2a900d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 25 Sep 2025 16:49:30 +0000
Subject: [PATCH 6/6] Ignore check-parent-suspense ESLint rule in packages/*
Co-authored-by: manudeli <61593290+manudeli@users.noreply.github.com>
---
configs/eslint-config/index.js | 6 ++++++
packages/react-query-4/src/SuspenseInfiniteQuery.test-d.tsx | 1 -
packages/react-query-4/src/SuspenseInfiniteQuery.tsx | 1 -
packages/react-query-4/src/SuspenseQueries.test-d.tsx | 1 -
packages/react-query-4/src/SuspenseQueries.tsx | 1 -
packages/react-query-4/src/SuspenseQuery.test-d.tsx | 1 -
packages/react-query-4/src/SuspenseQuery.tsx | 1 -
packages/react-query-5/src/SuspenseInfiniteQuery.test-d.tsx | 1 -
packages/react-query-5/src/SuspenseInfiniteQuery.tsx | 1 -
packages/react-query-5/src/SuspenseQueries.test-d.tsx | 1 -
packages/react-query-5/src/SuspenseQueries.tsx | 1 -
packages/react-query-5/src/SuspenseQuery.test-d.tsx | 1 -
packages/react-query-5/src/SuspenseQuery.tsx | 1 -
packages/react/src/lazy.spec.tsx | 1 -
14 files changed, 6 insertions(+), 13 deletions(-)
diff --git a/configs/eslint-config/index.js b/configs/eslint-config/index.js
index 7aa64efb1..b063b02a3 100644
--- a/configs/eslint-config/index.js
+++ b/configs/eslint-config/index.js
@@ -139,6 +139,12 @@ export const suspensiveReactTypeScriptConfig = tseslint.config(
version: 'detect',
},
},
+ },
+ {
+ files: ['packages/**/*.{ts,tsx}'],
+ rules: {
+ '@suspensive/check-parent-suspense': 'off',
+ },
}
)
diff --git a/packages/react-query-4/src/SuspenseInfiniteQuery.test-d.tsx b/packages/react-query-4/src/SuspenseInfiniteQuery.test-d.tsx
index 3ce54ac1b..aab59bee4 100644
--- a/packages/react-query-4/src/SuspenseInfiniteQuery.test-d.tsx
+++ b/packages/react-query-4/src/SuspenseInfiniteQuery.test-d.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @suspensive/check-parent-suspense */
import { type InfiniteData, type UseSuspenseInfiniteQueryResult, infiniteQueryOptions } from '@tanstack/react-query'
import type { ReactNode } from 'react'
import { describe, expectTypeOf, it } from 'vitest'
diff --git a/packages/react-query-4/src/SuspenseInfiniteQuery.tsx b/packages/react-query-4/src/SuspenseInfiniteQuery.tsx
index 90c0c5ed3..59fa3d8dc 100644
--- a/packages/react-query-4/src/SuspenseInfiniteQuery.tsx
+++ b/packages/react-query-4/src/SuspenseInfiniteQuery.tsx
@@ -29,7 +29,6 @@ export const SuspenseInfiniteQuery = <
children: (query: UseSuspenseInfiniteQueryResult) => ReactNode
}) => (
<>
- {/* eslint-disable-next-line @suspensive/check-parent-suspense */}
{children(useSuspenseInfiniteQuery(options))}
>
)
diff --git a/packages/react-query-4/src/SuspenseQueries.test-d.tsx b/packages/react-query-4/src/SuspenseQueries.test-d.tsx
index 293aaeecc..cc71bcfbe 100644
--- a/packages/react-query-4/src/SuspenseQueries.test-d.tsx
+++ b/packages/react-query-4/src/SuspenseQueries.test-d.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @suspensive/check-parent-suspense */
import { type UseSuspenseQueryResult, queryOptions } from '@tanstack/react-query'
import type { ReactNode } from 'react'
import { describe, expectTypeOf, it } from 'vitest'
diff --git a/packages/react-query-4/src/SuspenseQueries.tsx b/packages/react-query-4/src/SuspenseQueries.tsx
index dbd5b0620..4c20fa482 100644
--- a/packages/react-query-4/src/SuspenseQueries.tsx
+++ b/packages/react-query-4/src/SuspenseQueries.tsx
@@ -25,7 +25,6 @@ export function SuspenseQueries({
}) {
return (
<>
- {/* eslint-disable-next-line @suspensive/check-parent-suspense */}
{children(useSuspenseQueries({ queries }))}
>
)
diff --git a/packages/react-query-4/src/SuspenseQuery.test-d.tsx b/packages/react-query-4/src/SuspenseQuery.test-d.tsx
index 7bb732ce3..9cabe02a3 100644
--- a/packages/react-query-4/src/SuspenseQuery.test-d.tsx
+++ b/packages/react-query-4/src/SuspenseQuery.test-d.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @suspensive/check-parent-suspense */
import { type UseSuspenseQueryResult, queryOptions } from '@tanstack/react-query'
import type { ReactNode } from 'react'
import { describe, expectTypeOf, it } from 'vitest'
diff --git a/packages/react-query-4/src/SuspenseQuery.tsx b/packages/react-query-4/src/SuspenseQuery.tsx
index 8405808f7..e63ca2b07 100644
--- a/packages/react-query-4/src/SuspenseQuery.tsx
+++ b/packages/react-query-4/src/SuspenseQuery.tsx
@@ -34,7 +34,6 @@ export const SuspenseQuery = <
children: (queryResult: UseSuspenseQueryResult) => ReactNode
}) => (
<>
- {/* eslint-disable-next-line @suspensive/check-parent-suspense */}
{children(useSuspenseQuery(options))}
>
)
diff --git a/packages/react-query-5/src/SuspenseInfiniteQuery.test-d.tsx b/packages/react-query-5/src/SuspenseInfiniteQuery.test-d.tsx
index 98d8afdeb..1ba2b4ae8 100644
--- a/packages/react-query-5/src/SuspenseInfiniteQuery.test-d.tsx
+++ b/packages/react-query-5/src/SuspenseInfiniteQuery.test-d.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @suspensive/check-parent-suspense */
import type { InfiniteData, UseSuspenseInfiniteQueryResult } from '@tanstack/react-query'
import type { ReactNode } from 'react'
import { describe, expectTypeOf, it } from 'vitest'
diff --git a/packages/react-query-5/src/SuspenseInfiniteQuery.tsx b/packages/react-query-5/src/SuspenseInfiniteQuery.tsx
index bb55b5f97..286023abf 100644
--- a/packages/react-query-5/src/SuspenseInfiniteQuery.tsx
+++ b/packages/react-query-5/src/SuspenseInfiniteQuery.tsx
@@ -37,7 +37,6 @@ export const SuspenseInfiniteQuery = <
children: (query: UseSuspenseInfiniteQueryResult) => ReactNode
}) => (
<>
- {/* eslint-disable-next-line @suspensive/check-parent-suspense */}
{children(useSuspenseInfiniteQuery(options))}
>
)
diff --git a/packages/react-query-5/src/SuspenseQueries.test-d.tsx b/packages/react-query-5/src/SuspenseQueries.test-d.tsx
index 4004518d9..ea9f15040 100644
--- a/packages/react-query-5/src/SuspenseQueries.test-d.tsx
+++ b/packages/react-query-5/src/SuspenseQueries.test-d.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @suspensive/check-parent-suspense */
import type { UseSuspenseQueryResult } from '@tanstack/react-query'
import type { ReactNode } from 'react'
import { describe, expectTypeOf, it } from 'vitest'
diff --git a/packages/react-query-5/src/SuspenseQueries.tsx b/packages/react-query-5/src/SuspenseQueries.tsx
index d853e684f..04c70c548 100644
--- a/packages/react-query-5/src/SuspenseQueries.tsx
+++ b/packages/react-query-5/src/SuspenseQueries.tsx
@@ -27,7 +27,6 @@ export function SuspenseQueries
- {/* eslint-disable-next-line @suspensive/check-parent-suspense */}
{children(useSuspenseQueries({ queries, combine }))}
>
)
diff --git a/packages/react-query-5/src/SuspenseQuery.test-d.tsx b/packages/react-query-5/src/SuspenseQuery.test-d.tsx
index 1d90a940b..1fca85a3e 100644
--- a/packages/react-query-5/src/SuspenseQuery.test-d.tsx
+++ b/packages/react-query-5/src/SuspenseQuery.test-d.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @suspensive/check-parent-suspense */
import type { UseSuspenseQueryResult } from '@tanstack/react-query'
import type { ReactNode } from 'react'
import { describe, expectTypeOf, it } from 'vitest'
diff --git a/packages/react-query-5/src/SuspenseQuery.tsx b/packages/react-query-5/src/SuspenseQuery.tsx
index 6533abfad..4f330f1e5 100644
--- a/packages/react-query-5/src/SuspenseQuery.tsx
+++ b/packages/react-query-5/src/SuspenseQuery.tsx
@@ -34,7 +34,6 @@ export const SuspenseQuery = <
children: (queryResult: UseSuspenseQueryResult) => ReactNode
}) => (
<>
- {/* eslint-disable-next-line @suspensive/check-parent-suspense */}
{children(useSuspenseQuery(options))}
>
)
diff --git a/packages/react/src/lazy.spec.tsx b/packages/react/src/lazy.spec.tsx
index cee78c765..30bdd8ed6 100644
--- a/packages/react/src/lazy.spec.tsx
+++ b/packages/react/src/lazy.spec.tsx
@@ -1,5 +1,4 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
-/* eslint-disable @suspensive/check-parent-suspense */
import { act, render, screen } from '@testing-library/react'
import { type ComponentType, Suspense, useState } from 'react'
import { afterEach, describe, expect, it, vi } from 'vitest'