Electron + Webアプリ(Nuxt.js)でMonorepoな環境を作る

## はじめに

Nuxt.jsとElectronでWebとデスクトップアプリを両立させる方法として、Monorepoとして開発していくのがいいのではないかと思い、環境構築をしてみました。
その手順を残しておきたいと思います。

この記事も参考になると思うので、是非合わせて読んでみてください！
[Electron + Nuxt.js + TypeScriptの環境構築](https://qiita.com/282Haniwa/items/a3b0a7d3c622ad82ac8d)

この記事のコードのリポジトリは[こちら](https://github.com/282Haniwa/nuxt-electron-monorepo-example)です。

## ディレクトリ構成

以下のようなディレクトリ構成で構築するため、最初に設定を行います。

```
.
├── packages
│   ├── app
│   │   └── App(Electron)のプロジェクト
│   └── web
│       └── Web(Nuxt.js)のプロジェクト
├── package.json
└── yarn.lock
```

まずは、プロジェクトのディレクトリを作ります。

```shell-session
$ midir nuxt-electron-monorepo-example
$ cd nuxt-electron-monorepo-example
$ mkdir packages
$ mkdir packages/app
$ mkdir packages/web
```

`yarn init`をして`package.json`を生成し、`package.json`にmonorepo用のコンフィグを書き足します。

```shell
$ yarn init
```

```json:package.json
{
  ...
  "workspaces": {
    "packages": [
      "packages/app",
      "packages/web"
    ],
    "nohoist": [
      "**"
    ]
  }
}
```

この設定の意味は、`packages/app`と`packages/web`をそれぞれ別のプロジェクトとして扱い、`nohoist`を`"**"`に設定することで、`node_modules`を巻き上げないようにしています。
本来、`node_modules`はルートディレクトリに巻き上げられてそれぞれのプロジェクトではそれを参照するようになるのですが、問題が起こりやすい(ように感じる)ので今回はこのような設定にしています。
本来、monorepoではhoistが働くことで`node_modules`が削減されるなどのメリットがあります。


## Nuxt.jsのプロジェクトを作る

新しくNuxtのプロジェクトを作る場合は、`create-nuxt-app`を使って作ってください。
今回は、以下のような設定でプロジェクトを作っているものとして、進めていきますが、必要に合わせて読み替えながら進めてください。

既にあるプロジェクトをElectronに移植する場合は、プロジェクトの中身を全て`packages/web`ディレクトリの中に移動してください。
**※ `.git`ディレクトリや、`.github`ディレクトリ、CI/CD用の設定ファイルなどは移動しないように注意してください。**

```shell-session
$ cd packages/web
$ yarn create nuxt-app .
create-nuxt-app v3.3.0
✨  Generating Nuxt.js project in .
? Project name: web
? Programming language: TypeScript
? Package manager: Yarn
? UI framework: (Use arrow keys)
? UI framework: 
? UI framework: Vuetify.js
? Nuxt.js modules: (Press <space> to select, <a> to toggle all, <i>
 to invert selection)
? Linting tools: ESLint, Prettier, Lint staged files, StyleLint
? Testing framework: Jest
? Rendering mode: Single Page App
? Deployment target: Static (Static/JAMStack hosting)
? Development tools: (Press <space> to select, <a> to toggle all, <
i> to invert selection)
? Version control system: None
```

この時に、`package.json`の`name`プロパティがパッケージ名(`web`)と同じになるようにしてください。
また、`Version control system`はNoneを選んでおきましょう。


## Electronのプロジェクトを作る

`packages/app`のなかにElectronのプロジェクトを作っていきます。

```shell-session
$ cd packages/app
```

### Electron関係のパッケージを追加する

必要なパッケージを追加していきます。

```
$ yarn add --dev \
  electron  \
  electron-builder \
  electron-devtools-installer \
  typescript \
  @types/electron-devtools-installer
```

ビルド時に使うので`typescript`と`cross-env`を入れておきます。

```
$ yarn add --dev \
  typescript \
  cross-env
```

ついでに`eslint`と`husky`, `lint-staged`も入れておきましょう

```
$ yarn add --dev \
  eslint \
  @typescript-eslint/eslint-plugin \
  prettier \
  eslint-config-prettier \
  eslint-plugin-prettier \
  husky \
  lint-staged
```

後で使うので、`npm-run-all`も入れておきます。

```shell-session
$ yarn add --dev npm-run-all
```

### Electronのスクリプトを用意する

長くなるので、ここは省略します。
[pacages/app/src/main](https://github.com/282Haniwa/nuxt-electron-monorepo-example/tree/master/packages/app/src/main)のスクリプトを参考にして実装してください。

`tsconfig.json`の設定で`../../dist`に成果物を出力するようにしているのがポイントです。

### rendererのシンボリックリンクを貼ってビルドできるようにする

`packages/app/renderer`が`packages/web`を指すようにシンボリックリンクを貼ります。

```shell-session
$ cd packages/app/src
$ ln -s ../../web renderer
```

### Nuxtのプロジェクトのコンフィグを修正する

cross-evnでコンフィグを書き換えるためにenvの設定を作ります。

[packages/web/env](https://github.com/282Haniwa/nuxt-electron-monorepo-example/tree/master/packages/web/env)

そして、`nuxt.config.js`をapp用のコンフィグとweb用のコンフィグをenvの値によって変えられるように記述します。

```js:packages/web/nuxt.config.js
const appConfig = {
  ...baseConfig,
  // ssr: false,
  // target: 'static',
  dev: isDev,
  server: {
    host: env.NUXT_HOST,
    port: env.NUXT_PORT,
  },
  /*
   ** Electronではhashモードじゃないとだめ
   ** https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/commonIssues.html#blank-screen-on-builds-but-works-fine-on-serve
   */
  router: {
    ...baseConfig.router,
    base: undefined,
    mode: 'hash',
  },
  build: {
    ...baseConfig.build,
    extend(config, ctx) {
      baseConfig.build?.extend && baseConfig.build?.extend(config, ctx)
      if (!isDev) {
        // absolute path to files on production (default value: '/_nuxt/')
        // @ts-ignore
        config.output.publicPath = '_nuxt/'
      }
      config.target = 'web'
      config.node = {
        // eslint-disable-next-line @typescript-eslint/naming-convention
        __dirname: !isProduction,
        // eslint-disable-next-line @typescript-eslint/naming-convention
        __filename: !isProduction,
      }
    },
  },
  loaders: {
    ts: {
      loaderOptions: {
        compileOptions: {
          target: 'es5',
          module: 'commonjs',
        },
      },
    },
  },
  generate: {
    ...baseConfig.generate,
    dir: '../../packages/app/dist/nuxt-build',
  },
  telemetry: false,
}

const config = isApp ? appConfig : baseConfig

export default config
```

[packages/web/nuxt.config.js](https://github.com/282Haniwa/nuxt-electron-monorepo-example/tree/master/packages/web/nuxt.config.js)

このように、ビルド時のenvによってコンフィグを切り替えられるようにします。

### `scripts`を修正する

そして、ビルドできるようにwebとappの`package.json`を編集します。
[packages/app/package.json](https://github.com/282Haniwa/nuxt-electron-monorepo-example/tree/master/packages/app/package.json)と[packages/web/package.json](https://github.com/282Haniwa/nuxt-electron-monorepo-example/tree/master/packages/web/package.json)を参考にしてください。

webの方でも`cross-env`を使うので、忘れずに追加しておいてください。

```shell-session
$ cd packages/web
$ yarn add --dev cross-env
```

## ルートのpackage.jsonを編集する

ルートから簡単にdevやbuildを実行できるように`scripts`を編集します。

```json:package.json
{
  ...
  "scripts": {
    "dev:app": "yarn workspace app dev",
    "dev:web": "yarn workspace web dev",
    "build:app": "yarn workspace app build",
    "build:web": "yarn workspace web build"
  },
  ...
}
```

これで環境構築は終了です！

## まとめ

今回、Nuxt.jsで作成したプロジェクトをElectronに移植するニーズがあり、試行錯誤しながら環境構築をしていました。
せっかく苦労して作り上げたので、もし同じ問題で困っている人がいれば助けになれば良いかなぁと思います！

もっといい方法があるよって人は是非コメントで教えてください！
最後まで読んでいただきありがとうございました！！