@embeddables/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/README.md +116 -0
  2. package/bin/embeddables.mjs +2 -0
  3. package/dist/auth/index.d.ts +43 -0
  4. package/dist/auth/index.d.ts.map +1 -0
  5. package/dist/auth/index.js +100 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +75 -0
  9. package/dist/commands/build-workbench.d.ts +5 -0
  10. package/dist/commands/build-workbench.d.ts.map +1 -0
  11. package/dist/commands/build-workbench.js +122 -0
  12. package/dist/commands/build.d.ts +7 -0
  13. package/dist/commands/build.d.ts.map +1 -0
  14. package/dist/commands/build.js +22 -0
  15. package/dist/commands/dev.d.ts +11 -0
  16. package/dist/commands/dev.d.ts.map +1 -0
  17. package/dist/commands/dev.js +153 -0
  18. package/dist/commands/login.d.ts +2 -0
  19. package/dist/commands/login.d.ts.map +1 -0
  20. package/dist/commands/login.js +112 -0
  21. package/dist/commands/logout.d.ts +2 -0
  22. package/dist/commands/logout.d.ts.map +1 -0
  23. package/dist/commands/logout.js +18 -0
  24. package/dist/commands/pull.d.ts +7 -0
  25. package/dist/commands/pull.d.ts.map +1 -0
  26. package/dist/commands/pull.js +97 -0
  27. package/dist/compiler/errors.d.ts +20 -0
  28. package/dist/compiler/errors.d.ts.map +1 -0
  29. package/dist/compiler/errors.js +35 -0
  30. package/dist/compiler/evalStatic.d.ts +3 -0
  31. package/dist/compiler/evalStatic.d.ts.map +1 -0
  32. package/dist/compiler/evalStatic.js +57 -0
  33. package/dist/compiler/flatten.js +1 -0
  34. package/dist/compiler/helpers/duplicateIds.d.ts +9 -0
  35. package/dist/compiler/helpers/duplicateIds.d.ts.map +1 -0
  36. package/dist/compiler/helpers/duplicateIds.js +71 -0
  37. package/dist/compiler/index.d.ts +16 -0
  38. package/dist/compiler/index.d.ts.map +1 -0
  39. package/dist/compiler/index.js +934 -0
  40. package/dist/compiler/parsePage.d.ts +15 -0
  41. package/dist/compiler/parsePage.d.ts.map +1 -0
  42. package/dist/compiler/parsePage.js +562 -0
  43. package/dist/compiler/registry.d.ts +4 -0
  44. package/dist/compiler/registry.d.ts.map +1 -0
  45. package/dist/compiler/registry.js +44 -0
  46. package/dist/compiler/reverse.d.ts +17 -0
  47. package/dist/compiler/reverse.d.ts.map +1 -0
  48. package/dist/compiler/reverse.js +1632 -0
  49. package/dist/compiler/types.d.ts +21 -0
  50. package/dist/compiler/types.d.ts.map +1 -0
  51. package/dist/compiler/types.js +1 -0
  52. package/dist/components/index.d.ts +21 -0
  53. package/dist/components/index.d.ts.map +1 -0
  54. package/dist/components/index.js +21 -0
  55. package/dist/components/primitives/BaseComponent.d.ts +32 -0
  56. package/dist/components/primitives/BaseComponent.d.ts.map +1 -0
  57. package/dist/components/primitives/BaseComponent.js +26 -0
  58. package/dist/components/primitives/BookMeeting.d.ts +18 -0
  59. package/dist/components/primitives/BookMeeting.d.ts.map +1 -0
  60. package/dist/components/primitives/BookMeeting.js +5 -0
  61. package/dist/components/primitives/Chart.d.ts +41 -0
  62. package/dist/components/primitives/Chart.d.ts.map +1 -0
  63. package/dist/components/primitives/Chart.js +5 -0
  64. package/dist/components/primitives/Container.d.ts +8 -0
  65. package/dist/components/primitives/Container.d.ts.map +1 -0
  66. package/dist/components/primitives/Container.js +5 -0
  67. package/dist/components/primitives/CustomButton.d.ts +37 -0
  68. package/dist/components/primitives/CustomButton.d.ts.map +1 -0
  69. package/dist/components/primitives/CustomButton.js +10 -0
  70. package/dist/components/primitives/CustomHTML.d.ts +8 -0
  71. package/dist/components/primitives/CustomHTML.d.ts.map +1 -0
  72. package/dist/components/primitives/CustomHTML.js +5 -0
  73. package/dist/components/primitives/FileUpload.d.ts +18 -0
  74. package/dist/components/primitives/FileUpload.d.ts.map +1 -0
  75. package/dist/components/primitives/FileUpload.js +16 -0
  76. package/dist/components/primitives/InputBox.d.ts +34 -0
  77. package/dist/components/primitives/InputBox.d.ts.map +1 -0
  78. package/dist/components/primitives/InputBox.js +25 -0
  79. package/dist/components/primitives/Lottie.d.ts +11 -0
  80. package/dist/components/primitives/Lottie.d.ts.map +1 -0
  81. package/dist/components/primitives/Lottie.js +5 -0
  82. package/dist/components/primitives/MediaEmbed.d.ts +13 -0
  83. package/dist/components/primitives/MediaEmbed.d.ts.map +1 -0
  84. package/dist/components/primitives/MediaEmbed.js +6 -0
  85. package/dist/components/primitives/MediaImage.d.ts +8 -0
  86. package/dist/components/primitives/MediaImage.d.ts.map +1 -0
  87. package/dist/components/primitives/MediaImage.js +5 -0
  88. package/dist/components/primitives/OptionSelector.d.ts +35 -0
  89. package/dist/components/primitives/OptionSelector.d.ts.map +1 -0
  90. package/dist/components/primitives/OptionSelector.js +8 -0
  91. package/dist/components/primitives/PaypalCheckout.d.ts +25 -0
  92. package/dist/components/primitives/PaypalCheckout.d.ts.map +1 -0
  93. package/dist/components/primitives/PaypalCheckout.js +5 -0
  94. package/dist/components/primitives/PlainText.d.ts +6 -0
  95. package/dist/components/primitives/PlainText.d.ts.map +1 -0
  96. package/dist/components/primitives/PlainText.js +5 -0
  97. package/dist/components/primitives/ProgressBar.d.ts +15 -0
  98. package/dist/components/primitives/ProgressBar.d.ts.map +1 -0
  99. package/dist/components/primitives/ProgressBar.js +5 -0
  100. package/dist/components/primitives/RichText.d.ts +6 -0
  101. package/dist/components/primitives/RichText.d.ts.map +1 -0
  102. package/dist/components/primitives/RichText.js +5 -0
  103. package/dist/components/primitives/RichTextMarkdown.d.ts +6 -0
  104. package/dist/components/primitives/RichTextMarkdown.d.ts.map +1 -0
  105. package/dist/components/primitives/RichTextMarkdown.js +5 -0
  106. package/dist/components/primitives/Rive.d.ts +16 -0
  107. package/dist/components/primitives/Rive.d.ts.map +1 -0
  108. package/dist/components/primitives/Rive.js +8 -0
  109. package/dist/components/primitives/StripeCheckout.d.ts +52 -0
  110. package/dist/components/primitives/StripeCheckout.d.ts.map +1 -0
  111. package/dist/components/primitives/StripeCheckout.js +5 -0
  112. package/dist/components/primitives/StripeCheckout2.d.ts +30 -0
  113. package/dist/components/primitives/StripeCheckout2.d.ts.map +1 -0
  114. package/dist/components/primitives/StripeCheckout2.js +7 -0
  115. package/dist/proxy/injectApiInterceptor.d.ts +6 -0
  116. package/dist/proxy/injectApiInterceptor.d.ts.map +1 -0
  117. package/dist/proxy/injectApiInterceptor.js +66 -0
  118. package/dist/proxy/injectReload.d.ts +2 -0
  119. package/dist/proxy/injectReload.d.ts.map +1 -0
  120. package/dist/proxy/injectReload.js +14 -0
  121. package/dist/proxy/injectWorkbench.d.ts +4 -0
  122. package/dist/proxy/injectWorkbench.d.ts.map +1 -0
  123. package/dist/proxy/injectWorkbench.js +16 -0
  124. package/dist/proxy/server.d.ts +11 -0
  125. package/dist/proxy/server.d.ts.map +1 -0
  126. package/dist/proxy/server.js +246 -0
  127. package/dist/proxy/sse.d.ts +5 -0
  128. package/dist/proxy/sse.d.ts.map +1 -0
  129. package/dist/proxy/sse.js +17 -0
  130. package/dist/types-builder.d.ts +800 -0
  131. package/dist/types-builder.d.ts.map +1 -0
  132. package/dist/types-builder.js +20 -0
  133. package/dist/workbench/ActionsPanel.d.ts +6 -0
  134. package/dist/workbench/ActionsPanel.d.ts.map +1 -0
  135. package/dist/workbench/ActionsPanel.js +47 -0
  136. package/dist/workbench/AutofillPanel.d.ts +6 -0
  137. package/dist/workbench/AutofillPanel.d.ts.map +1 -0
  138. package/dist/workbench/AutofillPanel.js +543 -0
  139. package/dist/workbench/ComputedFieldsPanel.d.ts +6 -0
  140. package/dist/workbench/ComputedFieldsPanel.d.ts.map +1 -0
  141. package/dist/workbench/ComputedFieldsPanel.js +31 -0
  142. package/dist/workbench/ExperimentsPanel.d.ts +6 -0
  143. package/dist/workbench/ExperimentsPanel.d.ts.map +1 -0
  144. package/dist/workbench/ExperimentsPanel.js +182 -0
  145. package/dist/workbench/FieldEditorPanel.d.ts +9 -0
  146. package/dist/workbench/FieldEditorPanel.d.ts.map +1 -0
  147. package/dist/workbench/FieldEditorPanel.js +650 -0
  148. package/dist/workbench/InspectorPanel.d.ts +6 -0
  149. package/dist/workbench/InspectorPanel.d.ts.map +1 -0
  150. package/dist/workbench/InspectorPanel.js +341 -0
  151. package/dist/workbench/PageNavigator.d.ts +6 -0
  152. package/dist/workbench/PageNavigator.d.ts.map +1 -0
  153. package/dist/workbench/PageNavigator.js +123 -0
  154. package/dist/workbench/SchemaPanel.d.ts +6 -0
  155. package/dist/workbench/SchemaPanel.d.ts.map +1 -0
  156. package/dist/workbench/SchemaPanel.js +222 -0
  157. package/dist/workbench/UserDataPanel.d.ts +6 -0
  158. package/dist/workbench/UserDataPanel.d.ts.map +1 -0
  159. package/dist/workbench/UserDataPanel.js +350 -0
  160. package/dist/workbench/WorkbenchApp.d.ts +6 -0
  161. package/dist/workbench/WorkbenchApp.d.ts.map +1 -0
  162. package/dist/workbench/WorkbenchApp.js +193 -0
  163. package/dist/workbench/cloudflare-worker/README.md +31 -0
  164. package/dist/workbench/cloudflare-worker/public/workbench.css +1614 -0
  165. package/dist/workbench/cloudflare-worker/public/workbench.js +77 -0
  166. package/dist/workbench/cloudflare-worker/worker.js +40 -0
  167. package/dist/workbench/cloudflare-worker/wrangler.toml +10 -0
  168. package/dist/workbench/index.d.ts +9 -0
  169. package/dist/workbench/index.d.ts.map +1 -0
  170. package/dist/workbench/index.js +44 -0
  171. package/dist/workbench/workbench.css +1614 -0
  172. package/dist/workbench/workbench.js +77 -0
  173. package/package.json +79 -0
@@ -0,0 +1,934 @@
1
+ import fg from 'fast-glob';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { parsePageFromFile } from './parsePage.js';
5
+ import { CompileError } from './errors.js';
6
+ import CSSJSON from 'cssjson';
7
+ import { sanitizeFileName } from './reverse.js';
8
+ import { generateRandomIdByType } from './helpers/duplicateIds.js';
9
+ import { parse } from '@babel/parser';
10
+ import traverseImport from '@babel/traverse';
11
+ import * as generator from '@babel/generator';
12
+ import * as t from '@babel/types';
13
+ import { ALLOWED_PRIMITIVES } from './registry.js';
14
+ const traverse = (traverseImport.default ?? traverseImport);
15
+ const generate = generator.default.generate;
16
+ /**
17
+ * Deep clones an object or array while preserving the order of all properties
18
+ * at all nesting levels. Arrays preserve element order (already guaranteed),
19
+ * and objects preserve property insertion order.
20
+ */
21
+ function deepClonePreservingOrder(value) {
22
+ if (value === null || value === undefined) {
23
+ return value;
24
+ }
25
+ if (Array.isArray(value)) {
26
+ // For arrays, recursively clone each element
27
+ return value.map((item) => deepClonePreservingOrder(item));
28
+ }
29
+ if (typeof value === 'object') {
30
+ // For objects, preserve property order by iterating through keys
31
+ const cloned = {};
32
+ const keys = Object.keys(value);
33
+ for (const key of keys) {
34
+ cloned[key] = deepClonePreservingOrder(value[key]);
35
+ }
36
+ return cloned;
37
+ }
38
+ // Primitive values (string, number, boolean) are returned as-is
39
+ return value;
40
+ }
41
+ /**
42
+ * Returns a copy of newVal with object key order (recursively) matching oldVal
43
+ * where possible. Keeps existing keys in old order; appends new keys at the end.
44
+ * Arrays are not reordered; we recurse into elements by index for nested objects.
45
+ * Used so embeddable.json diffs show only actual changes, not reordering noise.
46
+ */
47
+ export function reorderObjectKeysToMatch(oldVal, newVal) {
48
+ if (newVal === null || newVal === undefined) {
49
+ return newVal;
50
+ }
51
+ if (Array.isArray(newVal)) {
52
+ const oldArr = Array.isArray(oldVal) ? oldVal : [];
53
+ // If array contains objects with 'id' properties, match by id instead of index
54
+ // This handles cases where components are removed (e.g., invalid parent_id)
55
+ if (newVal.length > 0 &&
56
+ typeof newVal[0] === 'object' &&
57
+ newVal[0] !== null &&
58
+ 'id' in newVal[0]) {
59
+ // Build a map of old items by id for efficient lookup
60
+ const oldById = new Map();
61
+ for (const oldItem of oldArr) {
62
+ if (typeof oldItem === 'object' && oldItem !== null && 'id' in oldItem) {
63
+ oldById.set(oldItem.id, oldItem);
64
+ }
65
+ }
66
+ // Match new items with old items by id, fallback to index if id not found
67
+ return newVal.map((newItem, i) => {
68
+ if (typeof newItem === 'object' && newItem !== null && 'id' in newItem) {
69
+ const id = newItem.id;
70
+ const oldItem = oldById.get(id);
71
+ if (oldItem !== undefined) {
72
+ return reorderObjectKeysToMatch(oldItem, newItem);
73
+ }
74
+ }
75
+ // Fallback to index-based matching if no id or id not found
76
+ return reorderObjectKeysToMatch(oldArr[i], newItem);
77
+ });
78
+ }
79
+ // For arrays without id-based objects, use index-based matching
80
+ return newVal.map((item, i) => reorderObjectKeysToMatch(oldArr[i], item));
81
+ }
82
+ if (typeof newVal === 'object' && newVal !== null) {
83
+ const oldObj = oldVal !== null && typeof oldVal === 'object' && !Array.isArray(oldVal)
84
+ ? oldVal
85
+ : {};
86
+ const newObj = newVal;
87
+ const oldKeys = Object.keys(oldObj);
88
+ const newKeysSet = new Set(Object.keys(newObj));
89
+ const orderedKeys = [
90
+ ...oldKeys.filter((k) => newKeysSet.has(k)),
91
+ ...Object.keys(newObj).filter((k) => !oldKeys.includes(k)),
92
+ ];
93
+ const result = {};
94
+ for (const k of orderedKeys) {
95
+ result[k] = reorderObjectKeysToMatch(oldObj[k], newObj[k]);
96
+ }
97
+ return result;
98
+ }
99
+ return newVal;
100
+ }
101
+ export async function compileAllPages(opts) {
102
+ const files = await fg(opts.pagesGlob, { dot: false });
103
+ if (files.length === 0) {
104
+ throw new CompileError(`No pages found for glob: ${opts.pagesGlob}`);
105
+ }
106
+ // First pass: collect all IDs from pages and detect duplicates
107
+ // Note: We check pages first, then global components are checked separately in loadGlobalComponents
108
+ const idOccurrences = new Map();
109
+ const filePages = [];
110
+ // Parse all page files first to collect IDs
111
+ for (const file of files) {
112
+ const code = fs.readFileSync(file, 'utf8');
113
+ const pageKey = derivePageKey(file, opts.pageKeyFrom);
114
+ const page = parsePageFromFile({ code, filePath: file, pageKey });
115
+ // Collect component and button IDs with their locations
116
+ for (let compIndex = 0; compIndex < page.components.length; compIndex++) {
117
+ const component = page.components[compIndex];
118
+ const compId = component.id;
119
+ if (!idOccurrences.has(compId)) {
120
+ idOccurrences.set(compId, []);
121
+ }
122
+ idOccurrences.get(compId).push({
123
+ file,
124
+ pageKey,
125
+ componentType: component.type,
126
+ componentIndex: compIndex,
127
+ id: compId,
128
+ });
129
+ // Collect button IDs from OptionSelector components
130
+ if (component.type === 'OptionSelector' && Array.isArray(component.buttons)) {
131
+ for (let btnIndex = 0; btnIndex < component.buttons.length; btnIndex++) {
132
+ const button = component.buttons[btnIndex];
133
+ if (button?.id) {
134
+ const buttonId = button.id;
135
+ if (!idOccurrences.has(buttonId)) {
136
+ idOccurrences.set(buttonId, []);
137
+ }
138
+ idOccurrences.get(buttonId).push({
139
+ file,
140
+ pageKey,
141
+ componentType: component.type,
142
+ componentIndex: compIndex,
143
+ buttonIndex: btnIndex,
144
+ id: buttonId,
145
+ });
146
+ }
147
+ }
148
+ }
149
+ }
150
+ filePages.push({ file, pageKey, page });
151
+ }
152
+ // Detect duplicates and generate fixes
153
+ const idFixes = new Map(); // Maps "file:componentIndex" or "file:componentIndex:buttonIndex" -> newId
154
+ const allIds = new Set();
155
+ // Collect all existing IDs
156
+ for (const occurrences of idOccurrences.values()) {
157
+ for (const occ of occurrences) {
158
+ allIds.add(occ.id);
159
+ }
160
+ }
161
+ // Find duplicates and generate unique replacements
162
+ for (const [id, occurrences] of idOccurrences.entries()) {
163
+ if (occurrences.length > 1) {
164
+ console.warn(`Found duplicate ID "${id}" used ${occurrences.length} times. Fixing...`);
165
+ // Keep first occurrence, fix the rest (type-based: plaintext_xxx, button_xxx, option_xxx)
166
+ for (let i = 1; i < occurrences.length; i++) {
167
+ const occ = occurrences[i];
168
+ const fixKey = occ.buttonIndex !== undefined
169
+ ? `${occ.file}:${occ.componentIndex}:${occ.buttonIndex}`
170
+ : `${occ.file}:${occ.componentIndex}`;
171
+ const isOptionButton = occ.buttonIndex !== undefined;
172
+ const newId = generateRandomIdByType(occ.componentType, isOptionButton, allIds, idFixes.values());
173
+ allIds.add(newId);
174
+ idFixes.set(fixKey, newId);
175
+ console.warn(` → Fixing duplicate ID "${id}" to "${newId}" in ${occ.file} (component ${occ.componentIndex}${occ.buttonIndex !== undefined ? `, button ${occ.buttonIndex}` : ''})`);
176
+ }
177
+ }
178
+ }
179
+ // Apply fixes to source files if any duplicates were found
180
+ if (idFixes.size > 0) {
181
+ // Fix page files
182
+ for (const { file } of filePages) {
183
+ const fixesForFile = Array.from(idFixes.entries()).filter(([key]) => key.startsWith(file + ':'));
184
+ if (fixesForFile.length > 0) {
185
+ const code = fs.readFileSync(file, 'utf8');
186
+ const updatedCode = fixIdsInSourceFile(code, file, fixesForFile);
187
+ fs.writeFileSync(file, updatedCode, 'utf8');
188
+ console.log(` ✓ Updated ${file} with fixed IDs`);
189
+ }
190
+ }
191
+ }
192
+ // Second pass: re-parse all files (now with fixed IDs) and compile
193
+ // If no duplicates were found, reuse the already-parsed pages
194
+ const pages = [];
195
+ const seenIds = new Map();
196
+ // Map of pageKey -> PageJson for applying metadata later
197
+ const pageMap = new Map();
198
+ if (idFixes.size > 0) {
199
+ // Re-parse files if we fixed any duplicates
200
+ for (const { file, pageKey } of filePages) {
201
+ const code = fs.readFileSync(file, 'utf8');
202
+ const page = parsePageFromFile({ code, filePath: file, pageKey });
203
+ // Global ID uniqueness: components + option ids
204
+ for (const c of page.components) {
205
+ checkUniqueId(seenIds, c.id, { file, pageKey });
206
+ if (c.type === 'OptionSelector' && Array.isArray(c.buttons)) {
207
+ for (const b of c.buttons) {
208
+ if (b?.id)
209
+ checkUniqueId(seenIds, b.id, { file, pageKey });
210
+ }
211
+ }
212
+ }
213
+ pages.push(page);
214
+ pageMap.set(pageKey, page);
215
+ }
216
+ }
217
+ else {
218
+ // No duplicates found, reuse already-parsed pages
219
+ for (const filePage of filePages) {
220
+ const { page, pageKey, file } = filePage;
221
+ // Global ID uniqueness: components + option ids
222
+ for (const c of page.components) {
223
+ checkUniqueId(seenIds, c.id, { file, pageKey });
224
+ if (c.type === 'OptionSelector' && Array.isArray(c.buttons)) {
225
+ for (const b of c.buttons) {
226
+ if (b?.id)
227
+ checkUniqueId(seenIds, b.id, { file, pageKey });
228
+ }
229
+ }
230
+ }
231
+ pages.push(page);
232
+ pageMap.set(pageKey, page);
233
+ }
234
+ }
235
+ // Read config.json if it exists
236
+ let config = null;
237
+ if (opts.configPath && fs.existsSync(opts.configPath)) {
238
+ try {
239
+ const configContent = fs.readFileSync(opts.configPath, 'utf8');
240
+ config = JSON.parse(configContent);
241
+ }
242
+ catch (error) {
243
+ console.warn(`Failed to parse config.json: ${error}`);
244
+ }
245
+ }
246
+ else if (opts.embeddableId) {
247
+ // Try to infer config path from embeddableId
248
+ const inferredConfigPath = path.join('embeddables', opts.embeddableId, 'config.json');
249
+ if (fs.existsSync(inferredConfigPath)) {
250
+ try {
251
+ const configContent = fs.readFileSync(inferredConfigPath, 'utf8');
252
+ config = JSON.parse(configContent);
253
+ }
254
+ catch (error) {
255
+ console.warn(`Failed to parse config.json: ${error}`);
256
+ }
257
+ }
258
+ }
259
+ // Apply page ordering and metadata from config
260
+ let orderedPages = pages;
261
+ if (config && config.pages && Array.isArray(config.pages)) {
262
+ // Create a map for quick lookup
263
+ const orderedPageMap = new Map();
264
+ for (const pageMetadata of config.pages) {
265
+ orderedPageMap.set(pageMetadata.key, pageMetadata);
266
+ }
267
+ // Sort pages according to config order
268
+ const orderedPageKeys = config.pages.map((p) => p.key);
269
+ const unorderedPages = [];
270
+ orderedPages = orderedPageKeys
271
+ .map((key) => {
272
+ const page = pageMap.get(key);
273
+ if (page) {
274
+ // Apply metadata from config (preserving nested object order)
275
+ const pageMetadata = orderedPageMap.get(key);
276
+ if (pageMetadata) {
277
+ // Merge metadata (excluding key and components)
278
+ // Preserve order by iterating through keys in original order
279
+ const pageMetadataKeys = Object.keys(pageMetadata);
280
+ for (const metaKey of pageMetadataKeys) {
281
+ if (metaKey !== 'key' && metaKey !== 'components') {
282
+ ;
283
+ page[metaKey] = deepClonePreservingOrder(pageMetadata[metaKey]);
284
+ }
285
+ }
286
+ }
287
+ return page;
288
+ }
289
+ return null;
290
+ })
291
+ .filter((p) => p !== null);
292
+ // Add any pages not in the config order
293
+ for (const page of pages) {
294
+ if (!orderedPageKeys.includes(page.key)) {
295
+ unorderedPages.push(page);
296
+ }
297
+ }
298
+ orderedPages = [...orderedPages, ...unorderedPages];
299
+ }
300
+ // Compile styles from CSS files
301
+ const styles = await compileStyles(opts.stylesDir || 'styles');
302
+ // Build embeddable object preserving the order from config
303
+ const embeddable = {};
304
+ // Read computedFields from JS files
305
+ let computedFields = [];
306
+ if (opts.embeddableId) {
307
+ computedFields = await loadComputedFields(opts.embeddableId);
308
+ }
309
+ // Read dataOutputs from JS files
310
+ let dataOutputs = [];
311
+ if (opts.embeddableId) {
312
+ dataOutputs = await loadDataOutputs(opts.embeddableId, config);
313
+ }
314
+ // Read global components from TSX files
315
+ let globalComponents = [];
316
+ if (opts.embeddableId) {
317
+ globalComponents = await loadGlobalComponents(opts.embeddableId, config);
318
+ }
319
+ // Preserve the order of top-level properties from config
320
+ if (config) {
321
+ const configKeys = Object.keys(config);
322
+ // Iterate through config keys in order to preserve property order
323
+ for (const key of configKeys) {
324
+ if (key === 'pages') {
325
+ // Replace pages with orderedPages (which include components)
326
+ embeddable.pages = orderedPages;
327
+ }
328
+ else if (key === 'styles') {
329
+ // Skip styles from config - they will be added from compiled CSS files later
330
+ continue;
331
+ }
332
+ else if (key === 'computedFields') {
333
+ // Replace computedFields with loaded computedFields (which include code)
334
+ if (computedFields.length > 0) {
335
+ embeddable.computedFields = computedFields;
336
+ }
337
+ }
338
+ else if (key === 'dataOutputs') {
339
+ // Replace dataOutputs with loaded dataOutputs (which include code)
340
+ if (dataOutputs.length > 0) {
341
+ embeddable.dataOutputs = dataOutputs;
342
+ }
343
+ }
344
+ else if (key === 'components') {
345
+ // Replace components with loaded global components
346
+ if (globalComponents.length > 0) {
347
+ embeddable.components = globalComponents;
348
+ }
349
+ }
350
+ else {
351
+ // Regular metadata property - preserve as-is, with order preserved recursively
352
+ const value = config[key];
353
+ if (value !== undefined && value !== null) {
354
+ embeddable[key] = deepClonePreservingOrder(value);
355
+ }
356
+ }
357
+ }
358
+ }
359
+ // If no config or pages not in config, add pages at the beginning
360
+ if (!embeddable.pages) {
361
+ embeddable.pages = orderedPages;
362
+ }
363
+ // If styles exist, insert them after pages (since styles are not in config.json)
364
+ // This preserves the logical grouping while maintaining order for other properties
365
+ if (Object.keys(styles).length > 0) {
366
+ const embeddableKeys = Object.keys(embeddable);
367
+ const pagesIndex = embeddableKeys.indexOf('pages');
368
+ if (pagesIndex !== -1) {
369
+ // Insert styles after pages by rebuilding the object
370
+ const newEmbeddable = {};
371
+ let stylesInserted = false;
372
+ for (const key of embeddableKeys) {
373
+ newEmbeddable[key] = embeddable[key];
374
+ if (key === 'pages' && !stylesInserted) {
375
+ newEmbeddable.styles = styles;
376
+ stylesInserted = true;
377
+ }
378
+ }
379
+ // If pages was the last key, add styles after it
380
+ if (!stylesInserted && pagesIndex === embeddableKeys.length - 1) {
381
+ newEmbeddable.styles = styles;
382
+ }
383
+ // Rebuild embeddable in the correct order by clearing and rebuilding
384
+ Object.keys(embeddable).forEach((key) => delete embeddable[key]);
385
+ Object.assign(embeddable, newEmbeddable);
386
+ }
387
+ else {
388
+ // Pages doesn't exist in embeddable, add styles at the end
389
+ embeddable.styles = styles;
390
+ }
391
+ }
392
+ // If computedFields or dataOutputs don't exist in config but we have them, add them at the end
393
+ if (computedFields.length > 0 && !embeddable.computedFields) {
394
+ embeddable.computedFields = computedFields;
395
+ }
396
+ if (dataOutputs.length > 0 && !embeddable.dataOutputs) {
397
+ embeddable.dataOutputs = dataOutputs;
398
+ }
399
+ if (globalComponents.length > 0 && !embeddable.components) {
400
+ embeddable.components = globalComponents;
401
+ }
402
+ let toWrite = embeddable;
403
+ if (fs.existsSync(opts.outPath)) {
404
+ try {
405
+ const existing = fs.readFileSync(opts.outPath, 'utf8');
406
+ const oldEmbeddable = JSON.parse(existing);
407
+ toWrite = reorderObjectKeysToMatch(oldEmbeddable, embeddable);
408
+ }
409
+ catch {
410
+ // If read/parse fails, keep embeddable as-is
411
+ }
412
+ }
413
+ writeAtomic(opts.outPath, JSON.stringify(toWrite, null, 2));
414
+ console.log(`Wrote ${opts.outPath} (${orderedPages.length} pages, ${Object.keys(styles).length > 0 ? 'with styles' : 'no styles'})`);
415
+ }
416
+ function derivePageKey(file, mode) {
417
+ if (mode === 'filename') {
418
+ const base = path.basename(file);
419
+ // email.page.tsx -> email
420
+ return base.replace(/\.page\.tsx$/, '').replace(/\.tsx$/, '');
421
+ }
422
+ // v1: export-based not implemented here
423
+ return path
424
+ .basename(file)
425
+ .replace(/\.page\.tsx$/, '')
426
+ .replace(/\.tsx$/, '');
427
+ }
428
+ /**
429
+ * Fixes duplicate IDs in a React source file by updating JSX attributes.
430
+ * @param code Original source code
431
+ * @param filePath File path (for error reporting)
432
+ * @param fixes Array of [fixKey, newId] tuples where fixKey is "file:componentIndex" or "file:componentIndex:buttonIndex"
433
+ */
434
+ function fixIdsInSourceFile(code, filePath, fixes) {
435
+ // Parse the file
436
+ let ast;
437
+ try {
438
+ ast = parse(code, {
439
+ sourceType: 'module',
440
+ plugins: ['typescript', 'jsx'],
441
+ sourceFilename: filePath,
442
+ });
443
+ }
444
+ catch (error) {
445
+ throw new CompileError(`Failed to parse file for ID fixing: ${error.message}`, {
446
+ file: filePath,
447
+ });
448
+ }
449
+ // Build a map of component index -> new ID, and component index + button index -> new ID
450
+ const componentIdMap = new Map();
451
+ const buttonIdMap = new Map(); // "componentIndex:buttonIndex" -> newId
452
+ for (const [fixKey, newId] of fixes) {
453
+ // fixKey format is "file:componentIndex" or "file:componentIndex:buttonIndex"
454
+ // Since we filtered by file prefix, remove the file prefix to get just the indices
455
+ const filePrefix = filePath + ':';
456
+ if (!fixKey.startsWith(filePrefix)) {
457
+ // This shouldn't happen since we filtered, but skip just in case
458
+ continue;
459
+ }
460
+ const indicesStr = fixKey.substring(filePrefix.length);
461
+ const parts = indicesStr.split(':');
462
+ if (parts.length === 1) {
463
+ // Component ID fix: "componentIndex"
464
+ const componentIndex = parseInt(parts[0], 10);
465
+ componentIdMap.set(componentIndex, newId);
466
+ }
467
+ else if (parts.length === 2) {
468
+ // Button ID fix: "componentIndex:buttonIndex"
469
+ const componentIndex = parseInt(parts[0], 10);
470
+ const buttonIndex = parseInt(parts[1], 10);
471
+ buttonIdMap.set(`${componentIndex}:${buttonIndex}`, newId);
472
+ }
473
+ }
474
+ let componentIndex = -1;
475
+ // Traverse AST to find and update IDs
476
+ traverse(ast, {
477
+ JSXElement(path) {
478
+ const opening = path.node.openingElement;
479
+ const tagName = opening.name.type === 'JSXIdentifier' ? opening.name.name : null;
480
+ // Only process allowed primitives (components), not regular HTML elements
481
+ if (!tagName || !ALLOWED_PRIMITIVES.has(tagName))
482
+ return;
483
+ // Check if this component has an id attribute
484
+ const hasIdAttr = opening.attributes.some((attr) => attr.type === 'JSXAttribute' &&
485
+ attr.name.type === 'JSXIdentifier' &&
486
+ attr.name.name === 'id');
487
+ if (hasIdAttr) {
488
+ componentIndex++;
489
+ // Check if we need to fix this component's ID
490
+ if (componentIdMap.has(componentIndex)) {
491
+ const newId = componentIdMap.get(componentIndex);
492
+ // Find and update the id attribute
493
+ for (const attr of opening.attributes) {
494
+ if (attr.type === 'JSXAttribute' &&
495
+ attr.name.type === 'JSXIdentifier' &&
496
+ attr.name.name === 'id') {
497
+ if (attr.value?.type === 'StringLiteral') {
498
+ attr.value.value = newId;
499
+ }
500
+ else if (attr.value?.type === 'JSXExpressionContainer') {
501
+ // Replace expression with string literal
502
+ attr.value = t.stringLiteral(newId);
503
+ }
504
+ break;
505
+ }
506
+ }
507
+ }
508
+ // Check if this is an OptionSelector and we need to fix button IDs
509
+ if (tagName === 'OptionSelector') {
510
+ // Find the buttons prop
511
+ for (const attr of opening.attributes) {
512
+ if (attr.type === 'JSXAttribute' &&
513
+ attr.name.type === 'JSXIdentifier' &&
514
+ attr.name.name === 'buttons' &&
515
+ attr.value?.type === 'JSXExpressionContainer') {
516
+ // The buttons prop is an array expression
517
+ const expr = attr.value.expression;
518
+ if (expr.type === 'ArrayExpression') {
519
+ let buttonIndex = 0;
520
+ for (const element of expr.elements) {
521
+ if (element &&
522
+ element.type === 'ObjectExpression' &&
523
+ buttonIdMap.has(`${componentIndex}:${buttonIndex}`)) {
524
+ const newButtonId = buttonIdMap.get(`${componentIndex}:${buttonIndex}`);
525
+ // Find the id property in this button object
526
+ for (const prop of element.properties) {
527
+ if (prop.type === 'ObjectProperty' &&
528
+ prop.key.type === 'Identifier' &&
529
+ prop.key.name === 'id') {
530
+ if (prop.value.type === 'StringLiteral') {
531
+ prop.value.value = newButtonId;
532
+ }
533
+ else {
534
+ prop.value = t.stringLiteral(newButtonId);
535
+ }
536
+ break;
537
+ }
538
+ }
539
+ }
540
+ buttonIndex++;
541
+ }
542
+ }
543
+ break;
544
+ }
545
+ }
546
+ }
547
+ }
548
+ },
549
+ });
550
+ // Generate updated code
551
+ const result = generate(ast, {}, code);
552
+ return result.code;
553
+ }
554
+ function checkUniqueId(seen, id, loc) {
555
+ const prev = seen.get(id);
556
+ if (prev) {
557
+ 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 });
558
+ }
559
+ seen.set(id, loc);
560
+ }
561
+ async function compileStyles(stylesDir) {
562
+ const stylesPath = path.resolve(stylesDir);
563
+ // Check if styles directory exists
564
+ if (!fs.existsSync(stylesPath)) {
565
+ return {};
566
+ }
567
+ // Find all CSS files in the styles directory
568
+ const cssFiles = await fg('**/*.css', {
569
+ cwd: stylesPath,
570
+ dot: false,
571
+ });
572
+ if (cssFiles.length === 0) {
573
+ return {};
574
+ }
575
+ // Combine all CSS files into a single JSON object
576
+ const combinedStyles = {};
577
+ for (const cssFile of cssFiles) {
578
+ const fullPath = path.join(stylesPath, cssFile);
579
+ const cssContent = fs.readFileSync(fullPath, 'utf8');
580
+ // Skip empty files
581
+ if (!cssContent.trim()) {
582
+ continue;
583
+ }
584
+ // Convert CSS to JSON
585
+ const cssJson = CSSJSON.toJSON(cssContent);
586
+ // Flatten the structure: extract selectors and their attributes
587
+ // cssjson returns { children: { selector: { children: {}, attributes: {...} } } }
588
+ if (cssJson && cssJson.children) {
589
+ for (const [selector, rule] of Object.entries(cssJson.children)) {
590
+ if (rule && typeof rule === 'object' && 'attributes' in rule) {
591
+ // Extract just the attributes (styles) for each selector
592
+ combinedStyles[selector] = rule.attributes || {};
593
+ }
594
+ }
595
+ }
596
+ }
597
+ return combinedStyles;
598
+ }
599
+ function writeAtomic(outPath, content) {
600
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
601
+ const tmp = `${outPath}.tmp`;
602
+ fs.writeFileSync(tmp, content, 'utf8');
603
+ fs.renameSync(tmp, outPath);
604
+ }
605
+ /**
606
+ * Loads computedFields from JS files in computed-fields/ folder.
607
+ * Each JS file corresponds to one computedField.
608
+ */
609
+ async function loadComputedFields(embeddableId) {
610
+ const computedFieldsDir = path.join('embeddables', embeddableId, 'computed-fields');
611
+ if (!fs.existsSync(computedFieldsDir)) {
612
+ return [];
613
+ }
614
+ // Find all JS files in computed-fields directory
615
+ const jsFiles = await fg('**/*.js', {
616
+ cwd: computedFieldsDir,
617
+ dot: false,
618
+ });
619
+ if (jsFiles.length === 0) {
620
+ return [];
621
+ }
622
+ const computedFields = [];
623
+ // Read config.json to get computedField metadata (if available)
624
+ const configPath = path.join('embeddables', embeddableId, 'config.json');
625
+ let config = null;
626
+ if (fs.existsSync(configPath)) {
627
+ try {
628
+ const configContent = fs.readFileSync(configPath, 'utf8');
629
+ config = JSON.parse(configContent);
630
+ }
631
+ catch (error) {
632
+ // Ignore config parsing errors
633
+ }
634
+ }
635
+ // Build a map of computed fields by key/id for quick lookup
636
+ const computedFieldsMap = new Map();
637
+ for (const jsFile of jsFiles) {
638
+ const filePath = path.join(computedFieldsDir, jsFile);
639
+ const code = fs.readFileSync(filePath, 'utf8');
640
+ // Extract key from filename (remove .js extension)
641
+ const key = path.basename(jsFile, '.js');
642
+ // Build computedField object
643
+ const computedField = {
644
+ id: key, // Use key as id if not specified in config
645
+ key,
646
+ formula: 'custom',
647
+ code,
648
+ };
649
+ // If config has computedFields metadata, try to find matching field
650
+ if (config && config.computedFields) {
651
+ const configField = config.computedFields.find((cf) => cf.key === key || cf.id === key);
652
+ if (configField) {
653
+ // Merge metadata from config (but keep code from file)
654
+ // Preserve order by cloning the config field first
655
+ const clonedConfigField = deepClonePreservingOrder(configField);
656
+ Object.assign(computedField, clonedConfigField);
657
+ computedField.code = code; // Ensure code comes from file
658
+ }
659
+ }
660
+ // Store in map using both key and id as lookup keys
661
+ computedFieldsMap.set(key, computedField);
662
+ if (computedField.id && computedField.id !== key) {
663
+ computedFieldsMap.set(computedField.id, computedField);
664
+ }
665
+ }
666
+ // If config has computedFields, preserve order from config
667
+ if (config && config.computedFields && Array.isArray(config.computedFields)) {
668
+ const orderedFields = [];
669
+ const processedKeys = new Set();
670
+ // First, add fields in config order
671
+ for (const configField of config.computedFields) {
672
+ const lookupKey = configField.key || configField.id;
673
+ if (lookupKey) {
674
+ const field = computedFieldsMap.get(lookupKey);
675
+ if (field) {
676
+ orderedFields.push(field);
677
+ processedKeys.add(lookupKey);
678
+ if (field.id && field.id !== lookupKey) {
679
+ processedKeys.add(field.id);
680
+ }
681
+ }
682
+ }
683
+ }
684
+ // Then, add any fields not in config (in filesystem order)
685
+ for (const jsFile of jsFiles) {
686
+ const key = path.basename(jsFile, '.js');
687
+ if (!processedKeys.has(key)) {
688
+ const field = computedFieldsMap.get(key);
689
+ if (field) {
690
+ orderedFields.push(field);
691
+ processedKeys.add(key);
692
+ }
693
+ }
694
+ }
695
+ return orderedFields;
696
+ }
697
+ // No config or no computedFields in config, return in filesystem order
698
+ // Use a Set to deduplicate since we may have multiple keys pointing to the same field
699
+ const seenFields = new Set();
700
+ const orderedFields = [];
701
+ for (const jsFile of jsFiles) {
702
+ const key = path.basename(jsFile, '.js');
703
+ const field = computedFieldsMap.get(key);
704
+ if (field && !seenFields.has(field)) {
705
+ orderedFields.push(field);
706
+ seenFields.add(field);
707
+ }
708
+ }
709
+ return orderedFields;
710
+ }
711
+ /**
712
+ * Loads dataOutputs (actions) from JS files in actions/ folder.
713
+ * Each JS file corresponds to one action.
714
+ */
715
+ async function loadDataOutputs(embeddableId, config = null) {
716
+ const actionsDir = path.join('embeddables', embeddableId, 'actions');
717
+ if (!fs.existsSync(actionsDir)) {
718
+ return [];
719
+ }
720
+ // Find all JS files in actions directory
721
+ const jsFiles = await fg('**/*.js', {
722
+ cwd: actionsDir,
723
+ dot: false,
724
+ });
725
+ if (jsFiles.length === 0) {
726
+ return [];
727
+ }
728
+ // Use provided config or read it if not provided
729
+ if (!config) {
730
+ const configPath = path.join('embeddables', embeddableId, 'config.json');
731
+ if (fs.existsSync(configPath)) {
732
+ try {
733
+ const configContent = fs.readFileSync(configPath, 'utf8');
734
+ config = JSON.parse(configContent);
735
+ }
736
+ catch (error) {
737
+ // Ignore config parsing errors
738
+ }
739
+ }
740
+ }
741
+ // Build a map of actions by various identifiers for quick lookup
742
+ const dataOutputsMap = new Map();
743
+ for (const jsFile of jsFiles) {
744
+ const filePath = path.join(actionsDir, jsFile);
745
+ const code = fs.readFileSync(filePath, 'utf8');
746
+ // Extract key/name from filename (remove .js extension)
747
+ const identifier = path.basename(jsFile, '.js');
748
+ // Build action object
749
+ const action = {
750
+ id: identifier, // Use identifier as id if not specified
751
+ name: identifier,
752
+ output: 'custom',
753
+ code,
754
+ };
755
+ // If config has dataOutputs metadata, try to find matching action
756
+ // Match by comparing sanitized names since filenames are based on sanitized action names
757
+ if (config && config.dataOutputs) {
758
+ const configAction = config.dataOutputs.find((act) => {
759
+ // Compare sanitized versions since filename is based on sanitized name
760
+ const sanitizedName = act.name ? sanitizeFileName(act.name) : null;
761
+ return act.name === identifier || act.id === identifier || sanitizedName === identifier;
762
+ });
763
+ if (configAction) {
764
+ // Merge metadata from config (but keep code from file)
765
+ // Preserve order by cloning the config action first
766
+ const clonedConfigAction = deepClonePreservingOrder(configAction);
767
+ Object.assign(action, clonedConfigAction);
768
+ action.code = code; // Ensure code comes from file
769
+ // Preserve the original name from config, not the sanitized filename
770
+ if (configAction.name) {
771
+ action.name = configAction.name;
772
+ }
773
+ }
774
+ delete action.key; // name is enough; don't emit key
775
+ }
776
+ // Store in map using identifier (filename) as the primary key
777
+ // Also store by id, key, name, and sanitized name for lookup
778
+ dataOutputsMap.set(identifier, action);
779
+ if (action.id && action.id !== identifier) {
780
+ dataOutputsMap.set(action.id, action);
781
+ }
782
+ if (action.name && action.name !== identifier && action.name !== action.id) {
783
+ const sanitizedName = sanitizeFileName(action.name);
784
+ if (sanitizedName !== identifier) {
785
+ dataOutputsMap.set(sanitizedName, action);
786
+ }
787
+ }
788
+ }
789
+ // If config has dataOutputs, preserve order from config
790
+ if (config && config.dataOutputs && Array.isArray(config.dataOutputs)) {
791
+ const orderedOutputs = [];
792
+ const processedIdentifiers = new Set();
793
+ // First, add actions in config order
794
+ for (const configAction of config.dataOutputs) {
795
+ // Try multiple matching strategies (name is enough; no key)
796
+ let matchedAction;
797
+ const possibleKeys = [
798
+ configAction.id,
799
+ configAction.name,
800
+ configAction.name ? sanitizeFileName(configAction.name) : null,
801
+ ].filter(Boolean);
802
+ for (const key of possibleKeys) {
803
+ const action = dataOutputsMap.get(key);
804
+ if (action && !processedIdentifiers.has(key)) {
805
+ matchedAction = action;
806
+ // Mark all possible identifiers as processed
807
+ processedIdentifiers.add(key);
808
+ if (action.id)
809
+ processedIdentifiers.add(action.id);
810
+ if (action.name) {
811
+ processedIdentifiers.add(action.name);
812
+ processedIdentifiers.add(sanitizeFileName(action.name));
813
+ }
814
+ break;
815
+ }
816
+ }
817
+ if (matchedAction) {
818
+ orderedOutputs.push(matchedAction);
819
+ }
820
+ }
821
+ // Then, add any actions not in config (in filesystem order)
822
+ for (const jsFile of jsFiles) {
823
+ const identifier = path.basename(jsFile, '.js');
824
+ if (!processedIdentifiers.has(identifier)) {
825
+ const action = dataOutputsMap.get(identifier);
826
+ if (action) {
827
+ orderedOutputs.push(action);
828
+ processedIdentifiers.add(identifier);
829
+ if (action.id)
830
+ processedIdentifiers.add(action.id);
831
+ if (action.name) {
832
+ processedIdentifiers.add(action.name);
833
+ processedIdentifiers.add(sanitizeFileName(action.name));
834
+ }
835
+ }
836
+ }
837
+ }
838
+ return orderedOutputs;
839
+ }
840
+ // No config or no dataOutputs in config, return in filesystem order
841
+ // Use a Set to deduplicate since we may have multiple keys pointing to the same action
842
+ const seenActions = new Set();
843
+ const orderedOutputs = [];
844
+ for (const jsFile of jsFiles) {
845
+ const identifier = path.basename(jsFile, '.js');
846
+ const action = dataOutputsMap.get(identifier);
847
+ if (action && !seenActions.has(action)) {
848
+ orderedOutputs.push(action);
849
+ seenActions.add(action);
850
+ }
851
+ }
852
+ return orderedOutputs;
853
+ }
854
+ /**
855
+ * Loads global components from TSX files in global-components/ folder.
856
+ * Each TSX file corresponds to components at a specific location.
857
+ * File naming: <location>.location.tsx (e.g., before_page.location.tsx)
858
+ * The _location is derived from the filename - no need for _location props in TSX or config.
859
+ */
860
+ async function loadGlobalComponents(embeddableId, _config = null // config.components is no longer used - location comes from filename
861
+ ) {
862
+ const globalComponentsDir = path.join('embeddables', embeddableId, 'global-components');
863
+ if (!fs.existsSync(globalComponentsDir)) {
864
+ return [];
865
+ }
866
+ // Find all TSX files in global-components directory
867
+ const tsxFiles = await fg('**/*.location.tsx', {
868
+ cwd: globalComponentsDir,
869
+ dot: false,
870
+ });
871
+ if (tsxFiles.length === 0) {
872
+ return [];
873
+ }
874
+ const allComponents = [];
875
+ const seenIds = new Map();
876
+ // Build a component map for parent resolution
877
+ const componentMap = new Map();
878
+ // Map component ID to file location (derived from filename)
879
+ const componentToFileLocation = new Map();
880
+ for (const tsxFile of tsxFiles) {
881
+ const filePath = path.join(globalComponentsDir, tsxFile);
882
+ const code = fs.readFileSync(filePath, 'utf8');
883
+ // Extract location from filename (e.g., "before_page.location.tsx" -> "before_page")
884
+ const locationMatch = tsxFile.match(/^(.+)\.location\.tsx$/);
885
+ if (!locationMatch) {
886
+ console.warn(`Global component file "${tsxFile}" doesn't match expected pattern <location>.location.tsx`);
887
+ continue;
888
+ }
889
+ const fileLocation = locationMatch[1];
890
+ // Parse components from the file
891
+ const { parseGlobalComponentsFromFile } = await import('./parsePage.js');
892
+ const fileComponents = parseGlobalComponentsFromFile({
893
+ code,
894
+ filePath,
895
+ });
896
+ // Add components to map and validate
897
+ for (const component of fileComponents) {
898
+ // Store file location for this component (derived from filename)
899
+ componentToFileLocation.set(component.id, fileLocation);
900
+ // Global ID uniqueness check (using file location as pageKey for global components)
901
+ checkUniqueId(seenIds, component.id, {
902
+ file: filePath,
903
+ pageKey: `global-${fileLocation}`,
904
+ });
905
+ // Validate OptionSelector buttons
906
+ if (component.type === 'OptionSelector' && Array.isArray(component.buttons)) {
907
+ for (const b of component.buttons) {
908
+ if (b?.id)
909
+ checkUniqueId(seenIds, b.id, {
910
+ file: filePath,
911
+ pageKey: `global-${fileLocation}`,
912
+ });
913
+ }
914
+ }
915
+ componentMap.set(component.id, component);
916
+ }
917
+ }
918
+ // Set _location for root components (no parent_id) based on filename
919
+ for (const [componentId, component] of componentMap) {
920
+ // Remove any _location prop that might have been in the TSX (we derive it from filename)
921
+ delete component._location;
922
+ // Root components (no parent_id) get _location from their filename
923
+ if (!component.parent_id) {
924
+ const fileLocation = componentToFileLocation.get(componentId);
925
+ if (!fileLocation) {
926
+ throw new CompileError(`Global component "${component.id}" (key: "${component.key}") has no parent_id but couldn't determine location from file.`);
927
+ }
928
+ component._location = fileLocation;
929
+ }
930
+ // Child components inherit location from parent - they don't need _location
931
+ allComponents.push(component);
932
+ }
933
+ return allComponents;
934
+ }