initial commit

This commit is contained in:
farion1231
2025-08-04 22:16:26 +08:00
commit e0a9c1ab4c
20 changed files with 1127 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
node_modules/
dist/
release/
.DS_Store
*.log
.env
.env.local

45
README.md Normal file
View File

@@ -0,0 +1,45 @@
# Claude Code 供应商切换器
一个用于管理和切换 Claude Code 不同供应商配置的桌面应用。
## 功能特性
- 🔄 一键切换不同供应商Anthropic、OpenRouter 等)
- 🔍 实时监控供应商状态和响应时间
- ⚡ 支持添加自定义供应商
- 🎨 简洁美观的图形界面
- 🔒 安全存储 API 密钥
## 开发
```bash
# 安装依赖
npm install
# 开发模式
npm run dev
# 构建应用
npm run build
# 打包发布
npm run dist
```
## 使用说明
1. 点击"添加供应商"添加你的 API 配置
2. 系统会自动检测每个供应商的状态
3. 选择要使用的供应商,点击单选按钮切换
4. 配置会自动保存到 Claude Code 的配置文件中
## 技术栈
- Electron
- React
- TypeScript
- Vite
## License
MIT

52
package.json Normal file
View File

@@ -0,0 +1,52 @@
{
"name": "cc-switch",
"version": "1.0.0",
"description": "Claude Code 供应商切换工具",
"main": "dist/main.js",
"scripts": {
"dev": "concurrently \"npm run dev:main\" \"npm run dev:renderer\"",
"dev:main": "tsc -w -p tsconfig.main.json",
"dev:renderer": "vite",
"build": "npm run build:renderer && npm run build:main",
"build:main": "tsc -p tsconfig.main.json",
"build:renderer": "vite build",
"start": "electron .",
"dist": "electron-builder"
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"@types/node": "^20.0.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^4.2.0",
"concurrently": "^8.2.0",
"electron": "^28.0.0",
"electron-builder": "^24.0.0",
"typescript": "^5.3.0",
"vite": "^5.0.0"
},
"dependencies": {
"axios": "^1.6.0",
"electron-store": "^8.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"build": {
"appId": "com.ccswitch.app",
"productName": "CC Switch",
"directories": {
"output": "release"
},
"mac": {
"category": "public.app-category.developer-tools"
},
"win": {
"target": "nsis"
},
"linux": {
"target": "AppImage"
}
}
}

94
src/main/index.ts Normal file
View File

@@ -0,0 +1,94 @@
import { app, BrowserWindow, ipcMain } from 'electron'
import path from 'path'
import Store from 'electron-store'
import { Provider, AppConfig } from '../shared/types'
import { checkProviderStatus, switchProvider, getClaudeCodeConfig } from './services'
const store = new Store<AppConfig>()
let mainWindow: BrowserWindow | null = null
function createWindow() {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
},
titleBarStyle: 'hiddenInset',
autoHideMenuBar: true
})
if (app.isPackaged) {
mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'))
} else {
mainWindow.loadURL('http://localhost:3000')
mainWindow.webContents.openDevTools()
}
mainWindow.on('closed', () => {
mainWindow = null
})
}
app.whenReady().then(() => {
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
// IPC handlers
ipcMain.handle('getProviders', () => {
return store.get('providers', {})
})
ipcMain.handle('getCurrentProvider', () => {
return store.get('current', '')
})
ipcMain.handle('addProvider', (_, provider: Provider) => {
const providers = store.get('providers', {})
providers[provider.id] = provider
store.set('providers', providers)
return true
})
ipcMain.handle('deleteProvider', (_, id: string) => {
const providers = store.get('providers', {})
delete providers[id]
store.set('providers', providers)
return true
})
ipcMain.handle('checkStatus', async (_, provider: Provider) => {
return await checkProviderStatus(provider)
})
ipcMain.handle('switchProvider', async (_, providerId: string) => {
const providers = store.get('providers', {})
const provider = providers[providerId]
if (provider) {
const success = await switchProvider(provider)
if (success) {
store.set('current', providerId)
}
return success
}
return false
})
ipcMain.handle('getClaudeCodeConfigPath', () => {
return getClaudeCodeConfig().path
})

12
src/main/preload.ts Normal file
View File

@@ -0,0 +1,12 @@
import { contextBridge, ipcRenderer } from 'electron'
import { Provider } from '../shared/types'
contextBridge.exposeInMainWorld('electronAPI', {
getProviders: () => ipcRenderer.invoke('getProviders'),
getCurrentProvider: () => ipcRenderer.invoke('getCurrentProvider'),
addProvider: (provider: Provider) => ipcRenderer.invoke('addProvider', provider),
deleteProvider: (id: string) => ipcRenderer.invoke('deleteProvider', id),
checkStatus: (provider: Provider) => ipcRenderer.invoke('checkStatus', provider),
switchProvider: (providerId: string) => ipcRenderer.invoke('switchProvider', providerId),
getClaudeCodeConfigPath: () => ipcRenderer.invoke('getClaudeCodeConfigPath')
})

86
src/main/services.ts Normal file
View File

@@ -0,0 +1,86 @@
import axios from 'axios'
import fs from 'fs/promises'
import path from 'path'
import os from 'os'
import { Provider, ProviderStatus } from '../shared/types'
export async function checkProviderStatus(provider: Provider): Promise<ProviderStatus> {
const startTime = Date.now()
try {
// 简单的健康检查请求
const response = await axios.post(
`${provider.apiUrl}/v1/messages`,
{
model: provider.model || 'claude-3-opus-20240229',
messages: [{ role: 'user', content: 'Hi' }],
max_tokens: 1
},
{
headers: {
'x-api-key': provider.apiKey,
'anthropic-version': '2023-06-01',
'content-type': 'application/json'
},
timeout: 5000
}
)
const responseTime = Date.now() - startTime
return {
isOnline: true,
responseTime,
lastChecked: new Date()
}
} catch (error) {
return {
isOnline: false,
responseTime: -1,
lastChecked: new Date(),
error: error instanceof Error ? error.message : '未知错误'
}
}
}
export function getClaudeCodeConfig() {
// Claude Code 配置文件路径
const configDir = path.join(os.homedir(), '.claude')
const configPath = path.join(configDir, 'settings.json')
return { path: configPath, dir: configDir }
}
export async function switchProvider(provider: Provider): Promise<boolean> {
try {
const { path: configPath, dir: configDir } = getClaudeCodeConfig()
// 确保目录存在
await fs.mkdir(configDir, { recursive: true })
// 读取现有配置
let config: any = {}
try {
const content = await fs.readFile(configPath, 'utf-8')
config = JSON.parse(content)
} catch {
// 文件不存在或解析失败,使用空配置
}
// 更新配置
config.api = {
...config.api,
baseURL: provider.apiUrl,
apiKey: provider.apiKey,
model: provider.model
}
// 写回配置文件
await fs.writeFile(configPath, JSON.stringify(config, null, 2))
return true
} catch (error) {
console.error('切换供应商失败:', error)
return false
}
}

72
src/renderer/App.css Normal file
View File

@@ -0,0 +1,72 @@
.app {
height: 100vh;
display: flex;
flex-direction: column;
}
.app-header {
background: #2c3e50;
color: white;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.app-header h1 {
font-size: 1.5rem;
font-weight: 500;
}
.header-actions {
display: flex;
gap: 1rem;
}
.refresh-btn, .add-btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s;
}
.refresh-btn {
background: #3498db;
color: white;
}
.refresh-btn:hover:not(:disabled) {
background: #2980b9;
}
.refresh-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.add-btn {
background: #27ae60;
color: white;
}
.add-btn:hover {
background: #229954;
}
.app-main {
flex: 1;
padding: 2rem;
overflow-y: auto;
}
.config-path {
margin-top: 2rem;
padding: 1rem;
background: #ecf0f1;
border-radius: 4px;
font-size: 0.9rem;
color: #7f8c8d;
}

131
src/renderer/App.tsx Normal file
View File

@@ -0,0 +1,131 @@
import React, { useState, useEffect } from 'react'
import { Provider, ProviderStatus } from '../shared/types'
import ProviderList from './components/ProviderList'
import AddProviderModal from './components/AddProviderModal'
import './App.css'
function App() {
const [providers, setProviders] = useState<Record<string, Provider>>({})
const [currentProviderId, setCurrentProviderId] = useState<string>('')
const [statuses, setStatuses] = useState<Record<string, ProviderStatus>>({})
const [isAddModalOpen, setIsAddModalOpen] = useState(false)
const [isRefreshing, setIsRefreshing] = useState(false)
const [configPath, setConfigPath] = useState<string>('')
// 加载供应商列表
useEffect(() => {
loadProviders()
loadConfigPath()
}, [])
// 定时检查状态
useEffect(() => {
checkAllStatuses()
const interval = setInterval(checkAllStatuses, 30000) // 每30秒检查一次
return () => clearInterval(interval)
}, [providers])
const loadProviders = async () => {
const loadedProviders = await window.electronAPI.getProviders()
const currentId = await window.electronAPI.getCurrentProvider()
setProviders(loadedProviders)
setCurrentProviderId(currentId)
}
const loadConfigPath = async () => {
const path = await window.electronAPI.getClaudeCodeConfigPath()
setConfigPath(path)
}
const checkAllStatuses = async () => {
if (Object.keys(providers).length === 0) return
setIsRefreshing(true)
const newStatuses: Record<string, ProviderStatus> = {}
await Promise.all(
Object.values(providers).map(async (provider) => {
const status = await window.electronAPI.checkStatus(provider)
newStatuses[provider.id] = status
})
)
setStatuses(newStatuses)
setIsRefreshing(false)
}
const handleAddProvider = async (provider: Omit<Provider, 'id'>) => {
const newProvider: Provider = {
...provider,
id: Date.now().toString()
}
await window.electronAPI.addProvider(newProvider)
await loadProviders()
setIsAddModalOpen(false)
}
const handleDeleteProvider = async (id: string) => {
if (confirm('确定要删除这个供应商吗?')) {
await window.electronAPI.deleteProvider(id)
await loadProviders()
}
}
const handleSwitchProvider = async (id: string) => {
const success = await window.electronAPI.switchProvider(id)
if (success) {
setCurrentProviderId(id)
alert('切换成功!')
} else {
alert('切换失败,请检查配置')
}
}
return (
<div className="app">
<header className="app-header">
<h1>Claude Code </h1>
<div className="header-actions">
<button
className="refresh-btn"
onClick={checkAllStatuses}
disabled={isRefreshing}
>
{isRefreshing ? '检查中...' : '刷新状态'}
</button>
<button
className="add-btn"
onClick={() => setIsAddModalOpen(true)}
>
</button>
</div>
</header>
<main className="app-main">
<ProviderList
providers={providers}
currentProviderId={currentProviderId}
statuses={statuses}
onSwitch={handleSwitchProvider}
onDelete={handleDeleteProvider}
/>
{configPath && (
<div className="config-path">
: {configPath}
</div>
)}
</main>
{isAddModalOpen && (
<AddProviderModal
onAdd={handleAddProvider}
onClose={() => setIsAddModalOpen(false)}
/>
)}
</div>
)
}
export default App

View File

@@ -0,0 +1,122 @@
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 8px;
padding: 2rem;
width: 90%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}
.modal-content h2 {
margin-bottom: 1.5rem;
color: #2c3e50;
}
.presets {
margin-bottom: 1.5rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid #ecf0f1;
}
.presets label {
display: block;
margin-bottom: 0.5rem;
color: #555;
font-size: 0.9rem;
}
.preset-buttons {
display: flex;
gap: 0.5rem;
}
.preset-btn {
padding: 0.375rem 0.75rem;
border: 1px solid #3498db;
background: white;
color: #3498db;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
transition: all 0.2s;
}
.preset-btn:hover {
background: #3498db;
color: white;
}
.form-group {
margin-bottom: 1.25rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
color: #555;
font-weight: 500;
}
.form-group input {
width: 100%;
padding: 0.625rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.95rem;
transition: border-color 0.2s;
}
.form-group input:focus {
outline: none;
border-color: #3498db;
}
.form-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
margin-top: 2rem;
}
.cancel-btn,
.submit-btn {
padding: 0.625rem 1.25rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.95rem;
transition: all 0.2s;
}
.cancel-btn {
background: #ecf0f1;
color: #555;
}
.cancel-btn:hover {
background: #bdc3c7;
}
.submit-btn {
background: #27ae60;
color: white;
}
.submit-btn:hover {
background: #229954;
}

View File

@@ -0,0 +1,146 @@
import React, { useState } from 'react'
import { Provider } from '../../shared/types'
import './AddProviderModal.css'
interface AddProviderModalProps {
onAdd: (provider: Omit<Provider, 'id'>) => void
onClose: () => void
}
const AddProviderModal: React.FC<AddProviderModalProps> = ({ onAdd, onClose }) => {
const [formData, setFormData] = useState({
name: '',
apiUrl: '',
apiKey: '',
model: 'claude-3-opus-20240229'
})
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (!formData.name || !formData.apiUrl || !formData.apiKey) {
alert('请填写所有必填字段')
return
}
onAdd(formData)
}
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
setFormData({
...formData,
[e.target.name]: e.target.value
})
}
// 预设的供应商配置
const presets = [
{
name: '官方 Anthropic',
apiUrl: 'https://api.anthropic.com',
model: 'claude-3-opus-20240229'
},
{
name: 'OpenRouter',
apiUrl: 'https://openrouter.ai/api/v1',
model: 'anthropic/claude-3-opus'
}
]
const applyPreset = (preset: typeof presets[0]) => {
setFormData({
...formData,
name: preset.name,
apiUrl: preset.apiUrl,
model: preset.model
})
}
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<h2></h2>
<div className="presets">
<label></label>
<div className="preset-buttons">
{presets.map((preset, index) => (
<button
key={index}
type="button"
className="preset-btn"
onClick={() => applyPreset(preset)}
>
{preset.name}
</button>
))}
</div>
</div>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="name"> *</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
placeholder="例如:官方 Anthropic"
required
/>
</div>
<div className="form-group">
<label htmlFor="apiUrl">API *</label>
<input
type="url"
id="apiUrl"
name="apiUrl"
value={formData.apiUrl}
onChange={handleChange}
placeholder="https://api.anthropic.com"
required
/>
</div>
<div className="form-group">
<label htmlFor="apiKey">API Key *</label>
<input
type="password"
id="apiKey"
name="apiKey"
value={formData.apiKey}
onChange={handleChange}
placeholder="sk-ant-..."
required
/>
</div>
<div className="form-group">
<label htmlFor="model"></label>
<input
type="text"
id="model"
name="model"
value={formData.model}
onChange={handleChange}
placeholder="claude-3-opus-20240229"
/>
</div>
<div className="form-actions">
<button type="button" className="cancel-btn" onClick={onClose}>
</button>
<button type="submit" className="submit-btn">
</button>
</div>
</form>
</div>
</div>
)
}
export default AddProviderModal

View File

@@ -0,0 +1,127 @@
.provider-list {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 1.5rem;
}
.empty-state {
text-align: center;
padding: 3rem 1rem;
color: #7f8c8d;
}
.empty-state p:first-child {
font-size: 1.1rem;
margin-bottom: 0.5rem;
}
.provider-items {
display: flex;
flex-direction: column;
gap: 1rem;
}
.provider-item {
display: flex;
align-items: center;
padding: 1rem;
border: 2px solid #ecf0f1;
border-radius: 6px;
transition: all 0.2s;
}
.provider-item:hover {
border-color: #3498db;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.provider-item.current {
border-color: #27ae60;
background: #f0fdf4;
}
.provider-info {
flex: 1;
min-width: 0;
}
.provider-name {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 500;
margin-bottom: 0.25rem;
}
.provider-name input[type="radio"] {
cursor: pointer;
}
.provider-name input[type="radio"]:disabled {
cursor: not-allowed;
}
.current-badge {
background: #27ae60;
color: white;
padding: 0.125rem 0.5rem;
border-radius: 3px;
font-size: 0.75rem;
}
.provider-url {
color: #7f8c8d;
font-size: 0.9rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.provider-status {
display: flex;
align-items: center;
gap: 0.5rem;
margin-right: 2rem;
}
.status-icon {
font-size: 1.1rem;
}
.status-text {
color: #555;
font-size: 0.9rem;
}
.response-time {
color: #3498db;
font-size: 0.85rem;
font-family: monospace;
}
.provider-actions {
display: flex;
gap: 0.5rem;
}
.delete-btn {
padding: 0.375rem 0.75rem;
border: 1px solid #e74c3c;
background: white;
color: #e74c3c;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
transition: all 0.2s;
}
.delete-btn:hover:not(:disabled) {
background: #e74c3c;
color: white;
}
.delete-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}

View File

@@ -0,0 +1,97 @@
import React from 'react'
import { Provider, ProviderStatus } from '../../shared/types'
import './ProviderList.css'
interface ProviderListProps {
providers: Record<string, Provider>
currentProviderId: string
statuses: Record<string, ProviderStatus>
onSwitch: (id: string) => void
onDelete: (id: string) => void
}
const ProviderList: React.FC<ProviderListProps> = ({
providers,
currentProviderId,
statuses,
onSwitch,
onDelete
}) => {
const formatResponseTime = (time: number) => {
if (time < 0) return '-'
return `${time}ms`
}
const getStatusIcon = (status?: ProviderStatus) => {
if (!status) return '⏳'
return status.isOnline ? '✅' : '❌'
}
const getStatusText = (status?: ProviderStatus) => {
if (!status) return '检查中...'
if (status.isOnline) return '正常'
return status.error || '连接失败'
}
return (
<div className="provider-list">
{Object.values(providers).length === 0 ? (
<div className="empty-state">
<p></p>
<p>"添加供应商"</p>
</div>
) : (
<div className="provider-items">
{Object.values(providers).map((provider) => {
const status = statuses[provider.id]
const isCurrent = provider.id === currentProviderId
return (
<div
key={provider.id}
className={`provider-item ${isCurrent ? 'current' : ''}`}
>
<div className="provider-info">
<div className="provider-name">
<input
type="radio"
name="provider"
checked={isCurrent}
onChange={() => onSwitch(provider.id)}
disabled={!status?.isOnline}
/>
<span>{provider.name}</span>
{isCurrent && <span className="current-badge">使</span>}
</div>
<div className="provider-url">{provider.apiUrl}</div>
</div>
<div className="provider-status">
<span className="status-icon">{getStatusIcon(status)}</span>
<span className="status-text">{getStatusText(status)}</span>
{status?.isOnline && (
<span className="response-time">
{formatResponseTime(status.responseTime)}
</span>
)}
</div>
<div className="provider-actions">
<button
className="delete-btn"
onClick={() => onDelete(provider.id)}
disabled={isCurrent}
>
</button>
</div>
</div>
)
})}
</div>
)}
</div>
)
}
export default ProviderList

21
src/renderer/index.css Normal file
View File

@@ -0,0 +1,21 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #f5f5f5;
color: #333;
}
#root {
height: 100vh;
display: flex;
flex-direction: column;
}

12
src/renderer/index.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Claude Code 供应商切换器</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>

10
src/renderer/main.tsx Normal file
View File

@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
)

33
src/shared/types.ts Normal file
View File

@@ -0,0 +1,33 @@
export interface Provider {
id: string
name: string
apiUrl: string
apiKey: string
model?: string
}
export interface ProviderStatus {
isOnline: boolean
responseTime: number
lastChecked: Date
error?: string
}
export interface AppConfig {
providers: Record<string, Provider>
current: string
}
declare global {
interface Window {
electronAPI: {
getProviders: () => Promise<Record<string, Provider>>
getCurrentProvider: () => Promise<string>
addProvider: (provider: Provider) => Promise<boolean>
deleteProvider: (id: string) => Promise<boolean>
checkStatus: (provider: Provider) => Promise<ProviderStatus>
switchProvider: (providerId: string) => Promise<boolean>
getClaudeCodeConfigPath: () => Promise<string>
}
}
}

20
tsconfig.json Normal file
View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/renderer/**/*"],
"references": [{ "path": "./tsconfig.node.json" }]
}

9
tsconfig.main.json Normal file
View File

@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.node.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src/main",
"types": ["node"]
},
"include": ["src/main/**/*", "src/shared/**/*"]
}

15
tsconfig.node.json Normal file
View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "CommonJS",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"target": "ES2020",
"strict": true,
"types": ["node"]
},
"include": ["vite.config.ts"]
}

16
vite.config.ts Normal file
View File

@@ -0,0 +1,16 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { resolve } from 'path'
export default defineConfig({
plugins: [react()],
root: resolve(__dirname, 'src/renderer'),
base: './',
build: {
outDir: resolve(__dirname, 'dist/renderer'),
emptyOutDir: true
},
server: {
port: 3000
}
})