Bundling custom icons for Iconify components

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

These examples show how to create icon bundles for Iconify components from custom icon sets. These are very basic examples, for more advanced stuff look in code examples list.

Instructions

This example assumes you have converted custom icon set to IconifyJSON format. If not, see similar example that uses Iconify Tools.

Installation:

# Node.js
npm install --save-dev @iconify/utils
# PHP
composer require iconify/json-tools

PHP version assumes you are using Composer to manage dependencies.

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 value of sources to correct location of JSON files.
  • Change list of icons in variable icons to icons you want to bundle.
  • Run script.

Components

Values for variable component are package names for components:

Bundle script

Example below create JavaScript files with icon data loaded with addCollection function that is imported from a component package.

Node.js
/**
* This is a simple example for creating icon bundles for Iconify SVG Framework.
*
* It bundles only icons from Iconify icon sets.
* For bundling custom icons see other code examples in documentation.
*/

import { readFileSync, writeFileSync, mkdirSync } from 'fs';
import { dirname } from 'path';

// Installation: npm install --save-dev @iconify/utils @iconify/json
import { getIcons, stringToIcon, minifyIconSet } from '@iconify/utils';

// 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 = 'assets/icons-bundle.js';

// JSON files location. Filename inside directory must match prefix. For example, for "line-md" prefix icons should be stored in "line-md.json"
const source = 'json';

// Icons to bundle, array
const iconsToBundle = [
   'line-md:home-twotone-alt',
   'line-md:github',
   'line-md:image-twotone',
   'octicon:book-24',
   'octicon:code-square-24',
];

// Organize icons by prefix
const sortedIcons = organizeIconsList(iconsToBundle);

// Load icons data
let output = commonJS
   ? "const { addCollection } = require('" + component + "');\n\n"
   : "import { addCollection } from '" + component + "';\n\n";

Object.keys(sortedIcons).forEach((prefix) => {
   const iconsList = sortedIcons[prefix];

   // Load icon set
   const filename = `${source}/${prefix}.json`;
   const iconSet = JSON.parse(readFileSync(filename, 'utf8'));

   // Get data for all icons as string
   const data = getIcons(iconSet, iconsList, true);
   if (!data) {
       throw new Error(`Could not get icons for "${prefix}" icon set.`);
   }
   if (data.not_found?.length) {
       throw new Error(
           `Could not find icons in "${prefix}" icon set: ${data.not_found.join(
               ', '
           )}
`

       );
   }
   minifyIconSet(data);

   output += 'addCollection(' + JSON.stringify(data) + ');\n';
});

// Save to file
const dir = dirname(target);
try {
   mkdirSync(dir, {
       recursive: true,
   });
} catch (err) {
   //
}
writeFileSync(target, output, 'utf8');

console.log(`Saved ${target} (${output.length} bytes)`);

/**
* 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;
}
PHP
<?php
/**
* This is a simple example for creating icon bundles for Iconify components.
*
* It bundles only icons from Iconify JSON files.
* For more advanced examples see other code examples in documentation.
*
* See Iconify Tools documentation on how to convert custom icons to Iconify JSON format:
* https://docs.iconify.design/tools/node/import.html
*/


require './vendor/autoload.php';

// Installation: composer require iconify/json-tools
use Iconify\JSONTools\Collection;

// 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
$component = '@iconify/react';

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

// File to save bundle to
$target = 'assets/icons-bundle.js';

// JSON files location. Filename inside directory must match prefix. For example, for "line-md" prefix icons should be stored in "line-md.json"
$source = 'json';

// Icons to bundle, array
$icons = [
   'line-md:home-twotone-alt',
   'line-md:github',
   'line-md:image-twotone',
   'octicon:book-24',
   'octicon:code-square-24',
];

// Organize icons by prefix
$icons = organizeIconsList($icons);

// Load icons data
$output = $commonJS ? "const { addCollection } = require('$component');\n\n" : "import { addCollection } from '$component';\n\n";
foreach ($icons as $prefix => $iconsList) {
   // Load icon set
   $collection = new Collection($prefix);
   if (!$collection->loadFromFile($source . '/' . $prefix . '.json')) {
       throw new Error('Cannot find file "' . $prefix . '.json"');
   }

   // Make sure all icons exist
   foreach ($iconsList as $name) {
       if (!$collection->iconExists($name)) {
           // Uncomment next line to throw error if an icon does not exist
           // throw new Error('Could not find icon: "' . $prefix . ':' . $name . '"');
           echo 'Could not find icon: "', $prefix, ':', $name, "\"\n";
       }
   }

   // Get data for all icons as string
   $output .= $collection->scriptify([
       'icons' => $iconsList,
       'callback' => 'addCollection',
       'optimize' => true,
   ]);
}

// Save to file
file_put_contents($target, $output);

echo 'Saved ', $target, ' (', strlen($output), " bytes)\n";

/**
* Organize icons list by prefix
*
* Result is an object, where key is prefix, value is array of icon names
*/

function organizeIconsList($icons)
{
   $results = [];

   foreach ($icons as $str) {
       // Split icon to prefix and name
       $icon = stringToIcon($str);
       if ($icon === null || $icon['provider'] !== '') {
           // Invalid name or icon name has provider.
           // All icons in this example are from Iconify, so providers are not supported.
           throw new Error('Invalid icon name: ' . $str);
       }

       $prefix = $icon['prefix'];
       $name = $icon['name'];

       // Add icon to results
       if (!isset($results[$prefix])) {
           $results[$prefix] = [$name];
           continue;
       }
       if (!in_array($name, $results[$prefix])) {
           $results[$prefix][] = $name;
       }
   }

   return $results;
}

/**
* Convert icon name from string to object.
*
* Object properties:
* - provider (ignored in this example)
* - prefix
* - name
*
* This function was converted to PHP from @iconify/utils/src/icon/name.ts
* See https://github.com/iconify/iconify/blob/master/packages/utils/src/icon/name.ts
*/

function stringToIcon($value)
{
   $provider = '';
   $colonSeparated = explode(':', $value);

   // Check for provider with correct '@' at start
   if (substr($value, 0, 1) === '@') {
       // First part is provider
       if (count($colonSeparated) < 2 || count($colonSeparated) > 3) {
           // "@provider:prefix:name" or "@provider:prefix-name"
           return null;
       }
       $provider = substr(array_shift($colonSeparated), 1);
   }

   // Check split by colon: "prefix:name", "provider:prefix:name"
   if (count($colonSeparated) > 3 || !count($colonSeparated)) {
       return null;
   }
   if (count($colonSeparated) > 1) {
       // "prefix:name"
       $name = array_pop($colonSeparated);
       $prefix = array_pop($colonSeparated);
       return [
           // Allow provider without '@': "provider:prefix:name"
           'provider' => count($colonSeparated) > 0 ? $colonSeparated[0] : $provider,
           'prefix' => $prefix,
           'name' => $name,
       ];
   }

   // Attempt to split by dash: "prefix-name"
   $dashSeparated = explode('-', $colonSeparated[0]);
   if (count($dashSeparated) > 1) {
       return [
           'provider' => $provider,
           'prefix' => array_shift($dashSeparated),
           'name' => implode('-', $dashSeparated),
       ];
   }

   return null;
}

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');