Skip to content

Commit d16f7f0

Browse files
authored
Merge pull request #99 from Automattic/feat/figma-source-payload
Send Figma source payload to Studio
2 parents 898046d + 3c5df00 commit d16f7f0

7 files changed

Lines changed: 249 additions & 28 deletions

File tree

docs/figma-studio-runner.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
# Figma to WordPress Studio
22

3-
`plugins/figma-to-wordpress-studio` moves a Figma file into WordPress by posting a Studio import source to the local WordPress Studio app. Studio creates the site and imports the result with Static Site Importer and Blocks Engine.
3+
`plugins/figma-to-wordpress-studio` moves a Figma file into WordPress by posting a Figma source payload to the local WordPress Studio app. Studio creates the site and imports the result with Static Site Importer and Blocks Engine.
44

55
## Architecture
66

77
```text
88
Figma plugin UI
9-
-> Figma document scene data
10-
-> Studio import artifact JSON
9+
-> current-page scenegraph + selected-node intent
10+
-> Studio Figma source payload
1111
-> local WordPress Studio handoff endpoint
1212
-> WordPress Studio site
1313
-> Static Site Importer / Blocks Engine inside WordPress
1414
```
1515

16-
The TypeScript boundary stops at artifact construction and a local handoff request. WordPress, PHP, Static Site Importer, Blocks Engine, and Studio site orchestration remain Studio runtime concerns.
16+
The TypeScript boundary stops at source capture and a local handoff request. WordPress, PHP, Static Site Importer, Blocks Engine, and Studio site orchestration remain Studio runtime concerns.
1717

1818
## Figma Iframe Limits
1919

@@ -27,11 +27,11 @@ Practical constraints for this browser-based flow:
2727
- Clipboard access can require user activation.
2828
- Local file URLs can behave differently between desktop Figma, browser Figma, and development builds.
2929

30-
Because of those constraints, the integration shape is a local Studio handoff: the plugin posts an artifact JSON source to Studio's loopback endpoint. If Studio is not running, the plugin surfaces the connection error.
30+
Because of those constraints, the integration shape is a local Studio handoff: the plugin posts Figma source data to Studio's loopback endpoint. If Studio is not running, the plugin surfaces the connection error.
3131

3232
## Studio And PHP Role
3333

34-
WordPress Studio is the correct place to run WordPress and PHP plugin logic. It provides the WordPress runtime and accepts flexible `studio create --from <source>` inputs, including artifact JSON, `.fig`, file, directory, zip, and URL shapes.
34+
WordPress Studio is the correct place to run WordPress and PHP plugin logic. It provides the WordPress runtime and accepts flexible `studio create --from <source>` inputs, including Figma source, artifact JSON, `.fig`, file, directory, zip, and URL shapes.
3535

3636
Static Site Importer and Blocks Engine should run in that WordPress runtime because they are WordPress/PHP import systems. The Figma plugin should not reimplement them in TypeScript. The plugin's job is to hand off design data with enough metadata for Studio to route through Static Site Importer and clean up importer dependencies after a successful import.
3737

@@ -40,8 +40,9 @@ Static Site Importer and Blocks Engine should run in that WordPress runtime beca
4040
Implemented now:
4141

4242
- `GeneratedWebsiteArtifact` for static generated files from Figma.
43-
- `blocks-engine/php-transformer/site-artifact/v1` for the Studio CLI artifact handoff.
44-
- A Figma UI client that posts the artifact JSON to local Studio.
43+
- `wordpress-studio/figma-source/v1` for the primary Studio Figma handoff.
44+
- `blocks-engine/php-transformer/site-artifact/v1` retained as debug/diagnostic context.
45+
- A Figma UI client that posts the source payload to local Studio.
4546

4647
Not implemented yet:
4748

@@ -68,4 +69,4 @@ Figma development test:
6869

6970
## Next Integration Step
7071

71-
The next meaningful step is richer Studio-side progress/error reporting around CLI imports and post-import block/visual validation. That should remain in the Studio layer, not by porting PHP importer behavior into the Figma plugin.
72+
The next meaningful step is Studio-side routing for `wordpress-studio/figma-source/v1`, plus richer progress/error reporting around imports and post-import block/visual validation. That should remain in the Studio layer, not by porting PHP importer behavior into the Figma plugin.

plugins/figma-to-wordpress-studio/README.md

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
11
# Figma to WordPress Studio
22

3-
This plugin moves a Figma file into WordPress by sending a Studio import payload to the local WordPress Studio app.
3+
This plugin moves a Figma file into WordPress by sending a Figma source payload to the local WordPress Studio app.
44

5-
It does not port Static Site Importer, Blocks Engine, WordPress, PHP, or Studio internals to TypeScript. The TypeScript code extracts Figma scene data, prepares an artifact JSON source, and posts it to Studio's local handoff endpoint. Studio owns site creation, import through Static Site Importer, and cleanup after a successful import. If Studio is not running, the plugin surfaces the connection error.
5+
It does not port Static Site Importer, Blocks Engine, WordPress, PHP, or Studio internals to TypeScript. The TypeScript code extracts Figma scene data, captures the user's page/frame selection intent, and posts that source payload to Studio's local handoff endpoint. Studio owns site creation, Figma-to-WordPress transform/import through Static Site Importer and Blocks Engine, and cleanup after a successful import. If Studio is not running, the plugin surfaces the connection error.
66

77
## Flow
88

99
```text
1010
Figma plugin controller + UI
11-
-> whole Figma document scene data
12-
-> Studio import artifact JSON
11+
-> current-page scenegraph + selected-node intent
12+
-> Studio Figma source payload
1313
-> local WordPress Studio handoff endpoint
1414
-> WordPress Studio site
1515
-> Static Site Importer imports through Blocks Engine
1616
```
1717

1818
## Boundaries
1919

20-
- Implemented: a Figma Desktop-loadable plugin shell with whole-file export.
21-
- Implemented: a reusable scene-to-HTML/CSS artifact generator with diagnostics and tests for the current local handoff.
20+
- Implemented: a Figma Desktop-loadable plugin shell with current-page scenegraph and selected-frame intent export.
21+
- Implemented: a reusable scene-to-HTML/CSS artifact generator retained as debug/diagnostic context.
2222
- Implemented: a Studio local handoff request.
2323
- Not implemented: running Static Site Importer from TypeScript.
2424
- Not implemented: automated visual parity/block validation after import.
@@ -29,7 +29,7 @@ Figma plugin controller + UI
2929
- `src/code.ts` is the Figma plugin main thread entry.
3030
- `src/ui.ts` is the Figma UI-side handoff logic.
3131
- `src/index.ts` contains the reusable Figma-scene-to-website-artifact generator.
32-
- `src/payload.ts` contains the reusable TypeScript interfaces for the Studio CLI artifact handoff.
32+
- `src/payload.ts` contains the reusable TypeScript interfaces for the Studio Figma source handoff and debug artifact bundle.
3333
- `src/ui.html` is the Figma UI shell bundled into `dist/ui.html`.
3434

3535
## Local Test
@@ -50,6 +50,58 @@ npm test --prefix plugins/figma-to-wordpress-studio
5050

5151
## Studio Handoff Boundary
5252

53-
Figma plugin UIs run in a constrained iframe-like environment and cannot spawn local shell commands. The supported boundary is a local Studio handoff: the plugin posts the import artifact to Studio's loopback endpoint. Studio accepts the artifact source, creates the site, runs Static Site Importer, and removes SSI after a successful import.
53+
Figma plugin UIs run in a constrained iframe-like environment and cannot spawn local shell commands. The supported boundary is a local Studio handoff: the plugin posts a Figma source payload to Studio's loopback endpoint. Studio accepts the source, creates the site, runs the Figma import route through Static Site Importer and Blocks Engine, and removes importer dependencies after a successful import.
54+
55+
The `Open in WordPress Studio` request body is source-first:
56+
57+
```json
58+
{
59+
"source": {
60+
"schema": "wordpress-studio/figma-source/v1",
61+
"source": {
62+
"type": "figma",
63+
"metadata": {
64+
"provider": "figma",
65+
"plugin": "figma-to-wordpress-studio",
66+
"fileKey": "...",
67+
"fileName": "Marketing site",
68+
"editorType": "figma",
69+
"currentPage": { "id": "0:1", "name": "Landing" }
70+
},
71+
"exportedAt": "2026-01-01T00:00:00.000Z"
72+
},
73+
"intent": {
74+
"scope": "selected-nodes",
75+
"pageId": "0:1",
76+
"pageName": "Landing",
77+
"selectedNodeIds": ["1:2"],
78+
"rootNodeIds": ["1:2"]
79+
},
80+
"scenegraph": {
81+
"currentPage": {},
82+
"selectedNodes": []
83+
},
84+
"assets": [],
85+
"transform": {
86+
"target": "wordpress",
87+
"route": "static-site-importer/figma",
88+
"options": {
89+
"selectionScope": "selected-nodes",
90+
"pageId": "0:1",
91+
"selectedNodeIds": ["1:2"],
92+
"preserveSourceScenegraph": true,
93+
"importAssets": true
94+
}
95+
},
96+
"debug": {
97+
"generatedArtifact": {},
98+
"diagnostics": []
99+
}
100+
},
101+
"siteName": "Marketing site"
102+
}
103+
```
104+
105+
`debug.generatedArtifact` is diagnostic context only. Studio should route from `source`, `scenegraph`, and `transform`, not from a pre-rendered website artifact bundle.
54106

55107
See [`../../docs/figma-studio-runner.md`](../../docs/figma-studio-runner.md) for the integration notes.

plugins/figma-to-wordpress-studio/src/code.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -229,27 +229,49 @@ async function exportAsset(node: SceneNode, format: AssetExportFormat, imageHash
229229
}
230230

231231
async function getDocument(): Promise<NormalizedDocument> {
232-
if ("loadAllPagesAsync" in figma) {
233-
await figma.loadAllPagesAsync();
232+
if ("loadAsync" in figma.currentPage) {
233+
await figma.currentPage.loadAsync();
234234
}
235235

236236
const context: NormalizeContext = { assets: new Map() };
237-
const pages = await Promise.all(figma.root.children.map((page) => normalizeNode(page, context, "PAGE")));
237+
const currentPage = await normalizeNode(figma.currentPage, context, "PAGE");
238+
const selectedNodes = await Promise.all(figma.currentPage.selection.map((node) => normalizeNode(node, context)));
238239
const root: NormalizedSceneNode = {
239240
id: figma.root.id,
240241
name: figma.root.name || "Figma document",
241242
type: "DOCUMENT",
242243
visible: true,
243-
children: pages,
244+
children: [currentPage],
244245
};
245246
applyDerivedBounds(root);
247+
const selectedNodeIds = selectedNodes.map((node) => node.id);
246248

247249
return {
248250
id: figma.root.id,
249251
name: figma.root.name || "Figma document",
250252
type: "DOCUMENT",
251253
exportedAt: new Date().toISOString(),
252254
root,
255+
source: {
256+
provider: "figma",
257+
plugin: "figma-to-wordpress-studio",
258+
fileKey: figma.fileKey,
259+
fileName: figma.root.name || "Figma document",
260+
editorType: figma.editorType,
261+
currentPage: {
262+
id: figma.currentPage.id,
263+
name: figma.currentPage.name,
264+
},
265+
},
266+
selectionIntent: {
267+
scope: selectedNodes.length ? "selected-nodes" : "current-page",
268+
pageId: figma.currentPage.id,
269+
pageName: figma.currentPage.name,
270+
selectedNodeIds,
271+
rootNodeIds: selectedNodes.length ? selectedNodeIds : [figma.currentPage.id],
272+
},
273+
currentPage,
274+
selectedNodes,
253275
assets: Array.from(context.assets.values()),
254276
};
255277
}

plugins/figma-to-wordpress-studio/src/payload.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { WebsiteArtifact } from "./index";
2-
import type { NormalizedSelection } from "./types";
2+
import type { GeneratedArtifact, NormalizedSelection } from "./types";
33

44
export type WebsiteArtifactFileRole = "html" | "css" | "js" | "asset" | "metadata";
55

@@ -19,6 +19,70 @@ export interface WebsiteArtifactBundle {
1919
import_source: "figma-to-wordpress-studio";
2020
}
2121

22+
export interface FigmaSourcePayload {
23+
schema: "wordpress-studio/figma-source/v1";
24+
source: {
25+
type: "figma";
26+
metadata: NormalizedSelection["source"];
27+
exportedAt: string;
28+
};
29+
intent: NormalizedSelection["selectionIntent"];
30+
scenegraph: {
31+
currentPage: NormalizedSelection["currentPage"];
32+
selectedNodes: NormalizedSelection["selectedNodes"];
33+
};
34+
assets: NormalizedSelection["assets"];
35+
transform: {
36+
target: "wordpress";
37+
route: "static-site-importer/figma";
38+
options: {
39+
selectionScope: NormalizedSelection["selectionIntent"]["scope"];
40+
pageId: string;
41+
selectedNodeIds: string[];
42+
preserveSourceScenegraph: true;
43+
importAssets: true;
44+
};
45+
};
46+
debug?: {
47+
generatedArtifact?: GeneratedArtifact["studioImportPayload"];
48+
diagnostics: GeneratedArtifact["diagnostics"];
49+
metadata?: GeneratedArtifact["metadata"];
50+
};
51+
}
52+
53+
export function toFigmaSourcePayload(selection: NormalizedSelection, artifact?: GeneratedArtifact | null): FigmaSourcePayload {
54+
return {
55+
schema: "wordpress-studio/figma-source/v1",
56+
source: {
57+
type: "figma",
58+
metadata: selection.source,
59+
exportedAt: selection.exportedAt,
60+
},
61+
intent: selection.selectionIntent,
62+
scenegraph: {
63+
currentPage: selection.currentPage,
64+
selectedNodes: selection.selectedNodes,
65+
},
66+
assets: selection.assets,
67+
transform: {
68+
target: "wordpress",
69+
route: "static-site-importer/figma",
70+
options: {
71+
selectionScope: selection.selectionIntent.scope,
72+
pageId: selection.selectionIntent.pageId,
73+
selectedNodeIds: selection.selectionIntent.selectedNodeIds,
74+
preserveSourceScenegraph: true,
75+
importAssets: true,
76+
},
77+
},
78+
debug: artifact ? {
79+
generatedArtifact: artifact.studioImportPayload,
80+
diagnostics: artifact.diagnostics,
81+
metadata: artifact.metadata,
82+
} : undefined,
83+
};
84+
}
85+
2286
export function toWebsiteArtifactBundle(artifact: WebsiteArtifact, _selection: NormalizedSelection): WebsiteArtifactBundle {
2387
return {
2488
schema: "blocks-engine/php-transformer/site-artifact/v1",

plugins/figma-to-wordpress-studio/src/types.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,35 @@ export type NormalizedSelection = {
7070
type: string;
7171
exportedAt: string;
7272
root: NormalizedSceneNode;
73+
source: FigmaSourceMetadata;
74+
selectionIntent: FigmaSelectionIntent;
75+
currentPage: NormalizedSceneNode;
76+
selectedNodes: NormalizedSceneNode[];
7377
assets: NormalizedAsset[];
7478
};
7579

7680
export type NormalizedDocument = NormalizedSelection;
7781

82+
export type FigmaSourceMetadata = {
83+
provider: "figma";
84+
plugin: "figma-to-wordpress-studio";
85+
fileKey?: string;
86+
fileName: string;
87+
editorType: string;
88+
currentPage: {
89+
id: string;
90+
name: string;
91+
};
92+
};
93+
94+
export type FigmaSelectionIntent = {
95+
scope: "selected-nodes" | "current-page";
96+
pageId: string;
97+
pageName: string;
98+
selectedNodeIds: string[];
99+
rootNodeIds: string[];
100+
};
101+
78102
export type GeneratedArtifact = {
79103
title: string;
80104
html: string;

plugins/figma-to-wordpress-studio/src/ui.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { GeneratedArtifact, NormalizedSelection, PluginToUiMessage, UiToPluginMessage } from "./types";
2+
import { toFigmaSourcePayload } from "./payload";
23

34
let currentArtifact: GeneratedArtifact | null = null;
45
let currentSelection: NormalizedSelection | null = null;
@@ -37,14 +38,14 @@ function artifactBundleSummary(artifact: GeneratedArtifact | null) {
3738
}
3839

3940
function updateActions() {
40-
const disabled = !currentArtifact;
41+
const disabled = !currentSelection;
4142

4243
if (studioButton) studioButton.disabled = disabled;
4344
}
4445

4546
async function openInStudio() {
46-
if (!currentArtifact) {
47-
log("Studio import requested before artifact was ready.");
47+
if (!currentSelection) {
48+
log("Studio import requested before Figma source data was ready.");
4849
return;
4950
}
5051

@@ -54,14 +55,15 @@ async function openInStudio() {
5455
setStatus("Sending this Figma file to WordPress Studio...");
5556

5657
try {
58+
const sourcePayload = toFigmaSourcePayload(currentSelection, currentArtifact);
5759
const response = await fetch(studioHandoffEndpoint, {
5860
method: "POST",
5961
headers: {
6062
"Content-Type": "application/json",
6163
},
6264
body: JSON.stringify({
63-
artifact: currentArtifact.studioImportPayload,
64-
siteName: currentArtifact.title,
65+
source: sourcePayload,
66+
siteName: currentSelection.name,
6567
}),
6668
});
6769
const data = await response.json().catch(() => null) as { success?: boolean; error?: string } | null;
@@ -74,7 +76,11 @@ async function openInStudio() {
7476
sendToPlugin({ type: "notify", message: "Sent to WordPress Studio." });
7577
log("Studio import handoff accepted.", {
7678
endpoint: studioHandoffEndpoint,
77-
...artifactBundleSummary(currentArtifact),
79+
schema: sourcePayload.schema,
80+
selectionScope: sourcePayload.intent.scope,
81+
pageId: sourcePayload.intent.pageId,
82+
selectedNodes: sourcePayload.intent.selectedNodeIds.length,
83+
debugArtifact: artifactBundleSummary(currentArtifact),
7884
});
7985
} catch (error) {
8086
console.error("[Figma to WordPress Studio] Studio handoff failed.", error);

0 commit comments

Comments
 (0)