ClojureScript 開発時の project.clj を理解する


本記事は [Clojure Advent Calendar 2016](http://qiita.com/advent-calendar/2016/clojure) 4日目の記事です。

## はじめに

ClojureScript を使って Web 開発をしたい、と思った時に、プロジェクトの環境設定は面倒くさく悩ましいネタの一つです。Clojure で代表的な Web フレームワークである[luminus](http://www.luminusweb.net/) や [duct](https://github.com/duct-framework/duct/)、あるいは(フレームワークではないかもしれないが) [chestnut](https://github.com/plexus/chestnut)等だと、めんどくさい設定等を間違えることなく良きにはからってくれる leiningen template が用意され、開発を容易に始めることができます。ですが、自動生成された環境は、時に自分の思い通りと少し違っていたり、オーバースペックゆえにいじりにくかったりする場合があります。

そこで本記事では、今時の ClojureScript を前提とした Web 開発の環境について、一旦シンプルなところから徐々に拡張していく感じで理解を進めていく、ということをしてみたいと思います。

## step1. まずは lein-cljsbuild

(Boot派の人ごめんなさい...)まずは ClojureScript でのビルドツールとして leiningen のプラグインである `lein-cljsbuild`を使うための最小限の環境を考えてみます。

まずは `project.clj`。REPL から使う前提として必要最小限だとこんな感じでしょうか。

```project.clj
;; https://github.com/emezeske/lein-cljsbuild を参考にわりと最小構成で
(defproject step01 "0.1.0-SNAPSHOT"
  :description "step01"
  :dependencies [[org.clojure/clojure "1.8.0"]
                 [org.clojure/clojurescript "1.9.293" :scope "provided"]]
  :plugins [[lein-cljsbuild "1.1.4"]]
  :clean-targets ^{:protect false} [:target-path :compile-path "resources/public/js"]
  :cljsbuild
  {:builds [{:source-paths ["src"]
             :compiler
             {:main "step01.core"
              :asset-path "js"
              :output-to "resources/public/js/main.js"
              :output-dir "resources/public/js"
              :source-map true
              :optimizations :none
              :pretty-print true}}]})
```

ディレクトリ構成は以下のような感じで、`index.html`と`core.cljs`のみ事前に作成しておきます。

```
    .
    ├── project.clj
    ├── resources
    │   └── public
    │       └── index.html
    │
    └── src
        └── step01
            └── core.cljs
```

`index.html`、`core.cljs`、は最小構成だと以下のような感じです。

```resources/public/index.html
<!DOCTYPE html>
<html>
    <head lang="en">
        <meta charset="UTF-8">
    </head>
    <body>
        <script src="js/main.js"></script>
    </body>
</html>
```

```src/step01/core.cljs
(ns step01.core)

(enable-console-print!)

(defn init
  []
  (.log js/console "Hello, world"))

(init)
```

これをビルドしてみます。コマンドは `lein cljsbuild once`。すると、`resources/public/js` 以下に JavaScript のコードが生成され、ブラウザで `index.html` を開くと、デバッガのコンソール上に `Hello, world` と出力されます。

コンパイル結果を確認するため `resources/public/js` 以下を見てみると、`main.js`の他にフォルダやらファイルやらが生成されているのが確認できます。

```
bash$ cd resources/public/js
bash$ ls
cljs/         cljs_deps.js  goog/         main.js       step01/
bash$
```

実際のところ、`main.js`は「それらを include するためのファイル」という形で生成されます。

```resources/public/js/main.js
var CLOSURE_UNCOMPILED_DEFINES = null;
if(typeof goog == "undefined") document.write('<script src="js/goog/base.js"></script>');
document.write('<script src="js/cljs_deps.js"></script>');
document.write('<script>if (typeof goog == "undefined") console.warn("ClojureScript could not load :main, did you forget to specify :asset-path?");</script>');
document.write('<script>goog.require("step01.core");</script>');
```

上記 `main.js` が `index.html` から参照されたとき、まず `js/goog/base.js` その他もろもろを動的に include していることがわかります。これは `:main` オプションを指定している場合であり、 `:main` オプションを指定しなければ、コンパイル時点で出力先の JavaScript (`main.js`) に全てのコードを突っ込む、という動作をします。

また、上記 `main.js` の中に見られる `js/` で始まるパスは、`:asset-path` の指定した値になっています。`:asset-path` が未指定の場合はフルパスになるようです(よって `:main` オプションを指定するパターンの場合は `:asset-path` は必須)。

以下にオプションを簡単に記載しておきます。もっと詳しい例は [lein-cljsbuild の sample.project.clj](https://github.com/emezeske/lein-cljsbuild/blob/1.1.4/sample.project.clj) を見ると良いです。

| option        | 説明                                                                    |
:---------------|:------------------------------------------------------------------------|
| :source-paths | 指定ディレクトリ以下にある ClojureScript ファイルをコンパイルします。   |
| :main         | 最初にコンパイルするべき namespace を指定します(コンパイルの起点っぽい) |
| :asset-path   | 上記参照。                                                              |
| :output-to    | 出力ファイル(JavaScriptファイル名)を指定します。                        |
| :output-dir   | コンパイル時の temporary file 群の出力先ディレクトリを指定します。ここでは include するべき JavaScript を正しく配置するために resources 以下に指定しています。 |
| :source-map   | source-map を出力するか否かを指定します。 |
| :optimizations| :none、:whitespace、:simple、:advanced、のいずれかを指定します。ブラウザのJavaScript デバッガでデバッグする際は :none、リリース向けには :advanced を指定しますが、:advanced 指定時にはいくつか他に設定が必要です（後述）。 |
| :pretty-print | コンパイルした JavaScript を pretty print するかどうかを指定します。ブラウザのJavaScript デバッガでデバッグする際は true、それ以外は false で良いと思います。 |

## step2. リリース向け設定

step1.では、JavaScript デバッグのために必要なファイルを出力することを意図していましたが、本番向けとしては圧縮できるものは可能な限り圧縮したいし、ファイルのデバッグ情報も必要ありません。そこで、cljsbuild の設定を変更します。

```project.clj
(defproject step02 "0.1.0-SNAPSHOT"
  :description "step02"
  :dependencies [[org.clojure/clojure "1.8.0"]
                 [org.clojure/clojurescript "1.9.293" :scope "provided"]]
  :plugins [[lein-cljsbuild "1.1.4"]]
  :clean-targets ^{:protect false} [:target-path :compile-path "resources/public/js"]
  :cljsbuild
  {:builds [{:source-paths ["src"]
             :compiler
             {:output-to "resources/public/js/main.js"
              :optimizations :advanced
              :pretty-print false}}]})
```

だいぶシンプルになりました。step1. との違いを書いておきます。

| option         | 説明                                                                    |
:----------------|:------------------------------------------------------------------------|
| :main          | 不要なので削除                                                          |
| :asset-path    | 不要なので削除                                                          |
| :output-dir    | 削除(デフォルトは target 以下に作られる)                                |
| :source-map    | 不要なので削除                                                          |
| :optimizations | `:none` から `:advanced` に変更                                         |
| :pretty-print  | `true` から `false` に変更                                              |

## step3. profileを分ける

step1、step2、では、`project.clj` そのものをデバッグ用/リリース用と分けていましたが、実際のところ１つの project.clj でデバッグ向け/リリース向けに切り替える必要があります。step3 ではこれを合体させることを考えます。

```project.clj
(defproject step03 "0.1.0-SNAPSHOT"
  :description "step03"
  :dependencies [[org.clojure/clojure "1.8.0"]
                 [org.clojure/clojurescript "1.9.293" :scope "provided"]]
  :plugins [[lein-cljsbuild "1.1.4"]
            [lein-environ "1.1.0"]]
  :resource-paths ["resources" "target/cljsbuild"]
  :target-path "target/%s/"
  :clean-targets ^{:protect false} [:target-path :compile-path "resources/public/js"]
  :profiles
  {:dev [:project/dev :profiles/dev]
   :uberjar {:omit-source true
             :prep-tasks ["compile" ["cljsbuild" "once" "min"]]
             :cljsbuild
             {:builds
              {:min
               {:source-paths ["src"]
                :compiler
                {:output-to "resources/public/js/main.js"
                 :optimizations :advanced
                 :pretty-print false}}}}}
   :project/dev {:cljsbuild
                 {:builds
                  {:dev
                   {:source-paths ["src"]
                    :compiler
                    {:main "step03.core"
                     :asset-path "js"
                     :output-to "resources/public/js/main.js"
                     :output-dir "resources/public/js"
                     :source-map true
                     :optimizations :none
                     :pretty-print true}}}}}
   :profiles/dev {}})
```

切り替えはざっくり profile で分けることにします。ここではデバッグ向けは `:dev`、リリース向けは `:uberjar` としています(他にテスト向け `:test` も考慮すべきですが簡単にするためここでは省略)。

上記設定では `:dev` をさらに `:project/dev`、`:profiles/dev` に分けています。ここらへんの設定は luminus の設定のまんまです。各プロファイルの `:cljsbuild`の設定は step1、step2、のそのままにしています。

これでテストしてみます。まずはデフォルトの `:dev` プロファイル。

```
bash$ lein clean
bash$ lein cljsbuild once
Compiling ClojureScript...
Compiling "resources/public/js/main.js" from ["src"]...
Successfully compiled "resources/public/js/main.js" in 1.196 seconds.
bash$ tree
.
├── project.clj
├── resources
│   └── public
│       ├── index.html
│       └── js
│           ├── cljs
│           │   ├── core.cljs
│           │   ├── core.js
│           │   └── core.js.map
│           ├── cljs_deps.js
│           ├── goog
│           │   ├── array
│           │   │   └── array.js
:            :
:            :  (中略)
:            :
│           ├── main.js
│           └── step03
│               ├── core.cljs
│               ├── core.cljs.cache.edn
│               ├── core.js
│               └── core.js.map
├── src
│   └── step03
│       └── core.cljs
└── target
:            :
:            :  (以下略)

26 directories, 26 files
bash$
```

次に `:uberjar` プロファイルで cljsbuild してみます。`lein with-profile <profile>` でプロファイルを指定して leiningen task を実行します。

```
bash$ lein clean
bash$ lein with-profile uberjar cljsbuild once
Compiling ClojureScript...
Compiling ClojureScript...
Compiling "resources/public/js/main.js" from ["src"]...
Successfully compiled "resources/public/js/main.js" in 19.953 seconds.
bash$ tree
.
├── project.clj
├── resources
│   └── public
│       ├── index.html
│       └── js
│           └── main.js
├── src
│   └── step03
│       └── core.cljs
└── target
:            :
:            :  (以下略)

18 directories, 10 files
bash$
```

あと、`:uberjar` プロファイルの `:prep-task` を指定しているので lein uberjar するタイミングで cljsbuild が走ります。

```
bash$ lein uberjar
Compiling ClojureScript...
Compiling "resources/public/js/main.js" from ["src"]...
Successfully compiled "resources/public/js/main.js" in 19.988 seconds.
Created /.../step03/target/uberjar/step03-0.1.0-SNAPSHOT.jar
Created /.../step03/target/uberjar/step03-0.1.0-SNAPSHOT-standalone.jar
bash$
```

この段階ではサーバーサイドのコードがないので uberjar しても意味はなさそうですが...。

## まとめ

本当は、サーバーサイドも含めたプロジェクト構成の設計(ClojureScript の REPL 環境を提供する[figwheel](https://github.com/bhauman/lein-figwheel)や、テスト用ツール [doo](https://github.com/bensu/doo)を組み込んだ状態)まで進みたかったのですが、基礎的なことに集中して記事にすることにしました。

(※本当の本当は om.next の [remoting](https://github.com/omcljs/om/wiki/Remote-Synchronization-Tutorial)について作業中だったのですが間に合わず...でした。せっかくなので [project.cljへのリンク](https://github.com/ponkore/om-next-leaflet/blob/master/project.clj) だけでも晒しておきます。)

この記事が、ClojureScript での開発をされるどなたかのお役に立てることができれば幸いです。