UNPKG

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