mcpField
E
electron-dev

electron-dev

github.com/jamditis開発438307 回インストール4 日前に更新
リポジトリ

electron-dev について

React と TypeScript を使った本番向け Electron アプリ開発のスキルです。安全な IPC やパッケージングに加え、デスクトップ機能の実装パターンを扱います。

主な機能

  1. 01Electron のセキュリティ設定
  2. 02contextBridge による型付き IPC
  3. 03システムトレイとグローバルショートカット
  4. 04PTY ターミナルと音声のパターン
  5. 05WebRTC とパッケージングのガイド

活用シーン

  1. 01Electron のセキュリティと IPC 設計の確認
  2. 02React と TypeScript によるデスクトップアプリ開発
  3. 03トレイ、ショートカット、ターミナル機能の追加

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/jamditis/claude-skills-journalism --skill electron-dev

インストール後、Claude Code で/electron-devを入力して読み込まれたか確認します。

同梱ファイルagents/openai.yaml

詳細

Electron デスクトップ開発

React と TypeScript を使用して、本番環境に適した Electron アプリケーションを構築するためのパターンとプラクティスです。

セキュリティの基本設定(Electron 30+)

Electron のデフォルト設定は、ここ数回のリリースで強化されています。Electron 28+ では、新しい BrowserWindow インスタンスの contextIsolation: true と sandbox: true がデフォルトになっています。以前のガイドにあるセキュリティに関するアドバイスの多くは、これらを有効にする必要があることを前提としていました。現在はその必要はありません。無効にする必要がありますが、無効にしないでください。

設定のずれによってセキュリティモデルが弱まらないよう、明示的に設定してください。

const win = new BrowserWindow({
  webPreferences: {
    contextIsolation: true,        // default since 12, mandatory for any prod app
    sandbox: true,                  // default since 28; renderer runs sandboxed
    nodeIntegration: false,         // never enable in renderer
    webSecurity: true,              // never disable
    preload: path.join(__dirname, 'preload.cjs')
  }
});

すべての IPC メッセージをメインプロセスで検証してください。レンダラーを信用してはいけません。

Electron Fuses と ASAR の整合性

Electron Fuses は、バイナリに組み込まれるパッケージ作成時の切り替え設定です。セキュリティを目的とした配布に関連するものは、次の 2 つです。

  • EnableEmbeddedAsarIntegrityValidation は、実行時に app.asar のハッシュをバイナリに埋め込まれたハッシュと照合します。インストール後に攻撃者が asar の内容を差し替えるのを防ぎます。
  • OnlyLoadAppFromAsar は、検証済みの asar 以外からアプリコードを読み込むことを拒否します。

これらはデフォルトではなく、オプトインです。本番環境では両方を有効にしてください。埋め込み可能な整合性情報を含む asar を生成するには、@electron/asar 3.1.0+ が必要です。electron-builder ではビルド設定の electronFuses で設定でき、@electron/fuses ではプログラムから設定できます。

CVE-2023-44402(ファイルタイプの混同を利用した ASAR の整合性検証の回避)が、これらが必要とされた代表的な理由です。整合性検証と asar 以外からの読み込み制限がなければ、アプリファイルを変更できる攻撃者が、気付かれずに動作を差し替えることができます。

レンダラー側でよくあるリスク
  • プリロードスクリプトの取り扱いの誤り:contextBridge.exposeInMainWorld を使って、範囲を絞った型付きのインターフェースのみを公開してください。ipcRenderer 自体を再エクスポートしてはいけません。特定のチャネルに対応する個別のメソッドを公開してください。
  • file:// IPC とナビゲーション:URL を変更させたくないウィンドウでは、webContents.on('will-navigate', e => e.preventDefault()) を使ってナビゲーションを制限してください。デフォルトで setWindowOpenHandler のリクエストを拒否し、許可リストに登録したオリジンのみを許可してください。
  • ユーザー入力を使った shell.openExternal:URL を開く前に、スキームを検証してください。攻撃者が制御する file:// や javascript: の URL によって、攻撃者にコード実行を許してしまいます。

アーキテクチャのパターン

プロジェクト構成
app/
├── electron/
│   ├── main.cjs              # Main process (CommonJS required)
│   ├── preload.cjs           # Context bridge for secure IPC
│   └── server.cjs            # Optional: WebSocket/HTTP server
├── src/
│   ├── components/           # React components
│   ├── services/             # Business logic (API clients, Firebase)
│   ├── utils/                # Utilities (audio, formatting)
│   ├── types.ts              # TypeScript interfaces
│   ├── App.tsx               # Root component
│   └── index.tsx             # React entry
├── assets/                   # Icons, sounds, images
├── package.json
├── vite.config.ts
└── electron-builder.yml      # Build configuration
IPC 通信パターン

メインプロセス(main.cjs):

const { ipcMain } = require('electron');

// Handle async requests from renderer
ipcMain.handle('action-name', async (event, args) => {
  try {
    const result = await someAsyncOperation(args);
    return { success: true, data: result };
  } catch (error) {
    return { success: false, error: error.message };
  }
});

// Send data to renderer
mainWindow.webContents.send('event-name', data);

プリロードスクリプト(preload.cjs):

const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electron', {
  actionName: (args) => ipcRenderer.invoke('action-name', args),
  onEventName: (callback) => {
    const handler = (event, data) => callback(data);
    ipcRenderer.on('event-name', handler);
    return () => ipcRenderer.removeListener('event-name', handler);
  }
});

レンダラー(React):

const result = await window.electron.actionName(args);

useEffect(() => {
  return window.electron.onEventName((data) => {
    setState(data);
  });
}, []);

システムトレイとの連携

const { Tray, Menu, nativeImage } = require('electron');

let tray = null;

function createTray() {
  const icon = nativeImage.createFromPath(path.join(__dirname, '../assets/tray-icon.png'));
  tray = new Tray(icon.resize({ width: 16, height: 16 }));

  tray.setToolTip('App Name');
  tray.setContextMenu(Menu.buildFromTemplate([
    { label: 'Show', click: () => mainWindow.show() },
    { label: 'Quit', click: () => app.quit() }
  ]));

  tray.on('click', () => {
    mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show();
  });
}

// Hide to tray instead of closing
mainWindow.on('close', (event) => {
  if (!app.isQuitting) {
    event.preventDefault();
    mainWindow.hide();
  }
});

グローバルショートカット

const { globalShortcut } = require('electron');

app.whenReady().then(() => {
  // Register with conflict detection
  const registered = globalShortcut.register('Alt+S', () => {
    mainWindow.webContents.send('shortcut-triggered', 'toggle-recording');
  });

  if (!registered) {
    console.error('Shortcut registration failed - conflict detected');
  }
});

app.on('will-quit', () => {
  globalShortcut.unregisterAll();
});

PTY ターミナルとの連携(node-pty)

const pty = require('node-pty');

const shell = process.platform === 'win32' ? 'powershell.exe' : process.env.SHELL || '/bin/bash';

const ptyProcess = pty.spawn(shell, [], {
  name: 'xterm-256color',
  cols: 80,
  rows: 24,
  cwd: process.env.HOME,
  env: process.env
});

ptyProcess.onData((data) => {
  mainWindow.webContents.send('terminal-data', { tabId, data });
});

ipcMain.on('terminal-write', (event, { tabId, data }) => {
  ptyProcess.write(data);
});

ipcMain.on('terminal-resize', (event, { tabId, cols, rows }) => {
  ptyProcess.resize(cols, rows);
});

音声録音のワークフロー

// Request microphone access
const stream = await navigator.mediaDevices.getUserMedia({
  audio: {
    echoCancellation: true,
    noiseSuppression: true,
    autoGainControl: true
  }
});

// Record audio
const mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm' });
const chunks: Blob[] = [];

mediaRecorder.ondataavailable = (e) => chunks.push(e.data);
mediaRecorder.onstop = async () => {
  const blob = new Blob(chunks, { type: 'audio/webm' });
  const base64 = await blobToBase64(blob);
  // Send to transcription API
};

mediaRecorder.start();
// Later: mediaRecorder.stop();

WebRTC のパターン(PeerJS)

import Peer from 'peerjs';

const peer = new Peer(userId, {
  host: 'peerjs-server.com',
  port: 443,
  secure: true
});

// Answer incoming calls
peer.on('call', (call) => {
  call.answer(localStream);
  call.on('stream', (remoteStream) => {
    audioElement.srcObject = remoteStream;
  });
});

// Make outgoing calls
const call = peer.call(remoteUserId, localStream);
call.on('stream', (remoteStream) => {
  audioElement.srcObject = remoteStream;
});

// Screen sharing via replaceTrack (no renegotiation)
const screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true });
const videoTrack = screenStream.getVideoTracks()[0];
const sender = peerConnection.getSenders().find(s => s.track?.kind === 'video');
await sender.replaceTrack(videoTrack);

ビルド設定(electron-builder.yml)

appId: com.yourname.appname
productName: AppName
directories:
  output: release

win:
  target:
    - target: nsis
      arch: [x64]
  icon: assets/icon.ico

nsis:
  oneClick: false
  allowToChangeInstallationDirectory: true
  installerIcon: assets/icon.ico
  uninstallerIcon: assets/icon.ico

mac:
  target:
    - target: dmg
      arch: [x64, arm64]
  icon: assets/icon.icns
  hardenedRuntime: true
  gatekeeperAssess: false
  entitlements: build/entitlements.mac.plist
  entitlementsInherit: build/entitlements.mac.plist
  notarize:
    teamId: YOUR_APPLE_TEAM_ID

linux:
  target:
    - target: AppImage
      arch: [x64]
  icon: assets/icon.png

publish:
  provider: github
  owner: username
  repo: repo-name

extraResources:
  - from: "node_modules/node-pty/build/Release/"
    to: "node-pty/"
    filter: ["*.node"]

macOS の公証は、App Store 外で配布するために必要です。Gatekeeper は、公証されていないアプリを初回起動時にブロックします。npm run package を実行する前に、環境変数 APPLE_ID、APPLE_APP_SPECIFIC_PASSWORD、APPLE_TEAM_ID を設定してください(または App Store Connect API キーを使用してください)。electron-builder ≥ 24.13 は mac.notarize フィールドを使った公証をネイティブで処理します。これより古いバージョンでは electron-notarize の afterSign フックが必要です。

Windows では、SmartScreen の警告を避けるために、EV 証明書によるコード署名がますます必要になっています。electron-builder は、環境変数 CSC_LINK(PFX)と CSC_KEY_PASSWORD を読み取ります。

よくある落とし穴

コールバック内の古いクロージャー:

// Problem: State is stale in async callbacks
const [state, setState] = useState(initialValue);
peer.on('call', () => {
  console.log(state); // Always shows initialValue
});

// Solution: Use refs for async callback access
const stateRef = useRef(state);
useEffect(() => { stateRef.current = state; }, [state]);
peer.on('call', () => {
  console.log(stateRef.current); // Current value
});

コンテキスト分離によるセキュリティ:

  • ipcRenderer をレンダラーに直接公開しないでください
  • 常に contextBridge.exposeInMainWorld() を使用してください
  • メインプロセスですべての IPC 引数を検証してください
  • IPC コントラクトには TypeScript インターフェースを使用してください

BrowserView は非推奨です。WebContentsView を使用してください:

BrowserView は Electron 30(2024 年 4 月)で非推奨となり、基盤となる実装は置き換えられました。BrowserView は WebContentsView 上の互換性シムとして引き続き動作しますが、新しいコードでは WebContentsView を直接使用してください。コンストラクターは同じ webPreferences の形式を受け取るため、移行のほとんどは機械的に行えます。知っておくべき違いは次のとおりです。

  • WebContentsView は win.addBrowserView(view) ではなく win.contentView.addChildView(view) で追加します
  • サイズ設定には view.setBounds({x, y, width, height}) を使用し、setAutoResize はありません。自動サイズ変更を行う場合は、独自のリサイズハンドラーを設定してください。
  • Z オーダーは addChildView の呼び出し順です。前面に表示するには、removeChildView を呼び出してから再度 addChildView を呼び出します。
const { WebContentsView } = require('electron');

const view = new WebContentsView({
  webPreferences: { contextIsolation: true, sandbox: true }
});
view.webContents.loadURL('https://example.com');
mainWindow.contentView.addChildView(view);
view.setBounds({ x: 0, y: 80, width: 800, height: 520 });

特殊なケース(ポップアップ、devtools、フォーカス管理)については、公式の BrowserView → WebContentsView 移行ガイドを参照してください。

クロスプラットフォームのシェル検出:

const shell = process.platform === 'win32'
  ? 'powershell.exe'
  : process.env.SHELL || '/bin/bash';

const shellArgs = process.platform === 'win32'
  ? ['-NoLogo']
  : [];

開発ワークフロー

# Development (hot reload)
npm run electron:dev

# Production build
npm run electron:build

# Run built app locally
npx electron dist/

# Package for distribution
npm run package

コメント