@figtools/cli 0.2.0 → 0.3.0

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/dist/cli.d.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  export type OutputFormat = "json" | "markdown";
2
+ export type ImageFormat = "webp" | "png" | "jpg";
2
3
  export interface ParsedArgs {
3
4
  urls: string[];
4
5
  format: OutputFormat;
5
6
  outputPath?: string;
7
+ imageFormat: ImageFormat;
6
8
  quiet: boolean;
7
9
  command?: "login";
8
10
  }
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAcA,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,UAAU,CAAC;AAE/C,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,MAAM,EAAE,YAAY,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,KAAK,cAAc,GAAG;IACpB,IAAI,EAAE,oBAAoB,GAAG,kCAAkC,GAAG,iBAAiB,CAAC;IACpF,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAMF,KAAK,aAAa,GAAG;IAAE,IAAI,EAAE,gBAAgB,GAAG,mBAAmB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtF,MAAM,MAAM,eAAe,GACvB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,UAAU,CAAA;CAAE,GAC/B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,cAAc,CAAA;CAAE,GACpC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,aAAa,CAAA;CAAE,CAAC;AAEvC,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACnC;IAAE,IAAI,EAAE,uBAAuB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AA8DzD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,eAAe,CAmCzD;AAED,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,MAAM,EAAE,YAAY,GACnB,YAAY,CAad"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAcA,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,UAAU,CAAC;AAE/C,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,KAAK,GAAG,KAAK,CAAC;AAEjD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,MAAM,EAAE,YAAY,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,WAAW,CAAC;IACzB,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,KAAK,cAAc,GAAG;IACpB,IAAI,EAAE,oBAAoB,GAAG,kCAAkC,GAAG,iBAAiB,CAAC;IACpF,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAMF,KAAK,aAAa,GAAG;IAAE,IAAI,EAAE,gBAAgB,GAAG,mBAAmB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtF,MAAM,MAAM,eAAe,GACvB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,UAAU,CAAA;CAAE,GAC/B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,cAAc,CAAA;CAAE,GACpC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,aAAa,CAAA;CAAE,CAAC;AAEvC,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACnC;IAAE,IAAI,EAAE,uBAAuB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAuEzD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,eAAe,CAmCzD;AAED,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,MAAM,EAAE,YAAY,GACnB,YAAY,CAad"}
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { extname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { Command, CommanderError, InvalidArgumentError } from "commander";
5
- import { CookieSessionStore, PlaywrightFigmaGateway, PlaywrightLogin, createFigmaScraperCore } from "@figtools/core";
5
+ import { CookieSessionStore, PlaywrightFigmaNodeSource, PlaywrightLogin, createFigmaScraperCore } from "@figtools/core";
6
6
  import { mkdir, writeFile } from "node:fs/promises";
7
7
  async function resolveAll(core, urls) {
8
8
  const sessionResult = await core.ensureSession();
@@ -55,50 +55,148 @@ function slugifyWithCollisions(names) {
55
55
  function isFigmaPage(result) {
56
56
  return "nodes" in result;
57
57
  }
58
- function leafContent(node) {
59
- return `# ${node.name}\n\ntype: ${node.type}\n`;
58
+ function colorToHex({ r, g, b, a }) {
59
+ const hex = [
60
+ r,
61
+ g,
62
+ b
63
+ ].map((c)=>Math.round(c).toString(16).padStart(2, "0")).join("").toUpperCase();
64
+ return a < 1 ? `#${hex} ${Math.round(100 * a)}%` : `#${hex}`;
60
65
  }
61
- function containerContent(name, children, childSlugs) {
62
- const links = children.map((child, i)=>{
63
- const slug = childSlugs[i];
64
- const href = 0 === child.children.length ? `${slug}.md` : `${slug}/index.md`;
65
- return `- [${child.name}](${href})`;
66
- }).join("\n");
66
+ function formatPaint(paint) {
67
+ const hex = colorToHex(paint.color);
68
+ return paint.styleName ? `${hex} (${paint.styleName})` : hex;
69
+ }
70
+ function formatEffect(effect) {
71
+ const color = effect.color ? colorToHex(effect.color) : "";
72
+ return [
73
+ effect.type,
74
+ `x=${effect.x}`,
75
+ `y=${effect.y}`,
76
+ `blur=${effect.blur}`,
77
+ `spread=${effect.spread}`,
78
+ color
79
+ ].filter(Boolean).join(" ");
80
+ }
81
+ function formatSize(value, mode) {
82
+ if ("Fill" === mode) return null !== value ? `Fill (${value}px)` : "Fill";
83
+ if (null === value) return null;
84
+ if ("Hug" === mode) return `Hug (${value}px)`;
85
+ if ("Fixed" === mode) return `Fixed (${value}px)`;
86
+ return `${value}px`;
87
+ }
88
+ function nodePropertyLines(node) {
89
+ const lines = [
90
+ `type: ${node.type}`
91
+ ];
92
+ if (!node.visible) lines.push("visible: false");
93
+ const { flow, widthSizing, heightSizing, fills, strokes, strokeWeight, strokeSide, cornerRadius, effects, opacity, paddingTop, paddingRight, paddingBottom, paddingLeft, itemSpacing, typography } = node.styles;
94
+ const layoutLines = [];
95
+ if (flow) layoutLines.push(`Flow: ${flow}`);
96
+ if (null !== node.position.x && null !== node.position.y) layoutLines.push(`Position: x=${node.position.x} y=${node.position.y}`);
97
+ const w = formatSize(node.size.width, widthSizing);
98
+ const h = formatSize(node.size.height, heightSizing);
99
+ if (null !== w) layoutLines.push(`Width: ${w}`);
100
+ if (null !== h) layoutLines.push(`Height: ${h}`);
101
+ if (void 0 !== paddingTop) layoutLines.push(`Padding top: ${paddingTop}px`);
102
+ if (void 0 !== paddingRight) layoutLines.push(`Padding right: ${paddingRight}px`);
103
+ if (void 0 !== paddingBottom) layoutLines.push(`Padding bottom: ${paddingBottom}px`);
104
+ if (void 0 !== paddingLeft) layoutLines.push(`Padding left: ${paddingLeft}px`);
105
+ if (void 0 !== itemSpacing) layoutLines.push(`Gap: ${itemSpacing}px`);
106
+ if (void 0 !== opacity) layoutLines.push(`Opacity: ${opacity}%`);
107
+ if (layoutLines.length > 0) lines.push("", "## Layout", ...layoutLines);
108
+ if (fills && fills.length > 0) lines.push("", "## Fill", ...fills.map(formatPaint));
109
+ const borderLines = [];
110
+ if (void 0 !== strokeWeight || strokes && strokes.length > 0) {
111
+ const label = strokeSide ? `Border ${strokeSide.toLowerCase()}` : "Border";
112
+ const parts = [];
113
+ if (void 0 !== strokeWeight) parts.push(`${strokeWeight}px`);
114
+ if (strokes && strokes.length > 0) parts.push(strokes.map(formatPaint).join(", "));
115
+ borderLines.push(`${label}: ${parts.join(" ")}`);
116
+ }
117
+ if (void 0 !== cornerRadius) borderLines.push(`Radius: ${cornerRadius}px`);
118
+ if (effects && effects.length > 0) for (const effect of effects)borderLines.push(formatEffect(effect));
119
+ if (borderLines.length > 0) lines.push("", "## Border", ...borderLines);
120
+ if (typography) {
121
+ const typoLines = [];
122
+ if (typography.styleName) typoLines.push(`Text style: ${typography.styleName}`);
123
+ if (typography.fontFamily) typoLines.push(`Font: ${typography.fontFamily}`);
124
+ if (typography.style) typoLines.push(`Style: ${typography.style}`);
125
+ if (null !== typography.fontWeight && void 0 !== typography.fontWeight) typoLines.push(`Weight: ${typography.fontWeight}`);
126
+ if (null !== typography.fontSize && void 0 !== typography.fontSize) typoLines.push(`Size: ${typography.fontSize}px`);
127
+ if (null !== typography.lineHeightPx && void 0 !== typography.lineHeightPx) typoLines.push(`Line height: ${typography.lineHeightPx}px`);
128
+ if (void 0 !== typography.letterSpacing) typoLines.push(`Letter spacing: ${typography.letterSpacing}`);
129
+ if (typography.textAlignHorizontal) typoLines.push(`Align: ${typography.textAlignHorizontal}`);
130
+ if (typography.textAlignVertical) typoLines.push(`Vertical align: ${typography.textAlignVertical}`);
131
+ if (typoLines.length > 0) lines.push("", "## Typography", ...typoLines);
132
+ }
133
+ if (null !== node.characters) lines.push("", "## Content", node.characters);
134
+ return lines;
135
+ }
136
+ function mediaHead(node, slug, format) {
137
+ const lines = [];
138
+ if (null !== node.image) lines.push(`![${node.name}](./${slug}.${format})`);
139
+ if (null !== node.svgCode) lines.push(`[SVG](./${slug}.svg)`);
140
+ return lines.length > 0 ? `\n\n${lines.join("\n")}` : "";
141
+ }
142
+ function nodeContent(node, slug, format) {
143
+ return `# ${node.name}${mediaHead(node, slug, format)}\n\n${nodePropertyLines(node).join("\n")}\n`;
144
+ }
145
+ function pageContent(name, nodes, childSlugs) {
146
+ const links = nodes.map((node, i)=>`- [${node.name}](${childSlugs[i]}/index.md)`).join("\n");
67
147
  return `# ${name}\n\n${links}\n`;
68
148
  }
69
- async function writeChildNode(node, parentDir, slug) {
70
- if (0 === node.children.length) return void await writeFile(join(parentDir, `${slug}.md`), leafContent(node), "utf8");
149
+ function containerContent(node, childSlugs, slug, format) {
150
+ const meta = nodePropertyLines(node);
151
+ const links = node.children.map((child, i)=>`- [${child.name}](${childSlugs[i]}/index.md)`).join("\n");
152
+ return `# ${node.name}${mediaHead(node, slug, format)}\n\n${meta.join("\n")}\n\n## Children\n\n${links}\n`;
153
+ }
154
+ async function writeNodeAssets(node, dir, slug, format) {
155
+ if (null !== node.image) await writeFile(join(dir, `${slug}.${format}`), node.image);
156
+ if (null !== node.svgCode) await writeFile(join(dir, `${slug}.svg`), node.svgCode, "utf8");
157
+ }
158
+ async function writeChildNode(node, parentDir, slug, options) {
71
159
  const nodeDir = join(parentDir, slug);
72
160
  await mkdir(nodeDir, {
73
161
  recursive: true
74
162
  });
163
+ const format = options.imageFormat ?? "png";
164
+ await writeNodeAssets(node, nodeDir, slug, format);
165
+ if (0 === node.children.length) return void await writeFile(join(nodeDir, "index.md"), nodeContent(node, slug, format), "utf8");
75
166
  const childSlugs = slugifyWithCollisions(node.children.map((c)=>c.name));
76
- await writeFile(join(nodeDir, "index.md"), containerContent(node.name, node.children, childSlugs), "utf8");
77
- for(let i = 0; i < node.children.length; i++)await writeChildNode(node.children[i], nodeDir, childSlugs[i]);
167
+ await writeFile(join(nodeDir, "index.md"), containerContent(node, childSlugs, slug, format), "utf8");
168
+ for(let i = 0; i < node.children.length; i++)await writeChildNode(node.children[i], nodeDir, childSlugs[i], options);
78
169
  }
79
170
  async function writeAsMarkdownTree(fileKey, result, options) {
80
171
  const fileKeyDir = join(options.outputDir, fileKey);
81
172
  await mkdir(fileKeyDir, {
82
173
  recursive: true
83
174
  });
175
+ const format = options.imageFormat ?? "png";
84
176
  if (isFigmaPage(result)) {
85
177
  const childSlugs = slugifyWithCollisions(result.nodes.map((n)=>n.name));
86
- await writeFile(join(fileKeyDir, "index.md"), containerContent(result.name, result.nodes, childSlugs), "utf8");
87
- for(let i = 0; i < result.nodes.length; i++)await writeChildNode(result.nodes[i], fileKeyDir, childSlugs[i]);
178
+ await writeFile(join(fileKeyDir, "index.md"), pageContent(result.name, result.nodes, childSlugs), "utf8");
179
+ for(let i = 0; i < result.nodes.length; i++)await writeChildNode(result.nodes[i], fileKeyDir, childSlugs[i], options);
88
180
  return;
89
181
  }
182
+ const [slug] = slugifyWithCollisions([
183
+ result.name
184
+ ]);
90
185
  if (0 === result.children.length) {
91
- const [slug] = slugifyWithCollisions([
92
- result.name
93
- ]);
94
- await writeFile(join(fileKeyDir, `${slug}.md`), leafContent(result), "utf8");
186
+ const nodeDir = join(fileKeyDir, slug);
187
+ await mkdir(nodeDir, {
188
+ recursive: true
189
+ });
190
+ await writeNodeAssets(result, nodeDir, slug, format);
191
+ await writeFile(join(nodeDir, "index.md"), nodeContent(result, slug, format), "utf8");
95
192
  return;
96
193
  }
194
+ await writeNodeAssets(result, fileKeyDir, slug, format);
97
195
  const childSlugs = slugifyWithCollisions(result.children.map((c)=>c.name));
98
- await writeFile(join(fileKeyDir, "index.md"), containerContent(result.name, result.children, childSlugs), "utf8");
99
- for(let i = 0; i < result.children.length; i++)await writeChildNode(result.children[i], fileKeyDir, childSlugs[i]);
196
+ await writeFile(join(fileKeyDir, "index.md"), containerContent(result, childSlugs, slug, format), "utf8");
197
+ for(let i = 0; i < result.children.length; i++)await writeChildNode(result.children[i], fileKeyDir, childSlugs[i], options);
100
198
  }
101
- var package_namespaceObject = /*#__PURE__*/ JSON.parse('{"rE":"0.2.0","h_":"Resolves one or more Figma URLs from the command line and writes the result as JSON or as a navigable markdown tree meant to be used as a data source by an LLM."}');
199
+ var package_namespaceObject = /*#__PURE__*/ JSON.parse('{"rE":"0.3.0","h_":"Resolves one or more Figma URLs from the command line and writes the result as JSON or as a navigable markdown tree meant to be used as a data source by an LLM."}');
102
200
  function createProgram() {
103
201
  let output = "";
104
202
  let parsed;
@@ -112,11 +210,12 @@ function createProgram() {
112
210
  });
113
211
  const resolveCommand = program.command("resolve", {
114
212
  isDefault: true
115
- }).description("Resolve one or more Figma URLs and write the result").argument("<urls...>", "One or more Figma URLs to resolve").option("--format <format>", "Output format (json or markdown)", parseFormat, "json").option("--output <path>", "File or directory to write the result to").option("--quiet", "Suppress progress messages on stderr", false).action((urls, opts)=>{
213
+ }).description("Resolve one or more Figma URLs and write the result").argument("<urls...>", "One or more Figma URLs to resolve").option("--format <format>", "Output format (json or markdown)", parseFormat, "json").option("--image-format <format>", "Image format for node previews (webp, png, jpg)", parseImageFormat, "png").option("--output <path>", "File or directory to write the result to").option("--quiet", "Suppress progress messages on stderr", false).action((urls, opts)=>{
116
214
  parsed = {
117
215
  urls,
118
216
  format: opts.format,
119
217
  outputPath: opts.output,
218
+ imageFormat: opts.imageFormat,
120
219
  quiet: opts.quiet
121
220
  };
122
221
  });
@@ -126,6 +225,7 @@ function createProgram() {
126
225
  command: "login",
127
226
  urls: [],
128
227
  format: "json",
228
+ imageFormat: "png",
129
229
  quiet: false
130
230
  };
131
231
  });
@@ -140,6 +240,10 @@ function parseFormat(value) {
140
240
  if ("json" !== value && "markdown" !== value) throw new InvalidArgumentError("Allowed choices are json, markdown.");
141
241
  return value;
142
242
  }
243
+ function parseImageFormat(value) {
244
+ if ("webp" !== value && "png" !== value && "jpg" !== value) throw new InvalidArgumentError("Allowed choices are webp, png, jpg.");
245
+ return value;
246
+ }
143
247
  function parseArgs(argv) {
144
248
  const { program, getParsed, getOutput } = createProgram();
145
249
  try {
@@ -231,11 +335,11 @@ async function main(argv) {
231
335
  process.stderr.write(`Error: ${parsed.error.message}\n`);
232
336
  process.exit(1);
233
337
  }
234
- const { command, urls, format, outputPath, quiet } = parsed.value;
338
+ const { command, urls, format, outputPath, imageFormat, quiet } = parsed.value;
235
339
  const core = createFigmaScraperCore({
236
340
  sessionStore: new CookieSessionStore(),
237
341
  interactiveLogin: new PlaywrightLogin(),
238
- gateway: new PlaywrightFigmaGateway()
342
+ gateway: new PlaywrightFigmaNodeSource()
239
343
  });
240
344
  if ("login" === command) {
241
345
  const result = await core.reauthenticate();
@@ -263,7 +367,8 @@ async function main(argv) {
263
367
  if ("markdown" === format) {
264
368
  const dir = "directory" === outputTarget.kind ? outputTarget.path : ".";
265
369
  await writeAsMarkdownTree(fileKey, result.value, {
266
- outputDir: dir
370
+ outputDir: dir,
371
+ imageFormat
267
372
  });
268
373
  } else {
269
374
  const writePath = "file" === outputTarget.kind ? outputTarget.path : "directory" === outputTarget.kind ? outputTarget.path : void 0;
@@ -1,6 +1,7 @@
1
1
  import type { FigmaScrapeResult } from "@figtools/core";
2
2
  export interface MarkdownWriterOptions {
3
3
  outputDir: string;
4
+ imageFormat?: "webp" | "png" | "jpg";
4
5
  }
5
6
  export declare function writeAsMarkdownTree(fileKey: string, result: FigmaScrapeResult, options: MarkdownWriterOptions): Promise<void>;
6
7
  //# sourceMappingURL=markdown-writer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"markdown-writer.d.ts","sourceRoot":"","sources":["../../src/output/markdown-writer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAwB,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAG9E,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC;CACnB;AAoCD,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,iBAAiB,EACzB,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAgCf"}
1
+ {"version":3,"file":"markdown-writer.d.ts","sourceRoot":"","sources":["../../src/output/markdown-writer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAiD,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAGvG,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,KAAK,CAAC;CACtC;AA8KD,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,iBAAiB,EACzB,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAmCf"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@figtools/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Resolves one or more Figma URLs from the command line and writes the result as JSON or as a navigable markdown tree meant to be used as a data source by an LLM.",
5
5
  "keywords": [
6
6
  "figma",
@@ -36,7 +36,7 @@
36
36
  "dependencies": {
37
37
  "commander": "^15.0.0",
38
38
  "playwright": "^1.61.1",
39
- "@figtools/core": "0.1.0"
39
+ "@figtools/core": "0.2.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@rslib/core": "^0.23.2",