@embeddables/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/README.md +116 -0
  2. package/bin/embeddables.mjs +2 -0
  3. package/dist/auth/index.d.ts +43 -0
  4. package/dist/auth/index.d.ts.map +1 -0
  5. package/dist/auth/index.js +100 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +75 -0
  9. package/dist/commands/build-workbench.d.ts +5 -0
  10. package/dist/commands/build-workbench.d.ts.map +1 -0
  11. package/dist/commands/build-workbench.js +122 -0
  12. package/dist/commands/build.d.ts +7 -0
  13. package/dist/commands/build.d.ts.map +1 -0
  14. package/dist/commands/build.js +22 -0
  15. package/dist/commands/dev.d.ts +11 -0
  16. package/dist/commands/dev.d.ts.map +1 -0
  17. package/dist/commands/dev.js +153 -0
  18. package/dist/commands/login.d.ts +2 -0
  19. package/dist/commands/login.d.ts.map +1 -0
  20. package/dist/commands/login.js +112 -0
  21. package/dist/commands/logout.d.ts +2 -0
  22. package/dist/commands/logout.d.ts.map +1 -0
  23. package/dist/commands/logout.js +18 -0
  24. package/dist/commands/pull.d.ts +7 -0
  25. package/dist/commands/pull.d.ts.map +1 -0
  26. package/dist/commands/pull.js +97 -0
  27. package/dist/compiler/errors.d.ts +20 -0
  28. package/dist/compiler/errors.d.ts.map +1 -0
  29. package/dist/compiler/errors.js +35 -0
  30. package/dist/compiler/evalStatic.d.ts +3 -0
  31. package/dist/compiler/evalStatic.d.ts.map +1 -0
  32. package/dist/compiler/evalStatic.js +57 -0
  33. package/dist/compiler/flatten.js +1 -0
  34. package/dist/compiler/helpers/duplicateIds.d.ts +9 -0
  35. package/dist/compiler/helpers/duplicateIds.d.ts.map +1 -0
  36. package/dist/compiler/helpers/duplicateIds.js +71 -0
  37. package/dist/compiler/index.d.ts +16 -0
  38. package/dist/compiler/index.d.ts.map +1 -0
  39. package/dist/compiler/index.js +934 -0
  40. package/dist/compiler/parsePage.d.ts +15 -0
  41. package/dist/compiler/parsePage.d.ts.map +1 -0
  42. package/dist/compiler/parsePage.js +562 -0
  43. package/dist/compiler/registry.d.ts +4 -0
  44. package/dist/compiler/registry.d.ts.map +1 -0
  45. package/dist/compiler/registry.js +44 -0
  46. package/dist/compiler/reverse.d.ts +17 -0
  47. package/dist/compiler/reverse.d.ts.map +1 -0
  48. package/dist/compiler/reverse.js +1632 -0
  49. package/dist/compiler/types.d.ts +21 -0
  50. package/dist/compiler/types.d.ts.map +1 -0
  51. package/dist/compiler/types.js +1 -0
  52. package/dist/components/index.d.ts +21 -0
  53. package/dist/components/index.d.ts.map +1 -0
  54. package/dist/components/index.js +21 -0
  55. package/dist/components/primitives/BaseComponent.d.ts +32 -0
  56. package/dist/components/primitives/BaseComponent.d.ts.map +1 -0
  57. package/dist/components/primitives/BaseComponent.js +26 -0
  58. package/dist/components/primitives/BookMeeting.d.ts +18 -0
  59. package/dist/components/primitives/BookMeeting.d.ts.map +1 -0
  60. package/dist/components/primitives/BookMeeting.js +5 -0
  61. package/dist/components/primitives/Chart.d.ts +41 -0
  62. package/dist/components/primitives/Chart.d.ts.map +1 -0
  63. package/dist/components/primitives/Chart.js +5 -0
  64. package/dist/components/primitives/Container.d.ts +8 -0
  65. package/dist/components/primitives/Container.d.ts.map +1 -0
  66. package/dist/components/primitives/Container.js +5 -0
  67. package/dist/components/primitives/CustomButton.d.ts +37 -0
  68. package/dist/components/primitives/CustomButton.d.ts.map +1 -0
  69. package/dist/components/primitives/CustomButton.js +10 -0
  70. package/dist/components/primitives/CustomHTML.d.ts +8 -0
  71. package/dist/components/primitives/CustomHTML.d.ts.map +1 -0
  72. package/dist/components/primitives/CustomHTML.js +5 -0
  73. package/dist/components/primitives/FileUpload.d.ts +18 -0
  74. package/dist/components/primitives/FileUpload.d.ts.map +1 -0
  75. package/dist/components/primitives/FileUpload.js +16 -0
  76. package/dist/components/primitives/InputBox.d.ts +34 -0
  77. package/dist/components/primitives/InputBox.d.ts.map +1 -0
  78. package/dist/components/primitives/InputBox.js +25 -0
  79. package/dist/components/primitives/Lottie.d.ts +11 -0
  80. package/dist/components/primitives/Lottie.d.ts.map +1 -0
  81. package/dist/components/primitives/Lottie.js +5 -0
  82. package/dist/components/primitives/MediaEmbed.d.ts +13 -0
  83. package/dist/components/primitives/MediaEmbed.d.ts.map +1 -0
  84. package/dist/components/primitives/MediaEmbed.js +6 -0
  85. package/dist/components/primitives/MediaImage.d.ts +8 -0
  86. package/dist/components/primitives/MediaImage.d.ts.map +1 -0
  87. package/dist/components/primitives/MediaImage.js +5 -0
  88. package/dist/components/primitives/OptionSelector.d.ts +35 -0
  89. package/dist/components/primitives/OptionSelector.d.ts.map +1 -0
  90. package/dist/components/primitives/OptionSelector.js +8 -0
  91. package/dist/components/primitives/PaypalCheckout.d.ts +25 -0
  92. package/dist/components/primitives/PaypalCheckout.d.ts.map +1 -0
  93. package/dist/components/primitives/PaypalCheckout.js +5 -0
  94. package/dist/components/primitives/PlainText.d.ts +6 -0
  95. package/dist/components/primitives/PlainText.d.ts.map +1 -0
  96. package/dist/components/primitives/PlainText.js +5 -0
  97. package/dist/components/primitives/ProgressBar.d.ts +15 -0
  98. package/dist/components/primitives/ProgressBar.d.ts.map +1 -0
  99. package/dist/components/primitives/ProgressBar.js +5 -0
  100. package/dist/components/primitives/RichText.d.ts +6 -0
  101. package/dist/components/primitives/RichText.d.ts.map +1 -0
  102. package/dist/components/primitives/RichText.js +5 -0
  103. package/dist/components/primitives/RichTextMarkdown.d.ts +6 -0
  104. package/dist/components/primitives/RichTextMarkdown.d.ts.map +1 -0
  105. package/dist/components/primitives/RichTextMarkdown.js +5 -0
  106. package/dist/components/primitives/Rive.d.ts +16 -0
  107. package/dist/components/primitives/Rive.d.ts.map +1 -0
  108. package/dist/components/primitives/Rive.js +8 -0
  109. package/dist/components/primitives/StripeCheckout.d.ts +52 -0
  110. package/dist/components/primitives/StripeCheckout.d.ts.map +1 -0
  111. package/dist/components/primitives/StripeCheckout.js +5 -0
  112. package/dist/components/primitives/StripeCheckout2.d.ts +30 -0
  113. package/dist/components/primitives/StripeCheckout2.d.ts.map +1 -0
  114. package/dist/components/primitives/StripeCheckout2.js +7 -0
  115. package/dist/proxy/injectApiInterceptor.d.ts +6 -0
  116. package/dist/proxy/injectApiInterceptor.d.ts.map +1 -0
  117. package/dist/proxy/injectApiInterceptor.js +66 -0
  118. package/dist/proxy/injectReload.d.ts +2 -0
  119. package/dist/proxy/injectReload.d.ts.map +1 -0
  120. package/dist/proxy/injectReload.js +14 -0
  121. package/dist/proxy/injectWorkbench.d.ts +4 -0
  122. package/dist/proxy/injectWorkbench.d.ts.map +1 -0
  123. package/dist/proxy/injectWorkbench.js +16 -0
  124. package/dist/proxy/server.d.ts +11 -0
  125. package/dist/proxy/server.d.ts.map +1 -0
  126. package/dist/proxy/server.js +246 -0
  127. package/dist/proxy/sse.d.ts +5 -0
  128. package/dist/proxy/sse.d.ts.map +1 -0
  129. package/dist/proxy/sse.js +17 -0
  130. package/dist/types-builder.d.ts +800 -0
  131. package/dist/types-builder.d.ts.map +1 -0
  132. package/dist/types-builder.js +20 -0
  133. package/dist/workbench/ActionsPanel.d.ts +6 -0
  134. package/dist/workbench/ActionsPanel.d.ts.map +1 -0
  135. package/dist/workbench/ActionsPanel.js +47 -0
  136. package/dist/workbench/AutofillPanel.d.ts +6 -0
  137. package/dist/workbench/AutofillPanel.d.ts.map +1 -0
  138. package/dist/workbench/AutofillPanel.js +543 -0
  139. package/dist/workbench/ComputedFieldsPanel.d.ts +6 -0
  140. package/dist/workbench/ComputedFieldsPanel.d.ts.map +1 -0
  141. package/dist/workbench/ComputedFieldsPanel.js +31 -0
  142. package/dist/workbench/ExperimentsPanel.d.ts +6 -0
  143. package/dist/workbench/ExperimentsPanel.d.ts.map +1 -0
  144. package/dist/workbench/ExperimentsPanel.js +182 -0
  145. package/dist/workbench/FieldEditorPanel.d.ts +9 -0
  146. package/dist/workbench/FieldEditorPanel.d.ts.map +1 -0
  147. package/dist/workbench/FieldEditorPanel.js +650 -0
  148. package/dist/workbench/InspectorPanel.d.ts +6 -0
  149. package/dist/workbench/InspectorPanel.d.ts.map +1 -0
  150. package/dist/workbench/InspectorPanel.js +341 -0
  151. package/dist/workbench/PageNavigator.d.ts +6 -0
  152. package/dist/workbench/PageNavigator.d.ts.map +1 -0
  153. package/dist/workbench/PageNavigator.js +123 -0
  154. package/dist/workbench/SchemaPanel.d.ts +6 -0
  155. package/dist/workbench/SchemaPanel.d.ts.map +1 -0
  156. package/dist/workbench/SchemaPanel.js +222 -0
  157. package/dist/workbench/UserDataPanel.d.ts +6 -0
  158. package/dist/workbench/UserDataPanel.d.ts.map +1 -0
  159. package/dist/workbench/UserDataPanel.js +350 -0
  160. package/dist/workbench/WorkbenchApp.d.ts +6 -0
  161. package/dist/workbench/WorkbenchApp.d.ts.map +1 -0
  162. package/dist/workbench/WorkbenchApp.js +193 -0
  163. package/dist/workbench/cloudflare-worker/README.md +31 -0
  164. package/dist/workbench/cloudflare-worker/public/workbench.css +1614 -0
  165. package/dist/workbench/cloudflare-worker/public/workbench.js +77 -0
  166. package/dist/workbench/cloudflare-worker/worker.js +40 -0
  167. package/dist/workbench/cloudflare-worker/wrangler.toml +10 -0
  168. package/dist/workbench/index.d.ts +9 -0
  169. package/dist/workbench/index.d.ts.map +1 -0
  170. package/dist/workbench/index.js +44 -0
  171. package/dist/workbench/workbench.css +1614 -0
  172. package/dist/workbench/workbench.js +77 -0
  173. package/package.json +79 -0
@@ -0,0 +1,1632 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import CSSJSON from 'cssjson';
4
+ import pc from 'picocolors';
5
+ import { TYPE_MAP } from './registry.js';
6
+ import { generateRandomIdByType } from './helpers/duplicateIds.js';
7
+ /**
8
+ * Deep clones an object or array while preserving the order of all properties
9
+ * at all nesting levels. Arrays preserve element order (already guaranteed),
10
+ * and objects preserve property insertion order.
11
+ */
12
+ function deepClonePreservingOrder(value) {
13
+ if (value === null || value === undefined) {
14
+ return value;
15
+ }
16
+ if (Array.isArray(value)) {
17
+ // For arrays, recursively clone each element
18
+ return value.map((item) => deepClonePreservingOrder(item));
19
+ }
20
+ if (typeof value === 'object') {
21
+ // For objects, preserve property order by iterating through keys
22
+ const cloned = {};
23
+ const keys = Object.keys(value);
24
+ for (const key of keys) {
25
+ cloned[key] = deepClonePreservingOrder(value[key]);
26
+ }
27
+ return cloned;
28
+ }
29
+ // Primitive values (string, number, boolean) are returned as-is
30
+ return value;
31
+ }
32
+ // Reverse map: JSON type -> TSX tag name
33
+ const REVERSE_TYPE_MAP = Object.fromEntries(Object.entries(TYPE_MAP).map(([k, v]) => [v, k]));
34
+ // Component types to ignore during reverse compilation
35
+ const IGNORED_COMPONENT_TYPES = new Set(['PageTitle']);
36
+ // Component properties to ignore during reverse compilation
37
+ const IGNORED_COMPONENT_PROPERTIES = new Set(['clearable', 'central_play_button']);
38
+ // Base component properties (from BaseComponentProps)
39
+ const BASE_COMPONENT_PROPS = new Set([
40
+ 'id',
41
+ 'key',
42
+ 'parent_id',
43
+ '_location',
44
+ 'hide',
45
+ 'conditions',
46
+ 'tags',
47
+ 'repeater_key',
48
+ 'element_id',
49
+ 'tooltip',
50
+ 'page_reactive_classes',
51
+ 'conditional_tags',
52
+ 'outputs_onmounted',
53
+ 'outputs_onunmounted',
54
+ 'outputs_onrepeatablerender',
55
+ 'always_rerender',
56
+ 'ignore_in_qa_tests',
57
+ 'ignore_recording',
58
+ ]);
59
+ // Input component properties (from InputComponentProps - extends BaseComponentProps)
60
+ const INPUT_COMPONENT_PROPS = new Set([...BASE_COMPONENT_PROPS, 'isRequired', 'doNotSave']);
61
+ // Required props for each component type (id and key are always required and handled separately)
62
+ const REQUIRED_PROPS = {
63
+ PlainText: new Set(['text']),
64
+ RichText: new Set(['text']),
65
+ RichTextMarkdown: new Set(['text']),
66
+ CustomButton: new Set(),
67
+ BookMeeting: new Set(['service', 'embedUrl']),
68
+ StripeCheckout: new Set(['action', 'publishable_key', 'amount']),
69
+ // Add more as needed based on component interfaces
70
+ };
71
+ // Component-specific valid properties (extends base props)
72
+ // This is a best-effort mapping - if a prop isn't here but matches common patterns, it's still allowed
73
+ const COMPONENT_SPECIFIC_PROPS = {
74
+ OptionSelector: new Set([
75
+ ...INPUT_COMPONENT_PROPS,
76
+ 'label',
77
+ 'conversions',
78
+ 'buttons',
79
+ 'multiple',
80
+ 'allow_deselect',
81
+ 'dropdown',
82
+ 'is_advanced_dropdown',
83
+ 'maxSelection',
84
+ 'block_selection_over_max',
85
+ 'checkbox',
86
+ 'layout',
87
+ 'checkbox_location',
88
+ 'allow_typing',
89
+ 'defaultVal',
90
+ 'preset_buttons',
91
+ 'button_repeater_key',
92
+ 'empty_invalid_message',
93
+ 'invalid_message',
94
+ 'validation_formula',
95
+ 'outputs_onchange',
96
+ 'fetch_uri',
97
+ 'fetch_headers',
98
+ 'fetch_transformer',
99
+ 'fetch_debounce',
100
+ 'outputs_onbuttonsrepeatablerender',
101
+ 'richDescription',
102
+ 'richText',
103
+ 'randomize_buttons',
104
+ 'always_disabled',
105
+ ]),
106
+ InputBox: new Set([
107
+ ...INPUT_COMPONENT_PROPS,
108
+ 'input_type',
109
+ 'label',
110
+ 'placeholder',
111
+ 'use_tel_input',
112
+ 'mismatch_values_message',
113
+ 'invalid_message',
114
+ 'empty_invalid_message',
115
+ 'max_length',
116
+ 'icon',
117
+ 'range_min',
118
+ 'range_max',
119
+ 'range_step',
120
+ 'width',
121
+ 'label_on_both_side',
122
+ 'off_label',
123
+ 'multiline',
124
+ 'trim_whitespace_on_blur',
125
+ 'format_email_on_blur',
126
+ 'validation_formula',
127
+ 'validate_on_blur',
128
+ 'outputs_onchange',
129
+ 'integer_only',
130
+ 'debounce_time',
131
+ 'disable_auto_next',
132
+ 'validate_value',
133
+ 'phone_display_format',
134
+ 'phone_store_format',
135
+ 'custom_validation_function',
136
+ 'always_disabled',
137
+ ]),
138
+ CustomButton: new Set([
139
+ ...BASE_COMPONENT_PROPS,
140
+ 'text',
141
+ 'action',
142
+ 'conversions',
143
+ 'actionUrl',
144
+ 'toggle',
145
+ 'icon',
146
+ 'emojiIcon',
147
+ 'imageUrl',
148
+ 'imageAltText',
149
+ 'richText',
150
+ 'description',
151
+ 'richDescription',
152
+ 'openUrlInNewTab',
153
+ 'ariaLabel',
154
+ 'info_box_key',
155
+ 'allow_skip_validation',
156
+ 'hide_if_no_nav_target',
157
+ 'needs_validation_passed',
158
+ 'validation_show_state',
159
+ 'shouldDownloadUrl',
160
+ 'downloadFilename',
161
+ 'outputs_onclick',
162
+ 'use_custom_code',
163
+ 'custom_code',
164
+ 'output_key',
165
+ 'track_clicks',
166
+ 'show_loading_until_actions_resolved',
167
+ 'success_message',
168
+ 'loading_message',
169
+ 'fail_message',
170
+ 'primary_button',
171
+ 'always_disabled',
172
+ ]),
173
+ PlainText: new Set([...BASE_COMPONENT_PROPS, 'text']),
174
+ RichText: new Set([...BASE_COMPONENT_PROPS, 'text']),
175
+ RichTextMarkdown: new Set([...BASE_COMPONENT_PROPS, 'text']),
176
+ MediaImage: new Set([...BASE_COMPONENT_PROPS, 'src', 'caption', 'alt_text']),
177
+ MediaEmbed: new Set([...BASE_COMPONENT_PROPS, 'src', 'caption']),
178
+ Container: new Set([...BASE_COMPONENT_PROPS, 'container_type']),
179
+ FileUpload: new Set([
180
+ ...INPUT_COMPONENT_PROPS,
181
+ 'label',
182
+ 'upload_type',
183
+ 'custom_upload_type',
184
+ 'icon',
185
+ 'files_max_limit',
186
+ ]),
187
+ Chart: new Set([
188
+ ...BASE_COMPONENT_PROPS,
189
+ 'chart_type',
190
+ 'data',
191
+ 'options',
192
+ 'scales',
193
+ 'maintain_aspect_ratio',
194
+ ]),
195
+ Rive: new Set([
196
+ ...BASE_COMPONENT_PROPS,
197
+ 'src',
198
+ 'autoplay',
199
+ 'artboard',
200
+ 'animation',
201
+ 'capture_events',
202
+ ]),
203
+ Lottie: new Set([...BASE_COMPONENT_PROPS, 'src', 'autoplay', 'loop']),
204
+ BookMeeting: new Set([
205
+ ...BASE_COMPONENT_PROPS,
206
+ 'service',
207
+ 'embedUrl',
208
+ 'next_on_complete',
209
+ 'next_on_complete_timeout',
210
+ 'isTesting',
211
+ ]),
212
+ StripeCheckout: new Set([
213
+ ...INPUT_COMPONENT_PROPS,
214
+ 'label',
215
+ 'action',
216
+ 'publishable_key',
217
+ 'amount',
218
+ 'currency',
219
+ 'description',
220
+ 'button_text',
221
+ 'pay_button_text',
222
+ 'use_new_stripe_elements',
223
+ ]),
224
+ StripeCheckout2: new Set([
225
+ ...INPUT_COMPONENT_PROPS,
226
+ 'label',
227
+ 'elements_to_display',
228
+ 'checkout_session',
229
+ 'init_checkout_options',
230
+ 'on_payment_complete_conversions',
231
+ ]),
232
+ PaypalCheckout: new Set([
233
+ ...INPUT_COMPONENT_PROPS,
234
+ 'label',
235
+ 'action',
236
+ 'amount',
237
+ 'currency',
238
+ 'description',
239
+ ]),
240
+ ProgressBar: new Set([...BASE_COMPONENT_PROPS, 'value', 'max', 'show_percentage']),
241
+ CustomHTML: new Set([...BASE_COMPONENT_PROPS, 'text']),
242
+ };
243
+ /**
244
+ * Checks if a component has all required props.
245
+ * Returns true if all required props are present, false otherwise.
246
+ * When fix is true: logs a warning and returns false if required props are missing.
247
+ * When fix is false/undefined: throws if required props are missing.
248
+ */
249
+ function hasRequiredProps(component, fix) {
250
+ const tagName = getComponentName(component.type);
251
+ const requiredProps = REQUIRED_PROPS[tagName] || REQUIRED_PROPS[component.type];
252
+ if (!requiredProps || requiredProps.size === 0) {
253
+ // No required props for this component type
254
+ return true;
255
+ }
256
+ const missingProps = [];
257
+ for (const prop of requiredProps) {
258
+ const value = component[prop];
259
+ if (value === null || value === undefined || value === '') {
260
+ missingProps.push(prop);
261
+ }
262
+ }
263
+ if (missingProps.length > 0) {
264
+ if (fix) {
265
+ console.warn(`Removed ${component.type} component (id: ${component.id}, key: ${component.key}) missing required props: ${missingProps.join(', ')}.`);
266
+ return false;
267
+ }
268
+ throw new Error(`${component.type} component (id: ${component.id}, key: ${component.key}) missing required props: ${missingProps.join(', ')}.`);
269
+ }
270
+ return true;
271
+ }
272
+ /**
273
+ * Validates props for a component.
274
+ * Returns valid prop keys to include in the generated JSX, and any extra (non-schema) props
275
+ * to store in the `props` attribute as a JSON object for round-trip preservation.
276
+ */
277
+ function validateComponentProps(component, validProps) {
278
+ const validPropKeys = new Set();
279
+ const extraProps = {};
280
+ // Always include id and key - these are handled separately but should be considered valid
281
+ const excludedProps = new Set([
282
+ 'id',
283
+ 'key',
284
+ 'tags',
285
+ 'type',
286
+ 'parent_id',
287
+ 'buttons',
288
+ ...IGNORED_COMPONENT_PROPERTIES,
289
+ ]);
290
+ for (const [key, value] of Object.entries(component)) {
291
+ // Skip excluded props (they're handled separately or ignored)
292
+ if (excludedProps.has(key)) {
293
+ continue;
294
+ }
295
+ // Skip null/undefined values
296
+ if (value === null || value === undefined) {
297
+ continue;
298
+ }
299
+ // Check if prop is valid
300
+ if (validProps.has(key)) {
301
+ validPropKeys.add(key);
302
+ }
303
+ else {
304
+ // Extra prop (not in schema) - store for round-trip via props={...}
305
+ extraProps[key] = value;
306
+ }
307
+ }
308
+ return { validPropKeys, extraProps };
309
+ }
310
+ // Fallback: if type not in map, use type name directly (component files match type names)
311
+ function getComponentName(type) {
312
+ // First try exact match
313
+ if (REVERSE_TYPE_MAP[type]) {
314
+ return REVERSE_TYPE_MAP[type];
315
+ }
316
+ // If not found, try case-insensitive match against TYPE_MAP values
317
+ // to find the correct casing and return the corresponding TSX tag name
318
+ const typeLower = type.toLowerCase();
319
+ for (const [tsxName, jsonType] of Object.entries(TYPE_MAP)) {
320
+ if (jsonType.toLowerCase() === typeLower) {
321
+ return tsxName; // Return the TSX tag name (e.g., "CustomHTML")
322
+ }
323
+ }
324
+ // If still not found, return type as-is (for unknown types)
325
+ return type;
326
+ }
327
+ /**
328
+ * Collects all component and button IDs from all pages and checks for duplicates.
329
+ * Returns a map of occurrence key (pageKey:componentIndex:buttonIndex) -> new unique ID for any duplicates found.
330
+ *
331
+ * Note: This function only deduplicates IDs, not keys. Duplicate keys are intentional
332
+ * in many cases and should be preserved.
333
+ */
334
+ function checkAndFixDuplicateIds(pages, fix) {
335
+ const idMapping = new Map(); // Maps occurrence key -> new unique ID
336
+ const idOccurrences = new Map();
337
+ const allIds = new Set();
338
+ // First pass: collect all IDs with their context
339
+ for (const page of pages) {
340
+ for (let compIndex = 0; compIndex < page.components.length; compIndex++) {
341
+ const component = page.components[compIndex];
342
+ // Handle components without type
343
+ if (!component.type) {
344
+ if (fix) {
345
+ // Will be removed in buildTree, skip here
346
+ continue;
347
+ }
348
+ throw new Error(`Component (id: ${component.id}, key: ${component.key}) is missing required type property.`);
349
+ }
350
+ // Skip ignored types
351
+ if (IGNORED_COMPONENT_TYPES.has(component.type)) {
352
+ continue;
353
+ }
354
+ const compId = component.id;
355
+ allIds.add(compId);
356
+ const occurrenceKey = `${page.key}:${compIndex}`;
357
+ if (!idOccurrences.has(compId)) {
358
+ idOccurrences.set(compId, []);
359
+ }
360
+ idOccurrences.get(compId).push({
361
+ pageKey: page.key,
362
+ type: `component (${component.type})`,
363
+ id: compId,
364
+ componentType: component.type,
365
+ componentIndex: compIndex,
366
+ occurrenceKey,
367
+ });
368
+ // Collect button IDs from OptionSelector components
369
+ if (component.type === 'OptionSelector' && component.buttons) {
370
+ const buttons = component.buttons;
371
+ if (Array.isArray(buttons)) {
372
+ for (let btnIndex = 0; btnIndex < buttons.length; btnIndex++) {
373
+ const button = buttons[btnIndex];
374
+ if (button && typeof button === 'object' && button.id) {
375
+ const buttonId = button.id;
376
+ allIds.add(buttonId);
377
+ const buttonOccurrenceKey = `${page.key}:${compIndex}:${btnIndex}`;
378
+ if (!idOccurrences.has(buttonId)) {
379
+ idOccurrences.set(buttonId, []);
380
+ }
381
+ idOccurrences.get(buttonId).push({
382
+ pageKey: page.key,
383
+ type: `button (in ${component.type} component)`,
384
+ id: buttonId,
385
+ componentType: component.type,
386
+ componentIndex: compIndex,
387
+ buttonIndex: btnIndex,
388
+ occurrenceKey: buttonOccurrenceKey,
389
+ });
390
+ }
391
+ }
392
+ }
393
+ }
394
+ }
395
+ }
396
+ // Second pass: find duplicates and create unique IDs
397
+ for (const [id, occurrences] of idOccurrences.entries()) {
398
+ if (occurrences.length > 1) {
399
+ // This ID is duplicated
400
+ const isButton = occurrences[0].buttonIndex !== undefined;
401
+ const suffix = isButton ? ' (OptionSelector button)' : '';
402
+ console.warn(`Found duplicate ID "${id}"${suffix} used ${occurrences.length} times:`);
403
+ for (let i = 0; i < occurrences.length; i++) {
404
+ const occ = occurrences[i];
405
+ const location = occ.buttonIndex !== undefined
406
+ ? `button at index ${occ.buttonIndex}`
407
+ : `component at index ${occ.componentIndex}`;
408
+ console.log(` - Page "${occ.pageKey}": ${occ.type} (${location}) with ID "${occ.id}"`);
409
+ }
410
+ // Create unique IDs for all but the first occurrence (type-based: plaintext_xxx, button_xxx, option_xxx)
411
+ for (let i = 1; i < occurrences.length; i++) {
412
+ const occ = occurrences[i];
413
+ const isOptionButton = occ.buttonIndex !== undefined;
414
+ const newId = generateRandomIdByType(occ.componentType, isOptionButton, allIds, idMapping.values());
415
+ allIds.add(newId);
416
+ // Map the occurrence key (not the ID) to the new unique ID
417
+ idMapping.set(occ.occurrenceKey, newId);
418
+ const location = occ.buttonIndex !== undefined
419
+ ? `button at index ${occ.buttonIndex}`
420
+ : `component at index ${occ.componentIndex}`;
421
+ console.log(` → Renamed duplicate ID "${occ.id}" to "${newId}" in page "${occ.pageKey}" (${location})`);
422
+ }
423
+ }
424
+ }
425
+ return idMapping;
426
+ }
427
+ /**
428
+ * Applies the ID mapping to all pages, updating component IDs and button IDs.
429
+ * The mapping uses occurrence keys (pageKey:componentIndex or pageKey:componentIndex:buttonIndex).
430
+ * Note: parent_id references are not automatically updated because we can't determine
431
+ * which specific occurrence a parent_id points to when there are duplicate IDs.
432
+ */
433
+ function applyIdMapping(pages, idMapping) {
434
+ // Update component and button IDs using occurrence keys
435
+ for (const page of pages) {
436
+ for (let compIndex = 0; compIndex < page.components.length; compIndex++) {
437
+ const component = page.components[compIndex];
438
+ // Skip components without type or with ignored types
439
+ if (!component.type || IGNORED_COMPONENT_TYPES.has(component.type)) {
440
+ continue;
441
+ }
442
+ const occurrenceKey = `${page.key}:${compIndex}`;
443
+ if (idMapping.has(occurrenceKey)) {
444
+ component.id = idMapping.get(occurrenceKey);
445
+ }
446
+ // Update button IDs in OptionSelector components
447
+ if (component.type === 'OptionSelector' && component.buttons) {
448
+ const buttons = component.buttons;
449
+ if (Array.isArray(buttons)) {
450
+ for (let btnIndex = 0; btnIndex < buttons.length; btnIndex++) {
451
+ const button = buttons[btnIndex];
452
+ if (button && typeof button === 'object' && button.id) {
453
+ const buttonOccurrenceKey = `${page.key}:${compIndex}:${btnIndex}`;
454
+ if (idMapping.has(buttonOccurrenceKey)) {
455
+ button.id = idMapping.get(buttonOccurrenceKey);
456
+ }
457
+ }
458
+ }
459
+ }
460
+ }
461
+ }
462
+ }
463
+ }
464
+ export async function reverseCompile(embeddable, embeddableId, opts) {
465
+ const fix = opts?.fix ?? false;
466
+ // Check for duplicate IDs across all pages and create a mapping
467
+ const idMapping = checkAndFixDuplicateIds(embeddable.pages, fix);
468
+ // Apply ID mapping to all pages
469
+ if (idMapping.size > 0) {
470
+ applyIdMapping(embeddable.pages, idMapping);
471
+ }
472
+ // Generate TSX pages
473
+ for (const page of embeddable.pages) {
474
+ await generatePageFile(page, embeddableId, fix);
475
+ }
476
+ // Generate CSS from styles
477
+ if (embeddable.styles && Object.keys(embeddable.styles).length > 0) {
478
+ await generateStylesFile(embeddable.styles, embeddableId);
479
+ }
480
+ // Generate config.json
481
+ await generateConfigFile(embeddable, embeddableId);
482
+ // Extract computedFields to JS files
483
+ if (embeddable.computedFields && embeddable.computedFields.length > 0) {
484
+ await extractComputedFields(embeddable.computedFields, embeddableId);
485
+ }
486
+ // Extract dataOutputs to JS files
487
+ if (embeddable.dataOutputs && embeddable.dataOutputs.length > 0) {
488
+ await extractDataOutputs(embeddable.dataOutputs, embeddableId);
489
+ }
490
+ // Extract global components to TSX files
491
+ if (embeddable.components &&
492
+ Array.isArray(embeddable.components) &&
493
+ embeddable.components.length > 0) {
494
+ await extractGlobalComponents(embeddable.components, embeddableId, fix);
495
+ }
496
+ console.log(`${pc.cyan(`✓ Generated ${embeddable.pages.length} page(s) and ${embeddable.styles && Object.keys(embeddable.styles).length > 0 ? 'styles' : 'no styles'}`)}`);
497
+ }
498
+ async function generatePageFile(page, embeddableId, fix) {
499
+ const pageKey = page.key;
500
+ const filePath = path.join('embeddables', embeddableId, 'pages', `${pageKey}.page.tsx`);
501
+ try {
502
+ // Build tree from flat component list
503
+ const tree = buildTree(page.components, pageKey, fix);
504
+ // Generate TSX code
505
+ const tsx = generateTSX(page, tree);
506
+ // Write to embeddables/<id>/pages/<pageKey>.page.tsx
507
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
508
+ fs.writeFileSync(filePath, tsx, 'utf8');
509
+ console.log(`${pc.gray(`Generated ${filePath}`)}`);
510
+ }
511
+ catch (error) {
512
+ if (error instanceof Error) {
513
+ throw new Error(`Page "${pageKey}" (${filePath}): ${error.message}`);
514
+ }
515
+ throw error;
516
+ }
517
+ }
518
+ function buildTree(components, pageKey, fix) {
519
+ // Filter out components without type and ignored component types
520
+ let filteredComponents = components.filter((comp) => {
521
+ if (!comp.type) {
522
+ if (fix) {
523
+ console.warn(`Removed component (id: ${comp.id}, key: ${comp.key}) – missing type property.`);
524
+ return false;
525
+ }
526
+ throw new Error(`Component (id: ${comp.id}, key: ${comp.key}) is missing required type property.`);
527
+ }
528
+ return !IGNORED_COMPONENT_TYPES.has(comp.type);
529
+ });
530
+ // Filter out components missing required props
531
+ filteredComponents = filteredComponents.filter((comp) => {
532
+ return hasRequiredProps(comp, fix);
533
+ });
534
+ // Create a map of id -> component
535
+ const componentMap = new Map();
536
+ for (const comp of filteredComponents) {
537
+ componentMap.set(comp.id, comp);
538
+ }
539
+ // Find root components (no parent_id)
540
+ const roots = [];
541
+ for (const comp of filteredComponents) {
542
+ if (!comp.parent_id) {
543
+ roots.push(comp);
544
+ }
545
+ }
546
+ // Allow blank pages - return null if no components
547
+ if (roots.length === 0) {
548
+ return null;
549
+ }
550
+ // Build tree recursively for each root
551
+ const rootNodes = roots.map((root) => buildNode(root, componentMap, pageKey, fix));
552
+ // Return single node or array of nodes
553
+ return rootNodes.length === 1 ? rootNodes[0] : rootNodes;
554
+ }
555
+ function buildNode(component, componentMap, pageKey, fix) {
556
+ const children = [];
557
+ // Find all children of this component (excluding ignored types and those missing required props)
558
+ for (const comp of componentMap.values()) {
559
+ if (comp.parent_id === component.id &&
560
+ !IGNORED_COMPONENT_TYPES.has(comp.type) &&
561
+ hasRequiredProps(comp, fix)) {
562
+ children.push(buildNode(comp, componentMap, pageKey, fix));
563
+ }
564
+ }
565
+ return {
566
+ component,
567
+ children,
568
+ };
569
+ }
570
+ function generateTSX(page, tree) {
571
+ const pageName = capitalizeFirst(page.key) + 'Page';
572
+ const pageKey = page.key;
573
+ const imports = tree ? collectImports(tree) : [];
574
+ const hasButtons = tree ? hasButtonConstants(tree) : false;
575
+ const { constants, nameMap } = tree
576
+ ? generateButtonConstants(tree)
577
+ : { constants: '', nameMap: new Map() };
578
+ const jsx = generateJSXFromTree(tree, 4, pageKey, nameMap);
579
+ return `"use client";
580
+
581
+ import React from "react";
582
+ ${imports
583
+ .map((imp) => `import { ${imp} } from "../../../src/components/primitives/${imp}";`)
584
+ .join('\n')}
585
+ ${hasButtons
586
+ ? `
587
+ import type { OptionSelectorButton } from "../../../src/types-builder";`
588
+ : ''}
589
+
590
+ export default function ${pageName}() {
591
+ ${constants}
592
+ return (
593
+ ${jsx}
594
+ );
595
+ }
596
+ `;
597
+ }
598
+ function collectImports(node) {
599
+ if (node === null) {
600
+ return [];
601
+ }
602
+ const imports = new Set();
603
+ const nodes = Array.isArray(node) ? node : [node];
604
+ for (const n of nodes) {
605
+ const type = n.component.type;
606
+ const tagName = getComponentName(type);
607
+ imports.add(tagName);
608
+ for (const child of n.children) {
609
+ for (const imp of collectImports(child)) {
610
+ imports.add(imp);
611
+ }
612
+ }
613
+ }
614
+ return Array.from(imports).sort();
615
+ }
616
+ function hasButtonConstants(node) {
617
+ if (node === null) {
618
+ return false;
619
+ }
620
+ function checkButtons(n) {
621
+ if (n.component.type === 'OptionSelector' && n.component.buttons) {
622
+ const buttons = n.component.buttons;
623
+ if (Array.isArray(buttons) && buttons.length > 0) {
624
+ return true;
625
+ }
626
+ }
627
+ for (const child of n.children) {
628
+ if (checkButtons(child)) {
629
+ return true;
630
+ }
631
+ }
632
+ return false;
633
+ }
634
+ const nodes = Array.isArray(node) ? node : [node];
635
+ for (const n of nodes) {
636
+ if (checkButtons(n)) {
637
+ return true;
638
+ }
639
+ }
640
+ return false;
641
+ }
642
+ function generateButtonConstants(node) {
643
+ if (node === null) {
644
+ return { constants: '', nameMap: new Map() };
645
+ }
646
+ const constants = [];
647
+ const usedNames = new Set(); // Track used constant names
648
+ const nameMap = new Map(); // Map component ID -> constant name
649
+ function collectButtons(n) {
650
+ if (n.component.type === 'OptionSelector' && n.component.buttons) {
651
+ const buttons = n.component.buttons;
652
+ if (Array.isArray(buttons) && buttons.length > 0) {
653
+ // Generate a constant name based on the component key or id
654
+ let baseName = camelCase(n.component.key || n.component.id) + 'Buttons';
655
+ let constName = baseName;
656
+ // Ensure uniqueness by checking if name is already used
657
+ let suffix = 1;
658
+ if (usedNames.has(constName)) {
659
+ // If name collision, append component ID suffix to ensure uniqueness
660
+ const idSuffix = camelCase(n.component.id);
661
+ constName = baseName.replace(/Buttons$/, '') + idSuffix + 'Buttons';
662
+ }
663
+ // If still not unique, add numeric suffix
664
+ while (usedNames.has(constName)) {
665
+ constName = baseName.replace(/Buttons$/, '') + suffix + 'Buttons';
666
+ suffix++;
667
+ }
668
+ usedNames.add(constName);
669
+ nameMap.set(n.component.id, constName);
670
+ const buttonArray = buttons
671
+ .map((b) => {
672
+ const props = [];
673
+ // Always include id - preserve from JSON or generate if missing
674
+ const buttonId = b.id || `option_${b.key}`;
675
+ props.push(`id: ${escapeStringForJS(buttonId)}`);
676
+ // Preserve key if present
677
+ if (b.key)
678
+ props.push(`key: ${escapeStringForJS(b.key)}`);
679
+ // Preserve text if present
680
+ if (b.text)
681
+ props.push(`text: ${escapeStringForJS(b.text)}`);
682
+ // Preserve other button properties from JSON (e.g., triggerEvent, description, imageUrl, etc.)
683
+ const buttonPropertyKeys = new Set(['id', 'key', 'text']);
684
+ for (const [key, value] of Object.entries(b)) {
685
+ if (buttonPropertyKeys.has(key))
686
+ continue;
687
+ if (value === null || value === undefined)
688
+ continue;
689
+ if (typeof value === 'string') {
690
+ props.push(`${key}: ${escapeStringForJS(value)}`);
691
+ }
692
+ else if (typeof value === 'number' || typeof value === 'boolean') {
693
+ props.push(`${key}: ${value}`);
694
+ }
695
+ else if (Array.isArray(value) || typeof value === 'object') {
696
+ // Handle arrays (e.g., conditions) and objects
697
+ props.push(`${key}: ${JSON.stringify(value)}`);
698
+ }
699
+ }
700
+ return `{ ${props.join(', ')} }`;
701
+ })
702
+ .join(',\n ');
703
+ constants.push(` const ${constName}: OptionSelectorButton[] = [\n ${buttonArray}\n ];`);
704
+ }
705
+ }
706
+ for (const child of n.children) {
707
+ collectButtons(child);
708
+ }
709
+ }
710
+ const nodes = Array.isArray(node) ? node : [node];
711
+ for (const n of nodes) {
712
+ collectButtons(n);
713
+ }
714
+ return {
715
+ constants: constants.length > 0 ? constants.join('\n') + '\n' : '',
716
+ nameMap,
717
+ };
718
+ }
719
+ function generateJSXFromTree(tree, indent = 4, pageKey, nameMap) {
720
+ if (tree === null) {
721
+ // Blank page - return empty fragment
722
+ const indentStr = ' '.repeat(indent);
723
+ return `${indentStr}<> </>`;
724
+ }
725
+ else if (Array.isArray(tree)) {
726
+ // Multiple root components - wrap in fragment
727
+ const childrenJSX = tree
728
+ .map((node) => generateJSX(node, indent + 2, pageKey, node.component.id, nameMap))
729
+ .join('\n');
730
+ const indentStr = ' '.repeat(indent);
731
+ return `${indentStr}<>\n${childrenJSX}\n${indentStr}</>`;
732
+ }
733
+ else {
734
+ // Single root component
735
+ return generateJSX(tree, indent, pageKey, tree.component.id, nameMap);
736
+ }
737
+ }
738
+ function generateJSX(node, indent = 4, pageKey, componentId, nameMap) {
739
+ const comp = node.component;
740
+ const type = comp.type;
741
+ const tagName = getComponentName(type);
742
+ // Note: We don't throw errors here, but if we did, we'd use the context
743
+ // The context is available for future error handling if needed
744
+ // Get valid props for this component type (default to base props if unknown)
745
+ const validProps = COMPONENT_SPECIFIC_PROPS[tagName] || COMPONENT_SPECIFIC_PROPS[type] || BASE_COMPONENT_PROPS;
746
+ // Validate props; get valid keys and any extra (non-schema) props to store in props={}
747
+ const { validPropKeys, extraProps } = validateComponentProps(comp, validProps);
748
+ const props = [];
749
+ // Always preserve component ID from JSON
750
+ props.push(`id=${escapeStringForJSX(comp.id)}`);
751
+ props.push(`key=${escapeStringForJSX(comp.key)}`);
752
+ // Add tags if present
753
+ if (comp.tags && Array.isArray(comp.tags) && comp.tags.length > 0) {
754
+ props.push(`tags={[${comp.tags.map((t) => `"${t}"`).join(', ')}]}`);
755
+ }
756
+ // Add other props (only include validated props, excluding id, key, tags, type, parent_id, buttons, _location, and ignored properties)
757
+ // _location is excluded because it's derived from the filename (e.g., before_page.location.tsx)
758
+ const excludedProps = new Set([
759
+ 'id',
760
+ 'key',
761
+ 'tags',
762
+ 'type',
763
+ 'parent_id',
764
+ 'buttons',
765
+ '_location',
766
+ ...IGNORED_COMPONENT_PROPERTIES,
767
+ ]);
768
+ for (const [key, value] of Object.entries(comp)) {
769
+ if (excludedProps.has(key))
770
+ continue;
771
+ // Only include props that were validated as valid
772
+ if (!validPropKeys.has(key))
773
+ continue;
774
+ if (value === null || value === undefined)
775
+ continue;
776
+ if (typeof value === 'string') {
777
+ // Handle style prop: convert CSS string to JSX style object
778
+ if (key === 'style') {
779
+ const styleObj = cssStringToStyleObject(value);
780
+ const styleStr = styleObjectToJSX(styleObj);
781
+ props.push(`style={${styleStr}}`);
782
+ }
783
+ else if (value.includes('{{')) {
784
+ // Handle template variables: convert {{variable}} strings to template literals
785
+ // e.g., "{{first_name}}'s plan" -> {`{{first_name}}'s plan`}
786
+ const templateLiteral = stringToTemplateLiteral(value);
787
+ props.push(`${key}={${templateLiteral}}`);
788
+ }
789
+ else {
790
+ props.push(`${key}=${escapeStringForJSX(value)}`);
791
+ }
792
+ }
793
+ else if (typeof value === 'number' || typeof value === 'boolean') {
794
+ props.push(`${key}={${value}}`);
795
+ }
796
+ else if (Array.isArray(value)) {
797
+ // Handle arrays (e.g., conditions, conditional_tags, outputs_onmounted, etc.)
798
+ // buttons are excluded and handled separately below
799
+ const arrayExpression = valueToJSXExpression(value);
800
+ props.push(`${key}={${arrayExpression}}`);
801
+ }
802
+ else if (typeof value === 'object') {
803
+ // Handle objects (e.g., nested condition objects, etc.)
804
+ const objectExpression = valueToJSXExpression(value);
805
+ props.push(`${key}={${objectExpression}}`);
806
+ }
807
+ }
808
+ // Handle OptionSelector buttons - use the name from the map if available
809
+ if (type === 'OptionSelector' && comp.buttons) {
810
+ const buttons = comp.buttons;
811
+ if (Array.isArray(buttons) && buttons.length > 0) {
812
+ const constName = nameMap?.get(comp.id) || camelCase(comp.key || comp.id) + 'Buttons';
813
+ props.push(`buttons={${constName}}`);
814
+ }
815
+ }
816
+ // Store extra (non-schema) props in a props={...} JSON object for round-trip preservation
817
+ if (Object.keys(extraProps).length > 0) {
818
+ props.push(`props={${valueToJSXExpression(extraProps)}}`);
819
+ }
820
+ // Handle CustomHTML: convert text prop to JSX children
821
+ let customHTMLChildren = null;
822
+ if (type === 'CustomHTML' && comp.text && typeof comp.text === 'string') {
823
+ // Remove text from props (we'll use children instead)
824
+ const textIndex = props.findIndex((p) => p.startsWith('text='));
825
+ if (textIndex !== -1) {
826
+ props.splice(textIndex, 1);
827
+ }
828
+ // Convert HTML to JSX children
829
+ customHTMLChildren = htmlToJSXChildren(comp.text, indent + 2);
830
+ }
831
+ const indentStr = ' '.repeat(indent);
832
+ const childIndent = indent + 2;
833
+ // If CustomHTML has children from HTML conversion, use those
834
+ if (customHTMLChildren !== null) {
835
+ return `${indentStr}<${tagName} ${props.join(' ')}>\n${customHTMLChildren}\n${indentStr}</${tagName}>`;
836
+ }
837
+ if (node.children.length === 0) {
838
+ return `${indentStr}<${tagName} ${props.join(' ')} />`;
839
+ }
840
+ const childrenJSX = node.children
841
+ .map((child) => generateJSX(child, childIndent, pageKey, child.component.id, nameMap))
842
+ .join('\n');
843
+ return `${indentStr}<${tagName} ${props.join(' ')}>\n${childrenJSX}\n${indentStr}</${tagName}>`;
844
+ }
845
+ async function generateStylesFile(styles, embeddableId) {
846
+ try {
847
+ // Convert JSON back to CSS
848
+ // CSSJSON format: { children: { selector: { attributes: {...} } } }
849
+ const cssJson = {
850
+ children: {},
851
+ };
852
+ for (const [selector, attributes] of Object.entries(styles)) {
853
+ cssJson.children[selector] = {
854
+ attributes: attributes,
855
+ };
856
+ }
857
+ const css = CSSJSON.toCSS(cssJson);
858
+ // Write to embeddables/<id>/styles/index.css
859
+ const filePath = path.join('embeddables', embeddableId, 'styles', 'index.css');
860
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
861
+ fs.writeFileSync(filePath, css, 'utf8');
862
+ console.log(`${pc.gray(`Generated ${filePath}`)}`);
863
+ }
864
+ catch (error) {
865
+ if (error instanceof Error) {
866
+ throw new Error(`Styles: ${error.message}`);
867
+ }
868
+ throw error;
869
+ }
870
+ }
871
+ // Helper functions
872
+ function capitalizeFirst(str) {
873
+ return str.charAt(0).toUpperCase() + str.slice(1);
874
+ }
875
+ function camelCase(str) {
876
+ return str
877
+ .replace(/[^a-zA-Z0-9]/g, ' ')
878
+ .split(' ')
879
+ .map((word, index) => {
880
+ if (index === 0) {
881
+ return word.toLowerCase();
882
+ }
883
+ return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
884
+ })
885
+ .join('')
886
+ .replace(/[^a-zA-Z0-9]/g, '');
887
+ }
888
+ function escapeString(str) {
889
+ // Escape backslashes first, then quotes, then all newline variants
890
+ return str
891
+ .replace(/\\/g, '\\\\')
892
+ .replace(/"/g, '\\"')
893
+ .replace(/\r\n/g, '\\n') // Windows line endings
894
+ .replace(/\r/g, '\\n') // Old Mac line endings
895
+ .replace(/\n/g, '\\n'); // Unix line endings
896
+ }
897
+ function escapeStringForJSX(str) {
898
+ // Check if string contains double quotes - if so, use template literals
899
+ const hasDoubleQuotes = str.includes('"');
900
+ if (hasDoubleQuotes) {
901
+ // Use template literals (backticks) - escape backticks, backslashes, and ${
902
+ let escaped = str
903
+ .replace(/\\/g, '\\\\') // Escape backslashes first
904
+ .replace(/`/g, '\\`') // Escape backticks
905
+ .replace(/\$\{/g, '\\${'); // Escape template literal expressions
906
+ // Escape newlines
907
+ escaped = escaped
908
+ .replace(/\r\n/g, '\\n') // Windows line endings
909
+ .replace(/\r/g, '\\n') // Old Mac line endings
910
+ .replace(/\n/g, '\\n'); // Unix line endings
911
+ return `{\`${escaped}\`}`;
912
+ }
913
+ // No double quotes - use regular quote selection
914
+ const doubleQuoteCount = (str.match(/"/g) || []).length;
915
+ const singleQuoteCount = (str.match(/'/g) || []).length;
916
+ // Escape backslashes first
917
+ let escaped = str.replace(/\\/g, '\\\\');
918
+ // Escape newlines
919
+ escaped = escaped
920
+ .replace(/\r\n/g, '\\n') // Windows line endings
921
+ .replace(/\r/g, '\\n') // Old Mac line endings
922
+ .replace(/\n/g, '\\n'); // Unix line endings
923
+ // Use single quotes if there are more double quotes, otherwise use double quotes
924
+ if (doubleQuoteCount > singleQuoteCount) {
925
+ // Use single quotes and escape single quotes
926
+ escaped = escaped.replace(/'/g, "\\'");
927
+ return `'${escaped}'`;
928
+ }
929
+ else {
930
+ // Use double quotes and escape double quotes
931
+ escaped = escaped.replace(/"/g, '\\"');
932
+ return `"${escaped}"`;
933
+ }
934
+ }
935
+ /**
936
+ * Converts HTML string to JSX children code.
937
+ * This is a simplified parser that handles common HTML cases.
938
+ */
939
+ function htmlToJSXChildren(html, indent) {
940
+ const indentStr = ' '.repeat(indent);
941
+ const result = [];
942
+ // Simple HTML parser using regex (handles common cases)
943
+ // This is a basic implementation - for production, consider using a proper HTML parser
944
+ let pos = 0;
945
+ const len = html.length;
946
+ while (pos < len) {
947
+ // Skip whitespace at the start
948
+ const whitespaceMatch = html.slice(pos).match(/^\s+/);
949
+ if (whitespaceMatch) {
950
+ pos += whitespaceMatch[0].length;
951
+ continue;
952
+ }
953
+ // Check for HTML comment and skip it
954
+ if (html.slice(pos).startsWith('<!--')) {
955
+ const commentEnd = html.indexOf('-->', pos);
956
+ if (commentEnd !== -1) {
957
+ pos = commentEnd + 3; // Skip past -->
958
+ continue;
959
+ }
960
+ else {
961
+ // Malformed comment - skip to end
962
+ break;
963
+ }
964
+ }
965
+ // Check for HTML tag
966
+ const tagMatch = html.slice(pos).match(/^<(\/?)([a-zA-Z][a-zA-Z0-9]*)\s*([^>]*)>/);
967
+ if (tagMatch) {
968
+ const isClosing = tagMatch[1] === '/';
969
+ const tagName = tagMatch[2];
970
+ const htmlAttrsStr = tagMatch[3];
971
+ const fullMatch = tagMatch[0];
972
+ if (isClosing) {
973
+ // Closing tag - we'll handle this in the opening tag logic
974
+ pos += fullMatch.length;
975
+ continue;
976
+ }
977
+ // Check if it's a self-closing tag
978
+ const isSelfClosing = htmlAttrsStr.endsWith('/') ||
979
+ [
980
+ 'img',
981
+ 'br',
982
+ 'hr',
983
+ 'input',
984
+ 'meta',
985
+ 'link',
986
+ 'area',
987
+ 'base',
988
+ 'col',
989
+ 'embed',
990
+ 'source',
991
+ 'track',
992
+ 'wbr',
993
+ ].includes(tagName.toLowerCase());
994
+ // Parse attributes
995
+ const attrs = [];
996
+ // First, find all attribute names (with or without values)
997
+ // Match: attr, attr=value, attr="value", attr='value'
998
+ // We need to handle attributes without values (like <img alt />) and skip them
999
+ const attrRegex = /(\w+)(?:="([^"]*)"|='([^']*)'|=(.+?)(?=[\s>]|$))?/g;
1000
+ let attrMatch;
1001
+ const seenAttrs = new Set();
1002
+ while ((attrMatch = attrRegex.exec(htmlAttrsStr)) !== null) {
1003
+ const attrName = attrMatch[1];
1004
+ const attrValue = attrMatch[2] || attrMatch[3] || attrMatch[4] || '';
1005
+ // Skip if we've already seen this attribute
1006
+ if (seenAttrs.has(attrName))
1007
+ continue;
1008
+ seenAttrs.add(attrName);
1009
+ // Skip attributes without values (like <img alt />) - these should be removed
1010
+ if (!attrMatch[0].includes('=')) {
1011
+ continue;
1012
+ }
1013
+ // Convert HTML attribute names to JSX (e.g., class -> className, for -> htmlFor)
1014
+ let jsxAttrName = attrName;
1015
+ if (attrName === 'class')
1016
+ jsxAttrName = 'className';
1017
+ else if (attrName === 'for')
1018
+ jsxAttrName = 'htmlFor';
1019
+ else if (attrName === 'tabindex')
1020
+ jsxAttrName = 'tabIndex';
1021
+ // Handle style attribute: convert CSS string to JSX object
1022
+ if (attrName === 'style' && attrValue) {
1023
+ const styleObj = cssStringToStyleObject(attrValue);
1024
+ const styleStr = styleObjectToJSX(styleObj);
1025
+ attrs.push(`style={${styleStr}}`);
1026
+ continue;
1027
+ }
1028
+ // Skip attributes with empty values (like alt="")
1029
+ if (attrValue === '') {
1030
+ continue;
1031
+ }
1032
+ // Escape the attribute value for JSX
1033
+ // JSX attribute strings don't support \" escaping like JS strings
1034
+ // So if the value contains double quotes, use single quotes for the outer delimiter
1035
+ const hasDoubleQuotes = attrValue.includes('"');
1036
+ const hasSingleQuotes = attrValue.includes("'");
1037
+ if (hasDoubleQuotes && !hasSingleQuotes) {
1038
+ // Use single quotes - only need to escape backslashes and newlines
1039
+ const escapedValue = attrValue
1040
+ .replace(/\\/g, '\\\\')
1041
+ .replace(/\n/g, '\\n')
1042
+ .replace(/\r/g, '\\r');
1043
+ attrs.push(`${jsxAttrName}='${escapedValue}'`);
1044
+ }
1045
+ else if (hasDoubleQuotes && hasSingleQuotes) {
1046
+ // Value has both quote types - use JSX expression with template literal
1047
+ const escapedValue = attrValue
1048
+ .replace(/\\/g, '\\\\')
1049
+ .replace(/`/g, '\\`')
1050
+ .replace(/\$\{/g, '\\${') // Escape template literal expressions
1051
+ .replace(/\n/g, '\\n')
1052
+ .replace(/\r/g, '\\r');
1053
+ attrs.push(`${jsxAttrName}={\`${escapedValue}\`}`);
1054
+ }
1055
+ else {
1056
+ // No double quotes - use double quotes as before
1057
+ const escapedValue = attrValue
1058
+ .replace(/\\/g, '\\\\')
1059
+ .replace(/\n/g, '\\n')
1060
+ .replace(/\r/g, '\\r');
1061
+ attrs.push(`${jsxAttrName}="${escapedValue}"`);
1062
+ }
1063
+ }
1064
+ pos += fullMatch.length;
1065
+ // Special handling for <style> and <script> tags: preserve content as raw text
1066
+ const isRawTextTag = tagName.toLowerCase() === 'style' || tagName.toLowerCase() === 'script';
1067
+ // Find the content and closing tag for non-self-closing tags
1068
+ if (!isSelfClosing) {
1069
+ // Find matching closing tag
1070
+ const closingTag = `</${tagName}>`;
1071
+ let depth = 1;
1072
+ let searchPos = pos;
1073
+ let contentEnd = -1;
1074
+ while (depth > 0 && searchPos < len) {
1075
+ const nextOpen = html.indexOf(`<${tagName}`, searchPos);
1076
+ const nextClose = html.indexOf(closingTag, searchPos);
1077
+ if (nextClose === -1) {
1078
+ // No closing tag found - treat as self-closing
1079
+ break;
1080
+ }
1081
+ if (nextOpen !== -1 && nextOpen < nextClose) {
1082
+ // Found nested opening tag
1083
+ depth++;
1084
+ searchPos = nextOpen + 1;
1085
+ }
1086
+ else {
1087
+ // Found closing tag
1088
+ depth--;
1089
+ if (depth === 0) {
1090
+ contentEnd = nextClose;
1091
+ }
1092
+ else {
1093
+ searchPos = nextClose + closingTag.length;
1094
+ }
1095
+ }
1096
+ }
1097
+ if (contentEnd !== -1) {
1098
+ const content = html.slice(pos, contentEnd);
1099
+ const attrsStr = attrs.length > 0 ? ' ' + attrs.join(' ') : '';
1100
+ if (isRawTextTag) {
1101
+ // For <style> and <script>, preserve content as raw text without processing
1102
+ // In JSX, this content must be wrapped in a template literal string expression
1103
+ const rawContent = content;
1104
+ if (rawContent.trim()) {
1105
+ // Escape backticks and ${ in the content to prevent template literal issues
1106
+ const escapedContent = rawContent
1107
+ .replace(/\\/g, '\\\\') // Escape backslashes first
1108
+ .replace(/`/g, '\\`') // Escape backticks
1109
+ .replace(/\${/g, '\\${'); // Escape ${ to prevent interpolation
1110
+ // Output as a template literal wrapped in curly braces for JSX
1111
+ result.push(`${indentStr}<${tagName}${attrsStr}>{`);
1112
+ result.push(`${indentStr} \`${escapedContent}\``);
1113
+ result.push(`${indentStr}}</${tagName}>`);
1114
+ }
1115
+ else {
1116
+ result.push(`${indentStr}<${tagName}${attrsStr} />`);
1117
+ }
1118
+ }
1119
+ else {
1120
+ // For other tags, process children recursively
1121
+ const childrenJSX = htmlToJSXChildren(content, indent + 2);
1122
+ if (childrenJSX.trim()) {
1123
+ result.push(`${indentStr}<${tagName}${attrsStr}>`);
1124
+ result.push(childrenJSX);
1125
+ result.push(`${indentStr}</${tagName}>`);
1126
+ }
1127
+ else {
1128
+ result.push(`${indentStr}<${tagName}${attrsStr} />`);
1129
+ }
1130
+ }
1131
+ pos = contentEnd + closingTag.length;
1132
+ continue;
1133
+ }
1134
+ }
1135
+ // Self-closing tag or no closing tag found
1136
+ const attrsStr = attrs.length > 0 ? ' ' + attrs.join(' ') : '';
1137
+ result.push(`${indentStr}<${tagName}${attrsStr} />`);
1138
+ continue;
1139
+ }
1140
+ // Text content
1141
+ const textEnd = html.indexOf('<', pos);
1142
+ if (textEnd === -1) {
1143
+ // Rest is text
1144
+ const text = html.slice(pos).trim();
1145
+ if (text) {
1146
+ // Unescape HTML entities
1147
+ const unescapedText = text
1148
+ .replace(/&amp;/g, '&')
1149
+ .replace(/&lt;/g, '<')
1150
+ .replace(/&gt;/g, '>')
1151
+ .replace(/&quot;/g, '"')
1152
+ .replace(/&#39;/g, "'")
1153
+ .replace(/&nbsp;/g, ' ');
1154
+ // Check if text contains template variables
1155
+ if (unescapedText.includes('{{')) {
1156
+ // Convert to template literal expression
1157
+ const templateLiteral = stringToTemplateLiteral(unescapedText);
1158
+ result.push(`${indentStr}{${templateLiteral}}`);
1159
+ }
1160
+ else {
1161
+ // Escape special characters for JSX text content
1162
+ const escapedText = escapeTextForJSX(unescapedText);
1163
+ result.push(`${indentStr}${escapedText}`);
1164
+ }
1165
+ }
1166
+ break;
1167
+ }
1168
+ else {
1169
+ const text = html.slice(pos, textEnd).trim();
1170
+ if (text) {
1171
+ // Unescape HTML entities
1172
+ const unescapedText = text
1173
+ .replace(/&amp;/g, '&')
1174
+ .replace(/&lt;/g, '<')
1175
+ .replace(/&gt;/g, '>')
1176
+ .replace(/&quot;/g, '"')
1177
+ .replace(/&#39;/g, "'")
1178
+ .replace(/&nbsp;/g, ' ');
1179
+ // Check if text contains template variables
1180
+ if (unescapedText.includes('{{')) {
1181
+ // Convert to template literal expression
1182
+ const templateLiteral = stringToTemplateLiteral(unescapedText);
1183
+ result.push(`${indentStr}{${templateLiteral}}`);
1184
+ }
1185
+ else {
1186
+ // Escape special characters for JSX text content
1187
+ const escapedText = escapeTextForJSX(unescapedText);
1188
+ result.push(`${indentStr}${escapedText}`);
1189
+ }
1190
+ }
1191
+ pos = textEnd;
1192
+ }
1193
+ }
1194
+ return result.join('\n');
1195
+ }
1196
+ /**
1197
+ * Converts a CSS string to a style object.
1198
+ * Example: "margin: 0; padding: 0; color: #171717;" -> { margin: 0, padding: 0, color: "#171717" }
1199
+ */
1200
+ function cssStringToStyleObject(cssString) {
1201
+ const styleObj = {};
1202
+ // Normalize the CSS string: remove newlines, extra spaces, and trim
1203
+ const normalized = cssString
1204
+ .replace(/\r\n/g, ' ')
1205
+ .replace(/\r/g, ' ')
1206
+ .replace(/\n/g, ' ')
1207
+ .replace(/\s+/g, ' ')
1208
+ .trim();
1209
+ // Split by semicolon and process each declaration
1210
+ const declarations = normalized.split(';').filter((decl) => decl.trim().length > 0);
1211
+ for (const decl of declarations) {
1212
+ const colonIndex = decl.indexOf(':');
1213
+ if (colonIndex === -1)
1214
+ continue;
1215
+ const key = decl.slice(0, colonIndex).trim();
1216
+ const value = decl.slice(colonIndex + 1).trim();
1217
+ if (key && value) {
1218
+ // Convert kebab-case to camelCase (e.g., line-height -> lineHeight)
1219
+ const camelKey = key.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
1220
+ styleObj[camelKey] = value;
1221
+ }
1222
+ }
1223
+ return styleObj;
1224
+ }
1225
+ /**
1226
+ * Converts a style object to JSX style expression string.
1227
+ * Example: { margin: 0, padding: 0, color: "#171717" } -> "{ margin: 0, padding: 0, color: \"#171717\" }"
1228
+ */
1229
+ function styleObjectToJSX(styleObj) {
1230
+ const entries = [];
1231
+ for (const [key, value] of Object.entries(styleObj)) {
1232
+ // Convert camelCase to camelCase (already in camelCase from cssStringToStyleObject)
1233
+ // Value might be a number string or a string that needs quotes
1234
+ let jsxValue;
1235
+ if (/^-?\d+(\.\d+)?$/.test(value.trim())) {
1236
+ // It's a number (including decimals)
1237
+ jsxValue = value.trim();
1238
+ }
1239
+ else {
1240
+ // It's a string, wrap in quotes and escape
1241
+ const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
1242
+ jsxValue = `"${escaped}"`;
1243
+ }
1244
+ entries.push(`${key}: ${jsxValue}`);
1245
+ }
1246
+ return `{ ${entries.join(', ')} }`;
1247
+ }
1248
+ /**
1249
+ * Escapes HTML/JSX special characters in text content for JSX.
1250
+ * This prevents characters like <, >, and & from being interpreted as JSX tags or entities.
1251
+ * Example: "< 18.5" -> "&lt; 18.5"
1252
+ */
1253
+ function escapeTextForJSX(text) {
1254
+ // Escape special characters that would break JSX/HTML
1255
+ // Order matters: escape & first to avoid double-escaping
1256
+ return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1257
+ }
1258
+ /**
1259
+ * Converts a value to a JSX expression string.
1260
+ * Handles arrays, objects, primitives, etc.
1261
+ */
1262
+ function valueToJSXExpression(value) {
1263
+ if (value === null || value === undefined) {
1264
+ return 'null';
1265
+ }
1266
+ if (typeof value === 'string') {
1267
+ return escapeStringForJS(value);
1268
+ }
1269
+ if (typeof value === 'number' || typeof value === 'boolean') {
1270
+ return String(value);
1271
+ }
1272
+ if (Array.isArray(value)) {
1273
+ if (value.length === 0) {
1274
+ return '[]';
1275
+ }
1276
+ const items = value.map((item) => valueToJSXExpression(item)).join(', ');
1277
+ return `[${items}]`;
1278
+ }
1279
+ if (typeof value === 'object') {
1280
+ const entries = [];
1281
+ for (const [key, val] of Object.entries(value)) {
1282
+ // Skip null/undefined values
1283
+ if (val === null || val === undefined)
1284
+ continue;
1285
+ // Convert key to valid JS identifier or string literal
1286
+ const jsKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : escapeStringForJS(key);
1287
+ entries.push(`${jsKey}: ${valueToJSXExpression(val)}`);
1288
+ }
1289
+ return `{ ${entries.join(', ')} }`;
1290
+ }
1291
+ // Fallback: convert to string
1292
+ return escapeStringForJS(String(value));
1293
+ }
1294
+ /**
1295
+ * Converts a string with {{variable}} syntax to a template literal expression.
1296
+ * Example: "{{first_name}}'s plan" -> "`{{first_name}}'s plan`"
1297
+ */
1298
+ function stringToTemplateLiteral(str) {
1299
+ // Escape backticks and backslashes for template literal
1300
+ const escaped = str.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\${/g, '\\${'); // Escape ${ to prevent interpolation
1301
+ return '`' + escaped + '`';
1302
+ }
1303
+ function escapeStringForJS(str) {
1304
+ // Check if string contains double quotes - if so, use template literals
1305
+ const hasDoubleQuotes = str.includes('"');
1306
+ if (hasDoubleQuotes) {
1307
+ // Use template literals (backticks) - escape backticks and backslashes
1308
+ let escaped = str
1309
+ .replace(/\\/g, '\\\\') // Escape backslashes first
1310
+ .replace(/`/g, '\\`'); // Escape backticks
1311
+ // Escape newlines
1312
+ escaped = escaped
1313
+ .replace(/\r\n/g, '\\n') // Windows line endings
1314
+ .replace(/\r/g, '\\n') // Old Mac line endings
1315
+ .replace(/\n/g, '\\n'); // Unix line endings
1316
+ return `\`${escaped}\``;
1317
+ }
1318
+ // No double quotes - use regular quote selection
1319
+ const doubleQuoteCount = (str.match(/"/g) || []).length;
1320
+ const singleQuoteCount = (str.match(/'/g) || []).length;
1321
+ // Escape backslashes first
1322
+ let escaped = str.replace(/\\/g, '\\\\');
1323
+ // Escape newlines
1324
+ escaped = escaped
1325
+ .replace(/\r\n/g, '\\n') // Windows line endings
1326
+ .replace(/\r/g, '\\n') // Old Mac line endings
1327
+ .replace(/\n/g, '\\n'); // Unix line endings
1328
+ // Use single quotes if there are more double quotes, otherwise use double quotes
1329
+ if (doubleQuoteCount > singleQuoteCount) {
1330
+ // Use single quotes and escape single quotes
1331
+ escaped = escaped.replace(/'/g, "\\'");
1332
+ return `'${escaped}'`;
1333
+ }
1334
+ else {
1335
+ // Use double quotes and escape double quotes
1336
+ escaped = escaped.replace(/"/g, '\\"');
1337
+ return `"${escaped}"`;
1338
+ }
1339
+ }
1340
+ /**
1341
+ * Generates config.json file from embeddable metadata.
1342
+ * This file controls page ordering and stores embeddable-level metadata.
1343
+ * Preserves the order of top-level properties from embeddable.json.
1344
+ */
1345
+ async function generateConfigFile(embeddable, embeddableId) {
1346
+ try {
1347
+ const configPath = path.join('embeddables', embeddableId, 'config.json');
1348
+ // Preserve the order of top-level properties from embeddable
1349
+ const embeddableKeys = Object.keys(embeddable);
1350
+ // Extract page metadata and order (excluding components which are in TSX files)
1351
+ const pages = embeddable.pages.map((page) => {
1352
+ const pageMetadata = {
1353
+ key: page.key,
1354
+ };
1355
+ // Include page-level metadata (excluding components which are in TSX files)
1356
+ // Preserve property order from the page object recursively
1357
+ const pageKeys = Object.keys(page);
1358
+ const excludePageProps = new Set(['components']);
1359
+ for (const key of pageKeys) {
1360
+ if (!excludePageProps.has(key) && key !== 'key') {
1361
+ const value = page[key];
1362
+ if (value !== undefined && value !== null) {
1363
+ pageMetadata[key] = deepClonePreservingOrder(value);
1364
+ }
1365
+ }
1366
+ }
1367
+ return pageMetadata;
1368
+ });
1369
+ // Extract computedFields metadata (without code, as code is in JS files)
1370
+ // Preserve order recursively in metadata objects
1371
+ const computedFieldsMetadata = [];
1372
+ if (embeddable.computedFields && Array.isArray(embeddable.computedFields)) {
1373
+ for (const field of embeddable.computedFields) {
1374
+ const cloned = deepClonePreservingOrder(field);
1375
+ const { code, ...metadata } = cloned;
1376
+ computedFieldsMetadata.push(metadata);
1377
+ }
1378
+ }
1379
+ // Extract dataOutputs metadata (without code, as code is in JS files)
1380
+ // Preserve order recursively in metadata objects
1381
+ const dataOutputsMetadata = [];
1382
+ if (embeddable.dataOutputs && Array.isArray(embeddable.dataOutputs)) {
1383
+ for (const action of embeddable.dataOutputs) {
1384
+ const cloned = deepClonePreservingOrder(action);
1385
+ const { code, ...metadata } = cloned;
1386
+ dataOutputsMetadata.push(metadata);
1387
+ }
1388
+ }
1389
+ // Build config object preserving the order from embeddable
1390
+ const config = {};
1391
+ const excludeProps = new Set(['pages', 'styles', 'computedFields', 'dataOutputs', 'components']);
1392
+ // Iterate through embeddable keys in order to preserve property order
1393
+ for (const key of embeddableKeys) {
1394
+ if (key === 'pages') {
1395
+ // Replace pages with page metadata (without components)
1396
+ config.pages = pages;
1397
+ }
1398
+ else if (key === 'styles') {
1399
+ // Skip styles (they're in CSS files)
1400
+ continue;
1401
+ }
1402
+ else if (key === 'computedFields') {
1403
+ // Replace computedFields with metadata (without code)
1404
+ if (computedFieldsMetadata.length > 0) {
1405
+ config.computedFields = computedFieldsMetadata;
1406
+ }
1407
+ }
1408
+ else if (key === 'dataOutputs') {
1409
+ // Replace dataOutputs with metadata (without code)
1410
+ if (dataOutputsMetadata.length > 0) {
1411
+ config.dataOutputs = dataOutputsMetadata;
1412
+ }
1413
+ }
1414
+ else if (key === 'components') {
1415
+ // Skip components (they're in TSX files in global-components/)
1416
+ continue;
1417
+ }
1418
+ else {
1419
+ // Regular metadata property - preserve as-is, with order preserved recursively
1420
+ const value = embeddable[key];
1421
+ if (value !== undefined && value !== null) {
1422
+ config[key] = deepClonePreservingOrder(value);
1423
+ }
1424
+ }
1425
+ }
1426
+ // Ensure id is set if it wasn't already
1427
+ if (!config.id) {
1428
+ config.id = embeddableId;
1429
+ }
1430
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
1431
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
1432
+ console.log(`${pc.gray(`Generated ${configPath}`)}`);
1433
+ }
1434
+ catch (error) {
1435
+ if (error instanceof Error) {
1436
+ throw new Error(`Config: ${error.message}`);
1437
+ }
1438
+ throw error;
1439
+ }
1440
+ }
1441
+ /**
1442
+ * Extracts computedFields to JS files in computed-fields/ folder.
1443
+ * Each computedField with code is saved as a separate JS file.
1444
+ */
1445
+ async function extractComputedFields(computedFields, embeddableId) {
1446
+ const computedFieldsDir = path.join('embeddables', embeddableId, 'computed-fields');
1447
+ fs.mkdirSync(computedFieldsDir, { recursive: true });
1448
+ for (const field of computedFields) {
1449
+ if (!field.code) {
1450
+ continue;
1451
+ }
1452
+ // Try using key or id as identifier
1453
+ const identifier = field.key || field.id;
1454
+ if (!identifier) {
1455
+ continue;
1456
+ }
1457
+ const fileName = `${sanitizeFileName(identifier)}.js`;
1458
+ const filePath = path.join(computedFieldsDir, fileName);
1459
+ // Write the code as-is (it should be valid JavaScript)
1460
+ fs.writeFileSync(filePath, field.code, 'utf8');
1461
+ console.log(`${pc.gray(`Generated ${filePath}`)}`);
1462
+ }
1463
+ }
1464
+ /**
1465
+ * Extracts dataOutputs (actions) to JS files in actions/ folder.
1466
+ * Each action with code is saved as a separate JS file.
1467
+ */
1468
+ async function extractDataOutputs(dataOutputs, embeddableId) {
1469
+ const actionsDir = path.join('embeddables', embeddableId, 'actions');
1470
+ fs.mkdirSync(actionsDir, { recursive: true });
1471
+ for (const action of dataOutputs) {
1472
+ if (!action.code) {
1473
+ continue;
1474
+ }
1475
+ // Use name as the filename, fallback to id if name doesn't exist
1476
+ const identifier = action.name || action.id;
1477
+ if (!identifier) {
1478
+ continue;
1479
+ }
1480
+ const fileName = `${sanitizeFileName(identifier)}.js`;
1481
+ const filePath = path.join(actionsDir, fileName);
1482
+ // Write the code as-is (it should be valid JavaScript)
1483
+ fs.writeFileSync(filePath, action.code, 'utf8');
1484
+ console.log(`${pc.gray(`Generated ${filePath}`)}`);
1485
+ }
1486
+ }
1487
+ /**
1488
+ * Helper function to recursively find the _location of a component.
1489
+ * If the component has a parent_id, it checks the parent's location recursively.
1490
+ * Returns undefined if no location is found in the component or any parent.
1491
+ */
1492
+ function findComponentLocation(component, componentMap) {
1493
+ if (component._location) {
1494
+ return component._location;
1495
+ }
1496
+ if (component.parent_id) {
1497
+ const parent = componentMap.get(component.parent_id);
1498
+ if (parent) {
1499
+ return findComponentLocation(parent, componentMap);
1500
+ }
1501
+ }
1502
+ return undefined;
1503
+ }
1504
+ /**
1505
+ * Extracts global components to TSX files in global-components/ folder.
1506
+ * Components are grouped by their _location (for root components without parent_id)
1507
+ * or by their parent's location (for child components).
1508
+ * File naming: <location>.location.tsx (e.g., before_page.location.tsx)
1509
+ */
1510
+ async function extractGlobalComponents(components, embeddableId, fix) {
1511
+ const globalComponentsDir = path.join('embeddables', embeddableId, 'global-components');
1512
+ fs.mkdirSync(globalComponentsDir, { recursive: true });
1513
+ if (components.length === 0) {
1514
+ return;
1515
+ }
1516
+ // Create a map of id -> component for location resolution
1517
+ const componentMap = new Map();
1518
+ for (const comp of components) {
1519
+ componentMap.set(comp.id, comp);
1520
+ }
1521
+ // Group components by location
1522
+ // Components without parent_id must have _location and go in their location file
1523
+ // Components with parent_id inherit location from parent and go in parent's location file
1524
+ const componentsByLocation = new Map();
1525
+ for (const component of components) {
1526
+ const location = findComponentLocation(component, componentMap);
1527
+ // Components without parent_id must have _location
1528
+ if (!component.parent_id && !location) {
1529
+ if (fix) {
1530
+ console.warn(`Removed global component "${component.id}" (key: "${component.key}") – missing _location (no parent_id).`);
1531
+ continue;
1532
+ }
1533
+ throw new Error(`Global component "${component.id}" (key: "${component.key}") must have _location since it has no parent_id.`);
1534
+ }
1535
+ if (location) {
1536
+ if (!componentsByLocation.has(location)) {
1537
+ componentsByLocation.set(location, []);
1538
+ }
1539
+ componentsByLocation.get(location).push(component);
1540
+ }
1541
+ }
1542
+ // Generate TSX file for each location
1543
+ for (const [location, locationComponents] of componentsByLocation) {
1544
+ const fileName = `${location}.location.tsx`;
1545
+ const filePath = path.join(globalComponentsDir, fileName);
1546
+ // Build tree from flat component list (similar to buildTree for pages)
1547
+ const tree = buildTreeForGlobalComponents(locationComponents, fix);
1548
+ // Generate TSX code
1549
+ const tsx = generateGlobalComponentTSX(locationComponents, tree);
1550
+ // Write to file
1551
+ fs.writeFileSync(filePath, tsx, 'utf8');
1552
+ console.log(`${pc.gray(`Generated ${filePath}`)}`);
1553
+ }
1554
+ }
1555
+ /**
1556
+ * Builds a component tree from a flat list of components (similar to buildTree for pages).
1557
+ */
1558
+ function buildTreeForGlobalComponents(components, fix) {
1559
+ // Filter out components without type and ignored component types
1560
+ let filteredComponents = components.filter((comp) => {
1561
+ if (!comp.type) {
1562
+ if (fix) {
1563
+ console.warn(`Removed global component (id: ${comp.id}, key: ${comp.key}) – missing type property.`);
1564
+ return false;
1565
+ }
1566
+ throw new Error(`Global component (id: ${comp.id}, key: ${comp.key}) is missing required type property.`);
1567
+ }
1568
+ return !IGNORED_COMPONENT_TYPES.has(comp.type);
1569
+ });
1570
+ // Filter out components missing required props
1571
+ filteredComponents = filteredComponents.filter((comp) => {
1572
+ return hasRequiredProps(comp, fix);
1573
+ });
1574
+ // Create a map of id -> component
1575
+ const componentMap = new Map();
1576
+ for (const comp of filteredComponents) {
1577
+ componentMap.set(comp.id, comp);
1578
+ }
1579
+ // Find root components (no parent_id)
1580
+ const roots = [];
1581
+ for (const comp of filteredComponents) {
1582
+ if (!comp.parent_id) {
1583
+ roots.push(comp);
1584
+ }
1585
+ }
1586
+ // Allow empty locations - return null if no components
1587
+ if (roots.length === 0) {
1588
+ return null;
1589
+ }
1590
+ // Build tree recursively for each root
1591
+ const rootNodes = roots.map((root) => buildNode(root, componentMap, '', fix));
1592
+ // Return single node or array of nodes
1593
+ return rootNodes.length === 1 ? rootNodes[0] : rootNodes;
1594
+ }
1595
+ /**
1596
+ * Generates TSX code for global components (similar to generateTSX for pages).
1597
+ */
1598
+ function generateGlobalComponentTSX(components, tree) {
1599
+ const imports = tree ? collectImports(tree) : [];
1600
+ const hasButtons = tree ? hasButtonConstants(tree) : false;
1601
+ const { constants, nameMap } = tree
1602
+ ? generateButtonConstants(tree)
1603
+ : { constants: '', nameMap: new Map() };
1604
+ const jsx = generateJSXFromTree(tree, 2, undefined, nameMap);
1605
+ return `"use client";
1606
+
1607
+ import React from "react";
1608
+ ${imports
1609
+ .map((imp) => `import { ${imp} } from "../../../src/components/primitives/${imp}";`)
1610
+ .join('\n')}
1611
+ ${hasButtons
1612
+ ? `
1613
+ import type { OptionSelectorButton } from "../../../src/types-builder";`
1614
+ : ''}
1615
+
1616
+ export default function GlobalComponents() {
1617
+ ${constants}
1618
+ return (
1619
+ ${jsx}
1620
+ );
1621
+ }
1622
+ `;
1623
+ }
1624
+ /**
1625
+ * Sanitizes a string to be safe for use as a filename.
1626
+ */
1627
+ export function sanitizeFileName(str) {
1628
+ return str
1629
+ .replace(/[^a-zA-Z0-9_-]/g, '_')
1630
+ .replace(/^_+|_+$/g, '')
1631
+ .replace(/_{2,}/g, '_');
1632
+ }