Merge remotedev-server

This commit is contained in:
Zalmoxisus 2019-01-04 00:30:48 +02:00
parent 9de536fae2
commit 4849081263
21 changed files with 1903 additions and 30 deletions

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 Mihail Diordiev
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.

View File

@ -0,0 +1,119 @@
RemoteDev Server
================
Bridge for communicating with an application remotely via [Redux DevTools extension](https://github.com/zalmoxisus/redux-devtools-extension), [Remote Redux DevTools](https://github.com/zalmoxisus/remote-redux-devtools) or [RemoteDev](https://github.com/zalmoxisus/remotedev). Running your server is optional, you can use [remotedev.io](https://remotedev.io) instead.
### Installation
```
npm install --save-dev remotedev-server
```
Also [there's a docker image](https://github.com/jhen0409/docker-remotedev-server) you can use.
### Usage
##### Add in your app's `package.json`:
```
"scripts": {
"remotedev": "remotedev --hostname=localhost --port=8000"
}
```
So, you can start remotedev server by running `npm run remotedev`.
##### Import in your `server.js` script you use for starting a development server:
```js
var remotedev = require('remotedev-server');
remotedev({ hostname: 'localhost', port: 8000 });
```
So, you can start remotedev server together with your dev server.
##### Install the package globally (not recommended) just run:
```
remotedev --hostname=localhost --port=8000
```
### Connection settings
Set `hostname` and `port` to the values you want. `hostname` by default is `localhost` and `port` is `8000`.
To use WSS, set `protocol` argument to `https` and provide `key`, `cert` and `passphrase` arguments.
#### Available options
| Console argument | description | default value |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| `--hostname` | hostname | localhost |
| `--port` | port | 8000 |
| `--protocol` | protocol | http |
| `--key` | the key file for [running an https server](https://github.com/SocketCluster/socketcluster#using-over-https) (`--protocol` must be set to 'https') | - |
| `--cert` | the cert file for [running an https server](https://github.com/SocketCluster/socketcluster#using-over-https) (`--protocol` must be set to 'https') | - |
| `--passphrase` | the key passphrase for [running an https server](https://github.com/SocketCluster/socketcluster#using-over-https) (`--protocol` must be set to 'https') | - |
| `--dbOptions` | database configuration, can be whether an object or a path (string) to json configuration file (by default it uses our `./defaultDbOptions.json` file. Set `migrate` key to `true` to use our migrations file. [More details bellow](https://github.com/zalmoxisus/remotedev-server#save-reports-and-logs). | - |
| `--logLevel` | the socket server log level - 0=none, 1=error, 2=warn, 3=info | 3 |
| `--wsEngine` | the socket server web socket engine - ws or uws (sc-uws) | ws |
### Inject to React Native local server
##### Add in your React Native app's `package.json`:
```
"scripts": {
"remotedev": "remotedev --hostname=localhost --port=8000 --injectserver=reactnative"
}
```
The `injectserver` value can be `reactnative` or `macos` ([react-native-macos](https://github.com/ptmt/react-native-macos)), it used `reactnative` by default.
Then, we can start React Native server and RemoteDev server with one command (`npm start`).
##### Revert the injection
Add in your React Native app's `package.json`:
```
"scripts": {
"remotedev-revert": "remotedev --revert=reactnative"
}
```
Or just run `$(npm bin)/remotedev --revert`.
### Connect from Android device or emulator
> Note that if you're using `injectserver` argument explained above, this step is not necessary.
If you're running an Android 5.0+ device connected via USB or an Android emulator, use [adb command line tool](http://developer.android.com/tools/help/adb.html) to setup port forwarding from the device to your computer:
```
adb reverse tcp:8000 tcp:8000
```
If you're still use Android 4.0, you should use `10.0.2.2` (Genymotion: `10.0.3.2`) instead of `localhost` in [remote-redux-devtools](https://github.com/zalmoxisus/remote-redux-devtools#storeconfigurestorejs) or [remotedev](https://github.com/zalmoxisus/remotedev#usage).
### Save reports and logs
You can store reports via [`redux-remotedev`](https://github.com/zalmoxisus/redux-remotedev) and get them replicated with [Redux DevTools extension](https://github.com/zalmoxisus/redux-devtools-extension) or [Remote Redux DevTools](https://github.com/zalmoxisus/remote-redux-devtools). You can get action history right in the extension just by clicking the link from a report. Open `http://localhost:8000/graphiql` (assuming you're using `localhost` as host and `8000`) to explore in GraphQL. Reports are posted to `http://localhost:8000/`. See examples in [tests](https://github.com/zalmoxisus/remotedev-server/blob/937cfa1f0ac9dc12ebf7068eeaa8b03022ec33bc/test/integration.spec.js#L110-L165).
Remotedev server is database agnostic using `knex` schema. By default everything is stored in the memory using sqlite database. See [`defaultDbOptions.json`](https://github.com/zalmoxisus/remotedev-server/blob/master/defaultDbOptions.json) for example of sqlite. You can replace `"connection": { "filename": ":memory:" },` with your file name (instead of `:memory:`) to persist teh database. Here's an example for PostgreSQL:
```
{
"client": "pg",
"connection": { "user": "myuser", "password": "mypassword", "database": "mydb" },
"debug": false,
"migrate": true
}
```
### Advanced
- [Writing your integration for a native application](https://github.com/zalmoxisus/remotedev-server/blob/master/docs/API/Realtime.md)
### License
MIT

View File

@ -0,0 +1,98 @@
var fs = require('fs');
var path = require('path');
var semver = require('semver');
var name = 'remotedev-server';
var startFlag = '/* ' + name + ' start */';
var endFlag = '/* ' + name + ' end */';
var serverFlags = {
'react-native': {
'0.0.1': ' _server(argv, config, resolve, reject);',
'0.31.0': " runServer(args, config, () => console.log('\\nReact packager ready.\\n'));",
'0.44.0-rc.0': ' runServer(args, config, startedCallback, readyCallback);',
'0.46.0-rc.0': ' runServer(runServerArgs, configT, startedCallback, readyCallback);',
'0.57.0': ' runServer(args, configT);'
},
'react-native-desktop': {
'0.0.1': ' _server(argv, config, resolve, reject);'
}
};
function getModuleVersion(modulePath) {
return JSON.parse(
fs.readFileSync(
path.join(modulePath, 'package.json'),
'utf-8'
)
).version;
}
function getServerFlag(moduleName, version) {
var flags = serverFlags[moduleName || 'react-native'];
var versions = Object.keys(flags);
var flag;
for (var i = 0; i < versions.length; i++) {
if (semver.gte(version, versions[i])) {
flag = flags[versions[i]];
}
}
return flag;
}
exports.dir = 'local-cli/server';
exports.file = 'server.js';
exports.fullPath = path.join(exports.dir, exports.file);
exports.inject = function(modulePath, options, moduleName) {
var filePath = path.join(modulePath, exports.fullPath);
if (!fs.existsSync(filePath)) return false;
var serverFlag = getServerFlag(
moduleName,
getModuleVersion(modulePath)
);
var code = [
startFlag,
' require("' + name + '")(' + JSON.stringify(options) + ')',
' .then(_remotedev =>',
' _remotedev.on("ready", () => {',
' if (!_remotedev.portAlreadyUsed) console.log("-".repeat(80));',
' ' + serverFlag,
' })',
' );',
endFlag,
].join('\n');
var serverCode = fs.readFileSync(filePath, 'utf-8');
var start = serverCode.indexOf(startFlag); // already injected ?
var end = serverCode.indexOf(endFlag) + endFlag.length;
if (start === -1) {
start = serverCode.indexOf(serverFlag);
end = start + serverFlag.length;
}
fs.writeFileSync(
filePath,
serverCode.substr(0, start) + code + serverCode.substr(end, serverCode.length)
);
return true;
};
exports.revert = function(modulePath, moduleName) {
var filePath = path.join(modulePath, exports.fullPath);
if (!fs.existsSync(filePath)) return false;
var serverFlag = getServerFlag(
moduleName,
getModuleVersion(modulePath)
);
var serverCode = fs.readFileSync(filePath, 'utf-8');
var start = serverCode.indexOf(startFlag); // already injected ?
var end = serverCode.indexOf(endFlag) + endFlag.length;
if (start !== -1) {
fs.writeFileSync(
filePath,
serverCode.substr(0, start) + serverFlag + serverCode.substr(end, serverCode.length)
);
}
return true;
};

View File

@ -0,0 +1,73 @@
#! /usr/bin/env node
var fs = require('fs');
var path = require('path');
var argv = require('minimist')(process.argv.slice(2));
var chalk = require('chalk');
var injectServer = require('./injectServer');
var getOptions = require('./../src/options');
function readFile(filePath) {
return fs.readFileSync(path.resolve(process.cwd(), filePath), 'utf-8');
}
if (argv.protocol === 'https') {
argv.key = argv.key ? readFile(argv.key) : null;
argv.cert = argv.cert ? readFile(argv.cert) : null;
}
function log(pass, msg) {
var prefix = pass ? chalk.green.bgBlack('PASS') : chalk.red.bgBlack('FAIL');
var color = pass ? chalk.blue : chalk.red;
console.log(prefix, color(msg));
}
function getModuleName(type) {
switch (type) {
case 'macos':
return 'react-native-macos';
// react-native-macos is renamed from react-native-desktop
case 'desktop':
return 'react-native-desktop';
case 'reactnative':
default:
return 'react-native';
}
}
function getModulePath(moduleName) {
return path.join(process.cwd(), 'node_modules', moduleName);
}
function getModule(type) {
var moduleName = getModuleName(type);
var modulePath = getModulePath(moduleName);
if (type === 'desktop' && !fs.existsSync(modulePath)) {
moduleName = getModuleName('macos');
modulePath = getModulePath(moduleName);
}
return {
name: moduleName,
path: modulePath
};
}
if (argv.revert) {
var module = getModule(argv.revert);
var pass = injectServer.revert(module.path, module.name);
var msg = 'Revert injection of RemoteDev server from React Native local server';
log(pass, msg + (!pass ? ', the file `' + path.join(module.name, injectServer.fullPath) + '` not found.' : '.'));
process.exit(pass ? 0 : 1);
}
if (argv.injectserver) {
var options = getOptions(argv);
var module = getModule(argv.injectserver);
var pass = injectServer.inject(module.path, options, module.name);
var msg = 'Inject RemoteDev server into React Native local server';
log(pass, msg + (pass ? '.' : ', the file `' + path.join(module.name, injectServer.fullPath) + '` not found.'));
process.exit(pass ? 0 : 1);
}
require('../index')(argv);

View File

@ -0,0 +1,13 @@
{
"client": "sqlite3",
"connection": { "filename": ":memory:" },
"pool": {
"min": 1,
"max": 1,
"idleTimeoutMillis": 360000000,
"disposeTimeout": 360000000
},
"useNullAsDefault": true,
"debug": false,
"migrate": true
}

View File

@ -0,0 +1,40 @@
var getPort = require('getport');
var SocketCluster = require('socketcluster');
var getOptions = require('./src/options');
var LOG_LEVEL_NONE = 0;
var LOG_LEVEL_ERROR = 1;
var LOG_LEVEL_WARN = 2;
var LOG_LEVEL_INFO = 3;
module.exports = function(argv) {
var options = Object.assign(getOptions(argv), {
workerController: __dirname + '/src/worker.js',
allowClientPublish: false
});
var port = options.port;
var logLevel = options.logLevel === undefined ? LOG_LEVEL_INFO : options.logLevel;
return new Promise(function(resolve) {
// Check port already used
getPort(port, function(err, p) {
if (err) {
if (logLevel >= LOG_LEVEL_ERROR) {
console.error(err);
}
return;
}
if (port !== p) {
if (logLevel >= LOG_LEVEL_WARN) {
console.log('[RemoteDev] Server port ' + port + ' is already used.');
}
resolve({ portAlreadyUsed: true, on: function(status, cb) { cb(); } });
} else {
if (logLevel >= LOG_LEVEL_INFO) {
console.log('[RemoteDev] Start server...');
console.log('-'.repeat(80) + '\n');
}
resolve(new SocketCluster(options));
}
});
});
};

View File

@ -0,0 +1,63 @@
{
"name": "remotedev-server",
"version": "0.3.1",
"description": "Run the RemoteDev monitor on your local server.",
"main": "index.js",
"bin": {
"remotedev": "bin/remotedev.js"
},
"files": [
"bin",
"src",
"views",
"index.js",
"defaultDbOptions.json"
],
"scripts": {
"test": "NODE_ENV=test mocha --recursive",
"test:watch": "NODE_ENV=test mocha --recursive --watch",
"prepublish": "npm run test"
},
"repository": {
"type": "git",
"url": "https://github.com/zalmoxisus/remotedev-server.git"
},
"keywords": [
"devtools",
"remotedev"
],
"engines": {
"node": ">=6.0.0"
},
"author": "Mihail Diordiev <zalmoxisus@gmail.com> (https://github.com/zalmoxisus)",
"license": "MIT",
"bugs": {
"url": "https://github.com/zalmoxisus/remotedev-server/issues"
},
"homepage": "https://github.com/zalmoxisus/remotedev-server",
"dependencies": {
"body-parser": "^1.15.0",
"chalk": "^1.1.3",
"cors": "^2.7.1",
"ejs": "^2.4.1",
"express": "^4.13.3",
"getport": "^0.1.0",
"graphql": "^0.13.0",
"graphql-server-express": "^1.4.0",
"graphql-tools": "^4.0.3",
"knex": "^0.15.2",
"lodash": "^4.15.0",
"minimist": "^1.2.0",
"morgan": "^1.7.0",
"semver": "^5.3.0",
"socketcluster": "^14.3.3",
"sqlite3": "^4.0.4",
"uuid": "^3.0.1"
},
"devDependencies": {
"expect": "^1.20.2",
"mocha": "^3.2.0",
"socketcluster-client": "^14.0.0",
"supertest": "^3.0.0"
}
}

View File

@ -0,0 +1,21 @@
var makeExecutableSchema = require('graphql-tools').makeExecutableSchema;
var requireSchema = require('../utils/requireSchema');
var schema = requireSchema('./schema_def.graphql', require);
var resolvers = {
Query: {
reports: function report(source, args, context, ast) {
return context.store.listAll();
},
report: function report(source, args, context, ast) {
return context.store.get(args.id);
}
}
};
var executableSchema = makeExecutableSchema({
typeDefs: schema,
resolvers: resolvers
});
module.exports = executableSchema;

View File

@ -0,0 +1,60 @@
# A list of options for the type of the report
enum ReportType {
STATE
ACTION
STATES
ACTIONS
}
type Report {
# Report ID
id: ID!
# Type of the report, can be: STATE, ACTION, STATES, ACTIONS
type: ReportType,
# Briefly what happened
title: String,
# Details supplied by the user
description: String,
# The last dispatched action before the report was sent
action: String,
# Stringified actions or the state or both, which should be loaded the application to reproduce the exact behavior
payload: String,
# Stringified preloaded state object. Could be the initial state of the app or committed state (after dispatching COMMIT action or reaching maxAge)
preloadedState: String,
# Screenshot url or blob as a string
screenshot: String,
# User Agent String
userAgent: String,
# Application version to group the reports and versioning
version: String,
# Used to identify the user who sent the report
userId: String,
# More detailed data about the user, usually it's a stringified object
user: String,
# Everything else you want to send
meta: String,
# Error message which invoked sending the report
exception: String,
# Id to identify the store in case there are multiple stores
instanceId: String,
# Timestamp when the report was added
added: String
# Id to identify the application (from apps table)
appId: ID
}
# Explore GraphQL query schema
type Query {
# List all reports
reports: [Report]
# Get a report by ID
report(
# Report ID
id: ID!
): Report
}
schema {
query: Query
#mutation: Mutation
}

View File

@ -0,0 +1,27 @@
var path = require('path');
var knexModule = require('knex');
module.exports = function connector(options) {
var dbOptions = options.dbOptions;
dbOptions.useNullAsDefault = true;
if (!dbOptions.migrate) {
return knexModule(dbOptions);
}
dbOptions.migrations = { directory: path.resolve(__dirname, 'migrations') };
dbOptions.seeds = { directory: path.resolve(__dirname, 'seeds') };
var knex = knexModule(dbOptions);
knex.migrate.latest()
.then(function() {
return knex.seed.run();
})
.then(function() {
console.log(' \x1b[0;32m[Done]\x1b[0m Migrations are finished\n');
})
.catch(function(error) {
console.error(error);
});
return knex;
};

View File

@ -0,0 +1,71 @@
exports.up = function(knex, Promise) {
return Promise.all([
knex.schema.createTable('remotedev_reports', function(table) {
table.uuid('id').primary();
table.string('type');
table.string('title');
table.string('description');
table.string('action');
table.text('payload', 'longtext');
table.text('preloadedState', 'longtext');
table.text('screenshot', 'longtext');
table.string('userAgent');
table.string('version');
table.string('user');
table.string('userId');
table.string('instanceId');
table.string('meta');
table.string('exception');
table.timestamp('added').defaultTo(knex.fn.now());
table.uuid('appId')
.references('id')
.inTable('remotedev_apps').onDelete('CASCADE').onUpdate('CASCADE')
.defaultTo('78626c31-e16b-4528-b8e5-f81301b627f4');
}),
knex.schema.createTable('remotedev_payloads', function(table){
table.uuid('id').primary();
table.text('state');
table.text('action');
table.timestamp('added').defaultTo(knex.fn.now());
table.uuid('reportId')
.references('id')
.inTable('remotedev_reports').onDelete('CASCADE').onUpdate('CASCADE');
}),
knex.schema.createTable('remotedev_apps', function(table){
table.uuid('id').primary();
table.string('title');
table.string('description');
table.string('url');
table.timestamps(false, true);
}),
knex.schema.createTable('remotedev_users', function(table){
table.uuid('id').primary();
table.string('name');
table.string('login');
table.string('email');
table.string('avatarUrl');
table.string('profileUrl');
table.string('oauthId');
table.string('oauthType');
table.string('token');
table.timestamps(false, true);
}),
knex.schema.createTable('remotedev_users_apps', function(table){
table.boolean('readOnly').defaultTo(false);
table.uuid('userId');
table.uuid('appId');
table.primary(['userId', 'appId']);
table.foreign('userId')
.references('id').inTable('remotedev_users').onDelete('CASCADE').onUpdate('CASCADE');
table.foreign('appId')
.references('id').inTable('remotedev_apps').onDelete('CASCADE').onUpdate('CASCADE');
})
])
};
exports.down = function(knex, Promise) {
return Promise.all([
knex.schema.dropTable('remotedev_reports'),
knex.schema.dropTable('remotedev_apps')
])
};

View File

@ -0,0 +1,12 @@
exports.seed = function(knex, Promise) {
return Promise.all([
knex('remotedev_apps').del()
]).then(function() {
return Promise.all([
knex('remotedev_apps').insert({
id: '78626c31-e16b-4528-b8e5-f81301b627f4',
title: 'Default'
})
]);
});
};

View File

@ -0,0 +1,13 @@
var graphiqlExpress = require('graphql-server-express').graphiqlExpress;
module.exports = graphiqlExpress({
endpointURL: '/graphql',
query:
'{\n' +
' reports {\n' +
' id,\n' +
' type,\n' +
' title\n' +
' }\n' +
'}'
});

View File

@ -0,0 +1,13 @@
var graphqlExpress = require('graphql-server-express').graphqlExpress;
var schema = require('../api/schema');
module.exports = function (store) {
return graphqlExpress(function() {
return {
schema: schema,
context: {
store: store
}
};
});
};

View File

@ -0,0 +1,26 @@
var path = require('path');
module.exports = function getOptions(argv) {
var dbOptions = argv.dbOptions;
if (typeof dbOptions === 'string') {
dbOptions = require(path.resolve(process.cwd(), argv.dbOptions));
} else if (typeof dbOptions === 'undefined') {
dbOptions = require('../defaultDbOptions.json');
}
return {
host: argv.hostname || process.env.npm_package_remotedev_hostname || null,
port: Number(argv.port || process.env.npm_package_remotedev_port) || 8000,
protocol: argv.protocol || process.env.npm_package_remotedev_protocol || 'http',
protocolOptions: !(argv.protocol === 'https') ? null : {
key: argv.key || process.env.npm_package_remotedev_key || null,
cert: argv.cert || process.env.npm_package_remotedev_cert || null,
passphrase: argv.passphrase || process.env.npm_package_remotedev_passphrase || null
},
dbOptions: dbOptions,
maxRequestBody: argv.passphrase || '16mb',
logHTTPRequests: argv.logHTTPRequests,
logLevel: argv.logLevel || 3,
wsEngine: argv.wsEngine || process.env.npm_package_remotedev_wsengine || 'ws'
};
}

View File

@ -0,0 +1,103 @@
var uuidV4 = require('uuid/v4');
var pick = require('lodash/pick');
var connector = require('./db/connector');
var reports = 'remotedev_reports';
// var payloads = 'remotedev_payloads';
var knex;
var baseFields = ['id', 'title', 'added'];
function error(msg) {
return new Promise(function(resolve, reject) {
return resolve({ error: msg });
});
}
function list(query, fields) {
var r = knex.select(fields || baseFields).from(reports);
if (query) return r.where(query);
return r;
}
function listAll(query) {
var r = knex.select().from(reports);
if (query) return r.where(query);
return r;
}
function get(id) {
if (!id) return error('No id specified.');
return knex(reports).where('id', id).first();
}
function add(data) {
if (!data.type || !data.payload) {
return error('Required parameters aren\'t specified.');
}
if (data.type !== 'ACTIONS' && data.type !== 'STATE') {
return error('Type ' + data.type + ' is not supported yet.');
}
var reportId = uuidV4();
var report = {
id: reportId,
type: data.type,
title: data.title || data.exception && data.exception.message || data.action,
description: data.description,
action: data.action,
payload: data.payload,
preloadedState: data.preloadedState,
screenshot: data.screenshot,
version: data.version,
userAgent: data.userAgent,
user: data.user,
userId: typeof data.user === 'object' ? data.user.id : data.user,
instanceId: data.instanceId,
meta: data.meta,
exception: composeException(data.exception),
added: new Date().toISOString(),
};
if (data.appId) report.appId = data.appId; // TODO check if the id exists and we have access to link it
/*
var payload = {
id: uuid.v4(),
reportId: reportId,
state: data.payload
};
*/
return knex.insert(report).into(reports)
.then(function (){ return byBaseFields(report); })
}
function byBaseFields(data) {
return pick(data, baseFields);
}
function createStore(options) {
knex = connector(options);
return {
list: list,
listAll: listAll,
get: get,
add: add
};
}
function composeException(exception) {
var message = '';
if (exception) {
message = 'Exception thrown: ';
if (exception.message)
message += exception.message;
if (exception.stack)
message += '\n' + exception.stack;
}
return message;
}
module.exports = createStore;

View File

@ -0,0 +1,6 @@
var fs = require('fs');
module.exports = function(name, require) {
return fs.readFileSync(require.resolve(name)).toString();
// return GraphQL.buildSchema(schema);
};

View File

@ -0,0 +1,131 @@
var SCWorker = require("socketcluster/scworker");
var path = require('path');
var app = require('express')();
var bodyParser = require('body-parser');
var cors = require('cors');
var morgan = require('morgan');
var graphiqlMiddleware = require('./middleware/graphiql');
var graphqlMiddleware = require('./middleware/graphql');
var createStore = require('./store');
class Worker extends SCWorker {
run() {
var httpServer = this.httpServer;
var scServer = this.scServer;
var options = this.options;
var store = createStore(options);
var limit = options.maxRequestBody;
var logHTTPRequests = options.logHTTPRequests;
httpServer.on('request', app);
app.set('view engine', 'ejs');
app.set('views', path.resolve(__dirname, '..', 'views'));
if (logHTTPRequests) {
if (typeof logHTTPRequests === 'object') app.use(morgan('combined', logHTTPRequests));
else app.use(morgan('combined'));
}
app.use('/graphiql', graphiqlMiddleware);
app.get('*', function (req, res) {
res.render('index', {port: options.port});
});
app.use(cors({methods: 'POST'}));
app.use(bodyParser.json({limit: limit}));
app.use(bodyParser.urlencoded({limit: limit, extended: false}));
app.use('/graphql', graphqlMiddleware(store));
app.post('/', function (req, res) {
if (!req.body) return res.status(404).end();
switch (req.body.op) {
case 'get':
store.get(req.body.id).then(function (r) {
res.send(r || {});
}).catch(function (error) {
console.error(error);
res.sendStatus(500)
});
break;
case 'list':
store.list(req.body.query, req.body.fields).then(function (r) {
res.send(r);
}).catch(function (error) {
console.error(error);
res.sendStatus(500)
});
break;
default:
store.add(req.body).then(function (r) {
res.send({id: r.id, error: r.error});
scServer.exchange.publish('report', {
type: 'add', data: r
});
}).catch(function (error) {
console.error(error);
res.status(500).send({})
});
}
});
scServer.addMiddleware(scServer.MIDDLEWARE_EMIT, function (req, next) {
var channel = req.event;
var data = req.data;
if (channel.substr(0, 3) === 'sc-' || channel === 'respond' || channel === 'log') {
scServer.exchange.publish(channel, data);
} else if (channel === 'log-noid') {
scServer.exchange.publish('log', {id: req.socket.id, data: data});
}
next();
});
scServer.addMiddleware(scServer.MIDDLEWARE_SUBSCRIBE, function (req, next) {
next();
if (req.channel === 'report') {
store.list().then(function (data) {
req.socket.emit(req.channel, {type: 'list', data: data});
}).catch(function (error) {
console.error(error);
});
}
});
scServer.on('connection', function (socket) {
var channelToWatch, channelToEmit;
socket.on('login', function (credentials, respond) {
if (credentials === 'master') {
channelToWatch = 'respond';
channelToEmit = 'log';
} else {
channelToWatch = 'log';
channelToEmit = 'respond';
}
this.exchange.subscribe('sc-' + socket.id).watch(function (msg) {
socket.emit(channelToWatch, msg);
});
respond(null, channelToWatch);
});
socket.on('getReport', function (id, respond) {
store.get(id).then(function (data) {
respond(null, data);
}).catch(function (error) {
console.error(error);
});
});
socket.on('disconnect', function () {
var channel = this.exchange.channel('sc-' + socket.id);
channel.unsubscribe();
channel.destroy();
scServer.exchange.publish(
channelToEmit,
{id: socket.id, type: 'DISCONNECTED'}
);
});
});
};
}
new Worker();

View File

@ -0,0 +1,186 @@
var childProcess = require('child_process');
var request = require('supertest');
var expect = require('expect');
var scClient = require('socketcluster-client');
var remotedev = require('../');
describe('Server', function() {
var scServer;
this.timeout(5000);
before(function(done) {
scServer = childProcess.fork(__dirname + '/../bin/remotedev.js');
setTimeout(done, 2000);
});
after(function() {
if (scServer) {
scServer.kill();
}
});
describe('Express backend', function() {
it('loads main page', function() {
request('http://localhost:8000')
.get('/')
.expect('Content-Type', /text\/html/)
.expect(200)
.then(function(res) {
expect(res.text).toMatch(/<title>RemoteDev<\/title>/);
})
});
it('resolves an inexistent url', function(done) {
request('http://localhost:8000/jreerfr/123')
.get('/')
.expect('Content-Type', /text\/html/)
.expect(200, done);
});
});
describe('Realtime monitoring', function() {
var socket, socket2, channel;
before(function() {
socket = scClient.connect({ hostname: 'localhost', port: 8000 });
socket.connect();
socket.on('error', function(error) {
console.error('Socket1 error', error);
});
socket2 = scClient.connect({ hostname: 'localhost', port: 8000 });
socket2.connect();
socket.on('error', function(error) {
console.error('Socket2 error', error);
});
});
after(function() {
socket.disconnect();
socket2.disconnect();
});
it('should connect', function(done) {
socket.on('connect', function(status) {
expect(status.id).toExist();
done();
});
});
it('should login', function() {
socket.emit('login', 'master', function(error, channelName) {
if (error) { console.log(error); return; }
expect(channelName).toBe('respond');
channel = socket.subscribe(channelName);
expect(channel.SUBSCRIBED).toBe('subscribed');
});
});
it('should send message', function(done) {
var data = {
"type": "ACTION",
"payload": {
"todos": "do some"
},
"action": {
"timestamp": 1483349708506,
"action": {
"type": "ADD_TODO",
"text": "hggg"
}
},
"instanceId": "tAmA7H5fclyWhvizAAAi",
"name": "LoggerInstance",
"id": "tAmA7H5fclyWhvizAAAi"
};
socket2.emit('login', '', function(error, channelName) {
if (error) { console.log(error); return; }
expect(channelName).toBe('log');
var channel2 = socket2.subscribe(channelName);
expect(channel2.SUBSCRIBED).toBe('subscribed');
channel2.on('subscribe', function() {
channel2.watch(function(message) {
expect(message).toEqual(data);
done();
});
socket.emit(channelName, data);
})
});
});
});
describe('REST backend', function() {
var id;
var report = {
type: 'ACTIONS',
title: 'Test report',
description: 'Test body report',
action: 'SOME_FINAL_ACTION',
payload: '[{"type":"ADD_TODO","text":"hi"},{"type":"SOME_FINAL_ACTION"}]',
preloadedState: '{"todos":[{"text":"Use Redux","completed":false,"id":0}]}',
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36'
};
it('should add a report', function() {
request('http://localhost:8000')
.post('/')
.send(report)
.set('Accept', 'application/json')
.expect('Content-Type', /application\/json/)
.expect(200)
.then(function(res) {
id = res.body.id;
expect(id).toExist();
});
});
it('should get the report', function() {
request('http://localhost:8000')
.post('/')
.send({
op: 'get',
id: id
})
.set('Accept', 'application/json')
.expect('Content-Type', /application\/json/)
.expect(200)
.then(function(res) {
expect(res.body).toInclude(report);
});
});
it('should list reports', function() {
request('http://localhost:8000')
.post('/')
.send({
op: 'list'
})
.set('Accept', 'application/json')
.expect('Content-Type', /application\/json/)
.expect(200)
.then(function(res) {
expect(res.body.length).toBe(1);
expect(res.body[0].id).toBe(id);
expect(res.body[0].title).toBe('Test report');
expect(res.body[0].added).toExist();
});
});
});
describe('GraphQL backend', function() {
it('should get the report', function() {
request('http://localhost:8000')
.post('/graphql')
.send({
query: '{ reports { id, type, title } }'
})
.set('Accept', 'application/json')
.expect('Content-Type', /application\/json/)
.expect(200)
.then(function(res) {
var reports = res.body.data.reports;
expect(reports.length).toBe(1);
expect(reports[0].id).toExist();
expect(reports[0].title).toBe('Test report');
expect(reports[0].type).toBe('ACTIONS');
});
});
});
});

View File

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>RemoteDev</title>
<style>
html {
min-width: 350px;
min-height: 300px;
}
body {
position: fixed;
overflow: hidden;
height: 100%;
width: 100%;
margin: 0;
padding: 0;
}
#root {
height: 100%;
}
#root > div {
height: 100%;
}
</style>
</head>
<body>
<div id="root"></div>
<script src="//unpkg.com/react@0.14/dist/react.min.js"></script>
<script src="//unpkg.com/react-dom@0.14/dist/react-dom.min.js"></script>
<script src="//unpkg.com/remotedev-app/dist/remotedev-app.min.js"></script>
<script>
window.remotedevOptions = {
hostname: location.hostname,
port: <%= port %>,
autoReconnect: true
};
ReactDOM.render(
React.createElement(RemoteDevApp, {
socketOptions: window.remotedevOptions
}),
document.querySelector('#root')
);
</script>
</body>
</html>

791
yarn.lock

File diff suppressed because it is too large Load Diff