エロサイトの作り方

2013年11月から勉強しながらエロサイトを作っています。

流行のgeneratorをCoffeeScript + Gulpでビルドする

最近CoffeeScriptでもgenerator(yield)構文が使えるようになったらしいので、 CoffeeScript初心者のくせに試してハマった記録です。

インストール

Node.js v0.11系を入れる

$ nvm ls-remote | grep v0.11
...
    v0.11.14

最新はv0.11.14。

$ nvm install v0.11.14

$ node -v
v0.11.14

$ nvm alias default v0.11.14

CoffeeScriptのgenerator対応版を入れる

$ npm install jashkenas/coffeescript -g

毎回オプション付けるのが面倒なのでaliasを設定する

.bashrcとかそこら辺に追記。

$ alias node='node --harmony_generators'

試してみる

generator.coffee

g = do ->
  yield 0
  yield 1
  return 2

console.log g.next()
console.log g.next()
console.log g.next()

実行すると、

$ coffee --nodejs --harmony_generators generator.coffee
child_process: customFds option is deprecated, use stdio instead.
{ value: 0, done: false }
{ value: 1, done: false }
{ value: 2, done: true }

実行できた。

が、なんか変なの出てる。

customFds option is deprecatedってなんだ?

child_process.spawn(command, [args], [options])

options Object

customFds Array Deprecated File descriptors for the child to use for stdio. (See below)

Child Process Node.js v0.10.32 Manual & Documentation

child_process.spawanのオプションらしい。

ドキュメント上ではDeprecatedなのはわかるけど、CoffeeScript内部で使っているものだからなぁ……

あまり詳しい情報はなかったけど、どうやらv0.11.14から発生しているらしい。

v0.11.13に落としてみる

$ nvm use v0.11.13
$ npm install jashkenas/coffeescript -g
$ coffee --nodejs --harmony_generators generators.coffee
{ value: 0, done: false }
{ value: 1, done: false }
{ value: 2, done: true }

メッセージが消えた。

$ nvm alias default v0.11.13

特に困る点もないのでv0.11.13を使うようにしよう。

Gulpでビルドさせる

まず、nodeのバージョンを変えたのでgulpを入れ直す。

$ npm install gulp -g

そして、以前作ったgulpfile.coffeeを使い、

$ gulp

そのまま実行するとエラー。

[ERROR:gulp-coffee] SyntaxError
/Users/hentai-kun/node/sandbox/generators.coffee:2:3: error: reserved word "yield"
  yield 0
  ^

何かオプションを足せばいいだろうと思ってたけど……

gulp.task 'compile', ->
  gulp.src csSrc
    .pipe plumber(errorHandler: errorHandler)
    .pipe coffee
      bare: true
    .pipe gulp.dest(dest)
  return

gulp-coffeeのオプション指定の仕方が分からない。

    .pipe coffee
      bare: true
      '--nodejs': true
      '--harmony_generators': true

とかでいけるかと思いきや、ダメだった。

gulp-coffeeのREADMEにはオプションの説明が無い

しかもサイト上にはオプションの説明は無い。

wearefractal/gulp-coffee · GitHub

テストを見ても使っているのはbare, header, literateくらい。

仕方ないので、ソースを追ってみる。

gulp-coffee/index.js

var coffee = require('coffee-script');
...

    var options = merge({
      bare: false,
      header: false,
      sourceMap: !!file.sourceMap,
      sourceRoot: false,
      literate: /\.(litcoffee|coffee\.md)$/.test(file.path),
      filename: file.path,
      sourceFiles: [file.relative],
      generatedFile: replaceExtension(file.relative)
    }, opt);

    try {
      data = coffee.compile(str, options);
    } catch (err) {
      return cb(new PluginError('gulp-coffee', err));
    }

初期値を設定して、coffee-scriptモジュールに投げているだけだった。

coffee-scriptモジュールでやっていること

gulp-coffeeが思った以上にペラいラッパーライブラリだったのでcoffee-scriptを追う。

coffee-script.coffee

gulp-coffeeで使っているcompile()を起点に動かすと、

exports.compile = compile = withPrettyErrors (code, options) ->
  {merge, extend} = helpers
  options = extend {}, options

  if options.sourceMap
    map = new SourceMap

  fragments = parser.parse(lexer.tokenize code, options).compileToFragments options

coffee-script.coffeeのlexer.tokenize code, optionsでエラーになる。

lexer.tokenize内で見ているoptionsの値には--nodejs--harmonyは無いので、gulp-coffeeではharmonyオプションには対応していないっぽい。

一方で、command.coffeeには気になる処理があった。

command.coffee

exports.run = ->
  parseOptions()
  # Make the REPL *CLI* use the global context so as to (a) be consistent with the
  # `node` REPL CLI and, therefore, (b) make packages that modify native prototypes
  # (such as 'colors' and 'sugar') work as expected.
  replCliOpts = useGlobal: yes
  return forkNode()                             if opts.nodejs

command.coffeeのrun()は/bin/coffeeで呼ばれるファイルなので、 コマンドラインで実行した場合に最初に実行されるところ。

bin/coffee

#!/usr/bin/env node

var path = require('path');
var fs   = require('fs');
var lib  = path.join(path.dirname(fs.realpathSync(__filename)), '../lib');

require(lib + '/coffee-script/command').run();

こんな内容。

そして、forkNodeを見てみると、

command.coffee

forkNode = ->
  nodeArgs = opts.nodejs.split /\s+/
  args     = process.argv[1..]
  args.splice args.indexOf('--nodejs'), 2
  p = spawn process.execPath, nodeArgs.concat(args),
    cwd:        process.cwd()
    env:        process.env
    customFds:  [0, 1, 2]
  p.on 'exit', (code) -> process.exit code

とりあえずここにcustomFdsがいた。こいつがv0.11.14でエラーを出している原因ですね。

で、何か良くわからないけど--nodejsオプションを除いてnodeコマンドを呼び直している。 おそらくこの処理をgulp-coffeeに実装し直してやればビルドしてくれそうな気がする。

だけど、疲れたのでgulp-coffeeを直す余力は無かった。

独自の手抜き実装をする

長いこと追いかけたけど、結局はcoffeeコマンドを呼んでいるだけなのだからchild_processで直接呼んでしまえばよいのでは、という開き直りにより実装したのが以下のもの。

gulpfile.coffee

gulp     = require 'gulp'
plumber  = require 'gulp-plumber'
gutil    = require 'gulp-util'
changed  = require 'gulp-changed'

chalk    = require 'chalk'
notifier = require 'node-notifier'
inspect  = require('util').inspect
path     = require 'path'
through  = require 'through2'
exec     = require('child_process').exec


csSrc = './coffee/src/**/*.coffee'
jsSrc = './src/**'
dest  = './dest'


errorHandler = (err) ->
  title    = '[ERROR:' + err.plugin + '] ' + err.name
  filename = if err.filename? then path.relative(process.env.PWD, err.filename)

  console.log chalk.white.bgRed.bold title
  console.log err.toString()

  notifier.notify
    title:   err.message,
    message: filename + '\n' + inspect(err.location),
    sound:   'Ping'


compileCoffee = (opt) ->
  return through.obj (file, enc, cb) ->
    console.log file.path
    cmd  = 'coffee ' + opt + ' --print ' + file.path
    exec cmd, (err, stdout, stderr) ->
      if stderr
        console.log 'stderr', stderr
        cb new gutil.PluginError 'compile-coffee',
          message: stderr
          filename: file.path
      else
        file.path     = gutil.replaceExtension file.path, '.js'
        file.contents = new Buffer stdout
        cb null, file


gulp.task 'compile', ->
  gulp.src csSrc
    .pipe plumber(errorHandler: errorHandler)
    .pipe changed(dest, extension:'.js')
    .pipe compileCoffee('--compile --bare --nodejs --harmony_generators')
    .pipe gulp.dest(dest)
  return


gulp.task 'copy', ->
  gulp.src jsSrc
    .pipe gulp.dest(dest)


gulp.task 'watch', ['copy', 'compile'], ->
  gulp.watch csSrc, ['compile']
  gulp.watch jsSrc, ['copy']


gulp.task 'default', ['copy', 'compile']

まあ、今のところはとりあえずビルドしてくれればいいんで、必要最低限のものです。

そのうち誰かがステキなライブラリを作ってくれることでしょう。