#!/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化する #============================================================================ 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}#{reset}" code = ret.result if (createFileFlagTable[src.toString]?) return FS.appendFile(output, code, 'utf8') else return FS.outputFile(output, code, 'utf8') .then (compile_loop_err) -> if (!compile_loop_err?) createFileFlagTable[src.toString] = 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() #echo argopt target = process.argv target.splice(0, 2) argm = MINIMIST(target) #echo argm #============================================================================ # オプションを取得 #============================================================================ 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) #echo sourcepath #============================================================================ # ソースファイルが指定されていない #============================================================================ 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") switch (otype) when 1 # 出力先がディレクトリ  ofile = "#{output}/"+PATH.basename(fname).replace(/\.coffee$/, ".min.js") when 2 # 出力先がファイル  ofile = output console.log("#{green}compile and minify: [#{fname}] to [#{ofile}]#{reset}") srcrevlist = {} # 各ソースファイルがどこに吐かれるかのリスト outrevlist = {} # 各保存先がどのソースファイルから生成されるかのリスト watchlist = [] # 監視するソースファイルのchokidarオブジェクト配列 for srcinfo in sourcepath # 監視対象をひとつ取り出して、行末のスラッシュを除去する srcfname = (srcinfo.src).replace(/\/*$/, "") # 情報を取り出す stype = srcinfo.stype output = srcinfo.output otype = srcinfo.otype try # ソースに指定されたファイル/ディレクトリが存在するかチェック FS.accessSync(srcfname, FS.F_OK) switch (stype) when 1 # ディレクトリ監視 watchlist[srcfname] = CHOKIDAR.watch srcfname, ignored: /[\/\\]\./ persistent: true interval: 1000 depth: 1 # ソースファイルから生成ファイル逆引きリストを作る srclist = get_sourcelist_in_path(srcinfo) for finfo in srclist finfostr = finfo.src.replace(/[\.\/]/g, "") srcrevlist[finfostr] = output # 生成ファイルからソースファイル一覧リストを作る outrevlist[output] = [] if (!outrevlist[output]?) Array.prototype.push.apply(outrevlist[output], srclist) # ソースに指定されたディレクトリの更新を監視する console.log("#{cyan}watching file/directory: #{srcfname}#{reset}") watchlist[srcfname].on 'change', (fpath, stat) -> fname = PATH.basename(fpath) fstr = fpath.replace(/[\.\/]/g, "") output = srcrevlist[fstr] if (output?) flist = outrevlist[output].concat() if (PATH.extname(fname) == ".coffee") console.log("#{yellow}modify file: #{fname}#{reset}") compile_sourcelist(flist).then (err) -> console.log("#{green}compile done: "+new Date()+reset+"\n") .catch (err) -> echo err ### # ファイル追加 watchlist[srcfname].on 'add', (fpath, stat) -> fname = PATH.basename(fpath) dir = PATH.dirname(fpath) if (PATH.extname(fname) == ".coffee") console.log("watching file: [#{fname}]") # ファイル削除 watchlist[srcfname].on 'unlink', (fpath, stat) -> fname = PATH.basename(fpath) if (PATH.extname(fname) == ".coffee") console.log("delete file: [#{fname}]") ### when 2 # ファイル監視 # ソースに指定されたファイルの更新を監視する WATCHER.add srcfname catch watch_err # target is not accessible echo "watch_error" echo watch_err.toString() 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")