@kubb/plugin-react-query 5.0.0-beta.3 → 5.0.0-beta.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +34 -83
  2. package/dist/{components-DTGLu4UV.js → components-BVmVgpLX.js} +379 -279
  3. package/dist/components-BVmVgpLX.js.map +1 -0
  4. package/dist/{components-dAKJEn9b.cjs → components-DLUeLMsz.cjs} +409 -279
  5. package/dist/components-DLUeLMsz.cjs.map +1 -0
  6. package/dist/components.cjs +1 -1
  7. package/dist/components.d.ts +4 -76
  8. package/dist/components.js +1 -1
  9. package/dist/{generators-CWEQsdO9.cjs → generators--AcF4Y4n.cjs} +332 -410
  10. package/dist/generators--AcF4Y4n.cjs.map +1 -0
  11. package/dist/{generators-C_fbcjpG.js → generators-BFn9CLBS.js} +333 -411
  12. package/dist/generators-BFn9CLBS.js.map +1 -0
  13. package/dist/generators.cjs +1 -1
  14. package/dist/generators.d.ts +41 -1
  15. package/dist/generators.js +1 -1
  16. package/dist/index.cjs +179 -26
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.ts +30 -1
  19. package/dist/index.js +179 -26
  20. package/dist/index.js.map +1 -1
  21. package/dist/types-DiZPLTXl.d.ts +400 -0
  22. package/extension.yaml +1507 -0
  23. package/package.json +16 -18
  24. package/src/components/InfiniteQuery.tsx +24 -13
  25. package/src/components/InfiniteQueryOptions.tsx +37 -55
  26. package/src/components/Mutation.tsx +35 -15
  27. package/src/components/MutationOptions.tsx +14 -13
  28. package/src/components/Query.tsx +14 -10
  29. package/src/components/QueryOptions.tsx +17 -34
  30. package/src/components/SuspenseInfiniteQuery.tsx +19 -13
  31. package/src/components/SuspenseInfiniteQueryOptions.tsx +28 -52
  32. package/src/components/SuspenseQuery.tsx +9 -10
  33. package/src/generators/customHookOptionsFileGenerator.tsx +18 -14
  34. package/src/generators/hookOptionsGenerator.tsx +42 -49
  35. package/src/generators/infiniteQueryGenerator.tsx +55 -76
  36. package/src/generators/mutationGenerator.tsx +51 -62
  37. package/src/generators/queryGenerator.tsx +52 -61
  38. package/src/generators/suspenseInfiniteQueryGenerator.tsx +50 -63
  39. package/src/generators/suspenseQueryGenerator.tsx +54 -74
  40. package/src/plugin.ts +45 -31
  41. package/src/resolvers/resolverReactQuery.ts +102 -6
  42. package/src/types.ts +199 -61
  43. package/src/utils.ts +10 -33
  44. package/dist/components-DTGLu4UV.js.map +0 -1
  45. package/dist/components-dAKJEn9b.cjs.map +0 -1
  46. package/dist/generators-CWEQsdO9.cjs.map +0 -1
  47. package/dist/generators-C_fbcjpG.js.map +0 -1
  48. package/dist/types-DfaFRSBf.d.ts +0 -284
@@ -1,19 +1,27 @@
1
1
  import path from 'node:path'
2
+ import { getOperationParameters, operationFileEntry, resolveOperationTypeNames } from '@internals/shared'
3
+ import { resolveZodSchemaNames } from '@internals/tanstack-query'
2
4
  import { ast, defineGenerator } from '@kubb/core'
3
5
  import { Client, pluginClientName } from '@kubb/plugin-client'
4
6
  import { pluginTsName } from '@kubb/plugin-ts'
5
7
  import { pluginZodName } from '@kubb/plugin-zod'
6
- import { File, jsxRenderer } from '@kubb/renderer-jsx'
8
+ import { File, jsxRendererSync } from '@kubb/renderer-jsx'
7
9
  import { difference } from 'remeda'
8
10
  import { QueryKey, SuspenseInfiniteQuery, SuspenseInfiniteQueryOptions } from '../components'
9
11
  import type { PluginReactQuery } from '../types'
10
- import { transformName } from '../utils.ts'
11
12
 
13
+ /**
14
+ * Built-in generator for `useSuspenseInfiniteQuery` hooks. Enabled when both
15
+ * `suspense` and `infinite` are configured. Combines suspense semantics with
16
+ * cursor-based pagination — handlers throw promises while loading and pull
17
+ * additional pages on demand.
18
+ */
12
19
  export const suspenseInfiniteQueryGenerator = defineGenerator<PluginReactQuery>({
13
20
  name: 'react-suspense-infinite-query',
14
- renderer: jsxRenderer,
21
+ renderer: jsxRendererSync,
15
22
  operation(node, ctx) {
16
- const { adapter, config, driver, resolver, root } = ctx
23
+ if (!ast.isHttpOperationNode(node)) return null
24
+ const { config, driver, resolver, root } = ctx
17
25
  const {
18
26
  output,
19
27
  query,
@@ -26,7 +34,6 @@ export const suspenseInfiniteQueryGenerator = defineGenerator<PluginReactQuery>(
26
34
  parser,
27
35
  client: clientOptions,
28
36
  group,
29
- transformers,
30
37
  customOptions,
31
38
  } = ctx.options
32
39
 
@@ -40,13 +47,13 @@ export const suspenseInfiniteQueryGenerator = defineGenerator<PluginReactQuery>(
40
47
  !isQuery &&
41
48
  difference(mutation ? mutation.methods : [], query ? query.methods : []).some((method) => node.method.toLowerCase() === method.toLowerCase())
42
49
  const isSuspense = !!suspense
43
- const infiniteOptions = infinite && typeof infinite === 'object' ? infinite : undefined
50
+ const infiniteOptions = infinite && typeof infinite === 'object' ? infinite : null
44
51
 
45
52
  if (!isQuery || isMutation || !isSuspense || !infiniteOptions) return null
46
53
 
47
54
  // Validate queryParam exists in operation's query parameters
48
55
  const normalizeKey = (key: string) => key.replace(/\?$/, '')
49
- const queryParamKeys = node.parameters.filter((p) => p.in === 'query').map((p) => p.name)
56
+ const queryParamKeys = getOperationParameters(node).query.map((p) => p.name)
50
57
  const hasQueryParam = infiniteOptions.queryParam ? queryParamKeys.some((k) => normalizeKey(k) === infiniteOptions.queryParam) : false
51
58
  const hasCursorParam = !infiniteOptions.cursorParam || true
52
59
 
@@ -54,64 +61,46 @@ export const suspenseInfiniteQueryGenerator = defineGenerator<PluginReactQuery>(
54
61
 
55
62
  const importPath = query ? query.importPath : '@tanstack/react-query'
56
63
 
57
- const baseName = resolver.resolveName(node.operationId)
58
- const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1)
59
- const queryName = transformName(`use${capitalize(baseName)}SuspenseInfinite`, 'function', transformers)
60
- const queryOptionsName = transformName(`${baseName}SuspenseInfiniteQueryOptions`, 'function', transformers)
61
- const queryKeyName = transformName(`${baseName}SuspenseInfiniteQueryKey`, 'const', transformers)
62
- const queryKeyTypeName = transformName(`${capitalize(baseName)}SuspenseInfiniteQueryKey`, 'type', transformers)
63
- const clientBaseName = transformName(`${baseName}SuspenseInfinite`, 'function', transformers)
64
+ const queryName = resolver.resolveSuspenseInfiniteQueryName(node)
65
+ const queryOptionsName = resolver.resolveSuspenseInfiniteQueryOptionsName(node)
66
+ const queryKeyName = resolver.resolveSuspenseInfiniteQueryKeyName(node)
67
+ const queryKeyTypeName = resolver.resolveSuspenseInfiniteQueryKeyTypeName(node)
68
+ const clientBaseName = resolver.resolveSuspenseInfiniteClientName(node)
64
69
 
65
70
  const meta = {
66
- file: resolver.resolveFile({ name: queryName, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path }, { root, output, group }),
67
- fileTs: tsResolver.resolveFile(
68
- { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
69
- { root, output: pluginTs.options?.output ?? output, group: pluginTs.options?.group },
70
- ),
71
+ file: resolver.resolveFile(operationFileEntry(node, queryName), { root, output, group: group ?? undefined }),
72
+ fileTs: tsResolver.resolveFile(operationFileEntry(node, node.operationId), {
73
+ root,
74
+ output: pluginTs.options?.output ?? output,
75
+ group: pluginTs.options?.group ?? undefined,
76
+ }),
71
77
  }
72
78
 
73
- const casedParams = ast.caseParams(node.parameters, paramsCasing)
74
- const pathParams = casedParams.filter((p) => p.in === 'path')
75
- const queryParams = casedParams.filter((p) => p.in === 'query')
76
- const headerParams = casedParams.filter((p) => p.in === 'header')
77
-
78
- const importedTypeNames = [
79
- node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : undefined,
80
- tsResolver.resolveResponseName(node),
81
- ...pathParams.map((p) => tsResolver.resolvePathParamsName(node, p)),
82
- ...queryParams.map((p) => tsResolver.resolveQueryParamsName(node, p)),
83
- ...headerParams.map((p) => tsResolver.resolveHeaderParamsName(node, p)),
84
- ...node.responses.map((res) => tsResolver.resolveResponseStatusName(node, res.statusCode)),
85
- ].filter(Boolean)
86
-
87
- const pluginZod = parser === 'zod' ? driver.getPlugin(pluginZodName) : undefined
88
- const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : undefined
79
+ const importedTypeNames = resolveOperationTypeNames(node, tsResolver, { paramsCasing, order: 'body-response-first' })
80
+
81
+ const pluginZod = parser === 'zod' ? driver.getPlugin(pluginZodName) : null
82
+ const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : null
89
83
  const fileZod = zodResolver
90
- ? zodResolver.resolveFile(
91
- { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
92
- { root, output: pluginZod?.options?.output ?? output, group: pluginZod?.options?.group },
93
- )
94
- : undefined
95
- const zodSchemaNames =
96
- zodResolver && parser === 'zod'
97
- ? [zodResolver.resolveResponseName?.(node), node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : undefined].filter(Boolean)
98
- : []
84
+ ? zodResolver.resolveFile(operationFileEntry(node, node.operationId), {
85
+ root,
86
+ output: pluginZod?.options?.output ?? output,
87
+ group: pluginZod?.options?.group ?? undefined,
88
+ })
89
+ : null
90
+ const zodSchemaNames = resolveZodSchemaNames(node, zodResolver)
99
91
 
100
92
  const clientPlugin = driver.getPlugin(pluginClientName)
101
93
  const hasClientPlugin = clientPlugin?.name === pluginClientName
102
94
  const shouldUseClientPlugin = hasClientPlugin && clientOptions.clientType !== 'class'
103
- const clientResolver = shouldUseClientPlugin ? driver.getResolver(pluginClientName) : undefined
95
+ const clientResolver = shouldUseClientPlugin ? driver.getResolver(pluginClientName) : null
104
96
 
105
97
  const clientFile = shouldUseClientPlugin
106
- ? clientResolver?.resolveFile(
107
- { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
108
- {
109
- root,
110
- output: clientPlugin?.options?.output ?? output,
111
- group: clientPlugin?.options?.group,
112
- },
113
- )
114
- : undefined
98
+ ? clientResolver?.resolveFile(operationFileEntry(node, node.operationId), {
99
+ root,
100
+ output: clientPlugin?.options?.output ?? output,
101
+ group: clientPlugin?.options?.group ?? undefined,
102
+ })
103
+ : null
115
104
 
116
105
  const resolvedClientName = shouldUseClientPlugin ? (clientResolver?.resolveName(node.operationId) ?? clientBaseName) : clientBaseName
117
106
 
@@ -120,29 +109,27 @@ export const suspenseInfiniteQueryGenerator = defineGenerator<PluginReactQuery>(
120
109
  baseName={meta.file.baseName}
121
110
  path={meta.file.path}
122
111
  meta={meta.file.meta}
123
- banner={resolver.resolveBanner(adapter.inputNode, { output, config })}
124
- footer={resolver.resolveFooter(adapter.inputNode, { output, config })}
112
+ banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
113
+ footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
125
114
  >
126
- {parser === 'zod' && fileZod && zodSchemaNames.length > 0 && (
127
- <File.Import name={zodSchemaNames as string[]} root={meta.file.path} path={fileZod.path} />
128
- )}
115
+ {fileZod && zodSchemaNames.length > 0 && <File.Import name={zodSchemaNames} root={meta.file.path} path={fileZod.path} />}
129
116
  {clientOptions.importPath ? (
130
117
  <>
131
- {!shouldUseClientPlugin && <File.Import name={'fetch'} path={clientOptions.importPath} />}
118
+ {!shouldUseClientPlugin && <File.Import name={'client'} path={clientOptions.importPath} />}
132
119
  <File.Import name={['Client', 'RequestConfig', 'ResponseErrorConfig']} path={clientOptions.importPath} isTypeOnly />
133
120
  {clientOptions.dataReturnType === 'full' && <File.Import name={['ResponseConfig']} path={clientOptions.importPath} isTypeOnly />}
134
121
  </>
135
122
  ) : (
136
123
  <>
137
- {!shouldUseClientPlugin && <File.Import name={['fetch']} root={meta.file.path} path={path.resolve(root, '.kubb/fetch.ts')} />}
124
+ {!shouldUseClientPlugin && <File.Import name={['client']} root={meta.file.path} path={path.resolve(root, '.kubb/client.ts')} />}
138
125
  <File.Import
139
126
  name={['Client', 'RequestConfig', 'ResponseErrorConfig']}
140
127
  root={meta.file.path}
141
- path={path.resolve(root, '.kubb/fetch.ts')}
128
+ path={path.resolve(root, '.kubb/client.ts')}
142
129
  isTypeOnly
143
130
  />
144
131
  {clientOptions.dataReturnType === 'full' && (
145
- <File.Import name={['ResponseConfig']} root={meta.file.path} path={path.resolve(root, '.kubb/fetch.ts')} isTypeOnly />
132
+ <File.Import name={['ResponseConfig']} root={meta.file.path} path={path.resolve(root, '.kubb/client.ts')} isTypeOnly />
146
133
  )}
147
134
  </>
148
135
  )}
@@ -1,33 +1,28 @@
1
1
  import path from 'node:path'
2
+ import { operationFileEntry, resolveOperationTypeNames } from '@internals/shared'
3
+ import { resolveZodSchemaNames } from '@internals/tanstack-query'
2
4
  import { ast, defineGenerator } from '@kubb/core'
3
5
  import { Client, pluginClientName } from '@kubb/plugin-client'
4
6
  import { pluginTsName } from '@kubb/plugin-ts'
5
7
  import { pluginZodName } from '@kubb/plugin-zod'
6
- import { File, jsxRenderer } from '@kubb/renderer-jsx'
8
+ import { File, jsxRendererSync } from '@kubb/renderer-jsx'
7
9
  import { difference } from 'remeda'
8
10
  import { QueryKey, QueryOptions, SuspenseQuery } from '../components'
9
11
  import type { PluginReactQuery } from '../types'
10
- import { transformName } from '../utils.ts'
11
12
 
13
+ /**
14
+ * Built-in generator for `useSuspenseQuery` hooks. Enabled when
15
+ * `pluginReactQuery({ suspense: {} })`. Emits one `useFooSuspenseQuery` hook
16
+ * per query operation. Suspense queries throw promises while loading and
17
+ * require a `<Suspense>` boundary in the React tree. TanStack Query v5+ only.
18
+ */
12
19
  export const suspenseQueryGenerator = defineGenerator<PluginReactQuery>({
13
20
  name: 'react-suspense-query',
14
- renderer: jsxRenderer,
21
+ renderer: jsxRendererSync,
15
22
  operation(node, ctx) {
16
- const { adapter, config, driver, resolver, root } = ctx
17
- const {
18
- output,
19
- query,
20
- mutation,
21
- suspense,
22
- paramsCasing,
23
- paramsType,
24
- pathParamsType,
25
- parser,
26
- client: clientOptions,
27
- group,
28
- transformers,
29
- customOptions,
30
- } = ctx.options
23
+ if (!ast.isHttpOperationNode(node)) return null
24
+ const { config, driver, resolver, root } = ctx
25
+ const { output, query, mutation, suspense, paramsCasing, paramsType, pathParamsType, parser, client: clientOptions, group, customOptions } = ctx.options
31
26
 
32
27
  const pluginTs = driver.getPlugin(pluginTsName)
33
28
  if (!pluginTs) return null
@@ -45,64 +40,50 @@ export const suspenseQueryGenerator = defineGenerator<PluginReactQuery>({
45
40
 
46
41
  const importPath = query ? query.importPath : '@tanstack/react-query'
47
42
 
48
- const baseName = resolver.resolveName(node.operationId)
49
- const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1)
50
- const queryName = transformName(`use${capitalize(baseName)}Suspense`, 'function', transformers)
51
- const queryOptionsName = transformName(`${baseName}SuspenseQueryOptions`, 'function', transformers)
52
- const queryKeyName = transformName(`${baseName}SuspenseQueryKey`, 'const', transformers)
53
- const queryKeyTypeName = transformName(`${capitalize(baseName)}SuspenseQueryKey`, 'type', transformers)
54
- const clientName = transformName(`${baseName}Suspense`, 'function', transformers)
43
+ const queryName = resolver.resolveSuspenseQueryName(node)
44
+ const queryOptionsName = resolver.resolveSuspenseQueryOptionsName(node)
45
+ const queryKeyName = resolver.resolveSuspenseQueryKeyName(node)
46
+ const queryKeyTypeName = resolver.resolveSuspenseQueryKeyTypeName(node)
47
+ const clientName = resolver.resolveSuspenseClientName(node)
55
48
 
56
49
  const meta = {
57
- file: resolver.resolveFile({ name: queryName, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path }, { root, output, group }),
58
- fileTs: tsResolver.resolveFile(
59
- { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
60
- { root, output: pluginTs.options?.output ?? output, group: pluginTs.options?.group },
61
- ),
50
+ file: resolver.resolveFile(operationFileEntry(node, queryName), { root, output, group: group ?? undefined }),
51
+ fileTs: tsResolver.resolveFile(operationFileEntry(node, node.operationId), {
52
+ root,
53
+ output: pluginTs.options?.output ?? output,
54
+ group: pluginTs.options?.group ?? undefined,
55
+ }),
62
56
  }
63
57
 
64
- const casedParams = ast.caseParams(node.parameters, paramsCasing)
65
- const pathParams = casedParams.filter((p) => p.in === 'path')
66
- const queryParams = casedParams.filter((p) => p.in === 'query')
67
- const headerParams = casedParams.filter((p) => p.in === 'header')
68
-
69
- const importedTypeNames = [
70
- node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : undefined,
71
- tsResolver.resolveResponseName(node),
72
- ...pathParams.map((p) => tsResolver.resolvePathParamsName(node, p)),
73
- ...queryParams.map((p) => tsResolver.resolveQueryParamsName(node, p)),
74
- ...headerParams.map((p) => tsResolver.resolveHeaderParamsName(node, p)),
75
- ...node.responses.map((res) => tsResolver.resolveResponseStatusName(node, res.statusCode)),
76
- ].filter((name): name is string => !!name && name !== queryKeyTypeName)
77
-
78
- const pluginZod = parser === 'zod' ? driver.getPlugin(pluginZodName) : undefined
79
- const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : undefined
58
+ const importedTypeNames = resolveOperationTypeNames(node, tsResolver, {
59
+ paramsCasing,
60
+ exclude: [queryKeyTypeName],
61
+ order: 'body-response-first',
62
+ })
63
+
64
+ const pluginZod = parser === 'zod' ? driver.getPlugin(pluginZodName) : null
65
+ const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : null
80
66
  const fileZod = zodResolver
81
- ? zodResolver.resolveFile(
82
- { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
83
- { root, output: pluginZod?.options?.output ?? output, group: pluginZod?.options?.group },
84
- )
85
- : undefined
86
- const zodSchemaNames =
87
- zodResolver && parser === 'zod'
88
- ? [zodResolver.resolveResponseName?.(node), node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : undefined].filter(Boolean)
89
- : []
67
+ ? zodResolver.resolveFile(operationFileEntry(node, node.operationId), {
68
+ root,
69
+ output: pluginZod?.options?.output ?? output,
70
+ group: pluginZod?.options?.group ?? undefined,
71
+ })
72
+ : null
73
+ const zodSchemaNames = resolveZodSchemaNames(node, zodResolver)
90
74
 
91
75
  const clientPlugin = driver.getPlugin(pluginClientName)
92
76
  const hasClientPlugin = clientPlugin?.name === pluginClientName
93
77
  const shouldUseClientPlugin = hasClientPlugin && clientOptions.clientType !== 'class'
94
- const clientResolver = shouldUseClientPlugin ? driver.getResolver(pluginClientName) : undefined
78
+ const clientResolver = shouldUseClientPlugin ? driver.getResolver(pluginClientName) : null
95
79
 
96
80
  const clientFile = shouldUseClientPlugin
97
- ? clientResolver?.resolveFile(
98
- { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
99
- {
100
- root,
101
- output: clientPlugin?.options?.output ?? output,
102
- group: clientPlugin?.options?.group,
103
- },
104
- )
105
- : undefined
81
+ ? clientResolver?.resolveFile(operationFileEntry(node, node.operationId), {
82
+ root,
83
+ output: clientPlugin?.options?.output ?? output,
84
+ group: clientPlugin?.options?.group ?? undefined,
85
+ })
86
+ : null
106
87
 
107
88
  const resolvedClientName = shouldUseClientPlugin ? (clientResolver?.resolveName(node.operationId) ?? clientName) : clientName
108
89
 
@@ -111,29 +92,27 @@ export const suspenseQueryGenerator = defineGenerator<PluginReactQuery>({
111
92
  baseName={meta.file.baseName}
112
93
  path={meta.file.path}
113
94
  meta={meta.file.meta}
114
- banner={resolver.resolveBanner(adapter.inputNode, { output, config })}
115
- footer={resolver.resolveFooter(adapter.inputNode, { output, config })}
95
+ banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
96
+ footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
116
97
  >
117
- {parser === 'zod' && fileZod && zodSchemaNames.length > 0 && (
118
- <File.Import name={zodSchemaNames as string[]} root={meta.file.path} path={fileZod.path} />
119
- )}
98
+ {fileZod && zodSchemaNames.length > 0 && <File.Import name={zodSchemaNames} root={meta.file.path} path={fileZod.path} />}
120
99
  {clientOptions.importPath ? (
121
100
  <>
122
- {!shouldUseClientPlugin && <File.Import name={'fetch'} path={clientOptions.importPath} />}
101
+ {!shouldUseClientPlugin && <File.Import name={'client'} path={clientOptions.importPath} />}
123
102
  <File.Import name={['Client', 'RequestConfig', 'ResponseErrorConfig']} path={clientOptions.importPath} isTypeOnly />
124
103
  {clientOptions.dataReturnType === 'full' && <File.Import name={['ResponseConfig']} path={clientOptions.importPath} isTypeOnly />}
125
104
  </>
126
105
  ) : (
127
106
  <>
128
- {!shouldUseClientPlugin && <File.Import name={['fetch']} root={meta.file.path} path={path.resolve(root, '.kubb/fetch.ts')} />}
107
+ {!shouldUseClientPlugin && <File.Import name={['client']} root={meta.file.path} path={path.resolve(root, '.kubb/client.ts')} />}
129
108
  <File.Import
130
109
  name={['Client', 'RequestConfig', 'ResponseErrorConfig']}
131
110
  root={meta.file.path}
132
- path={path.resolve(root, '.kubb/fetch.ts')}
111
+ path={path.resolve(root, '.kubb/client.ts')}
133
112
  isTypeOnly
134
113
  />
135
114
  {clientOptions.dataReturnType === 'full' && (
136
- <File.Import name={['ResponseConfig']} root={meta.file.path} path={path.resolve(root, '.kubb/fetch.ts')} isTypeOnly />
115
+ <File.Import name={['ResponseConfig']} root={meta.file.path} path={path.resolve(root, '.kubb/client.ts')} isTypeOnly />
137
116
  )}
138
117
  </>
139
118
  )}
@@ -181,6 +160,7 @@ export const suspenseQueryGenerator = defineGenerator<PluginReactQuery>({
181
160
  paramsType={paramsType}
182
161
  pathParamsType={pathParamsType}
183
162
  dataReturnType={clientOptions.dataReturnType || 'data'}
163
+ suspense
184
164
  />
185
165
 
186
166
  {suspense && (
package/src/plugin.ts CHANGED
@@ -1,14 +1,13 @@
1
1
  import path from 'node:path'
2
- import { camelCase } from '@internals/utils'
3
- import { ast, definePlugin, type Group } from '@kubb/core'
2
+ import { createGroupConfig } from '@internals/shared'
3
+ import { ast, definePlugin } from '@kubb/core'
4
4
  import { pluginClientName } from '@kubb/plugin-client'
5
5
  import { source as axiosClientSource } from '@kubb/plugin-client/templates/clients/axios.source'
6
6
  import { source as fetchClientSource } from '@kubb/plugin-client/templates/clients/fetch.source'
7
7
  import { source as configSource } from '@kubb/plugin-client/templates/config.source'
8
8
  import { pluginTsName } from '@kubb/plugin-ts'
9
9
  import { pluginZodName } from '@kubb/plugin-zod'
10
- import { MutationKey } from './components/MutationKey.tsx'
11
- import { QueryKey } from './components/QueryKey.tsx'
10
+ import { mutationKeyTransformer, queryKeyTransformer } from '@internals/tanstack-query'
12
11
  import {
13
12
  customHookOptionsFileGenerator,
14
13
  hookOptionsGenerator,
@@ -21,8 +20,37 @@ import {
21
20
  import { resolverReactQuery } from './resolvers/resolverReactQuery.ts'
22
21
  import type { PluginReactQuery } from './types.ts'
23
22
 
23
+ /**
24
+ * Canonical plugin name for `@kubb/plugin-react-query`. Used for driver lookups
25
+ * and cross-plugin dependency references.
26
+ */
24
27
  export const pluginReactQueryName = 'plugin-react-query' satisfies PluginReactQuery['name']
25
28
 
29
+ /**
30
+ * Generates one TanStack Query hook per OpenAPI operation for React. Queries
31
+ * become `useFooQuery`/`useFooSuspenseQuery`/`useFooInfiniteQuery`; mutations
32
+ * become `useFooMutation`. Each hook is fully typed: query keys, input
33
+ * variables, response data, and error shape all come from the spec.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * import { defineConfig } from 'kubb'
38
+ * import { pluginTs } from '@kubb/plugin-ts'
39
+ * import { pluginReactQuery } from '@kubb/plugin-react-query'
40
+ *
41
+ * export default defineConfig({
42
+ * input: { path: './petStore.yaml' },
43
+ * output: { path: './src/gen' },
44
+ * plugins: [
45
+ * pluginTs(),
46
+ * pluginReactQuery({
47
+ * output: { path: './hooks' },
48
+ * suspense: {},
49
+ * }),
50
+ * ],
51
+ * })
52
+ * ```
53
+ */
26
54
  export const pluginReactQuery = definePlugin<PluginReactQuery>((options) => {
27
55
  const {
28
56
  output = { path: 'hooks', barrelType: 'named' },
@@ -30,16 +58,15 @@ export const pluginReactQuery = definePlugin<PluginReactQuery>((options) => {
30
58
  exclude = [],
31
59
  include,
32
60
  override = [],
33
- parser = 'client',
61
+ parser = false,
34
62
  suspense = {},
35
63
  infinite = false,
36
- transformers = {},
37
64
  paramsType = 'inline',
38
65
  pathParamsType = paramsType === 'object' ? 'object' : options.pathParamsType || 'inline',
39
66
  mutation = {},
40
67
  query = {},
41
- mutationKey = MutationKey.getTransformer,
42
- queryKey = QueryKey.getTransformer,
68
+ mutationKey = mutationKeyTransformer,
69
+ queryKey = queryKeyTransformer,
43
70
  customOptions,
44
71
  paramsCasing,
45
72
  client,
@@ -61,33 +88,20 @@ export const pluginReactQuery = definePlugin<PluginReactQuery>((options) => {
61
88
  mutationGenerator,
62
89
  hookOptionsGenerator,
63
90
  customHookOptionsFileGenerator,
64
- ].filter(Boolean)
91
+ ].filter((generator): generator is NonNullable<typeof generator> => Boolean(generator))
65
92
 
66
- const groupConfig = group
67
- ? ({
68
- ...group,
69
- name: group.name
70
- ? group.name
71
- : (ctx: { group: string }) => {
72
- if (group.type === 'path') {
73
- return `${ctx.group.split('/')[1]}`
74
- }
75
- return `${camelCase(ctx.group)}Controller`
76
- },
77
- } satisfies Group)
78
- : undefined
93
+ const groupConfig = createGroupConfig(group, { suffix: 'Controller', honorName: true })
79
94
 
80
95
  return {
81
96
  name: pluginReactQueryName,
82
97
  options,
83
- dependencies: [pluginTsName, parser === 'zod' ? pluginZodName : undefined].filter(Boolean),
98
+ dependencies: [pluginTsName, parser === 'zod' ? pluginZodName : undefined].filter((dependency): dependency is string => Boolean(dependency)),
84
99
  hooks: {
85
100
  'kubb:plugin:setup'(ctx) {
86
101
  const resolver = userResolver ? { ...resolverReactQuery, ...userResolver } : resolverReactQuery
87
102
 
88
103
  ctx.setOptions({
89
104
  output,
90
- transformers,
91
105
  client: {
92
106
  bundle: client?.bundle,
93
107
  baseURL: client?.baseURL,
@@ -119,14 +133,14 @@ export const pluginReactQuery = definePlugin<PluginReactQuery>((options) => {
119
133
  ? {
120
134
  queryParam: 'id',
121
135
  initialPageParam: 0,
122
- cursorParam: undefined,
123
- nextParam: undefined,
124
- previousParam: undefined,
136
+ cursorParam: null,
137
+ nextParam: null,
138
+ previousParam: null,
125
139
  ...infinite,
126
140
  }
127
141
  : false,
128
142
  suspense,
129
- customOptions: customOptions ? { name: 'useCustomHookOptions', ...customOptions } : undefined,
143
+ customOptions: customOptions ? { name: 'useCustomHookOptions', ...customOptions } : null,
130
144
  parser,
131
145
  paramsType,
132
146
  pathParamsType,
@@ -154,11 +168,11 @@ export const pluginReactQuery = definePlugin<PluginReactQuery>((options) => {
154
168
 
155
169
  if (client?.bundle && !hasClientPlugin && !clientImportPath) {
156
170
  ctx.injectFile({
157
- baseName: 'fetch.ts',
158
- path: path.resolve(root, '.kubb/fetch.ts'),
171
+ baseName: 'client.ts',
172
+ path: path.resolve(root, '.kubb/client.ts'),
159
173
  sources: [
160
174
  ast.createSource({
161
- name: 'fetch',
175
+ name: 'client',
162
176
  nodes: [ast.createText(clientName === 'fetch' ? fetchClientSource : axiosClientSource)],
163
177
  isExportable: true,
164
178
  isIndexable: true,
@@ -2,21 +2,117 @@ import { camelCase } from '@internals/utils'
2
2
  import { defineResolver } from '@kubb/core'
3
3
  import type { PluginReactQuery } from '../types.ts'
4
4
 
5
+ function capitalize(name: string): string {
6
+ return `${name.charAt(0).toUpperCase()}${name.slice(1)}`
7
+ }
8
+
5
9
  /**
6
- * Naming convention resolver for React Query plugin.
10
+ * Default resolver used by `@kubb/plugin-react-query`. Decides the names and
11
+ * file paths for every generated TanStack Query hook (`useFooQuery`,
12
+ * `useFooMutation`, `useFooInfiniteQuery`, ...) and its companion helpers
13
+ * (`fooQueryKey`, `fooQueryOptions`).
14
+ *
15
+ * Functions and files use camelCase; hooks get the `use` prefix; suspense and
16
+ * infinite variants are suffixed with `Suspense`/`Infinite`.
7
17
  *
8
- * Provides default naming helpers using camelCase for functions and file paths.
18
+ * @example Resolve hook and helper names
19
+ * ```ts
20
+ * import { resolverReactQuery } from '@kubb/plugin-react-query'
9
21
  *
10
- * @example
11
- * `resolverReactQuery.default('list pets', 'function') // 'listPets'`
22
+ * resolverReactQuery.resolveQueryName(operationNode) // 'useGetPetById'
23
+ * resolverReactQuery.resolveMutationName(operationNode) // 'useUpdatePet'
24
+ * resolverReactQuery.resolveQueryKeyName(operationNode) // 'getPetByIdQueryKey'
25
+ * resolverReactQuery.resolveQueryOptionsName(operationNode) // 'getPetByIdQueryOptions'
26
+ * ```
12
27
  */
13
- export const resolverReactQuery = defineResolver<PluginReactQuery>((ctx) => ({
28
+ export const resolverReactQuery = defineResolver<PluginReactQuery>(() => ({
14
29
  name: 'default',
15
30
  pluginName: 'plugin-react-query',
16
31
  default(name, type) {
17
32
  return camelCase(name, { isFile: type === 'file' })
18
33
  },
19
34
  resolveName(name) {
20
- return ctx.default(name, 'function')
35
+ return this.default(name, 'function')
36
+ },
37
+ resolvePathName(name, type) {
38
+ return this.default(name, type)
39
+ },
40
+ resolveQueryName(node) {
41
+ return `use${capitalize(this.resolveName(node.operationId))}`
42
+ },
43
+ resolveSuspenseQueryName(node) {
44
+ return `use${capitalize(this.resolveName(node.operationId))}Suspense`
45
+ },
46
+ resolveInfiniteQueryName(node) {
47
+ return `use${capitalize(this.resolveName(node.operationId))}Infinite`
48
+ },
49
+ resolveSuspenseInfiniteQueryName(node) {
50
+ return `use${capitalize(this.resolveName(node.operationId))}SuspenseInfinite`
51
+ },
52
+ resolveMutationName(node) {
53
+ return `use${capitalize(this.resolveName(node.operationId))}`
54
+ },
55
+ resolveQueryOptionsName(node) {
56
+ return `${this.resolveName(node.operationId)}QueryOptions`
57
+ },
58
+ resolveSuspenseQueryOptionsName(node) {
59
+ return `${this.resolveName(node.operationId)}SuspenseQueryOptions`
60
+ },
61
+ resolveInfiniteQueryOptionsName(node) {
62
+ return `${this.resolveName(node.operationId)}InfiniteQueryOptions`
63
+ },
64
+ resolveSuspenseInfiniteQueryOptionsName(node) {
65
+ return `${this.resolveName(node.operationId)}SuspenseInfiniteQueryOptions`
66
+ },
67
+ resolveMutationOptionsName(node) {
68
+ return `${this.resolveName(node.operationId)}MutationOptions`
69
+ },
70
+ resolveQueryKeyName(node) {
71
+ return `${this.resolveName(node.operationId)}QueryKey`
72
+ },
73
+ resolveSuspenseQueryKeyName(node) {
74
+ return `${this.resolveName(node.operationId)}SuspenseQueryKey`
75
+ },
76
+ resolveInfiniteQueryKeyName(node) {
77
+ return `${this.resolveName(node.operationId)}InfiniteQueryKey`
78
+ },
79
+ resolveSuspenseInfiniteQueryKeyName(node) {
80
+ return `${this.resolveName(node.operationId)}SuspenseInfiniteQueryKey`
81
+ },
82
+ resolveMutationKeyName(node) {
83
+ return `${this.resolveName(node.operationId)}MutationKey`
84
+ },
85
+ resolveQueryKeyTypeName(node) {
86
+ return `${capitalize(this.resolveName(node.operationId))}QueryKey`
87
+ },
88
+ resolveSuspenseQueryKeyTypeName(node) {
89
+ return `${capitalize(this.resolveName(node.operationId))}SuspenseQueryKey`
90
+ },
91
+ resolveInfiniteQueryKeyTypeName(node) {
92
+ return `${capitalize(this.resolveName(node.operationId))}InfiniteQueryKey`
93
+ },
94
+ resolveSuspenseInfiniteQueryKeyTypeName(node) {
95
+ return `${capitalize(this.resolveName(node.operationId))}SuspenseInfiniteQueryKey`
96
+ },
97
+ resolveMutationTypeName(node) {
98
+ return capitalize(this.resolveName(node.operationId))
99
+ },
100
+ resolveClientName(node) {
101
+ return this.resolveName(node.operationId)
102
+ },
103
+ resolveSuspenseClientName(node) {
104
+ return `${this.resolveName(node.operationId)}Suspense`
105
+ },
106
+ resolveInfiniteClientName(node) {
107
+ return `${this.resolveName(node.operationId)}Infinite`
108
+ },
109
+ resolveSuspenseInfiniteClientName(node) {
110
+ return `${this.resolveName(node.operationId)}SuspenseInfinite`
111
+ },
112
+ resolveHookOptionsName() {
113
+ return 'HookOptions'
114
+ },
115
+ resolveCustomHookOptionsName() {
116
+ return 'getCustomHookOptions'
21
117
  },
22
118
  }))