PureScript + Halogen + Tailwind で Web フロント開発環境構築 (in VSCode)

# 概要

PureScript は活発に開発が行われているため、開発環境構築の方法を適宜まとめる必要があります。
この記事では PureScript のインストールから Web フロントエンドを開発できるところまでの環境構築をまとめます。

## 対象

- PureScript の環境構築を VSCode でしたい
- PureScript でフロントエンドの開発がしたい

## TL;DR

https://github.com/yukikurage/hello-purescript

にまとまってます

## 筆者の環境

細かくバージョン書いていますが多分気にしなくて大丈夫です。

- Windows 11
    - 22H2
- WSL2 
    - 5.15.68.1
- node 
    - 18.12.0
- npm 
    - 8.19.2

## PureScript のインストール

プロジェクトルートになるディレクトリを作って初期化します。

```shell
mkdir <project-name>
cd <project-name>
npm init -y
```

PureScript をインストールします

```shell
npm install --save-dev purescript
```

おおむね成功しますが、次のようにコケることがあります

```shell
...
npm ERR! [ FAILURE ] Check if 'stack' command is available
...
```

このあたりの事情にあまり詳しくないのですが、何回か `npm install purescript` をすると通ります。（それでいいのか？）
それでもダメなときは `stack` をインストールしてください。
`stack` は Haskell のプロジェクト / パッケージ管理ツールです。
詳しくはこちらから

https://docs.haskellstack.org/en/stable/

（結構インストールめんどくさいです）

## その他ツールチェインのインストール

- `spago`
    - プロジェクト / パッケージ管理ツール
- `purs-tidy`
    - フォーマッタ
- `esbuild`
    - `spago` が専らバンドラとして間接的に使う。

をインストールします

```
npm install --save-dev spago purs-tidy esbuild
```

## プロジェクトの初期化

```
npx spago init
npx spago run
```

```
...
[info] Build succeeded.
🍝
```

と出たら成功です！

## VSCode の拡張機能のインストールと設定

VSCode 拡張 PureScript IDE をインス―ルします

https://marketplace.visualstudio.com/items?itemName=nwolverson.ide-purescript

VSCode の設定に次を追加しておきます

```
  "purescript.formatter": "purs-tidy",
  "purescript.addNpmPath": true,
  "[purescript]": {
    "editor.formatOnSave": true,
  },
```

※ `formatOnSave` はお好みで。`addNpmPath` はローカルに purescript をインストールするなら必須です。

フォーマッタの設定をします

```
npx purs-tidy generate-config         
```

できた `.tidyrc.json` の JSON を書き換えます（これもお好みで）

```json
{
  "importSort": "ide",
  "importWrap": "source",
  "indent": 2,
  "operatorsFile": null,
  "ribbon": 1,
  "typeArrowPlacement": "first",
  "unicode": "source",
  "width": null
}
```

これでフォーマットや補完が効くようになったはずです。単に PureScript を使いたいのであれば、これで環境構築は終了となります

## Halogen を使う

Halogen は PureScript の SPA フレームワークです。PureScript で SPA を作るときは一番使われているでしょう。（統計はないですが）

まず Halogen をインストールします

```
npx spago install halogen
```

Main.purs を書き換えます

```haskell
module Main where

import Prelude

import Effect (Effect)
import Halogen (Component, defaultEval, mkComponent, mkEval)
import Halogen.Aff (awaitBody, runHalogenAff)
import Halogen.HTML as HH
import Halogen.VDom.Driver (runUI)

main :: Effect Unit
main = runHalogenAff do
  body <- awaitBody
  runUI component unit body

component :: forall q i o m. Component q i o m
component =
  mkComponent
    { initialState: \_ -> unit
    , render: \_ -> HH.div_ [ HH.text "Hello Halogen!" ]
    , eval: mkEval $ defaultEval
    }
```

バンドルします

```
npx spago bundle-app -y -t public/index.js
```

`public/index.html` ファイルを作ります

```haskell
<!DOCTYPE html>
<html>
  <head>
    <title>Halogen App</title>
    <script src="./index.js"></script>
  </head>
  <body></body>
</html>
```

これを読み込めば `Hello Halogen!` と表示されるはずです！

## Tailwind を使う

ちょっと面倒です

```
npm install --save-dev tailwindcss
npx tailwindcss init
```

`tailwind.config.js` を編集します

```javascript
{
  ...
  content: ["./src/**/*.{purs,html,js}"],
  ...
}
```

`src/index.css` を作成します。

```
@tailwind base;
@tailwind components;
@tailwind utilities;
```

VSCode の Tailwind CSS IntelliSense 拡張をインストールします

https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss

VSCode の設定に諸々の追加をして tailwind の補完が出るようにします（任意の文字列で出てしまうので、うざいときは切ってください）

```json
{
  "tailwindCSS.includeLanguages": {
    "purescript": ""
  },
  "[purescript]": {
    "tailwindCSS.experimental.classRegex": ["\"([^\"]*)\""],
  },
}
```

`Main.purs` をよしなに書き換えてビルドします

```haskell
module Main where

import Prelude

import Effect (Effect)
import Halogen (ClassName(..), Component, defaultEval, mkComponent, mkEval)
import Halogen.Aff (awaitBody, runHalogenAff)
import Halogen.HTML as HH
import Halogen.HTML.Properties as HP
import Halogen.VDom.Driver (runUI)

main :: Effect Unit
main = runHalogenAff do
  body <- awaitBody
  runUI component unit body

component :: forall q i o m. Component q i o m
component =
  mkComponent
    { initialState: \_ -> unit
    , render: \_ -> HH.div [ HP.class_ $ ClassName "w-screen h-screen text-5xl flex justify-center items-center" ] [ HH.text "Hello Halogen!" ]
    , eval: mkEval $ defaultEval
    }
```

```
npx spago bundle-app -y -t public/index.js
```

CSS ファイルをビルドします

```
npx tailwindcss -i ./src/index.css -o ./public/index.css
```

`index.html` から読み込みます

```html
  <head>
  ...
    <link rel="stylesheet" href="./index.css">
  ...
  </head>
```

これで `Hello Halogen!` が真ん中に移動します

## ホットリロードできるようにする

`live-server` を使います

```
npm install --save-dev live-server  
```

次のコマンドをそれぞれ別のターミナルで同時に動かします

```
npx live-server public     
```

```
npx spago bundle-app -y -t public/index.js -w 
```

```
npx tailwindcss -i ./src/index.css -o ./public/index.css -w
```

これで `localhost:8080` あたりを開くとファイルの変更がリアルタイムに反映される環境ができます

## コマンドをまとめる

`npm-run-all` を使います

```
npm install --save-dev npm-run-all
```

`package.json` の `script` を書き換えます

```json
  "scripts": {
    "bundle:script": "npx spago bundle-app -t ./public/index.js -y",
    "bundle:css": "tailwindcss -i ./src/index.css -o ./public/index.css -m",
    "bundle": "run-s bundle:*",
    "watch:script": "npx spago bundle-app -t ./public/index.js -w",
    "watch:css": "tailwindcss -i ./src/index.css -o ./public/index.css -w",
    "watch:server": "npx live-server ./public",
    "watch": "run-p watch:*"
  },
```

最終的にバンドルするには

```
npm run bundle
```

開発時にホットリロードするには

```
npm run watch
```

とします

## 余談

### 補完が効かないんだが？

PureScript の Laugage Server は output ディレクトリを読んで補完を行っているようです。
したがって、パッケージのインストール直後などはそのパッケージの補完が効きません

```
npx spago build --deps-only
```

をすると使っているパッケージのみビルドしてくれます。これをしてみましょう

これでもダメなら VSCode のコマンドパレットから `PureScript: Restart/Reconnect purs IDE server` を実行してみてください

逆にすでにアンインストールしたパッケージの補完が出てうざいというときは、`output` フォルダの該当するディレクトリを消すか、`output` ディレクトリごと消してから再ビルドすればよいです。

### purs-backend-optimizer を使う

purs-backend-optimizer は、purescript がビルドした後のコードを改変してからバンドルするツールで、動作速度の向上やファイルサイズの削減などが狙えます。（Halogen だとファイルサイズは変わらないか、大きくなることがあるそうです。backend-optimizer はどちらかというと Effect や ST モナドの最適化とインライン化による速度向上を重視しているようです）

```
npm install --save-dev purs-backend-es
```

`spago-bundle.dhall` を作成します

```
./spago.dhall // { backend = "purs-backend-es build" }
```

`.gitignore` に追加します

```
/output-es
```

バンドルスクリプトを変更します

```json
"bundle:script": "npx spago -x spago-bundle.dhall build && npx purs-backend-es bundle-app -s -t ./public/index.js -y",
```

### Jelly を使う

最近 Jelly というライブラリを作っています

https://jelly.yukikurage.net/

これを使ってアプリケーションを作ります

```
npx spago install jelly aff foldable-traversable jelly-hooks
```

`Main.purs` を書き換えます

```haskell
module Main where

import Prelude

import Data.Foldable (traverse_)
import Effect (Effect)
import Effect.Aff (launchAff_)
import Effect.Class (liftEffect)
import Jelly.Aff (awaitBody)
import Jelly.Component (Component, text)
import Jelly.Element as JE
import Jelly.Hooks (runHooks_)
import Jelly.Hydrate (mount)
import Jelly.Prop ((:=))

main :: Effect Unit
main = launchAff_ do
  bodyMaybe <- awaitBody
  liftEffect $ traverse_ (runHooks_ <<< mount component) bodyMaybe

component :: forall m. Component m
component =
  JE.div [ "class" := "w-screen h-screen text-5xl flex justify-center items-center" ] do
    text "Hello Jelly!"
```

これで OK です。Jelly についての記事はこちら

https://zenn.dev/yukikurage/articles/4735819c3b421b
https://zenn.dev/yukikurage/articles/d78a464b815aec
https://zenn.dev/yukikurage/articles/367d844d79de20

0.8.0 で破壊的変更を入れています。最新のドキュメントはここにあります↓

https://jelly.yukikurage.net