@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.
- package/README.md +116 -0
- package/bin/embeddables.mjs +2 -0
- package/dist/auth/index.d.ts +43 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +100 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +75 -0
- package/dist/commands/build-workbench.d.ts +5 -0
- package/dist/commands/build-workbench.d.ts.map +1 -0
- package/dist/commands/build-workbench.js +122 -0
- package/dist/commands/build.d.ts +7 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +22 -0
- package/dist/commands/dev.d.ts +11 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +153 -0
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +112 -0
- package/dist/commands/logout.d.ts +2 -0
- package/dist/commands/logout.d.ts.map +1 -0
- package/dist/commands/logout.js +18 -0
- package/dist/commands/pull.d.ts +7 -0
- package/dist/commands/pull.d.ts.map +1 -0
- package/dist/commands/pull.js +97 -0
- package/dist/compiler/errors.d.ts +20 -0
- package/dist/compiler/errors.d.ts.map +1 -0
- package/dist/compiler/errors.js +35 -0
- package/dist/compiler/evalStatic.d.ts +3 -0
- package/dist/compiler/evalStatic.d.ts.map +1 -0
- package/dist/compiler/evalStatic.js +57 -0
- package/dist/compiler/flatten.js +1 -0
- package/dist/compiler/helpers/duplicateIds.d.ts +9 -0
- package/dist/compiler/helpers/duplicateIds.d.ts.map +1 -0
- package/dist/compiler/helpers/duplicateIds.js +71 -0
- package/dist/compiler/index.d.ts +16 -0
- package/dist/compiler/index.d.ts.map +1 -0
- package/dist/compiler/index.js +934 -0
- package/dist/compiler/parsePage.d.ts +15 -0
- package/dist/compiler/parsePage.d.ts.map +1 -0
- package/dist/compiler/parsePage.js +562 -0
- package/dist/compiler/registry.d.ts +4 -0
- package/dist/compiler/registry.d.ts.map +1 -0
- package/dist/compiler/registry.js +44 -0
- package/dist/compiler/reverse.d.ts +17 -0
- package/dist/compiler/reverse.d.ts.map +1 -0
- package/dist/compiler/reverse.js +1632 -0
- package/dist/compiler/types.d.ts +21 -0
- package/dist/compiler/types.d.ts.map +1 -0
- package/dist/compiler/types.js +1 -0
- package/dist/components/index.d.ts +21 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +21 -0
- package/dist/components/primitives/BaseComponent.d.ts +32 -0
- package/dist/components/primitives/BaseComponent.d.ts.map +1 -0
- package/dist/components/primitives/BaseComponent.js +26 -0
- package/dist/components/primitives/BookMeeting.d.ts +18 -0
- package/dist/components/primitives/BookMeeting.d.ts.map +1 -0
- package/dist/components/primitives/BookMeeting.js +5 -0
- package/dist/components/primitives/Chart.d.ts +41 -0
- package/dist/components/primitives/Chart.d.ts.map +1 -0
- package/dist/components/primitives/Chart.js +5 -0
- package/dist/components/primitives/Container.d.ts +8 -0
- package/dist/components/primitives/Container.d.ts.map +1 -0
- package/dist/components/primitives/Container.js +5 -0
- package/dist/components/primitives/CustomButton.d.ts +37 -0
- package/dist/components/primitives/CustomButton.d.ts.map +1 -0
- package/dist/components/primitives/CustomButton.js +10 -0
- package/dist/components/primitives/CustomHTML.d.ts +8 -0
- package/dist/components/primitives/CustomHTML.d.ts.map +1 -0
- package/dist/components/primitives/CustomHTML.js +5 -0
- package/dist/components/primitives/FileUpload.d.ts +18 -0
- package/dist/components/primitives/FileUpload.d.ts.map +1 -0
- package/dist/components/primitives/FileUpload.js +16 -0
- package/dist/components/primitives/InputBox.d.ts +34 -0
- package/dist/components/primitives/InputBox.d.ts.map +1 -0
- package/dist/components/primitives/InputBox.js +25 -0
- package/dist/components/primitives/Lottie.d.ts +11 -0
- package/dist/components/primitives/Lottie.d.ts.map +1 -0
- package/dist/components/primitives/Lottie.js +5 -0
- package/dist/components/primitives/MediaEmbed.d.ts +13 -0
- package/dist/components/primitives/MediaEmbed.d.ts.map +1 -0
- package/dist/components/primitives/MediaEmbed.js +6 -0
- package/dist/components/primitives/MediaImage.d.ts +8 -0
- package/dist/components/primitives/MediaImage.d.ts.map +1 -0
- package/dist/components/primitives/MediaImage.js +5 -0
- package/dist/components/primitives/OptionSelector.d.ts +35 -0
- package/dist/components/primitives/OptionSelector.d.ts.map +1 -0
- package/dist/components/primitives/OptionSelector.js +8 -0
- package/dist/components/primitives/PaypalCheckout.d.ts +25 -0
- package/dist/components/primitives/PaypalCheckout.d.ts.map +1 -0
- package/dist/components/primitives/PaypalCheckout.js +5 -0
- package/dist/components/primitives/PlainText.d.ts +6 -0
- package/dist/components/primitives/PlainText.d.ts.map +1 -0
- package/dist/components/primitives/PlainText.js +5 -0
- package/dist/components/primitives/ProgressBar.d.ts +15 -0
- package/dist/components/primitives/ProgressBar.d.ts.map +1 -0
- package/dist/components/primitives/ProgressBar.js +5 -0
- package/dist/components/primitives/RichText.d.ts +6 -0
- package/dist/components/primitives/RichText.d.ts.map +1 -0
- package/dist/components/primitives/RichText.js +5 -0
- package/dist/components/primitives/RichTextMarkdown.d.ts +6 -0
- package/dist/components/primitives/RichTextMarkdown.d.ts.map +1 -0
- package/dist/components/primitives/RichTextMarkdown.js +5 -0
- package/dist/components/primitives/Rive.d.ts +16 -0
- package/dist/components/primitives/Rive.d.ts.map +1 -0
- package/dist/components/primitives/Rive.js +8 -0
- package/dist/components/primitives/StripeCheckout.d.ts +52 -0
- package/dist/components/primitives/StripeCheckout.d.ts.map +1 -0
- package/dist/components/primitives/StripeCheckout.js +5 -0
- package/dist/components/primitives/StripeCheckout2.d.ts +30 -0
- package/dist/components/primitives/StripeCheckout2.d.ts.map +1 -0
- package/dist/components/primitives/StripeCheckout2.js +7 -0
- package/dist/proxy/injectApiInterceptor.d.ts +6 -0
- package/dist/proxy/injectApiInterceptor.d.ts.map +1 -0
- package/dist/proxy/injectApiInterceptor.js +66 -0
- package/dist/proxy/injectReload.d.ts +2 -0
- package/dist/proxy/injectReload.d.ts.map +1 -0
- package/dist/proxy/injectReload.js +14 -0
- package/dist/proxy/injectWorkbench.d.ts +4 -0
- package/dist/proxy/injectWorkbench.d.ts.map +1 -0
- package/dist/proxy/injectWorkbench.js +16 -0
- package/dist/proxy/server.d.ts +11 -0
- package/dist/proxy/server.d.ts.map +1 -0
- package/dist/proxy/server.js +246 -0
- package/dist/proxy/sse.d.ts +5 -0
- package/dist/proxy/sse.d.ts.map +1 -0
- package/dist/proxy/sse.js +17 -0
- package/dist/types-builder.d.ts +800 -0
- package/dist/types-builder.d.ts.map +1 -0
- package/dist/types-builder.js +20 -0
- package/dist/workbench/ActionsPanel.d.ts +6 -0
- package/dist/workbench/ActionsPanel.d.ts.map +1 -0
- package/dist/workbench/ActionsPanel.js +47 -0
- package/dist/workbench/AutofillPanel.d.ts +6 -0
- package/dist/workbench/AutofillPanel.d.ts.map +1 -0
- package/dist/workbench/AutofillPanel.js +543 -0
- package/dist/workbench/ComputedFieldsPanel.d.ts +6 -0
- package/dist/workbench/ComputedFieldsPanel.d.ts.map +1 -0
- package/dist/workbench/ComputedFieldsPanel.js +31 -0
- package/dist/workbench/ExperimentsPanel.d.ts +6 -0
- package/dist/workbench/ExperimentsPanel.d.ts.map +1 -0
- package/dist/workbench/ExperimentsPanel.js +182 -0
- package/dist/workbench/FieldEditorPanel.d.ts +9 -0
- package/dist/workbench/FieldEditorPanel.d.ts.map +1 -0
- package/dist/workbench/FieldEditorPanel.js +650 -0
- package/dist/workbench/InspectorPanel.d.ts +6 -0
- package/dist/workbench/InspectorPanel.d.ts.map +1 -0
- package/dist/workbench/InspectorPanel.js +341 -0
- package/dist/workbench/PageNavigator.d.ts +6 -0
- package/dist/workbench/PageNavigator.d.ts.map +1 -0
- package/dist/workbench/PageNavigator.js +123 -0
- package/dist/workbench/SchemaPanel.d.ts +6 -0
- package/dist/workbench/SchemaPanel.d.ts.map +1 -0
- package/dist/workbench/SchemaPanel.js +222 -0
- package/dist/workbench/UserDataPanel.d.ts +6 -0
- package/dist/workbench/UserDataPanel.d.ts.map +1 -0
- package/dist/workbench/UserDataPanel.js +350 -0
- package/dist/workbench/WorkbenchApp.d.ts +6 -0
- package/dist/workbench/WorkbenchApp.d.ts.map +1 -0
- package/dist/workbench/WorkbenchApp.js +193 -0
- package/dist/workbench/cloudflare-worker/README.md +31 -0
- package/dist/workbench/cloudflare-worker/public/workbench.css +1614 -0
- package/dist/workbench/cloudflare-worker/public/workbench.js +77 -0
- package/dist/workbench/cloudflare-worker/worker.js +40 -0
- package/dist/workbench/cloudflare-worker/wrangler.toml +10 -0
- package/dist/workbench/index.d.ts +9 -0
- package/dist/workbench/index.d.ts.map +1 -0
- package/dist/workbench/index.js +44 -0
- package/dist/workbench/workbench.css +1614 -0
- package/dist/workbench/workbench.js +77 -0
- 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
|
+
}
|