#!/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") ARGV = require("argv") MINIMIST = require("minimist") ASYNC = require("async") FS = require("fs-extra") PATH = require("path") PROMISE = require("bluebird") FORM = require("ndlog").form READLINE = require("readline") WATCHER = require("filewatcher") forcePolling: false debounce: 10 interval: 1000 persistent: true echo = require("ndlog").echo packinfo = require("./package.json") #============================================================================ # 色コード #============================================================================ 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化した結果をリストで返す # coffeecode = CoffeeScriptコード文字列 # ret = err: エラーコード 0=正常終了 0以外=エラー # message: 結果 # result: 正常終了の時はJS、エラーの時はエラーメッセージ #============================================================================ compile = (coffeecode) -> return new PROMISE (resolve, reject) -> if (nominify) inlineopt = !inline_map else inlineopt = true COFFEE_OPTS = inlineMap: inlineopt bare: true try COFFEE.fromSource coffeecode, COFFEE_OPTS, (compile_err, jsstr) -> if (compile_err?) reject err: compile_err.errno status: "compile error." message: compile_err.message else if (nominify) code = jsstr else if (inline_map) terser_opts = {} else terser_opts = sourceMap: url: "inline" code = (TERSER.minify(jsstr, terser_opts)).code resolve err: 0 status: "compiled." message: "" result: code catch e reject err: compile_err.errno status: "compile error." message: 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)-> return new Promise (resolve, reject) -> src = srcinfo.src output = srcinfo.output stype = srcinfo.stype otype = srcinfo.otype ret_srclist = [] if (stype == 1) # ディレクトリ FS.readdir src, (err, files) -> 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 resolve(ret_srclist) else if (stype == 2) # ファイル if (!src.match(/\.coffee$/)) reject(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 resolve(ret_srclist) #============================================================================ # 渡されたソースファイル名リストからソースを読み込んで配列にして返す #============================================================================ sourcelist_fileread = (sourcepath_list) -> return new Promise (resolve, reject) -> compile_strings = [] ASYNC.whilst -> # コンパイルするソースファイルがなくなったらループを抜ける if (sourcepath_list.length > 0) return true else return false , (callback) -> # ファイルパスをひとつ取り出す srcinfo = sourcepath_list.shift() src = srcinfo.src output = srcinfo.output # outputの最初の処理の時はリストを初期化する if (!compile_strings[output]?) compile_strings[output] = {} compile_strings[output]['code'] = "" compile_strings[output]['src'] = [] # ソースを読み込む FS.readFile src, "utf-8", (err, code) -> if (err) callback(err, null) else # 同じoutputのところに追記する compile_strings[output]['code'] += code compile_strings[output]['src'].push(src) callback(null, 0) , (err, result) -> if (result) reject(undefined) else resolve(compile_strings) #============================================================================ # 渡されたソースの配列をコンパイルして保存する #============================================================================ sourcelist_compile = (compile_strings) -> return new Promise (resolve, reject) -> # output一覧配列を取得(これでループを回す) output_list = Object.keys(compile_strings) # ループしながら順番に(同期して)コンパイルする ASYNC.whilst -> # コンパイルされるファイル名がなくなったらループを抜ける if (output_list.length > 0) return true else return false , (callback) -> # コンパイル対象ファイル名をひとつ取り出す output = output_list.shift() code = compile_strings[output]['code'] srclist = compile_strings[output]['src'] srclist.map (s) -> console.log "#{cyan}===> compile #{s}" compile(code).then (ret) -> minify = ret.result FS.writeFile output, minify, 'utf8', -> callback(null, 0) .catch (err) -> message = err.message console.log "#{red}#{message}#{reset}" , (ret, result) -> if (result < 0) reject(result) else resolve(0) #============================================================================ # 渡されたディレクトリ内の追加されたCoffeeScriptファイルを監視対象にする #============================================================================ setFileWatchIntoDirectory = (srcinfo) -> return new Promise (resolve, reject) -> src = srcinfo.src.replace(/\/*$/, "") stype = srcinfo.stype output = srcinfo.output otype = srcinfo.otype get_sourcelist_in_path(srcinfo).then (srclist) -> compile_list = [] ASYNC.whilst -> if (srclist.length > 0) true else false , (callback) -> f = srclist.shift() fname2 = (f.src).replace(/[\.\/]/g, "") # ソースファイルが追加された if (!SRC2OUTPUT[fname2]?) 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 compile_list.push(ofile) OUTPUT2SRCLIST[ofile] = [] if (!OUTPUT2SRCLIST[ofile]?) SRC2OUTPUT[fname2] = ofile OUTPUT2SRCLIST[ofile].push src: f.src output: ofile callback(null, 0) , (err, result) -> if (err) reject(-1) else resolve(compile_list) #============================================================================ # 渡された出力ファイルを構成するCoffeeScriptをコンパイルする #============================================================================ output2compile = (output)-> return new Promise (resolve, reject) -> srclist = OUTPUT2SRCLIST[output].concat() sourcelist_fileread(srclist).then (srcjoinlist) -> return sourcelist_compile(srcjoinlist) .then (err) -> if (err == 0) console.log("#{green}create [#{yellow}#{PATH.basename(output)}#{green}] done: "+new Date()+reset+"\n") .catch (err) -> if (err) reject(-1) else console.log("error: #{err}") 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]" ARGV.option name: "nomap" short: "n" type: "string" description: "not include inline sourceMap." example: "terffee -n" ARGV.option name: "nominify" short: "m" type: "string" description: "not minify source." example: "terffee -n" ARGV.option name: "version" short: "v" type: "string" description: "display this menu." example: "terffee -v" argopt = ARGV.run() if (argopt.options.version) console.log "ver #{packinfo.version}" process.exit(0) target = process.argv target.splice(0, 2) argm = MINIMIST(target) #============================================================================ # オプションを取得 #============================================================================ c_opt = argm.c || argm.compile outputlist_tmp = argm.o || argm.output inline_map = argm.n || argm.nomap nominify = argm.m || argm.nominify #============================================================================ # コンパイルするソースファイル一覧を取得する #============================================================================ sourcepath_tmp = [] directotypath = [] sourcepath_tmp = argm._ if (c_opt?) if (typeof c_opt == 'string') c_opt = [c_opt] 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(0) #============================================================================ # ソースファイルが指定されていない #============================================================================ if (target.length == 0) ARGV.run(["-h"]) process.exit(0) if (argm.w || argm.watch) #========================================================================== # ソースファイル/ディレクトリ監視 #========================================================================== WATCHER .on "change", (fpath, stat) -> fname2 = fpath.replace(/[\.\/]/g, "") if (!stat.deleted?) fname = PATH.basename(fpath) if (PATH.extname(fname) == ".coffee") # ファイル更新 output = SRC2OUTPUT[fname2] output2compile(output) else # ファイル追加 srcinfo = undefined # 追加されたディレクトリを取得 sourcepath.map (info) -> if (info.src == fpath) srcinfo = info # 追加されたディレクトリ内の追加されたファイルを取得 setFileWatchIntoDirectory(srcinfo).then (compile_list) -> # 追加されたファイルがあった場合は出力先を取り出す(コンパイルされる) if (compile_list.length > 0) output = compile_list[0] output2compile(output) else # 監視ファイル削除 WATCHER.remove(fpath) output = SRC2OUTPUT[fname2] idx = 0 i = 0 target_list = OUTPUT2SRCLIST[output] target_list.map (tmp) -> if (tmp.src == fpath) idx = i i++ target_list.splice(idx, 1) delete SRC2OUTPUT[fname2] if (target_list.length == 0) delete OUTPUT2SRCLIST[output] delete_output = output output = undefined try FS.unlink delete_output, (err) -> catch e output2compile(output) if (output?) # 監視対象を列挙 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("#{yellow}watching directory [#{green}#{src}#{reset}]") WATCHER.add srcinfo.src setFileWatchIntoDirectory(srcinfo) 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}") process.exit(0) WATCHER.close() else #========================================================================== # ソースファイルコンパイル #========================================================================== # ソース指定がディレクトリの場合を想定して展開する sourcepath_expand = [] ASYNC.whilst -> if (sourcepath.length > 0) return true else return false , (callback) -> srcinfo = sourcepath.shift() get_sourcelist_in_path(srcinfo).then (srclist) -> if (srclist?) Array.prototype.push.apply(sourcepath_expand, srclist) callback(null, 0) , (err, result) -> # コンパイルする sourcelist_fileread(sourcepath_expand).then (srcjoinlist) -> return sourcelist_compile(srcjoinlist) .then (err) -> if (err == 0) console.log("#{green}compile done: "+new Date()+reset+"\n")