From 3fa11ea1b84ee9bbdc278147daa89ecc5c0fdd4a Mon Sep 17 00:00:00 2001 From: Bao Nguyen Date: Fri, 4 Sep 2026 21:15:26 +0700 Subject: [PATCH] fix: don't append custom collection prefixes to the shared collection list `getRuntimeCollections` returns the module-level `collectionNames` array itself when `fallbackToApi` is enabled or `serverBundle` is a string, and then pushes every custom collection prefix onto it. That mutates a generated constant shared by `IconUsageScanner`, `discoverInstalledCollections` and the `@nuxt/icon/utils` public export for the rest of the process, so a second app built in the same process inherits the first one's prefixes. It also leaks back into the same build: with `serverBundle: 'remote'` the polluted list reaches `_resolveServerBundle`, and the generated server bundle gains a jsdelivr entry for a collection that has no `@iconify-json` package. The object branch already built a fresh array with `.map()`; copy in the two string branches so appending is always local. --- src/context.ts | 6 ++-- test/runtime-collections.test.ts | 51 ++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 test/runtime-collections.test.ts diff --git a/src/context.ts b/src/context.ts index bdac8988..c86c3ef1 100644 --- a/src/context.ts +++ b/src/context.ts @@ -26,10 +26,12 @@ export class NuxtIconModuleContext { public scanner: IconUsageScanner | undefined getRuntimeCollections(runtimeOptions: NuxtIconRuntimeOptions): string[] { + // Copy: the custom prefixes below are appended in place, and `collectionNames` + // is a module-level constant shared with every other consumer in the process const resolved = runtimeOptions.fallbackToApi - ? collectionNames + ? [...collectionNames] : typeof this.options.serverBundle === 'string' - ? collectionNames + ? [...collectionNames] : this.options.serverBundle ? this.options.serverBundle.collections ?.map(c => typeof c === 'string' ? c : c.prefix) || [] diff --git a/test/runtime-collections.test.ts b/test/runtime-collections.test.ts new file mode 100644 index 00000000..4bd9750f --- /dev/null +++ b/test/runtime-collections.test.ts @@ -0,0 +1,51 @@ +import { expect, it } from 'vitest' +import type { Nuxt } from '@nuxt/schema' +import type { IconifyJSON } from '@iconify/types' +import { NuxtIconModuleContext } from '../src/context' +import type { ModuleOptions, NuxtIconRuntimeOptions } from '../src/types' +import { collectionNames } from '../src/collection-names' + +function createContext(options: Partial) { + const nuxt = { + options: { + rootDir: '/root', + nitro: {}, + dev: false, + }, + } + return new NuxtIconModuleContext(nuxt as unknown as Nuxt, options as ModuleOptions) +} + +// `module.ts` only reads `fallbackToApi` out of the runtime options here +const runtimeOptions = { fallbackToApi: true } as NuxtIconRuntimeOptions + +const customCollection: IconifyJSON = { + prefix: 'my-icons', + icons: { + logo: { body: '' }, + }, +} + +it('keeps custom collection prefixes out of the shared Iconify collection list', () => { + const ctx = createContext({ customCollections: [customCollection] }) + expect(ctx.getRuntimeCollections(runtimeOptions)).toContain('my-icons') + + // Another app built in the same process must not inherit the first one's prefixes + const other = createContext({}) + expect(other.getRuntimeCollections(runtimeOptions)).not.toContain('my-icons') + expect(collectionNames).not.toContain('my-icons') +}) + +it('does not add a remote endpoint for a custom collection', async () => { + const ctx = createContext({ + provider: 'server', + serverBundle: 'remote', + customCollections: [customCollection], + }) + // `module.ts` fills `appConfig.icon.collections` from this before the server bundle resolves + ctx.getRuntimeCollections(runtimeOptions) + + const { collections } = await ctx.resolveServerBundle() + const entries = collections.filter(c => (typeof c === 'string' ? c : c.prefix) === 'my-icons') + expect(entries).toEqual([customCollection]) +})