流行の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.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']
まあ、今のところはとりあえずビルドしてくれればいいんで、必要最低限のものです。
そのうち誰かがステキなライブラリを作ってくれることでしょう。