UNPKG

16 kBJavaScriptView Raw
1/*!
2* Srcerer, build tool
3* Copyright(c) 2010-2017 Jesse Tijnagel (Guilala)
4* MIT Licensed
5*/
6
7/*jslint node: true */
8"use strict";
9
10const fs = require("fs");
11const path = require("path");
12const assert = require("assert");
13const ugly = require("uglify-js");
14const less = require("less");
15const svgo = require("svgo");
16const util = require("./util");
17
18const getBuildName = function(next) {
19 var basePath = this.urlBase.split(path.sep);
20 if(basePath.length) {
21 this.build = basePath.pop();
22
23 // create runtime object for app
24 if(!this.runtime.apps.hasOwnProperty(this.build)) this.runtime.apps[this.build] = {};
25
26 next();
27 } else {
28 console.error("getBuildName: build name unknown");
29 this.res.status(500).send("Build name unknown");
30 }
31};
32
33const loadApplicationConfiguration = function(next) {
34 this.appRoot = path.join(
35 this.runtime.conf.root,
36 this.runtime.conf.appsRoot,
37 this.build + "/"
38 );
39
40 var appConfFile = this.appRoot + "app.json";
41
42 // check if modified
43 util.modified(appConfFile, this.build).then((modified) => {
44 if(modified) {
45 util.localJson(appConfFile).then((confData) => {
46 this.appConf = confData;
47 this.appConf.modified = modified;
48 this.runtime.apps[this.build].appConf = this.appConf;
49 next();
50 }, () => {
51 console.error("loadApplicationConfiguration", appConfFile);
52 this.res.status(500).send("Error: " + this.build);
53 });
54 }
55
56 else {
57 this.appConf = this.runtime.apps[this.build].appConf;
58 this.appConf.modified = false;
59 next();
60 }
61 }, (err) => {
62 console.error("loadApplicationConfiguration", err);
63 this.res.status(500).send("Error: " + this.build);
64 });
65};
66
67const setStaticRoot = function(next){
68 if(this.appConf.staticRoot && this.appConf.staticRoot.constructor === String) {
69
70 // set static root
71 this.staticRoot = path.join (
72 this.appRoot,
73 this.appConf.staticRoot
74 );
75
76 next();
77 } else {
78 console.error("serveStatic, String expected: appConf.staticRoot");
79 this.res.status(500).send("Expected staticRoot");
80 }
81};
82
83// ensure root for static files
84const makeStaticRoot = function(next){
85 var exists;
86 new util.que(this)
87
88 .add((next) => {
89 fs.exists(this.staticRoot, function(existing){
90 exists = existing;
91 next();
92 });
93 })
94
95 .then(() => {
96 if (exists){
97 next();
98 } else {
99 console.log("'%s', new path '%s'", this.appConf.name, this.staticRoot);
100 fs.mkdir(this.staticRoot, "0775", () => {
101 next();
102 });
103 }
104 });
105};
106
107const serveStaticRoot = function(next) {
108 // start serving static
109 if(!this.runtime.apps[this.build].hasOwnProperty("serveStatic")) {
110 this.runtime.router.use(this.urlBase, this.runtime.static(this.staticRoot));
111 this.runtime.apps[this.build].serveStatic = true;
112 }
113
114 // exit build if request is not index.html
115 if(this.urlPath.length > 1 && this.urlPath != "/index.html") {
116 this.exit();
117 }
118
119 // build index.html
120 else {
121 next();
122 }
123};
124
125const controlCache = function(next) {
126 if (this.runtime.lastDebugMode !== this.debug) {
127 util.clearModificationCache();
128 this.runtime.lastDebugMode = this.debug;
129 this.appConf.modified = true;
130 }
131 next();
132};
133
134const confLess = function(next) {
135 // paths for @import
136 this.lessConfig = {
137 paths: (() => {
138 var lessImportPaths = [];
139 this.appConf.stylePaths.forEach((stylePath) => {
140 lessImportPaths.push(path.join(this.appRoot + stylePath));
141 });
142 return lessImportPaths;
143 })(),
144 filename: "Cannot parse less", // for error messages
145 compress: !this.debug
146 };
147 next();
148};
149
150const addBlob = function(blobName, blobPath, done) {
151 var pathPrefix = path.join(
152 blobPath,
153 blobName,
154 "/",
155 blobName
156 );
157
158 var blob = {
159 name: blobName,
160 path: blobPath,
161 blobRoot: this.blobRoot,
162 lessConfig: this.lessConfig,
163 debug: this.debug,
164
165 mvcFile: pathPrefix + ".js",
166 mvcModified: undefined,
167
168 cssFile: pathPrefix + ".less",
169 cssModified: undefined,
170
171 svgFile: pathPrefix + ".svg",
172 svgModified: undefined
173 };
174
175 new util.all()
176
177 // is mvc modified
178 .add((done) => {
179 util.modified(blob.mvcFile, blobName + "Mvc").then((state) => {
180 blob.mvcModified = state;
181 blob.mvc = true;
182 done();
183 }, (err) => {
184 blob.mvcModified = false;
185 done();
186 });
187 })
188
189 // is css modified
190 .add((done) => {
191 util.modified(blob.cssFile, blobName + "Css").then((state) => {
192 blob.cssModified = state;
193 blob.css = true;
194 done();
195 }, (err) => {
196 blob.cssModified = false;
197 done();
198 });
199 })
200
201 // is svg modified
202 .add((done) => {
203 util.modified(blob.svgFile, blobName + "Svg").then((state) => {
204 blob.svgModified = state;
205 blob.svg = true;
206 done();
207 }, (err) => {
208 blob.svgModified = false;
209 done();
210 });
211 })
212
213 // is blob modified
214 .then(() => {
215 if(blob.mvcModified || blob.cssModified || blob.svgModified) {
216 this.blobs.push(blob);
217 }
218 done();
219 });
220};
221
222// find and configure all blobs
223const blobsConfigure = function(next) {
224 // define static root for blobs
225 this.blobRoot = path.join(
226 this.staticRoot,
227 "bin/"
228 );
229
230 // async configure all blobs
231 var configureBlobs = new util.all(this);
232
233 if(util.obj.isArray(this.appConf.blobs)) {
234 this.appConf.blobs.forEach((blobNode) => {
235 var blobList;
236
237 // parse path to blob node
238 const blobPath = util.parsePaths([
239 this.appRoot,
240 blobNode.path
241 ]);
242
243 if(blobNode.select) {
244 // select is a list of blob src child directories
245 blobList = util.obj.arrayify(blobNode.select);
246 } else {
247 // else any child directory is a blob src
248 blobList = util.dir.getDirectories(blobPath);
249 }
250
251 blobList.forEach((blobName) => {
252 var myPath = blobPath;
253 configureBlobs.add((done) => {
254 addBlob.call(this, blobName, myPath, done);
255 });
256 });
257 });
258 }
259
260 configureBlobs.then(next);
261};
262
263// ensure root directory for blob bin files
264const blobsStaticRoot = function(next){
265 var exists;
266
267 new util.que(this)
268
269 .add((next) => {
270 fs.exists(this.blobRoot, function(existing){
271 exists = existing;
272 next();
273 });
274 })
275
276 .then(() => {
277 if (exists){
278 next();
279 } else {
280 console.log("'%s', new path '%s'", this.appConf.name, this.blobRoot);
281 fs.mkdir(this.blobRoot, "0775", () => {
282 next();
283 });
284 }
285 });
286};
287
288const blobCss = function(done) {
289 fs.readFile(this.cssFile, "utf8", (err, data) => {
290 if (err) {
291 console.error("Blob, style sheet not found", this.cssFile);
292 this.css = false;
293 done();
294 } else {
295 less.render(data, this.lessConfig, (lessErr, cssResult) => {
296 if (lessErr) {
297 console.error(lessErr);
298 this.css = false;
299 } else {
300 this.css = cssResult.css;
301 }
302 done();
303 });
304 }
305 });
306};
307
308// blob compile svg
309const blobSvg = function(done) {
310 fs.readFile(this.svgFile, "utf8", (err, data) => {
311 if (err) {
312 console.error("Blob, svg not found", this.svgFile);
313 done();
314 } else {
315 if(this.debug) {
316 this.svg = data;
317 done();
318 } else {
319 new svgo({
320 full: true,
321 plugins: [
322 {
323 removeViewBox: false,
324 cleanupIDs: false,
325 moveElemsAttrsToGroup: false,
326 moveGroupAttrsToElems: false,
327 collapseGroups: false,
328 convertPathData: false,
329 convertTransform: false,
330 mergePaths: false
331 }
332 ]
333 }).optimize(data, (result) => {
334 this.svg = result.data;
335 done();
336 });
337 }
338 }
339 });
340};
341
342// blob compile mvc
343const blobMvc = function(done) {
344 var blob = this;
345
346 var parseBlob = (data) => {
347 // shim bubbleSet
348 const blobData = [
349 this.debug ? "//# sourceURL=" + this.name + "\n" : "",
350 "bubbleSet." + this.name + "(function(){\n",
351 this.css ? "this.css = true;\n" : "",
352 this.svg ? "this.svg = true;\n" : "",
353 data,
354 "});"
355 ];
356
357 data = blobData.join("");
358
359 if(!this.debug) {
360 data = ugly.minify(data, {
361 warnings: true,
362 fromString: true,
363 mangle: true
364 }).code;
365 }
366
367 this.mvc = data;
368 done();
369 };
370
371 if(blob.mvc) {
372 fs.readFile(this.mvcFile, "utf8", (err, data) => {
373 if(err) {
374 console.error("Blob, mvc not found", this.mvcFile);
375 done();
376 } else {
377 parseBlob(data);
378 }
379 });
380 } else {
381 parseBlob("");
382 }
383};
384
385// blob write to disk
386const blobExport = function(done) {
387 const writeFiles = new util.all(this);
388
389 if(this.css) {
390 writeFiles.add(function(ready){
391 fs.writeFile(this.blobRoot + this.name + ".css", this.css, "utf8", (err) => {
392 assert.equal(err);
393 ready();
394 });
395 });
396 }
397
398 if(this.svg) {
399 writeFiles.add(function(ready){
400 fs.writeFile(this.blobRoot + this.name + ".svg", this.svg, "utf8", (err) => {
401 assert.equal(err);
402 ready();
403 });
404 });
405 }
406
407 writeFiles.add(function(ready){
408 fs.writeFile(this.blobRoot + this.name + ".js", this.mvc, "utf8", (err) => {
409 assert.equal(err);
410 ready();
411 });
412 });
413
414 writeFiles.then(done);
415};
416
417// compile changed blobs simultaneously
418const blobsCompile = function(next) {
419 var logTime = (t) => { return t ? new Date(t).toString() : "unchanged";};
420 var compiling = new util.all();
421
422 this.blobs.forEach((blob) => {
423 compiling.add((done) => {
424 var parts = new util.all(blob);
425
426 console.log("'%s' blob \x1b[36m%s\x1b[0m", this.appConf.name, blob.name);
427
428 if(blob.css) {
429 console.log("\x1b[34m\tCSS %s\x1b[0m", logTime(blob.cssModified));
430 parts.add(blobCss);
431 }
432
433 if(blob.svg) {
434 console.log("\x1b[32m\tSVG %s\x1b[0m", logTime(blob.svgModified));
435 parts.add(blobSvg);
436 }
437
438 console.log("\x1b[33m\tMVC %s\x1b[0m", logTime(blob.mvcModified));
439 parts.add(blobMvc);
440
441 parts.then(() => {
442 blobExport.call(blob, done);
443 });
444 });
445 });
446 compiling.then(next);
447};
448
449// parse client side dependencie script
450const parseBubbles = function(ready) {
451 if(this.appConf.modified) {
452 var pathBubbles = __dirname + "/bubbles.js";
453 fs.readFile(pathBubbles, "utf8", (err, data) => {
454 if(err) {
455 console.error(`bubbles.js, fs.readFile: ${
456 err
457 }`);
458 ready();
459 } else {
460 // minify if not in debug mode
461 if(!this.debug) {
462 var uglyfied = ugly.minify(data, {
463 warnings: true,
464 fromString: true,
465 mangle: true
466 });
467 assert.equal(uglyfied.error);
468 data = uglyfied.code;
469 }
470
471 var filePath = path.join(
472 this.blobRoot,
473 "bubbles.js"
474 );
475
476 fs.writeFile(filePath, data, "utf8", (err) => {
477 assert.equal(err);
478 ready();
479 });
480 }
481 });
482 } else {
483 ready();
484 }
485};
486
487// parse index.html and write to disk
488const indexWrite = function (next) {
489 if(this.appConf.modified) {
490 var dom = new util.dombo();
491
492 // name comment
493 dom.comment(`${
494 this.appConf.name || "app"
495 } v${
496 this.appConf.version || 0
497 }: ${
498 this.runtime.conf.name || "server"
499 } v${
500 this.runtime.conf.version || 0
501 } ${
502 new Date()
503 }`);
504
505 // meta tags
506 if(this.appConf.meta && this.appConf.meta.length) {
507 this.appConf.meta.forEach(function (meta) {
508 dom.head("meta", false, meta);
509 });
510 }
511
512 // custom header tags
513 if(this.appConf.tag && this.appConf.tag.length) {
514 this.appConf.tag.forEach(function (tag) {
515 dom.head(tag.name, false, tag.attr);
516 });
517 }
518
519 // don't cache in debug mode
520 if(this.debug) {
521 dom.head("meta", false, {
522 "http-equiv": "cache-control",
523 "content": "no-cache"
524 });
525 }
526
527 // title
528 dom.head("title", this.appConf.name || "app");
529
530 var scriptTags = [{
531 src: "bin/bubbles.js",
532 type: "text/javascript",
533 charset: "utf-8",
534 async: undefined
535 }];
536
537 if(util.obj.isArray(this.appConf.externalScripts)) {
538 scriptTags = scriptTags.concat(this.appConf.externalScripts);
539 }
540
541 // script source tags
542 scriptTags.forEach((src) => {
543 dom.head("script", "", src);
544 });
545
546 // write index to disk
547 if (this.appConf.index) {
548 fs.writeFile(this.staticRoot + this.appConf.index, dom.parse(), "utf8", (err) => {
549 assert.equal(err);
550 next();
551 });
552 }
553 } else {
554 next();
555 }
556};
557
558// generate nginx conf
559const confNginx = function(){
560 var nginx = this.appConf.nginx;
561
562 if(nginx && this.appConf.modified) {
563 var tpl = "";
564
565 new util.que(this)
566
567 .add((done) => {
568 fs.readFile(util.parsePaths(nginx.template), "utf8", (err, data) => {
569 if (err) console.error("confNginx", err);
570 else {
571 tpl = data;
572 done();
573 }
574 });
575 })
576
577 .then(() => {
578 tpl = tpl
579 .replace("<<title>>", this.appConf.name + " " + this.appConf.version)
580 .replace("<<domains>>", nginx.domains)
581 .replace("<<srcerer>>", nginx.srcerer);
582
583 // replace any <<key>> from nginx.paths in template
584 for(var key in nginx.paths) {
585 tpl = tpl.replace("<<" + key + ">>", util.parsePaths(
586 nginx.paths[key]
587 ));
588 }
589
590 fs.writeFile(util.parsePaths(nginx.file), tpl, "utf8", (err) => {
591 if(err) assert.equal(err);
592 else console.log("'%s' new\x1b[36m nginx", this.appConf.name);
593 });
594 });
595 }
596};
597
598// generate nginx conf
599const exitBuild = function(next){
600 this.exit();
601 next();
602};
603
604// export build
605module.exports = function () {
606 // this = {
607 // configuration
608 // configurationFilePath
609 // express: {
610 // req
611 // res
612 // next
613 // },
614 // mountType
615 // mountPoint
616 // root
617 // util
618 // }
619
620 new util.que({
621 server: this.express.req.server,
622 runtime: this.express.req.runtime,
623 res: this.express.res,
624 urlBase: this.express.req.baseUrl,
625 urlPath: this.express.req.path,
626 build: undefined,
627 appConf: undefined,
628 blobs: [],
629 debug: this.express.req.query.debug === "true",
630 exit: this.express.next
631 })
632
633 .add(getBuildName)
634 .add(loadApplicationConfiguration)
635 .add(setStaticRoot)
636 .add(makeStaticRoot)
637 .add(serveStaticRoot)
638 .add(controlCache)
639 .add(confLess)
640 .add(blobsConfigure)
641 .add(blobsStaticRoot)
642 .add(blobsCompile)
643 .add(parseBubbles)
644 .add(indexWrite)
645 .add(exitBuild)
646 .then(confNginx);
647};
648