UNPKG

@kurtharriger/nel

Version:

Node.js Evaluation Loop (NEL): npm package to implement a Node.js REPL session

1,041 lines (909 loc) 57.1 kB
'use strict'; var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); /* * BSD 3-Clause License * * Copyright (c) 2015, Nicolas Riesco and others as credited in the AUTHORS file * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. Neither the name of the copyright holder nor the names of its contributors * may be used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * */ /** @module nel * * @description Module `nel` provides a Javascript REPL session. A Javascript * session can be used to run Javascript code within `Node.js`, pass the result * to a callback function and even capture its `stdout` and `stderr` streams. * */ var _events = require('events'); var _events2 = _interopRequireDefault(_events); var _socket = require('socket.io-client'); var _socket2 = _interopRequireDefault(_socket); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } module.exports = { Session: Session }; var console = require("console"); var fs = require("fs"); var path = require("path"); var spawn = require("child_process").spawn; var doc = require("./mdn.js"); // Documentation for Javascript builtins // Setup logging helpers var log; var dontLog = function dontLog() {}; var doLog = function doLog() { process.stderr.write("NEL: "); console.error.apply(this, arguments); }; if (process.env.DEBUG) { global.DEBUG = true; try { doLog = require("debug")("NEL:"); } catch (err) {} } log = global.DEBUG ? doLog : dontLog; // File paths var paths = { node: process.argv[0], thisFile: fs.realpathSync(module.filename) }; paths.thisFolder = path.dirname(paths.thisFile); paths.client = paths.thisFile; paths.server = path.join(paths.thisFolder, "nel_server.js"); /** * Javascript session configuration. * * @typedef Config * * @property {string} [cwd] Session current working directory * @property {module:nel~Transpiler} * [transpile] Function that transpiles the request code * into Javascript that can be run by the * Node.js session. * * @see {@link module:nel~Session} */ /** * Function that transpiles the request code into Javascript that can be run by * the Node.js session. * * @typedef Transpiler * * @type {function} * @param {string} code Request code * @returns {string} Transpiled code * * @see {@link module:nel~Config} */ var ProcessServer = function () { function ProcessServer(command, args, config) { var _this = this; _classCallCheck(this, ProcessServer); this.emitter = new _events2.default(); this.server = spawn(command, args, config); this.server.on("message", function (data) { return _this.emitter.emit('message', data); }); } _createClass(ProcessServer, [{ key: 'onMessage', value: function onMessage(callback) { this.emitter.on("message", callback); } }, { key: 'send', value: function send(msg) { this.server.send(msg); } }, { key: 'kill', value: function kill() { var signal = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "SIGTERM"; var killCB = arguments[1]; this.server.removeAllListeners(); this.server.kill(signal); this.server.on("exit", function (code, signal) { if (killCB) { killCB(code, signal); } }.bind(this)); } }]); return ProcessServer; }(); var SocketServer = function () { function SocketServer(server) { var _this2 = this; _classCallCheck(this, SocketServer); this.emitter = new _events2.default(); log('connecting to ' + server); this.socket = _socket2.default.connect(server, { reconnect: true }); this.socket.on('connect_error', function (error) { log('failed to connect to server: ', error); }); this.socket.on('connect', function () { log('connected'); }); this.socket.on('disconnect', function () { log('disconnected'); }); this.socket.on('message', function (msg) { return _this2.emitter.emit('message', msg); }); } _createClass(SocketServer, [{ key: 'onMessage', value: function onMessage(callback) { this.emitter.on("message", callback); } }, { key: 'send', value: function send(msg) { this.socket.send(msg); } }, { key: 'kill', value: function kill() { var signal = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "SIGTERM"; var killCB = arguments[1]; this.socket.close(); if (killCB) { killCB(); } } }]); return SocketServer; }(); /** * @class * @classdesc Implements a Node.js session * @param {module:nel~Config} [nelConfig] Session configuration. */ function Session(nelConfig) { nelConfig = nelConfig || {}; /** * Function that transpiles the request code into Javascript that can be run * by the Node.js session (null/undefined if no transpilation is needed). * @member {?module:nel~Transpiler} */ this.transpile = nelConfig.transpile; /** * Queue of tasks to be run * @member {module:nel~Task[]} * @private */ this._tasks = []; /** * Task currently being run (null if the last running task has finished) * @member {module:nel~Task} * @private */ this._currentTask = null; /** * Table of execution contexts * (execution contexts are created to allow running multiple execution * requests asynchronously) * @member {Object.<number, module:nel~Task>} * @private */ this._contextTable = {}; /** * Last execution context id (0 if none have been created) * @member {number} * @private */ this._lastContextId = 0; /** * Last run task (null if none have been run) * @member {module:nel~Task} * @private */ this._lastTask = null; /** * Session configuration * @member {module:nel~Config} * @private */ this._config = { cwd: nelConfig.cwd, stdio: global.DEBUG ? [process.stdin, process.stdout, process.stderr, "ipc"] : ["ignore", "ignore", "ignore", "ipc"] }; /** * Server that runs the code requests for this session * @member {module:child_process~ChildProcess} * @private */ if (nelConfig.server) { this._server = new SocketServer(nelConfig.server); } else { this._server = new ProcessServer(Session._command, Session._args, this._config); } /** * True after calling {@link module:nel~Session.kill}, otherwise false * @member {Boolean} * @private */ this._killed = false; this._server.onMessage(Session.prototype._onMessage.bind(this)); } /** * Path to node executable * @member {String} * @private */ Session._command = paths.node; /** * Arguments passed onto the node executable * @member {String[]} * @private */ Session._args = [paths.server]; /** * Combination of a piece of code to be run within a session and all the * associated callbacks. * @see {@link module:nel~Session#_run} * * @typedef Task * * @property {string} action Type of task: * "run" to evaluate a piece of * code and return the result; * "getAllPropertyNames" to * evaluate a piece of code and * return all the property names * of the result; * "inspect" to inspect an object * and return information such as * the list of constructors, * string representation, * length... * @property {string} code Code to evaluate * @property {module:nel~OnSuccessCB} [onSuccess] Called if no errors occurred * @property {module:nel~OnErrorCB} [onError] Called if an error occurred * @property {module:nel~BeforeRunCB} [beforeRun] Called before running the code * @property {module:nel~AfterRunCB} [afterRun] Called after running the code * @property {module:nel~OnStdioCB} [onStdout] Called if process.stdout data * @property {module:nel~OnStdioCB} [onStderr] Called if process.stderr data * * @private */ /** * Callback invoked with the data written on `process.stdout` or * `process.stderr` after a request to the server. * @see {@link module:nel~Task} * * @callback OnStdioCB * @param {string} data */ /** * Callback invoked before running a task * @see {@link module:nel~Task} * * @callback BeforeRunCB */ /** * Callback invoked after running a task (regardless of success or failure) * @see {@link module:nel~Task} * * @callback AfterRunCB */ /** * Callback invoked with the error obtained while running a task * @see {@link module:nel~Task} * * @callback OnErrorCB * @param {module:nel~ErrorResult} error */ /** * Callback invoked with the result of a task * @see {@link module:nel~Task} * * @typedef OnSuccessCB { * module:nel~OnExecutionSuccessCB | * module:nel~OnCompletionSuccessCB | * module:nel~OnInspectionSuccessCB | * module:nel~OnNameListSuccessCB * } */ /** * Callback run with the result of an execution request * @see {@link module:nel~Session#execute} * * @callback OnExecutionSuccessCB * @param {module:nel~ExecutionMessage} result MIME representations */ /** * Callback run with the result of an completion request * @see {@link module:nel~Session#complete} * * @callback OnCompletionSuccessCB * @param {module:nel~CompletionMessage} result Completion request results */ /** * Callback run with the result of an inspection request * @see {@link module:nel~Session#inspect} * * @callback OnInspectionSuccessCB * @param {module:nel~InspectionMessage} result Inspection request result */ /** * Callback run with the list of all the property names * * @callback OnNameListSuccessCB * @param {module:nel~NameListMessage} result List of all the property names * * @private */ /** * Callback run after the session server has been killed * @see {@link module:nel~Session#kill} * * @callback KillCB * @param {Number} [code] Exit code from session server if exited normally * @param {String} [signal] Signal passed to kill the session server */ /** * Callback run after the session server has been restarted * @see {@link module:nel~Session#restart} * * @callback RestartCB * @param {Number} [code] Exit code from old session if exited normally * @param {String} [signal] Signal passed to kill the old session */ /** * Message received from the session server * * @typedef Message { * module:nel~LogMessage | * module:nel~StdoutMessage | * module:nel~StderrMessage | * module:nel~ErrorMessage | * module:nel~SuccessMessage * } */ /** * Log message received from the session server * * @typedef LogMessage * * @property {string} log Message for logging purposes * * @private */ /** * Stdout message received from the session server * * @typedef StdoutMessage * * @property {number} id Execution context id * @property {string} stdout Data written on the session stdout * * @private */ /** * Stderr message received from the session server * * @typedef StderrMessage * * @property {number} id Execution context id * @property {string} stderr Data written on the session stderr * * @private */ /** * Error thrown when running a task within a session * @see {@link module:nel~Session#execute}, {@link module:nel~Session#complete}, * and {@link module:nel~Session#inspect} * * @typedef ErrorMessage * * @property {number} [id] Execution context id * (deleted before passing the message * onto the API user) * @property {boolean} [end] Flag to terminate the execution context * @property error * @property {String} error.ename Error name * @property {String} error.evalue Error value * @property {String[]} error.traceback Error traceback */ /** * Request result * @see {@link module:nel~Session#execute}, {@link module:nel~Session#complete}, * and {@link module:nel~Session#inspect} * * @typedef SuccessMessage { * module:nel~ExecutionMessage | * module:nel~CompletionMessage | * module:nel~InspectionMessage | * module:nel~NameListMessage * } */ /** * MIME representations of the result of an execution request * @see {@link module:nel~Session#execute} * * @typedef ExecutionMessage * * @property {number} [id] Execution context id * (deleted before the message reaches the API user) * @property {boolean} [end] Flag to terminate the execution context * @property mime * @property {string} [mime."text/plain"] Result in plain text * @property {string} [mime."text/html"] Result in HTML format * @property {string} [mime."image/svg+xml"] Result in SVG format * @property {string} [mime."image/png"] Result as PNG in a base64 string * @property {string} [mime."image/jpeg"] Result as JPEG in a base64 string */ /** * Results of a completion request * @see {@link module:nel~Session#complete} * * @typedef CompletionMessage * * @property {number} [id] Execution context id * (deleted before passing the * message onto the API user) * @property completion * @property {String[]} completion.list Array of completion matches * @property {String} completion.code Javascript code to be completed * @property {Integer} completion.cursorPos Cursor position within * `completion.code` * @property {String} completion.matchedText Text within `completion.code` * that has been matched * @property {Integer} completion.cursorStart Position of the start of * `completion.matchedText` within * `completion.code` * @property {Integer} completion.cursorEnd Position of the end of * `completion.matchedText` within * `completion.code` */ /** * Results of an inspection request * @see {@link module:nel~Session#inspect} * * @typedef InspectionMessage * * @property {number} [id] Execution context id * (deleted before passing the * message onto the API user) * @property inspection * @property {String} inspection.code Javascript code to be inspected * @property {Integer} inspection.cursorPos Cursor position within * `inspection.code`. * @property {String} inspection.matchedText Text within `inspection.code` * that has been matched as an * expression. * @property {String} inspection.string String representation * @property {String} inspection.type Javascript type * @property {String[]} [inspection.constructorList] * List of constructors (not * defined for `null` or * `undefined`). * @property {Integer} [inspection.length] Length property (if present) * * @property [doc] Defined only for calls to {@link * module:nel~inspect} that succeed * to find documentation for a * Javascript expression * @property {String} doc.description Description * @property {String} [doc.usage] Usage * @property {String} doc.url Link to the documentation source */ /** * Results of an "getAllPropertyNames" action * @see {@link module:nel~Task} * * @typedef NameListMessage * * @property {number} [id] Execution context id * (deleted before the message reaches the API user) * @property {String[]} names List of all property names * * @private */ /** * Callback to handle messages from the session server * * @param {module:nel~Message} message * @private */ Session.prototype._onMessage = function (message) { log("SESSION: MESSAGE:", message); var contextId = message.id; delete message.id; var endMessage = message.end; delete message.end; // Handle message.log if (message.log) { log(message.log); return; } // Get execution context // (if context is missing, default to using the last context) var task = this._contextTable[contextId]; if (!task) { log("SESSION: MESSAGE: Missing context, using last context, id =", contextId); task = this._lastTask; if (!task) { log("SESSION: MESSAGE: DROPPED: There is no last context"); return; } } // Handle message.stdout if (message.stdout) { if (task.onStdout) { task.onStdout(message.stdout); } else { log("SESSION: MESSAGE: Missing stderr callback"); } return; } // Handle message.stderr if (message.stderr) { if (task.onStderr) { task.onStderr(message.stderr); } else { log("SESSION: MESSAGE: Missing stderr callback"); } return; } // Handle error and success messages if (message.hasOwnProperty("error")) { if (task.onError) { task.onError(message); } else { log("SESSION: MESSAGE: Missing onError callback"); } } else { if (task.onSuccess) { task.onSuccess(message); } else { log("SESSION: MESSAGE: Missing onSuccess callback"); } } // Handle message.end if (endMessage) { if (task) { log("SESSION: MESSAGE: END: id =", contextId); delete this._contextTable[contextId]; if (task.afterRun) { task.afterRun(); } } else { log("SESSION: MESSAGE: END: DROPPED: id =", contextId); } } // If the task for this message is the last running task, // proceed to run the next task on the queue. if (task && task === this._currentTask) { this._currentTask = null; if (this._tasks.length > 0) { this._runNow(this._tasks.shift()); } } }; /** * Run a task * * @param {module:nel~Task} task * @private */ Session.prototype._run = function (task) { if (this._killed) { return; } log("SESSION: TASK:", task); if (this._currentTask === null) { this._runNow(task); } else { this._runLater(task); } }; /** * Run a task now * * @param {module:nel~Task} task * @private */ Session.prototype._runNow = function (task) { this._currentTask = task; this._lastContextId++; this._lastTask = this._currentTask; this._contextTable[this._lastContextId] = this._lastTask; if (this._lastTask.beforeRun) { this._lastTask.beforeRun(); } if (this.transpile && this._lastTask.action === "run") { try { // Adapted from https://github.com/n-riesco/nel/issues/1 by kebot var transpiledCode = this.transpile(this._lastTask.code); log("transpile: \n", transpiledCode, "\n"); this._lastTask.code = transpiledCode; } catch (error) { this._onMessage({ error: { ename: error && error.name ? error.name : typeof error === 'undefined' ? 'undefined' : _typeof(error), evalue: error && error.message ? error.message : util.inspect(error), traceback: error && error.stack ? error.stack.split("\n") : "" } }); return; } } this._server.send([this._lastTask.action, this._lastTask.code, this._lastContextId]); }; /** * Run a task later * * @param {module:nel~Task} task * @private */ Session.prototype._runLater = function (task) { this._tasks.push(task); }; /** * Make an execution request * * @param {String} code Code to execute in session * @param [callbacks] * @param {OnExecutionSuccessCB} [callbacks.onSuccess] * @param {OnErrorCB} [callbacks.onError] * @param {BeforeRunCB} [callbacks.beforeRun] * @param {AfterRunCB} [callbacks.afterRun] * @param {OnStdioCB} [callbacks.onStdout] * @param {OnStdioCB} [callbacks.onStderr] */ Session.prototype.execute = function (code, callbacks) { log("SESSION: EXECUTE:", code); var task = { action: "run", code: code }; if (callbacks) { if (callbacks.onSuccess) { task.onSuccess = callbacks.onSuccess; } if (callbacks.onError) { task.onError = callbacks.onError; } if (callbacks.beforeRun) { task.beforeRun = callbacks.beforeRun; } if (callbacks.afterRun) { task.afterRun = callbacks.afterRun; } if (callbacks.onStdout) { task.onStdout = callbacks.onStdout; } if (callbacks.onStderr) { task.onStderr = callbacks.onStderr; } } this._run(task); }; /** * Complete a Javascript expression * * @param {String} code Javascript code * @param {Number} cursorPos Cursor position in code * @param [callbacks] * @param {OnCompletionSuccessCB} [callbacks.onSuccess] * @param {OnErrorCB} [callbacks.onError] * @param {BeforeRunCB} [callbacks.beforeRun] * @param {AfterRunCB} [callbacks.afterRun] * @param {OnStdioCB} [callbacks.onStdout] * @param {OnStdioCB} [callbacks.onStderr] */ Session.prototype.complete = function (code, cursorPos, callbacks) { var matchList = []; var matchedText; var cursorStart; var cursorEnd; var expression = parseExpression(code, cursorPos); log("SESSION: COMPLETE: expression", expression); if (expression === null) { if (callbacks) { if (callbacks.beforeRun) { callbacks.beforeRun(); } if (callbacks.onSuccess) { callbacks.onSuccess({ completion: { list: matchList, code: code, cursorPos: cursorPos, matchedText: "", cursorStart: cursorPos, cursorEnd: cursorPos } }); } if (callbacks.afterRun) { callbacks.afterRun(); } } return; } var task = { action: "getAllPropertyNames", code: expression.scope === "" ? "global" : expression.scope }; if (callbacks) { if (callbacks.onError) { task.onError = callbacks.onError; } if (callbacks.beforeRun) { task.beforeRun = callbacks.beforeRun; } if (callbacks.afterRun) { task.afterRun = callbacks.afterRun; } if (callbacks.onStdout) { task.onStdout = callbacks.onStdout; } if (callbacks.onStderr) { task.onStderr = callbacks.onStderr; } } task.onSuccess = function (result) { // append list of all property names matchList = matchList.concat(result.names); // append list of reserved words if (expression.scope === "") { matchList = matchList.concat(javascriptKeywords); } // filter matches if (expression.selector) { matchList = matchList.filter(function (e) { return e.lastIndexOf(expression.selector, 0) === 0; }); } // append expression.rightOp to each match var left = expression.scope + expression.leftOp; var right = expression.rightOp; if (left || right) { matchList = matchList.map(function (e) { return left + e + right; }); } // find range of text that should be replaced if (matchList.length > 0) { var shortestMatch = matchList.reduce(function (p, c) { return p.length <= c.length ? p : c; }); cursorStart = code.indexOf(expression.matchedText); cursorEnd = cursorStart; var cl = code.length; var ml = shortestMatch.length; for (var i = 0; i < ml && cursorEnd < cl; i++, cursorEnd++) { if (shortestMatch.charAt(i) !== code.charAt(cursorEnd)) { break; } } } else { cursorStart = cursorPos; cursorEnd = cursorPos; } // return completion results to the callback matchedText = expression.matchedText; if (callbacks && callbacks.onSuccess) { callbacks.onSuccess({ completion: { list: matchList, code: code, cursorPos: cursorPos, matchedText: matchedText, cursorStart: cursorStart, cursorEnd: cursorEnd } }); } }; this._run(task); }; /** * Inspect a Javascript expression * * @param {String} code Javascript code * @param {Number} cursorPos Cursor position in code * @param [callbacks] * @param {OnInspectionSuccessCB} [callbacks.onSuccess] * @param {OnErrorCB} [callbacks.onError] * @param {BeforeRunCB} [callbacks.beforeRun] * @param {AfterRunCB} [callbacks.afterRun] * @param {OnStdioCB} [callbacks.onStdout] * @param {OnStdioCB} [callbacks.onStderr] */ Session.prototype.inspect = function (code, cursorPos, callbacks) { var expression = parseExpression(code, cursorPos); log("SESSION: INSPECT: expression:", expression); if (expression === null) { if (callbacks) { if (callbacks.beforeRun) { callbacks.beforeRun(); } if (callbacks.onSuccess) { callbacks.onSuccess({ inspection: { code: code, cursorPos: cursorPos, matchedText: "", string: "", type: "" } }); } if (callbacks.afterRun) { callbacks.afterRun(); } } return; } var inspectionResult; var task = { action: "inspect", code: expression.matchedText }; if (callbacks) { if (callbacks.onError) { task.onError = callbacks.onError; } if (callbacks.beforeRun) { task.beforeRun = callbacks.beforeRun; } if (callbacks.onStdout) { task.onStdout = callbacks.onStdout; } if (callbacks.onStderr) { task.onStderr = callbacks.onStderr; } } task.onSuccess = function (result) { inspectionResult = result; inspectionResult.inspection.code = code; inspectionResult.inspection.cursorPos = cursorPos; inspectionResult.inspection.matchedText = expression.matchedText; getDocumentationAndInvokeCallbacks.call(this); }.bind(this); this._run(task); return; function getDocumentationAndInvokeCallbacks() { var doc; // Find documentation associated with the matched text if (!expression.scope) { doc = getDocumentation(expression.matchedText); if (doc) { inspectionResult.doc = doc; } if (callbacks) { if (callbacks.onSuccess) { callbacks.onSuccess(inspectionResult); }