UNPKG

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