@embeddables/cli 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) 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/command-history.d.ts +13 -0
  8. package/dist/command-history.d.ts.map +1 -0
  9. package/dist/command-history.js +34 -0
  10. package/dist/commands/branch.d.ts +4 -0
  11. package/dist/commands/branch.d.ts.map +1 -0
  12. package/dist/commands/branch.js +67 -0
  13. package/dist/commands/build-workbench.d.ts +5 -0
  14. package/dist/commands/build-workbench.d.ts.map +1 -0
  15. package/dist/commands/build-workbench.js +116 -0
  16. package/dist/commands/build.d.ts +8 -0
  17. package/dist/commands/build.d.ts.map +1 -0
  18. package/dist/commands/build.js +60 -0
  19. package/dist/commands/builder-open.d.ts +4 -0
  20. package/dist/commands/builder-open.d.ts.map +1 -0
  21. package/dist/commands/builder-open.js +74 -0
  22. package/dist/commands/dev.d.ts +12 -0
  23. package/dist/commands/dev.d.ts.map +1 -0
  24. package/dist/commands/dev.js +226 -0
  25. package/dist/commands/diff.d.ts +76 -0
  26. package/dist/commands/diff.d.ts.map +1 -0
  27. package/dist/commands/diff.js +653 -0
  28. package/dist/commands/experiments-connect.d.ts +6 -0
  29. package/dist/commands/experiments-connect.d.ts.map +1 -0
  30. package/dist/commands/experiments-connect.js +140 -0
  31. package/dist/commands/feedback.d.ts +29 -0
  32. package/dist/commands/feedback.d.ts.map +1 -0
  33. package/dist/commands/feedback.js +267 -0
  34. package/dist/commands/init.d.ts +5 -0
  35. package/dist/commands/init.d.ts.map +1 -0
  36. package/dist/commands/init.js +384 -0
  37. package/dist/commands/inspect.d.ts +9 -0
  38. package/dist/commands/inspect.d.ts.map +1 -0
  39. package/dist/commands/inspect.js +293 -0
  40. package/dist/commands/login.d.ts +2 -0
  41. package/dist/commands/login.d.ts.map +1 -0
  42. package/dist/commands/login.js +117 -0
  43. package/dist/commands/logout.d.ts +2 -0
  44. package/dist/commands/logout.d.ts.map +1 -0
  45. package/dist/commands/logout.js +19 -0
  46. package/dist/commands/pull.d.ts +16 -0
  47. package/dist/commands/pull.d.ts.map +1 -0
  48. package/dist/commands/pull.js +395 -0
  49. package/dist/commands/save.d.ts +30 -0
  50. package/dist/commands/save.d.ts.map +1 -0
  51. package/dist/commands/save.js +597 -0
  52. package/dist/commands/upgrade.d.ts +2 -0
  53. package/dist/commands/upgrade.d.ts.map +1 -0
  54. package/dist/commands/upgrade.js +50 -0
  55. package/dist/compiler/errors.d.ts +20 -0
  56. package/dist/compiler/errors.d.ts.map +1 -0
  57. package/dist/compiler/errors.js +35 -0
  58. package/dist/compiler/evalStatic.d.ts +3 -0
  59. package/dist/compiler/evalStatic.d.ts.map +1 -0
  60. package/dist/compiler/evalStatic.js +57 -0
  61. package/dist/compiler/flatten.js +1 -0
  62. package/dist/compiler/helpers/duplicateIds.d.ts +9 -0
  63. package/dist/compiler/helpers/duplicateIds.d.ts.map +1 -0
  64. package/dist/compiler/helpers/duplicateIds.js +71 -0
  65. package/dist/compiler/helpers/numericLeadingKeys.d.ts +8 -0
  66. package/dist/compiler/helpers/numericLeadingKeys.d.ts.map +1 -0
  67. package/dist/compiler/helpers/numericLeadingKeys.js +17 -0
  68. package/dist/compiler/index.d.ts +18 -0
  69. package/dist/compiler/index.d.ts.map +1 -0
  70. package/dist/compiler/index.js +1272 -0
  71. package/dist/compiler/parsePage.d.ts +15 -0
  72. package/dist/compiler/parsePage.d.ts.map +1 -0
  73. package/dist/compiler/parsePage.js +654 -0
  74. package/dist/compiler/registry.d.ts +4 -0
  75. package/dist/compiler/registry.d.ts.map +1 -0
  76. package/dist/compiler/registry.js +44 -0
  77. package/dist/compiler/reverse.d.ts +23 -0
  78. package/dist/compiler/reverse.d.ts.map +1 -0
  79. package/dist/compiler/reverse.js +1920 -0
  80. package/dist/compiler/types.d.ts +21 -0
  81. package/dist/compiler/types.d.ts.map +1 -0
  82. package/dist/compiler/types.js +1 -0
  83. package/dist/components/index.d.ts +21 -0
  84. package/dist/components/index.d.ts.map +1 -0
  85. package/dist/components/index.js +21 -0
  86. package/dist/components/primitives/BaseComponent.d.ts +33 -0
  87. package/dist/components/primitives/BaseComponent.d.ts.map +1 -0
  88. package/dist/components/primitives/BaseComponent.js +26 -0
  89. package/dist/components/primitives/BookMeeting.d.ts +18 -0
  90. package/dist/components/primitives/BookMeeting.d.ts.map +1 -0
  91. package/dist/components/primitives/BookMeeting.js +5 -0
  92. package/dist/components/primitives/Chart.d.ts +41 -0
  93. package/dist/components/primitives/Chart.d.ts.map +1 -0
  94. package/dist/components/primitives/Chart.js +5 -0
  95. package/dist/components/primitives/Container.d.ts +8 -0
  96. package/dist/components/primitives/Container.d.ts.map +1 -0
  97. package/dist/components/primitives/Container.js +5 -0
  98. package/dist/components/primitives/CustomButton.d.ts +37 -0
  99. package/dist/components/primitives/CustomButton.d.ts.map +1 -0
  100. package/dist/components/primitives/CustomButton.js +10 -0
  101. package/dist/components/primitives/CustomHTML.d.ts +8 -0
  102. package/dist/components/primitives/CustomHTML.d.ts.map +1 -0
  103. package/dist/components/primitives/CustomHTML.js +5 -0
  104. package/dist/components/primitives/FileUpload.d.ts +18 -0
  105. package/dist/components/primitives/FileUpload.d.ts.map +1 -0
  106. package/dist/components/primitives/FileUpload.js +16 -0
  107. package/dist/components/primitives/InputBox.d.ts +34 -0
  108. package/dist/components/primitives/InputBox.d.ts.map +1 -0
  109. package/dist/components/primitives/InputBox.js +25 -0
  110. package/dist/components/primitives/Lottie.d.ts +11 -0
  111. package/dist/components/primitives/Lottie.d.ts.map +1 -0
  112. package/dist/components/primitives/Lottie.js +5 -0
  113. package/dist/components/primitives/MediaEmbed.d.ts +13 -0
  114. package/dist/components/primitives/MediaEmbed.d.ts.map +1 -0
  115. package/dist/components/primitives/MediaEmbed.js +6 -0
  116. package/dist/components/primitives/MediaImage.d.ts +8 -0
  117. package/dist/components/primitives/MediaImage.d.ts.map +1 -0
  118. package/dist/components/primitives/MediaImage.js +5 -0
  119. package/dist/components/primitives/OptionSelector.d.ts +38 -0
  120. package/dist/components/primitives/OptionSelector.d.ts.map +1 -0
  121. package/dist/components/primitives/OptionSelector.js +8 -0
  122. package/dist/components/primitives/PaypalCheckout.d.ts +25 -0
  123. package/dist/components/primitives/PaypalCheckout.d.ts.map +1 -0
  124. package/dist/components/primitives/PaypalCheckout.js +5 -0
  125. package/dist/components/primitives/PlainText.d.ts +6 -0
  126. package/dist/components/primitives/PlainText.d.ts.map +1 -0
  127. package/dist/components/primitives/PlainText.js +5 -0
  128. package/dist/components/primitives/ProgressBar.d.ts +15 -0
  129. package/dist/components/primitives/ProgressBar.d.ts.map +1 -0
  130. package/dist/components/primitives/ProgressBar.js +5 -0
  131. package/dist/components/primitives/RichText.d.ts +6 -0
  132. package/dist/components/primitives/RichText.d.ts.map +1 -0
  133. package/dist/components/primitives/RichText.js +5 -0
  134. package/dist/components/primitives/RichTextMarkdown.d.ts +6 -0
  135. package/dist/components/primitives/RichTextMarkdown.d.ts.map +1 -0
  136. package/dist/components/primitives/RichTextMarkdown.js +5 -0
  137. package/dist/components/primitives/Rive.d.ts +16 -0
  138. package/dist/components/primitives/Rive.d.ts.map +1 -0
  139. package/dist/components/primitives/Rive.js +8 -0
  140. package/dist/components/primitives/StripeCheckout.d.ts +52 -0
  141. package/dist/components/primitives/StripeCheckout.d.ts.map +1 -0
  142. package/dist/components/primitives/StripeCheckout.js +5 -0
  143. package/dist/components/primitives/StripeCheckout2.d.ts +30 -0
  144. package/dist/components/primitives/StripeCheckout2.d.ts.map +1 -0
  145. package/dist/components/primitives/StripeCheckout2.js +7 -0
  146. package/dist/config/index.d.ts +23 -0
  147. package/dist/config/index.d.ts.map +1 -0
  148. package/dist/config/index.js +42 -0
  149. package/dist/constants.d.ts +9 -0
  150. package/dist/constants.d.ts.map +1 -0
  151. package/dist/constants.js +9 -0
  152. package/dist/helpers/TEMP helpers file.d.ts +1 -0
  153. package/dist/helpers/TEMP helpers file.d.ts.map +1 -0
  154. package/dist/helpers/TEMP helpers file.js +1 -0
  155. package/dist/helpers/dates.d.ts +5 -0
  156. package/dist/helpers/dates.d.ts.map +1 -0
  157. package/dist/helpers/dates.js +7 -0
  158. package/dist/helpers/json.d.ts +47 -0
  159. package/dist/helpers/json.d.ts.map +1 -0
  160. package/dist/helpers/json.js +622 -0
  161. package/dist/helpers/prompt.d.ts +15 -0
  162. package/dist/helpers/prompt.d.ts.map +1 -0
  163. package/dist/helpers/prompt.js +35 -0
  164. package/dist/helpers/utils.d.ts +13 -0
  165. package/dist/helpers/utils.d.ts.map +1 -0
  166. package/dist/helpers/utils.js +28 -0
  167. package/dist/logger.d.ts +11 -0
  168. package/dist/logger.d.ts.map +1 -0
  169. package/dist/logger.js +21 -0
  170. package/dist/patches/prompts-escape.d.ts +14 -0
  171. package/dist/patches/prompts-escape.d.ts.map +1 -0
  172. package/dist/patches/prompts-escape.js +23 -0
  173. package/dist/prompts/branches.d.ts +20 -0
  174. package/dist/prompts/branches.d.ts.map +1 -0
  175. package/dist/prompts/branches.js +86 -0
  176. package/dist/prompts/embeddables.d.ts +43 -0
  177. package/dist/prompts/embeddables.d.ts.map +1 -0
  178. package/dist/prompts/embeddables.js +200 -0
  179. package/dist/prompts/experiments.d.ts +28 -0
  180. package/dist/prompts/experiments.d.ts.map +1 -0
  181. package/dist/prompts/experiments.js +89 -0
  182. package/dist/prompts/index.d.ts +11 -0
  183. package/dist/prompts/index.d.ts.map +1 -0
  184. package/dist/prompts/index.js +6 -0
  185. package/dist/prompts/projects.d.ts +22 -0
  186. package/dist/prompts/projects.d.ts.map +1 -0
  187. package/dist/prompts/projects.js +92 -0
  188. package/dist/prompts/versions.d.ts +18 -0
  189. package/dist/prompts/versions.d.ts.map +1 -0
  190. package/dist/prompts/versions.js +95 -0
  191. package/dist/proxy/injectApiInterceptor.d.ts +6 -0
  192. package/dist/proxy/injectApiInterceptor.d.ts.map +1 -0
  193. package/dist/proxy/injectApiInterceptor.js +66 -0
  194. package/dist/proxy/injectReload.d.ts +2 -0
  195. package/dist/proxy/injectReload.d.ts.map +1 -0
  196. package/dist/proxy/injectReload.js +14 -0
  197. package/dist/proxy/injectWorkbench.d.ts +5 -0
  198. package/dist/proxy/injectWorkbench.d.ts.map +1 -0
  199. package/dist/proxy/injectWorkbench.js +22 -0
  200. package/dist/proxy/server.d.ts +11 -0
  201. package/dist/proxy/server.d.ts.map +1 -0
  202. package/dist/proxy/server.js +304 -0
  203. package/dist/proxy/sse.d.ts +5 -0
  204. package/dist/proxy/sse.d.ts.map +1 -0
  205. package/dist/proxy/sse.js +17 -0
  206. package/dist/sentry-context.d.ts +48 -0
  207. package/dist/sentry-context.d.ts.map +1 -0
  208. package/dist/sentry-context.js +156 -0
  209. package/dist/stdout.d.ts +61 -0
  210. package/dist/stdout.d.ts.map +1 -0
  211. package/dist/stdout.js +163 -0
  212. package/dist/types-builder.d.ts +800 -0
  213. package/dist/types-builder.d.ts.map +1 -0
  214. package/dist/types-builder.js +20 -0
  215. package/dist/workbench/ActionsPanel.d.ts +6 -0
  216. package/dist/workbench/ActionsPanel.d.ts.map +1 -0
  217. package/dist/workbench/ActionsPanel.js +47 -0
  218. package/dist/workbench/AutofillPanel.d.ts +6 -0
  219. package/dist/workbench/AutofillPanel.d.ts.map +1 -0
  220. package/dist/workbench/AutofillPanel.js +543 -0
  221. package/dist/workbench/ComputedFieldsPanel.d.ts +6 -0
  222. package/dist/workbench/ComputedFieldsPanel.d.ts.map +1 -0
  223. package/dist/workbench/ComputedFieldsPanel.js +31 -0
  224. package/dist/workbench/ExperimentsPanel.d.ts +6 -0
  225. package/dist/workbench/ExperimentsPanel.d.ts.map +1 -0
  226. package/dist/workbench/ExperimentsPanel.js +182 -0
  227. package/dist/workbench/FieldEditorPanel.d.ts +9 -0
  228. package/dist/workbench/FieldEditorPanel.d.ts.map +1 -0
  229. package/dist/workbench/FieldEditorPanel.js +650 -0
  230. package/dist/workbench/InspectorPanel.d.ts +6 -0
  231. package/dist/workbench/InspectorPanel.d.ts.map +1 -0
  232. package/dist/workbench/InspectorPanel.js +341 -0
  233. package/dist/workbench/PageNavigator.d.ts +6 -0
  234. package/dist/workbench/PageNavigator.d.ts.map +1 -0
  235. package/dist/workbench/PageNavigator.js +123 -0
  236. package/dist/workbench/SchemaPanel.d.ts +6 -0
  237. package/dist/workbench/SchemaPanel.d.ts.map +1 -0
  238. package/dist/workbench/SchemaPanel.js +222 -0
  239. package/dist/workbench/UserDataPanel.d.ts +6 -0
  240. package/dist/workbench/UserDataPanel.d.ts.map +1 -0
  241. package/dist/workbench/UserDataPanel.js +350 -0
  242. package/dist/workbench/WorkbenchApp.d.ts +7 -0
  243. package/dist/workbench/WorkbenchApp.d.ts.map +1 -0
  244. package/dist/workbench/WorkbenchApp.js +193 -0
  245. package/dist/workbench/cloudflare-worker/README.md +31 -0
  246. package/dist/workbench/cloudflare-worker/public/workbench.css +1614 -0
  247. package/dist/workbench/cloudflare-worker/public/workbench.js +77 -0
  248. package/dist/workbench/cloudflare-worker/worker.js +40 -0
  249. package/dist/workbench/cloudflare-worker/wrangler.toml +10 -0
  250. package/dist/workbench/index.d.ts +10 -0
  251. package/dist/workbench/index.d.ts.map +1 -0
  252. package/dist/workbench/index.js +44 -0
  253. package/dist/workbench/workbench.css +1614 -0
  254. package/dist/workbench/workbench.js +77 -0
  255. package/package.json +1 -1
@@ -0,0 +1,1272 @@
1
+ import fg from 'fast-glob';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import * as stdout from '../stdout.js';
5
+ import { parsePageFromFile } from './parsePage.js';
6
+ import { CompileError } from './errors.js';
7
+ import CSSJSON from 'cssjson';
8
+ import { sanitizeFileName } from './reverse.js';
9
+ import { generateRandomIdByType } from './helpers/duplicateIds.js';
10
+ import { normalizeKeyIfStartsWithDigit, keyOrIdStartsWithDigit, } from './helpers/numericLeadingKeys.js';
11
+ import { parse } from '@babel/parser';
12
+ import traverseImport from '@babel/traverse';
13
+ import { ALLOWED_PRIMITIVES } from './registry.js';
14
+ const traverse = (traverseImport.default ?? traverseImport);
15
+ /**
16
+ * Deep clones an object or array while preserving the order of all properties
17
+ * at all nesting levels. Arrays preserve element order (already guaranteed),
18
+ * and objects preserve property insertion order.
19
+ */
20
+ function deepClonePreservingOrder(value) {
21
+ if (value === null || value === undefined) {
22
+ return value;
23
+ }
24
+ if (Array.isArray(value)) {
25
+ // For arrays, recursively clone each element
26
+ return value.map((item) => deepClonePreservingOrder(item));
27
+ }
28
+ if (typeof value === 'object') {
29
+ // For objects, preserve property order by iterating through keys
30
+ const cloned = {};
31
+ const keys = Object.keys(value);
32
+ for (const key of keys) {
33
+ cloned[key] = deepClonePreservingOrder(value[key]);
34
+ }
35
+ return cloned;
36
+ }
37
+ // Primitive values (string, number, boolean) are returned as-is
38
+ return value;
39
+ }
40
+ /**
41
+ * Returns a copy of newVal with object key order (recursively) matching oldVal
42
+ * where possible. Keeps existing keys in old order; appends new keys at the end.
43
+ * Arrays are not reordered; we recurse into elements by index for nested objects.
44
+ * Used so embeddable.json diffs show only actual changes, not reordering noise.
45
+ */
46
+ export function reorderObjectKeysToMatch(oldVal, newVal) {
47
+ if (newVal === null || newVal === undefined) {
48
+ return newVal;
49
+ }
50
+ if (Array.isArray(newVal)) {
51
+ const oldArr = Array.isArray(oldVal) ? oldVal : [];
52
+ // If array contains objects with 'id' properties, match by id instead of index
53
+ // This handles cases where components are removed (e.g., invalid parent_id)
54
+ if (newVal.length > 0 &&
55
+ typeof newVal[0] === 'object' &&
56
+ newVal[0] !== null &&
57
+ 'id' in newVal[0]) {
58
+ // Build a map of old items by id for efficient lookup
59
+ const oldById = new Map();
60
+ for (const oldItem of oldArr) {
61
+ if (typeof oldItem === 'object' && oldItem !== null && 'id' in oldItem) {
62
+ oldById.set(oldItem.id, oldItem);
63
+ }
64
+ }
65
+ // Match new items with old items by id, fallback to index if id not found
66
+ return newVal.map((newItem, i) => {
67
+ if (typeof newItem === 'object' && newItem !== null && 'id' in newItem) {
68
+ const id = newItem.id;
69
+ const oldItem = oldById.get(id);
70
+ if (oldItem !== undefined) {
71
+ return reorderObjectKeysToMatch(oldItem, newItem);
72
+ }
73
+ }
74
+ // Fallback to index-based matching if no id or id not found
75
+ return reorderObjectKeysToMatch(oldArr[i], newItem);
76
+ });
77
+ }
78
+ // For arrays without id-based objects, use index-based matching
79
+ return newVal.map((item, i) => reorderObjectKeysToMatch(oldArr[i], item));
80
+ }
81
+ if (typeof newVal === 'object' && newVal !== null) {
82
+ const oldObj = oldVal !== null && typeof oldVal === 'object' && !Array.isArray(oldVal)
83
+ ? oldVal
84
+ : {};
85
+ const newObj = newVal;
86
+ const oldKeys = Object.keys(oldObj);
87
+ const newKeysSet = new Set(Object.keys(newObj));
88
+ const orderedKeys = [
89
+ ...oldKeys.filter((k) => newKeysSet.has(k)),
90
+ ...Object.keys(newObj).filter((k) => !oldKeys.includes(k)),
91
+ ];
92
+ const result = {};
93
+ for (const k of orderedKeys) {
94
+ result[k] = reorderObjectKeysToMatch(oldObj[k], newObj[k]);
95
+ }
96
+ return result;
97
+ }
98
+ return newVal;
99
+ }
100
+ /**
101
+ * Orders an array of items with an `id` property to match the given id list.
102
+ * Items whose id is in the list appear first in that order; items not in the list appear at the end.
103
+ */
104
+ function orderByIdList(items, idList) {
105
+ if (!Array.isArray(idList) || idList.length === 0)
106
+ return items;
107
+ const byId = new Map();
108
+ for (const item of items) {
109
+ if (item.id !== undefined && item.id !== null)
110
+ byId.set(item.id, item);
111
+ }
112
+ const ordered = [];
113
+ const orderedIds = new Set(idList);
114
+ for (const id of idList) {
115
+ const item = byId.get(id);
116
+ if (item)
117
+ ordered.push(item);
118
+ }
119
+ for (const item of items) {
120
+ if (item.id === undefined || item.id === null || !orderedIds.has(item.id)) {
121
+ ordered.push(item);
122
+ }
123
+ }
124
+ return ordered;
125
+ }
126
+ export async function compileAllPages(opts) {
127
+ const files = await fg(opts.pagesGlob, { dot: false });
128
+ if (files.length === 0) {
129
+ throw new CompileError(`No pages found for glob: ${opts.pagesGlob}`);
130
+ }
131
+ // First pass: parse and collect lint issues (duplicate IDs, keys/IDs starting with a digit)
132
+ const idOccurrences = new Map();
133
+ const filePages = [];
134
+ for (const file of files) {
135
+ const code = fs.readFileSync(file, 'utf8');
136
+ const pageKey = derivePageKey(file, opts.pageKeyFrom);
137
+ const page = parsePageFromFile({ code, filePath: file, pageKey });
138
+ for (let compIndex = 0; compIndex < page.components.length; compIndex++) {
139
+ const component = page.components[compIndex];
140
+ const compId = component.id;
141
+ if (!idOccurrences.has(compId)) {
142
+ idOccurrences.set(compId, []);
143
+ }
144
+ idOccurrences.get(compId).push({
145
+ file,
146
+ pageKey,
147
+ componentType: component.type,
148
+ componentIndex: compIndex,
149
+ id: compId,
150
+ });
151
+ if (component.type === 'OptionSelector' && Array.isArray(component.buttons)) {
152
+ for (let btnIndex = 0; btnIndex < component.buttons.length; btnIndex++) {
153
+ const button = component.buttons[btnIndex];
154
+ if (button?.id) {
155
+ const buttonId = button.id;
156
+ if (!idOccurrences.has(buttonId)) {
157
+ idOccurrences.set(buttonId, []);
158
+ }
159
+ idOccurrences.get(buttonId).push({
160
+ file,
161
+ pageKey,
162
+ componentType: component.type,
163
+ componentIndex: compIndex,
164
+ buttonIndex: btnIndex,
165
+ id: buttonId,
166
+ });
167
+ }
168
+ }
169
+ }
170
+ }
171
+ filePages.push({ file, pageKey, page });
172
+ }
173
+ // Collect duplicate ID fixes (do not apply yet)
174
+ const idFixes = new Map();
175
+ const allIds = new Set();
176
+ for (const occurrences of idOccurrences.values()) {
177
+ for (const occ of occurrences) {
178
+ allIds.add(occ.id);
179
+ }
180
+ }
181
+ for (const [id, occurrences] of idOccurrences.entries()) {
182
+ if (occurrences.length > 1) {
183
+ for (let i = 1; i < occurrences.length; i++) {
184
+ const occ = occurrences[i];
185
+ const fixKey = occ.buttonIndex !== undefined
186
+ ? `${occ.file}:${occ.componentIndex}:${occ.buttonIndex}`
187
+ : `${occ.file}:${occ.componentIndex}`;
188
+ const isOptionButton = occ.buttonIndex !== undefined;
189
+ const newId = generateRandomIdByType(occ.componentType, isOptionButton, allIds, idFixes.values());
190
+ allIds.add(newId);
191
+ idFixes.set(fixKey, newId);
192
+ }
193
+ }
194
+ }
195
+ // Collect numeric-leading key/id fixes (component key, component id, button key, button id)
196
+ const numericLeadingFixes = [];
197
+ for (const { file, pageKey, page } of filePages) {
198
+ for (let compIndex = 0; compIndex < page.components.length; compIndex++) {
199
+ const comp = page.components[compIndex];
200
+ if (keyOrIdStartsWithDigit(comp.key)) {
201
+ const newKey = normalizeKeyIfStartsWithDigit(comp.key, pageKey);
202
+ if (newKey)
203
+ numericLeadingFixes.push({
204
+ file,
205
+ componentIndex: compIndex,
206
+ kind: 'componentKey',
207
+ oldValue: comp.key,
208
+ newValue: newKey,
209
+ });
210
+ }
211
+ if (keyOrIdStartsWithDigit(comp.id)) {
212
+ const newId = normalizeKeyIfStartsWithDigit(comp.id, 'comp');
213
+ if (newId)
214
+ numericLeadingFixes.push({
215
+ file,
216
+ componentIndex: compIndex,
217
+ kind: 'componentId',
218
+ oldValue: comp.id,
219
+ newValue: newId,
220
+ });
221
+ }
222
+ if (comp.type === 'OptionSelector' && Array.isArray(comp.buttons)) {
223
+ const prefix = comp.key ?? 'option';
224
+ for (let btnIndex = 0; btnIndex < comp.buttons.length; btnIndex++) {
225
+ const btn = comp.buttons[btnIndex];
226
+ if (btn && keyOrIdStartsWithDigit(btn.key)) {
227
+ const newKey = normalizeKeyIfStartsWithDigit(btn.key, prefix);
228
+ if (newKey)
229
+ numericLeadingFixes.push({
230
+ file,
231
+ componentIndex: compIndex,
232
+ buttonIndex: btnIndex,
233
+ kind: 'buttonKey',
234
+ oldValue: btn.key,
235
+ newValue: newKey,
236
+ });
237
+ }
238
+ if (btn && keyOrIdStartsWithDigit(btn.id)) {
239
+ const safeKey = normalizeKeyIfStartsWithDigit(btn.key, prefix);
240
+ const newId = btn.key != null && btn.id === `option_${btn.key}`
241
+ ? safeKey != null
242
+ ? `option_${safeKey}`
243
+ : normalizeKeyIfStartsWithDigit(btn.id, 'option')
244
+ : normalizeKeyIfStartsWithDigit(btn.id, 'option');
245
+ if (newId)
246
+ numericLeadingFixes.push({
247
+ file,
248
+ componentIndex: compIndex,
249
+ buttonIndex: btnIndex,
250
+ kind: 'buttonId',
251
+ oldValue: btn.id,
252
+ newValue: newId,
253
+ });
254
+ }
255
+ }
256
+ }
257
+ }
258
+ }
259
+ const totalLintIssues = idFixes.size + numericLeadingFixes.length;
260
+ let shouldApplyFixes = false;
261
+ if (totalLintIssues > 0) {
262
+ // Report all issues
263
+ if (idFixes.size > 0) {
264
+ stdout.warn(`Found ${idFixes.size} duplicate ID(s). Keys and IDs must be unique across the embeddable.`);
265
+ for (const [fixKey, newId] of idFixes) {
266
+ const parts = fixKey.split(':');
267
+ const file = parts[0];
268
+ const compIdx = parts[1];
269
+ const btnIdx = parts[2];
270
+ const loc = btnIdx !== undefined ? `component ${compIdx}, button ${btnIdx}` : `component ${compIdx}`;
271
+ stdout.warn(` → ${file}: ${loc} → "${newId}"`);
272
+ }
273
+ }
274
+ if (numericLeadingFixes.length > 0) {
275
+ stdout.warn(`Found ${numericLeadingFixes.length} key(s)/ID(s) that start with a number. Use a prefix (e.g. range_1_to_2_weeks).`);
276
+ for (const f of numericLeadingFixes) {
277
+ const loc = f.buttonIndex !== undefined
278
+ ? `component ${f.componentIndex}, button ${f.buttonIndex} (${f.kind})`
279
+ : `component ${f.componentIndex} (${f.kind})`;
280
+ stdout.warn(` → ${f.file}: "${f.oldValue}" → "${f.newValue}" (${loc})`);
281
+ }
282
+ }
283
+ if (opts.fixLint === true) {
284
+ shouldApplyFixes = true;
285
+ }
286
+ else if (opts.fixLint === 'prompt') {
287
+ const { prompt: promptWithCancel } = await import('../helpers/prompt.js');
288
+ const res = await promptWithCancel({
289
+ type: 'confirm',
290
+ name: 'apply',
291
+ message: `Apply ${totalLintIssues} fix(es) to source files?`,
292
+ initial: true,
293
+ });
294
+ shouldApplyFixes = res.apply === true;
295
+ }
296
+ if (!shouldApplyFixes) {
297
+ stdout.warn(`${totalLintIssues} lint issue(s) found — run with --fix to auto-fix`);
298
+ throw new CompileError(`Build failed: ${totalLintIssues} lint issue(s) (duplicate IDs and/or keys/IDs starting with a number). Run with --fix or answer 'y' when prompted to fix.`, { file: files[0] });
299
+ }
300
+ // Apply fixes
301
+ for (const { file } of filePages) {
302
+ const idFixesForFile = Array.from(idFixes.entries()).filter(([key]) => key.startsWith(file + ':'));
303
+ const numericForFile = numericLeadingFixes.filter((f) => f.file === file);
304
+ if (idFixesForFile.length > 0 || numericForFile.length > 0) {
305
+ const originalCode = fs.readFileSync(file, 'utf8');
306
+ let code = originalCode;
307
+ if (idFixesForFile.length > 0) {
308
+ code = fixIdsInSourceFile(code, file, idFixesForFile);
309
+ }
310
+ if (numericForFile.length > 0) {
311
+ code = fixNumericLeadingInSourceFile(code, file, numericForFile);
312
+ }
313
+ if (code !== originalCode) {
314
+ fs.writeFileSync(file, code, 'utf8');
315
+ stdout.success(`Updated ${file}`);
316
+ }
317
+ else {
318
+ stdout.warn(` Could not auto-fix duplicate IDs in ${file}. Please fix them manually.`);
319
+ }
320
+ }
321
+ }
322
+ // Re-parse after fixes
323
+ filePages.length = 0;
324
+ for (const file of files) {
325
+ const code = fs.readFileSync(file, 'utf8');
326
+ const pageKey = derivePageKey(file, opts.pageKeyFrom);
327
+ const page = parsePageFromFile({ code, filePath: file, pageKey });
328
+ filePages.push({ file, pageKey, page });
329
+ }
330
+ }
331
+ const pages = [];
332
+ const seenIds = new Map();
333
+ const pageMap = new Map();
334
+ for (const filePage of filePages) {
335
+ const { page, pageKey, file } = filePage;
336
+ for (const c of page.components) {
337
+ checkUniqueId(seenIds, c.id, { file, pageKey });
338
+ if (c.type === 'OptionSelector' && Array.isArray(c.buttons)) {
339
+ for (const b of c.buttons) {
340
+ if (b?.id)
341
+ checkUniqueId(seenIds, b.id, { file, pageKey });
342
+ }
343
+ }
344
+ }
345
+ pages.push(page);
346
+ pageMap.set(pageKey, page);
347
+ }
348
+ // Read config.json if it exists
349
+ let config = null;
350
+ if (opts.configPath && fs.existsSync(opts.configPath)) {
351
+ try {
352
+ const configContent = fs.readFileSync(opts.configPath, 'utf8');
353
+ config = JSON.parse(configContent);
354
+ }
355
+ catch (error) {
356
+ stdout.warn(`Failed to parse config.json: ${error}`);
357
+ }
358
+ }
359
+ else if (opts.embeddableId) {
360
+ // Try to infer config path from embeddableId
361
+ const inferredConfigPath = path.join('embeddables', opts.embeddableId, 'config.json');
362
+ if (fs.existsSync(inferredConfigPath)) {
363
+ try {
364
+ const configContent = fs.readFileSync(inferredConfigPath, 'utf8');
365
+ config = JSON.parse(configContent);
366
+ }
367
+ catch (error) {
368
+ stdout.warn(`Failed to parse config.json: ${error}`);
369
+ }
370
+ }
371
+ }
372
+ // Apply page ordering and metadata from config
373
+ let orderedPages = pages;
374
+ if (config && config.pages && Array.isArray(config.pages)) {
375
+ // Create a map for quick lookup
376
+ const orderedPageMap = new Map();
377
+ for (const pageMetadata of config.pages) {
378
+ orderedPageMap.set(pageMetadata.key, pageMetadata);
379
+ }
380
+ // Sort pages according to config order
381
+ const orderedPageKeys = config.pages.map((p) => p.key);
382
+ const unorderedPages = [];
383
+ orderedPages = orderedPageKeys
384
+ .map((key) => {
385
+ const page = pageMap.get(key);
386
+ if (page) {
387
+ // Apply metadata from config (preserving nested object order)
388
+ const pageMetadata = orderedPageMap.get(key);
389
+ if (pageMetadata) {
390
+ // Merge metadata (excluding key, components, and components_order which is used only for ordering)
391
+ const pageMetadataKeys = Object.keys(pageMetadata);
392
+ for (const metaKey of pageMetadataKeys) {
393
+ if (metaKey !== 'key' && metaKey !== 'components' && metaKey !== 'components_order') {
394
+ ;
395
+ page[metaKey] = deepClonePreservingOrder(pageMetadata[metaKey]);
396
+ }
397
+ }
398
+ // If components_order is present (from pull --preserve), order page.components to match
399
+ const orderIds = pageMetadata.components_order;
400
+ if (Array.isArray(orderIds) &&
401
+ orderIds.length > 0 &&
402
+ page.components &&
403
+ page.components.length > 0) {
404
+ page.components = orderByIdList(page.components, orderIds);
405
+ }
406
+ }
407
+ return page;
408
+ }
409
+ return null;
410
+ })
411
+ .filter((p) => p !== null);
412
+ // Add any pages not in the config order
413
+ for (const page of pages) {
414
+ if (!orderedPageKeys.includes(page.key)) {
415
+ unorderedPages.push(page);
416
+ }
417
+ }
418
+ orderedPages = [...orderedPages, ...unorderedPages];
419
+ }
420
+ // Compile styles from CSS files
421
+ const styles = await compileStyles(opts.stylesDir || 'styles');
422
+ // Build embeddable object preserving the order from config
423
+ const embeddable = {};
424
+ // Read computedFields from JS files
425
+ let computedFields = [];
426
+ if (opts.embeddableId) {
427
+ computedFields = await loadComputedFields(opts.embeddableId);
428
+ }
429
+ // Read dataOutputs from JS files
430
+ let dataOutputs = [];
431
+ if (opts.embeddableId) {
432
+ dataOutputs = await loadDataOutputs(opts.embeddableId, config);
433
+ }
434
+ // Read global components from TSX files
435
+ let globalComponents = [];
436
+ if (opts.embeddableId) {
437
+ globalComponents = await loadGlobalComponents(opts.embeddableId, config);
438
+ }
439
+ // Preserve the order of top-level properties from config
440
+ if (config) {
441
+ const configKeys = Object.keys(config);
442
+ // Iterate through config keys in order to preserve property order
443
+ for (const key of configKeys) {
444
+ if (key === 'pages') {
445
+ // Replace pages with orderedPages (which include components)
446
+ embeddable.pages = orderedPages;
447
+ }
448
+ else if (key === 'styles') {
449
+ // Skip styles from config - they will be added from compiled CSS files later
450
+ continue;
451
+ }
452
+ else if (key === '_version') {
453
+ // Skip _version - this is CLI metadata (tracked version number), not part of the embeddable
454
+ continue;
455
+ }
456
+ else if (key === '_branch_id' || key === '_branch_name') {
457
+ // Skip _branch_id / _branch_name - CLI metadata (current branch), not part of the embeddable
458
+ continue;
459
+ }
460
+ else if (key === 'components_order') {
461
+ // Skip components_order - used only to order global components when building, not part of output
462
+ continue;
463
+ }
464
+ else if (key === 'computedFields') {
465
+ // Replace computedFields with loaded computedFields (which include code)
466
+ if (computedFields.length > 0) {
467
+ embeddable.computedFields = computedFields;
468
+ }
469
+ }
470
+ else if (key === 'dataOutputs') {
471
+ // Replace dataOutputs with loaded dataOutputs (which include code)
472
+ if (dataOutputs.length > 0) {
473
+ embeddable.dataOutputs = dataOutputs;
474
+ }
475
+ }
476
+ else if (key === 'components') {
477
+ // Replace components with loaded global components (optionally ordered by config.components_order)
478
+ if (globalComponents.length > 0) {
479
+ const orderIds = config.components_order;
480
+ embeddable.components =
481
+ Array.isArray(orderIds) && orderIds.length > 0
482
+ ? orderByIdList(globalComponents, orderIds)
483
+ : globalComponents;
484
+ }
485
+ }
486
+ else {
487
+ // Regular metadata property - preserve as-is, with order preserved recursively
488
+ const value = config[key];
489
+ if (value !== undefined && value !== null) {
490
+ embeddable[key] = deepClonePreservingOrder(value);
491
+ }
492
+ }
493
+ }
494
+ }
495
+ // If no config or pages not in config, add pages at the beginning
496
+ if (!embeddable.pages) {
497
+ embeddable.pages = orderedPages;
498
+ }
499
+ // If styles exist, insert them after pages (since styles are not in config.json)
500
+ // This preserves the logical grouping while maintaining order for other properties
501
+ if (Object.keys(styles).length > 0) {
502
+ const embeddableKeys = Object.keys(embeddable);
503
+ const pagesIndex = embeddableKeys.indexOf('pages');
504
+ if (pagesIndex !== -1) {
505
+ // Insert styles after pages by rebuilding the object
506
+ const newEmbeddable = {};
507
+ let stylesInserted = false;
508
+ for (const key of embeddableKeys) {
509
+ newEmbeddable[key] = embeddable[key];
510
+ if (key === 'pages' && !stylesInserted) {
511
+ newEmbeddable.styles = styles;
512
+ stylesInserted = true;
513
+ }
514
+ }
515
+ // If pages was the last key, add styles after it
516
+ if (!stylesInserted && pagesIndex === embeddableKeys.length - 1) {
517
+ newEmbeddable.styles = styles;
518
+ }
519
+ // Rebuild embeddable in the correct order by clearing and rebuilding
520
+ Object.keys(embeddable).forEach((key) => delete embeddable[key]);
521
+ Object.assign(embeddable, newEmbeddable);
522
+ }
523
+ else {
524
+ // Pages doesn't exist in embeddable, add styles at the end
525
+ embeddable.styles = styles;
526
+ }
527
+ }
528
+ // If computedFields or dataOutputs don't exist in config but we have them, add them at the end
529
+ if (computedFields.length > 0 && !embeddable.computedFields) {
530
+ embeddable.computedFields = computedFields;
531
+ }
532
+ if (dataOutputs.length > 0 && !embeddable.dataOutputs) {
533
+ embeddable.dataOutputs = dataOutputs;
534
+ }
535
+ if (globalComponents.length > 0 && !embeddable.components) {
536
+ const orderIds = config?.components_order;
537
+ embeddable.components =
538
+ Array.isArray(orderIds) && orderIds.length > 0
539
+ ? orderByIdList(globalComponents, orderIds)
540
+ : globalComponents;
541
+ }
542
+ let toWrite = embeddable;
543
+ if (fs.existsSync(opts.outPath)) {
544
+ try {
545
+ const existing = fs.readFileSync(opts.outPath, 'utf8');
546
+ const oldEmbeddable = JSON.parse(existing);
547
+ toWrite = reorderObjectKeysToMatch(oldEmbeddable, embeddable);
548
+ }
549
+ catch {
550
+ // If read/parse fails, keep embeddable as-is
551
+ }
552
+ }
553
+ writeAtomic(opts.outPath, JSON.stringify(toWrite, null, 2));
554
+ stdout.print(`Wrote ${opts.outPath} (${orderedPages.length} pages, ${Object.keys(styles).length > 0 ? 'with styles' : 'no styles'})`);
555
+ }
556
+ function derivePageKey(file, mode) {
557
+ if (mode === 'filename') {
558
+ const base = path.basename(file);
559
+ // email.page.tsx -> email
560
+ return base.replace(/\.page\.tsx$/, '').replace(/\.tsx$/, '');
561
+ }
562
+ // v1: export-based not implemented here
563
+ return path
564
+ .basename(file)
565
+ .replace(/\.page\.tsx$/, '')
566
+ .replace(/\.tsx$/, '');
567
+ }
568
+ /**
569
+ * Fixes duplicate IDs in a React source file by applying targeted text replacements.
570
+ * Preserves original formatting (no Babel code generation) so unchanged files are not rewritten.
571
+ * Handles both inline button arrays and const-referenced button arrays.
572
+ * @param code Original source code
573
+ * @param filePath File path (for error reporting)
574
+ * @param fixes Array of [fixKey, newId] tuples where fixKey is "file:componentIndex" or "file:componentIndex:buttonIndex"
575
+ */
576
+ function fixIdsInSourceFile(code, filePath, fixes) {
577
+ // Parse the file
578
+ let ast;
579
+ try {
580
+ ast = parse(code, {
581
+ sourceType: 'module',
582
+ plugins: ['typescript', 'jsx'],
583
+ sourceFilename: filePath,
584
+ });
585
+ }
586
+ catch (error) {
587
+ throw new CompileError(`Failed to parse file for ID fixing: ${error.message}`, {
588
+ file: filePath,
589
+ });
590
+ }
591
+ // Build a map of component index -> new ID, and component index + button index -> new ID
592
+ const componentIdMap = new Map();
593
+ const buttonIdMap = new Map(); // "componentIndex:buttonIndex" -> newId
594
+ for (const [fixKey, newId] of fixes) {
595
+ const filePrefix = filePath + ':';
596
+ if (!fixKey.startsWith(filePrefix)) {
597
+ continue;
598
+ }
599
+ const indicesStr = fixKey.substring(filePrefix.length);
600
+ const parts = indicesStr.split(':');
601
+ if (parts.length === 1) {
602
+ const componentIndex = parseInt(parts[0], 10);
603
+ componentIdMap.set(componentIndex, newId);
604
+ }
605
+ else if (parts.length === 2) {
606
+ const componentIndex = parseInt(parts[0], 10);
607
+ const buttonIndex = parseInt(parts[1], 10);
608
+ buttonIdMap.set(`${componentIndex}:${buttonIndex}`, newId);
609
+ }
610
+ }
611
+ const replacements = [];
612
+ const pushReplacement = (node, newValue) => {
613
+ if (node.start == null || node.end == null)
614
+ return;
615
+ replacements.push({ start: node.start, end: node.end, newText: JSON.stringify(newValue) });
616
+ };
617
+ // Track const variable names referenced by OptionSelector buttons props
618
+ const constNameToComponentIndex = new Map();
619
+ let componentIndex = -1;
620
+ traverse(ast, {
621
+ JSXElement(path) {
622
+ const opening = path.node.openingElement;
623
+ const tagName = opening.name.type === 'JSXIdentifier' ? opening.name.name : null;
624
+ if (!tagName || !ALLOWED_PRIMITIVES.has(tagName))
625
+ return;
626
+ const hasIdAttr = opening.attributes.some((attr) => attr.type === 'JSXAttribute' &&
627
+ attr.name.type === 'JSXIdentifier' &&
628
+ attr.name.name === 'id');
629
+ if (hasIdAttr) {
630
+ componentIndex++;
631
+ if (componentIdMap.has(componentIndex)) {
632
+ const newId = componentIdMap.get(componentIndex);
633
+ for (const attr of opening.attributes) {
634
+ if (attr.type === 'JSXAttribute' &&
635
+ attr.name.type === 'JSXIdentifier' &&
636
+ attr.name.name === 'id') {
637
+ const valueNode = attr.value?.type === 'JSXExpressionContainer' ? attr.value.expression : attr.value;
638
+ if (valueNode?.type === 'StringLiteral') {
639
+ pushReplacement(valueNode, newId);
640
+ }
641
+ break;
642
+ }
643
+ }
644
+ }
645
+ if (tagName === 'OptionSelector') {
646
+ for (const attr of opening.attributes) {
647
+ if (attr.type === 'JSXAttribute' &&
648
+ attr.name.type === 'JSXIdentifier' &&
649
+ attr.name.name === 'buttons' &&
650
+ attr.value?.type === 'JSXExpressionContainer') {
651
+ const expr = attr.value.expression;
652
+ if (expr.type === 'ArrayExpression') {
653
+ collectButtonIdReplacements(expr, componentIndex, buttonIdMap, pushReplacement);
654
+ }
655
+ else if (expr.type === 'Identifier') {
656
+ constNameToComponentIndex.set(expr.name, componentIndex);
657
+ }
658
+ break;
659
+ }
660
+ }
661
+ }
662
+ }
663
+ },
664
+ });
665
+ // Second pass: fix button IDs in const declarations referenced by OptionSelector buttons props
666
+ if (constNameToComponentIndex.size > 0) {
667
+ traverse(ast, {
668
+ VariableDeclarator(path) {
669
+ const id = path.node.id;
670
+ const init = path.node.init;
671
+ if (id.type !== 'Identifier')
672
+ return;
673
+ const compIndex = constNameToComponentIndex.get(id.name);
674
+ if (compIndex === undefined)
675
+ return;
676
+ if (init?.type !== 'ArrayExpression')
677
+ return;
678
+ collectButtonIdReplacements(init, compIndex, buttonIdMap, pushReplacement);
679
+ },
680
+ });
681
+ }
682
+ if (replacements.length === 0)
683
+ return code;
684
+ replacements.sort((a, b) => b.start - a.start);
685
+ let result = code;
686
+ for (const { start, end, newText } of replacements) {
687
+ result = result.slice(0, start) + newText + result.slice(end);
688
+ }
689
+ return result;
690
+ }
691
+ /**
692
+ * Collects text replacements for button IDs in an ArrayExpression node.
693
+ */
694
+ function collectButtonIdReplacements(arrayExpr, componentIndex, buttonIdMap, pushReplacement) {
695
+ let buttonIndex = 0;
696
+ for (const element of arrayExpr.elements) {
697
+ if (element && element.type === 'ObjectExpression') {
698
+ const key = `${componentIndex}:${buttonIndex}`;
699
+ if (buttonIdMap.has(key)) {
700
+ const newButtonId = buttonIdMap.get(key);
701
+ for (const prop of element.properties) {
702
+ if (prop.type === 'ObjectProperty' &&
703
+ prop.key.type === 'Identifier' &&
704
+ prop.key.name === 'id') {
705
+ if (prop.value.type === 'StringLiteral') {
706
+ pushReplacement(prop.value, newButtonId);
707
+ }
708
+ break;
709
+ }
710
+ }
711
+ }
712
+ }
713
+ buttonIndex++;
714
+ }
715
+ }
716
+ function fixNumericLeadingInSourceFile(code, filePath, fixes) {
717
+ let ast;
718
+ try {
719
+ ast = parse(code, {
720
+ sourceType: 'module',
721
+ plugins: ['typescript', 'jsx'],
722
+ sourceFilename: filePath,
723
+ });
724
+ }
725
+ catch (error) {
726
+ throw new CompileError(`Failed to parse file for key/id fixing: ${error.message}`, {
727
+ file: filePath,
728
+ });
729
+ }
730
+ const componentKeyMap = new Map();
731
+ const componentIdMap = new Map();
732
+ const buttonKeyMap = new Map();
733
+ const buttonIdMap = new Map();
734
+ for (const f of fixes) {
735
+ if (f.buttonIndex !== undefined) {
736
+ const k = `${f.componentIndex}:${f.buttonIndex}`;
737
+ if (f.kind === 'buttonKey')
738
+ buttonKeyMap.set(k, f.newValue);
739
+ else if (f.kind === 'buttonId')
740
+ buttonIdMap.set(k, f.newValue);
741
+ }
742
+ else {
743
+ if (f.kind === 'componentKey')
744
+ componentKeyMap.set(f.componentIndex, f.newValue);
745
+ else if (f.kind === 'componentId')
746
+ componentIdMap.set(f.componentIndex, f.newValue);
747
+ }
748
+ }
749
+ // Map from buttons variable name (e.g. "genderButtons") -> OptionSelector key (e.g. "gender") for fixing const arrays
750
+ const buttonsConstNameToPrefix = new Map();
751
+ // Collect (start, end, newText) replacements so we can patch the source without reformatting (preserves line breaks, etc.)
752
+ const replacements = [];
753
+ const pushReplacement = (node, newValue) => {
754
+ if (node.start == null || node.end == null)
755
+ return;
756
+ replacements.push({ start: node.start, end: node.end, newText: JSON.stringify(newValue) });
757
+ };
758
+ let componentIndex = -1;
759
+ traverse(ast, {
760
+ JSXElement(path) {
761
+ const opening = path.node.openingElement;
762
+ const tagName = opening.name.type === 'JSXIdentifier' ? opening.name.name : null;
763
+ if (!tagName || !ALLOWED_PRIMITIVES.has(tagName))
764
+ return;
765
+ const hasIdAttr = opening.attributes.some((a) => a.type === 'JSXAttribute' && a.name.type === 'JSXIdentifier' && a.name.name === 'id');
766
+ const hasKeyAttr = opening.attributes.some((a) => a.type === 'JSXAttribute' && a.name.type === 'JSXIdentifier' && a.name.name === 'key');
767
+ if (hasIdAttr || hasKeyAttr) {
768
+ componentIndex++;
769
+ }
770
+ const recordAttrReplacement = (name, newValue) => {
771
+ for (const attr of opening.attributes) {
772
+ if (attr.type === 'JSXAttribute' &&
773
+ attr.name.type === 'JSXIdentifier' &&
774
+ attr.name.name === name) {
775
+ const valueNode = attr.value?.type === 'JSXExpressionContainer' ? attr.value.expression : attr.value;
776
+ if (valueNode?.type === 'StringLiteral') {
777
+ pushReplacement(valueNode, newValue);
778
+ }
779
+ break;
780
+ }
781
+ }
782
+ };
783
+ if (componentKeyMap.has(componentIndex)) {
784
+ recordAttrReplacement('key', componentKeyMap.get(componentIndex));
785
+ }
786
+ if (componentIdMap.has(componentIndex)) {
787
+ recordAttrReplacement('id', componentIdMap.get(componentIndex));
788
+ }
789
+ if (tagName === 'OptionSelector') {
790
+ let componentKey;
791
+ for (const attr of opening.attributes) {
792
+ if (attr.type === 'JSXAttribute' &&
793
+ attr.name.type === 'JSXIdentifier' &&
794
+ attr.name.name === 'key' &&
795
+ attr.value?.type === 'StringLiteral') {
796
+ componentKey = attr.value.value;
797
+ break;
798
+ }
799
+ }
800
+ const prefix = componentKey ?? 'option';
801
+ for (const attr of opening.attributes) {
802
+ if (attr.type === 'JSXAttribute' &&
803
+ attr.name.type === 'JSXIdentifier' &&
804
+ attr.name.name === 'buttons' &&
805
+ attr.value?.type === 'JSXExpressionContainer') {
806
+ const expr = attr.value.expression;
807
+ if (expr.type === 'ArrayExpression') {
808
+ let buttonIndex = 0;
809
+ for (const element of expr.elements) {
810
+ if (element && element.type === 'ObjectExpression') {
811
+ const key = `${componentIndex}:${buttonIndex}`;
812
+ for (const prop of element.properties) {
813
+ if (prop.type === 'ObjectProperty' && prop.key.type === 'Identifier') {
814
+ if (prop.key.name === 'key' && buttonKeyMap.has(key)) {
815
+ pushReplacement(prop.value, buttonKeyMap.get(key));
816
+ }
817
+ else if (prop.key.name === 'id' && buttonIdMap.has(key)) {
818
+ pushReplacement(prop.value, buttonIdMap.get(key));
819
+ }
820
+ }
821
+ }
822
+ }
823
+ buttonIndex++;
824
+ }
825
+ }
826
+ else if (expr.type === 'Identifier') {
827
+ if (!buttonsConstNameToPrefix.has(expr.name)) {
828
+ buttonsConstNameToPrefix.set(expr.name, prefix);
829
+ }
830
+ }
831
+ break;
832
+ }
833
+ }
834
+ }
835
+ },
836
+ });
837
+ // Fix button key/id in const arrays (e.g. const genderButtons = [{ id: "1_...", key: "..." }])
838
+ traverse(ast, {
839
+ VariableDeclarator(path) {
840
+ const id = path.node.id;
841
+ const init = path.node.init;
842
+ if (id.type !== 'Identifier' || init?.type !== 'ArrayExpression')
843
+ return;
844
+ const constName = id.name;
845
+ const prefix = buttonsConstNameToPrefix.get(constName) ?? 'option';
846
+ for (const element of init.elements) {
847
+ if (!element || element.type !== 'ObjectExpression')
848
+ continue;
849
+ for (const prop of element.properties) {
850
+ if (prop.type !== 'ObjectProperty' || prop.key.type !== 'Identifier')
851
+ continue;
852
+ if (prop.key.name !== 'key' && prop.key.name !== 'id')
853
+ continue;
854
+ const val = prop.value;
855
+ let current = null;
856
+ if (val.type === 'StringLiteral') {
857
+ current = val.value;
858
+ }
859
+ else if (val.type === 'TemplateLiteral' &&
860
+ val.quasis.length === 1 &&
861
+ !val.quasis[0].value.raw.includes('$')) {
862
+ current = val.quasis[0].value.raw;
863
+ }
864
+ if (current === null || !keyOrIdStartsWithDigit(current))
865
+ continue;
866
+ const newValue = normalizeKeyIfStartsWithDigit(current, prefix);
867
+ if (newValue === null)
868
+ continue;
869
+ pushReplacement(val, newValue);
870
+ }
871
+ }
872
+ },
873
+ });
874
+ // Apply replacements from end to start so indices stay valid; preserves all other formatting
875
+ replacements.sort((a, b) => b.start - a.start);
876
+ let result = code;
877
+ for (const { start, end, newText } of replacements) {
878
+ result = result.slice(0, start) + newText + result.slice(end);
879
+ }
880
+ return result;
881
+ }
882
+ function checkUniqueId(seen, id, loc) {
883
+ const prev = seen.get(id);
884
+ if (prev) {
885
+ throw new CompileError(`Duplicate id "${id}"\n- first: ${prev.file} (page: ${prev.pageKey})\n- second: ${loc.file} (page: ${loc.pageKey})\nIDs must be globally unique across all pages.`, { file: loc.file });
886
+ }
887
+ seen.set(id, loc);
888
+ }
889
+ async function compileStyles(stylesDir) {
890
+ const stylesPath = path.resolve(stylesDir);
891
+ // Check if styles directory exists
892
+ if (!fs.existsSync(stylesPath)) {
893
+ return {};
894
+ }
895
+ // Find all CSS files in the styles directory
896
+ const cssFiles = await fg('**/*.css', {
897
+ cwd: stylesPath,
898
+ dot: false,
899
+ });
900
+ if (cssFiles.length === 0) {
901
+ return {};
902
+ }
903
+ // Combine all CSS files into a single JSON object
904
+ const combinedStyles = {};
905
+ for (const cssFile of cssFiles) {
906
+ const fullPath = path.join(stylesPath, cssFile);
907
+ const cssContent = fs.readFileSync(fullPath, 'utf8');
908
+ // Skip empty files
909
+ if (!cssContent.trim()) {
910
+ continue;
911
+ }
912
+ // Convert CSS to JSON
913
+ const cssJson = CSSJSON.toJSON(cssContent);
914
+ // Flatten the structure: extract selectors and their attributes
915
+ // cssjson returns { children: { selector: { children: {}, attributes: {...} } } }
916
+ if (cssJson && cssJson.children) {
917
+ for (const [selector, rule] of Object.entries(cssJson.children)) {
918
+ if (rule && typeof rule === 'object' && 'attributes' in rule) {
919
+ // Extract just the attributes (styles) for each selector
920
+ combinedStyles[selector] = rule.attributes || {};
921
+ }
922
+ }
923
+ }
924
+ }
925
+ return combinedStyles;
926
+ }
927
+ function writeAtomic(outPath, content) {
928
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
929
+ const tmp = `${outPath}.tmp`;
930
+ fs.writeFileSync(tmp, content, 'utf8');
931
+ fs.renameSync(tmp, outPath);
932
+ }
933
+ /**
934
+ * Loads computedFields from JS files in computed-fields/ folder.
935
+ * Each JS file corresponds to one computedField.
936
+ */
937
+ async function loadComputedFields(embeddableId) {
938
+ const computedFieldsDir = path.join('embeddables', embeddableId, 'computed-fields');
939
+ if (!fs.existsSync(computedFieldsDir)) {
940
+ return [];
941
+ }
942
+ // Find all JS files in computed-fields directory
943
+ const jsFiles = await fg('**/*.js', {
944
+ cwd: computedFieldsDir,
945
+ dot: false,
946
+ });
947
+ if (jsFiles.length === 0) {
948
+ return [];
949
+ }
950
+ const computedFields = [];
951
+ // Read config.json to get computedField metadata (if available)
952
+ const configPath = path.join('embeddables', embeddableId, 'config.json');
953
+ let config = null;
954
+ if (fs.existsSync(configPath)) {
955
+ try {
956
+ const configContent = fs.readFileSync(configPath, 'utf8');
957
+ config = JSON.parse(configContent);
958
+ }
959
+ catch (error) {
960
+ // Ignore config parsing errors
961
+ }
962
+ }
963
+ // Build a map of computed fields by key/id for quick lookup
964
+ const computedFieldsMap = new Map();
965
+ for (const jsFile of jsFiles) {
966
+ const filePath = path.join(computedFieldsDir, jsFile);
967
+ const code = fs.readFileSync(filePath, 'utf8');
968
+ // Extract key from filename (remove .js extension)
969
+ const key = path.basename(jsFile, '.js');
970
+ // Build computedField object
971
+ const computedField = {
972
+ id: key, // Use key as id if not specified in config
973
+ key,
974
+ formula: 'custom',
975
+ code,
976
+ };
977
+ // If config has computedFields metadata, try to find matching field
978
+ if (config && config.computedFields) {
979
+ const configField = config.computedFields.find((cf) => cf.key === key || cf.id === key);
980
+ if (configField) {
981
+ // Merge metadata from config (but keep code from file)
982
+ // Preserve order by cloning the config field first
983
+ const clonedConfigField = deepClonePreservingOrder(configField);
984
+ Object.assign(computedField, clonedConfigField);
985
+ computedField.code = code; // Ensure code comes from file
986
+ }
987
+ }
988
+ // Store in map using both key and id as lookup keys
989
+ computedFieldsMap.set(key, computedField);
990
+ if (computedField.id && computedField.id !== key) {
991
+ computedFieldsMap.set(computedField.id, computedField);
992
+ }
993
+ }
994
+ // If config has computedFields, preserve order from config
995
+ if (config && config.computedFields && Array.isArray(config.computedFields)) {
996
+ const orderedFields = [];
997
+ const processedKeys = new Set();
998
+ // First, add fields in config order
999
+ for (const configField of config.computedFields) {
1000
+ const lookupKey = configField.key || configField.id;
1001
+ if (lookupKey) {
1002
+ const field = computedFieldsMap.get(lookupKey);
1003
+ if (field) {
1004
+ orderedFields.push(field);
1005
+ processedKeys.add(lookupKey);
1006
+ if (field.id && field.id !== lookupKey) {
1007
+ processedKeys.add(field.id);
1008
+ }
1009
+ }
1010
+ }
1011
+ }
1012
+ // Then, add any fields not in config (in filesystem order)
1013
+ for (const jsFile of jsFiles) {
1014
+ const key = path.basename(jsFile, '.js');
1015
+ if (!processedKeys.has(key)) {
1016
+ const field = computedFieldsMap.get(key);
1017
+ if (field) {
1018
+ orderedFields.push(field);
1019
+ processedKeys.add(key);
1020
+ }
1021
+ }
1022
+ }
1023
+ return orderedFields;
1024
+ }
1025
+ // No config or no computedFields in config, return in filesystem order
1026
+ // Use a Set to deduplicate since we may have multiple keys pointing to the same field
1027
+ const seenFields = new Set();
1028
+ const orderedFields = [];
1029
+ for (const jsFile of jsFiles) {
1030
+ const key = path.basename(jsFile, '.js');
1031
+ const field = computedFieldsMap.get(key);
1032
+ if (field && !seenFields.has(field)) {
1033
+ orderedFields.push(field);
1034
+ seenFields.add(field);
1035
+ }
1036
+ }
1037
+ return orderedFields;
1038
+ }
1039
+ /**
1040
+ * Loads dataOutputs (actions) from JS files in actions/ folder.
1041
+ * Each JS file corresponds to one action.
1042
+ */
1043
+ async function loadDataOutputs(embeddableId, config = null) {
1044
+ const actionsDir = path.join('embeddables', embeddableId, 'actions');
1045
+ const hasActionsDir = fs.existsSync(actionsDir);
1046
+ // Find all JS files in actions directory
1047
+ let jsFiles = [];
1048
+ if (hasActionsDir) {
1049
+ jsFiles = await fg('**/*.js', {
1050
+ cwd: actionsDir,
1051
+ dot: false,
1052
+ });
1053
+ }
1054
+ // Use provided config or read it if not provided
1055
+ if (!config) {
1056
+ const configPath = path.join('embeddables', embeddableId, 'config.json');
1057
+ if (fs.existsSync(configPath)) {
1058
+ try {
1059
+ const configContent = fs.readFileSync(configPath, 'utf8');
1060
+ config = JSON.parse(configContent);
1061
+ }
1062
+ catch (error) {
1063
+ // Ignore config parsing errors
1064
+ }
1065
+ }
1066
+ }
1067
+ const hasConfigDataOutputs = config &&
1068
+ config.dataOutputs &&
1069
+ Array.isArray(config.dataOutputs) &&
1070
+ config.dataOutputs.length > 0;
1071
+ if (jsFiles.length === 0 && !hasConfigDataOutputs) {
1072
+ return [];
1073
+ }
1074
+ // Build a map of actions by various identifiers for quick lookup
1075
+ const dataOutputsMap = new Map();
1076
+ for (const jsFile of jsFiles) {
1077
+ const filePath = path.join(actionsDir, jsFile);
1078
+ const code = fs.readFileSync(filePath, 'utf8');
1079
+ // Extract key/name from filename (remove .js extension)
1080
+ const identifier = path.basename(jsFile, '.js');
1081
+ // Build action object
1082
+ const action = {
1083
+ id: identifier, // Use identifier as id if not specified
1084
+ name: identifier,
1085
+ output: 'custom',
1086
+ code,
1087
+ };
1088
+ // If config has dataOutputs metadata, try to find matching action
1089
+ // Match by comparing sanitized names since filenames are based on sanitized action names
1090
+ if (config && config.dataOutputs) {
1091
+ const configAction = config.dataOutputs.find((act) => {
1092
+ // Compare sanitized versions since filename is based on sanitized name
1093
+ const sanitizedName = act.name ? sanitizeFileName(act.name) : null;
1094
+ return act.name === identifier || act.id === identifier || sanitizedName === identifier;
1095
+ });
1096
+ if (configAction) {
1097
+ // Merge metadata from config (but keep code from file)
1098
+ // Preserve order by cloning the config action first
1099
+ const clonedConfigAction = deepClonePreservingOrder(configAction);
1100
+ Object.assign(action, clonedConfigAction);
1101
+ action.code = code; // Ensure code comes from file
1102
+ // Preserve the original name from config, not the sanitized filename
1103
+ if (configAction.name) {
1104
+ action.name = configAction.name;
1105
+ }
1106
+ }
1107
+ delete action.key; // name is enough; don't emit key
1108
+ }
1109
+ // Store in map using identifier (filename) as the primary key
1110
+ // Also store by id, key, name, and sanitized name for lookup
1111
+ dataOutputsMap.set(identifier, action);
1112
+ if (action.id && action.id !== identifier) {
1113
+ dataOutputsMap.set(action.id, action);
1114
+ }
1115
+ if (action.name && action.name !== identifier && action.name !== action.id) {
1116
+ const sanitizedName = sanitizeFileName(action.name);
1117
+ if (sanitizedName !== identifier) {
1118
+ dataOutputsMap.set(sanitizedName, action);
1119
+ }
1120
+ }
1121
+ }
1122
+ // If config has dataOutputs, preserve order from config
1123
+ if (config && config.dataOutputs && Array.isArray(config.dataOutputs)) {
1124
+ const orderedOutputs = [];
1125
+ const processedIdentifiers = new Set();
1126
+ // First, add actions in config order
1127
+ for (const configAction of config.dataOutputs) {
1128
+ // Try multiple matching strategies (name is enough; no key)
1129
+ let matchedAction;
1130
+ const possibleKeys = [
1131
+ configAction.id,
1132
+ configAction.name,
1133
+ configAction.name ? sanitizeFileName(configAction.name) : null,
1134
+ ].filter(Boolean);
1135
+ for (const key of possibleKeys) {
1136
+ const action = dataOutputsMap.get(key);
1137
+ if (action && !processedIdentifiers.has(key)) {
1138
+ matchedAction = action;
1139
+ // Mark all possible identifiers as processed
1140
+ processedIdentifiers.add(key);
1141
+ if (action.id)
1142
+ processedIdentifiers.add(action.id);
1143
+ if (action.name) {
1144
+ processedIdentifiers.add(action.name);
1145
+ processedIdentifiers.add(sanitizeFileName(action.name));
1146
+ }
1147
+ break;
1148
+ }
1149
+ }
1150
+ if (matchedAction) {
1151
+ orderedOutputs.push(matchedAction);
1152
+ }
1153
+ else {
1154
+ // No .js file for this action (e.g. metadata-only / code-less action like diahook).
1155
+ // Preserve it from config so round-trip does not drop it.
1156
+ orderedOutputs.push(deepClonePreservingOrder(configAction));
1157
+ }
1158
+ }
1159
+ // Then, add any actions not in config (in filesystem order)
1160
+ for (const jsFile of jsFiles) {
1161
+ const identifier = path.basename(jsFile, '.js');
1162
+ if (!processedIdentifiers.has(identifier)) {
1163
+ const action = dataOutputsMap.get(identifier);
1164
+ if (action) {
1165
+ orderedOutputs.push(action);
1166
+ processedIdentifiers.add(identifier);
1167
+ if (action.id)
1168
+ processedIdentifiers.add(action.id);
1169
+ if (action.name) {
1170
+ processedIdentifiers.add(action.name);
1171
+ processedIdentifiers.add(sanitizeFileName(action.name));
1172
+ }
1173
+ }
1174
+ }
1175
+ }
1176
+ return orderedOutputs;
1177
+ }
1178
+ // No config or no dataOutputs in config, return in filesystem order
1179
+ // Use a Set to deduplicate since we may have multiple keys pointing to the same action
1180
+ const seenActions = new Set();
1181
+ const orderedOutputs = [];
1182
+ for (const jsFile of jsFiles) {
1183
+ const identifier = path.basename(jsFile, '.js');
1184
+ const action = dataOutputsMap.get(identifier);
1185
+ if (action && !seenActions.has(action)) {
1186
+ orderedOutputs.push(action);
1187
+ seenActions.add(action);
1188
+ }
1189
+ }
1190
+ return orderedOutputs;
1191
+ }
1192
+ /**
1193
+ * Loads global components from TSX files in global-components/ folder.
1194
+ * Each TSX file corresponds to components at a specific location.
1195
+ * File naming: <location>.location.tsx (e.g., before_page.location.tsx)
1196
+ * The _location is derived from the filename - no need for _location props in TSX or config.
1197
+ */
1198
+ async function loadGlobalComponents(embeddableId, _config = null // config.components is no longer used - location comes from filename
1199
+ ) {
1200
+ const globalComponentsDir = path.join('embeddables', embeddableId, 'global-components');
1201
+ if (!fs.existsSync(globalComponentsDir)) {
1202
+ return [];
1203
+ }
1204
+ // Find all TSX files in global-components directory
1205
+ const tsxFiles = await fg('**/*.location.tsx', {
1206
+ cwd: globalComponentsDir,
1207
+ dot: false,
1208
+ });
1209
+ if (tsxFiles.length === 0) {
1210
+ return [];
1211
+ }
1212
+ const allComponents = [];
1213
+ const seenIds = new Map();
1214
+ // Build a component map for parent resolution
1215
+ const componentMap = new Map();
1216
+ // Map component ID to file location (derived from filename)
1217
+ const componentToFileLocation = new Map();
1218
+ for (const tsxFile of tsxFiles) {
1219
+ const filePath = path.join(globalComponentsDir, tsxFile);
1220
+ const code = fs.readFileSync(filePath, 'utf8');
1221
+ // Extract location from filename (e.g., "before_page.location.tsx" -> "before_page")
1222
+ const locationMatch = tsxFile.match(/^(.+)\.location\.tsx$/);
1223
+ if (!locationMatch) {
1224
+ stdout.warn(`Global component file "${tsxFile}" doesn't match expected pattern <location>.location.tsx`);
1225
+ continue;
1226
+ }
1227
+ const fileLocation = locationMatch[1];
1228
+ // Parse components from the file
1229
+ const { parseGlobalComponentsFromFile } = await import('./parsePage.js');
1230
+ const fileComponents = parseGlobalComponentsFromFile({
1231
+ code,
1232
+ filePath,
1233
+ });
1234
+ // Add components to map and validate
1235
+ for (const component of fileComponents) {
1236
+ // Store file location for this component (derived from filename)
1237
+ componentToFileLocation.set(component.id, fileLocation);
1238
+ // Global ID uniqueness check (using file location as pageKey for global components)
1239
+ checkUniqueId(seenIds, component.id, {
1240
+ file: filePath,
1241
+ pageKey: `global-${fileLocation}`,
1242
+ });
1243
+ // Validate OptionSelector buttons
1244
+ if (component.type === 'OptionSelector' && Array.isArray(component.buttons)) {
1245
+ for (const b of component.buttons) {
1246
+ if (b?.id)
1247
+ checkUniqueId(seenIds, b.id, {
1248
+ file: filePath,
1249
+ pageKey: `global-${fileLocation}`,
1250
+ });
1251
+ }
1252
+ }
1253
+ componentMap.set(component.id, component);
1254
+ }
1255
+ }
1256
+ // Set _location for root components (no parent_id) based on filename
1257
+ for (const [componentId, component] of componentMap) {
1258
+ // Remove any _location prop that might have been in the TSX (we derive it from filename)
1259
+ delete component._location;
1260
+ // Root components (no parent_id) get _location from their filename
1261
+ if (!component.parent_id) {
1262
+ const fileLocation = componentToFileLocation.get(componentId);
1263
+ if (!fileLocation) {
1264
+ throw new CompileError(`Global component "${component.id}" (key: "${component.key}") has no parent_id but couldn't determine location from file.`);
1265
+ }
1266
+ component._location = fileLocation;
1267
+ }
1268
+ // Child components inherit location from parent - they don't need _location
1269
+ allComponents.push(component);
1270
+ }
1271
+ return allComponents;
1272
+ }