@embeddables/cli 0.8.0 → 0.8.2

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 (235) hide show
  1. package/dist/auth/index.d.ts +43 -0
  2. package/dist/auth/index.d.ts.map +1 -0
  3. package/dist/auth/index.js +102 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +210 -0
  7. package/dist/commands/branch.d.ts +4 -0
  8. package/dist/commands/branch.d.ts.map +1 -0
  9. package/dist/commands/branch.js +67 -0
  10. package/dist/commands/build-workbench.d.ts +5 -0
  11. package/dist/commands/build-workbench.d.ts.map +1 -0
  12. package/dist/commands/build-workbench.js +116 -0
  13. package/dist/commands/build.d.ts +8 -0
  14. package/dist/commands/build.d.ts.map +1 -0
  15. package/dist/commands/build.js +60 -0
  16. package/dist/commands/builder-open.d.ts +4 -0
  17. package/dist/commands/builder-open.d.ts.map +1 -0
  18. package/dist/commands/builder-open.js +74 -0
  19. package/dist/commands/dev.d.ts +12 -0
  20. package/dist/commands/dev.d.ts.map +1 -0
  21. package/dist/commands/dev.js +226 -0
  22. package/dist/commands/experiments-connect.d.ts +6 -0
  23. package/dist/commands/experiments-connect.d.ts.map +1 -0
  24. package/dist/commands/experiments-connect.js +140 -0
  25. package/dist/commands/init.d.ts +5 -0
  26. package/dist/commands/init.d.ts.map +1 -0
  27. package/dist/commands/init.js +384 -0
  28. package/dist/commands/inspect.d.ts +9 -0
  29. package/dist/commands/inspect.d.ts.map +1 -0
  30. package/dist/commands/inspect.js +293 -0
  31. package/dist/commands/login.d.ts +2 -0
  32. package/dist/commands/login.d.ts.map +1 -0
  33. package/dist/commands/login.js +117 -0
  34. package/dist/commands/logout.d.ts +2 -0
  35. package/dist/commands/logout.d.ts.map +1 -0
  36. package/dist/commands/logout.js +19 -0
  37. package/dist/commands/pull.d.ts +16 -0
  38. package/dist/commands/pull.d.ts.map +1 -0
  39. package/dist/commands/pull.js +395 -0
  40. package/dist/commands/save.d.ts +30 -0
  41. package/dist/commands/save.d.ts.map +1 -0
  42. package/dist/commands/save.js +597 -0
  43. package/dist/commands/upgrade.d.ts +2 -0
  44. package/dist/commands/upgrade.d.ts.map +1 -0
  45. package/dist/commands/upgrade.js +50 -0
  46. package/dist/compiler/errors.d.ts +20 -0
  47. package/dist/compiler/errors.d.ts.map +1 -0
  48. package/dist/compiler/errors.js +35 -0
  49. package/dist/compiler/evalStatic.d.ts +3 -0
  50. package/dist/compiler/evalStatic.d.ts.map +1 -0
  51. package/dist/compiler/evalStatic.js +57 -0
  52. package/dist/compiler/helpers/duplicateIds.d.ts +9 -0
  53. package/dist/compiler/helpers/duplicateIds.d.ts.map +1 -0
  54. package/dist/compiler/helpers/duplicateIds.js +71 -0
  55. package/dist/compiler/helpers/numericLeadingKeys.d.ts +8 -0
  56. package/dist/compiler/helpers/numericLeadingKeys.d.ts.map +1 -0
  57. package/dist/compiler/helpers/numericLeadingKeys.js +17 -0
  58. package/dist/compiler/index.d.ts +18 -0
  59. package/dist/compiler/index.d.ts.map +1 -0
  60. package/dist/compiler/index.js +1272 -0
  61. package/dist/compiler/parsePage.d.ts +15 -0
  62. package/dist/compiler/parsePage.d.ts.map +1 -0
  63. package/dist/compiler/parsePage.js +654 -0
  64. package/dist/compiler/registry.d.ts +4 -0
  65. package/dist/compiler/registry.d.ts.map +1 -0
  66. package/dist/compiler/registry.js +44 -0
  67. package/dist/compiler/reverse.d.ts +23 -0
  68. package/dist/compiler/reverse.d.ts.map +1 -0
  69. package/dist/compiler/reverse.js +1938 -0
  70. package/dist/compiler/types.d.ts +21 -0
  71. package/dist/compiler/types.d.ts.map +1 -0
  72. package/dist/compiler/types.js +1 -0
  73. package/dist/components/index.d.ts +21 -0
  74. package/dist/components/index.d.ts.map +1 -0
  75. package/dist/components/index.js +21 -0
  76. package/dist/components/primitives/BaseComponent.d.ts +33 -0
  77. package/dist/components/primitives/BaseComponent.d.ts.map +1 -0
  78. package/dist/components/primitives/BaseComponent.js +26 -0
  79. package/dist/components/primitives/BookMeeting.d.ts +18 -0
  80. package/dist/components/primitives/BookMeeting.d.ts.map +1 -0
  81. package/dist/components/primitives/BookMeeting.js +5 -0
  82. package/dist/components/primitives/Chart.d.ts +41 -0
  83. package/dist/components/primitives/Chart.d.ts.map +1 -0
  84. package/dist/components/primitives/Chart.js +5 -0
  85. package/dist/components/primitives/Container.d.ts +8 -0
  86. package/dist/components/primitives/Container.d.ts.map +1 -0
  87. package/dist/components/primitives/Container.js +5 -0
  88. package/dist/components/primitives/CustomButton.d.ts +37 -0
  89. package/dist/components/primitives/CustomButton.d.ts.map +1 -0
  90. package/dist/components/primitives/CustomButton.js +10 -0
  91. package/dist/components/primitives/CustomHTML.d.ts +8 -0
  92. package/dist/components/primitives/CustomHTML.d.ts.map +1 -0
  93. package/dist/components/primitives/CustomHTML.js +5 -0
  94. package/dist/components/primitives/FileUpload.d.ts +18 -0
  95. package/dist/components/primitives/FileUpload.d.ts.map +1 -0
  96. package/dist/components/primitives/FileUpload.js +16 -0
  97. package/dist/components/primitives/InputBox.d.ts +34 -0
  98. package/dist/components/primitives/InputBox.d.ts.map +1 -0
  99. package/dist/components/primitives/InputBox.js +25 -0
  100. package/dist/components/primitives/Lottie.d.ts +11 -0
  101. package/dist/components/primitives/Lottie.d.ts.map +1 -0
  102. package/dist/components/primitives/Lottie.js +5 -0
  103. package/dist/components/primitives/MediaEmbed.d.ts +13 -0
  104. package/dist/components/primitives/MediaEmbed.d.ts.map +1 -0
  105. package/dist/components/primitives/MediaEmbed.js +6 -0
  106. package/dist/components/primitives/MediaImage.d.ts +8 -0
  107. package/dist/components/primitives/MediaImage.d.ts.map +1 -0
  108. package/dist/components/primitives/MediaImage.js +5 -0
  109. package/dist/components/primitives/OptionSelector.d.ts +38 -0
  110. package/dist/components/primitives/OptionSelector.d.ts.map +1 -0
  111. package/dist/components/primitives/OptionSelector.js +8 -0
  112. package/dist/components/primitives/PaypalCheckout.d.ts +25 -0
  113. package/dist/components/primitives/PaypalCheckout.d.ts.map +1 -0
  114. package/dist/components/primitives/PaypalCheckout.js +5 -0
  115. package/dist/components/primitives/PlainText.d.ts +6 -0
  116. package/dist/components/primitives/PlainText.d.ts.map +1 -0
  117. package/dist/components/primitives/PlainText.js +5 -0
  118. package/dist/components/primitives/ProgressBar.d.ts +15 -0
  119. package/dist/components/primitives/ProgressBar.d.ts.map +1 -0
  120. package/dist/components/primitives/ProgressBar.js +5 -0
  121. package/dist/components/primitives/RichText.d.ts +6 -0
  122. package/dist/components/primitives/RichText.d.ts.map +1 -0
  123. package/dist/components/primitives/RichText.js +5 -0
  124. package/dist/components/primitives/RichTextMarkdown.d.ts +6 -0
  125. package/dist/components/primitives/RichTextMarkdown.d.ts.map +1 -0
  126. package/dist/components/primitives/RichTextMarkdown.js +5 -0
  127. package/dist/components/primitives/Rive.d.ts +16 -0
  128. package/dist/components/primitives/Rive.d.ts.map +1 -0
  129. package/dist/components/primitives/Rive.js +8 -0
  130. package/dist/components/primitives/StripeCheckout.d.ts +52 -0
  131. package/dist/components/primitives/StripeCheckout.d.ts.map +1 -0
  132. package/dist/components/primitives/StripeCheckout.js +5 -0
  133. package/dist/components/primitives/StripeCheckout2.d.ts +30 -0
  134. package/dist/components/primitives/StripeCheckout2.d.ts.map +1 -0
  135. package/dist/components/primitives/StripeCheckout2.js +7 -0
  136. package/dist/config/index.d.ts +23 -0
  137. package/dist/config/index.d.ts.map +1 -0
  138. package/dist/config/index.js +42 -0
  139. package/dist/constants.d.ts +9 -0
  140. package/dist/constants.d.ts.map +1 -0
  141. package/dist/constants.js +9 -0
  142. package/dist/helpers/dates.d.ts +5 -0
  143. package/dist/helpers/dates.d.ts.map +1 -0
  144. package/dist/helpers/dates.js +7 -0
  145. package/dist/helpers/json.d.ts +47 -0
  146. package/dist/helpers/json.d.ts.map +1 -0
  147. package/dist/helpers/json.js +622 -0
  148. package/dist/helpers/prompt.d.ts +15 -0
  149. package/dist/helpers/prompt.d.ts.map +1 -0
  150. package/dist/helpers/prompt.js +35 -0
  151. package/dist/helpers/utils.d.ts +13 -0
  152. package/dist/helpers/utils.d.ts.map +1 -0
  153. package/dist/helpers/utils.js +28 -0
  154. package/dist/logger.d.ts +11 -0
  155. package/dist/logger.d.ts.map +1 -0
  156. package/dist/logger.js +21 -0
  157. package/dist/patches/prompts-escape.d.ts +14 -0
  158. package/dist/patches/prompts-escape.d.ts.map +1 -0
  159. package/dist/patches/prompts-escape.js +23 -0
  160. package/dist/prompts/branches.d.ts +20 -0
  161. package/dist/prompts/branches.d.ts.map +1 -0
  162. package/dist/prompts/branches.js +86 -0
  163. package/dist/prompts/embeddables.d.ts +43 -0
  164. package/dist/prompts/embeddables.d.ts.map +1 -0
  165. package/dist/prompts/embeddables.js +200 -0
  166. package/dist/prompts/experiments.d.ts +28 -0
  167. package/dist/prompts/experiments.d.ts.map +1 -0
  168. package/dist/prompts/experiments.js +89 -0
  169. package/dist/prompts/index.d.ts +11 -0
  170. package/dist/prompts/index.d.ts.map +1 -0
  171. package/dist/prompts/index.js +6 -0
  172. package/dist/prompts/projects.d.ts +22 -0
  173. package/dist/prompts/projects.d.ts.map +1 -0
  174. package/dist/prompts/projects.js +92 -0
  175. package/dist/prompts/versions.d.ts +18 -0
  176. package/dist/prompts/versions.d.ts.map +1 -0
  177. package/dist/prompts/versions.js +95 -0
  178. package/dist/proxy/injectApiInterceptor.d.ts +6 -0
  179. package/dist/proxy/injectApiInterceptor.d.ts.map +1 -0
  180. package/dist/proxy/injectApiInterceptor.js +66 -0
  181. package/dist/proxy/injectReload.d.ts +2 -0
  182. package/dist/proxy/injectReload.d.ts.map +1 -0
  183. package/dist/proxy/injectReload.js +14 -0
  184. package/dist/proxy/injectWorkbench.d.ts +5 -0
  185. package/dist/proxy/injectWorkbench.d.ts.map +1 -0
  186. package/dist/proxy/injectWorkbench.js +22 -0
  187. package/dist/proxy/server.d.ts +11 -0
  188. package/dist/proxy/server.d.ts.map +1 -0
  189. package/dist/proxy/server.js +304 -0
  190. package/dist/proxy/sse.d.ts +5 -0
  191. package/dist/proxy/sse.d.ts.map +1 -0
  192. package/dist/proxy/sse.js +17 -0
  193. package/dist/sentry-context.d.ts +48 -0
  194. package/dist/sentry-context.d.ts.map +1 -0
  195. package/dist/sentry-context.js +156 -0
  196. package/dist/stdout.d.ts +61 -0
  197. package/dist/stdout.d.ts.map +1 -0
  198. package/dist/stdout.js +163 -0
  199. package/dist/types-builder.d.ts +800 -0
  200. package/dist/types-builder.d.ts.map +1 -0
  201. package/dist/types-builder.js +20 -0
  202. package/dist/workbench/ActionsPanel.d.ts +6 -0
  203. package/dist/workbench/ActionsPanel.d.ts.map +1 -0
  204. package/dist/workbench/ActionsPanel.js +47 -0
  205. package/dist/workbench/AutofillPanel.d.ts +6 -0
  206. package/dist/workbench/AutofillPanel.d.ts.map +1 -0
  207. package/dist/workbench/AutofillPanel.js +543 -0
  208. package/dist/workbench/ComputedFieldsPanel.d.ts +6 -0
  209. package/dist/workbench/ComputedFieldsPanel.d.ts.map +1 -0
  210. package/dist/workbench/ComputedFieldsPanel.js +31 -0
  211. package/dist/workbench/ExperimentsPanel.d.ts +6 -0
  212. package/dist/workbench/ExperimentsPanel.d.ts.map +1 -0
  213. package/dist/workbench/ExperimentsPanel.js +182 -0
  214. package/dist/workbench/FieldEditorPanel.d.ts +9 -0
  215. package/dist/workbench/FieldEditorPanel.d.ts.map +1 -0
  216. package/dist/workbench/FieldEditorPanel.js +650 -0
  217. package/dist/workbench/InspectorPanel.d.ts +6 -0
  218. package/dist/workbench/InspectorPanel.d.ts.map +1 -0
  219. package/dist/workbench/InspectorPanel.js +341 -0
  220. package/dist/workbench/PageNavigator.d.ts +6 -0
  221. package/dist/workbench/PageNavigator.d.ts.map +1 -0
  222. package/dist/workbench/PageNavigator.js +123 -0
  223. package/dist/workbench/SchemaPanel.d.ts +6 -0
  224. package/dist/workbench/SchemaPanel.d.ts.map +1 -0
  225. package/dist/workbench/SchemaPanel.js +222 -0
  226. package/dist/workbench/UserDataPanel.d.ts +6 -0
  227. package/dist/workbench/UserDataPanel.d.ts.map +1 -0
  228. package/dist/workbench/UserDataPanel.js +350 -0
  229. package/dist/workbench/WorkbenchApp.d.ts +7 -0
  230. package/dist/workbench/WorkbenchApp.d.ts.map +1 -0
  231. package/dist/workbench/WorkbenchApp.js +193 -0
  232. package/dist/workbench/index.d.ts +10 -0
  233. package/dist/workbench/index.d.ts.map +1 -0
  234. package/dist/workbench/index.js +44 -0
  235. package/package.json +2 -1
@@ -0,0 +1,1938 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import CSSJSON from 'cssjson';
4
+ import * as stdout from '../stdout.js';
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
+ stdout.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
+ 'parent_key',
288
+ 'buttons',
289
+ ...IGNORED_COMPONENT_PROPERTIES,
290
+ ]);
291
+ for (const [key, value] of Object.entries(component)) {
292
+ // Skip excluded props (they're handled separately or ignored)
293
+ if (excludedProps.has(key)) {
294
+ continue;
295
+ }
296
+ // Skip null/undefined values
297
+ if (value === null || value === undefined) {
298
+ continue;
299
+ }
300
+ // Skip lang-- prefixed properties (handled separately as `languages` prop)
301
+ if (key.startsWith('lang--')) {
302
+ continue;
303
+ }
304
+ // Check if prop is valid
305
+ if (validProps.has(key)) {
306
+ validPropKeys.add(key);
307
+ }
308
+ else {
309
+ // Extra prop (not in schema) - store for round-trip via props={...}
310
+ extraProps[key] = value;
311
+ }
312
+ }
313
+ return { validPropKeys, extraProps };
314
+ }
315
+ /**
316
+ * Collects multi-language properties from a component JSON object.
317
+ * Properties with the pattern `lang--{languageCode}--{attributeKey}` are grouped
318
+ * into a nested structure: { languageCode: { attributeKey: value } }.
319
+ * Returns null if no language properties are found.
320
+ */
321
+ function collectLanguageProps(component) {
322
+ const languages = {};
323
+ let found = false;
324
+ for (const [key, value] of Object.entries(component)) {
325
+ const match = key.match(/^lang--([^-]+)--(.+)$/);
326
+ if (match) {
327
+ const [, langCode, attrKey] = match;
328
+ if (!languages[langCode]) {
329
+ languages[langCode] = {};
330
+ }
331
+ languages[langCode][attrKey] = value;
332
+ found = true;
333
+ }
334
+ }
335
+ return found ? languages : null;
336
+ }
337
+ // Fallback: if type not in map, use type name directly (component files match type names)
338
+ function getComponentName(type) {
339
+ // First try exact match
340
+ if (REVERSE_TYPE_MAP[type]) {
341
+ return REVERSE_TYPE_MAP[type];
342
+ }
343
+ // If not found, try case-insensitive match against TYPE_MAP values
344
+ // to find the correct casing and return the corresponding TSX tag name
345
+ const typeLower = type.toLowerCase();
346
+ for (const [tsxName, jsonType] of Object.entries(TYPE_MAP)) {
347
+ if (jsonType.toLowerCase() === typeLower) {
348
+ return tsxName; // Return the TSX tag name (e.g., "CustomHTML")
349
+ }
350
+ }
351
+ // If still not found, return type as-is (for unknown types)
352
+ return type;
353
+ }
354
+ /**
355
+ * Collects all component and button IDs from all pages and checks for duplicates.
356
+ * Returns a map of occurrence key (pageKey:componentIndex:buttonIndex) -> new unique ID for any duplicates found.
357
+ *
358
+ * Note: This function only deduplicates IDs, not keys. Duplicate keys are intentional
359
+ * in many cases and should be preserved.
360
+ */
361
+ function checkAndFixDuplicateIds(pages, fix) {
362
+ const idMapping = new Map(); // Maps occurrence key -> new unique ID
363
+ const idOccurrences = new Map();
364
+ const allIds = new Set();
365
+ // First pass: collect all IDs with their context
366
+ for (const page of pages) {
367
+ for (let compIndex = 0; compIndex < page.components.length; compIndex++) {
368
+ const component = page.components[compIndex];
369
+ // Handle components without type
370
+ if (!component.type) {
371
+ if (fix) {
372
+ // Will be removed in buildTree, skip here
373
+ continue;
374
+ }
375
+ throw new Error(`Component (id: ${component.id}, key: ${component.key}) is missing required type property.`);
376
+ }
377
+ // Skip ignored types
378
+ if (IGNORED_COMPONENT_TYPES.has(component.type)) {
379
+ continue;
380
+ }
381
+ const compId = component.id;
382
+ allIds.add(compId);
383
+ const occurrenceKey = `${page.key}:${compIndex}`;
384
+ if (!idOccurrences.has(compId)) {
385
+ idOccurrences.set(compId, []);
386
+ }
387
+ idOccurrences.get(compId).push({
388
+ pageKey: page.key,
389
+ type: `component (${component.type})`,
390
+ id: compId,
391
+ componentType: component.type,
392
+ componentIndex: compIndex,
393
+ occurrenceKey,
394
+ });
395
+ // Collect button IDs from OptionSelector components
396
+ if (component.type === 'OptionSelector' && component.buttons) {
397
+ const buttons = component.buttons;
398
+ if (Array.isArray(buttons)) {
399
+ for (let btnIndex = 0; btnIndex < buttons.length; btnIndex++) {
400
+ const button = buttons[btnIndex];
401
+ if (button && typeof button === 'object' && button.id) {
402
+ const buttonId = button.id;
403
+ allIds.add(buttonId);
404
+ const buttonOccurrenceKey = `${page.key}:${compIndex}:${btnIndex}`;
405
+ if (!idOccurrences.has(buttonId)) {
406
+ idOccurrences.set(buttonId, []);
407
+ }
408
+ idOccurrences.get(buttonId).push({
409
+ pageKey: page.key,
410
+ type: `button (in ${component.type} component)`,
411
+ id: buttonId,
412
+ componentType: component.type,
413
+ componentIndex: compIndex,
414
+ buttonIndex: btnIndex,
415
+ occurrenceKey: buttonOccurrenceKey,
416
+ });
417
+ }
418
+ }
419
+ }
420
+ }
421
+ }
422
+ }
423
+ // Second pass: find duplicates and create unique IDs
424
+ for (const [id, occurrences] of idOccurrences.entries()) {
425
+ if (occurrences.length > 1) {
426
+ // This ID is duplicated
427
+ const isButton = occurrences[0].buttonIndex !== undefined;
428
+ const suffix = isButton ? ' (OptionSelector button)' : '';
429
+ stdout.warn(`Found duplicate ID "${id}"${suffix} used ${occurrences.length} times:`);
430
+ for (let i = 0; i < occurrences.length; i++) {
431
+ const occ = occurrences[i];
432
+ const location = occ.buttonIndex !== undefined
433
+ ? `button at index ${occ.buttonIndex}`
434
+ : `component at index ${occ.componentIndex}`;
435
+ stdout.print(` - Page "${occ.pageKey}": ${occ.type} (${location}) with ID "${occ.id}"`);
436
+ }
437
+ // Create unique IDs for all but the first occurrence (type-based: plaintext_xxx, button_xxx, option_xxx)
438
+ for (let i = 1; i < occurrences.length; i++) {
439
+ const occ = occurrences[i];
440
+ const isOptionButton = occ.buttonIndex !== undefined;
441
+ const newId = generateRandomIdByType(occ.componentType, isOptionButton, allIds, idMapping.values());
442
+ allIds.add(newId);
443
+ // Map the occurrence key (not the ID) to the new unique ID
444
+ idMapping.set(occ.occurrenceKey, newId);
445
+ const location = occ.buttonIndex !== undefined
446
+ ? `button at index ${occ.buttonIndex}`
447
+ : `component at index ${occ.componentIndex}`;
448
+ stdout.print(` → Renamed duplicate ID "${occ.id}" to "${newId}" in page "${occ.pageKey}" (${location})`);
449
+ }
450
+ }
451
+ }
452
+ return idMapping;
453
+ }
454
+ /**
455
+ * Fixes deprecated parent_key: resolves to parent_id using first component with matching key,
456
+ * removes parent_key, and removes components that still have no parent_id after resolution.
457
+ * Only runs when fix is enabled.
458
+ */
459
+ function fixParentKeyDeprecation(components, fix) {
460
+ if (!fix || components.length === 0)
461
+ return;
462
+ // Build key -> id map (first occurrence wins, exclude ignored types)
463
+ const keyToId = new Map();
464
+ for (const comp of components) {
465
+ if (comp.key &&
466
+ comp.id &&
467
+ !keyToId.has(comp.key) &&
468
+ comp.type &&
469
+ !IGNORED_COMPONENT_TYPES.has(comp.type)) {
470
+ keyToId.set(comp.key, comp.id);
471
+ }
472
+ }
473
+ // Resolve parent_key -> parent_id, remove parent_key, track components to remove
474
+ const toRemove = new Map(); // id -> parent_key for warning
475
+ for (const comp of components) {
476
+ const parentKey = comp.parent_key;
477
+ if (!parentKey)
478
+ continue;
479
+ if (!comp.parent_id) {
480
+ const resolvedId = keyToId.get(parentKey);
481
+ if (resolvedId) {
482
+ comp.parent_id = resolvedId;
483
+ stdout.warn(`Fixed parent_key on component (id: ${comp.id}, key: ${comp.key}) – resolved parent_key "${parentKey}" to parent_id.`);
484
+ }
485
+ else {
486
+ toRemove.set(comp.id, parentKey);
487
+ }
488
+ }
489
+ else {
490
+ stdout.warn(`Fixed parent_key on component (id: ${comp.id}, key: ${comp.key}) – removed deprecated parent_key (already has parent_id).`);
491
+ }
492
+ delete comp.parent_key;
493
+ }
494
+ // Remove components that couldn't resolve parent
495
+ if (toRemove.size > 0) {
496
+ let i = 0;
497
+ while (i < components.length) {
498
+ const comp = components[i];
499
+ if (toRemove.has(comp.id)) {
500
+ stdout.warn(`Removed component (id: ${comp.id}, key: ${comp.key}) – parent_key "${toRemove.get(comp.id)}" not found, no parent_id.`);
501
+ components.splice(i, 1);
502
+ }
503
+ else {
504
+ i++;
505
+ }
506
+ }
507
+ }
508
+ }
509
+ /**
510
+ * Applies the ID mapping to all pages, updating component IDs and button IDs.
511
+ * The mapping uses occurrence keys (pageKey:componentIndex or pageKey:componentIndex:buttonIndex).
512
+ * Note: parent_id references are not automatically updated because we can't determine
513
+ * which specific occurrence a parent_id points to when there are duplicate IDs.
514
+ */
515
+ function applyIdMapping(pages, idMapping) {
516
+ // Update component and button IDs using occurrence keys
517
+ for (const page of pages) {
518
+ for (let compIndex = 0; compIndex < page.components.length; compIndex++) {
519
+ const component = page.components[compIndex];
520
+ // Skip components without type or with ignored types
521
+ if (!component.type || IGNORED_COMPONENT_TYPES.has(component.type)) {
522
+ continue;
523
+ }
524
+ const occurrenceKey = `${page.key}:${compIndex}`;
525
+ if (idMapping.has(occurrenceKey)) {
526
+ component.id = idMapping.get(occurrenceKey);
527
+ }
528
+ // Update button IDs in OptionSelector components
529
+ if (component.type === 'OptionSelector' && component.buttons) {
530
+ const buttons = component.buttons;
531
+ if (Array.isArray(buttons)) {
532
+ for (let btnIndex = 0; btnIndex < buttons.length; btnIndex++) {
533
+ const button = buttons[btnIndex];
534
+ if (button && typeof button === 'object' && button.id) {
535
+ const buttonOccurrenceKey = `${page.key}:${compIndex}:${btnIndex}`;
536
+ if (idMapping.has(buttonOccurrenceKey)) {
537
+ button.id = idMapping.get(buttonOccurrenceKey);
538
+ }
539
+ }
540
+ }
541
+ }
542
+ }
543
+ }
544
+ }
545
+ }
546
+ function hasDeprecatedParentKey(components) {
547
+ return components.some((comp) => comp.parent_key != null);
548
+ }
549
+ export async function reverseCompile(embeddable, embeddableId, opts) {
550
+ const fix = opts?.fix ?? false;
551
+ const preserve = opts?.preserve ?? false;
552
+ const pullMetadata = opts?.pullMetadata;
553
+ // When fix is disabled, throw on deprecated parent_key so user gets interactive retry prompt
554
+ if (!fix) {
555
+ for (const page of embeddable.pages) {
556
+ if (hasDeprecatedParentKey(page.components)) {
557
+ const count = page.components.filter((c) => c.parent_key != null).length;
558
+ throw new Error(`Found deprecated parent_key on ${count} component(s) in page "${page.key}". Run with --fix to resolve.`);
559
+ }
560
+ }
561
+ if (embeddable.components && Array.isArray(embeddable.components)) {
562
+ if (hasDeprecatedParentKey(embeddable.components)) {
563
+ const count = embeddable.components.filter((c) => c.parent_key != null).length;
564
+ throw new Error(`Found deprecated parent_key on ${count} global component(s). Run with --fix to resolve.`);
565
+ }
566
+ }
567
+ }
568
+ // Check for duplicate IDs across all pages and create a mapping
569
+ const idMapping = checkAndFixDuplicateIds(embeddable.pages, fix);
570
+ // Apply ID mapping to all pages
571
+ if (idMapping.size > 0) {
572
+ applyIdMapping(embeddable.pages, idMapping);
573
+ }
574
+ // Fix deprecated parent_key (resolve to parent_id, remove parent_key, drop orphans).
575
+ // Must run before extractGlobalComponents so resolved parent_id avoids "must have _location since it has no parent_id" errors.
576
+ if (fix) {
577
+ for (const page of embeddable.pages) {
578
+ fixParentKeyDeprecation(page.components, fix);
579
+ }
580
+ if (embeddable.components && Array.isArray(embeddable.components)) {
581
+ fixParentKeyDeprecation(embeddable.components, fix);
582
+ }
583
+ }
584
+ // Generate TSX pages
585
+ for (const page of embeddable.pages) {
586
+ await generatePageFile(page, embeddableId, fix);
587
+ }
588
+ // Generate CSS from styles
589
+ if (embeddable.styles && Object.keys(embeddable.styles).length > 0) {
590
+ await generateStylesFile(embeddable.styles, embeddableId);
591
+ }
592
+ // Generate config.json
593
+ await generateConfigFile(embeddable, embeddableId, { pullMetadata, preserve });
594
+ // Extract computedFields to JS files
595
+ if (embeddable.computedFields && embeddable.computedFields.length > 0) {
596
+ await extractComputedFields(embeddable.computedFields, embeddableId);
597
+ }
598
+ // Extract dataOutputs to JS files
599
+ if (embeddable.dataOutputs && embeddable.dataOutputs.length > 0) {
600
+ await extractDataOutputs(embeddable.dataOutputs, embeddableId);
601
+ }
602
+ // Extract global components to TSX files
603
+ if (embeddable.components &&
604
+ Array.isArray(embeddable.components) &&
605
+ embeddable.components.length > 0) {
606
+ await extractGlobalComponents(embeddable.components, embeddableId, fix);
607
+ }
608
+ stdout.dim(`Generated ${embeddable.pages.length} page(s) and ${embeddable.styles && Object.keys(embeddable.styles).length > 0 ? 'styles' : 'no styles'}`);
609
+ }
610
+ async function generatePageFile(page, embeddableId, fix) {
611
+ const pageKey = page.key;
612
+ const filePath = path.join('embeddables', embeddableId, 'pages', `${pageKey}.page.tsx`);
613
+ try {
614
+ // Build tree from flat component list
615
+ const tree = buildTree(page.components, pageKey, fix);
616
+ // Generate TSX code
617
+ const tsx = generateTSX(page, tree);
618
+ // Write to embeddables/<id>/pages/<pageKey>.page.tsx
619
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
620
+ fs.writeFileSync(filePath, tsx, 'utf8');
621
+ stdout.dim(`Generated ${filePath}`);
622
+ }
623
+ catch (error) {
624
+ if (error instanceof Error) {
625
+ throw new Error(`Page "${pageKey}" (${filePath}): ${error.message}`);
626
+ }
627
+ throw error;
628
+ }
629
+ }
630
+ function buildTree(components, pageKey, fix) {
631
+ // Filter out components without type and ignored component types
632
+ let filteredComponents = components.filter((comp) => {
633
+ if (!comp.type) {
634
+ if (fix) {
635
+ stdout.warn(`Removed component (id: ${comp.id}, key: ${comp.key}) – missing type property.`);
636
+ return false;
637
+ }
638
+ throw new Error(`Component (id: ${comp.id}, key: ${comp.key}) is missing required type property.`);
639
+ }
640
+ return !IGNORED_COMPONENT_TYPES.has(comp.type);
641
+ });
642
+ // Filter out components missing required props
643
+ filteredComponents = filteredComponents.filter((comp) => {
644
+ return hasRequiredProps(comp, fix);
645
+ });
646
+ // Create a map of id -> component
647
+ const componentMap = new Map();
648
+ for (const comp of filteredComponents) {
649
+ componentMap.set(comp.id, comp);
650
+ }
651
+ // Find root components (no parent_id)
652
+ const roots = [];
653
+ for (const comp of filteredComponents) {
654
+ if (!comp.parent_id) {
655
+ roots.push(comp);
656
+ }
657
+ }
658
+ // Allow blank pages - return null if no components
659
+ if (roots.length === 0) {
660
+ return null;
661
+ }
662
+ // Build tree recursively for each root
663
+ const rootNodes = roots.map((root) => buildNode(root, componentMap, pageKey, fix));
664
+ // Return single node or array of nodes
665
+ return rootNodes.length === 1 ? rootNodes[0] : rootNodes;
666
+ }
667
+ function buildNode(component, componentMap, pageKey, fix) {
668
+ const children = [];
669
+ // Find all children of this component (excluding ignored types and those missing required props)
670
+ for (const comp of componentMap.values()) {
671
+ if (comp.parent_id === component.id &&
672
+ !IGNORED_COMPONENT_TYPES.has(comp.type) &&
673
+ hasRequiredProps(comp, fix)) {
674
+ children.push(buildNode(comp, componentMap, pageKey, fix));
675
+ }
676
+ }
677
+ return {
678
+ component,
679
+ children,
680
+ };
681
+ }
682
+ function generateTSX(page, tree) {
683
+ const pageName = capitalizeFirst(page.key) + 'Page';
684
+ const pageKey = page.key;
685
+ const imports = tree ? collectImports(tree) : [];
686
+ const hasButtons = tree ? hasButtonConstants(tree) : false;
687
+ const { constants, nameMap } = tree
688
+ ? generateButtonConstants(tree)
689
+ : { constants: '', nameMap: new Map() };
690
+ const { declarations: validationFunctions, validationFunctionNameMap, identifierToGeneratedName, } = tree
691
+ ? generateCustomValidationFunctions(tree)
692
+ : {
693
+ declarations: '',
694
+ validationFunctionNameMap: new Map(),
695
+ identifierToGeneratedName: new Map(),
696
+ };
697
+ const jsx = generateJSXFromTree(tree, 4, pageKey, nameMap, validationFunctionNameMap, identifierToGeneratedName);
698
+ return `"use client";
699
+
700
+ ${imports.map((imp) => `import { ${imp} } from "@embeddables/cli/components";`).join('\n')}
701
+ ${hasButtons
702
+ ? `
703
+ import type { OptionSelectorButtonWithLanguages as OptionSelectorButton } from "@embeddables/cli/components";`
704
+ : ''}
705
+
706
+ export default function ${pageName}() {
707
+ ${constants}${validationFunctions}
708
+ return (
709
+ ${jsx}
710
+ );
711
+ }
712
+ `;
713
+ }
714
+ function collectImports(node) {
715
+ if (node === null) {
716
+ return [];
717
+ }
718
+ const imports = new Set();
719
+ const nodes = Array.isArray(node) ? node : [node];
720
+ for (const n of nodes) {
721
+ const type = n.component.type;
722
+ const tagName = getComponentName(type);
723
+ imports.add(tagName);
724
+ for (const child of n.children) {
725
+ for (const imp of collectImports(child)) {
726
+ imports.add(imp);
727
+ }
728
+ }
729
+ }
730
+ return Array.from(imports).sort();
731
+ }
732
+ function hasButtonConstants(node) {
733
+ if (node === null) {
734
+ return false;
735
+ }
736
+ function checkButtons(n) {
737
+ if (n.component.type === 'OptionSelector' && n.component.buttons) {
738
+ const buttons = n.component.buttons;
739
+ if (Array.isArray(buttons) && buttons.length > 0) {
740
+ return true;
741
+ }
742
+ }
743
+ for (const child of n.children) {
744
+ if (checkButtons(child)) {
745
+ return true;
746
+ }
747
+ }
748
+ return false;
749
+ }
750
+ const nodes = Array.isArray(node) ? node : [node];
751
+ for (const n of nodes) {
752
+ if (checkButtons(n)) {
753
+ return true;
754
+ }
755
+ }
756
+ return false;
757
+ }
758
+ /** True if the string is a valid JS identifier (used as function name only). */
759
+ function isSimpleIdentifier(str) {
760
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(str) && str.length > 0;
761
+ }
762
+ /**
763
+ * Extract the body of a function from source like "function name(value) { ... }".
764
+ * Returns the inner content between the first { and its matching }, or null if not found.
765
+ */
766
+ function extractFunctionBody(source) {
767
+ const open = source.indexOf('{');
768
+ if (open === -1)
769
+ return null;
770
+ let depth = 1;
771
+ let i = open + 1;
772
+ while (depth > 0 && i < source.length) {
773
+ if (source[i] === '{')
774
+ depth++;
775
+ else if (source[i] === '}')
776
+ depth--;
777
+ i++;
778
+ }
779
+ return depth === 0 ? source.slice(open + 1, i - 1).trim() : null;
780
+ }
781
+ /**
782
+ * Generate custom validation function declarations and a map from component id to function name.
783
+ * - If value is a simple identifier (e.g. "customValidate"): emit a stub and reference it by name.
784
+ * - If value is function code: emit one named function per component with the actual body; map component id -> name.
785
+ */
786
+ function generateCustomValidationFunctions(node) {
787
+ const validationFunctionNameMap = new Map();
788
+ const identifierNames = new Set();
789
+ const codeByComponentId = new Map();
790
+ if (node === null) {
791
+ return {
792
+ declarations: '',
793
+ validationFunctionNameMap: new Map(),
794
+ identifierToGeneratedName: new Map(),
795
+ };
796
+ }
797
+ function collect(n) {
798
+ if (n.component.type === 'InputBox') {
799
+ if (n.component.validation_formula !== 'custom') {
800
+ for (const child of n.children)
801
+ collect(child);
802
+ return;
803
+ }
804
+ const value = n.component.custom_validation_function;
805
+ if (typeof value !== 'string' || !value)
806
+ return;
807
+ if (isSimpleIdentifier(value)) {
808
+ identifierNames.add(value);
809
+ }
810
+ else {
811
+ codeByComponentId.set(n.component.id, value);
812
+ }
813
+ }
814
+ for (const child of n.children)
815
+ collect(child);
816
+ }
817
+ const nodes = Array.isArray(node) ? node : [node];
818
+ for (const n of nodes)
819
+ collect(n);
820
+ const lines = [];
821
+ // Use hardcoded "validate" (then validate2, validate3 ...) and emit JS, not TS
822
+ let usedValidateIndex = 0;
823
+ const nextValidateName = () => {
824
+ const n = usedValidateIndex;
825
+ usedValidateIndex++;
826
+ return n === 0 ? 'validate' : `validate${n + 1}`;
827
+ };
828
+ const identifierToGeneratedName = new Map();
829
+ // Stubs for identifier-only names: all named "validate", "validate2", ...
830
+ for (const name of Array.from(identifierNames).sort()) {
831
+ const fnName = nextValidateName();
832
+ identifierToGeneratedName.set(name, fnName);
833
+ lines.push(` function ${fnName}(value) {\n return true\n }`);
834
+ }
835
+ // One function per component with inline code; name is "validate", "validate2", ...
836
+ for (const [compId, code] of codeByComponentId) {
837
+ const fnName = nextValidateName();
838
+ validationFunctionNameMap.set(compId, fnName);
839
+ const body = extractFunctionBody(code);
840
+ if (body !== null) {
841
+ const indentedBody = body
842
+ .split('\n')
843
+ .map((l) => ' ' + l.trimStart())
844
+ .join('\n');
845
+ lines.push(` function ${fnName}(value) {\n${indentedBody}\n }`);
846
+ }
847
+ else {
848
+ lines.push(` function ${fnName}(value) {\n return (${code})\n }`);
849
+ }
850
+ }
851
+ const declarations = lines.length > 0 ? lines.join('\n') + '\n' : '';
852
+ return { declarations, validationFunctionNameMap, identifierToGeneratedName };
853
+ }
854
+ function findComponentNodeById(node, id) {
855
+ if (node === null)
856
+ return null;
857
+ const nodes = Array.isArray(node) ? node : [node];
858
+ for (const n of nodes) {
859
+ if (n.component.id === id)
860
+ return n;
861
+ for (const child of n.children) {
862
+ const found = findComponentNodeById(child, id);
863
+ if (found)
864
+ return found;
865
+ }
866
+ }
867
+ return null;
868
+ }
869
+ function generateButtonConstants(node) {
870
+ if (node === null) {
871
+ return { constants: '', nameMap: new Map() };
872
+ }
873
+ const constants = [];
874
+ const usedNames = new Set(); // Track used constant names
875
+ const nameMap = new Map(); // Map component ID -> constant name
876
+ function collectButtons(n) {
877
+ if (n.component.type === 'OptionSelector' && n.component.buttons) {
878
+ const buttons = n.component.buttons;
879
+ if (Array.isArray(buttons) && buttons.length > 0) {
880
+ // Generate a constant name based on the component key or id
881
+ let baseName = camelCase(n.component.key || n.component.id) + 'Buttons';
882
+ let constName = baseName;
883
+ // Ensure uniqueness by checking if name is already used
884
+ let suffix = 1;
885
+ if (usedNames.has(constName)) {
886
+ // If name collision, append component ID suffix to ensure uniqueness
887
+ const idSuffix = camelCase(n.component.id);
888
+ constName = baseName.replace(/Buttons$/, '') + idSuffix + 'Buttons';
889
+ }
890
+ // If still not unique, add numeric suffix
891
+ while (usedNames.has(constName)) {
892
+ constName = baseName.replace(/Buttons$/, '') + suffix + 'Buttons';
893
+ suffix++;
894
+ }
895
+ usedNames.add(constName);
896
+ nameMap.set(n.component.id, constName);
897
+ const buttonArray = buttons
898
+ .map((b) => {
899
+ const props = [];
900
+ // Always include id - preserve from JSON or generate if missing
901
+ const buttonId = b.id || `option_${b.key}`;
902
+ props.push(`id: ${escapeStringForJS(buttonId)}`);
903
+ // Preserve key if present
904
+ if (b.key)
905
+ props.push(`key: ${escapeStringForJS(b.key)}`);
906
+ // Preserve text if present
907
+ if (b.text)
908
+ props.push(`text: ${escapeStringForJS(b.text)}`);
909
+ // Preserve other button properties from JSON (e.g., triggerEvent, description, imageUrl, etc.)
910
+ const buttonPropertyKeys = new Set(['id', 'key', 'text']);
911
+ const buttonLangs = {};
912
+ for (const [key, value] of Object.entries(b)) {
913
+ if (buttonPropertyKeys.has(key))
914
+ continue;
915
+ if (value === null || value === undefined)
916
+ continue;
917
+ // Collect lang-- properties into a languages object
918
+ const langMatch = key.match(/^lang--([^-]+)--(.+)$/);
919
+ if (langMatch) {
920
+ const [, langCode, attrKey] = langMatch;
921
+ if (!buttonLangs[langCode])
922
+ buttonLangs[langCode] = {};
923
+ buttonLangs[langCode][attrKey] = value;
924
+ continue;
925
+ }
926
+ if (typeof value === 'string') {
927
+ props.push(`${key}: ${escapeStringForJS(value)}`);
928
+ }
929
+ else if (typeof value === 'number' || typeof value === 'boolean') {
930
+ props.push(`${key}: ${value}`);
931
+ }
932
+ else if (Array.isArray(value) || typeof value === 'object') {
933
+ // Handle arrays (e.g., conditions) and objects
934
+ props.push(`${key}: ${JSON.stringify(value)}`);
935
+ }
936
+ }
937
+ // Add languages prop if any lang-- properties were found
938
+ if (Object.keys(buttonLangs).length > 0) {
939
+ props.push(`languages: ${valueToJSXExpression(buttonLangs)}`);
940
+ }
941
+ return `{ ${props.join(', ')} }`;
942
+ })
943
+ .join(',\n ');
944
+ constants.push(` const ${constName}: OptionSelectorButton[] = [\n ${buttonArray}\n ];`);
945
+ }
946
+ }
947
+ for (const child of n.children) {
948
+ collectButtons(child);
949
+ }
950
+ }
951
+ const nodes = Array.isArray(node) ? node : [node];
952
+ for (const n of nodes) {
953
+ collectButtons(n);
954
+ }
955
+ return {
956
+ constants: constants.length > 0 ? constants.join('\n') + '\n' : '',
957
+ nameMap,
958
+ };
959
+ }
960
+ function generateJSXFromTree(tree, indent = 4, pageKey, nameMap, validationFunctionNameMap, identifierToGeneratedName) {
961
+ if (tree === null) {
962
+ const indentStr = ' '.repeat(indent);
963
+ return `${indentStr}<> </>`;
964
+ }
965
+ else if (Array.isArray(tree)) {
966
+ const childrenJSX = tree
967
+ .map((node) => generateJSX(node, indent + 2, pageKey, node.component.id, nameMap, validationFunctionNameMap, identifierToGeneratedName))
968
+ .join('\n');
969
+ const indentStr = ' '.repeat(indent);
970
+ return `${indentStr}<>\n${childrenJSX}\n${indentStr}</>`;
971
+ }
972
+ else {
973
+ return generateJSX(tree, indent, pageKey, tree.component.id, nameMap, validationFunctionNameMap, identifierToGeneratedName);
974
+ }
975
+ }
976
+ function generateJSX(node, indent = 4, pageKey, componentId, nameMap, validationFunctionNameMap, identifierToGeneratedName) {
977
+ const comp = node.component;
978
+ const type = comp.type;
979
+ const tagName = getComponentName(type);
980
+ // Note: We don't throw errors here, but if we did, we'd use the context
981
+ // The context is available for future error handling if needed
982
+ // Get valid props for this component type (default to base props if unknown)
983
+ const validProps = COMPONENT_SPECIFIC_PROPS[tagName] || COMPONENT_SPECIFIC_PROPS[type] || BASE_COMPONENT_PROPS;
984
+ // Validate props; get valid keys and any extra (non-schema) props to store in props={}
985
+ const { validPropKeys, extraProps } = validateComponentProps(comp, validProps);
986
+ const props = [];
987
+ // Always preserve component ID from JSON
988
+ props.push(`id=${escapeStringForJSX(comp.id)}`);
989
+ props.push(`key=${escapeStringForJSX(comp.key)}`);
990
+ // Add tags if present
991
+ if (comp.tags && Array.isArray(comp.tags) && comp.tags.length > 0) {
992
+ props.push(`tags={[${comp.tags.map((t) => `"${t}"`).join(', ')}]}`);
993
+ }
994
+ // Add other props (only include validated props, excluding id, key, tags, type, parent_id, buttons, _location, and ignored properties)
995
+ // _location is excluded because it's derived from the filename (e.g., before_page.location.tsx)
996
+ const excludedProps = new Set([
997
+ 'id',
998
+ 'key',
999
+ 'tags',
1000
+ 'type',
1001
+ 'parent_id',
1002
+ 'parent_key',
1003
+ 'buttons',
1004
+ '_location',
1005
+ ...IGNORED_COMPONENT_PROPERTIES,
1006
+ ]);
1007
+ for (const [key, value] of Object.entries(comp)) {
1008
+ if (excludedProps.has(key))
1009
+ continue;
1010
+ // Only include props that were validated as valid
1011
+ if (!validPropKeys.has(key))
1012
+ continue;
1013
+ if (value === null || value === undefined)
1014
+ continue;
1015
+ if (typeof value === 'string') {
1016
+ // custom_validation_function: only emit when validation_formula is "custom"; otherwise ignore (removed on save).
1017
+ if (key === 'custom_validation_function') {
1018
+ if (comp.validation_formula !== 'custom')
1019
+ continue;
1020
+ const fnName = validationFunctionNameMap?.get(comp.id) ?? identifierToGeneratedName?.get(value) ?? value;
1021
+ props.push(`custom_validation_function={${fnName}}`);
1022
+ continue;
1023
+ }
1024
+ // Handle style prop: convert CSS string to JSX style object
1025
+ if (key === 'style') {
1026
+ const styleObj = cssStringToStyleObject(value);
1027
+ const styleStr = styleObjectToJSX(styleObj);
1028
+ props.push(`style={${styleStr}}`);
1029
+ }
1030
+ else if (value.includes('{{')) {
1031
+ // Handle template variables: convert {{variable}} strings to template literals
1032
+ // e.g., "{{first_name}}'s plan" -> {`{{first_name}}'s plan`}
1033
+ const templateLiteral = stringToTemplateLiteral(value);
1034
+ props.push(`${key}={${templateLiteral}}`);
1035
+ }
1036
+ else {
1037
+ props.push(`${key}=${escapeStringForJSX(value)}`);
1038
+ }
1039
+ }
1040
+ else if (typeof value === 'number' || typeof value === 'boolean') {
1041
+ props.push(`${key}={${value}}`);
1042
+ }
1043
+ else if (Array.isArray(value)) {
1044
+ // Handle arrays (e.g., conditions, conditional_tags, outputs_onmounted, etc.)
1045
+ // buttons are excluded and handled separately below
1046
+ const arrayExpression = valueToJSXExpression(value);
1047
+ props.push(`${key}={${arrayExpression}}`);
1048
+ }
1049
+ else if (typeof value === 'object') {
1050
+ // Handle objects (e.g., nested condition objects, etc.)
1051
+ const objectExpression = valueToJSXExpression(value);
1052
+ props.push(`${key}={${objectExpression}}`);
1053
+ }
1054
+ }
1055
+ // Handle OptionSelector buttons - use the name from the map if available
1056
+ if (type === 'OptionSelector' && comp.buttons) {
1057
+ const buttons = comp.buttons;
1058
+ if (Array.isArray(buttons) && buttons.length > 0) {
1059
+ const constName = nameMap?.get(comp.id) || camelCase(comp.key || comp.id) + 'Buttons';
1060
+ props.push(`buttons={${constName}}`);
1061
+ }
1062
+ }
1063
+ // Store extra (non-schema) props in a props={...} JSON object for round-trip preservation
1064
+ if (Object.keys(extraProps).length > 0) {
1065
+ props.push(`props={${valueToJSXExpression(extraProps)}}`);
1066
+ }
1067
+ // Collect multi-language properties (lang--{lang}--{attr}) into a languages prop
1068
+ const languages = collectLanguageProps(comp);
1069
+ if (languages) {
1070
+ props.push(`languages={${valueToJSXExpression(languages)}}`);
1071
+ }
1072
+ // Handle CustomHTML: convert text prop to JSX children
1073
+ let customHTMLChildren = null;
1074
+ if (type === 'CustomHTML' && comp.text && typeof comp.text === 'string') {
1075
+ // Remove text from props (we'll use children instead)
1076
+ const textIndex = props.findIndex((p) => p.startsWith('text='));
1077
+ if (textIndex !== -1) {
1078
+ props.splice(textIndex, 1);
1079
+ }
1080
+ // Convert HTML to JSX children
1081
+ customHTMLChildren = htmlToJSXChildren(comp.text, indent + 2);
1082
+ }
1083
+ const indentStr = ' '.repeat(indent);
1084
+ const childIndent = indent + 2;
1085
+ // If CustomHTML has children from HTML conversion, use those
1086
+ if (customHTMLChildren !== null) {
1087
+ return `${indentStr}<${tagName} ${props.join(' ')}>\n${customHTMLChildren}\n${indentStr}</${tagName}>`;
1088
+ }
1089
+ if (node.children.length === 0) {
1090
+ return `${indentStr}<${tagName} ${props.join(' ')} />`;
1091
+ }
1092
+ const childrenJSX = node.children
1093
+ .map((child) => generateJSX(child, childIndent, pageKey, child.component.id, nameMap, validationFunctionNameMap, identifierToGeneratedName))
1094
+ .join('\n');
1095
+ return `${indentStr}<${tagName} ${props.join(' ')}>\n${childrenJSX}\n${indentStr}</${tagName}>`;
1096
+ }
1097
+ async function generateStylesFile(styles, embeddableId) {
1098
+ try {
1099
+ // Convert JSON back to CSS
1100
+ // CSSJSON format: { children: { selector: { attributes: {...} } } }
1101
+ const cssJson = {
1102
+ children: {},
1103
+ };
1104
+ for (const [selector, attributes] of Object.entries(styles)) {
1105
+ cssJson.children[selector] = {
1106
+ attributes: attributes,
1107
+ };
1108
+ }
1109
+ const css = CSSJSON.toCSS(cssJson);
1110
+ // Write to embeddables/<id>/styles/index.css
1111
+ const filePath = path.join('embeddables', embeddableId, 'styles', 'index.css');
1112
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
1113
+ fs.writeFileSync(filePath, css, 'utf8');
1114
+ stdout.dim(`Generated ${filePath}`);
1115
+ }
1116
+ catch (error) {
1117
+ if (error instanceof Error) {
1118
+ throw new Error(`Styles: ${error.message}`);
1119
+ }
1120
+ throw error;
1121
+ }
1122
+ }
1123
+ // Helper functions
1124
+ function capitalizeFirst(str) {
1125
+ return str.charAt(0).toUpperCase() + str.slice(1);
1126
+ }
1127
+ function camelCase(str) {
1128
+ return str
1129
+ .replace(/[^a-zA-Z0-9]/g, ' ')
1130
+ .split(' ')
1131
+ .map((word, index) => {
1132
+ if (index === 0) {
1133
+ return word.toLowerCase();
1134
+ }
1135
+ return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
1136
+ })
1137
+ .join('')
1138
+ .replace(/[^a-zA-Z0-9]/g, '');
1139
+ }
1140
+ function escapeStringForJSX(str) {
1141
+ const hasDoubleQuotes = str.includes('"');
1142
+ const hasNewlines = /[\r\n]/.test(str);
1143
+ // Use a template literal expression {`...`} when JS escape processing is needed:
1144
+ // - String contains double quotes (can't use them as JSX attribute delimiter)
1145
+ // - String contains actual newline characters (JSX attribute strings can't represent these)
1146
+ if (hasDoubleQuotes || hasNewlines) {
1147
+ let escaped = str
1148
+ .replace(/\\/g, '\\\\') // Escape backslashes (template literals process escape sequences)
1149
+ .replace(/`/g, '\\`') // Escape backticks
1150
+ .replace(/\$\{/g, '\\${'); // Escape template literal expressions
1151
+ escaped = escaped
1152
+ .replace(/\r\n/g, '\\n') // Windows line endings
1153
+ .replace(/\r/g, '\\n') // Old Mac line endings
1154
+ .replace(/\n/g, '\\n'); // Unix line endings
1155
+ return `{\`${escaped}\`}`;
1156
+ }
1157
+ // JSX attribute strings treat all characters literally (no escape processing).
1158
+ // Backslashes, \n, etc. are NOT interpreted — they are stored as-is.
1159
+ // Since there are no double quotes in the string, use double quotes as delimiter.
1160
+ return `"${str}"`;
1161
+ }
1162
+ /**
1163
+ * Converts HTML string to JSX children code.
1164
+ * This is a simplified parser that handles common HTML cases.
1165
+ */
1166
+ function htmlToJSXChildren(html, indent) {
1167
+ const indentStr = ' '.repeat(indent);
1168
+ const result = [];
1169
+ // Simple HTML parser using regex (handles common cases)
1170
+ // This is a basic implementation - for production, consider using a proper HTML parser
1171
+ let pos = 0;
1172
+ const len = html.length;
1173
+ while (pos < len) {
1174
+ // Skip whitespace at the start
1175
+ const whitespaceMatch = html.slice(pos).match(/^\s+/);
1176
+ if (whitespaceMatch) {
1177
+ pos += whitespaceMatch[0].length;
1178
+ continue;
1179
+ }
1180
+ // Check for HTML comment and skip it
1181
+ if (html.slice(pos).startsWith('<!--')) {
1182
+ const commentEnd = html.indexOf('-->', pos);
1183
+ if (commentEnd !== -1) {
1184
+ pos = commentEnd + 3; // Skip past -->
1185
+ continue;
1186
+ }
1187
+ else {
1188
+ // Malformed comment - skip to end
1189
+ break;
1190
+ }
1191
+ }
1192
+ // Skip <!DOCTYPE ...>, <?xml ...?>, and other non-element tags that the
1193
+ // main tag regex cannot match. Without this guard `pos` would never
1194
+ // advance, causing an infinite loop.
1195
+ if (html[pos] === '<' && (html[pos + 1] === '!' || html[pos + 1] === '?')) {
1196
+ const gtIdx = html.indexOf('>', pos);
1197
+ if (gtIdx !== -1) {
1198
+ pos = gtIdx + 1;
1199
+ continue;
1200
+ }
1201
+ else {
1202
+ break;
1203
+ }
1204
+ }
1205
+ // Check for HTML tag
1206
+ const tagMatch = html.slice(pos).match(/^<(\/?)([a-zA-Z][a-zA-Z0-9]*)\s*([^>]*)>/);
1207
+ if (tagMatch) {
1208
+ const isClosing = tagMatch[1] === '/';
1209
+ const tagName = tagMatch[2];
1210
+ const htmlAttrsStr = tagMatch[3];
1211
+ const fullMatch = tagMatch[0];
1212
+ if (isClosing) {
1213
+ // Closing tag - we'll handle this in the opening tag logic
1214
+ pos += fullMatch.length;
1215
+ continue;
1216
+ }
1217
+ // Check if it's a self-closing tag
1218
+ const isSelfClosing = htmlAttrsStr.endsWith('/') ||
1219
+ [
1220
+ 'img',
1221
+ 'br',
1222
+ 'hr',
1223
+ 'input',
1224
+ 'meta',
1225
+ 'link',
1226
+ 'area',
1227
+ 'base',
1228
+ 'col',
1229
+ 'embed',
1230
+ 'source',
1231
+ 'track',
1232
+ 'wbr',
1233
+ ].includes(tagName.toLowerCase());
1234
+ // Parse attributes
1235
+ const attrs = [];
1236
+ // First, find all attribute names (with or without values)
1237
+ // Match: attr, attr=value, attr="value", attr='value'
1238
+ // We need to handle attributes without values (like <img alt />) and skip them
1239
+ const attrRegex = /(\w+)(?:="([^"]*)"|='([^']*)'|=(.+?)(?=[\s>]|$))?/g;
1240
+ let attrMatch;
1241
+ const seenAttrs = new Set();
1242
+ while ((attrMatch = attrRegex.exec(htmlAttrsStr)) !== null) {
1243
+ const attrName = attrMatch[1];
1244
+ const attrValue = attrMatch[2] || attrMatch[3] || attrMatch[4] || '';
1245
+ // Skip if we've already seen this attribute
1246
+ if (seenAttrs.has(attrName))
1247
+ continue;
1248
+ seenAttrs.add(attrName);
1249
+ // Skip attributes without values (like <img alt />) - these should be removed
1250
+ if (!attrMatch[0].includes('=')) {
1251
+ continue;
1252
+ }
1253
+ // In CustomHTML we emit HTML-like attribute names (class, for, tabindex) so the
1254
+ // TSX matches the serialized HTML. Normalize classname (wrong serialization) to class.
1255
+ let jsxAttrName = attrName;
1256
+ const attrLower = attrName.toLowerCase();
1257
+ if (attrLower === 'class' || attrLower === 'classname')
1258
+ jsxAttrName = 'class';
1259
+ else if (attrLower === 'for')
1260
+ jsxAttrName = 'for';
1261
+ else if (attrLower === 'tabindex')
1262
+ jsxAttrName = 'tabindex';
1263
+ // Handle style attribute: convert CSS string to JSX object
1264
+ if (attrName === 'style' && attrValue) {
1265
+ const styleObj = cssStringToStyleObject(attrValue);
1266
+ const styleStr = styleObjectToJSX(styleObj);
1267
+ attrs.push(`style={${styleStr}}`);
1268
+ continue;
1269
+ }
1270
+ // Skip attributes with empty values (like alt="")
1271
+ if (attrValue === '') {
1272
+ continue;
1273
+ }
1274
+ // Escape the attribute value for JSX
1275
+ // JSX attribute strings don't support \" escaping like JS strings
1276
+ // So if the value contains double quotes, use single quotes for the outer delimiter
1277
+ const hasDoubleQuotes = attrValue.includes('"');
1278
+ const hasSingleQuotes = attrValue.includes("'");
1279
+ const hasNewlines = /[\r\n]/.test(attrValue);
1280
+ if ((hasDoubleQuotes && hasSingleQuotes) || hasNewlines) {
1281
+ // Use JSX expression with template literal (JS escape processing applies)
1282
+ const escapedValue = attrValue
1283
+ .replace(/\\/g, '\\\\')
1284
+ .replace(/`/g, '\\`')
1285
+ .replace(/\$\{/g, '\\${')
1286
+ .replace(/\r\n/g, '\\n')
1287
+ .replace(/\r/g, '\\n')
1288
+ .replace(/\n/g, '\\n');
1289
+ attrs.push(`${jsxAttrName}={\`${escapedValue}\`}`);
1290
+ }
1291
+ else if (hasDoubleQuotes) {
1292
+ // JSX attribute strings are literal — no backslash escaping needed
1293
+ attrs.push(`${jsxAttrName}='${attrValue}'`);
1294
+ }
1295
+ else {
1296
+ // JSX attribute strings are literal — no backslash escaping needed
1297
+ attrs.push(`${jsxAttrName}="${attrValue}"`);
1298
+ }
1299
+ }
1300
+ pos += fullMatch.length;
1301
+ // Special handling for <style> and <script> tags: preserve content as raw text
1302
+ const isRawTextTag = tagName.toLowerCase() === 'style' || tagName.toLowerCase() === 'script';
1303
+ // Find the content and closing tag for non-self-closing tags
1304
+ if (!isSelfClosing) {
1305
+ // Find matching closing tag
1306
+ const closingTag = `</${tagName}>`;
1307
+ let depth = 1;
1308
+ let searchPos = pos;
1309
+ let contentEnd = -1;
1310
+ while (depth > 0 && searchPos < len) {
1311
+ const nextOpen = html.indexOf(`<${tagName}`, searchPos);
1312
+ const nextClose = html.indexOf(closingTag, searchPos);
1313
+ if (nextClose === -1) {
1314
+ // No closing tag found - treat as self-closing
1315
+ break;
1316
+ }
1317
+ if (nextOpen !== -1 && nextOpen < nextClose) {
1318
+ // Found nested opening tag
1319
+ depth++;
1320
+ searchPos = nextOpen + 1;
1321
+ }
1322
+ else {
1323
+ // Found closing tag
1324
+ depth--;
1325
+ if (depth === 0) {
1326
+ contentEnd = nextClose;
1327
+ }
1328
+ else {
1329
+ searchPos = nextClose + closingTag.length;
1330
+ }
1331
+ }
1332
+ }
1333
+ if (contentEnd !== -1) {
1334
+ const content = html.slice(pos, contentEnd);
1335
+ const attrsStr = attrs.length > 0 ? ' ' + attrs.join(' ') : '';
1336
+ if (isRawTextTag) {
1337
+ // For <style> and <script>, preserve content as raw text without processing
1338
+ // In JSX, this content must be wrapped in a template literal string expression
1339
+ const rawContent = content;
1340
+ if (rawContent.trim()) {
1341
+ // Escape backticks and ${ in the content to prevent template literal issues
1342
+ const escapedContent = rawContent
1343
+ .replace(/\\/g, '\\\\') // Escape backslashes first
1344
+ .replace(/`/g, '\\`') // Escape backticks
1345
+ .replace(/\${/g, '\\${'); // Escape ${ to prevent interpolation
1346
+ // Output as a template literal wrapped in curly braces for JSX
1347
+ result.push(`${indentStr}<${tagName}${attrsStr}>{`);
1348
+ result.push(`${indentStr} \`${escapedContent}\``);
1349
+ result.push(`${indentStr}}</${tagName}>`);
1350
+ }
1351
+ else {
1352
+ result.push(`${indentStr}<${tagName}${attrsStr} />`);
1353
+ }
1354
+ }
1355
+ else {
1356
+ // For other tags, process children recursively
1357
+ const childrenJSX = htmlToJSXChildren(content, indent + 2);
1358
+ if (childrenJSX.trim()) {
1359
+ result.push(`${indentStr}<${tagName}${attrsStr}>`);
1360
+ result.push(childrenJSX);
1361
+ result.push(`${indentStr}</${tagName}>`);
1362
+ }
1363
+ else {
1364
+ result.push(`${indentStr}<${tagName}${attrsStr} />`);
1365
+ }
1366
+ }
1367
+ pos = contentEnd + closingTag.length;
1368
+ continue;
1369
+ }
1370
+ }
1371
+ // Self-closing tag or no closing tag found
1372
+ const attrsStr = attrs.length > 0 ? ' ' + attrs.join(' ') : '';
1373
+ result.push(`${indentStr}<${tagName}${attrsStr} />`);
1374
+ continue;
1375
+ }
1376
+ // Text content — if the next '<' is at the current position the character
1377
+ // wasn't consumed by any handler above. Skip it so we never stall.
1378
+ const textEnd = html.indexOf('<', pos);
1379
+ if (textEnd === pos) {
1380
+ pos++;
1381
+ continue;
1382
+ }
1383
+ if (textEnd === -1) {
1384
+ // Rest is text
1385
+ const text = html.slice(pos).trim();
1386
+ if (text) {
1387
+ // Unescape HTML entities
1388
+ const unescapedText = text
1389
+ .replace(/&amp;/g, '&')
1390
+ .replace(/&lt;/g, '<')
1391
+ .replace(/&gt;/g, '>')
1392
+ .replace(/&quot;/g, '"')
1393
+ .replace(/&#39;/g, "'")
1394
+ .replace(/&nbsp;/g, ' ');
1395
+ // Check if text contains template variables
1396
+ if (unescapedText.includes('{{')) {
1397
+ // Convert to template literal expression
1398
+ const templateLiteral = stringToTemplateLiteral(unescapedText);
1399
+ result.push(`${indentStr}{${templateLiteral}}`);
1400
+ }
1401
+ else {
1402
+ // Escape special characters for JSX text content
1403
+ const escapedText = escapeTextForJSX(unescapedText);
1404
+ result.push(`${indentStr}${escapedText}`);
1405
+ }
1406
+ }
1407
+ break;
1408
+ }
1409
+ else {
1410
+ const text = html.slice(pos, textEnd).trim();
1411
+ if (text) {
1412
+ // Unescape HTML entities
1413
+ const unescapedText = text
1414
+ .replace(/&amp;/g, '&')
1415
+ .replace(/&lt;/g, '<')
1416
+ .replace(/&gt;/g, '>')
1417
+ .replace(/&quot;/g, '"')
1418
+ .replace(/&#39;/g, "'")
1419
+ .replace(/&nbsp;/g, ' ');
1420
+ // Check if text contains template variables
1421
+ if (unescapedText.includes('{{')) {
1422
+ // Convert to template literal expression
1423
+ const templateLiteral = stringToTemplateLiteral(unescapedText);
1424
+ result.push(`${indentStr}{${templateLiteral}}`);
1425
+ }
1426
+ else {
1427
+ // Escape special characters for JSX text content
1428
+ const escapedText = escapeTextForJSX(unescapedText);
1429
+ result.push(`${indentStr}${escapedText}`);
1430
+ }
1431
+ }
1432
+ pos = textEnd;
1433
+ }
1434
+ }
1435
+ return result.join('\n');
1436
+ }
1437
+ /**
1438
+ * Converts a CSS string to a style object.
1439
+ * Example: "margin: 0; padding: 0; color: #171717;" -> { margin: 0, padding: 0, color: "#171717" }
1440
+ */
1441
+ function cssStringToStyleObject(cssString) {
1442
+ const styleObj = {};
1443
+ // Normalize the CSS string: remove newlines, extra spaces, and trim
1444
+ const normalized = cssString
1445
+ .replace(/\r\n/g, ' ')
1446
+ .replace(/\r/g, ' ')
1447
+ .replace(/\n/g, ' ')
1448
+ .replace(/\s+/g, ' ')
1449
+ .trim();
1450
+ // Split by semicolon and process each declaration
1451
+ const declarations = normalized.split(';').filter((decl) => decl.trim().length > 0);
1452
+ for (const decl of declarations) {
1453
+ const colonIndex = decl.indexOf(':');
1454
+ if (colonIndex === -1)
1455
+ continue;
1456
+ const key = decl.slice(0, colonIndex).trim();
1457
+ const value = decl.slice(colonIndex + 1).trim();
1458
+ if (key && value) {
1459
+ // Preserve CSS custom properties (e.g., --ta, --my-variable) as-is
1460
+ if (key.startsWith('--')) {
1461
+ styleObj[key] = value;
1462
+ }
1463
+ else {
1464
+ // Convert kebab-case to camelCase (e.g., line-height -> lineHeight)
1465
+ const camelKey = key.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
1466
+ styleObj[camelKey] = value;
1467
+ }
1468
+ }
1469
+ }
1470
+ return styleObj;
1471
+ }
1472
+ /**
1473
+ * Converts a style object to JSX style expression string.
1474
+ * Example: { margin: 0, padding: 0, color: "#171717" } -> "{ margin: 0, padding: 0, color: \"#171717\" }"
1475
+ */
1476
+ function styleObjectToJSX(styleObj) {
1477
+ const entries = [];
1478
+ for (const [key, value] of Object.entries(styleObj)) {
1479
+ // Convert camelCase to camelCase (already in camelCase from cssStringToStyleObject)
1480
+ // Value might be a number string or a string that needs quotes
1481
+ let jsxValue;
1482
+ if (/^-?\d+(\.\d+)?$/.test(value.trim())) {
1483
+ // It's a number (including decimals)
1484
+ jsxValue = value.trim();
1485
+ }
1486
+ else {
1487
+ // It's a string, wrap in quotes and escape
1488
+ const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
1489
+ jsxValue = `"${escaped}"`;
1490
+ }
1491
+ // Quote keys that aren't valid JS identifiers (e.g., CSS custom properties like --ta)
1492
+ const jsxKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : `"${key}"`;
1493
+ entries.push(`${jsxKey}: ${jsxValue}`);
1494
+ }
1495
+ return `{ ${entries.join(', ')} }`;
1496
+ }
1497
+ /**
1498
+ * Escapes HTML/JSX special characters in text content for JSX.
1499
+ * This prevents characters like <, >, and & from being interpreted as JSX tags or entities.
1500
+ * Example: "< 18.5" -> "&lt; 18.5"
1501
+ */
1502
+ function escapeTextForJSX(text) {
1503
+ // Escape special characters that would break JSX/HTML
1504
+ // Order matters: escape & first to avoid double-escaping
1505
+ return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1506
+ }
1507
+ /**
1508
+ * Converts a value to a JSX expression string.
1509
+ * Handles arrays, objects, primitives, etc.
1510
+ */
1511
+ function valueToJSXExpression(value) {
1512
+ if (value === null || value === undefined) {
1513
+ return 'null';
1514
+ }
1515
+ if (typeof value === 'string') {
1516
+ return escapeStringForJS(value);
1517
+ }
1518
+ if (typeof value === 'number' || typeof value === 'boolean') {
1519
+ return String(value);
1520
+ }
1521
+ if (Array.isArray(value)) {
1522
+ if (value.length === 0) {
1523
+ return '[]';
1524
+ }
1525
+ const items = value.map((item) => valueToJSXExpression(item)).join(', ');
1526
+ return `[${items}]`;
1527
+ }
1528
+ if (typeof value === 'object') {
1529
+ const entries = [];
1530
+ for (const [key, val] of Object.entries(value)) {
1531
+ // Skip null/undefined values
1532
+ if (val === null || val === undefined)
1533
+ continue;
1534
+ // Convert key to valid JS identifier or string literal
1535
+ const jsKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : escapeStringForJS(key);
1536
+ entries.push(`${jsKey}: ${valueToJSXExpression(val)}`);
1537
+ }
1538
+ return `{ ${entries.join(', ')} }`;
1539
+ }
1540
+ // Fallback: convert to string
1541
+ return escapeStringForJS(String(value));
1542
+ }
1543
+ /**
1544
+ * Converts a string with {{variable}} syntax to a template literal expression.
1545
+ * Example: "{{first_name}}'s plan" -> "`{{first_name}}'s plan`"
1546
+ */
1547
+ function stringToTemplateLiteral(str) {
1548
+ // Escape backticks and backslashes for template literal
1549
+ const escaped = str.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\${/g, '\\${'); // Escape ${ to prevent interpolation
1550
+ return '`' + escaped + '`';
1551
+ }
1552
+ function escapeStringForJS(str) {
1553
+ // Check if string contains double quotes - if so, use template literals
1554
+ const hasDoubleQuotes = str.includes('"');
1555
+ if (hasDoubleQuotes) {
1556
+ // Use template literals (backticks) - escape backticks, backslashes, and ${ (template interpolation)
1557
+ let escaped = str
1558
+ .replace(/\\/g, '\\\\') // Escape backslashes first
1559
+ .replace(/\$\{/g, '\\${') // Escape ${ so literal ${{ ... }} (e.g. product_data) is preserved
1560
+ .replace(/`/g, '\\`'); // Escape backticks
1561
+ // Escape newlines
1562
+ escaped = escaped
1563
+ .replace(/\r\n/g, '\\n') // Windows line endings
1564
+ .replace(/\r/g, '\\n') // Old Mac line endings
1565
+ .replace(/\n/g, '\\n'); // Unix line endings
1566
+ return `\`${escaped}\``;
1567
+ }
1568
+ // No double quotes - use regular quote selection
1569
+ const doubleQuoteCount = (str.match(/"/g) || []).length;
1570
+ const singleQuoteCount = (str.match(/'/g) || []).length;
1571
+ // Escape backslashes first
1572
+ let escaped = str.replace(/\\/g, '\\\\');
1573
+ // Escape newlines
1574
+ escaped = escaped
1575
+ .replace(/\r\n/g, '\\n') // Windows line endings
1576
+ .replace(/\r/g, '\\n') // Old Mac line endings
1577
+ .replace(/\n/g, '\\n'); // Unix line endings
1578
+ // Use single quotes if there are more double quotes, otherwise use double quotes
1579
+ if (doubleQuoteCount > singleQuoteCount) {
1580
+ // Use single quotes and escape single quotes
1581
+ escaped = escaped.replace(/'/g, "\\'");
1582
+ return `'${escaped}'`;
1583
+ }
1584
+ else {
1585
+ // Use double quotes and escape double quotes
1586
+ escaped = escaped.replace(/"/g, '\\"');
1587
+ return `"${escaped}"`;
1588
+ }
1589
+ }
1590
+ /**
1591
+ * Generates config.json file from embeddable metadata.
1592
+ * This file controls page ordering and stores embeddable-level metadata.
1593
+ * Preserves the order of top-level properties from embeddable.json.
1594
+ */
1595
+ async function generateConfigFile(embeddable, embeddableId, opts) {
1596
+ const pullMetadata = opts?.pullMetadata;
1597
+ const preserve = opts?.preserve ?? false;
1598
+ try {
1599
+ const configPath = path.join('embeddables', embeddableId, 'config.json');
1600
+ // CLI-only fields: from pull (when branching/saving) or from existing config file.
1601
+ let preservedVersion;
1602
+ let preservedBranchId;
1603
+ let preservedBranchName;
1604
+ if (fs.existsSync(configPath)) {
1605
+ try {
1606
+ const existing = JSON.parse(fs.readFileSync(configPath, 'utf8'));
1607
+ if (typeof existing._version === 'number')
1608
+ preservedVersion = existing._version;
1609
+ if (typeof existing._branch_id === 'string' && existing._branch_id)
1610
+ preservedBranchId = existing._branch_id;
1611
+ if (typeof existing._branch_name === 'string' && existing._branch_name)
1612
+ preservedBranchName = existing._branch_name;
1613
+ }
1614
+ catch {
1615
+ /* ignore */
1616
+ }
1617
+ }
1618
+ // Preserve the order of top-level properties from embeddable
1619
+ const embeddableKeys = Object.keys(embeddable);
1620
+ // Extract page metadata and order (excluding components which are in TSX files)
1621
+ const pages = embeddable.pages.map((page) => {
1622
+ const pageMetadata = {
1623
+ key: page.key,
1624
+ };
1625
+ // Include page-level metadata (excluding components which are in TSX files)
1626
+ // Preserve property order from the page object recursively
1627
+ const pageKeys = Object.keys(page);
1628
+ const excludePageProps = new Set(['components']);
1629
+ for (const key of pageKeys) {
1630
+ if (!excludePageProps.has(key) && key !== 'key') {
1631
+ const value = page[key];
1632
+ if (value !== undefined && value !== null) {
1633
+ pageMetadata[key] = deepClonePreservingOrder(value);
1634
+ }
1635
+ }
1636
+ }
1637
+ // When --preserve: record component order for forward compile
1638
+ if (preserve && Array.isArray(page.components) && page.components.length > 0) {
1639
+ pageMetadata.components_order = page.components.map((c) => c.id);
1640
+ }
1641
+ return pageMetadata;
1642
+ });
1643
+ // Extract computedFields metadata (without code, as code is in JS files)
1644
+ // Preserve order recursively in metadata objects
1645
+ const computedFieldsMetadata = [];
1646
+ if (embeddable.computedFields && Array.isArray(embeddable.computedFields)) {
1647
+ for (const field of embeddable.computedFields) {
1648
+ const cloned = deepClonePreservingOrder(field);
1649
+ const { code, ...metadata } = cloned;
1650
+ computedFieldsMetadata.push(metadata);
1651
+ }
1652
+ }
1653
+ // Extract dataOutputs metadata (without code, as code is in JS files)
1654
+ // Preserve order recursively in metadata objects
1655
+ const dataOutputsMetadata = [];
1656
+ if (embeddable.dataOutputs && Array.isArray(embeddable.dataOutputs)) {
1657
+ for (const action of embeddable.dataOutputs) {
1658
+ const cloned = deepClonePreservingOrder(action);
1659
+ const { code, ...metadata } = cloned;
1660
+ dataOutputsMetadata.push(metadata);
1661
+ }
1662
+ }
1663
+ // Build config object preserving the order from embeddable
1664
+ const config = {};
1665
+ const excludeProps = new Set(['pages', 'styles', 'computedFields', 'dataOutputs', 'components']);
1666
+ // Iterate through embeddable keys in order to preserve property order
1667
+ for (const key of embeddableKeys) {
1668
+ if (key === 'pages') {
1669
+ // Replace pages with page metadata (without components)
1670
+ config.pages = pages;
1671
+ }
1672
+ else if (key === 'styles') {
1673
+ // Skip styles (they're in CSS files)
1674
+ continue;
1675
+ }
1676
+ else if (key === 'computedFields') {
1677
+ // Replace computedFields with metadata (without code)
1678
+ if (computedFieldsMetadata.length > 0) {
1679
+ config.computedFields = computedFieldsMetadata;
1680
+ }
1681
+ }
1682
+ else if (key === 'dataOutputs') {
1683
+ // Replace dataOutputs with metadata (without code)
1684
+ if (dataOutputsMetadata.length > 0) {
1685
+ config.dataOutputs = dataOutputsMetadata;
1686
+ }
1687
+ }
1688
+ else if (key === 'components') {
1689
+ // Skip components (they're in TSX files in global-components/)
1690
+ // When --preserve: record global component order for forward compile
1691
+ if (preserve && embeddable.components && Array.isArray(embeddable.components)) {
1692
+ config.components_order = embeddable.components.map((c) => c.id);
1693
+ }
1694
+ continue;
1695
+ }
1696
+ else {
1697
+ // Regular metadata property - preserve as-is, with order preserved recursively
1698
+ const value = embeddable[key];
1699
+ if (value !== undefined && value !== null) {
1700
+ config[key] = deepClonePreservingOrder(value);
1701
+ }
1702
+ }
1703
+ }
1704
+ // Ensure id is set if it wasn't already
1705
+ if (!config.id) {
1706
+ config.id = embeddableId;
1707
+ }
1708
+ // Restore CLI-only fields: prefer pullMetadata (passed by pull/branch) so branch is never lost.
1709
+ const versionToWrite = pullMetadata?.version ?? preservedVersion;
1710
+ if (versionToWrite !== undefined)
1711
+ config._version = versionToWrite;
1712
+ if (pullMetadata !== undefined) {
1713
+ if (pullMetadata.branchId !== undefined) {
1714
+ config._branch_id = pullMetadata.branchId;
1715
+ if (pullMetadata.branchName !== undefined)
1716
+ config._branch_name = pullMetadata.branchName;
1717
+ else
1718
+ delete config._branch_name;
1719
+ }
1720
+ else {
1721
+ delete config._branch_id;
1722
+ delete config._branch_name;
1723
+ }
1724
+ }
1725
+ else {
1726
+ if (preservedBranchId !== undefined)
1727
+ config._branch_id = preservedBranchId;
1728
+ if (preservedBranchName !== undefined)
1729
+ config._branch_name = preservedBranchName;
1730
+ }
1731
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
1732
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
1733
+ stdout.dim(`Generated ${configPath}`);
1734
+ }
1735
+ catch (error) {
1736
+ if (error instanceof Error) {
1737
+ throw new Error(`Config: ${error.message}`);
1738
+ }
1739
+ throw error;
1740
+ }
1741
+ }
1742
+ /**
1743
+ * Extracts computedFields to JS files in computed-fields/ folder.
1744
+ * Each computedField with code is saved as a separate JS file.
1745
+ */
1746
+ async function extractComputedFields(computedFields, embeddableId) {
1747
+ const computedFieldsDir = path.join('embeddables', embeddableId, 'computed-fields');
1748
+ fs.mkdirSync(computedFieldsDir, { recursive: true });
1749
+ for (const field of computedFields) {
1750
+ if (!field.code) {
1751
+ continue;
1752
+ }
1753
+ // Try using key or id as identifier
1754
+ const identifier = field.key || field.id;
1755
+ if (!identifier) {
1756
+ continue;
1757
+ }
1758
+ const fileName = `${sanitizeFileName(identifier)}.js`;
1759
+ const filePath = path.join(computedFieldsDir, fileName);
1760
+ // Write the code as-is (it should be valid JavaScript)
1761
+ fs.writeFileSync(filePath, field.code, 'utf8');
1762
+ stdout.dim(`Generated ${filePath}`);
1763
+ }
1764
+ }
1765
+ /**
1766
+ * Extracts dataOutputs (actions) to JS files in actions/ folder.
1767
+ * Each action with code is saved as a separate JS file.
1768
+ */
1769
+ async function extractDataOutputs(dataOutputs, embeddableId) {
1770
+ const actionsDir = path.join('embeddables', embeddableId, 'actions');
1771
+ fs.mkdirSync(actionsDir, { recursive: true });
1772
+ for (const action of dataOutputs) {
1773
+ if (!action.code) {
1774
+ continue;
1775
+ }
1776
+ // Use name as the filename, fallback to id if name doesn't exist
1777
+ const identifier = action.name || action.id;
1778
+ if (!identifier) {
1779
+ continue;
1780
+ }
1781
+ const fileName = `${sanitizeFileName(identifier)}.js`;
1782
+ const filePath = path.join(actionsDir, fileName);
1783
+ // Write the code as-is (it should be valid JavaScript)
1784
+ fs.writeFileSync(filePath, action.code, 'utf8');
1785
+ stdout.dim(`Generated ${filePath}`);
1786
+ }
1787
+ }
1788
+ /**
1789
+ * Helper function to recursively find the _location of a component.
1790
+ * parent_id takes priority over _location so that child components are always
1791
+ * grouped with their parent, even if the child has its own _location set.
1792
+ * Returns undefined if no location is found in the component or any parent.
1793
+ */
1794
+ function findComponentLocation(component, componentMap) {
1795
+ if (component.parent_id) {
1796
+ const parent = componentMap.get(component.parent_id);
1797
+ if (parent) {
1798
+ return findComponentLocation(parent, componentMap);
1799
+ }
1800
+ }
1801
+ return component._location;
1802
+ }
1803
+ /**
1804
+ * Extracts global components to TSX files in global-components/ folder.
1805
+ * Components are grouped by their _location (for root components without parent_id)
1806
+ * or by their parent's location (for child components).
1807
+ * File naming: <location>.location.tsx (e.g., before_page.location.tsx)
1808
+ */
1809
+ async function extractGlobalComponents(components, embeddableId, fix) {
1810
+ const globalComponentsDir = path.join('embeddables', embeddableId, 'global-components');
1811
+ fs.mkdirSync(globalComponentsDir, { recursive: true });
1812
+ if (components.length === 0) {
1813
+ return;
1814
+ }
1815
+ // Create a map of id -> component for location resolution
1816
+ const componentMap = new Map();
1817
+ for (const comp of components) {
1818
+ componentMap.set(comp.id, comp);
1819
+ }
1820
+ // Group components by location
1821
+ // Components without parent_id must have _location and go in their location file
1822
+ // Components with parent_id inherit location from parent and go in parent's location file
1823
+ const componentsByLocation = new Map();
1824
+ for (const component of components) {
1825
+ const location = findComponentLocation(component, componentMap);
1826
+ // Components without parent_id must have _location
1827
+ if (!component.parent_id && !location) {
1828
+ if (fix) {
1829
+ stdout.warn(`Removed global component "${component.id}" (key: "${component.key}") – missing _location (no parent_id).`);
1830
+ continue;
1831
+ }
1832
+ throw new Error(`Global component "${component.id}" (key: "${component.key}") must have _location since it has no parent_id.`);
1833
+ }
1834
+ if (location) {
1835
+ if (!componentsByLocation.has(location)) {
1836
+ componentsByLocation.set(location, []);
1837
+ }
1838
+ componentsByLocation.get(location).push(component);
1839
+ }
1840
+ }
1841
+ // Generate TSX file for each location
1842
+ for (const [location, locationComponents] of componentsByLocation) {
1843
+ const fileName = `${location}.location.tsx`;
1844
+ const filePath = path.join(globalComponentsDir, fileName);
1845
+ // Build tree from flat component list (similar to buildTree for pages)
1846
+ const tree = buildTreeForGlobalComponents(locationComponents, fix);
1847
+ // Generate TSX code
1848
+ const tsx = generateGlobalComponentTSX(locationComponents, tree);
1849
+ // Write to file
1850
+ fs.writeFileSync(filePath, tsx, 'utf8');
1851
+ stdout.dim(`Generated ${filePath}`);
1852
+ }
1853
+ }
1854
+ /**
1855
+ * Builds a component tree from a flat list of components (similar to buildTree for pages).
1856
+ * Components whose parent_id points outside this location group are treated as roots
1857
+ * (cross-location roots) so they're not orphaned.
1858
+ */
1859
+ function buildTreeForGlobalComponents(components, fix) {
1860
+ // Filter out components without type and ignored component types
1861
+ let filteredComponents = components.filter((comp) => {
1862
+ if (!comp.type) {
1863
+ if (fix) {
1864
+ stdout.warn(`Removed global component (id: ${comp.id}, key: ${comp.key}) – missing type property.`);
1865
+ return false;
1866
+ }
1867
+ throw new Error(`Global component (id: ${comp.id}, key: ${comp.key}) is missing required type property.`);
1868
+ }
1869
+ return !IGNORED_COMPONENT_TYPES.has(comp.type);
1870
+ });
1871
+ // Filter out components missing required props
1872
+ filteredComponents = filteredComponents.filter((comp) => {
1873
+ return hasRequiredProps(comp, fix);
1874
+ });
1875
+ // Create a map of id -> component
1876
+ const componentMap = new Map();
1877
+ for (const comp of filteredComponents) {
1878
+ componentMap.set(comp.id, comp);
1879
+ }
1880
+ // Find root components: no parent_id, OR parent_id points to a component
1881
+ // not in this location group (cross-location reference)
1882
+ const roots = [];
1883
+ for (const comp of filteredComponents) {
1884
+ if (!comp.parent_id || !componentMap.has(comp.parent_id)) {
1885
+ roots.push(comp);
1886
+ }
1887
+ }
1888
+ // Allow empty locations - return null if no components
1889
+ if (roots.length === 0) {
1890
+ return null;
1891
+ }
1892
+ // Build tree recursively for each root
1893
+ const rootNodes = roots.map((root) => buildNode(root, componentMap, '', fix));
1894
+ // Return single node or array of nodes
1895
+ return rootNodes.length === 1 ? rootNodes[0] : rootNodes;
1896
+ }
1897
+ /**
1898
+ * Generates TSX code for global components (similar to generateTSX for pages).
1899
+ */
1900
+ function generateGlobalComponentTSX(components, tree) {
1901
+ const imports = tree ? collectImports(tree) : [];
1902
+ const hasButtons = tree ? hasButtonConstants(tree) : false;
1903
+ const { constants, nameMap } = tree
1904
+ ? generateButtonConstants(tree)
1905
+ : { constants: '', nameMap: new Map() };
1906
+ const { declarations: validationFunctions, validationFunctionNameMap, identifierToGeneratedName, } = tree
1907
+ ? generateCustomValidationFunctions(tree)
1908
+ : {
1909
+ declarations: '',
1910
+ validationFunctionNameMap: new Map(),
1911
+ identifierToGeneratedName: new Map(),
1912
+ };
1913
+ const jsx = generateJSXFromTree(tree, 2, undefined, nameMap, validationFunctionNameMap, identifierToGeneratedName);
1914
+ return `"use client";
1915
+
1916
+ ${imports.map((imp) => `import { ${imp} } from "@embeddables/cli/components";`).join('\n')}
1917
+ ${hasButtons
1918
+ ? `
1919
+ import type { OptionSelectorButtonWithLanguages as OptionSelectorButton } from "@embeddables/cli/components";`
1920
+ : ''}
1921
+
1922
+ export default function GlobalComponents() {
1923
+ ${constants}${validationFunctions}
1924
+ return (
1925
+ ${jsx}
1926
+ );
1927
+ }
1928
+ `;
1929
+ }
1930
+ /**
1931
+ * Sanitizes a string to be safe for use as a filename.
1932
+ */
1933
+ export function sanitizeFileName(str) {
1934
+ return str
1935
+ .replace(/[^a-zA-Z0-9_-]/g, '_')
1936
+ .replace(/^_+|_+$/g, '')
1937
+ .replace(/_{2,}/g, '_');
1938
+ }