Bundling everything for SVG Framework

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:

  • Iconify SVG framework.
  • 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 @iconify/iconify

Usage:

  • Change variable target to correct location of bundle.
  • Change sources configuration in variable sources.

Sources

This bundle script can import icons from multiple sources.

Comment out sources that you do not need.

Available sources:

SVG framework

Option svgFramework bundles Iconify SVG framework.

If you want to bundle entire Iconify SVG framework, use this configuration:

const sources = {
   svgFramework: require.resolve('@iconify/iconify'),

   // ...
};

If you want to make sure API is not available, bundle Iconify SVG framework without API:

const sources = {
   svgFramework: require.resolve(
       '@iconify/iconify/dist/iconify.without-api.min'
   ),

   // ...
};

Build script will also copy TypeScript definitions file to target location. This makes it easy to use bundle with TypeScript and code completion in tools like VSCode.

If you do not want to bundle Iconify SVG framework, remove iconify property from configuration.

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 SVG framework will have the name custom:home:

<span class="iconify" data-icon="custom:home"></span>

iconifyIcons

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 {
   // Source file for Iconify SVG Framework:
   // Use require.resolve('@iconify/iconify') for full version
   // Use require.resolve('@iconify/iconify/dist/iconify.without-api.min') for version without API
   svgFramework?: string;

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

   // Icons to bundled from @iconify/json packages
   iconifyIcons?: 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 = {
   svgFramework: require.resolve(
       '@iconify/iconify/dist/iconify.without-api.min'
   ),

   svg: [
       {
           dir: 'svg',
           monotone: true,
           prefix: 'custom',
       },
       {
           dir: 'emojis',
           monotone: false,
           prefix: 'emoji',
       },
   ],

   iconifyIcons: [
       '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',
           ],
       },
   ],
};

// File to save bundle to
const target = 'assets/iconify-bundle.js';

/**
* Do stuff!
*/

(async function () {
   let bundle = '';

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

   /**
    * Bundle SVG framework
    */

   const isIconifyBundled = !!sources.svgFramework;
   const wrapperFunction = isIconifyBundled ? 'Iconify.addCollection' : 'add';
   if (sources.svgFramework) {
       bundle += await fs.readFile(sources.svgFramework, 'utf8');
       console.log('Bundled SVG framework');

       // Try to copy .d.ts
       const tsSource = sources.svgFramework.replace('.js', '.d.ts');
       try {
           const tsContent = await fs.readFile(tsSource);
           await fs.writeFile(target.replace('.js', '.d.ts'), tsContent);
       } catch (err) {
           //
       }
   }

   /**
    * Convert sources.iconifyIcons to sources.json
    */

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

       // Sort icons by prefix
       const organizedList = organizeIconsList(sources.iconifyIcons);
       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 += wrapperFunction + '(' + 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 += wrapperFunction + '(' + JSON.stringify(content) + ');\n';
       }
   }

   /**
    * Add wrapper function if SVG framework is not in bundle
    */

   if (!isIconifyBundled) {
       // Wrap in custom code that checks for Iconify.addCollection and IconifyPreload
       bundle = `(function() {
function add(data) {
    try {
        if (typeof self.Iconify === 'object' && self.Iconify.addCollection) {
            self.Iconify.addCollection(data);
            return;
        }
        if (typeof self.IconifyPreload === 'undefined') {
            self.IconifyPreload = [];
        }
        self.IconifyPreload.push(data);
    } catch (err) {
    }
}
${bundle}
})();\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

If you are using Iconify SVG framework in scripts and import it, you can import bundle instead. Bundle has all the same exports as SVG framework and TypeScript definitions file.

Before bundle:

import Iconify from '@iconify/iconify';

// do stuff...

With bundle:

import Iconify from './iconify-bundle';

// do stuff...

Usage in HTML

If you bundle Iconify SVG framework, you only need to include full file. You can link to a script using <script> tag or you can import it.

If you do not bundle Iconify SVG framework, generated bundle can be included before SVG framework or after it:

<html>
   <head>
       <script src="/assets/icons-bundle.js"></script>
       <script src="https://code.iconify.design/3/3.0.1/iconify.min.js"></script>
   </head>
   <body>
       <!-- content here -->
   </body>
</html>
Loading bundle before SVG framework in head section.
<html>
   <body>
       <!-- content here -->
       <script src="/assets/icons-bundle.js"></script>
       <script src="https://code.iconify.design/3/3.0.1/iconify.min.js"></script>
   </body>
</html>
Loading bundle before SVG framework in footer.
<html>
   <head>
       <script src="https://code.iconify.design/3/3.0.1/iconify.min.js"></script>
       <script src="/assets/icons-bundle.js"></script>
   </head>
   <body>
       <!-- content here -->
   </body>
</html>
Loading bundle after SVG framework in head section.