Skip to content

Commit c985a00

Browse files
committed
Improve ways to debug
1 parent 82c16af commit c985a00

13 files changed

Lines changed: 735 additions & 170 deletions

File tree

apps/plugin/package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"@figma/plugin-typings": "^1.108.0",
1414
"backend": "workspace:*",
1515
"clsx": "^2.1.1",
16+
"copy-to-clipboard": "^3.3.3",
1617
"lucide-react": "^0.479.0",
1718
"motion": "^12.4.10",
1819
"nanoid": "^5.1.3",
@@ -23,16 +24,16 @@
2324
"tailwindcss-animate": "^1.0.7"
2425
},
2526
"devDependencies": {
26-
"@types/node": "^22.13.9",
27+
"@types/node": "^22.13.10",
2728
"@types/react": "^19.0.10",
2829
"@types/react-dom": "^19.0.4",
2930
"@typescript-eslint/eslint-plugin": "^8.26.0",
3031
"@typescript-eslint/parser": "^8.26.0",
3132
"@vitejs/plugin-react": "^4.3.4",
3233
"@vitejs/plugin-react-swc": "^3.8.0",
33-
"autoprefixer": "^10.4.20",
34+
"autoprefixer": "^10.4.21",
3435
"concurrently": "^9.1.2",
35-
"esbuild": "^0.25.0",
36+
"esbuild": "^0.25.1",
3637
"eslint-config-custom": "workspace:*",
3738
"eslint-plugin-react-hooks": "^5.2.0",
3839
"eslint-plugin-react-refresh": "^0.4.19",
@@ -42,6 +43,6 @@
4243
"types": "workspace:*",
4344
"typescript": "^5.8.2",
4445
"vite": "^5.4.14",
45-
"vite-plugin-singlefile": "^2.1.0"
46+
"vite-plugin-singlefile": "^2.2.0"
4647
}
4748
}

apps/plugin/plugin-src/code.ts

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ import {
88
htmlMain,
99
postSettingsChanged,
1010
} from "backend";
11+
import { convertNodesToAltNodes } from "backend/src/altNodes/altConversion";
12+
import {
13+
disableParent,
14+
oldConvertNodesToAltNodes,
15+
setDisableParent,
16+
} from "backend/src/altNodes/oldAltConversion";
1117
import { nodesToJSON } from "backend/src/code";
1218
import { retrieveGenericSolidUIColors } from "backend/src/common/retrieveUI/retrieveColors";
1319
import { flutterCodeGenTextStyles } from "backend/src/flutter/flutterMain";
@@ -21,7 +27,7 @@ export const defaultPluginSettings: PluginSettings = {
2127
framework: "HTML",
2228
optimizeLayout: true,
2329
showLayerNames: false,
24-
inlineStyle: true,
30+
useOldPluginVersion2025: false,
2531
responsiveRoot: false,
2632
flutterGenerationMode: "snippet",
2733
swiftUIGenerationMode: "snippet",
@@ -150,7 +156,7 @@ const standardMode = async () => {
150156
safeRun(userPluginSettings);
151157
});
152158

153-
figma.ui.onmessage = (msg) => {
159+
figma.ui.onmessage = async (msg) => {
154160
console.log("[DEBUG] figma.ui.onmessage", msg);
155161

156162
if (msg.type === "pluginSettingWillChange") {
@@ -159,6 +165,56 @@ const standardMode = async () => {
159165
(userPluginSettings as any)[key] = value;
160166
figma.clientStorage.setAsync("userPluginSettings", userPluginSettings);
161167
safeRun(userPluginSettings);
168+
} else if (msg.type === "get-selection-json") {
169+
console.log("[DEBUG] get-selection-json message received");
170+
171+
const nodes = figma.currentPage.selection;
172+
if (nodes.length === 0) {
173+
figma.ui.postMessage({
174+
type: "selection-json",
175+
data: { message: "No nodes selected" },
176+
});
177+
return;
178+
}
179+
const result: {
180+
json?: SceneNode[];
181+
oldConversion?: any;
182+
newConversion?: any;
183+
} = {};
184+
185+
try {
186+
result.json = (await Promise.all(
187+
nodes.map(
188+
async (node) =>
189+
(
190+
(await node.exportAsync({
191+
format: "JSON_REST_V1",
192+
})) as any
193+
).document,
194+
),
195+
)) as SceneNode[];
196+
} catch (error) {
197+
console.error("Error exporting JSON:", error);
198+
}
199+
200+
try {
201+
result.newConversion = await convertNodesToAltNodes(
202+
result.json || [],
203+
null,
204+
);
205+
} catch (error) {
206+
console.error("Error in new conversion:", error);
207+
}
208+
209+
const nodeJson = result;
210+
211+
console.log("[DEBUG] Exported node JSON:", nodeJson);
212+
213+
// Send the JSON data back to the UI
214+
figma.ui.postMessage({
215+
type: "selection-json",
216+
data: nodeJson,
217+
});
162218
}
163219
};
164220
};

apps/plugin/ui-src/App.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
Warning,
1414
} from "types";
1515
import { postUISettingsChangingMessage } from "./messaging";
16+
import copy from "copy-to-clipboard";
1617

1718
interface AppState {
1819
code: string;
@@ -26,6 +27,7 @@ interface AppState {
2627
}
2728

2829
const emptyPreview = { size: { width: 0, height: 0 }, content: "" };
30+
2931
export default function App() {
3032
const [state, setState] = useState<AppState>({
3133
code: "",
@@ -100,6 +102,12 @@ export default function App() {
100102
isLoading: false,
101103
}));
102104
break;
105+
106+
case "selection-json":
107+
console.log("selection json");
108+
const json = event.data.pluginMessage.data;
109+
copy(JSON.stringify(json, null, 2));
110+
103111
default:
104112
break;
105113
}

packages/backend/src/altNodes/altConversion.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
assignChildren,
66
isTypeOrGroupOfTypes,
77
} from "./altNodeUtils";
8+
import { addWarning } from "../common/commonConversionWarnings";
89

910
// List of types that can be flattened into SVG
1011
const canBeFlattened = (node: SceneNode): boolean => {
@@ -62,9 +63,11 @@ export const convertNodeToAltNode =
6263
case "SLICE":
6364
return null;
6465
default:
65-
throw new Error(
66-
`Sorry, an unsupported node type was selected. Type:${node.type} id:${node.id}`,
67-
);
66+
addWarning(`${node.type} node is not supported`);
67+
return node;
68+
// throw new Error(
69+
// `Sorry, an unsupported node type was selected. Type:${node.type} id:${node.id}`,
70+
// );
6871
}
6972
};
7073

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import { StyledTextSegmentSubset, ParentNode, AltNode } from "types";
2+
import {
3+
assignParent,
4+
isNotEmpty,
5+
assignRectangleType,
6+
assignChildren,
7+
} from "./altNodeUtils";
8+
import { curry } from "../common/curry";
9+
10+
export const isTypeOrGroupOfTypes = curry(
11+
(matchTypes: NodeType[], node: SceneNode): boolean => {
12+
if (node.visible === false || matchTypes.includes(node.type)) return true;
13+
14+
if ("children" in node) {
15+
for (let i = 0; i < node.children.length; i++) {
16+
const childNode = node.children[i];
17+
const result = isTypeOrGroupOfTypes(matchTypes, childNode);
18+
if (result) continue;
19+
// child is false
20+
return false;
21+
}
22+
// all children are true
23+
return true;
24+
}
25+
26+
// not group or vector
27+
return false;
28+
},
29+
);
30+
31+
export let globalTextStyleSegments: Record<string, StyledTextSegmentSubset[]> =
32+
{};
33+
34+
// List of types that can be flattened into SVG
35+
const canBeFlattened = isTypeOrGroupOfTypes([
36+
"VECTOR",
37+
"STAR",
38+
"POLYGON",
39+
"BOOLEAN_OPERATION",
40+
]);
41+
42+
export const convertNodeToAltNode =
43+
(parent: ParentNode | null) =>
44+
(node: SceneNode): SceneNode => {
45+
const type = node.type;
46+
switch (type) {
47+
// Standard nodes
48+
case "RECTANGLE":
49+
case "ELLIPSE":
50+
case "LINE":
51+
case "STAR":
52+
case "POLYGON":
53+
case "VECTOR":
54+
case "BOOLEAN_OPERATION":
55+
return cloneNode(node, parent);
56+
57+
// Group nodes
58+
case "FRAME":
59+
case "INSTANCE":
60+
case "COMPONENT":
61+
case "COMPONENT_SET":
62+
// if the frame, instance etc. has no children, convert the frame to rectangle
63+
if (node.children.length === 0)
64+
return cloneAsRectangleNode(node, parent);
65+
// goto SECTION
66+
67+
case "GROUP":
68+
// if a Group is visible and has only one child, the Group should be ungrouped.
69+
if (type === "GROUP" && node.children.length === 1 && node.visible)
70+
return convertNodeToAltNode(parent)(node.children[0]);
71+
// goto SECTION
72+
73+
case "SECTION":
74+
const group = cloneNode(node, parent);
75+
const groupChildren = oldConvertNodesToAltNodes(node.children, group);
76+
return assignChildren(groupChildren, group);
77+
78+
// Text Nodes
79+
case "TEXT":
80+
globalTextStyleSegments[node.id] = extractStyledTextSegments(node);
81+
return cloneNode(node, parent);
82+
83+
// Unsupported Nodes
84+
case "SLICE":
85+
throw new Error(
86+
`Sorry, Slices are not supported. Type:${node.type} id:${node.id}`,
87+
);
88+
default:
89+
throw new Error(
90+
`Sorry, an unsupported node type was selected. Type:${node.type} id:${node.id}`,
91+
);
92+
}
93+
};
94+
95+
export const oldConvertNodesToAltNodes = (
96+
sceneNode: ReadonlyArray<SceneNode>,
97+
parent: ParentNode | null,
98+
): Array<SceneNode> =>
99+
sceneNode.map(convertNodeToAltNode(parent)).filter(isNotEmpty);
100+
101+
export const cloneNode = <T extends BaseNode>(
102+
node: T,
103+
parent: ParentNode | null,
104+
): T => {
105+
// Create the cloned object with the correct prototype
106+
const cloned = {} as T;
107+
// Create a new object with only the desired descriptors (excluding 'parent' and 'children')
108+
for (const prop in node) {
109+
if (
110+
prop !== "parent" &&
111+
prop !== "children" &&
112+
prop !== "horizontalPadding" &&
113+
prop !== "verticalPadding" &&
114+
prop !== "mainComponent" &&
115+
prop !== "masterComponent" &&
116+
prop !== "variantProperties" &&
117+
prop !== "get_annotations" &&
118+
prop !== "componentPropertyDefinitions" &&
119+
prop !== "exposedInstances" &&
120+
prop !== "instances" &&
121+
prop !== "componentProperties" &&
122+
prop !== "componenPropertyReferences" &&
123+
prop !== "constrainProportions"
124+
) {
125+
cloned[prop as keyof T] = node[prop as keyof T];
126+
}
127+
}
128+
129+
// Set parent explicitly in addition to using assignParent
130+
assignParent(parent, cloned);
131+
// if (parent) {
132+
// (cloned as any).parent = parent;
133+
// }
134+
135+
const altNode = {
136+
...cloned,
137+
parent: cloned.parent,
138+
originalNode: node,
139+
canBeFlattened: canBeFlattened(node),
140+
} as AltNode<T>;
141+
142+
if (globalTextStyleSegments[node.id]) {
143+
altNode.styledTextSegments = globalTextStyleSegments[node.id];
144+
}
145+
146+
console.log("altnode:", altNode.parent, cloned.parent);
147+
148+
return altNode;
149+
};
150+
151+
// auto convert Frame to Rectangle when Frame has no Children
152+
const cloneAsRectangleNode = <T extends BaseNode>(
153+
node: T,
154+
parent: ParentNode | null,
155+
): RectangleNode => {
156+
const clonedNode = cloneNode(node, parent);
157+
158+
assignRectangleType(clonedNode);
159+
160+
return clonedNode as unknown as RectangleNode;
161+
};
162+
163+
const extractStyledTextSegments = (node: TextNode) =>
164+
node.getStyledTextSegments([
165+
"fontName",
166+
"fills",
167+
"fontSize",
168+
"fontWeight",
169+
"hyperlink",
170+
"indentation",
171+
"letterSpacing",
172+
"lineHeight",
173+
"listOptions",
174+
"textCase",
175+
"textDecoration",
176+
"textStyleId",
177+
"fillStyleId",
178+
"openTypeFeatures",
179+
]);

0 commit comments

Comments
 (0)