Changed codebase to typescript

pull/454/head
ioedeveloper 5 years ago
parent ea9783dba6
commit 1780f57184
  1. 16
      bin/remixd.ts
  2. 100
      package-lock.json
  3. 4
      package.json
  4. 0
      src/index.ts
  5. 52
      src/router.js
  6. 139
      src/services/remixdClient.ts
  7. 3
      src/servicesList.js
  8. 77
      src/websocket.js
  9. 58
      src/websocket.ts
  10. 1
      tsconfig.json
  11. 24
      types/@remixproject/plugin-ws/index.d.ts

@ -1,6 +1,6 @@
#!/usr/bin/env node
const Router = require('../src/router')
const servicesList = require('../src/servicesList')
import WebSocket from '../src/websocket'
import RemixdClient from '../src/services/remixdClient'
const program = require('commander')
program
@ -23,13 +23,11 @@ console.log('\x1b[33m%s\x1b[0m', '[WARN] You may now only use IDE at ' + program
if (program.sharedFolder) {
console.log('\x1b[33m%s\x1b[0m', '[WARN] Any application that runs on your computer can potentially read from and write to all files in the directory.')
console.log('\x1b[33m%s\x1b[0m', '[WARN] Symbolic links are not forwarded to Remix IDE\n')
const sharedFolderrouter = new Router(65520, servicesList['sharedfolder'], { remixIdeUrl: program.remixIde }, (webSocket: WebSocket) => {
servicesList['sharedfolder'].setWebSocket(webSocket)
servicesList['sharedfolder'].setupNotifications(program.sharedFolder)
servicesList['sharedfolder'].sharedFolder(program.sharedFolder, program.readOnly || false)
// buildWebsocketClient(webSocket.connection, new servicesList['sharedfolder']())
})
killCallBack.push(sharedFolderrouter.start())
const remixdClient = new RemixdClient()
const websocketHandler = new WebSocket(65520, { remixIdeUrl: program.remixIde }, remixdClient)
websocketHandler.start()
killCallBack.push(websocketHandler.close)
}
// kill

100
package-lock.json generated

@ -77,10 +77,10 @@
"integrity": "sha512-90hiq6/VqtQgX8Sp0EzeIsv3r+ellbGj4URKj5j30tLlZvRUpnAe9YbYnjl3pJM93GyXU0tghHhvXHq+5rnCKA==",
"dev": true
},
"@types/websocket": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/websocket/-/websocket-1.0.0.tgz",
"integrity": "sha512-MLr8hDM8y7vvdAdnoDEP5LotRoYJj7wgT6mWzCUQH/gHqzS4qcnOT/K4dhC0WimWIUiA3Arj9QAJGGKNRiRZKA==",
"@types/ws": {
"version": "7.2.4",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.2.4.tgz",
"integrity": "sha512-9S6Ask71vujkVyeEXKxjBSUV8ZUB0mjL5la4IncBoheu04bDaYyUKErh1BQcY9+WzOUOiKqz/OnpJHYckbMfNg==",
"dev": true,
"requires": {
"@types/node": "*"
@ -728,15 +728,6 @@
"integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==",
"dev": true
},
"d": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
"integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==",
"requires": {
"es5-ext": "^0.10.50",
"type": "^1.0.1"
}
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@ -907,35 +898,6 @@
"is-symbol": "^1.0.2"
}
},
"es5-ext": {
"version": "0.10.53",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz",
"integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==",
"requires": {
"es6-iterator": "~2.0.3",
"es6-symbol": "~3.1.3",
"next-tick": "~1.0.0"
}
},
"es6-iterator": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
"integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=",
"requires": {
"d": "1",
"es5-ext": "^0.10.35",
"es6-symbol": "^3.1.1"
}
},
"es6-symbol": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz",
"integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==",
"requires": {
"d": "^1.0.1",
"ext": "^1.1.2"
}
},
"escape-goat": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz",
@ -1259,21 +1221,6 @@
}
}
},
"ext": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz",
"integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==",
"requires": {
"type": "^2.0.0"
},
"dependencies": {
"type": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/type/-/type-2.0.0.tgz",
"integrity": "sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow=="
}
}
},
"extend-shallow": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
@ -2004,7 +1951,8 @@
"is-typedarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
"dev": true
},
"is-windows": {
"version": "1.0.2",
@ -2267,7 +2215,8 @@
"nan": {
"version": "2.14.1",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz",
"integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw=="
"integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==",
"optional": true
},
"nanomatch": {
"version": "1.2.13",
@ -2293,11 +2242,6 @@
"integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
"dev": true
},
"next-tick": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
"integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw="
},
"nice-try": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
@ -3508,11 +3452,6 @@
"integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==",
"dev": true
},
"type": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz",
"integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg=="
},
"type-check": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
@ -3532,6 +3471,7 @@
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
"integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
"dev": true,
"requires": {
"is-typedarray": "^1.0.0"
}
@ -3739,18 +3679,6 @@
"spdx-expression-parse": "^3.0.0"
}
},
"websocket": {
"version": "1.0.31",
"resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.31.tgz",
"integrity": "sha512-VAouplvGKPiKFDTeCCO65vYHsyay8DqoBSlzIO3fayrfOgU94lQN5a1uWVnFrMLceTJw/+fQXR5PGbUVRaHshQ==",
"requires": {
"debug": "^2.2.0",
"es5-ext": "^0.10.50",
"nan": "^2.14.0",
"typedarray-to-buffer": "^3.1.5",
"yaeti": "^0.0.6"
}
},
"which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
@ -3802,17 +3730,17 @@
"typedarray-to-buffer": "^3.1.5"
}
},
"ws": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.3.0.tgz",
"integrity": "sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w=="
},
"xdg-basedir": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz",
"integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==",
"dev": true
},
"yaeti": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz",
"integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc="
},
"yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",

@ -36,7 +36,7 @@
"commander": "^2.9.0",
"fs-extra": "^3.0.1",
"isbinaryfile": "^3.0.2",
"websocket": "^1.0.24"
"ws": "^7.3.0"
},
"python": {
"execPath": "python3",
@ -46,7 +46,7 @@
},
"devDependencies": {
"@types/node": "^14.0.5",
"@types/websocket": "^1.0.0",
"@types/ws": "^7.2.4",
"eslint": "6.8.0",
"eslint-config-standard": "14.1.1",
"eslint-plugin-import": "2.20.2",

@ -1,52 +0,0 @@
var Websocket = require('./websocket')
class Router {
constructor (port, service, opt, initCallback) {
this.opt = opt
this.port = port
this.service = service
this.initCallback = initCallback
}
start () {
var websocket = new Websocket(this.port, this.opt)
this.websocket = websocket
this.websocket.start((message) => {
this.call(message.id, message.service, message.fn, message.args)
})
if (this.initCallback) this.initCallback(this.websocket)
return function () {
if (websocket) {
websocket.close()
}
}
}
call (callid, name, fn, args) {
try {
this.service[fn](args, (error, data) => {
var response = {
id: callid,
type: 'reply',
scope: name,
result: data,
error: error
}
this.websocket.send(JSON.stringify(response))
})
} catch (e) {
var msg = 'Unexpected Error ' + e.message
console.log('\x1b[31m%s\x1b[0m', '[ERR] ' + msg)
if (this.websocket) {
this.websocket.send(JSON.stringify({
id: callid,
type: 'reply',
scope: name,
error: msg
}))
}
}
}
}
module.exports = Router

@ -1,75 +1,87 @@
import WebSocket from '../websocket'
var utils = require('../utils')
var isbinaryfile = require('isbinaryfile')
var fs = require('fs-extra')
var chokidar = require('chokidar')
const { PluginClient } = require('@remixproject/plugin')
import { PluginClient } from '@remixproject/plugin'
Object.create(PluginClient, {
trackDownStreamUpdate: {},
websocket: null,
alreadyNotified: {},
export default class RemixdClient extends PluginClient {
trackDownStreamUpdate: {
[key: string]: string
}
websocket: WebSocket | null
currentSharedFolder: string
readOnly: boolean
setWebSocket: function (websocket) {
setWebSocket (websocket: WebSocket) {
this.websocket = websocket
},
}
sharedFolder: function (currentSharedFolder, readOnly) {
sharedFolder (currentSharedFolder: string, readOnly: boolean) {
this.currentSharedFolder = currentSharedFolder
this.readOnly = readOnly
if (this.websocket.connection) this.websocket.send(message('rootFolderChanged', {}))
},
}
list: function (args, cb) {
list (args: {
[key: string]: string
}, cb: Function) {
try {
cb(null, utils.walkSync(this.currentSharedFolder, {}, this.currentSharedFolder))
} catch (e) {
cb(e.message)
}
},
}
resolveDirectory: function (args, cb) {
resolveDirectory (args: {
[key: string]: string
}, cb: Function) {
try {
var path = utils.absolutePath(args.path, this.currentSharedFolder)
if (this.websocket && !this.alreadyNotified[path]) {
this.alreadyNotified[path] = 1
this.setupNotifications(path)
}
cb(null, utils.resolveDirectory(path, this.currentSharedFolder))
} catch (e) {
cb(e.message)
}
},
}
folderIsReadOnly: function (args, cb) {
folderIsReadOnly (args: {
[key: string]: string
}, cb: Function) {
return cb(null, this.readOnly)
},
}
get: function (args, cb) {
get (args: {
[key: string]: string
}, cb: Function) {
var path = utils.absolutePath(args.path, this.currentSharedFolder)
if (!fs.existsSync(path)) {
return cb('File not found ' + path)
}
if (!isRealPath(path, cb)) return
isbinaryfile(path, (error, isBinary) => {
isbinaryfile(path, (error: Error, isBinary: boolean) => {
if (error) console.log(error)
if (isBinary) {
cb(null, { content: '<binary content not displayed>', readonly: true })
} else {
fs.readFile(path, 'utf8', (error, data) => {
fs.readFile(path, 'utf8', (error: Error, data: string) => {
if (error) console.log(error)
cb(error, { content: data, readonly: false })
})
}
})
},
}
exists: function (args, cb) {
exists (args: {
[key: string]: string
}, cb: Function) {
const path = utils.absolutePath(args.path, this.currentSharedFolder)
cb(null, fs.existsSync(path))
},
}
set: function (args, cb) {
set (args: {
[key: string]: string
}, cb: Function) {
if (this.readOnly) return cb('Cannot write file: read-only mode selected')
const isFolder = args.path.endsWith('/')
var path = utils.absolutePath(args.path, this.currentSharedFolder)
@ -80,18 +92,20 @@ Object.create(PluginClient, {
}
this.trackDownStreamUpdate[path] = path
if (isFolder) {
fs.mkdirp(path).then(_ => cb()).catch(e => cb(e))
fs.mkdirp(path).then(() => cb()).catch((e: Error) => cb(e))
} else {
fs.ensureFile(path).then(() => {
fs.writeFile(path, args.content, 'utf8', (error, data) => {
fs.writeFile(path, args.content, 'utf8', (error: Error, data: string) => {
if (error) console.log(error)
cb(error, data)
})
}).catch(e => cb(e))
}).catch((e: Error) => cb(e))
}
}
},
rename: function (args, cb) {
rename (args: {
[key: string]: string
}, cb: Function) {
if (this.readOnly) return cb('Cannot rename file: read-only mode selected')
var oldpath = utils.absolutePath(args.oldPath, this.currentSharedFolder)
if (!fs.existsSync(oldpath)) {
@ -99,73 +113,48 @@ Object.create(PluginClient, {
}
var newpath = utils.absolutePath(args.newPath, this.currentSharedFolder)
if (!isRealPath(oldpath, cb)) return
fs.move(oldpath, newpath, (error, data) => {
fs.move(oldpath, newpath, (error: Error, data: string) => {
if (error) console.log(error)
cb(error, data)
})
},
}
remove: function (args, cb) {
remove (args: {
[key: string]: string
}, cb: Function) {
if (this.readOnly) return cb('Cannot remove file: read-only mode selected')
var path = utils.absolutePath(args.path, this.currentSharedFolder)
if (!fs.existsSync(path)) {
return cb('File not found ' + path)
}
if (!isRealPath(path, cb)) return
fs.remove(path, (error) => {
fs.remove(path, (error: Error) => {
if (error) {
console.log(error)
return cb('Failed to remove file/directory: ' + error)
}
cb(error, true)
})
},
}
isDirectory: function (args, cb) {
isDirectory (args: {
[key: string]: string
}, cb: Function) {
const path = utils.absolutePath(args.path, this.currentSharedFolder)
cb(null, fs.statSync(path).isDirectory())
},
}
isFile: function (args, cb) {
isFile (args: {
[key: string]: string
}, cb: Function) {
const path = utils.absolutePath(args.path, this.currentSharedFolder)
cb(null, fs.statSync(path).isFile())
},
setupNotifications: function (path) {
if (!isRealPath(path)) return
var watcher = chokidar.watch(path, { depth: 0, ignorePermissionErrors: true })
console.log('setup notifications for ' + path)
/* we can't listen on created file / folder
watcher.on('add', (f, stat) => {
isbinaryfile(f, (error, isBinary) => {
if (error) console.log(error)
console.log('add', f)
if (this.websocket.connection) this.websocket.send(message('created', { path: utils.relativePath(f, this.currentSharedFolder), isReadOnly: isBinary, isFolder: false }))
})
})
watcher.on('addDir', (f, stat) => {
if (this.websocket.connection) this.websocket.send(message('created', { path: utils.relativePath(f, this.currentSharedFolder), isReadOnly: false, isFolder: true }))
})
*/
watcher.on('change', (f, curr, prev) => {
if (this.trackDownStreamUpdate[f]) {
delete this.trackDownStreamUpdate[f]
return
}
if (this.websocket.connection) this.websocket.send(message('changed', utils.relativePath(f, this.currentSharedFolder)))
})
watcher.on('unlink', (f) => {
if (this.websocket.connection) this.websocket.send(message('removed', { path: utils.relativePath(f, this.currentSharedFolder), isFolder: false }))
})
watcher.on('unlinkDir', (f) => {
if (this.websocket.connection) this.websocket.send(message('removed', { path: utils.relativePath(f, this.currentSharedFolder), isFolder: true }))
})
}
})
}
function isRealPath (path, cb) {
function isRealPath (path: string, cb: Function) {
var realPath = fs.realpathSync(path)
var isRealPath = path === realPath
var mes = '[WARN] Symbolic link modification not allowed : ' + path + ' | ' + realPath
@ -175,7 +164,3 @@ function isRealPath (path, cb) {
if (cb && !isRealPath) cb(mes)
return isRealPath
}
function message (name, value) {
return JSON.stringify({ type: 'notification', scope: 'sharedfolder', name: name, value: value })
}

@ -1,3 +0,0 @@
module.exports = {
sharedfolder: require('./services/sharedFolder')
}

@ -1,77 +0,0 @@
#!/usr/bin/env node
var WebSocketServer = require('websocket').server
var http = require('http')
class WebSocket {
constructor (port, opt) {
this.connection = null
this.port = port
this.opt = opt
}
start (callback) {
this.server = http.createServer(function (request, response) {
console.log((new Date()) + ' Received request for ' + request.url)
response.writeHead(404)
response.end()
})
var loopback = '127.0.0.1'
this.server.listen(this.port, loopback, function () {
console.log((new Date()) + ' Remixd is listening on ' + loopback + ':65520')
})
this.wsServer = new WebSocketServer({
httpServer: this.server,
autoAcceptConnections: false,
maxReceivedFrameSize: 131072,
maxReceivedMessageSize: 10 * 1024 * 1024
})
this.wsServer.on('request', (request) => {
if (!originIsAllowed(request.origin, this)) {
request.reject()
console.log((new Date()) + ' Connection from origin ' + request.origin + ' rejected.')
return
}
if (this.connection) {
console.log('closing previous connection')
this.wsServer.closeAllConnections()
this.connection = null
return
}
this.connection = request.accept('echo-protocol', request.origin)
console.log((new Date()) + ' Connection accepted.')
this.connection.on('message', (message) => {
if (message.type === 'utf8') {
callback(JSON.parse(message.utf8Data))
}
})
this.connection.on('close', (reasonCode, description) => {
console.log((new Date()) + ' Remix ' + this.connection.remoteAddress + ' disconnected.')
this.connection = null
})
})
}
send (data) {
this.connection.sendUTF(data)
}
close () {
if (this.connection) {
this.connection.close()
this.connection = null
}
if (this.server) {
this.server.close()
this.server = null
}
}
}
function originIsAllowed (origin, self) {
return origin === self.opt.remixIdeUrl
}
module.exports = WebSocket

@ -0,0 +1,58 @@
import * as WS from 'ws'
import * as http from 'http'
import RemixdClient from './services/remixdClient'
import { buildWebsocketClient } from '@remixproject/plugin-ws'
export default class WebSocket {
port: number
opt: {
[key: string]: string
}
server: http.Server
wsServer: WS.Server
connection: WS
remixdClient: RemixdClient
constructor (port: number, opt: {
[key: string]: string
}, remixdClient: RemixdClient) {
this.port = port
this.opt = opt
this.remixdClient = remixdClient
}
start (callback?: Function) {
const obj = this
this.server = http.createServer(function (request, response) {
console.log((new Date()) + ' Received request for ' + request.url)
response.writeHead(404)
response.end()
})
const loopback = '127.0.0.1'
this.server.listen(this.port, loopback, function () {
console.log((new Date()) + ' Remixd is listening on ' + loopback + ':65520')
})
this.wsServer = new WS.Server({ server: this.server })
this.wsServer.on('connection', function connection(ws) {
obj.connection = ws
const client = buildWebsocketClient(obj.connection, obj.remixdClient)
if(callback) callback(client)
})
}
send (data: any) {
this.connection.send(data)
}
close () {
if (this.connection) {
this.connection.close()
}
if (this.server) {
this.server.close()
}
}
}

@ -10,6 +10,7 @@
/* Experimental Options */
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
"emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
"typeRoots": [ "./types"]
},
"exclude": ["./lib"]
}

@ -0,0 +1,24 @@
declare module '@remixproject/plugin-ws' {
import { PluginApi, ApiMap, ProfileMap, Api, RemixApi } from '../utils';
import { PluginClient, PluginOptions } from '@remixproject/plugin/client';
export interface WS {
send(data: string): void;
on(type: 'message', cb: (event: WSData) => any): this;
}
export interface WSData {
toString(): string;
}
export declare function connectWS(socket: WS, client: PluginClient): void;
/**
* Connect the client to the socket
* @param client A plugin client
*/
export declare function buildWebsocketClient<T extends Api, App extends ApiMap = RemixApi>(socket: WS, client: PluginClient<T, App>): PluginApi<GetApi<typeof client.options.customApi>> & PluginClient<T, App>;
/**
* Create a plugin client that listen on socket messages
* @param options The options for the client
*/
export declare function createWebsocketClient<T extends Api, App extends ApiMap = RemixApi>(socket: WS, options?: Partial<PluginOptions<App>>): PluginApi<GetApi<typeof options.customApi>> & PluginClient<T, App>;
declare type GetApi<T> = T extends ProfileMap<infer I> ? I : never;
export {};
}
Loading…
Cancel
Save