@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 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.0...HEAD
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 get components
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 countNodes = (n3) => 1 + (n3.children?.reduce((sum, c5) => sum + countNodes(c5), 0) || 0);
4728
- const total = countNodes(result);
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: Number(args.depth),
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 all components" },
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dannote/figma-use",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Control Figma from the command line. Full read/write access for AI agents.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- figma.root.findAll((node) => {
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
- components.push(serializeNode(node));
89
+ if (!nameLower || node.name.toLowerCase().includes(nameLower)) {
90
+ components.push(serializeNode(node));
91
+ }
87
92
  }
88
- return false;
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
- base.componentPropertyDefinitions = node.componentPropertyDefinitions;
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;