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!
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.
You should have an environment file called .env in your root directory, and you can declare your Personal Access Token:
Then back in our download-project-assets.js file we will destructure that from process.env
Next, we can initiate Commander program that we’ll use to give our app some options:
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:
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.
To get that project ID from the command line, we’ll start configuring our program from Commander:
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.
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:
Let’s try it! We can grab a project ID from the address bar of Zeplin’s Webapp when you’re inspecting a project.
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.
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()
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
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:
Ok, moment of truth! We’ll run this again and see that our assets are being downloaded for each screen.
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.
If you have any questions or want to share what you're building, come join the developer community on our Discord server!