Ctrl K
ComunidadGuías…

electron.vite.config.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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
              }
            ]
          }
        }
      }
    }
  }
})