framer-code-link 0.1.3 → 0.1.4
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/dist/index.js +551 -144
- package/package.json +12 -12
- package/src/controller.test.ts +22 -137
- package/src/controller.ts +242 -106
- package/src/helpers/connection.ts +10 -10
- package/src/helpers/files.ts +99 -37
- package/src/helpers/installer.ts +11 -11
- package/src/helpers/user-actions.ts +7 -11
- package/src/helpers/watcher.ts +4 -9
- package/src/index.ts +7 -4
- package/src/types.ts +4 -2
- package/src/utils/{hashing.ts → hash-tracker.ts} +1 -17
- package/src/utils/logging.ts +191 -6
- package/src/utils/project.ts +15 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "framer-code-link",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
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": "^2.1.
|
|
30
|
+
"@types/node": "^22.19.2",
|
|
31
|
+
"@types/ws": "^8.18.1",
|
|
32
|
+
"tsdown": "^0.17.3",
|
|
33
|
+
"tsx": "^4.21.0",
|
|
34
|
+
"vitest": "^2.1.9"
|
|
35
35
|
}
|
|
36
36
|
}
|
package/src/controller.test.ts
CHANGED
|
@@ -1,98 +1,6 @@
|
|
|
1
|
-
import { describe, it, expect
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
2
|
import { transition } from "./controller.js"
|
|
3
|
-
import { createHashTracker } from "./utils/hashing.js"
|
|
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"
|
|
9
3
|
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
4
|
describe("State Machine", () => {
|
|
97
5
|
describe("HANDSHAKE transition", () => {
|
|
98
6
|
it("transitions from disconnected to handshaking", () => {
|
|
@@ -176,7 +84,7 @@ describe("State Machine", () => {
|
|
|
176
84
|
expect(result.effects[0]).toMatchObject({ type: "PERSIST_STATE" })
|
|
177
85
|
expect(result.effects[1]).toMatchObject({
|
|
178
86
|
type: "LOG",
|
|
179
|
-
level: "
|
|
87
|
+
level: "debug",
|
|
180
88
|
})
|
|
181
89
|
})
|
|
182
90
|
})
|
|
@@ -206,7 +114,7 @@ describe("State Machine", () => {
|
|
|
206
114
|
expect(result.effects).toHaveLength(2)
|
|
207
115
|
expect(result.effects[0]).toMatchObject({
|
|
208
116
|
type: "LOG",
|
|
209
|
-
level: "
|
|
117
|
+
level: "debug",
|
|
210
118
|
})
|
|
211
119
|
expect(result.effects[1]).toMatchObject({
|
|
212
120
|
type: "DETECT_CONFLICTS",
|
|
@@ -362,7 +270,7 @@ describe("State Machine", () => {
|
|
|
362
270
|
})
|
|
363
271
|
|
|
364
272
|
describe("REMOTE_FILE_DELETE transition", () => {
|
|
365
|
-
it("
|
|
273
|
+
it("applies delete immediately in watching mode", () => {
|
|
366
274
|
const initialState = {
|
|
367
275
|
mode: "watching" as const,
|
|
368
276
|
socket: {} as WebSocket,
|
|
@@ -387,36 +295,9 @@ describe("State Machine", () => {
|
|
|
387
295
|
})
|
|
388
296
|
|
|
389
297
|
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"
|
|
298
|
+
expect(result.effects.some((e) => e.type === "DELETE_LOCAL_FILES")).toBe(
|
|
299
|
+
true
|
|
395
300
|
)
|
|
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
301
|
const deleteEffect = result.effects.find(
|
|
421
302
|
(e) => e.type === "DELETE_LOCAL_FILES"
|
|
422
303
|
)
|
|
@@ -424,12 +305,10 @@ describe("State Machine", () => {
|
|
|
424
305
|
type: "DELETE_LOCAL_FILES",
|
|
425
306
|
names: ["Test.tsx"],
|
|
426
307
|
})
|
|
427
|
-
expect(
|
|
428
|
-
result.effects.some((e) => e.type === "PERSIST_STATE")
|
|
429
|
-
).toBe(true)
|
|
308
|
+
expect(result.effects.some((e) => e.type === "PERSIST_STATE")).toBe(true)
|
|
430
309
|
})
|
|
431
310
|
|
|
432
|
-
it("
|
|
311
|
+
it("applies deletes immediately during snapshot processing", () => {
|
|
433
312
|
const initialState = {
|
|
434
313
|
mode: "snapshot_processing" as const,
|
|
435
314
|
socket: {} as WebSocket,
|
|
@@ -445,7 +324,9 @@ describe("State Machine", () => {
|
|
|
445
324
|
})
|
|
446
325
|
|
|
447
326
|
expect(result.state.mode).toBe("snapshot_processing")
|
|
448
|
-
expect(result.effects.some((e) => e.type === "
|
|
327
|
+
expect(result.effects.some((e) => e.type === "DELETE_LOCAL_FILES")).toBe(
|
|
328
|
+
true
|
|
329
|
+
)
|
|
449
330
|
expect(result.effects.some((e) => e.type === "LOG")).toBe(true)
|
|
450
331
|
})
|
|
451
332
|
|
|
@@ -465,7 +346,9 @@ describe("State Machine", () => {
|
|
|
465
346
|
})
|
|
466
347
|
|
|
467
348
|
expect(result.state.mode).toBe("disconnected")
|
|
468
|
-
expect(result.effects.some((e) => e.type === "
|
|
349
|
+
expect(result.effects.some((e) => e.type === "DELETE_LOCAL_FILES")).toBe(
|
|
350
|
+
false
|
|
351
|
+
)
|
|
469
352
|
expect(
|
|
470
353
|
result.effects.some((e) => e.type === "LOG" && e.level === "warn")
|
|
471
354
|
).toBe(true)
|
|
@@ -665,7 +548,7 @@ describe("State Machine", () => {
|
|
|
665
548
|
|
|
666
549
|
it("ignores events when disconnected", () => {
|
|
667
550
|
const initialState = {
|
|
668
|
-
mode: "
|
|
551
|
+
mode: "disconnected" as const,
|
|
669
552
|
socket: null,
|
|
670
553
|
files: new Map(),
|
|
671
554
|
queuedDiffs: [],
|
|
@@ -774,13 +657,15 @@ describe("State Machine", () => {
|
|
|
774
657
|
const writeEffects = result.effects.filter(
|
|
775
658
|
(e) => e.type === "WRITE_FILES"
|
|
776
659
|
)
|
|
777
|
-
|
|
660
|
+
// Each conflict gets its own WRITE_FILES effect
|
|
661
|
+
expect(writeEffects).toHaveLength(2)
|
|
778
662
|
expect(writeEffects[0]).toMatchObject({
|
|
779
663
|
type: "WRITE_FILES",
|
|
780
|
-
files: [
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
664
|
+
files: [{ name: "Test1.tsx", content: "remote 1" }],
|
|
665
|
+
})
|
|
666
|
+
expect(writeEffects[1]).toMatchObject({
|
|
667
|
+
type: "WRITE_FILES",
|
|
668
|
+
files: [{ name: "Test2.tsx", content: "remote 2" }],
|
|
784
669
|
})
|
|
785
670
|
expect(result.effects.some((e) => e.type === "PERSIST_STATE")).toBe(true)
|
|
786
671
|
})
|