Skip to content

Instantly share code, notes, and snippets.

@vovkasm
Created December 3, 2018 20:45
Show Gist options
  • Save vovkasm/2ec93fa5e5a8e6d331e558198d5145f7 to your computer and use it in GitHub Desktop.
Save vovkasm/2ec93fa5e5a8e6d331e558198d5145f7 to your computer and use it in GitHub Desktop.
// tslint:disable:max-classes-per-file
import { Client, FileImageResponse, Node } from 'figma-js'
import * as fs from 'fs'
import * as https from 'https'
import * as mkdirp from 'mkdirp'
import * as path from 'path'
import * as request from 'request'
const TOKEN = 'xxxx-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
const FILE_IDENT = 'you-file-ident' // see: https://jongold.github.io/figma-js/interfaces/clientinterface.html#file
const ROOT_DIR = path.dirname(__dirname)
const ASSETS_DIR = path.normalize(path.join(ROOT_DIR, 'static'))
const NAME_RE = /^ex_([a-z0-9-]+)_([a-z0-9-_]+)/ // assets will be found by symbol names with format `ex_<subdir>_<file>`
const client = Client({ personalAccessToken: TOKEN })
const httpsAgent = new https.Agent({ maxSockets: 4 })
info(`Fetch document`)
client
.file(FILE_IDENT)
.then((file) => findAssets(getChildren(file.data.document)))
.then((assets) => fetchAssets(assets))
.catch((e) => {
// tslint:disable-next-line:no-console
console.log(e)
})
function getChildren(node: Node): ReadonlyArray<Node> {
const anyNode = node as any
if (anyNode.children && Array.isArray(anyNode.children)) {
return anyNode.children
}
return []
}
function findAssets(nodeList: ReadonlyArray<Node>): AssetsCollection {
const assetsCollection = new AssetsCollection()
findAssetsWalk(nodeList, assetsCollection)
return assetsCollection
}
function findAssetsWalk(nodeList: ReadonlyArray<Node>, assetsCollection: AssetsCollection) {
for (const node of nodeList) {
if (node.name.match(NAME_RE)) {
assetsCollection.addAsset(node.id, node.name)
} else {
findAssetsWalk(getChildren(node), assetsCollection)
}
}
}
function fetchAssets(assets: AssetsCollection): Promise<void> {
if (assets.empty()) {
info(`No assets in document`)
return Promise.resolve()
}
info(`Found assets: ${assets.getNames().join(', ')}`)
info('Fetch assets info for 1x')
return client
.fileImages(FILE_IDENT, { ids: assets.getIds(), scale: 1, format: 'png' })
.then((result) => {
saveAssetsResult(assets, 1, result.data)
info('Fetch assets info for 2x')
return client.fileImages(FILE_IDENT, { ids: assets.getIds(), scale: 2, format: 'png' })
})
.then((result) => {
saveAssetsResult(assets, 2, result.data)
info('Fetch assets info for 3x')
return client.fileImages(FILE_IDENT, { ids: assets.getIds(), scale: 3, format: 'png' })
})
.then((result) => {
saveAssetsResult(assets, 3, result.data)
})
.then(() => {
info('Download all assets')
assets.download()
})
}
function saveAssetsResult(assets: AssetsCollection, scale: number, response: FileImageResponse) {
if (response.err) {
throw new Error(`image info error: ${response.err}`)
}
const images = response.images
for (const id of Object.keys(images)) {
assets.setAssetUrl(id, scale, images[id])
}
}
function info(msg: string) {
// tslint:disable-next-line:no-console
console.info(msg)
}
function error(msg: string) {
// tslint:disable-next-line:no-console
console.error(msg)
}
class AssetsCollection {
private assets: Asset[]
private byFigmaId: Map<string, Asset>
constructor() {
this.assets = []
this.byFigmaId = new Map()
}
addAsset(id: string, name: string) {
const asset = new Asset(id, name)
this.assets.push(asset)
this.byFigmaId.set(asset.figmaId, asset)
}
empty(): boolean {
return this.assets.length === 0
}
getIds(): string[] {
return this.assets.map((asset) => asset.figmaId)
}
getNames(): string[] {
return this.assets.map((asset) => asset.figmaName)
}
getAsset(figmaId: string): Asset {
const asset = this.byFigmaId.get(figmaId)
if (!asset) {
throw new Error(`Asset id ${figmaId} not found`)
}
return asset
}
setAssetUrl(figmaId: string, scale: number, url: string) {
const asset = this.getAsset(figmaId)
if (scale === 1) {
asset.assetUrl = url
} else if (scale === 2) {
asset.assetUrl2x = url
} else if (scale === 3) {
asset.assetUrl3x = url
} else {
throw new Error(`unknow scale '${scale}', should be 1, 2 or 3`)
}
}
download() {
for (const asset of this.assets) {
asset.downloadForScale(1)
asset.downloadForScale(2)
asset.downloadForScale(3)
}
}
}
class Asset {
figmaId: string
figmaName: string
assetUrl: string | undefined
assetUrl2x: string | undefined
assetUrl3x: string | undefined
constructor(id: string, name: string) {
this.figmaId = id
this.figmaName = name
}
downloadForScale(scale: number) {
const url = this.getUrlForScale(scale)
const dest = this.getSavePathForScale(scale)
if (!url || !dest) {
error(`Will not download ${this.figmaName} with scale=${scale}, probably figma can't render it`)
return
}
info(`Downloading ${url} to ${dest}`)
const destDir = path.dirname(dest)
mkdirp(destDir, (err, made) => {
if (err) {
error(`Can't create directory ${destDir}: ${err.message}`)
return
}
const ws = fs.createWriteStream(dest)
ws.on('close', () => {
info(`Done writing ${dest}`)
})
ws.on('error', (e: Error) => {
error(`Error writing ${dest}: ${e.message}`)
})
request({ url, pool: httpsAgent })
.on('error', (e: Error) => {
error(`Error downloding ${dest}: ${e.message}`)
})
.pipe(ws)
})
}
getUrlForScale(scale: number): string | undefined {
if (scale === 1) {
return this.assetUrl
} else if (scale === 2) {
return this.assetUrl2x
} else if (scale === 3) {
return this.assetUrl3x
} else {
throw new Error(`unknown scale '${scale}', should be 1, 2 or 3`)
}
}
getSavePathForScale(scale: number): string | undefined {
const res = NAME_RE.exec(this.figmaName)
if (!res) return undefined
const subdir = res[1]
const name = res[2]
const filename = scale === 1 ? `${name}.png` : `${name}@${scale}x.png`
return path.join(ASSETS_DIR, subdir, filename)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment