Last active
May 18, 2020 18:24
-
-
Save devsnek/153357210b245a75b44168c7dec9f7b1 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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