@symbo.ls/cli 2.33.11 → 2.33.13

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.
@@ -0,0 +1,474 @@
1
+ import { normalizeKeys } from './compareUtils.js'
2
+
3
+ // Keys managed by the CLI filesystem representation (exclude settings/schema/key/thumbnail/etc.)
4
+ export const DATA_KEYS = [
5
+ 'designSystem','components','state','pages','snippets',
6
+ 'methods','functions','dependencies','files'
7
+ ]
8
+
9
+ function stripMetaDeep(val) {
10
+ if (Array.isArray(val)) {
11
+ return val.map(stripMetaDeep)
12
+ }
13
+ if (val && typeof val === 'object') {
14
+ const out = {}
15
+ for (const [k, v] of Object.entries(val)) {
16
+ if (k === '__order') continue
17
+ out[k] = stripMetaDeep(v)
18
+ }
19
+ return out
20
+ }
21
+ return val
22
+ }
23
+
24
+ function asPlain(obj) {
25
+ // Ensure consistent comparison and strip meta keys (like __order)
26
+ return stripMetaDeep(normalizeKeys(obj || {}))
27
+ }
28
+
29
+ function equal(a, b) {
30
+ // Coarse compare; sufficient for top-level merges
31
+ try {
32
+ return JSON.stringify(a) === JSON.stringify(b)
33
+ } catch (_) {
34
+ return a === b
35
+ }
36
+ }
37
+
38
+ export function computeChangedKeys(base, local, keys = DATA_KEYS) {
39
+ const changed = []
40
+ const a = asPlain(base)
41
+ const b = asPlain(local)
42
+ // Only consider top-level data keys; ignore 'schema' entirely
43
+ for (const key of [...keys]) {
44
+ const ak = a?.[key]
45
+ const bk = b?.[key]
46
+ if (bk === undefined && ak !== undefined) {
47
+ changed.push(key)
48
+ continue
49
+ }
50
+ if (!equal(ak, bk)) {
51
+ changed.push(key)
52
+ }
53
+ }
54
+ return changed
55
+ }
56
+
57
+ export function computeCoarseChanges(base, local, keys = DATA_KEYS) {
58
+ const changes = []
59
+ const a = asPlain(base)
60
+ const b = asPlain(local)
61
+
62
+ // Generate per-item granular changes one level deeper for each data type.
63
+ // Ignore 'schema' comparisons; manage schema side-effects below.
64
+ for (const typeKey of [...keys]) {
65
+ const aSection = a?.[typeKey] || {}
66
+ const bSection = b?.[typeKey] || {}
67
+
68
+ // If sections are not plain objects (or are arrays), fallback to coarse replacement on the section itself
69
+ const aIsObject = aSection && typeof aSection === 'object' && !Array.isArray(aSection)
70
+ const bIsObject = bSection && typeof bSection === 'object' && !Array.isArray(bSection)
71
+ if (!aIsObject || !bIsObject) {
72
+ if (b?.hasOwnProperty(typeKey) === false && a?.hasOwnProperty(typeKey) === true) {
73
+ changes.push(['delete', [typeKey]])
74
+ } else if (!equal(aSection, bSection)) {
75
+ changes.push(['update', [typeKey], bSection])
76
+ }
77
+ continue
78
+ }
79
+
80
+ // Handle deletions (items present in base but not in local)
81
+ for (const itemKey of Object.keys(aSection)) {
82
+ if (!(itemKey in bSection)) {
83
+ changes.push(['delete', [typeKey, itemKey]])
84
+ // When an item is deleted, also delete its schema entry
85
+ changes.push(['delete', ['schema', typeKey, itemKey]])
86
+ }
87
+ }
88
+
89
+ // Handle additions and updates (items present in local)
90
+ for (const itemKey of Object.keys(bSection)) {
91
+ const aVal = aSection[itemKey]
92
+ const bVal = bSection[itemKey]
93
+ if (aVal === undefined) {
94
+ // New item
95
+ changes.push(['update', [typeKey, itemKey], bVal])
96
+ // Ensure any stale schema code is removed (safety)
97
+ changes.push(['delete', ['schema', typeKey, itemKey, 'code']])
98
+ } else if (!equal(aVal, bVal)) {
99
+ // Updated item
100
+ changes.push(['update', [typeKey, itemKey], bVal])
101
+ // When an item changes, drop its schema.code to be regenerated
102
+ changes.push(['delete', ['schema', typeKey, itemKey, 'code']])
103
+ }
104
+ }
105
+ }
106
+
107
+ return changes
108
+ }
109
+
110
+ export function threeWayRebase(base, local, remote, keys = DATA_KEYS) {
111
+ const oursKeys = computeChangedKeys(base, local, keys)
112
+ const theirsKeys = computeChangedKeys(base, remote, keys)
113
+ const conflicts = oursKeys.filter(k => theirsKeys.includes(k))
114
+
115
+ const ours = computeCoarseChanges(base, local, keys).filter(([_, [k]]) => oursKeys.includes(k))
116
+ const theirs = computeCoarseChanges(base, remote, keys).filter(([_, [k]]) => theirsKeys.includes(k))
117
+
118
+ let finalChanges = []
119
+ if (conflicts.length === 0) {
120
+ // Safe to apply our edited keys onto remote
121
+ finalChanges = ours
122
+ }
123
+
124
+ return {
125
+ ours,
126
+ theirs,
127
+ conflicts,
128
+ finalChanges
129
+ }
130
+ }
131
+
132
+ // -------------------- Orders computation (adapted for CLI plain objects) --------------------
133
+ function isObjectLike(val) {
134
+ return val && typeof val === 'object' && !Array.isArray(val)
135
+ }
136
+
137
+ function normalizePath(path) {
138
+ if (Array.isArray(path)) return path
139
+ if (typeof path === 'string') return [path]
140
+ return []
141
+ }
142
+
143
+ function getByPathPlain(root, path = []) {
144
+ if (!root) return null
145
+ try {
146
+ let cur = root
147
+ for (let i = 0; i < path.length; i++) {
148
+ const seg = path[i]
149
+ if (cur == null || typeof cur !== 'object') return null
150
+ cur = cur[seg]
151
+ }
152
+ return cur
153
+ } catch (_) {
154
+ return null
155
+ }
156
+ }
157
+
158
+ function getParentPathsFromTuples(tuples = []) {
159
+ const seen = new Set()
160
+ const parents = []
161
+ const META_KEYS = new Set([
162
+ 'style', 'class', 'text', 'html', 'content', 'data', 'attr', 'state', 'scope',
163
+ 'define', 'on', 'extend', 'extends', 'childExtend', 'childExtends',
164
+ 'children', 'component', 'context', 'tag', 'key', '__order', 'if'
165
+ ])
166
+ for (let i = 0; i < tuples.length; i++) {
167
+ const tuple = tuples[i]
168
+ if (!Array.isArray(tuple) || tuple.length < 2) continue
169
+ const path = normalizePath(tuple[1])
170
+ if (!path.length) continue
171
+ if (path[0] === 'schema') continue
172
+ const immediateParent = path.slice(0, -1)
173
+ if (immediateParent.length) {
174
+ const key = JSON.stringify(immediateParent)
175
+ if (!seen.has(key)) {
176
+ seen.add(key)
177
+ parents.push(immediateParent)
178
+ }
179
+ }
180
+ const last = path[path.length - 1]
181
+ if (META_KEYS.has(last) && path.length >= 2) {
182
+ const containerParent = path.slice(0, -2)
183
+ if (containerParent.length) {
184
+ const key2 = JSON.stringify(containerParent)
185
+ if (!seen.has(key2)) {
186
+ seen.add(key2)
187
+ parents.push(containerParent)
188
+ }
189
+ }
190
+ }
191
+ for (let j = 0; j < path.length; j++) {
192
+ const seg = path[j]
193
+ if (!META_KEYS.has(seg)) continue
194
+ const containerParent2 = path.slice(0, j)
195
+ if (!containerParent2.length) continue
196
+ const key3 = JSON.stringify(containerParent2)
197
+ if (!seen.has(key3)) {
198
+ seen.add(key3)
199
+ parents.push(containerParent2)
200
+ }
201
+ }
202
+ }
203
+ return parents
204
+ }
205
+
206
+ function computeOrdersFromStatePlain(root, parentPaths = []) {
207
+ const orders = []
208
+ const EXCLUDE_KEYS = new Set(['__order'])
209
+ for (let i = 0; i < parentPaths.length; i++) {
210
+ const parentPath = parentPaths[i]
211
+ const obj = getByPathPlain(root, parentPath)
212
+ if (!isObjectLike(obj)) continue
213
+ const keys = Object.keys(obj).filter(k => !EXCLUDE_KEYS.has(k))
214
+ orders.push({ path: parentPath, keys })
215
+ }
216
+ return orders
217
+ }
218
+
219
+ // --- Schema `code` parsing helpers (adapted) ---
220
+ function normaliseSchemaCode(code) {
221
+ if (typeof code !== 'string' || !code.length) return ''
222
+ return code
223
+ .replaceAll('/////n', '\n')
224
+ .replaceAll('/////tilde', '`')
225
+ }
226
+
227
+ function parseExportedObject(code) {
228
+ const src = normaliseSchemaCode(code)
229
+ if (!src) return null
230
+ const body = src.replace(/^\s*export\s+default\s*/u, 'return ')
231
+ try {
232
+ // eslint-disable-next-line no-new-func
233
+ return new Function(body)()
234
+ } catch {
235
+ return null
236
+ }
237
+ }
238
+
239
+ function extractTopLevelKeysFromCode(code) {
240
+ const obj = parseExportedObject(code)
241
+ if (!obj || typeof obj !== 'object') return []
242
+ return Object.keys(obj)
243
+ }
244
+
245
+ /**
246
+ * Compute ordered key arrays for each parent path using a plain root object and granular tuples.
247
+ */
248
+ export function computeOrdersForTuples(root, tuples = []) {
249
+ const pendingChildrenByContainer = new Map()
250
+ for (let i = 0; i < tuples.length; i++) {
251
+ const t = tuples[i]
252
+ if (!Array.isArray(t)) continue
253
+ const [action, path] = t
254
+ const p = normalizePath(path)
255
+ if (!Array.isArray(p) || p.length < 3) continue
256
+ if (p[0] === 'schema') continue
257
+ const [typeName, containerKey, childKey] = p
258
+ const containerPath = [typeName, containerKey]
259
+ const key = JSON.stringify(containerPath)
260
+ if (!pendingChildrenByContainer.has(key)) {
261
+ pendingChildrenByContainer.set(key, new Set())
262
+ }
263
+ if (action === 'update' || action === 'set') {
264
+ pendingChildrenByContainer.get(key).add(childKey)
265
+ }
266
+ }
267
+
268
+ const preferredOrderMap = new Map()
269
+ for (let i = 0; i < tuples.length; i++) {
270
+ const t = tuples[i]
271
+ if (!Array.isArray(t)) continue
272
+ const [action, path, value] = t
273
+ const p = normalizePath(path)
274
+ if (action !== 'update' || !Array.isArray(p) || p.length < 3) continue
275
+ if (p[0] !== 'schema') continue
276
+ const [, type, key] = p
277
+ const containerPath = [type, key]
278
+
279
+ const obj = getByPathPlain(root, containerPath)
280
+ if (!obj) continue
281
+
282
+ const present = new Set(Object.keys(obj))
283
+ const EXCLUDE_KEYS = new Set(['__order'])
284
+ const uses = value && Array.isArray(value.uses) ? value.uses : null
285
+ const code = value && value.code
286
+ const codeKeys = extractTopLevelKeysFromCode(code)
287
+ let resolved = []
288
+
289
+ const pendingKey = JSON.stringify(containerPath)
290
+ const pendingChildren = pendingChildrenByContainer.get(pendingKey) || new Set()
291
+ const eligible = new Set([...present, ...pendingChildren])
292
+
293
+ if (Array.isArray(codeKeys) && codeKeys.length) {
294
+ resolved = codeKeys.filter(k => eligible.has(k) && !EXCLUDE_KEYS.has(k))
295
+ }
296
+ if (Array.isArray(uses) && uses.length) {
297
+ for (let u = 0; u < uses.length; u++) {
298
+ const keyName = uses[u]
299
+ if (eligible.has(keyName) && !EXCLUDE_KEYS.has(keyName) && !resolved.includes(keyName)) {
300
+ resolved.push(keyName)
301
+ }
302
+ }
303
+ }
304
+ if (pendingChildren.size) {
305
+ for (const child of pendingChildren) {
306
+ if (!EXCLUDE_KEYS.has(child) && !resolved.includes(child)) {
307
+ resolved.push(child)
308
+ }
309
+ }
310
+ }
311
+ if (resolved.length) {
312
+ preferredOrderMap.set(JSON.stringify(containerPath), { path: containerPath, keys: resolved })
313
+ }
314
+ }
315
+
316
+ const parents = getParentPathsFromTuples(tuples)
317
+ const orders = []
318
+ const seen = new Set()
319
+ preferredOrderMap.forEach(v => {
320
+ const k = JSON.stringify(v.path)
321
+ if (!seen.has(k)) { seen.add(k); orders.push(v) }
322
+ })
323
+ const fallbackOrders = computeOrdersFromStatePlain(root, parents)
324
+ for (let i = 0; i < fallbackOrders.length; i++) {
325
+ const v = fallbackOrders[i]
326
+ const k = JSON.stringify(v.path)
327
+ if (seen.has(k)) continue
328
+ const pending = pendingChildrenByContainer.get(k)
329
+ if (pending && pending.size) {
330
+ const existing = new Set(v.keys)
331
+ for (const child of pending) {
332
+ if (existing.has(child)) continue
333
+ v.keys.push(child)
334
+ }
335
+ }
336
+ seen.add(k)
337
+ orders.push(v)
338
+ }
339
+ return orders
340
+ }
341
+
342
+ // -------------------- Granular expansion (preprocess) --------------------
343
+ function isPlainObject(val) {
344
+ return val && typeof val === 'object' && !Array.isArray(val)
345
+ }
346
+
347
+ function diffJsonPlain(prev, next, basePath = []) {
348
+ const ops = []
349
+ const prevIsObj = isPlainObject(prev)
350
+ const nextIsObj = isPlainObject(next)
351
+ if (!prevIsObj || !nextIsObj) {
352
+ if (!equal(prev, next)) {
353
+ ops.push({ action: 'set', path: [], value: next })
354
+ }
355
+ return ops
356
+ }
357
+ const prevKeys = new Set(Object.keys(prev || {}))
358
+ const nextKeys = new Set(Object.keys(next || {}))
359
+ // deletions
360
+ prevKeys.forEach((k) => {
361
+ if (!nextKeys.has(k)) {
362
+ ops.push({ action: 'del', path: [k] })
363
+ }
364
+ })
365
+ // additions/updates
366
+ nextKeys.forEach((k) => {
367
+ const pv = prev[k]
368
+ const nv = next[k]
369
+ if (!prevKeys.has(k)) {
370
+ ops.push({ action: 'set', path: [k], value: nv })
371
+ } else if (!equal(pv, nv)) {
372
+ if (isPlainObject(pv) && isPlainObject(nv)) {
373
+ const child = diffJsonPlain(pv, nv, [...basePath, k])
374
+ if (child.length === 0) {
375
+ ops.push({ action: 'set', path: [k], value: nv })
376
+ } else {
377
+ for (let i = 0; i < child.length; i++) {
378
+ const c = child[i]
379
+ ops.push({ action: c.action, path: [k, ...c.path], value: c.value })
380
+ }
381
+ }
382
+ } else {
383
+ ops.push({ action: 'set', path: [k], value: nv })
384
+ }
385
+ }
386
+ })
387
+ return ops
388
+ }
389
+
390
+ export function preprocessChanges(root, tuples = [], options = {}) {
391
+ const META_FILES = 'files'
392
+
393
+ const getByPath = (path) => getByPathPlain(root, path)
394
+
395
+ const expandTuple = (t) => {
396
+ const [action, path, value] = t || []
397
+ const isSchemaPath = Array.isArray(path) && path[0] === 'schema'
398
+ const isFilesPath = Array.isArray(path) && path[0] === META_FILES
399
+ if (action === 'delete') return [t]
400
+ const canConsiderExpansion = (
401
+ action === 'update' &&
402
+ Array.isArray(path) &&
403
+ (
404
+ path.length === 1 ||
405
+ path.length === 2 ||
406
+ (isSchemaPath && path.length === 3)
407
+ ) &&
408
+ isPlainObject(value)
409
+ )
410
+ if (!canConsiderExpansion || isFilesPath || (value && value.type === META_FILES)) return [t]
411
+
412
+ const prevRaw = getByPath(path)
413
+ const isCreatePath = (
414
+ Array.isArray(path) &&
415
+ action === 'update' &&
416
+ ((!isSchemaPath && path.length === 2) || (isSchemaPath && path.length === 3)) &&
417
+ (prevRaw === null || typeof prevRaw === 'undefined')
418
+ )
419
+ if (isCreatePath) return [t]
420
+
421
+ const prev = prevRaw || {}
422
+ const next = value || {}
423
+ if (!isPlainObject(prev) || !isPlainObject(next)) return [t]
424
+
425
+ const ops = diffJsonPlain(prev, next, [])
426
+ if (!ops.length) return [t]
427
+
428
+ const out = []
429
+ for (let i = 0; i < ops.length; i++) {
430
+ const op = ops[i]
431
+ const fullPath = [...path, ...op.path]
432
+ const last = fullPath[fullPath.length - 1]
433
+ if (op.action === 'set') {
434
+ out.push(['update', fullPath, op.value])
435
+ } else if (op.action === 'del') {
436
+ if (last !== '__order') out.push(['delete', fullPath])
437
+ }
438
+ }
439
+ return out
440
+ }
441
+
442
+ const minimizeTuples = (input) => {
443
+ const out = []
444
+ const seen = new Set()
445
+ for (let i = 0; i < input.length; i++) {
446
+ const expanded = expandTuple(input[i])
447
+ for (let k = 0; k < expanded.length; k++) {
448
+ const tuple = expanded[k]
449
+ const isDelete = Array.isArray(tuple) && tuple[0] === 'delete'
450
+ const isOrderKey = isDelete && Array.isArray(tuple[1]) && tuple[1][tuple[1].length - 1] === '__order'
451
+ if (!isOrderKey) {
452
+ const key = JSON.stringify(tuple)
453
+ if (!seen.has(key)) { seen.add(key); out.push(tuple) }
454
+ }
455
+ }
456
+ }
457
+ return out
458
+ }
459
+
460
+ const granularChanges = (() => {
461
+ try {
462
+ const res = minimizeTuples(tuples || [])
463
+ if (options.append && options.append.length) res.push(...options.append)
464
+ return res
465
+ } catch {
466
+ return Array.isArray(tuples) ? tuples.slice() : []
467
+ }
468
+ })()
469
+
470
+ const orders = computeOrdersForTuples(root || {}, granularChanges)
471
+ return { granularChanges, orders }
472
+ }
473
+
474
+
@@ -24,6 +24,95 @@ export function normalizeKeys (obj) {
24
24
  }, {})
25
25
  }
26
26
 
27
+ export function generateDefaultSchema(key, type) {
28
+ // Base schema for components and pages
29
+ const baseSchema = {
30
+ key,
31
+ title: toTitleCase(key),
32
+ description: '',
33
+ type: type.slice(0, -1), // Convert plural to singular
34
+ code: '',
35
+ state: {},
36
+ props: {},
37
+ settings: {
38
+ gridOptions: {
39
+ x: 0,
40
+ y: 0,
41
+ w: 1,
42
+ h: 1
43
+ }
44
+ }
45
+ }
46
+
47
+ // Type-specific schema generation
48
+ switch (type.toLowerCase()) {
49
+ case 'components':
50
+ return {
51
+ ...baseSchema,
52
+ category: ['comp'],
53
+ draft: false,
54
+ highlighted: false,
55
+ tags: [],
56
+ dataTypes: [],
57
+ interactivity: [],
58
+ error: null,
59
+ thumbnail: null,
60
+ pdf: null,
61
+ uses: {
62
+ components: [],
63
+ icons: [],
64
+ themes: [],
65
+ colors: [],
66
+ dependencies: []
67
+ }
68
+ }
69
+
70
+ case 'pages':
71
+ return {
72
+ ...baseSchema,
73
+ key: key.startsWith('/') ? key : `/${key}`,
74
+ type: 'page'
75
+ }
76
+
77
+ case 'functions':
78
+ case 'snippets':
79
+ case 'methods':
80
+ return {
81
+ key,
82
+ title: toTitleCase(key),
83
+ description: '',
84
+ type: type.slice(0, -1),
85
+ code: ''
86
+ }
87
+
88
+ case 'dependencies':
89
+ return {
90
+ key,
91
+ type: 'dependency'
92
+ }
93
+
94
+ case 'secrets':
95
+ return {
96
+ key,
97
+ type: 'secret'
98
+ }
99
+
100
+ case 'files':
101
+ return {
102
+ key,
103
+ format: key.split('.').pop() || '',
104
+ type: 'file'
105
+ }
106
+
107
+ case 'designsystem':
108
+ // Design system items (colors, typography, etc) don't need schema entries
109
+ return null
110
+
111
+ default:
112
+ return baseSchema
113
+ }
114
+ }
115
+
27
116
  export function generateChanges (oldData, newData) {
28
117
  const changes = []
29
118
  const diffs = []
@@ -32,7 +121,7 @@ export function generateChanges (oldData, newData) {
32
121
  throw new Error('Both oldData and newData must be provided')
33
122
  }
34
123
 
35
- // Filter out non-allowed top-level fields before comparison
124
+ // Filter allowed fields
36
125
  const filteredOldData = Object.keys(oldData)
37
126
  .filter(key => ALLOWED_FIELDS.includes(key.toLowerCase()))
38
127
  .reduce((obj, key) => {
@@ -47,7 +136,27 @@ export function generateChanges (oldData, newData) {
47
136
  return obj
48
137
  }, {})
49
138
 
50
- compareObjects(filteredOldData, filteredNewData, [], changes, diffs)
139
+ // Compare and generate changes
140
+ for (const type of ALLOWED_FIELDS) {
141
+ const oldSection = filteredOldData[type] || {}
142
+ const newSection = filteredNewData[type] || {}
143
+
144
+ // Check for new items
145
+ for (const key of Object.keys(newSection)) {
146
+ if (!oldSection[key]) {
147
+ // New item added - generate schema
148
+ const defaultSchema = generateDefaultSchema(key, type)
149
+ changes.push(
150
+ ['update', [type, key], newSection[key]],
151
+ ['update', ['schema', type, key], defaultSchema]
152
+ )
153
+ diffs.push(generateDiffDisplay('add', [type, key], null, newSection[key]))
154
+ }
155
+ }
156
+
157
+ // Handle other changes
158
+ compareObjects(oldSection, newSection, [type], changes, diffs)
159
+ }
51
160
 
52
161
  return { changes, diffs }
53
162
  }
@@ -133,3 +242,28 @@ function handleAdditionsAndUpdates (oldObj, newObj, currentPath, changes, diffs)
133
242
  }
134
243
  }
135
244
  }
245
+
246
+ /**
247
+ * Converts a string to title case format
248
+ * Examples:
249
+ * - "myComponent" -> "My Component"
250
+ * - "my-component" -> "My Component"
251
+ * - "my_component" -> "My Component"
252
+ * - "MyComponent" -> "My Component"
253
+ */
254
+ export function toTitleCase(str) {
255
+ if (!str) return ''
256
+
257
+ // Handle kebab-case and snake_case
258
+ str = str.replace(/[-_]/g, ' ')
259
+
260
+ // Handle camelCase and PascalCase
261
+ str = str.replace(/([a-z])([A-Z])/g, '$1 $2')
262
+
263
+ // Capitalize first letter of each word
264
+ return str
265
+ .split(' ')
266
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
267
+ .join(' ')
268
+ .trim()
269
+ }