pokemon-showdown/lib/repl.ts
Guangcong Luo 87ef0cd5c4 TypeScript: export default
I'm currently pretty annoyed at TypeScript and TC39 for default exports
being a mess.

The goal here is to be able to type

    import Dex from './dex';

instead of any of

    import Dex = require('./dex');
    import {Dex} from './dex';
    import * as Dex from './dex';

This part involves a significant amount of struggle.

First, you can't automatically package up all your exports as your
default export. This leads to extremely un-DRY code, like in sim/index:

    export {
        Pokemon,
        Side,
        Battle,
        PRNG,
        Dex,
        TeamValidator,

        BattleStream,
    };

    export const Sim = {
        Pokemon,
        Side,
        Battle,
        PRNG,
        Dex,
        TeamValidator,

        BattleStream,
    };

(Both of these exports would be entirely unnecessary if you could just
automatically declare the file's exports as a default namespace.)

Second, a default export can't easily be a namespace. And TypeScript
doesn't allow types to exist in objects. Take the example from earlier:

    export const Sim = {
        Pokemon,
    };

If we were to try to use it:

    import Sim from './sim';
    let pokemon: Sim.Pokemon;

you'll get this error:

    Cannot find namespace 'Sim'. ts(2503)

You can, of course, fix this by making Sim a namespace:

    const PokemonT = Pokemon;
    type PokemonT = Pokemon;
    export namespace Sim {
        export const Pokemon = PokemonT;
        type Pokemon = PokemonT;
    }

But this quickly gets ridiculous the more classes you try to export.

You'd think there'd be a better way to do this. But I, at least,
haven't found one.
2019-05-14 20:33:33 +10:00

123 lines
3.2 KiB
TypeScript

/**
* REPL
*
* Documented in logs/repl/README.md
* https://github.com/Zarel/Pokemon-Showdown/blob/master/logs/repl/README.md
*
* @author kota
* @license MIT
*/
import * as fs from 'fs';
import * as net from 'net';
import * as path from 'path';
import * as repl from 'repl';
import crashlogger from './crashlogger';
export const Repl = new class {
/**
* Contains the pathnames of all active REPL sockets.
*/
socketPathnames: Set<string> = new Set();
listenersSetup: boolean = false;
setupListeners() {
if (Repl.listenersSetup) return;
Repl.listenersSetup = true;
// Clean up REPL sockets and child processes on forced exit.
process.once('exit', code => {
for (const s of Repl.socketPathnames) {
try {
fs.unlinkSync(s);
} catch (e) {}
}
if (code === 129 || code === 130) {
process.exitCode = 0;
}
});
if (!process.listeners('SIGHUP').length) {
process.once('SIGHUP', () => process.exit(128 + 1));
}
if (!process.listeners('SIGINT').length) {
process.once('SIGINT', () => process.exit(128 + 2));
}
}
/**
* Starts a REPL server, using a UNIX socket for IPC. The eval function
* parametre is passed in because there is no other way to access a file's
* non-global context.
*/
start(filename: string, evalFunction: (input: string) => any) {
if ('repl' in Config && !Config.repl) return;
// TODO: Windows does support the REPL when using named pipes. For now,
// this only supports UNIX sockets.
if (process.platform === 'win32') return;
Repl.setupListeners();
if (filename === 'app') {
// Clean up old REPL sockets.
const directory = path.dirname(path.resolve(__dirname, '..', Config.replsocketprefix || 'logs/repl', 'app'));
for (const file of fs.readdirSync(directory)) {
const pathname = path.resolve(directory, file);
const stat = fs.statSync(pathname);
if (!stat.isSocket()) continue;
const socket = net.connect(pathname, () => {
socket.end();
socket.destroy();
}).on('error', () => {
fs.unlink(pathname, () => {});
});
}
}
const server = net.createServer(socket => {
repl.start({
input: socket,
output: socket,
// tslint:disable-next-line:ban-types
eval(cmd: string, context: any, unusedFilename: string, callback: Function): any {
try {
return callback(null, evalFunction(cmd));
} catch (e) {
return callback(e);
}
},
}).on('exit', () => socket.end());
socket.on('error', () => socket.destroy());
});
const pathname = path.resolve(__dirname, '..', Config.replsocketprefix || 'logs/repl', filename);
server.listen(pathname, () => {
fs.chmodSync(pathname, Config.replsocketmode || 0o600);
Repl.socketPathnames.add(pathname);
});
server.once('error', (err: NodeJS.ErrnoException) => {
if (err.code === "EADDRINUSE") {
// tslint:disable-next-line:variable-name
fs.unlink(pathname, _err => {
if (_err && _err.code !== "ENOENT") {
crashlogger(_err, `REPL: ${filename}`);
}
server.close();
});
} else {
crashlogger(err, `REPL: ${filename}`);
server.close();
}
});
server.once('close', () => {
Repl.socketPathnames.delete(pathname);
Repl.start(filename, evalFunction);
});
}
};
export default Repl;