#!/usr/bin/env coffee #============================================================================ # CoffeeScriptのソースファイルと保存先を指定し、コンパイル&minify化を行う # 「-c」オプションでソースファイルだけを指定した場合は、生成されるファイル # はソースファイルと同じ場所に保存される。 # 例)terffee -c hoge.coffee # # 「-o」オプションで保存先を指定する。 # 保存場所の最後をスラッシュにするか、すでに存在するディレクトリ名を指定し # た場合は、ディレクトリとみなしその中に生成したファイルが「.min.js」の拡 # 張子で保存される。 # 例)terffee -c hoge.coffee -o ./apps/js/ → ./apps/js/hoge.min.jsが生成される # # 最後がスラッシュではない場合は、指定したソースファイルのコンパイル&minify # されたものがひとつのファイルとして保存される。 # 例)terffee -c hoge.coffee -c foo.coffee -o hogefoo.min.js # # ソースファイルと保存場所の対応は、記述した順番になる。 # 保存先の数がソースファイルの数よりも少ない場合、足りない分は最後の保存場所 # がそのまま使われる。 # 例)terffee -c hoge.coffee -o hoge.min.js -c foo.coffee -o foo.min.js -c bar.coffee # (上記の例では、「bar.coffee」は「foo.min.js」に結合される) # # type: -1 指定したpathが存在しない # 0 (未使用) # 1 ディレクトリ # 2 ファイル  #============================================================================ TERSER = require("terser") COFFEE = require("coffee-compiler2") CHOKIDAR = require("chokidar") ARGV = require("argv") MINIMIST = require("minimist") ASYNC = require("async") FS = require("fs-extra") PATH = require("path") PROMISE = require("bluebird") FORM = require("ndlog").form WATCHER = require("filewatcher") forcePolling: false debounce: 10 interval: 1000 persistent: true echo = require("ndlog").echo #============================================================================ # 色コード #============================================================================ black = '\u001b[01;30m' red = '\u001b[01;31m' green = '\u001b[01;32m' yellow = '\u001b[01;33m' blue = '\u001b[01;34m' magenta = '\u001b[01;35m' cyan = '\u001b[01;36m' white = '\u001b[01;37m' reset = '\u001b[0m' #============================================================================ # CoffeeScriptをコンパイルし、minify化した結果をリストで返す # ret = err: エラーコード 0=正常終了 0以外=エラー # message: 結果 # result: 正常終了の時はJS、エラーの時はエラーメッセージ #============================================================================ compile = (fpath) -> return new PROMISE (resolve, reject) -> COFFEE_OPTS = sourceMap: true bare: true try COFFEE.fromFile fpath, COFFEE_OPTS, (compile_err, jsstr) -> if (compile_err?) reject err: compile_err.errno message: "compile error." result: compile_err.message else code = (TERSER.minify(jsstr)).code resolve err: 0 message: "compiled." result: code catch e reject err: compile_err.errno message: "compile error." result: compile_err.message #============================================================================ # 保存先として指定された場所/ファイルをチェックする #============================================================================ path_check = (path) -> if (path.match(/\/$/)) # pathがスラッシュで終わっている type = 1 # ディレクトリ fname = undefined else # pathがスラッシュで終わっていない try # pathが既存ディレクトリ if (FS.statSync(path).isDirectory()) type = 1 # ディレクトリ fname = undefined else type = 2 # ファイル fname = PATH.basename("./#{path}") catch path_check_err # pathが存在しない type = -1 # 存在しない fname = undefined return type: type fname: fname #============================================================================ # 渡されたソースがディレクトリの場合は中を探査しCoffeeScriptファイルを列挙し返す #============================================================================ get_sourcelist_in_path = (srcinfo)-> src = srcinfo.src output = srcinfo.output stype = srcinfo.stype otype = srcinfo.otype ret_srclist = [] if (stype == 1) # ディレクトリ files = FS.readdirSync(src) for srcfname in files srcfullpath = FORM("%@/%@", src, srcfname) # ソースの指定がCoffeeScriptではない、またはディレクトリの場合は処理しない if (!srcfname.match(/\.coffee$/) || FS.statSync(srcfullpath).isDirectory()) continue switch (otype) when 1 # 出力先がディレクトリ  ofile = "#{output}/"+PATH.basename(srcfname).replace(/\.coffee$/, ".min.js") when 2 # 出力先がファイル  ofile = output ret_srclist.push src: srcfullpath output: ofile else if (stype == 2) # ファイル if (!src.match(/\.coffee$/)) return undefined switch (otype) when 1 # 出力先がディレクトリ  ofile = "#{output}/"+PATH.basename(src).replace(/\.coffee$/, ".min.js") when 2 # 出力先がファイル  ofile = output ret_srclist.push src: src output: ofile return ret_srclist #============================================================================ # 渡されたソースファイル情報リストをコンパイルする #============================================================================ compile_sourcelist = (sourcepath_list) -> return new Promise (resolve, reject) -> createFileFlagTable = {} ASYNC.whilst -> # コンパイルするソースファイルがなくなったらループを抜ける if (sourcepath_list.length > 0) return true else return false , (callback) -> # ファイルパスをひとつ取り出す srcinfo = sourcepath_list.shift() src = srcinfo.src output = srcinfo.output compile(src).then (ret) -> console.log "#{cyan}===> compile #{src} -> #{yellow}#{output}#{reset}" code = ret.result # 最初の出力先への書き込みは新規作成、二回目からは追記 appendkey = output.replace(/[\.\/]/g, "") if (createFileFlagTable[appendkey]?) return FS.appendFile(output, code, 'utf8') else return FS.outputFile(output, code, 'utf8') .then (compile_loop_err) -> if (!compile_loop_err?) createFileFlagTable[output.replace(/[\.\/]/g, "")] = true callback(null, 1) .catch (ret) -> console.log "\n\n#{red}"+ret.result+"#{reset}" if (!ret.err?) callback(null, 1) , (ret, result) -> if (ret?) switch (ret.err) when -1, -2 console.log("#{red}"+ret.result+reset) reject(ret.err) else resolve(0) #============================================================================ #============================================================================ #============================================================================ #============================================================================ # メイン処理 #============================================================================ # 引数チェック ARGV.option name: "watch" short: "w" type: "string" description: "watch source file change." example: "terffee -wc [source file path]" ARGV.option name: "compile" short: "c" type: "path" description: "compile source file." example: "terffee -c [source file path]" ARGV.option name: "output" short: "o" type: "path" description: "compiled file output directory." example: "terffee -o [output directroy]" argopt = ARGV.run() target = process.argv target.splice(0, 2) argm = MINIMIST(target) #============================================================================ # オプションを取得 #============================================================================ c_opt = argm.c || argm.compile outputlist_tmp = argm.o || argm.output #============================================================================ # コンパイルするソースファイル一覧を取得する #============================================================================ sourcepath_tmp = [] directotypath = [] sourcepath_tmp = argm._ if (c_opt?) if (typeof c_opt == 'string') c_opt = [c_opt] #c_opt.push.apply(c_opt, argm._) sourcepath_tmp.push.apply(sourcepath_tmp, c_opt) #============================================================================ # コンパイル/minify化したファイルを保存する一覧を取得する  #============================================================================ if (typeof outputlist_tmp == "object") outputlist = outputlist_tmp else outputlist = [outputlist_tmp] #============================================================================ # 引数で指定されたソース一覧と保存先一覧を整理する #============================================================================ sourcepath = [] sourcepath_tmp.map (fpath, cnt) -> #=========================================================================== # ソースの種類(ファイルかディレクトリか)と存在するかチェック #=========================================================================== # 処理するファイル src = fpath stype = path_check(src).type # ソースに指定されたファイル/ディレクトリが存在する場合は処理する if (stype > 0) # 保存先リストからひとつ取り出す output = outputlist[cnt] || outputlist[outputlist.length-1] # 保存先がundefined if (!output?) # 保存先が無い場合は、保存先をsrcから生成する otype = 1 if (FS.statSync(src).isDirectory()) # srcがディレクトリだった output = src else # srcがファイルだった output = PATH.dirname(src) else # 保存先が存在する otype = path_check(output).type # outputが存在しなかったらファイル if (otype == -1) otype = 2 # srcの末尾に「/」があったら除去する src = src.replace(/\/*$/, "") # outputの末尾に「/」があったら除去する output = output.replace(/\/*$/, "") sourcepath.push src: src stype: stype output: output otype: otype else console.log "\n#{red}File/Directory not found: #{src}#{reset}" process.exit(-1) #============================================================================ # ソースファイルが指定されていない #============================================================================ if (target.length == 0) ARGV.run(["-h"]) process.exit(1) if (argm.w || argm.watch) #========================================================================== # ソースファイル/ディレクトリ監視 #========================================================================== # ファイル監視用のオブジェクト生成 WATCHER.on "change", (fpath, stat) -> fname = PATH.basename(fpath) if (PATH.extname(fname) == ".coffee") output = src2output[fpath.replace(/[\.\/]/g, "")] srclist = output2srclist[output].concat() # コンパイルする compile_sourcelist(srclist).then (err) -> if (err == 0) console.log("#{green}compile [#{yellow}#{PATH.basename(output)}#{green}] done: "+new Date()+reset+"\n") .catch (err) -> console.log("error: #{err}") # 監視対象を列挙 src2output = {} output2srclist = {} for srcinfo in sourcepath try # ソースとして指定されたファイル/ディレクトリが存在するかチェック FS.accessSync(srcinfo.src, FS.F_OK) # 監視対象をひとつ取り出して、行末のスラッシュを除去する src = srcinfo.src.replace(/\/*$/, "") stype = srcinfo.stype output = srcinfo.output otype = srcinfo.otype # 監視対象がディレクトリの場合は中のファイルを走査し処理する switch (stype) when 1 # 監視対象がディレクトリ console.log("watching directory [#{yellow}#{src}#{reset}]") srclist = get_sourcelist_in_path(srcinfo) for f in srclist WATCHER.add f.src # 出力先から出力ファイル名を生成する switch (otype) when 1 # 出力先がディレクトリ  fname = PATH.basename(f.src) ofile = "#{output}/"+PATH.basename(fname).replace(/\.coffee$/, ".min.js") when 2 # 出力先がファイル  ofile = output output2srclist[ofile] = [] if (!output2srclist[ofile]?) fname = (f.src).replace(/[\.\/]/g, "") src2output[fname] = ofile output2srclist[ofile].push src: f.src output: ofile when 2 # 監視対象がファイル console.log("watching file [#{yellow}#{src}#{reset}]") WATCHER.add src # 出力先から出力ファイル名を生成する switch (otype) when 1 # 出力先がディレクトリ  fname = PATH.basename(src) ofile = "#{output}/"+PATH.basename(fname).replace(/\.coffee$/, ".min.js") when 2 # 出力先がファイル  ofile = output output2srclist[ofile] = [] if (!output2srclist[ofile]?) # ソースファイルに対する出力先のファイル名を設定する src2output[src.replace(/[\.\/]/g, "")] = ofile output2srclist[ofile].push src: src output: ofile catch e #echo e console.log("File/Directory not found: #{src}") else #process.exit(0) #========================================================================== # ソースファイルコンパイル #========================================================================== # ソース指定がディレクトリの場合を想定して展開する sourcepath_expand = [] for srcinfo in sourcepath srclist = get_sourcelist_in_path(srcinfo) if (!srclist?) continue Array.prototype.push.apply(sourcepath_expand, srclist) # コンパイルする compile_sourcelist(sourcepath_expand).then (err) -> if (err == 0) console.log("#{green}compile done: "+new Date()+reset+"\n")