electron.vite.config.ts
Rama main · réplica de lectura del repositorio original.
import { readdirSync, readFileSync } from 'fs'
import { createRequire } from 'node:module'
import { join, resolve } from 'path'
import { sentryVitePlugin } from '@sentry/vite-plugin'
import tailwindcss from '@tailwindcss/vite'
import { tanstackRouter } from '@tanstack/router-plugin/vite'
import react from '@vitejs/plugin-react'
import { CodeInspectorPlugin } from 'code-inspector-plugin'
import { defineConfig } from 'electron-vite'
import { visualizer } from 'rollup-plugin-visualizer'
import type { Plugin } from 'vite'
import { parse } from 'yaml'
// Import attributes are not supported by the current Electron config loader.
// import pkg from './package.json' assert { type: 'json' }
import pkg from './package.json'
import { buildFlatContractCss } from './packages/ui/scripts/build-theme-css'
import { chunkExportGuardPlugin } from './scripts/checkChunkExports'
import { claudeCodeBinaryManifest } from './scripts/claudeCodeBinaryManifest'
import { uiContractPlugin } from './scripts/uiContract/vitePlugin'
import { APP_EDITIONS, type AppEdition } from './src/shared/types/appEdition'
import { parseReleaseHistory, validateCurrentReleaseHistory } from './src/shared/utils/releaseNotes'
import { getSentryBuildContext } from './src/shared/utils/sentry'
const { buildSelectionPanel, sourcePath: selectionPanelSource } = createRequire(import.meta.url)(
'./native/selection-panel/build'
)
const { buildExternalApp, sourceFiles: externalAppSources } = createRequire(import.meta.url)(
'./native/external-app/build.cjs'
)
type ElectronBuilderConfig = {
releaseInfo?: {
releaseNotes?: unknown
}
}
const electronBuilderConfig = parse(
readFileSync(resolve(__dirname, 'electron-builder.yml'), 'utf8')
) as ElectronBuilderConfig
const bundledReleaseNotes = electronBuilderConfig.releaseInfo?.releaseNotes
const bundledReleaseHistory = parseReleaseHistory(
readFileSync(resolve(__dirname, 'resources/cherry-studio/release-history.json'), 'utf8')
)
if (typeof bundledReleaseNotes !== 'string' || !bundledReleaseNotes.trim()) {
throw new Error('electron-builder.yml must define non-empty releaseInfo.releaseNotes')
}
validateCurrentReleaseHistory({ releaseNotes: bundledReleaseNotes, version: pkg.version }, bundledReleaseHistory)
const visualizerPlugin = (type: 'renderer' | 'main') => {
return process.env[`VISUALIZER_${type.toUpperCase()}`] ? [visualizer({ open: true })] : []
}
const isDev = process.env.NODE_ENV === 'development'
const isProd = process.env.NODE_ENV === 'production'
const SENTRY_UPLOAD_ENV_KEYS = ['SENTRY_AUTH_TOKEN', 'SENTRY_ORG', 'SENTRY_PROJECT'] as const
export function resolveSentryBuildSettings(env: NodeJS.ProcessEnv) {
const sourceMapUploadEnabled = env.NODE_ENV === 'production' && env.SENTRY_SOURCE_MAP_UPLOAD === 'true'
const missingUploadEnv = sourceMapUploadEnabled ? SENTRY_UPLOAD_ENV_KEYS.filter((key) => !env[key]?.trim()) : []
if (missingUploadEnv.length > 0) {
throw new Error(`Sentry production builds require: ${missingUploadEnv.join(', ')}`)
}
return { sourceMapUploadEnabled }
}
export function resolveRendererEdition(value: string | undefined): AppEdition {
const edition = value?.trim().toLowerCase() || 'global'
if (APP_EDITIONS.includes(edition as AppEdition)) return edition as AppEdition
throw new Error(`Unsupported renderer edition: ${edition}`)
}
const rendererEdition = resolveRendererEdition(process.env.CHERRY_EDITION)
const sentryBuildContext = getSentryBuildContext(pkg.name, pkg.version, rendererEdition)
const { sourceMapUploadEnabled } = resolveSentryBuildSettings(process.env)
const sentrySourceMap = sourceMapUploadEnabled ? ('hidden' as const) : isDev
const sentrySourceMapPlugins = (outputDirectory: 'main' | 'preload' | 'renderer') =>
sourceMapUploadEnabled
? sentryVitePlugin({
authToken: process.env.SENTRY_AUTH_TOKEN,
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
telemetry: false,
release: {
name: sentryBuildContext.release,
create: false,
finalize: false,
setCommits: false
},
sourcemaps: {
filesToDeleteAfterUpload: `./out/${outputDirectory}/**/*.map`
}
})
: []
// `dependencies` stay external and ship whole; everything else is bundled. Only R1–R4 packages
// belong there: see "Main-Process Dependencies" in CLAUDE.md.
const mainExternalDependencies = [
...Object.keys(pkg.dependencies),
// optionalDependencies too: platform-gated natives (e.g. node-mac-permissions) are real import
// targets, not napi sub-packages, so rollup would fail on the .node; production keeps them installed.
...Object.keys(pkg.optionalDependencies ?? {})
]
export const mainExternalModules = ['bufferutil', 'utf-8-validate', 'electron', ...mainExternalDependencies]
export const isMainExternalModule = (id: string) => {
return mainExternalModules.some((moduleId) => id === moduleId || id.startsWith(`${moduleId}/`))
}
// Ships the flat theme contract next to the main bundle so mini apps can be served
// `assets/miniAppTheme.css`. Built in-process: nothing in the app pipeline runs the ui package build.
const miniAppThemeAssetPlugin = (): Plugin => ({
name: 'cherry-mini-app-theme-asset',
// The sources live outside the main bundle's module graph, so `electron-vite dev` would
// serve a stale `miniAppTheme.css` after a token edit. Registered in `buildStart` because
// `addWatchFile` is a build-phase call — it is not available from `generateBundle`.
buildStart() {
const stylesDir = resolve('packages/ui/src/styles')
for (const name of readdirSync(stylesDir, { recursive: true, encoding: 'utf8' })) {
if (name.endsWith('.css')) this.addWatchFile(join(stylesDir, name))
}
},
async generateBundle() {
let source: string
try {
source = await buildFlatContractCss()
} catch (error) {
return this.error(`failed to build assets/miniAppTheme.css: ${error instanceof Error ? error.message : error}`)
}
this.emitFile({ type: 'asset', fileName: 'assets/miniAppTheme.css', source })
}
})
/** Shared with the utility-process entries build, which must resolve identically. */
export const mainResolveAlias = {
'@main': resolve('src/main'),
'@application': resolve('src/main/core/application/Application'),
'@data': resolve('src/main/data'),
'@shared': resolve('src/shared'),
'@logger': resolve('src/main/core/logger/LoggerService'),
'@cherrystudio/ai-core/provider': resolve('packages/aiCore/src/core/providers'),
'@cherrystudio/ai-core/built-in/plugins': resolve('packages/aiCore/src/core/plugins/built-in'),
'@cherrystudio/ai-core': resolve('packages/aiCore/src'),
'@cherrystudio/ai-sdk-provider': resolve('packages/ai-sdk-provider/src'),
'@cherrystudio/provider-registry/node': resolve('packages/provider-registry/src/registry-loader'),
'@cherrystudio/provider-registry': resolve('packages/provider-registry/src'),
'@cherrystudio/web-search/catalog': resolve('packages/web-search/src/catalog.ts'),
'@cherrystudio/web-search/providers/zhipu': resolve('packages/web-search/src/providers/zhipu.ts'),
'@cherrystudio/web-search/providers/tavily': resolve('packages/web-search/src/providers/tavily.ts'),
'@cherrystudio/web-search/providers/searxng': resolve('packages/web-search/src/providers/searxng.ts'),
'@cherrystudio/web-search/providers/exa': resolve('packages/web-search/src/providers/exa.ts'),
'@cherrystudio/web-search/providers/exa-mcp': resolve('packages/web-search/src/providers/exaMcp.ts'),
'@cherrystudio/web-search/providers/bocha': resolve('packages/web-search/src/providers/bocha.ts'),
'@cherrystudio/web-search/providers/querit': resolve('packages/web-search/src/providers/querit.ts'),
'@cherrystudio/web-search/providers/jina': resolve('packages/web-search/src/providers/jina.ts'),
'@cherrystudio/web-search/providers/firecrawl': resolve('packages/web-search/src/providers/firecrawl.ts'),
'@cherrystudio/web-search/providers/crawl4ai': resolve('packages/web-search/src/providers/crawl4ai.ts'),
'@cherrystudio/web-search/providers/parallel': resolve('packages/web-search/src/providers/parallel.ts'),
'@cherrystudio/web-search/providers/serply': resolve('packages/web-search/src/providers/serply.ts'),
'@cherrystudio/web-search/providers/yuanbao': resolve('packages/web-search/src/providers/yuanbao.ts'),
'@cherrystudio/web-search/providers/deepseek': resolve('packages/web-search/src/providers/deepseek.ts'),
'@cherrystudio/web-search/providers/desearch': resolve('packages/web-search/src/providers/desearch.ts'),
'@cherrystudio/web-search/providers/searchapi': resolve('packages/web-search/src/providers/searchapi.ts'),
'@cherrystudio/web-search/tools/config': resolve('packages/web-search/src/tools/config.ts'),
'@cherrystudio/web-search/tools/url-filter': resolve('packages/web-search/src/tools/urlFilter.ts'),
'@cherrystudio/web-search/tools/content': resolve('packages/web-search/src/tools/content.ts'),
'@cherrystudio/web-search': resolve('packages/web-search/src/index.ts'),
'@test-mocks': resolve('tests/__mocks__'),
'@test-helpers': resolve('tests/helpers')
}
export default defineConfig({
main: {
define: {
__APP_EDITION__: JSON.stringify(rendererEdition),
__CLAUDE_CODE_BINARY_MANIFEST__: JSON.stringify(claudeCodeBinaryManifest(__dirname))
},
plugins: [
{
name: 'cherry-selection-panel',
buildStart() {
this.addWatchFile(selectionPanelSource)
buildSelectionPanel()
}
},
{
name: 'cherry-external-app',
apply: 'build',
buildStart() {
for (const file of externalAppSources) this.addWatchFile(file)
buildExternalApp()
}
},
chunkExportGuardPlugin(),
miniAppThemeAssetPlugin(),
...visualizerPlugin('main'),
...sentrySourceMapPlugins('main')
],
resolve: {
alias: [
...Object.entries(mainResolveAlias).map(([find, replacement]) => ({ find, replacement })),
// file-stream-rotator requires Moment's callable CommonJS export, not its ESM namespace.
{ find: /^moment$/, replacement: 'moment/moment.js' }
]
},
build: {
externalizeDeps: {
include: mainExternalModules
},
lib: { entry: resolve(__dirname, 'src/main/main.ts') },
rolldownOptions: {
output: {
manualChunks: (id) => {
// conf removes its containing file from require.cache; isolate it so the app entry stays cached.
if (id.includes('/node_modules/conf/')) return 'electron-store-conf'
// rolldown drops this chunk's named exports when it merges with a re-export-only
// facade chunk, leaving createOpenAI undefined at runtime. Keep it alone.
if (id.includes('/node_modules/@ai-sdk/openai/')) return 'ai-sdk-openai'
return undefined
},
comments: isProd ? { legal: false } : undefined
},
onwarn(warning, warn) {
if (warning.code === 'COMMONJS_VARIABLE_IN_ESM') return
warn(warning)
}
},
sourcemap: sentrySourceMap
},
optimizeDeps: {
noDiscovery: isDev
}
},
preload: {
plugins: [...sentrySourceMapPlugins('preload')],
resolve: {
alias: {
'@shared': resolve('src/shared'),
'@cherrystudio/web-search/catalog': resolve('packages/web-search/src/catalog.ts'),
'@cherrystudio/web-search': resolve('packages/web-search/src/index.ts')
}
},
build: {
externalizeDeps: false,
isolatedEntries: true,
sourcemap: sentrySourceMap,
rolldownOptions: {
// Unlike renderer which auto-discovers entries from HTML files,
// preload requires explicit entry point configuration for multiple scripts
input: {
preload: resolve(__dirname, 'src/preload/preload.ts'),
simplest: resolve(__dirname, 'src/preload/simplest.ts'), // Minimal preload
webview: resolve(__dirname, 'src/preload/webview.ts'), // Site `<webview>` guests
miniAppBridge: resolve(__dirname, 'src/preload/miniAppBridge.ts') // Local mini app guests (`window.cherry`)
},
external: ['electron'],
output: {
entryFileNames: '[name].js',
format: 'cjs'
}
}
}
},
renderer: {
define: {
__APP_EDITION__: JSON.stringify(rendererEdition),
__APP_RELEASE_HISTORY__: JSON.stringify(bundledReleaseHistory),
__APP_RELEASE_NOTES__: JSON.stringify(bundledReleaseNotes),
__APP_RELEASE_VERSION__: JSON.stringify(pkg.version)
},
plugins: [
uiContractPlugin(),
tanstackRouter({
target: 'react',
autoCodeSplitting: true,
routesDirectory: resolve('src/renderer/routes'),
generatedRouteTree: resolve('src/renderer/routeTree.gen.ts')
}),
tailwindcss(),
react(),
...(isDev ? [CodeInspectorPlugin({ bundler: 'vite' })] : []), // 只在开发环境下启用 CodeInspectorPlugin
...visualizerPlugin('renderer'),
...sentrySourceMapPlugins('renderer')
],
resolve: {
alias: {
'@renderer': resolve('src/renderer'),
'@shared': resolve('src/shared'),
'@logger': resolve('src/renderer/services/LoggerService'),
'@data': resolve('src/renderer/data'),
'@cherrystudio/ai-core/provider': resolve('packages/aiCore/src/core/providers'),
'@cherrystudio/ai-core/built-in/plugins': resolve('packages/aiCore/src/core/plugins/built-in'),
'@cherrystudio/ai-core': resolve('packages/aiCore/src'),
'@cherrystudio/extension-table-plus': resolve('packages/extension-table-plus/src'),
'@cherrystudio/ai-sdk-provider': resolve('packages/ai-sdk-provider/src'),
'@cherrystudio/provider-registry/node': resolve('packages/provider-registry/src/registry-loader'),
'@cherrystudio/provider-registry': resolve('packages/provider-registry/src'),
'@cherrystudio/web-search/catalog': resolve('packages/web-search/src/catalog.ts'),
'@cherrystudio/web-search/providers/zhipu': resolve('packages/web-search/src/providers/zhipu.ts'),
'@cherrystudio/web-search/providers/tavily': resolve('packages/web-search/src/providers/tavily.ts'),
'@cherrystudio/web-search/providers/searxng': resolve('packages/web-search/src/providers/searxng.ts'),
'@cherrystudio/web-search/providers/exa': resolve('packages/web-search/src/providers/exa.ts'),
'@cherrystudio/web-search/providers/exa-mcp': resolve('packages/web-search/src/providers/exaMcp.ts'),
'@cherrystudio/web-search/providers/bocha': resolve('packages/web-search/src/providers/bocha.ts'),
'@cherrystudio/web-search/providers/querit': resolve('packages/web-search/src/providers/querit.ts'),
'@cherrystudio/web-search/providers/jina': resolve('packages/web-search/src/providers/jina.ts'),
'@cherrystudio/web-search/providers/firecrawl': resolve('packages/web-search/src/providers/firecrawl.ts'),
'@cherrystudio/web-search/providers/crawl4ai': resolve('packages/web-search/src/providers/crawl4ai.ts'),
'@cherrystudio/web-search/providers/parallel': resolve('packages/web-search/src/providers/parallel.ts'),
'@cherrystudio/web-search/providers/serply': resolve('packages/web-search/src/providers/serply.ts'),
'@cherrystudio/web-search/providers/deepseek': resolve('packages/web-search/src/providers/deepseek.ts'),
'@cherrystudio/web-search/providers/desearch': resolve('packages/web-search/src/providers/desearch.ts'),
'@cherrystudio/web-search/providers/searchapi': resolve('packages/web-search/src/providers/searchapi.ts'),
'@cherrystudio/web-search/tools/config': resolve('packages/web-search/src/tools/config.ts'),
'@cherrystudio/web-search/tools/url-filter': resolve('packages/web-search/src/tools/urlFilter.ts'),
'@cherrystudio/web-search/tools/content': resolve('packages/web-search/src/tools/content.ts'),
'@cherrystudio/web-search': resolve('packages/web-search/src/index.ts'),
'@cherrystudio/file-preview/core': resolve('packages/file-preview/src/core.ts'),
'@cherrystudio/file-preview/react': resolve('packages/file-preview/src/react.ts'),
'@cherrystudio/ui/icons/providers': resolve('packages/ui/src/components/icons/providers'),
'@cherrystudio/ui/icons': resolve('packages/ui/src/components/icons'),
'@cherrystudio/ui': resolve('packages/ui/src'),
'@test-mocks': resolve('tests/__mocks__')
}
},
optimizeDeps: {
exclude: ['pyodide'],
include: ['pdfjs-dist/build/pdf.worker.mjs'],
rolldownOptions: {
transform: {
target: 'esnext' // for dev
}
}
},
worker: {
format: 'es'
},
build: {
sourcemap: sentrySourceMap,
target: 'esnext', // for build
rolldownOptions: {
input: {
index: resolve(__dirname, 'src/renderer/windows/main/index.html'),
quickAssistant: resolve(__dirname, 'src/renderer/windows/quickAssistant/index.html'),
selectionToolbar: resolve(__dirname, 'src/renderer/windows/selection/toolbar/index.html'),
selectionAction: resolve(__dirname, 'src/renderer/windows/selection/action/index.html'),
selectionAgent: resolve(__dirname, 'src/renderer/windows/selection/agent/index.html'),
selectionCapsule: resolve(__dirname, 'src/renderer/windows/selection/capsule/index.html'),
migrationV2: resolve(__dirname, 'src/renderer/windows/migrationV2/index.html'),
userDataRelocation: resolve(__dirname, 'src/renderer/windows/userDataRelocation/index.html'),
subWindow: resolve(__dirname, 'src/renderer/windows/subWindow/index.html'),
screenshot: resolve(__dirname, 'src/renderer/windows/screenshot/index.html')
},
onwarn(warning, warn) {
if (warning.code === 'COMMONJS_VARIABLE_IN_ESM') return
warn(warning)
},
output: {
comments: isProd ? { legal: false } : undefined,
advancedChunks: {
// Without this, groups recursively capture dependencies — React
// itself ends up inside an icon bucket and every window preloads it.
includeDependenciesRecursively: false,
groups: [
// Bucket per-icon lazy modules into mid-size chunks instead of one
// tiny chunk per icon. Model icons only: they are reached solely
// through the dynamic loaders, so the buckets stay off every
// window's eager graph. Provider icons must NOT be grouped — a few
// files statically import specific providers from
// @cherrystudio/ui/icons/providers, and bucketing would chain
// whole buckets of unrelated SVGs into those windows' first load.
// Only the SVG component files (*.tsx) may match: each icon dir's
// meta.ts is eagerly imported by the meta-catalogs.
{
name: 'icons-models',
test: /packages\/ui\/src\/components\/icons\/models\/[^/]+\/(?:index|light|dark|avatar)\.tsx$/,
maxSize: 150_000
}
]
}
}
}
}
}
})