From 8e9e098391dcbabf4744597abb4af5f177da6694 Mon Sep 17 00:00:00 2001 From: KokerZhou Date: Mon, 20 Jul 2026 15:46:49 +0800 Subject: [PATCH] fix: bundle frontend icons locally --- apps/desktop/electron.vite.config.ts | 8 ++------ apps/desktop/tsconfig.node.json | 1 + apps/web-wasm/src/main.ts | 3 +++ package.json | 5 +++++ pnpm-lock.yaml | 29 ++++++++++++++++++++++++++++ src/env.d.ts | 1 + src/icons/disable-iconify-api.ts | 5 +++++ src/main.ts | 3 +++ vite.icon-bundle.config.ts | 17 ++++++++++++++++ vite.web-wasm.config.mts | 2 ++ vite.web.config.mts | 4 ++-- 11 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 src/icons/disable-iconify-api.ts create mode 100644 vite.icon-bundle.config.ts diff --git a/apps/desktop/electron.vite.config.ts b/apps/desktop/electron.vite.config.ts index 23e7e31c..00d4ccd0 100644 --- a/apps/desktop/electron.vite.config.ts +++ b/apps/desktop/electron.vite.config.ts @@ -3,6 +3,7 @@ import { readFileSync } from 'fs' import { defineConfig } from 'electron-vite' import vue from '@vitejs/plugin-vue' import ui from '@nuxt/ui/vite' +import { chatlabIconBundle } from '../../vite.icon-bundle.config' const rootDir = resolve(__dirname, '../..') const rootPkg = JSON.parse(readFileSync(resolve(rootDir, 'package.json'), 'utf-8')) @@ -107,6 +108,7 @@ export default defineConfig({ }, }, }), + chatlabIconBundle(rootDir), ], root: resolve(rootDir, 'src/'), build: { @@ -126,12 +128,6 @@ export default defineConfig({ if (id.includes('node_modules/echarts')) { return 'vendor-echarts' } - if (id.includes('node_modules/@nuxt/ui')) { - return 'vendor-nuxt-ui' - } - if (id.includes('node_modules/reka-ui')) { - return 'vendor-reka-ui' - } if (id.includes('node_modules/@zumer/snapdom')) { return 'vendor-snapdom' } diff --git a/apps/desktop/tsconfig.node.json b/apps/desktop/tsconfig.node.json index 8cdb5069..b12e0815 100644 --- a/apps/desktop/tsconfig.node.json +++ b/apps/desktop/tsconfig.node.json @@ -2,6 +2,7 @@ "extends": "@electron-toolkit/tsconfig/tsconfig.node.json", "include": [ "electron.vite.config.*", + "../../vite.icon-bundle.config.ts", "main/**/*", "preload/**/*", "shared/**/*", diff --git a/apps/web-wasm/src/main.ts b/apps/web-wasm/src/main.ts index cbe65719..13f8c982 100644 --- a/apps/web-wasm/src/main.ts +++ b/apps/web-wasm/src/main.ts @@ -1,3 +1,6 @@ +import '@/icons/disable-iconify-api' +import 'virtual:nuxt-icon-bundle/register' + import { createApp } from 'vue' import { createPinia } from 'pinia' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' diff --git a/package.json b/package.json index 474c18d5..fce1e3fa 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,11 @@ "@openchatlab/tools": "workspace:*", "@openchatlab/web-runtime": "workspace:*", "@electron-toolkit/tsconfig": "^1.0.1", + "@iconify-json/heroicons": "^1.2.3", + "@iconify-json/lucide": "^1.2.118", + "@iconify-json/simple-icons": "1.2.83", + "@iconify/vue": "5.0.1", + "@nuxt/icon": "2.3.1", "@nuxt/ui": "^4.9.0", "@tailwindcss/vite": "^4.0.0", "@types/markdown-it": "^14.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b50d1abf..0fe1830b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,6 +39,21 @@ importers: '@electron-toolkit/tsconfig': specifier: ^1.0.1 version: 1.0.1(@types/node@25.2.2) + '@iconify-json/heroicons': + specifier: ^1.2.3 + version: 1.2.3 + '@iconify-json/lucide': + specifier: ^1.2.118 + version: 1.2.118 + '@iconify-json/simple-icons': + specifier: 1.2.83 + version: 1.2.83 + '@iconify/vue': + specifier: 5.0.1 + version: 5.0.1(vue@3.5.27(typescript@6.0.3)) + '@nuxt/icon': + specifier: 2.3.1 + version: 2.3.1(vite@7.3.3(@types/node@25.2.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.27(typescript@6.0.3)) '@nuxt/ui': specifier: ^4.9.0 version: 4.9.0(s6g7ddwkwqprfo6zxngcqjtvte) @@ -1441,6 +1456,12 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@iconify-json/heroicons@1.2.3': + resolution: {integrity: sha512-n+vmCEgTesRsOpp5AB5ILB6srsgsYK+bieoQBNlafvoEhjVXLq8nIGN4B0v/s4DUfa0dOrjwE/cKJgIKdJXOEg==} + + '@iconify-json/lucide@1.2.118': + resolution: {integrity: sha512-JBnK4YOq6K/lA0JP//27QxFxJ4120TjvfXAzGZZIGjCcXcRRRFxl1rcV7+IWdcVCe90KXdqVaAwLaLf6G3HELw==} + '@iconify-json/simple-icons@1.2.83': resolution: {integrity: sha512-6Pp9V++XisT9RKH7FB4RLPqUDzcmLtSma0ovOEIoEWGrXtHwBFsH7oN1z8vvCVCb95fb87QgR46/zRLyN9Y3kg==} @@ -8090,6 +8111,14 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@iconify-json/heroicons@1.2.3': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify-json/lucide@1.2.118': + dependencies: + '@iconify/types': 2.0.0 + '@iconify-json/simple-icons@1.2.83': dependencies: '@iconify/types': 2.0.0 diff --git a/src/env.d.ts b/src/env.d.ts index fce1d53f..63f50fe3 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -1,4 +1,5 @@ /// +/// declare const __IS_ELECTRON__: boolean | undefined declare const __IS_WEB_WASM__: boolean | undefined diff --git a/src/icons/disable-iconify-api.ts b/src/icons/disable-iconify-api.ts new file mode 100644 index 00000000..5944c86c --- /dev/null +++ b/src/icons/disable-iconify-api.ts @@ -0,0 +1,5 @@ +import { _api as iconifyAPI } from '@iconify/vue' + +// All icons used by the app are registered by virtual:nuxt-icon-bundle/register. +// Disable Iconify's fallback fetcher so an unbundled dynamic name cannot contact a CDN. +iconifyAPI.setFetch(undefined as unknown as typeof globalThis.fetch) diff --git a/src/main.ts b/src/main.ts index d572f65b..d159e601 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,3 +1,6 @@ +import '@/icons/disable-iconify-api' +import 'virtual:nuxt-icon-bundle/register' + import { mountChatLabApp } from '@/bootstrap/mount-app' void mountChatLabApp().catch((error) => { diff --git a/vite.icon-bundle.config.ts b/vite.icon-bundle.config.ts new file mode 100644 index 00000000..af67cd8c --- /dev/null +++ b/vite.icon-bundle.config.ts @@ -0,0 +1,17 @@ +import { NuxtIconBundle } from '@nuxt/icon/vite' +import type { PluginOption } from 'vite' + +/** Bundle every icon used by ChatLab and Nuxt UI so rendering never depends on an icon CDN. */ +export function chatlabIconBundle(rootDir: string): PluginOption { + return NuxtIconBundle({ + cwd: rootDir, + scan: { + globInclude: [ + 'src/**/*.{vue,ts,tsx}', + 'apps/web-wasm/src/**/*.{vue,ts,tsx}', + 'node_modules/@nuxt/ui/dist/**/*.{vue,js,mjs,ts}', + ], + globExclude: [], + }, + }) +} diff --git a/vite.web-wasm.config.mts b/vite.web-wasm.config.mts index 34625120..711798c1 100644 --- a/vite.web-wasm.config.mts +++ b/vite.web-wasm.config.mts @@ -3,6 +3,7 @@ import { resolve } from 'node:path' import vue from '@vitejs/plugin-vue' import ui from '@nuxt/ui/vite' import { defineConfig } from 'vite' +import { chatlabIconBundle } from './vite.icon-bundle.config' const rootDir = import.meta.dirname @@ -20,6 +21,7 @@ export default defineConfig({ }, }, }), + chatlabIconBundle(rootDir), ], resolve: { alias: { diff --git a/vite.web.config.mts b/vite.web.config.mts index 3a212c72..37a405d3 100644 --- a/vite.web.config.mts +++ b/vite.web.config.mts @@ -19,6 +19,7 @@ import vue from '@vitejs/plugin-vue' import ui from '@nuxt/ui/vite' import { DEFAULT_API_PORT } from './packages/config/src/schema' import { createChatlabStartCommand, terminateChatlabStartProcess } from './scripts/dev-server-command.mjs' +import { chatlabIconBundle } from './vite.icon-bundle.config' const BACKEND_PORT = DEFAULT_API_PORT @@ -167,6 +168,7 @@ export default defineConfig({ }, }, }), + chatlabIconBundle(__dirname), chatlabServePlugin(), ], build: { @@ -182,8 +184,6 @@ export default defineConfig({ if (id.includes('node_modules/echarts-wordcloud')) return 'vendor-echarts-wordcloud' if (id.includes('node_modules/zrender')) return 'vendor-zrender' if (id.includes('node_modules/echarts')) return 'vendor-echarts' - if (id.includes('node_modules/@nuxt/ui')) return 'vendor-nuxt-ui' - if (id.includes('node_modules/reka-ui')) return 'vendor-reka-ui' if (id.includes('node_modules/@zumer/snapdom')) return 'vendor-snapdom' return undefined },