framer-code-link 0.1.1 → 0.1.2
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/package.json +1 -1
- package/src/controller.ts +34 -2
- package/src/helpers/connection.ts +47 -9
- package/src/index.ts +22 -2
- package/src/utils/project.ts +21 -2
package/package.json
CHANGED
package/src/controller.ts
CHANGED
|
@@ -760,7 +760,12 @@ async function executeEffect(
|
|
|
760
760
|
|
|
761
761
|
case "SEND_MESSAGE": {
|
|
762
762
|
if (syncState.socket) {
|
|
763
|
-
await sendMessage(syncState.socket, effect.payload)
|
|
763
|
+
const sent = await sendMessage(syncState.socket, effect.payload)
|
|
764
|
+
if (!sent) {
|
|
765
|
+
warn(`Failed to send message: ${effect.payload.type}`)
|
|
766
|
+
}
|
|
767
|
+
} else {
|
|
768
|
+
warn(`No socket available to send: ${effect.payload.type}`)
|
|
764
769
|
}
|
|
765
770
|
return []
|
|
766
771
|
}
|
|
@@ -976,11 +981,30 @@ export async function start(config: Config): Promise<void> {
|
|
|
976
981
|
// State Machine Execution Helper
|
|
977
982
|
// Process events through state machine and execute effects recursively
|
|
978
983
|
async function processEvent(event: SyncEvent) {
|
|
984
|
+
const socketState = syncState.socket?.readyState
|
|
985
|
+
info(
|
|
986
|
+
`[STATE] Processing event: ${event.type} (mode: ${syncState.mode}, socket: ${socketState ?? "none"})`
|
|
987
|
+
)
|
|
988
|
+
|
|
979
989
|
const result = transition(syncState, event)
|
|
980
990
|
syncState = result.state
|
|
981
991
|
|
|
992
|
+
if (result.effects.length > 0) {
|
|
993
|
+
info(
|
|
994
|
+
`[STATE] Event produced ${result.effects.length} effects: ${result.effects.map((e) => e.type).join(", ")}`
|
|
995
|
+
)
|
|
996
|
+
}
|
|
997
|
+
|
|
982
998
|
// Execute all effects and process any follow-up events
|
|
983
999
|
for (const effect of result.effects) {
|
|
1000
|
+
// Check socket state before each effect
|
|
1001
|
+
const currentSocketState = syncState.socket?.readyState
|
|
1002
|
+
if (currentSocketState !== undefined && currentSocketState !== 1) {
|
|
1003
|
+
warn(
|
|
1004
|
+
`[STATE] Socket not open (state: ${currentSocketState}) before executing ${effect.type}`
|
|
1005
|
+
)
|
|
1006
|
+
}
|
|
1007
|
+
|
|
984
1008
|
const followUpEvents = await executeEffect(effect, {
|
|
985
1009
|
config,
|
|
986
1010
|
hashTracker,
|
|
@@ -1051,9 +1075,17 @@ export async function start(config: Config): Promise<void> {
|
|
|
1051
1075
|
event = { type: "REQUEST_FILES" }
|
|
1052
1076
|
break
|
|
1053
1077
|
|
|
1054
|
-
case "file-list":
|
|
1078
|
+
case "file-list": {
|
|
1079
|
+
const totalSize = message.files.reduce(
|
|
1080
|
+
(sum, f) => sum + (f.content?.length ?? 0),
|
|
1081
|
+
0
|
|
1082
|
+
)
|
|
1083
|
+
info(
|
|
1084
|
+
`[FILE_LIST] Received ${message.files.length} files (${(totalSize / 1024).toFixed(1)}KB total)`
|
|
1085
|
+
)
|
|
1055
1086
|
event = { type: "FILE_LIST", files: message.files }
|
|
1056
1087
|
break
|
|
1088
|
+
}
|
|
1057
1089
|
|
|
1058
1090
|
case "file-change":
|
|
1059
1091
|
event = {
|
|
@@ -33,11 +33,13 @@ export interface Connection {
|
|
|
33
33
|
export function initConnection(port: number): Connection {
|
|
34
34
|
const wss = new WebSocketServer({ port })
|
|
35
35
|
const handlers: Partial<ConnectionCallbacks> = {}
|
|
36
|
+
let connectionId = 0
|
|
36
37
|
|
|
37
38
|
info(`WebSocket server listening on port ${port}`)
|
|
38
39
|
|
|
39
40
|
wss.on("connection", (ws: WebSocket) => {
|
|
40
|
-
|
|
41
|
+
const connId = ++connectionId
|
|
42
|
+
info(`[CONN ${connId}] Client connected (readyState: ${ws.readyState})`)
|
|
41
43
|
|
|
42
44
|
ws.on("message", (data: Buffer) => {
|
|
43
45
|
try {
|
|
@@ -45,22 +47,25 @@ export function initConnection(port: number): Connection {
|
|
|
45
47
|
|
|
46
48
|
// Special handling for handshake
|
|
47
49
|
if (message.type === "handshake") {
|
|
50
|
+
info(`[CONN ${connId}] Received handshake`)
|
|
48
51
|
handlers.onHandshake?.(ws, message)
|
|
49
52
|
} else {
|
|
50
53
|
handlers.onMessage?.(message)
|
|
51
54
|
}
|
|
52
55
|
} catch (err) {
|
|
53
|
-
error(
|
|
56
|
+
error(`[CONN ${connId}] Failed to parse message:`, err)
|
|
54
57
|
}
|
|
55
58
|
})
|
|
56
59
|
|
|
57
|
-
ws.on("close", () => {
|
|
58
|
-
info(
|
|
60
|
+
ws.on("close", (code, reason) => {
|
|
61
|
+
info(
|
|
62
|
+
`[CONN ${connId}] Client disconnected (code: ${code}, reason: ${reason?.toString() || "none"})`
|
|
63
|
+
)
|
|
59
64
|
handlers.onDisconnect?.()
|
|
60
65
|
})
|
|
61
66
|
|
|
62
67
|
ws.on("error", (err) => {
|
|
63
|
-
error(
|
|
68
|
+
error(`[CONN ${connId}] WebSocket error:`, err)
|
|
64
69
|
})
|
|
65
70
|
})
|
|
66
71
|
|
|
@@ -86,17 +91,50 @@ export function initConnection(port: number): Connection {
|
|
|
86
91
|
}
|
|
87
92
|
}
|
|
88
93
|
|
|
94
|
+
/**
|
|
95
|
+
* WebSocket readyState constants for reference
|
|
96
|
+
*/
|
|
97
|
+
const READY_STATE = {
|
|
98
|
+
CONNECTING: 0,
|
|
99
|
+
OPEN: 1,
|
|
100
|
+
CLOSING: 2,
|
|
101
|
+
CLOSED: 3,
|
|
102
|
+
} as const
|
|
103
|
+
|
|
104
|
+
function readyStateToString(state: number): string {
|
|
105
|
+
switch (state) {
|
|
106
|
+
case 0: return "CONNECTING"
|
|
107
|
+
case 1: return "OPEN"
|
|
108
|
+
case 2: return "CLOSING"
|
|
109
|
+
case 3: return "CLOSED"
|
|
110
|
+
default: return `UNKNOWN(${state})`
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
89
114
|
/**
|
|
90
115
|
* Sends a message to a connected socket
|
|
116
|
+
* Returns false if the socket is not open (instead of throwing)
|
|
91
117
|
*/
|
|
92
118
|
export function sendMessage(
|
|
93
119
|
socket: WebSocket,
|
|
94
120
|
message: OutgoingMessage
|
|
95
|
-
): Promise<
|
|
96
|
-
return new Promise((resolve
|
|
121
|
+
): Promise<boolean> {
|
|
122
|
+
return new Promise((resolve) => {
|
|
123
|
+
// Check socket state before attempting to send
|
|
124
|
+
if (socket.readyState !== READY_STATE.OPEN) {
|
|
125
|
+
const stateStr = readyStateToString(socket.readyState)
|
|
126
|
+
info(`[WS] Cannot send ${message.type}: socket is ${stateStr}`)
|
|
127
|
+
resolve(false)
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
97
131
|
socket.send(JSON.stringify(message), (err) => {
|
|
98
|
-
if (err)
|
|
99
|
-
|
|
132
|
+
if (err) {
|
|
133
|
+
error(`[WS] Send error for ${message.type}:`, err.message)
|
|
134
|
+
resolve(false)
|
|
135
|
+
} else {
|
|
136
|
+
resolve(true)
|
|
137
|
+
}
|
|
100
138
|
})
|
|
101
139
|
})
|
|
102
140
|
}
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { start } from "./controller.js"
|
|
|
12
12
|
import type { Config } from "./types.js"
|
|
13
13
|
import { setLogLevel, LogLevel, info } from "./utils/logging.js"
|
|
14
14
|
import { getPortFromHash } from "./utils/hashing.js"
|
|
15
|
+
import { getProjectHashFromCwd } from "./utils/project.js"
|
|
15
16
|
|
|
16
17
|
const program = new Command()
|
|
17
18
|
|
|
@@ -27,7 +28,10 @@ program
|
|
|
27
28
|
.name("code-link")
|
|
28
29
|
.description("Sync Framer code components to your local filesystem")
|
|
29
30
|
.version("0.1.0")
|
|
30
|
-
.argument(
|
|
31
|
+
.argument(
|
|
32
|
+
"[projectHash]",
|
|
33
|
+
"Framer Project ID Hash (auto-detected from package.json if omitted)"
|
|
34
|
+
)
|
|
31
35
|
.option("-n, --name <name>", "Project name (optional)")
|
|
32
36
|
.option("-d, --dir <directory>", "Explicit project directory")
|
|
33
37
|
.option("-v, --verbose", "Enable verbose logging")
|
|
@@ -36,7 +40,23 @@ program
|
|
|
36
40
|
"--dangerously-auto-delete",
|
|
37
41
|
"Automatically delete remote files without confirmation"
|
|
38
42
|
)
|
|
39
|
-
.action(async (projectHash: string, options) => {
|
|
43
|
+
.action(async (projectHash: string | undefined, options) => {
|
|
44
|
+
// If no projectHash provided, try to read from cwd's package.json
|
|
45
|
+
if (!projectHash) {
|
|
46
|
+
const detected = await getProjectHashFromCwd()
|
|
47
|
+
if (detected) {
|
|
48
|
+
projectHash = detected
|
|
49
|
+
} else {
|
|
50
|
+
console.error(
|
|
51
|
+
"No project ID provided and no framerProjectId found in package.json."
|
|
52
|
+
)
|
|
53
|
+
console.error(
|
|
54
|
+
"Either run this command from a project directory or copy the command from the Code Link Plugin."
|
|
55
|
+
)
|
|
56
|
+
process.exit(1)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
40
60
|
// Auto-enable debug in development unless overridden
|
|
41
61
|
const isDev = process.env.NODE_ENV === "development"
|
|
42
62
|
|
package/src/utils/project.ts
CHANGED
|
@@ -17,6 +17,24 @@ export function toPackageName(name: string): string {
|
|
|
17
17
|
.replace(/-+/g, "-")
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export function toDirName(name: string): string {
|
|
21
|
+
return name
|
|
22
|
+
.replace(/[^a-zA-Z0-9-]/g, "-")
|
|
23
|
+
.replace(/^-+|-+$/g, "")
|
|
24
|
+
.replace(/-+/g, "-")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function getProjectHashFromCwd(): Promise<string | null> {
|
|
28
|
+
try {
|
|
29
|
+
const packageJsonPath = path.join(process.cwd(), "package.json")
|
|
30
|
+
const content = await fs.readFile(packageJsonPath, "utf-8")
|
|
31
|
+
const pkg = JSON.parse(content) as PackageJson
|
|
32
|
+
return pkg.framerProjectId ?? null
|
|
33
|
+
} catch {
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
20
38
|
export async function findOrCreateProjectDir(
|
|
21
39
|
projectHash: string,
|
|
22
40
|
projectName?: string,
|
|
@@ -40,12 +58,13 @@ export async function findOrCreateProjectDir(
|
|
|
40
58
|
)
|
|
41
59
|
}
|
|
42
60
|
|
|
43
|
-
const dirName =
|
|
61
|
+
const dirName = toDirName(projectName)
|
|
62
|
+
const pkgName = toPackageName(projectName)
|
|
44
63
|
const projectDir = path.join(cwd, dirName || projectHash.slice(0, 6))
|
|
45
64
|
|
|
46
65
|
await fs.mkdir(path.join(projectDir, "files"), { recursive: true })
|
|
47
66
|
const pkg: PackageJson = {
|
|
48
|
-
name:
|
|
67
|
+
name: pkgName || projectHash,
|
|
49
68
|
version: "1.0.0",
|
|
50
69
|
private: true,
|
|
51
70
|
framerProjectId: projectHash,
|