Skip to content

Commit 4205c83

Browse files
committed
feat(core): add new icon configuration and fetch commands; update dependencies in pnpm-lock.yaml and restructure CLI commands
1 parent ffb0aa5 commit 4205c83

File tree

15 files changed

+590
-105
lines changed

15 files changed

+590
-105
lines changed

apps/docs/figmicon.config.ts

Lines changed: 0 additions & 26 deletions
This file was deleted.

apps/docs/icon.config.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { defineConfig, parseFigmaUrl } from "@figmicon/core";
2+
3+
// https://www.figma.com/design/iH0P8rAVJjqln9jiuNqY7R/VergeCloud?node-id=10806-24901&m=dev
4+
5+
// TODO: check this URL
6+
// https://www.figma.com/design/B6R9BOyrbu0h3dVMhh1kkT/coolicons-%7C-Free-Iconset--Community-?node-id=30789-32015&m=dev
7+
8+
export default defineConfig({
9+
figma: {
10+
token: process.env.FIGMA_TOKEN!,
11+
// fileId: "iH0P8rAVJjqln9jiuNqY7R",
12+
// nodeId: "10806-24901",
13+
// INFO: you can use parseFigmaUrl to parse the url alternative to fileId and nodeId
14+
...parseFigmaUrl(
15+
"https://www.figma.com/design/B6R9BOyrbu0h3dVMhh1kkT/coolicons-%7C-Free-Iconset--Community-?node-id=30789-32168&m=dev"
16+
),
17+
},
18+
fetch: {
19+
// nodeTypes: ["COMPONENT", "COMPONENT_SET"],
20+
generateFileName: (node, parentNode) => node.name + "--" + parentNode.name,
21+
outDir: "icons/badri",
22+
sanitizeName: true,
23+
},
24+
});

apps/docs/tmp/icons/sample.svg

Lines changed: 0 additions & 4 deletions
This file was deleted.

packages/core/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# @figmicon/core
22

3+
## 1.1.0
4+
5+
### Minor Changes
6+
7+
- add new icon configuration and fetch commands; update dependencies in pnpm-lock.yaml and restructure CLI commands
8+
39
## 1.0.1
410

511
### Patch Changes

packages/core/package.json

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@figmicon/core",
3-
"version": "1.0.1",
3+
"version": "1.1.0",
44
"sideEffects": false,
55
"license": "MIT",
66
"files": [
@@ -18,9 +18,13 @@
1818
"exports": {
1919
".": {
2020
"import": "./dist/index.mjs",
21-
"types": "./dist/index.d.mts"
21+
"require": "./dist/index.js",
22+
"types": "./dist/index.d.ts"
2223
}
2324
},
25+
"main": "./dist/index.js",
26+
"module": "./dist/index.mjs",
27+
"types": "./dist/index.d.ts",
2428
"devDependencies": {
2529
"@acme/eslint-config": "workspace:*",
2630
"@acme/tsconfig": "workspace:*",
@@ -30,8 +34,11 @@
3034
"typescript": "5.5.4"
3135
},
3236
"dependencies": {
37+
"@figma/rest-api-spec": "^0.33.0",
38+
"c12": "^3.2.0",
3339
"commander": "^14.0.0",
34-
"kolorist": "^1.8.0"
40+
"kolorist": "^1.8.0",
41+
"zod": "^4.1.5"
3542
},
3643
"publishConfig": {
3744
"access": "public"

packages/core/src/cli.ts

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,26 @@
11
import { Command } from "commander";
2-
import { green, cyan } from "kolorist";
3-
import fs from "node:fs/promises";
4-
import path from "node:path";
5-
import { loadConfig } from "./config.js";
2+
3+
import { fetchCommand } from "./command/fetch/index.js";
64

75
const program = new Command();
86

97
program
10-
.name("acme")
8+
.name("figmicon")
119
.description(
1210
"Fetch icons from Figma and build React components or sprite.svg"
1311
)
14-
.version("0.1.0");
12+
.version("1.0.1");
1513

16-
// نمونه‌ی ساده: یک فایل SVG تمرینی می‌سازد تا خروجی را تست کنی
14+
// fetch command
1715
program
1816
.command("fetch")
1917
.description("Fetch SVG icons from Figma by node IDs (demo write)")
2018
.option("-o, --out <dir>", "Output folder for raw SVGs", "tmp/icons")
21-
.action(async ({ out }) => {
22-
const outDir = path.resolve(process.cwd(), out);
23-
await fs.mkdir(outDir, { recursive: true });
24-
25-
const sampleSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24">
26-
<path d="M5 12h14" stroke="currentColor" fill="none" stroke-width="2"/>
27-
<path d="M13 5l7 7-7 7" stroke="currentColor" fill="none" stroke-width="2"/>
28-
</svg>`;
29-
const filePath = path.join(outDir, "sample.svg");
30-
await fs.writeFile(filePath, sampleSvg, "utf8");
31-
console.log(green("✔ fetch is working 4"));
32-
const config = await loadConfig();
33-
console.log("config", config);
34-
console.log(cyan(`→ wrote ${filePath}`));
19+
.action(async () => {
20+
await fetchCommand();
3521
});
3622

37-
// نمونه‌ی بسیار ساده‌ی دوم
23+
// hello command
3824
program
3925
.command("hello")
4026
.description("Prints a greeting")
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import fs from "node:fs/promises";
2+
import path from "node:path";
3+
import { type Node } from "@figma/rest-api-spec";
4+
import { type GenerateFileName } from "../../types";
5+
import { bgBlue, cyan, green } from "kolorist";
6+
7+
export const downloadNode = async ({
8+
fileId,
9+
apiToken,
10+
node,
11+
outDir = "icons",
12+
generateFileName = (node: Node, parentNode: Node) => node.name || node.id,
13+
sanitizeName = true,
14+
parentNode,
15+
}: {
16+
fileId: string;
17+
apiToken: string;
18+
node: Node;
19+
parentNode: Node;
20+
outDir?: string;
21+
generateFileName?: GenerateFileName;
22+
sanitizeName?: boolean;
23+
}) => {
24+
// 1) get temporary URL for SVG
25+
const apiUrl = new URL(`https://api.figma.com/v1/images/${fileId}`);
26+
apiUrl.searchParams.set("ids", node.id);
27+
apiUrl.searchParams.set("format", "svg");
28+
apiUrl.searchParams.set("svg_include_id", "true");
29+
30+
const res = await fetch(apiUrl, {
31+
headers: { "X-Figma-Token": apiToken },
32+
});
33+
34+
if (!res.ok) {
35+
throw new Error(
36+
`Failed to call Figma API: ${res.status} ${res.statusText}`
37+
);
38+
}
39+
40+
const json: { images: Record<string, string>; err?: string } =
41+
await res.json();
42+
if (json.err) throw new Error(`Figma API error: ${json.err}`);
43+
44+
const svgUrl = json.images[node.id];
45+
if (!svgUrl) throw new Error(`No SVG URL returned for node ${node.id}`);
46+
47+
// 2) download the SVG
48+
const svgRes = await fetch(svgUrl);
49+
if (!svgRes.ok) {
50+
throw new Error(
51+
`Failed to download SVG: ${svgRes.status} ${svgRes.statusText}`
52+
);
53+
}
54+
55+
const svg = await svgRes.text();
56+
57+
let relPath: string;
58+
59+
if (sanitizeName) {
60+
relPath = `${sanitizeFileName(generateFileName(node, parentNode), node.id)}.svg`;
61+
} else {
62+
relPath = `${generateFileName(node, parentNode)}.svg`;
63+
}
64+
65+
const filePath = path.join(outDir, relPath);
66+
67+
// 4) create folders
68+
await fs.mkdir(path.dirname(filePath), { recursive: true });
69+
70+
// 5) write file
71+
await fs.writeFile(filePath, svg, "utf8");
72+
73+
console.log(
74+
bgBlue(" Figma "),
75+
green("✔"),
76+
`Downloaded ${cyan(node.name || node.id)}${green(filePath)}`
77+
);
78+
return filePath;
79+
};
80+
81+
function sanitizeFileName(name: string, fallback: string): string {
82+
return (
83+
name
84+
.toLowerCase()
85+
.replace(/[^a-z0-9-_]+/gi, "-")
86+
.replace(/-+/g, "-")
87+
.replace(/^-|-$/g, "") || fallback
88+
);
89+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import type { GetFileNodesResponse } from "@figma/rest-api-spec";
2+
import { green, cyan, bgBlue } from "kolorist";
3+
4+
/**
5+
* Options for fetching a Figma document node.
6+
* @interface FigmaNodeFetchOptions
7+
* @property {string} fileId - The ID of the Figma file to fetch from
8+
* @property {string} nodeId - The ID of the node to fetch (supports both "123-456" and "123:456" formats)
9+
* @property {string} apiToken - The Figma API access token for authentication
10+
*/
11+
export interface FigmaNodeFetchOptions {
12+
fileId: string;
13+
nodeId: string;
14+
apiToken: string;
15+
}
16+
17+
/**
18+
* Represents a node in the Figma document tree.
19+
* This is a base type that includes all properties from the Figma API response.
20+
*/
21+
export type FigmaDocumentNode =
22+
GetFileNodesResponse["nodes"][string]["document"];
23+
24+
/**
25+
* Represents a Figma node that contains child nodes.
26+
* This type ensures the node has a children array containing other document nodes.
27+
*/
28+
export type FigmaParentNode = FigmaDocumentNode & {
29+
children: FigmaDocumentNode[];
30+
};
31+
32+
/**
33+
* Type guard that checks if a node has children.
34+
* @param {FigmaDocumentNode} node - The node to check
35+
* @returns {boolean} True if the node has children, false otherwise
36+
*/
37+
export function isFigmaParentNode(
38+
node: FigmaDocumentNode
39+
): node is FigmaParentNode {
40+
return Array.isArray((node as any)?.children);
41+
}
42+
43+
/**
44+
* Fetches a Figma document node by its ID.
45+
* @returns The document node if found and has children, null if not found.
46+
* @throws Error if the node exists but has no children or if the API request fails.
47+
*/
48+
export const fetchFigmaDocumentNode = async ({
49+
fileId,
50+
nodeId,
51+
apiToken,
52+
}: FigmaNodeFetchOptions): Promise<FigmaParentNode | null> => {
53+
const formattedNodeId = nodeId.includes(":")
54+
? nodeId
55+
: nodeId.replace(/-/g, ":"); // Figma IDs use ":" not "-"
56+
57+
// Log fetch initiation
58+
console.log(
59+
bgBlue(" Figma "),
60+
"Fetching node:",
61+
cyan(formattedNodeId),
62+
"from file:",
63+
green(fileId)
64+
);
65+
66+
const url = `https://api.figma.com/v1/files/${fileId}/nodes?ids=${formattedNodeId}`;
67+
68+
const response = await fetch(url, {
69+
headers: { "X-Figma-Token": apiToken },
70+
});
71+
72+
// Log API response status
73+
if (response.ok) {
74+
console.log(bgBlue(" Figma "), green("✔"), "API request successful");
75+
}
76+
77+
if (!response.ok) {
78+
const errorMessage = `Failed to fetch Figma node (${response.status} ${response.statusText})`;
79+
console.error(bgBlue(" Figma "), "❌", errorMessage);
80+
throw new Error(errorMessage);
81+
}
82+
83+
const data: GetFileNodesResponse = await response.json();
84+
const nodeData = data.nodes?.[formattedNodeId];
85+
86+
if (!nodeData?.document) {
87+
console.warn(bgBlue(" Figma "), "⚠️", `Node not found: ${formattedNodeId}`);
88+
return null;
89+
}
90+
91+
const documentNode = nodeData.document as FigmaDocumentNode;
92+
93+
if (isFigmaParentNode(documentNode)) {
94+
console.log(
95+
bgBlue(" Figma "),
96+
green("✔"),
97+
"Node retrieved successfully with",
98+
cyan(documentNode.children.length.toString()),
99+
"children"
100+
);
101+
return documentNode;
102+
} else {
103+
const errorMessage = `Selected node must have children (node type: ${documentNode.type})`;
104+
console.error(bgBlue(" Figma "), "❌", errorMessage);
105+
throw new Error(errorMessage);
106+
}
107+
};

0 commit comments

Comments
 (0)