UNPKG

15.6 kBJavaScriptView Raw
1var fs = require('fs');
2var crypto = require('crypto');
3var async = require('async');
4var EventEmitter = require('events').EventEmitter;
5var browserify = require('browserify');
6var UglifyJS = require('uglify-js');
7var cssmin = require('cssmin');
8var path = require('path');
9
10
11function Moonboots(opts) {
12 var item;
13 //'opts' has to be an object
14 if (typeof opts !== 'object') {
15 throw new Error('Invalid options');
16 }
17 //'main' is the only required parameter, throw if it's missing
18 if (!opts.main) {
19 throw new Error("You must supply at minimum a `main` file for your moonboots app: {main: 'myApp.js'}");
20 }
21
22 //Defaults
23 this.config = {
24 libraries: [],
25 stylesheets: [],
26 jsFileName: 'app',
27 cssFileName: 'styles',
28 browserify: {}, // overridable browserify options
29 uglify: {}, // overridable uglify options
30 beforeBuildJS: function (cb) { cb(); },
31 beforeBuildCSS: function (cb) { cb(); },
32 sourceMaps: false, //turns on browserify debug
33 resourcePrefix: '/',
34 minify: true,
35 cache: true,
36 developmentMode: false,
37 timingMode: false
38 };
39
40 // Were we'll store generated source code, etc.
41 this.result = {
42 js: {ext: 'js', source: ''},
43 css: {ext: 'css'},
44 html: {}
45 };
46
47 for (item in opts) {
48 this.config[item] = opts[item];
49 }
50
51 // Uglify fromString must be true
52 this.config.uglify.fromString = true;
53
54 // Use sourceMaps option to set browserify.debug if its not set already
55 if (typeof this.config.browserify.debug === 'undefined') {
56 this.config.browserify.debug = this.config.sourceMaps;
57 }
58
59 // Replace transforms with transform in the browserify config since
60 // that is how browserify expects them
61 if (this.config.browserify.transforms && !this.config.browserify.transform) {
62 this.config.browserify.transform = this.config.browserify.transforms;
63 delete this.config.browserify.transforms;
64 }
65
66 // Ensure browserify transforms is set
67 if (typeof this.config.browserify.transform === 'undefined') {
68 this.config.browserify.transform = [];
69 }
70
71 // developmentMode forces minify to false and never build no matter what
72 if (this.config.developmentMode) {
73 this.config.minify = false;
74 this.config.buildDirectory = undefined;
75 this.config.cache = false;
76 }
77
78 //We'll re-add extensions later
79 if (path.extname(this.config.jsFileName) === '.js') {
80 this.config.jsFileName = this.config.jsFileName.slice(0, -3);
81 }
82 if (path.extname(this.config.cssFileName) === '.css') {
83 this.config.cssFileName = this.config.cssFileName.slice(0, -4);
84 }
85
86 // inherit from event emitter and then wait for nextTick to do anything so that our parent has a chance to listen for events
87 EventEmitter.call(this);
88 process.nextTick(this.build.bind(this));
89}
90
91// Inherit from event emitter
92Moonboots.prototype = Object.create(EventEmitter.prototype, {
93 constructor: {
94 value: Moonboots
95 }
96});
97
98//Initial build, in development mode we just set hashes and filenames, otherwise we prime the sources
99//Emits 'ready' when done
100Moonboots.prototype.build = function () {
101 var self = this;
102
103 self.timing('timing', 'build start');
104 async.series([
105 function _buildFiles(buildFilesDone) {
106 var parts;
107 if (!self.config.buildDirectory) {
108 return buildFilesDone();
109 }
110 fs.readdir(self.config.buildDirectory, function (err, files) {
111 self.timing('reading buildDirectory start');
112 if (err) {
113 self.config.buildDirectory = undefined;
114 return buildFilesDone();
115 }
116 async.each(files, function (fileName, next) {
117 if (path.extname(fileName) === '.js' && fileName.indexOf(self.config.jsFileName) === 0) {
118 return fs.readFile(path.join(self.config.buildDirectory, fileName), 'utf8', function (err, data) {
119 if (err) {
120 self.config.buildDirectory = undefined;
121 return next(true);
122 }
123 parts = fileName.split('.');
124 self.result.js.hash = parts[1];
125 self.result.js.source = data;
126 self.result.js.filename = fileName;
127 self.result.js.fromBuild = true;
128 next();
129 });
130 }
131 if (path.extname(fileName) === '.css' && fileName.indexOf(self.config.cssFileName) === 0) {
132 return fs.readFile(path.join(self.config.buildDirectory, fileName), 'utf8', function (err, data) {
133 if (err) {
134 self.config.buildDirectory = undefined;
135 return next(true);
136 }
137 parts = fileName.split('.');
138 self.result.css.hash = parts[1];
139 self.result.css.source = data;
140 self.result.css.filename = fileName;
141 self.result.css.fromBuild = true;
142 next();
143 });
144 }
145 next();
146 }, function () {
147 self.timing('reading buildDirectory finish');
148 buildFilesDone();
149 });
150 });
151 },
152 function _buildBundles(buildBundlesDone) {
153 if (self.result.js.filename && self.result.css.filename) {
154 //buildFiles found existing files we don't have to build bundles
155 return buildBundlesDone();
156 }
157 async.parallel([
158 function _buildCSS(buildCSSDone) {
159 self.timing('build css start');
160 //If we're rebuilding on each request we just have to set the hash
161 if (!self.config.cache) {
162 self.result.css.hash = 'nonCached';
163 return buildCSSDone();
164 }
165 self.bundleCSS(true, buildCSSDone);
166 },
167 function _buildJS(buildJSDone) {
168 //If we're rebuilding on each request we just have to set the hash
169 if (!self.config.cache) {
170 self.result.js.hash = 'nonCached';
171 return buildJSDone();
172 }
173 self.bundleJS(true, buildJSDone);
174 }
175 ], buildBundlesDone);
176 },
177 function _setResults(setResultsDone) {
178 var cssFileName = self.config.cssFileName + '.' + self.result.css.hash;
179 var jsFileName = self.config.jsFileName + '.' + self.result.js.hash;
180
181 if (self.config.minify) {
182 cssFileName += '.min';
183 jsFileName += '.min';
184 }
185
186 cssFileName += '.css';
187 jsFileName += '.js';
188
189 self.result.css.fileName = cssFileName;
190 self.result.js.fileName = jsFileName;
191
192 self.result.html.source = '<!DOCTYPE html>\n';
193 if (self.config.stylesheets.length > 0) {
194 self.result.html.source += linkTag(self.config.resourcePrefix + self.cssFileName());
195 }
196 self.result.html.source += scriptTag(self.config.resourcePrefix + self.jsFileName());
197 self.result.html.context = {
198 jsFileName: self.result.js.fileName,
199 cssFileName: self.result.css.fileName
200 };
201 setResultsDone();
202 },
203 function _createBuildFiles(createBuildFilesDone) {
204 if (!self.config.buildDirectory) {
205 return createBuildFilesDone();
206 }
207
208 async.parallel([
209 function (next) {
210 if (self.result.js.fromBuild) {
211 return next();
212 }
213 fs.writeFile(path.join(self.config.buildDirectory, self.result.css.fileName), self.result.css.source, next);
214 }, function (next) {
215 if (self.result.css.fromBuild) {
216 return next();
217 }
218 fs.writeFile(path.join(self.config.buildDirectory, self.result.js.fileName), self.result.js.source, next);
219 }
220 ], createBuildFilesDone);
221 }
222 ], function () {
223 self.timing('build finish');
224 self.emit('ready');
225 });
226};
227
228// Actually generate the CSS bundle
229Moonboots.prototype.bundleCSS = function (setHash, done) {
230 var self = this;
231 async.series([
232 function _beforeBuildCSS(next) {
233 self.timing('beforeBuildCSS start');
234 if (self.config.beforeBuildCSS.length) {
235 self.config.beforeBuildCSS(function (err) {
236 self.timing('beforeBuildCSS finish');
237 next(err);
238 });
239 } else {
240 self.config.beforeBuildCSS();
241 self.timing('beforeBuildCSS finish');
242 next();
243 }
244 },
245 function _buildCSS(next) {
246 var csssha;
247 self.timing('buildCSS start');
248 self.result.css.source = concatFiles(self.config.stylesheets);
249 if (setHash) {
250 csssha = crypto.createHash('sha1'); // we'll calculate this to know whether to change the filename
251 csssha.update(self.result.css.source);
252 self.result.css.hash = csssha.digest('hex').slice(0, 8);
253 }
254 if (self.config.minify) {
255 self.result.css.source = cssmin(self.result.css.source);
256 }
257 self.timing('buildCSS finish');
258 next();
259 }
260 ], function _bundleCSSDone(err) {
261 if (err) {
262 self.emit('log', ['moonboots', 'error'], err);
263 }
264 done(null, self.result.css.source);
265 });
266};
267
268// Actually generate the JS bundle
269Moonboots.prototype.bundleJS = function (setHash, done) {
270 var self = this;
271 var jssha = crypto.createHash('sha1'); // we'll calculate this to know whether to change the filename
272 async.series([
273 function _beforeBuildJS(next) {
274 self.timing('beforeBuildJS start');
275 if (self.config.beforeBuildJS.length) {
276 self.config.beforeBuildJS(function (err) {
277 self.timing('beforeBuildJS finish');
278 next(err);
279 });
280 } else {
281 self.config.beforeBuildJS();
282 self.timing('beforeBuildJS finish');
283 next();
284 }
285 },
286 function _concatLibs(next) {
287 //Start w/ external libraries
288 self.timing('build libraries start');
289 self.result.js.source = concatFiles(self.config.libraries);
290 jssha.update(self.result.js.source);
291 self.timing('build libraries finish');
292 next();
293 },
294 function (next) {
295 self.browserify(next);
296 },
297 function (next) {
298 if (setHash) {
299 jssha.update(self.result.js.source);
300 self.result.js.hash = jssha.digest('hex').slice(0, 8);
301 }
302 if (self.config.minify) {
303 self.timing('minify start');
304 self.result.js.source = UglifyJS.minify(self.result.js.source, self.config.uglify).code;
305 self.timing('minify finish');
306 }
307 next();
308 }
309 ], function _bundleJSDone(err) {
310 if (err) {
311 self.emit('log', ['moonboots', 'error'], err);
312 if (self.config.developmentMode) {
313 self.result.js.source = errorTrace(err);
314 } else {
315 throw err;
316 }
317 }
318 done(null, self.result.js.source);
319 });
320};
321
322
323Moonboots.prototype.browserify = function (done) {
324 var modules, args, bundle;
325 var self = this;
326
327 self.timing('browserify start');
328
329 bundle = browserify(self.config.browserify);
330
331 // handle module folder that you want to be able to require without relative paths.
332 if (self.config.modulesDir) {
333 modules = fs.readdirSync(self.config.modulesDir);
334 modules.forEach(function (moduleFileName) {
335 if (path.extname(moduleFileName) === '.js') {
336 args = [
337 path.join(self.config.modulesDir, moduleFileName),
338 {expose: path.basename(moduleFileName, '.js')}
339 ];
340 bundle.require.apply(bundle, args);
341 }
342 });
343 }
344
345 // add main import
346 bundle.add(self.config.main);
347
348 bundle.bundle(function (err, js) {
349 if (self.result.js.source.trim().slice(-1) !== ';') {
350 js = ';' + js;
351 }
352 self.result.js.source = self.result.js.source + js;
353
354 self.timing('browserify finish');
355 done(err);
356 });
357};
358
359/*
360* Main moonboots functions.
361* These should be the only methods you call.
362*/
363
364//Send jsSource to callback, rebuilding every time if in development mode
365Moonboots.prototype.jsSource = function (cb) {
366 if (this.config.cache) {
367 return cb(null, this.result.js.source);
368 }
369 this.bundleJS(false, cb);
370};
371
372//Send cssSource to callback, rebuilding every time if in development mode
373Moonboots.prototype.cssSource = function (cb) {
374 if (this.config.cache) {
375 return cb(null, this.result.css.source);
376 }
377 this.bundleCSS(false, cb);
378};
379
380//Return jsFileName, which never changes
381Moonboots.prototype.jsFileName = function () {
382 return this.result.js.fileName;
383};
384
385//Return jsFileName, which never changes
386Moonboots.prototype.cssFileName = function () {
387 return this.result.css.fileName;
388};
389
390//Return htmlContext, which never changes
391Moonboots.prototype.htmlContext = function () {
392 return this.result.html.context;
393};
394
395//Return htmlSource, which never changes
396Moonboots.prototype.htmlSource = function () {
397 return this.result.html.source;
398};
399
400Moonboots.prototype.timing = function (message) {
401 if (this.config.timingMode) {
402 this.emit('log', ['moonboots', 'timing', 'debug'], message, Date.now());
403 }
404};
405
406/*
407* End main moonboots functions.
408*/
409
410// Main export
411module.exports = Moonboots;
412
413
414// a few helpers
415function concatFiles(arrayOfFiles) {
416 return arrayOfFiles.map(function (fileName) {
417 var source = fs.readFileSync(fileName, 'utf8');
418 if (path.extname(fileName) !== '.js') {
419 return source;
420 }
421 if (source.trim().slice(-1) !== ';') {
422 source = source + ';';
423 }
424 return source;
425 }).join('\n');
426}
427
428function linkTag(filename) {
429 return '<link href="' + filename + '" rel="stylesheet" type="text/css">\n';
430}
431
432function scriptTag(filename) {
433 return '<script src="' + filename + '"></script>';
434}
435
436function errorTrace(err) {
437 var trace;
438 if (err.stack) {
439 trace = err.stack;
440 } else {
441 trace = JSON.stringify(err);
442 }
443 trace = trace.split('\n').join('<br>').replace(/"/g, '&quot;');
444 return 'document.write("<pre style=\'background:#ECFOF2; color:#444; padding: 20px\'>' + trace + '</pre>");';
445}