mcpField
E
electron-base

electron-base

github.com/jezweb開発1.1k263 回インストール3 週間前に更新
リポジトリ

electron-base について

electron-baseは、Electron 33、Vite、React、TypeScriptを使うデスクトップアプリ開発者向けのガイドです。プロセス分離、型安全なIPC、セキュリティ、OAuth、ネイティブモジュール、パッケージ化を扱います。

主な機能

  1. 01ElectronとViteのプロジェクト構成
  2. 02メイン・レンダラープロセスの分離
  3. 03型安全なIPCパターン
  4. 04セキュリティ設定とOAuthの例
  5. 05ネイティブモジュール対応とパッケージ化

活用シーン

  1. 01ReactとTypeScriptのデスクトップアプリ作成
  2. 02レンダラー向けの安全なIPC API設計
  3. 03カスタムプロトコルによるOAuthコールバック処理
  4. 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.json
references/
security-checklist.md
rules/
electron-base.md
templates/
electron-builder.json
main.ts
preload.ts
vite.config.ts
templates/ipc-handlers/
auth.ts
store.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 のスタイリング向け

コメント