@mstrathman/figma 0.1.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 +4 -0
- package/README.md +126 -32
- package/dist/main.js +1892 -564
- package/dist/main.js.map +1 -1
- package/package.json +12 -3
package/dist/main.js
CHANGED
|
@@ -60,9 +60,9 @@ var init_client = __esm({
|
|
|
60
60
|
this.timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
61
61
|
this.maxRetries = options.maxRetries ?? MAX_RETRIES;
|
|
62
62
|
}
|
|
63
|
-
async request(method,
|
|
63
|
+
async request(method, path8, options = {}, retryCount = 0) {
|
|
64
64
|
const baseWithSlash = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
65
|
-
const pathWithoutLeadingSlash =
|
|
65
|
+
const pathWithoutLeadingSlash = path8.startsWith("/") ? path8.slice(1) : path8;
|
|
66
66
|
const url = new URL(pathWithoutLeadingSlash, baseWithSlash);
|
|
67
67
|
if (options.params) {
|
|
68
68
|
for (const [key, value] of Object.entries(options.params)) {
|
|
@@ -110,12 +110,12 @@ var init_client = __esm({
|
|
|
110
110
|
const retryAfter = response.headers.get("Retry-After");
|
|
111
111
|
const delayMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : INITIAL_RETRY_DELAY_MS * Math.pow(2, retryCount);
|
|
112
112
|
await this.sleep(delayMs);
|
|
113
|
-
return this.request(method,
|
|
113
|
+
return this.request(method, path8, options, retryCount + 1);
|
|
114
114
|
}
|
|
115
115
|
if (response.status >= 500 && retryCount < this.maxRetries) {
|
|
116
116
|
const delayMs = INITIAL_RETRY_DELAY_MS * Math.pow(2, retryCount);
|
|
117
117
|
await this.sleep(delayMs);
|
|
118
|
-
return this.request(method,
|
|
118
|
+
return this.request(method, path8, options, retryCount + 1);
|
|
119
119
|
}
|
|
120
120
|
if (!response.ok) {
|
|
121
121
|
let body;
|
|
@@ -133,7 +133,7 @@ var init_client = __esm({
|
|
|
133
133
|
return response.json();
|
|
134
134
|
}
|
|
135
135
|
sleep(ms) {
|
|
136
|
-
return new Promise((
|
|
136
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
137
137
|
}
|
|
138
138
|
// User endpoints
|
|
139
139
|
async getMe() {
|
|
@@ -281,8 +281,8 @@ var init_client = __esm({
|
|
|
281
281
|
);
|
|
282
282
|
}
|
|
283
283
|
// Generic API method for raw access
|
|
284
|
-
async api(method,
|
|
285
|
-
const normalizedPath =
|
|
284
|
+
async api(method, path8, options) {
|
|
285
|
+
const normalizedPath = path8.startsWith("/") ? path8 : `/${path8}`;
|
|
286
286
|
return this.request(method.toUpperCase(), normalizedPath, options);
|
|
287
287
|
}
|
|
288
288
|
};
|
|
@@ -578,19 +578,19 @@ async function findAvailablePort(startPort, maxPort = MAX_PORT) {
|
|
|
578
578
|
if (startPort > maxPort) {
|
|
579
579
|
throw new Error(`No available ports found between 8585 and ${maxPort}`);
|
|
580
580
|
}
|
|
581
|
-
return new Promise((
|
|
581
|
+
return new Promise((resolve3, reject) => {
|
|
582
582
|
const server = http.createServer();
|
|
583
583
|
server.listen(startPort, "127.0.0.1", () => {
|
|
584
584
|
const address = server.address();
|
|
585
585
|
if (address && typeof address === "object") {
|
|
586
586
|
const port = address.port;
|
|
587
|
-
server.close(() =>
|
|
587
|
+
server.close(() => resolve3(port));
|
|
588
588
|
} else {
|
|
589
589
|
reject(new Error("Could not get server address"));
|
|
590
590
|
}
|
|
591
591
|
});
|
|
592
592
|
server.on("error", () => {
|
|
593
|
-
findAvailablePort(startPort + 1, maxPort).then(
|
|
593
|
+
findAvailablePort(startPort + 1, maxPort).then(resolve3).catch(reject);
|
|
594
594
|
});
|
|
595
595
|
});
|
|
596
596
|
}
|
|
@@ -599,7 +599,7 @@ async function startOAuthFlow(config, onOpenUrl) {
|
|
|
599
599
|
const redirectUri = config.redirectUri || `http://127.0.0.1:${port}/callback`;
|
|
600
600
|
const state = generateState();
|
|
601
601
|
const scopes = config.scopes || ["files:read"];
|
|
602
|
-
return new Promise((
|
|
602
|
+
return new Promise((resolve3, reject) => {
|
|
603
603
|
let serverClosed = false;
|
|
604
604
|
const closeServer = () => {
|
|
605
605
|
if (!serverClosed) {
|
|
@@ -656,7 +656,7 @@ async function startOAuthFlow(config, onOpenUrl) {
|
|
|
656
656
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
657
657
|
res.end(getSuccessHtml());
|
|
658
658
|
closeServer();
|
|
659
|
-
|
|
659
|
+
resolve3({
|
|
660
660
|
accessToken: tokenData.access_token,
|
|
661
661
|
refreshToken: tokenData.refresh_token,
|
|
662
662
|
expiresIn: tokenData.expires_in,
|
|
@@ -876,7 +876,7 @@ function createFactory() {
|
|
|
876
876
|
}
|
|
877
877
|
|
|
878
878
|
// src/cmd/root.ts
|
|
879
|
-
import { Command as
|
|
879
|
+
import { Command as Command30 } from "commander";
|
|
880
880
|
|
|
881
881
|
// src/cmd/auth/index.ts
|
|
882
882
|
import { Command as Command4 } from "commander";
|
|
@@ -1163,7 +1163,7 @@ function createAuthCommand(factory) {
|
|
|
1163
1163
|
}
|
|
1164
1164
|
|
|
1165
1165
|
// src/cmd/file/index.ts
|
|
1166
|
-
import { Command as
|
|
1166
|
+
import { Command as Command9 } from "commander";
|
|
1167
1167
|
|
|
1168
1168
|
// src/cmd/file/get.ts
|
|
1169
1169
|
import { Command as Command5 } from "commander";
|
|
@@ -1228,25 +1228,78 @@ function formatValue(value) {
|
|
|
1228
1228
|
return String(value);
|
|
1229
1229
|
}
|
|
1230
1230
|
|
|
1231
|
+
// src/internal/url/parser.ts
|
|
1232
|
+
import {
|
|
1233
|
+
parseFileAndNodeId,
|
|
1234
|
+
parseFileId,
|
|
1235
|
+
formatNodeId
|
|
1236
|
+
} from "@design-sdk/figma-url";
|
|
1237
|
+
function parseFigmaInput(input) {
|
|
1238
|
+
const trimmed = input.trim();
|
|
1239
|
+
if (trimmed.includes("figma.com/")) {
|
|
1240
|
+
const result = parseFileAndNodeId(trimmed);
|
|
1241
|
+
if (result) {
|
|
1242
|
+
return {
|
|
1243
|
+
fileKey: result.file,
|
|
1244
|
+
nodeId: result.node || void 0
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
try {
|
|
1248
|
+
const fileKey = parseFileId(trimmed);
|
|
1249
|
+
return { fileKey };
|
|
1250
|
+
} catch (error) {
|
|
1251
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1252
|
+
throw new Error(`Could not parse Figma URL: ${trimmed}. ${message}`);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
return { fileKey: trimmed };
|
|
1256
|
+
}
|
|
1257
|
+
function parseNodeIds(input) {
|
|
1258
|
+
const rawIds = input.split(",").map((id) => id.trim()).filter(Boolean);
|
|
1259
|
+
const validIds = [];
|
|
1260
|
+
const invalidIds = [];
|
|
1261
|
+
for (const id of rawIds) {
|
|
1262
|
+
const formatted = formatNodeId(id);
|
|
1263
|
+
if (formatted) {
|
|
1264
|
+
validIds.push(formatted);
|
|
1265
|
+
} else {
|
|
1266
|
+
invalidIds.push(id);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
if (invalidIds.length > 0) {
|
|
1270
|
+
throw new Error(
|
|
1271
|
+
`Invalid node ID(s): ${invalidIds.join(", ")}. Node IDs should be in format "1:2" or "1-2".`
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
return validIds;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1231
1277
|
// src/cmd/file/get.ts
|
|
1232
1278
|
function createFileGetCommand(factory) {
|
|
1233
|
-
const cmd = new Command5("get").description("Get file JSON structure").argument("<
|
|
1279
|
+
const cmd = new Command5("get").description("Get file JSON structure").argument("<url>", "Figma URL or file key").option("-v, --version <version>", "File version to get").option(
|
|
1234
1280
|
"-d, --depth <depth>",
|
|
1235
1281
|
"Depth of node tree to return (default: full tree)",
|
|
1236
1282
|
parseInt
|
|
1237
|
-
).option("
|
|
1238
|
-
async (
|
|
1283
|
+
).option("--id <ids>", "Comma-separated node IDs (overrides URL node-id)").option("--geometry <paths>", "Include geometry data (paths)").option("-f, --format <format>", "Output format (json, table)", "json").option("-t, --token <token>", "Override authentication token").action(
|
|
1284
|
+
async (url, options) => {
|
|
1239
1285
|
const { io } = factory;
|
|
1240
1286
|
try {
|
|
1241
|
-
const
|
|
1287
|
+
const parsed = parseFigmaInput(url);
|
|
1288
|
+
const fileKey = parsed.fileKey;
|
|
1289
|
+
let nodeIds;
|
|
1290
|
+
if (options.id) {
|
|
1291
|
+
nodeIds = parseNodeIds(options.id);
|
|
1292
|
+
} else if (parsed.nodeId) {
|
|
1293
|
+
nodeIds = [parsed.nodeId];
|
|
1294
|
+
}
|
|
1242
1295
|
const client = await factory.getClient();
|
|
1243
1296
|
if (options.token) {
|
|
1244
1297
|
const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
1245
|
-
const overrideClient = new FigmaClient2({ token });
|
|
1298
|
+
const overrideClient = new FigmaClient2({ token: options.token });
|
|
1246
1299
|
const file2 = await overrideClient.getFile(fileKey, {
|
|
1247
1300
|
version: options.version,
|
|
1248
1301
|
depth: options.depth,
|
|
1249
|
-
ids:
|
|
1302
|
+
ids: nodeIds,
|
|
1250
1303
|
geometry: options.geometry
|
|
1251
1304
|
});
|
|
1252
1305
|
io.out.write(
|
|
@@ -1258,7 +1311,7 @@ function createFileGetCommand(factory) {
|
|
|
1258
1311
|
const file = await client.getFile(fileKey, {
|
|
1259
1312
|
version: options.version,
|
|
1260
1313
|
depth: options.depth,
|
|
1261
|
-
ids:
|
|
1314
|
+
ids: nodeIds,
|
|
1262
1315
|
geometry: options.geometry
|
|
1263
1316
|
});
|
|
1264
1317
|
io.out.write(
|
|
@@ -1282,13 +1335,27 @@ function createFileGetCommand(factory) {
|
|
|
1282
1335
|
import { Command as Command6 } from "commander";
|
|
1283
1336
|
import chalk6 from "chalk";
|
|
1284
1337
|
function createFileNodesCommand(factory) {
|
|
1285
|
-
const cmd = new Command6("nodes").description("Get specific nodes from a file").argument("<
|
|
1286
|
-
async (
|
|
1338
|
+
const cmd = new Command6("nodes").description("Get specific nodes from a file").argument("<url>", "Figma URL (with node-id) or file key").option("--id <ids>", "Comma-separated node IDs (overrides URL node-id)").option("-v, --version <version>", "File version to get").option("-d, --depth <depth>", "Depth of node tree to return", parseInt).option("--geometry <paths>", "Include geometry data (paths)").option("-f, --format <format>", "Output format (json, table)", "json").option("-t, --token <token>", "Override authentication token").action(
|
|
1339
|
+
async (url, options) => {
|
|
1287
1340
|
const { io } = factory;
|
|
1288
1341
|
try {
|
|
1342
|
+
const parsed = parseFigmaInput(url);
|
|
1343
|
+
const fileKey = parsed.fileKey;
|
|
1344
|
+
let nodeIds;
|
|
1345
|
+
if (options.id) {
|
|
1346
|
+
nodeIds = parseNodeIds(options.id);
|
|
1347
|
+
} else if (parsed.nodeId) {
|
|
1348
|
+
nodeIds = [parsed.nodeId];
|
|
1349
|
+
} else {
|
|
1350
|
+
io.err.write(
|
|
1351
|
+
chalk6.red(
|
|
1352
|
+
"Error: node ID required. Use a URL with node-id or --id flag.\n"
|
|
1353
|
+
)
|
|
1354
|
+
);
|
|
1355
|
+
process.exit(1);
|
|
1356
|
+
}
|
|
1289
1357
|
const token = await factory.getToken(options.token);
|
|
1290
1358
|
const client = await factory.getClient();
|
|
1291
|
-
const nodeIds = options.ids.split(",").map((id) => id.trim());
|
|
1292
1359
|
if (options.token) {
|
|
1293
1360
|
const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
1294
1361
|
const overrideClient = new FigmaClient2({ token });
|
|
@@ -1402,208 +1469,894 @@ function sanitizeFilename(name) {
|
|
|
1402
1469
|
return name.replace(/[:/\\?*"<>|]/g, "-");
|
|
1403
1470
|
}
|
|
1404
1471
|
|
|
1405
|
-
// src/
|
|
1406
|
-
function
|
|
1407
|
-
const
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
async (fileKey, options) => {
|
|
1416
|
-
const { io } = factory;
|
|
1417
|
-
const validFormats = ["jpg", "png", "svg", "pdf"];
|
|
1418
|
-
if (!validFormats.includes(options.format)) {
|
|
1419
|
-
io.err.write(
|
|
1420
|
-
chalk7.red(
|
|
1421
|
-
`Invalid format "${options.format}". Valid formats: ${validFormats.join(", ")}
|
|
1422
|
-
`
|
|
1423
|
-
)
|
|
1424
|
-
);
|
|
1425
|
-
process.exit(1);
|
|
1426
|
-
}
|
|
1427
|
-
const imageFormat = options.format;
|
|
1428
|
-
try {
|
|
1429
|
-
const token = await factory.getToken(options.token);
|
|
1430
|
-
const client = await factory.getClient();
|
|
1431
|
-
const nodeIds = options.ids.split(",").map((id) => id.trim());
|
|
1432
|
-
let activeClient = client;
|
|
1433
|
-
if (options.token) {
|
|
1434
|
-
const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
1435
|
-
activeClient = new FigmaClient2({ token });
|
|
1436
|
-
}
|
|
1437
|
-
const images = await activeClient.getImages(fileKey, nodeIds, {
|
|
1438
|
-
scale: options.scale,
|
|
1439
|
-
format: imageFormat,
|
|
1440
|
-
svg_include_id: options.svgIncludeId,
|
|
1441
|
-
svg_simplify_stroke: options.svgSimplifyStroke
|
|
1442
|
-
});
|
|
1443
|
-
if (options.output) {
|
|
1444
|
-
const imageUrls = images.images || {};
|
|
1445
|
-
const downloadItems = Object.entries(imageUrls).map(
|
|
1446
|
-
([nodeId, url]) => ({
|
|
1447
|
-
id: nodeId,
|
|
1448
|
-
url: url ?? null,
|
|
1449
|
-
filename: `${sanitizeFilename(nodeId)}.${imageFormat}`
|
|
1450
|
-
})
|
|
1451
|
-
);
|
|
1452
|
-
const results = await downloadFiles(downloadItems, options.output);
|
|
1453
|
-
const downloadResults = results.map((r) => ({
|
|
1454
|
-
nodeId: r.id,
|
|
1455
|
-
file: r.file,
|
|
1456
|
-
status: r.status === "success" ? "downloaded" : r.error ?? "error"
|
|
1457
|
-
}));
|
|
1458
|
-
io.out.write(
|
|
1459
|
-
formatOutput(downloadResults, {
|
|
1460
|
-
format: options.outputFormat
|
|
1461
|
-
})
|
|
1462
|
-
);
|
|
1463
|
-
io.out.write("\n");
|
|
1464
|
-
} else {
|
|
1465
|
-
io.out.write(
|
|
1466
|
-
formatOutput(images, {
|
|
1467
|
-
format: options.outputFormat
|
|
1468
|
-
})
|
|
1469
|
-
);
|
|
1470
|
-
io.out.write("\n");
|
|
1471
|
-
}
|
|
1472
|
-
} catch (error) {
|
|
1473
|
-
io.err.write(chalk7.red("Error exporting images.\n"));
|
|
1474
|
-
if (error instanceof Error) {
|
|
1475
|
-
io.err.write(chalk7.dim(`${error.message}
|
|
1476
|
-
`));
|
|
1477
|
-
}
|
|
1478
|
-
process.exit(1);
|
|
1479
|
-
}
|
|
1480
|
-
}
|
|
1481
|
-
);
|
|
1482
|
-
return cmd;
|
|
1472
|
+
// src/internal/transform/tokens.ts
|
|
1473
|
+
function figmaColorToCss(color) {
|
|
1474
|
+
const r = Math.round(color.r * 255);
|
|
1475
|
+
const g = Math.round(color.g * 255);
|
|
1476
|
+
const b = Math.round(color.b * 255);
|
|
1477
|
+
const a = color.a ?? 1;
|
|
1478
|
+
if (a === 1) {
|
|
1479
|
+
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
|
1480
|
+
}
|
|
1481
|
+
return `rgba(${r}, ${g}, ${b}, ${a.toFixed(2)})`;
|
|
1483
1482
|
}
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
function createFileCommand(factory) {
|
|
1487
|
-
const cmd = new Command8("file").description("Work with Figma files");
|
|
1488
|
-
cmd.addCommand(createFileGetCommand(factory));
|
|
1489
|
-
cmd.addCommand(createFileNodesCommand(factory));
|
|
1490
|
-
cmd.addCommand(createFileImagesCommand(factory));
|
|
1491
|
-
return cmd;
|
|
1483
|
+
function toKebabCase(str) {
|
|
1484
|
+
return str.replace(/\s+/g, "-").replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").toLowerCase();
|
|
1492
1485
|
}
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1486
|
+
function shadowToCss(shadow) {
|
|
1487
|
+
const inset = shadow.inset ? "inset " : "";
|
|
1488
|
+
return `${inset}${shadow.x}px ${shadow.y}px ${shadow.blur}px ${shadow.spread}px ${shadow.color}`;
|
|
1489
|
+
}
|
|
1490
|
+
function extractStylesFromFile(file, styleMetadata) {
|
|
1491
|
+
const tokens = {
|
|
1492
|
+
colors: {},
|
|
1493
|
+
typography: {},
|
|
1494
|
+
shadows: {},
|
|
1495
|
+
radii: {},
|
|
1496
|
+
spacing: {},
|
|
1497
|
+
effects: {}
|
|
1498
|
+
};
|
|
1499
|
+
const seenRadii = /* @__PURE__ */ new Set();
|
|
1500
|
+
const seenSpacing = /* @__PURE__ */ new Set();
|
|
1501
|
+
const styleNodeMap = /* @__PURE__ */ new Map();
|
|
1502
|
+
for (const style of styleMetadata) {
|
|
1503
|
+
const nodeId = style.node_id;
|
|
1504
|
+
if (nodeId) {
|
|
1505
|
+
styleNodeMap.set(nodeId, style);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
function traverseNode(node) {
|
|
1509
|
+
const nodeWithStyles = node;
|
|
1510
|
+
if ("styles" in nodeWithStyles && nodeWithStyles.styles) {
|
|
1511
|
+
const styles = nodeWithStyles.styles;
|
|
1512
|
+
if (styles.fill && "fills" in nodeWithStyles) {
|
|
1513
|
+
const fills = nodeWithStyles.fills;
|
|
1514
|
+
if (fills && fills.length > 0 && fills[0].type === "SOLID") {
|
|
1515
|
+
const fill = fills[0];
|
|
1516
|
+
if (fill.color) {
|
|
1517
|
+
const styleInfo = styleNodeMap.get(node.id);
|
|
1518
|
+
const styleName = styleInfo?.name || node.name;
|
|
1519
|
+
const tokenName = toKebabCase(styleName);
|
|
1520
|
+
tokens.colors[tokenName] = {
|
|
1521
|
+
name: styleName,
|
|
1522
|
+
type: "color",
|
|
1523
|
+
value: figmaColorToCss(fill.color),
|
|
1524
|
+
description: styleInfo?.description
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1525
1527
|
}
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1528
|
+
}
|
|
1529
|
+
if (styles.text && "style" in nodeWithStyles && nodeWithStyles.style) {
|
|
1530
|
+
const textStyle = nodeWithStyles.style;
|
|
1531
|
+
const styleInfo = styleNodeMap.get(node.id);
|
|
1532
|
+
const styleName = styleInfo?.name || node.name;
|
|
1533
|
+
const tokenName = toKebabCase(styleName);
|
|
1534
|
+
tokens.typography[tokenName] = {
|
|
1535
|
+
name: styleName,
|
|
1536
|
+
type: "typography",
|
|
1537
|
+
value: {
|
|
1538
|
+
fontFamily: textStyle.fontFamily || "sans-serif",
|
|
1539
|
+
fontSize: `${textStyle.fontSize || 16}px`,
|
|
1540
|
+
fontWeight: textStyle.fontWeight || 400,
|
|
1541
|
+
lineHeight: textStyle.lineHeightPx !== void 0 ? `${textStyle.lineHeightPx}px` : "normal",
|
|
1542
|
+
letterSpacing: textStyle.letterSpacing !== void 0 ? `${textStyle.letterSpacing}px` : "normal"
|
|
1543
|
+
},
|
|
1544
|
+
description: styleInfo?.description
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
if (styles.effect && "effects" in nodeWithStyles && nodeWithStyles.effects) {
|
|
1548
|
+
const effects = nodeWithStyles.effects;
|
|
1549
|
+
const styleInfo = styleNodeMap.get(node.id);
|
|
1550
|
+
const styleName = styleInfo?.name || node.name;
|
|
1551
|
+
const baseTokenName = toKebabCase(styleName);
|
|
1552
|
+
let shadowIndex = 0;
|
|
1553
|
+
for (const effect of effects) {
|
|
1554
|
+
if ((effect.type === "DROP_SHADOW" || effect.type === "INNER_SHADOW") && effect.visible !== false) {
|
|
1555
|
+
const shadowEffect = effect;
|
|
1556
|
+
const colorValue = shadowEffect.color ? figmaColorToCss(shadowEffect.color) : "rgba(0, 0, 0, 0.25)";
|
|
1557
|
+
const tokenName = shadowIndex === 0 ? baseTokenName : `${baseTokenName}-${shadowIndex}`;
|
|
1558
|
+
tokens.shadows[tokenName] = {
|
|
1559
|
+
name: shadowIndex === 0 ? styleName : `${styleName} ${shadowIndex + 1}`,
|
|
1560
|
+
type: "shadow",
|
|
1561
|
+
value: {
|
|
1562
|
+
x: shadowEffect.offset?.x ?? 0,
|
|
1563
|
+
y: shadowEffect.offset?.y ?? 0,
|
|
1564
|
+
blur: shadowEffect.radius ?? 0,
|
|
1565
|
+
spread: shadowEffect.spread ?? 0,
|
|
1566
|
+
color: colorValue,
|
|
1567
|
+
inset: effect.type === "INNER_SHADOW"
|
|
1568
|
+
},
|
|
1569
|
+
description: styleInfo?.description
|
|
1570
|
+
};
|
|
1571
|
+
shadowIndex++;
|
|
1572
|
+
}
|
|
1532
1573
|
}
|
|
1533
|
-
process.exit(1);
|
|
1534
1574
|
}
|
|
1535
1575
|
}
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
format: "table",
|
|
1566
|
-
columns: ["key", "name", "last_modified"]
|
|
1567
|
-
})
|
|
1568
|
-
);
|
|
1569
|
-
} else {
|
|
1570
|
-
io.out.write(formatOutput(files, { format: "json" }));
|
|
1576
|
+
if ("cornerRadius" in nodeWithStyles && nodeWithStyles.cornerRadius !== void 0) {
|
|
1577
|
+
const radius = nodeWithStyles.cornerRadius;
|
|
1578
|
+
if (radius > 0) {
|
|
1579
|
+
const radiusValue = `${radius}px`;
|
|
1580
|
+
if (!seenRadii.has(radiusValue)) {
|
|
1581
|
+
seenRadii.add(radiusValue);
|
|
1582
|
+
const tokenName = `radius-${radius}`;
|
|
1583
|
+
tokens.radii[tokenName] = {
|
|
1584
|
+
name: `Radius ${radius}`,
|
|
1585
|
+
type: "radius",
|
|
1586
|
+
value: radiusValue
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
if ("rectangleCornerRadii" in nodeWithStyles && nodeWithStyles.rectangleCornerRadii) {
|
|
1592
|
+
const [topLeft, topRight, bottomRight, bottomLeft] = nodeWithStyles.rectangleCornerRadii;
|
|
1593
|
+
for (const radius of [topLeft, topRight, bottomRight, bottomLeft]) {
|
|
1594
|
+
if (radius > 0) {
|
|
1595
|
+
const radiusValue = `${radius}px`;
|
|
1596
|
+
if (!seenRadii.has(radiusValue)) {
|
|
1597
|
+
seenRadii.add(radiusValue);
|
|
1598
|
+
const tokenName = `radius-${radius}`;
|
|
1599
|
+
tokens.radii[tokenName] = {
|
|
1600
|
+
name: `Radius ${radius}`,
|
|
1601
|
+
type: "radius",
|
|
1602
|
+
value: radiusValue
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1571
1605
|
}
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
if ("layoutMode" in nodeWithStyles && nodeWithStyles.layoutMode !== "NONE") {
|
|
1609
|
+
if (nodeWithStyles.itemSpacing !== void 0 && nodeWithStyles.itemSpacing > 0) {
|
|
1610
|
+
const spacingValue = `${nodeWithStyles.itemSpacing}px`;
|
|
1611
|
+
if (!seenSpacing.has(spacingValue)) {
|
|
1612
|
+
seenSpacing.add(spacingValue);
|
|
1613
|
+
const tokenName = `spacing-${nodeWithStyles.itemSpacing}`;
|
|
1614
|
+
tokens.spacing[tokenName] = {
|
|
1615
|
+
name: `Spacing ${nodeWithStyles.itemSpacing}`,
|
|
1616
|
+
type: "spacing",
|
|
1617
|
+
value: spacingValue
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
const paddings = [
|
|
1622
|
+
nodeWithStyles.paddingTop,
|
|
1623
|
+
nodeWithStyles.paddingRight,
|
|
1624
|
+
nodeWithStyles.paddingBottom,
|
|
1625
|
+
nodeWithStyles.paddingLeft
|
|
1626
|
+
];
|
|
1627
|
+
for (const padding of paddings) {
|
|
1628
|
+
if (padding !== void 0 && padding > 0) {
|
|
1629
|
+
const spacingValue = `${padding}px`;
|
|
1630
|
+
if (!seenSpacing.has(spacingValue)) {
|
|
1631
|
+
seenSpacing.add(spacingValue);
|
|
1632
|
+
const tokenName = `spacing-${padding}`;
|
|
1633
|
+
tokens.spacing[tokenName] = {
|
|
1634
|
+
name: `Spacing ${padding}`,
|
|
1635
|
+
type: "spacing",
|
|
1636
|
+
value: spacingValue
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1578
1639
|
}
|
|
1579
|
-
process.exit(1);
|
|
1580
1640
|
}
|
|
1581
1641
|
}
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1642
|
+
if ("children" in node) {
|
|
1643
|
+
const parent = node;
|
|
1644
|
+
for (const child of parent.children || []) {
|
|
1645
|
+
traverseNode(child);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
if (file.document) {
|
|
1650
|
+
traverseNode(file.document);
|
|
1651
|
+
}
|
|
1652
|
+
return tokens;
|
|
1653
|
+
}
|
|
1654
|
+
function tokensToCss(tokens) {
|
|
1655
|
+
const lines = [":root {"];
|
|
1656
|
+
if (tokens.colors && Object.keys(tokens.colors).length > 0) {
|
|
1657
|
+
lines.push(" /* Colors */");
|
|
1658
|
+
for (const [name, token] of Object.entries(tokens.colors)) {
|
|
1659
|
+
lines.push(` --color-${name}: ${token.value};`);
|
|
1660
|
+
}
|
|
1661
|
+
lines.push("");
|
|
1662
|
+
}
|
|
1663
|
+
if (tokens.typography && Object.keys(tokens.typography).length > 0) {
|
|
1664
|
+
lines.push(" /* Typography */");
|
|
1665
|
+
for (const [name, token] of Object.entries(tokens.typography)) {
|
|
1666
|
+
const value = token.value;
|
|
1667
|
+
lines.push(` --font-${name}-family: ${value.fontFamily};`);
|
|
1668
|
+
lines.push(` --font-${name}-size: ${value.fontSize};`);
|
|
1669
|
+
lines.push(` --font-${name}-weight: ${value.fontWeight};`);
|
|
1670
|
+
lines.push(` --font-${name}-line-height: ${value.lineHeight};`);
|
|
1671
|
+
lines.push(` --font-${name}-letter-spacing: ${value.letterSpacing};`);
|
|
1672
|
+
}
|
|
1673
|
+
lines.push("");
|
|
1674
|
+
}
|
|
1675
|
+
if (tokens.shadows && Object.keys(tokens.shadows).length > 0) {
|
|
1676
|
+
lines.push(" /* Shadows */");
|
|
1677
|
+
for (const [name, token] of Object.entries(tokens.shadows)) {
|
|
1678
|
+
const value = token.value;
|
|
1679
|
+
const shadowCss = shadowToCss(value);
|
|
1680
|
+
lines.push(` --shadow-${name}: ${shadowCss};`);
|
|
1681
|
+
}
|
|
1682
|
+
lines.push("");
|
|
1683
|
+
}
|
|
1684
|
+
if (tokens.radii && Object.keys(tokens.radii).length > 0) {
|
|
1685
|
+
lines.push(" /* Border Radii */");
|
|
1686
|
+
for (const [name, token] of Object.entries(tokens.radii)) {
|
|
1687
|
+
lines.push(` --${name}: ${token.value};`);
|
|
1688
|
+
}
|
|
1689
|
+
lines.push("");
|
|
1690
|
+
}
|
|
1691
|
+
if (tokens.spacing && Object.keys(tokens.spacing).length > 0) {
|
|
1692
|
+
lines.push(" /* Spacing */");
|
|
1693
|
+
for (const [name, token] of Object.entries(tokens.spacing)) {
|
|
1694
|
+
lines.push(` --${name}: ${token.value};`);
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
lines.push("}");
|
|
1698
|
+
return lines.join("\n");
|
|
1699
|
+
}
|
|
1700
|
+
function tokensToScss(tokens) {
|
|
1701
|
+
const lines = [];
|
|
1702
|
+
if (tokens.colors && Object.keys(tokens.colors).length > 0) {
|
|
1703
|
+
lines.push("// Colors");
|
|
1704
|
+
for (const [name, token] of Object.entries(tokens.colors)) {
|
|
1705
|
+
lines.push(`$color-${name}: ${token.value};`);
|
|
1706
|
+
}
|
|
1707
|
+
lines.push("");
|
|
1708
|
+
}
|
|
1709
|
+
if (tokens.typography && Object.keys(tokens.typography).length > 0) {
|
|
1710
|
+
lines.push("// Typography");
|
|
1711
|
+
for (const [name, token] of Object.entries(tokens.typography)) {
|
|
1712
|
+
const value = token.value;
|
|
1713
|
+
lines.push(`$font-${name}-family: ${value.fontFamily};`);
|
|
1714
|
+
lines.push(`$font-${name}-size: ${value.fontSize};`);
|
|
1715
|
+
lines.push(`$font-${name}-weight: ${value.fontWeight};`);
|
|
1716
|
+
lines.push(`$font-${name}-line-height: ${value.lineHeight};`);
|
|
1717
|
+
lines.push(`$font-${name}-letter-spacing: ${value.letterSpacing};`);
|
|
1718
|
+
}
|
|
1719
|
+
lines.push("");
|
|
1720
|
+
}
|
|
1721
|
+
if (tokens.shadows && Object.keys(tokens.shadows).length > 0) {
|
|
1722
|
+
lines.push("// Shadows");
|
|
1723
|
+
for (const [name, token] of Object.entries(tokens.shadows)) {
|
|
1724
|
+
const value = token.value;
|
|
1725
|
+
const shadowCss = shadowToCss(value);
|
|
1726
|
+
lines.push(`$shadow-${name}: ${shadowCss};`);
|
|
1727
|
+
}
|
|
1728
|
+
lines.push("");
|
|
1729
|
+
}
|
|
1730
|
+
if (tokens.radii && Object.keys(tokens.radii).length > 0) {
|
|
1731
|
+
lines.push("// Border Radii");
|
|
1732
|
+
for (const [name, token] of Object.entries(tokens.radii)) {
|
|
1733
|
+
lines.push(`$${name}: ${token.value};`);
|
|
1734
|
+
}
|
|
1735
|
+
lines.push("");
|
|
1736
|
+
}
|
|
1737
|
+
if (tokens.spacing && Object.keys(tokens.spacing).length > 0) {
|
|
1738
|
+
lines.push("// Spacing");
|
|
1739
|
+
for (const [name, token] of Object.entries(tokens.spacing)) {
|
|
1740
|
+
lines.push(`$${name}: ${token.value};`);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
return lines.join("\n");
|
|
1744
|
+
}
|
|
1745
|
+
function tokensToStyleDictionary(tokens) {
|
|
1746
|
+
const output = {};
|
|
1747
|
+
if (tokens.colors && Object.keys(tokens.colors).length > 0) {
|
|
1748
|
+
output.color = {};
|
|
1749
|
+
for (const [name, token] of Object.entries(tokens.colors)) {
|
|
1750
|
+
output.color[name] = {
|
|
1751
|
+
value: token.value,
|
|
1752
|
+
type: "color",
|
|
1753
|
+
description: token.description
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
if (tokens.typography && Object.keys(tokens.typography).length > 0) {
|
|
1758
|
+
output.typography = {};
|
|
1759
|
+
for (const [name, token] of Object.entries(tokens.typography)) {
|
|
1760
|
+
output.typography[name] = {
|
|
1761
|
+
value: token.value,
|
|
1762
|
+
type: "typography",
|
|
1763
|
+
description: token.description
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
if (tokens.shadows && Object.keys(tokens.shadows).length > 0) {
|
|
1768
|
+
output.shadow = {};
|
|
1769
|
+
for (const [name, token] of Object.entries(tokens.shadows)) {
|
|
1770
|
+
output.shadow[name] = {
|
|
1771
|
+
value: token.value,
|
|
1772
|
+
type: "shadow",
|
|
1773
|
+
description: token.description
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
if (tokens.radii && Object.keys(tokens.radii).length > 0) {
|
|
1778
|
+
output.borderRadius = {};
|
|
1779
|
+
for (const [name, token] of Object.entries(tokens.radii)) {
|
|
1780
|
+
output.borderRadius[name] = {
|
|
1781
|
+
value: token.value,
|
|
1782
|
+
type: "dimension",
|
|
1783
|
+
description: token.description
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
if (tokens.spacing && Object.keys(tokens.spacing).length > 0) {
|
|
1788
|
+
output.spacing = {};
|
|
1789
|
+
for (const [name, token] of Object.entries(tokens.spacing)) {
|
|
1790
|
+
output.spacing[name] = {
|
|
1791
|
+
value: token.value,
|
|
1792
|
+
type: "dimension",
|
|
1793
|
+
description: token.description
|
|
1794
|
+
};
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
return JSON.stringify(output, null, 2);
|
|
1798
|
+
}
|
|
1799
|
+
function tokensToTailwind(tokens) {
|
|
1800
|
+
const config = {
|
|
1801
|
+
theme: {
|
|
1802
|
+
extend: {}
|
|
1803
|
+
}
|
|
1804
|
+
};
|
|
1805
|
+
if (tokens.colors && Object.keys(tokens.colors).length > 0) {
|
|
1806
|
+
config.theme.extend.colors = {};
|
|
1807
|
+
for (const [name, token] of Object.entries(tokens.colors)) {
|
|
1808
|
+
config.theme.extend.colors[name] = token.value;
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
if (tokens.typography && Object.keys(tokens.typography).length > 0) {
|
|
1812
|
+
config.theme.extend.fontFamily = {};
|
|
1813
|
+
config.theme.extend.fontSize = {};
|
|
1814
|
+
for (const [name, token] of Object.entries(tokens.typography)) {
|
|
1815
|
+
const value = token.value;
|
|
1816
|
+
config.theme.extend.fontFamily[name] = [value.fontFamily];
|
|
1817
|
+
config.theme.extend.fontSize[name] = value.fontSize;
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
if (tokens.shadows && Object.keys(tokens.shadows).length > 0) {
|
|
1821
|
+
config.theme.extend.boxShadow = {};
|
|
1822
|
+
for (const [name, token] of Object.entries(tokens.shadows)) {
|
|
1823
|
+
config.theme.extend.boxShadow[name] = shadowToCss(token.value);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
if (tokens.radii && Object.keys(tokens.radii).length > 0) {
|
|
1827
|
+
config.theme.extend.borderRadius = {};
|
|
1828
|
+
for (const [name, token] of Object.entries(tokens.radii)) {
|
|
1829
|
+
config.theme.extend.borderRadius[name] = token.value;
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
if (tokens.spacing && Object.keys(tokens.spacing).length > 0) {
|
|
1833
|
+
config.theme.extend.spacing = {};
|
|
1834
|
+
for (const [name, token] of Object.entries(tokens.spacing)) {
|
|
1835
|
+
config.theme.extend.spacing[name] = token.value;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
return `/** @type {import('tailwindcss').Config} */
|
|
1839
|
+
module.exports = ${JSON.stringify(config, null, 2)};`;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
// src/cmd/file/images.ts
|
|
1843
|
+
function getNodeInfoMap(nodes, nodeIds) {
|
|
1844
|
+
const nodeInfoMap = /* @__PURE__ */ new Map();
|
|
1845
|
+
for (const nodeId of nodeIds) {
|
|
1846
|
+
const nodeData = nodes[nodeId];
|
|
1847
|
+
if (nodeData && nodeData.document) {
|
|
1848
|
+
const node = nodeData.document;
|
|
1849
|
+
const nameParts = node.name.split("/");
|
|
1850
|
+
const name = nameParts[nameParts.length - 1];
|
|
1851
|
+
const parentNames = nameParts.slice(0, -1);
|
|
1852
|
+
nodeInfoMap.set(nodeId, {
|
|
1853
|
+
name,
|
|
1854
|
+
parentNames
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
return nodeInfoMap;
|
|
1859
|
+
}
|
|
1860
|
+
function generateFilename(nodeId, nodeInfo, namingStrategy, format, isVariant, variantSuffix) {
|
|
1861
|
+
let baseName;
|
|
1862
|
+
switch (namingStrategy) {
|
|
1863
|
+
case "name":
|
|
1864
|
+
if (nodeInfo) {
|
|
1865
|
+
baseName = toKebabCase(nodeInfo.name);
|
|
1866
|
+
} else {
|
|
1867
|
+
baseName = sanitizeFilename(nodeId);
|
|
1868
|
+
}
|
|
1869
|
+
break;
|
|
1870
|
+
case "path":
|
|
1871
|
+
if (nodeInfo && nodeInfo.parentNames.length > 0) {
|
|
1872
|
+
const pathParts = [...nodeInfo.parentNames, nodeInfo.name];
|
|
1873
|
+
baseName = pathParts.map((p) => toKebabCase(p)).join("--");
|
|
1874
|
+
} else if (nodeInfo) {
|
|
1875
|
+
baseName = toKebabCase(nodeInfo.name);
|
|
1876
|
+
} else {
|
|
1877
|
+
baseName = sanitizeFilename(nodeId);
|
|
1878
|
+
}
|
|
1879
|
+
break;
|
|
1880
|
+
case "id":
|
|
1881
|
+
default:
|
|
1882
|
+
baseName = sanitizeFilename(nodeId);
|
|
1883
|
+
break;
|
|
1884
|
+
}
|
|
1885
|
+
if (isVariant && variantSuffix) {
|
|
1886
|
+
baseName = `${baseName}--${toKebabCase(variantSuffix)}`;
|
|
1887
|
+
}
|
|
1888
|
+
return `${baseName}.${format}`;
|
|
1889
|
+
}
|
|
1890
|
+
function createFileImagesCommand(factory) {
|
|
1891
|
+
const cmd = new Command7("images").description("Export images from a file").argument("<url>", "Figma URL (with node-id) or file key").option("--id <ids>", "Comma-separated node IDs (overrides URL node-id)").option("-s, --scale <scale>", "Scale factor (0.01-4)", parseFloat, 1).option("--format <format>", "Image format (jpg, png, svg, pdf)", "png").option("--svg-include-id", "Include node ID as an attribute in SVGs").option("--svg-simplify-stroke", "Simplify strokes in SVG export").option("-o, --output <dir>", "Output directory for downloaded images").option(
|
|
1892
|
+
"-O, --output-format <format>",
|
|
1893
|
+
"Output format (json, table)",
|
|
1894
|
+
"json"
|
|
1895
|
+
).option(
|
|
1896
|
+
"--naming <strategy>",
|
|
1897
|
+
"Naming strategy: id, name, or path (default: id)",
|
|
1898
|
+
"id"
|
|
1899
|
+
).option("-t, --token <token>", "Override authentication token").action(
|
|
1900
|
+
async (url, options) => {
|
|
1901
|
+
const { io } = factory;
|
|
1902
|
+
const validFormats = ["jpg", "png", "svg", "pdf"];
|
|
1903
|
+
if (!validFormats.includes(options.format)) {
|
|
1904
|
+
io.err.write(
|
|
1905
|
+
chalk7.red(
|
|
1906
|
+
`Invalid format "${options.format}". Valid formats: ${validFormats.join(", ")}
|
|
1907
|
+
`
|
|
1908
|
+
)
|
|
1909
|
+
);
|
|
1910
|
+
process.exit(1);
|
|
1911
|
+
}
|
|
1912
|
+
const imageFormat = options.format;
|
|
1913
|
+
if (isNaN(options.scale) || options.scale < 0.01 || options.scale > 4) {
|
|
1914
|
+
io.err.write(
|
|
1915
|
+
chalk7.red(
|
|
1916
|
+
`Invalid scale "${options.scale}". Scale must be between 0.01 and 4.
|
|
1917
|
+
`
|
|
1918
|
+
)
|
|
1919
|
+
);
|
|
1920
|
+
process.exit(1);
|
|
1921
|
+
}
|
|
1922
|
+
const validNamingStrategies = ["id", "name", "path"];
|
|
1923
|
+
if (!validNamingStrategies.includes(options.naming)) {
|
|
1924
|
+
io.err.write(
|
|
1925
|
+
chalk7.red(
|
|
1926
|
+
`Invalid naming strategy "${options.naming}". Valid strategies: ${validNamingStrategies.join(", ")}
|
|
1927
|
+
`
|
|
1928
|
+
)
|
|
1929
|
+
);
|
|
1930
|
+
process.exit(1);
|
|
1931
|
+
}
|
|
1932
|
+
const namingStrategy = options.naming;
|
|
1933
|
+
try {
|
|
1934
|
+
const parsed = parseFigmaInput(url);
|
|
1935
|
+
const fileKey = parsed.fileKey;
|
|
1936
|
+
let nodeIds;
|
|
1937
|
+
if (options.id) {
|
|
1938
|
+
nodeIds = parseNodeIds(options.id);
|
|
1939
|
+
} else if (parsed.nodeId) {
|
|
1940
|
+
nodeIds = [parsed.nodeId];
|
|
1941
|
+
} else {
|
|
1942
|
+
io.err.write(
|
|
1943
|
+
chalk7.red(
|
|
1944
|
+
"Error: node ID required. Use a URL with node-id or --id flag.\n"
|
|
1945
|
+
)
|
|
1946
|
+
);
|
|
1947
|
+
process.exit(1);
|
|
1948
|
+
}
|
|
1949
|
+
const client = await factory.getClient();
|
|
1950
|
+
let activeClient = client;
|
|
1951
|
+
if (options.token) {
|
|
1952
|
+
const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
1953
|
+
activeClient = new FigmaClient2({ token: options.token });
|
|
1954
|
+
}
|
|
1955
|
+
const images = await activeClient.getImages(fileKey, nodeIds, {
|
|
1956
|
+
scale: options.scale,
|
|
1957
|
+
format: imageFormat,
|
|
1958
|
+
svg_include_id: options.svgIncludeId,
|
|
1959
|
+
svg_simplify_stroke: options.svgSimplifyStroke
|
|
1960
|
+
});
|
|
1961
|
+
if (options.output) {
|
|
1962
|
+
const imageUrls = images.images || {};
|
|
1963
|
+
if (Object.keys(imageUrls).length === 0) {
|
|
1964
|
+
io.err.write(
|
|
1965
|
+
chalk7.yellow(
|
|
1966
|
+
"Warning: No image URLs returned from Figma API.\n"
|
|
1967
|
+
)
|
|
1968
|
+
);
|
|
1969
|
+
}
|
|
1970
|
+
let nodeInfoMap = /* @__PURE__ */ new Map();
|
|
1971
|
+
if (namingStrategy === "name" || namingStrategy === "path") {
|
|
1972
|
+
try {
|
|
1973
|
+
const nodesResponse = await activeClient.getFileNodes(
|
|
1974
|
+
fileKey,
|
|
1975
|
+
nodeIds
|
|
1976
|
+
);
|
|
1977
|
+
nodeInfoMap = getNodeInfoMap(nodesResponse.nodes, nodeIds);
|
|
1978
|
+
} catch (fetchError) {
|
|
1979
|
+
const errorMsg = fetchError instanceof Error ? fetchError.message : String(fetchError);
|
|
1980
|
+
io.err.write(
|
|
1981
|
+
chalk7.yellow(
|
|
1982
|
+
`Warning: Could not fetch node names (${errorMsg}), falling back to ID naming.
|
|
1983
|
+
`
|
|
1984
|
+
)
|
|
1985
|
+
);
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
const downloadItems = Object.entries(imageUrls).map(
|
|
1989
|
+
([nodeId, url2]) => {
|
|
1990
|
+
const nodeInfo = nodeInfoMap.get(nodeId);
|
|
1991
|
+
const filename = generateFilename(
|
|
1992
|
+
nodeId,
|
|
1993
|
+
nodeInfo,
|
|
1994
|
+
namingStrategy,
|
|
1995
|
+
imageFormat,
|
|
1996
|
+
false
|
|
1997
|
+
);
|
|
1998
|
+
return {
|
|
1999
|
+
id: nodeId,
|
|
2000
|
+
url: url2 ?? null,
|
|
2001
|
+
filename
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
2004
|
+
);
|
|
2005
|
+
const results = await downloadFiles(downloadItems, options.output);
|
|
2006
|
+
const failures = results.filter((r) => r.status === "error");
|
|
2007
|
+
const downloadResults = results.map((r) => ({
|
|
2008
|
+
nodeId: r.id,
|
|
2009
|
+
file: r.file,
|
|
2010
|
+
status: r.status === "success" ? "downloaded" : r.error ?? "error"
|
|
2011
|
+
}));
|
|
2012
|
+
io.out.write(
|
|
2013
|
+
formatOutput(downloadResults, {
|
|
2014
|
+
format: options.outputFormat
|
|
2015
|
+
})
|
|
2016
|
+
);
|
|
2017
|
+
io.out.write("\n");
|
|
2018
|
+
if (failures.length > 0) {
|
|
2019
|
+
io.err.write(
|
|
2020
|
+
chalk7.yellow(
|
|
2021
|
+
`${failures.length} image(s) failed to download.
|
|
2022
|
+
`
|
|
2023
|
+
)
|
|
2024
|
+
);
|
|
2025
|
+
process.exit(1);
|
|
2026
|
+
}
|
|
2027
|
+
} else {
|
|
2028
|
+
io.out.write(
|
|
2029
|
+
formatOutput(images, {
|
|
2030
|
+
format: options.outputFormat
|
|
2031
|
+
})
|
|
2032
|
+
);
|
|
2033
|
+
io.out.write("\n");
|
|
2034
|
+
}
|
|
2035
|
+
} catch (error) {
|
|
2036
|
+
io.err.write(chalk7.red("Error exporting images.\n"));
|
|
2037
|
+
if (error instanceof Error) {
|
|
2038
|
+
io.err.write(chalk7.dim(`${error.message}
|
|
2039
|
+
`));
|
|
2040
|
+
}
|
|
2041
|
+
process.exit(1);
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
);
|
|
2045
|
+
return cmd;
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
// src/cmd/file/structure.ts
|
|
2049
|
+
import { Command as Command8 } from "commander";
|
|
2050
|
+
import chalk8 from "chalk";
|
|
2051
|
+
import * as fs4 from "fs";
|
|
2052
|
+
import * as path4 from "path";
|
|
2053
|
+
function extractFrameInfo(node, currentDepth, maxDepth, includeHidden) {
|
|
2054
|
+
const nodeWithBounds = node;
|
|
2055
|
+
if (!includeHidden && nodeWithBounds.visible === false) {
|
|
2056
|
+
return null;
|
|
2057
|
+
}
|
|
2058
|
+
const relevantTypes = [
|
|
2059
|
+
"FRAME",
|
|
2060
|
+
"COMPONENT",
|
|
2061
|
+
"COMPONENT_SET",
|
|
2062
|
+
"SECTION",
|
|
2063
|
+
"GROUP",
|
|
2064
|
+
"INSTANCE"
|
|
2065
|
+
];
|
|
2066
|
+
if (!relevantTypes.includes(node.type)) {
|
|
2067
|
+
return null;
|
|
2068
|
+
}
|
|
2069
|
+
const frame = {
|
|
2070
|
+
id: node.id,
|
|
2071
|
+
name: node.name,
|
|
2072
|
+
type: node.type
|
|
2073
|
+
};
|
|
2074
|
+
if (nodeWithBounds.absoluteBoundingBox) {
|
|
2075
|
+
frame.width = Math.round(nodeWithBounds.absoluteBoundingBox.width);
|
|
2076
|
+
frame.height = Math.round(nodeWithBounds.absoluteBoundingBox.height);
|
|
2077
|
+
}
|
|
2078
|
+
if (currentDepth < maxDepth && "children" in node) {
|
|
2079
|
+
const parent = node;
|
|
2080
|
+
const children = (parent.children || []).map(
|
|
2081
|
+
(child) => extractFrameInfo(child, currentDepth + 1, maxDepth, includeHidden)
|
|
2082
|
+
).filter((child) => child !== null);
|
|
2083
|
+
if (children.length > 0) {
|
|
2084
|
+
frame.children = children;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
return frame;
|
|
2088
|
+
}
|
|
2089
|
+
function extractStructure(file, maxDepth, includeHidden) {
|
|
2090
|
+
const pages = [];
|
|
2091
|
+
if (file.document && "children" in file.document) {
|
|
2092
|
+
const doc = file.document;
|
|
2093
|
+
for (const pageNode of doc.children || []) {
|
|
2094
|
+
if (pageNode.type !== "CANVAS") continue;
|
|
2095
|
+
const page = {
|
|
2096
|
+
id: pageNode.id,
|
|
2097
|
+
name: pageNode.name,
|
|
2098
|
+
frames: []
|
|
2099
|
+
};
|
|
2100
|
+
if ("children" in pageNode) {
|
|
2101
|
+
const pageWithChildren = pageNode;
|
|
2102
|
+
for (const child of pageWithChildren.children || []) {
|
|
2103
|
+
const frame = extractFrameInfo(child, 1, maxDepth, includeHidden);
|
|
2104
|
+
if (frame) {
|
|
2105
|
+
page.frames.push(frame);
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
pages.push(page);
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
return {
|
|
2113
|
+
name: file.name,
|
|
2114
|
+
lastModified: file.lastModified,
|
|
2115
|
+
version: file.version,
|
|
2116
|
+
pages
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
function formatTree(structure) {
|
|
2120
|
+
const lines = [];
|
|
2121
|
+
lines.push(`\u{1F4C1} ${structure.name}`);
|
|
2122
|
+
lines.push(` Last Modified: ${structure.lastModified}`);
|
|
2123
|
+
lines.push("");
|
|
2124
|
+
for (let i = 0; i < structure.pages.length; i++) {
|
|
2125
|
+
const page = structure.pages[i];
|
|
2126
|
+
const isLastPage = i === structure.pages.length - 1;
|
|
2127
|
+
const pagePrefix = isLastPage ? "\u2514\u2500\u2500" : "\u251C\u2500\u2500";
|
|
2128
|
+
lines.push(`${pagePrefix} \u{1F4C4} ${page.name} (${page.id})`);
|
|
2129
|
+
const indent = isLastPage ? " " : "\u2502 ";
|
|
2130
|
+
formatFramesTree(page.frames, indent, lines);
|
|
2131
|
+
}
|
|
2132
|
+
return lines.join("\n");
|
|
2133
|
+
}
|
|
2134
|
+
function formatFramesTree(frames, indent, lines) {
|
|
2135
|
+
for (let i = 0; i < frames.length; i++) {
|
|
2136
|
+
const frame = frames[i];
|
|
2137
|
+
const isLast = i === frames.length - 1;
|
|
2138
|
+
const prefix = isLast ? "\u2514\u2500\u2500" : "\u251C\u2500\u2500";
|
|
2139
|
+
const icon = getTypeIcon(frame.type);
|
|
2140
|
+
const sizeInfo = frame.width && frame.height ? ` (${frame.width}\xD7${frame.height})` : "";
|
|
2141
|
+
lines.push(`${indent}${prefix} ${icon} ${frame.name}${sizeInfo}`);
|
|
2142
|
+
if (frame.children && frame.children.length > 0) {
|
|
2143
|
+
const childIndent = indent + (isLast ? " " : "\u2502 ");
|
|
2144
|
+
formatFramesTree(frame.children, childIndent, lines);
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
function getTypeIcon(type) {
|
|
2149
|
+
switch (type) {
|
|
2150
|
+
case "COMPONENT":
|
|
2151
|
+
return "\u{1F537}";
|
|
2152
|
+
case "COMPONENT_SET":
|
|
2153
|
+
return "\u{1F536}";
|
|
2154
|
+
case "FRAME":
|
|
2155
|
+
return "\u2B1C";
|
|
2156
|
+
case "SECTION":
|
|
2157
|
+
return "\u{1F4C2}";
|
|
2158
|
+
case "GROUP":
|
|
2159
|
+
return "\u{1F4E6}";
|
|
2160
|
+
case "INSTANCE":
|
|
2161
|
+
return "\u25C7";
|
|
2162
|
+
default:
|
|
2163
|
+
return "\u25AB";
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
function createFileStructureCommand(factory) {
|
|
2167
|
+
const cmd = new Command8("structure").description("Get simplified file structure").argument("<url>", "Figma URL or file key").option("-d, --depth <depth>", "Max depth of hierarchy (default: 2)", "2").option("--include-hidden", "Include hidden nodes", false).option("-f, --format <format>", "Output format (json, tree)", "json").option("-o, --output <file>", "Write output to file").option("-t, --token <token>", "Override authentication token").action(
|
|
2168
|
+
async (url, options) => {
|
|
2169
|
+
const { io } = factory;
|
|
2170
|
+
try {
|
|
2171
|
+
const parsed = parseFigmaInput(url);
|
|
2172
|
+
const fileKey = parsed.fileKey;
|
|
2173
|
+
const maxDepth = parseInt(options.depth, 10);
|
|
2174
|
+
if (isNaN(maxDepth) || maxDepth < 1) {
|
|
2175
|
+
io.err.write(
|
|
2176
|
+
chalk8.red(
|
|
2177
|
+
`Invalid depth "${options.depth}". Depth must be a positive integer.
|
|
2178
|
+
`
|
|
2179
|
+
)
|
|
2180
|
+
);
|
|
2181
|
+
process.exit(1);
|
|
2182
|
+
}
|
|
2183
|
+
const validFormats = ["json", "tree"];
|
|
2184
|
+
if (!validFormats.includes(
|
|
2185
|
+
options.format
|
|
2186
|
+
)) {
|
|
2187
|
+
io.err.write(
|
|
2188
|
+
chalk8.red(
|
|
2189
|
+
`Invalid format "${options.format}". Valid formats: ${validFormats.join(", ")}
|
|
2190
|
+
`
|
|
2191
|
+
)
|
|
2192
|
+
);
|
|
2193
|
+
process.exit(1);
|
|
2194
|
+
}
|
|
2195
|
+
const client = await factory.getClient();
|
|
2196
|
+
let activeClient = client;
|
|
2197
|
+
if (options.token) {
|
|
2198
|
+
const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
2199
|
+
activeClient = new FigmaClient2({ token: options.token });
|
|
2200
|
+
}
|
|
2201
|
+
const file = await activeClient.getFile(fileKey, {
|
|
2202
|
+
depth: maxDepth + 1
|
|
2203
|
+
// +1 to include top-level frames
|
|
2204
|
+
});
|
|
2205
|
+
const structure = extractStructure(
|
|
2206
|
+
file,
|
|
2207
|
+
maxDepth,
|
|
2208
|
+
options.includeHidden
|
|
2209
|
+
);
|
|
2210
|
+
let output;
|
|
2211
|
+
if (options.format === "tree") {
|
|
2212
|
+
output = formatTree(structure);
|
|
2213
|
+
} else {
|
|
2214
|
+
output = JSON.stringify(structure, null, 2);
|
|
2215
|
+
}
|
|
2216
|
+
if (options.output) {
|
|
2217
|
+
const outputPath = path4.resolve(options.output);
|
|
2218
|
+
fs4.writeFileSync(outputPath, output + "\n");
|
|
2219
|
+
io.out.write(chalk8.green(`Structure written to ${outputPath}
|
|
2220
|
+
`));
|
|
2221
|
+
} else {
|
|
2222
|
+
io.out.write(output + "\n");
|
|
2223
|
+
}
|
|
2224
|
+
} catch (error) {
|
|
2225
|
+
io.err.write(chalk8.red("Error fetching file structure.\n"));
|
|
2226
|
+
if (error instanceof Error) {
|
|
2227
|
+
io.err.write(chalk8.dim(`${error.message}
|
|
2228
|
+
`));
|
|
2229
|
+
}
|
|
2230
|
+
process.exit(1);
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
);
|
|
2234
|
+
return cmd;
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
// src/cmd/file/index.ts
|
|
2238
|
+
function createFileCommand(factory) {
|
|
2239
|
+
const cmd = new Command9("file").description("Work with Figma files");
|
|
2240
|
+
cmd.addCommand(createFileGetCommand(factory));
|
|
2241
|
+
cmd.addCommand(createFileNodesCommand(factory));
|
|
2242
|
+
cmd.addCommand(createFileImagesCommand(factory));
|
|
2243
|
+
cmd.addCommand(createFileStructureCommand(factory));
|
|
2244
|
+
return cmd;
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
// src/cmd/project/index.ts
|
|
2248
|
+
import { Command as Command12 } from "commander";
|
|
2249
|
+
|
|
2250
|
+
// src/cmd/project/list.ts
|
|
2251
|
+
import { Command as Command10 } from "commander";
|
|
2252
|
+
import chalk9 from "chalk";
|
|
2253
|
+
function createProjectListCommand(factory) {
|
|
2254
|
+
const cmd = new Command10("list").description("List team projects").requiredOption("--team <id>", "Team ID").option("-f, --format <format>", "Output format (json, table)", "json").option("-t, --token <token>", "Override authentication token").action(
|
|
2255
|
+
async (options) => {
|
|
2256
|
+
const { io } = factory;
|
|
2257
|
+
try {
|
|
2258
|
+
const token = await factory.getToken(options.token);
|
|
2259
|
+
let client = await factory.getClient();
|
|
2260
|
+
if (options.token) {
|
|
2261
|
+
const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
2262
|
+
client = new FigmaClient2({ token });
|
|
2263
|
+
}
|
|
2264
|
+
const projects = await client.getTeamProjects(options.team);
|
|
2265
|
+
if (options.format === "table") {
|
|
2266
|
+
const tableData = projects.projects.map((p) => ({
|
|
2267
|
+
id: p.id,
|
|
2268
|
+
name: p.name
|
|
2269
|
+
}));
|
|
2270
|
+
io.out.write(
|
|
2271
|
+
formatOutput(tableData, {
|
|
2272
|
+
format: "table",
|
|
2273
|
+
columns: ["id", "name"]
|
|
2274
|
+
})
|
|
2275
|
+
);
|
|
2276
|
+
} else {
|
|
2277
|
+
io.out.write(formatOutput(projects, { format: "json" }));
|
|
2278
|
+
}
|
|
2279
|
+
io.out.write("\n");
|
|
2280
|
+
} catch (error) {
|
|
2281
|
+
io.err.write(chalk9.red("Error fetching projects.\n"));
|
|
2282
|
+
if (error instanceof Error) {
|
|
2283
|
+
io.err.write(chalk9.dim(`${error.message}
|
|
2284
|
+
`));
|
|
2285
|
+
}
|
|
2286
|
+
process.exit(1);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
);
|
|
2290
|
+
return cmd;
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
// src/cmd/project/files.ts
|
|
2294
|
+
import { Command as Command11 } from "commander";
|
|
2295
|
+
import chalk10 from "chalk";
|
|
2296
|
+
function createProjectFilesCommand(factory) {
|
|
2297
|
+
const cmd = new Command11("files").description("List files in a project").argument("<project-id>", "Project ID").option("--branch-data", "Include branch metadata").option("-f, --format <format>", "Output format (json, table)", "json").option("-t, --token <token>", "Override authentication token").action(
|
|
2298
|
+
async (projectId, options) => {
|
|
2299
|
+
const { io } = factory;
|
|
2300
|
+
try {
|
|
2301
|
+
const token = await factory.getToken(options.token);
|
|
2302
|
+
let client = await factory.getClient();
|
|
2303
|
+
if (options.token) {
|
|
2304
|
+
const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
2305
|
+
client = new FigmaClient2({ token });
|
|
2306
|
+
}
|
|
2307
|
+
const files = await client.getProjectFiles(projectId, {
|
|
2308
|
+
branch_data: options.branchData
|
|
2309
|
+
});
|
|
2310
|
+
if (options.format === "table") {
|
|
2311
|
+
const tableData = files.files.map((f) => ({
|
|
2312
|
+
key: f.key,
|
|
2313
|
+
name: f.name,
|
|
2314
|
+
last_modified: f.last_modified
|
|
2315
|
+
}));
|
|
2316
|
+
io.out.write(
|
|
2317
|
+
formatOutput(tableData, {
|
|
2318
|
+
format: "table",
|
|
2319
|
+
columns: ["key", "name", "last_modified"]
|
|
2320
|
+
})
|
|
2321
|
+
);
|
|
2322
|
+
} else {
|
|
2323
|
+
io.out.write(formatOutput(files, { format: "json" }));
|
|
2324
|
+
}
|
|
2325
|
+
io.out.write("\n");
|
|
2326
|
+
} catch (error) {
|
|
2327
|
+
io.err.write(chalk10.red("Error fetching project files.\n"));
|
|
2328
|
+
if (error instanceof Error) {
|
|
2329
|
+
io.err.write(chalk10.dim(`${error.message}
|
|
2330
|
+
`));
|
|
2331
|
+
}
|
|
2332
|
+
process.exit(1);
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
);
|
|
2336
|
+
return cmd;
|
|
2337
|
+
}
|
|
2338
|
+
|
|
1586
2339
|
// src/cmd/project/index.ts
|
|
1587
2340
|
function createProjectCommand(factory) {
|
|
1588
|
-
const cmd = new
|
|
2341
|
+
const cmd = new Command12("project").description("Work with Figma projects");
|
|
1589
2342
|
cmd.addCommand(createProjectListCommand(factory));
|
|
1590
2343
|
cmd.addCommand(createProjectFilesCommand(factory));
|
|
1591
2344
|
return cmd;
|
|
1592
2345
|
}
|
|
1593
2346
|
|
|
1594
2347
|
// src/cmd/component/index.ts
|
|
1595
|
-
import { Command as
|
|
2348
|
+
import { Command as Command15 } from "commander";
|
|
1596
2349
|
|
|
1597
2350
|
// src/cmd/component/list.ts
|
|
1598
|
-
import { Command as
|
|
1599
|
-
import
|
|
2351
|
+
import { Command as Command13 } from "commander";
|
|
2352
|
+
import chalk11 from "chalk";
|
|
1600
2353
|
function createComponentListCommand(factory) {
|
|
1601
|
-
const cmd = new
|
|
2354
|
+
const cmd = new Command13("list").description("List components (from team or file)").option("--team <id>", "Team ID to list components from").option("--file <url>", "Figma URL or file key").option("--page-size <size>", "Number of results per page", parseInt).option("-f, --format <format>", "Output format (json, table)", "json").option("-t, --token <token>", "Override authentication token").action(
|
|
1602
2355
|
async (options) => {
|
|
1603
2356
|
const { io } = factory;
|
|
1604
2357
|
if (!options.team && !options.file) {
|
|
1605
2358
|
io.err.write(
|
|
1606
|
-
|
|
2359
|
+
chalk11.red("Error: Either --team or --file must be specified.\n")
|
|
1607
2360
|
);
|
|
1608
2361
|
process.exit(1);
|
|
1609
2362
|
}
|
|
@@ -1634,7 +2387,8 @@ function createComponentListCommand(factory) {
|
|
|
1634
2387
|
io.out.write(formatOutput(components, { format: "json" }));
|
|
1635
2388
|
}
|
|
1636
2389
|
} else if (options.file) {
|
|
1637
|
-
const
|
|
2390
|
+
const parsed = parseFigmaInput(options.file);
|
|
2391
|
+
const components = await client.getFileComponents(parsed.fileKey);
|
|
1638
2392
|
if (options.format === "table") {
|
|
1639
2393
|
const tableData = components.meta?.components?.map((c) => ({
|
|
1640
2394
|
key: c.key,
|
|
@@ -1653,9 +2407,9 @@ function createComponentListCommand(factory) {
|
|
|
1653
2407
|
}
|
|
1654
2408
|
io.out.write("\n");
|
|
1655
2409
|
} catch (error) {
|
|
1656
|
-
io.err.write(
|
|
2410
|
+
io.err.write(chalk11.red("Error fetching components.\n"));
|
|
1657
2411
|
if (error instanceof Error) {
|
|
1658
|
-
io.err.write(
|
|
2412
|
+
io.err.write(chalk11.dim(`${error.message}
|
|
1659
2413
|
`));
|
|
1660
2414
|
}
|
|
1661
2415
|
process.exit(1);
|
|
@@ -1666,10 +2420,10 @@ function createComponentListCommand(factory) {
|
|
|
1666
2420
|
}
|
|
1667
2421
|
|
|
1668
2422
|
// src/cmd/component/get.ts
|
|
1669
|
-
import { Command as
|
|
1670
|
-
import
|
|
2423
|
+
import { Command as Command14 } from "commander";
|
|
2424
|
+
import chalk12 from "chalk";
|
|
1671
2425
|
function createComponentGetCommand(factory) {
|
|
1672
|
-
const cmd = new
|
|
2426
|
+
const cmd = new Command14("get").description("Get component details").argument("<key>", "Component key").option("-f, --format <format>", "Output format (json, table)", "json").option("-t, --token <token>", "Override authentication token").action(
|
|
1673
2427
|
async (componentKey, options) => {
|
|
1674
2428
|
const { io } = factory;
|
|
1675
2429
|
try {
|
|
@@ -1687,9 +2441,9 @@ function createComponentGetCommand(factory) {
|
|
|
1687
2441
|
);
|
|
1688
2442
|
io.out.write("\n");
|
|
1689
2443
|
} catch (error) {
|
|
1690
|
-
io.err.write(
|
|
2444
|
+
io.err.write(chalk12.red("Error fetching component.\n"));
|
|
1691
2445
|
if (error instanceof Error) {
|
|
1692
|
-
io.err.write(
|
|
2446
|
+
io.err.write(chalk12.dim(`${error.message}
|
|
1693
2447
|
`));
|
|
1694
2448
|
}
|
|
1695
2449
|
process.exit(1);
|
|
@@ -1701,7 +2455,7 @@ function createComponentGetCommand(factory) {
|
|
|
1701
2455
|
|
|
1702
2456
|
// src/cmd/component/index.ts
|
|
1703
2457
|
function createComponentCommand(factory) {
|
|
1704
|
-
const cmd = new
|
|
2458
|
+
const cmd = new Command15("component").description(
|
|
1705
2459
|
"Work with Figma components"
|
|
1706
2460
|
);
|
|
1707
2461
|
cmd.addCommand(createComponentListCommand(factory));
|
|
@@ -1710,18 +2464,18 @@ function createComponentCommand(factory) {
|
|
|
1710
2464
|
}
|
|
1711
2465
|
|
|
1712
2466
|
// src/cmd/style/index.ts
|
|
1713
|
-
import { Command as
|
|
2467
|
+
import { Command as Command18 } from "commander";
|
|
1714
2468
|
|
|
1715
2469
|
// src/cmd/style/list.ts
|
|
1716
|
-
import { Command as
|
|
1717
|
-
import
|
|
2470
|
+
import { Command as Command16 } from "commander";
|
|
2471
|
+
import chalk13 from "chalk";
|
|
1718
2472
|
function createStyleListCommand(factory) {
|
|
1719
|
-
const cmd = new
|
|
2473
|
+
const cmd = new Command16("list").description("List styles (from team or file)").option("--team <id>", "Team ID to list styles from").option("--file <url>", "Figma URL or file key").option("--page-size <size>", "Number of results per page", parseInt).option("-f, --format <format>", "Output format (json, table)", "json").option("-t, --token <token>", "Override authentication token").action(
|
|
1720
2474
|
async (options) => {
|
|
1721
2475
|
const { io } = factory;
|
|
1722
2476
|
if (!options.team && !options.file) {
|
|
1723
2477
|
io.err.write(
|
|
1724
|
-
|
|
2478
|
+
chalk13.red("Error: Either --team or --file must be specified.\n")
|
|
1725
2479
|
);
|
|
1726
2480
|
process.exit(1);
|
|
1727
2481
|
}
|
|
@@ -1753,7 +2507,8 @@ function createStyleListCommand(factory) {
|
|
|
1753
2507
|
io.out.write(formatOutput(styles, { format: "json" }));
|
|
1754
2508
|
}
|
|
1755
2509
|
} else if (options.file) {
|
|
1756
|
-
const
|
|
2510
|
+
const parsed = parseFigmaInput(options.file);
|
|
2511
|
+
const styles = await client.getFileStyles(parsed.fileKey);
|
|
1757
2512
|
if (options.format === "table") {
|
|
1758
2513
|
const tableData = styles.meta?.styles?.map((s) => ({
|
|
1759
2514
|
key: s.key,
|
|
@@ -1773,9 +2528,9 @@ function createStyleListCommand(factory) {
|
|
|
1773
2528
|
}
|
|
1774
2529
|
io.out.write("\n");
|
|
1775
2530
|
} catch (error) {
|
|
1776
|
-
io.err.write(
|
|
2531
|
+
io.err.write(chalk13.red("Error fetching styles.\n"));
|
|
1777
2532
|
if (error instanceof Error) {
|
|
1778
|
-
io.err.write(
|
|
2533
|
+
io.err.write(chalk13.dim(`${error.message}
|
|
1779
2534
|
`));
|
|
1780
2535
|
}
|
|
1781
2536
|
process.exit(1);
|
|
@@ -1786,10 +2541,10 @@ function createStyleListCommand(factory) {
|
|
|
1786
2541
|
}
|
|
1787
2542
|
|
|
1788
2543
|
// src/cmd/style/get.ts
|
|
1789
|
-
import { Command as
|
|
1790
|
-
import
|
|
2544
|
+
import { Command as Command17 } from "commander";
|
|
2545
|
+
import chalk14 from "chalk";
|
|
1791
2546
|
function createStyleGetCommand(factory) {
|
|
1792
|
-
const cmd = new
|
|
2547
|
+
const cmd = new Command17("get").description("Get style details").argument("<key>", "Style key").option("-f, --format <format>", "Output format (json, table)", "json").option("-t, --token <token>", "Override authentication token").action(
|
|
1793
2548
|
async (styleKey, options) => {
|
|
1794
2549
|
const { io } = factory;
|
|
1795
2550
|
try {
|
|
@@ -1807,9 +2562,9 @@ function createStyleGetCommand(factory) {
|
|
|
1807
2562
|
);
|
|
1808
2563
|
io.out.write("\n");
|
|
1809
2564
|
} catch (error) {
|
|
1810
|
-
io.err.write(
|
|
2565
|
+
io.err.write(chalk14.red("Error fetching style.\n"));
|
|
1811
2566
|
if (error instanceof Error) {
|
|
1812
|
-
io.err.write(
|
|
2567
|
+
io.err.write(chalk14.dim(`${error.message}
|
|
1813
2568
|
`));
|
|
1814
2569
|
}
|
|
1815
2570
|
process.exit(1);
|
|
@@ -1821,23 +2576,25 @@ function createStyleGetCommand(factory) {
|
|
|
1821
2576
|
|
|
1822
2577
|
// src/cmd/style/index.ts
|
|
1823
2578
|
function createStyleCommand(factory) {
|
|
1824
|
-
const cmd = new
|
|
2579
|
+
const cmd = new Command18("style").description("Work with Figma styles");
|
|
1825
2580
|
cmd.addCommand(createStyleListCommand(factory));
|
|
1826
2581
|
cmd.addCommand(createStyleGetCommand(factory));
|
|
1827
2582
|
return cmd;
|
|
1828
2583
|
}
|
|
1829
2584
|
|
|
1830
2585
|
// src/cmd/variable/index.ts
|
|
1831
|
-
import { Command as
|
|
2586
|
+
import { Command as Command21 } from "commander";
|
|
1832
2587
|
|
|
1833
2588
|
// src/cmd/variable/list.ts
|
|
1834
|
-
import { Command as
|
|
1835
|
-
import
|
|
2589
|
+
import { Command as Command19 } from "commander";
|
|
2590
|
+
import chalk15 from "chalk";
|
|
1836
2591
|
function createVariableListCommand(factory) {
|
|
1837
|
-
const cmd = new
|
|
1838
|
-
async (
|
|
2592
|
+
const cmd = new Command19("list").description("List variables from a file (Enterprise)").argument("<url>", "Figma URL or file key").option("--published", "List published variables only").option("-f, --format <format>", "Output format (json, table)", "json").option("-t, --token <token>", "Override authentication token").action(
|
|
2593
|
+
async (url, options) => {
|
|
1839
2594
|
const { io } = factory;
|
|
1840
2595
|
try {
|
|
2596
|
+
const parsed = parseFigmaInput(url);
|
|
2597
|
+
const fileKey = parsed.fileKey;
|
|
1841
2598
|
const token = await factory.getToken(options.token);
|
|
1842
2599
|
let client = await factory.getClient();
|
|
1843
2600
|
if (options.token) {
|
|
@@ -1865,13 +2622,13 @@ function createVariableListCommand(factory) {
|
|
|
1865
2622
|
}
|
|
1866
2623
|
io.out.write("\n");
|
|
1867
2624
|
} catch (error) {
|
|
1868
|
-
io.err.write(
|
|
2625
|
+
io.err.write(chalk15.red("Error fetching variables.\n"));
|
|
1869
2626
|
if (error instanceof Error) {
|
|
1870
|
-
io.err.write(
|
|
2627
|
+
io.err.write(chalk15.dim(`${error.message}
|
|
1871
2628
|
`));
|
|
1872
2629
|
}
|
|
1873
2630
|
io.err.write(
|
|
1874
|
-
|
|
2631
|
+
chalk15.dim("\nNote: Variables API requires Figma Enterprise plan.\n")
|
|
1875
2632
|
);
|
|
1876
2633
|
process.exit(1);
|
|
1877
2634
|
}
|
|
@@ -1881,28 +2638,115 @@ function createVariableListCommand(factory) {
|
|
|
1881
2638
|
}
|
|
1882
2639
|
|
|
1883
2640
|
// src/cmd/variable/export.ts
|
|
1884
|
-
import { Command as
|
|
1885
|
-
import
|
|
1886
|
-
import * as
|
|
2641
|
+
import { Command as Command20 } from "commander";
|
|
2642
|
+
import chalk16 from "chalk";
|
|
2643
|
+
import * as fs5 from "fs";
|
|
2644
|
+
var SCOPE_CATEGORIES = {
|
|
2645
|
+
colors: [
|
|
2646
|
+
"ALL_FILLS",
|
|
2647
|
+
"FRAME_FILL",
|
|
2648
|
+
"SHAPE_FILL",
|
|
2649
|
+
"TEXT_FILL",
|
|
2650
|
+
"STROKE_COLOR"
|
|
2651
|
+
],
|
|
2652
|
+
spacing: ["GAP", "WIDTH_HEIGHT"],
|
|
2653
|
+
radius: ["CORNER_RADIUS"],
|
|
2654
|
+
effects: ["EFFECT_COLOR"],
|
|
2655
|
+
typography: [
|
|
2656
|
+
"FONT_FAMILY",
|
|
2657
|
+
"FONT_SIZE",
|
|
2658
|
+
"FONT_WEIGHT",
|
|
2659
|
+
"LINE_HEIGHT",
|
|
2660
|
+
"LETTER_SPACING"
|
|
2661
|
+
],
|
|
2662
|
+
other: []
|
|
2663
|
+
};
|
|
2664
|
+
function categorizeVariables(variables) {
|
|
2665
|
+
const categorized = {
|
|
2666
|
+
colors: [],
|
|
2667
|
+
spacing: [],
|
|
2668
|
+
radius: [],
|
|
2669
|
+
effects: [],
|
|
2670
|
+
typography: [],
|
|
2671
|
+
other: []
|
|
2672
|
+
};
|
|
2673
|
+
for (const variable of Object.values(variables)) {
|
|
2674
|
+
const scopes = variable.scopes || [];
|
|
2675
|
+
let placed = false;
|
|
2676
|
+
for (const [category, categoryScopes] of Object.entries(SCOPE_CATEGORIES)) {
|
|
2677
|
+
if (category === "other") continue;
|
|
2678
|
+
if (scopes.some((scope) => categoryScopes.includes(scope))) {
|
|
2679
|
+
categorized[category].push(variable);
|
|
2680
|
+
placed = true;
|
|
2681
|
+
break;
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
if (!placed) {
|
|
2685
|
+
switch (variable.resolvedType) {
|
|
2686
|
+
case "COLOR":
|
|
2687
|
+
categorized.colors.push(variable);
|
|
2688
|
+
break;
|
|
2689
|
+
case "FLOAT": {
|
|
2690
|
+
const nameLower = variable.name.toLowerCase();
|
|
2691
|
+
if (nameLower.includes("radius") || nameLower.includes("corner")) {
|
|
2692
|
+
categorized.radius.push(variable);
|
|
2693
|
+
} else if (nameLower.includes("spacing") || nameLower.includes("gap") || nameLower.includes("padding") || nameLower.includes("margin")) {
|
|
2694
|
+
categorized.spacing.push(variable);
|
|
2695
|
+
} else {
|
|
2696
|
+
categorized.other.push(variable);
|
|
2697
|
+
}
|
|
2698
|
+
break;
|
|
2699
|
+
}
|
|
2700
|
+
default:
|
|
2701
|
+
categorized.other.push(variable);
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
return Object.fromEntries(
|
|
2706
|
+
Object.entries(categorized).filter(([, vars]) => vars.length > 0)
|
|
2707
|
+
);
|
|
2708
|
+
}
|
|
1887
2709
|
function createVariableExportCommand(factory) {
|
|
1888
|
-
const cmd = new
|
|
2710
|
+
const cmd = new Command20("export").description("Export variables as design tokens (Enterprise)").argument("<url>", "Figma URL or file key").option(
|
|
1889
2711
|
"--format <format>",
|
|
1890
2712
|
"Output format (json, css, scss, style-dictionary)",
|
|
1891
2713
|
"json"
|
|
1892
|
-
).option("--mode <mode>", "Specific mode to export").option(
|
|
1893
|
-
|
|
2714
|
+
).option("--mode <mode>", "Specific mode to export").option(
|
|
2715
|
+
"--categorize",
|
|
2716
|
+
"Group variables by scope (CORNER_RADIUS, GAP, etc.)",
|
|
2717
|
+
false
|
|
2718
|
+
).option("-o, --output <file>", "Output file path").option("-t, --token <token>", "Override authentication token").action(
|
|
2719
|
+
async (url, options) => {
|
|
1894
2720
|
const { io } = factory;
|
|
2721
|
+
const validFormats = [
|
|
2722
|
+
"json",
|
|
2723
|
+
"css",
|
|
2724
|
+
"scss",
|
|
2725
|
+
"style-dictionary"
|
|
2726
|
+
];
|
|
2727
|
+
if (!validFormats.includes(
|
|
2728
|
+
options.format
|
|
2729
|
+
)) {
|
|
2730
|
+
io.err.write(
|
|
2731
|
+
chalk16.red(
|
|
2732
|
+
`Invalid format "${options.format}". Valid formats: ${validFormats.join(", ")}
|
|
2733
|
+
`
|
|
2734
|
+
)
|
|
2735
|
+
);
|
|
2736
|
+
process.exit(1);
|
|
2737
|
+
}
|
|
1895
2738
|
try {
|
|
1896
|
-
const
|
|
2739
|
+
const parsed = parseFigmaInput(url);
|
|
2740
|
+
const fileKey = parsed.fileKey;
|
|
1897
2741
|
let client = await factory.getClient();
|
|
1898
2742
|
if (options.token) {
|
|
1899
2743
|
const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
1900
|
-
client = new FigmaClient2({ token });
|
|
2744
|
+
client = new FigmaClient2({ token: options.token });
|
|
1901
2745
|
}
|
|
1902
2746
|
const response = await client.getLocalVariables(fileKey);
|
|
1903
2747
|
const meta = response.meta;
|
|
1904
2748
|
if (!meta) {
|
|
1905
|
-
io.err.write(
|
|
2749
|
+
io.err.write(chalk16.red("No variables found in file.\n"));
|
|
1906
2750
|
process.exit(1);
|
|
1907
2751
|
}
|
|
1908
2752
|
const variables = meta.variables;
|
|
@@ -1919,13 +2763,13 @@ function createVariableExportCommand(factory) {
|
|
|
1919
2763
|
}
|
|
1920
2764
|
}
|
|
1921
2765
|
if (!targetModeId) {
|
|
1922
|
-
io.err.write(
|
|
2766
|
+
io.err.write(chalk16.red(`Mode "${options.mode}" not found.
|
|
1923
2767
|
`));
|
|
1924
2768
|
const allModes = Object.values(collections).flatMap(
|
|
1925
2769
|
(c) => c.modes.map((m) => m.name)
|
|
1926
2770
|
);
|
|
1927
2771
|
io.err.write(
|
|
1928
|
-
|
|
2772
|
+
chalk16.dim(`Available modes: ${allModes.join(", ")}
|
|
1929
2773
|
`)
|
|
1930
2774
|
);
|
|
1931
2775
|
process.exit(1);
|
|
@@ -1934,37 +2778,53 @@ function createVariableExportCommand(factory) {
|
|
|
1934
2778
|
let output;
|
|
1935
2779
|
switch (options.format) {
|
|
1936
2780
|
case "css":
|
|
1937
|
-
output = exportToCss(
|
|
2781
|
+
output = exportToCss(
|
|
2782
|
+
variables,
|
|
2783
|
+
collections,
|
|
2784
|
+
targetModeId,
|
|
2785
|
+
options.categorize
|
|
2786
|
+
);
|
|
1938
2787
|
break;
|
|
1939
2788
|
case "scss":
|
|
1940
|
-
output = exportToScss(
|
|
2789
|
+
output = exportToScss(
|
|
2790
|
+
variables,
|
|
2791
|
+
collections,
|
|
2792
|
+
targetModeId,
|
|
2793
|
+
options.categorize
|
|
2794
|
+
);
|
|
1941
2795
|
break;
|
|
1942
2796
|
case "style-dictionary":
|
|
1943
2797
|
output = exportToStyleDictionary(
|
|
1944
2798
|
variables,
|
|
1945
2799
|
collections,
|
|
1946
|
-
targetModeId
|
|
2800
|
+
targetModeId,
|
|
2801
|
+
options.categorize
|
|
1947
2802
|
);
|
|
1948
2803
|
break;
|
|
1949
2804
|
default:
|
|
1950
|
-
|
|
2805
|
+
if (options.categorize) {
|
|
2806
|
+
const categorized = categorizeVariables(variables);
|
|
2807
|
+
output = JSON.stringify({ categorized, collections }, null, 2);
|
|
2808
|
+
} else {
|
|
2809
|
+
output = JSON.stringify({ variables, collections }, null, 2);
|
|
2810
|
+
}
|
|
1951
2811
|
}
|
|
1952
2812
|
if (options.output) {
|
|
1953
|
-
|
|
1954
|
-
io.out.write(
|
|
2813
|
+
fs5.writeFileSync(options.output, output);
|
|
2814
|
+
io.out.write(chalk16.green(`\u2713 Exported to ${options.output}
|
|
1955
2815
|
`));
|
|
1956
2816
|
} else {
|
|
1957
2817
|
io.out.write(output);
|
|
1958
2818
|
io.out.write("\n");
|
|
1959
2819
|
}
|
|
1960
2820
|
} catch (error) {
|
|
1961
|
-
io.err.write(
|
|
2821
|
+
io.err.write(chalk16.red("Error exporting variables.\n"));
|
|
1962
2822
|
if (error instanceof Error) {
|
|
1963
|
-
io.err.write(
|
|
2823
|
+
io.err.write(chalk16.dim(`${error.message}
|
|
1964
2824
|
`));
|
|
1965
2825
|
}
|
|
1966
2826
|
io.err.write(
|
|
1967
|
-
|
|
2827
|
+
chalk16.dim("\nNote: Variables API requires Figma Enterprise plan.\n")
|
|
1968
2828
|
);
|
|
1969
2829
|
process.exit(1);
|
|
1970
2830
|
}
|
|
@@ -1972,9 +2832,6 @@ function createVariableExportCommand(factory) {
|
|
|
1972
2832
|
);
|
|
1973
2833
|
return cmd;
|
|
1974
2834
|
}
|
|
1975
|
-
function toKebabCase(str) {
|
|
1976
|
-
return str.replace(/\s+/g, "-").replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").toLowerCase();
|
|
1977
|
-
}
|
|
1978
2835
|
function resolveValue(value, variables, _modeId) {
|
|
1979
2836
|
if (typeof value === "object" && value !== null) {
|
|
1980
2837
|
if ("r" in value && "g" in value && "b" in value) {
|
|
@@ -2001,47 +2858,90 @@ function resolveValue(value, variables, _modeId) {
|
|
|
2001
2858
|
}
|
|
2002
2859
|
return String(value);
|
|
2003
2860
|
}
|
|
2004
|
-
function exportToCss(variables, collections, modeId) {
|
|
2861
|
+
function exportToCss(variables, collections, modeId, categorize) {
|
|
2005
2862
|
const lines = [":root {"];
|
|
2006
|
-
|
|
2007
|
-
const
|
|
2008
|
-
const
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2863
|
+
if (categorize) {
|
|
2864
|
+
const categorized = categorizeVariables(variables);
|
|
2865
|
+
for (const [category, categoryVars] of Object.entries(categorized)) {
|
|
2866
|
+
if (categoryVars.length === 0) continue;
|
|
2867
|
+
lines.push(
|
|
2868
|
+
` /* ${category.charAt(0).toUpperCase() + category.slice(1)} */`
|
|
2869
|
+
);
|
|
2870
|
+
for (const variable of categoryVars) {
|
|
2871
|
+
const collection = collections[variable.variableCollectionId];
|
|
2872
|
+
const targetMode = modeId || collection?.defaultModeId;
|
|
2873
|
+
const value = variable.valuesByMode[targetMode];
|
|
2874
|
+
if (value !== void 0) {
|
|
2875
|
+
const cssName = toKebabCase(variable.name);
|
|
2876
|
+
const cssValue = resolveValue(value, variables, targetMode);
|
|
2877
|
+
lines.push(` --${cssName}: ${cssValue};`);
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
lines.push("");
|
|
2881
|
+
}
|
|
2882
|
+
} else {
|
|
2883
|
+
for (const variable of Object.values(variables)) {
|
|
2884
|
+
const collection = collections[variable.variableCollectionId];
|
|
2885
|
+
const targetMode = modeId || collection?.defaultModeId;
|
|
2886
|
+
const value = variable.valuesByMode[targetMode];
|
|
2887
|
+
if (value !== void 0) {
|
|
2888
|
+
const cssName = toKebabCase(variable.name);
|
|
2889
|
+
const cssValue = resolveValue(value, variables, targetMode);
|
|
2890
|
+
lines.push(` --${cssName}: ${cssValue};`);
|
|
2891
|
+
}
|
|
2014
2892
|
}
|
|
2015
2893
|
}
|
|
2016
2894
|
lines.push("}");
|
|
2017
2895
|
return lines.join("\n");
|
|
2018
2896
|
}
|
|
2019
|
-
function exportToScss(variables, collections, modeId) {
|
|
2897
|
+
function exportToScss(variables, collections, modeId, categorize) {
|
|
2020
2898
|
const lines = [];
|
|
2021
|
-
|
|
2022
|
-
const
|
|
2023
|
-
const
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
const
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2899
|
+
if (categorize) {
|
|
2900
|
+
const categorized = categorizeVariables(variables);
|
|
2901
|
+
for (const [category, categoryVars] of Object.entries(categorized)) {
|
|
2902
|
+
if (categoryVars.length === 0) continue;
|
|
2903
|
+
lines.push(`// ${category.charAt(0).toUpperCase() + category.slice(1)}`);
|
|
2904
|
+
for (const variable of categoryVars) {
|
|
2905
|
+
const collection = collections[variable.variableCollectionId];
|
|
2906
|
+
const targetMode = modeId || collection?.defaultModeId;
|
|
2907
|
+
const value = variable.valuesByMode[targetMode];
|
|
2908
|
+
if (value !== void 0) {
|
|
2909
|
+
const scssName = toKebabCase(variable.name);
|
|
2910
|
+
const scssValue = resolveValue(value, variables, targetMode).replace(
|
|
2911
|
+
/var\(--([^)]+)\)/g,
|
|
2912
|
+
"$$$1"
|
|
2913
|
+
);
|
|
2914
|
+
lines.push(`$${scssName}: ${scssValue};`);
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
lines.push("");
|
|
2918
|
+
}
|
|
2919
|
+
} else {
|
|
2920
|
+
for (const variable of Object.values(variables)) {
|
|
2921
|
+
const collection = collections[variable.variableCollectionId];
|
|
2922
|
+
const targetMode = modeId || collection?.defaultModeId;
|
|
2923
|
+
const value = variable.valuesByMode[targetMode];
|
|
2924
|
+
if (value !== void 0) {
|
|
2925
|
+
const scssName = toKebabCase(variable.name);
|
|
2926
|
+
const scssValue = resolveValue(value, variables, targetMode).replace(
|
|
2927
|
+
/var\(--([^)]+)\)/g,
|
|
2928
|
+
"$$$1"
|
|
2929
|
+
);
|
|
2930
|
+
lines.push(`$${scssName}: ${scssValue};`);
|
|
2931
|
+
}
|
|
2032
2932
|
}
|
|
2033
2933
|
}
|
|
2034
2934
|
return lines.join("\n");
|
|
2035
2935
|
}
|
|
2036
|
-
function exportToStyleDictionary(variables, collections, modeId) {
|
|
2936
|
+
function exportToStyleDictionary(variables, collections, modeId, categorize) {
|
|
2037
2937
|
const tokens = {};
|
|
2038
|
-
|
|
2938
|
+
const processVariable = (variable, targetTokens) => {
|
|
2039
2939
|
const collection = collections[variable.variableCollectionId];
|
|
2040
2940
|
const targetMode = modeId || collection?.defaultModeId;
|
|
2041
2941
|
const value = variable.valuesByMode[targetMode];
|
|
2042
|
-
if (value === void 0)
|
|
2942
|
+
if (value === void 0) return;
|
|
2043
2943
|
const pathParts = variable.name.split("/").map((p) => p.trim());
|
|
2044
|
-
let current =
|
|
2944
|
+
let current = targetTokens;
|
|
2045
2945
|
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
2046
2946
|
const part = pathParts[i];
|
|
2047
2947
|
if (!current[part]) {
|
|
@@ -2055,6 +2955,20 @@ function exportToStyleDictionary(variables, collections, modeId) {
|
|
|
2055
2955
|
type: mapTypeToStyleDictionary(variable.resolvedType),
|
|
2056
2956
|
description: variable.description || void 0
|
|
2057
2957
|
};
|
|
2958
|
+
};
|
|
2959
|
+
if (categorize) {
|
|
2960
|
+
const categorized = categorizeVariables(variables);
|
|
2961
|
+
for (const [category, categoryVars] of Object.entries(categorized)) {
|
|
2962
|
+
if (categoryVars.length === 0) continue;
|
|
2963
|
+
tokens[category] = {};
|
|
2964
|
+
for (const variable of categoryVars) {
|
|
2965
|
+
processVariable(variable, tokens[category]);
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
} else {
|
|
2969
|
+
for (const variable of Object.values(variables)) {
|
|
2970
|
+
processVariable(variable, tokens);
|
|
2971
|
+
}
|
|
2058
2972
|
}
|
|
2059
2973
|
return JSON.stringify(tokens, null, 2);
|
|
2060
2974
|
}
|
|
@@ -2096,266 +3010,89 @@ function mapTypeToStyleDictionary(figmaType) {
|
|
|
2096
3010
|
return figmaType.toLowerCase();
|
|
2097
3011
|
}
|
|
2098
3012
|
}
|
|
2099
|
-
|
|
2100
|
-
// src/cmd/variable/index.ts
|
|
2101
|
-
function createVariableCommand(factory) {
|
|
2102
|
-
const cmd = new
|
|
2103
|
-
"Work with Figma variables (Enterprise)"
|
|
2104
|
-
);
|
|
2105
|
-
cmd.addCommand(createVariableListCommand(factory));
|
|
2106
|
-
cmd.addCommand(createVariableExportCommand(factory));
|
|
2107
|
-
return cmd;
|
|
2108
|
-
}
|
|
2109
|
-
|
|
2110
|
-
// src/cmd/export/index.ts
|
|
2111
|
-
import { Command as Command24 } from "commander";
|
|
2112
|
-
|
|
2113
|
-
// src/cmd/export/tokens.ts
|
|
2114
|
-
import { Command as Command21 } from "commander";
|
|
2115
|
-
import chalk16 from "chalk";
|
|
2116
|
-
import * as fs5 from "fs";
|
|
2117
|
-
|
|
2118
|
-
// src/internal/transform/tokens.ts
|
|
2119
|
-
function figmaColorToCss(color) {
|
|
2120
|
-
const r = Math.round(color.r * 255);
|
|
2121
|
-
const g = Math.round(color.g * 255);
|
|
2122
|
-
const b = Math.round(color.b * 255);
|
|
2123
|
-
const a = color.a ?? 1;
|
|
2124
|
-
if (a === 1) {
|
|
2125
|
-
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
|
2126
|
-
}
|
|
2127
|
-
return `rgba(${r}, ${g}, ${b}, ${a.toFixed(2)})`;
|
|
2128
|
-
}
|
|
2129
|
-
function toKebabCase2(str) {
|
|
2130
|
-
return str.replace(/\s+/g, "-").replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").toLowerCase();
|
|
2131
|
-
}
|
|
2132
|
-
function extractStylesFromFile(file, styleMetadata) {
|
|
2133
|
-
const tokens = {
|
|
2134
|
-
colors: {},
|
|
2135
|
-
typography: {},
|
|
2136
|
-
spacing: {},
|
|
2137
|
-
effects: {}
|
|
2138
|
-
};
|
|
2139
|
-
const styleNodeMap = /* @__PURE__ */ new Map();
|
|
2140
|
-
for (const style of styleMetadata) {
|
|
2141
|
-
const nodeId = style.node_id;
|
|
2142
|
-
if (nodeId) {
|
|
2143
|
-
styleNodeMap.set(nodeId, style);
|
|
2144
|
-
}
|
|
2145
|
-
}
|
|
2146
|
-
function traverseNode(node) {
|
|
2147
|
-
const nodeWithStyles = node;
|
|
2148
|
-
if ("styles" in nodeWithStyles && nodeWithStyles.styles) {
|
|
2149
|
-
const styles = nodeWithStyles.styles;
|
|
2150
|
-
if (styles.fill && "fills" in nodeWithStyles) {
|
|
2151
|
-
const fills = nodeWithStyles.fills;
|
|
2152
|
-
if (fills && fills.length > 0 && fills[0].type === "SOLID") {
|
|
2153
|
-
const fill = fills[0];
|
|
2154
|
-
if (fill.color) {
|
|
2155
|
-
const styleInfo = styleNodeMap.get(node.id);
|
|
2156
|
-
const styleName = styleInfo?.name || node.name;
|
|
2157
|
-
const tokenName = toKebabCase2(styleName);
|
|
2158
|
-
tokens.colors[tokenName] = {
|
|
2159
|
-
name: styleName,
|
|
2160
|
-
type: "color",
|
|
2161
|
-
value: figmaColorToCss(fill.color),
|
|
2162
|
-
description: styleInfo?.description
|
|
2163
|
-
};
|
|
2164
|
-
}
|
|
2165
|
-
}
|
|
2166
|
-
}
|
|
2167
|
-
if (styles.text && "style" in nodeWithStyles && nodeWithStyles.style) {
|
|
2168
|
-
const textStyle = nodeWithStyles.style;
|
|
2169
|
-
const styleInfo = styleNodeMap.get(node.id);
|
|
2170
|
-
const styleName = styleInfo?.name || node.name;
|
|
2171
|
-
const tokenName = toKebabCase2(styleName);
|
|
2172
|
-
tokens.typography[tokenName] = {
|
|
2173
|
-
name: styleName,
|
|
2174
|
-
type: "typography",
|
|
2175
|
-
value: {
|
|
2176
|
-
fontFamily: textStyle.fontFamily || "sans-serif",
|
|
2177
|
-
fontSize: `${textStyle.fontSize || 16}px`,
|
|
2178
|
-
fontWeight: textStyle.fontWeight || 400,
|
|
2179
|
-
lineHeight: textStyle.lineHeightPx !== void 0 ? `${textStyle.lineHeightPx}px` : "normal",
|
|
2180
|
-
letterSpacing: textStyle.letterSpacing !== void 0 ? `${textStyle.letterSpacing}px` : "normal"
|
|
2181
|
-
},
|
|
2182
|
-
description: styleInfo?.description
|
|
2183
|
-
};
|
|
2184
|
-
}
|
|
2185
|
-
}
|
|
2186
|
-
if ("children" in node) {
|
|
2187
|
-
const parent = node;
|
|
2188
|
-
for (const child of parent.children || []) {
|
|
2189
|
-
traverseNode(child);
|
|
2190
|
-
}
|
|
2191
|
-
}
|
|
2192
|
-
}
|
|
2193
|
-
if (file.document) {
|
|
2194
|
-
traverseNode(file.document);
|
|
2195
|
-
}
|
|
2196
|
-
return tokens;
|
|
2197
|
-
}
|
|
2198
|
-
function tokensToCss(tokens) {
|
|
2199
|
-
const lines = [":root {"];
|
|
2200
|
-
if (tokens.colors) {
|
|
2201
|
-
lines.push(" /* Colors */");
|
|
2202
|
-
for (const [name, token] of Object.entries(tokens.colors)) {
|
|
2203
|
-
lines.push(` --color-${name}: ${token.value};`);
|
|
2204
|
-
}
|
|
2205
|
-
lines.push("");
|
|
2206
|
-
}
|
|
2207
|
-
if (tokens.typography) {
|
|
2208
|
-
lines.push(" /* Typography */");
|
|
2209
|
-
for (const [name, token] of Object.entries(tokens.typography)) {
|
|
2210
|
-
const value = token.value;
|
|
2211
|
-
lines.push(` --font-${name}-family: ${value.fontFamily};`);
|
|
2212
|
-
lines.push(` --font-${name}-size: ${value.fontSize};`);
|
|
2213
|
-
lines.push(` --font-${name}-weight: ${value.fontWeight};`);
|
|
2214
|
-
lines.push(` --font-${name}-line-height: ${value.lineHeight};`);
|
|
2215
|
-
lines.push(` --font-${name}-letter-spacing: ${value.letterSpacing};`);
|
|
2216
|
-
}
|
|
2217
|
-
lines.push("");
|
|
2218
|
-
}
|
|
2219
|
-
if (tokens.spacing) {
|
|
2220
|
-
lines.push(" /* Spacing */");
|
|
2221
|
-
for (const [name, token] of Object.entries(tokens.spacing)) {
|
|
2222
|
-
lines.push(` --spacing-${name}: ${token.value};`);
|
|
2223
|
-
}
|
|
2224
|
-
}
|
|
2225
|
-
lines.push("}");
|
|
2226
|
-
return lines.join("\n");
|
|
2227
|
-
}
|
|
2228
|
-
function tokensToScss(tokens) {
|
|
2229
|
-
const lines = [];
|
|
2230
|
-
if (tokens.colors) {
|
|
2231
|
-
lines.push("// Colors");
|
|
2232
|
-
for (const [name, token] of Object.entries(tokens.colors)) {
|
|
2233
|
-
lines.push(`$color-${name}: ${token.value};`);
|
|
2234
|
-
}
|
|
2235
|
-
lines.push("");
|
|
2236
|
-
}
|
|
2237
|
-
if (tokens.typography) {
|
|
2238
|
-
lines.push("// Typography");
|
|
2239
|
-
for (const [name, token] of Object.entries(tokens.typography)) {
|
|
2240
|
-
const value = token.value;
|
|
2241
|
-
lines.push(`$font-${name}-family: ${value.fontFamily};`);
|
|
2242
|
-
lines.push(`$font-${name}-size: ${value.fontSize};`);
|
|
2243
|
-
lines.push(`$font-${name}-weight: ${value.fontWeight};`);
|
|
2244
|
-
lines.push(`$font-${name}-line-height: ${value.lineHeight};`);
|
|
2245
|
-
lines.push(`$font-${name}-letter-spacing: ${value.letterSpacing};`);
|
|
2246
|
-
}
|
|
2247
|
-
lines.push("");
|
|
2248
|
-
}
|
|
2249
|
-
if (tokens.spacing) {
|
|
2250
|
-
lines.push("// Spacing");
|
|
2251
|
-
for (const [name, token] of Object.entries(tokens.spacing)) {
|
|
2252
|
-
lines.push(`$spacing-${name}: ${token.value};`);
|
|
2253
|
-
}
|
|
2254
|
-
}
|
|
2255
|
-
return lines.join("\n");
|
|
2256
|
-
}
|
|
2257
|
-
function tokensToStyleDictionary(tokens) {
|
|
2258
|
-
const output = {};
|
|
2259
|
-
if (tokens.colors && Object.keys(tokens.colors).length > 0) {
|
|
2260
|
-
output.color = {};
|
|
2261
|
-
for (const [name, token] of Object.entries(tokens.colors)) {
|
|
2262
|
-
output.color[name] = {
|
|
2263
|
-
value: token.value,
|
|
2264
|
-
type: "color",
|
|
2265
|
-
description: token.description
|
|
2266
|
-
};
|
|
2267
|
-
}
|
|
2268
|
-
}
|
|
2269
|
-
if (tokens.typography && Object.keys(tokens.typography).length > 0) {
|
|
2270
|
-
output.typography = {};
|
|
2271
|
-
for (const [name, token] of Object.entries(tokens.typography)) {
|
|
2272
|
-
output.typography[name] = {
|
|
2273
|
-
value: token.value,
|
|
2274
|
-
type: "typography",
|
|
2275
|
-
description: token.description
|
|
2276
|
-
};
|
|
2277
|
-
}
|
|
2278
|
-
}
|
|
2279
|
-
if (tokens.spacing && Object.keys(tokens.spacing).length > 0) {
|
|
2280
|
-
output.spacing = {};
|
|
2281
|
-
for (const [name, token] of Object.entries(tokens.spacing)) {
|
|
2282
|
-
output.spacing[name] = {
|
|
2283
|
-
value: token.value,
|
|
2284
|
-
type: "dimension",
|
|
2285
|
-
description: token.description
|
|
2286
|
-
};
|
|
2287
|
-
}
|
|
2288
|
-
}
|
|
2289
|
-
return JSON.stringify(output, null, 2);
|
|
2290
|
-
}
|
|
2291
|
-
function tokensToTailwind(tokens) {
|
|
2292
|
-
const config = {
|
|
2293
|
-
theme: {
|
|
2294
|
-
extend: {}
|
|
2295
|
-
}
|
|
2296
|
-
};
|
|
2297
|
-
if (tokens.colors && Object.keys(tokens.colors).length > 0) {
|
|
2298
|
-
config.theme.extend.colors = {};
|
|
2299
|
-
for (const [name, token] of Object.entries(tokens.colors)) {
|
|
2300
|
-
config.theme.extend.colors[name] = token.value;
|
|
2301
|
-
}
|
|
2302
|
-
}
|
|
2303
|
-
if (tokens.typography && Object.keys(tokens.typography).length > 0) {
|
|
2304
|
-
config.theme.extend.fontFamily = {};
|
|
2305
|
-
config.theme.extend.fontSize = {};
|
|
2306
|
-
for (const [name, token] of Object.entries(tokens.typography)) {
|
|
2307
|
-
const value = token.value;
|
|
2308
|
-
config.theme.extend.fontFamily[name] = [value.fontFamily];
|
|
2309
|
-
config.theme.extend.fontSize[name] = value.fontSize;
|
|
2310
|
-
}
|
|
2311
|
-
}
|
|
2312
|
-
if (tokens.spacing && Object.keys(tokens.spacing).length > 0) {
|
|
2313
|
-
config.theme.extend.spacing = {};
|
|
2314
|
-
for (const [name, token] of Object.entries(tokens.spacing)) {
|
|
2315
|
-
config.theme.extend.spacing[name] = token.value;
|
|
2316
|
-
}
|
|
2317
|
-
}
|
|
2318
|
-
return `/** @type {import('tailwindcss').Config} */
|
|
2319
|
-
module.exports = ${JSON.stringify(config, null, 2)};`;
|
|
3013
|
+
|
|
3014
|
+
// src/cmd/variable/index.ts
|
|
3015
|
+
function createVariableCommand(factory) {
|
|
3016
|
+
const cmd = new Command21("variable").description(
|
|
3017
|
+
"Work with Figma variables (Enterprise)"
|
|
3018
|
+
);
|
|
3019
|
+
cmd.addCommand(createVariableListCommand(factory));
|
|
3020
|
+
cmd.addCommand(createVariableExportCommand(factory));
|
|
3021
|
+
return cmd;
|
|
2320
3022
|
}
|
|
2321
3023
|
|
|
3024
|
+
// src/cmd/export/index.ts
|
|
3025
|
+
import { Command as Command25 } from "commander";
|
|
3026
|
+
|
|
2322
3027
|
// src/cmd/export/tokens.ts
|
|
3028
|
+
import { Command as Command22 } from "commander";
|
|
3029
|
+
import chalk17 from "chalk";
|
|
3030
|
+
import * as fs6 from "fs";
|
|
2323
3031
|
function createExportTokensCommand(factory) {
|
|
2324
|
-
const cmd = new
|
|
3032
|
+
const cmd = new Command22("tokens").description("Export design tokens from a Figma file").argument("<url>", "Figma URL or file key").option(
|
|
2325
3033
|
"--format <format>",
|
|
2326
3034
|
"Output format (json, css, scss, style-dictionary, tailwind)",
|
|
2327
3035
|
"json"
|
|
2328
3036
|
).option("-o, --output <file>", "Output file path").option("-t, --token <token>", "Override authentication token").action(
|
|
2329
|
-
async (
|
|
3037
|
+
async (url, options) => {
|
|
2330
3038
|
const { io } = factory;
|
|
3039
|
+
const validFormats = [
|
|
3040
|
+
"json",
|
|
3041
|
+
"css",
|
|
3042
|
+
"scss",
|
|
3043
|
+
"style-dictionary",
|
|
3044
|
+
"tailwind"
|
|
3045
|
+
];
|
|
3046
|
+
if (!validFormats.includes(
|
|
3047
|
+
options.format
|
|
3048
|
+
)) {
|
|
3049
|
+
io.err.write(
|
|
3050
|
+
chalk17.red(
|
|
3051
|
+
`Invalid format "${options.format}". Valid formats: ${validFormats.join(", ")}
|
|
3052
|
+
`
|
|
3053
|
+
)
|
|
3054
|
+
);
|
|
3055
|
+
process.exit(1);
|
|
3056
|
+
}
|
|
2331
3057
|
try {
|
|
2332
|
-
const
|
|
3058
|
+
const parsed = parseFigmaInput(url);
|
|
3059
|
+
const fileKey = parsed.fileKey;
|
|
2333
3060
|
let client = await factory.getClient();
|
|
2334
3061
|
if (options.token) {
|
|
2335
3062
|
const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
2336
|
-
client = new FigmaClient2({ token });
|
|
3063
|
+
client = new FigmaClient2({ token: options.token });
|
|
2337
3064
|
}
|
|
2338
|
-
io.err.write(
|
|
3065
|
+
io.err.write(chalk17.dim("Fetching file styles...\n"));
|
|
2339
3066
|
const stylesResponse = await client.getFileStyles(fileKey);
|
|
2340
3067
|
const styles = stylesResponse.meta?.styles || [];
|
|
2341
3068
|
if (styles.length === 0) {
|
|
2342
3069
|
io.err.write(
|
|
2343
|
-
|
|
3070
|
+
chalk17.yellow("No styles found in file. Trying variables...\n")
|
|
2344
3071
|
);
|
|
2345
3072
|
try {
|
|
2346
3073
|
const variablesResponse = await client.getLocalVariables(fileKey);
|
|
2347
3074
|
if (variablesResponse.meta?.variables) {
|
|
2348
3075
|
io.err.write(
|
|
2349
|
-
|
|
3076
|
+
chalk17.dim(
|
|
2350
3077
|
"Found variables. Use 'figma variable export' for variable-based tokens.\n"
|
|
2351
3078
|
)
|
|
2352
3079
|
);
|
|
2353
3080
|
}
|
|
2354
|
-
} catch {
|
|
3081
|
+
} catch (error) {
|
|
3082
|
+
const message = error instanceof Error ? error.message.toLowerCase() : "";
|
|
3083
|
+
const isPermissionError = message.includes("403") || message.includes("enterprise") || message.includes("permission") || message.includes("forbidden");
|
|
3084
|
+
if (!isPermissionError) {
|
|
3085
|
+
io.err.write(
|
|
3086
|
+
chalk17.dim(
|
|
3087
|
+
`Note: Could not check for variables: ${error instanceof Error ? error.message : String(error)}
|
|
3088
|
+
`
|
|
3089
|
+
)
|
|
3090
|
+
);
|
|
3091
|
+
}
|
|
2355
3092
|
}
|
|
2356
3093
|
process.exit(0);
|
|
2357
3094
|
}
|
|
2358
|
-
io.err.write(
|
|
3095
|
+
io.err.write(chalk17.dim("Extracting token values...\n"));
|
|
2359
3096
|
const file = await client.getFile(fileKey);
|
|
2360
3097
|
const tokens = extractStylesFromFile(file, styles);
|
|
2361
3098
|
let output;
|
|
@@ -2376,17 +3113,17 @@ function createExportTokensCommand(factory) {
|
|
|
2376
3113
|
output = JSON.stringify(tokens, null, 2);
|
|
2377
3114
|
}
|
|
2378
3115
|
if (options.output) {
|
|
2379
|
-
|
|
2380
|
-
io.out.write(
|
|
3116
|
+
fs6.writeFileSync(options.output, output);
|
|
3117
|
+
io.out.write(chalk17.green(`\u2713 Exported to ${options.output}
|
|
2381
3118
|
`));
|
|
2382
3119
|
} else {
|
|
2383
3120
|
io.out.write(output);
|
|
2384
3121
|
io.out.write("\n");
|
|
2385
3122
|
}
|
|
2386
3123
|
} catch (error) {
|
|
2387
|
-
io.err.write(
|
|
3124
|
+
io.err.write(chalk17.red("Error exporting tokens.\n"));
|
|
2388
3125
|
if (error instanceof Error) {
|
|
2389
|
-
io.err.write(
|
|
3126
|
+
io.err.write(chalk17.dim(`${error.message}
|
|
2390
3127
|
`));
|
|
2391
3128
|
}
|
|
2392
3129
|
process.exit(1);
|
|
@@ -2397,19 +3134,67 @@ function createExportTokensCommand(factory) {
|
|
|
2397
3134
|
}
|
|
2398
3135
|
|
|
2399
3136
|
// src/cmd/export/icons.ts
|
|
2400
|
-
import { Command as
|
|
2401
|
-
import
|
|
3137
|
+
import { Command as Command23 } from "commander";
|
|
3138
|
+
import chalk18 from "chalk";
|
|
3139
|
+
import * as path5 from "path";
|
|
3140
|
+
import * as fs7 from "fs";
|
|
3141
|
+
function parseVariantName(name) {
|
|
3142
|
+
const parts = name.split("/");
|
|
3143
|
+
const variants = [];
|
|
3144
|
+
let baseName = "";
|
|
3145
|
+
for (const part of parts) {
|
|
3146
|
+
if (part.includes("=")) {
|
|
3147
|
+
const [, value] = part.split("=");
|
|
3148
|
+
if (value) {
|
|
3149
|
+
variants.push(value.trim());
|
|
3150
|
+
}
|
|
3151
|
+
} else {
|
|
3152
|
+
baseName = baseName ? `${baseName}-${part}` : part;
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
return { baseName: baseName || name, variants };
|
|
3156
|
+
}
|
|
3157
|
+
function generateVariantFilename(name, variantFormat, prefix, imageFormat) {
|
|
3158
|
+
const { baseName, variants } = parseVariantName(name);
|
|
3159
|
+
const kebabBase = toKebabCase(baseName);
|
|
3160
|
+
switch (variantFormat) {
|
|
3161
|
+
case "suffix":
|
|
3162
|
+
if (variants.length > 0) {
|
|
3163
|
+
const variantSuffix = variants.map((v) => toKebabCase(v)).join("-");
|
|
3164
|
+
return {
|
|
3165
|
+
filename: `${prefix}${kebabBase}--${variantSuffix}.${imageFormat}`
|
|
3166
|
+
};
|
|
3167
|
+
}
|
|
3168
|
+
return { filename: `${prefix}${kebabBase}.${imageFormat}` };
|
|
3169
|
+
case "folder":
|
|
3170
|
+
if (variants.length > 0) {
|
|
3171
|
+
const variantSuffix = variants.map((v) => toKebabCase(v)).join("-");
|
|
3172
|
+
return {
|
|
3173
|
+
filename: `${variantSuffix}.${imageFormat}`,
|
|
3174
|
+
subdir: `${prefix}${kebabBase}`
|
|
3175
|
+
};
|
|
3176
|
+
}
|
|
3177
|
+
return { filename: `${prefix}${kebabBase}.${imageFormat}` };
|
|
3178
|
+
case "flat":
|
|
3179
|
+
default:
|
|
3180
|
+
return { filename: `${prefix}${toKebabCase(name)}.${imageFormat}` };
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
2402
3183
|
function createExportIconsCommand(factory) {
|
|
2403
|
-
const cmd = new
|
|
3184
|
+
const cmd = new Command23("icons").description("Export icons from a Figma file").argument("<url>", "Figma URL or file key").option(
|
|
2404
3185
|
"--frame <name>",
|
|
2405
3186
|
"Name of frame/page containing icons (searches all pages if not specified)"
|
|
2406
|
-
).option("--format <format>", "Export format (svg, png)", "svg").option("-s, --scale <scale>", "Scale factor for PNG export", parseFloat, 1).option("-o, --output <dir>", "Output directory", "./icons").option("--prefix <prefix>", "Prefix for icon filenames", "icon-").option(
|
|
2407
|
-
|
|
3187
|
+
).option("--format <format>", "Export format (svg, png)", "svg").option("-s, --scale <scale>", "Scale factor for PNG export", parseFloat, 1).option("-o, --output <dir>", "Output directory", "./icons").option("--prefix <prefix>", "Prefix for icon filenames", "icon-").option(
|
|
3188
|
+
"--variant-format <format>",
|
|
3189
|
+
"How to handle variants: flat, suffix, or folder (default: flat)",
|
|
3190
|
+
"flat"
|
|
3191
|
+
).option("-t, --token <token>", "Override authentication token").action(
|
|
3192
|
+
async (url, options) => {
|
|
2408
3193
|
const { io } = factory;
|
|
2409
3194
|
const validFormats = ["svg", "png"];
|
|
2410
3195
|
if (!validFormats.includes(options.format)) {
|
|
2411
3196
|
io.err.write(
|
|
2412
|
-
|
|
3197
|
+
chalk18.red(
|
|
2413
3198
|
`Invalid format "${options.format}". Valid formats: ${validFormats.join(", ")}
|
|
2414
3199
|
`
|
|
2415
3200
|
)
|
|
@@ -2445,30 +3230,32 @@ function createExportIconsCommand(factory) {
|
|
|
2445
3230
|
);
|
|
2446
3231
|
};
|
|
2447
3232
|
var findIcons = findIcons2, hasDeepChildren = hasDeepChildren2;
|
|
3233
|
+
const parsed = parseFigmaInput(url);
|
|
3234
|
+
const fileKey = parsed.fileKey;
|
|
2448
3235
|
const token = await factory.getToken(options.token);
|
|
2449
3236
|
let client = await factory.getClient();
|
|
2450
3237
|
if (options.token) {
|
|
2451
3238
|
const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
2452
3239
|
client = new FigmaClient2({ token });
|
|
2453
3240
|
}
|
|
2454
|
-
io.err.write(
|
|
3241
|
+
io.err.write(chalk18.dim("Fetching file structure...\n"));
|
|
2455
3242
|
const file = await client.getFile(fileKey, { depth: 2 });
|
|
2456
3243
|
const iconNodes = [];
|
|
2457
3244
|
if (file.document) {
|
|
2458
3245
|
findIcons2(file.document, false);
|
|
2459
3246
|
}
|
|
2460
3247
|
if (iconNodes.length === 0) {
|
|
2461
|
-
io.err.write(
|
|
3248
|
+
io.err.write(chalk18.yellow("No icons found in file.\n"));
|
|
2462
3249
|
if (options.frame) {
|
|
2463
3250
|
io.err.write(
|
|
2464
|
-
|
|
3251
|
+
chalk18.dim(`Looking for frame: "${options.frame}"
|
|
2465
3252
|
`)
|
|
2466
3253
|
);
|
|
2467
3254
|
}
|
|
2468
3255
|
process.exit(0);
|
|
2469
3256
|
}
|
|
2470
3257
|
io.err.write(
|
|
2471
|
-
|
|
3258
|
+
chalk18.dim(`Found ${iconNodes.length} icons. Exporting...
|
|
2472
3259
|
`)
|
|
2473
3260
|
);
|
|
2474
3261
|
const nodeIds = iconNodes.map((n) => n.id);
|
|
@@ -2479,12 +3266,51 @@ function createExportIconsCommand(factory) {
|
|
|
2479
3266
|
svg_simplify_stroke: true
|
|
2480
3267
|
});
|
|
2481
3268
|
const imageUrls = imagesResponse.images || {};
|
|
2482
|
-
const
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
3269
|
+
const validVariantFormats = ["flat", "suffix", "folder"];
|
|
3270
|
+
if (!validVariantFormats.includes(
|
|
3271
|
+
options.variantFormat
|
|
3272
|
+
)) {
|
|
3273
|
+
io.err.write(
|
|
3274
|
+
chalk18.red(
|
|
3275
|
+
`Invalid variant format "${options.variantFormat}". Valid formats: ${validVariantFormats.join(", ")}
|
|
3276
|
+
`
|
|
3277
|
+
)
|
|
3278
|
+
);
|
|
3279
|
+
process.exit(1);
|
|
3280
|
+
}
|
|
3281
|
+
const variantFormat = options.variantFormat;
|
|
3282
|
+
const subdirs = /* @__PURE__ */ new Set();
|
|
3283
|
+
for (const node of iconNodes) {
|
|
3284
|
+
const { subdir } = generateVariantFilename(
|
|
3285
|
+
node.name,
|
|
3286
|
+
variantFormat,
|
|
3287
|
+
options.prefix,
|
|
3288
|
+
imageFormat
|
|
3289
|
+
);
|
|
3290
|
+
if (subdir) {
|
|
3291
|
+
subdirs.add(subdir);
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3294
|
+
fs7.mkdirSync(options.output, { recursive: true });
|
|
3295
|
+
for (const subdir of subdirs) {
|
|
3296
|
+
fs7.mkdirSync(path5.join(options.output, subdir), {
|
|
3297
|
+
recursive: true
|
|
3298
|
+
});
|
|
3299
|
+
}
|
|
3300
|
+
const downloadItems = iconNodes.map((node) => {
|
|
3301
|
+
const { filename, subdir } = generateVariantFilename(
|
|
3302
|
+
node.name,
|
|
3303
|
+
variantFormat,
|
|
3304
|
+
options.prefix,
|
|
3305
|
+
imageFormat
|
|
3306
|
+
);
|
|
3307
|
+
return {
|
|
3308
|
+
id: node.id,
|
|
3309
|
+
name: node.name,
|
|
3310
|
+
url: imageUrls[node.id] ?? null,
|
|
3311
|
+
filename: subdir ? path5.join(subdir, filename) : filename
|
|
3312
|
+
};
|
|
3313
|
+
});
|
|
2488
3314
|
const results = await downloadFiles(downloadItems, options.output);
|
|
2489
3315
|
const successCount = results.filter(
|
|
2490
3316
|
(r) => r.status === "success"
|
|
@@ -2492,25 +3318,25 @@ function createExportIconsCommand(factory) {
|
|
|
2492
3318
|
const failures = results.filter((r) => r.status === "error");
|
|
2493
3319
|
for (const failure of failures) {
|
|
2494
3320
|
io.err.write(
|
|
2495
|
-
|
|
3321
|
+
chalk18.yellow(
|
|
2496
3322
|
`Warning: Failed to download "${failure.name ?? failure.id}": ${failure.error}
|
|
2497
3323
|
`
|
|
2498
3324
|
)
|
|
2499
3325
|
);
|
|
2500
3326
|
}
|
|
2501
3327
|
io.err.write(
|
|
2502
|
-
|
|
3328
|
+
chalk18.green(`Exported ${successCount} icons to ${options.output}
|
|
2503
3329
|
`)
|
|
2504
3330
|
);
|
|
2505
3331
|
if (failures.length > 0) {
|
|
2506
|
-
io.err.write(
|
|
3332
|
+
io.err.write(chalk18.yellow(`${failures.length} icons failed
|
|
2507
3333
|
`));
|
|
2508
3334
|
process.exit(1);
|
|
2509
3335
|
}
|
|
2510
3336
|
} catch (error) {
|
|
2511
|
-
io.err.write(
|
|
3337
|
+
io.err.write(chalk18.red("Error exporting icons.\n"));
|
|
2512
3338
|
if (error instanceof Error) {
|
|
2513
|
-
io.err.write(
|
|
3339
|
+
io.err.write(chalk18.dim(`${error.message}
|
|
2514
3340
|
`));
|
|
2515
3341
|
}
|
|
2516
3342
|
process.exit(1);
|
|
@@ -2521,33 +3347,35 @@ function createExportIconsCommand(factory) {
|
|
|
2521
3347
|
}
|
|
2522
3348
|
|
|
2523
3349
|
// src/cmd/export/theme.ts
|
|
2524
|
-
import { Command as
|
|
2525
|
-
import
|
|
2526
|
-
import * as
|
|
2527
|
-
import * as
|
|
3350
|
+
import { Command as Command24 } from "commander";
|
|
3351
|
+
import chalk19 from "chalk";
|
|
3352
|
+
import * as fs8 from "fs";
|
|
3353
|
+
import * as path6 from "path";
|
|
2528
3354
|
function createExportThemeCommand(factory) {
|
|
2529
|
-
const cmd = new
|
|
3355
|
+
const cmd = new Command24("theme").description("Export complete theme package from a Figma file").argument("<url>", "Figma URL or file key").option("-o, --output <dir>", "Output directory", "./theme").option(
|
|
2530
3356
|
"--formats <formats>",
|
|
2531
3357
|
"Comma-separated list of formats (json, css, scss, style-dictionary, tailwind)",
|
|
2532
3358
|
"json,css"
|
|
2533
3359
|
).option("-t, --token <token>", "Override authentication token").action(
|
|
2534
|
-
async (
|
|
3360
|
+
async (url, options) => {
|
|
2535
3361
|
const { io } = factory;
|
|
2536
3362
|
try {
|
|
3363
|
+
const parsed = parseFigmaInput(url);
|
|
3364
|
+
const fileKey = parsed.fileKey;
|
|
2537
3365
|
const token = await factory.getToken(options.token);
|
|
2538
3366
|
let client = await factory.getClient();
|
|
2539
3367
|
if (options.token) {
|
|
2540
3368
|
const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
2541
3369
|
client = new FigmaClient2({ token });
|
|
2542
3370
|
}
|
|
2543
|
-
io.err.write(
|
|
3371
|
+
io.err.write(chalk19.dim("Fetching file styles...\n"));
|
|
2544
3372
|
const stylesResponse = await client.getFileStyles(fileKey);
|
|
2545
3373
|
const styles = stylesResponse.meta?.styles || [];
|
|
2546
|
-
io.err.write(
|
|
3374
|
+
io.err.write(chalk19.dim("Extracting token values...\n"));
|
|
2547
3375
|
const file = await client.getFile(fileKey);
|
|
2548
3376
|
const tokens = extractStylesFromFile(file, styles);
|
|
2549
|
-
if (!
|
|
2550
|
-
|
|
3377
|
+
if (!fs8.existsSync(options.output)) {
|
|
3378
|
+
fs8.mkdirSync(options.output, { recursive: true });
|
|
2551
3379
|
}
|
|
2552
3380
|
const formats = options.formats.split(",").map((f) => f.trim());
|
|
2553
3381
|
const outputs = [];
|
|
@@ -2576,8 +3404,8 @@ function createExportThemeCommand(factory) {
|
|
|
2576
3404
|
output = JSON.stringify(tokens, null, 2);
|
|
2577
3405
|
filename = "tokens.json";
|
|
2578
3406
|
}
|
|
2579
|
-
const filepath =
|
|
2580
|
-
|
|
3407
|
+
const filepath = path6.join(options.output, filename);
|
|
3408
|
+
fs8.writeFileSync(filepath, output);
|
|
2581
3409
|
outputs.push(filename);
|
|
2582
3410
|
}
|
|
2583
3411
|
const metadata = {
|
|
@@ -2587,22 +3415,22 @@ function createExportThemeCommand(factory) {
|
|
|
2587
3415
|
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2588
3416
|
formats
|
|
2589
3417
|
};
|
|
2590
|
-
|
|
2591
|
-
|
|
3418
|
+
fs8.writeFileSync(
|
|
3419
|
+
path6.join(options.output, "metadata.json"),
|
|
2592
3420
|
JSON.stringify(metadata, null, 2)
|
|
2593
3421
|
);
|
|
2594
|
-
io.out.write(
|
|
3422
|
+
io.out.write(chalk19.green(`\u2713 Theme exported to ${options.output}
|
|
2595
3423
|
`));
|
|
2596
|
-
io.out.write(
|
|
3424
|
+
io.out.write(chalk19.dim(" Files:\n"));
|
|
2597
3425
|
for (const filename of outputs) {
|
|
2598
|
-
io.out.write(
|
|
3426
|
+
io.out.write(chalk19.dim(` - ${filename}
|
|
2599
3427
|
`));
|
|
2600
3428
|
}
|
|
2601
|
-
io.out.write(
|
|
3429
|
+
io.out.write(chalk19.dim(" - metadata.json\n"));
|
|
2602
3430
|
} catch (error) {
|
|
2603
|
-
io.err.write(
|
|
3431
|
+
io.err.write(chalk19.red("Error exporting theme.\n"));
|
|
2604
3432
|
if (error instanceof Error) {
|
|
2605
|
-
io.err.write(
|
|
3433
|
+
io.err.write(chalk19.dim(`${error.message}
|
|
2606
3434
|
`));
|
|
2607
3435
|
}
|
|
2608
3436
|
process.exit(1);
|
|
@@ -2614,7 +3442,7 @@ function createExportThemeCommand(factory) {
|
|
|
2614
3442
|
|
|
2615
3443
|
// src/cmd/export/index.ts
|
|
2616
3444
|
function createExportCommand(factory) {
|
|
2617
|
-
const cmd = new
|
|
3445
|
+
const cmd = new Command25("export").description(
|
|
2618
3446
|
"Design-to-code export commands"
|
|
2619
3447
|
);
|
|
2620
3448
|
cmd.addCommand(createExportTokensCommand(factory));
|
|
@@ -2625,10 +3453,10 @@ function createExportCommand(factory) {
|
|
|
2625
3453
|
|
|
2626
3454
|
// src/cmd/api/index.ts
|
|
2627
3455
|
init_client();
|
|
2628
|
-
import { Command as
|
|
2629
|
-
import
|
|
3456
|
+
import { Command as Command26 } from "commander";
|
|
3457
|
+
import chalk20 from "chalk";
|
|
2630
3458
|
function createApiCommand(factory) {
|
|
2631
|
-
const cmd = new
|
|
3459
|
+
const cmd = new Command26("api").description("Make direct API requests to Figma").argument("<path>", "API path (e.g., /v1/files/:key)").option("-X, --method <method>", "HTTP method", "GET").option("-d, --data <json>", "Request body as JSON").option("-q, --query <params>", "Query parameters (key=value,key2=value2)").option("-t, --token <token>", "Override authentication token").option("--raw", "Output raw response without formatting").action(
|
|
2632
3460
|
async (apiPath, options) => {
|
|
2633
3461
|
const { io } = factory;
|
|
2634
3462
|
try {
|
|
@@ -2653,7 +3481,7 @@ function createApiCommand(factory) {
|
|
|
2653
3481
|
try {
|
|
2654
3482
|
body = JSON.parse(options.data);
|
|
2655
3483
|
} catch {
|
|
2656
|
-
io.err.write(
|
|
3484
|
+
io.err.write(chalk20.red("Error: Invalid JSON in --data\n"));
|
|
2657
3485
|
process.exit(1);
|
|
2658
3486
|
}
|
|
2659
3487
|
}
|
|
@@ -2670,15 +3498,15 @@ function createApiCommand(factory) {
|
|
|
2670
3498
|
} catch (error) {
|
|
2671
3499
|
if (error instanceof FigmaApiError) {
|
|
2672
3500
|
io.err.write(
|
|
2673
|
-
|
|
3501
|
+
chalk20.red(`API Error: ${error.status} ${error.statusText}
|
|
2674
3502
|
`)
|
|
2675
3503
|
);
|
|
2676
3504
|
if (error.body) {
|
|
2677
|
-
io.err.write(
|
|
3505
|
+
io.err.write(chalk20.dim(JSON.stringify(error.body, null, 2)));
|
|
2678
3506
|
io.err.write("\n");
|
|
2679
3507
|
}
|
|
2680
3508
|
} else if (error instanceof Error) {
|
|
2681
|
-
io.err.write(
|
|
3509
|
+
io.err.write(chalk20.red(`Error: ${error.message}
|
|
2682
3510
|
`));
|
|
2683
3511
|
}
|
|
2684
3512
|
process.exit(1);
|
|
@@ -2699,15 +3527,515 @@ Examples:
|
|
|
2699
3527
|
return cmd;
|
|
2700
3528
|
}
|
|
2701
3529
|
|
|
3530
|
+
// src/cmd/frame/index.ts
|
|
3531
|
+
import { Command as Command28 } from "commander";
|
|
3532
|
+
|
|
3533
|
+
// src/cmd/frame/list.ts
|
|
3534
|
+
import { Command as Command27 } from "commander";
|
|
3535
|
+
import chalk21 from "chalk";
|
|
3536
|
+
var FRAME_TYPES = [
|
|
3537
|
+
"FRAME",
|
|
3538
|
+
"COMPONENT",
|
|
3539
|
+
"COMPONENT_SET",
|
|
3540
|
+
"SECTION",
|
|
3541
|
+
"GROUP",
|
|
3542
|
+
"INSTANCE"
|
|
3543
|
+
];
|
|
3544
|
+
function extractFrames(file, options) {
|
|
3545
|
+
const frames = [];
|
|
3546
|
+
const allowedTypes = options.types || FRAME_TYPES;
|
|
3547
|
+
if (!file.document || !("children" in file.document)) {
|
|
3548
|
+
return frames;
|
|
3549
|
+
}
|
|
3550
|
+
const doc = file.document;
|
|
3551
|
+
for (const pageNode of doc.children || []) {
|
|
3552
|
+
let traverseNode2 = function(node, isTopLevel) {
|
|
3553
|
+
const nodeWithBounds = node;
|
|
3554
|
+
if (allowedTypes.includes(node.type)) {
|
|
3555
|
+
if (!options.topLevel || isTopLevel) {
|
|
3556
|
+
const frame = {
|
|
3557
|
+
id: node.id,
|
|
3558
|
+
name: node.name,
|
|
3559
|
+
type: node.type,
|
|
3560
|
+
page: pageNode.name,
|
|
3561
|
+
pageId: pageNode.id
|
|
3562
|
+
};
|
|
3563
|
+
if (nodeWithBounds.absoluteBoundingBox) {
|
|
3564
|
+
frame.width = Math.round(nodeWithBounds.absoluteBoundingBox.width);
|
|
3565
|
+
frame.height = Math.round(
|
|
3566
|
+
nodeWithBounds.absoluteBoundingBox.height
|
|
3567
|
+
);
|
|
3568
|
+
}
|
|
3569
|
+
frames.push(frame);
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
if (!options.topLevel && "children" in node) {
|
|
3573
|
+
const parent = node;
|
|
3574
|
+
for (const child of parent.children || []) {
|
|
3575
|
+
traverseNode2(child, false);
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
};
|
|
3579
|
+
var traverseNode = traverseNode2;
|
|
3580
|
+
if (pageNode.type !== "CANVAS") continue;
|
|
3581
|
+
if (options.page && pageNode.name !== options.page) continue;
|
|
3582
|
+
if (!("children" in pageNode)) continue;
|
|
3583
|
+
const page = pageNode;
|
|
3584
|
+
for (const child of page.children || []) {
|
|
3585
|
+
traverseNode2(child, true);
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
3588
|
+
return frames;
|
|
3589
|
+
}
|
|
3590
|
+
function formatTable2(frames) {
|
|
3591
|
+
if (frames.length === 0) {
|
|
3592
|
+
return "No frames found.";
|
|
3593
|
+
}
|
|
3594
|
+
const headers = ["ID", "Name", "Type", "Page", "Size"];
|
|
3595
|
+
const rows = frames.map((f) => [
|
|
3596
|
+
f.id,
|
|
3597
|
+
f.name,
|
|
3598
|
+
f.type,
|
|
3599
|
+
f.page,
|
|
3600
|
+
f.width && f.height ? `${f.width}\xD7${f.height}` : "-"
|
|
3601
|
+
]);
|
|
3602
|
+
const widths = headers.map(
|
|
3603
|
+
(h, i) => Math.max(h.length, ...rows.map((r) => r[i].length))
|
|
3604
|
+
);
|
|
3605
|
+
const separator = widths.map((w) => "-".repeat(w)).join("-+-");
|
|
3606
|
+
const headerLine = headers.map((h, i) => h.padEnd(widths[i])).join(" | ");
|
|
3607
|
+
const rowLines = rows.map(
|
|
3608
|
+
(r) => r.map((cell, i) => cell.padEnd(widths[i])).join(" | ")
|
|
3609
|
+
);
|
|
3610
|
+
return [headerLine, separator, ...rowLines].join("\n");
|
|
3611
|
+
}
|
|
3612
|
+
function createFrameListCommand(factory) {
|
|
3613
|
+
const cmd = new Command27("list").description("List frames in a file").requiredOption("--file <url>", "Figma URL or file key").option("--page <name>", "Filter to specific page").option(
|
|
3614
|
+
"--type <types>",
|
|
3615
|
+
"Comma-separated types: frame,component,component_set,section,group,instance"
|
|
3616
|
+
).option("--top-level", "Only top-level frames", false).option("-f, --format <format>", "Output format (json, table)", "json").option("-t, --token <token>", "Override authentication token").action(
|
|
3617
|
+
async (options) => {
|
|
3618
|
+
const { io } = factory;
|
|
3619
|
+
const validFormats = ["json", "table"];
|
|
3620
|
+
if (!validFormats.includes(
|
|
3621
|
+
options.format
|
|
3622
|
+
)) {
|
|
3623
|
+
io.err.write(
|
|
3624
|
+
chalk21.red(
|
|
3625
|
+
`Invalid format "${options.format}". Valid formats: ${validFormats.join(", ")}
|
|
3626
|
+
`
|
|
3627
|
+
)
|
|
3628
|
+
);
|
|
3629
|
+
process.exit(1);
|
|
3630
|
+
}
|
|
3631
|
+
try {
|
|
3632
|
+
const parsed = parseFigmaInput(options.file);
|
|
3633
|
+
const fileKey = parsed.fileKey;
|
|
3634
|
+
let types;
|
|
3635
|
+
if (options.type) {
|
|
3636
|
+
const typeMap = {
|
|
3637
|
+
frame: "FRAME",
|
|
3638
|
+
component: "COMPONENT",
|
|
3639
|
+
component_set: "COMPONENT_SET",
|
|
3640
|
+
section: "SECTION",
|
|
3641
|
+
group: "GROUP",
|
|
3642
|
+
instance: "INSTANCE"
|
|
3643
|
+
};
|
|
3644
|
+
types = options.type.split(",").map((t) => {
|
|
3645
|
+
const mapped = typeMap[t.trim().toLowerCase()];
|
|
3646
|
+
if (!mapped) {
|
|
3647
|
+
throw new Error(
|
|
3648
|
+
`Invalid type "${t}". Valid types: ${Object.keys(typeMap).join(", ")}`
|
|
3649
|
+
);
|
|
3650
|
+
}
|
|
3651
|
+
return mapped;
|
|
3652
|
+
});
|
|
3653
|
+
}
|
|
3654
|
+
const client = await factory.getClient();
|
|
3655
|
+
let activeClient = client;
|
|
3656
|
+
if (options.token) {
|
|
3657
|
+
const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
3658
|
+
activeClient = new FigmaClient2({ token: options.token });
|
|
3659
|
+
}
|
|
3660
|
+
const file = await activeClient.getFile(fileKey, {
|
|
3661
|
+
depth: options.topLevel ? 2 : void 0
|
|
3662
|
+
});
|
|
3663
|
+
const frames = extractFrames(file, {
|
|
3664
|
+
page: options.page,
|
|
3665
|
+
types,
|
|
3666
|
+
topLevel: options.topLevel
|
|
3667
|
+
});
|
|
3668
|
+
if (options.format === "table") {
|
|
3669
|
+
io.out.write(formatTable2(frames) + "\n");
|
|
3670
|
+
} else {
|
|
3671
|
+
io.out.write(
|
|
3672
|
+
formatOutput(frames, { format: options.format })
|
|
3673
|
+
);
|
|
3674
|
+
io.out.write("\n");
|
|
3675
|
+
}
|
|
3676
|
+
} catch (error) {
|
|
3677
|
+
io.err.write(chalk21.red("Error listing frames.\n"));
|
|
3678
|
+
if (error instanceof Error) {
|
|
3679
|
+
io.err.write(chalk21.dim(`${error.message}
|
|
3680
|
+
`));
|
|
3681
|
+
}
|
|
3682
|
+
process.exit(1);
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3685
|
+
);
|
|
3686
|
+
return cmd;
|
|
3687
|
+
}
|
|
3688
|
+
|
|
3689
|
+
// src/cmd/frame/index.ts
|
|
3690
|
+
function createFrameCommand(factory) {
|
|
3691
|
+
const cmd = new Command28("frame").description("Work with Figma frames");
|
|
3692
|
+
cmd.addCommand(createFrameListCommand(factory));
|
|
3693
|
+
return cmd;
|
|
3694
|
+
}
|
|
3695
|
+
|
|
3696
|
+
// src/cmd/handoff/index.ts
|
|
3697
|
+
import { Command as Command29 } from "commander";
|
|
3698
|
+
import chalk22 from "chalk";
|
|
3699
|
+
import * as fs9 from "fs";
|
|
3700
|
+
import * as path7 from "path";
|
|
3701
|
+
function countTokens(tokens) {
|
|
3702
|
+
return {
|
|
3703
|
+
colors: Object.keys(tokens.colors || {}).length,
|
|
3704
|
+
typography: Object.keys(tokens.typography || {}).length,
|
|
3705
|
+
shadows: Object.keys(tokens.shadows || {}).length,
|
|
3706
|
+
radii: Object.keys(tokens.radii || {}).length,
|
|
3707
|
+
spacing: Object.keys(tokens.spacing || {}).length
|
|
3708
|
+
};
|
|
3709
|
+
}
|
|
3710
|
+
function extractComponents(file) {
|
|
3711
|
+
const components = [];
|
|
3712
|
+
function traverseNode(node) {
|
|
3713
|
+
if (node.type === "COMPONENT" || node.type === "COMPONENT_SET") {
|
|
3714
|
+
components.push({
|
|
3715
|
+
id: node.id,
|
|
3716
|
+
name: node.name,
|
|
3717
|
+
type: node.type
|
|
3718
|
+
});
|
|
3719
|
+
}
|
|
3720
|
+
if ("children" in node) {
|
|
3721
|
+
const parent = node;
|
|
3722
|
+
for (const child of parent.children || []) {
|
|
3723
|
+
traverseNode(child);
|
|
3724
|
+
}
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
if (file.document) {
|
|
3728
|
+
traverseNode(file.document);
|
|
3729
|
+
}
|
|
3730
|
+
return components;
|
|
3731
|
+
}
|
|
3732
|
+
function countPagesAndFrames(file) {
|
|
3733
|
+
let pages = 0;
|
|
3734
|
+
let frames = 0;
|
|
3735
|
+
if (file.document && "children" in file.document) {
|
|
3736
|
+
const doc = file.document;
|
|
3737
|
+
for (const pageNode of doc.children || []) {
|
|
3738
|
+
if (pageNode.type === "CANVAS") {
|
|
3739
|
+
pages++;
|
|
3740
|
+
if ("children" in pageNode) {
|
|
3741
|
+
const page = pageNode;
|
|
3742
|
+
frames += (page.children || []).length;
|
|
3743
|
+
}
|
|
3744
|
+
}
|
|
3745
|
+
}
|
|
3746
|
+
}
|
|
3747
|
+
return { pages, frames };
|
|
3748
|
+
}
|
|
3749
|
+
function generateHandoffReadme(summary, tokenFormats) {
|
|
3750
|
+
const lines = [];
|
|
3751
|
+
lines.push(`# Design Handoff: ${summary.name}`);
|
|
3752
|
+
lines.push("");
|
|
3753
|
+
lines.push(`Source: ${summary.url}`);
|
|
3754
|
+
lines.push(`Generated: ${summary.generatedAt}`);
|
|
3755
|
+
lines.push("");
|
|
3756
|
+
lines.push("## Tokens Summary");
|
|
3757
|
+
lines.push(`- Colors: ${summary.tokens.colors} tokens`);
|
|
3758
|
+
lines.push(`- Typography: ${summary.tokens.typography} tokens`);
|
|
3759
|
+
lines.push(`- Shadows: ${summary.tokens.shadows} tokens`);
|
|
3760
|
+
lines.push(`- Radii: ${summary.tokens.radii} tokens`);
|
|
3761
|
+
lines.push(`- Spacing: ${summary.tokens.spacing} tokens`);
|
|
3762
|
+
lines.push("");
|
|
3763
|
+
if (summary.components.length > 0) {
|
|
3764
|
+
lines.push("## Component Inventory");
|
|
3765
|
+
lines.push("| ID | Name | Type | Asset |");
|
|
3766
|
+
lines.push("|----|------|------|-------|");
|
|
3767
|
+
for (const comp of summary.components) {
|
|
3768
|
+
lines.push(
|
|
3769
|
+
`| ${comp.id} | ${comp.name} | ${comp.type} | ${comp.asset || "-"} |`
|
|
3770
|
+
);
|
|
3771
|
+
}
|
|
3772
|
+
lines.push("");
|
|
3773
|
+
}
|
|
3774
|
+
lines.push("## File Structure");
|
|
3775
|
+
lines.push(
|
|
3776
|
+
`${summary.pages} pages, ${summary.frames} frames, ${summary.components.length} components`
|
|
3777
|
+
);
|
|
3778
|
+
lines.push("");
|
|
3779
|
+
lines.push("## Implementation Notes");
|
|
3780
|
+
if (tokenFormats.includes("css")) {
|
|
3781
|
+
lines.push("- Import tokens: `@import './tokens/tokens.css'`");
|
|
3782
|
+
}
|
|
3783
|
+
if (tokenFormats.includes("scss")) {
|
|
3784
|
+
lines.push("- SCSS import: `@import './tokens/tokens.scss'`");
|
|
3785
|
+
}
|
|
3786
|
+
if (summary.components.some((c) => c.asset)) {
|
|
3787
|
+
lines.push("- Asset path: `./assets/{name}.svg`");
|
|
3788
|
+
}
|
|
3789
|
+
lines.push("");
|
|
3790
|
+
return lines.join("\n");
|
|
3791
|
+
}
|
|
3792
|
+
function extractSimplifiedStructure(file) {
|
|
3793
|
+
const structure = {
|
|
3794
|
+
name: file.name,
|
|
3795
|
+
lastModified: file.lastModified,
|
|
3796
|
+
pages: []
|
|
3797
|
+
};
|
|
3798
|
+
if (file.document && "children" in file.document) {
|
|
3799
|
+
const doc = file.document;
|
|
3800
|
+
for (const pageNode of doc.children || []) {
|
|
3801
|
+
if (pageNode.type !== "CANVAS") continue;
|
|
3802
|
+
const page = {
|
|
3803
|
+
id: pageNode.id,
|
|
3804
|
+
name: pageNode.name,
|
|
3805
|
+
frames: []
|
|
3806
|
+
};
|
|
3807
|
+
if ("children" in pageNode) {
|
|
3808
|
+
const pageWithChildren = pageNode;
|
|
3809
|
+
for (const child of pageWithChildren.children || []) {
|
|
3810
|
+
const childWithBounds = child;
|
|
3811
|
+
const frame = {
|
|
3812
|
+
id: child.id,
|
|
3813
|
+
name: child.name,
|
|
3814
|
+
type: child.type
|
|
3815
|
+
};
|
|
3816
|
+
if (childWithBounds.absoluteBoundingBox) {
|
|
3817
|
+
frame.width = Math.round(childWithBounds.absoluteBoundingBox.width);
|
|
3818
|
+
frame.height = Math.round(
|
|
3819
|
+
childWithBounds.absoluteBoundingBox.height
|
|
3820
|
+
);
|
|
3821
|
+
}
|
|
3822
|
+
page.frames.push(frame);
|
|
3823
|
+
}
|
|
3824
|
+
}
|
|
3825
|
+
structure.pages.push(page);
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3828
|
+
return structure;
|
|
3829
|
+
}
|
|
3830
|
+
function createHandoffCommand(factory) {
|
|
3831
|
+
const cmd = new Command29("handoff").description("Export complete design handoff package").argument("<url>", "Figma URL or file key").option("-o, --output <dir>", "Output directory", "./figma-handoff").option(
|
|
3832
|
+
"--tokens <formats>",
|
|
3833
|
+
"Token formats to export (comma-separated: css,json,scss)",
|
|
3834
|
+
"css,json"
|
|
3835
|
+
).option("--assets", "Export component assets", false).option("--asset-format <format>", "Asset format (svg, png)", "svg").option("--structure", "Include structure.json", false).option("--readme", "Generate HANDOFF.md", false).option("--all", "Export all (tokens, assets, structure, readme)", false).option("-t, --token <token>", "Override authentication token").action(
|
|
3836
|
+
async (url, options) => {
|
|
3837
|
+
const { io } = factory;
|
|
3838
|
+
try {
|
|
3839
|
+
const parsed = parseFigmaInput(url);
|
|
3840
|
+
const fileKey = parsed.fileKey;
|
|
3841
|
+
const exportAssets = options.all || options.assets;
|
|
3842
|
+
const exportStructure = options.all || options.structure;
|
|
3843
|
+
const exportReadme = options.all || options.readme;
|
|
3844
|
+
const tokenFormats = options.tokens.split(",").map((f) => f.trim());
|
|
3845
|
+
const validTokenFormats = ["css", "scss", "json"];
|
|
3846
|
+
for (const format of tokenFormats) {
|
|
3847
|
+
if (!validTokenFormats.includes(
|
|
3848
|
+
format
|
|
3849
|
+
)) {
|
|
3850
|
+
io.err.write(
|
|
3851
|
+
chalk22.red(
|
|
3852
|
+
`Invalid token format "${format}". Valid formats: ${validTokenFormats.join(", ")}
|
|
3853
|
+
`
|
|
3854
|
+
)
|
|
3855
|
+
);
|
|
3856
|
+
process.exit(1);
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
const validAssetFormats = ["svg", "png"];
|
|
3860
|
+
if (!validAssetFormats.includes(
|
|
3861
|
+
options.assetFormat
|
|
3862
|
+
)) {
|
|
3863
|
+
io.err.write(
|
|
3864
|
+
chalk22.red(
|
|
3865
|
+
`Invalid asset format "${options.assetFormat}". Valid formats: ${validAssetFormats.join(", ")}
|
|
3866
|
+
`
|
|
3867
|
+
)
|
|
3868
|
+
);
|
|
3869
|
+
process.exit(1);
|
|
3870
|
+
}
|
|
3871
|
+
const assetFormat = options.assetFormat;
|
|
3872
|
+
const outputDir = path7.resolve(options.output);
|
|
3873
|
+
const cwd = process.cwd();
|
|
3874
|
+
const homeDir = process.env.HOME || "";
|
|
3875
|
+
if (!outputDir.startsWith(cwd) && !outputDir.startsWith(homeDir)) {
|
|
3876
|
+
io.err.write(
|
|
3877
|
+
chalk22.red(
|
|
3878
|
+
`Error: Output directory "${outputDir}" is outside your project and home directory. Use a relative path or a path within your home directory.
|
|
3879
|
+
`
|
|
3880
|
+
)
|
|
3881
|
+
);
|
|
3882
|
+
process.exit(1);
|
|
3883
|
+
}
|
|
3884
|
+
const tokensDir = path7.join(outputDir, "tokens");
|
|
3885
|
+
const assetsDir = path7.join(outputDir, "assets");
|
|
3886
|
+
fs9.mkdirSync(tokensDir, { recursive: true });
|
|
3887
|
+
if (exportAssets) {
|
|
3888
|
+
fs9.mkdirSync(assetsDir, { recursive: true });
|
|
3889
|
+
}
|
|
3890
|
+
io.err.write(chalk22.dim("Fetching Figma file...\n"));
|
|
3891
|
+
const client = await factory.getClient();
|
|
3892
|
+
let activeClient = client;
|
|
3893
|
+
if (options.token) {
|
|
3894
|
+
const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
3895
|
+
activeClient = new FigmaClient2({ token: options.token });
|
|
3896
|
+
}
|
|
3897
|
+
const [file, stylesResponse] = await Promise.all([
|
|
3898
|
+
activeClient.getFile(fileKey),
|
|
3899
|
+
activeClient.getFileStyles(fileKey)
|
|
3900
|
+
]);
|
|
3901
|
+
const styles = stylesResponse.meta?.styles || [];
|
|
3902
|
+
io.err.write(chalk22.dim("Extracting design tokens...\n"));
|
|
3903
|
+
const tokens = extractStylesFromFile(file, styles);
|
|
3904
|
+
for (const format of tokenFormats) {
|
|
3905
|
+
let content;
|
|
3906
|
+
let filename;
|
|
3907
|
+
switch (format) {
|
|
3908
|
+
case "css":
|
|
3909
|
+
content = tokensToCss(tokens);
|
|
3910
|
+
filename = "tokens.css";
|
|
3911
|
+
break;
|
|
3912
|
+
case "scss":
|
|
3913
|
+
content = tokensToScss(tokens);
|
|
3914
|
+
filename = "tokens.scss";
|
|
3915
|
+
break;
|
|
3916
|
+
case "json":
|
|
3917
|
+
default:
|
|
3918
|
+
content = JSON.stringify(tokens, null, 2);
|
|
3919
|
+
filename = "tokens.json";
|
|
3920
|
+
break;
|
|
3921
|
+
}
|
|
3922
|
+
fs9.writeFileSync(path7.join(tokensDir, filename), content + "\n");
|
|
3923
|
+
io.err.write(chalk22.green(` \u2713 ${filename}
|
|
3924
|
+
`));
|
|
3925
|
+
}
|
|
3926
|
+
const components = extractComponents(file);
|
|
3927
|
+
const { pages, frames } = countPagesAndFrames(file);
|
|
3928
|
+
if (exportAssets && components.length > 0) {
|
|
3929
|
+
io.err.write(chalk22.dim("Exporting component assets...\n"));
|
|
3930
|
+
const componentIds = components.map((c) => c.id);
|
|
3931
|
+
const images = await activeClient.getImages(fileKey, componentIds, {
|
|
3932
|
+
format: assetFormat
|
|
3933
|
+
});
|
|
3934
|
+
const imageUrls = images.images || {};
|
|
3935
|
+
if (Object.keys(imageUrls).length === 0) {
|
|
3936
|
+
io.err.write(
|
|
3937
|
+
chalk22.yellow(
|
|
3938
|
+
" Warning: No image URLs returned from Figma API.\n"
|
|
3939
|
+
)
|
|
3940
|
+
);
|
|
3941
|
+
}
|
|
3942
|
+
const downloadItems = Object.entries(imageUrls).map(
|
|
3943
|
+
([nodeId, imageUrl]) => {
|
|
3944
|
+
const component = components.find((c) => c.id === nodeId);
|
|
3945
|
+
const filename = component ? `${toKebabCase(component.name)}.${assetFormat}` : `${sanitizeFilename(nodeId)}.${assetFormat}`;
|
|
3946
|
+
if (component) {
|
|
3947
|
+
component.asset = filename;
|
|
3948
|
+
}
|
|
3949
|
+
return {
|
|
3950
|
+
id: nodeId,
|
|
3951
|
+
url: imageUrl ?? null,
|
|
3952
|
+
filename
|
|
3953
|
+
};
|
|
3954
|
+
}
|
|
3955
|
+
);
|
|
3956
|
+
const downloadResults = await downloadFiles(
|
|
3957
|
+
downloadItems,
|
|
3958
|
+
assetsDir
|
|
3959
|
+
);
|
|
3960
|
+
const successCount = downloadResults.filter(
|
|
3961
|
+
(r) => r.status === "success"
|
|
3962
|
+
).length;
|
|
3963
|
+
const failures = downloadResults.filter(
|
|
3964
|
+
(r) => r.status === "error"
|
|
3965
|
+
);
|
|
3966
|
+
io.err.write(chalk22.green(` \u2713 ${successCount} assets exported
|
|
3967
|
+
`));
|
|
3968
|
+
if (failures.length > 0) {
|
|
3969
|
+
io.err.write(
|
|
3970
|
+
chalk22.yellow(
|
|
3971
|
+
` Warning: ${failures.length} asset(s) failed to download:
|
|
3972
|
+
`
|
|
3973
|
+
)
|
|
3974
|
+
);
|
|
3975
|
+
for (const failure of failures) {
|
|
3976
|
+
io.err.write(
|
|
3977
|
+
chalk22.dim(
|
|
3978
|
+
` - ${failure.name ?? failure.id}: ${failure.error}
|
|
3979
|
+
`
|
|
3980
|
+
)
|
|
3981
|
+
);
|
|
3982
|
+
}
|
|
3983
|
+
process.exit(1);
|
|
3984
|
+
}
|
|
3985
|
+
}
|
|
3986
|
+
if (exportStructure) {
|
|
3987
|
+
io.err.write(chalk22.dim("Generating structure.json...\n"));
|
|
3988
|
+
const structure = extractSimplifiedStructure(file);
|
|
3989
|
+
fs9.writeFileSync(
|
|
3990
|
+
path7.join(outputDir, "structure.json"),
|
|
3991
|
+
JSON.stringify(structure, null, 2) + "\n"
|
|
3992
|
+
);
|
|
3993
|
+
io.err.write(chalk22.green(" \u2713 structure.json\n"));
|
|
3994
|
+
}
|
|
3995
|
+
if (exportReadme) {
|
|
3996
|
+
io.err.write(chalk22.dim("Generating HANDOFF.md...\n"));
|
|
3997
|
+
const summary = {
|
|
3998
|
+
name: file.name,
|
|
3999
|
+
url: `https://www.figma.com/file/${fileKey}`,
|
|
4000
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4001
|
+
tokens: countTokens(tokens),
|
|
4002
|
+
components,
|
|
4003
|
+
pages,
|
|
4004
|
+
frames
|
|
4005
|
+
};
|
|
4006
|
+
const readme = generateHandoffReadme(summary, tokenFormats);
|
|
4007
|
+
fs9.writeFileSync(path7.join(outputDir, "HANDOFF.md"), readme);
|
|
4008
|
+
io.err.write(chalk22.green(" \u2713 HANDOFF.md\n"));
|
|
4009
|
+
}
|
|
4010
|
+
io.out.write(
|
|
4011
|
+
chalk22.bold.green(`
|
|
4012
|
+
\u2713 Handoff package created at ${outputDir}
|
|
4013
|
+
`)
|
|
4014
|
+
);
|
|
4015
|
+
} catch (error) {
|
|
4016
|
+
io.err.write(chalk22.red("Error creating handoff package.\n"));
|
|
4017
|
+
if (error instanceof Error) {
|
|
4018
|
+
io.err.write(chalk22.dim(`${error.message}
|
|
4019
|
+
`));
|
|
4020
|
+
}
|
|
4021
|
+
process.exit(1);
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
4024
|
+
);
|
|
4025
|
+
return cmd;
|
|
4026
|
+
}
|
|
4027
|
+
|
|
2702
4028
|
// src/cmd/root.ts
|
|
2703
4029
|
function createRootCommand(factory) {
|
|
2704
|
-
const program = new
|
|
4030
|
+
const program = new Command30();
|
|
2705
4031
|
program.name("figma").description("CLI for the Figma API").version("1.0.0").configureHelp({
|
|
2706
4032
|
sortSubcommands: true,
|
|
2707
4033
|
sortOptions: true
|
|
2708
4034
|
});
|
|
2709
4035
|
program.addCommand(createAuthCommand(factory));
|
|
2710
4036
|
program.addCommand(createFileCommand(factory));
|
|
4037
|
+
program.addCommand(createFrameCommand(factory));
|
|
4038
|
+
program.addCommand(createHandoffCommand(factory));
|
|
2711
4039
|
program.addCommand(createProjectCommand(factory));
|
|
2712
4040
|
program.addCommand(createComponentCommand(factory));
|
|
2713
4041
|
program.addCommand(createStyleCommand(factory));
|