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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "framer-code-link",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "CLI tool for syncing Framer code components - controller-centric architecture",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
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
- info("Client connected")
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("Failed to parse message:", err)
56
+ error(`[CONN ${connId}] Failed to parse message:`, err)
54
57
  }
55
58
  })
56
59
 
57
- ws.on("close", () => {
58
- info("Client disconnected")
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("WebSocket error:", err)
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<void> {
96
- return new Promise((resolve, reject) => {
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) reject(err)
99
- else resolve()
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("<projectHash>", "Framer Project ID Hash")
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
 
@@ -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 = toPackageName(projectName)
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: dirName || projectHash,
67
+ name: pkgName || projectHash,
49
68
  version: "1.0.0",
50
69
  private: true,
51
70
  framerProjectId: projectHash,