// // Figma project stats // Pulls statistics like number of files, frames, versions etc for a project. // // Usage: // export FIGMA_API_ACCESS_TOKEN='your-token' // node figma-project-stats.js // // You can generate tokens in your account settings or at // https://www.figma.com/developers/explorer#personal-access-token // // is the last number in the URL path of a project. // For instance, in this URL: // https://www.figma.com/files/5027479082769520/project/712/Project-Title // The project id is "712" // const http = require('https') let FIGMA_API_ACCESS_TOKEN = '' // populated from env let FIGMA_API_HOST = 'api.figma.com' // may be populated from env function findfkey(url) { return url.match(/\.com\/file\/([^\/]+)/)[1] } function apiget(path) { return new Promise((resolve, reject) => { let shortCircuited = false let req = http.request( { protocol: 'https:', host: FIGMA_API_HOST, method: 'GET', path: '/v1/' + path, headers: { 'X-FIGMA-TOKEN': FIGMA_API_ACCESS_TOKEN, }, }, res => { if (res.statusCode == 504 || res.statusCode == '504') { console.log(`${path} failed with status ${res.statusCode} -- retrying...`) shortCircuited = true return setTimeout(() => { apiget(path).then(resolve).catch(reject) }, 1000) } // console.log(`STATUS: ${res.statusCode}`) // console.log(`HEADERS:`, JSON.parse(JSON.stringify(res.headers))) let buf = '' res.setEncoding('utf8') res.on('data', chunk => { buf += chunk }) res.on('end', () => { let r = null try { r = JSON.parse(buf) } catch (err) { console.error(`failed to parse response body: ${err}`) console.error(`original body:\n------${buf}\n------`) return reject(err) } resolve(r, { status: res.statusCode }) }) } ) req.on('error', err => { if (!shortCircuited) { reject(err) } }) req.end() }) // promise } const frameLikeTypes = new Set('FRAME GROUP COMPONENT INSTANCE'.split(' ')) function countFrameLikeNodes(parent) { let n = 0 for (let node of parent.children) { if (frameLikeTypes.has(node.type)) { n++ } } return n } function usage() { console.error( `usage: ${require('path').basename(__filename)} \n` + `Environment variables:\n` + ` FIGMA_API_ACCESS_TOKEN API access token (required)\n` + ` FIGMA_API_HOST API server hostname (e.g. "local-api.figma.com")\n` ) process.exit(1) } function main(args) { FIGMA_API_ACCESS_TOKEN = process.env['FIGMA_API_ACCESS_TOKEN'] || '' FIGMA_API_HOST = process.env['FIGMA_API_HOST'] || FIGMA_API_HOST if (args.length < 1) { console.error('missing project id') usage() } if (args.indexOf('-h') != -1 || args.indexOf('--help') != -1) { usage() } if (FIGMA_API_ACCESS_TOKEN.length == 0) { console.error( `Missing env variable FIGMA_API_ACCESS_TOKEN\n` + `Visit the following URL to generate a token:\n` + ` https://www.figma.com/developers/explorer#personal-access-token\n` ) usage() } let projectId = args[0].replace(/[^\d]+/g, '') console.log(`collecting stats for project ${projectId} on ${FIGMA_API_HOST}`) apiget(`projects/${projectId}/files`).then(r => { console.log(`inspecting ${r.files.length} project files...`) let nextFileIndex = 0 let nTotalPages = 0 let nTotalVersions = 0 let nTotalFrames = 0 let doNextFile = (key) => { let file = r.files[nextFileIndex++] if (!file) { // done console.log('project summary:') console.log('files: ', r.files.length) console.log('pages: ', nTotalPages) console.log('versions: ', nTotalVersions) console.log('top-level frames:', nTotalFrames) return } console.log(`file ${file.key} "${file.name}":`) let nversions = 0 let npages = 0 let nTopLevelFrames = 0 Promise.all([ apiget(`files/${file.key}/versions`).then(r => { nversions = r.versions.length }), apiget(`files/${file.key}`).then(r => { let pages = r.document.children npages = pages.length for (let page of pages) { nTopLevelFrames += countFrameLikeNodes(page) } }), ]).then(() => { console.log(' versions: ', nversions) console.log(' pages: ', npages) console.log(' top-level frames:', nTopLevelFrames) nTotalPages += npages nTotalVersions += nversions nTotalFrames += nTopLevelFrames doNextFile() }).catch(err => { console.error(err.stack || String(err)) }) } doNextFile() }) } main(process.argv.slice(2))