@dannote/figma-use 0.2.0 → 0.2.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/CHANGELOG.md +26 -1
- package/README.md +21 -2
- package/SKILL.md +5 -2
- package/dist/cli/index.js +152 -5
- package/package.json +1 -1
- package/packages/plugin/dist/main.js +26 -6
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.1] - 2025-01-17
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`profile` command** — performance profiling via Chrome DevTools Protocol
|
|
15
|
+
- Profile any command: `figma-use profile "get components --limit 20"`
|
|
16
|
+
- Shows time breakdown (Figma WASM vs JS vs GC)
|
|
17
|
+
- Lists top functions by CPU time
|
|
18
|
+
- Requires Figma with `--remote-debugging-port=9222`
|
|
19
|
+
- `get components --name` — filter components by name
|
|
20
|
+
- `get components --limit` — limit results (default 50)
|
|
21
|
+
- `get components --page` — filter by page
|
|
22
|
+
- `find --type` now works without `--name`
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- `get components` uses early-exit recursion for better performance on large files
|
|
27
|
+
- `node tree --depth` now affects node count check (won't block with high depth limit)
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- Variant components no longer crash when accessing `componentPropertyDefinitions`
|
|
32
|
+
- 86 tests passing
|
|
33
|
+
|
|
10
34
|
## [0.2.0] - 2025-01-17
|
|
11
35
|
|
|
12
36
|
### Added
|
|
@@ -113,7 +137,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
113
137
|
- Export commands: PNG/SVG/PDF export, screenshot
|
|
114
138
|
- Inline styling: `--fill`, `--stroke`, `--radius` etc. on create commands
|
|
115
139
|
|
|
116
|
-
[unreleased]: https://github.com/dannote/figma-use/compare/v0.2.
|
|
140
|
+
[unreleased]: https://github.com/dannote/figma-use/compare/v0.2.1...HEAD
|
|
141
|
+
[0.2.1]: https://github.com/dannote/figma-use/compare/v0.2.0...v0.2.1
|
|
117
142
|
[0.2.0]: https://github.com/dannote/figma-use/compare/v0.1.5...v0.2.0
|
|
118
143
|
[0.1.5]: https://github.com/dannote/figma-use/compare/v0.1.4...v0.1.5
|
|
119
144
|
[0.1.4]: https://github.com/dannote/figma-use/compare/v0.1.3...v0.1.4
|
package/README.md
CHANGED
|
@@ -93,8 +93,9 @@ All create commands support inline styling — no need for separate `set` calls.
|
|
|
93
93
|
```bash
|
|
94
94
|
figma-use node get <id> # Get node properties
|
|
95
95
|
figma-use node tree [id] # Get formatted tree (default: current page)
|
|
96
|
-
figma-use node tree --depth 2 # Limit tree depth
|
|
96
|
+
figma-use node tree --depth 2 # Limit tree depth (also limits node count check)
|
|
97
97
|
figma-use node tree -i # Only interactive elements
|
|
98
|
+
figma-use node tree --force # Skip 500 node limit
|
|
98
99
|
figma-use node children <id> # Get child nodes
|
|
99
100
|
figma-use node delete <id> # Delete node
|
|
100
101
|
figma-use node clone <id> # Clone node
|
|
@@ -225,8 +226,10 @@ figma-use viewport zoom-to-fit <ids...>
|
|
|
225
226
|
```bash
|
|
226
227
|
figma-use find --name "Button"
|
|
227
228
|
figma-use find --name "Icon" --type FRAME
|
|
229
|
+
figma-use find --type INSTANCE # Find all instances on current page
|
|
228
230
|
figma-use get pages
|
|
229
|
-
figma-use get components
|
|
231
|
+
figma-use get components --name "Button" # Filter by name
|
|
232
|
+
figma-use get components --limit 50 # Limit results (default: 100)
|
|
230
233
|
figma-use get styles
|
|
231
234
|
```
|
|
232
235
|
|
|
@@ -255,6 +258,22 @@ figma-use eval "await figma.loadFontAsync({family: 'Inter', style: 'Bold'})"
|
|
|
255
258
|
figma-use import --svg "<svg>...</svg>" --x 0 --y 0
|
|
256
259
|
```
|
|
257
260
|
|
|
261
|
+
### Performance Profiling
|
|
262
|
+
|
|
263
|
+
Profile any command using Chrome DevTools Protocol:
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
# Start Figma with debug port
|
|
267
|
+
/Applications/Figma.app/Contents/MacOS/Figma --remote-debugging-port=9222
|
|
268
|
+
|
|
269
|
+
# Profile a command
|
|
270
|
+
figma-use profile "get components --limit 20"
|
|
271
|
+
figma-use profile "node tree --depth 2"
|
|
272
|
+
figma-use profile "find --type INSTANCE"
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Output shows time breakdown (Figma WASM vs JS vs GC) and top functions by CPU time.
|
|
276
|
+
|
|
258
277
|
## Output Format
|
|
259
278
|
|
|
260
279
|
Human-readable by default:
|
package/SKILL.md
CHANGED
|
@@ -52,8 +52,9 @@ figma-use create text --x 0 --y 0 --text "Hello" \
|
|
|
52
52
|
```bash
|
|
53
53
|
figma-use node get <id> # Get properties
|
|
54
54
|
figma-use node tree [id] # Get formatted tree (see structure at a glance)
|
|
55
|
-
figma-use node tree --depth 2 # Limit tree depth
|
|
55
|
+
figma-use node tree --depth 2 # Limit tree depth (also limits node count)
|
|
56
56
|
figma-use node tree -i # Only interactive elements
|
|
57
|
+
figma-use node tree --force # Skip 500 node limit
|
|
57
58
|
figma-use node children <id> # List children
|
|
58
59
|
figma-use node move <id> --x 100 --y 200
|
|
59
60
|
figma-use node resize <id> --width 300 --height 200
|
|
@@ -133,7 +134,9 @@ figma-use viewport zoom-to-fit <ids...>
|
|
|
133
134
|
```bash
|
|
134
135
|
figma-use find --name "Button"
|
|
135
136
|
figma-use find --type FRAME
|
|
136
|
-
figma-use
|
|
137
|
+
figma-use find --type INSTANCE # All instances on page
|
|
138
|
+
figma-use get components --name "Button" # Filter components by name
|
|
139
|
+
figma-use get components --limit 50 # Limit results
|
|
137
140
|
```
|
|
138
141
|
|
|
139
142
|
### Boolean & Group
|
package/dist/cli/index.js
CHANGED
|
@@ -3295,6 +3295,7 @@ __export(exports_commands, {
|
|
|
3295
3295
|
set: () => set_default,
|
|
3296
3296
|
selection: () => selection_default,
|
|
3297
3297
|
proxy: () => proxy_default,
|
|
3298
|
+
profile: () => profile_default,
|
|
3298
3299
|
plugin: () => plugin_default,
|
|
3299
3300
|
page: () => page_default2,
|
|
3300
3301
|
node: () => node_default,
|
|
@@ -4566,6 +4567,142 @@ var plugin_default = defineCommand({
|
|
|
4566
4567
|
}
|
|
4567
4568
|
}
|
|
4568
4569
|
});
|
|
4570
|
+
// packages/cli/src/commands/profile.ts
|
|
4571
|
+
init_format();
|
|
4572
|
+
import { spawn as spawn2 } from "child_process";
|
|
4573
|
+
var profile_default = defineCommand({
|
|
4574
|
+
meta: { description: "Profile any figma-use command via Chrome DevTools Protocol" },
|
|
4575
|
+
args: {
|
|
4576
|
+
command: { type: "positional", description: 'Command to profile (e.g., "get components --limit 10")', required: true },
|
|
4577
|
+
port: { type: "string", description: "Chrome DevTools port", default: "9222" },
|
|
4578
|
+
top: { type: "string", description: "Number of top functions to show", default: "20" }
|
|
4579
|
+
},
|
|
4580
|
+
async run({ args }) {
|
|
4581
|
+
const port = Number(args.port);
|
|
4582
|
+
const topN = Number(args.top);
|
|
4583
|
+
const targets = await fetch(`http://localhost:${port}/json`).then((r5) => r5.json()).catch(() => null);
|
|
4584
|
+
if (!targets) {
|
|
4585
|
+
console.error(fail(`Cannot connect to DevTools on port ${port}`));
|
|
4586
|
+
console.error(`Start Figma with: /Applications/Figma.app/Contents/MacOS/Figma --remote-debugging-port=${port}`);
|
|
4587
|
+
process.exit(1);
|
|
4588
|
+
}
|
|
4589
|
+
const figmaTab = targets.find((t3) => t3.title?.includes("Figma") && t3.type === "page" && t3.url?.includes("figma.com/design"));
|
|
4590
|
+
if (!figmaTab) {
|
|
4591
|
+
console.error(fail("No Figma design tab found. Open a Figma file first."));
|
|
4592
|
+
process.exit(1);
|
|
4593
|
+
}
|
|
4594
|
+
console.log(`Profiling: figma-use ${args.command}`);
|
|
4595
|
+
console.log(`Target: ${figmaTab.title}
|
|
4596
|
+
`);
|
|
4597
|
+
const ws = new WebSocket(figmaTab.webSocketDebuggerUrl);
|
|
4598
|
+
let msgId = 1;
|
|
4599
|
+
const pending = new Map;
|
|
4600
|
+
ws.onmessage = (event) => {
|
|
4601
|
+
const data = JSON.parse(event.data);
|
|
4602
|
+
const resolve3 = pending.get(data.id);
|
|
4603
|
+
if (resolve3) {
|
|
4604
|
+
pending.delete(data.id);
|
|
4605
|
+
resolve3(data);
|
|
4606
|
+
}
|
|
4607
|
+
};
|
|
4608
|
+
const send = (method, params = {}) => {
|
|
4609
|
+
const id = msgId++;
|
|
4610
|
+
return new Promise((resolve3) => {
|
|
4611
|
+
pending.set(id, resolve3);
|
|
4612
|
+
ws.send(JSON.stringify({ id, method, params }));
|
|
4613
|
+
});
|
|
4614
|
+
};
|
|
4615
|
+
await new Promise((r5) => {
|
|
4616
|
+
ws.onopen = () => r5();
|
|
4617
|
+
});
|
|
4618
|
+
await send("Profiler.enable");
|
|
4619
|
+
await send("Profiler.start");
|
|
4620
|
+
const startTime = Date.now();
|
|
4621
|
+
const cmdResult = await new Promise((resolve3) => {
|
|
4622
|
+
const proc = spawn2(process.argv[0], [process.argv[1], ...args.command.split(" "), "--json"], {
|
|
4623
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4624
|
+
});
|
|
4625
|
+
let stdout3 = "";
|
|
4626
|
+
let stderr = "";
|
|
4627
|
+
proc.stdout.on("data", (d3) => stdout3 += d3);
|
|
4628
|
+
proc.stderr.on("data", (d3) => stderr += d3);
|
|
4629
|
+
proc.on("close", (code) => resolve3({ stdout: stdout3, stderr, code: code || 0 }));
|
|
4630
|
+
});
|
|
4631
|
+
const duration = Date.now() - startTime;
|
|
4632
|
+
const profile = await send("Profiler.stop");
|
|
4633
|
+
ws.close();
|
|
4634
|
+
let resultItems = "N/A";
|
|
4635
|
+
let resultError = "";
|
|
4636
|
+
try {
|
|
4637
|
+
const parsed = JSON.parse(cmdResult.stdout);
|
|
4638
|
+
if (Array.isArray(parsed))
|
|
4639
|
+
resultItems = String(parsed.length);
|
|
4640
|
+
else if (parsed.error)
|
|
4641
|
+
resultError = parsed.error;
|
|
4642
|
+
} catch {
|
|
4643
|
+
if (cmdResult.stderr)
|
|
4644
|
+
resultError = cmdResult.stderr.trim();
|
|
4645
|
+
}
|
|
4646
|
+
console.log(`Duration: ${duration}ms`);
|
|
4647
|
+
console.log(`Result: ${resultItems} items`);
|
|
4648
|
+
if (resultError)
|
|
4649
|
+
console.log(`Error: ${resultError}`);
|
|
4650
|
+
if (cmdResult.code !== 0)
|
|
4651
|
+
console.log(`Exit code: ${cmdResult.code}`);
|
|
4652
|
+
console.log();
|
|
4653
|
+
const nodes = profile.result.profile.nodes;
|
|
4654
|
+
const samples = profile.result.profile.samples || [];
|
|
4655
|
+
const timeDeltas = profile.result.profile.timeDeltas || [];
|
|
4656
|
+
const counts = {};
|
|
4657
|
+
samples.forEach((nodeId, idx) => {
|
|
4658
|
+
counts[nodeId] = (counts[nodeId] || 0) + (timeDeltas[idx] || 1);
|
|
4659
|
+
});
|
|
4660
|
+
let wasmTime = 0, gcTime = 0, idleTime = 0, jsTime = 0;
|
|
4661
|
+
for (const [nodeId, time] of Object.entries(counts)) {
|
|
4662
|
+
const node = nodes.find((n3) => n3.id === parseInt(nodeId));
|
|
4663
|
+
const name = node?.callFrame?.functionName || "";
|
|
4664
|
+
const url = node?.callFrame?.url || "";
|
|
4665
|
+
if (name === "(garbage collector)")
|
|
4666
|
+
gcTime += time;
|
|
4667
|
+
else if (name === "(idle)")
|
|
4668
|
+
idleTime += time;
|
|
4669
|
+
else if (url.includes(".wasm"))
|
|
4670
|
+
wasmTime += time;
|
|
4671
|
+
else
|
|
4672
|
+
jsTime += time;
|
|
4673
|
+
}
|
|
4674
|
+
const total = wasmTime + gcTime + jsTime;
|
|
4675
|
+
if (total > 0) {
|
|
4676
|
+
console.log("Time breakdown:");
|
|
4677
|
+
console.log(` Figma WASM: ${(wasmTime / 1000).toFixed(0).padStart(5)}ms (${(wasmTime / total * 100).toFixed(0)}%)`);
|
|
4678
|
+
console.log(` GC: ${(gcTime / 1000).toFixed(0).padStart(5)}ms (${(gcTime / total * 100).toFixed(0)}%)`);
|
|
4679
|
+
console.log(` JS: ${(jsTime / 1000).toFixed(0).padStart(5)}ms (${(jsTime / total * 100).toFixed(0)}%)`);
|
|
4680
|
+
console.log(` Idle: ${(idleTime / 1000).toFixed(0).padStart(5)}ms`);
|
|
4681
|
+
console.log();
|
|
4682
|
+
}
|
|
4683
|
+
const funcs = Object.entries(counts).map(([nodeId, time]) => {
|
|
4684
|
+
const node = nodes.find((n3) => n3.id === parseInt(nodeId));
|
|
4685
|
+
const cf = node?.callFrame || {};
|
|
4686
|
+
return {
|
|
4687
|
+
name: cf.functionName || "(anonymous)",
|
|
4688
|
+
url: cf.url || "",
|
|
4689
|
+
line: cf.lineNumber || 0,
|
|
4690
|
+
time
|
|
4691
|
+
};
|
|
4692
|
+
}).filter((f5) => f5.time > 500 && f5.name !== "(idle)").sort((a3, b3) => b3.time - a3.time).slice(0, topN);
|
|
4693
|
+
if (funcs.length > 0) {
|
|
4694
|
+
console.log(`Top ${Math.min(topN, funcs.length)} functions by CPU time:`);
|
|
4695
|
+
console.log("\u2500".repeat(75));
|
|
4696
|
+
for (const f5 of funcs) {
|
|
4697
|
+
const shortUrl = f5.url.split("/").pop()?.slice(0, 25) || "";
|
|
4698
|
+
const loc = shortUrl ? `${shortUrl}:${f5.line}` : "";
|
|
4699
|
+
console.log(`${(f5.time / 1000).toFixed(1).padStart(7)}ms ${f5.name.slice(0, 40).padEnd(40)} ${loc}`);
|
|
4700
|
+
}
|
|
4701
|
+
console.log();
|
|
4702
|
+
}
|
|
4703
|
+
console.log(ok("Profile complete"));
|
|
4704
|
+
}
|
|
4705
|
+
});
|
|
4569
4706
|
// packages/cli/src/commands/eval.ts
|
|
4570
4707
|
var eval_default = defineCommand({
|
|
4571
4708
|
meta: { description: "Execute JavaScript in Figma plugin context" },
|
|
@@ -4724,8 +4861,13 @@ var tree_default = defineCommand({
|
|
|
4724
4861
|
try {
|
|
4725
4862
|
const id = args.id || (await sendCommand("get-current-page", {})).id;
|
|
4726
4863
|
const result = await sendCommand("get-node-tree", { id });
|
|
4727
|
-
const
|
|
4728
|
-
const
|
|
4864
|
+
const maxDepth = Number(args.depth);
|
|
4865
|
+
const countNodes = (n3, depth) => {
|
|
4866
|
+
if (maxDepth !== -1 && depth > maxDepth)
|
|
4867
|
+
return 0;
|
|
4868
|
+
return 1 + (n3.children?.reduce((sum, c5) => sum + countNodes(c5, depth + 1), 0) || 0);
|
|
4869
|
+
};
|
|
4870
|
+
const total = countNodes(result, 0);
|
|
4729
4871
|
if (!args.force && total > MAX_NODES) {
|
|
4730
4872
|
console.error(fail(`Tree has ${total} nodes (limit: ${MAX_NODES}). Use --depth to limit or --force to override.`));
|
|
4731
4873
|
process.exit(1);
|
|
@@ -4736,7 +4878,7 @@ var tree_default = defineCommand({
|
|
|
4736
4878
|
}
|
|
4737
4879
|
const options = {
|
|
4738
4880
|
showHidden: args.hidden || false,
|
|
4739
|
-
maxDepth
|
|
4881
|
+
maxDepth,
|
|
4740
4882
|
interactive: args.interactive || false
|
|
4741
4883
|
};
|
|
4742
4884
|
const lines = formatTreeNode(result, 0, 0, options);
|
|
@@ -5721,13 +5863,18 @@ var pages_default = defineCommand({
|
|
|
5721
5863
|
|
|
5722
5864
|
// packages/cli/src/commands/get/components.ts
|
|
5723
5865
|
var components_default = defineCommand({
|
|
5724
|
-
meta: { description: "Get
|
|
5866
|
+
meta: { description: "Get components" },
|
|
5725
5867
|
args: {
|
|
5868
|
+
name: { type: "string", description: "Filter by name (case-insensitive)" },
|
|
5869
|
+
limit: { type: "string", description: "Max results", default: "100" },
|
|
5726
5870
|
json: { type: "boolean", description: "Output as JSON" }
|
|
5727
5871
|
},
|
|
5728
5872
|
async run({ args }) {
|
|
5729
5873
|
try {
|
|
5730
|
-
const result = await sendCommand("get-all-components"
|
|
5874
|
+
const result = await sendCommand("get-all-components", {
|
|
5875
|
+
name: args.name,
|
|
5876
|
+
limit: Number(args.limit)
|
|
5877
|
+
});
|
|
5731
5878
|
printResult(result, args.json);
|
|
5732
5879
|
} catch (e3) {
|
|
5733
5880
|
handleError(e3);
|
package/package.json
CHANGED
|
@@ -80,13 +80,30 @@
|
|
|
80
80
|
return serializeTree(node);
|
|
81
81
|
}
|
|
82
82
|
case "get-all-components": {
|
|
83
|
+
const { name, limit = 50, page } = args || {};
|
|
83
84
|
const components = [];
|
|
84
|
-
|
|
85
|
+
const nameLower = name == null ? void 0 : name.toLowerCase();
|
|
86
|
+
const searchNode = (node) => {
|
|
87
|
+
if (components.length >= limit) return false;
|
|
85
88
|
if (node.type === "COMPONENT" || node.type === "COMPONENT_SET") {
|
|
86
|
-
|
|
89
|
+
if (!nameLower || node.name.toLowerCase().includes(nameLower)) {
|
|
90
|
+
components.push(serializeNode(node));
|
|
91
|
+
}
|
|
87
92
|
}
|
|
88
|
-
|
|
89
|
-
|
|
93
|
+
if ("children" in node) {
|
|
94
|
+
for (const child of node.children) {
|
|
95
|
+
if (!searchNode(child)) return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return components.length < limit;
|
|
99
|
+
};
|
|
100
|
+
const pages = page ? figma.root.children.filter((p) => p.id === page || p.name === page) : figma.root.children;
|
|
101
|
+
for (const pageNode of pages) {
|
|
102
|
+
if (components.length >= limit) break;
|
|
103
|
+
for (const child of pageNode.children) {
|
|
104
|
+
if (!searchNode(child)) break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
90
107
|
return components;
|
|
91
108
|
}
|
|
92
109
|
case "get-pages":
|
|
@@ -518,7 +535,7 @@
|
|
|
518
535
|
const { name, type, exact } = args;
|
|
519
536
|
const results = [];
|
|
520
537
|
figma.currentPage.findAll((n) => {
|
|
521
|
-
const nameMatch = exact ? n.name === name : n.name.toLowerCase().includes(name.toLowerCase());
|
|
538
|
+
const nameMatch = !name || (exact ? n.name === name : n.name.toLowerCase().includes(name.toLowerCase()));
|
|
522
539
|
const typeMatch = !type || n.type === type;
|
|
523
540
|
if (nameMatch && typeMatch) results.push(serializeNode(n));
|
|
524
541
|
return false;
|
|
@@ -1020,7 +1037,10 @@
|
|
|
1020
1037
|
base.strokeWeight = node.strokeWeight;
|
|
1021
1038
|
}
|
|
1022
1039
|
if ("componentPropertyDefinitions" in node) {
|
|
1023
|
-
|
|
1040
|
+
try {
|
|
1041
|
+
base.componentPropertyDefinitions = node.componentPropertyDefinitions;
|
|
1042
|
+
} catch (e) {
|
|
1043
|
+
}
|
|
1024
1044
|
}
|
|
1025
1045
|
if ("componentProperties" in node) {
|
|
1026
1046
|
base.componentProperties = node.componentProperties;
|