@piccollage/figma-asset-downloader
Version:
A Node.js tool for downloading and converting Figma assets for Android projects
950 lines (815 loc) • 31.8 kB
JavaScript
/**
* Figma Asset Downloader
*
* A Node.js tool for downloading and converting Figma assets for mobile projects.
* This tool allows you to download icons as SVG and convert them to Android vector XML format or iOS PDF format,
* as well as download images in different resolutions for both platforms.
*/
// Import required dependencies
const fs = require('fs-extra');
const path = require('path');
const axios = require('axios');
const chalk = require('chalk');
const { program } = require('commander');
const yaml = require('js-yaml');
const ora = require('ora');
const sharp = require('sharp');
const svgo = require('svgo');
const svg2vectordrawable = require('svg2vectordrawable');
require('dotenv').config();
// Constants
const API_BASE_URL = 'https://api.figma.com/v1';
const NEW_CONFIG_PATH = '.figma/figma-asset-downloader.config.yaml';
const OLD_CONFIG_PATH = '.figma/asset_download.yaml';
// Platform-specific scales
const ANDROID_DPI_SCALES = {
ldpi: 0.75,
mdpi: 1,
hdpi: 1.5,
xhdpi: 2,
xxhdpi: 3,
xxxhdpi: 4
};
const IOS_SCALES = {
'1x': 1,
'2x': 2,
'3x': 3,
'ipad_1x': 2,
'ipad_2x': 3
};
// Get Figma token from environment variable
const FIGMA_TOKEN = process.env.FIGMA_TOKEN;
if (!FIGMA_TOKEN) {
console.error(chalk.red('Error: Figma token is required'));
console.log('Please set the FIGMA_TOKEN environment variable');
console.log('Example: export FIGMA_TOKEN=your_figma_api_token');
process.exit(1);
}
// Configure the API client
const figmaApi = axios.create({
baseURL: API_BASE_URL,
headers: {
'X-Figma-Token': FIGMA_TOKEN
}
});
// Set up command line options
program
.version('1.2.1')
.description('Download and convert Figma assets for mobile projects')
.argument('[componentNames...]', 'Component names to download (e.g., icon/home img/banner)')
.option('-a, --all', 'Download all components')
.option('-f, --find-duplicate', 'Find and list all duplicate components')
.option('-s, --section <section>', 'Download components from a specific section')
.parse(process.argv);
const componentNames = program.args;
const downloadAll = program.opts().all;
const findDuplicate = program.opts().findDuplicate;
const sectionName = program.opts().section;
// Show help message if no component names are provided and neither --all nor --find-duplicate flags are set
if (componentNames.length === 0 && !downloadAll && !findDuplicate && !sectionName) {
console.log(chalk.blue('Figma Asset Downloader'));
console.log(chalk.blue('======================'));
console.log('\nUsage:');
console.log(' figma-asset-downloader [options] [componentNames...]');
console.log('\nOptions:');
console.log(' -a, --all Download all components');
console.log(' -f, --find-duplicate Find and list all duplicate components');
console.log(' -s, --section <section> Download components from a specific section');
console.log(' -V, --version Output the version number');
console.log(' -h, --help Display help for command');
console.log('\nExamples:');
console.log(' figma-asset-downloader icon/home # Download a specific icon');
console.log(' figma-asset-downloader img/banner # Download a specific image');
console.log(' figma-asset-downloader icon/home img/logo # Download multiple components');
console.log(' figma-asset-downloader --all # Download all components');
console.log(' figma-asset-downloader --find-duplicate # Find and list all duplicate components');
console.log(' figma-asset-downloader --section="Section Name" # Download components from a specific section');
process.exit(0);
}
/**
* Find and report duplicate components
*/
async function findDuplicateComponents(fileId, pageId = '', pageName = '') {
const spinner = ora('Fetching components from Figma to find duplicates...').start();
try {
// Get the file data from Figma API
const response = await figmaApi.get(`/files/${fileId}`);
const fileData = response.data;
// Extract all components, filtered by page ID or page name if provided
const allComponents = extractComponents(fileData, pageId, pageName);
// Filter to only include icon/ and img/ components
const relevantComponents = allComponents.filter(component =>
component.name.startsWith('icon/') || component.name.startsWith('img/')
);
spinner.succeed(`Found ${relevantComponents.length} icon and image components`);
// Group components by name to find duplicates
const componentsByName = new Map();
relevantComponents.forEach(component => {
if (!componentsByName.has(component.name)) {
componentsByName.set(component.name, []);
}
componentsByName.get(component.name).push(component);
});
// Filter to only include names with multiple components (duplicates)
const duplicates = new Map();
componentsByName.forEach((components, name) => {
if (components.length > 1) {
duplicates.set(name, components);
}
});
// Report results
if (duplicates.size === 0) {
console.log(chalk.green('\nNo duplicate components found!'));
} else {
console.log(chalk.red(`\nFound ${duplicates.size} component names with duplicates:`));
duplicates.forEach((components, name) => {
console.log(chalk.red(`\n${name} (${components.length} duplicates):`));
// Print links to the Figma file with the duplicated components focused
components.forEach((component, index) => {
const figmaLink = `https://www.figma.com/file/${fileId}?node-id=${encodeURIComponent(component.id)}`;
console.log(chalk.cyan(` ${index + 1}. ${component.path} - ${figmaLink}`));
});
});
console.log(chalk.yellow('\nPlease rename these components to ensure they have unique names.'));
}
return duplicates.size > 0;
} catch (error) {
spinner.fail('Error fetching components from Figma');
handleApiError(error);
process.exit(1);
}
}
/**
* Load configuration from YAML file
*/
function loadConfig() {
try {
let configFile;
// First try to read from the new path (.figma/figma-asset-downloader.config.yaml)
if (fs.existsSync(NEW_CONFIG_PATH)) {
configPath = NEW_CONFIG_PATH;
configFile = fs.readFileSync(NEW_CONFIG_PATH, 'utf8');
console.log(chalk.green(`Using configuration file from: ${NEW_CONFIG_PATH}`));
}
// If not found, try the old path (.figma/asset_download.yaml)
else if (fs.existsSync(OLD_CONFIG_PATH)) {
configPath = OLD_CONFIG_PATH;
configFile = fs.readFileSync(OLD_CONFIG_PATH, 'utf8');
console.log(chalk.green(`Using configuration file from: ${OLD_CONFIG_PATH}`));
}
// If neither exists, show error
else {
console.error(chalk.red(`Error: Configuration file not found at ${NEW_CONFIG_PATH} or ${OLD_CONFIG_PATH}`));
console.log('Please create a configuration file as described in the README');
process.exit(1);
}
const config = yaml.load(configFile);
// Validate required configuration
if (!config.fileId) {
console.error(chalk.red('Error: fileId is required in the configuration file'));
process.exit(1);
}
if (!config.platform) {
console.error(chalk.red('Error: platform is required in the configuration file'));
process.exit(1);
}
if (!['android', 'ios'].includes(config.platform)) {
console.error(chalk.red('Error: platform must be either "android" or "ios"'));
process.exit(1);
}
// Set default values if not provided
// pageId can be a string or an array of strings
if (!config.pageId) {
config.pageId = ''; // Empty string means search the entire file
}
// pageName can be a string or an array of strings
if (!config.pageName) {
config.pageName = ''; // Empty string means search the entire file
}
// Set platform-specific defaults
if (config.platform === 'android') {
if (!config.icons) {
config.icons = { path: 'res' };
}
if (!config.icons.prefix) {
config.icons.prefix = 'ic_';
}
if (!config.images) {
config.images = { path: 'res', format: 'webp', quality: 90 };
}
if (!config.images.format) {
config.images.format = 'webp';
}
if (!config.images.quality) {
config.images.quality = 90;
}
if (!config.images.prefix) {
config.images.prefix = 'img_';
}
} else { // iOS
if (!config.icons) {
config.icons = {};
}
if (!config.icons.path) {
config.icons.path = 'Assets.xcassets';
}
if (!config.icons.prefix) {
config.icons.prefix = '';
}
if (!config.images) {
config.images = {};
}
if (!config.images.path) {
config.images.path = 'Assets.xcassets';
}
if (!config.images.format) {
config.images.format = 'png';
}
if (!config.images.quality) {
config.images.quality = 90;
}
if (!config.images.prefix) {
config.images.prefix = '';
}
}
return config;
} catch (error) {
console.error(chalk.red('Error loading configuration:'), error.message);
process.exit(1);
}
}
/**
* Fetch components from Figma file
*/
async function fetchComponents(fileId, componentNames, pageId = '', pageName = '') {
const spinner = ora('Fetching components from Figma...').start();
try {
// Get the file data from Figma API
const response = await figmaApi.get(`/files/${fileId}`);
const fileData = response.data;
// Extract components from the file, filtered by page ID or page name if provided
const allComponents = extractComponents(fileData, pageId, pageName);
// Filter components by name if componentNames are provided and --all flag is not set
let filteredComponents = allComponents;
// Filter by section if section name is provided
if (sectionName) {
filteredComponents = allComponents.filter(component => {
// Check if the component path contains the section name
return component.path.includes(sectionName);
});
if (filteredComponents.length === 0) {
spinner.fail(`No components found in section: ${sectionName}`);
process.exit(1);
}
spinner.succeed(`Found ${filteredComponents.length} components in section: ${sectionName}`);
}
if (componentNames && componentNames.length > 0) {
// Check for duplicate component names
const nameMap = new Map();
const duplicates = new Map();
// First, find all exact matches and identify duplicates
componentNames.forEach(requestedName => {
const exactMatches = allComponents.filter(component => component.name === requestedName);
if (exactMatches.length > 1) {
// Store duplicates for error reporting
duplicates.set(requestedName, exactMatches);
} else if (exactMatches.length === 1) {
// Store single matches
nameMap.set(requestedName, exactMatches[0]);
}
});
// Handle duplicates if any
if (duplicates.size > 0) {
spinner.fail('Found duplicate components with the same name');
duplicates.forEach((components, name) => {
console.error(chalk.red(`Error: Multiple components found with the exact name "${name}"`));
console.error(chalk.red(`Cannot download because of ambiguity. Please rename components to be unique.`));
// Print links to the Figma file with the duplicated components focused
console.log(chalk.yellow('\nLinks to duplicated components:'));
components.forEach((component, index) => {
const figmaLink = `https://www.figma.com/file/${fileId}?node-id=${encodeURIComponent(component.id)}`;
console.log(chalk.cyan(`${index + 1}. ${component.path} - ${figmaLink}`));
});
});
process.exit(1);
}
// Filter to only include exact matches
filteredComponents = Array.from(nameMap.values());
} else if (!downloadAll && !sectionName) {
// This case should not happen due to the help message check at the beginning,
// but we'll keep it as a safeguard
spinner.fail('No component names provided. Use --all flag to download all components or --section to specify a section.');
process.exit(1);
}
if (filteredComponents.length === 0) {
spinner.fail('No components found matching the provided names exactly');
process.exit(1);
}
spinner.succeed(`Found ${filteredComponents.length} components with exact name matches`);
return filteredComponents;
} catch (error) {
spinner.fail('Error fetching components from Figma');
handleApiError(error);
process.exit(1);
}
}
/**
* Extract components from the Figma file data
*/
function extractComponents(fileData, pageId = '', pageName = '') {
const components = [];
const componentSets = new Map();
// Start with the document as the root node
let rootNode = fileData.document;
let foundPages = [];
// Convert pageId and pageName to arrays if they're strings
const pageIds = Array.isArray(pageId) ? pageId : (pageId ? [pageId] : []);
const pageNames = Array.isArray(pageName) ? pageName : (pageName ? [pageName] : []);
// Only search for specific pages if pageIds or pageNames are provided
if (pageIds.length > 0 || pageNames.length > 0) {
// Find pages that match either the ID or name
const findPages = (node) => {
const matchedPages = [];
// Check if this node is a page that matches our criteria
if (node.type === 'CANVAS') {
const idMatch = pageIds.includes(node.id);
const nameMatch = pageNames.includes(node.name);
if (idMatch || nameMatch) {
matchedPages.push(node);
if (idMatch) {
console.log(chalk.green(`Found page with ID: ${node.id} (${node.name})`));
}
if (nameMatch) {
console.log(chalk.green(`Found page with name: ${node.name} (${node.id})`));
}
}
}
// Recursively check children
if (node.children) {
for (const child of node.children) {
const childMatches = findPages(child);
matchedPages.push(...childMatches);
}
}
return matchedPages;
};
foundPages = findPages(rootNode);
if (foundPages.length > 0) {
console.log(chalk.green(`Found ${foundPages.length} matching pages`));
} else {
console.log(chalk.yellow(`Warning: No pages found matching the specified criteria. Searching the entire file instead.`));
}
}
// Process each found page or the entire document if no pages were found
const nodesToProcess = foundPages.length > 0 ? foundPages : [rootNode];
// First pass: collect component sets from all matching pages
for (const node of nodesToProcess) {
traverseNode(node, (node, path) => {
if (node.type === 'COMPONENT_SET') {
componentSets.set(node.id, {
node,
path
});
}
});
}
// Second pass: collect components from all matching pages
for (const node of nodesToProcess) {
traverseNode(node, (node, path) => {
if (node.type === 'COMPONENT') {
// Check if this component is part of a component set
let parentComponentSet = null;
if (node.componentSetId) {
const componentSetInfo = componentSets.get(node.componentSetId);
if (componentSetInfo) {
parentComponentSet = {
id: node.componentSetId,
name: componentSetInfo.node.name,
path: componentSetInfo.path
};
}
}
components.push({
id: node.id,
name: node.name,
path: path.join(' / '),
type: node.type,
description: node.description || '',
width: node.absoluteBoundingBox ? node.absoluteBoundingBox.width : null,
height: node.absoluteBoundingBox ? node.absoluteBoundingBox.height : null,
componentSetId: node.componentSetId || null,
componentSet: parentComponentSet
});
}
});
}
return components;
}
/**
* Traverse the Figma document tree recursively
*/
function traverseNode(node, callback, path = [], depth = 0) {
// Skip nodes without a name or with names starting with '#'
if (!node.name || node.name.startsWith('#')) {
return;
}
const currentPath = [...path, node.name];
// Call the callback for this node
callback(node, currentPath, depth);
// Recursively process children
if (node.children) {
for (const child of node.children) {
traverseNode(child, callback, currentPath, depth + 1);
}
}
}
/**
* Get image URLs for components
*/
async function getImageUrls(fileId, components, format = 'svg', scale = 1, platform) {
try {
const componentIds = components.map(component => component.id).join(',');
// Request the highest scale for images to ensure high quality
const response = await figmaApi.get(`/images/${fileId}?ids=${componentIds}&format=${format}&scale=${scale}`);
if (!response.data.images) {
throw new Error(`No image URLs returned from Figma API (format: ${format}, scale: ${scale})`);
}
return response.data.images;
} catch (error) {
handleApiError(error);
throw error;
}
}
/**
* Download an SVG file from a URL
*/
async function downloadSvg(url) {
try {
const response = await axios.get(url);
return response.data;
} catch (error) {
console.error(chalk.red(`Error downloading SVG: ${error.message}`));
throw error;
}
}
/**
* Optimize SVG content
*/
async function optimizeSvg(svgContent) {
try {
const result = await svgo.optimize(svgContent, {
plugins: [
{
name: 'preset-default',
params: {
overrides: {
cleanupIds: false,
removeViewBox: false
}
}
}
]
});
return result.data;
} catch (error) {
console.error(chalk.red(`Error optimizing SVG: ${error.message}`));
return svgContent; // Return original content if optimization fails
}
}
/**
* Convert SVG to platform-specific format
*/
async function convertSvgForPlatform(svgContent, platform) {
try {
if (platform === 'android') {
// Convert to Android vector drawable
const options = {
xmlTag: true,
fillBlack: false
};
const xmlContent = await svg2vectordrawable(svgContent, options);
return xmlContent;
} else {
// For iOS, keep the optimized SVG
return svgContent;
}
} catch (error) {
console.error(chalk.red(`Error converting SVG for ${platform}: ${error.message}`));
throw error;
}
}
/**
* Download an image from a URL
*/
async function downloadImage(url) {
try {
const response = await axios.get(url, { responseType: 'arraybuffer' });
return Buffer.from(response.data, 'binary');
} catch (error) {
console.error(chalk.red(`Error downloading image: ${error.message}`));
throw error;
}
}
/**
* Create Contents.json for iOS assets
*/
function createContentsJson(name, format = 'png') {
if (format === 'svg') {
return JSON.stringify({
images: [
{
filename: `${name}.${format}`,
idiom: 'universal'
}
],
info: {
author: 'Figma Asset Downloader',
version: 1
},
properties: {
"template-rendering-intent": "original"
}
}, null, 2);
}
return JSON.stringify({
images: [
{
filename: `${name}.${format}`,
idiom: 'universal',
scale: '1x'
},
{
filename: `${name}@2x.${format}`,
idiom: 'universal',
scale: '2x'
},
{
filename: `${name}@3x.${format}`,
idiom: 'universal',
scale: '3x'
},
{
filename: `${name}~ipad.${format}`,
idiom: 'ipad',
scale: '1x'
},
{
filename: `${name}~ipad@2x.${format}`,
idiom: 'ipad',
scale: '2x'
}
],
info: {
version: 1,
author: 'xcode'
},
properties: {
"template-rendering-intent": "original"
}
}, null, 2);
}
/**
* Handle API errors
*/
function handleApiError(error) {
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
console.error(chalk.red(`Status: ${error.response.status}`));
console.error(chalk.red(`Message: ${JSON.stringify(error.response.data)}`));
if (error.response.status === 403) {
console.error(chalk.yellow('This might be due to an invalid Figma token or insufficient permissions.'));
} else if (error.response.status === 404) {
console.error(chalk.yellow('The Figma file could not be found.'));
console.error(chalk.yellow('Make sure the file ID is correct and that you have access to this file.'));
}
} else if (error.request) {
// The request was made but no response was received
console.error(chalk.red('No response received from the Figma API.'));
console.error(chalk.yellow('Please check your internet connection and try again.'));
} else {
// Something happened in setting up the request that triggered an Error
console.error(chalk.red(`Message: ${error.message}`));
}
}
/**
* Process and save icons from Figma components
*/
async function processIcons(components, fileId, config) {
const iconComponents = components.filter(component => component.name.startsWith('icon/'));
if (iconComponents.length === 0) return [];
console.log(chalk.yellow(`\nProcessing ${iconComponents.length} icons...`));
// Get SVG URLs for icons
const spinner = ora(`Getting image URLs from Figma...`).start();
let iconUrls;
try {
iconUrls = await getImageUrls(fileId, iconComponents, 'svg', 1, config.platform);
spinner.succeed(`Successfully retrieved image URLs`);
} catch (error) {
spinner.fail(`Error getting image URLs from Figma`);
console.error(chalk.red(error.message));
return [];
}
// Process each icon
let iconCounter = 0;
const totalIcons = iconComponents.length;
const processedComponentNames = new Set();
for (const component of iconComponents) {
iconCounter++;
const imageUrl = iconUrls[component.id];
if (imageUrl) {
const spinner = ora(`Processing icon (${iconCounter}/${totalIcons}): ${component.name}`).start();
try {
// Extract the icon name from the component name (remove 'icon/' prefix)
const iconName = component.name.replace('icon/', '');
const sanitizedName = iconName.replace(/\s+/g, '_').toLowerCase();
const fileName = `${config.icons.prefix}${sanitizedName}`;
// Download and process the SVG
const svgContent = await downloadSvg(imageUrl);
const optimizedSvg = await optimizeSvg(svgContent);
if (config.platform === 'android') {
// Convert to vector drawable XML
const xmlContent = await convertSvgForPlatform(optimizedSvg, 'android');
const drawablePath = path.join(config.icons.path, 'drawable');
await fs.ensureDir(drawablePath);
const filePath = path.join(drawablePath, `${fileName}.xml`);
await fs.writeFile(filePath, xmlContent, 'utf8');
} else {
// Create asset catalog structure
const assetPath = path.join(config.icons.path, `${fileName}.imageset`);
await fs.ensureDir(assetPath);
await fs.emptyDir(assetPath);
// Save SVG directly for iOS
const filePath = path.join(assetPath, `${fileName}.svg`);
await fs.writeFile(filePath, optimizedSvg, 'utf8');
// Create Contents.json
const contentsPath = path.join(assetPath, 'Contents.json');
const contents = createContentsJson(fileName, config.images.format);
await fs.writeFile(contentsPath, contents, 'utf8');
}
spinner.succeed(`Icon saved (${iconCounter}/${totalIcons}): ${fileName}`);
processedComponentNames.add(component.name);
} catch (error) {
spinner.fail(`Failed to process icon (${iconCounter}/${totalIcons}): ${component.name}`);
console.error(chalk.red(error.message));
}
} else {
console.error(chalk.red(`No image URL found for icon (${iconCounter}/${totalIcons}): ${component.name}`));
}
}
return processedComponentNames;
}
/**
* Process and save images from Figma components
*/
async function processImages(components, fileId, config) {
const imageComponents = components.filter(component => component.name.startsWith('img/'));
if (imageComponents.length === 0) return [];
console.log(chalk.yellow(`\nProcessing ${imageComponents.length} images...`));
// Process each image
let imageCounter = 0;
const totalImages = imageComponents.length;
const processedComponentNames = new Set();
for (const component of imageComponents) {
imageCounter++;
// Extract the image name from the component name (remove 'img/' prefix)
const imageName = component.name.replace('img/', '');
const sanitizedName = imageName.replace(/\s+/g, '_').toLowerCase();
const fileNameBase = `${config.images.prefix}${sanitizedName}`;
let failed = false;
if (config.platform === 'android') {
// Get the list of DPIs to process (exclude any in skipDpi)
const dpisToProcess = Object.keys(ANDROID_DPI_SCALES).filter(dpi =>
!config.images.skipDpi || !config.images.skipDpi.includes(dpi)
);
// Process for each DPI
for (const dpi of dpisToProcess) {
const scale = ANDROID_DPI_SCALES[dpi];
const spinner = ora(`Processing image (${imageCounter}/${totalImages}): ${component.name} [${dpi}]`).start();
try {
const drawablePath = path.join(config.images.path, `drawable-${dpi}`);
await fs.ensureDir(drawablePath);
const fileName = `${fileNameBase}.${config.images.format}`;
const filePath = path.join(drawablePath, fileName);
// Download the image from Figma at the correct scale
const imageUrls = await getImageUrls(fileId, [component], 'png', scale, config.platform);
const imageUrl = imageUrls[component.id];
if (!imageUrl) {
spinner.fail(`No image URL found for image (${imageCounter}/${totalImages}) [${dpi}]: ${component.name}`);
failed = true;
continue;
}
let processedImage = await downloadImage(imageUrl);
// Convert to WebP if needed
if (config.images.format === 'webp') {
processedImage = await sharp(processedImage)
.webp({ quality: config.images.quality })
.toBuffer();
}
// Save the processed image
await fs.writeFile(filePath, processedImage);
spinner.succeed(`Image saved (${imageCounter}/${totalImages}): ${fileNameBase} [${dpi}]`);
} catch (error) {
spinner.fail(`Failed to process image (${imageCounter}/${totalImages}): ${component.name} [${dpi}]`);
console.error(chalk.red(error.message));
failed = true;
}
}
} else {
// Create asset catalog structure
const assetPath = path.join(config.images.path, `${fileNameBase}.imageset`);
await fs.ensureDir(assetPath);
await fs.emptyDir(assetPath);
// Process for each scale (1x, 2x, 3x for universal and 1x, 2x for iPad)
for (const [scale, factor] of Object.entries(IOS_SCALES)) {
const spinner = ora(`Processing image (${imageCounter}/${totalImages}): ${component.name} [${scale}]`).start();
// Download the image from Figma at the correct scale
const imageUrls = await getImageUrls(fileId, [component], config.images.format, factor, config.platform);
const imageUrl = imageUrls[component.id];
if (!imageUrl) {
console.error(chalk.red(`No image URL found for image (${imageCounter}/${totalImages}) [${scale}]: ${component.name}`));
failed = true;
continue;
}
let scaleFileName;
if (scale.startsWith('ipad_')) {
// Handle iPad-specific scales
const ipadScale = scale.replace('ipad_', '');
scaleFileName = ipadScale === '1x' ?
`${fileNameBase}~ipad` :
`${fileNameBase}~ipad@${ipadScale}`;
} else {
// Handle universal scales
scaleFileName = scale === '1x' ?
fileNameBase :
`${fileNameBase}@${scale}`;
}
try {
const filePath = path.join(assetPath, `${scaleFileName}.${config.images.format}`);
const processedImage = await downloadImage(imageUrl);
await fs.writeFile(filePath, processedImage);
spinner.succeed(`Image saved (${imageCounter}/${totalImages}): ${fileNameBase} [${scale}]`);
} catch (error) {
spinner.fail(`Failed to process image (${imageCounter}/${totalImages}): ${component.name} [${scale}]`);
console.error(chalk.red(error.message));
failed = true;
}
}
// Create Contents.json
const contentsPath = path.join(assetPath, 'Contents.json');
const contents = createContentsJson(fileNameBase, config.images.format);
await fs.writeFile(contentsPath, contents, 'utf8');
}
if (!failed) {
processedComponentNames.add(component.name);
}
}
return processedComponentNames;
}
/**
* Main function to run the application
*/
async function main() {
console.log(chalk.blue('Figma Asset Downloader'));
console.log(chalk.blue('======================'));
try {
// Load configuration
let config = loadConfig();
const fileId = config.fileId;
const pageId = config.pageId;
const pageName = config.pageName;
const platform = config.platform;
console.log(chalk.green(`Loaded configuration for file: ${fileId}`));
console.log(chalk.green(`Platform: ${platform}`));
// If --find-duplicate flag is set, find and report duplicate components
if (findDuplicate) {
await findDuplicateComponents(fileId, pageId, pageName);
return;
}
// Fetch components
const components = await fetchComponents(fileId, componentNames, pageId, pageName);
// Process icons and images
const processedIconNames = await processIcons(components, fileId, config);
const processedImageNames = await processImages(components, fileId, config);
// Combine all processed component names
const allProcessedComponentNames = new Set([...processedIconNames, ...processedImageNames]);
// Find unprocessed components
const unprocessedComponentNames = componentNames.filter(componentName =>
!allProcessedComponentNames.has(componentName)
);
if (unprocessedComponentNames.length > 0) {
console.log(chalk.red('\nThe following components were not processed:'));
unprocessedComponentNames.forEach(componentName => {
const componentExists = components.some(c => c.name === componentName);
console.log(chalk.red(`- ${componentName}${!componentExists ? ' (not found)' : ''}`));
});
}
console.log(chalk.green('\nAsset download and processing complete!'));
} catch (error) {
console.error(chalk.red('An error occurred:'), error.message);
process.exit(1);
}
}
// Start the application
main();