@teleporthq/teleport-plugin-next-data-source 0.40.15

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 (240) hide show
  1. package/ARRAY_MAPPER_PAGINATION.md +1128 -0
  2. package/LICENSE +21 -0
  3. package/README.md +40 -0
  4. package/SEARCH_IMPLEMENTATION_SUMMARY.md +983 -0
  5. package/__tests__/fetchers.test.ts +545 -0
  6. package/__tests__/integration.test.ts +561 -0
  7. package/__tests__/mocks.ts +241 -0
  8. package/__tests__/pagination.test.ts +31 -0
  9. package/__tests__/plugin.test.ts +577 -0
  10. package/__tests__/utils.test.ts +430 -0
  11. package/__tests__/validation.test.ts +348 -0
  12. package/dist/cjs/array-mapper-pagination.d.ts +32 -0
  13. package/dist/cjs/array-mapper-pagination.d.ts.map +1 -0
  14. package/dist/cjs/array-mapper-pagination.js +77 -0
  15. package/dist/cjs/array-mapper-pagination.js.map +1 -0
  16. package/dist/cjs/count-fetchers.d.ts +12 -0
  17. package/dist/cjs/count-fetchers.d.ts.map +1 -0
  18. package/dist/cjs/count-fetchers.js +46 -0
  19. package/dist/cjs/count-fetchers.js.map +1 -0
  20. package/dist/cjs/data-source-fetchers.d.ts +14 -0
  21. package/dist/cjs/data-source-fetchers.d.ts.map +1 -0
  22. package/dist/cjs/data-source-fetchers.js +185 -0
  23. package/dist/cjs/data-source-fetchers.js.map +1 -0
  24. package/dist/cjs/fetchers/airtable.d.ts +6 -0
  25. package/dist/cjs/fetchers/airtable.d.ts.map +1 -0
  26. package/dist/cjs/fetchers/airtable.js +27 -0
  27. package/dist/cjs/fetchers/airtable.js.map +1 -0
  28. package/dist/cjs/fetchers/clickhouse.d.ts +6 -0
  29. package/dist/cjs/fetchers/clickhouse.d.ts.map +1 -0
  30. package/dist/cjs/fetchers/clickhouse.js +29 -0
  31. package/dist/cjs/fetchers/clickhouse.js.map +1 -0
  32. package/dist/cjs/fetchers/csv-file.d.ts +7 -0
  33. package/dist/cjs/fetchers/csv-file.d.ts.map +1 -0
  34. package/dist/cjs/fetchers/csv-file.js +36 -0
  35. package/dist/cjs/fetchers/csv-file.js.map +1 -0
  36. package/dist/cjs/fetchers/firestore.d.ts +6 -0
  37. package/dist/cjs/fetchers/firestore.d.ts.map +1 -0
  38. package/dist/cjs/fetchers/firestore.js +35 -0
  39. package/dist/cjs/fetchers/firestore.js.map +1 -0
  40. package/dist/cjs/fetchers/google-sheets.d.ts +6 -0
  41. package/dist/cjs/fetchers/google-sheets.d.ts.map +1 -0
  42. package/dist/cjs/fetchers/google-sheets.js +30 -0
  43. package/dist/cjs/fetchers/google-sheets.js.map +1 -0
  44. package/dist/cjs/fetchers/index.d.ts +17 -0
  45. package/dist/cjs/fetchers/index.d.ts.map +1 -0
  46. package/dist/cjs/fetchers/index.js +56 -0
  47. package/dist/cjs/fetchers/index.js.map +1 -0
  48. package/dist/cjs/fetchers/javascript.d.ts +7 -0
  49. package/dist/cjs/fetchers/javascript.d.ts.map +1 -0
  50. package/dist/cjs/fetchers/javascript.js +40 -0
  51. package/dist/cjs/fetchers/javascript.js.map +1 -0
  52. package/dist/cjs/fetchers/mariadb.d.ts +3 -0
  53. package/dist/cjs/fetchers/mariadb.d.ts.map +1 -0
  54. package/dist/cjs/fetchers/mariadb.js +23 -0
  55. package/dist/cjs/fetchers/mariadb.js.map +1 -0
  56. package/dist/cjs/fetchers/mongodb.d.ts +7 -0
  57. package/dist/cjs/fetchers/mongodb.d.ts.map +1 -0
  58. package/dist/cjs/fetchers/mongodb.js +52 -0
  59. package/dist/cjs/fetchers/mongodb.js.map +1 -0
  60. package/dist/cjs/fetchers/mysql.d.ts +3 -0
  61. package/dist/cjs/fetchers/mysql.d.ts.map +1 -0
  62. package/dist/cjs/fetchers/mysql.js +30 -0
  63. package/dist/cjs/fetchers/mysql.js.map +1 -0
  64. package/dist/cjs/fetchers/postgresql.d.ts +3 -0
  65. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -0
  66. package/dist/cjs/fetchers/postgresql.js +25 -0
  67. package/dist/cjs/fetchers/postgresql.js.map +1 -0
  68. package/dist/cjs/fetchers/redis.d.ts +6 -0
  69. package/dist/cjs/fetchers/redis.d.ts.map +1 -0
  70. package/dist/cjs/fetchers/redis.js +46 -0
  71. package/dist/cjs/fetchers/redis.js.map +1 -0
  72. package/dist/cjs/fetchers/redshift.d.ts +2 -0
  73. package/dist/cjs/fetchers/redshift.d.ts.map +1 -0
  74. package/dist/cjs/fetchers/redshift.js +24 -0
  75. package/dist/cjs/fetchers/redshift.js.map +1 -0
  76. package/dist/cjs/fetchers/rest-api.d.ts +6 -0
  77. package/dist/cjs/fetchers/rest-api.d.ts.map +1 -0
  78. package/dist/cjs/fetchers/rest-api.js +58 -0
  79. package/dist/cjs/fetchers/rest-api.js.map +1 -0
  80. package/dist/cjs/fetchers/static-collection.d.ts +7 -0
  81. package/dist/cjs/fetchers/static-collection.d.ts.map +1 -0
  82. package/dist/cjs/fetchers/static-collection.js +24 -0
  83. package/dist/cjs/fetchers/static-collection.js.map +1 -0
  84. package/dist/cjs/fetchers/supabase.d.ts +7 -0
  85. package/dist/cjs/fetchers/supabase.d.ts.map +1 -0
  86. package/dist/cjs/fetchers/supabase.js +42 -0
  87. package/dist/cjs/fetchers/supabase.js.map +1 -0
  88. package/dist/cjs/fetchers/turso.d.ts +6 -0
  89. package/dist/cjs/fetchers/turso.d.ts.map +1 -0
  90. package/dist/cjs/fetchers/turso.js +25 -0
  91. package/dist/cjs/fetchers/turso.js.map +1 -0
  92. package/dist/cjs/index.d.ts +9 -0
  93. package/dist/cjs/index.d.ts.map +1 -0
  94. package/dist/cjs/index.js +325 -0
  95. package/dist/cjs/index.js.map +1 -0
  96. package/dist/cjs/pagination-plugin.d.ts +5 -0
  97. package/dist/cjs/pagination-plugin.d.ts.map +1 -0
  98. package/dist/cjs/pagination-plugin.js +1484 -0
  99. package/dist/cjs/pagination-plugin.js.map +1 -0
  100. package/dist/cjs/pagination-with-count.d.ts +6 -0
  101. package/dist/cjs/pagination-with-count.d.ts.map +1 -0
  102. package/dist/cjs/pagination-with-count.js +63 -0
  103. package/dist/cjs/pagination-with-count.js.map +1 -0
  104. package/dist/cjs/tsconfig.tsbuildinfo +1 -0
  105. package/dist/cjs/utils.d.ts +31 -0
  106. package/dist/cjs/utils.d.ts.map +1 -0
  107. package/dist/cjs/utils.js +763 -0
  108. package/dist/cjs/utils.js.map +1 -0
  109. package/dist/cjs/validation.d.ts +5 -0
  110. package/dist/cjs/validation.d.ts.map +1 -0
  111. package/dist/cjs/validation.js +29 -0
  112. package/dist/cjs/validation.js.map +1 -0
  113. package/dist/esm/array-mapper-pagination.d.ts +32 -0
  114. package/dist/esm/array-mapper-pagination.d.ts.map +1 -0
  115. package/dist/esm/array-mapper-pagination.js +72 -0
  116. package/dist/esm/array-mapper-pagination.js.map +1 -0
  117. package/dist/esm/count-fetchers.d.ts +12 -0
  118. package/dist/esm/count-fetchers.d.ts.map +1 -0
  119. package/dist/esm/count-fetchers.js +35 -0
  120. package/dist/esm/count-fetchers.js.map +1 -0
  121. package/dist/esm/data-source-fetchers.d.ts +14 -0
  122. package/dist/esm/data-source-fetchers.d.ts.map +1 -0
  123. package/dist/esm/data-source-fetchers.js +179 -0
  124. package/dist/esm/data-source-fetchers.js.map +1 -0
  125. package/dist/esm/fetchers/airtable.d.ts +6 -0
  126. package/dist/esm/fetchers/airtable.d.ts.map +1 -0
  127. package/dist/esm/fetchers/airtable.js +22 -0
  128. package/dist/esm/fetchers/airtable.js.map +1 -0
  129. package/dist/esm/fetchers/clickhouse.d.ts +6 -0
  130. package/dist/esm/fetchers/clickhouse.d.ts.map +1 -0
  131. package/dist/esm/fetchers/clickhouse.js +24 -0
  132. package/dist/esm/fetchers/clickhouse.js.map +1 -0
  133. package/dist/esm/fetchers/csv-file.d.ts +7 -0
  134. package/dist/esm/fetchers/csv-file.d.ts.map +1 -0
  135. package/dist/esm/fetchers/csv-file.js +30 -0
  136. package/dist/esm/fetchers/csv-file.js.map +1 -0
  137. package/dist/esm/fetchers/firestore.d.ts +6 -0
  138. package/dist/esm/fetchers/firestore.d.ts.map +1 -0
  139. package/dist/esm/fetchers/firestore.js +30 -0
  140. package/dist/esm/fetchers/firestore.js.map +1 -0
  141. package/dist/esm/fetchers/google-sheets.d.ts +6 -0
  142. package/dist/esm/fetchers/google-sheets.d.ts.map +1 -0
  143. package/dist/esm/fetchers/google-sheets.js +25 -0
  144. package/dist/esm/fetchers/google-sheets.js.map +1 -0
  145. package/dist/esm/fetchers/index.d.ts +17 -0
  146. package/dist/esm/fetchers/index.d.ts.map +1 -0
  147. package/dist/esm/fetchers/index.js +17 -0
  148. package/dist/esm/fetchers/index.js.map +1 -0
  149. package/dist/esm/fetchers/javascript.d.ts +7 -0
  150. package/dist/esm/fetchers/javascript.d.ts.map +1 -0
  151. package/dist/esm/fetchers/javascript.js +34 -0
  152. package/dist/esm/fetchers/javascript.js.map +1 -0
  153. package/dist/esm/fetchers/mariadb.d.ts +3 -0
  154. package/dist/esm/fetchers/mariadb.d.ts.map +1 -0
  155. package/dist/esm/fetchers/mariadb.js +18 -0
  156. package/dist/esm/fetchers/mariadb.js.map +1 -0
  157. package/dist/esm/fetchers/mongodb.d.ts +7 -0
  158. package/dist/esm/fetchers/mongodb.d.ts.map +1 -0
  159. package/dist/esm/fetchers/mongodb.js +46 -0
  160. package/dist/esm/fetchers/mongodb.js.map +1 -0
  161. package/dist/esm/fetchers/mysql.d.ts +3 -0
  162. package/dist/esm/fetchers/mysql.d.ts.map +1 -0
  163. package/dist/esm/fetchers/mysql.js +25 -0
  164. package/dist/esm/fetchers/mysql.js.map +1 -0
  165. package/dist/esm/fetchers/postgresql.d.ts +3 -0
  166. package/dist/esm/fetchers/postgresql.d.ts.map +1 -0
  167. package/dist/esm/fetchers/postgresql.js +20 -0
  168. package/dist/esm/fetchers/postgresql.js.map +1 -0
  169. package/dist/esm/fetchers/redis.d.ts +6 -0
  170. package/dist/esm/fetchers/redis.d.ts.map +1 -0
  171. package/dist/esm/fetchers/redis.js +41 -0
  172. package/dist/esm/fetchers/redis.js.map +1 -0
  173. package/dist/esm/fetchers/redshift.d.ts +2 -0
  174. package/dist/esm/fetchers/redshift.d.ts.map +1 -0
  175. package/dist/esm/fetchers/redshift.js +20 -0
  176. package/dist/esm/fetchers/redshift.js.map +1 -0
  177. package/dist/esm/fetchers/rest-api.d.ts +6 -0
  178. package/dist/esm/fetchers/rest-api.d.ts.map +1 -0
  179. package/dist/esm/fetchers/rest-api.js +53 -0
  180. package/dist/esm/fetchers/rest-api.js.map +1 -0
  181. package/dist/esm/fetchers/static-collection.d.ts +7 -0
  182. package/dist/esm/fetchers/static-collection.d.ts.map +1 -0
  183. package/dist/esm/fetchers/static-collection.js +18 -0
  184. package/dist/esm/fetchers/static-collection.js.map +1 -0
  185. package/dist/esm/fetchers/supabase.d.ts +7 -0
  186. package/dist/esm/fetchers/supabase.d.ts.map +1 -0
  187. package/dist/esm/fetchers/supabase.js +36 -0
  188. package/dist/esm/fetchers/supabase.js.map +1 -0
  189. package/dist/esm/fetchers/turso.d.ts +6 -0
  190. package/dist/esm/fetchers/turso.d.ts.map +1 -0
  191. package/dist/esm/fetchers/turso.js +20 -0
  192. package/dist/esm/fetchers/turso.js.map +1 -0
  193. package/dist/esm/index.d.ts +9 -0
  194. package/dist/esm/index.d.ts.map +1 -0
  195. package/dist/esm/index.js +306 -0
  196. package/dist/esm/index.js.map +1 -0
  197. package/dist/esm/pagination-plugin.d.ts +5 -0
  198. package/dist/esm/pagination-plugin.d.ts.map +1 -0
  199. package/dist/esm/pagination-plugin.js +1457 -0
  200. package/dist/esm/pagination-plugin.js.map +1 -0
  201. package/dist/esm/pagination-with-count.d.ts +6 -0
  202. package/dist/esm/pagination-with-count.d.ts.map +1 -0
  203. package/dist/esm/pagination-with-count.js +34 -0
  204. package/dist/esm/pagination-with-count.js.map +1 -0
  205. package/dist/esm/tsconfig.tsbuildinfo +1 -0
  206. package/dist/esm/utils.d.ts +31 -0
  207. package/dist/esm/utils.d.ts.map +1 -0
  208. package/dist/esm/utils.js +722 -0
  209. package/dist/esm/utils.js.map +1 -0
  210. package/dist/esm/validation.d.ts +5 -0
  211. package/dist/esm/validation.d.ts.map +1 -0
  212. package/dist/esm/validation.js +25 -0
  213. package/dist/esm/validation.js.map +1 -0
  214. package/package.json +33 -0
  215. package/src/array-mapper-pagination.ts +113 -0
  216. package/src/count-fetchers.ts +99 -0
  217. package/src/data-source-fetchers.ts +313 -0
  218. package/src/fetchers/airtable.ts +153 -0
  219. package/src/fetchers/clickhouse.ts +127 -0
  220. package/src/fetchers/csv-file.ts +163 -0
  221. package/src/fetchers/firestore.ts +138 -0
  222. package/src/fetchers/google-sheets.ts +189 -0
  223. package/src/fetchers/index.ts +32 -0
  224. package/src/fetchers/javascript.ts +150 -0
  225. package/src/fetchers/mariadb.ts +230 -0
  226. package/src/fetchers/mongodb.ts +239 -0
  227. package/src/fetchers/mysql.ts +237 -0
  228. package/src/fetchers/postgresql.ts +247 -0
  229. package/src/fetchers/redis.ts +152 -0
  230. package/src/fetchers/redshift.ts +138 -0
  231. package/src/fetchers/rest-api.ts +148 -0
  232. package/src/fetchers/static-collection.ts +149 -0
  233. package/src/fetchers/supabase.ts +246 -0
  234. package/src/fetchers/turso.ts +131 -0
  235. package/src/index.ts +352 -0
  236. package/src/pagination-plugin.ts +2335 -0
  237. package/src/pagination-with-count.ts +89 -0
  238. package/src/utils.ts +1013 -0
  239. package/src/validation.ts +32 -0
  240. package/tsconfig.json +9 -0
package/src/utils.ts ADDED
@@ -0,0 +1,1013 @@
1
+ import {
2
+ UIDLDataSourceItemNode,
3
+ UIDLDataSourceListNode,
4
+ ChunkDefinition,
5
+ GeneratorOptions,
6
+ FileType,
7
+ UIDLDataSource,
8
+ DataSourceType,
9
+ ChunkType,
10
+ } from '@teleporthq/teleport-types'
11
+ import * as types from '@babel/types'
12
+ import { ASTUtils } from '@teleporthq/teleport-plugin-common'
13
+ import { StringUtils } from '@teleporthq/teleport-shared'
14
+ import { generateDataSourceFetcherWithCore } from './data-source-fetchers'
15
+
16
+ const VALID_DATA_SOURCE_TYPES: DataSourceType[] = [
17
+ 'rest-api',
18
+ 'postgresql',
19
+ 'mysql',
20
+ 'mariadb',
21
+ 'amazon-redshift',
22
+ 'mongodb',
23
+ 'cockroachdb',
24
+ 'tidb',
25
+ 'redis',
26
+ 'firestore',
27
+ 'clickhouse',
28
+ 'airtable',
29
+ 'supabase',
30
+ 'turso',
31
+ 'javascript',
32
+ 'google-sheets',
33
+ 'csv-file',
34
+ 'static-collection',
35
+ ]
36
+
37
+ export const sanitizeFileName = (input: string): string => {
38
+ if (!input || typeof input !== 'string') {
39
+ return 'unknown'
40
+ }
41
+
42
+ return (
43
+ input
44
+ // Remove path traversal attempts
45
+ .replace(/\.\./g, '')
46
+ .replace(/[\/\\]/g, '-')
47
+ // Remove invalid filename characters
48
+ .replace(/[<>:"|?*\x00-\x1F]/g, '')
49
+ // Replace spaces with dashes
50
+ .replace(/\s+/g, '-')
51
+ // Remove leading/trailing dashes and dots
52
+ .replace(/^[-_.]+|[-_.]+$/g, '')
53
+ // Limit length to prevent filesystem issues
54
+ .substring(0, 200) || 'unknown'
55
+ )
56
+ }
57
+
58
+ export const isNonEmptyString = (value: unknown): value is string => {
59
+ return typeof value === 'string' && value.trim().length > 0
60
+ }
61
+
62
+ export const hoistLoadingFromRepeater = (dataSourceNode: any): void => {
63
+ if (!dataSourceNode || !dataSourceNode.content) {
64
+ return
65
+ }
66
+
67
+ const children = dataSourceNode.children || []
68
+
69
+ for (const child of children) {
70
+ if (child.type === 'cms-list-repeater' && child.content?.nodes?.loading) {
71
+ if (!dataSourceNode.content.nodes) {
72
+ dataSourceNode.content.nodes = {}
73
+ }
74
+
75
+ if (
76
+ !dataSourceNode.content.nodes.loading &&
77
+ child.content.nodes.loading.content?.children?.length > 0
78
+ ) {
79
+ dataSourceNode.content.nodes.loading = child.content.nodes.loading
80
+ }
81
+
82
+ break
83
+ }
84
+ }
85
+
86
+ if (dataSourceNode.content.nodes?.success?.content?.children) {
87
+ for (const child of dataSourceNode.content.nodes.success.content.children) {
88
+ if (child.type === 'cms-list-repeater' && child.content?.nodes?.loading) {
89
+ if (
90
+ !dataSourceNode.content.nodes.loading &&
91
+ child.content.nodes.loading.content?.children?.length > 0
92
+ ) {
93
+ dataSourceNode.content.nodes.loading = child.content.nodes.loading
94
+ }
95
+
96
+ break
97
+ }
98
+ }
99
+ }
100
+ }
101
+
102
+ // tslint:disable-next-line:no-any
103
+ export const validateResourceDefinition = (
104
+ resourceDefinition: any
105
+ ): {
106
+ isValid: boolean
107
+ error?: string
108
+ } => {
109
+ if (!resourceDefinition || typeof resourceDefinition !== 'object') {
110
+ return { isValid: false, error: 'Resource definition is missing or invalid' }
111
+ }
112
+
113
+ const { dataSourceId, tableName, dataSourceType } = resourceDefinition
114
+
115
+ if (!isNonEmptyString(dataSourceId)) {
116
+ return { isValid: false, error: 'Data source ID is missing or invalid' }
117
+ }
118
+
119
+ if (!isNonEmptyString(dataSourceType)) {
120
+ return { isValid: false, error: 'Data source type is missing or invalid' }
121
+ }
122
+
123
+ if (!VALID_DATA_SOURCE_TYPES.includes(dataSourceType as DataSourceType)) {
124
+ return { isValid: false, error: `Invalid data source type: ${dataSourceType}` }
125
+ }
126
+
127
+ // tableName is optional for some data sources (like REST API, JavaScript, etc.)
128
+ if (tableName !== undefined && !isNonEmptyString(tableName)) {
129
+ return { isValid: false, error: 'Table name must be a non-empty string when provided' }
130
+ }
131
+
132
+ return { isValid: true }
133
+ }
134
+
135
+ // tslint:disable-next-line:no-any
136
+ export const validateNodeContent = (content: any): { isValid: boolean; error?: string } => {
137
+ if (!content || typeof content !== 'object') {
138
+ return { isValid: false, error: 'Node content is missing or invalid' }
139
+ }
140
+
141
+ if (!content.resourceDefinition) {
142
+ return { isValid: false, error: 'Resource definition is missing from node content' }
143
+ }
144
+
145
+ // key is optional - we can generate one from the resource definition if missing
146
+
147
+ return { isValid: true }
148
+ }
149
+
150
+ export const validateDataSourceConfig = (
151
+ dataSource: UIDLDataSource
152
+ ): { isValid: boolean; error?: string } => {
153
+ if (!dataSource || typeof dataSource !== 'object') {
154
+ return { isValid: false, error: 'Data source is missing or invalid' }
155
+ }
156
+
157
+ if (!isNonEmptyString(dataSource.id)) {
158
+ return { isValid: false, error: 'Data source ID is missing' }
159
+ }
160
+
161
+ if (!isNonEmptyString(dataSource.type)) {
162
+ return { isValid: false, error: 'Data source type is missing' }
163
+ }
164
+
165
+ if (!dataSource.config || typeof dataSource.config !== 'object') {
166
+ return { isValid: false, error: 'Data source config is missing or invalid' }
167
+ }
168
+
169
+ return { isValid: true }
170
+ }
171
+
172
+ export const generateSafeFileName = (
173
+ dataSourceType: string,
174
+ tableName: string,
175
+ dataSourceId: string
176
+ ): string => {
177
+ const sanitizedType = sanitizeFileName(dataSourceType)
178
+ const sanitizedTable = sanitizeFileName(tableName || 'data')
179
+ const sanitizedId = sanitizeFileName(dataSourceId)
180
+
181
+ // Create a unique identifier using parts of the dataSourceId
182
+ const shortId = sanitizedId.substring(0, 8)
183
+
184
+ const baseName = `${sanitizedType}-${sanitizedTable}-${shortId}`
185
+ return StringUtils.camelCaseToDashCase(baseName)
186
+ }
187
+
188
+ export const extractDataSourceIntoNextAPIFolder = (
189
+ node: UIDLDataSourceItemNode | UIDLDataSourceListNode,
190
+ dataSources: Record<string, UIDLDataSource>,
191
+ componentChunk: ChunkDefinition,
192
+ extractedResources: GeneratorOptions['extractedResources']
193
+ ) => {
194
+ try {
195
+ // Validate node content structure
196
+ const contentValidation = validateNodeContent(node.content)
197
+ if (!contentValidation.isValid) {
198
+ return
199
+ }
200
+
201
+ const { resourceDefinition } = node.content
202
+
203
+ // Validate resource definition
204
+ const resourceValidation = validateResourceDefinition(resourceDefinition)
205
+ if (!resourceValidation.isValid) {
206
+ return
207
+ }
208
+
209
+ const { dataSourceId, tableName, dataSourceType } = resourceDefinition
210
+
211
+ // Check if dataSources object exists
212
+ if (!dataSources || typeof dataSources !== 'object') {
213
+ return
214
+ }
215
+
216
+ // Check if data source exists
217
+ const dataSource = dataSources[dataSourceId]
218
+ if (!dataSource) {
219
+ return
220
+ }
221
+
222
+ // Validate data source configuration
223
+ const configValidation = validateDataSourceConfig(dataSource)
224
+ if (!configValidation.isValid) {
225
+ return
226
+ }
227
+
228
+ // Check if component chunk has meta and nodesLookup
229
+ if (!componentChunk.meta || !componentChunk.meta.nodesLookup) {
230
+ return
231
+ }
232
+
233
+ // Generate safe file name
234
+ const fileName = generateSafeFileName(dataSourceType, tableName || 'data', dataSourceId)
235
+
236
+ // Check if file name is valid
237
+ if (!fileName || fileName === 'unknown') {
238
+ return
239
+ }
240
+
241
+ // Find JSX node by searching through nodesLookup
242
+ // The JSX generator creates keys like: ds-{dataSourceId}-{timestamp}
243
+ // We need to find the node that matches our resourceDefinition
244
+ // IMPORTANT: Match by both dataSourceId AND renderPropIdentifier to handle multiple DataProviders with same data source
245
+ let jsxNode: types.JSXElement | null = null
246
+ const targetRenderProp = node.content?.renderPropIdentifier
247
+
248
+ // tslint:disable-next-line:no-any
249
+ for (const jsxElement of Object.values(componentChunk.meta.nodesLookup)) {
250
+ // tslint:disable-next-line:no-any
251
+ if ((jsxElement as any).type === 'JSXElement') {
252
+ const attrs = (jsxElement as types.JSXElement).openingElement.attributes
253
+
254
+ // Look for resourceDefinition attribute
255
+ // tslint:disable-next-line:no-any
256
+ const resourceDefAttr = attrs.find(
257
+ (attr) =>
258
+ (attr as any).type === 'JSXAttribute' &&
259
+ (attr as types.JSXAttribute).name.name === 'resourceDefinition'
260
+ ) as types.JSXAttribute | undefined
261
+
262
+ // Look for name attribute to match with renderPropIdentifier
263
+ // tslint:disable-next-line:no-any
264
+ const nameAttr = attrs.find(
265
+ (attr) =>
266
+ (attr as any).type === 'JSXAttribute' &&
267
+ (attr as types.JSXAttribute).name.name === 'name'
268
+ ) as types.JSXAttribute | undefined
269
+
270
+ if (
271
+ resourceDefAttr &&
272
+ resourceDefAttr.value &&
273
+ resourceDefAttr.value.type === 'JSXExpressionContainer'
274
+ ) {
275
+ const expr = resourceDefAttr.value.expression
276
+ if (expr.type === 'ObjectExpression') {
277
+ // Check if this matches our resourceDefinition
278
+ const props = expr.properties as types.ObjectProperty[]
279
+ // tslint:disable-next-line:no-any
280
+ const idProp = props.find((p: any) => p.key?.value === 'dataSourceId')
281
+ // tslint:disable-next-line:no-any
282
+ const idValue = (idProp as any)?.value?.value
283
+
284
+ // Match by dataSourceId
285
+ const dataSourceMatches = idValue === dataSourceId
286
+
287
+ // Also check name prop matches renderPropIdentifier
288
+ let renderPropMatches = !targetRenderProp // If no targetRenderProp, don't check it
289
+
290
+ if (targetRenderProp && nameAttr && nameAttr.value) {
291
+ // The name attribute value is usually a StringLiteral or JSXExpressionContainer
292
+ if (nameAttr.value.type === 'StringLiteral') {
293
+ if (nameAttr.value.value === targetRenderProp) {
294
+ renderPropMatches = true
295
+ }
296
+ } else if (nameAttr.value.type === 'JSXExpressionContainer') {
297
+ const nameExpr = nameAttr.value.expression
298
+ if (nameExpr.type === 'StringLiteral' && nameExpr.value === targetRenderProp) {
299
+ renderPropMatches = true
300
+ }
301
+ }
302
+ }
303
+
304
+ // Before matching, check if this node already has fetchData - if so, skip it
305
+ // This is crucial for handling multiple DataProviders with same dataSourceId and name
306
+ const alreadyHasFetchData = attrs.some(
307
+ (attr) => (attr as types.JSXAttribute).name?.name === 'fetchData'
308
+ )
309
+
310
+ if (dataSourceMatches && renderPropMatches && !alreadyHasFetchData) {
311
+ jsxNode = jsxElement as types.JSXElement
312
+ break
313
+ } else if (dataSourceMatches && renderPropMatches && alreadyHasFetchData) {
314
+ // Continue searching for the next matching node without fetchData
315
+ }
316
+ }
317
+ }
318
+ }
319
+ }
320
+
321
+ if (!jsxNode || jsxNode.type !== 'JSXElement') {
322
+ return
323
+ }
324
+
325
+ // Ensure opening element and attributes exist
326
+ if (!jsxNode.openingElement || !Array.isArray(jsxNode.openingElement.attributes)) {
327
+ return
328
+ }
329
+
330
+ // Check if this node has already been processed (has fetchData attribute)
331
+ const existingFetchData = jsxNode.openingElement.attributes.find(
332
+ (attr) => (attr as types.JSXAttribute).name?.name === 'fetchData'
333
+ )
334
+
335
+ if (existingFetchData) {
336
+ return
337
+ }
338
+
339
+ // Check if there are resource params
340
+ const resourceParams = node.content.resource?.params || {}
341
+ const hasParams = Object.keys(resourceParams).length > 0
342
+
343
+ // Build resource path - use template literal if params exist
344
+ let resourcePath: types.StringLiteral | types.TemplateLiteral
345
+ if (hasParams) {
346
+ resourcePath = types.templateLiteral(
347
+ [
348
+ types.templateElement({ raw: `/api/${fileName}?`, cooked: `/api/${fileName}?` }),
349
+ types.templateElement({ raw: '', cooked: '' }),
350
+ ],
351
+ [types.newExpression(types.identifier('URLSearchParams'), [types.identifier('params')])]
352
+ )
353
+ } else {
354
+ resourcePath = types.stringLiteral(`/api/${fileName}`)
355
+ }
356
+
357
+ const fetchAST = types.callExpression(
358
+ types.memberExpression(
359
+ types.callExpression(types.identifier('fetch'), [
360
+ resourcePath,
361
+ types.objectExpression([
362
+ types.objectProperty(
363
+ types.identifier('headers'),
364
+ types.objectExpression([
365
+ types.objectProperty(
366
+ types.stringLiteral('Content-Type'),
367
+ types.stringLiteral('application/json')
368
+ ),
369
+ ])
370
+ ),
371
+ ]),
372
+ ]),
373
+ types.identifier('then')
374
+ ),
375
+ [
376
+ types.arrowFunctionExpression(
377
+ [types.identifier('res')],
378
+ types.callExpression(
379
+ types.memberExpression(types.identifier('res'), types.identifier('json')),
380
+ []
381
+ )
382
+ ),
383
+ ]
384
+ )
385
+
386
+ const dataExpression = ASTUtils.generateMemberExpressionASTFromPath([
387
+ 'response',
388
+ 'data',
389
+ ]) as types.OptionalMemberExpression
390
+
391
+ // If there are params, the arrow function should accept params
392
+ const resourceAST = types.arrowFunctionExpression(
393
+ hasParams ? [types.identifier('params')] : [],
394
+ types.callExpression(types.memberExpression(fetchAST, types.identifier('then'), false), [
395
+ types.arrowFunctionExpression([types.identifier('response')], dataExpression),
396
+ ])
397
+ )
398
+
399
+ jsxNode.openingElement.attributes.unshift(
400
+ types.jSXAttribute(
401
+ types.jsxIdentifier('fetchData'),
402
+ types.jsxExpressionContainer(resourceAST)
403
+ )
404
+ )
405
+
406
+ const hasPersistDataAttr = jsxNode.openingElement.attributes.some(
407
+ (attr) =>
408
+ attr.type === 'JSXAttribute' &&
409
+ (attr as types.JSXAttribute).name?.name === 'persistDataDuringLoading'
410
+ )
411
+
412
+ if (!hasPersistDataAttr) {
413
+ jsxNode.openingElement.attributes.push(
414
+ types.jsxAttribute(
415
+ types.jsxIdentifier('persistDataDuringLoading'),
416
+ types.jsxExpressionContainer(types.booleanLiteral(true))
417
+ )
418
+ )
419
+ }
420
+
421
+ // Ensure extracted resources object exists
422
+ if (!extractedResources || typeof extractedResources !== 'object') {
423
+ return
424
+ }
425
+
426
+ // Check if a utils file already exists for this data source (from getStaticProps)
427
+ // If so, create an API route that re-exports handler from it to avoid code duplication
428
+ if (extractedResources[`utils/${fileName}`]) {
429
+ const apiRouteCode = `import dataSourceModule from '../../utils/data-sources/${fileName}'
430
+
431
+ export default dataSourceModule.handler
432
+ `
433
+
434
+ extractedResources[`api/${fileName}`] = {
435
+ fileName,
436
+ fileType: FileType.JS,
437
+ path: ['pages', 'api'],
438
+ content: apiRouteCode,
439
+ }
440
+ return
441
+ }
442
+
443
+ // Generate fetcher code with BOTH fetchData and handler
444
+ let fetcherCode: string
445
+ try {
446
+ fetcherCode = generateDataSourceFetcherWithCore(dataSource, tableName || '')
447
+ } catch (error) {
448
+ return
449
+ }
450
+
451
+ extractedResources[`api/${fileName}`] = {
452
+ fileName,
453
+ fileType: FileType.JS,
454
+ path: ['pages', 'api'],
455
+ content: fetcherCode,
456
+ }
457
+ } catch (error) {
458
+ // Catch any unexpected errors to prevent plugin from crashing
459
+ }
460
+ }
461
+
462
+ export const isEmbeddedDataSource = (dataSourceType: string): boolean => {
463
+ return ['javascript', 'csv-file', 'static-collection'].includes(dataSourceType)
464
+ }
465
+
466
+ export const replaceSecretReference = (
467
+ value: unknown,
468
+ options: { templateLiteral?: boolean } = {}
469
+ ): string => {
470
+ const { templateLiteral = false } = options
471
+ // Handle null and undefined
472
+ if (value === null) {
473
+ return 'null'
474
+ }
475
+
476
+ if (value === undefined) {
477
+ return 'undefined'
478
+ }
479
+
480
+ // Handle non-string values
481
+ if (typeof value !== 'string') {
482
+ try {
483
+ return JSON.stringify(value)
484
+ } catch (error) {
485
+ // Handle circular references or non-serializable values
486
+ return 'null'
487
+ }
488
+ }
489
+
490
+ // Check if it's a secret reference
491
+ if (value.startsWith('teleporthq.secrets.')) {
492
+ const envVar = value.replace('teleporthq.secrets.', '')
493
+
494
+ // Validate environment variable name
495
+ // Must be alphanumeric, underscores, and start with letter or underscore
496
+ if (/^[A-Z_][A-Z0-9_]*$/i.test(envVar)) {
497
+ return templateLiteral ? `\${process.env.${envVar}}` : `process.env.${envVar}`
498
+ } else {
499
+ // Invalid env var name, use as regular string
500
+ return JSON.stringify(value)
501
+ }
502
+ }
503
+
504
+ // Regular string value
505
+ try {
506
+ return JSON.stringify(value)
507
+ } catch (error) {
508
+ // Fallback for any serialization issues
509
+ return '""'
510
+ }
511
+ }
512
+
513
+ export const sanitizeNumericParam = (value: unknown, defaultValue: number = 0): number => {
514
+ if (typeof value === 'number' && !isNaN(value) && isFinite(value)) {
515
+ return Math.max(0, Math.floor(value))
516
+ }
517
+
518
+ if (typeof value === 'string') {
519
+ const parsed = parseInt(value, 10)
520
+ if (!isNaN(parsed) && isFinite(parsed)) {
521
+ return Math.max(0, parsed)
522
+ }
523
+ }
524
+
525
+ return defaultValue
526
+ }
527
+
528
+ export const sanitizePort = (port: unknown, defaultPort: number): number => {
529
+ const sanitized = sanitizeNumericParam(port, defaultPort)
530
+ // Ensure port is in valid range (1-65535)
531
+ if (sanitized < 1 || sanitized > 65535) {
532
+ return defaultPort
533
+ }
534
+ return sanitized
535
+ }
536
+
537
+ export const isValidUrl = (url: unknown): boolean => {
538
+ if (typeof url !== 'string' || !url) {
539
+ return false
540
+ }
541
+
542
+ try {
543
+ const parsed = new URL(url)
544
+ // Check for valid protocols
545
+ return ['http:', 'https:', 'mongodb:', 'redis:', 'postgresql:', 'mysql:'].includes(
546
+ parsed.protocol
547
+ )
548
+ } catch {
549
+ return false
550
+ }
551
+ }
552
+
553
+ export const sanitizeIdentifier = (identifier: unknown): string => {
554
+ if (typeof identifier !== 'string' || !identifier) {
555
+ return ''
556
+ }
557
+
558
+ // Remove dangerous characters, only allow safe ones
559
+ return identifier.replace(/[^a-zA-Z0-9_-]/g, '').substring(0, 64)
560
+ }
561
+
562
+ // tslint:disable-next-line:no-any
563
+ export const extractDataSourceIntoGetStaticProps = (
564
+ node: UIDLDataSourceItemNode | UIDLDataSourceListNode,
565
+ dataSources: Record<string, UIDLDataSource>,
566
+ componentChunk: ChunkDefinition,
567
+ getStaticPropsChunk: any,
568
+ chunks: any[],
569
+ extractedResources: GeneratorOptions['extractedResources'],
570
+ dependencies: Record<string, any>
571
+ ): { success: boolean; chunk?: any } => {
572
+ try {
573
+ // Validate node content
574
+ const contentValidation = validateNodeContent(node.content)
575
+ if (!contentValidation.isValid) {
576
+ return { success: false }
577
+ }
578
+
579
+ const { resourceDefinition } = node.content
580
+ const resourceValidation = validateResourceDefinition(resourceDefinition)
581
+ if (!resourceValidation.isValid) {
582
+ return { success: false }
583
+ }
584
+
585
+ const { dataSourceId, tableName, dataSourceType } = resourceDefinition
586
+
587
+ if (!dataSources || typeof dataSources !== 'object') {
588
+ return { success: false }
589
+ }
590
+
591
+ const dataSource = dataSources[dataSourceId]
592
+ if (!dataSource) {
593
+ return { success: false }
594
+ }
595
+
596
+ const configValidation = validateDataSourceConfig(dataSource)
597
+ if (!configValidation.isValid) {
598
+ return { success: false }
599
+ }
600
+
601
+ if (!componentChunk.meta || !componentChunk.meta.nodesLookup) {
602
+ return { success: false }
603
+ }
604
+
605
+ // Generate prop key first
606
+ const sanitizedDsName = StringUtils.dashCaseToCamelCase(
607
+ sanitizeFileName(dataSource.name || dataSourceId)
608
+ )
609
+ const sanitizedTableName = StringUtils.dashCaseToCamelCase(
610
+ sanitizeFileName(tableName || 'data')
611
+ )
612
+ const propKey = `${sanitizedDsName}_${sanitizedTableName}_data`
613
+
614
+ // Find ALL JSX nodes matching this dataSourceId AND tableName and add initialData to ALL of them
615
+ const matchingJsxNodes: types.JSXElement[] = []
616
+
617
+ // Helper function to recursively traverse the AST and find all matching JSXElements
618
+ const traverseAST = (astNode: any) => {
619
+ if (!astNode || typeof astNode !== 'object') {
620
+ return
621
+ }
622
+
623
+ if (astNode.type === 'JSXElement') {
624
+ const jsxElement = astNode as types.JSXElement
625
+ const attrs = jsxElement.openingElement.attributes
626
+
627
+ const resourceDefAttr = attrs.find(
628
+ (attr) =>
629
+ (attr as any).type === 'JSXAttribute' &&
630
+ (attr as types.JSXAttribute).name.name === 'resourceDefinition'
631
+ ) as types.JSXAttribute | undefined
632
+
633
+ if (
634
+ resourceDefAttr &&
635
+ resourceDefAttr.value &&
636
+ resourceDefAttr.value.type === 'JSXExpressionContainer'
637
+ ) {
638
+ const expr = resourceDefAttr.value.expression
639
+ if (expr.type === 'ObjectExpression') {
640
+ const props = expr.properties as types.ObjectProperty[]
641
+ // tslint:disable-next-line:no-any
642
+ const idProp = props.find((p: any) => p.key?.value === 'dataSourceId')
643
+ // tslint:disable-next-line:no-any
644
+ const idValue = (idProp as any)?.value?.value
645
+
646
+ // Also check tableName to ensure we're matching the right data source
647
+ // tslint:disable-next-line:no-any
648
+ const tableNameProp = props.find((p: any) => p.key?.value === 'tableName')
649
+ // tslint:disable-next-line:no-any
650
+ const tableNameValue = (tableNameProp as any)?.value?.value
651
+
652
+ if (idValue === dataSourceId && tableNameValue === tableName) {
653
+ matchingJsxNodes.push(jsxElement)
654
+ }
655
+ }
656
+ }
657
+ }
658
+
659
+ // Recursively traverse all properties
660
+ for (const key in astNode) {
661
+ if (astNode.hasOwnProperty(key)) {
662
+ const value = astNode[key]
663
+ if (Array.isArray(value)) {
664
+ value.forEach((item) => traverseAST(item))
665
+ } else if (typeof value === 'object') {
666
+ traverseAST(value)
667
+ }
668
+ }
669
+ }
670
+ }
671
+
672
+ // Traverse the entire component AST content
673
+ traverseAST(componentChunk.content)
674
+
675
+ if (matchingJsxNodes.length === 0) {
676
+ return { success: false }
677
+ }
678
+
679
+ // Update ALL matching JSX nodes with initialData
680
+ for (const jsxNode of matchingJsxNodes) {
681
+ // For SSR/SSG with initialData, rename 'children' to 'renderSuccess'
682
+ const childrenAttrIndex = jsxNode.openingElement.attributes.findIndex(
683
+ (attr) => (attr as types.JSXAttribute).name?.name === 'children'
684
+ )
685
+
686
+ if (childrenAttrIndex !== -1) {
687
+ const childrenAttr = jsxNode.openingElement.attributes[
688
+ childrenAttrIndex
689
+ ] as types.JSXAttribute
690
+ const renderSuccessAttr = types.jsxAttribute(
691
+ types.jsxIdentifier('renderSuccess'),
692
+ childrenAttr.value
693
+ )
694
+ jsxNode.openingElement.attributes[childrenAttrIndex] = renderSuccessAttr
695
+ }
696
+
697
+ // Remove existing initialData and persistDataDuringLoading attributes to avoid duplicates
698
+ jsxNode.openingElement.attributes = jsxNode.openingElement.attributes.filter(
699
+ (attr) =>
700
+ (attr as types.JSXAttribute).name?.name !== 'initialData' &&
701
+ (attr as types.JSXAttribute).name?.name !== 'persistDataDuringLoading'
702
+ )
703
+
704
+ // Add initialData attribute
705
+ const initialDataAttr = types.jsxAttribute(
706
+ types.jsxIdentifier('initialData'),
707
+ types.jsxExpressionContainer(
708
+ types.memberExpression(types.identifier('props'), types.identifier(propKey))
709
+ )
710
+ )
711
+ jsxNode.openingElement.attributes.push(initialDataAttr)
712
+
713
+ // Add persistDataDuringLoading={true}
714
+ const persistDataAttr = types.jsxAttribute(
715
+ types.jsxIdentifier('persistDataDuringLoading'),
716
+ types.jsxExpressionContainer(types.booleanLiteral(true))
717
+ )
718
+ jsxNode.openingElement.attributes.push(persistDataAttr)
719
+ }
720
+
721
+ // Generate safe file name for the fetcher
722
+ const fileName = generateSafeFileName(dataSourceType, tableName || 'data', dataSourceId)
723
+
724
+ if (!fileName || fileName === 'unknown') {
725
+ return { success: false }
726
+ }
727
+
728
+ // Generate fetcher code with core function
729
+ let fetcherCode: string
730
+ try {
731
+ fetcherCode = generateDataSourceFetcherWithCore(dataSource, tableName || '')
732
+ } catch (error) {
733
+ // tslint:disable-next-line:no-console
734
+ console.error(`Failed to generate fetcher for ${dataSourceType} (${dataSourceId}):`, error)
735
+ return { success: false }
736
+ }
737
+
738
+ // Ensure extracted resources object exists
739
+ if (!extractedResources || typeof extractedResources !== 'object') {
740
+ return { success: false }
741
+ }
742
+
743
+ // Add the fetcher to utils folder (for server-side use)
744
+ // Use a unique key to avoid conflicts with API routes
745
+ extractedResources[`utils/${fileName}`] = {
746
+ fileName,
747
+ fileType: FileType.JS,
748
+ path: ['utils', 'data-sources'],
749
+ content: fetcherCode,
750
+ }
751
+
752
+ // Add dependency for the fetcher
753
+ const fetcherImportName = StringUtils.dashCaseToCamelCase(fileName)
754
+ dependencies[fetcherImportName] = {
755
+ type: 'local',
756
+ path: `../utils/data-sources/${fileName}`,
757
+ }
758
+
759
+ // Build params object from resource params
760
+ // tslint:disable-next-line:no-any
761
+ const resourceParams = (node.content as any).resource?.params || {}
762
+ const paramsProperties: types.ObjectProperty[] = []
763
+
764
+ // tslint:disable-next-line:no-any
765
+ Object.entries(resourceParams).forEach(([key, value]: [string, any]) => {
766
+ if (value.type === 'static') {
767
+ let astValue: any
768
+
769
+ if (value.content === null || value.content === undefined) {
770
+ astValue = types.nullLiteral()
771
+ } else if (Array.isArray(value.content)) {
772
+ // Handle array values (like queryColumns)
773
+ astValue = types.arrayExpression(
774
+ value.content.map((item: any) => {
775
+ if (typeof item === 'string') {
776
+ return types.stringLiteral(item)
777
+ }
778
+ if (typeof item === 'number') {
779
+ return types.numericLiteral(item)
780
+ }
781
+ if (typeof item === 'boolean') {
782
+ return types.booleanLiteral(item)
783
+ }
784
+ return types.nullLiteral()
785
+ })
786
+ )
787
+ } else if (typeof value.content === 'string') {
788
+ astValue = types.stringLiteral(value.content)
789
+ } else if (typeof value.content === 'number') {
790
+ astValue = types.numericLiteral(value.content)
791
+ } else if (typeof value.content === 'boolean') {
792
+ astValue = types.booleanLiteral(value.content)
793
+ } else {
794
+ astValue = types.nullLiteral()
795
+ }
796
+
797
+ paramsProperties.push(types.objectProperty(types.stringLiteral(key), astValue))
798
+ }
799
+ // Note: We don't handle 'expr' or 'dynamic' params in getStaticProps
800
+ // as those should be detected by hasResourceDynamicParams check earlier
801
+ })
802
+
803
+ const fetchCallExpression = types.callExpression(
804
+ types.memberExpression(types.identifier(fetcherImportName), types.identifier('fetchData')),
805
+ [types.objectExpression(paramsProperties)]
806
+ )
807
+
808
+ const safeFetchExpression = createSafeFetchExpression(fetchCallExpression, propKey)
809
+
810
+ // Get response value path (if specified)
811
+ const responseMemberAST = node.content.valuePath
812
+ ? ASTUtils.generateMemberExpressionASTFromPath([
813
+ propKey,
814
+ ...ASTUtils.parseValuePath(node.content.valuePath),
815
+ ])
816
+ : types.identifier(propKey)
817
+
818
+ // Create or update getStaticProps chunk
819
+ let tryBlock: types.TryStatement | null = null
820
+
821
+ if (!getStaticPropsChunk) {
822
+ tryBlock = types.tryStatement(
823
+ types.blockStatement([
824
+ types.returnStatement(
825
+ types.objectExpression([
826
+ types.objectProperty(
827
+ types.identifier('props'),
828
+ types.objectExpression([
829
+ types.objectProperty(types.identifier(propKey), responseMemberAST, false, false),
830
+ ])
831
+ ),
832
+ types.objectProperty(types.identifier('revalidate'), types.numericLiteral(1)),
833
+ ])
834
+ ),
835
+ ]),
836
+ types.catchClause(
837
+ types.identifier('error'),
838
+ types.blockStatement([
839
+ types.expressionStatement(
840
+ types.callExpression(
841
+ types.memberExpression(types.identifier('console'), types.identifier('error')),
842
+ [types.stringLiteral('Error in getStaticProps:'), types.identifier('error')]
843
+ )
844
+ ),
845
+ types.returnStatement(
846
+ types.objectExpression([
847
+ types.objectProperty(types.identifier('props'), types.objectExpression([])),
848
+ ])
849
+ ),
850
+ ])
851
+ )
852
+ )
853
+
854
+ getStaticPropsChunk = {
855
+ name: 'getStaticProps',
856
+ type: ChunkType.AST,
857
+ fileType: FileType.JS,
858
+ content: types.exportNamedDeclaration(
859
+ types.functionDeclaration(
860
+ types.identifier('getStaticProps'),
861
+ [types.identifier('context')],
862
+ types.blockStatement([tryBlock]),
863
+ false,
864
+ true
865
+ )
866
+ ),
867
+ linkAfter: ['jsx-component'],
868
+ }
869
+
870
+ chunks.push(getStaticPropsChunk)
871
+ } else {
872
+ // Update existing getStaticProps
873
+ const functionDeclaration = (getStaticPropsChunk.content as types.ExportNamedDeclaration)
874
+ .declaration as types.FunctionDeclaration
875
+ const functionBody = functionDeclaration.body.body
876
+ tryBlock = functionBody.find(
877
+ (subNode) => subNode.type === 'TryStatement'
878
+ ) as types.TryStatement
879
+
880
+ if (!tryBlock) {
881
+ return { success: false }
882
+ }
883
+ }
884
+
885
+ if (!tryBlock) {
886
+ return { success: false }
887
+ }
888
+
889
+ // Ensure props include current data source
890
+ const returnStatement: types.ReturnStatement = tryBlock.block.body.find(
891
+ (subNode) => subNode.type === 'ReturnStatement'
892
+ ) as types.ReturnStatement
893
+
894
+ if (!returnStatement) {
895
+ return { success: false }
896
+ }
897
+
898
+ const propsObject = (returnStatement.argument as types.ObjectExpression).properties.find(
899
+ (property) => ((property as types.ObjectProperty).key as types.Identifier).name === 'props'
900
+ ) as types.ObjectProperty
901
+
902
+ const propsValue = propsObject.value as types.ObjectExpression
903
+
904
+ // Check if propKey already exists in the parallel fetch metadata
905
+ const parallelFetchMeta = getStaticPropsChunk.meta?.parallelFetch as
906
+ | ParallelFetchMeta
907
+ | undefined
908
+ const existingInFetchMeta = parallelFetchMeta?.names.includes(propKey)
909
+
910
+ // Always ensure both the fetch and the prop are in sync
911
+ if (!existingInFetchMeta) {
912
+ // Add to parallel fetch metadata
913
+ registerParallelFetch(getStaticPropsChunk, tryBlock, propKey, safeFetchExpression)
914
+
915
+ // Check if prop exists in return object
916
+ const existingProp = propsValue.properties.find(
917
+ (prop) =>
918
+ prop.type === 'ObjectProperty' &&
919
+ prop.key.type === 'Identifier' &&
920
+ (prop.key as types.Identifier).name === propKey
921
+ )
922
+
923
+ // Add prop if it doesn't exist
924
+ if (!existingProp) {
925
+ propsValue.properties.unshift(
926
+ types.objectProperty(types.identifier(propKey), responseMemberAST, false, false)
927
+ )
928
+ }
929
+ }
930
+
931
+ return { success: true, chunk: getStaticPropsChunk }
932
+ } catch (error) {
933
+ return { success: false }
934
+ }
935
+ }
936
+
937
+ interface ParallelFetchMeta {
938
+ names: string[]
939
+ expressions: types.Expression[]
940
+ declaration?: types.VariableDeclaration
941
+ }
942
+
943
+ const createSafeFetchExpression = (
944
+ fetchCallExpression: types.CallExpression,
945
+ label: string
946
+ ): types.Expression => {
947
+ const errorIdentifier = types.identifier('error')
948
+
949
+ const catchHandler = types.arrowFunctionExpression(
950
+ [errorIdentifier],
951
+ types.blockStatement([
952
+ types.expressionStatement(
953
+ types.callExpression(
954
+ types.memberExpression(types.identifier('console'), types.identifier('error')),
955
+ [types.stringLiteral(`Error fetching ${label}:`), errorIdentifier]
956
+ )
957
+ ),
958
+ types.returnStatement(types.arrayExpression([])),
959
+ ])
960
+ )
961
+
962
+ return types.callExpression(
963
+ types.memberExpression(fetchCallExpression, types.identifier('catch')),
964
+ [catchHandler]
965
+ )
966
+ }
967
+
968
+ const registerParallelFetch = (
969
+ getStaticPropsChunk: ChunkDefinition,
970
+ tryBlock: types.TryStatement,
971
+ propKey: string,
972
+ expression: types.Expression
973
+ ) => {
974
+ if (!getStaticPropsChunk.meta) {
975
+ getStaticPropsChunk.meta = {}
976
+ }
977
+
978
+ const meta =
979
+ (getStaticPropsChunk.meta.parallelFetchData as ParallelFetchMeta) ??
980
+ ((getStaticPropsChunk.meta.parallelFetchData = {
981
+ names: [],
982
+ expressions: [],
983
+ }) as ParallelFetchMeta)
984
+
985
+ meta.names.push(propKey)
986
+ meta.expressions.push(expression)
987
+
988
+ updateParallelFetchStatement(tryBlock, meta)
989
+ }
990
+
991
+ const updateParallelFetchStatement = (tryBlock: types.TryStatement, meta: ParallelFetchMeta) => {
992
+ if (meta.declaration) {
993
+ const existingIndex = tryBlock.block.body.indexOf(meta.declaration)
994
+ if (existingIndex !== -1) {
995
+ tryBlock.block.body.splice(existingIndex, 1)
996
+ }
997
+ }
998
+
999
+ const promiseAllCall = types.awaitExpression(
1000
+ types.callExpression(
1001
+ types.memberExpression(types.identifier('Promise'), types.identifier('all')),
1002
+ [types.arrayExpression(meta.expressions.map((expression) => expression))]
1003
+ )
1004
+ )
1005
+
1006
+ const arrayPattern = types.arrayPattern(meta.names.map((name) => types.identifier(name)))
1007
+
1008
+ meta.declaration = types.variableDeclaration('const', [
1009
+ types.variableDeclarator(arrayPattern, promiseAllCall),
1010
+ ])
1011
+
1012
+ tryBlock.block.body.unshift(meta.declaration)
1013
+ }