lib/api.ts (235 lines of code) (raw):
import fs from "fs";
import sizeOf from "image-size";
import https from "https";
import path from "path";
import retry from "async-retry";
import {
SiteMap,
Page,
Section,
Canvas,
Frame,
File,
Project,
ImageData,
} from "./types";
const RETRY_LIMIT = 10;
const RETRY_TIMEOUT = 1000 * 30; // 30s
const PROJECT_DATA_PATH = path.join(process.cwd(), "./data/project.json");
const SITE_MAP_DATA_PATH = path.join(process.cwd(), "./data/siteMap.json");
const getImageDataPath = (key) =>
path.join(process.cwd(), `./public/frames/${key}.json`);
const getImageFilePath = (key) =>
path.join(process.cwd(), `./public/frames/${key}.png`);
function getPageKey(canvas: Canvas, frame: Frame) {
return (
`${canvas.name}-${frame.name}`
.toLowerCase()
// Convert common expressions
.replace(/\s\/\s/g, "-")
.replace(/\s\\\s/g, "-")
.replace(/\s-\s/g, "-")
.replace(/\s\+\s/g, "-")
.replace(/\s&\s/g, "-")
.replace(/:\s/g, "-")
.replace(/\s/g, "-")
// Remove problematic characters
.replace(/\//g, "")
.replace(/:/g, "")
.replace(/\./g, "")
.replace(/\(/g, "")
.replace(/\)/g, "")
);
}
/** Returns a list of sections, each of which is parent to a list of pages. */
async function getSiteMap(): Promise<SiteMap> {
let project: Project;
try {
const projectFromDisk = fs.readFileSync(PROJECT_DATA_PATH);
project = JSON.parse(projectFromDisk.toString());
} catch (er) {
try {
const response = await fetch(
`https://api.figma.com/v1/projects/${process.env.FIGMA_PROJECT_ID}/files`,
{
headers: {
"X-FIGMA-TOKEN": process.env.FIGMA_AUTH_TOKEN,
},
}
);
const contentType = response.headers.get("Content-Type");
if (contentType === "application/json; charset=utf-8") {
project = await response.json();
try {
fs.writeFileSync(PROJECT_DATA_PATH, JSON.stringify(project));
} catch (er) {
console.log(
"There was a problem saving the figma project data to disk."
);
console.log(er);
}
} else {
throw new Error(await response.text());
}
} catch (er) {
console.log("There was a problem fetching the figma project.");
console.log(er);
}
}
let siteMap: SiteMap = [];
try {
const siteMapFromDisk = fs.readFileSync(SITE_MAP_DATA_PATH);
siteMap = JSON.parse(siteMapFromDisk.toString());
} catch (er) {
for (const { key: fileKey, name: fileName } of project.files) {
let file: File;
try {
const response = await fetch(
`https://api.figma.com/v1/files/${fileKey}?depth=2`,
{
headers: {
"X-FIGMA-TOKEN": process.env.FIGMA_AUTH_TOKEN,
},
}
);
const contentType = response.headers.get("Content-Type");
if (contentType === "application/json; charset=utf-8") {
file = await response.json();
} else {
throw new Error(await response.text());
}
} catch (er) {
console.log(`There was a problem fetching the figma file ${fileKey}.`);
console.log(er);
}
// Bail early if figma file is mal-formed.
if (!file || !file.document) {
console.log("The figma file we got is mal-formed.");
console.log(file);
return [];
}
// Only use figma canvases and visible frames that start with a capital letter.
// Note, we intentionally leave out most of the data sent over from Figma
// because static prop data is inlined on the page.
for (const canvas of file.document.children) {
console.log(file.name, canvas.name, canvas.type);
if (canvas.name.match(/^[A-Z]/)) {
const section: Section = {
id: canvas.id,
name: canvas.name,
children: [],
};
for (const frame of canvas.children) {
console.log(" ", frame.name, frame.type);
if (
frame.type === "FRAME" &&
frame.name.match(/^[A-Z]/) &&
frame.visible !== false // `visible` is only present when it is false
) {
const page: Page = {
id: frame.id,
name: frame.name,
// Add some additional metadata to make things easier later on.
key: getPageKey(canvas, frame),
fileKey,
fileName,
sectionName: section.name,
};
section.children.push(page);
}
}
if (section.children.length > 0) {
siteMap.push(section);
}
}
}
}
try {
fs.writeFileSync(SITE_MAP_DATA_PATH, JSON.stringify(siteMap));
} catch (er) {
console.log(
`There was a problem saving the figma project files to disk.`
);
console.log(er);
}
}
return siteMap;
}
async function getImage(page: Page): Promise<ImageData> {
const image: ImageData = {
src: `/frames/${page.key}.png`,
height: 0,
width: 0,
};
console.log(image.src);
try {
const dimensionsFromDisk = fs.readFileSync(getImageDataPath(page.key));
const dimensions = JSON.parse(dimensionsFromDisk.toString());
image.height = dimensions.height;
image.width = dimensions.width;
console.log(`Image for [${page.key}] found locally.`);
} catch (er) {
console.log(`No image for [${page.key}] found locally...`);
try {
await retry(
async () => {
const response = await fetch(
`https://api.figma.com/v1/images/${page.fileKey}?ids=${page.id}&format=png&scale=1`,
{
headers: {
"X-FIGMA-TOKEN": process.env.FIGMA_AUTH_TOKEN,
},
}
);
const contentType = response.headers.get("Content-Type");
if (contentType === "application/json; charset=utf-8") {
const json = await response.json();
if (json.images && json.images[page.id]) {
const imageUrl = json.images[page.id];
console.log(`Image generation for [${page.key}] was successful!`);
try {
const dimensions = await saveImageToDisk(imageUrl, page.key);
image.height = dimensions.height;
image.width = dimensions.width;
console.log("Image saved to disk successfully.");
} catch (er) {
console.log(`There was a problem saving the image to disk.`);
console.log(er);
}
} else {
throw new Error(JSON.stringify(json));
}
} else {
throw new Error(await response.text());
}
},
{
retries: RETRY_LIMIT,
minTimeout: RETRY_TIMEOUT,
onRetry: (er) => {
console.log(
`There was a problem fetching the image for [${page.key}]. Retrying...`
);
console.log(er);
},
}
);
} catch (er) {
console.log(
`There was a problem fetching the image for [${page.key}]. Giving up.`
);
console.log(er);
console.log(image);
}
}
return image;
}
function saveImageToDisk(url, key): Promise<{ height: number; width: number }> {
return new Promise((resolve) => {
const file = fs.createWriteStream(getImageFilePath(key));
file.on("finish", () => {
const dimensions = sizeOf(getImageFilePath(key));
try {
fs.writeFileSync(getImageDataPath(key), JSON.stringify(dimensions));
} catch (er) {
console.log("There was an error saving image dimensions.");
console.error(er);
}
resolve(dimensions);
});
https.get(url, (response) => response.pipe(file));
});
}
export { getSiteMap, getImage };