Bundling everything for Iconify components

This article is a part of Iconify icon bundles code examples.

This is the most advanced example, that shows how to create icon bundles from various sources. It is easy to configure.

It bundles:

  • Icons from Iconify icon sets.
  • Icons from custom icon sets.
  • Custom JSON files.
  • Custom SVG files.

You need to configure script before running it.

Instructions

Installation:

npm install --save-dev @iconify/tools @iconify/json

Usage:

  • Change value of component to correct component. See below.
  • If you want script to output CommonJS code (that uses require()), set commonJS to true.
  • Change value of target to correct location of bundle.
  • Change sources configuration in variable sources.

Components

Values for variable component are package names for components:

Sources

This bundle script can import icons from multiple sources.

Comment out sources that you do not need.

Available sources:

svg

This option bundles custom SVG files. It converts SVG files to IconifyJSON format, then bundles it.

This method is used on Iconify documentation website to render custom icons and logo.

Value is array of objects. Each object has the following properties:

  • dir: directory where SVG files are stored. Sub-directories are not parsed.
  • monotone: true if icons are monotone, false if not. This affects how icons are cleaned up. If you have different icons, it is better to split them into 2 directories and make separate entries for monotone and colored icons.
  • prefix: Icon set prefix. It does not have to be unique, you can re-use same prefix for multiple sources, just make sure icon names are different.

Icons will imported from custom SVG directories can be used by name "prefix:name", where "prefix" is prefix you have configured, "name" is icon name.

For example:

const sources = {
   svg: [
       dir: 'svg',
       monotone: true,
       prefix: 'custom'
   ]
}

If you have icon svg/home.svg, after importing it, that icon in component will have the name custom:home:

<Icon icon="custom:home" />

icons

This option bundles icons from Iconify icon sets. It is similar to basic importer example.

Value is an array of icon names, including icon set prefix.

json

This option will bundle Iconify JSON files.

Value is an array of filenames.

If you want to include only few icons from JSON file, instead of filename you can provide an object with 2 properties: filename that points to JSON file and icons that lists icons you want to filter.

To bundle Iconify icon set, use require.resolve() to get its location.

Example that uses all variations:

const sources = {
   // ...

   json: [
       // Custom JSON file
       'json/gg.json',
       // Iconify JSON file (@iconify/json is a package name, /json/ is directory where files are, then filename)
       require.resolve('@iconify/json/json/tabler.json'),
       // Custom file with only few icons
       {
           filename: require.resolve('@iconify/json/json/line-md.json'),
           icons: [
               'home-twotone-alt',
               'github',
               'document-list',
               'document-code',
               'image-twotone',
           ],
       },
   ],

   // ...
};

Code

Code below is written with TypeScript. If you want simple JavaScript file, remove types and import type line.

Code is asynchronous. It is wrapped in anonymous asynchronous function because top level await, at moment of writing documentation, is not available in all currently used versions of Node.

create-bundle.ts
/**
* This is an advanced example for creating icon bundles for Iconify SVG Framework.
*
* It creates a bundle from:
* - All SVG files in a directory.
* - Custom JSON files.
* - Iconify icon sets.
* - SVG framework.
*
* This example uses Iconify Tools to import and clean up icons.
* For Iconify Tools documentation visit https://docs.iconify.design/tools/tools2/
*/

import { promises as fs } from 'fs';
import { dirname } from 'path';

// Installation: npm install --save-dev @iconify/tools @iconify/utils @iconify/json @iconify/iconify
import {
   importDirectory,
   cleanupSVG,
   parseColors,
   isEmptyColor,
   runSVGO,
} from '@iconify/tools';
import { getIcons, stringToIcon, minifyIconSet } from '@iconify/utils';
import type { IconifyJSON, IconifyMetaData } from '@iconify/types';

/**
* Script configuration
*/

interface BundleScriptCustomSVGConfig {
   // Path to SVG files
   dir: string;

   // True if icons should be treated as monotone: colors replaced with currentColor
   monotone: boolean;

   // Icon set prefix
   prefix: string;
}

interface BundleScriptCustomJSONConfig {
   // Path to JSON file
   filename: string;

   // List of icons to import. If missing, all icons will be imported
   icons?: string[];
}

interface BundleScriptConfig {
   // Custom SVG to import and bundle
   svg?: BundleScriptCustomSVGConfig[];

   // Icons to bundled from @iconify/json packages
   icons?: string[];

   // List of JSON files to bundled
   // Entry can be a string, pointing to filename or a BundleScriptCustomJSONConfig object (see type above)
   // If entry is a string or object without 'icons' property, an entire JSON file will be bundled
   json?: (string | BundleScriptCustomJSONConfig)[];
}

let sources: BundleScriptConfig;
sources = {
   svg: [
       {
           dir: 'svg',
           monotone: true,
           prefix: 'custom',
       },
       {
           dir: 'emojis',
           monotone: false,
           prefix: 'emoji',
       },
   ],

   icons: [
       'mdi:home',
       'mdi:account',
       'mdi:login',
       'mdi:logout',
       'octicon:book-24',
       'octicon:code-square-24',
   ],

   json: [
       // Custom JSON file
       'json/gg.json',
       // Iconify JSON file (@iconify/json is a package name, /json/ is directory where files are, then filename)
       require.resolve('@iconify/json/json/tabler.json'),
       // Custom file with only few icons
       {
           filename: require.resolve('@iconify/json/json/line-md.json'),
           icons: [
               'home-twotone-alt',
               'github',
               'document-list',
               'document-code',
               'image-twotone',
           ],
       },
   ],
};

// Iconify component (this changes import statement in generated file)
// Available options: '@iconify/react' for React, '@iconify/vue' for Vue 3, '@iconify/vue2' for Vue 2, '@iconify/svelte' for Svelte
const component = '@iconify/react';

// Set to true to use require() instead of import
const commonJS = false;

// File to save bundle to
const target = 'lib/icons-bundle.js';

/**
* Do stuff!
*/

(async function () {
   let bundle = commonJS
       ? "const { addCollection } = require('" + component + "');\n\n"
       : "import { addCollection } from '" + component + "';\n\n";

   // Create directory for output if missing
   const dir = dirname(target);
   try {
       await fs.mkdir(dir, {
           recursive: true,
       });
   } catch (err) {
       //
   }

   /**
    * Convert sources.icons to sources.json
    */

   if (sources.icons) {
       const sourcesJSON = sources.json ? sources.json : (sources.json = []);

       // Sort icons by prefix
       const organizedList = organizeIconsList(sources.icons);
       for (const prefix in organizedList) {
           const filename = require.resolve(`@iconify/json/json/${prefix}.json`);
           sourcesJSON.push({
               filename,
               icons: organizedList[prefix],
           });
       }
   }

   /**
    * Bundle JSON files
    */

   if (sources.json) {
       for (let i = 0; i < sources.json.length; i++) {
           const item = sources.json[i];

           // Load icon set
           const filename = typeof item === 'string' ? item : item.filename;
           let content = JSON.parse(
               await fs.readFile(filename, 'utf8')
           ) as IconifyJSON;

           // Filter icons
           if (typeof item !== 'string' && item.icons?.length) {
               const filteredContent = getIcons(content, item.icons);
               if (!filteredContent) {
                   throw new Error(`Cannot find required icons in ${filename}`);
               }
               content = filteredContent;
           }

           // Remove metadata and add to bundle
           removeMetaData(content);
           minifyIconSet(content);
           bundle += 'addCollection(' + JSON.stringify(content) + ');\n';
           console.log(`Bundled icons from ${filename}`);
       }
   }

   /**
    * Custom SVG
    */

   if (sources.svg) {
       for (let i = 0; i < sources.svg.length; i++) {
           const source = sources.svg[i];

           // Import icons
           const iconSet = await importDirectory(source.dir, {
               prefix: source.prefix,
           });

           // Validate, clean up, fix palette and optimise
           await iconSet.forEach(async (name, type) => {
               if (type !== 'icon') {
                   return;
               }

               // Get SVG instance for parsing
               const svg = iconSet.toSVG(name);
               if (!svg) {
                   // Invalid icon
                   iconSet.remove(name);
                   return;
               }

               // Clean up and optimise icons
               try {
                   // Clean up icon code
                   await cleanupSVG(svg);

                   if (source.monotone) {
                       // Replace color with currentColor, add if missing
                       // If icon is not monotone, remove this code
                       await parseColors(svg, {
                           defaultColor: 'currentColor',
                           callback: (attr, colorStr, color) => {
                               return !color || isEmptyColor(color)
                                   ? colorStr
                                   : 'currentColor';
                           },
                       });
                   }

                   // Optimise
                   await runSVGO(svg);
               } catch (err) {
                   // Invalid icon
                   console.error(`Error parsing ${name} from ${source.dir}:`, err);
                   iconSet.remove(name);
                   return;
               }

               // Update icon from SVG instance
               iconSet.fromSVG(name, svg);
           });
           console.log(`Bundled ${iconSet.count()} icons from ${source.dir}`);

           // Export to JSON
           const content = iconSet.export();
           bundle += 'addCollection(' + JSON.stringify(content) + ');\n';
       }
   }

   // Save to file
   await fs.writeFile(target, bundle, 'utf8');

   console.log(`Saved ${target} (${bundle.length} bytes)`);
})().catch((err) => {
   console.error(err);
});

/**
* Remove metadata from icon set
*/

function removeMetaData(iconSet: IconifyJSON) {
   const props: (keyof IconifyMetaData)[] = [
       'info',
       'chars',
       'categories',
       'themes',
       'prefixes',
       'suffixes',
   ];
   props.forEach((prop) => {
       delete iconSet[prop];
   });
}

/**
* Sort icon names by prefix
*/

function organizeIconsList(icons: string[]): Record<string, string[]> {
   const sorted: Record<string, string[]> = Object.create(null);
   icons.forEach((icon) => {
       const item = stringToIcon(icon);
       if (!item) {
           return;
       }

       const prefix = item.prefix;
       const prefixList = sorted[prefix] ? sorted[prefix] : (sorted[prefix] = []);

       const name = item.name;
       if (prefixList.indexOf(name) === -1) {
           prefixList.push(name);
       }
   });

   return sorted;
}

Part of code is taken from Iconify Tools import examples.

Importing bundle

Bundle generated by script above must be imported in your application:

import './icons-bundle.js';

If you are using require() and have set commonJS to true, use this code:

require('./icons-bundle.js');