Gruntfileでcoffeescript コンパイル & wsh 利用 !

仕事ではWindowsなので、何か作る場合には wsh を利用すると簡単に作れていい。 でも素の JScript を書くのはめんどくさいし、どうせなら今流行り(?)の coffeescript で 書きたいとか思っちゃう。

そしてさらにそのコンパイルは自動化したくて、なんかいいのないかと探していたら、ナウい方法は Grunt を使用することみたいじゃあないですか。

ナウい Grunt を使用して、フルい wsh を書くってのもなんか変だけど、とりあえずやってみようと いうことでやってみた。

node.js のインストール

まずは、 nodebrew をインストールする。 nodebrew は、node.js のいろんなバージョンをインストール出来るやつ。

$ curl https://raw.github.com/hokaccha/nodebrew/master/nodebrew | perl - setup

.zshenv にパス追加。

path=(
    # homebrew
    /usr/local/bin(N-/)
    # coreutils
    /usr/local/opt/coreutils/libexec/gnubin(N-/)
    # nodebrew
    $HOME/.nodebrew/current/bin(N-/)
)

こんな感じ。(実際にはもっとたくさん書いてあるけど。)

(N-/) で、存在する場合だけ追加してくれる。

以下のコマンドでインストール出来るバージョンが確認出来る。

$ nodebrew ls-remote

最新をバイナリでインストール。

$ nodebrew install-binary latest
$ nodebrew use v0.11.2

coffeescript のインストール

npm で簡単にインストール出来る。

$ npm install -g coffeescript

Grunt のインストール

これも npm で。 Grunt には grunt-cligrunt がある。 グローバルに grunt-cli をインストールして、 grunt はプロジェクト固有の ディレクトリにインストールのがいいみたい。

$ npm install -g grunt-cli

package.json の作成

とりあえずプロジェクトの説明として package.json ってのが必要みたい。 以下のコマンドで作成。いろいろ聞かれるので適当に答える。

$ npm init

出来上がった package.json はこんな感じ。

{
  "name": "wsh",
  "version": "0.0.0",
  "description": "windows script hosting",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": "",
  "author": "yuyunko",
  "license": "BSD",
  "readmeFilename": "README.md",
}

なんかいろいろダメっぽいところあるけどとりあえずまぁこれでよし。

依存するパッケージのインストール

Grunt で必要なパッケージをインストールする。

$ npm install grunt --save-dev

何はさておき、 grunt をインストールする。その際、 –save-dev オプション をつけると、 package.json に自動で記述を追加してくれる。

他にも必要なパッケージをインストール。

$ npm install grunt-contrib-concat --save-dev
$ npm install grunt-contrib-coffee --save-dev
$ npm install grunt-contrib-watch --save-dev
$ npm install grunt-contrib-uglify --save-dev
$ npm install grunt-contrib-clean --save-dev
$ npm install glob --save-dev

上記コマンドで package.json はこんなのに変わってるはず。

{
  "name": "wsh",
  "version": "0.0.0",
  "description": "windows script hosting",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": "",
  "author": "yuyunko",
  "license": "BSD",
  "readmeFilename": "README.md",
  "devDependencies": {
    "grunt": "~0.4.1",
    "grunt-contrib-concat": "~0.3.0",
    "grunt-contrib-coffee": "~0.7.0",
    "grunt-contrib-watch": "~0.4.4",
    "grunt-contrib-uglify": "~0.2.1",
    "grunt-contrib-clean": "~0.4.1",
    "glob": "~3.2.1"
  }
}

Gruntfile の作成

Grunt の実行には Gruntfile が必要。 Gruntfile.js か、 Gruntfile.coffeecoffeescript で書くのがいいらしいので、そっちで書く。

全部書くとこんな感じ。

module.exports = (grunt) ->
    pkg = grunt.file.readJSON 'package.json'
    grunt.initConfig
        pkg : grunt.file.readJSON 'package.json'

        # config
        watch:
            default:
                files: ['coffee/**/*.coffee']
                tasks: ['coffee', 'concat', 'nkf:concat:Shift_JIS:dos']
            build:
                files: ['coffee/**/*.coffee']
                tasks: ['coffee', 'concat', 'uglify', 'nkf:uglify:Shift_JIS:dos']

        coffee:
            compile:
                options:
                    sourceMap: true
                    bare: true
                files: [
                    expand: true
                    cwd: 'coffee/'
                    src: ['**/*.coffee']
                    dest: 'js/'
                    ext: '.js'
                ]

        uglify:
            uglify_target:
                options:
                    beautify: false
                files: [
                    expand: true
                    cwd: 'js/'
                    src: ['**/*.js']
                    dest: 'uglify/'
                    ext: '.js'
                ]

        concat:
            options:
                banner: '''
                        @if(0)==(0) ECHO OFF
                            pushd "%~dp0" > nul
                            CScript.exe //NoLogo //E:JScript "%~f0" %*
                            popd > nul
                        pause
                        GOTO :EOF
                        @end
                        // <%= grunt.template.today("yyyy-mm-dd") %> compiled !
                        '''+"\n\n"
            default:
                files:
                    'concat/helloworld.cmd': ['js/lib/Lib.js', 'js/lib/File.js', 'js/lib/Shell.js', 'js/helloworld.js']
                    'concat/hellogrunt.cmd': ['js/lib/Lib.js', 'js/lib/File.js', 'js/hellogrunt.js']
            build:
                files:
                    'concat/helloworld.cmd': ['uglify/lib/Lib.js', 'uglify/lib/File.js', 'uglify/lib/Shell.js', 'uglify/helloworld.js']
                    'concat/hellogrunt.cmd': ['uglify/lib/Lib.js', 'uglify/lib/File.js', 'uglify/hellogrunt.js']

        clean: [
            'js'
            'concat'
            'uglify'
        ]

    # nkf task
    grunt.registerTask 'nkf', 'Change encoding and newline character', (dir, encoding, nl) ->
        {exec} = require 'child_process'
        glob = require 'glob'

        dir ?= '.'

        switch encoding
            when 'ISO-2022-JP'
                encOpt = 'j'
            when 'Shift_JIS'
                encOpt = 's'
            when 'EUC-JP'
                encOpt = 'e'
            else
                encOpt = 'w'

        switch nl
            when 'dos'
                nlOpt = 'w'
            when 'mac'
                nlOpt = 'm'
            when 'unix'
                nlOpt = 'u'
            else
                nlOpt = null

        cmdSfx = if nlOpt? then "nkf -O#{encOpt}L#{nlOpt} " else "nkf -O#{encOpt} "
        done = @async()
        glob "#{dir}/**/*.cmd", (err, files) ->
            if err?
                console.log err
                done false
            else
                for file in files
                    cmd = cmdSfx + "#{file} cmd/#{file.split("#{dir}/").join('')}"
                    console.log cmd

                    # exec nkf
                    options = {timeout: 5000}
                    exec cmd, options, (error, stdout, stderr) ->
                        if error?
                            console.log 'ERR', error, stderr
                            done false
                        else
                            console.log stdout
            done()

    # loadNpmTasks
    # read from package.json
    for taskName of pkg.devDependencies when taskName.substring(0, 6) is 'grunt-'
        grunt.loadNpmTasks taskName

    grunt.registerTask 'default', ['coffee', 'concat:default', 'nkf:concat:Shift_JIS:dos', 'watch:default']
    grunt.registerTask 'build', ['coffee', 'uglify', 'concat:build', 'nkf:concat:Shift_JIS:dos', 'clean']

上記の設定で、

$ grunt

と実行すると、以下が行われる。

  1. coffeescript のコンパイル
  2. ファイルの結合 ( wsh として実行出来るようにヘッダも追加)
  3. nkf で UTF-8 から Shift_JIS への変換 (改行コードも LF から CRLF に変換)
  4. ファイルが更新されるのを監視して、更新されたら、1. から再実行

さらに、

$ grunt build

とやると、以下が行われる。

  1. coffeescript のコンパイル
  2. js の圧縮
  3. ファイルの結合 ( wsh として実行出来るようにヘッダも追加)
  4. nkf で UTF-8 から Shift_JIS への変換 (改行コードも LF から CRLF に変換)
  5. jsconcatuglify ディレクトリの削除

ちなみにディレクトリ構成はこんな感じ。

.
├── Gruntfile.coffee
├── README.md
├── cmd
│   ├── helloworld.cmd
│   └── hellogrunt.cmd
├── coffee
│   ├── helloworld.coffee
│   ├── hellogrunt.coffee
│   ├── lib
│   │   ├── Excel.coffee
│   │   ├── File.coffee
│   │   ├── Lib.coffee
│   │   ├── Network.coffee
│   │   └── Shell.coffee
├── concat
│   ├── helloworld.cmd
│   └── hellogrunt.cmd
├── js
│   ├── helloworld.js
│   ├── helloworld.js.map
│   ├── hellogrunt.js
│   ├── hellogrunt.js.map
│   ├── lib
│   │   ├── Excel.js
│   │   ├── Excel.js.map
│   │   ├── File.js
│   │   ├── File.js.map
│   │   ├── Lib.js
│   │   ├── Lib.js.map
│   │   ├── Network.js
│   │   ├── Network.js.map
│   │   ├── Shell.js
│   │   └── Shell.js.map
├── node_modules (配下は略)
│
└── package.json

便利。