@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.
- package/bin/collab.js +334 -0
- package/bin/fetch.js +77 -54
- package/bin/fs.js +150 -61
- package/bin/index.js +1 -0
- package/bin/install.js +6 -19
- package/bin/login.js +82 -62
- package/bin/push.js +157 -117
- package/bin/require.js +140 -6
- package/bin/socket-server.js +5 -10
- package/bin/sync.js +313 -112
- package/helpers/apiUtils.js +60 -58
- package/helpers/buildMessages.js +76 -0
- package/helpers/changesUtils.js +474 -0
- package/helpers/compareUtils.js +136 -2
- package/helpers/config.js +131 -18
- package/helpers/credentialManager.js +66 -0
- package/helpers/fileUtils.js +8 -2
- package/helpers/symbolsConfig.js +35 -0
- package/helpers/transportUtils.js +58 -0
- package/package.json +7 -5
|
@@ -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
|
+
|
package/helpers/compareUtils.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
+
}
|