Generating multi-brand multi-platform icons with Sketch and a Node.js script — Part #2
By: Cristiano Rastelli
Tags: Tools For Design, Nodejs, JavaScript, Sketch, Design Systems

The Sketch files, with all the icons collected, styled and properly named, were ready. Now it was time to start writing the code.

Suffice to say, the process was very much a trial and error: after the important initial code core, developed by my team lead Nikhil Verma (who set the script foundations), I went through an incremental process that required at least three phases of refactoring and quite a few revisions. For this reason, I won’t go into too much detail on how the script was developed, but rather focus on how the script works today, in its final shape.

The build script

The build script — written in Node.js — is relatively straightforward in its flow: once imported the dependencies, declared the list of Sketch files to process (as a list of brands, and for each brand a list of files for that brand) and checked that Sketch is installed on the client, the script loops on the array of brands, and for each one of these it executes these steps in sequence:

get the design tokens for the brand (we need the color values) clone the Sketch files associated with the brand, unzip them to expose the internal JSON files, and manipulate some of the internal values of these JSON files (more on this later) read the relevant meta-data out from Sketch JSON files (document.json, meta.json and pages/pageUniqueID.json); in particular we need the list of shared styles and the list of assets/icons contained in the files after a few further manipulations of the Sketch JSON files, zip them back and, using the (cloned and updated) Sketch files, export and generate the final output files for the three platforms (iOS, Android, Mobile Web)

You can view the relevant parts of the main build script here:

// ... modules imports here const SKETCH_FILES = {

badoo: ['icons_common'],

blendr: ['icons_common', 'icons_blendr'],

fiesta: ['icons_common', 'icons_fiesta'],

hotornot: ['icons_common', 'icons_hotornot'],

}; const SKETCH_FOLDER_PATH = path.resolve(__dirname, '../src/');

const SKETCH_TEMP_PATH = path.resolve(SKETCH_FOLDER_PATH, 'tmp');

const DESTINATION_PATH = path.resolve(__dirname, '../dist'); console.log('Build started...'); if (sketchtool.check()) {

console.log(`Processing Sketch file via ${sketchtool.version()}`);

build();

} else {

console.info('You need Sketch installed to run this script');

process.exit(1);

} // ---------------------------------------- function build() {

// be sure to start with a blank slate

del.sync([SKETCH_TEMP_PATH, DESTINATION_PATH]); // process all the brands declared in the list of Sketch files

Object.keys(SKETCH_FILES).forEach(async (brand) => { // get the design tokens for the brand

const brandTokens = getDesignTokens(brand);



// prepare the Sketch files (unzipped) and get a list of them

const sketchUnzipFolders = await prepareSketchFiles({

brand,

sketchFileNames: SKETCH_FILES[brand],

sketchFolder: SKETCH_FOLDER_PATH,

sketchTempFolder: SKETCH_TEMP_PATH

}); // get the Sketch metadata

const sketchMetadata = getSketchMetadata(sketchUnzipFolders);

const sketchDataSharedStyles = sketchMetadata.sharedStyles;

const sketchDataAssets = sketchMetadata.assetsMetadata; generateAssetsPDF({

platform: 'ios',

brand,

brandTokens,

sketchDataSharedStyles,

sketchDataAssets

});

generateAssetsSVGDynamicMobileWeb({

platform: 'mw',

brand,

brandTokens,

sketchDataSharedStyles,

sketchDataAssets

});

generateAssetsVectorDrawableDynamicAndroid({

platform: 'android',

brand,

brandTokens,

sketchDataSharedStyles,

sketchDataAssets

}); });

}

Actually, the entire pipelin code is much more complex than this, and the complexity lies in the prepareSketchFiles, getSketchMetadata and generateAssets[format][platform] functions. I’ll try to explain them in more detail below.

Preparing the Sketch files

The first step in the build process is the preparation of the Sketch files, so that they can be used later for the export of the assets for the different platforms.

The files associated with the brand — for Blendr, for example, the files icons_common.sketch and icons_blendr.sketch — are initially cloned in a temporary folder (more precisely, in a subfolder named after the brand that is being processed) and unzipped.

Then the internal JSON files are processed, to a prefix added to the assets that are to undergo AB testing, so that when exported they will be saved in a subfolder with a predefined name (the unique name of the experiment). To understand which assets are for testing, we simply check if the name of the page in which they are stored in Sketch is prefixed with “XP_”.

A comparison of the layer names, inside the Sketch files, before and after the update.

In the example above, when exported the assets will be saved in the subfolder “this__is_an_experiment”, with a filename “icon-name[variant-name].ext”.

Reading the Sketch metadata

The second important step in the process is to get all the relevant meta-data out of the Sketch files, in particular out of their internal JSON files. As explained above, these files are the two main files (document.json and meta.json) and the pages files (pages/pageUniqueId.json).

The document.json file is used to obtain the list of the Shared Styles, which appear under the layerStyles object property:

{

"_class": "document",

"do_objectID": "45D2DA82-B3F4-49D1-A886-9530678D71DC",

"colorSpace": 1,

...

"layerStyles": {

"_class": "sharedStyleContainer",

"objects": [

{

"_class": "sharedStyle",

"do_objectID": "9BC39AAD-CDE6-4698-8EA5-689C3C942DB4",

"name": "features/feature-like",

"value": {

"_class": "style",

"fills": [

{

"_class": "fill",

"isEnabled": true,

"color": {

"_class": "color",

"alpha": 1,

"blue": 0.10588235408067703,

"green": 0.4000000059604645,

"red": 1

},

"fillType": 0,

"noiseIndex": 0,

"noiseIntensity": 0,

"patternFillType": 1,

"patternTileScale": 1

}

],

"blur": {...},

"startMarkerType": 0,

"endMarkerType": 0,

"miterLimit": 10,

"windingRule": 1

}

},

...

For each style, we store some basic information in a key-value object. This will be used later whenever we need to retrieve the name of a style based on its unique ID (in Sketch, the do_objectID property):

const parsedSharedStyles = {};

parsedDocument.layerStyles.objects.forEach((object) => {

parsedSharedStyles[object.do_objectID] = {

name: object.name,

isFill: _.get(object, 'value.fills[0].color') !== undefined,

isBorder: _.get(object, 'value.borders[0].color') !== undefined,

};

});

At this point, we move on the meta.json file to get the list of pages, in particular we need their unique-id and name:

{

"commit": "623a23f2c4848acdbb1a38c2689e571eb73eb823",

"pagesAndArtboards": {

"EE6BE8D9-9FAD-4976-B0D8-AB33D2B5DBB7": {

"name": "Icons",

"artboards": {

"3275987C-CE1B-4369-B789-06366EDA4C98": {

"name": "badge-feature-like"

},

"C6992142-8439-45E7-A346-FC35FA01440F": {

"name": "badge-feature-crush"

},

...

"7F58A1C4-D624-40E3-A8C6-6AF15FD0C32D": {

"name": "tabbar-livestream"

}

...

}

},

"ACF82F4E-4B92-4BE1-A31C-DDEB2E54D761": {

"name": "XP_this__is_an_experiment",

"artboards": {

"31A812E8-D960-499F-A10F-C2006DDAEB65": {

"name": "this__is_an_experiment/tabbar-livestream[variant1]"

},

"20F03053-ED77-486B-9770-32E6BA73A0B8": {

"name": "this__is_an_experiment/tabbar-livestream[variant2]"

},

"801E65A4-3CC6-411B-B097-B1DBD33EC6CC": {

"name": "this__is_an_experiment/tabbar-livestream[control]"

}

}

},

Then, for every page we read the corresponding JSON file under the pages folder (as already said, the filename is [pageUniqueId].json), and we go through the assets contained in that page (they appear as layers). In this way, for every icon we get its name, its width/height, the Sketch meta-data for that layer icon, and if it’s on an experiment page, the name of the AB test in question, and the name of the variant for that icon.

Notice: the “page.json” object is very complex, so I won’t go into it here. If you are curious and want to see what it looks like, I suggest you to create a new, blank Sketch file, add some content in it, and save it; then rename its extension in zip, unzip it and look into one of the files that appear under the “pages” folder.

While processing the artboards, we also create a list of experiments (with their corresponding assets) that will be used later to determine which icon variants are used and for which experiment, associating the name of the icon variants to the “icon base” object.

For each Sketch file being processed that is associated with the brand, we produce an assetsMetadata object that looks like this:

{

"navigation-bar-edit": {

"do_objectID": "86321895-37CE-4B3B-9AA6-6838BEDB0977",

...sketch_artboard_properties,

"name": "navigation-bar-edit",

"assetname": "navigation-bar-edit",

"source": "icons_common",

"width": 48,

"height": 48

"layers": [

{

"do_objectID": "A15FA03C-DEA6-4732-9F85-CA0412A57DF4",

"name": "Path",

...sketch_layer_properties,

"sharedStyleID": "6A3C0FEE-C8A3-4629-AC48-4FC6005796F5",

"style": {

...

"fills": [

{

"_class": "fill",

"isEnabled": true,

"color": {

"_class": "color",

"alpha": 1,

"blue": 0.8784313725490196,

"green": 0.8784313725490196,

"red": 0.8784313725490196

},

}

],

"miterLimit": 10,

"startMarkerType": 0,

"windingRule": 1

},

},

],

...

},

"experiment-name/navigation-bar-edit[variant]": {

"do_objectID": "00C0A829-D8ED-4E62-8346-E7EFBC04A7C7",

...sketch_artboard_properties,

"name": "experiment-name/navigation-bar-edit[variant]",

"assetname": "navigation-bar-edit",

"source": "icons_common",

"width": 48,

"height": 48

...

As you can see, the same “icon” (in this case navigation-bar-edit) can have multiple “assets” associated with it, in term of experiments. But the same icon can appear with the same name in a second Sketch file associated with the brand, and this is very useful: it’s the trick we have used, to compile a common set of icons and then define specific different icon variants depending on the brand. That’s why we declared the Sketch files associated with each particular brand as an array:

const SKETCH_FILES = {

badoo: ['icons_common'],

blendr: ['icons_common', 'icons_blendr'],

fiesta: ['icons_common', 'icons_fiesta'],

hotornot: ['icons_common', 'icons_hotornot'],

};

Because in this case the order matters. And in fact, in the function getSketchMetadata, called by the build script, we don’t return the assetsMetadata objects (one per file) as a list, but rather do a deep merge of each object, one into the other, and then we return a single merged assetsMetadata object.

This is nothing more than the “logical” merge of the Sketch files, and their assets, into one single file. But the logic is not actually as simple as it looks. Here is the schema that we had to create to figure out what happens when there are icons with the same name (possibly under AB testing) in different files associated with the same brand:

The logical schema of how the “overriding” of the same icon works, between a common/shared set of icons and icons specifically designed for white-labels (also considering the case of AB testing)

Generating the final files in different formats for different platforms

The last step of the process is the actual generation of the icon files with different formats for the different platforms (PDF for iOS, SVG/JSX for Web, and VectorDrawable for Android).

As you can see from the number of parameters passed to the functions generateAssets[format][platform] this is the most complex part of the pipeline. Here is where the process starts to split and diverge for the different platforms. See below the complete logical flow of the script, and how the part related to the generation of the assets splits in three similar but non identical flows: