Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ jobs:
- name: Build Figma plugin
run: bun run build:figma

- name: Test Figma plugin messaging
run: bun run --filter './packages/figma' test

- name: Build Gutenberg integration
run: bun run build:gutenberg

Expand Down
1 change: 1 addition & 0 deletions packages/figma/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"main": "esbuild main/code.ts --bundle --outfile=dist/code.js",
"editor": "vite build --config vite.editor.config.ts",
"editor:watch": "vite build --config vite.editor.config.ts --watch",
"test": "bun test ./tests",
"ui": "tsc && vite build --minify esbuild",
"build": "bun run editor && concurrently -n main,ui \"bun run main\" \"bun run ui\"",
"dev": "bun run editor && concurrently -n editor,main,ui \"bun run editor:watch\" \"bun run main -- --watch\" \"bun run ui -- --watch\""
Expand Down
15 changes: 12 additions & 3 deletions packages/figma/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ import "./style.scss";
import { ColorVariable, Declaration, Preset, SimpleVariable } from "./types";
import { generateFluidTypographyObjects } from "./typography/getFluidTypeVariables";
import { footerLinks } from "./utils/footer";
import { isMessageFromEditor, postMessageToIframe, postMessageToParent } from "./utils/frameMessaging";
import {
getPluginMessage,
isMessageFromEditor,
postMessageToIframe,
postMessageToParent,
} from "./utils/frameMessaging";

// Sync variables to Figma - must be outside component to avoid stale closures
function syncVariables(presetData: Preset, colorVariables: ColorVariable[]) {
Expand Down Expand Up @@ -116,7 +121,6 @@ function App() {
// Raw messages belong to the bundled editor iframe. Figma host messages
// arrive wrapped in event.data.pluginMessage.
if (event.data?.type && !isMessageFromEditor(event)) return;
if (event.data?.pluginMessage && event.source !== parent) return;

if (event.data.type === "figma-reopen") {
setPreset(null);
Expand Down Expand Up @@ -145,7 +149,12 @@ function App() {

// Handle messages from Figma main code (code.ts)
// This handler must be in app.tsx because SelectProjectSection unmounts after project loads
const pluginMessage = event.data?.pluginMessage;
const pluginMessage = getPluginMessage<{
type?: string;
error?: string;
preset?: Preset;
projectId?: string;
}>(event);
if (pluginMessage?.type === "wordpress-response") {
postMessageToIframe("cf-figma-wordpress-response", pluginMessage);
}
Expand Down
20 changes: 15 additions & 5 deletions packages/figma/src/components/SelectProjectSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import { generateSpacingObjects } from "../spacing/getFluidSpacingVariables";
import { ColorVariable, Declaration, Preset, SimpleVariable } from "../types";
import { generateFluidTypographyObjects } from "../typography/getFluidTypeVariables";
import { devLog } from "../utils";
import { isMessageFromEditor, postMessageToIframe, postMessageToParent } from "../utils/frameMessaging";
import {
getPluginMessage,
isMessageFromEditor,
postMessageToIframe,
postMessageToParent,
} from "../utils/frameMessaging";
import { Card } from "./Card";

interface SelectProjectSection {
Expand Down Expand Up @@ -174,9 +179,14 @@ export const SelectProjectSection = memo<SelectProjectSection>(({ handleLoadedPr
};
}>,
) => {
if (event.data?.pluginMessage && event.source !== parent) return;
const pluginMessage = getPluginMessage<{
type?: string;
error?: string;
projectId?: string;
preset?: Preset;
}>(event);
if (!pluginMessage) return;

const pluginMessage = event?.data?.pluginMessage;
devLog("pluginMessage", pluginMessage);
switch (pluginMessage?.type) {
case "import-project": {
Expand All @@ -193,7 +203,7 @@ export const SelectProjectSection = memo<SelectProjectSection>(({ handleLoadedPr
break;
}
case "get-project-id": {
const receivedApiKey = event.data.pluginMessage?.projectId;
const receivedApiKey = pluginMessage.projectId;

if (receivedApiKey) {
postMessageToIframe("cf-figma-set-api-key", { apiKey: receivedApiKey });
Expand All @@ -203,7 +213,7 @@ export const SelectProjectSection = memo<SelectProjectSection>(({ handleLoadedPr
break;
}
case "get-project-locally": {
const preset = event.data.pluginMessage?.preset;
const preset = pluginMessage.preset;
if (preset) {
setLocalPreset(preset);
}
Expand Down
6 changes: 6 additions & 0 deletions packages/figma/src/utils/frameMessaging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ export const isMessageFromEditor = (event: MessageEvent) => {
return Boolean(iframe?.contentWindow && event.source === iframe.contentWindow);
};

export const getPluginMessage = <T extends Record<string, unknown>>(event: MessageEvent): T | null => {
const pluginMessage = event.data?.pluginMessage;

return pluginMessage && typeof pluginMessage === "object" ? (pluginMessage as T) : null;
};

export const postMessageToParent = (message: Record<string, unknown>) => {
parent.postMessage({ pluginMessage: message }, "*");
};
29 changes: 29 additions & 0 deletions packages/figma/tests/frameMessaging.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, expect, test } from "bun:test";
import { getPluginMessage } from "../src/utils/frameMessaging";

describe("getPluginMessage", () => {
test("accepts Figma host messages when event.source is null", () => {
const pluginMessage = { type: "import-project", projectId: "connection-key" };
const event = {
data: { pluginMessage },
source: null,
} as unknown as MessageEvent;

expect(getPluginMessage(event)).toEqual(pluginMessage);
});

test("accepts Figma host messages without assuming the parent source", () => {
const pluginMessage = { type: "import-project-error", error: "Failed to fetch preset" };
const event = {
data: { pluginMessage },
source: {} as MessageEventSource,
} as MessageEvent;

expect(getPluginMessage(event)).toEqual(pluginMessage);
});

test("rejects raw editor and malformed messages", () => {
expect(getPluginMessage({ data: { type: "cf-figma-ready" } } as MessageEvent)).toBeNull();
expect(getPluginMessage({ data: { pluginMessage: "invalid" } } as MessageEvent)).toBeNull();
});
});
Loading