My gulpfile.coffee

シコシコと作ってきた `gulpfile.coffee` を晒してみる。
まだ出来ることはあると思うけど、まあ使えるかなってくらいにはなってるかと。
説明は気が向いたら追加しよ。

- ディレクトリの説明追加（2015/9/11）
- スタイルガイド、ユニットテスト追加。その他の修正。（2015/10/13）


## ディレクトリ

```
┬ htdocs ─┬ styles
│         ├ images
│         └ scripts
│
├ jade ───┬ extends
│         ├ includes
│         └ src
│
├ stylus ─┬ imports
│         └ src
│
├ images ─┬ sprites
│         └ src
│
├ webpack ┬ requires
│         ├ src
│         └ vendors
│
├ styleguide ┬ dist
│            ├ src
│            ├ stylus
│            └ config.md
│
├ test
│
├ node_modules
├ bower_components
│
├ package.json
├ bower.json
├ gulpfile.coffee
├ karma.conf.coffee
└ README.md
```

### 各 `src`
`src` 以下のファイルが、ディレクトリ構造を保ったまま `htdocs` に出力される。

#### 例
> `jade/src/index.jade` -> `htdocs/index.html`
> `stylus/src/css/style.styl` -> `htdocs/css/style.css`
> `images/src/sample/images/logo.jpg` -> `htdocs/sample/images/logo.jpg`
> `webpack/src/scripts/index.coffee` -> `htdocs/scripts/index.js`

### `jade`
- `extends` -> テンプレートファイル
- `includes` -> インクルードファイル

### `stylus`
- `imports` -> インポートファイル

### `images`
- `sprites` -> スプライト画像ファイル
- `src` -> 圧縮前の画像ファイル

`sprites` 以下の画像ファイルが、ディレクトリ構造を保ったまま sprite 化して `htdocs` に出力される。
sprite 指定用の `css` も `stylus/imports/sprite.styl` に記述されて出力される。

#### 例
> `images/sprites/images/sample/1.png` 
> `images/sprites/images/sample/2.png`
> `images/sprites/images/sample/3.png`
> -> `htdocs/images/sample.png`

### `webpack`
JavaScript 用のディレクトリ

- `requires` -> require 用ファイル
- `vendors` -> ベンダーのスクリプトファイル（bower install 出来ない場合）

### `styleguide`
styleguide 用のディレクトリ

- `dist` -> 出力用のディレクトリ
- `src` -> スタイルガイド生成マークダウンファイル用のディレクトリ
- `stylus` -> `stylus` 用のディレクトリ
- `config.md` -> レイアウト用ファイル

### `test`
ユニットテスト用のディレクトリ

### `node_modules`
`npm install` での自動生成ディレクトリ
gulp 関係のモジュールが格納される

### `bower_components`
`bower install` での自動生成ディレクトリ
JavaScript のライブラリ・プラグインが格納される

### `package.json`
npm のパッケージ管理ファイル

### `bower.json`
bower のパッケージ管理ファイル

### `gulpfile.coffee`
gulp の設定ファイル

### `karma.conf.coffee`
ユニットテストの設定ファイル

### `README.md`
プロジェクトの説明ファイル


## gulpfile.coffee

```coffeescript:gulpfile.coffee
###
tasks
- development                 -> [ gulp ]
- development watch           -> [ gulp watch ]
- development conding         -> [ gulp coding ]
- development conding watch   -> [ gulp coding-watch ]
- development scripting       -> [ gulp scripting ]
- development scripting watch -> [ gulp scripting-watch ]
- production                  -> [ gulp production ]
- javascript test             -> [ gulp test ]
###


gulp = require 'gulp'
gulpif = require 'gulp-if'
{ forEach, map, merge, flatten, union } = require 'lodash'
{ File, PluginError, log, replaceExtension } = require 'gulp-util'
{ join, relative, dirname, basename } = require 'path'
fs = require 'fs'
through = require 'through2'
watch = require 'gulp-watch'
browserSync = require 'browser-sync'
runSequence = require 'run-sequence'
jade = require 'gulp-jade/node_modules/jade'
gulpJade = require 'gulp-jade'
data = require 'gulp-data'
htmlhint = require 'gulp-htmlhint'
stylus = require 'gulp-stylus'
nib = require 'nib'
url = require 'gulp-css-url'
bust = require 'gulp-css-cache-bust'
styledown = require 'gulp-styledown'
stylusSprites = require 'gulp-stylus-sprites'
webpack = require 'webpack'
eslint = require 'gulp-eslint'
bower = require 'gulp-bower'
imagemin = require 'gulp-imagemin'
pngquant = require 'imagemin-pngquant'
sourcemaps = require 'gulp-sourcemaps'
crLfReplace = require 'gulp-cr-lf-replace'
iconv = require 'gulp-iconv'
extensonChange = require 'gulp-extension-change'
cache = require 'gulp-cached'
changed = require 'gulp-changed'
plumber = require 'gulp-plumber'
notify = require 'gulp-notify'
karma = require 'karma'


###
production flag
###
isProduction = false


###
images change flag
###
isImagesChanged  = true
isSpritesChanged = true


###
styleguide mode
###
isStyleguideMarkdownMode = true


###
directory
###
DEST_ROOT = './htdocs'

JADE_BASE  = './jade'
JADE_SRC   = join JADE_BASE, './src'
JADE_OTHER = join JADE_BASE, './!(src)'
JADE_DEST  = DEST_ROOT

STYLUS_BASE  = './stylus'
STYLUS_SRC   = join STYLUS_BASE, './src'
STYLUS_OTHER = join STYLUS_BASE, './!(src)'
STYLUS_DEST  = DEST_ROOT

IMAGES_BASE = './images'

SPRITE_SRC      = join IMAGES_BASE, './sprites'
SPRITE_DEST     = DEST_ROOT
SPRITE_CSS_DEST = join STYLUS_BASE,'./imports'

IMAGEMIN_SRC  = join IMAGES_BASE, './src'
IMAGEMIN_DEST = DEST_ROOT

WEBPACK_BASE  = './webpack'
WEBPACK_SRC   = join WEBPACK_BASE, './src'
WEBPACK_OTHER = join WEBPACK_BASE, './!(src)'
WEBPACK_DEST  = DEST_ROOT

STYLEGUIDE_BASE     = './styleguide'
STYLEGUIDE_SRC      = join STYLEGUIDE_BASE, './src'
STYLEGUIDE_SRC_FILE =
  if isStyleguideMarkdownMode
    join STYLEGUIDE_SRC, '/**/*.md'
  else
    join STYLUS_BASE, '/**/*.styl'
STYLEGUIDE_STYLUS   = join STYLEGUIDE_BASE, './stylus'
STYLEGUIDE_DEST     = join STYLEGUIDE_BASE, './dist'

IMAGES_JSON = [
  {
    base: DEST_ROOT
    srcs: [
      './images/sample1'
      './images/sample2'
    ]
    dest: join DEST_ROOT, './json'
  }
  {
    base: DEST_ROOT
    srcs: [
      './sp/images/sample1'
      './sp/images/sample2'
    ]
    dest: join DEST_ROOT, './sp/json'
  }
]

TEST_CONFIG_SRC = "#{__dirname}/karma.conf.coffee"
TEST_SRC        = './test'


###
change images json suffixes
###
changeImagesJsonName = (str) ->
  str
    .replace /\//g, '-'
    .replace '.', ''
    .replace /^-/, ''


###
error
###
PLUMBER_OPTS = { errorHandler: notify.onError '<%= error.message %>' }


###
default
###
gulp.task 'default', [ 'development' ]


###
development
###
gulp.task 'development', [ 'all' ]


###
production
###
gulp.task 'production', ->
  isProduction = true
  gulp.start 'all'


###
codeing
###
gulp.task 'codeing', ->
  runSequence(
    'sprite'
    [ 'jade', 'stylus', 'imagemin' ]
    [ 'styleguide', 'styleguide-stylus', 'coding-watch', 'browser-sync' ]
  )


###
scripting
###
gulp.task 'all', ->
  runSequence(
    'webpack-first'
    flatten map IMAGES_JSON, (obj) ->
      map obj.srcs, (src) ->
        name = changeImagesJsonName src
        "images-json-#{name}"
    [ 'scripting-watch', 'browser-sync' ]
  )


###
all
###
gulp.task 'all', ->
  runSequence(
    'sprite'
    [ 'jade', 'stylus', 'imagemin', 'webpack-first' ]
    flatten map IMAGES_JSON, (obj) ->
      map obj.srcs, (src) ->
        name = changeImagesJsonName src
        "images-json-#{name}"
    [ 'styleguide', 'styleguide-stylus', 'all-watch', 'browser-sync' ]
  )


###
watch
###
gulp.task 'watch', [ 'all-watch', 'browser-sync' ]


###
watch start
###
watchStart = (files, cb) ->
  watch files, { ignoreInitial: false }, cb


###
coding watch
###
gulp.task 'coding-watch', ->

  watchStart [ join JADE_SRC  , '/**/*.jade' ], -> gulp.start [ 'jade'    , 'imagemin' ]
  watchStart [ join JADE_OTHER, '/**/*.jade' ], -> gulp.start [ 'jade-all', 'imagemin' ]

  styleguideTask = (stylusTask) ->
    tasks = [
      'sprite'
      [ stylusTask, 'imagemin' ]
    ]
    tasks.push('styleguide') unless isStyleguideMarkdownMode
    runSequence.apply @, tasks
  watchStart [ join STYLUS_SRC   , '/**/*.styl' ], -> styleguideTask('stylus')
  watchStart [ join STYLUS_OTHER , '/**/*.styl' ], -> styleguideTask('stylus-all')

  watchStart [ join STYLEGUIDE_BASE  , '/**/*.md'   ], -> gulp.start 'styleguide'
  watchStart [ join STYLEGUIDE_STYLUS, '/**/*.styl' ], -> gulp.start 'styleguide-stylus'

  watchStart [ join IMAGEMIN_SRC, '/**/*.+(png|jpg|gif|svg)' ], -> isImagesChanged  = true
  watchStart [ join SPRITE_SRC  , '/**/*.+(png|jpg|gif|svg)' ], -> isSpritesChanged = true


###
scripting watch
###
gulp.task 'scripting-watch', ->

  watchStart [ join WEBPACK_SRC  , '/**/*.coffee' ], -> gulp.start 'webpack'
  watchStart [ join WEBPACK_OTHER, '/**/*.coffee' ], -> gulp.start 'webpack-all'

  forEach IMAGES_JSON, (obj) ->
    forEach obj.srcs, (src) ->
      watchStart [ join obj.base, src, '/**/*+(png|jpg|gif|svg)' ], ->
        name = changeImagesJsonName src
        gulp.start "images-json-#{name}"

###
all watch
###
gulp.task 'all-watch', [ 'coding-watch', 'scripting-watch' ]


###
browser-sync
###
gulp.task 'browser-sync', ->
  browserSync
    server:
      baseDir: DEST_ROOT
    open: false


###
jade
###
jadeMember = (file, callback) ->
  ret =
    dirname : dirname file.relative
    filename: replaceExtension basename(file.relative), '.html'
    relative: (path) ->
      path = relative ret.dirname, path
      if path then "#{path}/" else './'
  callback null, ret

# jade.filters.sample = (block) ->
#   block

gulp.task 'jade', ->
  jadeTask true

gulp.task 'jade-all', ->
  jadeTask false

jadeTask = (isSrcDir) ->

  jadeOpts =
    jade   : jade
    pretty : true
    # pretty : gulpif isProduction, false, true
    basedir: join __dirname, JADE_BASE

  gulp.src join JADE_SRC, '/**/*.jade'
    .pipe gulpif isSrcDir, cache 'jade'
    .pipe plumber PLUMBER_OPTS
    .pipe data jadeMember
    .pipe gulpJade jadeOpts
    # .pipe crLfReplace
    #   changeCode: 'CR+LF'
    # .pipe iconv
    #   encoding: 'shift_jis'
    .pipe gulp.dest JADE_DEST
    # .pipe extensonChange
    #   afterExtension: 'php'
    #   copy: true
    # .pipe gulp.dest JADE_DEST
    .pipe htmlhint()
    .pipe browserSync.reload
      stream: true


###
stylus
###
gulp.task 'stylus', ->
  stylusTask true

gulp.task 'stylus-all', ->
  stylusTask false

stylusTask = (isSrcDir) ->

  stylusOpts =
    use: [ nib() ]
    compress: false
    # compress: gulpif isProduction, true, false
    sourcemap:
      inline: true

  gulp.src join STYLUS_SRC, '/**/*.styl'
    .pipe gulpif isSrcDir, cache 'stylus'
    .pipe plumber PLUMBER_OPTS
    .pipe stylus stylusOpts
    .pipe sourcemaps.init
      loadMaps: true
    .pipe sourcemaps.write '.'
    .pipe bust
      base: 'htdocs'
    .pipe gulp.dest STYLUS_DEST
    .pipe url
      mode: 'relative'
      base: STYLUS_DEST
    .pipe gulp.dest STYLUS_DEST
    .pipe browserSync.reload
      stream: true


###
css sprite images
###
gulp.task 'sprite', ->

  return unless isSpritesChanged

  isSpritesChanged = false

  gulp.src join SPRITE_SRC, '/**/*.png'
    .pipe changed SPRITE_DEST
    .pipe plumber PLUMBER_OPTS
    .pipe stylusSprites
      imgSrcBase: SPRITE_SRC.replace './', '/'
      stylusFileName: 'sprite'
    .pipe gulpif '*.styl', cache 'stylus'
    .pipe gulpif '*.styl', gulp.dest SPRITE_CSS_DEST
    # .pipe gulpif '*.png', imagemin
    #   optimizationLevel: 0
    #   use: [
    #     pngquant
    #       quality: 60-80
    #       speed: 1
    #   ]
    .pipe gulpif '*.png', gulp.dest SPRITE_DEST
    .pipe browserSync.reload
      stream: true

###
imagemin
###
gulp.task 'imagemin', ->

  return unless isImagesChanged

  gulp.start [
    'imagemin-png'
    'imagemin-jpg'
    'imagemin-gif'
    'imagemin-svg'
  ]
  isImagesChanged = false

imageminConfig =
  png:
    extension: 'png'
    opts:
      optimizationLevel: 0
      use: [
        pngquant
          quality: 60-80
          speed: 1
      ]
  jpg:
    extension: 'jpg'
    opts:
      progressive: true
  gif:
    extension: 'gif'
    opts:
      interlaced: false
  svg:
    extension: 'svg'
    opts:
      multipass: false

forEach imageminConfig, (val, key) ->
  gulp.task "imagemin-#{key}", ->
    gulp.src join IMAGEMIN_SRC, "/**/*.#{val.extension}"
      .pipe plumber PLUMBER_OPTS
      .pipe changed IMAGEMIN_DEST
      .pipe imagemin val.opts
      .pipe gulp.dest IMAGEMIN_DEST


###
webpack (JavaScript)
###
gulp.task 'webpack-first', ->
  bower()
    .once 'end', ->
      gulp.start 'webpack'

gulp.task 'webpack', ->
  webpackTask true

gulp.task 'webpack-all', ->
  webpackTask false

webpackTask = (isSrcDir) ->

  webpackOpts = ->
    opts =
      resolve:
        root      : [ join __dirname, 'bower_components' ]
        extensions: [ '', '.coffee', '.js' ]
        alias:
          'html5shiv'  : 'html5shiv/dist/html5shiv.min'
          'jquery'     : 'jquery/dist/jquery.min'
          'lodash'     : 'lodash/dist/lodash.min'
          'Velocity'   : 'velocity/velocity.min'
          'Velocity.ui': 'velocity/velocity.ui.min'
      module:
        loaders: [
          test  : /\.coffee$/
          loader: 'coffee'
        ]
      devtool: 'source-map'
      plugins: [
        new webpack.ResolverPlugin(
          new webpack.ResolverPlugin.DirectoryDescriptionFilePlugin 'bower.json', [ 'main' ]
        )
      ]
    if isProduction
      merge opts.plugins, [
        new webpack.optimize.DedupePlugin()
        new webpack.optimize.UglifyJsPlugin()
        new webpack.optimize.OccurenceOrderPlugin()
        new webpack.optimize.AggressiveMergingPlugin()
      ]
    opts

  lintOpts =
    useEslintrc: true
    rules:
      'no-console': 2

  build = (opts) ->
    { basedir, src, dest, webpackOpts } = opts
    webpackBaseOpts = (entry, outputPath, outputFilename) ->
      entry: entry
      output:
        path: outputPath
        filename: "#{outputFilename}.js"
    transform = (data, encode, callback) ->
      destDirname    = dirname join(basedir, dest, relative(src, data.path))
      destFilename   = basename data.path, '.coffee'
      webpackAllOpts = merge webpackBaseOpts(data.path, destDirname, destFilename), webpackOpts
      webpack webpackAllOpts, (err, stats) ->
        if err
          throw new PluginError 'webpack', err
        log '[webpack]', stats.toString()
        lint relative(basedir, "#{destDirname}/#{destFilename}.js")
        reload data.path
        callback()
    flush = (callback) ->
      callback()
    through.obj transform, flush

  lint = (path) ->
    gulp.src path
      .pipe plumber PLUMBER_OPTS
      .pipe eslint lintOpts
      .pipe eslint.format()
      .pipe eslint.failAfterError()

  reload = (path) ->
    gulp.src path
      .pipe browserSync.reload
        stream: true

  gulp.src join WEBPACK_SRC, '/**/*.coffee'
    .pipe plumber PLUMBER_OPTS
    .pipe gulpif isSrcDir, cache 'webpack'
    .pipe build
      basedir    : __dirname
      src        : WEBPACK_SRC
      dest       : WEBPACK_DEST
      webpackOpts: webpackOpts()


###
images json
###
forEach IMAGES_JSON, (obj) ->
  forEach obj.srcs, (src) ->
    name = changeImagesJsonName src
    gulp.task "images-json-#{name}", ->

      build = (opts) ->
        { srcBase, fileName } = opts
        imagePaths = []
        transform = (data, encode, callback) ->
          imagePaths.push data.path.split(join(__dirname, srcBase))[1]
          callback()
        flush = (callback) ->
          @push new File
            path: "#{fileName}.json"
            contents: new Buffer JSON.stringify imagePaths, null, '  '
          callback()
        through.obj transform, flush

      gulp.src join obj.base, src, '/**/*.+(png|jpg|gif|svg)'
        .pipe plumber PLUMBER_OPTS
        .pipe build
          srcBase: obj.base
          fileName: name
        .pipe gulp.dest obj.dest
        .pipe browserSync.reload
          stream: true


###
style guide
###
gulp.task 'styleguide', ->
  gulp.src STYLEGUIDE_SRC_FILE
    .pipe plumber PLUMBER_OPTS
    .pipe styledown
      config: join STYLEGUIDE_BASE, '/config.md'
    .pipe gulp.dest STYLEGUIDE_DEST

gulp.task 'styleguide-stylus', ->

  stylusOpts =
    use: [ nib() ]
    compress: false
    # compress: gulpif isProduction, true, false
    sourcemap:
      inline: true

  gulp.src join STYLEGUIDE_STYLUS, '/**/*.styl'
    .pipe cache 'stylus'
    .pipe plumber PLUMBER_OPTS
    .pipe stylus stylusOpts
    .pipe sourcemaps.init
      loadMaps: true
    .pipe sourcemaps.write '.'
    .pipe bust
      base: 'htdocs'
    .pipe gulp.dest STYLEGUIDE_DEST
    .pipe url
      mode: 'relative'
      base: STYLEGUIDE_DEST
    .pipe gulp.dest STYLEGUIDE_DEST


###
javascript test
###
gulp.task 'test', ->
  watch [ join TEST_SRC , '/**/*' ], -> gulp.start 'test'
  karma.server.start
    configFile: TEST_CONFIG_SRC

```