@embeddables/cli 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prompts/embeddables-cli.md +2 -2
- package/dist/cli.js +3 -1
- package/dist/commands/branch.d.ts.map +1 -1
- package/dist/commands/branch.js +5 -1
- package/dist/commands/build.d.ts +2 -1
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/build.js +12 -1
- package/dist/commands/dev.d.ts +1 -0
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +3 -0
- package/dist/commands/pull.d.ts +4 -2
- package/dist/commands/pull.d.ts.map +1 -1
- package/dist/commands/pull.js +116 -29
- package/dist/commands/save.d.ts.map +1 -1
- package/dist/commands/save.js +121 -9
- package/dist/compiler/helpers/numericLeadingKeys.d.ts +8 -0
- package/dist/compiler/helpers/numericLeadingKeys.d.ts.map +1 -0
- package/dist/compiler/helpers/numericLeadingKeys.js +17 -0
- package/dist/compiler/index.d.ts +2 -0
- package/dist/compiler/index.d.ts.map +1 -1
- package/dist/compiler/index.js +309 -57
- package/dist/compiler/reverse.d.ts +5 -0
- package/dist/compiler/reverse.d.ts.map +1 -1
- package/dist/compiler/reverse.js +129 -2
- package/dist/types-builder.d.ts +1 -1
- package/dist/types-builder.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/compiler/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { CompileError } from './errors.js';
|
|
|
6
6
|
import CSSJSON from 'cssjson';
|
|
7
7
|
import { sanitizeFileName } from './reverse.js';
|
|
8
8
|
import { generateRandomIdByType } from './helpers/duplicateIds.js';
|
|
9
|
+
import { normalizeKeyIfStartsWithDigit, keyOrIdStartsWithDigit, } from './helpers/numericLeadingKeys.js';
|
|
9
10
|
import { parse } from '@babel/parser';
|
|
10
11
|
import traverseImport from '@babel/traverse';
|
|
11
12
|
import * as generator from '@babel/generator';
|
|
@@ -103,16 +104,13 @@ export async function compileAllPages(opts) {
|
|
|
103
104
|
if (files.length === 0) {
|
|
104
105
|
throw new CompileError(`No pages found for glob: ${opts.pagesGlob}`);
|
|
105
106
|
}
|
|
106
|
-
// First pass: collect
|
|
107
|
-
// Note: We check pages first, then global components are checked separately in loadGlobalComponents
|
|
107
|
+
// First pass: parse and collect lint issues (duplicate IDs, keys/IDs starting with a digit)
|
|
108
108
|
const idOccurrences = new Map();
|
|
109
109
|
const filePages = [];
|
|
110
|
-
// Parse all page files first to collect IDs
|
|
111
110
|
for (const file of files) {
|
|
112
111
|
const code = fs.readFileSync(file, 'utf8');
|
|
113
112
|
const pageKey = derivePageKey(file, opts.pageKeyFrom);
|
|
114
113
|
const page = parsePageFromFile({ code, filePath: file, pageKey });
|
|
115
|
-
// Collect component and button IDs with their locations
|
|
116
114
|
for (let compIndex = 0; compIndex < page.components.length; compIndex++) {
|
|
117
115
|
const component = page.components[compIndex];
|
|
118
116
|
const compId = component.id;
|
|
@@ -126,7 +124,6 @@ export async function compileAllPages(opts) {
|
|
|
126
124
|
componentIndex: compIndex,
|
|
127
125
|
id: compId,
|
|
128
126
|
});
|
|
129
|
-
// Collect button IDs from OptionSelector components
|
|
130
127
|
if (component.type === 'OptionSelector' && Array.isArray(component.buttons)) {
|
|
131
128
|
for (let btnIndex = 0; btnIndex < component.buttons.length; btnIndex++) {
|
|
132
129
|
const button = component.buttons[btnIndex];
|
|
@@ -149,20 +146,16 @@ export async function compileAllPages(opts) {
|
|
|
149
146
|
}
|
|
150
147
|
filePages.push({ file, pageKey, page });
|
|
151
148
|
}
|
|
152
|
-
//
|
|
153
|
-
const idFixes = new Map();
|
|
149
|
+
// Collect duplicate ID fixes (do not apply yet)
|
|
150
|
+
const idFixes = new Map();
|
|
154
151
|
const allIds = new Set();
|
|
155
|
-
// Collect all existing IDs
|
|
156
152
|
for (const occurrences of idOccurrences.values()) {
|
|
157
153
|
for (const occ of occurrences) {
|
|
158
154
|
allIds.add(occ.id);
|
|
159
155
|
}
|
|
160
156
|
}
|
|
161
|
-
// Find duplicates and generate unique replacements
|
|
162
157
|
for (const [id, occurrences] of idOccurrences.entries()) {
|
|
163
158
|
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
159
|
for (let i = 1; i < occurrences.length; i++) {
|
|
167
160
|
const occ = occurrences[i];
|
|
168
161
|
const fixKey = occ.buttonIndex !== undefined
|
|
@@ -172,65 +165,154 @@ export async function compileAllPages(opts) {
|
|
|
172
165
|
const newId = generateRandomIdByType(occ.componentType, isOptionButton, allIds, idFixes.values());
|
|
173
166
|
allIds.add(newId);
|
|
174
167
|
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
168
|
}
|
|
177
169
|
}
|
|
178
170
|
}
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
171
|
+
// Collect numeric-leading key/id fixes (component key, component id, button key, button id)
|
|
172
|
+
const numericLeadingFixes = [];
|
|
173
|
+
for (const { file, pageKey, page } of filePages) {
|
|
174
|
+
for (let compIndex = 0; compIndex < page.components.length; compIndex++) {
|
|
175
|
+
const comp = page.components[compIndex];
|
|
176
|
+
if (keyOrIdStartsWithDigit(comp.key)) {
|
|
177
|
+
const newKey = normalizeKeyIfStartsWithDigit(comp.key, pageKey);
|
|
178
|
+
if (newKey)
|
|
179
|
+
numericLeadingFixes.push({
|
|
180
|
+
file,
|
|
181
|
+
componentIndex: compIndex,
|
|
182
|
+
kind: 'componentKey',
|
|
183
|
+
oldValue: comp.key,
|
|
184
|
+
newValue: newKey,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
if (keyOrIdStartsWithDigit(comp.id)) {
|
|
188
|
+
const newId = normalizeKeyIfStartsWithDigit(comp.id, 'comp');
|
|
189
|
+
if (newId)
|
|
190
|
+
numericLeadingFixes.push({
|
|
191
|
+
file,
|
|
192
|
+
componentIndex: compIndex,
|
|
193
|
+
kind: 'componentId',
|
|
194
|
+
oldValue: comp.id,
|
|
195
|
+
newValue: newId,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
if (comp.type === 'OptionSelector' && Array.isArray(comp.buttons)) {
|
|
199
|
+
const prefix = comp.key ?? 'option';
|
|
200
|
+
for (let btnIndex = 0; btnIndex < comp.buttons.length; btnIndex++) {
|
|
201
|
+
const btn = comp.buttons[btnIndex];
|
|
202
|
+
if (btn && keyOrIdStartsWithDigit(btn.key)) {
|
|
203
|
+
const newKey = normalizeKeyIfStartsWithDigit(btn.key, prefix);
|
|
204
|
+
if (newKey)
|
|
205
|
+
numericLeadingFixes.push({
|
|
206
|
+
file,
|
|
207
|
+
componentIndex: compIndex,
|
|
208
|
+
buttonIndex: btnIndex,
|
|
209
|
+
kind: 'buttonKey',
|
|
210
|
+
oldValue: btn.key,
|
|
211
|
+
newValue: newKey,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
if (btn && keyOrIdStartsWithDigit(btn.id)) {
|
|
215
|
+
const safeKey = normalizeKeyIfStartsWithDigit(btn.key, prefix);
|
|
216
|
+
const newId = btn.key != null && btn.id === `option_${btn.key}`
|
|
217
|
+
? safeKey != null
|
|
218
|
+
? `option_${safeKey}`
|
|
219
|
+
: normalizeKeyIfStartsWithDigit(btn.id, 'option')
|
|
220
|
+
: normalizeKeyIfStartsWithDigit(btn.id, 'option');
|
|
221
|
+
if (newId)
|
|
222
|
+
numericLeadingFixes.push({
|
|
223
|
+
file,
|
|
224
|
+
componentIndex: compIndex,
|
|
225
|
+
buttonIndex: btnIndex,
|
|
226
|
+
kind: 'buttonId',
|
|
227
|
+
oldValue: btn.id,
|
|
228
|
+
newValue: newId,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const totalLintIssues = idFixes.size + numericLeadingFixes.length;
|
|
236
|
+
let shouldApplyFixes = false;
|
|
237
|
+
if (totalLintIssues > 0) {
|
|
238
|
+
// Report all issues
|
|
239
|
+
if (idFixes.size > 0) {
|
|
240
|
+
console.warn(`Found ${idFixes.size} duplicate ID(s). Keys and IDs must be unique across the embeddable.`);
|
|
241
|
+
for (const [fixKey, newId] of idFixes) {
|
|
242
|
+
const parts = fixKey.split(':');
|
|
243
|
+
const file = parts[0];
|
|
244
|
+
const compIdx = parts[1];
|
|
245
|
+
const btnIdx = parts[2];
|
|
246
|
+
const loc = btnIdx !== undefined ? `component ${compIdx}, button ${btnIdx}` : `component ${compIdx}`;
|
|
247
|
+
console.warn(` → ${file}: ${loc} → "${newId}"`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (numericLeadingFixes.length > 0) {
|
|
251
|
+
console.warn(`Found ${numericLeadingFixes.length} key(s)/ID(s) that start with a number. Use a prefix (e.g. range_1_to_2_weeks).`);
|
|
252
|
+
for (const f of numericLeadingFixes) {
|
|
253
|
+
const loc = f.buttonIndex !== undefined
|
|
254
|
+
? `component ${f.componentIndex}, button ${f.buttonIndex} (${f.kind})`
|
|
255
|
+
: `component ${f.componentIndex} (${f.kind})`;
|
|
256
|
+
console.warn(` → ${f.file}: "${f.oldValue}" → "${f.newValue}" (${loc})`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (opts.fixLint === true) {
|
|
260
|
+
shouldApplyFixes = true;
|
|
261
|
+
}
|
|
262
|
+
else if (opts.fixLint === 'prompt') {
|
|
263
|
+
const { default: prompts } = await import('prompts');
|
|
264
|
+
const res = await prompts({
|
|
265
|
+
type: 'confirm',
|
|
266
|
+
name: 'apply',
|
|
267
|
+
message: `Apply ${totalLintIssues} fix(es) to source files?`,
|
|
268
|
+
initial: true,
|
|
269
|
+
});
|
|
270
|
+
shouldApplyFixes = res.apply === true;
|
|
271
|
+
}
|
|
272
|
+
if (!shouldApplyFixes) {
|
|
273
|
+
throw new CompileError(`Build failed: ${totalLintIssues} lint issue(s) (duplicate IDs and/or keys/IDs starting with a number). Run with --fix or answer 'y' when prompted to fix.`, { file: files[0] });
|
|
274
|
+
}
|
|
275
|
+
// Apply fixes
|
|
182
276
|
for (const { file } of filePages) {
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
277
|
+
const idFixesForFile = Array.from(idFixes.entries()).filter(([key]) => key.startsWith(file + ':'));
|
|
278
|
+
const numericForFile = numericLeadingFixes.filter((f) => f.file === file);
|
|
279
|
+
if (idFixesForFile.length > 0 || numericForFile.length > 0) {
|
|
280
|
+
let code = fs.readFileSync(file, 'utf8');
|
|
281
|
+
if (idFixesForFile.length > 0) {
|
|
282
|
+
code = fixIdsInSourceFile(code, file, idFixesForFile);
|
|
283
|
+
}
|
|
284
|
+
if (numericForFile.length > 0) {
|
|
285
|
+
code = fixNumericLeadingInSourceFile(code, file, numericForFile);
|
|
286
|
+
}
|
|
287
|
+
fs.writeFileSync(file, code, 'utf8');
|
|
288
|
+
console.log(` ✓ Updated ${file}`);
|
|
189
289
|
}
|
|
190
290
|
}
|
|
291
|
+
// Re-parse after fixes
|
|
292
|
+
filePages.length = 0;
|
|
293
|
+
for (const file of files) {
|
|
294
|
+
const code = fs.readFileSync(file, 'utf8');
|
|
295
|
+
const pageKey = derivePageKey(file, opts.pageKeyFrom);
|
|
296
|
+
const page = parsePageFromFile({ code, filePath: file, pageKey });
|
|
297
|
+
filePages.push({ file, pageKey, page });
|
|
298
|
+
}
|
|
191
299
|
}
|
|
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
300
|
const pages = [];
|
|
195
301
|
const seenIds = new Map();
|
|
196
|
-
// Map of pageKey -> PageJson for applying metadata later
|
|
197
302
|
const pageMap = new Map();
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
for (const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
}
|
|
303
|
+
for (const filePage of filePages) {
|
|
304
|
+
const { page, pageKey, file } = filePage;
|
|
305
|
+
for (const c of page.components) {
|
|
306
|
+
checkUniqueId(seenIds, c.id, { file, pageKey });
|
|
307
|
+
if (c.type === 'OptionSelector' && Array.isArray(c.buttons)) {
|
|
308
|
+
for (const b of c.buttons) {
|
|
309
|
+
if (b?.id)
|
|
310
|
+
checkUniqueId(seenIds, b.id, { file, pageKey });
|
|
229
311
|
}
|
|
230
312
|
}
|
|
231
|
-
pages.push(page);
|
|
232
|
-
pageMap.set(pageKey, page);
|
|
233
313
|
}
|
|
314
|
+
pages.push(page);
|
|
315
|
+
pageMap.set(pageKey, page);
|
|
234
316
|
}
|
|
235
317
|
// Read config.json if it exists
|
|
236
318
|
let config = null;
|
|
@@ -333,6 +415,10 @@ export async function compileAllPages(opts) {
|
|
|
333
415
|
// Skip _version - this is CLI metadata (tracked version number), not part of the embeddable
|
|
334
416
|
continue;
|
|
335
417
|
}
|
|
418
|
+
else if (key === '_branch_id' || key === '_branch_name') {
|
|
419
|
+
// Skip _branch_id / _branch_name - CLI metadata (current branch), not part of the embeddable
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
336
422
|
else if (key === 'computedFields') {
|
|
337
423
|
// Replace computedFields with loaded computedFields (which include code)
|
|
338
424
|
if (computedFields.length > 0) {
|
|
@@ -555,6 +641,172 @@ function fixIdsInSourceFile(code, filePath, fixes) {
|
|
|
555
641
|
const result = generate(ast, {}, code);
|
|
556
642
|
return result.code;
|
|
557
643
|
}
|
|
644
|
+
function fixNumericLeadingInSourceFile(code, filePath, fixes) {
|
|
645
|
+
let ast;
|
|
646
|
+
try {
|
|
647
|
+
ast = parse(code, {
|
|
648
|
+
sourceType: 'module',
|
|
649
|
+
plugins: ['typescript', 'jsx'],
|
|
650
|
+
sourceFilename: filePath,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
catch (error) {
|
|
654
|
+
throw new CompileError(`Failed to parse file for key/id fixing: ${error.message}`, {
|
|
655
|
+
file: filePath,
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
const componentKeyMap = new Map();
|
|
659
|
+
const componentIdMap = new Map();
|
|
660
|
+
const buttonKeyMap = new Map();
|
|
661
|
+
const buttonIdMap = new Map();
|
|
662
|
+
for (const f of fixes) {
|
|
663
|
+
if (f.buttonIndex !== undefined) {
|
|
664
|
+
const k = `${f.componentIndex}:${f.buttonIndex}`;
|
|
665
|
+
if (f.kind === 'buttonKey')
|
|
666
|
+
buttonKeyMap.set(k, f.newValue);
|
|
667
|
+
else if (f.kind === 'buttonId')
|
|
668
|
+
buttonIdMap.set(k, f.newValue);
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
if (f.kind === 'componentKey')
|
|
672
|
+
componentKeyMap.set(f.componentIndex, f.newValue);
|
|
673
|
+
else if (f.kind === 'componentId')
|
|
674
|
+
componentIdMap.set(f.componentIndex, f.newValue);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
// Map from buttons variable name (e.g. "genderButtons") -> OptionSelector key (e.g. "gender") for fixing const arrays
|
|
678
|
+
const buttonsConstNameToPrefix = new Map();
|
|
679
|
+
// Collect (start, end, newText) replacements so we can patch the source without reformatting (preserves line breaks, etc.)
|
|
680
|
+
const replacements = [];
|
|
681
|
+
const pushReplacement = (node, newValue) => {
|
|
682
|
+
if (node.start == null || node.end == null)
|
|
683
|
+
return;
|
|
684
|
+
replacements.push({ start: node.start, end: node.end, newText: JSON.stringify(newValue) });
|
|
685
|
+
};
|
|
686
|
+
let componentIndex = -1;
|
|
687
|
+
traverse(ast, {
|
|
688
|
+
JSXElement(path) {
|
|
689
|
+
const opening = path.node.openingElement;
|
|
690
|
+
const tagName = opening.name.type === 'JSXIdentifier' ? opening.name.name : null;
|
|
691
|
+
if (!tagName || !ALLOWED_PRIMITIVES.has(tagName))
|
|
692
|
+
return;
|
|
693
|
+
const hasIdAttr = opening.attributes.some((a) => a.type === 'JSXAttribute' && a.name.type === 'JSXIdentifier' && a.name.name === 'id');
|
|
694
|
+
const hasKeyAttr = opening.attributes.some((a) => a.type === 'JSXAttribute' && a.name.type === 'JSXIdentifier' && a.name.name === 'key');
|
|
695
|
+
if (hasIdAttr || hasKeyAttr) {
|
|
696
|
+
componentIndex++;
|
|
697
|
+
}
|
|
698
|
+
const recordAttrReplacement = (name, newValue) => {
|
|
699
|
+
for (const attr of opening.attributes) {
|
|
700
|
+
if (attr.type === 'JSXAttribute' &&
|
|
701
|
+
attr.name.type === 'JSXIdentifier' &&
|
|
702
|
+
attr.name.name === name) {
|
|
703
|
+
const valueNode = attr.value?.type === 'JSXExpressionContainer'
|
|
704
|
+
? attr.value.expression
|
|
705
|
+
: attr.value;
|
|
706
|
+
if (valueNode?.type === 'StringLiteral') {
|
|
707
|
+
pushReplacement(valueNode, newValue);
|
|
708
|
+
}
|
|
709
|
+
break;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
if (componentKeyMap.has(componentIndex)) {
|
|
714
|
+
recordAttrReplacement('key', componentKeyMap.get(componentIndex));
|
|
715
|
+
}
|
|
716
|
+
if (componentIdMap.has(componentIndex)) {
|
|
717
|
+
recordAttrReplacement('id', componentIdMap.get(componentIndex));
|
|
718
|
+
}
|
|
719
|
+
if (tagName === 'OptionSelector') {
|
|
720
|
+
let componentKey;
|
|
721
|
+
for (const attr of opening.attributes) {
|
|
722
|
+
if (attr.type === 'JSXAttribute' &&
|
|
723
|
+
attr.name.type === 'JSXIdentifier' &&
|
|
724
|
+
attr.name.name === 'key' &&
|
|
725
|
+
attr.value?.type === 'StringLiteral') {
|
|
726
|
+
componentKey = attr.value.value;
|
|
727
|
+
break;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
const prefix = componentKey ?? 'option';
|
|
731
|
+
for (const attr of opening.attributes) {
|
|
732
|
+
if (attr.type === 'JSXAttribute' &&
|
|
733
|
+
attr.name.type === 'JSXIdentifier' &&
|
|
734
|
+
attr.name.name === 'buttons' &&
|
|
735
|
+
attr.value?.type === 'JSXExpressionContainer') {
|
|
736
|
+
const expr = attr.value.expression;
|
|
737
|
+
if (expr.type === 'ArrayExpression') {
|
|
738
|
+
let buttonIndex = 0;
|
|
739
|
+
for (const element of expr.elements) {
|
|
740
|
+
if (element && element.type === 'ObjectExpression') {
|
|
741
|
+
const key = `${componentIndex}:${buttonIndex}`;
|
|
742
|
+
for (const prop of element.properties) {
|
|
743
|
+
if (prop.type === 'ObjectProperty' && prop.key.type === 'Identifier') {
|
|
744
|
+
if (prop.key.name === 'key' && buttonKeyMap.has(key)) {
|
|
745
|
+
pushReplacement(prop.value, buttonKeyMap.get(key));
|
|
746
|
+
}
|
|
747
|
+
else if (prop.key.name === 'id' && buttonIdMap.has(key)) {
|
|
748
|
+
pushReplacement(prop.value, buttonIdMap.get(key));
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
buttonIndex++;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
else if (expr.type === 'Identifier') {
|
|
757
|
+
if (!buttonsConstNameToPrefix.has(expr.name)) {
|
|
758
|
+
buttonsConstNameToPrefix.set(expr.name, prefix);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
break;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
},
|
|
766
|
+
});
|
|
767
|
+
// Fix button key/id in const arrays (e.g. const genderButtons = [{ id: "1_...", key: "..." }])
|
|
768
|
+
traverse(ast, {
|
|
769
|
+
VariableDeclarator(path) {
|
|
770
|
+
const id = path.node.id;
|
|
771
|
+
const init = path.node.init;
|
|
772
|
+
if (id.type !== 'Identifier' || init?.type !== 'ArrayExpression')
|
|
773
|
+
return;
|
|
774
|
+
const constName = id.name;
|
|
775
|
+
const prefix = buttonsConstNameToPrefix.get(constName) ?? 'option';
|
|
776
|
+
for (const element of init.elements) {
|
|
777
|
+
if (!element || element.type !== 'ObjectExpression')
|
|
778
|
+
continue;
|
|
779
|
+
for (const prop of element.properties) {
|
|
780
|
+
if (prop.type !== 'ObjectProperty' || prop.key.type !== 'Identifier')
|
|
781
|
+
continue;
|
|
782
|
+
if (prop.key.name !== 'key' && prop.key.name !== 'id')
|
|
783
|
+
continue;
|
|
784
|
+
const val = prop.value;
|
|
785
|
+
let current = null;
|
|
786
|
+
if (val.type === 'StringLiteral') {
|
|
787
|
+
current = val.value;
|
|
788
|
+
}
|
|
789
|
+
else if (val.type === 'TemplateLiteral' && val.quasis.length === 1 && !val.quasis[0].value.raw.includes('$')) {
|
|
790
|
+
current = val.quasis[0].value.raw;
|
|
791
|
+
}
|
|
792
|
+
if (current === null || !keyOrIdStartsWithDigit(current))
|
|
793
|
+
continue;
|
|
794
|
+
const newValue = normalizeKeyIfStartsWithDigit(current, prefix);
|
|
795
|
+
if (newValue === null)
|
|
796
|
+
continue;
|
|
797
|
+
pushReplacement(val, newValue);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
},
|
|
801
|
+
});
|
|
802
|
+
// Apply replacements from end to start so indices stay valid; preserves all other formatting
|
|
803
|
+
replacements.sort((a, b) => b.start - a.start);
|
|
804
|
+
let result = code;
|
|
805
|
+
for (const { start, end, newText } of replacements) {
|
|
806
|
+
result = result.slice(0, start) + newText + result.slice(end);
|
|
807
|
+
}
|
|
808
|
+
return result;
|
|
809
|
+
}
|
|
558
810
|
function checkUniqueId(seen, id, loc) {
|
|
559
811
|
const prev = seen.get(id);
|
|
560
812
|
if (prev) {
|
|
@@ -9,6 +9,11 @@ export declare function reverseCompile(embeddable: {
|
|
|
9
9
|
[key: string]: any;
|
|
10
10
|
}, embeddableId: string, opts?: {
|
|
11
11
|
fix?: boolean;
|
|
12
|
+
pullMetadata?: {
|
|
13
|
+
version?: number;
|
|
14
|
+
branchId?: string;
|
|
15
|
+
branchName?: string;
|
|
16
|
+
};
|
|
12
17
|
}): Promise<void>;
|
|
13
18
|
/**
|
|
14
19
|
* Sanitizes a string to be safe for use as a filename.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reverse.d.ts","sourceRoot":"","sources":["../../src/compiler/reverse.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,QAAQ,EAAiB,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"reverse.d.ts","sourceRoot":"","sources":["../../src/compiler/reverse.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,QAAQ,EAAiB,MAAM,YAAY,CAAA;AAqlBzD,wBAAsB,cAAc,CAClC,UAAU,EAAE;IACV,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,QAAQ,EAAE,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC5B,cAAc,CAAC,EAAE,GAAG,EAAE,CAAA;IACtB,WAAW,CAAC,EAAE,GAAG,EAAE,CAAA;IACnB,UAAU,CAAC,EAAE,GAAG,EAAE,CAAA;IAClB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CACnB,EACD,YAAY,EAAE,MAAM,EACpB,IAAI,CAAC,EAAE;IACL,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,YAAY,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAC5E,iBAmFF;AA+0CD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAKpD"}
|
package/dist/compiler/reverse.js
CHANGED
|
@@ -284,6 +284,7 @@ function validateComponentProps(component, validProps) {
|
|
|
284
284
|
'tags',
|
|
285
285
|
'type',
|
|
286
286
|
'parent_id',
|
|
287
|
+
'parent_key',
|
|
287
288
|
'buttons',
|
|
288
289
|
...IGNORED_COMPONENT_PROPERTIES,
|
|
289
290
|
]);
|
|
@@ -424,6 +425,61 @@ function checkAndFixDuplicateIds(pages, fix) {
|
|
|
424
425
|
}
|
|
425
426
|
return idMapping;
|
|
426
427
|
}
|
|
428
|
+
/**
|
|
429
|
+
* Fixes deprecated parent_key: resolves to parent_id using first component with matching key,
|
|
430
|
+
* removes parent_key, and removes components that still have no parent_id after resolution.
|
|
431
|
+
* Only runs when fix is enabled.
|
|
432
|
+
*/
|
|
433
|
+
function fixParentKeyDeprecation(components, fix) {
|
|
434
|
+
if (!fix || components.length === 0)
|
|
435
|
+
return;
|
|
436
|
+
// Build key -> id map (first occurrence wins, exclude ignored types)
|
|
437
|
+
const keyToId = new Map();
|
|
438
|
+
for (const comp of components) {
|
|
439
|
+
if (comp.key &&
|
|
440
|
+
comp.id &&
|
|
441
|
+
!keyToId.has(comp.key) &&
|
|
442
|
+
comp.type &&
|
|
443
|
+
!IGNORED_COMPONENT_TYPES.has(comp.type)) {
|
|
444
|
+
keyToId.set(comp.key, comp.id);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
// Resolve parent_key -> parent_id, remove parent_key, track components to remove
|
|
448
|
+
const toRemove = new Map(); // id -> parent_key for warning
|
|
449
|
+
for (const comp of components) {
|
|
450
|
+
const parentKey = comp.parent_key;
|
|
451
|
+
if (!parentKey)
|
|
452
|
+
continue;
|
|
453
|
+
if (!comp.parent_id) {
|
|
454
|
+
const resolvedId = keyToId.get(parentKey);
|
|
455
|
+
if (resolvedId) {
|
|
456
|
+
comp.parent_id = resolvedId;
|
|
457
|
+
console.warn(`Fixed parent_key on component (id: ${comp.id}, key: ${comp.key}) – resolved parent_key "${parentKey}" to parent_id.`);
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
toRemove.set(comp.id, parentKey);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
console.warn(`Fixed parent_key on component (id: ${comp.id}, key: ${comp.key}) – removed deprecated parent_key (already has parent_id).`);
|
|
465
|
+
}
|
|
466
|
+
delete comp.parent_key;
|
|
467
|
+
}
|
|
468
|
+
// Remove components that couldn't resolve parent
|
|
469
|
+
if (toRemove.size > 0) {
|
|
470
|
+
let i = 0;
|
|
471
|
+
while (i < components.length) {
|
|
472
|
+
const comp = components[i];
|
|
473
|
+
if (toRemove.has(comp.id)) {
|
|
474
|
+
console.warn(`Removed component (id: ${comp.id}, key: ${comp.key}) – parent_key "${toRemove.get(comp.id)}" not found, no parent_id.`);
|
|
475
|
+
components.splice(i, 1);
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
i++;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
427
483
|
/**
|
|
428
484
|
* Applies the ID mapping to all pages, updating component IDs and button IDs.
|
|
429
485
|
* The mapping uses occurrence keys (pageKey:componentIndex or pageKey:componentIndex:buttonIndex).
|
|
@@ -461,14 +517,43 @@ function applyIdMapping(pages, idMapping) {
|
|
|
461
517
|
}
|
|
462
518
|
}
|
|
463
519
|
}
|
|
520
|
+
function hasDeprecatedParentKey(components) {
|
|
521
|
+
return components.some((comp) => comp.parent_key != null);
|
|
522
|
+
}
|
|
464
523
|
export async function reverseCompile(embeddable, embeddableId, opts) {
|
|
465
524
|
const fix = opts?.fix ?? false;
|
|
525
|
+
const pullMetadata = opts?.pullMetadata;
|
|
526
|
+
// When fix is disabled, throw on deprecated parent_key so user gets interactive retry prompt
|
|
527
|
+
if (!fix) {
|
|
528
|
+
for (const page of embeddable.pages) {
|
|
529
|
+
if (hasDeprecatedParentKey(page.components)) {
|
|
530
|
+
const count = page.components.filter((c) => c.parent_key != null).length;
|
|
531
|
+
throw new Error(`Found deprecated parent_key on ${count} component(s) in page "${page.key}". Run with --fix to resolve.`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (embeddable.components && Array.isArray(embeddable.components)) {
|
|
535
|
+
if (hasDeprecatedParentKey(embeddable.components)) {
|
|
536
|
+
const count = embeddable.components.filter((c) => c.parent_key != null).length;
|
|
537
|
+
throw new Error(`Found deprecated parent_key on ${count} global component(s). Run with --fix to resolve.`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
466
541
|
// Check for duplicate IDs across all pages and create a mapping
|
|
467
542
|
const idMapping = checkAndFixDuplicateIds(embeddable.pages, fix);
|
|
468
543
|
// Apply ID mapping to all pages
|
|
469
544
|
if (idMapping.size > 0) {
|
|
470
545
|
applyIdMapping(embeddable.pages, idMapping);
|
|
471
546
|
}
|
|
547
|
+
// Fix deprecated parent_key (resolve to parent_id, remove parent_key, drop orphans).
|
|
548
|
+
// Must run before extractGlobalComponents so resolved parent_id avoids "must have _location since it has no parent_id" errors.
|
|
549
|
+
if (fix) {
|
|
550
|
+
for (const page of embeddable.pages) {
|
|
551
|
+
fixParentKeyDeprecation(page.components, fix);
|
|
552
|
+
}
|
|
553
|
+
if (embeddable.components && Array.isArray(embeddable.components)) {
|
|
554
|
+
fixParentKeyDeprecation(embeddable.components, fix);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
472
557
|
// Generate TSX pages
|
|
473
558
|
for (const page of embeddable.pages) {
|
|
474
559
|
await generatePageFile(page, embeddableId, fix);
|
|
@@ -478,7 +563,7 @@ export async function reverseCompile(embeddable, embeddableId, opts) {
|
|
|
478
563
|
await generateStylesFile(embeddable.styles, embeddableId);
|
|
479
564
|
}
|
|
480
565
|
// Generate config.json
|
|
481
|
-
await generateConfigFile(embeddable, embeddableId);
|
|
566
|
+
await generateConfigFile(embeddable, embeddableId, pullMetadata);
|
|
482
567
|
// Extract computedFields to JS files
|
|
483
568
|
if (embeddable.computedFields && embeddable.computedFields.length > 0) {
|
|
484
569
|
await extractComputedFields(embeddable.computedFields, embeddableId);
|
|
@@ -758,6 +843,7 @@ function generateJSX(node, indent = 4, pageKey, componentId, nameMap) {
|
|
|
758
843
|
'tags',
|
|
759
844
|
'type',
|
|
760
845
|
'parent_id',
|
|
846
|
+
'parent_key',
|
|
761
847
|
'buttons',
|
|
762
848
|
'_location',
|
|
763
849
|
...IGNORED_COMPONENT_PROPERTIES,
|
|
@@ -1347,9 +1433,27 @@ function escapeStringForJS(str) {
|
|
|
1347
1433
|
* This file controls page ordering and stores embeddable-level metadata.
|
|
1348
1434
|
* Preserves the order of top-level properties from embeddable.json.
|
|
1349
1435
|
*/
|
|
1350
|
-
async function generateConfigFile(embeddable, embeddableId) {
|
|
1436
|
+
async function generateConfigFile(embeddable, embeddableId, pullMetadata) {
|
|
1351
1437
|
try {
|
|
1352
1438
|
const configPath = path.join('embeddables', embeddableId, 'config.json');
|
|
1439
|
+
// CLI-only fields: from pull (when branching/saving) or from existing config file.
|
|
1440
|
+
let preservedVersion;
|
|
1441
|
+
let preservedBranchId;
|
|
1442
|
+
let preservedBranchName;
|
|
1443
|
+
if (fs.existsSync(configPath)) {
|
|
1444
|
+
try {
|
|
1445
|
+
const existing = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
1446
|
+
if (typeof existing._version === 'number')
|
|
1447
|
+
preservedVersion = existing._version;
|
|
1448
|
+
if (typeof existing._branch_id === 'string' && existing._branch_id)
|
|
1449
|
+
preservedBranchId = existing._branch_id;
|
|
1450
|
+
if (typeof existing._branch_name === 'string' && existing._branch_name)
|
|
1451
|
+
preservedBranchName = existing._branch_name;
|
|
1452
|
+
}
|
|
1453
|
+
catch {
|
|
1454
|
+
/* ignore */
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1353
1457
|
// Preserve the order of top-level properties from embeddable
|
|
1354
1458
|
const embeddableKeys = Object.keys(embeddable);
|
|
1355
1459
|
// Extract page metadata and order (excluding components which are in TSX files)
|
|
@@ -1432,6 +1536,29 @@ async function generateConfigFile(embeddable, embeddableId) {
|
|
|
1432
1536
|
if (!config.id) {
|
|
1433
1537
|
config.id = embeddableId;
|
|
1434
1538
|
}
|
|
1539
|
+
// Restore CLI-only fields: prefer pullMetadata (passed by pull/branch) so branch is never lost.
|
|
1540
|
+
const versionToWrite = pullMetadata?.version ?? preservedVersion;
|
|
1541
|
+
if (versionToWrite !== undefined)
|
|
1542
|
+
config._version = versionToWrite;
|
|
1543
|
+
if (pullMetadata !== undefined) {
|
|
1544
|
+
if (pullMetadata.branchId !== undefined) {
|
|
1545
|
+
config._branch_id = pullMetadata.branchId;
|
|
1546
|
+
if (pullMetadata.branchName !== undefined)
|
|
1547
|
+
config._branch_name = pullMetadata.branchName;
|
|
1548
|
+
else
|
|
1549
|
+
delete config._branch_name;
|
|
1550
|
+
}
|
|
1551
|
+
else {
|
|
1552
|
+
delete config._branch_id;
|
|
1553
|
+
delete config._branch_name;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
else {
|
|
1557
|
+
if (preservedBranchId !== undefined)
|
|
1558
|
+
config._branch_id = preservedBranchId;
|
|
1559
|
+
if (preservedBranchName !== undefined)
|
|
1560
|
+
config._branch_name = preservedBranchName;
|
|
1561
|
+
}
|
|
1435
1562
|
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
1436
1563
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
1437
1564
|
console.log(`${pc.gray(`Generated ${configPath}`)}`);
|
package/dist/types-builder.d.ts
CHANGED
|
@@ -551,7 +551,7 @@ export interface OptionSelectorButton {
|
|
|
551
551
|
}
|
|
552
552
|
type OptionSelectorButtonTriggerEvent = 'no-action' | 'next-page' | 'open-url';
|
|
553
553
|
export interface ProgressBar extends Base {
|
|
554
|
-
progress_type
|
|
554
|
+
progress_type: 'circle' | 'simple' | 'custom';
|
|
555
555
|
allow_nav?: boolean;
|
|
556
556
|
fill_connectors?: boolean;
|
|
557
557
|
node_icon?: string;
|