Skip to content

feat(unplugin): pre-bundle used icons into the Vue/Vite build#6635

Draft
benjamincanac wants to merge 13 commits into
v4from
feat/icon-bundle-vue
Draft

feat(unplugin): pre-bundle used icons into the Vue/Vite build#6635
benjamincanac wants to merge 13 commits into
v4from
feat/icon-bundle-vue

Conversation

@benjamincanac

@benjamincanac benjamincanac commented Jun 25, 2026

Copy link
Copy Markdown
Member

Resolves #5242 and supersedes #5894.

Context

On the Vue/Vite side, the icons Nuxt UI uses were never bundled, so @iconify/vue fetched 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/vite sub-plugin (src/plugins/icons.ts) loads the SVG data at build time with @iconify/utils and emits a data-only virtual:nuxt-ui-icons module. A runtime plugin (runtime/vue/plugins/icons) imports that data and registers it through @iconify/vue's addIcon, running on both server and client so icons render synchronously during SSR and fully offline, while keeping the i-lucide-* string syntax. It bundles in dev too, not only in vite build.

It reuses the src/utils/icons.ts helpers from #6633 and resolves collections from the Vite config.root rather than process.cwd() (the nuxt/icon#502 lesson), so workspace and monorepo builds work too.

Options

Mirrors @nuxt/icon's clientBundle. Bundling is on by default for Nuxt UI's own icons; you extend, scan, or disable it through icon.clientBundle:

import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    ui({
      icon: {
        clientBundle: {
          // bundle extra icons explicitly
          icons: ['lucide:heart', 'simple-icons:github'],
          // or scan your source and bundle the icons you actually use
          scan: true
          // set `clientBundle: false` to opt out entirely
        }
      }
    })
  ]
})
  • icons accepts i-{collection}-{name} or {collection}:{name} (use the colon form for multi-word collections like material-symbols:menu).
  • scan is 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/lucide as a hard dependency. Icons are bundled only when their collection is installed; anything else falls back to runtime loading.

How to test

  1. Install a collection in your Vue app, for example @iconify-json/lucide.
  2. Build or SSR-render a page using a Nuxt UI component with an icon: it appears in the output/server-rendered HTML with no request to api.iconify.design.
  3. Add icon: { clientBundle: { scan: true } } and the icons used in your own components get bundled too.

Notes

benjamincanac and others added 3 commits June 25, 2026 11:27
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>
@benjamincanac benjamincanac changed the title feat: pre-bundle used icons into the Vue/Vite build feat(unplugin): pre-bundle used icons into the Vue/Vite build Jun 25, 2026
Base automatically changed from feat/pre-bundle-icons to v4 June 30, 2026 12:39
# Conflicts:
#	package.json
#	pnpm-lock.yaml
#	src/module.ts
#	src/utils/icons.ts
#	test/utils/icons.spec.ts
@pkg-pr-new

pkg-pr-new Bot commented Jun 30, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/@nuxt/ui@6635

commit: 6e21061

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.
@benjamincanac benjamincanac marked this pull request as ready for review June 30, 2026 13:34
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 89d12040-cff3-4fb6-a606-79c76a23d877

📥 Commits

Reviewing files that changed from the base of the PR and between f089d77 and 6e21061.

📒 Files selected for processing (2)
  • src/plugins/icons.ts
  • src/unplugin.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/unplugin.ts
  • src/plugins/icons.ts

📝 Walkthrough

Walkthrough

Adds build-time icon bundling for Nuxt UI in Vue/Vite mode. IconsPlugin generates a virtual:nuxt-ui-icons module from installed @iconify-json/* packages, parseIconName normalizes icon identifiers, a runtime Vue plugin registers bundled icons with addIcon, and NuxtUIOptions.icon gains a clientBundle option for explicit icons, scanning, or disabling bundling. Dependencies and docs were updated for local icon datasets and SSR behavior.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: pre-bundling used icons into the Vue/Vite build.
Description check ✅ Passed The description is directly related to the changeset and accurately describes the new icon bundling behavior.
Docstring Coverage ✅ Passed Docstring coverage is 81.82% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/icon-bundle-vue

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
docs/content/docs/1.getting-started/6.integrations/1.icons/2.vue.md (1)

102-104: 📐 Maintainability & Code Quality | 🔵 Trivial

Verify the anchor target for this note.

The note advises {collection}:{name} form for dash-containing collections. Ensure the vite.config.ts example above this note also works with the i- prefix syntax (e.g., i-material-symbols-menu) since users may try that first. Consider adding a brief mention that clientBundle.icons requires the explicit {collection}:{name} form, not the i- 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 win

Add 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 main config.root claim 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8d46034 and cb856a6.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (12)
  • docs/content/docs/1.getting-started/6.integrations/1.icons/2.vue.md
  • docs/content/docs/1.getting-started/6.integrations/6.ssr.md
  • package.json
  • playgrounds/vue/package.json
  • src/plugins/icons.ts
  • src/plugins/plugins.ts
  • src/runtime/types/icons.d.ts
  • src/runtime/vue/plugins/icons.ts
  • src/unplugin.ts
  • src/utils/icons.ts
  • test/utils/icon-bundle.spec.ts
  • test/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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 78e5e67 and 406f40e.

📒 Files selected for processing (3)
  • docs/content/docs/1.getting-started/6.integrations/1.icons/2.vue.md
  • src/plugins/icons.ts
  • src/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

Comment thread src/plugins/icons.ts
Comment thread src/plugins/icons.ts Outdated
…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`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v4 #4488

Projects

None yet

Development

Successfully merging this pull request may close these issues.

How to bundle default Nuxt UI icons (proper offline icons) with Vite?

1 participant