carlo + parcelでお手軽デスクトップアプリケーション

ウェブ技術でデスクトップアプリを開発するとなると、一番メジャーなのはやはり[electron/electron](https://github.com/electron/electron)でしょう。ところがElectronは割と面倒です。配布ファイルのサイズ、セキュリティなど。

それとバンドラとして有名なWebpackですが、こっちも仰々しいです。

そこで今回はライトウェイトでゼロコンフィグな、carloとparcelの組み合わせについて書きます。

## carlo

[GoogleChromeLabs/carlo](https://github.com/GoogleChromeLabs/carlo)は先日登場したばかりの headful Node app framework です。headlessならぬheadfulです。

中身はぶっちゃけると [puppeteer-core](https://www.npmjs.com/package/puppeteer-core) の薄いラッパーです。

普通のPuppeteerはオープンソースのChromiumをダウンロードしてきて動作しますが、puppeteer-coreは既にローカルにインストールされているChromeなどを対象に動くものです。それゆえにElectronほどの暴力的な容量を食いません。

使い方は、carloのREADME.mdを読めば速攻で理解できると思います。[carlo/API.md](https://github.com/GoogleChromeLabs/carlo/blob/master/API.md)なんかも一瞬で読めてしまう程度の分量です。

```js
const carlo = require('carlo')

const bootstrap = async () => {
  const app = await carlo.launch()
  app.serveFolder('./dist')
  await app.load('index.html')
}

bootstrap()
```

carloではElectron同様にあるディレクトリのHTMLファイルをpuppeteer-core経由でアプリケーションっぽい状態にして起動します。この例では、dist/index.htmlを読み込んでいます。

これは、Electronでいうところのレンダラプロセスに該当するものですが、Electronとは違い、Node.jsが組み込まれていないため、ブラウザで動く範囲のコードしか動きません。

それだとさすがにアプリを作るには不便なので、Node.js側と連動するための仕組みが提供されています。

```js
  await app.exposeFunction('cwd', () => process.cwd())
```

たとえばこのように`app.exposeFunction`を叩くとNode.jsの関数をブラウザ側にグローバル変数 `cwd` としてPromiseが晒されます。

```js
cwd().then(s => console.log(s))
```

## parcel

[parcel-bundler/parcel](https://github.com/parcel-bundler/parcel)は、Blazing fast, zero configuration web application bundler で、要するに設定いらずで動作も速いバンドラです。

どれくらい設定いらずかというと、parcelにHTMLファイルを渡せば、必要なJS, CSSなどすべてのファイルを勝手によしなにしてくれるというものです。

たとえば、`index.html`から`index.ts`を読み込んでいると、インストールされていなければ勝手にTypeScriptをインストールしてコンパイルまでしてくれます。

Parcelは[API](https://parceljs.org/api.html)を叩くことで、JavaScriptのコードで、バンドルできます。

```js
const Bundler = require('parcel-bundler')

const bundle = async entryFiles => {
  const opts = {
    outFile: 'index.html'
  }

  const bundler = new Bundler(entryFiles, opts)
  await bundler.bundle()
}
```

あとは`bundle('src/app/index.html')`のように呼び出せばバンドル処理が走ってくれます。あとはせいぜいオプションを足す位です。

## carlo + parcel

carloで作ったアプリは普通のブラウザ+αで動くので、ブラウザ向けにバンドルする必要があります。そこで、parcelと組み合わせるのです。

まずはcarloとparcelをインストールします。今回はyarnを使います。

```shell
yarn init -y
yarn add carlo parcel-bundler
```

carloもparcel-bundlerも両方、devじゃない方でインストールしています。もちろん事前にビルドすることにすれば、parcel-bundlerはdevでインストールすればいいです。

さて、carloもparcelもモダンなプロダクトなので、Promiseを活用しています。そこで起動コードは async/await を使って書きます。

```js
// src/index.js
const path = require('path')

const carlo = require('carlo')
const Bundler = require('parcel-bundler')

const bootstrap = async () => {
  const outDir = path.join(__dirname, '..', 'dist')
  const entryFile = path.join(__dirname, 'app', 'index.html')
  const opts = {
    outDir,
    outFile: 'index.html',
    sourceMaps: true,
    hmr: false
  }

  const bundler = new Bundler(entryFile, opts)
  await bundler.bundle()
  const cApp = await carlo.launch()
  await cApp.exposeFunction('cwd', () => process.cwd())
  cApp.serveFolder(outDir)
  await cApp.load('index.html')
}

bootstrap()
```

現時点のcarloではWebSocketが制御されてないためHMRが動かないのでオフにしています。carloは薄いラッパーに過ぎないため、直接puppeteer-coreを叩いてWebSocketに対応すればHMRも動くはずです。

```html
<!-- src/app/index.html -->
<html>
  <body>
    <div id="root"></div>
    <script src="./index.ts"></script>
  </body>
</html>
```

```js
// src/app/index.ts
const render = async () => {
  const root = document.getElementById('root')
  root.textContent = await (window as any).cwd()
}

render()
```

cwdはシステムからは認識されていないグローバル変数であるためいったん window を any 扱いしています。

## 注意点

Parcelは`.cache` というディレクトリを生成し、Carloは `.profile` というディレクトリを生成します。これらは `.gitignore` に追加しておく必要があります。

あと、TypeScriptをインストールせずにParcelに `.ts` ファイルを喰わせると、devで追加してしまうので、今回のようにアプリそのものに組み込みたい場合は、横着せずに手動でインストールするべきです。

## まとめ

* Carlo は既にインストールされてるChrome/Chromiumを有効活用するため、超絶軽量なデスクトップアプリを作れる
* Parcel は設定いらずのバンドラ
* Carlo と Parcel を組み合わせれば、お手軽にデスクトップアプリ開発できる

[erukiti/sample-carlo-parcel](https://github.com/erukiti/sample-carlo-parcel) が今回のサンプルリポジトリです。

## 参考

* [CarloでNode.jsのツールにGUIをもたせる - Qiita](https://qiita.com/Quramy/items/d38b26a7c0007e757353)