@mp3wizard/figma-console-mcp 1.32.2 → 1.34.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -17
- package/dist/cloudflare/core/cloud-websocket-connector.js +18 -0
- package/dist/cloudflare/core/design-code-tools.js +60 -17
- package/dist/cloudflare/core/design-system-manifest.js +19 -14
- package/dist/cloudflare/core/design-system-tools.js +43 -34
- package/dist/cloudflare/core/diagnose-tool.js +4 -0
- package/dist/cloudflare/core/enrichment/enrichment-service.js +11 -5
- package/dist/cloudflare/core/enrichment/style-resolver.js +38 -18
- package/dist/cloudflare/core/figma-api.js +118 -54
- package/dist/cloudflare/core/figma-tools.js +179 -63
- package/dist/cloudflare/core/port-discovery.js +404 -31
- package/dist/cloudflare/core/tokens/alias-resolver.js +75 -5
- package/dist/cloudflare/core/tokens/config.js +10 -0
- package/dist/cloudflare/core/tokens/dialect.js +232 -0
- package/dist/cloudflare/core/tokens/figma-converter.js +144 -16
- package/dist/cloudflare/core/tokens/formatters/css-vars.js +21 -12
- package/dist/cloudflare/core/tokens/formatters/dtcg.js +106 -30
- package/dist/cloudflare/core/tokens/formatters/json.js +28 -10
- package/dist/cloudflare/core/tokens/formatters/scss.js +19 -13
- package/dist/cloudflare/core/tokens/formatters/style-dictionary-v3.js +15 -9
- package/dist/cloudflare/core/tokens/formatters/tailwind-v4.js +14 -9
- package/dist/cloudflare/core/tokens/formatters/tokens-studio.js +11 -5
- package/dist/cloudflare/core/tokens/index.js +2 -1
- package/dist/cloudflare/core/tokens/parsers/dtcg.js +32 -5
- package/dist/cloudflare/core/tokens/schemas.js +4 -0
- package/dist/cloudflare/core/tokens-tools.js +1017 -88
- package/dist/cloudflare/core/version-tools.js +44 -3
- package/dist/cloudflare/core/websocket-connector.js +42 -0
- package/dist/cloudflare/core/websocket-server.js +99 -8
- package/dist/cloudflare/core/write-tools.js +355 -86
- package/dist/cloudflare/index.js +7 -7
- package/dist/core/design-code-tools.d.ts.map +1 -1
- package/dist/core/design-code-tools.js +60 -17
- package/dist/core/design-code-tools.js.map +1 -1
- package/dist/core/design-system-manifest.d.ts +1 -0
- package/dist/core/design-system-manifest.d.ts.map +1 -1
- package/dist/core/design-system-manifest.js +19 -14
- package/dist/core/design-system-manifest.js.map +1 -1
- package/dist/core/design-system-tools.d.ts.map +1 -1
- package/dist/core/design-system-tools.js +43 -34
- package/dist/core/design-system-tools.js.map +1 -1
- package/dist/core/diagnose-tool.d.ts +8 -0
- package/dist/core/diagnose-tool.d.ts.map +1 -1
- package/dist/core/diagnose-tool.js +4 -0
- package/dist/core/diagnose-tool.js.map +1 -1
- package/dist/core/enrichment/enrichment-service.d.ts.map +1 -1
- package/dist/core/enrichment/enrichment-service.js +11 -5
- package/dist/core/enrichment/enrichment-service.js.map +1 -1
- package/dist/core/enrichment/style-resolver.d.ts +7 -2
- package/dist/core/enrichment/style-resolver.d.ts.map +1 -1
- package/dist/core/enrichment/style-resolver.js +38 -18
- package/dist/core/enrichment/style-resolver.js.map +1 -1
- package/dist/core/figma-api.d.ts +18 -9
- package/dist/core/figma-api.d.ts.map +1 -1
- package/dist/core/figma-api.js +118 -54
- package/dist/core/figma-api.js.map +1 -1
- package/dist/core/figma-connector.d.ts +12 -0
- package/dist/core/figma-connector.d.ts.map +1 -1
- package/dist/core/figma-tools.d.ts.map +1 -1
- package/dist/core/figma-tools.js +179 -63
- package/dist/core/figma-tools.js.map +1 -1
- package/dist/core/port-discovery.d.ts +40 -0
- package/dist/core/port-discovery.d.ts.map +1 -1
- package/dist/core/port-discovery.js +404 -31
- package/dist/core/port-discovery.js.map +1 -1
- package/dist/core/tokens/alias-resolver.d.ts +45 -3
- package/dist/core/tokens/alias-resolver.d.ts.map +1 -1
- package/dist/core/tokens/alias-resolver.js +75 -5
- package/dist/core/tokens/alias-resolver.js.map +1 -1
- package/dist/core/tokens/config.d.ts +28 -0
- package/dist/core/tokens/config.d.ts.map +1 -1
- package/dist/core/tokens/config.js +10 -0
- package/dist/core/tokens/config.js.map +1 -1
- package/dist/core/tokens/dialect.d.ts +107 -0
- package/dist/core/tokens/dialect.d.ts.map +1 -0
- package/dist/core/tokens/dialect.js +233 -0
- package/dist/core/tokens/dialect.js.map +1 -0
- package/dist/core/tokens/figma-converter.d.ts +23 -2
- package/dist/core/tokens/figma-converter.d.ts.map +1 -1
- package/dist/core/tokens/figma-converter.js +144 -16
- package/dist/core/tokens/figma-converter.js.map +1 -1
- package/dist/core/tokens/formatters/css-vars.d.ts.map +1 -1
- package/dist/core/tokens/formatters/css-vars.js +21 -12
- package/dist/core/tokens/formatters/css-vars.js.map +1 -1
- package/dist/core/tokens/formatters/dtcg.d.ts +2 -2
- package/dist/core/tokens/formatters/dtcg.d.ts.map +1 -1
- package/dist/core/tokens/formatters/dtcg.js +106 -30
- package/dist/core/tokens/formatters/dtcg.js.map +1 -1
- package/dist/core/tokens/formatters/json.d.ts.map +1 -1
- package/dist/core/tokens/formatters/json.js +28 -10
- package/dist/core/tokens/formatters/json.js.map +1 -1
- package/dist/core/tokens/formatters/scss.d.ts.map +1 -1
- package/dist/core/tokens/formatters/scss.js +19 -13
- package/dist/core/tokens/formatters/scss.js.map +1 -1
- package/dist/core/tokens/formatters/style-dictionary-v3.d.ts.map +1 -1
- package/dist/core/tokens/formatters/style-dictionary-v3.js +15 -9
- package/dist/core/tokens/formatters/style-dictionary-v3.js.map +1 -1
- package/dist/core/tokens/formatters/tailwind-v4.d.ts.map +1 -1
- package/dist/core/tokens/formatters/tailwind-v4.js +14 -9
- package/dist/core/tokens/formatters/tailwind-v4.js.map +1 -1
- package/dist/core/tokens/formatters/tokens-studio.d.ts.map +1 -1
- package/dist/core/tokens/formatters/tokens-studio.js +11 -5
- package/dist/core/tokens/formatters/tokens-studio.js.map +1 -1
- package/dist/core/tokens/index.d.ts +2 -1
- package/dist/core/tokens/index.d.ts.map +1 -1
- package/dist/core/tokens/index.js +2 -1
- package/dist/core/tokens/index.js.map +1 -1
- package/dist/core/tokens/parsers/dtcg.js +32 -5
- package/dist/core/tokens/parsers/dtcg.js.map +1 -1
- package/dist/core/tokens/schemas.d.ts +3 -0
- package/dist/core/tokens/schemas.d.ts.map +1 -1
- package/dist/core/tokens/schemas.js +4 -0
- package/dist/core/tokens/schemas.js.map +1 -1
- package/dist/core/tokens/types.d.ts +57 -1
- package/dist/core/tokens/types.d.ts.map +1 -1
- package/dist/core/tokens/types.js.map +1 -1
- package/dist/core/tokens-tools.d.ts +250 -7
- package/dist/core/tokens-tools.d.ts.map +1 -1
- package/dist/core/tokens-tools.js +1017 -88
- package/dist/core/tokens-tools.js.map +1 -1
- package/dist/core/version-tools.d.ts.map +1 -1
- package/dist/core/version-tools.js +44 -3
- package/dist/core/version-tools.js.map +1 -1
- package/dist/core/websocket-connector.d.ts +38 -0
- package/dist/core/websocket-connector.d.ts.map +1 -1
- package/dist/core/websocket-connector.js +42 -0
- package/dist/core/websocket-connector.js.map +1 -1
- package/dist/core/websocket-server.d.ts +23 -0
- package/dist/core/websocket-server.d.ts.map +1 -1
- package/dist/core/websocket-server.js +99 -8
- package/dist/core/websocket-server.js.map +1 -1
- package/dist/core/write-tools.d.ts.map +1 -1
- package/dist/core/write-tools.js +355 -86
- package/dist/core/write-tools.js.map +1 -1
- package/dist/local.d.ts +0 -1
- package/dist/local.d.ts.map +1 -1
- package/dist/local.js +253 -63
- package/dist/local.js.map +1 -1
- package/figma-desktop-bridge/code.js +382 -28
- package/figma-desktop-bridge/ui.html +578 -292
- package/package.json +2 -2
|
@@ -663,7 +663,11 @@ getMetadataChanges) {
|
|
|
663
663
|
});
|
|
664
664
|
}
|
|
665
665
|
catch (e) {
|
|
666
|
-
|
|
666
|
+
// Degrading to null metadata IS surfaced to callers (formatter shows
|
|
667
|
+
// "metadata not available") — just make sure the underlying reason
|
|
668
|
+
// lands in the log for debugging.
|
|
669
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
670
|
+
logger.warn({ err: e, reason, page }, "Author enrichment lookup failed; continuing without it");
|
|
667
671
|
break;
|
|
668
672
|
}
|
|
669
673
|
const versions = response?.versions || [];
|
|
@@ -929,6 +933,13 @@ getMetadataChanges) {
|
|
|
929
933
|
apiCalls += candidates.apiCalls;
|
|
930
934
|
cacheHits += candidates.cacheHits;
|
|
931
935
|
const versions = candidates.versions; // newest-first, all OLDER than start
|
|
936
|
+
// If the version listing itself failed and NOTHING was collected, the
|
|
937
|
+
// binary search would run over an empty range and confidently report
|
|
938
|
+
// "introduced at start_version" when nothing was actually searched.
|
|
939
|
+
// Fail loudly instead of returning a wrong answer.
|
|
940
|
+
if (candidates.listFetchFailed && versions.length === 0) {
|
|
941
|
+
return errorResponse("version_list_unavailable", `version list unavailable: ${candidates.listFetchError ?? "unknown error"} — cannot attribute the change because no version history could be fetched to search. Retry, or check that your token has the file_versions:read scope ('Versions' Read on a PAT).`);
|
|
942
|
+
}
|
|
932
943
|
// Step 3: Binary search for the LARGEST index (oldest version) where target exists.
|
|
933
944
|
// Existence is assumed monotonic: if target exists at an OLDER version (larger
|
|
934
945
|
// index), it must also exist at all NEWER versions up to start. We search for
|
|
@@ -1014,6 +1025,13 @@ getMetadataChanges) {
|
|
|
1014
1025
|
certainty = "exact";
|
|
1015
1026
|
}
|
|
1016
1027
|
}
|
|
1028
|
+
// Version listing failed partway through — the candidate list is
|
|
1029
|
+
// truncated, so the searched range may not cover the true
|
|
1030
|
+
// introduction point. Surface the gap and downgrade confidence.
|
|
1031
|
+
if (candidates.listFetchFailed) {
|
|
1032
|
+
certainty = downgradeCertainty(certainty);
|
|
1033
|
+
notes.push(`Version listing failed partway through the blame walk (${candidates.listFetchError ?? "unknown error"}); only ${versions.length} version(s) were collected and searched. The true introduction point may lie outside the searched range, so attribution_certainty was downgraded one level.`);
|
|
1034
|
+
}
|
|
1017
1035
|
notes.push("Binary search assumes the target's existence is monotonic (added once, never removed). If the target was added, removed, then re-added, this tool may report a different introduction point than the original.");
|
|
1018
1036
|
if (usedSelection) {
|
|
1019
1037
|
notes.push(`Auto-scoped to node ${resolvedNodeId} from the current Figma selection. Pass node_id explicitly to override.`);
|
|
@@ -1058,6 +1076,8 @@ getMetadataChanges) {
|
|
|
1058
1076
|
let cursor;
|
|
1059
1077
|
let apiCalls = 0;
|
|
1060
1078
|
const cacheHits = 0; // version-list pagination is not snapshot-cached
|
|
1079
|
+
let listFetchFailed = false;
|
|
1080
|
+
let listFetchError;
|
|
1061
1081
|
// Once we hit start_version's id in the list, switch to "collecting older" mode
|
|
1062
1082
|
let foundStart = isCurrentSentinel(startVer);
|
|
1063
1083
|
const MAX_PAGES = 10; // 10 × 50 = 500 versions hard cap on scan
|
|
@@ -1071,7 +1091,9 @@ getMetadataChanges) {
|
|
|
1071
1091
|
apiCalls++;
|
|
1072
1092
|
}
|
|
1073
1093
|
catch (e) {
|
|
1074
|
-
|
|
1094
|
+
listFetchFailed = true;
|
|
1095
|
+
listFetchError = e instanceof Error ? e.message : String(e);
|
|
1096
|
+
logger.warn({ err: e, reason: listFetchError, page }, "Version list fetch failed during blame walk");
|
|
1075
1097
|
break;
|
|
1076
1098
|
}
|
|
1077
1099
|
const versions = response?.versions || [];
|
|
@@ -1099,9 +1121,28 @@ getMetadataChanges) {
|
|
|
1099
1121
|
break;
|
|
1100
1122
|
cursor = last.id;
|
|
1101
1123
|
}
|
|
1102
|
-
return { versions: collected, apiCalls, cacheHits };
|
|
1124
|
+
return { versions: collected, apiCalls, cacheHits, listFetchFailed, listFetchError };
|
|
1103
1125
|
};
|
|
1104
1126
|
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Downgrade an attribution_certainty value one confidence level when the
|
|
1129
|
+
* searched version range is known to be incomplete (truncated version listing).
|
|
1130
|
+
* "exact" and "system_attributed" both mean "found the introducing version"
|
|
1131
|
+
* and downgrade to "exists_at_lookback_horizon" (the true introduction may lie
|
|
1132
|
+
* outside the searched range); the horizon case downgrades to
|
|
1133
|
+
* "metadata_unavailable", which is already the floor.
|
|
1134
|
+
*/
|
|
1135
|
+
function downgradeCertainty(certainty) {
|
|
1136
|
+
switch (certainty) {
|
|
1137
|
+
case "exact":
|
|
1138
|
+
case "system_attributed":
|
|
1139
|
+
return "exists_at_lookback_horizon";
|
|
1140
|
+
case "exists_at_lookback_horizon":
|
|
1141
|
+
return "metadata_unavailable";
|
|
1142
|
+
default:
|
|
1143
|
+
return certainty;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1105
1146
|
// Returns true if the target is present in the given node tree.
|
|
1106
1147
|
function targetExists(node, target) {
|
|
1107
1148
|
if (target.target_component_property) {
|
|
@@ -9,6 +9,40 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { createChildLogger } from './logger.js';
|
|
11
11
|
const logger = createChildLogger({ component: 'websocket-connector' });
|
|
12
|
+
/**
|
|
13
|
+
* Number of variants a CREATE_COMPONENT_SET request will produce:
|
|
14
|
+
* Mode B = componentIds.length; Mode A = the cartesian product of the
|
|
15
|
+
* property axes ({State:[a,b,c], Size:[s,l]} → 6).
|
|
16
|
+
*/
|
|
17
|
+
export function componentSetVariantCount(params) {
|
|
18
|
+
if (params.componentIds?.length)
|
|
19
|
+
return params.componentIds.length;
|
|
20
|
+
if (params.properties) {
|
|
21
|
+
let count = 1;
|
|
22
|
+
for (const values of Object.values(params.properties)) {
|
|
23
|
+
count *= Math.max(1, values?.length ?? 1);
|
|
24
|
+
}
|
|
25
|
+
return count;
|
|
26
|
+
}
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Server-hop timeout for CREATE_COMPONENT_SET, scaled by variant count.
|
|
31
|
+
* The plugin builds all variants in ONE uncancellable pass, so timing out
|
|
32
|
+
* early doesn't stop the work — it just makes the report contradict the
|
|
33
|
+
* file state (set gets created after the "failure") and invites duplicate
|
|
34
|
+
* retries. Budget ~1.2s per variant with a 30s floor and 120s cap, plus a
|
|
35
|
+
* 5s buffer over the ui.html hop (which uses the same base formula) so the
|
|
36
|
+
* plugin-side result or error always wins the race.
|
|
37
|
+
*
|
|
38
|
+
* NOTE: ui.html's CREATE_COMPONENT_SET route and
|
|
39
|
+
* cloud-websocket-connector.ts mirror the base formula — keep all three in
|
|
40
|
+
* sync.
|
|
41
|
+
*/
|
|
42
|
+
export function componentSetTimeoutMs(params) {
|
|
43
|
+
const count = componentSetVariantCount(params);
|
|
44
|
+
return Math.min(120000, Math.max(30000, count * 1200)) + 5000;
|
|
45
|
+
}
|
|
12
46
|
export class WebSocketConnector {
|
|
13
47
|
constructor(wsServer) {
|
|
14
48
|
this.wsServer = wsServer;
|
|
@@ -188,6 +222,14 @@ export class WebSocketConnector {
|
|
|
188
222
|
}
|
|
189
223
|
return this.wsServer.sendCommand('INSTANTIATE_COMPONENT', params);
|
|
190
224
|
}
|
|
225
|
+
async createComponentSet(params) {
|
|
226
|
+
// Cloning + combining large variant matrices is slow on heavy components
|
|
227
|
+
// and the plugin-side pass is uncancellable — a fixed 30s ceiling made a
|
|
228
|
+
// 48-variant set REPORT failure while the plugin kept going and created
|
|
229
|
+
// the set anyway (retry → duplicates). Scale the timeout with variant
|
|
230
|
+
// count instead (precedent: the token-import create phase).
|
|
231
|
+
return this.wsServer.sendCommand('CREATE_COMPONENT_SET', params, componentSetTimeoutMs(params));
|
|
232
|
+
}
|
|
191
233
|
// ============================================================================
|
|
192
234
|
// Node manipulation
|
|
193
235
|
// ============================================================================
|
|
@@ -28,6 +28,41 @@ try {
|
|
|
28
28
|
catch {
|
|
29
29
|
// Non-critical — version will show as 0.0.0
|
|
30
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
|
+
}
|
|
31
66
|
const logger = createChildLogger({ component: 'websocket-server' });
|
|
32
67
|
export class FigmaWebSocketServer extends EventEmitter {
|
|
33
68
|
constructor(options) {
|
|
@@ -113,10 +148,12 @@ export class FigmaWebSocketServer extends EventEmitter {
|
|
|
113
148
|
// Mitigate Cross-Site WebSocket Hijacking (CSWSH):
|
|
114
149
|
// Reject connections from unexpected browser origins.
|
|
115
150
|
const origin = info.origin;
|
|
151
|
+
// Exact match only — startsWith would let e.g.
|
|
152
|
+
// https://www.figma.com.attacker.example through.
|
|
116
153
|
const allowed = !origin || // No origin — local process (e.g. Node.js client)
|
|
117
154
|
origin === 'null' || // Sandboxed iframe / Figma Desktop plugin UI
|
|
118
|
-
origin
|
|
119
|
-
origin
|
|
155
|
+
origin === 'https://www.figma.com' ||
|
|
156
|
+
origin === 'https://figma.com';
|
|
120
157
|
if (allowed) {
|
|
121
158
|
callback(true);
|
|
122
159
|
}
|
|
@@ -250,15 +287,35 @@ export class FigmaWebSocketServer extends EventEmitter {
|
|
|
250
287
|
// Response to a command we sent
|
|
251
288
|
if (message.id && this.pendingRequests.has(message.id)) {
|
|
252
289
|
const pending = this.pendingRequests.get(message.id);
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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.
|
|
257
307
|
}
|
|
258
308
|
else {
|
|
259
|
-
pending.
|
|
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;
|
|
260
318
|
}
|
|
261
|
-
return;
|
|
262
319
|
}
|
|
263
320
|
// Unsolicited data from plugin (FILE_INFO, events, forwarded data)
|
|
264
321
|
if (message.type) {
|
|
@@ -384,7 +441,15 @@ export class FigmaWebSocketServer extends EventEmitter {
|
|
|
384
441
|
if (this._activeFileKey === previousEntry.fileKey) {
|
|
385
442
|
this._activeFileKey = null;
|
|
386
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');
|
|
387
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
|
+
});
|
|
388
453
|
}
|
|
389
454
|
// If same fileKey already connected with a DIFFERENT ws, clean up old connection
|
|
390
455
|
const existing = this.clients.get(fileKey);
|
|
@@ -401,6 +466,30 @@ export class FigmaWebSocketServer extends EventEmitter {
|
|
|
401
466
|
existing.ws.close(1000, 'Replaced by same file reconnection');
|
|
402
467
|
}
|
|
403
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
|
+
}
|
|
404
493
|
// Create client connection (preserve per-file state from previous connection of same file)
|
|
405
494
|
this.clients.set(fileKey, {
|
|
406
495
|
ws,
|
|
@@ -411,6 +500,8 @@ export class FigmaWebSocketServer extends EventEmitter {
|
|
|
411
500
|
currentPageId: data.currentPageId || null,
|
|
412
501
|
editorType: data.editorType || 'figma',
|
|
413
502
|
connectedAt: Date.now(),
|
|
503
|
+
pluginVersion,
|
|
504
|
+
pluginUpdateAvailable,
|
|
414
505
|
},
|
|
415
506
|
selection: existing?.selection || null,
|
|
416
507
|
documentChanges: existing?.documentChanges || [],
|