@bravostudioai/react 0.1.0

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 (127) hide show
  1. package/bin/encore-lib.js +3 -0
  2. package/dist/_virtual/_commonjsHelpers.js +7 -0
  3. package/dist/_virtual/_commonjsHelpers.js.map +1 -0
  4. package/dist/_virtual/main.js +8 -0
  5. package/dist/_virtual/main.js.map +1 -0
  6. package/dist/_virtual/main2.js +5 -0
  7. package/dist/_virtual/main2.js.map +1 -0
  8. package/dist/app.js +9 -0
  9. package/dist/app.js.map +1 -0
  10. package/dist/cli/commands/download.js +82 -0
  11. package/dist/cli/commands/download.js.map +1 -0
  12. package/dist/cli/commands/generate.js +1526 -0
  13. package/dist/cli/commands/generate.js.map +1 -0
  14. package/dist/cli.js +25 -0
  15. package/dist/cli.js.map +1 -0
  16. package/dist/components/DynamicComponent.js +24 -0
  17. package/dist/components/DynamicComponent.js.map +1 -0
  18. package/dist/components/EncoreApp.js +259 -0
  19. package/dist/components/EncoreApp.js.map +1 -0
  20. package/dist/components/EncoreErrorBoundary.js +33 -0
  21. package/dist/components/EncoreErrorBoundary.js.map +1 -0
  22. package/dist/components/EncoreLoadingFallback.js +20 -0
  23. package/dist/components/EncoreLoadingFallback.js.map +1 -0
  24. package/dist/components.js +1454 -0
  25. package/dist/components.js.map +1 -0
  26. package/dist/constants.d.ts +3 -0
  27. package/dist/constants.d.ts.map +1 -0
  28. package/dist/contexts/EncoreActionContext.js +6 -0
  29. package/dist/contexts/EncoreActionContext.js.map +1 -0
  30. package/dist/contexts/EncoreAppContext.js +9 -0
  31. package/dist/contexts/EncoreAppContext.js.map +1 -0
  32. package/dist/contexts/EncoreBindingContext.js +6 -0
  33. package/dist/contexts/EncoreBindingContext.js.map +1 -0
  34. package/dist/contexts/EncoreComponentIdContext.js +8 -0
  35. package/dist/contexts/EncoreComponentIdContext.js.map +1 -0
  36. package/dist/contexts/EncoreRepeatingContainerContext.js +6 -0
  37. package/dist/contexts/EncoreRepeatingContainerContext.js.map +1 -0
  38. package/dist/hooks/usePusherUpdates.js +60 -0
  39. package/dist/hooks/usePusherUpdates.js.map +1 -0
  40. package/dist/index.js +16 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/lib/dynamicModules.js +132 -0
  43. package/dist/lib/dynamicModules.js.map +1 -0
  44. package/dist/lib/fetcher.js +58 -0
  45. package/dist/lib/fetcher.js.map +1 -0
  46. package/dist/lib/localMode.js +21 -0
  47. package/dist/lib/localMode.js.map +1 -0
  48. package/dist/lib/packages.js +18 -0
  49. package/dist/lib/packages.js.map +1 -0
  50. package/dist/node_modules/dotenv/lib/main.js +198 -0
  51. package/dist/node_modules/dotenv/lib/main.js.map +1 -0
  52. package/dist/node_modules/dotenv/package.json.js +8 -0
  53. package/dist/node_modules/dotenv/package.json.js.map +1 -0
  54. package/dist/packages/encore-lib/constants.js +6 -0
  55. package/dist/packages/encore-lib/constants.js.map +1 -0
  56. package/dist/src/app.d.ts +5 -0
  57. package/dist/src/app.d.ts.map +1 -0
  58. package/dist/src/cli/commands/download.d.ts +2 -0
  59. package/dist/src/cli/commands/download.d.ts.map +1 -0
  60. package/dist/src/cli/commands/generate.d.ts +2 -0
  61. package/dist/src/cli/commands/generate.d.ts.map +1 -0
  62. package/dist/src/cli/index.d.ts +2 -0
  63. package/dist/src/cli/index.d.ts.map +1 -0
  64. package/dist/src/components/DynamicComponent.d.ts +12 -0
  65. package/dist/src/components/DynamicComponent.d.ts.map +1 -0
  66. package/dist/src/components/EncoreApp.d.ts +27 -0
  67. package/dist/src/components/EncoreApp.d.ts.map +1 -0
  68. package/dist/src/components/EncoreErrorBoundary.d.ts +17 -0
  69. package/dist/src/components/EncoreErrorBoundary.d.ts.map +1 -0
  70. package/dist/src/components/EncoreLoadingFallback.d.ts +4 -0
  71. package/dist/src/components/EncoreLoadingFallback.d.ts.map +1 -0
  72. package/dist/src/components.d.ts +4 -0
  73. package/dist/src/components.d.ts.map +1 -0
  74. package/dist/src/contexts/EncoreActionContext.d.ts +13 -0
  75. package/dist/src/contexts/EncoreActionContext.d.ts.map +1 -0
  76. package/dist/src/contexts/EncoreAppContext.d.ts +8 -0
  77. package/dist/src/contexts/EncoreAppContext.d.ts.map +1 -0
  78. package/dist/src/contexts/EncoreBindingContext.d.ts +5 -0
  79. package/dist/src/contexts/EncoreBindingContext.d.ts.map +1 -0
  80. package/dist/src/contexts/EncoreComponentIdContext.d.ts +8 -0
  81. package/dist/src/contexts/EncoreComponentIdContext.d.ts.map +1 -0
  82. package/dist/src/contexts/EncoreRepeatingContainerContext.d.ts +21 -0
  83. package/dist/src/contexts/EncoreRepeatingContainerContext.d.ts.map +1 -0
  84. package/dist/src/hooks/useAuthRedirect.d.ts +3 -0
  85. package/dist/src/hooks/useAuthRedirect.d.ts.map +1 -0
  86. package/dist/src/hooks/usePusherUpdates.d.ts +18 -0
  87. package/dist/src/hooks/usePusherUpdates.d.ts.map +1 -0
  88. package/dist/src/index.d.ts +8 -0
  89. package/dist/src/index.d.ts.map +1 -0
  90. package/dist/src/lib/dynamicModules.d.ts +8 -0
  91. package/dist/src/lib/dynamicModules.d.ts.map +1 -0
  92. package/dist/src/lib/fetcher.d.ts +5 -0
  93. package/dist/src/lib/fetcher.d.ts.map +1 -0
  94. package/dist/src/lib/localMode.d.ts +3 -0
  95. package/dist/src/lib/localMode.d.ts.map +1 -0
  96. package/dist/src/lib/packages.d.ts +6 -0
  97. package/dist/src/lib/packages.d.ts.map +1 -0
  98. package/dist/src/stores/useEncoreState.d.ts +33 -0
  99. package/dist/src/stores/useEncoreState.d.ts.map +1 -0
  100. package/dist/stores/useEncoreState.js +70 -0
  101. package/dist/stores/useEncoreState.js.map +1 -0
  102. package/package.json +60 -0
  103. package/src/AGENTS.md +161 -0
  104. package/src/README.md +110 -0
  105. package/src/app.ts +5 -0
  106. package/src/cli/commands/download.ts +133 -0
  107. package/src/cli/commands/generate.ts +3045 -0
  108. package/src/cli/index.ts +35 -0
  109. package/src/components/DynamicComponent.tsx +40 -0
  110. package/src/components/EncoreApp.tsx +759 -0
  111. package/src/components/EncoreErrorBoundary.tsx +49 -0
  112. package/src/components/EncoreLoadingFallback.tsx +25 -0
  113. package/src/components.tsx +3155 -0
  114. package/src/contexts/EncoreActionContext.ts +18 -0
  115. package/src/contexts/EncoreAppContext.ts +13 -0
  116. package/src/contexts/EncoreBindingContext.ts +6 -0
  117. package/src/contexts/EncoreComponentIdContext.ts +12 -0
  118. package/src/contexts/EncoreRepeatingContainerContext.ts +30 -0
  119. package/src/hooks/useAuthRedirect.ts +63 -0
  120. package/src/hooks/usePusherUpdates.ts +156 -0
  121. package/src/index.ts +16 -0
  122. package/src/lib/dynamicModules.ts +193 -0
  123. package/src/lib/fetcher.ts +108 -0
  124. package/src/lib/localMode.ts +30 -0
  125. package/src/lib/moduleRegistry.ts +24 -0
  126. package/src/lib/packages.ts +33 -0
  127. package/src/stores/useEncoreState.ts +121 -0
@@ -0,0 +1,3045 @@
1
+
2
+
3
+ import { writeFile, mkdir, readFile } from "fs/promises";
4
+ import { join, dirname } from "path";
5
+ import { existsSync } from "fs";
6
+ import dotenv from "dotenv";
7
+ import {
8
+ CONST_APPS_SERVICE_URL,
9
+ CONST_COMPONENTS_CDN_URL,
10
+ } from "../../../constants";
11
+
12
+ dotenv.config();
13
+
14
+ // Default apps service URL (can be overridden with APPS_SERVICE_URL env var)
15
+ const APPS_SERVICE_URL =
16
+ process.env.APPS_SERVICE_URL ||
17
+ process.env.VITE_APPS_SERVICE_URL ||
18
+ CONST_APPS_SERVICE_URL ||
19
+ "https://apps-service-dev.bravostudio.app";
20
+ console.log(`Using APPS_SERVICE_URL: ${APPS_SERVICE_URL}`);
21
+ const COMPONENTS_CDN_URL =
22
+ CONST_COMPONENTS_CDN_URL || "https://apps-public-dev.bravostudio.app";
23
+
24
+ interface ComponentInfo {
25
+ id: string;
26
+ name: string;
27
+ type: string;
28
+ tags: string[];
29
+ propName: string; // Sanitized name for prop
30
+ propType: string; // TypeScript type for the prop
31
+ }
32
+
33
+ interface ArrayContainerInfo {
34
+ id: string;
35
+ name: string;
36
+ propName: string;
37
+ components: ComponentInfo[];
38
+ }
39
+
40
+ interface SliderInfo {
41
+ id: string;
42
+ name: string;
43
+ arrayContainer: ArrayContainerInfo | null;
44
+ }
45
+
46
+ interface InputGroupInfo {
47
+ groupName: string;
48
+ groupType: string; // "single" for radio button behavior
49
+ elements: Array<{
50
+ id: string;
51
+ name: string;
52
+ }>;
53
+ }
54
+
55
+ interface FormInfo {
56
+ formId: string;
57
+ formName: string;
58
+ submitButtonId?: string;
59
+ inputs: Array<{
60
+ id: string;
61
+ name: string;
62
+ type: string;
63
+ propName: string; // Sanitized and qualified prop name
64
+ _parentPath?: string[]; // Temporary: parent path for qualification
65
+ }>;
66
+ }
67
+
68
+ interface SelectInputInfo {
69
+ id: string;
70
+ name: string;
71
+ propName: string; // e.g., "contractType" for value prop, generates onContractTypeChange handler
72
+ _parentPath?: string[]; // For qualification if duplicates
73
+ }
74
+
75
+ interface ActionButtonInfo {
76
+ id: string;
77
+ name: string;
78
+ propName: string; // e.g., "bookButton" generates onBookButtonClick handler
79
+ actionType: string; // "remote", "link", etc.
80
+ _parentPath?: string[];
81
+ }
82
+
83
+ async function downloadFile(
84
+ url: string,
85
+ headers?: Record<string, string>
86
+ ): Promise<string> {
87
+ const response = await fetch(url, {
88
+ headers: headers || {},
89
+ });
90
+
91
+ if (!response.ok) {
92
+ throw new Error(
93
+ `Failed to download ${url}: ${response.status} ${response.statusText}`
94
+ );
95
+ }
96
+
97
+ return await response.text();
98
+ }
99
+
100
+ function sanitizePropName(name: string): string {
101
+ // Convert to camelCase and remove invalid characters
102
+ const cleaned = name
103
+ .replace(/[^a-zA-Z0-9\s]/g, "") // Remove special chars
104
+ .trim();
105
+
106
+ if (!cleaned) return "item";
107
+
108
+ return cleaned
109
+ .split(/\s+/)
110
+ .map((word, index) => {
111
+ if (!word) return "";
112
+ // Handle all-uppercase words (like "SHOP", "NOW") by lowercasing them first
113
+ const normalizedWord =
114
+ word === word.toUpperCase() && word.length > 1
115
+ ? word.toLowerCase()
116
+ : word;
117
+ const firstChar = normalizedWord.charAt(0);
118
+ const rest = normalizedWord.slice(1);
119
+ if (index === 0) {
120
+ return firstChar.toLowerCase() + rest;
121
+ }
122
+ return firstChar.toUpperCase() + rest;
123
+ })
124
+ .join("")
125
+ .replace(/^[0-9]/, "_$&"); // Prefix numbers with underscore
126
+ }
127
+
128
+ function generateQualifiedPropName(
129
+ componentName: string,
130
+ parentPath: string[]
131
+ ): string {
132
+ // Start with the component's own name
133
+ const baseName = sanitizePropName(componentName);
134
+
135
+ // If no parent path, just return the base name
136
+ if (parentPath.length === 0) {
137
+ return baseName;
138
+ }
139
+
140
+ // Filter out empty parts and reverse so we build from root to leaf
141
+ const validParentParts = parentPath
142
+ .filter((part) => part && part.trim())
143
+ .reverse();
144
+
145
+ if (validParentParts.length === 0) {
146
+ return baseName;
147
+ }
148
+
149
+ // Build qualified name: parent1Parent2ComponentName
150
+ // Each parent part is sanitized individually, then combined
151
+ const sanitizedParentParts = validParentParts.map((part) =>
152
+ sanitizePropName(part)
153
+ );
154
+
155
+ // Join parent parts (all start uppercase) with base name (starts lowercase)
156
+ const parentPrefix = sanitizedParentParts
157
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
158
+ .join("");
159
+
160
+ return parentPrefix + baseName;
161
+ }
162
+
163
+ /**
164
+ * Finds the minimal distinguishing path suffix for a component when compared to others.
165
+ * Returns the shortest suffix (from root side, working towards leaf) that makes this path unique.
166
+ *
167
+ * The algorithm finds where paths first diverge from the root, then takes the minimal
168
+ * distinguishing part from that divergence point towards the leaf.
169
+ *
170
+ * Example:
171
+ * - thisPath: [Frame11, Frame31, Frame18]
172
+ * - otherPaths: [[Frame11, Frame31, Frame28, Frame29, Frame15]]
173
+ * - Common prefix: [Frame11, Frame31]
174
+ * - Divergence at index 2
175
+ * - Returns: [Frame18] (just the immediate distinguishing parent)
176
+ */
177
+ function findMinimalDistinguishingPath(
178
+ thisPath: string[],
179
+ otherPaths: string[][]
180
+ ): string[] {
181
+ if (otherPaths.length === 0) {
182
+ // No other paths to compare, return empty (no qualification needed)
183
+ return [];
184
+ }
185
+
186
+ // Find the longest common prefix (from root side)
187
+ let commonPrefixLength = 0;
188
+ const maxCommonLength = Math.min(
189
+ thisPath.length,
190
+ ...otherPaths.map((p) => p.length)
191
+ );
192
+
193
+ for (let i = 0; i < maxCommonLength; i++) {
194
+ const thisPart = thisPath[i];
195
+ // Check if all other paths have the same part at this position
196
+ const allMatch = otherPaths.every((otherPath) => {
197
+ return otherPath[i] === thisPart;
198
+ });
199
+
200
+ if (allMatch) {
201
+ commonPrefixLength++;
202
+ } else {
203
+ break;
204
+ }
205
+ }
206
+
207
+ // The distinguishing part starts after the common prefix
208
+ const distinguishingSuffix = thisPath.slice(commonPrefixLength);
209
+
210
+ // Now find the minimal suffix that's unique
211
+ // Try progressively shorter suffixes (from root side) until we find one that's unique
212
+ for (
213
+ let suffixLength = 1;
214
+ suffixLength <= distinguishingSuffix.length;
215
+ suffixLength++
216
+ ) {
217
+ const thisSuffix = distinguishingSuffix.slice(0, suffixLength);
218
+
219
+ // Check if this suffix is unique when combined with the common prefix
220
+ const isUnique = otherPaths.every((otherPath) => {
221
+ // If other path is shorter than common prefix + suffix, it can't match
222
+ if (otherPath.length < commonPrefixLength + suffixLength) {
223
+ return true; // Different length means unique
224
+ }
225
+ // Check if the suffix matches at the divergence point
226
+ const otherSuffix = otherPath.slice(
227
+ commonPrefixLength,
228
+ commonPrefixLength + suffixLength
229
+ );
230
+ return !arraysEqual(thisSuffix, otherSuffix);
231
+ });
232
+
233
+ if (isUnique) {
234
+ // Found minimal distinguishing suffix
235
+ return thisSuffix;
236
+ }
237
+ }
238
+
239
+ // If we get here, paths are identical (shouldn't happen, but handle it)
240
+ return distinguishingSuffix;
241
+ }
242
+
243
+ /**
244
+ * Helper to compare two arrays for equality
245
+ */
246
+ function arraysEqual(a: string[], b: string[]): boolean {
247
+ if (a.length !== b.length) return false;
248
+ return a.every((val, idx) => val === b[idx]);
249
+ }
250
+
251
+ function getComponentPropType(
252
+ componentType: string,
253
+ _componentName: string
254
+ ): string {
255
+ if (componentType === "component:image") {
256
+ return "string"; // imageUrl
257
+ }
258
+ if (componentType === "component:text") {
259
+ return "string"; // text
260
+ }
261
+ if (componentType?.startsWith("component:input-")) {
262
+ // Input types generally return strings
263
+ if (componentType === "component:input-image") {
264
+ return "string"; // image URL
265
+ }
266
+ return "string"; // text inputs, email, password, etc.
267
+ }
268
+ // Default to any for unknown types
269
+ return "any";
270
+ }
271
+
272
+ function getComponentPropName(componentType: string): string {
273
+ if (componentType === "component:image") {
274
+ return "imageUrl";
275
+ }
276
+ if (componentType === "component:text") {
277
+ return "text";
278
+ }
279
+ return "value";
280
+ }
281
+
282
+ function findSlidersAndDataBindings(pageData: any): SliderInfo[] {
283
+ const sliders: SliderInfo[] = [];
284
+
285
+ function traverse(node: any, path: string[] = []): void {
286
+ if (!node || typeof node !== "object") return;
287
+
288
+ // Check if this is a slider container
289
+ if (node.type === "container:slider") {
290
+ const slider: SliderInfo = {
291
+ id: node.id,
292
+ name: node.name || "Slider",
293
+ arrayContainer: null,
294
+ };
295
+
296
+ // Recursively look for containers with encore:data:array tag inside the slider
297
+ let arrayContainer: any = null;
298
+
299
+ function findArrayContainer(containerNode: any): void {
300
+ if (arrayContainer) return; // Already found one
301
+
302
+ if (!containerNode || typeof containerNode !== "object") return;
303
+
304
+ // Check if this node is the array container
305
+ if (
306
+ Array.isArray(containerNode.tags) &&
307
+ (containerNode.tags.includes("encore:data:array") ||
308
+ containerNode.tags.includes("bravo:data:array"))
309
+ ) {
310
+ arrayContainer = containerNode;
311
+ return;
312
+ }
313
+
314
+ // Recursively search children
315
+ if (containerNode.body && Array.isArray(containerNode.body)) {
316
+ containerNode.body.forEach(findArrayContainer);
317
+ }
318
+ if (
319
+ containerNode.containers &&
320
+ Array.isArray(containerNode.containers)
321
+ ) {
322
+ containerNode.containers.forEach(findArrayContainer);
323
+ }
324
+ if (
325
+ containerNode.components &&
326
+ Array.isArray(containerNode.components)
327
+ ) {
328
+ containerNode.components.forEach(findArrayContainer);
329
+ }
330
+ }
331
+
332
+ // Start search from the slider node itself (checking its children)
333
+ if (node.containers && Array.isArray(node.containers)) {
334
+ node.containers.forEach(findArrayContainer);
335
+ }
336
+ // Also check components if they exist directly
337
+ if (
338
+ !arrayContainer &&
339
+ node.components &&
340
+ Array.isArray(node.components)
341
+ ) {
342
+ node.components.forEach(findArrayContainer);
343
+ }
344
+
345
+ if (arrayContainer) {
346
+ const container = arrayContainer;
347
+ let components: ComponentInfo[] = [];
348
+
349
+ // Find all components with encore:data tag, and also image components
350
+ const imageComponents: any[] = [];
351
+
352
+ function findDataComponents(
353
+ comp: any,
354
+ parentPath: string[] = []
355
+ ): void {
356
+ if (!comp || typeof comp !== "object") return;
357
+
358
+ // Track image components separately (they might not have encore:data tag)
359
+ if (comp.type === "component:image") {
360
+ imageComponents.push(comp);
361
+ }
362
+
363
+ if (
364
+ Array.isArray(comp.tags) &&
365
+ (comp.tags.includes("encore:data") ||
366
+ comp.tags.includes("bravo:data"))
367
+ ) {
368
+ const basePropName = sanitizePropName(comp.name || "item");
369
+ const propType = getComponentPropType(comp.type, comp.name);
370
+ // const propKey = getComponentPropName(comp.type);
371
+
372
+ components.push({
373
+ id: comp.id,
374
+ name: comp.name || "Unnamed",
375
+ type: comp.type,
376
+ tags: comp.tags || [],
377
+ propName: basePropName, // Will be qualified later if needed
378
+ propType,
379
+ // Store parent path for later qualification
380
+ _parentPath: [...parentPath],
381
+ } as ComponentInfo & { _parentPath: string[] });
382
+ }
383
+
384
+ // Build parent path: include this node's name if it's a container/component with a name
385
+ // Skip generic "Frame" names - they're usually not meaningful for distinction
386
+ const currentParentPath = [...parentPath];
387
+ if (
388
+ comp.name &&
389
+ (comp.type?.startsWith("container:") ||
390
+ comp.type?.startsWith("component:"))
391
+ ) {
392
+ const name = comp.name.trim();
393
+ // Filter out generic "Frame" names (case-insensitive, with or without numbers/spaces)
394
+ // But keep meaningful names like "TripSlideFrame" that contain other words
395
+ const isGenericFrame =
396
+ /^frame\s*\d*$/i.test(name) || name.toUpperCase() === "FRAME";
397
+ if (name && !isGenericFrame) {
398
+ currentParentPath.push(comp.name);
399
+ }
400
+ }
401
+
402
+ // Recursively search children
403
+ if (comp.components && Array.isArray(comp.components)) {
404
+ comp.components.forEach((child: any) =>
405
+ findDataComponents(child, currentParentPath)
406
+ );
407
+ }
408
+ }
409
+
410
+ if (container.components && Array.isArray(container.components)) {
411
+ container.components.forEach((comp: any) =>
412
+ findDataComponents(comp, [])
413
+ );
414
+ }
415
+
416
+ // After finding all components, if we have image components but no image with encore:data,
417
+ // add the first image component
418
+ const hasImageWithData = components.some(
419
+ (c) => c.type === "component:image"
420
+ );
421
+ if (!hasImageWithData && imageComponents.length > 0) {
422
+ const imageComp = imageComponents[0];
423
+ // For image components, use "imageUrl" as the prop name
424
+ // Clean the name first, then if it contains "image", just use "imageUrl"
425
+ const rawName = (imageComp.name || "image").toLowerCase();
426
+ const basePropName = rawName.includes("image")
427
+ ? "imageUrl"
428
+ : sanitizePropName(imageComp.name || "image");
429
+ components.push({
430
+ id: imageComp.id,
431
+ name: imageComp.name || "Image",
432
+ type: imageComp.type,
433
+ tags: imageComp.tags || [],
434
+ propName: basePropName, // Will be qualified later if needed
435
+ propType: "string", // imageUrl is always string
436
+ _parentPath: [],
437
+ } as ComponentInfo & { _parentPath: string[] });
438
+ }
439
+
440
+ // Detect duplicates and qualify them with minimal distinguishing paths
441
+ // First pass: collect all base prop names and group duplicates
442
+ const propNameGroups = new Map<
443
+ string,
444
+ Array<ComponentInfo & { _parentPath: string[] }>
445
+ >();
446
+ components.forEach((comp) => {
447
+ const compWithPath = comp as ComponentInfo & {
448
+ _parentPath: string[];
449
+ };
450
+ const baseName = comp.propName;
451
+ if (!propNameGroups.has(baseName)) {
452
+ propNameGroups.set(baseName, []);
453
+ }
454
+ propNameGroups.get(baseName)!.push(compWithPath);
455
+ });
456
+
457
+ // Second pass: for each group with duplicates, find minimal distinguishing paths
458
+ // and ensure all qualified names are unique
459
+ propNameGroups.forEach((group, _baseName) => {
460
+ if (group.length === 1) {
461
+ // No duplicates, keep the simple name
462
+ return;
463
+ }
464
+
465
+ // First, find minimal distinguishing paths for all components
466
+ group.forEach((comp) => {
467
+ const otherPaths = group
468
+ .filter((c) => c.id !== comp.id)
469
+ .map((c) => c._parentPath || []);
470
+
471
+ const minimalPath = findMinimalDistinguishingPath(
472
+ comp._parentPath || [],
473
+ otherPaths
474
+ );
475
+
476
+ // Use the minimal distinguishing path to qualify the name
477
+ comp.propName = generateQualifiedPropName(
478
+ comp.name || "item",
479
+ minimalPath
480
+ );
481
+ });
482
+
483
+ // Check if qualified names are still duplicates and expand paths if needed
484
+ let hasDuplicates = true;
485
+ let iteration = 0;
486
+ const maxIterations = 10; // Safety limit
487
+
488
+ while (hasDuplicates && iteration < maxIterations) {
489
+ iteration++;
490
+ const qualifiedNameGroups = new Map<
491
+ string,
492
+ Array<ComponentInfo & { _parentPath: string[] }>
493
+ >();
494
+ group.forEach((comp) => {
495
+ if (!qualifiedNameGroups.has(comp.propName)) {
496
+ qualifiedNameGroups.set(comp.propName, []);
497
+ }
498
+ qualifiedNameGroups.get(comp.propName)!.push(comp);
499
+ });
500
+
501
+ hasDuplicates = false;
502
+ // For each group of still-duplicated qualified names, expand their paths
503
+ qualifiedNameGroups.forEach((dupGroup, _qualifiedName) => {
504
+ if (dupGroup.length > 1) {
505
+ hasDuplicates = true;
506
+ // Expand the distinguishing path for each duplicate
507
+ dupGroup.forEach((comp) => {
508
+
509
+
510
+ // Find a longer distinguishing path by comparing with others in the duplicate group
511
+ const fullPath = comp._parentPath || [];
512
+ const otherFullPaths = dupGroup
513
+ .filter((c) => c.id !== comp.id)
514
+ .map((c) => c._parentPath || []);
515
+
516
+ // Find where this path diverges from others in the duplicate group
517
+ let commonPrefixLength = 0;
518
+ const maxCommonLength = Math.min(
519
+ fullPath.length,
520
+ ...otherFullPaths.map((p) => p.length)
521
+ );
522
+
523
+ for (let i = 0; i < maxCommonLength; i++) {
524
+ const thisPart = fullPath[i];
525
+ const allMatch = otherFullPaths.every((otherPath) => {
526
+ return otherPath[i] === thisPart;
527
+ });
528
+ if (allMatch) {
529
+ commonPrefixLength++;
530
+ } else {
531
+ break;
532
+ }
533
+ }
534
+
535
+ // Use progressively more of the distinguishing suffix until unique
536
+ const distinguishingSuffix =
537
+ fullPath.slice(commonPrefixLength);
538
+
539
+ // Try expanding the distinguishing suffix until we find a unique name
540
+ let foundUnique = false;
541
+ for (
542
+ let suffixLength = 1;
543
+ suffixLength <= distinguishingSuffix.length;
544
+ suffixLength++
545
+ ) {
546
+ const expandedPath = distinguishingSuffix.slice(
547
+ 0,
548
+ suffixLength
549
+ );
550
+ const testQualifiedName = generateQualifiedPropName(
551
+ comp.name || "item",
552
+ expandedPath
553
+ );
554
+
555
+ // Check if this qualified name is unique among ALL components (not just duplicates)
556
+ const isUnique = components.every((otherComp) => {
557
+ if (otherComp.id === comp.id) return true;
558
+ // If other component is in the same duplicate group, compare expanded paths
559
+ if (dupGroup.some((c) => c.id === otherComp.id)) {
560
+ const otherFullPath =
561
+ (
562
+ otherComp as ComponentInfo & {
563
+ _parentPath: string[];
564
+ }
565
+ )._parentPath || [];
566
+ const otherCommonPrefixLength = Math.min(
567
+ commonPrefixLength,
568
+ otherFullPath.length
569
+ );
570
+ const otherDistinguishingSuffix = otherFullPath.slice(
571
+ otherCommonPrefixLength
572
+ );
573
+ const otherExpandedPath =
574
+ otherDistinguishingSuffix.slice(0, suffixLength);
575
+ const otherQualifiedName = generateQualifiedPropName(
576
+ otherComp.name || "item",
577
+ otherExpandedPath
578
+ );
579
+ return testQualifiedName !== otherQualifiedName;
580
+ }
581
+ // For components outside the duplicate group, just check the final prop name
582
+ return testQualifiedName !== otherComp.propName;
583
+ });
584
+
585
+ if (isUnique) {
586
+ comp.propName = testQualifiedName;
587
+ foundUnique = true;
588
+ break;
589
+ }
590
+ }
591
+
592
+ // If we couldn't find a unique name with the distinguishing suffix,
593
+ // use the distinguishing suffix we found (it's the minimal we can do)
594
+ // We'll handle truly identical paths with numeric suffixes in the final pass
595
+ if (!foundUnique) {
596
+ // Use the distinguishing suffix - it's the minimal distinguishing part
597
+ // Even if it's not globally unique yet, it's better than the full path
598
+ comp.propName = generateQualifiedPropName(
599
+ comp.name || "item",
600
+ distinguishingSuffix.length > 0
601
+ ? distinguishingSuffix
602
+ : []
603
+ );
604
+ }
605
+ });
606
+ }
607
+ });
608
+ }
609
+
610
+ // Final check: if there are still duplicates after using full paths,
611
+ // and they have identical paths, use numeric suffixes as last resort
612
+ const finalQualifiedNameGroups = new Map<
613
+ string,
614
+ Array<ComponentInfo & { _parentPath: string[] }>
615
+ >();
616
+ group.forEach((comp) => {
617
+ if (!finalQualifiedNameGroups.has(comp.propName)) {
618
+ finalQualifiedNameGroups.set(comp.propName, []);
619
+ }
620
+ finalQualifiedNameGroups.get(comp.propName)!.push(comp);
621
+ });
622
+
623
+ finalQualifiedNameGroups.forEach(
624
+ (finalDupGroup, finalQualifiedName) => {
625
+ if (finalDupGroup.length > 1) {
626
+ // Check if all duplicates have identical paths
627
+ const allPathsIdentical = finalDupGroup.every((comp) => {
628
+ const thisPath = comp._parentPath || [];
629
+ return finalDupGroup.every((otherComp) => {
630
+ if (otherComp.id === comp.id) return true;
631
+ const otherPath = otherComp._parentPath || [];
632
+ return arraysEqual(thisPath, otherPath);
633
+ });
634
+ });
635
+
636
+ // Only use numeric suffixes if paths are truly identical
637
+ if (allPathsIdentical) {
638
+ let index = 0;
639
+ finalDupGroup.forEach((comp) => {
640
+ if (index > 0) {
641
+ comp.propName = `${finalQualifiedName}${index + 1}`;
642
+ }
643
+ index++;
644
+ });
645
+ }
646
+ }
647
+ }
648
+ );
649
+
650
+ // Remove the temporary _parentPath property
651
+ group.forEach((comp) => {
652
+ delete (comp as any)._parentPath;
653
+ });
654
+ });
655
+
656
+ // If we have an image component, remove color components with similar names
657
+ // (they're usually placeholders/backgrounds)
658
+ if (imageComponents.length > 0) {
659
+ const imageComp = imageComponents[0];
660
+ const imageName = (imageComp.name || "").toLowerCase();
661
+ components = components.filter((comp) => {
662
+ // Keep image components
663
+ if (comp.type === "component:image") return true;
664
+ // Remove color components that seem to be placeholders for images
665
+ if (comp.type === "component:color") {
666
+ const compName = (comp.name || "").toLowerCase();
667
+ // If color component name is similar to image name, it's likely a placeholder
668
+ if (imageName.includes(compName) || compName.includes("image")) {
669
+ return false;
670
+ }
671
+ }
672
+ return true;
673
+ });
674
+ }
675
+
676
+ slider.arrayContainer = {
677
+ id: container.id,
678
+ name: container.name || "Item",
679
+ propName: sanitizePropName(container.name || "items"),
680
+ components,
681
+ };
682
+ }
683
+
684
+ sliders.push(slider);
685
+ }
686
+
687
+ // Recursively traverse children
688
+ if (node.body && Array.isArray(node.body)) {
689
+ node.body.forEach((child: any) => traverse(child, [...path, "body"]));
690
+ }
691
+ if (node.containers && Array.isArray(node.containers)) {
692
+ node.containers.forEach((child: any) =>
693
+ traverse(child, [...path, "containers"])
694
+ );
695
+ }
696
+ if (node.components && Array.isArray(node.components)) {
697
+ node.components.forEach((child: any) =>
698
+ traverse(child, [...path, "components"])
699
+ );
700
+ }
701
+ }
702
+
703
+ // Start traversal from page data
704
+ // Try multiple possible locations for the body
705
+ const body =
706
+ pageData.data?.body || pageData.body || (pageData as any).data?.body || [];
707
+
708
+ if (Array.isArray(body) && body.length > 0) {
709
+ body.forEach((node: any) => traverse(node));
710
+ }
711
+
712
+ return sliders;
713
+ }
714
+
715
+ function findStandaloneComponents(pageData: any): ComponentInfo[] {
716
+ const components: ComponentInfo[] = [];
717
+ const sliderIds = new Set<string>();
718
+
719
+ // First, collect all slider IDs to exclude their children
720
+ function collectSliderIds(node: any): void {
721
+ if (!node || typeof node !== "object") return;
722
+
723
+ if (node.type === "container:slider") {
724
+ sliderIds.add(node.id);
725
+ }
726
+
727
+ if (node.body && Array.isArray(node.body)) {
728
+ node.body.forEach(collectSliderIds);
729
+ }
730
+ if (node.containers && Array.isArray(node.containers)) {
731
+ node.containers.forEach(collectSliderIds);
732
+ }
733
+ }
734
+
735
+ // Traverse to find standalone components with bravo:data tags
736
+ // Track parent component names for qualification
737
+ function traverse(
738
+ node: any,
739
+ parentId?: string,
740
+ parentPath: string[] = []
741
+ ): void {
742
+ if (!node || typeof node !== "object") return;
743
+
744
+ // Skip if we're inside a slider
745
+ if (parentId && sliderIds.has(parentId)) return;
746
+
747
+ // Check if this component has bravo:data tag
748
+ if (
749
+ Array.isArray(node.tags) &&
750
+ (node.tags.includes("encore:data") || node.tags.includes("bravo:data")) &&
751
+ (node.type === "component:text" || node.type === "component:image")
752
+ ) {
753
+ const basePropName = sanitizePropName(node.name || "item");
754
+ const propType = getComponentPropType(node.type, node.name);
755
+
756
+ components.push({
757
+ id: node.id,
758
+ name: node.name || "Unnamed",
759
+ type: node.type,
760
+ tags: node.tags || [],
761
+ propName: basePropName, // Will be qualified later if needed
762
+ propType,
763
+ // Store parent path for later qualification
764
+ _parentPath: [...parentPath],
765
+ } as ComponentInfo & { _parentPath: string[] });
766
+ }
767
+
768
+ // Build parent path: include this node's name if it's a container/component with a name
769
+ // Skip generic "Frame" names - they're usually not meaningful for distinction
770
+ const currentParentPath = [...parentPath];
771
+ if (
772
+ node.name &&
773
+ (node.type?.startsWith("container:") ||
774
+ node.type?.startsWith("component:"))
775
+ ) {
776
+ const name = node.name.trim();
777
+ // Filter out generic "Frame" names (case-insensitive, with or without numbers/spaces)
778
+ // But keep meaningful names like "TripSlideFrame" that contain other words
779
+ const isGenericFrame =
780
+ /^frame\s*\d*$/i.test(name) || name.toUpperCase() === "FRAME";
781
+ if (name && !isGenericFrame) {
782
+ currentParentPath.push(node.name);
783
+ }
784
+ }
785
+
786
+ // Recursively traverse children
787
+ const currentId = node.id;
788
+ if (node.body && Array.isArray(node.body)) {
789
+ node.body.forEach((child: any) =>
790
+ traverse(child, currentId, currentParentPath)
791
+ );
792
+ }
793
+ if (node.containers && Array.isArray(node.containers)) {
794
+ node.containers.forEach((child: any) =>
795
+ traverse(child, currentId, currentParentPath)
796
+ );
797
+ }
798
+ if (node.components && Array.isArray(node.components)) {
799
+ node.components.forEach((child: any) =>
800
+ traverse(child, currentId, currentParentPath)
801
+ );
802
+ }
803
+ }
804
+
805
+ // Start traversal from page data
806
+ const body =
807
+ pageData.data?.body || pageData.body || (pageData as any).data?.body || [];
808
+
809
+ if (Array.isArray(body) && body.length > 0) {
810
+ body.forEach(collectSliderIds);
811
+ body.forEach((node: any) => traverse(node));
812
+ }
813
+
814
+ // Detect duplicates and qualify them with minimal distinguishing paths
815
+ // First pass: collect all base prop names and group duplicates
816
+ const propNameGroups = new Map<
817
+ string,
818
+ Array<ComponentInfo & { _parentPath: string[] }>
819
+ >();
820
+ components.forEach((comp) => {
821
+ const compWithPath = comp as ComponentInfo & { _parentPath: string[] };
822
+ const baseName = comp.propName;
823
+ if (!propNameGroups.has(baseName)) {
824
+ propNameGroups.set(baseName, []);
825
+ }
826
+ propNameGroups.get(baseName)!.push(compWithPath);
827
+ });
828
+
829
+ // Second pass: for each group with duplicates, find minimal distinguishing paths
830
+ // and ensure all qualified names are unique
831
+ propNameGroups.forEach((group, _baseName) => {
832
+ if (group.length === 1) {
833
+ // No duplicates, keep the simple name
834
+ return;
835
+ }
836
+
837
+ // First, find minimal distinguishing paths for all components
838
+ group.forEach((comp) => {
839
+ const otherPaths = group
840
+ .filter((c) => c.id !== comp.id)
841
+ .map((c) => c._parentPath || []);
842
+
843
+ const minimalPath = findMinimalDistinguishingPath(
844
+ comp._parentPath || [],
845
+ otherPaths
846
+ );
847
+
848
+ // Use the minimal distinguishing path to qualify the name
849
+ comp.propName = generateQualifiedPropName(
850
+ comp.name || "item",
851
+ minimalPath
852
+ );
853
+ });
854
+
855
+ // Check if qualified names are still duplicates and expand paths if needed
856
+ let hasDuplicates = true;
857
+ let iteration = 0;
858
+ const maxIterations = 10; // Safety limit
859
+
860
+ while (hasDuplicates && iteration < maxIterations) {
861
+ iteration++;
862
+ const qualifiedNameGroups = new Map<
863
+ string,
864
+ Array<ComponentInfo & { _parentPath: string[] }>
865
+ >();
866
+ group.forEach((comp) => {
867
+ if (!qualifiedNameGroups.has(comp.propName)) {
868
+ qualifiedNameGroups.set(comp.propName, []);
869
+ }
870
+ qualifiedNameGroups.get(comp.propName)!.push(comp);
871
+ });
872
+
873
+ hasDuplicates = false;
874
+ // For each group of still-duplicated qualified names, expand their paths
875
+ qualifiedNameGroups.forEach((dupGroup, _qualifiedName) => {
876
+ if (dupGroup.length > 1) {
877
+ hasDuplicates = true;
878
+ // Expand the distinguishing path for each duplicate
879
+ dupGroup.forEach((comp) => {
880
+
881
+
882
+ // Find a longer distinguishing path by comparing with others in the duplicate group
883
+ const fullPath = comp._parentPath || [];
884
+ const otherFullPaths = dupGroup
885
+ .filter((c) => c.id !== comp.id)
886
+ .map((c) => c._parentPath || []);
887
+
888
+ // Find where this path diverges from others in the duplicate group
889
+ let commonPrefixLength = 0;
890
+ const maxCommonLength = Math.min(
891
+ fullPath.length,
892
+ ...otherFullPaths.map((p) => p.length)
893
+ );
894
+
895
+ for (let i = 0; i < maxCommonLength; i++) {
896
+ const thisPart = fullPath[i];
897
+ const allMatch = otherFullPaths.every((otherPath) => {
898
+ return otherPath[i] === thisPart;
899
+ });
900
+ if (allMatch) {
901
+ commonPrefixLength++;
902
+ } else {
903
+ break;
904
+ }
905
+ }
906
+
907
+ // Use progressively more of the distinguishing suffix until unique
908
+ const distinguishingSuffix = fullPath.slice(commonPrefixLength);
909
+
910
+ // Try expanding the distinguishing suffix until we find a unique name
911
+ let foundUnique = false;
912
+ for (
913
+ let suffixLength = 1;
914
+ suffixLength <= distinguishingSuffix.length;
915
+ suffixLength++
916
+ ) {
917
+ const expandedPath = distinguishingSuffix.slice(0, suffixLength);
918
+ const testQualifiedName = generateQualifiedPropName(
919
+ comp.name || "item",
920
+ expandedPath
921
+ );
922
+
923
+ // Check if this qualified name is unique among ALL components (not just duplicates)
924
+ const isUnique = components.every((otherComp) => {
925
+ if (otherComp.id === comp.id) return true;
926
+ // If other component is in the same duplicate group, compare expanded paths
927
+ if (dupGroup.some((c) => c.id === otherComp.id)) {
928
+ const otherFullPath =
929
+ (otherComp as ComponentInfo & { _parentPath: string[] })
930
+ ._parentPath || [];
931
+ const otherCommonPrefixLength = Math.min(
932
+ commonPrefixLength,
933
+ otherFullPath.length
934
+ );
935
+ const otherDistinguishingSuffix = otherFullPath.slice(
936
+ otherCommonPrefixLength
937
+ );
938
+ const otherExpandedPath = otherDistinguishingSuffix.slice(
939
+ 0,
940
+ suffixLength
941
+ );
942
+ const otherQualifiedName = generateQualifiedPropName(
943
+ otherComp.name || "item",
944
+ otherExpandedPath
945
+ );
946
+ return testQualifiedName !== otherQualifiedName;
947
+ }
948
+ // For components outside the duplicate group, just check the final prop name
949
+ return testQualifiedName !== otherComp.propName;
950
+ });
951
+
952
+ if (isUnique) {
953
+ comp.propName = testQualifiedName;
954
+ foundUnique = true;
955
+ break;
956
+ }
957
+ }
958
+
959
+ // If we couldn't find a unique name with the distinguishing suffix,
960
+ // use the full path to ensure uniqueness (even if it makes names longer)
961
+ if (!foundUnique) {
962
+ comp.propName = generateQualifiedPropName(
963
+ comp.name || "item",
964
+ fullPath
965
+ );
966
+ }
967
+ });
968
+ }
969
+ });
970
+ }
971
+
972
+ // Final check: if there are still duplicates after using full paths,
973
+ // and they have identical paths, use numeric suffixes as last resort
974
+ const finalQualifiedNameGroups = new Map<
975
+ string,
976
+ Array<ComponentInfo & { _parentPath: string[] }>
977
+ >();
978
+ group.forEach((comp) => {
979
+ if (!finalQualifiedNameGroups.has(comp.propName)) {
980
+ finalQualifiedNameGroups.set(comp.propName, []);
981
+ }
982
+ finalQualifiedNameGroups.get(comp.propName)!.push(comp);
983
+ });
984
+
985
+ finalQualifiedNameGroups.forEach((finalDupGroup, finalQualifiedName) => {
986
+ if (finalDupGroup.length > 1) {
987
+ // Check if all duplicates have identical paths
988
+ const allPathsIdentical = finalDupGroup.every((comp) => {
989
+ const thisPath = comp._parentPath || [];
990
+ return finalDupGroup.every((otherComp) => {
991
+ if (otherComp.id === comp.id) return true;
992
+ const otherPath = otherComp._parentPath || [];
993
+ return arraysEqual(thisPath, otherPath);
994
+ });
995
+ });
996
+
997
+ // Only use numeric suffixes if paths are truly identical
998
+ if (allPathsIdentical) {
999
+ let index = 0;
1000
+ finalDupGroup.forEach((comp) => {
1001
+ if (index > 0) {
1002
+ comp.propName = `${finalQualifiedName}${index + 1}`;
1003
+ }
1004
+ index++;
1005
+ });
1006
+ }
1007
+ }
1008
+ });
1009
+
1010
+ // Remove the temporary _parentPath property
1011
+ group.forEach((comp) => {
1012
+ delete (comp as any)._parentPath;
1013
+ });
1014
+ });
1015
+
1016
+ return components;
1017
+ }
1018
+
1019
+ function findInputGroups(pageData: any): InputGroupInfo[] {
1020
+ const groupsMap = new Map<string, InputGroupInfo>();
1021
+
1022
+ function traverse(node: any): void {
1023
+ if (!node || typeof node !== "object") return;
1024
+
1025
+ // Check if this is an input-stateful-set with input-group tag
1026
+ if (
1027
+ node.type === "component:input-stateful-set" &&
1028
+ Array.isArray(node.tags)
1029
+ ) {
1030
+ const inputGroupTag = node.tags.find((tag: string) =>
1031
+ tag.startsWith("input-group:")
1032
+ );
1033
+ if (inputGroupTag) {
1034
+ const parts = inputGroupTag.split(":");
1035
+ if (parts.length >= 3) {
1036
+ const groupType = parts[1];
1037
+ const groupName = parts[2];
1038
+
1039
+ if (!groupsMap.has(groupName)) {
1040
+ groupsMap.set(groupName, {
1041
+ groupName,
1042
+ groupType,
1043
+ elements: [],
1044
+ });
1045
+ }
1046
+
1047
+ const group = groupsMap.get(groupName)!;
1048
+ group.elements.push({
1049
+ id: node.id,
1050
+ name: node.name || "Unnamed",
1051
+ });
1052
+ }
1053
+ }
1054
+ }
1055
+
1056
+ // Recursively traverse children
1057
+ if (node.body && Array.isArray(node.body)) {
1058
+ node.body.forEach(traverse);
1059
+ }
1060
+ if (node.containers && Array.isArray(node.containers)) {
1061
+ node.containers.forEach(traverse);
1062
+ }
1063
+ if (node.components && Array.isArray(node.components)) {
1064
+ node.components.forEach(traverse);
1065
+ }
1066
+ }
1067
+
1068
+ // Start traversal from page data
1069
+ const body =
1070
+ pageData.data?.body || pageData.body || (pageData as any).data?.body || [];
1071
+
1072
+ if (Array.isArray(body) && body.length > 0) {
1073
+ body.forEach((node: any) => traverse(node));
1074
+ }
1075
+
1076
+ return Array.from(groupsMap.values());
1077
+ }
1078
+
1079
+ function findForms(pageData: any): FormInfo[] {
1080
+ const forms: FormInfo[] = [];
1081
+
1082
+ function traverse(node: any, parentContainer?: any): void {
1083
+ if (!node || typeof node !== "object") return;
1084
+
1085
+ // Check if this is a container that might be a form
1086
+ const isContainer =
1087
+ node.type?.startsWith("container:") || node.type === "container:default";
1088
+ const isNamedForm =
1089
+ node.name?.toLowerCase().includes("form") ||
1090
+ (Array.isArray(node.tags) && node.tags.includes("form"));
1091
+
1092
+ // Check if this container or any child has a submit action
1093
+ let hasSubmitAction = false;
1094
+ let submitButtonId: string | undefined;
1095
+
1096
+ function checkForSubmitAction(n: any): void {
1097
+ if (!n || typeof n !== "object") return;
1098
+
1099
+ // Check if this node has a submit action
1100
+ if (
1101
+ Array.isArray(n.tags) &&
1102
+ (n.tags.includes("action:submit") || n.tags.includes("submit"))
1103
+ ) {
1104
+ hasSubmitAction = true;
1105
+ submitButtonId = n.id;
1106
+ return;
1107
+ }
1108
+
1109
+ // Check actions
1110
+ if (n.actions?.tap?.action === "submit") {
1111
+ hasSubmitAction = true;
1112
+ submitButtonId = n.id;
1113
+ return;
1114
+ }
1115
+
1116
+ // Recursively check children
1117
+ if (n.components && Array.isArray(n.components)) {
1118
+ n.components.forEach(checkForSubmitAction);
1119
+ }
1120
+ if (n.body && Array.isArray(n.body)) {
1121
+ n.body.forEach(checkForSubmitAction);
1122
+ }
1123
+ }
1124
+
1125
+ // If this looks like a form container, check for submit actions
1126
+ if (isContainer && (isNamedForm || parentContainer === undefined)) {
1127
+ checkForSubmitAction(node);
1128
+ }
1129
+
1130
+ // If we found a form container (has submit action or is named "form")
1131
+ if (isContainer && (hasSubmitAction || isNamedForm)) {
1132
+ const inputs: FormInfo["inputs"] = [];
1133
+
1134
+ // Find all input components within this container
1135
+ function findInputs(n: any, parentPath: string[] = []): void {
1136
+ if (!n || typeof n !== "object") return;
1137
+
1138
+ // Check if this is an input component
1139
+ if (
1140
+ n.type?.startsWith("component:input-") ||
1141
+ n.type === "component:input-text" ||
1142
+ n.type === "component:input-image" ||
1143
+ n.type === "component:input-email" ||
1144
+ n.type === "component:input-password" ||
1145
+ n.type === "component:input-select"
1146
+ ) {
1147
+ const basePropName = sanitizePropName(n.name || "Unnamed input");
1148
+ inputs.push({
1149
+ id: n.id,
1150
+ name: n.name || "Unnamed input",
1151
+ type: n.type,
1152
+ propName: basePropName, // Will be qualified later if needed
1153
+ _parentPath: [...parentPath], // Store parent path for qualification
1154
+ });
1155
+ }
1156
+
1157
+ // Build parent path: include this node's name if it's a container/component with a name
1158
+ // Skip generic "Frame" names - they're usually not meaningful for distinction
1159
+ const currentParentPath = [...parentPath];
1160
+ if (
1161
+ n.name &&
1162
+ (n.type?.startsWith("container:") || n.type?.startsWith("component:"))
1163
+ ) {
1164
+ const name = n.name.trim();
1165
+ // Filter out generic "Frame" names (case-insensitive, with or without numbers/spaces)
1166
+ // But keep meaningful names like "TripSlideFrame" that contain other words
1167
+ const isGenericFrame =
1168
+ /^frame\s*\d*$/i.test(name) || name.toUpperCase() === "FRAME";
1169
+ if (name && !isGenericFrame) {
1170
+ currentParentPath.push(n.name);
1171
+ }
1172
+ }
1173
+
1174
+ // Recursively search children
1175
+ if (n.components && Array.isArray(n.components)) {
1176
+ n.components.forEach((child: any) =>
1177
+ findInputs(child, currentParentPath)
1178
+ );
1179
+ }
1180
+ if (n.body && Array.isArray(n.body)) {
1181
+ n.body.forEach((child: any) => findInputs(child, currentParentPath));
1182
+ }
1183
+ }
1184
+
1185
+ findInputs(node, []);
1186
+
1187
+ // Only add form if it has inputs
1188
+ if (inputs.length > 0) {
1189
+ forms.push({
1190
+ formId: node.id,
1191
+ formName: node.name || "Form",
1192
+ submitButtonId,
1193
+ inputs,
1194
+ });
1195
+ }
1196
+ }
1197
+
1198
+ // Recursively traverse children
1199
+ if (node.body && Array.isArray(node.body)) {
1200
+ node.body.forEach((child: any) =>
1201
+ traverse(child, isContainer ? node : parentContainer)
1202
+ );
1203
+ }
1204
+ if (node.containers && Array.isArray(node.containers)) {
1205
+ node.containers.forEach((child: any) =>
1206
+ traverse(child, isContainer ? node : parentContainer)
1207
+ );
1208
+ }
1209
+ if (node.components && Array.isArray(node.components)) {
1210
+ node.components.forEach((child: any) =>
1211
+ traverse(child, isContainer ? node : parentContainer)
1212
+ );
1213
+ }
1214
+ }
1215
+
1216
+ // Start traversal from page data
1217
+ const body =
1218
+ pageData.data?.body || pageData.body || (pageData as any).data?.body || [];
1219
+
1220
+ if (Array.isArray(body) && body.length > 0) {
1221
+ body.forEach((node: any) => traverse(node));
1222
+ }
1223
+
1224
+ return forms;
1225
+ }
1226
+
1227
+ /**
1228
+ * Finds standalone select input components (input-select) that are NOT inside forms.
1229
+ * These should be exposed as controlled inputs with value and onChange props.
1230
+ */
1231
+ function findStandaloneSelectInputs(
1232
+ pageData: any,
1233
+ forms: FormInfo[]
1234
+ ): SelectInputInfo[] {
1235
+ const selectInputs: SelectInputInfo[] = [];
1236
+
1237
+ // Collect all input IDs that are already in forms
1238
+ const formInputIds = new Set<string>();
1239
+ forms.forEach((form) => {
1240
+ form.inputs.forEach((input) => {
1241
+ formInputIds.add(input.id);
1242
+ });
1243
+ });
1244
+
1245
+ // Traverse to find input-select components not in forms
1246
+ function traverse(node: any, parentPath: string[] = []): void {
1247
+ if (!node || typeof node !== "object") return;
1248
+
1249
+ // Check if this is an input-select component
1250
+ if (node.type === "component:input-select" && !formInputIds.has(node.id)) {
1251
+ const basePropName = sanitizePropName(node.name || "selectInput");
1252
+ selectInputs.push({
1253
+ id: node.id,
1254
+ name: node.name || "Select Input",
1255
+ propName: basePropName,
1256
+ _parentPath: [...parentPath],
1257
+ });
1258
+ }
1259
+
1260
+ // Build parent path for qualification
1261
+ const currentParentPath = [...parentPath];
1262
+ if (
1263
+ node.name &&
1264
+ (node.type?.startsWith("container:") ||
1265
+ node.type?.startsWith("component:"))
1266
+ ) {
1267
+ const name = node.name.trim();
1268
+ const isGenericFrame =
1269
+ /^frame\s*\d*$/i.test(name) || name.toUpperCase() === "FRAME";
1270
+ if (name && !isGenericFrame) {
1271
+ currentParentPath.push(node.name);
1272
+ }
1273
+ }
1274
+
1275
+ // Recursively traverse children
1276
+ if (node.body && Array.isArray(node.body)) {
1277
+ node.body.forEach((child: any) => traverse(child, currentParentPath));
1278
+ }
1279
+ if (node.containers && Array.isArray(node.containers)) {
1280
+ node.containers.forEach((child: any) =>
1281
+ traverse(child, currentParentPath)
1282
+ );
1283
+ }
1284
+ if (node.components && Array.isArray(node.components)) {
1285
+ node.components.forEach((child: any) =>
1286
+ traverse(child, currentParentPath)
1287
+ );
1288
+ }
1289
+ }
1290
+
1291
+ // Start traversal from page data
1292
+ const body =
1293
+ pageData.data?.body || pageData.body || (pageData as any).data?.body || [];
1294
+
1295
+ if (Array.isArray(body) && body.length > 0) {
1296
+ body.forEach((node: any) => traverse(node));
1297
+ }
1298
+
1299
+ // Qualify duplicate prop names
1300
+ qualifySelectInputs(selectInputs);
1301
+
1302
+ return selectInputs;
1303
+ }
1304
+
1305
+ /**
1306
+ * Qualifies select input prop names to ensure uniqueness.
1307
+ */
1308
+ function qualifySelectInputs(selectInputs: SelectInputInfo[]): void {
1309
+ const propNameGroups = new Map<
1310
+ string,
1311
+ Array<SelectInputInfo & { _parentPath: string[] }>
1312
+ >();
1313
+
1314
+ selectInputs.forEach((input) => {
1315
+ const inputWithPath = input as SelectInputInfo & { _parentPath: string[] };
1316
+ const baseName = input.propName;
1317
+ if (!propNameGroups.has(baseName)) {
1318
+ propNameGroups.set(baseName, []);
1319
+ }
1320
+ propNameGroups.get(baseName)!.push(inputWithPath);
1321
+ });
1322
+
1323
+ propNameGroups.forEach((group, _baseName) => {
1324
+ if (group.length === 1) {
1325
+ delete (group[0] as any)._parentPath;
1326
+ return;
1327
+ }
1328
+
1329
+ // Find minimal distinguishing paths for duplicates
1330
+ group.forEach((input) => {
1331
+ const otherPaths = group
1332
+ .filter((i) => i.id !== input.id)
1333
+ .map((i) => i._parentPath || []);
1334
+
1335
+ const minimalPath = findMinimalDistinguishingPath(
1336
+ input._parentPath || [],
1337
+ otherPaths
1338
+ );
1339
+
1340
+ input.propName = generateQualifiedPropName(
1341
+ input.name || "input",
1342
+ minimalPath
1343
+ );
1344
+ });
1345
+
1346
+ // Clean up
1347
+ group.forEach((input) => {
1348
+ delete (input as any)._parentPath;
1349
+ });
1350
+ });
1351
+ }
1352
+
1353
+ /**
1354
+ * Finds action buttons - components that have action tags (action:remote, action:link, etc.)
1355
+ * or have actions defined. These should be exposed with onClick handlers.
1356
+ */
1357
+ function findActionButtons(pageData: any): ActionButtonInfo[] {
1358
+ const buttons: ActionButtonInfo[] = [];
1359
+
1360
+ function traverse(node: any, parentPath: string[] = []): void {
1361
+ if (!node || typeof node !== "object") return;
1362
+
1363
+ // Check if this component has an action tag or actions defined
1364
+ const hasActionTag =
1365
+ Array.isArray(node.tags) &&
1366
+ node.tags.some((tag: string) => tag.startsWith("action:"));
1367
+ const hasActions = node.actions && typeof node.actions === "object";
1368
+
1369
+ if (hasActionTag || hasActions) {
1370
+ // Determine the action type
1371
+ let actionType = "tap";
1372
+ if (Array.isArray(node.tags)) {
1373
+ const actionTag = node.tags.find((tag: string) =>
1374
+ tag.startsWith("action:")
1375
+ );
1376
+ if (actionTag) {
1377
+ actionType = actionTag.replace("action:", "");
1378
+ }
1379
+ }
1380
+ if (node.actions?.tap?.action) {
1381
+ actionType = node.actions.tap.action;
1382
+ }
1383
+
1384
+ const basePropName = sanitizePropName(node.name || "button");
1385
+ buttons.push({
1386
+ id: node.id,
1387
+ name: node.name || "Button",
1388
+ propName: basePropName,
1389
+ actionType,
1390
+ _parentPath: [...parentPath],
1391
+ });
1392
+ }
1393
+
1394
+ // Build parent path for qualification
1395
+ const currentParentPath = [...parentPath];
1396
+ if (
1397
+ node.name &&
1398
+ (node.type?.startsWith("container:") ||
1399
+ node.type?.startsWith("component:"))
1400
+ ) {
1401
+ const name = node.name.trim();
1402
+ const isGenericFrame =
1403
+ /^frame\s*\d*$/i.test(name) || name.toUpperCase() === "FRAME";
1404
+ if (name && !isGenericFrame) {
1405
+ currentParentPath.push(node.name);
1406
+ }
1407
+ }
1408
+
1409
+ // Recursively traverse children
1410
+ if (node.body && Array.isArray(node.body)) {
1411
+ node.body.forEach((child: any) => traverse(child, currentParentPath));
1412
+ }
1413
+ if (node.containers && Array.isArray(node.containers)) {
1414
+ node.containers.forEach((child: any) =>
1415
+ traverse(child, currentParentPath)
1416
+ );
1417
+ }
1418
+ if (node.components && Array.isArray(node.components)) {
1419
+ node.components.forEach((child: any) =>
1420
+ traverse(child, currentParentPath)
1421
+ );
1422
+ }
1423
+ }
1424
+
1425
+ // Start traversal from page data
1426
+ const body =
1427
+ pageData.data?.body || pageData.body || (pageData as any).data?.body || [];
1428
+
1429
+ if (Array.isArray(body) && body.length > 0) {
1430
+ body.forEach((node: any) => traverse(node));
1431
+ }
1432
+
1433
+ // Qualify duplicate prop names
1434
+ qualifyActionButtons(buttons);
1435
+
1436
+ return buttons;
1437
+ }
1438
+
1439
+ /**
1440
+ * Qualifies action button prop names to ensure uniqueness.
1441
+ */
1442
+ function qualifyActionButtons(buttons: ActionButtonInfo[]): void {
1443
+ const propNameGroups = new Map<
1444
+ string,
1445
+ Array<ActionButtonInfo & { _parentPath: string[] }>
1446
+ >();
1447
+
1448
+ buttons.forEach((button) => {
1449
+ const buttonWithPath = button as ActionButtonInfo & {
1450
+ _parentPath: string[];
1451
+ };
1452
+ const baseName = button.propName;
1453
+ if (!propNameGroups.has(baseName)) {
1454
+ propNameGroups.set(baseName, []);
1455
+ }
1456
+ propNameGroups.get(baseName)!.push(buttonWithPath);
1457
+ });
1458
+
1459
+ propNameGroups.forEach((group, _baseName) => {
1460
+ if (group.length === 1) {
1461
+ delete (group[0] as any)._parentPath;
1462
+ return;
1463
+ }
1464
+
1465
+ // Find minimal distinguishing paths for duplicates
1466
+ group.forEach((button) => {
1467
+ const otherPaths = group
1468
+ .filter((b) => b.id !== button.id)
1469
+ .map((b) => b._parentPath || []);
1470
+
1471
+ const minimalPath = findMinimalDistinguishingPath(
1472
+ button._parentPath || [],
1473
+ otherPaths
1474
+ );
1475
+
1476
+ button.propName = generateQualifiedPropName(
1477
+ button.name || "button",
1478
+ minimalPath
1479
+ );
1480
+ });
1481
+
1482
+ // Clean up
1483
+ group.forEach((button) => {
1484
+ delete (button as any)._parentPath;
1485
+ });
1486
+ });
1487
+ }
1488
+
1489
+ /**
1490
+ * Qualifies form input prop names to ensure uniqueness within each form.
1491
+ * Only qualifies where necessary, using minimal distinguishing paths.
1492
+ */
1493
+ function qualifyFormInputs(forms: FormInfo[]): void {
1494
+ forms.forEach((form) => {
1495
+ const inputs = form.inputs;
1496
+
1497
+ // Group inputs by base prop name
1498
+ const propNameGroups = new Map<
1499
+ string,
1500
+ Array<FormInfo["inputs"][0] & { _parentPath: string[] }>
1501
+ >();
1502
+
1503
+ inputs.forEach((input) => {
1504
+ const inputWithPath = input as FormInfo["inputs"][0] & {
1505
+ _parentPath: string[];
1506
+ };
1507
+ const baseName = input.propName;
1508
+ if (!propNameGroups.has(baseName)) {
1509
+ propNameGroups.set(baseName, []);
1510
+ }
1511
+ propNameGroups.get(baseName)!.push(inputWithPath);
1512
+ });
1513
+
1514
+ // For each group with duplicates, find minimal distinguishing paths
1515
+ propNameGroups.forEach((group, _baseName) => {
1516
+ if (group.length === 1) {
1517
+ // No duplicates, keep the simple name
1518
+ // Remove the temporary _parentPath property
1519
+ delete (group[0] as any)._parentPath;
1520
+ return;
1521
+ }
1522
+
1523
+ // Find minimal distinguishing paths for all inputs
1524
+ group.forEach((input) => {
1525
+ const otherPaths = group
1526
+ .filter((i) => i.id !== input.id)
1527
+ .map((i) => i._parentPath || []);
1528
+
1529
+ const minimalPath = findMinimalDistinguishingPath(
1530
+ input._parentPath || [],
1531
+ otherPaths
1532
+ );
1533
+
1534
+ // Use the minimal distinguishing path to qualify the name
1535
+ input.propName = generateQualifiedPropName(
1536
+ input.name || "input",
1537
+ minimalPath
1538
+ );
1539
+ });
1540
+
1541
+ // Check if qualified names are still duplicates and expand paths if needed
1542
+ let hasDuplicates = true;
1543
+ let iteration = 0;
1544
+ const maxIterations = 10; // Safety limit
1545
+
1546
+ while (hasDuplicates && iteration < maxIterations) {
1547
+ iteration++;
1548
+ const qualifiedNameGroups = new Map<
1549
+ string,
1550
+ Array<FormInfo["inputs"][0] & { _parentPath: string[] }>
1551
+ >();
1552
+ group.forEach((input) => {
1553
+ if (!qualifiedNameGroups.has(input.propName)) {
1554
+ qualifiedNameGroups.set(input.propName, []);
1555
+ }
1556
+ qualifiedNameGroups.get(input.propName)!.push(input);
1557
+ });
1558
+
1559
+ hasDuplicates = false;
1560
+ // For each group of still-duplicated qualified names, expand their paths
1561
+ qualifiedNameGroups.forEach((dupGroup, _qualifiedName) => {
1562
+ if (dupGroup.length > 1) {
1563
+ hasDuplicates = true;
1564
+ // Expand the distinguishing path for each duplicate
1565
+ dupGroup.forEach((input) => {
1566
+ const fullPath = input._parentPath || [];
1567
+ const otherFullPaths = dupGroup
1568
+ .filter((i) => i.id !== input.id)
1569
+ .map((i) => i._parentPath || []);
1570
+
1571
+ // Find where this path diverges from others in the duplicate group
1572
+ let commonPrefixLength = 0;
1573
+ const maxCommonLength = Math.min(
1574
+ fullPath.length,
1575
+ ...otherFullPaths.map((p) => p.length)
1576
+ );
1577
+
1578
+ for (let i = 0; i < maxCommonLength; i++) {
1579
+ const thisPart = fullPath[i];
1580
+ const allMatch = otherFullPaths.every((otherPath) => {
1581
+ return otherPath[i] === thisPart;
1582
+ });
1583
+ if (allMatch) {
1584
+ commonPrefixLength++;
1585
+ } else {
1586
+ break;
1587
+ }
1588
+ }
1589
+
1590
+ // Use progressively more of the distinguishing suffix until unique
1591
+ const distinguishingSuffix = fullPath.slice(commonPrefixLength);
1592
+
1593
+ // Try expanding the distinguishing suffix until we find a unique name
1594
+ let foundUnique = false;
1595
+ for (
1596
+ let suffixLength = 1;
1597
+ suffixLength <= distinguishingSuffix.length;
1598
+ suffixLength++
1599
+ ) {
1600
+ const expandedPath = distinguishingSuffix.slice(
1601
+ 0,
1602
+ suffixLength
1603
+ );
1604
+ const testQualifiedName = generateQualifiedPropName(
1605
+ input.name || "input",
1606
+ expandedPath
1607
+ );
1608
+
1609
+ // Check if this qualified name is unique among ALL inputs in this form
1610
+ const isUnique = inputs.every((otherInput) => {
1611
+ if (otherInput.id === input.id) return true;
1612
+ // If other input is in the same duplicate group, compare expanded paths
1613
+ if (dupGroup.some((i) => i.id === otherInput.id)) {
1614
+ const otherFullPath =
1615
+ (
1616
+ otherInput as FormInfo["inputs"][0] & {
1617
+ _parentPath: string[];
1618
+ }
1619
+ )._parentPath || [];
1620
+ const otherCommonPrefixLength = Math.min(
1621
+ commonPrefixLength,
1622
+ otherFullPath.length
1623
+ );
1624
+ const otherDistinguishingSuffix = otherFullPath.slice(
1625
+ otherCommonPrefixLength
1626
+ );
1627
+ const otherExpandedPath = otherDistinguishingSuffix.slice(
1628
+ 0,
1629
+ suffixLength
1630
+ );
1631
+ const otherQualifiedName = generateQualifiedPropName(
1632
+ otherInput.name || "input",
1633
+ otherExpandedPath
1634
+ );
1635
+ return testQualifiedName !== otherQualifiedName;
1636
+ }
1637
+ // For inputs outside the duplicate group, just check the final prop name
1638
+ return testQualifiedName !== otherInput.propName;
1639
+ });
1640
+
1641
+ if (isUnique) {
1642
+ input.propName = testQualifiedName;
1643
+ foundUnique = true;
1644
+ break;
1645
+ }
1646
+ }
1647
+
1648
+ // If we couldn't find a unique name with the distinguishing suffix,
1649
+ // use the distinguishing suffix we found (it's the minimal we can do)
1650
+ if (!foundUnique) {
1651
+ input.propName = generateQualifiedPropName(
1652
+ input.name || "input",
1653
+ distinguishingSuffix.length > 0 ? distinguishingSuffix : []
1654
+ );
1655
+ }
1656
+ });
1657
+ }
1658
+ });
1659
+ }
1660
+
1661
+ // Final check: if there are still duplicates after using full paths,
1662
+ // and they have identical paths, use numeric suffixes as last resort
1663
+ const finalQualifiedNameGroups = new Map<
1664
+ string,
1665
+ Array<FormInfo["inputs"][0] & { _parentPath: string[] }>
1666
+ >();
1667
+ group.forEach((input) => {
1668
+ if (!finalQualifiedNameGroups.has(input.propName)) {
1669
+ finalQualifiedNameGroups.set(input.propName, []);
1670
+ }
1671
+ finalQualifiedNameGroups.get(input.propName)!.push(input);
1672
+ });
1673
+
1674
+ finalQualifiedNameGroups.forEach((finalDupGroup, finalQualifiedName) => {
1675
+ if (finalDupGroup.length > 1) {
1676
+ // Check if all duplicates have identical paths
1677
+ const allPathsIdentical = finalDupGroup.every((input) => {
1678
+ const thisPath = input._parentPath || [];
1679
+ return finalDupGroup.every((otherInput) => {
1680
+ if (otherInput.id === input.id) return true;
1681
+ const otherPath = otherInput._parentPath || [];
1682
+ return arraysEqual(thisPath, otherPath);
1683
+ });
1684
+ });
1685
+
1686
+ // Only use numeric suffixes if paths are truly identical
1687
+ if (allPathsIdentical) {
1688
+ let index = 0;
1689
+ finalDupGroup.forEach((input) => {
1690
+ if (index > 0) {
1691
+ input.propName = `${finalQualifiedName}${index + 1}`;
1692
+ }
1693
+ index++;
1694
+ });
1695
+ }
1696
+ }
1697
+ });
1698
+
1699
+ // Remove the temporary _parentPath property
1700
+ group.forEach((input) => {
1701
+ delete (input as any)._parentPath;
1702
+ });
1703
+ });
1704
+ });
1705
+ }
1706
+
1707
+ function generateComponentCode(
1708
+ appId: string,
1709
+ pageId: string,
1710
+ componentName: string,
1711
+ sliders: SliderInfo[],
1712
+ standaloneComponents: ComponentInfo[],
1713
+ inputGroups: InputGroupInfo[],
1714
+ forms: FormInfo[],
1715
+ selectInputs: SelectInputInfo[],
1716
+ actionButtons: ActionButtonInfo[],
1717
+ isProduction: boolean = false
1718
+ ): string {
1719
+ // Generate prop types
1720
+ const propTypes: string[] = [];
1721
+ const controlPropTypes: string[] = [];
1722
+ const inputGroupPropTypes: string[] = [];
1723
+ const formPropTypes: string[] = [];
1724
+ const selectInputPropTypes: string[] = [];
1725
+ const actionButtonPropTypes: string[] = [];
1726
+
1727
+ // Add standalone component props
1728
+ standaloneComponents.forEach((comp) => {
1729
+ propTypes.push(` ${comp.propName}?: ${comp.propType};`);
1730
+ });
1731
+
1732
+ // Add input group props
1733
+ inputGroups.forEach((group) => {
1734
+ const propName = sanitizePropName(group.groupName);
1735
+ inputGroupPropTypes.push(` ${propName}?: string;`);
1736
+ inputGroupPropTypes.push(
1737
+ ` on${propName[0].toUpperCase()}${propName.slice(
1738
+ 1
1739
+ )}Change?: (value: string) => void;`
1740
+ );
1741
+ });
1742
+
1743
+ // Generate form data interfaces and props
1744
+ const formDataInterfaces: string[] = [];
1745
+ forms.forEach((form) => {
1746
+ const formPropName = sanitizePropName(form.formName);
1747
+ const formDataTypeName = `${formPropName[0].toUpperCase()}${formPropName.slice(
1748
+ 1
1749
+ )}FormData`;
1750
+
1751
+ // Generate interface for form data with human-readable property names
1752
+ const formDataProps: string[] = [];
1753
+ form.inputs.forEach((input) => {
1754
+ const inputPropName = input.propName; // Use qualified prop name
1755
+ const inputType = getComponentPropType(input.type, input.name);
1756
+ formDataProps.push(` ${inputPropName}: ${inputType};`);
1757
+ });
1758
+
1759
+ formDataInterfaces.push(`export interface ${formDataTypeName} {
1760
+ ${formDataProps.join("\n")}
1761
+ }`);
1762
+
1763
+ // Add the callback prop with proper typing
1764
+ formPropTypes.push(
1765
+ ` on${formPropName[0].toUpperCase()}${formPropName.slice(
1766
+ 1
1767
+ )}Submit?: (formData: ${formDataTypeName}) => void;`
1768
+ );
1769
+ });
1770
+
1771
+ // Add standalone select input props (controlled value + onChange + options)
1772
+ selectInputs.forEach((input) => {
1773
+ const propName = input.propName;
1774
+ const capitalizedPropName = propName[0].toUpperCase() + propName.slice(1);
1775
+ selectInputPropTypes.push(` ${propName}?: string;`);
1776
+ selectInputPropTypes.push(
1777
+ ` ${propName}Options?: Array<string | { value: string; label: string }>;`
1778
+ );
1779
+ selectInputPropTypes.push(
1780
+ ` on${capitalizedPropName}Change?: (value: string) => void;`
1781
+ );
1782
+ });
1783
+
1784
+ // Add action button props (onClick)
1785
+ actionButtons.forEach((button) => {
1786
+ const propName = button.propName;
1787
+ const capitalizedPropName = propName[0].toUpperCase() + propName.slice(1);
1788
+ actionButtonPropTypes.push(` on${capitalizedPropName}Click?: () => void;`);
1789
+ });
1790
+
1791
+ sliders.forEach((slider) => {
1792
+ if (slider.arrayContainer && slider.arrayContainer.components.length > 0) {
1793
+ const container = slider.arrayContainer;
1794
+ const itemTypeName = `${container.propName[0].toUpperCase()}${container.propName.slice(
1795
+ 1
1796
+ )}Item`;
1797
+ propTypes.push(` ${container.propName}: ${itemTypeName}[];`);
1798
+ }
1799
+
1800
+ // Add control props for each slider/repeating container
1801
+ const sliderPropName = sanitizePropName(slider.name || "container");
1802
+ controlPropTypes.push(` ${sliderPropName}CurrentIndex?: number;`);
1803
+ controlPropTypes.push(
1804
+ ` on${sliderPropName[0].toUpperCase()}${sliderPropName.slice(
1805
+ 1
1806
+ )}IndexChange?: (index: number) => void;`
1807
+ );
1808
+ });
1809
+
1810
+ const allPropTypes = [
1811
+ ...propTypes,
1812
+ ...controlPropTypes,
1813
+ ...inputGroupPropTypes,
1814
+ ...formPropTypes,
1815
+ ...selectInputPropTypes,
1816
+ ...actionButtonPropTypes,
1817
+ ];
1818
+ const hasProps = allPropTypes.length > 0;
1819
+ const propsInterface = hasProps
1820
+ ? `export interface ${componentName}Props {
1821
+ ${allPropTypes.join("\n")}
1822
+ }`
1823
+ : "";
1824
+
1825
+ const itemTypes = sliders
1826
+ .filter((s) => s.arrayContainer && s.arrayContainer.components.length > 0)
1827
+ .map((slider) => {
1828
+ const container = slider.arrayContainer!;
1829
+ const itemTypeName = `${container.propName[0].toUpperCase()}${container.propName.slice(
1830
+ 1
1831
+ )}Item`;
1832
+ const itemProps = container.components
1833
+ .map((comp) => {
1834
+ return ` ${comp.propName}: ${comp.propType};`;
1835
+ })
1836
+ .join("\n");
1837
+ return `export interface ${itemTypeName} {
1838
+ ${itemProps}
1839
+ }`;
1840
+ })
1841
+ .join("\n\n");
1842
+
1843
+ const formDataTypes = formDataInterfaces.join("\n\n");
1844
+
1845
+ // Generate data mapping
1846
+ const dataMapping: string[] = [];
1847
+ const controlMapping: string[] = [];
1848
+
1849
+ // Add standalone component mappings
1850
+ standaloneComponents.forEach((comp) => {
1851
+ const propKey = getComponentPropName(comp.type);
1852
+ dataMapping.push(` // ${comp.name}
1853
+ ...(props.${comp.propName} !== undefined && { "${comp.id}": { ${propKey}: props.${comp.propName} } as any }),`);
1854
+ });
1855
+
1856
+ // Add select input mappings (for controlled values and options)
1857
+ selectInputs.forEach((input) => {
1858
+ dataMapping.push(` // ${input.name}
1859
+ ...((props.${input.propName} !== undefined || props.${input.propName}Options !== undefined) && {
1860
+ "${input.id}": {
1861
+ ...(props.${input.propName} !== undefined && { value: props.${input.propName} }),
1862
+ ...(props.${input.propName}Options !== undefined && { options: props.${input.propName}Options }),
1863
+ } as any
1864
+ }),`);
1865
+ });
1866
+
1867
+ sliders.forEach((slider) => {
1868
+ if (slider.arrayContainer && slider.arrayContainer.components.length > 0) {
1869
+ const container = slider.arrayContainer;
1870
+ const itemMapping = container.components
1871
+ .map((comp) => {
1872
+ const propKey = getComponentPropName(comp.type);
1873
+ return ` // ${comp.name}
1874
+ "${comp.id}": {
1875
+ ${propKey}: item.${comp.propName},
1876
+ }`;
1877
+ })
1878
+ .join(",\n");
1879
+
1880
+ dataMapping.push(` // ${container.name}
1881
+ "${container.id}": props.${container.propName}.map((item) => ({
1882
+ ${itemMapping}
1883
+ })),`);
1884
+ }
1885
+
1886
+ // Generate control mapping for each slider
1887
+ const sliderPropName = sanitizePropName(slider.name || "container");
1888
+ const controlPropName = `${sliderPropName[0].toUpperCase()}${sliderPropName.slice(
1889
+ 1
1890
+ )}IndexChange`;
1891
+ const controlEntry: string[] = [];
1892
+ if (slider.id) {
1893
+ controlEntry.push(` // ${slider.name}`);
1894
+ controlEntry.push(` "${slider.id}": {`);
1895
+ controlEntry.push(
1896
+ ` currentIndex: props.${sliderPropName}CurrentIndex,`
1897
+ );
1898
+ controlEntry.push(` onIndexChange: props.on${controlPropName},`);
1899
+ controlEntry.push(` }`);
1900
+ }
1901
+ if (controlEntry.length > 0) {
1902
+ controlMapping.push(controlEntry.join("\n"));
1903
+ }
1904
+ });
1905
+
1906
+ const repeatingContainerControlsCode =
1907
+ controlMapping.length > 0
1908
+ ? `\n repeatingContainerControls={{
1909
+ ${controlMapping.join(",\n")}
1910
+ }}`
1911
+ : "";
1912
+
1913
+ // Generate input group mappings
1914
+ const inputGroupMapping: string[] = [];
1915
+ const inputGroupHandlers: string[] = [];
1916
+
1917
+ // Generate form submission handlers
1918
+ // const formHandlers: string[] = [];
1919
+
1920
+ // Start action handler if we have input groups, forms, select inputs, or action buttons
1921
+ if (
1922
+ inputGroups.length > 0 ||
1923
+ forms.length > 0 ||
1924
+ selectInputs.length > 0 ||
1925
+ actionButtons.length > 0
1926
+ ) {
1927
+ inputGroupHandlers.push(` const handleAction = (payload: any) => {`);
1928
+ inputGroupHandlers.push(` const { action } = payload?.bravo || {};`);
1929
+ inputGroupHandlers.push(``);
1930
+
1931
+ // Add select input handling
1932
+ if (selectInputs.length > 0) {
1933
+ inputGroupHandlers.push(` // Handle select input changes`);
1934
+ inputGroupHandlers.push(
1935
+ ` if (action?.action === "input-change" || action?.action === "select-change") {`
1936
+ );
1937
+ inputGroupHandlers.push(` const nodeId = action?.nodeId;`);
1938
+ inputGroupHandlers.push(` const value = action?.params?.value;`);
1939
+ inputGroupHandlers.push(``);
1940
+
1941
+ selectInputs.forEach((input) => {
1942
+ const propName = input.propName;
1943
+ const capitalizedPropName =
1944
+ propName[0].toUpperCase() + propName.slice(1);
1945
+ const handlerPropName = `on${capitalizedPropName}Change`;
1946
+ inputGroupHandlers.push(` // ${input.name}`);
1947
+ inputGroupHandlers.push(
1948
+ ` if (nodeId === "${input.id}" && props.${handlerPropName}) {`
1949
+ );
1950
+ inputGroupHandlers.push(` props.${handlerPropName}(value);`);
1951
+ inputGroupHandlers.push(` return;`);
1952
+ inputGroupHandlers.push(` }`);
1953
+ });
1954
+
1955
+ inputGroupHandlers.push(` }`);
1956
+ inputGroupHandlers.push(``);
1957
+ }
1958
+
1959
+ // Add action button handling
1960
+ if (actionButtons.length > 0) {
1961
+ inputGroupHandlers.push(` // Handle button clicks`);
1962
+ inputGroupHandlers.push(
1963
+ ` if (action?.action === "remote" || action?.action === "tap" || action?.action === "link") {`
1964
+ );
1965
+ inputGroupHandlers.push(` const nodeId = action?.nodeId;`);
1966
+ inputGroupHandlers.push(``);
1967
+
1968
+ actionButtons.forEach((button) => {
1969
+ const propName = button.propName;
1970
+ const capitalizedPropName =
1971
+ propName[0].toUpperCase() + propName.slice(1);
1972
+ const handlerPropName = `on${capitalizedPropName}Click`;
1973
+ inputGroupHandlers.push(` // ${button.name}`);
1974
+ inputGroupHandlers.push(
1975
+ ` if (nodeId === "${button.id}" && props.${handlerPropName}) {`
1976
+ );
1977
+ inputGroupHandlers.push(` props.${handlerPropName}();`);
1978
+ inputGroupHandlers.push(` return;`);
1979
+ inputGroupHandlers.push(` }`);
1980
+ });
1981
+
1982
+ inputGroupHandlers.push(` }`);
1983
+ inputGroupHandlers.push(``);
1984
+ }
1985
+
1986
+ // Add input group handling
1987
+ if (inputGroups.length > 0) {
1988
+ inputGroupHandlers.push(
1989
+ ` if (action?.action === "input-group-change") {`
1990
+ );
1991
+ inputGroupHandlers.push(
1992
+ ` const { groupName, value } = action.params;`
1993
+ );
1994
+
1995
+ inputGroups.forEach((group) => {
1996
+ const propName = sanitizePropName(group.groupName);
1997
+ const handlerPropName = `on${propName[0].toUpperCase()}${propName.slice(
1998
+ 1
1999
+ )}Change`;
2000
+ inputGroupHandlers.push(
2001
+ ` if (groupName === "${group.groupName}" && props.${handlerPropName}) {`
2002
+ );
2003
+ inputGroupHandlers.push(` props.${handlerPropName}(value);`);
2004
+ inputGroupHandlers.push(` return;`);
2005
+ inputGroupHandlers.push(` }`);
2006
+ });
2007
+
2008
+ inputGroupHandlers.push(` }`);
2009
+ inputGroupHandlers.push(``);
2010
+ }
2011
+
2012
+ // Add form submission handling
2013
+ if (forms.length > 0) {
2014
+ inputGroupHandlers.push(` if (action?.action === "submit") {`);
2015
+ inputGroupHandlers.push(` // Get form inputs from Encore state`);
2016
+ inputGroupHandlers.push(
2017
+ ` const formInputs = useEncoreState.getState().formInputs["${pageId}"] || {};`
2018
+ );
2019
+ inputGroupHandlers.push(` const submitNodeId = action?.nodeId;`);
2020
+ inputGroupHandlers.push(``);
2021
+
2022
+ forms.forEach((form, index) => {
2023
+ const formPropName = sanitizePropName(form.formName);
2024
+ const handlerPropName = `on${formPropName[0].toUpperCase()}${formPropName.slice(
2025
+ 1
2026
+ )}Submit`;
2027
+
2028
+ if (index > 0) {
2029
+ inputGroupHandlers.push(``);
2030
+ }
2031
+ inputGroupHandlers.push(
2032
+ ` // Form: ${form.formName} (${form.formId})`
2033
+ );
2034
+ // Check if this form's submit button was clicked (if submitButtonId is available)
2035
+ if (form.submitButtonId) {
2036
+ inputGroupHandlers.push(
2037
+ ` if (submitNodeId === "${form.submitButtonId}" && props.${handlerPropName}) {`
2038
+ );
2039
+ } else if (forms.length === 1) {
2040
+ // If only one form, don't check submit button ID
2041
+ inputGroupHandlers.push(` if (props.${handlerPropName}) {`);
2042
+ } else {
2043
+ // Multiple forms but no submit button ID - check all
2044
+ inputGroupHandlers.push(` if (props.${handlerPropName}) {`);
2045
+ }
2046
+ inputGroupHandlers.push(` // Extract form inputs for this form`);
2047
+ const formDataTypeName = `${formPropName[0].toUpperCase()}${formPropName.slice(
2048
+ 1
2049
+ )}FormData`;
2050
+ inputGroupHandlers.push(
2051
+ ` const formData: ${formDataTypeName} = {`
2052
+ );
2053
+ const formDataEntries: string[] = [];
2054
+ form.inputs.forEach((input) => {
2055
+ const inputPropName = input.propName; // Use qualified prop name
2056
+ formDataEntries.push(
2057
+ ` ${inputPropName}: formInputs["${input.id}"]`
2058
+ );
2059
+ });
2060
+ inputGroupHandlers.push(formDataEntries.join(",\n"));
2061
+ inputGroupHandlers.push(` };`);
2062
+ inputGroupHandlers.push(` props.${handlerPropName}(formData);`);
2063
+ inputGroupHandlers.push(
2064
+ ` // Note: Default form submission will still proceed after callback`
2065
+ );
2066
+ inputGroupHandlers.push(` }`);
2067
+ });
2068
+
2069
+ inputGroupHandlers.push(` }`);
2070
+ }
2071
+
2072
+ inputGroupHandlers.push(` };`);
2073
+ }
2074
+
2075
+ // Generate input groups code
2076
+ inputGroups.forEach((group) => {
2077
+ const propName = sanitizePropName(group.groupName);
2078
+ inputGroupMapping.push(
2079
+ ` ...(props.${propName} !== undefined && { ${propName}: props.${propName} }),`
2080
+ );
2081
+ });
2082
+
2083
+ const inputGroupsCode =
2084
+ inputGroupMapping.length > 0
2085
+ ? `\n inputGroups={{
2086
+ ${inputGroupMapping.join("\n")}
2087
+ }}`
2088
+ : "";
2089
+
2090
+ const onActionCode =
2091
+ inputGroupHandlers.length > 0 ? `\n onAction={handleAction}` : "";
2092
+
2093
+ const propsParameter = hasProps ? `props: ${componentName}Props` : "";
2094
+ const propsInterfaceSection = propsInterface ? `${propsInterface}\n\n` : "";
2095
+
2096
+ return `/**
2097
+ * ${componentName}
2098
+ *
2099
+ * Wrapper component for Encore Studio app.
2100
+ * See README.md for detailed documentation.
2101
+ */
2102
+
2103
+ import { EncoreApp${
2104
+ forms.length > 0 ? ", useEncoreState" : ""
2105
+ } } from "@bravostudioai/react";
2106
+ ${isProduction ? `import productionData from "./data.json";` : ""}
2107
+
2108
+ ${itemTypes ? `${itemTypes}\n\n` : ""}${
2109
+ formDataTypes ? `${formDataTypes}\n\n` : ""
2110
+ }${propsInterfaceSection}export function ${componentName}(${propsParameter}) {
2111
+ ${inputGroupHandlers.length > 0 ? inputGroupHandlers.join("\n") : ""}
2112
+ return (
2113
+ <EncoreApp
2114
+ appId="${appId}"
2115
+ pageId="${pageId}"
2116
+ ${isProduction ? `appDefinition={productionData.app}
2117
+ pageDefinition={productionData.page}
2118
+ componentCode={productionData.componentCode}` : ""}
2119
+ data={{
2120
+ ${dataMapping.join("\n")}
2121
+ }}${repeatingContainerControlsCode}${inputGroupsCode}${onActionCode}
2122
+ />
2123
+ );
2124
+ }
2125
+
2126
+ export default ${componentName};
2127
+ `;
2128
+ }
2129
+
2130
+ function generateReadme(
2131
+ appId: string,
2132
+ pageId: string,
2133
+ appName: string,
2134
+ pageName: string,
2135
+ componentName: string,
2136
+ sliders: SliderInfo[],
2137
+ standaloneComponents: ComponentInfo[],
2138
+ inputGroups: InputGroupInfo[],
2139
+ forms: FormInfo[],
2140
+ selectInputs: SelectInputInfo[],
2141
+ actionButtons: ActionButtonInfo[]
2142
+ ): string {
2143
+ const componentDocs: string[] = [];
2144
+ const controlDocs: string[] = [];
2145
+ const selectInputDocs: string[] = [];
2146
+ const actionButtonDocs: string[] = [];
2147
+
2148
+ // Add standalone component documentation
2149
+ standaloneComponents.forEach((comp) => {
2150
+ componentDocs.push(`### \`${comp.propName}\` (${comp.propType}, optional)
2151
+
2152
+ ${comp.name} (${comp.type}) - Component ID: ${comp.id}`);
2153
+ });
2154
+
2155
+ sliders.forEach((slider) => {
2156
+ if (slider.arrayContainer && slider.arrayContainer.components.length > 0) {
2157
+ const container = slider.arrayContainer;
2158
+
2159
+ // Generate component documentation
2160
+ const compDocs = container.components
2161
+ .map((comp) => {
2162
+ return `- \`${comp.propName}\` (${comp.propType}): ${comp.name} (${comp.type}) - Component ID: ${comp.id}`;
2163
+ })
2164
+ .join("\n");
2165
+
2166
+ componentDocs.push(`### \`${
2167
+ container.propName
2168
+ }\` (${container.propName[0].toUpperCase()}${container.propName.slice(
2169
+ 1
2170
+ )}Item[])
2171
+
2172
+ Array of items for "${container.name}" container (ID: ${container.id})
2173
+
2174
+ **Properties:**
2175
+
2176
+ ${compDocs}`);
2177
+ }
2178
+
2179
+ // Generate control documentation for each slider/repeating container
2180
+ const sliderPropName = sanitizePropName(slider.name || "container");
2181
+ const controlPropName = `${sliderPropName[0].toUpperCase()}${sliderPropName.slice(
2182
+ 1
2183
+ )}IndexChange`;
2184
+ controlDocs.push(`### \`${sliderPropName}CurrentIndex\` (number, optional)
2185
+
2186
+ Controls the currently visible slide/index for the "${slider.name}" container (ID: ${slider.id}).
2187
+
2188
+ When provided, the slider operates in controlled mode - the parent component controls which slide is displayed. When not provided, the slider manages its own state.
2189
+
2190
+ ### \`on${controlPropName}\` ((index: number) => void, optional)
2191
+
2192
+ Callback fired when the user navigates to a different slide. Called with the new slide index (0-based).
2193
+
2194
+ This event fires whenever the slide changes, whether by user interaction, automatic advancement, or programmatic control.`);
2195
+ });
2196
+
2197
+ const dataPropsSection =
2198
+ componentDocs.length > 0
2199
+ ? componentDocs.join("\n\n")
2200
+ : "This component currently has no data-bound props.";
2201
+
2202
+ const controlPropsSection =
2203
+ controlDocs.length > 0
2204
+ ? `## Control Props
2205
+
2206
+ These props allow you to imperatively control repeating containers (sliders, lists, etc.) and receive notifications when the current index changes.
2207
+
2208
+ ${controlDocs.join("\n\n")}`
2209
+ : "";
2210
+
2211
+ // Generate input group documentation
2212
+ const inputGroupDocs: string[] = [];
2213
+ inputGroups.forEach((group) => {
2214
+ const propName = sanitizePropName(group.groupName);
2215
+ const handlerPropName = `on${propName[0].toUpperCase()}${propName.slice(
2216
+ 1
2217
+ )}Change`;
2218
+ const elementsList = group.elements
2219
+ .map((el) => `- "${el.name}"`)
2220
+ .join("\n");
2221
+
2222
+ inputGroupDocs.push(`### \`${propName}\` (string, optional)
2223
+
2224
+ Sets which element is active in the "${group.groupName}" input group (type: ${group.groupType}).
2225
+
2226
+ **Available elements:**
2227
+ ${elementsList}
2228
+
2229
+ ### \`${handlerPropName}\` ((value: string) => void, optional)
2230
+
2231
+ Callback fired when the user selects a different element in the "${group.groupName}" input group. Called with the name of the selected element.`);
2232
+ });
2233
+
2234
+ const inputGroupPropsSection =
2235
+ inputGroupDocs.length > 0
2236
+ ? `## Input Group Props
2237
+
2238
+ These props allow you to control input groups (radio button-like behavior) and receive notifications when the selection changes.
2239
+
2240
+ ${inputGroupDocs.join("\n\n")}`
2241
+ : "";
2242
+
2243
+ // Generate form documentation
2244
+ const formDocs: string[] = [];
2245
+ forms.forEach((form) => {
2246
+ const formPropName = sanitizePropName(form.formName);
2247
+ const handlerPropName = `on${formPropName[0].toUpperCase()}${formPropName.slice(
2248
+ 1
2249
+ )}Submit`;
2250
+ const formDataTypeName = `${formPropName[0].toUpperCase()}${formPropName.slice(
2251
+ 1
2252
+ )}FormData`;
2253
+ const inputsList = form.inputs
2254
+ .map((input) => {
2255
+ const inputPropName = input.propName; // Use qualified prop name
2256
+ const inputType = getComponentPropType(input.type, input.name);
2257
+ return `- \`${inputPropName}\` (${inputType}) - ${input.name}`;
2258
+ })
2259
+ .join("\n");
2260
+
2261
+ formDocs.push(`### \`${handlerPropName}\` ((formData: ${formDataTypeName}) => void, optional)
2262
+
2263
+ Callback fired when the "${
2264
+ form.formName
2265
+ }" form is submitted. Called with a typed object containing all form input values with human-readable property names.
2266
+
2267
+ **Form data shape:**
2268
+ \`\`\`typescript
2269
+ interface ${formDataTypeName} {
2270
+ ${form.inputs
2271
+ .map((input) => {
2272
+ const inputPropName = input.propName; // Use qualified prop name
2273
+ const inputType = getComponentPropType(input.type, input.name);
2274
+ return ` ${inputPropName}: ${inputType};`;
2275
+ })
2276
+ .join("\n")}
2277
+ }
2278
+ \`\`\`
2279
+
2280
+ **Form inputs:**
2281
+ ${inputsList}`);
2282
+ });
2283
+
2284
+ const formPropsSection =
2285
+ formDocs.length > 0
2286
+ ? `## Form Submission Props
2287
+
2288
+ These props allow you to handle form submissions and access form input values.
2289
+
2290
+ ${formDocs.join("\n\n")}`
2291
+ : "";
2292
+
2293
+ // Generate select input documentation
2294
+ selectInputs.forEach((input) => {
2295
+ const propName = input.propName;
2296
+ const capitalizedPropName = propName[0].toUpperCase() + propName.slice(1);
2297
+ const handlerPropName = `on${capitalizedPropName}Change`;
2298
+ const optionsPropName = `${propName}Options`;
2299
+
2300
+ selectInputDocs.push(`### \`${propName}\` (string, optional)
2301
+
2302
+ Controls the selected value of the "${input.name}" dropdown (Component ID: ${input.id}).
2303
+
2304
+ When provided, the select input operates in controlled mode - the parent component controls the current value.
2305
+
2306
+ ### \`${optionsPropName}\` (Array<string | { value: string; label: string }>, optional)
2307
+
2308
+ Sets the available options for the "${input.name}" dropdown. Can be an array of strings (used as both value and label) or an array of objects with \`value\` and \`label\` properties.
2309
+
2310
+ **Example:**
2311
+ \`\`\`tsx
2312
+ // Simple string array
2313
+ ${optionsPropName}={["Option 1", "Option 2", "Option 3"]}
2314
+
2315
+ // Object array with separate values and labels
2316
+ ${optionsPropName}={[
2317
+ { value: "opt1", label: "Option 1" },
2318
+ { value: "opt2", label: "Option 2" },
2319
+ ]}
2320
+ \`\`\`
2321
+
2322
+ ### \`${handlerPropName}\` ((value: string) => void, optional)
2323
+
2324
+ Callback fired when the user selects a different option in the "${input.name}" dropdown. Called with the selected value.`);
2325
+ });
2326
+
2327
+ const selectInputPropsSection =
2328
+ selectInputDocs.length > 0
2329
+ ? `## Select Input Props
2330
+
2331
+ These props allow you to control select/dropdown inputs and respond to value changes.
2332
+
2333
+ ${selectInputDocs.join("\n\n")}`
2334
+ : "";
2335
+
2336
+ // Generate action button documentation
2337
+ actionButtons.forEach((button) => {
2338
+ const propName = button.propName;
2339
+ const capitalizedPropName = propName[0].toUpperCase() + propName.slice(1);
2340
+ const handlerPropName = `on${capitalizedPropName}Click`;
2341
+
2342
+ actionButtonDocs.push(`### \`${handlerPropName}\` (() => void, optional)
2343
+
2344
+ Callback fired when the "${button.name}" button is clicked (Component ID: ${button.id}).
2345
+
2346
+ Action type: \`${button.actionType}\``);
2347
+ });
2348
+
2349
+ const actionButtonPropsSection =
2350
+ actionButtonDocs.length > 0
2351
+ ? `## Action Button Props
2352
+
2353
+ These props allow you to respond to button clicks and other user interactions.
2354
+
2355
+ ${actionButtonDocs.join("\n\n")}`
2356
+ : "";
2357
+
2358
+ const propsSection = `## Props
2359
+
2360
+ ### Data Props
2361
+
2362
+ ${dataPropsSection}
2363
+
2364
+ ${controlPropsSection}
2365
+
2366
+ ${inputGroupPropsSection}
2367
+
2368
+ ${formPropsSection}
2369
+
2370
+ ${selectInputPropsSection}
2371
+
2372
+ ${actionButtonPropsSection}`;
2373
+
2374
+ const usageExample =
2375
+ sliders.length > 0 && sliders[0].arrayContainer
2376
+ ? `<${componentName}
2377
+ ${sliders
2378
+ .map((s) =>
2379
+ s.arrayContainer
2380
+ ? `${s.arrayContainer.propName}={[
2381
+ {
2382
+ ${s.arrayContainer?.components
2383
+ .map(
2384
+ (c) =>
2385
+ `${c.propName}: "${
2386
+ c.type === "component:image"
2387
+ ? "https://example.com/image.jpg"
2388
+ : "Example value"
2389
+ }"`
2390
+ )
2391
+ .join(",\n ")}
2392
+ }
2393
+ ]}`
2394
+ : ""
2395
+ )
2396
+ .filter(Boolean)
2397
+ .join("\n ")}
2398
+ />`
2399
+ : `<${componentName} />`;
2400
+
2401
+ // Generate control example
2402
+ let controlExample = "";
2403
+ if (sliders.length > 0 && sliders[0]) {
2404
+ const firstSlider = sliders[0];
2405
+ const sliderPropName = sanitizePropName(firstSlider.name || "container");
2406
+ const controlPropName = `${sliderPropName[0].toUpperCase()}${sliderPropName.slice(
2407
+ 1
2408
+ )}IndexChange`;
2409
+
2410
+ controlExample = `## Controlling Slides
2411
+
2412
+ You can imperatively control which slide is displayed and listen for slide changes:
2413
+
2414
+ \`\`\`tsx
2415
+ import { useState } from "react";
2416
+ import { ${componentName} } from "./${componentName}";
2417
+
2418
+ function MyComponent() {
2419
+ const [currentSlide, setCurrentSlide] = useState(0);
2420
+
2421
+ return (
2422
+ <>
2423
+ <button onClick={() => setCurrentSlide((prev) => Math.max(0, prev - 1))}>
2424
+ Previous
2425
+ </button>
2426
+ <button onClick={() => setCurrentSlide((prev) => prev + 1)}>
2427
+ Next
2428
+ </button>
2429
+ <${componentName}
2430
+ ${sliders
2431
+ .map((s) =>
2432
+ s.arrayContainer
2433
+ ? `${s.arrayContainer.propName}={[/* array of items */]}`
2434
+ : ""
2435
+ )
2436
+ .filter(Boolean)
2437
+ .join("\n ")}
2438
+ ${sliderPropName}CurrentIndex={currentSlide}
2439
+ on${controlPropName}={(index) => setCurrentSlide(index)}
2440
+ />
2441
+ </>
2442
+ );
2443
+ }
2444
+ \`\`\``;
2445
+ }
2446
+
2447
+ return `# ${componentName}
2448
+
2449
+ Encore App Wrapper Component
2450
+
2451
+ This component wraps the Encore Studio app **"${appName}"** (App ID: \`${appId}\`) for the page **"${pageName}"** (Page ID: \`${pageId}\`).
2452
+
2453
+ The component automatically maps props to data-bound components within the app. Components marked with \`encore:data\` tags are exposed as props, allowing you to dynamically populate content.
2454
+
2455
+ ${propsSection}
2456
+
2457
+ ## Usage
2458
+
2459
+ \`\`\`tsx
2460
+ import { ${componentName} } from "./${componentName}";
2461
+
2462
+ function MyComponent() {
2463
+ return (
2464
+ <${componentName}
2465
+ ${sliders
2466
+ .map((s) =>
2467
+ s.arrayContainer
2468
+ ? `${s.arrayContainer.propName}={[/* array of items */]}`
2469
+ : ""
2470
+ )
2471
+ .filter(Boolean)
2472
+ .join("\n ")}
2473
+ />
2474
+ );
2475
+ }
2476
+ \`\`\`
2477
+
2478
+ ## Example
2479
+
2480
+ \`\`\`tsx
2481
+ ${usageExample}
2482
+ \`\`\`
2483
+
2484
+ ${controlExample}
2485
+ `;
2486
+ }
2487
+
2488
+ function generateNames(
2489
+ appName: string,
2490
+ pageName: string
2491
+ ): { directoryPath: string; componentName: string } {
2492
+ const appCamel = sanitizePropName(appName);
2493
+ const pageCamel = sanitizePropName(pageName);
2494
+ // Capitalize first letter for PascalCase
2495
+ const appPascal = appCamel.charAt(0).toUpperCase() + appCamel.slice(1);
2496
+ const pagePascal = pageCamel.charAt(0).toUpperCase() + pageCamel.slice(1);
2497
+ return {
2498
+ directoryPath: join(appPascal, pagePascal),
2499
+ componentName: pagePascal,
2500
+ };
2501
+ }
2502
+
2503
+ async function getAppPages(
2504
+ appId: string
2505
+ ): Promise<{ pages: any[]; appData: any }> {
2506
+ const url = `${APPS_SERVICE_URL}/devices/apps/${appId}`;
2507
+ console.log(`Fetching app data from ${url}...`);
2508
+ try {
2509
+ const content = await downloadFile(url);
2510
+ const appData = JSON.parse(content);
2511
+ const pages = appData?.app?.data?.pages || [];
2512
+ return { pages, appData };
2513
+ } catch (error) {
2514
+ console.error("Failed to fetch app data:", error);
2515
+ throw error;
2516
+ }
2517
+ }
2518
+
2519
+ async function generateWrapper({
2520
+ appId,
2521
+ pageId,
2522
+ outputPath,
2523
+ cachedAppData,
2524
+ isProduction,
2525
+ }: {
2526
+ appId: string;
2527
+ pageId: string;
2528
+ outputPath: string;
2529
+ cachedAppData?: any;
2530
+ isProduction?: boolean;
2531
+ }) {
2532
+ console.log(`Generating wrapper for app: ${appId}, page: ${pageId}`);
2533
+
2534
+ // Determine final output path - we'll update it after we get app/page names
2535
+ let finalOutputPath = outputPath;
2536
+ let needsFilenameGeneration = false;
2537
+ const pathExt = outputPath.split(".").pop()?.toLowerCase();
2538
+ if (
2539
+ pathExt !== "tsx" &&
2540
+ pathExt !== "ts" &&
2541
+ pathExt !== "jsx" &&
2542
+ pathExt !== "js"
2543
+ ) {
2544
+ // Path doesn't have a file extension, treat as directory
2545
+ const { stat } = await import("fs/promises");
2546
+ try {
2547
+ const pathStats = await stat(outputPath);
2548
+ if (pathStats.isDirectory()) {
2549
+ needsFilenameGeneration = true;
2550
+ }
2551
+ } catch {
2552
+ // Path doesn't exist, check if it looks like a directory (no extension)
2553
+ if (!pathExt || outputPath.endsWith("/")) {
2554
+ needsFilenameGeneration = true;
2555
+ }
2556
+ }
2557
+ }
2558
+
2559
+ if (needsFilenameGeneration) {
2560
+ console.log(
2561
+ `Output path is a directory, will generate filename from app/page names`
2562
+ );
2563
+ }
2564
+
2565
+ console.log(`Output path: ${outputPath}`);
2566
+
2567
+ // Create temp directory for downloaded files
2568
+ const tempDir = join(process.cwd(), ".temp-bravo", appId, pageId);
2569
+ if (!existsSync(tempDir)) {
2570
+ await mkdir(tempDir, { recursive: true });
2571
+ }
2572
+
2573
+ // Download files (skip app.json if we have cached data)
2574
+ const files: Array<{
2575
+ url: string;
2576
+ filename: string;
2577
+ headers?: Record<string, string>;
2578
+ skipIfCached?: boolean;
2579
+ }> = [
2580
+ {
2581
+ url: `${APPS_SERVICE_URL}/devices/apps/${appId}`,
2582
+ filename: "app.json",
2583
+ skipIfCached: true,
2584
+ },
2585
+ {
2586
+ url: `${APPS_SERVICE_URL}/devices/apps/${appId}/node/${pageId}`,
2587
+ filename: "page.json",
2588
+ },
2589
+ {
2590
+ url: `${COMPONENTS_CDN_URL}/${appId}/draft/components/${pageId}.js`,
2591
+ filename: "component.js",
2592
+ },
2593
+ ];
2594
+
2595
+ for (const file of files) {
2596
+ // Skip app.json if we have cached data
2597
+ if (file.skipIfCached && cachedAppData) {
2598
+ console.log(`Using cached ${file.filename} (skipping download)`);
2599
+ const filePath = join(tempDir, file.filename);
2600
+ await writeFile(
2601
+ filePath,
2602
+ JSON.stringify(cachedAppData, null, 2),
2603
+ "utf-8"
2604
+ );
2605
+ console.log(`āœ“ Saved ${file.filename} from cache`);
2606
+ continue;
2607
+ }
2608
+
2609
+ try {
2610
+ console.log(`Downloading ${file.filename} from ${file.url}...`);
2611
+ const content = await downloadFile(file.url, file.headers);
2612
+ const filePath = join(tempDir, file.filename);
2613
+ await writeFile(filePath, content, "utf-8");
2614
+ console.log(`āœ“ Saved ${file.filename}`);
2615
+ } catch (error) {
2616
+ console.error(`āœ— Failed to download ${file.filename}:`, error);
2617
+ throw error;
2618
+ }
2619
+ }
2620
+
2621
+ // Read and parse page.json
2622
+ const pageJsonPath = join(tempDir, "page.json");
2623
+ const pageJsonContent = await readFile(pageJsonPath, "utf-8");
2624
+ const pageResponse = JSON.parse(pageJsonContent);
2625
+
2626
+ // Extract page data - API response has it under 'data' field
2627
+ let pageData = pageResponse.data || pageResponse;
2628
+
2629
+ // Debug: log structure
2630
+ console.log("Page response keys:", Object.keys(pageResponse));
2631
+ if (pageResponse.data) {
2632
+ console.log("Page data keys:", Object.keys(pageResponse.data));
2633
+ }
2634
+ console.log("Has body?", !!pageData.body);
2635
+ console.log("Has data.body?", !!(pageData as any).data?.body);
2636
+
2637
+ // If pageData doesn't have body, try using cached app.json or downloading as fallback
2638
+ if (!pageData.body && !(pageData as any).data?.body) {
2639
+ console.log("Page data doesn't have body, trying app.json...");
2640
+ try {
2641
+ let appData = cachedAppData;
2642
+
2643
+ // If we don't have cached data, download it
2644
+ if (!appData) {
2645
+ const appUrl = `${APPS_SERVICE_URL}/devices/apps/${appId}`;
2646
+ const appContent = await downloadFile(appUrl);
2647
+ appData = JSON.parse(appContent);
2648
+ } else {
2649
+ console.log("Using cached app.json data");
2650
+ }
2651
+
2652
+ console.log("App data keys:", Object.keys(appData));
2653
+ console.log(
2654
+ "App.app keys:",
2655
+ appData?.app ? Object.keys(appData.app) : "N/A"
2656
+ );
2657
+ console.log(
2658
+ "App.app.data keys:",
2659
+ appData?.app?.data ? Object.keys(appData.app.data) : "N/A"
2660
+ );
2661
+
2662
+ // Find the page in app.data.pages
2663
+ const pages = appData?.app?.data?.pages || [];
2664
+ console.log(`Found ${pages.length} pages in app.json`);
2665
+ if (pages.length > 0) {
2666
+ console.log("First page ID:", pages[0]?.id);
2667
+ }
2668
+
2669
+ const page = pages.find((p: any) => p.id === pageId);
2670
+ if (page) {
2671
+ pageData = page;
2672
+ console.log("āœ“ Found page data in app.json");
2673
+ console.log("Page from app.json has body?", !!pageData.body);
2674
+ console.log(
2675
+ "Page body length:",
2676
+ Array.isArray(pageData.body) ? pageData.body.length : "N/A"
2677
+ );
2678
+ } else {
2679
+ console.warn(`Page ${pageId} not found in app.json`);
2680
+ console.warn(
2681
+ "Available page IDs:",
2682
+ pages.map((p: any) => p.id)
2683
+ );
2684
+
2685
+ // Try using saved-apps as fallback
2686
+ const savedPagePath = join(
2687
+ process.cwd(),
2688
+ "saved-apps",
2689
+ appId,
2690
+ `page-${pageId}.json`
2691
+ );
2692
+ if (existsSync(savedPagePath)) {
2693
+ console.log("Trying saved-apps page file...");
2694
+ try {
2695
+ const savedPageContent = await readFile(savedPagePath, "utf-8");
2696
+ const savedPageResponse = JSON.parse(savedPageContent);
2697
+ const savedPageData = savedPageResponse.data || savedPageResponse;
2698
+ if (savedPageData.body) {
2699
+ pageData = savedPageData;
2700
+ console.log("āœ“ Found page data in saved-apps");
2701
+ console.log(
2702
+ "Page body length:",
2703
+ Array.isArray(pageData.body) ? pageData.body.length : "N/A"
2704
+ );
2705
+ }
2706
+ } catch (error) {
2707
+ console.warn("Could not load saved-apps page:", error);
2708
+ }
2709
+ }
2710
+ }
2711
+ } catch (error) {
2712
+ console.warn("Could not load app.json:", error);
2713
+
2714
+ // Try using saved-apps as fallback
2715
+ const savedPagePath = join(
2716
+ process.cwd(),
2717
+ "saved-apps",
2718
+ appId,
2719
+ `page-${pageId}.json`
2720
+ );
2721
+ if (existsSync(savedPagePath)) {
2722
+ console.log("Trying saved-apps page file...");
2723
+ try {
2724
+ const savedPageContent = await readFile(savedPagePath, "utf-8");
2725
+ const savedPageResponse = JSON.parse(savedPageContent);
2726
+ const savedPageData = savedPageResponse.data || savedPageResponse;
2727
+ if (savedPageData.body) {
2728
+ pageData = savedPageData;
2729
+ console.log("āœ“ Found page data in saved-apps");
2730
+ console.log(
2731
+ "Page body length:",
2732
+ Array.isArray(pageData.body) ? pageData.body.length : "N/A"
2733
+ );
2734
+ }
2735
+ } catch (error) {
2736
+ console.warn("Could not load saved-apps page:", error);
2737
+ }
2738
+ }
2739
+ }
2740
+ }
2741
+
2742
+ // Find sliders and data bindings
2743
+ const sliders = findSlidersAndDataBindings(pageData);
2744
+
2745
+ // Find standalone components with bravo:data tags
2746
+ const standaloneComponents = findStandaloneComponents(pageData);
2747
+
2748
+ // Extract app name and page name
2749
+ let appName = "Encore App";
2750
+ let pageName = "Page";
2751
+
2752
+ // Try to get app name from app.json (use cached data if available)
2753
+ try {
2754
+ let appDataForName = cachedAppData;
2755
+ if (!appDataForName) {
2756
+ const appJsonPath = join(tempDir, "app.json");
2757
+ if (existsSync(appJsonPath)) {
2758
+ const appJsonContent = await readFile(appJsonPath, "utf-8");
2759
+ appDataForName = JSON.parse(appJsonContent);
2760
+ }
2761
+ }
2762
+ if (appDataForName) {
2763
+ appName = appDataForName?.app?.store?.name || appName;
2764
+ console.log(`App name: ${appName}`);
2765
+ }
2766
+ } catch (error) {
2767
+ // Ignore errors, use defaults
2768
+ }
2769
+
2770
+ // Get page name from page data
2771
+ pageName = pageData.name || pageData.id || pageName;
2772
+ console.log(`Page name: ${pageName}`);
2773
+
2774
+ console.log(`\nFound ${sliders.length} slider(s):`);
2775
+ sliders.forEach((slider, index) => {
2776
+ console.log(`\n Slider ${index + 1}:`);
2777
+ console.log(` ID: ${slider.id}`);
2778
+ console.log(` Name: ${slider.name}`);
2779
+ if (slider.arrayContainer) {
2780
+ console.log(
2781
+ ` Array Container: ${slider.arrayContainer.name} (${slider.arrayContainer.id})`
2782
+ );
2783
+ console.log(` Components:`);
2784
+ slider.arrayContainer.components.forEach((comp) => {
2785
+ console.log(
2786
+ ` - ${comp.name} (${comp.id}): ${comp.type} -> prop: ${comp.propName}: ${comp.propType}`
2787
+ );
2788
+ });
2789
+ } else {
2790
+ console.log(` No array container found`);
2791
+ }
2792
+ });
2793
+
2794
+ console.log(
2795
+ `\nFound ${standaloneComponents.length} standalone component(s):`
2796
+ );
2797
+ standaloneComponents.forEach((comp) => {
2798
+ console.log(
2799
+ ` - ${comp.name} (${comp.id}): ${comp.type} -> prop: ${comp.propName}: ${comp.propType}`
2800
+ );
2801
+ });
2802
+
2803
+ // Find input groups
2804
+ const inputGroups = findInputGroups(pageData);
2805
+ console.log(`\nFound ${inputGroups.length} input group(s):`);
2806
+ inputGroups.forEach((group, index) => {
2807
+ console.log(`\n Input Group ${index + 1}:`);
2808
+ console.log(` Name: ${group.groupName}`);
2809
+ console.log(` Type: ${group.groupType}`);
2810
+ console.log(` Elements:`);
2811
+ group.elements.forEach((el) => {
2812
+ console.log(` - ${el.name} (${el.id})`);
2813
+ });
2814
+ });
2815
+
2816
+ // Find forms
2817
+ const forms = findForms(pageData);
2818
+
2819
+ // Qualify form input prop names to ensure uniqueness
2820
+ qualifyFormInputs(forms);
2821
+
2822
+ console.log(`\nFound ${forms.length} form(s):`);
2823
+ forms.forEach((form, index) => {
2824
+ console.log(`\n Form ${index + 1}:`);
2825
+ console.log(` Name: ${form.formName}`);
2826
+ console.log(` ID: ${form.formId}`);
2827
+ if (form.submitButtonId) {
2828
+ console.log(` Submit Button ID: ${form.submitButtonId}`);
2829
+ }
2830
+ console.log(` Inputs:`);
2831
+ form.inputs.forEach((input) => {
2832
+ console.log(` - ${input.name} (${input.id}): ${input.type}`);
2833
+ });
2834
+ });
2835
+
2836
+ // Find standalone select inputs (not in forms)
2837
+ const selectInputs = findStandaloneSelectInputs(pageData, forms);
2838
+
2839
+ console.log(`\nFound ${selectInputs.length} standalone select input(s):`);
2840
+ selectInputs.forEach((input) => {
2841
+ console.log(
2842
+ ` - ${input.name} (${input.id}) -> prop: ${
2843
+ input.propName
2844
+ }, on${input.propName[0].toUpperCase()}${input.propName.slice(1)}Change`
2845
+ );
2846
+ });
2847
+
2848
+ // Find action buttons
2849
+ const actionButtons = findActionButtons(pageData);
2850
+
2851
+ console.log(`\nFound ${actionButtons.length} action button(s):`);
2852
+ actionButtons.forEach((button) => {
2853
+ console.log(
2854
+ ` - ${button.name} (${
2855
+ button.id
2856
+ }) -> on${button.propName[0].toUpperCase()}${button.propName.slice(
2857
+ 1
2858
+ )}Click (action: ${button.actionType})`
2859
+ );
2860
+ });
2861
+
2862
+ if (
2863
+ sliders.length === 0 &&
2864
+ standaloneComponents.length === 0 &&
2865
+ inputGroups.length === 0 &&
2866
+ forms.length === 0 &&
2867
+ selectInputs.length === 0 &&
2868
+ actionButtons.length === 0
2869
+ ) {
2870
+ console.warn(
2871
+ "\n⚠ No sliders, standalone components, input groups, forms, select inputs, or action buttons found. Generating empty wrapper."
2872
+ );
2873
+ }
2874
+
2875
+ // Generate component name and directory name
2876
+ const { directoryPath, componentName } = generateNames(appName, pageName);
2877
+ const directoryName = directoryPath;
2878
+
2879
+ // Update output path - always create a directory structure
2880
+ if (needsFilenameGeneration) {
2881
+ finalOutputPath = join(outputPath, directoryName);
2882
+ } else {
2883
+ // If a file path was provided, use its directory and create a subdirectory
2884
+ const providedDir = dirname(finalOutputPath);
2885
+ finalOutputPath = join(providedDir, directoryName);
2886
+ }
2887
+
2888
+ console.log(`Component directory: ${directoryName}`);
2889
+
2890
+ // Generate component code and README
2891
+ const componentCode = generateComponentCode(
2892
+ appId,
2893
+ pageId,
2894
+ componentName,
2895
+ sliders,
2896
+ standaloneComponents,
2897
+ inputGroups,
2898
+ forms,
2899
+ selectInputs,
2900
+ actionButtons,
2901
+ !!isProduction
2902
+ );
2903
+ const readmeContent = generateReadme(
2904
+ appId,
2905
+ pageId,
2906
+ appName,
2907
+ pageName,
2908
+ componentName,
2909
+ sliders,
2910
+ standaloneComponents,
2911
+ inputGroups,
2912
+ forms,
2913
+ selectInputs,
2914
+ actionButtons
2915
+ );
2916
+
2917
+ // Ensure output directory exists
2918
+ if (!existsSync(finalOutputPath)) {
2919
+ await mkdir(finalOutputPath, { recursive: true });
2920
+ }
2921
+
2922
+ // Write files
2923
+ if (isProduction) {
2924
+ const componentJsPath = join(tempDir, "component.js");
2925
+ let componentCodeContent = "";
2926
+ try {
2927
+ componentCodeContent = await readFile(componentJsPath, "utf-8");
2928
+ } catch (e) {
2929
+ console.warn("Could not read component.js for production build", e);
2930
+ }
2931
+
2932
+ let appDataForProd = cachedAppData;
2933
+ if (!appDataForProd) {
2934
+ try {
2935
+ const appJsonPath = join(tempDir, "app.json");
2936
+ const appJsonContent = await readFile(appJsonPath, "utf-8");
2937
+ appDataForProd = JSON.parse(appJsonContent);
2938
+ } catch(e) {
2939
+ console.warn("Could not read app.json for production build", e);
2940
+ }
2941
+ }
2942
+
2943
+ const productionData = {
2944
+ app: appDataForProd,
2945
+ page: pageData,
2946
+ componentCode: componentCodeContent
2947
+ };
2948
+ await writeFile(join(finalOutputPath, "data.json"), JSON.stringify(productionData, null, 2), "utf-8");
2949
+ console.log(`āœ“ Generated data.json for production mode`);
2950
+ }
2951
+ const indexPath = join(finalOutputPath, "index.tsx");
2952
+ const readmePath = join(finalOutputPath, "README.md");
2953
+
2954
+ await writeFile(indexPath, componentCode, "utf-8");
2955
+ await writeFile(readmePath, readmeContent, "utf-8");
2956
+
2957
+ console.log(`\nāœ“ Generated wrapper component at: ${indexPath}`);
2958
+ console.log(`āœ“ Generated documentation at: ${readmePath}`);
2959
+ }
2960
+
2961
+ function printUsage() {
2962
+ console.log(`
2963
+ Usage: generate-wrapper.ts <appId> [pageId] <outputPath>
2964
+
2965
+ Arguments:
2966
+ appId The Encore app ID
2967
+ pageId The Encore page ID (optional - if omitted, generates wrappers for ALL pages)
2968
+ outputPath Path where the generated TSX file(s) should be saved
2969
+
2970
+ Environment variables:
2971
+ APPS_SERVICE_URL Base URL for the apps service (default: http://localhost:3000)
2972
+
2973
+ Example:
2974
+ generate-wrapper.ts 01KA964B1T6KCKSKCNMYSTKRKZ 01KA964B2F42MN4WGCYDTG1Y70 ./src/components/MyEncoreApp.tsx
2975
+ generate-wrapper.ts 01KA964B1T6KCKSKCNMYSTKRKZ ./src/components/ (Generates all pages)
2976
+ `);
2977
+ }
2978
+
2979
+
2980
+ export async function runGenerate(args: string[]) {
2981
+ const isProduction = args.includes("--production");
2982
+ const cleanArgs = args.filter((arg) => arg !== "--production");
2983
+
2984
+ if (cleanArgs.length < 2 || cleanArgs.includes("--help") || cleanArgs.includes("-h")) {
2985
+ printUsage();
2986
+ process.exit(cleanArgs.includes("--help") || cleanArgs.includes("-h") ? 0 : 1);
2987
+ }
2988
+
2989
+ // Handle 2 arguments: appId, outputPath (generate for all pages)
2990
+ if (cleanArgs.length === 2) {
2991
+ const [appId, outputPath] = cleanArgs;
2992
+
2993
+ try {
2994
+ const { pages, appData } = await getAppPages(appId);
2995
+ console.log(`Found ${pages.length} pages.`);
2996
+
2997
+ if (pages.length === 0) {
2998
+ console.warn("No pages found for this app.");
2999
+ return;
3000
+ }
3001
+
3002
+ // Cache app.json data and reuse it for all pages
3003
+ console.log(
3004
+ `Caching app.json data for reuse across ${pages.length} page(s)...`
3005
+ );
3006
+
3007
+ for (const page of pages) {
3008
+ if (!page.id) continue;
3009
+ console.log(`\n----------------------------------------`);
3010
+ console.log(`Processing page: ${page.name || "Unnamed"} (${page.id})`);
3011
+ console.log(`----------------------------------------`);
3012
+ try { await generateWrapper({
3013
+ appId,
3014
+ pageId: page.id,
3015
+ outputPath,
3016
+ cachedAppData: appData,
3017
+ isProduction
3018
+ });
3019
+ } catch (error) {
3020
+ console.warn(`Error with page:`, error instanceof Error ? error.message : error);
3021
+ }
3022
+ }
3023
+ } catch (error) {
3024
+ console.error("\nError:", error instanceof Error ? error.message : error);
3025
+ process.exit(1);
3026
+ }
3027
+ return;
3028
+ }
3029
+
3030
+ const [appId, pageId, outputPath] = cleanArgs;
3031
+
3032
+ if (!appId || !pageId || !outputPath) {
3033
+ console.error("Error: Missing required arguments");
3034
+ printUsage();
3035
+ process.exit(1);
3036
+ }
3037
+
3038
+ try {
3039
+ await generateWrapper({ appId, pageId, outputPath, isProduction });
3040
+ } catch (error) {
3041
+ console.error("\nError:", error instanceof Error ? error.message : error);
3042
+ process.exit(1);
3043
+ }
3044
+ }
3045
+