@teleporthq/teleport-plugin-next-data-source 0.42.5 → 0.42.6

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 (35) hide show
  1. package/dist/cjs/array-mapper-pagination.d.ts +5 -0
  2. package/dist/cjs/array-mapper-pagination.d.ts.map +1 -1
  3. package/dist/cjs/array-mapper-pagination.js.map +1 -1
  4. package/dist/cjs/array-mapper-registry.d.ts +84 -0
  5. package/dist/cjs/array-mapper-registry.d.ts.map +1 -0
  6. package/dist/cjs/array-mapper-registry.js +291 -0
  7. package/dist/cjs/array-mapper-registry.js.map +1 -0
  8. package/dist/cjs/index.d.ts.map +1 -1
  9. package/dist/cjs/index.js +36 -14
  10. package/dist/cjs/index.js.map +1 -1
  11. package/dist/cjs/pagination-plugin.d.ts +1 -3
  12. package/dist/cjs/pagination-plugin.d.ts.map +1 -1
  13. package/dist/cjs/pagination-plugin.js +895 -1405
  14. package/dist/cjs/pagination-plugin.js.map +1 -1
  15. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  16. package/dist/esm/array-mapper-pagination.d.ts +5 -0
  17. package/dist/esm/array-mapper-pagination.d.ts.map +1 -1
  18. package/dist/esm/array-mapper-pagination.js.map +1 -1
  19. package/dist/esm/array-mapper-registry.d.ts +84 -0
  20. package/dist/esm/array-mapper-registry.d.ts.map +1 -0
  21. package/dist/esm/array-mapper-registry.js +287 -0
  22. package/dist/esm/array-mapper-registry.js.map +1 -0
  23. package/dist/esm/index.d.ts.map +1 -1
  24. package/dist/esm/index.js +36 -14
  25. package/dist/esm/index.js.map +1 -1
  26. package/dist/esm/pagination-plugin.d.ts +1 -3
  27. package/dist/esm/pagination-plugin.d.ts.map +1 -1
  28. package/dist/esm/pagination-plugin.js +896 -1406
  29. package/dist/esm/pagination-plugin.js.map +1 -1
  30. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  31. package/package.json +2 -2
  32. package/src/array-mapper-pagination.ts +5 -0
  33. package/src/array-mapper-registry.ts +408 -0
  34. package/src/index.ts +24 -1
  35. package/src/pagination-plugin.ts +1846 -2201
@@ -5,20 +5,232 @@ import {
5
5
  FileType,
6
6
  } from '@teleporthq/teleport-types'
7
7
  import * as types from '@babel/types'
8
- import { generatePaginationLogic, ArrayMapperPaginationInfo } from './array-mapper-pagination'
9
- import { extractDataSourceIntoNextAPIFolder } from './utils'
10
-
11
- interface DetectedPagination {
12
- paginationNodeClass: string
13
- prevButtonClass: string | null
14
- nextButtonClass: string | null
8
+ import { StringUtils } from '@teleporthq/teleport-shared'
9
+ import { generateSafeFileName } from './utils'
10
+
11
+ // ==================== UIDL-FIRST STATE MANAGEMENT ====================
12
+ // This module uses a UIDL-first approach: we scan the UIDL FIRST to identify
13
+ // ALL data source usages and assign unique state IDs BEFORE any JSX processing.
14
+ // This ensures consistent state mapping across DataProviders, search inputs, and pagination buttons.
15
+
16
+ interface DataSourceUsage {
17
+ // Unique sequential index for this usage (0, 1, 2, ...)
18
+ index: number
19
+ // The data-source-list renderPropIdentifier (e.g., "dsadsa3_users_data")
15
20
  dataSourceIdentifier: string
16
- dataProviderJSX: any
17
- arrayMapperRenderProp?: string
18
- searchInputClass?: string | null
19
- searchInputJSX?: any
21
+ // The cms-list-repeater renderPropIdentifier (e.g., "context_1i871")
22
+ arrayMapperRenderProp: string
23
+ // Resource definition from UIDL
24
+ resourceDefinition: {
25
+ dataSourceId: string
26
+ tableName: string
27
+ dataSourceType: string
28
+ }
29
+ // Pagination config
30
+ paginated: boolean
31
+ perPage: number
32
+ // Search config
33
+ searchEnabled: boolean
34
+ searchDebounce: number
35
+ // Query columns from resource params
36
+ queryColumns: string[]
37
+ // Computed category
38
+ category: 'paginated+search' | 'paginated-only' | 'search-only' | 'plain'
39
+ }
40
+
41
+ interface StateRegistry {
42
+ usages: DataSourceUsage[]
43
+ // Map from dataSourceIdentifier to all usages with that identifier
44
+ byDataSourceId: Map<string, DataSourceUsage[]>
45
+ // Map from arrayMapperRenderProp to usage
46
+ byArrayMapperRenderProp: Map<string, DataSourceUsage>
47
+ }
48
+
49
+ // Scan UIDL to find all data source usages and build a registry
50
+ function buildStateRegistry(uidlNode: any): StateRegistry {
51
+ const usages: DataSourceUsage[] = []
52
+ const byDataSourceId = new Map<string, DataSourceUsage[]>()
53
+ const byArrayMapperRenderProp = new Map<string, DataSourceUsage>()
54
+ let index = 0
55
+
56
+ const traverse = (
57
+ node: any,
58
+ parentDataSource?: { identifier: string; resourceDef: any; resourceParams: any }
59
+ ): void => {
60
+ if (!node || typeof node !== 'object') {
61
+ return
62
+ }
63
+
64
+ // Found a data-source-list (DataProvider)
65
+ if (node.type === 'data-source-list' && node.content?.renderPropIdentifier) {
66
+ const dsIdentifier = node.content.renderPropIdentifier
67
+ const resourceDef = node.content.resourceDefinition || {}
68
+ const resourceParams = node.content.resource?.params || {}
69
+
70
+ // Look for cms-list-repeater inside this data-source-list
71
+ const newParent = {
72
+ identifier: dsIdentifier,
73
+ resourceDef,
74
+ resourceParams,
75
+ }
76
+
77
+ // Traverse into success/error/loading nodes
78
+ if (node.content.nodes?.success) {
79
+ traverse(node.content.nodes.success, newParent)
80
+ }
81
+ if (node.content.nodes?.error) {
82
+ traverse(node.content.nodes.error, newParent)
83
+ }
84
+ if (node.content.nodes?.loading) {
85
+ traverse(node.content.nodes.loading, newParent)
86
+ }
87
+ return
88
+ }
89
+
90
+ // Found a cms-list-repeater (Repeater with pagination/search config)
91
+ const isCmsListRepeater =
92
+ node.type === 'cms-list-repeater' ||
93
+ (node.type === 'element' && node.content?.elementType === 'cms-list-repeater')
94
+
95
+ if (isCmsListRepeater && parentDataSource) {
96
+ const content = node.content || node
97
+ const arrayMapperRenderProp = content.renderPropIdentifier
98
+
99
+ if (arrayMapperRenderProp) {
100
+ // Extract query columns from parent's resource params
101
+ let queryColumns: string[] = []
102
+ if (parentDataSource.resourceParams?.queryColumns?.content) {
103
+ queryColumns = parentDataSource.resourceParams.queryColumns.content
104
+ }
105
+
106
+ // Extract limit from parent's resource params (for plain array mappers)
107
+ let limit = 0
108
+ if (parentDataSource.resourceParams?.limit?.content) {
109
+ limit = parentDataSource.resourceParams.limit.content
110
+ }
111
+
112
+ // For paginated mappers, use perPage from cms-list-repeater
113
+ // For plain mappers, use limit from data-source-list resource params
114
+ const effectivePerPage = content.paginated ? content.perPage : limit || content.perPage
115
+
116
+ const usage: DataSourceUsage = {
117
+ index: index++,
118
+ dataSourceIdentifier: parentDataSource.identifier,
119
+ arrayMapperRenderProp,
120
+ resourceDefinition: {
121
+ dataSourceId: parentDataSource.resourceDef.dataSourceId || '',
122
+ tableName: parentDataSource.resourceDef.tableName || '',
123
+ dataSourceType: parentDataSource.resourceDef.dataSourceType || '',
124
+ },
125
+ paginated: !!content.paginated,
126
+ perPage: effectivePerPage,
127
+ searchEnabled: !!content.searchEnabled,
128
+ searchDebounce: content.searchDebounce || 300,
129
+ queryColumns,
130
+ category: 'plain',
131
+ }
132
+
133
+ // Determine category
134
+ if (usage.paginated && usage.searchEnabled) {
135
+ usage.category = 'paginated+search'
136
+ } else if (usage.paginated) {
137
+ usage.category = 'paginated-only'
138
+ } else if (usage.searchEnabled) {
139
+ usage.category = 'search-only'
140
+ }
141
+
142
+ usages.push(usage)
143
+
144
+ // Add to maps
145
+ if (!byDataSourceId.has(usage.dataSourceIdentifier)) {
146
+ byDataSourceId.set(usage.dataSourceIdentifier, [])
147
+ }
148
+ byDataSourceId.get(usage.dataSourceIdentifier)!.push(usage)
149
+ byArrayMapperRenderProp.set(arrayMapperRenderProp, usage)
150
+ }
151
+
152
+ // Continue traversing inside the repeater
153
+ if (content.nodes?.list) {
154
+ traverse(content.nodes.list, parentDataSource)
155
+ }
156
+ return
157
+ }
158
+
159
+ // Recurse into children
160
+ if (node.content?.children && Array.isArray(node.content.children)) {
161
+ for (const child of node.content.children) {
162
+ traverse(child, parentDataSource)
163
+ }
164
+ }
165
+ if (node.content?.node) {
166
+ traverse(node.content.node, parentDataSource)
167
+ }
168
+ if (node.content?.nodes) {
169
+ if (node.content.nodes.success) {
170
+ traverse(node.content.nodes.success, parentDataSource)
171
+ }
172
+ if (node.content.nodes.error) {
173
+ traverse(node.content.nodes.error, parentDataSource)
174
+ }
175
+ if (node.content.nodes.loading) {
176
+ traverse(node.content.nodes.loading, parentDataSource)
177
+ }
178
+ if (node.content.nodes.list) {
179
+ traverse(node.content.nodes.list, parentDataSource)
180
+ }
181
+ if (node.content.nodes.empty) {
182
+ traverse(node.content.nodes.empty, parentDataSource)
183
+ }
184
+ }
185
+ if (Array.isArray(node.children)) {
186
+ for (const child of node.children) {
187
+ traverse(child, parentDataSource)
188
+ }
189
+ }
190
+ }
191
+
192
+ traverse(uidlNode)
193
+
194
+ return { usages, byDataSourceId, byArrayMapperRenderProp }
195
+ }
196
+
197
+ // Generate state variable names for a usage
198
+ function getStateVarsForUsage(usage: DataSourceUsage): {
199
+ pageStateVar: string
200
+ setPageStateVar: string
201
+ maxPagesStateVar: string
202
+ setMaxPagesStateVar: string
203
+ searchQueryVar: string
204
+ setSearchQueryVar: string
205
+ debouncedSearchQueryVar: string
206
+ setDebouncedSearchQueryVar: string
207
+ combinedStateVar: string
208
+ setCombinedStateVar: string
209
+ skipDebounceRefVar: string
210
+ skipCountFetchRefVar: string
211
+ propsPrefix: string
212
+ } {
213
+ const idx = usage.index
214
+
215
+ return {
216
+ pageStateVar: `ds_${idx}_page`,
217
+ setPageStateVar: `setDs_${idx}_page`,
218
+ maxPagesStateVar: `ds_${idx}_maxPages`,
219
+ setMaxPagesStateVar: `setDs_${idx}_maxPages`,
220
+ searchQueryVar: `ds_${idx}_searchQuery`,
221
+ setSearchQueryVar: `setDs_${idx}_searchQuery`,
222
+ debouncedSearchQueryVar: `ds_${idx}_debouncedQuery`,
223
+ setDebouncedSearchQueryVar: `setDs_${idx}_debouncedQuery`,
224
+ combinedStateVar: `ds_${idx}_state`,
225
+ setCombinedStateVar: `setDs_${idx}_state`,
226
+ skipDebounceRefVar: `ds_${idx}_skipDebounce`,
227
+ skipCountFetchRefVar: `ds_${idx}_skipCountFetch`,
228
+ propsPrefix: `${usage.dataSourceIdentifier}_ds_${idx}`,
229
+ }
20
230
  }
21
231
 
232
+ // ==================== MAIN PLUGIN ====================
233
+
22
234
  export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> = () => {
23
235
  const paginationPlugin: ComponentPlugin = async (structure) => {
24
236
  const { uidl, chunks, dependencies, options } = structure
@@ -46,62 +258,56 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
46
258
  }
47
259
  const blockStatement = arrowFunction.body as types.BlockStatement
48
260
 
49
- const { paginatedMappers, searchOnlyMappers, paginationOnlyMappers } =
50
- detectPaginationsAndSearchFromJSX(blockStatement, uidl.node)
261
+ // STEP 1: Build state registry from UIDL
262
+ const registry = buildStateRegistry(uidl.node)
51
263
 
52
- if (
53
- paginatedMappers.length === 0 &&
54
- searchOnlyMappers.length === 0 &&
55
- paginationOnlyMappers.length === 0
56
- ) {
264
+ if (registry.usages.length === 0) {
57
265
  return structure
58
266
  }
59
267
 
60
- // Combine pagination+search and pagination-only into one array for processing
61
- const detectedPaginations = [...paginatedMappers, ...paginationOnlyMappers]
62
- const detectedSearchOnly = searchOnlyMappers
268
+ // Check if this is a page or component
269
+ const getStaticPropsChunk = chunks.find((chunk) => chunk.name === 'getStaticProps')
270
+ const isPage = !!getStaticPropsChunk
63
271
 
272
+ // Add React dependencies
64
273
  if (!dependencies.useState) {
65
274
  dependencies.useState = {
66
275
  type: 'library',
67
276
  path: 'react',
68
277
  version: '',
69
- meta: {
70
- namedImport: true,
71
- },
278
+ meta: { namedImport: true },
72
279
  }
73
280
  }
74
-
75
281
  if (!dependencies.useMemo) {
76
282
  dependencies.useMemo = {
77
283
  type: 'library',
78
284
  path: 'react',
79
285
  version: '',
80
- meta: {
81
- namedImport: true,
82
- },
286
+ meta: { namedImport: true },
83
287
  }
84
288
  }
85
-
86
289
  if (!dependencies.useCallback) {
87
290
  dependencies.useCallback = {
88
291
  type: 'library',
89
292
  path: 'react',
90
293
  version: '',
91
- meta: {
92
- namedImport: true,
93
- },
294
+ meta: { namedImport: true },
94
295
  }
95
296
  }
96
-
97
297
  if (!dependencies.useRef) {
98
298
  dependencies.useRef = {
99
299
  type: 'library',
100
300
  path: 'react',
101
301
  version: '',
102
- meta: {
103
- namedImport: true,
104
- },
302
+ meta: { namedImport: true },
303
+ }
304
+ }
305
+ if (!dependencies.useEffect) {
306
+ dependencies.useEffect = {
307
+ type: 'library',
308
+ path: 'react',
309
+ version: '',
310
+ meta: { namedImport: true },
105
311
  }
106
312
  }
107
313
 
@@ -110,2472 +316,1911 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
110
316
  }
111
317
  componentChunk.meta.isClientComponent = true
112
318
 
113
- const paginationInfos: ArrayMapperPaginationInfo[] = []
114
-
115
- // Check if this is a page (has getStaticProps) or a component
116
- const getStaticPropsChunk = chunks.find((chunk) => chunk.name === 'getStaticProps')
117
- const isPage = !!getStaticPropsChunk
118
- const isComponent = !isPage
119
-
120
- // Get pagination and search config from early extraction (done before transformations)
121
- const opts = options as any
122
- const perPageMap = opts.paginationConfig?.perPageMap || new Map<string, number>()
123
- const searchConfigMap = opts.paginationConfig?.searchConfigMap || new Map<string, any>()
124
- const queryColumnsMap = opts.paginationConfig?.queryColumnsMap || new Map<string, string[]>()
125
-
319
+ // STEP 2: Generate state declarations for all usages
126
320
  const stateDeclarations: types.Statement[] = []
321
+ const effectStatements: types.Statement[] = []
127
322
 
128
- detectedPaginations.forEach((detected, index) => {
129
- const paginationNodeId = `pg_${index}`
130
- // Use arrayMapperRenderProp if available, otherwise fall back to dataSourceIdentifier
131
- const lookupKey = detected.arrayMapperRenderProp || detected.dataSourceIdentifier
132
- const perPage = perPageMap.get(lookupKey) || 10
133
- const searchConfig = searchConfigMap.get(lookupKey)
134
- // queryColumns is keyed by dataSourceIdentifier, not array mapper render prop
135
- const queryColumns = queryColumnsMap.get(detected.dataSourceIdentifier)
136
-
137
- const info = generatePaginationLogic(
138
- paginationNodeId,
139
- detected.dataSourceIdentifier,
140
- perPage,
141
- searchConfig,
142
- queryColumns
143
- )
144
- paginationInfos.push(info)
145
-
146
- // Add refs to track first render for each useEffect (add first)
147
- if (info.searchEnabled) {
148
- const skipCountFetchOnMountRefVar = `skipCountFetchOnMount_pg_${index}`
149
- const skipCountFetchRefAST = types.variableDeclaration('const', [
150
- types.variableDeclarator(
151
- types.identifier(skipCountFetchOnMountRefVar),
152
- types.callExpression(types.identifier('useRef'), [types.booleanLiteral(true)])
153
- ),
154
- ])
155
- stateDeclarations.push(skipCountFetchRefAST)
156
- ;(info as any).skipCountFetchOnMountRefVar = skipCountFetchOnMountRefVar
157
-
158
- const skipDebounceOnMountRefVar = `skipDebounceOnMount_pg_${index}`
159
- const skipDebounceRefAST = types.variableDeclaration('const', [
160
- types.variableDeclarator(
161
- types.identifier(skipDebounceOnMountRefVar),
162
- types.callExpression(types.identifier('useRef'), [types.booleanLiteral(true)])
163
- ),
164
- ])
165
- stateDeclarations.push(skipDebounceRefAST)
166
- ;(info as any).skipDebounceOnMountRefVar = skipDebounceOnMountRefVar
323
+ registry.usages.forEach((usage) => {
324
+ if (usage.category === 'plain') {
325
+ return // Plain mappers don't need state
167
326
  }
168
327
 
169
- // Add maxPages state
170
- const maxPagesStateVar = `${info.pageStateVar.replace('_page', '')}_maxPages`
171
- const setMaxPagesStateVar = `set${
172
- maxPagesStateVar.charAt(0).toUpperCase() + maxPagesStateVar.slice(1)
173
- }`
174
-
175
- // For pages: initialize from props (with pagination-specific prop name), for components: initialize to 0
176
- const maxPagesInitValue = isPage
177
- ? types.logicalExpression(
178
- '||',
179
- types.optionalMemberExpression(
180
- types.identifier('props'),
181
- types.identifier(`${info.dataSourceIdentifier}_pg_${index}_maxPages`),
182
- false,
183
- true
184
- ),
185
- types.numericLiteral(0)
186
- )
187
- : types.numericLiteral(0)
188
-
189
- const maxPagesStateAST = types.variableDeclaration('const', [
190
- types.variableDeclarator(
191
- types.arrayPattern([
192
- types.identifier(maxPagesStateVar),
193
- types.identifier(setMaxPagesStateVar),
194
- ]),
195
- types.callExpression(types.identifier('useState'), [maxPagesInitValue])
196
- ),
197
- ])
198
- stateDeclarations.push(maxPagesStateAST)
199
-
200
- // Store these for later use
201
- ;(info as any).maxPagesStateVar = maxPagesStateVar
202
- ;(info as any).setMaxPagesStateVar = setMaxPagesStateVar
203
-
204
- // If both pagination and search are enabled, combine them into a single state object
205
- if (info.searchEnabled && info.searchQueryVar && info.setSearchQueryVar) {
206
- // Combined state: { page: 1, debouncedQuery: '' }
207
- const combinedStateVar = `paginationState_pg_${index}`
208
- const setCombinedStateVar = `setPaginationState_pg_${index}`
209
-
210
- const combinedStateAST = types.variableDeclaration('const', [
211
- types.variableDeclarator(
212
- types.arrayPattern([
213
- types.identifier(combinedStateVar),
214
- types.identifier(setCombinedStateVar),
215
- ]),
216
- types.callExpression(types.identifier('useState'), [
217
- types.objectExpression([
218
- types.objectProperty(types.identifier('page'), types.numericLiteral(1)),
219
- types.objectProperty(types.identifier('debouncedQuery'), types.stringLiteral('')),
220
- ]),
221
- ])
222
- ),
223
- ])
224
- stateDeclarations.push(combinedStateAST)
225
-
226
- // Still need the immediate search query state for the input
227
- const searchStateAST = types.variableDeclaration('const', [
228
- types.variableDeclarator(
229
- types.arrayPattern([
230
- types.identifier(info.searchQueryVar),
231
- types.identifier(info.setSearchQueryVar),
232
- ]),
233
- types.callExpression(types.identifier('useState'), [types.stringLiteral('')])
234
- ),
235
- ])
236
- stateDeclarations.push(searchStateAST)
328
+ const vars = getStateVarsForUsage(usage)
237
329
 
238
- // Store the combined state var names
239
- ;(info as any).combinedStateVar = combinedStateVar
240
- ;(info as any).setCombinedStateVar = setCombinedStateVar
241
- } else {
242
- // If search is not enabled, just add regular page state
243
- const pageStateAST = types.variableDeclaration('const', [
244
- types.variableDeclarator(
245
- types.arrayPattern([
246
- types.identifier(info.pageStateVar),
247
- types.identifier(info.setPageStateVar),
248
- ]),
249
- types.callExpression(types.identifier('useState'), [types.numericLiteral(1)])
250
- ),
251
- ])
252
- stateDeclarations.push(pageStateAST)
253
- }
254
- })
330
+ if (usage.category === 'paginated+search') {
331
+ // Combined state object for pagination + search
332
+ stateDeclarations.push(
333
+ types.variableDeclaration('const', [
334
+ types.variableDeclarator(
335
+ types.identifier(vars.skipDebounceRefVar),
336
+ types.callExpression(types.identifier('useRef'), [types.booleanLiteral(true)])
337
+ ),
338
+ ])
339
+ )
255
340
 
256
- // Add all state declarations at once to the beginning in correct order
257
- stateDeclarations.reverse().forEach((stateDecl) => {
258
- blockStatement.body.unshift(stateDecl)
259
- })
341
+ // Only add skipCountFetchRef for pages (where we have server-side count)
342
+ // For components, we need to fetch count on mount
343
+ if (isPage) {
344
+ stateDeclarations.push(
345
+ types.variableDeclaration('const', [
346
+ types.variableDeclarator(
347
+ types.identifier(vars.skipCountFetchRefVar),
348
+ types.callExpression(types.identifier('useRef'), [types.booleanLiteral(true)])
349
+ ),
350
+ ])
351
+ )
352
+ }
260
353
 
261
- // Add useEffect dependency if any pagination has search enabled
262
- const hasSearchEnabled = paginationInfos.some((info) => info.searchEnabled)
263
- if (hasSearchEnabled && !dependencies.useEffect) {
264
- dependencies.useEffect = {
265
- type: 'library',
266
- path: 'react',
267
- version: '',
268
- meta: {
269
- namedImport: true,
270
- },
271
- }
272
- }
354
+ // maxPages state
355
+ const maxPagesInit = isPage
356
+ ? types.logicalExpression(
357
+ '||',
358
+ types.optionalMemberExpression(
359
+ types.identifier('props'),
360
+ types.identifier(`${vars.propsPrefix}_maxPages`),
361
+ false,
362
+ true
363
+ ),
364
+ types.numericLiteral(0)
365
+ )
366
+ : types.numericLiteral(0)
367
+
368
+ stateDeclarations.push(
369
+ types.variableDeclaration('const', [
370
+ types.variableDeclarator(
371
+ types.arrayPattern([
372
+ types.identifier(vars.maxPagesStateVar),
373
+ types.identifier(vars.setMaxPagesStateVar),
374
+ ]),
375
+ types.callExpression(types.identifier('useState'), [maxPagesInit])
376
+ ),
377
+ ])
378
+ )
273
379
 
274
- // Add all useEffect hooks AFTER state declarations
275
- // Find the first return statement and insert effects before it
276
- const firstReturnIndex = blockStatement.body.findIndex(
277
- (stmt: any) => stmt.type === 'ReturnStatement'
278
- )
279
- const insertIndex = firstReturnIndex !== -1 ? firstReturnIndex : blockStatement.body.length
280
-
281
- // Add effects in reverse order so they appear in correct order
282
- paginationInfos.forEach((info) => {
283
- if (
284
- info.searchEnabled &&
285
- info.searchQueryVar &&
286
- info.debouncedSearchQueryVar &&
287
- info.setDebouncedSearchQueryVar
288
- ) {
289
- // Add effect that updates debounced search after delay
290
- const skipDebounceOnMountRefVar = (info as any).skipDebounceOnMountRefVar
291
- const setCombinedStateVar = (info as any).setCombinedStateVar
292
-
293
- const debounceEffect = types.expressionStatement(
294
- types.callExpression(types.identifier('useEffect'), [
295
- types.arrowFunctionExpression(
296
- [],
297
- types.blockStatement([
298
- types.ifStatement(
299
- types.memberExpression(
300
- types.identifier(skipDebounceOnMountRefVar),
301
- types.identifier('current')
302
- ),
303
- types.blockStatement([
304
- types.expressionStatement(
305
- types.assignmentExpression(
306
- '=',
307
- types.memberExpression(
308
- types.identifier(skipDebounceOnMountRefVar),
309
- types.identifier('current')
310
- ),
311
- types.booleanLiteral(false)
312
- )
313
- ),
314
- types.returnStatement(),
315
- ])
316
- ),
317
- types.variableDeclaration('const', [
318
- types.variableDeclarator(
319
- types.identifier('timer'),
320
- types.callExpression(types.identifier('setTimeout'), [
321
- types.arrowFunctionExpression(
322
- [],
323
- types.blockStatement([
324
- types.expressionStatement(
325
- types.callExpression(types.identifier(setCombinedStateVar), [
326
- types.objectExpression([
327
- types.objectProperty(
328
- types.identifier('page'),
329
- types.numericLiteral(1)
330
- ),
331
- types.objectProperty(
332
- types.identifier('debouncedQuery'),
333
- types.identifier(info.searchQueryVar)
334
- ),
335
- ]),
336
- ])
337
- ),
338
- ])
339
- ),
340
- types.numericLiteral(info.searchDebounce || 300),
341
- ])
342
- ),
380
+ // Combined state { page, debouncedQuery }
381
+ stateDeclarations.push(
382
+ types.variableDeclaration('const', [
383
+ types.variableDeclarator(
384
+ types.arrayPattern([
385
+ types.identifier(vars.combinedStateVar),
386
+ types.identifier(vars.setCombinedStateVar),
387
+ ]),
388
+ types.callExpression(types.identifier('useState'), [
389
+ types.objectExpression([
390
+ types.objectProperty(types.identifier('page'), types.numericLiteral(1)),
391
+ types.objectProperty(types.identifier('debouncedQuery'), types.stringLiteral('')),
343
392
  ]),
344
- types.returnStatement(
345
- types.arrowFunctionExpression(
346
- [],
347
- types.callExpression(types.identifier('clearTimeout'), [
348
- types.identifier('timer'),
349
- ])
350
- )
351
- ),
352
393
  ])
353
394
  ),
354
- types.arrayExpression([types.identifier(info.searchQueryVar)]),
355
395
  ])
356
396
  )
357
397
 
358
- blockStatement.body.splice(insertIndex, 0, debounceEffect)
359
-
360
- // Add useEffect to refetch count when search changes (for both pages and components)
361
- const detected = detectedPaginations.find(
362
- (d) => d.dataSourceIdentifier === info.dataSourceIdentifier
363
- )
364
- if (!detected) {
365
- return
366
- }
367
-
368
- const resourceDefAttr = detected.dataProviderJSX.openingElement.attributes.find(
369
- (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'resourceDefinition'
398
+ // Immediate search query state
399
+ stateDeclarations.push(
400
+ types.variableDeclaration('const', [
401
+ types.variableDeclarator(
402
+ types.arrayPattern([
403
+ types.identifier(vars.searchQueryVar),
404
+ types.identifier(vars.setSearchQueryVar),
405
+ ]),
406
+ types.callExpression(types.identifier('useState'), [types.stringLiteral('')])
407
+ ),
408
+ ])
370
409
  )
371
410
 
372
- if (
373
- resourceDefAttr &&
374
- resourceDefAttr.value &&
375
- resourceDefAttr.value.type === 'JSXExpressionContainer'
376
- ) {
377
- const resourceDef = resourceDefAttr.value.expression
378
- if (resourceDef.type === 'ObjectExpression') {
379
- const dataSourceIdProp = (resourceDef.properties as any[]).find(
380
- (p: any) => p.type === 'ObjectProperty' && p.key.value === 'dataSourceId'
381
- )
382
- const tableNameProp = (resourceDef.properties as any[]).find(
383
- (p: any) => p.type === 'ObjectProperty' && p.key.value === 'tableName'
384
- )
385
- const dataSourceTypeProp = (resourceDef.properties as any[]).find(
386
- (p: any) => p.type === 'ObjectProperty' && p.key.value === 'dataSourceType'
387
- )
388
-
389
- if (dataSourceIdProp && tableNameProp && dataSourceTypeProp) {
390
- const dataSourceId = dataSourceIdProp.value.value
391
- const tableName = tableNameProp.value.value
392
- const dataSourceType = dataSourceTypeProp.value.value
393
- const fileName = `${dataSourceType}-${tableName}-${dataSourceId.substring(0, 8)}`
394
- const setMaxPagesStateVar = (info as any).setMaxPagesStateVar
395
-
396
- // Create useEffect to refetch count when debounced search changes
397
- const skipCountFetchOnMountRefVar = (info as any).skipCountFetchOnMountRefVar
398
- const combinedStateVar = (info as any).combinedStateVar
399
-
400
- // Build URLSearchParams properties - query is always included, queryColumns is optional
401
- const urlSearchParamsProperties: any[] = [
402
- types.objectProperty(
403
- types.identifier('query'),
404
- types.memberExpression(
405
- types.identifier(combinedStateVar),
406
- types.identifier('debouncedQuery')
407
- )
408
- ),
409
- ]
410
-
411
- // Add queryColumns only if they exist
412
- if (info.queryColumns && info.queryColumns.length > 0) {
413
- urlSearchParamsProperties.push(
414
- types.objectProperty(
415
- types.identifier('queryColumns'),
416
- types.callExpression(
417
- types.memberExpression(
418
- types.identifier('JSON'),
419
- types.identifier('stringify')
420
- ),
421
- [
422
- types.arrayExpression(
423
- info.queryColumns.map((col) => types.stringLiteral(col))
424
- ),
425
- ]
426
- )
427
- )
428
- )
429
- }
430
-
431
- const refetchCountEffect = types.expressionStatement(
432
- types.callExpression(types.identifier('useEffect'), [
433
- types.arrowFunctionExpression(
434
- [],
411
+ // Debounce effect
412
+ effectStatements.push(
413
+ types.expressionStatement(
414
+ types.callExpression(types.identifier('useEffect'), [
415
+ types.arrowFunctionExpression(
416
+ [],
417
+ types.blockStatement([
418
+ types.ifStatement(
419
+ types.memberExpression(
420
+ types.identifier(vars.skipDebounceRefVar),
421
+ types.identifier('current')
422
+ ),
435
423
  types.blockStatement([
436
- types.ifStatement(
437
- types.memberExpression(
438
- types.identifier(skipCountFetchOnMountRefVar),
439
- types.identifier('current')
440
- ),
441
- types.blockStatement([
442
- types.expressionStatement(
443
- types.assignmentExpression(
444
- '=',
445
- types.memberExpression(
446
- types.identifier(skipCountFetchOnMountRefVar),
447
- types.identifier('current')
448
- ),
449
- types.booleanLiteral(false)
450
- )
451
- ),
452
- types.returnStatement(),
453
- ])
454
- ),
455
424
  types.expressionStatement(
456
- types.callExpression(
425
+ types.assignmentExpression(
426
+ '=',
457
427
  types.memberExpression(
458
- types.callExpression(
459
- types.memberExpression(
460
- types.callExpression(types.identifier('fetch'), [
461
- types.templateLiteral(
462
- [
463
- types.templateElement({
464
- raw: `/api/${fileName}-count?`,
465
- cooked: `/api/${fileName}-count?`,
466
- }),
467
- types.templateElement({ raw: '', cooked: '' }),
468
- ],
469
- [
470
- types.newExpression(types.identifier('URLSearchParams'), [
471
- types.objectExpression(urlSearchParamsProperties),
472
- ]),
473
- ]
474
- ),
475
- ]),
476
- types.identifier('then')
477
- ),
478
- [
479
- types.arrowFunctionExpression(
480
- [types.identifier('res')],
481
- types.callExpression(
482
- types.memberExpression(
483
- types.identifier('res'),
484
- types.identifier('json')
485
- ),
486
- []
487
- )
488
- ),
489
- ]
490
- ),
491
- types.identifier('then')
428
+ types.identifier(vars.skipDebounceRefVar),
429
+ types.identifier('current')
492
430
  ),
493
- [
494
- types.arrowFunctionExpression(
495
- [types.identifier('data')],
496
- types.blockStatement([
497
- types.ifStatement(
498
- types.logicalExpression(
499
- '&&',
500
- types.identifier('data'),
501
- types.binaryExpression(
502
- 'in',
503
- types.stringLiteral('count'),
504
- types.identifier('data')
505
- )
506
- ),
507
- types.blockStatement([
508
- types.expressionStatement(
509
- types.callExpression(types.identifier(setMaxPagesStateVar), [
510
- types.conditionalExpression(
511
- types.binaryExpression(
512
- '===',
513
- types.memberExpression(
514
- types.identifier('data'),
515
- types.identifier('count')
516
- ),
517
- types.numericLiteral(0)
518
- ),
519
- types.numericLiteral(0),
520
- types.callExpression(
521
- types.memberExpression(
522
- types.identifier('Math'),
523
- types.identifier('ceil')
524
- ),
525
- [
526
- types.binaryExpression(
527
- '/',
528
- types.memberExpression(
529
- types.identifier('data'),
530
- types.identifier('count')
531
- ),
532
- types.numericLiteral(info.perPage)
533
- ),
534
- ]
535
- )
536
- ),
537
- ])
538
- ),
539
- ])
540
- ),
541
- ])
542
- ),
543
- ]
431
+ types.booleanLiteral(false)
544
432
  )
545
433
  ),
434
+ types.returnStatement(),
546
435
  ])
547
436
  ),
548
- types.arrayExpression([
549
- types.memberExpression(
550
- types.identifier(combinedStateVar),
551
- types.identifier('debouncedQuery')
437
+ types.variableDeclaration('const', [
438
+ types.variableDeclarator(
439
+ types.identifier('timer'),
440
+ types.callExpression(types.identifier('setTimeout'), [
441
+ types.arrowFunctionExpression(
442
+ [],
443
+ types.blockStatement([
444
+ types.expressionStatement(
445
+ types.callExpression(types.identifier(vars.setCombinedStateVar), [
446
+ types.objectExpression([
447
+ types.objectProperty(
448
+ types.identifier('page'),
449
+ types.numericLiteral(1)
450
+ ),
451
+ types.objectProperty(
452
+ types.identifier('debouncedQuery'),
453
+ types.identifier(vars.searchQueryVar)
454
+ ),
455
+ ]),
456
+ ])
457
+ ),
458
+ ])
459
+ ),
460
+ types.numericLiteral(usage.searchDebounce),
461
+ ])
552
462
  ),
553
463
  ]),
464
+ types.returnStatement(
465
+ types.arrowFunctionExpression(
466
+ [],
467
+ types.callExpression(types.identifier('clearTimeout'), [
468
+ types.identifier('timer'),
469
+ ])
470
+ )
471
+ ),
554
472
  ])
555
- )
556
-
557
- blockStatement.body.splice(insertIndex, 0, refetchCountEffect)
558
- }
559
- }
560
- }
561
- }
562
- })
563
-
564
- // For components, add useEffect to fetch count on mount
565
- if (isComponent) {
566
- if (!dependencies.useEffect) {
567
- dependencies.useEffect = {
568
- type: 'library',
569
- path: 'react',
570
- version: '',
571
- meta: {
572
- namedImport: true,
573
- },
574
- }
575
- }
576
-
577
- // Group paginationInfos by data source identifier to avoid duplicate fetches
578
- const dataSourceToInfos = new Map<
579
- string,
580
- Array<{ info: ArrayMapperPaginationInfo; detected: any; fileName: string }>
581
- >()
473
+ ),
474
+ types.arrayExpression([types.identifier(vars.searchQueryVar)]),
475
+ ])
476
+ )
477
+ )
582
478
 
583
- paginationInfos.forEach((info) => {
584
- const detected = detectedPaginations.find(
585
- (d) => d.dataSourceIdentifier === info.dataSourceIdentifier
479
+ // Count refetch effect
480
+ const fileName = generateSafeFileName(
481
+ usage.resourceDefinition.dataSourceType,
482
+ usage.resourceDefinition.tableName,
483
+ usage.resourceDefinition.dataSourceId
586
484
  )
587
- if (!detected) {
588
- return
485
+ const urlParams: types.ObjectProperty[] = [
486
+ types.objectProperty(
487
+ types.identifier('query'),
488
+ types.memberExpression(
489
+ types.identifier(vars.combinedStateVar),
490
+ types.identifier('debouncedQuery')
491
+ )
492
+ ),
493
+ ]
494
+ if (usage.queryColumns.length > 0) {
495
+ urlParams.push(
496
+ types.objectProperty(
497
+ types.identifier('queryColumns'),
498
+ types.callExpression(
499
+ types.memberExpression(types.identifier('JSON'), types.identifier('stringify')),
500
+ [types.arrayExpression(usage.queryColumns.map((c) => types.stringLiteral(c)))]
501
+ )
502
+ )
503
+ )
589
504
  }
590
505
 
591
- const resourceDefAttr = detected.dataProviderJSX.openingElement.attributes.find(
592
- (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'resourceDefinition'
593
- )
594
-
595
- if (
596
- !resourceDefAttr ||
597
- !resourceDefAttr.value ||
598
- resourceDefAttr.value.type !== 'JSXExpressionContainer'
599
- ) {
600
- return
601
- }
506
+ // Build the count fetch effect body
507
+ const countFetchEffectBody: types.Statement[] = []
602
508
 
603
- const resourceDef = resourceDefAttr.value.expression
604
- if (resourceDef.type !== 'ObjectExpression') {
605
- return
509
+ // Only add skip-on-mount check for pages (where we have server-side count)
510
+ if (isPage) {
511
+ countFetchEffectBody.push(
512
+ types.ifStatement(
513
+ types.memberExpression(
514
+ types.identifier(vars.skipCountFetchRefVar),
515
+ types.identifier('current')
516
+ ),
517
+ types.blockStatement([
518
+ types.expressionStatement(
519
+ types.assignmentExpression(
520
+ '=',
521
+ types.memberExpression(
522
+ types.identifier(vars.skipCountFetchRefVar),
523
+ types.identifier('current')
524
+ ),
525
+ types.booleanLiteral(false)
526
+ )
527
+ ),
528
+ types.returnStatement(),
529
+ ])
530
+ )
531
+ )
606
532
  }
607
533
 
608
- const dataSourceIdProp = (resourceDef.properties as any[]).find(
609
- (p: any) => p.type === 'ObjectProperty' && p.key.value === 'dataSourceId'
534
+ // Add the fetch call
535
+ countFetchEffectBody.push(
536
+ types.expressionStatement(
537
+ types.callExpression(
538
+ types.memberExpression(
539
+ types.callExpression(
540
+ types.memberExpression(
541
+ types.callExpression(types.identifier('fetch'), [
542
+ types.templateLiteral(
543
+ [
544
+ types.templateElement({
545
+ raw: `/api/${fileName}-count?`,
546
+ cooked: `/api/${fileName}-count?`,
547
+ }),
548
+ types.templateElement({ raw: '', cooked: '' }),
549
+ ],
550
+ [
551
+ types.newExpression(types.identifier('URLSearchParams'), [
552
+ types.objectExpression(urlParams),
553
+ ]),
554
+ ]
555
+ ),
556
+ ]),
557
+ types.identifier('then')
558
+ ),
559
+ [
560
+ types.arrowFunctionExpression(
561
+ [types.identifier('res')],
562
+ types.callExpression(
563
+ types.memberExpression(types.identifier('res'), types.identifier('json')),
564
+ []
565
+ )
566
+ ),
567
+ ]
568
+ ),
569
+ types.identifier('then')
570
+ ),
571
+ [
572
+ types.arrowFunctionExpression(
573
+ [types.identifier('data')],
574
+ types.blockStatement([
575
+ types.ifStatement(
576
+ types.logicalExpression(
577
+ '&&',
578
+ types.identifier('data'),
579
+ types.binaryExpression(
580
+ 'in',
581
+ types.stringLiteral('count'),
582
+ types.identifier('data')
583
+ )
584
+ ),
585
+ types.blockStatement([
586
+ types.expressionStatement(
587
+ types.callExpression(types.identifier(vars.setMaxPagesStateVar), [
588
+ types.conditionalExpression(
589
+ types.binaryExpression(
590
+ '===',
591
+ types.memberExpression(
592
+ types.identifier('data'),
593
+ types.identifier('count')
594
+ ),
595
+ types.numericLiteral(0)
596
+ ),
597
+ types.numericLiteral(0),
598
+ types.callExpression(
599
+ types.memberExpression(
600
+ types.identifier('Math'),
601
+ types.identifier('ceil')
602
+ ),
603
+ [
604
+ types.binaryExpression(
605
+ '/',
606
+ types.memberExpression(
607
+ types.identifier('data'),
608
+ types.identifier('count')
609
+ ),
610
+ types.numericLiteral(usage.perPage)
611
+ ),
612
+ ]
613
+ )
614
+ ),
615
+ ])
616
+ ),
617
+ ])
618
+ ),
619
+ ])
620
+ ),
621
+ ]
622
+ )
623
+ )
610
624
  )
611
- const tableNameProp = (resourceDef.properties as any[]).find(
612
- (p: any) => p.type === 'ObjectProperty' && p.key.value === 'tableName'
625
+
626
+ effectStatements.push(
627
+ types.expressionStatement(
628
+ types.callExpression(types.identifier('useEffect'), [
629
+ types.arrowFunctionExpression([], types.blockStatement(countFetchEffectBody)),
630
+ types.arrayExpression([
631
+ types.memberExpression(
632
+ types.identifier(vars.combinedStateVar),
633
+ types.identifier('debouncedQuery')
634
+ ),
635
+ ]),
636
+ ])
637
+ )
613
638
  )
614
- const dataSourceTypeProp = (resourceDef.properties as any[]).find(
615
- (p: any) => p.type === 'ObjectProperty' && p.key.value === 'dataSourceType'
639
+ } else if (usage.category === 'paginated-only') {
640
+ // Simple page state
641
+ const maxPagesInit = isPage
642
+ ? types.logicalExpression(
643
+ '||',
644
+ types.optionalMemberExpression(
645
+ types.identifier('props'),
646
+ types.identifier(`${vars.propsPrefix}_maxPages`),
647
+ false,
648
+ true
649
+ ),
650
+ types.numericLiteral(0)
651
+ )
652
+ : types.numericLiteral(0)
653
+
654
+ stateDeclarations.push(
655
+ types.variableDeclaration('const', [
656
+ types.variableDeclarator(
657
+ types.arrayPattern([
658
+ types.identifier(vars.maxPagesStateVar),
659
+ types.identifier(vars.setMaxPagesStateVar),
660
+ ]),
661
+ types.callExpression(types.identifier('useState'), [maxPagesInit])
662
+ ),
663
+ ])
616
664
  )
617
665
 
618
- if (!dataSourceIdProp || !tableNameProp || !dataSourceTypeProp) {
619
- return
620
- }
621
-
622
- const dataSourceId = dataSourceIdProp.value.value
623
- const tableName = tableNameProp.value.value
624
- const dataSourceType = dataSourceTypeProp.value.value
625
- const fileName = `${dataSourceType}-${tableName}-${dataSourceId.substring(0, 8)}`
626
-
627
- // Group by data source identifier
628
- if (!dataSourceToInfos.has(info.dataSourceIdentifier)) {
629
- dataSourceToInfos.set(info.dataSourceIdentifier, [])
630
- }
631
- dataSourceToInfos.get(info.dataSourceIdentifier)!.push({ info, detected, fileName })
632
- })
633
-
634
- // Create ONE useEffect per unique data source
635
- // Collect all useEffect statements first
636
- const componentUseEffects: types.Statement[] = []
637
-
638
- dataSourceToInfos.forEach((infos) => {
639
- const { fileName } = infos[0]
666
+ stateDeclarations.push(
667
+ types.variableDeclaration('const', [
668
+ types.variableDeclarator(
669
+ types.arrayPattern([
670
+ types.identifier(vars.pageStateVar),
671
+ types.identifier(vars.setPageStateVar),
672
+ ]),
673
+ types.callExpression(types.identifier('useState'), [types.numericLiteral(1)])
674
+ ),
675
+ ])
676
+ )
640
677
 
641
- // Create array of setState calls - one for each pagination using this data source
642
- const setStateStatements = infos.map(({ info }) => {
643
- const setMaxPagesStateVar = (info as any).setMaxPagesStateVar
644
- return types.expressionStatement(
645
- types.callExpression(types.identifier(setMaxPagesStateVar), [
646
- types.callExpression(
647
- types.memberExpression(types.identifier('Math'), types.identifier('ceil')),
648
- [
649
- types.binaryExpression(
650
- '/',
651
- types.memberExpression(types.identifier('data'), types.identifier('count')),
652
- types.numericLiteral(info.perPage)
653
- ),
654
- ]
655
- ),
656
- ])
678
+ // For components (not pages), add a useEffect to fetch count on mount
679
+ // Pages get count from getStaticProps, but components need to fetch it client-side
680
+ if (!isPage) {
681
+ const fileName = generateSafeFileName(
682
+ usage.resourceDefinition.dataSourceType,
683
+ usage.resourceDefinition.tableName,
684
+ usage.resourceDefinition.dataSourceId
657
685
  )
658
- })
659
686
 
660
- // Create useEffect to fetch count on mount - sets ALL maxPages for this data source
661
- const useEffectAST = types.expressionStatement(
662
- types.callExpression(types.identifier('useEffect'), [
663
- types.arrowFunctionExpression(
664
- [],
665
- types.blockStatement([
666
- types.expressionStatement(
667
- types.callExpression(
668
- types.memberExpression(
687
+ effectStatements.push(
688
+ types.expressionStatement(
689
+ types.callExpression(types.identifier('useEffect'), [
690
+ types.arrowFunctionExpression(
691
+ [],
692
+ types.blockStatement([
693
+ types.expressionStatement(
669
694
  types.callExpression(
670
695
  types.memberExpression(
671
- types.callExpression(types.identifier('fetch'), [
672
- types.stringLiteral(`/api/${fileName}-count`),
673
- ]),
696
+ types.callExpression(
697
+ types.memberExpression(
698
+ types.callExpression(types.identifier('fetch'), [
699
+ types.stringLiteral(`/api/${fileName}-count`),
700
+ ]),
701
+ types.identifier('then')
702
+ ),
703
+ [
704
+ types.arrowFunctionExpression(
705
+ [types.identifier('res')],
706
+ types.callExpression(
707
+ types.memberExpression(
708
+ types.identifier('res'),
709
+ types.identifier('json')
710
+ ),
711
+ []
712
+ )
713
+ ),
714
+ ]
715
+ ),
674
716
  types.identifier('then')
675
717
  ),
676
718
  [
677
719
  types.arrowFunctionExpression(
678
- [types.identifier('res')],
679
- types.callExpression(
680
- types.memberExpression(
681
- types.identifier('res'),
682
- types.identifier('json')
720
+ [types.identifier('data')],
721
+ types.blockStatement([
722
+ types.ifStatement(
723
+ types.logicalExpression(
724
+ '&&',
725
+ types.identifier('data'),
726
+ types.binaryExpression(
727
+ 'in',
728
+ types.stringLiteral('count'),
729
+ types.identifier('data')
730
+ )
731
+ ),
732
+ types.blockStatement([
733
+ types.expressionStatement(
734
+ types.callExpression(
735
+ types.identifier(vars.setMaxPagesStateVar),
736
+ [
737
+ types.conditionalExpression(
738
+ types.binaryExpression(
739
+ '===',
740
+ types.memberExpression(
741
+ types.identifier('data'),
742
+ types.identifier('count')
743
+ ),
744
+ types.numericLiteral(0)
745
+ ),
746
+ types.numericLiteral(0),
747
+ types.callExpression(
748
+ types.memberExpression(
749
+ types.identifier('Math'),
750
+ types.identifier('ceil')
751
+ ),
752
+ [
753
+ types.binaryExpression(
754
+ '/',
755
+ types.memberExpression(
756
+ types.identifier('data'),
757
+ types.identifier('count')
758
+ ),
759
+ types.numericLiteral(usage.perPage)
760
+ ),
761
+ ]
762
+ )
763
+ ),
764
+ ]
765
+ )
766
+ ),
767
+ ])
683
768
  ),
684
- []
685
- )
769
+ ])
686
770
  ),
687
771
  ]
688
- ),
689
- types.identifier('then')
772
+ )
690
773
  ),
691
- [
692
- types.arrowFunctionExpression(
693
- [types.identifier('data')],
694
- types.blockStatement([
695
- types.ifStatement(
696
- types.logicalExpression(
697
- '&&',
698
- types.identifier('data'),
699
- types.memberExpression(
700
- types.identifier('data'),
701
- types.identifier('count')
702
- )
703
- ),
704
- types.blockStatement(setStateStatements)
705
- ),
706
- ])
707
- ),
708
- ]
709
- )
774
+ ])
710
775
  ),
776
+ types.arrayExpression([]), // Empty dependency array - fetch on mount only
711
777
  ])
778
+ )
779
+ )
780
+ }
781
+ } else if (usage.category === 'search-only') {
782
+ // Search-only state
783
+ stateDeclarations.push(
784
+ types.variableDeclaration('const', [
785
+ types.variableDeclarator(
786
+ types.identifier(vars.skipDebounceRefVar),
787
+ types.callExpression(types.identifier('useRef'), [types.booleanLiteral(true)])
712
788
  ),
713
- types.arrayExpression([]),
714
789
  ])
715
790
  )
716
791
 
717
- componentUseEffects.push(useEffectAST)
718
- })
792
+ stateDeclarations.push(
793
+ types.variableDeclaration('const', [
794
+ types.variableDeclarator(
795
+ types.arrayPattern([
796
+ types.identifier(vars.debouncedSearchQueryVar),
797
+ types.identifier(vars.setDebouncedSearchQueryVar),
798
+ ]),
799
+ types.callExpression(types.identifier('useState'), [types.stringLiteral('')])
800
+ ),
801
+ ])
802
+ )
719
803
 
720
- // Insert all component useEffect hooks after state declarations but before return
721
- // Find the first return statement
722
- const componentReturnIndex = blockStatement.body.findIndex(
723
- (stmt: any) => stmt.type === 'ReturnStatement'
724
- )
725
- const componentEffectsInsertIndex =
726
- componentReturnIndex !== -1 ? componentReturnIndex : blockStatement.body.length
727
-
728
- // Insert in reverse order to maintain correct order
729
- componentUseEffects.reverse().forEach((effect) => {
730
- blockStatement.body.splice(componentEffectsInsertIndex, 0, effect)
731
- })
732
- }
733
-
734
- createAPIRoutesForPaginatedDataSources(
735
- uidl.node,
736
- options.dataSources,
737
- componentChunk,
738
- options.extractedResources,
739
- paginationInfos,
740
- isComponent
741
- )
804
+ stateDeclarations.push(
805
+ types.variableDeclaration('const', [
806
+ types.variableDeclarator(
807
+ types.arrayPattern([
808
+ types.identifier(vars.searchQueryVar),
809
+ types.identifier(vars.setSearchQueryVar),
810
+ ]),
811
+ types.callExpression(types.identifier('useState'), [types.stringLiteral('')])
812
+ ),
813
+ ])
814
+ )
742
815
 
743
- detectedPaginations.forEach((detected, index) => {
744
- addPaginationParamsToDataProvider(detected.dataProviderJSX, paginationInfos[index], index)
816
+ // Debounce effect
817
+ effectStatements.push(
818
+ types.expressionStatement(
819
+ types.callExpression(types.identifier('useEffect'), [
820
+ types.arrowFunctionExpression(
821
+ [],
822
+ types.blockStatement([
823
+ types.ifStatement(
824
+ types.memberExpression(
825
+ types.identifier(vars.skipDebounceRefVar),
826
+ types.identifier('current')
827
+ ),
828
+ types.blockStatement([
829
+ types.expressionStatement(
830
+ types.assignmentExpression(
831
+ '=',
832
+ types.memberExpression(
833
+ types.identifier(vars.skipDebounceRefVar),
834
+ types.identifier('current')
835
+ ),
836
+ types.booleanLiteral(false)
837
+ )
838
+ ),
839
+ types.returnStatement(),
840
+ ])
841
+ ),
842
+ types.variableDeclaration('const', [
843
+ types.variableDeclarator(
844
+ types.identifier('timer'),
845
+ types.callExpression(types.identifier('setTimeout'), [
846
+ types.arrowFunctionExpression(
847
+ [],
848
+ types.blockStatement([
849
+ types.expressionStatement(
850
+ types.callExpression(
851
+ types.identifier(vars.setDebouncedSearchQueryVar),
852
+ [types.identifier(vars.searchQueryVar)]
853
+ )
854
+ ),
855
+ ])
856
+ ),
857
+ types.numericLiteral(usage.searchDebounce),
858
+ ])
859
+ ),
860
+ ]),
861
+ types.returnStatement(
862
+ types.arrowFunctionExpression(
863
+ [],
864
+ types.callExpression(types.identifier('clearTimeout'), [
865
+ types.identifier('timer'),
866
+ ])
867
+ )
868
+ ),
869
+ ])
870
+ ),
871
+ types.arrayExpression([types.identifier(vars.searchQueryVar)]),
872
+ ])
873
+ )
874
+ )
875
+ }
745
876
  })
746
877
 
747
- modifyPaginationButtons(blockStatement, detectedPaginations, paginationInfos)
748
- modifySearchInputs(blockStatement, detectedPaginations, paginationInfos)
878
+ // Insert state declarations at the beginning
879
+ stateDeclarations.reverse().forEach((s) => blockStatement.body.unshift(s))
749
880
 
750
- if (isPage) {
751
- modifyGetStaticPropsForPagination(chunks, paginationInfos)
752
- }
881
+ // Insert effects before return statement
882
+ const returnIndex = blockStatement.body.findIndex((s: any) => s.type === 'ReturnStatement')
883
+ const insertIndex = returnIndex !== -1 ? returnIndex : blockStatement.body.length
884
+ effectStatements.reverse().forEach((e) => blockStatement.body.splice(insertIndex, 0, e))
753
885
 
754
- // Handle search-only array mappers (no need to filter - they're already separate)
755
- // The detection function already separated them into different arrays
756
- const searchOnlyDataSources = detectedSearchOnly
886
+ // STEP 3: Find all DataProviders in JSX that have Repeaters and wire them to correct states
887
+ const dataProviders = findAllDataProvidersInJSX(blockStatement)
757
888
 
758
- if (searchOnlyDataSources.length > 0) {
759
- handleSearchOnlyArrayMappers(
760
- blockStatement,
761
- searchOnlyDataSources,
762
- searchConfigMap,
763
- queryColumnsMap,
764
- dependencies
765
- )
889
+ // Filter to only DataProviders with Repeaters (array mappers)
890
+ const dataProvidersWithRepeaters = dataProviders.filter((dp) => {
891
+ const hasRepeater = findArrayMapperRenderPropInDataProvider(dp) !== undefined
892
+ return hasRepeater
893
+ })
894
+
895
+ // Track which usage index we're on for each dataSourceIdentifier
896
+ // We use pure order-based matching - the order of DataProviders in JSX should match UIDL order
897
+ const usageIndexByDataSourceId = new Map<string, number>()
766
898
 
767
- // Create API routes for search-only data sources
768
- createAPIRoutesForSearchOnlyDataSources(
769
- uidl.node,
770
- options.dataSources,
771
- componentChunk,
772
- options.extractedResources,
773
- searchOnlyDataSources
899
+ dataProvidersWithRepeaters.forEach((dp) => {
900
+ const nameAttr = dp.openingElement.attributes.find(
901
+ (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'name'
774
902
  )
775
- }
903
+ if (!nameAttr?.value?.expression?.value) {
904
+ return
905
+ }
776
906
 
777
- cleanupStaticDataProviders(blockStatement)
907
+ const dataSourceIdentifier = nameAttr.value.expression.value
778
908
 
779
- return structure
780
- }
909
+ // Use pure order-based matching within each dataSourceIdentifier
910
+ const usages = registry.byDataSourceId.get(dataSourceIdentifier) || []
911
+ const currentIndex = usageIndexByDataSourceId.get(dataSourceIdentifier) || 0
781
912
 
782
- return paginationPlugin
783
- }
913
+ if (currentIndex >= usages.length) {
914
+ return
915
+ }
784
916
 
785
- function cleanupStaticDataProviders(blockStatement: types.BlockStatement): void {
786
- const findAllDataProviders = (node: any, results: any[] = []): any[] => {
787
- if (!node) {
788
- return results
789
- }
917
+ const usage = usages[currentIndex]
918
+ usageIndexByDataSourceId.set(dataSourceIdentifier, currentIndex + 1)
790
919
 
791
- if (node.type === 'JSXElement' && node.openingElement?.name?.name === 'DataProvider') {
792
- results.push(node)
793
- }
920
+ const vars = getStateVarsForUsage(usage)
921
+ const fileName = generateSafeFileName(
922
+ usage.resourceDefinition.dataSourceType,
923
+ usage.resourceDefinition.tableName,
924
+ usage.resourceDefinition.dataSourceId
925
+ )
794
926
 
795
- if (node.type === 'ReturnStatement' && node.argument) {
796
- findAllDataProviders(node.argument, results)
797
- } else if (node.type === 'JSXElement' || node.type === 'JSXFragment') {
798
- if (node.children && Array.isArray(node.children)) {
799
- node.children.forEach((child: any) => findAllDataProviders(child, results))
800
- }
801
- } else if (node.type === 'JSXExpressionContainer') {
802
- if (
803
- node.expression &&
804
- (node.expression.type === 'JSXElement' || node.expression.type === 'JSXFragment')
805
- ) {
806
- findAllDataProviders(node.expression, results)
927
+ // Update DataProvider based on category
928
+ if (usage.category === 'paginated+search') {
929
+ updateDataProviderForPaginatedSearch(dp, usage, vars, fileName)
930
+ } else if (usage.category === 'paginated-only') {
931
+ updateDataProviderForPaginationOnly(dp, usage, vars, fileName)
932
+ } else if (usage.category === 'search-only') {
933
+ updateDataProviderForSearchOnly(dp, usage, vars, fileName)
934
+ } else {
935
+ updateDataProviderForPlain(dp, vars)
807
936
  }
808
- } else if (node.type === 'BlockStatement') {
809
- if (node.body && Array.isArray(node.body)) {
810
- node.body.forEach((stmt: any) => findAllDataProviders(stmt, results))
937
+
938
+ // Create API route if needed
939
+ if (usage.category !== 'plain') {
940
+ ensureAPIRouteExists(options.extractedResources, usage)
811
941
  }
812
- } else if (node.type === 'ConditionalExpression') {
813
- findAllDataProviders(node.consequent, results)
814
- findAllDataProviders(node.alternate, results)
815
- }
942
+ })
816
943
 
817
- return results
818
- }
944
+ // STEP 3.5: Handle DataProviders WITHOUT repeaters (data-source-item type)
945
+ // These access single items like data[0].name and should not re-render on state changes
946
+ // We wrap their params in useMemo to prevent reference changes from triggering re-renders
947
+ const dataProvidersWithoutRepeaters = dataProviders.filter((dp) => {
948
+ const hasRepeater = findArrayMapperRenderPropInDataProvider(dp) !== undefined
949
+ return !hasRepeater
950
+ })
819
951
 
820
- const allDataProviders = findAllDataProviders(blockStatement)
952
+ dataProvidersWithoutRepeaters.forEach((dp) => {
953
+ stabilizeDataProviderWithoutRepeater(dp)
954
+ })
821
955
 
822
- allDataProviders.forEach((dataProvider) => {
823
- const hasInitialData = dataProvider.openingElement.attributes.some(
824
- (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'initialData'
825
- )
826
- const hasFetchData = dataProvider.openingElement.attributes.some(
827
- (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'fetchData'
828
- )
829
- const paramsAttr = dataProvider.openingElement.attributes.find(
830
- (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'params'
831
- )
956
+ // STEP 4: Wire search inputs
957
+ // Match search inputs to usages by order (within each dataSourceIdentifier that has search enabled)
958
+ const searchInputs = findAllSearchInputsInJSX(blockStatement)
832
959
 
833
- // Case 1: Static SSR/SSG DataProviders (initialData, no fetchData, params)
834
- // Remove params to prevent refetch attempts - data was already fetched in getStaticProps
835
- if (hasInitialData && !hasFetchData && paramsAttr) {
836
- dataProvider.openingElement.attributes = dataProvider.openingElement.attributes.filter(
837
- (attr: any) => attr.type !== 'JSXAttribute' || attr.name.name !== 'params'
838
- )
839
- }
960
+ // Get all search-enabled usages in order
961
+ const searchEnabledUsages = registry.usages.filter((u) => u.searchEnabled)
840
962
 
841
- // Case 2: Client-side DataProviders with plain object params (fetchData, non-memoized params)
842
- // Wrap params in useMemo to prevent infinite refetch loops
843
- else if (hasFetchData && paramsAttr && paramsAttr.value?.type === 'JSXExpressionContainer') {
844
- const paramsExpression = paramsAttr.value.expression
963
+ // Match by order - search input 0 -> searchEnabledUsages[0], etc.
964
+ searchInputs.forEach((input, idx) => {
965
+ if (idx >= searchEnabledUsages.length) {
966
+ return
967
+ }
845
968
 
846
- // Check if params are already memoized (useMemo or useCallback call)
847
- const isAlreadyMemoized =
848
- paramsExpression.type === 'CallExpression' &&
849
- paramsExpression.callee.type === 'Identifier' &&
850
- (paramsExpression.callee.name === 'useMemo' ||
851
- paramsExpression.callee.name === 'useCallback')
969
+ const usage = searchEnabledUsages[idx]
970
+ const vars = getStateVarsForUsage(usage)
971
+ wireSearchInput(input.node, vars)
972
+ })
852
973
 
853
- // If params are a plain ObjectExpression, wrap in useMemo
854
- if (!isAlreadyMemoized && paramsExpression.type === 'ObjectExpression') {
855
- const memoizedParams = types.callExpression(types.identifier('useMemo'), [
856
- types.arrowFunctionExpression([], paramsExpression),
857
- types.arrayExpression([]), // Empty deps - params are static
858
- ])
974
+ // STEP 5: Wire pagination buttons
975
+ // Match pagination nodes to usages by order (within paginated usages)
976
+ const paginationNodes = findAllPaginationNodesInJSX(blockStatement)
859
977
 
860
- paramsAttr.value.expression = memoizedParams
978
+ // Get all paginated usages in order
979
+ const paginatedUsages = registry.usages.filter((u) => u.paginated)
980
+
981
+ // Match by order - pagination node 0 -> paginatedUsages[0], etc.
982
+ paginationNodes.forEach((paginationNode, idx) => {
983
+ if (idx >= paginatedUsages.length) {
984
+ return
861
985
  }
986
+
987
+ const usage = paginatedUsages[idx]
988
+ const vars = getStateVarsForUsage(usage)
989
+ wirePaginationButtons(paginationNode.node, usage, vars)
990
+ })
991
+
992
+ // STEP 6: Update getStaticProps if this is a page
993
+ if (isPage) {
994
+ updateGetStaticProps(chunks, registry)
862
995
  }
863
- })
864
- }
865
996
 
866
- function findParentNode(root: any, target: any, currentParent: any = null): any | null {
867
- if (!root || !target) {
868
- return null
997
+ return structure
869
998
  }
870
999
 
871
- if (root === target) {
872
- return currentParent
873
- }
1000
+ return paginationPlugin
1001
+ }
874
1002
 
875
- if (root.type === 'JSXElement' || root.type === 'JSXFragment') {
876
- if (root.children && Array.isArray(root.children)) {
877
- for (const child of root.children) {
878
- const found = findParentNode(child, target, root)
879
- if (found !== null) {
880
- return found
881
- }
882
- }
1003
+ // ==================== HELPER FUNCTIONS ====================
1004
+
1005
+ function findAllDataProvidersInJSX(blockStatement: types.BlockStatement): any[] {
1006
+ const results: any[] = []
1007
+
1008
+ const traverse = (node: any): void => {
1009
+ if (!node) {
1010
+ return
883
1011
  }
884
- } else if (root.type === 'JSXExpressionContainer') {
885
- if (root.expression) {
886
- const found = findParentNode(root.expression, target, root)
887
- if (found !== null) {
888
- return found
889
- }
1012
+
1013
+ if (node.type === 'JSXElement' && node.openingElement?.name?.name === 'DataProvider') {
1014
+ results.push(node)
890
1015
  }
891
- } else if (root.type === 'BlockStatement') {
892
- if (root.body && Array.isArray(root.body)) {
893
- for (const stmt of root.body) {
894
- const found = findParentNode(stmt, target, root)
895
- if (found !== null) {
896
- return found
897
- }
898
- }
1016
+
1017
+ if (node.children && Array.isArray(node.children)) {
1018
+ node.children.forEach((c: any) => traverse(c))
899
1019
  }
900
- } else if (root.type === 'ReturnStatement') {
901
- if (root.argument) {
902
- const found = findParentNode(root.argument, target, root)
903
- if (found !== null) {
904
- return found
1020
+
1021
+ if (node.body) {
1022
+ if (Array.isArray(node.body)) {
1023
+ node.body.forEach((s: any) => traverse(s))
1024
+ } else {
1025
+ traverse(node.body)
905
1026
  }
906
1027
  }
907
- } else if (root.type === 'ConditionalExpression') {
908
- const foundConsequent = findParentNode(root.consequent, target, root)
909
- if (foundConsequent !== null) {
910
- return foundConsequent
1028
+
1029
+ if (node.consequent) {
1030
+ traverse(node.consequent)
1031
+ }
1032
+
1033
+ if (node.alternate) {
1034
+ traverse(node.alternate)
911
1035
  }
912
- const foundAlternate = findParentNode(root.alternate, target, root)
913
- if (foundAlternate !== null) {
914
- return foundAlternate
1036
+
1037
+ if (node.expression) {
1038
+ traverse(node.expression)
915
1039
  }
916
- }
917
1040
 
918
- return null
919
- }
1041
+ if (node.argument) {
1042
+ traverse(node.argument)
1043
+ }
920
1044
 
921
- function detectPaginationsAndSearchFromJSX(
922
- blockStatement: types.BlockStatement,
923
- uidlNode: any
924
- ): {
925
- paginatedMappers: DetectedPagination[]
926
- searchOnlyMappers: DetectedPagination[]
927
- paginationOnlyMappers: DetectedPagination[]
928
- plainMappers: DetectedPagination[]
929
- } {
930
- interface DataProviderInfo {
931
- identifier: string
932
- dataProvider: any
933
- arrayMapperRenderProp?: string
934
- paginationNode?: { class: string; prevClass: string | null; nextClass: string | null }
935
- searchInput?: { class: string | null; jsx: any }
936
- hasPagination: boolean
937
- hasSearch: boolean
1045
+ if (node.arguments) {
1046
+ node.arguments.forEach((a: any) => traverse(a))
1047
+ }
938
1048
  }
939
1049
 
940
- const dataProviderList: DataProviderInfo[] = []
1050
+ traverse(blockStatement)
1051
+ return results
1052
+ }
1053
+
1054
+ function findArrayMapperRenderPropInDataProvider(dataProvider: any): string | undefined {
1055
+ const renderSuccessAttr = dataProvider.openingElement.attributes.find(
1056
+ (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'renderSuccess'
1057
+ )
941
1058
 
942
- // First pass: collect array mapper info from UIDL, keyed by array mapper render prop (unique)
943
- interface ArrayMapperInfo {
944
- dataSourceIdentifier: string
945
- renderProp: string
946
- paginated: boolean
947
- searchEnabled: boolean
1059
+ if (!renderSuccessAttr?.value?.expression) {
1060
+ return undefined
948
1061
  }
949
- const arrayMapperInfoMap = new Map<string, ArrayMapperInfo>()
950
1062
 
951
- if (uidlNode) {
952
- const collectArrayMapperInfo = (node: any): void => {
953
- if (!node) {
954
- return
955
- }
956
- if (node.type === 'data-source-list' && node.content?.renderPropIdentifier) {
957
- const dataSourceIdentifier = node.content.renderPropIdentifier
958
- if (node.content.nodes?.success?.content?.children) {
959
- node.content.nodes.success.content.children.forEach((child: any) => {
960
- if (child.type === 'cms-list-repeater' && child.content?.renderPropIdentifier) {
961
- const arrayMapperRenderProp = child.content.renderPropIdentifier
962
- // Key by array mapper render prop (unique), not data source identifier
963
- arrayMapperInfoMap.set(arrayMapperRenderProp, {
964
- dataSourceIdentifier,
965
- renderProp: arrayMapperRenderProp,
966
- paginated: child.content.paginated || false,
967
- searchEnabled: child.content.searchEnabled || false,
968
- })
969
- }
970
- })
971
- }
972
- }
973
- if (node.content?.children) {
974
- node.content.children.forEach((child: any) => collectArrayMapperInfo(child))
975
- }
976
- if (node.children && Array.isArray(node.children)) {
977
- node.children.forEach((child: any) => collectArrayMapperInfo(child))
978
- }
1063
+ const findRepeater = (node: any): any => {
1064
+ if (!node) {
1065
+ return null
979
1066
  }
980
- collectArrayMapperInfo(uidlNode)
981
- }
982
1067
 
983
- // Second pass: find all DataProviders and their parent containers
984
- interface DataProviderWithParent {
985
- dataProvider: any
986
- parent: any
987
- }
988
- const dataProvidersWithParents: DataProviderWithParent[] = []
1068
+ if (node.type === 'JSXElement' && node.openingElement?.name?.name === 'Repeater') {
1069
+ return node
1070
+ }
989
1071
 
990
- const findDataProvidersAndParents = (node: any, parent: any = null): void => {
991
- if (!node) {
992
- return
1072
+ if (node.body) {
1073
+ return findRepeater(node.body)
993
1074
  }
994
1075
 
995
- // Check if this is a DataProvider
996
- if (node.type === 'JSXElement' && node.openingElement?.name?.name === 'DataProvider') {
997
- dataProvidersWithParents.push({
998
- dataProvider: node,
999
- parent,
1000
- })
1001
- }
1002
-
1003
- // Only recurse through JSX structure, not into embedded expressions or code
1004
- if (node.type === 'ReturnStatement' && node.argument) {
1005
- findDataProvidersAndParents(node.argument, parent)
1006
- } else if (node.type === 'JSXElement' || node.type === 'JSXFragment') {
1007
- // Recurse through JSX children
1008
- if (node.children && Array.isArray(node.children)) {
1009
- node.children.forEach((child: any) => findDataProvidersAndParents(child, node))
1010
- }
1011
- } else if (node.type === 'JSXExpressionContainer') {
1012
- // For expression containers, continue but don't go into the expression itself
1013
- if (
1014
- node.expression &&
1015
- (node.expression.type === 'JSXElement' || node.expression.type === 'JSXFragment')
1016
- ) {
1017
- findDataProvidersAndParents(node.expression, parent)
1018
- }
1019
- } else if (node.type === 'BlockStatement') {
1020
- // For block statements (function bodies), look through body array
1021
- if (node.body && Array.isArray(node.body)) {
1022
- node.body.forEach((stmt: any) => findDataProvidersAndParents(stmt, node))
1076
+ if (node.children && Array.isArray(node.children)) {
1077
+ for (const c of node.children) {
1078
+ const r = findRepeater(c)
1079
+ if (r) {
1080
+ return r
1081
+ }
1023
1082
  }
1024
- } else if (node.type === 'ConditionalExpression') {
1025
- // Check both branches of conditional
1026
- findDataProvidersAndParents(node.consequent, parent)
1027
- findDataProvidersAndParents(node.alternate, parent)
1028
1083
  }
1084
+ return null
1029
1085
  }
1030
1086
 
1031
- findDataProvidersAndParents(blockStatement)
1087
+ const repeater = findRepeater(renderSuccessAttr.value.expression)
1088
+ if (!repeater) {
1089
+ return undefined
1090
+ }
1032
1091
 
1033
- // Now process each DataProvider and find its siblings
1034
- dataProvidersWithParents.forEach(({ dataProvider, parent }) => {
1035
- const nameAttr = dataProvider.openingElement.attributes.find(
1036
- (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'name'
1037
- )
1092
+ const renderItemAttr = repeater.openingElement.attributes.find(
1093
+ (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'renderItem'
1094
+ )
1038
1095
 
1039
- // Find the array mapper render prop by looking inside renderSuccess -> Repeater -> renderItem param
1040
- const renderSuccessAttr = dataProvider.openingElement.attributes.find(
1041
- (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'renderSuccess'
1042
- )
1096
+ if (!renderItemAttr?.value?.expression?.params?.[0]?.name) {
1097
+ return undefined
1098
+ }
1043
1099
 
1044
- let arrayMapperRenderProp: string | undefined
1045
- if (renderSuccessAttr && renderSuccessAttr.value?.type === 'JSXExpressionContainer') {
1046
- const renderFunc = renderSuccessAttr.value.expression
1047
- if (renderFunc.type === 'ArrowFunctionExpression') {
1048
- // Look for Repeater inside the render function
1049
- const findRepeater = (node: any): any => {
1050
- if (!node) {
1051
- return null
1052
- }
1053
- if (node.type === 'JSXElement' && node.openingElement?.name?.name === 'Repeater') {
1054
- return node
1055
- }
1056
- if (node.body) {
1057
- return findRepeater(node.body)
1058
- }
1059
- if (node.children && Array.isArray(node.children)) {
1060
- for (const child of node.children) {
1061
- const result = findRepeater(child)
1062
- if (result) {
1063
- return result
1064
- }
1065
- }
1066
- }
1067
- if (node.type === 'JSXFragment' || node.type === 'JSXElement') {
1068
- if (node.children && Array.isArray(node.children)) {
1069
- for (const child of node.children) {
1070
- const result = findRepeater(child)
1071
- if (result) {
1072
- return result
1073
- }
1074
- }
1075
- }
1076
- }
1077
- if (node.type === 'JSXExpressionContainer') {
1078
- return findRepeater(node.expression)
1079
- }
1080
- if (node.expression) {
1081
- return findRepeater(node.expression)
1082
- }
1083
- if (node.consequent) {
1084
- const result = findRepeater(node.consequent)
1085
- if (result) {
1086
- return result
1087
- }
1088
- }
1089
- if (node.alternate) {
1090
- return findRepeater(node.alternate)
1091
- }
1092
- return null
1093
- }
1100
+ return renderItemAttr.value.expression.params[0].name
1101
+ }
1094
1102
 
1095
- const repeater = findRepeater(renderFunc)
1096
- if (repeater) {
1097
- // Find renderItem attribute on Repeater
1098
- const renderItemAttr = repeater.openingElement.attributes.find(
1099
- (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'renderItem'
1100
- )
1101
- if (renderItemAttr && renderItemAttr.value?.type === 'JSXExpressionContainer') {
1102
- const renderItemFunc = renderItemAttr.value.expression
1103
- if (
1104
- renderItemFunc.type === 'ArrowFunctionExpression' &&
1105
- renderItemFunc.params &&
1106
- renderItemFunc.params.length > 0
1107
- ) {
1108
- const param = renderItemFunc.params[0]
1109
- if (param.type === 'Identifier') {
1110
- arrayMapperRenderProp = param.name
1111
- }
1112
- }
1113
- }
1114
- }
1115
- }
1116
- }
1103
+ function findAllSearchInputsInJSX(
1104
+ blockStatement: types.BlockStatement
1105
+ ): Array<{ node: any; className: string }> {
1106
+ const results: Array<{ node: any; className: string }> = []
1117
1107
 
1118
- if (
1119
- !nameAttr ||
1120
- !nameAttr.value ||
1121
- nameAttr.value.type !== 'JSXExpressionContainer' ||
1122
- !arrayMapperRenderProp
1123
- ) {
1108
+ const traverse = (node: any): void => {
1109
+ if (!node) {
1124
1110
  return
1125
1111
  }
1126
1112
 
1127
- const dataProviderIdentifier = nameAttr.value.expression.value
1128
- let paginationNodeInfo: {
1129
- class: string
1130
- prevClass: string | null
1131
- nextClass: string | null
1132
- } | null = null
1133
- let searchInputInfo: { class: string | null; jsx: any } | null = null
1134
-
1135
- const findSearchAndPaginationInScope = (scopeNode: any, skipNode: any = null): boolean => {
1136
- if (!scopeNode || !scopeNode.children || !Array.isArray(scopeNode.children)) {
1137
- return false
1138
- }
1139
-
1140
- let foundSomething = false
1141
-
1142
- for (const child of scopeNode.children) {
1143
- if (child === skipNode) {
1144
- continue
1145
- }
1146
-
1147
- if (child.type === 'JSXElement') {
1148
- const childClassName = getClassName(child.openingElement?.attributes || [])
1149
- const childElementName = child.openingElement?.name?.name
1150
-
1151
- // Found pagination node
1152
- if (childClassName && childClassName.includes('cms-pagination-node')) {
1153
- const prevClass = findChildWithClass(child, 'previous')
1154
- const nextClass = findChildWithClass(child, 'next')
1155
- if (prevClass || nextClass) {
1156
- paginationNodeInfo = {
1157
- class: childClassName,
1158
- prevClass,
1159
- nextClass,
1160
- }
1161
- foundSomething = true
1162
- }
1163
- }
1164
-
1165
- // Found search container - search for input inside it
1166
- if (childClassName && childClassName.includes('data-source-search-node')) {
1167
- if (child.children && Array.isArray(child.children)) {
1168
- for (const searchChild of child.children) {
1169
- if (searchChild.type === 'JSXElement') {
1170
- const searchChildElementName = searchChild.openingElement?.name?.name
1171
- const searchChildClassName = getClassName(
1172
- searchChild.openingElement?.attributes || []
1173
- )
1174
- if (
1175
- searchChildClassName &&
1176
- searchChildClassName.includes('search-input') &&
1177
- searchChildElementName === 'input'
1178
- ) {
1179
- searchInputInfo = {
1180
- class: searchChildClassName,
1181
- jsx: searchChild,
1182
- }
1183
- foundSomething = true
1184
- }
1185
- }
1186
- }
1187
- }
1188
- }
1189
-
1190
- // Also check if search input is a direct child
1191
- if (
1192
- childClassName &&
1193
- childClassName.includes('search-input') &&
1194
- childElementName === 'input'
1195
- ) {
1196
- searchInputInfo = {
1197
- class: childClassName,
1198
- jsx: child,
1199
- }
1200
- foundSomething = true
1201
- }
1202
-
1203
- // Stop searching if we found both or if we found what we're looking for
1204
- if (foundSomething && (searchInputInfo || paginationNodeInfo)) {
1205
- return true
1206
- }
1207
-
1208
- // Only recurse if we haven't found what we're looking for yet
1209
- if (!searchInputInfo || !paginationNodeInfo) {
1210
- if (findSearchAndPaginationInScope(child, skipNode)) {
1211
- return true
1212
- }
1213
- }
1214
- }
1113
+ if (node.type === 'JSXElement' && node.openingElement?.name?.name === 'input') {
1114
+ const classAttr = node.openingElement.attributes?.find(
1115
+ (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'className'
1116
+ )
1117
+ const className = classAttr?.value?.value || classAttr?.value?.expression?.value || ''
1118
+ if (className.includes('search-input')) {
1119
+ results.push({ node, className })
1215
1120
  }
1216
-
1217
- return foundSomething
1218
1121
  }
1219
1122
 
1220
- let currentScope = parent
1221
- let depth = 0
1222
- const maxDepth = 5
1223
-
1224
- while (currentScope && (!searchInputInfo || !paginationNodeInfo) && depth < maxDepth) {
1225
- const found = findSearchAndPaginationInScope(currentScope, depth === 0 ? dataProvider : null)
1226
- // Stop searching up the tree if we found a pagination node at this level
1227
- if (found && paginationNodeInfo) {
1228
- break
1123
+ if (node.children && Array.isArray(node.children)) {
1124
+ node.children.forEach((c: any) => traverse(c))
1125
+ }
1126
+ if (node.body) {
1127
+ if (Array.isArray(node.body)) {
1128
+ node.body.forEach((s: any) => traverse(s))
1129
+ } else {
1130
+ traverse(node.body)
1229
1131
  }
1230
- currentScope = findParentNode(blockStatement, currentScope)
1231
- depth++
1232
- }
1233
-
1234
- // Record the DataProvider with its pagination/search info
1235
- // Use array instead of Map to handle multiple DataProviders with same name
1236
- dataProviderList.push({
1237
- identifier: dataProviderIdentifier,
1238
- dataProvider,
1239
- arrayMapperRenderProp,
1240
- paginationNode: paginationNodeInfo || undefined,
1241
- searchInput: searchInputInfo || undefined,
1242
- hasPagination: !!paginationNodeInfo,
1243
- hasSearch: !!searchInputInfo,
1244
- })
1245
- })
1246
-
1247
- // Categorize data providers
1248
- const paginatedMappers: DetectedPagination[] = []
1249
- const searchOnlyMappers: DetectedPagination[] = []
1250
- const paginationOnlyMappers: DetectedPagination[] = []
1251
- const plainMappers: DetectedPagination[] = []
1252
-
1253
- dataProviderList.forEach((info) => {
1254
- // Check UIDL flags for this array mapper
1255
- const uidlInfo = info.arrayMapperRenderProp
1256
- ? arrayMapperInfoMap.get(info.arrayMapperRenderProp)
1257
- : null
1258
-
1259
- // Only process pagination/search if UIDL explicitly enables it
1260
- const shouldHavePagination = uidlInfo?.paginated && info.hasPagination
1261
- const shouldHaveSearch = uidlInfo?.searchEnabled && info.hasSearch
1262
-
1263
- if (shouldHavePagination && shouldHaveSearch) {
1264
- // Pagination + Search
1265
- paginatedMappers.push({
1266
- paginationNodeClass: info.paginationNode!.class,
1267
- prevButtonClass: info.paginationNode!.prevClass,
1268
- nextButtonClass: info.paginationNode!.nextClass,
1269
- dataSourceIdentifier: info.identifier,
1270
- dataProviderJSX: info.dataProvider,
1271
- arrayMapperRenderProp: info.arrayMapperRenderProp,
1272
- searchInputClass: info.searchInput?.class,
1273
- searchInputJSX: info.searchInput?.jsx,
1274
- })
1275
- } else if (shouldHavePagination && !shouldHaveSearch) {
1276
- // Pagination only
1277
- paginationOnlyMappers.push({
1278
- paginationNodeClass: info.paginationNode!.class,
1279
- prevButtonClass: info.paginationNode!.prevClass,
1280
- nextButtonClass: info.paginationNode!.nextClass,
1281
- dataSourceIdentifier: info.identifier,
1282
- dataProviderJSX: info.dataProvider,
1283
- arrayMapperRenderProp: info.arrayMapperRenderProp,
1284
- searchInputClass: undefined,
1285
- searchInputJSX: undefined,
1286
- })
1287
- } else if (!shouldHavePagination && shouldHaveSearch) {
1288
- // Search only
1289
- searchOnlyMappers.push({
1290
- paginationNodeClass: '',
1291
- prevButtonClass: null,
1292
- nextButtonClass: null,
1293
- dataSourceIdentifier: info.identifier,
1294
- dataProviderJSX: info.dataProvider,
1295
- arrayMapperRenderProp: info.arrayMapperRenderProp,
1296
- searchInputClass: info.searchInput?.class,
1297
- searchInputJSX: info.searchInput?.jsx,
1298
- })
1299
- } else {
1300
- // Plain (no pagination, no search) - UIDL doesn't enable it or no controls found
1301
- plainMappers.push({
1302
- paginationNodeClass: '',
1303
- prevButtonClass: null,
1304
- nextButtonClass: null,
1305
- dataSourceIdentifier: info.identifier,
1306
- dataProviderJSX: info.dataProvider,
1307
- arrayMapperRenderProp: info.arrayMapperRenderProp,
1308
- searchInputClass: undefined,
1309
- searchInputJSX: undefined,
1310
- })
1311
1132
  }
1312
- })
1133
+ if (node.consequent) {
1134
+ traverse(node.consequent)
1135
+ }
1136
+ if (node.alternate) {
1137
+ traverse(node.alternate)
1138
+ }
1139
+ if (node.expression) {
1140
+ traverse(node.expression)
1141
+ }
1142
+ if (node.argument) {
1143
+ traverse(node.argument)
1144
+ }
1145
+ }
1313
1146
 
1314
- return { paginatedMappers, searchOnlyMappers, paginationOnlyMappers, plainMappers }
1147
+ traverse(blockStatement)
1148
+ return results
1315
1149
  }
1316
1150
 
1317
- function handleSearchOnlyArrayMappers(
1318
- blockStatement: types.BlockStatement,
1319
- searchOnlyDataSources: DetectedPagination[],
1320
- searchConfigMap: Map<string, any>,
1321
- queryColumnsMap: Map<string, string[]>,
1322
- dependencies: any
1323
- ): void {
1324
- const searchOnlyStates: types.Statement[] = []
1325
- const searchOnlyEffects: types.Statement[] = []
1326
-
1327
- searchOnlyDataSources.forEach((detected, index) => {
1328
- const searchId = `search_${index}`
1329
- const searchQueryVar = `${searchId}_query`
1330
- const setSearchQueryVar = `set${
1331
- searchQueryVar.charAt(0).toUpperCase() + searchQueryVar.slice(1)
1332
- }`
1333
- const debouncedSearchQueryVar = `debounced${
1334
- searchQueryVar.charAt(0).toUpperCase() + searchQueryVar.slice(1)
1335
- }`
1336
- const setDebouncedSearchQueryVar = `set${
1337
- debouncedSearchQueryVar.charAt(0).toUpperCase() + debouncedSearchQueryVar.slice(1)
1338
- }`
1339
- const skipDebounceOnMountRefVar = `skipDebounceOnMount${searchId}`
1340
-
1341
- const searchConfig = searchConfigMap.get(detected.dataSourceIdentifier)
1342
- const searchDebounce = searchConfig?.searchDebounce || 300
1343
- const queryColumns = queryColumnsMap.get(detected.dataSourceIdentifier)
1344
-
1345
- // Add skip ref
1346
- const skipRefAST = types.variableDeclaration('const', [
1347
- types.variableDeclarator(
1348
- types.identifier(skipDebounceOnMountRefVar),
1349
- types.callExpression(types.identifier('useRef'), [types.booleanLiteral(true)])
1350
- ),
1351
- ])
1352
- searchOnlyStates.push(skipRefAST)
1353
-
1354
- // Add debounced search state
1355
- const debouncedSearchStateAST = types.variableDeclaration('const', [
1356
- types.variableDeclarator(
1357
- types.arrayPattern([
1358
- types.identifier(debouncedSearchQueryVar),
1359
- types.identifier(setDebouncedSearchQueryVar),
1360
- ]),
1361
- types.callExpression(types.identifier('useState'), [types.stringLiteral('')])
1362
- ),
1363
- ])
1364
- searchOnlyStates.push(debouncedSearchStateAST)
1365
-
1366
- // Add search query state
1367
- const searchStateAST = types.variableDeclaration('const', [
1368
- types.variableDeclarator(
1369
- types.arrayPattern([types.identifier(searchQueryVar), types.identifier(setSearchQueryVar)]),
1370
- types.callExpression(types.identifier('useState'), [types.stringLiteral('')])
1371
- ),
1372
- ])
1373
- searchOnlyStates.push(searchStateAST)
1151
+ function findAllPaginationNodesInJSX(
1152
+ blockStatement: types.BlockStatement
1153
+ ): Array<{ node: any; className: string }> {
1154
+ const results: Array<{ node: any; className: string }> = []
1374
1155
 
1375
- // Add useEffect for debouncing
1376
- if (!dependencies.useEffect) {
1377
- dependencies.useEffect = {
1378
- type: 'library',
1379
- path: 'react',
1380
- version: '',
1381
- meta: {
1382
- namedImport: true,
1383
- },
1384
- }
1156
+ const traverse = (node: any): void => {
1157
+ if (!node) {
1158
+ return
1385
1159
  }
1386
1160
 
1387
- const debounceEffect = types.expressionStatement(
1388
- types.callExpression(types.identifier('useEffect'), [
1389
- types.arrowFunctionExpression(
1390
- [],
1391
- types.blockStatement([
1392
- types.ifStatement(
1393
- types.memberExpression(
1394
- types.identifier(skipDebounceOnMountRefVar),
1395
- types.identifier('current')
1396
- ),
1397
- types.blockStatement([
1398
- types.expressionStatement(
1399
- types.assignmentExpression(
1400
- '=',
1401
- types.memberExpression(
1402
- types.identifier(skipDebounceOnMountRefVar),
1403
- types.identifier('current')
1404
- ),
1405
- types.booleanLiteral(false)
1406
- )
1407
- ),
1408
- types.returnStatement(),
1409
- ])
1410
- ),
1411
- types.variableDeclaration('const', [
1412
- types.variableDeclarator(
1413
- types.identifier('timer'),
1414
- types.callExpression(types.identifier('setTimeout'), [
1415
- types.arrowFunctionExpression(
1416
- [],
1417
- types.blockStatement([
1418
- types.expressionStatement(
1419
- types.callExpression(types.identifier(setDebouncedSearchQueryVar), [
1420
- types.identifier(searchQueryVar),
1421
- ])
1422
- ),
1423
- ])
1424
- ),
1425
- types.numericLiteral(searchDebounce),
1426
- ])
1427
- ),
1428
- ]),
1429
- types.returnStatement(
1430
- types.arrowFunctionExpression(
1431
- [],
1432
- types.callExpression(types.identifier('clearTimeout'), [types.identifier('timer')])
1433
- )
1434
- ),
1435
- ])
1436
- ),
1437
- types.arrayExpression([types.identifier(searchQueryVar)]),
1438
- ])
1439
- )
1440
- searchOnlyEffects.push(debounceEffect)
1441
-
1442
- // Modify DataProvider to include search params (even without queryColumns)
1443
- if (detected.dataProviderJSX) {
1444
- addSearchParamsToDataProvider(detected.dataProviderJSX, {
1445
- debouncedSearchQueryVar,
1446
- queryColumns: queryColumns || [],
1447
- })
1161
+ if (node.type === 'JSXElement') {
1162
+ const classAttr = node.openingElement?.attributes?.find(
1163
+ (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'className'
1164
+ )
1165
+ const className = classAttr?.value?.value || classAttr?.value?.expression?.value || ''
1166
+ if (className.includes('cms-pagination-node')) {
1167
+ results.push({ node, className })
1168
+ }
1448
1169
  }
1449
1170
 
1450
- // Modify search input
1451
- if (detected.searchInputJSX) {
1452
- addSearchInputHandlers(detected.searchInputJSX, {
1453
- searchQueryVar,
1454
- setSearchQueryVar,
1455
- searchEnabled: true,
1456
- } as any)
1171
+ if (node.children && Array.isArray(node.children)) {
1172
+ node.children.forEach((c: any) => traverse(c))
1457
1173
  }
1458
- })
1459
-
1460
- // Add all state declarations at the beginning in correct order
1461
- searchOnlyStates.reverse().forEach((stateDecl) => {
1462
- blockStatement.body.unshift(stateDecl)
1463
- })
1464
-
1465
- // Recalculate insertIndex after adding states (since unshift shifted everything)
1466
- const newInsertIndex = blockStatement.body.findIndex(
1467
- (stmt: any) => stmt.type === 'ReturnStatement'
1468
- )
1469
- const finalInsertIndex = newInsertIndex !== -1 ? newInsertIndex : blockStatement.body.length
1174
+ if (node.body) {
1175
+ if (Array.isArray(node.body)) {
1176
+ node.body.forEach((s: any) => traverse(s))
1177
+ } else {
1178
+ traverse(node.body)
1179
+ }
1180
+ }
1181
+ if (node.consequent) {
1182
+ traverse(node.consequent)
1183
+ }
1184
+ if (node.alternate) {
1185
+ traverse(node.alternate)
1186
+ }
1187
+ if (node.expression) {
1188
+ traverse(node.expression)
1189
+ }
1190
+ if (node.argument) {
1191
+ traverse(node.argument)
1192
+ }
1193
+ }
1470
1194
 
1471
- // Add all useEffect hooks before the return statement
1472
- searchOnlyEffects.reverse().forEach((effect) => {
1473
- blockStatement.body.splice(finalInsertIndex, 0, effect)
1474
- })
1195
+ traverse(blockStatement)
1196
+ return results
1475
1197
  }
1476
1198
 
1477
- function addSearchParamsToDataProvider(
1478
- dataProviderJSX: any,
1479
- config: { debouncedSearchQueryVar: string; queryColumns: string[] }
1199
+ function updateDataProviderForPaginatedSearch(
1200
+ dp: any,
1201
+ usage: DataSourceUsage,
1202
+ vars: ReturnType<typeof getStateVarsForUsage>,
1203
+ fileName: string
1480
1204
  ): void {
1481
- if (!dataProviderJSX || !dataProviderJSX.openingElement) {
1482
- return
1483
- }
1484
-
1485
- const { debouncedSearchQueryVar, queryColumns } = config
1205
+ const attrs = dp.openingElement.attributes
1486
1206
 
1487
- // 1. Update params to be wrapped in useMemo
1488
- const existingParamsAttr = dataProviderJSX.openingElement.attributes.find(
1489
- (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'params'
1207
+ // Remove existing params, key, initialData, fetchData, persistDataDuringLoading
1208
+ dp.openingElement.attributes = attrs.filter(
1209
+ (attr: any) =>
1210
+ !['params', 'key', 'initialData', 'fetchData', 'persistDataDuringLoading'].includes(
1211
+ attr.name?.name
1212
+ )
1490
1213
  )
1491
1214
 
1492
- if (existingParamsAttr && existingParamsAttr.value?.type === 'JSXExpressionContainer') {
1493
- const paramsObj = existingParamsAttr.value.expression
1494
-
1495
- if (paramsObj.type === 'ObjectExpression') {
1496
- // Add query to existing params properties
1497
- paramsObj.properties.push(
1498
- types.objectProperty(types.identifier('query'), types.identifier(debouncedSearchQueryVar))
1215
+ // Add params with useMemo
1216
+ const paramsProps: types.ObjectProperty[] = [
1217
+ types.objectProperty(
1218
+ types.identifier('page'),
1219
+ types.memberExpression(types.identifier(vars.combinedStateVar), types.identifier('page'))
1220
+ ),
1221
+ types.objectProperty(types.identifier('perPage'), types.numericLiteral(usage.perPage)),
1222
+ types.objectProperty(
1223
+ types.identifier('query'),
1224
+ types.memberExpression(
1225
+ types.identifier(vars.combinedStateVar),
1226
+ types.identifier('debouncedQuery')
1499
1227
  )
1500
-
1501
- // Add queryColumns only if they exist
1502
- if (queryColumns.length > 0) {
1503
- paramsObj.properties.push(
1504
- types.objectProperty(
1505
- types.identifier('queryColumns'),
1506
- types.callExpression(
1507
- types.memberExpression(types.identifier('JSON'), types.identifier('stringify')),
1508
- [types.arrayExpression(queryColumns.map((col) => types.stringLiteral(col)))]
1509
- )
1510
- )
1228
+ ),
1229
+ ]
1230
+ if (usage.queryColumns.length > 0) {
1231
+ paramsProps.push(
1232
+ types.objectProperty(
1233
+ types.identifier('queryColumns'),
1234
+ types.callExpression(
1235
+ types.memberExpression(types.identifier('JSON'), types.identifier('stringify')),
1236
+ [types.arrayExpression(usage.queryColumns.map((c) => types.stringLiteral(c)))]
1511
1237
  )
1512
- }
1238
+ )
1239
+ )
1240
+ }
1513
1241
 
1514
- // Wrap in useMemo with debouncedSearchQueryVar as dependency
1515
- const memoizedParamsValue = types.callExpression(types.identifier('useMemo'), [
1516
- types.arrowFunctionExpression([], paramsObj),
1517
- types.arrayExpression([types.identifier(debouncedSearchQueryVar)]),
1518
- ])
1242
+ dp.openingElement.attributes.push(
1243
+ types.jsxAttribute(
1244
+ types.jsxIdentifier('params'),
1245
+ types.jsxExpressionContainer(
1246
+ types.callExpression(types.identifier('useMemo'), [
1247
+ types.arrowFunctionExpression([], types.objectExpression(paramsProps)),
1248
+ types.arrayExpression([types.identifier(vars.combinedStateVar)]),
1249
+ ])
1250
+ )
1251
+ )
1252
+ )
1519
1253
 
1520
- existingParamsAttr.value.expression = memoizedParamsValue
1521
- }
1522
- }
1254
+ // Add initialData
1255
+ const initialDataCondition = types.logicalExpression(
1256
+ '&&',
1257
+ types.binaryExpression(
1258
+ '===',
1259
+ types.memberExpression(types.identifier(vars.combinedStateVar), types.identifier('page')),
1260
+ types.numericLiteral(1)
1261
+ ),
1262
+ types.unaryExpression(
1263
+ '!',
1264
+ types.memberExpression(
1265
+ types.identifier(vars.combinedStateVar),
1266
+ types.identifier('debouncedQuery')
1267
+ ),
1268
+ true
1269
+ )
1270
+ )
1271
+ dp.openingElement.attributes.push(
1272
+ types.jsxAttribute(
1273
+ types.jsxIdentifier('initialData'),
1274
+ types.jsxExpressionContainer(
1275
+ types.conditionalExpression(
1276
+ initialDataCondition,
1277
+ types.optionalMemberExpression(
1278
+ types.identifier('props'),
1279
+ types.identifier(vars.propsPrefix),
1280
+ false,
1281
+ true
1282
+ ),
1283
+ types.identifier('undefined')
1284
+ )
1285
+ )
1286
+ )
1287
+ )
1523
1288
 
1524
- // 2. Update initialData to be conditional on empty search
1525
- const existingInitialDataAttr = dataProviderJSX.openingElement.attributes.find(
1526
- (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'initialData'
1289
+ // Add key
1290
+ dp.openingElement.attributes.push(
1291
+ types.jsxAttribute(
1292
+ types.jsxIdentifier('key'),
1293
+ types.jsxExpressionContainer(
1294
+ types.templateLiteral(
1295
+ [
1296
+ types.templateElement({
1297
+ raw: `${usage.dataSourceIdentifier}-`,
1298
+ cooked: `${usage.dataSourceIdentifier}-`,
1299
+ }),
1300
+ types.templateElement({ raw: '-', cooked: '-' }),
1301
+ types.templateElement({ raw: '', cooked: '' }),
1302
+ ],
1303
+ [
1304
+ types.memberExpression(
1305
+ types.identifier(vars.combinedStateVar),
1306
+ types.identifier('page')
1307
+ ),
1308
+ types.memberExpression(
1309
+ types.identifier(vars.combinedStateVar),
1310
+ types.identifier('debouncedQuery')
1311
+ ),
1312
+ ]
1313
+ )
1314
+ )
1315
+ )
1527
1316
  )
1528
1317
 
1529
- if (existingInitialDataAttr && existingInitialDataAttr.value?.type === 'JSXExpressionContainer') {
1530
- const currentInitialData = existingInitialDataAttr.value.expression
1318
+ // Add fetchData
1319
+ dp.openingElement.attributes.push(createFetchDataAttribute(fileName))
1531
1320
 
1532
- // Make it conditional: only use initialData if search is empty
1533
- existingInitialDataAttr.value.expression = types.conditionalExpression(
1534
- types.unaryExpression('!', types.identifier(debouncedSearchQueryVar), true),
1535
- currentInitialData,
1536
- types.identifier('undefined')
1321
+ // Add persistDataDuringLoading
1322
+ dp.openingElement.attributes.push(
1323
+ types.jsxAttribute(
1324
+ types.jsxIdentifier('persistDataDuringLoading'),
1325
+ types.jsxExpressionContainer(types.booleanLiteral(true))
1537
1326
  )
1538
- }
1327
+ )
1328
+ }
1329
+
1330
+ function updateDataProviderForPaginationOnly(
1331
+ dp: any,
1332
+ usage: DataSourceUsage,
1333
+ vars: ReturnType<typeof getStateVarsForUsage>,
1334
+ fileName: string
1335
+ ): void {
1336
+ const attrs = dp.openingElement.attributes
1337
+
1338
+ dp.openingElement.attributes = attrs.filter(
1339
+ (attr: any) =>
1340
+ !['params', 'key', 'initialData', 'fetchData', 'persistDataDuringLoading'].includes(
1341
+ attr.name?.name
1342
+ )
1343
+ )
1539
1344
 
1540
- // 3. Update key to include search query
1541
- const existingKeyAttr = dataProviderJSX.openingElement.attributes.find(
1542
- (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'key'
1345
+ // Add params
1346
+ dp.openingElement.attributes.push(
1347
+ types.jsxAttribute(
1348
+ types.jsxIdentifier('params'),
1349
+ types.jsxExpressionContainer(
1350
+ types.callExpression(types.identifier('useMemo'), [
1351
+ types.arrowFunctionExpression(
1352
+ [],
1353
+ types.objectExpression([
1354
+ types.objectProperty(types.identifier('page'), types.identifier(vars.pageStateVar)),
1355
+ types.objectProperty(
1356
+ types.identifier('perPage'),
1357
+ types.numericLiteral(usage.perPage)
1358
+ ),
1359
+ ])
1360
+ ),
1361
+ types.arrayExpression([types.identifier(vars.pageStateVar)]),
1362
+ ])
1363
+ )
1364
+ )
1543
1365
  )
1544
1366
 
1545
- const keyExpression = types.templateLiteral(
1546
- [
1547
- types.templateElement({ raw: 'search-', cooked: 'search-' }),
1548
- types.templateElement({ raw: '', cooked: '' }),
1549
- ],
1550
- [types.identifier(debouncedSearchQueryVar)]
1367
+ // Add initialData
1368
+ dp.openingElement.attributes.push(
1369
+ types.jsxAttribute(
1370
+ types.jsxIdentifier('initialData'),
1371
+ types.jsxExpressionContainer(
1372
+ types.conditionalExpression(
1373
+ types.binaryExpression(
1374
+ '===',
1375
+ types.identifier(vars.pageStateVar),
1376
+ types.numericLiteral(1)
1377
+ ),
1378
+ types.optionalMemberExpression(
1379
+ types.identifier('props'),
1380
+ types.identifier(vars.propsPrefix),
1381
+ false,
1382
+ true
1383
+ ),
1384
+ types.identifier('undefined')
1385
+ )
1386
+ )
1387
+ )
1551
1388
  )
1552
1389
 
1553
- const keyAttr = types.jsxAttribute(
1554
- types.jsxIdentifier('key'),
1555
- types.jsxExpressionContainer(keyExpression)
1390
+ // Add key
1391
+ dp.openingElement.attributes.push(
1392
+ types.jsxAttribute(
1393
+ types.jsxIdentifier('key'),
1394
+ types.jsxExpressionContainer(
1395
+ types.templateLiteral(
1396
+ [
1397
+ types.templateElement({
1398
+ raw: `${usage.dataSourceIdentifier}-page-`,
1399
+ cooked: `${usage.dataSourceIdentifier}-page-`,
1400
+ }),
1401
+ types.templateElement({ raw: '', cooked: '' }),
1402
+ ],
1403
+ [types.identifier(vars.pageStateVar)]
1404
+ )
1405
+ )
1406
+ )
1556
1407
  )
1557
1408
 
1558
- if (existingKeyAttr) {
1559
- const index = dataProviderJSX.openingElement.attributes.indexOf(existingKeyAttr)
1560
- dataProviderJSX.openingElement.attributes[index] = keyAttr
1561
- } else {
1562
- dataProviderJSX.openingElement.attributes.push(keyAttr)
1563
- }
1409
+ // Add fetchData
1410
+ dp.openingElement.attributes.push(createFetchDataAttribute(fileName))
1411
+
1412
+ dp.openingElement.attributes.push(
1413
+ types.jsxAttribute(
1414
+ types.jsxIdentifier('persistDataDuringLoading'),
1415
+ types.jsxExpressionContainer(types.booleanLiteral(true))
1416
+ )
1417
+ )
1564
1418
  }
1565
1419
 
1566
- function addPaginationParamsToDataProvider(
1567
- dataProviderJSX: any,
1568
- info: ArrayMapperPaginationInfo,
1569
- paginationIndex: number
1420
+ function updateDataProviderForSearchOnly(
1421
+ dp: any,
1422
+ usage: DataSourceUsage,
1423
+ vars: ReturnType<typeof getStateVarsForUsage>,
1424
+ fileName: string
1570
1425
  ): void {
1571
- if (!dataProviderJSX || !dataProviderJSX.openingElement) {
1572
- return
1573
- }
1426
+ const attrs = dp.openingElement.attributes
1574
1427
 
1575
- const combinedStateVar = (info as any).combinedStateVar
1576
-
1577
- let paramsProperties: any[]
1578
- let dependencies: any[]
1428
+ dp.openingElement.attributes = attrs.filter(
1429
+ (attr: any) =>
1430
+ !['params', 'key', 'initialData', 'fetchData', 'persistDataDuringLoading'].includes(
1431
+ attr.name?.name
1432
+ )
1433
+ )
1579
1434
 
1580
- if (info.searchEnabled && combinedStateVar) {
1581
- // Use combined state for both page and query
1582
- paramsProperties = [
1583
- types.objectProperty(
1584
- types.identifier('page'),
1585
- types.memberExpression(types.identifier(combinedStateVar), types.identifier('page'))
1586
- ),
1587
- types.objectProperty(types.identifier('perPage'), types.numericLiteral(info.perPage)),
1435
+ // Add params
1436
+ const paramsProps: types.ObjectProperty[] = [
1437
+ types.objectProperty(types.identifier('query'), types.identifier(vars.debouncedSearchQueryVar)),
1438
+ ]
1439
+ if (usage.queryColumns.length > 0) {
1440
+ paramsProps.push(
1588
1441
  types.objectProperty(
1589
- types.identifier('query'),
1590
- types.memberExpression(
1591
- types.identifier(combinedStateVar),
1592
- types.identifier('debouncedQuery')
1593
- )
1594
- ),
1595
- ]
1596
-
1597
- if (info.queryColumns && info.queryColumns.length > 0) {
1598
- paramsProperties.push(
1599
- types.objectProperty(
1600
- types.identifier('queryColumns'),
1601
- types.callExpression(
1602
- types.memberExpression(types.identifier('JSON'), types.identifier('stringify')),
1603
- [types.arrayExpression(info.queryColumns.map((col) => types.stringLiteral(col)))]
1604
- )
1442
+ types.identifier('queryColumns'),
1443
+ types.callExpression(
1444
+ types.memberExpression(types.identifier('JSON'), types.identifier('stringify')),
1445
+ [types.arrayExpression(usage.queryColumns.map((c) => types.stringLiteral(c)))]
1605
1446
  )
1606
1447
  )
1607
- }
1608
-
1609
- // Single dependency: the combined state object
1610
- dependencies = [types.identifier(combinedStateVar)]
1611
- } else {
1612
- // Pagination only (no search)
1613
- paramsProperties = [
1614
- types.objectProperty(types.identifier('page'), types.identifier(info.pageStateVar)),
1615
- types.objectProperty(types.identifier('perPage'), types.numericLiteral(info.perPage)),
1616
- ]
1617
-
1618
- dependencies = [types.identifier(info.pageStateVar)]
1448
+ )
1619
1449
  }
1620
1450
 
1621
- // Wrap params in useMemo to prevent unnecessary refetches
1622
- const memoizedParamsValue = types.callExpression(types.identifier('useMemo'), [
1623
- types.arrowFunctionExpression([], types.objectExpression(paramsProperties)),
1624
- types.arrayExpression(dependencies),
1625
- ])
1451
+ dp.openingElement.attributes.push(
1452
+ types.jsxAttribute(
1453
+ types.jsxIdentifier('params'),
1454
+ types.jsxExpressionContainer(
1455
+ types.callExpression(types.identifier('useMemo'), [
1456
+ types.arrowFunctionExpression([], types.objectExpression(paramsProps)),
1457
+ types.arrayExpression([types.identifier(vars.debouncedSearchQueryVar)]),
1458
+ ])
1459
+ )
1460
+ )
1461
+ )
1626
1462
 
1627
- const paramsAttr = types.jsxAttribute(
1628
- types.jsxIdentifier('params'),
1629
- types.jsxExpressionContainer(memoizedParamsValue)
1463
+ // Add initialData
1464
+ dp.openingElement.attributes.push(
1465
+ types.jsxAttribute(
1466
+ types.jsxIdentifier('initialData'),
1467
+ types.jsxExpressionContainer(
1468
+ types.conditionalExpression(
1469
+ types.unaryExpression('!', types.identifier(vars.debouncedSearchQueryVar), true),
1470
+ types.optionalMemberExpression(
1471
+ types.identifier('props'),
1472
+ types.identifier(vars.propsPrefix),
1473
+ false,
1474
+ true
1475
+ ),
1476
+ types.identifier('undefined')
1477
+ )
1478
+ )
1479
+ )
1630
1480
  )
1631
1481
 
1632
- const existingParamsAttr = dataProviderJSX.openingElement.attributes.find(
1633
- (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'params'
1482
+ // Add key
1483
+ dp.openingElement.attributes.push(
1484
+ types.jsxAttribute(
1485
+ types.jsxIdentifier('key'),
1486
+ types.jsxExpressionContainer(
1487
+ types.templateLiteral(
1488
+ [
1489
+ types.templateElement({ raw: 'search-', cooked: 'search-' }),
1490
+ types.templateElement({ raw: '', cooked: '' }),
1491
+ ],
1492
+ [types.identifier(vars.debouncedSearchQueryVar)]
1493
+ )
1494
+ )
1495
+ )
1634
1496
  )
1635
1497
 
1636
- if (existingParamsAttr) {
1637
- const index = dataProviderJSX.openingElement.attributes.indexOf(existingParamsAttr)
1638
- dataProviderJSX.openingElement.attributes[index] = paramsAttr
1639
- } else {
1640
- dataProviderJSX.openingElement.attributes.push(paramsAttr)
1641
- }
1498
+ // Add fetchData
1499
+ dp.openingElement.attributes.push(createFetchDataAttribute(fileName))
1642
1500
 
1643
- const existingInitialDataAttr = dataProviderJSX.openingElement.attributes.find(
1644
- (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'initialData'
1501
+ dp.openingElement.attributes.push(
1502
+ types.jsxAttribute(
1503
+ types.jsxIdentifier('persistDataDuringLoading'),
1504
+ types.jsxExpressionContainer(types.booleanLiteral(true))
1505
+ )
1645
1506
  )
1507
+ }
1646
1508
 
1647
- if (existingInitialDataAttr && existingInitialDataAttr.value) {
1648
- // Update initialData to use the paginated prop name
1649
- const paginatedPropName = `${info.dataSourceIdentifier}_pg_${paginationIndex}`
1509
+ function updateDataProviderForPlain(dp: any, vars: ReturnType<typeof getStateVarsForUsage>): void {
1510
+ const attrs = dp.openingElement.attributes
1650
1511
 
1651
- // If search is enabled, use combined state; otherwise use page state
1652
- let condition: any
1512
+ // Remove params and fetchData - plain mappers only use initialData
1513
+ dp.openingElement.attributes = attrs.filter(
1514
+ (attr: any) => !['params', 'fetchData'].includes(attr.name?.name)
1515
+ )
1653
1516
 
1654
- if (info.searchEnabled && combinedStateVar) {
1655
- condition = types.logicalExpression(
1656
- '&&',
1657
- types.binaryExpression(
1658
- '===',
1659
- types.memberExpression(types.identifier(combinedStateVar), types.identifier('page')),
1660
- types.numericLiteral(1)
1661
- ),
1662
- types.unaryExpression(
1663
- '!',
1664
- types.memberExpression(
1665
- types.identifier(combinedStateVar),
1666
- types.identifier('debouncedQuery')
1667
- ),
1668
- true
1669
- )
1670
- )
1671
- } else {
1672
- condition = types.binaryExpression(
1673
- '===',
1674
- types.identifier(info.pageStateVar),
1675
- types.numericLiteral(1)
1676
- )
1677
- }
1517
+ // Update initialData to use correct prop
1518
+ const existingInitialData = dp.openingElement.attributes.find(
1519
+ (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'initialData'
1520
+ )
1678
1521
 
1679
- existingInitialDataAttr.value.expression = types.conditionalExpression(
1680
- condition,
1522
+ if (existingInitialData) {
1523
+ existingInitialData.value = types.jsxExpressionContainer(
1681
1524
  types.optionalMemberExpression(
1682
1525
  types.identifier('props'),
1683
- types.identifier(paginatedPropName),
1526
+ types.identifier(vars.propsPrefix),
1684
1527
  false,
1685
1528
  true
1686
- ),
1687
- types.identifier('undefined')
1529
+ )
1688
1530
  )
1689
1531
  }
1532
+ }
1533
+
1534
+ function stabilizeDataProviderWithoutRepeater(dp: any): void {
1535
+ const attrs = dp.openingElement.attributes
1690
1536
 
1691
- const existingKeyAttr = dataProviderJSX.openingElement.attributes.find(
1692
- (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'key'
1537
+ // Find the params attribute
1538
+ const paramsAttrIndex = attrs.findIndex(
1539
+ (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'params'
1693
1540
  )
1694
1541
 
1695
- // Include page and search in key to trigger refetch when they change
1696
- let keyExpression: any
1697
- if (info.searchEnabled && combinedStateVar) {
1698
- keyExpression = types.templateLiteral(
1699
- [
1700
- types.templateElement({
1701
- raw: `${info.dataSourceIdentifier}-page-`,
1702
- cooked: `${info.dataSourceIdentifier}-page-`,
1703
- }),
1704
- types.templateElement({ raw: '-search-', cooked: '-search-' }),
1705
- types.templateElement({ raw: '', cooked: '' }),
1706
- ],
1707
- [
1708
- types.memberExpression(types.identifier(combinedStateVar), types.identifier('page')),
1709
- types.memberExpression(
1710
- types.identifier(combinedStateVar),
1711
- types.identifier('debouncedQuery')
1712
- ),
1713
- ]
1714
- )
1715
- } else {
1716
- keyExpression = types.templateLiteral(
1717
- [
1718
- types.templateElement({
1719
- raw: `${info.dataSourceIdentifier}-`,
1720
- cooked: `${info.dataSourceIdentifier}-`,
1721
- }),
1722
- types.templateElement({ raw: '', cooked: '' }),
1723
- ],
1724
- [types.identifier(info.pageStateVar)]
1725
- )
1542
+ if (paramsAttrIndex === -1) {
1543
+ return
1726
1544
  }
1727
1545
 
1728
- const keyAttr = types.jsxAttribute(
1729
- types.jsxIdentifier('key'),
1730
- types.jsxExpressionContainer(keyExpression)
1731
- )
1546
+ const paramsAttr = attrs[paramsAttrIndex] as types.JSXAttribute
1732
1547
 
1733
- if (existingKeyAttr) {
1734
- const index = dataProviderJSX.openingElement.attributes.indexOf(existingKeyAttr)
1735
- dataProviderJSX.openingElement.attributes[index] = keyAttr
1736
- } else {
1737
- dataProviderJSX.openingElement.attributes.push(keyAttr)
1548
+ // Check if params is already wrapped in useMemo
1549
+ if (
1550
+ paramsAttr.value?.type === 'JSXExpressionContainer' &&
1551
+ paramsAttr.value.expression.type === 'CallExpression' &&
1552
+ (paramsAttr.value.expression.callee as types.Identifier)?.name === 'useMemo'
1553
+ ) {
1554
+ return
1738
1555
  }
1739
1556
 
1740
- // For pagination, always create a fresh fetchData that calls the API route
1741
- // Get the resource definition to build the API URL
1742
- const resourceDefAttr = dataProviderJSX.openingElement.attributes.find(
1743
- (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'resourceDefinition'
1744
- )
1557
+ // Get the current params value expression
1558
+ let paramsExpression: types.Expression | null = null
1745
1559
 
1746
- if (resourceDefAttr && resourceDefAttr.value?.type === 'JSXExpressionContainer') {
1747
- const resourceDef = resourceDefAttr.value.expression
1748
- if (resourceDef.type === 'ObjectExpression') {
1749
- const dataSourceIdProp = (resourceDef.properties as any[]).find(
1750
- (p: any) => p.type === 'ObjectProperty' && p.key.value === 'dataSourceId'
1751
- )
1752
- const tableNameProp = (resourceDef.properties as any[]).find(
1753
- (p: any) => p.type === 'ObjectProperty' && p.key.value === 'tableName'
1754
- )
1755
- const dataSourceTypeProp = (resourceDef.properties as any[]).find(
1756
- (p: any) => p.type === 'ObjectProperty' && p.key.value === 'dataSourceType'
1757
- )
1560
+ if (paramsAttr.value?.type === 'JSXExpressionContainer') {
1561
+ paramsExpression = paramsAttr.value.expression as types.Expression
1562
+ }
1758
1563
 
1759
- if (dataSourceIdProp && tableNameProp && dataSourceTypeProp) {
1760
- const dataSourceId = dataSourceIdProp.value.value
1761
- const tableName = tableNameProp.value.value
1762
- const dataSourceType = dataSourceTypeProp.value.value
1763
- const fileName = `${dataSourceType}-${tableName}-${dataSourceId.substring(0, 8)}`
1564
+ if (!paramsExpression) {
1565
+ return
1566
+ }
1764
1567
 
1765
- // Create fetchData attribute with proper fetch chain wrapped in useCallback
1766
- const fetchDataValue = types.callExpression(types.identifier('useCallback'), [
1767
- types.arrowFunctionExpression(
1768
- [types.identifier('params')],
1769
- types.callExpression(
1770
- types.memberExpression(
1771
- types.callExpression(
1772
- types.memberExpression(
1773
- types.callExpression(types.identifier('fetch'), [
1774
- types.templateLiteral(
1775
- [
1776
- types.templateElement({
1777
- raw: `/api/${fileName}?`,
1778
- cooked: `/api/${fileName}?`,
1779
- }),
1780
- types.templateElement({ raw: '', cooked: '' }),
1781
- ],
1782
- [
1783
- types.newExpression(types.identifier('URLSearchParams'), [
1784
- types.identifier('params'),
1785
- ]),
1786
- ]
1568
+ // Wrap params in useMemo with empty dependencies array
1569
+ // This ensures the object reference stays stable across re-renders
1570
+ const memoizedParams = types.callExpression(types.identifier('useMemo'), [
1571
+ types.arrowFunctionExpression([], paramsExpression),
1572
+ types.arrayExpression([]),
1573
+ ])
1574
+
1575
+ // Replace the params attribute
1576
+ attrs[paramsAttrIndex] = types.jsxAttribute(
1577
+ types.jsxIdentifier('params'),
1578
+ types.jsxExpressionContainer(memoizedParams)
1579
+ )
1580
+ }
1581
+
1582
+ function createFetchDataAttribute(fileName: string): types.JSXAttribute {
1583
+ return types.jsxAttribute(
1584
+ types.jsxIdentifier('fetchData'),
1585
+ types.jsxExpressionContainer(
1586
+ types.callExpression(types.identifier('useCallback'), [
1587
+ types.arrowFunctionExpression(
1588
+ [types.identifier('params')],
1589
+ types.callExpression(
1590
+ types.memberExpression(
1591
+ types.callExpression(
1592
+ types.memberExpression(
1593
+ types.callExpression(types.identifier('fetch'), [
1594
+ types.templateLiteral(
1595
+ [
1596
+ types.templateElement({
1597
+ raw: `/api/${fileName}?`,
1598
+ cooked: `/api/${fileName}?`,
1599
+ }),
1600
+ types.templateElement({ raw: '', cooked: '' }),
1601
+ ],
1602
+ [
1603
+ types.newExpression(types.identifier('URLSearchParams'), [
1604
+ types.identifier('params'),
1605
+ ]),
1606
+ ]
1607
+ ),
1608
+ types.objectExpression([
1609
+ types.objectProperty(
1610
+ types.identifier('headers'),
1611
+ types.objectExpression([
1612
+ types.objectProperty(
1613
+ types.stringLiteral('Content-Type'),
1614
+ types.stringLiteral('application/json')
1615
+ ),
1616
+ ])
1787
1617
  ),
1788
- types.objectExpression([
1789
- types.objectProperty(
1790
- types.identifier('headers'),
1791
- types.objectExpression([
1792
- types.objectProperty(
1793
- types.stringLiteral('Content-Type'),
1794
- types.stringLiteral('application/json')
1795
- ),
1796
- ])
1797
- ),
1798
- ]),
1799
1618
  ]),
1800
- types.identifier('then')
1801
- ),
1802
- [
1803
- types.arrowFunctionExpression(
1804
- [types.identifier('res')],
1805
- types.callExpression(
1806
- types.memberExpression(types.identifier('res'), types.identifier('json')),
1807
- []
1808
- )
1809
- ),
1810
- ]
1619
+ ]),
1620
+ types.identifier('then')
1811
1621
  ),
1812
- types.identifier('then')
1622
+ [
1623
+ types.arrowFunctionExpression(
1624
+ [types.identifier('res')],
1625
+ types.callExpression(
1626
+ types.memberExpression(types.identifier('res'), types.identifier('json')),
1627
+ []
1628
+ )
1629
+ ),
1630
+ ]
1813
1631
  ),
1814
- [
1815
- types.arrowFunctionExpression(
1816
- [types.identifier('response')],
1817
- types.optionalMemberExpression(
1818
- types.identifier('response'),
1819
- types.identifier('data'),
1820
- false,
1821
- true
1822
- )
1823
- ),
1824
- ]
1825
- )
1826
- ),
1827
- types.arrayExpression([]),
1828
- ])
1632
+ types.identifier('then')
1633
+ ),
1634
+ [
1635
+ types.arrowFunctionExpression(
1636
+ [types.identifier('response')],
1637
+ types.optionalMemberExpression(
1638
+ types.identifier('response'),
1639
+ types.identifier('data'),
1640
+ false,
1641
+ true
1642
+ )
1643
+ ),
1644
+ ]
1645
+ )
1646
+ ),
1647
+ types.arrayExpression([]),
1648
+ ])
1649
+ )
1650
+ )
1651
+ }
1829
1652
 
1830
- const newFetchDataAttr = types.jsxAttribute(
1831
- types.jsxIdentifier('fetchData'),
1832
- types.jsxExpressionContainer(fetchDataValue)
1833
- )
1653
+ function wireSearchInput(inputNode: any, vars: ReturnType<typeof getStateVarsForUsage>): void {
1654
+ // Remove existing onChange and value
1655
+ inputNode.openingElement.attributes = inputNode.openingElement.attributes.filter(
1656
+ (attr: any) => !['onChange', 'value'].includes(attr.name?.name)
1657
+ )
1834
1658
 
1835
- // Remove existing fetchData attribute if present
1836
- const existingFetchDataIndex = dataProviderJSX.openingElement.attributes.findIndex(
1837
- (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'fetchData'
1659
+ // Add onChange
1660
+ inputNode.openingElement.attributes.push(
1661
+ types.jsxAttribute(
1662
+ types.jsxIdentifier('onChange'),
1663
+ types.jsxExpressionContainer(
1664
+ types.arrowFunctionExpression(
1665
+ [types.identifier('e')],
1666
+ types.callExpression(types.identifier(vars.setSearchQueryVar), [
1667
+ types.memberExpression(
1668
+ types.memberExpression(types.identifier('e'), types.identifier('target')),
1669
+ types.identifier('value')
1670
+ ),
1671
+ ])
1838
1672
  )
1673
+ )
1674
+ )
1675
+ )
1839
1676
 
1840
- if (existingFetchDataIndex !== -1) {
1841
- dataProviderJSX.openingElement.attributes[existingFetchDataIndex] = newFetchDataAttr
1842
- } else {
1843
- dataProviderJSX.openingElement.attributes.push(newFetchDataAttr)
1844
- }
1845
- }
1846
- }
1847
- }
1677
+ // Add value
1678
+ inputNode.openingElement.attributes.push(
1679
+ types.jsxAttribute(
1680
+ types.jsxIdentifier('value'),
1681
+ types.jsxExpressionContainer(types.identifier(vars.searchQueryVar))
1682
+ )
1683
+ )
1848
1684
  }
1849
1685
 
1850
- function findChildWithClass(node: any, classSubstring: string): string | null {
1851
- if (!node) {
1852
- return null
1853
- }
1854
-
1855
- if (node.type === 'JSXElement') {
1856
- const className = getClassName(node.openingElement?.attributes || [])
1857
- if (className && className.includes(classSubstring)) {
1858
- return className
1686
+ function wirePaginationButtons(
1687
+ paginationNode: any,
1688
+ usage: DataSourceUsage,
1689
+ vars: ReturnType<typeof getStateVarsForUsage>
1690
+ ): void {
1691
+ // Find prev and next buttons
1692
+ const findButton = (node: any, direction: 'previous' | 'next'): any => {
1693
+ if (!node) {
1694
+ return null
1859
1695
  }
1860
- }
1861
1696
 
1862
- if (node.children) {
1863
- for (const child of node.children) {
1864
- const found = findChildWithClass(child, classSubstring)
1865
- if (found) {
1866
- return found
1697
+ if (node.type === 'JSXElement') {
1698
+ const classAttr = node.openingElement?.attributes?.find(
1699
+ (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'className'
1700
+ )
1701
+ const className = classAttr?.value?.value || classAttr?.value?.expression?.value || ''
1702
+ if (className.includes(direction)) {
1703
+ return node
1867
1704
  }
1868
1705
  }
1869
- }
1870
1706
 
1871
- if (typeof node === 'object') {
1872
- for (const value of Object.values(node)) {
1873
- if (Array.isArray(value)) {
1874
- for (const item of value) {
1875
- const found = findChildWithClass(item, classSubstring)
1876
- if (found) {
1877
- return found
1878
- }
1879
- }
1880
- } else if (typeof value === 'object') {
1881
- const found = findChildWithClass(value, classSubstring)
1707
+ if (node.children && Array.isArray(node.children)) {
1708
+ for (const c of node.children) {
1709
+ const found = findButton(c, direction)
1882
1710
  if (found) {
1883
1711
  return found
1884
1712
  }
1885
1713
  }
1886
1714
  }
1715
+ return null
1887
1716
  }
1888
1717
 
1889
- return null
1890
- }
1718
+ const prevButton = findButton(paginationNode, 'previous')
1719
+ const nextButton = findButton(paginationNode, 'next')
1891
1720
 
1892
- function modifyPaginationButtons(
1893
- blockStatement: types.BlockStatement,
1894
- detectedPaginations: DetectedPagination[],
1895
- paginationInfos: ArrayMapperPaginationInfo[]
1896
- ): void {
1897
- const modifiedButtons = new Set<any>()
1721
+ const isCombinedState = usage.category === 'paginated+search'
1898
1722
 
1899
- const modifyNode = (node: any): void => {
1900
- if (!node) {
1901
- return
1723
+ if (prevButton) {
1724
+ // Change to button element
1725
+ prevButton.openingElement.name.name = 'button'
1726
+ if (prevButton.closingElement) {
1727
+ prevButton.closingElement.name.name = 'button'
1902
1728
  }
1903
1729
 
1904
- if (node.type === 'JSXElement') {
1905
- const openingElement = node.openingElement
1906
- if (openingElement && openingElement.name && openingElement.name.type === 'JSXIdentifier') {
1907
- const className = getClassName(openingElement.attributes)
1908
-
1909
- if (className && !modifiedButtons.has(node)) {
1910
- for (let index = 0; index < detectedPaginations.length; index++) {
1911
- const detected = detectedPaginations[index]
1912
- const info = paginationInfos[index]
1913
-
1914
- if (!info) {
1915
- continue
1916
- }
1917
-
1918
- if (className === detected.prevButtonClass) {
1919
- convertToButton(node, info, 'prev')
1920
- modifiedButtons.add(node)
1921
- break
1922
- } else if (className === detected.nextButtonClass) {
1923
- convertToButton(node, info, 'next')
1924
- modifiedButtons.add(node)
1925
- break
1926
- }
1927
- }
1928
- }
1929
- }
1730
+ // Add type="button"
1731
+ const hasType = prevButton.openingElement.attributes.some((a: any) => a.name?.name === 'type')
1732
+ if (!hasType) {
1733
+ prevButton.openingElement.attributes.push(
1734
+ types.jsxAttribute(types.jsxIdentifier('type'), types.stringLiteral('button'))
1735
+ )
1930
1736
  }
1931
1737
 
1932
- if (typeof node === 'object') {
1933
- Object.values(node).forEach((value) => {
1934
- if (Array.isArray(value)) {
1935
- value.forEach((item) => modifyNode(item))
1936
- } else if (typeof value === 'object') {
1937
- modifyNode(value)
1938
- }
1939
- })
1940
- }
1941
- }
1738
+ // Add onClick
1739
+ prevButton.openingElement.attributes = prevButton.openingElement.attributes.filter(
1740
+ (a: any) => a.name?.name !== 'onClick'
1741
+ )
1942
1742
 
1943
- modifyNode(blockStatement)
1944
- }
1743
+ if (isCombinedState) {
1744
+ prevButton.openingElement.attributes.push(
1745
+ types.jsxAttribute(
1746
+ types.jsxIdentifier('onClick'),
1747
+ types.jsxExpressionContainer(
1748
+ types.arrowFunctionExpression(
1749
+ [],
1750
+ types.callExpression(types.identifier(vars.setCombinedStateVar), [
1751
+ types.arrowFunctionExpression(
1752
+ [types.identifier('state')],
1753
+ types.objectExpression([
1754
+ types.spreadElement(types.identifier('state')),
1755
+ types.objectProperty(
1756
+ types.identifier('page'),
1757
+ types.callExpression(
1758
+ types.memberExpression(types.identifier('Math'), types.identifier('max')),
1759
+ [
1760
+ types.numericLiteral(1),
1761
+ types.binaryExpression(
1762
+ '-',
1763
+ types.memberExpression(
1764
+ types.identifier('state'),
1765
+ types.identifier('page')
1766
+ ),
1767
+ types.numericLiteral(1)
1768
+ ),
1769
+ ]
1770
+ )
1771
+ ),
1772
+ ])
1773
+ ),
1774
+ ])
1775
+ )
1776
+ )
1777
+ )
1778
+ )
1945
1779
 
1946
- function modifySearchInputs(
1947
- blockStatement: types.BlockStatement,
1948
- detectedPaginations: DetectedPagination[],
1949
- paginationInfos: ArrayMapperPaginationInfo[]
1950
- ): void {
1951
- const modifiedInputs = new Set<any>()
1780
+ // Add disabled
1781
+ prevButton.openingElement.attributes = prevButton.openingElement.attributes.filter(
1782
+ (a: any) => a.name?.name !== 'disabled'
1783
+ )
1784
+ prevButton.openingElement.attributes.push(
1785
+ types.jsxAttribute(
1786
+ types.jsxIdentifier('disabled'),
1787
+ types.jsxExpressionContainer(
1788
+ types.binaryExpression(
1789
+ '<=',
1790
+ types.memberExpression(
1791
+ types.identifier(vars.combinedStateVar),
1792
+ types.identifier('page')
1793
+ ),
1794
+ types.numericLiteral(1)
1795
+ )
1796
+ )
1797
+ )
1798
+ )
1799
+ } else {
1800
+ prevButton.openingElement.attributes.push(
1801
+ types.jsxAttribute(
1802
+ types.jsxIdentifier('onClick'),
1803
+ types.jsxExpressionContainer(
1804
+ types.arrowFunctionExpression(
1805
+ [],
1806
+ types.callExpression(types.identifier(vars.setPageStateVar), [
1807
+ types.arrowFunctionExpression(
1808
+ [types.identifier('page')],
1809
+ types.callExpression(
1810
+ types.memberExpression(types.identifier('Math'), types.identifier('max')),
1811
+ [
1812
+ types.numericLiteral(1),
1813
+ types.binaryExpression(
1814
+ '-',
1815
+ types.identifier('page'),
1816
+ types.numericLiteral(1)
1817
+ ),
1818
+ ]
1819
+ )
1820
+ ),
1821
+ ])
1822
+ )
1823
+ )
1824
+ )
1825
+ )
1952
1826
 
1953
- const modifyNode = (node: any): void => {
1954
- if (!node) {
1955
- return
1827
+ prevButton.openingElement.attributes = prevButton.openingElement.attributes.filter(
1828
+ (a: any) => a.name?.name !== 'disabled'
1829
+ )
1830
+ prevButton.openingElement.attributes.push(
1831
+ types.jsxAttribute(
1832
+ types.jsxIdentifier('disabled'),
1833
+ types.jsxExpressionContainer(
1834
+ types.binaryExpression(
1835
+ '<=',
1836
+ types.identifier(vars.pageStateVar),
1837
+ types.numericLiteral(1)
1838
+ )
1839
+ )
1840
+ )
1841
+ )
1956
1842
  }
1843
+ }
1957
1844
 
1958
- if (node.type === 'JSXElement') {
1959
- const openingElement = node.openingElement
1960
- if (openingElement && openingElement.name && openingElement.name.type === 'JSXIdentifier') {
1961
- const className = getClassName(openingElement.attributes)
1962
-
1963
- if (className && !modifiedInputs.has(node)) {
1964
- for (let index = 0; index < detectedPaginations.length; index++) {
1965
- const detected = detectedPaginations[index]
1966
- const info = paginationInfos[index]
1967
-
1968
- if (!info || !info.searchEnabled) {
1969
- continue
1970
- }
1971
-
1972
- if (className === detected.searchInputClass) {
1973
- addSearchInputHandlers(node, info)
1974
- modifiedInputs.add(node)
1975
- break
1976
- }
1977
- }
1978
- }
1979
- }
1845
+ if (nextButton) {
1846
+ nextButton.openingElement.name.name = 'button'
1847
+ if (nextButton.closingElement) {
1848
+ nextButton.closingElement.name.name = 'button'
1980
1849
  }
1981
1850
 
1982
- if (typeof node === 'object') {
1983
- Object.values(node).forEach((value) => {
1984
- if (Array.isArray(value)) {
1985
- value.forEach((item) => modifyNode(item))
1986
- } else if (typeof value === 'object') {
1987
- modifyNode(value)
1988
- }
1989
- })
1851
+ const hasType = nextButton.openingElement.attributes.some((a: any) => a.name?.name === 'type')
1852
+ if (!hasType) {
1853
+ nextButton.openingElement.attributes.push(
1854
+ types.jsxAttribute(types.jsxIdentifier('type'), types.stringLiteral('button'))
1855
+ )
1990
1856
  }
1991
- }
1992
-
1993
- modifyNode(blockStatement)
1994
- }
1995
-
1996
- function addSearchInputHandlers(jsxElement: any, info: ArrayMapperPaginationInfo): void {
1997
- if (!info.searchQueryVar || !info.setSearchQueryVar) {
1998
- return
1999
- }
2000
-
2001
- const openingElement = jsxElement.openingElement
2002
-
2003
- removeAttribute(openingElement.attributes, 'onChange')
2004
- removeAttribute(openingElement.attributes, 'value')
2005
-
2006
- const onChangeHandler = types.arrowFunctionExpression(
2007
- [types.identifier('e')],
2008
- types.callExpression(types.identifier(info.setSearchQueryVar!), [
2009
- types.memberExpression(
2010
- types.memberExpression(types.identifier('e'), types.identifier('target')),
2011
- types.identifier('value')
2012
- ),
2013
- ])
2014
- )
2015
-
2016
- openingElement.attributes.push(
2017
- types.jsxAttribute(
2018
- types.jsxIdentifier('onChange'),
2019
- types.jsxExpressionContainer(onChangeHandler)
2020
- )
2021
- )
2022
1857
 
2023
- openingElement.attributes.push(
2024
- types.jsxAttribute(
2025
- types.jsxIdentifier('value'),
2026
- types.jsxExpressionContainer(types.identifier(info.searchQueryVar!))
1858
+ nextButton.openingElement.attributes = nextButton.openingElement.attributes.filter(
1859
+ (a: any) => a.name?.name !== 'onClick'
2027
1860
  )
2028
- )
2029
- }
2030
-
2031
- function convertToButton(
2032
- jsxElement: any,
2033
- info: ArrayMapperPaginationInfo,
2034
- buttonType: 'prev' | 'next'
2035
- ): void {
2036
- const openingElement = jsxElement.openingElement
2037
-
2038
- if (openingElement.name.type === 'JSXIdentifier') {
2039
- openingElement.name.name = 'button'
2040
- }
2041
- if (jsxElement.closingElement && jsxElement.closingElement.name.type === 'JSXIdentifier') {
2042
- jsxElement.closingElement.name.name = 'button'
2043
- }
2044
1861
 
2045
- removeAttribute(openingElement.attributes, 'onClick')
2046
- removeAttribute(openingElement.attributes, 'disabled')
2047
- removeAttribute(openingElement.attributes, 'type')
2048
-
2049
- const combinedStateVar = (info as any).combinedStateVar
2050
- const setCombinedStateVar = (info as any).setCombinedStateVar
2051
-
2052
- let onClickHandler: any
2053
- if (info.searchEnabled && combinedStateVar && setCombinedStateVar) {
2054
- // Use combined state: update only the page property
2055
- onClickHandler =
2056
- buttonType === 'prev'
2057
- ? types.arrowFunctionExpression(
2058
- [],
2059
- types.callExpression(types.identifier(setCombinedStateVar), [
2060
- types.arrowFunctionExpression(
2061
- [types.identifier('state')],
2062
- types.objectExpression([
2063
- types.spreadElement(types.identifier('state')),
2064
- types.objectProperty(
2065
- types.identifier('page'),
2066
- types.callExpression(
2067
- types.memberExpression(types.identifier('Math'), types.identifier('max')),
2068
- [
2069
- types.numericLiteral(1),
2070
- types.binaryExpression(
2071
- '-',
2072
- types.memberExpression(
2073
- types.identifier('state'),
2074
- types.identifier('page')
2075
- ),
2076
- types.numericLiteral(1)
2077
- ),
2078
- ]
2079
- )
2080
- ),
2081
- ])
2082
- ),
2083
- ])
2084
- )
2085
- : types.arrowFunctionExpression(
2086
- [],
2087
- types.callExpression(types.identifier(setCombinedStateVar), [
2088
- types.arrowFunctionExpression(
2089
- [types.identifier('state')],
2090
- types.objectExpression([
2091
- types.spreadElement(types.identifier('state')),
2092
- types.objectProperty(
2093
- types.identifier('page'),
2094
- types.binaryExpression(
2095
- '+',
2096
- types.memberExpression(types.identifier('state'), types.identifier('page')),
2097
- types.numericLiteral(1)
2098
- )
2099
- ),
2100
- ])
2101
- ),
2102
- ])
1862
+ if (isCombinedState) {
1863
+ nextButton.openingElement.attributes.push(
1864
+ types.jsxAttribute(
1865
+ types.jsxIdentifier('onClick'),
1866
+ types.jsxExpressionContainer(
1867
+ types.arrowFunctionExpression(
1868
+ [],
1869
+ types.callExpression(types.identifier(vars.setCombinedStateVar), [
1870
+ types.arrowFunctionExpression(
1871
+ [types.identifier('state')],
1872
+ types.objectExpression([
1873
+ types.spreadElement(types.identifier('state')),
1874
+ types.objectProperty(
1875
+ types.identifier('page'),
1876
+ types.binaryExpression(
1877
+ '+',
1878
+ types.memberExpression(types.identifier('state'), types.identifier('page')),
1879
+ types.numericLiteral(1)
1880
+ )
1881
+ ),
1882
+ ])
1883
+ ),
1884
+ ])
1885
+ )
2103
1886
  )
2104
- } else {
2105
- // Regular pagination (no search)
2106
- onClickHandler =
2107
- buttonType === 'prev'
2108
- ? types.arrowFunctionExpression(
2109
- [],
2110
- types.callExpression(types.identifier(info.setPageStateVar), [
2111
- types.arrowFunctionExpression(
2112
- [types.identifier('p')],
2113
- types.callExpression(
2114
- types.memberExpression(types.identifier('Math'), types.identifier('max')),
2115
- [
2116
- types.numericLiteral(1),
2117
- types.binaryExpression('-', types.identifier('p'), types.numericLiteral(1)),
2118
- ]
2119
- )
1887
+ )
1888
+ )
1889
+
1890
+ nextButton.openingElement.attributes = nextButton.openingElement.attributes.filter(
1891
+ (a: any) => a.name?.name !== 'disabled'
1892
+ )
1893
+ nextButton.openingElement.attributes.push(
1894
+ types.jsxAttribute(
1895
+ types.jsxIdentifier('disabled'),
1896
+ types.jsxExpressionContainer(
1897
+ types.binaryExpression(
1898
+ '>=',
1899
+ types.memberExpression(
1900
+ types.identifier(vars.combinedStateVar),
1901
+ types.identifier('page')
2120
1902
  ),
2121
- ])
1903
+ types.identifier(vars.maxPagesStateVar)
1904
+ )
2122
1905
  )
2123
- : types.arrowFunctionExpression(
2124
- [],
2125
- types.callExpression(types.identifier(info.setPageStateVar), [
2126
- types.arrowFunctionExpression(
2127
- [types.identifier('p')],
2128
- types.binaryExpression('+', types.identifier('p'), types.numericLiteral(1))
2129
- ),
2130
- ])
1906
+ )
1907
+ )
1908
+ } else {
1909
+ nextButton.openingElement.attributes.push(
1910
+ types.jsxAttribute(
1911
+ types.jsxIdentifier('onClick'),
1912
+ types.jsxExpressionContainer(
1913
+ types.arrowFunctionExpression(
1914
+ [],
1915
+ types.callExpression(types.identifier(vars.setPageStateVar), [
1916
+ types.arrowFunctionExpression(
1917
+ [types.identifier('page')],
1918
+ types.binaryExpression('+', types.identifier('page'), types.numericLiteral(1))
1919
+ ),
1920
+ ])
1921
+ )
2131
1922
  )
2132
- }
2133
-
2134
- openingElement.attributes.push(
2135
- types.jsxAttribute(types.jsxIdentifier('onClick'), types.jsxExpressionContainer(onClickHandler))
2136
- )
2137
- openingElement.attributes.push(
2138
- types.jsxAttribute(types.jsxIdentifier('type'), types.stringLiteral('button'))
2139
- )
2140
-
2141
- // Add disabled attribute with simple page number checks
2142
- const maxPagesStateVar = (info as any).maxPagesStateVar
2143
- let disabledExpr: any
1923
+ )
1924
+ )
2144
1925
 
2145
- if (info.searchEnabled && combinedStateVar) {
2146
- disabledExpr =
2147
- buttonType === 'prev'
2148
- ? types.binaryExpression(
2149
- '<=',
2150
- types.memberExpression(types.identifier(combinedStateVar), types.identifier('page')),
2151
- types.numericLiteral(1)
2152
- )
2153
- : types.binaryExpression(
2154
- '>=',
2155
- types.memberExpression(types.identifier(combinedStateVar), types.identifier('page')),
2156
- types.identifier(maxPagesStateVar)
2157
- )
2158
- } else {
2159
- disabledExpr =
2160
- buttonType === 'prev'
2161
- ? types.binaryExpression('<=', types.identifier(info.pageStateVar), types.numericLiteral(1))
2162
- : types.binaryExpression(
2163
- '>=',
2164
- types.identifier(info.pageStateVar),
2165
- types.identifier(maxPagesStateVar)
1926
+ nextButton.openingElement.attributes = nextButton.openingElement.attributes.filter(
1927
+ (a: any) => a.name?.name !== 'disabled'
1928
+ )
1929
+ nextButton.openingElement.attributes.push(
1930
+ types.jsxAttribute(
1931
+ types.jsxIdentifier('disabled'),
1932
+ types.jsxExpressionContainer(
1933
+ types.binaryExpression(
1934
+ '>=',
1935
+ types.identifier(vars.pageStateVar),
1936
+ types.identifier(vars.maxPagesStateVar)
1937
+ )
2166
1938
  )
1939
+ )
1940
+ )
1941
+ }
2167
1942
  }
2168
-
2169
- openingElement.attributes.push(
2170
- types.jsxAttribute(types.jsxIdentifier('disabled'), types.jsxExpressionContainer(disabledExpr))
2171
- )
2172
1943
  }
2173
1944
 
2174
- function getClassName(attributes: any[]): string | null {
2175
- const classNameAttr = attributes.find(
2176
- (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'className'
1945
+ function ensureAPIRouteExists(extractedResources: any, usage: DataSourceUsage): void {
1946
+ // Generate file name for the API route
1947
+ const fileName = generateSafeFileName(
1948
+ usage.resourceDefinition.dataSourceType,
1949
+ usage.resourceDefinition.tableName,
1950
+ usage.resourceDefinition.dataSourceId
2177
1951
  )
2178
- if (classNameAttr && classNameAttr.value && classNameAttr.value.type === 'StringLiteral') {
2179
- return classNameAttr.value.value
2180
- }
2181
- return null
2182
- }
2183
1952
 
2184
- function removeAttribute(attrs: any[], attributeName: string): void {
2185
- const index = attrs.findIndex(
2186
- (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === attributeName
2187
- )
2188
- if (index !== -1) {
2189
- attrs.splice(index, 1)
2190
- }
2191
- }
1953
+ // Check if the utils data source file exists - if so, create API routes that re-export from it
1954
+ if (extractedResources[`utils/${fileName}`]) {
1955
+ // Create main data API route if not exists
1956
+ if (!extractedResources[`api/${fileName}`]) {
1957
+ const apiRouteCode = `import dataSourceModule from '../../utils/data-sources/${fileName}'
1958
+
1959
+ export default dataSourceModule.handler
1960
+ `
1961
+ extractedResources[`api/${fileName}`] = {
1962
+ fileName,
1963
+ fileType: FileType.JS,
1964
+ path: ['pages', 'api'],
1965
+ content: apiRouteCode,
1966
+ }
1967
+ }
2192
1968
 
2193
- function modifyGetStaticPropsForPagination(
2194
- chunks: any[],
2195
- paginationInfos: ArrayMapperPaginationInfo[]
2196
- ): void {
2197
- const getStaticPropsChunk = chunks.find((chunk) => chunk.name === 'getStaticProps')
2198
- if (!getStaticPropsChunk || getStaticPropsChunk.type !== 'ast') {
2199
- return
1969
+ // Create count API route if not exists (needed for paginated+search cases)
1970
+ const countFileName = `${fileName}-count`
1971
+ if (!extractedResources[`api/${countFileName}`]) {
1972
+ const countApiRouteCode = `import dataSourceModule from '../../utils/data-sources/${fileName}'
1973
+
1974
+ export default dataSourceModule.getCount
1975
+ `
1976
+ extractedResources[`api/${countFileName}`] = {
1977
+ fileName: countFileName,
1978
+ fileType: FileType.JS,
1979
+ path: ['pages', 'api'],
1980
+ content: countApiRouteCode,
1981
+ }
1982
+ }
2200
1983
  }
1984
+ }
2201
1985
 
2202
- const exportDeclaration = getStaticPropsChunk.content as types.ExportNamedDeclaration
2203
- if (!exportDeclaration || exportDeclaration.type !== 'ExportNamedDeclaration') {
1986
+ function updateGetStaticProps(chunks: any[], registry: StateRegistry): void {
1987
+ const getStaticPropsChunk = chunks.find((c) => c.name === 'getStaticProps')
1988
+ if (!getStaticPropsChunk || getStaticPropsChunk.type !== ChunkType.AST) {
2204
1989
  return
2205
1990
  }
2206
1991
 
2207
- const functionDeclaration = exportDeclaration.declaration as types.FunctionDeclaration
2208
- if (!functionDeclaration || functionDeclaration.type !== 'FunctionDeclaration') {
1992
+ const content = getStaticPropsChunk.content as types.ExportNamedDeclaration
1993
+ if (!content.declaration || content.declaration.type !== 'FunctionDeclaration') {
2209
1994
  return
2210
1995
  }
2211
1996
 
2212
- // Find Promise.all and add NEW fetchData calls for each paginated DataProvider
2213
- const functionBody = functionDeclaration.body.body
2214
- const tryBlock = functionBody.find((stmt: any) => stmt.type === 'TryStatement') as
1997
+ const funcBody = content.declaration.body
1998
+ const tryStmt = funcBody.body.find((s: any) => s.type === 'TryStatement') as
2215
1999
  | types.TryStatement
2216
2000
  | undefined
2217
-
2218
- if (!tryBlock) {
2001
+ if (!tryStmt) {
2219
2002
  return
2220
2003
  }
2221
2004
 
2222
- const tryBody = tryBlock.block.body
2005
+ const tryBlock = tryStmt.block
2223
2006
 
2224
- // Find the Promise.all statement
2225
- const promiseAllStmt = tryBody.find(
2226
- (stmt: any) =>
2227
- stmt.type === 'VariableDeclaration' &&
2228
- stmt.declarations?.[0]?.init?.type === 'AwaitExpression' &&
2229
- stmt.declarations?.[0]?.init?.argument?.type === 'CallExpression' &&
2230
- stmt.declarations?.[0]?.init?.argument?.callee?.type === 'MemberExpression' &&
2231
- stmt.declarations?.[0]?.init?.argument?.callee?.property?.name === 'all'
2007
+ // Find existing Promise.all
2008
+ const promiseAllDecl = tryBlock.body.find(
2009
+ (s: any) =>
2010
+ s.type === 'VariableDeclaration' &&
2011
+ s.declarations?.[0]?.init?.type === 'AwaitExpression' &&
2012
+ s.declarations?.[0]?.init?.argument?.type === 'CallExpression' &&
2013
+ s.declarations?.[0]?.init?.argument?.callee?.type === 'MemberExpression' &&
2014
+ s.declarations?.[0]?.init?.argument?.callee?.object?.name === 'Promise'
2232
2015
  ) as types.VariableDeclaration | undefined
2233
2016
 
2234
- if (!promiseAllStmt) {
2017
+ if (!promiseAllDecl) {
2235
2018
  return
2236
2019
  }
2237
2020
 
2238
- const awaitExpr = promiseAllStmt.declarations[0].init as types.AwaitExpression
2021
+ const declarator = promiseAllDecl.declarations[0] as types.VariableDeclarator
2022
+ const awaitExpr = declarator.init as types.AwaitExpression
2239
2023
  const promiseAllCall = awaitExpr.argument as types.CallExpression
2240
- const promiseArray = promiseAllCall.arguments[0] as types.ArrayExpression
2241
- const destructuringPattern = promiseAllStmt.declarations[0].id as types.ArrayPattern
2242
-
2243
- // Map import names to data source identifiers from existing fetchData calls
2244
- // Also track which indices to remove (non-paginated calls that will be replaced)
2245
- const importToDataSource = new Map<string, string>()
2246
- const indicesToRemove: number[] = []
2247
-
2248
- promiseArray.elements.forEach((element: any, index: number) => {
2249
- if (element && element.type === 'CallExpression') {
2250
- let fetchCallExpr = element
2251
-
2252
- // If wrapped in .catch(), unwrap it
2253
- if (
2254
- element.callee?.type === 'MemberExpression' &&
2255
- element.callee?.property?.name === 'catch' &&
2256
- element.callee?.object?.type === 'CallExpression'
2257
- ) {
2258
- fetchCallExpr = element.callee.object
2259
- }
2260
-
2261
- // Now find the .fetchData() call
2262
- if (
2263
- fetchCallExpr.callee?.type === 'MemberExpression' &&
2264
- fetchCallExpr.callee?.property?.name === 'fetchData' &&
2265
- fetchCallExpr.callee?.object?.type === 'Identifier'
2266
- ) {
2267
- const importName = fetchCallExpr.callee.object.name
2268
- const dataSourceVar = (destructuringPattern.elements[index] as types.Identifier).name
2269
-
2270
- // Check if this fetchData call has page/perPage params
2271
- const params = fetchCallExpr.arguments[0]
2272
- const hasPageParam =
2273
- params &&
2274
- params.type === 'ObjectExpression' &&
2275
- params.properties.some(
2276
- (prop: any) =>
2277
- prop.type === 'ObjectProperty' &&
2278
- (prop.key.name === 'page' || prop.key.name === 'perPage')
2279
- )
2024
+ const fetchesArray = promiseAllCall.arguments[0] as types.ArrayExpression
2280
2025
 
2281
- // If this is a data source that will be paginated but this call has NO pagination params,
2282
- // mark it for removal
2283
- if (
2284
- !hasPageParam &&
2285
- paginationInfos.some((info) => info.dataSourceIdentifier === dataSourceVar)
2286
- ) {
2287
- indicesToRemove.push(index)
2288
- }
2026
+ // Find return statement
2027
+ const returnStmt = tryBlock.body.find((s: any) => s.type === 'ReturnStatement') as
2028
+ | types.ReturnStatement
2029
+ | undefined
2030
+ if (!returnStmt || returnStmt.argument?.type !== 'ObjectExpression') {
2031
+ return
2032
+ }
2289
2033
 
2290
- importToDataSource.set(importName, dataSourceVar)
2291
- }
2292
- }
2293
- })
2034
+ const returnObj = returnStmt.argument as types.ObjectExpression
2035
+ const propsProperty = returnObj.properties.find(
2036
+ (p: any) => p.type === 'ObjectProperty' && p.key.type === 'Identifier' && p.key.name === 'props'
2037
+ ) as types.ObjectProperty | undefined
2294
2038
 
2295
- // Remove non-paginated fetchData calls in reverse order to preserve indices
2296
- indicesToRemove.reverse().forEach((index) => {
2297
- // Get the prop name BEFORE removing it
2298
- const propToRemove = (destructuringPattern.elements[index] as types.Identifier)?.name
2039
+ if (!propsProperty || propsProperty.value.type !== 'ObjectExpression') {
2040
+ return
2041
+ }
2299
2042
 
2300
- promiseArray.elements.splice(index, 1)
2301
- destructuringPattern.elements.splice(index, 1)
2043
+ const propsObj = propsProperty.value as types.ObjectExpression
2044
+ const arrayPattern = declarator.id as types.ArrayPattern
2302
2045
 
2303
- // Also remove from props in return statement
2304
- if (propToRemove) {
2305
- const foundReturnStmt = tryBody.find((stmt: any) => stmt.type === 'ReturnStatement') as
2306
- | types.ReturnStatement
2307
- | undefined
2046
+ // Track unique data sources for count fetching
2047
+ const dataSourcesNeedingCount = new Set<string>()
2308
2048
 
2309
- if (foundReturnStmt && foundReturnStmt.argument?.type === 'ObjectExpression') {
2310
- const propsProperty = (foundReturnStmt.argument as types.ObjectExpression).properties.find(
2311
- (prop: any) =>
2312
- prop.type === 'ObjectProperty' && (prop.key as types.Identifier).name === 'props'
2313
- ) as types.ObjectProperty | undefined
2049
+ registry.usages.forEach((usage) => {
2050
+ const vars = getStateVarsForUsage(usage)
2051
+ const fileName = generateSafeFileName(
2052
+ usage.resourceDefinition.dataSourceType,
2053
+ usage.resourceDefinition.tableName,
2054
+ usage.resourceDefinition.dataSourceId
2055
+ )
2056
+ // Use consistent import name generation (matches extractDataSourceIntoGetStaticProps)
2057
+ const fetcherImportName = StringUtils.dashCaseToCamelCase(fileName)
2314
2058
 
2315
- if (propsProperty && propsProperty.value.type === 'ObjectExpression') {
2316
- const propsObject = propsProperty.value as types.ObjectExpression
2059
+ // Add fetch call
2060
+ const fetchParams: types.ObjectProperty[] = []
2317
2061
 
2318
- const propIndex = propsObject.properties.findIndex(
2319
- (prop: any) =>
2320
- prop.type === 'ObjectProperty' && (prop.key as types.Identifier).name === propToRemove
2321
- )
2322
- if (propIndex !== -1) {
2323
- propsObject.properties.splice(propIndex, 1)
2324
- }
2325
- }
2326
- }
2062
+ if (usage.paginated) {
2063
+ // For paginated array mappers, add page and perPage
2064
+ fetchParams.push(types.objectProperty(types.identifier('page'), types.numericLiteral(1)))
2065
+ fetchParams.push(
2066
+ types.objectProperty(types.identifier('perPage'), types.numericLiteral(usage.perPage))
2067
+ )
2068
+ } else if (usage.perPage > 0) {
2069
+ // For non-paginated array mappers with a limit, add the limit as perPage
2070
+ // This ensures the initial data fetch respects the limit from the UIDL
2071
+ fetchParams.push(
2072
+ types.objectProperty(types.identifier('perPage'), types.numericLiteral(usage.perPage))
2073
+ )
2327
2074
  }
2328
- })
2329
-
2330
- // Add NEW fetchData calls for each paginated DataProvider
2331
- paginationInfos.forEach((info, index) => {
2332
- // Try exact match first, then case-insensitive match
2333
- let importName = Array.from(importToDataSource.entries()).find(
2334
- ([_, dataSourceVar]) => dataSourceVar === info.dataSourceIdentifier
2335
- )?.[0]
2336
-
2337
- if (!importName) {
2338
- // Try case-insensitive match
2339
- const normalizedIdentifier = info.dataSourceIdentifier.toLowerCase().replace(/[_-]/g, '')
2340
- importName = Array.from(importToDataSource.entries()).find(
2341
- ([_, dataSourceVar]) =>
2342
- dataSourceVar.toLowerCase().replace(/[_-]/g, '') === normalizedIdentifier
2343
- )?.[0]
2344
- }
2345
-
2346
- if (importName) {
2347
- const paginatedVarName = `${info.dataSourceIdentifier}_pg_${index}`
2348
-
2349
- const fetchParams = [
2350
- types.objectProperty(types.identifier('page'), types.numericLiteral(1)),
2351
- types.objectProperty(types.identifier('perPage'), types.numericLiteral(info.perPage)),
2352
- ]
2353
-
2354
- // Add queryColumns if they exist
2355
- if (info.queryColumns && info.queryColumns.length > 0) {
2356
- fetchParams.push(
2357
- types.objectProperty(
2358
- types.identifier('queryColumns'),
2359
- types.arrayExpression(info.queryColumns.map((col) => types.stringLiteral(col)))
2075
+ if (usage.queryColumns.length > 0) {
2076
+ fetchParams.push(
2077
+ types.objectProperty(
2078
+ types.identifier('queryColumns'),
2079
+ types.callExpression(
2080
+ types.memberExpression(types.identifier('JSON'), types.identifier('stringify')),
2081
+ [types.arrayExpression(usage.queryColumns.map((c) => types.stringLiteral(c)))]
2360
2082
  )
2361
2083
  )
2362
- }
2363
-
2364
- // Create new fetchData call with pagination params
2365
- const newFetchDataCall = types.callExpression(
2366
- types.memberExpression(
2367
- types.callExpression(
2368
- types.memberExpression(types.identifier(importName), types.identifier('fetchData')),
2369
- [types.objectExpression(fetchParams)]
2370
- ),
2371
- types.identifier('catch')
2372
- ),
2373
- [
2374
- types.arrowFunctionExpression(
2375
- [types.identifier('error')],
2376
- types.blockStatement([
2377
- types.expressionStatement(
2378
- types.callExpression(
2379
- types.memberExpression(types.identifier('console'), types.identifier('error')),
2380
- [
2381
- types.stringLiteral(`Error fetching ${paginatedVarName}:`),
2382
- types.identifier('error'),
2383
- ]
2384
- )
2385
- ),
2386
- types.returnStatement(types.arrayExpression([])),
2387
- ])
2388
- ),
2389
- ]
2390
2084
  )
2391
-
2392
- promiseArray.elements.push(newFetchDataCall)
2393
- destructuringPattern.elements.push(types.identifier(paginatedVarName))
2394
2085
  }
2395
- })
2396
2086
 
2397
- // Add fetchCount calls for paginated data sources (deduplicated by data source)
2087
+ // Check if this fetch already exists
2088
+ const existingFetchIndex = arrayPattern.elements.findIndex(
2089
+ (el: any) => el?.type === 'Identifier' && el.name === vars.propsPrefix
2090
+ )
2398
2091
 
2399
- // Deduplicate by data source identifier
2400
- const uniqueDataSources = new Set(paginationInfos.map((info) => info.dataSourceIdentifier))
2401
- const addedCountFetches = new Set<string>()
2092
+ if (existingFetchIndex === -1) {
2093
+ arrayPattern.elements.push(types.identifier(vars.propsPrefix))
2402
2094
 
2403
- uniqueDataSources.forEach((dataSourceId) => {
2404
- let importName = Array.from(importToDataSource.entries()).find(
2405
- ([_, dataSourceVar]) => dataSourceVar === dataSourceId
2406
- )?.[0]
2095
+ fetchesArray.elements.push(
2096
+ types.callExpression(
2097
+ types.memberExpression(
2098
+ types.callExpression(
2099
+ types.memberExpression(
2100
+ types.identifier(fetcherImportName),
2101
+ types.identifier('fetchData')
2102
+ ),
2103
+ [types.objectExpression(fetchParams)]
2104
+ ),
2105
+ types.identifier('catch')
2106
+ ),
2107
+ [
2108
+ types.arrowFunctionExpression(
2109
+ [types.identifier('error')],
2110
+ types.blockStatement([
2111
+ types.expressionStatement(
2112
+ types.callExpression(
2113
+ types.memberExpression(types.identifier('console'), types.identifier('error')),
2114
+ [
2115
+ types.stringLiteral(`Error fetching ${vars.propsPrefix}:`),
2116
+ types.identifier('error'),
2117
+ ]
2118
+ )
2119
+ ),
2120
+ types.returnStatement(types.arrayExpression([])),
2121
+ ])
2122
+ ),
2123
+ ]
2124
+ )
2125
+ )
2407
2126
 
2408
- if (!importName) {
2409
- // Try case-insensitive match
2410
- const normalizedIdentifier = dataSourceId.toLowerCase().replace(/[_-]/g, '')
2411
- importName = Array.from(importToDataSource.entries()).find(
2412
- ([_, dataSourceVar]) =>
2413
- dataSourceVar.toLowerCase().replace(/[_-]/g, '') === normalizedIdentifier
2414
- )?.[0]
2127
+ // Add to props
2128
+ propsObj.properties.push(
2129
+ types.objectProperty(types.identifier(vars.propsPrefix), types.identifier(vars.propsPrefix))
2130
+ )
2415
2131
  }
2416
2132
 
2417
- if (importName && !addedCountFetches.has(dataSourceId)) {
2418
- const fetchCountCall = types.callExpression(
2419
- types.memberExpression(types.identifier(importName), types.identifier('fetchCount')),
2420
- []
2133
+ // Track for count fetching
2134
+ if (usage.paginated) {
2135
+ dataSourcesNeedingCount.add(
2136
+ `${usage.resourceDefinition.dataSourceType}:${usage.resourceDefinition.tableName}:${usage.resourceDefinition.dataSourceId}`
2421
2137
  )
2422
- promiseArray.elements.push(fetchCountCall)
2423
- destructuringPattern.elements.push(types.identifier(`${dataSourceId}_count`))
2424
- addedCountFetches.add(dataSourceId)
2425
2138
  }
2426
- })
2427
-
2428
- // Calculate and add maxPages before return
2429
- const returnStmt = tryBody.find((stmt: any) => stmt.type === 'ReturnStatement') as
2430
- | types.ReturnStatement
2431
- | undefined
2432
2139
 
2433
- if (returnStmt && returnStmt.argument?.type === 'ObjectExpression') {
2434
- const propsProperty = (returnStmt.argument as types.ObjectExpression).properties.find(
2435
- (prop: any) =>
2436
- prop.type === 'ObjectProperty' && (prop.key as types.Identifier).name === 'props'
2437
- ) as types.ObjectProperty | undefined
2140
+ // Add maxPages calculation for paginated
2141
+ if (usage.paginated) {
2142
+ const maxPagesPropName = `${vars.propsPrefix}_maxPages`
2143
+ const countVarName = `${usage.dataSourceIdentifier}_count`
2438
2144
 
2439
- if (propsProperty && propsProperty.value.type === 'ObjectExpression') {
2440
- const propsObject = propsProperty.value as types.ObjectExpression
2441
- const returnIndex = tryBody.indexOf(returnStmt)
2442
-
2443
- paginationInfos.forEach((info, index) => {
2444
- const paginatedVarName = `${info.dataSourceIdentifier}_pg_${index}`
2445
- const countVarName = `${info.dataSourceIdentifier}_count`
2446
- const maxPagesVarName = `${info.dataSourceIdentifier}_pg_${index}_maxPages`
2145
+ // Check if maxPages calculation already exists
2146
+ const existingMaxPages = tryBlock.body.find(
2147
+ (s: any) =>
2148
+ s.type === 'VariableDeclaration' && s.declarations?.[0]?.id?.name === maxPagesPropName
2149
+ )
2447
2150
 
2448
- const maxPagesCalc = types.variableDeclaration('const', [
2449
- types.variableDeclarator(
2450
- types.identifier(maxPagesVarName),
2451
- types.callExpression(
2452
- types.memberExpression(types.identifier('Math'), types.identifier('ceil')),
2453
- [
2454
- types.binaryExpression(
2455
- '/',
2456
- types.logicalExpression(
2457
- '||',
2458
- types.identifier(countVarName),
2459
- types.numericLiteral(0)
2151
+ if (!existingMaxPages) {
2152
+ // Insert maxPages calculation before return
2153
+ const returnIndex = tryBlock.body.indexOf(returnStmt)
2154
+ tryBlock.body.splice(
2155
+ returnIndex,
2156
+ 0,
2157
+ types.variableDeclaration('const', [
2158
+ types.variableDeclarator(
2159
+ types.identifier(maxPagesPropName),
2160
+ types.callExpression(
2161
+ types.memberExpression(types.identifier('Math'), types.identifier('ceil')),
2162
+ [
2163
+ types.binaryExpression(
2164
+ '/',
2165
+ types.logicalExpression(
2166
+ '||',
2167
+ types.identifier(countVarName),
2168
+ types.numericLiteral(0)
2169
+ ),
2170
+ types.numericLiteral(usage.perPage)
2460
2171
  ),
2461
- types.numericLiteral(info.perPage)
2462
- ),
2463
- ]
2464
- )
2465
- ),
2466
- ])
2467
-
2468
- tryBody.splice(returnIndex, 0, maxPagesCalc)
2172
+ ]
2173
+ )
2174
+ ),
2175
+ ])
2176
+ )
2469
2177
 
2470
- // Add both the paginated data and maxPages to props
2471
- propsObject.properties.push(
2178
+ // Add maxPages to props
2179
+ propsObj.properties.push(
2472
2180
  types.objectProperty(
2473
- types.identifier(paginatedVarName),
2474
- types.identifier(paginatedVarName)
2181
+ types.identifier(maxPagesPropName),
2182
+ types.identifier(maxPagesPropName)
2475
2183
  )
2476
2184
  )
2477
- propsObject.properties.push(
2478
- types.objectProperty(types.identifier(maxPagesVarName), types.identifier(maxPagesVarName))
2479
- )
2480
- })
2481
- }
2482
- }
2483
- }
2484
-
2485
- function createAPIRoutesForPaginatedDataSources(
2486
- uidlNode: any,
2487
- dataSources: any,
2488
- componentChunk: any,
2489
- extractedResources: any,
2490
- paginationInfos: ArrayMapperPaginationInfo[],
2491
- isComponent: boolean
2492
- ): void {
2493
- const paginatedDataSourceIds = new Set(paginationInfos.map((info) => info.dataSourceIdentifier))
2494
-
2495
- const searchEnabledDataSources = new Set(
2496
- paginationInfos.filter((info) => info.searchEnabled).map((info) => info.dataSourceIdentifier)
2497
- )
2498
-
2499
- const createdCountRoutes = new Set<string>()
2500
-
2501
- const traverseForDataSources = (node: any): void => {
2502
- if (!node) {
2503
- return
2504
- }
2505
-
2506
- if (node.type === 'data-source-list' || node.type === 'data-source-item') {
2507
- const renderProp = node.content.renderPropIdentifier
2508
-
2509
- if (renderProp && paginatedDataSourceIds.has(renderProp)) {
2510
- extractDataSourceIntoNextAPIFolder(node, dataSources, componentChunk, extractedResources)
2511
-
2512
- const hasSearch = searchEnabledDataSources.has(renderProp)
2513
- const needsCountRoute = isComponent || hasSearch
2514
-
2515
- if (needsCountRoute) {
2516
- const resourceDef = node.content.resourceDefinition
2517
- if (resourceDef) {
2518
- const dataSourceId = resourceDef.dataSourceId
2519
- const tableName = resourceDef.tableName
2520
- const dataSourceType = resourceDef.dataSourceType
2521
- const fileName = `${dataSourceType}-${tableName}-${dataSourceId.substring(0, 8)}`
2522
- const countFileName = `${fileName}-count`
2523
-
2524
- if (!createdCountRoutes.has(countFileName)) {
2525
- extractedResources[`api/${countFileName}`] = {
2526
- fileName: countFileName,
2527
- fileType: FileType.JS,
2528
- path: ['pages', 'api'],
2529
- content: `import dataSource from '../../utils/data-sources/${fileName}'
2530
-
2531
- export default dataSource.getCount
2532
- `,
2533
- }
2534
- createdCountRoutes.add(countFileName)
2535
- }
2536
- }
2537
- }
2538
2185
  }
2539
2186
  }
2187
+ })
2540
2188
 
2541
- if (node.content?.children) {
2542
- node.content.children.forEach((child: any) => traverseForDataSources(child))
2543
- }
2544
- }
2545
-
2546
- traverseForDataSources(uidlNode)
2547
- }
2548
-
2549
- function createAPIRoutesForSearchOnlyDataSources(
2550
- uidlNode: any,
2551
- dataSources: any,
2552
- componentChunk: any,
2553
- extractedResources: any,
2554
- searchOnlyDataSources: DetectedPagination[]
2555
- ): void {
2556
- const searchOnlyDataSourceIds = new Set(
2557
- searchOnlyDataSources.map((info) => info.dataSourceIdentifier)
2558
- )
2559
-
2560
- const traverseForDataSources = (node: any): void => {
2561
- if (!node) {
2189
+ // Add count fetches for unique data sources
2190
+ dataSourcesNeedingCount.forEach((key) => {
2191
+ const [dataSourceType, tableName, dataSourceId] = key.split(':')
2192
+ const fileName = generateSafeFileName(dataSourceType, tableName, dataSourceId)
2193
+ // Use consistent import name generation (matches extractDataSourceIntoGetStaticProps)
2194
+ const fetcherImportName = StringUtils.dashCaseToCamelCase(fileName)
2195
+
2196
+ // Find usage to get dataSourceIdentifier
2197
+ const usage = registry.usages.find(
2198
+ (u) =>
2199
+ u.resourceDefinition.dataSourceId === dataSourceId &&
2200
+ u.resourceDefinition.tableName === tableName
2201
+ )
2202
+ if (!usage) {
2562
2203
  return
2563
2204
  }
2564
2205
 
2565
- if (node.type === 'data-source-list' || node.type === 'data-source-item') {
2566
- const renderProp = node.content.renderPropIdentifier
2206
+ const countVarName = `${usage.dataSourceIdentifier}_count`
2567
2207
 
2568
- if (renderProp && searchOnlyDataSourceIds.has(renderProp)) {
2569
- extractDataSourceIntoNextAPIFolder(node, dataSources, componentChunk, extractedResources)
2570
- }
2571
- }
2208
+ // Check if count fetch already exists
2209
+ const existingCount = arrayPattern.elements.findIndex(
2210
+ (el: any) => el?.type === 'Identifier' && el.name === countVarName
2211
+ )
2572
2212
 
2573
- if (node.content?.children) {
2574
- node.content.children.forEach((child: any) => traverseForDataSources(child))
2213
+ if (existingCount === -1) {
2214
+ arrayPattern.elements.push(types.identifier(countVarName))
2215
+ fetchesArray.elements.push(
2216
+ types.callExpression(
2217
+ types.memberExpression(
2218
+ types.identifier(fetcherImportName),
2219
+ types.identifier('fetchCount')
2220
+ ),
2221
+ []
2222
+ )
2223
+ )
2575
2224
  }
2576
- }
2577
-
2578
- traverseForDataSources(uidlNode)
2225
+ })
2579
2226
  }
2580
-
2581
- export default createNextArrayMapperPaginationPlugin()