feat(electron): scaffold electron-vite project structure

Initialize Wave 1 Electron integration with:
- electron v40.6.1, electron-vite v5.0.0, electron-builder v26.8.1
- Main entry point: electron/main/index.ts with app:// protocol registration
- Preload placeholder: electron/preload/index.ts for Wave 2 IPC
- electron.vite.config.ts with unified renderer chunk splitting strategy
- tsconfig.electron.json with ESNext module support
- Updated package.json with electron dependencies and scripts:
  - electron:dev, electron:build, electron:preview
- All original scripts (dev, build, test, etc) remain unchanged
- TypeScript compilation verified: tsc --noEmit passes
- Ready for Wave 2: IPC bridge and contextBridge exposure
This commit is contained in:
DrSmoothl
2026-03-02 22:49:01 +08:00
parent 91765400f6
commit e6862e227b
6 changed files with 301 additions and 2 deletions

View File

@@ -0,0 +1,150 @@
import { defineConfig } from 'electron-vite'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
main: {
entry: 'electron/main/index.ts',
vite: {
build: {
rollupOptions: {
external: ['electron'],
},
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
},
},
preload: {
entry: 'electron/preload/index.ts',
vite: {
build: {
rollupOptions: {
external: ['electron'],
},
},
},
},
renderer: {
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
plugins: [react()],
server: {
port: 7999,
proxy: {
'/api': {
target: 'http://127.0.0.1:8001',
changeOrigin: true,
ws: true,
cookieDomainRewrite: '',
cookiePathRewrite: '/',
},
},
},
build: {
rollupOptions: {
output: {
manualChunks: {
// React core
'react-vendor': ['react', 'react-dom', 'react/jsx-runtime'],
// TanStack Router
router: ['@tanstack/react-router', '@tanstack/react-virtual'],
// Radix UI core
'radix-core': [
'@radix-ui/react-dialog',
'@radix-ui/react-select',
'@radix-ui/react-checkbox',
'@radix-ui/react-label',
'@radix-ui/react-slot',
'@radix-ui/react-toast',
'@radix-ui/react-tooltip',
],
// Radix UI extras
'radix-extra': [
'@radix-ui/react-alert-dialog',
'@radix-ui/react-avatar',
'@radix-ui/react-collapsible',
'@radix-ui/react-context-menu',
'@radix-ui/react-popover',
'@radix-ui/react-progress',
'@radix-ui/react-scroll-area',
'@radix-ui/react-separator',
'@radix-ui/react-slider',
'@radix-ui/react-switch',
'@radix-ui/react-tabs',
],
// Icons
icons: ['lucide-react'],
// Charts
charts: ['recharts'],
// CodeMirror
codemirror: [
'@uiw/react-codemirror',
'@codemirror/lang-javascript',
'@codemirror/lang-json',
'@codemirror/lang-python',
'@codemirror/lint',
'@codemirror/theme-one-dark',
],
// ReactFlow
reactflow: ['reactflow', 'dagre'],
// Markdown
markdown: [
'react-markdown',
'remark-gfm',
'remark-math',
'rehype-katex',
'katex',
],
// Uppy
uppy: [
'@uppy/core',
'@uppy/dashboard',
'@uppy/react',
'@uppy/xhr-upload',
],
// Drag and drop
dnd: [
'@dnd-kit/core',
'@dnd-kit/sortable',
'@dnd-kit/utilities',
],
// Utils
utils: [
'date-fns',
'clsx',
'tailwind-merge',
'class-variance-authority',
'axios',
],
// Misc
misc: [
'react-joyride',
'react-day-picker',
'cmdk',
],
},
},
},
chunkSizeWarningLimit: 500,
},
},
})

View File

@@ -0,0 +1,98 @@
import { app, BrowserWindow, protocol } from 'electron'
import path from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
let mainWindow: BrowserWindow | null = null
/**
* Register app:// custom protocol BEFORE app.whenReady()
* This is critical for electron-vite to work correctly
*/
function registerAppProtocol() {
protocol.registerSchemesAsPrivileged([
{
scheme: 'app',
privileges: {
secure: true,
standard: true,
allowServiceWorkers: true,
},
},
])
}
/**
* Create the main application window
*/
function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 800,
minHeight: 600,
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
nodeIntegration: false,
contextIsolation: true,
},
})
// Load the app using app:// protocol
// electron-vite will handle serving the renderer from app://host/index.html
if (process.env.VITE_DEV_SERVER_URL) {
// Development: load from electron-vite dev server
mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL)
} else {
// Production: load from bundled renderer
mainWindow.loadURL('app://host/index.html')
}
mainWindow.on('closed', () => {
mainWindow = null
})
}
/**
* Register app:// protocol handler (for production)
*/
function registerAppProtocolHandler() {
protocol.handle('app', (request) => {
const filePath = new URL(request.url).pathname
return new Response(
`Cannot handle app:// requests. Renderer should be served by electron-vite.`
)
})
}
/**
* App event: when app is ready
*/
app.on('ready', () => {
registerAppProtocolHandler()
createWindow()
})
/**
* App event: when all windows are closed (non-macOS behavior)
*/
app.on('window-all-closed', () => {
// On macOS, applications typically stay open until the user quits
if (process.platform !== 'darwin') {
app.quit()
}
})
/**
* App event: when app is activated (macOS)
*/
app.on('activate', () => {
if (mainWindow === null) {
createWindow()
}
})
// Register protocol BEFORE app.whenReady()
registerAppProtocol()

View File

@@ -0,0 +1,7 @@
/**
* Preload script for Electron renderer process
* This script runs in the main process before renderer loads
* Use contextBridge to safely expose APIs
*
* Wave 2 implementation will expose specific IPC methods here
*/

View File

@@ -3,6 +3,7 @@
"private": true, "private": true,
"version": "1.0.0", "version": "1.0.0",
"type": "module", "type": "module",
"main": "./out/main/index.js",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
@@ -10,7 +11,10 @@
"preview": "vite preview", "preview": "vite preview",
"format": "prettier --write \"src/**/*.{ts,tsx,css}\"", "format": "prettier --write \"src/**/*.{ts,tsx,css}\"",
"test": "vitest", "test": "vitest",
"test:ui": "vitest --ui" "test:ui": "vitest --ui",
"electron:dev": "electron-vite dev",
"electron:build": "electron-vite build",
"electron:preview": "electron-vite preview"
}, },
"dependencies": { "dependencies": {
"@codemirror/lang-css": "^6.3.1", "@codemirror/lang-css": "^6.3.1",
@@ -88,6 +92,9 @@
"@vitejs/plugin-react": "^5.1.2", "@vitejs/plugin-react": "^5.1.2",
"@vitest/ui": "^4.0.18", "@vitest/ui": "^4.0.18",
"autoprefixer": "^10.4.22", "autoprefixer": "^10.4.22",
"electron": "^40.6.1",
"electron-builder": "^26.8.1",
"electron-vite": "^5.0.0",
"eslint": "^9.39.1", "eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24", "eslint-plugin-react-refresh": "^0.4.24",

View File

@@ -0,0 +1,36 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.electron.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020"],
"module": "ESNext",
"moduleResolution": "bundler",
"types": ["electron"],
"skipLibCheck": true,
/* Module resolution */
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": false,
"moduleDetection": "force",
"noEmit": true,
/* Path aliases */
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
/* Linting */
"strict": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"skipLibCheck": true
},
"include": ["electron/**/*"]
}

View File

@@ -3,6 +3,7 @@
"references": [ "references": [
{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.node.json" },
{ "path": "./tsconfig.vitest.json" } { "path": "./tsconfig.vitest.json" },
{ "path": "./tsconfig.electron.json" }
] ]
} }