@symbo.ls/sync 3.1.2 → 3.2.7

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/index.js CHANGED
@@ -1,85 +1,254 @@
1
- 'use strict'
2
-
3
1
  import { router } from '@domql/router'
4
- import { init } from '@symbo.ls/init'
5
- import { connect } from '@symbo.ls/socket/client'
6
- import { window, overwriteShallow } from '@domql/utils'
2
+ import { init } from 'smbls'
3
+ import { io } from 'socket.io-client'
4
+ import { window, overwriteShallow, overwriteDeep } from '@domql/utils'
7
5
  import { connectedToSymbols, Notifications } from './SyncNotifications'
8
6
  import { Inspect } from './Inspect'
9
7
  export { Inspect, Notifications }
10
8
 
11
- const isLocalhost = window && window.location && window.location.host.includes('local')
9
+ const isLocal = process.env.NODE_ENV === 'local'
10
+
11
+ // ---------------------------------------------
12
+ // Utility helpers to apply ops
12
13
 
13
- const onConnect = (element, state) => {
14
- return (socketId, socket) => {
15
- // send('components', { COMPONENTS: a(COMPONENTS) })
14
+ const deletePath = (obj, path) => {
15
+ if (!obj || !Array.isArray(path)) {
16
+ return
16
17
  }
18
+ path.reduce((acc, v, i, arr) => {
19
+ if (acc && v in acc) {
20
+ if (i !== arr.length - 1) {
21
+ return acc[v]
22
+ }
23
+ delete acc[v]
24
+ }
25
+ return void 0
26
+ }, obj)
17
27
  }
18
28
 
19
- const onDisconnect = (element, state) => {
20
- return () => {}
29
+ const setPath = (obj, path, value, createNestedObjects = false) => {
30
+ if (!obj || !Array.isArray(path)) {
31
+ return
32
+ }
33
+ path.reduce((acc, v, i, arr) => {
34
+ if (!acc) {
35
+ return void 0
36
+ }
37
+ if (i !== arr.length - 1) {
38
+ if (!acc[v] && createNestedObjects) {
39
+ acc[v] = {}
40
+ }
41
+ return acc[v]
42
+ }
43
+ acc[v] = value
44
+ return void 0
45
+ }, obj)
21
46
  }
22
47
 
23
- const onChange = (el, s, ctx) => {
24
- return (event, data) => {
25
- if (event === 'change') {
26
- const obj = JSON.parse(data)
27
- if (!obj?.DATA) return
28
- const { state, designSystem, pages, components, snippets, functions } = obj.DATA
29
- const { utils } = ctx
30
-
31
- if (pages) {
32
- // overwriteShallow(ctx.pages, pages)
33
- overwriteShallow(ctx.pages, pages)
34
- }
48
+ const applyOpsToCtx = (ctx, changes) => {
49
+ const topLevelChanged = new Set()
50
+ if (!Array.isArray(changes)) {
51
+ return topLevelChanged
52
+ }
53
+ for (const [action, path, change] of changes) {
54
+ if (!Array.isArray(path) || !path.length) {
55
+ continue
56
+ }
57
+ topLevelChanged.add(path[0])
58
+ switch (action) {
59
+ case 'delete':
60
+ deletePath(ctx, path)
61
+ break
62
+ case 'update':
63
+ case 'set':
64
+ setPath(ctx, path, change, true)
65
+ break
66
+ default:
67
+ // Unsupported action – ignore
68
+ break
69
+ }
70
+ }
71
+ return topLevelChanged
72
+ }
35
73
 
36
- if (components) {
37
- overwriteShallow(ctx.components, components)
38
- }
74
+ // ---------------------------------------------
39
75
 
40
- if (functions) {
41
- overwriteShallow(ctx.functions, functions)
42
- }
76
+ const fetchServiceToken = async () => {
77
+ try {
78
+ const urlBase = isLocal
79
+ ? 'http://localhost:8080'
80
+ : 'https://api.symbols.app'
81
+ const res = await window.fetch(`${urlBase}/service-token`, {
82
+ method: 'GET'
83
+ })
43
84
 
44
- if (snippets) {
45
- overwriteShallow(ctx.snippets, snippets)
85
+ // Attempt to parse JSON first – recent versions return `{ token: "..." }`
86
+ // Fall back to treating the response as raw text for backward-compatibility.
87
+ let txt
88
+ try {
89
+ const json = await res.clone().json()
90
+ if (json && typeof json.token === 'string') {
91
+ return json.token.trim()
46
92
  }
93
+ // If json parsing succeeds but no token field, fall back to text below.
94
+ txt = await res.text()
95
+ } catch {
96
+ // Response is not JSON – treat as plain text token.
97
+ txt = await res.text()
98
+ }
99
+
100
+ return (txt || '').replace(/\s+/gu, '') || void 0
101
+ } catch (e) {
102
+ console.error('[sync] Failed to fetch service-token', e)
103
+ }
104
+ }
47
105
 
48
- if (state) {
49
- const route = state.route
50
- if (route) (utils.router || router)(route.replace('/state', '') || '/', el, {})
51
- else if (!(snippets && functions && components && pages)) s.update(state)
106
+ const onSnapshot =
107
+ (el, s, ctx) =>
108
+ (payload = {}) => {
109
+ let { data } = payload
110
+ const { schema } = payload
111
+ if (!data) {
112
+ return
113
+ }
114
+
115
+ data = el.call(
116
+ 'deepDestringifyFunctions',
117
+ data,
118
+ Array.isArray(data) ? [] : {}
119
+ )
120
+
121
+ // Overwrite high-level objects shallowly so references are preserved
122
+ Object.entries(data).forEach(([key, val]) => {
123
+ if (ctx[key] && typeof ctx[key] === 'object') {
124
+ if (key === 'designSystem') {
125
+ init(val)
126
+ } else {
127
+ overwriteShallow(ctx[key], val)
128
+ }
129
+ } else {
130
+ ctx[key] = val
52
131
  }
132
+ })
133
+
134
+ // Optionally make schema available on ctx
135
+ if (schema) {
136
+ ctx.schema = schema
137
+ }
138
+ }
53
139
 
54
- if (snippets || functions || components || pages) {
55
- const { pathname, search, hash } = ctx.window.location
56
- ;(utils.router || router)(pathname + search + hash, el, {})
140
+ const onOps =
141
+ (el, s, ctx) =>
142
+ (payload = {}) => {
143
+ let { changes } = payload
144
+ if (!changes || !Array.isArray(changes) || !changes.length) {
145
+ return
146
+ }
147
+
148
+ changes = el.call(
149
+ 'deepDestringifyFunctions',
150
+ changes,
151
+ Array.isArray(changes) ? [] : {}
152
+ )
153
+
154
+
155
+ const changed = applyOpsToCtx(ctx, changes)
156
+
157
+ // React to specific top-level changes
158
+ if (changed.has('state')) {
159
+ const route = ctx.state?.route
160
+ if (route) {
161
+ el.call(
162
+ 'router',
163
+ route.replace('/state', '') || '/',
164
+ el.__ref.root,
165
+ {},
166
+ { scrollToTop: false }
167
+ )
168
+ } else {
169
+ s.update(ctx.state)
57
170
  }
171
+ }
58
172
 
59
- if (designSystem) init(designSystem)
173
+ if (
174
+ ['pages', 'components', 'snippets', 'functions'].some((k) =>
175
+ changed.has(k)
176
+ )
177
+ ) {
178
+ const { pathname, search, hash } = ctx.window.location
179
+ el.call(
180
+ 'router',
181
+ pathname + search + hash,
182
+ el.__ref.root,
183
+ {},
184
+ { scrollToTop: false }
185
+ )
60
186
  }
61
187
 
62
- if (ctx.editor.verbose && event === 'clients') {
63
- connectedToSymbols(data, el, s)
188
+ if (changed.has('designSystem')) {
189
+ init(ctx.designSystem)
64
190
  }
65
191
  }
66
- }
67
192
 
68
- export const connectToSocket = (el, s, ctx) => {
69
- return connect(ctx.key, {
70
- source: isLocalhost ? 'localhost' : 'client',
71
- socketUrl: isLocalhost ? 'localhost:13336' : 'socket.symbols.app',
72
- location: window.location.host,
73
- onConnect: onConnect(el, s, ctx),
74
- onDisconnect: onDisconnect(el, s, ctx),
75
- onChange: onChange(el, s, ctx)
193
+ export const connectToSocket = async (el, s, ctx) => {
194
+ const token = await fetchServiceToken()
195
+ if (!token) {
196
+ console.warn('[sync] No service token live collaboration disabled')
197
+ return null
198
+ }
199
+
200
+ const projectKey = ctx.key
201
+ if (!projectKey) {
202
+ console.warn(
203
+ '[sync] ctx.key missing – cannot establish collaborative connection'
204
+ )
205
+ return null
206
+ }
207
+
208
+ const socketBaseUrl = isLocal
209
+ ? 'http://localhost:8080'
210
+ : 'https://api.symbols.app'
211
+
212
+ const socket = io(socketBaseUrl, {
213
+ path: '/collab-socket',
214
+ transports: ['websocket'],
215
+ auth: {
216
+ token,
217
+ projectKey,
218
+ branch: 'main',
219
+ live: true,
220
+ clientType: 'platform'
221
+ },
222
+ reconnectionAttempts: Infinity,
223
+ reconnectionDelayMax: 4000
76
224
  })
225
+
226
+ socket.on('connect', () => {
227
+ if (ctx.editor?.verbose) {
228
+ console.info('[sync] Connected to collab socket')
229
+ }
230
+ })
231
+
232
+ socket.on('snapshot', onSnapshot(el, s, ctx))
233
+ socket.on('ops', onOps(el, s, ctx))
234
+
235
+ socket.on('clients', (data) => {
236
+ if (ctx.editor?.verbose) {
237
+ connectedToSymbols(data, el, s)
238
+ }
239
+ })
240
+
241
+ socket.on('disconnect', (reason) => {
242
+ if (ctx.editor?.verbose) {
243
+ console.info('[sync] Disconnected from collab socket', reason)
244
+ }
245
+ })
246
+
247
+ return socket
77
248
  }
78
249
 
79
250
  export const SyncComponent = {
80
- on: {
81
- initSync: connectToSocket
82
- }
251
+ onInitSync: connectToSocket
83
252
  }
84
253
 
85
254
  export const DefaultSyncApp = {
package/package.json CHANGED
@@ -1,42 +1,68 @@
1
1
  {
2
2
  "name": "@symbo.ls/sync",
3
- "version": "3.1.2",
4
- "main": "index.js",
5
- "module": "index.js",
6
- "gitHead": "429b36616aa04c8587a26ce3c129815115e35897",
3
+ "version": "3.2.7",
4
+ "main": "./index.js",
5
+ "module": "./index.js",
6
+ "gitHead": "9fc1b79b41cdc725ca6b24aec64920a599634681",
7
7
  "files": [
8
- "*.js",
9
- "dist"
8
+ "dist",
9
+ "*.js"
10
10
  ],
11
11
  "repository": "https://github.com/symbo-ls/scratch",
12
12
  "type": "module",
13
- "unpkg": "dist/iife/index.js",
14
- "jsdelivr": "dist/iife/index.js",
13
+ "unpkg": "./dist/iife/index.js",
14
+ "jsdelivr": "./dist/iife/index.js",
15
15
  "exports": {
16
16
  ".": {
17
- "kalduna": "./index.js",
18
- "default": "./dist/cjs/index.js"
17
+ "import": "./index.js",
18
+ "require": "./index.js",
19
+ "browser": "./index.js",
20
+ "default": "./index.js"
21
+ },
22
+ "./client": {
23
+ "import": "./client.js",
24
+ "require": "./client.js",
25
+ "default": "./client.js"
26
+ },
27
+ "./server": {
28
+ "import": "./server.js",
29
+ "default": "./server.js"
30
+ },
31
+ "./*.js": {
32
+ "import": "./dist/esm/*.js",
33
+ "require": "./dist/cjs/*.js",
34
+ "default": "./dist/esm/*.js"
35
+ },
36
+ "./*": {
37
+ "import": "./dist/esm/*.js",
38
+ "require": "./dist/cjs/*.js",
39
+ "default": "./dist/esm/*.js"
19
40
  }
20
41
  },
21
42
  "source": "index.js",
22
43
  "publishConfig": {},
23
44
  "scripts": {
24
45
  "copy:package:cjs": "cp ../../build/package-cjs.json dist/cjs/package.json",
25
- "build:esm": "npx esbuild *.js --target=es2017 --format=esm --outdir=dist/esm --loader:.svg=text",
26
- "build:cjs": "npx esbuild *.js --target=node16 --format=cjs --outdir=dist/cjs --loader:.svg=text",
27
- "build:iife": "npx esbuild ./index.js --target=es2017 --format=iife --outdir=dist/iife --loader:.svg=text --bundle --minify",
28
- "build": "npm run build:esm; npm run build:cjs",
29
- "prepublish": "rimraf -I dist && npm run build && npm run copy:package:cjs"
46
+ "build:esm": "cross-env NODE_ENV=$NODE_ENV esbuild *.js --target=es2020 --format=esm --outdir=dist/esm --define:process.env.NODE_ENV=process.env.NODE_ENV",
47
+ "build:cjs": "cross-env NODE_ENV=$NODE_ENV esbuild *.js --target=node18 --format=cjs --outdir=dist/cjs --define:process.env.NODE_ENV=process.env.NODE_ENV",
48
+ "build:iife": "cross-env NODE_ENV=$NODE_ENV esbuild index.js --bundle --target=es2020 --format=iife --global-name=SmblsSync --outfile=dist/iife/index.js --define:process.env.NODE_ENV=process.env.NODE_ENV --external:smbls --external:@domql/* --external:@symbo.ls/* --external:socket.io-client --external:socket.io --external:express --external:css-in-props",
49
+ "build": "node ../../build/build.js",
50
+ "prepublish": "npm run build && npm run copy:package:cjs"
30
51
  },
31
52
  "dependencies": {
32
- "@domql/router": "^3.1.2",
33
- "@domql/utils": "^3.1.2",
34
- "@symbo.ls/init": "^3.1.2",
35
- "@symbo.ls/scratch": "^3.1.2",
36
- "@symbo.ls/socket": "^3.1.2",
37
- "@symbo.ls/uikit": "^3.1.2"
53
+ "@domql/router": "^3.2.3",
54
+ "@domql/utils": "^3.2.3",
55
+ "@symbo.ls/scratch": "^3.2.3",
56
+ "@symbo.ls/uikit": "^3.2.3",
57
+ "chalk": "^5.4.1",
58
+ "express": "^4.21.2",
59
+ "http": "^0.0.1-security",
60
+ "socket.io": "^4.8.1",
61
+ "socket.io-client": "^4.8.1"
38
62
  },
39
63
  "devDependencies": {
40
64
  "@babel/core": "^7.26.0"
41
- }
65
+ },
66
+ "browser": "./dist/iife/index.js",
67
+ "sideEffects": false
42
68
  }
package/server.js ADDED
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs'
4
+ import chalk from 'chalk'
5
+ import express from 'express'
6
+ import http from 'http'
7
+ import { Server } from 'socket.io'
8
+ import { createRequire } from 'module'
9
+ import * as utils from '@domql/utils'
10
+ const { overwriteDeep } = utils.default || utils
11
+
12
+ const require = createRequire(import.meta.url) // construct the require method
13
+ const DES_SYS_DEFAULT_FILE = require('./dynamic.json') // Bring in the ability to create the 'require' method
14
+
15
+ const app = express()
16
+ let io
17
+
18
+ const debugMsg = chalk.dim(
19
+ 'Use --verbose to debug the error or open the issue at https://github.com/symbo-ls/smbls'
20
+ )
21
+
22
+ export const updateDynamycFile = (changes, options = {}) => {
23
+ const { verbose, prettify, verboseCode } = options
24
+ const file = require('./dynamic.json')
25
+
26
+ const newMerge = overwriteDeep(file, changes)
27
+ const mergeStr = JSON.stringify(newMerge, null, 2)
28
+ const initPath = `${process.cwd()}/node_modules/@symbo.ls/init/dynamic.json`
29
+
30
+ console.log(chalk.dim('\n----------------\n'))
31
+
32
+ console.log(chalk.dim('Received update:'))
33
+ console.log(Object.keys(changes).join(', '))
34
+ if (verboseCode)
35
+ console.log(chalk.dim(JSON.stringify(changes, null, prettify ?? 2)))
36
+
37
+ try {
38
+ fs.writeFileSync(initPath, mergeStr)
39
+ if (verbose) {
40
+ console.log(chalk.bold.green('\nChanges wrote to the file'))
41
+ }
42
+ } catch (e) {
43
+ console.log('')
44
+ console.log(chalk.bold.red('Error writing file'))
45
+ if (verbose) {
46
+ console.error(e)
47
+ } else {
48
+ console.log(debugMsg)
49
+ }
50
+ }
51
+ }
52
+
53
+ // eslint-disable-next-line no-unused-vars
54
+ export const sync = (desSysFile = DES_SYS_DEFAULT_FILE, opts = {}) => {
55
+ const server = http.createServer(app)
56
+ const { key } = opts
57
+
58
+ io = new Server(server, {
59
+ transports: ['websocket', 'polling', 'flashsocket'],
60
+ cors: {
61
+ origin: '*'
62
+ }
63
+ })
64
+
65
+ app.get('/', (req, res) => {
66
+ res.end('open')
67
+ })
68
+
69
+ io.on('connection', socket => {
70
+ socket.join(key)
71
+ let source
72
+
73
+ socket.on('initConnect', options => {
74
+ const { clientsCount } = io.engine
75
+ socket.to(key).emit('clientsCount', clientsCount)
76
+ source = options.source
77
+ console.log('Connected', key, source)
78
+ console.log('from', options.location)
79
+ })
80
+
81
+ socket.on('components', (data, options) => {
82
+ io.to(key).emit('change', data, options)
83
+ })
84
+
85
+ socket.on('route', (data, options) => {
86
+ io.to(key).emit('route', data, options)
87
+ })
88
+
89
+ socket.on('change', updateDynamycFile)
90
+
91
+ // eslint-disable-next-line no-unused-vars
92
+ socket.on('disconnect', (changes, options) => {
93
+ const { clientsCount } = io.engine
94
+ socket.to(key).emit('clientsCount', clientsCount)
95
+ console.log('Disconnected', key, source)
96
+ })
97
+ })
98
+
99
+ server.listen(13336, () => {
100
+ console.log('listening on *:13336')
101
+ })
102
+ }
@@ -1,4 +0,0 @@
1
- {
2
- "type": "commonjs",
3
- "main": "index.js"
4
- }