framer-code-link 0.1.3 → 0.2.0
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/README.md +1 -1
- package/dist/{index.js → index.mjs} +703 -260
- package/package.json +12 -12
- package/src/controller.test.ts +63 -138
- package/src/controller.ts +296 -109
- package/src/helpers/connection.ts +18 -11
- package/src/helpers/files.ts +125 -40
- package/src/helpers/installer.ts +12 -16
- package/src/helpers/user-actions.ts +7 -11
- package/src/helpers/watcher.test.ts +74 -0
- package/src/helpers/watcher.ts +4 -9
- package/src/index.ts +7 -4
- package/src/types.ts +8 -3
- package/src/utils/{hashing.ts → hash-tracker.ts} +1 -17
- package/src/utils/logging.ts +194 -6
- package/src/utils/project.ts +15 -8
- package/dist/project-DhpsFg77.js +0 -53
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "framer-code-link",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "CLI tool for syncing Framer code components - controller-centric architecture",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -19,18 +19,18 @@
|
|
|
19
19
|
"author": "",
|
|
20
20
|
"license": "MIT",
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@typescript/ata": "^0.9.
|
|
23
|
-
"chokidar": "^
|
|
24
|
-
"commander": "^
|
|
25
|
-
"prettier": "^3.
|
|
26
|
-
"typescript": "^5.
|
|
27
|
-
"ws": "^8.18.
|
|
22
|
+
"@typescript/ata": "^0.9.8",
|
|
23
|
+
"chokidar": "^5.0.0",
|
|
24
|
+
"commander": "^14.0.2",
|
|
25
|
+
"prettier": "^3.7.4",
|
|
26
|
+
"typescript": "^5.9.3",
|
|
27
|
+
"ws": "^8.18.3"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@types/node": "^22.
|
|
31
|
-
"@types/ws": "^8.
|
|
32
|
-
"tsdown": "^0.
|
|
33
|
-
"tsx": "^4.
|
|
34
|
-
"vitest": "^
|
|
30
|
+
"@types/node": "^22.19.2",
|
|
31
|
+
"@types/ws": "^8.18.1",
|
|
32
|
+
"tsdown": "^0.17.4",
|
|
33
|
+
"tsx": "^4.21.0",
|
|
34
|
+
"vitest": "^4.0.15"
|
|
35
35
|
}
|
|
36
36
|
}
|
package/src/controller.test.ts
CHANGED
|
@@ -1,98 +1,7 @@
|
|
|
1
|
-
import { describe, it, expect
|
|
2
|
-
import { transition } from "./controller.js"
|
|
3
|
-
import { createHashTracker } from "./utils/
|
|
4
|
-
import type { Conflict, Config } from "./types.js"
|
|
5
|
-
import type { Installer } from "./helpers/installer.js"
|
|
6
|
-
import * as connection from "./helpers/connection.js"
|
|
7
|
-
import * as filesHelper from "./helpers/files.js"
|
|
8
|
-
import * as statePersistence from "./utils/state-persistence.js"
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import { filterEchoedFiles, transition } from "./controller.js"
|
|
3
|
+
import { createHashTracker } from "./utils/hash-tracker.js"
|
|
9
4
|
import type { WebSocket } from "ws"
|
|
10
|
-
import { UserActionCoordinator } from "./helpers/user-actions.js"
|
|
11
|
-
|
|
12
|
-
vi.mock("ws", () => {
|
|
13
|
-
class MockWebSocket {
|
|
14
|
-
send(
|
|
15
|
-
_data: string,
|
|
16
|
-
callback: (err?: Error | null) => void = () => undefined
|
|
17
|
-
) {
|
|
18
|
-
callback(null)
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
class MockWebSocketServer {
|
|
23
|
-
constructor(_options: unknown) {}
|
|
24
|
-
on(): void {}
|
|
25
|
-
close(): void {}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return {
|
|
29
|
-
WebSocket: MockWebSocket,
|
|
30
|
-
WebSocketServer: MockWebSocketServer,
|
|
31
|
-
}
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
const sendMessageSpy = vi.spyOn(connection, "sendMessage")
|
|
35
|
-
const writeRemoteFilesSpy = vi.spyOn(filesHelper, "writeRemoteFiles")
|
|
36
|
-
const savePersistedStateSpy = vi.spyOn(statePersistence, "savePersistedState")
|
|
37
|
-
const readFileSafeSpy = vi.spyOn(filesHelper, "readFileSafe")
|
|
38
|
-
|
|
39
|
-
function createState(): RuntimeState {
|
|
40
|
-
return {
|
|
41
|
-
socket: {} as WebSocket,
|
|
42
|
-
initialSyncComplete: false,
|
|
43
|
-
pendingWrites: [],
|
|
44
|
-
pendingLocalAdds: [],
|
|
45
|
-
lastRemoteSync: new Map(),
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function createConfig(): Config & { projectDir: string; filesDir: string } {
|
|
50
|
-
return {
|
|
51
|
-
port: 0,
|
|
52
|
-
projectHash: "test-project",
|
|
53
|
-
projectDir: "/tmp/project",
|
|
54
|
-
filesDir: "/tmp/project/files",
|
|
55
|
-
dangerouslyAutoDelete: false,
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function createInstaller(): Installer {
|
|
60
|
-
return {
|
|
61
|
-
process: vi.fn(),
|
|
62
|
-
} as unknown as Installer
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function createMockUserActions(): UserActionCoordinator & {
|
|
66
|
-
requestDeleteDecision: ReturnType<typeof vi.fn>
|
|
67
|
-
requestConflictDecisions: ReturnType<typeof vi.fn>
|
|
68
|
-
handleConfirmation: ReturnType<typeof vi.fn>
|
|
69
|
-
cleanup: ReturnType<typeof vi.fn>
|
|
70
|
-
} {
|
|
71
|
-
return {
|
|
72
|
-
requestDeleteDecision: vi.fn(),
|
|
73
|
-
requestConflictDecisions: vi.fn(),
|
|
74
|
-
handleConfirmation: vi.fn(),
|
|
75
|
-
cleanup: vi.fn(),
|
|
76
|
-
} as unknown as UserActionCoordinator & {
|
|
77
|
-
requestDeleteDecision: ReturnType<typeof vi.fn>
|
|
78
|
-
requestConflictDecisions: ReturnType<typeof vi.fn>
|
|
79
|
-
handleConfirmation: ReturnType<typeof vi.fn>
|
|
80
|
-
cleanup: ReturnType<typeof vi.fn>
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
beforeEach(() => {
|
|
85
|
-
sendMessageSpy.mockReset()
|
|
86
|
-
sendMessageSpy.mockResolvedValue()
|
|
87
|
-
writeRemoteFilesSpy.mockReset()
|
|
88
|
-
writeRemoteFilesSpy.mockResolvedValue()
|
|
89
|
-
savePersistedStateSpy.mockReset()
|
|
90
|
-
savePersistedStateSpy.mockResolvedValue()
|
|
91
|
-
readFileSafeSpy.mockReset()
|
|
92
|
-
readFileSafeSpy.mockResolvedValue(null)
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
// Legacy tests - commented out after state machine migration
|
|
96
5
|
describe("State Machine", () => {
|
|
97
6
|
describe("HANDSHAKE transition", () => {
|
|
98
7
|
it("transitions from disconnected to handshaking", () => {
|
|
@@ -176,7 +85,7 @@ describe("State Machine", () => {
|
|
|
176
85
|
expect(result.effects[0]).toMatchObject({ type: "PERSIST_STATE" })
|
|
177
86
|
expect(result.effects[1]).toMatchObject({
|
|
178
87
|
type: "LOG",
|
|
179
|
-
level: "
|
|
88
|
+
level: "debug",
|
|
180
89
|
})
|
|
181
90
|
})
|
|
182
91
|
})
|
|
@@ -206,7 +115,7 @@ describe("State Machine", () => {
|
|
|
206
115
|
expect(result.effects).toHaveLength(2)
|
|
207
116
|
expect(result.effects[0]).toMatchObject({
|
|
208
117
|
type: "LOG",
|
|
209
|
-
level: "
|
|
118
|
+
level: "debug",
|
|
210
119
|
})
|
|
211
120
|
expect(result.effects[1]).toMatchObject({
|
|
212
121
|
type: "DETECT_CONFLICTS",
|
|
@@ -362,7 +271,7 @@ describe("State Machine", () => {
|
|
|
362
271
|
})
|
|
363
272
|
|
|
364
273
|
describe("REMOTE_FILE_DELETE transition", () => {
|
|
365
|
-
it("
|
|
274
|
+
it("applies delete immediately in watching mode", () => {
|
|
366
275
|
const initialState = {
|
|
367
276
|
mode: "watching" as const,
|
|
368
277
|
socket: {} as WebSocket,
|
|
@@ -387,36 +296,9 @@ describe("State Machine", () => {
|
|
|
387
296
|
})
|
|
388
297
|
|
|
389
298
|
expect(result.state.mode).toBe("watching")
|
|
390
|
-
expect(
|
|
391
|
-
|
|
392
|
-
).toBe(true)
|
|
393
|
-
const confirmEffect = result.effects.find(
|
|
394
|
-
(e) => e.type === "REQUEST_DELETE_CONFIRMATION"
|
|
299
|
+
expect(result.effects.some((e) => e.type === "DELETE_LOCAL_FILES")).toBe(
|
|
300
|
+
true
|
|
395
301
|
)
|
|
396
|
-
expect(confirmEffect).toMatchObject({
|
|
397
|
-
type: "REQUEST_DELETE_CONFIRMATION",
|
|
398
|
-
fileName: "Test.tsx",
|
|
399
|
-
requireConfirmation: true,
|
|
400
|
-
})
|
|
401
|
-
})
|
|
402
|
-
|
|
403
|
-
it("auto-deletes when autoDelete is true", () => {
|
|
404
|
-
const initialState = {
|
|
405
|
-
mode: "watching" as const,
|
|
406
|
-
socket: {} as WebSocket,
|
|
407
|
-
files: new Map(),
|
|
408
|
-
queuedDiffs: [],
|
|
409
|
-
pendingOperations: new Map(),
|
|
410
|
-
nextOperationId: 1,
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
const result = transition(initialState, {
|
|
414
|
-
type: "REMOTE_FILE_DELETE",
|
|
415
|
-
fileName: "Test.tsx",
|
|
416
|
-
autoDelete: true,
|
|
417
|
-
})
|
|
418
|
-
|
|
419
|
-
expect(result.state.mode).toBe("watching")
|
|
420
302
|
const deleteEffect = result.effects.find(
|
|
421
303
|
(e) => e.type === "DELETE_LOCAL_FILES"
|
|
422
304
|
)
|
|
@@ -424,12 +306,10 @@ describe("State Machine", () => {
|
|
|
424
306
|
type: "DELETE_LOCAL_FILES",
|
|
425
307
|
names: ["Test.tsx"],
|
|
426
308
|
})
|
|
427
|
-
expect(
|
|
428
|
-
result.effects.some((e) => e.type === "PERSIST_STATE")
|
|
429
|
-
).toBe(true)
|
|
309
|
+
expect(result.effects.some((e) => e.type === "PERSIST_STATE")).toBe(true)
|
|
430
310
|
})
|
|
431
311
|
|
|
432
|
-
it("
|
|
312
|
+
it("applies deletes immediately during snapshot processing", () => {
|
|
433
313
|
const initialState = {
|
|
434
314
|
mode: "snapshot_processing" as const,
|
|
435
315
|
socket: {} as WebSocket,
|
|
@@ -445,7 +325,9 @@ describe("State Machine", () => {
|
|
|
445
325
|
})
|
|
446
326
|
|
|
447
327
|
expect(result.state.mode).toBe("snapshot_processing")
|
|
448
|
-
expect(result.effects.some((e) => e.type === "
|
|
328
|
+
expect(result.effects.some((e) => e.type === "DELETE_LOCAL_FILES")).toBe(
|
|
329
|
+
true
|
|
330
|
+
)
|
|
449
331
|
expect(result.effects.some((e) => e.type === "LOG")).toBe(true)
|
|
450
332
|
})
|
|
451
333
|
|
|
@@ -465,7 +347,9 @@ describe("State Machine", () => {
|
|
|
465
347
|
})
|
|
466
348
|
|
|
467
349
|
expect(result.state.mode).toBe("disconnected")
|
|
468
|
-
expect(result.effects.some((e) => e.type === "
|
|
350
|
+
expect(result.effects.some((e) => e.type === "DELETE_LOCAL_FILES")).toBe(
|
|
351
|
+
false
|
|
352
|
+
)
|
|
469
353
|
expect(
|
|
470
354
|
result.effects.some((e) => e.type === "LOG" && e.level === "warn")
|
|
471
355
|
).toBe(true)
|
|
@@ -665,7 +549,7 @@ describe("State Machine", () => {
|
|
|
665
549
|
|
|
666
550
|
it("ignores events when disconnected", () => {
|
|
667
551
|
const initialState = {
|
|
668
|
-
mode: "
|
|
552
|
+
mode: "disconnected" as const,
|
|
669
553
|
socket: null,
|
|
670
554
|
files: new Map(),
|
|
671
555
|
queuedDiffs: [],
|
|
@@ -774,13 +658,15 @@ describe("State Machine", () => {
|
|
|
774
658
|
const writeEffects = result.effects.filter(
|
|
775
659
|
(e) => e.type === "WRITE_FILES"
|
|
776
660
|
)
|
|
777
|
-
|
|
661
|
+
// Each conflict gets its own WRITE_FILES effect
|
|
662
|
+
expect(writeEffects).toHaveLength(2)
|
|
778
663
|
expect(writeEffects[0]).toMatchObject({
|
|
779
664
|
type: "WRITE_FILES",
|
|
780
|
-
files: [
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
665
|
+
files: [{ name: "Test1.tsx", content: "remote 1" }],
|
|
666
|
+
})
|
|
667
|
+
expect(writeEffects[1]).toMatchObject({
|
|
668
|
+
type: "WRITE_FILES",
|
|
669
|
+
files: [{ name: "Test2.tsx", content: "remote 2" }],
|
|
784
670
|
})
|
|
785
671
|
expect(result.effects.some((e) => e.type === "PERSIST_STATE")).toBe(true)
|
|
786
672
|
})
|
|
@@ -963,4 +849,43 @@ describe("State Machine", () => {
|
|
|
963
849
|
}
|
|
964
850
|
})
|
|
965
851
|
})
|
|
852
|
+
|
|
853
|
+
describe("echo prevention filter", () => {
|
|
854
|
+
it("skips inbound file-change that matches last local send", () => {
|
|
855
|
+
const hashTracker = createHashTracker()
|
|
856
|
+
hashTracker.remember("Hey.tsx", "content")
|
|
857
|
+
|
|
858
|
+
const filtered = filterEchoedFiles(
|
|
859
|
+
[
|
|
860
|
+
{
|
|
861
|
+
name: "Hey.tsx",
|
|
862
|
+
content: "content",
|
|
863
|
+
modifiedAt: Date.now(),
|
|
864
|
+
},
|
|
865
|
+
],
|
|
866
|
+
hashTracker
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
expect(filtered).toHaveLength(0)
|
|
870
|
+
})
|
|
871
|
+
|
|
872
|
+
it("keeps inbound change when content differs", () => {
|
|
873
|
+
const hashTracker = createHashTracker()
|
|
874
|
+
hashTracker.remember("Hey.tsx", "old content")
|
|
875
|
+
|
|
876
|
+
const filtered = filterEchoedFiles(
|
|
877
|
+
[
|
|
878
|
+
{
|
|
879
|
+
name: "Hey.tsx",
|
|
880
|
+
content: "new content",
|
|
881
|
+
modifiedAt: Date.now(),
|
|
882
|
+
},
|
|
883
|
+
],
|
|
884
|
+
hashTracker
|
|
885
|
+
)
|
|
886
|
+
|
|
887
|
+
expect(filtered).toHaveLength(1)
|
|
888
|
+
expect(filtered[0]?.content).toBe("new content")
|
|
889
|
+
})
|
|
890
|
+
})
|
|
966
891
|
})
|