import path from 'path'; import { readSvgDirectory, getCurrentDirPath, readAllMetadata, readMetadata, readGroups, } from '../tools/build-helpers/helpers.ts'; import { type IconMetadata } from '../tools/build-icons/types.ts'; const currentDir = getCurrentDirPath(import.meta.url); const ICONS_DIR = path.resolve(currentDir, '../icons'); let icons: Record; try { icons = await readAllMetadata(ICONS_DIR) as Record; } catch (error) { console.error(`Failed to read icon metadata: ${error instanceof Error ? error.message : String(error)}`); console.error('This may be due to invalid extends or group references, or circular dependencies.'); process.exit(1); } const CATEGORIES_DIR = path.resolve(currentDir, '../categories'); const categories = await readAllMetadata(CATEGORIES_DIR) as Record;; console.log('Reading all icons'); const svgFiles = await readSvgDirectory(ICONS_DIR); const iconNames = svgFiles.map((icon) => icon.split('.')[0]); let error = false; iconNames.forEach((iconName) => { if (typeof icons[iconName] === 'undefined') { console.error(`'${iconName}.svg' does not have a matching JSON file.`); error = true; } }); Object.keys(icons).forEach((iconName) => { const icon = icons[iconName]; if (iconNames.indexOf(iconName) === -1) { console.error(`'${iconName}.svg' does not exist.`); error = true; } icon.categories?.forEach((categoryName) => { if (typeof categories[categoryName] === 'undefined') { console.error(`Icon '${iconName}' refers to the non-existing category '${categoryName}'.`); error = true; } }); }); const EXTENDS_PREFIX = '$extends:'; const GROUP_PREFIX = '$group:'; const MARKED_FIELDS = ['tags', 'categories', 'contributors'] as const; type MarkedField = (typeof MARKED_FIELDS)[number]; const FIELD_LABELS: Record = { tags: 'tag', categories: 'category', contributors: 'contributor', }; const isMarker = (entry: string) => entry.startsWith(EXTENDS_PREFIX) || entry.startsWith(GROUP_PREFIX); const groups = await readGroups(); const usedGroups = new Set(); // `readAllMetadata` expands the markers, so the duplicate checks below read the // JSON as it is written on disk to see which entries are literals. const iconJsonFiles = await readSvgDirectory(ICONS_DIR, '.json'); const rawIcons = Object.fromEntries( await Promise.all( iconJsonFiles.map(async (file) => [ path.basename(file, '.json'), (await readMetadata(file, ICONS_DIR)) as Partial>, ]), ), ) as Record>>; /** The values a single marker contributes to `field`. */ const resolveMarker = (marker: string, field: MarkedField): string[] => { if (marker.startsWith(GROUP_PREFIX)) { const groupName = marker.slice(GROUP_PREFIX.length); usedGroups.add(groupName); return groups[groupName] ?? []; } const parentName = marker.slice(EXTENDS_PREFIX.length); return (icons[parentName]?.[field] as string[] | undefined) ?? []; }; Object.entries(rawIcons).forEach(([iconName, rawMetadata]) => { MARKED_FIELDS.forEach((field) => { const entries = rawMetadata[field] ?? []; const literals = new Set(entries.filter((entry) => !isMarker(entry))); const seenMarkers = new Set(); entries.filter(isMarker).forEach((marker) => { if (seenMarkers.has(marker)) { console.error( `Icon '${iconName}': '${marker}' is listed twice in '${field}' — remove the duplicate marker.`, ); error = true; return; } seenMarkers.add(marker); resolveMarker(marker, field) .filter((value) => literals.has(value)) .forEach((value) => { console.error( `Icon '${iconName}': ${FIELD_LABELS[field]} '${value}' is already provided by ${marker} — remove the literal.`, ); error = true; }); }); }); }); // Check if the icon has literal tags that could be replaced by group references. Object.entries(rawIcons).forEach(([iconName, rawMetadata]) => { const tags = rawMetadata.tags ?? []; const unreferencedGroups = Object.entries(groups).filter( ([groupName]) => !tags.includes(`${GROUP_PREFIX}${groupName}`), ); tags .filter((entry) => !isMarker(entry)) .forEach((tag) => { const matchingMarkers = unreferencedGroups .filter(([, groupTags]) => groupTags.includes(tag)) .map(([groupName]) => `"${GROUP_PREFIX}${groupName}"`); if (matchingMarkers.length > 0) { console.error( `Icon '${iconName}': tag '${tag}' is in ${matchingMarkers.join(', ')} — consider using the group instead.`, ); } }); }); Object.keys(categories).forEach((categoryName) => { const category = categories[categoryName]; if (!category?.icon) { console.error(`Category '${categoryName}' does not use an icon '${category.icon}'.`); error = true; } else if (typeof icons[category.icon] === 'undefined') { console.error(`Category '${categoryName}' uses the non-existing icon '${category.icon}'.`); error = true; } }); Object.keys(groups) .filter((groupName) => !usedGroups.has(groupName)) .forEach((groupName) => { console.warn(`Group '${groupName}' is not used by any icon.`); }); if (error) { console.error('At least one error in icon JSONs prevents from committing changes.'); process.exit(1); }