Skip to content

Feature: bundle KibaIcon icons at build time - #259

Merged
krishan711 merged 13 commits into
mainfrom
bundled-icons
Nov 2, 2026
Merged

krishan711 merged 13 commits into
mainfrom
bundled-icons

Conversation

@krishan711

@krishan711 krishan711 commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

What/why: KibaIcon fetched every icon from assets-cdn.kiba.dev on every mount (no cache, no retry, stale responses could win). This bundles the icons an app uses at build time so we can retire that proxy. The KibaIcon iconId='ion-home' API is unchanged.

How apps enable it (config.build.js, and install the icon packages they use; they're optional peer deps, ui-react's own components only need ionicons):

import { kibaIconsPlugin } from '@kibalabs/ui-react/vite-plugin';
newConfig.viteConfigModifier = (viteConfig) => { viteConfig.plugins.push(kibaIconsPlugin()); return viteConfig; };
  • The plugin is a separate node-only entry (dist/vitePlugin, imports only node:fs/path/url).
  • In build and dev it scans src (ts/tsx/js/jsx/mdx) and ui-react's own dist for quoted ids like 'ion-home'. It reads the svgs straight from the installed packages: ionicons/dist/svg, @material-design-icons/svg/{filled,outlined,…} (mui-account-box is filled/account_box.svg, and a -outlined/-round/-sharp/-two-tone suffix picks that style), bootstrap-icons/icons, feather-icons/dist/icons and remixicon/icons/<Category>. Ids that don't resolve are skipped. If an id's package isn't installed you get one warning.
  • It normalises the svgs the same way the runtime path does, then registers them with registerKibaIcons from an inline module script it adds before the app's entry (and from non-html entries such as the ssr build).
  • In dev, editing a file to add a new icon reloads the page.
  • I read ionicons' svg files rather than the ionicons/icons data uris because those uris style outlines with ion-icon's css classes.

Fallback: ids it can't see (e.g. `ion-${name}`, ids from an api) and apps without the plugin still fetch from the CDN, now once per url per page, with one retry, ignoring responses for an old iconId. The preconnect in KibaApp stays for now because that fallback still uses the CDN.

Also:

  • AppDownloadButton badges are bundled (each in its own lazy chunk, 7-20kb) instead of loaded from the CDN.
  • Carousel uses ion-chevron-back/forward (was mui-chevron-left/right).
  • Fixed the AppDownloadButton docs page, which pointed at a story that doesn't exist.

Tested:

  • lint, type-check, build and build-docs pass. In the built storybook all story icons and badges render with every non-127.0.0.1 host blocked, and nothing is requested from assets-cdn.
  • In an ssr build fixture, icons are registered on the server entry.
  • yieldseeker app, with a packed tarball and only ionicons installed: 49 icons bundled (24.4kb raw / 5.8kb gzip).
    • First-page JS went from 951.3kb to 980.2kb raw (305.3kb to 313.0kb gzip, +7.7kb).
    • Total JS grew +255kb raw, almost all from the 16 lazy badge chunks, which are never loaded.
    • This replaces about 19-38 CDN requests per page.
    • I ran it end to end with the offline docker-compose (dev server and production build) in headless Chrome on 12 pages (Home, Account, Steer, Overview, Chat, Alerts, Leaderboard, Create agent, Insights, Account pro, Onchain, Support). Main had the CDN allowed and this branch had every non-127.0.0.1 host blocked. The same icons rendered with 0 requests to assets-cdn and no new console errors. On main with the CDN blocked, no icons rendered.

Fonts (kibaFontsPlugin), a separate plugin from the same @kibalabs/ui-react/vite-plugin entry, works like next/font for Google Fonts. Apps keep their @import url('https://fonts.googleapis.com/css2?...') (or an index.html <link>) and add viteConfig.plugins.push(kibaFontsPlugin());.

  • Build only. It runs on the compiled css after vite:css, so @imports in sass partials are found too. It downloads the stylesheet (with a Chrome UA, so it gets woff2 + unicode-range subsets) and each font file. The fonts are emitted as hashed assets and the @import is replaced by @font-face rules rebuilt from an allowlist of descriptors. Font urls go into the css through a content-hash placeholder that's resolved in generateBundle, so the css hash changes when a font does.
  • It only fetches from https://fonts.googleapis.com / https://fonts.gstatic.com, with no redirects. It checks content-type/extension plus woff2/woff/ttf magic bytes, caps sizes at 512kb for css and 5mb per font, and uses a 20s timeout. There are no new dependencies (Node's fetch/crypto).
  • It preloads only the latin, normal-style file of each family (the one closest to 400) used by css the page links. Lazy-chunk fonts aren't preloaded.
  • Downloads are cached in node_modules/.cache/kiba-fonts (keyed by url hash and validated on read). Offline or failed downloads leave the @import as is with one warning, and the build never fails. In dev the @import is left alone (fonts load from Google).
  • Tested on yieldseeker: Host Grotesk became 4 woff2 files (20.3/22.0/11.8/12.9kb), the css has no googleapis reference left, and 1 preload was added. With every non-127.0.0.1 host blocked in headless Chrome, document.fonts.check('16px "Host Grotesk"') is true and body text computes to Host Grotesk. There were 0 requests to Google and no font-related console errors. The text regions are pixel-identical to the same build without the plugin loaded with Google reachable. A cached rebuild with the network blocked works, an offline uncached build falls back with a warning, and make start still works. A fixture also covered an html <link>, a sass partial, a lazy css chunk, base: './' and cssCodeSplit: false.

…e per page

registerKibaIcons adds bundled svgs that KibaIcon renders on its first render. Other icons are still fetched,
now shared between KibaIcons (one request per url), retried once, and a stale response can't replace a newer icon.
…at build time

The plugin finds icon ids in the app's source and ui-react's own build, reads them from the installed icon
packages (now optional peer dependencies) and registers them before the app's entry runs.
…ts-cdn.kiba.dev

Each badge is its own lazily loaded chunk, so apps only download the ones they show.
A separate module made rolldown move all of ui-react into a shared chunk instead of keeping it in dist/index.js.
@krishan711
krishan711 merged commit 585788f into main Nov 2, 2026
4 checks passed
@krishan711
krishan711 deleted the bundled-icons branch November 2, 2026 19:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant