Created
December 3, 2018 20:45
-
-
Save vovkasm/2ec93fa5e5a8e6d331e558198d5145f7 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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