@muloka/figma-console-mcp 0.1.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.
Files changed (443) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +898 -0
  3. package/dist/apps/design-system-dashboard/scoring/accessibility.d.ts +14 -0
  4. package/dist/apps/design-system-dashboard/scoring/accessibility.d.ts.map +1 -0
  5. package/dist/apps/design-system-dashboard/scoring/accessibility.js +278 -0
  6. package/dist/apps/design-system-dashboard/scoring/accessibility.js.map +1 -0
  7. package/dist/apps/design-system-dashboard/scoring/component-metadata.d.ts +29 -0
  8. package/dist/apps/design-system-dashboard/scoring/component-metadata.d.ts.map +1 -0
  9. package/dist/apps/design-system-dashboard/scoring/component-metadata.js +358 -0
  10. package/dist/apps/design-system-dashboard/scoring/component-metadata.js.map +1 -0
  11. package/dist/apps/design-system-dashboard/scoring/consistency.d.ts +14 -0
  12. package/dist/apps/design-system-dashboard/scoring/consistency.d.ts.map +1 -0
  13. package/dist/apps/design-system-dashboard/scoring/consistency.js +342 -0
  14. package/dist/apps/design-system-dashboard/scoring/consistency.js.map +1 -0
  15. package/dist/apps/design-system-dashboard/scoring/coverage.d.ts +14 -0
  16. package/dist/apps/design-system-dashboard/scoring/coverage.d.ts.map +1 -0
  17. package/dist/apps/design-system-dashboard/scoring/coverage.js +231 -0
  18. package/dist/apps/design-system-dashboard/scoring/coverage.js.map +1 -0
  19. package/dist/apps/design-system-dashboard/scoring/engine.d.ts +27 -0
  20. package/dist/apps/design-system-dashboard/scoring/engine.d.ts.map +1 -0
  21. package/dist/apps/design-system-dashboard/scoring/engine.js +93 -0
  22. package/dist/apps/design-system-dashboard/scoring/engine.js.map +1 -0
  23. package/dist/apps/design-system-dashboard/scoring/naming-semantics.d.ts +14 -0
  24. package/dist/apps/design-system-dashboard/scoring/naming-semantics.d.ts.map +1 -0
  25. package/dist/apps/design-system-dashboard/scoring/naming-semantics.js +309 -0
  26. package/dist/apps/design-system-dashboard/scoring/naming-semantics.js.map +1 -0
  27. package/dist/apps/design-system-dashboard/scoring/token-architecture.d.ts +14 -0
  28. package/dist/apps/design-system-dashboard/scoring/token-architecture.d.ts.map +1 -0
  29. package/dist/apps/design-system-dashboard/scoring/token-architecture.js +350 -0
  30. package/dist/apps/design-system-dashboard/scoring/token-architecture.js.map +1 -0
  31. package/dist/apps/design-system-dashboard/scoring/types.d.ts +89 -0
  32. package/dist/apps/design-system-dashboard/scoring/types.d.ts.map +1 -0
  33. package/dist/apps/design-system-dashboard/scoring/types.js +41 -0
  34. package/dist/apps/design-system-dashboard/scoring/types.js.map +1 -0
  35. package/dist/apps/design-system-dashboard/server.d.ts +24 -0
  36. package/dist/apps/design-system-dashboard/server.d.ts.map +1 -0
  37. package/dist/apps/design-system-dashboard/server.js +160 -0
  38. package/dist/apps/design-system-dashboard/server.js.map +1 -0
  39. package/dist/apps/token-browser/server.d.ts +26 -0
  40. package/dist/apps/token-browser/server.d.ts.map +1 -0
  41. package/dist/apps/token-browser/server.js +137 -0
  42. package/dist/apps/token-browser/server.js.map +1 -0
  43. package/dist/browser/base.d.ts +58 -0
  44. package/dist/browser/base.d.ts.map +1 -0
  45. package/dist/browser/base.js +6 -0
  46. package/dist/browser/base.js.map +1 -0
  47. package/dist/browser/local.d.ts +87 -0
  48. package/dist/browser/local.d.ts.map +1 -0
  49. package/dist/browser/local.js +318 -0
  50. package/dist/browser/local.js.map +1 -0
  51. package/dist/cloudflare/apps/design-system-dashboard/scoring/accessibility.js +277 -0
  52. package/dist/cloudflare/apps/design-system-dashboard/scoring/component-metadata.js +357 -0
  53. package/dist/cloudflare/apps/design-system-dashboard/scoring/consistency.js +341 -0
  54. package/dist/cloudflare/apps/design-system-dashboard/scoring/coverage.js +230 -0
  55. package/dist/cloudflare/apps/design-system-dashboard/scoring/engine.js +92 -0
  56. package/dist/cloudflare/apps/design-system-dashboard/scoring/naming-semantics.js +308 -0
  57. package/dist/cloudflare/apps/design-system-dashboard/scoring/token-architecture.js +349 -0
  58. package/dist/cloudflare/apps/design-system-dashboard/scoring/types.js +40 -0
  59. package/dist/cloudflare/apps/design-system-dashboard/server.js +159 -0
  60. package/dist/cloudflare/apps/token-browser/server.js +136 -0
  61. package/dist/cloudflare/browser-manager.js +157 -0
  62. package/dist/cloudflare/core/accessibility-tools.js +361 -0
  63. package/dist/cloudflare/core/annotation-tools.js +230 -0
  64. package/dist/cloudflare/core/cloud-websocket-connector.js +419 -0
  65. package/dist/cloudflare/core/cloud-websocket-relay.js +198 -0
  66. package/dist/cloudflare/core/comment-tools.js +292 -0
  67. package/dist/cloudflare/core/config.js +153 -0
  68. package/dist/cloudflare/core/console-monitor.js +427 -0
  69. package/dist/cloudflare/core/deep-component-tools.js +128 -0
  70. package/dist/cloudflare/core/design-code-tools.js +2793 -0
  71. package/dist/cloudflare/core/design-system-manifest.js +265 -0
  72. package/dist/cloudflare/core/design-system-tools.js +882 -0
  73. package/dist/cloudflare/core/diagnose-tool.js +100 -0
  74. package/dist/cloudflare/core/diff/changelog-formatter.js +275 -0
  75. package/dist/cloudflare/core/diff/diff-engine.js +334 -0
  76. package/dist/cloudflare/core/diff/property-compare.js +36 -0
  77. package/dist/cloudflare/core/diff/version-cache.js +74 -0
  78. package/dist/cloudflare/core/enrichment/enrichment-service.js +374 -0
  79. package/dist/cloudflare/core/enrichment/index.js +7 -0
  80. package/dist/cloudflare/core/enrichment/relationship-mapper.js +351 -0
  81. package/dist/cloudflare/core/enrichment/style-resolver.js +346 -0
  82. package/dist/cloudflare/core/figjam-tools.js +547 -0
  83. package/dist/cloudflare/core/figma-api.js +509 -0
  84. package/dist/cloudflare/core/figma-connector.js +7 -0
  85. package/dist/cloudflare/core/figma-reconstruction-spec.js +402 -0
  86. package/dist/cloudflare/core/figma-style-extractor.js +311 -0
  87. package/dist/cloudflare/core/figma-tools.js +3302 -0
  88. package/dist/cloudflare/core/identity.js +96 -0
  89. package/dist/cloudflare/core/library-tools.js +580 -0
  90. package/dist/cloudflare/core/logger.js +53 -0
  91. package/dist/cloudflare/core/port-discovery.js +935 -0
  92. package/dist/cloudflare/core/resolve-package-root.js +11 -0
  93. package/dist/cloudflare/core/slides-tools.js +714 -0
  94. package/dist/cloudflare/core/slot-tools.js +334 -0
  95. package/dist/cloudflare/core/snippet-injector.js +96 -0
  96. package/dist/cloudflare/core/tokens/alias-resolver.js +205 -0
  97. package/dist/cloudflare/core/tokens/config.js +294 -0
  98. package/dist/cloudflare/core/tokens/dialect.js +232 -0
  99. package/dist/cloudflare/core/tokens/figma-converter.js +323 -0
  100. package/dist/cloudflare/core/tokens/formatters/css-vars.js +338 -0
  101. package/dist/cloudflare/core/tokens/formatters/dtcg.js +376 -0
  102. package/dist/cloudflare/core/tokens/formatters/index.js +45 -0
  103. package/dist/cloudflare/core/tokens/formatters/json.js +205 -0
  104. package/dist/cloudflare/core/tokens/formatters/less.js +4 -0
  105. package/dist/cloudflare/core/tokens/formatters/scss.js +258 -0
  106. package/dist/cloudflare/core/tokens/formatters/stubs.js +13 -0
  107. package/dist/cloudflare/core/tokens/formatters/style-dictionary-v3.js +213 -0
  108. package/dist/cloudflare/core/tokens/formatters/tailwind-v3.js +237 -0
  109. package/dist/cloudflare/core/tokens/formatters/tailwind-v4.js +335 -0
  110. package/dist/cloudflare/core/tokens/formatters/tokens-studio.js +256 -0
  111. package/dist/cloudflare/core/tokens/formatters/ts-module.js +198 -0
  112. package/dist/cloudflare/core/tokens/index.js +16 -0
  113. package/dist/cloudflare/core/tokens/parsers/css-vars.js +4 -0
  114. package/dist/cloudflare/core/tokens/parsers/dtcg.js +280 -0
  115. package/dist/cloudflare/core/tokens/parsers/index.js +138 -0
  116. package/dist/cloudflare/core/tokens/parsers/json.js +7 -0
  117. package/dist/cloudflare/core/tokens/parsers/scss.js +4 -0
  118. package/dist/cloudflare/core/tokens/parsers/stubs.js +20 -0
  119. package/dist/cloudflare/core/tokens/parsers/style-dictionary-v3.js +4 -0
  120. package/dist/cloudflare/core/tokens/parsers/tailwind-v3.js +4 -0
  121. package/dist/cloudflare/core/tokens/parsers/tailwind-v4.js +4 -0
  122. package/dist/cloudflare/core/tokens/parsers/tokens-studio.js +4 -0
  123. package/dist/cloudflare/core/tokens/schemas.js +152 -0
  124. package/dist/cloudflare/core/tokens/transforms/color.js +12 -0
  125. package/dist/cloudflare/core/tokens/transforms/index.js +29 -0
  126. package/dist/cloudflare/core/tokens/transforms/size.js +7 -0
  127. package/dist/cloudflare/core/tokens/types.js +18 -0
  128. package/dist/cloudflare/core/tokens-tools.js +1788 -0
  129. package/dist/cloudflare/core/types/design-code.js +4 -0
  130. package/dist/cloudflare/core/types/enriched.js +5 -0
  131. package/dist/cloudflare/core/types/index.js +4 -0
  132. package/dist/cloudflare/core/variable-resolver.js +85 -0
  133. package/dist/cloudflare/core/version-tools.js +1199 -0
  134. package/dist/cloudflare/core/websocket-connector.js +436 -0
  135. package/dist/cloudflare/core/websocket-server.js +956 -0
  136. package/dist/cloudflare/core/write-tools.js +2506 -0
  137. package/dist/cloudflare/index.js +3010 -0
  138. package/dist/core/accessibility-tools.d.ts +21 -0
  139. package/dist/core/accessibility-tools.d.ts.map +1 -0
  140. package/dist/core/accessibility-tools.js +362 -0
  141. package/dist/core/accessibility-tools.js.map +1 -0
  142. package/dist/core/annotation-tools.d.ts +14 -0
  143. package/dist/core/annotation-tools.d.ts.map +1 -0
  144. package/dist/core/annotation-tools.js +231 -0
  145. package/dist/core/annotation-tools.js.map +1 -0
  146. package/dist/core/comment-tools.d.ts +11 -0
  147. package/dist/core/comment-tools.d.ts.map +1 -0
  148. package/dist/core/comment-tools.js +293 -0
  149. package/dist/core/comment-tools.js.map +1 -0
  150. package/dist/core/config.d.ts +17 -0
  151. package/dist/core/config.d.ts.map +1 -0
  152. package/dist/core/config.js +154 -0
  153. package/dist/core/config.js.map +1 -0
  154. package/dist/core/console-monitor.d.ts +82 -0
  155. package/dist/core/console-monitor.d.ts.map +1 -0
  156. package/dist/core/console-monitor.js +428 -0
  157. package/dist/core/console-monitor.js.map +1 -0
  158. package/dist/core/deep-component-tools.d.ts +14 -0
  159. package/dist/core/deep-component-tools.d.ts.map +1 -0
  160. package/dist/core/deep-component-tools.js +129 -0
  161. package/dist/core/deep-component-tools.js.map +1 -0
  162. package/dist/core/design-code-tools.d.ts +116 -0
  163. package/dist/core/design-code-tools.d.ts.map +1 -0
  164. package/dist/core/design-code-tools.js +2794 -0
  165. package/dist/core/design-code-tools.js.map +1 -0
  166. package/dist/core/design-system-manifest.d.ts +273 -0
  167. package/dist/core/design-system-manifest.d.ts.map +1 -0
  168. package/dist/core/design-system-manifest.js +266 -0
  169. package/dist/core/design-system-manifest.js.map +1 -0
  170. package/dist/core/design-system-tools.d.ts +67 -0
  171. package/dist/core/design-system-tools.d.ts.map +1 -0
  172. package/dist/core/design-system-tools.js +883 -0
  173. package/dist/core/design-system-tools.js.map +1 -0
  174. package/dist/core/diagnose-tool.d.ts +41 -0
  175. package/dist/core/diagnose-tool.d.ts.map +1 -0
  176. package/dist/core/diagnose-tool.js +101 -0
  177. package/dist/core/diagnose-tool.js.map +1 -0
  178. package/dist/core/diff/changelog-formatter.d.ts +35 -0
  179. package/dist/core/diff/changelog-formatter.d.ts.map +1 -0
  180. package/dist/core/diff/changelog-formatter.js +276 -0
  181. package/dist/core/diff/changelog-formatter.js.map +1 -0
  182. package/dist/core/diff/diff-engine.d.ts +127 -0
  183. package/dist/core/diff/diff-engine.d.ts.map +1 -0
  184. package/dist/core/diff/diff-engine.js +335 -0
  185. package/dist/core/diff/diff-engine.js.map +1 -0
  186. package/dist/core/diff/property-compare.d.ts +19 -0
  187. package/dist/core/diff/property-compare.d.ts.map +1 -0
  188. package/dist/core/diff/property-compare.js +37 -0
  189. package/dist/core/diff/property-compare.js.map +1 -0
  190. package/dist/core/diff/version-cache.d.ts +40 -0
  191. package/dist/core/diff/version-cache.d.ts.map +1 -0
  192. package/dist/core/diff/version-cache.js +75 -0
  193. package/dist/core/diff/version-cache.js.map +1 -0
  194. package/dist/core/enrichment/enrichment-service.d.ts +52 -0
  195. package/dist/core/enrichment/enrichment-service.d.ts.map +1 -0
  196. package/dist/core/enrichment/enrichment-service.js +375 -0
  197. package/dist/core/enrichment/enrichment-service.js.map +1 -0
  198. package/dist/core/enrichment/index.d.ts +8 -0
  199. package/dist/core/enrichment/index.d.ts.map +1 -0
  200. package/dist/core/enrichment/index.js +8 -0
  201. package/dist/core/enrichment/index.js.map +1 -0
  202. package/dist/core/enrichment/relationship-mapper.d.ts +106 -0
  203. package/dist/core/enrichment/relationship-mapper.d.ts.map +1 -0
  204. package/dist/core/enrichment/relationship-mapper.js +352 -0
  205. package/dist/core/enrichment/relationship-mapper.js.map +1 -0
  206. package/dist/core/enrichment/style-resolver.d.ts +85 -0
  207. package/dist/core/enrichment/style-resolver.d.ts.map +1 -0
  208. package/dist/core/enrichment/style-resolver.js +347 -0
  209. package/dist/core/enrichment/style-resolver.js.map +1 -0
  210. package/dist/core/figjam-tools.d.ts +8 -0
  211. package/dist/core/figjam-tools.d.ts.map +1 -0
  212. package/dist/core/figjam-tools.js +548 -0
  213. package/dist/core/figjam-tools.js.map +1 -0
  214. package/dist/core/figma-api.d.ts +254 -0
  215. package/dist/core/figma-api.d.ts.map +1 -0
  216. package/dist/core/figma-api.js +510 -0
  217. package/dist/core/figma-api.js.map +1 -0
  218. package/dist/core/figma-connector.d.ts +214 -0
  219. package/dist/core/figma-connector.d.ts.map +1 -0
  220. package/dist/core/figma-connector.js +8 -0
  221. package/dist/core/figma-connector.js.map +1 -0
  222. package/dist/core/figma-desktop-connector.d.ts +312 -0
  223. package/dist/core/figma-desktop-connector.d.ts.map +1 -0
  224. package/dist/core/figma-desktop-connector.js +1298 -0
  225. package/dist/core/figma-desktop-connector.js.map +1 -0
  226. package/dist/core/figma-reconstruction-spec.d.ts +166 -0
  227. package/dist/core/figma-reconstruction-spec.d.ts.map +1 -0
  228. package/dist/core/figma-reconstruction-spec.js +403 -0
  229. package/dist/core/figma-reconstruction-spec.js.map +1 -0
  230. package/dist/core/figma-style-extractor.d.ts +76 -0
  231. package/dist/core/figma-style-extractor.d.ts.map +1 -0
  232. package/dist/core/figma-style-extractor.js +312 -0
  233. package/dist/core/figma-style-extractor.js.map +1 -0
  234. package/dist/core/figma-tools.d.ts +22 -0
  235. package/dist/core/figma-tools.d.ts.map +1 -0
  236. package/dist/core/figma-tools.js +3303 -0
  237. package/dist/core/figma-tools.js.map +1 -0
  238. package/dist/core/identity.d.ts +41 -0
  239. package/dist/core/identity.d.ts.map +1 -0
  240. package/dist/core/identity.js +97 -0
  241. package/dist/core/identity.js.map +1 -0
  242. package/dist/core/library-tools.d.ts +17 -0
  243. package/dist/core/library-tools.d.ts.map +1 -0
  244. package/dist/core/library-tools.js +581 -0
  245. package/dist/core/library-tools.js.map +1 -0
  246. package/dist/core/logger.d.ts +22 -0
  247. package/dist/core/logger.d.ts.map +1 -0
  248. package/dist/core/logger.js +54 -0
  249. package/dist/core/logger.js.map +1 -0
  250. package/dist/core/port-discovery.d.ts +211 -0
  251. package/dist/core/port-discovery.d.ts.map +1 -0
  252. package/dist/core/port-discovery.js +936 -0
  253. package/dist/core/port-discovery.js.map +1 -0
  254. package/dist/core/resolve-package-root.d.ts +2 -0
  255. package/dist/core/resolve-package-root.d.ts.map +1 -0
  256. package/dist/core/resolve-package-root.js +12 -0
  257. package/dist/core/resolve-package-root.js.map +1 -0
  258. package/dist/core/slides-tools.d.ts +8 -0
  259. package/dist/core/slides-tools.d.ts.map +1 -0
  260. package/dist/core/slides-tools.js +715 -0
  261. package/dist/core/slides-tools.js.map +1 -0
  262. package/dist/core/slot-tools.d.ts +7 -0
  263. package/dist/core/slot-tools.d.ts.map +1 -0
  264. package/dist/core/slot-tools.js +335 -0
  265. package/dist/core/slot-tools.js.map +1 -0
  266. package/dist/core/snippet-injector.d.ts +24 -0
  267. package/dist/core/snippet-injector.d.ts.map +1 -0
  268. package/dist/core/snippet-injector.js +97 -0
  269. package/dist/core/snippet-injector.js.map +1 -0
  270. package/dist/core/tokens/alias-resolver.d.ts +97 -0
  271. package/dist/core/tokens/alias-resolver.d.ts.map +1 -0
  272. package/dist/core/tokens/alias-resolver.js +206 -0
  273. package/dist/core/tokens/alias-resolver.js.map +1 -0
  274. package/dist/core/tokens/config.d.ts +380 -0
  275. package/dist/core/tokens/config.d.ts.map +1 -0
  276. package/dist/core/tokens/config.js +295 -0
  277. package/dist/core/tokens/config.js.map +1 -0
  278. package/dist/core/tokens/dialect.d.ts +107 -0
  279. package/dist/core/tokens/dialect.d.ts.map +1 -0
  280. package/dist/core/tokens/dialect.js +233 -0
  281. package/dist/core/tokens/dialect.js.map +1 -0
  282. package/dist/core/tokens/figma-converter.d.ts +102 -0
  283. package/dist/core/tokens/figma-converter.d.ts.map +1 -0
  284. package/dist/core/tokens/figma-converter.js +324 -0
  285. package/dist/core/tokens/figma-converter.js.map +1 -0
  286. package/dist/core/tokens/formatters/css-vars.d.ts +24 -0
  287. package/dist/core/tokens/formatters/css-vars.d.ts.map +1 -0
  288. package/dist/core/tokens/formatters/css-vars.js +339 -0
  289. package/dist/core/tokens/formatters/css-vars.js.map +1 -0
  290. package/dist/core/tokens/formatters/dtcg.d.ts +28 -0
  291. package/dist/core/tokens/formatters/dtcg.d.ts.map +1 -0
  292. package/dist/core/tokens/formatters/dtcg.js +377 -0
  293. package/dist/core/tokens/formatters/dtcg.js.map +1 -0
  294. package/dist/core/tokens/formatters/index.d.ts +30 -0
  295. package/dist/core/tokens/formatters/index.d.ts.map +1 -0
  296. package/dist/core/tokens/formatters/index.js +46 -0
  297. package/dist/core/tokens/formatters/index.js.map +1 -0
  298. package/dist/core/tokens/formatters/json.d.ts +37 -0
  299. package/dist/core/tokens/formatters/json.d.ts.map +1 -0
  300. package/dist/core/tokens/formatters/json.js +206 -0
  301. package/dist/core/tokens/formatters/json.js.map +1 -0
  302. package/dist/core/tokens/formatters/less.d.ts +4 -0
  303. package/dist/core/tokens/formatters/less.d.ts.map +1 -0
  304. package/dist/core/tokens/formatters/less.js +5 -0
  305. package/dist/core/tokens/formatters/less.js.map +1 -0
  306. package/dist/core/tokens/formatters/scss.d.ts +26 -0
  307. package/dist/core/tokens/formatters/scss.d.ts.map +1 -0
  308. package/dist/core/tokens/formatters/scss.js +259 -0
  309. package/dist/core/tokens/formatters/scss.js.map +1 -0
  310. package/dist/core/tokens/formatters/stubs.d.ts +9 -0
  311. package/dist/core/tokens/formatters/stubs.d.ts.map +1 -0
  312. package/dist/core/tokens/formatters/stubs.js +14 -0
  313. package/dist/core/tokens/formatters/stubs.js.map +1 -0
  314. package/dist/core/tokens/formatters/style-dictionary-v3.d.ts +45 -0
  315. package/dist/core/tokens/formatters/style-dictionary-v3.d.ts.map +1 -0
  316. package/dist/core/tokens/formatters/style-dictionary-v3.js +214 -0
  317. package/dist/core/tokens/formatters/style-dictionary-v3.js.map +1 -0
  318. package/dist/core/tokens/formatters/tailwind-v3.d.ts +37 -0
  319. package/dist/core/tokens/formatters/tailwind-v3.d.ts.map +1 -0
  320. package/dist/core/tokens/formatters/tailwind-v3.js +238 -0
  321. package/dist/core/tokens/formatters/tailwind-v3.js.map +1 -0
  322. package/dist/core/tokens/formatters/tailwind-v4.d.ts +41 -0
  323. package/dist/core/tokens/formatters/tailwind-v4.d.ts.map +1 -0
  324. package/dist/core/tokens/formatters/tailwind-v4.js +336 -0
  325. package/dist/core/tokens/formatters/tailwind-v4.js.map +1 -0
  326. package/dist/core/tokens/formatters/tokens-studio.d.ts +44 -0
  327. package/dist/core/tokens/formatters/tokens-studio.d.ts.map +1 -0
  328. package/dist/core/tokens/formatters/tokens-studio.js +257 -0
  329. package/dist/core/tokens/formatters/tokens-studio.js.map +1 -0
  330. package/dist/core/tokens/formatters/ts-module.d.ts +35 -0
  331. package/dist/core/tokens/formatters/ts-module.d.ts.map +1 -0
  332. package/dist/core/tokens/formatters/ts-module.js +199 -0
  333. package/dist/core/tokens/formatters/ts-module.js.map +1 -0
  334. package/dist/core/tokens/index.d.ts +18 -0
  335. package/dist/core/tokens/index.d.ts.map +1 -0
  336. package/dist/core/tokens/index.js +17 -0
  337. package/dist/core/tokens/index.js.map +1 -0
  338. package/dist/core/tokens/parsers/css-vars.d.ts +3 -0
  339. package/dist/core/tokens/parsers/css-vars.d.ts.map +1 -0
  340. package/dist/core/tokens/parsers/css-vars.js +5 -0
  341. package/dist/core/tokens/parsers/css-vars.js.map +1 -0
  342. package/dist/core/tokens/parsers/dtcg.d.ts +21 -0
  343. package/dist/core/tokens/parsers/dtcg.d.ts.map +1 -0
  344. package/dist/core/tokens/parsers/dtcg.js +281 -0
  345. package/dist/core/tokens/parsers/dtcg.js.map +1 -0
  346. package/dist/core/tokens/parsers/index.d.ts +37 -0
  347. package/dist/core/tokens/parsers/index.d.ts.map +1 -0
  348. package/dist/core/tokens/parsers/index.js +139 -0
  349. package/dist/core/tokens/parsers/index.js.map +1 -0
  350. package/dist/core/tokens/parsers/json.d.ts +4 -0
  351. package/dist/core/tokens/parsers/json.d.ts.map +1 -0
  352. package/dist/core/tokens/parsers/json.js +8 -0
  353. package/dist/core/tokens/parsers/json.js.map +1 -0
  354. package/dist/core/tokens/parsers/scss.d.ts +3 -0
  355. package/dist/core/tokens/parsers/scss.d.ts.map +1 -0
  356. package/dist/core/tokens/parsers/scss.js +5 -0
  357. package/dist/core/tokens/parsers/scss.js.map +1 -0
  358. package/dist/core/tokens/parsers/stubs.d.ts +15 -0
  359. package/dist/core/tokens/parsers/stubs.d.ts.map +1 -0
  360. package/dist/core/tokens/parsers/stubs.js +21 -0
  361. package/dist/core/tokens/parsers/stubs.js.map +1 -0
  362. package/dist/core/tokens/parsers/style-dictionary-v3.d.ts +3 -0
  363. package/dist/core/tokens/parsers/style-dictionary-v3.d.ts.map +1 -0
  364. package/dist/core/tokens/parsers/style-dictionary-v3.js +5 -0
  365. package/dist/core/tokens/parsers/style-dictionary-v3.js.map +1 -0
  366. package/dist/core/tokens/parsers/tailwind-v3.d.ts +3 -0
  367. package/dist/core/tokens/parsers/tailwind-v3.d.ts.map +1 -0
  368. package/dist/core/tokens/parsers/tailwind-v3.js +5 -0
  369. package/dist/core/tokens/parsers/tailwind-v3.js.map +1 -0
  370. package/dist/core/tokens/parsers/tailwind-v4.d.ts +3 -0
  371. package/dist/core/tokens/parsers/tailwind-v4.d.ts.map +1 -0
  372. package/dist/core/tokens/parsers/tailwind-v4.js +5 -0
  373. package/dist/core/tokens/parsers/tailwind-v4.js.map +1 -0
  374. package/dist/core/tokens/parsers/tokens-studio.d.ts +3 -0
  375. package/dist/core/tokens/parsers/tokens-studio.d.ts.map +1 -0
  376. package/dist/core/tokens/parsers/tokens-studio.js +5 -0
  377. package/dist/core/tokens/parsers/tokens-studio.js.map +1 -0
  378. package/dist/core/tokens/schemas.d.ts +155 -0
  379. package/dist/core/tokens/schemas.d.ts.map +1 -0
  380. package/dist/core/tokens/schemas.js +153 -0
  381. package/dist/core/tokens/schemas.js.map +1 -0
  382. package/dist/core/tokens/transforms/color.d.ts +9 -0
  383. package/dist/core/tokens/transforms/color.d.ts.map +1 -0
  384. package/dist/core/tokens/transforms/color.js +13 -0
  385. package/dist/core/tokens/transforms/color.js.map +1 -0
  386. package/dist/core/tokens/transforms/index.d.ts +36 -0
  387. package/dist/core/tokens/transforms/index.d.ts.map +1 -0
  388. package/dist/core/tokens/transforms/index.js +30 -0
  389. package/dist/core/tokens/transforms/index.js.map +1 -0
  390. package/dist/core/tokens/transforms/size.d.ts +7 -0
  391. package/dist/core/tokens/transforms/size.d.ts.map +1 -0
  392. package/dist/core/tokens/transforms/size.js +8 -0
  393. package/dist/core/tokens/transforms/size.js.map +1 -0
  394. package/dist/core/tokens/types.d.ts +284 -0
  395. package/dist/core/tokens/types.d.ts.map +1 -0
  396. package/dist/core/tokens/types.js +19 -0
  397. package/dist/core/tokens/types.js.map +1 -0
  398. package/dist/core/tokens-tools.d.ts +285 -0
  399. package/dist/core/tokens-tools.d.ts.map +1 -0
  400. package/dist/core/tokens-tools.js +1789 -0
  401. package/dist/core/tokens-tools.js.map +1 -0
  402. package/dist/core/types/design-code.d.ts +271 -0
  403. package/dist/core/types/design-code.d.ts.map +1 -0
  404. package/dist/core/types/design-code.js +5 -0
  405. package/dist/core/types/design-code.js.map +1 -0
  406. package/dist/core/types/enriched.d.ts +213 -0
  407. package/dist/core/types/enriched.d.ts.map +1 -0
  408. package/dist/core/types/enriched.js +6 -0
  409. package/dist/core/types/enriched.js.map +1 -0
  410. package/dist/core/types/index.d.ts +104 -0
  411. package/dist/core/types/index.d.ts.map +1 -0
  412. package/dist/core/types/index.js +5 -0
  413. package/dist/core/types/index.js.map +1 -0
  414. package/dist/core/variable-resolver.d.ts +45 -0
  415. package/dist/core/variable-resolver.d.ts.map +1 -0
  416. package/dist/core/variable-resolver.js +86 -0
  417. package/dist/core/variable-resolver.js.map +1 -0
  418. package/dist/core/version-tools.d.ts +59 -0
  419. package/dist/core/version-tools.d.ts.map +1 -0
  420. package/dist/core/version-tools.js +1200 -0
  421. package/dist/core/version-tools.js.map +1 -0
  422. package/dist/core/websocket-connector.d.ts +247 -0
  423. package/dist/core/websocket-connector.d.ts.map +1 -0
  424. package/dist/core/websocket-connector.js +437 -0
  425. package/dist/core/websocket-connector.js.map +1 -0
  426. package/dist/core/websocket-server.d.ts +290 -0
  427. package/dist/core/websocket-server.d.ts.map +1 -0
  428. package/dist/core/websocket-server.js +957 -0
  429. package/dist/core/websocket-server.js.map +1 -0
  430. package/dist/core/write-tools.d.ts +7 -0
  431. package/dist/core/write-tools.d.ts.map +1 -0
  432. package/dist/core/write-tools.js +2507 -0
  433. package/dist/core/write-tools.js.map +1 -0
  434. package/dist/local.d.ts +105 -0
  435. package/dist/local.d.ts.map +1 -0
  436. package/dist/local.js +3314 -0
  437. package/dist/local.js.map +1 -0
  438. package/figma-desktop-bridge/README.md +365 -0
  439. package/figma-desktop-bridge/code.js +7359 -0
  440. package/figma-desktop-bridge/icon.png +0 -0
  441. package/figma-desktop-bridge/manifest.json +67 -0
  442. package/figma-desktop-bridge/ui.html +2783 -0
  443. package/package.json +103 -0
@@ -0,0 +1,956 @@
1
+ /**
2
+ * WebSocket Bridge Server (Multi-Client)
3
+ *
4
+ * Creates a WebSocket server that multiple Desktop Bridge plugin instances connect to.
5
+ * Each instance represents a different Figma file and is identified by its fileKey
6
+ * (sent via FILE_INFO on connection). Per-file state (selection, document changes,
7
+ * console logs) is maintained independently.
8
+ *
9
+ * Active file tracking: The "active" file is automatically switched when the user
10
+ * interacts with a file (selection/page changes) or can be set explicitly via
11
+ * setActiveFile(). All backward-compatible getters return data from the active file.
12
+ *
13
+ * Data flow: MCP Server ←WebSocket→ ui.html ←postMessage→ code.js ←figma.*→ Figma
14
+ */
15
+ import { WebSocketServer as WSServer, WebSocket } from 'ws';
16
+ import { EventEmitter } from 'events';
17
+ import { createServer as createHttpServer } from 'http';
18
+ import { readFileSync } from 'fs';
19
+ import { join } from 'path';
20
+ import { createChildLogger } from './logger.js';
21
+ import { PACKAGE_ROOT } from './resolve-package-root.js';
22
+ // Read version from package.json using the resolved package root.
23
+ // PACKAGE_ROOT uses import.meta.url in ESM (production) and __dirname in CJS (Jest).
24
+ let SERVER_VERSION = '0.0.0';
25
+ try {
26
+ SERVER_VERSION = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf-8')).version;
27
+ }
28
+ catch {
29
+ // Non-critical — version will show as 0.0.0
30
+ }
31
+ /**
32
+ * Extract the PLUGIN_VERSION constant from figma-desktop-bridge/code.js source.
33
+ * Returns null when the constant is absent or malformed.
34
+ */
35
+ export function parseBundledPluginVersion(codeJsSource) {
36
+ const match = codeJsSource.match(/var PLUGIN_VERSION\s*=\s*'(\d+\.\d+\.\d+)'/);
37
+ return match ? match[1] : null;
38
+ }
39
+ // The version of the plugin files THIS package ships — i.e. what a manifest
40
+ // re-import would install. This is deliberately NOT the package version:
41
+ // server-only releases (deps, docs, server code) bump package.json without
42
+ // touching figma-desktop-bridge/, and comparing the plugin against
43
+ // SERVER_VERSION would falsely nag users to re-import an already-current
44
+ // plugin (seen live: plugin 1.33.0 + server 1.33.1 → spurious update banner).
45
+ let BUNDLED_PLUGIN_VERSION = SERVER_VERSION;
46
+ try {
47
+ const parsed = parseBundledPluginVersion(readFileSync(join(PACKAGE_ROOT, 'figma-desktop-bridge', 'code.js'), 'utf-8'));
48
+ if (parsed)
49
+ BUNDLED_PLUGIN_VERSION = parsed;
50
+ }
51
+ catch {
52
+ // Non-critical — fall back to SERVER_VERSION (pre-v1.33.2 behavior)
53
+ }
54
+ /** Version of the plugin files bundled with this server (what a re-import installs). */
55
+ export function getBundledPluginVersion() {
56
+ return BUNDLED_PLUGIN_VERSION;
57
+ }
58
+ /**
59
+ * A plugin needs re-importing when it reports no version (predates version
60
+ * reporting) or a version different from the plugin files this server ships.
61
+ * The server's own package version is irrelevant here.
62
+ */
63
+ export function computePluginUpdateAvailable(pluginVersion, bundledPluginVersion) {
64
+ return !pluginVersion || pluginVersion !== bundledPluginVersion;
65
+ }
66
+ const logger = createChildLogger({ component: 'websocket-server' });
67
+ export class FigmaWebSocketServer extends EventEmitter {
68
+ constructor(options) {
69
+ super();
70
+ this.wss = null;
71
+ this.httpServer = null;
72
+ /** Named clients indexed by fileKey — each represents a connected Figma file */
73
+ this.clients = new Map();
74
+ /** Clients awaiting FILE_INFO identification, mapped to their pending timeout */
75
+ this._pendingClients = new Map();
76
+ /** The fileKey of the currently active (targeted) file */
77
+ this._activeFileKey = null;
78
+ this.pendingRequests = new Map();
79
+ this.requestIdCounter = 0;
80
+ this._isStarted = false;
81
+ this._startedAt = Date.now();
82
+ this.consoleBufferSize = 1000;
83
+ this.documentChangeBufferSize = 200;
84
+ /** Heartbeat interval for detecting dead connections via ping/pong */
85
+ this._heartbeatInterval = null;
86
+ this.options = options;
87
+ this._startedAt = Date.now();
88
+ }
89
+ /**
90
+ * Handle HTTP requests on the same port as WebSocket.
91
+ * Serves a JSON `/health` (also at `/`) endpoint with version and connected-file
92
+ * info. No plugin-UI route exists — the plugin loads its own `ui.html` from disk
93
+ * via the Figma plugin runtime; the legacy `/plugin/ui` bootloader endpoint was
94
+ * removed in the Phase 3 cleanup.
95
+ */
96
+ handleHttpRequest(req, res) {
97
+ // CORS headers for Figma plugin iframe (sandboxed, origin: null)
98
+ res.setHeader('Access-Control-Allow-Origin', '*');
99
+ res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
100
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
101
+ if (req.method === 'OPTIONS') {
102
+ res.writeHead(204);
103
+ res.end();
104
+ return;
105
+ }
106
+ const url = req.url || '/';
107
+ // Health/version endpoint
108
+ if (url === '/health' || url === '/') {
109
+ const now = Date.now();
110
+ const connectedClients = Array.from(this.clients.values()).filter(c => c.ws.readyState === WebSocket.OPEN && (now - c.lastPongAt) < 90000).length;
111
+ res.writeHead(200, { 'Content-Type': 'application/json' });
112
+ res.end(JSON.stringify({
113
+ status: 'ok',
114
+ version: SERVER_VERSION,
115
+ clients: this.clients.size,
116
+ connectedClients,
117
+ uptime: Math.floor((now - this._startedAt) / 1000),
118
+ }));
119
+ return;
120
+ }
121
+ // 404 for anything else
122
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
123
+ res.end('Not Found');
124
+ }
125
+ /**
126
+ * Start the HTTP + WebSocket server.
127
+ * HTTP serves the plugin UI content; WebSocket handles plugin communication.
128
+ */
129
+ async start() {
130
+ if (this._isStarted)
131
+ return;
132
+ return new Promise((resolve, reject) => {
133
+ let rejected = false;
134
+ const rejectOnce = (error) => {
135
+ if (!rejected) {
136
+ rejected = true;
137
+ reject(error);
138
+ }
139
+ };
140
+ try {
141
+ // Create HTTP server first — handles plugin UI requests
142
+ this.httpServer = createHttpServer((req, res) => this.handleHttpRequest(req, res));
143
+ // Attach WebSocket server to the HTTP server (shares the same port)
144
+ this.wss = new WSServer({
145
+ server: this.httpServer,
146
+ maxPayload: 100 * 1024 * 1024, // 100MB — screenshots and large component data can be big
147
+ verifyClient: (info, callback) => {
148
+ // Mitigate Cross-Site WebSocket Hijacking (CSWSH):
149
+ // Reject connections from unexpected browser origins.
150
+ const origin = info.origin;
151
+ // Exact match only — startsWith would let e.g.
152
+ // https://www.figma.com.attacker.example through.
153
+ const allowed = !origin || // No origin — local process (e.g. Node.js client)
154
+ origin === 'null' || // Sandboxed iframe / Figma Desktop plugin UI
155
+ origin === 'https://www.figma.com' ||
156
+ origin === 'https://figma.com';
157
+ if (allowed) {
158
+ callback(true);
159
+ }
160
+ else {
161
+ logger.warn({ origin }, 'Rejected WebSocket connection from unauthorized origin');
162
+ callback(false, 403, 'Unauthorized Origin');
163
+ }
164
+ },
165
+ });
166
+ // Error handler for startup failures (EADDRINUSE, etc.)
167
+ // Must be on BOTH httpServer and wss — the WSS re-emits HTTP server errors
168
+ // and throws if no listener is attached.
169
+ const onStartupError = (error) => {
170
+ if (!this._isStarted) {
171
+ try {
172
+ if (this.wss) {
173
+ this.wss.close();
174
+ this.wss = null;
175
+ }
176
+ }
177
+ catch { /* ignore */ }
178
+ try {
179
+ if (this.httpServer) {
180
+ this.httpServer.close();
181
+ this.httpServer = null;
182
+ }
183
+ }
184
+ catch { /* ignore */ }
185
+ rejectOnce(error);
186
+ }
187
+ else {
188
+ logger.error({ error }, 'HTTP/WebSocket server error');
189
+ }
190
+ };
191
+ this.httpServer.on('error', onStartupError);
192
+ this.wss.on('error', onStartupError);
193
+ // Start listening on the HTTP server (which also handles WS upgrades)
194
+ this.httpServer.listen(this.options.port, this.options.host || 'localhost', () => {
195
+ this._isStarted = true;
196
+ this.startHeartbeat();
197
+ logger.info({ port: this.options.port, host: this.options.host || 'localhost' }, 'WebSocket bridge server started (with HTTP plugin UI endpoint)');
198
+ resolve();
199
+ });
200
+ this.wss.on('connection', (ws) => {
201
+ // Add to pending until FILE_INFO identifies the file
202
+ const pendingTimeout = setTimeout(() => {
203
+ if (this._pendingClients.has(ws)) {
204
+ this._pendingClients.delete(ws);
205
+ logger.warn('Pending WebSocket client timed out without sending FILE_INFO');
206
+ ws.close(1000, 'File identification timeout');
207
+ }
208
+ }, 30000);
209
+ this._pendingClients.set(ws, pendingTimeout);
210
+ // Send server identity to the client for debugging and logging
211
+ try {
212
+ ws.send(JSON.stringify({
213
+ type: 'SERVER_HELLO',
214
+ data: {
215
+ port: this.wss.address()?.port,
216
+ pid: process.pid,
217
+ serverVersion: SERVER_VERSION,
218
+ startedAt: this._startedAt,
219
+ },
220
+ }));
221
+ }
222
+ catch {
223
+ // Non-critical — client will still work without SERVER_HELLO
224
+ }
225
+ logger.info({ totalClients: this.clients.size, pendingClients: this._pendingClients.size }, 'New WebSocket connection (pending file identification)');
226
+ ws.on('message', (data) => {
227
+ try {
228
+ let text;
229
+ if (typeof data === 'string') {
230
+ text = data;
231
+ }
232
+ else if (Buffer.isBuffer(data)) {
233
+ text = data.toString();
234
+ }
235
+ else if (Array.isArray(data)) {
236
+ text = Buffer.concat(data).toString();
237
+ }
238
+ else {
239
+ text = Buffer.from(data).toString();
240
+ }
241
+ const message = JSON.parse(text);
242
+ this.handleMessage(message, ws);
243
+ }
244
+ catch (error) {
245
+ logger.error({ error }, 'Failed to parse WebSocket message');
246
+ }
247
+ });
248
+ ws.on('close', (code, reason) => {
249
+ this.handleClientDisconnect(ws, code, reason.toString());
250
+ });
251
+ ws.on('error', (error) => {
252
+ logger.error({ error }, 'WebSocket client error');
253
+ });
254
+ // Track pong responses for heartbeat-based liveness detection.
255
+ // Browser WebSocket clients auto-respond to pings per RFC 6455 —
256
+ // no plugin-side code changes needed.
257
+ ws.isAlive = true;
258
+ ws.on('pong', () => {
259
+ ws.isAlive = true;
260
+ // Update lastPongAt on the named client if identified
261
+ const found = this.findClientByWs(ws);
262
+ if (found) {
263
+ found.client.lastPongAt = Date.now();
264
+ }
265
+ });
266
+ });
267
+ }
268
+ catch (error) {
269
+ rejectOnce(error);
270
+ }
271
+ });
272
+ }
273
+ /**
274
+ * Find a named client connection by its WebSocket reference
275
+ */
276
+ findClientByWs(ws) {
277
+ for (const [fileKey, client] of this.clients) {
278
+ if (client.ws === ws)
279
+ return { fileKey, client };
280
+ }
281
+ return null;
282
+ }
283
+ /**
284
+ * Handle incoming message from a plugin UI WebSocket connection
285
+ */
286
+ handleMessage(message, ws) {
287
+ // Response to a command we sent
288
+ if (message.id && this.pendingRequests.has(message.id)) {
289
+ const pending = this.pendingRequests.get(message.id);
290
+ // Verify the response arrived on the socket belonging to the file the
291
+ // command targeted — a matching id alone must not let a different
292
+ // client resolve (or spoof) another file's request.
293
+ const sender = this.findClientByWs(ws);
294
+ const socketVerified = sender
295
+ ? sender.fileKey === pending.targetFileKey
296
+ // Pre-identification socket (no FILE_INFO yet): only safe when it is
297
+ // unambiguous — exactly one socket connected to the server at all.
298
+ : (this.wss?.clients.size ?? 0) === 1;
299
+ if (!socketVerified) {
300
+ logger.warn({
301
+ id: message.id,
302
+ method: pending.method,
303
+ senderFileKey: sender?.fileKey ?? null,
304
+ targetFileKey: pending.targetFileKey,
305
+ }, 'Ignoring response from unexpected socket (id matched, sender did not) — treating as unsolicited');
306
+ // Leave the pending request in place; fall through to unsolicited handling.
307
+ }
308
+ else {
309
+ clearTimeout(pending.timeoutId);
310
+ this.pendingRequests.delete(message.id);
311
+ if (message.error) {
312
+ pending.reject(new Error(message.error));
313
+ }
314
+ else {
315
+ pending.resolve(message.result);
316
+ }
317
+ return;
318
+ }
319
+ }
320
+ // Unsolicited data from plugin (FILE_INFO, events, forwarded data)
321
+ if (message.type) {
322
+ // FILE_INFO promotes pending clients to named clients
323
+ if (message.type === 'FILE_INFO' && message.data) {
324
+ this.handleFileInfo(message.data, ws);
325
+ }
326
+ // Buffer document changes for the specific file
327
+ if (message.type === 'DOCUMENT_CHANGE' && message.data) {
328
+ const found = this.findClientByWs(ws);
329
+ if (found) {
330
+ const entry = {
331
+ hasStyleChanges: message.data.hasStyleChanges,
332
+ hasNodeChanges: message.data.hasNodeChanges,
333
+ changedNodeIds: message.data.changedNodeIds || [],
334
+ changeCount: message.data.changeCount || 0,
335
+ timestamp: message.data.timestamp || Date.now(),
336
+ };
337
+ found.client.documentChanges.push(entry);
338
+ if (found.client.documentChanges.length > this.documentChangeBufferSize) {
339
+ found.client.documentChanges.shift();
340
+ }
341
+ found.client.lastActivity = Date.now();
342
+ }
343
+ this.emit('documentChange', { fileKey: found?.fileKey ?? null, ...message.data });
344
+ }
345
+ // v1.25.0: buffer description/annotation changes for the specific file.
346
+ // These are the diff-engine blind spots that Figma REST doesn't expose;
347
+ // the diff engine consults this buffer when correlating to a version
348
+ // range, so changes made while the plugin was connected become visible.
349
+ if (message.type === 'METADATA_CHANGE' && message.data) {
350
+ const found = this.findClientByWs(ws);
351
+ if (found) {
352
+ const changes = Array.isArray(message.data.changes) ? message.data.changes : [];
353
+ for (const c of changes) {
354
+ if (!c || typeof c.node_id !== 'string' || !c.field)
355
+ continue;
356
+ const entry = {
357
+ node_id: c.node_id,
358
+ node_name: c.node_name ?? null,
359
+ node_type: c.node_type ?? null,
360
+ field: c.field === 'annotations' ? 'annotations' : 'description',
361
+ new_value: c.new_value,
362
+ timestamp: typeof c.timestamp === 'number' ? c.timestamp : Date.now(),
363
+ };
364
+ found.client.metadataChanges.push(entry);
365
+ if (found.client.metadataChanges.length > this.documentChangeBufferSize) {
366
+ found.client.metadataChanges.shift();
367
+ }
368
+ }
369
+ found.client.lastActivity = Date.now();
370
+ }
371
+ this.emit('metadataChange', { fileKey: found?.fileKey ?? null, changes: message.data.changes });
372
+ }
373
+ // Track selection changes — user interaction makes this the active file
374
+ if (message.type === 'SELECTION_CHANGE' && message.data) {
375
+ const found = this.findClientByWs(ws);
376
+ if (found) {
377
+ found.client.selection = message.data;
378
+ found.client.lastActivity = Date.now();
379
+ this._activeFileKey = found.fileKey;
380
+ }
381
+ this.emit('selectionChange', { fileKey: found?.fileKey ?? null, ...message.data });
382
+ }
383
+ // Track page changes — user interaction makes this the active file
384
+ if (message.type === 'PAGE_CHANGE' && message.data) {
385
+ const found = this.findClientByWs(ws);
386
+ if (found) {
387
+ found.client.fileInfo.currentPage = message.data.pageName;
388
+ found.client.fileInfo.currentPageId = message.data.pageId || null;
389
+ found.client.lastActivity = Date.now();
390
+ this._activeFileKey = found.fileKey;
391
+ }
392
+ this.emit('pageChange', { fileKey: found?.fileKey ?? null, ...message.data });
393
+ }
394
+ // Capture console logs for the specific file
395
+ if (message.type === 'CONSOLE_CAPTURE' && message.data) {
396
+ const found = this.findClientByWs(ws);
397
+ const data = message.data;
398
+ const entry = {
399
+ timestamp: data.timestamp || Date.now(),
400
+ level: data.level || 'log',
401
+ message: typeof data.message === 'string' ? data.message.substring(0, 1000) : String(data.message),
402
+ args: Array.isArray(data.args) ? data.args.slice(0, 10) : [],
403
+ source: 'plugin',
404
+ };
405
+ if (found) {
406
+ found.client.consoleLogs.push(entry);
407
+ if (found.client.consoleLogs.length > this.consoleBufferSize) {
408
+ found.client.consoleLogs.shift();
409
+ }
410
+ found.client.lastActivity = Date.now();
411
+ }
412
+ this.emit('consoleLog', entry);
413
+ }
414
+ this.emit('pluginMessage', message);
415
+ return;
416
+ }
417
+ logger.debug({ message }, 'Unhandled WebSocket message');
418
+ }
419
+ /**
420
+ * Handle FILE_INFO message — promotes pending clients to named clients.
421
+ * This is the critical multi-client identification step: each plugin reports
422
+ * its fileKey on connect, allowing the server to track multiple files.
423
+ */
424
+ handleFileInfo(data, ws) {
425
+ const fileKey = data.fileKey || null;
426
+ if (!fileKey) {
427
+ logger.warn('FILE_INFO received without fileKey — client remains pending');
428
+ return;
429
+ }
430
+ // Remove from pending clients (cancel identification timeout)
431
+ const pendingTimeout = this._pendingClients.get(ws);
432
+ if (pendingTimeout) {
433
+ clearTimeout(pendingTimeout);
434
+ this._pendingClients.delete(ws);
435
+ }
436
+ // Check if this ws was already registered under a different fileKey
437
+ // (shouldn't happen in practice — each plugin instance is per-file)
438
+ const previousEntry = this.findClientByWs(ws);
439
+ if (previousEntry && previousEntry.fileKey !== fileKey) {
440
+ this.clients.delete(previousEntry.fileKey);
441
+ if (this._activeFileKey === previousEntry.fileKey) {
442
+ this._activeFileKey = null;
443
+ }
444
+ // In-flight commands to the old file can never receive a response now —
445
+ // reject them immediately instead of leaving them to hang until timeout
446
+ // (mirrors the same-file replacement path below).
447
+ this.rejectPendingRequestsForFile(previousEntry.fileKey, 'Plugin switched files');
448
+ logger.info({ oldFileKey: previousEntry.fileKey, newFileKey: fileKey }, 'WebSocket client switched files');
449
+ this.emit('fileDisconnected', {
450
+ fileKey: previousEntry.fileKey,
451
+ fileName: previousEntry.client.fileInfo.fileName,
452
+ });
453
+ }
454
+ // If same fileKey already connected with a DIFFERENT ws, clean up old connection
455
+ const existing = this.clients.get(fileKey);
456
+ if (existing && existing.ws !== ws) {
457
+ logger.info({ fileKey }, 'Replacing existing connection for same file');
458
+ if (existing.gracePeriodTimer) {
459
+ clearTimeout(existing.gracePeriodTimer);
460
+ }
461
+ // Reject any in-flight commands before replacing — the old ws close event
462
+ // won't find this fileKey in the map after overwrite, so pending requests
463
+ // would hang until timeout otherwise.
464
+ this.rejectPendingRequestsForFile(fileKey, 'Connection replaced by same file reconnection');
465
+ if (existing.ws.readyState === WebSocket.OPEN || existing.ws.readyState === WebSocket.CONNECTING) {
466
+ existing.ws.close(1000, 'Replaced by same file reconnection');
467
+ }
468
+ }
469
+ // Version handshake: the plugin reports its code.js version in FILE_INFO.
470
+ // Figma caches plugin files at the app level, so a version mismatch means
471
+ // the user is running stale plugin code and must re-import manifest.json.
472
+ // A missing version means the plugin predates version reporting — also stale.
473
+ // Compared against the BUNDLED plugin version (what a re-import would
474
+ // install), NOT the server's package version — server-only releases don't
475
+ // change the plugin files, so they must not trigger the re-import banner.
476
+ const bundledPluginVersion = this.options.bundledPluginVersion ?? BUNDLED_PLUGIN_VERSION;
477
+ const pluginVersion = data.pluginVersion || null;
478
+ const pluginUpdateAvailable = computePluginUpdateAvailable(pluginVersion, bundledPluginVersion);
479
+ if (pluginUpdateAvailable) {
480
+ logger.warn({ fileKey, pluginVersion, bundledPluginVersion, serverVersion: SERVER_VERSION }, 'Imported plugin version differs from the plugin files bundled with this server — re-import manifest.json to update');
481
+ try {
482
+ ws.send(JSON.stringify({
483
+ type: 'PLUGIN_UPDATE_AVAILABLE',
484
+ serverVersion: SERVER_VERSION,
485
+ bundledPluginVersion,
486
+ pluginVersion,
487
+ }));
488
+ }
489
+ catch {
490
+ // Non-critical notification — never let it disrupt registration
491
+ }
492
+ }
493
+ // Create client connection (preserve per-file state from previous connection of same file)
494
+ this.clients.set(fileKey, {
495
+ ws,
496
+ fileInfo: {
497
+ fileName: data.fileName,
498
+ fileKey,
499
+ currentPage: data.currentPage,
500
+ currentPageId: data.currentPageId || null,
501
+ editorType: data.editorType || 'figma',
502
+ connectedAt: Date.now(),
503
+ pluginVersion,
504
+ pluginUpdateAvailable,
505
+ },
506
+ selection: existing?.selection || null,
507
+ documentChanges: existing?.documentChanges || [],
508
+ metadataChanges: existing?.metadataChanges || [],
509
+ consoleLogs: existing?.consoleLogs || [],
510
+ lastActivity: Date.now(),
511
+ lastPongAt: Date.now(),
512
+ gracePeriodTimer: null,
513
+ });
514
+ // Most recently connected file becomes active (user just opened the plugin there).
515
+ // On bulk reconnect the order is non-deterministic, but the first user interaction
516
+ // (SELECTION_CHANGE or PAGE_CHANGE) will correct the active file immediately.
517
+ this._activeFileKey = fileKey;
518
+ logger.info({
519
+ fileName: data.fileName,
520
+ fileKey,
521
+ totalClients: this.clients.size,
522
+ isActive: this._activeFileKey === fileKey,
523
+ }, 'File connected via WebSocket');
524
+ // Emit both events for backward compat and new features
525
+ this.emit('connected');
526
+ this.emit('fileConnected', { fileKey, fileName: data.fileName });
527
+ }
528
+ /**
529
+ * Handle a client WebSocket disconnecting.
530
+ * Starts a grace period before removing the client to allow reconnection.
531
+ */
532
+ handleClientDisconnect(ws, code, reason) {
533
+ // Check if it was a pending client (never identified itself)
534
+ const pendingTimeout = this._pendingClients.get(ws);
535
+ if (pendingTimeout) {
536
+ clearTimeout(pendingTimeout);
537
+ this._pendingClients.delete(ws);
538
+ logger.info('Pending WebSocket client disconnected before file identification');
539
+ this.emit('disconnected');
540
+ return;
541
+ }
542
+ // Find which named client this belongs to
543
+ const found = this.findClientByWs(ws);
544
+ if (!found) {
545
+ logger.debug('Unknown WebSocket client disconnected');
546
+ this.emit('disconnected');
547
+ return;
548
+ }
549
+ const { fileKey, client } = found;
550
+ logger.info({ fileKey, fileName: client.fileInfo.fileName, code, reason }, 'File disconnected from WebSocket');
551
+ // Start grace period — keep state but clean up if not reconnected
552
+ client.gracePeriodTimer = setTimeout(() => {
553
+ client.gracePeriodTimer = null;
554
+ // Only remove if the client in the map is still the disconnected one
555
+ const current = this.clients.get(fileKey);
556
+ if (current && current.ws === ws) {
557
+ this.clients.delete(fileKey);
558
+ this.rejectPendingRequestsForFile(fileKey, 'WebSocket client disconnected');
559
+ // If active file disconnected, switch to another connected file
560
+ if (this._activeFileKey === fileKey) {
561
+ this._activeFileKey = null;
562
+ for (const [fk, c] of this.clients) {
563
+ if (c.ws.readyState === WebSocket.OPEN) {
564
+ this._activeFileKey = fk;
565
+ break;
566
+ }
567
+ }
568
+ }
569
+ this.emit('fileDisconnected', { fileKey, fileName: client.fileInfo.fileName });
570
+ }
571
+ }, 5000);
572
+ this.emit('disconnected');
573
+ }
574
+ /**
575
+ * Send a command to a plugin UI and wait for the response.
576
+ * By default targets the active file. Pass targetFileKey to target a specific file.
577
+ */
578
+ sendCommand(method, params = {}, timeoutMs = 15000, targetFileKey) {
579
+ return new Promise((resolve, reject) => {
580
+ const fileKey = targetFileKey || this._activeFileKey;
581
+ if (!fileKey) {
582
+ reject(new Error('No active file connected. Make sure the Desktop Bridge plugin is open in Figma.'));
583
+ return;
584
+ }
585
+ const client = this.clients.get(fileKey);
586
+ if (!client || client.ws.readyState !== WebSocket.OPEN) {
587
+ reject(new Error('No WebSocket client connected. Make sure the Desktop Bridge plugin is open in Figma.'));
588
+ return;
589
+ }
590
+ const id = `ws_${++this.requestIdCounter}_${Date.now()}`;
591
+ const timeoutId = setTimeout(() => {
592
+ if (this.pendingRequests.has(id)) {
593
+ this.pendingRequests.delete(id);
594
+ reject(new Error(`WebSocket command ${method} timed out after ${timeoutMs}ms`));
595
+ }
596
+ }, timeoutMs);
597
+ this.pendingRequests.set(id, {
598
+ resolve,
599
+ reject,
600
+ method,
601
+ timeoutId,
602
+ createdAt: Date.now(),
603
+ targetFileKey: fileKey,
604
+ });
605
+ const message = JSON.stringify({ id, method, params });
606
+ try {
607
+ client.ws.send(message);
608
+ }
609
+ catch (sendError) {
610
+ this.pendingRequests.delete(id);
611
+ clearTimeout(timeoutId);
612
+ reject(new Error(`Failed to send WebSocket command ${method}: ${sendError instanceof Error ? sendError.message : String(sendError)}`));
613
+ return;
614
+ }
615
+ client.lastActivity = Date.now();
616
+ logger.debug({ id, method, fileKey }, 'Sent WebSocket command');
617
+ });
618
+ }
619
+ /**
620
+ * Start the heartbeat interval that pings all connected clients every 30s.
621
+ * Detects silently dropped connections (e.g., macOS sleep, network change)
622
+ * that the OS TCP keepalive would take 30-120s to catch.
623
+ * Browser WebSocket clients auto-respond to pings per RFC 6455.
624
+ */
625
+ startHeartbeat() {
626
+ this._heartbeatInterval = setInterval(() => {
627
+ if (!this.wss)
628
+ return;
629
+ for (const ws of this.wss.clients) {
630
+ if (ws.isAlive === false) {
631
+ logger.info('Terminating unresponsive WebSocket client (missed heartbeat pong)');
632
+ ws.terminate();
633
+ continue;
634
+ }
635
+ ws.isAlive = false;
636
+ ws.ping();
637
+ }
638
+ }, 30000);
639
+ }
640
+ /**
641
+ * Check if any named client is connected (transport availability check).
642
+ * Checks both socket readyState and heartbeat pong freshness to avoid
643
+ * reporting phantom-connected state on silently dropped connections.
644
+ */
645
+ isClientConnected() {
646
+ const now = Date.now();
647
+ for (const [, client] of this.clients) {
648
+ if (client.ws.readyState === WebSocket.OPEN && (now - client.lastPongAt) < 90000) {
649
+ return true;
650
+ }
651
+ }
652
+ return false;
653
+ }
654
+ /**
655
+ * Whether the server has been started
656
+ */
657
+ isStarted() {
658
+ return this._isStarted;
659
+ }
660
+ /**
661
+ * Get the bound address info (port, host, family).
662
+ * Only available after the server has started listening.
663
+ * Returns the actual port — critical when using port 0 for OS-assigned ports.
664
+ */
665
+ address() {
666
+ // Use the HTTP server's address (which is the actual listening socket)
667
+ if (this.httpServer) {
668
+ const addr = this.httpServer.address();
669
+ if (typeof addr === 'string' || !addr)
670
+ return null;
671
+ return addr;
672
+ }
673
+ // Fallback for backward compat
674
+ if (!this.wss)
675
+ return null;
676
+ const addr = this.wss.address();
677
+ if (typeof addr === 'string')
678
+ return null;
679
+ return addr;
680
+ }
681
+ // ============================================================================
682
+ // Active file getters (backward compatible — return active file's state)
683
+ // ============================================================================
684
+ /**
685
+ * Get info about the currently active Figma file.
686
+ * Returns null if no file is active or connected.
687
+ */
688
+ getConnectedFileInfo() {
689
+ if (!this._activeFileKey)
690
+ return null;
691
+ const client = this.clients.get(this._activeFileKey);
692
+ return client?.fileInfo || null;
693
+ }
694
+ /**
695
+ * Get the current user selection in the active Figma file
696
+ */
697
+ getCurrentSelection() {
698
+ if (!this._activeFileKey)
699
+ return null;
700
+ const client = this.clients.get(this._activeFileKey);
701
+ return client?.selection || null;
702
+ }
703
+ /**
704
+ * Get buffered document change events from the active file
705
+ */
706
+ getDocumentChanges(options) {
707
+ if (!this._activeFileKey)
708
+ return [];
709
+ const client = this.clients.get(this._activeFileKey);
710
+ if (!client)
711
+ return [];
712
+ let filtered = [...client.documentChanges];
713
+ if (options?.since !== undefined) {
714
+ filtered = filtered.filter((e) => e.timestamp >= options.since);
715
+ }
716
+ if (options?.count !== undefined && options.count > 0) {
717
+ filtered = filtered.slice(-options.count);
718
+ }
719
+ return filtered;
720
+ }
721
+ /**
722
+ * Clear document change buffer for the active file
723
+ */
724
+ clearDocumentChanges() {
725
+ if (!this._activeFileKey)
726
+ return 0;
727
+ const client = this.clients.get(this._activeFileKey);
728
+ if (!client)
729
+ return 0;
730
+ const count = client.documentChanges.length;
731
+ client.documentChanges = [];
732
+ return count;
733
+ }
734
+ /**
735
+ * v1.25.0: Get buffered description/annotation change events for a file.
736
+ *
737
+ * Used by `figma_diff_versions` to surface changes that Figma REST doesn't
738
+ * expose. The diff engine passes a time window (from-version → to-version
739
+ * `last_modified` timestamps converted to Unix ms) and optional scoping by
740
+ * node IDs.
741
+ *
742
+ * Defaults to the active file if `fileKey` is omitted.
743
+ */
744
+ getMetadataChanges(options) {
745
+ const fileKey = options?.fileKey ?? this._activeFileKey;
746
+ if (!fileKey)
747
+ return [];
748
+ const client = this.clients.get(fileKey);
749
+ if (!client)
750
+ return [];
751
+ let filtered = [...client.metadataChanges];
752
+ if (options?.since !== undefined) {
753
+ filtered = filtered.filter((e) => e.timestamp >= options.since);
754
+ }
755
+ if (options?.until !== undefined) {
756
+ filtered = filtered.filter((e) => e.timestamp <= options.until);
757
+ }
758
+ if (options?.nodeIds && options.nodeIds.length > 0) {
759
+ const set = new Set(options.nodeIds);
760
+ filtered = filtered.filter((e) => set.has(e.node_id));
761
+ }
762
+ return filtered;
763
+ }
764
+ /**
765
+ * v1.25.0: Clear metadata-change buffer for the active file.
766
+ */
767
+ clearMetadataChanges() {
768
+ if (!this._activeFileKey)
769
+ return 0;
770
+ const client = this.clients.get(this._activeFileKey);
771
+ if (!client)
772
+ return 0;
773
+ const count = client.metadataChanges.length;
774
+ client.metadataChanges = [];
775
+ return count;
776
+ }
777
+ /**
778
+ * Get console logs from the active file with optional filtering
779
+ */
780
+ getConsoleLogs(options) {
781
+ if (!this._activeFileKey)
782
+ return [];
783
+ const client = this.clients.get(this._activeFileKey);
784
+ if (!client)
785
+ return [];
786
+ let filtered = [...client.consoleLogs];
787
+ if (options?.since !== undefined) {
788
+ filtered = filtered.filter((log) => log.timestamp >= options.since);
789
+ }
790
+ if (options?.level && options.level !== 'all') {
791
+ filtered = filtered.filter((log) => log.level === options.level);
792
+ }
793
+ if (options?.count !== undefined && options.count > 0) {
794
+ filtered = filtered.slice(-options.count);
795
+ }
796
+ return filtered;
797
+ }
798
+ /**
799
+ * Clear console log buffer for the active file
800
+ */
801
+ clearConsoleLogs() {
802
+ if (!this._activeFileKey)
803
+ return 0;
804
+ const client = this.clients.get(this._activeFileKey);
805
+ if (!client)
806
+ return 0;
807
+ const count = client.consoleLogs.length;
808
+ client.consoleLogs = [];
809
+ return count;
810
+ }
811
+ /**
812
+ * Get console monitoring status for the active file
813
+ */
814
+ getConsoleStatus() {
815
+ const client = this._activeFileKey ? this.clients.get(this._activeFileKey) : null;
816
+ const logs = client?.consoleLogs || [];
817
+ return {
818
+ isMonitoring: !!(client && client.ws.readyState === WebSocket.OPEN),
819
+ anyClientConnected: this.isClientConnected(),
820
+ logCount: logs.length,
821
+ bufferSize: this.consoleBufferSize,
822
+ workerCount: 0,
823
+ oldestTimestamp: logs[0]?.timestamp,
824
+ newestTimestamp: logs[logs.length - 1]?.timestamp,
825
+ };
826
+ }
827
+ // ============================================================================
828
+ // Multi-client methods
829
+ // ============================================================================
830
+ /**
831
+ * Get info about all connected Figma files.
832
+ * Returns an array of ConnectedFileInfo for each file with an active WebSocket.
833
+ */
834
+ getConnectedFiles() {
835
+ const files = [];
836
+ for (const [fileKey, client] of this.clients) {
837
+ if (client.ws.readyState === WebSocket.OPEN) {
838
+ files.push({
839
+ ...client.fileInfo,
840
+ isActive: fileKey === this._activeFileKey,
841
+ });
842
+ }
843
+ }
844
+ return files;
845
+ }
846
+ /**
847
+ * Get the last pong timestamp for the active client.
848
+ * Returns null if no active client or no pong received yet.
849
+ */
850
+ getActiveClientLastPongAt() {
851
+ if (!this._activeFileKey)
852
+ return null;
853
+ const client = this.clients.get(this._activeFileKey);
854
+ return client?.lastPongAt ?? null;
855
+ }
856
+ /**
857
+ * Set the active file by fileKey. Returns true if the file is connected.
858
+ */
859
+ setActiveFile(fileKey) {
860
+ const client = this.clients.get(fileKey);
861
+ if (client && client.ws.readyState === WebSocket.OPEN) {
862
+ this._activeFileKey = fileKey;
863
+ logger.info({ fileKey, fileName: client.fileInfo.fileName }, 'Active file switched');
864
+ this.emit('activeFileChanged', { fileKey, fileName: client.fileInfo.fileName });
865
+ return true;
866
+ }
867
+ return false;
868
+ }
869
+ /**
870
+ * Get the currently active file's key
871
+ */
872
+ getActiveFileKey() {
873
+ return this._activeFileKey;
874
+ }
875
+ /**
876
+ * Get the editor type of the currently active file.
877
+ * Returns 'figma' if no file is connected or editorType wasn't reported.
878
+ */
879
+ getEditorType() {
880
+ if (!this._activeFileKey)
881
+ return 'figma';
882
+ const client = this.clients.get(this._activeFileKey);
883
+ return client?.fileInfo?.editorType || 'figma';
884
+ }
885
+ // ============================================================================
886
+ // Cleanup
887
+ // ============================================================================
888
+ /**
889
+ * Reject pending requests that were sent to a specific file
890
+ */
891
+ rejectPendingRequestsForFile(fileKey, reason) {
892
+ for (const [id, pending] of this.pendingRequests) {
893
+ if (pending.targetFileKey === fileKey) {
894
+ clearTimeout(pending.timeoutId);
895
+ pending.reject(new Error(reason));
896
+ this.pendingRequests.delete(id);
897
+ }
898
+ }
899
+ }
900
+ /**
901
+ * Reject all pending requests (used during shutdown)
902
+ */
903
+ rejectPendingRequests(reason) {
904
+ for (const [, pending] of this.pendingRequests) {
905
+ clearTimeout(pending.timeoutId);
906
+ pending.reject(new Error(reason));
907
+ }
908
+ this.pendingRequests.clear();
909
+ }
910
+ /**
911
+ * Stop the server and clean up all connections
912
+ */
913
+ async stop() {
914
+ // Clear heartbeat interval
915
+ if (this._heartbeatInterval) {
916
+ clearInterval(this._heartbeatInterval);
917
+ this._heartbeatInterval = null;
918
+ }
919
+ // Clear all per-client grace period timers
920
+ for (const [, client] of this.clients) {
921
+ if (client.gracePeriodTimer) {
922
+ clearTimeout(client.gracePeriodTimer);
923
+ client.gracePeriodTimer = null;
924
+ }
925
+ }
926
+ // Clear pending client identification timeouts
927
+ for (const [, timeout] of this._pendingClients) {
928
+ clearTimeout(timeout);
929
+ }
930
+ this._pendingClients.clear();
931
+ this.rejectPendingRequests('WebSocket server shutting down');
932
+ // Terminate all connected clients so wss.close() resolves promptly
933
+ if (this.wss) {
934
+ for (const ws of this.wss.clients) {
935
+ ws.terminate();
936
+ }
937
+ }
938
+ this.clients.clear();
939
+ this._activeFileKey = null;
940
+ // Close WS server first (handles WebSocket connections)
941
+ if (this.wss) {
942
+ await new Promise((resolve) => {
943
+ this.wss.close(() => resolve());
944
+ });
945
+ }
946
+ // Then close HTTP server (releases the port)
947
+ if (this.httpServer) {
948
+ await new Promise((resolve) => {
949
+ this.httpServer.close(() => resolve());
950
+ });
951
+ this.httpServer = null;
952
+ }
953
+ this._isStarted = false;
954
+ logger.info('WebSocket bridge server stopped');
955
+ }
956
+ }