diff --git a/.github/instructions/metadata.instructions.md b/.github/instructions/metadata.instructions.md index a1e99a8e6..99f18c5ff 100644 --- a/.github/instructions/metadata.instructions.md +++ b/.github/instructions/metadata.instructions.md @@ -19,6 +19,33 @@ Provide tag suggestions based on the name of the icon and the use cases provided The `categories` property is an array of strings that specify the categories to which the icon belongs, such as "devices", "interface", "media", etc. See the `categories` property in the `icon.schema.json` for more details on the allowed values. The categories should be chosen based on the use cases provided in the PR description and the existing categories in the repository. The categories should be relevant to the icon and should help users find the icon when searching for specific types of icons. The name of icon should not be included in the categories, as it is already specified. Suggest categories based on the name of the icon and the use cases provided in the PR description. Use the existing categories in the repository as a reference for consistency and to avoid duplicates. Categories should always be in lowercase and should not contain spaces. +## Extending metadata from other icons + +Some icons are variants of another icon (for example `alarm-clock-minus` is an `alarm-clock`). Instead of repeating the same values, an icon can inherit `tags`, `categories`, or `contributors` from its **base icon** using an inline `"$extends:"` marker placed directly inside the relevant array. At build time each marker is replaced, in place, with the referenced icon's fully resolved values for that same field. + +```jsonc +{ + "tags": ["$extends:alarm-clock", "remove"], + "categories": ["$extends:alarm-clock"], + "contributors": ["$extends:alarm-clock", "some-user"] +} +``` + +When checking or adding a `$extends:` marker: + +- A marker must be exactly `"$extends:"` followed by the name of an **existing** icon — one that has its own `icons/.json` and `icons/.svg`. The icon name is the file name without the `.json` extension, written in lowercase kebab-case. Referencing a non-existent icon is invalid. +- Markers are only supported inside the `tags`, `categories`, and `contributors` arrays. They are not a separate property. +- Only use `$extends:` for the icon's **base icon** — the thing the icon _is_ (`file-plus` is a `file`, `alarm-clock-minus` is an `alarm-clock`). Never extend a **modifier** icon such as `plus`, `minus`, `x` or `check`: their tags describe that glyph on its own (`minus` carries `divider`, `hr`, `markdown`, `separator`…), which makes no sense on an alarm clock. Add the modifier's meaning as literal tags instead. +- An icon may not extend itself, and circular chains (A extends B, B extends A) are not allowed. +- Ordering is preserved: inherited values are inserted at the marker's position, so place `$extends:` markers where the inherited values should appear (conventionally first, before the icon's own literal values). +- Run `pnpm checkIcons` to validate all references. It fails on missing icons, self-references, and circular dependencies. Do not assume a marker is valid without confirming the target icon exists. + +## There should not be duplicates + +Never list a value that a `$extends:` marker already provides. Before adding a literal `tag`, `category`, or `contributor`, look at the referenced icon's metadata: if that icon already contains the value, do not also add it as a literal entry in the same array — the inherited value already covers it. For example, if `alarm-clock` already has the tag `morning`, an icon that uses `"$extends:alarm-clock"` in its `tags` must **not** also list `"morning"`. + +Likewise, do not reference the same icon more than once in a single field (no duplicate `$extends:` markers), and do not repeat literal values. The `tags`, `categories`, and `contributors` arrays must contain only unique entries (`uniqueItems` in `icon.schema.json`), and after extends resolution the final arrays must not contain any duplicates. + ## Use Cases The `use-cases` property is an array of strings that describe the specific scenarios in which the icon would be used. Each use case should be concise and clearly convey the context in which the icon is applicable. Use the existing use cases in the repository as a reference for consistency and to avoid duplicates. diff --git a/.github/workflows/linting-icons.yml b/.github/workflows/linting-icons.yml index 5a6c2f213..0e5e9b5f3 100644 --- a/.github/workflows/linting-icons.yml +++ b/.github/workflows/linting-icons.yml @@ -42,6 +42,25 @@ jobs: env: CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} + check-icons: + name: Check Icons & Categories + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + cache: 'pnpm' + node-version-file: 'package.json' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Check icons and categories + run: pnpm checkIcons + check-lab-icons: name: Check Lab Icons runs-on: ubuntu-latest diff --git a/docs/.vitepress/data/categoriesData.json b/docs/.vitepress/data/categoriesData.json index 03b97984f..3106ffc8c 100644 --- a/docs/.vitepress/data/categoriesData.json +++ b/docs/.vitepress/data/categoriesData.json @@ -75,6 +75,10 @@ "name": "layout", "title": "Layout" }, + { + "name": "letters-digits", + "title": "Letters & digits" + }, { "name": "mail", "title": "Mail" @@ -97,7 +101,7 @@ }, { "name": "navigation", - "title": "Navigation & Places" + "title": "Navigation & places" }, { "name": "notifications", diff --git a/docs/.vitepress/lib/icons.ts b/docs/.vitepress/lib/icons.ts deleted file mode 100644 index 05a78bbb0..000000000 --- a/docs/.vitepress/lib/icons.ts +++ /dev/null @@ -1,49 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import { IconNodeWithKeys } from '../theme/types'; -import iconNodes from '../data/iconNodes'; -import releaseMeta from '../data/releaseMetaData.json'; - -const DATE_OF_FORK = '2020-06-08T16:39:52+0100'; - -const directory = path.join(process.cwd(), '../icons'); - -export interface GetDataOptions { - withChildKeys?: boolean; -} - -export async function getData(name: string) { - const jsonPath = path.join(directory, `${name}.json`); - const jsonContent = fs.readFileSync(jsonPath, 'utf8'); - const { tags, categories, contributors } = JSON.parse(jsonContent); - - const iconNode = iconNodes[name]; - - const releaseData = releaseMeta?.[name] ?? { - fromFork: false, - awaitingRelease: false, - createdRelease: { - version: '0.0.0', - date: DATE_OF_FORK, - }, - changedRelease: { - version: '0.0.0', - date: DATE_OF_FORK, - }, - }; - - return { - name, - tags, - categories, - iconNode, - contributors, - ...releaseData, - }; -} - -export async function getAllData(): Promise<{ name: string; iconNode: IconNodeWithKeys }[]> { - const names = Object.keys(iconNodes); - - return Promise.all(names.map((name) => getData(name))); -} diff --git a/docs/contribute/icons/metadata-conventions/index.md b/docs/contribute/icons/metadata-conventions/index.md index ecab18d72..464ea32ea 100644 --- a/docs/contribute/icons/metadata-conventions/index.md +++ b/docs/contribute/icons/metadata-conventions/index.md @@ -135,6 +135,32 @@ Write metadata for the specific variant: When a request covers several icons, assign each use case only to the icon it describes. Do not copy metadata to icons it does not describe. +## Reusing metadata: `$extends:` + +Variant icons do not have to repeat the metadata of the icon they are built on. A `$extends:` marker means **is a**: at build time it is replaced, in place, by the resolved `tags`, `categories` or `contributors` of that icon. + +`alarm-clock-minus` is an alarm clock: + +```json +{ + "tags": ["$extends:alarm-clock", "remove"], + "categories": ["$extends:alarm-clock"] +} +``` + +which resolves to: + +```json +{ + "tags": ["morning", "remove"], + "categories": ["devices", "notifications", "time"] +} +``` + +Use `$extends:` only for the **base icon** — the thing the icon is. Do not extend a modifier icon such as `plus`, `minus`, `x` or `check`: their tags describe that glyph on its own (`minus` carries `divider`, `hr`, `markdown`…), which is wrong on a file or an alarm clock. Add the modifier's meaning as the icon's own tags instead. + +Put the marker first, before the icon's own values, and never repeat a value the marker already provides — `pnpm checkIcons` fails on that. + ## Aliases Some icons can include an optional `aliases` field for alternate names. @@ -149,6 +175,12 @@ Before submitting metadata changes, validate the icon JSON files: pnpm run lint:json:icons ``` +`$extends:` markers are checked separately: + +```sh +pnpm checkIcons +``` + You can also format changed files with Prettier: ```sh @@ -166,4 +198,5 @@ Before opening a pull request, confirm that: - Tags are lowercase and useful for search. - Categories are allowed by `icon.schema.json`. - Metadata is specific to the icon and its variants. -- `pnpm run lint:json:icons` passes. +- Reused metadata uses `$extends:` for the base icon, with no repeated values. +- `pnpm run lint:json:icons` and `pnpm checkIcons` pass. diff --git a/docs/scripts/writeIconDetails.mts b/docs/scripts/writeIconDetails.mts index d3f44a96a..8a9785c52 100644 --- a/docs/scripts/writeIconDetails.mts +++ b/docs/scripts/writeIconDetails.mts @@ -1,10 +1,11 @@ import fs from 'fs'; import path from 'path'; -import { readSvgDirectory, toCamelCase } from '@lucide/helpers'; +import { getIconMetaData } from '@lucide/build-icons'; +import { toCamelCase } from '@lucide/helpers'; const currentDir = process.cwd(); const ICONS_DIR = path.resolve(currentDir, '../icons'); -const icons = await readSvgDirectory(ICONS_DIR, '.json'); +const iconMetaData = await getIconMetaData(ICONS_DIR); const iconDetailsDirectory = path.resolve(currentDir, '.vitepress/data', 'iconDetails'); @@ -18,18 +19,27 @@ if (!fs.existsSync(iconDetailsDirectory)) { const indexFile = path.resolve(iconDetailsDirectory, `index.ts`); -const writeIconFiles = icons.map(async (iconFileName) => { - const iconName = path.basename(iconFileName, '.json'); +const writeIconFiles = Object.entries(iconMetaData).map(async ([iconName, metaData]) => { const location = path.resolve(iconDetailsDirectory, `${iconName}.ts`); + const { + tags, + categories, + contributors, + aliases, + deprecated, + deprecationReason, + toBeRemovedInVersion, + } = metaData; const contents = `\ import iconNode from '../iconNodes/${iconName}.node.json' with { type: 'json' }; -import metaData from '../../../../icons/${iconName}.json' with { type: 'json' }; import releaseData from '../releaseMetadata/${iconName}.json' with { type: 'json' }; import popularity from '../iconPopularity/${iconName}.json' with { type: 'json' }; import { IconMetaData, IconNode } from '~/.vitepress/theme/types'; -const { tags, categories, contributors, aliases, deprecated, deprecationReason, toBeRemovedInVersion }: IconMetaData = metaData; +const metaData: IconMetaData = ${JSON.stringify({ tags, categories, contributors, aliases, deprecated, deprecationReason, toBeRemovedInVersion })}; + +const { tags, categories, contributors, aliases, deprecated, deprecationReason, toBeRemovedInVersion } = metaData; const iconDetails = { name: '${iconName}', diff --git a/docs/scripts/writeIconMetaIndex.mts b/docs/scripts/writeIconMetaIndex.mts index 2a86f73ec..7f7a752d2 100644 --- a/docs/scripts/writeIconMetaIndex.mts +++ b/docs/scripts/writeIconMetaIndex.mts @@ -1,10 +1,9 @@ import fs from 'fs'; import path from 'path'; -import { readSvgDirectory, toCamelCase } from '@lucide/helpers'; +import { readAllMetadata } from '@lucide/helpers'; const currentDir = process.cwd(); const ICONS_DIR = path.resolve(currentDir, '../icons'); -const iconJsonFiles = await readSvgDirectory(ICONS_DIR, '.json'); const location = path.resolve(currentDir, '.vitepress/data', 'iconMetaData.ts'); @@ -12,29 +11,15 @@ if (fs.existsSync(location)) { fs.unlinkSync(location); } -const iconMetaIndexFileImports = []; -const iconMetaIndexFileExports = []; - -iconJsonFiles.forEach((iconJsonFile) => { - const iconName = path.basename(iconJsonFile, '.json'); - - iconMetaIndexFileImports.push( - `import ${toCamelCase(iconName)}Metadata from '../../../icons/${iconName}.json';`, - ); - iconMetaIndexFileExports.push(` '${iconName}': ${toCamelCase(iconName)}Metadata,`); -}); +// The docs read this index raw and never re-resolve, so resolve the +// `$extends:` markers here and bake the inherited +// tags/categories/contributors into the generated file. +const iconMetaData = await readAllMetadata(ICONS_DIR); try { await fs.promises.writeFile( location, - `\ -${iconMetaIndexFileImports.join('\n')} - - - export default { -${iconMetaIndexFileExports.join('\n')} - } - `, + `export default ${JSON.stringify(iconMetaData, null, 2)};\n`, 'utf-8', ); diff --git a/icon.schema.json b/icon.schema.json index a35a63a23..01dbca816 100644 --- a/icon.schema.json +++ b/icon.schema.json @@ -50,51 +50,59 @@ "categories": { "type": "array", "items": { - "type": "string", - "enum": [ - "accessibility", - "account", - "animals", - "arrows", - "buildings", - "charts", - "communication", - "connectivity", - "cursors", - "design", - "development", - "devices", - "emoji", - "files", - "finance", - "food-beverage", - "gaming", - "home", - "layout", - "letters-digits", - "mail", - "math", - "medical", - "multimedia", - "nature", - "navigation", - "notifications", - "people", - "photography", - "science", - "seasons", - "security", - "shapes", - "shopping", - "social", - "sports", - "sustainability", - "text", - "time", - "tools", - "transportation", - "travel", - "weather" + "anyOf": [ + { + "type": "string", + "enum": [ + "accessibility", + "account", + "animals", + "arrows", + "buildings", + "charts", + "communication", + "connectivity", + "cursors", + "design", + "development", + "devices", + "emoji", + "files", + "finance", + "food-beverage", + "gaming", + "home", + "layout", + "letters-digits", + "mail", + "math", + "medical", + "multimedia", + "nature", + "navigation", + "notifications", + "people", + "photography", + "science", + "seasons", + "security", + "shapes", + "shopping", + "social", + "sports", + "sustainability", + "text", + "time", + "tools", + "transportation", + "travel", + "weather" + ] + }, + { + "type": "string", + "pattern": "^\\$extends:[a-z0-9]+(-[a-z0-9]+)*$" + } ] }, "uniqueItems": true @@ -102,7 +110,13 @@ "contributors": { "type": "array", "items": { - "type": "string" + "type": "string", + "if": { + "pattern": "^\\$[a-zA-Z]" + }, + "then": { + "pattern": "^\\$extends:[a-z0-9]+(-[a-z0-9]+)*$" + } }, "minItems": 1, "uniqueItems": true @@ -110,7 +124,13 @@ "tags": { "type": "array", "items": { - "type": "string" + "type": "string", + "if": { + "pattern": "^\\$[a-zA-Z]" + }, + "then": { + "pattern": "^\\$extends:[a-z0-9]+(-[a-z0-9]+)*$" + } }, "minItems": 1, "uniqueItems": true diff --git a/icons/alarm-clock-minus.json b/icons/alarm-clock-minus.json index 1b6ffa662..28a0c8541 100644 --- a/icons/alarm-clock-minus.json +++ b/icons/alarm-clock-minus.json @@ -8,12 +8,11 @@ ], "use-cases": [], "tags": [ + "$extends:alarm-clock", "remove" ], "categories": [ - "devices", - "notifications", - "time" + "$extends:alarm-clock" ], "aliases": [ { diff --git a/scripts/checkIconsAndCategories.mts b/scripts/checkIconsAndCategories.mts index a5d6dd9e4..b77568aaa 100644 --- a/scripts/checkIconsAndCategories.mts +++ b/scripts/checkIconsAndCategories.mts @@ -3,12 +3,22 @@ import { readSvgDirectory, getCurrentDirPath, readAllMetadata, + readMetadata, } 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'); -const icons = await readAllMetadata(ICONS_DIR) as Record; +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 references or circular dependencies.'); + process.exit(1); +} + const CATEGORIES_DIR = path.resolve(currentDir, '../categories'); const categories = await readAllMetadata(CATEGORIES_DIR) as Record { }); }); +const EXTENDS_PREFIX = '$extends:'; + +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); + +// `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[] => { + 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; + }); + }); + }); +}); + Object.keys(categories).forEach((categoryName) => { const category = categories[categoryName]; if (!category?.icon) { diff --git a/tools/build-helpers/src/readAllMetadata.ts b/tools/build-helpers/src/readAllMetadata.ts index 175296376..270b8ea91 100644 --- a/tools/build-helpers/src/readAllMetadata.ts +++ b/tools/build-helpers/src/readAllMetadata.ts @@ -1,6 +1,7 @@ import fs from 'fs/promises'; import path from 'path'; import { readMetadata } from './readMetadata.ts'; +import { resolveMetadataExtends } from './resolveMetadataExtends.ts'; import { type IconMetadata } from '../../build-icons/types.ts'; /** @@ -22,5 +23,14 @@ export const readAllMetadata = async (directory: string): Promise value.startsWith(EXTENDS_PREFIX); +const getExtendsTarget = (value: string): string => value.slice(EXTENDS_PREFIX.length); + +interface ResolveOptions { + validateReferences?: boolean; +} + +/** + * Resolves metadata extends for all icons + * Recursively resolves extends chains and merges inherited properties + * Inline `$extends:` markers inside the tags/categories/contributors + * arrays are expanded in place with the referenced icon's resolved values. + */ +export const resolveMetadataExtends = async ( + allMetadata: Record, + options: ResolveOptions = {}, +): Promise> => { + const { validateReferences = true } = options; + const visited = new Set(); + const resolved: Record = {}; + + // Validate all extends references first if requested + if (validateReferences) { + for (const [iconName, metadata] of Object.entries(allMetadata)) { + validateExtendsReferences(iconName, metadata, allMetadata); + } + } + + // Resolve each icon. Icons already resolved as a parent of an earlier icon + // are returned from the cache by resolveIconExtends, so every icon still ends + // up in the resulting map. + for (const iconName of Object.keys(allMetadata)) { + resolved[iconName] = resolveIconExtends(iconName, allMetadata, visited, new Set()); + } + + return resolved; +}; + +/** + * Resolves extends for a single icon + * Uses recursion stack to detect circular dependencies + */ +export const resolveIconExtends = ( + iconName: string, + allMetadata: Record, + visited: Set, + recursionStack: Set, +): IconMetadata => { + // Check for circular dependency + if (recursionStack.has(iconName)) { + throw new Error(`Circular dependency detected: Icon '${iconName}' extends itself in a cycle`); + } + + // Return cached result if already resolved + if (visited.has(iconName)) { + return allMetadata[iconName] as IconMetadata; + } + + const metadata = allMetadata[iconName]; + if (!metadata) { + throw new Error(`Icon '${iconName}' not found`); + } + + recursionStack.add(iconName); + + // Expand inline `$extends:` markers within a single field, keeping + // literal values in place and de-duplicating the result (first occurrence wins). + const resolveField = (field: ExtendableField): string[] => { + const source = (metadata[field] as string[] | undefined) ?? []; + const expanded: string[] = []; + + for (const entry of source) { + if (isExtendsMarker(entry)) { + const parentName = getExtendsTarget(entry); + validateParentExists(iconName, parentName, allMetadata); + validateNotSelfReference(iconName, parentName); + + const parent = resolveIconExtends(parentName, allMetadata, visited, recursionStack); + expanded.push(...((parent[field] as string[] | undefined) ?? [])); + } else { + expanded.push(entry); + } + } + + return mergeArrays(expanded, []); + }; + + const resolvedMetadata = { + ...metadata, + tags: resolveField('tags'), + categories: resolveField('categories'), + contributors: resolveField('contributors'), + } as IconMetadata; + + recursionStack.delete(iconName); + visited.add(iconName); + + // Update the metadata in place for caching + Object.assign(allMetadata[iconName], resolvedMetadata); + + return resolvedMetadata; +}; + +/** + * Validates that all inline extends markers point to existing icons + */ +export const validateExtendsReferences = ( + iconName: string, + metadata: IconMetadata, + allMetadata: Record, +): void => { + for (const field of EXTENDABLE_FIELDS) { + for (const entry of (metadata[field] as string[] | undefined) ?? []) { + if (!isExtendsMarker(entry)) continue; + + const parentName = getExtendsTarget(entry); + if (!allMetadata[parentName]) { + throw new Error(`Icon '${iconName}' extends non-existent icon '${parentName}'.`); + } + } + } +}; + +/** + * Validates that a parent exists in metadata + */ +function validateParentExists( + iconName: string, + parentName: string, + allMetadata: Record, +): void { + if (!allMetadata[parentName]) { + throw new Error(`Icon '${iconName}' extends non-existent icon '${parentName}'`); + } +} + +/** + * Validates that an icon doesn't extend from itself + */ +function validateNotSelfReference(iconName: string, parentName: string): void { + if (iconName === parentName) { + throw new Error(`Icon '${iconName}' cannot extend from itself`); + } +} diff --git a/tools/build-icons/types.ts b/tools/build-icons/types.ts index 4ba3eee5b..41f8e08c5 100644 --- a/tools/build-icons/types.ts +++ b/tools/build-icons/types.ts @@ -36,10 +36,12 @@ export type IconDeprecationReason = 'icon.design' | 'icon.use-case' | ''; export type IconMetadataBase = { categories: string[]; + contributors?: string[]; aliases?: AliasDeprecation[]; tags: string[]; deprecationReason?: IconDeprecationReason; deprecated?: boolean; + toBeRemovedInVersion?: string; }; export type IconMetadataWithDeprecation = IconMetadataBase & { diff --git a/tools/build-icons/utils/getIconMetaData.ts b/tools/build-icons/utils/getIconMetaData.ts index e3c0d94f8..ca7430a73 100644 --- a/tools/build-icons/utils/getIconMetaData.ts +++ b/tools/build-icons/utils/getIconMetaData.ts @@ -2,6 +2,7 @@ import path from 'path'; import { pathToFileURL } from 'url'; import { readSvgDirectory } from '@lucide/helpers'; import { type IconMetadata } from '../types.ts'; +import { resolveMetadataExtends } from '../../build-helpers/src/resolveMetadataExtends.ts'; async function getIconMetaData(iconDirectory: string): Promise> { const iconJsons = await readSvgDirectory(iconDirectory, '.json'); @@ -15,7 +16,10 @@ async function getIconMetaData(iconDirectory: string): Promise