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 13 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:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 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 docstrings
🧪 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`.
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
A new
@nuxt/ui/vitesub-plugin (src/plugins/icons.ts) loads the SVG data at build time with@iconify/utilsand emits a data-onlyvirtual:nuxt-ui-iconsmodule. A runtime plugin (runtime/vue/plugins/icons) imports that data and registers it through@iconify/vue'saddIcon, running on both server and client so icons render synchronously during SSR and fully offline, while keeping thei-lucide-*string syntax. It bundles in dev too, not only invite build.It reuses the
src/utils/icons.tshelpers from #6633 and resolves collections from the Viteconfig.rootrather thanprocess.cwd()(the nuxt/icon#502 lesson), so workspace and monorepo builds work too.Options
Mirrors
@nuxt/icon'sclientBundle. 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, like@nuxt/icon). It scans.vue/.jsx/.tsx/.md/...for icon usages from your 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
vite-plugin-iconify-bundleinspired the approach.