package/0000775000175000017500000000000011732670206011417 5ustar daviddavidpackage/bones.js0000644000175000017500000000176111701101057013053 0ustar daviddavidif (global.__BonesPlugin__) { console.trace("\033[0;31mMultiple instances of bones are not supported.\033[0m"); process.exit(4); } exports.$ = require('jquery'); exports._ = require('underscore'); exports.mirror = require('mirror'); exports.utils = require('bones/server/utils'); exports.middleware = require('bones/server/middleware'); exports.server = true; exports.Backbone = require('bones/server/backbone'); exports.Router = require('bones/server/router'); exports.Model = require('bones/server/model'); exports.Collection = require('bones/server/collection'); exports.View = require('bones/server/view'); exports.Server = require('bones/server/server'); exports.Command = require('bones/server/command'); exports.load = function(dir) { return exports.plugin.load(dir); }; exports.start = function(callback) { return exports.plugin.start(callback); }; var Plugin = require('./server/plugin'); exports.plugin = global.__BonesPlugin__ = new Plugin(); exports.plugin.load(__dirname); package/test/0000755000175000017500000000000011715315015012367 5ustar daviddavidpackage/test/router.test.js0000644000175000017500000000044411701101057015217 0ustar daviddavidprocess.env.NODE_ENV = 'test'; var assert = require('assert'); var fs = require('fs'); var fixture = require('bones').plugin; exports['router behavior'] = function() { assert['throws'](function() { new fixture.routers.Page; }, "Can't initialize router without server."); }; package/test/hostname.test.js0000644000175000017500000000253211701101057015515 0ustar daviddavidprocess.env.NODE_ENV = 'test'; var assert = require('assert'); var os = require('os'); var server = require('./fixture/start').servers.Core; exports['hostname'] = function() { assert.response(server, { url: '/hostname' }, { status: 200 }); assert.response(server, { url: '/hostname', headers: { host: os.hostname() } }, { body: os.hostname(), status: 200 }); assert.response(server, { url: '/hostname', headers: { host: os.hostname() + ':3000' } }, { body: os.hostname() + ':3000', status: 200 }); assert.response(server, { url: '/hostname', headers: { host: 'other' } }, { body: 'other', status: 200 }); assert.response(server, { url: '/hostname', headers: { host: 'foo.third' } }, { body: 'foo.third', status: 200 }); assert.response(server, { url: '/hostname', headers: { host: 'foo.third:3000' } }, { body: 'foo.third:3000', status: 200 }); assert.response(server, { url: '/hostname', headers: { host: 'other.foo.third' } }, { status: 400 }); assert.response(server, { url: '/hostname', headers: { host: 'asdf' } }, { status: 400 }); }; package/test/fixture/0000755000175000017500000000000011715315015014055 5ustar daviddavidpackage/test/fixture/start.js0000644000175000017500000000021211701101057015535 0ustar daviddavid// Load application. require('./'); process.env.NODE_ENV = 'test'; process.argv[2] = 'start'; module.exports = require('bones').start(); package/test/fixture/config.json0000644000175000017500000000006411701101057016207 0ustar daviddavid{ "adminParty": true, "unknownOption": 42 } package/test/fixture/templates/0000755000175000017500000000000011715315015016053 5ustar daviddavidpackage/test/fixture/templates/Error._0000644000175000017500000000010411701101057017271 0ustar daviddavid
<%= error %>
package/test/fixture/templates/ServerSide.server._0000644000175000017500000000005211701101057021562 0ustar daviddavidThis is only available on the server side.package/test/fixture/views/0000755000175000017500000000000011715315015015212 5ustar daviddavidpackage/test/fixture/views/App.bones0000644000175000017500000000202211701101057016750 0ustar daviddavidview = Backbone.View.extend({ events: { 'click a.route': 'route' }, href: function(el) { var href = $(el).get(0).getAttribute('href', 2); if ($.browser.msie && $.browser.version < 8) { return /^([a-z]+:\/\/.+?)?(\/.*?)$/.exec(href)[2]; } else { return href; } }, route: function(ev) { var fragment = _(ev).isString() ? ev : this.href(ev.currentTarget); if (fragment.charAt(0) === '/') { // Remove the basepath from the fragment, but leave a /. fragment = fragment.substr(req.query.basepath.length - 1); var matched = _.any(Backbone.history.handlers, function(handler) { if (handler.route.test(fragment)) { handler.callback(fragment); return true; } }); if (matched) { Backbone.history.saveLocation(fragment); return false; } } return true; } }); package/test/fixture/views/Error.bones0000644000175000017500000000037611701101057017333 0ustar daviddavidview = Backbone.View.extend({ initialize: function(options) { _.bindAll(this, 'render'); this.render().trigger('attach'); }, render: function() { this.el = templates.Error(this.options); return this; } }); package/test/fixture/views/App.server.bones0000644000175000017500000000020411701101057020255 0ustar daviddavidviews['App'].augment({ render: function(parent) { this.el = templates.App(this.options); return this; } }); package/test/fixture/index.js0000755000175000017500000000026511701101057015522 0ustar daviddavidrequire('othermodule'); require('submodule'); // Explicit order require('./views/Error'); require('bones').load(__dirname); if (!module.parent) { require('bones').start(); } package/test/fixture/routers/0000755000175000017500000000000011715315015015560 5ustar daviddavidpackage/test/fixture/routers/Page.bones0000644000175000017500000000061011701101057017453 0ustar daviddavidrouter = Backbone.Router.extend({ routes: { '/': 'home', '/page/:id': 'page', '/page/special': 'pageSpecial' }, page: function(id) { this.res && this.res.send('page ' + id); }, pageSpecial: function() { this.res && this.res.send('special page'); }, home: function() { this.res && this.res.send('home'); } }); package/test/fixture/servers/0000755000175000017500000000000011715315015015546 5ustar daviddavidpackage/test/fixture/servers/Before.bones0000644000175000017500000000050711701101057017774 0ustar daviddavidserver = Bones.Server.extend({ initialize: function() { this.get('/page/baz', this.pageSpecial); this.get('/hostname', function(req, res, next) { res.send(req.headers.host); }); }, pageSpecial: function(req, res, next) { res.send('bones router special page'); } }); package/test/fixture/servers/After.bones0000644000175000017500000000034211701101057017630 0ustar daviddavidserver = Bones.Server.extend({ initialize: function() { this.get('/page/special', this.pageSpecial); }, pageSpecial: function(req, res, next) { res.send('this should never be returned'); } }); package/test/fixture/servers/Route.bones0000644000175000017500000000031011701101057017660 0ustar daviddavidservers['Route'].augment({ initialize: function(parent, app) { this.use(new servers['Before'](app)); parent.call(this, app); this.use(new servers['After'](app)); } }); package/test/fixture/package.json0000644000175000017500000000006211701101057016333 0ustar daviddavid{ "name": "fixture", "version": "1.0.0" } package/test/fixture/assets/0000755000175000017500000000000011715315015015357 5ustar daviddavidpackage/test/fixture/assets/foo0000644000175000017500000000001311701101057016051 0ustar daviddavidlorem ipsumpackage/test/fixture/models/0000755000175000017500000000000011715315015015340 5ustar daviddavidpackage/test/fixture/models/Houses.bones0000644000175000017500000000035711701101057017635 0ustar daviddavidmodel = Backbone.Collection.extend({ model: models.House, sync: function(method, model, options) { options.success([ {'foo': 'bar'}, {'foo': 'baz'}, {'foo': 'blah'} ]); } }); package/test/fixture/models/Failure.bones0000644000175000017500000000004111701101057017744 0ustar daviddavidmodel = Backbone.Model.extend(); package/test/fixture/models/Page.server.bones0000644000175000017500000000041111701101057020537 0ustar daviddavidmodels['Page'].augment({ sync: function(parent, method, model, options) { if (model.get('id') === 'asdf') { options.error(); } else { model.set({ method: method }); options.success(model); } } }); package/test/fixture/models/Failures.bones0000644000175000017500000000021511701101057020132 0ustar daviddavidmodel = Backbone.Collection.extend({ model: models.Fail, sync: function(method, model, options) { options.error(); } }); package/test/fixture/models/Page.bones0000644000175000017500000000004111701101057017231 0ustar daviddavidmodel = Backbone.Model.extend(); package/test/fixture/models/House.bones0000644000175000017500000000004111701101057017440 0ustar daviddavidmodel = Backbone.Model.extend(); package/test/fixture/commands/0000755000175000017500000000000011715315015015656 5ustar daviddavidpackage/test/fixture/commands/global.bones0000644000175000017500000000037411701101057020144 0ustar daviddavidBones.Command.options['adminParty'] = { 'title': 'adminParty', 'description': 'Celebrate with administrators!', 'default': false }; Bones.Command.options.host['default'].push('other'); Bones.Command.options.host['default'].push('*.third');package/test/fixture/commands/foo.bones0000644000175000017500000000073211701101057017465 0ustar daviddavidcommand = Bones.Command.extend(); command.description = 'demo command'; command.options['lorem'] = { 'description': 'Lorem ipsum dolor sit amet.', 'default': 'ipsum' }; command.options['dolor'] = { 'shortcut': 'd', 'default': function() { return __dirname; } } command.prototype.initialize = function(plugin, callback) { if (callback) callback('successfully started!'); else if (plugin) console.log(JSON.stringify(plugin.config)); }; package/test/assets.test.js0000644000175000017500000001413711701101057015205 0ustar daviddavidprocess.env.NODE_ENV = 'test'; var assert = require('assert'); var fs = require('fs'); var server = require('./fixture/start').servers.Core; exports['assets'] = function(beforeExit) { assert.response(server, { url: '/assets/fixture/does-not-exist', method: 'GET' }, { body: 'Not Found', status: 404 }); assert.response(server, { url: '/assets/fixture/foo', method: 'GET' }, { body: 'lorem ipsum', status: 200 }); }; exports['/assets/bones/core.js'] = function() { assert.response(server, { url: '/assets/bones/core.js', method: 'GET' }, { status: 200 }, function(res) { assert.ok(res.body.indexOf(fs.readFileSync(require.resolve('bones/client/backbone.js'))) >= 0); assert.ok(res.body.indexOf(fs.readFileSync(require.resolve('bones/client/utils.js'))) >= 0); assert.ok(res.body.indexOf(fs.readFileSync(require.resolve('bones/shared/backbone.js'))) >= 0); assert.ok(res.body.indexOf(fs.readFileSync(require.resolve('bones/shared/utils.js'))) >= 0); }); }; exports['/assets/bones/core.js'] = function() { assert.response(server, { url: '/assets/bones/vendor.js', method: 'GET' }, { status: 200 }, function(res) { assert.ok(res.body.indexOf(fs.readFileSync(require.resolve('bones/assets/jquery.js'))) >= 0); assert.ok(res.body.indexOf(fs.readFileSync(require.resolve('backbone'))) >= 0); assert.ok(res.body.indexOf(fs.readFileSync(require.resolve('underscore'))) >= 0); }); }; exports['/assets/bones/routers.js'] = function() { assert.response(server, { url: '/assets/bones/routers.js', method: 'GET' }, { status: 200 }, function(res) { assert.ok(res.body.indexOf(fs.readFileSync(require.resolve('bones/test/fixture/node_modules/submodule/routers/Foo'))) >= 0); assert.ok(res.body.indexOf(fs.readFileSync(require.resolve('bones/test/fixture/routers/Page'))) >= 0); // Correct order. assert.ok(res.body.indexOf('// ---- start test/fixture/node_modules/submodule/routers/Foo.bones ----') >= 0); assert.ok(res.body.indexOf('// ---- start test/fixture/node_modules/submodule/routers/Foo.bones ----') < res.body.indexOf('// ---- start test/fixture/routers/Page.bones ----')); assert.ok(res.body.indexOf('// ---- end test/fixture/node_modules/submodule/routers/Foo.bones ----') >= 0); assert.ok(res.body.indexOf('// ---- end test/fixture/node_modules/submodule/routers/Foo.bones ----') < res.body.indexOf('// ---- start test/fixture/routers/Page.bones ----')); }); }; exports['/assets/bones/models.js'] = function() { assert.response(server, { url: '/assets/bones/models.js', method: 'GET' }, { status: 200 }, function(res) { assert.ok(res.body.indexOf(fs.readFileSync(require.resolve('bones/test/fixture/models/Failure'))) >= 0); assert.ok(res.body.indexOf(fs.readFileSync(require.resolve('bones/test/fixture/models/Failures'))) >= 0); assert.ok(res.body.indexOf(fs.readFileSync(require.resolve('bones/test/fixture/models/House'))) >= 0); assert.ok(res.body.indexOf(fs.readFileSync(require.resolve('bones/test/fixture/models/Houses'))) >= 0); assert.ok(res.body.indexOf(fs.readFileSync(require.resolve('bones/test/fixture/models/Page'))) >= 0); // Doesn't include server files. assert.ok(res.body.indexOf(fs.readFileSync(require.resolve('bones/test/fixture/models/Page.server'))) < 0); // Correct order. assert.ok(res.body.indexOf('// ---- start test/fixture/models/Failure.bones ----') >= 0); assert.ok(res.body.indexOf('// ---- start test/fixture/models/Failure.bones ----') < res.body.indexOf('// ---- start test/fixture/models/Failures.bones ----')); assert.ok(res.body.indexOf('// ---- start test/fixture/models/Failures.bones ----') < res.body.indexOf('// ---- start test/fixture/models/House.bones ----')); assert.ok(res.body.indexOf('// ---- start test/fixture/models/House.bones ----') < res.body.indexOf('// ---- start test/fixture/models/Houses.bones ----')); assert.ok(res.body.indexOf('// ---- start test/fixture/models/Houses.bones ----') < res.body.indexOf('// ---- start test/fixture/models/Page.bones ----')); }); }; exports['/assets/bones/views.js'] = function() { assert.response(server, { url: '/assets/bones/views.js', method: 'GET' }, { status: 200 }, function(res) { assert.ok(res.body.indexOf(fs.readFileSync(require.resolve('bones/test/fixture/views/Error'))) >= 0); assert.ok(res.body.indexOf(fs.readFileSync(require.resolve('bones/test/fixture/views/App'))) >= 0); // Doesn't include server files. assert.ok(res.body.indexOf(fs.readFileSync(require.resolve('bones/test/fixture/views/App.server'))) < 0); // Correct order. assert.ok(res.body.indexOf('// ---- start test/fixture/views/Error.bones ----') >= 0); assert.ok(res.body.indexOf('// ---- start test/fixture/views/Error.bones ----') < res.body.indexOf('// ---- start test/fixture/views/App.bones ----')); }); }; exports['/assets/bones/templates.js'] = function() { assert.response(server, { url: '/assets/bones/templates.js', method: 'GET' }, { status: 200 }, function(res) { assert.ok(res.body.indexOf(require('bones/test/fixture/templates/Error._').toString()) >= 0); assert.ok(res.body.indexOf(require('bones/test/fixture/node_modules/othermodule/templates/Other._').toString()) >= 0); // Doesn't include server files. assert.ok(res.body.indexOf(require('bones/test/fixture/templates/ServerSide.server._').toString()) < 0); // Correct order. assert.ok(res.body.indexOf('// ---- start test/fixture/templates/Error._ ----') >= 0); assert.ok(res.body.indexOf('// ---- start test/fixture/templates/Error._ ----') > res.body.indexOf('// ---- start test/fixture/node_modules/othermodule/templates/Other._ ----')); }); }; package/test/collections.test.js0000644000175000017500000000207411701101057016216 0ustar daviddavidprocess.env.NODE_ENV = 'test'; var assert = require('assert'); var server = require('./fixture/start').servers.Core; exports['api endpoints'] = function() { assert.response(server, { url: '/api/House', method: 'GET' }, { body: '[{"foo":"bar"},{"foo":"baz"},{"foo":"blah"}]', status: 200 }); assert.response(server, { url: '/api/Failure', method: 'GET' }, { body: 'Internal Server Error', status: 500 }); assert.response(server, { url: '/api/Failure', method: 'GET', headers: { accept: 'application/json' } }, { body: '{"message":"Internal Server Error"}', status: 500 }); assert.response(server, { url: '/api/DoesNotExist', method: 'GET' }, { body: 'Not Found', status: 404 }); assert.response(server, { url: '/api/DoesNotExist', method: 'GET', headers: { accept: 'application/json' } }, { body: '{"message":"Not Found"}', status: 404 }); }; package/test/stringify.test.js0000644000175000017500000000302111701101057015707 0ustar daviddavidprocess.env.NODE_ENV = 'test'; var assert = require('assert'); var fs = require('fs'); require('./fixture') var fixture = require('bones').plugin; exports['stringify'] = function() { assert.equal(fixture.routers.Foo + '', ''); assert.equal(fixture.routers.Page + '', ''); var srv = new (require('bones').Server.extend({}, { title: 'Demo' })); assert.equal(new fixture.routers.Page({ server: srv }) + '', '[Router Page]'); assert.equal(fixture.models.Failure + '', ''); assert.equal(fixture.models.Failures + '', ''); assert.equal(fixture.models.House + '', ''); assert.equal(fixture.models.Houses + '', ''); assert.equal(new fixture.models.Houses + '', '[Collection Houses]'); assert.equal(fixture.models.Page + '', ''); assert.equal(new fixture.models.Page + '', '[Model Page]'); assert.equal(fixture.servers.Asset + '', ''); assert.equal(fixture.servers.Core + '', ''); assert.equal(fixture.servers.Middleware + '', ''); assert.equal(new fixture.servers.Asset({directories: []}) + '', '[Server Asset]'); assert.equal(fixture.commands.foo + '', ''); assert.equal(new fixture.commands.foo + '', '[Command foo]'); assert.equal(fixture.views.App + '', ''); assert.equal(fixture.views.Error + '', ''); assert.equal(new fixture.views.Error({ error: 'test' }) + '', '[View Error]'); }; package/test/urls.test.js0000644000175000017500000000152611701101057014666 0ustar daviddavidprocess.env.NODE_ENV = 'test'; var assert = require('assert'); var server = require('./fixture/start').servers.Core; exports['_escaped_fragment_ redirect'] = function() { assert.response(server, { url: '/page/special?_escaped_fragment_=/something/different', method: 'GET' }, { body: '

Moved Permanently. Redirecting to http://127.0.0.1:3000/something/different

', status: 301 }); } exports['hash request'] = function() { assert.response(server, { url: '/#!/map/devseed-hq', method: 'GET' }, { body: 'home', status: 200 }); } exports['query request'] = function() { assert.response(server, { url: '/?foo', method: 'GET' }, { body: 'home', status: 200 }); } package/test/demo.test.js0000644000175000017500000000354111701101057014624 0ustar daviddavidprocess.env.NODE_ENV = 'test'; var assert = require('assert'); var os = require('os'); var server = require('./fixture/start').servers.Core; exports['routes'] = function(beforeExit) { assert.response(server, { url: '/submodule-page', method: 'GET' }, { body: 'submodule page', status: 200 }); assert.response(server, { url: '/page/foo', method: 'GET' }, { body: 'page foo', status: 200 }); assert.response(server, { url: '/page/bar', method: 'GET' }, { body: 'page bar', status: 200 }); assert.response(server, { url: '/page/special', method: 'GET' }, { body: 'special page', status: 200 }); assert.response(server, { url: '/?_escaped_fragment_=/page/special', method: 'GET', headers: { host: os.hostname() } }, { body: '

Moved Permanently. Redirecting to http://' + os.hostname() + '/page/special

', status: 301 }, function(res) { assert.equal(res.headers.location, 'http://' + os.hostname() + '/page/special'); }); assert.response(server, { url: '/page/baz', method: 'GET' }, { body: 'bones router special page', status: 200 }); assert.response(server, { url: '/page/foo', method: 'POST' }, { body: 'Forbidden', status: 403 }); assert.response(server, { url: '/page/foo', method: 'POST', headers: { 'content-type': 'application/json', 'cookie': 'bones.token=1f4a1137268b8e384e50d0fb72c627c4' }, body: '{"bones.token":"1f4a1137268b8e384e50d0fb72c627c4"}' }, { body: 'Not Found', status: 404 }); }; package/test/cli.test.js0000644000175000017500000000772011701101057014452 0ustar daviddavidvar assert = require('assert'); var os = require('os'); var exec = require('child_process').exec; var hostnameDescription = 'Hostnames allowed for requests. Wildcards are allowed. (Default: ["127.0.0.1","localhost","' + os.hostname() + '","other","*.third"])'; exports['test --help'] = function() { exec('node test/fixture --help', function(err, stdout, stderr) { assert.equal(err.code, 1); assert.equal(stderr, 'Usage: node ./test/fixture [command] --help for a list of options.\n' + 'Available commands are:\n' + ' start: start application\n' + ' foo: demo command\n'); assert.equal(stdout, ''); }); }; exports['test start --help'] = function() { exec('node test/fixture start --help', function(err, stdout, stderr) { assert.equal(err.code, 1); assert.equal(stderr, 'Usage: node ./test/fixture [options...]\n' + 'Commands: start application\n' + ' start \n' + '\n' + 'Options:\n' + ' --host ' + hostnameDescription + '\n' + ' --adminParty Celebrate with administrators! (Default: false)\n' + ' --config=[path] Path to JSON configuration file.\n'); assert.equal(stdout, ''); }); }; exports['test foo --help'] = function() { exec('node test/fixture foo --help', function(err, stdout, stderr) { assert.equal(err.code, 1); assert.equal(stderr, 'Usage: node ./test/fixture [options...]\n' + 'Commands: demo command\n' + ' foo \n' + '\n' + 'Options:\n' + ' --lorem Lorem ipsum dolor sit amet. (Default: "ipsum")\n' + ' -d --dolor (Default: "' + __dirname + '/fixture/commands")\n' + ' --host ' + hostnameDescription + '\n' + ' --adminParty Celebrate with administrators! (Default: false)\n' + ' --config=[path] Path to JSON configuration file.\n'); assert.equal(stdout, ''); }); }; exports['test foo --config=test/fixture/config.json'] = function() { exec('node test/fixture foo --config=test/fixture/config.json', function(err, stdout, stderr) { assert.ok(!err); assert.deepEqual(JSON.parse(stdout), { adminParty: true, unknownOption: 42, lorem: "ipsum", dolor: __dirname + '/fixture/commands', host: [ '127.0.0.1', 'localhost', os.hostname(), 'other', '*.third' ] }); assert.equal(stderr, 'Note: Unknown option "unknownOption" in config file.\n'); }); }; exports['test foo --dolor=pain'] = function() { exec('node test/fixture foo --dolor=pain', function(err, stdout, stderr) { assert.ok(!err); assert.deepEqual(JSON.parse(stdout), { adminParty: false, lorem: "ipsum", dolor: 'pain', host: [ '127.0.0.1', 'localhost', os.hostname(), 'other', '*.third' ] }); assert.equal(stderr, ''); }); }; exports['test foo --config=test/fixture/config.json --show-config'] = function() { exec('node test/fixture foo --config=test/fixture/config.json --show-config', function(err, stdout, stderr) { assert.ok(!err); assert.equal(stdout, ''); assert.equal(stderr, 'Note: Unknown option "unknownOption" in config file.\n' + 'Using configuration:\n' + '{\n' + ' "adminParty": true,\n' + ' "unknownOption": 42,\n' + ' "lorem": "ipsum",\n' + ' "dolor": "' + __dirname + '/fixture/commands",\n' + ' "host": [\n' + ' "127.0.0.1",\n' + ' "localhost",\n' + ' "' + os.hostname() + '",\n' + ' "other",\n' + ' "*.third"\n' + ' ]\n' + '}\n'); }); }; package/test/pluralize.test.js0000644000175000017500000000442311701101057015707 0ustar daviddavidvar assert = require('assert'); var utils = require('bones').utils; module.exports = { 'test .pluralize()': function() { assert.equal('ids', utils.pluralize('id')); assert.equal('friends', utils.pluralize('friend')); assert.equal('buses', utils.pluralize('bus')); assert.equal('misses', utils.pluralize('miss')); assert.equal('wishes', utils.pluralize('wish')); assert.equal('watches', utils.pluralize('watch')); assert.equal('foxes', utils.pluralize('fox')); assert.equal('potatoes', utils.pluralize('potato')); assert.equal('parties', utils.pluralize('party')); assert.equal('quizzes', utils.pluralize('quiz')); assert.equal('things', utils.pluralize('thing')); assert.equal('men', utils.pluralize('man')); assert.equal('kisses', utils.pluralize('kiss')); assert.equal('dishes', utils.pluralize('dish')); assert.equal('judges', utils.pluralize('judge')); assert.equal('massages', utils.pluralize('massage')); assert.equal('monkeys', utils.pluralize('monkey')); assert.equal('keys', utils.pluralize('key')); assert.equal('dogs', utils.pluralize('dog')); assert.equal('boys', utils.pluralize('boy')); assert.equal('oxen', utils.pluralize('ox')); assert.equal('indices', utils.pluralize('index')); assert.equal('indices', utils.pluralize('indice')); }, 'test .singularize()': function() { assert.equal('paper', utils.singularize('papers')); assert.equal('ox', utils.singularize('oxen')); assert.equal('shoe', utils.singularize('shoes')); assert.equal('thing', utils.singularize('things')); assert.equal('thing', utils.singularize('thing')); assert.equal('man', utils.singularize('men')); assert.equal('parenthesi', utils.singularize('parenthesis')); assert.equal('bus', utils.singularize('bus')); assert.equal('miss', utils.singularize('miss')); assert.equal('kiss', utils.singularize('kiss')); assert.equal('man', utils.singularize('man')); assert.equal('monkey', utils.singularize('monkeys')); assert.equal('key', utils.singularize('keys')); assert.equal('boy', utils.singularize('boys')); assert.equal('movie', utils.singularize('movies')); assert.equal('series', utils.singularize('series')); assert.equal('index', utils.singularize('indices')); } };package/test/coverage.sh0000755000175000017500000000157111701101057014517 0ustar daviddavid#!/usr/bin/env sh cd .. rm -rf coverage/node_modules mkdir -p coverage/node_modules jscoverage --no-instrument=test \ --no-instrument=node_modules \ --no-instrument=assets \ --no-instrument=client \ --no-instrument=server/command.prefix.js \ --no-instrument=server/command.suffix.js \ --no-instrument=server/router.prefix.js \ --no-instrument=server/router.suffix.js \ --no-instrument=server/model.prefix.js \ --no-instrument=server/model.suffix.js \ --no-instrument=server/server.prefix.js \ --no-instrument=server/server.suffix.js \ --no-instrument=server/view.prefix.js \ --no-instrument=server/view.suffix.js \ --exclude=examples \ bones \ coverage/node_modules/bones cd coverage/node_modules/bones expresso package/test/models.test.js0000644000175000017500000000160011701101057015155 0ustar daviddavidprocess.env.NODE_ENV = 'test'; var assert = require('assert'); var server = require('./fixture/start').servers.Core; exports['api endpoints'] = function() { assert.response(server, { url: '/api/Page/foo', method: 'GET' }, { body: '{"id":"foo","method":"read"}', status: 200 }); assert.response(server, { url: '/api/page/foo', method: 'GET' }, { body: 'Not Found', status: 404 }); assert.response(server, { url: '/api/Page/foo', method: 'PUT', headers: { 'content-type': 'application/json', 'cookie': 'bones.token=1f4a1137268b8e384e50d0fb72c627c4' }, body: '{"bones.token":"1f4a1137268b8e384e50d0fb72c627c4","id":"foo","key":"value"}' }, { body: '{"id":"foo","key":"value","method":"update"}', status: 200 }); }; package/test/error.test.js0000644000175000017500000000754511701101057015041 0ustar daviddavidprocess.env.NODE_ENV = 'test'; var assert = require('assert'); var server = require('./fixture/start').servers.Core; exports['error 404'] = function(beforeExit) { assert.response(server, { url: '/does-not-exist', method: 'GET' }, { body: 'Not Found', status: 404 }); assert.response(server, { url: '/does-not-exist', method: 'GET', headers: { 'accept': 'application/json' } }, { body: '{"message":"Not Found"}', status: 404 }); assert.response(server, { url: '/does-not-exist', method: 'POST', headers: { 'accept': 'application/json' } }, { body: '{"message":"Forbidden"}', status: 403 }); assert.response(server, { url: '/does-not-exist', method: 'POST', headers: { 'content-type': 'application/json', 'cookie': 'bones.token=cfd72122969dfaefddd180725fadf53f;' }, body: JSON.stringify({ 'bones.token': 'cfd72122969dfaefddd180725fadf53f' }) }, { body: 'Not Found', status: 404 }); assert.response(server, { url: '/does-not-exist', method: 'POST', headers: { 'accept': 'application/json', 'content-type': 'application/json', 'cookie': 'bones.token=cfd72122969dfaefddd180725fadf53f;' }, body: JSON.stringify({ 'bones.token': 'cfd72122969dfaefddd180725fadf53f' }) }, { body: '{"message":"Not Found"}', status: 404 }); assert.response(server, { url: '/api/DoesNotExit/asdf', method: 'GET' }, { body: 'Not Found', status: 404 }); assert.response(server, { url: '/api/DoesNotExit/asdf', method: 'GET', headers: { 'accept': 'application/json' } }, { body: '{"message":"Not Found"}', status: 404 }); assert.response(server, { url: '/api/Page/asdf', method: 'GET' }, { body: 'Not Found', status: 404 }); assert.response(server, { url: '/api/Page/asdf', method: 'GET', headers: { 'accept': 'application/json' } }, { body: '{"message":"Not Found"}', status: 404 }); assert.response(server, { url: '/api/Page/asdf', method: 'PUT', headers: { 'content-type': 'application/json', 'cookie': 'bones.token=cfd72122969dfaefddd180725fadf53f;' }, body: JSON.stringify({ 'bones.token': 'cfd72122969dfaefddd180725fadf53f' }) }, { body: 'Conflict', status: 409 }); assert.response(server, { url: '/api/Page/asdf', method: 'PUT', headers: { 'accept': 'application/json', 'content-type': 'application/json', 'cookie': 'bones.token=cfd72122969dfaefddd180725fadf53f;' }, body: JSON.stringify({ 'bones.token': 'cfd72122969dfaefddd180725fadf53f' }) }, { body: '{"message":"Conflict"}', status: 409 }); assert.response(server, { url: '/api/Page/asdf', method: 'DELETE', headers: { 'content-type': 'application/json', 'cookie': 'bones.token=cfd72122969dfaefddd180725fadf53f;' }, body: JSON.stringify({ 'bones.token': 'cfd72122969dfaefddd180725fadf53f' }) }, { body: 'Conflict', status: 409 }); assert.response(server, { url: '/api/Page/asdf', method: 'DELETE', headers: { 'accept': 'application/json', 'content-type': 'application/json', 'cookie': 'bones.token=cfd72122969dfaefddd180725fadf53f;' }, body: JSON.stringify({ 'bones.token': 'cfd72122969dfaefddd180725fadf53f' }) }, { body: '{"message":"Conflict"}', status: 409 }); }; package/test/cli-coverage.test.js0000644000175000017500000001224511701101057016241 0ustar daviddavidvar assert = require('assert'); var os = require('os'); var exec = require('child_process').exec; var hostnameDescription = 'Hostnames allowed for requests. Wildcards are allowed. (Default: ["127.0.0.1","localhost","' + os.hostname() + '","other","*.third"])'; // Remove all hidden properties from an object. function makePlain(json) { return JSON.parse(JSON.stringify(json)); } require('./fixture'); exports['test --help'] = function(beforeExit) { var completed = false; require('optimist').argv = { _: [], '$0': 'node ./test/fixture', help: true }; require('bones').start(function(output) { completed = true; assert.deepEqual(output, [ [ 'Usage: %s for a list of options.', '\u001b[0;32mnode ./test/fixture [command] --help\u001b[0m' ], [ 'Available commands are:' ], [ ' start: start application' ], [ ' foo: demo command' ] ]); }); beforeExit(function() { assert.ok(completed); }); }; exports['test start --help'] = function(beforeExit) { var completed = false; require('optimist').argv = { _: ['start'], '$0': 'node ./test/fixture', help: true }; require('bones').start(function(output) { completed = true; assert.deepEqual(output, [ [ 'Usage: %s', '\u001b[0;32mnode ./test/fixture [options...]\u001b[0m' ], [ 'Commands: start application' ], [ ' %s %s', '\u001b[1;33mstart\u001b[0m', '\u001b[0;33m\u001b[0m' ], [ '\nOptions:' ], [ ' --host ' + hostnameDescription ], [ ' --adminParty Celebrate with administrators! (Default: false)' ], [ ' --config=[path] Path to JSON configuration file.' ] ]); }); beforeExit(function() { assert.ok(completed); }); }; exports['test foo --help'] = function(beforeExit) { var completed = false; require('optimist').argv = { _: ['foo'], '$0': 'node ./test/fixture', help: true }; require('bones').start(function(output) { completed = true; assert.deepEqual(output, [ [ 'Usage: %s', '\u001b[0;32mnode ./test/fixture [options...]\u001b[0m' ], [ 'Commands: demo command' ], [ ' %s %s', '\u001b[1;33mfoo\u001b[0m', '\u001b[0;33m\u001b[0m' ], [ '\nOptions:' ], [ ' --lorem Lorem ipsum dolor sit amet. (Default: "ipsum")' ], [ ' -d --dolor (Default: "' + __dirname + '/fixture/commands")' ], [ ' --host ' + hostnameDescription ], [ ' --adminParty Celebrate with administrators! (Default: false)' ], [ ' --config=[path] Path to JSON configuration file.' ] ]); }); beforeExit(function() { assert.ok(completed); }); }; exports['test foo'] = function(beforeExit) { var completed = false; require('optimist').argv = { _: ['foo'], '$0': 'node ./test/fixture' }; require('bones').start(function(output) { completed = true; assert.equal(output, 'successfully started!'); }); beforeExit(function() { assert.ok(completed); }); }; exports['test foo --config=test/fixture/config.json'] = function(beforeExit) { var completed = false; require('optimist').argv = { _: ['foo'], '$0': 'node ./test/fixture', config: 'test/fixture/config.json' }; require('bones').start(function(output) { completed = true; assert.deepEqual(makePlain(require('bones').plugin.config), { lorem: 'ipsum', dolor: __dirname + '/fixture/commands', host: [ '127.0.0.1', 'localhost', os.hostname(), 'other', '*.third' ], adminParty: true, unknownOption: 42 }); assert.equal(output, 'successfully started!'); }); beforeExit(function() { assert.ok(completed); }); }; exports['test foo --dolor=pain'] = function(beforeExit) { var completed = false; require('optimist').argv = { _: ['foo'], '$0': 'node ./test/fixture', dolor: 'pain' }; require('bones').plugin.config = {}; require('bones').start(function(output) { completed = true; assert.deepEqual(makePlain(require('bones').plugin.config), { lorem: 'ipsum', dolor: 'pain', host: [ '127.0.0.1', 'localhost', os.hostname(), 'other', '*.third' ], adminParty: false }); assert.equal(output, 'successfully started!'); }); beforeExit(function() { assert.ok(completed); }); }; exports['test foo --config=test/fixture/config.json --show-config'] = function(beforeExit) { var completed = false; require('optimist').argv = { _: ['foo'], '$0': 'node ./test/fixture', config: 'test/fixture/config.json', 'show-config': true }; require('bones').start(function(output) { completed = true; assert.equal(output, undefined); assert.deepEqual(makePlain(require('bones').plugin.config), { lorem: 'ipsum', dolor: 'pain', host: [ '127.0.0.1', 'localhost', os.hostname(), 'other', '*.third' ], adminParty: true, unknownOption: 42 }); }); beforeExit(function() { assert.ok(completed); }); }; package/shared/0000755000175000017500000000000011715315015012656 5ustar daviddavidpackage/shared/backbone.js0000644000175000017500000000215611701101057014756 0ustar daviddavidif (typeof process !== 'undefined' && process.versions && process.versions.node) { module.exports = Backbone = require('backbone'); _ = require('underscore'); $ = require('jquery'); } Backbone.Model.augment = Backbone.Collection.augment = Backbone.Router.augment = Backbone.View.augment = function(props) { var obj = this.prototype; for (var key in props) { if (typeof props[key] === 'function') { obj[key] = _.wrap(obj[key], props[key]); } else if (_.isArray(props[key])) { obj[key] = _.isArray(obj[key]) ? obj[key].concat(props[key]) : props[key]; } else if (typeof props[key] === 'object') { obj[key] = _.extend({}, obj[key], props[key]); } else { obj[key] = props[key]; } } return this; }; var extend = Backbone.Router.extend; Backbone.Model.extend = Backbone.Collection.extend = Backbone.Router.extend = Backbone.View.extend = function() { var child = extend.apply(this, arguments); delete child.title; return child; }; Backbone.View.prototype.html = function() { return $(this.el).html(); }; package/shared/utils.js0000644000175000017500000000764311701101057014360 0ustar daviddavidvar Bones = Bones || {}; Bones.utils = Bones.utils || {}; if (typeof process !== 'undefined' && process.versions && process.versions.node) { module.exports = Bones.utils; } Bones.utils.callback = function(callback) { return { success: function(model, response) { callback(null, response); }, error: function(model, err) { callback(err); } }; }; // Multifetch. Pass a hash of models and fetch each in parallel. Bones.utils.fetch = function(models, callback) { var remaining = _(models).size(); var error = null; _(models).each(function(model) { model.fetch({ success: function() { if (--remaining === 0) callback(error, models); }, error: function(m, err) { if (!error) error = err; model.error = err; if (--remaining === 0) callback(error, models); } }); }); }; // From https://github.com/visionmedia/lingo/blob/master/lib/languages/en.js Bones.utils.uncountable = [ 'advice', 'enegery', 'excretion', 'digestion', 'cooperation', 'health', 'justice', 'jeans', 'labour', 'machinery', 'equipment', 'information', 'pollution', 'sewage', 'paper', 'money', 'species', 'series', 'rain', 'rice', 'fish', 'sheep', 'moose', 'deer', 'bison', 'proceedings', 'shears', 'pincers', 'breeches', 'hijinks', 'clippers', 'chassis', 'innings', 'elk', 'rhinoceros', 'swine', 'you', 'news' ]; Bones.utils.singularize = function(text) { if (Bones.utils.uncountable.indexOf(text.toLowerCase()) >= 0) return text; for (var i = Bones.utils.singularize.rules.length - 1; i >= 0; i--) { var rule = Bones.utils.singularize.rules[i]; if (rule[0].test(text)) { return text.replace(rule[0], rule[1]); } } return text; }; // From https://github.com/visionmedia/lingo/blob/master/lib/languages/en.js Bones.utils.singularize.rules = [ [ (/s$/i), "" ], [ (/(bu|mis|kis)s$/i), "$1s" ], [ (/([ti])a$/i), "$1um" ], [ (/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i), "$1$2sis" ], [ (/(^analy)ses$/i), "$1sis" ], [ (/([^f])ves$/i), "$1fe" ], [ (/([lr])ves$/i), "$1f" ], [ (/([^aeiouy]|qu)ies$/i), "$1y" ], [ (/ies$/i), "ie" ], [ (/(x|ch|ss|sh)es$/i), "$1" ], [ (/([m|l])ice$/i), "$1ouse" ], [ (/(bus)es$/i), "$1" ], [ (/(o)es$/i), "$1" ], [ (/(shoe)s$/i), "$1" ], [ (/(cris|ax|test)es$/i), "$1is" ], [ (/(octop|vir)i$/i), "$1us" ], [ (/(alias|status)es$/i), "$1" ], [ (/^(ox)en/i), "$1" ], [ (/(vert|ind)ices$/i), "$1ex" ], [ (/(matr)ices$/i), "$1ix" ], [ (/(quiz)zes$/i), "$1" ], [ (/^(p)eople$/i), "$erson" ], [ (/^(m)en$/i), "$1an" ], [ (/^(child)ren$/i), "$1" ], [ (/^(move)s$/i), "$1" ], [ (/^(sex)$es/i), "$1" ] ]; Bones.utils.pluralize = function(text) { if (Bones.utils.uncountable.indexOf(text.toLowerCase()) >= 0) return text; for (var i = Bones.utils.pluralize.rules.length - 1; i >= 0; i--) { var rule = Bones.utils.pluralize.rules[i]; if (rule[0].test(text)) { return text.replace(rule[0], rule[1]); } } return text; }; // From https://github.com/visionmedia/lingo/blob/master/lib/languages/en.js Bones.utils.pluralize.rules = [ [ (/$/), "s" ], [ (/(s|ss|sh|ch|x|o)$/i), "$1es" ], [ (/y$/i), "ies" ], [ (/(o|e)y$/i), "$1ys" ], [ (/(octop|vir)us$/i), "$1i" ], [ (/(alias|status)$/i), "$1es" ], [ (/(bu)s$/i), "$1ses" ], [ (/([ti])um$/i), "$1a" ], [ (/sis$/i), "ses" ], [ (/(?:([^f])fe|([lr])f)$/i), "$1$2ves" ], [ (/([^aeiouy]|qu)y$/i), "$1ies" ], [ (/(matr|vert|ind)(?:ix|ex)$/i), "$1ices" ], [ (/([m|l])ouse$/i), "$1ice" ], [ (/^(ox)$/i), "$1en" ], [ (/(quiz)$/i), "$1zes" ], [ (/^(p)erson$/i), "$1eople" ], [ (/^(m)an$/i), "$1en" ], [ (/^(child)$/i), "$1ren" ], [ (/^(move)$/i), "$1s" ], [ (/^(sex)$/i), "$1es" ] ]; package/servers/0000755000175000017500000000000011715315015013101 5ustar daviddavidpackage/servers/Asset.bones0000644000175000017500000000105111701101057015177 0ustar daviddavidvar fs = require('fs'), path = require('path'), middleware = require('express'), env = process.env.NODE_ENV || 'development'; server = Bones.Server.extend({}); server.prototype.initialize = function(app) { app.directories.forEach(function(dir) { var pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')); this.use('/assets/' + pkg.name, middleware['static']( path.join(dir, 'assets'), { maxAge: env === 'production' ? 3600 * 1000 : 0 } // 1 hour )); }, this); }; package/servers/Core.bones0000644000175000017500000000161111701101057015012 0ustar daviddavidvar env = process.env.NODE_ENV || 'development'; var middleware = require('..').middleware; server = Bones.Server.extend({}); server.prototype.port = 3000; server.prototype.initialize = function(app) { this.port = app.config.port || this.port; // Middleware provides body decoding, CSRF validation et al. // Place other servers before this one ONLY when you intend to circumvent // these safeguards. this.use(new servers['Middleware'](app)); // Debugging server provides facilities for easier client side debugging. if (env === 'development') { this.use(new servers['Debug'](app)); } // The Route server provides default routes for /api/Model as well as // the /assets/bones routes. this.use(new servers['Route'](app)); // The Asset server provides each plugin's asset folder at /asset/pluginname. this.use(new servers['Asset'](app)); }; package/servers/Debug.bones0000644000175000017500000000067611701101057015162 0ustar daviddavidvar env = process.env.NODE_ENV || 'development'; server = Bones.Server.extend({ initialize: function(app) { _.bindAll(this, 'logError'); if (env === 'development') { this.get('/api/Error', this.logError); } }, logError: function(req, res, next) { console.error("Client Error: %s\n at %s:%d", req.query.message, req.query.url, req.query.line); res.send(); } }); package/servers/Middleware.bones0000644000175000017500000000052711701101057016204 0ustar daviddavidvar middleware = require('..').middleware; server = Bones.Server.extend({}); server.prototype.initialize = function(app) { this.use(middleware.sanitizeHost(app)); this.use(middleware.bodyParser()); this.use(middleware.cookieParser()); this.use(middleware.validateCSRFToken()); this.use(middleware.fragmentRedirect()); }; package/servers/Route.bones0000644000175000017500000001175111701101057015226 0ustar daviddavidvar env = process.env.NODE_ENV || 'development'; var headers = { 'Content-Type': 'application/json' }; server = Bones.Server.extend({}); var options = { type: '.js', wrapper: Bones.utils.wrapClientFile, sort: Bones.utils.sortByLoadOrder }; // TODO: This should be moved to the initialize method! server.prototype.assets = { vendor: new mirror([ require.resolve('bones/assets/jquery'), require.resolve('underscore'), require.resolve('backbone') ], { type: '.js' }), core: new mirror([ require.resolve('bones/shared/utils'), require.resolve('bones/client/utils'), require.resolve('bones/shared/backbone'), require.resolve('bones/client/backbone') ], { type: '.js' }), models: new mirror([], options), views: new mirror([], options), routers: new mirror([], options), templates: new mirror([], options) }; if (env === 'development') { server.prototype.assets.core.unshift(require.resolve('bones/assets/debug')); } // TODO: This should be moved to the initialize method! server.prototype.assets.all = new mirror([ server.prototype.assets.vendor, server.prototype.assets.core, server.prototype.assets.routers, server.prototype.assets.models, server.prototype.assets.views, server.prototype.assets.templates ], { type: '.js' }); // Stores models, views served by this server. // TODO: This should be moved to the initialize method! server.prototype.models = {}; server.prototype.views = {}; // Stores instances of routers registered with this server. // TODO: This should be moved to the initialize method! server.prototype.routers = {}; server.prototype.initialize = function(app) { this.registerComponents(app); this.initializeAssets(app); this.initializeModels(app); }; server.prototype.registerComponents = function(app) { var components = ['routers', 'models', 'views', 'templates']; components.forEach(function(kind) { for (var name in app[kind]) { app[kind][name].register(this); } }, this); }; server.prototype.initializeAssets = function(app) { this.get('/assets/bones/vendor.js', this.assets.vendor); this.get('/assets/bones/core.js', this.assets.core); this.get('/assets/bones/routers.js', this.assets.routers); this.get('/assets/bones/models.js', this.assets.models); this.get('/assets/bones/views.js', this.assets.views); this.get('/assets/bones/templates.js', this.assets.templates); this.get('/assets/bones/all.js', this.assets.all); }; server.prototype.initializeModels = function(app) { this.models = app.models; _.bindAll(this, 'loadModel', 'getModel', 'saveModel', 'delModel', 'loadCollection'); this.get('/api/:model/:id', this.loadModel, this.getModel); this.post('/api/:model', this.loadModel, this.saveModel); this.put('/api/:model/:id', this.loadModel, this.saveModel); this.del('/api/:model/:id', this.loadModel, this.delModel); this.get('/api/:collection', this.loadCollection.bind(this)); }; server.prototype.loadCollection = function(req, res, next) { var name = Bones.utils.pluralize(req.params.collection); if (name in this.models) { // Pass any querystring paramaters to the collection. req.collection = new this.models[name]([], req.query); req.collection.fetch({ success: function(collection, resp) { res.send(resp, headers); }, error: function(collection, err) { err = err instanceof Object ? err.toString() : err; next(new Error.HTTP(err, 500)); } }); } else { next(); } }; server.prototype.loadModel = function(req, res, next) { var name = req.params.model; if (name in this.models) { // Pass any querystring paramaters to the model. req.model = new this.models[name]({ id: req.params.id }, req.query); } next(); }; server.prototype.getModel = function(req, res, next) { if (!req.model) return next(); req.model.fetch({ success: function(model, resp) { res.send(resp, headers); }, error: function(model, err) { err = err instanceof Object ? err.toString() : err; next(new Error.HTTP(err, 404)); } }); }; server.prototype.saveModel = function(req, res, next) { if (!req.model) return next(); req.model.save(req.body, { success: function(model, resp) { res.send(resp, headers); }, error: function(model, err) { err = err instanceof Object ? err.toString() : err; next(new Error.HTTP(err, 409)); } }); }; server.prototype.delModel = function(req, res, next) { if (!req.model) return next(); req.model.destroy({ success: function(model, resp) { res.send({}, headers); }, error: function(model, err) { err = err instanceof Object ? err.toString() : err; next(new Error.HTTP(err, 409)); } }); }; package/examples/0000755000175000017500000000000011715315015013226 5ustar daviddavidpackage/examples/simple/0000755000175000017500000000000011715315015014517 5ustar daviddavidpackage/examples/simple/templates/0000755000175000017500000000000011715315015016515 5ustar daviddavidpackage/examples/simple/templates/Error._0000644000175000017500000000024711701101057017743 0ustar daviddavid

<%= status %>

<%= message %>
Return to homepage
package/examples/simple/templates/App._0000644000175000017500000000131011701101057017362 0ustar daviddavid <%= title %>
<%= main %>
package/examples/simple/templates/About._0000644000175000017500000000136511701101057017726 0ustar daviddavid

About this application

Bones itself tries to make very few assumptions about just how you'll use the API that backbone provides. It's missions is to simply make that API work on the server. This sample application is an example of how you'd use the Backbone API running on both the client and server together. It makes a bunch of assumptions for about how you want things to work, Bones itself doesn't make many assumptions at all.

In this application we're assuming a pretty traditional application model, but also that pages can render either on the client or the server, and that all client-side routing will be done via pushstate. If your browser doesn't have pushstate, you'll be fetching full pages from the server more often.

package/examples/simple/templates/Project._0000644000175000017500000000011411701101057020251 0ustar daviddavid

<%= name %>

<%= description %>

<%= readme %>
package/examples/simple/templates/Home._0000644000175000017500000000063011701101057017536 0ustar daviddavid

What is Bones?

Bones is a helper library that allows you to run the excellent Backbone.js library on your server. This example application is designed to demonstrate how bones works, and introduce you to the other excellent libraries that it uses.

Excellent Libraries

    <% _.each(projects, function(v) { %>
  • <%= v %>
  • <% }); %>
package/examples/simple/views/0000755000175000017500000000000011715315015015654 5ustar daviddavidpackage/examples/simple/views/Home.bones0000644000175000017500000000043611701101057017571 0ustar daviddavidview = views.Main.extend({ render: function() { var projects = []; this.collection.each(function(item) { projects.push(item.escape('id')); }); $(this.el).empty().append(templates.Home({projects:projects})); return this; } }); package/examples/simple/views/Main.bones0000644000175000017500000000204611701101057017564 0ustar daviddavidview = Backbone.View.extend({ id: 'main', initialize: function() { this.app = new views.App(); }, attach: function() { return this; }, // Scrolls to top or fragment // -------------------------- scrollTop: function() { var offset = $(window.location.hash).offset(); var top = offset ? offset.top : 0; // Scroll top FF, IE, Chrome safe if ($('body').scrollTop(0)) { $('body').scrollTop(top); return this; } if ($('html').scrollTop(0)) { $('html').scrollTop(top); } return this; }, activeLinks: function() { var activePath = window.location.pathname; $('a.active').removeClass('active'); $('a.exact').each(function(i, a) { activePath == $(a).attr('href') && $(a).addClass('active'); }); $('a:not(.exact)').each(function(i, a) { (activePath.indexOf($(a).attr('href')) == 0) && $(a).addClass('active'); }); return this; } }); package/examples/simple/views/Project.bones0000644000175000017500000000062111701101057020303 0ustar daviddavidview = views.Main.extend({ render: function() { // TODO Format links in the readme as link. // TODO avoid double encoding issues. $(this.el).empty().append(templates.Project({ name: this.model.escape('name'), description: this.model.escape('description'), readme: this.model.escape('readme') })); return this; } }); package/examples/simple/views/App.bones0000644000175000017500000000440411701101057017420 0ustar daviddavid// Starts routing on client // ------------------------ var start = _.once(function() { var bypass = true, _loadUrl = Backbone.History.prototype.loadUrl; Backbone.History.prototype.loadUrl = function(e) { if (bypass) { bypass = false; return; } _loadUrl.call(this, e); } Bones.start({pushState: true, root: ""}); }); // Sets up key tracking on client // ------------------------------ // TODO: should we use Bones.currentKeys? var keyTracking = _.once(function() { $(function() { // Global tracking of pressed keys. $(document).keydown(function(ev) { window.currentKeys = window.currentKeys || {}; window.currentKeys[ev.keyCode] = ev; }); $(document).keyup(function(ev) { window.currentKeys = window.currentKeys || {}; if (window.currentKeys[ev.keyCode]) { delete window.currentKeys[ev.keyCode]; } }); }); }); // Topmost view // ------------ view = Backbone.View.extend({ _ensureElement: function() { this.el = $('body'); }, initialize: function() { if (!Bones.server) { keyTracking(); } } }); // Registers event handler for all click events // -------------------------------------------- view.prototype.events = { 'click a': 'routeClick' }; // Routes a click event // -------------------- view.prototype.routeClick = function(ev) { if (_.size(window.currentKeys)) { return true; } // We only route client side if the browser supports push state. // The check here is borrowed from Backbone. if (window.history && window.history.pushState) { var href = $(ev.currentTarget).get(0).getAttribute('href', 2); if (href) return view.route($(ev.currentTarget).get(0).getAttribute('href', 2)); } return true; }; // Routes a path // ------------- view.route = function(path) { start(); if (path.charAt(0) === '/') { var matched = _.any(Backbone.history.handlers, function(handler) { if (handler.route.test(path)) { Backbone.history.navigate(path, true); return true; } }); return !matched; } return true; }; package/examples/simple/views/About.bones0000644000175000017500000000021111701101057017742 0ustar daviddavidview = views.Main.extend({ render: function() { $(this.el).empty().append(templates.About()); return this; } }); package/examples/simple/views/Error.bones0000644000175000017500000000126211701101057017770 0ustar daviddavidview = views.Main.extend({ initialize: function(options) { _.bindAll(this, 'render'); options = options || {}; // TODO: Push responseText parsing all the way up into Bones. if (options.responseText) { try { options = $.parseJSON(options.responseText); } catch (err) {} } this.options = {}; this.options.status = options.status || '404'; this.options.message = options.message || 'Not found'; views.Main.prototype.initialize.call(this, options); }, render: function() { $(this.el).empty().append(templates.Error(this.options)); return this; } }); package/examples/simple/views/App.server.bones0000644000175000017500000000035011701101057020721 0ustar daviddavid// On the client we override this, but on the server we need the default // behavior. // TODO explain better... views.App.prototype._ensureElement = function() { Backbone.View.prototype._ensureElement.apply(this, arguments); }; package/examples/simple/index.js0000755000175000017500000000041211701101057016156 0ustar daviddavid#!/usr/bin/env node var bones = require('bones') // Bones load views/models/etc.. in alphabetical order. To explicity load // certain ones early simply require them here. require('./views/Main'); bones.load(__dirname); if (!module.parent) { bones.start(); } package/examples/simple/routers/0000755000175000017500000000000011715315015016222 5ustar daviddavidpackage/examples/simple/routers/App.bones0000644000175000017500000000521011701101057017762 0ustar daviddavidrouter = Backbone.Router.extend({ routes: { '/': 'home', '/about': 'about', '/project/:project': 'project' }, // The `home` route... home: function() { var router = this, fetcher = this.fetcher(), projects = new models.Projects(); fetcher.push(projects); fetcher.fetch(function(err) { if (err) return router.error(err); router.send(views.Home, {collection: projects}); }); }, // The `About` route is completely static. It only needs to pass the proper // view (views.About) to be run. about: function() { this.send(views.About); }, // The 'project' route needs to load a particular project. project: function(project) { var router = this, fetcher = this.fetcher(), project = new models.Project({id: project}); fetcher.push(project); fetcher.fetch(function(err) { if (err) return router.error(err); router.send(views.Project, {model: project}); }); }, // Helper to assemble the page title. pageTitle: function(view) { var title = 'What is Bones?'; return (view.pageTitle ? view.pageTitle + ' | ' + title : title); }, // The send method is... send: function(view) { var options = (arguments.length > 1 ? arguments[1] : {}); var v = new view(options); // Populate the #page div with the main view. $('#page').empty().append(v.el); // TODO explain this! v.render().attach().activeLinks().scrollTop(); // Set the page title. document.title = this.pageTitle(v); }, // Generic error handling for our Router. error: function(error) { this.send(views.Error, _.isArray(error) ? error.shift() : error); }, // Helper to fetch a set of models/collections in parrellel. fetcher: function() { var models = []; return { push: function(item) { models.push(item) }, fetch: function(callback) { if (!models.length) return callback(); var errors = []; var _done = _.after(models.length, function() { callback(errors.length ? errors : null); }); _.each(models, function(model) { model.fetch({ success: _done, error: function(error) { errors.push(error); _done(); } }); }); } } } }); package/examples/simple/routers/App.server.bones0000644000175000017500000000340311701101057021271 0ustar daviddavid// Setup a closure variable whice records the time that the server was started. // This is used to make sure static resources (css, js) have a different url // when the code underneath them changes. var time = Date.now(); /** * On the server the send method is overridden to provide the path to actually * send rendered pages to the browser. In addition to a rendered page, we also * append aJSON versions of any models/collections to the page that were used. * to construct it. While duplicative this allow us to easily re-attach those * same views back onto the DOM client side. */ routers.App.prototype.send = function(view, options) { var options = arguments.length > 1 ? arguments[1] : {}; // Execute the main view. var main = new view(options); main.render(); // Provide all models with the data that well be used to prop them back up // on the browser. var o = '{el: $("#main"),'; _.each(options, function(v, k) { // Any options that is a model or collection will have it's title // declared. Use this to re-hydrate it. if (v.constructor.title != undefined) { o += JSON.stringify(k) + ': new models.'+ v.constructor.title +'('+ JSON.stringify(options[k]) + '),'; } else { o += JSON.stringify(k) + ':' + JSON.stringify(options[k]) +','; } }); o = o.replace(/,$/, '}'); // Finally send the page to the client. this.res.send(Bones.plugin.templates.App({ version: time, title: this.pageTitle(main), main: $(main.el).html(), startup: 'Bones.initialize(function(models, views, routers, templates) {'+ 'new views.' + main.constructor.title +'('+ o +').attach().activeLinks().scrollTop()'+ '});' })); }; package/examples/simple/package.json0000644000175000017500000000006011701101057016773 0ustar daviddavid{ "name": "simple", "version": "1.0.0" }package/examples/simple/assets/0000755000175000017500000000000011715315015016021 5ustar daviddavidpackage/examples/simple/assets/stylesheets/0000755000175000017500000000000011715315015020375 5ustar daviddavidpackage/examples/simple/assets/stylesheets/style.css0000644000175000017500000000067211701101057022246 0ustar daviddavid#header { height: 30px; background: #666; } #header ul { width: 800px; margin: 0px auto; } #header ul li { list-style: none; float: left; padding-right: 20px; } #header ul li a { color: #eee; font-size: 16px; line-height: 30px; } #page { width: 800px; margin: 0px auto; } pre { border: 1px solid #666; background: #ffb; color: #666; padding: 5px; overflow: auto; } package/examples/simple/models/0000755000175000017500000000000011715315015016002 5ustar daviddavidpackage/examples/simple/models/Project.bones0000644000175000017500000000020511701101057020427 0ustar daviddavidmodel = Backbone.Model.extend({ url: function() { return '/api/Project/' + encodeURIComponent(this.get('id')); } }); package/examples/simple/models/Projects.bones0000644000175000017500000000013411701101057020613 0ustar daviddavidmodel = Backbone.Collection.extend({ model: models.Project, url: '/api/Project' }); package/examples/simple/models/Project.server.bones0000644000175000017500000000475211701101057021747 0ustar daviddavidvar fs = require('fs'), path = require('path'); /** * The only method we need to override for on the server is `sync` which is * called to load the resource. By default Backbone models will attempt to * retrieve their state from a URI returned from their 'URL' method. Clearly * that won't work on the server, as we're trying to power that very same * URI. Here we do the hard work! */ models.Project.prototype.sync = function(method, model, options) { // Project data can only be read, so return an error it the client is // trying to do anything else. if (method != 'read') return options.error('Unsupported method'); var projectDir = path.dirname(require.resolve('bones')) + '/node_modules/' + model.id, resp = {id: model.id}; var fetchPackage = function(callback) { fs.readFile(projectDir +'/package.json', 'utf8', function(err, data) { if (err) return callback('Could not retrieve project information.'); data = JSON.parse(data); resp = _.extend(resp, data); callback(); }); }; var fetchReadme = function(callback) { // Setup the regex we'll use for detecting README files. var re = /^readme(|\.\w*)$/i; // Scan the project directory, and send a readme back to the client. fs.readdir(projectDir, function(err, files) { if (err) return callback('Project not found.'); for (var i = 0; i < files.length; i++) { var match = re.exec(files[i]); if (match) { fs.readFile(projectDir +'/'+ match.input, 'utf8', function(err, data) { if (err) return callback('Could not retrieve project information.'); // 80 chars folks... data = wrap(data); resp.readme = data; callback(); }); break; } } }); } fetchPackage(function(err) { if (err) return options.error(err); fetchReadme(function(err) { if (err) return options.error(err); options.success(resp); }); }); }; var wrap = function(str) { var lines = []; _(str.split(/\n/)).each(function(v) { if (v.length > 80) { var parts = v.match(/.{80}|.+$/g); lines.push(parts.join('\n')); } else { lines.push(v); } }); return lines.join('\n'); }; package/examples/simple/models/Projects.server.bones0000644000175000017500000000121211701101057022116 0ustar daviddavidvar fs = require('fs'), path = require('path'); models.Projects.prototype.sync = function(method, model, options) { // Scan the project directory, and send a readme back to the client. var file = path.dirname(require.resolve('bones')) + '/package.json'; fs.readFile(file, 'utf8', function(err, data) { if (err) return options.error('Failed to load projects.'); data = JSON.parse(data); var models = []; _(data.dependencies).each(function(ver, id) { models.push({ id: id, version: ver }); }); options.success(models); }); }; package/CHANGELOG.md0000644000175000017500000000057311701101057013220 0ustar daviddavid ## Bones 2.x.x - Upgraded to backbone.js 0.5.1. Most important changes affecting Bones applications: - `Backbone.Controller` is now `Backbone.Router` - `Backbone.sync(model, method, success, error)` changed to `Backbone.sync(model, method, options)` - Removed hashbang #! support. - Removed 'attach' mechanism, use *.server.bones style overrides instead #18 ## Bones 1.3.11 package/package.json0000644000175000017500000000112711715314760013705 0ustar daviddavid{ "name": "bones", "description": "Framework for using backbone.js on the client and server.", "version": "2.0.1", "author": { "name": "Development Seed", "url": "http://developmentseed.org/", "email": "info@developmentseed.org" }, "main": "./bones.js", "dependencies": { "underscore": "1.1.x", "express": "2.4.4", "backbone": "0.5.1", "jquery": "1.6.x", "optimist": "0.1.x", "mirror": "0.3.x" }, "scripts": { "test": "expresso -I ..", "coverage": "./test/coverage.sh" } } package/assets/0000755000175000017500000000000011732670206012717 5ustar daviddavidpackage/assets/debug.js0000644000175000017500000000051111701101057014325 0ustar daviddavidwindow.onerror = function(message, url, line) { // Don't attempt to handle non-standard errors (e.g. failed // HTTP request via jQuery). if (typeof message !== 'string') return; $.ajax('/api/Error', { data: { message: message, url: url, line: line } }); }; package/server/0000755000175000017500000000000011715315015012716 5ustar daviddavidpackage/server/model.suffix.js0000644000175000017500000000003011701101057015642 0ustar daviddavidmodule.exports = model; package/server/view.prefix.js0000644000175000017500000000034211701101057015513 0ustar daviddavidvar Bones = require('bones'); var $ = Bones.$, jQuery = $; var _ = Bones._; var Backbone = Bones.Backbone; var models = Bones.plugin.models; var views = Bones.plugin.views; var templates = Bones.plugin.templates; var view; package/server/command.prefix.js0000644000175000017500000000052311701101057016160 0ustar daviddavidvar Bones = require('bones'); var $ = Bones.$, jQuery = $; var _ = Bones._; var Backbone = Bones.Backbone; var models = Bones.plugin.models; var views = Bones.plugin.views; var routers = Bones.plugin.routers; var templates = Bones.plugin.templates; var servers = Bones.plugin.servers; var commands = Bones.plugin.commands; var command; package/server/middleware.js0000644000175000017500000000670611701101057015374 0ustar daviddavidvar env = process.env.NODE_ENV || 'development'; var host = require('os').hostname(); exports = module.exports = require('express'); exports['sanitizeHost'] = function sanitizeHost(app) { var hosts = app.config.host; if (!hosts) { hosts = app.config.host = []; } else if (!Array.isArray(hosts)) { hosts = app.config.host = [ hosts ]; } hosts.forEach(function(host, i) { if (typeof host === 'string') { hosts[i] = new RegExp('^' + hosts[i].replace(/\./g, '\\.').replace(/\*/g, '[a-z0-9_-]+') + '(:\\d+)?$', 'i'); // Make sure we get the original host names when stringifying the host name matcher. hosts[i].toJSON = function() { return host; }; } }); return function(req, res, next) { if (!req.headers.host) { return next(); } else if (!hosts.length) { // Check that the supplied hostname is harmless. If not, we'll // substitute it with the hostname reported by the machine. if (/^\w([\w-]*\w)?(\.\w([\w-]*\w)?)*(:\d+)?$/.test(req.headers.host)) { return next(); } } else { for (var i = 0; i < hosts.length; i++) { if (hosts[i].test(req.headers.host)) { return next(); } } } res.send(400); }; }; exports['validateCSRFToken'] = function validateCSRFToken() { return function(req, res, next) { if (req.method === 'GET' || req.method === 'HEAD') { next(); } else if (req.body && req.cookies['bones.token'] && req.body['bones.token'] === req.cookies['bones.token']) { delete req.body['bones.token']; next(); } else { next(new Error.HTTP(403)); } }; }; exports['fragmentRedirect'] = function fragmentRedirect() { return function(req, res, next) { // @see https://code.google.com/web/ajaxcrawling/docs/specification.html if (req.query._escaped_fragment_ === undefined) { next(); } else { // Force the first char of the path to be a slash to prevent // foreign redirects. var path = '/' + req.query._escaped_fragment_.substr(1); res.redirect(path, 301); } }; }; exports['showError'] = function showError() { return function showError(err, req, res, next) { if (!err.status) err.status = 500; // Output unexpected errors to console but hide them from public eyes. if (err.status >= 500) { if (process.env.NODE_ENV != 'test') console.error(err.stack || err.toString()); if (process.env.NODE_ENV == 'production') err.message = 'Internal Server Error'; } if ((req.headers.accept + '' || '').indexOf('json') >= 0) { res.writeHead(err.status, { 'Content-Type': 'application/json' }); if (env === 'development') { res.end(JSON.stringify(err)); } else { res.end(JSON.stringify({ message: err.message })); } } else { res.writeHead(err.status, { 'Content-Type': 'text/plain' }); if (env === 'development') { res.end(err.stack); } else { res.end(err.message); } } }; }; exports['notFound'] = function notFound() { return function notFound(req, res, next) { next(new Error.HTTP(404)); }; }; package/server/view.suffix.js0000644000175000017500000000002711701101057015522 0ustar daviddavidmodule.exports = view; package/server/router.js0000644000175000017500000000406711701101057014575 0ustar daviddavidvar Backbone = require('./backbone'); var _ = require('underscore'); module.exports = Backbone.Router; Backbone.Router.register = function(server) { // Add the router if it's not a server-only router. this.files.forEach(function(filename) { if (!(/\.server\.bones$/).test(filename) && server.assets && server.assets.routers.indexOf(filename) < 0) { server.assets.routers.push(filename); } }); // TODO push the order of the routers to the client return server.routers[this.title] = new this({ server: server }); }; Backbone.Router.toString = function() { return ''; }; Backbone.Router.prototype.initialize = function(options) { if (!options.server) { throw new Error("Can't initialize router without server."); } this.server = options.server; // Bind routes. if (this.routes) { var router = this, routes = this.routes; // Add the last routes first. _(this.routes).keys().reverse().forEach(function(route) { var name = routes[route]; router.route(route, name, router[name]); }); } }; Backbone.Router.prototype.toString = function() { return '[Router ' + this.constructor.title + ']'; }; Backbone.Router.prototype._bindRoutes = function() { // Noop. Routes are bound in initialize(); }; Backbone.Router.prototype.route = function(route, name, callback) { if (!_.isRegExp(route)) route = this._routeToRegExp(route); if (!_.isFunction(callback)) throw new Error("'" + name + "' is not a function in " + this); // Add route to express server. var router = this; this.server.get(route, function(req, res, next) { var fragment = (req.query && req.query['_escaped_fragment_']) || req.url.replace(/[#?].*$/, ''); var args = router._extractParameters(route, fragment); var context = Object.create(router, { req: { value: req }, res: { value: res } }); callback.apply(context, args); router.trigger.apply(router, ['route:' + name].concat(args)); }); }; package/server/plugin.js0000644000175000017500000002213511701101057014547 0ustar daviddavidvar path = require('path'); var fs = require('fs'); var util = require('util'); var assert = require('assert'); var Module = require('module'); var _ = require('underscore'); var Bones = require('bones'); var utils = Bones.utils; // Load wrappers var wrappers = {}; fs.readdirSync(__dirname).forEach(function(name) { var match = name.match(/^(.+)\.(prefix|suffix)\.js$/); if (match) { wrappers[match[1]] = wrappers[match[1]] || {}; wrappers[match[1]][match[2]] = fs.readFileSync(path.join(__dirname, name), 'utf8') .split('\n').join(''); } }); require.extensions['.bones'] = function(module, filename) { var content = fs.readFileSync(filename, 'utf8'); var kind = utils.singularize(path.basename(path.dirname(filename))); wrappers[kind] = wrappers[kind] || {}; wrappers[kind].prefix = wrappers[kind].prefix || ''; wrappers[kind].suffix = wrappers[kind].suffix || ''; content = wrappers[kind].prefix + ';' + content + '\n;' + wrappers[kind].suffix; module._compile(content, filename); if (module.exports) { Bones.plugin.add(module.exports, filename); } }; // Default template engine. require.extensions['._'] = function(module, filename) { var content = fs.readFileSync(filename, 'utf8'); var name = path.basename(filename).replace(/\..+$/, ''); try { module.exports = _.template(content); Bones.plugin.add(module.exports, filename); } catch (err) { var lines = err.message.split('\n'); lines.splice(1, 0, ' in template ' + filename); err.message = lines.join('\n'); throw err; } module.exports.register = function(app) { if (app.assets && !(/\.server\._$/.test(filename))) { app.assets.templates.push({ filename: filename, content: 'template = ' + module.exports + ';' }); } }; }; module.exports = Plugin; function Plugin() { this.directories = []; this.order = []; this.config = {}; this.routers = {}; this.models = {}; this.templates = {}; this.views = {}; this.servers = {}; this.commands = {}; }; function alphabetical(a, b) { return a.toLowerCase().localeCompare(b.toLowerCase()); } Plugin.prototype.load = function(dir) { if (this.directories.indexOf(dir) < 0) { this.directories.push(dir); this.require(dir, 'routers'); this.require(dir, 'models'); this.require(dir, 'templates'); this.require(dir, 'views'); this.require(dir, 'servers'); this.require(dir, 'commands'); } return this; }; Plugin.prototype.require = function(dir, kind) { dir = path.join(dir, kind); try { fs.readdirSync(dir).sort(alphabetical).forEach(function(name) { var file = path.join(dir, name); if (path.extname(file) in require.extensions && path.basename(file)[0] !== '.' && fs.statSync(file).isFile()) { require(file); } }); } catch(err) { if (err.code !== 'ENOENT') throw err; } return this; }; Plugin.prototype.add = function(component, filename) { if (!component.files) component.files = []; component.files.push(filename); if (!component.title) { component.title = path.basename(filename).replace(/\..+$/, ''); } var kind = path.basename(path.dirname(filename)); Bones.plugin[kind][component.title] = component; Bones.plugin.order.push(filename); }; Plugin.prototype.start = function(callback) { this.argv = require('optimist').argv; var command = this.argv._.length ? this.argv._[0] : 'start'; if (this.argv.help || !(command in this.commands)) { this.help(callback); } else { var command = this.commands[command]; if (this.loadConfig(command)) { return new command(this, callback); } else if (callback) { callback(); } } }; Plugin.prototype.loadConfig = function(command) { var config = this.config; command.options = command.options || {}; if (this.argv.config) { try { _.extend(config, JSON.parse(fs.readFileSync(this.argv.config, 'utf8'))); } catch(e) { console.error(utils.colorize('Invalid JSON config file: ' + this.argv.config, 'red')); process.exit(2); } } for (var key in command.options) { if (!(key in config)) { config[key] = command.options[key]['default']; } if (command.options[key]['shortcut'] in this.argv) { config[key] = this.argv[command.options[key]['shortcut']]; delete this.argv[command.options[key]['shortcut']]; } if (key in this.argv) { config[key] = this.argv[key]; delete this.argv[key]; } } _.defaults(config, this.argv); var showConfig = false; for (var key in config) { if (key === 'config' || key === '_' || key[0] === '$') { delete config[key]; } else if (key === 'show-config') { showConfig = config[key]; delete config[key]; } else { if (!(key in command.options)) { if (key in this.argv) { // It was specified on the command line. console.warn(utils.colorize('Note: Unknown option "' + key + '".', 'yellow')); } else { // It's from the config file. console.warn(utils.colorize('Note: Unknown option "' + key + '" in config file.', 'yellow')); } } else if (command.options[key].required && typeof config[key] === 'undefined') { console.warn(utils.colorize('Error: "' + key + '" is required.', 'red')); process.exit(2); } } } for (var key in config) { if (typeof config[key] === 'function') { config[key] = config[key](this, config); } } if (showConfig) { console.warn(utils.colorize('Using configuration:', 'green')); console.warn(JSON.stringify(config, false, 4)); return false; } else { return true; } }; Plugin.prototype.help = function(callback) { var output = []; var command = this.argv._.length ? this.argv._[0] : false; if (command !== false && command in this.commands) { // Display information about this command. var command = this.commands[command]; output.push(['Usage: %s', utils.colorize(this.argv['$0'] + ' [options...]', 'green')]); output.push(['Commands: ' + command.description]); var usage = command.usage || ['']; _(_.isArray(usage) ? usage : [usage]).each(function(item) { output.push([ ' %s %s', utils.colorize(command.title, 'yellow', 'bold'), utils.colorize(item, 'yellow') ]); }); output.push(['\nOptions:']); var options = []; for (var key in command.options) { var option = command.options[key]; var required = ''; if (option.required) { required = utils.colorize('(Required)', 'default', 'bold'); } else { var value = option['default']; if (typeof value === 'function') value = value(this); required = '(Default: ' + JSON.stringify(value) +')'; } options.push([ option.shortcut ? '-' + option.shortcut : '', '--' + (option.title || key), (option.description ? option.description + ' ' : '') + required ]); } options.push([ '', '--config=[path]', 'Path to JSON configuration file.' ]); table(options).forEach(function(line) { output.push(line); }); } else { // Display information about all available commands. output.push(['Usage: %s for a list of options.', utils.colorize(this.argv['$0'] + ' ' + (command || '[command]') + ' --help', 'green')]); output.push(['Available commands are:']); var commands = []; for (var key in this.commands) { commands.push([ this.commands[key].title + ':', this.commands[key].description || '']); } table(commands).forEach(function(line) { output.push(line); }); } if (callback) { callback(output); } else { output.forEach(function(params) { console.warn.apply(console, params); }); process.exit(1); } }; function table(fields) { var output = []; if (!fields[0]) return output; var lengths = fields[0].map(function(val, i) { return Math.max.apply(Math, fields.map(function(field) { return field[i].length; })); }); fields.forEach(function(field) { output.push([ ' ' + field.map(function(val, i) { if (i >= lengths.length - 1) return val; return val + Array(lengths[i] - val.length + 1).join(' '); }).join(' ') ]); }); return output; }; package/server/server.prefix.js0000644000175000017500000000055211701101057016052 0ustar daviddavidvar Bones = require('bones'); var $ = Bones.$, jQuery = $; var _ = Bones._; var Backbone = Bones.Backbone; var middleware = Bones.middleware; var mirror = Bones.mirror; var models = Bones.plugin.models; var views = Bones.plugin.views; var routers = Bones.plugin.routers; var templates = Bones.plugin.templates; var servers = Bones.plugin.servers; var server; package/server/server.js0000644000175000017500000000262411701101057014560 0ustar daviddavidvar Backbone = require('./backbone'); var _ = require('underscore'); var HTTPServer = require('express').HTTPServer; var middleware = require('..').middleware; module.exports = Server; function Server(plugin) { HTTPServer.call(this, []); this.plugin = plugin; this.initialize(plugin); this.conclude(plugin); }; Server.prototype.__proto__ = HTTPServer.prototype; _.extend(Server.prototype, Backbone.Events, { initialize : function(plugin) {}, conclude: function(plugin) { // Add catchall 404 middleware and error handler for root servers. if (this.port) { this.use(middleware.notFound()); this.error(middleware.showError()); // Remove redundant frontmost middleware from each server that will not // be a root server. See `express/lib/http.js`. } else { this.stack.shift(); } }, port: null, start: function(callback) { this.port && this.listen(this.port, callback); return this; }, toString: function() { if (this.port) { return '[Server ' + this.constructor.title + ':' + this.address().port + ']'; } else { return '[Server ' + this.constructor.title + ']'; } } }); Server.augment = Backbone.Router.augment; Server.extend = Backbone.Router.extend; Server.toString = function() { return ''; }; package/server/model.prefix.js0000644000175000017500000000023311701101057015640 0ustar daviddavidvar Bones = require('bones'); var $ = Bones.$, jQuery = $; var _ = Bones._; var Backbone = Bones.Backbone; var models = Bones.plugin.models; var model; package/server/backbone.js0000644000175000017500000000021711701101057015012 0ustar daviddavidvar Backbone = module.exports = require('../shared/backbone'); Backbone.sync = function() { throw new Error('No default sync method'); }; package/server/view.js0000644000175000017500000000204511701101057014221 0ustar daviddavidvar Backbone = require('./backbone'); var $ = require('jquery'); var _ = require('underscore'); module.exports = Backbone.View; Backbone.View.toString = function() { return ''; }; Backbone.View.prototype.toString = function() { return '[View ' + this.constructor.title + ']'; }; Backbone.View.register = function(server) { // Add the views if it's not a server-only view. this.files.forEach(function(filename) { if (!(/\.server\.bones$/).test(filename) && server.assets && server.assets.views.indexOf(filename) < 0) { server.assets.views.push(filename); } }); server.views[this.title] = this; }; Backbone.View.prototype.delegateEvents = function() {}; Backbone.View.prototype.template = function(template, data) { throw new Error('not supported'); }; Backbone.View.prototype.make = function(tagName, attributes, content) { var el = $('<' + tagName + '>'); if (attributes) $(el).attr(attributes); if (content) $(el).html(content); return el; }; package/server/command.js0000644000175000017500000000203611701101057014665 0ustar daviddavidvar Backbone = require('./backbone'); var _ = require('underscore'); module.exports = Command; function Command(plugin, callback) { this.bootstrap(plugin, function() { this.initialize(plugin, callback); }.bind(this)); }; Command.prototype.bootstrap = function(plugin, callback) { callback(); }; Command.prototype.initialize = function(plugin, callback) {}; Command.prototype.toString = function() { return '[Command ' + this.constructor.title + ']'; }; Command.augment = Backbone.Router.augment; Command.extend = Backbone.Router.extend; Command.extend = _.wrap(Command.extend, function(parent, props, staticProps) { var result = parent.call(this, props, staticProps); result.options = Object.create(this.options); return result; }); Command.toString = function() { return ''; }; Command.options = { 'host': { 'description': 'Hostnames allowed for requests. Wildcards are allowed.', 'default': [ '127.0.0.1', 'localhost', require('os').hostname() ] } }; package/server/server.suffix.js0000644000175000017500000000003111701101057016051 0ustar daviddavidmodule.exports = server; package/server/router.prefix.js0000644000175000017500000000034011701101057016057 0ustar daviddavidvar Bones = require('bones'); var $ = Bones.$, jQuery = $; var _ = Bones._; var Backbone = Bones.Backbone; var models = Bones.plugin.models; var views = Bones.plugin.views; var routers = Bones.plugin.routers; var router; package/server/command.suffix.js0000644000175000017500000000021511701101057016165 0ustar daviddavidif (command && !command.title) { command.title = require('path').basename(__filename).replace(/\..+$/, ''); } module.exports = command; package/server/collection.js0000644000175000017500000000120311701101057015375 0ustar daviddavidvar Backbone = require('./backbone'); var _ = require('underscore'); module.exports = Backbone.Collection; Backbone.Collection.toString = function() { return ''; }; Backbone.Collection.register = function(app) { // Add the collection if it's not a server-only collection. this.files.forEach(function(filename) { if (!(/\.server\.bones$/).test(filename) && app.assets) { app.assets.models.push(filename); } }); app.models[this.title] = this; }; Backbone.Collection.prototype.toString = function() { return '[Collection ' + this.constructor.title + ']'; }; package/server/model.js0000644000175000017500000000124111701101057014344 0ustar daviddavidvar Backbone = require('./backbone'); var _ = require('underscore'); module.exports = Backbone.Model; Backbone.Model.toString = function() { return ''; }; Backbone.Model.register = function(server) { // Add the model if it's not a server-only model. this.files.forEach(function(filename) { if (!(/\.server\.bones$/).test(filename) && server.assets && server.assets.models.indexOf(filename) < 0) { server.assets.models.push(filename); } }); server.models[this.title] = this; }; Backbone.Model.prototype.toString = function() { return '[Model ' + this.constructor.title + ']'; }; package/server/utils.js0000644000175000017500000000540011701101057014405 0ustar daviddavidvar utils = module.exports = require('../shared/utils'); var fs = require('fs'); var http = require('http'); var path = require('path'); var tty = require('tty'); var bones = require('..'); var colors = { 'default': 1, black: 30, red: 31, green: 32, yellow: 33, blue: 34, purple: 35, cyan: 36, white: 37 }; var styles = { regular: 0, bold: 1, underline: 4 }; if (tty.isatty(process.stdout.fd) && tty.isatty(process.stderr.fd)) { utils.colorize = function(text, color, style) { color = color || 'red'; style = style || 'regular'; return "\033[" + styles[style] + ";" + colors[color] + "m" + text + "\033[0m"; }; } else { utils.colorize = function(text) { return text; }; } // Load client-side wrappers var wrappers = {}; var wrapperDir = path.join(__dirname, '../client'); fs.readdirSync(wrapperDir).forEach(function(name) { var match = name.match(/^(.+)\.(prefix|suffix)\.js$/); if (match) { wrappers[match[1]] = wrappers[match[1]] || {}; wrappers[match[1]][match[2]] = fs.readFileSync(path.join(wrapperDir, name), 'utf8'); } }); // Remove common prefix between the working directory and filename so that we don't // leak information about the directory structure. utils.removePrefix = function(str) { var prefix = process.cwd().split('/'); str = str.split('/'); while (prefix.length && str[0] === prefix[0]) { str.shift(); prefix.shift(); } return str.join('/'); }; utils.wrapClientFile = function(content, filename) { var kind = utils.singularize(path.basename(path.dirname(filename))); var name = path.basename(filename).replace(/\..+$/, ''); var file = utils.removePrefix(filename); wrappers[kind] = wrappers[kind] || {}; wrappers[kind].prefix = wrappers[kind].prefix || ''; wrappers[kind].suffix = wrappers[kind].suffix || ''; return wrappers[kind].prefix.replace(/__NAME__/g, name).replace(/__FILE__/g, file) + "\n" + content + "\n" + wrappers[kind].suffix.replace(/__NAME__/g, name).replace(/__FILE__/g, file); }; utils.sortByLoadOrder = function(assets) { var reference = bones.plugin.order; assets.sort(function loadOrderSort(a, b) { a = reference.indexOf(a); b = reference.indexOf(b); return (a < 0 || b < 0) ? b - a : a - b; }); }; Error.HTTP = function(message, status) { if (typeof message === 'number') { status = message; message = null; } if (!message) { message = http.STATUS_CODES[status] || 'Unknown'; } Error.call(this, message); Error.captureStackTrace(this, arguments.callee); this.message = message; this.status = status; }; Error.HTTP.prototype.__proto__ = Error.prototype; package/server/router.suffix.js0000644000175000017500000000003111701101057016063 0ustar daviddavidmodule.exports = router; package/LICENSE0000644000175000017500000000271411701101057012413 0ustar daviddavidCopyright (c), Development Seed All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name "Development Seed" nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. package/commands/0000755000175000017500000000000011715315015013211 5ustar daviddavidpackage/commands/start.bones0000644000175000017500000000167711701101057015403 0ustar daviddavidcommand = Bones.Command.extend(); command.description = 'start application'; command.prototype.initialize = function(plugin, callback) { if (!Object.keys(plugin.servers).length) { console.warn(Bones.utils.colorize('No servers defined.', 'red')); return; } this.servers = {}; var queue = _(plugin.servers) .filter(function(server) { return !!server.prototype.port }) .length; _(plugin.servers) .chain() .filter(function(server) { return !!server.prototype.port }) .each(function(server) { this.servers[server.title] = new server(plugin); this.servers[server.title].start(function() { console.warn('Started %s.', Bones.utils.colorize(this, 'green')); this.emit('start'); queue--; queue === 0 && callback && callback(); }.bind(this.servers[server.title])); }.bind(this)); }; package/client/0000755000175000017500000000000011715315015012666 5ustar daviddavidpackage/client/model.suffix.js0000644000175000017500000000014111701101057015615 0ustar daviddavidif (model && !model.title) model.title = '__NAME__'; return model; }); // ---- end __FILE__ ----package/client/view.prefix.js0000644000175000017500000000014511701101057015464 0ustar daviddavid// ---- start __FILE__ ---- Bones.initialize('view', function(models, views, templates) { var view; package/client/view.suffix.js0000644000175000017500000000013511701101057015472 0ustar daviddavidif (view && !view.title) view.title = '__NAME__'; return view; }); // ---- end __FILE__ ----package/client/template.prefix.js0000644000175000017500000000013611701101057016325 0ustar daviddavid// ---- start __FILE__ ---- Bones.initialize('template', function(templates) { var template; package/client/model.prefix.js0000644000175000017500000000012511701101057015610 0ustar daviddavid// ---- start __FILE__ ---- Bones.initialize('model', function(models) { var model; package/client/template.suffix.js0000644000175000017500000000015511701101057016335 0ustar daviddavidif (template && !template.title) template.title = '__NAME__'; return template; }); // ---- end __FILE__ ----package/client/backbone.js0000644000175000017500000000503611715314654015004 0ustar daviddavidBackbone.Router.prototype.route = function(route, name, callback) { Backbone.history || (Backbone.history = new Backbone.History); if (!_.isRegExp(route)) route = this._routeToRegExp(route); Backbone.history.route(route, _.bind(function(fragment) { var args = this._extractParameters(route, fragment); callback.apply(this, args); this.trigger.apply(this, ['route:' + name].concat(args)); }, this)); }; // Generate CSRF protection token that is valid for the specified amount of // msec. The default is 1 second. Callers should provide the request path to // ensure the cookie is not pervasive across requests. Backbone.csrf = function(path, timeout) { var chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXZY0123456789'; var token = ''; while (token.length < 32) { token += chars.charAt(Math.floor(Math.random() * chars.length)); } // Remove hashes, query strings from cookie path. path = path || '/'; path = path.split('#')[0].split('?')[0]; var expires = new Date(+new Date + (timeout || 2000)).toGMTString(); document.cookie = 'bones.token=' + token + ';expires=' + expires + ';path=' + path + ';'; return token; }; // Client-side override of `Backbone.sync`. Adds CSRF double-cookie // confirmation protection to all PUT/POST/DELETE requests. The csrf middleware // must be used server-side to invalidate requests without this CSRF // protection. The original `Backbone.sync` cannot be reused because it does // not send a request body for DELETE requests. Backbone.sync = function(method, model, options) { function getUrl(object) { if (!(object && object.url)) throw new Error("A 'url' property or function must be specified"); return _.isFunction(object.url) ? object.url() : object.url; }; var type = { 'create': 'POST', 'update': 'PUT', 'delete': 'DELETE', 'read' : 'GET' }[method]; if (method !== 'read') { var modelJSON = model.toJSON ? model.toJSON() : model; modelJSON['bones.token'] = Backbone.csrf(getUrl(model)); modelJSON = JSON.stringify(modelJSON); } // Default JSON-request options. var params = { url: getUrl(model), type: type, contentType: 'application/json', data: (modelJSON || null), dataType: 'json', processData: false, success: options.success, error: options.error }; // Make the request. return $.ajax(params); }; package/client/router.prefix.js0000644000175000017500000000014711701101057016034 0ustar daviddavid// ---- start __FILE__ ---- Bones.initialize('router', function(models, views, routers) { var router; package/client/utils.js0000644000175000017500000000256511701101057014366 0ustar daviddavid$(function() { // Fix for [IE8 AJAX payload caching][1]. // [1]: http://stackoverflow.com/questions/1013637/unexpected-caching-of-ajax-results-in-ie8 $.browser.msie && $.ajaxSetup({ cache: false }); }); (function() { // Closure for models/views/routers. var models = {}, views = {}, routers = {}, templates = {}; Bones.server = false; Bones.initialize = function(kind, callback) { if (kind === 'model') { var model = callback(models); if (model) models[model.title] = model; } else if (kind === 'view') { var view = callback(models, views, templates); if (view) views[view.title] = view; } else if (kind === 'router') { var router = callback(models, views, routers); if (router) routers[router.title] = router; } else if (kind === 'template') { var template = callback(templates); if (template) templates[template.title] = template; } else if (_.isFunction(kind)) { kind(models, views, routers, templates); } }; Bones.start = function(options) { for (var k in routers) { new routers[k]; } Backbone.history.start(options); }; Bones.DEBUG = { models: models, views: views, routers: routers, templates: templates }; })(); package/client/router.suffix.js0000644000175000017500000000014511701101057016041 0ustar daviddavidif (router && !router.title) router.title = '__NAME__'; return router; }); // ---- end __FILE__ ----package/README.md0000644000175000017500000000205711701101057012665 0ustar daviddavid# Bones Bones provides conventions for [Backbone](http://documentcloud.github.com/backbone/) applications. It allows most code to be shared on the server and the client. Bones exposes your Backbone routes as regular paths on the server so they can be accessed by non-JavaScript agents, while capable clients can enjoy the normal client-side Backbone experience. ## Getting started The [wiki](https://github.com/developmentseed/bones/wiki) contains more information on [**Getting Started**](https://github.com/developmentseed/bones/wiki/Getting-Started) and on the [concepts](https://github.com/developmentseed/bones/wiki/Plugin-Architecture) in Bones. ## Example Application * The [simple app example](https://github.com/developmentseed/bones/tree/master/examples/simple) is a good quickstart to making a bones application. ## Testing To run the test suite, type `npm test`. **Note**: bones has to be in a folder named `node_modules` for tests to work correctly. ## License Bones is [BSD licensed](https://github.com/developmentseed/bones/raw/master/LICENSE).