Download all assets from a project with Zeplin Javascript SDK
Collaboration
Today we’ll go over how to build a command line app with Node to help download all assets in a project. As a developer using Zeplin, I can inspect a screen and download an individual asset from that screen, or download all the assets for the screen altogether. This workflow is generally fine because when I’m working on a frontend, I’m working on just one screen or one component at a time that requires that asset. There are some developers out there who would prefer to grab everything for the entire project in one go, and we’re going to be showing you how to leverage Zeplin’s public API and Javascript SDK to do just that.
If you have not used the Zeplin API before, make sure to check out our first blog post about Zeplin’s Javascript SDK to learn how to get your Personal Access Token for the API and get everything set up. But if you’re already good to go there, then let’s dive in and talk about the dependencies we’ll need to build out this app.
Project Setup
You’ll need Axios for our HTTP request handler. Zeplin’s Javascript SDK actually uses Axios already behind the scenes, but we’re going to supply the SDK with a custom handler that uses axios-rate-limit to help make sure we throttle the requests a bit. The Rate Limit for requests to the Zeplin API is 200 requests per minute per user, so if you’re working on a big project with a lot of screens and assets adding this custom HTTP request handler with rate-limiting built-in will ensure you don’t get blocked by the rate limits.
Downloading and writing so many files at a time can be taxing on your system, so, similar to rate limiting, we'll be using the p-limit library to manage the number of concurrent downloads.
And you may not want all of the assets, so if you just want to download SVG’s or just the PNG assets, we can give our app some extra options like that using Commander.
Finally, we’ll use dotenv to help safely store our personal access token and any other environment variables you might need.
If you want to install all of those in one line, here you go!
$ npm install @zeplin/sdk axios axios-rate-limit p-limit commander dotenv
Now taking it into our editor for our Node app, we can create a file called download-project-assets.js and import those packages. We’ll also import Node’s built-in “FS” module to handle our file system operations for downloading the assets or creating directories.
// download-project-assets.js
import { Command } from 'commander';
import 'dotenv/config';
import { ZeplinApi, Configuration } from '@zeplin/sdk';
import axios from 'axios';
import fs from 'fs/promises';
import pLimit from 'p-limit';
import rateLimit from 'axios-rate-limit';
You should have an environment file called .env in your root directory, and you can declare your Personal Access Token:
// .env
PERSONAL_ACCESS_TOKEN=12345
Then back in our download-project-assets.js file we will destructure that from process.env
// download-project-assets.js
const { PERSONAL_ACCESS_TOKEN } = process.env;
Next, we can initiate Commander program that we’ll use to give our app some options:
const program = new Command();
And now we’ll get our Zeplin SDK started with a custom HTTP handler for rate limiting. If you’re working on a large project with a lot of assets, having this rate limiting built-in will help ensure you do not max out the 200 requests per minute from Zeplin’s API. Create a variable called "http" and call on the rateLimit() function, passing in axios.create(), as well as an object of options for our rate limiting, which will be set to 200 requests per minute.
When creating the Zeplin client that we’ll call zeplin, the third argument of our configuration will be the custom http handler we’ve just created:
const http = rateLimit(axios.create(), { maxRequests: 200, perMilliseconds: 60000 });
const zeplin = new ZeplinApi(
new Configuration(
{ accessToken: PERSONAL_ACCESS_TOKEN },
),
undefined,
http,
);
Accessing project screens
The process for downloading all project assets with the Zeplin API consists of three main steps. First, we need to get an array of screens with their ID’s from the project using the getProjectScreens endpoint.
The data for a single screen from that endpoint will only contain a summary, so to get more data about the list of assets for that screen, we’ll use the screen’s ID to query the getLatestScreenVersion endpoint. Now we’ll have access to the asset URL from Zeplin’s CDN and we can then create a function to download the assets.
We’ll create our first function to get all the project screens using the SDK method zeplin.screens.getProjectScreens, passing in the project ID.
const getProjectScreens = async (projectId) => {
const { data } = await zeplin.screens.getProjectScreens(projectId);
return data;
};
To get that project ID from the command line, we’ll start configuring our program from Commander:
program
.requiredOption('-p, --projectId ', 'Project ID')
The parameters for the required option are a string with the short form flag, we’ll use “-p” for projectId, and the long form "—projectId" with the variable you’ll use in angled brackets, and lastly a string for a description of the flag that can be used as a hint when you enter "node get-project-assets --help" from the command line.
Now we can start adding our other options. One will be the parent directory to download our assets into, and we can just set a default here as well which can be the third argument for this option method to be“Output.”
Then for our last option, we’ll need an array of the file types Zeplin supports for assets.
program
.requiredOption('-p, --projectId ', 'Project ID')
.option('-d, --directory ', 'Output directory', 'Output')
.option('-f, --formats ', 'Formats to download', ['png', 'jpg', 'webp', 'svg', 'pdf'])
These ellipses in <formats…> will indicate that it’s going to be an array, and then as the default options for the third argument, we’ll list out all the file types that Zeplin supports for their asset generation. If the user of this app does not select any options, all file types will be downloaded.
After we’ve defined all the options, we’ll define the action of this program. We can make an async function passing in the variables that are our options we defined using their long-form syntax. In our case,-p is the short form and projectId is the longform that we can use. And to start off let’s just console log our projects, then call program.parseAsync()which will evaluate all the options you’ve given it and run the action:
program
.requiredOption('-p, --projectId ', 'Project ID')
.option('-d, --directory ', 'Output directory', 'Output')
.option('-f, --formats ', 'Formats to download', ['png', 'jpg', 'webp', 'svg', 'pdf'])
.action(async ({ projectId, directory, formats }) => {
const projectScreens = await getProjectScreens(projectId);
console.log(projectScreens);
});
program.parseAsync(process.argv);
Let’s try it! We can grab a project ID from the address bar of Zeplin’s Webapp when you’re inspecting a project.
$ node download-project-assets -p YOUR_PROJECT_ID
We can see that this returns an array of our screens and we have access to useful data like the ID and the name of that screen. Check out the screen model from Zeplin’s developer documentation to see what other data is available: https://docs.zeplin.dev/reference/screen
Generating a list of assets from the screen
The next step is to get the asset list for each screen, which is available on the getLatestScreenVersion endpoint. So we’ll create a function called getAssetData, and take in the parameters for the screen itself, the project ID, and the list of formats from the options we defined earlier.
const getAssetData = async (screen, projectId, formats) => {
const { id, name } = screen;
const { data } = await zeplin.screens
.getLatestScreenVersion(projectId, id);
return data.assets.flatMap(({ displayName, contents }) => {
// remove any asset that are not in the formats defined in PROJECT_OPTIONS.formats
const filteredContents = contents.filter((content) => (
formats.includes(content.format)
));
return filteredContents.map(({ url, format, density }) => ({
name,
url,
filename: `${displayName.replaceAll('/', '-')}-${density}x.${format}`,
}));
});
};
In the above code, we destructure the ID and name property from the screen, and also set our data property from the API’s return by destructuring that too, then use the SDK method screens.getLatestScreenVersion, passing in the project ID and the screen ID.
The array of assets here needs to be flattened and then we’ll destructure these properties for displayName and contents, and then we’ll want to apply a filter in a new array called filteredContents. In that new array we check if the content format is in the list of formats we supplied from our options.
When it comes to creating our folder structure for these assets to live in, we’ll want to create a folder named the same thing as the screen name as it appears in your Zeplin project.
Sometimes asset names will contain a forward slash character, and when we use the FS module later to create directories, FS will interpret that as a nested directory so we should just replace those with dashes by renaming our file. It’ll also be helpful to append the density of that image to the file name here for organization's sake.
Downloading the assets for each screen
The function we just created above will only be getting assets for a single screen, so we can collect all the assets for all screens inside our program’s action method. We’ll create a new variable for assets, then in Promise.all we’ll map over the projectScreens array, to run getAssetData for each of those screens, passing in the screen object itself, project ID, and list of formats from our options. Those results will need to be flattened, since it’s an array of nested arrays for each screen, so we’ll add .flat()
program
.requiredOption('-p, --projectId ', 'Project ID')
.option('-d, --directory ', 'Output directory', 'Output')
.option('-f, --formats ', 'Formats to download', ['png', 'jpg', 'webp', 'svg', 'pdf'])
.action(async ({ projectId, directory, formats }) => {
const projectScreens = await getProjectScreens(projectId);
const assets = (await Promise.all(projectScreens.map(
async (screen) => getAssetData(screen, projectId, formats),
))).flat();
});
program.parseAsync(process.argv);
Each asset in our flattened array will have a URL property that we use for downloading from Zeplin’s CDN, so we can start on that functionality now with a new async function called downloadAsset
const downloadAsset = async ({ name, url, filename }, dir) => {
try {
const { data } = await axios.get(url, { responseType: 'stream' });
await fs.mkdir(`${dir}/${name}`, { recursive: true });
await fs.writeFile(`${dir}/${name}/${filename}`, data);
} catch (err) {
console.log(`Error downloading ${name}`);
console.log(err.config.url);
}
};
We’ll pass in the asset object and destructure the screen name, url, and filename. We also want to know the parent directory by passing in“dir” from our Commander option.
Next, in a try/catch create a variable“data” which will await for Axios to get the url and set the responseType to stream.
Then call on FS to make the directory with the parent directory name and the screen’s name as a child folder, then call on FS again to write the file using the filename and the data that Axios just returned as a source for the file.
In case of any errors we can just apply a simple catch to log the error in the console.
Putting it all together
We're at the home stretch! We’ll go back to our action method in the program, and call on FS to remove the parent directory in case it already exists so that you can start this script fresh every time. Then in the next line create the parent directory.
Now we’ll use the p-limit library to manage concurrency because it can get really taxing on your system to be creating so many files at the same time. So we’ll limit it to 20 to start, and then in a variable called downloadAssetPromises, you can map over our list of assets, passing in that downloadAsset function to a limited promise instance.
Here’s the entire program:
program
.requiredOption('-p, --projectId ', 'Project ID')
.option('-d, --directory ', 'Output directory', 'Output')
.option('-f, --formats ', 'Formats to download', ['png', 'jpg', 'webp', 'svg', 'pdf'])
.action(async ({ projectId, directory, formats }) => {
const projectScreens = await getProjectScreens(projectId);
console.log(projectScreens);
const limit = pLimit(20);
const downloadAssetPromises = assets.map((asset) => (
limit(() => downloadAsset(asset, directory))));
await Promise.all(downloadAssetPromises);
});
program.parseAsync(process.argv);
Ok, moment of truth! We’ll run this again and see that our assets are being downloaded for each screen.
$ node download-projects-assets -p
Your project likely has a large amount of assets, and at this point it’s not easy to tell when the script has completed, but with the Progress library we can solve that problem. I go over how to add progress bars in the accompanying video for this post.
You can also check out the source code and more helpful scripts that use Zeplin’s Javascript SDK from the Zeplin Community GitHub Repo. That’s all for now!
If you have any questions or want to share what you're building, come join the developer community on our Discord server!
Fin