mirror of
https://github.com/reduxjs/redux-devtools.git
synced 2024-11-22 09:36:43 +03:00
Merge remotedev-server
This commit is contained in:
parent
9de536fae2
commit
4849081263
21
packages/redux-devtools-cli/LICENSE.md
Normal file
21
packages/redux-devtools-cli/LICENSE.md
Normal 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.
|
119
packages/redux-devtools-cli/README.md
Normal file
119
packages/redux-devtools-cli/README.md
Normal 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
|
98
packages/redux-devtools-cli/bin/injectServer.js
Normal file
98
packages/redux-devtools-cli/bin/injectServer.js
Normal 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;
|
||||
};
|
73
packages/redux-devtools-cli/bin/remotedev.js
Executable file
73
packages/redux-devtools-cli/bin/remotedev.js
Executable 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);
|
13
packages/redux-devtools-cli/defaultDbOptions.json
Normal file
13
packages/redux-devtools-cli/defaultDbOptions.json
Normal 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
|
||||
}
|
40
packages/redux-devtools-cli/index.js
Normal file
40
packages/redux-devtools-cli/index.js
Normal 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));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
63
packages/redux-devtools-cli/package.json
Normal file
63
packages/redux-devtools-cli/package.json
Normal 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"
|
||||
}
|
||||
}
|
21
packages/redux-devtools-cli/src/api/schema.js
Normal file
21
packages/redux-devtools-cli/src/api/schema.js
Normal 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;
|
60
packages/redux-devtools-cli/src/api/schema_def.graphql
Normal file
60
packages/redux-devtools-cli/src/api/schema_def.graphql
Normal 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
|
||||
}
|
27
packages/redux-devtools-cli/src/db/connector.js
Normal file
27
packages/redux-devtools-cli/src/db/connector.js
Normal 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;
|
||||
};
|
71
packages/redux-devtools-cli/src/db/migrations/index.js
Normal file
71
packages/redux-devtools-cli/src/db/migrations/index.js
Normal 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')
|
||||
])
|
||||
};
|
12
packages/redux-devtools-cli/src/db/seeds/index.js
Normal file
12
packages/redux-devtools-cli/src/db/seeds/index.js
Normal 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'
|
||||
})
|
||||
]);
|
||||
});
|
||||
};
|
13
packages/redux-devtools-cli/src/middleware/graphiql.js
Normal file
13
packages/redux-devtools-cli/src/middleware/graphiql.js
Normal 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' +
|
||||
'}'
|
||||
});
|
13
packages/redux-devtools-cli/src/middleware/graphql.js
Normal file
13
packages/redux-devtools-cli/src/middleware/graphql.js
Normal 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
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
26
packages/redux-devtools-cli/src/options.js
Normal file
26
packages/redux-devtools-cli/src/options.js
Normal 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'
|
||||
};
|
||||
}
|
103
packages/redux-devtools-cli/src/store.js
Normal file
103
packages/redux-devtools-cli/src/store.js
Normal 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;
|
6
packages/redux-devtools-cli/src/utils/requireSchema.js
Normal file
6
packages/redux-devtools-cli/src/utils/requireSchema.js
Normal 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);
|
||||
};
|
131
packages/redux-devtools-cli/src/worker.js
Normal file
131
packages/redux-devtools-cli/src/worker.js
Normal 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();
|
186
packages/redux-devtools-cli/test/integration.spec.js
Normal file
186
packages/redux-devtools-cli/test/integration.spec.js
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
46
packages/redux-devtools-cli/views/index.ejs
Normal file
46
packages/redux-devtools-cli/views/index.ejs
Normal 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>
|
Loading…
Reference in New Issue
Block a user