UNPKG

18.6 kBJavaScriptView Raw
1//-
2//- Usage
3//- litejs build
4//-
5//- build options
6//- --banner, -b Add commented banner to output
7//- --input, -i Input file
8//- --output, -o Output file
9//- --readme, -r Replase readme tags in file
10//-
11//- Examples
12//- litejs build -r README.md -i ui/dev.html -o ui/index.html
13//-
14
15
16var undef, conf
17, fs = require("fs")
18, spawn = require("child_process").spawn
19, now = new Date()
20, path = require("../path")
21, events = require("../events")
22, cli = require("./")
23, Fn = require("../fn.js").Fn
24, files = {}
25, fileHashes = {}
26, hasOwn = files.hasOwnProperty
27, adapters = File.adapters = {
28 css: {
29 split: cssSplit, min: cssMin, banner: "/*! {0} */\n"
30 },
31 html: {
32 split: htmlSplit, sep: "", banner: "<!-- {0} -->\n",
33 transpilers: {
34 js: jsToHtml, css: cssToHtml
35 }
36 },
37 js: {
38 min: jsMin, banner: "/*! {0} */\n",
39 transpilers: {
40 tpl: tplToJs, view: tplToJs
41 }
42 },
43 tpl: {
44 min: tplMin, banner: "/{0}\n"
45 }
46}
47, translate = {
48 // http://nodejs.org/api/documentation.html
49 stability: "0 - Deprecated,1 - Experimental,2 - Unstable,3 - Stable,4 - API Frozen,5 - Locked".split(","),
50 date: now.toISOString().split("T")[0]
51}
52, linked = __dirname.indexOf(process.cwd()) !== 0
53
54adapters.view = adapters.tpl
55
56try {
57 conf = require(path.resolve("package.json"))
58} catch(e) {
59 console.error(e)
60 conf = {}
61}
62
63if (linked) {
64 module.paths = require("module")._nodeModulePaths(process.cwd())
65 // module.paths.push(path.resolve(process.env.npm_config_prefix, "lib", "node_modules"))
66 // module.paths.push(path.resolve(process.cwd(), "node_modules"))
67}
68
69function File(_name, _opts) {
70 var file = this
71 , name = _name === "-" ? _name : path.resolve(_name.split("?")[0])
72
73 if (_name && files[name]) {
74 return files[name]
75 }
76 if (!(file instanceof File)) {
77 return new File(_name, _opts)
78 }
79
80 var opts = file.opts = _opts || {}
81 , ext = file.ext = opts.ext || (
82 name === "-" ?
83 "" + opts.input :
84 name
85 ).split(".").pop()
86
87 files[name] = file
88 file._depends = []
89 file.write = file.write.bind(file)
90
91 if (!("root" in opts)) {
92 opts.root = name.replace(/[^\/]*$/, "")
93 }
94 file.name = opts.name = name//.slice(opts.root.length)
95
96 if (typeof opts.input == "string") {
97 opts.input = [ opts.input ]
98 }
99 if (!opts.warnings) opts.warnings = []
100
101 if (opts.sourceMap === true) {
102 opts.sourceMap = name.replace(/\?|$/, ".map$&").slice(opts.root.length)
103 }
104 if (opts.drop) {
105 if (!opts.replace) {
106 opts.replace = []
107 }
108 opts.replace.push(
109 [ new RegExp("\\/\\/(?=\\*\\*\\s+(?:" + opts.drop.replace(/[\s,]+/g, "|") + "))", "g"), "/"],
110 [ new RegExp("\\/(\\*{2,})\\s+(?:" + opts.drop.replace(/[^\w]+/g, "|") + ")\\s+\\1\\/", "g"), "$&/*"]
111 )
112 }
113
114 file.reset()
115
116 setImmediate(file.wait())
117
118 file.build()
119
120 return file
121}
122
123File.prototype = {
124 wait: Fn.hold,
125 syncMethods: ["on", "toString"],
126 depends: function(child) {
127 var file = this
128 child.on("change", file.write)
129 },
130 reset: function() {
131 var file = this
132
133 file._depends.forEach(function() {
134 child.off("change", file.write)
135 })
136 file._depends.length = 0
137 file.content = []
138 },
139 build: function() {
140 var file = this
141 , opts = file.opts
142 , resume = file.wait()
143 , adapter = adapters[file.ext] || {}
144 , buildResume = Fn.wait(min)
145
146 if (opts.input) {
147 file.content = opts.input.map(function(fileName, i, arr) {
148 var child = fileName
149 if (!(fileName instanceof File)) {
150 if (!fs.existsSync(path.resolve(fileName))) {
151 fileName = arr[i] = require.resolve(fileName)
152 }
153 child = File(fileName, {
154 root: opts.root,
155 warnings: opts.warnings
156 })
157 }
158 child.then(buildResume.wait())
159 file.depends(child)
160 return child
161 })
162 file.write()
163 } else {
164 if (!opts.mem) {
165 var source = cli.readFile(file.name)
166 file.content = adapter.split ? adapter.split(source, opts) : [ source ]
167 }
168 file.content.forEach(function(junk, i, arr) {
169 if (junk instanceof File) {
170 file.depends(junk)
171 junk.then(buildResume.wait())
172 }
173 })
174 }
175
176 setImmediate(buildResume)
177
178 function min() {
179 file.src = file.content
180 .filter(Boolean)
181 .map(function(f) {
182 if (
183 typeof f === "string" ||
184 adapters[file.ext] === adapters[f.ext] ||
185 !adapters[file.ext].transpilers ||
186 !adapters[file.ext].transpilers[f.ext]
187 ) return f
188 return adapters[file.ext].transpilers[f.ext](f.toString())
189 })
190 .join("sep" in adapter ? adapter.sep : "\n")
191
192 if (opts.replace) {
193 opts.replace.forEach(function(arr) {
194 file.src = file.src.replace(arr[0], arr[1] || "")
195 })
196 }
197
198 if (adapter.min && opts.min) {
199 var map = file.content.reduce(function(map, f) {
200 var str = f instanceof File ? f.src : f
201 if (opts.replace) opts.replace.forEach(function(arr) {
202 str = str.replace(arr[0], arr[1])
203 })
204 map[f.name] = (
205 typeof f !== "string" &&
206 adapters[file.ext] !== adapters[f.ext] &&
207 adapters[file.ext].transpilers &&
208 adapters[file.ext].transpilers[f.ext] ?
209 adapters[file.ext].transpilers[f.ext](f.toString()) :
210 str
211 )
212 return map
213 }, {})
214 adapter.min(map, opts, function(err, res) {
215 file.min = res
216 resume()
217 })
218 } else {
219 resume()
220 }
221 }
222 },
223 write: function(by) {
224 var file = this
225 if (file.name === "-") {
226 process.stdout.write(file.toString())
227 } else if (!file.opts.mem) {
228 cli.writeFile(file.name, file.toString())
229 }
230 if (file.opts.warnings.length) {
231 console.error("WARNINGS:\n - " + file.opts.warnings.join("\n - "))
232 }
233 },
234 then: function(next, scope) {
235 if (typeof next == "function") {
236 next.call(scope || this)
237 }
238 return this
239 },
240 toString: function() {
241 var file = this
242 , opts = file.opts
243 , adapter = adapters[file.ext] || {}
244 , banner = opts.banner && adapter.banner && adapter.banner.replace(/\{0\}/g, opts.banner)
245 , str = adapter.min && opts.min ? file.min : format(file.src)
246 , out = (
247 (banner ? format(banner) : "") +
248 str.trim() +
249 (opts.sourceMap ? "\n//# sourceMappingURL=" + opts.sourceMap + "\n" : "")
250 )
251
252 if (opts.outPrefix) {
253 out = opts.outPrefix + out.split("\n").join("\n" + opts.outPrefix)
254 }
255
256 return out
257 }
258}
259
260events.asEmitter(File.prototype)
261
262function defMap(str) {
263 var chr = str.charAt(0)
264 , slice = str.slice(1)
265 return chr == "+" ? lastStr + slice :
266 chr == "%" ? ((chr = lastStr.lastIndexOf(slice.charAt(0))), (chr > 0 ? lastStr.slice(0, chr) : lastStr)) + slice :
267 (chr == "." && this.root ? this.root : "") + (lastStr = str)
268}
269
270function htmlQuote(val) {
271 // a valid unquoted attribute value in HTML is
272 // a not empty string that doesn’t contain spaces, tabs, line feeds, form feeds, carriage returns, "'`=<>
273 return (
274 /^[^\s'"`<>=]+$/.test(val) ? '"' + val + '"' :
275 val
276 )
277}
278
279function htmlSplit(str, opts) {
280 var newOpts, pos, file, ext, file2, match, match2, match3, out, min, replace, tmp, haveInlineJS
281 , mined = []
282 , lastIndex = 0
283 , re = /<link[^>]+href="([^"]*?)"[^>]*?>|<(script)[^>]+src="([^>]*?)"[^>]*><\/\2>/ig
284 , banner, bannerRe = /\sbanner=(("|')([^]+?)\2|[^\s]+)/i
285 , inline, inlineRe = /\sinline\b/i
286 , drop, dropRe = /\sdrop=(("|')([^]*?)\2|[^\s]+)/i
287 , minRe = /\smin\b(?:=["']?(.+?)["'])?/i
288 , requireRe = /\srequire=(("|')([^]*?)\2|[^\s]+)/i
289 , excludeRe = /\sexclude\b/i
290 , loadFiles = []
291 , hashes = {}
292
293 str = str
294 .replace(/<!--(?!\[if)[^]*?-->/g, "")
295
296 for (out = [ str ]; match = re.exec(str); ) {
297 file = opts.root + (match[1] || match[3])
298 ext = file.split(".").pop()
299 pos = out.length
300 out.splice(-1, 1,
301 str.slice(lastIndex, match.index), "",
302 str.slice(lastIndex = re.lastIndex)
303 )
304
305 banner = bannerRe.exec(match[0])
306 inline = inlineRe.test(match[0])
307 drop = dropRe.exec(match[0])
308
309 if (match2 = requireRe.exec(match[0])) {
310 lastStr = opts.root
311 tmp = (match2[2] ? match2[3] : match2[1]).match(/[^,\s]+/g)
312 match2 = File(file, {
313 input: tmp ? tmp.map(defMap, opts) : [],
314 drop: drop ? drop[3] || drop[1] : ""
315 })
316 if (!tmp) {
317 match2._requireNext = true
318 }
319 }
320
321 if (excludeRe.test(match[0])) {
322 continue
323 }
324
325 newOpts = {
326 min: 1,
327 replace: inline && [
328 ["/*!{loadFiles}*/", loadFiles],
329 ["/*!{loadHashes}*/", JSON.stringify(hashes).slice(1, -1)]
330 ],
331 banner: banner ? banner[3] || banner[1] : "",
332 drop: drop ? drop[3] || drop[1] : ""
333 }
334
335 if (match3 = minRe.exec(match[0])) {
336 lastStr = file.slice(opts.root.length)
337 file2 = (
338 match3[1] ? path.resolve(opts.root, defMap.call(opts, match3[1])) :
339 min && (
340 adapters[min.ext] === adapters[ext] ||
341 (adapters[min.ext].transpilers||[])[ext]
342 ) ? min.name :
343 opts.root + mined.length.toString(32) + "." + ext
344 )
345 if (!min || min.name !== file2) {
346 newOpts.input = []
347 min = File(file2, newOpts)
348 mined.push(min.wait())
349 }
350 min.opts.input.push(match2 || file.replace(/\?.*/, ""))
351 if (match2 && match2._requireNext) {
352 min = match2
353 }
354 if (min.isLoaded) {
355 continue
356 }
357 min.isLoaded = 1
358 file = file2
359 }
360 var dataIf = /\sif="([^"?]+)/.exec(match[0])
361 if (inline) {
362 if (match2 && !match3) {
363 newOpts.input = [match2]
364 newOpts.mem = true
365 file = "mem:" + file
366 }
367 tmp = File(file, newOpts)
368 if (match[2]) haveInlineJS = true
369 mined.push(tmp.wait())
370 out[pos] = tmp
371 } else if ((haveInlineJS && match[2]) || dataIf) {
372 loadFiles.push(
373 (dataIf ? "(" + dataIf[1] + ")&&'" : "'") +
374 replacePath(path.relative(opts.root, file), opts) + "'"
375 )
376 } else {
377 tmp = match[0]
378 if (match3) {
379 tmp = tmp
380 .replace(minRe, "")
381 .replace(requireRe, "")
382 .replace(bannerRe, "")
383 .replace(match[1] || match[3], path.relative(opts.root, file))
384 }
385 out[pos] = tmp
386 }
387 }
388 mined.forEach(function(fn) { fn() })
389 return out.filter(Boolean).map(htmlMin, opts)
390}
391
392function htmlMin(str) {
393 var opts = this
394 return typeof str !== "string" ? str : str
395 .replace(/[\r\n][\r\n\s]*[\r\n]/g, "\n")
396 .replace(/\t/g, " ")
397 .replace(/\s+(?=<|\/?>|$)/g, "")
398 .replace(/\b(href|src)="(?!data:)(.+?)"/gi, function(_, tag, file) {
399 return tag + '="' + replacePath(file, opts) + '"'
400 })
401}
402
403function jsToHtml(str) {
404 return '<script>' + str + '</script>'
405
406}
407function cssToHtml(str) {
408 return '<style>' + str + '</style>'
409}
410
411function cssSplit(str, opts) {
412 var match, out
413 , lastIndex = 0
414 , re = /@import\s+url\((['"]?)(?!data:)(.+?)\1\);*/ig
415
416 if (opts.root !== opts.name.replace(/[^\/]*$/, "")) {
417 str = str.replace(/\/\*(?!!)[^]*?\*\/|url\((['"]?)(?!data:)(.+?)\1\)/ig, function(_, q, name) {
418 return name ?
419 'url("' + replacePath(path.relative(opts.root, path.resolve(opts.name.replace(/[^\/]*$/, name))), opts) + '")' :
420 _
421 })
422 }
423
424 for (out = [ str ]; match = re.exec(str); ) {
425 out.splice(-1, 1,
426 str.slice(lastIndex, match.index),
427 File(path.resolve(opts.root, match[2]), opts),
428 str.slice(lastIndex = re.lastIndex)
429 )
430 }
431 return out.filter(Boolean)
432}
433
434function cssMin(map, opts, next) {
435 var name
436 , out = ""
437 for (name in map) if (hasOwn.call(map, name)) {
438 out += typeof map[name] !== "string" ? map[name] : map[name]
439 .replace(/\/\*(?!!)[^]*?\*\//g, "")
440 .replace(/[\r\n]+/g, "\n")
441
442 .replace(/(.*)\/\*!\s*([\w-]+)\s*([\w-.]*)\s*\*\//g, function(_, line, cmd, param) {
443 switch (cmd) {
444 case "data-uri":
445 line = line.replace(/url\((['"]?)(.+?)\1\)/g, function(_, quote, fileName) {
446 var str = fs.readFileSync(path.resolve(opts.root + fileName), "base64")
447 return 'url("data:image/' + fileName.split(".").pop() + ";base64," + str + '")'
448 })
449 break;
450 }
451 return line
452 })
453
454 // Remove optional spaces and put each rule to separated line
455 .replace(/(["'])((?:\\?.)*?)\1|[^"']+/g, function(_, q, str) {
456 if (q) return q == "'" && str.indexOf('"') == -1 ? '"' + str + '"' : _
457 return _.replace(/[\t\n]/g, " ")
458 .replace(/ *([,;{}>~+]) */g, "$1")
459 .replace(/^ +|;(?=})/g, "")
460 .replace(/: +/g, ":")
461 .replace(/ and\(/g, " and (")
462 .replace(/}(?!})/g, "}\n")
463 })
464
465 // Use CSS shorthands
466 //.replace(/([^0-9])-?0(px|em|%|in|cm|mm|pc|pt|ex)/g, "$10")
467 //.replace(/:0 0( 0 0)?(;|})/g, ":0$2")
468 .replace(/url\("([\w\/_.-]*)"\)/g, "url($1)")
469 .replace(/([ :,])0\.([0-9]+)/g, "$1.$2")
470 }
471 next(null, out)
472}
473
474var npmChild
475
476function jsMin(map, opts, next) {
477 if (!cli.command("uglifyjs")) {
478 console.error("Error: uglify-js not found, run: npm i -g uglify-js\n")
479 process.exit(1)
480 }
481 var name
482 , result = ""
483 , child = spawn("uglifyjs", [
484 "--warn",
485 "--compress", "evaluate=false,properties=false",
486 "--mangle",
487 "--beautify", "beautify=false,semicolons=false,keep_quoted_props=true"
488 ])
489
490 child.stderr.on("data", function onError(data) {
491 data = data.toString().trim()
492 if (data !== "") opts.warnings.push(data)
493 })
494 child.stdout.on("data", function(data) {
495 result += data.toString()
496 })
497 child.on("close", function(code) {
498 if (code !== 0) {
499 console.error(opts.warnings)
500 throw Error("uglifyjs exited with " + code)
501 }
502 next(null, result)
503 })
504 for (name in map) if (hasOwn.call(map, name)) {
505 child.stdin.write(map[name])
506 }
507 child.stdin.end()
508}
509
510function tplMin(map, opts, next) {
511 var out = Object.keys(map)
512 , pos = 0
513
514 min()
515
516 function min() {
517 var i = pos++
518 if (i < out.length) {
519 _tplSplit(map[out[i]], opts, function(err, str) {
520 out[i] = str
521 min()
522 })
523 } else {
524 next(null, out.join("\n"))
525 }
526 }
527}
528
529function _tplSplit(str, opts, next) {
530 var templateRe = /^([ \t]*)(@?)((?:("|')(?:\\?.)*?\4|[-\w:.#[\]=])*)[ \t]*(([\])}]?).*?([[({]?))$/gm
531 , out = [""]
532 , parent = 0
533 , stack = [-1]
534 , resume = Fn.wait(function() {
535 next(null, out.join("\n"))
536 })
537
538 str.replace(templateRe, work)
539
540 resume()
541
542 function work(all, indent, plugin, name, q, text, mapEnd, mapStart, offset) {
543 if (offset && all === indent) return
544
545 for (q = indent.length; q <= stack[0]; ) {
546 if (typeof out[parent] !== "string") {
547 parent = out.push("") - 1
548 }
549 stack.shift()
550 }
551
552 if (typeof out[parent] !== "string") {
553 if (!out[parent].content.length) out[parent].content.push(all)
554 else out[parent].content[0] += all + "\n"
555 } else if (plugin && (name === "js" || name === "css")) {
556 out[parent] += all
557 parent = out.push(
558 File("", {mem:1, min:1, ext:name, outPrefix: indent + " "}).then(resume.wait())
559 ) - 1
560 stack.unshift(q)
561 } else {
562 if (text && text.charAt(0) === "/") return
563 out[parent] += all + "\n"
564 }
565 }
566}
567
568function tplToJs(input) {
569 var i = input.length
570 , singles = 0
571 , doubles = 0
572 for (; i--; ) {
573 if (input.charCodeAt(i) === 34) doubles++
574 else if (input.charCodeAt(i) === 39) singles++
575 }
576 input = input.replace(/\n+/g, "\\n")
577 return(
578 singles > doubles ?
579 'El.tpl("' + input.replace(/"/g, '\\"') + '")' :
580 "El.tpl('" + input.replace(/'/g, "\\'") + "')"
581 )
582}
583
584function readFileHashes(next) {
585 var leftover = ""
586 , cwd = process.cwd() + "/"
587 , git = spawn("git", ["ls-files", "-sz", "--abbrev=1"])
588
589 git.stdout.on("data", onData).on("end", onEnd)
590 git.stderr.pipe(process.stderr)
591
592 function onData(data) {
593 var lines = (leftover + data).split("\0")
594 // keep the last partial line buffered
595 leftover = lines.pop()
596 lines.forEach(onLine)
597 }
598
599 function onEnd() {
600 onLine(leftover)
601 next()
602 }
603
604 function onLine(line) {
605 if (line !== "") {
606 fileHashes[cwd + line.slice(1 + line.indexOf("\t"))] = line.split(" ")[1]
607 }
608 }
609
610 // $ git ls-tree -r --abbrev=1 HEAD
611 // 100644 blob 1f537 public/robots.txt
612 // 100644 blob 0230 public/templates/devices.haml
613 // $ git cat-file -p 1f537
614}
615
616function execute(args, i) {
617 var arg, banner, input, output
618
619 for (; arg = args[i++]; ) {
620 switch (arg) {
621 case "-b":
622 case "--banner":
623 banner = args[i++]
624 break;
625 case "-i":
626 case "--input":
627 if (!input) input = []
628 input.push(args[i++])
629 break;
630 case "-o":
631 case "--output":
632 output = args[i++]
633 break;
634 case "-w":
635 case "--worker":
636 var opts = { warnings: [] }
637 updateWorker(args[i++], opts, {})
638 break;
639 case "-r":
640 case "--readme":
641 updateReadme(args[i++])
642 break;
643 case "-v":
644 case "--version":
645 var opts = { warnings: [] }
646 updateVersion(args[i++])
647 break;
648 default:
649 if (arg.charAt(0) == "-") {
650 args.splice.apply(
651 args,
652 [i, 0].concat(arg.replace(/\w(?!$)/g,"$& " + args[i] + " -").split(" "))
653 )
654 }
655 }
656 if (input && output) {
657 File(output, {
658 banner: banner,
659 input: input,
660 min: 1
661 })
662 banner = input = output = ""
663 }
664 }
665}
666
667if (module.parent) {
668 // Used as module
669 exports.File = File
670 exports.updateReadme = updateReadme
671 exports.execute = function(args, i) {
672 readFileHashes(function() {
673 exports.execute = execute
674 if (args.length > i) execute(args, i)
675 else if (conf.litejs && Array.isArray(conf.litejs.build)) {
676 conf.litejs.build.forEach(function(row) {
677 execute(row.split(/\s+/), 0)
678 })
679 }
680 })
681 }
682}
683
684function replacePath(_p, opts) {
685 var p = path.normalize(_p)
686 if (p.indexOf("{hash}") > -1) {
687 var full = path.resolve(opts.root, p.split("?")[0])
688 p = p.replace(/{hash}/g, fileHashes[full] || +now)
689 if (!fileHashes[full]) {
690 opts.warnings.pushUniq("'" + full + "' not commited?")
691 }
692 }
693 return p
694}
695
696function format(str) {
697 return str.replace(/([\s\*\/]*@(version|date|author|stability)\s+).*/g, function(all, match, tag) {
698 tag = translate[tag] ? translate[tag][conf[tag]] || translate[tag] : conf[tag]
699 return tag ? match + tag : all
700 })
701}
702
703function updateReadme(file) {
704 var current = cli.readFile(file)
705 , updated = format(current)
706
707 if (current != updated) {
708 console.error("# Update readme: " + file)
709 cli.writeFile(file, updated)
710 }
711}
712
713function updateVersion(file) {
714 var re = /(\s+VERSION\s*=\s*)("|').*?\2/
715 , current = cli.readFile(file)
716 , updated = current.replace(re, function(_, a, q) {
717 return a + q + now.toISOString() + q
718 })
719 if (current !== updated) {
720 console.error("# Update version: " + file)
721 cli.writeFile(file, updated)
722 }
723}
724
725function updateWorker(file, opts, hashes) {
726 var root = file.replace(/[^\/]+$/, "")
727 , re = /(\s+VERSION\s*=\s*)("|').*?\2/
728 , current = cli.readFile(file)
729 , updated = current
730 .replace(re, function(_, a, q) {
731 return a + q + now.toISOString() + q
732 })
733 .replace(/ FILES = (\[[^\]]+?\])/, function(all, files) {
734 files = JSON.parse(files)
735 .map(function(line) {
736 var name = line.replace(/\?.*/, "")
737 , full = path.resolve(root, name)
738 if (!fileHashes[full]) {
739 opts.warnings.pushUniq("'" + full + "' not commited?")
740 } else if (name !== line) {
741 hashes[name] = fileHashes[full]
742 return name + "?" + fileHashes[full]
743 }
744 return line
745 })
746 return " FILES = " + JSON.stringify(files, null, "\t")
747 })
748
749 if (current != updated) {
750 console.error("# Update worker: " + file)
751 cli.writeFile(file, updated)
752 }
753}
754
755