diff --git a/README.md b/README.md index a9f85e3..7e58574 100644 --- a/README.md +++ b/README.md @@ -21,21 +21,21 @@ You can run HomeBridge easily from the command line: > homebridge server ``` -It will look for any locally-installed providers and load them up automatically. +It will look for any locally-installed plugins and load them up automatically. -## Providers +## Plugins -HomeBridge does nothing by itself; in order to expose your home to HomeKit, you'll need to install one or more HomeBridge "Providers." A Provider is an npm module that connects with HomeBridge and registers accessories for devices in your home. +HomeBridge does nothing by itself; in order to expose your home to HomeKit, you'll need to install one or more HomeBridge "Plugins." A Plugin is an npm module that connects with HomeBridge and registers accessories for devices in your home. -Providers must be published to npm and tagged with `homebridge-provider`. The package name must contain the prefix `homebridge-`. For example, a valid package might be `homebridge-lockitron`. +Plugins must be published to npm and tagged with `homebridge-plugin`. The package name must contain the prefix `homebridge-`. For example, a valid package might be `homebridge-lockitron`. -Providers are automatically discovered and loaded from your home directory inside the `.homebridge` folder. For instance, the Lockitron provider would be placed here: +Plugins are automatically discovered and loaded from your home directory inside the `.homebridge` folder. For instance, the Lockitron plugin would be placed here: ```sh -~/.homebridge/providers/homebridge-lockitron +~/.homebridge/plugins/homebridge-lockitron ``` -Right now you must copy providers manually (or symlink them from another location). The HomeBridge server will load and validate your Provider on startup. You can find an example Provider in [example-providers/homebridge-lockitron](). +Right now you must copy plugins manually (or symlink them from another location). The HomeBridge server will load and validate your Plugin on startup. You can find an example Plugin in [example-plugins/homebridge-lockitron](). ## Running from Source diff --git a/bin/homebridge b/bin/homebridge index 4ac4444..d87633c 100755 --- a/bin/homebridge +++ b/bin/homebridge @@ -8,16 +8,10 @@ process.title = 'homebridge'; -// Use babel to transparently enable ES6 features -require("babel/register"); - // Find the HomeBridge lib var path = require('path'); var fs = require('fs'); var lib = path.join(path.dirname(fs.realpathSync(__filename)), '../lib'); -// Export HomeBridge as a global for providers -global.homebridge = require(lib + '/homebridge'); - -// Run the HomeBridge CLI -require(lib + '/cli')(); +// Run HomeBridge +require(lib + '/cli')(); \ No newline at end of file diff --git a/example-plugins/homebridge-lockitron/index.js b/example-plugins/homebridge-lockitron/index.js new file mode 100644 index 0000000..ad3e5d9 --- /dev/null +++ b/example-plugins/homebridge-lockitron/index.js @@ -0,0 +1,41 @@ +var request = require('basic-request'); +var hap = require('HAP-NodeJS'); + +module.exports = { + providers: [LockitronProvider] +} + +function LockitronProvider(log, config) { + this._log = log; + this._config = config; +} + +LockitronProvider.title = "Lockitron"; + +LockitronProvider.config = { + accessToken: { + type: 'string', + description: "You can find your personal Access Token at: https://api.lockitron.com", + required: true + }, + lockID: { + type: 'string', + description: "If specified, only the lock with this ID will be exposed as an accessory.", + } +} + +LockitronProvider.prototype.validateConfig = function(callback) { + + // validate the accessToken + var accessToken = this._config.accessToken; + + // prove that we got a value + this._log.info('Access Token: ' + accessToken); + + // all is well. + callback(); +} + +LockitronProvider.prototype.getAccessories = function(callback) { + +} diff --git a/example-providers/homebridge-lockitron/package.json b/example-plugins/homebridge-lockitron/package.json similarity index 61% rename from example-providers/homebridge-lockitron/package.json rename to example-plugins/homebridge-lockitron/package.json index 2e549f7..4188cda 100644 --- a/example-providers/homebridge-lockitron/package.json +++ b/example-plugins/homebridge-lockitron/package.json @@ -1,15 +1,15 @@ { - "name": "homebridge-lockitron", + "name": "plugin-lockitron", "version": "0.0.0", "description": "Lockitron Support for HomeBridge", "license": "ISC", - "engines": { + "keywords": [ + "homebridge-plugin" + ], + "peerDepdendencies": { "homebridge": ">=0.0.0" }, - "keywords": [ - "homebridge-provider" - ], "dependencies": { - "request": "^2.58.0" + "basic-request": "^0.1.1" } } diff --git a/example-providers/homebridge-lockitron/index.js b/example-providers/homebridge-lockitron/index.js deleted file mode 100644 index 266dd24..0000000 --- a/example-providers/homebridge-lockitron/index.js +++ /dev/null @@ -1,30 +0,0 @@ -import request from 'request'; - -// Create a logger for our provider -let log = homebridge.logger('homebridge-lockitron'); - -// Demonstrate that we were loaded - run homebridge with the "-D" option to see this message -log.debug("Lockitron provider loaded!"); - -module.exports = { - - config: { - accessToken: { - type: 'string', - description: "You can find your personal Access Token at: https://api.lockitron.com", - required: true - } - }, - - validateConfig: function(callback) { - - // validate the accessToken - let accessToken = homebridge.config.get('homebridge-lockitron.accessToken'); - - // prove that we got a value - log.info(`Access Token: ${accessToken}`); - - // all is well. - callback(); - } -} diff --git a/lib/Plugin.js b/lib/Plugin.js new file mode 100644 index 0000000..966b5b2 --- /dev/null +++ b/lib/Plugin.js @@ -0,0 +1,184 @@ +var path = require('path'); +var fs = require('fs'); +var semver = require('semver'); +var User = require('./user').User; +var version = require('./version'); + +'use strict'; + +module.exports = { + Plugin: Plugin +} + +/** + * Homebridge Plugin. + * + * Allows for discovering and loading installed Homebridge plugins. + */ + +function Plugin(pluginPath) { + this.pluginPath = pluginPath; // like "/usr/local/lib/node_modules/plugin-lockitron" + this.providers = []; // the provider constructors pulled from the loaded plugin module +} + +Plugin.prototype.name = function() { + return path.basename(this.pluginPath); +} + +Plugin.prototype.load = function(options) { + options = options || {}; + + // does this plugin exist at all? + if (!fs.existsSync(this.pluginPath)) { + throw new Error("Plugin " + this.pluginPath + " was not found. Make sure the module '" + this.pluginPath + "' is installed."); + } + + // attempt to load package.json + var pjson = Plugin.loadPackageJSON(this.pluginPath); + + // pluck out the HomeBridge version requirement + if (!pjson.peerDepdendencies || !pjson.peerDepdendencies.homebridge) { + throw new Error("Plugin " + this.pluginPath + " does not contain the 'homebridge' package in 'peerDepdendencies'."); + } + + var versionRequired = pjson.peerDepdendencies.homebridge; + + // make sure the version is satisfied by the currently running version of HomeBridge + if (!semver.satisfies(version, versionRequired)) { + throw new Error("Plugin " + this.pluginPath + " requires a HomeBridge version of " + versionRequired + " which does not satisfy the current HomeBridge version of " + version + ". You may need to upgrade your installation of HomeBridge."); + } + + // figure out the main module - index.js unless otherwise specified + var main = pjson.main || "./index.js"; + + var mainPath = path.join(this.pluginPath, main); + + // try to require() it + var pluginModule = require(mainPath); + + // pull out the configuration data, if any + // var pluginConfig = loadedPlugin.config; + // + // // verify that all required values are present + // if (pluginConfig && !options.skipConfigCheck) { + // for (var key in pluginConfig) { + // + // var configParams = pluginConfig[key]; + // + // if (configParams.required && !User.config().get(this.name() + '.' + key)) { + // throw new Error("Plugin " + this.pluginPath + " requires the config value " + key + " to be set."); + // } + // } + // } + + this.providers = pluginModule.providers; +} + +Plugin.loadPackageJSON = function(pluginPath) { + // check for a package.json + var pjsonPath = path.join(pluginPath, "package.json"); + var pjson = null; + + if (!fs.existsSync(pjsonPath)) { + throw new Error("Plugin " + pluginPath + " does not contain a package.json."); + } + + try { + // attempt to parse package.json + pjson = JSON.parse(fs.readFileSync(pjsonPath)); + } + catch (err) { + throw new Error("Plugin " + pluginPath + " contains an invalid package.json. Error: " + err); + } + + // verify that it's tagged with the correct keyword + if (!pjson.keywords || pjson.keywords.indexOf("homebridge-plugin") == -1) { + throw new Error("Plugin " + pluginPath + " package.json does not contain the keyword 'homebridge-plugin'."); + } + + return pjson; +} + +Plugin.getDefaultPaths = function() { + var win32 = process.platform === 'win32'; + var paths = []; + + // add the paths used by require() + paths = paths.concat(require.main.paths); + + // THIS SECTION FROM: https://github.com/yeoman/environment/blob/master/lib/resolver.js + + // Adding global npm directories + // We tried using npm to get the global modules path, but it haven't work out + // because of bugs in the parseable implementation of `ls` command and mostly + // performance issues. So, we go with our best bet for now. + if (process.env.NODE_PATH) { + paths = process.env.NODE_PATH.split(path.delimiter) + .filter(function(p) { return !!p; }) // trim out empty values + .concat(paths); + } else { + // Default paths for each system + if (win32) { + paths.push(path.join(process.env.APPDATA, 'npm/node_modules')); + } else { + paths.push('/usr/local/lib/node_modules'); + paths.push('/usr/lib/node_modules'); + } + } + + return paths; +} + +// All search paths we will use to discover installed plugins +Plugin.paths = Plugin.getDefaultPaths(); + +Plugin.addPluginPath = function(pluginPath) { + Plugin.paths.unshift(path.resolve(process.cwd(), pluginPath)); +} + +// Gets all plugins installed on the local system +Plugin.installed = function() { + + var plugins = []; + var pluginsByName = {}; // don't add duplicate plugins + + // search for plugins among all known paths, in order + for (var index in Plugin.paths) { + var requirePath = Plugin.paths[index]; + + // just because this path is in require.main.paths doesn't mean it necessarily exists! + if (!fs.existsSync(requirePath)) + continue; + + var names = fs.readdirSync(requirePath); + + // read through each directory in this node_modules folder + for (var index2 in names) { + var name = names[index2]; + + // reconstruct full path + var pluginPath = path.join(requirePath, name); + + // we only care about directories + if (!fs.statSync(pluginPath).isDirectory()) continue; + + // does this module contain a package.json? + try { + // throws an Error if this isn't a homebridge plugin + Plugin.loadPackageJSON(pluginPath); + } + catch (err) { + // swallow error and skip this module + continue; + } + + // add it to the return list + if (!pluginsByName[name]) { + pluginsByName[name] = true; + plugins.push(new Plugin(pluginPath)); + } + } + } + + return plugins; +} diff --git a/lib/cli.js b/lib/cli.js index ac486c9..426439b 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -1,151 +1,17 @@ -import program from 'commander'; -import prompt from 'prompt'; -import { HOMEBRIDGE_VERSION } from './homebridge'; -import { User } from './user'; -import { Server } from './server'; -import { Provider } from './provider'; -import { log, setDebugEnabled } from './logger'; -import { camelCaseToRegularForm } from './util'; +var program = require('commander'); +var version = require('./version'); +var Server = require('./server').Server; +var Plugin = require('./Plugin').Plugin; -export default function() { +'use strict'; - // Global options (none currently) and version printout - program - .version(HOMEBRIDGE_VERSION) - .option('-D, --debug', 'turn on debug level logging', () => setDebugEnabled(true)); - - // Run the HomeBridge server - program - .command('server') - .description('Run the HomeBridge server.') - .action(runServer); - - program - .command('providers') - .description('List installed providers.') - .action(listInstalledProviders); +module.exports = function() { program - .command('setup [provider]') - .description('Sets up a new HomeBridge provider or re-configures an existing one.') - .action((providerName, options) => new CliProviderSetup(providerName, options).setup()); - - // Parse options and execute HomeBridge - program.parse(process.argv); - - // Display help by default if no commands or options given - if (!process.argv.slice(2).length) { - program.help(); - } -} - -function runServer(options) { - - // get all installed providers - let providers = Provider.installed(); // array of Provider - - // load and validate providers - check for valid package.json, etc. - try { - this.providerModules = providers.map((provider) => provider.load()); - } - catch (err) { - log.error(err.message); - process.exit(1); - } -} - -function listInstalledProviders(options) { - Provider.installed().forEach((provider) => log.info(provider.name)); -} - -// Encapsulates configuring a provider via the command line. -class CliProviderSetup { - constructor(providerName, options) { - - // if you didn't specify a provider, print help - if (!providerName) { - log.error("You must specify the name of the provider to setup. Type 'homebridge providers' to list the providers currently installed."); - process.exit(1); - } - - this.providerName = providerName; - this.options = options; // command-line options (currently none) - } - - setup() { - try { - let provider = new Provider(this.providerName); - this.providerModule = provider.load({skipConfigCheck: true}); - - if (this.providerModule.config) { - - prompt.message = ""; - prompt.delimiter = ""; - prompt.start(); - prompt.get(this.buildPromptSchema(), (err, result) => { - - // add a linebreak after our last prompt - console.log(''); - - // apply configuration values entered by the user - for (let key in result) { - let value = result[key]; - - User.config.set(`${this.providerName}.${key}`, value); - } - - this.validateProviderConfig(); - }); - } - else { - this.validateProviderConfig(); - } - } - catch (err) { - log.error(`Setup failed: ${err.message}`); - } - } - - validateProviderConfig() { - - let currentlyInsideValidateConfigCall = false; - - // we allow for the provider's validateConfig to call our callback immediately/synchronously - // from inside validateConfig() itself. - let callback = (err) => { - - if (!err) { - log.info(`Setup complete.`); - } - else { - log.error(`Setup failed: ${err.message || err}`); - } - } - - currentlyInsideValidateConfigCall = true; - this.providerModule.validateConfig(callback); - currentlyInsideValidateConfigCall = false; - } - - // builds a "schema" obejct for the prompt lib based on the provider's config spec - buildPromptSchema() { - let properties = {}; - - for (let key in this.providerModule.config) { - let spec = this.providerModule.config[key]; - - // do we have a value for this config key currently? - let currentValue = User.config.get(`${this.providerName}.${key}`); - - // copy over config spec with some modifications - properties[key] = { - description: `\n${spec.description}\n${camelCaseToRegularForm(key).white}:`, - type: spec.type, - required: spec.required, - default: currentValue - } - } - - return { properties }; - } + .version(version) + .option('-P, --plugin-path [path]', 'look for plugins installed at [path] as well as node_modules', function(p) { Plugin.addPluginPath(p); }) + .option('-D, --debug', 'turn on debug level logging', function() { logger.setDebugEnabled(true) }) + .parse(process.argv); + + new Server().run(); } diff --git a/lib/config.js b/lib/config.js index a7c13fa..962d1fc 100644 --- a/lib/config.js +++ b/lib/config.js @@ -1,40 +1,49 @@ -import fs from 'fs'; +var fs = require('fs'); -export class Config { - - constructor(path, data = {}) { - this.path = path; - this.data = data; - } - - get(key) { - this.validateKey(key); - let [providerName, keyName] = key.split("."); - return this.data[providerName] && this.data[providerName][keyName]; - } - - set(key, value) { - this.validateKey(key); - let [providerName, keyName] = key.split("."); - this.data[providerName] = this.data[providerName] || {}; - this.data[providerName][keyName] = value; - this.save(); - } - - validateKey(key) { - if (key.split(".").length != 2) - throw new Error(`The config key '${key}' is invalid. Configuration keys must be in the form [my-provider].[myKey]`); - } - - static load(configPath) { - // load up the previous config if found - if (fs.existsSync(configPath)) - return new Config(configPath, JSON.parse(fs.readFileSync(configPath))); - else - return new Config(configPath); // empty initial config - } - - save() { - fs.writeFileSync(this.path, JSON.stringify(this.data, null, 2)); - } +'use strict'; + +module.exports = { + Config: Config +} + +/** + * API for plugins to manage their own configuration settings + */ + +function Config(path, data) { + this.path = path; + this.data = data || {}; +} + +Config.prototype.get = function(key) { + this._validateKey(key); + var pluginName = key.split('.')[0]; + var keyName = key.split('.')[1]; + return this.data[pluginName] && this.data[pluginName][keyName]; +} + +Config.prototype.set = function(key, value) { + this._validateKey(key); + var pluginName = key.split('.')[0]; + var keyName = key.split('.')[1]; + this.data[pluginName] = this.data[pluginName] || {}; + this.data[pluginName][keyName] = value; + this.save(); +} + +Config.prototype._validateKey = function(key) { + if (key.split(".").length != 2) + throw new Error("The config key '" + key + "' is invalid. Configuration keys must be in the form [my-plugin].[myKey]"); +} + +Config.load = function(configPath) { + // load up the previous config if found + if (fs.existsSync(configPath)) + return new Config(configPath, JSON.parse(fs.readFileSync(configPath))); + else + return new Config(configPath); // empty initial config +} + +Config.prototype.save = function() { + fs.writeFileSync(this.path, JSON.stringify(this.data, null, 2)); } \ No newline at end of file diff --git a/lib/homebridge.js b/lib/homebridge.js deleted file mode 100644 index 04c822d..0000000 --- a/lib/homebridge.js +++ /dev/null @@ -1,14 +0,0 @@ -import fs from 'fs'; -import { User } from './user'; -import { Logger } from './logger'; - -// -// Main HomeBridge Module with global exports. -// - -// HomeBridge version -export const HOMEBRIDGE_VERSION = JSON.parse(fs.readFileSync('package.json')).version; - -// HomeBridge API -export let config = User.config; // instance of Config -export let logger = Logger.forProvider; // logger('provider-name') -> instance of Logger \ No newline at end of file diff --git a/lib/logger.js b/lib/logger.js index fa290bb..fecd081 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -1,58 +1,64 @@ -import chalk from 'chalk'; +var chalk = require('chalk'); -let DEBUG_ENABLED = false; +'use strict'; + +module.exports = { + Logger: Logger, + setDebugEnabled: setDebugEnabled, + _system: new Logger() // system logger, for internal use only +} + +var DEBUG_ENABLED = false; // Turns on debug level logging -export function setDebugEnabled(enabled) { +function setDebugEnabled(enabled) { DEBUG_ENABLED = enabled; } -// global cache of logger instances by provider name -let loggerCache = {}; +// global cache of logger instances by plugin name +var loggerCache = {}; -export class Logger { - - constructor(providerName) { - this.providerName = providerName; - } +/** + * Logger class + */ - debug(msg) { - if (DEBUG_ENABLED) - this.log('debug', msg); - } - - info(msg) { - this.log('info', msg); - } - - warn(msg) { - this.log('warn', msg); - } - - error(msg) { - this.log('error', msg); - } - - log(level, msg) { - - if (level == 'debug') - msg = chalk.gray(msg); - else if (level == 'warn') - msg = chalk.yellow(msg); - else if (level == 'error') - msg = chalk.bold.red(msg); - - // prepend provider name if applicable - if (this.providerName) - msg = chalk.cyan(`[${this.providerName}]`) + " " + msg; - - console.log(msg); - } - - static forProvider(providerName) { - return loggerCache[providerName] || (loggerCache[providerName] = new Logger(providerName)); - } +function Logger(pluginName) { + this.pluginName = pluginName; } -// system logger, for internal use only -export let log = new Logger(); +Logger.prototype.debug = function(msg) { + if (DEBUG_ENABLED) + this.log('debug', msg); +} + +Logger.prototype.info = function(msg) { + this.log('info', msg); +} + +Logger.prototype.warn = function(msg) { + this.log('warn', msg); +} + +Logger.prototype.error = function(msg) { + this.log('error', msg); +} + +Logger.prototype.log = function(level, msg) { + + if (level == 'debug') + msg = chalk.gray(msg); + else if (level == 'warn') + msg = chalk.yellow(msg); + else if (level == 'error') + msg = chalk.bold.red(msg); + + // prepend plugin name if applicable + if (this.pluginName) + msg = chalk.cyan("[" + this.pluginName + "]") + " " + msg; + + console.log(msg); +} + +Logger.forPlugin = function(pluginName) { + return loggerCache[pluginName] || (loggerCache[pluginName] = new Logger(pluginName)); +} diff --git a/lib/provider.js b/lib/provider.js deleted file mode 100644 index 6fb13e3..0000000 --- a/lib/provider.js +++ /dev/null @@ -1,98 +0,0 @@ -import path from 'path'; -import fs from 'fs'; -import semver from 'semver'; -import { User } from './user'; -import { HOMEBRIDGE_VERSION } from './homebridge'; - -// This class represents a HomeBridge Provider that may or may not be installed. -export class Provider { - - constructor(name) { - this.name = name; - } - - get path() { - return path.join(User.providersPath, this.name); - } - - load(options = {}) { - - // does this provider exist at all? - if (!fs.existsSync(this.path)) { - throw new Error(`Provider ${this.name} was not found. Make sure the directory '~/.homebridge/providers/${this.name}' exists.`) - } - - // check for a package.json - let pjsonPath = path.join(this.path, "package.json"); - let pjson = null; - - if (!fs.existsSync(pjsonPath)) { - throw new Error(`Provider ${this.name} does not contain a package.json.`); - } - - try { - // attempt to parse package.json - pjson = JSON.parse(fs.readFileSync(pjsonPath)); - } - catch (err) { - throw new Error(`Provider ${this.name} contains an invalid package.json. Error: ${err}`); - } - - // pluck out the HomeBridge version requirement - if (!pjson.engines || !pjson.engines.homebridge) { - throw new Error(`Provider ${this.name} does not contain a valid HomeBridge version requirement.`); - } - - let versionRequired = pjson.engines.homebridge; - - // make sure the version is satisfied by the currently running version of HomeBridge - if (!semver.satisfies(HOMEBRIDGE_VERSION, versionRequired)) { - throw new Error(`Provider ${this.name} requires a HomeBridge version of "${versionRequired}" which does not satisfy the current HomeBridge version of ${HOMEBRIDGE_VERSION}. You may need to upgrade your installation of HomeBridge.`); - } - - // figure out the main module - index.js unless otherwise specified - let main = pjson.main || "./index.js"; - - let mainPath = path.join(this.path, main); - - // try to require() it - let loadedProvider = require(mainPath); - - // pull out the configuration data, if any - let providerConfig = loadedProvider.config; - - // verify that all required values are present - if (providerConfig && !options.skipConfigCheck) { - for (let key in providerConfig) { - - let configParams = providerConfig[key]; - - if (configParams.required && !User.config.get(`${this.name}-${key}`)) { - throw new Error(`Provider ${this.name} requires the config value ${key} to be set.`); - } - } - } - - return loadedProvider; - } - - // Gets all providers installed on the local system - static installed() { - - let providers = []; - let names = fs.readdirSync(User.providersPath); - - for (let name of names) { - - // reconstruct full path - let fullPath = path.join(User.providersPath, name); - - // we only care about directories - if (!fs.statSync(fullPath).isDirectory()) continue; - - providers.push(new Provider(name)); - } - - return providers; - } -} diff --git a/lib/server.js b/lib/server.js index f7655b0..d1c5a02 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1,12 +1,119 @@ -import { Provider } from './provider'; -import { User, Config } from './user'; +var path = require('path'); +var http = require('http'); +var express = require('express'); +var jsxtransform = require('express-jsxtransform'); +var autoamd = require('./util/autoamd'); +var io = require('socket.io'); +var diffsync = require('diffsync'); +var Plugin = require('./Plugin').Plugin; +var User = require('./User').User; -export class Server { +'use strict'; - constructor(providers) { - this.providers = providers; // providers[name] = loaded provider JS module - } - - run() { - } +module.exports = { + Server: Server } + +function Server() { + this._plugins = {}; // plugins[name] = plugin + this._httpServer = null; // http.Server + this._dataAdapter = new diffsync.InMemoryDataAdapter(); // our "database" + this._diffsyncServer = null; // diffsync.Server + + // load and validate plugins - check for valid package.json, etc. + Plugin.installed().forEach(function(plugin) { + + // add it to our dict for easy lookup later + this._plugins[plugin.name()] = plugin; + + // attempt to load it + try { + plugin.load(); + } + catch (err) { + console.error(err); + plugin.loadError = err; + } + + }.bind(this)); +} + +Server.prototype.run = function() { + + // setting up express and socket.io + var app = express(); + + // our web assets are all located in a sibling folder 'public' + var root = path.dirname(__dirname); + var pub = path.join(root, 'public'); + + // middleware to convert our JS (written in CommonJS style) to AMD (require.js style) + app.use(autoamd('/public/js/')); + + // middleware to compile JSX on the fly + app.use(jsxtransform()); + + // middleware to serve static files in the public directory + app.use('/public', express.static(pub)); + + // match any path without a period (assuming period means you're asking for a static file) + app.get(/^[^\.]*$/, function(req, res){ + res.sendFile(path.join(pub, 'index.html')); + }); + + // HTTP web server + this._httpServer = http.createServer(app); + + // diffsync server + this._diffsyncServer = new diffsync.Server(this._dataAdapter, io(this._httpServer)); + + // grab our global "root" data object and fill it out with inital data for the browser + this._dataAdapter.getData("root", this._onRootLoaded.bind(this)); +} + +Server.prototype._onRootLoaded = function(err, root) { + + // we've loaded our "root" object from the DB - now fill it out before we make it available + // to clients. + + root.plugins = Object.keys(this._plugins).map(function(name) { + var plugin = this._plugins[name]; + var dict = { name: name }; + if (plugin.loadError) + dict.loadError = loadError; + + dict.providers = plugin.providers.map(function(provider) { + return { + name: provider.name, + title: provider.title, + config: provider.config, + } + }); + + return dict; + }.bind(this)); + + root.providers = root.providers || []; + + root.notifications = []; + + // if we're using browser-refresh for development, pass on the refresh script URL for the browser to load + root.browserRefreshURL = process.env.BROWSER_REFRESH_URL; + + // start the server! + this._httpServer.listen(4000, this._onHttpServerListen.bind(this)); +} + +Server.prototype._onHttpServerListen = function() { + + // we are now fully online - if we're using browser-refresh to auto-reload the browser during + // development, then it expects to receive this signal + if (process.send) + process.send('online'); +} + +// Forces diffsync to persist the latest version of the data under the given id (which may have been +// changed without its knowledge), and notify any connected clients about the change. +Server.prototype._forceSync = function(id) { + this._diffsyncServer.transport.to(id).emit(diffsync.COMMANDS.remoteUpdateIncoming, null); +} \ No newline at end of file diff --git a/lib/user.js b/lib/user.js index 5616b75..bbd7acb 100644 --- a/lib/user.js +++ b/lib/user.js @@ -1,31 +1,32 @@ -import path from 'path'; -import fs from 'fs'; -import { Config } from './config'; +var path = require('path'); +var fs = require('fs'); +var Config = require('./Config').Config; -// -// Manages user settings and storage locations. -// +'use strict'; -// global cached config -let config; - -export class User { - - static get config() { - return config || (config = Config.load(User.configPath)); - } - - static get storagePath() { - let home = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE; - return path.join(home, ".homebridge"); - } - - static get configPath() { - return path.join(User.storagePath, "config.json"); - } - - static get providersPath() { - return path.join(User.storagePath, "providers"); - } +module.exports = { + User: User } +/** + * Manages user settings and storage locations. + */ + +// global cached config +var config; + +function User() { +} + +User.config = function() { + return config || (config = Config.load(User.configPath())); +} + +User.storagePath = function() { + var home = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE; + return path.join(home, ".homebridge"); +} + +User.configPath = function() { + return path.join(User.storagePath(), "config.json"); +} diff --git a/lib/util/autoamd.js b/lib/util/autoamd.js new file mode 100644 index 0000000..0bd7d74 --- /dev/null +++ b/lib/util/autoamd.js @@ -0,0 +1,30 @@ +var interceptor = require('express-interceptor'); + +/** + * Express middleware that converts CommonJS-style code to RequireJS style for the browser (assuming require.js is loaded). + */ + +module.exports = function(urlPrefix) { + return interceptor(function(req, res){ + return { + // Only URLs with the given prefix will be converted to require.js style + isInterceptable: function(){ + return req.originalUrl.indexOf(urlPrefix) == 0; + }, + intercept: function(body, send) { + send(toRequireJS(body)); + } + }; + }); +} + +// From https://github.com/shovon/connect-commonjs-amd/blob/master/src/middleware.coffee +function toRequireJS(str) { + var requireCalls = str.match(/require\((\s+)?('[^'\\]*(?:\\.[^'\\]*)*'|"[^"\\]*(?:\\.[^"\\]*)*")(\s+)?\)/g) || []; + requireCalls = requireCalls.map(function(str) { + return (str.match(/('[^'\\]*(?:\\.[^'\\]*)*'|"[^"\\]*(?:\\.[^"\\]*)*")/))[0]; + }); + requireCalls.unshift("'require'"); + str = "define([" + (requireCalls.join(', ')) + "], function (require) {\nvar module = { exports: {} }\n , exports = module.exports;\n\n(function () {\n\n" + str + "\n\n})();\n\nreturn module.exports;\n});"; + return str; +}; \ No newline at end of file diff --git a/lib/util.js b/lib/util/camelcase.js similarity index 66% rename from lib/util.js rename to lib/util/camelcase.js index ccd60e2..15aff01 100644 --- a/lib/util.js +++ b/lib/util/camelcase.js @@ -1,9 +1,13 @@ +module.exports = { + camelCaseToRegularForm: camelCaseToRegularForm +} + // Converts "accessToken" to "Access Token" -export function camelCaseToRegularForm(camelCase) { +function camelCaseToRegularForm(camelCase) { return camelCase // insert a space before all caps .replace(/([A-Z])/g, ' $1') // uppercase the first character .replace(/^./, function(str){ return str.toUpperCase(); }) -} \ No newline at end of file +} diff --git a/lib/version.js b/lib/version.js new file mode 100644 index 0000000..c0d9c15 --- /dev/null +++ b/lib/version.js @@ -0,0 +1,12 @@ +var fs = require('fs'); +var path = require('path'); + +'use strict'; + +module.exports = getVersion(); + +function getVersion() { + var packageJSONPath = path.join(__dirname, '../package.json'); + var packageJSON = JSON.parse(fs.readFileSync(packageJSONPath)); + return packageJSON.version; +} diff --git a/package.json b/package.json index 8c6a582..8592ca6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homebridge", - "description": "HomeKit support for existing home devices, today.", + "description": "HomeKit support for the impatient", "version": "0.0.0", "author": { "name": "Nick Farina" @@ -26,11 +26,14 @@ }, "preferGlobal": true, "dependencies": { - "babel": "^5.6.14", - "chalk": "^1.1.0", "commander": "^2.8.1", - "hap-nodejs": "git+https://github.com/khaost/HAP-NodeJS#2a1bc8d99a2009317ab5da93faebea34c89f197c", - "prompt": "^0.2.14", - "semver": "^4.3.6" + "diffsync": "^2.1.2", + "express": "^4.13.3", + "express-interceptor": "^1.1.1", + "express-jsxtransform": "^3.0.3", + "hap-nodejs": "git+https://github.com/nfarina/HAP-NodeJS#3b1eaa8", + "react-tools": "^0.13.3", + "semver": "^4.3.6", + "socket.io": "^1.3.6" } } diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..48a45f0 --- /dev/null +++ b/public/index.html @@ -0,0 +1,41 @@ + + + + + homebridge + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/js/admin.jsx b/public/js/admin.jsx new file mode 100644 index 0000000..0837890 --- /dev/null +++ b/public/js/admin.jsx @@ -0,0 +1,16 @@ + + +module.exports.Admin = React.createClass({ + render: function() { + return ( +
+

Installed Plugins

+ +
+ ); + } +}); diff --git a/public/js/app.jsx b/public/js/app.jsx new file mode 100644 index 0000000..d4f0d7d --- /dev/null +++ b/public/js/app.jsx @@ -0,0 +1,116 @@ +var Route = ReactRouter.Route; +var DefaultRoute = ReactRouter.DefaultRoute; +var NotFoundRoute = ReactRouter.NotFoundRoute; +var RouteHandler = ReactRouter.RouteHandler; +var Link = ReactRouter.Link; +var ProviderGrid = require('./providers.jsx').ProviderGrid; +var Admin = require('./admin.jsx').Admin; +var NotificationCenter = require('./notifications.jsx').NotificationCenter; + +var App = React.createClass({ + getInitialState: function() { + return { root: null }; + }, + + componentDidMount: function() { + + // pass the connection and the id of the data you want to synchronize + var client = new diffsync.Client(io(), "root"); + + client.on('connected', function(){ + // initial data was loaded - pass it on to our state + this.setState({ root: client.getData() }); + + // if we're using browser-refresh to auto-reload the browser during development, then + // we'll receive the URL to a JS script in this location (see Server.js) + if (this.state.root.browserRefreshURL) { + var script = document.createElement('script'); + script.setAttribute('src', this.state.root.browserRefreshURL); + document.head.appendChild(script); + } + + }.bind(this)); + + client.on('synced', function(){ + // server has updated our data - pass it on to our state + this.setState({ root: client.getData() }); + + }.bind(this)); + + client.initialize(); + }, + + render: function() { + var root = this.state.root; + + return root && ( +
+ + +
+ ); + } +}); + +var Home = React.createClass({ + render: function() { + return ( +
+ +
+ ); + } +}); + +var NotFound = React.createClass({ + render() { + return ( +
+

That page could not be found.

+
+ ) + } +}); + +var routes = ( + + + + + +); + +ReactRouter.run(routes, ReactRouter.HistoryLocation, function (Handler) { + React.render(, document.body); +}); diff --git a/public/js/notifications.jsx b/public/js/notifications.jsx new file mode 100644 index 0000000..9b78792 --- /dev/null +++ b/public/js/notifications.jsx @@ -0,0 +1,19 @@ + +module.exports.NotificationCenter = React.createClass({ + render: function() { + + var notifications = this.props.notifications; + + if (!notifications || notifications.length == 0) return ( +
No Notifications
+ ) + + return ( +
+ { notifications.map(function(notification, index) { + return
{notification.message}
; + }) } +
+ ); + } +}); \ No newline at end of file diff --git a/public/js/providers.jsx b/public/js/providers.jsx new file mode 100644 index 0000000..4217a8e --- /dev/null +++ b/public/js/providers.jsx @@ -0,0 +1,218 @@ +var Link = ReactRouter.Link; + +/** + * Provider Grid - displays all created providers. + */ + +module.exports.ProviderGrid = React.createClass({ + + render() { + var root = this.props.root; + + return ( +
+ + {/* need a wrapper div to counteract card margins around the conatiner edges */} +
+ {root.providers.map(function(provider) { + + })} + + +
+ + {/* add provider */} + + +
+ ) + }, +}); + +/** + * Provider "Card" + */ + +var ProviderCard = React.createClass({ + render() { + var provider = this.props.provider; + + var imageStyle = { + background: "url(//pbs.twimg.com/profile_images/519977105543528448/HAc6jtgo_400x400.png)", + backgroundSize: "cover", + height: "100%" + } + + return ( +
+
+
+
+
+ WeMo + + 5 accessories + +
+
+ ) + }, + + cardClicked() { + console.log("Click!"); + } +}); + +/** + * Add Provider button + dialog + */ + +var AddProviderButton = React.createClass({ + getInitialState() { + return { + selectedPlugin: null, + selectedProvider: null, + newProviderName: null + } + }, + + render() { + var plugins = this.props.plugins; + + return ( +
+ +
+ +
+ + + +
+ ) + }, + + onSelectProvider(plugin, provider) { + this.setState({ + selectedPlugin: plugin, + selectedProvider: provider + }); + } +}); + + +/** + * Providers Dropdown + */ + +var ProvidersDropdown = React.createClass({ + render() { + var plugins = this.props.plugins; + + var items = []; + + plugins.forEach(function(plugin) { + items.push(
  • {plugin.name}
  • ); + + plugin.providers.forEach(function(provider) { + items.push( +
  • + + {provider.title} + +
  • + ); + }.bind(this)); + + }.bind(this)); + + return ( +
    + +
      + { items } +
    +
    + ) + }, + + onSelectProvider(plugin, provider) { + if (this.props.onSelectProvider) + this.props.onSelectProvider(plugin, provider); + } +}); + + +/** + * CSS Styles + */ + +var Styles = { + cardsWrapper: { + margin: "-10px" + }, + card: { + width: "150px", + display: "inline-block", + margin: "10px", + cursor: "pointer" + }, + cardBody: { + height: "150px", + padding: "0" + }, + cardFooter: { + fontSize:"100%", + fontWeight:"bold", + textAlign: "center", + background: "#fafafa" + }, + cardTimestamp: { + fontSize: "80%", + fontWeight: "lighter", + display: "block", + }, + addProvider: { + margin: "30px", + textAlign: "center" + } +}; \ No newline at end of file diff --git a/public/vendor/ReactRouter.js b/public/vendor/ReactRouter.js new file mode 100644 index 0000000..81b8307 --- /dev/null +++ b/public/vendor/ReactRouter.js @@ -0,0 +1,3386 @@ +(function webpackUniversalModuleDefinition(root, factory) { + if(typeof exports === 'object' && typeof module === 'object') + module.exports = factory(require("react")); + else if(typeof define === 'function' && define.amd) + define(["react"], factory); + else if(typeof exports === 'object') + exports["ReactRouter"] = factory(require("react")); + else + root["ReactRouter"] = factory(root["React"]); +})(this, function(__WEBPACK_EXTERNAL_MODULE_21__) { +return /******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) +/******/ return installedModules[moduleId].exports; +/******/ +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ exports: {}, +/******/ id: moduleId, +/******/ loaded: false +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.loaded = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + exports.DefaultRoute = __webpack_require__(1); + exports.Link = __webpack_require__(2); + exports.NotFoundRoute = __webpack_require__(3); + exports.Redirect = __webpack_require__(4); + exports.Route = __webpack_require__(5); + exports.ActiveHandler = __webpack_require__(6); + exports.RouteHandler = exports.ActiveHandler; + + exports.HashLocation = __webpack_require__(7); + exports.HistoryLocation = __webpack_require__(8); + exports.RefreshLocation = __webpack_require__(9); + exports.StaticLocation = __webpack_require__(10); + exports.TestLocation = __webpack_require__(11); + + exports.ImitateBrowserBehavior = __webpack_require__(12); + exports.ScrollToTopBehavior = __webpack_require__(13); + + exports.History = __webpack_require__(14); + exports.Navigation = __webpack_require__(15); + exports.State = __webpack_require__(16); + + exports.createRoute = __webpack_require__(17).createRoute; + exports.createDefaultRoute = __webpack_require__(17).createDefaultRoute; + exports.createNotFoundRoute = __webpack_require__(17).createNotFoundRoute; + exports.createRedirect = __webpack_require__(17).createRedirect; + exports.createRoutesFromReactChildren = __webpack_require__(18); + + exports.create = __webpack_require__(19); + exports.run = __webpack_require__(20); + +/***/ }, +/* 1 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }; + + var _inherits = function (subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + + var PropTypes = __webpack_require__(22); + var RouteHandler = __webpack_require__(6); + var Route = __webpack_require__(5); + + /** + * A component is a special kind of that + * renders when its parent matches but none of its siblings do. + * Only one such route may be used at any given level in the + * route hierarchy. + */ + + var DefaultRoute = (function (_Route) { + function DefaultRoute() { + _classCallCheck(this, DefaultRoute); + + if (_Route != null) { + _Route.apply(this, arguments); + } + } + + _inherits(DefaultRoute, _Route); + + return DefaultRoute; + })(Route); + + // TODO: Include these in the above class definition + // once we can use ES7 property initializers. + // https://github.com/babel/babel/issues/619 + + DefaultRoute.propTypes = { + name: PropTypes.string, + path: PropTypes.falsy, + children: PropTypes.falsy, + handler: PropTypes.func.isRequired + }; + + DefaultRoute.defaultProps = { + handler: RouteHandler + }; + + module.exports = DefaultRoute; + +/***/ }, +/* 2 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }; + + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + + var _inherits = function (subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + + var React = __webpack_require__(21); + var assign = __webpack_require__(33); + var PropTypes = __webpack_require__(22); + + function isLeftClickEvent(event) { + return event.button === 0; + } + + function isModifiedEvent(event) { + return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); + } + + /** + * components are used to create an element that links to a route. + * When that route is active, the link gets an "active" class name (or the + * value of its `activeClassName` prop). + * + * For example, assuming you have the following route: + * + * + * + * You could use the following component to link to that route: + * + * + * + * In addition to params, links may pass along query string parameters + * using the `query` prop. + * + * + */ + + var Link = (function (_React$Component) { + function Link() { + _classCallCheck(this, Link); + + if (_React$Component != null) { + _React$Component.apply(this, arguments); + } + } + + _inherits(Link, _React$Component); + + _createClass(Link, [{ + key: 'handleClick', + value: function handleClick(event) { + var allowTransition = true; + var clickResult; + + if (this.props.onClick) clickResult = this.props.onClick(event); + + if (isModifiedEvent(event) || !isLeftClickEvent(event)) { + return; + }if (clickResult === false || event.defaultPrevented === true) allowTransition = false; + + event.preventDefault(); + + if (allowTransition) this.context.router.transitionTo(this.props.to, this.props.params, this.props.query); + } + }, { + key: 'getHref', + + /** + * Returns the value of the "href" attribute to use on the DOM element. + */ + value: function getHref() { + return this.context.router.makeHref(this.props.to, this.props.params, this.props.query); + } + }, { + key: 'getClassName', + + /** + * Returns the value of the "class" attribute to use on the DOM element, which contains + * the value of the activeClassName property when this is active. + */ + value: function getClassName() { + var className = this.props.className; + + if (this.getActiveState()) className += ' ' + this.props.activeClassName; + + return className; + } + }, { + key: 'getActiveState', + value: function getActiveState() { + return this.context.router.isActive(this.props.to, this.props.params, this.props.query); + } + }, { + key: 'render', + value: function render() { + var props = assign({}, this.props, { + href: this.getHref(), + className: this.getClassName(), + onClick: this.handleClick.bind(this) + }); + + if (props.activeStyle && this.getActiveState()) props.style = props.activeStyle; + + return React.DOM.a(props, this.props.children); + } + }]); + + return Link; + })(React.Component); + + // TODO: Include these in the above class definition + // once we can use ES7 property initializers. + // https://github.com/babel/babel/issues/619 + + Link.contextTypes = { + router: PropTypes.router.isRequired + }; + + Link.propTypes = { + activeClassName: PropTypes.string.isRequired, + to: PropTypes.oneOfType([PropTypes.string, PropTypes.route]).isRequired, + params: PropTypes.object, + query: PropTypes.object, + activeStyle: PropTypes.object, + onClick: PropTypes.func + }; + + Link.defaultProps = { + activeClassName: 'active', + className: '' + }; + + module.exports = Link; + +/***/ }, +/* 3 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }; + + var _inherits = function (subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + + var PropTypes = __webpack_require__(22); + var RouteHandler = __webpack_require__(6); + var Route = __webpack_require__(5); + + /** + * A is a special kind of that + * renders when the beginning of its parent's path matches + * but none of its siblings do, including any . + * Only one such route may be used at any given level in the + * route hierarchy. + */ + + var NotFoundRoute = (function (_Route) { + function NotFoundRoute() { + _classCallCheck(this, NotFoundRoute); + + if (_Route != null) { + _Route.apply(this, arguments); + } + } + + _inherits(NotFoundRoute, _Route); + + return NotFoundRoute; + })(Route); + + // TODO: Include these in the above class definition + // once we can use ES7 property initializers. + // https://github.com/babel/babel/issues/619 + + NotFoundRoute.propTypes = { + name: PropTypes.string, + path: PropTypes.falsy, + children: PropTypes.falsy, + handler: PropTypes.func.isRequired + }; + + NotFoundRoute.defaultProps = { + handler: RouteHandler + }; + + module.exports = NotFoundRoute; + +/***/ }, +/* 4 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }; + + var _inherits = function (subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + + var PropTypes = __webpack_require__(22); + var Route = __webpack_require__(5); + + /** + * A component is a special kind of that always + * redirects to another route when it matches. + */ + + var Redirect = (function (_Route) { + function Redirect() { + _classCallCheck(this, Redirect); + + if (_Route != null) { + _Route.apply(this, arguments); + } + } + + _inherits(Redirect, _Route); + + return Redirect; + })(Route); + + // TODO: Include these in the above class definition + // once we can use ES7 property initializers. + // https://github.com/babel/babel/issues/619 + + Redirect.propTypes = { + path: PropTypes.string, + from: PropTypes.string, // Alias for path. + to: PropTypes.string, + handler: PropTypes.falsy + }; + + // Redirects should not have a default handler + Redirect.defaultProps = {}; + + module.exports = Redirect; + +/***/ }, +/* 5 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }; + + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + + var _inherits = function (subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + + var React = __webpack_require__(21); + var invariant = __webpack_require__(34); + var PropTypes = __webpack_require__(22); + var RouteHandler = __webpack_require__(6); + + /** + * components specify components that are rendered to the page when the + * URL matches a given pattern. + * + * Routes are arranged in a nested tree structure. When a new URL is requested, + * the tree is searched depth-first to find a route whose path matches the URL. + * When one is found, all routes in the tree that lead to it are considered + * "active" and their components are rendered into the DOM, nested in the same + * order as they are in the tree. + * + * The preferred way to configure a router is using JSX. The XML-like syntax is + * a great way to visualize how routes are laid out in an application. + * + * var routes = [ + * + * + * + * + * + * ]; + * + * Router.run(routes, function (Handler) { + * React.render(, document.body); + * }); + * + * Handlers for Route components that contain children can render their active + * child route using a element. + * + * var App = React.createClass({ + * render: function () { + * return ( + *
    + * + *
    + * ); + * } + * }); + * + * If no handler is provided for the route, it will render a matched child route. + */ + + var Route = (function (_React$Component) { + function Route() { + _classCallCheck(this, Route); + + if (_React$Component != null) { + _React$Component.apply(this, arguments); + } + } + + _inherits(Route, _React$Component); + + _createClass(Route, [{ + key: 'render', + value: function render() { + invariant(false, '%s elements are for router configuration only and should not be rendered', this.constructor.name); + } + }]); + + return Route; + })(React.Component); + + // TODO: Include these in the above class definition + // once we can use ES7 property initializers. + // https://github.com/babel/babel/issues/619 + + Route.propTypes = { + name: PropTypes.string, + path: PropTypes.string, + handler: PropTypes.func, + ignoreScrollBehavior: PropTypes.bool + }; + + Route.defaultProps = { + handler: RouteHandler + }; + + module.exports = Route; + +/***/ }, +/* 6 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }; + + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + + var _inherits = function (subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + + var React = __webpack_require__(21); + var ContextWrapper = __webpack_require__(23); + var assign = __webpack_require__(33); + var PropTypes = __webpack_require__(22); + + var REF_NAME = '__routeHandler__'; + + /** + * A component renders the active child route handler + * when routes are nested. + */ + + var RouteHandler = (function (_React$Component) { + function RouteHandler() { + _classCallCheck(this, RouteHandler); + + if (_React$Component != null) { + _React$Component.apply(this, arguments); + } + } + + _inherits(RouteHandler, _React$Component); + + _createClass(RouteHandler, [{ + key: 'getChildContext', + value: function getChildContext() { + return { + routeDepth: this.context.routeDepth + 1 + }; + } + }, { + key: 'componentDidMount', + value: function componentDidMount() { + this._updateRouteComponent(this.refs[REF_NAME]); + } + }, { + key: 'componentDidUpdate', + value: function componentDidUpdate() { + this._updateRouteComponent(this.refs[REF_NAME]); + } + }, { + key: 'componentWillUnmount', + value: function componentWillUnmount() { + this._updateRouteComponent(null); + } + }, { + key: '_updateRouteComponent', + value: function _updateRouteComponent(component) { + this.context.router.setRouteComponentAtDepth(this.getRouteDepth(), component); + } + }, { + key: 'getRouteDepth', + value: function getRouteDepth() { + return this.context.routeDepth; + } + }, { + key: 'createChildRouteHandler', + value: function createChildRouteHandler(props) { + var route = this.context.router.getRouteAtDepth(this.getRouteDepth()); + + if (route == null) { + return null; + }var childProps = assign({}, props || this.props, { + ref: REF_NAME, + params: this.context.router.getCurrentParams(), + query: this.context.router.getCurrentQuery() + }); + + return React.createElement(route.handler, childProps); + } + }, { + key: 'render', + value: function render() { + var handler = this.createChildRouteHandler(); + //