UNPKG

18.5 kBPlain TextView Raw
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
30TERSER = require("terser")
31COFFEE = require("coffee-compiler2")
32ARGV = require("argv")
33MINIMIST = require("minimist")
34ASYNC = require("async")
35FS = require("fs-extra")
36PATH = require("path")
37PROMISE = require("bluebird")
38FORM = require("ndlog").form
39READLINE = require("readline")
40WATCHER = require("filewatcher")
41 forcePolling: false
42 debounce: 10
43 interval: 1000
44 persistent: true
45
46echo = require("ndlog").echo
47packinfo = require("./package.json")
48
49#============================================================================
50# 色コード
51#============================================================================
52black = '\u001b[01;30m'
53red = '\u001b[01;31m'
54green = '\u001b[01;32m'
55yellow = '\u001b[01;33m'
56blue = '\u001b[01;34m'
57magenta = '\u001b[01;35m'
58cyan = '\u001b[01;36m'
59white = '\u001b[01;37m'
60reset = '\u001b[0m'
61
62#============================================================================
63# CoffeeScriptをコンパイルし、minify化した結果をリストで返す
64# coffeecode = CoffeeScriptコード文字列
65# ret = err: エラーコード 0=正常終了 0以外=エラー
66# message: 結果
67# result: 正常終了の時はJS、エラーの時はエラーメッセージ
68#============================================================================
69compile = (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#============================================================================
102path_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#============================================================================
129get_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#============================================================================
167sourcelist_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#============================================================================
208sourcelist_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#============================================================================
248setFileWatchIntoDirectory = (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# 引数チェック
283ARGV.option
284 name: "watch"
285 short: "w"
286 type: "string"
287 description: "watch source file change."
288 example: "terffee -wc [source file path]"
289ARGV.option
290 name: "compile"
291 short: "c"
292 type: "path"
293 description: "compile source file."
294 example: "terffee -c [source file path]"
295ARGV.option
296 name: "output"
297 short: "o"
298 type: "path"
299 description: "compiled file output directory."
300 example: "terffee -o [output directroy]"
301ARGV.option
302 name: "nomap"
303 short: "n"
304 type: "string"
305 description: "not include inline sourceMap."
306 example: "terffee -n"
307ARGV.option
308 name: "version"
309 short: "v"
310 type: "string"
311 description: "display this menu."
312 example: "terffee -v"
313argopt = ARGV.run()
314
315if (argopt.options.version)
316 console.log "ver #{packinfo.version}"
317 process.exit(0)
318
319target = process.argv
320target.splice(0, 2)
321argm = MINIMIST(target)
322
323#============================================================================
324# オプションを取得
325#============================================================================
326c_opt = argm.c || argm.compile
327outputlist_tmp = argm.o || argm.output
328inline_map = argm.n || argm.nomap
329
330#============================================================================
331# コンパイルするソースファイル一覧を取得する
332#============================================================================
333sourcepath_tmp = []
334directotypath = []
335sourcepath_tmp = argm._
336if (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#============================================================================
345if (typeof outputlist_tmp == "object")
346 outputlist = outputlist_tmp
347else
348 outputlist = [outputlist_tmp]
349
350#============================================================================
351# 引数で指定されたソース一覧と保存先一覧を整理する
352#============================================================================
353sourcepath = []
354sourcepath_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#============================================================================
403if (target.length == 0)
404 ARGV.run(["-h"])
405 process.exit(0)
406
407if (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
506else
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