@embeddables/cli 0.7.11 → 0.7.14

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