Codegen
Generate a manifest and per-icon import map for any icon set, for use in a custom adapter.
@10x-media/fields/icon/codegen generates the artifacts an adapter needs: a manifest (names, tags, categories for browsing and validation) and, optionally, a per-icon import map (for lazy loading) and bulk glyph node-data (for fast drawer rendering). Run it at build time in your project and commit the output next to your adapter.
import { generateIconManifest } from '@10x-media/fields/icon/codegen'
await generateIconManifest({
source: {
icons: [
{ name: 'rocket', tags: ['launch', 'ship'], categories: ['brand'] },
{ name: 'planet', tags: ['world'], categories: ['brand'] },
],
importFor: (icon) => ({ module: `@acme/icons/${icon.name}` }),
},
outDir: './src/icons/acme',
regenCommand: 'pnpm generate:acme-icons',
})You describe the icon set as a source: either a built-in library name ('lucide' | 'radix' | 'tabler') or a custom object with an icons array (each entry a { name, tags, categories }), an optional importFor that maps an icon to its dynamic import, and optional nodes bulk glyph data.
The run emits into outDir:
manifest.tsalways, exporting a lazily importablemanifest.imports.tswhenimportForis set, exporting aniconImportsmap of name to dynamic import.nodes.tswhennodesis set, for inline-SVG drawer rendering (omit it for libraries the drawer renders through the per-iconIconcomponent, like Radix).
Icon names are validated as kebab-case, de-duplicated, and sorted, so committed output is byte-stable across machines. allowMissingCategories lets a source that cannot fetch category metadata emit without categories (off by default, so a broken run fails loudly instead of dropping categories silently).
Wire the output into an adapter and its client components:
import { defineIconAdapter } from '@10x-media/fields/icon'
export const acmeIconsAdapter = defineIconAdapter({
slug: 'acme',
label: 'Acme Icons',
loadManifest: () => import('./manifest').then((m) => m.manifest),
Icon: '@/icons/acme/client#AcmeAdapterIcon',
Assets: '@/icons/acme/client#AcmeAdapterAssets',
})The Icon component resolves a name through the generated iconImports map (a React.lazy per entry for the client, an awaited import for RSC).
In-house SVG sets
For a custom set of SVG files, build the icons array yourself, deriving each name from a file (kebab-case) and categories from its folder, then pass it as the source with an importFor that points at your per-icon modules or a wrapper component. Codegen validates and emits; it does not crawl a directory for you.
Recipes
- Heroicons / Phosphor: build the
iconsarray from the package's icon list and pointimportForat each icon's module. Each style variant (outline, solid) works best as its own adapter slug so stored values stay unambiguous. - Full shadcn set: shadcn projects standardize on Lucide, so the first-party
lucideAdapter()already covers them; add codegen adapters only for the extra sets your design system pulls in. - Regeneration: re-run codegen when you bump the icon package. The manifest is data, so a new version adding icons is additive; removing icons turns stored values into gracefully degraded ones rather than errors.
Icon name rules
A name must be non-empty and free of whitespace, control characters, : (which separates library from name in a stored value), and the ' and \ that would escape the emitted import key. Everything else is legitimate, including uppercase, digits, dots and non-latin scripts, so a library keyed by code (HUN, SUI) generates like any other.
Names that differ only by case are rejected. Drawer search normalises case, so HUN beside hun would be indistinguishable to an editor.