PureScript の build tool`pulp` を動かしてみる

この記事は [(bouzuya) PureScript Advent Calendar 2016][purescript-advent-calendar] の 3 日目。 (bouzuya) PureScript Advent Calendar 2016 は bouzuya の PureScript 学習の記録だ。

- ← 2 日目 『 [PureScript の Interactive mode (`psci`) を動かしてみる - Qiita][2016-12-02] 』
- → 4 日目 『 [PureScript の Getting Started を読む & 続 `psci` を試す - Qiita][2016-12-04] 』

## 概要

今日は [PureScript][purescript] のためのビルドツール `pulp` を試してみる。

`pulp` の repository は [bodil/pulp][] だ。こんな記事を読むより 1 次ソースを参照するほうが早そうだ。

## `pulp` をインストール

```
$ # Node.js がインストールされている前提で
$ mkdir project2/
$ cd project2/
$ npm init -y
$ npm install -D pulp purescript bower
$ npm list --depth 0
project2@1.0.0 /Users/bouzuya/project2
├── bower@1.8.0
├── pulp@10.0.0
└── purescript@0.10.2
```

今回は `pulp@10.0.0` のようだ。とりあえず動かしてみる。

```
$ $(npm bin)/pulp
* Error: Expected command
Usage: pulp [global-options] <command> [command-options]

Global options:
  --before <string>       Run a shell command before the operation begins. Useful with `--watch`, eg. `--watch --before clear`.
  --bower-file -b <file>  Read this bower.json file instead of autodetecting it.
  --else <string>         Run a shell command if an operation finishes. Useful with `--watch`, eg. `--watch --then 'say Done' --else 'say Failed'`
  --help -h               Show this help message.
  --monochrome            Don't colourise log output.
  --then <string>         Run a shell command after the operation finishes successfully. Useful with `--watch`, eg. `--watch --then 'say Done'`
  --version -v            Show current pulp version.
  --watch -w              Watch source directories and re-run command if something changes.

Commands:
  browserify  Produce a deployable bundle using Browserify.
  build       Build the project.
  docs        Generate project documentation.
  init        Generate an example PureScript project.
  login       Obtain and store a token for uploading packages to Pursuit.
  psci        Launch a PureScript REPL configured for the project.
  publish     Publish a previously tagged version to Bower and Pursuit.
  run         Compile and run the project.
  server      Launch a development server.
  test        Run project tests.
  version     Bump and tag a new version in preparation for release.

Use `pulp <command> --help` to learn about command specific options.
```

[README][bodil/pulp] とは違って、今回はインストール時に `npm install --global` にしていないので、このまま `pulp` を使うと、次のようなエラーが表示される。

```
$ $(npm bin)/pulp --version
* ERROR: `psc` executable not found.
```

`pulp` は `npm install --global purescript bower` を前提にしているので、 PATH に `psc` などを含めるよう注意が必要だ。ぼくは global を維持でも回避したいので、ここは npm run-scripts を活用して PATH に `psc` などを含めることにする。

`package.json` の `scripts` に次のように設定する。

```
{
  "name": "project2",
  "version": "1.0.0",
  "author": "bouzuya <m@bouzuya.net> (http://bouzuya.net/)",
  "devDependencies": {
    "bower": "^1.8.0",
    "pulp": "^10.0.0",
    "purescript": "^0.10.2"
  },
  "keywords": [],
  "license": "MIT",
  "main": "index.js",
  "scripts": {
    "pulp": "pulp",
    "pulp:browserify": "pulp browserify",
    "pulp:build": "pulp build",
    "pulp:docs": "pulp docs",
    "pulp:init": "pulp init",
    "pulp:login": "pulp login",
    "pulp:psci": "pulp psci",
    "pulp:publish": "pulp publish",
    "pulp:run": "pulp run",
    "pulp:server": "pulp server",
    "pulp:test": "pulp test",
    "pulp:version": "pulp version"
  }
}
```

これで改めて動かしてみる。

```
$ npm run pulp -- --version
> project2@1.0.0 pulp /Users/bouzuya/project2
> pulp "--version"

Pulp version 10.0.0
psc version 0.10.2 using /Users/bouzuya/project2/node_modules/.bin/psc
```

よさそう。

## `pulp init`

`pulp` はいくつかのコマンドがあるようだが、まずはいかにも最初に実行すべきっぽい `pulp init` を実行してみる。

> Generate an example PureScript project.

とのことなので PureScript プロジェクトの例を生成してくれるのだろう。

```
$ npm run pulp:init
> project2@1.0.0 pulp:init /Users/bouzuya/project2
> pulp init

* Generating project skeleton in /Users/bouzuya/project2
bower purescript-prelude#*      cached https://github.com/purescript/purescript-prelude.git#2.1.0
bower purescript-prelude#*    validate 2.1.0 against https://github.com/purescript/purescript-prelude.git#*
bower purescript-console#*      cached https://github.com/purescript/purescript-console.git#2.0.0
bower purescript-console#*    validate 2.0.0 against https://github.com/purescript/purescript-console.git#*
bower purescript-eff#^2.0.0     cached https://github.com/purescript/purescript-eff.git#2.0.0
bower purescript-eff#^2.0.0   validate 2.0.0 against https://github.com/purescript/purescript-eff.git#^2.0.0
bower purescript-console#^2.0.0          install purescript-console#2.0.0
bower purescript-prelude#^2.1.0          install purescript-prelude#2.1.0
bower purescript-eff#^2.0.0              install purescript-eff#2.0.0

purescript-console#2.0.0 bower_components/purescript-console
└── purescript-eff#2.0.0

purescript-prelude#2.1.0 bower_components/purescript-prelude

purescript-eff#2.0.0 bower_components/purescript-eff
└── purescript-prelude#2.1.0
bower purescript-psci-support#* cached https://github.com/purescript/purescript-psci-support.git#2.0.0
bower purescript-psci-support#*         validate 2.0.0 against https://github.com/purescript/purescript-psci-support.git#*
bower purescript-psci-support#^2.0.0     install purescript-psci-support#2.0.0

purescript-psci-support#2.0.0 bower_components/purescript-psci-support
└── purescript-console#2.0.0
```

何やら `bower` が動いているようだ。調べた感じ、いくつかのファイルが生成され、`bower install` が動いたようだ。

## `pulp init` で生成されたファイル

`pulp init` で生成されたファイルは次のとおりだ。たったの 4 つ。

```
.gitignore
bower.json
src/Main.purs
test/Main.purs
```

```
$ cat .gitignore
/bower_components/
/node_modules/
/.pulp-cache/
/output/
/.psc*
/.psa*
```

ふむ。

```
$ cat bower.json
{
  "name": "project2",
  "ignore": [
    "**/.*",
    "node_modules",
    "bower_components",
    "output"
  ],
  "dependencies": {
    "purescript-prelude": "^2.1.0",
    "purescript-console": "^2.0.0"
  },
  "devDependencies": {
    "purescript-psci-support": "^2.0.0"
  }
}
```

ふむふむ。 `purescript-prelude#2.1.0` / `purescript-console#2.0.0` / `purescript-psci-support#2.0.0` はとても一般的な依存関係のようだ。

```
$ cat src/Main.purs
module Main where

import Prelude
import Control.Monad.Eff (Eff)
import Control.Monad.Eff.Console (CONSOLE, log)

main :: forall e. Eff (console :: CONSOLE | e) Unit
main = do
  log "Hello sailor!"
```

ふむふむふむ。

ファイル名は大文字から開始 (モジュール名と一致している (?)) され、`.purs` という拡張子を持っているみたいだ。

`Main` というモジュールに `main` 関数が生えている。

`import Prelude` は `purescript-prelude` が提供するモジュールだろう。

`import Control.Monad.Eff.Console` はおそらく `purescript-console` が提供するモジュールだろう。

まあ、後回しかな。

```
$ cat test/Main.purs
module Test.Main where

import Prelude
import Control.Monad.Eff (Eff)
import Control.Monad.Eff.Console (CONSOLE, log)

main :: forall e. Eff (console :: CONSOLE | e) Unit
main = do
  log "You should add some tests."
```

ふむふむふむふむ。特にテストフレームワークなどは組み込まれないっぽい。

## `pulp build`

[bodil/pulp の 10.0.0 の README][bodil/pulp] に従って、次は `pulp build` してみよう。

```
$ npm run pulp:build

> project2@1.0.0 pulp:build /Users/bouzuya/project2
> pulp build

* Building project in /Users/bouzuya/project2
Compiling Data.NaturalTransformation
Compiling Data.Show
Compiling Control.Semigroupoid
Compiling Data.Boolean
Compiling Control.Category
Compiling Data.Void
Compiling Data.Function
Compiling Data.Unit
Compiling Data.HeytingAlgebra
Compiling Data.Eq
Compiling Data.Semiring
Compiling Data.Semigroup
Compiling Data.Functor
Compiling Data.Ring
Compiling Data.Ordering
Compiling Data.BooleanAlgebra
Compiling Data.CommutativeRing
Compiling Data.Ord.Unsafe
Compiling Data.Ord
Compiling Data.EuclideanRing
Compiling Control.Apply
Compiling Data.Field
Compiling Control.Applicative
Compiling Control.Bind
Compiling Data.Bounded
Compiling Control.Monad
Compiling Control.Monad.Eff
Compiling Prelude
Compiling Control.Monad.Eff.Class
Compiling Control.Monad.Eff.Unsafe
Compiling Control.Monad.Eff.Console
Compiling PSCI.Support
Compiling Main
* Build successful.
```

ビルドされた (雑) 。

`output/` にコンパイルされたファイルがあるようだ。

```
$ ls output/
Control.Applicative/
Control.Apply/
Control.Bind/
Control.Category/
Control.Monad/
Control.Monad.Eff/
Control.Monad.Eff.Class/
Control.Monad.Eff.Console/
Control.Monad.Eff.Unsafe/
Control.Semigroupoid/
Data.Boolean/
Data.BooleanAlgebra/
Data.Bounded/
Data.CommutativeRing/
Data.Eq/
Data.EuclideanRing/
Data.Field/
Data.Function/
Data.Functor/
Data.HeytingAlgebra/
Data.NaturalTransformation/
Data.Ord/
Data.Ord.Unsafe/
Data.Ordering/
Data.Ring/
Data.Semigroup/
Data.Semiring/
Data.Show/
Data.Unit/
Data.Void/
Main/
PSCI.Support/
Prelude/
```

モジュール単位でコンパイル済みの JavaScript ファイルがあるようだ。

```
$ cat output/Main/index.js
// Generated by psc version 0.10.2
"use strict";
var Prelude = require("../Prelude");
var Control_Monad_Eff = require("../Control.Monad.Eff");
var Control_Monad_Eff_Console = require("../Control.Monad.Eff.Console");
var main = Control_Monad_Eff_Console.log("Hello sailor!");
module.exports = {
    main: main
};
```

なるほど、commonjs 。

## `pulp run`

次は `pulp run` で動かしてみる。

```
$ npm run pulp:run
> project2@1.0.0 pulp:run /Users/bouzuya/project2
> pulp run

* Building project in/Users/bouzuya/project2
* Build successful.
Hello sailor!
```

動くぞ！ `node` に渡しているだけっぽい。 `--runtime phantomjs` のように別のコマンドも指定できるようだ。

## `pulp browserify`

いかにもな `pulp browserify` は……

```
$ npm run pulp:browserify -- --to output/bundle.js

> project2@1.0.0 pulp:browserify /Users/bouzuya/project2
> pulp browserify "--to" "output/bundle.js"

* Browserifying project in /Users/bouzuya/project2
* Building project in /Users/bouzuya/project2
* Build successful.
* Browserifying...
* Browserified.
```

思ったよりは短い感じだけど、ここに貼るのは厳しいかな。普通に `browserify` しただけっぽい。

`--optimise` というオプションで未使用コードを削れるらしい。

```
$ npm run pulp:browserify -- --optimise --to output/bundle.min.js

> project2@1.0.0 pulp:browserify /Users/bouzuya/project2
> pulp browserify "--optimise" "--to" "output/bundle.min.js"

* Browserifying project in /Users/bouzuya/project2
* Building project in /Users/bouzuya/project2
* Build successful.
* Browserifying...
* Browserified.
```

```
$ cat output/bundle.min.js
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
// Generated by psc-bundle 0.10.2
var PS = {};
(function(exports) {
    "use strict";

  exports.log = function (s) {
    return function () {
      console.log(s);
      return {};
    };
  };
})(PS["Control.Monad.Eff.Console"] = PS["Control.Monad.Eff.Console"] || {});
(function(exports) {
  // Generated by psc version 0.10.2
  "use strict";
  var $foreign = PS["Control.Monad.Eff.Console"];
  var Control_Monad_Eff = PS["Control.Monad.Eff"];
  var Data_Show = PS["Data.Show"];
  var Data_Unit = PS["Data.Unit"];
  exports["log"] = $foreign.log;
})(PS["Control.Monad.Eff.Console"] = PS["Control.Monad.Eff.Console"] || {});
(function(exports) {
  // Generated by psc version 0.10.2
  "use strict";
  var Prelude = PS["Prelude"];
  var Control_Monad_Eff = PS["Control.Monad.Eff"];
  var Control_Monad_Eff_Console = PS["Control.Monad.Eff.Console"];
  var main = Control_Monad_Eff_Console.log("Hello sailor!");
  exports["main"] = main;
})(PS["Main"] = PS["Main"] || {});
PS["Main"].main();

},{}]},{},[1]);
```

確かに縮む。

## `pulp docs`

```
$ npm run pulp:docs
> project2@1.0.0 pulp:docs /Users/bouzuya/project2
> pulp docs

* Generating documentation in /Users/bouzuya/project2
* Documentation generated.
```

ふむ。出力先を書いてほしいところだけど……、`docs/` っぽい。

    $ cat docs/Main.md
    ## Module Main

    #### `main`

    ``` purescript
    main :: forall e. Eff (console :: CONSOLE | e) Unit
    ```

Markdown だ。

## `pulp psci`

REPL 。

```
npm run pulp:psci
> project2@1.0.0 pulp:psci /Users/bouzuya/project2
> pulp psci

PSCi, version 0.10.2
Type :? for help

>
```

引数に `*.purs` を指定しなくてもなんとかしてくれている。良い。

## `pulp server`

`/app.js` に bundle したファイルを置いて server を立ててくれるっぽい。あまり凝ったことはできなさそうだけど、手軽でいいかも。

```
npm run pulp:server
> project2@1.0.0 pulp:server /Users/bouzuya/project2
> pulp server

* Server listening on http://localhost:1337/
* Building project in /Users/bouzuya/project2
* Build successful.
* Bundling JavaScript...
* Bundled.
```

```
$ curl http://localhost:1337/app.js
# (中略)
PS["Main"].main();
```

たしかに。 root は `package.json` のあるディレクトリみたいなので `index.html` を置くと普通に見れる。

## `pulp test`

```
npm run pulp:test

> project2@1.0.0 pulp:test /Users/bouzuya/project2
> pulp test

* Building project in /Users/bouzuya/project2
Compiling Test.Main
* Build successful.
* Running tests...
You should add some tests.
* Tests OK.
```

うん。なるほど。

## その他

`pulp build` や `pulp test` あたりは `--watch` や `--before` / `--then` / `--else` などのオプションを取れるようだ。

ほかのオプションも結構スルーしているので、詳しくは [README][bodil/pulp] を参照。

## まとめ

```
$ npm run pulp -- -h

> project2@1.0.0 pulp /Users/bouzuya/project2
> pulp "-h"

Usage: pulp [global-options] <command> [command-options]

Global options:
  --before <string>       Run a shell command before the operation begins. Useful with `--watch`, eg. `--watch --before clear`.
  --bower-file -b <file>  Read this bower.json file instead of autodetecting it.
  --else <string>         Run a shell command if an operation finishes. Useful with `--watch`, eg. `--watch --then 'say Done' --else 'say Failed'`
  --help -h               Show this help message.
  --monochrome            Don't colourise log output.
  --then <string>         Run a shell command after the operation finishes successfully. Useful with `--watch`, eg. `--watch --then 'say Done'`
  --version -v            Show current pulp version.
  --watch -w              Watch source directories and re-run command if something changes.

Commands:
  browserify  Produce a deployable bundle using Browserify.
  build       Build the project.
  docs        Generate project documentation.
  init        Generate an example PureScript project.
  login       Obtain and store a token for uploading packages to Pursuit.
  psci        Launch a PureScript REPL configured for the project.
  publish     Publish a previously tagged version to Bower and Pursuit.
  run         Compile and run the project.
  server      Launch a development server.
  test        Run project tests.
  version     Bump and tag a new version in preparation for release.

Use `pulp <command> --help` to learn about command specific options.
```

## 参考

- [bodil/pulp][]

## 次回以降の TODO

- `psc-package` を試す
- `psci` の未使用コマンドを試す
- PureScript ドキュメントを読む

[2016-12-01]: http://qiita.com/bouzuya/items/a5be85ec3e496e5bf436
[2016-12-02]: http://qiita.com/bouzuya/items/806335ea74ead0e33d95
[2016-12-03]: http://qiita.com/bouzuya/items/86cc9aaaddcc1aa790cf
[2016-12-04]: http://qiita.com/bouzuya/items/b2b80c22c50d0c1e739c
[bodil/pulp]: https://github.com/bodil/pulp/tree/v10.0.0
[purescript-advent-calendar]: http://www.adventar.org/calendars/1494
[purescript]: http://www.purescript.org/