@bravostudioai/react 0.1.0 → 0.1.1
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.
- package/dist/components/EncoreApp.js +145 -131
- package/dist/components/EncoreApp.js.map +1 -1
- package/dist/contexts/EncoreRouterContext.js +13 -0
- package/dist/contexts/EncoreRouterContext.js.map +1 -0
- package/dist/hooks/usePusherUpdates.js +4 -2
- package/dist/hooks/usePusherUpdates.js.map +1 -1
- package/dist/lib/dynamicModules.js +75 -85
- package/dist/lib/dynamicModules.js.map +1 -1
- package/dist/lib/moduleRegistry.js +20 -0
- package/dist/lib/moduleRegistry.js.map +1 -0
- package/dist/lib/packages.js +1 -3
- package/dist/lib/packages.js.map +1 -1
- package/dist/src/codegen/generator.d.ts +10 -0
- package/dist/src/codegen/generator.d.ts.map +1 -0
- package/dist/src/codegen/parser.d.ts +37 -0
- package/dist/src/codegen/parser.d.ts.map +1 -0
- package/dist/src/codegen/types.d.ts +53 -0
- package/dist/src/codegen/types.d.ts.map +1 -0
- package/dist/src/components/EncoreApp.d.ts.map +1 -1
- package/dist/src/contexts/EncoreRouterContext.d.ts +10 -0
- package/dist/src/contexts/EncoreRouterContext.d.ts.map +1 -0
- package/dist/src/hooks/useAuthRedirect.d.ts.map +1 -1
- package/dist/src/lib/dynamicModules.d.ts +1 -5
- package/dist/src/lib/dynamicModules.d.ts.map +1 -1
- package/dist/src/lib/moduleRegistry.d.ts +9 -0
- package/dist/src/lib/moduleRegistry.d.ts.map +1 -0
- package/dist/src/lib/packages.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/generate.ts +88 -2723
- package/src/codegen/generator.ts +877 -0
- package/src/codegen/index.ts +3 -0
- package/src/codegen/parser.ts +1614 -0
- package/src/codegen/types.ts +58 -0
- package/src/components/EncoreApp.tsx +20 -1
- package/src/contexts/EncoreRouterContext.ts +28 -0
- package/src/hooks/useAuthRedirect.ts +56 -55
- package/src/lib/packages.ts +8 -15
|
@@ -1,2488 +1,53 @@
|
|
|
1
|
+
import { promises as fs } from "fs";
|
|
2
|
+
import { join, dirname } from "path";
|
|
3
|
+
import { existsSync } from "fs";
|
|
4
|
+
import * as dotenv from "dotenv";
|
|
5
|
+
import {
|
|
6
|
+
CONST_APPS_SERVICE_URL,
|
|
7
|
+
CONST_COMPONENTS_CDN_URL,
|
|
8
|
+
} from "../../../constants";
|
|
9
|
+
import {
|
|
10
|
+
findSlidersAndDataBindings,
|
|
11
|
+
findStandaloneComponents,
|
|
12
|
+
findInputGroups,
|
|
13
|
+
findForms,
|
|
14
|
+
findStandaloneSelectInputs,
|
|
15
|
+
findActionButtons,
|
|
16
|
+
qualifyFormInputs,
|
|
17
|
+
generateComponentCode,
|
|
18
|
+
generateReadme,
|
|
19
|
+
sanitizePropName,
|
|
20
|
+
} from "../../codegen";
|
|
21
|
+
|
|
22
|
+
const { writeFile, mkdir, readFile } = fs;
|
|
1
23
|
|
|
24
|
+
dotenv.config();
|
|
2
25
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
}
|
|
26
|
+
// Default apps service URL (can be overridden with APPS_SERVICE_URL env var)
|
|
27
|
+
const APPS_SERVICE_URL =
|
|
28
|
+
process.env.APPS_SERVICE_URL ||
|
|
29
|
+
process.env.VITE_APPS_SERVICE_URL ||
|
|
30
|
+
CONST_APPS_SERVICE_URL ||
|
|
31
|
+
"https://apps-service-dev.bravostudio.app";
|
|
32
|
+
console.log(`Using APPS_SERVICE_URL: ${APPS_SERVICE_URL}`);
|
|
33
|
+
const COMPONENTS_CDN_URL =
|
|
34
|
+
CONST_COMPONENTS_CDN_URL || "https://apps-public-dev.bravostudio.app";
|
|
1885
35
|
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
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
|
-
}
|
|
36
|
+
async function downloadFile(
|
|
37
|
+
url: string,
|
|
38
|
+
headers?: Record<string, string>
|
|
39
|
+
): Promise<string> {
|
|
40
|
+
const response = await fetch(url, {
|
|
41
|
+
headers: headers || {},
|
|
1904
42
|
});
|
|
1905
43
|
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
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} }),`
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`Failed to download ${url}: ${response.status} ${response.statusText}`
|
|
2080
47
|
);
|
|
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
48
|
}
|
|
2446
49
|
|
|
2447
|
-
return
|
|
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
|
-
`;
|
|
50
|
+
return await response.text();
|
|
2486
51
|
}
|
|
2487
52
|
|
|
2488
53
|
function generateNames(
|
|
@@ -2626,14 +191,6 @@ async function generateWrapper({
|
|
|
2626
191
|
// Extract page data - API response has it under 'data' field
|
|
2627
192
|
let pageData = pageResponse.data || pageResponse;
|
|
2628
193
|
|
|
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
194
|
// If pageData doesn't have body, try using cached app.json or downloading as fallback
|
|
2638
195
|
if (!pageData.body && !(pageData as any).data?.body) {
|
|
2639
196
|
console.log("Page data doesn't have body, trying app.json...");
|
|
@@ -2645,111 +202,36 @@ async function generateWrapper({
|
|
|
2645
202
|
const appUrl = `${APPS_SERVICE_URL}/devices/apps/${appId}`;
|
|
2646
203
|
const appContent = await downloadFile(appUrl);
|
|
2647
204
|
appData = JSON.parse(appContent);
|
|
2648
|
-
} else {
|
|
2649
|
-
console.log("Using cached app.json data");
|
|
2650
205
|
}
|
|
2651
206
|
|
|
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
207
|
// Find the page in app.data.pages
|
|
2663
208
|
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
209
|
const page = pages.find((p: any) => p.id === pageId);
|
|
2670
210
|
if (page) {
|
|
2671
211
|
pageData = page;
|
|
2672
212
|
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
213
|
}
|
|
2711
214
|
} catch (error) {
|
|
2712
215
|
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
216
|
}
|
|
2740
217
|
}
|
|
2741
218
|
|
|
2742
|
-
// Find
|
|
219
|
+
// Find components using extracted logic
|
|
2743
220
|
const sliders = findSlidersAndDataBindings(pageData);
|
|
2744
|
-
|
|
2745
|
-
// Find standalone components with bravo:data tags
|
|
2746
221
|
const standaloneComponents = findStandaloneComponents(pageData);
|
|
222
|
+
const inputGroups = findInputGroups(pageData);
|
|
223
|
+
const forms = findForms(pageData);
|
|
224
|
+
|
|
225
|
+
// Qualify form input prop names
|
|
226
|
+
qualifyFormInputs(forms);
|
|
227
|
+
|
|
228
|
+
const selectInputs = findStandaloneSelectInputs(pageData, forms);
|
|
229
|
+
const actionButtons = findActionButtons(pageData);
|
|
2747
230
|
|
|
2748
231
|
// Extract app name and page name
|
|
2749
232
|
let appName = "Encore App";
|
|
2750
233
|
let pageName = "Page";
|
|
2751
234
|
|
|
2752
|
-
// Try to get app name from app.json (use cached data if available)
|
|
2753
235
|
try {
|
|
2754
236
|
let appDataForName = cachedAppData;
|
|
2755
237
|
if (!appDataForName) {
|
|
@@ -2761,132 +243,25 @@ async function generateWrapper({
|
|
|
2761
243
|
}
|
|
2762
244
|
if (appDataForName) {
|
|
2763
245
|
appName = appDataForName?.app?.store?.name || appName;
|
|
2764
|
-
console.log(`App name: ${appName}`);
|
|
2765
246
|
}
|
|
2766
247
|
} catch (error) {
|
|
2767
248
|
// Ignore errors, use defaults
|
|
2768
249
|
}
|
|
2769
250
|
|
|
2770
|
-
// Get page name from page data
|
|
2771
251
|
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
252
|
|
|
2875
253
|
// Generate component name and directory name
|
|
2876
254
|
const { directoryPath, componentName } = generateNames(appName, pageName);
|
|
2877
255
|
const directoryName = directoryPath;
|
|
2878
256
|
|
|
2879
|
-
// Update output path
|
|
257
|
+
// Update output path
|
|
2880
258
|
if (needsFilenameGeneration) {
|
|
2881
259
|
finalOutputPath = join(outputPath, directoryName);
|
|
2882
260
|
} else {
|
|
2883
|
-
// If a file path was provided, use its directory and create a subdirectory
|
|
2884
261
|
const providedDir = dirname(finalOutputPath);
|
|
2885
262
|
finalOutputPath = join(providedDir, directoryName);
|
|
2886
263
|
}
|
|
2887
264
|
|
|
2888
|
-
console.log(`Component directory: ${directoryName}`);
|
|
2889
|
-
|
|
2890
265
|
// Generate component code and README
|
|
2891
266
|
const componentCode = generateComponentCode(
|
|
2892
267
|
appId,
|
|
@@ -2900,6 +275,7 @@ async function generateWrapper({
|
|
|
2900
275
|
actionButtons,
|
|
2901
276
|
!!isProduction
|
|
2902
277
|
);
|
|
278
|
+
|
|
2903
279
|
const readmeContent = generateReadme(
|
|
2904
280
|
appId,
|
|
2905
281
|
pageId,
|
|
@@ -2925,37 +301,36 @@ async function generateWrapper({
|
|
|
2925
301
|
let componentCodeContent = "";
|
|
2926
302
|
try {
|
|
2927
303
|
componentCodeContent = await readFile(componentJsPath, "utf-8");
|
|
2928
|
-
} catch (e) {
|
|
2929
|
-
|
|
2930
|
-
}
|
|
2931
|
-
|
|
304
|
+
} catch (e) {}
|
|
305
|
+
|
|
2932
306
|
let appDataForProd = cachedAppData;
|
|
2933
307
|
if (!appDataForProd) {
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
console.warn("Could not read app.json for production build", e);
|
|
2940
|
-
}
|
|
308
|
+
try {
|
|
309
|
+
const appJsonPath = join(tempDir, "app.json");
|
|
310
|
+
const appJsonContent = await readFile(appJsonPath, "utf-8");
|
|
311
|
+
appDataForProd = JSON.parse(appJsonContent);
|
|
312
|
+
} catch (e) {}
|
|
2941
313
|
}
|
|
2942
314
|
|
|
2943
315
|
const productionData = {
|
|
2944
316
|
app: appDataForProd,
|
|
2945
317
|
page: pageData,
|
|
2946
|
-
componentCode: componentCodeContent
|
|
318
|
+
componentCode: componentCodeContent,
|
|
2947
319
|
};
|
|
2948
|
-
await writeFile(
|
|
2949
|
-
|
|
320
|
+
await writeFile(
|
|
321
|
+
join(finalOutputPath, "data.json"),
|
|
322
|
+
JSON.stringify(productionData, null, 2),
|
|
323
|
+
"utf-8"
|
|
324
|
+
);
|
|
2950
325
|
}
|
|
326
|
+
|
|
2951
327
|
const indexPath = join(finalOutputPath, "index.tsx");
|
|
2952
328
|
const readmePath = join(finalOutputPath, "README.md");
|
|
2953
329
|
|
|
2954
330
|
await writeFile(indexPath, componentCode, "utf-8");
|
|
2955
331
|
await writeFile(readmePath, readmeContent, "utf-8");
|
|
2956
332
|
|
|
2957
|
-
console.log(
|
|
2958
|
-
console.log(`✓ Generated documentation at: ${readmePath}`);
|
|
333
|
+
console.log(`✓ Generated wrapper component at: ${indexPath}`);
|
|
2959
334
|
}
|
|
2960
335
|
|
|
2961
336
|
function printUsage() {
|
|
@@ -2972,52 +347,49 @@ Environment variables:
|
|
|
2972
347
|
|
|
2973
348
|
Example:
|
|
2974
349
|
generate-wrapper.ts 01KA964B1T6KCKSKCNMYSTKRKZ 01KA964B2F42MN4WGCYDTG1Y70 ./src/components/MyEncoreApp.tsx
|
|
2975
|
-
generate-wrapper.ts 01KA964B1T6KCKSKCNMYSTKRKZ ./src/components/ (Generates all pages)
|
|
2976
350
|
`);
|
|
2977
351
|
}
|
|
2978
352
|
|
|
2979
|
-
|
|
2980
353
|
export async function runGenerate(args: string[]) {
|
|
2981
354
|
const isProduction = args.includes("--production");
|
|
2982
355
|
const cleanArgs = args.filter((arg) => arg !== "--production");
|
|
2983
356
|
|
|
2984
|
-
if (
|
|
357
|
+
if (
|
|
358
|
+
cleanArgs.length < 2 ||
|
|
359
|
+
cleanArgs.includes("--help") ||
|
|
360
|
+
cleanArgs.includes("-h")
|
|
361
|
+
) {
|
|
2985
362
|
printUsage();
|
|
2986
|
-
process.exit(
|
|
363
|
+
process.exit(
|
|
364
|
+
cleanArgs.includes("--help") || cleanArgs.includes("-h") ? 0 : 1
|
|
365
|
+
);
|
|
2987
366
|
}
|
|
2988
367
|
|
|
2989
|
-
// Handle 2 arguments: appId, outputPath (generate for all pages)
|
|
2990
368
|
if (cleanArgs.length === 2) {
|
|
2991
369
|
const [appId, outputPath] = cleanArgs;
|
|
2992
370
|
|
|
2993
371
|
try {
|
|
2994
372
|
const { pages, appData } = await getAppPages(appId);
|
|
2995
|
-
console.log(`Found ${pages.length} pages.`);
|
|
2996
|
-
|
|
2997
373
|
if (pages.length === 0) {
|
|
2998
374
|
console.warn("No pages found for this app.");
|
|
2999
375
|
return;
|
|
3000
376
|
}
|
|
3001
377
|
|
|
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
378
|
for (const page of pages) {
|
|
3008
379
|
if (!page.id) continue;
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
isProduction
|
|
3018
|
-
});
|
|
380
|
+
try {
|
|
381
|
+
await generateWrapper({
|
|
382
|
+
appId,
|
|
383
|
+
pageId: page.id,
|
|
384
|
+
outputPath,
|
|
385
|
+
cachedAppData: appData,
|
|
386
|
+
isProduction,
|
|
387
|
+
});
|
|
3019
388
|
} catch (error) {
|
|
3020
|
-
console.warn(
|
|
389
|
+
console.warn(
|
|
390
|
+
`Error with page ${page.id}:`,
|
|
391
|
+
error instanceof Error ? error.message : error
|
|
392
|
+
);
|
|
3021
393
|
}
|
|
3022
394
|
}
|
|
3023
395
|
} catch (error) {
|
|
@@ -3029,12 +401,6 @@ export async function runGenerate(args: string[]) {
|
|
|
3029
401
|
|
|
3030
402
|
const [appId, pageId, outputPath] = cleanArgs;
|
|
3031
403
|
|
|
3032
|
-
if (!appId || !pageId || !outputPath) {
|
|
3033
|
-
console.error("Error: Missing required arguments");
|
|
3034
|
-
printUsage();
|
|
3035
|
-
process.exit(1);
|
|
3036
|
-
}
|
|
3037
|
-
|
|
3038
404
|
try {
|
|
3039
405
|
await generateWrapper({ appId, pageId, outputPath, isProduction });
|
|
3040
406
|
} catch (error) {
|
|
@@ -3042,4 +408,3 @@ export async function runGenerate(args: string[]) {
|
|
|
3042
408
|
process.exit(1);
|
|
3043
409
|
}
|
|
3044
410
|
}
|
|
3045
|
-
|