Skip to content

icon.customCollections array duplicated when using Nuxt layers due to registerModule double-merging with defu #6098

@mirjalol-norkulov

Description

@mirjalol-norkulov

Environment

  • @nuxt/ui: 4.4.0
  • @nuxt/icon: 2.2.1
  • defu: 6.1.4
  • nuxt: 4.x
  • Using pnpm workspaces with Nuxt layers

Is this bug related to Nuxt or Vue?

Nuxt

Package

v4.x

Version

v4.4.0

Reproduction

Monorepo with two packages:

packages/shared/nuxt.config.ts (base layer):
import { createResolver } from '@nuxt/kit'
const { resolve } = createResolver(import.meta.url)

export default defineNuxtConfig({
modules: ['@nuxt/ui'],
icon: {
customCollections: [{
prefix: 'local',
dir: resolve('./app/assets/icons')
}]
}
})

packages/main/nuxt.config.ts:
export default defineNuxtConfig({
extends: ['@my/shared'],
modules: ['@nuxt/ui']
})

Put any SVG files in packages/shared/app/assets/icons/ and run the dev server from the main package. The icons fail to load at runtime.

Root cause

The issue is in the registerModule helper in @nuxt/ui:

// @nuxt/ui/dist/module.mjs
async function registerModule(name, key, options) {
if (!hasNuxtModule(name)) {
await installModule(name, defu(nuxt.options[key], options));
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
} else {
nuxt.options[key] = defu(nuxt.options[key], options);
}
}

await registerModule("@nuxt/icon", "icon", { cssLayer: "components" });

installModule is called with defu(nuxt.options.icon, { cssLayer: "components" }) as inline options. This object includes customCollections: [entry] from the merged layer config.

Then inside @nuxt/kit's defineNuxtModule, the module options are created via:

defu(inlineOptions, nuxt.options[configKey], defaults)

Since defu concatenates arrays when both sources have the same array key, customCollections from inlineOptions and nuxt.options.icon are merged:

[entry] + [entry] → [entry, entry]

This results in:

  • The server bundle having duplicate 'local' keys in the collections object

  • appConfig.icon.customCollections being ["local", "local"]

  • The client-side setCustomIconsLoader being called twice for the same prefix

  • The server icon endpoint returning cached error responses

    Suggested fix

    The registerModule helper should only pass its own options to installModule, not the full nuxt.options[key]:

    async function registerModule(name, key, options) {
    if (!hasNuxtModule(name)) {
    await installModule(name, options); // Let defineNuxtModule handle merging with nuxt.options[key]
    } else {
    nuxt.options[key] = defu(nuxt.options[key], options);
    }
    }

    Or alternatively, @nuxt/icon could deduplicate customCollections by prefix in its setup().

Workaround

Add clientBundle.includeCustomCollections: true to bypass the server endpoint:

icon: {
customCollections: [{
prefix: 'local',
dir: resolve('./app/assets/icons')
}],
clientBundle: {
includeCustomCollections: true
}
}

Description

When defining icon.customCollections in a Nuxt layer's nuxt.config.ts, the custom collections array gets duplicated at runtime, causing the client-side icon loader to fail with:

ERROR Failed to load custom icons Invalid data404

Additional context

No response

Logs

 

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingtriageAwaiting initial review and prioritizationv4#4488

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions