diff --git a/nuxt.config.ts b/nuxt.config.ts index 508ab3c6..3705788e 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -1,11 +1,15 @@ import { existsSync } from 'node:fs' import { fileURLToPath } from 'node:url' import { unpublishedAgentPaths } from './layers/marketing/app/utils/agent-pages' +import pkg from './package.json' import { dependencyPluginCompat } from './scripts/lib/dependency-plugin-compat' import { withBuildAssetMissFallthrough } from './scripts/lib/static-asset-fallthrough' import { externalCheckin } from './shared/checkin-external' +import { iconifyCollections } from './shared/icon-collections' import { SENTRY_DSN, sentryRelease, sentryReportingEnabled } from './shared/sentry' +const iconCollections = iconifyCollections(pkg) + const hasSentryAuthToken = Boolean(process.env.SENTRY_AUTH_TOKEN) || existsSync('.env.sentry-build-plugin') @@ -608,6 +612,7 @@ export default defineNuxtConfig({ // inlines just those. The Iconify API fallback is off, so a visitor's browser // never calls api.iconify.design. See docs/ops/bundle-baseline-2026-07-23.md. icon: { + collections: iconCollections, serverBundle: false, fallbackToApi: false, clientBundle: { diff --git a/shared/icon-collections.ts b/shared/icon-collections.ts new file mode 100644 index 00000000..e3b9bb4b --- /dev/null +++ b/shared/icon-collections.ts @@ -0,0 +1,26 @@ +/** + * The collection names `icon.collections` in nuxt.config.ts must list. + * + * With `serverBundle: false` and no API fallback, @nuxt/icon registers no + * collection names, so it splits `i-vscode-icons-foo` at the first hyphen and + * looks up collection `vscode`. The icon never resolves: SSR ships a bare class + * and logs a warning. Naming every installed collection fixes the split. + * + * Only `dependencies` count. An `@iconify-json/*` package that slips into + * `devDependencies` silently drops its collection here, which is exactly the + * failure above, so every icon package this site renders with must stay a + * runtime dependency. + */ + +const ICONIFY_PREFIX = '@iconify-json/' + +export interface PackageManifest { + dependencies?: Record + devDependencies?: Record +} + +export function iconifyCollections(pkg: PackageManifest): string[] { + return Object.keys(pkg.dependencies ?? {}) + .filter(name => name.startsWith(ICONIFY_PREFIX)) + .map(name => name.slice(ICONIFY_PREFIX.length)) +} diff --git a/test/unit/icon-collections.test.ts b/test/unit/icon-collections.test.ts new file mode 100644 index 00000000..ce453f83 --- /dev/null +++ b/test/unit/icon-collections.test.ts @@ -0,0 +1,34 @@ +// @vitest-environment node +import { describe, expect, it } from 'vitest' +import { iconifyCollections } from '#shared/icon-collections' + +describe('iconifyCollections', () => { + it('names every installed collection in dependency order', () => { + expect(iconifyCollections({ + dependencies: { + '@iconify-json/vscode-icons': 'catalog:', + '@iconify-json/simple-icons': 'catalog:', + }, + })).toEqual(['vscode-icons', 'simple-icons']) + }) + + it('ignores dependencies that are not icon collections', () => { + expect(iconifyCollections({ + dependencies: { + 'nuxt': 'catalog:', + '@iconify-json/lucide': 'catalog:', + }, + })).toEqual(['lucide']) + }) + + it('excludes an icon package that only sits in devDependencies', () => { + expect(iconifyCollections({ + dependencies: { '@iconify-json/lucide': 'catalog:' }, + devDependencies: { '@iconify-json/vscode-icons': 'catalog:' }, + })).toEqual(['lucide']) + }) + + it('names nothing when no icon packages are installed', () => { + expect(iconifyCollections({ dependencies: { nuxt: 'catalog:' } })).toEqual([]) + }) +})