Skip to content

Instantly share code, notes, and snippets.

@devsnek
Last active May 18, 2020 18:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save devsnek/153357210b245a75b44168c7dec9f7b1 to your computer and use it in GitHub Desktop.
Save devsnek/153357210b245a75b44168c7dec9f7b1 to your computer and use it in GitHub Desktop.
'use strict';
const { createInterface, clearScreenDown } = require('readline');
const { EventEmitter } = require('events');
const { spawn } = require('child_process');
const WebSocket = require('ws');
const acorn = require('acorn-loose');
const { isIdentifier, strEscape } = require('./src/util');
const highlight = require('./src/highlight');
const PROMPT = '> ';
class Session extends EventEmitter {
constructor(url) {
super();
this.ws = new WebSocket(url);
this.ws.on('message', (d) => {
this.onMessage(d);
});
this.ws.on('open', () => {
this.emit('open');
});
this.messageCounter = 0;
this.messages = new Map();
}
static create(url) {
return new Promise((resolve) => {
const s = new Session(url);
s.once('open', () => resolve(s));
});
}
onMessage(d) {
const { id, method, params, result, error } = JSON.parse(d);
if (method) {
this.emit(method, params);
} else {
const { resolve, reject } = this.messages.get(id);
this.messages.delete(id);
if (error) {
const e = new Error(error.message);
e.code = error.code;
reject(e);
} else {
resolve(result);
}
}
}
post(method, params) {
return new Promise((resolve, reject) => {
const id = this.messageCounter;
this.messageCounter += 1;
const message = {
method,
params,
id,
};
this.messages.set(id, { resolve, reject });
this.ws.send(JSON.stringify(message));
});
}
}
const child = spawn(process.execPath, ['--inspect-publish-uid=http', './stub.js'], {
cwd: process.cwd(),
windowsHide: true,
});
child.stdout.on('data', (data) => {
process.stdout.write(data);
});
child.stderr.on('data', (data) => {
const s = data.toString();
if (s.startsWith('__DEBUGGER_URL__')) {
start(s.split(' ')[1]);
} else if (s !== 'Debugger attached.\n') {
process.stderr.write(data);
}
});
async function start(wsUrl) {
const session = await Session.create(wsUrl);
session.post('Runtime.enable');
const [{ context }] = await EventEmitter.once(session, 'Runtime.executionContextCreated');
const { result: remoteGlobal } = await session.post('Runtime.evaluate', {
expression: 'globalThis',
});
const getGlobalNames = () => Promise.all([
session.post('Runtime.globalLexicalScopeNames')
.then((r) => r.names),
session.post('Runtime.getProperties', {
objectId: remoteGlobal.objectId,
}).then((r) => r.result.map((p) => p.name)),
]).then((r) => r.flat());
const evaluate = (source, throwOnSideEffect) => session.post('Runtime.evaluate', {
expression: source,
throwOnSideEffect,
timeout: throwOnSideEffect ? 200 : undefined,
objectGroup: 'OBJECT_GROUP',
});
const callFunctionOn = (f, args) => session.post('Runtime.callFunctionOn', {
executionContextId: context.id,
functionDeclaration: f,
arguments: args,
objectGroup: 'OBJECT_GROUP',
});
const completeLine = async (line, cutLineStart) => {
if (line.length === 0) {
return getGlobalNames();
}
const statements = acorn.parse(line, { ecmaVersion: 2020 }).body;
const statement = statements[statements.length - 1];
if (statement.type !== 'ExpressionStatement') {
return undefined;
}
let { expression } = statement;
if (expression.operator === 'void') {
expression = expression.argument;
}
let keys;
let filter;
if (expression.type === 'Identifier') {
keys = await getGlobalNames();
filter = expression.name;
if (keys.includes(filter)) {
return undefined;
}
} else if (expression.type === 'MemberExpression') {
const expr = line.slice(expression.object.start, expression.object.end);
if (expression.computed && expression.property.type === 'Literal') {
filter = expression.property.raw;
} else if (expression.property.type === 'Identifier') {
if (expression.property.name === '✖') {
filter = undefined;
} else {
filter = expression.property.name;
if (expression.computed) {
keys = await getGlobalNames();
}
}
} else {
return undefined;
}
if (!keys) {
let evaluateResult = await evaluate(expr, true);
if (evaluateResult.exceptionDetails) {
return undefined;
}
// Convert inspection target to object.
if (evaluateResult.result.type !== 'object'
&& evaluateResult.result.type !== 'undefined'
&& evaluateResult.result.subtype !== 'null') {
evaluateResult = await evaluate(`Object(${expr})`, true);
if (evaluateResult.exceptionDetails) {
return undefined;
}
}
const own = [];
const inherited = [];
(await session.post('Runtime.getProperties', {
objectId: evaluateResult.result.objectId,
generatePreview: true,
}))
.result
.filter(({ symbol }) => !symbol)
.forEach(({ isOwn, name }) => {
if (isOwn) {
own.push(name);
} else {
inherited.push(name);
}
});
keys = [...own, ...inherited];
if (keys.length === 0) {
return undefined;
}
if (expression.computed) {
if (line.endsWith(']')) {
return undefined;
}
keys = keys.map((key) => {
let r;
if (`${+key}` === key) {
r = key;
} else {
r = strEscape(key);
}
if (cutLineStart) {
return `${r}]`;
}
return r;
});
} else {
keys = keys.filter(isIdentifier);
}
}
} else if ((expression.type === 'CallExpression' || expression.type === 'NewExpression')
&& !/\)(?:(?:\s+)?;)?(?:\s+)?$/.test(line)) {
const callee = line.slice(expression.callee.start, expression.callee.end);
const { result, exceptionDetails } = await evaluate(callee, true);
if (exceptionDetails) {
return undefined;
}
const { result: annotation } = await callFunctionOn(
`function complete(fn, expression, line) {
const { completeCall } = require('./src/annotations');
const a = completeCall(fn, expression, line);
return a;
}`,
[result, { value: expression }, { value: line }],
);
if (annotation.type === 'string') {
return [annotation.value];
}
return undefined;
}
if (keys) {
if (filter) {
keys = keys.filter((k) => k.startsWith(filter));
if (cutLineStart) {
keys = keys.map((k) => k.slice(filter.length));
}
}
return keys;
}
return undefined;
};
const getPreview = (line) => evaluate(line, true)
.then(({ result, exceptionDetails }) => {
if (exceptionDetails) {
throw new Error();
}
return callFunctionOn(
`function inspect(v) {
return util.inspect(v, {
colors: false,
breakLength: Infinity,
compact: true,
maxArrayLength: 10,
depth: 1,
});
}`,
[result],
);
})
.then(({ result }) => result.value)
.catch(() => undefined);
const rl = createInterface({
input: process.stdin,
output: process.stdout,
prompt: PROMPT,
completer(line, cb) {
completeLine(line, false)
.then((completions) => {
cb(null, [completions || [], line]);
})
.catch((e) => {
cb(e);
});
},
postprocessor(line) {
return highlight(line);
},
});
const ttyWrite = rl._ttyWrite.bind(rl);
rl._ttyWrite = (d, key) => {
ttyWrite(d, key);
if (rl.line !== '') {
process.stdout.cursorTo(PROMPT.length + rl.line.length);
clearScreenDown(process.stdout);
process.stdout.cursorTo(PROMPT.length + rl.cursor);
const inspectedLine = rl.line;
Promise.all([
completeLine(inspectedLine, true),
getPreview(inspectedLine),
])
.then(([completion, preview]) => {
if (rl.line !== inspectedLine) {
return;
}
if (completion && completion.length > 0) {
process.stdout.cursorTo(PROMPT.length + rl.line.length);
process.stdout.write(`\u001b[90m${completion[0]}\u001b[39m`);
}
if (preview) {
process.stdout.write(`\n\u001b[90m${preview.slice(0, process.stdout.columns - 1)}\u001b[39m`);
process.stdout.moveCursor(0, -1);
}
if (completion || preview) {
process.stdout.cursorTo(PROMPT.length + rl.cursor);
}
});
}
};
rl.prompt();
for await (const line of rl) {
rl.pause();
clearScreenDown(process.stdout);
const { result, exceptionDetails } = await evaluate(line);
const uncaught = !!exceptionDetails;
const { result: inspected } = await callFunctionOn(
`function inspect(v) {
return util.inspect(v, {
colors: true,
showProxy: true,
});
}`,
[result],
);
process.stdout.write(`${uncaught ? 'Uncaught: ' : ''}${inspected.value}\n`);
await session.post('Runtime.releaseObjectGroup', {
objectGroup: 'OBJECT_GROUP',
});
rl.resume();
rl.prompt();
}
}
'use strict';
const Module = require('module');
const path = require('path');
const inspector = require('inspector');
const util = require('util');
inspector.open(true);
process.stderr.write(`__DEBUGGER_URL__ ${inspector.url()}`);
if (process.platform !== 'win32') {
util.inspect.styles.number = 'blue';
util.inspect.styles.bigint = 'blue';
}
setTimeout(() => {}, 2147483647);
Module.builtinModules
.filter((x) => !/^_|\//.test(x))
.forEach((name) => {
if (name === 'domain' || name === 'repl' || name === 'sys') {
return;
}
global[name] = require(name);
});
try {
// Hack for require.resolve("./relative") to work properly.
module.filename = path.resolve('repl');
} catch (e) {
// path.resolve('repl') fails when the current working directory has been
// deleted. Fall back to the directory name of the (absolute) executable
// path. It's not really correct but what are the alternatives?
const dirname = path.dirname(process.execPath);
module.filename = path.resolve(dirname, 'repl');
}
// Hack for repl require to work properly with node_modules folders
module.paths = Module._nodeModulePaths(module.filename);
const parentModule = module;
{
const module = new Module('<repl>');
module.paths = Module._resolveLookupPaths('<repl>', parentModule, true) || [];
module._compile('module.exports = require;', '<repl>');
Object.defineProperty(global, 'module', {
configurable: true,
writable: true,
value: module,
});
Object.defineProperty(global, 'require', {
configurable: true,
writable: true,
value: module.exports,
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment