import axios from "axios";
import dotenv from "dotenv";
import { writeFile } from "fs/promises";
dotenv.config();
const FIGMA_TOKEN = process.env.FIGMA_TOKEN;
const figmaApi = axios.create({
baseURL: "https://api.figma.com/v1/",
headers: { "X-Figma-Token": FIGMA_TOKEN },
});
// Helper function to normalize class names (replace colons and special chars)
function normalizeClassName(id: string): string {
return id.replace(/:/g, '-').replace(/;/g, '-').replace(/\./g, '-');
}
// Helper function to generate CSS from Figma node
function generateCSS(node: any, selector = ".figma-component", isChild = false): string {
let css = `${selector} {\n`;
// Add positioning for layout
if (node.type === "FRAME" || node.type === "GROUP" || node.type === "COMPONENT") {
css += ` position: relative;\n`;
}
if (node.absoluteBoundingBox) {
css += ` width: ${Math.round(node.absoluteBoundingBox.width)}px;\n`;
css += ` height: ${Math.round(node.absoluteBoundingBox.height)}px;\n`;
}
if (node.fills && node.fills.length > 0) {
const fill = node.fills[0];
if (fill.type === "SOLID" && fill.color && fill.visible !== false) {
const { r, g, b, a = 1 } = fill.color;
if (a > 0) {
css += ` background-color: rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${a});\n`;
}
}
}
if (node.strokes && node.strokes.length > 0 && node.strokeWeight) {
const stroke = node.strokes[0];
if (stroke.type === "SOLID" && stroke.color) {
const { r, g, b, a = 1 } = stroke.color;
css += ` border: ${node.strokeWeight}px solid rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${a});\n`;
}
}
if (node.cornerRadius && node.cornerRadius > 0) {
css += ` border-radius: ${node.cornerRadius}px;\n`;
}
if (node.type === "TEXT" && node.style) {
css += ` font-family: "${node.style.fontFamily}";\n`;
css += ` font-size: ${node.style.fontSize}px;\n`;
css += ` font-weight: ${node.style.fontWeight};\n`;
css += ` line-height: ${node.style.lineHeightPx || node.style.fontSize * 1.2}px;\n`;
if (node.style.textAlignHorizontal) {
css += ` text-align: ${node.style.textAlignHorizontal.toLowerCase()};\n`;
}
css += ` white-space: pre-wrap;\n`;
css += ` word-wrap: break-word;\n`;
}
if (node.effects && node.effects.length > 0) {
const shadows = node.effects
.filter((e: any) => e.type === "DROP_SHADOW" && e.visible !== false)
.map((e: any) => {
const { r, g, b, a = 1 } = e.color;
return `${e.offset.x}px ${e.offset.y}px ${e.radius}px rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${a})`;
});
if (shadows.length > 0) {
css += ` box-shadow: ${shadows.join(", ")};\n`;
}
}
// Add some padding for frames to improve readability
if (node.type === "FRAME" && node.children && node.children.length > 0) {
css += ` padding: 10px;\n`;
css += ` margin-bottom: 20px;\n`;
}
css += "}\n";
return css;
}
// Helper function to generate HTML from Figma node
function generateHTML(node: any, depth = 0): string {
const indent = " ".repeat(depth);
let html = "";
const normalizedId = normalizeClassName(node.id);
if (node.type === "TEXT") {
html += `${indent}<div class="text-${normalizedId}">${node.characters || ""}</div>\n`;
} else if (node.type === "FRAME" || node.type === "GROUP" || node.type === "COMPONENT") {
const className = `${node.type.toLowerCase()}-${normalizedId}`;
html += `${indent}<div class="${className}">\n`;
if (node.children) {
for (const child of node.children) {
html += generateHTML(child, depth + 1);
}
}
html += `${indent}</div>\n`;
} else if (node.type === "RECTANGLE" || node.type === "ELLIPSE" || node.type === "VECTOR") {
html += `${indent}<div class="${node.type.toLowerCase()}-${normalizedId}"></div>\n`;
} else if (node.type === "INSTANCE") {
const className = `instance-${normalizedId}`;
html += `${indent}<div class="${className}">\n`;
if (node.children) {
for (const child of node.children) {
html += generateHTML(child, depth + 1);
}
}
html += `${indent}</div>\n`;
}
return html;
}
async function generateDesignWithScreenshot(fileKey: string, nodeId: string) {
try {
console.log(`\nšØ Generating design with screenshot reference...\n`);
console.log(`File: ${fileKey}`);
console.log(`Node: ${nodeId}\n`);
// Step 1: Get the design data
console.log("š Step 1: Fetching Figma design data...");
const response = await figmaApi.get(`files/${fileKey}/nodes`, {
params: { ids: nodeId },
});
const nodeData = response.data.nodes[nodeId];
if (!nodeData || !nodeData.document) {
console.error(`ā Node ${nodeId} not found in file ${fileKey}`);
return;
}
const node = nodeData.document;
console.log(`ā
Found node: ${node.name} (${node.type})`);
console.log(` Size: ${node.absoluteBoundingBox?.width}x${node.absoluteBoundingBox?.height}px\n`);
// Step 2: Get the screenshot
console.log("šø Step 2: Fetching screenshot...");
let screenshotUrl = "";
let screenshotInfo = "";
try {
const imageResponse = await figmaApi.get(`images/${fileKey}`, {
params: { ids: nodeId, format: "png", scale: 2 }
});
if (imageResponse.data.images && imageResponse.data.images[nodeId]) {
screenshotUrl = imageResponse.data.images[nodeId];
console.log(`ā
Screenshot URL obtained`);
// Download the screenshot
console.log("ā¬ļø Downloading screenshot...");
const imgData = await axios.get(screenshotUrl, { responseType: "arraybuffer" });
const screenshotFile = `output/screenshot-${normalizeClassName(nodeId)}.png`;
await writeFile(screenshotFile, Buffer.from(imgData.data));
console.log(`ā
Screenshot saved: ${screenshotFile}`);
console.log(` Size: ${(imgData.data.length / 1024).toFixed(2)} KB\n`);
screenshotInfo = `
/*
DESIGN REFERENCE SCREENSHOT: ${screenshotFile}
Original URL: ${screenshotUrl}
šø A screenshot has been saved locally for visual reference.
Compare the generated HTML with this image for accuracy.
š” Tips for matching the design:
- Compare element positioning and spacing with screenshot
- Check font sizes and weights against visual appearance
- Verify colors match what you see in the image
- Adjust padding/margins for pixel-perfect alignment
- Consider using flexbox/grid for better layout control
*/
`;
}
} catch (imgError: any) {
console.warn(`ā ļø Could not fetch screenshot: ${imgError.message}`);
screenshotInfo = "/* Could not fetch screenshot, using Figma API data only */\n";
}
// Step 3: Generate CSS and HTML
console.log("šØ Step 3: Generating CSS and HTML...");
let allCSS = "";
function collectCSS(n: any): void {
const normalizedId = normalizeClassName(n.id);
const selector = `.${n.type.toLowerCase()}-${normalizedId}`;
allCSS += generateCSS(n, selector) + "\n";
if (n.children) {
for (const child of n.children) {
collectCSS(child);
}
}
}
collectCSS(node);
const html = generateHTML(node);
// Step 4: Create enhanced HTML
const fullHTML = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>${node.name || 'Figma Design'}</title>
<style>
${screenshotInfo}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
line-height: 1.6;
color: #333;
background: #f5f5f5;
padding: 20px;
}
/* GENERATED STYLES FROM FIGMA API */
${allCSS}
/*
š DESIGN INFORMATION:
- Component: ${node.name}
- Original size: ${node.absoluteBoundingBox?.width}x${node.absoluteBoundingBox?.height}px
- Generated from Figma API data with screenshot reference
ā
NEXT STEPS:
1. Open the screenshot file and compare with this HTML output
2. Adjust spacing, positioning, and colors as needed
3. Consider adding flexbox/grid for better layout matching
4. Fine-tune typography to match visual appearance
5. Add hover states and interactions if needed
*/
</style>
</head>
<body>
${screenshotUrl ? `<!--
šø SCREENSHOT REFERENCE SAVED: output/screenshot-${normalizeClassName(nodeId)}.png
Open this file side-by-side to compare and refine the design.
-->
` : ''}${html}</body>
</html>`;
// Save to file
const outputFile = "output/output-with-screenshot.html";
await writeFile(outputFile, fullHTML);
console.log(`ā
HTML/CSS saved: ${outputFile}\n`);
// Summary
console.log("š GENERATION SUMMARY:");
console.log(` ā
Design data: Extracted from Figma API`);
console.log(` ā
Screenshot: ${screenshotUrl ? 'Downloaded and saved' : 'Not available'}`);
console.log(` ā
HTML/CSS: Generated with reference comments`);
console.log(` ā
Output file: ${outputFile}\n`);
console.log("šÆ RECOMMENDED WORKFLOW:");
console.log(" 1. Open output/output-with-screenshot.html in browser");
if (screenshotUrl) {
console.log(` 2. Open output/screenshot-${normalizeClassName(nodeId)}.png`);
console.log(" 3. Compare side-by-side and adjust CSS as needed");
}
console.log(" 4. Fine-tune spacing, colors, and typography");
console.log(" 5. Test responsiveness and interactions\n");
} catch (error: any) {
console.error("ā Error:", error.message);
if (error.response) {
console.error("Response:", error.response.data);
}
}
}
// Test with Threat Intel Hub design
const fileKey = "INORb289gzR56ny4JuCPLo";
const nodeId = "2068:24962";
generateDesignWithScreenshot(fileKey, nodeId);