@ohos-ports/redux-devtools-cli 5.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +162 -0
- package/app/electron.cjs +29 -0
- package/app/index.html +46 -0
- package/app/package.json +8 -0
- package/bin/redux-devtools.js +3 -0
- package/defaultDbOptions.json +7 -0
- package/dist/api/schema.d.ts +16 -0
- package/dist/api/schema.js +12 -0
- package/dist/api/schema_def.graphql +60 -0
- package/dist/bin/injectServer.d.ts +6 -0
- package/dist/bin/injectServer.js +78 -0
- package/dist/bin/openApp.d.ts +2 -0
- package/dist/bin/openApp.js +40 -0
- package/dist/bin/redux-devtools.d.ts +2 -0
- package/dist/bin/redux-devtools.js +73 -0
- package/dist/db/connector.d.ts +3 -0
- package/dist/db/connector.js +49 -0
- package/dist/db/migrations/index.d.ts +3 -0
- package/dist/db/migrations/index.js +84 -0
- package/dist/db/seeds/index.d.ts +2 -0
- package/dist/db/seeds/index.js +10 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +125 -0
- package/dist/options.d.ts +29 -0
- package/dist/options.js +29 -0
- package/dist/routes.d.ts +5 -0
- package/dist/routes.js +100 -0
- package/dist/store.d.ts +57 -0
- package/dist/store.js +97 -0
- package/package.json +97 -0
- package/src/api/schema.ts +26 -0
- package/src/api/schema_def.graphql +60 -0
- package/src/bin/injectServer.ts +104 -0
- package/src/bin/openApp.ts +53 -0
- package/src/bin/redux-devtools.ts +94 -0
- package/src/db/connector.ts +67 -0
- package/src/db/migrations/index.ts +87 -0
- package/src/db/seeds/index.ts +12 -0
- package/src/index.ts +137 -0
- package/src/options.ts +68 -0
- package/src/routes.ts +144 -0
- package/src/store.ts +164 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# A list of options for the type of the report
|
|
2
|
+
enum ReportType {
|
|
3
|
+
STATE
|
|
4
|
+
ACTION
|
|
5
|
+
STATES
|
|
6
|
+
ACTIONS
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type Report {
|
|
10
|
+
# Report ID
|
|
11
|
+
id: ID!
|
|
12
|
+
# Type of the report, can be: STATE, ACTION, STATES, ACTIONS
|
|
13
|
+
type: ReportType
|
|
14
|
+
# Briefly what happened
|
|
15
|
+
title: String
|
|
16
|
+
# Details supplied by the user
|
|
17
|
+
description: String
|
|
18
|
+
# The last dispatched action before the report was sent
|
|
19
|
+
action: String
|
|
20
|
+
# Stringified actions or the state or both, which should be loaded the application to reproduce the exact behavior
|
|
21
|
+
payload: String
|
|
22
|
+
# Stringified preloaded state object. Could be the initial state of the app or committed state (after dispatching COMMIT action or reaching maxAge)
|
|
23
|
+
preloadedState: String
|
|
24
|
+
# Screenshot url or blob as a string
|
|
25
|
+
screenshot: String
|
|
26
|
+
# User Agent String
|
|
27
|
+
userAgent: String
|
|
28
|
+
# Application version to group the reports and versioning
|
|
29
|
+
version: String
|
|
30
|
+
# Used to identify the user who sent the report
|
|
31
|
+
userId: String
|
|
32
|
+
# More detailed data about the user, usually it's a stringified object
|
|
33
|
+
user: String
|
|
34
|
+
# Everything else you want to send
|
|
35
|
+
meta: String
|
|
36
|
+
# Error message which invoked sending the report
|
|
37
|
+
exception: String
|
|
38
|
+
# Id to identify the store in case there are multiple stores
|
|
39
|
+
instanceId: String
|
|
40
|
+
# Timestamp when the report was added
|
|
41
|
+
added: String
|
|
42
|
+
# Id to identify the application (from apps table)
|
|
43
|
+
appId: ID
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Explore GraphQL query schema
|
|
47
|
+
type Query {
|
|
48
|
+
# List all reports
|
|
49
|
+
reports: [Report]
|
|
50
|
+
# Get a report by ID
|
|
51
|
+
report(
|
|
52
|
+
# Report ID
|
|
53
|
+
id: ID!
|
|
54
|
+
): Report
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
schema {
|
|
58
|
+
query: Query
|
|
59
|
+
#mutation: Mutation
|
|
60
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import semver from 'semver';
|
|
4
|
+
import type { Options } from '../options.js';
|
|
5
|
+
|
|
6
|
+
const name = '@redux-devtools/cli';
|
|
7
|
+
const startFlag = '/* ' + name + ' start */';
|
|
8
|
+
const endFlag = '/* ' + name + ' end */';
|
|
9
|
+
const serverFlags: { [moduleName: string]: { [version: string]: string } } = {
|
|
10
|
+
'react-native': {
|
|
11
|
+
'0.0.1': ' _server(argv, config, resolve, reject);',
|
|
12
|
+
'0.31.0':
|
|
13
|
+
" runServer(args, config, () => console.log('\\nReact packager ready.\\n'));",
|
|
14
|
+
'0.44.0-rc.0': ' runServer(args, config, startedCallback, readyCallback);',
|
|
15
|
+
'0.46.0-rc.0':
|
|
16
|
+
' runServer(runServerArgs, configT, startedCallback, readyCallback);',
|
|
17
|
+
'0.57.0': ' runServer(args, configT);',
|
|
18
|
+
},
|
|
19
|
+
'react-native-desktop': {
|
|
20
|
+
'0.0.1': ' _server(argv, config, resolve, reject);',
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function getModuleVersion(modulePath: string): string {
|
|
25
|
+
return JSON.parse(
|
|
26
|
+
fs.readFileSync(path.join(modulePath, 'package.json'), 'utf-8'),
|
|
27
|
+
).version;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getServerFlag(moduleName: string, version: string): string {
|
|
31
|
+
const flags = serverFlags[moduleName || 'react-native'];
|
|
32
|
+
const versions = Object.keys(flags);
|
|
33
|
+
let flag;
|
|
34
|
+
for (let i = 0; i < versions.length; i++) {
|
|
35
|
+
if (semver.gte(version, versions[i])) {
|
|
36
|
+
flag = flags[versions[i]];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return flag as string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const dir = 'local-cli/server';
|
|
43
|
+
export const file = 'server.js';
|
|
44
|
+
export const fullPath = path.join(dir, file);
|
|
45
|
+
|
|
46
|
+
export function inject(
|
|
47
|
+
modulePath: string,
|
|
48
|
+
options: Options,
|
|
49
|
+
moduleName: string,
|
|
50
|
+
) {
|
|
51
|
+
const filePath = path.join(modulePath, fullPath);
|
|
52
|
+
if (!fs.existsSync(filePath)) return false;
|
|
53
|
+
|
|
54
|
+
const serverFlag = getServerFlag(moduleName, getModuleVersion(modulePath));
|
|
55
|
+
const code = [
|
|
56
|
+
startFlag,
|
|
57
|
+
' require("' + name + '")(' + JSON.stringify(options) + ')',
|
|
58
|
+
' .then(_remotedev =>',
|
|
59
|
+
' _remotedev.ready.then(() => {',
|
|
60
|
+
' if (!_remotedev.portAlreadyUsed) console.log("-".repeat(80));',
|
|
61
|
+
' ' + serverFlag,
|
|
62
|
+
' })',
|
|
63
|
+
' );',
|
|
64
|
+
endFlag,
|
|
65
|
+
].join('\n');
|
|
66
|
+
|
|
67
|
+
const serverCode = fs.readFileSync(filePath, 'utf-8');
|
|
68
|
+
let start = serverCode.indexOf(startFlag); // already injected ?
|
|
69
|
+
let end = serverCode.indexOf(endFlag) + endFlag.length;
|
|
70
|
+
if (start === -1) {
|
|
71
|
+
start = serverCode.indexOf(serverFlag);
|
|
72
|
+
end = start + serverFlag.length;
|
|
73
|
+
}
|
|
74
|
+
fs.writeFileSync(
|
|
75
|
+
filePath,
|
|
76
|
+
serverCode.substr(0, start) +
|
|
77
|
+
code +
|
|
78
|
+
serverCode.substr(end, serverCode.length),
|
|
79
|
+
);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function revert(
|
|
84
|
+
modulePath: string,
|
|
85
|
+
options: Options,
|
|
86
|
+
moduleName: string,
|
|
87
|
+
) {
|
|
88
|
+
const filePath = path.join(modulePath, fullPath);
|
|
89
|
+
if (!fs.existsSync(filePath)) return false;
|
|
90
|
+
|
|
91
|
+
const serverFlag = getServerFlag(moduleName, getModuleVersion(modulePath));
|
|
92
|
+
const serverCode = fs.readFileSync(filePath, 'utf-8');
|
|
93
|
+
const start = serverCode.indexOf(startFlag); // already injected ?
|
|
94
|
+
const end = serverCode.indexOf(endFlag) + endFlag.length;
|
|
95
|
+
if (start !== -1) {
|
|
96
|
+
fs.writeFileSync(
|
|
97
|
+
filePath,
|
|
98
|
+
serverCode.substr(0, start) +
|
|
99
|
+
serverFlag +
|
|
100
|
+
serverCode.substr(end, serverCode.length),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import open from 'open';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { createRequire } from 'module';
|
|
5
|
+
import spawn from 'cross-spawn';
|
|
6
|
+
import type { Options } from '../options.js';
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
|
|
10
|
+
export default async function openApp(app: true | string, options: Options) {
|
|
11
|
+
if (app === true || app === 'electron') {
|
|
12
|
+
try {
|
|
13
|
+
const port = options.port ? `--port=${options.port}` : '';
|
|
14
|
+
const host = options.host ? `--host=${options.host}` : '';
|
|
15
|
+
const protocol = options.protocol ? `--protocol=${options.protocol}` : '';
|
|
16
|
+
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
18
|
+
spawn(require('electron') as string, [
|
|
19
|
+
path.join(
|
|
20
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
21
|
+
'..',
|
|
22
|
+
'..',
|
|
23
|
+
'app',
|
|
24
|
+
),
|
|
25
|
+
port,
|
|
26
|
+
host,
|
|
27
|
+
protocol,
|
|
28
|
+
]);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
/* eslint-disable no-console */
|
|
31
|
+
if ((error as Error).message === "Cannot find module 'electron'") {
|
|
32
|
+
// TODO: Move electron to dev-dependences to make our package installation faster when not needed.
|
|
33
|
+
console.log(
|
|
34
|
+
' \x1b[1;31m[Warn]\x1b[0m Electron module not installed.\n',
|
|
35
|
+
);
|
|
36
|
+
/*
|
|
37
|
+
We will use "npm" to install Electron via "npm install -D".
|
|
38
|
+
Do you want to install 'electron' (yes/no): yes
|
|
39
|
+
Installing 'electron' (running 'npm install -D webpack-cli')...
|
|
40
|
+
*/
|
|
41
|
+
} else {
|
|
42
|
+
console.log(error);
|
|
43
|
+
}
|
|
44
|
+
/* eslint-enable no-console */
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
await open(
|
|
50
|
+
`${options.protocol}://${options.host ?? 'localhost'}:${options.port}/`,
|
|
51
|
+
app !== 'browser' ? { app: { name: app } } : undefined,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#! /usr/bin/env node
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import parseArgs from 'minimist';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import * as injectServer from './injectServer.js';
|
|
7
|
+
import getOptions from '../options.js';
|
|
8
|
+
import server from '../index.js';
|
|
9
|
+
import openApp from './openApp.js';
|
|
10
|
+
|
|
11
|
+
const argv = parseArgs(process.argv.slice(2));
|
|
12
|
+
|
|
13
|
+
const options = getOptions(argv);
|
|
14
|
+
|
|
15
|
+
function readFile(filePath: string) {
|
|
16
|
+
return fs.readFileSync(path.resolve(process.cwd(), filePath), 'utf-8');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (argv.protocol === 'https') {
|
|
20
|
+
argv.key = argv.key ? readFile(argv.key as string) : null;
|
|
21
|
+
argv.cert = argv.cert ? readFile(argv.cert as string) : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function log(pass: boolean, msg: string) {
|
|
25
|
+
const prefix = pass ? chalk.green.bgBlack('PASS') : chalk.red.bgBlack('FAIL');
|
|
26
|
+
const color = pass ? chalk.blue : chalk.red;
|
|
27
|
+
console.log(prefix, color(msg)); // eslint-disable-line no-console
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getModuleName(type: string) {
|
|
31
|
+
switch (type) {
|
|
32
|
+
case 'macos':
|
|
33
|
+
return 'react-native-macos';
|
|
34
|
+
// react-native-macos is renamed from react-native-desktop
|
|
35
|
+
case 'desktop':
|
|
36
|
+
return 'react-native-desktop';
|
|
37
|
+
case 'reactnative':
|
|
38
|
+
default:
|
|
39
|
+
return 'react-native';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getModulePath(moduleName: string) {
|
|
44
|
+
return path.join(process.cwd(), 'node_modules', moduleName);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getModule(type: string) {
|
|
48
|
+
let moduleName = getModuleName(type);
|
|
49
|
+
let modulePath = getModulePath(moduleName);
|
|
50
|
+
if (type === 'desktop' && !fs.existsSync(modulePath)) {
|
|
51
|
+
moduleName = getModuleName('macos');
|
|
52
|
+
modulePath = getModulePath(moduleName);
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
name: moduleName,
|
|
56
|
+
path: modulePath,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function injectRN(type: string, msg: string) {
|
|
61
|
+
const module = getModule(type);
|
|
62
|
+
const fn = type === 'revert' ? injectServer.revert : injectServer.inject;
|
|
63
|
+
const pass = fn(module.path, options, module.name);
|
|
64
|
+
log(
|
|
65
|
+
pass,
|
|
66
|
+
msg +
|
|
67
|
+
(pass
|
|
68
|
+
? '.'
|
|
69
|
+
: ', the file `' +
|
|
70
|
+
path.join(module.name, injectServer.fullPath) +
|
|
71
|
+
'` not found.'),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
process.exit(pass ? 0 : 1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (argv.revert) {
|
|
78
|
+
injectRN(
|
|
79
|
+
argv.revert as string,
|
|
80
|
+
'Revert injection of ReduxDevTools server from React Native local server',
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
if (argv.injectserver) {
|
|
84
|
+
injectRN(
|
|
85
|
+
argv.injectserver as string,
|
|
86
|
+
'Inject ReduxDevTools server into React Native local server',
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const response = await server(argv);
|
|
91
|
+
if (argv.open && argv.open !== 'false') {
|
|
92
|
+
await response.ready;
|
|
93
|
+
await openApp(argv.open as string, options);
|
|
94
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import knex from 'knex';
|
|
4
|
+
import type { Knex } from 'knex';
|
|
5
|
+
import { AGServer } from 'socketcluster-server';
|
|
6
|
+
|
|
7
|
+
// HarmonyOS compatibility: knex's sqlite3 dialect hardcodes require('sqlite3'),
|
|
8
|
+
// but on HarmonyOS the driver is published as '@ohos-ports/sqlite3'. Install a
|
|
9
|
+
// module resolver alias so knex resolves 'sqlite3' to the HarmonyOS driver.
|
|
10
|
+
import { createRequire } from 'module';
|
|
11
|
+
const nodeRequire = createRequire(import.meta.url);
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
+
const nodeModule = nodeRequire('module') as any;
|
|
14
|
+
const originalResolveFilename = nodeModule._resolveFilename as (
|
|
15
|
+
request: string,
|
|
16
|
+
...args: unknown[]
|
|
17
|
+
) => string;
|
|
18
|
+
nodeModule._resolveFilename = function (request: string, ...args: unknown[]) {
|
|
19
|
+
if (request === 'sqlite3') {
|
|
20
|
+
try {
|
|
21
|
+
request = nodeRequire.resolve('@ohos-ports/sqlite3');
|
|
22
|
+
} catch {
|
|
23
|
+
// driver unavailable; fall through to the original resolver
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return originalResolveFilename.call(this, request, ...args);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
30
|
+
type KnexFunction = <TRecord extends {} = any, TResult = unknown[]>(
|
|
31
|
+
config: Knex.Config | string,
|
|
32
|
+
) => Knex<TRecord, TResult>;
|
|
33
|
+
|
|
34
|
+
export default function connector(options: AGServer.AGServerOptions) {
|
|
35
|
+
const dbOptions = options.dbOptions as Knex.Config;
|
|
36
|
+
dbOptions.useNullAsDefault = true;
|
|
37
|
+
if (!(dbOptions as any).migrate) {
|
|
38
|
+
return (knex as unknown as KnexFunction)(dbOptions);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
dbOptions.migrations = {
|
|
42
|
+
directory: path.join(
|
|
43
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
44
|
+
'migrations',
|
|
45
|
+
),
|
|
46
|
+
};
|
|
47
|
+
dbOptions.seeds = {
|
|
48
|
+
directory: path.join(path.dirname(fileURLToPath(import.meta.url)), 'seeds'),
|
|
49
|
+
};
|
|
50
|
+
const knexInstance = (knex as unknown as KnexFunction)(dbOptions);
|
|
51
|
+
|
|
52
|
+
/* eslint-disable no-console */
|
|
53
|
+
knexInstance.migrate
|
|
54
|
+
.latest({ loadExtensions: ['.js'] })
|
|
55
|
+
.then(function () {
|
|
56
|
+
return knexInstance.seed.run({ loadExtensions: ['.js'] });
|
|
57
|
+
})
|
|
58
|
+
.then(function () {
|
|
59
|
+
console.log(' \x1b[0;32m[Done]\x1b[0m Migrations are finished\n');
|
|
60
|
+
})
|
|
61
|
+
.catch(function (error) {
|
|
62
|
+
console.error(error);
|
|
63
|
+
});
|
|
64
|
+
/* eslint-enable no-console */
|
|
65
|
+
|
|
66
|
+
return knexInstance;
|
|
67
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Knex } from 'knex';
|
|
2
|
+
|
|
3
|
+
export function up(knex: Knex) {
|
|
4
|
+
return Promise.all([
|
|
5
|
+
knex.schema.createTable('remotedev_reports', function (table) {
|
|
6
|
+
table.uuid('id').primary();
|
|
7
|
+
table.string('type');
|
|
8
|
+
table.string('title');
|
|
9
|
+
table.string('description');
|
|
10
|
+
table.string('action');
|
|
11
|
+
table.text('payload', 'longtext');
|
|
12
|
+
table.text('preloadedState', 'longtext');
|
|
13
|
+
table.text('screenshot', 'longtext');
|
|
14
|
+
table.string('userAgent');
|
|
15
|
+
table.string('version');
|
|
16
|
+
table.string('user');
|
|
17
|
+
table.string('userId');
|
|
18
|
+
table.string('instanceId');
|
|
19
|
+
table.string('meta');
|
|
20
|
+
table.string('exception');
|
|
21
|
+
table.timestamp('added').defaultTo(knex.fn.now());
|
|
22
|
+
table
|
|
23
|
+
.uuid('appId')
|
|
24
|
+
.references('id')
|
|
25
|
+
.inTable('remotedev_apps')
|
|
26
|
+
.onDelete('CASCADE')
|
|
27
|
+
.onUpdate('CASCADE')
|
|
28
|
+
.defaultTo('78626c31-e16b-4528-b8e5-f81301b627f4');
|
|
29
|
+
}),
|
|
30
|
+
knex.schema.createTable('remotedev_payloads', function (table) {
|
|
31
|
+
table.uuid('id').primary();
|
|
32
|
+
table.text('state');
|
|
33
|
+
table.text('action');
|
|
34
|
+
table.timestamp('added').defaultTo(knex.fn.now());
|
|
35
|
+
table
|
|
36
|
+
.uuid('reportId')
|
|
37
|
+
.references('id')
|
|
38
|
+
.inTable('remotedev_reports')
|
|
39
|
+
.onDelete('CASCADE')
|
|
40
|
+
.onUpdate('CASCADE');
|
|
41
|
+
}),
|
|
42
|
+
knex.schema.createTable('remotedev_apps', function (table) {
|
|
43
|
+
table.uuid('id').primary();
|
|
44
|
+
table.string('title');
|
|
45
|
+
table.string('description');
|
|
46
|
+
table.string('url');
|
|
47
|
+
table.timestamps(false, true);
|
|
48
|
+
}),
|
|
49
|
+
knex.schema.createTable('remotedev_users', function (table) {
|
|
50
|
+
table.uuid('id').primary();
|
|
51
|
+
table.string('name');
|
|
52
|
+
table.string('login');
|
|
53
|
+
table.string('email');
|
|
54
|
+
table.string('avatarUrl');
|
|
55
|
+
table.string('profileUrl');
|
|
56
|
+
table.string('oauthId');
|
|
57
|
+
table.string('oauthType');
|
|
58
|
+
table.string('token');
|
|
59
|
+
table.timestamps(false, true);
|
|
60
|
+
}),
|
|
61
|
+
knex.schema.createTable('remotedev_users_apps', function (table) {
|
|
62
|
+
table.boolean('readOnly').defaultTo(false);
|
|
63
|
+
table.uuid('userId');
|
|
64
|
+
table.uuid('appId');
|
|
65
|
+
table.primary(['userId', 'appId']);
|
|
66
|
+
table
|
|
67
|
+
.foreign('userId')
|
|
68
|
+
.references('id')
|
|
69
|
+
.inTable('remotedev_users')
|
|
70
|
+
.onDelete('CASCADE')
|
|
71
|
+
.onUpdate('CASCADE');
|
|
72
|
+
table
|
|
73
|
+
.foreign('appId')
|
|
74
|
+
.references('id')
|
|
75
|
+
.inTable('remotedev_apps')
|
|
76
|
+
.onDelete('CASCADE')
|
|
77
|
+
.onUpdate('CASCADE');
|
|
78
|
+
}),
|
|
79
|
+
]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function down(knex: Knex) {
|
|
83
|
+
return Promise.all([
|
|
84
|
+
knex.schema.dropTable('remotedev_reports'),
|
|
85
|
+
knex.schema.dropTable('remotedev_apps'),
|
|
86
|
+
]);
|
|
87
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Knex } from 'knex';
|
|
2
|
+
|
|
3
|
+
export function seed(knex: Knex) {
|
|
4
|
+
return Promise.all([knex('remotedev_apps').del()]).then(function () {
|
|
5
|
+
return Promise.all([
|
|
6
|
+
knex('remotedev_apps').insert({
|
|
7
|
+
id: '78626c31-e16b-4528-b8e5-f81301b627f4',
|
|
8
|
+
title: 'Default',
|
|
9
|
+
}),
|
|
10
|
+
]);
|
|
11
|
+
});
|
|
12
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import http from 'http';
|
|
3
|
+
import getPort from 'get-port';
|
|
4
|
+
import socketClusterServer from 'socketcluster-server';
|
|
5
|
+
import getOptions from './options.js';
|
|
6
|
+
import routes from './routes.js';
|
|
7
|
+
import createStore from './store.js';
|
|
8
|
+
|
|
9
|
+
// const LOG_LEVEL_NONE = 0;
|
|
10
|
+
// const LOG_LEVEL_ERROR = 1;
|
|
11
|
+
const LOG_LEVEL_WARN = 2;
|
|
12
|
+
const LOG_LEVEL_INFO = 3;
|
|
13
|
+
|
|
14
|
+
export default async function (argv: { [arg: string]: any }): Promise<{
|
|
15
|
+
portAlreadyUsed?: boolean;
|
|
16
|
+
ready: Promise<void>;
|
|
17
|
+
}> {
|
|
18
|
+
const options = Object.assign(getOptions(argv), {
|
|
19
|
+
allowClientPublish: false,
|
|
20
|
+
});
|
|
21
|
+
const port = options.port;
|
|
22
|
+
const logLevel =
|
|
23
|
+
options.logLevel === undefined ? LOG_LEVEL_INFO : options.logLevel;
|
|
24
|
+
// Check port already used
|
|
25
|
+
const p = await getPort({ port });
|
|
26
|
+
if (port !== p) {
|
|
27
|
+
if (logLevel >= LOG_LEVEL_WARN) {
|
|
28
|
+
console.log(`[ReduxDevTools] Server port ${port} is already used.`);
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
portAlreadyUsed: true,
|
|
32
|
+
ready: Promise.resolve(),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (logLevel >= LOG_LEVEL_INFO) {
|
|
37
|
+
console.log('[ReduxDevTools] Start server...');
|
|
38
|
+
console.log('-'.repeat(80) + '\n');
|
|
39
|
+
}
|
|
40
|
+
const httpServer = http.createServer();
|
|
41
|
+
const agServer = socketClusterServer.attach(httpServer, options);
|
|
42
|
+
|
|
43
|
+
const app = express();
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
45
|
+
httpServer.on('request', app);
|
|
46
|
+
const store = createStore(options);
|
|
47
|
+
app.use(routes(options, store, agServer));
|
|
48
|
+
|
|
49
|
+
agServer.setMiddleware(
|
|
50
|
+
agServer.MIDDLEWARE_INBOUND,
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
52
|
+
async (middlewareStream) => {
|
|
53
|
+
for await (const action of middlewareStream) {
|
|
54
|
+
if (action.type === action.TRANSMIT) {
|
|
55
|
+
const channel = action.receiver;
|
|
56
|
+
const data = action.data;
|
|
57
|
+
if (
|
|
58
|
+
channel.substring(0, 3) === 'sc-' ||
|
|
59
|
+
channel === 'respond' ||
|
|
60
|
+
channel === 'log'
|
|
61
|
+
) {
|
|
62
|
+
void agServer.exchange.transmitPublish(channel, data);
|
|
63
|
+
} else if (channel === 'log-noid') {
|
|
64
|
+
void agServer.exchange.transmitPublish('log', {
|
|
65
|
+
id: action.socket.id,
|
|
66
|
+
data: data,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
} else if (action.type === action.SUBSCRIBE) {
|
|
70
|
+
if (action.channel === 'report') {
|
|
71
|
+
store
|
|
72
|
+
.list()
|
|
73
|
+
.then(function (data) {
|
|
74
|
+
void agServer.exchange.transmitPublish('report', {
|
|
75
|
+
type: 'list',
|
|
76
|
+
data: data,
|
|
77
|
+
});
|
|
78
|
+
})
|
|
79
|
+
.catch(function (error) {
|
|
80
|
+
console.error(error);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
action.allow();
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
void (async () => {
|
|
90
|
+
for await (const { socket } of agServer.listener('connection')) {
|
|
91
|
+
let channelToWatch: string, channelToEmit: string;
|
|
92
|
+
void (async () => {
|
|
93
|
+
for await (const request of socket.procedure('login')) {
|
|
94
|
+
const credentials = request.data;
|
|
95
|
+
if (credentials === 'master') {
|
|
96
|
+
channelToWatch = 'respond';
|
|
97
|
+
channelToEmit = 'log';
|
|
98
|
+
} else {
|
|
99
|
+
channelToWatch = 'log';
|
|
100
|
+
channelToEmit = 'respond';
|
|
101
|
+
}
|
|
102
|
+
request.end(channelToWatch);
|
|
103
|
+
}
|
|
104
|
+
})();
|
|
105
|
+
void (async () => {
|
|
106
|
+
for await (const request of socket.procedure('getReport')) {
|
|
107
|
+
const id = request.data as string;
|
|
108
|
+
store
|
|
109
|
+
.get(id)
|
|
110
|
+
.then(function (data) {
|
|
111
|
+
request.end(data);
|
|
112
|
+
})
|
|
113
|
+
.catch(function (error) {
|
|
114
|
+
console.error(error);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
})();
|
|
118
|
+
void (async () => {
|
|
119
|
+
for await (const data of socket.listener('disconnect')) {
|
|
120
|
+
const channel = agServer.exchange.channel('sc-' + socket.id);
|
|
121
|
+
channel.unsubscribe();
|
|
122
|
+
void agServer.exchange.transmitPublish(channelToEmit!, {
|
|
123
|
+
id: socket.id,
|
|
124
|
+
type: 'DISCONNECTED',
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
})();
|
|
128
|
+
}
|
|
129
|
+
})();
|
|
130
|
+
|
|
131
|
+
httpServer.listen(options.port);
|
|
132
|
+
return {
|
|
133
|
+
ready: (async () => {
|
|
134
|
+
await agServer.listener('ready' as 'error').once();
|
|
135
|
+
})(),
|
|
136
|
+
};
|
|
137
|
+
}
|
package/src/options.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
|
|
3
|
+
interface ProtocolOptions {
|
|
4
|
+
key: string | undefined;
|
|
5
|
+
cert: string | undefined;
|
|
6
|
+
passphrase: string | undefined;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface DbOptions {
|
|
10
|
+
client: string;
|
|
11
|
+
connection: {
|
|
12
|
+
filename: string;
|
|
13
|
+
};
|
|
14
|
+
useNullAsDefault: boolean;
|
|
15
|
+
debug: boolean;
|
|
16
|
+
migrate: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface Options {
|
|
20
|
+
host: string | undefined;
|
|
21
|
+
port: number;
|
|
22
|
+
protocol: 'http' | 'https';
|
|
23
|
+
protocolOptions: ProtocolOptions | undefined;
|
|
24
|
+
dbOptions: DbOptions;
|
|
25
|
+
maxRequestBody: string;
|
|
26
|
+
logHTTPRequests?: boolean;
|
|
27
|
+
logLevel: 0 | 1 | 3 | 2;
|
|
28
|
+
wsEngine: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default function getOptions(argv: { [arg: string]: any }): Options {
|
|
32
|
+
let dbOptions = argv.dbOptions;
|
|
33
|
+
if (typeof dbOptions === 'string') {
|
|
34
|
+
dbOptions = JSON.parse(fs.readFileSync(dbOptions, 'utf8'));
|
|
35
|
+
} else if (typeof dbOptions === 'undefined') {
|
|
36
|
+
dbOptions = JSON.parse(
|
|
37
|
+
fs.readFileSync(
|
|
38
|
+
new URL('../defaultDbOptions.json', import.meta.url),
|
|
39
|
+
'utf8',
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
host:
|
|
46
|
+
argv.hostname || process.env.npm_package_remotedev_hostname || undefined,
|
|
47
|
+
port: Number(argv.port || process.env.npm_package_remotedev_port) || 8000,
|
|
48
|
+
protocol:
|
|
49
|
+
argv.protocol || process.env.npm_package_remotedev_protocol || 'http',
|
|
50
|
+
protocolOptions: !(argv.protocol === 'https')
|
|
51
|
+
? undefined
|
|
52
|
+
: {
|
|
53
|
+
key: argv.key || process.env.npm_package_remotedev_key || undefined,
|
|
54
|
+
cert:
|
|
55
|
+
argv.cert || process.env.npm_package_remotedev_cert || undefined,
|
|
56
|
+
passphrase:
|
|
57
|
+
argv.passphrase ||
|
|
58
|
+
process.env.npm_package_remotedev_passphrase ||
|
|
59
|
+
undefined,
|
|
60
|
+
},
|
|
61
|
+
dbOptions: dbOptions,
|
|
62
|
+
maxRequestBody: argv.passphrase || '16mb',
|
|
63
|
+
logHTTPRequests: argv.logHTTPRequests,
|
|
64
|
+
logLevel: argv.logLevel || 3,
|
|
65
|
+
wsEngine:
|
|
66
|
+
argv.wsEngine || process.env.npm_package_remotedev_wsengine || 'ws',
|
|
67
|
+
};
|
|
68
|
+
}
|