@symbo.ls/cli 2.33.35 → 2.33.36

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 CHANGED
@@ -11,6 +11,14 @@ import { stringifyFunctionsForTransport } from '../helpers/transportUtils.js'
11
11
  import { getCurrentProjectData } from '../helpers/apiUtils.js'
12
12
  import { computeCoarseChanges, computeOrdersForTuples, preprocessChanges } from '../helpers/changesUtils.js'
13
13
  import { createFs } from './fs.js'
14
+ import { stripOrderFields } from '../helpers/orderUtils.js'
15
+ import { normalizeKeys } from '../helpers/compareUtils.js'
16
+ import {
17
+ augmentProjectWithLocalPackageDependencies,
18
+ ensureSchemaDependencies,
19
+ findNearestPackageJson,
20
+ syncPackageJsonDependencies
21
+ } from '../helpers/dependenciesUtils.js'
14
22
 
15
23
  // Lazy import socket.io-client and chokidar to avoid adding cost for non-collab users
16
24
  async function importDeps() {
@@ -21,6 +29,29 @@ async function importDeps() {
21
29
  return { io, chokidar }
22
30
  }
23
31
 
32
+ function toExportNameFromFileStem(stem) {
33
+ // Mirror fs.js behavior loosely: kebab/snake/path -> camelCase export name.
34
+ // e.g. "add-network" -> "addNetwork"
35
+ if (!stem || typeof stem !== 'string') return stem
36
+ const parts = stem.split(/[^a-zA-Z0-9]+/).filter(Boolean)
37
+ if (!parts.length) return stem
38
+ const first = parts[0]
39
+ return (
40
+ first +
41
+ parts
42
+ .slice(1)
43
+ .map((p) => (p ? p[0].toUpperCase() + p.slice(1) : ''))
44
+ .join('')
45
+ )
46
+ }
47
+
48
+ function toPagesRouteKeyFromFileStem(stem) {
49
+ // createFs writes `/foo` -> pages/foo.js and `/` -> pages/main.js
50
+ if (!stem || typeof stem !== 'string') return stem
51
+ if (stem === 'main') return '/'
52
+ return `/${stem}`
53
+ }
54
+
24
55
  function debounce(fn, wait) {
25
56
  let t = null
26
57
  const debounced = (...args) => {
@@ -52,6 +83,8 @@ export async function startCollab(options) {
52
83
  resolveDistDir(symbolsConfig) ||
53
84
  path.join(process.cwd(), 'smbls')
54
85
 
86
+ const packageJsonPath = findNearestPackageJson(process.cwd())
87
+
55
88
  if (!appKey) {
56
89
  console.log(chalk.red('Missing project key. Add it to symbols.json or .symbols/config.json'))
57
90
  process.exit(1)
@@ -141,9 +174,19 @@ export async function startCollab(options) {
141
174
  }
142
175
 
143
176
  async function writeProjectAndFs(fullObj) {
177
+ // Avoid persisting ordering metadata into local repository files
178
+ const persistedObj = stripOrderFields(fullObj)
179
+ // Keep schema.dependencies consistent and sync dependencies into local package.json
180
+ try {
181
+ ensureSchemaDependencies(persistedObj)
182
+ if (packageJsonPath && persistedObj?.dependencies) {
183
+ syncPackageJsonDependencies(packageJsonPath, persistedObj.dependencies, { overwriteExisting: true })
184
+ }
185
+ } catch (_) {}
186
+
144
187
  const { projectPath } = getConfigPaths()
145
188
  try {
146
- await fs.promises.writeFile(projectPath, JSON.stringify(fullObj, null, 2))
189
+ await fs.promises.writeFile(projectPath, JSON.stringify(persistedObj, null, 2))
147
190
  } catch (_) {}
148
191
  // Avoid echoing the changes we are about to materialize
149
192
  suppressLocalChanges = true
@@ -152,7 +195,7 @@ export async function startCollab(options) {
152
195
  sendLocalChanges.cancel()
153
196
  }
154
197
  try {
155
- await createFs(fullObj, distDir, { update: true, metadata: false })
198
+ await createFs(persistedObj, distDir, { update: true, metadata: false })
156
199
  } finally {
157
200
  // Extend suppression window to allow file events to settle fully
158
201
  suppressUntil = Date.now() + suppressionWindowMs
@@ -167,6 +210,12 @@ export async function startCollab(options) {
167
210
  { branch, includePending: true }
168
211
  )
169
212
  const initialData = prime?.data || {}
213
+ try {
214
+ ensureSchemaDependencies(initialData)
215
+ if (packageJsonPath && initialData?.dependencies) {
216
+ syncPackageJsonDependencies(packageJsonPath, initialData.dependencies, { overwriteExisting: true })
217
+ }
218
+ } catch (_) {}
170
219
  const etag = prime?.etag || null
171
220
  writeLock({
172
221
  etag,
@@ -180,7 +229,7 @@ export async function startCollab(options) {
180
229
  try {
181
230
  const { projectPath } = getConfigPaths()
182
231
  await fs.promises.mkdir(path.dirname(projectPath), { recursive: true })
183
- await fs.promises.writeFile(projectPath, JSON.stringify(initialData, null, 2))
232
+ await fs.promises.writeFile(projectPath, JSON.stringify(stripOrderFields(initialData), null, 2))
184
233
  } catch (_) {}
185
234
  currentBase = { ...(initialData || {}) }
186
235
 
@@ -240,9 +289,8 @@ export async function startCollab(options) {
240
289
  : preprocessChanges(currentBase || {}, payload?.changes || []).granularChanges
241
290
  if (!Array.isArray(tuples) || !tuples.length) return
242
291
  applyTuples(currentBase, tuples)
243
- if (Array.isArray(payload?.orders) && payload.orders.length) {
244
- applyOrders(currentBase, payload.orders)
245
- }
292
+ // If server omits schema.dependencies updates, ensure it's present locally
293
+ ensureSchemaDependencies(currentBase)
246
294
  await writeProjectAndFs(currentBase)
247
295
  writeLock({ pulledAt: new Date().toISOString() })
248
296
  if (options.verbose) console.log(chalk.gray('Applied incoming ops to local workspace'))
@@ -268,7 +316,8 @@ export async function startCollab(options) {
268
316
  const { loadModule } = await import('./require.js')
269
317
  await buildDirectory(distDir, outputDir)
270
318
  const loaded = await loadModule(outputFile, { silent: true, noCache: true })
271
- return loaded
319
+ // Ensure a plain, mutable object (avoid getter-only export objects)
320
+ return normalizeKeys(loaded)
272
321
  } catch (e) {
273
322
  if (options.verbose) console.error('Build failed while watching:', e.message)
274
323
  return null
@@ -312,9 +361,17 @@ export async function startCollab(options) {
312
361
  const filename = entries[j]
313
362
  if (!filename.endsWith('.js') || filename === 'index.js') continue
314
363
 
315
- const key = filename.slice(0, -3)
364
+ const fileStem = filename.slice(0, -3)
365
+ const key = type === 'pages'
366
+ ? toPagesRouteKeyFromFileStem(fileStem)
367
+ : fileStem
368
+ const altKey = type === 'pages' ? fileStem : null
369
+
370
+ // Skip if already present (support legacy/broken non-slash page keys too)
316
371
  if (Object.prototype.hasOwnProperty.call(container, key)) continue
372
+ if (altKey && Object.prototype.hasOwnProperty.call(container, altKey)) continue
317
373
  if (Object.prototype.hasOwnProperty.call(baseSection, key)) continue
374
+ if (altKey && Object.prototype.hasOwnProperty.call(baseSection, altKey)) continue
318
375
 
319
376
  const compiledPath = path.join(outputDir, type, filename)
320
377
  let mod
@@ -332,7 +389,14 @@ export async function startCollab(options) {
332
389
 
333
390
  let value = null
334
391
  if (mod && typeof mod === 'object') {
335
- value = mod.default || mod[key] || null
392
+ const exportName = toExportNameFromFileStem(fileStem)
393
+ value =
394
+ mod.default ||
395
+ mod[exportName] ||
396
+ mod[fileStem] ||
397
+ mod[key] ||
398
+ (altKey ? mod[altKey] : null) ||
399
+ null
336
400
  }
337
401
  if (!value || typeof value !== 'object') continue
338
402
 
@@ -343,9 +407,11 @@ export async function startCollab(options) {
343
407
 
344
408
  const sendLocalChanges = debounce(async () => {
345
409
  if (suppressLocalChanges) return
346
- const local = await loadLocalProject()
410
+ let local = await loadLocalProject()
347
411
  if (!local) return
348
412
  await augmentLocalWithNewFsItems(local)
413
+ // Include package.json deps into local snapshot so dependency edits can be synced
414
+ local = augmentProjectWithLocalPackageDependencies(local, packageJsonPath) || local
349
415
  // Prepare safe, JSON-serialisable snapshots for diffing & transport
350
416
  const base = currentBase || {}
351
417
  const safeBase = stringifyFunctionsForTransport(base)
@@ -371,9 +437,6 @@ export async function startCollab(options) {
371
437
  ? granularChanges
372
438
  : changes
373
439
  applyTuples(currentBase, tuplesToApply)
374
- if (Array.isArray(orders) && orders.length) {
375
- applyOrders(currentBase, orders)
376
- }
377
440
  } catch (e) {
378
441
  if (options.verbose) {
379
442
  console.error('Failed to apply local ops to in-memory base', e)
@@ -407,6 +470,18 @@ export async function startCollab(options) {
407
470
  .on('change', onFsEvent)
408
471
  .on('unlink', onFsEvent)
409
472
 
473
+ // Also watch package.json for dependency changes
474
+ if (packageJsonPath) {
475
+ const pkgWatcher = chokidar.watch(packageJsonPath, {
476
+ ignoreInitial: true,
477
+ persistent: true
478
+ })
479
+ pkgWatcher
480
+ .on('add', onFsEvent)
481
+ .on('change', onFsEvent)
482
+ .on('unlink', onFsEvent)
483
+ }
484
+
410
485
  console.log(chalk.green('Watching local changes and syncing over socket...'))
411
486
  console.log(chalk.gray('Press Ctrl+C to exit'))
412
487
  }
package/bin/fetch.js CHANGED
@@ -12,6 +12,8 @@ import { getCurrentProjectData } from '../helpers/apiUtils.js'
12
12
  import { showAuthRequiredMessages } from '../helpers/buildMessages.js'
13
13
  import { loadSymbolsConfig, resolveDistDir } from '../helpers/symbolsConfig.js'
14
14
  import { loadCliConfig, readLock, writeLock, updateLegacySymbolsJson, getConfigPaths } from '../helpers/config.js'
15
+ import { ensureSchemaDependencies, findNearestPackageJson, syncPackageJsonDependencies } from '../helpers/dependenciesUtils.js'
16
+ import { stripOrderFields } from '../helpers/orderUtils.js'
15
17
  const { isObjectLike } = (utils.default || utils)
16
18
 
17
19
  const debugMsg = chalk.dim(
@@ -81,7 +83,9 @@ export const fetchFromCli = async (opts) => {
81
83
  try {
82
84
  const { projectPath } = getConfigPaths()
83
85
  await fs.promises.mkdir(path.dirname(projectPath), { recursive: true })
84
- await fs.promises.writeFile(projectPath, JSON.stringify(payload, null, 2))
86
+ // Ensure schema.dependencies exists for payload.dependencies
87
+ ensureSchemaDependencies(payload)
88
+ await fs.promises.writeFile(projectPath, JSON.stringify(stripOrderFields(payload), null, 2))
85
89
  } catch (e) {
86
90
  console.error(chalk.bold.red('\nError writing file'))
87
91
  if (verbose) console.error(e)
@@ -89,6 +93,19 @@ export const fetchFromCli = async (opts) => {
89
93
  process.exit(1)
90
94
  }
91
95
 
96
+ // Sync project dependencies into local package.json
97
+ try {
98
+ const packageJsonPath = findNearestPackageJson(process.cwd())
99
+ if (packageJsonPath && payload?.dependencies) {
100
+ const res = syncPackageJsonDependencies(packageJsonPath, payload.dependencies, { overwriteExisting: true })
101
+ if (verbose && res?.ok && res.changed) {
102
+ console.log(chalk.gray(`Updated package.json dependencies from fetched project data`))
103
+ }
104
+ }
105
+ } catch (e) {
106
+ if (verbose) console.error('Failed updating package.json dependencies', e)
107
+ }
108
+
92
109
  const { version: fetchedVersion, ...config } = payload
93
110
 
94
111
  for (const t in config) {
@@ -117,9 +134,9 @@ export const fetchFromCli = async (opts) => {
117
134
  }
118
135
 
119
136
  if (update || force) {
120
- createFs(payload, distDir, { update: true, metadata: false })
137
+ createFs(stripOrderFields(payload), distDir, { update: true, metadata: false })
121
138
  } else {
122
- createFs(payload, distDir, { metadata: false })
139
+ createFs(stripOrderFields(payload), distDir, { metadata: false })
123
140
  }
124
141
  }
125
142
 
package/bin/fs.js CHANGED
@@ -226,25 +226,38 @@ export async function createFs (
226
226
  const dirs = []
227
227
 
228
228
  if (body[key] && isObject(body[key])) {
229
- const promises = Object.entries(body[key])
230
- // Skip meta/reserved identifier entries (e.g. "__order", "default")
231
- .filter(([entryKey]) => !shouldSkipEntryKey(entryKey))
232
- .map(
233
- async ([entryKey, value]) => {
234
- // if pages
229
+ // Normalize + dedupe entries before writing files. This is especially
230
+ // important for `pages` where both "/node" and "node" can otherwise map
231
+ // to the same filename and cause concurrent writes / corrupted output.
232
+ const normalized = new Map()
233
+ for (const [rawEntryKey, value] of Object.entries(body[key])) {
234
+ if (shouldSkipEntryKey(rawEntryKey)) continue
235
+
236
+ let entryKey = rawEntryKey
237
+ if (key === 'pages') {
235
238
  if (entryKey.startsWith('/')) entryKey = entryKey.slice(1)
236
239
  if (entryKey === '') entryKey = 'main'
237
240
  if (entryKey.includes('*')) entryKey = 'fallback'
241
+ }
238
242
 
239
- await createOrUpdateFile(dirPath, entryKey, value, update)
240
- dirs.push(entryKey)
243
+ // Prefer the canonical slash-prefixed route form when there are collisions
244
+ const priority = key === 'pages' && rawEntryKey.startsWith('/') ? 1 : 0
245
+ const existing = normalized.get(entryKey)
246
+ if (!existing || priority > existing.priority) {
247
+ normalized.set(entryKey, { value, priority })
241
248
  }
242
- )
249
+ }
250
+
251
+ const promises = Array.from(normalized.entries()).map(async ([entryKey, info]) => {
252
+ await createOrUpdateFile(dirPath, entryKey, info.value, update)
253
+ dirs.push(entryKey)
254
+ })
243
255
 
244
256
  await Promise.all(promises)
245
257
  }
246
258
 
247
- await generateIndexjsFile(dirs, dirPath, key)
259
+ // Ensure deterministic + unique index generation
260
+ await generateIndexjsFile(Array.from(new Set(dirs)), dirPath, key)
248
261
  }
249
262
 
250
263
  async function createOrUpdateFile(dirPath, childKey, value, update) {
package/bin/push.js CHANGED
@@ -14,6 +14,8 @@ import { computeCoarseChanges, computeOrdersForTuples, preprocessChanges } from
14
14
  import { showAuthRequiredMessages, showProjectNotFoundMessages, showBuildErrorMessages } from '../helpers/buildMessages.js'
15
15
  import { loadSymbolsConfig, resolveDistDir } from '../helpers/symbolsConfig.js'
16
16
  import { loadCliConfig, readLock, writeLock, updateLegacySymbolsJson } from '../helpers/config.js'
17
+ import { stripOrderFields } from '../helpers/orderUtils.js'
18
+ import { augmentProjectWithLocalPackageDependencies, findNearestPackageJson } from '../helpers/dependenciesUtils.js'
17
19
 
18
20
 
19
21
  async function buildLocalProject (distDir) {
@@ -117,11 +119,16 @@ export async function pushProjectChanges(options) {
117
119
  resolveDistDir(symbolsConfig) ||
118
120
  path.join(process.cwd(), 'smbls')
119
121
 
122
+ const packageJsonPath = findNearestPackageJson(process.cwd())
123
+
120
124
  // Build and load local project
121
125
  console.log(chalk.dim('Building local project...'))
122
126
  let localProject
123
127
  try {
124
128
  localProject = await buildLocalProject(distDir)
129
+ localProject = augmentProjectWithLocalPackageDependencies(localProject, packageJsonPath) || localProject
130
+ // Never push `__order` (platform metadata) from local files
131
+ localProject = stripOrderFields(localProject)
125
132
  console.log(chalk.gray('Local project built successfully'))
126
133
  } catch (buildError) {
127
134
  showBuildErrorMessages(buildError)
@@ -146,8 +153,8 @@ export async function pushProjectChanges(options) {
146
153
  console.log(chalk.gray('Server state fetched successfully'))
147
154
 
148
155
  // Calculate coarse local changes vs server snapshot (or base)
149
- const base = normalizeKeys(serverProject || {})
150
- const changes = computeCoarseChanges(base, localProject)
156
+ const base = normalizeKeys(stripOrderFields(serverProject || {}))
157
+ const changes = computeCoarseChanges(base, stripOrderFields(localProject))
151
158
 
152
159
  if (!changes.length) {
153
160
  console.log(chalk.bold.yellow('\nNo changes to push'))
@@ -178,7 +185,7 @@ export async function pushProjectChanges(options) {
178
185
  const operationId = `cli-${Date.now()}`
179
186
  // Derive granular changes against server base and compute orders using local for pending children
180
187
  const { granularChanges } = preprocessChanges(base, changes)
181
- const orders = computeOrdersForTuples(localProject, granularChanges)
188
+ const orders = computeOrdersForTuples(stripOrderFields(localProject), granularChanges)
182
189
  const result = await postProjectChanges(projectId, authToken, {
183
190
  branch,
184
191
  type,
package/bin/sync.js CHANGED
@@ -16,6 +16,13 @@ import { createFs } from './fs.js'
16
16
  import { showAuthRequiredMessages, showBuildErrorMessages } from '../helpers/buildMessages.js'
17
17
  import { loadSymbolsConfig, resolveDistDir } from '../helpers/symbolsConfig.js'
18
18
  import { loadCliConfig, readLock, writeLock, getConfigPaths, updateLegacySymbolsJson } from '../helpers/config.js'
19
+ import { stripOrderFields } from '../helpers/orderUtils.js'
20
+ import {
21
+ augmentProjectWithLocalPackageDependencies,
22
+ ensureSchemaDependencies,
23
+ findNearestPackageJson,
24
+ syncPackageJsonDependencies
25
+ } from '../helpers/dependenciesUtils.js'
19
26
 
20
27
  async function buildLocalProject(distDir) {
21
28
  try {
@@ -167,6 +174,8 @@ export async function syncProjectChanges(options) {
167
174
  resolveDistDir(symbolsConfig) ||
168
175
  path.join(process.cwd(), 'smbls')
169
176
 
177
+ const packageJsonPath = findNearestPackageJson(process.cwd())
178
+
170
179
  if (options.verbose) {
171
180
  console.log(chalk.dim('\nSync configuration:'))
172
181
  console.log(chalk.gray(`App Key: ${chalk.cyan(appKey)}`))
@@ -182,6 +191,10 @@ export async function syncProjectChanges(options) {
182
191
  let localProject
183
192
  try {
184
193
  localProject = await buildLocalProject(distDir)
194
+ // Include local package.json dependencies into the project object for diffing/sync
195
+ localProject = augmentProjectWithLocalPackageDependencies(localProject, packageJsonPath) || localProject
196
+ // Never sync/persist `__order` (platform metadata)
197
+ localProject = stripOrderFields(localProject)
185
198
  console.log(chalk.gray('Local project built successfully'))
186
199
  } catch (buildError) {
187
200
  showBuildErrorMessages(buildError)
@@ -192,7 +205,7 @@ export async function syncProjectChanges(options) {
192
205
  const baseSnapshot = (() => {
193
206
  try {
194
207
  const raw = fs.readFileSync(projectPath, 'utf8')
195
- return JSON.parse(raw)
208
+ return stripOrderFields(JSON.parse(raw))
196
209
  } catch (_) {
197
210
  return {}
198
211
  }
@@ -208,10 +221,15 @@ export async function syncProjectChanges(options) {
208
221
  const serverProject = serverResp.data || {}
209
222
  console.log(chalk.gray('Server data fetched successfully'))
210
223
 
224
+ // Ensure schema.dependencies exists wherever dependencies exist (base/remote/local)
225
+ ensureSchemaDependencies(baseSnapshot)
226
+ ensureSchemaDependencies(serverProject)
227
+ ensureSchemaDependencies(localProject)
228
+
211
229
  // Generate coarse local and remote changes via simple three-way rebase
212
230
  const base = normalizeKeys(baseSnapshot || {})
213
- const local = normalizeKeys(localProject || {})
214
- const remote = normalizeKeys(serverProject || {})
231
+ const local = normalizeKeys(stripOrderFields(localProject || {}))
232
+ const remote = normalizeKeys(stripOrderFields(serverProject || {}))
215
233
  const { ours, theirs, conflicts, finalChanges } = threeWayRebase(base, local, remote)
216
234
 
217
235
  const localChanges = ours
@@ -290,9 +308,17 @@ export async function syncProjectChanges(options) {
290
308
  )
291
309
  const updatedServerData = updated?.data || {}
292
310
 
311
+ // Ensure fetched snapshot has dependency schema and sync deps into local package.json
312
+ try {
313
+ ensureSchemaDependencies(updatedServerData)
314
+ if (packageJsonPath && updatedServerData?.dependencies) {
315
+ syncPackageJsonDependencies(packageJsonPath, updatedServerData.dependencies, { overwriteExisting: true })
316
+ }
317
+ } catch (_) {}
318
+
293
319
  // Apply changes to local files
294
320
  console.log(chalk.dim('Updating local files...'))
295
- await createFs(updatedServerData, distDir, { update: true, metadata: false })
321
+ await createFs(stripOrderFields(updatedServerData), distDir, { update: true, metadata: false })
296
322
  console.log(chalk.gray('Local files updated successfully'))
297
323
 
298
324
  console.log(chalk.bold.green('\nProject synced successfully!'))
@@ -308,7 +334,7 @@ export async function syncProjectChanges(options) {
308
334
  })
309
335
  try {
310
336
  const { projectPath } = getConfigPaths()
311
- await fs.promises.writeFile(projectPath, JSON.stringify(updatedServerData, null, 2))
337
+ await fs.promises.writeFile(projectPath, JSON.stringify(stripOrderFields(updatedServerData), null, 2))
312
338
  } catch (_) {}
313
339
 
314
340
  } catch (error) {
@@ -10,6 +10,9 @@ export const DATA_KEYS = [
10
10
  ]
11
11
 
12
12
  const SCHEMA_CODE_TYPES = new Set(['pages', 'components', 'functions', 'methods', 'snippets'])
13
+ // Types that should auto-create a schema entry when a new key is added.
14
+ // NOTE: dependencies schema objects are not "code", but we still want them present for transport.
15
+ const SCHEMA_AUTO_CREATE_TYPES = new Set([...SCHEMA_CODE_TYPES, 'dependencies'])
13
16
 
14
17
  function stripMetaDeep(val) {
15
18
  if (Array.isArray(val)) {
@@ -103,7 +106,7 @@ export function computeCoarseChanges(base, local, keys = DATA_KEYS) {
103
106
  // New item
104
107
  changes.push(['update', [typeKey, itemKey], bVal])
105
108
  const hadSchema = aSchemaSection && Object.prototype.hasOwnProperty.call(aSchemaSection, itemKey)
106
- if (SCHEMA_CODE_TYPES.has(typeKey) && !hadSchema) {
109
+ if (SCHEMA_AUTO_CREATE_TYPES.has(typeKey) && !hadSchema) {
107
110
  const schemaItem = buildSchemaItemFromData(typeKey, itemKey, bVal)
108
111
  if (schemaItem) {
109
112
  changes.push(['update', ['schema', typeKey, itemKey], schemaItem])
@@ -112,8 +115,10 @@ export function computeCoarseChanges(base, local, keys = DATA_KEYS) {
112
115
  } else if (!equal(aVal, bVal)) {
113
116
  // Updated item
114
117
  changes.push(['update', [typeKey, itemKey], bVal])
115
- // When an item changes, drop its schema.code to be regenerated
116
- changes.push(['delete', ['schema', typeKey, itemKey, 'code']])
118
+ // When a code-backed item changes, drop its schema.code to be regenerated
119
+ if (SCHEMA_CODE_TYPES.has(typeKey)) {
120
+ changes.push(['delete', ['schema', typeKey, itemKey, 'code']])
121
+ }
117
122
  }
118
123
  }
119
124
  }
@@ -277,6 +282,20 @@ export function buildSchemaCodeFromObject(obj) {
277
282
  function buildSchemaItemFromData(type, key, value) {
278
283
  const schemaType = type
279
284
 
285
+ if (schemaType === 'dependencies') {
286
+ const version =
287
+ typeof value === 'string' && value.length
288
+ ? value
289
+ : (value && typeof value === 'object' && typeof value.version === 'string' ? value.version : 'latest')
290
+ return {
291
+ key,
292
+ resolvedVersion: version,
293
+ type: 'dependency',
294
+ version,
295
+ status: 'done'
296
+ }
297
+ }
298
+
280
299
  const base = {
281
300
  title: key,
282
301
  key,
@@ -0,0 +1,174 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+
4
+ function isPlainObject(val) {
5
+ return !!val && typeof val === 'object' && !Array.isArray(val)
6
+ }
7
+
8
+ export function findNearestPackageJson(startDir = process.cwd()) {
9
+ let cur = path.resolve(startDir)
10
+ for (let i = 0; i < 50; i++) {
11
+ const candidate = path.join(cur, 'package.json')
12
+ if (fs.existsSync(candidate)) return candidate
13
+ const parent = path.dirname(cur)
14
+ if (parent === cur) break
15
+ cur = parent
16
+ }
17
+ return null
18
+ }
19
+
20
+ export function readPackageJson(packageJsonPath) {
21
+ try {
22
+ const raw = fs.readFileSync(packageJsonPath, 'utf8')
23
+ const parsed = JSON.parse(raw)
24
+ return isPlainObject(parsed) ? parsed : null
25
+ } catch (_) {
26
+ return null
27
+ }
28
+ }
29
+
30
+ export function writePackageJson(packageJsonPath, json) {
31
+ try {
32
+ fs.writeFileSync(packageJsonPath, JSON.stringify(json, null, 2) + '\n')
33
+ return true
34
+ } catch (_) {
35
+ return false
36
+ }
37
+ }
38
+
39
+ export function getPackageDependencies(packageJsonPath) {
40
+ const pkg = readPackageJson(packageJsonPath)
41
+ if (!pkg) return {}
42
+ const deps = isPlainObject(pkg.dependencies) ? pkg.dependencies : {}
43
+ return { ...deps }
44
+ }
45
+
46
+ function sortObjectKeys(obj) {
47
+ if (!isPlainObject(obj)) return obj
48
+ const out = {}
49
+ Object.keys(obj).sort().forEach((k) => {
50
+ out[k] = obj[k]
51
+ })
52
+ return out
53
+ }
54
+
55
+ /**
56
+ * Merge dependency map into package.json dependencies.
57
+ * - If overwriteExisting is true, remote versions win.
58
+ * - If false, only missing deps are added.
59
+ */
60
+ export function syncPackageJsonDependencies(packageJsonPath, depsMap, { overwriteExisting = true } = {}) {
61
+ if (!packageJsonPath) return { ok: false, reason: 'missing_package_json_path' }
62
+ const pkg = readPackageJson(packageJsonPath)
63
+ if (!pkg) return { ok: false, reason: 'invalid_package_json' }
64
+ if (!isPlainObject(depsMap) || !Object.keys(depsMap).length) {
65
+ return { ok: true, changed: false }
66
+ }
67
+
68
+ const existing = isPlainObject(pkg.dependencies) ? { ...pkg.dependencies } : {}
69
+ let changed = false
70
+
71
+ for (const [name, ver] of Object.entries(depsMap)) {
72
+ if (typeof name !== 'string' || !name) continue
73
+ if (typeof ver !== 'string' || !ver) continue
74
+ if (!Object.prototype.hasOwnProperty.call(existing, name)) {
75
+ existing[name] = ver
76
+ changed = true
77
+ continue
78
+ }
79
+ if (overwriteExisting && existing[name] !== ver) {
80
+ existing[name] = ver
81
+ changed = true
82
+ }
83
+ }
84
+
85
+ if (!changed) return { ok: true, changed: false }
86
+ pkg.dependencies = sortObjectKeys(existing)
87
+ const ok = writePackageJson(packageJsonPath, pkg)
88
+ return { ok, changed: ok }
89
+ }
90
+
91
+ function ensureSchemaContainer(project) {
92
+ if (!project || typeof project !== 'object') return null
93
+ if (!isPlainObject(project.schema)) project.schema = {}
94
+ return project.schema
95
+ }
96
+
97
+ export function ensureSchemaDependencies(project) {
98
+ if (!project || typeof project !== 'object') return project
99
+ const deps = isPlainObject(project.dependencies) ? project.dependencies : null
100
+ if (!deps) return project
101
+
102
+ const schema = ensureSchemaContainer(project)
103
+ if (!isPlainObject(schema.dependencies)) schema.dependencies = {}
104
+
105
+ for (const [name, ver] of Object.entries(deps)) {
106
+ if (typeof name !== 'string' || !name) continue
107
+ const version = typeof ver === 'string' && ver.length ? ver : 'latest'
108
+ const existing = schema.dependencies[name]
109
+ if (isPlainObject(existing)) {
110
+ if (typeof existing.key !== 'string') existing.key = name
111
+ if (typeof existing.type !== 'string') existing.type = 'dependency'
112
+ if (typeof existing.version !== 'string') existing.version = version
113
+ if (typeof existing.resolvedVersion !== 'string') existing.resolvedVersion = version
114
+ if (typeof existing.status !== 'string') existing.status = 'done'
115
+ } else {
116
+ schema.dependencies[name] = {
117
+ key: name,
118
+ resolvedVersion: version,
119
+ type: 'dependency',
120
+ version,
121
+ status: 'done'
122
+ }
123
+ }
124
+ }
125
+ return project
126
+ }
127
+
128
+ /**
129
+ * Augment a project object with local package.json dependencies so sync/collab
130
+ * can detect and push dependency additions/updates.
131
+ */
132
+ export function augmentProjectWithLocalPackageDependencies(project, packageJsonPath) {
133
+ if (!project || typeof project !== 'object') return project
134
+ if (!packageJsonPath) return project
135
+ const pkgDeps = getPackageDependencies(packageJsonPath)
136
+ if (!Object.keys(pkgDeps).length) return project
137
+
138
+ // Some build outputs expose getters-only exports (e.g. Babel interop / CJS wrappers).
139
+ // Avoid mutating such objects by cloning into a plain, extensible object.
140
+ let target = project
141
+ try {
142
+ const desc = Object.getOwnPropertyDescriptor(target, 'dependencies')
143
+ const canAssign =
144
+ !desc ||
145
+ desc.writable === true ||
146
+ typeof desc.set === 'function'
147
+ if (!canAssign || !Object.isExtensible(target)) {
148
+ target = { ...target }
149
+ }
150
+ } catch (_) {
151
+ target = { ...target }
152
+ }
153
+
154
+ const existing = isPlainObject(target.dependencies) ? target.dependencies : {}
155
+ const merged = { ...existing, ...pkgDeps }
156
+
157
+ try {
158
+ target.dependencies = merged
159
+ } catch (_) {
160
+ try {
161
+ Object.defineProperty(target, 'dependencies', {
162
+ value: merged,
163
+ enumerable: true,
164
+ configurable: true,
165
+ writable: true
166
+ })
167
+ } catch (_) {}
168
+ }
169
+
170
+ ensureSchemaDependencies(target)
171
+ return target
172
+ }
173
+
174
+
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Strip Symbols ordering metadata (`__order`) from a project object.
3
+ *
4
+ * We treat `__order` as platform/transport metadata and avoid persisting it
5
+ * into local repositories (generated files or base snapshots).
6
+ *
7
+ * - Preserves functions and non-plain objects as-is
8
+ * - Clones arrays and plain objects
9
+ * - Handles cycles via WeakMap
10
+ */
11
+ export function stripOrderFields(input) {
12
+ const seen = new WeakMap()
13
+
14
+ function isPlainObject(v) {
15
+ return Object.prototype.toString.call(v) === '[object Object]'
16
+ }
17
+
18
+ function walk(value) {
19
+ if (Array.isArray(value)) {
20
+ if (seen.has(value)) return seen.get(value)
21
+ const out = new Array(value.length)
22
+ seen.set(value, out)
23
+ for (let i = 0; i < value.length; i++) out[i] = walk(value[i])
24
+ return out
25
+ }
26
+
27
+ if (value && typeof value === 'object') {
28
+ // Keep non-plain objects (Date, Map, etc.) as-is
29
+ if (!isPlainObject(value)) return value
30
+ if (seen.has(value)) return seen.get(value)
31
+ const out = {}
32
+ seen.set(value, out)
33
+ for (const k of Object.keys(value)) {
34
+ if (k === '__order') continue
35
+ out[k] = walk(value[k])
36
+ }
37
+ return out
38
+ }
39
+
40
+ return value
41
+ }
42
+
43
+ return walk(input)
44
+ }
45
+
46
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbo.ls/cli",
3
- "version": "2.33.35",
3
+ "version": "2.33.36",
4
4
  "description": "Fetch your Symbols configuration",
5
5
  "main": "bin/fetch.js",
6
6
  "author": "Symbols",
@@ -15,9 +15,9 @@
15
15
  "vpatch": "npm version patch && npm publish"
16
16
  },
17
17
  "dependencies": {
18
- "@symbo.ls/fetch": "^2.33.35",
19
- "@symbo.ls/init": "^2.33.35",
20
- "@symbo.ls/socket": "^2.33.35",
18
+ "@symbo.ls/fetch": "^2.33.36",
19
+ "@symbo.ls/init": "^2.33.36",
20
+ "@symbo.ls/socket": "^2.33.36",
21
21
  "chalk": "^5.4.1",
22
22
  "chokidar": "^4.0.3",
23
23
  "commander": "^13.1.0",
@@ -28,5 +28,5 @@
28
28
  "socket.io-client": "^4.8.1",
29
29
  "v8-compile-cache": "^2.4.0"
30
30
  },
31
- "gitHead": "3ad860e75a24c41afaac79d639f7a9162c2f4c81"
31
+ "gitHead": "c49777e3aee153b8b68f55a2aa9a3a5988483e2f"
32
32
  }