feat(unplugin): pre-bundle used icons into the Vue/Vite build#6635
feat(unplugin): pre-bundle used icons into the Vue/Vite build#6635benjamincanac wants to merge 17 commits into
Conversation
Add the icons Nuxt UI uses (chevrons, loading spinner, close, etc.) to `@nuxt/icon`'s client bundle via the `icon:clientBundleIcons` hook so they're embedded at build time instead of fetched at runtime, removing the first-paint flash. Icons are only added when their collection data is installed (resolved from `rootDir`/`workspaceDir` to match `@nuxt/icon`), so missing collections fall back to runtime loading instead of failing the build. Resolves #6295
Temporary: relies on nuxt/icon#502 (client bundle resolves collections from rootDir/workspaceDir). Revert to a normal version once it's released.
Embed the icons Nuxt UI uses at build time via a new `@nuxt/ui/vite` sub-plugin so they render during SSR and work fully offline with no runtime Iconify API fetch, mirroring what #6633 does on the Nuxt side. Adds an `icon.clientBundle` option to bundle your own icons on top of the defaults. Co-authored-by: Typed SIGTERM <145281501+typed-sigterm@users.noreply.github.com>
# Conflicts: # package.json # pnpm-lock.yaml # src/module.ts # src/utils/icons.ts # test/utils/icons.spec.ts
commit: |
A virtual module can't resolve a bare specifier like `@iconify/vue` in a non-workspace install, which broke standalone Vue builds. Export only the icon data from `virtual:nuxt-ui-icons` and do the `addIcon` registration in a real runtime plugin that owns the `@iconify/vue` import.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds build-time icon bundling for Nuxt UI in Vue/Vite mode. Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
docs/content/docs/1.getting-started/6.integrations/1.icons/2.vue.md (1)
102-104: 📐 Maintainability & Code Quality | 🔵 TrivialVerify the anchor target for this note.
The note advises
{collection}:{name}form for dash-containing collections. Ensure thevite.config.tsexample above this note also works with thei-prefix syntax (e.g.,i-material-symbols-menu) since users may try that first. Consider adding a brief mention thatclientBundle.iconsrequires the explicit{collection}:{name}form, not thei-prefixed class-style syntax.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/content/docs/1.getting-started/6.integrations/1.icons/2.vue.md` around lines 102 - 104, The note currently only points users to the {collection}:{name} format, but the nearby vite.config.ts example should also explicitly clarify that the i- prefix syntax (for example, i-material-symbols-menu) is not supported by clientBundle.icons. Update the documentation around the icons example and the note so it explains that collections with dashes must use the explicit {collection}:{name} form, and add a brief mention that users should install the matching `@iconify-json/`{collection_name} package for each referenced collection.test/utils/icon-bundle.spec.ts (1)
8-9: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd one fixture test for a non-root
cwd.These assertions only exercise
loadIconsData(..., process.cwd()), so the new workspace/monorepo behavior is still unguarded. A focused test that passes a nested project root would protect the mainconfig.rootclaim in this PR.Also applies to: 17-17, 25-25
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/utils/icon-bundle.spec.ts` around lines 8 - 9, Add a focused fixture test in icon-bundle.spec.ts that exercises loadIconsData with a non-root cwd instead of only process.cwd(). Use the existing loadIconsData test cases as a guide, but pass a nested workspace/project root fixture so the monorepo behavior is covered and the config.root behavior is asserted. Keep the test colocated with the current loadIconsData coverage so the new cwd scenario protects the same path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@docs/content/docs/1.getting-started/6.integrations/1.icons/2.vue.md`:
- Around line 102-104: The note currently only points users to the
{collection}:{name} format, but the nearby vite.config.ts example should also
explicitly clarify that the i- prefix syntax (for example,
i-material-symbols-menu) is not supported by clientBundle.icons. Update the
documentation around the icons example and the note so it explains that
collections with dashes must use the explicit {collection}:{name} form, and add
a brief mention that users should install the matching
`@iconify-json/`{collection_name} package for each referenced collection.
In `@test/utils/icon-bundle.spec.ts`:
- Around line 8-9: Add a focused fixture test in icon-bundle.spec.ts that
exercises loadIconsData with a non-root cwd instead of only process.cwd(). Use
the existing loadIconsData test cases as a guide, but pass a nested
workspace/project root fixture so the monorepo behavior is covered and the
config.root behavior is asserted. Keep the test colocated with the current
loadIconsData coverage so the new cwd scenario protects the same path.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: db2805e5-1041-4547-82cb-97c265c8cb47
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (12)
docs/content/docs/1.getting-started/6.integrations/1.icons/2.vue.mddocs/content/docs/1.getting-started/6.integrations/6.ssr.mdpackage.jsonplaygrounds/vue/package.jsonsrc/plugins/icons.tssrc/plugins/plugins.tssrc/runtime/types/icons.d.tssrc/runtime/vue/plugins/icons.tssrc/unplugin.tssrc/utils/icons.tstest/utils/icon-bundle.spec.tstest/utils/icons.spec.ts
Scans the project source for icon usages from installed collections and bundles them, mirroring `@nuxt/icon`'s `clientBundle.scan`, so icons used in your own components render offline too. Opt-in, off by default.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/plugins/icons.ts`:
- Around line 115-118: Escape each collection name before building the
alternation in createMatchRegex so filesystem-derived names cannot inject regex
metacharacters; update the regex construction to use escaped values while
keeping the existing sort and matching behavior intact.
- Around line 141-146: The icon scan in extractUsedIcons currently reads every
matched file at once via Promise.all(files.map(...)), which can exhaust file
descriptors in large workspaces. Update the file-processing loop in
src/plugins/icons.ts to read files sequentially or with a small concurrency
limit, while preserving the existing readFile, extractUsedIcons, and names.add
behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 7aeb6603-3319-450e-90bf-a4bb096a9546
📒 Files selected for processing (3)
docs/content/docs/1.getting-started/6.integrations/1.icons/2.vue.mdsrc/plugins/icons.tssrc/unplugin.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- src/unplugin.ts
- docs/content/docs/1.getting-started/6.integrations/1.icons/2.vue.md
…options Type `icon.clientBundle` as `@nuxt/icon`'s `ClientBundleOptions` (minus `includeCustomCollections`, which has no Vue equivalent) instead of a hand-rolled subset, and implement the previously-missing options: `sizeLimitKb` (build-size guard, default 256), and `scan.ignoreCollections` / `scan.additionalCollections`.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Instead of a hand-rolled scanner/loader, use the canonical primitives from @nuxt/icon 2.3.1 (nuxt/icon#506): `resolveBundleIcons` (with Nuxt UI's defaults on the soft `extraIcons` tier so a missing collection degrades to runtime instead of failing the build), `IconUsageScanner` for `scan`, and `generateClientBundleCode` (which emits `init(addIcon)` and enforces `sizeLimitKb`). The runtime plugin registers via `init(addIcon)` on Nuxt UI's own `@iconify/vue`.
- Parse user `clientBundle.icons` against @nuxt/icon's known collection list so multi-word dash forms (e.g. `i-material-symbols-menu`) resolve instead of mis-splitting and hard-failing the build. - Read scanned files in bounded batches to avoid EMFILE on large workspaces (@nuxt/icon's scanFiles reads all at once). - Don't cache a rejected generation promise, so a fix is picked up without a dev-server restart. - Drop the now-unused `@iconify/utils` dependency and root `@iconify-json/lucide` devDependency (leftovers from the pre-delegation implementation and removed SSR test).
Match `@nuxt/icon`'s vite plugin: an unresolved `clientBundle.icons` entry throws on build but only warns in dev, so a typo doesn't crash the dev server (it falls back to runtime there). This removes the reason for the rejected-promise cache workaround, so `load()` goes back to a plain memoized `generate()`.
Resolves #5242 and supersedes #5894.
Context
On the Vue/Vite side, the icons Nuxt UI uses were never bundled, so
@iconify/vuefetched them from the Iconify API at runtime. That broke offline use, caused the SSR "icon appears after hydration" flash, and added network round-trips. This is the Vue equivalent of what #6633 wires up for Nuxt through@nuxt/icon's client bundle.Unlike Nuxt, pure Vue has no server to serve icons from, so embedding them in the client build is the only way to render them offline and during SSR.
What this does
The
@nuxt/ui/vitesub-plugin (src/plugins/icons.ts) delegates the bundling to@nuxt/icon's framework-agnostic primitives (@nuxt/icon/utils, from nuxt/icon#506) rather than reimplementing it, so this bumps the@nuxt/icondependency to^2.3.1. It uses the three-tierresolveBundleIconsmodel:extraIconstier, so a missing collection degrades to runtime loading instead of failing the build (Nuxt UI hard-depends on no collection).clientBundle.iconsgo through theiconstier (an unresolved one hard-fails the build, as in@nuxt/icon), andscanreuses@nuxt/icon'sIconUsageScanner.config.root(the fix: resolve client bundle collections from rootDir/workspaceDir icon#502 lesson), so workspace/monorepo builds work.generateClientBundleCodeproduces avirtual:nuxt-ui-iconsmodule exportinginit(addIcon); the runtime pluginruntime/vue/plugins/iconscalls it with@iconify/vue'saddIconon both server and client, so icons render synchronously during SSR and fully offline while keeping thei-lucide-*syntax. Bundling runs in dev too, not only invite build.Per
@nuxt/icon's guidance for library authors, this is the intended integration path (the standaloneNuxtIconBundleplugin is for end-user apps and can't softly inject a library's own defaults).Options
Mirrors
@nuxt/icon'sclientBundle(icons,scan,sizeLimitKb). Bundling is on by default for Nuxt UI's own icons; you extend, scan, or disable it throughicon.clientBundle:iconsacceptsi-{collection}-{name}or{collection}:{name}(use the colon form for multi-word collections likematerial-symbols:menu).scanis opt-in (off by default). It scans your source (.vue/.jsx/.tsx/.md/...) for icon usages and bundles the ones from installed collections, so icons used in your own components are bundled too.Following the same decision as #6633, this does not add
@iconify-json/lucideas a hard dependency. Icons are bundled only when their collection is installed; anything else falls back to runtime loading.How to test
@iconify-json/lucide.api.iconify.design.icon: { clientBundle: { scan: true } }and the icons used in your own components get bundled too.Notes
@nuxt/icon's new standalone bundling primitives (feat: standalone Vite plugin and reusable utils for client icon bundling icon#506, released in 2.3.1).vite-plugin-iconify-bundleinspired the original approach.