@teleporthq/teleport-plugin-next-data-source 0.42.4 → 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 +908 -1351
  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 +909 -1352
  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 +1860 -2132
@@ -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,2389 +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
- )
506
+ // Build the count fetch effect body
507
+ const countFetchEffectBody: types.Statement[] = []
594
508
 
595
- if (
596
- !resourceDefAttr ||
597
- !resourceDefAttr.value ||
598
- resourceDefAttr.value.type !== 'JSXExpressionContainer'
599
- ) {
600
- return
601
- }
602
-
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
+ })
766
894
 
767
- // Create API routes for search-only data sources
768
- createAPIRoutesForSearchOnlyDataSources(
769
- uidl.node,
770
- options.dataSources,
771
- componentChunk,
772
- options.extractedResources,
773
- searchOnlyDataSources
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>()
898
+
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
- return structure
778
- }
907
+ const dataSourceIdentifier = nameAttr.value.expression.value
779
908
 
780
- return paginationPlugin
781
- }
909
+ // Use pure order-based matching within each dataSourceIdentifier
910
+ const usages = registry.byDataSourceId.get(dataSourceIdentifier) || []
911
+ const currentIndex = usageIndexByDataSourceId.get(dataSourceIdentifier) || 0
782
912
 
783
- function findParentNode(root: any, target: any, currentParent: any = null): any | null {
784
- if (!root || !target) {
785
- return null
786
- }
913
+ if (currentIndex >= usages.length) {
914
+ return
915
+ }
787
916
 
788
- if (root === target) {
789
- return currentParent
790
- }
917
+ const usage = usages[currentIndex]
918
+ usageIndexByDataSourceId.set(dataSourceIdentifier, currentIndex + 1)
791
919
 
792
- if (root.type === 'JSXElement' || root.type === 'JSXFragment') {
793
- if (root.children && Array.isArray(root.children)) {
794
- for (const child of root.children) {
795
- const found = findParentNode(child, target, root)
796
- if (found !== null) {
797
- return found
798
- }
799
- }
800
- }
801
- } else if (root.type === 'JSXExpressionContainer') {
802
- if (root.expression) {
803
- const found = findParentNode(root.expression, target, root)
804
- if (found !== null) {
805
- return found
806
- }
807
- }
808
- } else if (root.type === 'BlockStatement') {
809
- if (root.body && Array.isArray(root.body)) {
810
- for (const stmt of root.body) {
811
- const found = findParentNode(stmt, target, root)
812
- if (found !== null) {
813
- return found
814
- }
920
+ const vars = getStateVarsForUsage(usage)
921
+ const fileName = generateSafeFileName(
922
+ usage.resourceDefinition.dataSourceType,
923
+ usage.resourceDefinition.tableName,
924
+ usage.resourceDefinition.dataSourceId
925
+ )
926
+
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)
815
936
  }
816
- }
817
- } else if (root.type === 'ReturnStatement') {
818
- if (root.argument) {
819
- const found = findParentNode(root.argument, target, root)
820
- if (found !== null) {
821
- return found
937
+
938
+ // Create API route if needed
939
+ if (usage.category !== 'plain') {
940
+ ensureAPIRouteExists(options.extractedResources, usage)
822
941
  }
823
- }
824
- } else if (root.type === 'ConditionalExpression') {
825
- const foundConsequent = findParentNode(root.consequent, target, root)
826
- if (foundConsequent !== null) {
827
- return foundConsequent
828
- }
829
- const foundAlternate = findParentNode(root.alternate, target, root)
830
- if (foundAlternate !== null) {
831
- return foundAlternate
832
- }
833
- }
942
+ })
834
943
 
835
- return null
836
- }
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
+ })
837
951
 
838
- function detectPaginationsAndSearchFromJSX(
839
- blockStatement: types.BlockStatement,
840
- uidlNode: any
841
- ): {
842
- paginatedMappers: DetectedPagination[]
843
- searchOnlyMappers: DetectedPagination[]
844
- paginationOnlyMappers: DetectedPagination[]
845
- plainMappers: DetectedPagination[]
846
- } {
847
- interface DataProviderInfo {
848
- identifier: string
849
- dataProvider: any
850
- arrayMapperRenderProp?: string
851
- paginationNode?: { class: string; prevClass: string | null; nextClass: string | null }
852
- searchInput?: { class: string | null; jsx: any }
853
- hasPagination: boolean
854
- hasSearch: boolean
855
- }
952
+ dataProvidersWithoutRepeaters.forEach((dp) => {
953
+ stabilizeDataProviderWithoutRepeater(dp)
954
+ })
856
955
 
857
- const dataProviderList: DataProviderInfo[] = []
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)
858
959
 
859
- // First pass: collect array mapper info from UIDL, keyed by array mapper render prop (unique)
860
- interface ArrayMapperInfo {
861
- dataSourceIdentifier: string
862
- renderProp: string
863
- paginated: boolean
864
- searchEnabled: boolean
865
- }
866
- const arrayMapperInfoMap = new Map<string, ArrayMapperInfo>()
960
+ // Get all search-enabled usages in order
961
+ const searchEnabledUsages = registry.usages.filter((u) => u.searchEnabled)
867
962
 
868
- if (uidlNode) {
869
- const collectArrayMapperInfo = (node: any): void => {
870
- if (!node) {
963
+ // Match by order - search input 0 -> searchEnabledUsages[0], etc.
964
+ searchInputs.forEach((input, idx) => {
965
+ if (idx >= searchEnabledUsages.length) {
871
966
  return
872
967
  }
873
- if (node.type === 'data-source-list' && node.content?.renderPropIdentifier) {
874
- const dataSourceIdentifier = node.content.renderPropIdentifier
875
- if (node.content.nodes?.success?.content?.children) {
876
- node.content.nodes.success.content.children.forEach((child: any) => {
877
- if (child.type === 'cms-list-repeater' && child.content?.renderPropIdentifier) {
878
- const arrayMapperRenderProp = child.content.renderPropIdentifier
879
- // Key by array mapper render prop (unique), not data source identifier
880
- arrayMapperInfoMap.set(arrayMapperRenderProp, {
881
- dataSourceIdentifier,
882
- renderProp: arrayMapperRenderProp,
883
- paginated: child.content.paginated || false,
884
- searchEnabled: child.content.searchEnabled || false,
885
- })
886
- }
887
- })
888
- }
889
- }
890
- if (node.content?.children) {
891
- node.content.children.forEach((child: any) => collectArrayMapperInfo(child))
892
- }
893
- if (node.children && Array.isArray(node.children)) {
894
- node.children.forEach((child: any) => collectArrayMapperInfo(child))
968
+
969
+ const usage = searchEnabledUsages[idx]
970
+ const vars = getStateVarsForUsage(usage)
971
+ wireSearchInput(input.node, vars)
972
+ })
973
+
974
+ // STEP 5: Wire pagination buttons
975
+ // Match pagination nodes to usages by order (within paginated usages)
976
+ const paginationNodes = findAllPaginationNodesInJSX(blockStatement)
977
+
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
895
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)
896
995
  }
897
- collectArrayMapperInfo(uidlNode)
898
- }
899
996
 
900
- // Second pass: find all DataProviders and their parent containers
901
- interface DataProviderWithParent {
902
- dataProvider: any
903
- parent: any
997
+ return structure
904
998
  }
905
- const dataProvidersWithParents: DataProviderWithParent[] = []
906
999
 
907
- const findDataProvidersAndParents = (node: any, parent: any = null): void => {
1000
+ return paginationPlugin
1001
+ }
1002
+
1003
+ // ==================== HELPER FUNCTIONS ====================
1004
+
1005
+ function findAllDataProvidersInJSX(blockStatement: types.BlockStatement): any[] {
1006
+ const results: any[] = []
1007
+
1008
+ const traverse = (node: any): void => {
908
1009
  if (!node) {
909
1010
  return
910
1011
  }
911
1012
 
912
- // Check if this is a DataProvider
913
1013
  if (node.type === 'JSXElement' && node.openingElement?.name?.name === 'DataProvider') {
914
- dataProvidersWithParents.push({
915
- dataProvider: node,
916
- parent,
917
- })
918
- }
919
-
920
- // Only recurse through JSX structure, not into embedded expressions or code
921
- if (node.type === 'ReturnStatement' && node.argument) {
922
- findDataProvidersAndParents(node.argument, parent)
923
- } else if (node.type === 'JSXElement' || node.type === 'JSXFragment') {
924
- // Recurse through JSX children
925
- if (node.children && Array.isArray(node.children)) {
926
- node.children.forEach((child: any) => findDataProvidersAndParents(child, node))
927
- }
928
- } else if (node.type === 'JSXExpressionContainer') {
929
- // For expression containers, continue but don't go into the expression itself
930
- if (
931
- node.expression &&
932
- (node.expression.type === 'JSXElement' || node.expression.type === 'JSXFragment')
933
- ) {
934
- findDataProvidersAndParents(node.expression, parent)
935
- }
936
- } else if (node.type === 'BlockStatement') {
937
- // For block statements (function bodies), look through body array
938
- if (node.body && Array.isArray(node.body)) {
939
- node.body.forEach((stmt: any) => findDataProvidersAndParents(stmt, node))
940
- }
941
- } else if (node.type === 'ConditionalExpression') {
942
- // Check both branches of conditional
943
- findDataProvidersAndParents(node.consequent, parent)
944
- findDataProvidersAndParents(node.alternate, parent)
1014
+ results.push(node)
945
1015
  }
946
- }
947
1016
 
948
- findDataProvidersAndParents(blockStatement)
1017
+ if (node.children && Array.isArray(node.children)) {
1018
+ node.children.forEach((c: any) => traverse(c))
1019
+ }
949
1020
 
950
- // Now process each DataProvider and find its siblings
951
- dataProvidersWithParents.forEach(({ dataProvider, parent }) => {
952
- const nameAttr = dataProvider.openingElement.attributes.find(
953
- (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'name'
954
- )
1021
+ if (node.body) {
1022
+ if (Array.isArray(node.body)) {
1023
+ node.body.forEach((s: any) => traverse(s))
1024
+ } else {
1025
+ traverse(node.body)
1026
+ }
1027
+ }
955
1028
 
956
- // Find the array mapper render prop by looking inside renderSuccess -> Repeater -> renderItem param
957
- const renderSuccessAttr = dataProvider.openingElement.attributes.find(
958
- (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'renderSuccess'
959
- )
1029
+ if (node.consequent) {
1030
+ traverse(node.consequent)
1031
+ }
960
1032
 
961
- let arrayMapperRenderProp: string | undefined
962
- if (renderSuccessAttr && renderSuccessAttr.value?.type === 'JSXExpressionContainer') {
963
- const renderFunc = renderSuccessAttr.value.expression
964
- if (renderFunc.type === 'ArrowFunctionExpression') {
965
- // Look for Repeater inside the render function
966
- const findRepeater = (node: any): any => {
967
- if (!node) {
968
- return null
969
- }
970
- if (node.type === 'JSXElement' && node.openingElement?.name?.name === 'Repeater') {
971
- return node
972
- }
973
- if (node.body) {
974
- return findRepeater(node.body)
975
- }
976
- if (node.children && Array.isArray(node.children)) {
977
- for (const child of node.children) {
978
- const result = findRepeater(child)
979
- if (result) {
980
- return result
981
- }
982
- }
983
- }
984
- if (node.type === 'JSXFragment' || node.type === 'JSXElement') {
985
- if (node.children && Array.isArray(node.children)) {
986
- for (const child of node.children) {
987
- const result = findRepeater(child)
988
- if (result) {
989
- return result
990
- }
991
- }
992
- }
993
- }
994
- if (node.type === 'JSXExpressionContainer') {
995
- return findRepeater(node.expression)
996
- }
997
- if (node.expression) {
998
- return findRepeater(node.expression)
999
- }
1000
- if (node.consequent) {
1001
- const result = findRepeater(node.consequent)
1002
- if (result) {
1003
- return result
1004
- }
1005
- }
1006
- if (node.alternate) {
1007
- return findRepeater(node.alternate)
1008
- }
1009
- return null
1010
- }
1033
+ if (node.alternate) {
1034
+ traverse(node.alternate)
1035
+ }
1011
1036
 
1012
- const repeater = findRepeater(renderFunc)
1013
- if (repeater) {
1014
- // Find renderItem attribute on Repeater
1015
- const renderItemAttr = repeater.openingElement.attributes.find(
1016
- (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'renderItem'
1017
- )
1018
- if (renderItemAttr && renderItemAttr.value?.type === 'JSXExpressionContainer') {
1019
- const renderItemFunc = renderItemAttr.value.expression
1020
- if (
1021
- renderItemFunc.type === 'ArrowFunctionExpression' &&
1022
- renderItemFunc.params &&
1023
- renderItemFunc.params.length > 0
1024
- ) {
1025
- const param = renderItemFunc.params[0]
1026
- if (param.type === 'Identifier') {
1027
- arrayMapperRenderProp = param.name
1028
- }
1029
- }
1030
- }
1031
- }
1032
- }
1037
+ if (node.expression) {
1038
+ traverse(node.expression)
1033
1039
  }
1034
1040
 
1035
- if (
1036
- !nameAttr ||
1037
- !nameAttr.value ||
1038
- nameAttr.value.type !== 'JSXExpressionContainer' ||
1039
- !arrayMapperRenderProp
1040
- ) {
1041
- return
1041
+ if (node.argument) {
1042
+ traverse(node.argument)
1042
1043
  }
1043
1044
 
1044
- const dataProviderIdentifier = nameAttr.value.expression.value
1045
- let paginationNodeInfo: {
1046
- class: string
1047
- prevClass: string | null
1048
- nextClass: string | null
1049
- } | null = null
1050
- let searchInputInfo: { class: string | null; jsx: any } | null = null
1045
+ if (node.arguments) {
1046
+ node.arguments.forEach((a: any) => traverse(a))
1047
+ }
1048
+ }
1051
1049
 
1052
- const findSearchAndPaginationInScope = (scopeNode: any, skipNode: any = null): boolean => {
1053
- if (!scopeNode || !scopeNode.children || !Array.isArray(scopeNode.children)) {
1054
- return false
1055
- }
1050
+ traverse(blockStatement)
1051
+ return results
1052
+ }
1056
1053
 
1057
- let foundSomething = false
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
+ )
1058
1058
 
1059
- for (const child of scopeNode.children) {
1060
- if (child === skipNode) {
1061
- continue
1062
- }
1059
+ if (!renderSuccessAttr?.value?.expression) {
1060
+ return undefined
1061
+ }
1063
1062
 
1064
- if (child.type === 'JSXElement') {
1065
- const childClassName = getClassName(child.openingElement?.attributes || [])
1066
- const childElementName = child.openingElement?.name?.name
1067
-
1068
- // Found pagination node
1069
- if (childClassName && childClassName.includes('cms-pagination-node')) {
1070
- const prevClass = findChildWithClass(child, 'previous')
1071
- const nextClass = findChildWithClass(child, 'next')
1072
- if (prevClass || nextClass) {
1073
- paginationNodeInfo = {
1074
- class: childClassName,
1075
- prevClass,
1076
- nextClass,
1077
- }
1078
- foundSomething = true
1079
- }
1080
- }
1081
-
1082
- // Found search container - search for input inside it
1083
- if (childClassName && childClassName.includes('data-source-search-node')) {
1084
- if (child.children && Array.isArray(child.children)) {
1085
- for (const searchChild of child.children) {
1086
- if (searchChild.type === 'JSXElement') {
1087
- const searchChildElementName = searchChild.openingElement?.name?.name
1088
- const searchChildClassName = getClassName(
1089
- searchChild.openingElement?.attributes || []
1090
- )
1091
- if (
1092
- searchChildClassName &&
1093
- searchChildClassName.includes('search-input') &&
1094
- searchChildElementName === 'input'
1095
- ) {
1096
- searchInputInfo = {
1097
- class: searchChildClassName,
1098
- jsx: searchChild,
1099
- }
1100
- foundSomething = true
1101
- }
1102
- }
1103
- }
1104
- }
1105
- }
1106
-
1107
- // Also check if search input is a direct child
1108
- if (
1109
- childClassName &&
1110
- childClassName.includes('search-input') &&
1111
- childElementName === 'input'
1112
- ) {
1113
- searchInputInfo = {
1114
- class: childClassName,
1115
- jsx: child,
1116
- }
1117
- foundSomething = true
1118
- }
1119
-
1120
- // Stop searching if we found both or if we found what we're looking for
1121
- if (foundSomething && (searchInputInfo || paginationNodeInfo)) {
1122
- return true
1123
- }
1124
-
1125
- // Only recurse if we haven't found what we're looking for yet
1126
- if (!searchInputInfo || !paginationNodeInfo) {
1127
- if (findSearchAndPaginationInScope(child, skipNode)) {
1128
- return true
1129
- }
1130
- }
1131
- }
1132
- }
1063
+ const findRepeater = (node: any): any => {
1064
+ if (!node) {
1065
+ return null
1066
+ }
1133
1067
 
1134
- return foundSomething
1068
+ if (node.type === 'JSXElement' && node.openingElement?.name?.name === 'Repeater') {
1069
+ return node
1135
1070
  }
1136
1071
 
1137
- let currentScope = parent
1138
- let depth = 0
1139
- const maxDepth = 5
1072
+ if (node.body) {
1073
+ return findRepeater(node.body)
1074
+ }
1140
1075
 
1141
- while (currentScope && (!searchInputInfo || !paginationNodeInfo) && depth < maxDepth) {
1142
- const found = findSearchAndPaginationInScope(currentScope, depth === 0 ? dataProvider : null)
1143
- // Stop searching up the tree if we found a pagination node at this level
1144
- if (found && paginationNodeInfo) {
1145
- break
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
+ }
1146
1082
  }
1147
- currentScope = findParentNode(blockStatement, currentScope)
1148
- depth++
1149
- }
1150
-
1151
- // Record the DataProvider with its pagination/search info
1152
- // Use array instead of Map to handle multiple DataProviders with same name
1153
- dataProviderList.push({
1154
- identifier: dataProviderIdentifier,
1155
- dataProvider,
1156
- arrayMapperRenderProp,
1157
- paginationNode: paginationNodeInfo || undefined,
1158
- searchInput: searchInputInfo || undefined,
1159
- hasPagination: !!paginationNodeInfo,
1160
- hasSearch: !!searchInputInfo,
1161
- })
1162
- })
1163
-
1164
- // Categorize data providers
1165
- const paginatedMappers: DetectedPagination[] = []
1166
- const searchOnlyMappers: DetectedPagination[] = []
1167
- const paginationOnlyMappers: DetectedPagination[] = []
1168
- const plainMappers: DetectedPagination[] = []
1169
-
1170
- dataProviderList.forEach((info) => {
1171
- // Check UIDL flags for this array mapper
1172
- const uidlInfo = info.arrayMapperRenderProp
1173
- ? arrayMapperInfoMap.get(info.arrayMapperRenderProp)
1174
- : null
1175
-
1176
- // Only process pagination/search if UIDL explicitly enables it
1177
- const shouldHavePagination = uidlInfo?.paginated && info.hasPagination
1178
- const shouldHaveSearch = uidlInfo?.searchEnabled && info.hasSearch
1179
-
1180
- if (shouldHavePagination && shouldHaveSearch) {
1181
- // Pagination + Search
1182
- paginatedMappers.push({
1183
- paginationNodeClass: info.paginationNode!.class,
1184
- prevButtonClass: info.paginationNode!.prevClass,
1185
- nextButtonClass: info.paginationNode!.nextClass,
1186
- dataSourceIdentifier: info.identifier,
1187
- dataProviderJSX: info.dataProvider,
1188
- arrayMapperRenderProp: info.arrayMapperRenderProp,
1189
- searchInputClass: info.searchInput?.class,
1190
- searchInputJSX: info.searchInput?.jsx,
1191
- })
1192
- } else if (shouldHavePagination && !shouldHaveSearch) {
1193
- // Pagination only
1194
- paginationOnlyMappers.push({
1195
- paginationNodeClass: info.paginationNode!.class,
1196
- prevButtonClass: info.paginationNode!.prevClass,
1197
- nextButtonClass: info.paginationNode!.nextClass,
1198
- dataSourceIdentifier: info.identifier,
1199
- dataProviderJSX: info.dataProvider,
1200
- arrayMapperRenderProp: info.arrayMapperRenderProp,
1201
- searchInputClass: undefined,
1202
- searchInputJSX: undefined,
1203
- })
1204
- } else if (!shouldHavePagination && shouldHaveSearch) {
1205
- // Search only
1206
- searchOnlyMappers.push({
1207
- paginationNodeClass: '',
1208
- prevButtonClass: null,
1209
- nextButtonClass: null,
1210
- dataSourceIdentifier: info.identifier,
1211
- dataProviderJSX: info.dataProvider,
1212
- arrayMapperRenderProp: info.arrayMapperRenderProp,
1213
- searchInputClass: info.searchInput?.class,
1214
- searchInputJSX: info.searchInput?.jsx,
1215
- })
1216
- } else {
1217
- // Plain (no pagination, no search) - UIDL doesn't enable it or no controls found
1218
- plainMappers.push({
1219
- paginationNodeClass: '',
1220
- prevButtonClass: null,
1221
- nextButtonClass: null,
1222
- dataSourceIdentifier: info.identifier,
1223
- dataProviderJSX: info.dataProvider,
1224
- arrayMapperRenderProp: info.arrayMapperRenderProp,
1225
- searchInputClass: undefined,
1226
- searchInputJSX: undefined,
1227
- })
1228
1083
  }
1229
- })
1084
+ return null
1085
+ }
1230
1086
 
1231
- return { paginatedMappers, searchOnlyMappers, paginationOnlyMappers, plainMappers }
1087
+ const repeater = findRepeater(renderSuccessAttr.value.expression)
1088
+ if (!repeater) {
1089
+ return undefined
1090
+ }
1091
+
1092
+ const renderItemAttr = repeater.openingElement.attributes.find(
1093
+ (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'renderItem'
1094
+ )
1095
+
1096
+ if (!renderItemAttr?.value?.expression?.params?.[0]?.name) {
1097
+ return undefined
1098
+ }
1099
+
1100
+ return renderItemAttr.value.expression.params[0].name
1232
1101
  }
1233
1102
 
1234
- function handleSearchOnlyArrayMappers(
1235
- blockStatement: types.BlockStatement,
1236
- searchOnlyDataSources: DetectedPagination[],
1237
- searchConfigMap: Map<string, any>,
1238
- queryColumnsMap: Map<string, string[]>,
1239
- dependencies: any
1240
- ): void {
1241
- const searchOnlyStates: types.Statement[] = []
1242
- const searchOnlyEffects: types.Statement[] = []
1243
-
1244
- searchOnlyDataSources.forEach((detected, index) => {
1245
- const searchId = `search_${index}`
1246
- const searchQueryVar = `${searchId}_query`
1247
- const setSearchQueryVar = `set${
1248
- searchQueryVar.charAt(0).toUpperCase() + searchQueryVar.slice(1)
1249
- }`
1250
- const debouncedSearchQueryVar = `debounced${
1251
- searchQueryVar.charAt(0).toUpperCase() + searchQueryVar.slice(1)
1252
- }`
1253
- const setDebouncedSearchQueryVar = `set${
1254
- debouncedSearchQueryVar.charAt(0).toUpperCase() + debouncedSearchQueryVar.slice(1)
1255
- }`
1256
- const skipDebounceOnMountRefVar = `skipDebounceOnMount${searchId}`
1257
-
1258
- const searchConfig = searchConfigMap.get(detected.dataSourceIdentifier)
1259
- const searchDebounce = searchConfig?.searchDebounce || 300
1260
- const queryColumns = queryColumnsMap.get(detected.dataSourceIdentifier)
1261
-
1262
- // Add skip ref
1263
- const skipRefAST = types.variableDeclaration('const', [
1264
- types.variableDeclarator(
1265
- types.identifier(skipDebounceOnMountRefVar),
1266
- types.callExpression(types.identifier('useRef'), [types.booleanLiteral(true)])
1267
- ),
1268
- ])
1269
- searchOnlyStates.push(skipRefAST)
1270
-
1271
- // Add debounced search state
1272
- const debouncedSearchStateAST = types.variableDeclaration('const', [
1273
- types.variableDeclarator(
1274
- types.arrayPattern([
1275
- types.identifier(debouncedSearchQueryVar),
1276
- types.identifier(setDebouncedSearchQueryVar),
1277
- ]),
1278
- types.callExpression(types.identifier('useState'), [types.stringLiteral('')])
1279
- ),
1280
- ])
1281
- searchOnlyStates.push(debouncedSearchStateAST)
1282
-
1283
- // Add search query state
1284
- const searchStateAST = types.variableDeclaration('const', [
1285
- types.variableDeclarator(
1286
- types.arrayPattern([types.identifier(searchQueryVar), types.identifier(setSearchQueryVar)]),
1287
- types.callExpression(types.identifier('useState'), [types.stringLiteral('')])
1288
- ),
1289
- ])
1290
- searchOnlyStates.push(searchStateAST)
1103
+ function findAllSearchInputsInJSX(
1104
+ blockStatement: types.BlockStatement
1105
+ ): Array<{ node: any; className: string }> {
1106
+ const results: Array<{ node: any; className: string }> = []
1291
1107
 
1292
- // Add useEffect for debouncing
1293
- if (!dependencies.useEffect) {
1294
- dependencies.useEffect = {
1295
- type: 'library',
1296
- path: 'react',
1297
- version: '',
1298
- meta: {
1299
- namedImport: true,
1300
- },
1301
- }
1108
+ const traverse = (node: any): void => {
1109
+ if (!node) {
1110
+ return
1302
1111
  }
1303
1112
 
1304
- const debounceEffect = types.expressionStatement(
1305
- types.callExpression(types.identifier('useEffect'), [
1306
- types.arrowFunctionExpression(
1307
- [],
1308
- types.blockStatement([
1309
- types.ifStatement(
1310
- types.memberExpression(
1311
- types.identifier(skipDebounceOnMountRefVar),
1312
- types.identifier('current')
1313
- ),
1314
- types.blockStatement([
1315
- types.expressionStatement(
1316
- types.assignmentExpression(
1317
- '=',
1318
- types.memberExpression(
1319
- types.identifier(skipDebounceOnMountRefVar),
1320
- types.identifier('current')
1321
- ),
1322
- types.booleanLiteral(false)
1323
- )
1324
- ),
1325
- types.returnStatement(),
1326
- ])
1327
- ),
1328
- types.variableDeclaration('const', [
1329
- types.variableDeclarator(
1330
- types.identifier('timer'),
1331
- types.callExpression(types.identifier('setTimeout'), [
1332
- types.arrowFunctionExpression(
1333
- [],
1334
- types.blockStatement([
1335
- types.expressionStatement(
1336
- types.callExpression(types.identifier(setDebouncedSearchQueryVar), [
1337
- types.identifier(searchQueryVar),
1338
- ])
1339
- ),
1340
- ])
1341
- ),
1342
- types.numericLiteral(searchDebounce),
1343
- ])
1344
- ),
1345
- ]),
1346
- types.returnStatement(
1347
- types.arrowFunctionExpression(
1348
- [],
1349
- types.callExpression(types.identifier('clearTimeout'), [types.identifier('timer')])
1350
- )
1351
- ),
1352
- ])
1353
- ),
1354
- types.arrayExpression([types.identifier(searchQueryVar)]),
1355
- ])
1356
- )
1357
- searchOnlyEffects.push(debounceEffect)
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 })
1120
+ }
1121
+ }
1358
1122
 
1359
- // Modify DataProvider to include search params (even without queryColumns)
1360
- if (detected.dataProviderJSX) {
1361
- addSearchParamsToDataProvider(detected.dataProviderJSX, {
1362
- debouncedSearchQueryVar,
1363
- queryColumns: queryColumns || [],
1364
- })
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)
1131
+ }
1365
1132
  }
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
+ }
1146
+
1147
+ traverse(blockStatement)
1148
+ return results
1149
+ }
1366
1150
 
1367
- // Modify search input
1368
- if (detected.searchInputJSX) {
1369
- addSearchInputHandlers(detected.searchInputJSX, {
1370
- searchQueryVar,
1371
- setSearchQueryVar,
1372
- searchEnabled: true,
1373
- } as any)
1151
+ function findAllPaginationNodesInJSX(
1152
+ blockStatement: types.BlockStatement
1153
+ ): Array<{ node: any; className: string }> {
1154
+ const results: Array<{ node: any; className: string }> = []
1155
+
1156
+ const traverse = (node: any): void => {
1157
+ if (!node) {
1158
+ return
1374
1159
  }
1375
- })
1376
1160
 
1377
- // Add all state declarations at the beginning in correct order
1378
- searchOnlyStates.reverse().forEach((stateDecl) => {
1379
- blockStatement.body.unshift(stateDecl)
1380
- })
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
+ }
1169
+ }
1381
1170
 
1382
- // Recalculate insertIndex after adding states (since unshift shifted everything)
1383
- const newInsertIndex = blockStatement.body.findIndex(
1384
- (stmt: any) => stmt.type === 'ReturnStatement'
1385
- )
1386
- const finalInsertIndex = newInsertIndex !== -1 ? newInsertIndex : blockStatement.body.length
1171
+ if (node.children && Array.isArray(node.children)) {
1172
+ node.children.forEach((c: any) => traverse(c))
1173
+ }
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
+ }
1387
1194
 
1388
- // Add all useEffect hooks before the return statement
1389
- searchOnlyEffects.reverse().forEach((effect) => {
1390
- blockStatement.body.splice(finalInsertIndex, 0, effect)
1391
- })
1195
+ traverse(blockStatement)
1196
+ return results
1392
1197
  }
1393
1198
 
1394
- function addSearchParamsToDataProvider(
1395
- dataProviderJSX: any,
1396
- config: { debouncedSearchQueryVar: string; queryColumns: string[] }
1199
+ function updateDataProviderForPaginatedSearch(
1200
+ dp: any,
1201
+ usage: DataSourceUsage,
1202
+ vars: ReturnType<typeof getStateVarsForUsage>,
1203
+ fileName: string
1397
1204
  ): void {
1398
- if (!dataProviderJSX || !dataProviderJSX.openingElement) {
1399
- return
1400
- }
1205
+ const attrs = dp.openingElement.attributes
1401
1206
 
1402
- const { debouncedSearchQueryVar, queryColumns } = config
1403
-
1404
- // 1. Update params to be wrapped in useMemo
1405
- const existingParamsAttr = dataProviderJSX.openingElement.attributes.find(
1406
- (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
+ )
1407
1213
  )
1408
1214
 
1409
- if (existingParamsAttr && existingParamsAttr.value?.type === 'JSXExpressionContainer') {
1410
- const paramsObj = existingParamsAttr.value.expression
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')
1227
+ )
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)))]
1237
+ )
1238
+ )
1239
+ )
1240
+ }
1411
1241
 
1412
- if (paramsObj.type === 'ObjectExpression') {
1413
- // Add query to existing params properties
1414
- paramsObj.properties.push(
1415
- types.objectProperty(types.identifier('query'), types.identifier(debouncedSearchQueryVar))
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
+ ])
1416
1250
  )
1251
+ )
1252
+ )
1417
1253
 
1418
- // Add queryColumns only if they exist
1419
- if (queryColumns.length > 0) {
1420
- paramsObj.properties.push(
1421
- types.objectProperty(
1422
- types.identifier('queryColumns'),
1423
- types.callExpression(
1424
- types.memberExpression(types.identifier('JSON'), types.identifier('stringify')),
1425
- [types.arrayExpression(queryColumns.map((col) => types.stringLiteral(col)))]
1426
- )
1427
- )
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')
1428
1284
  )
1429
- }
1285
+ )
1286
+ )
1287
+ )
1430
1288
 
1431
- // Wrap in useMemo with debouncedSearchQueryVar as dependency
1432
- const memoizedParamsValue = types.callExpression(types.identifier('useMemo'), [
1433
- types.arrowFunctionExpression([], paramsObj),
1434
- types.arrayExpression([types.identifier(debouncedSearchQueryVar)]),
1435
- ])
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
+ )
1316
+ )
1436
1317
 
1437
- existingParamsAttr.value.expression = memoizedParamsValue
1438
- }
1439
- }
1318
+ // Add fetchData
1319
+ dp.openingElement.attributes.push(createFetchDataAttribute(fileName))
1320
+
1321
+ // Add persistDataDuringLoading
1322
+ dp.openingElement.attributes.push(
1323
+ types.jsxAttribute(
1324
+ types.jsxIdentifier('persistDataDuringLoading'),
1325
+ types.jsxExpressionContainer(types.booleanLiteral(true))
1326
+ )
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
1440
1337
 
1441
- // 2. Update initialData to be conditional on empty search
1442
- const existingInitialDataAttr = dataProviderJSX.openingElement.attributes.find(
1443
- (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'initialData'
1338
+ dp.openingElement.attributes = attrs.filter(
1339
+ (attr: any) =>
1340
+ !['params', 'key', 'initialData', 'fetchData', 'persistDataDuringLoading'].includes(
1341
+ attr.name?.name
1342
+ )
1444
1343
  )
1445
1344
 
1446
- if (existingInitialDataAttr && existingInitialDataAttr.value?.type === 'JSXExpressionContainer') {
1447
- const currentInitialData = existingInitialDataAttr.value.expression
1448
-
1449
- // Make it conditional: only use initialData if search is empty
1450
- existingInitialDataAttr.value.expression = types.conditionalExpression(
1451
- types.unaryExpression('!', types.identifier(debouncedSearchQueryVar), true),
1452
- currentInitialData,
1453
- types.identifier('undefined')
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
+ )
1454
1364
  )
1455
- }
1456
-
1457
- // 3. Update key to include search query
1458
- const existingKeyAttr = dataProviderJSX.openingElement.attributes.find(
1459
- (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'key'
1460
1365
  )
1461
1366
 
1462
- const keyExpression = types.templateLiteral(
1463
- [
1464
- types.templateElement({ raw: 'search-', cooked: 'search-' }),
1465
- types.templateElement({ raw: '', cooked: '' }),
1466
- ],
1467
- [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
+ )
1468
1388
  )
1469
1389
 
1470
- const keyAttr = types.jsxAttribute(
1471
- types.jsxIdentifier('key'),
1472
- 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
+ )
1473
1407
  )
1474
1408
 
1475
- if (existingKeyAttr) {
1476
- const index = dataProviderJSX.openingElement.attributes.indexOf(existingKeyAttr)
1477
- dataProviderJSX.openingElement.attributes[index] = keyAttr
1478
- } else {
1479
- dataProviderJSX.openingElement.attributes.push(keyAttr)
1480
- }
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
+ )
1481
1418
  }
1482
1419
 
1483
- function addPaginationParamsToDataProvider(
1484
- dataProviderJSX: any,
1485
- info: ArrayMapperPaginationInfo,
1486
- paginationIndex: number
1420
+ function updateDataProviderForSearchOnly(
1421
+ dp: any,
1422
+ usage: DataSourceUsage,
1423
+ vars: ReturnType<typeof getStateVarsForUsage>,
1424
+ fileName: string
1487
1425
  ): void {
1488
- if (!dataProviderJSX || !dataProviderJSX.openingElement) {
1489
- return
1490
- }
1491
-
1492
- const combinedStateVar = (info as any).combinedStateVar
1426
+ const attrs = dp.openingElement.attributes
1493
1427
 
1494
- let paramsProperties: any[]
1495
- 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
+ )
1496
1434
 
1497
- if (info.searchEnabled && combinedStateVar) {
1498
- // Use combined state for both page and query
1499
- paramsProperties = [
1500
- types.objectProperty(
1501
- types.identifier('page'),
1502
- types.memberExpression(types.identifier(combinedStateVar), types.identifier('page'))
1503
- ),
1504
- 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(
1505
1441
  types.objectProperty(
1506
- types.identifier('query'),
1507
- types.memberExpression(
1508
- types.identifier(combinedStateVar),
1509
- types.identifier('debouncedQuery')
1510
- )
1511
- ),
1512
- ]
1513
-
1514
- if (info.queryColumns && info.queryColumns.length > 0) {
1515
- paramsProperties.push(
1516
- types.objectProperty(
1517
- types.identifier('queryColumns'),
1518
- types.callExpression(
1519
- types.memberExpression(types.identifier('JSON'), types.identifier('stringify')),
1520
- [types.arrayExpression(info.queryColumns.map((col) => types.stringLiteral(col)))]
1521
- )
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)))]
1522
1446
  )
1523
1447
  )
1524
- }
1525
-
1526
- // Single dependency: the combined state object
1527
- dependencies = [types.identifier(combinedStateVar)]
1528
- } else {
1529
- // Pagination only (no search)
1530
- paramsProperties = [
1531
- types.objectProperty(types.identifier('page'), types.identifier(info.pageStateVar)),
1532
- types.objectProperty(types.identifier('perPage'), types.numericLiteral(info.perPage)),
1533
- ]
1534
-
1535
- dependencies = [types.identifier(info.pageStateVar)]
1448
+ )
1536
1449
  }
1537
1450
 
1538
- // Wrap params in useMemo to prevent unnecessary refetches
1539
- const memoizedParamsValue = types.callExpression(types.identifier('useMemo'), [
1540
- types.arrowFunctionExpression([], types.objectExpression(paramsProperties)),
1541
- types.arrayExpression(dependencies),
1542
- ])
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
+ )
1543
1462
 
1544
- const paramsAttr = types.jsxAttribute(
1545
- types.jsxIdentifier('params'),
1546
- 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
+ )
1547
1480
  )
1548
1481
 
1549
- const existingParamsAttr = dataProviderJSX.openingElement.attributes.find(
1550
- (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
+ )
1551
1496
  )
1552
1497
 
1553
- if (existingParamsAttr) {
1554
- const index = dataProviderJSX.openingElement.attributes.indexOf(existingParamsAttr)
1555
- dataProviderJSX.openingElement.attributes[index] = paramsAttr
1556
- } else {
1557
- dataProviderJSX.openingElement.attributes.push(paramsAttr)
1558
- }
1498
+ // Add fetchData
1499
+ dp.openingElement.attributes.push(createFetchDataAttribute(fileName))
1559
1500
 
1560
- const existingInitialDataAttr = dataProviderJSX.openingElement.attributes.find(
1561
- (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
+ )
1562
1506
  )
1507
+ }
1563
1508
 
1564
- if (existingInitialDataAttr && existingInitialDataAttr.value) {
1565
- // Update initialData to use the paginated prop name
1566
- const paginatedPropName = `${info.dataSourceIdentifier}_pg_${paginationIndex}`
1509
+ function updateDataProviderForPlain(dp: any, vars: ReturnType<typeof getStateVarsForUsage>): void {
1510
+ const attrs = dp.openingElement.attributes
1567
1511
 
1568
- // If search is enabled, use combined state; otherwise use page state
1569
- 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
+ )
1570
1516
 
1571
- if (info.searchEnabled && combinedStateVar) {
1572
- condition = types.logicalExpression(
1573
- '&&',
1574
- types.binaryExpression(
1575
- '===',
1576
- types.memberExpression(types.identifier(combinedStateVar), types.identifier('page')),
1577
- types.numericLiteral(1)
1578
- ),
1579
- types.unaryExpression(
1580
- '!',
1581
- types.memberExpression(
1582
- types.identifier(combinedStateVar),
1583
- types.identifier('debouncedQuery')
1584
- ),
1585
- true
1586
- )
1587
- )
1588
- } else {
1589
- condition = types.binaryExpression(
1590
- '===',
1591
- types.identifier(info.pageStateVar),
1592
- types.numericLiteral(1)
1593
- )
1594
- }
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
+ )
1595
1521
 
1596
- existingInitialDataAttr.value.expression = types.conditionalExpression(
1597
- condition,
1522
+ if (existingInitialData) {
1523
+ existingInitialData.value = types.jsxExpressionContainer(
1598
1524
  types.optionalMemberExpression(
1599
1525
  types.identifier('props'),
1600
- types.identifier(paginatedPropName),
1526
+ types.identifier(vars.propsPrefix),
1601
1527
  false,
1602
1528
  true
1603
- ),
1604
- types.identifier('undefined')
1529
+ )
1605
1530
  )
1606
1531
  }
1532
+ }
1607
1533
 
1608
- const existingKeyAttr = dataProviderJSX.openingElement.attributes.find(
1609
- (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'key'
1534
+ function stabilizeDataProviderWithoutRepeater(dp: any): void {
1535
+ const attrs = dp.openingElement.attributes
1536
+
1537
+ // Find the params attribute
1538
+ const paramsAttrIndex = attrs.findIndex(
1539
+ (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'params'
1610
1540
  )
1611
1541
 
1612
- // Include page and search in key to trigger refetch when they change
1613
- let keyExpression: any
1614
- if (info.searchEnabled && combinedStateVar) {
1615
- keyExpression = types.templateLiteral(
1616
- [
1617
- types.templateElement({
1618
- raw: `${info.dataSourceIdentifier}-page-`,
1619
- cooked: `${info.dataSourceIdentifier}-page-`,
1620
- }),
1621
- types.templateElement({ raw: '-search-', cooked: '-search-' }),
1622
- types.templateElement({ raw: '', cooked: '' }),
1623
- ],
1624
- [
1625
- types.memberExpression(types.identifier(combinedStateVar), types.identifier('page')),
1626
- types.memberExpression(
1627
- types.identifier(combinedStateVar),
1628
- types.identifier('debouncedQuery')
1629
- ),
1630
- ]
1631
- )
1632
- } else {
1633
- keyExpression = types.templateLiteral(
1634
- [
1635
- types.templateElement({
1636
- raw: `${info.dataSourceIdentifier}-`,
1637
- cooked: `${info.dataSourceIdentifier}-`,
1638
- }),
1639
- types.templateElement({ raw: '', cooked: '' }),
1640
- ],
1641
- [types.identifier(info.pageStateVar)]
1642
- )
1542
+ if (paramsAttrIndex === -1) {
1543
+ return
1643
1544
  }
1644
1545
 
1645
- const keyAttr = types.jsxAttribute(
1646
- types.jsxIdentifier('key'),
1647
- types.jsxExpressionContainer(keyExpression)
1648
- )
1546
+ const paramsAttr = attrs[paramsAttrIndex] as types.JSXAttribute
1649
1547
 
1650
- if (existingKeyAttr) {
1651
- const index = dataProviderJSX.openingElement.attributes.indexOf(existingKeyAttr)
1652
- dataProviderJSX.openingElement.attributes[index] = keyAttr
1653
- } else {
1654
- 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
1655
1555
  }
1656
1556
 
1657
- // For pagination, always create a fresh fetchData that calls the API route
1658
- // Get the resource definition to build the API URL
1659
- const resourceDefAttr = dataProviderJSX.openingElement.attributes.find(
1660
- (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'resourceDefinition'
1661
- )
1557
+ // Get the current params value expression
1558
+ let paramsExpression: types.Expression | null = null
1662
1559
 
1663
- if (resourceDefAttr && resourceDefAttr.value?.type === 'JSXExpressionContainer') {
1664
- const resourceDef = resourceDefAttr.value.expression
1665
- if (resourceDef.type === 'ObjectExpression') {
1666
- const dataSourceIdProp = (resourceDef.properties as any[]).find(
1667
- (p: any) => p.type === 'ObjectProperty' && p.key.value === 'dataSourceId'
1668
- )
1669
- const tableNameProp = (resourceDef.properties as any[]).find(
1670
- (p: any) => p.type === 'ObjectProperty' && p.key.value === 'tableName'
1671
- )
1672
- const dataSourceTypeProp = (resourceDef.properties as any[]).find(
1673
- (p: any) => p.type === 'ObjectProperty' && p.key.value === 'dataSourceType'
1674
- )
1560
+ if (paramsAttr.value?.type === 'JSXExpressionContainer') {
1561
+ paramsExpression = paramsAttr.value.expression as types.Expression
1562
+ }
1675
1563
 
1676
- if (dataSourceIdProp && tableNameProp && dataSourceTypeProp) {
1677
- const dataSourceId = dataSourceIdProp.value.value
1678
- const tableName = tableNameProp.value.value
1679
- const dataSourceType = dataSourceTypeProp.value.value
1680
- const fileName = `${dataSourceType}-${tableName}-${dataSourceId.substring(0, 8)}`
1564
+ if (!paramsExpression) {
1565
+ return
1566
+ }
1681
1567
 
1682
- // Create fetchData attribute with proper fetch chain wrapped in useCallback
1683
- const fetchDataValue = types.callExpression(types.identifier('useCallback'), [
1684
- types.arrowFunctionExpression(
1685
- [types.identifier('params')],
1686
- types.callExpression(
1687
- types.memberExpression(
1688
- types.callExpression(
1689
- types.memberExpression(
1690
- types.callExpression(types.identifier('fetch'), [
1691
- types.templateLiteral(
1692
- [
1693
- types.templateElement({
1694
- raw: `/api/${fileName}?`,
1695
- cooked: `/api/${fileName}?`,
1696
- }),
1697
- types.templateElement({ raw: '', cooked: '' }),
1698
- ],
1699
- [
1700
- types.newExpression(types.identifier('URLSearchParams'), [
1701
- types.identifier('params'),
1702
- ]),
1703
- ]
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
+ ])
1704
1617
  ),
1705
- types.objectExpression([
1706
- types.objectProperty(
1707
- types.identifier('headers'),
1708
- types.objectExpression([
1709
- types.objectProperty(
1710
- types.stringLiteral('Content-Type'),
1711
- types.stringLiteral('application/json')
1712
- ),
1713
- ])
1714
- ),
1715
- ]),
1716
1618
  ]),
1717
- types.identifier('then')
1718
- ),
1719
- [
1720
- types.arrowFunctionExpression(
1721
- [types.identifier('res')],
1722
- types.callExpression(
1723
- types.memberExpression(types.identifier('res'), types.identifier('json')),
1724
- []
1725
- )
1726
- ),
1727
- ]
1619
+ ]),
1620
+ types.identifier('then')
1728
1621
  ),
1729
- 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
+ ]
1730
1631
  ),
1731
- [
1732
- types.arrowFunctionExpression(
1733
- [types.identifier('response')],
1734
- types.optionalMemberExpression(
1735
- types.identifier('response'),
1736
- types.identifier('data'),
1737
- false,
1738
- true
1739
- )
1740
- ),
1741
- ]
1742
- )
1743
- ),
1744
- types.arrayExpression([]),
1745
- ])
1746
-
1747
- const newFetchDataAttr = types.jsxAttribute(
1748
- types.jsxIdentifier('fetchData'),
1749
- types.jsxExpressionContainer(fetchDataValue)
1750
- )
1751
-
1752
- // Remove existing fetchData attribute if present
1753
- const existingFetchDataIndex = dataProviderJSX.openingElement.attributes.findIndex(
1754
- (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'fetchData'
1755
- )
1756
-
1757
- if (existingFetchDataIndex !== -1) {
1758
- dataProviderJSX.openingElement.attributes[existingFetchDataIndex] = newFetchDataAttr
1759
- } else {
1760
- dataProviderJSX.openingElement.attributes.push(newFetchDataAttr)
1761
- }
1762
- }
1763
- }
1764
- }
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
+ )
1765
1651
  }
1766
1652
 
1767
- function findChildWithClass(node: any, classSubstring: string): string | null {
1768
- if (!node) {
1769
- return null
1770
- }
1771
-
1772
- if (node.type === 'JSXElement') {
1773
- const className = getClassName(node.openingElement?.attributes || [])
1774
- if (className && className.includes(classSubstring)) {
1775
- return className
1776
- }
1777
- }
1778
-
1779
- if (node.children) {
1780
- for (const child of node.children) {
1781
- const found = findChildWithClass(child, classSubstring)
1782
- if (found) {
1783
- return found
1784
- }
1785
- }
1786
- }
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
+ )
1787
1658
 
1788
- if (typeof node === 'object') {
1789
- for (const value of Object.values(node)) {
1790
- if (Array.isArray(value)) {
1791
- for (const item of value) {
1792
- const found = findChildWithClass(item, classSubstring)
1793
- if (found) {
1794
- return found
1795
- }
1796
- }
1797
- } else if (typeof value === 'object') {
1798
- const found = findChildWithClass(value, classSubstring)
1799
- if (found) {
1800
- return found
1801
- }
1802
- }
1803
- }
1804
- }
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
+ ])
1672
+ )
1673
+ )
1674
+ )
1675
+ )
1805
1676
 
1806
- return null
1677
+ // Add value
1678
+ inputNode.openingElement.attributes.push(
1679
+ types.jsxAttribute(
1680
+ types.jsxIdentifier('value'),
1681
+ types.jsxExpressionContainer(types.identifier(vars.searchQueryVar))
1682
+ )
1683
+ )
1807
1684
  }
1808
1685
 
1809
- function modifyPaginationButtons(
1810
- blockStatement: types.BlockStatement,
1811
- detectedPaginations: DetectedPagination[],
1812
- paginationInfos: ArrayMapperPaginationInfo[]
1686
+ function wirePaginationButtons(
1687
+ paginationNode: any,
1688
+ usage: DataSourceUsage,
1689
+ vars: ReturnType<typeof getStateVarsForUsage>
1813
1690
  ): void {
1814
- const modifiedButtons = new Set<any>()
1815
-
1816
- const modifyNode = (node: any): void => {
1691
+ // Find prev and next buttons
1692
+ const findButton = (node: any, direction: 'previous' | 'next'): any => {
1817
1693
  if (!node) {
1818
- return
1694
+ return null
1819
1695
  }
1820
1696
 
1821
1697
  if (node.type === 'JSXElement') {
1822
- const openingElement = node.openingElement
1823
- if (openingElement && openingElement.name && openingElement.name.type === 'JSXIdentifier') {
1824
- const className = getClassName(openingElement.attributes)
1825
-
1826
- if (className && !modifiedButtons.has(node)) {
1827
- for (let index = 0; index < detectedPaginations.length; index++) {
1828
- const detected = detectedPaginations[index]
1829
- const info = paginationInfos[index]
1830
-
1831
- if (!info) {
1832
- continue
1833
- }
1834
-
1835
- if (className === detected.prevButtonClass) {
1836
- convertToButton(node, info, 'prev')
1837
- modifiedButtons.add(node)
1838
- break
1839
- } else if (className === detected.nextButtonClass) {
1840
- convertToButton(node, info, 'next')
1841
- modifiedButtons.add(node)
1842
- break
1843
- }
1844
- }
1845
- }
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
1846
1704
  }
1847
1705
  }
1848
1706
 
1849
- if (typeof node === 'object') {
1850
- Object.values(node).forEach((value) => {
1851
- if (Array.isArray(value)) {
1852
- value.forEach((item) => modifyNode(item))
1853
- } else if (typeof value === 'object') {
1854
- modifyNode(value)
1707
+ if (node.children && Array.isArray(node.children)) {
1708
+ for (const c of node.children) {
1709
+ const found = findButton(c, direction)
1710
+ if (found) {
1711
+ return found
1855
1712
  }
1856
- })
1713
+ }
1857
1714
  }
1715
+ return null
1858
1716
  }
1859
1717
 
1860
- modifyNode(blockStatement)
1861
- }
1862
-
1863
- function modifySearchInputs(
1864
- blockStatement: types.BlockStatement,
1865
- detectedPaginations: DetectedPagination[],
1866
- paginationInfos: ArrayMapperPaginationInfo[]
1867
- ): void {
1868
- const modifiedInputs = new Set<any>()
1718
+ const prevButton = findButton(paginationNode, 'previous')
1719
+ const nextButton = findButton(paginationNode, 'next')
1869
1720
 
1870
- const modifyNode = (node: any): void => {
1871
- if (!node) {
1872
- return
1873
- }
1721
+ const isCombinedState = usage.category === 'paginated+search'
1874
1722
 
1875
- if (node.type === 'JSXElement') {
1876
- const openingElement = node.openingElement
1877
- if (openingElement && openingElement.name && openingElement.name.type === 'JSXIdentifier') {
1878
- const className = getClassName(openingElement.attributes)
1879
-
1880
- if (className && !modifiedInputs.has(node)) {
1881
- for (let index = 0; index < detectedPaginations.length; index++) {
1882
- const detected = detectedPaginations[index]
1883
- const info = paginationInfos[index]
1884
-
1885
- if (!info || !info.searchEnabled) {
1886
- continue
1887
- }
1888
-
1889
- if (className === detected.searchInputClass) {
1890
- addSearchInputHandlers(node, info)
1891
- modifiedInputs.add(node)
1892
- break
1893
- }
1894
- }
1895
- }
1896
- }
1723
+ if (prevButton) {
1724
+ // Change to button element
1725
+ prevButton.openingElement.name.name = 'button'
1726
+ if (prevButton.closingElement) {
1727
+ prevButton.closingElement.name.name = 'button'
1897
1728
  }
1898
1729
 
1899
- if (typeof node === 'object') {
1900
- Object.values(node).forEach((value) => {
1901
- if (Array.isArray(value)) {
1902
- value.forEach((item) => modifyNode(item))
1903
- } else if (typeof value === 'object') {
1904
- modifyNode(value)
1905
- }
1906
- })
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
+ )
1907
1736
  }
1908
- }
1909
-
1910
- modifyNode(blockStatement)
1911
- }
1912
-
1913
- function addSearchInputHandlers(jsxElement: any, info: ArrayMapperPaginationInfo): void {
1914
- if (!info.searchQueryVar || !info.setSearchQueryVar) {
1915
- return
1916
- }
1917
-
1918
- const openingElement = jsxElement.openingElement
1919
-
1920
- removeAttribute(openingElement.attributes, 'onChange')
1921
- removeAttribute(openingElement.attributes, 'value')
1922
-
1923
- const onChangeHandler = types.arrowFunctionExpression(
1924
- [types.identifier('e')],
1925
- types.callExpression(types.identifier(info.setSearchQueryVar!), [
1926
- types.memberExpression(
1927
- types.memberExpression(types.identifier('e'), types.identifier('target')),
1928
- types.identifier('value')
1929
- ),
1930
- ])
1931
- )
1932
-
1933
- openingElement.attributes.push(
1934
- types.jsxAttribute(
1935
- types.jsxIdentifier('onChange'),
1936
- types.jsxExpressionContainer(onChangeHandler)
1937
- )
1938
- )
1939
-
1940
- openingElement.attributes.push(
1941
- types.jsxAttribute(
1942
- types.jsxIdentifier('value'),
1943
- types.jsxExpressionContainer(types.identifier(info.searchQueryVar!))
1944
- )
1945
- )
1946
- }
1947
-
1948
- function convertToButton(
1949
- jsxElement: any,
1950
- info: ArrayMapperPaginationInfo,
1951
- buttonType: 'prev' | 'next'
1952
- ): void {
1953
- const openingElement = jsxElement.openingElement
1954
-
1955
- if (openingElement.name.type === 'JSXIdentifier') {
1956
- openingElement.name.name = 'button'
1957
- }
1958
- if (jsxElement.closingElement && jsxElement.closingElement.name.type === 'JSXIdentifier') {
1959
- jsxElement.closingElement.name.name = 'button'
1960
- }
1961
-
1962
- removeAttribute(openingElement.attributes, 'onClick')
1963
- removeAttribute(openingElement.attributes, 'disabled')
1964
- removeAttribute(openingElement.attributes, 'type')
1965
1737
 
1966
- const combinedStateVar = (info as any).combinedStateVar
1967
- const setCombinedStateVar = (info as any).setCombinedStateVar
1738
+ // Add onClick
1739
+ prevButton.openingElement.attributes = prevButton.openingElement.attributes.filter(
1740
+ (a: any) => a.name?.name !== 'onClick'
1741
+ )
1968
1742
 
1969
- let onClickHandler: any
1970
- if (info.searchEnabled && combinedStateVar && setCombinedStateVar) {
1971
- // Use combined state: update only the page property
1972
- onClickHandler =
1973
- buttonType === 'prev'
1974
- ? types.arrowFunctionExpression(
1975
- [],
1976
- types.callExpression(types.identifier(setCombinedStateVar), [
1977
- types.arrowFunctionExpression(
1978
- [types.identifier('state')],
1979
- types.objectExpression([
1980
- types.spreadElement(types.identifier('state')),
1981
- types.objectProperty(
1982
- types.identifier('page'),
1983
- types.callExpression(
1984
- types.memberExpression(types.identifier('Math'), types.identifier('max')),
1985
- [
1986
- types.numericLiteral(1),
1987
- types.binaryExpression(
1988
- '-',
1989
- types.memberExpression(
1990
- types.identifier('state'),
1991
- types.identifier('page')
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)
1992
1768
  ),
1993
- types.numericLiteral(1)
1994
- ),
1995
- ]
1996
- )
1997
- ),
1998
- ])
1999
- ),
2000
- ])
1769
+ ]
1770
+ )
1771
+ ),
1772
+ ])
1773
+ ),
1774
+ ])
1775
+ )
2001
1776
  )
2002
- : types.arrowFunctionExpression(
2003
- [],
2004
- types.callExpression(types.identifier(setCombinedStateVar), [
2005
- types.arrowFunctionExpression(
2006
- [types.identifier('state')],
2007
- types.objectExpression([
2008
- types.spreadElement(types.identifier('state')),
2009
- types.objectProperty(
2010
- types.identifier('page'),
2011
- types.binaryExpression(
2012
- '+',
2013
- types.memberExpression(types.identifier('state'), types.identifier('page')),
2014
- types.numericLiteral(1)
2015
- )
2016
- ),
2017
- ])
1777
+ )
1778
+ )
1779
+
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')
2018
1793
  ),
2019
- ])
1794
+ types.numericLiteral(1)
1795
+ )
2020
1796
  )
2021
- } else {
2022
- // Regular pagination (no search)
2023
- onClickHandler =
2024
- buttonType === 'prev'
2025
- ? types.arrowFunctionExpression(
2026
- [],
2027
- types.callExpression(types.identifier(info.setPageStateVar), [
2028
- types.arrowFunctionExpression(
2029
- [types.identifier('p')],
2030
- types.callExpression(
2031
- types.memberExpression(types.identifier('Math'), types.identifier('max')),
2032
- [
2033
- types.numericLiteral(1),
2034
- types.binaryExpression('-', types.identifier('p'), types.numericLiteral(1)),
2035
- ]
2036
- )
2037
- ),
2038
- ])
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
+ )
2039
1823
  )
2040
- : types.arrowFunctionExpression(
2041
- [],
2042
- types.callExpression(types.identifier(info.setPageStateVar), [
2043
- types.arrowFunctionExpression(
2044
- [types.identifier('p')],
2045
- types.binaryExpression('+', types.identifier('p'), types.numericLiteral(1))
2046
- ),
2047
- ])
1824
+ )
1825
+ )
1826
+
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
+ )
2048
1839
  )
1840
+ )
1841
+ )
1842
+ }
2049
1843
  }
2050
1844
 
2051
- openingElement.attributes.push(
2052
- types.jsxAttribute(types.jsxIdentifier('onClick'), types.jsxExpressionContainer(onClickHandler))
2053
- )
2054
- openingElement.attributes.push(
2055
- types.jsxAttribute(types.jsxIdentifier('type'), types.stringLiteral('button'))
2056
- )
1845
+ if (nextButton) {
1846
+ nextButton.openingElement.name.name = 'button'
1847
+ if (nextButton.closingElement) {
1848
+ nextButton.closingElement.name.name = 'button'
1849
+ }
1850
+
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
+ )
1856
+ }
2057
1857
 
2058
- // Add disabled attribute with simple page number checks
2059
- const maxPagesStateVar = (info as any).maxPagesStateVar
2060
- let disabledExpr: any
1858
+ nextButton.openingElement.attributes = nextButton.openingElement.attributes.filter(
1859
+ (a: any) => a.name?.name !== 'onClick'
1860
+ )
2061
1861
 
2062
- if (info.searchEnabled && combinedStateVar) {
2063
- disabledExpr =
2064
- buttonType === 'prev'
2065
- ? types.binaryExpression(
2066
- '<=',
2067
- types.memberExpression(types.identifier(combinedStateVar), types.identifier('page')),
2068
- types.numericLiteral(1)
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
+ )
1886
+ )
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')
1902
+ ),
1903
+ types.identifier(vars.maxPagesStateVar)
1904
+ )
2069
1905
  )
2070
- : types.binaryExpression(
2071
- '>=',
2072
- types.memberExpression(types.identifier(combinedStateVar), types.identifier('page')),
2073
- types.identifier(maxPagesStateVar)
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
+ )
2074
1922
  )
2075
- } else {
2076
- disabledExpr =
2077
- buttonType === 'prev'
2078
- ? types.binaryExpression('<=', types.identifier(info.pageStateVar), types.numericLiteral(1))
2079
- : types.binaryExpression(
2080
- '>=',
2081
- types.identifier(info.pageStateVar),
2082
- types.identifier(maxPagesStateVar)
1923
+ )
1924
+ )
1925
+
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
+ )
2083
1938
  )
1939
+ )
1940
+ )
1941
+ }
2084
1942
  }
2085
-
2086
- openingElement.attributes.push(
2087
- types.jsxAttribute(types.jsxIdentifier('disabled'), types.jsxExpressionContainer(disabledExpr))
2088
- )
2089
1943
  }
2090
1944
 
2091
- function getClassName(attributes: any[]): string | null {
2092
- const classNameAttr = attributes.find(
2093
- (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
2094
1951
  )
2095
- if (classNameAttr && classNameAttr.value && classNameAttr.value.type === 'StringLiteral') {
2096
- return classNameAttr.value.value
2097
- }
2098
- return null
2099
- }
2100
1952
 
2101
- function removeAttribute(attrs: any[], attributeName: string): void {
2102
- const index = attrs.findIndex(
2103
- (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === attributeName
2104
- )
2105
- if (index !== -1) {
2106
- attrs.splice(index, 1)
2107
- }
2108
- }
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
+ }
2109
1968
 
2110
- function modifyGetStaticPropsForPagination(
2111
- chunks: any[],
2112
- paginationInfos: ArrayMapperPaginationInfo[]
2113
- ): void {
2114
- const getStaticPropsChunk = chunks.find((chunk) => chunk.name === 'getStaticProps')
2115
- if (!getStaticPropsChunk || getStaticPropsChunk.type !== 'ast') {
2116
- 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
+ }
2117
1983
  }
1984
+ }
2118
1985
 
2119
- const exportDeclaration = getStaticPropsChunk.content as types.ExportNamedDeclaration
2120
- 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) {
2121
1989
  return
2122
1990
  }
2123
1991
 
2124
- const functionDeclaration = exportDeclaration.declaration as types.FunctionDeclaration
2125
- if (!functionDeclaration || functionDeclaration.type !== 'FunctionDeclaration') {
1992
+ const content = getStaticPropsChunk.content as types.ExportNamedDeclaration
1993
+ if (!content.declaration || content.declaration.type !== 'FunctionDeclaration') {
2126
1994
  return
2127
1995
  }
2128
1996
 
2129
- // Find Promise.all and add NEW fetchData calls for each paginated DataProvider
2130
- const functionBody = functionDeclaration.body.body
2131
- 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
2132
1999
  | types.TryStatement
2133
2000
  | undefined
2134
-
2135
- if (!tryBlock) {
2001
+ if (!tryStmt) {
2136
2002
  return
2137
2003
  }
2138
2004
 
2139
- const tryBody = tryBlock.block.body
2005
+ const tryBlock = tryStmt.block
2140
2006
 
2141
- // Find the Promise.all statement
2142
- const promiseAllStmt = tryBody.find(
2143
- (stmt: any) =>
2144
- stmt.type === 'VariableDeclaration' &&
2145
- stmt.declarations?.[0]?.init?.type === 'AwaitExpression' &&
2146
- stmt.declarations?.[0]?.init?.argument?.type === 'CallExpression' &&
2147
- stmt.declarations?.[0]?.init?.argument?.callee?.type === 'MemberExpression' &&
2148
- 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'
2149
2015
  ) as types.VariableDeclaration | undefined
2150
2016
 
2151
- if (!promiseAllStmt) {
2017
+ if (!promiseAllDecl) {
2152
2018
  return
2153
2019
  }
2154
2020
 
2155
- 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
2156
2023
  const promiseAllCall = awaitExpr.argument as types.CallExpression
2157
- const promiseArray = promiseAllCall.arguments[0] as types.ArrayExpression
2158
- const destructuringPattern = promiseAllStmt.declarations[0].id as types.ArrayPattern
2159
-
2160
- // Map import names to data source identifiers from existing fetchData calls
2161
- // Also track which indices to remove (non-paginated calls that will be replaced)
2162
- const importToDataSource = new Map<string, string>()
2163
- const indicesToRemove: number[] = []
2164
-
2165
- promiseArray.elements.forEach((element: any, index: number) => {
2166
- if (element && element.type === 'CallExpression') {
2167
- let fetchCallExpr = element
2168
-
2169
- // If wrapped in .catch(), unwrap it
2170
- if (
2171
- element.callee?.type === 'MemberExpression' &&
2172
- element.callee?.property?.name === 'catch' &&
2173
- element.callee?.object?.type === 'CallExpression'
2174
- ) {
2175
- fetchCallExpr = element.callee.object
2176
- }
2177
-
2178
- // Now find the .fetchData() call
2179
- if (
2180
- fetchCallExpr.callee?.type === 'MemberExpression' &&
2181
- fetchCallExpr.callee?.property?.name === 'fetchData' &&
2182
- fetchCallExpr.callee?.object?.type === 'Identifier'
2183
- ) {
2184
- const importName = fetchCallExpr.callee.object.name
2185
- const dataSourceVar = (destructuringPattern.elements[index] as types.Identifier).name
2186
-
2187
- // Check if this fetchData call has page/perPage params
2188
- const params = fetchCallExpr.arguments[0]
2189
- const hasPageParam =
2190
- params &&
2191
- params.type === 'ObjectExpression' &&
2192
- params.properties.some(
2193
- (prop: any) =>
2194
- prop.type === 'ObjectProperty' &&
2195
- (prop.key.name === 'page' || prop.key.name === 'perPage')
2196
- )
2024
+ const fetchesArray = promiseAllCall.arguments[0] as types.ArrayExpression
2197
2025
 
2198
- // If this is a data source that will be paginated but this call has NO pagination params,
2199
- // mark it for removal
2200
- if (
2201
- !hasPageParam &&
2202
- paginationInfos.some((info) => info.dataSourceIdentifier === dataSourceVar)
2203
- ) {
2204
- indicesToRemove.push(index)
2205
- }
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
+ }
2206
2033
 
2207
- importToDataSource.set(importName, dataSourceVar)
2208
- }
2209
- }
2210
- })
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
2211
2038
 
2212
- // Remove non-paginated fetchData calls in reverse order to preserve indices
2213
- indicesToRemove.reverse().forEach((index) => {
2214
- // Get the prop name BEFORE removing it
2215
- const propToRemove = (destructuringPattern.elements[index] as types.Identifier)?.name
2039
+ if (!propsProperty || propsProperty.value.type !== 'ObjectExpression') {
2040
+ return
2041
+ }
2216
2042
 
2217
- promiseArray.elements.splice(index, 1)
2218
- destructuringPattern.elements.splice(index, 1)
2043
+ const propsObj = propsProperty.value as types.ObjectExpression
2044
+ const arrayPattern = declarator.id as types.ArrayPattern
2219
2045
 
2220
- // Also remove from props in return statement
2221
- if (propToRemove) {
2222
- const foundReturnStmt = tryBody.find((stmt: any) => stmt.type === 'ReturnStatement') as
2223
- | types.ReturnStatement
2224
- | undefined
2046
+ // Track unique data sources for count fetching
2047
+ const dataSourcesNeedingCount = new Set<string>()
2225
2048
 
2226
- if (foundReturnStmt && foundReturnStmt.argument?.type === 'ObjectExpression') {
2227
- const propsProperty = (foundReturnStmt.argument as types.ObjectExpression).properties.find(
2228
- (prop: any) =>
2229
- prop.type === 'ObjectProperty' && (prop.key as types.Identifier).name === 'props'
2230
- ) 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)
2231
2058
 
2232
- if (propsProperty && propsProperty.value.type === 'ObjectExpression') {
2233
- const propsObject = propsProperty.value as types.ObjectExpression
2059
+ // Add fetch call
2060
+ const fetchParams: types.ObjectProperty[] = []
2234
2061
 
2235
- const propIndex = propsObject.properties.findIndex(
2236
- (prop: any) =>
2237
- prop.type === 'ObjectProperty' && (prop.key as types.Identifier).name === propToRemove
2238
- )
2239
- if (propIndex !== -1) {
2240
- propsObject.properties.splice(propIndex, 1)
2241
- }
2242
- }
2243
- }
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
+ )
2244
2074
  }
2245
- })
2246
-
2247
- // Add NEW fetchData calls for each paginated DataProvider
2248
- paginationInfos.forEach((info, index) => {
2249
- // Try exact match first, then case-insensitive match
2250
- let importName = Array.from(importToDataSource.entries()).find(
2251
- ([_, dataSourceVar]) => dataSourceVar === info.dataSourceIdentifier
2252
- )?.[0]
2253
-
2254
- if (!importName) {
2255
- // Try case-insensitive match
2256
- const normalizedIdentifier = info.dataSourceIdentifier.toLowerCase().replace(/[_-]/g, '')
2257
- importName = Array.from(importToDataSource.entries()).find(
2258
- ([_, dataSourceVar]) =>
2259
- dataSourceVar.toLowerCase().replace(/[_-]/g, '') === normalizedIdentifier
2260
- )?.[0]
2261
- }
2262
-
2263
- if (importName) {
2264
- const paginatedVarName = `${info.dataSourceIdentifier}_pg_${index}`
2265
-
2266
- const fetchParams = [
2267
- types.objectProperty(types.identifier('page'), types.numericLiteral(1)),
2268
- types.objectProperty(types.identifier('perPage'), types.numericLiteral(info.perPage)),
2269
- ]
2270
-
2271
- // Add queryColumns if they exist
2272
- if (info.queryColumns && info.queryColumns.length > 0) {
2273
- fetchParams.push(
2274
- types.objectProperty(
2275
- types.identifier('queryColumns'),
2276
- 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)))]
2277
2082
  )
2278
2083
  )
2279
- }
2280
-
2281
- // Create new fetchData call with pagination params
2282
- const newFetchDataCall = types.callExpression(
2283
- types.memberExpression(
2284
- types.callExpression(
2285
- types.memberExpression(types.identifier(importName), types.identifier('fetchData')),
2286
- [types.objectExpression(fetchParams)]
2287
- ),
2288
- types.identifier('catch')
2289
- ),
2290
- [
2291
- types.arrowFunctionExpression(
2292
- [types.identifier('error')],
2293
- types.blockStatement([
2294
- types.expressionStatement(
2295
- types.callExpression(
2296
- types.memberExpression(types.identifier('console'), types.identifier('error')),
2297
- [
2298
- types.stringLiteral(`Error fetching ${paginatedVarName}:`),
2299
- types.identifier('error'),
2300
- ]
2301
- )
2302
- ),
2303
- types.returnStatement(types.arrayExpression([])),
2304
- ])
2305
- ),
2306
- ]
2307
2084
  )
2308
-
2309
- promiseArray.elements.push(newFetchDataCall)
2310
- destructuringPattern.elements.push(types.identifier(paginatedVarName))
2311
2085
  }
2312
- })
2313
2086
 
2314
- // 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
+ )
2315
2091
 
2316
- // Deduplicate by data source identifier
2317
- const uniqueDataSources = new Set(paginationInfos.map((info) => info.dataSourceIdentifier))
2318
- const addedCountFetches = new Set<string>()
2092
+ if (existingFetchIndex === -1) {
2093
+ arrayPattern.elements.push(types.identifier(vars.propsPrefix))
2319
2094
 
2320
- uniqueDataSources.forEach((dataSourceId) => {
2321
- let importName = Array.from(importToDataSource.entries()).find(
2322
- ([_, dataSourceVar]) => dataSourceVar === dataSourceId
2323
- )?.[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
+ )
2324
2126
 
2325
- if (!importName) {
2326
- // Try case-insensitive match
2327
- const normalizedIdentifier = dataSourceId.toLowerCase().replace(/[_-]/g, '')
2328
- importName = Array.from(importToDataSource.entries()).find(
2329
- ([_, dataSourceVar]) =>
2330
- dataSourceVar.toLowerCase().replace(/[_-]/g, '') === normalizedIdentifier
2331
- )?.[0]
2127
+ // Add to props
2128
+ propsObj.properties.push(
2129
+ types.objectProperty(types.identifier(vars.propsPrefix), types.identifier(vars.propsPrefix))
2130
+ )
2332
2131
  }
2333
2132
 
2334
- if (importName && !addedCountFetches.has(dataSourceId)) {
2335
- const fetchCountCall = types.callExpression(
2336
- types.memberExpression(types.identifier(importName), types.identifier('fetchCount')),
2337
- []
2133
+ // Track for count fetching
2134
+ if (usage.paginated) {
2135
+ dataSourcesNeedingCount.add(
2136
+ `${usage.resourceDefinition.dataSourceType}:${usage.resourceDefinition.tableName}:${usage.resourceDefinition.dataSourceId}`
2338
2137
  )
2339
- promiseArray.elements.push(fetchCountCall)
2340
- destructuringPattern.elements.push(types.identifier(`${dataSourceId}_count`))
2341
- addedCountFetches.add(dataSourceId)
2342
2138
  }
2343
- })
2344
-
2345
- // Calculate and add maxPages before return
2346
- const returnStmt = tryBody.find((stmt: any) => stmt.type === 'ReturnStatement') as
2347
- | types.ReturnStatement
2348
- | undefined
2349
2139
 
2350
- if (returnStmt && returnStmt.argument?.type === 'ObjectExpression') {
2351
- const propsProperty = (returnStmt.argument as types.ObjectExpression).properties.find(
2352
- (prop: any) =>
2353
- prop.type === 'ObjectProperty' && (prop.key as types.Identifier).name === 'props'
2354
- ) 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`
2355
2144
 
2356
- if (propsProperty && propsProperty.value.type === 'ObjectExpression') {
2357
- const propsObject = propsProperty.value as types.ObjectExpression
2358
- const returnIndex = tryBody.indexOf(returnStmt)
2359
-
2360
- paginationInfos.forEach((info, index) => {
2361
- const paginatedVarName = `${info.dataSourceIdentifier}_pg_${index}`
2362
- const countVarName = `${info.dataSourceIdentifier}_count`
2363
- 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
+ )
2364
2150
 
2365
- const maxPagesCalc = types.variableDeclaration('const', [
2366
- types.variableDeclarator(
2367
- types.identifier(maxPagesVarName),
2368
- types.callExpression(
2369
- types.memberExpression(types.identifier('Math'), types.identifier('ceil')),
2370
- [
2371
- types.binaryExpression(
2372
- '/',
2373
- types.logicalExpression(
2374
- '||',
2375
- types.identifier(countVarName),
2376
- 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)
2377
2171
  ),
2378
- types.numericLiteral(info.perPage)
2379
- ),
2380
- ]
2381
- )
2382
- ),
2383
- ])
2384
-
2385
- tryBody.splice(returnIndex, 0, maxPagesCalc)
2172
+ ]
2173
+ )
2174
+ ),
2175
+ ])
2176
+ )
2386
2177
 
2387
- // Add both the paginated data and maxPages to props
2388
- propsObject.properties.push(
2178
+ // Add maxPages to props
2179
+ propsObj.properties.push(
2389
2180
  types.objectProperty(
2390
- types.identifier(paginatedVarName),
2391
- types.identifier(paginatedVarName)
2181
+ types.identifier(maxPagesPropName),
2182
+ types.identifier(maxPagesPropName)
2392
2183
  )
2393
2184
  )
2394
- propsObject.properties.push(
2395
- types.objectProperty(types.identifier(maxPagesVarName), types.identifier(maxPagesVarName))
2396
- )
2397
- })
2398
- }
2399
- }
2400
- }
2401
-
2402
- function createAPIRoutesForPaginatedDataSources(
2403
- uidlNode: any,
2404
- dataSources: any,
2405
- componentChunk: any,
2406
- extractedResources: any,
2407
- paginationInfos: ArrayMapperPaginationInfo[],
2408
- isComponent: boolean
2409
- ): void {
2410
- const paginatedDataSourceIds = new Set(paginationInfos.map((info) => info.dataSourceIdentifier))
2411
-
2412
- const searchEnabledDataSources = new Set(
2413
- paginationInfos.filter((info) => info.searchEnabled).map((info) => info.dataSourceIdentifier)
2414
- )
2415
-
2416
- const createdCountRoutes = new Set<string>()
2417
-
2418
- const traverseForDataSources = (node: any): void => {
2419
- if (!node) {
2420
- return
2421
- }
2422
-
2423
- if (node.type === 'data-source-list' || node.type === 'data-source-item') {
2424
- const renderProp = node.content.renderPropIdentifier
2425
-
2426
- if (renderProp && paginatedDataSourceIds.has(renderProp)) {
2427
- extractDataSourceIntoNextAPIFolder(node, dataSources, componentChunk, extractedResources)
2428
-
2429
- const hasSearch = searchEnabledDataSources.has(renderProp)
2430
- const needsCountRoute = isComponent || hasSearch
2431
-
2432
- if (needsCountRoute) {
2433
- const resourceDef = node.content.resourceDefinition
2434
- if (resourceDef) {
2435
- const dataSourceId = resourceDef.dataSourceId
2436
- const tableName = resourceDef.tableName
2437
- const dataSourceType = resourceDef.dataSourceType
2438
- const fileName = `${dataSourceType}-${tableName}-${dataSourceId.substring(0, 8)}`
2439
- const countFileName = `${fileName}-count`
2440
-
2441
- if (!createdCountRoutes.has(countFileName)) {
2442
- extractedResources[`api/${countFileName}`] = {
2443
- fileName: countFileName,
2444
- fileType: FileType.JS,
2445
- path: ['pages', 'api'],
2446
- content: `import dataSource from '../../utils/data-sources/${fileName}'
2447
-
2448
- export default dataSource.getCount
2449
- `,
2450
- }
2451
- createdCountRoutes.add(countFileName)
2452
- }
2453
- }
2454
- }
2455
2185
  }
2456
2186
  }
2187
+ })
2457
2188
 
2458
- if (node.content?.children) {
2459
- node.content.children.forEach((child: any) => traverseForDataSources(child))
2460
- }
2461
- }
2462
-
2463
- traverseForDataSources(uidlNode)
2464
- }
2465
-
2466
- function createAPIRoutesForSearchOnlyDataSources(
2467
- uidlNode: any,
2468
- dataSources: any,
2469
- componentChunk: any,
2470
- extractedResources: any,
2471
- searchOnlyDataSources: DetectedPagination[]
2472
- ): void {
2473
- const searchOnlyDataSourceIds = new Set(
2474
- searchOnlyDataSources.map((info) => info.dataSourceIdentifier)
2475
- )
2476
-
2477
- const traverseForDataSources = (node: any): void => {
2478
- 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) {
2479
2203
  return
2480
2204
  }
2481
2205
 
2482
- if (node.type === 'data-source-list' || node.type === 'data-source-item') {
2483
- const renderProp = node.content.renderPropIdentifier
2206
+ const countVarName = `${usage.dataSourceIdentifier}_count`
2484
2207
 
2485
- if (renderProp && searchOnlyDataSourceIds.has(renderProp)) {
2486
- extractDataSourceIntoNextAPIFolder(node, dataSources, componentChunk, extractedResources)
2487
- }
2488
- }
2208
+ // Check if count fetch already exists
2209
+ const existingCount = arrayPattern.elements.findIndex(
2210
+ (el: any) => el?.type === 'Identifier' && el.name === countVarName
2211
+ )
2489
2212
 
2490
- if (node.content?.children) {
2491
- 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
+ )
2492
2224
  }
2493
- }
2494
-
2495
- traverseForDataSources(uidlNode)
2225
+ })
2496
2226
  }
2497
-
2498
- export default createNextArrayMapperPaginationPlugin()