All files / packages/icons/scripts ui-icons-json.ts

94.11% Statements 32/34
83.33% Branches 20/24
100% Functions 6/6
93.93% Lines 31/33

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97                      1x       5x   1x   1x       2x   1x                       12x 12x 12x 12x 18x 18x 18x 6x 6x 5x   12x 5x     2x                         3x 5x 5x 11x 2x   9x     5x   8x         5x                   1x   1x 1x    
/*
 * Builds the published `ui.icons.json` contract. Kept separate from build.ts
 * (which runs the full build on import) so the parity-critical ordering,
 * filtering, and formatting can be unit tested.
 */
import fs from 'node:fs/promises';
import path from 'node:path';
 
import type { IconFile, IconType, UiIconsCategory, UiIconsEntry } from '../src/types.js';
 
/* Legacy action filter: `new_custom*` symbols ship as assets but are left out of ui.icons.json. */
const LEGACY_EXCLUDED_ACTION_SYMBOL = /^new_custom/;
 
/* Returns the human-readable category description embedded in ui.icons.json. */
function getCategoryDescription(iconType: IconType): string {
  switch (iconType) {
    case 'action':
      return 'Actions can be seen throughout the interface and represent actions a user can take on any given screen.';
    case 'doctype':
      return 'Doctype icons represent a type of file when a preview or image is unavailable';
    case 'standard':
      return 'Standard icons represent entities and objects within Salesforce.';
    case 'utility':
      return "Utility icons are used throughout the interface and are SVG's for extensibility.";
    case 'custom':
      return 'Custom icons are available for the identity of user created objects.';
    default:
      return '';
  }
}
 
/*
 * Compares symbols the way the legacy ui.icons.json was ordered: digit runs
 * numerically (`custom2` before `custom10`), everything else by code point
 * (`_` before letters).
 */
export function compareSymbols(a: string, b: string): number {
  const chunkRegex = /\d+|\D+/g;
  const aChunks = a.match(chunkRegex) ?? [];
  const bChunks = b.match(chunkRegex) ?? [];
  for (let i = 0; i < Math.min(aChunks.length, bChunks.length); i++) {
    const aChunk = aChunks[i];
    const bChunk = bChunks[i];
    if (/^\d/.test(aChunk) && /^\d/.test(bChunk)) {
      const diff = Number(aChunk) - Number(bChunk);
      if (diff !== 0) {
        return diff;
      }
    } else if (aChunk !== bChunk) {
      return aChunk < bChunk ? -1 : 1;
    }
  }
  return aChunks.length - bChunks.length;
}
 
/*
 * Builds one category per icon type in alphabetical order, each listing its
 * unique symbols sorted by `compareSymbols`. Matches the legacy published
 * layout, including the legacy action filter that leaves `new_custom*` symbols
 * out of the list even though their SVGs, PNGs, and sprite symbols still ship.
 */
export function buildUiIconsCategories(
  iconTypes: IconType[],
  iconsByType: Record<IconType, IconFile[]>,
): UiIconsCategory[] {
  return [...iconTypes].sort().map((iconType) => {
    const uniqueSymbols = new Set<string>();
    for (const icon of iconsByType[iconType]) {
      if (iconType === 'action' && LEGACY_EXCLUDED_ACTION_SYMBOL.test(icon.symbol)) {
        continue;
      }
      uniqueSymbols.add(icon.symbol);
    }
 
    const icons = Array.from(uniqueSymbols)
      .sort(compareSymbols)
      .map<UiIconsEntry>((symbol) => ({
        sprite: iconType,
        symbol,
      }));
 
    return {
      name: iconType,
      description: getCategoryDescription(iconType),
      icons,
    };
  });
}
 
/* Writes `ui.icons.json` into distDir and returns its path. */
export async function writeUiIconsJson(distDir: string, categories: UiIconsCategory[]): Promise<string> {
  const jsonPath = path.join(distDir, 'ui.icons.json');
  // No trailing newline, byte-matching the legacy ui.icons.json.
  await fs.writeFile(jsonPath, JSON.stringify(categories, null, 2), 'utf8');
  return jsonPath;
}