electron-base について
electron-baseは、Electron 33、Vite、React、TypeScriptを使うデスクトップアプリ開発者向けのガイドです。プロセス分離、型安全なIPC、セキュリティ、OAuth、ネイティブモジュール、パッケージ化を扱います。
主な機能
- 01ElectronとViteのプロジェクト構成
- 02メイン・レンダラープロセスの分離
- 03型安全なIPCパターン
- 04セキュリティ設定とOAuthの例
- 05ネイティブモジュール対応とパッケージ化
活用シーン
- 01ReactとTypeScriptのデスクトップアプリ作成
- 02レンダラー向けの安全なIPC API設計
- 03カスタムプロトコルによるOAuthコールバック処理
- 04ネイティブモジュールのABIやサンドボックス問題の調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jezweb/ claude-skills --skill electron-baseインストール後、Claude Code で/electron-baseを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
README.md.claude-plugin/plugin.jsonreferences/security-checklist.mdrules/electron-base.mdtemplates/electron-builder.jsonmain.tspreload.tsvite.config.tstemplates/ipc-handlers/auth.tsstore.ts詳細
Electron Base Skill
Electron 33、Vite、React、TypeScript を使用して、安全でモダンなデスクトップアプリケーションを構築します。
クイックスタート
1. プロジェクトを初期化する
# Create Vite project
npm create vite@latest my-app -- --template react-ts
cd my-app
# Install Electron dependencies
npm install electron electron-store
npm install -D vite-plugin-electron vite-plugin-electron-renderer electron-builder
2. プロジェクト構成
my-app/
├── electron/
│ ├── main.ts # Main process entry
│ ├── preload.ts # Preload script (contextBridge)
│ └── ipc-handlers/ # Modular IPC handlers
│ ├── auth.ts
│ └── store.ts
├── src/ # React app (renderer)
├── vite.config.ts # Dual-entry Vite config
├── electron-builder.json # Build config
└── package.json
3. Package.json の更新
{
"main": "dist-electron/main.mjs",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "electron .",
"package": "electron-builder"
}
}
アーキテクチャパターン
メインプロセスとレンダラープロセスの分離
┌─────────────────────────────────────────────────────────────┐
│ MAIN PROCESS │
│ (Node.js + Electron APIs) │
│ - File system access │
│ - Native modules (better-sqlite3) │
│ - System dialogs │
│ - Protocol handlers │
└─────────────────────┬───────────────────────────────────────┘
│ IPC (invoke/handle)
│ Events (send/on)
┌─────────────────────▼───────────────────────────────────────┐
│ PRELOAD SCRIPT │
│ (contextBridge.exposeInMainWorld) │
│ - Type-safe API exposed to renderer │
│ - No direct ipcRenderer exposure │
└─────────────────────┬───────────────────────────────────────┘
│ window.electron.*
┌─────────────────────▼───────────────────────────────────────┐
│ RENDERER PROCESS │
│ (Browser context - React app) │
│ - No Node.js APIs │
│ - Uses window.electron.* for IPC │
└─────────────────────────────────────────────────────────────┘
型安全な IPC パターン
プリロードスクリプトで、レンダラーに型付き API を公開します。
// electron/preload.ts
export interface ElectronAPI {
auth: {
startOAuth: (provider: 'google' | 'github') => Promise<void>;
getSession: () => Promise<Session | null>;
logout: () => Promise<void>;
onSuccess: (callback: (session: Session) => void) => () => void;
onError: (callback: (error: string) => void) => () => void;
};
app: {
getVersion: () => Promise<string>;
openExternal: (url: string) => Promise<void>;
};
}
// Expose to renderer
contextBridge.exposeInMainWorld('electron', electronAPI);
// Global type declaration
declare global {
interface Window {
electron: ElectronAPI;
}
}
セキュリティのベストプラクティス
必須:コンテキスト分離
コンテキスト分離を常に有効にし、Node 統合を無効にしてください。
// electron/main.ts
const mainWindow = new BrowserWindow({
webPreferences: {
preload: join(__dirname, 'preload.cjs'),
contextIsolation: true, // REQUIRED - isolates preload from renderer
nodeIntegration: false, // REQUIRED - no Node.js in renderer
sandbox: false, // May need to disable for native modules
},
});
禁止:暗号化キーのハードコード
// WRONG - hardcoded key is a security vulnerability
const store = new Store({
encryptionKey: 'my-secret-key', // DO NOT DO THIS
});
// CORRECT - derive from machine ID
import { machineIdSync } from 'node-machine-id';
const store = new Store({
encryptionKey: machineIdSync().slice(0, 32), // Machine-unique key
});
サンドボックスのトレードオフ
better-sqlite3 などのネイティブモジュールには、sandbox: false が必要です。このトレードオフを文書化してください。
webPreferences: {
sandbox: false, // Required for better-sqlite3 - document security trade-off
}
sandbox: false が必要なモジュール:
- better-sqlite3
- node-pty
- native-keymap
sandbox: true で動作するモジュール:
- electron-store(純粋な JS)
- keytar(Electron の safeStorage を使用)
カスタムプロトコルハンドラーを使用した OAuth
1. プロトコルを登録する(main.ts)
// In development, need to pass executable path
if (process.defaultApp) {
if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient('myapp', process.execPath, [process.argv[1]]);
}
} else {
app.setAsDefaultProtocolClient('myapp');
}
2. プロトコル URL を処理する
// Single instance lock (required for reliable protocol handling)
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
} else {
app.on('second-instance', (_event, commandLine) => {
const url = commandLine.find((arg) => arg.startsWith('myapp://'));
if (url) handleProtocolUrl(url);
if (mainWindow?.isMinimized()) mainWindow.restore();
mainWindow?.focus();
});
}
// macOS handles protocol differently
app.on('open-url', (_event, url) => {
handleProtocolUrl(url);
});
function handleProtocolUrl(url: string) {
const parsedUrl = new URL(url);
if (parsedUrl.pathname.includes('/auth/callback')) {
const token = parsedUrl.searchParams.get('token');
const state = parsedUrl.searchParams.get('state');
const error = parsedUrl.searchParams.get('error');
if (error) {
mainWindow?.webContents.send('auth:error', error);
} else if (token && state) {
handleAuthCallback(token, state)
.then((session) => mainWindow?.webContents.send('auth:success', session))
.catch((err) => mainWindow?.webContents.send('auth:error', err.message));
}
}
}
3. CSRF 対策のための State 検証
// Start OAuth - generate and store state
ipcMain.handle('auth:start-oauth', async (_event, provider) => {
const state = crypto.randomUUID();
store.set('pendingState', state);
const authUrl = `${BACKEND_URL}/api/auth/signin/${provider}?state=${state}`;
await shell.openExternal(authUrl);
});
// Verify state on callback
export async function handleAuthCallback(token: string, state: string): Promise<Session> {
const pendingState = store.get('pendingState');
if (state !== pendingState) {
throw new Error('State mismatch - possible CSRF attack');
}
store.set('pendingState', null);
// ... rest of auth flow
}
ネイティブモジュールの互換性
better-sqlite3
Electron の Node ABI に合わせて再ビルドする必要があります。
# Install
npm install better-sqlite3
# Rebuild for Electron
npm install -D electron-rebuild
npx electron-rebuild -f -w better-sqlite3
Vite の設定 - ネイティブモジュールを外部化する:
// vite.config.ts
electron({
main: {
entry: 'electron/main.ts',
vite: {
build: {
rollupOptions: {
external: ['electron', 'better-sqlite3', 'electron-store'],
},
},
},
},
});
electron-store
サンドボックスを有効にしても動作しますが、暗号化キーはマシンに基づいて導出してください。
import Store from 'electron-store';
import { machineIdSync } from 'node-machine-id';
interface StoreSchema {
session: Session | null;
settings: Settings;
}
const store = new Store<StoreSchema>({
name: 'myapp-data',
encryptionKey: machineIdSync().slice(0, 32),
defaults: {
session: null,
settings: { theme: 'system' },
},
});
ビルドとパッケージング
electron-builder.json
{
"$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json",
"appId": "com.yourcompany.myapp",
"productName": "MyApp",
"directories": {
"output": "release"
},
"files": [
"dist/**/*",
"dist-electron/**/*"
],
"mac": {
"category": "public.app-category.productivity",
"icon": "build/icon.icns",
"hardenedRuntime": true,
"gatekeeperAssess": false,
"entitlements": "build/entitlements.mac.plist",
"entitlementsInherit": "build/entitlements.mac.plist",
"target": [
{ "target": "dmg", "arch": ["x64", "arm64"] }
],
"protocols": [
{ "name": "MyApp", "schemes": ["myapp"] }
]
},
"win": {
"icon": "build/icon.ico",
"target": [
{ "target": "nsis", "arch": ["x64"] }
]
},
"linux": {
"icon": "build/icons",
"target": ["AppImage"],
"category": "Office"
}
}
macOS の Entitlements(build/entitlements.mac.plist)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>
ビルドコマンド
# Development
npm run dev
# Production build
npm run build
# Package for current platform
npm run package
# Package for specific platform
npx electron-builder --mac
npx electron-builder --win
npx electron-builder --linux
既知の問題と予防策
1. Preload での「Cannot read properties of undefined」
原因: preload の完了前に window.electron にアクセスしています。
修正: オプショナルチェーンを使用するか、存在を確認します。
// In React component
useEffect(() => {
if (!window.electron?.auth) return;
const unsubscribe = window.electron.auth.onSuccess((session) => {
setSession(session);
});
return unsubscribe;
}, []);
2. NODE_MODULE_VERSION の不一致
原因: ネイティブモジュールが Electron で使用されるバージョンとは異なる Node.js バージョン向けにコンパイルされています。
修正:
# Rebuild native modules for Electron
npx electron-rebuild -f -w better-sqlite3
# Or add to package.json postinstall
"scripts": {
"postinstall": "electron-rebuild"
}
3. OAuth State の不一致
原因: OAuth の開始からコールバックまでの間に State が保存されていない、または失われています。
修正: メモリではなく、永続ストレージ(electron-store)を使用します。
// WRONG - state lost if app restarts
let pendingState: string | null = null;
// CORRECT - persisted storage
const store = new Store({ ... });
store.set('pendingState', state);
4. ネイティブモジュールとのサンドボックスの競合
原因: サンドボックスによって、ネイティブの .node ファイルの読み込みが妨げられます。
修正: サンドボックスを無効にする(トレードオフを文書化する)か、純粋な JS の代替手段を使用します。
webPreferences: {
sandbox: false, // Required for better-sqlite3
// Alternative: Use sql.js (WASM) if sandbox required
}
5. 2 つの認証システムを維持する負担
原因: better-auth を設定しながら手動 OAuth を使用すると、混乱を招きます。
修正: どちらか一方を選択します。
- better-auth を全面的に使用する、または
- 手動 OAuth のみを使用する(better-auth を削除する)
6. トークンの更新がない場合の有効期限切れ
原因: 更新の仕組みがない状態で、有効期限をハードコードしています。
修正: トークンの更新またはスライディングセッションを実装します。
// Check expiration with buffer
const session = store.get('session');
const expiresAt = new Date(session.expiresAt);
const bufferMs = 5 * 60 * 1000; // 5 minutes
if (Date.now() > expiresAt.getTime() - bufferMs) {
await refreshToken(session.token);
}
7. エラーを隠してしまう空の catch ブロック
原因: エラーを黙って無視しています。
修正: エラーをログに記録し、エラーの種類を区別します。
// WRONG
try {
await fetch(url);
} catch {
// Silent failure - user has no idea what happened
}
// CORRECT
try {
await fetch(url);
} catch (err) {
if (err instanceof TypeError) {
console.error('[Network] Offline or DNS failure:', err.message);
} else {
console.error('[Auth] Unexpected error:', err);
}
throw err; // Re-throw for caller to handle
}
8. 暗号化キーのハードコード
原因: 暗号化に文字列リテラルを使用しています。
修正: マシン識別子から導出します。
import { machineIdSync } from 'node-machine-id';
const store = new Store({
encryptionKey: machineIdSync().slice(0, 32),
});
Vite の設定
vite.config.ts の全体設定
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
import electron from 'vite-plugin-electron/simple';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export default defineConfig({
plugins: [
react(),
tailwindcss(),
electron({
main: {
entry: 'electron/main.ts',
vite: {
build: {
outDir: 'dist-electron',
rollupOptions: {
external: ['electron', 'better-sqlite3', 'electron-store'],
output: {
format: 'es',
entryFileNames: '[name].mjs',
},
},
},
},
},
preload: {
input: 'electron/preload.ts',
vite: {
build: {
outDir: 'dist-electron',
rollupOptions: {
output: {
format: 'cjs',
entryFileNames: '[name].cjs',
},
},
},
},
},
renderer: {},
}),
],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
build: {
outDir: 'dist',
},
optimizeDeps: {
include: ['react', 'react-dom'],
},
});
依存関係のリファレンス
本番環境の依存関係
{
"dependencies": {
"electron-store": "^10.0.0",
"electron-updater": "^6.3.0"
},
"optionalDependencies": {
"better-sqlite3": "^11.0.0",
"node-machine-id": "^1.1.12"
}
}
開発環境の依存関係
{
"devDependencies": {
"electron": "^33.0.0",
"electron-builder": "^25.0.0",
"electron-rebuild": "^3.2.9",
"vite-plugin-electron": "^0.28.0",
"vite-plugin-electron-renderer": "^0.14.0"
}
}
セキュリティチェックリスト
リリース前に、以下を確認してください。
- webPreferences で
contextIsolation: trueを設定している - webPreferences で
nodeIntegration: falseを設定している - 暗号化キーをハードコードしていない
- OAuth の State 検証を実装している
- IPC チャネル名に機密データを含めていない
- 外部リンクをシステムブラウザーで開く(
shell.openExternal) - 本番環境向けの CSP ヘッダーを設定している
- macOS の Hardened Runtime を有効にしている
- コード署名を設定している(配布する場合)
- エラーを隠す空の catch ブロックがない
関連スキル
- better-auth - Electron OAuth と組み合わせるバックエンド認証向け
- cloudflare-worker-base - バックエンド API の構築向け
- tailwind-v4-shadcn - レンダラー UI のスタイリング向け