@symbo.ls/cli 2.33.35 → 2.33.37
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 +88 -13
- package/bin/fetch.js +20 -3
- package/bin/fs.js +23 -10
- package/bin/push.js +14 -4
- package/bin/sync.js +34 -6
- package/helpers/changesUtils.js +22 -3
- package/helpers/dependenciesUtils.js +174 -0
- package/helpers/orderUtils.js +46 -0
- package/package.json +5 -5
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(
|
|
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(
|
|
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
|
-
|
|
244
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
240
|
-
|
|
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
|
-
|
|
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,9 @@ 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'
|
|
19
|
+
import { stringifyFunctionsForTransport } from '../helpers/transportUtils.js'
|
|
17
20
|
|
|
18
21
|
|
|
19
22
|
async function buildLocalProject (distDir) {
|
|
@@ -117,11 +120,16 @@ export async function pushProjectChanges(options) {
|
|
|
117
120
|
resolveDistDir(symbolsConfig) ||
|
|
118
121
|
path.join(process.cwd(), 'smbls')
|
|
119
122
|
|
|
123
|
+
const packageJsonPath = findNearestPackageJson(process.cwd())
|
|
124
|
+
|
|
120
125
|
// Build and load local project
|
|
121
126
|
console.log(chalk.dim('Building local project...'))
|
|
122
127
|
let localProject
|
|
123
128
|
try {
|
|
124
129
|
localProject = await buildLocalProject(distDir)
|
|
130
|
+
localProject = augmentProjectWithLocalPackageDependencies(localProject, packageJsonPath) || localProject
|
|
131
|
+
// Never push `__order` (platform metadata) from local files
|
|
132
|
+
localProject = stripOrderFields(localProject)
|
|
125
133
|
console.log(chalk.gray('Local project built successfully'))
|
|
126
134
|
} catch (buildError) {
|
|
127
135
|
showBuildErrorMessages(buildError)
|
|
@@ -146,8 +154,10 @@ export async function pushProjectChanges(options) {
|
|
|
146
154
|
console.log(chalk.gray('Server state fetched successfully'))
|
|
147
155
|
|
|
148
156
|
// Calculate coarse local changes vs server snapshot (or base)
|
|
149
|
-
|
|
150
|
-
const
|
|
157
|
+
// Prepare safe, JSON-serialisable snapshots for diffing & transport (stringify functions)
|
|
158
|
+
const base = normalizeKeys(stringifyFunctionsForTransport(stripOrderFields(serverProject || {})))
|
|
159
|
+
const local = normalizeKeys(stringifyFunctionsForTransport(stripOrderFields(localProject)))
|
|
160
|
+
const changes = computeCoarseChanges(base, local)
|
|
151
161
|
|
|
152
162
|
if (!changes.length) {
|
|
153
163
|
console.log(chalk.bold.yellow('\nNo changes to push'))
|
|
@@ -162,7 +172,7 @@ export async function pushProjectChanges(options) {
|
|
|
162
172
|
})
|
|
163
173
|
|
|
164
174
|
// Confirm push
|
|
165
|
-
const shouldProceed = await confirmChanges(changes, base,
|
|
175
|
+
const shouldProceed = await confirmChanges(changes, base, local)
|
|
166
176
|
if (!shouldProceed) {
|
|
167
177
|
console.log(chalk.yellow('Push cancelled'))
|
|
168
178
|
return
|
|
@@ -178,7 +188,7 @@ export async function pushProjectChanges(options) {
|
|
|
178
188
|
const operationId = `cli-${Date.now()}`
|
|
179
189
|
// Derive granular changes against server base and compute orders using local for pending children
|
|
180
190
|
const { granularChanges } = preprocessChanges(base, changes)
|
|
181
|
-
const orders = computeOrdersForTuples(
|
|
191
|
+
const orders = computeOrdersForTuples(local, granularChanges)
|
|
182
192
|
const result = await postProjectChanges(projectId, authToken, {
|
|
183
193
|
branch,
|
|
184
194
|
type,
|
package/bin/sync.js
CHANGED
|
@@ -16,6 +16,14 @@ 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 { stringifyFunctionsForTransport } from '../helpers/transportUtils.js'
|
|
21
|
+
import {
|
|
22
|
+
augmentProjectWithLocalPackageDependencies,
|
|
23
|
+
ensureSchemaDependencies,
|
|
24
|
+
findNearestPackageJson,
|
|
25
|
+
syncPackageJsonDependencies
|
|
26
|
+
} from '../helpers/dependenciesUtils.js'
|
|
19
27
|
|
|
20
28
|
async function buildLocalProject(distDir) {
|
|
21
29
|
try {
|
|
@@ -167,6 +175,8 @@ export async function syncProjectChanges(options) {
|
|
|
167
175
|
resolveDistDir(symbolsConfig) ||
|
|
168
176
|
path.join(process.cwd(), 'smbls')
|
|
169
177
|
|
|
178
|
+
const packageJsonPath = findNearestPackageJson(process.cwd())
|
|
179
|
+
|
|
170
180
|
if (options.verbose) {
|
|
171
181
|
console.log(chalk.dim('\nSync configuration:'))
|
|
172
182
|
console.log(chalk.gray(`App Key: ${chalk.cyan(appKey)}`))
|
|
@@ -182,6 +192,10 @@ export async function syncProjectChanges(options) {
|
|
|
182
192
|
let localProject
|
|
183
193
|
try {
|
|
184
194
|
localProject = await buildLocalProject(distDir)
|
|
195
|
+
// Include local package.json dependencies into the project object for diffing/sync
|
|
196
|
+
localProject = augmentProjectWithLocalPackageDependencies(localProject, packageJsonPath) || localProject
|
|
197
|
+
// Never sync/persist `__order` (platform metadata)
|
|
198
|
+
localProject = stripOrderFields(localProject)
|
|
185
199
|
console.log(chalk.gray('Local project built successfully'))
|
|
186
200
|
} catch (buildError) {
|
|
187
201
|
showBuildErrorMessages(buildError)
|
|
@@ -192,7 +206,7 @@ export async function syncProjectChanges(options) {
|
|
|
192
206
|
const baseSnapshot = (() => {
|
|
193
207
|
try {
|
|
194
208
|
const raw = fs.readFileSync(projectPath, 'utf8')
|
|
195
|
-
return JSON.parse(raw)
|
|
209
|
+
return stripOrderFields(JSON.parse(raw))
|
|
196
210
|
} catch (_) {
|
|
197
211
|
return {}
|
|
198
212
|
}
|
|
@@ -208,10 +222,16 @@ export async function syncProjectChanges(options) {
|
|
|
208
222
|
const serverProject = serverResp.data || {}
|
|
209
223
|
console.log(chalk.gray('Server data fetched successfully'))
|
|
210
224
|
|
|
225
|
+
// Ensure schema.dependencies exists wherever dependencies exist (base/remote/local)
|
|
226
|
+
ensureSchemaDependencies(baseSnapshot)
|
|
227
|
+
ensureSchemaDependencies(serverProject)
|
|
228
|
+
ensureSchemaDependencies(localProject)
|
|
229
|
+
|
|
211
230
|
// Generate coarse local and remote changes via simple three-way rebase
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
const
|
|
231
|
+
// Prepare safe, JSON-serialisable snapshots for diffing & transport (stringify functions)
|
|
232
|
+
const base = normalizeKeys(stringifyFunctionsForTransport(stripOrderFields(baseSnapshot || {})))
|
|
233
|
+
const local = normalizeKeys(stringifyFunctionsForTransport(stripOrderFields(localProject || {})))
|
|
234
|
+
const remote = normalizeKeys(stringifyFunctionsForTransport(stripOrderFields(serverProject || {})))
|
|
215
235
|
const { ours, theirs, conflicts, finalChanges } = threeWayRebase(base, local, remote)
|
|
216
236
|
|
|
217
237
|
const localChanges = ours
|
|
@@ -290,9 +310,17 @@ export async function syncProjectChanges(options) {
|
|
|
290
310
|
)
|
|
291
311
|
const updatedServerData = updated?.data || {}
|
|
292
312
|
|
|
313
|
+
// Ensure fetched snapshot has dependency schema and sync deps into local package.json
|
|
314
|
+
try {
|
|
315
|
+
ensureSchemaDependencies(updatedServerData)
|
|
316
|
+
if (packageJsonPath && updatedServerData?.dependencies) {
|
|
317
|
+
syncPackageJsonDependencies(packageJsonPath, updatedServerData.dependencies, { overwriteExisting: true })
|
|
318
|
+
}
|
|
319
|
+
} catch (_) {}
|
|
320
|
+
|
|
293
321
|
// Apply changes to local files
|
|
294
322
|
console.log(chalk.dim('Updating local files...'))
|
|
295
|
-
await createFs(updatedServerData, distDir, { update: true, metadata: false })
|
|
323
|
+
await createFs(stripOrderFields(updatedServerData), distDir, { update: true, metadata: false })
|
|
296
324
|
console.log(chalk.gray('Local files updated successfully'))
|
|
297
325
|
|
|
298
326
|
console.log(chalk.bold.green('\nProject synced successfully!'))
|
|
@@ -308,7 +336,7 @@ export async function syncProjectChanges(options) {
|
|
|
308
336
|
})
|
|
309
337
|
try {
|
|
310
338
|
const { projectPath } = getConfigPaths()
|
|
311
|
-
await fs.promises.writeFile(projectPath, JSON.stringify(updatedServerData, null, 2))
|
|
339
|
+
await fs.promises.writeFile(projectPath, JSON.stringify(stripOrderFields(updatedServerData), null, 2))
|
|
312
340
|
} catch (_) {}
|
|
313
341
|
|
|
314
342
|
} catch (error) {
|
package/helpers/changesUtils.js
CHANGED
|
@@ -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 (
|
|
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
|
|
116
|
-
|
|
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.
|
|
3
|
+
"version": "2.33.37",
|
|
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.
|
|
19
|
-
"@symbo.ls/init": "^2.33.
|
|
20
|
-
"@symbo.ls/socket": "^2.33.
|
|
18
|
+
"@symbo.ls/fetch": "^2.33.37",
|
|
19
|
+
"@symbo.ls/init": "^2.33.37",
|
|
20
|
+
"@symbo.ls/socket": "^2.33.37",
|
|
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": "
|
|
31
|
+
"gitHead": "383eb71ccab284ca3b27477941b24d7152d51e67"
|
|
32
32
|
}
|