From e6862e227b7385dfab3bc7707bd3056f36818a4a Mon Sep 17 00:00:00 2001 From: DrSmoothl <1787882683@qq.com> Date: Mon, 2 Mar 2026 22:49:01 +0800 Subject: [PATCH] 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 --- dashboard/electron.vite.config.ts | 150 ++++++++++++++++++++++++++++ dashboard/electron/main/index.ts | 98 ++++++++++++++++++ dashboard/electron/preload/index.ts | 7 ++ dashboard/package.json | 9 +- dashboard/tsconfig.electron.json | 36 +++++++ dashboard/tsconfig.json | 3 +- 6 files changed, 301 insertions(+), 2 deletions(-) create mode 100644 dashboard/electron.vite.config.ts create mode 100644 dashboard/electron/main/index.ts create mode 100644 dashboard/electron/preload/index.ts create mode 100644 dashboard/tsconfig.electron.json diff --git a/dashboard/electron.vite.config.ts b/dashboard/electron.vite.config.ts new file mode 100644 index 00000000..1e9c2b0f --- /dev/null +++ b/dashboard/electron.vite.config.ts @@ -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, + }, + }, +}) diff --git a/dashboard/electron/main/index.ts b/dashboard/electron/main/index.ts new file mode 100644 index 00000000..4c30f9ae --- /dev/null +++ b/dashboard/electron/main/index.ts @@ -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() diff --git a/dashboard/electron/preload/index.ts b/dashboard/electron/preload/index.ts new file mode 100644 index 00000000..39ec94b6 --- /dev/null +++ b/dashboard/electron/preload/index.ts @@ -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 + */ diff --git a/dashboard/package.json b/dashboard/package.json index b627cddb..c30afc0a 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -3,6 +3,7 @@ "private": true, "version": "1.0.0", "type": "module", + "main": "./out/main/index.js", "scripts": { "dev": "vite", "build": "tsc -b && vite build", @@ -10,7 +11,10 @@ "preview": "vite preview", "format": "prettier --write \"src/**/*.{ts,tsx,css}\"", "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": { "@codemirror/lang-css": "^6.3.1", @@ -88,6 +92,9 @@ "@vitejs/plugin-react": "^5.1.2", "@vitest/ui": "^4.0.18", "autoprefixer": "^10.4.22", + "electron": "^40.6.1", + "electron-builder": "^26.8.1", + "electron-vite": "^5.0.0", "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", diff --git a/dashboard/tsconfig.electron.json b/dashboard/tsconfig.electron.json new file mode 100644 index 00000000..8ec25cc8 --- /dev/null +++ b/dashboard/tsconfig.electron.json @@ -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/**/*"] +} diff --git a/dashboard/tsconfig.json b/dashboard/tsconfig.json index 08c8a904..04af6c3f 100644 --- a/dashboard/tsconfig.json +++ b/dashboard/tsconfig.json @@ -3,6 +3,7 @@ "references": [ { "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }, - { "path": "./tsconfig.vitest.json" } + { "path": "./tsconfig.vitest.json" }, + { "path": "./tsconfig.electron.json" } ] }