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