create-react-appとtypescriptでelectronをやる

# 2020/08/28 追記
electron-webpackを使った新作を書きました。
[electron-webpackでTypescript+Reactプロジェクトを作成する](https://qiita.com/thalathalaylah/items/25831909d1488eef49dd)

#背景
- create-react-appは便利
    - typescript対応もしている
    - webpack書かなくて良い
- electronは便利
    - フロントエンドの表現力
    - ローカルへのアクセス

ということで適当に足したら爆発したため、まとめていきます。

動くプロジェクトはこれです
[thalathalaylah/create-react-electron-typescript-app](https://github.com/thalathalaylah/create-react-electron-typescript-app)

記事の構成としては、
[1.やること](#1やること)
[2.やったこと](#2やったこと)
[3.なぜなのか](#3なぜなのか)
の3部構成です。何をどうしたかだけ気になる方はやったことだけ読んでいただければ。

#参考文献
- [Building an Electron application with create-react-app](https://www.freecodecamp.org/news/building-an-electron-application-with-create-react-app-97945861647c/)
    - electron + create-react-app(not typescript)によるアプリ構築
- [React+Electronアプリを作ってみよう](https://dev.to/origamium/create-reactelectron-application-in-quickly--36nl)
    - 上記の記事の日本語訳+パッケージング周りの設定

#1.やること
- create-react-appから作成したプロジェクトにelectronを組み込む
- Nodeの能力が利用できることを示すために「ボタンを押すとファイルを生成する」機能を実装する
- ReactDevToolsを利用可能にする

#2.やったこと
コミットのリンクと、コミットでやったことを書いていきます
###[Initial commit from Create React App](https://github.com/thalathalaylah/create-react-electron-typescript-app/commit/dd3467458567e29c3b1dd6797839ea3c9da0fbac)
1. `yarn create react-app <project name> --typescript` でreact+typescriptのプロジェクトを作成

###[[add] electronからcreate-react-appのページを表示できる](https://github.com/thalathalaylah/create-react-electron-typescript-app/commit/7a30ff31ef1a7f4b505e04b3f87e0ddf593de61b)
1. [electron-quick-start](https://github.com/electron/electron-quick-start)のmain.jsを`src_main/entrypoint.js`にコピー
1. `src_main/entrypoint.js`のBrowserWindowを生成している箇所で、webPreferenceを空にする
1. `src_main/entrypoint.js`の`mainWindow.loadFile('index.html')`を`mainWindow.loadURL('http://localhost:3000')`に変更
1. `package.json`に`"main": "src_main/entrypoint.js"`を追加
1. `package.json`のscriptsに`"electron": "electron ."`を追加
1. `yarn add --dev electron`でelectronを追加
1. この時点で`yarn start`が起動している状態で`yarn electron`を実行するとelectronでcreate-react-appのページが表示できる

###[[add] packageコマンドでappを生成できる](https://github.com/thalathalaylah/create-react-electron-typescript-app/commit/96136cab89a3165367b0bafe6c62425b72fb4e4d)
1. コミットの内容ではないが、Macの場合は`brew install wine`しておく
1. `src_main/entrypoint.js`に`app.isPackaged`によって`mainWindow.loadURL`の読み込み対象をurlかpathか分岐させる処理を追加
1. `package.json`に`"homepage": "./"`を追加
1. `.gitignore`に`/release`を追加
1. `package.json`のscriptsに`"package": "yarn build && electron-packager . my-app --platform=all --arch=x64 --prune --out=release --overwrite"`を追加
1. この時点で`yarn package`によってパッケージを生成し、`yarn start`を**行わずに**生成されたパッケージからアプリを起動できる

###[[add] packageコマンドではMac用のビルドのみ行う](https://github.com/thalathalaylah/create-react-electron-typescript-app/commit/e4f3468a5a9f177626208b799eae4e94ea405eed)
1. `package.json`のscriptsで`package`コマンドを`package-all`にリネーム、`package`コマンドはplatformを`darwin`のみ指定するようにした

###[[add] devコマンドからサーバー起動とelectron起動を同時に行うことができる](https://github.com/thalathalaylah/create-react-electron-typescript-app/commit/6c12ce49e58f1f387d4e59dff05cb0745c2d3828)
1. `yarn add --dev foreman`でforemanを追加
1. `dev_env/Procfile`を追加
1. `dev_env/Procfile`から読む`dev_env/electron-wait-react.js`を追加
1. `package.json`のscriptsに`"dev": "nf start -j dev_env/Procfile"`を追加
1. `src_main/entrypoint.js`で文字列でURLを指定している部分を環境変数から読むように変更

###[[add] ReactDevToolsの追加](https://github.com/thalathalaylah/create-react-electron-typescript-app/commit/62337f5e1d94ed8254fcbffbdb85748580c82502)
1. 手元のChromeにReactDevTools extensionをインストール [ReactDevTools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi)
1. `dev_env/.dotenv`を作成し、`ELECTRON_DEV_TOOLS_PATH`という環境変数を定義し、インストールしたReactDevToolsのパスを設定(コミット上ではtemplateとして追加している)
1. `package.json`のdevコマンドが`dev_env/.dotenv`を読み込めるように変更
1. `app.isPackaged`がfalseの時にELECTRON_DEV_TOOLS_PATHからReactDevToolsを読むように変更
1. `app.isPackaged`がfalseの時は最初から開発者ツールが開くように設定、付随してウィンドウサイズも大きくする

###[[add] ボタン作成](https://github.com/thalathalaylah/create-react-electron-typescript-app/commit/0275a0be092c605be707d8d4c6e979eb7a616701)
1. `src/CreateFileButton.tsx`を作成
1. CreateFileButtonを`src/App.tsx`から読み込む
1. この時点でelectronアプリのページの下方にクリック可能なボタンが生成されるようになる

###[[add] uuidで作成するファイル名を生成](https://github.com/thalathalaylah/create-react-electron-typescript-app/commit/ed97555e1f1fb478c9941662b33f5d283d3f06ac)
1. `yarn add --dev uuid @types/uuid`
1. `src/CreateFileButton.tsx`でuuidによってファイル名を生成し、`console.log`で出力するように変更
1. `window.require`でelectronを読み込めるように`declare global`でWindow interfaceにrequireを追加
1. electronからRemoteをimport
1. `window.require('electron').remote`でRemote型のオブジェクトであるremoteを取得

###[[add] ボタンを押すとファイルが生成される](https://github.com/thalathalaylah/create-react-electron-typescript-app/commit/4f822694312aad089c982c1159e586b13966ccaf)
1. `.gitignore`に`/test`を追加
1. `fs`と`path`を`fsType`と`pathType`としてimport
1. remoteから`fs`と`path`をrequireし、`typeof fsType`と`typeof pathType`で型付けする
1. ファイルを生成する処理を書く

これで無事、ボタンを連打するとファイルが大量に生成されるアプリが完成しました！

#3.なぜなのか
###[Initial commit from Create React App](https://github.com/thalathalaylah/create-react-electron-typescript-app/commit/dd3467458567e29c3b1dd6797839ea3c9da0fbac)
特筆することは無いです、スッと作る

###[[add] electronからcreate-react-appのページを表示できる](https://github.com/thalathalaylah/create-react-electron-typescript-app/commit/7a30ff31ef1a7f4b505e04b3f87e0ddf593de61b)
ここは[Building an Electron application with create-react-app](https://www.freecodecamp.org/news/building-an-electron-application-with-create-react-app-97945861647c/)とほぼ同じですが、typescriptを使うために一部調整しています。
まず、webPreferenceを空にしているのは[electron-quick-start](https://github.com/electron/electron-quick-start)にpreload.jsを読み込む仕組みが組み込まれたためです。
今回のアプリにはpreloadする処理は無いため、preloadの処理を消しています。
また、`entrypoint.js`を`src`に入れず`src_main`に入れています。これは、src下のファイルはcreate-react-app内部で設定されたwebpackの支配下にあるため、適当にjsを入れておくとエラーが発生してしまうためです。

###[[add] packageコマンドでappを生成できる](https://github.com/thalathalaylah/create-react-electron-typescript-app/commit/96136cab89a3165367b0bafe6c62425b72fb4e4d)
`brew install wine`はWindows用のビルドのために必要となっています。
`app.isPackaged`はdev環境か否かの判別に使っています。Package化している場合はProduction、Package化していない場合はDevelopという判定です。
`package.json`に`"homepage": "."`を加えるのは[Building an Electron application with create-react-app](https://www.freecodecamp.org/news/building-an-electron-application-with-create-react-app-97945861647c/)の通りで、これが無いとpackage化した際に`index.html`から他のファイルが読めなくなります。
`package`コマンドの内容は[React+Electronアプリを作ってみよう](https://dev.to/origamium/create-reactelectron-application-in-quickly--36nl)の通りです。

###[[add] packageコマンドではMac用のビルドのみ行う](https://github.com/thalathalaylah/create-react-electron-typescript-app/commit/e4f3468a5a9f177626208b799eae4e94ea405eed)
electronの`v5.0.3`で試したところ、`package`コマンドにかかる時間が`v5.0.1`と比べて2倍くらい長くなったため開発中はMacのみをターゲットにビルドすることにしました（利用しているOSを対象にすると良いでしょう）。

###[[add] devコマンドからサーバー起動とelectron起動を同時に行うことができる](https://github.com/thalathalaylah/create-react-electron-typescript-app/commit/6c12ce49e58f1f387d4e59dff05cb0745c2d3828)
[Building an Electron application with create-react-app](https://www.freecodecamp.org/news/building-an-electron-application-with-create-react-app-97945861647c/)の通りです。
[React+Electronアプリを作ってみよう](https://dev.to/origamium/create-reactelectron-application-in-quickly--36nl)では`npm-run-all`を使うと良い、と書いてありますが、`yarn start`が起動し終わるのを待たずに`yarn electron`が走ってしまうため、electron側をリロードせねばならないのが良くないと思いました。
そのため、今回はforemanの方を採用しています。

###[[add] ReactDevToolsの追加](https://github.com/thalathalaylah/create-react-electron-typescript-app/commit/62337f5e1d94ed8254fcbffbdb85748580c82502)
[DevTools Extension](https://electronjs.org/docs/tutorial/devtools-extension)にある通りです。
このアプリでは先述の通り、`app.isPackaged`でdev、prodを区別しているため、`app.isPackaged`がfalseの場合のみ有効にしています。
ここで、devでReactDevToolsを有効にした場合、有効にしたことが`~/LibraryApplication Support/<App名>`に記録されるのですが、package化したprodも同じ場所を参照するため、prodでも有効なままになります。
同ディレクトリを削除した状態でprod環境のものを起動した場合は、無効なまま正常に起動するのでご安心を。

###[[add] ボタン作成](https://github.com/thalathalaylah/create-react-electron-typescript-app/commit/0275a0be092c605be707d8d4c6e979eb7a616701)
シュッとボタン作るだけです。

###[[add] uuidで作成するファイル名を生成](https://github.com/thalathalaylah/create-react-electron-typescript-app/commit/ed97555e1f1fb478c9941662b33f5d283d3f06ac)
ここはuuidでファイル名を生成するというコミットメッセージになっていますが、うっかりelectronのremoteオブジェクトを取得するところまでやっています。
electronの能力にアクセスするためにelectronをimportしたいのですが、普通にimportを書いてしまうとtsからのトランスパイル時にcreate-react-app側のwebpackが解決しようとするため、失敗します。
これを回避するために、`window.require`というものがelectronでは定義されています。
しかし、create-react-app+typescriptの文脈ではwindowはWindowインターフェースを実装したものなので、`window.require('electron')`などと書いてもトランスパイル時に「`window.require`なんてメソッドは無いよ」と言われてしまいます。
そこで、

```typescript
declare global {
  interface Window {
    require: any;
  }
}
```
と書いてやることでrequireを呼べるようにします。
また、`window.require`は上に書いた通りanyが返ってしまうため、electronのRemoteの型情報はimportしておいて、型付けを行っています。

###[[add] ボタンを押すとファイルが生成される](https://github.com/thalathalaylah/create-react-electron-typescript-app/commit/4f822694312aad089c982c1159e586b13966ccaf)
ここからは`window.require`からではなく`remote.require`でnodeの機能を呼び出しています。
`window.require`でも呼び出せるのですが、[Breaking Changes](https://electronjs.org/docs/api/breaking-changes#require-in-sandboxed-renderers)を見た感じ`remote.require`から呼ぶ方が良いのかな、特にv6.0.0以降は、という感じです。
`fs`と`path`をimportしていますが、これは`remote.require`でこれらを取得した際に型をつけるためです。
型定義ファイルを見るとわかるのですが、`fs`と`path`はモジュールであってinterfaceとかではないので、typeofで無理やり型にして型付けをしています。

#コンクルージョン
これで無事electronからcreate-react-app+typescriptを利用することができるというわけです。
electron+create-react-appの場合の罠とelectron+typescriptの場合の罠が複合して出現したため厳しい気持ちになりましたが、これで型付きelectron生活を無事送ることができるようになりました。