Jade+Stylus+CoffeeScriptなページを生成・アップロードするgruntタスク

Jade、Stylus、CoffeeScriptで簡単にページを作ってアップロードするgruntタスクを紹介します。作ってある機能は以下の通りです。

* 開発用
  * 各種ファイルの変更監視
  * サーバ起動
  * Jade、Stylus、CoffeeScript
  * CSSのvendor prefix拡張
  * 素のHTML、CSS、JavaScript、画像への対応
* デプロイ用
  * HTML、CSS、JavaScript、画像の圧縮
  * FTPでの差分アップロード

普段は、これをベースに機能を削って使っています。

## ディレクトリ構成

以下のようなディレクトリ構成で使います。

```
/
 .ftppass（FTP使わないなら不要）
 .gitignore（git使わないなら不要だけど、使うときに事故起こさないようにいつも入れてる）
 Gruntfile.coffee
 package.json
 src/ （アップロードしたいファイルを入れる。ディレクトリ構成は自由です！）
   index.jade
   sample/
     index.html
   css/
     styles.styl
     normalize.css
   js/
     scripts.coffee
     jquery.js
 dist/ （自動で作られる。圧縮済みのファイルが入る）
```

## 各ファイルの中身

### package.json

```json
{
  "name": "example-site",
  "version": "0.0.0",
  "description": "build example website.",
  "devDependencies": {
    "grunt": "*",
    "grunt-contrib-watch": "*",
    "grunt-contrib-connect": "*",
    "grunt-contrib-clean": "*",
    "grunt-contrib-copy": "*",
    "grunt-contrib-coffee": "*",
    "grunt-closurecompiler": "*",
    "grunt-contrib-jade": "*",
    "grunt-contrib-htmlmin": "*",
    "grunt-contrib-stylus": "*",
    "grunt-autoprefixer": "*",
    "grunt-contrib-cssmin": "*",
    "grunt-contrib-imagemin": "*",
    "grunt-ftpush": "*"
  }
}
```

### Gruntfile.coffee

```coffeescript
module.exports = (grunt) ->
  pkg = grunt.file.readJSON('package.json')

  grunt.initConfig
    pkg: pkg
    watch:
      gruntfile:
        files: 'Gruntfile.coffee'
      html:
        files: 'src/**/*.html'
        tasks: 'copy:html'
      css:
        files: 'src/**/*.css'
        tasks: ['copy:css', 'autoprefixer:dist']
      js:
        files: 'src/**/*.js'
        tasks: 'copy:js'
      jade:
        files: 'src/**/*.jade'
        tasks: 'jade:dist'
      stylus:
        files: 'src/**/*.styl'
        tasks: ['stylus:dist', 'autoprefixer:dist']
      coffee:
        files: 'src/**/*.coffee'
        tasks: 'coffee:dist'
      options:
        livereload: true
    connect:
      server:
        options:
          base: '.tmp'
          useAvailablePort: true
      dist:
        options:
          keepalive: true
          useAvailablePort: true
          base: 'dist'
    clean:
      server: '.tmp'
      dist: 'dist'
    copy:
      image:
        expand: true
        cwd: 'src'
        src: '**/*.{png,jpg,gif}'
        dest: '.tmp'
        filter: 'isFile'
      html:
        expand: true
        cwd: 'src'
        src: '**/*.html'
        dest: '.tmp'
        filter: 'isFile'
      css:
        expand: true
        cwd: 'src'
        src: '**/*.css'
        dest: '.tmp'
        filter: 'isFile'
      js:
        expand: true
        cwd: 'src'
        src: '**/*.js'
        dest: '.tmp'
        filter: 'isFile'
    jade:
      options:
        client: false
      dist:
        expand: true
        cwd: 'src'
        src: '**/*.jade'
        dest: '.tmp'
        ext: '.html'
    stylus:
      dist:
        expand: true
        cwd: 'src'
        src: ['**/*.styl', '!**/_*.styl']
        dest: '.tmp'
        ext: '.css'
    autoprefixer:
      dist:
        expand: true
        cwd: '.tmp'
        src: '**/*.css'
        dest: '.tmp'
        ext: '.css'
    coffee:
      options:
        bare: true
      dist:
        expand: true
        cwd: 'src'
        src: '**/*.coffee'
        dest: '.tmp'
        ext: '.js'
    htmlmin:
      options:
        removeComments: true
        collapseWhitespace: true
      dist:
        expand: true
        cwd: '.tmp'
        src: '**/*.html'
        dest: 'dist'
    cssmin:
      dist:
        expand: true
        cwd: '.tmp',
        src: '**/*.css'
        dest: 'dist'
    closurecompiler:
      options:
        compilation_level: 'SIMPLE_OPTIMIZATIONS'
        max_processes: 5
      dist:
        expand: true
        cwd: '.tmp',
        src: '**/*.js'
        dest: 'dist'
    imagemin:
      options:
        optimizationLevel: 7
      dist:
        expand: true
        cwd: '.tmp'
        src: '**/*.{png,jpg,gif}'
        dest: 'dist'
    ftpush:
      dist:
        auth:
          host: 'server.domain.tld' # アップロード先サーバ
          port: 21
          authKey: pkg.name
        src: 'dist'
        dest: '/' # アップロード先のディレクトリなので環境によって変わる
        exclusions: 'dist/**/{.DS_Store,Thumbs.db}'
        simple: false
        useList: false

  grunt.loadNpmTasks task for task of pkg.devDependencies when /^grunt-/.test(task)

  grunt.registerTask 'generate', [
    'clean:server'
    'copy'
    'jade:dist'
    'stylus:dist'
    'autoprefixer:dist'
    'coffee:dist'
  ]

  grunt.registerTask 'compile', [
    'clean:dist'
    'htmlmin:dist'
    'cssmin:dist'
    'closurecompiler:dist'
    'imagemin:dist'
  ]

  grunt.registerTask 'build', [
    'generate'
    'compile'
  ]

  grunt.registerTask 'serve', [
    'generate'
    'connect:server'
    'watch'
  ]

  grunt.registerTask 'serve:dist', [
    'build'
    'connect:dist'
  ]

  grunt.registerTask 'deploy', [
    'build'
    'ftpush:dist'
  ]

  grunt.registerTask 'default', [
    'serve'
  ]
```

### .ftppass

```json
{
  "example-site": {
    "username": "USERNAME",
    "password": "PASSWORD"
  }
}
```

当たり前ですが、このファイルは.gitignoreの対象にしておきましょう。

### .gitignore

```
/node_modules
/.ftppass
/.tmp
/dist
```

`dist`をレポジトリの管理下に置きたいことはよくあるので、その時は取り除きます。

## 使い方

```bash
grunt # 開発用のサーバが起動します。圧縮しません。
grunt serve:dist # 圧縮した場合のデータが見れるサーバが起動します。
grunt build # 圧縮したデータを生成します。
grunt deploy # 圧縮したデータを生成し、FTPでアップロードします。
```

もちろん事前に`npm i --save-dev`してからですが。

## 補足

割とオーソドックスな構成ですが、他に比べたら一般的ではないgrunt-closurecompilerとgrunt-ftpushについて。

grunt-closurecompilerは趣味的なものです。grunt-contrib-uglifyを使う場合は、package.jsonを置き換えて、Gruntfileのタスク名を置き換えれば良いでしょう。closurecompilerを使うgruntはいくつかありますが、これを使っているのは、何も考えずに`npm i --save-dev`するだけで他のセットアップが不要だからです。オススメです。

grunt-ftpushは差分アップロードに対応しているという理由で使っています。他に良いタスクがあれば教えていただきたいです。あと、多分、差分アップロードなので、他のところからFTPでアップロードすると消されたりするのではないかと思います（未検証）。そういう専用のディレクトリに対して適用しないといけない可能性が高いです。

## 問題点

* デプロイするたびに変更していないものの圧縮、特に画像の圧縮が走るのが無駄