fe-cli-electron — Electron Desktop App Scaffolding
Workflow
Step 1: Gather Options
- Framework: React 19 / Vue 3
- Styling: Tailwind CSS / Ant Design / MUI / 纯 SCSS
- CSS Preprocessor: Sass / Less
- State Management: Zustand / Redux Toolkit / None
- Router: React Router / Vue Router
- i18n: Yes / No
- Auto Update: Yes (default) / No
- System Tray: Yes / No
- Testing: Vitest / None
- Pre-commit: Yes / No
- Project name: string
Step 2: Scaffold
pnpm create vite <project-name> --template react-ts
cd <project-name>
pnpm add -D electron electron-builder concurrently wait-on
pnpm add electron-updater electron-store # if auto-update selected
Step 3: Project Structure
project-name/
├── electron/
│ ├── main.ts # Main process entry
│ ├── preload.ts # Preload script (contextBridge)
│ └── updater.ts # Auto-update logic (if selected)
├── src/ # Renderer process (React/Vue)
│ ├── components/ # Global components (from shared infrastructure)
│ ├── pages/ # Page components
│ ├── hooks/ # Common hooks (from shared infrastructure)
│ ├── store/ # State management (from shared infrastructure)
│ ├── theme/ # Theme system (from shared infrastructure)
│ ├── config/ # App constants + routes (from shared infrastructure)
│ ├── services/ # API layer (from shared base)
│ ├── utils/ # Utilities (from shared base)
│ ├── styles/ # Global styles (from shared base)
│ ├── electron.d.ts # IPC type declarations
│ ├── App.tsx
│ └── main.tsx
├── package.json
├── vite.config.ts
├── electron-builder.yml # Build config
└── resources/ # App icons
Step 4: Key Templates
electron/main.ts:
import { app, BrowserWindow, ipcMain, Tray, Menu, nativeImage } from 'electron';
import path from 'path';
import { setupUpdater } from './updater'; // if auto-update
let mainWindow: BrowserWindow | null = null;
let tray: Tray | null = null;
const isDev = !app.isPackaged;
function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 800,
minHeight: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
},
titleBarStyle: 'hiddenInset', // macOS native title bar
});
if (isDev) {
mainWindow.loadURL('http://localhost:5173');
mainWindow.webContents.openDevTools();
} else {
mainWindow.loadFile(path.join(__dirname, '../dist/index.html'));
}
// System tray (if selected)
if (process.env.NODE_ENV !== 'linux') {
const icon = nativeImage.createFromPath(path.join(__dirname, '../resources/icon.png'));
tray = new Tray(icon.resize({ width: 16, height: 16 }));
tray.setToolTip('Your App');
tray.setContextMenu(Menu.buildFromTemplate([
{ label: 'Show', click: () => mainWindow?.show() },
{ type: 'separator' },
{ label: 'Quit', click: () => app.quit() },
]));
}
}
// IPC handlers
ipcMain.handle('get-app-version', () => app.getVersion());
ipcMain.handle('get-platform', () => process.platform);
app.whenReady().then(() => {
createWindow();
if (process.env.AUTO_UPDATE !== 'false') setupUpdater(mainWindow!);
app.on('activate', () => { if (!mainWindow) createWindow(); });
});
app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });
electron/preload.ts:
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('electronAPI', {
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
getPlatform: () => ipcRenderer.invoke('get-platform'),
onUpdateAvailable: (cb: (info: unknown) => void) => ipcRenderer.on('update-available', (_, info) => cb(info)),
onUpdateDownloaded: (cb: () => void) => ipcRenderer.on('update-downloaded', () => cb()),
installUpdate: () => ipcRenderer.send('install-update'),
minimize: () => ipcRenderer.send('window-minimize'),
maximize: () => ipcRenderer.send('window-maximize'),
close: () => ipcRenderer.send('window-close'),
});
electron/updater.ts (if auto-update):
import { autoUpdater } from 'electron-updater';
import { BrowserWindow } from 'electron';
export function setupUpdater(mainWindow: BrowserWindow) {
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = true;
autoUpdater.on('update-available', (info) => {
mainWindow.webContents.send('update-available', info);
});
autoUpdater.on('update-downloaded', () => {
mainWindow.webContents.send('update-downloaded');
});
autoUpdater.on('error', (err) => {
console.error('Update error:', err);
});
// Check for updates every 4 hours
autoUpdater.checkForUpdatesAndNotify();
setInterval(() => autoUpdater.checkForUpdatesAndNotify(), 4 * 60 * 60 * 1000);
}
src/electron.d.ts:
export {};
declare global {
interface Window {
electronAPI: {
getAppVersion: () => Promise<string>;
getPlatform: () => Promise<string>;
onUpdateAvailable: (cb: (info: unknown) => void) => void;
onUpdateDownloaded: (cb: () => void) => void;
installUpdate: () => void;
minimize: () => void;
maximize: () => void;
close: () => void;
};
}
}
electron-builder.yml:
appId: <YOUR_APP_ID> # e.g. com.yourcompany.yourapp — replace with actual app ID (see parameterization rule in main SKILL.md)
productName: <YOUR_APP_NAME> # e.g. My App — replace with actual app name
directories:
output: release
files:
- dist/**/*
- electron/**/*
- package.json
mac:
target:
- dmg
- zip
artifactName: ${productName}-${version}-mac-${arch}.${ext}
hardenedRuntime: true
gatekeeperAssess: false
entitlements: build/entitlements.mac.plist
entitlementsInherit: build/entitlements.mac.plist
win:
target:
- nsis
artifactName: ${productName}-${version}-win-${arch}.${ext}
nsis:
oneClick: false
allowToChangeInstallationDirectory: true
publish:
- provider: generic
url: <YOUR_UPDATE_URL> # e.g. https://update.yourcompany.com — replace with actual update server URL
package.json scripts:
{
"main": "electron/main.js",
"scripts": {
"dev": "concurrently \"vite\" \"wait-on http://localhost:5173 && electron .\"",
"dev:renderer": "vite",
"dev:electron": "electron .",
"build": "vite build && tsc -p tsconfig.electron.json",
"build:prod": "vite build && tsc -p tsconfig.electron.json && electron-builder",
"preview": "vite preview",
"pack": "electron-builder --dir",
"dist": "electron-builder"
}
}
Step 5: Vite Config
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
resolve: { alias: { '@': path.resolve(__dirname, 'src') } },
base: './', // Electron loads from file:// so relative paths
build: { outDir: 'dist' },
server: { port: 5173 },
});
Step 6: Shared Layer + Infrastructure + Final
Read the following reference files and generate all shared code:
../references/shared-base.md— services, utils, styles, types, env files../references/shared-config.md— vite, tsconfig, eslint, prettier configs../references/shared-infrastructure.md— store, theme, i18n, hooks, layouts, auth guard, config/constants
Only generate env files for VITE_ variables (no API proxy needed — Electron uses direct connection).
Conditional generation (only if user selected the corresponding option):
- Selected Zustand/Redux Toolkit/Pinia → generate
src/store/ - Selected i18n → generate
src/locales/ - Always generate:
src/hooks/,src/components/AppProvider.tsx,src/components/AuthGuard.tsx,src/components/GlobalLoading.tsx,src/config/,src/theme/
After setup:
cd <project-name>
pnpm install
pnpm dev
Announce: project with Electron + auto-update config, dev starts both Vite and Electron.