1 | #!/usr/bin/env coffee
|
2 | #============================================================================
|
3 | # CoffeeScriptのソースファイルと保存先を指定し、コンパイル&minify化を行う
|
4 | # 「-c」オプションでソースファイルだけを指定した場合は、生成されるファイル
|
5 | # はソースファイルと同じ場所に保存される。
|
6 | # 例)terffee -c hoge.coffee
|
7 | #
|
8 | # 「-o」オプションで保存先を指定する。
|
9 | # 保存場所の最後をスラッシュにするか、すでに存在するディレクトリ名を指定し
|
10 | # た場合は、ディレクトリとみなしその中に生成したファイルが「.min.js」の拡
|
11 | # 張子で保存される。
|
12 | # 例)terffee -c hoge.coffee -o ./apps/js/ → ./apps/js/hoge.min.jsが生成される
|
13 | #
|
14 | # 最後がスラッシュではない場合は、指定したソースファイルのコンパイル&minify
|
15 | # されたものがひとつのファイルとして保存される。
|
16 | # 例)terffee -c hoge.coffee -c foo.coffee -o hogefoo.min.js
|
17 | #
|
18 | # ソースファイルと保存場所の対応は、記述した順番になる。
|
19 | # 保存先の数がソースファイルの数よりも少ない場合、足りない分は最後の保存場所
|
20 | # がそのまま使われる。
|
21 | # 例)terffee -c hoge.coffee -o hoge.min.js -c foo.coffee -o foo.min.js -c bar.coffee
|
22 | # (上記の例では、「bar.coffee」は「foo.min.js」に結合される)
|
23 | #
|
24 | # type: -1 指定したpathが存在しない
|
25 | # 0 (未使用)
|
26 | # 1 ディレクトリ
|
27 | # 2 ファイル
|
28 | #============================================================================
|
29 |
|
30 | TERSER = require("terser")
|
31 | COFFEE = require("coffee-compiler2")
|
32 | ARGV = require("argv")
|
33 | MINIMIST = require("minimist")
|
34 | ASYNC = require("async")
|
35 | FS = require("fs-extra")
|
36 | PATH = require("path")
|
37 | PROMISE = require("bluebird")
|
38 | FORM = require("ndlog").form
|
39 | READLINE = require("readline")
|
40 | WATCHER = require("filewatcher")
|
41 | forcePolling: false
|
42 | debounce: 10
|
43 | interval: 1000
|
44 | persistent: true
|
45 |
|
46 | echo = require("ndlog").echo
|
47 | packinfo = require("./package.json")
|
48 |
|
49 | #============================================================================
|
50 | # 色コード
|
51 | #============================================================================
|
52 | black = '\u001b[01;30m'
|
53 | red = '\u001b[01;31m'
|
54 | green = '\u001b[01;32m'
|
55 | yellow = '\u001b[01;33m'
|
56 | blue = '\u001b[01;34m'
|
57 | magenta = '\u001b[01;35m'
|
58 | cyan = '\u001b[01;36m'
|
59 | white = '\u001b[01;37m'
|
60 | reset = '\u001b[0m'
|
61 |
|
62 | #============================================================================
|
63 | # CoffeeScriptをコンパイルし、minify化した結果をリストで返す
|
64 | # coffeecode = CoffeeScriptコード文字列
|
65 | # ret = err: エラーコード 0=正常終了 0以外=エラー
|
66 | # message: 結果
|
67 | # result: 正常終了の時はJS、エラーの時はエラーメッセージ
|
68 | #============================================================================
|
69 | compile = (coffeecode) ->
|
70 | return new PROMISE (resolve, reject) ->
|
71 | COFFEE_OPTS =
|
72 | inlineMap: true
|
73 | bare: true
|
74 | try
|
75 | COFFEE.fromSource coffeecode, COFFEE_OPTS, (compile_err, jsstr) ->
|
76 | if (compile_err?)
|
77 | reject
|
78 | err: compile_err.errno
|
79 | message: "compile error."
|
80 | result: compile_err.message
|
81 | else
|
82 | if (inline_map)
|
83 | terser_opts = {}
|
84 | else
|
85 | terser_opts =
|
86 | sourceMap:
|
87 | url: "inline"
|
88 | code = (TERSER.minify(jsstr, terser_opts)).code
|
89 | resolve
|
90 | err: 0
|
91 | message: "compiled."
|
92 | result: code
|
93 | catch e
|
94 | reject
|
95 | err: compile_err.errno
|
96 | message: "compile error."
|
97 | result: compile_err.message
|
98 |
|
99 | #============================================================================
|
100 | # 指定された場所/ファイルをチェックする
|
101 | #============================================================================
|
102 | path_check = (path) ->
|
103 | if (path.match(/\/$/))
|
104 | # pathがスラッシュで終わっている
|
105 | type = 1 # ディレクトリ
|
106 | fname = undefined
|
107 | else
|
108 | # pathがスラッシュで終わっていない
|
109 | try
|
110 | # pathが既存ディレクトリ
|
111 | if (FS.statSync(path).isDirectory())
|
112 | type = 1 # ディレクトリ
|
113 | fname = undefined
|
114 | else
|
115 | type = 2 # ファイル
|
116 | fname = PATH.basename("./#{path}")
|
117 | catch path_check_err
|
118 | # pathが存在しない
|
119 | type = -1 # 存在しない
|
120 | fname = undefined
|
121 |
|
122 | return
|
123 | type: type
|
124 | fname: fname
|
125 |
|
126 | #============================================================================
|
127 | # 渡されたソースがディレクトリの場合は中を探査しCoffeeScriptファイルを列挙し返す
|
128 | #============================================================================
|
129 | get_sourcelist_in_path = (srcinfo)->
|
130 | src = srcinfo.src
|
131 | output = srcinfo.output
|
132 | stype = srcinfo.stype
|
133 | otype = srcinfo.otype
|
134 |
|
135 | ret_srclist = []
|
136 | if (stype == 1) # ディレクトリ
|
137 | files = FS.readdir(src)
|
138 | for srcfname in files
|
139 | srcfullpath = FORM("%@/%@", src, srcfname)
|
140 | # ソースの指定がCoffeeScriptではない、またはディレクトリの場合は処理しない
|
141 | if (!srcfname.match(/\.coffee$/) || FS.statSync(srcfullpath).isDirectory())
|
142 | continue
|
143 | switch (otype)
|
144 | when 1 # 出力先がディレクトリ
|
145 | ofile = "#{output}/"+PATH.basename(srcfname).replace(/\.coffee$/, ".min.js")
|
146 | when 2 # 出力先がファイル
|
147 | ofile = output
|
148 | ret_srclist.push
|
149 | src: srcfullpath
|
150 | output: ofile
|
151 | else if (stype == 2) # ファイル
|
152 | if (!src.match(/\.coffee$/))
|
153 | return undefined
|
154 | switch (otype)
|
155 | when 1 # 出力先がディレクトリ
|
156 | ofile = "#{output}/"+PATH.basename(src).replace(/\.coffee$/, ".min.js")
|
157 | when 2 # 出力先がファイル
|
158 | ofile = output
|
159 | ret_srclist.push
|
160 | src: src
|
161 | output: ofile
|
162 | return ret_srclist
|
163 |
|
164 | #============================================================================
|
165 | # 渡されたソースファイル名リストからソースを読み込んで配列にして返す
|
166 | #============================================================================
|
167 | sourcelist_fileread = (sourcepath_list) ->
|
168 | return new Promise (resolve, reject) ->
|
169 | compile_strings = []
|
170 | ASYNC.whilst ->
|
171 | # コンパイルするソースファイルがなくなったらループを抜ける
|
172 | if (sourcepath_list.length > 0)
|
173 | return true
|
174 | else
|
175 | return false
|
176 |
|
177 | , (callback) ->
|
178 | # ファイルパスをひとつ取り出す
|
179 | srcinfo = sourcepath_list.shift()
|
180 | src = srcinfo.src
|
181 | output = srcinfo.output
|
182 |
|
183 | # outputの最初の処理の時はリストを初期化する
|
184 | if (!compile_strings[output]?)
|
185 | compile_strings[output] = {}
|
186 | compile_strings[output]['code'] = ""
|
187 | compile_strings[output]['src'] = []
|
188 |
|
189 | # ソースを読み込む
|
190 | FS.readFile src, "utf-8", (err, code) ->
|
191 | if (err)
|
192 | callback(err, null)
|
193 | else
|
194 | # 同じoutputのところに追記する
|
195 | compile_strings[output]['code'] += code
|
196 | compile_strings[output]['src'].push(src)
|
197 | callback(null, 0)
|
198 |
|
199 | , (err, result) ->
|
200 | if (result)
|
201 | reject(undefined)
|
202 | else
|
203 | resolve(compile_strings)
|
204 |
|
205 | #============================================================================
|
206 | # 渡されたソースの配列をコンパイルして保存する
|
207 | #============================================================================
|
208 | sourcelist_compile = (compile_strings) ->
|
209 | return new Promise (resolve, reject) ->
|
210 | # output一覧配列を取得(これでループを回す)
|
211 | output_list = Object.keys(compile_strings)
|
212 |
|
213 | # ループしながら順番に(同期して)コンパイルする
|
214 | ASYNC.whilst ->
|
215 | # コンパイルされるファイル名がなくなったらループを抜ける
|
216 | if (output_list.length > 0)
|
217 | return true
|
218 | else
|
219 | return false
|
220 |
|
221 | , (callback) ->
|
222 | # 生成ファイルをひとつ取り出す
|
223 | output = output_list.shift()
|
224 | code = compile_strings[output]['code']
|
225 | srclist = compile_strings[output]['src']
|
226 | srclist.map (s) ->
|
227 | console.log "#{cyan}===> compile #{s}"
|
228 | compile(code).then (ret) ->
|
229 | minify = ret.result
|
230 | err = ret.err
|
231 | message = ret.message
|
232 | if (err < 0)
|
233 | callback(err, null)
|
234 | else
|
235 | FS.writeFile output, minify, 'utf8', ->
|
236 | callback(null, 0)
|
237 |
|
238 | , (ret, result) ->
|
239 | if (result < 0)
|
240 | reject(result)
|
241 | else
|
242 | resolve(0)
|
243 |
|
244 |
|
245 | #============================================================================
|
246 | # 渡されたディレクトリ内のCoffeeScriptファイルを監視対象にする
|
247 | #============================================================================
|
248 | setFileWatchIntoDirectory = (srcinfo) ->
|
249 | src = srcinfo.src.replace(/\/*$/, "")
|
250 | stype = srcinfo.stype
|
251 | output = srcinfo.output
|
252 | otype = srcinfo.otype
|
253 | srclist = get_sourcelist_in_path(srcinfo)
|
254 | compile_list = []
|
255 | for f in srclist
|
256 | fname2 = (f.src).replace(/[\.\/]/g, "")
|
257 | if (!src2output[fname2]?)
|
258 | WATCHER.add f.src
|
259 | # 出力先から出力ファイル名を生成する
|
260 | switch (otype)
|
261 | when 1 # 出力先がディレクトリ
|
262 | fname = PATH.basename(f.src)
|
263 | ofile = "#{output}/"+PATH.basename(fname).replace(/\.coffee$/, ".min.js")
|
264 | when 2 # 出力先がファイル
|
265 | ofile = output
|
266 | compile_list.push(ofile)
|
267 | output2srclist[ofile] = [] if (!output2srclist[ofile]?)
|
268 | src2output[fname2] = ofile
|
269 | output2srclist[ofile].push
|
270 | src: f.src
|
271 | output: ofile
|
272 | return compile_list
|
273 |
|
274 | #============================================================================
|
275 | #============================================================================
|
276 | #============================================================================
|
277 |
|
278 | #============================================================================
|
279 | # メイン処理
|
280 | #============================================================================
|
281 |
|
282 | # 引数チェック
|
283 | ARGV.option
|
284 | name: "watch"
|
285 | short: "w"
|
286 | type: "string"
|
287 | description: "watch source file change."
|
288 | example: "terffee -wc [source file path]"
|
289 | ARGV.option
|
290 | name: "compile"
|
291 | short: "c"
|
292 | type: "path"
|
293 | description: "compile source file."
|
294 | example: "terffee -c [source file path]"
|
295 | ARGV.option
|
296 | name: "output"
|
297 | short: "o"
|
298 | type: "path"
|
299 | description: "compiled file output directory."
|
300 | example: "terffee -o [output directroy]"
|
301 | ARGV.option
|
302 | name: "nomap"
|
303 | short: "n"
|
304 | type: "string"
|
305 | description: "not include inline sourceMap."
|
306 | example: "terffee -n"
|
307 | ARGV.option
|
308 | name: "version"
|
309 | short: "v"
|
310 | type: "string"
|
311 | description: "display this menu."
|
312 | example: "terffee -v"
|
313 | argopt = ARGV.run()
|
314 |
|
315 | if (argopt.options.version)
|
316 | console.log "ver #{packinfo.version}"
|
317 | process.exit(0)
|
318 |
|
319 | target = process.argv
|
320 | target.splice(0, 2)
|
321 | argm = MINIMIST(target)
|
322 |
|
323 | #============================================================================
|
324 | # オプションを取得
|
325 | #============================================================================
|
326 | c_opt = argm.c || argm.compile
|
327 | outputlist_tmp = argm.o || argm.output
|
328 | inline_map = argm.n || argm.nomap
|
329 |
|
330 | #============================================================================
|
331 | # コンパイルするソースファイル一覧を取得する
|
332 | #============================================================================
|
333 | sourcepath_tmp = []
|
334 | directotypath = []
|
335 | sourcepath_tmp = argm._
|
336 | if (c_opt?)
|
337 | if (typeof c_opt == 'string')
|
338 | c_opt = [c_opt]
|
339 | #c_opt.push.apply(c_opt, argm._)
|
340 | sourcepath_tmp.push.apply(sourcepath_tmp, c_opt)
|
341 |
|
342 | #============================================================================
|
343 | # コンパイル/minify化したファイルを保存する一覧を取得する
|
344 | #============================================================================
|
345 | if (typeof outputlist_tmp == "object")
|
346 | outputlist = outputlist_tmp
|
347 | else
|
348 | outputlist = [outputlist_tmp]
|
349 |
|
350 | #============================================================================
|
351 | # 引数で指定されたソース一覧と保存先一覧を整理する
|
352 | #============================================================================
|
353 | sourcepath = []
|
354 | sourcepath_tmp.map (fpath, cnt) ->
|
355 |
|
356 | #===========================================================================
|
357 | # ソースの種類(ファイルかディレクトリか)と存在するかチェック
|
358 | #===========================================================================
|
359 | # 処理するファイル
|
360 | src = fpath
|
361 | stype = path_check(src).type
|
362 | # ソースに指定されたファイル/ディレクトリが存在する場合は処理する
|
363 | if (stype > 0)
|
364 | # 保存先リストからひとつ取り出す
|
365 | output = outputlist[cnt] || outputlist[outputlist.length-1]
|
366 |
|
367 | # 保存先がundefined
|
368 | if (!output?)
|
369 | # 保存先が無い場合は、保存先をsrcから生成する
|
370 | otype = 1
|
371 | if (FS.statSync(src).isDirectory())
|
372 | # srcがディレクトリだった
|
373 | output = src
|
374 | else
|
375 | # srcがファイルだった
|
376 | output = PATH.dirname(src)
|
377 |
|
378 | else
|
379 | # 保存先が存在する
|
380 | otype = path_check(output).type
|
381 | # outputが存在しなかったらファイル
|
382 | if (otype == -1)
|
383 | otype = 2
|
384 |
|
385 | # srcの末尾に「/」があったら除去する
|
386 | src = src.replace(/\/*$/, "")
|
387 | # outputの末尾に「/」があったら除去する
|
388 | output = output.replace(/\/*$/, "")
|
389 |
|
390 | sourcepath.push
|
391 | src: src
|
392 | stype: stype
|
393 | output: output
|
394 | otype: otype
|
395 | else
|
396 |
|
397 | console.log "\n#{red}File/Directory not found: #{src}#{reset}"
|
398 | process.exit(0)
|
399 |
|
400 | #============================================================================
|
401 | # ソースファイルが指定されていない
|
402 | #============================================================================
|
403 | if (target.length == 0)
|
404 | ARGV.run(["-h"])
|
405 | process.exit(0)
|
406 |
|
407 | if (argm.w || argm.watch)
|
408 | #==========================================================================
|
409 | # ソースファイル/ディレクトリ監視
|
410 | #==========================================================================
|
411 | WATCHER
|
412 | .on "change", (fpath, stat) ->
|
413 | fname2 = fpath.replace(/[\.\/]/g, "")
|
414 | if (!stat.deleted?)
|
415 | fname = PATH.basename(fpath)
|
416 | # ファイル更新
|
417 | if (PATH.extname(fname) == ".coffee")
|
418 | output = src2output[fname2]
|
419 | else
|
420 | # ファイル追加
|
421 | srcinfo = undefined
|
422 | sourcepath.map (info) ->
|
423 | if (info.src == fpath)
|
424 | srcinfo = info
|
425 | compile_list = setFileWatchIntoDirectory(srcinfo)
|
426 | if (compile_list.length > 0)
|
427 | output = compile_list[0]
|
428 | else
|
429 | output = undefined
|
430 |
|
431 | else
|
432 | # 監視ファイル削除
|
433 | WATCHER.remove(fpath)
|
434 | output = src2output[fname2]
|
435 | idx = 0
|
436 | i = 0
|
437 | target_list = output2srclist[output]
|
438 | target_list.map (tmp) ->
|
439 | if (tmp.src == fpath)
|
440 | idx = i
|
441 | i++
|
442 | target_list.splice(idx, 1)
|
443 | delete src2output[fname2]
|
444 | if (target_list.length == 0)
|
445 | delete output2srclist[output]
|
446 | delete_output = output
|
447 | output = undefined
|
448 | FS.unlink(delete_output)
|
449 |
|
450 | if (output?)
|
451 | # コンパイルする
|
452 | srclist = output2srclist[output].concat()
|
453 | sourcelist_fileread(srclist).then (srcjoinlist) ->
|
454 | return sourcelist_compile(srcjoinlist)
|
455 | .then (err) ->
|
456 | if (err == 0)
|
457 | console.log("#{green}create [#{yellow}#{PATH.basename(output)}#{green}] done: "+new Date()+reset+"\n")
|
458 | .catch (err) ->
|
459 | console.log("error: #{err}")
|
460 |
|
461 |
|
462 | # 監視対象を列挙
|
463 | src2output = {}
|
464 | output2srclist = {}
|
465 | for srcinfo in sourcepath
|
466 | try
|
467 | # ソースとして指定されたファイル/ディレクトリが存在するかチェック
|
468 | FS.accessSync(srcinfo.src, FS.F_OK)
|
469 | # 監視対象をひとつ取り出して、行末のスラッシュを除去する
|
470 | src = srcinfo.src.replace(/\/*$/, "")
|
471 | stype = srcinfo.stype
|
472 | output = srcinfo.output
|
473 | otype = srcinfo.otype
|
474 |
|
475 | # 監視対象がディレクトリの場合は中のファイルを走査し処理する
|
476 | switch (stype)
|
477 | when 1 # 監視対象がディレクトリ
|
478 | console.log("#{yellow}watching directory [#{green}#{src}#{reset}]")
|
479 | WATCHER.add srcinfo.src
|
480 | setFileWatchIntoDirectory(srcinfo)
|
481 |
|
482 | when 2 # 監視対象がファイル
|
483 | console.log("watching file [#{yellow}#{src}#{reset}]")
|
484 | WATCHER.add src
|
485 | # 出力先から出力ファイル名を生成する
|
486 | switch (otype)
|
487 | when 1 # 出力先がディレクトリ
|
488 | fname = PATH.basename(src)
|
489 | ofile = "#{output}/"+PATH.basename(fname).replace(/\.coffee$/, ".min.js")
|
490 | when 2 # 出力先がファイル
|
491 | ofile = output
|
492 | output2srclist[ofile] = [] if (!output2srclist[ofile]?)
|
493 | # ソースファイルに対する出力先のファイル名を設定する
|
494 | src2output[src.replace(/[\.\/]/g, "")] = ofile
|
495 | output2srclist[ofile].push
|
496 | src: src
|
497 | output: ofile
|
498 |
|
499 | catch e
|
500 | #echo e
|
501 | console.log("File/Directory not found: #{src}")
|
502 | process.exit(0)
|
503 | WATCHER.close()
|
504 |
|
505 |
|
506 | else
|
507 |
|
508 | #==========================================================================
|
509 | # ソースファイルコンパイル
|
510 | #==========================================================================
|
511 | # ソース指定がディレクトリの場合を想定して展開する
|
512 | sourcepath_expand = []
|
513 | for srcinfo in sourcepath
|
514 | srclist = get_sourcelist_in_path(srcinfo)
|
515 | if (!srclist?)
|
516 | continue
|
517 | Array.prototype.push.apply(sourcepath_expand, srclist)
|
518 |
|
519 | # コンパイルする
|
520 | sourcelist_fileread(sourcepath_expand).then (srcjoinlist) ->
|
521 | return sourcelist_compile(srcjoinlist)
|
522 | .then (err) ->
|
523 | if (err == 0)
|
524 | console.log("#{green}compile done: "+new Date()+reset+"\n")
|
525 |
|
526 |
|
527 |
|