diff --git a/.gitignore b/.gitignore
index ff1ed318b..09ec932e4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,4 @@
-npm-debug.log
node_modules
-docs
-.tern-project
-.tern-config
-
+lerna-debug.log
+npm-debug.log.*
+npm-debug.log
diff --git a/docs/contributing.md b/docs/contributing.md
new file mode 100644
index 000000000..8892484fb
--- /dev/null
+++ b/docs/contributing.md
@@ -0,0 +1,56 @@
+# Contributing
+
+### Getting Started
+
+Clone & switch to the right branch.
+
+```
+git clone git@github.com:skellock/reactotron.git
+cd reactotron
+git checkout next
+```
+
+Run the setup script to install the dependencies & run tests.
+
+```
+npm run welcome
+```
+
+
+### Code Style
+
+We use [standard.js](https://github.com/feross/standard). It is passive-agressively enforced.
+Stern looks will be handed out. For repeat offenders, there WILL be finger wagging.
+
+
+### Monorepo & Lerna
+
+This is a monorepo: multiple JS packages in 1 git repo.
+
+We use [lerna](https://github.com/lerna/lerna) to help us.
+
+
+### Funky internal dependencies
+
+`demo-react-native` is a React Native sample app. It depends on
+`reactotron-react-native` which in turn depends on both `socket.io` and
+`reactotron-core-client`.
+
+Usually npm handles this right?
+
+So why am I typing this?
+
+Well, React Native (<=0.30.0) makes this hard to link to the dependencies
+within this repo.
+
+How we current get around this is by:
+
+1. adding `socket.io` to the deps of `demo-react-native` :(
+1. copy built versions of `reactotron-core-client` and `reactotron-react-native` manually to `node_modules` :(
+
+Many tears were shed and this is the least shitty solution I could muster.
+
+In the root, you can run `npm copy-internal-deps` to make them copy over.
+
+But since you're copying built dependencies, you'll need to run `npm run build` first
+if you're doing this to bring forward changes in those dependency libraries.
diff --git a/docs/images/apisauce/apisauce.jpg b/docs/images/apisauce/apisauce.jpg
new file mode 100644
index 000000000..443cf18da
Binary files /dev/null and b/docs/images/apisauce/apisauce.jpg differ
diff --git a/docs/images/quick-start-react-js/first-connect.jpg b/docs/images/quick-start-react-js/first-connect.jpg
new file mode 100644
index 000000000..cac881dcf
Binary files /dev/null and b/docs/images/quick-start-react-js/first-connect.jpg differ
diff --git a/docs/images/quick-start-react-js/hello-1.jpg b/docs/images/quick-start-react-js/hello-1.jpg
new file mode 100644
index 000000000..f151fab39
Binary files /dev/null and b/docs/images/quick-start-react-js/hello-1.jpg differ
diff --git a/docs/images/quick-start-react-js/hello-2.jpg b/docs/images/quick-start-react-js/hello-2.jpg
new file mode 100644
index 000000000..5ec1bcf32
Binary files /dev/null and b/docs/images/quick-start-react-js/hello-2.jpg differ
diff --git a/docs/images/quick-start-react-js/installing.jpg b/docs/images/quick-start-react-js/installing.jpg
new file mode 100644
index 000000000..23e149f90
Binary files /dev/null and b/docs/images/quick-start-react-js/installing.jpg differ
diff --git a/docs/images/quick-start-react-js/react-demo-js-reactotron.jpg b/docs/images/quick-start-react-js/react-demo-js-reactotron.jpg
new file mode 100644
index 000000000..65e9e64f6
Binary files /dev/null and b/docs/images/quick-start-react-js/react-demo-js-reactotron.jpg differ
diff --git a/docs/images/quick-start-react-js/react-demo-js.jpg b/docs/images/quick-start-react-js/react-demo-js.jpg
new file mode 100644
index 000000000..c38f7a811
Binary files /dev/null and b/docs/images/quick-start-react-js/react-demo-js.jpg differ
diff --git a/docs/images/quick-start-react-js/spammy.jpg b/docs/images/quick-start-react-js/spammy.jpg
new file mode 100644
index 000000000..14351204e
Binary files /dev/null and b/docs/images/quick-start-react-js/spammy.jpg differ
diff --git a/docs/images/quick-start-react-native/first-connect.jpg b/docs/images/quick-start-react-native/first-connect.jpg
new file mode 100644
index 000000000..b3dcc8fa1
Binary files /dev/null and b/docs/images/quick-start-react-native/first-connect.jpg differ
diff --git a/docs/images/quick-start-react-native/hello-1.jpg b/docs/images/quick-start-react-native/hello-1.jpg
new file mode 100644
index 000000000..f414bd346
Binary files /dev/null and b/docs/images/quick-start-react-native/hello-1.jpg differ
diff --git a/docs/images/quick-start-react-native/hello-2.jpg b/docs/images/quick-start-react-native/hello-2.jpg
new file mode 100644
index 000000000..fc4e46a61
Binary files /dev/null and b/docs/images/quick-start-react-native/hello-2.jpg differ
diff --git a/docs/images/quick-start-react-native/react-demo-native-reactotron.jpg b/docs/images/quick-start-react-native/react-demo-native-reactotron.jpg
new file mode 100644
index 000000000..70fe92b44
Binary files /dev/null and b/docs/images/quick-start-react-native/react-demo-native-reactotron.jpg differ
diff --git a/docs/images/quick-start-react-native/react-demo-native.jpg b/docs/images/quick-start-react-native/react-demo-native.jpg
new file mode 100644
index 000000000..95658e5c8
Binary files /dev/null and b/docs/images/quick-start-react-native/react-demo-native.jpg differ
diff --git a/docs/images/quick-start-react-native/spammy.jpg b/docs/images/quick-start-react-native/spammy.jpg
new file mode 100644
index 000000000..0a0bcd925
Binary files /dev/null and b/docs/images/quick-start-react-native/spammy.jpg differ
diff --git a/docs/images/readme/Reactotron-128.png b/docs/images/readme/Reactotron-128.png
new file mode 100644
index 000000000..29527f8be
Binary files /dev/null and b/docs/images/readme/Reactotron-128.png differ
diff --git a/docs/images/readme/Reactotron-64.png b/docs/images/readme/Reactotron-64.png
new file mode 100644
index 000000000..40951d16c
Binary files /dev/null and b/docs/images/readme/Reactotron-64.png differ
diff --git a/docs/images/readme/reactotron-demo-app.gif b/docs/images/readme/reactotron-demo-app.gif
new file mode 100644
index 000000000..32060fd75
Binary files /dev/null and b/docs/images/readme/reactotron-demo-app.gif differ
diff --git a/docs/images/readme/reactotron-demo-cli.gif b/docs/images/readme/reactotron-demo-cli.gif
new file mode 100644
index 000000000..88c10c4f2
Binary files /dev/null and b/docs/images/readme/reactotron-demo-cli.gif differ
diff --git a/docs/images/redux/dispatching.jpg b/docs/images/redux/dispatching.jpg
new file mode 100644
index 000000000..92a537a2e
Binary files /dev/null and b/docs/images/redux/dispatching.jpg differ
diff --git a/docs/images/redux/redux-keys-values.jpg b/docs/images/redux/redux-keys-values.jpg
new file mode 100644
index 000000000..1fc0931b2
Binary files /dev/null and b/docs/images/redux/redux-keys-values.jpg differ
diff --git a/docs/images/redux/subscriptions.jpg b/docs/images/redux/subscriptions.jpg
new file mode 100644
index 000000000..486e567f0
Binary files /dev/null and b/docs/images/redux/subscriptions.jpg differ
diff --git a/docs/images/track-global-errors/stack-trace.jpg b/docs/images/track-global-errors/stack-trace.jpg
new file mode 100644
index 000000000..b8be557d2
Binary files /dev/null and b/docs/images/track-global-errors/stack-trace.jpg differ
diff --git a/docs/plugin-apisauce.md b/docs/plugin-apisauce.md
new file mode 100644
index 000000000..748656041
--- /dev/null
+++ b/docs/plugin-apisauce.md
@@ -0,0 +1,44 @@
+# reactotron-apisauce
+
+[Apisauce](http://github.com/skellock/apisauce) is a lightweight wrapper around the fantastic [Axios](https://github.com/mzabriskie/axios) networking library.
+
+![API Response](./images/apisauce/apisauce.jpg)
+
+# Installing
+
+`npm i --save-dev reactotron-apisauce`
+
+
+# Configuring
+
+In the place where you setup your reactotron configuration, you import `reactotron-apisauce` plugin and throw it at Reactotron.
+
+```js
+import apisaucePlugin from 'reactotron-apisauce' // <--- import
+
+// then plug it in when you configure Reactotron.
+
+Reactotron
+ .configure()
+ .use(apisaucePlugin()) // <-- here we go!!!
+ .connect()
+```
+
+Next, wherever you create your api for you application, bring in Reactotron and attach the monitor to your apisauce instance.
+
+```js
+import Reactotron from 'reactotron-react-js'
+// import Reactotron from 'reactotron-react-native' // or use this for mobile
+
+// Apisauce has a feature where you can attach a handler to watch
+// all requests/response flowing through your api. You can hook this up:
+api.addMonitor(Reactotron.apisauce)
+
+// or ...
+
+// if you just wanted to track on 500's
+api.addMonitor(response => {
+ if (response.problem === 'SERVER_ERROR')
+ Reactotron.apisauce(response)
+})
+```
diff --git a/docs/plugin-redux.md b/docs/plugin-redux.md
new file mode 100644
index 000000000..2580a221c
--- /dev/null
+++ b/docs/plugin-redux.md
@@ -0,0 +1,88 @@
+# reactotron-redux
+
+[Redux](http://redux.js.org/) is library for managing global state. It's pretty damn awesome.
+
+And everyone knows when you combine two great things, you get a super-awesome thing. Except for toothpaste & orange juice. Keep those separate.
+
+![Keys and Values](./images/redux/redux-keys-values.jpg)
+
+![Dispatching](./images/redux/dispatching.jpg)
+
+![Subscriptions](./images/redux/subscriptions.jpg)
+
+
+# Installing
+
+`reactotron-redux` is a plugin which lives in a different npm package. You can install it by typing:
+
+```
+npm install --save-dev reactotron-redux
+```
+
+# Features
+
+* track when actions are dispatch including timing
+* subscribe to changes within the state tree
+* pull values out on demand
+* view list of keys
+* dispatch actions from Reactotron
+
+# Setting Up
+
+This plugin connects Reactotron to Redux via Redux's [Store Enhancer](http://redux.js.org/docs/Glossary.html#store-enhancer) plugin system.
+
+You'll configure where you setup your Redux store. In that file, we'll import both `reactotron-redux` and Reactotron.
+
+```js
+import Reactotron from 'reactotron-react-js'
+// import Reactotron from 'reactotron-react-native' // if on mobile
+import createReactotronEnhancer from 'reactotron-redux'
+```
+
+`createReactotronEnhancer` is a function that creates an enhancer ready to inject into Redux. There are 2 parameters.
+
+1. `reactotronInstance` (required) - `Reactotron` itself. It's the object that came out of the import at the top.
+2. `options` (optional) - An object providing configuration option to the store enhancer.
+
+Use take the return value and put that into your call to `createStore()`.
+
+**IMPORTANT: Your enhancer should go first if there are multiple enhancers.**
+
+Here's a few examples in action:
+
+```js
+const reactotronEnhancer = createReactotronEnhancer(Reactotron)
+
+// where there are no other enhancers
+createStore(rootReducer, reactotronEnhancer)
+
+// using Redux's compose() to bring together multiple enhancers
+createStore(rootReducer, compose(reactotronEnhancer, applyMiddleware(logger, sagaMiddleware)))
+```
+
+See the demos for more examples.
+
+# Options
+
+`createReactotronEnhancer` supports options as the 2nd parameter.
+
+`except` is an array of strings that match actions flowing through Redux.
+
+If you have some actions you'd rather just not see (for example, `redux-saga`)
+triggers a little bit of noise, you can suppress them:
+
+```js
+createReactotronEnhancer(Reactotron, {
+ except: ['EFFECT_TRIGGERED', 'EFFECT_RESOLVED', 'EFFECT_REJECTED']
+})
+```
+
+`isActionImportant` is a way to mark certain actions as "important". Important messages are display in a bolder style that gets your attention within Reactotron.
+
+It is a function that accepts the action and returns a `boolean`. `true` is important. `false` is normal.
+
+```js
+createReactotronEnhancer(Reactotron, {
+ isActionImportant: action => action.type === 'FORMAT_HARD_DRIVE'
+})
+```
diff --git a/docs/plugin-track-global-errors.md b/docs/plugin-track-global-errors.md
new file mode 100644
index 000000000..1a01e36a4
--- /dev/null
+++ b/docs/plugin-track-global-errors.md
@@ -0,0 +1,84 @@
+# Track Global Errors
+
+Both `reactotron-react-native` and `reactotron-react-js` ship with a plugin called `trackGlobalErrors`.
+
+The goal of this plugin is to ensure all errors will get thrown over to Reactotron for display.
+
+![Installing The App](./images/track-global-errors/stack-trace.jpg)
+
+
+# Usage
+
+Wherever you setup your Reactotron in your app, you also add the additional plugin on the `import` line.
+
+```js
+import Reactotron, { trackGlobalErrors } from 'reactotron-react-native'
+```
+
+or
+
+```js
+import Reactotron, { trackGlobalErrors } from 'reactotron-react-js'
+```
+
+Next, add it as a plugin to Reactotron.
+
+```js
+Reactotron
+ .configure()
+ .use(trackGlobalErrors()) // <--- here we go!
+ .connect()
+```
+
+One option `trackGlobalErrors()` supports is `veto`. Veto is function that allows you to some frames you would like to leave out of the stack trace passed along.
+
+For example, on React Native, perhaps you'd like to leave off any frames sourced from React Native itself?
+
+```js
+Reactotron
+ .configure()
+ .use(trackGlobalErrors({
+ veto: frame => frame.fileName.indexOf('/node_modules/react-native/') >= 0
+ }))
+ .connect()
+```
+
+`veto` is a function that takes an `object` and returns a `boolean`. `true` = ditch it. `false` = keep it.
+
+The frame object passed into `veto` has these properties.
+
+```
+functionName: the name of the function or null if an anonymous function
+lineNumber: the line number of the error
+columnNumber: the column number of the error
+fileName: the name of the file
+```
+
+### React JS Source Maps
+
+Source maps for projects in webpack need to have the `devtool` set to `source-map`. On `create-react-app`-based apps, they use `eval`, so this will not work. You will have to switch your development webpack configuration to support this.
+
+You also have the option to not do source map lookups, but still pass errors along by going into offline mode.
+
+```js
+Reactotron
+ .configure()
+ .use(trackGlobalErrors({
+ offline: true
+ }))
+ .connect()
+```
+
+
+# How It Works Internally
+
+### React JS
+
+It hijacks the browser's `window.onerror` event, immediately calling the previous `onerror`, then attempts to resolve the source of the errors via a source-map lookup. If successful, it throws a message over to Reactotron.
+
+
+### React Native
+
+On React Native, it hooks `NativeModules.ExceptionsManager.updateExceptionMessage`. This is a message that occurs after the source map lookup for React Native. It always calls the original function so it won't break anything internally. Your Red Box will still show.
+
+Because we're hooking a Facebook internal function, this is a bit fragile. I'll stay on top of any React Native upgrades that might cause problems should the API change.
diff --git a/docs/quick-start-react-js.md b/docs/quick-start-react-js.md
new file mode 100644
index 000000000..28c6ebaa5
--- /dev/null
+++ b/docs/quick-start-react-js.md
@@ -0,0 +1,140 @@
+# Quick Start for React JS
+
+## Installing Reactotron.app
+
+Let’s [download the desktop app](https://github.com/reactotron/reactotron/releases/download/v0.94.0/Reactotron.app.zip) to start. It’s OS X only at this point, but will shortly make available on Windows and Linux.
+
+Unzip & run.
+
+![Installing The App](./images/quick-start-react-js/installing.jpg)
+
+
+## Humble Beginnings
+
+Did you B.Y.O.App? Skip to the next section.
+
+Let’s use [create-react-app](https://github.com/facebookincubator/create-react-app) to bootstrap a new React JS app because, wow, is it fantastic.
+
+Download `create-react-app` if you haven't yet:
+```
+npm i -g create-react-app
+```
+
+Then spin up a brand new React web app.
+```
+create-react-app this-better-work
+cd this-better-work
+```
+
+## Installing Reactotron
+
+Let's install Reactotron as a dev dependency.
+
+```
+npm i --save-dev reactotron-react-js
+```
+
+I like a separate file for initializing. Create `src/ReactotronConfig.js` in your editor of choice and paste this:
+
+```js
+import Reactotron from 'reactotron-react-js'
+
+Reactotron
+ .configure() // we can use plugins here -- more on this later
+ .connect() // let's connect!
+```
+
+Finally, we import this on startup in `src/index.js` on line 1:
+
+```js
+import './ReactotronConfig'
+```
+
+At this point, Reactotron is hooked up.
+
+Refresh your web page (or start it up `npm start`) and have a look at Reactotron now. Do you see the `CONNECTION` line? Click that to expand.
+
+![We Have Contact](./images/quick-start-react-js/first-connect.jpg)
+
+
+Go back to your web page and refresh the web page 5 or 6 times. Now look.
+
+![Chatty](./images/quick-start-react-js/spammy.jpg)
+
+Pretty underwhelming huh?
+
+
+## Hello World
+
+Let's do some classic programming.
+
+Open up `src/App.js`.
+
+At the top, let's put
+
+```js
+import Reactotron from 'reactotron-react-js'
+```
+
+Next, inside the `render()` function, put this as the first line:
+
+```js
+Reactotron.log('hello rendering world')
+```
+
+Save that file and refresh your web page if you don't have live reloading.
+
+Now Reactotron looks like this:
+
+![Hello 1](./images/quick-start-react-js/hello-1.jpg)
+
+While collapsed, the grey area to the right shows a preview. Click to open.
+
+![Hello 2](./images/quick-start-react-js/hello-2.jpg)
+
+Let's change our log statement to:
+
+```js
+Reactotron.log({ numbers: [1, 2, 3], boolean: false, nested: { here: 'we go' } })
+```
+
+Or this
+
+```js
+Reactotron.warn('*glares*')
+```
+
+Or this
+
+```js
+Reactotron.error('Now you\'ve done it.')
+```
+
+Or this
+
+```js
+Reactotron.display({
+ name: 'KNOCK KNOCK',
+ preview: 'Who\'s there?',
+ value: 'Orange.'
+})
+
+Reactotron.display({
+ name: 'ORANGE',
+ preview: 'Who?',
+ value: 'Orange you glad you don\'t know me in real life?',
+ important: true
+})
+```
+
+## Now What?
+
+Well, at this point, we have a complicated version of `console.log`.
+
+Where Reactotron starts to shine is when you start plugging into Redux, tracking global errors, and watching network requests.
+
+Check out our [Demo](../packages/demo-react-js) for more goodies.
+
+![Demo Web App](./images/quick-start-react-js/react-demo-js.jpg)
+
+![Demo Reactotron](./images/quick-start-react-js/react-demo-js-reactotron.jpg)
diff --git a/docs/quick-start-react-native.md b/docs/quick-start-react-native.md
new file mode 100644
index 000000000..b30e145d2
--- /dev/null
+++ b/docs/quick-start-react-native.md
@@ -0,0 +1,140 @@
+# Quick Start for React Native
+
+## Installing Reactotron.app
+
+Let’s [download the desktop app](https://github.com/reactotron/reactotron/releases/download/v0.94.0/Reactotron.app.zip) to start. It’s OS X only at this point, but will shortly make available on Windows and Linux.
+
+Unzip & run.
+
+![Installing The App](./images/quick-start-react-js/installing.jpg)
+
+
+## From Scratch
+
+Let's start a brand new app from scratch. If you want to use your own, skip to the next section.
+
+Download `react-native-cli` if you haven't yet:
+```
+npm i -g react-native-cli
+```
+
+Then spin up a brand new React Native app.
+```
+react-native init ReactotronDemo
+cd ReactotronDemo
+```
+
+You'll need to run this in an emulator for Android or the simulator for iOS. Facebook has some [great guides](http://facebook.github.io/react-native/docs/getting-started.html#content) on getting started.
+
+## Installing Reactotron
+
+Let's install Reactotron as a dev dependency.
+
+```
+npm i --save-dev reactotron-react-native
+```
+
+I like a separate file for initializing. Create `ReactotronConfig.js` in your editor of choice and paste this:
+
+```js
+import Reactotron from 'reactotron-react-native'
+
+Reactotron
+ .configure() // we can use plugins here -- more on this later
+ .connect() // let's connect!
+```
+
+Finally, we import this on startup in `index.ios.js` and `index.android.js` on line 1:
+
+```js
+import './ReactotronConfig'
+```
+
+At this point, Reactotron is hooked up.
+
+Refresh your app (or start it up `react-native start`) and have a look at Reactotron now. Do you see the `CONNECTION` line? Click that to expand.
+
+![We Have Contact](./images/quick-start-react-native/first-connect.jpg)
+
+
+Go back to your app and refresh it 5 or 6 times. Now look.
+
+![Chatty](./images/quick-start-react-native/spammy.jpg)
+
+Pretty underwhelming huh?
+
+
+## Hello World
+
+Let's do some classic programming.
+
+Open up `index.ios.js` or `index.android.js`.
+
+Right after the line you just added in the previous step lets put this:
+
+```js
+import Reactotron from 'reactotron-react-native'
+```
+
+Next, inside the `render()` function, put this as the first line:
+
+```js
+Reactotron.log('hello rendering world')
+```
+
+Save that file and refresh your web page if you don't have live reloading.
+
+Now Reactotron looks like this:
+
+![Hello 1](./images/quick-start-react-native/hello-1.jpg)
+
+While collapsed, the grey area to the right shows a preview. Click to open.
+
+![Hello 2](./images/quick-start-react-native/hello-2.jpg)
+
+Let's change our log statement to:
+
+```js
+Reactotron.log({ numbers: [1, 2, 3], boolean: false, nested: { here: 'we go' } })
+```
+
+Or this
+
+```js
+Reactotron.warn('*glares*')
+```
+
+Or this
+
+```js
+Reactotron.error('Now you\'ve done it.')
+```
+
+Or this
+
+```js
+Reactotron.display({
+ name: 'KNOCK KNOCK',
+ preview: 'Who\'s there?',
+ value: 'Orange.'
+})
+
+Reactotron.display({
+ name: 'ORANGE',
+ preview: 'Who?',
+ value: 'Orange you glad you don\'t know me in real life?',
+ important: true
+})
+```
+
+## Now What?
+
+Well, at this point, we have a complicated version of `console.log`.
+
+Where Reactotron starts to shine is when you start plugging into Redux, tracking global errors, and watching network requests.
+
+Check out our [Demo](../packages/demo-react-native) for more goodies.
+
+![Demo Native App](./images/quick-start-react-native/react-demo-native.jpg)
+
+![Demo Reactotron](./images/quick-start-react-native/react-demo-native-reactotron.jpg)
diff --git a/examples/ReactNativeExample/App/Config/ReactotronConfig.js b/examples/ReactNativeExample/App/Config/ReactotronConfig.js
deleted file mode 100644
index 8c0602061..000000000
--- a/examples/ReactNativeExample/App/Config/ReactotronConfig.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import Reactotron from '../../client' // in a real app, you would use 'reactotron'
-import {Platform} from 'react-native'
-
-Reactotron.connect({
- enabled: true,
- name: 'ReactNativeExample',
- userAgent: Platform.OS
-})
-
diff --git a/examples/ReactNativeExample/App/Containers/RootContainer.js b/examples/ReactNativeExample/App/Containers/RootContainer.js
deleted file mode 100644
index c0f644e9b..000000000
--- a/examples/ReactNativeExample/App/Containers/RootContainer.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import React, { View, Text, TouchableOpacity } from 'react-native'
-import { connect } from 'react-redux'
-import Actions from '../Actions/Creators'
-import Styles from './Styles/RootContainerStyles'
-import Reactotron from '../../client' // in a real app, you would use 'reactotron'
-
-export default class RootContainer extends React.Component {
-
- constructor (props) {
- super(props)
- this.handlePress = this.handlePress.bind(this)
- }
-
- handlePress () {
- const {dispatch} = this.props
- Reactotron.log('A touchable was pressed.🔥🦄')
- dispatch(Actions.requestTemperature('Toronto'))
- }
-
- componentWillMount () {
- const { dispatch } = this.props
- dispatch(Actions.startup())
- }
-
-// componentDidMount () {
-// navigator.geolocation.getCurrentPosition(
-// (position) => Reactotron.log(position),
-// (error) => alert(error.message),
-// {enableHighAccuracy: true, timeout: 20000, maximumAge: 1000}
-// )
-// }
-
- render () {
- const {city, temperature, fetching} = this.props
- return (
-
-
-
- {`The weather in ${city} is ${fetching ? 'loading' : temperature}.`}
-
-
-
- )
- }
-
-}
-
-const mapStateToProps = (state) => {
- return {
- city: state.weather.city,
- temperature: state.weather.temperature,
- fetching: state.weather.fetching
- }
-}
-
-export default connect(mapStateToProps)(RootContainer)
-
diff --git a/examples/ReactNativeExample/App/Sagas/WeatherSaga.js b/examples/ReactNativeExample/App/Sagas/WeatherSaga.js
deleted file mode 100644
index e85a28ca4..000000000
--- a/examples/ReactNativeExample/App/Sagas/WeatherSaga.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import { take, call, put } from 'redux-saga/effects'
-import RS from 'ramdasauce'
-import Types from '../Actions/Types'
-import Actions from '../Actions/Creators'
-import Reactotron from '../../client' // normally you would use 'reactotron'
-
-const SCALE = 'C'
-
-export function * getWeather (api, city) {
- const bench = Reactotron.bench('weather check')
- const response = yield call(api.get, '/find/name', { q: city })
- bench.step('after api')
- if (response.ok) {
- const kelvin = RS.dotPath('data.list.0.main.temp', response)
- const celcius = kelvin - 273.15
- const farenheit = (celcius * 1.8000) + 32
-
- bench.step('after mathy things')
- if (SCALE === 'F') {
- yield put(Actions.receiveTemperature(Math.round(farenheit)))
- } else {
- yield put(Actions.receiveTemperature(Math.round(celcius)))
- }
- } else {
- yield put(Actions.receiveTemperatureFailure())
- }
- bench.stop()
-}
-
-export default (api) => {
- function * watchWeatherRequest () {
- while (true) {
- const action = yield take(Types.TEMPERATURE_REQUEST)
- const { city } = action
- yield call(getWeather, api, city)
- }
- }
-
- return {
- watchWeatherRequest
- }
-}
diff --git a/examples/ReactNativeExample/App/Store/Store.js b/examples/ReactNativeExample/App/Store/Store.js
deleted file mode 100644
index f1f05dc9f..000000000
--- a/examples/ReactNativeExample/App/Store/Store.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import {createStore, applyMiddleware, compose} from 'redux'
-import Config from '../Config/Config'
-import createLogger from 'redux-logger'
-import rootReducer from '../Reducers/'
-import sagaMiddleware from 'redux-saga'
-import sagas from '../Sagas/'
-import R from 'ramda'
-import Reactotron from '../../client' // in a real app, you would use 'reactotron'
-
-// the logger master switch
-const USE_LOGGING = Config.sagas.logging
-// silence these saga-based messages
-const SAGA_LOGGING_BLACKLIST = ['EFFECT_TRIGGERED', 'EFFECT_RESOLVED', 'EFFECT_REJECTED']
-// creat the logger
-const logger = createLogger({
- predicate: (getState, { type }) => USE_LOGGING && R.not(R.contains(type, SAGA_LOGGING_BLACKLIST)),
- stateTransformer: (state) => R.map((v) => v.asMutable({deep: true}), state)
-})
-
-// a function which can create our store and auto-persist the data
-export default () => {
- const enhancer = compose(
- applyMiddleware(
- logger,
- sagaMiddleware(...sagas)
- ),
- Reactotron.storeEnhancer()
- )
-
- return createStore(rootReducer, enhancer)
-}
diff --git a/examples/ReactNativeWebExample/App/Containers/Styles/RootContainerStyles.js b/examples/ReactNativeWebExample/App/Containers/Styles/RootContainerStyles.js
deleted file mode 100644
index ad9513e1e..000000000
--- a/examples/ReactNativeWebExample/App/Containers/Styles/RootContainerStyles.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import {StyleSheet} from 'react-native'
-
-export default StyleSheet.create({
- container: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center'
- },
- welcome: {
- textAlign: 'center',
- padding: 50,
- fontSize: 25
- }
-})
diff --git a/examples/ReactNativeWebExample/App/Reducers/CreateReducer.js b/examples/ReactNativeWebExample/App/Reducers/CreateReducer.js
deleted file mode 100644
index 42ce745f1..000000000
--- a/examples/ReactNativeWebExample/App/Reducers/CreateReducer.js
+++ /dev/null
@@ -1,11 +0,0 @@
-/**
- Creates a reducer.
- @param {string} initialState - The initial state for this reducer.
- @param {object} handlers - Keys are action types (strings), values are reducers (functions).
- @return {object} A reducer object.
- */
-export default (initialState = null, handlers = {}) => (state = initialState, action) => {
- if (!action && !action.type) return state
- const handler = handlers[action.type]
- return handler && handler(state, action) || state
-}
diff --git a/examples/ReactNativeWebExample/App/Reducers/WeatherReducer.js b/examples/ReactNativeWebExample/App/Reducers/WeatherReducer.js
deleted file mode 100644
index a1bdaf75b..000000000
--- a/examples/ReactNativeWebExample/App/Reducers/WeatherReducer.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import Types from '../Actions/Types'
-import Immutable from 'seamless-immutable'
-import createReducer from './CreateReducer'
-
-export const INITIAL_STATE = Immutable({
- temperature: null,
- fetching: false,
- error: null,
- city: null
-})
-
-// request temp
-const request = (state, action) =>
- state.merge({
- fetching: true,
- city: action.city,
- temperature: null
- })
-
-// receive temp
-const receive = (state, action) =>
- state.merge({
- fetching: false,
- error: null,
- temperature: action.temperature
- })
-
-// temp failure
-const failure = (state, action) =>
- state.merge({
- fetching: false,
- error: true,
- temperature: null
- })
-
-// map our types to our handlers
-const ACTION_HANDLERS = {
- [Types.TEMPERATURE_REQUEST]: request,
- [Types.TEMPERATURE_RECEIVE]: receive,
- [Types.TEMPERATURE_FAILURE]: failure
-}
-
-export default createReducer(INITIAL_STATE, ACTION_HANDLERS)
diff --git a/examples/ReactNativeWebExample/App/Reducers/index.js b/examples/ReactNativeWebExample/App/Reducers/index.js
deleted file mode 100644
index 78733fef3..000000000
--- a/examples/ReactNativeWebExample/App/Reducers/index.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import { combineReducers } from 'redux'
-import WeatherReducer from './WeatherReducer'
-
-// glue all the reducers together into 1 root reducer
-export default combineReducers({
- weather: WeatherReducer
-})
diff --git a/examples/ReactNativeWebExample/App/Sagas/StartupSaga.js b/examples/ReactNativeWebExample/App/Sagas/StartupSaga.js
deleted file mode 100644
index 98ded7b15..000000000
--- a/examples/ReactNativeWebExample/App/Sagas/StartupSaga.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import { take, put, select } from 'redux-saga/effects'
-import Types from '../Actions/Types'
-import Actions from '../Actions/Creators'
-import R from 'ramda'
-
-// process STARTUP actions
-export function * watchStartup () {
- while (true) {
- yield take(Types.STARTUP)
- const temp = yield select((state) => state.weather.temperature)
- // only fetch new temps when we don't have one yet
- if (!R.is(Number, temp)) {
- yield put(Actions.requestTemperature('San Francisco'))
- }
- }
-}
diff --git a/examples/ReactNativeWebExample/App/Sagas/index.js b/examples/ReactNativeWebExample/App/Sagas/index.js
deleted file mode 100644
index 0006d30f3..000000000
--- a/examples/ReactNativeWebExample/App/Sagas/index.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import ApiSauce from 'apisauce'
-import { watchStartup } from './StartupSaga'
-import WeatherSaga from './WeatherSaga'
-import Reactotron from '../../client' // in a real app, you would use 'reactotron'
-
-const api = ApiSauce.create({
- baseURL: 'http://openweathermap.org/data/2.1'
-})
-api.addMonitor((response) => {
- Reactotron.apiLog(response)
-})
-
-const {watchWeatherRequest} = WeatherSaga(api)
-
-// start the daemons
-export default [
- watchStartup,
- watchWeatherRequest
-]
diff --git a/.babelrc b/legacy/.babelrc
similarity index 100%
rename from .babelrc
rename to legacy/.babelrc
diff --git a/legacy/.gitignore b/legacy/.gitignore
new file mode 100644
index 000000000..88602cb40
--- /dev/null
+++ b/legacy/.gitignore
@@ -0,0 +1,5 @@
+npm-debug.log
+node_modules
+docs
+.tern-project
+.tern-config
diff --git a/.jsdoc.json b/legacy/.jsdoc.json
similarity index 100%
rename from .jsdoc.json
rename to legacy/.jsdoc.json
diff --git a/.npmignore b/legacy/.npmignore
similarity index 100%
rename from .npmignore
rename to legacy/.npmignore
diff --git a/CHANGES.md b/legacy/CHANGES.md
similarity index 100%
rename from CHANGES.md
rename to legacy/CHANGES.md
diff --git a/README.md b/legacy/README.md
similarity index 100%
rename from README.md
rename to legacy/README.md
diff --git a/bin/reactotron.js b/legacy/bin/reactotron.js
similarity index 100%
rename from bin/reactotron.js
rename to legacy/bin/reactotron.js
diff --git a/bump_client.sh b/legacy/bump_client.sh
similarity index 100%
rename from bump_client.sh
rename to legacy/bump_client.sh
diff --git a/client/client.js b/legacy/client/client.js
similarity index 100%
rename from client/client.js
rename to legacy/client/client.js
diff --git a/dist/client.js b/legacy/dist/client.js
similarity index 100%
rename from dist/client.js
rename to legacy/dist/client.js
diff --git a/dist/index.js b/legacy/dist/index.js
similarity index 100%
rename from dist/index.js
rename to legacy/dist/index.js
diff --git a/examples/README.md b/legacy/examples/README.md
similarity index 100%
rename from examples/README.md
rename to legacy/examples/README.md
diff --git a/examples/ReactDomExample/.gitignore b/legacy/examples/ReactDomExample/.gitignore
similarity index 100%
rename from examples/ReactDomExample/.gitignore
rename to legacy/examples/ReactDomExample/.gitignore
diff --git a/examples/ReactDomExample/App/Actions/CreateAction.js b/legacy/examples/ReactDomExample/App/Actions/CreateAction.js
similarity index 100%
rename from examples/ReactDomExample/App/Actions/CreateAction.js
rename to legacy/examples/ReactDomExample/App/Actions/CreateAction.js
diff --git a/examples/ReactDomExample/App/Actions/Creators.js b/legacy/examples/ReactDomExample/App/Actions/Creators.js
similarity index 100%
rename from examples/ReactDomExample/App/Actions/Creators.js
rename to legacy/examples/ReactDomExample/App/Actions/Creators.js
diff --git a/examples/ReactDomExample/App/Actions/Types.js b/legacy/examples/ReactDomExample/App/Actions/Types.js
similarity index 100%
rename from examples/ReactDomExample/App/Actions/Types.js
rename to legacy/examples/ReactDomExample/App/Actions/Types.js
diff --git a/examples/ReactDomExample/App/App.js b/legacy/examples/ReactDomExample/App/App.js
similarity index 100%
rename from examples/ReactDomExample/App/App.js
rename to legacy/examples/ReactDomExample/App/App.js
diff --git a/examples/ReactDomExample/App/Config/Config.js b/legacy/examples/ReactDomExample/App/Config/Config.js
similarity index 100%
rename from examples/ReactDomExample/App/Config/Config.js
rename to legacy/examples/ReactDomExample/App/Config/Config.js
diff --git a/examples/ReactDomExample/App/Config/ReactotronConfig.js b/legacy/examples/ReactDomExample/App/Config/ReactotronConfig.js
similarity index 100%
rename from examples/ReactDomExample/App/Config/ReactotronConfig.js
rename to legacy/examples/ReactDomExample/App/Config/ReactotronConfig.js
diff --git a/examples/ReactDomExample/App/Reducers/CreateReducer.js b/legacy/examples/ReactDomExample/App/Reducers/CreateReducer.js
similarity index 100%
rename from examples/ReactDomExample/App/Reducers/CreateReducer.js
rename to legacy/examples/ReactDomExample/App/Reducers/CreateReducer.js
diff --git a/examples/ReactDomExample/App/Reducers/GithubReducer.js b/legacy/examples/ReactDomExample/App/Reducers/GithubReducer.js
similarity index 100%
rename from examples/ReactDomExample/App/Reducers/GithubReducer.js
rename to legacy/examples/ReactDomExample/App/Reducers/GithubReducer.js
diff --git a/examples/ReactDomExample/App/Reducers/index.js b/legacy/examples/ReactDomExample/App/Reducers/index.js
similarity index 100%
rename from examples/ReactDomExample/App/Reducers/index.js
rename to legacy/examples/ReactDomExample/App/Reducers/index.js
diff --git a/examples/ReactDomExample/App/Root.js b/legacy/examples/ReactDomExample/App/Root.js
similarity index 100%
rename from examples/ReactDomExample/App/Root.js
rename to legacy/examples/ReactDomExample/App/Root.js
diff --git a/examples/ReactDomExample/App/Sagas/GithubSaga.js b/legacy/examples/ReactDomExample/App/Sagas/GithubSaga.js
similarity index 100%
rename from examples/ReactDomExample/App/Sagas/GithubSaga.js
rename to legacy/examples/ReactDomExample/App/Sagas/GithubSaga.js
diff --git a/examples/ReactDomExample/App/Sagas/StartupSaga.js b/legacy/examples/ReactDomExample/App/Sagas/StartupSaga.js
similarity index 100%
rename from examples/ReactDomExample/App/Sagas/StartupSaga.js
rename to legacy/examples/ReactDomExample/App/Sagas/StartupSaga.js
diff --git a/examples/ReactDomExample/App/Sagas/index.js b/legacy/examples/ReactDomExample/App/Sagas/index.js
similarity index 100%
rename from examples/ReactDomExample/App/Sagas/index.js
rename to legacy/examples/ReactDomExample/App/Sagas/index.js
diff --git a/examples/ReactDomExample/App/Store/Store.js b/legacy/examples/ReactDomExample/App/Store/Store.js
similarity index 100%
rename from examples/ReactDomExample/App/Store/Store.js
rename to legacy/examples/ReactDomExample/App/Store/Store.js
diff --git a/examples/ReactDomExample/App/index.js b/legacy/examples/ReactDomExample/App/index.js
similarity index 100%
rename from examples/ReactDomExample/App/index.js
rename to legacy/examples/ReactDomExample/App/index.js
diff --git a/examples/ReactDomExample/index.html b/legacy/examples/ReactDomExample/index.html
similarity index 100%
rename from examples/ReactDomExample/index.html
rename to legacy/examples/ReactDomExample/index.html
diff --git a/examples/ReactDomExample/package.json b/legacy/examples/ReactDomExample/package.json
similarity index 100%
rename from examples/ReactDomExample/package.json
rename to legacy/examples/ReactDomExample/package.json
diff --git a/examples/ReactDomExample/server.js b/legacy/examples/ReactDomExample/server.js
similarity index 100%
rename from examples/ReactDomExample/server.js
rename to legacy/examples/ReactDomExample/server.js
diff --git a/examples/ReactDomExample/webpack.config.js b/legacy/examples/ReactDomExample/webpack.config.js
similarity index 100%
rename from examples/ReactDomExample/webpack.config.js
rename to legacy/examples/ReactDomExample/webpack.config.js
diff --git a/examples/ReactNativeExample/.buckconfig b/legacy/examples/ReactNativeExample/.buckconfig
similarity index 100%
rename from examples/ReactNativeExample/.buckconfig
rename to legacy/examples/ReactNativeExample/.buckconfig
diff --git a/examples/ReactNativeExample/.flowconfig b/legacy/examples/ReactNativeExample/.flowconfig
similarity index 100%
rename from examples/ReactNativeExample/.flowconfig
rename to legacy/examples/ReactNativeExample/.flowconfig
diff --git a/examples/ReactNativeExample/.gitignore b/legacy/examples/ReactNativeExample/.gitignore
similarity index 100%
rename from examples/ReactNativeExample/.gitignore
rename to legacy/examples/ReactNativeExample/.gitignore
diff --git a/examples/ReactNativeExample/.watchmanconfig b/legacy/examples/ReactNativeExample/.watchmanconfig
similarity index 100%
rename from examples/ReactNativeExample/.watchmanconfig
rename to legacy/examples/ReactNativeExample/.watchmanconfig
diff --git a/examples/ReactNativeExample/android/app/BUCK b/legacy/examples/ReactNativeExample/android/app/BUCK
similarity index 100%
rename from examples/ReactNativeExample/android/app/BUCK
rename to legacy/examples/ReactNativeExample/android/app/BUCK
diff --git a/examples/ReactNativeExample/android/app/build.gradle b/legacy/examples/ReactNativeExample/android/app/build.gradle
similarity index 100%
rename from examples/ReactNativeExample/android/app/build.gradle
rename to legacy/examples/ReactNativeExample/android/app/build.gradle
diff --git a/examples/ReactNativeExample/android/app/proguard-rules.pro b/legacy/examples/ReactNativeExample/android/app/proguard-rules.pro
similarity index 100%
rename from examples/ReactNativeExample/android/app/proguard-rules.pro
rename to legacy/examples/ReactNativeExample/android/app/proguard-rules.pro
diff --git a/examples/ReactNativeExample/android/app/src/main/AndroidManifest.xml b/legacy/examples/ReactNativeExample/android/app/src/main/AndroidManifest.xml
similarity index 100%
rename from examples/ReactNativeExample/android/app/src/main/AndroidManifest.xml
rename to legacy/examples/ReactNativeExample/android/app/src/main/AndroidManifest.xml
diff --git a/examples/ReactNativeExample/android/app/src/main/java/com/reactnativeexample/MainActivity.java b/legacy/examples/ReactNativeExample/android/app/src/main/java/com/reactnativeexample/MainActivity.java
similarity index 100%
rename from examples/ReactNativeExample/android/app/src/main/java/com/reactnativeexample/MainActivity.java
rename to legacy/examples/ReactNativeExample/android/app/src/main/java/com/reactnativeexample/MainActivity.java
diff --git a/examples/ReactNativeExample/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/legacy/examples/ReactNativeExample/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
similarity index 100%
rename from examples/ReactNativeExample/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
rename to legacy/examples/ReactNativeExample/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
diff --git a/examples/ReactNativeExample/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/legacy/examples/ReactNativeExample/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
similarity index 100%
rename from examples/ReactNativeExample/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
rename to legacy/examples/ReactNativeExample/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
diff --git a/examples/ReactNativeExample/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/legacy/examples/ReactNativeExample/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
similarity index 100%
rename from examples/ReactNativeExample/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
rename to legacy/examples/ReactNativeExample/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
diff --git a/examples/ReactNativeExample/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/legacy/examples/ReactNativeExample/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
similarity index 100%
rename from examples/ReactNativeExample/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
rename to legacy/examples/ReactNativeExample/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
diff --git a/examples/ReactNativeExample/android/app/src/main/res/values/strings.xml b/legacy/examples/ReactNativeExample/android/app/src/main/res/values/strings.xml
similarity index 100%
rename from examples/ReactNativeExample/android/app/src/main/res/values/strings.xml
rename to legacy/examples/ReactNativeExample/android/app/src/main/res/values/strings.xml
diff --git a/examples/ReactNativeExample/android/app/src/main/res/values/styles.xml b/legacy/examples/ReactNativeExample/android/app/src/main/res/values/styles.xml
similarity index 100%
rename from examples/ReactNativeExample/android/app/src/main/res/values/styles.xml
rename to legacy/examples/ReactNativeExample/android/app/src/main/res/values/styles.xml
diff --git a/examples/ReactNativeExample/android/build.gradle b/legacy/examples/ReactNativeExample/android/build.gradle
similarity index 100%
rename from examples/ReactNativeExample/android/build.gradle
rename to legacy/examples/ReactNativeExample/android/build.gradle
diff --git a/examples/ReactNativeExample/android/gradle.properties b/legacy/examples/ReactNativeExample/android/gradle.properties
similarity index 100%
rename from examples/ReactNativeExample/android/gradle.properties
rename to legacy/examples/ReactNativeExample/android/gradle.properties
diff --git a/examples/ReactNativeExample/android/gradle/wrapper/gradle-wrapper.jar b/legacy/examples/ReactNativeExample/android/gradle/wrapper/gradle-wrapper.jar
similarity index 100%
rename from examples/ReactNativeExample/android/gradle/wrapper/gradle-wrapper.jar
rename to legacy/examples/ReactNativeExample/android/gradle/wrapper/gradle-wrapper.jar
diff --git a/examples/ReactNativeExample/android/gradle/wrapper/gradle-wrapper.properties b/legacy/examples/ReactNativeExample/android/gradle/wrapper/gradle-wrapper.properties
similarity index 100%
rename from examples/ReactNativeExample/android/gradle/wrapper/gradle-wrapper.properties
rename to legacy/examples/ReactNativeExample/android/gradle/wrapper/gradle-wrapper.properties
diff --git a/examples/ReactNativeExample/android/gradlew b/legacy/examples/ReactNativeExample/android/gradlew
similarity index 100%
rename from examples/ReactNativeExample/android/gradlew
rename to legacy/examples/ReactNativeExample/android/gradlew
diff --git a/examples/ReactNativeExample/android/gradlew.bat b/legacy/examples/ReactNativeExample/android/gradlew.bat
similarity index 100%
rename from examples/ReactNativeExample/android/gradlew.bat
rename to legacy/examples/ReactNativeExample/android/gradlew.bat
diff --git a/examples/ReactNativeExample/android/keystores/BUCK b/legacy/examples/ReactNativeExample/android/keystores/BUCK
similarity index 100%
rename from examples/ReactNativeExample/android/keystores/BUCK
rename to legacy/examples/ReactNativeExample/android/keystores/BUCK
diff --git a/examples/ReactNativeExample/android/keystores/debug.keystore.properties b/legacy/examples/ReactNativeExample/android/keystores/debug.keystore.properties
similarity index 100%
rename from examples/ReactNativeExample/android/keystores/debug.keystore.properties
rename to legacy/examples/ReactNativeExample/android/keystores/debug.keystore.properties
diff --git a/examples/ReactNativeExample/android/settings.gradle b/legacy/examples/ReactNativeExample/android/settings.gradle
similarity index 100%
rename from examples/ReactNativeExample/android/settings.gradle
rename to legacy/examples/ReactNativeExample/android/settings.gradle
diff --git a/examples/ReactNativeExample/ios/ReactNativeExample.xcodeproj/project.pbxproj b/legacy/examples/ReactNativeExample/ios/ReactNativeExample.xcodeproj/project.pbxproj
similarity index 100%
rename from examples/ReactNativeExample/ios/ReactNativeExample.xcodeproj/project.pbxproj
rename to legacy/examples/ReactNativeExample/ios/ReactNativeExample.xcodeproj/project.pbxproj
diff --git a/examples/ReactNativeExample/ios/ReactNativeExample.xcodeproj/xcshareddata/xcschemes/ReactNativeExample.xcscheme b/legacy/examples/ReactNativeExample/ios/ReactNativeExample.xcodeproj/xcshareddata/xcschemes/ReactNativeExample.xcscheme
similarity index 100%
rename from examples/ReactNativeExample/ios/ReactNativeExample.xcodeproj/xcshareddata/xcschemes/ReactNativeExample.xcscheme
rename to legacy/examples/ReactNativeExample/ios/ReactNativeExample.xcodeproj/xcshareddata/xcschemes/ReactNativeExample.xcscheme
diff --git a/examples/ReactNativeExample/ios/ReactNativeExample/AppDelegate.h b/legacy/examples/ReactNativeExample/ios/ReactNativeExample/AppDelegate.h
similarity index 100%
rename from examples/ReactNativeExample/ios/ReactNativeExample/AppDelegate.h
rename to legacy/examples/ReactNativeExample/ios/ReactNativeExample/AppDelegate.h
diff --git a/examples/ReactNativeExample/ios/ReactNativeExample/AppDelegate.m b/legacy/examples/ReactNativeExample/ios/ReactNativeExample/AppDelegate.m
similarity index 100%
rename from examples/ReactNativeExample/ios/ReactNativeExample/AppDelegate.m
rename to legacy/examples/ReactNativeExample/ios/ReactNativeExample/AppDelegate.m
diff --git a/examples/ReactNativeExample/ios/ReactNativeExample/Base.lproj/LaunchScreen.xib b/legacy/examples/ReactNativeExample/ios/ReactNativeExample/Base.lproj/LaunchScreen.xib
similarity index 100%
rename from examples/ReactNativeExample/ios/ReactNativeExample/Base.lproj/LaunchScreen.xib
rename to legacy/examples/ReactNativeExample/ios/ReactNativeExample/Base.lproj/LaunchScreen.xib
diff --git a/examples/ReactNativeExample/ios/ReactNativeExample/Images.xcassets/AppIcon.appiconset/Contents.json b/legacy/examples/ReactNativeExample/ios/ReactNativeExample/Images.xcassets/AppIcon.appiconset/Contents.json
similarity index 100%
rename from examples/ReactNativeExample/ios/ReactNativeExample/Images.xcassets/AppIcon.appiconset/Contents.json
rename to legacy/examples/ReactNativeExample/ios/ReactNativeExample/Images.xcassets/AppIcon.appiconset/Contents.json
diff --git a/examples/ReactNativeExample/ios/ReactNativeExample/Info.plist b/legacy/examples/ReactNativeExample/ios/ReactNativeExample/Info.plist
similarity index 100%
rename from examples/ReactNativeExample/ios/ReactNativeExample/Info.plist
rename to legacy/examples/ReactNativeExample/ios/ReactNativeExample/Info.plist
diff --git a/examples/ReactNativeExample/ios/ReactNativeExample/main.m b/legacy/examples/ReactNativeExample/ios/ReactNativeExample/main.m
similarity index 100%
rename from examples/ReactNativeExample/ios/ReactNativeExample/main.m
rename to legacy/examples/ReactNativeExample/ios/ReactNativeExample/main.m
diff --git a/examples/ReactNativeExample/ios/ReactNativeExampleTests/Info.plist b/legacy/examples/ReactNativeExample/ios/ReactNativeExampleTests/Info.plist
similarity index 100%
rename from examples/ReactNativeExample/ios/ReactNativeExampleTests/Info.plist
rename to legacy/examples/ReactNativeExample/ios/ReactNativeExampleTests/Info.plist
diff --git a/examples/ReactNativeExample/ios/ReactNativeExampleTests/ReactNativeExampleTests.m b/legacy/examples/ReactNativeExample/ios/ReactNativeExampleTests/ReactNativeExampleTests.m
similarity index 100%
rename from examples/ReactNativeExample/ios/ReactNativeExampleTests/ReactNativeExampleTests.m
rename to legacy/examples/ReactNativeExample/ios/ReactNativeExampleTests/ReactNativeExampleTests.m
diff --git a/examples/ReactNativeExample/package.json b/legacy/examples/ReactNativeExample/package.json
similarity index 100%
rename from examples/ReactNativeExample/package.json
rename to legacy/examples/ReactNativeExample/package.json
diff --git a/examples/ReactNativeWebExample/.buckconfig b/legacy/examples/ReactNativeWebExample/.buckconfig
similarity index 100%
rename from examples/ReactNativeWebExample/.buckconfig
rename to legacy/examples/ReactNativeWebExample/.buckconfig
diff --git a/examples/ReactNativeWebExample/.flowconfig b/legacy/examples/ReactNativeWebExample/.flowconfig
similarity index 100%
rename from examples/ReactNativeWebExample/.flowconfig
rename to legacy/examples/ReactNativeWebExample/.flowconfig
diff --git a/examples/ReactNativeWebExample/.gitignore b/legacy/examples/ReactNativeWebExample/.gitignore
similarity index 100%
rename from examples/ReactNativeWebExample/.gitignore
rename to legacy/examples/ReactNativeWebExample/.gitignore
diff --git a/examples/ReactNativeWebExample/.watchmanconfig b/legacy/examples/ReactNativeWebExample/.watchmanconfig
similarity index 100%
rename from examples/ReactNativeWebExample/.watchmanconfig
rename to legacy/examples/ReactNativeWebExample/.watchmanconfig
diff --git a/examples/ReactNativeExample/App/Actions/CreateAction.js b/legacy/examples/ReactNativeWebExample/App/Actions/CreateAction.js
similarity index 100%
rename from examples/ReactNativeExample/App/Actions/CreateAction.js
rename to legacy/examples/ReactNativeWebExample/App/Actions/CreateAction.js
diff --git a/examples/ReactNativeExample/App/Actions/Creators.js b/legacy/examples/ReactNativeWebExample/App/Actions/Creators.js
similarity index 100%
rename from examples/ReactNativeExample/App/Actions/Creators.js
rename to legacy/examples/ReactNativeWebExample/App/Actions/Creators.js
diff --git a/examples/ReactNativeWebExample/App/Actions/Types.js b/legacy/examples/ReactNativeWebExample/App/Actions/Types.js
similarity index 100%
rename from examples/ReactNativeWebExample/App/Actions/Types.js
rename to legacy/examples/ReactNativeWebExample/App/Actions/Types.js
diff --git a/examples/ReactNativeExample/App/Config/Config.js b/legacy/examples/ReactNativeWebExample/App/Config/Config.js
similarity index 100%
rename from examples/ReactNativeExample/App/Config/Config.js
rename to legacy/examples/ReactNativeWebExample/App/Config/Config.js
diff --git a/examples/ReactNativeWebExample/App/Config/ReactotronConfig.js b/legacy/examples/ReactNativeWebExample/App/Config/ReactotronConfig.js
similarity index 100%
rename from examples/ReactNativeWebExample/App/Config/ReactotronConfig.js
rename to legacy/examples/ReactNativeWebExample/App/Config/ReactotronConfig.js
diff --git a/examples/ReactNativeExample/App/Containers/ReduxContainer.js b/legacy/examples/ReactNativeWebExample/App/Containers/ReduxContainer.js
similarity index 100%
rename from examples/ReactNativeExample/App/Containers/ReduxContainer.js
rename to legacy/examples/ReactNativeWebExample/App/Containers/ReduxContainer.js
diff --git a/examples/ReactNativeWebExample/App/Containers/RootContainer.js b/legacy/examples/ReactNativeWebExample/App/Containers/RootContainer.js
similarity index 100%
rename from examples/ReactNativeWebExample/App/Containers/RootContainer.js
rename to legacy/examples/ReactNativeWebExample/App/Containers/RootContainer.js
diff --git a/examples/ReactNativeExample/App/Containers/Styles/RootContainerStyles.js b/legacy/examples/ReactNativeWebExample/App/Containers/Styles/RootContainerStyles.js
similarity index 100%
rename from examples/ReactNativeExample/App/Containers/Styles/RootContainerStyles.js
rename to legacy/examples/ReactNativeWebExample/App/Containers/Styles/RootContainerStyles.js
diff --git a/examples/ReactNativeExample/App/Reducers/CreateReducer.js b/legacy/examples/ReactNativeWebExample/App/Reducers/CreateReducer.js
similarity index 100%
rename from examples/ReactNativeExample/App/Reducers/CreateReducer.js
rename to legacy/examples/ReactNativeWebExample/App/Reducers/CreateReducer.js
diff --git a/examples/ReactNativeExample/App/Reducers/WeatherReducer.js b/legacy/examples/ReactNativeWebExample/App/Reducers/WeatherReducer.js
similarity index 100%
rename from examples/ReactNativeExample/App/Reducers/WeatherReducer.js
rename to legacy/examples/ReactNativeWebExample/App/Reducers/WeatherReducer.js
diff --git a/examples/ReactNativeExample/App/Reducers/index.js b/legacy/examples/ReactNativeWebExample/App/Reducers/index.js
similarity index 100%
rename from examples/ReactNativeExample/App/Reducers/index.js
rename to legacy/examples/ReactNativeWebExample/App/Reducers/index.js
diff --git a/examples/ReactNativeExample/App/Sagas/StartupSaga.js b/legacy/examples/ReactNativeWebExample/App/Sagas/StartupSaga.js
similarity index 100%
rename from examples/ReactNativeExample/App/Sagas/StartupSaga.js
rename to legacy/examples/ReactNativeWebExample/App/Sagas/StartupSaga.js
diff --git a/examples/ReactNativeWebExample/App/Sagas/WeatherSaga.js b/legacy/examples/ReactNativeWebExample/App/Sagas/WeatherSaga.js
similarity index 100%
rename from examples/ReactNativeWebExample/App/Sagas/WeatherSaga.js
rename to legacy/examples/ReactNativeWebExample/App/Sagas/WeatherSaga.js
diff --git a/examples/ReactNativeExample/App/Sagas/index.js b/legacy/examples/ReactNativeWebExample/App/Sagas/index.js
similarity index 100%
rename from examples/ReactNativeExample/App/Sagas/index.js
rename to legacy/examples/ReactNativeWebExample/App/Sagas/index.js
diff --git a/examples/ReactNativeWebExample/App/Store/Store.js b/legacy/examples/ReactNativeWebExample/App/Store/Store.js
similarity index 100%
rename from examples/ReactNativeWebExample/App/Store/Store.js
rename to legacy/examples/ReactNativeWebExample/App/Store/Store.js
diff --git a/examples/ReactNativeWebExample/android/app/BUCK b/legacy/examples/ReactNativeWebExample/android/app/BUCK
similarity index 100%
rename from examples/ReactNativeWebExample/android/app/BUCK
rename to legacy/examples/ReactNativeWebExample/android/app/BUCK
diff --git a/examples/ReactNativeWebExample/android/app/build.gradle b/legacy/examples/ReactNativeWebExample/android/app/build.gradle
similarity index 100%
rename from examples/ReactNativeWebExample/android/app/build.gradle
rename to legacy/examples/ReactNativeWebExample/android/app/build.gradle
diff --git a/examples/ReactNativeWebExample/android/app/proguard-rules.pro b/legacy/examples/ReactNativeWebExample/android/app/proguard-rules.pro
similarity index 100%
rename from examples/ReactNativeWebExample/android/app/proguard-rules.pro
rename to legacy/examples/ReactNativeWebExample/android/app/proguard-rules.pro
diff --git a/examples/ReactNativeWebExample/android/app/src/main/AndroidManifest.xml b/legacy/examples/ReactNativeWebExample/android/app/src/main/AndroidManifest.xml
similarity index 100%
rename from examples/ReactNativeWebExample/android/app/src/main/AndroidManifest.xml
rename to legacy/examples/ReactNativeWebExample/android/app/src/main/AndroidManifest.xml
diff --git a/examples/ReactNativeWebExample/android/app/src/main/java/com/reactnativeexample/MainActivity.java b/legacy/examples/ReactNativeWebExample/android/app/src/main/java/com/reactnativeexample/MainActivity.java
similarity index 100%
rename from examples/ReactNativeWebExample/android/app/src/main/java/com/reactnativeexample/MainActivity.java
rename to legacy/examples/ReactNativeWebExample/android/app/src/main/java/com/reactnativeexample/MainActivity.java
diff --git a/examples/ReactNativeWebExample/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/legacy/examples/ReactNativeWebExample/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
similarity index 100%
rename from examples/ReactNativeWebExample/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
rename to legacy/examples/ReactNativeWebExample/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
diff --git a/examples/ReactNativeWebExample/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/legacy/examples/ReactNativeWebExample/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
similarity index 100%
rename from examples/ReactNativeWebExample/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
rename to legacy/examples/ReactNativeWebExample/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
diff --git a/examples/ReactNativeWebExample/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/legacy/examples/ReactNativeWebExample/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
similarity index 100%
rename from examples/ReactNativeWebExample/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
rename to legacy/examples/ReactNativeWebExample/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
diff --git a/examples/ReactNativeWebExample/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/legacy/examples/ReactNativeWebExample/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
similarity index 100%
rename from examples/ReactNativeWebExample/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
rename to legacy/examples/ReactNativeWebExample/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
diff --git a/examples/ReactNativeWebExample/android/app/src/main/res/values/strings.xml b/legacy/examples/ReactNativeWebExample/android/app/src/main/res/values/strings.xml
similarity index 100%
rename from examples/ReactNativeWebExample/android/app/src/main/res/values/strings.xml
rename to legacy/examples/ReactNativeWebExample/android/app/src/main/res/values/strings.xml
diff --git a/examples/ReactNativeWebExample/android/app/src/main/res/values/styles.xml b/legacy/examples/ReactNativeWebExample/android/app/src/main/res/values/styles.xml
similarity index 100%
rename from examples/ReactNativeWebExample/android/app/src/main/res/values/styles.xml
rename to legacy/examples/ReactNativeWebExample/android/app/src/main/res/values/styles.xml
diff --git a/examples/ReactNativeWebExample/android/build.gradle b/legacy/examples/ReactNativeWebExample/android/build.gradle
similarity index 100%
rename from examples/ReactNativeWebExample/android/build.gradle
rename to legacy/examples/ReactNativeWebExample/android/build.gradle
diff --git a/examples/ReactNativeWebExample/android/gradle.properties b/legacy/examples/ReactNativeWebExample/android/gradle.properties
similarity index 100%
rename from examples/ReactNativeWebExample/android/gradle.properties
rename to legacy/examples/ReactNativeWebExample/android/gradle.properties
diff --git a/examples/ReactNativeWebExample/android/gradle/wrapper/gradle-wrapper.jar b/legacy/examples/ReactNativeWebExample/android/gradle/wrapper/gradle-wrapper.jar
similarity index 100%
rename from examples/ReactNativeWebExample/android/gradle/wrapper/gradle-wrapper.jar
rename to legacy/examples/ReactNativeWebExample/android/gradle/wrapper/gradle-wrapper.jar
diff --git a/examples/ReactNativeWebExample/android/gradle/wrapper/gradle-wrapper.properties b/legacy/examples/ReactNativeWebExample/android/gradle/wrapper/gradle-wrapper.properties
similarity index 100%
rename from examples/ReactNativeWebExample/android/gradle/wrapper/gradle-wrapper.properties
rename to legacy/examples/ReactNativeWebExample/android/gradle/wrapper/gradle-wrapper.properties
diff --git a/examples/ReactNativeWebExample/android/gradlew b/legacy/examples/ReactNativeWebExample/android/gradlew
similarity index 100%
rename from examples/ReactNativeWebExample/android/gradlew
rename to legacy/examples/ReactNativeWebExample/android/gradlew
diff --git a/examples/ReactNativeWebExample/android/gradlew.bat b/legacy/examples/ReactNativeWebExample/android/gradlew.bat
similarity index 100%
rename from examples/ReactNativeWebExample/android/gradlew.bat
rename to legacy/examples/ReactNativeWebExample/android/gradlew.bat
diff --git a/examples/ReactNativeWebExample/android/keystores/BUCK b/legacy/examples/ReactNativeWebExample/android/keystores/BUCK
similarity index 100%
rename from examples/ReactNativeWebExample/android/keystores/BUCK
rename to legacy/examples/ReactNativeWebExample/android/keystores/BUCK
diff --git a/examples/ReactNativeWebExample/android/keystores/debug.keystore.properties b/legacy/examples/ReactNativeWebExample/android/keystores/debug.keystore.properties
similarity index 100%
rename from examples/ReactNativeWebExample/android/keystores/debug.keystore.properties
rename to legacy/examples/ReactNativeWebExample/android/keystores/debug.keystore.properties
diff --git a/examples/ReactNativeWebExample/android/settings.gradle b/legacy/examples/ReactNativeWebExample/android/settings.gradle
similarity index 100%
rename from examples/ReactNativeWebExample/android/settings.gradle
rename to legacy/examples/ReactNativeWebExample/android/settings.gradle
diff --git a/examples/ReactNativeWebExample/index.android.js b/legacy/examples/ReactNativeWebExample/index.android.js
similarity index 100%
rename from examples/ReactNativeWebExample/index.android.js
rename to legacy/examples/ReactNativeWebExample/index.android.js
diff --git a/examples/ReactNativeWebExample/index.ios.js b/legacy/examples/ReactNativeWebExample/index.ios.js
similarity index 100%
rename from examples/ReactNativeWebExample/index.ios.js
rename to legacy/examples/ReactNativeWebExample/index.ios.js
diff --git a/examples/ReactNativeWebExample/index.web.js b/legacy/examples/ReactNativeWebExample/index.web.js
similarity index 100%
rename from examples/ReactNativeWebExample/index.web.js
rename to legacy/examples/ReactNativeWebExample/index.web.js
diff --git a/examples/ReactNativeWebExample/ios/ReactNativeExample.xcodeproj/project.pbxproj b/legacy/examples/ReactNativeWebExample/ios/ReactNativeExample.xcodeproj/project.pbxproj
similarity index 100%
rename from examples/ReactNativeWebExample/ios/ReactNativeExample.xcodeproj/project.pbxproj
rename to legacy/examples/ReactNativeWebExample/ios/ReactNativeExample.xcodeproj/project.pbxproj
diff --git a/examples/ReactNativeWebExample/ios/ReactNativeExample.xcodeproj/xcshareddata/xcschemes/ReactNativeExample.xcscheme b/legacy/examples/ReactNativeWebExample/ios/ReactNativeExample.xcodeproj/xcshareddata/xcschemes/ReactNativeExample.xcscheme
similarity index 100%
rename from examples/ReactNativeWebExample/ios/ReactNativeExample.xcodeproj/xcshareddata/xcschemes/ReactNativeExample.xcscheme
rename to legacy/examples/ReactNativeWebExample/ios/ReactNativeExample.xcodeproj/xcshareddata/xcschemes/ReactNativeExample.xcscheme
diff --git a/examples/ReactNativeWebExample/ios/ReactNativeExample/AppDelegate.h b/legacy/examples/ReactNativeWebExample/ios/ReactNativeExample/AppDelegate.h
similarity index 100%
rename from examples/ReactNativeWebExample/ios/ReactNativeExample/AppDelegate.h
rename to legacy/examples/ReactNativeWebExample/ios/ReactNativeExample/AppDelegate.h
diff --git a/examples/ReactNativeWebExample/ios/ReactNativeExample/AppDelegate.m b/legacy/examples/ReactNativeWebExample/ios/ReactNativeExample/AppDelegate.m
similarity index 100%
rename from examples/ReactNativeWebExample/ios/ReactNativeExample/AppDelegate.m
rename to legacy/examples/ReactNativeWebExample/ios/ReactNativeExample/AppDelegate.m
diff --git a/examples/ReactNativeWebExample/ios/ReactNativeExample/Base.lproj/LaunchScreen.xib b/legacy/examples/ReactNativeWebExample/ios/ReactNativeExample/Base.lproj/LaunchScreen.xib
similarity index 100%
rename from examples/ReactNativeWebExample/ios/ReactNativeExample/Base.lproj/LaunchScreen.xib
rename to legacy/examples/ReactNativeWebExample/ios/ReactNativeExample/Base.lproj/LaunchScreen.xib
diff --git a/examples/ReactNativeWebExample/ios/ReactNativeExample/Images.xcassets/AppIcon.appiconset/Contents.json b/legacy/examples/ReactNativeWebExample/ios/ReactNativeExample/Images.xcassets/AppIcon.appiconset/Contents.json
similarity index 100%
rename from examples/ReactNativeWebExample/ios/ReactNativeExample/Images.xcassets/AppIcon.appiconset/Contents.json
rename to legacy/examples/ReactNativeWebExample/ios/ReactNativeExample/Images.xcassets/AppIcon.appiconset/Contents.json
diff --git a/examples/ReactNativeWebExample/ios/ReactNativeExample/Info.plist b/legacy/examples/ReactNativeWebExample/ios/ReactNativeExample/Info.plist
similarity index 100%
rename from examples/ReactNativeWebExample/ios/ReactNativeExample/Info.plist
rename to legacy/examples/ReactNativeWebExample/ios/ReactNativeExample/Info.plist
diff --git a/examples/ReactNativeWebExample/ios/ReactNativeExample/main.m b/legacy/examples/ReactNativeWebExample/ios/ReactNativeExample/main.m
similarity index 100%
rename from examples/ReactNativeWebExample/ios/ReactNativeExample/main.m
rename to legacy/examples/ReactNativeWebExample/ios/ReactNativeExample/main.m
diff --git a/examples/ReactNativeWebExample/ios/ReactNativeExampleTests/Info.plist b/legacy/examples/ReactNativeWebExample/ios/ReactNativeExampleTests/Info.plist
similarity index 100%
rename from examples/ReactNativeWebExample/ios/ReactNativeExampleTests/Info.plist
rename to legacy/examples/ReactNativeWebExample/ios/ReactNativeExampleTests/Info.plist
diff --git a/examples/ReactNativeWebExample/ios/ReactNativeExampleTests/ReactNativeExampleTests.m b/legacy/examples/ReactNativeWebExample/ios/ReactNativeExampleTests/ReactNativeExampleTests.m
similarity index 100%
rename from examples/ReactNativeWebExample/ios/ReactNativeExampleTests/ReactNativeExampleTests.m
rename to legacy/examples/ReactNativeWebExample/ios/ReactNativeExampleTests/ReactNativeExampleTests.m
diff --git a/examples/ReactNativeWebExample/package.json b/legacy/examples/ReactNativeWebExample/package.json
similarity index 100%
rename from examples/ReactNativeWebExample/package.json
rename to legacy/examples/ReactNativeWebExample/package.json
diff --git a/examples/ReactNativeWebExample/web-html.ejs b/legacy/examples/ReactNativeWebExample/web-html.ejs
similarity index 100%
rename from examples/ReactNativeWebExample/web-html.ejs
rename to legacy/examples/ReactNativeWebExample/web-html.ejs
diff --git a/examples/ReactNativeWebExample/webpack.config.js b/legacy/examples/ReactNativeWebExample/webpack.config.js
similarity index 100%
rename from examples/ReactNativeWebExample/webpack.config.js
rename to legacy/examples/ReactNativeWebExample/webpack.config.js
diff --git a/gulpfile.js b/legacy/gulpfile.js
similarity index 100%
rename from gulpfile.js
rename to legacy/gulpfile.js
diff --git a/images/0.2.0.gif b/legacy/images/0.2.0.gif
similarity index 100%
rename from images/0.2.0.gif
rename to legacy/images/0.2.0.gif
diff --git a/images/0.6.0.png b/legacy/images/0.6.0.png
similarity index 100%
rename from images/0.6.0.png
rename to legacy/images/0.6.0.png
diff --git a/images/AllThreePlatforms.png b/legacy/images/AllThreePlatforms.png
similarity index 100%
rename from images/AllThreePlatforms.png
rename to legacy/images/AllThreePlatforms.png
diff --git a/images/Api.png b/legacy/images/Api.png
similarity index 100%
rename from images/Api.png
rename to legacy/images/Api.png
diff --git a/images/Dispatch.png b/legacy/images/Dispatch.png
similarity index 100%
rename from images/Dispatch.png
rename to legacy/images/Dispatch.png
diff --git a/images/MainInterface.png b/legacy/images/MainInterface.png
similarity index 100%
rename from images/MainInterface.png
rename to legacy/images/MainInterface.png
diff --git a/images/Reactotron.gif b/legacy/images/Reactotron.gif
similarity index 100%
rename from images/Reactotron.gif
rename to legacy/images/Reactotron.gif
diff --git a/images/ReduxActions.png b/legacy/images/ReduxActions.png
similarity index 100%
rename from images/ReduxActions.png
rename to legacy/images/ReduxActions.png
diff --git a/images/ReduxSubscriptions.png b/legacy/images/ReduxSubscriptions.png
similarity index 100%
rename from images/ReduxSubscriptions.png
rename to legacy/images/ReduxSubscriptions.png
diff --git a/images/ShowingPrompts.png b/legacy/images/ShowingPrompts.png
similarity index 100%
rename from images/ShowingPrompts.png
rename to legacy/images/ShowingPrompts.png
diff --git a/images/Yellowbox.png b/legacy/images/Yellowbox.png
similarity index 100%
rename from images/Yellowbox.png
rename to legacy/images/Yellowbox.png
diff --git a/index.js b/legacy/index.js
similarity index 100%
rename from index.js
rename to legacy/index.js
diff --git a/legacy/package.json b/legacy/package.json
new file mode 100644
index 000000000..e8b558661
--- /dev/null
+++ b/legacy/package.json
@@ -0,0 +1,66 @@
+{
+ "name": "reactotron",
+ "version": "0.9.0",
+ "description": "A console-based remote control for React and React Native.",
+ "main": "index.js",
+ "bin": {
+ "reactotron": "./bin/reactotron.js"
+ },
+ "scripts": {
+ "test": "ava",
+ "docs": "node_modules/.bin/jsdoc --configure .jsdoc.json",
+ "dist": "npm run clean && mkdir bin && echo '#!/usr/bin/env node\n' > ./bin/reactotron.js && gulp build && cat dist/index.js >> ./bin/reactotron.js",
+ "clean": "rm -rf bin dist",
+ "gulp": "gulp build",
+ "start": "babel-node server/index.js",
+ "cpdistc": "rm examples/ReactDomExample/client.js && cp dist/client.js examples/ReactDomExample/ && rm examples/ReactNativeExample/client.js && cp dist/client.js examples/ReactNativeExample/ && rm examples/ReactNativeWebExample/client.js && cp dist/client.js examples/ReactNativeWebExample/",
+ "cprawc": "rm examples/ReactDomExample/client.js && cp client/client.js examples/ReactDomExample/ && rm examples/ReactNativeExample/client.js && cp client/client.js examples/ReactNativeExample/ && rm examples/ReactNativeWebExample/client.js && cp client/client.js examples/ReactNativeWebExample/",
+ "build:minor": "npm run dist && xyz -i minor -s bump_client.sh",
+ "build:patch": "npm run dist && xyz -i patch -s bump_client.sh"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/skellock/reactotron.git"
+ },
+ "author": {
+ "name": "Steve Kellock",
+ "email": "steve@kellock.ca"
+ },
+ "license": "MIT",
+ "files": [
+ "index.js",
+ "dist/*.js",
+ "bin/reactotron.js"
+ ],
+ "dependencies": {
+ "blessed": "^0.1.81",
+ "gemoji": "^1.0.0",
+ "moment": "^2.12.0",
+ "ramda": "^0.21.0",
+ "ramdasauce": "^1.0.0",
+ "socket.io": "^1.4.5",
+ "socket.io-client": "^1.4.5",
+ "strman": "^1.0.1"
+ },
+ "devDependencies": {
+ "ava": "^0.14.0",
+ "babel-cli": "^6.7.5",
+ "babel-core": "^6.7.7",
+ "babel-plugin-transform-object-rest-spread": "^6.6.5",
+ "babel-preset-es2015": "^6.6.0",
+ "babel-preset-stage-0": "^6.5.0",
+ "docdash": "^0.1.0",
+ "gulp": "^3.9.1",
+ "gulp-babel": "^6.1.2",
+ "gulp-rollup": "^1.8.0",
+ "jsdoc": "^3.4.0",
+ "rollup": "^0.25.8",
+ "xyz": "^0.5.0"
+ },
+ "ava": {
+ "require": [
+ "babel-core/register"
+ ],
+ "babel": "inherit"
+ }
+}
diff --git a/lerna.json b/lerna.json
new file mode 100644
index 000000000..9fc204ddf
--- /dev/null
+++ b/lerna.json
@@ -0,0 +1,4 @@
+{
+ "lerna": "2.0.0-beta.26",
+ "version": "0.94.0"
+}
diff --git a/package.json b/package.json
index e8b558661..046c326a5 100644
--- a/package.json
+++ b/package.json
@@ -1,66 +1,17 @@
{
- "name": "reactotron",
- "version": "0.9.0",
- "description": "A console-based remote control for React and React Native.",
- "main": "index.js",
- "bin": {
- "reactotron": "./bin/reactotron.js"
- },
"scripts": {
- "test": "ava",
- "docs": "node_modules/.bin/jsdoc --configure .jsdoc.json",
- "dist": "npm run clean && mkdir bin && echo '#!/usr/bin/env node\n' > ./bin/reactotron.js && gulp build && cat dist/index.js >> ./bin/reactotron.js",
- "clean": "rm -rf bin dist",
- "gulp": "gulp build",
- "start": "babel-node server/index.js",
- "cpdistc": "rm examples/ReactDomExample/client.js && cp dist/client.js examples/ReactDomExample/ && rm examples/ReactNativeExample/client.js && cp dist/client.js examples/ReactNativeExample/ && rm examples/ReactNativeWebExample/client.js && cp dist/client.js examples/ReactNativeWebExample/",
- "cprawc": "rm examples/ReactDomExample/client.js && cp client/client.js examples/ReactDomExample/ && rm examples/ReactNativeExample/client.js && cp client/client.js examples/ReactNativeExample/ && rm examples/ReactNativeWebExample/client.js && cp client/client.js examples/ReactNativeWebExample/",
- "build:minor": "npm run dist && xyz -i minor -s bump_client.sh",
- "build:patch": "npm run dist && xyz -i patch -s bump_client.sh"
- },
- "repository": {
- "type": "git",
- "url": "https://github.com/skellock/reactotron.git"
- },
- "author": {
- "name": "Steve Kellock",
- "email": "steve@kellock.ca"
- },
- "license": "MIT",
- "files": [
- "index.js",
- "dist/*.js",
- "bin/reactotron.js"
- ],
- "dependencies": {
- "blessed": "^0.1.81",
- "gemoji": "^1.0.0",
- "moment": "^2.12.0",
- "ramda": "^0.21.0",
- "ramdasauce": "^1.0.0",
- "socket.io": "^1.4.5",
- "socket.io-client": "^1.4.5",
- "strman": "^1.0.1"
+ "bootstrap": "lerna bootstrap",
+ "build": "lerna run build",
+ "clean": "lerna clean",
+ "test": "lerna run test",
+ "copy-internal-deps": "lerna run copy-internal-deps",
+ "welcome": "./scripts/welcome.sh",
+ "e2e": "npm run bootstrap && npm run build && npm run copy-internal-deps && npm run test"
},
"devDependencies": {
- "ava": "^0.14.0",
- "babel-cli": "^6.7.5",
- "babel-core": "^6.7.7",
- "babel-plugin-transform-object-rest-spread": "^6.6.5",
- "babel-preset-es2015": "^6.6.0",
- "babel-preset-stage-0": "^6.5.0",
- "docdash": "^0.1.0",
- "gulp": "^3.9.1",
- "gulp-babel": "^6.1.2",
- "gulp-rollup": "^1.8.0",
- "jsdoc": "^3.4.0",
- "rollup": "^0.25.8",
- "xyz": "^0.5.0"
+ "lerna": "2.0.0-beta.26"
},
- "ava": {
- "require": [
- "babel-core/register"
- ],
- "babel": "inherit"
+ "dependencies": {
+ "stringify-object": "^2.4.0"
}
}
diff --git a/packages/demo-react-js/.gitignore b/packages/demo-react-js/.gitignore
new file mode 100644
index 000000000..8e8b40aff
--- /dev/null
+++ b/packages/demo-react-js/.gitignore
@@ -0,0 +1,11 @@
+# See http://help.github.com/ignore-files/ for more about ignoring files.
+
+# dependencies
+node_modules
+
+# production
+build
+
+# misc
+.DS_Store
+npm-debug.log
diff --git a/packages/demo-react-js/README.md b/packages/demo-react-js/README.md
new file mode 100644
index 000000000..79d5aaa25
--- /dev/null
+++ b/packages/demo-react-js/README.md
@@ -0,0 +1,20 @@
+## Reactotron Notes
+
+With `create-react-app`, the default devtool is `eval`. We need that set to `source-map` to
+get the stack traces proper.
+
+Instead of ejecting for this demo, let's just reach into the node_modules and change it.
+
+SPARTA!!!!!!
+
+`atom ./node_modules/react-scripts/config/webpack.config.dev.js`
+
+Change the line that currently says
+
+` devtool: 'eval',`
+
+to
+
+`devtool: 'source-map',`
+
+It's the first line after the module exports. (line 18, but that will change as new version get put out)
diff --git a/packages/demo-react-js/copy-internal-deps.sh b/packages/demo-react-js/copy-internal-deps.sh
new file mode 100755
index 000000000..476faadb8
--- /dev/null
+++ b/packages/demo-react-js/copy-internal-deps.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+cp ../reactotron-react-js/dist/index.js ./node_modules/reactotron-react-js/index.js
+cp ../reactotron-core-client/dist/index.js ./node_modules/reactotron-core-client/index.js
+cp ../reactotron-redux/dist/index.js ./node_modules/reactotron-redux/index.js
+cp ../reactotron-apisauce/dist/index.js ./node_modules/reactotron-apisauce/index.js
diff --git a/packages/demo-react-js/favicon.ico b/packages/demo-react-js/favicon.ico
new file mode 100644
index 000000000..5c125de5d
Binary files /dev/null and b/packages/demo-react-js/favicon.ico differ
diff --git a/packages/demo-react-js/index.html b/packages/demo-react-js/index.html
new file mode 100644
index 000000000..72e10e94c
--- /dev/null
+++ b/packages/demo-react-js/index.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+ React App
+
+
+
+
+
+
diff --git a/packages/demo-react-js/package.json b/packages/demo-react-js/package.json
new file mode 100644
index 000000000..fc7b07144
--- /dev/null
+++ b/packages/demo-react-js/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "demo-react-js",
+ "version": "0.94.0",
+ "private": true,
+ "devDependencies": {
+ "react-scripts": "0.2.1"
+ },
+ "dependencies": {
+ "apisauce": "^0.3.0",
+ "ramda": "^0.22.1",
+ "ramdasauce": "^1.1.0",
+ "react": "^15.2.1",
+ "react-dom": "^15.2.1",
+ "react-redux": "^4.4.5",
+ "reactotron-apisauce": "^0.94.0",
+ "reactotron-core-client": "^0.94.0",
+ "reactotron-react-js": "^0.94.0",
+ "reactotron-redux": "^0.94.0",
+ "redux": "^3.5.2",
+ "redux-logger": "^2.6.1",
+ "redux-saga": "^0.11.0",
+ "socket.io": "^1.4.8",
+ "stacktrace-js": "^1.3.1"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build-prod": "react-scripts build",
+ "eject": "react-scripts eject",
+ "copy-internal-deps": "./copy-internal-deps.sh"
+ },
+ "eslintConfig": {
+ "extends": "./node_modules/react-scripts/config/eslint.js"
+ }
+}
diff --git a/packages/demo-react-js/src/App.css b/packages/demo-react-js/src/App.css
new file mode 100644
index 000000000..074be41c0
--- /dev/null
+++ b/packages/demo-react-js/src/App.css
@@ -0,0 +1,19 @@
+.App {
+ text-align: center;
+}
+
+
+.App-header {
+ background-color: #222;
+ padding: 20px;
+ color: white;
+}
+
+.App-intro {
+ font-size: large;
+}
+
+@keyframes App-logo-spin {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
diff --git a/packages/demo-react-js/src/App.js b/packages/demo-react-js/src/App.js
new file mode 100644
index 000000000..063a69dcd
--- /dev/null
+++ b/packages/demo-react-js/src/App.js
@@ -0,0 +1,152 @@
+import React, { Component, PropTypes } from 'react';
+import logo from './logo.svg';
+import './App.css';
+import { connect } from 'react-redux'
+import { Actions as StartupActions } from './Redux/Startup.redux'
+import { Actions as LogoActions } from './Redux/Logo.redux'
+import { Actions as RepoActions } from './Redux/Repo.redux'
+import makeErrorForFun from './ErrorMaker'
+
+const Styles = {
+ button: {
+ fontSize: 15,
+ padding: 10,
+ margin: '10px 10px',
+ backgroundColor: '#333333',
+ border: 0,
+ borderRadius: 4,
+ color: '#eeeeee'
+ },
+ avatar: {
+ height: 80,
+ width: 80,
+ borderRadius: 40
+ }
+}
+
+
+class App extends Component {
+
+ static propTypes = {
+ startup: PropTypes.func.isRequired,
+ message: PropTypes.string,
+ url: PropTypes.string,
+ name: PropTypes.string,
+ sha: PropTypes.string,
+ error: PropTypes.string,
+ fetching: PropTypes.bool
+ }
+
+ componentWillMount () {
+ // this.props.startup()
+ }
+
+ renderError () {
+ const { error } = this.props
+ return (
+
+
Big Ol Error
+
+ { error }
+
+
+ )
+ }
+
+ renderMessage () {
+ const { fetching, name, url, message, sha } = this.props
+ if (fetching) {
+ return (Hang tight
)
+ }
+
+ return (
+
+ )
+ }
+
+ render () {
+ const { error, speed, size, repo, avatar } = this.props
+ const logoStyles = {
+ animation: `App-logo-spin infinite ${speed}s linear`,
+ height: `${size}px`
+ }
+
+ return (
+
+
+
Reactotron Demo
+
+ Slow
+ Fast
+ Big
+ Small
+
+
+
+ { repo &&
+
Latest Commit From {repo} }
+ { avatar &&
+
+ }
+
Reactotron
+
React Native
+
Mobx
+
Redux
+
Bad
+
+ { error ? this.renderError() : this.renderMessage() }
+
+
+ Custom Messages
+
+
+
+
+ );
+ }
+}
+
+const mapStateToProps = state => ({ ...state.repo, ...state.logo })
+
+const mapDispatchToProps = dispatch => ({
+ startup: () => dispatch(StartupActions.startup()),
+ faster: () => dispatch(LogoActions.changeSpeed(1)),
+ slower: () => dispatch(LogoActions.changeSpeed(20)),
+ bigger: () => dispatch(LogoActions.changeSize(160)),
+ smaller: () => dispatch(LogoActions.changeSize(40)),
+ requestReactotron: () => dispatch(RepoActions.request('reactotron/reactotron')),
+ requestReactNative: () => dispatch(RepoActions.request('facebook/react-native')),
+ requestMobx: () => dispatch(RepoActions.request('mobxjs/mobx')),
+ requestRedux: () => dispatch(RepoActions.request('reactjs/redux')),
+ requestBad: () => dispatch(RepoActions.request('zzzz/zzzzz')),
+ handleLogoPress: () => {
+ console.tron.log('wait for it...')
+ setTimeout(() => { makeErrorForFun('boom') }, 500)
+ },
+ display: () => {
+ console.tron.display({ name: 'HELLO', value: 'You\'re awesome.', preview: 'Guess what?' })
+ console.tron.display({ name: 'DANGER', value: 9001, important: true, preview: 'It\'s over 9000!' })
+ console.tron.display({ name: 'ORDER', preview: 'Here\'s your order...', value: {
+ app: {
+ color: 'green',
+ vegetable: 'spinach',
+ variant: 'baby',
+ salad: true
+ },
+ main: {
+ type: 'poutine'
+ },
+ when: new Date()
+ }
+ })
+ console.tron.display({ name: 'LIST', value: [1, 'a', true, new Date()], preview: '4 things' })
+ }
+})
+
+export default connect(mapStateToProps, mapDispatchToProps)(App);
diff --git a/packages/demo-react-js/src/ErrorMaker.js b/packages/demo-react-js/src/ErrorMaker.js
new file mode 100644
index 000000000..f35a7a6e6
--- /dev/null
+++ b/packages/demo-react-js/src/ErrorMaker.js
@@ -0,0 +1,3 @@
+export default function makeErrorForFun(message) {
+ throw new Error(message)
+}
diff --git a/packages/demo-react-js/src/ReactotronConfig.js b/packages/demo-react-js/src/ReactotronConfig.js
new file mode 100644
index 000000000..f9abd5fe6
--- /dev/null
+++ b/packages/demo-react-js/src/ReactotronConfig.js
@@ -0,0 +1,10 @@
+import Reactotron, { trackGlobalErrors } from 'reactotron-react-js'
+import tronsauce from 'reactotron-apisauce'
+
+Reactotron
+ .configure({ name: 'Demo Time!', secure: false })
+ .use(tronsauce())
+ .use(trackGlobalErrors({ offline: false }))
+ .connect()
+
+console.tron = Reactotron
diff --git a/packages/demo-react-js/src/Redux/Logo.redux.js b/packages/demo-react-js/src/Redux/Logo.redux.js
new file mode 100644
index 000000000..3761507f1
--- /dev/null
+++ b/packages/demo-react-js/src/Redux/Logo.redux.js
@@ -0,0 +1,33 @@
+export const Types = {
+ Speed: 'logo.speed',
+ Size: 'logo.size',
+ Reset: 'logo.reset'
+}
+
+export const Actions = {
+ changeSpeed: (speed) => ({ type: Types.Speed, speed }),
+ changeSize: (size) => ({ type: Types.Size, size }),
+ reset: () => ({ type: Types.Reset })
+}
+
+export const INITIAL_STATE = {
+ size: 100,
+ speed: 5
+}
+
+const changeSpeed = (state, { speed }) => ({ ...state, speed })
+const changeSize = (state, { size }) => ({ ...state, size })
+const reset = (state, { size }) => INITIAL_STATE
+
+// actions ->
+const reducerMap = {
+ [Types.Speed]: changeSpeed,
+ [Types.Size]: changeSize,
+ [Types.Reset]: reset
+}
+
+// our reducer
+export const reducer = (state = INITIAL_STATE, action) => {
+ const handler = reducerMap[action.type]
+ return typeof handler === 'function' ? handler(state, action) : state
+}
diff --git a/packages/demo-react-js/src/Redux/Repo.redux.js b/packages/demo-react-js/src/Redux/Repo.redux.js
new file mode 100644
index 000000000..9846cdc7e
--- /dev/null
+++ b/packages/demo-react-js/src/Redux/Repo.redux.js
@@ -0,0 +1,48 @@
+export const Types = {
+ Request: 'repo.request',
+ Receive: 'repo.receive',
+ Failure: 'repo.failure'
+}
+
+export const Actions = {
+ request: (repo) => ({ type: Types.Request, repo }),
+ receive: (message, url, name, sha, avatar) => ({ type: Types.Receive, message, url, name, sha, avatar }),
+ failure: (error) => ({ type: Types.Failure, error })
+}
+
+export const INITIAL_STATE = {
+ message: null,
+ repo: null,
+ url: null,
+ name: null,
+ sha: null,
+ fetching: false,
+ avatar: null,
+ error: null
+}
+
+// we're going out for the repo message
+const request = (state, { repo }) =>
+ ({ ...INITIAL_STATE, fetching: true, repo })
+
+// we've got a repo message
+const receive = (state, { message, url, name, sha, avatar }) => {
+ return { ...state, fetching: false, message, url, name, sha, avatar }
+}
+
+// we failed to get the repo message :(
+const failure = (state, action) =>
+ ({ ...state, fetching: false, error: action.error })
+
+// actions ->
+const reducerMap = {
+ [Types.Request]: request,
+ [Types.Receive]: receive,
+ [Types.Failure]: failure
+}
+
+// our reducer
+export const reducer = (state = INITIAL_STATE, action) => {
+ const handler = reducerMap[action.type]
+ return typeof handler === 'function' ? handler(state, action) : state
+}
diff --git a/packages/demo-react-js/src/Redux/RootReducer.js b/packages/demo-react-js/src/Redux/RootReducer.js
new file mode 100644
index 000000000..6717b6f59
--- /dev/null
+++ b/packages/demo-react-js/src/Redux/RootReducer.js
@@ -0,0 +1,8 @@
+import { combineReducers } from 'redux'
+import { reducer as repoReducer } from './Repo.redux'
+import { reducer as logoReducer } from './Logo.redux'
+
+export default combineReducers({
+ repo: repoReducer,
+ logo: logoReducer
+})
diff --git a/packages/demo-react-js/src/Redux/Startup.redux.js b/packages/demo-react-js/src/Redux/Startup.redux.js
new file mode 100644
index 000000000..65ad20f3e
--- /dev/null
+++ b/packages/demo-react-js/src/Redux/Startup.redux.js
@@ -0,0 +1,7 @@
+export const Types = {
+ Startup: 'Startup'
+}
+
+export const Actions = {
+ startup: () => ({ type: Types.Startup })
+}
diff --git a/packages/demo-react-js/src/Redux/Store.js b/packages/demo-react-js/src/Redux/Store.js
new file mode 100644
index 000000000..94c170b56
--- /dev/null
+++ b/packages/demo-react-js/src/Redux/Store.js
@@ -0,0 +1,36 @@
+import { not, contains } from 'ramda'
+import {createStore, applyMiddleware, compose} from 'redux'
+import createLogger from 'redux-logger'
+import createSagaMiddleware from 'redux-saga'
+import rootReducer from './RootReducer'
+import rootSaga from '../Sagas'
+import Reactotron from 'reactotron-react-js'
+import createTronohancer from 'reactotron-redux'
+import { Types as LogoTypes } from './Logo.redux'
+
+// the logger master switch
+const USE_LOGGING = false
+
+// silence these saga-based messages
+const SAGA_LOGGING_BLACKLIST = ['EFFECT_TRIGGERED', 'EFFECT_RESOLVED', 'EFFECT_REJECTED']
+
+// create the logger
+const logger = createLogger({
+ predicate: (getState, { type }) => USE_LOGGING && not(contains(type, SAGA_LOGGING_BLACKLIST))
+})
+
+// a function which can create our store and auto-persist the data
+export default () => {
+ const sagaMiddleware = createSagaMiddleware()
+ const tracker = createTronohancer(Reactotron, {
+ isActionImportant: action => action.type === LogoTypes.Size && action.size > 100
+ })
+ const enhancers = compose(
+ tracker,
+ applyMiddleware(logger, sagaMiddleware)
+ )
+
+ const store = createStore(rootReducer, enhancers)
+ sagaMiddleware.run(rootSaga)
+ return store
+}
diff --git a/packages/demo-react-js/src/Sagas/Repo.sagas.js b/packages/demo-react-js/src/Sagas/Repo.sagas.js
new file mode 100644
index 000000000..4a2f562b6
--- /dev/null
+++ b/packages/demo-react-js/src/Sagas/Repo.sagas.js
@@ -0,0 +1,23 @@
+import RS from 'ramdasauce'
+import { delay } from 'redux-saga'
+import { call, put } from 'redux-saga/effects'
+import * as Repo from '../Redux/Repo.redux'
+
+export function * request (api, action) {
+ // make the call to github
+ const { repo } = action
+ const { ok, data, status, problem } = yield call(api.get, `/repos/${repo}/commits`)
+ yield call(delay, 200)
+ // are we good?
+ if (ok) {
+ const entry = RS.dotPath('0', data)
+ const { commit, author, html_url: url } = entry
+ const { message, tree } = commit
+ const { login, avatar_url } = author
+ const { sha } = tree
+ // record the last commit's message
+ yield put(Repo.Actions.receive(message, url, login, sha, avatar_url))
+ } else {
+ yield put(Repo.Actions.failure(`uh oh: ${problem} status: ${status}`))
+ }
+}
diff --git a/packages/demo-react-js/src/Sagas/Startup.sagas.js b/packages/demo-react-js/src/Sagas/Startup.sagas.js
new file mode 100644
index 000000000..63810e1a4
--- /dev/null
+++ b/packages/demo-react-js/src/Sagas/Startup.sagas.js
@@ -0,0 +1,7 @@
+// import { put } from 'redux-saga/effects'
+// import * as RepoMessage from '../Redux/RepoMessage.redux'
+
+// process STARTUP actions
+export function * startup () {
+ // yield put(RepoMessage.Actions.request())
+}
diff --git a/packages/demo-react-js/src/Sagas/index.js b/packages/demo-react-js/src/Sagas/index.js
new file mode 100644
index 000000000..e0c9e72da
--- /dev/null
+++ b/packages/demo-react-js/src/Sagas/index.js
@@ -0,0 +1,25 @@
+import { takeEvery, takeLatest } from 'redux-saga'
+import ApiSauce from 'apisauce'
+import Reactotron from 'reactotron-react-js'
+
+import * as Startup from '../Redux/Startup.redux'
+import * as Repo from '../Redux/Repo.redux'
+
+import { startup } from './Startup.sagas'
+import { request as requestRepo } from './Repo.sagas'
+
+const api = ApiSauce.create({
+ baseURL: 'https://api.github.com',
+ headers: {
+ 'Accept': 'application/vnd.github.v3+json'
+ }
+})
+
+api.addMonitor(Reactotron.apisauce)
+
+export default function * rootSaga () {
+ yield [
+ takeLatest(Repo.Types.Request, requestRepo, api),
+ takeEvery(Startup.Types.Startup, startup)
+ ]
+}
diff --git a/packages/demo-react-js/src/index.css b/packages/demo-react-js/src/index.css
new file mode 100644
index 000000000..b4cc7250b
--- /dev/null
+++ b/packages/demo-react-js/src/index.css
@@ -0,0 +1,5 @@
+body {
+ margin: 0;
+ padding: 0;
+ font-family: sans-serif;
+}
diff --git a/packages/demo-react-js/src/index.js b/packages/demo-react-js/src/index.js
new file mode 100644
index 000000000..552d0d387
--- /dev/null
+++ b/packages/demo-react-js/src/index.js
@@ -0,0 +1,16 @@
+import './ReactotronConfig' // we put this guy first
+
+import React from 'react';
+import ReactDOM from 'react-dom';
+import App from './App';
+import './index.css';
+
+import { Provider } from 'react-redux'
+import createStore from './Redux/Store'
+
+const store = createStore()
+
+ReactDOM.render(
+ ,
+ document.getElementById('root')
+);
diff --git a/packages/demo-react-js/src/logo.svg b/packages/demo-react-js/src/logo.svg
new file mode 100644
index 000000000..6b60c1042
--- /dev/null
+++ b/packages/demo-react-js/src/logo.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/examples/ReactNativeExample/.babelrc b/packages/demo-react-native/.babelrc
similarity index 100%
rename from examples/ReactNativeExample/.babelrc
rename to packages/demo-react-native/.babelrc
diff --git a/packages/demo-react-native/.buckconfig b/packages/demo-react-native/.buckconfig
new file mode 100644
index 000000000..934256cb2
--- /dev/null
+++ b/packages/demo-react-native/.buckconfig
@@ -0,0 +1,6 @@
+
+[android]
+ target = Google Inc.:Google APIs:23
+
+[maven_repositories]
+ central = https://repo1.maven.org/maven2
diff --git a/packages/demo-react-native/.flowconfig b/packages/demo-react-native/.flowconfig
new file mode 100644
index 000000000..e28e2f5bd
--- /dev/null
+++ b/packages/demo-react-native/.flowconfig
@@ -0,0 +1,41 @@
+[ignore]
+
+# We fork some components by platform.
+.*/*.android.js
+
+# Ignore templates with `@flow` in header
+.*/local-cli/generator.*
+
+# Ignore malformed json
+.*/node_modules/y18n/test/.*\.json
+
+[include]
+
+[libs]
+node_modules/react-native/Libraries/react-native/react-native-interface.js
+node_modules/react-native/flow
+flow/
+
+[options]
+module.system=haste
+
+esproposal.class_static_fields=enable
+esproposal.class_instance_fields=enable
+
+experimental.strict_type_args=true
+
+munge_underscores=true
+
+module.name_mapper='^image![a-zA-Z0-9$_-]+$' -> 'GlobalImageStub'
+module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub'
+
+suppress_type=$FlowIssue
+suppress_type=$FlowFixMe
+suppress_type=$FixMe
+
+suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(2[0-7]\\|1[0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
+suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(2[0-7]\\|1[0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+
+suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
+
+[version]
+^0.27.0
diff --git a/packages/demo-react-native/.gitignore b/packages/demo-react-native/.gitignore
new file mode 100644
index 000000000..eb1535e41
--- /dev/null
+++ b/packages/demo-react-native/.gitignore
@@ -0,0 +1,41 @@
+# OSX
+#
+.DS_Store
+
+# Xcode
+#
+build/
+*.pbxuser
+!default.pbxuser
+*.mode1v3
+!default.mode1v3
+*.mode2v3
+!default.mode2v3
+*.perspectivev3
+!default.perspectivev3
+xcuserdata
+*.xccheckout
+*.moved-aside
+DerivedData
+*.hmap
+*.ipa
+*.xcuserstate
+project.xcworkspace
+
+# Android/IJ
+#
+*.iml
+.idea
+.gradle
+local.properties
+
+# node.js
+#
+node_modules/
+npm-debug.log
+
+# BUCK
+buck-out/
+\.buckd/
+android/app/libs
+android/keystores/debug.keystore
diff --git a/packages/demo-react-native/.watchmanconfig b/packages/demo-react-native/.watchmanconfig
new file mode 100644
index 000000000..9e26dfeeb
--- /dev/null
+++ b/packages/demo-react-native/.watchmanconfig
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/examples/ReactNativeWebExample/App/Actions/CreateAction.js b/packages/demo-react-native/App/Actions/CreateAction.js
similarity index 100%
rename from examples/ReactNativeWebExample/App/Actions/CreateAction.js
rename to packages/demo-react-native/App/Actions/CreateAction.js
diff --git a/examples/ReactNativeWebExample/App/Actions/Creators.js b/packages/demo-react-native/App/Actions/Creators.js
similarity index 100%
rename from examples/ReactNativeWebExample/App/Actions/Creators.js
rename to packages/demo-react-native/App/Actions/Creators.js
diff --git a/examples/ReactNativeExample/App/Actions/Types.js b/packages/demo-react-native/App/Actions/Types.js
similarity index 100%
rename from examples/ReactNativeExample/App/Actions/Types.js
rename to packages/demo-react-native/App/Actions/Types.js
diff --git a/packages/demo-react-native/App/Components/Button.js b/packages/demo-react-native/App/Components/Button.js
new file mode 100644
index 000000000..bb3998fc9
--- /dev/null
+++ b/packages/demo-react-native/App/Components/Button.js
@@ -0,0 +1,22 @@
+import React, { Component, PropTypes } from 'react'
+import { View, Text, TouchableOpacity } from 'react-native'
+import Styles from './Styles/ButtonStyles'
+
+class Button extends Component {
+
+ static propTypes = {
+ onPress: PropTypes.func,
+ text: PropTypes.string
+ }
+
+ render () {
+ return (
+
+ {this.props.text}
+
+ )
+ }
+
+}
+
+export default Button
diff --git a/packages/demo-react-native/App/Components/Repo.js b/packages/demo-react-native/App/Components/Repo.js
new file mode 100644
index 000000000..ea50077d0
--- /dev/null
+++ b/packages/demo-react-native/App/Components/Repo.js
@@ -0,0 +1,101 @@
+import React, { Component, PropTypes } from 'react'
+import { Animated, Easing, TouchableWithoutFeedback, View, Text, Image } from 'react-native'
+import Styles from './Styles/RepoStyles'
+import Button from './Button'
+import { merge } from 'ramda'
+
+const ROTATION = { inputRange: [0, 1], outputRange: ['0deg', '360deg'] }
+
+class Repo extends Component {
+
+ static propTypes = {
+ repo: PropTypes.string,
+ name: PropTypes.string,
+ avatar: PropTypes.string,
+ message: PropTypes.string,
+ bigger: PropTypes.func,
+ smaller: PropTypes.func,
+ faster: PropTypes.func,
+ slower: PropTypes.func,
+ reset: PropTypes.func,
+ size: PropTypes.number,
+ speed: PropTypes.number
+ }
+
+ constructor (props) {
+ super(props)
+ this.state = {
+ spinny: new Animated.Value(0)
+ }
+ this.animate = this.animate.bind(this)
+ }
+
+ componentWillReceiveProps (newProps) {
+ if (newProps.avatar && newProps.speed) {
+ // stop the current running animation
+ if (this.animation) {
+ this.animation.stop()
+ this.animation = null
+ }
+ setTimeout(this.animate, 10)
+ }
+ }
+
+ animate () {
+
+ const duration = 100 * this.props.speed
+ const easing = Easing.linear
+ this.state.spinny.setValue(0)
+ this.animation = Animated.sequence([
+ Animated.timing(this.state.spinny, { toValue: 1, duration, easing }),
+ // Animated.timing(this.state.spinny, { toValue: 0, duration: 0 })
+ ]).start( ({finished}) => {
+ if (finished) {
+ this.animate()
+ } else {
+ this.animation = null
+ }
+ })
+ }
+
+
+ getAnimationStyle () {
+ return {
+ transform: [
+ { rotate: this.state.spinny.interpolate(ROTATION) }
+ ]
+ }
+ }
+
+ render () {
+ const { repo, name, avatar, message, size, speed } = this.props
+ const avatarSource = avatar && { uri: avatar }
+
+ const avatarStyles = merge(Styles.avatar, { width: size, height: size, borderRadius: size * 0.5 })
+ const centerStyles = merge(Styles.center, this.getAnimationStyle())
+ return (
+
+ {repo || ' '}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {name || ' '}
+ {message}
+
+ )
+ }
+}
+
+export default Repo
diff --git a/packages/demo-react-native/App/Components/Styles/ButtonStyles.js b/packages/demo-react-native/App/Components/Styles/ButtonStyles.js
new file mode 100644
index 000000000..60949d023
--- /dev/null
+++ b/packages/demo-react-native/App/Components/Styles/ButtonStyles.js
@@ -0,0 +1,18 @@
+export default {
+ container: {
+ paddingVertical: 10,
+ backgroundColor: '#243E5D',
+ borderRadius: 4,
+ marginTop: 10,
+ marginHorizontal: 5,
+ justifyContent: 'center',
+ alignItems: 'center',
+ width: 80,
+ height: 40
+ },
+ text: {
+ fontSize: 12,
+ color: 'white',
+ textAlign: 'center'
+ }
+}
diff --git a/packages/demo-react-native/App/Components/Styles/RepoStyles.js b/packages/demo-react-native/App/Components/Styles/RepoStyles.js
new file mode 100644
index 000000000..f45068d14
--- /dev/null
+++ b/packages/demo-react-native/App/Components/Styles/RepoStyles.js
@@ -0,0 +1,48 @@
+export default {
+ container: {
+ alignItems: 'center',
+ },
+ name: {
+ color: '#ffffff'
+ },
+ repo: {
+ fontWeight: 'bold',
+ color: '#ffffff'
+ },
+ avatar: {
+ marginVertical: 15,
+ width: 80,
+ height: 80,
+ borderRadius: 40,
+ borderWidth: 4,
+ borderColor: '#ffffff',
+ backgroundColor: '#3d3d3d'
+ },
+ message: {
+ color: '#BBD1EA',
+ marginTop: 5,
+ fontSize: 12,
+ height: 100,
+ overflow: 'hidden',
+ paddingHorizontal: 50,
+ marginTop: 20
+ },
+ middle: {
+ flexDirection: 'row',
+ alignItems: 'center'
+ },
+ center: {
+ alignItems: 'center',
+ justifyContent: 'center'
+ },
+ left: {
+ alignItems: 'flex-end',
+ justifyContent: 'flex-end',
+ paddingRight: 10
+ },
+ right: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingLeft: 10
+ }
+}
diff --git a/examples/ReactNativeWebExample/App/Config/Config.js b/packages/demo-react-native/App/Config/Config.js
similarity index 100%
rename from examples/ReactNativeWebExample/App/Config/Config.js
rename to packages/demo-react-native/App/Config/Config.js
diff --git a/packages/demo-react-native/App/Config/ReactotronConfig.js b/packages/demo-react-native/App/Config/ReactotronConfig.js
new file mode 100644
index 000000000..b9f8dea07
--- /dev/null
+++ b/packages/demo-react-native/App/Config/ReactotronConfig.js
@@ -0,0 +1,14 @@
+import Reactotron, { trackGlobalErrors } from 'reactotron-react-native'
+import tronsauce from 'reactotron-apisauce'
+
+if (__DEV__) {
+ Reactotron
+ .configure({ name: 'React Native Demo' })
+ .use(tronsauce())
+ .use(trackGlobalErrors({
+ veto: frame => frame.fileName.indexOf('/node_modules/react-native/') >= 0
+ }))
+ .connect()
+
+ console.tron = Reactotron
+}
diff --git a/examples/ReactNativeWebExample/App/Containers/ReduxContainer.js b/packages/demo-react-native/App/Containers/ReduxContainer.js
similarity index 62%
rename from examples/ReactNativeWebExample/App/Containers/ReduxContainer.js
rename to packages/demo-react-native/App/Containers/ReduxContainer.js
index 0e7bb1b82..baef5be53 100644
--- a/examples/ReactNativeWebExample/App/Containers/ReduxContainer.js
+++ b/packages/demo-react-native/App/Containers/ReduxContainer.js
@@ -1,11 +1,11 @@
-import React from 'react-native'
+import React, { Component } from 'react'
import { Provider } from 'react-redux'
-import configureStore from '../Store/Store'
+import configureStore from '../Redux'
import RootContainer from './RootContainer'
const store = configureStore()
-export default class ReduxContainer extends React.Component {
+export default class ReduxContainer extends Component {
render () {
return (
diff --git a/packages/demo-react-native/App/Containers/RootContainer.js b/packages/demo-react-native/App/Containers/RootContainer.js
new file mode 100644
index 000000000..2fd3a2b9c
--- /dev/null
+++ b/packages/demo-react-native/App/Containers/RootContainer.js
@@ -0,0 +1,85 @@
+import React, { Component } from 'react'
+import { ScrollView, View, Text, TouchableOpacity } from 'react-native'
+import { connect } from 'react-redux'
+import Actions from '../Actions/Creators'
+import Styles from './Styles/RootContainerStyles'
+// import Reactotron from 'reactotron-react-native'
+import Button from '../Components/Button'
+import Repo from '../Components/Repo'
+import { Actions as RepoActions } from '../Redux/RepoRedux'
+import { Actions as LogoActions } from '../Redux/LogoRedux'
+import makeErrorForFun from '../Lib/ErrorMaker'
+import { keys, map, join } from 'ramda'
+
+export default class RootContainer extends Component {
+
+ constructor (props) {
+ super(props)
+ this.handlePress = this.handlePress.bind(this)
+ this.handlePressDebug = () => console.tron.debug('This is a debug message')
+ this.handlePressWarn = () => console.tron.warn('This is a warn message')
+ this.handlePressError = () => console.tron.error('This is a error message')
+
+ }
+
+ handlePress () {
+ console.tron.log('A touchable was pressed.🔥🦄')
+ }
+
+ render () {
+ const { avatar, repo, name, message, size, speed } = this.props
+ const { reset, faster, slower, bigger, smaller } = this.props
+
+ return (
+
+
+ Awesome Github Viewer!
+ Reactotron Demo
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+
+}
+
+const mapStateToProps = (state) => {
+ return {
+ ...state.repo,
+ ...state.logo
+ }
+}
+
+const mapDispatchToProps = dispatch => ({
+ startup: () => dispatch(StartupActions.startup()),
+ faster: () => dispatch(LogoActions.changeSpeed(10)),
+ slower: () => dispatch(LogoActions.changeSpeed(50)),
+ bigger: () => dispatch(LogoActions.changeSize(140)),
+ smaller: () => dispatch(LogoActions.changeSize(40)),
+ reset: () => dispatch(LogoActions.reset()),
+ requestReactotron: () => dispatch(RepoActions.request('reactotron/reactotron')),
+ requestReactNative: () => dispatch(RepoActions.request('facebook/react-native')),
+ requestMobx: () => dispatch(RepoActions.request('mobxjs/mobx')),
+ requestRedux: () => dispatch(RepoActions.request('reactjs/redux')),
+ bomb: () => {
+ console.tron.log('wait for it...')
+ setTimeout(() => { makeErrorForFun('boom') }, 500)
+ }
+})
+
+
+export default connect(mapStateToProps, mapDispatchToProps)(RootContainer)
diff --git a/packages/demo-react-native/App/Containers/Styles/RootContainerStyles.js b/packages/demo-react-native/App/Containers/Styles/RootContainerStyles.js
new file mode 100644
index 000000000..fced461da
--- /dev/null
+++ b/packages/demo-react-native/App/Containers/Styles/RootContainerStyles.js
@@ -0,0 +1,36 @@
+import {StyleSheet} from 'react-native'
+
+export default StyleSheet.create({
+ container: {
+ backgroundColor: '#4A90E2'
+ },
+ content: {
+ },
+ titleContainer: {
+ flex: 1,
+ backgroundColor: '#3B73B5',
+ paddingTop: 50,
+ paddingBottom: 20,
+ marginBottom: 10
+ },
+ title: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ textAlign: 'center',
+ color: '#FFD898',
+ },
+ subtitle: {
+ fontSize: 18,
+ fontWeight: 'bold',
+ textAlign: 'center',
+ color: '#ffffff',
+ },
+ repoContainer: {
+ alignItems: 'center'
+ },
+ buttons: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ marginBottom: 20
+ }
+})
diff --git a/packages/demo-react-native/App/Lib/ErrorMaker.js b/packages/demo-react-native/App/Lib/ErrorMaker.js
new file mode 100644
index 000000000..f35a7a6e6
--- /dev/null
+++ b/packages/demo-react-native/App/Lib/ErrorMaker.js
@@ -0,0 +1,3 @@
+export default function makeErrorForFun(message) {
+ throw new Error(message)
+}
diff --git a/packages/demo-react-native/App/Redux/LogoRedux.js b/packages/demo-react-native/App/Redux/LogoRedux.js
new file mode 100644
index 000000000..27320f272
--- /dev/null
+++ b/packages/demo-react-native/App/Redux/LogoRedux.js
@@ -0,0 +1,33 @@
+export const Types = {
+ Speed: 'logo.speed',
+ Size: 'logo.size',
+ Reset: 'logo.reset'
+}
+
+export const Actions = {
+ changeSpeed: (speed) => ({ type: Types.Speed, speed }),
+ changeSize: (size) => ({ type: Types.Size, size }),
+ reset: () => ({ type: Types.Reset })
+}
+
+export const INITIAL_STATE = {
+ size: 80,
+ speed: 25
+}
+
+const changeSpeed = (state, { speed }) => ({ ...state, speed })
+const changeSize = (state, { size }) => ({ ...state, size })
+const reset = (state, action) => INITIAL_STATE
+
+// actions ->
+const reducerMap = {
+ [Types.Speed]: changeSpeed,
+ [Types.Size]: changeSize,
+ [Types.Reset]: reset
+}
+
+// our reducer
+export const reducer = (state = INITIAL_STATE, action) => {
+ const handler = reducerMap[action.type]
+ return typeof handler === 'function' ? handler(state, action) : state
+}
diff --git a/packages/demo-react-native/App/Redux/RepoRedux.js b/packages/demo-react-native/App/Redux/RepoRedux.js
new file mode 100644
index 000000000..9846cdc7e
--- /dev/null
+++ b/packages/demo-react-native/App/Redux/RepoRedux.js
@@ -0,0 +1,48 @@
+export const Types = {
+ Request: 'repo.request',
+ Receive: 'repo.receive',
+ Failure: 'repo.failure'
+}
+
+export const Actions = {
+ request: (repo) => ({ type: Types.Request, repo }),
+ receive: (message, url, name, sha, avatar) => ({ type: Types.Receive, message, url, name, sha, avatar }),
+ failure: (error) => ({ type: Types.Failure, error })
+}
+
+export const INITIAL_STATE = {
+ message: null,
+ repo: null,
+ url: null,
+ name: null,
+ sha: null,
+ fetching: false,
+ avatar: null,
+ error: null
+}
+
+// we're going out for the repo message
+const request = (state, { repo }) =>
+ ({ ...INITIAL_STATE, fetching: true, repo })
+
+// we've got a repo message
+const receive = (state, { message, url, name, sha, avatar }) => {
+ return { ...state, fetching: false, message, url, name, sha, avatar }
+}
+
+// we failed to get the repo message :(
+const failure = (state, action) =>
+ ({ ...state, fetching: false, error: action.error })
+
+// actions ->
+const reducerMap = {
+ [Types.Request]: request,
+ [Types.Receive]: receive,
+ [Types.Failure]: failure
+}
+
+// our reducer
+export const reducer = (state = INITIAL_STATE, action) => {
+ const handler = reducerMap[action.type]
+ return typeof handler === 'function' ? handler(state, action) : state
+}
diff --git a/packages/demo-react-native/App/Redux/StartupRedux.js b/packages/demo-react-native/App/Redux/StartupRedux.js
new file mode 100644
index 000000000..65ad20f3e
--- /dev/null
+++ b/packages/demo-react-native/App/Redux/StartupRedux.js
@@ -0,0 +1,7 @@
+export const Types = {
+ Startup: 'Startup'
+}
+
+export const Actions = {
+ startup: () => ({ type: Types.Startup })
+}
diff --git a/packages/demo-react-native/App/Redux/index.js b/packages/demo-react-native/App/Redux/index.js
new file mode 100644
index 000000000..61c04c87b
--- /dev/null
+++ b/packages/demo-react-native/App/Redux/index.js
@@ -0,0 +1,43 @@
+import { combineReducers } from 'redux'
+import { reducer as repoReducer } from './RepoRedux'
+import { reducer as logoReducer } from './LogoRedux'
+import { not, contains } from 'ramda'
+import {createStore, applyMiddleware, compose} from 'redux'
+import createLogger from 'redux-logger'
+import createSagaMiddleware from 'redux-saga'
+import rootSaga from '../Sagas'
+
+// Reactotron Stuff
+import Reactotron from 'reactotron-react-native'
+import createTrackingEnhancer from 'reactotron-redux'
+
+// make our root reducer
+const rootReducer = combineReducers({
+ repo: repoReducer,
+ logo: logoReducer
+})
+
+// the logger master switch
+const USE_LOGGING = false
+
+// silence these saga-based messages
+const SAGA_LOGGING_BLACKLIST = ['EFFECT_TRIGGERED', 'EFFECT_RESOLVED', 'EFFECT_REJECTED']
+
+// create the logger
+const logger = createLogger({
+ predicate: (getState, { type }) => USE_LOGGING && not(contains(type, SAGA_LOGGING_BLACKLIST))
+})
+
+// a function which can create our store and auto-persist the data
+export default () => {
+ const sagaMiddleware = createSagaMiddleware()
+ const tracker = createTrackingEnhancer(Reactotron, {})
+ const enhancers = compose(
+ tracker,
+ applyMiddleware(logger, sagaMiddleware)
+ )
+
+ const store = createStore(rootReducer, enhancers)
+ sagaMiddleware.run(rootSaga)
+ return store
+}
diff --git a/packages/demo-react-native/App/Sagas/RepoSagas.js b/packages/demo-react-native/App/Sagas/RepoSagas.js
new file mode 100644
index 000000000..2fbb45592
--- /dev/null
+++ b/packages/demo-react-native/App/Sagas/RepoSagas.js
@@ -0,0 +1,28 @@
+import RS from 'ramdasauce'
+import { delay } from 'redux-saga'
+import { call, put } from 'redux-saga/effects'
+import * as Repo from '../Redux/RepoRedux'
+
+export function * request (api, action) {
+ try {
+ // make the call to github
+ const { repo } = action
+ const { ok, data, status, problem } = yield call(api.get, `/repos/${repo}/commits`)
+ yield call(delay, 200)
+ // are we good?
+ if (ok) {
+ const entry = RS.dotPath('0', data)
+ const { commit, author, html_url: url } = entry
+ const { message, tree } = commit
+ const { login, avatar_url } = author
+ const { sha } = tree
+ // record the last commit's message
+ yield put(Repo.Actions.receive(message, url, login, sha, avatar_url))
+ } else {
+ yield put(Repo.Actions.failure(`uh oh: ${problem} status: ${status}`))
+ }
+ } catch (e) {
+ console.log(e.message)
+ console.log(e)
+ }
+}
diff --git a/packages/demo-react-native/App/Sagas/StartupSagas.js b/packages/demo-react-native/App/Sagas/StartupSagas.js
new file mode 100644
index 000000000..63810e1a4
--- /dev/null
+++ b/packages/demo-react-native/App/Sagas/StartupSagas.js
@@ -0,0 +1,7 @@
+// import { put } from 'redux-saga/effects'
+// import * as RepoMessage from '../Redux/RepoMessage.redux'
+
+// process STARTUP actions
+export function * startup () {
+ // yield put(RepoMessage.Actions.request())
+}
diff --git a/packages/demo-react-native/App/Sagas/index.js b/packages/demo-react-native/App/Sagas/index.js
new file mode 100644
index 000000000..3492e908d
--- /dev/null
+++ b/packages/demo-react-native/App/Sagas/index.js
@@ -0,0 +1,24 @@
+import { takeEvery, takeLatest } from 'redux-saga'
+import * as Startup from '../Redux/StartupRedux'
+import * as Repo from '../Redux/RepoRedux'
+import { startup } from './StartupSagas'
+import { request as requestRepo } from './RepoSagas'
+import ApiSauce from 'apisauce'
+
+import Reactotron from 'reactotron-react-native'
+
+const api = ApiSauce.create({
+ baseURL: 'https://api.github.com',
+ headers: {
+ 'Accept': 'application/vnd.github.v3+json'
+ }
+})
+
+api.addMonitor(Reactotron.apisauce)
+
+export default function * rootSaga () {
+ yield [
+ takeLatest(Repo.Types.Request, requestRepo, api),
+ takeEvery(Startup.Types.Startup, startup)
+ ]
+}
diff --git a/packages/demo-react-native/README.md b/packages/demo-react-native/README.md
new file mode 100644
index 000000000..3ffb1af22
--- /dev/null
+++ b/packages/demo-react-native/README.md
@@ -0,0 +1,11 @@
+# React Native + Reactotron Demo
+
+### About the dependencies
+
+`socket.io` is installed as a dependency because `reactotron-react-native` is
+not installed quite the same as the other dependencies for this repo only.
+
+You won't have this requirement on your own projects. This is only important
+for those of you who want to run this demo within this project.
+
+Cobbler's shoes. Or something.
diff --git a/packages/demo-react-native/android/app/BUCK b/packages/demo-react-native/android/app/BUCK
new file mode 100644
index 000000000..97ef968ea
--- /dev/null
+++ b/packages/demo-react-native/android/app/BUCK
@@ -0,0 +1,66 @@
+import re
+
+# To learn about Buck see [Docs](https://buckbuild.com/).
+# To run your application with Buck:
+# - install Buck
+# - `npm start` - to start the packager
+# - `cd android`
+# - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"`
+# - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck
+# - `buck install -r android/app` - compile, install and run application
+#
+
+lib_deps = []
+for jarfile in glob(['libs/*.jar']):
+ name = 'jars__' + re.sub(r'^.*/([^/]+)\.jar$', r'\1', jarfile)
+ lib_deps.append(':' + name)
+ prebuilt_jar(
+ name = name,
+ binary_jar = jarfile,
+ )
+
+for aarfile in glob(['libs/*.aar']):
+ name = 'aars__' + re.sub(r'^.*/([^/]+)\.aar$', r'\1', aarfile)
+ lib_deps.append(':' + name)
+ android_prebuilt_aar(
+ name = name,
+ aar = aarfile,
+ )
+
+android_library(
+ name = 'all-libs',
+ exported_deps = lib_deps
+)
+
+android_library(
+ name = 'app-code',
+ srcs = glob([
+ 'src/main/java/**/*.java',
+ ]),
+ deps = [
+ ':all-libs',
+ ':build_config',
+ ':res',
+ ],
+)
+
+android_build_config(
+ name = 'build_config',
+ package = 'com.demoreactnative',
+)
+
+android_resource(
+ name = 'res',
+ res = 'src/main/res',
+ package = 'com.demoreactnative',
+)
+
+android_binary(
+ name = 'app',
+ package_type = 'debug',
+ manifest = 'src/main/AndroidManifest.xml',
+ keystore = '//android/keystores:debug',
+ deps = [
+ ':app-code',
+ ],
+)
diff --git a/packages/demo-react-native/android/app/build.gradle b/packages/demo-react-native/android/app/build.gradle
new file mode 100644
index 000000000..c9e30e4c7
--- /dev/null
+++ b/packages/demo-react-native/android/app/build.gradle
@@ -0,0 +1,139 @@
+apply plugin: "com.android.application"
+
+import com.android.build.OutputFile
+
+/**
+ * The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets
+ * and bundleReleaseJsAndAssets).
+ * These basically call `react-native bundle` with the correct arguments during the Android build
+ * cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the
+ * bundle directly from the development server. Below you can see all the possible configurations
+ * and their defaults. If you decide to add a configuration block, make sure to add it before the
+ * `apply from: "../../node_modules/react-native/react.gradle"` line.
+ *
+ * project.ext.react = [
+ * // the name of the generated asset file containing your JS bundle
+ * bundleAssetName: "index.android.bundle",
+ *
+ * // the entry file for bundle generation
+ * entryFile: "index.android.js",
+ *
+ * // whether to bundle JS and assets in debug mode
+ * bundleInDebug: false,
+ *
+ * // whether to bundle JS and assets in release mode
+ * bundleInRelease: true,
+ *
+ * // whether to bundle JS and assets in another build variant (if configured).
+ * // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants
+ * // The configuration property can be in the following formats
+ * // 'bundleIn${productFlavor}${buildType}'
+ * // 'bundleIn${buildType}'
+ * // bundleInFreeDebug: true,
+ * // bundleInPaidRelease: true,
+ * // bundleInBeta: true,
+ *
+ * // the root of your project, i.e. where "package.json" lives
+ * root: "../../",
+ *
+ * // where to put the JS bundle asset in debug mode
+ * jsBundleDirDebug: "$buildDir/intermediates/assets/debug",
+ *
+ * // where to put the JS bundle asset in release mode
+ * jsBundleDirRelease: "$buildDir/intermediates/assets/release",
+ *
+ * // where to put drawable resources / React Native assets, e.g. the ones you use via
+ * // require('./image.png')), in debug mode
+ * resourcesDirDebug: "$buildDir/intermediates/res/merged/debug",
+ *
+ * // where to put drawable resources / React Native assets, e.g. the ones you use via
+ * // require('./image.png')), in release mode
+ * resourcesDirRelease: "$buildDir/intermediates/res/merged/release",
+ *
+ * // by default the gradle tasks are skipped if none of the JS files or assets change; this means
+ * // that we don't look at files in android/ or ios/ to determine whether the tasks are up to
+ * // date; if you have any other folders that you want to ignore for performance reasons (gradle
+ * // indexes the entire tree), add them here. Alternatively, if you have JS files in android/
+ * // for example, you might want to remove it from here.
+ * inputExcludes: ["android/**", "ios/**"],
+ *
+ * // override which node gets called and with what additional arguments
+ * nodeExecutableAndArgs: ["node"]
+ *
+ * // supply additional arguments to the packager
+ * extraPackagerArgs: []
+ * ]
+ */
+
+apply from: "../../node_modules/react-native/react.gradle"
+
+/**
+ * Set this to true to create two separate APKs instead of one:
+ * - An APK that only works on ARM devices
+ * - An APK that only works on x86 devices
+ * The advantage is the size of the APK is reduced by about 4MB.
+ * Upload all the APKs to the Play Store and people will download
+ * the correct one based on the CPU architecture of their device.
+ */
+def enableSeparateBuildPerCPUArchitecture = false
+
+/**
+ * Run Proguard to shrink the Java bytecode in release builds.
+ */
+def enableProguardInReleaseBuilds = false
+
+android {
+ compileSdkVersion 23
+ buildToolsVersion "23.0.1"
+
+ defaultConfig {
+ applicationId "com.demoreactnative"
+ minSdkVersion 16
+ targetSdkVersion 22
+ versionCode 1
+ versionName "1.0"
+ ndk {
+ abiFilters "armeabi-v7a", "x86"
+ }
+ }
+ splits {
+ abi {
+ reset()
+ enable enableSeparateBuildPerCPUArchitecture
+ universalApk false // If true, also generate a universal APK
+ include "armeabi-v7a", "x86"
+ }
+ }
+ buildTypes {
+ release {
+ minifyEnabled enableProguardInReleaseBuilds
+ proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
+ }
+ }
+ // applicationVariants are e.g. debug, release
+ applicationVariants.all { variant ->
+ variant.outputs.each { output ->
+ // For each separate APK per architecture, set a unique version code as described here:
+ // http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits
+ def versionCodes = ["armeabi-v7a":1, "x86":2]
+ def abi = output.getFilter(OutputFile.ABI)
+ if (abi != null) { // null for the universal-debug, universal-release variants
+ output.versionCodeOverride =
+ versionCodes.get(abi) * 1048576 + defaultConfig.versionCode
+ }
+ }
+ }
+}
+
+dependencies {
+ compile fileTree(dir: "libs", include: ["*.jar"])
+ compile "com.android.support:appcompat-v7:23.0.1"
+ compile "com.facebook.react:react-native:+" // From node_modules
+}
+
+// Run this once to be able to run the application with BUCK
+// puts all compile dependencies into folder libs for BUCK to use
+task copyDownloadableDepsToLibs(type: Copy) {
+ from configurations.compile
+ into 'libs'
+}
diff --git a/packages/demo-react-native/android/app/proguard-rules.pro b/packages/demo-react-native/android/app/proguard-rules.pro
new file mode 100644
index 000000000..48361a901
--- /dev/null
+++ b/packages/demo-react-native/android/app/proguard-rules.pro
@@ -0,0 +1,66 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Disabling obfuscation is useful if you collect stack traces from production crashes
+# (unless you are using a system that supports de-obfuscate the stack traces).
+-dontobfuscate
+
+# React Native
+
+# Keep our interfaces so they can be used by other ProGuard rules.
+# See http://sourceforge.net/p/proguard/bugs/466/
+-keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip
+-keep,allowobfuscation @interface com.facebook.proguard.annotations.KeepGettersAndSetters
+-keep,allowobfuscation @interface com.facebook.common.internal.DoNotStrip
+
+# Do not strip any method/class that is annotated with @DoNotStrip
+-keep @com.facebook.proguard.annotations.DoNotStrip class *
+-keep @com.facebook.common.internal.DoNotStrip class *
+-keepclassmembers class * {
+ @com.facebook.proguard.annotations.DoNotStrip *;
+ @com.facebook.common.internal.DoNotStrip *;
+}
+
+-keepclassmembers @com.facebook.proguard.annotations.KeepGettersAndSetters class * {
+ void set*(***);
+ *** get*();
+}
+
+-keep class * extends com.facebook.react.bridge.JavaScriptModule { *; }
+-keep class * extends com.facebook.react.bridge.NativeModule { *; }
+-keepclassmembers,includedescriptorclasses class * { native ; }
+-keepclassmembers class * { @com.facebook.react.uimanager.UIProp ; }
+-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactProp ; }
+-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup ; }
+
+-dontwarn com.facebook.react.**
+
+# okhttp
+
+-keepattributes Signature
+-keepattributes *Annotation*
+-keep class okhttp3.** { *; }
+-keep interface okhttp3.** { *; }
+-dontwarn okhttp3.**
+
+# okio
+
+-keep class sun.misc.Unsafe { *; }
+-dontwarn java.nio.file.*
+-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
+-dontwarn okio.**
diff --git a/packages/demo-react-native/android/app/src/main/AndroidManifest.xml b/packages/demo-react-native/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..9de3ddcc6
--- /dev/null
+++ b/packages/demo-react-native/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/demo-react-native/android/app/src/main/java/com/demoreactnative/MainActivity.java b/packages/demo-react-native/android/app/src/main/java/com/demoreactnative/MainActivity.java
new file mode 100644
index 000000000..96c37c467
--- /dev/null
+++ b/packages/demo-react-native/android/app/src/main/java/com/demoreactnative/MainActivity.java
@@ -0,0 +1,15 @@
+package com.demoreactnative;
+
+import com.facebook.react.ReactActivity;
+
+public class MainActivity extends ReactActivity {
+
+ /**
+ * Returns the name of the main component registered from JavaScript.
+ * This is used to schedule rendering of the component.
+ */
+ @Override
+ protected String getMainComponentName() {
+ return "DemoReactNative";
+ }
+}
diff --git a/packages/demo-react-native/android/app/src/main/java/com/demoreactnative/MainApplication.java b/packages/demo-react-native/android/app/src/main/java/com/demoreactnative/MainApplication.java
new file mode 100644
index 000000000..f4c9d04c6
--- /dev/null
+++ b/packages/demo-react-native/android/app/src/main/java/com/demoreactnative/MainApplication.java
@@ -0,0 +1,35 @@
+package com.demoreactnative;
+
+import android.app.Application;
+import android.util.Log;
+
+import com.facebook.react.ReactApplication;
+import com.facebook.react.ReactInstanceManager;
+import com.facebook.react.ReactNativeHost;
+import com.facebook.react.ReactPackage;
+import com.facebook.react.shell.MainReactPackage;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class MainApplication extends Application implements ReactApplication {
+
+ private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
+ @Override
+ protected boolean getUseDeveloperSupport() {
+ return BuildConfig.DEBUG;
+ }
+
+ @Override
+ protected List getPackages() {
+ return Arrays.asList(
+ new MainReactPackage()
+ );
+ }
+ };
+
+ @Override
+ public ReactNativeHost getReactNativeHost() {
+ return mReactNativeHost;
+ }
+}
diff --git a/packages/demo-react-native/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/demo-react-native/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..cde69bccc
Binary files /dev/null and b/packages/demo-react-native/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/packages/demo-react-native/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/demo-react-native/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..c133a0cbd
Binary files /dev/null and b/packages/demo-react-native/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/packages/demo-react-native/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/demo-react-native/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..bfa42f0e7
Binary files /dev/null and b/packages/demo-react-native/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/packages/demo-react-native/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/demo-react-native/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..324e72cdd
Binary files /dev/null and b/packages/demo-react-native/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/packages/demo-react-native/android/app/src/main/res/values/strings.xml b/packages/demo-react-native/android/app/src/main/res/values/strings.xml
new file mode 100644
index 000000000..24b704db4
--- /dev/null
+++ b/packages/demo-react-native/android/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ DemoReactNative
+
diff --git a/packages/demo-react-native/android/app/src/main/res/values/styles.xml b/packages/demo-react-native/android/app/src/main/res/values/styles.xml
new file mode 100644
index 000000000..319eb0ca1
--- /dev/null
+++ b/packages/demo-react-native/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/packages/demo-react-native/android/build.gradle b/packages/demo-react-native/android/build.gradle
new file mode 100644
index 000000000..fcba4c587
--- /dev/null
+++ b/packages/demo-react-native/android/build.gradle
@@ -0,0 +1,24 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ repositories {
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:1.3.1'
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ mavenLocal()
+ jcenter()
+ maven {
+ // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
+ url "$rootDir/../node_modules/react-native/android"
+ }
+ }
+}
diff --git a/packages/demo-react-native/android/gradle.properties b/packages/demo-react-native/android/gradle.properties
new file mode 100644
index 000000000..1fd964e90
--- /dev/null
+++ b/packages/demo-react-native/android/gradle.properties
@@ -0,0 +1,20 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+# Default value: -Xmx10248m -XX:MaxPermSize=256m
+# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+
+android.useDeprecatedNdk=true
diff --git a/packages/demo-react-native/android/gradle/wrapper/gradle-wrapper.jar b/packages/demo-react-native/android/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..b5166dad4
Binary files /dev/null and b/packages/demo-react-native/android/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/packages/demo-react-native/android/gradle/wrapper/gradle-wrapper.properties b/packages/demo-react-native/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..b9fbfaba0
--- /dev/null
+++ b/packages/demo-react-native/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip
diff --git a/packages/demo-react-native/android/gradlew b/packages/demo-react-native/android/gradlew
new file mode 100755
index 000000000..91a7e269e
--- /dev/null
+++ b/packages/demo-react-native/android/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/packages/demo-react-native/android/gradlew.bat b/packages/demo-react-native/android/gradlew.bat
new file mode 100644
index 000000000..aec99730b
--- /dev/null
+++ b/packages/demo-react-native/android/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/packages/demo-react-native/android/keystores/BUCK b/packages/demo-react-native/android/keystores/BUCK
new file mode 100644
index 000000000..15da20e6b
--- /dev/null
+++ b/packages/demo-react-native/android/keystores/BUCK
@@ -0,0 +1,8 @@
+keystore(
+ name = 'debug',
+ store = 'debug.keystore',
+ properties = 'debug.keystore.properties',
+ visibility = [
+ 'PUBLIC',
+ ],
+)
diff --git a/packages/demo-react-native/android/keystores/debug.keystore.properties b/packages/demo-react-native/android/keystores/debug.keystore.properties
new file mode 100644
index 000000000..121bfb49f
--- /dev/null
+++ b/packages/demo-react-native/android/keystores/debug.keystore.properties
@@ -0,0 +1,4 @@
+key.store=debug.keystore
+key.alias=androiddebugkey
+key.store.password=android
+key.alias.password=android
diff --git a/packages/demo-react-native/android/settings.gradle b/packages/demo-react-native/android/settings.gradle
new file mode 100644
index 000000000..6fae8e5c2
--- /dev/null
+++ b/packages/demo-react-native/android/settings.gradle
@@ -0,0 +1,3 @@
+rootProject.name = 'DemoReactNative'
+
+include ':app'
diff --git a/packages/demo-react-native/copy-internal-deps.sh b/packages/demo-react-native/copy-internal-deps.sh
new file mode 100755
index 000000000..a2b72e63f
--- /dev/null
+++ b/packages/demo-react-native/copy-internal-deps.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+cp ../reactotron-react-native/dist/index.js ./node_modules/reactotron-react-native/index.js
+cp ../reactotron-core-client/dist/index.js ./node_modules/reactotron-core-client/index.js
+cp ../reactotron-redux/dist/index.js ./node_modules/reactotron-redux/index.js
+cp ../reactotron-apisauce/dist/index.js ./node_modules/reactotron-apisauce/index.js
diff --git a/examples/ReactNativeExample/index.android.js b/packages/demo-react-native/index.android.js
similarity index 60%
rename from examples/ReactNativeExample/index.android.js
rename to packages/demo-react-native/index.android.js
index 025f7a5ee..6b0ff088e 100644
--- a/examples/ReactNativeExample/index.android.js
+++ b/packages/demo-react-native/index.android.js
@@ -1,15 +1,16 @@
// imported from a file to ensure the connect statement gets run first!
import './App/Config/ReactotronConfig'
-import React, { AppRegistry, Component } from 'react-native'
+import React, { Component } from 'react'
+import { AppRegistry } from 'react-native'
import ReduxContainer from './App/Containers/ReduxContainer'
-class ReactNativeExample extends Component {
+class DemoReactNative extends Component {
render () {
return
}
}
AppRegistry.registerComponent(
- 'ReactNativeExample',
- () => ReactNativeExample
+ 'DemoReactNative',
+ () => DemoReactNative
)
diff --git a/examples/ReactNativeExample/index.ios.js b/packages/demo-react-native/index.ios.js
similarity index 60%
rename from examples/ReactNativeExample/index.ios.js
rename to packages/demo-react-native/index.ios.js
index 025f7a5ee..358d5ef3f 100644
--- a/examples/ReactNativeExample/index.ios.js
+++ b/packages/demo-react-native/index.ios.js
@@ -1,15 +1,16 @@
// imported from a file to ensure the connect statement gets run first!
+import React, { Component } from 'react'
+import { AppRegistry } from 'react-native'
import './App/Config/ReactotronConfig'
-import React, { AppRegistry, Component } from 'react-native'
import ReduxContainer from './App/Containers/ReduxContainer'
-class ReactNativeExample extends Component {
+class DemoReactNative extends Component {
render () {
return
}
}
AppRegistry.registerComponent(
- 'ReactNativeExample',
- () => ReactNativeExample
+ 'DemoReactNative',
+ () => DemoReactNative
)
diff --git a/packages/demo-react-native/ios/DemoReactNative.xcodeproj/project.pbxproj b/packages/demo-react-native/ios/DemoReactNative.xcodeproj/project.pbxproj
new file mode 100644
index 000000000..426cf3a42
--- /dev/null
+++ b/packages/demo-react-native/ios/DemoReactNative.xcodeproj/project.pbxproj
@@ -0,0 +1,765 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 46;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */; };
+ 00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */; };
+ 00C302E81ABCBA2D00DB3ED1 /* libRCTImage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */; };
+ 00C302E91ABCBA2D00DB3ED1 /* libRCTNetwork.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */; };
+ 00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302E41ABCB9EE00DB3ED1 /* libRCTVibration.a */; };
+ 00E356F31AD99517003FC87E /* DemoReactNativeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* DemoReactNativeTests.m */; };
+ 133E29F31AD74F7200F7D852 /* libRCTLinking.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 78C398B91ACF4ADC00677621 /* libRCTLinking.a */; };
+ 139105C61AF99C1200B5F7CC /* libRCTSettings.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */; };
+ 139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 139FDEF41B06529B00C62182 /* libRCTWebSocket.a */; };
+ 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; };
+ 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; };
+ 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
+ 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
+ 140ED2AC1D01E1AD002B40FF /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 146834041AC3E56700842450 /* libReact.a */; };
+ 146834051AC3E58100842450 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 146834041AC3E56700842450 /* libReact.a */; };
+ 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 00C302AB1ABCB8CE00DB3ED1 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = 134814201AA4EA6300B7C361;
+ remoteInfo = RCTActionSheet;
+ };
+ 00C302B91ABCB90400DB3ED1 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = 134814201AA4EA6300B7C361;
+ remoteInfo = RCTGeolocation;
+ };
+ 00C302BF1ABCB91800DB3ED1 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = 58B5115D1A9E6B3D00147676;
+ remoteInfo = RCTImage;
+ };
+ 00C302DB1ABCB9D200DB3ED1 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = 58B511DB1A9E6C8500147676;
+ remoteInfo = RCTNetwork;
+ };
+ 00C302E31ABCB9EE00DB3ED1 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = 832C81801AAF6DEF007FA2F7;
+ remoteInfo = RCTVibration;
+ };
+ 00E356F41AD99517003FC87E /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 13B07F861A680F5B00A75B9A;
+ remoteInfo = DemoReactNative;
+ };
+ 139105C01AF99BAD00B5F7CC /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = 134814201AA4EA6300B7C361;
+ remoteInfo = RCTSettings;
+ };
+ 139FDEF31B06529B00C62182 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = 3C86DF461ADF2C930047B81A;
+ remoteInfo = RCTWebSocket;
+ };
+ 146834031AC3E56700842450 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = 83CBBA2E1A601D0E00E9B192;
+ remoteInfo = React;
+ };
+ 78C398B81ACF4ADC00677621 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = 134814201AA4EA6300B7C361;
+ remoteInfo = RCTLinking;
+ };
+ 832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = 58B5119B1A9E6C1200147676;
+ remoteInfo = RCTText;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXFileReference section */
+ 008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = main.jsbundle; path = main.jsbundle; sourceTree = ""; };
+ 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTActionSheet.xcodeproj; path = ../node_modules/react-native/Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj; sourceTree = ""; };
+ 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTGeolocation.xcodeproj; path = ../node_modules/react-native/Libraries/Geolocation/RCTGeolocation.xcodeproj; sourceTree = ""; };
+ 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTImage.xcodeproj; path = ../node_modules/react-native/Libraries/Image/RCTImage.xcodeproj; sourceTree = ""; };
+ 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTNetwork.xcodeproj; path = ../node_modules/react-native/Libraries/Network/RCTNetwork.xcodeproj; sourceTree = ""; };
+ 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTVibration.xcodeproj; path = ../node_modules/react-native/Libraries/Vibration/RCTVibration.xcodeproj; sourceTree = ""; };
+ 00E356EE1AD99517003FC87E /* DemoReactNativeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DemoReactNativeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ 00E356F21AD99517003FC87E /* DemoReactNativeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DemoReactNativeTests.m; sourceTree = ""; };
+ 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTSettings.xcodeproj; path = ../node_modules/react-native/Libraries/Settings/RCTSettings.xcodeproj; sourceTree = ""; };
+ 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTWebSocket.xcodeproj; path = ../node_modules/react-native/Libraries/WebSocket/RCTWebSocket.xcodeproj; sourceTree = ""; };
+ 13B07F961A680F5B00A75B9A /* DemoReactNative.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DemoReactNative.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = DemoReactNative/AppDelegate.h; sourceTree = ""; };
+ 13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = DemoReactNative/AppDelegate.m; sourceTree = ""; };
+ 13B07FB21A68108700A75B9A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; };
+ 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = DemoReactNative/Images.xcassets; sourceTree = ""; };
+ 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = DemoReactNative/Info.plist; sourceTree = ""; };
+ 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = DemoReactNative/main.m; sourceTree = ""; };
+ 146833FF1AC3E56700842450 /* React.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = React.xcodeproj; path = ../node_modules/react-native/React/React.xcodeproj; sourceTree = ""; };
+ 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = ../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj; sourceTree = ""; };
+ 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = ../node_modules/react-native/Libraries/Text/RCTText.xcodeproj; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 00E356EB1AD99517003FC87E /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 140ED2AC1D01E1AD002B40FF /* libReact.a in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 13B07F8C1A680F5B00A75B9A /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 146834051AC3E58100842450 /* libReact.a in Frameworks */,
+ 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */,
+ 00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */,
+ 00C302E81ABCBA2D00DB3ED1 /* libRCTImage.a in Frameworks */,
+ 133E29F31AD74F7200F7D852 /* libRCTLinking.a in Frameworks */,
+ 00C302E91ABCBA2D00DB3ED1 /* libRCTNetwork.a in Frameworks */,
+ 139105C61AF99C1200B5F7CC /* libRCTSettings.a in Frameworks */,
+ 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */,
+ 00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */,
+ 139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 00C302A81ABCB8CE00DB3ED1 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 00C302B61ABCB90400DB3ED1 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 00C302BC1ABCB91800DB3ED1 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 00C302D41ABCB9D200DB3ED1 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 00C302E01ABCB9EE00DB3ED1 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 00C302E41ABCB9EE00DB3ED1 /* libRCTVibration.a */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 00E356EF1AD99517003FC87E /* DemoReactNativeTests */ = {
+ isa = PBXGroup;
+ children = (
+ 00E356F21AD99517003FC87E /* DemoReactNativeTests.m */,
+ 00E356F01AD99517003FC87E /* Supporting Files */,
+ );
+ path = DemoReactNativeTests;
+ sourceTree = "";
+ };
+ 00E356F01AD99517003FC87E /* Supporting Files */ = {
+ isa = PBXGroup;
+ children = (
+ 00E356F11AD99517003FC87E /* Info.plist */,
+ );
+ name = "Supporting Files";
+ sourceTree = "";
+ };
+ 139105B71AF99BAD00B5F7CC /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 139FDEE71B06529A00C62182 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 139FDEF41B06529B00C62182 /* libRCTWebSocket.a */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 13B07FAE1A68108700A75B9A /* DemoReactNative */ = {
+ isa = PBXGroup;
+ children = (
+ 008F07F21AC5B25A0029DE68 /* main.jsbundle */,
+ 13B07FAF1A68108700A75B9A /* AppDelegate.h */,
+ 13B07FB01A68108700A75B9A /* AppDelegate.m */,
+ 13B07FB51A68108700A75B9A /* Images.xcassets */,
+ 13B07FB61A68108700A75B9A /* Info.plist */,
+ 13B07FB11A68108700A75B9A /* LaunchScreen.xib */,
+ 13B07FB71A68108700A75B9A /* main.m */,
+ );
+ name = DemoReactNative;
+ sourceTree = "";
+ };
+ 146834001AC3E56700842450 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 146834041AC3E56700842450 /* libReact.a */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 78C398B11ACF4ADC00677621 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 78C398B91ACF4ADC00677621 /* libRCTLinking.a */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 832341AE1AAA6A7D00B99B32 /* Libraries */ = {
+ isa = PBXGroup;
+ children = (
+ 146833FF1AC3E56700842450 /* React.xcodeproj */,
+ 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */,
+ 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */,
+ 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */,
+ 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */,
+ 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */,
+ 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */,
+ 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */,
+ 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */,
+ 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */,
+ );
+ name = Libraries;
+ sourceTree = "";
+ };
+ 832341B11AAA6A8300B99B32 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 832341B51AAA6A8300B99B32 /* libRCTText.a */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 83CBB9F61A601CBA00E9B192 = {
+ isa = PBXGroup;
+ children = (
+ 13B07FAE1A68108700A75B9A /* DemoReactNative */,
+ 832341AE1AAA6A7D00B99B32 /* Libraries */,
+ 00E356EF1AD99517003FC87E /* DemoReactNativeTests */,
+ 83CBBA001A601CBA00E9B192 /* Products */,
+ );
+ indentWidth = 2;
+ sourceTree = "";
+ tabWidth = 2;
+ };
+ 83CBBA001A601CBA00E9B192 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 13B07F961A680F5B00A75B9A /* DemoReactNative.app */,
+ 00E356EE1AD99517003FC87E /* DemoReactNativeTests.xctest */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 00E356ED1AD99517003FC87E /* DemoReactNativeTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "DemoReactNativeTests" */;
+ buildPhases = (
+ 00E356EA1AD99517003FC87E /* Sources */,
+ 00E356EB1AD99517003FC87E /* Frameworks */,
+ 00E356EC1AD99517003FC87E /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 00E356F51AD99517003FC87E /* PBXTargetDependency */,
+ );
+ name = DemoReactNativeTests;
+ productName = DemoReactNativeTests;
+ productReference = 00E356EE1AD99517003FC87E /* DemoReactNativeTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ 13B07F861A680F5B00A75B9A /* DemoReactNative */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "DemoReactNative" */;
+ buildPhases = (
+ 13B07F871A680F5B00A75B9A /* Sources */,
+ 13B07F8C1A680F5B00A75B9A /* Frameworks */,
+ 13B07F8E1A680F5B00A75B9A /* Resources */,
+ 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = DemoReactNative;
+ productName = "Hello World";
+ productReference = 13B07F961A680F5B00A75B9A /* DemoReactNative.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 83CBB9F71A601CBA00E9B192 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastUpgradeCheck = 0610;
+ ORGANIZATIONNAME = Facebook;
+ TargetAttributes = {
+ 00E356ED1AD99517003FC87E = {
+ CreatedOnToolsVersion = 6.2;
+ TestTargetID = 13B07F861A680F5B00A75B9A;
+ };
+ };
+ };
+ buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "DemoReactNative" */;
+ compatibilityVersion = "Xcode 3.2";
+ developmentRegion = English;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 83CBB9F61A601CBA00E9B192;
+ productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
+ projectDirPath = "";
+ projectReferences = (
+ {
+ ProductGroup = 00C302A81ABCB8CE00DB3ED1 /* Products */;
+ ProjectRef = 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */;
+ },
+ {
+ ProductGroup = 00C302B61ABCB90400DB3ED1 /* Products */;
+ ProjectRef = 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */;
+ },
+ {
+ ProductGroup = 00C302BC1ABCB91800DB3ED1 /* Products */;
+ ProjectRef = 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */;
+ },
+ {
+ ProductGroup = 78C398B11ACF4ADC00677621 /* Products */;
+ ProjectRef = 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */;
+ },
+ {
+ ProductGroup = 00C302D41ABCB9D200DB3ED1 /* Products */;
+ ProjectRef = 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */;
+ },
+ {
+ ProductGroup = 139105B71AF99BAD00B5F7CC /* Products */;
+ ProjectRef = 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */;
+ },
+ {
+ ProductGroup = 832341B11AAA6A8300B99B32 /* Products */;
+ ProjectRef = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */;
+ },
+ {
+ ProductGroup = 00C302E01ABCB9EE00DB3ED1 /* Products */;
+ ProjectRef = 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */;
+ },
+ {
+ ProductGroup = 139FDEE71B06529A00C62182 /* Products */;
+ ProjectRef = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */;
+ },
+ {
+ ProductGroup = 146834001AC3E56700842450 /* Products */;
+ ProjectRef = 146833FF1AC3E56700842450 /* React.xcodeproj */;
+ },
+ );
+ projectRoot = "";
+ targets = (
+ 13B07F861A680F5B00A75B9A /* DemoReactNative */,
+ 00E356ED1AD99517003FC87E /* DemoReactNativeTests */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXReferenceProxy section */
+ 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */ = {
+ isa = PBXReferenceProxy;
+ fileType = archive.ar;
+ path = libRCTActionSheet.a;
+ remoteRef = 00C302AB1ABCB8CE00DB3ED1 /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */ = {
+ isa = PBXReferenceProxy;
+ fileType = archive.ar;
+ path = libRCTGeolocation.a;
+ remoteRef = 00C302B91ABCB90400DB3ED1 /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */ = {
+ isa = PBXReferenceProxy;
+ fileType = archive.ar;
+ path = libRCTImage.a;
+ remoteRef = 00C302BF1ABCB91800DB3ED1 /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ 00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */ = {
+ isa = PBXReferenceProxy;
+ fileType = archive.ar;
+ path = libRCTNetwork.a;
+ remoteRef = 00C302DB1ABCB9D200DB3ED1 /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ 00C302E41ABCB9EE00DB3ED1 /* libRCTVibration.a */ = {
+ isa = PBXReferenceProxy;
+ fileType = archive.ar;
+ path = libRCTVibration.a;
+ remoteRef = 00C302E31ABCB9EE00DB3ED1 /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */ = {
+ isa = PBXReferenceProxy;
+ fileType = archive.ar;
+ path = libRCTSettings.a;
+ remoteRef = 139105C01AF99BAD00B5F7CC /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ 139FDEF41B06529B00C62182 /* libRCTWebSocket.a */ = {
+ isa = PBXReferenceProxy;
+ fileType = archive.ar;
+ path = libRCTWebSocket.a;
+ remoteRef = 139FDEF31B06529B00C62182 /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ 146834041AC3E56700842450 /* libReact.a */ = {
+ isa = PBXReferenceProxy;
+ fileType = archive.ar;
+ path = libReact.a;
+ remoteRef = 146834031AC3E56700842450 /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ 78C398B91ACF4ADC00677621 /* libRCTLinking.a */ = {
+ isa = PBXReferenceProxy;
+ fileType = archive.ar;
+ path = libRCTLinking.a;
+ remoteRef = 78C398B81ACF4ADC00677621 /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ 832341B51AAA6A8300B99B32 /* libRCTText.a */ = {
+ isa = PBXReferenceProxy;
+ fileType = archive.ar;
+ path = libRCTText.a;
+ remoteRef = 832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+/* End PBXReferenceProxy section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 00E356EC1AD99517003FC87E /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 13B07F8E1A680F5B00A75B9A /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
+ 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Bundle React Native code and images";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "export NODE_BINARY=node\n../node_modules/react-native/packager/react-native-xcode.sh";
+ showEnvVarsInLog = 1;
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 00E356EA1AD99517003FC87E /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 00E356F31AD99517003FC87E /* DemoReactNativeTests.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 13B07F871A680F5B00A75B9A /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */,
+ 13B07FC11A68108700A75B9A /* main.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 00E356F51AD99517003FC87E /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 13B07F861A680F5B00A75B9A /* DemoReactNative */;
+ targetProxy = 00E356F41AD99517003FC87E /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+ 13B07FB11A68108700A75B9A /* LaunchScreen.xib */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 13B07FB21A68108700A75B9A /* Base */,
+ );
+ name = LaunchScreen.xib;
+ path = DemoReactNative;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 00E356F61AD99517003FC87E /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ INFOPLIST_FILE = DemoReactNativeTests/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 8.2;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DemoReactNative.app/DemoReactNative";
+ };
+ name = Debug;
+ };
+ 00E356F71AD99517003FC87E /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ COPY_PHASE_STRIP = NO;
+ INFOPLIST_FILE = DemoReactNativeTests/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 8.2;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DemoReactNative.app/DemoReactNative";
+ };
+ name = Release;
+ };
+ 13B07F941A680F5B00A75B9A /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ DEAD_CODE_STRIPPING = NO;
+ HEADER_SEARCH_PATHS = (
+ "$(inherited)",
+ /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
+ "$(SRCROOT)/../node_modules/react-native/React/**",
+ );
+ INFOPLIST_FILE = "DemoReactNative/Info.plist";
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ OTHER_LDFLAGS = (
+ "$(inherited)",
+ "-ObjC",
+ "-lc++",
+ );
+ PRODUCT_NAME = DemoReactNative;
+ };
+ name = Debug;
+ };
+ 13B07F951A680F5B00A75B9A /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ HEADER_SEARCH_PATHS = (
+ "$(inherited)",
+ /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
+ "$(SRCROOT)/../node_modules/react-native/React/**",
+ );
+ INFOPLIST_FILE = "DemoReactNative/Info.plist";
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ OTHER_LDFLAGS = (
+ "$(inherited)",
+ "-ObjC",
+ "-lc++",
+ );
+ PRODUCT_NAME = DemoReactNative;
+ };
+ name = Release;
+ };
+ 83CBBA201A601CBA00E9B192 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_SYMBOLS_PRIVATE_EXTERN = NO;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ HEADER_SEARCH_PATHS = (
+ "$(inherited)",
+ /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
+ "$(SRCROOT)/../node_modules/react-native/React/**",
+ );
+ IPHONEOS_DEPLOYMENT_TARGET = 7.0;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ };
+ name = Debug;
+ };
+ 83CBBA211A601CBA00E9B192 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = YES;
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ HEADER_SEARCH_PATHS = (
+ "$(inherited)",
+ /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
+ "$(SRCROOT)/../node_modules/react-native/React/**",
+ );
+ IPHONEOS_DEPLOYMENT_TARGET = 7.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "DemoReactNativeTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 00E356F61AD99517003FC87E /* Debug */,
+ 00E356F71AD99517003FC87E /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "DemoReactNative" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 13B07F941A680F5B00A75B9A /* Debug */,
+ 13B07F951A680F5B00A75B9A /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "DemoReactNative" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 83CBBA201A601CBA00E9B192 /* Debug */,
+ 83CBBA211A601CBA00E9B192 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */;
+}
diff --git a/packages/demo-react-native/ios/DemoReactNative.xcodeproj/xcshareddata/xcschemes/DemoReactNative.xcscheme b/packages/demo-react-native/ios/DemoReactNative.xcodeproj/xcshareddata/xcschemes/DemoReactNative.xcscheme
new file mode 100644
index 000000000..7cb041277
--- /dev/null
+++ b/packages/demo-react-native/ios/DemoReactNative.xcodeproj/xcshareddata/xcschemes/DemoReactNative.xcscheme
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/demo-react-native/ios/DemoReactNative/AppDelegate.h b/packages/demo-react-native/ios/DemoReactNative/AppDelegate.h
new file mode 100644
index 000000000..a9654d5e0
--- /dev/null
+++ b/packages/demo-react-native/ios/DemoReactNative/AppDelegate.h
@@ -0,0 +1,16 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+#import
+
+@interface AppDelegate : UIResponder
+
+@property (nonatomic, strong) UIWindow *window;
+
+@end
diff --git a/packages/demo-react-native/ios/DemoReactNative/AppDelegate.m b/packages/demo-react-native/ios/DemoReactNative/AppDelegate.m
new file mode 100644
index 000000000..96ffe5d42
--- /dev/null
+++ b/packages/demo-react-native/ios/DemoReactNative/AppDelegate.m
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+#import "AppDelegate.h"
+
+#import "RCTBundleURLProvider.h"
+#import "RCTRootView.h"
+
+@implementation AppDelegate
+
+- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
+{
+ NSURL *jsCodeLocation;
+
+ jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil];
+
+ RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
+ moduleName:@"DemoReactNative"
+ initialProperties:nil
+ launchOptions:launchOptions];
+ rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];
+
+ self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
+ UIViewController *rootViewController = [UIViewController new];
+ rootViewController.view = rootView;
+ self.window.rootViewController = rootViewController;
+ [self.window makeKeyAndVisible];
+ return YES;
+}
+
+@end
diff --git a/packages/demo-react-native/ios/DemoReactNative/Base.lproj/LaunchScreen.xib b/packages/demo-react-native/ios/DemoReactNative/Base.lproj/LaunchScreen.xib
new file mode 100644
index 000000000..36ef0a7a5
--- /dev/null
+++ b/packages/demo-react-native/ios/DemoReactNative/Base.lproj/LaunchScreen.xib
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/demo-react-native/ios/DemoReactNative/Images.xcassets/AppIcon.appiconset/Contents.json b/packages/demo-react-native/ios/DemoReactNative/Images.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 000000000..118c98f74
--- /dev/null
+++ b/packages/demo-react-native/ios/DemoReactNative/Images.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "images" : [
+ {
+ "idiom" : "iphone",
+ "size" : "29x29",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "29x29",
+ "scale" : "3x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "40x40",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "40x40",
+ "scale" : "3x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "60x60",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "60x60",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/packages/demo-react-native/ios/DemoReactNative/Info.plist b/packages/demo-react-native/ios/DemoReactNative/Info.plist
new file mode 100644
index 000000000..f06097249
--- /dev/null
+++ b/packages/demo-react-native/ios/DemoReactNative/Info.plist
@@ -0,0 +1,58 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 1.0
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 1
+ LSRequiresIPhoneOS
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIRequiredDeviceCapabilities
+
+ armv7
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UIViewControllerBasedStatusBarAppearance
+
+ NSLocationWhenInUseUsageDescription
+
+ NSAppTransportSecurity
+
+ NSExceptionDomains
+
+ localhost
+
+ NSTemporaryExceptionAllowsInsecureHTTPLoads
+
+
+ openweathermap.org
+
+ NSTemporaryExceptionAllowsInsecureHTTPLoads
+
+
+
+
+
+
diff --git a/packages/demo-react-native/ios/DemoReactNative/main.m b/packages/demo-react-native/ios/DemoReactNative/main.m
new file mode 100644
index 000000000..3d767fcbb
--- /dev/null
+++ b/packages/demo-react-native/ios/DemoReactNative/main.m
@@ -0,0 +1,18 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+#import
+
+#import "AppDelegate.h"
+
+int main(int argc, char * argv[]) {
+ @autoreleasepool {
+ return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
+ }
+}
diff --git a/packages/demo-react-native/ios/DemoReactNativeTests/DemoReactNativeTests.m b/packages/demo-react-native/ios/DemoReactNativeTests/DemoReactNativeTests.m
new file mode 100644
index 000000000..6c2c01fd9
--- /dev/null
+++ b/packages/demo-react-native/ios/DemoReactNativeTests/DemoReactNativeTests.m
@@ -0,0 +1,70 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+#import
+#import
+
+#import "RCTLog.h"
+#import "RCTRootView.h"
+
+#define TIMEOUT_SECONDS 600
+#define TEXT_TO_LOOK_FOR @"Welcome to React Native!"
+
+@interface DemoReactNativeTests : XCTestCase
+
+@end
+
+@implementation DemoReactNativeTests
+
+- (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test
+{
+ if (test(view)) {
+ return YES;
+ }
+ for (UIView *subview in [view subviews]) {
+ if ([self findSubviewInView:subview matching:test]) {
+ return YES;
+ }
+ }
+ return NO;
+}
+
+- (void)testRendersWelcomeScreen
+{
+ UIViewController *vc = [[[[UIApplication sharedApplication] delegate] window] rootViewController];
+ NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS];
+ BOOL foundElement = NO;
+
+ __block NSString *redboxError = nil;
+ RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) {
+ if (level >= RCTLogLevelError) {
+ redboxError = message;
+ }
+ });
+
+ while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) {
+ [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
+ [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
+
+ foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) {
+ if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) {
+ return YES;
+ }
+ return NO;
+ }];
+ }
+
+ RCTSetLogFunction(RCTDefaultLogFunction);
+
+ XCTAssertNil(redboxError, @"RedBox error: %@", redboxError);
+ XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS);
+}
+
+
+@end
diff --git a/packages/demo-react-native/ios/DemoReactNativeTests/Info.plist b/packages/demo-react-native/ios/DemoReactNativeTests/Info.plist
new file mode 100644
index 000000000..886825ccc
--- /dev/null
+++ b/packages/demo-react-native/ios/DemoReactNativeTests/Info.plist
@@ -0,0 +1,24 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ BNDL
+ CFBundleShortVersionString
+ 1.0
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 1
+
+
diff --git a/packages/demo-react-native/package.json b/packages/demo-react-native/package.json
new file mode 100644
index 000000000..8e6f1810b
--- /dev/null
+++ b/packages/demo-react-native/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "DemoReactNative",
+ "version": "0.94.0",
+ "private": true,
+ "scripts": {
+ "start": "node node_modules/react-native/local-cli/cli.js start",
+ "copy-internal-deps": "./copy-internal-deps.sh"
+ },
+ "dependencies": {
+ "apisauce": "^0.3.0",
+ "ramda": "^0.22.0",
+ "ramdasauce": "^1.0.0",
+ "react": "15.2.1",
+ "react-native": "^0.31.0",
+ "react-redux": "^4.4.5",
+ "redux": "^3.5.1",
+ "redux-logger": "^2.6.1",
+ "redux-saga": "^0.11.0",
+ "seamless-immutable": "^6.0.1",
+ "socket.io": "^1.4.8",
+ "reactotron-core-client": "^0.94.0",
+ "reactotron-react-native": "^0.94.0",
+ "reactotron-apisauce": "^0.94.0",
+ "reactotron-redux": "^0.94.0"
+ },
+ "globals": [
+ "__DEV__"
+ ],
+ "standard": {
+ "parser": "eslint"
+ }
+}
diff --git a/packages/reactotron-apisauce/.babelrc b/packages/reactotron-apisauce/.babelrc
new file mode 100644
index 000000000..e66618ff6
--- /dev/null
+++ b/packages/reactotron-apisauce/.babelrc
@@ -0,0 +1,3 @@
+{
+ "presets": [ "es2015", "stage-1" ]
+}
diff --git a/packages/reactotron-apisauce/.gitignore b/packages/reactotron-apisauce/.gitignore
new file mode 100644
index 000000000..a78e18fe1
--- /dev/null
+++ b/packages/reactotron-apisauce/.gitignore
@@ -0,0 +1,5 @@
+node_modules
+npm-debug.log
+coverage
+.nyc_output
+dist
diff --git a/packages/reactotron-apisauce/LICENSE b/packages/reactotron-apisauce/LICENSE
new file mode 100644
index 000000000..b36092b50
--- /dev/null
+++ b/packages/reactotron-apisauce/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 Steve Kellock
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/reactotron-apisauce/README.md b/packages/reactotron-apisauce/README.md
new file mode 100644
index 000000000..2ee93721f
--- /dev/null
+++ b/packages/reactotron-apisauce/README.md
@@ -0,0 +1,40 @@
+# reactotron-redux
+
+Converts responses sent via (apisauce)[https://github.com/skellock/apisauce] into
+Reactotron.
+
+# Installing
+
+`npm i --save-dev reactotron-apisauce`
+
+
+# Configuring
+
+In the file that you create your Redux store, add these two imports at the top:
+
+```js
+// in your reactotron config (where you setup Reactotron) add this as a plugin.
+import tronsauce from 'reactotron-apisauce'
+
+// then plug it in when you configure Reactotron.
+
+Reactotron
+ .configure()
+ .use(tronsauce) // <-- here we go!!!
+ .connect()
+
+// meanwhile, in a different file, when you get a response back
+// from apisauce, pass it `Reactotron.apisauce(myAwesomeResponse)`
+Reactotron.apisauce(theResponseWeJustTalkedAbout)
+
+// Apisauce has a feature where you can attach a handler to watch
+// all requests/response flowing through your api. You can hook this up:
+api.addMonitor(Reactotron.apisauce)
+
+// or if you just wanted to track on 500's
+api.addMonitor(response => {
+ if (response.problem === 'SERVER_ERROR')
+ Reactotron.apisauce(response)
+})
+
+// see https://github.com/skellock/apisauce for more details.
diff --git a/packages/reactotron-apisauce/package.json b/packages/reactotron-apisauce/package.json
new file mode 100644
index 000000000..00018f00f
--- /dev/null
+++ b/packages/reactotron-apisauce/package.json
@@ -0,0 +1,61 @@
+{
+ "name": "reactotron-apisauce",
+ "version": "0.94.0",
+ "description": "A Reactotron plugin for Apisauce.",
+ "main": "dist/index.js",
+ "scripts": {
+ "test": "ava",
+ "watch": "ava --watch",
+ "coverage": "nyc ava",
+ "build": "rollup -c"
+ },
+ "repository": "https://github.com/reactotron/reactotron/tree/master/packages/reactotron-apisauce",
+ "author": "Steve Kellock",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/reactotron/reactotron/issues"
+ },
+ "homepage": "https://reactotron.com",
+ "files": [
+ "dist",
+ "LICENSE",
+ "README.md"
+ ],
+ "peerDependencies": {},
+ "devDependencies": {
+ "ava": "^0.16.0",
+ "babel-cli": "^6.11.4",
+ "babel-core": "^6.13.2",
+ "babel-eslint": "^6.1.2",
+ "babel-preset-es2015": "^6.13.2",
+ "babel-preset-es2015-rollup": "^1.2.0",
+ "babel-preset-stage-1": "^6.13.0",
+ "json-server": "^0.8.17",
+ "mockery": "^1.7.0",
+ "nyc": "^8.1.0",
+ "rollup": "^0.34.8",
+ "rollup-plugin-babel": "^2.6.1",
+ "standard": "^7.1.2"
+ },
+ "ava": {
+ "require": [
+ "babel-core/register"
+ ],
+ "babel": {
+ "babelrc": false,
+ "presets": [
+ "es2015",
+ "stage-1"
+ ]
+ }
+ },
+ "standard": {
+ "parser": "babel-eslint"
+ },
+ "dependencies": {
+ "apisauce": "^0.3.0",
+ "ramda": "^0.22.1",
+ "ramdasauce": "^1.1.0",
+ "reactotron-core-client": "^0.94.0"
+ }
+}
diff --git a/packages/reactotron-apisauce/rollup.config.js b/packages/reactotron-apisauce/rollup.config.js
new file mode 100644
index 000000000..95b71c52b
--- /dev/null
+++ b/packages/reactotron-apisauce/rollup.config.js
@@ -0,0 +1,14 @@
+import babel from 'rollup-plugin-babel'
+
+export default {
+ entry: 'src/index.js',
+ format: 'cjs',
+ plugins: [
+ babel({
+ babelrc: false,
+ runtimeHelpers: true,
+ presets: ['es2015-rollup', 'stage-1']
+ })
+ ],
+ dest: 'dist/index.js'
+}
diff --git a/packages/reactotron-apisauce/src/index.js b/packages/reactotron-apisauce/src/index.js
new file mode 100644
index 000000000..ee8cf8c14
--- /dev/null
+++ b/packages/reactotron-apisauce/src/index.js
@@ -0,0 +1,28 @@
+import RS from 'ramdasauce'
+
+// apisauce uses axios, so let's deconstruct that format
+const convertResponse = (source) => {
+ const url = RS.dotPath('config.url', source)
+ const method = RS.dotPath('config.method', source)
+ const requestData = RS.dotPath('config.data', source)
+ const requestHeaders = RS.dotPath('config.headers', source)
+ const duration = RS.dotPath('duration', source)
+ const status = RS.dotPath('status', source)
+ const body = RS.dotPath('data', source)
+ const responseHeaders = RS.dotPath('headers', source)
+ const request = { url, method, data: requestData, headers: requestHeaders }
+ const response = { body, status, headers: responseHeaders }
+
+ return [ request, response, duration ]
+}
+
+/**
+ * Sends an apisauce response to the server.
+ */
+export default () => reactotron => {
+ return {
+ features: {
+ apisauce: (source) => reactotron.apiResponse(...convertResponse(source))
+ }
+ }
+}
diff --git a/packages/reactotron-apisauce/test/_get-free-port.js b/packages/reactotron-apisauce/test/_get-free-port.js
new file mode 100644
index 000000000..fade86c5f
--- /dev/null
+++ b/packages/reactotron-apisauce/test/_get-free-port.js
@@ -0,0 +1,9 @@
+import net from 'net'
+
+export default function getFreePort (cb) {
+ const server = net.createServer()
+ server.listen(() => {
+ const port = server.address().port
+ server.close(() => cb(port))
+ })
+}
diff --git a/packages/reactotron-apisauce/test/plugin-test.js b/packages/reactotron-apisauce/test/plugin-test.js
new file mode 100644
index 000000000..7e92e2e13
--- /dev/null
+++ b/packages/reactotron-apisauce/test/plugin-test.js
@@ -0,0 +1,55 @@
+import test from 'ava'
+import jsonServer from 'json-server'
+import getFreePort from './_get-free-port'
+import apisauce from 'apisauce'
+import createPlugin from '../src/index'
+
+test.cb('parses responses', t => {
+ getFreePort(port => {
+ // create a json server
+ const server = jsonServer.create()
+
+ // hookup some leet server codez
+ server.get('/hey', (req, res) => {
+ res.json({ a: 'ok', b: 1 })
+ })
+
+ // start listening
+ server.listen(port)
+
+ // captured these guys from our fake plugin
+ let request
+ let response
+ let duration
+
+ // create a fake plugin to receive the real plugin's functionality
+ const plugin = createPlugin()({
+ apiResponse: (a, b, c) => {
+ request = a
+ response = b
+ duration = c
+ }
+ })
+
+ // create the api
+ const api = apisauce.create({ baseURL: `http://localhost:${port}` })
+
+ // make the call
+ api
+ .get('/hey')
+ .then(plugin.features.apisauce)
+ .then(() => {
+ // can't seem to deep equals here... it's like we're getting a wierd Object()
+ t.is(request.url, `http://localhost:${port}/hey`)
+ t.is(request.method, 'get')
+ t.is(request.headers['Accept'], 'application/json, text/plain, */*')
+ t.is(request.headers['User-Agent'], 'axios/0.12.0')
+ t.falsy(request.data)
+ t.deepEqual(response.body, {a: 'ok', b: 1})
+ t.is(response.status, 200)
+ t.is(response.headers['x-powered-by'], 'Express')
+ t.true(duration > 0)
+ t.end()
+ })
+ })
+})
diff --git a/packages/reactotron-app/.babelrc b/packages/reactotron-app/.babelrc
new file mode 100644
index 000000000..7607eb182
--- /dev/null
+++ b/packages/reactotron-app/.babelrc
@@ -0,0 +1,14 @@
+{
+ "presets": ["es2015", "stage-0", "react"],
+ "plugins": ["transform-decorators-legacy", "add-module-exports"],
+ "env": {
+ "development": {
+ "presets": ["react-hmre"]
+ },
+ "test": {
+ "plugins": [
+ ["webpack-loaders", { "config": "webpack.config.node.js", "verbose": false }]
+ ]
+ }
+ }
+}
diff --git a/packages/reactotron-app/.editorconfig b/packages/reactotron-app/.editorconfig
new file mode 100644
index 000000000..7ec999433
--- /dev/null
+++ b/packages/reactotron-app/.editorconfig
@@ -0,0 +1,19 @@
+root = true
+
+[*]
+indent_style = space
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.{json,js,jsx,html,css}]
+indent_style = space
+indent_size = 2
+
+[.eslintrc]
+indent_style = space
+indent_size = 2
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/packages/reactotron-app/.gitattributes b/packages/reactotron-app/.gitattributes
new file mode 100644
index 000000000..176a458f9
--- /dev/null
+++ b/packages/reactotron-app/.gitattributes
@@ -0,0 +1 @@
+* text=auto
diff --git a/packages/reactotron-app/.gitignore b/packages/reactotron-app/.gitignore
new file mode 100644
index 000000000..177a2d21e
--- /dev/null
+++ b/packages/reactotron-app/.gitignore
@@ -0,0 +1,36 @@
+# Logs
+logs
+*.log
+
+# Runtime data
+pids
+*.pid
+*.seed
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+
+# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (http://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directory
+# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
+node_modules
+
+# OSX
+.DS_Store
+
+# App packaged
+dist
+release
+main.js
+main.js.map
diff --git a/packages/reactotron-app/App/App.icns b/packages/reactotron-app/App/App.icns
new file mode 100644
index 000000000..43dd20705
Binary files /dev/null and b/packages/reactotron-app/App/App.icns differ
diff --git a/packages/reactotron-app/App/Commands/ApiResponseCommand.js b/packages/reactotron-app/App/Commands/ApiResponseCommand.js
new file mode 100644
index 000000000..12a4e649e
--- /dev/null
+++ b/packages/reactotron-app/App/Commands/ApiResponseCommand.js
@@ -0,0 +1,137 @@
+import React, { Component, PropTypes } from 'react'
+import Command from '../Shared/Command'
+import ObjectTree from '../Shared/ObjectTree'
+import { dotPath, isNilOrEmpty } from 'ramdasauce'
+import { toUpper, equals } from 'ramda'
+import makeTable from '../Shared/MakeTable'
+import Colors from '../Theme/Colors'
+import AppStyles from '../Theme/AppStyles'
+import SectionLink from './SectionLink'
+import Content from '../Shared/Content'
+
+const COMMAND_TITLE = 'API RESPONSE'
+const REQUEST_HEADER_TITLE = 'Request Headers'
+const RESPONSE_HEADER_TITLE = 'Response Headers'
+const REQUEST_BODY_TITLE = 'Request'
+const RESPONSE_BODY_TITLE = 'Response'
+const NO_REQUEST_BODY = 'Nothing sent.'
+
+const Styles = {
+ container: {
+ },
+ method: {},
+ status: {},
+ duration: {
+ },
+ url: {
+ wordBreak: 'break-all',
+ color: Colors.constant,
+ paddingBottom: 10
+ },
+ headerTitle: {
+ margin: 0,
+ padding: 0,
+ paddingTop: 8,
+ paddingBottom: 0,
+ color: Colors.constant
+ },
+ pre: {
+ whiteSpace: 'pre-wrap'
+ },
+ sectionLinks: {
+ ...AppStyles.Layout.hbox,
+ paddingTop: 10,
+ paddingBottom: 10
+ },
+ spacer: {
+ flex: 1
+ }
+}
+
+const INITIAL_STATE = {
+ showRequestHeaders: false,
+ showResponseHeaders: false,
+ showRequestBody: false,
+ showResponseBody: false
+}
+
+class ApiResponseCommand extends Component {
+
+ static propTypes = {
+ command: PropTypes.oneOfType([PropTypes.object, PropTypes.array]).isRequired
+ }
+
+ constructor (props) {
+ super(props)
+ this.state = INITIAL_STATE
+ this.toggleRequestHeaders = this.toggleRequestHeaders.bind(this)
+ this.toggleResponseHeaders = this.toggleResponseHeaders.bind(this)
+ this.toggleRequestBody = this.toggleRequestBody.bind(this)
+ this.toggleResponseBody = this.toggleResponseBody.bind(this)
+ }
+
+ toggleRequestHeaders () {
+ this.setState({ ...INITIAL_STATE, showRequestHeaders: !this.state.showRequestHeaders })
+ }
+
+ toggleResponseHeaders () {
+ this.setState({ ...INITIAL_STATE, showResponseHeaders: !this.state.showResponseHeaders })
+ }
+
+ toggleRequestBody () {
+ this.setState({ ...INITIAL_STATE, showRequestBody: !this.state.showRequestBody })
+ }
+
+ toggleResponseBody () {
+ this.setState({ ...INITIAL_STATE, showResponseBody: !this.state.showResponseBody })
+ }
+
+ shouldComponentUpdate (nextProps, nextState) {
+ return !(equals(nextProps, this.props) && equals(this.state, nextState))
+ }
+
+ render () {
+ const { command } = this.props
+ const { showRequestHeaders, showResponseHeaders, showRequestBody, showResponseBody } = this.state
+ const { payload } = command
+ const { duration } = payload
+ const status = dotPath('response.status', payload)
+ const url = dotPath('request.url', payload)
+ const method = toUpper(dotPath('request.method', payload) || '')
+ const requestHeaders = dotPath('request.headers', payload)
+ const responseHeaders = dotPath('response.headers', payload)
+ const requestBody = dotPath('request.data', payload)
+ const responseBody = dotPath('response.body', payload)
+ const subtitle = `${status} ${method} in ${duration || '?'}ms`
+ const preview = subtitle
+ const summary = { 'Status Code': status, 'Method': method, 'Duration (ms)': duration }
+
+ return (
+
+
+
+
{url}
+
+ {makeTable(summary)}
+
+
+
+
+ {!isNilOrEmpty(requestBody) && }
+
+
+
+
+ {showResponseBody && }
+ {showResponseHeaders && makeTable(responseHeaders)}
+ {showRequestBody && (isNilOrEmpty(requestBody) ? NO_REQUEST_BODY : )}
+ {showRequestHeaders && makeTable(requestHeaders)}
+
+
+
+
+ )
+ }
+}
+
+export default ApiResponseCommand
diff --git a/packages/reactotron-app/App/Commands/BenchmarkReportCommand.js b/packages/reactotron-app/App/Commands/BenchmarkReportCommand.js
new file mode 100644
index 000000000..dd5cab2c7
--- /dev/null
+++ b/packages/reactotron-app/App/Commands/BenchmarkReportCommand.js
@@ -0,0 +1,119 @@
+import React, { Component, PropTypes } from 'react'
+import Command from '../Shared/Command'
+import Colors from '../Theme/Colors'
+import { clone, map, addIndex, tail, merge, last } from 'ramda'
+import { isNilOrEmpty } from 'ramdasauce'
+import AppStyles from '../Theme/AppStyles'
+import ReactTooltip from 'react-tooltip'
+
+const COMMAND_TITLE = 'BENCHMARK'
+const LAST_STEP_DEFAULT = 'Last'
+const MS_LABEL = 'ms'
+
+const mapIndexed = addIndex(map)
+
+const graphUsed = Colors.backgroundLighter
+const graphEmpty = Colors.background
+
+function percentStyle (start, length, total) {
+ const p1 = Number((start / total * 100).toFixed(0))
+ const p2 = Number((length / total * 100).toFixed(0)) + p1
+ const p3 = 100 - p2 - p1
+
+ const stop1 = `${graphEmpty} 0%`
+ const stop2 = `${graphEmpty} ${p1}%`
+ const stop3 = `${graphUsed} 0%`
+ const stop4 = `${graphUsed} ${p2}%`
+ const stop5 = `${graphEmpty} 0%`
+ const stop6 = `${graphEmpty} ${p3}%`
+
+ return {'background': `-webkit-linear-gradient(left, ${stop1}, ${stop2}, ${stop3}, ${stop4}, ${stop5}, ${stop6})`}
+}
+
+const Styles = {
+ step: {
+ position: 'relative',
+ margin: '2px 0',
+ ...AppStyles.Layout.hbox,
+ padding: '4px 4px',
+ justifyContent: 'space-between',
+ WebkitUserSelect: 'all'
+ },
+ stepLast: {
+ paddingTop: 4
+ },
+ reportTitle: {
+ wordBreak: 'break-all',
+ paddingBottom: 10,
+ color: Colors.bold
+ },
+ stepNumber: {
+ paddingRight: 10
+ },
+ stepTitle: {
+ flex: 1,
+ wordBreak: 'break-all'
+ },
+ delta: {
+ textAlign: 'right',
+ width: 100
+ },
+ elapsed: {
+ width: 75,
+ textAlign: 'right'
+ }
+}
+
+const makeStep = (step, idx, last, totalDuration) => {
+ const { time, title, delta } = step
+ const pct = Number(delta / totalDuration * 100.0).toFixed(0)
+ const startedAt = Number(time - delta).toFixed(0)
+ const endedAt = Number(time).toFixed(0)
+ const timeText = `${startedAt} - ${endedAt} ${{MS_LABEL}} (${pct}%)`
+ const key = `step-${idx}`
+ const titleText = (last && isNilOrEmpty(title)) ? LAST_STEP_DEFAULT : title
+ const pStyle = percentStyle(step.time - step.delta, step.delta, totalDuration)
+ const stepStyle = merge(merge(Styles.step, last && Styles.stepLast), pStyle)
+ return (
+
+
{titleText}
+
{delta}{MS_LABEL}
+
+ )
+}
+
+class BenchmarkReportCommand extends Component {
+
+ static propTypes = {
+ command: PropTypes.object.isRequired
+ }
+
+ shouldComponentUpdate (nextProps) {
+ return this.props.command.id !== nextProps.command.id
+ }
+
+ componentDidReact () {
+ ReactTooltip.rebuild()
+ }
+
+ render () {
+ const { command } = this.props
+ const { payload } = command
+ const { title, steps } = clone(payload)
+ const duration = last(steps).time
+ const preview = `${title} in ${duration}ms`
+
+ return (
+
+ {title}
+ {
+ mapIndexed(
+ (step, idx) => makeStep(step, idx, idx === steps.length - 2, duration),
+ tail(steps))
+ }
+
+ )
+ }
+}
+
+export default BenchmarkReportCommand
diff --git a/packages/reactotron-app/App/Commands/ClientIntroCommand.js b/packages/reactotron-app/App/Commands/ClientIntroCommand.js
new file mode 100644
index 000000000..a30bae0ff
--- /dev/null
+++ b/packages/reactotron-app/App/Commands/ClientIntroCommand.js
@@ -0,0 +1,30 @@
+import React, { Component, PropTypes } from 'react'
+import Command from '../Shared/Command'
+import makeTable from '../Shared/MakeTable'
+
+const COMMAND_TITLE = 'CONNECTION'
+
+class ClientIntroCommand extends Component {
+
+ static propTypes = {
+ command: PropTypes.object.isRequired
+ }
+
+ shouldComponentUpdate (nextProps) {
+ return this.props.command.id !== nextProps.command.id
+ }
+
+ render () {
+ const { command } = this.props
+ const { payload } = command
+ const preview = payload.name
+
+ return (
+
+ {makeTable(payload)}
+
+ )
+ }
+}
+
+export default ClientIntroCommand
diff --git a/packages/reactotron-app/App/Commands/DisplayCommand.js b/packages/reactotron-app/App/Commands/DisplayCommand.js
new file mode 100644
index 000000000..6dfc551fe
--- /dev/null
+++ b/packages/reactotron-app/App/Commands/DisplayCommand.js
@@ -0,0 +1,30 @@
+import React, { Component, PropTypes } from 'react'
+import Command from '../Shared/Command'
+import Content from '../Shared/Content'
+
+const COMMAND_TITLE = 'DISPLAY'
+
+class DisplayCommand extends Component {
+
+ static propTypes = {
+ command: PropTypes.object.isRequired
+ }
+
+ shouldComponentUpdate (nextProps) {
+ return this.props.command.id !== nextProps.command.id
+ }
+
+ render () {
+ const { command } = this.props
+ const { payload, important } = command
+ const { name, value, preview } = payload
+
+ return (
+
+
+
+ )
+ }
+}
+
+export default DisplayCommand
diff --git a/packages/reactotron-app/App/Commands/LogCommand.js b/packages/reactotron-app/App/Commands/LogCommand.js
new file mode 100644
index 000000000..e3fca4dd1
--- /dev/null
+++ b/packages/reactotron-app/App/Commands/LogCommand.js
@@ -0,0 +1,130 @@
+import React, { Component, PropTypes } from 'react'
+import Command from '../Shared/Command'
+import { take, replace, merge, map } from 'ramda'
+import Colors from '../Theme/Colors'
+import AppStyles from '../Theme/AppStyles'
+import Content from '../Shared/Content'
+
+const STACK_TITLE = 'YE OLDE STACK TRACE'
+const PREVIEW_LENGTH = 500
+
+const getName = level => {
+ switch (level) {
+ case 'debug': return 'DEBUG'
+ case 'warn': return 'WARNING'
+ case 'error': return 'ERROR'
+ default: return 'LOG'
+ }
+}
+
+const Styles = {
+ container: {
+ paddingTop: 4
+ },
+ stack: {
+ marginTop: 10,
+ ...AppStyles.Layout.vbox
+ },
+ stackFrame: {
+ marginBottom: 10,
+ flex: 1,
+ wordBreak: 'break-all'
+ },
+ number: {
+ paddingRight: 7,
+ color: Colors.constant
+ },
+ stackMiddle: {
+ },
+ fileName: {
+ wordBreak: 'break-all'
+ },
+ lineNumber: {
+ color: Colors.bold,
+ wordBreak: 'break-all'
+ },
+ stackLabel: {
+ color: Colors.foregroundDark,
+ margin: '0 7px',
+ wordBreak: 'break-all'
+ },
+ stackTitle: {
+ color: Colors.constant,
+ paddingBottom: 10,
+ wordBreak: 'break-all'
+ }
+}
+
+class LogCommand extends Component {
+
+ static propTypes = {
+ command: PropTypes.object.isRequired
+ }
+
+ constructor (props) {
+ super(props)
+ this.renderStackFrame = this.renderStackFrame.bind(this)
+ }
+
+ shouldComponentUpdate (nextProps) {
+ return this.props.command.id !== nextProps.command.id
+ }
+
+ renderStackFrame (stackFrame, number) {
+ const key = `stack-${number}`
+ let { fileName, functionName, lineNumber } = stackFrame
+ fileName = fileName && replace('webpack://', '', fileName)
+ functionName = functionName && replace('webpack://', '', functionName)
+ return (
+
+ {number}.
+ {functionName || '(anonymous function)'}
+ :
+ {fileName} :
+ {lineNumber}
+
+ )
+ }
+
+ renderStack (stack) {
+ let i = 0
+ return (
+
+
{STACK_TITLE}
+ {map(stackFrame => {
+ i++
+ return this.renderStackFrame(stackFrame, i)
+ }, stack)}
+
+ )
+ }
+
+ getPreview (message) {
+ if (typeof message === 'string') {
+ return `${take(PREVIEW_LENGTH, message)}`
+ }
+ return null
+ }
+
+ render () {
+ const { command } = this.props
+ const { payload } = command
+ const { level } = payload
+ const { message, stack } = payload
+ const title = getName(level)
+ const containerTypes = merge(Styles.container, { color: level === 'debug' ? Colors.foreground : Colors.foreground })
+
+ let preview = this.getPreview(message)
+
+ return (
+
+
+
+ {stack && this.renderStack(stack)}
+
+
+ )
+ }
+}
+
+export default LogCommand
diff --git a/packages/reactotron-app/App/Commands/SectionLink.js b/packages/reactotron-app/App/Commands/SectionLink.js
new file mode 100644
index 000000000..e74dd9e9c
--- /dev/null
+++ b/packages/reactotron-app/App/Commands/SectionLink.js
@@ -0,0 +1,48 @@
+import React, { Component, PropTypes } from 'react'
+import Colors from '../Theme/Colors'
+import { merge } from 'ramda'
+
+const Styles = {
+ container: {
+ backgroundColor: Colors.backgroundLighter,
+ padding: '4px 8px',
+ margin: 4,
+ borderRadius: 4,
+ cursor: 'pointer',
+ alignItems: 'center',
+ justifyContent: 'center'
+ },
+ containerActive: {
+ backgroundColor: Colors.constant
+ },
+ text: {
+ color: Colors.foreground,
+ textAlign: 'center'
+ },
+ textActive: {
+ color: Colors.background
+ }
+}
+
+class SectionLink extends Component {
+
+ static propTypes = {
+ isActive: PropTypes.bool.isRequired,
+ text: PropTypes.string.isRequired,
+ onClick: PropTypes.func.isRequired
+ }
+
+ render () {
+ const { isActive = false, text, onClick } = this.props
+ const containerStyles = merge(Styles.container, isActive ? Styles.containerActive : {})
+ const textStyles = merge(Styles.text, isActive ? Styles.textActive : {})
+ return (
+
+ )
+ }
+
+}
+
+export default SectionLink
diff --git a/packages/reactotron-app/App/Commands/StateActionCompleteCommand.js b/packages/reactotron-app/App/Commands/StateActionCompleteCommand.js
new file mode 100644
index 000000000..4302f6c93
--- /dev/null
+++ b/packages/reactotron-app/App/Commands/StateActionCompleteCommand.js
@@ -0,0 +1,43 @@
+import React, { Component, PropTypes } from 'react'
+import Command from '../Shared/Command'
+import Colors from '../Theme/Colors'
+import Content from '../Shared/Content'
+
+const COMMAND_TITLE = 'ACTION'
+
+const Styles = {
+ name: {
+ color: Colors.bold,
+ margin: 0,
+ paddingBottom: 10
+ }
+}
+
+class StateActionComplete extends Component {
+
+ static propTypes = {
+ command: PropTypes.object.isRequired
+ }
+
+ shouldComponentUpdate (nextProps) {
+ return this.props.command.id !== nextProps.command.id
+ }
+
+ render () {
+ const { command } = this.props
+ const { payload } = command
+ const { ms, action, name } = payload
+ const preview = `${name}`
+
+ return (
+
+
+
+ )
+ }
+}
+
+export default StateActionComplete
diff --git a/packages/reactotron-app/App/Commands/StateKeysResponseCommand.js b/packages/reactotron-app/App/Commands/StateKeysResponseCommand.js
new file mode 100644
index 000000000..b29481381
--- /dev/null
+++ b/packages/reactotron-app/App/Commands/StateKeysResponseCommand.js
@@ -0,0 +1,129 @@
+import React, { Component, PropTypes } from 'react'
+import Command from '../Shared/Command'
+import Colors from '../Theme/Colors'
+import { map, isNil, sortBy, toLower } from 'ramda'
+import { inject, observer } from 'mobx-react'
+import AppStyles from '../Theme/AppStyles'
+
+const COMMAND_TITLE = 'STATE KEYS'
+const NULL_MESSAGE = '¯\\_(ツ)_/¯'
+const EMPTY_MESSAGE = 'Sorry, no keys in there.'
+const ROOT_TEXT = '(root)'
+const PATH_LABEL = ''
+
+const Styles = {
+ path: {
+ padding: '0 0 10px 0',
+ color: Colors.bold
+ },
+ pathLabel: {
+ color: Colors.foregroundDark
+ },
+ stringValue: {
+ color: Colors.text,
+ WebkitUserSelect: 'all',
+ wordBreak: 'break-all'
+ },
+ null: {
+ },
+ empty: {
+ },
+ keyList: {
+ ...AppStyles.Layout.hbox,
+ flexWrap: 'wrap'
+ },
+ key: {
+ backgroundColor: Colors.backgroundLighter,
+ padding: '4px 8px',
+ margin: 4,
+ borderRadius: 4,
+ cursor: 'pointer'
+ }
+}
+
+const sortKeys = sortBy(toLower)
+
+class StateKey extends Component {
+ static propTypes = {
+ stateKey: PropTypes.string.isRequired,
+ onClick: PropTypes.func
+ }
+
+ constructor (props) {
+ super(props)
+ this.handleClick = this.handleClick.bind(this)
+ }
+
+ handleClick = () => {
+ const { onClick, stateKey } = this.props
+ onClick && onClick(stateKey)
+ }
+
+ render () {
+ const { stateKey } = this.props
+ return (
+
+ {stateKey}
+
+ )
+ }
+}
+
+@inject('session')
+@observer
+class StateKeysResponseCommand extends Component {
+
+ static propTypes = {
+ command: PropTypes.object.isRequired
+ }
+
+ constructor (props) {
+ super(props)
+ this.renderKey = this.renderKey.bind(this)
+ this.handleKeyClick = this.handleKeyClick.bind(this)
+ }
+
+ shouldComponentUpdate (nextProps) {
+ return this.props.command.id !== nextProps.command.id
+ }
+
+ renderKey (key) {
+ return
+ }
+
+ handleKeyClick (key) {
+ const { session, command } = this.props
+ const { ui } = session
+ const path = isNil(command.payload.path) ? '' : command.payload.path + '.'
+ ui.getStateValues(`${path}${key}`)
+ }
+
+ renderKeys (keys) {
+ if (isNil(keys)) return {NULL_MESSAGE}>
+ if (keys.length === 0) return {EMPTY_MESSAGE}
+
+ return (
+
+ {map(this.renderKey, sortKeys(keys))}
+
+ )
+ }
+
+ render () {
+ const { command } = this.props
+ const { payload } = command
+ const { path, keys } = payload
+ const pathText = path || ROOT_TEXT
+
+ return (
+
+
+
{PATH_LABEL} {pathText}
+ {this.renderKeys(keys)}
+
+
+ )
+ }
+}
+
+export default StateKeysResponseCommand
diff --git a/packages/reactotron-app/App/Commands/StateValuesChangeCommand.js b/packages/reactotron-app/App/Commands/StateValuesChangeCommand.js
new file mode 100644
index 000000000..636ecfb55
--- /dev/null
+++ b/packages/reactotron-app/App/Commands/StateValuesChangeCommand.js
@@ -0,0 +1,61 @@
+import React, { Component, PropTypes } from 'react'
+import Command from '../Shared/Command'
+import Colors from '../Theme/Colors'
+import Content from '../Shared/Content'
+import { keys, concat, join } from 'ramda'
+import { mapKeys, isNilOrEmpty } from 'ramdasauce'
+
+const COMMAND_TITLE = 'SUBSCRIPTIONS'
+
+const Styles = {
+ name: {
+ color: Colors.bold,
+ margin: 0,
+ paddingBottom: 10
+ }
+}
+
+class StateValuesChangeCommand extends Component {
+
+ static propTypes = {
+ command: PropTypes.object.isRequired
+ }
+
+ shouldComponentUpdate (nextProps) {
+ return this.props.command.id !== nextProps.command.id
+ }
+
+ render () {
+ const { command } = this.props
+ const { payload } = command
+ const phrase = []
+ let { changed, added, removed } = payload
+ const hasAdded = !isNilOrEmpty(added)
+ const hasRemoved = !isNilOrEmpty(removed)
+ const hasChanged = !isNilOrEmpty(changed)
+ if (hasChanged) {
+ phrase.push(`${keys(changed).length} changed`)
+ }
+ if (hasAdded) {
+ added = mapKeys(concat('+ '), added)
+ phrase.push(`${keys(added).length} added`)
+ }
+ if (hasRemoved) {
+ removed = mapKeys(concat('- '), removed)
+ phrase.push(`${keys(removed).length} removed`)
+ }
+ const preview = join(' ', phrase)
+
+ return (
+
+
+ {hasChanged && }
+ {hasAdded && }
+ {hasRemoved && }
+
+
+ )
+ }
+}
+
+export default StateValuesChangeCommand
diff --git a/packages/reactotron-app/App/Commands/StateValuesResponseCommand.js b/packages/reactotron-app/App/Commands/StateValuesResponseCommand.js
new file mode 100644
index 000000000..ad756d2fe
--- /dev/null
+++ b/packages/reactotron-app/App/Commands/StateValuesResponseCommand.js
@@ -0,0 +1,51 @@
+import React, { Component, PropTypes } from 'react'
+import Command from '../Shared/Command'
+import Colors from '../Theme/Colors'
+import Content from '../Shared/Content'
+
+const ROOT_TEXT = '(root)'
+const COMMAND_TITLE = 'STATE'
+const PATH_LABEL = ''
+
+const Styles = {
+ path: {
+ padding: '0 0 10px 0',
+ color: Colors.bold
+ },
+ pathLabel: {
+ color: Colors.foregroundDark
+ },
+ stringValue: {
+ WebkitUserSelect: 'all',
+ wordBreak: 'break-all'
+ }
+}
+
+class StateValuesResponseCommand extends Component {
+
+ static propTypes = {
+ command: PropTypes.object.isRequired
+ }
+
+ shouldComponentUpdate (nextProps) {
+ return this.props.command.id !== nextProps.command.id
+ }
+
+ render () {
+ const { command } = this.props
+ const { payload } = command
+ const { path, value } = payload
+ const pathText = path || ROOT_TEXT
+
+ return (
+
+
+
{PATH_LABEL} {pathText}
+
+
+
+ )
+ }
+}
+
+export default StateValuesResponseCommand
diff --git a/packages/reactotron-app/App/Commands/UnknownCommand.js b/packages/reactotron-app/App/Commands/UnknownCommand.js
new file mode 100644
index 000000000..02c379234
--- /dev/null
+++ b/packages/reactotron-app/App/Commands/UnknownCommand.js
@@ -0,0 +1,29 @@
+import React, { Component, PropTypes } from 'react'
+import Command from '../Shared/Command'
+import ObjectTree from '../Shared/ObjectTree'
+
+const COMMAND_TITLE = 'MYSTERY 👻'
+
+class UnknownCommand extends Component {
+
+ static propTypes = {
+ command: PropTypes.object.isRequired
+ }
+
+ shouldComponentUpdate (nextProps) {
+ return this.props.command.id !== nextProps.command.id
+ }
+
+ render () {
+ const { command } = this.props
+ const { payload, type } = command
+
+ return (
+
+
+
+ )
+ }
+}
+
+export default UnknownCommand
diff --git a/packages/reactotron-app/App/Commands/index.js b/packages/reactotron-app/App/Commands/index.js
new file mode 100644
index 000000000..9864b1b74
--- /dev/null
+++ b/packages/reactotron-app/App/Commands/index.js
@@ -0,0 +1,27 @@
+import LogCommand from './LogCommand'
+import UnknownCommand from './UnknownCommand'
+import StateActionCompleteCommand from './StateActionCompleteCommand'
+import ApiResponseCommand from './ApiResponseCommand'
+import ClientIntroCommand from './ClientIntroCommand'
+import BenchmarkReportCommand from './BenchmarkReportCommand'
+import StateValuesResponseCommand from './StateValuesResponseCommand'
+import StateKeysResponseCommand from './StateKeysResponseCommand'
+import StateValuesChangeCommand from './StateValuesChangeCommand'
+import DisplayCommand from './DisplayCommand'
+
+export default command => {
+ const { type } = command
+
+ switch (type) {
+ case 'benchmark.report': return BenchmarkReportCommand
+ case 'log': return LogCommand
+ case 'state.action.complete': return StateActionCompleteCommand
+ case 'api.response': return ApiResponseCommand
+ case 'client.intro': return ClientIntroCommand
+ case 'state.values.response': return StateValuesResponseCommand
+ case 'state.keys.response': return StateKeysResponseCommand
+ case 'state.values.change': return StateValuesChangeCommand
+ case 'display': return DisplayCommand
+ default: return UnknownCommand
+ }
+}
diff --git a/packages/reactotron-app/App/Dialogs/HelpDialog.js b/packages/reactotron-app/App/Dialogs/HelpDialog.js
new file mode 100644
index 000000000..7aa5ae674
--- /dev/null
+++ b/packages/reactotron-app/App/Dialogs/HelpDialog.js
@@ -0,0 +1,147 @@
+import React, { Component } from 'react'
+import ReactDOM from 'react-dom'
+import { ModalPortal, ModalBackground, ModalDialog } from 'react-modal-dialog'
+import { inject, observer } from 'mobx-react'
+import AppStyles from '../Theme/AppStyles'
+import Colors from '../Theme/Colors'
+
+const ESCAPE_KEYSTROKE = 'Esc'
+const ESCAPE_HINT = 'Close'
+const DIALOG_TITLE = 'Reactotron Quick-Help'
+const INSTRUCTIONS = (
+ Shortcut list
+)
+
+const Styles = {
+ dialog: {
+ borderRadius: 4,
+ padding: 4,
+ width: 450,
+ backgroundColor: Colors.background,
+ color: Colors.foreground
+ },
+ container: {
+ ...AppStyles.Layout.vbox
+ },
+ keystrokes: {
+ ...AppStyles.Layout.hbox,
+ alignSelf: 'center',
+ paddingTop: 10,
+ paddingBottom: 20
+ },
+ hotkey: {
+ padding: '0 10px'
+ },
+ keystroke: {
+ backgroundColor: Colors.backgroundHighlight,
+ color: Colors.foreground,
+ padding: '4px 8px',
+ borderRadius: 4
+ },
+ header: {
+ ...AppStyles.Layout.vbox,
+ padding: '2em 2em 1em'
+ },
+ body: {
+ ...AppStyles.Layout.vbox,
+ padding: '0.5em 2em 3em'
+ },
+ helpShortcut: {
+ ...AppStyles.Layout.hbox,
+ margin: '2px 0'
+ },
+ title: {
+ margin: 0,
+ padding: 0,
+ textAlign: 'left',
+ fontWeight: 'normal',
+ fontSize: 24,
+ color: Colors.heading
+ },
+ subtitle: {
+ color: Colors.foreground,
+ textAlign: 'left',
+ padding: 0,
+ margin: 0
+ },
+ helpLabel: {
+ // borderBottom: `1px solid ${Colors.line}`,
+ color: Colors.bold,
+ textTransform: 'uppercase',
+ width: 100
+ },
+ helpDetail: {
+ flex: 1
+ },
+ group: {
+ marginTop: 10,
+ marginBottom: 2,
+ paddingBottom: 2,
+ color: Colors.highlight,
+ borderBottom: `1px solid ${Colors.line}`
+ }
+}
+
+@inject('session')
+@observer
+class StateDispatchDialog extends Component {
+
+ handleChange = (e) => {
+ const { session } = this.props
+ session.ui.actionToDispatch = e.target.value
+ }
+
+ render () {
+ const { ui } = this.props.session
+ const open = ui.showHelpDialog
+ if (!open) return null
+
+ return (
+
+
+
+
+
+
{DIALOG_TITLE}
+
+ {INSTRUCTIONS}
+
+
+
+
Working With State
+
+
Cmd + F
+
find keys or values
+
+
+
Cmd + N
+
new subscription
+
+
+
Cmd + D
+
dispatch an action
+
+
Miscellaneous
+
+
+
Cmd + /
+
toggle help
+
+
+
+
+ {ESCAPE_KEYSTROKE} {ESCAPE_HINT}
+
+
+
+
+
+
+ )
+ }
+}
+
+export default StateDispatchDialog
diff --git a/packages/reactotron-app/App/Dialogs/StateDispatchDialog.js b/packages/reactotron-app/App/Dialogs/StateDispatchDialog.js
new file mode 100644
index 000000000..f8e859469
--- /dev/null
+++ b/packages/reactotron-app/App/Dialogs/StateDispatchDialog.js
@@ -0,0 +1,140 @@
+import React, { Component } from 'react'
+import ReactDOM from 'react-dom'
+import { ModalPortal, ModalBackground, ModalDialog } from 'react-modal-dialog'
+import { inject, observer } from 'mobx-react'
+import AppStyles from '../Theme/AppStyles'
+import Colors from '../Theme/Colors'
+
+const ESCAPE_KEYSTROKE = 'Esc'
+const ESCAPE_HINT = 'Cancel'
+const ENTER_KEYSTROKE = '⌘ + Enter'
+const ENTER_HINT = 'Dispatch'
+const DIALOG_TITLE = 'Dispatch Action'
+const INSTRUCTIONS = (
+ Create an action that will be dispatched to the client to run.
+)
+const INPUT_PLACEHOLDER = '{ type: \'RepoMessage.Request\' }'
+const FIELD_LABEL = 'Action'
+
+const Styles = {
+ dialog: {
+ borderRadius: 4,
+ padding: 4,
+ width: 450,
+ backgroundColor: Colors.background,
+ color: Colors.foreground
+ },
+ container: {
+ ...AppStyles.Layout.vbox
+ },
+ keystrokes: {
+ ...AppStyles.Layout.hbox,
+ alignSelf: 'center',
+ paddingTop: 10,
+ paddingBottom: 20,
+ fontSize: 13
+ },
+ hotkey: {
+ padding: '0 10px'
+ },
+ keystroke: {
+ backgroundColor: Colors.backgroundHighlight,
+ color: Colors.foreground,
+ padding: '4px 8px',
+ borderRadius: 4
+ },
+ header: {
+ ...AppStyles.Layout.vbox,
+ padding: '2em 2em 1em'
+ },
+ body: {
+ ...AppStyles.Layout.vbox,
+ padding: '2em 2em 4em'
+ },
+ title: {
+ margin: 0,
+ padding: 0,
+ textAlign: 'left',
+ fontWeight: 'normal',
+ fontSize: 24,
+ color: Colors.heading
+ },
+ subtitle: {
+ color: Colors.foreground,
+ textAlign: 'left',
+ padding: 0,
+ margin: 0
+ },
+ fieldLabel: {
+ color: Colors.heading,
+ fontSize: 13,
+ textTransform: 'uppercase'
+ },
+ dispatchField: {
+ borderTop: 0,
+ borderLeft: 0,
+ borderRight: 0,
+ borderBottom: `1px solid ${Colors.line}`,
+ fontSize: 23,
+ color: Colors.foregroundLight,
+ backgroundColor: 'inherit',
+ height: 200
+ }
+}
+
+@inject('session')
+@observer
+class StateDispatchDialog extends Component {
+
+ handleChange = (e) => {
+ const { session } = this.props
+ session.ui.actionToDispatch = e.target.value
+ }
+
+ render () {
+ const { ui } = this.props.session
+ const open = ui.showStateDispatchDialog
+ if (!open) return null
+
+ // need to find a less hacky way of doing this
+ setTimeout(() => ReactDOM.findDOMNode(this.refs.dispatchField).focus(), 1)
+ return (
+
+
+
+
+
+
{DIALOG_TITLE}
+
+ {INSTRUCTIONS}
+
+
+
+ {FIELD_LABEL}
+
+
+
+
+ {ESCAPE_KEYSTROKE} {ESCAPE_HINT}
+
+
+ {ENTER_KEYSTROKE} {ENTER_HINT}
+
+
+
+
+
+
+ )
+ }
+}
+
+export default StateDispatchDialog
diff --git a/packages/reactotron-app/App/Dialogs/StateKeysAndValuesDialog.js b/packages/reactotron-app/App/Dialogs/StateKeysAndValuesDialog.js
new file mode 100644
index 000000000..452cd60f7
--- /dev/null
+++ b/packages/reactotron-app/App/Dialogs/StateKeysAndValuesDialog.js
@@ -0,0 +1,161 @@
+import React, { Component } from 'react'
+import ReactDOM from 'react-dom'
+import { ModalPortal, ModalBackground, ModalDialog } from 'react-modal-dialog'
+import { inject, observer } from 'mobx-react'
+import AppStyles from '../Theme/AppStyles'
+import Colors from '../Theme/Colors'
+
+const INPUT_PLACEHOLDER = 'smurfs.7.name'
+const ESCAPE_KEYSTROKE = 'Esc'
+const ESCAPE_HINT = 'OMG Cancel'
+const ENTER_KEYSTROKE = 'Enter'
+const ENTER_HINT = 'Search'
+const TAB_KEYSTROKE = 'Tab'
+const TAB_HINT = 'Keys/Values'
+const DIALOG_TITLE_KEYS = 'State Keys'
+const DIALOG_TITLE_VALUES = 'State Values'
+const STATE_VALUES_INSTRUCTIONS = (Retrieves a value from the state tree at the given path and all values below it . )
+const STATE_KEYS_INSTRUCTIONS = (Retrieves a list of keys located at the given path in the state tree. )
+const FIELD_LABEL = 'Path'
+
+const Styles = {
+ dialog: {
+ borderRadius: 4,
+ padding: 4,
+ width: 450,
+ backgroundColor: Colors.background,
+ color: Colors.foreground
+ },
+ container: {
+ ...AppStyles.Layout.vbox
+ },
+ keystrokes: {
+ ...AppStyles.Layout.hbox,
+ alignSelf: 'center',
+ paddingTop: 10,
+ paddingBottom: 20,
+ fontSize: 13
+ },
+ hotkey: {
+ padding: '0 10px'
+ },
+ keystroke: {
+ backgroundColor: Colors.backgroundHighlight,
+ color: Colors.foreground,
+ padding: '4px 8px',
+ borderRadius: 4
+ },
+ header: {
+ ...AppStyles.Layout.vbox,
+ padding: '2em 2em 1em'
+ },
+ body: {
+ ...AppStyles.Layout.vbox,
+ padding: '2em 2em 4em'
+ },
+ title: {
+ margin: 0,
+ padding: 0,
+ textAlign: 'left',
+ fontWeight: 'normal',
+ fontSize: 24,
+ color: Colors.heading
+ },
+ subtitle: {
+ color: Colors.foreground,
+ textAlign: 'left',
+ padding: 0,
+ margin: 0
+ },
+ fieldLabel: {
+ color: Colors.heading,
+ fontSize: 13,
+ textTransform: 'uppercase'
+ },
+ textField: {
+ borderTop: 0,
+ borderLeft: 0,
+ borderRight: 0,
+ borderBottom: `1px solid ${Colors.line}`,
+ fontSize: 23,
+ color: Colors.foregroundLight,
+ lineHeight: '40px',
+ backgroundColor: 'inherit'
+ }
+}
+
+@inject('session')
+@observer
+class StateKeysAndValuesDialog extends Component {
+
+ constructor (props) {
+ super(props)
+ this.state = {
+ path: null
+ }
+ }
+
+ handleChange = (e) => {
+ this.setState({ path: e.target.value })
+ }
+
+ handleKeyPress = (e) => {
+ const { ui } = this.props.session
+ const { path } = this.state
+ if (e.key === 'Enter') {
+ this.setState({path: null})
+ ui.getStateKeysOrValues(path)
+ ui.closeStateFindDialog()
+ }
+ }
+
+ render () {
+ const { ui } = this.props.session
+ const open = ui.showStateFindDialog
+ const isKeys = ui.keysOrValues === 'keys'
+ if (!open) return null
+
+ // need to find a less hacky way of doing this
+ setTimeout(() => ReactDOM.findDOMNode(this.refs.textField).focus(), 1)
+ return (
+
+
+
+
+
+
{isKeys ? DIALOG_TITLE_KEYS : DIALOG_TITLE_VALUES}
+
+ {isKeys ? STATE_KEYS_INSTRUCTIONS : STATE_VALUES_INSTRUCTIONS}
+
+
+
+ {FIELD_LABEL}
+
+
+
+
+ {ESCAPE_KEYSTROKE} {ESCAPE_HINT}
+
+
+ {TAB_KEYSTROKE} {TAB_HINT}
+
+
+ {ENTER_KEYSTROKE} {ENTER_HINT}
+
+
+
+
+
+
+ )
+ }
+}
+
+export default StateKeysAndValuesDialog
diff --git a/packages/reactotron-app/App/Dialogs/StateWatchDialog.js b/packages/reactotron-app/App/Dialogs/StateWatchDialog.js
new file mode 100644
index 000000000..cc5d0fbed
--- /dev/null
+++ b/packages/reactotron-app/App/Dialogs/StateWatchDialog.js
@@ -0,0 +1,151 @@
+import React, { Component } from 'react'
+import ReactDOM from 'react-dom'
+import { ModalPortal, ModalBackground, ModalDialog } from 'react-modal-dialog'
+import { inject, observer } from 'mobx-react'
+import AppStyles from '../Theme/AppStyles'
+import Colors from '../Theme/Colors'
+
+const ESCAPE_KEYSTROKE = 'Esc'
+const ESCAPE_HINT = 'Cancel'
+const ENTER_KEYSTROKE = 'Enter'
+const ENTER_HINT = 'Subscribe'
+const DIALOG_TITLE = 'Add Subscription'
+
+const INPUT_PLACEHOLDER = ''
+const FIELD_LABEL = 'path'
+
+const Styles = {
+ dialog: {
+ borderRadius: 4,
+ padding: 4,
+ width: 450,
+ backgroundColor: Colors.background,
+ color: Colors.foreground
+ },
+ examples: {
+ },
+ example: {
+ padding: 0,
+ margin: '0 0 0 40px',
+ color: Colors.bold
+ },
+ container: {
+ ...AppStyles.Layout.vbox
+ },
+ keystrokes: {
+ ...AppStyles.Layout.hbox,
+ alignSelf: 'center',
+ paddingTop: 10,
+ paddingBottom: 20,
+ fontSize: 13
+ },
+ hotkey: {
+ padding: '0 10px'
+ },
+ keystroke: {
+ backgroundColor: Colors.backgroundHighlight,
+ color: Colors.foreground,
+ padding: '4px 8px',
+ borderRadius: 4
+ },
+ header: {
+ ...AppStyles.Layout.vbox,
+ padding: '1em 2em 0em'
+ },
+ body: {
+ ...AppStyles.Layout.vbox,
+ padding: '1em 2em 4em'
+ },
+ title: {
+ margin: 0,
+ padding: 0,
+ textAlign: 'left',
+ fontWeight: 'normal',
+ fontSize: 24,
+ color: Colors.heading
+ },
+ subtitle: {
+ color: Colors.foreground,
+ textAlign: 'left',
+ padding: 0,
+ margin: 0
+ },
+ fieldLabel: {
+ color: Colors.heading,
+ fontSize: 13,
+ textTransform: 'uppercase'
+ },
+ textField: {
+ borderTop: 0,
+ borderLeft: 0,
+ borderRight: 0,
+ borderBottom: `1px solid ${Colors.line}`,
+ fontSize: 23,
+ color: Colors.foregroundLight,
+ lineHeight: '40px',
+ backgroundColor: 'inherit'
+ }
+}
+
+const INSTRUCTIONS =
+
Enter a path you would like to subscribe. Here are some examples to get you started:
+
user.firstName
+
repo
+
repo.*
+
+
+@inject('session')
+@observer
+class StateWatchDialog extends Component {
+
+ handleChange = (e) => {
+ const { session } = this.props
+ session.ui.watchToAdd = e.target.value
+ }
+
+ render () {
+ const { ui } = this.props.session
+ const open = ui.showStateWatchDialog
+ if (!open) return null
+
+ // need to find a less hacky way of doing this
+ setTimeout(() => ReactDOM.findDOMNode(this.refs.textField).focus(), 1)
+ return (
+
+
+
+
+
+
{DIALOG_TITLE}
+
+ {INSTRUCTIONS}
+
+
+
+ {FIELD_LABEL}
+
+
+
+
+ {ESCAPE_KEYSTROKE} {ESCAPE_HINT}
+
+
+ {ENTER_KEYSTROKE} {ENTER_HINT}
+
+
+
+
+
+
+ )
+ }
+}
+
+export default StateWatchDialog
diff --git a/packages/reactotron-app/App/Foundation/App.js b/packages/reactotron-app/App/Foundation/App.js
new file mode 100644
index 000000000..2e50b1804
--- /dev/null
+++ b/packages/reactotron-app/App/Foundation/App.js
@@ -0,0 +1,17 @@
+import React, { Component } from 'react'
+import VisualRoot from './VisualRoot'
+import { Provider } from 'mobx-react'
+import SessionStore from '../Stores/SessionStore'
+
+const session = new SessionStore()
+
+export default class App extends Component {
+
+ render () {
+ return (
+
+
+
+ )
+ }
+}
diff --git a/packages/reactotron-app/App/Foundation/Footer.js b/packages/reactotron-app/App/Foundation/Footer.js
new file mode 100644
index 000000000..5e42b6cb8
--- /dev/null
+++ b/packages/reactotron-app/App/Foundation/Footer.js
@@ -0,0 +1,85 @@
+import React, { Component } from 'react'
+import Colors from '../Theme/Colors'
+import AppStyles from '../Theme/AppStyles'
+import { observer, inject } from 'mobx-react'
+const logoUrl = require('../Theme/Reactotron-128.png')
+
+const APP_NAME = 'Reactotron'
+const APP_VERSION = '1.0.0'
+const PORT_LABEL = 'port'
+const CONNECTIONS_SUFFIX_SINGULAR = 'connection'
+const CONNECTIONS_SUFFIX_PLURAL = 'connections'
+
+const Styles = {
+ container: {
+ backgroundColor: Colors.chrome,
+ borderTop: `1px solid ${Colors.chromeLine}`,
+ color: Colors.foregroundDark,
+ paddingLeft: 5,
+ paddingRight: 10,
+ paddingTop: 5,
+ paddingBottom: 5,
+ },
+ content: {
+ ...AppStyles.Layout.hbox,
+ alignItems: 'center',
+ height: '100%'
+ },
+ line: {
+ height: 30,
+ width: 1,
+ marginLeft: 10,
+ marginRight: 10,
+ backgroundColor: Colors.background
+ },
+ reactotronContainer: {
+ paddingLeft: 5,
+ paddingRight: 10,
+ ...AppStyles.Layout.vbox,
+ flex: 0,
+ alignItems: 'flex-start'
+ },
+ reactotron: {
+ },
+ version: {
+ fontSize: 12,
+ fontWeight: 'bold'
+ },
+ logo: { width: 32, height: 32 },
+ github: { margin: '0 6px', color: Colors.text },
+ settings: { margin: '0 6px', color: Colors.text },
+ feedback: { margin: '0 6px', color: Colors.text },
+ web: { margin: '0 6px', color: Colors.text },
+ twitter: { margin: '0 6px', color: Colors.text },
+ stretcher: { flex: 1 }
+}
+
+@inject('session')
+@observer
+class Footer extends Component {
+
+ render () {
+ const { server } = this.props.session
+ const { port } = server.options
+ const connectionCount = server.connectionCount
+ return (
+
+
+
+
+
+
{APP_NAME}
+
{APP_VERSION}
+
+
+
{PORT_LABEL} {port}
+
+
{connectionCount} {connectionCount === 1 ? CONNECTIONS_SUFFIX_SINGULAR : CONNECTIONS_SUFFIX_PLURAL}
+
+
+ )
+ }
+
+}
+
+export default Footer
diff --git a/packages/reactotron-app/App/Foundation/Help.js b/packages/reactotron-app/App/Foundation/Help.js
new file mode 100644
index 000000000..e05120cff
--- /dev/null
+++ b/packages/reactotron-app/App/Foundation/Help.js
@@ -0,0 +1,68 @@
+import React, { Component } from 'react'
+import Colors from '../Theme/Colors'
+import AppStyles from '../Theme/AppStyles'
+import HelpHeader from './HelpHeader'
+import HelpKeystrokes from './HelpKeystrokes'
+import HelpFeedback from './HelpFeedback'
+
+const FEEDBACK = 'Let\'s Connect!'
+const KEYSTROKES = 'Keystrokes'
+
+const logoUrl = require('../Theme/Reactotron-128.png')
+
+const Styles = {
+ container: {
+ ...AppStyles.Layout.vbox,
+ margin: 0,
+ flex: 1
+ },
+ content: {
+ padding: 20,
+ overflowY: 'scroll',
+ overflowX: 'hidden',
+ ...AppStyles.Layout.vbox
+ },
+ logoPanel: {
+ alignSelf: 'center'
+ },
+ logo: {
+ alignSelf: 'center',
+ height: 128,
+ marginTop: 20,
+ marginBottom: 20
+ },
+ title: {
+ fontSize: 18,
+ marginTop: 10,
+ marginBottom: 10,
+ color: Colors.foregroundLight,
+ paddingBottom: 2,
+ borderBottom: `1px solid ${Colors.highlight}`
+ }
+}
+
+class Help extends Component {
+
+ render () {
+ return (
+
+
+
+
+
+
+
+
{FEEDBACK}
+
+
+
{KEYSTROKES}
+
+
+
+
+
+ )
+ }
+}
+
+export default Help
diff --git a/packages/reactotron-app/App/Foundation/HelpFeedback.js b/packages/reactotron-app/App/Foundation/HelpFeedback.js
new file mode 100644
index 000000000..e18a61b5e
--- /dev/null
+++ b/packages/reactotron-app/App/Foundation/HelpFeedback.js
@@ -0,0 +1,85 @@
+import React, { Component } from 'react'
+import Colors from '../Theme/Colors'
+import AppStyles from '../Theme/AppStyles'
+import { shell } from 'electron'
+
+const Styles = {
+ container: {
+ color: Colors.foreground,
+ marginBottom: 50
+ },
+ content: {
+ ...AppStyles.Layout.hbox,
+ alignItems: 'flex-start'
+ },
+ iconSize: 40,
+ link: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ textAlign: 'center',
+ padding: 10,
+ cursor: 'pointer',
+ backgroundColor: Colors.chrome,
+ margin: 5,
+ borderRadius: 8,
+ width: 100,
+ border: `1px solid ${Colors.chromeLine}`
+ },
+ icon: {
+ marginBottom: 8,
+ color: Colors.foregroundLight
+ },
+ text: {
+ },
+ spacer: {
+ flex: 1
+ }
+}
+
+const RepoIcon = require('react-icons/lib/go/mark-github')
+const FeedbackIcon = require('react-icons/lib/go/comment')
+const ReleaseIcon = require('react-icons/lib/go/squirrel')
+const TwitterIcon = require('react-icons/lib/fa/twitter')
+
+class HelpFeedback extends Component {
+
+ constructor (props) {
+ super(props)
+ this.openRepo = () => shell.openExternal('https://github.com/reactotron/reactotron/tree/next')
+ this.feedback = () => shell.openExternal('https://github.com/reactotron/reactotron/issues/new')
+ this.checkUpdates = () => shell.openExternal('https://github.com/reactotron/reactotron/releases')
+ this.twitter = () => shell.openExternal('https://twitter.com/reactotron')
+ }
+
+ render () {
+ return (
+
+ )
+ }
+}
+
+export default HelpFeedback
diff --git a/packages/reactotron-app/App/Foundation/HelpHeader.js b/packages/reactotron-app/App/Foundation/HelpHeader.js
new file mode 100644
index 000000000..2ae1b9901
--- /dev/null
+++ b/packages/reactotron-app/App/Foundation/HelpHeader.js
@@ -0,0 +1,44 @@
+import React, { Component } from 'react'
+import Colors from '../Theme/Colors'
+import AppStyles from '../Theme/AppStyles'
+import { inject, observer } from 'mobx-react'
+
+const TITLE = 'Using and Abusing Reactotron'
+
+const Styles = {
+ container: {
+ WebkitAppRegion: 'drag',
+ backgroundColor: Colors.backgroundSubtleLight,
+ borderBottom: `1px solid ${Colors.chromeLine}`,
+ color: Colors.foregroundDark,
+ boxShadow: `0px 0px 30px ${Colors.glow}`
+ },
+ content: {
+ height: 60,
+ ...AppStyles.Layout.hbox,
+ alignItems: 'center',
+ justifyContent: 'center'
+ },
+ title: {
+ color: Colors.foregroundLight,
+ textAlign: 'center'
+ }
+}
+
+@inject('session')
+@observer
+class TimelineHeader extends Component {
+
+ render () {
+ return (
+
+ )
+ }
+
+}
+
+export default TimelineHeader
diff --git a/packages/reactotron-app/App/Foundation/HelpKeystrokes.js b/packages/reactotron-app/App/Foundation/HelpKeystrokes.js
new file mode 100644
index 000000000..41ca2695e
--- /dev/null
+++ b/packages/reactotron-app/App/Foundation/HelpKeystrokes.js
@@ -0,0 +1,97 @@
+import React, { Component } from 'react'
+import Colors from '../Theme/Colors'
+import AppStyles from '../Theme/AppStyles'
+import Key from '../Shared/Key'
+
+const Styles = {
+ container: {
+ color: Colors.foreground
+ },
+ helpLabel: {
+ width: 120
+ },
+ key: {
+ color: Colors.foregroundLight,
+ textTransform: 'uppercase',
+ borderRadius: 4,
+ backgroundColor: Colors.foreground,
+ padding: '4px 8px',
+ fontWeight: 'bold',
+ borderBottom: `2px solid ${Colors.highlight}`
+ },
+ helpDetail: {
+ color: Colors.foreground
+ },
+ group: {
+ ...AppStyles.Layout.vbox,
+ marginTop: 0,
+ marginBottom: 30,
+ color: Colors.highlight
+ },
+ category: {
+ color: Colors.highlight
+ },
+ helpShortcut: {
+ ...AppStyles.Layout.hbox,
+ padding: '10px 0px'
+ },
+ both: {
+ }
+}
+
+class HelpKeystrokes extends Component {
+
+ render () {
+ return (
+
+
+
+
+
+
Navigation
+
+
+
+
+
view subscriptions
+
+
+
+
+
+
State Goodies
+
+
+
+
find keys or values
+
+
+
+
+
+
dispatch an action
+
+
+
+
+
+
+
+
+ )
+ }
+}
+
+export default HelpKeystrokes
diff --git a/packages/reactotron-app/App/Foundation/Sidebar.js b/packages/reactotron-app/App/Foundation/Sidebar.js
new file mode 100644
index 000000000..23e466052
--- /dev/null
+++ b/packages/reactotron-app/App/Foundation/Sidebar.js
@@ -0,0 +1,63 @@
+import React, { Component } from 'react'
+import AppStyles from '../Theme/AppStyles'
+import Colors from '../Theme/Colors'
+import SidebarButton from './SidebarButton'
+import { inject, observer } from 'mobx-react'
+
+const Styles = {
+ container: {
+ zIndex: 5,
+ width: 80,
+ backgroundColor: Colors.backgroundSubtleDark,
+ boxShadow: `0px 0px 30px ${Colors.glow}`,
+ borderRight: `1px solid ${Colors.chromeLine}`,
+ WebkitAppRegion: 'drag'
+ },
+ content: {
+ ...AppStyles.Layout.vbox,
+ height: '100vh',
+ alignItems: 'center'
+ },
+ tabs: {
+ paddingTop: 20
+ },
+ spacer: {
+ flex: 1
+ }
+}
+
+@inject('session')
+@observer
+class Sidebar extends Component {
+
+ constructor (props) {
+ super(props)
+ this.handleClickTimeline = () => { this.props.session.ui.switchTab('timeline') }
+ this.handleClickSubscriptions = () => { this.props.session.ui.switchTab('subscriptions') }
+ this.handleClickHelp = () => { this.props.session.ui.switchTab('help') }
+ this.handleClickSettings = () => { this.props.session.ui.switchTab('settings') }
+ }
+
+ render () {
+ const { session } = this.props
+ const { ui } = session
+
+ return (
+
+ )
+ }
+
+}
+
+export default Sidebar
diff --git a/packages/reactotron-app/App/Foundation/SidebarButton.js b/packages/reactotron-app/App/Foundation/SidebarButton.js
new file mode 100644
index 000000000..5e76efc8d
--- /dev/null
+++ b/packages/reactotron-app/App/Foundation/SidebarButton.js
@@ -0,0 +1,44 @@
+import React, { Component, PropTypes } from 'react'
+import AppStyles from '../Theme/AppStyles'
+import Colors from '../Theme/Colors'
+import { merge } from 'ramda'
+
+const Styles = {
+ container: {
+ ...AppStyles.Layout.vbox,
+ alignItems: 'center',
+ marginTop: 20,
+ marginBottom: 20,
+ color: Colors.highlight,
+ cursor: 'pointer'
+ },
+ containerActive: {
+ color: Colors.foregroundLight
+ },
+ iconSize: 32
+}
+
+class SidebarButton extends Component {
+
+ static propTypes = {
+ icon: PropTypes.string.isRequired,
+ text: PropTypes.string,
+ isActive: PropTypes.bool.isRequired,
+ onClick: PropTypes.func.isRequired
+ }
+
+ render () {
+ const { icon, isActive, onClick } = this.props
+ const Icon = require(`react-icons/lib/md/${icon}`)
+ const containerStyles = merge(Styles.container, isActive ? Styles.containerActive : {})
+
+ return (
+
+
+
+ )
+ }
+
+}
+
+export default SidebarButton
diff --git a/packages/reactotron-app/App/Foundation/Subscriptions.js b/packages/reactotron-app/App/Foundation/Subscriptions.js
new file mode 100644
index 000000000..39879e6db
--- /dev/null
+++ b/packages/reactotron-app/App/Foundation/Subscriptions.js
@@ -0,0 +1,86 @@
+import React, { Component } from 'react'
+import Colors from '../Theme/Colors'
+import AppStyles from '../Theme/AppStyles'
+import { inject, observer } from 'mobx-react'
+import { is, map, merge } from 'ramda'
+import ObjectTree from '../Shared/ObjectTree'
+import { colorForValue, textForValue } from '../Shared/MakeTable'
+import SubscriptionsHeader from './SubscriptionsHeader'
+
+const Styles = {
+ container: {
+ ...AppStyles.Layout.vbox,
+ margin: 0,
+ flex: 1
+ },
+ watches: {
+ margin: 0,
+ padding: 0,
+ overflowY: 'scroll',
+ overflowX: 'hidden'
+ },
+ watch: {
+ ...AppStyles.Layout.hbox,
+ padding: '15px 20px',
+ justifyContent: 'space-between',
+ borderBottom: `1px solid ${Colors.line}`
+ },
+ watchLeft: {
+ flex: 0.3,
+ wordBreak: 'break-all'
+ },
+ watchPath: {
+ cursor: 'pointer',
+ color: Colors.tag
+ },
+ watchValue: {
+ flex: 0.7,
+ wordBreak: 'break-all'
+ },
+ title: {
+ color: Colors.tag
+ }
+}
+
+@inject('session')
+@observer
+class WatchPanel extends Component {
+
+ constructor (props) {
+ super(props)
+ this.renderWatch = this.renderWatch.bind(this)
+ }
+
+ renderWatch (watch, indent = 0) {
+ const unsubscribe = (path) => {
+ this.props.session.ui.removeStateWatch(path)
+ }
+ const key = `watch-${watch.path}`
+ const value = is(Object, watch.value) ? : textForValue(watch.value)
+ const watchValueStyles = merge(Styles.watchValue, { color: colorForValue(watch.value) })
+ return (
+
+ )
+ }
+
+ render () {
+ const { watches } = this.props.session
+ return (
+
+
+
+ {map(this.renderWatch, watches)}
+
+
+ )
+ }
+}
+
+export default WatchPanel
diff --git a/packages/reactotron-app/App/Foundation/SubscriptionsHeader.js b/packages/reactotron-app/App/Foundation/SubscriptionsHeader.js
new file mode 100644
index 000000000..f2bdf54dd
--- /dev/null
+++ b/packages/reactotron-app/App/Foundation/SubscriptionsHeader.js
@@ -0,0 +1,83 @@
+import React, { Component } from 'react'
+import Colors from '../Theme/Colors'
+import AppStyles from '../Theme/AppStyles'
+import { inject, observer } from 'mobx-react'
+import IconAdd from 'react-icons/lib/md/add'
+import IconClear from 'react-icons/lib/md/delete-forever'
+
+const TITLE = 'State Subscriptions'
+
+const toolbarButton = {
+ cursor: 'pointer'
+}
+
+const Styles = {
+ container: {
+ WebkitAppRegion: 'drag',
+ backgroundColor: Colors.backgroundSubtleLight,
+ borderBottom: `1px solid ${Colors.chromeLine}`,
+ color: Colors.foregroundDark,
+ boxShadow: `0px 0px 30px ${Colors.glow}`
+ },
+ content: {
+ height: 60,
+ paddingLeft: 10,
+ paddingRight: 10,
+ ...AppStyles.Layout.hbox,
+ justifyContent: 'space-between'
+ },
+ left: {
+ ...AppStyles.Layout.hbox,
+ width: 100
+ },
+ right: {
+ width: 100,
+ ...AppStyles.Layout.hbox,
+ justifyContent: 'flex-end',
+ alignItems: 'center'
+ },
+ center: {
+ ...AppStyles.Layout.vbox,
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center'
+ },
+ title: {
+ color: Colors.foregroundLight,
+ textAlign: 'center'
+ },
+ iconSize: 32,
+ toolbarAdd: {
+ ...toolbarButton
+ },
+ toolbarClear: {
+ ...toolbarButton
+ }
+}
+
+@inject('session')
+@observer
+class SubscriptionsHeader extends Component {
+
+ render () {
+ const { ui } = this.props.session
+
+ return (
+
+ )
+ }
+
+}
+
+export default SubscriptionsHeader
diff --git a/packages/reactotron-app/App/Foundation/Timeline.js b/packages/reactotron-app/App/Foundation/Timeline.js
new file mode 100644
index 000000000..7ab29ccd3
--- /dev/null
+++ b/packages/reactotron-app/App/Foundation/Timeline.js
@@ -0,0 +1,68 @@
+import React, { Component } from 'react'
+import { inject, observer } from 'mobx-react'
+import getCommandComponent from '../Commands'
+import TimelineHeader from './TimelineHeader'
+import { map } from 'ramda'
+import AppStyles from '../Theme/AppStyles'
+
+const Styles = {
+ container: {
+ ...AppStyles.Layout.vbox,
+ margin: 0,
+ flex: 1
+ },
+ commands: {
+ margin: 0,
+ padding: 0,
+ overflowY: 'scroll',
+ overflowX: 'hidden'
+ }
+}
+
+@inject('session')
+@observer
+class Timeline extends Component {
+
+ // fires when we will update
+ componentWillUpdate () {
+ const node = this.refs.commands
+ // http://blog.vjeux.com/2013/javascript/scroll-position-with-react.html
+ // remember our height, position, and if we're at the top
+ this.scrollHeight = node.scrollHeight
+ this.scrollTop = node.scrollTop
+ this.isPinned = this.scrollTop === 0
+ }
+
+ // fires after we did update
+ componentDidUpdate () {
+ // should we be pinned to top, let's not auto-scroll
+ if (this.isPinned) return
+ const node = this.refs.commands
+ // scroll to the place we were before
+ // TODO: this falls apart as we reach max queue size as the scrollHeigh no longer changes
+ node.scrollTop = this.scrollTop + node.scrollHeight - this.scrollHeight
+ }
+
+ render () {
+ // grab the commands, but sdrawkcab
+ const commands = this.props.session.commands
+
+ const renderItem = command => {
+ const CommandComponent = getCommandComponent(command)
+ return
+ }
+
+ return (
+
+
+
+
+ {map(renderItem, commands)}
+
+
+ )
+ }
+
+}
+
+export default Timeline
diff --git a/packages/reactotron-app/App/Foundation/TimelineHeader.js b/packages/reactotron-app/App/Foundation/TimelineHeader.js
new file mode 100644
index 000000000..02e3a272c
--- /dev/null
+++ b/packages/reactotron-app/App/Foundation/TimelineHeader.js
@@ -0,0 +1,44 @@
+import React, { Component } from 'react'
+import Colors from '../Theme/Colors'
+import AppStyles from '../Theme/AppStyles'
+import { inject, observer } from 'mobx-react'
+
+const TITLE = 'Event Timeline'
+
+const Styles = {
+ container: {
+ WebkitAppRegion: 'drag',
+ backgroundColor: Colors.backgroundSubtleLight,
+ borderBottom: `1px solid ${Colors.chromeLine}`,
+ color: Colors.foregroundDark,
+ boxShadow: `0px 0px 30px ${Colors.glow}`
+ },
+ content: {
+ height: 60,
+ ...AppStyles.Layout.hbox,
+ alignItems: 'center',
+ justifyContent: 'center'
+ },
+ title: {
+ color: Colors.foregroundLight,
+ textAlign: 'center'
+ }
+}
+
+@inject('session')
+@observer
+class TimelineHeader extends Component {
+
+ render () {
+ return (
+
+ )
+ }
+
+}
+
+export default TimelineHeader
diff --git a/packages/reactotron-app/App/Foundation/VisualRoot.js b/packages/reactotron-app/App/Foundation/VisualRoot.js
new file mode 100644
index 000000000..c74601f9b
--- /dev/null
+++ b/packages/reactotron-app/App/Foundation/VisualRoot.js
@@ -0,0 +1,83 @@
+import React, { Component } from 'react'
+import Colors from '../Theme/Colors'
+import AppStyles from '../Theme/AppStyles'
+import Timeline from './Timeline'
+import StateKeysAndValuesDialog from '../Dialogs/StateKeysAndValuesDialog'
+import StateDispatchDialog from '../Dialogs/StateDispatchDialog'
+import HelpDialog from '../Dialogs/HelpDialog'
+import StateWatchDialog from '../Dialogs/StateWatchDialog'
+import Subscriptions from './Subscriptions'
+import Sidebar from './Sidebar'
+import Help from './Help'
+import { inject, observer } from 'mobx-react'
+
+const Styles = {
+ container: {
+ ...AppStyles.Layout.vbox
+ },
+ content: {
+ ...AppStyles.Layout.vbox,
+ backgroundColor: Colors.background,
+ color: Colors.foreground,
+ height: '100vh',
+ scroll: 'hidden'
+ },
+ body: {
+ ...AppStyles.Layout.hbox
+ },
+ app: {
+ ...AppStyles.Layout.vbox,
+ scroll: 'none',
+ overflow: 'hidden'
+ },
+ page: {
+ ...AppStyles.Layout.vbox
+ },
+ pageHidden: {
+ flex: 0,
+ height: 0,
+ visibility: 'hidden'
+ }
+}
+
+@inject('session')
+@observer
+export default class VisualRoot extends Component {
+
+ render () {
+ const { session } = this.props
+ const { ui } = session
+ const showTimeline = ui.tab === 'timeline'
+ const showSubscriptions = ui.tab === 'subscriptions'
+ const showHelp = ui.tab === 'help'
+ const showSettings = ui.tab === 'settings'
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Settings
+
+
+
+
+
+
+
+
+
+ )
+ }
+}
diff --git a/packages/reactotron-app/App/Lib/IsShallow.js b/packages/reactotron-app/App/Lib/IsShallow.js
new file mode 100644
index 000000000..5af53e19f
--- /dev/null
+++ b/packages/reactotron-app/App/Lib/IsShallow.js
@@ -0,0 +1,11 @@
+import { pipe, values, without, map, reject, contains, __, length, equals } from 'ramda'
+
+// inspects an object's values to see if they go deeper than 1 level.
+export default pipe(
+ values,
+ without([null, undefined]),
+ map(x => typeof x),
+ reject(contains(__, ['number', 'string', 'boolean'])),
+ length,
+ equals(0)
+)
diff --git a/packages/reactotron-app/App/Lib/Mousetrap.min.js b/packages/reactotron-app/App/Lib/Mousetrap.min.js
new file mode 100644
index 000000000..012895005
--- /dev/null
+++ b/packages/reactotron-app/App/Lib/Mousetrap.min.js
@@ -0,0 +1,11 @@
+/* mousetrap v1.6.0 craig.is/killing/mice */
+(function(r,t,g){function u(a,b,h){a.addEventListener?a.addEventListener(b,h,!1):a.attachEvent("on"+b,h)}function y(a){if("keypress"==a.type){var b=String.fromCharCode(a.which);a.shiftKey||(b=b.toLowerCase());return b}return k[a.which]?k[a.which]:p[a.which]?p[a.which]:String.fromCharCode(a.which).toLowerCase()}function D(a){var b=[];a.shiftKey&&b.push("shift");a.altKey&&b.push("alt");a.ctrlKey&&b.push("ctrl");a.metaKey&&b.push("meta");return b}function v(a){return"shift"==a||"ctrl"==a||"alt"==a||
+"meta"==a}function z(a,b){var h,c,e,g=[];h=a;"+"===h?h=["+"]:(h=h.replace(/\+{2}/g,"+plus"),h=h.split("+"));for(e=0;el||k.hasOwnProperty(l)&&(n[k[l]]=l)}e=n[h]?"keydown":"keypress"}"keypress"==e&&g.length&&(e="keydown");return{key:c,modifiers:g,action:e}}function C(a,b){return null===a||a===t?!1:a===b?!0:C(a.parentNode,b)}function c(a){function b(a){a=
+a||{};var b=!1,m;for(m in n)a[m]?b=!0:n[m]=0;b||(w=!1)}function h(a,b,m,f,c,h){var g,e,k=[],l=m.type;if(!d._callbacks[a])return[];"keyup"==l&&v(a)&&(b=[a]);for(g=0;g":".","?":"/","|":"\\"},A={option:"alt",command:"meta","return":"enter",
+escape:"esc",plus:"+",mod:/Mac|iPod|iPhone|iPad/.test(navigator.platform)?"meta":"ctrl"},n;for(g=1;20>g;++g)k[111+g]="f"+g;for(g=0;9>=g;++g)k[g+96]=g;c.prototype.bind=function(a,b,c){a=a instanceof Array?a:[a];this._bindMultiple.call(this,a,b,c);return this};c.prototype.unbind=function(a,b){return this.bind.call(this,a,function(){},b)};c.prototype.trigger=function(a,b){if(this._directMap[a+":"+b])this._directMap[a+":"+b]({},a);return this};c.prototype.reset=function(){this._callbacks={};this._directMap=
+{};return this};c.prototype.stopCallback=function(a,b){return-1<(" "+b.className+" ").indexOf(" mousetrap ")||C(b,this.target)?!1:"INPUT"==b.tagName||"SELECT"==b.tagName||"TEXTAREA"==b.tagName||b.isContentEditable};c.prototype.handleKey=function(){return this._handleKey.apply(this,arguments)};c.addKeycodes=function(a){for(var b in a)a.hasOwnProperty(b)&&(k[b]=a[b]);n=null};c.init=function(){var a=c(t),b;for(b in a)"_"!==b.charAt(0)&&(c[b]=function(b){return function(){return a[b].apply(a,arguments)}}(b))};
+c.init();r.Mousetrap=c;"undefined"!==typeof module&&module.exports&&(module.exports=c);"function"===typeof define&&define.amd&&define(function(){return c})}})("undefined"!==typeof window?window:null,"undefined"!==typeof window?document:null);
diff --git a/packages/reactotron-app/App/Lib/ShallowDiff.js b/packages/reactotron-app/App/Lib/ShallowDiff.js
new file mode 100644
index 000000000..0aa4dd953
--- /dev/null
+++ b/packages/reactotron-app/App/Lib/ShallowDiff.js
@@ -0,0 +1,54 @@
+// https://github.com/ramda/ramda/wiki/Cookbook#diffobjs---diffing-objects-similar-to-guavas-mapsdifference
+import {
+ curry, pipe, useWith, __, map,
+ toPairs, last, fromPairs,
+ groupBy, mergeWith, always, has, both,
+ objOf, merge, prop, apply, ifElse,
+ cond, values, equals, evolve
+} from 'ramda' // boom
+
+const groupObjBy = curry(
+ pipe(
+ // Call groupBy with the object as pairs, passing only the value to the key function
+ useWith(groupBy, [useWith(__, [last]), toPairs]),
+ map(fromPairs)
+ )
+)
+
+const LEFT_VALUE = 'leftValue'
+const RIGHT_VALUE = 'rightValue'
+const COMMON = 'common'
+const DIFFERENCE = 'difference'
+const ONLY_ON_LEFT = 'onlyOnLeft'
+const ONLY_ON_RIGHT = 'onlyOnRight'
+
+const diffObjs = pipe(
+ useWith(
+ mergeWith(merge),
+ [map(objOf(LEFT_VALUE)), map(objOf(RIGHT_VALUE))]
+ ),
+
+ groupObjBy(cond([
+ [
+ both(has(LEFT_VALUE), has(RIGHT_VALUE)),
+ pipe(
+ values,
+ ifElse(
+ apply(equals),
+ always(COMMON),
+ always(DIFFERENCE)
+ )
+ )
+ ],
+ [has(LEFT_VALUE), always(ONLY_ON_LEFT)],
+ [has(RIGHT_VALUE), always(ONLY_ON_RIGHT)]
+ ])),
+
+ evolve({
+ [COMMON]: map(prop(LEFT_VALUE)),
+ [ONLY_ON_LEFT]: map(prop(LEFT_VALUE)),
+ [ONLY_ON_RIGHT]: map(prop(RIGHT_VALUE))
+ })
+)
+
+export default diffObjs
diff --git a/packages/reactotron-app/App/Shared/Command.js b/packages/reactotron-app/App/Shared/Command.js
new file mode 100644
index 000000000..6d86a628c
--- /dev/null
+++ b/packages/reactotron-app/App/Shared/Command.js
@@ -0,0 +1,155 @@
+import React, { Component, PropTypes } from 'react'
+import Colors from '../Theme/Colors'
+import AppStyles from '../Theme/AppStyles'
+import Timestamp from '../Shared/Timestamp'
+import { observer } from 'mobx-react'
+import { merge, equals } from 'ramda'
+import CommandToolbar from './CommandToolbar'
+import DisplayIcon from 'react-icons/lib/md/label'
+
+const IconOpen = require('react-icons/lib/md/expand-more')
+const IconClosed = require('react-icons/lib/md/chevron-right')
+
+const Styles = {
+ container: {
+ ...AppStyles.Layout.hbox,
+ marginTop: 0,
+ alignItems: 'flex-start',
+ borderBottom: `1px solid ${Colors.line}`
+ },
+ containerOpen: {
+ backgroundColor: Colors.backgroundSubtleLight
+ },
+ icon: {
+ color: Colors.backgroundHighlight
+ },
+ body: {
+ ...AppStyles.Layout.vbox,
+ marginLeft: 0
+ },
+ topRow: {
+ ...AppStyles.Layout.hbox,
+ justifyContent: 'space-between',
+ alignItems: 'flex-start',
+ padding: '15px 20px',
+ cursor: 'pointer'
+ },
+ title: {
+ textAlign: 'left',
+ width: 168
+ },
+ titleText: {
+ color: Colors.tag
+ },
+ titleTextInverse: {
+ backgroundColor: Colors.tag,
+ color: Colors.tagComplement,
+ borderRadius: 4,
+ padding: '4px 8px'
+ },
+ displayIcon: {
+ marginRight: 4,
+ marginBottom: 4
+ },
+ displayIconSize: 16,
+ preview: {
+ color: Colors.highlight,
+ textAlign: 'left',
+ paddingRight: 16,
+ overflow: 'hidden',
+ WebkitLineClamp: 3,
+ WebkitBoxOrient: 'vertical',
+ textOverflow: 'ellipsis',
+ display: '-webkit-box',
+ flex: 1
+ },
+ duration: {
+ color: Colors.foregroundDark,
+ paddingRight: 10
+ },
+ timestamp: {
+ color: Colors.foregroundDark,
+ paddingRight: 10
+ },
+ spacer: {
+ flex: 1
+ },
+ children: {
+ overflow: 'hidden',
+ animation: 'fade-up 0.25s',
+ willChange: 'transform opacity',
+ padding: '0 40px 30px 40px'
+ }
+}
+
+@observer
+class Command extends Component {
+
+ static propTypes = {
+ command: PropTypes.object.isRequired,
+ title: PropTypes.string.isRequired,
+ preview: PropTypes.string,
+ subtitle: PropTypes.string,
+ duration: PropTypes.number
+ }
+
+ state = {
+ isOpen: false
+ }
+
+ constructor (props) {
+ super(props)
+ this.state = {
+ isOpen: props.startsOpen || false
+ }
+ this.handleToggleOpen = this.handleToggleOpen.bind(this)
+ }
+
+ shouldComponentUpdate (nextProps, nextState) {
+ return !(equals(nextProps, this.props) && equals(this.state, nextState))
+ }
+
+ handleToggleOpen () {
+ this.setState({ isOpen: !this.state.isOpen })
+ }
+
+ render () {
+ const { isOpen } = this.state
+ const { command, children, title, preview } = this.props
+ const { important, type } = command
+ const isDisplay = type === 'display'
+ const { date } = command
+ const titleTextStyle = merge(Styles.titleText, important ? Styles.titleTextInverse : {})
+ const topRowStyle = Styles.topRow
+ const timestampStyle = Styles.timestamp
+ const Icon = isOpen ? IconOpen : IconClosed
+ const containerStyles = merge(Styles.container, isOpen && Styles.containerOpen)
+ return (
+
+
+
+
+
+
+ {isDisplay && }
+ {title}
+
+
+ {!isOpen &&
{preview} }
+ {isOpen &&
}
+ {isOpen &&
}
+
+
+ {isOpen &&
+
+ {children}
+
+ }
+
+
+ )
+ }
+
+}
+
+export default Command
diff --git a/packages/reactotron-app/App/Shared/CommandIcon.js b/packages/reactotron-app/App/Shared/CommandIcon.js
new file mode 100644
index 000000000..1d27a8357
--- /dev/null
+++ b/packages/reactotron-app/App/Shared/CommandIcon.js
@@ -0,0 +1,61 @@
+import React, { Component, PropTypes } from 'react'
+import BenchmarkIcon from 'react-icons/lib/md/assignment-turned-in'
+import DebugIcon from 'react-icons/lib/md/info'
+import WarningIcon from 'react-icons/lib/md/warning'
+import ErrorIcon from 'react-icons/lib/md/error'
+import ActionIcon from 'react-icons/lib/md/check-box'
+import UnknownIcon from 'react-icons/lib/md/block'
+import Colors from '../Theme/Colors'
+
+const Styles = {
+ container: {
+
+ },
+ iconColor: Colors.Palette.grey,
+ iconSize: 40
+}
+
+const benchmark =
+const debug =
+const warning =
+const error =
+const action =
+const unknown =
+
+const getIcon = (command) => {
+ const { type, payload } = command
+
+ switch (type) {
+ case 'benchmark.report': return benchmark
+ case 'state.action.complete': return action
+ case 'log':
+ const { level } = payload
+ switch (level) {
+ case 'warn': return warning
+ case 'error': return error
+ default: return debug
+ }
+
+ default: return unknown
+ }
+}
+
+class CommandIcon extends Component {
+
+ static propTypes = {
+ command: PropTypes.object.isRequired
+ }
+
+ render () {
+ const { command } = this.props
+ const icon = getIcon(command)
+
+ return (
+
+ {icon}
+
+ )
+ }
+}
+
+export default CommandIcon
diff --git a/packages/reactotron-app/App/Shared/CommandToolbar.js b/packages/reactotron-app/App/Shared/CommandToolbar.js
new file mode 100644
index 000000000..02cb65db6
--- /dev/null
+++ b/packages/reactotron-app/App/Shared/CommandToolbar.js
@@ -0,0 +1,73 @@
+import React, { Component, PropTypes } from 'react'
+import { inject, observer } from 'mobx-react'
+import Button from './CommandToolbarButton'
+import AppStyles from '../Theme/AppStyles'
+import stringifyObject from 'stringify-object'
+
+const Styles = {
+ container: {
+ ...AppStyles.Layout.hbox,
+ marginLeft: -6
+ }
+}
+
+// the tips
+const TIP_REPLAY_ACTION = 'Repeat this action.'
+const TIP_CUSTOMIZE_REPLAY_ACTION = 'Edit and dispatch this action.'
+
+// the buttons (minus the onClick)
+const ReplayButton = props =>
+const CustomizeReplayButton = props =>
+
+@inject('session')
+@observer
+class CommandToolbar extends Component {
+
+ static propTypes = {
+ command: PropTypes.object.isRequired
+ }
+
+ // fires when it is time to replay an action
+ handleReplayAction = event => {
+ const { command, session } = this.props
+ const { ui } = session
+
+ ui.dispatchAction(command.payload.action)
+ event.stopPropagation()
+ }
+
+ // customize this action before replaying
+ handleCustomizeReplayAction = event => {
+ const { command, session } = this.props
+ const { ui } = session
+ const { payload } = command
+ const { action } = payload
+ const newAction = stringifyObject(action, {
+ indent: ' ',
+ singleQuotes: true
+ })
+
+ ui.setActionToDispatch(newAction)
+ event.stopPropagation()
+ }
+
+ render () {
+ const { command } = this.props
+ const showReplayAction = command.type === 'state.action.complete'
+ const showCustomizeReplayAction = command.type === 'state.action.complete'
+
+ return (
+
+ {showReplayAction &&
+
+ }
+ {showCustomizeReplayAction &&
+
+ }
+
+ )
+ }
+
+}
+
+export default CommandToolbar
diff --git a/packages/reactotron-app/App/Shared/CommandToolbarButton.js b/packages/reactotron-app/App/Shared/CommandToolbarButton.js
new file mode 100644
index 000000000..b4fc13e9b
--- /dev/null
+++ b/packages/reactotron-app/App/Shared/CommandToolbarButton.js
@@ -0,0 +1,37 @@
+import React, { Component, PropTypes } from 'react'
+import Colors from '../Theme/Colors'
+import ReactTooltip from 'react-tooltip'
+
+const Styles = {
+ container: {
+ color: Colors.highlight,
+ marginTop: -2,
+ marginRight: 8
+ },
+ iconSize: 24,
+ icon: {
+ }
+}
+
+class CommandToolbarButton extends Component {
+
+ static propTypes = {
+ tip: PropTypes.string,
+ icon: PropTypes.string.isRequired,
+ onClick: PropTypes.func.isRequired
+ }
+
+ render () {
+ const { tip, icon, onClick } = this.props
+ const Icon = require(`react-icons/lib/md/${icon}`)
+ return (
+
+
+
+
+ )
+ }
+
+}
+
+export default CommandToolbarButton
diff --git a/packages/reactotron-app/App/Shared/Content.js b/packages/reactotron-app/App/Shared/Content.js
new file mode 100644
index 000000000..c47d90b86
--- /dev/null
+++ b/packages/reactotron-app/App/Shared/Content.js
@@ -0,0 +1,69 @@
+import React, { Component, PropTypes } from 'react'
+import { map, trim, split, isNil } from 'ramda'
+import ObjectTree from './ObjectTree'
+import isShallow from '../Lib/IsShallow'
+import makeTable from './MakeTable'
+
+const NULL_TEXT = '¯\\_(ツ)_/¯'
+
+class Content extends Component {
+
+ static propTypes = {
+ value: PropTypes.oneOfType([ PropTypes.object, PropTypes.array, PropTypes.string, PropTypes.number, PropTypes.bool ]),
+ treeLevel: PropTypes.number
+ }
+
+ constructor (props) {
+ super(props)
+ this.spanCount = 0
+ this.breakIntoSpans = this.breakIntoSpans.bind(this)
+ }
+
+ breakIntoSpans (part) {
+ this.spanCount++
+ return (
+ {part}
+ )
+ }
+
+ renderString () {
+ const { value } = this.props
+ return (
+
+ {map(this.breakIntoSpans, split('\n', trim(value)))}
+
+ )
+ }
+
+ // render as object with shallow objects rendered as tables
+ renderObject () {
+ const { treeLevel = 0, value } = this.props
+ return isShallow(value) ? makeTable(value) :
+ }
+
+ // arrays just render as objects at this point
+ renderArray () {
+ this.renderObject()
+ }
+
+ renderMysteryMeat () {
+ const { value } = this.props
+ return (
+ {String(value)}
+ )
+ }
+
+ render () {
+ const { value } = this.props
+ if (isNil(value)) return {NULL_TEXT}
+ const valueType = typeof value
+ switch (valueType) {
+ case 'string': return this.renderString()
+ case 'object': return this.renderObject()
+ case 'array': return this.renderArray()
+ default: return this.renderMysteryMeat()
+ }
+ }
+}
+
+export default Content
diff --git a/packages/reactotron-app/App/Shared/Key.js b/packages/reactotron-app/App/Shared/Key.js
new file mode 100644
index 000000000..2ab132dc6
--- /dev/null
+++ b/packages/reactotron-app/App/Shared/Key.js
@@ -0,0 +1,36 @@
+import React, { Component, PropTypes } from 'react'
+import Colors from '../Theme/Colors'
+
+const Styles = {
+ container: {
+ color: Colors.background,
+ textTransform: 'uppercase',
+ borderRadius: 4,
+ backgroundColor: Colors.foreground,
+ padding: '4px 12px',
+ fontWeight: 'bold',
+ borderBottom: `2px solid ${Colors.highlight}`,
+ marginLeft: 2,
+ marginRight: 2
+ }
+}
+
+class Key extends Component {
+
+ static propTypes = {
+ text: PropTypes.string.isRequired
+ }
+
+ render () {
+ const { text } = this.props
+
+ return (
+
+ {text}
+
+ )
+ }
+
+}
+
+export default Key
diff --git a/packages/reactotron-app/App/Shared/MakeTable.js b/packages/reactotron-app/App/Shared/MakeTable.js
new file mode 100644
index 000000000..f208bec21
--- /dev/null
+++ b/packages/reactotron-app/App/Shared/MakeTable.js
@@ -0,0 +1,67 @@
+import React from 'react'
+import Colors from '../Theme/Colors'
+import { merge, map, toPairs, identity, isNil, T, cond, always } from 'ramda'
+
+const NULL_TEXT = '¯\\_(ツ)_/¯'
+const TRUE_TEXT = 'true'
+const FALSE_TEXT = 'false'
+
+const Styles = {
+ row: {
+ display: 'flex',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ WebkitUserSelect: 'all',
+ padding: '2px 0'
+ },
+ key: {
+ width: 210,
+ paddingRight: 10,
+ wordBreak: 'break-all',
+ textAlign: 'left',
+ color: Colors.foregroundDark
+ },
+ value: {
+ flex: 1,
+ wordBreak: 'break-all'
+ }
+}
+
+export function textForValue (value) {
+ return cond([
+ [isNil, always(NULL_TEXT)],
+ [x => typeof x === 'boolean', always(value ? TRUE_TEXT : FALSE_TEXT)],
+ [T, identity]
+ ])(value)
+}
+
+export function colorForValue (value) {
+ if (isNil(value)) return Colors.foregroundDark
+ const valueType = typeof value
+ switch (valueType) {
+ case 'boolean': return Colors.constant
+ case 'string': return Colors.foreground
+ case 'number': return Colors.constant
+ default: return Colors.foreground
+ }
+}
+
+const makeRow = ([key, value]) => {
+ const textValue = textForValue(value)
+ const valueStyle = merge(Styles.value, { color: colorForValue(value) })
+
+ return (
+
+
{key}
+
+ {textValue}
+
+
+ )
+}
+
+export default headers => (
+
+ {map(makeRow, toPairs(headers))}
+
+)
diff --git a/packages/reactotron-app/App/Shared/ObjectTree.js b/packages/reactotron-app/App/Shared/ObjectTree.js
new file mode 100644
index 000000000..725b23918
--- /dev/null
+++ b/packages/reactotron-app/App/Shared/ObjectTree.js
@@ -0,0 +1,49 @@
+import React, { Component, PropTypes } from 'react'
+import JSONTree from 'react-json-tree'
+import Colors from '../Theme/Colors'
+
+const theme = { ...Colors.theme, base0B: Colors.foreground }
+
+const Styles = {
+ container: {},
+ theme: {
+ tree: { backgroundColor: 'transparent', marginTop: -3 },
+ ...theme
+ },
+ muted: {
+ color: Colors.highlight
+ }
+}
+
+class ObjectTree extends Component {
+
+ static propTypes = {
+ object: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
+ level: PropTypes.number
+ }
+
+ render () {
+ const { object, level = 1 } = this.props
+ return (
+
+ minLevel <= level}
+ theme={Styles.theme}
+ invertTheme={Colors.invertTheme}
+ getItemString={(type, data, itemType, itemString) => {
+ if (type === 'Object') return {itemType}
+ return {itemType} {itemString}
+ }}
+ valueRenderer={(transformed, untransformed) => {
+ return `${untransformed || transformed}`
+ }}
+ />
+
+ )
+ }
+
+}
+
+export default ObjectTree
diff --git a/packages/reactotron-app/App/Shared/Timestamp.js b/packages/reactotron-app/App/Shared/Timestamp.js
new file mode 100644
index 000000000..5d01f56d2
--- /dev/null
+++ b/packages/reactotron-app/App/Shared/Timestamp.js
@@ -0,0 +1,40 @@
+import React, { Component, PropTypes } from 'react'
+import moment from 'moment'
+import Colors from '../Theme/Colors'
+
+const Styles = {
+ container: {
+ margin: 0,
+ padding: 0
+ },
+ left: {
+ color: Colors.foregroundDark
+ },
+ right: {
+ color: Colors.foreground
+ }
+}
+
+class Timestamp extends Component {
+
+ static propTypes = {
+ date: PropTypes.object.isRequired,
+ style: PropTypes.object
+ }
+
+ render () {
+ const date = moment(this.props.date)
+ const left = date.format('h:mm')
+ // const right = date.format('ss.SS')
+ const right = date.format(':ss')
+ const containerStyles = {...Styles.container, ...this.props.style}
+ return (
+
+ {left}
+ {right}
+
+ )
+ }
+}
+
+export default Timestamp
diff --git a/packages/reactotron-app/App/Stores/SessionStore.js b/packages/reactotron-app/App/Stores/SessionStore.js
new file mode 100644
index 000000000..054490cd0
--- /dev/null
+++ b/packages/reactotron-app/App/Stores/SessionStore.js
@@ -0,0 +1,70 @@
+import UiStore from './UiStore'
+import { createServer } from 'reactotron-core-server'
+import { computed, reaction } from 'mobx'
+import { last, isNil, reject, equals, reverse, pipe, propEq, map, fromPairs } from 'ramda'
+import { dotPath } from 'ramdasauce'
+import shallowDiff from '../Lib/ShallowDiff'
+
+const isSubscription = propEq('type', 'state.values.change')
+const isSubscriptionCommandWithEmptyChanges = command => isSubscription(command) && dotPath('payload.changes.length', command) === 0
+
+class Session {
+
+ // holds the last known state of the subscription values
+ subscriptions = {}
+
+ // checks if it was the exact same as last time
+ isSubscriptionValuesSameAsLastTime (command) {
+ if (!isSubscription(command)) return false
+ const rawChanges = command.payload && command.payload.changes || []
+ const newSubscriptions = fromPairs(map(change => ([change.path, change.value]), rawChanges))
+ const isNew = !equals(this.subscriptions, newSubscriptions)
+
+ // 🚨🚨🚨 side effect alert 🚨🚨🚨
+ if (isNew) {
+ const diff = shallowDiff(this.subscriptions, newSubscriptions)
+ command.payload.changed = map(v => v.rightValue, diff.difference || [])
+ command.payload.added = diff.onlyOnRight || []
+ command.payload.removed = diff.onlyOnLeft || []
+ this.subscriptions = newSubscriptions
+ }
+ // 🚨🚨🚨 side effect alert 🚨🚨🚨
+
+ return !isNew
+ }
+
+ @computed get commands () {
+ return pipe(
+ dotPath('server.commands.all'),
+ reject(isSubscriptionCommandWithEmptyChanges),
+ reject(this.isSubscriptionValuesSameAsLastTime),
+ reverse
+ )(this)
+ }
+
+ @computed get watches () {
+ const changeCommands = this.server.commands['state.values.change']
+ if (isNil(changeCommands)) return []
+ if (changeCommands.length === 0) return []
+ const recentCommand = last(changeCommands)
+ return dotPath('payload.changes', recentCommand) || []
+ }
+
+ constructor (port = 9090) {
+ this.server = createServer({ port })
+ this.server.start()
+ this.isSubscriptionValuesSameAsLastTime = this.isSubscriptionValuesSameAsLastTime.bind(this)
+
+ // create the ui store
+ this.ui = new UiStore(this.server)
+
+ // hide or show the watch panel depending if we have watches
+ reaction(
+ () => this.watches.length > 0,
+ show => { this.ui.showWatchPanel = show }
+ )
+ }
+
+}
+
+export default Session
diff --git a/packages/reactotron-app/App/Stores/UiStore.js b/packages/reactotron-app/App/Stores/UiStore.js
new file mode 100644
index 000000000..7a66d5e43
--- /dev/null
+++ b/packages/reactotron-app/App/Stores/UiStore.js
@@ -0,0 +1,194 @@
+import { observable, action } from 'mobx'
+import Mousetrap from '../Lib/Mousetrap.min.js'
+import { isNilOrEmpty } from 'ramdasauce'
+
+/**
+ * Handles UI state.
+ */
+class UI {
+ /**
+ * Which tab are we on?
+ */
+ @observable tab = 'timeline'
+
+ /**
+ * Targets state keys or values from the UI & commands.
+ */
+ @observable keysOrValues = 'keys'
+
+ // whether or not to show the state find dialog
+ @observable showStateFindDialog = false
+
+ // whether or not to show the state dispatch dialog
+ @observable showStateDispatchDialog = false
+
+ // whether or not to show the help dialog
+ @observable showHelpDialog = false
+
+ // the watch dialog
+ @observable showStateWatchDialog = false
+
+ // the current watch to add
+ @observable watchToAdd
+
+ // the current action to dispatch
+ @observable actionToDispatch
+
+ // show the watch panel?
+ @observable showWatchPanel = false
+
+ constructor (server) {
+ this.server = server
+
+ Mousetrap.prototype.stopCallback = () => false
+
+ Mousetrap.bind('command+k', this.reset)
+ Mousetrap.bind('command+f', this.openStateFindDialog)
+ Mousetrap.bind('command+d', this.openStateDispatchDialog)
+ Mousetrap.bind('tab', this.toggleKeysValues)
+ Mousetrap.bind('escape', this.popState)
+ Mousetrap.bind('enter', this.submitCurrentForm)
+ Mousetrap.bind('command+enter', this.submitCurrentFormDelicately)
+ Mousetrap.bind('command+n', this.openStateWatchDialog)
+ Mousetrap.bind('command+1', this.switchTab.bind(this, 'timeline'))
+ Mousetrap.bind('command+2', this.switchTab.bind(this, 'subscriptions'))
+ Mousetrap.bind('command+/', this.switchTab.bind(this, 'help'))
+ Mousetrap.bind('command+?', this.switchTab.bind(this, 'help'))
+ }
+
+ @action switchTab = (newTab) => {
+ this.tab = newTab
+ }
+
+ @action popState = () => {
+ if (this.showStateFindDialog) {
+ this.closeStateFindDialog()
+ }
+ if (this.showHelpDialog) {
+ this.closeHelpDialog()
+ }
+ }
+
+ @action submitCurrentForm = () => {
+ if (this.showStateWatchDialog) {
+ this.submitStateWatch()
+ }
+ }
+
+ @action submitCurrentFormDelicately = () => {
+ if (this.showStateDispatchDialog) {
+ this.submitStateDispatch()
+ }
+ }
+
+ @action submitStateWatch = () => {
+ this.server.stateValuesSubscribe(this.watchToAdd)
+ this.showStateWatchDialog = false
+ this.watchToAdd = null
+ }
+
+ @action removeStateWatch = (path) => {
+ this.server.stateValuesUnsubscribe(path)
+ }
+
+ @action clearStateWatches = () => {
+ this.server.stateValuesClearSubscriptions()
+ }
+
+ @action setActionToDispatch (action) {
+ this.actionToDispatch = action
+ this.showStateDispatchDialog = true
+ }
+
+ @action submitStateDispatch = () => {
+ // try not to blow up the frame
+ let action = null
+ try {
+ // brackets are need on chromium side, huh.
+ action = eval('(' + this.actionToDispatch + ')') // lulz - straight to hell.
+ } catch (e) {
+ }
+ // jet if not valid
+ if (isNilOrEmpty(action)) return
+
+ // let's attempt to dispatch
+ this.dispatchAction(action)
+ // close the form
+ this.showStateDispatchDialog = false
+ }
+
+ @action openStateFindDialog = () => {
+ this.showStateFindDialog = true
+ }
+
+ @action closeStateFindDialog = () => {
+ this.showStateFindDialog = false
+ }
+
+ @action openStateWatchDialog = () => {
+ this.showStateWatchDialog = true
+ }
+
+ @action closeStateWatchDialog = () => {
+ this.showStateWatchDialog = false
+ }
+
+ @action openStateDispatchDialog = () => {
+ this.showStateDispatchDialog = true
+ }
+
+ @action toggleHelpDialog = () => {
+ this.showHelpDialog = !this.showHelpDialog
+ }
+
+ @action openHelpDialog = () => {
+ this.showHelpDialog = true
+ }
+
+ @action closeStateDispatchDialog = () => {
+ this.showStateDispatchDialog = false
+ }
+
+ @action closeHelpDialog = () => {
+ this.showHelpDialog = false
+ }
+
+ @action reset = () => {
+ this.server.commands.all.clear()
+ }
+
+ @action getStateKeysOrValues = (path) => {
+ if (this.keysOrValues === 'keys') {
+ this.getStateKeys(path)
+ } else {
+ this.getStateValues(path)
+ }
+ }
+
+ @action getStateValues = (path) => {
+ this.server.stateValuesRequest(path)
+ }
+
+ @action getStateKeys = (path) => {
+ this.server.stateKeysRequest(path)
+ }
+
+ @action dispatchAction = action => {
+ this.server.stateActionDispatch(action)
+ }
+
+ @action toggleKeysValues = () => {
+ if (this.keysOrValues === 'keys') {
+ this.keysOrValues = 'values'
+ } else {
+ this.keysOrValues = 'keys'
+ }
+ }
+
+ @action toggleWatchPanel = () => {
+ this.showWatchPanel = !this.showWatchPanel
+ }
+
+}
+
+export default UI
diff --git a/packages/reactotron-app/App/Theme/AppStyles.js b/packages/reactotron-app/App/Theme/AppStyles.js
new file mode 100644
index 000000000..eaed0aa23
--- /dev/null
+++ b/packages/reactotron-app/App/Theme/AppStyles.js
@@ -0,0 +1,16 @@
+const Layout = {
+ vbox: {
+ display: 'flex',
+ flex: 1,
+ flexDirection: 'column'
+ },
+ hbox: {
+ display: 'flex',
+ flex: 1,
+ flexDirection: 'row'
+ }
+}
+
+export default {
+ Layout
+}
diff --git a/packages/reactotron-app/App/Theme/Colors.js b/packages/reactotron-app/App/Theme/Colors.js
new file mode 100644
index 000000000..5bf2bb311
--- /dev/null
+++ b/packages/reactotron-app/App/Theme/Colors.js
@@ -0,0 +1,63 @@
+import Color from 'color'
+import { createStyling } from 'react-base16-styling'
+
+// https://github.com/chriskempson/base16/blob/master/styling.md
+
+const getStylingFromBase16 = base16Theme => ({
+ roles: {
+ background: base16Theme.base00, // base00 - Default Background
+ backgroundSubtleLight: Color(base16Theme.base00).lighten(0.05).hslString(),
+ backgroundSubtleDark: Color(base16Theme.base00).darken(0.3).hslString(),
+ backgroundLighter: base16Theme.base01, // base01 - Lighter Background (Used for status bars)
+ line: Color(base16Theme.base01).darken(0.1).hslString(),
+ backgroundHighlight: base16Theme.base02, // base02 - Selection Background
+ highlight: base16Theme.base03, // base03 - Comments, Invisibles, Line Highlighting
+ foregroundDark: base16Theme.base04, // base04 - Dark Foreground (Used for status bars)
+ foreground: base16Theme.base05, // base05 - Default Foreground, Caret, Delimiters, Operators
+ foregroundLight: base16Theme.base06, // base06 - Light Foreground (Not often used)
+ backgroundLight: base16Theme.base07, // base07 - Light Background (Not often used)
+ tag: base16Theme.base08, // base08 - Variables, XML Tags, Markup Link Text, Markup Lists, Diff Deleted
+ tagComplement: Color(base16Theme.base08).lighten(0.65).hslString(),
+ constant: base16Theme.base09, // base09 - Integers, Boolean, Constants, XML Attributes, Markup Link Url
+ bold: base16Theme.base0A, // base0A - Classes, Markup Bold, Search Text Background
+ glow: Color(base16Theme.base00).darken(0.2).clearer(0.2).hslString(),
+ string: base16Theme.base0B, // base0B - Strings, Inherited Class, Markup Code, Diff Inserted
+ support: base16Theme.base0C, // base0C - Support, Regular Expressions, Escape Characters, Markup Quotes
+ heading: base16Theme.base0D, // base0D - Functions, Methods, Attribute IDs, Headings
+ keyword: base16Theme.base0E, // base0E - Keywords, Storage, Selector, Markup Italic, Diff Changed
+ warning: base16Theme.base0F, // base0F - Deprecated, Opening/Closing Embedded Language Tags e.g.
+ chrome: Color(base16Theme.base00).lighten(0.1).hslString(),
+ chromeLine: Color(base16Theme.base00).lighten(0.25).hslString()
+ },
+ theme: base16Theme // TODO: figure out why I'm doing this?
+})
+
+// the default theme until i figure out how to customize it on the fly
+// http://chriskempson.github.io/base16/
+// const defaultTheme = 'atliersavanah'
+// const defaultTheme = 'ocean'
+// const defaultTheme = 'mocha'
+// const defaultTheme = 'railscasts'
+// const defaultTheme = 'greenscreen'
+const defaultTheme = 'twilight'
+
+// the natural or inverted look
+const invertTheme = false
+
+// some kind of wierd factory?
+const createStylingFromTheme = createStyling(getStylingFromBase16, {})
+
+// here's where I think i should be allowing user customization?
+const styling = createStylingFromTheme(defaultTheme, invertTheme)
+
+// fish out the roles because I haven't committed fully to styling in the components just yet
+const roles = styling('roles').style
+
+// awkwardly expose the theme for the ObjectTree component
+const theme = styling('theme').style
+
+export default {
+ ...roles,
+ theme,
+ invertTheme
+}
diff --git a/packages/reactotron-app/App/Theme/Reactotron-128.png b/packages/reactotron-app/App/Theme/Reactotron-128.png
new file mode 100644
index 000000000..29527f8be
Binary files /dev/null and b/packages/reactotron-app/App/Theme/Reactotron-128.png differ
diff --git a/packages/reactotron-app/App/app.global.css b/packages/reactotron-app/App/app.global.css
new file mode 100644
index 000000000..a7b8719ae
--- /dev/null
+++ b/packages/reactotron-app/App/app.global.css
@@ -0,0 +1,55 @@
+html, body {
+ padding: 0;
+ margin: 0;
+ border: 0;
+ cursor: default;
+ /*font-family: 'Fira Code', 'Inconsolata-dz for Powerline', 'Roboto', '-apple-system', 'San Francisco', 'Helvetica Neue', sans-serif;;*/
+ /*font-family: 'Inconsolata-dz for Powerline', 'Roboto', '-apple-system', 'San Francisco', 'Helvetica Neue', sans-serif;;*/
+ /*font-family: 'Roboto', '-apple-system', 'San Francisco', 'Helvetica Neue', sans-serif;;*/
+ /*font-family: 'Thonburi', '-apple-system', 'San Francisco', 'Helvetica Neue', sans-serif;;*/
+ font-family: '-apple-system', 'San Francisco', 'Helvetica Neue', sans-serif;;
+ /*font-family: 'Comic Sans MS';*/
+ font-size: 0.94em;
+ -webkit-user-select: none;
+}
+
+::-webkit-scrollbar {
+ background-color: transparent;
+ width: 8px;
+ height: 8px;
+}
+::-webkit-scrollbar-track {
+ background-color: transparent;
+ border: 0px;
+ border-radius: 0px;
+}
+::-webkit-scrollbar-thumb {
+ background-color: rgba(128, 128, 128, 0.25) !important;
+ border-radius: 4px;
+}
+
+/* leet hacks */
+a.closeButton--jss-0-1 { display: none; }
+
+*:focus {
+ outline: none;
+}
+
+@-webkit-keyframes fade-up {
+ 0% { -webkit-transform: translateY(5%); opacity: 0; }
+ 100% { -webkit-transform: translateY(0%); opacity: 1; }
+}
+
+@-webkit-keyframes slide-up {
+ 0% { -webkit-transform: translateY(100%); }
+ 100% { -webkit-transform: translateY(0px); }
+}
+
+@-webkit-keyframes slide-down {
+ 0% { -webkit-transform: translateY(0px); }
+ 100% { -webkit-transform: translateY(100%); }
+}
+
+.tooltipTheme {
+ background-color: 'green'
+}
diff --git a/packages/reactotron-app/App/app.html b/packages/reactotron-app/App/app.html
new file mode 100644
index 000000000..c049c0cb0
--- /dev/null
+++ b/packages/reactotron-app/App/app.html
@@ -0,0 +1,27 @@
+
+
+
+
+ Reactotron
+
+
+
+
+
+
+
diff --git a/packages/reactotron-app/App/index.js b/packages/reactotron-app/App/index.js
new file mode 100644
index 000000000..9bf2f8728
--- /dev/null
+++ b/packages/reactotron-app/App/index.js
@@ -0,0 +1,12 @@
+import React from 'react'
+import { render } from 'react-dom'
+import App from './Foundation/App'
+import './App.global.css'
+import injectTapEventPlugin from 'react-tap-event-plugin'
+injectTapEventPlugin()
+
+render(
+
+ ,
+ document.getElementById('root')
+)
diff --git a/packages/reactotron-app/CHANGELOG.md b/packages/reactotron-app/CHANGELOG.md
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/reactotron-app/LICENSE b/packages/reactotron-app/LICENSE
new file mode 100644
index 000000000..b36092b50
--- /dev/null
+++ b/packages/reactotron-app/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 Steve Kellock
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/reactotron-app/README.md b/packages/reactotron-app/README.md
new file mode 100644
index 000000000..acd2d1430
--- /dev/null
+++ b/packages/reactotron-app/README.md
@@ -0,0 +1 @@
+# Reactotron UI
diff --git a/packages/reactotron-app/main.development.js b/packages/reactotron-app/main.development.js
new file mode 100644
index 000000000..ff5ee5b6f
--- /dev/null
+++ b/packages/reactotron-app/main.development.js
@@ -0,0 +1,121 @@
+import { app, BrowserWindow, Menu, shell } from 'electron'
+
+let menu
+let template
+let mainWindow = null
+
+if (process.env.NODE_ENV === 'development') {
+ require('electron-debug')()
+}
+
+app.on('window-all-closed', () => {
+ if (process.platform !== 'darwin') app.quit()
+})
+
+app.on('ready', () => {
+ mainWindow = new BrowserWindow({
+ show: false,
+ width: 650,
+ height: 800,
+ useContentHeight: true,
+ titleBarStyle: 'hidden'
+ })
+
+ // toggle the menu on top/bottom
+ function toggleAlwaysOnTop () {
+ const nextValue = !mainWindow.isAlwaysOnTop()
+ mainWindow.setAlwaysOnTop(nextValue)
+ // fragile way of getting the Always On Top
+ const menuItem = menu.items[3].submenu.items[0]
+ menuItem.checked = nextValue
+ }
+
+ mainWindow.loadURL(`file://${__dirname}/App/App.html`)
+
+ mainWindow.webContents.on('did-finish-load', () => {
+ mainWindow.show()
+ mainWindow.focus()
+ })
+
+ mainWindow.on('closed', () => {
+ mainWindow = null
+ })
+
+ if (process.platform === 'darwin') {
+ template = [{
+ label: 'Reactotron',
+ submenu: [
+ { label: 'About Reactotron', selector: 'orderFrontStandardAboutPanel:' },
+ { type: 'separator' },
+ { label: 'Services', submenu: [] },
+ { type: 'separator' },
+ { label: 'Hide Reactotron', accelerator: 'Command+H', selector: 'hide:' },
+ { label: 'Hide Others', accelerator: 'Command+Shift+H', selector: 'hideOtherApplications:' },
+ { label: 'Show All', selector: 'unhideAllApplications:' },
+ { type: 'separator' },
+ { label: 'Quit', accelerator: 'Command+Q', click () { app.quit() } }
+ ]
+ }, {
+ label: 'Edit',
+ submenu: [
+ { label: 'Undo', accelerator: 'Command+Z', selector: 'undo:' },
+ { label: 'Redo', accelerator: 'Shift+Command+Z', selector: 'redo:' },
+ { type: 'separator' },
+ { label: 'Cut', accelerator: 'Command+X', selector: 'cut:' },
+ { label: 'Copy', accelerator: 'Command+C', selector: 'copy:' },
+ { label: 'Paste', accelerator: 'Command+V', selector: 'paste:' },
+ { label: 'Select All', accelerator: 'Command+A', selector: 'selectAll:' }
+ ]
+ }, {
+ label: 'View',
+ submenu: (process.env.NODE_ENV === 'development') ? [
+ { label: 'Reload', accelerator: 'Command+R', click () { mainWindow.webContents.reload() } },
+ { label: 'Toggle Developer Tools', accelerator: 'Alt+Command+I', click () { mainWindow.toggleDevTools() } }
+ ] : [
+ { label: 'Toggle Full Screen', accelerator: 'Ctrl+Command+F', click () { mainWindow.setFullScreen(!mainWindow.isFullScreen()) } }
+ ]
+ }, {
+ label: 'Window',
+ submenu: [
+ { type: 'checkbox', label: 'Always On Top', click () { toggleAlwaysOnTop() } },
+ { type: 'separator' },
+ { label: 'Minimize', accelerator: 'Command+M', selector: 'performMiniaturize:' },
+ { label: 'Close', accelerator: 'Command+W', selector: 'performClose:' },
+ { type: 'separator' },
+ { label: 'Bring All to Front', selector: 'arrangeInFront:' }
+ ]
+ }, {
+ label: 'Help',
+ submenu: [
+ { label: 'Visit on Github', click () { shell.openExternal('https://github.com/reactotron/reactotron') } }
+ ]
+ }]
+
+ menu = Menu.buildFromTemplate(template)
+ Menu.setApplicationMenu(menu)
+ } else {
+ template = [{
+ label: '&File',
+ submenu: [
+ { label: '&Open', accelerator: 'Ctrl+O' },
+ { label: '&Close', accelerator: 'Ctrl+W', click () { mainWindow.close() } }
+ ]
+ }, {
+ label: '&View',
+ submenu: (process.env.NODE_ENV === 'development') ? [
+ { label: '&Reload', accelerator: 'Ctrl+R', click () { mainWindow.webContents.reload() } },
+ { label: 'Toggle &Developer Tools', accelerator: 'Alt+Ctrl+I', click () { mainWindow.toggleDevTools() } }
+ ] : [
+ { type: 'checkbox', label: 'Always On Top', click () { toggleAlwaysOnTop(this) } },
+ { label: 'Toggle &Full Screen', accelerator: 'F11', click () { mainWindow.setFullScreen(!mainWindow.isFullScreen()) } }
+ ]
+ }, {
+ label: 'Help',
+ submenu: [
+ { label: 'Visit on Github', click () { shell.openExternal('https://github.com/reactotron/reactotron') } }
+ ]
+ }]
+ menu = Menu.buildFromTemplate(template)
+ mainWindow.setMenu(menu)
+ }
+})
diff --git a/packages/reactotron-app/package.js b/packages/reactotron-app/package.js
new file mode 100644
index 000000000..a97765695
--- /dev/null
+++ b/packages/reactotron-app/package.js
@@ -0,0 +1,127 @@
+'use strict'
+
+require('babel-polyfill')
+const os = require('os')
+const webpack = require('webpack')
+const electronCfg = require('./webpack.config.electron.js')
+const cfg = require('./webpack.config.production.js')
+const packager = require('electron-packager')
+const del = require('del')
+const exec = require('child_process').exec
+const argv = require('minimist')(process.argv.slice(2))
+const pkg = require('./package.json')
+const deps = Object.keys(pkg.dependencies)
+const devDeps = Object.keys(pkg.devDependencies)
+
+const appName = argv.name || argv.n || pkg.productName
+const shouldUseAsar = argv.asar || argv.a || false
+const shouldBuildAll = argv.all || false
+
+const DEFAULT_OPTS = {
+ dir: './',
+ name: appName,
+ asar: shouldUseAsar,
+ 'app-bundle-id': 'com.reactotron.app',
+ ignore: [
+ '^/test($|/)',
+ '^/release($|/)',
+ '^/main.development.js'
+ ].concat(devDeps.map(name => `/node_modules/${name}($|/)`))
+ .concat(
+ deps.filter(name => !electronCfg.externals.includes(name))
+ .map(name => `/node_modules/${name}($|/)`)
+ )
+}
+
+const icon = argv.icon || argv.i || 'App/App'
+
+if (icon) {
+ DEFAULT_OPTS.icon = icon
+}
+
+const version = argv.version || argv.v
+
+if (version) {
+ DEFAULT_OPTS.version = version
+ startPack()
+} else {
+ // use the same version as the currently-installed electron-prebuilt
+ exec('npm list electron-prebuilt --dev', (err, stdout) => {
+ if (err) {
+ DEFAULT_OPTS.version = '1.3.2'
+ } else {
+ DEFAULT_OPTS.version = stdout.split('electron-prebuilt@')[1].replace(/\s/g, '')
+ }
+
+ startPack()
+ })
+}
+
+function build (cfg) {
+ return new Promise((resolve, reject) => {
+ webpack(cfg, (err, stats) => {
+ if (err) return reject(err)
+ resolve(stats)
+ })
+ })
+}
+
+function startPack () {
+ console.log('start pack...')
+ build(electronCfg)
+ .then(() => build(cfg))
+ .then(() => del('release'))
+ .then(paths => {
+ if (shouldBuildAll) {
+ // build for all platforms
+ const archs = ['ia32', 'x64']
+ const platforms = ['linux', 'win32', 'darwin']
+
+ platforms.forEach(plat => {
+ archs.forEach(arch => {
+ pack(plat, arch, log(plat, arch))
+ })
+ })
+ } else {
+ // build for current platform only
+ pack(os.platform(), os.arch(), log(os.platform(), os.arch()))
+ }
+ })
+ .catch(err => {
+ console.error(err)
+ })
+}
+
+function pack (plat, arch, cb) {
+ // there is no darwin ia32 electron
+ if (plat === 'darwin' && arch === 'ia32') return
+
+ const iconObj = {
+ icon: DEFAULT_OPTS.icon + (() => {
+ let extension = '.png'
+ if (plat === 'darwin') {
+ extension = '.icns'
+ } else if (plat === 'win32') {
+ extension = '.ico'
+ }
+ return extension
+ })()
+ }
+
+ const opts = Object.assign({}, DEFAULT_OPTS, iconObj, {
+ platform: plat,
+ arch,
+ prune: true,
+ 'app-version': pkg.version || DEFAULT_OPTS.version,
+ out: `release/${plat}-${arch}`
+ })
+
+ packager(opts, cb)
+}
+
+function log (plat, arch) {
+ return (err, filepath) => {
+ if (err) return console.error(err)
+ console.log(`${plat}-${arch} finished!`)
+ }
+}
diff --git a/packages/reactotron-app/package.json b/packages/reactotron-app/package.json
new file mode 100644
index 000000000..4d5a70e70
--- /dev/null
+++ b/packages/reactotron-app/package.json
@@ -0,0 +1,104 @@
+{
+ "name": "reactotron-app",
+ "productName": "Reactotron",
+ "version": "0.94.0",
+ "description": "Reactotron desktop mode engage!",
+ "main": "main.js",
+ "scripts": {
+ "test-nope": "cross-env NODE_ENV=test mocha --compilers js:babel-register --recursive --require ./test/setup.js test/**/*.spec.js",
+ "hot-server": "node -r babel-register server.js",
+ "build-main": "cross-env NODE_ENV=production node -r babel-register ./node_modules/webpack/bin/webpack --config webpack.config.electron.js --progress --profile --colors",
+ "build-renderer": "cross-env NODE_ENV=production node -r babel-register ./node_modules/webpack/bin/webpack --config webpack.config.production.js --progress --profile --colors",
+ "build": "npm run build-main && npm run build-renderer",
+ "start-prod": "cross-env NODE_ENV=production electron ./",
+ "start-hot": "cross-env HOT=1 NODE_ENV=development electron -r babel-register ./main.development",
+ "package": "cross-env NODE_ENV=production node -r babel-register package.js",
+ "package-all": "npm run package -- --all",
+ "postinstall-old": "node node_modules/fbjs-scripts/node/check-dev-engines.js package.json",
+ "start": "concurrently --kill-others \"npm run hot-server\" \"npm run start-hot\""
+ },
+ "bin": {
+ "electron": "./node_modules/.bin/electron"
+ },
+ "repository": "https://github.com/reactotron/reactotron/tree/master/packages/reactotron-app",
+ "author": {
+ "name": "Steve Kellock",
+ "email": "steve@kellock.ca",
+ "url": "https://github.com/reactotron"
+ },
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/reactotron/reactotron/issues"
+ },
+ "keywords": [
+ "react",
+ "react native"
+ ],
+ "homepage": "https://github.com/reactotron/reactotron#readme",
+ "devDependencies": {
+ "asar": "^0.12.1",
+ "babel-core": "^6.13.2",
+ "babel-eslint": "^6.1.2",
+ "babel-loader": "^6.2.4",
+ "babel-plugin-add-module-exports": "^0.2.1",
+ "babel-plugin-transform-decorators-legacy": "^1.3.4",
+ "babel-plugin-webpack-loaders": "^0.7.1",
+ "babel-polyfill": "^6.13.0",
+ "babel-preset-es2015": "^6.13.2",
+ "babel-preset-react": "^6.11.0",
+ "babel-preset-react-hmre": "^1.1.1",
+ "babel-preset-stage-0": "^6.5.0",
+ "babel-register": "^6.11.6",
+ "concurrently": "^2.2.0",
+ "cross-env": "^2.0.0",
+ "css-loader": "^0.23.1",
+ "del": "^2.2.1",
+ "electron-packager": "^7.5.1",
+ "electron-prebuilt": "^1.3.2",
+ "electron-rebuild": "^1.2.0",
+ "express": "^4.14.0",
+ "extract-text-webpack-plugin": "^1.0.1",
+ "fbjs-scripts": "^0.7.1",
+ "file-loader": "^0.9.0",
+ "json-loader": "^0.5.4",
+ "minimist": "^1.2.0",
+ "node-libs-browser": "^1.0.0",
+ "standard": "^7.1.2",
+ "style-loader": "^0.13.1",
+ "url-loader": "^0.5.7",
+ "webpack": "^1.13.1",
+ "webpack-dev-middleware": "^1.6.1",
+ "webpack-hot-middleware": "^2.12.2"
+ },
+ "dependencies": {
+ "color": "^0.11.3",
+ "css-modules-require-hook": "^4.0.0",
+ "electron-debug": "^1.0.1",
+ "font-awesome": "^4.6.3",
+ "mobx": "^2.4.2",
+ "mobx-react": "^3.5.4",
+ "moment": "^2.14.1",
+ "postcss": "^5.1.0",
+ "ramda": "^0.21.0",
+ "ramdasauce": "^1.1.0",
+ "react": "^15.3.0",
+ "react-base16-styling": "^0.4.1",
+ "react-dom": "^15.3.0",
+ "react-icons": "^2.2.1",
+ "react-json-tree": "^0.10.0",
+ "react-modal-dialog": "^3.0.2",
+ "react-tap-event-plugin": "^1.0.0",
+ "react-tooltip": "^3.1.5",
+ "reactotron-core-server": "^0.94.0",
+ "socket.io": "1.4.8",
+ "source-map-support": "^0.4.2",
+ "stringify-object": "^2.4.0"
+ },
+ "devEngines": {
+ "node": "6.x",
+ "npm": "3.x"
+ },
+ "standard": {
+ "parser": "babel-eslint"
+ }
+}
diff --git a/packages/reactotron-app/server.js b/packages/reactotron-app/server.js
new file mode 100644
index 000000000..714e4c63b
--- /dev/null
+++ b/packages/reactotron-app/server.js
@@ -0,0 +1,38 @@
+import express from 'express'
+import webpack from 'webpack'
+import webpackDevMiddleware from 'webpack-dev-middleware'
+import webpackHotMiddleware from 'webpack-hot-middleware'
+
+import config from './webpack.config.development'
+
+const app = express()
+const compiler = webpack(config)
+const PORT = 3001
+
+const wdm = webpackDevMiddleware(compiler, {
+ publicPath: config.output.publicPath,
+ stats: {
+ colors: true
+ }
+})
+
+app.use(wdm)
+
+app.use(webpackHotMiddleware(compiler))
+
+const server = app.listen(PORT, 'localhost', err => {
+ if (err) {
+ console.error(err)
+ return
+ }
+
+ console.log(`Listening at http://localhost:${PORT}`)
+})
+
+process.on('SIGTERM', () => {
+ console.log('Stopping dev server')
+ wdm.close()
+ server.close(() => {
+ process.exit(0)
+ })
+})
diff --git a/packages/reactotron-app/sign.sh b/packages/reactotron-app/sign.sh
new file mode 100755
index 000000000..f8e93a6c7
--- /dev/null
+++ b/packages/reactotron-app/sign.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env zsh
+
+# clean up the release
+rm -rf release
+
+# build the app
+npm run build
+npm run package
+
+# let's sign it up!
+codesign -s $REACTOTRON_CODESIGN_IDENTITY -vvv --deep --force ./release/darwin-x64/Reactotron-darwin-x64/Reactotron.app
diff --git a/packages/reactotron-app/webpack.config.base.js b/packages/reactotron-app/webpack.config.base.js
new file mode 100644
index 000000000..cb306773f
--- /dev/null
+++ b/packages/reactotron-app/webpack.config.base.js
@@ -0,0 +1,35 @@
+import path from 'path'
+
+export default {
+ module: {
+ loaders: [{
+ test: /\.jsx?$/,
+ loaders: ['babel-loader'],
+ exclude: /node_modules/
+ }, {
+ test: /\.json$/,
+ loader: 'json-loader'
+ }, {
+ test: /\.png$/,
+ loader: 'url'
+ }]
+ },
+ output: {
+ path: path.join(__dirname, 'dist'),
+ filename: 'bundle.js',
+ libraryTarget: 'commonjs2'
+ },
+ resolve: {
+ extensions: ['', '.js', '.jsx'],
+ packageMains: ['webpack', 'browser', 'web', 'browserify', ['jam', 'main'], 'main']
+ },
+ plugins: [
+
+ ],
+ externals: [
+ // put your node 3rd party libraries which can't be built with webpack here
+ // (mysql, mongodb, and so on..)
+ 'socket.io',
+ 'socket.io-client'
+ ]
+}
diff --git a/packages/reactotron-app/webpack.config.development.js b/packages/reactotron-app/webpack.config.development.js
new file mode 100644
index 000000000..d7f9d8334
--- /dev/null
+++ b/packages/reactotron-app/webpack.config.development.js
@@ -0,0 +1,59 @@
+import webpack from 'webpack'
+import baseConfig from './webpack.config.base'
+
+const config = {
+ ...baseConfig,
+
+ debug: true,
+
+ devtool: 'cheap-module-eval-source-map',
+
+ entry: [
+ 'webpack-hot-middleware/client?path=http://localhost:3001/__webpack_hmr',
+ './App/Index'
+ ],
+
+ output: {
+ ...baseConfig.output,
+ publicPath: 'http://localhost:3001/dist/'
+ },
+
+ module: {
+ ...baseConfig.module,
+ loaders: [
+ ...baseConfig.module.loaders,
+
+ {
+ test: /\.global\.css$/,
+ loaders: [
+ 'style-loader',
+ 'css-loader?sourceMap'
+ ]
+ },
+
+ {
+ test: /^((?!\.global).)*\.css$/,
+ loaders: [
+ 'style-loader',
+ 'css-loader?modules&sourceMap&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]'
+ ]
+ }
+ ]
+ },
+
+ plugins: [
+ ...baseConfig.plugins,
+ new webpack.HotModuleReplacementPlugin(),
+ new webpack.NoErrorsPlugin(),
+ new webpack.DefinePlugin({
+ __DEV__: true,
+ 'process.env': {
+ NODE_ENV: JSON.stringify('development')
+ }
+ })
+ ],
+
+ target: 'electron-renderer'
+}
+
+export default config
diff --git a/packages/reactotron-app/webpack.config.electron.js b/packages/reactotron-app/webpack.config.electron.js
new file mode 100644
index 000000000..44826c326
--- /dev/null
+++ b/packages/reactotron-app/webpack.config.electron.js
@@ -0,0 +1,45 @@
+import webpack from 'webpack'
+import baseConfig from './webpack.config.base'
+
+export default {
+ ...baseConfig,
+
+ devtool: 'source-map',
+
+ entry: './main.development',
+
+ output: {
+ path: __dirname,
+ filename: './main.js'
+ },
+
+ plugins: [
+ new webpack.optimize.UglifyJsPlugin({
+ compressor: {
+ warnings: false
+ }
+ }),
+ new webpack.BannerPlugin(
+ 'require("source-map-support").install()',
+ { raw: true, entryOnly: false }
+ ),
+ new webpack.DefinePlugin({
+ 'process.env': {
+ NODE_ENV: JSON.stringify('production')
+ }
+ })
+ ],
+
+ target: 'electron-main',
+
+ node: {
+ __dirname: false,
+ __filename: false
+ },
+
+ externals: [
+ ...baseConfig.externals,
+ 'font-awesome',
+ 'source-map-support'
+ ]
+}
diff --git a/packages/reactotron-app/webpack.config.node.js b/packages/reactotron-app/webpack.config.node.js
new file mode 100644
index 000000000..e7fec0f9a
--- /dev/null
+++ b/packages/reactotron-app/webpack.config.node.js
@@ -0,0 +1,12 @@
+// for babel-plugin-webpack-loaders
+require('babel-register')
+const devConfigs = require('./webpack.config.development')
+
+module.exports = {
+ output: {
+ libraryTarget: 'commonjs2'
+ },
+ module: {
+ loaders: devConfigs.module.loaders.slice(1) // remove babel-loader
+ }
+}
diff --git a/packages/reactotron-app/webpack.config.production.js b/packages/reactotron-app/webpack.config.production.js
new file mode 100644
index 000000000..80f7393df
--- /dev/null
+++ b/packages/reactotron-app/webpack.config.production.js
@@ -0,0 +1,63 @@
+import webpack from 'webpack'
+import ExtractTextPlugin from 'extract-text-webpack-plugin'
+import baseConfig from './webpack.config.base'
+
+const config = {
+ ...baseConfig,
+
+ devtool: 'source-map',
+
+ entry: './App/Index',
+
+ output: {
+ ...baseConfig.output,
+
+ publicPath: '../dist/'
+ },
+
+ module: {
+ ...baseConfig.module,
+
+ loaders: [
+ ...baseConfig.module.loaders,
+
+ {
+ test: /\.global\.css$/,
+ loader: ExtractTextPlugin.extract(
+ 'style-loader',
+ 'css-loader'
+ )
+ },
+
+ {
+ test: /^((?!\.global).)*\.css$/,
+ loader: ExtractTextPlugin.extract(
+ 'style-loader',
+ 'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]'
+ )
+ }
+ ]
+ },
+
+ plugins: [
+ ...baseConfig.plugins,
+ new webpack.optimize.OccurenceOrderPlugin(),
+ new webpack.DefinePlugin({
+ __DEV__: false,
+ 'process.env': {
+ NODE_ENV: JSON.stringify('production')
+ }
+ }),
+ new webpack.optimize.UglifyJsPlugin({
+ compressor: {
+ screw_ie8: true,
+ warnings: false
+ }
+ }),
+ new ExtractTextPlugin('style.css', { allChunks: true })
+ ],
+
+ target: 'electron-renderer'
+}
+
+export default config
diff --git a/packages/reactotron-cli/.babelrc b/packages/reactotron-cli/.babelrc
new file mode 100644
index 000000000..b4370729b
--- /dev/null
+++ b/packages/reactotron-cli/.babelrc
@@ -0,0 +1,4 @@
+{
+ "presets": [ "es2015", "stage-1" ],
+ "plugins": [ "transform-decorators-legacy" ]
+}
diff --git a/packages/reactotron-cli/.gitignore b/packages/reactotron-cli/.gitignore
new file mode 100644
index 000000000..a78e18fe1
--- /dev/null
+++ b/packages/reactotron-cli/.gitignore
@@ -0,0 +1,5 @@
+node_modules
+npm-debug.log
+coverage
+.nyc_output
+dist
diff --git a/packages/reactotron-cli/package.json b/packages/reactotron-cli/package.json
new file mode 100644
index 000000000..f79461e8e
--- /dev/null
+++ b/packages/reactotron-cli/package.json
@@ -0,0 +1,59 @@
+{
+ "name": "reactotron-cli",
+ "version": "0.94.0",
+ "description": "Reactotron terminal mode engage!",
+ "bin": {
+ "reactotron": "./dist/index.js"
+ },
+ "scripts": {
+ "test": "ava",
+ "watch": "ava --watch",
+ "coverage": "nyc ava",
+ "start": "babel-node src/index.js",
+ "build": "rollup -c"
+ },
+ "repository": "https://github.com/reactotron/reactotron/tree/master/packages/reactotron-cli",
+ "author": "Steve Kellock",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/reactotron/reactotron/issues"
+ },
+ "homepage": "https://reactotron.com",
+ "files": [
+ "dist",
+ "LICENSE",
+ "README.md"
+ ],
+ "devDependencies": {
+ "ava": "^0.16.0",
+ "babel-core": "^6.13.2",
+ "babel-eslint": "^6.1.2",
+ "babel-plugin-transform-decorators-legacy": "^1.3.4",
+ "babel-preset-es2015": "^6.13.2",
+ "babel-preset-es2015-rollup": "^1.2.0",
+ "babel-preset-stage-1": "^6.13.0",
+ "nyc": "^8.1.0",
+ "rollup": "^0.34.8",
+ "rollup-plugin-babel": "^2.6.1",
+ "standard": "^7.1.2"
+ },
+ "ava": {
+ "require": [
+ "babel-core/register"
+ ],
+ "babel": "inherit"
+ },
+ "standard": {
+ "parser": "babel-eslint"
+ },
+ "dependencies": {
+ "mobx": "^2.4.3",
+ "ramda": "^0.22.1",
+ "ramdasauce": "^1.1.0",
+ "strman": "^1.0.1",
+ "blessed": "^0.1.81",
+ "gemoji": "^1.0.0",
+ "moment": "^2.12.0",
+ "reactotron-core-server": "^0.94.0"
+ }
+}
diff --git a/packages/reactotron-cli/rollup.config.js b/packages/reactotron-cli/rollup.config.js
new file mode 100644
index 000000000..a4d9713f0
--- /dev/null
+++ b/packages/reactotron-cli/rollup.config.js
@@ -0,0 +1,13 @@
+import babel from 'rollup-plugin-babel'
+
+export default {
+ entry: 'src/index.js',
+ format: 'cjs',
+ plugins: [
+ babel({
+ babelrc: false,
+ presets: ['es2015-rollup', 'stage-1']
+ })
+ ],
+ dest: 'dist/index.js'
+}
diff --git a/server/commands/commands/commandRepeat.js b/packages/reactotron-cli/src/commands/commands/commandRepeat.js
similarity index 100%
rename from server/commands/commands/commandRepeat.js
rename to packages/reactotron-cli/src/commands/commands/commandRepeat.js
diff --git a/server/commands/content/contentClear.js b/packages/reactotron-cli/src/commands/content/contentClear.js
similarity index 75%
rename from server/commands/content/contentClear.js
rename to packages/reactotron-cli/src/commands/content/contentClear.js
index 606e68aaf..fb80db4f3 100644
--- a/server/commands/content/contentClear.js
+++ b/packages/reactotron-cli/src/commands/content/contentClear.js
@@ -3,8 +3,8 @@ const COMMAND = 'content.clear'
const process = (context, action) => {
context.ui.logBox.setContent('')
context.ui.apiBox.setContent('')
- context.ui.reduxActionBox.setContent('')
- context.ui.reduxWatchBox.setContent('')
+ context.ui.stateActionBox.setContent('')
+ context.ui.stateWatchBox.setContent('')
context.ui.benchBox.setContent('')
context.ui.screen.render()
}
diff --git a/server/commands/content/contentScore.js b/packages/reactotron-cli/src/commands/content/contentScore.js
similarity index 88%
rename from server/commands/content/contentScore.js
rename to packages/reactotron-cli/src/commands/content/contentScore.js
index 2c7e7a689..a1c0b1bb1 100644
--- a/server/commands/content/contentScore.js
+++ b/packages/reactotron-cli/src/commands/content/contentScore.js
@@ -4,7 +4,7 @@ const SCORE = '\n-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-\n'
const process = (context, action) => {
context.ui.logBox.log(SCORE)
context.ui.apiBox.log(SCORE)
- context.ui.reduxActionBox.log(SCORE)
+ context.ui.stateActionBox.log(SCORE)
context.ui.benchBox.log(SCORE)
context.ui.screen.render()
}
diff --git a/server/commands/devMenu/devMenuReload.js b/packages/reactotron-cli/src/commands/devMenu/devMenuReload.js
similarity index 100%
rename from server/commands/devMenu/devMenuReload.js
rename to packages/reactotron-cli/src/commands/devMenu/devMenuReload.js
diff --git a/packages/reactotron-cli/src/commands/index.js b/packages/reactotron-cli/src/commands/index.js
new file mode 100644
index 000000000..8ac0e1b67
--- /dev/null
+++ b/packages/reactotron-cli/src/commands/index.js
@@ -0,0 +1,61 @@
+// state things
+import stateDispatch from './state/stateDispatch'
+import stateValueRequest from './state/stateValueRequest'
+import stateKeyRequest from './state/stateKeyRequest'
+import stateValuePrompt from './state/stateValuePrompt'
+import stateKeyPrompt from './state/stateKeyPrompt'
+import stateDispatchPrompt from './state/stateDispatchPrompt'
+import stateSubscribeAdd from './state/stateSubscribeAdd'
+import stateSubscribeAddPrompt from './state/stateSubscribeAddPrompt'
+import stateSubscribeDelete from './state/stateSubscribeDelete'
+import stateSubscribeDeletePrompt from './state/stateSubscribeDeletePrompt'
+import stateSubscribeClear from './state/stateSubscribeClear'
+import devMenuReload from './devMenu/devMenuReload'
+
+// program things
+import die from './program/die'
+
+// command things
+import commandRepeat from './commands/commandRepeat'
+
+// content things
+import contentClear from './content/contentClear'
+import contentScore from './content/contentScore'
+
+// menu things
+import menuPush from './menus/menuPush'
+import menuPop from './menus/menuPop'
+import menuMain from './menus/menuMain'
+import menuState from './menus/menuState'
+import menuHelp from './menus/menuHelp'
+import menuStateSubscribe from './menus/menuStateSubscribe'
+import menuDevMenu from './menus/menuDevMenu'
+import menuClients from './menus/menuClients'
+
+// come together. right now. over me.
+export default [
+ stateDispatch,
+ stateValueRequest,
+ stateKeyRequest,
+ stateValuePrompt,
+ stateKeyPrompt,
+ stateDispatchPrompt,
+ stateSubscribeAdd,
+ stateSubscribeAddPrompt,
+ stateSubscribeDelete,
+ stateSubscribeDeletePrompt,
+ stateSubscribeClear,
+ contentClear,
+ contentScore,
+ menuPush,
+ menuPop,
+ menuMain,
+ menuState,
+ menuHelp,
+ menuStateSubscribe,
+ menuDevMenu,
+ menuClients,
+ commandRepeat,
+ devMenuReload,
+ die
+]
diff --git a/packages/reactotron-cli/src/commands/menus/menuClients.js b/packages/reactotron-cli/src/commands/menus/menuClients.js
new file mode 100644
index 000000000..d46a6e939
--- /dev/null
+++ b/packages/reactotron-cli/src/commands/menus/menuClients.js
@@ -0,0 +1,32 @@
+import R from 'ramda'
+
+const formatClient = (client = {}, prefix = '-') => {
+ return `${prefix} {green-fg}[${client.address}]{/} <${client.userAgent}> <${client.reactotronVersion}>`
+}
+
+const formatClients = (clients = {}, prefix = '-') => {
+ return R.pipe(
+ R.values,
+ R.map((c) => formatClient(c, prefix)),
+ R.join('\n')
+ )(clients)
+}
+
+const COMMAND = 'menu.clients'
+
+const process = (context, action) => {
+ const clients = formatClients(context.server.connections.slice(), ' ')
+
+ const messageText = `
+ {bold}Clients{/bold}
+ -------------------------------------------------------
+${clients}
+`
+
+ context.ui.info(' {yellow-fg}reactotron{/} {blue-fg}clients{/} ', messageText)
+}
+
+export default {
+ name: COMMAND,
+ process
+}
diff --git a/server/commands/menus/menuDevMenu.js b/packages/reactotron-cli/src/commands/menus/menuDevMenu.js
similarity index 100%
rename from server/commands/menus/menuDevMenu.js
rename to packages/reactotron-cli/src/commands/menus/menuDevMenu.js
diff --git a/server/commands/menus/menuHelp.js b/packages/reactotron-cli/src/commands/menus/menuHelp.js
similarity index 82%
rename from server/commands/menus/menuHelp.js
rename to packages/reactotron-cli/src/commands/menus/menuHelp.js
index 4693a3998..b6e18242a 100644
--- a/server/commands/menus/menuHelp.js
+++ b/packages/reactotron-cli/src/commands/menus/menuHelp.js
@@ -12,7 +12,7 @@ const process = (context, action) => {
`
- context.info(' {yellow-fg}reactotron{/} {blue-fg}help{/} ', messageText)
+ context.ui.info(' {yellow-fg}reactotron{/} {blue-fg}help{/} ', messageText)
}
export default {
diff --git a/server/commands/menus/menuHelpers.js b/packages/reactotron-cli/src/commands/menus/menuHelpers.js
similarity index 100%
rename from server/commands/menus/menuHelpers.js
rename to packages/reactotron-cli/src/commands/menus/menuHelpers.js
diff --git a/server/commands/menus/menuMain.js b/packages/reactotron-cli/src/commands/menus/menuMain.js
similarity index 86%
rename from server/commands/menus/menuMain.js
rename to packages/reactotron-cli/src/commands/menus/menuMain.js
index 6d2e52340..5a0df5598 100644
--- a/server/commands/menus/menuMain.js
+++ b/packages/reactotron-cli/src/commands/menus/menuMain.js
@@ -4,7 +4,7 @@ const process = (context, action) => {
const menu = {
name: 'main',
commands: [
- {key: 'r', name: 'redux', commands: [{type: 'menu.redux'}]},
+ {key: 's', name: 'state', commands: [{type: 'menu.state'}]},
{key: 'c', name: 'clients', commands: [{type: 'menu.clients'}]},
{key: 'h', name: 'help', commands: [{type: 'menu.help'}]},
{key: 'q', name: 'quit', commands: [{type: 'program.die'}]}
diff --git a/server/commands/menus/menuPop.js b/packages/reactotron-cli/src/commands/menus/menuPop.js
similarity index 100%
rename from server/commands/menus/menuPop.js
rename to packages/reactotron-cli/src/commands/menus/menuPop.js
diff --git a/server/commands/menus/menuPush.js b/packages/reactotron-cli/src/commands/menus/menuPush.js
similarity index 100%
rename from server/commands/menus/menuPush.js
rename to packages/reactotron-cli/src/commands/menus/menuPush.js
diff --git a/server/commands/menus/menuRedux.js b/packages/reactotron-cli/src/commands/menus/menuState.js
similarity index 51%
rename from server/commands/menus/menuRedux.js
rename to packages/reactotron-cli/src/commands/menus/menuState.js
index 1b3fdd998..03336e585 100644
--- a/server/commands/menus/menuRedux.js
+++ b/packages/reactotron-cli/src/commands/menus/menuState.js
@@ -1,13 +1,13 @@
-const COMMAND = 'menu.redux'
+const COMMAND = 'menu.state'
const process = (context, action) => {
const menu = {
- name: 'redux',
+ name: 'state',
commands: [
- {key: 'v', name: 'values', commands: [{type: 'redux.value.prompt'}, {type: 'menu.pop'}]},
- {key: 'k', name: 'keys', commands: [{type: 'redux.key.prompt'}, {type: 'menu.pop'}]},
- {key: 'd', name: 'dispatch', commands: [{type: 'redux.dispatch.prompt'}, {type: 'menu.pop'}]},
- {key: 's', name: 'subscribe', commands: [{type: 'menu.redux.subscribe'}]},
+ {key: 'v', name: 'values', commands: [{type: 'state.value.prompt'}, {type: 'menu.pop'}]},
+ {key: 'k', name: 'keys', commands: [{type: 'state.key.prompt'}, {type: 'menu.pop'}]},
+ {key: 'd', name: 'dispatch', commands: [{type: 'state.dispatch.prompt'}, {type: 'menu.pop'}]},
+ {key: 's', name: 'subscribe', commands: [{type: 'menu.state.subscribe'}]},
{key: 'escape', name: 'back', commands: [{type: 'menu.pop'}]}
]
}
diff --git a/server/commands/menus/menuReduxSubscribe.js b/packages/reactotron-cli/src/commands/menus/menuStateSubscribe.js
similarity index 65%
rename from server/commands/menus/menuReduxSubscribe.js
rename to packages/reactotron-cli/src/commands/menus/menuStateSubscribe.js
index 3b9839608..79555b33a 100644
--- a/server/commands/menus/menuReduxSubscribe.js
+++ b/packages/reactotron-cli/src/commands/menus/menuStateSubscribe.js
@@ -1,12 +1,12 @@
-const COMMAND = 'menu.redux.subscribe'
+const COMMAND = 'menu.state.subscribe'
const process = (context, action) => {
const menu = {
name: 'subscribe',
commands: [
- {key: 'a', name: 'add', commands: [{type: 'redux.subscribe.add.prompt'}, {type: 'menu.pop'}, {type: 'menu.pop'}]},
- {key: 'd', name: 'delete', commands: [{type: 'redux.subscribe.delete.prompt'}, {type: 'menu.pop'}, {type: 'menu.pop'}]},
- {key: 'c', name: 'clear', commands: [{type: 'redux.subscribe.clear'}, {type: 'menu.pop'}, {type: 'menu.pop'}]},
+ {key: 'a', name: 'add', commands: [{type: 'state.subscribe.add.prompt'}, {type: 'menu.pop'}, {type: 'menu.pop'}]},
+ {key: 'd', name: 'delete', commands: [{type: 'state.subscribe.delete.prompt'}, {type: 'menu.pop'}, {type: 'menu.pop'}]},
+ {key: 'c', name: 'clear', commands: [{type: 'state.subscribe.clear'}, {type: 'menu.pop'}, {type: 'menu.pop'}]},
{key: 'escape', name: 'back', commands: [{type: 'menu.pop'}]}
]
}
diff --git a/server/commands/program/die.js b/packages/reactotron-cli/src/commands/program/die.js
similarity index 100%
rename from server/commands/program/die.js
rename to packages/reactotron-cli/src/commands/program/die.js
diff --git a/packages/reactotron-cli/src/commands/state/stateDispatch.js b/packages/reactotron-cli/src/commands/state/stateDispatch.js
new file mode 100644
index 000000000..73201f60b
--- /dev/null
+++ b/packages/reactotron-cli/src/commands/state/stateDispatch.js
@@ -0,0 +1,14 @@
+const COMMAND = 'state.dispatch'
+
+/**
+ Sends a request to dispatch an action into state.
+ */
+const process = (context, { action }) => {
+ context.server.stateActionDispatch(action)
+}
+
+export default {
+ name: COMMAND,
+ repeatable: true,
+ process
+}
diff --git a/server/commands/redux/reduxDispatchPrompt.js b/packages/reactotron-cli/src/commands/state/stateDispatchPrompt.js
similarity index 64%
rename from server/commands/redux/reduxDispatchPrompt.js
rename to packages/reactotron-cli/src/commands/state/stateDispatchPrompt.js
index 3a9fbde53..7ade740fc 100644
--- a/server/commands/redux/reduxDispatchPrompt.js
+++ b/packages/reactotron-cli/src/commands/state/stateDispatchPrompt.js
@@ -1,11 +1,11 @@
import RS from 'ramdasauce'
-const COMMAND = 'redux.dispatch.prompt'
+const COMMAND = 'state.dispatch.prompt'
/**
-Prompts for a path to grab some redux keys from.
+Prompts for a path to grab some state keys from.
*/
const process = (context, action) => {
- context.prompt('Action to dispatch (e.g. {type: \'MY_ACTION\'})', (value) => {
+ context.ui.prompt('Action to dispatch (e.g. {type: \'MY_ACTION\'})', (value) => {
let action = null
// try not to blow up the frame
@@ -18,7 +18,7 @@ const process = (context, action) => {
if (RS.isNilOrEmpty(action)) return
// got an object? ship an object.
- context.post({type: 'redux.dispatch', action})
+ context.post({type: 'state.dispatch', action})
})
}
diff --git a/packages/reactotron-cli/src/commands/state/stateKeyPrompt.js b/packages/reactotron-cli/src/commands/state/stateKeyPrompt.js
new file mode 100644
index 000000000..80a462dbd
--- /dev/null
+++ b/packages/reactotron-cli/src/commands/state/stateKeyPrompt.js
@@ -0,0 +1,18 @@
+import RS from 'ramdasauce'
+const COMMAND = 'state.key.prompt'
+
+/**
+ Prompts for a path to grab some state keys from.
+ */
+const process = (context, action) => {
+ context.ui.prompt('Enter a state path: eg. weather.temperature', (value) => {
+ const path = RS.isNilOrEmpty(value) ? null : value
+ context.post({type: 'state.key.request', path})
+ })
+}
+
+export default {
+ name: COMMAND,
+ repeatable: true,
+ process
+}
diff --git a/packages/reactotron-cli/src/commands/state/stateKeyRequest.js b/packages/reactotron-cli/src/commands/state/stateKeyRequest.js
new file mode 100644
index 000000000..eb350b6a7
--- /dev/null
+++ b/packages/reactotron-cli/src/commands/state/stateKeyRequest.js
@@ -0,0 +1,14 @@
+const COMMAND = 'state.key.request'
+
+/**
+ Sends a request to get the keys at the path in state.
+ */
+const process = (context, action) => {
+ context.server.stateKeysRequest(action.path)
+}
+
+export default {
+ name: COMMAND,
+ repeatable: true,
+ process
+}
diff --git a/packages/reactotron-cli/src/commands/state/stateSubscribeAdd.js b/packages/reactotron-cli/src/commands/state/stateSubscribeAdd.js
new file mode 100644
index 000000000..143bafa9c
--- /dev/null
+++ b/packages/reactotron-cli/src/commands/state/stateSubscribeAdd.js
@@ -0,0 +1,13 @@
+const COMMAND = 'state.subscribe.add'
+
+/**
+ * Subscribes to a path in the client state.
+ */
+const process = (context, { path }) => {
+ context.server.stateValuesSubscribe(path)
+}
+
+export default {
+ name: COMMAND,
+ process
+}
diff --git a/packages/reactotron-cli/src/commands/state/stateSubscribeAddPrompt.js b/packages/reactotron-cli/src/commands/state/stateSubscribeAddPrompt.js
new file mode 100644
index 000000000..bb9c6fa19
--- /dev/null
+++ b/packages/reactotron-cli/src/commands/state/stateSubscribeAddPrompt.js
@@ -0,0 +1,18 @@
+import RS from 'ramdasauce'
+const COMMAND = 'state.subscribe.add.prompt'
+
+/**
+ Prompts for a path to grab some state keys from.
+ */
+const process = (context, action) => {
+ context.ui.prompt('Enter a state path: eg. weather.temperature', (value) => {
+ // logical default
+ const path = RS.isNilOrEmpty(value) ? null : value
+ context.post({type: 'state.subscribe.add', path: path})
+ })
+}
+
+export default {
+ name: COMMAND,
+ process
+}
diff --git a/server/commands/redux/reduxSubscribeClear.js b/packages/reactotron-cli/src/commands/state/stateSubscribeClear.js
similarity index 52%
rename from server/commands/redux/reduxSubscribeClear.js
rename to packages/reactotron-cli/src/commands/state/stateSubscribeClear.js
index 74b23e4c1..13fa935b7 100644
--- a/server/commands/redux/reduxSubscribeClear.js
+++ b/packages/reactotron-cli/src/commands/state/stateSubscribeClear.js
@@ -1,11 +1,10 @@
-const COMMAND = 'redux.subscribe.clear'
+const COMMAND = 'state.subscribe.clear'
/**
Clears the subscriptions being watched.
*/
const process = (context, action) => {
- context.config.subscriptions = []
- context.post({type: 'redux.subscribe.request'})
+ context.server.stateValuesClearSubscriptions()
}
export default {
diff --git a/packages/reactotron-cli/src/commands/state/stateSubscribeDelete.js b/packages/reactotron-cli/src/commands/state/stateSubscribeDelete.js
new file mode 100644
index 000000000..b2e546006
--- /dev/null
+++ b/packages/reactotron-cli/src/commands/state/stateSubscribeDelete.js
@@ -0,0 +1,13 @@
+const COMMAND = 'state.subscribe.delete'
+
+/**
+ * Removes a subscription.
+ */
+const process = (context, { path }) => {
+ context.server.stateValuesUnsubscribe(path)
+}
+
+export default {
+ name: COMMAND,
+ process
+}
diff --git a/packages/reactotron-cli/src/commands/state/stateSubscribeDeletePrompt.js b/packages/reactotron-cli/src/commands/state/stateSubscribeDeletePrompt.js
new file mode 100644
index 000000000..691695c2b
--- /dev/null
+++ b/packages/reactotron-cli/src/commands/state/stateSubscribeDeletePrompt.js
@@ -0,0 +1,18 @@
+import RS from 'ramdasauce'
+const COMMAND = 'state.subscribe.delete.prompt'
+
+/**
+ Prompts for a path to grab some state keys from.
+ */
+const process = (context, action) => {
+ context.ui.prompt('Enter a state path: eg. weather.temperature', (value) => {
+ // logical default
+ const path = RS.isNilOrEmpty(value) ? null : value
+ context.post({type: 'state.subscribe.delete', path: path})
+ })
+}
+
+export default {
+ name: COMMAND,
+ process
+}
diff --git a/server/commands/redux/reduxValuePrompt.js b/packages/reactotron-cli/src/commands/state/stateValuePrompt.js
similarity index 50%
rename from server/commands/redux/reduxValuePrompt.js
rename to packages/reactotron-cli/src/commands/state/stateValuePrompt.js
index 1a29ed703..0a7e5af33 100644
--- a/server/commands/redux/reduxValuePrompt.js
+++ b/packages/reactotron-cli/src/commands/state/stateValuePrompt.js
@@ -1,13 +1,13 @@
import RS from 'ramdasauce'
-const COMMAND = 'redux.value.prompt'
+const COMMAND = 'state.value.prompt'
/**
- Prompts for a path to grab some redux values from.
+ Prompts for a path to grab some state values from.
*/
const process = (context, action) => {
- context.prompt('Enter a redux path', (value) => {
+ context.ui.prompt('Enter a state path', (value) => {
const path = RS.isNilOrEmpty(value) ? null : value
- context.post({type: 'redux.value.request', path})
+ context.post({type: 'state.value.request', path})
})
}
diff --git a/packages/reactotron-cli/src/commands/state/stateValueRequest.js b/packages/reactotron-cli/src/commands/state/stateValueRequest.js
new file mode 100644
index 000000000..c256939c0
--- /dev/null
+++ b/packages/reactotron-cli/src/commands/state/stateValueRequest.js
@@ -0,0 +1,14 @@
+const COMMAND = 'state.value.request'
+
+/**
+ Sends a request to get the values at the path in state.
+ */
+const process = (context, action) => {
+ context.server.stateValuesRequest(action.path)
+}
+
+export default {
+ name: COMMAND,
+ repeatable: true,
+ process
+}
diff --git a/packages/reactotron-cli/src/context.js b/packages/reactotron-cli/src/context.js
new file mode 100644
index 000000000..8e2c42a3d
--- /dev/null
+++ b/packages/reactotron-cli/src/context.js
@@ -0,0 +1,34 @@
+import R from 'ramda'
+import Router from './router'
+
+export default class Context {
+
+ constructor (parts) {
+ this.ui = parts.ui
+ this.router = parts.router
+ this.menuStack = []
+ this.clients = {}
+ this.lastRepeatableMessage = null
+ this.stateActionLoggingStyle = 'full'
+ this.apiLoggingStyle = 'short'
+ this.config = {}
+ this.server = parts.server
+ }
+
+ post (message) {
+ // sanity
+ if (R.isNil(message) || !Router.isValidMessage(message)) return false
+ // send each command the message
+ const command = this.router.commands[message.type]
+ if (command) {
+ // kick off the command
+ command.process(this, message)
+
+ // unless this is a command to repeat, then record the command
+ if (command.repeatable) {
+ this.lastRepeatableMessage = message
+ }
+ }
+ }
+
+}
diff --git a/packages/reactotron-cli/src/index.js b/packages/reactotron-cli/src/index.js
new file mode 100644
index 000000000..fb4570687
--- /dev/null
+++ b/packages/reactotron-cli/src/index.js
@@ -0,0 +1,43 @@
+import R from 'ramda'
+import { createServer } from 'reactotron-core-server'
+import Context from './context'
+import Router from './router'
+import commands from './commands/index'
+import ui from './ui/index'
+import reactions from './ui/reactions'
+
+const PORT = 9090
+const server = createServer({
+ port: PORT,
+ onCommand: command => {
+ context.post(command)
+ }
+})
+
+const router = Router.createRouter()
+R.forEach((command) => router.register(command), commands)
+const context = new Context({ ui, router, server })
+
+// some parts of the ui can react to mobx changes. they go in here. so awesome.
+// i'd love to move more in here.
+reactions(context)
+
+// always control-c to die
+ui.screen.key('C-c', () => context.post({type: 'program.die'}))
+
+// . to replay
+ui.screen.key('.', () => context.post({type: 'command.repeat'}))
+
+// - to score
+ui.screen.key('-', () => context.post({type: 'content.score'}))
+
+// del to clear
+ui.screen.key(['delete', 'backspace'], () => context.post({type: 'content.clear'}))
+
+// let's start with the main menu
+context.post({type: 'menu.main'})
+
+// initial render
+ui.screen.render()
+
+server.start()
diff --git a/server/router.js b/packages/reactotron-cli/src/router.js
similarity index 100%
rename from server/router.js
rename to packages/reactotron-cli/src/router.js
diff --git a/packages/reactotron-cli/src/ui/draw-api-response.js b/packages/reactotron-cli/src/ui/draw-api-response.js
new file mode 100644
index 000000000..67b236d9d
--- /dev/null
+++ b/packages/reactotron-cli/src/ui/draw-api-response.js
@@ -0,0 +1,36 @@
+import R from 'ramda'
+import RS from 'ramdasauce'
+import { timeStamp } from './formatting'
+
+/**
+ * Draws the API response.
+ */
+export default (layout, config) => payload => {
+ const time = timeStamp()
+ const status = RS.dotPath('response.status', payload)
+ // not even a status!
+ if (!R.is(Number, status)) {
+ layout.apiBox.log(`${time} {red-fg}${status}{/}`)
+ return
+ }
+
+ const url = RS.dotPath('request.url', payload)
+ const rawMethod = RS.dotPath('request.method')(payload) || '???'
+ const method = R.pipe(R.take(3), R.toUpper)(rawMethod)
+ const statusMessage = R.cond([
+ [RS.isWithin(200, 299), R.always(`{green-fg}${status}{/}`)],
+ [RS.isWithin(400, 599), R.always(`{red-fg}${status}{/}`)],
+ [R.T, R.identity]
+ ])(status)
+ const durationMs = RS.dotPath('duration', payload)
+ const duration = `{white-fg}${durationMs}{/}ms`
+
+ layout.apiBox.log(`${time} ${statusMessage} {blue-fg}${method}{/} ${url}{|}${duration}`)
+ if (config.apiLoggingStyle === 'full') {
+ const data = RS.dotPath('response.body', payload)
+ if (R.is(Object, data)) {
+ const json = JSON.stringify(data, null, 2)
+ layout.apiBox.log(json)
+ }
+ }
+}
diff --git a/server/commands/bench/benchReport.js b/packages/reactotron-cli/src/ui/draw-benchmark-report.js
similarity index 60%
rename from server/commands/bench/benchReport.js
rename to packages/reactotron-cli/src/ui/draw-benchmark-report.js
index 670eb67f5..7f1a31781 100644
--- a/server/commands/bench/benchReport.js
+++ b/packages/reactotron-cli/src/ui/draw-benchmark-report.js
@@ -1,27 +1,26 @@
import R from 'ramda'
-import {leftPad} from 'strman'
+import { leftPad } from 'strman'
+import { timeStamp } from './formatting'
-const COMMAND = 'bench.report'
-
-const drawStep = (step) => {
+const drawStep = step => {
const elapsed = leftPad(step.time.toFixed(0), 7, ' ')
const delta = leftPad('+' + step.delta.toFixed(0), 7, ' ')
return ` ${step.title}{|}{white-fg}${elapsed}{/}ms {yellow-fg}${delta}{/}ms`
}
-const process = (context, action) => {
- const timeStamp = context.timeStamp()
- const {title, steps} = action.message
+export default layout => payload => {
+ const time = timeStamp()
+ const { title, steps } = payload
const first = R.head(steps)
if (steps.length === 2) {
const time = R.last(steps).time - R.head(steps).time
const timeString = time.toFixed(0)
- context.ui.benchBox.log(`${timeStamp} {blue-fg}${title}{/}{|}{yellow-fg}${timeString}{/}ms`)
- context.ui.screen.render()
+ layout.benchBox.log(`${time} {blue-fg}${title}{/}{|}{yellow-fg}${timeString}{/}ms`)
+ layout.screen.render()
return
}
- context.ui.benchBox.log(`${timeStamp} {blue-fg}${title}{/}`)
+ layout.benchBox.log(`${time} {blue-fg}${title}{/}`)
// transform the data
let i = 0
@@ -36,13 +35,8 @@ const process = (context, action) => {
}, steps)
const lines = R.map(drawStep, data)
- R.forEach((line) => context.ui.benchBox.log(line), lines)
+ R.forEach((line) => layout.benchBox.log(line), lines)
// repaint
- context.ui.screen.render()
-}
-export default {
- name: COMMAND,
- process
+ layout.screen.render()
}
-
diff --git a/server/commands/redux/reduxKeyResponse.js b/packages/reactotron-cli/src/ui/draw-state-keys-response.js
similarity index 61%
rename from server/commands/redux/reduxKeyResponse.js
rename to packages/reactotron-cli/src/ui/draw-state-keys-response.js
index 1279b74dd..5a21cae83 100644
--- a/server/commands/redux/reduxKeyResponse.js
+++ b/packages/reactotron-cli/src/ui/draw-state-keys-response.js
@@ -1,14 +1,13 @@
-import RS from 'ramdasauce'
import R from 'ramda'
-
-const COMMAND = 'redux.key.response'
+import RS from 'ramdasauce'
+import { timeStamp } from './formatting'
/**
- Receives a list of keys from the server.
+ Draws state.keys.response commands.
*/
-const process = (context, action) => {
- const {path, keys} = action.message
- const time = context.timeStamp()
+export default (layout) => payload => {
+ const {path, keys} = payload
+ const time = timeStamp()
const keyPrefix = RS.isNilOrEmpty(path) ? '' : `${path}.`
@@ -22,11 +21,6 @@ const process = (context, action) => {
const fullMessage = `{white-fg}${time}{/} ${title} \n${sayKeys}`
- context.ui.logBox.log(fullMessage)
- context.ui.screen.render()
-}
-
-export default {
- name: COMMAND,
- process
+ layout.logBox.log(fullMessage)
+ layout.screen.render()
}
diff --git a/packages/reactotron-cli/src/ui/draw-state-values-change.js b/packages/reactotron-cli/src/ui/draw-state-values-change.js
new file mode 100644
index 000000000..0c01bc112
--- /dev/null
+++ b/packages/reactotron-cli/src/ui/draw-state-values-change.js
@@ -0,0 +1,12 @@
+import R from 'ramda'
+
+/**
+ Draws state.keys.response commands.
+ */
+export default (layout) => payload => {
+ const { changes } = payload
+ const each = R.map(change => `{cyan-fg}${change.path}{/}{|}{white-fg}${change.value}{/}`, changes)
+ const message = R.join('\n', each)
+ layout.stateWatchBox.setContent(message)
+ layout.screen.render()
+}
diff --git a/packages/reactotron-cli/src/ui/draw-state-values-response.js b/packages/reactotron-cli/src/ui/draw-state-values-response.js
new file mode 100644
index 000000000..8ee4a9cf5
--- /dev/null
+++ b/packages/reactotron-cli/src/ui/draw-state-values-response.js
@@ -0,0 +1,18 @@
+import RS from 'ramdasauce'
+import { timeStamp } from './formatting'
+
+/**
+ Draws state.values.response commands.
+ */
+export default (layout) => payload => {
+ const {path, value} = payload
+ const time = timeStamp()
+ if (RS.isNilOrEmpty(path)) {
+ layout.logBox.log(`{white-fg}${time}{/} {blue-fg}values in{/} {cyan-fg}/{/}`)
+ } else {
+ layout.logBox.log(`{white-fg}${time}{/} {blue-fg}values in{/} {cyan-fg}${path}{/}`)
+ }
+ layout.logBox.log(value)
+ layout.logBox.log('')
+ layout.screen.render()
+}
diff --git a/packages/reactotron-cli/src/ui/drawing.js b/packages/reactotron-cli/src/ui/drawing.js
new file mode 100644
index 000000000..b7d79ed66
--- /dev/null
+++ b/packages/reactotron-cli/src/ui/drawing.js
@@ -0,0 +1,93 @@
+import { addSpaceForEmoji } from './emoji'
+import R from 'ramda'
+import RS from 'ramdasauce'
+import { clientCount, timeStamp, formatClient } from './formatting'
+import createDrawApiResponse from './draw-api-response'
+import createDrawBenchmarkReport from './draw-benchmark-report'
+import createDrawStateKeysResponse from './draw-state-keys-response'
+import createDrawStateValuesResponse from './draw-state-values-response'
+import createDrawStateValuesChange from './draw-state-values-change'
+
+export default (layout, config) => {
+ const drawPort = port => {
+ layout.brandingBox.setContent(`{yellow-fg}Reactotron{/} port ${port || '?'}`)
+ layout.screen.render()
+ }
+
+ // draw the client count in the appropriate box
+ const drawClientCount = count => {
+ layout.connectionBox.setContent(clientCount(count))
+ layout.screen.render()
+ }
+
+ const drawConnection = connection => {
+ log(formatClient(connection, 'Connected'))
+ layout.screen.render()
+ }
+
+ const drawDisconnection = connection => {
+ log(formatClient(connection, 'Disconnected'))
+ layout.screen.render()
+ }
+
+ const log = (message, level = 'debug') => {
+ const time = timeStamp()
+ if (R.is(Object, message)) {
+ layout.logBox.log(time)
+ layout.logBox.log(message)
+ layout.logBox.log('')
+ } else {
+ if (!RS.isNilOrEmpty(message)) {
+ message = addSpaceForEmoji(message)
+ }
+ switch (level) {
+ case 'debug':
+ layout.logBox.log(`${time} 💡 ${message}`)
+ break
+
+ case 'warn':
+ layout.logBox.log(`${time} {yellow-fg}⚠️ ${message}{/}`)
+ break
+
+ case 'error':
+ layout.logBox.log(`${time} {red-fg}🚨 ${message}{/}`)
+ break
+ }
+ }
+
+ layout.screen.render()
+ }
+
+ const drawApiResponse = createDrawApiResponse(layout, config)
+ const drawBenchmarkReport = createDrawBenchmarkReport(layout)
+ const drawStateKeysResponse = createDrawStateKeysResponse(layout, log)
+ const drawStateValuesResponse = createDrawStateValuesResponse(layout, log)
+ const drawStateValuesChange = createDrawStateValuesChange(layout)
+
+ const drawStateActionComplete = payload => {
+ const {name, ms, action} = payload
+ const msText = ms && Number(ms).toFixed(0)
+ const msSuffix = ms && 'ms'
+ const time = timeStamp()
+ layout.stateActionBox.log(`${time} {cyan-fg}${name}{/}{|}{white-fg}${msText}{/}${msSuffix}`)
+ if (config.stateActionLoggingStyle === 'full') {
+ layout.stateActionBox.log(action)
+ layout.stateActionBox.log('')
+ }
+ }
+
+ return {
+ drawClientCount,
+ drawPort,
+ timeStamp,
+ drawConnection,
+ drawDisconnection,
+ drawApiResponse,
+ drawBenchmarkReport,
+ drawStateActionComplete,
+ drawStateKeysResponse,
+ drawStateValuesResponse,
+ drawStateValuesChange,
+ log
+ }
+}
diff --git a/packages/reactotron-cli/src/ui/emoji.js b/packages/reactotron-cli/src/ui/emoji.js
new file mode 100644
index 000000000..ee65eb90c
--- /dev/null
+++ b/packages/reactotron-cli/src/ui/emoji.js
@@ -0,0 +1,11 @@
+import gemoji from 'gemoji'
+import R from 'ramda'
+
+// A way to add extra spacing for emoji characters. As it
+// turns out, the emojis are double-wide code points, but
+// the terminal renders it as a single slot. I literally
+// understand nothing anymore. Seems to work great tho!
+const keys = R.keys(gemoji.unicode)
+const emojiPattern = '(' + keys.join('|') + ')+'
+const emojiRegex = new RegExp(emojiPattern, 'g')
+export const addSpaceForEmoji = (str) => str.replace(emojiRegex, '$1 ')
diff --git a/packages/reactotron-cli/src/ui/formatting.js b/packages/reactotron-cli/src/ui/formatting.js
new file mode 100644
index 000000000..b2f77b370
--- /dev/null
+++ b/packages/reactotron-cli/src/ui/formatting.js
@@ -0,0 +1,18 @@
+import moment from 'moment'
+
+export const clientCount = (numberOfClients = 0) => {
+ if (numberOfClients > 0) {
+ return `{right}{black-bg}{green-fg}${numberOfClients} Online{/}{/}{/}`
+ } else {
+ return `{right}{black-bg}{red-fg}${numberOfClients} Online{/}{/}{/}`
+ }
+}
+
+export const timeStamp = () => {
+ const t = moment()
+ return `${t.format('HH:mm:')}{grey-fg}${t.format('ss.SS')}{/}`
+}
+
+export const formatClient = (connection = {}, prefix = '-') => {
+ return `${prefix} {green-fg}[${connection.address}]{/} <${connection.userAgent}> <${connection.reactotronVersion}>`
+}
diff --git a/packages/reactotron-cli/src/ui/index.js b/packages/reactotron-cli/src/ui/index.js
new file mode 100644
index 000000000..3102a4529
--- /dev/null
+++ b/packages/reactotron-cli/src/ui/index.js
@@ -0,0 +1,22 @@
+import createLayout from './layout'
+import createDrawing from './drawing'
+import createInteractions from './interactions'
+
+// little boxes on the hillside, little boxes made of ticky tacky
+const layout = createLayout()
+
+// helper drawer
+const drawing = createDrawing(layout, {
+ apiLoggingStyle: 'short', // short | full
+ stateActionLoggingStyle: 'short' // short | full
+})
+const interactions = createInteractions(layout, drawing)
+
+// glue them together like a superband and hope they don't clobber each other
+const ui = {
+ ...layout,
+ ...drawing,
+ ...interactions
+}
+
+export default ui
diff --git a/packages/reactotron-cli/src/ui/interactions.js b/packages/reactotron-cli/src/ui/interactions.js
new file mode 100644
index 000000000..35be65cb2
--- /dev/null
+++ b/packages/reactotron-cli/src/ui/interactions.js
@@ -0,0 +1,36 @@
+export default (layout, drawing) => ({
+
+ prompt (title, callback) {
+ layout.promptBox.setFront()
+ layout.screen.render()
+ layout.promptBox.input(title, '', (err, value) => {
+ if (!err) {
+ callback(value)
+ layout.screen.render()
+ }
+ })
+ },
+
+ message (displayText, callback) {
+ layout.messageBox.setFront()
+ layout.screen.render()
+ layout.messageBox.display(displayText, 0, (err, value) => {
+ if (!err) {
+ if (callback) callback(value)
+ layout.screen.render()
+ }
+ })
+ },
+
+ info (title, displayText, callback) {
+ layout.infoBox.setFront()
+ layout.screen.render()
+ layout.infoBox.setLabel(title)
+ layout.infoBox.display(displayText, 0, (err, value) => {
+ if (!err) {
+ if (callback) callback(value)
+ layout.screen.render()
+ }
+ })
+ }
+})
diff --git a/server/ui.js b/packages/reactotron-cli/src/ui/layout.js
similarity index 82%
rename from server/ui.js
rename to packages/reactotron-cli/src/ui/layout.js
index 91a8a1044..ed05abcb2 100644
--- a/server/ui.js
+++ b/packages/reactotron-cli/src/ui/layout.js
@@ -41,7 +41,6 @@ const infoBox = blessed.message({
left: 'center',
height: 'shrink',
width: '40%',
- // width: 'shrink',
border: 'line',
label: ' {blue-fg}Info{/} ',
tags: true,
@@ -76,7 +75,7 @@ const logBox = blessed.log({
}
})
-const reduxContainer = blessed.box({
+const stateContainer = blessed.box({
parent: screen,
left: 'center',
width: '33%',
@@ -84,8 +83,8 @@ const reduxContainer = blessed.box({
height: '100%-1'
})
-const reduxActionBox = blessed.log({
- parent: reduxContainer,
+const stateActionBox = blessed.log({
+ parent: stateContainer,
scrollable: true,
left: '0',
top: 0,
@@ -96,15 +95,15 @@ const reduxActionBox = blessed.log({
keys: true,
vi: true,
mouse: true,
- label: ' {white-fg}Redux Actions{/} ',
+ label: ' {white-fg}State Actions{/} ',
scrollbar: {
ch: ' ',
inverse: true
}
})
-const reduxWatchBox = blessed.log({
- parent: reduxContainer,
+const stateWatchBox = blessed.log({
+ parent: stateContainer,
scrollable: true,
left: 0,
width: '100%',
@@ -115,7 +114,7 @@ const reduxWatchBox = blessed.log({
keys: false,
vi: false,
mouse: true,
- label: ' {white-fg}Redux Subscriptions{/}',
+ label: ' {white-fg}State Subscriptions{/}',
scrollbar: {
ch: ' ',
inverse: true
@@ -186,46 +185,38 @@ const instructionsBox = blessed.box({
tags: true
})
-blessed.box({
+const brandingBox = blessed.box({
parent: statusBox,
width: 'shrink',
height: '100%',
left: 0,
top: 0,
tags: true,
- content: '{yellow-fg}reactotron{/}'
+ content: ''
})
-const clientCount = (numberOfClients = 0) => {
- if (numberOfClients > 0) {
- return `{right}{black-bg}{green-fg}${numberOfClients} Online{/}{/}{/}`
- } else {
- return `{right}{black-bg}{red-fg}${numberOfClients} Online{/}{/}{/}`
- }
-}
-
const connectionBox = blessed.box({
parent: statusBox,
top: 0,
right: 0,
height: '100%',
width: 'shrink',
- content: clientCount(),
+ content: '',
tags: true
})
-export default {
+export default () => ({
screen,
connectionBox,
promptBox,
messageBox,
infoBox,
logBox,
- reduxActionBox,
- reduxWatchBox,
+ stateActionBox,
+ stateWatchBox,
apiBox,
benchBox,
instructionsBox,
statusBox,
- clientCount
-}
+ brandingBox
+})
diff --git a/packages/reactotron-cli/src/ui/reactions.js b/packages/reactotron-cli/src/ui/reactions.js
new file mode 100644
index 000000000..903e5d5a5
--- /dev/null
+++ b/packages/reactotron-cli/src/ui/reactions.js
@@ -0,0 +1,40 @@
+import { reaction, observe } from 'mobx'
+import R from 'ramda'
+
+export default (context) => {
+ const { ui, server } = context
+
+ // when the connection count changes, update the connected total
+ reaction(() => server.connections.length, ui.drawClientCount, true)
+
+ // when port changes
+ reaction(() => server.options.port, ui.drawPort, true)
+
+ // when clients come & go
+ observe(server.connections, ({added, removed}) => {
+ R.forEach(ui.drawDisconnection, removed)
+ })
+
+ // a list of commands & functions to call when we see 'em'
+ const subscriptions = [
+ { command: 'log', handler: ({ message, level }) => ui.log(message, level) },
+ { command: 'api.response', handler: ui.drawApiResponse },
+ { command: 'benchmark.report', handler: ui.drawBenchmarkReport },
+ { command: 'state.action.complete', handler: ui.drawStateActionComplete },
+ { command: 'state.keys.response', handler: ui.drawStateKeysResponse },
+ { command: 'state.values.response', handler: ui.drawStateValuesResponse },
+ { command: 'state.values.change', handler: ui.drawStateValuesChange },
+ { command: 'client.intro', handler: ui.drawConnection }
+ ]
+
+ // how we hook that up
+ const subscribe = ({command, handler}) => {
+ observe(server.commands[command], ({added}) => {
+ const draw = ({payload}) => handler(payload)
+ R.forEach(draw, added)
+ })
+ }
+
+ // let's hook it up now
+ R.forEach(subscribe, subscriptions)
+}
diff --git a/test/placeholder.js b/packages/reactotron-cli/test/placeholder.js
similarity index 100%
rename from test/placeholder.js
rename to packages/reactotron-cli/test/placeholder.js
diff --git a/packages/reactotron-core-client/.babelrc b/packages/reactotron-core-client/.babelrc
new file mode 100644
index 000000000..e66618ff6
--- /dev/null
+++ b/packages/reactotron-core-client/.babelrc
@@ -0,0 +1,3 @@
+{
+ "presets": [ "es2015", "stage-1" ]
+}
diff --git a/packages/reactotron-core-client/.editorconfig b/packages/reactotron-core-client/.editorconfig
new file mode 100644
index 000000000..ea02fa4e5
--- /dev/null
+++ b/packages/reactotron-core-client/.editorconfig
@@ -0,0 +1,7 @@
+root = true
+
+[*]
+end_of_line = lf
+charset = utf-8
+indent_style = space
+tab_width = 2
diff --git a/packages/reactotron-core-client/.gitignore b/packages/reactotron-core-client/.gitignore
new file mode 100644
index 000000000..a78e18fe1
--- /dev/null
+++ b/packages/reactotron-core-client/.gitignore
@@ -0,0 +1,5 @@
+node_modules
+npm-debug.log
+coverage
+.nyc_output
+dist
diff --git a/packages/reactotron-core-client/LICENSE b/packages/reactotron-core-client/LICENSE
new file mode 100644
index 000000000..b36092b50
--- /dev/null
+++ b/packages/reactotron-core-client/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 Steve Kellock
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/reactotron-core-client/README.md b/packages/reactotron-core-client/README.md
new file mode 100644
index 000000000..0eaa43fc5
--- /dev/null
+++ b/packages/reactotron-core-client/README.md
@@ -0,0 +1,518 @@
+# reactotron-core-client
+
+This provides the core functionality of the clients allowing it talk to talk to the server.
+
+It is used by `reactotron-react-dom` and `reactotron-react-native`.
+
+# Usage
+
+```js
+import { createClient } from 'reactotron-core-client'
+import io from 'socket.io-client'
+
+// setup a reactotron client
+const client = createClient({
+ io,
+
+ host: 'localhost',
+ port: 9090,
+ name: 'I am a client!',
+
+ // fires when we get connected to a server
+ onConnect: () => console.log('hi'),
+
+ // fires when we get disconnected from the server
+ onDisconnect: () => console.log('bye'),
+
+ // fires when the server is telling us something
+ onCommand: ({type, payload}) => {
+ switch (type) {
+ case 'server.intro':
+ const { name, version } = payload
+ break
+
+ case 'state.values.request':
+ const { path } = payload // the path to the state
+ break
+
+ case 'state.keys.request':
+ const { path } = payload // the path to the state
+ break
+
+ case 'state.values.subscribe':
+ const { paths } = payload // string array of state paths
+ break
+
+ case 'state.action.dispatch':
+ const { action } = payload // an object to be dispatch through the state plugin
+ break
+
+ }
+ console.log(`I just received a ${type} command`)
+ console.log(payload)
+ }
+})
+
+// connect to the server
+client.connect()
+
+// send a log message as a string
+client.send('log', { level: 'debug', message: 'hello!' })
+
+// send a log message as a string that's important
+client.send('log', { level: 'debug', message: 'hello!' }, true)
+
+// sending an object log message
+client.send('log', {
+ level: 'debug',
+ message: {
+ nested: [1,2, {hello: 'there'}],
+ fun: true
+ }
+})
+
+// send a warning
+client.send('log', { level: 'warn', message: 'oops' })
+
+// send an error with an optional stack trace
+client.send('log', {
+ level: 'error',
+ message: 'crap',
+ stackTrace: []
+})
+
+// report that an action is complete
+client.send('state.action.complete', {
+ 'name': 'LOGIN_REQUEST',
+ 'action': {
+ 'type': 'LOGIN_REQUEST',
+ 'email': 'steve@kellock.ca',
+ 'password': 'secret...shhh....'
+ }
+})
+
+// report that values have changed
+client.send('state.values.response', {
+ path: 'user.givenName',
+ value: 'Steve',
+ valid: true
+})
+
+// list the keys at a given path in state
+client.send('state.keys.response', {
+ path: 'user',
+ keys: ['givenName', 'familyName'],
+ valid: true
+})
+
+// let the server know the state values they're subscribed to have changed
+client.send('state.values.change', {
+ changes: [
+ { path: 'user.givenName', value: 'Steve' },
+ {
+ path: 'user',
+ value: { givenName: 'Steve', familyName: 'Kellock' }
+ }
+ ]
+})
+
+// report any API activity
+client.send('api.response', {
+ request: {
+ url: 'https://api.example.com/v1/people',
+ method: 'POST',
+ data: {
+ user: { givenName: 'Steve', familyName: 'Kellock' }
+ },
+ headers: {
+ 'Accept': 'application/json',
+ 'Cookie': '__ispy=mylittleye; __something=blue'
+ }
+ },
+ response: {
+ body: {result: 'ok'},
+ status: 200,
+ headers: {
+ 'Connection': 'keep-alive',
+ 'Server': 'cloudflare-nginx'
+ }
+ },
+ duration: 150.0
+})
+
+// send a benchmark report up to the server
+client.send('bench.report', {
+ title: 'My Fast Algorithmz',
+ steps: [
+ { title: 'Step 1', time: 0 },
+ { title: 'Step 2', time: 123 },
+ { title: 'Step 3', time: 1024 }
+ ]
+})
+
+// a utility to time things
+const elapsed = client.startTimer()
+// do something you want to time
+const ms = elapsed() // the number of ms it took. ish.
+
+// display a custom event
+client.display({
+ name: 'MY EVENT',
+ value: { color: 'green', vegetable: 'spinach', variant: 'baby', salad: true },
+ important: true,
+ preview: 'What\'s in my appetizer?'
+})
+
+
+```
+
+# Why are we passing socket.io down?
+
+It might seem wierd to pass the `io` function in. The problem I'm trying to solve here
+is that React Native and React have really different initialization patterns.
+
+On React Native, we have to patch the `User-Agent`. On React JS, we have to make
+sure the require happens a certain way or we suffer the wrath of WebPack warnings.
+
+I don't want to worry about that at this library, so we pass it down and assume
+the environment is sane.
+
+Not unlike how `window` or `console` might work on their respective platforms.
+
+For the record. I don't like this. But the rest of socket.io is stellar!
+
+# Messages
+
+### client.intro
+
+The client sends this message to the server when it first connects. It contains
+all the configuration information used to configure the client.
+
+For example:
+
+```js
+{
+ "host": "localhost", // the server we're connecting to
+ "port": 9090, // the server's port
+ "name": "My Fantastic App", // the name of our app
+ "userAgent": "Internet Explorer 3.0", // the user agent
+ "reactotronVersion": "0.99.1", // the version of reactotron
+ "environment": "development" // our environment
+}
+```
+
+### server.intro
+
+The client receives this message from the server once connected. It contains
+configuration information used by the server.
+
+Right now the payload is empty because I haven't even created the server!
+
+It'll probably have things like directory, version... I really don't know yet.
+
+```json
+{
+ "name": "I Am Server. Roar.",
+ "version": "0.99.1"
+}
+```
+
+### log
+
+The client sends this to the server to log a message, warning or error. For
+warnings and errors, we pass through an optional stackTrace array.
+
+Log:
+
+```json
+{
+ "value": "hello!",
+ "level": "debug"
+}
+```
+
+Warn:
+
+```json
+{
+ "value": "hello!",
+ "level": "warn",
+ "stackTrace": null
+}
+```
+
+Error:
+
+```json
+{
+ "value": "hello!",
+ "level": "error",
+ "stackTrace": [
+ {"lineNo": 1, "file": "foo.js"}
+ ]
+}
+```
+
+TBD: The actual stack trace format. I've seen a couple of formats unfortunately
+and I need to research what these will look like.
+
+Also, how is source maps going to factor in?
+
+### state.action.complete
+
+Sent from the client to the server when an action is complete. It's up to you
+to decide what an action is. For Redux, these are actions dispatched. For MobX,
+these are the results of `spy`.
+
+```json
+{
+ "name": "MY_ACTION",
+ "value": {}
+}
+```
+
+### state.action.dispatch
+
+Sent from the client to the server in order to dispatch this action through the
+state system.
+
+```json
+{
+ "action": { "type": "LOGIN_REQUEST", "password": "s3cr3t@g3ntm@n" }
+}
+```
+
+### state.values.request
+
+Sent from the server to the client to ask for the values of state.
+
+```json
+{
+ "path": "account"
+}
+```
+
+### state.values.response
+
+Sent from the client to the server in response to `state.values.request`.
+
+```json
+{
+ "path": "account",
+ "valid": true,
+ "value": {
+ "givenName": "Steve",
+ "familyName": "Kellock"
+ }
+}
+```
+
+### state.values.subscribe
+
+Sent from the server to the client to ask for notification when something
+in the state changes.
+
+```json
+{
+ "paths": [ "account", "cart.total" ]
+}
+```
+
+### state.values.change
+
+Sent from the client to the server when one of the subscriptions found in
+`state.values.subscribe` has changed.
+
+```json
+{
+ "changes": [
+ {
+ "path": "account",
+ "value": {
+ "email": "steve@kellock.ca"
+ }
+ },
+ {
+ "path": "cart.total",
+ "value": 100.01
+ }
+ ]
+}
+```
+
+### state.keys.request
+
+Sent from the server to the client to enumerate the keys inside state.
+
+```json
+{
+ "path": "account"
+}
+```
+
+### state.keys.response
+
+Sent from the client to server in response to `state.keys.request`.
+
+```json
+{
+ "path": "account",
+ "valid": true,
+ "keys": ["givenName", "familyName"]
+}
+```
+
+### api.response
+
+Sent from the client to server when an API has finished a request.
+
+```json
+{
+ "request": {
+ "url": "https://api.example.com/people/1",
+ "method": "PUT",
+ "data": {
+ "firstName": "Steve",
+ "lastName": "Kellock"
+ },
+ "headers": {
+ "Accept": "application/json",
+ "Cookie": "__ispy=mylittleye; __something=blue"
+ }
+ },
+ "response": {
+ "body": {},
+ "status": 200,
+ "headers": {
+ "Connection": "keep-alive",
+ "Server": "cloudflare-nginx"
+ }
+ },
+ "duration": 120.0
+}
+```
+
+### bench.report
+
+Sent from the client to server when it's time to report some performance details.
+
+```json
+{
+ "title": "My Sorting Algorithm",
+ "steps": [
+ { "title": "start", "time": 0 },
+ { "title": "lookup tables", "time": 123 },
+ { "title": "randomize", "time": 422 }
+ ]
+}
+```
+
+### display
+
+Sent from the client to the server to provide a way to show "custom" commands.
+```json
+{
+ "name": "MY EVENT",
+ "value": {
+ "color": "green",
+ "vegetable": "spinach",
+ "variant": "baby",
+ "salad": true
+ },
+ "important": true,
+ "preview": "What's in my appetizer?"
+}
+```
+
+
+# Plugins
+
+Reactotron is extensible via plugins. You add plugins by calling the `use`
+function on the the client.
+
+A plugin looks like this:
+
+```js
+export default () => reactotron => {}
+```
+
+* A function that:
+ * returns a function with 1 parameter (reactotron) that:
+ * returns an object
+
+
+### The 1st Function
+
+You use the first function to configure your plugin. If you don't have any
+configuration required for your plugin, just leave it empty like above.
+
+### The 2nd function
+
+The 2nd function gets called with the reactotron object. Among other things,
+it contains (most importantly) a function called `send()`.
+
+### The return object
+
+This contains hooks into reactotron. By naming the keys certain things, you're
+able to hook into guts to do stuff. Most importantly `onCommand` to receive
+events from the server and `features` to define extra functions on reactotron.
+
+
+```js
+// counter-plugin.js
+export default () => reactotron => {
+ let commandCounter = 0
+ return {
+ onCommand: command => {
+ commandCounter++
+ if (commandCounter === 69) console.log('tee hee')
+ }
+ }
+}
+```
+
+Here's what a plugin can do.
+
+```js
+{
+ // Fires whenever a command is received from the server.
+ //
+ // command is an object with:
+ // .type - String - the name of the command
+ // .payload - anything - maybe null, maybe a string, maybe an object, not a function
+ // its whatever the server sent.
+ onCommand: command => {
+ const { type, payload } = command
+ },
+
+ // Fires when we connect to the server. Will only be called if the plugin
+ // is setup before connecting to the server.
+ onConnect: () => {},
+
+ // Fires when we disconnect from the server.
+ onDisconnect: () => {},
+
+ // fires when the plugin is attached (this only happens once at initialization)
+ onPlugin: reactotron => console.log('I have been attached to ', reactotron),
+
+ // This is an object (not a function). The keys are strings. The values are functions.
+ // Every entry in here will become a method on the Reactotron client object.
+ // Collisions are handled on a first-come first-serve basis.
+ //
+ // These names are reserved:
+ // connect, configure, send, use, options, connected, plugins, and socket.
+ //
+ // Sorry.
+ //
+ // I went with this mixin approach because the interface feels nice from the
+ // calling code point-of-view.
+ features: {
+ // Reactotron.log('hello!')
+ log: (message) => send('log', { level: 'debug', message } ),
+
+ // Reactotron.warn('look out! falling rocks!')
+ warn: (message) => send('log', { level: 'warn', message } ),
+ }
+
+}
+```
diff --git a/packages/reactotron-core-client/package.json b/packages/reactotron-core-client/package.json
new file mode 100644
index 000000000..dd18eb57d
--- /dev/null
+++ b/packages/reactotron-core-client/package.json
@@ -0,0 +1,58 @@
+{
+ "name": "reactotron-core-client",
+ "version": "0.94.0",
+ "description": "Grants Reactotron clients the ability to talk to a Reactotron server.",
+ "main": "dist/index.js",
+ "scripts": {
+ "test": "ava",
+ "watch": "ava --watch",
+ "coverage": "nyc ava",
+ "build": "rollup -c",
+ "send": "babel-node scripts/send.js"
+ },
+ "repository": "https://github.com/reactotron/reactotron/tree/master/packages/reactotron-core-client",
+ "author": "Steve Kellock",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/reactotron/reactotron/issues"
+ },
+ "homepage": "https://reactotron.com",
+ "files": [
+ "dist",
+ "LICENSE",
+ "README.md"
+ ],
+ "devDependencies": {
+ "ava": "^0.16.0",
+ "babel-cli": "^6.11.4",
+ "babel-core": "^6.13.2",
+ "babel-eslint": "^6.1.2",
+ "babel-preset-es2015": "^6.13.2",
+ "babel-preset-es2015-rollup": "^1.2.0",
+ "babel-preset-stage-1": "^6.13.0",
+ "nyc": "^8.1.0",
+ "rollup": "^0.34.8",
+ "rollup-plugin-babel": "^2.6.1",
+ "standard": "^7.1.2"
+ },
+ "ava": {
+ "require": [
+ "babel-core/register"
+ ],
+ "babel": {
+ "babelrc": false,
+ "presets": [
+ "es2015",
+ "stage-1"
+ ]
+ }
+ },
+ "standard": {
+ "parser": "babel-eslint"
+ },
+ "dependencies": {
+ "ramda": "^0.22.1",
+ "ramdasauce": "^1.1.0",
+ "socket.io": "^1.4.8"
+ }
+}
diff --git a/packages/reactotron-core-client/rollup.config.js b/packages/reactotron-core-client/rollup.config.js
new file mode 100644
index 000000000..a4d9713f0
--- /dev/null
+++ b/packages/reactotron-core-client/rollup.config.js
@@ -0,0 +1,13 @@
+import babel from 'rollup-plugin-babel'
+
+export default {
+ entry: 'src/index.js',
+ format: 'cjs',
+ plugins: [
+ babel({
+ babelrc: false,
+ presets: ['es2015-rollup', 'stage-1']
+ })
+ ],
+ dest: 'dist/index.js'
+}
diff --git a/packages/reactotron-core-client/scripts/send.js b/packages/reactotron-core-client/scripts/send.js
new file mode 100644
index 000000000..22b090d73
--- /dev/null
+++ b/packages/reactotron-core-client/scripts/send.js
@@ -0,0 +1,84 @@
+import { createClient, CorePlugins } from '../src/index'
+import io from 'socket.io-client'
+
+const sleep = (time) => new Promise((resolve) => setTimeout(resolve, time))
+
+const client = createClient({ io })
+
+// possible messages
+const sendDebug = (message) => client.debug(message)
+const sendWarn = (message) => client.warn(message)
+const sendError = (message) => client.error(message)
+const sendBenchmark = async (title) => {
+ const bench = client.benchmark(title)
+ await sleep(50)
+ bench.step('about to sleep')
+ await sleep(100)
+ bench.step('finished sleeping')
+ await sleep(5)
+ bench.last('cleaning up')
+}
+const sendAction = (action) =>
+ client.send('state.action.complete', { ms: 123, name: action.type, action })
+
+// send a bunch of messages
+const shotgun = async () => {
+ sendDebug('This is a debug message. 🦄🔥🔥🔥🔥🔥🔥')
+ sendDebug('This is a debug message as well.')
+ sendDebug({
+ someNumbers: [1, 3, 5, 6, 9],
+ users: [
+ { firstName: 'Matthew', lastName: 'Kellock' },
+ { firstName: 'Liam', lastName: 'Kellock' }
+ ]
+ })
+ sendAction({ type: 'SLEEP_REQUEST', duration: 50 })
+ await sleep(50)
+ sendAction({ type: 'SLEEP_FINISHED', success: true })
+ sendWarn('Warning: This is an almost an error. Beware!')
+ await sleep(100)
+ sendError('Attention! Things just got real.')
+ await sleep(100)
+ const giant = `
+ Here\'s another debug message. This one is a little longer just to see what wrapping looks like. \
+ I hope you enjoy. Thank you. And have a wonderful day. \
+ Here\'s another debug message. This one is a little longer just to see what wrapping looks like. \
+ I hope you enjoy. Thank you. And have a wonderful day. \
+ I hope you enjoy. Thank you. And have a wonderful day. \
+ I hope you enjoy. Thank you. And have a wonderful day. \
+ \n\n
+ Here\'s another debug message. This one is a little longer just to see what wrapping looks like. \
+ I hope you enjoy. Thank you. And have a wonderful day. \
+ Here\'s another debug message. This one is a little longer just to see what wrapping looks like. \
+ Here\'s another debug message. This one is a little longer just to see what wrapping looks like. \
+ I hope you enjoy. Thank you. And have a wonderful day. \
+ Here\'s another debug message. This one is a little longer just to see what wrapping looks like. \
+ I hope you enjoy. Thank you. And have a wonderful day. \
+ `
+ sendAction({ type: 'POST_GIANT_MESSAGE', message: giant })
+ sendAction({ type: 'OBJECTS_LOLS', theGoods: {
+ cities: ['Toronto', 'Halifax'],
+ towns: ['Tiny', 'Pictou'],
+ thingsAroundMyOffice: { 'htc': 'vive', 'yeti': 'blue' },
+ trueThing: true,
+ falseThing: false,
+ undefinedThing: undefined,
+ nullThing: null,
+ symbolThing: Symbol('hi')
+ }})
+ sendDebug(giant)
+ await sendBenchmark('perf test for myCustomSort()')
+
+ client.socket.close()
+}
+
+client.configure({
+ onConnect: () => {
+ console.log('connected')
+ shotgun()
+ },
+ onDisconnect: () => console.log('disconnected'),
+ plugins: CorePlugins
+})
+
+client.connect()
diff --git a/packages/reactotron-core-client/src/index.js b/packages/reactotron-core-client/src/index.js
new file mode 100644
index 000000000..5206d5821
--- /dev/null
+++ b/packages/reactotron-core-client/src/index.js
@@ -0,0 +1,187 @@
+import R from 'ramda'
+import validate from './validate'
+import logger from './plugins/logger'
+import benchmark from './plugins/benchmark'
+import stateResponses from './plugins/state-responses'
+import apiResponse from './plugins/api-response'
+import { start } from './stopwatch'
+export { start } from './stopwatch'
+
+export const CorePlugins = [
+ logger(),
+ benchmark(),
+ stateResponses(),
+ apiResponse()
+]
+
+const DEFAULTS = {
+ io: null, // the socket.io function to create a socket
+ host: 'localhost', // the server to connect (required)
+ port: 9090, // the port to connect (required)
+ name: 'reactotron-core-client', // some human-friendly session name
+ secure: false, // use wss instead of ws
+ plugins: CorePlugins, // needed to make society function
+ onCommand: cmd => null, // the function called when we receive a command
+ onConnect: () => null, // fires when we connect
+ onDisconnect: () => null // fires when we disconnect
+}
+
+// these are not for you.
+const isReservedFeature = R.contains(R.__, [
+ 'options', 'connected', 'socket', 'plugins',
+ 'configure', 'connect', 'send', 'use',
+ 'startTimer'
+])
+
+export class Client {
+
+ // the configuration options
+ options = R.merge({}, DEFAULTS)
+ connected = false
+ socket = null
+ plugins = []
+
+ startTimer = () => start()
+
+ constructor () {
+ // we will be invoking send from callbacks other than inside this file
+ this.send = this.send.bind(this)
+ }
+
+ /**
+ * Set the configuration options.
+ */
+ configure (options = {}) {
+ // options get merged & validated before getting set
+ const newOptions = R.merge(this.options, options)
+ validate(newOptions)
+ this.options = newOptions
+
+ // if we have plugins, let's add them here
+ if (R.isArrayLike(this.options.plugins)) {
+ R.forEach(this.use.bind(this), this.options.plugins)
+ }
+
+ return this
+ }
+
+ /**
+ * Connect to the Reactotron server.
+ */
+ connect () {
+ this.connected = true
+ const { io, secure, host, port, name, userAgent, environment, reactotronVersion } = this.options
+ const { onCommand, onConnect, onDisconnect } = this.options
+
+ // establish a socket.io connection to the server
+ const protocol = secure ? 'wss' : 'ws'
+ const socket = io(`${protocol}://${host}:${port}`, {
+ jsonp: false,
+ transports: ['websocket', 'polling']
+ })
+
+ // fires when we talk to the server
+ socket.on('connect', () => {
+ // fire our optional onConnect handler
+ onConnect && onConnect()
+
+ // trigger our plugins onConnect
+ R.forEach(plugin => plugin.onConnect && plugin.onConnect(), this.plugins)
+
+ // introduce ourselves
+ this.send('client.intro', { host, port, name, userAgent, reactotronVersion, environment })
+ })
+
+ // fires when we disconnect
+ socket.on('disconnect', () => {
+ // trigger our disconnect handler
+ onDisconnect && onDisconnect()
+
+ // as well as the plugin's onDisconnect
+ R.forEach(plugin => plugin.onDisconnect && plugin.onDisconnect(), this.plugins)
+ })
+
+ // fires when we receive a command, just forward it off
+ socket.on('command', command => {
+ // trigger our own command handler
+ onCommand && onCommand(command)
+
+ // trigger our plugins onCommand
+ R.forEach(plugin => plugin.onCommand && plugin.onCommand(command), this.plugins)
+ })
+
+ // assign the socket to the instance
+ this.socket = socket
+
+ return this
+ }
+
+ /**
+ * Sends a command to the server
+ */
+ send (type, payload, important = false) {
+ this.socket && this.socket.emit('command', { type, payload, important: !!important })
+ }
+
+ /**
+ * Sends a custom command to the server to displays nicely.
+ */
+ display ({ name, value, preview, important = false }) {
+ this.send('display', { name, value, preview }, important)
+ }
+
+ /**
+ * Adds a plugin to the system
+ */
+ use (pluginCreator) {
+ // we're supposed to be given a function
+ if (typeof pluginCreator !== 'function') throw new Error('plugins must be a function')
+
+ // execute it immediately passing the send function
+ const plugin = pluginCreator.bind(this)(this)
+
+ // ensure we get an Object-like creature back
+ if (!R.is(Object, plugin)) throw new Error('plugins must return an object')
+
+ // do we have features to mixin?
+ if (plugin.features) {
+ // validate
+ if (!R.is(Object, plugin.features)) throw new Error('features must be an object')
+
+ // here's how we're going to inject these in
+ const inject = (key) => {
+ // grab the function
+ const featureFunction = plugin.features[key]
+
+ // only functions may pass
+ if (typeof featureFunction !== 'function') throw new Error(`feature ${key} is not a function`)
+
+ // ditch reserved names
+ if (isReservedFeature(key)) throw new Error(`feature ${key} is a reserved name`)
+
+ // ok, let's glue it up... and lose all respect from elite JS champions.
+ this[key] = featureFunction
+ }
+
+ // let's inject
+ R.forEach(inject, R.keys(plugin.features))
+ }
+
+ // add it to the list
+ this.plugins.push(plugin)
+
+ // call the plugins onPlugin
+ plugin.onPlugin && typeof plugin.onPlugin === 'function' && plugin.onPlugin.bind(this)(this)
+
+ // chain-friendly
+ return this
+ }
+
+}
+
+// convenience factory function
+export const createClient = (options) => {
+ const client = new Client()
+ client.configure(options)
+ return client
+}
diff --git a/packages/reactotron-core-client/src/plugins/api-response.js b/packages/reactotron-core-client/src/plugins/api-response.js
new file mode 100644
index 000000000..9e9f3681c
--- /dev/null
+++ b/packages/reactotron-core-client/src/plugins/api-response.js
@@ -0,0 +1,16 @@
+import { isWithin } from 'ramdasauce'
+
+/**
+ * Sends API request/response information.
+ */
+export default () => reactotron => {
+ return {
+ features: {
+ apiResponse: (request, response, duration) => {
+ const ok = response && response.status && isWithin(200, 299, response.status)
+ const important = !ok
+ reactotron.send('api.response', { request, response, duration }, important)
+ }
+ }
+ }
+}
diff --git a/packages/reactotron-core-client/src/plugins/benchmark.js b/packages/reactotron-core-client/src/plugins/benchmark.js
new file mode 100644
index 000000000..860483995
--- /dev/null
+++ b/packages/reactotron-core-client/src/plugins/benchmark.js
@@ -0,0 +1,28 @@
+import { length, last } from 'ramda'
+
+/**
+ * Runs small high-unscientific benchmarks for you.
+ */
+export default () => reactotron => {
+ const { startTimer } = reactotron
+
+ const benchmark = title => {
+ const steps = []
+ const elapsed = startTimer()
+ const step = stepTitle => {
+ const previousTime = length(steps) === 0 ? 0 : last(steps).time
+ const nextTime = elapsed()
+ steps.push({ title: stepTitle, time: nextTime, delta: nextTime - previousTime })
+ }
+ steps.push({ title, time: 0, delta: 0 })
+ const stop = stopTitle => {
+ step(stopTitle)
+ reactotron.send('benchmark.report', { title, steps })
+ }
+ return { step, stop, last: stop }
+ }
+
+ return {
+ features: { benchmark }
+ }
+}
diff --git a/packages/reactotron-core-client/src/plugins/logger.js b/packages/reactotron-core-client/src/plugins/logger.js
new file mode 100644
index 000000000..1598ed007
--- /dev/null
+++ b/packages/reactotron-core-client/src/plugins/logger.js
@@ -0,0 +1,13 @@
+/**
+ * Provides 4 features for logging. log & debug are the same.
+ */
+export default () => reactotron => {
+ return {
+ features: {
+ log: (message, important = false) => reactotron.send('log', { level: 'debug', message}, !!important),
+ debug: (message, important = false) => reactotron.send('log', { level: 'debug', message}, !!important),
+ warn: (message) => reactotron.send('log', { level: 'warn', message }, true),
+ error: (message, stack) => reactotron.send('log', { level: 'error', message, stack }, true)
+ }
+ }
+}
diff --git a/packages/reactotron-core-client/src/plugins/state-responses.js b/packages/reactotron-core-client/src/plugins/state-responses.js
new file mode 100644
index 000000000..33c24ef11
--- /dev/null
+++ b/packages/reactotron-core-client/src/plugins/state-responses.js
@@ -0,0 +1,20 @@
+/**
+ * Provides helper functions for send state responses.
+ */
+export default () => reactotron => {
+ return {
+ features: {
+ stateActionComplete: (name, action, important = false) =>
+ reactotron.send('state.action.complete', { name, action }, !!important),
+
+ stateValuesResponse: (path, value, valid = true) =>
+ reactotron.send('state.values.response', { path, value, valid }),
+
+ stateKeysResponse: (path, keys, valid = true) =>
+ reactotron.send('state.keys.response', { path, keys, valid }),
+
+ stateValuesChange: changes =>
+ reactotron.send('state.values.change', { changes })
+ }
+ }
+}
diff --git a/packages/reactotron-core-client/src/stopwatch.js b/packages/reactotron-core-client/src/stopwatch.js
new file mode 100644
index 000000000..8b279af54
--- /dev/null
+++ b/packages/reactotron-core-client/src/stopwatch.js
@@ -0,0 +1,37 @@
+const hasHirezNodeTimer = false && typeof process === 'object' && process && process.hrtime && typeof process.hrtime === 'function'
+
+// the default timer
+const defaultPerformanceNow = () => Date.now()
+
+// try to find the browser-based performance timer
+const nativePerformance = typeof window !== 'undefined' && window && (window.performance || window.msPerformance || window.webkitPerformance)
+
+// if we do find it, let's setup to call it
+const nativePerformanceNow = () => nativePerformance.now()
+
+// the function we're trying to assign
+let performanceNow = defaultPerformanceNow
+
+// accepts an already started time and returns the number of milliseconds
+let delta = started => performanceNow() - started
+
+// node will use a high rez timer
+if (hasHirezNodeTimer) {
+ performanceNow = process.hrtime
+ delta = started => performanceNow(started)[1] / 1000000
+} else if (nativePerformance) {
+ performanceNow = nativePerformanceNow
+}
+
+// this is the interface the callers will use
+// export const performanceNow = nativePerformance ? nativePerformanceNow : defaultPerformanceNow
+
+/**
+ * Starts a lame, low-res timer. Returns a function which when invoked,
+ * gives you the number of milliseconds since passing. ish.
+ */
+export const start = () => {
+ // record the start time
+ const started = performanceNow()
+ return () => delta(started)
+}
diff --git a/packages/reactotron-core-client/src/validate.js b/packages/reactotron-core-client/src/validate.js
new file mode 100644
index 000000000..63a08fb10
--- /dev/null
+++ b/packages/reactotron-core-client/src/validate.js
@@ -0,0 +1,22 @@
+import R from 'ramda'
+import RS from 'ramdasauce'
+
+const isIoValid = (io) => !R.isNil(io)
+const isHostValid = R.allPass([R.complement(RS.isNilOrEmpty), R.is(String)])
+const isPortValid = R.allPass([R.complement(R.isNil), R.is(Number), RS.isWithin(1, 65535)])
+const onCommandValid = (fn) => typeof fn === 'function'
+
+/**
+ * Ensures the options are sane to run this baby. Throw if not. These
+ * are basically sanity checks.
+ */
+const validate = (options) => {
+ const { io, host, port, onCommand } = options
+
+ if (!isIoValid(io)) throw new Error('invalid io function')
+ if (!isHostValid(host)) throw new Error('invalid host')
+ if (!isPortValid(port)) throw new Error('invalid port')
+ if (!onCommandValid(onCommand)) throw new Error('invalid onCommand handler')
+}
+
+export default validate
diff --git a/packages/reactotron-core-client/test/_fake-io.js b/packages/reactotron-core-client/test/_fake-io.js
new file mode 100644
index 000000000..83148e926
--- /dev/null
+++ b/packages/reactotron-core-client/test/_fake-io.js
@@ -0,0 +1,6 @@
+export default (x) => {
+ return {
+ on: (command, callback) => true,
+ emit: () => true
+ }
+}
diff --git a/packages/reactotron-core-client/test/_get-free-port.js b/packages/reactotron-core-client/test/_get-free-port.js
new file mode 100644
index 000000000..fade86c5f
--- /dev/null
+++ b/packages/reactotron-core-client/test/_get-free-port.js
@@ -0,0 +1,9 @@
+import net from 'net'
+
+export default function getFreePort (cb) {
+ const server = net.createServer()
+ server.listen(() => {
+ const port = server.address().port
+ server.close(() => cb(port))
+ })
+}
diff --git a/packages/reactotron-core-client/test/api-response-test.js b/packages/reactotron-core-client/test/api-response-test.js
new file mode 100644
index 000000000..5533747a8
--- /dev/null
+++ b/packages/reactotron-core-client/test/api-response-test.js
@@ -0,0 +1,83 @@
+import test from 'ava'
+import { createClient, CorePlugins } from '../src'
+import socketClient from 'socket.io-client'
+import plugin from '../src/plugins/state-responses'
+
+test('stateActionComplete', t => {
+ const client = createClient({ io: socketClient })
+ let type
+ let name
+ let action
+ client.send = (x, y) => {
+ type = x
+ name = y.name
+ action = y.action
+ }
+ client.use(plugin())
+ t.is(client.plugins.length, CorePlugins.length + 1)
+ t.is(typeof client.stateActionComplete, 'function')
+ client.stateActionComplete('name', { action: 123 })
+ t.is(type, 'state.action.complete')
+ t.is(name, 'name')
+ t.deepEqual(action, { action: 123 })
+})
+
+test('stateValuesResponse', t => {
+ const client = createClient({ io: socketClient })
+ let type
+ let path
+ let value
+ let valid = true
+ client.send = (x, y) => {
+ type = x
+ path = y.path
+ value = y.value
+ valid = y.valid
+ }
+ client.use(plugin())
+ t.is(client.plugins.length, CorePlugins.length + 1)
+ t.is(typeof client.stateValuesResponse, 'function')
+ client.stateValuesResponse('user.password', 'password', false)
+ t.is(type, 'state.values.response')
+ t.is(path, 'user.password')
+ t.is(value, 'password')
+ t.false(valid)
+})
+
+test('stateKeysResponse', t => {
+ const client = createClient({ io: socketClient })
+ let type
+ let path
+ let keys
+ let valid = true
+ client.send = (x, y) => {
+ type = x
+ path = y.path
+ keys = y.keys
+ valid = y.valid
+ }
+ client.use(plugin())
+ t.is(client.plugins.length, CorePlugins.length + 1)
+ t.is(typeof client.stateKeysResponse, 'function')
+ client.stateKeysResponse('user', ['name', 'password'], false)
+ t.is(type, 'state.keys.response')
+ t.is(path, 'user')
+ t.deepEqual(keys, ['name', 'password'])
+ t.false(valid)
+})
+
+test('stateValuesChange', t => {
+ const client = createClient({ io: socketClient })
+ let type
+ let changes
+ client.send = (x, y) => {
+ type = x
+ changes = y.changes
+ }
+ client.use(plugin())
+ t.is(client.plugins.length, CorePlugins.length + 1)
+ t.is(typeof client.stateValuesChange, 'function')
+ client.stateValuesChange([{ path: 'a', value: 1 }, { path: 'b', value: 2 }])
+ t.is(type, 'state.values.change')
+ t.deepEqual(changes, [{ path: 'a', value: 1 }, { path: 'b', value: 2 }])
+})
diff --git a/packages/reactotron-core-client/test/configure-test.js b/packages/reactotron-core-client/test/configure-test.js
new file mode 100644
index 000000000..2c1e334da
--- /dev/null
+++ b/packages/reactotron-core-client/test/configure-test.js
@@ -0,0 +1,40 @@
+import test from 'ava'
+import { createClient } from '../src'
+import io from './_fake-io'
+
+test('has defaults', t => {
+ const client = createClient({io})
+ t.is(client.options.host, 'localhost')
+ t.is(client.options.port, 9090)
+ t.is(client.options.name, 'reactotron-core-client')
+})
+
+test('options can be overridden', t => {
+ const client = createClient({ io, host: 'hey', port: 1 })
+ t.is(client.options.host, 'hey')
+ t.is(client.options.port, 1)
+})
+
+test('io is required', t => {
+ t.throws(() => createClient())
+ t.throws(() => createClient({}))
+ t.throws(() => createClient({ io: null }))
+})
+
+test('onCommand is required', t => {
+ t.throws(() => createClient({ io, onCommand: undefined }))
+ t.throws(() => createClient({ io, onCommand: null }))
+})
+
+test('host is required', t => {
+ t.throws(() => createClient({ io, host: undefined }))
+ t.throws(() => createClient({ io, host: null }))
+ t.throws(() => createClient({ io, host: '' }))
+})
+
+test('port is required', t => {
+ t.throws(() => createClient({ io, port: null }))
+ t.throws(() => createClient({ io, port: undefined }))
+ t.throws(() => createClient({ io, port: 0 }))
+ t.throws(() => createClient({ io, port: 65536 }))
+})
diff --git a/packages/reactotron-core-client/test/connect-test.js b/packages/reactotron-core-client/test/connect-test.js
new file mode 100644
index 000000000..c1bcfa762
--- /dev/null
+++ b/packages/reactotron-core-client/test/connect-test.js
@@ -0,0 +1,27 @@
+import test from 'ava'
+import { createClient } from '../src'
+import io from './_fake-io'
+
+test('has a connect method', t => {
+ const client = createClient({ io })
+ t.truthy(client.connect)
+})
+
+test('connect returns itself', t => {
+ const client = createClient({ io })
+ t.is(client.connect(), client)
+})
+
+test('we start off unconnected', t => {
+ t.false(createClient({ io }).connected)
+})
+
+test('connecting shows us a connected', t => {
+ t.true(createClient({ io }).connect().connected)
+})
+
+test('builds a socket', t => {
+ const client = createClient({ io })
+ client.connect()
+ t.truthy(client.socket)
+})
diff --git a/packages/reactotron-core-client/test/create-client-test.js b/packages/reactotron-core-client/test/create-client-test.js
new file mode 100644
index 000000000..f2e0491ec
--- /dev/null
+++ b/packages/reactotron-core-client/test/create-client-test.js
@@ -0,0 +1,8 @@
+import test from 'ava'
+import { createClient } from '../src'
+import io from './_fake-io'
+
+test('returns a client', t => {
+ const client = createClient({ io })
+ t.truthy(client)
+})
diff --git a/packages/reactotron-core-client/test/on-command-test.js b/packages/reactotron-core-client/test/on-command-test.js
new file mode 100644
index 000000000..6df7e94fc
--- /dev/null
+++ b/packages/reactotron-core-client/test/on-command-test.js
@@ -0,0 +1,38 @@
+import test from 'ava'
+import { createClient } from '../src'
+import socketServer from 'socket.io'
+import socketClient from 'socket.io-client'
+import getFreePort from './_get-free-port'
+import io from './_fake-io'
+
+const mockType = 'TEST'
+const mockPayload = [1, 2, 'three', { four: true }]
+
+test('the default onCommand does nothing', t => {
+ const client = createClient({ io })
+ t.falsy(client.options.onCommand())
+})
+
+test.cb('receives a valid command', t => {
+ getFreePort(port => {
+ // client should receive the command
+ const client = createClient({
+ io: socketClient,
+ port: port,
+ onCommand: ({type, payload}) => {
+ t.is(type, mockType)
+ t.deepEqual(payload, mockPayload)
+ t.end()
+ }
+ })
+
+ // when the server gets a connection, send the command
+ const server = socketServer(port)
+ server.on('connection', socket => {
+ server.sockets.emit('command', {type: mockType, payload: mockPayload})
+ })
+
+ // kick it off
+ client.connect()
+ })
+})
diff --git a/packages/reactotron-core-client/test/on-connect-test.js b/packages/reactotron-core-client/test/on-connect-test.js
new file mode 100644
index 000000000..5cda458dd
--- /dev/null
+++ b/packages/reactotron-core-client/test/on-connect-test.js
@@ -0,0 +1,28 @@
+import test from 'ava'
+import { createClient } from '../src'
+import socketServer from 'socket.io'
+import socketClient from 'socket.io-client'
+import getFreePort from './_get-free-port'
+
+test.cb('fires onConnect upon successful connection', t => {
+ getFreePort(port => {
+ // plan to see 1 assertion
+ t.plan(1)
+
+ // setup a server
+ socketServer(port)
+
+ // setup the client
+ const client = createClient({
+ io: socketClient,
+ port: port,
+ onConnect: () => {
+ t.pass(true)
+ t.end()
+ }
+ })
+
+ // kick it off
+ client.connect()
+ })
+})
diff --git a/packages/reactotron-core-client/test/on-disconnect-test.js b/packages/reactotron-core-client/test/on-disconnect-test.js
new file mode 100644
index 000000000..716c1b559
--- /dev/null
+++ b/packages/reactotron-core-client/test/on-disconnect-test.js
@@ -0,0 +1,31 @@
+import test from 'ava'
+import { createClient } from '../src'
+import socketServer from 'socket.io'
+import socketClient from 'socket.io-client'
+import getFreePort from './_get-free-port'
+
+test.cb('fires onConnect upon successful connection', t => {
+ getFreePort(port => {
+ // plan to see 1 assertion
+ t.plan(1)
+
+ // setup a server
+ const server = socketServer(port)
+ server.on('connection', socket => {
+ socket.disconnect()
+ })
+
+ // setup the client
+ const client = createClient({
+ io: socketClient,
+ port: port,
+ onDisconnect: () => {
+ t.pass(true)
+ t.end()
+ }
+ })
+
+ // kick it off
+ client.connect()
+ })
+})
diff --git a/packages/reactotron-core-client/test/plugin-benchmark-test.js b/packages/reactotron-core-client/test/plugin-benchmark-test.js
new file mode 100644
index 000000000..0236cc6c8
--- /dev/null
+++ b/packages/reactotron-core-client/test/plugin-benchmark-test.js
@@ -0,0 +1,43 @@
+import test from 'ava'
+import { createClient, CorePlugins } from '../src'
+import socketClient from 'socket.io-client'
+import plugin from '../src/plugins/benchmark'
+
+// a promise based delay that's not accurate at all
+const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
+
+test('adds benchmarks to a report', async t => {
+ const client = createClient({ io: socketClient })
+ let commandType
+ let report
+
+ // mock the send to capture
+ client.send = (type, payload) => {
+ commandType = type
+ report = payload
+ }
+
+ // register
+ client.use(plugin())
+
+ t.is(client.plugins.length, CorePlugins.length + 1)
+ t.is(typeof client.benchmark, 'function')
+
+ // use the benchmark feature
+ const bench = client.benchmark('a')
+ await delay(50)
+ bench.step('b')
+ await delay(50)
+ bench.stop('c')
+
+ // checkout our results
+ t.is(commandType, 'benchmark.report')
+ t.is(report.title, 'a')
+ t.is(report.steps.length, 3)
+ t.is(report.steps[0].title, 'a')
+ t.is(report.steps[1].title, 'b')
+ t.is(report.steps[2].title, 'c')
+ t.is(report.steps[0].time, 0)
+ t.true(report.steps[1].time > 0)
+ t.true(report.steps[2].time > report.steps[1].time)
+})
diff --git a/packages/reactotron-core-client/test/plugin-features-test.js b/packages/reactotron-core-client/test/plugin-features-test.js
new file mode 100644
index 000000000..06c9d9f20
--- /dev/null
+++ b/packages/reactotron-core-client/test/plugin-features-test.js
@@ -0,0 +1,45 @@
+import test from 'ava'
+import { createClient } from '../src'
+import io from './_fake-io'
+import R from 'ramda'
+
+test('features must be an object if they appear', t => {
+ const client = createClient({ io })
+ t.throws(() => client.use(reactotron => ({ features: 1 })))
+})
+
+test('some names are not allowed', t => {
+ const client = createClient({ io })
+ const createPlugin = features => reactotron => ({features})
+
+ const badPlugins = R.map(
+ name => createPlugin({ [name]: R.identity }),
+ ['options', 'connected', 'socket', 'plugins', 'configure', 'connect', 'send', 'use', 'startTimer']
+ )
+
+ R.forEach(plugin => {
+ t.throws(() => { client.use(plugin) })
+ }, badPlugins)
+})
+
+test('features can be added and called', t => {
+ const client = createClient({ io })
+ const plugin = () => reactotron => {
+ const features = {
+ magic: () => 42
+ }
+ return { features }
+ }
+ client.use(plugin())
+ t.is(typeof client.magic, 'function')
+ t.is(client.magic(), 42)
+})
+
+test('you can overwrite other feature names', t => {
+ const client = createClient({ io })
+ const createPlugin = number => reactotron => ({ features: { hello: () => number } })
+ client.use(createPlugin(69))
+ t.is(client.hello(), 69)
+ client.use(createPlugin(9001))
+ t.is(client.hello(), 9001)
+})
diff --git a/packages/reactotron-core-client/test/plugin-interface-test.js b/packages/reactotron-core-client/test/plugin-interface-test.js
new file mode 100644
index 000000000..e0bfbe01d
--- /dev/null
+++ b/packages/reactotron-core-client/test/plugin-interface-test.js
@@ -0,0 +1,65 @@
+import test from 'ava'
+import { createClient, CorePlugins } from '../src'
+import io from './_fake-io'
+
+test('client accepts plugins', t => {
+ const client = createClient({ io })
+ t.truthy(client.plugins)
+ t.is(client.plugins.length, CorePlugins.length)
+})
+
+test('plugins are functions', t => {
+ const client = createClient({ io })
+ t.throws(() => client.use())
+ t.throws(() => client.use(null))
+ t.throws(() => client.use(''))
+ t.throws(() => client.use(1))
+})
+
+test('plugins are invoke and return an object', t => {
+ const client = createClient({ io })
+ t.throws(() => client.use(() => null))
+ t.throws(() => client.use(() => 1))
+ t.throws(() => client.use(() => ''))
+ t.throws(() => client.use(() => undefined))
+ client.use(() => ({}))
+ client.use(() => () => true)
+})
+
+test('plugins can literally do nothing', t => {
+ const client = createClient({ io })
+ const empty = reactotron => ({})
+ client.use(empty)
+ t.is(client.plugins.length, CorePlugins.length + 1)
+})
+
+test.cb('initialized with the config object', t => {
+ const client = createClient({ io })
+ client.use(reactotron => {
+ t.is(typeof reactotron, 'object')
+ t.is(reactotron, client)
+ t.is(typeof reactotron.send, 'function')
+ t.end()
+ return {}
+ })
+ t.is(client.plugins.length, CorePlugins.length + 1)
+})
+
+test('can be added in createClient', t => {
+ const createPlugin = (name, value) => reactotron => ({ features: { [name]: () => value } })
+ const client = createClient({
+ io,
+ plugins: [
+ createPlugin('sayHello', 'hello'),
+ createPlugin('sayGoodbye', 'goodbye')
+ ]
+ })
+
+ t.is(client.sayHello(), 'hello')
+ t.is(client.sayGoodbye(), 'goodbye')
+})
+
+test('plugins in createClient must be an array', t => {
+ const client = createClient({ io, plugins: 5 })
+ t.is(client.plugins.length, 0)
+})
diff --git a/packages/reactotron-core-client/test/plugin-logger-test.js b/packages/reactotron-core-client/test/plugin-logger-test.js
new file mode 100644
index 000000000..f3524ce92
--- /dev/null
+++ b/packages/reactotron-core-client/test/plugin-logger-test.js
@@ -0,0 +1,33 @@
+import test from 'ava'
+import { createClient, CorePlugins } from '../src'
+import socketClient from 'socket.io-client'
+import plugin from '../src/plugins/logger'
+
+test('the 4 functions send the right data', t => {
+ const client = createClient({ io: socketClient })
+ const results = []
+ client.send = (type, payload) => { results.push({type, payload}) }
+ client.use(plugin())
+ t.is(client.plugins.length, CorePlugins.length + 1)
+ t.is(typeof client.log, 'function')
+ t.is(typeof client.debug, 'function')
+ t.is(typeof client.warn, 'function')
+ t.is(typeof client.error, 'function')
+ client.log('a')
+ client.debug('b')
+ client.warn('c')
+ client.error('d')
+ t.is(results.length, 4)
+ t.is(results[0].type, 'log')
+ t.is(results[1].type, 'log')
+ t.is(results[2].type, 'log')
+ t.is(results[3].type, 'log')
+ t.is(results[0].payload.level, 'debug')
+ t.is(results[1].payload.level, 'debug')
+ t.is(results[2].payload.level, 'warn')
+ t.is(results[3].payload.level, 'error')
+ t.is(results[0].payload.message, 'a')
+ t.is(results[1].payload.message, 'b')
+ t.is(results[2].payload.message, 'c')
+ t.is(results[3].payload.message, 'd')
+})
diff --git a/packages/reactotron-core-client/test/plugin-on-command-test.js b/packages/reactotron-core-client/test/plugin-on-command-test.js
new file mode 100644
index 000000000..985b45a0d
--- /dev/null
+++ b/packages/reactotron-core-client/test/plugin-on-command-test.js
@@ -0,0 +1,34 @@
+import test from 'ava'
+import { createClient } from '../src'
+import getFreePort from './_get-free-port'
+import socketClient from 'socket.io-client'
+import socketServer from 'socket.io'
+
+test.cb('plugins support command', t => {
+ const mockType = 'type'
+ const mockPayload = 'payload'
+
+ // the plugin to capture the command
+ const plugin = () => config => {
+ return {
+ onCommand: command => {
+ t.is(command.type, mockType)
+ t.deepEqual(command.payload, mockPayload)
+ t.end()
+ }
+ }
+ }
+
+ getFreePort(port => {
+ // the server waits for the command
+ socketServer(port)
+ .on('connection', socket => {
+ socket.emit('command', {type: mockType, payload: mockPayload})
+ })
+
+ // create the client, add the plugin, and connect
+ const client = createClient({ io: socketClient, port: port })
+ client.use(plugin())
+ client.connect()
+ })
+})
diff --git a/packages/reactotron-core-client/test/plugin-on-connect-test.js b/packages/reactotron-core-client/test/plugin-on-connect-test.js
new file mode 100644
index 000000000..43ebab900
--- /dev/null
+++ b/packages/reactotron-core-client/test/plugin-on-connect-test.js
@@ -0,0 +1,25 @@
+import test from 'ava'
+import { createClient } from '../src'
+import getFreePort from './_get-free-port'
+import socketClient from 'socket.io-client'
+import socketServer from 'socket.io'
+
+test.cb('plugins support onConnect', t => {
+ // this plugin supports onConnect
+ const plugin = send => ({
+ onConnect: () => {
+ t.pass()
+ t.end()
+ }
+ })
+
+ getFreePort(port => {
+ // fire up a server
+ socketServer(port)
+
+ // create a client & add the plugin
+ createClient({ io: socketClient, port })
+ .use(plugin)
+ .connect()
+ })
+})
diff --git a/packages/reactotron-core-client/test/plugin-on-disconnect-test.js b/packages/reactotron-core-client/test/plugin-on-disconnect-test.js
new file mode 100644
index 000000000..86800b7d8
--- /dev/null
+++ b/packages/reactotron-core-client/test/plugin-on-disconnect-test.js
@@ -0,0 +1,22 @@
+import test from 'ava'
+import { createClient } from '../src'
+import getFreePort from './_get-free-port'
+import socketClient from 'socket.io-client'
+import socketServer from 'socket.io'
+
+test.cb('plugins support onDisconnect', t => {
+ getFreePort(port => {
+ socketServer(port).on('connection', socket => socket.disconnect())
+
+ const plugin = () => send => ({
+ onDisconnect: () => {
+ t.pass()
+ t.end()
+ }
+ })
+
+ createClient({ io: socketClient, port })
+ .use(plugin())
+ .connect()
+ })
+})
diff --git a/packages/reactotron-core-client/test/plugin-on-plugin-test.js b/packages/reactotron-core-client/test/plugin-on-plugin-test.js
new file mode 100644
index 000000000..32b6200d8
--- /dev/null
+++ b/packages/reactotron-core-client/test/plugin-on-plugin-test.js
@@ -0,0 +1,48 @@
+import test from 'ava'
+import { createClient } from '../src'
+import getFreePort from './_get-free-port'
+import socketClient from 'socket.io-client'
+import socketServer from 'socket.io'
+
+test.cb('plugins support onPlugin', t => {
+ t.plan(3)
+ const mockType = 'hello'
+ const mockPayload = 'lol'
+
+ getFreePort(port => {
+ // create a client
+ const client = createClient({ io: socketClient, port })
+
+ // let's capture whatn onPlugin's instance scop ie
+ let capturedInstance
+
+ // make a plugin to capture onPlugin
+ const plugin = () => send => ({
+ onPlugin: (instance) => {
+ t.is(instance, client)
+ capturedInstance = instance
+ }
+ })
+
+ // fire up a server
+ socketServer(port)
+ .on('connection', socket => {
+ // send through the one we recieved in the plugin
+ capturedInstance.send(mockType, mockPayload)
+
+ // fires the server receives a command
+ socket.on('command', ({type, payload}) => {
+ if (type === 'client.intro') return
+ t.is(type, mockType)
+ t.deepEqual(payload, mockPayload)
+ t.end()
+ })
+ })
+
+ // add the plugin
+ client.use(plugin())
+
+ // kick it off
+ client.connect()
+ })
+})
diff --git a/packages/reactotron-core-client/test/plugin-send-test.js b/packages/reactotron-core-client/test/plugin-send-test.js
new file mode 100644
index 000000000..532faf5dd
--- /dev/null
+++ b/packages/reactotron-core-client/test/plugin-send-test.js
@@ -0,0 +1,41 @@
+import test from 'ava'
+import { createClient } from '../src'
+import getFreePort from './_get-free-port'
+import socketClient from 'socket.io-client'
+import socketServer from 'socket.io'
+
+test.cb('plugins support send', t => {
+ const mockType = 'type'
+ const mockPayload = 'payload'
+
+ // odd way to hold on to the plugin's send function
+ let capturedSend
+
+ // the plugin to extract the send function
+ const plugin = () => reactotron => {
+ capturedSend = reactotron.send
+ return {}
+ }
+
+ getFreePort(port => {
+ // the server waits for the command
+ socketServer(port)
+ .on('connection', socket => {
+ // send through the one we recieved in the plugin
+ capturedSend(mockType, mockPayload)
+
+ // fires the server receives a command
+ socket.on('command', ({type, payload}) => {
+ if (type === 'client.intro') return
+ t.is(type, mockType)
+ t.deepEqual(payload, mockPayload)
+ t.end()
+ })
+ })
+
+ // create the client, add the plugin, and connect
+ createClient({ io: socketClient, port: port })
+ .use(plugin())
+ .connect()
+ })
+})
diff --git a/packages/reactotron-core-client/test/plugin-state-responses-test.js b/packages/reactotron-core-client/test/plugin-state-responses-test.js
new file mode 100644
index 000000000..66a166584
--- /dev/null
+++ b/packages/reactotron-core-client/test/plugin-state-responses-test.js
@@ -0,0 +1,30 @@
+import test from 'ava'
+import { createClient, CorePlugins } from '../src'
+import socketClient from 'socket.io-client'
+import plugin from '../src/plugins/api-response'
+
+test('apiResponse', t => {
+ const client = createClient({ io: socketClient })
+ let type
+ let request
+ let response
+ let duration
+ client.send = (x, y) => {
+ type = x
+ request = y.request
+ response = y.response
+ duration = y.duration
+ }
+ client.use(plugin())
+ t.is(client.plugins.length, CorePlugins.length + 1)
+ t.is(typeof client.apiResponse, 'function')
+ client.apiResponse(
+ {a: 1},
+ {b: 2},
+ 12
+ )
+ t.is(type, 'api.response')
+ t.deepEqual(request, { a: 1 })
+ t.deepEqual(response, { b: 2 })
+ t.is(duration, 12)
+})
diff --git a/packages/reactotron-core-client/test/send-test.js b/packages/reactotron-core-client/test/send-test.js
new file mode 100644
index 000000000..71a01906b
--- /dev/null
+++ b/packages/reactotron-core-client/test/send-test.js
@@ -0,0 +1,28 @@
+import test from 'ava'
+import { createClient } from '../src'
+import socketServer from 'socket.io'
+import socketClient from 'socket.io-client'
+import getFreePort from './_get-free-port'
+
+const mockType = 'GO!'
+const mockPayload = [1, 2, 'three', { four: true }]
+
+test.cb('sends a valid command', t => {
+ getFreePort(port => {
+ // the server waits for the command
+ const server = socketServer(port)
+ server.on('connection', socket => {
+ socket.on('command', ({type, payload}) => {
+ if (type === 'client.intro') return
+ t.is(type, mockType)
+ t.deepEqual(payload, mockPayload)
+ t.end()
+ })
+ })
+
+ // client should send the command
+ const client = createClient({ io: socketClient, port: port })
+ client.connect()
+ client.send(mockType, mockPayload)
+ })
+})
diff --git a/packages/reactotron-core-client/test/start-timer-test.js b/packages/reactotron-core-client/test/start-timer-test.js
new file mode 100644
index 000000000..3da68f27b
--- /dev/null
+++ b/packages/reactotron-core-client/test/start-timer-test.js
@@ -0,0 +1,10 @@
+import test from 'ava'
+import { createClient } from '../src'
+import io from './_fake-io'
+
+test('has a startTimer function', t => {
+ const client = createClient({ io })
+ t.is(typeof client.startTimer, 'function')
+ const elapsed = client.startTimer()
+ t.is(typeof elapsed, 'function')
+})
diff --git a/packages/reactotron-core-client/test/stopwatch-test.js b/packages/reactotron-core-client/test/stopwatch-test.js
new file mode 100644
index 000000000..8712209cd
--- /dev/null
+++ b/packages/reactotron-core-client/test/stopwatch-test.js
@@ -0,0 +1,32 @@
+import test from 'ava'
+import { start } from '../src/stopwatch'
+
+// aim for 30 fps -- even though fps has nothing to do with anything at all
+// const TICK = 1.0 / 30.0
+
+// a promise based delay that's not accurate at all
+// const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
+
+test('start returns the right interface', t => {
+ const elapsed = start()
+ t.is(typeof elapsed, 'function')
+})
+//
+// // using >= because on node there's event loop overhead and new Date() instantiation
+// test('gives us milliseconds', async t => {
+// const elapsed = start()
+// t.true(elapsed() <= 0.1) // just a sanity test to make sure i'm not the problem
+// await delay(TICK)
+// t.true(elapsed() >= TICK)
+// await delay(TICK)
+// t.true(elapsed() >= TICK * 2)
+// })
+//
+// // here's a callback version
+// test.cb('gives us milliseconds', t => {
+// const elapsed = start()
+// setTimeout(() => {
+// t.true(elapsed() >= TICK)
+// t.end()
+// }, TICK)
+// })
diff --git a/packages/reactotron-core-server/.babelrc b/packages/reactotron-core-server/.babelrc
new file mode 100644
index 000000000..a10f8ce73
--- /dev/null
+++ b/packages/reactotron-core-server/.babelrc
@@ -0,0 +1,4 @@
+{
+ "presets": [ "es2015", "stage-0" ],
+ "plugins": [ "transform-decorators-legacy", "transform-runtime", "transform-async-to-generator" ]
+}
diff --git a/packages/reactotron-core-server/.editorconfig b/packages/reactotron-core-server/.editorconfig
new file mode 100644
index 000000000..ea02fa4e5
--- /dev/null
+++ b/packages/reactotron-core-server/.editorconfig
@@ -0,0 +1,7 @@
+root = true
+
+[*]
+end_of_line = lf
+charset = utf-8
+indent_style = space
+tab_width = 2
diff --git a/packages/reactotron-core-server/.gitignore b/packages/reactotron-core-server/.gitignore
new file mode 100644
index 000000000..a78e18fe1
--- /dev/null
+++ b/packages/reactotron-core-server/.gitignore
@@ -0,0 +1,5 @@
+node_modules
+npm-debug.log
+coverage
+.nyc_output
+dist
diff --git a/packages/reactotron-core-server/LICENSE b/packages/reactotron-core-server/LICENSE
new file mode 100644
index 000000000..b36092b50
--- /dev/null
+++ b/packages/reactotron-core-server/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 Steve Kellock
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/reactotron-core-server/README.md b/packages/reactotron-core-server/README.md
new file mode 100644
index 000000000..d2f25604d
--- /dev/null
+++ b/packages/reactotron-core-server/README.md
@@ -0,0 +1,61 @@
+# reactotron-core-server
+
+This provides the core functionality of the servers allowing it talk to talk to the client.
+
+It is used by `reactotron-app` and `reactotron-cli`.
+
+# Usage
+
+```js
+import { createServer } from 'reactotron-core-server'
+
+// configure a server
+const server = createServer({
+ port: 9090, // default
+
+ onStart: () => null, // fires when we start the server
+ onStop: () => null, // fires when we stop the server
+ onConnect: ({ id, address }) => null, // fires when a client connects
+ onDisconnect: ({ id, address }) => null, // fires when a client disconnects
+
+ // a handler that fires whenever a message is received
+ onCommand: ({type, payload, messageId, date}) => {
+ switch (type) {
+ case 'hello.client':
+ break
+ case 'log':
+ break
+ case 'state.action.complete':
+ break
+ case 'state.values.response':
+ break
+ case 'state.keys.response':
+ break
+ case 'state.values.change':
+ break
+ case 'api.response':
+ break
+ case 'bench.report':
+ break
+ }
+ }
+})
+
+// start the server
+server.start()
+
+// say hello when we connect (this is automatic, you don't send this)
+server.send('hello.server', {})
+
+// request some values from state
+server.send('state.values.request', { path: 'user.givenName' })
+
+// request some keys from state
+server.send('state.keys.request', { path: 'user' })
+
+// subscribe to some state paths so when then change, we get notified
+server.send('state.values.subscribe', { paths: ['user.givenName', 'user'] })
+
+// stop the server
+server.stop()
+```
diff --git a/packages/reactotron-core-server/package.json b/packages/reactotron-core-server/package.json
new file mode 100644
index 000000000..3688863e2
--- /dev/null
+++ b/packages/reactotron-core-server/package.json
@@ -0,0 +1,60 @@
+{
+ "name": "reactotron-core-server",
+ "version": "0.94.0",
+ "description": "Grants Reactotron servers the ability to talk to a Reactotron client.",
+ "main": "dist/index.js",
+ "scripts": {
+ "test": "ava",
+ "watch": "ava --watch",
+ "coverage": "nyc ava",
+ "build": "rollup -c"
+ },
+ "repository": "https://github.com/reactotron/reactotron/tree/master/packages/reactotron-core-server",
+ "author": "Steve Kellock",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/reactotron/reactotron/issues"
+ },
+ "homepage": "https://reactotron.com",
+ "files": [
+ "dist",
+ "LICENSE",
+ "README.md"
+ ],
+ "devDependencies": {
+ "ava": "^0.16.0",
+ "babel-core": "^6.13.2",
+ "babel-eslint": "^6.1.2",
+ "babel-plugin-transform-async-to-generator": "^6.8.0",
+ "babel-plugin-transform-decorators-legacy": "^1.3.4",
+ "babel-plugin-transform-runtime": "^6.9.0",
+ "babel-preset-es2015": "^6.13.2",
+ "babel-preset-es2015-rollup": "^1.2.0",
+ "babel-preset-stage-0": "^6.5.0",
+ "nyc": "^8.1.0",
+ "standard": "^7.1.2",
+ "rollup": "^0.34.8",
+ "rollup-plugin-babel": "^2.6.1"
+ },
+ "ava": {
+ "require": [
+ "babel-core/register"
+ ],
+ "babel": {
+ "babelrc": false,
+ "presets": [
+ "es2015",
+ "stage-1"
+ ]
+ }
+ },
+ "standard": {
+ "parser": "babel-eslint"
+ },
+ "dependencies": {
+ "mobx": "^2.4.3",
+ "ramda": "^0.22.1",
+ "ramdasauce": "^1.1.0",
+ "socket.io": "^1.4.8"
+ }
+}
diff --git a/packages/reactotron-core-server/rollup.config.js b/packages/reactotron-core-server/rollup.config.js
new file mode 100644
index 000000000..5fd6a3145
--- /dev/null
+++ b/packages/reactotron-core-server/rollup.config.js
@@ -0,0 +1,15 @@
+import babel from 'rollup-plugin-babel'
+
+export default {
+ entry: 'src/index.js',
+ format: 'cjs',
+ plugins: [
+ babel({
+ babelrc: false,
+ presets: ['es2015-rollup', 'stage-1'],
+ plugins: ['transform-decorators-legacy']
+ })
+ ],
+ dest: 'dist/index.js',
+ exports: 'named'
+}
diff --git a/packages/reactotron-core-server/src/commands.js b/packages/reactotron-core-server/src/commands.js
new file mode 100644
index 000000000..6dd433333
--- /dev/null
+++ b/packages/reactotron-core-server/src/commands.js
@@ -0,0 +1,62 @@
+import { observable, action, extendObservable, asFlat } from 'mobx'
+import R from 'ramda'
+import CommandTypes from './types'
+
+// how many commands we're allowed to have stored at one time (per-list)
+export const DEFAULT_MAXIMUM_LIST_SIZE = 100
+export const ALL_MAXIMUM_LIST_SIZE = 1000
+
+/**
+ * Holds the lists of commands.
+ *
+ * For example: this['log'] gets the list of log commands.
+ */
+class Commands {
+
+ /**
+ * It's all the comands. Like all of them.
+ */
+ @observable all = asFlat([])
+
+ /**
+ * Constructor with an optional overrideable max list size.
+ */
+ constructor (maximumListSize = DEFAULT_MAXIMUM_LIST_SIZE, allMaximumListSize = ALL_MAXIMUM_LIST_SIZE) {
+ // create an observable list for each (named after the type)
+ R.forEach(type => {
+ extendObservable(this, {
+ [type]: asFlat([])
+ })
+ // this[type] = observable([])
+ }, CommandTypes)
+ this.maximumListSize = maximumListSize
+ this.allMaximumListSize = allMaximumListSize
+ }
+
+ /**
+ * Here's action. Put it in the right list please.
+ */
+ @action addCommand (command) {
+ // which command type?
+ const { type } = command
+ // grab that list
+ const list = this[type]
+ // but if we can't, jet
+ if (R.isNil(list)) return
+ // add this to the all list
+ this.all.push(command)
+ // enforce the all cap
+ if (this.all.length > this.allMaximumListSize) {
+ this.all.shift()
+ }
+ // add this new one on the end
+ list.push(command)
+ // shift off the head of the list if we've filled up
+ if (list.length > this.maximumListSize) {
+ list.shift()
+ }
+ }
+
+}
+
+export default Commands
diff --git a/packages/reactotron-core-server/src/index.js b/packages/reactotron-core-server/src/index.js
new file mode 100644
index 000000000..9442d98ce
--- /dev/null
+++ b/packages/reactotron-core-server/src/index.js
@@ -0,0 +1,232 @@
+import { merge, length, find, propEq, without, contains, forEach, pluck, reject } from 'ramda'
+import Commands from './commands'
+import validate from './validation'
+import { observable, computed, asFlat } from 'mobx'
+import socketIO from 'socket.io'
+
+const DEFAULTS = {
+ port: 9090, // the port to live (required)
+ onCommand: command => null, // handles inbound commands
+ onStart: () => null, // handles inbound commands
+ onStop: () => null, // handles inbound commands
+ onConnect: connection => null, // notify connections
+ onDisconnect: connection => null // notify disconnections
+}
+
+class Server {
+
+ // the configuration options
+ @observable options = merge({}, DEFAULTS)
+ started = false
+ messageId = 0
+ subscriptions = []
+ partialConnections = []
+ io
+
+ /**
+ * Holds the commands the client has sent.
+ */
+ commands = new Commands()
+
+ /**
+ * Holds the currently connected clients.
+ */
+ @observable connections = asFlat([])
+
+ /**
+ * How many people are connected?
+ */
+ @computed get connectionCount () {
+ return length(this.connections)
+ }
+
+ constructor (createTransport) {
+ this.send = this.send.bind(this)
+ }
+
+ findConnectionById = id => find(propEq('id', id), this.connections)
+ findPartialConnectionById = id => find(propEq('id', id), this.partialConnections)
+
+ /**
+ * Set the configuration options.
+ */
+ configure (options = {}) {
+ // options get merged & validated before getting set
+ const newOptions = merge(this.options, options)
+ validate(newOptions)
+ this.options = newOptions
+ return this
+ }
+
+ /**
+ * Starts the server
+ */
+ start () {
+ this.started = true
+ const { port, onStart } = this.options
+ const { onCommand, onConnect, onDisconnect } = this.options
+
+ // start listening
+ this.io = socketIO(port)
+
+ // register events
+ this.io.on('connection', socket => {
+ // a wild client appears
+ const partialConnection = {
+ id: socket.id,
+ address: socket.request.connection.remoteAddress,
+ socket
+ }
+
+ // tuck them away in a "almost connected status"
+ this.partialConnections.push(partialConnection)
+
+ // trigger onConnect
+ onConnect(partialConnection)
+
+ // when this client disconnects
+ socket.on('disconnect', () => {
+ onDisconnect(socket.id)
+ // remove them from the list partial list
+ this.partialConnections = reject(propEq('id', socket.id), this.partialConnections)
+
+ // remove them from the main connections list
+ const severingConnection = find(propEq('id', socket.id), this.connections)
+ if (severingConnection) {
+ this.connections.remove(severingConnection)
+ onDisconnect && onDisconnect(severingConnection)
+ }
+ })
+
+ // when we receive a command from the client
+ socket.on('command', ({ type, important, payload }) => {
+ this.messageId++
+ const date = new Date()
+ const fullCommand = { type, important, payload, messageId: this.messageId, date }
+
+ // for client intros
+ if (type === 'client.intro') {
+ // find them in the partial connection list
+ const partialConnection = find(propEq('id', socket.id), this.partialConnections)
+
+ // add their address in
+ fullCommand.payload.address = partialConnection.address
+
+ // remove them from the partial connections list
+ this.partialConnections = reject(propEq('id', socket.id), this.partialConnections)
+
+ // bestow the payload onto the connection
+ const connection = merge(payload, { id: socket.id, address: partialConnection.address })
+
+ // then trigger the connection
+ this.connections.push(connection)
+ }
+
+ // refresh subscriptions
+ if (type === 'state.values.change') {
+ this.subscriptions = pluck('path', payload.changes || [])
+ }
+
+ this.commands.addCommand(fullCommand)
+ onCommand(fullCommand)
+ })
+
+ // resend the subscriptions to the client upon connecting
+ this.stateValuesSendSubscriptions()
+ })
+
+ // trigger the start message
+ onStart && onStart()
+
+ return this
+ }
+
+ /**
+ * Stops the server
+ */
+ stop () {
+ const { onStop } = this.options
+ this.started = false
+ forEach(s => s && s.connected && s.disconnect(), pluck('socket', this.connections))
+ this.io.close()
+
+ // trigger the stop message
+ onStop && onStop()
+
+ return this
+ }
+
+ /**
+ * Sends a command to the client
+ */
+ send (type, payload) {
+ this.io.sockets.emit('command', { type, payload })
+ }
+
+ /**
+ * Sends a request to the client for state values.
+ */
+ stateValuesRequest (path) {
+ this.send('state.values.request', { path })
+ }
+
+ /**
+ * Sends a request to the client for keys for a state object.
+ */
+ stateKeysRequest (path) {
+ this.send('state.keys.request', { path })
+ }
+
+ /**
+ * Dispatches an action through to the state.
+ */
+ stateActionDispatch (action) {
+ this.send('state.action.dispatch', { action })
+ }
+
+ /**
+ * Sends a list of subscribed paths to the client for state subscription.
+ */
+ stateValuesSendSubscriptions () {
+ this.send('state.values.subscribe', { paths: this.subscriptions })
+ }
+
+ /**
+ * Subscribe to a path in the client's state.
+ */
+ stateValuesSubscribe (path) {
+ // prevent duplicates
+ if (contains(path, this.subscriptions)) return
+ // subscribe
+ this.subscriptions.push(path)
+ this.stateValuesSendSubscriptions()
+ }
+
+ /**
+ * Unsubscribe from this path.
+ */
+ stateValuesUnsubscribe (path) {
+ // if it doesn't exist, jet
+ if (!contains(path, this.subscriptions)) return
+ this.subscriptions = without([path], this.subscriptions)
+ this.stateValuesSendSubscriptions()
+ }
+
+ /**
+ * Clears the subscriptions.
+ */
+ stateValuesClearSubscriptions () {
+ this.subscriptions = []
+ this.stateValuesSendSubscriptions()
+ }
+
+}
+
+export default Server
+
+// convenience factory function
+export const createServer = (options) => {
+ const server = new Server()
+ server.configure(options)
+ return server
+}
diff --git a/packages/reactotron-core-server/src/types.js b/packages/reactotron-core-server/src/types.js
new file mode 100644
index 000000000..04adadc60
--- /dev/null
+++ b/packages/reactotron-core-server/src/types.js
@@ -0,0 +1,12 @@
+// the list of command types we support
+export default [
+ 'client.intro',
+ 'log',
+ 'state.action.complete',
+ 'state.values.response',
+ 'state.keys.response',
+ 'state.values.change',
+ 'api.response',
+ 'benchmark.report',
+ 'display'
+]
diff --git a/packages/reactotron-core-server/src/validation.js b/packages/reactotron-core-server/src/validation.js
new file mode 100644
index 000000000..febdb6f64
--- /dev/null
+++ b/packages/reactotron-core-server/src/validation.js
@@ -0,0 +1,29 @@
+import R from 'ramda'
+import RS from 'ramdasauce'
+
+/**
+ * Is this a valid port?
+ */
+const isPortValid = R.allPass([
+ R.complement(R.isNil),
+ R.is(Number),
+ RS.isWithin(1, 65535)
+])
+
+/**
+ * Is this a valid command?
+ */
+const isOnCommandValid = R.allPass([
+ R.complement(R.isNil)
+])
+
+/**
+ * Ensures the options are sane to run this baby. Throw if not. These
+ * are basically sanity checks.
+ */
+export default options => {
+ const { port, onCommand } = options
+
+ if (!isPortValid(port)) throw new Error('invalid port')
+ if (!isOnCommandValid(onCommand)) throw new Error('onCommand is required')
+}
diff --git a/packages/reactotron-core-server/test/_get-free-port.js b/packages/reactotron-core-server/test/_get-free-port.js
new file mode 100644
index 000000000..fade86c5f
--- /dev/null
+++ b/packages/reactotron-core-server/test/_get-free-port.js
@@ -0,0 +1,9 @@
+import net from 'net'
+
+export default function getFreePort (cb) {
+ const server = net.createServer()
+ server.listen(() => {
+ const port = server.address().port
+ server.close(() => cb(port))
+ })
+}
diff --git a/packages/reactotron-core-server/test/_socket-client.js b/packages/reactotron-core-server/test/_socket-client.js
new file mode 100644
index 000000000..f925eadcc
--- /dev/null
+++ b/packages/reactotron-core-server/test/_socket-client.js
@@ -0,0 +1,4 @@
+import ioClient from 'socket.io-client'
+
+export default (address) =>
+ ioClient(address, { jsonp: false, transports: ['websocket'] })
diff --git a/packages/reactotron-core-server/test/commands-test.js b/packages/reactotron-core-server/test/commands-test.js
new file mode 100644
index 000000000..166e13b39
--- /dev/null
+++ b/packages/reactotron-core-server/test/commands-test.js
@@ -0,0 +1,47 @@
+import test from 'ava'
+import Commands from '../src/commands'
+import CommandTypes from '../src/types'
+import { length, map, forEach } from 'ramda'
+
+test('has the right set of lists', t => {
+ const c = new Commands()
+ t.plan(length(CommandTypes))
+ forEach(key => t.truthy(c[key]), CommandTypes)
+})
+
+test('added to the right list', t => {
+ t.plan(3 * length(CommandTypes) + 1)
+ const c = new Commands()
+ map(type => {
+ const action = { type }
+ t.is(c[type].length, 0)
+ c.addCommand(action)
+ t.is(c[type].length, 1)
+ t.deepEqual(c[type][0], action)
+ }, CommandTypes)
+ t.is(c.all.length, length(CommandTypes))
+})
+
+test('enforces a max list size', t => {
+ t.plan(9)
+ const type = 'log'
+ // set a low cap for testing
+ const c = new Commands(1, 1)
+
+ // starts empty
+ t.is(c[type].length, 0)
+ t.is(c.all.length, 0)
+
+ // adds 1 as normal
+ c.addCommand({ type, payload: 1 })
+ t.is(c[type].length, 1)
+ t.is(c.all.length, 1)
+ t.deepEqual(c[type][0], { type, payload: 1 })
+
+ // now add another and ensure the first is pushed off
+ c.addCommand({ type, payload: 2 })
+ t.is(c[type].length, 1)
+ t.is(c.all.length, 1)
+ t.deepEqual(c[type][0], { type, payload: 2 })
+ t.deepEqual(c.all[0], { type, payload: 2 })
+})
diff --git a/packages/reactotron-core-server/test/configure-test.js b/packages/reactotron-core-server/test/configure-test.js
new file mode 100644
index 000000000..1b63bc741
--- /dev/null
+++ b/packages/reactotron-core-server/test/configure-test.js
@@ -0,0 +1,19 @@
+import test from 'ava'
+import { createServer } from '../src'
+
+test('has defaults', t => {
+ const client = createServer({})
+ t.is(client.options.port, 9090)
+})
+
+test('options can be overridden', t => {
+ const server = createServer({ port: 1 })
+ t.is(server.options.port, 1)
+})
+
+test('port is required', t => {
+ t.throws(() => createServer({ port: null }))
+ t.throws(() => createServer({ port: undefined }))
+ t.throws(() => createServer({ port: 0 }))
+ t.throws(() => createServer({ port: 65536 }))
+})
diff --git a/packages/reactotron-core-server/test/connections-test.js b/packages/reactotron-core-server/test/connections-test.js
new file mode 100644
index 000000000..cef28801d
--- /dev/null
+++ b/packages/reactotron-core-server/test/connections-test.js
@@ -0,0 +1,25 @@
+import test from 'ava'
+import getFreePort from './_get-free-port'
+import { createServer } from '../src'
+import socketClient from './_socket-client'
+
+test.cb('keeps track of connections', t => {
+ getFreePort(port => {
+ const server = createServer({
+ port,
+ onCommand: ({type, payload}) => {
+ t.is(server.connections.length, 1)
+ t.end()
+ }
+ }).start()
+
+ // start empty
+ t.is(server.connections.length, 0)
+
+ // add 1
+ const client1 = socketClient(`ws://localhost:${port}`)
+ client1.on('connect', () => {
+ client1.emit('command', {type: 'client.intro', payload: {}})
+ })
+ })
+})
diff --git a/packages/reactotron-core-server/test/create-server-test.js b/packages/reactotron-core-server/test/create-server-test.js
new file mode 100644
index 000000000..d0e05df26
--- /dev/null
+++ b/packages/reactotron-core-server/test/create-server-test.js
@@ -0,0 +1,7 @@
+import test from 'ava'
+import { createServer } from '../src'
+
+test('returns a server', t => {
+ const server = createServer()
+ t.truthy(server)
+})
diff --git a/packages/reactotron-core-server/test/on-command-test.js b/packages/reactotron-core-server/test/on-command-test.js
new file mode 100644
index 000000000..fd5f07506
--- /dev/null
+++ b/packages/reactotron-core-server/test/on-command-test.js
@@ -0,0 +1,31 @@
+import test from 'ava'
+import getFreePort from './_get-free-port'
+import { createServer } from '../src'
+import socketClient from './_socket-client'
+
+test.cb('receives a valid command', t => {
+ getFreePort(port => {
+ const mockCommand = {
+ type: 'hello.client',
+ payload: { hi: 'there' }
+ }
+
+ const server = createServer({
+ port,
+ onCommand: ({ type, payload, messageId, date }) => {
+ t.is(type, mockCommand.type)
+ t.deepEqual(payload, mockCommand.payload)
+ t.is(messageId, 1)
+ t.truthy(date)
+ t.is(server.messageId, 1)
+ t.end()
+ }
+ }).start()
+
+ // add
+ const client = socketClient(`ws://localhost:${port}`)
+ client.on('connect', () => {
+ client.emit('command', mockCommand)
+ })
+ })
+})
diff --git a/packages/reactotron-core-server/test/send-test.js b/packages/reactotron-core-server/test/send-test.js
new file mode 100644
index 000000000..ca28cf0b3
--- /dev/null
+++ b/packages/reactotron-core-server/test/send-test.js
@@ -0,0 +1,35 @@
+import test from 'ava'
+import getFreePort from './_get-free-port'
+import { createServer } from '../src'
+import socketClient from './_socket-client'
+
+const mockType = 'hello.server'
+const mockPayload = { fun: true }
+
+test.cb('sends a valid command', t => {
+ getFreePort(port => {
+ // setup a server, wait for a command, then send one
+ const server = createServer({
+ port,
+ onCommand: ({ type, payload, messageId, date }) => {
+ server.send(mockType, mockPayload)
+ }
+ }).start()
+
+ // setup the client
+ const client = socketClient(`ws://localhost:${port}`)
+
+ // when the client receives a command, validate
+ client.on('command', ({ type, payload }) => {
+ if (type === 'state.values.subscribe') return
+ t.is(type, mockType)
+ t.deepEqual(payload, mockPayload)
+ t.end()
+ })
+
+ // when the client connects, send a command trigger
+ client.on('connect', () => {
+ client.emit('command', {type: 'trigger'})
+ })
+ })
+})
diff --git a/packages/reactotron-core-server/test/start-stop-test.js b/packages/reactotron-core-server/test/start-stop-test.js
new file mode 100644
index 000000000..8c43b27b2
--- /dev/null
+++ b/packages/reactotron-core-server/test/start-stop-test.js
@@ -0,0 +1,15 @@
+import test from 'ava'
+import getFreePort from './_get-free-port'
+import { createServer } from '../src'
+
+test.cb('sets the started flag', t => {
+ getFreePort(port => {
+ const server = createServer({ port })
+ t.false(server.started)
+ server.start()
+ t.true(server.started)
+ server.stop()
+ t.false(server.started)
+ t.end()
+ })
+})
diff --git a/packages/reactotron-react-js/.babelrc b/packages/reactotron-react-js/.babelrc
new file mode 100644
index 000000000..0b08012f2
--- /dev/null
+++ b/packages/reactotron-react-js/.babelrc
@@ -0,0 +1,3 @@
+{
+ "presets": [ "es2015-rollup", "stage-1" ]
+}
diff --git a/packages/reactotron-react-js/.gitignore b/packages/reactotron-react-js/.gitignore
new file mode 100644
index 000000000..a78e18fe1
--- /dev/null
+++ b/packages/reactotron-react-js/.gitignore
@@ -0,0 +1,5 @@
+node_modules
+npm-debug.log
+coverage
+.nyc_output
+dist
diff --git a/packages/reactotron-react-js/LICENSE b/packages/reactotron-react-js/LICENSE
new file mode 100644
index 000000000..b36092b50
--- /dev/null
+++ b/packages/reactotron-react-js/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 Steve Kellock
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/reactotron-react-js/README.md b/packages/reactotron-react-js/README.md
new file mode 100644
index 000000000..a3051f584
--- /dev/null
+++ b/packages/reactotron-react-js/README.md
@@ -0,0 +1,3 @@
+# reactotron-react-js
+
+A development tool to explore, inspect, and diagnosis your React DOM/JS apps.
diff --git a/packages/reactotron-react-js/package.json b/packages/reactotron-react-js/package.json
new file mode 100644
index 000000000..93da87833
--- /dev/null
+++ b/packages/reactotron-react-js/package.json
@@ -0,0 +1,64 @@
+{
+ "name": "reactotron-react-js",
+ "version": "0.94.0",
+ "description": "A development tool to explore, inspect, and diagnosis your React JS/DOM apps.",
+ "main": "dist/index.js",
+ "scripts": {
+ "test-nope": "ava",
+ "watch": "ava --watch",
+ "coverage": "nyc ava",
+ "build": "rollup -c"
+ },
+ "repository": "https://github.com/reactotron/reactotron/tree/master/packages/reactotron-react-js",
+ "author": "Steve Kellock",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/reactotron/reactotron/issues"
+ },
+ "homepage": "https://reactotron.com",
+ "files": [
+ "dist",
+ "LICENSE",
+ "README.md"
+ ],
+ "peerDependencies": {},
+ "devDependencies": {
+ "ava": "^0.16.0",
+ "babel-cli": "^6.11.4",
+ "babel-core": "^6.11.4",
+ "babel-eslint": "^6.1.2",
+ "babel-preset-es2015-rollup": "^1.2.0",
+ "babel-preset-stage-1": "^6.5.0",
+ "babelrc-rollup": "^3.0.0",
+ "mockery": "^1.7.0",
+ "nyc": "^8.1.0",
+ "rollup": "^0.34.8",
+ "rollup-plugin-babel": "^2.6.1",
+ "standard": "^7.1.2"
+ },
+ "ava": {
+ "require": [
+ "babel-core/register"
+ ],
+ "babel": {
+ "babelrc": false,
+ "presets": [
+ "es2015-rollup",
+ "stage-1"
+ ]
+ }
+ },
+ "standard": {
+ "parser": "babel-eslint",
+ "globals": [
+ "__DEV__"
+ ]
+ },
+ "dependencies": {
+ "react": "^15.3.0",
+ "react-dom": "^15.3.0",
+ "stacktrace-js": "^1.3.1",
+ "socket.io": "^1.4.8",
+ "reactotron-core-client": "^0.94.0"
+ }
+}
diff --git a/packages/reactotron-react-js/rollup.config.js b/packages/reactotron-react-js/rollup.config.js
new file mode 100644
index 000000000..0afe9acdc
--- /dev/null
+++ b/packages/reactotron-react-js/rollup.config.js
@@ -0,0 +1,19 @@
+import babel from 'rollup-plugin-babel'
+import babelrc from 'babelrc-rollup'
+
+const pkg = require('./package.json')
+const external = Object.keys(pkg.dependencies)
+
+export default {
+ entry: 'src/index.js',
+ format: 'cjs',
+ plugins: [
+ babel({
+ babelrc: babelrc(),
+ presets: ['es2015-rollup', 'stage-1']
+ })
+ ],
+ external,
+ dest: 'dist/index.js',
+ exports: 'named'
+}
diff --git a/packages/reactotron-react-js/src/index.js b/packages/reactotron-react-js/src/index.js
new file mode 100644
index 000000000..b21115d6e
--- /dev/null
+++ b/packages/reactotron-react-js/src/index.js
@@ -0,0 +1,22 @@
+import { createClient } from 'reactotron-core-client'
+import io from 'socket.io-client'
+export trackGlobalErrors from './plugins/track-global-errors'
+
+// ---------------------
+// DEFAULT CONFIGURATION
+// ---------------------
+
+var DEFAULTS = {
+ io,
+ host: 'localhost',
+ port: 9090,
+ name: 'React JS App',
+ userAgent: window.navigator.userAgent,
+ reactotronVersion: 'BETA' // TODO: figure this out for realz. why is this hard? it must be me.
+}
+
+// -----------
+// HERE WE GO!
+// -----------
+// Create the default reactotron.
+export default createClient(DEFAULTS)
diff --git a/packages/reactotron-react-js/src/plugins/track-global-errors.js b/packages/reactotron-react-js/src/plugins/track-global-errors.js
new file mode 100644
index 000000000..1ab04f12f
--- /dev/null
+++ b/packages/reactotron-react-js/src/plugins/track-global-errors.js
@@ -0,0 +1,75 @@
+/**
+ * Provides a global error handler to report errors with sourcemap lookup.
+ */
+import StackTrace from 'stacktrace-js'
+import { merge } from 'ramda'
+
+// what to say whe we can't resolve source maps
+const CANNOT_RESOLVE_ERROR = 'Unable to resolve error. Either support CORS by changing webpack\'s devtool to "source-maps" or run in offline mode.'
+
+// defaults
+const PLUGIN_DEFAULTS = {
+ offline: false // true = don't do source maps lookup cross domain
+}
+
+// our plugin entry point
+export default options => reactotron => {
+ // setup configuration
+ const config = merge(PLUGIN_DEFAULTS, options || {})
+
+ // holds the previous window.onerror when needed
+ let swizzledOnError = null
+ let isSwizzled = false
+
+ // the functionality of our window.onerror.
+ // we could have used window.addEventListener("error", ...) but that doesn't work on all browsers
+ function windowOnError (msg, file, line, col, error) {
+ // resolve the stack trace
+ StackTrace
+ .fromError(error, { offline: config.offline })
+ // then try to send it up to the server
+ .then(stackFrames =>
+ reactotron.error(msg, stackFrames)
+ )
+ // can't resolve, well, let the user know, but still upload something sane
+ .catch(resolvingError =>
+ reactotron.error({
+ message: CANNOT_RESOLVE_ERROR,
+ original: { msg, file, line, col, error },
+ resolvingError
+ })
+ )
+
+ // call back the previous window.onerror if we have one
+ if (swizzledOnError) {
+ swizzledOnError(msg, file, line, col, error)
+ }
+ }
+
+ // swizzles window.onerror dropping in our new one
+ function trackGlobalErrors () {
+ if (isSwizzled) return
+ swizzledOnError = window.onerror
+ window.onerror = windowOnError
+ isSwizzled = true
+ }
+
+ // restore the original
+ function untrackGlobalErrors () {
+ if (!swizzledOnError) return
+ window.onerror = swizzledOnError
+ isSwizzled = false
+ }
+
+ // auto start this
+ trackGlobalErrors()
+
+ // the reactotron plugin interface
+ return {
+ // attach these functions to the Reactotron
+ features: {
+ trackGlobalErrors,
+ untrackGlobalErrors
+ }
+ }
+}
diff --git a/packages/reactotron-react-native/.babelrc b/packages/reactotron-react-native/.babelrc
new file mode 100644
index 000000000..e66618ff6
--- /dev/null
+++ b/packages/reactotron-react-native/.babelrc
@@ -0,0 +1,3 @@
+{
+ "presets": [ "es2015", "stage-1" ]
+}
diff --git a/packages/reactotron-react-native/.gitignore b/packages/reactotron-react-native/.gitignore
new file mode 100644
index 000000000..a78e18fe1
--- /dev/null
+++ b/packages/reactotron-react-native/.gitignore
@@ -0,0 +1,5 @@
+node_modules
+npm-debug.log
+coverage
+.nyc_output
+dist
diff --git a/packages/reactotron-react-native/LICENSE b/packages/reactotron-react-native/LICENSE
new file mode 100644
index 000000000..b36092b50
--- /dev/null
+++ b/packages/reactotron-react-native/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 Steve Kellock
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/reactotron-react-native/README.md b/packages/reactotron-react-native/README.md
new file mode 100644
index 000000000..929621301
--- /dev/null
+++ b/packages/reactotron-react-native/README.md
@@ -0,0 +1,3 @@
+# reactotron-react-native
+
+A development tool to explore, inspect, and diagnosis your React Native apps.
diff --git a/packages/reactotron-react-native/package.json b/packages/reactotron-react-native/package.json
new file mode 100644
index 000000000..93a511a8a
--- /dev/null
+++ b/packages/reactotron-react-native/package.json
@@ -0,0 +1,70 @@
+{
+ "name": "reactotron-react-native",
+ "version": "0.94.0",
+ "description": "A development tool to explore, inspect, and diagnosis your React Native apps.",
+ "main": "dist/index.js",
+ "scripts": {
+ "test": "ava",
+ "watch": "ava --watch",
+ "coverage": "nyc ava",
+ "build": "rollup -c"
+ },
+ "repository": "https://github.com/reactotron/reactotron/tree/master/packages/reactotron-react-native",
+ "author": "Steve Kellock",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/reactotron/reactotron/issues"
+ },
+ "homepage": "https://reactotron.com",
+ "files": [
+ "dist",
+ "LICENSE",
+ "README.md"
+ ],
+ "peerDependencies": {
+ "react": "^15.2.1",
+ "react-native": "^0.31.0"
+ },
+ "devDependencies": {
+ "ava": "^0.16.0",
+ "babel-core": "^6.13.2",
+ "babel-eslint": "^6.1.2",
+ "babel-preset-es2015": "^6.13.2",
+ "babel-preset-es2015-rollup": "^1.2.0",
+ "babel-preset-stage-1": "^6.13.0",
+ "mockery": "^1.7.0",
+ "nyc": "^8.1.0",
+ "react-native-mock": "^0.2.5",
+ "rollup": "^0.34.8",
+ "rollup-plugin-babel": "^2.6.1",
+ "standard": "^7.1.2",
+ "react": "^15.2.1",
+ "react-native": "^0.31.0"
+ },
+ "ava": {
+ "require": [
+ "babel-core/register",
+ "react-native-mock/mock",
+ "./test/_setup"
+ ],
+ "babel": {
+ "babelrc": false,
+ "presets": [
+ "es2015",
+ "stage-1"
+ ]
+ }
+ },
+ "standard": {
+ "parser": "babel-eslint",
+ "globals": [
+ "__DEV__"
+ ]
+ },
+ "dependencies": {
+ "ramda": "^0.22.1",
+ "ramdasauce": "^1.1.0",
+ "socket.io": "^1.4.8",
+ "reactotron-core-client": "^0.94.0"
+ }
+}
diff --git a/packages/reactotron-react-native/rollup.config.js b/packages/reactotron-react-native/rollup.config.js
new file mode 100644
index 000000000..942ed86da
--- /dev/null
+++ b/packages/reactotron-react-native/rollup.config.js
@@ -0,0 +1,15 @@
+import babel from 'rollup-plugin-babel'
+
+export default {
+ entry: 'src/index.js',
+ format: 'cjs',
+ plugins: [
+ babel({
+ babelrc: false,
+ runtimeHelpers: true,
+ presets: ['es2015-rollup', 'stage-1']
+ })
+ ],
+ dest: 'dist/index.js',
+ exports: 'named'
+}
diff --git a/packages/reactotron-react-native/src/index.js b/packages/reactotron-react-native/src/index.js
new file mode 100644
index 000000000..f74d5adb8
--- /dev/null
+++ b/packages/reactotron-react-native/src/index.js
@@ -0,0 +1,56 @@
+// -----------
+// FIRST PARTY
+// -----------
+
+// import validate from './validate'
+export trackGlobalErrors from './plugins/track-global-errors'
+
+// ------------
+// SECOND PARTY
+// ------------
+
+import { createClient } from 'reactotron-core-client'
+
+// -----------
+// THIRD PARTY
+// -----------
+
+// import R from 'ramda'
+
+// -------------
+// THE HACK ZONE
+// -------------
+
+// set a userAgent manually so socket.io works.
+if (!window.navigator || !window.navigator.userAgent) {
+ window.navigator.userAgent = 'reactotron-react-native'
+}
+
+// Only then do we load socket.io. This has to be done as a require to preserve
+// the order of user agent being set first. Also, it's a var so it doesn't get
+// hoisted.
+
+var io = require('socket.io-client/socket.io')
+
+// ---------------------
+// DEFAULT CONFIGURATION
+// ---------------------
+
+const DEFAULTS = {
+ io,
+ host: 'localhost',
+ port: 9090,
+ name: 'React Native App',
+ userAgent: 'reactotron-react-native',
+ reactotronVersion: 'BETA', // TODO: figure this out for realz. why is this hard? it must be me.
+ environment: __DEV__ ? 'development' : 'production' // naive... TODO: find the right way ya lazy bum.
+}
+
+// -----------
+// HERE WE GO!
+// -----------
+// Create the default reactotron.
+const reactotron = createClient(DEFAULTS)
+
+// send it back
+export default reactotron
diff --git a/packages/reactotron-react-native/src/plugins/track-global-errors.js b/packages/reactotron-react-native/src/plugins/track-global-errors.js
new file mode 100644
index 000000000..827941ec9
--- /dev/null
+++ b/packages/reactotron-react-native/src/plugins/track-global-errors.js
@@ -0,0 +1,74 @@
+/**
+ * Provides a global error handler to report errors..
+ */
+import { merge, map, reject, contains } from 'ramda'
+import { NativeModules } from 'react-native'
+
+// defaults
+const PLUGIN_DEFAULTS = {
+ veto: null // frame -> boolean
+}
+
+// const reactNativeFrameFinder = frame => contains('/node_modules/react-native/', frame.fileName)
+
+// our plugin entry point
+export default options => reactotron => {
+ // setup configuration
+ const config = merge(PLUGIN_DEFAULTS, options || {})
+
+ let swizzled = null
+ let isSwizzled = false
+
+ function reactotronExceptionHijacker (message, prettyStack, currentExceptionID) {
+ // do Facebook's stuff first
+ swizzled(message, prettyStack, currentExceptionID)
+
+ // then convert & transport it
+ try {
+ // rewrite the stack frames to be in the format we're expecting
+ let stack = map(frame => ({
+ functionName: frame.methodName === '' ? null : frame.methodName,
+ lineNumber: frame.lineNumber,
+ columnNumber: frame.column,
+ fileName: frame.file
+ }), prettyStack)
+
+ // does the dev want us to keep each frame?
+ if (config.veto) {
+ stack = reject(config.veto, stack)
+ }
+
+ // throw it over to us
+ reactotron.error(message, stack)
+ } catch (e) {
+ // TODO: no one must ever know our dark secrets
+ }
+ }
+
+ // here's how to swizzle
+ function trackGlobalErrors () {
+ if (isSwizzled) return
+ swizzled = NativeModules.ExceptionsManager.updateExceptionMessage
+ NativeModules.ExceptionsManager.updateExceptionMessage = reactotronExceptionHijacker
+ isSwizzled = true
+ }
+
+ // restore the original
+ function untrackGlobalErrors () {
+ if (!swizzled) return
+ NativeModules.ExceptionsManager.updateExceptionMessage = swizzled
+ isSwizzled = false
+ }
+
+ // auto start this
+ trackGlobalErrors()
+
+ // the reactotron plugin interface
+ return {
+ // attach these functions to the Reactotron
+ features: {
+ trackGlobalErrors,
+ untrackGlobalErrors
+ }
+ }
+}
diff --git a/packages/reactotron-react-native/src/validate.js b/packages/reactotron-react-native/src/validate.js
new file mode 100644
index 000000000..5b5ccf578
--- /dev/null
+++ b/packages/reactotron-react-native/src/validate.js
@@ -0,0 +1,18 @@
+import R from 'ramda'
+import RS from 'ramdasauce'
+
+const isHostValid = R.allPass([R.complement(RS.isNilOrEmpty), R.is(String)])
+const isPortValid = R.allPass([R.complement(R.isNil), R.is(Number), RS.isWithin(1, 65535)])
+
+/**
+ * Ensures the options are sane to run this baby. Throw if not. These
+ * are basically sanity checks.
+ */
+const validate = (options) => {
+ const { host, port } = options
+
+ if (!isHostValid(host)) throw new Error('invalid host')
+ if (!isPortValid(port)) throw new Error('invalid port')
+}
+
+export default validate
diff --git a/packages/reactotron-react-native/test/_get-free-port.js b/packages/reactotron-react-native/test/_get-free-port.js
new file mode 100644
index 000000000..fade86c5f
--- /dev/null
+++ b/packages/reactotron-react-native/test/_get-free-port.js
@@ -0,0 +1,9 @@
+import net from 'net'
+
+export default function getFreePort (cb) {
+ const server = net.createServer()
+ server.listen(() => {
+ const port = server.address().port
+ server.close(() => cb(port))
+ })
+}
diff --git a/packages/reactotron-react-native/test/_setup.js b/packages/reactotron-react-native/test/_setup.js
new file mode 100644
index 000000000..01f024bbf
--- /dev/null
+++ b/packages/reactotron-react-native/test/_setup.js
@@ -0,0 +1,19 @@
+import mockery from 'mockery'
+
+// prep a __DEV__
+global.__DEV__ = true
+
+// prep a window
+global.window = {
+ navigator: {}
+}
+
+// We enable mockery and leave it on.
+mockery.enable()
+
+// Silence the warnings when *real* modules load... this is a change from
+// the norm. We want to opt-in instead of opt-out because not everything
+// will be mocked.
+mockery.warnOnUnregistered(false)
+
+mockery.registerMock('socket.io-client/socket.io', require('socket.io-client'))
diff --git a/packages/reactotron-react-native/test/interface-test.js b/packages/reactotron-react-native/test/interface-test.js
new file mode 100644
index 000000000..d9f9fdc58
--- /dev/null
+++ b/packages/reactotron-react-native/test/interface-test.js
@@ -0,0 +1,16 @@
+import test from 'ava'
+import reactotron from '../src'
+
+test('it\'s an object', t => {
+ t.truthy(reactotron)
+})
+
+test('spot check some of the functions', t => {
+ t.is(typeof reactotron.configure, 'function')
+ t.is(typeof reactotron.connect, 'function')
+ t.is(typeof reactotron.use, 'function')
+})
+
+test('has the right name', t => {
+ t.is(reactotron.options.name, 'React Native App')
+})
diff --git a/packages/reactotron-redux/.babelrc b/packages/reactotron-redux/.babelrc
new file mode 100644
index 000000000..e66618ff6
--- /dev/null
+++ b/packages/reactotron-redux/.babelrc
@@ -0,0 +1,3 @@
+{
+ "presets": [ "es2015", "stage-1" ]
+}
diff --git a/packages/reactotron-redux/.gitignore b/packages/reactotron-redux/.gitignore
new file mode 100644
index 000000000..a78e18fe1
--- /dev/null
+++ b/packages/reactotron-redux/.gitignore
@@ -0,0 +1,5 @@
+node_modules
+npm-debug.log
+coverage
+.nyc_output
+dist
diff --git a/packages/reactotron-redux/LICENSE b/packages/reactotron-redux/LICENSE
new file mode 100644
index 000000000..b36092b50
--- /dev/null
+++ b/packages/reactotron-redux/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 Steve Kellock
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/reactotron-redux/README.md b/packages/reactotron-redux/README.md
new file mode 100644
index 000000000..db5dfd907
--- /dev/null
+++ b/packages/reactotron-redux/README.md
@@ -0,0 +1,64 @@
+# reactotron-redux
+
+Let's plug Redux into Reactotron!
+
+Ships with middleware ready to plug into your Redux store.
+
+* Track your actions with performance metrics
+* Query your state tree
+* Subscribe to paths within your state tree watching for changes
+* Execute arbitrary actions
+
+# Installing
+
+`npm i --save-dev reactotron-redux`
+
+
+# Configuring
+
+In the file that you create your Redux store, add these two imports at the top:
+
+```js
+import Reactotron from 'reactotron-react-native'
+import createReactotronTrackingEnhancer from 'reactotron-redux'
+```
+
+Then you either add it as the 2nd parameter to Redux's `createStore` or
+you add it the list of enhancers you're composing.
+
+Here's a bigger example:
+
+```js
+
+// Here we're composing our enhancers, this example is already using the
+// `applyMiddleware` enhancer which typical in Redux apps.
+// So we just put our call to `createReactotronTrackingEnhancer` here.
+//
+// Also, we have to pass it the Reactotron we're using for our app which
+// came in up in the import section.
+const enhancers = compose(
+ applyMiddleware(logger),
+ createReactotronTrackingEnhancer(Reactotron, {
+ // optional flagging of important actions
+ isActionImportant: action => action.type === 'FORMAT_HARD_DRIVE'
+ })
+)
+
+// This creates our store (rootReducer is just from a sample app, you've
+// probably got something similar).
+const store = createStore(rootReducer, enhancers)
+```
+
+
+# Options
+
+`createReactotronTrackingEnhancer()` has a 2nd parameter. It's an object.
+
+`except` is an array of strings that match actions flowing through Redux.
+
+If you have some actions you'd rather just not see (for example, `redux-saga`)
+triggers a little bit of noise, you can suppress them:
+
+`createReactotronTrackingEnhancer(Reactotron, {
+ except: ['EFFECT_TRIGGERED', 'EFFECT_RESOLVED', 'EFFECT_REJECTED']
+})`
diff --git a/packages/reactotron-redux/package.json b/packages/reactotron-redux/package.json
new file mode 100644
index 000000000..f33b068e7
--- /dev/null
+++ b/packages/reactotron-redux/package.json
@@ -0,0 +1,62 @@
+{
+ "name": "reactotron-redux",
+ "version": "0.94.0",
+ "description": "A Reactotron plugin for Redux.",
+ "main": "dist/index.js",
+ "scripts": {
+ "test": "ava",
+ "watch": "ava --watch",
+ "coverage": "nyc ava",
+ "build": "rollup -c "
+ },
+ "repository": "https://github.com/reactotron/reactotron/tree/master/packages/reactotron-redux",
+ "author": "Steve Kellock",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/reactotron/reactotron/issues"
+ },
+ "homepage": "https://reactotron.com",
+ "files": [
+ "dist",
+ "LICENSE",
+ "README.md"
+ ],
+ "peerDependencies": {},
+ "devDependencies": {
+ "ava": "^0.16.0",
+ "babel-core": "^6.13.2",
+ "babel-eslint": "^6.1.2",
+ "babel-preset-es2015": "^6.13.2",
+ "babel-preset-es2015-rollup": "^1.2.0",
+ "babel-preset-stage-1": "^6.13.0",
+ "mockery": "^1.7.0",
+ "nyc": "^8.1.0",
+ "rollup": "^0.34.8",
+ "rollup-plugin-babel": "^2.6.1",
+ "standard": "^7.1.2",
+ "redux": "^3.5.2",
+ "ramda": "^0.22.1",
+ "ramdasauce": "^1.1.0"
+ },
+ "ava": {
+ "require": [
+ "babel-core/register"
+ ],
+ "babel": {
+ "babelrc": false,
+ "presets": [
+ "es2015",
+ "stage-1"
+ ]
+ }
+ },
+ "standard": {
+ "parser": "babel-eslint"
+ },
+ "dependencies": {
+ "redux": "^3.5.2",
+ "ramda": "^0.22.1",
+ "ramdasauce": "^1.1.0",
+ "reactotron-core-client": "^0.94.0"
+ }
+}
diff --git a/packages/reactotron-redux/rollup.config.js b/packages/reactotron-redux/rollup.config.js
new file mode 100644
index 000000000..95b71c52b
--- /dev/null
+++ b/packages/reactotron-redux/rollup.config.js
@@ -0,0 +1,14 @@
+import babel from 'rollup-plugin-babel'
+
+export default {
+ entry: 'src/index.js',
+ format: 'cjs',
+ plugins: [
+ babel({
+ babelrc: false,
+ runtimeHelpers: true,
+ presets: ['es2015-rollup', 'stage-1']
+ })
+ ],
+ dest: 'dist/index.js'
+}
diff --git a/packages/reactotron-redux/src/get-subscription-values.js b/packages/reactotron-redux/src/get-subscription-values.js
new file mode 100644
index 000000000..6dc83e1a0
--- /dev/null
+++ b/packages/reactotron-redux/src/get-subscription-values.js
@@ -0,0 +1,25 @@
+import R from 'ramda'
+import RS from 'ramdasauce'
+
+// fishes out the values for the subscriptions in state and returns them
+export default (subscriptions, state) =>
+ R.pipe(
+ R.filter(RS.endsWith('.*')),
+ R.map((key) => {
+ const keyMinusWildcard = R.slice(0, -2, key)
+ const value = RS.dotPath(keyMinusWildcard, state)
+ if (R.is(Object, value) && !RS.isNilOrEmpty(value)) {
+ return R.pipe(
+ R.keys,
+ R.map((key) => `${keyMinusWildcard}.${key}`)
+ )(value)
+ }
+ return []
+ }),
+ R.concat(subscriptions),
+ R.flatten,
+ R.reject(RS.endsWith('.*')),
+ R.uniq,
+ R.sortBy(R.identity),
+ R.map(key => ({ path: key, value: RS.dotPath(key, state) }))
+ )(subscriptions)
diff --git a/packages/reactotron-redux/src/index.js b/packages/reactotron-redux/src/index.js
new file mode 100644
index 000000000..57c625da4
--- /dev/null
+++ b/packages/reactotron-redux/src/index.js
@@ -0,0 +1 @@
+export default from './store-enhancer'
diff --git a/packages/reactotron-redux/src/keys-request.js b/packages/reactotron-redux/src/keys-request.js
new file mode 100644
index 000000000..f1576c26d
--- /dev/null
+++ b/packages/reactotron-redux/src/keys-request.js
@@ -0,0 +1,12 @@
+import R from 'ramda'
+import RS from 'ramdasauce'
+
+// sends the key names at the given location
+export default (state, reactotron, path) => {
+ if (RS.isNilOrEmpty(path)) {
+ reactotron.stateKeysResponse(null, R.keys(state))
+ } else {
+ const keys = R.keys(RS.dotPath(path, state))
+ reactotron.stateKeysResponse(path, keys)
+ }
+}
diff --git a/packages/reactotron-redux/src/plugin.js b/packages/reactotron-redux/src/plugin.js
new file mode 100644
index 000000000..aa38220dc
--- /dev/null
+++ b/packages/reactotron-redux/src/plugin.js
@@ -0,0 +1,79 @@
+import R from 'ramda'
+import requestKeys from './keys-request'
+import requestValues from './values-request'
+import getSubscriptionValues from './get-subscription-values'
+
+const createPlugin = (store, pluginConfig = {}) => {
+ // hold onto the send
+ let capturedSend
+
+ // which subscribed paths we're current listening to
+ let subscriptions = []
+
+ // here's the plugin
+ const plugin = reactotron => {
+ // remember the plugin's send function for use in the report() below. :(
+ capturedSend = reactotron.send
+
+ const sendSubscriptions = () => {
+ const changes = getSubscriptionValues(subscriptions, store.getState())
+ reactotron.stateValuesChange(changes)
+ }
+
+ const sendSubscriptionsIfNeeded = () => {
+ const changes = getSubscriptionValues(subscriptions, store.getState())
+ if (!R.isEmpty(changes)) {
+ sendSubscriptions()
+ }
+ }
+
+ store.subscribe(sendSubscriptionsIfNeeded)
+
+ return {
+ // fires
+ onCommand: ({type, payload}) => {
+ switch (type) {
+ // client is asking for keys
+ case 'state.keys.request':
+ return requestKeys(store.getState(), reactotron, payload.path)
+
+ // client is asking for values
+ case 'state.values.request':
+ return requestValues(store.getState(), reactotron, payload.path)
+
+ // client is asking to subscribe to some paths
+ case 'state.values.subscribe':
+ subscriptions = R.pipe(R.flatten, R.uniq)(payload.paths)
+ sendSubscriptions()
+ return
+
+ // server is asking to dispatch this action
+ case 'state.action.dispatch':
+ store.dispatch(payload.action)
+ return
+
+ }
+ }
+ }
+ }
+
+ // attach a function that we can call from the enhancer
+ plugin.report = (action, ms, important = false) => {
+ if (!capturedSend) return
+
+ // let's call the type, name because that's "generic" name in Reactotron
+ let { type: name } = action
+
+ // convert from symbol to type if necessary
+ if (typeof name === 'symbol') {
+ name = name.toString().replace(/^Symbol\(/, '').replace(/\)$/, '')
+ }
+
+ // off ya go!
+ capturedSend('state.action.complete', { name, action, ms }, important)
+ }
+
+ return plugin
+}
+
+export default createPlugin
diff --git a/packages/reactotron-redux/src/store-enhancer.js b/packages/reactotron-redux/src/store-enhancer.js
new file mode 100644
index 000000000..7fca5d92a
--- /dev/null
+++ b/packages/reactotron-redux/src/store-enhancer.js
@@ -0,0 +1,67 @@
+import R from 'ramda'
+import createPlugin from './plugin'
+
+const DEFAULTS = {
+ // except: [] // which actions
+}
+
+/**
+ * Create the enhancer.
+ *
+ * @params {Object} reactotron - The Reactotron instance to which we're attaching.
+ * @params {Object} enhancerOptions - The options for the enhancer.
+ */
+const createReactotronStoreEnhancer = (reactotron, enhancerOptions = {}) => {
+ // verify reactotron
+ if (!(R.is(Object, reactotron) && typeof reactotron.use === 'function')) {
+ throw new Error('invalid reactotron passed')
+ }
+
+ // assemble a crack team of options to use
+ const options = R.merge(DEFAULTS, enhancerOptions)
+
+ // an enhancer is a function that returns a store
+ const reactotronEnhancer = createStore => (reducer, initialState, enhancer) => {
+ // the store to create
+ const store = createStore(reducer, initialState, enhancer)
+
+ // swizzle the current dispatch
+ const originalDispatch = store.dispatch
+
+ // create our dispatch
+ const dispatch = action => {
+ // start a timer
+ const elapsed = reactotron.startTimer()
+
+ // call the original dispatch that actually does the real work
+ const result = originalDispatch(action)
+
+ // stop the timer
+ const ms = elapsed()
+
+ // action not blacklisted?
+ if (!R.contains(action.type, options.except || [])) {
+ // check if the app considers this important
+ let important = false
+ if (enhancerOptions && typeof enhancerOptions.isActionImportant === 'function') {
+ important = !!enhancerOptions.isActionImportant(action)
+ }
+
+ plugin.report(action, ms, important)
+ }
+
+ // return the real work's result
+ return result
+ }
+ const newStore = R.merge(store, { dispatch: dispatch.bind(store) })
+ const plugin = createPlugin(newStore)
+ reactotron.use(plugin)
+
+ // send the store back, but with our our dispatch
+ return newStore
+ }
+
+ return reactotronEnhancer
+}
+
+export default createReactotronStoreEnhancer
diff --git a/packages/reactotron-redux/src/values-request.js b/packages/reactotron-redux/src/values-request.js
new file mode 100644
index 000000000..0edd52898
--- /dev/null
+++ b/packages/reactotron-redux/src/values-request.js
@@ -0,0 +1,12 @@
+import RS from 'ramdasauce'
+
+// sends the values at the given location
+export default (state, reactotron, path) => {
+ if (RS.isNilOrEmpty(path)) {
+ // send the whole damn tree
+ reactotron.stateValuesResponse(null, state)
+ } else {
+ // send a leaf of the tree
+ reactotron.stateValuesResponse(path, RS.dotPath(path, state))
+ }
+}
diff --git a/packages/reactotron-redux/src/values-subscribe.js b/packages/reactotron-redux/src/values-subscribe.js
new file mode 100644
index 000000000..4955f1e5f
--- /dev/null
+++ b/packages/reactotron-redux/src/values-subscribe.js
@@ -0,0 +1,5 @@
+import R from 'ramda'
+
+// subscribe to a new set of paths
+export default (subscriptions, state, reactotron, paths = []) =>
+ R.pipe(R.flatten, R.uniq)(paths)
diff --git a/packages/reactotron-redux/test/_fake-io.js b/packages/reactotron-redux/test/_fake-io.js
new file mode 100644
index 000000000..83148e926
--- /dev/null
+++ b/packages/reactotron-redux/test/_fake-io.js
@@ -0,0 +1,6 @@
+export default (x) => {
+ return {
+ on: (command, callback) => true,
+ emit: () => true
+ }
+}
diff --git a/packages/reactotron-redux/test/dispatch-test.js b/packages/reactotron-redux/test/dispatch-test.js
new file mode 100644
index 000000000..5b7888ad6
--- /dev/null
+++ b/packages/reactotron-redux/test/dispatch-test.js
@@ -0,0 +1,21 @@
+import test from 'ava'
+import reduxotron from '../src/plugin'
+
+test.cb('handles incoming dispatch calls', t => {
+ t.plan(1)
+ const store = {
+ subscribe: () => {},
+ dispatch: action => {
+ t.deepEqual(action, { a: 1, b: false })
+ t.end()
+ }
+ }
+ const reactotron = {
+ send: (type, payload) => {},
+ stateValuesChange: (paths) => null
+ }
+ const createPlugin = reduxotron(store)
+ const plugin = createPlugin(reactotron)
+ const command = { type: 'state.action.dispatch', payload: { action: { a: 1, b: false } } }
+ plugin.onCommand(command)
+})
diff --git a/packages/reactotron-redux/test/store-enhancer-test.js b/packages/reactotron-redux/test/store-enhancer-test.js
new file mode 100644
index 000000000..7f00f06b4
--- /dev/null
+++ b/packages/reactotron-redux/test/store-enhancer-test.js
@@ -0,0 +1,76 @@
+import test from 'ava'
+import createEnhancer from '../src/store-enhancer'
+import { createClient, CorePlugins } from 'reactotron-core-client'
+import { createStore, applyMiddleware, compose, combineReducers } from 'redux'
+
+test('detect invalid reactotron client', t => {
+ t.throws(() => {
+ createEnhancer()
+ })
+})
+
+test('tests pretty much everything', t => {
+ // these guys will hold the values of the command jumping the wire
+ let capturedType
+ let capturedPayload
+ let capturedImportant = false
+ let importantCount = 0
+
+ // create our own socket.io which captures the contents of emit
+ const io = x => {
+ return {
+ on: (command, callback) => true,
+ emit: (command, { type, payload, important }) => {
+ capturedType = type
+ capturedPayload = payload
+ capturedImportant = important
+ }
+ }
+ }
+
+ // test the important callback
+ const isActionImportant = (action) => {
+ importantCount++
+ return true
+ }
+
+ // grab the enhancer
+ const client = createClient({ io, plugins: CorePlugins })
+ const enhancer = createEnhancer(client, { isActionImportant })
+ t.is(typeof enhancer, 'function')
+
+ // things to make sure our internal middleware chains dispatch properly
+ const initialState = { i: 7 }
+ const fun = (state = initialState, action) => action.type === 'add' ? { ...state, i: state.i + 1 } : state
+ const rootReducer = combineReducers({ fun })
+ const action = { type: 'add', foo: 'bar' }
+
+ // custom middleware to ensure the middleware chaining still works
+ let sideEffect = false
+ const uglyMiddleware = store => next => action => {
+ sideEffect = true
+ return next(action)
+ }
+ const enhancers = compose(applyMiddleware(uglyMiddleware), enhancer)
+ const store = createStore(rootReducer, enhancers)
+
+ // ready to go! let's do this!
+ client.connect()
+
+ // fire the event through
+ store.dispatch(action)
+
+ // do reducers still work?
+ t.is(store.getState().fun.i, 8)
+
+ // does middleware still work?
+ t.true(sideEffect)
+
+ // did we attempt to deliver the command to the server?
+ t.is(capturedType, 'state.action.complete')
+ t.is(capturedPayload.name, 'add')
+ t.deepEqual(capturedPayload.action, action)
+ t.true(capturedPayload.ms >= 0)
+ t.is(importantCount, 1)
+ t.true(capturedImportant)
+})
diff --git a/packages/reactotron-redux/test/test-plugin.js b/packages/reactotron-redux/test/test-plugin.js
new file mode 100644
index 000000000..62be1d74c
--- /dev/null
+++ b/packages/reactotron-redux/test/test-plugin.js
@@ -0,0 +1,18 @@
+import test from 'ava'
+import reduxotron from '../src/plugin'
+
+test('has the right interface', t => {
+ const store = {
+ subscribe: () => {}
+ }
+ const reactotron = {
+ send: (type, payload) => {},
+ stateValuesChange: (paths) => null
+ }
+ t.is(typeof reduxotron, 'function')
+ const createPlugin = reduxotron(store)
+ t.is(typeof createPlugin, 'function')
+ t.is(typeof createPlugin.report, 'function')
+ const plugin = createPlugin(reactotron)
+ t.is(typeof plugin.onCommand, 'function')
+})
diff --git a/readme.md b/readme.md
new file mode 100644
index 000000000..ed42012eb
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,52 @@
+# ![CLI](./docs/images/readme/Reactotron-64.png) Reactotron
+
+Reactotron is a developer tool which allows you to peak under the kilt of your [React JS](https://facebook.github.io/react/) and [React Native](https://facebook.github.io/react-native/) apps.
+
+
+Use it to:
+
+* view your application state
+* show API requests & responses
+* perform quick performance benchmarks
+* subscribe to parts of your application state
+* display messages similar to console.log
+* track global errors with source-mapped stack traces
+* dispatch actions like a government-run mind control experiment
+
+You plug it into your app as a dev dependency so it adds nothing to your product builds.
+
+The app comes in two forms:
+
+### Desktop
+
+Reactotron on the left, demo React Native app on the right.
+
+![Desktop](./docs/images/readme/reactotron-demo-app.gif)
+
+### Command Line
+
+![CLI](./docs/images/readme/reactotron-demo-cli.gif)
+
+
+Welcome to flavour country.
+
+# Quick Start
+
+* See the [React JS Quick Start](docs/quick-start-react-js.md).
+* See the [React Native Quick Start](docs/quick-start-react-native.md).
+
+# Usage
+
+* [Track Errors](docs/plugin-track-global-errors.md)
+* Integrate with [Redux](docs/plugin-redux.md)
+* Networking monitoring with [Apisauce](docs/plugin-apisauce.md)
+* Start making fetch happen (plugin coming soon...)
+* Creating Your Own Plugins (tutorial coming soon...)
+* The JSON interface between client & server (coming soon...)
+
+
+# Change Log
+
+* August 21st, 2016 - [0.94.0](https://github.com/reactotron/reactotron/releases/tag/v0.94.0)
+* August 18th, 2016 - [0.93.0](https://github.com/reactotron/reactotron/releases/tag/v0.93.0)
+* August 16th, 2016 - [0.92.0](https://github.com/reactotron/reactotron/releases/tag/v0.92.0)
diff --git a/scripts/publish.sh b/scripts/publish.sh
new file mode 100755
index 000000000..215cf9521
--- /dev/null
+++ b/scripts/publish.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+echo "-=-=-=-=-=-=-=-=-=-=-=-=-=-"
+echo "Pushing All Y'all Up To NPM"
+echo "-=-=-=-=-=-=-=-=-=-=-=-=-=-"
+cd packages/reactotron-core-client
+npm run build
+npm publish
+cd ../reactotron-apisauce
+npm run build
+npm publish
+cd ../reactotron-redux
+npm run build
+npm publish
+cd ../reactotron-react-js
+npm run build
+npm publish
+cd ../reactotron-react-native
+npm run build
+npm publish
+cd ../reactotron-core-server
+npm run build
+npm publish
+cd ../reactotron-cli
+npm run build
+npm publish
+cd ../..
diff --git a/scripts/welcome.sh b/scripts/welcome.sh
new file mode 100755
index 000000000..9cc82d283
--- /dev/null
+++ b/scripts/welcome.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+echo "-=-=-=-=-=-=-=-=-="
+echo "Welcome Developers"
+echo "-=-=-=-=-=-=-=-=-="
+echo ""
+echo "Let's get your build environment ready."
+echo ""
+echo "This will take about 5 minutes because.... well... npm. amirite???"
+echo ""
+echo ""
+npm run bootstrap
+npm run build
+npm run copy-internal-deps
+npm test
diff --git a/server/commands/api/apiLog.js b/server/commands/api/apiLog.js
deleted file mode 100644
index cd47fc0ec..000000000
--- a/server/commands/api/apiLog.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import RS from 'ramdasauce'
-import R from 'ramda'
-
-const COMMAND = 'api.log'
-
-const process = (context, action) => {
- const time = context.timeStamp()
- const problem = RS.dotPath('response.problem', action.message)
- const status = RS.dotPath('response.status', action.message)
- if (!R.is(Number, status)) {
- context.ui.apiBox.log(`${time} {red-fg}${problem}{/}`)
- return
- }
-
- const url = RS.dotPath('response.config.url', action.message)
- const baseURL = RS.dotPath('response.config.baseURL', action.message)
- const path = R.replace(baseURL, '', url)
-
- const rawMethod = RS.dotPath('response.config.method')(action.message) || '???'
- const method = R.pipe(R.take(3), R.toUpper)(rawMethod)
- const statusMessage = R.cond([
- [RS.isWithin(200, 299), R.always(`{green-fg}${status}{/}`)],
- [RS.isWithin(400, 599), R.always(`{red-fg}${status}{/}`)],
- [R.T, R.identity]
- ])(status)
- const durationMs = RS.dotPath('response.duration', action.message)
- const duration = `{white-fg}${durationMs}{/}ms`
- context.ui.apiBox.log(`${time} ${statusMessage} {blue-fg}${method}{/} ${path}{|}${duration}`)
- if (context.apiLoggingStyle === 'full') {
- const data = RS.dotPath('response.data', action.message)
- if (R.is(Object, data)) {
- const json = JSON.stringify(data, null, 2)
- context.ui.apiBox.log(json)
- }
- }
-}
-
-export default {
- name: COMMAND,
- process
-}
diff --git a/server/commands/client/clientAdd.js b/server/commands/client/clientAdd.js
deleted file mode 100644
index 377b9b365..000000000
--- a/server/commands/client/clientAdd.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import {displayConnectedMessage, updateClients} from './clientHelpers'
-
-const COMMAND = 'client.add'
-
-const process = (context, action) => {
- const {clients} = context
- const clientInfo = action.client
-
- clients[clientInfo.socket.id] = clientInfo
-
- displayConnectedMessage(context, clientInfo)
- updateClients(context)
-}
-
-export default {
- name: COMMAND,
- process
-}
diff --git a/server/commands/client/clientHelpers.js b/server/commands/client/clientHelpers.js
deleted file mode 100644
index 940248ebd..000000000
--- a/server/commands/client/clientHelpers.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import R from 'ramda'
-
-export const formatClient = (client = {}, prefix = "-") => {
- return `${prefix} {green-fg}[${client.ip}]{/} ${client.name} <${client.userAgent}> <${client.version}>`
-}
-
-export const formatClients = (clients = {}, prefix = "-") => {
- return R.pipe(
- R.values,
- R.map((c) => formatClient(c, prefix)),
- R.join('\n')
- )(clients)
-}
-
-// displays a message in the log when this client connects
-export const displayConnectedMessage = (context, client) => {
- const message = formatClient(client)
- context.log(message)
- context.ui.screen.render()
-}
-
-// updates the connected client count
-export const updateClients = (context) => {
- context.ui.connectionBox.setContent(
- context.ui.clientCount(R.values(context.clients).length)
- )
-
- context.ui.screen.render()
-}
diff --git a/server/commands/client/clientRemove.js b/server/commands/client/clientRemove.js
deleted file mode 100644
index 67a16b2cb..000000000
--- a/server/commands/client/clientRemove.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import {updateClients} from './clientHelpers'
-
-const COMMAND = 'client.remove'
-
-const process = (context, action) => {
- const {clients} = context
- const {socket} = action
-
- delete clients[socket.id]
- updateClients(context)
-}
-
-export default {
- name: COMMAND,
- process
-}
diff --git a/server/commands/console/consoleError.js b/server/commands/console/consoleError.js
deleted file mode 100644
index 652beb668..000000000
--- a/server/commands/console/consoleError.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import RS from 'ramdasauce'
-const COMMAND = 'console.error'
-
-/**
- Receives a console.error from the app.
- */
-const process = (context, action) => {
- const {message} = action.message
- const time = context.timeStamp()
- const isWarning = RS.startsWith('Warning: ', message)
- const color = isWarning ? 'yellow' : 'red'
- context.ui.logBox.log(`${time} {${color}-fg}${message}{/}`)
- context.ui.screen.render()
-}
-
-export default {
- name: COMMAND,
- process
-}
diff --git a/server/commands/content/contentLog.js b/server/commands/content/contentLog.js
deleted file mode 100644
index 2c9200fc4..000000000
--- a/server/commands/content/contentLog.js
+++ /dev/null
@@ -1,10 +0,0 @@
-const COMMAND = 'content.log'
-
-const process = (context, action) => {
- context.log(action.message)
-}
-
-export default {
- name: COMMAND,
- process
-}
diff --git a/server/commands/index.js b/server/commands/index.js
deleted file mode 100644
index 017428de9..000000000
--- a/server/commands/index.js
+++ /dev/null
@@ -1,87 +0,0 @@
-// redux things
-import reduxDispatch from './redux/reduxDispatch'
-import reduxValueRequest from './redux/reduxValueRequest'
-import reduxKeyRequest from './redux/reduxKeyRequest'
-import reduxValueResponse from './redux/reduxValueResponse'
-import reduxKeyResponse from './redux/reduxKeyResponse'
-import reduxValuePrompt from './redux/reduxValuePrompt'
-import reduxKeyPrompt from './redux/reduxKeyPrompt'
-import reduxDispatchPrompt from './redux/reduxDispatchPrompt'
-import reduxActionDone from './redux/reduxActionDone'
-import reduxSubscribeRequest from './redux/reduxSubscribeRequest'
-import reduxSubscribeValues from './redux/reduxSubscribeValues'
-import reduxSubscribeAdd from './redux/reduxSubscribeAdd'
-import reduxSubscribeAddPrompt from './redux/reduxSubscribeAddPrompt'
-import reduxSubscribeDelete from './redux/reduxSubscribeDelete'
-import reduxSubscribeDeletePrompt from './redux/reduxSubscribeDeletePrompt'
-import reduxSubscribeClear from './redux/reduxSubscribeClear'
-import devMenuReload from './devMenu/devMenuReload'
-import benchReport from './bench/benchReport'
-
-// program things
-import die from './program/die'
-
-// api things
-import apiLog from './api/apiLog'
-
-// client things
-import clientAdd from './client/clientAdd'
-import clientRemove from './client/clientRemove'
-
-// command things
-import commandRepeat from './commands/commandRepeat'
-
-// content things
-import contentLog from './content/contentLog'
-import contentClear from './content/contentClear'
-import contentScore from './content/contentScore'
-
-// menu things
-import menuPush from './menus/menuPush'
-import menuPop from './menus/menuPop'
-import menuMain from './menus/menuMain'
-import menuRedux from './menus/menuRedux'
-import menuHelp from './menus/menuHelp'
-import menuReduxSubscribe from './menus/menuReduxSubscribe'
-import menuDevMenu from './menus/menuDevMenu'
-import menuClients from './menus/menuClients'
-import consoleError from './console/consoleError'
-
-// come together. right now. over me.
-export default [
- clientAdd,
- clientRemove,
- reduxDispatch,
- reduxValueRequest,
- reduxKeyRequest,
- reduxValueResponse,
- reduxKeyResponse,
- reduxValuePrompt,
- reduxKeyPrompt,
- reduxDispatchPrompt,
- reduxActionDone,
- reduxSubscribeRequest,
- reduxSubscribeValues,
- reduxSubscribeAdd,
- reduxSubscribeAddPrompt,
- reduxSubscribeDelete,
- reduxSubscribeDeletePrompt,
- reduxSubscribeClear,
- apiLog,
- contentLog,
- contentClear,
- contentScore,
- menuPush,
- menuPop,
- menuMain,
- menuRedux,
- menuHelp,
- menuReduxSubscribe,
- menuDevMenu,
- menuClients,
- commandRepeat,
- devMenuReload,
- consoleError,
- benchReport,
- die
-]
diff --git a/server/commands/menus/menuClients.js b/server/commands/menus/menuClients.js
deleted file mode 100644
index 32de1500a..000000000
--- a/server/commands/menus/menuClients.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import { formatClients } from '../client/clientHelpers'
-
-const COMMAND = 'menu.clients'
-
-const process = (context, action) => {
- const clients = formatClients(context.clients, ' ')
-
- const messageText = `
- {bold}Clients{/bold}
- ---------------------------------
-${clients}
-`
-
- context.info(' {yellow-fg}reactotron{/} {blue-fg}clients{/} ', messageText)
-}
-
-export default {
- name: COMMAND,
- process
-}
diff --git a/server/commands/redux/reduxActionDone.js b/server/commands/redux/reduxActionDone.js
deleted file mode 100644
index bcfdb883d..000000000
--- a/server/commands/redux/reduxActionDone.js
+++ /dev/null
@@ -1,19 +0,0 @@
-const COMMAND = 'redux.action.done'
-
-/**
- Received when a redux action is complete.
- */
-const process = (context, action) => {
- const {type, ms} = action.message
- const time = context.timeStamp()
- context.ui.reduxActionBox.log(`${time} {cyan-fg}${type}{/}{|}{white-fg}${ms}{/}ms`)
- if (context.reduxActionLoggingStyle === 'full') {
- context.ui.reduxActionBox.log(action.message.action)
- context.ui.reduxActionBox.log('')
- }
-}
-
-export default {
- name: COMMAND,
- process
-}
diff --git a/server/commands/redux/reduxDispatch.js b/server/commands/redux/reduxDispatch.js
deleted file mode 100644
index a79adb381..000000000
--- a/server/commands/redux/reduxDispatch.js
+++ /dev/null
@@ -1,14 +0,0 @@
-const COMMAND = 'redux.dispatch'
-
-/**
- Sends a request to dispatch an action into Redux.
- */
-const process = (context, action) => {
- context.send(action)
-}
-
-export default {
- name: COMMAND,
- repeatable: true,
- process
-}
diff --git a/server/commands/redux/reduxKeyPrompt.js b/server/commands/redux/reduxKeyPrompt.js
deleted file mode 100644
index 4efb5e697..000000000
--- a/server/commands/redux/reduxKeyPrompt.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import RS from 'ramdasauce'
-const COMMAND = 'redux.key.prompt'
-
-/**
- Prompts for a path to grab some redux keys from.
- */
-const process = (context, action) => {
- context.prompt('Enter a redux path: eg. weather.temperature', (value) => {
- const path = RS.isNilOrEmpty(value) ? null : value
- context.post({type: 'redux.key.request', path})
- })
-}
-
-export default {
- name: COMMAND,
- repeatable: true,
- process
-}
diff --git a/server/commands/redux/reduxKeyRequest.js b/server/commands/redux/reduxKeyRequest.js
deleted file mode 100644
index 3d4b6b4e4..000000000
--- a/server/commands/redux/reduxKeyRequest.js
+++ /dev/null
@@ -1,14 +0,0 @@
-const COMMAND = 'redux.key.request'
-
-/**
- Sends a request to get the keys at the path in redux.
- */
-const process = (context, action) => {
- context.send(action)
-}
-
-export default {
- name: COMMAND,
- repeatable: true,
- process
-}
diff --git a/server/commands/redux/reduxSubscribeAdd.js b/server/commands/redux/reduxSubscribeAdd.js
deleted file mode 100644
index e1d2f214f..000000000
--- a/server/commands/redux/reduxSubscribeAdd.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import R from 'ramda'
-const COMMAND = 'redux.subscribe.add'
-
-/**
- Prompts for a path to grab some redux keys from.
- */
-const process = (context, action) => {
- const path = action.path
- // create the config.subscriptions unless it exist
- if (R.isNil(context.config.subscriptions)) {
- context.config.subscriptions = []
- }
- // subscribe
- if (!R.contains(path, context.config.subscriptions)) {
- context.config.subscriptions.push(path)
- context.post({type: 'redux.subscribe.request'})
- }
-}
-
-export default {
- name: COMMAND,
- process
-}
diff --git a/server/commands/redux/reduxSubscribeAddPrompt.js b/server/commands/redux/reduxSubscribeAddPrompt.js
deleted file mode 100644
index 95e77d772..000000000
--- a/server/commands/redux/reduxSubscribeAddPrompt.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import RS from 'ramdasauce'
-const COMMAND = 'redux.subscribe.add.prompt'
-
-/**
- Prompts for a path to grab some redux keys from.
- */
-const process = (context, action) => {
- context.prompt('Enter a redux path: eg. weather.temperature', (value) => {
- // logical default
- const path = RS.isNilOrEmpty(value) ? null : value
- context.post({type: 'redux.subscribe.add', path: path})
- })
-}
-
-export default {
- name: COMMAND,
- process
-}
diff --git a/server/commands/redux/reduxSubscribeDelete.js b/server/commands/redux/reduxSubscribeDelete.js
deleted file mode 100644
index 468525d51..000000000
--- a/server/commands/redux/reduxSubscribeDelete.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import R from 'ramda'
-const COMMAND = 'redux.subscribe.delete'
-
-/**
- Prompts for a path to grab some redux keys from.
- */
-const process = (context, action) => {
- const path = action.path
- // create the config.subscriptions unless it exist
- if (R.isNil(context.config.subscriptions)) {
- context.config.subscriptions = []
- }
- // remove
- context.config.subscriptions = R.without([path], context.config.subscriptions)
- // refresh
- context.post({type: 'redux.subscribe.request'})
-}
-
-export default {
- name: COMMAND,
- process
-}
diff --git a/server/commands/redux/reduxSubscribeDeletePrompt.js b/server/commands/redux/reduxSubscribeDeletePrompt.js
deleted file mode 100644
index 55a8936f4..000000000
--- a/server/commands/redux/reduxSubscribeDeletePrompt.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import RS from 'ramdasauce'
-const COMMAND = 'redux.subscribe.delete.prompt'
-
-/**
- Prompts for a path to grab some redux keys from.
- */
-const process = (context, action) => {
- context.prompt('Enter a redux path: eg. weather.temperature', (value) => {
- // logical default
- const path = RS.isNilOrEmpty(value) ? null : value
- context.post({type: 'redux.subscribe.delete', path: path})
- })
-}
-
-export default {
- name: COMMAND,
- process
-}
diff --git a/server/commands/redux/reduxSubscribeRequest.js b/server/commands/redux/reduxSubscribeRequest.js
deleted file mode 100644
index 7889b57cb..000000000
--- a/server/commands/redux/reduxSubscribeRequest.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import R from 'ramda'
-import RS from 'ramdasauce'
-
-const COMMAND = 'redux.subscribe.request'
-
-/**
- Sends a request to get the keys at the path in redux.
- */
-const process = (context, action) => {
- const paths = R.without([null], RS.dotPath('config.subscriptions', context) || [])
- context.send(R.merge(action, {paths}))
-}
-
-export default {
- name: COMMAND,
- repeatable: true,
- process
-}
diff --git a/server/commands/redux/reduxSubscribeValues.js b/server/commands/redux/reduxSubscribeValues.js
deleted file mode 100644
index b80749b85..000000000
--- a/server/commands/redux/reduxSubscribeValues.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import R from 'ramda'
-
-const COMMAND = 'redux.subscribe.values'
-
-/**
- Receive the subscribed key paths.
- */
-const process = (context, action) => {
- const {values} = action.message
- const each = R.map(([k, v]) => `{cyan-fg}${k}{/}{|}{white-fg}${v}{/}`, values)
- const message = R.join('\n', each)
- context.ui.reduxWatchBox.setContent(message)
- context.ui.screen.render()
-}
-
-export default {
- name: COMMAND,
- process
-}
diff --git a/server/commands/redux/reduxValueRequest.js b/server/commands/redux/reduxValueRequest.js
deleted file mode 100644
index 59ee259dd..000000000
--- a/server/commands/redux/reduxValueRequest.js
+++ /dev/null
@@ -1,14 +0,0 @@
-const COMMAND = 'redux.value.request'
-
-/**
- Sends a request to get the values at the path in redux.
- */
-const process = (context, action) => {
- context.send(action)
-}
-
-export default {
- name: COMMAND,
- repeatable: true,
- process
-}
diff --git a/server/commands/redux/reduxValueResponse.js b/server/commands/redux/reduxValueResponse.js
deleted file mode 100644
index 8a9412b6f..000000000
--- a/server/commands/redux/reduxValueResponse.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import RS from 'ramdasauce'
-
-const COMMAND = 'redux.value.response'
-
-/**
- Receives some values inside redux.
- */
-const process = (context, action) => {
- const {path, values} = action.message
- const time = context.timeStamp()
- if (RS.isNilOrEmpty(path)) {
- context.ui.logBox.log(`{white-fg}${time}{/} {blue-fg}values in{/} {cyan-fg}/{/}`)
- } else {
- context.ui.logBox.log(`{white-fg}${time}{/} {blue-fg}values in{/} {cyan-fg}${path}{/}`)
- }
- context.ui.logBox.log(values)
- context.ui.logBox.log('')
- context.ui.screen.render()
-}
-
-export default {
- name: COMMAND,
- process
-}
diff --git a/server/context.js b/server/context.js
deleted file mode 100644
index 7a5dd6b13..000000000
--- a/server/context.js
+++ /dev/null
@@ -1,93 +0,0 @@
-import R from 'ramda'
-import moment from 'moment'
-import Router from './router'
-
-export default class Context {
-
- constructor (parts) {
- this.io = parts.io
- this.ui = parts.ui
- this.router = parts.router
- this.menuStack = []
- this.clients = {}
- this.lastRepeatableMessage = null
- this.reduxActionLoggingStyle = 'short'
- this.apiLoggingStyle = 'short'
- this.config = {}
- }
-
- send (action) {
- const body = action
- const bodyJson = JSON.stringify(body)
- this.io.sockets.emit('command', bodyJson)
- }
-
- post (message) {
- // sanity
- if (R.isNil(message) || !Router.isValidMessage(message)) return false
- // send each command the message
- const command = this.router.commands[message.type]
- if (command) {
- // kick off the command
- command.process(this, message)
-
- // unless this is a command to repeat, then record the command
- if (command.repeatable) {
- this.lastRepeatableMessage = message
- }
- }
- }
-
- prompt (title, callback) {
- this.ui.promptBox.setFront()
- this.ui.screen.render()
- this.ui.promptBox.input(title, '', (err, value) => {
- if (!err) {
- callback(value)
- this.ui.screen.render()
- }
- })
- }
-
- message (displayText, callback = null) {
- this.ui.messageBox.setFront()
- this.ui.screen.render()
- this.ui.messageBox.display(displayText, 0, (err, value) => {
- if (!err) {
- if (callback) callback(value)
- this.ui.screen.render()
- }
- })
- }
-
- info (title, displayText, callback = null) {
- this.ui.infoBox.setFront()
- this.ui.screen.render()
- this.ui.infoBox.setLabel(title)
- this.ui.infoBox.display(displayText, 0, (err, value) => {
- if (!err) {
- if (callback) callback(value)
- this.ui.screen.render()
- }
- })
- }
-
- timeStamp () {
- const t = moment()
- return `${t.format('HH:mm:')}{white-fg}${t.format('ss.SS')}{/}`
- }
-
- log (message) {
- const time = this.timeStamp()
- if (R.is(Object, message)) {
- this.ui.logBox.log(time)
- this.ui.logBox.log(message)
- this.ui.logBox.log('')
- } else {
- this.ui.logBox.log(`${time} ${message}`)
- }
-
- this.ui.screen.render()
- }
-
-}
diff --git a/server/index.js b/server/index.js
deleted file mode 100644
index 47ae4f6c2..000000000
--- a/server/index.js
+++ /dev/null
@@ -1,81 +0,0 @@
-import R from 'ramda'
-import SocketIO from 'socket.io'
-import Context from './context'
-import Router from './router'
-import commands from './commands/index'
-import ui from './ui'
-import gemoji from 'gemoji'
-
-// A way to add extra spacing for emoji characters. As it
-// turns out, the emojis are double-wide code points, but
-// the terminal renders it as a single slot. I literally
-// understand nothing anymore. Seems to work great tho!
-const keys = R.keys(gemoji.unicode)
-const emojiPattern = '(' + keys.join('|') + ')+'
-const emojiRegex = new RegExp(emojiPattern, 'g')
-const addSpaceForEmoji = (str) => str.replace(emojiRegex, '$1 ')
-
-const PORT = 3334
-const io = SocketIO(PORT)
-const router = Router.createRouter()
-R.forEach((command) => router.register(command), commands)
-const context = new Context({
- ui,
- io,
- router
-})
-
-io.on('connection', (socket) => {
- // When a socket connects, we also want to wait for
- // additional context about the client.
- socket.on('ready', (clientConfig) => {
- const socketInfo = {
- socket: socket,
- ip: socket.request.connection.remoteAddress === '::1' ? 'localhost' : socket.request.connection.remoteAddress,
- userAgent: socket.request.headers['user-agent'] || 'Unknown'
- }
-
- const clientInfo = R.merge(socketInfo, clientConfig)
- // const clientInfo = {
- // ...socketInfo,
- // ...clientConfig
- // }
-
- // Add new client
- context.post({type: 'client.add', client: clientInfo})
-
- // new connects need the subscribe redux
- context.post({type: 'redux.subscribe.request'})
- })
-
- ui.screen.render()
-
- socket.on('command', (data) => {
- const action = JSON.parse(addSpaceForEmoji(data))
- context.post(action)
- ui.screen.render()
- })
-
- socket.on('disconnect', () => {
- context.post({type: 'client.remove', socket})
- ui.screen.render()
- })
-})
-
-// always control-c to die
-ui.screen.key('C-c', () => context.post({type: 'program.die'}))
-
-// . to replay
-ui.screen.key('.', () => context.post({type: 'command.repeat'}))
-
-// - to score
-ui.screen.key('-', () => context.post({type: 'content.score'}))
-
-// del to clear
-ui.screen.key(['delete', 'backspace'], () => context.post({type: 'content.clear'}))
-
-// let's start with the main menu
-context.post({type: 'menu.main'})
-
-// initial render
-ui.screen.render()