├── .gitignore ├── .jshintignore ├── .jshintrc ├── Makefile ├── changelog.md ├── fixtures ├── README.md ├── app │ ├── app.js │ ├── appImports.js │ ├── appJadeImport.js │ └── badapp.js ├── build1 │ ├── app.deadbeef.min.js │ └── styles.deadbeef.min.css ├── build2 │ ├── app.deadbeef.min.js │ └── styles.deadbeef.min.css ├── libraries │ ├── iife-no-semicolon.js │ └── lib.js ├── modules │ ├── README.md │ ├── bar.js │ ├── baz.js │ ├── foo.js │ └── qux.jade └── stylesheets │ ├── app.css │ └── style.css ├── index.js ├── package.json ├── readme.md └── test ├── build.js ├── consistentHash.js ├── css.js ├── dev.js ├── errors.js ├── html.js ├── jade.js ├── js.js ├── sourceMaps.js └── timing.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | sample/.build 4 | sample-build/ 5 | npm-debug.loh 6 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | sample 3 | fixtures/build1/app.deadbeef.min.js 4 | fixtures/app/app.js 5 | fixtures/app/badapp.js 6 | fixtures/app/appImports.js 7 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "indent": 4, 3 | "asi": false, 4 | "expr": true, 5 | "loopfunc": true, 6 | "curly": false, 7 | "evil": true, 8 | "white": true, 9 | "undef": true, 10 | "unused": true, 11 | "node": true 12 | } 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | @node node_modules/lab/bin/lab -t 100 -m 40000 3 | test-no-cov: 4 | @node node_modules/lab/bin/lab 5 | test-cov-html: 6 | @node node_modules/lab/bin/lab -r html -o coverage.html 7 | complexity: 8 | @node node_modules/complexity-report/src/index.js -o complexity.md -f markdown index.js 9 | 10 | .PHONY: test test-no-cov test-cov-html complexity 11 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # 5.0.0 (2016-04-15) 2 | 3 | - Update to Browserify v13 [#71](https://github.com/HenrikJoreteg/moonboots/pull/71) 4 | 5 | # 4.0.0 (2015-03-03) 6 | 7 | - Update to Browserify v9 [#62](https://github.com/HenrikJoreteg/moonboots/pull/62) 8 | - Allow setting individual transform options [#65](https://github.com/HenrikJoreteg/moonboots/pull/65) 9 | - Allow transforms to be passed in using the correct `browserify.transform` option name [#52](https://github.com/HenrikJoreteg/moonboots/pull/52) -------------------------------------------------------------------------------- /fixtures/README.md: -------------------------------------------------------------------------------- 1 | # Test fixtures 2 | -------------------------------------------------------------------------------- /fixtures/app/app.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | var x = 1; 3 | }; 4 | 5 | module.exports(); 6 | -------------------------------------------------------------------------------- /fixtures/app/appImports.js: -------------------------------------------------------------------------------- 1 | var foo = require('../modules/foo'); 2 | var bar = require('../modules/bar'); 3 | var baz = require('../modules/baz'); 4 | 5 | 6 | (function () { 7 | console.log(1); 8 | })(); -------------------------------------------------------------------------------- /fixtures/app/appJadeImport.js: -------------------------------------------------------------------------------- 1 | require('../modules/qux.jade'); 2 | 3 | 4 | (function () { 5 | console.log(1); 6 | })(); -------------------------------------------------------------------------------- /fixtures/app/badapp.js: -------------------------------------------------------------------------------- 1 | var foo = require('not-a-module'); 2 | 3 | module.exports = function () { 4 | var x = 1; 5 | }; 6 | 7 | module.exports(); 8 | 9 | -------------------------------------------------------------------------------- /fixtures/build1/app.deadbeef.min.js: -------------------------------------------------------------------------------- 1 | nothing -------------------------------------------------------------------------------- /fixtures/build1/styles.deadbeef.min.css: -------------------------------------------------------------------------------- 1 | readable 2 | -------------------------------------------------------------------------------- /fixtures/build2/app.deadbeef.min.js: -------------------------------------------------------------------------------- 1 | undefined; 2 | -------------------------------------------------------------------------------- /fixtures/build2/styles.deadbeef.min.css: -------------------------------------------------------------------------------- 1 | nothing -------------------------------------------------------------------------------- /fixtures/libraries/iife-no-semicolon.js: -------------------------------------------------------------------------------- 1 | /*jshint asi:true, browser:true, unused:false*/ 2 | (function () { 3 | if (typeof window !== 'undefined') { 4 | window.BadLib = function (foo) { 5 | if (this.baz && this.baz === foo) { 6 | this.console.log('baz is', foo) 7 | } 8 | }; 9 | } 10 | })() -------------------------------------------------------------------------------- /fixtures/libraries/lib.js: -------------------------------------------------------------------------------- 1 | /*jshint asi:true, browser:true, unused:false*/ 2 | (function () { 3 | if (typeof window !== 'undefined') { 4 | window.Lib = function (bar) { 5 | if (this.biz && this.biz === bar) { 6 | this.console.log('biz is', bar); 7 | } 8 | }; 9 | } 10 | })(); -------------------------------------------------------------------------------- /fixtures/modules/README.md: -------------------------------------------------------------------------------- 1 | # This file will be ignored by moonboots modulesDir 2 | -------------------------------------------------------------------------------- /fixtures/modules/bar.js: -------------------------------------------------------------------------------- 1 | var baz = require('./baz'); 2 | var Backbone = require('backbone'); 3 | 4 | 5 | module.exports = function () { 6 | baz.apply(null, arguments); 7 | return Backbone; 8 | }; 9 | -------------------------------------------------------------------------------- /fixtures/modules/baz.js: -------------------------------------------------------------------------------- 1 | var async = require('async'); 2 | 3 | module.exports = function (arr, iterator, done) { 4 | async.map([1, 2, 3], iterator, done); 5 | }; 6 | -------------------------------------------------------------------------------- /fixtures/modules/foo.js: -------------------------------------------------------------------------------- 1 | module.exports = 'foo'; 2 | -------------------------------------------------------------------------------- /fixtures/modules/qux.jade: -------------------------------------------------------------------------------- 1 | p All that you require to crash 2 | -------------------------------------------------------------------------------- /fixtures/stylesheets/app.css: -------------------------------------------------------------------------------- 1 | div {border: 1px solid #000;} 2 | -------------------------------------------------------------------------------- /fixtures/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body {background: #ccc;} 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var crypto = require('crypto'); 3 | var async = require('async'); 4 | var EventEmitter = require('events').EventEmitter; 5 | var browserify = require('browserify'); 6 | var UglifyJS = require('uglify-js'); 7 | var cssmin = require('cssmin'); 8 | var path = require('path'); 9 | 10 | 11 | function 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 92 | Moonboots.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 100 | Moonboots.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 = '\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 229 | Moonboots.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 269 | Moonboots.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 | self.timing('build libraries finish'); 291 | next(); 292 | }, 293 | function (next) { 294 | self.browserify(next); 295 | }, 296 | function (next) { 297 | if (setHash) { 298 | jssha.update(self.result.js.source); 299 | self.result.js.hash = jssha.digest('hex').slice(0, 8); 300 | } 301 | if (self.config.minify) { 302 | self.timing('minify start'); 303 | self.result.js.source = UglifyJS.minify(self.result.js.source, self.config.uglify).code; 304 | self.timing('minify finish'); 305 | } 306 | next(); 307 | } 308 | ], function _bundleJSDone(err) { 309 | if (err) { 310 | self.emit('log', ['moonboots', 'error'], err); 311 | if (self.config.developmentMode) { 312 | self.result.js.source = errorTrace(err); 313 | } else { 314 | throw err; 315 | } 316 | } 317 | done(null, self.result.js.source); 318 | }); 319 | }; 320 | 321 | 322 | Moonboots.prototype.browserify = function (done) { 323 | var modules, args, bundle; 324 | var self = this; 325 | 326 | self.timing('browserify start'); 327 | 328 | bundle = browserify(self.config.browserify); 329 | 330 | // handle module folder that you want to be able to require without relative paths. 331 | if (self.config.modulesDir) { 332 | modules = fs.readdirSync(self.config.modulesDir); 333 | modules.forEach(function (moduleFileName) { 334 | if (path.extname(moduleFileName) === '.js') { 335 | args = [ 336 | path.join(self.config.modulesDir, moduleFileName), 337 | {expose: path.basename(moduleFileName, '.js')} 338 | ]; 339 | bundle.require.apply(bundle, args); 340 | } 341 | }); 342 | } 343 | 344 | // add main import 345 | bundle.add(self.config.main); 346 | 347 | bundle.bundle(function (err, js) { 348 | if (self.result.js.source.trim().slice(-1) !== ';') { 349 | js = ';' + js; 350 | } 351 | self.result.js.source = self.result.js.source + js; 352 | 353 | self.timing('browserify finish'); 354 | done(err); 355 | }); 356 | }; 357 | 358 | /* 359 | * Main moonboots functions. 360 | * These should be the only methods you call. 361 | */ 362 | 363 | //Send jsSource to callback, rebuilding every time if in development mode 364 | Moonboots.prototype.jsSource = function (cb) { 365 | if (this.config.cache) { 366 | return cb(null, this.result.js.source); 367 | } 368 | this.bundleJS(false, cb); 369 | }; 370 | 371 | //Send cssSource to callback, rebuilding every time if in development mode 372 | Moonboots.prototype.cssSource = function (cb) { 373 | if (this.config.cache) { 374 | return cb(null, this.result.css.source); 375 | } 376 | this.bundleCSS(false, cb); 377 | }; 378 | 379 | //Return jsFileName, which never changes 380 | Moonboots.prototype.jsFileName = function () { 381 | return this.result.js.fileName; 382 | }; 383 | 384 | //Return jsFileName, which never changes 385 | Moonboots.prototype.cssFileName = function () { 386 | return this.result.css.fileName; 387 | }; 388 | 389 | //Return htmlContext, which never changes 390 | Moonboots.prototype.htmlContext = function () { 391 | return this.result.html.context; 392 | }; 393 | 394 | //Return htmlSource, which never changes 395 | Moonboots.prototype.htmlSource = function () { 396 | return this.result.html.source; 397 | }; 398 | 399 | Moonboots.prototype.timing = function (message) { 400 | if (this.config.timingMode) { 401 | this.emit('log', ['moonboots', 'timing', 'debug'], message, Date.now()); 402 | } 403 | }; 404 | 405 | /* 406 | * End main moonboots functions. 407 | */ 408 | 409 | // Main export 410 | module.exports = Moonboots; 411 | 412 | 413 | // a few helpers 414 | function concatFiles(arrayOfFiles) { 415 | return arrayOfFiles.map(function (fileName) { 416 | var source = fs.readFileSync(fileName, 'utf8'); 417 | if (path.extname(fileName) !== '.js') { 418 | return source; 419 | } 420 | if (source.trim().slice(-1) !== ';') { 421 | source = source + ';'; 422 | } 423 | return source; 424 | }).join('\n'); 425 | } 426 | 427 | function linkTag(filename) { 428 | return '\n'; 429 | } 430 | 431 | function scriptTag(filename) { 432 | return ''; 433 | } 434 | 435 | function errorTrace(err) { 436 | var trace; 437 | if (err.stack) { 438 | trace = err.stack; 439 | } else { 440 | trace = JSON.stringify(err); 441 | } 442 | trace = trace.split('\n').join('
').replace(/"/g, '"'); 443 | return 'document.write("
' + trace + '
");'; 444 | } 445 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moonboots", 3 | "description": "A set of tools and conventions for building/serving clientside apps with node.js", 4 | "version": "5.0.1", 5 | "author": "Henrik Joreteg ", 6 | "bugs": "https://github.com/henrikjoreteg/moonboots/issues", 7 | "contributors": [ 8 | { 9 | "name": "Philip Roberts", 10 | "email": "phil@latentflip.com" 11 | }, 12 | { 13 | "name": "Luke Karrys", 14 | "email": "luke@andyet.net" 15 | }, 16 | { 17 | "name": "Michael Garvin", 18 | "email": "gar@comrade.us" 19 | } 20 | ], 21 | "dependencies": { 22 | "async": "^1.5.2", 23 | "browserify": "^13.0.0", 24 | "bundle-metadata": "^1.0.1", 25 | "cssmin": "^0.4.3", 26 | "module-deps": "^4.0.5", 27 | "uglify-js": "^2.6.2" 28 | }, 29 | "devDependencies": { 30 | "backbone": "^1.0.0", 31 | "code": "^2.2.0", 32 | "jade": "^1.11.0", 33 | "jadeify": "^4.1.0", 34 | "jquery": "^2.2.3", 35 | "lab": "^10.3.1", 36 | "precommit-hook": "^3.0.0", 37 | "through": "^2.3.4" 38 | }, 39 | "homepage": "https://github.com/henrikjoreteg/moonboots", 40 | "keywords": [ 41 | "browserify", 42 | "clientside", 43 | "commonjs", 44 | "singlepage" 45 | ], 46 | "license": "MIT", 47 | "main": "index.js", 48 | "pre-commit": [ 49 | "lint", 50 | "validate", 51 | "test" 52 | ], 53 | "repository": { 54 | "type": "git", 55 | "url": "git@github.com:HenrikJoreteg/moonboots.git" 56 | }, 57 | "scripts": { 58 | "lint": "jshint .", 59 | "test": "make test", 60 | "validate": "npm ls" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # moonboots 2 | 3 | Moonboots makes it incredibly easy to jump into single-page-app development by encapsulating a set of conventions and tools for building, bundling, and serving SPAs with node.js. 4 | 5 | Powered by [browserify](http://browserify.org/), moonboots gives us a structured way to include non-CommonJS libraries, work in development mode and agressively cache built JS and CSS files for production. 6 | 7 | 8 | ## What it does 9 | 10 | 1. Saves us from re-inventing this process for each app. 11 | 1. Lets a developer focus on building a great clientside experience, not boiler plate. 12 | 1. Lets you use CommonJS modules to structure your clientside code. 13 | 1. Manages clientside files during development so you can just write code. 14 | 1. Compiles/minifies/uniquely named JS files (and CSS files optionally) containing your application allowing really aggressive caching (since the name will change if the app does). 15 | 1. Plays nicely with [express.js](http://expressjs.com), [hapi.js](http://hapijs.com), or even straight [node http](http://nodejs.org/api/http.html) 16 | 17 | ## Why? 18 | 19 | 1. Because single page apps are different. You're shipping an application to be run on the browser instead of running an application to ship a view to the browser. 20 | 1. Engineering a good client-side app requires a good set of conventions and structure for organizing your code. 21 | 1. Effeciently building/sending a client-side app to the browser is a tricky problem. It's easy to build convoluted solutions. We want something a bit simpler to use. 22 | 23 | 24 | ## How to use it 25 | 26 | You grab your moonboots, pass it a config and listen for the `ready` event. Then tell your http library which urls to serve your single page app at. 27 | 28 | That's it. 29 | 30 | ```js 31 | var express = require('express'); 32 | var Moonboots = require('moonboots'); 33 | var app = express(); 34 | 35 | // configure our app 36 | var clientApp = new Moonboots({ 37 | main: __dirname + '/sample/app/app.js', 38 | libraries: [ 39 | __dirname + '/sample/libraries/jquery.js' 40 | ], 41 | stylesheets: [ 42 | __dirname + '/styles.css' 43 | ] 44 | }); 45 | 46 | clientApp.on('ready', function () { 47 | app.get(clientApp.jsFileName(), 48 | function (req, res) { 49 | clientApp.jsSource(function (err, js) { 50 | res.send(js); 51 | }) 52 | } 53 | ); 54 | app.get('/app*', clientApp.htmlSource()); 55 | 56 | // start listening for http requests 57 | app.listen(3000); 58 | }); 59 | ``` 60 | 61 | 62 | ## Options 63 | 64 | Available options that can be passed to Moonboots: 65 | 66 | - `main` (required, filepath) - The main entry point of your client app. Browserify uses this to build out your dependency tree. 67 | - `libraries` (optional, array of file paths, default: []) - An array of paths of JS files to concatenate and include before any CommonJS bundled code. This is useful for stuff like jQuery and jQuery plugins. Note that they will be included in the order specified. So if you're including a jQuery plugin, you'd better be sure that jQuery is listed first. 68 | - `stylesheets` (optional, array of file paths, default: []) - An array of CSS files to concatenate 69 | - `jsFileName` (optional, string, default: app) - the name of the JS file that will be built 70 | - `cssFileName` (optional, string, default: styles) - the name of the CSS file that will be built 71 | - `browserify` (optional, object, default: {debug: false}) - options to pass directly into `browserify`, as detailed [here](https://github.com/substack/node-browserify#browserifyfiles--opts). Additional options are: 72 | - `browserify.transform` (optional, list, default: []) - list of transforms to apply to the browserify bundle, see [here](https://github.com/substack/node-browserify#btransformtr-opts) for more details. 73 | - `uglify` (optional, object, default: {}) - options to pass directly into uglify, as detailed [here](https://github.com/mishoo/UglifyJS2) 74 | - `modulesDir` (optional, directory path, default: '') - directory path of modules to be directly requirable (without using relative require paths). For example if you've got some helper modules that are not on npm but you still want to be able to require directly by name, you can include them here. So you can, for example, put a modified version of backbone in here and still just `require('backbone')`. 75 | - `beforeBuildJS` (optional, function, default: nothing) - function to run before building the browserify bundle during development. This is useful for stuff like compiling clientside templates that need to be included in the bundle. If you specify a callback moonboots will wait for you to call it. If not, it will be run synchronously (by the magic of Function.prototype.length). 76 | - `beforeBuildCSS` (optional, function, default: nothing) - function to run before concatenating your CSS files during development. This is useful for stuff like generating your CSS files from a preprocessor. If you specify a callback moonboots will wait for you to call it. If not, it will be run synchronously (by the magic of Function.prototype.length). 77 | - `sourceMaps` (optional, boolean, default: false) - set to true to enable sourcemaps (sets browserify.debug to true) 78 | - `resourcePrefix` (optional, string, default: '/') - specify what dirname should be prefixed when generating html source. If you're serving the whole app statically you may need relative paths. So just passing resourcePrefix: '' would make the template render with `` instead of ``. 79 | - `minify` (optional, boolean, default: true) - an option for whether to minify JS and CSS. 80 | - `cache` (optional, boolean, default: true) - an option for whether or not to recalculate the bundles each time 81 | - `buildDirectory` (optional, string, default: nothing) - directory path in which to write the js and css bundles after building and optionally minifying. If this is set, moonboots will first look in this folder for files matching the jsFileName and cssFileName parameters and use them if present. Those files will be trusted implicitly and no hashing or building will be done. 82 | - `developmentMode` (optional, boolean, default: false) - If this is true, forces cache to false, minify to false, and disables buildDirectory 83 | - `timingMode` (optional, boolean, default: false) - If set to true, moonboots will emit log events w/ a 'timing' flag at various stages of building to assist with performance issues 84 | 85 | ## About Source Maps 86 | 87 | Sourcemaps let you send the actual code to the browser along with a mapping to the individual module files. This makes it easier to debug, since you can get relevant line numbers that correspond to your actual source within your modules instead of the built bundle source. 88 | 89 | Please note that if you are using `libraries` your line numbers will be off, because that prepends those files to the main bundle. If it is important for you to maintain line numbers in your source maps, consider using [browserify-shim](https://github.com/thlorenz/browserify-shim) in your transforms to include those non-commonjs files in your app 90 | 91 | ## Methods 92 | 93 | **moonboots.jsFileName()** - returns string of the current js filename. 94 | 95 | **moonboots.jsSource()** - returns compiled (and optionally minified js source) 96 | 97 | **moonboots.cssFileName()** - returns string of the current css filename. 98 | 99 | **moonboots.cssSource()** - returns concatenated (and optionally minified css stylesheets) 100 | 101 | **moonboots.htmlSource()** - returns default html to serve that represents your compiled app w/ a script and optional style tag 102 | 103 | **moonboots.htmlContext()** - returns object w/ jsFileName and cssFileName attributes to pass to your http server's templating engine to build your own html source 104 | 105 | 106 | ## Full example 107 | 108 | For a working example, check out [moonboots-hapi](https://github.com/wraithgar/moonboots-hapi) or [moonboots-express](https://github.com/lukekarrys/moonboots-express) or even [moonboots-static](https://github.com/lukekarrys/moonboots-static) 109 | 110 | 111 | ## License 112 | 113 | MIT 114 | -------------------------------------------------------------------------------- /test/build.js: -------------------------------------------------------------------------------- 1 | var Lab = require('lab'); 2 | var Code = require('code'); 3 | var lab = exports.lab = Lab.script(); 4 | var async = require('async'); 5 | var os = require('os'); 6 | var fs = require('fs'); 7 | var path = require('path'); 8 | var crypto = require('crypto'); 9 | var Moonboots = require('..'); 10 | var moonboots; 11 | 12 | lab.experiment('files get read from buildDirectory', function () { 13 | var tmpHash = crypto.randomBytes(16).toString('hex'); 14 | var buildDir = path.join(os.tmpdir(), tmpHash); 15 | lab.before(function (done) { 16 | async.series([ 17 | function (next) { 18 | fs.mkdir(buildDir, next); 19 | }, 20 | function (next) { 21 | fs.writeFile(path.join(buildDir, 'app.deadbeef.min.js'), 'javascript!' + tmpHash, next); 22 | }, 23 | function (next) { 24 | fs.writeFile(path.join(buildDir, 'app.deadbeef.min.css'), 'cascading stylesheets!' + tmpHash, next); 25 | }, 26 | function (next) { 27 | fs.writeFile(path.join(buildDir, 'readme.md'), '# this file will be ignored in the builddir', next); 28 | } 29 | ], function () { 30 | var options = { 31 | main: __dirname + '/../fixtures/app/app.js', 32 | jsFileName: 'app', 33 | cssFileName: 'app', 34 | buildDirectory: buildDir, 35 | stylesheets: [ 36 | __dirname + '/../fixtures/stylesheets/style.css' 37 | ] 38 | }; 39 | moonboots = new Moonboots(options); 40 | moonboots.on('ready', done); 41 | }); 42 | }); 43 | lab.after(function (done) { 44 | async.series([ 45 | function (next) { 46 | fs.unlink(path.join(buildDir, 'app.deadbeef.min.js'), next); 47 | }, 48 | function (next) { 49 | fs.unlink(path.join(buildDir, 'app.deadbeef.min.css'), next); 50 | }, 51 | function (next) { 52 | fs.unlink(path.join(buildDir, 'readme.md'), next); 53 | }, 54 | function (next) { 55 | fs.rmdir(buildDir, next); 56 | } 57 | ], function (err) { 58 | if (err) {throw err; } 59 | done(); 60 | }); 61 | }); 62 | lab.test('htmlContext', function (done) { 63 | var context = moonboots.htmlContext(); 64 | Code.expect(context).to.include(['jsFileName', 'cssFileName']); 65 | Code.expect(context.jsFileName).to.equal('app.deadbeef.min.js'); 66 | Code.expect(context.cssFileName).to.equal('app.deadbeef.min.css'); 67 | done(); 68 | }); 69 | lab.test('js', function (done) { 70 | moonboots.jsSource(function (err, js) { 71 | Code.expect(js).to.equal('javascript!' + tmpHash); 72 | done(); 73 | }); 74 | }); 75 | lab.test('css', function (done) { 76 | moonboots.cssSource(function (err, css) { 77 | Code.expect(css).to.equal('cascading stylesheets!' + tmpHash); 78 | done(); 79 | }); 80 | }); 81 | }); 82 | 83 | lab.experiment('Files get written to build directory', function () { 84 | var tmpHash = crypto.randomBytes(16).toString('hex'); 85 | var buildDir = path.join(os.tmpdir(), tmpHash); 86 | lab.before(function (done) { 87 | fs.mkdir(buildDir, function () { 88 | var options = { 89 | main: __dirname + '/../fixtures/app/app.js', 90 | jsFileName: 'app', 91 | cssFileName: 'app', 92 | buildDirectory: buildDir, 93 | stylesheets: [ 94 | __dirname + '/../fixtures/stylesheets/style.css' 95 | ], 96 | libraries: [ 97 | __dirname + '/../fixtures/libraries/iife-no-semicolon.js', 98 | __dirname + '/../fixtures/libraries/lib.js' 99 | ] 100 | }; 101 | moonboots = new Moonboots(options); 102 | moonboots.on('ready', done); 103 | }); 104 | }); 105 | lab.after(function (done) { 106 | async.series([ 107 | function (next) { 108 | fs.unlink(path.join(buildDir, 'app.9b1ed6d6.min.js'), next); 109 | }, 110 | function (next) { 111 | fs.unlink(path.join(buildDir, 'app.38ea6c96.min.css'), next); 112 | }, 113 | function (next) { 114 | fs.rmdir(buildDir, next); 115 | } 116 | ], function (err) { 117 | if (err) {throw err; } 118 | done(); 119 | }); 120 | }); 121 | lab.test('js file was written', function (done) { 122 | var jsFileName = moonboots.jsFileName(); 123 | var filePath = path.join(buildDir, jsFileName); 124 | Code.expect(jsFileName).to.equal('app.9b1ed6d6.min.js'); 125 | fs.readFile(filePath, 'utf8', function (err) { 126 | Code.expect(err).to.not.be.ok; 127 | // Test that iife-no-semicolon.js doesn't introduce a parsing bug 128 | // via a (function () {…})\n(function () {…}) sequence 129 | Code.expect(function () { require(filePath); }).to.not.throw(); 130 | done(); 131 | }); 132 | }); 133 | lab.test('css file was written', function (done) { 134 | var cssFileName = moonboots.cssFileName(); 135 | Code.expect(cssFileName).to.equal('app.38ea6c96.min.css'); 136 | fs.readFile(path.join(buildDir, cssFileName), 'utf8', function (err) { 137 | Code.expect(err).to.not.be.ok; 138 | done(); 139 | }); 140 | }); 141 | }); 142 | -------------------------------------------------------------------------------- /test/consistentHash.js: -------------------------------------------------------------------------------- 1 | var Lab = require('lab'); 2 | var Code = require('code'); 3 | var lab = exports.lab = Lab.script(); 4 | var async = require('async'); 5 | var Moonboots = require('..'); 6 | 7 | function arrEqual(arr) { 8 | if (arr.length > 0) { 9 | for (var i = 1; i < arr.length; i++) { 10 | if (arr[i] !== arr[0]) { 11 | return arr[i]; 12 | } 13 | } 14 | } 15 | return true; 16 | } 17 | 18 | 19 | lab.experiment('Hash is the same', function () { 20 | function setup(done) { 21 | var options = { 22 | main: __dirname + '/../fixtures/app/appImports.js', 23 | jsFileName: 'app', 24 | minify: false 25 | }; 26 | var moonboots = new Moonboots(options); 27 | moonboots.on('ready', function () { 28 | done(moonboots); 29 | }); 30 | } 31 | 32 | lab.test('50 times', function (done) { 33 | async.timesSeries(50, function (index, next) { 34 | setup(function (moonboots) { 35 | var filename = moonboots.jsFileName(); 36 | moonboots.jsSource(function (err, js) { 37 | next(null, [filename, js]); 38 | }); 39 | }); 40 | }, function (err, results) { 41 | var filenames = results.map(function (r) { 42 | return r[0]; 43 | }); 44 | var js = results.map(function (r) { 45 | return r[1]; 46 | }); 47 | Code.expect(arrEqual(filenames)).to.equal(true); 48 | Code.expect(arrEqual(js)).to.equal(true); 49 | done(); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/css.js: -------------------------------------------------------------------------------- 1 | var Lab = require('lab'); 2 | var Code = require('code'); 3 | var lab = exports.lab = Lab.script(); 4 | var Moonboots = require('..'); 5 | var moonboots; 6 | var EXPECTED_CSS_HASH = 'app.38ea6c96.css'; 7 | var EXPECTED_CSS_MIN_HASH = 'app.38ea6c96.min.css'; 8 | 9 | 10 | lab.experiment('css with default options', function () { 11 | lab.before(function (done) { 12 | var default_options = { 13 | main: __dirname + '/../fixtures/app/app.js', 14 | cssFileName: 'app', 15 | stylesheets: [ 16 | __dirname + '/../fixtures/stylesheets/style.css' 17 | ] 18 | }; 19 | moonboots = new Moonboots(default_options); 20 | moonboots.on('ready', done); 21 | }); 22 | lab.test('filename', function (done) { 23 | Code.expect(moonboots.cssFileName(), 'css filename').to.equal(EXPECTED_CSS_MIN_HASH); 24 | done(); 25 | }); 26 | lab.test('content', function (done) { 27 | moonboots.cssSource(function (err, css) { 28 | Code.expect(css, 'css source').to.equal('body{background:#ccc}'); 29 | }); 30 | done(); 31 | }); 32 | }); 33 | 34 | lab.experiment('css with no minify', function () { 35 | lab.before(function (done) { 36 | var no_minify = { 37 | main: __dirname + '/../fixtures/app/app.js', 38 | cssFileName: 'app', 39 | minify: false, 40 | stylesheets: [ 41 | __dirname + '/../fixtures/stylesheets/style.css' 42 | ] 43 | }; 44 | moonboots = new Moonboots(no_minify); 45 | moonboots.on('ready', done); 46 | }); 47 | lab.test('filename', function (done) { 48 | Code.expect(moonboots.cssFileName(), 'css filename').to.equal(EXPECTED_CSS_HASH); 49 | done(); 50 | }); 51 | lab.test('content', function (done) { 52 | moonboots.cssSource(function (err, css) { 53 | Code.expect(css, 'css source').to.equal('body {background: #ccc;}\n'); 54 | }); 55 | done(); 56 | }); 57 | }); 58 | 59 | lab.experiment('css with .css already added', function () { 60 | lab.before(function (done) { 61 | var options = { 62 | main: __dirname + '/../fixtures/app/app.js', 63 | cssFileName: 'app.css', 64 | stylesheets: [ 65 | __dirname + '/../fixtures/stylesheets/style.css' 66 | ] 67 | }; 68 | moonboots = new Moonboots(options); 69 | moonboots.on('ready', done); 70 | }); 71 | lab.test('filename', function (done) { 72 | Code.expect(moonboots.cssFileName(), 'css filename').to.equal(EXPECTED_CSS_MIN_HASH); 73 | done(); 74 | }); 75 | }); 76 | 77 | 78 | lab.experiment('async beforeBuildCSS', function () { 79 | var beforeRan = false; 80 | lab.before(function (done) { 81 | var no_minify = { 82 | main: __dirname + '/../fixtures/app/app.js', 83 | cssFileName: 'app', 84 | minify: false, 85 | beforeBuildCSS: function (next) { 86 | beforeRan = true; 87 | next(); 88 | }, 89 | stylesheets: [ 90 | __dirname + '/../fixtures/stylesheets/style.css' 91 | ] 92 | }; 93 | moonboots = new Moonboots(no_minify); 94 | moonboots.on('ready', done); 95 | }); 96 | lab.test('ran', function (done) { 97 | Code.expect(beforeRan).to.equal(true); 98 | done(); 99 | }); 100 | }); 101 | 102 | lab.experiment('sync beforeBuildCSS', function () { 103 | var beforeRan = false; 104 | lab.before(function (done) { 105 | var no_minify = { 106 | main: __dirname + '/../fixtures/app/app.js', 107 | cssFileName: 'app', 108 | minify: false, 109 | beforeBuildCSS: function () { 110 | beforeRan = true; 111 | }, 112 | stylesheets: [ 113 | __dirname + '/../fixtures/stylesheets/style.css' 114 | ] 115 | }; 116 | moonboots = new Moonboots(no_minify); 117 | moonboots.on('ready', done); 118 | }); 119 | lab.test('ran', function (done) { 120 | Code.expect(beforeRan).to.equal(true); 121 | done(); 122 | }); 123 | }); 124 | 125 | lab.experiment('bad css', function () { 126 | lab.before(function (done) { 127 | var bad_css = { 128 | main: __dirname + '/../fixtures/app/app.js', 129 | cssFileName: 'app', 130 | stylesheets: [ 131 | __dirname + '/../fixtures/stylesheets/style.css' 132 | ], 133 | beforeBuildCSS: function (done) { 134 | done('Could not build css'); 135 | } 136 | }; 137 | moonboots = new Moonboots(bad_css); 138 | moonboots.on('ready', done); 139 | }); 140 | lab.test('empty css, no crashing', function (done) { 141 | moonboots.cssSource(function (err, css) { 142 | Code.expect(css, 'css source').to.equal(undefined); 143 | done(); 144 | }); 145 | }); 146 | }); 147 | -------------------------------------------------------------------------------- /test/dev.js: -------------------------------------------------------------------------------- 1 | var Lab = require('lab'); 2 | var Code = require('code'); 3 | var lab = exports.lab = Lab.script(); 4 | var Moonboots = require('..'); 5 | var moonboots, beforeBuildJSRan, beforeBuildCSSRan, transformRan; 6 | 7 | lab.experiment('development mode', function () { 8 | lab.before(function (done) { 9 | var options = { 10 | developmentMode: true, 11 | main: __dirname + '/../fixtures/app/app.js', 12 | jsFileName: 'app', 13 | cssFileName: 'app', 14 | beforeBuildJS: function (cb) { 15 | beforeBuildJSRan = true; 16 | cb(); 17 | }, 18 | beforeBuildCSS: function (cb) { 19 | beforeBuildCSSRan = true; 20 | cb(); 21 | }, 22 | modulesDir: __dirname + '/../fixtures/modules', 23 | browserify: { 24 | transforms: [ 25 | function () { 26 | var through = require('through'); 27 | transformRan = true; 28 | return through( 29 | function write(data) { 30 | this.queue(data); 31 | }, 32 | function _end() { 33 | this.queue(null); 34 | } 35 | ); 36 | } 37 | ] 38 | }, 39 | stylesheets: [ 40 | __dirname + '/../fixtures/stylesheets/style.css' 41 | ] 42 | }; 43 | moonboots = new Moonboots(options); 44 | moonboots.on('ready', done); 45 | }); 46 | lab.test('htmlContext', function (done) { 47 | var context = moonboots.htmlContext(); 48 | Code.expect(context).to.include(['jsFileName', 'cssFileName']); 49 | Code.expect(context.jsFileName).to.equal('app.nonCached.js'); 50 | Code.expect(context.cssFileName).to.equal('app.nonCached.css'); 51 | done(); 52 | }); 53 | lab.test('js rebuilds every request', function (done) { 54 | beforeBuildJSRan = false; 55 | moonboots.jsSource(function (err, js) { 56 | Code.expect(beforeBuildJSRan).to.equal(true); 57 | Code.expect(transformRan).to.equal(true); 58 | Code.expect(js, 'js source').to.contain('"foo"'); 59 | done(); 60 | }); 61 | }); 62 | lab.test('css rebuilds every request', function (done) { 63 | beforeBuildCSSRan = false; 64 | transformRan = false; 65 | moonboots.cssSource(function () { 66 | Code.expect(beforeBuildCSSRan).to.equal(true); 67 | done(); 68 | }); 69 | }); 70 | }); 71 | 72 | -------------------------------------------------------------------------------- /test/errors.js: -------------------------------------------------------------------------------- 1 | var Lab = require('lab'); 2 | var Code = require('code'); 3 | var lab = exports.lab = Lab.script(); 4 | var Moonboots = require('..'); 5 | var domain = require('domain'); 6 | var moonboots; 7 | 8 | lab.experiment('error states', function () { 9 | lab.test('missing main options', function (done) { 10 | function initBad() { 11 | moonboots = new Moonboots({}); 12 | } 13 | Code.expect(initBad).to.throw(Error); 14 | done(); 15 | }); 16 | lab.test('invalid options', function (done) { 17 | function initEmpty() { 18 | moonboots = new Moonboots(); 19 | } 20 | Code.expect(initEmpty).to.throw(Error); 21 | done(); 22 | }); 23 | lab.test('invalid build directory', function (done) { 24 | moonboots = new Moonboots({ 25 | main: __dirname + '/../fixtures/app/app.js', 26 | stylesheets: [ 27 | __dirname + '/../fixtures/stylesheets/style.css' 28 | ], 29 | buildDirectory: __dirname + '/nonexistant' 30 | }); 31 | moonboots.on('ready', function () { 32 | var context = moonboots.htmlContext(); 33 | Code.expect(context.jsFileName).to.equal('app.794c89f5.min.js'); 34 | done(); 35 | }); 36 | }); 37 | lab.test('unreadable build js file', function (done) { 38 | moonboots = new Moonboots({ 39 | main: __dirname + '/../fixtures/app/app.js', 40 | stylesheets: [ 41 | __dirname + '/../fixtures/stylesheets/style.css' 42 | ], 43 | buildDirectory: __dirname + '/../fixtures/build1' 44 | }); 45 | moonboots.on('ready', function () { 46 | var context = moonboots.htmlContext(); 47 | Code.expect(context.jsFileName).to.equal('app.794c89f5.min.js'); 48 | done(); 49 | }); 50 | }); 51 | lab.test('unreadable build css file', function (done) { 52 | moonboots = new Moonboots({ 53 | main: __dirname + '/../fixtures/app/app.js', 54 | buildDirectory: __dirname + '/../fixtures/build2' 55 | }); 56 | moonboots.on('ready', function () { 57 | var context = moonboots.htmlContext(); 58 | Code.expect(context.jsFileName).to.equal('app.248957fa.min.js'); 59 | done(); 60 | }); 61 | }); 62 | lab.test('browserify error in development mode', function (done) { 63 | moonboots = new Moonboots({ 64 | main: __dirname + '/../fixtures/app/badapp.js', 65 | developmentMode: true 66 | }); 67 | moonboots.on('ready', function () { 68 | moonboots.jsSource(function (err, source) { 69 | Code.expect(source.indexOf('document.write'), 'inline error').to.equal(0); 70 | Code.expect(source.indexOf("Error: Cannot find module 'not-a-module' from"), 'inline error').to.not.equal(-1); 71 | done(); 72 | }); 73 | }); 74 | }); 75 | lab.test('browserify error not in development mode should throw', function (done) { 76 | var errDomain = domain.create(); 77 | 78 | errDomain.run(function () { 79 | moonboots = new Moonboots({ 80 | main: __dirname + '/../fixtures/app/badapp.js' 81 | }); 82 | }); 83 | 84 | errDomain.on('error', function (e) { 85 | Code.expect(e.message).to.match(/not found/); 86 | done(); 87 | }); 88 | }); 89 | lab.test('beforeBuildJS error in development mode', function (done) { 90 | var errMsg = 'This is a before build error!'; 91 | moonboots = new Moonboots({ 92 | main: __dirname + '/../fixtures/app/app.js', 93 | developmentMode: true, 94 | beforeBuildJS: function (cb) { 95 | cb(new Error(errMsg)); 96 | } 97 | }); 98 | moonboots.on('ready', function () { 99 | moonboots.jsSource(function (err, source) { 100 | Code.expect(source.indexOf('document.write'), 'inline error').to.equal(0); 101 | Code.expect(source.indexOf(errMsg), 'inline error').to.not.equal(-1); 102 | done(); 103 | }); 104 | }); 105 | }); 106 | lab.test('beforeBuildJS errors without an error object', function (done) { 107 | var errMsg = 'This is a before build error!'; 108 | var errMsgKey = 'errorMessage'; 109 | moonboots = new Moonboots({ 110 | main: __dirname + '/../fixtures/app/app.js', 111 | developmentMode: true, 112 | beforeBuildJS: function (cb) { 113 | var err = {}; 114 | err[errMsgKey] = errMsg; 115 | cb(err); 116 | } 117 | }); 118 | moonboots.on('ready', function () { 119 | moonboots.jsSource(function (err, source) { 120 | Code.expect(source.indexOf('document.write'), 'inline error').to.equal(0); 121 | Code.expect(source.indexOf(errMsg), 'inline error').to.not.equal(-1); 122 | Code.expect(source.indexOf(errMsgKey), 'inline error').to.not.equal(-1); 123 | done(); 124 | }); 125 | }); 126 | }); 127 | lab.test('beforeBuildJS error not in development mode should throw', function (done) { 128 | var errDomain = domain.create(); 129 | var errMsg = 'This is a before build error!'; 130 | 131 | errDomain.run(function () { 132 | moonboots = new Moonboots({ 133 | main: __dirname + '/../fixtures/app/app.js', 134 | beforeBuildJS: function (cb) { 135 | cb(new Error(errMsg)); 136 | } 137 | }); 138 | }); 139 | 140 | errDomain.on('error', function (e) { 141 | Code.expect(e.message).to.equal(errMsg); 142 | done(); 143 | }); 144 | }); 145 | }); 146 | -------------------------------------------------------------------------------- /test/html.js: -------------------------------------------------------------------------------- 1 | var Lab = require('lab'); 2 | var Code = require('code'); 3 | var lab = exports.lab = Lab.script(); 4 | var Moonboots = require('..'); 5 | var moonboots; 6 | var EXPECTED_JS_MIN_HASH = 'app.794c89f5.min.js'; 7 | var EXPECTED_CSS_MIN_HASH = 'app.38ea6c96.min.css'; 8 | 9 | lab.experiment('html with default options', function () { 10 | lab.before(function (done) { 11 | var options = { 12 | main: __dirname + '/../fixtures/app/app.js', 13 | jsFileName: 'app', 14 | cssFileName: 'app', 15 | stylesheets: [ 16 | __dirname + '/../fixtures/stylesheets/style.css' 17 | ] 18 | }; 19 | moonboots = new Moonboots(options); 20 | moonboots.on('ready', done); 21 | }); 22 | lab.test('htmlContext', function (done) { 23 | var context = moonboots.htmlContext(); 24 | Code.expect(context).to.include(['jsFileName', 'cssFileName']); 25 | Code.expect(context.jsFileName).to.equal(EXPECTED_JS_MIN_HASH); 26 | Code.expect(context.cssFileName).to.equal(EXPECTED_CSS_MIN_HASH); 27 | done(); 28 | }); 29 | lab.test('htmlSource', function (done) { 30 | var source = moonboots.htmlSource(); 31 | Code.expect(source).to.equal('\n\n'); 32 | done(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/jade.js: -------------------------------------------------------------------------------- 1 | var Lab = require('lab'); 2 | var Code = require('code'); 3 | var lab = exports.lab = Lab.script(); 4 | var Moonboots = require('..'); 5 | var options = function (transform) { 6 | return { 7 | main: __dirname + '/../fixtures/app/appJadeImport.js', 8 | developmentMode: true, 9 | jsFileName: 'app', 10 | browserify: { 11 | transforms: [transform] 12 | } 13 | }; 14 | }; 15 | 16 | 17 | lab.experiment('Jade transform', function () { 18 | lab.test('ran', function (done) { 19 | var moonboots = new Moonboots(options('jadeify')); 20 | moonboots.on('ready', function () { 21 | moonboots.jsSource(function (err, js) { 22 | Code.expect(js).to.contain('"

All that you require to crash

"'); 23 | done(); 24 | }); 25 | }); 26 | }); 27 | lab.test('ran with pretty:true', function (done) { 28 | var moonboots = new Moonboots(options(['jadeify', {pretty: true}])); 29 | moonboots.on('ready', function () { 30 | moonboots.jsSource(function (err, js) { 31 | Code.expect(js).to.contain('"\\n

All that you require to crash

"'); 32 | done(); 33 | }); 34 | }); 35 | }); 36 | }); 37 | 38 | 39 | -------------------------------------------------------------------------------- /test/js.js: -------------------------------------------------------------------------------- 1 | var Lab = require('lab'); 2 | var Code = require('code'); 3 | var lab = exports.lab = Lab.script(); 4 | var Moonboots = require('..'); 5 | var moonboots; 6 | 7 | var EXPECTED_JS_HASH = 'app.794c89f5.js'; 8 | var EXPECTED_JS_MIN_HASH = 'app.794c89f5.min.js'; 9 | 10 | lab.experiment('js with default options', function () { 11 | lab.before(function (done) { 12 | var options = { 13 | main: __dirname + '/../fixtures/app/app.js', 14 | jsFileName: 'app' 15 | }; 16 | moonboots = new Moonboots(options); 17 | moonboots.on('ready', done); 18 | }); 19 | lab.test('filename', function (done) { 20 | Code.expect(moonboots.jsFileName(), 'js filename').to.equal(EXPECTED_JS_MIN_HASH); 21 | done(); 22 | }); 23 | /* 24 | lab.test('content', function (done) { 25 | Code.expect(moonboots.jsSource(), 'js source').to.equal('how do we even test this?'); 26 | done(); 27 | }); 28 | */ 29 | }); 30 | 31 | lab.experiment('js with uglify options', function () { 32 | lab.before(function (done) { 33 | var options = { 34 | main: __dirname + '/../fixtures/app/app.js', 35 | jsFileName: 'app', 36 | uglify: { 37 | mangle: false 38 | } 39 | }; 40 | 41 | moonboots = new Moonboots(options); 42 | moonboots.on('ready', done); 43 | }); 44 | lab.test('filename', function (done) { 45 | Code.expect(moonboots.jsFileName(), 'js filename').to.equal(EXPECTED_JS_MIN_HASH); 46 | done(); 47 | }); 48 | }); 49 | 50 | lab.experiment('js with no minify', function () { 51 | lab.before(function (done) { 52 | var options = { 53 | main: __dirname + '/../fixtures/app/app.js', 54 | jsFileName: 'app', 55 | minify: false 56 | }; 57 | moonboots = new Moonboots(options); 58 | moonboots.on('ready', done); 59 | }); 60 | lab.test('filename', function (done) { 61 | Code.expect(moonboots.jsFileName(), 'js filename').to.equal(EXPECTED_JS_HASH); 62 | done(); 63 | }); 64 | /* 65 | lab.test('content', function (done) { 66 | Code.expect(moonboots.jsSource(), 'js source').to.equal('how do we even test this?'); 67 | done(); 68 | }); 69 | */ 70 | }); 71 | 72 | lab.experiment('js with .js already added', function () { 73 | lab.before(function (done) { 74 | var options = { 75 | main: __dirname + '/../fixtures/app/app.js', 76 | jsFileName: 'app.js' 77 | }; 78 | moonboots = new Moonboots(options); 79 | moonboots.on('ready', done); 80 | }); 81 | lab.test('filename', function (done) { 82 | Code.expect(moonboots.jsFileName(), 'js filename').to.equal(EXPECTED_JS_MIN_HASH); 83 | done(); 84 | }); 85 | }); 86 | 87 | lab.experiment('modulesDir', function () { 88 | lab.before(function (done) { 89 | var options = { 90 | main: __dirname + '/../fixtures/app/app.js', 91 | jsFileName: 'app', 92 | modulesDir: __dirname + '/../fixtures/modules' 93 | }; 94 | moonboots = new Moonboots(options); 95 | moonboots.on('ready', done); 96 | }); 97 | lab.test('module foo is in source', function (done) { 98 | moonboots.jsSource(function (err, js) { 99 | Code.expect(js, 'js source').to.contain('"foo"'); 100 | done(); 101 | }); 102 | }); 103 | }); 104 | 105 | lab.experiment('transforms', function () { 106 | var transformRan = 0; 107 | lab.before(function (done) { 108 | var options = { 109 | main: __dirname + '/../fixtures/app/app.js', 110 | jsFileName: 'app', 111 | browserify: { 112 | transforms: [ 113 | function () { 114 | var through = require('through'); 115 | transformRan++; 116 | return through( 117 | function write() {}, 118 | function _end() { 119 | this.queue(null); 120 | } 121 | ); 122 | } 123 | ] 124 | } 125 | }; 126 | moonboots = new Moonboots(options); 127 | moonboots.on('ready', done); 128 | }); 129 | lab.test('ran', function (done) { 130 | Code.expect(transformRan).to.equal(1); 131 | done(); 132 | }); 133 | }); 134 | 135 | 136 | lab.experiment('transforms with transform option', function () { 137 | var transformRan = 0; 138 | lab.before(function (done) { 139 | var options = { 140 | main: __dirname + '/../fixtures/app/app.js', 141 | jsFileName: 'app', 142 | browserify: { 143 | transform: [ 144 | function () { 145 | var through = require('through'); 146 | transformRan++; 147 | return through( 148 | function write() {}, 149 | function _end() { 150 | this.queue(null); 151 | } 152 | ); 153 | } 154 | ] 155 | } 156 | }; 157 | moonboots = new Moonboots(options); 158 | moonboots.on('ready', done); 159 | }); 160 | lab.test('ran', function (done) { 161 | Code.expect(transformRan).to.equal(1); 162 | done(); 163 | }); 164 | }); 165 | 166 | lab.experiment('sync beforeBuildJS', function () { 167 | var beforeRan = false; 168 | lab.before(function (done) { 169 | var options = { 170 | main: __dirname + '/../fixtures/app/app.js', 171 | jsFileName: 'app', 172 | minify: false, 173 | beforeBuildJS: function () { 174 | beforeRan = true; 175 | } 176 | }; 177 | moonboots = new Moonboots(options); 178 | moonboots.on('ready', done); 179 | }); 180 | lab.test('ran', function (done) { 181 | Code.expect(beforeRan).to.equal(true); 182 | done(); 183 | }); 184 | }); 185 | -------------------------------------------------------------------------------- /test/sourceMaps.js: -------------------------------------------------------------------------------- 1 | var Lab = require('lab'); 2 | var Code = require('code'); 3 | var lab = exports.lab = Lab.script(); 4 | var Moonboots = require('..'); 5 | var moonboots; 6 | 7 | lab.experiment('sourceMaps option sets browserify.debug', function () { 8 | lab.before(function (done) { 9 | var options = { 10 | main: __dirname + '/../fixtures/app/app.js', 11 | jsFileName: 'app', 12 | sourceMaps: true 13 | }; 14 | moonboots = new Moonboots(options); 15 | moonboots.on('ready', done); 16 | }); 17 | 18 | lab.test('filename', function (done) { 19 | Code.expect(moonboots.config.browserify.debug).to.equal(true); 20 | done(); 21 | }); 22 | }); 23 | 24 | lab.experiment('default is false', function () { 25 | lab.before(function (done) { 26 | var options = { 27 | main: __dirname + '/../fixtures/app/app.js', 28 | jsFileName: 'app' 29 | }; 30 | moonboots = new Moonboots(options); 31 | moonboots.on('ready', done); 32 | }); 33 | 34 | lab.test('filename', function (done) { 35 | Code.expect(moonboots.config.browserify.debug).to.equal(false); 36 | done(); 37 | }); 38 | }); 39 | 40 | lab.experiment('sourceMaps option can be overwritten by browserify.debug', function () { 41 | lab.before(function (done) { 42 | var options = { 43 | main: __dirname + '/../fixtures/app/app.js', 44 | jsFileName: 'app', 45 | sourceMaps: true, 46 | browserify: { 47 | debug: false 48 | } 49 | }; 50 | moonboots = new Moonboots(options); 51 | moonboots.on('ready', done); 52 | }); 53 | 54 | lab.test('filename', function (done) { 55 | Code.expect(moonboots.config.browserify.debug).to.equal(false); 56 | done(); 57 | }); 58 | }); -------------------------------------------------------------------------------- /test/timing.js: -------------------------------------------------------------------------------- 1 | var Lab = require('lab'); 2 | var Code = require('code'); 3 | var lab = exports.lab = Lab.script(); 4 | var Moonboots = require('..'); 5 | var moonboots; 6 | var timingEvents = 0; 7 | 8 | lab.experiment('timingMode', function () { 9 | lab.before(function (done) { 10 | var options = { 11 | main: __dirname + '/../fixtures/app/app.js', 12 | timingMode: true 13 | }; 14 | moonboots = new Moonboots(options); 15 | moonboots.on('log', function (levels) { 16 | if (levels.indexOf('timing') > -1) { 17 | timingEvents = timingEvents + 1; 18 | } 19 | }); 20 | moonboots.on('ready', done); 21 | }); 22 | lab.test('emits timing events', function (done) { 23 | Code.expect(timingEvents).to.be.above(0); 24 | done(); 25 | }); 26 | }); 27 | 28 | --------------------------------------------------------------------------------