diff --git a/doc/api/experimental_repl.md b/doc/api/experimental_repl.md new file mode 100644 index 00000000000000..2625e2374576e9 --- /dev/null +++ b/doc/api/experimental_repl.md @@ -0,0 +1,70 @@ +# Experimental REPL + + + +> Stability: 1 - Early Development + + + +Before diving into the details of the experimental REPL, it's essential to +understand its current development stage. Being an experimental feature, its +stability cannot be assured. It's accessible solely through the command-line +argument `--experimental-repl`. + +## Overview + +The experimental REPL seeks to enhance the existing REPL experience by +introducing innovative features such as real-time Syntax Highlighting, with +plans for annotations in future iterations. It's implemented asynchronously, +utilizing an ECMAScript class for its functionality. + +## Key Features + +1. **Syntax Highlighting**: Real-time syntax highlighting for improved +readability and a more enriched coding environment. + +2. **Annotations** (Upcoming): Annotations will allow users to preview the +signature of a function. + +## Usage + +To interact with the experimental REPL, initialize it using the command-line +argument `--experimental-repl`. However, exercise caution as this feature is +still experimental and might demonstrate unexpected behavior. + +## Runtime + +Upon launching the REPL, expect the prompt to follow this format: + +```javascript +In [1]: 1+1 +Out[1]: 2 +``` + +Retrieve the output of a specific line using the command `_` followed by the +line number: + +```javascript +In [1]: Math.max(10, 100) +Out[1]: 100 + +In [2]: _1 +Out[2]: 100 +``` + +For accessing the output of the last three lines, use `_`, `__`, and `___` +respectively. + +### Known Limitations + +Since this REPL is experimental, several limitations are known: + +1. Autocompletion doesn't apply to object properties. +2. Tab completion doesn't list all properties when TAB is double-pressed +(with no line). +3. Annotations are under development. +4. Cursor location isn't synchronized after multiple-line text. + +If you'd like to propose a solution, feel free to open a +[pull request](https://github.com/nodejs/node/pulls). If you've identified an +issue not listed above, open an [issue](https://github.com/nodejs/node/issues). diff --git a/doc/api/repl.md b/doc/api/repl.md index 16378cca8abfc2..43045ab53ef416 100644 --- a/doc/api/repl.md +++ b/doc/api/repl.md @@ -1,10 +1,13 @@ # REPL +> This file documents the **stable REPL**. There is a seperate document for +> the [experimental REPL](./experimental_repl.md) + > Stability: 2 - Stable - + The `node:repl` module provides a Read-Eval-Print-Loop (REPL) implementation that is available both as a standalone program or includible in other diff --git a/doc/node.1 b/doc/node.1 index d055485bf0a0d2..0fd8b05f7ddd5c 100644 --- a/doc/node.1 +++ b/doc/node.1 @@ -177,6 +177,9 @@ Enable the experimental permission model. .It Fl -experimental-policy Use the specified file as a security policy. . +.It Fl -experimental-repl +Use the experimental REPL. +. .It Fl -experimental-shadow-realm Use this flag to enable ShadowRealm support. . diff --git a/lib/internal/main/repl.js b/lib/internal/main/repl.js index f7aa3a3e2602fa..959e1c0030b5da 100644 --- a/lib/internal/main/repl.js +++ b/lib/internal/main/repl.js @@ -36,9 +36,6 @@ if (process.env.NODE_REPL_EXTERNAL_MODULE) { } require('internal/modules/run_main').runEntryPointWithESMLoader(() => { - console.log(`Welcome to Node.js ${process.version}.\n` + - 'Type ".help" for more information.'); - const cliRepl = require('internal/repl'); cliRepl.createInternalRepl(process.env, (err, repl) => { if (err) { diff --git a/lib/internal/repl.js b/lib/internal/repl.js index 2dc79b2784e189..75489637586e37 100644 --- a/lib/internal/repl.js +++ b/lib/internal/repl.js @@ -6,13 +6,22 @@ const { NumberParseInt, } = primordials; -const REPL = require('repl'); -const { kStandaloneREPL } = require('internal/repl/utils'); +const StableREPL = require('internal/repl/stable/index'); +const ExperimentalREPL = require('internal/repl/experimental/index'); +const { kStandaloneREPL } = require('internal/repl/stable/utils'); +const { + getOptionValue, +} = require('internal/options'); + +const useExperimentalRepl = getOptionValue('--experimental-repl') || false; + +module.exports = { __proto__: useExperimentalRepl ? ExperimentalREPL : StableREPL }; +module.exports.createInternalRepl = useExperimentalRepl ? createExperimentalRepl : createStableRepl; -module.exports = { __proto__: REPL }; -module.exports.createInternalRepl = createRepl; +function createStableRepl(env, opts, cb) { + process.stdout.write(`Welcome to Node.js ${process.version}.\n` + + 'Type ".help" for more information.\n'); -function createRepl(env, opts, cb) { if (typeof opts === 'function') { cb = opts; opts = null; @@ -31,13 +40,13 @@ function createRepl(env, opts, cb) { if (env.NODE_REPL_MODE) { opts.replMode = { - 'strict': REPL.REPL_MODE_STRICT, - 'sloppy': REPL.REPL_MODE_SLOPPY, + 'strict': StableREPL.REPL_MODE_STRICT, + 'sloppy': StableREPL.REPL_MODE_SLOPPY, }[env.NODE_REPL_MODE.toLowerCase().trim()]; } if (opts.replMode === undefined) { - opts.replMode = REPL.REPL_MODE_SLOPPY; + opts.replMode = StableREPL.REPL_MODE_SLOPPY; } const historySize = Number(env.NODE_REPL_HISTORY_SIZE); @@ -47,7 +56,12 @@ function createRepl(env, opts, cb) { opts.historySize = 1000; } - const repl = REPL.start(opts); + const repl = StableREPL.start(opts); const term = 'terminal' in opts ? opts.terminal : process.stdout.isTTY; repl.setupHistory(term ? env.NODE_REPL_HISTORY : '', cb); } + +async function createExperimentalRepl() { + const repl = new ExperimentalREPL(); + await repl.start(); +} diff --git a/lib/internal/repl/experimental/deps/emphasize.js b/lib/internal/repl/experimental/deps/emphasize.js new file mode 100644 index 00000000000000..8c32fdf2f246a8 --- /dev/null +++ b/lib/internal/repl/experimental/deps/emphasize.js @@ -0,0 +1,82 @@ +/* eslint-disable */ +/* +Below is a stripped version of Highlight.JS. +The original source code can be found at https://highlightjs.org/ +It is licensed under the BSD 3-Clause License by Ivan Sagalaev. + +The original source code has been modified to remove all languages except for JavaScript, +and condensed to fit within this file. + +Version: 10.4.1 (Not the latest version, intentionally) +File: core.js +License: https://github.com/highlightjs/highlight.js/blob/main/LICENSE +*/ +const hljs = (() => { + function deepFreeze(e){return e instanceof Map?e.clear=e.delete=e.set=function(){throw Error("map is read-only")}:e instanceof Set&&(e.add=e.clear=e.delete=function(){throw Error("set is read-only")}),Object.freeze(e),Object.getOwnPropertyNames(e).forEach(function(t){var n=e[t];"object"!=typeof n||Object.isFrozen(n)||deepFreeze(n)}),e}var deepFreezeEs6=deepFreeze,_default=deepFreeze;deepFreezeEs6.default=_default;class Response{constructor(e){void 0===e.data&&(e.data={}),this.data=e.data}ignoreMatch(){this.ignore=!0}}function escapeHTML(e){return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function inherit(e,...t){let n=Object.create(null);for(let r in e)n[r]=e[r];return t.forEach(function(e){for(let t in e)n[t]=e[t]}),n}function tag(e){return e.nodeName.toLowerCase()}function nodeStream(e){let t=[];return!function e(n,r){for(let a=n.firstChild;a;a=a.nextSibling)3===a.nodeType?r+=a.nodeValue.length:1!==a.nodeType||(t.push({event:"start",offset:r,node:a}),r=e(a,r),tag(a).match(/br|hr|img|input/)||t.push({event:"stop",offset:r,node:a}));return r}(e,0),t}function mergeStreams(e,t,n){let r=0,a="",i=[];function s(){return e.length&&t.length?e[0].offset!==t[0].offset?e[0].offset"}function o(e){a+=""}function u(e){("start"===e.event?l:o)(e.node)}for(;e.length||t.length;){let c=s();if(a+=escapeHTML(n.substring(r,c[0].offset)),r=c[0].offset,c===e){i.reverse().forEach(o);do u(c.splice(0,1)[0]),c=s();while(c===e&&c.length&&c[0].offset===r);i.reverse().forEach(l)}else"start"===c[0].event?i.push(c[0].node):i.pop(),u(c.splice(0,1)[0])}return a+escapeHTML(n.substr(r))}var utils=Object.freeze({__proto__:null,escapeHTML:escapeHTML,inherit:inherit,nodeStream:nodeStream,mergeStreams:mergeStreams});const SPAN_CLOSE="",emitsWrappingTags=e=>!!e.kind;class HTMLRenderer{constructor(e,t){this.buffer="",this.classPrefix=t.classPrefix,e.walk(this)}addText(e){this.buffer+=escapeHTML(e)}openNode(e){if(!emitsWrappingTags(e))return;let t=e.kind;e.sublanguage||(t=`${this.classPrefix}${t}`),this.span(t)}closeNode(e){emitsWrappingTags(e)&&(this.buffer+="")}value(){return this.buffer}span(e){this.buffer+=``}}class TokenTree{constructor(){this.rootNode={children:[]},this.stack=[this.rootNode]}get top(){return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){this.top.children.push(e)}openNode(e){let t={kind:e,children:[]};this.add(t),this.stack.push(t)}closeNode(){if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)}walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,t){return"string"==typeof t?e.addText(t):t.children&&(e.openNode(t),t.children.forEach(t=>this._walk(e,t)),e.closeNode(t)),e}static _collapse(e){"string"!=typeof e&&e.children&&(e.children.every(e=>"string"==typeof e)?e.children=[e.children.join("")]:e.children.forEach(e=>{TokenTree._collapse(e)}))}}class TokenTreeEmitter extends TokenTree{constructor(e){super(),this.options=e}addKeyword(e,t){""!==e&&(this.openNode(t),this.addText(e),this.closeNode())}addText(e){""!==e&&this.add(e)}addSublanguage(e,t){let n=e.root;n.kind=t,n.sublanguage=!0,this.add(n)}toHTML(){let e=new HTMLRenderer(this,this.options);return e.value()}finalize(){return!0}}function escape(e){return RegExp(e.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&"),"m")}function source(e){return e?"string"==typeof e?e:e.source:null}function concat(...e){let t=e.map(e=>source(e)).join("");return t}function countMatchGroups(e){return RegExp(e.toString()+"|").exec("").length-1}function startsWith(e,t){let n=e&&e.exec(t);return n&&0===n.index}function join(e,t="|"){let n=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./,r=0,a="";for(let i=0;i0&&(a+=t),a+="(";l.length>0;){let o=n.exec(l);if(null==o){a+=l;break}a+=l.substring(0,o.index),l=l.substring(o.index+o[0].length),"\\"===o[0][0]&&o[1]?a+="\\"+String(Number(o[1])+s):(a+=o[0],"("===o[0]&&r++)}a+=")"}return a}const IDENT_RE="[a-zA-Z]\\w*",UNDERSCORE_IDENT_RE="[a-zA-Z_]\\w*",NUMBER_RE="\\b\\d+(\\.\\d+)?",C_NUMBER_RE="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",BINARY_NUMBER_RE="\\b(0b[01]+)",RE_STARTERS_RE="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",SHEBANG=(e={})=>{let t=/^#![ ]*\//;return e.binary&&(e.begin=concat(t,/.*\b/,e.binary,/\b.*/)),inherit({className:"meta",begin:t,end:/$/,relevance:0,"on:begin"(e,t){0!==e.index&&t.ignoreMatch()}},e)},BACKSLASH_ESCAPE={begin:"\\\\[\\s\\S]",relevance:0},APOS_STRING_MODE={className:"string",begin:"'",end:"'",illegal:"\\n",contains:[BACKSLASH_ESCAPE]},QUOTE_STRING_MODE={className:"string",begin:'"',end:'"',illegal:"\\n",contains:[BACKSLASH_ESCAPE]},PHRASAL_WORDS_MODE={begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},COMMENT=function(e,t,n={}){let r=inherit({className:"comment",begin:e,end:t,contains:[]},n);return r.contains.push(PHRASAL_WORDS_MODE),r.contains.push({className:"doctag",begin:"(?:TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):",relevance:0}),r},C_LINE_COMMENT_MODE=COMMENT("//","$"),C_BLOCK_COMMENT_MODE=COMMENT("/\\*","\\*/"),HASH_COMMENT_MODE=COMMENT("#","$"),NUMBER_MODE={className:"number",begin:NUMBER_RE,relevance:0},C_NUMBER_MODE={className:"number",begin:C_NUMBER_RE,relevance:0},BINARY_NUMBER_MODE={className:"number",begin:BINARY_NUMBER_RE,relevance:0},CSS_NUMBER_MODE={className:"number",begin:NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",relevance:0},REGEXP_MODE={begin:/(?=\/[^/\n]*\/)/,contains:[{className:"regexp",begin:/\//,end:/\/[gimuy]*/,illegal:/\n/,contains:[BACKSLASH_ESCAPE,{begin:/\[/,end:/\]/,relevance:0,contains:[BACKSLASH_ESCAPE]}]}]},TITLE_MODE={className:"title",begin:IDENT_RE,relevance:0},UNDERSCORE_TITLE_MODE={className:"title",begin:UNDERSCORE_IDENT_RE,relevance:0},METHOD_GUARD={begin:"\\.\\s*"+UNDERSCORE_IDENT_RE,relevance:0},END_SAME_AS_BEGIN=function(e){return Object.assign(e,{"on:begin"(e,t){t.data._beginMatch=e[1]},"on:end"(e,t){t.data._beginMatch!==e[1]&&t.ignoreMatch()}})};var MODES=Object.freeze({__proto__:null,IDENT_RE:IDENT_RE,UNDERSCORE_IDENT_RE:UNDERSCORE_IDENT_RE,NUMBER_RE:NUMBER_RE,C_NUMBER_RE:C_NUMBER_RE,BINARY_NUMBER_RE:BINARY_NUMBER_RE,RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",SHEBANG:SHEBANG,BACKSLASH_ESCAPE:BACKSLASH_ESCAPE,APOS_STRING_MODE:APOS_STRING_MODE,QUOTE_STRING_MODE:QUOTE_STRING_MODE,PHRASAL_WORDS_MODE:PHRASAL_WORDS_MODE,COMMENT:COMMENT,C_LINE_COMMENT_MODE:C_LINE_COMMENT_MODE,C_BLOCK_COMMENT_MODE:C_BLOCK_COMMENT_MODE,HASH_COMMENT_MODE:HASH_COMMENT_MODE,NUMBER_MODE:NUMBER_MODE,C_NUMBER_MODE:C_NUMBER_MODE,BINARY_NUMBER_MODE:BINARY_NUMBER_MODE,CSS_NUMBER_MODE:CSS_NUMBER_MODE,REGEXP_MODE:REGEXP_MODE,TITLE_MODE:TITLE_MODE,UNDERSCORE_TITLE_MODE:UNDERSCORE_TITLE_MODE,METHOD_GUARD:METHOD_GUARD,END_SAME_AS_BEGIN:END_SAME_AS_BEGIN});const COMMON_KEYWORDS=["of","and","for","in","not","or","if","then","parent","list","value"];function compileLanguage(e){function t(t,n){return RegExp(source(t),"m"+(e.case_insensitive?"i":"")+(n?"g":""))}class n{constructor(){this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0}addRule(e,t){t.position=this.position++,this.matchIndexes[this.matchAt]=t,this.regexes.push([t,e]),this.matchAt+=countMatchGroups(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null);let e=this.regexes.map(e=>e[1]);this.matcherRe=t(join(e),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex;let t=this.matcherRe.exec(e);if(!t)return null;let n=t.findIndex((e,t)=>t>0&&void 0!==e),r=this.matchIndexes[n];return t.splice(0,n),Object.assign(t,r)}}class r{constructor(){this.rules=[],this.multiRegexes=[],this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){if(this.multiRegexes[e])return this.multiRegexes[e];let t=new n;return this.rules.slice(e).forEach(([e,n])=>t.addRule(e,n)),t.compile(),this.multiRegexes[e]=t,t}resumingScanAtSamePosition(){return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,t){this.rules.push([e,t]),"begin"===t.type&&this.count++}exec(e){let t=this.getMatcher(this.regexIndex);t.lastIndex=this.lastIndex;let n=t.exec(e);if(this.resumingScanAtSamePosition()){if(n&&n.index===this.lastIndex);else{let r=this.getMatcher(0);r.lastIndex=this.lastIndex+1,n=r.exec(e)}}return n&&(this.regexIndex+=n.position+1,this.regexIndex===this.count&&this.considerAll()),n}}function a(e,t){let n=e.input[e.index-1];"."===n&&t.ignoreMatch()}if(e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.");return e.classNameAliases=inherit(e.classNameAliases||{}),function n(i,s){let l=i;if(i.compiled)return l;i.compiled=!0,i.__beforeBegin=null,i.keywords=i.keywords||i.beginKeywords;let o=null;if("object"==typeof i.keywords&&(o=i.keywords.$pattern,delete i.keywords.$pattern),i.keywords&&(i.keywords=compileKeywords(i.keywords,e.case_insensitive)),i.lexemes&&o)throw Error("ERR: Prefer `keywords.$pattern` to `mode.lexemes`, BOTH are not allowed. (see mode reference) ");return l.keywordPatternRe=t(i.lexemes||o||/\w+/,!0),s&&(i.beginKeywords&&(i.begin="\\b("+i.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)",i.__beforeBegin=a),i.begin||(i.begin=/\B|\b/),l.beginRe=t(i.begin),i.endSameAsBegin&&(i.end=i.begin),i.end||i.endsWithParent||(i.end=/\B|\b/),i.end&&(l.endRe=t(i.end)),l.terminator_end=source(i.end)||"",i.endsWithParent&&s.terminator_end&&(l.terminator_end+=(i.end?"|":"")+s.terminator_end)),i.illegal&&(l.illegalRe=t(i.illegal)),void 0===i.relevance&&(i.relevance=1),i.contains||(i.contains=[]),i.contains=[].concat(...i.contains.map(function(e){return expandOrCloneMode("self"===e?i:e)})),i.contains.forEach(function(e){n(e,l)}),i.starts&&n(i.starts,s),l.matcher=function e(t){let n=new r;return t.contains.forEach(e=>n.addRule(e.begin,{rule:e,type:"begin"})),t.terminator_end&&n.addRule(t.terminator_end,{type:"end"}),t.illegal&&n.addRule(t.illegal,{type:"illegal"}),n}(l),l}(e)}function dependencyOnParent(e){return!!e&&(e.endsWithParent||dependencyOnParent(e.starts))}function expandOrCloneMode(e){return(e.variants&&!e.cached_variants&&(e.cached_variants=e.variants.map(function(t){return inherit(e,{variants:null},t)})),e.cached_variants)?e.cached_variants:dependencyOnParent(e)?inherit(e,{starts:e.starts?inherit(e.starts):null}):Object.isFrozen(e)?inherit(e):e}function compileKeywords(e,t){let n={};return"string"==typeof e?r("keyword",e):Object.keys(e).forEach(function(t){r(t,e[t])}),n;function r(e,r){t&&(r=r.toLowerCase()),r.split(" ").forEach(function(t){let r=t.split("|");n[r[0]]=[e,scoreForKeyword(r[0],r[1])]})}}function scoreForKeyword(e,t){return t?Number(t):commonKeyword(e)?0:1}function commonKeyword(e){return COMMON_KEYWORDS.includes(e.toLowerCase())}var version="10.4.1";function hasValueOrEmptyAttribute(e){return Boolean(e||""===e)}function BuildVuePlugin(e){let t={props:["language","code","autodetect"],data:function(){return{detectedLanguage:"",unknownLanguage:!1}},computed:{className(){return this.unknownLanguage?"":"hljs "+this.detectedLanguage},highlighted(){if(!this.autoDetect&&!e.getLanguage(this.language))return console.warn(`The language "${this.language}" you specified could not be found.`),this.unknownLanguage=!0,escapeHTML(this.code);let t;return this.autoDetect?(t=e.highlightAuto(this.code),this.detectedLanguage=t.language):(t=e.highlight(this.language,this.code,this.ignoreIllegals),this.detectedLanguage=this.language),t.value},autoDetect(){return!this.language||hasValueOrEmptyAttribute(this.autodetect)},ignoreIllegals:()=>!0},render(e){return e("pre",{},[e("code",{class:this.className,domProps:{innerHTML:this.highlighted}})])}};return{Component:t,VuePlugin:{install(e){e.component("highlightjs",t)}}}}const escape$1=escapeHTML,inherit$1=inherit,{nodeStream:nodeStream$1,mergeStreams:mergeStreams$1}=utils,NO_MATCH=Symbol("nomatch"),HLJS=function(e){let t=[],n=Object.create(null),r=Object.create(null),a=[],i=!0,s=/(^(<[^>]+>|\t|)+|\n)/gm,l="Could not find the language '{}', did you forget to load/include a language module?",o={disableAutodetect:!0,name:"Plain text",contains:[]},u={noHighlightRe:/^(no-?highlight)$/i,languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:null,__emitter:TokenTreeEmitter};function c(e){return u.noHighlightRe.test(e)}function d(e,t,n,r){let a={code:t,language:e};N("before:highlight",a);let i=a.result?a.result:g(a.language,a.code,n,r);return i.code=a.code,N("after:highlight",i),i}function g(e,t,r,a){let s=t;function o(e,t){let n=M.case_insensitive?t[0].toLowerCase():t[0];return Object.prototype.hasOwnProperty.call(e.keywords,n)&&e.keywords[n]}function c(){null!=b.subLanguage?function e(){if(""===S)return;let t=null;if("string"==typeof b.subLanguage){if(!n[b.subLanguage]){$.addText(S);return}t=g(b.subLanguage,S,!0,O[b.subLanguage]),O[b.subLanguage]=t.top}else t=h(S,b.subLanguage.length?b.subLanguage:null);b.relevance>0&&(x+=t.relevance),$.addSublanguage(t.emitter,t.language)}():function e(){if(!b.keywords){$.addText(S);return}let t=0;b.keywordPatternRe.lastIndex=0;let n=b.keywordPatternRe.exec(S),r="";for(;n;){r+=S.substring(t,n.index);let a=o(b,n);if(a){let[i,s]=a;$.addText(r),r="",x+=s;let l=M.classNameAliases[i]||i;$.addKeyword(n[0],l)}else r+=n[0];t=b.keywordPatternRe.lastIndex,n=b.keywordPatternRe.exec(S)}r+=S.substr(t),$.addText(r)}(),S=""}function d(e){return e.className&&$.openNode(M.classNameAliases[e.className]||e.className),b=Object.create(e,{parent:{value:b}})}function f(e){return 0===b.matcher.regexIndex?(S+=e[0],1):(w=!0,0)}let E={};function p(t,n){let a=n&&n[0];if(S+=t,null==a)return c(),0;if("begin"===E.type&&"end"===n.type&&E.index===n.index&&""===a){if(S+=s.slice(n.index,n.index+1),!i){let l=Error("0 width match regex");throw l.languageName=e,l.badRule=E.rule,l}return 1}if(E=n,"begin"===n.type)return function e(t){let n=t[0],r=t.rule,a=new Response(r),i=[r.__beforeBegin,r["on:begin"]];for(let s of i)if(s&&(s(t,a),a.ignore))return f(n);return r&&r.endSameAsBegin&&(r.endRe=escape(n)),r.skip?S+=n:(r.excludeBegin&&(S+=n),c(),r.returnBegin||r.excludeBegin||(S=n)),d(r),r.returnBegin?0:n.length}(n);if("illegal"!==n.type||r){if("end"===n.type){let o=function e(t){let n=t[0],r=s.substr(t.index),a=function e(t,n,r){let a=startsWith(t.endRe,r);if(a){if(t["on:end"]){let i=new Response(t);t["on:end"](n,i),i.ignore&&(a=!1)}if(a){for(;t.endsParent&&t.parent;)t=t.parent;return t}}if(t.endsWithParent)return e(t.parent,n,r)}(b,t,r);if(!a)return NO_MATCH;let i=b;i.skip?S+=n:(i.returnEnd||i.excludeEnd||(S+=n),c(),i.excludeEnd&&(S=n));do b.className&&$.closeNode(),b.skip||b.subLanguage||(x+=b.relevance),b=b.parent;while(b!==a.parent);return a.starts&&(a.endSameAsBegin&&(a.starts.endRe=a.endRe),d(a.starts)),i.returnEnd?0:n.length}(n);if(o!==NO_MATCH)return o}}else{let u=Error('Illegal lexeme "'+a+'" for mode "'+(b.className||"")+'"');throw u.mode=b,u}if("illegal"===n.type&&""===a)return 1;if(v>1e5&&v>3*n.index){let g=Error("potential infinite loop, way more iterations than matches");throw g}return S+=a,a.length}let M=m(e);if(!M)throw console.error(l.replace("{}",e)),Error('Unknown language: "'+e+'"');let R=compileLanguage(M),N="",b=a||R,O={},$=new u.__emitter(u);!function e(){let t=[];for(let n=b;n!==M;n=n.parent)n.className&&t.unshift(n.className);t.forEach(e=>$.openNode(e))}();let S="",x=0,T=0,v=0,w=!1;try{for(b.matcher.considerAll();;){v++,w?w=!1:b.matcher.considerAll(),b.matcher.lastIndex=T;let A=b.matcher.exec(s);if(!A)break;let D=s.substring(T,A.index),y=p(D,A);T=A.index+y}return p(s.substr(T)),$.closeAllNodes(),$.finalize(),N=$.toHTML(),{relevance:x,value:N,language:e,illegal:!1,emitter:$,top:b}}catch(L){if(L.message&&L.message.includes("Illegal"))return{illegal:!0,illegalBy:{msg:L.message,context:s.slice(T-100,T+100),mode:L.mode},sofar:N,relevance:0,value:escape$1(s),emitter:$};if(i)return{illegal:!1,relevance:0,value:escape$1(s),emitter:$,language:e,top:b,errorRaised:L};throw L}}function h(e,t){t=t||u.languages||Object.keys(n);let r=function e(t){let n={relevance:0,emitter:new u.__emitter(u),value:escape$1(t),illegal:!1,top:o};return n.emitter.addText(t),n}(e),a=t.filter(m).filter(R).map(t=>g(t,e,!1));a.unshift(r);let i=a.sort((e,t)=>{if(e.relevance!==t.relevance)return t.relevance-e.relevance;if(e.language&&t.language){if(m(e.language).supersetOf===t.language)return 1;if(m(t.language).supersetOf===e.language)return -1}return 0}),[s,l]=i,c=s;return c.second_best=l,c}function f(e){return u.tabReplace||u.useBR?e.replace(s,e=>"\n"===e?u.useBR?"
":e:u.tabReplace?e.replace(/\t/g,u.tabReplace):e):e}function E(e){let t=null,n=function e(t){let n=t.className+" ";n+=t.parentNode?t.parentNode.className:"";let r=u.languageDetectRe.exec(n);if(r){let a=m(r[1]);return a||(console.warn(l.replace("{}",r[1])),console.warn("Falling back to no-highlight mode for this block.",t)),a?r[1]:"no-highlight"}return n.split(/\s+/).find(e=>c(e)||m(e))}(e);if(c(n))return;N("before:highlightBlock",{block:e,language:n}),u.useBR?(t=document.createElement("div")).innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n"):t=e;let a=t.textContent,i=n?d(n,a,!0):h(a),s=nodeStream$1(t);if(s.length){let o=document.createElement("div");o.innerHTML=i.value,i.value=mergeStreams$1(s,nodeStream$1(o),a)}i.value=f(i.value),N("after:highlightBlock",{block:e,result:i}),e.innerHTML=i.value,e.className=function e(t,n,a){let i=n?r[n]:a,s=[t.trim()];return t.match(/\bhljs\b/)||s.push("hljs"),t.includes(i)||s.push(i),s.join(" ").trim()}(e.className,n,i.language),e.result={language:i.language,re:i.relevance,relavance:i.relevance},i.second_best&&(e.second_best={language:i.second_best.language,re:i.second_best.relevance,relavance:i.second_best.relevance})}let p=()=>{if(p.called)return;p.called=!0;let e=document.querySelectorAll("pre code");t.forEach.call(e,E)};function m(e){return n[e=(e||"").toLowerCase()]||n[r[e]]}function M(e,{languageName:t}){"string"==typeof e&&(e=[e]),e.forEach(e=>{r[e]=t})}function R(e){let t=m(e);return t&&!t.disableAutodetect}function N(e,t){let n=e;a.forEach(function(e){e[n]&&e[n](t)})}for(let b in Object.assign(e,{highlight:d,highlightAuto:h,fixMarkup:function e(t){return console.warn("fixMarkup is deprecated and will be removed entirely in v11.0"),console.warn("Please see https://github.com/highlightjs/highlight.js/issues/2534"),f(t)},highlightBlock:E,configure:function e(t){t.useBR&&(console.warn("'useBR' option is deprecated and will be removed entirely in v11.0"),console.warn("Please see https://github.com/highlightjs/highlight.js/issues/2559")),u=inherit$1(u,t)},initHighlighting:p,initHighlightingOnLoad:function e(){window.addEventListener("DOMContentLoaded",p,!1)},registerLanguage:function t(r,a){let s=null;try{s=a(e)}catch(l){if(console.error("Language definition for '{}' could not be registered.".replace("{}",r)),i)console.error(l);else throw l;s=o}s.name||(s.name=r),n[r]=s,s.rawDefinition=a.bind(null,e),s.aliases&&M(s.aliases,{languageName:r})},listLanguages:function e(){return Object.keys(n)},getLanguage:m,registerAliases:M,requireLanguage:function e(t){console.warn("requireLanguage is deprecated and will be removed entirely in the future."),console.warn("Please see https://github.com/highlightjs/highlight.js/pull/2844");let n=m(t);if(n)return n;let r=Error("The '{}' language is required, but not loaded.".replace("{}",t));throw r},autoDetection:R,inherit:inherit$1,addPlugin:function e(t){a.push(t)},vuePlugin:BuildVuePlugin(e).VuePlugin}),e.debugMode=function(){i=!1},e.safeMode=function(){i=!0},e.versionString=version,MODES)"object"==typeof MODES[b]&&deepFreezeEs6(MODES[b]);return Object.assign(e,MODES),e};var highlight=HLJS({}); + + return highlight; +})(); + +/* +Below is a stripped version of Highlight.JS. +The original source code can be found at https://highlightjs.org/ +It is licensed under the BSD 3-Clause License by Ivan Sagalaev. + +The original source code has been modified to remove all languages except for JavaScript, +and condensed to fit within this file. + +Version: 10.4.1 (Not the latest version, intentionally) +File: languages/javascript.js +License: https://github.com/highlightjs/highlight.js/blob/main/LICENSE +*/ +((hljs) => { + const IDENT_RE="[A-Za-z$_][0-9A-Za-z$_]*",KEYWORDS=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],LITERALS=["true","false","null","undefined","NaN","Infinity"],TYPES=["Intl","DataView","Number","Math","Date","String","RegExp","Object","Function","Boolean","Error","Symbol","Set","Map","WeakSet","WeakMap","Proxy","Reflect","JSON","Promise","Float64Array","Int16Array","Int32Array","Int8Array","Uint16Array","Uint32Array","Float32Array","Array","Uint8Array","Uint8ClampedArray","ArrayBuffer"],ERROR_TYPES=["EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"],BUILT_IN_GLOBALS=["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],BUILT_IN_VARIABLES=["arguments","this","super","console","window","document","localStorage","module","global"],BUILT_INS=[].concat(BUILT_IN_GLOBALS,BUILT_IN_VARIABLES,TYPES,ERROR_TYPES);function source(e){return e?"string"==typeof e?e:e.source:null}function lookahead(e){return concat("(?=",e,")")}function concat(...e){let n=e.map(e=>source(e)).join("");return n}function javascript(e){let n=(e,{after:n})=>{let a="",end:""},t={begin:/<[A-Za-z0-9\\._:-]+/,end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag(e,a){let s=e[0].length+e.index,t=e.input[s];if("<"===t){a.ignoreMatch();return}">"!==t||n(e,{after:s})||a.ignoreMatch()}},i={$pattern:IDENT_RE,keyword:KEYWORDS.join(" "),literal:LITERALS.join(" "),built_in:BUILT_INS.join(" ")},r="[0-9](_?[0-9])*",c=`\\.(${r})`,o="0|[1-9](_?[0-9])*|0[0-7]*[89][0-9]*",l={className:"number",variants:[{begin:`(\\b(${o})((${c})|\\.)?|(${c}))[eE][+-]?(${r})\\b`},{begin:`\\b(${o})\\b((${c})\\b|\\.)?|(${c})\\b`},{begin:"\\b(0|[1-9](_?[0-9])*)n\\b"},{begin:"\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*n?\\b"},{begin:"\\b0[bB][0-1](_?[0-1])*n?\\b"},{begin:"\\b0[oO][0-7](_?[0-7])*n?\\b"},{begin:"\\b0[0-7]+n?\\b"},],relevance:0},b={className:"subst",begin:"\\$\\{",end:"\\}",keywords:i,contains:[]},E={begin:"html`",end:"",starts:{end:"`",returnEnd:!1,contains:[e.BACKSLASH_ESCAPE,b],subLanguage:"xml"}},g={begin:"css`",end:"",starts:{end:"`",returnEnd:!1,contains:[e.BACKSLASH_ESCAPE,b],subLanguage:"css"}},d={className:"string",begin:"`",end:"`",contains:[e.BACKSLASH_ESCAPE,b]},u=e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{className:"doctag",begin:"@[A-Za-z]+",contains:[{className:"type",begin:"\\{",end:"\\}",relevance:0},{className:"variable",begin:a+"(?=\\s*(-)|$)",endsParent:!0,relevance:0},{begin:/(?=[^\n])\s/,relevance:0}]}]}),$={className:"comment",variants:[u,e.C_BLOCK_COMMENT_MODE,e.C_LINE_COMMENT_MODE]},N=[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,E,g,d,l,e.REGEXP_MODE];b.contains=N.concat({begin:/\{/,end:/\}/,keywords:i,contains:["self"].concat(N)});let I=[].concat($,b.contains),T=I.concat([{begin:/\(/,end:/\)/,keywords:i,contains:["self"].concat(I)}]),R={className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:i,contains:T};return{name:"Javascript",aliases:["js","jsx","mjs","cjs"],keywords:i,exports:{PARAMS_CONTAINS:T},illegal:/#(?![$_A-z])/,contains:[e.SHEBANG({label:"shebang",binary:"node",relevance:5}),{label:"use_strict",className:"meta",relevance:10,begin:/^\s*['"]use (strict|asm)['"]/},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,E,g,d,$,l,{begin:concat(/[{,\n]\s*/,lookahead(concat(/(((\/\/.*$)|(\/\*(\*[^/]|[^*])*\*\/))\s*)*/,a+"\\s*:"))),relevance:0,contains:[{className:"attr",begin:a+lookahead("\\s*:"),relevance:0}]},{begin:"("+e.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",contains:[$,e.REGEXP_MODE,{className:"function",begin:"(\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)|"+e.UNDERSCORE_IDENT_RE+")\\s*=>",returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:e.UNDERSCORE_IDENT_RE,relevance:0},{className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:i,contains:T}]}]},{begin:/,/,relevance:0},{className:"",begin:/\s/,end:/\s*/,skip:!0},{variants:[{begin:s.begin,end:s.end},{begin:t.begin,"on:begin":t.isTrulyOpeningTag,end:t.end}],subLanguage:"xml",contains:[{begin:t.begin,end:t.end,skip:!0,contains:["self"]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/[{;]/,excludeEnd:!0,keywords:i,contains:["self",e.inherit(e.TITLE_MODE,{begin:a}),R],illegal:/%/},{beginKeywords:"while if switch catch for"},{className:"function",begin:e.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{",returnBegin:!0,contains:[R,e.inherit(e.TITLE_MODE,{begin:a}),]},{variants:[{begin:"\\."+a},{begin:"\\$"+a}],relevance:0},{className:"class",beginKeywords:"class",end:/[{;=]/,excludeEnd:!0,illegal:/[:"[\]]/,contains:[{beginKeywords:"extends"},e.UNDERSCORE_TITLE_MODE]},{begin:/\b(?=constructor)/,end:/[{;]/,excludeEnd:!0,contains:[e.inherit(e.TITLE_MODE,{begin:a}),"self",R]},{begin:"(get|set)\\s+(?="+a+"\\()",end:/\{/,keywords:"get set",contains:[e.inherit(e.TITLE_MODE,{begin:a}),{begin:/\(\)/},R]},{begin:/\$[(.]/}]}}; + + hljs.registerLanguage("javascript", javascript); +})(hljs); + +/* +Below is a stripped version of Lowlight. +The original source code can be found at https://github.com/wooorm/lowlight +It is licensed under the MIT License by Titus Wormer. + +The original source code has been condensed to fit within this file, and +modified to use the pre-compiled highlight.js above. + +Additionally, the `fault` dependency has been removed. + +Version: 1.17.0 (Not the latest version, intentionally) +File: lib/core.js +License: https://github.com/wooorm/lowlight/blob/main/license +*/ +const lowlight = ((high) => { + const lowlight={}; + + lowlight.highlight=highlight,lowlight.highlightAuto=highlightAuto,lowlight.registerLanguage=registerLanguage,lowlight.listLanguages=listLanguages,lowlight.registerAlias=registerAlias,Emitter.prototype.addText=text,Emitter.prototype.addKeyword=addKeyword,Emitter.prototype.addSublanguage=addSublanguage,Emitter.prototype.openNode=open,Emitter.prototype.closeNode=close,Emitter.prototype.closeAllNodes=noop,Emitter.prototype.finalize=noop,Emitter.prototype.toHTML=toHtmlNoop;var defaultPrefix="hljs-";function highlight(e,t,i){var r,n=high.configure({}),o=(i||{}).prefix;if(null==o&&(o=defaultPrefix),high.configure({__emitter:Emitter,classPrefix:o}),r=high.highlight(e,t,!0),high.configure(n||{}),r.errorRaised)throw r.errorRaised;return{relevance:r.relevance,language:r.language,value:r.emitter.rootNode.children}}function highlightAuto(e,t){var i,r,n,o,a=t||{},l=a.subset||high.listLanguages(),g=a.prefix,s=l.length,h=-1;for(null==g&&(g=defaultPrefix),r={relevance:0,language:null,value:[]},i={relevance:0,language:null,value:[]};++hr.relevance&&(r=n),n.relevance>i.relevance&&(r=i,i=n));return r.language&&(i.secondBest=r),i}function registerLanguage(e,t){high.registerLanguage(e,t)}function listLanguages(){return high.listLanguages()}function registerAlias(e,t){var i,r=e;for(i in t&&((r={})[e]=t),r)high.registerAliases(r[i],{languageName:i})}function Emitter(e){this.options=e,this.rootNode={children:[]},this.stack=[this.rootNode]}function addKeyword(e,t){this.openNode(t),this.addText(e),this.closeNode()}function addSublanguage(e,t){var i=this.stack,r=i[i.length-1],n=e.rootNode.children;r.children=r.children.concat(t?{type:"element",tagName:"span",properties:{className:[t]},children:n}:n)}function text(e){var t,i,r=this.stack;""!==e&&((i=(t=r[r.length-1]).children[t.children.length-1])&&"text"===i.type?i.value+=e:t.children.push({type:"text",value:e}))}function open(e){var t=this.stack,i=this.options.classPrefix+e,r=t[t.length-1],n={type:"element",tagName:"span",properties:{className:[i]},children:[]};r.children.push(n),t.push(n)}function close(){this.stack.pop()}function toHtmlNoop(){return""}function noop(){} + + return lowlight; +})(hljs); + +/* +Below is a stripped version of Emphasize. +The original source code can be found at https://github.com/wooorm/emphasize +It is licensed under the MIT License by Titus Wormer. + +The original source code has been condensed to fit within this file, and +modified to use the pre-compiled highlight.js and lowlight above. + +Additionally, the `chalk` dependency has been removed. + +Version: 4.2.0 (Not the latest version, intentionally) +File: lib/core.js +License: https://github.com/wooorm/emphasize/blob/main/license +*/ + +const emphasize = ((lowlight) => { + var high=lowlight.highlight,auto=lowlight.highlightAuto;function Lowlight(){}Lowlight.prototype=lowlight;var emphasize=new Lowlight;function highlight(i,h,t){var l=high.call(this,i,h);return l.value=all(t,l.value),l}function highlightAuto(i,h){var t,l,e;return h&&(h.subset?(l=h.sheet,e={subset:h.subset}):l=h),(t=auto.call(this,i,e)).value=all(l,t.value),t.secondBest&&(t.secondBest.value=all(l,t.secondBest.value)),t}function visit(i,h){var t,l,e,a,n,s=(h.properties||{}).className,u={};for(t in s=s?s[0].replace(/hljs-/,""):"",i)l=t.split(" "),e=i[t],l[0]===s?1===l.length?a=e:u[l.slice(1).join(" ")]=e:u[t]=e;return n="","value"in h&&(n=h.value),"children"in h&&(n=all(u,h.children)),a&&(n=a(n)),n}function all(i,h){for(var t=[],l=h.length,e=-1;++e `\x1b[${color[0]}m${s}\x1b[${color[1]}m`; +} + +const sheet = { + 'comment': makeStyled(util.inspect.colors.grey), + 'quote': makeStyled(util.inspect.colors.grey), + + 'keyword': makeStyled(util.inspect.colors.green), + 'addition': makeStyled(util.inspect.colors.green), + + 'number': makeStyled(util.inspect.colors.yellow), + 'string': makeStyled(util.inspect.colors.green), + 'meta meta-string': makeStyled(util.inspect.colors.cyan), + 'literal': makeStyled(util.inspect.colors.cyan), + 'doctag': makeStyled(util.inspect.colors.cyan), + 'regexp': makeStyled(util.inspect.colors.cyan), + + 'attribute': undefined, + 'attr': undefined, + 'variable': makeStyled(util.inspect.colors.yellow), + 'template-variable': makeStyled(util.inspect.colors.yellow), + 'class title': makeStyled(util.inspect.colors.yellow), + 'type': makeStyled(util.inspect.colors.yellow), + + 'symbol': makeStyled(util.inspect.colors.magenta), + 'bullet': makeStyled(util.inspect.colors.magenta), + 'subst': makeStyled(util.inspect.colors.magenta), + 'meta': makeStyled(util.inspect.colors.magenta), + 'meta keyword': makeStyled(util.inspect.colors.magenta), + 'link': makeStyled(util.inspect.colors.magenta), + + 'built_in': makeStyled(util.inspect.colors.cyan), + 'deletion': makeStyled(util.inspect.colors.red), + + 'emphasis': makeStyled(util.inspect.colors.italic), + 'strong': makeStyled(util.inspect.colors.bold), + 'formula': makeStyled(util.inspect.colors.inverse), +}; + +module.exports = (s) => + emphasize.highlight('js', s, sheet).value; diff --git a/lib/internal/repl/experimental/history.js b/lib/internal/repl/experimental/history.js new file mode 100644 index 00000000000000..ae04d7081e40a2 --- /dev/null +++ b/lib/internal/repl/experimental/history.js @@ -0,0 +1,47 @@ +'use strict'; + +const fs = require('internal/fs/promises'); +const permission = require('internal/process/permission'); +const path = require('path'); +const os = require('os'); + +module.exports = async () => { + let handle; + + try { + const historyPath = path.join(os.homedir(), '.node_repl_history'); + if (permission.isEnabled() && permission.has('fs.write', historyPath) === false) { + process.stdout.write('\nAccess to FileSystemWrite is restricted.\n' + + 'REPL session history will not be persisted.\n'); + return { + __proto__: null, + history: [], + writeHistory: () => false, + }; + } + + handle = await fs.open(historyPath, 'a+', 0o0600); + const data = await handle.readFile({ encoding: 'utf8' }); + const history = data.split(os.EOL, 1000); + const writeHistory = async (d) => { + if (!handle) { + return false; + } + try { + await handle.truncate(0); + await handle.writeFile(d.join(os.EOL)); + return true; + } catch { + handle.close().catch(() => undefined); + handle = null; + return false; + } + }; + return { __proto__: null, history, writeHistory }; + } catch { + if (handle) { + handle.close().catch(() => undefined); + } + return { __proto__: null, history: [], writeHistory: () => false }; + } +}; diff --git a/lib/internal/repl/experimental/index.js b/lib/internal/repl/experimental/index.js new file mode 100755 index 00000000000000..63fb6eadcbc48b --- /dev/null +++ b/lib/internal/repl/experimental/index.js @@ -0,0 +1,503 @@ +'use strict'; + +const { createInterface, clearScreenDown } = require('readline'); +const { parse: acornParse } = require('internal/deps/acorn/acorn/dist/acorn'); +const { isIdentifier, strEscape, underlineIgnoreANSI } = require('internal/repl/experimental/util'); +const highlight = require('internal/repl/experimental/highlight'); +const getHistory = require('internal/repl/experimental/history'); +const util = require('util'); +const vm = require('vm'); +const { runInContext } = vm.Script.prototype; +const { Module: CJSModule } = require('internal/modules/cjs/loader'); +const { makeRequireFunction } = require('internal/modules/helpers'); +const { makeContextifyScript } = require('internal/vm'); +const { getOrInitializeCascadedLoader } = require('internal/modules/esm/loader'); +const { pathToFileURL } = require('internal/url'); +const { emitExperimentalWarning } = require('internal/util'); +const path = require('path'); + +const { + ObjectDefineProperty, + ObjectGetOwnPropertyNames, + ReflectApply, + Symbol, + MathFloor, +} = primordials; + +function stripAnsi(text) { + // eslint-disable-next-line no-control-regex + const ansiEscape = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g; + return text.replace(ansiEscape, ''); +} + +function makePrompt(i) { + return util.styleText('green', `In [${util.styleText('bold', '' + i)}]: `); +} + +function makePromptOut(inspected, i) { + if (/[\r\n\u2028\u2029]/u.test(inspected)) { + return ''; + } + return util.styleText('red', `Out[${util.styleText('bold', '' + i)}]: `); +} + +function promptLength(i) { + return `In [${i}]: `.length; +} + +function importModuleDynamically(specifier, _, importAttributes) { + const cascadedLoader = getOrInitializeCascadedLoader(); + return cascadedLoader.import(specifier, pathToFileURL(path.join(process.cwd(), 'repl')).href, importAttributes); +} + +class ExperimentalREPLServer { + constructor() { + this.context = vm.createContext(); + this.initialize(); + } + + getGlobalNames() { + return ObjectGetOwnPropertyNames(this.context); + } + + evaluate(source) { + const wrapped = /^\s*{/.test(source) && !/;\s*$/.test(source) ? `(${source})` : source; + const contextified = makeContextifyScript( + wrapped, + 'repl', + 0, + 0, + undefined, + false, + undefined, + Symbol('repl'), + importModuleDynamically, + ); + + return ReflectApply(runInContext, contextified, [this.context, { + displayErrors: false, + }]); + } + + completeLine(line) { + if (line.length === 0) { + return this.getGlobalNames(); + } + + const statements = acornParse(line, { ecmaVersion: 2021 }).body; + const statement = statements[statements.length - 1]; + + if (!statement || statement.type !== 'ExpressionStatement') { + return undefined; + } + + let { expression } = statement; + if (expression.operator === 'void') { + expression = expression.argument; + } + + let keys; + let filter; + if (expression.type === 'Identifier') { + keys = this.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') { + // eslint-disable-next-line node-core/non-ascii-character + if (expression.property.name === '✖') { + filter = undefined; + } else { + filter = expression.property.name; + if (expression.computed) { + keys = this.getGlobalNames(); + } + } + } else { + return undefined; + } + + if (!keys) { + let evaluateResult = this.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 = this.evaluate(`Object(${expr})`, true); + if (evaluateResult.exceptionDetails) { + return undefined; + } + } + + const own = []; + const inherited = []; + + 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); + } + return `${r}]`; + }); + } else { + keys = keys.filter(isIdentifier); + } + } + } + + if (keys) { + if (filter) { + keys = keys + .filter((k) => k.startsWith(filter) && k !== filter) + .map((k) => k.slice(filter.length)); + } + return { fillable: true, completions: keys }; + } + + return undefined; + } + + getPreview(line) { + try { + const result = this.evaluate(line); + const inspected = util.inspect(result, { + colors: false, + breakLength: Infinity, + compact: true, + maxArrayLength: 10, + depth: 1, + }); + + let noAnsi = stripAnsi(inspected); + const length = process.stdout.columns - promptLength(this.promptIndex) - 2; + if (noAnsi.length > length) { + noAnsi = noAnsi.slice(0, length - 3) + '...'; + } + + return noAnsi; + // eslint-disable-next-line no-unused-vars + } catch (_) { + return undefined; + } + } + + countLines(line) { + let count = 0; + line.split(/\r?\n/).forEach((inner) => { + inner = stripAnsi(inner); + count += 1; + if (inner.length > process.stdout.columns) { + count += MathFloor(inner.length / process.stdout.columns); + } + }); + return count; + } + + initialize() { + const replModule = new CJSModule(''); + replModule.paths = CJSModule._resolveLookupPaths('', module, true); + ObjectDefineProperty(this.context, 'module', { + __proto__: null, + configurable: true, + writable: true, + value: replModule, + }); + ObjectDefineProperty(this.context, 'require', { + __proto__: null, + configurable: true, + writable: true, + value: makeRequireFunction(replModule), + }); + + CJSModule.builtinModules + .filter((x) => !/^_|\//.test(x)) + .forEach((name) => { + if (name === 'domain' || name === 'repl' || name === 'sys') { + return; + } + ObjectDefineProperty(this.context, name, { + __proto__: null, + value: require(name), + writable: true, + enumerable: false, + configurable: true, + }); + }); + + ['_', '__', '___', '_err'].forEach((prop) => { + ObjectDefineProperty(this.context, prop, { + __proto__: null, + value: undefined, + writable: true, + enumerable: false, + configurable: true, + }); + }); + + process.on('uncaughtException', (e) => { + process.stdout.write(`Uncaught ${util.inspect(e)}\n`); + }); + + process.on('unhandledRejection', (reason) => { + process.stdout.write(`Unhandled ${util.inspect(reason)}\n`); + }); + } + + updateInspect(uncaught, line, value) { + if (uncaught) { + this.context._err = value; + } else { + this.context.___ = this.context.__; + this.context.__ = this.context._; + this.context._ = value; + ObjectDefineProperty(this.context, `_${line}`, { + __proto__: null, + value, + writable: true, + enumerable: false, + configurable: true, + }); + } + return util.inspect(value, { + colors: true, + showProxy: true, + }); + } + + close() { + this.rl.close(); + } + + async start() { + this.rl = createInterface({ + input: process.stdin, + output: process.stdout, + prompt: makePrompt(1), + completer: (line, cb) => { + try { + const completion = this.completeLine(line); + if (completion.fillable) { + cb(null, [(completion.completions || []).map((l) => line + l), line]); + } else { + cb(null, [[], line]); + } + // eslint-disable-next-line no-unused-vars + } catch (_) { + cb(null, [[], line]); + } + }, + postprocessor: (line) => highlight(line), + }); + + this.rl.pause(); + + if (!this.rl.postprocessor) { + this.rl._insertString = (c) => { + const beg = this.rl.line.slice(0, this.rl.cursor); + const end = this.rl.line.slice(this.rl.cursor, this.rl.line.length); + this.rl.line = beg + c + end; + this.rl.cursor += c.length; + this.rl._refreshLine(); + }; + } + + const history = await getHistory(); + this.rl.history = history.history; + + let MODE = 'NORMAL'; + this.promptIndex = 1; + let nextCtrlCKills = false; + let nextCtrlDKills = false; + + this.rl.on('SIGINT', () => { + nextCtrlDKills = false; + if (MODE === 'REVERSE') { + MODE = 'NORMAL'; + process.stdout.moveCursor(0, -1); + process.stdout.cursorTo(0); + this.rl._refreshLine(); + } else if (this.rl.line.length) { + this.rl.line = ''; + this.rl.cursor = 0; + this.rl._refreshLine(); + } else if (nextCtrlCKills) { + this.close(); + process.exit(); + } else { + nextCtrlCKills = true; + process.stdout.write(`\n(To exit, press ^C again)\n${this.rl.getPrompt()}`); + } + }); + + let completionCache; + const ttyWrite = this.rl._ttyWrite.bind(this.rl); + + this.rl._ttyWrite = (d, key) => { + + if (key.name === 'tab') { + clearScreenDown(process.stdout); + } + + if (!(key.ctrl && key.name === 'c')) { + nextCtrlCKills = false; + } + + if (key.ctrl && key.name === 'd') { + if (nextCtrlDKills) process.exit(); + nextCtrlDKills = true; + process.stdout.write(`\n(To exit, press ^D again)\n${this.rl.getPrompt()}`); + return; + } + + nextCtrlDKills = false; + if (key.ctrl && key.name === 'r' && MODE === 'NORMAL') { + MODE = 'REVERSE'; + process.stdout.write('\n'); + this.rl._refreshLine(); + return; + } + + if (key.name === 'return' && MODE === 'REVERSE') { + MODE = 'NORMAL'; + const match = this.rl.history.find((h) => h.includes(this.rl.line)); + process.stdout.moveCursor(0, -1); + process.stdout.cursorTo(0); + process.stdout.clearScreenDown(); + this.rl.cursor = match.indexOf(this.rl.line) + this.rl.line.length; + this.rl.line = match; + this.rl._refreshLine(); + return; + } + + ttyWrite(d, key); + + if (key.name === 'right' && this.rl.cursor === this.rl.line.length) { + if (completionCache) { + this.rl._insertString(completionCache); + } + } + }; + + const refreshLine = this.rl._refreshLine.bind(this.rl); + this.rl._refreshLine = () => { + completionCache = undefined; + const inspectedLine = this.rl.line; + + if (MODE === 'REVERSE') { + process.stdout.moveCursor(0, -1); + process.stdout.cursorTo(promptLength(this.promptIndex)); + clearScreenDown(process.stdout); + let match; + if (inspectedLine) { + match = this.rl.history.find((h) => h.includes(inspectedLine)); + } + if (match) { + match = highlight(match); + match = underlineIgnoreANSI(match, inspectedLine); + } + process.stdout.write(`${match || ''}\n(reverse-i-search): ${inspectedLine}`); + process.stdout.cursorTo('(reverse-i-search): '.length + this.rl.cursor); + return; + } + + if (this.rl.postprocessor === undefined) { + this.rl.line = highlight(inspectedLine); + } + + refreshLine(); + this.rl.line = inspectedLine; + + process.stdout.cursorTo(promptLength(this.promptIndex) + this.rl.cursor); + + if (inspectedLine !== '') { + try { + const completion = this.completeLine(inspectedLine); + const preview = this.getPreview(inspectedLine + (completion?.completions[0] || '')); + + if (this.rl.line !== inspectedLine) { + return; + } + + let rows = 0; + if (completion && completion.completions.length > 0) { + if (completion.fillable) { + completionCache = completion.completions[0]; + } + process.stdout.cursorTo(promptLength(this.promptIndex) + this.rl.line.length); + process.stdout.write(util.styleText('grey', completion.completions[0])); + rows += this.countLines(completion.completions[0]) - 1; + } + + if (preview) { + process.stdout.write(util.styleText('grey', `\nOut[${this.promptIndex}]: ${preview}\n`)); + rows += this.countLines(preview) + 1; + } + + process.stdout.cursorTo(promptLength(this.promptIndex) + this.rl.cursor); + process.stdout.moveCursor(0, -rows); + // eslint-disable-next-line no-unused-vars, no-empty + } catch (_) { } + } + }; + + process.stdout.write( + 'Node.js ' + process.versions.node + ' (V8 ' + process.versions.v8 + ')\n' + + '(Highly) Experimental REPL\n\n', + ); + + emitExperimentalWarning('Experimental REPL'); + + this.rl.resume(); + this.rl.prompt(); + + for await (const line of this.rl) { + this.rl.pause(); + clearScreenDown(process.stdout); + + let result; + let uncaught = false; + try { + result = this.evaluate(line); + } catch (error) { + result = error; + uncaught = true; + } + + const inspected = this.updateInspect(uncaught, this.promptIndex, result); + + process.stdout.write(`${makePromptOut(inspected, this.promptIndex)}${uncaught ? 'Uncaught ' : ''}${inspected}\n\n`); + + this.promptIndex++; + this.rl.setPrompt(makePrompt(this.promptIndex)); + + await history.writeHistory(this.rl.history); + + this.rl.resume(); + this.rl.prompt(); + } + } +} + +module.exports = ExperimentalREPLServer; diff --git a/lib/internal/repl/experimental/util.js b/lib/internal/repl/experimental/util.js new file mode 100644 index 00000000000000..85b19c382c813d --- /dev/null +++ b/lib/internal/repl/experimental/util.js @@ -0,0 +1,202 @@ +// Copyright Node.js contributors. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// +// https://github.com/nodejs/node/blob/master/lib/internal/util/inspect.js +// https://github.com/nodejs/node/blob/master/lib/util.js + +'use strict'; + +const { isIdentifierStart, isIdentifierChar } = require('internal/deps/acorn/acorn/dist/acorn'); + +function isIdentifier(str) { + if (str === '') { + return false; + } + const first = str.codePointAt(0); + if (!isIdentifierStart(first)) { + return false; + } + const firstLen = first > 0xffff ? 2 : 1; + for (let i = firstLen; i < str.length; i += 1) { + const cp = str.codePointAt(i); + if (!isIdentifierChar(cp)) { + return false; + } + if (cp > 0xffff) { + i += 1; + } + } + return true; +} + +/* eslint-disable no-control-regex */ +const strEscapeSequencesRegExp = /[\x00-\x1f\x27\x5c]/; +const strEscapeSequencesReplacer = /[\x00-\x1f\x27\x5c]/g; + +const ansi = /[\u001B\u009B][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[-a-zA-Z\d/#&.:=?%@~_]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-ntqry=><~]))/g; +/* eslint-enable no-control-regex */ + +// Escaped special characters. Use empty strings to fill up unused entries. +const meta = [ + '\\u0000', '\\u0001', '\\u0002', '\\u0003', '\\u0004', + '\\u0005', '\\u0006', '\\u0007', '\\b', '\\t', + '\\n', '\\u000b', '\\f', '\\r', '\\u000e', + '\\u000f', '\\u0010', '\\u0011', '\\u0012', '\\u0013', + '\\u0014', '\\u0015', '\\u0016', '\\u0017', '\\u0018', + '\\u0019', '\\u001a', '\\u001b', '\\u001c', '\\u001d', + '\\u001e', '\\u001f', '', '', '', + '', '', '', '', "\\'", '', '', '', '', '', + '', '', '', '', '', '', '', '', '', '', + '', '', '', '', '', '', '', '', '', '', + '', '', '', '', '', '', '', '', '', '', + '', '', '', '', '', '', '', '', '', '', + '', '', '', '', '', '', '', '\\\\', +]; + +const escapeFn = (str) => meta[str.charCodeAt(0)]; + +const strEscape = (str) => { + // Some magic numbers that worked out fine while benchmarking with v8 6.0 + if (str.length < 5000 && !strEscapeSequencesRegExp.test(str)) { + return `'${str}'`; + } + if (str.length > 100) { + return `'${str.replace(strEscapeSequencesReplacer, escapeFn)}'`; + } + let result = ''; + let last = 0; + let i = 0; + for (; i < str.length; i += 1) { + const point = str.charCodeAt(i); + if (point === 39 || point === 92 || point < 32) { + if (last === i) { + result += meta[point]; + } else { + result += `${str.slice(last, i)}${meta[point]}`; + } + last = i + 1; + } + } + if (last === 0) { + result = str; + } else if (last !== i) { + result += str.slice(last); + } + return `'${result}'`; +}; + +const stripVTControlCharacters = (str) => str.replace(ansi, ''); + +const isFullWidthCodePoint = (code) => +// Code points are partially derived from: +// http://www.unicode.org/Public/UNIDATA/EastAsianWidth.txt + code >= 0x1100 && ( + code <= 0x115f || // Hangul Jamo + code === 0x2329 || // LEFT-POINTING ANGLE BRACKET + code === 0x232a || // RIGHT-POINTING ANGLE BRACKET + // CJK Radicals Supplement .. Enclosed CJK Letters and Months + (code >= 0x2e80 && code <= 0x3247 && code !== 0x303f) || + // Enclosed CJK Letters and Months .. CJK Unified Ideographs Extension A + (code >= 0x3250 && code <= 0x4dbf) || + // CJK Unified Ideographs .. Yi Radicals + (code >= 0x4e00 && code <= 0xa4c6) || + // Hangul Jamo Extended-A + (code >= 0xa960 && code <= 0xa97c) || + // Hangul Syllables + (code >= 0xac00 && code <= 0xd7a3) || + // CJK Compatibility Ideographs + (code >= 0xf900 && code <= 0xfaff) || + // Vertical Forms + (code >= 0xfe10 && code <= 0xfe19) || + // CJK Compatibility Forms .. Small Form Variants + (code >= 0xfe30 && code <= 0xfe6b) || + // Halfwidth and Fullwidth Forms + (code >= 0xff01 && code <= 0xff60) || + (code >= 0xffe0 && code <= 0xffe6) || + // Kana Supplement + (code >= 0x1b000 && code <= 0x1b001) || + // Enclosed Ideographic Supplement + (code >= 0x1f200 && code <= 0x1f251) || + // Miscellaneous Symbols and Pictographs 0x1f300 - 0x1f5ff + // Emoticons 0x1f600 - 0x1f64f + (code >= 0x1f300 && code <= 0x1f64f) || + // CJK Unified Ideographs Extension B .. Tertiary Ideographic Plane + (code >= 0x20000 && code <= 0x3fffd) + ); + +const isZeroWidthCodePoint = (code) => + code <= 0x1F || // C0 control codes + (code > 0x7F && code <= 0x9F) || // C1 control codes + (code >= 0x300 && code <= 0x36F) || // Combining Diacritical Marks + (code >= 0x200B && code <= 0x200F) || // Modifying Invisible Characters + (code >= 0xFE00 && code <= 0xFE0F) || // Variation Selectors + (code >= 0xFE20 && code <= 0xFE2F) || // Combining Half Marks + (code >= 0xE0100 && code <= 0xE01EF); // Variation Selectors + +const getStringWidth = (str, removeControlChars = true) => { + let width = 0; + + if (removeControlChars) { + str = stripVTControlCharacters(str); + } + + for (const char of str) { + const code = char.codePointAt(0); + if (isFullWidthCodePoint(code)) { + width += 2; + } else if (!isZeroWidthCodePoint(code)) { + width += 1; + } + } + + return width; +}; + +const underlineIgnoreANSI = (str, needle) => { + let start = -1; + outer: + while (true) { + start = str.indexOf(needle[0], start + 1); + if (start === -1) { + return str; + } + let strIndex = start; + for (let i = 0; i < needle.length; i += 1) { + if (needle[i] !== str[strIndex]) { + continue outer; + } + strIndex += 1; + if (str[strIndex] === '\u001b') { + // Assumes this ansi escape is a mode override (m) + strIndex = str.indexOf('m', strIndex) + 1; + } + } + const u = `\u001b[4m${str.slice(start, strIndex)}\u001b[24m`; + return str.slice(0, start) + u + str.slice(strIndex); + } +}; + +module.exports = { + isIdentifier, + strEscape, + getStringWidth, + underlineIgnoreANSI, + stripVTControlCharacters, +}; diff --git a/lib/internal/repl/await.js b/lib/internal/repl/stable/await.js similarity index 100% rename from lib/internal/repl/await.js rename to lib/internal/repl/stable/await.js diff --git a/lib/internal/repl/history.js b/lib/internal/repl/stable/history.js similarity index 100% rename from lib/internal/repl/history.js rename to lib/internal/repl/stable/history.js diff --git a/lib/internal/repl/stable/index.js b/lib/internal/repl/stable/index.js new file mode 100644 index 00000000000000..272011928536e7 --- /dev/null +++ b/lib/internal/repl/stable/index.js @@ -0,0 +1,1888 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +/* A REPL library that you can include in your own code to get a runtime + * interface to your program. + * + * const repl = require("repl"); + * // start repl on stdin + * repl.start("prompt> "); + * + * // listen for unix socket connections and start repl on them + * net.createServer(function(socket) { + * repl.start("node via Unix socket> ", socket); + * }).listen("/tmp/node-repl-sock"); + * + * // listen for TCP socket connections and start repl on them + * net.createServer(function(socket) { + * repl.start("node via TCP socket> ", socket); + * }).listen(5001); + * + * // expose foo to repl context + * repl.start("node > ").context.foo = "stdin is fun"; + */ + +'use strict'; + +const { + ArrayPrototypeAt, + ArrayPrototypeFilter, + ArrayPrototypeFindLastIndex, + ArrayPrototypeForEach, + ArrayPrototypeIncludes, + ArrayPrototypeJoin, + ArrayPrototypeMap, + ArrayPrototypePop, + ArrayPrototypePush, + ArrayPrototypePushApply, + ArrayPrototypeShift, + ArrayPrototypeSlice, + ArrayPrototypeSome, + ArrayPrototypeSort, + ArrayPrototypeUnshift, + Boolean, + Error: MainContextError, + FunctionPrototypeBind, + JSONStringify, + MathMaxApply, + NumberIsNaN, + NumberParseFloat, + ObjectAssign, + ObjectDefineProperty, + ObjectGetOwnPropertyDescriptor, + ObjectGetOwnPropertyNames, + ObjectGetPrototypeOf, + ObjectKeys, + ObjectSetPrototypeOf, + Promise, + ReflectApply, + RegExp, + RegExpPrototypeExec, + SafePromiseRace, + SafeSet, + SafeWeakSet, + StringPrototypeCharAt, + StringPrototypeCodePointAt, + StringPrototypeEndsWith, + StringPrototypeIncludes, + StringPrototypeRepeat, + StringPrototypeSlice, + StringPrototypeSplit, + StringPrototypeStartsWith, + StringPrototypeTrim, + StringPrototypeTrimStart, + StringPrototypeToLocaleLowerCase, + Symbol, + SyntaxError, + SyntaxErrorPrototype, + globalThis, +} = primordials; + +const { BuiltinModule } = require('internal/bootstrap/realm'); +const { + makeRequireFunction, + addBuiltinLibsToObject, +} = require('internal/modules/helpers'); +const { + isIdentifierStart, + isIdentifierChar, + parse: acornParse, +} = require('internal/deps/acorn/acorn/dist/acorn'); +const acornWalk = require('internal/deps/acorn/acorn-walk/dist/walk'); +const { + decorateErrorStack, + isError, + deprecate, + SideEffectFreeRegExpPrototypeSymbolReplace, + SideEffectFreeRegExpPrototypeSymbolSplit, +} = require('internal/util'); +const { inspect } = require('internal/util/inspect'); +const vm = require('vm'); + +const { runInThisContext, runInContext } = vm.Script.prototype; + +const path = require('path'); +const fs = require('fs'); +const { Interface } = require('readline'); +const { + commonPrefix, +} = require('internal/readline/utils'); +const { Console } = require('console'); +const { shouldColorize } = require('internal/util/colors'); +const CJSModule = require('internal/modules/cjs/loader').Module; +let _builtinLibs = ArrayPrototypeFilter( + CJSModule.builtinModules, + (e) => !StringPrototypeStartsWith(e, '_'), +); +const nodeSchemeBuiltinLibs = ArrayPrototypeMap( + _builtinLibs, (lib) => `node:${lib}`); +ArrayPrototypeForEach( + BuiltinModule.getSchemeOnlyModuleNames(), + (lib) => ArrayPrototypePush(nodeSchemeBuiltinLibs, `node:${lib}`), +); +const domain = require('domain'); +let debug = require('internal/util/debuglog').debuglog('repl', (fn) => { + debug = fn; +}); +const { + codes: { + ERR_CANNOT_WATCH_SIGINT, + ERR_INVALID_REPL_EVAL_CONFIG, + ERR_INVALID_REPL_INPUT, + ERR_MISSING_ARGS, + ERR_SCRIPT_EXECUTION_INTERRUPTED, + }, + isErrorStackTraceLimitWritable, + overrideStackTrace, + ErrorPrepareStackTrace, +} = require('internal/errors'); +const { sendInspectorCommand } = require('internal/util/inspector'); +const { getOptionValue } = require('internal/options'); +const { + validateFunction, + validateObject, +} = require('internal/validators'); +const experimentalREPLAwait = getOptionValue( + '--experimental-repl-await', +); +const pendingDeprecation = getOptionValue('--pending-deprecation'); +const { + REPL_MODE_SLOPPY, + REPL_MODE_STRICT, + isRecoverableError, + kStandaloneREPL, + setupPreview, + setupReverseSearch, +} = require('internal/repl/stable/utils'); +const { + constants: { + ALL_PROPERTIES, + SKIP_SYMBOLS, + }, + getOwnNonIndexProperties, +} = internalBinding('util'); +const { + startSigintWatchdog, + stopSigintWatchdog, +} = internalBinding('contextify'); + +const history = require('internal/repl/stable/history'); +const { + extensionFormatMap, +} = require('internal/modules/esm/formats'); +const { + makeContextifyScript, +} = require('internal/vm'); +let nextREPLResourceNumber = 1; +// This prevents v8 code cache from getting confused and using a different +// cache from a resource of the same name +function getREPLResourceName() { + return `REPL${nextREPLResourceNumber++}`; +} + +// Lazy-loaded. +let processTopLevelAwait; + +const globalBuiltins = + new SafeSet(vm.runInNewContext('Object.getOwnPropertyNames(globalThis)')); + +const parentModule = module; +const domainSet = new SafeWeakSet(); + +const kBufferedCommandSymbol = Symbol('bufferedCommand'); +const kContextId = Symbol('contextId'); +const kLoadingSymbol = Symbol('loading'); + +let addedNewListener = false; + +try { + // Hack for require.resolve("./relative") to work properly. + module.filename = path.resolve('repl'); +} catch { + // 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 = CJSModule._nodeModulePaths(module.filename); + +// This is the default "writer" value, if none is passed in the REPL options, +// and it can be overridden by custom print functions, such as `probe` or +// `eyes.js`. +const writer = (obj) => inspect(obj, writer.options); +writer.options = { ...inspect.defaultOptions, showProxy: true }; + +// Converts static import statement to dynamic import statement +const toDynamicImport = (codeLine) => { + let dynamicImportStatement = ''; + const ast = acornParse(codeLine, { __proto__: null, sourceType: 'module', ecmaVersion: 'latest' }); + acornWalk.ancestor(ast, { + ImportDeclaration(node) { + const awaitDynamicImport = `await import(${JSONStringify(node.source.value)});`; + if (node.specifiers.length === 0) { + dynamicImportStatement += awaitDynamicImport; + } else if (node.specifiers.length === 1 && node.specifiers[0].type === 'ImportNamespaceSpecifier') { + dynamicImportStatement += `const ${node.specifiers[0].local.name} = ${awaitDynamicImport}`; + } else { + const importNames = ArrayPrototypeJoin(ArrayPrototypeMap(node.specifiers, ({ local, imported }) => + (local.name === imported?.name ? local.name : `${imported?.name ?? 'default'}: ${local.name}`), + ), ', '); + dynamicImportStatement += `const { ${importNames} } = ${awaitDynamicImport}`; + } + }, + }); + return dynamicImportStatement; +}; + +function REPLServer(prompt, + stream, + eval_, + useGlobal, + ignoreUndefined, + replMode) { + if (!(this instanceof REPLServer)) { + return new REPLServer(prompt, + stream, + eval_, + useGlobal, + ignoreUndefined, + replMode); + } + + let options; + if (prompt !== null && typeof prompt === 'object') { + // An options object was given. + options = { ...prompt }; + stream = options.stream || options.socket; + eval_ = options.eval; + useGlobal = options.useGlobal; + ignoreUndefined = options.ignoreUndefined; + prompt = options.prompt; + replMode = options.replMode; + } else { + options = {}; + } + + if (!options.input && !options.output) { + // Legacy API, passing a 'stream'/'socket' option. + if (!stream) { + // Use stdin and stdout as the default streams if none were given. + stream = process; + } + // We're given a duplex readable/writable Stream, like a `net.Socket` + // or a custom object with 2 streams, or the `process` object. + options.input = stream.stdin || stream; + options.output = stream.stdout || stream; + } + + if (options.terminal === undefined) { + options.terminal = options.output.isTTY; + } + options.terminal = !!options.terminal; + + if (options.terminal && options.useColors === undefined) { + // If possible, check if stdout supports colors or not. + options.useColors = shouldColorize(options.output); + } + + // TODO(devsnek): Add a test case for custom eval functions. + const preview = options.terminal && + (options.preview !== undefined ? !!options.preview : !eval_); + + ObjectDefineProperty(this, 'inputStream', { + __proto__: null, + get: pendingDeprecation ? + deprecate(() => this.input, + 'repl.inputStream and repl.outputStream are deprecated. ' + + 'Use repl.input and repl.output instead', + 'DEP0141') : + () => this.input, + set: pendingDeprecation ? + deprecate((val) => this.input = val, + 'repl.inputStream and repl.outputStream are deprecated. ' + + 'Use repl.input and repl.output instead', + 'DEP0141') : + (val) => this.input = val, + enumerable: false, + configurable: true, + }); + ObjectDefineProperty(this, 'outputStream', { + __proto__: null, + get: pendingDeprecation ? + deprecate(() => this.output, + 'repl.inputStream and repl.outputStream are deprecated. ' + + 'Use repl.input and repl.output instead', + 'DEP0141') : + () => this.output, + set: pendingDeprecation ? + deprecate((val) => this.output = val, + 'repl.inputStream and repl.outputStream are deprecated. ' + + 'Use repl.input and repl.output instead', + 'DEP0141') : + (val) => this.output = val, + enumerable: false, + configurable: true, + }); + + this.allowBlockingCompletions = !!options.allowBlockingCompletions; + this.useColors = !!options.useColors; + this._domain = options.domain || domain.create(); + this.useGlobal = !!useGlobal; + this.ignoreUndefined = !!ignoreUndefined; + this.replMode = replMode || module.exports.REPL_MODE_SLOPPY; + this.underscoreAssigned = false; + this.last = undefined; + this.underscoreErrAssigned = false; + this.lastError = undefined; + this.breakEvalOnSigint = !!options.breakEvalOnSigint; + this.editorMode = false; + // Context id for use with the inspector protocol. + this[kContextId] = undefined; + + if (this.breakEvalOnSigint && eval_) { + // Allowing this would not reflect user expectations. + // breakEvalOnSigint affects only the behavior of the default eval(). + throw new ERR_INVALID_REPL_EVAL_CONFIG(); + } + + if (options[kStandaloneREPL]) { + // It is possible to introspect the running REPL accessing this variable + // from inside the REPL. This is useful for anyone working on the REPL. + module.exports.repl = this; + } else if (!addedNewListener) { + // Add this listener only once and use a WeakSet that contains the REPLs + // domains. Otherwise we'd have to add a single listener to each REPL + // instance and that could trigger the `MaxListenersExceededWarning`. + process.prependListener('newListener', (event, listener) => { + if (event === 'uncaughtException' && + process.domain && + listener.name !== 'domainUncaughtExceptionClear' && + domainSet.has(process.domain)) { + // Throw an error so that the event will not be added and the current + // domain takes over. That way the user is notified about the error + // and the current code evaluation is stopped, just as any other code + // that contains an error. + throw new ERR_INVALID_REPL_INPUT( + 'Listeners for `uncaughtException` cannot be used in the REPL'); + } + }); + addedNewListener = true; + } + + domainSet.add(this._domain); + + const savedRegExMatches = ['', '', '', '', '', '', '', '', '', '']; + const sep = '\u0000\u0000\u0000'; + const regExMatcher = new RegExp(`^${sep}(.*)${sep}(.*)${sep}(.*)${sep}(.*)` + + `${sep}(.*)${sep}(.*)${sep}(.*)${sep}(.*)` + + `${sep}(.*)$`); + + eval_ = eval_ || defaultEval; + + const self = this; + + // Pause taking in new input, and store the keys in a buffer. + const pausedBuffer = []; + let paused = false; + function pause() { + paused = true; + } + + function unpause() { + if (!paused) return; + paused = false; + let entry; + const tmpCompletionEnabled = self.isCompletionEnabled; + while ((entry = ArrayPrototypeShift(pausedBuffer)) !== undefined) { + const { 0: type, 1: payload, 2: isCompletionEnabled } = entry; + switch (type) { + case 'key': { + const { 0: d, 1: key } = payload; + self.isCompletionEnabled = isCompletionEnabled; + self._ttyWrite(d, key); + break; + } + case 'close': + self.emit('exit'); + break; + } + if (paused) { + break; + } + } + self.isCompletionEnabled = tmpCompletionEnabled; + } + + function defaultEval(code, context, file, cb) { + let result, script, wrappedErr; + let err = null; + let wrappedCmd = false; + let awaitPromise = false; + const input = code; + + // It's confusing for `{ a : 1 }` to be interpreted as a block + // statement rather than an object literal. So, we first try + // to wrap it in parentheses, so that it will be interpreted as + // an expression. Note that if the above condition changes, + // lib/internal/repl/utils.js needs to be changed to match. + if (RegExpPrototypeExec(/^\s*{/, code) !== null && + RegExpPrototypeExec(/;\s*$/, code) === null) { + code = `(${StringPrototypeTrim(code)})\n`; + wrappedCmd = true; + } + + const hostDefinedOptionId = Symbol(`eval:${file}`); + let parentURL; + try { + const { pathToFileURL } = require('internal/url'); + // Adding `/repl` prevents dynamic imports from loading relative + // to the parent of `process.cwd()`. + parentURL = pathToFileURL(path.join(process.cwd(), 'repl')).href; + } catch { + // Continue regardless of error. + } + async function importModuleDynamically(specifier, _, importAttributes) { + const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader(); + return cascadedLoader.import(specifier, parentURL, importAttributes); + } + // `experimentalREPLAwait` is set to true by default. + // Shall be false in case `--no-experimental-repl-await` flag is used. + if (experimentalREPLAwait && StringPrototypeIncludes(code, 'await')) { + if (processTopLevelAwait === undefined) { + ({ processTopLevelAwait } = require('internal/repl/await')); + } + + try { + const potentialWrappedCode = processTopLevelAwait(code); + if (potentialWrappedCode !== null) { + code = potentialWrappedCode; + wrappedCmd = true; + awaitPromise = true; + } + } catch (e) { + let recoverableError = false; + if (e.name === 'SyntaxError') { + // Remove all "await"s and attempt running the script + // in order to detect if error is truly non recoverable + const fallbackCode = SideEffectFreeRegExpPrototypeSymbolReplace(/\bawait\b/g, code, ''); + try { + makeContextifyScript( + fallbackCode, // code + file, // filename, + 0, // lineOffset + 0, // columnOffset, + undefined, // cachedData + false, // produceCachedData + undefined, // parsingContext + hostDefinedOptionId, // hostDefinedOptionId + importModuleDynamically, // importModuleDynamically + ); + } catch (fallbackError) { + if (isRecoverableError(fallbackError, fallbackCode)) { + recoverableError = true; + err = new Recoverable(e); + } + } + } + if (!recoverableError) { + decorateErrorStack(e); + err = e; + } + } + } + + // First, create the Script object to check the syntax + if (code === '\n') + return cb(null); + + if (err === null) { + while (true) { + try { + if (self.replMode === module.exports.REPL_MODE_STRICT && + RegExpPrototypeExec(/^\s*$/, code) === null) { + // "void 0" keeps the repl from returning "use strict" as the result + // value for statements and declarations that don't return a value. + code = `'use strict'; void 0;\n${code}`; + } + script = makeContextifyScript( + code, // code + file, // filename, + 0, // lineOffset + 0, // columnOffset, + undefined, // cachedData + false, // produceCachedData + undefined, // parsingContext + hostDefinedOptionId, // hostDefinedOptionId + importModuleDynamically, // importModuleDynamically + ); + } catch (e) { + debug('parse error %j', code, e); + if (wrappedCmd) { + // Unwrap and try again + wrappedCmd = false; + awaitPromise = false; + code = input; + wrappedErr = e; + continue; + } + // Preserve original error for wrapped command + const error = wrappedErr || e; + if (isRecoverableError(error, code)) + err = new Recoverable(error); + else + err = error; + } + break; + } + } + + // This will set the values from `savedRegExMatches` to corresponding + // predefined RegExp properties `RegExp.$1`, `RegExp.$2` ... `RegExp.$9` + RegExpPrototypeExec(regExMatcher, + ArrayPrototypeJoin(savedRegExMatches, sep)); + + let finished = false; + function finishExecution(err, result) { + if (finished) return; + finished = true; + + // After executing the current expression, store the values of RegExp + // predefined properties back in `savedRegExMatches` + for (let idx = 1; idx < savedRegExMatches.length; idx += 1) { + savedRegExMatches[idx] = RegExp[`$${idx}`]; + } + + cb(err, result); + } + + if (!err) { + // Unset raw mode during evaluation so that Ctrl+C raises a signal. + let previouslyInRawMode; + if (self.breakEvalOnSigint) { + // Start the SIGINT watchdog before entering raw mode so that a very + // quick Ctrl+C doesn't lead to aborting the process completely. + if (!startSigintWatchdog()) + throw new ERR_CANNOT_WATCH_SIGINT(); + previouslyInRawMode = self._setRawMode(false); + } + + try { + try { + const scriptOptions = { + displayErrors: false, + breakOnSigint: self.breakEvalOnSigint, + }; + + if (self.useGlobal) { + result = ReflectApply(runInThisContext, script, [scriptOptions]); + } else { + result = ReflectApply(runInContext, script, [context, scriptOptions]); + } + } finally { + if (self.breakEvalOnSigint) { + // Reset terminal mode to its previous value. + self._setRawMode(previouslyInRawMode); + + // Returns true if there were pending SIGINTs *after* the script + // has terminated without being interrupted itself. + if (stopSigintWatchdog()) { + self.emit('SIGINT'); + } + } + } + } catch (e) { + err = e; + + if (process.domain) { + debug('not recoverable, send to domain'); + process.domain.emit('error', err); + process.domain.exit(); + return; + } + } + + if (awaitPromise && !err) { + let sigintListener; + pause(); + let promise = result; + if (self.breakEvalOnSigint) { + const interrupt = new Promise((resolve, reject) => { + sigintListener = () => { + const tmp = MainContextError.stackTraceLimit; + if (isErrorStackTraceLimitWritable()) MainContextError.stackTraceLimit = 0; + const err = new ERR_SCRIPT_EXECUTION_INTERRUPTED(); + if (isErrorStackTraceLimitWritable()) MainContextError.stackTraceLimit = tmp; + reject(err); + }; + prioritizedSigintQueue.add(sigintListener); + }); + promise = SafePromiseRace([promise, interrupt]); + } + + (async () => { + try { + const result = (await promise)?.value; + finishExecution(null, result); + } catch (err) { + if (err && process.domain) { + debug('not recoverable, send to domain'); + process.domain.emit('error', err); + process.domain.exit(); + return; + } + finishExecution(err); + } finally { + // Remove prioritized SIGINT listener if it was not called. + prioritizedSigintQueue.delete(sigintListener); + unpause(); + } + })(); + } + } + + if (!awaitPromise || err) { + finishExecution(err, result); + } + } + + self.eval = self._domain.bind(eval_); + + self._domain.on('error', function debugDomainError(e) { + debug('domain error'); + let errStack = ''; + + if (typeof e === 'object' && e !== null) { + overrideStackTrace.set(e, (error, stackFrames) => { + let frames; + if (typeof stackFrames === 'object') { + // Search from the bottom of the call stack to + // find the first frame with a null function name + const idx = ArrayPrototypeFindLastIndex( + stackFrames, + (frame) => frame.getFunctionName() === null, + ); + // If found, get rid of it and everything below it + frames = ArrayPrototypeSlice(stackFrames, 0, idx); + } else { + frames = stackFrames; + } + // FIXME(devsnek): this is inconsistent with the checks + // that the real prepareStackTrace dispatch uses in + // lib/internal/errors.js. + if (typeof MainContextError.prepareStackTrace === 'function') { + return MainContextError.prepareStackTrace(error, frames); + } + return ErrorPrepareStackTrace(error, frames); + }); + decorateErrorStack(e); + + if (e.domainThrown) { + delete e.domain; + delete e.domainThrown; + } + + if (isError(e)) { + if (e.stack) { + if (e.name === 'SyntaxError') { + // Remove stack trace. + e.stack = SideEffectFreeRegExpPrototypeSymbolReplace( + /^\s+at\s.*\n?/gm, + SideEffectFreeRegExpPrototypeSymbolReplace(/^REPL\d+:\d+\r?\n/, e.stack, ''), + ''); + const importErrorStr = 'Cannot use import statement outside a ' + + 'module'; + if (StringPrototypeIncludes(e.message, importErrorStr)) { + e.message = 'Cannot use import statement inside the Node.js ' + + 'REPL, alternatively use dynamic import: ' + toDynamicImport(ArrayPrototypeAt(self.lines, -1)); + e.stack = SideEffectFreeRegExpPrototypeSymbolReplace( + /SyntaxError:.*\n/, + e.stack, + `SyntaxError: ${e.message}\n`); + } + } else if (self.replMode === module.exports.REPL_MODE_STRICT) { + e.stack = SideEffectFreeRegExpPrototypeSymbolReplace( + /(\s+at\s+REPL\d+:)(\d+)/, + e.stack, + (_, pre, line) => pre + (line - 1), + ); + } + } + errStack = self.writer(e); + + // Remove one line error braces to keep the old style in place. + if (errStack[0] === '[' && errStack[errStack.length - 1] === ']') { + errStack = StringPrototypeSlice(errStack, 1, -1); + } + } + } + + if (!self.underscoreErrAssigned) { + self.lastError = e; + } + + if (options[kStandaloneREPL] && + process.listenerCount('uncaughtException') !== 0) { + process.nextTick(() => { + process.emit('uncaughtException', e); + self.clearBufferedCommand(); + self.lines.level = []; + self.displayPrompt(); + }); + } else { + if (errStack === '') { + errStack = self.writer(e); + } + const lines = SideEffectFreeRegExpPrototypeSymbolSplit(/(?<=\n)/, errStack); + let matched = false; + + errStack = ''; + ArrayPrototypeForEach(lines, (line) => { + if (!matched && + RegExpPrototypeExec(/^\[?([A-Z][a-z0-9_]*)*Error/, line) !== null) { + errStack += writer.options.breakLength >= line.length ? + `Uncaught ${line}` : + `Uncaught:\n${line}`; + matched = true; + } else { + errStack += line; + } + }); + if (!matched) { + const ln = lines.length === 1 ? ' ' : ':\n'; + errStack = `Uncaught${ln}${errStack}`; + } + // Normalize line endings. + errStack += StringPrototypeEndsWith(errStack, '\n') ? '' : '\n'; + self.output.write(errStack); + self.clearBufferedCommand(); + self.lines.level = []; + self.displayPrompt(); + } + }); + + self.clearBufferedCommand(); + + function completer(text, cb) { + ReflectApply(complete, self, + [text, self.editorMode ? self.completeOnEditorMode(cb) : cb]); + } + + ReflectApply(Interface, this, [{ + input: options.input, + output: options.output, + completer: options.completer || completer, + terminal: options.terminal, + historySize: options.historySize, + prompt, + }]); + + self.resetContext(); + + this.commands = { __proto__: null }; + defineDefaultCommands(this); + + // Figure out which "writer" function to use + self.writer = options.writer || module.exports.writer; + + if (self.writer === writer) { + // Conditionally turn on ANSI coloring. + writer.options.colors = self.useColors; + + if (options[kStandaloneREPL]) { + ObjectDefineProperty(inspect, 'replDefaults', { + __proto__: null, + get() { + return writer.options; + }, + set(options) { + validateObject(options, 'options'); + return ObjectAssign(writer.options, options); + }, + enumerable: true, + configurable: true, + }); + } + } + + function _parseREPLKeyword(keyword, rest) { + const cmd = this.commands[keyword]; + if (cmd) { + ReflectApply(cmd.action, this, [rest]); + return true; + } + return false; + } + + self.on('close', function emitExit() { + if (paused) { + ArrayPrototypePush(pausedBuffer, ['close']); + return; + } + self.emit('exit'); + }); + + let sawSIGINT = false; + let sawCtrlD = false; + const prioritizedSigintQueue = new SafeSet(); + self.on('SIGINT', function onSigInt() { + if (prioritizedSigintQueue.size > 0) { + for (const task of prioritizedSigintQueue) { + task(); + } + return; + } + + const empty = self.line.length === 0; + self.clearLine(); + _turnOffEditorMode(self); + + const cmd = self[kBufferedCommandSymbol]; + if (!(cmd && cmd.length > 0) && empty) { + if (sawSIGINT) { + self.close(); + sawSIGINT = false; + return; + } + self.output.write( + '(To exit, press Ctrl+C again or Ctrl+D or type .exit)\n', + ); + sawSIGINT = true; + } else { + sawSIGINT = false; + } + + self.clearBufferedCommand(); + self.lines.level = []; + self.displayPrompt(); + }); + + self.on('line', function onLine(cmd) { + debug('line %j', cmd); + cmd = cmd || ''; + sawSIGINT = false; + + if (self.editorMode) { + self[kBufferedCommandSymbol] += cmd + '\n'; + + // code alignment + const matches = self._sawKeyPress && !self[kLoadingSymbol] ? + RegExpPrototypeExec(/^\s+/, cmd) : null; + if (matches) { + const prefix = matches[0]; + self.write(prefix); + self.line = prefix; + self.cursor = prefix.length; + } + ReflectApply(_memory, self, [cmd]); + return; + } + + // Check REPL keywords and empty lines against a trimmed line input. + const trimmedCmd = StringPrototypeTrim(cmd); + + // Check to see if a REPL keyword was used. If it returns true, + // display next prompt and return. + if (trimmedCmd) { + if (StringPrototypeCharAt(trimmedCmd, 0) === '.' && + StringPrototypeCharAt(trimmedCmd, 1) !== '.' && + NumberIsNaN(NumberParseFloat(trimmedCmd))) { + const matches = RegExpPrototypeExec(/^\.([^\s]+)\s*(.*)$/, trimmedCmd); + const keyword = matches && matches[1]; + const rest = matches && matches[2]; + if (ReflectApply(_parseREPLKeyword, self, [keyword, rest]) === true) { + return; + } + if (!self[kBufferedCommandSymbol]) { + self.output.write('Invalid REPL keyword\n'); + finish(null); + return; + } + } + } + + const evalCmd = self[kBufferedCommandSymbol] + cmd + '\n'; + + debug('eval %j', evalCmd); + self.eval(evalCmd, self.context, getREPLResourceName(), finish); + + function finish(e, ret) { + debug('finish', e, ret); + ReflectApply(_memory, self, [cmd]); + + if (e && !self[kBufferedCommandSymbol] && + StringPrototypeStartsWith(StringPrototypeTrim(cmd), 'npm ')) { + self.output.write('npm should be run outside of the ' + + 'Node.js REPL, in your normal shell.\n' + + '(Press Ctrl+D to exit.)\n'); + self.displayPrompt(); + return; + } + + // If error was SyntaxError and not JSON.parse error + if (e) { + if (e instanceof Recoverable && !sawCtrlD) { + // Start buffering data like that: + // { + // ... x: 1 + // ... } + self[kBufferedCommandSymbol] += cmd + '\n'; + self.displayPrompt(); + return; + } + self._domain.emit('error', e.err || e); + } + + // Clear buffer if no SyntaxErrors + self.clearBufferedCommand(); + sawCtrlD = false; + + // If we got any output - print it (if no error) + if (!e && + // When an invalid REPL command is used, error message is printed + // immediately. We don't have to print anything else. So, only when + // the second argument to this function is there, print it. + arguments.length === 2 && + (!self.ignoreUndefined || ret !== undefined)) { + if (!self.underscoreAssigned) { + self.last = ret; + } + self.output.write(self.writer(ret) + '\n'); + } + + // Display prompt again (unless we already did by emitting the 'error' + // event on the domain instance). + if (!e) { + self.displayPrompt(); + } + } + }); + + self.on('SIGCONT', function onSigCont() { + if (self.editorMode) { + self.output.write(`${self._initialPrompt}.editor\n`); + self.output.write( + '// Entering editor mode (Ctrl+D to finish, Ctrl+C to cancel)\n'); + self.output.write(`${self[kBufferedCommandSymbol]}\n`); + self.prompt(true); + } else { + self.displayPrompt(true); + } + }); + + const { reverseSearch } = setupReverseSearch(this); + + const { + clearPreview, + showPreview, + } = setupPreview( + this, + kContextId, + kBufferedCommandSymbol, + preview, + ); + + // Wrap readline tty to enable editor mode and pausing. + const ttyWrite = FunctionPrototypeBind(self._ttyWrite, self); + self._ttyWrite = (d, key) => { + key = key || {}; + if (paused && !(self.breakEvalOnSigint && key.ctrl && key.name === 'c')) { + ArrayPrototypePush(pausedBuffer, + ['key', [d, key], self.isCompletionEnabled]); + return; + } + if (!self.editorMode || !self.terminal) { + // Before exiting, make sure to clear the line. + if (key.ctrl && key.name === 'd' && + self.cursor === 0 && self.line.length === 0) { + self.clearLine(); + } + clearPreview(key); + if (!reverseSearch(d, key)) { + ttyWrite(d, key); + const showCompletionPreview = key.name !== 'escape'; + showPreview(showCompletionPreview); + } + return; + } + + // Editor mode + if (key.ctrl && !key.shift) { + switch (key.name) { + // TODO(BridgeAR): There should not be a special mode necessary for full + // multiline support. + case 'd': // End editor mode + _turnOffEditorMode(self); + sawCtrlD = true; + ttyWrite(d, { name: 'return' }); + break; + case 'n': // Override next history item + case 'p': // Override previous history item + break; + default: + ttyWrite(d, key); + } + } else { + switch (key.name) { + case 'up': // Override previous history item + case 'down': // Override next history item + break; + case 'tab': + // Prevent double tab behavior + self._previousKey = null; + ttyWrite(d, key); + break; + default: + ttyWrite(d, key); + } + } + }; + + self.displayPrompt(); +} +ObjectSetPrototypeOf(REPLServer.prototype, Interface.prototype); +ObjectSetPrototypeOf(REPLServer, Interface); + +// Prompt is a string to print on each line for the prompt, +// source is a stream to use for I/O, defaulting to stdin/stdout. +function start(prompt, source, eval_, useGlobal, ignoreUndefined, replMode) { + return new REPLServer( + prompt, source, eval_, useGlobal, ignoreUndefined, replMode); +} + +REPLServer.prototype.setupHistory = function setupHistory(historyFile, cb) { + history(this, historyFile, cb); +}; + +REPLServer.prototype.clearBufferedCommand = function clearBufferedCommand() { + this[kBufferedCommandSymbol] = ''; +}; + +REPLServer.prototype.close = function close() { + if (this.terminal && this._flushing && !this._closingOnFlush) { + this._closingOnFlush = true; + this.once('flushHistory', () => + ReflectApply(Interface.prototype.close, this, []), + ); + + return; + } + process.nextTick(() => + ReflectApply(Interface.prototype.close, this, []), + ); +}; + +REPLServer.prototype.createContext = function() { + let context; + if (this.useGlobal) { + context = globalThis; + } else { + sendInspectorCommand((session) => { + session.post('Runtime.enable'); + session.once('Runtime.executionContextCreated', ({ params }) => { + this[kContextId] = params.context.id; + }); + context = vm.createContext(); + session.post('Runtime.disable'); + }, () => { + context = vm.createContext(); + }); + ArrayPrototypeForEach(ObjectGetOwnPropertyNames(globalThis), (name) => { + // Only set properties that do not already exist as a global builtin. + if (!globalBuiltins.has(name)) { + ObjectDefineProperty(context, name, + { + __proto__: null, + ...ObjectGetOwnPropertyDescriptor(globalThis, name), + }); + } + }); + context.global = context; + const _console = new Console(this.output); + ObjectDefineProperty(context, 'console', { + __proto__: null, + configurable: true, + writable: true, + value: _console, + }); + } + + const replModule = new CJSModule(''); + replModule.paths = CJSModule._resolveLookupPaths('', parentModule); + + ObjectDefineProperty(context, 'module', { + __proto__: null, + configurable: true, + writable: true, + value: replModule, + }); + ObjectDefineProperty(context, 'require', { + __proto__: null, + configurable: true, + writable: true, + value: makeRequireFunction(replModule), + }); + + addBuiltinLibsToObject(context, ''); + + return context; +}; + +REPLServer.prototype.resetContext = function() { + this.context = this.createContext(); + this.underscoreAssigned = false; + this.underscoreErrAssigned = false; + // TODO(BridgeAR): Deprecate the lines. + this.lines = []; + this.lines.level = []; + + ObjectDefineProperty(this.context, '_', { + __proto__: null, + configurable: true, + get: () => this.last, + set: (value) => { + this.last = value; + if (!this.underscoreAssigned) { + this.underscoreAssigned = true; + this.output.write('Expression assignment to _ now disabled.\n'); + } + }, + }); + + ObjectDefineProperty(this.context, '_error', { + __proto__: null, + configurable: true, + get: () => this.lastError, + set: (value) => { + this.lastError = value; + if (!this.underscoreErrAssigned) { + this.underscoreErrAssigned = true; + this.output.write( + 'Expression assignment to _error now disabled.\n'); + } + }, + }); + + // Allow REPL extensions to extend the new context + this.emit('reset', this.context); +}; + +REPLServer.prototype.displayPrompt = function(preserveCursor) { + let prompt = this._initialPrompt; + if (this[kBufferedCommandSymbol].length) { + prompt = '...'; + const len = this.lines.level.length ? this.lines.level.length - 1 : 0; + const levelInd = StringPrototypeRepeat('..', len); + prompt += levelInd + ' '; + } + + // Do not overwrite `_initialPrompt` here + ReflectApply(Interface.prototype.setPrompt, this, [prompt]); + this.prompt(preserveCursor); +}; + +// When invoked as an API method, overwrite _initialPrompt +REPLServer.prototype.setPrompt = function setPrompt(prompt) { + this._initialPrompt = prompt; + ReflectApply(Interface.prototype.setPrompt, this, [prompt]); +}; + +const importRE = /\bimport\s*\(\s*['"`](([\w@./:-]+\/)?(?:[\w@./:-]*))(?![^'"`])$/; +const requireRE = /\brequire\s*\(\s*['"`](([\w@./:-]+\/)?(?:[\w@./:-]*))(?![^'"`])$/; +const fsAutoCompleteRE = /fs(?:\.promises)?\.\s*[a-z][a-zA-Z]+\(\s*["'](.*)/; +const simpleExpressionRE = + /(?:[\w$'"`[{(](?:\w|\$|['"`\]})])*\??\.)*[a-zA-Z_$](?:\w|\$)*\??\.?$/; +const versionedFileNamesRe = /-\d+\.\d+/; + +function isIdentifier(str) { + if (str === '') { + return false; + } + const first = StringPrototypeCodePointAt(str, 0); + if (!isIdentifierStart(first)) { + return false; + } + const firstLen = first > 0xffff ? 2 : 1; + for (let i = firstLen; i < str.length; i += 1) { + const cp = StringPrototypeCodePointAt(str, i); + if (!isIdentifierChar(cp)) { + return false; + } + if (cp > 0xffff) { + i += 1; + } + } + return true; +} + +function isNotLegacyObjectPrototypeMethod(str) { + return isIdentifier(str) && + str !== '__defineGetter__' && + str !== '__defineSetter__' && + str !== '__lookupGetter__' && + str !== '__lookupSetter__'; +} + +function filteredOwnPropertyNames(obj) { + if (!obj) return []; + // `Object.prototype` is the only non-contrived object that fulfills + // `Object.getPrototypeOf(X) === null && + // Object.getPrototypeOf(Object.getPrototypeOf(X.constructor)) === X`. + let isObjectPrototype = false; + if (ObjectGetPrototypeOf(obj) === null) { + const ctorDescriptor = ObjectGetOwnPropertyDescriptor(obj, 'constructor'); + if (ctorDescriptor && ctorDescriptor.value) { + const ctorProto = ObjectGetPrototypeOf(ctorDescriptor.value); + isObjectPrototype = ctorProto && ObjectGetPrototypeOf(ctorProto) === obj; + } + } + const filter = ALL_PROPERTIES | SKIP_SYMBOLS; + return ArrayPrototypeFilter( + getOwnNonIndexProperties(obj, filter), + isObjectPrototype ? isNotLegacyObjectPrototypeMethod : isIdentifier); +} + +function getGlobalLexicalScopeNames(contextId) { + return sendInspectorCommand((session) => { + let names = []; + session.post('Runtime.globalLexicalScopeNames', { + executionContextId: contextId, + }, (error, result) => { + if (!error) names = result.names; + }); + return names; + }, () => []); +} + +REPLServer.prototype.complete = function() { + ReflectApply(this.completer, this, arguments); +}; + +function gracefulReaddir(...args) { + try { + return ReflectApply(fs.readdirSync, null, args); + } catch { + // Continue regardless of error. + } +} + +function completeFSFunctions(match) { + let baseName = ''; + let filePath = match[1]; + let fileList = gracefulReaddir(filePath, { withFileTypes: true }); + + if (!fileList) { + baseName = path.basename(filePath); + filePath = path.dirname(filePath); + fileList = gracefulReaddir(filePath, { withFileTypes: true }) || []; + } + + const completions = ArrayPrototypeMap( + ArrayPrototypeFilter( + fileList, + (dirent) => StringPrototypeStartsWith(dirent.name, baseName), + ), + (d) => d.name, + ); + + return [[completions], baseName]; +} + +// Provide a list of completions for the given leading text. This is +// given to the readline interface for handling tab completion. +// +// Example: +// complete('let foo = util.') +// -> [['util.print', 'util.debug', 'util.log', 'util.inspect'], +// 'util.' ] +// +// Warning: This eval's code like "foo.bar.baz", so it will run property +// getter code. +function complete(line, callback) { + // List of completion lists, one for each inheritance "level" + let completionGroups = []; + let completeOn, group; + + // Ignore right whitespace. It could change the outcome. + line = StringPrototypeTrimStart(line); + + let filter = ''; + + let match; + // REPL commands (e.g. ".break"). + if ((match = RegExpPrototypeExec(/^\s*\.(\w*)$/, line)) !== null) { + ArrayPrototypePush(completionGroups, ObjectKeys(this.commands)); + completeOn = match[1]; + if (completeOn.length) { + filter = completeOn; + } + } else if ((match = RegExpPrototypeExec(requireRE, line)) !== null) { + // require('...') + completeOn = match[1]; + filter = completeOn; + if (this.allowBlockingCompletions) { + const subdir = match[2] || ''; + const extensions = ObjectKeys(this.context.require.extensions); + const indexes = ArrayPrototypeMap(extensions, + (extension) => `index${extension}`); + ArrayPrototypePush(indexes, 'package.json', 'index'); + + group = []; + let paths = []; + + if (completeOn === '.') { + group = ['./', '../']; + } else if (completeOn === '..') { + group = ['../']; + } else if (RegExpPrototypeExec(/^\.\.?\//, completeOn) !== null) { + paths = [process.cwd()]; + } else { + paths = []; + ArrayPrototypePushApply(paths, module.paths); + ArrayPrototypePushApply(paths, CJSModule.globalPaths); + } + + ArrayPrototypeForEach(paths, (dir) => { + dir = path.resolve(dir, subdir); + const dirents = gracefulReaddir(dir, { withFileTypes: true }) || []; + ArrayPrototypeForEach(dirents, (dirent) => { + if (RegExpPrototypeExec(versionedFileNamesRe, dirent.name) !== null || + dirent.name === '.npm') { + // Exclude versioned names that 'npm' installs. + return; + } + const extension = path.extname(dirent.name); + const base = StringPrototypeSlice(dirent.name, 0, -extension.length); + if (!dirent.isDirectory()) { + if (StringPrototypeIncludes(extensions, extension) && + (!subdir || base !== 'index')) { + ArrayPrototypePush(group, `${subdir}${base}`); + } + return; + } + ArrayPrototypePush(group, `${subdir}${dirent.name}/`); + const absolute = path.resolve(dir, dirent.name); + if (ArrayPrototypeSome( + gracefulReaddir(absolute) || [], + (subfile) => ArrayPrototypeIncludes(indexes, subfile), + )) { + ArrayPrototypePush(group, `${subdir}${dirent.name}`); + } + }); + }); + if (group.length) { + ArrayPrototypePush(completionGroups, group); + } + } + + ArrayPrototypePush(completionGroups, _builtinLibs, nodeSchemeBuiltinLibs); + } else if ((match = RegExpPrototypeExec(importRE, line)) !== null) { + // import('...') + completeOn = match[1]; + filter = completeOn; + if (this.allowBlockingCompletions) { + const subdir = match[2] || ''; + // File extensions that can be imported: + const extensions = ObjectKeys(extensionFormatMap); + + // Only used when loading bare module specifiers from `node_modules`: + const indexes = ArrayPrototypeMap(extensions, (ext) => `index${ext}`); + ArrayPrototypePush(indexes, 'package.json'); + + group = []; + let paths = []; + if (completeOn === '.') { + group = ['./', '../']; + } else if (completeOn === '..') { + group = ['../']; + } else if (RegExpPrototypeExec(/^\.\.?\//, completeOn) !== null) { + paths = [process.cwd()]; + } else { + paths = ArrayPrototypeSlice(module.paths); + } + + ArrayPrototypeForEach(paths, (dir) => { + dir = path.resolve(dir, subdir); + const isInNodeModules = path.basename(dir) === 'node_modules'; + const dirents = gracefulReaddir(dir, { withFileTypes: true }) || []; + ArrayPrototypeForEach(dirents, (dirent) => { + const { name } = dirent; + if (RegExpPrototypeExec(versionedFileNamesRe, name) !== null || + name === '.npm') { + // Exclude versioned names that 'npm' installs. + return; + } + + if (!dirent.isDirectory()) { + const extension = path.extname(name); + if (StringPrototypeIncludes(extensions, extension)) { + ArrayPrototypePush(group, `${subdir}${name}`); + } + return; + } + + ArrayPrototypePush(group, `${subdir}${name}/`); + if (!subdir && isInNodeModules) { + const absolute = path.resolve(dir, name); + const subfiles = gracefulReaddir(absolute) || []; + if (ArrayPrototypeSome(subfiles, (subfile) => { + return ArrayPrototypeIncludes(indexes, subfile); + })) { + ArrayPrototypePush(group, `${subdir}${name}`); + } + } + }); + }); + + if (group.length) { + ArrayPrototypePush(completionGroups, group); + } + } + + ArrayPrototypePush(completionGroups, _builtinLibs, nodeSchemeBuiltinLibs); + } else if ((match = RegExpPrototypeExec(fsAutoCompleteRE, line)) !== null && + this.allowBlockingCompletions) { + ({ 0: completionGroups, 1: completeOn } = completeFSFunctions(match)); + // Handle variable member lookup. + // We support simple chained expressions like the following (no function + // calls, etc.). That is for simplicity and also because we *eval* that + // leading expression so for safety (see WARNING above) don't want to + // eval function calls. + // + // foo.bar<|> # completions for 'foo' with filter 'bar' + // spam.eggs.<|> # completions for 'spam.eggs' with filter '' + // foo<|> # all scope vars with filter 'foo' + // foo.<|> # completions for 'foo' with filter '' + } else if (line.length === 0 || + RegExpPrototypeExec(/\w|\.|\$/, line[line.length - 1]) !== null) { + const { 0: match } = RegExpPrototypeExec(simpleExpressionRE, line) || ['']; + if (line.length !== 0 && !match) { + completionGroupsLoaded(); + return; + } + let expr = ''; + completeOn = match; + if (StringPrototypeEndsWith(line, '.')) { + expr = StringPrototypeSlice(match, 0, -1); + } else if (line.length !== 0) { + const bits = StringPrototypeSplit(match, '.'); + filter = ArrayPrototypePop(bits); + expr = ArrayPrototypeJoin(bits, '.'); + } + + // Resolve expr and get its completions. + if (!expr) { + // Get global vars synchronously + ArrayPrototypePush(completionGroups, + getGlobalLexicalScopeNames(this[kContextId])); + let contextProto = this.context; + while ((contextProto = ObjectGetPrototypeOf(contextProto)) !== null) { + ArrayPrototypePush(completionGroups, + filteredOwnPropertyNames(contextProto)); + } + const contextOwnNames = filteredOwnPropertyNames(this.context); + if (!this.useGlobal) { + // When the context is not `global`, builtins are not own + // properties of it. + // `globalBuiltins` is a `SafeSet`, not an Array-like. + ArrayPrototypePush(contextOwnNames, ...globalBuiltins); + } + ArrayPrototypePush(completionGroups, contextOwnNames); + if (filter !== '') addCommonWords(completionGroups); + completionGroupsLoaded(); + return; + } + + let chaining = '.'; + if (StringPrototypeEndsWith(expr, '?')) { + expr = StringPrototypeSlice(expr, 0, -1); + chaining = '?.'; + } + + const memberGroups = []; + const evalExpr = `try { ${expr} } catch {}`; + this.eval(evalExpr, this.context, getREPLResourceName(), (e, obj) => { + try { + let p; + if ((typeof obj === 'object' && obj !== null) || + typeof obj === 'function') { + memberGroups.push(filteredOwnPropertyNames(obj)); + p = ObjectGetPrototypeOf(obj); + } else { + p = obj.constructor ? obj.constructor.prototype : null; + } + // Circular refs possible? Let's guard against that. + let sentinel = 5; + while (p !== null && sentinel-- !== 0) { + memberGroups.push(filteredOwnPropertyNames(p)); + p = ObjectGetPrototypeOf(p); + } + } catch { + // Maybe a Proxy object without `getOwnPropertyNames` trap. + // We simply ignore it here, as we don't want to break the + // autocompletion. Fixes the bug + // https://github.com/nodejs/node/issues/2119 + } + + if (memberGroups.length) { + expr += chaining; + ArrayPrototypeForEach(memberGroups, (group) => { + ArrayPrototypePush(completionGroups, + ArrayPrototypeMap(group, + (member) => `${expr}${member}`)); + }); + if (filter) { + filter = `${expr}${filter}`; + } + } + + completionGroupsLoaded(); + }); + return; + } + + return completionGroupsLoaded(); + + // Will be called when all completionGroups are in place + // Useful for async autocompletion + function completionGroupsLoaded() { + // Filter, sort (within each group), uniq and merge the completion groups. + if (completionGroups.length && filter) { + const newCompletionGroups = []; + const lowerCaseFilter = StringPrototypeToLocaleLowerCase(filter); + ArrayPrototypeForEach(completionGroups, (group) => { + const filteredGroup = ArrayPrototypeFilter(group, (str) => { + // Filter is always case-insensitive following chromium autocomplete + // behavior. + return StringPrototypeStartsWith( + StringPrototypeToLocaleLowerCase(str), + lowerCaseFilter, + ); + }); + if (filteredGroup.length) { + ArrayPrototypePush(newCompletionGroups, filteredGroup); + } + }); + completionGroups = newCompletionGroups; + } + + const completions = []; + // Unique completions across all groups. + const uniqueSet = new SafeSet(); + uniqueSet.add(''); + // Completion group 0 is the "closest" (least far up the inheritance + // chain) so we put its completions last: to be closest in the REPL. + ArrayPrototypeForEach(completionGroups, (group) => { + ArrayPrototypeSort(group, (a, b) => (b > a ? 1 : -1)); + const setSize = uniqueSet.size; + ArrayPrototypeForEach(group, (entry) => { + if (!uniqueSet.has(entry)) { + ArrayPrototypeUnshift(completions, entry); + uniqueSet.add(entry); + } + }); + // Add a separator between groups. + if (uniqueSet.size !== setSize) { + ArrayPrototypeUnshift(completions, ''); + } + }); + + // Remove obsolete group entry, if present. + if (completions[0] === '') { + ArrayPrototypeShift(completions); + } + + callback(null, [completions, completeOn]); + } +} + +REPLServer.prototype.completeOnEditorMode = (callback) => (err, results) => { + if (err) return callback(err); + + const { 0: completions, 1: completeOn = '' } = results; + let result = ArrayPrototypeFilter(completions, Boolean); + + if (completeOn && result.length !== 0) { + result = [commonPrefix(result)]; + } + + callback(null, [result, completeOn]); +}; + +REPLServer.prototype.defineCommand = function(keyword, cmd) { + if (typeof cmd === 'function') { + cmd = { action: cmd }; + } else { + validateFunction(cmd.action, 'cmd.action'); + } + this.commands[keyword] = cmd; +}; + +// TODO(BridgeAR): This should be replaced with acorn to build an AST. The +// language became more complex and using a simple approach like this is not +// sufficient anymore. +function _memory(cmd) { + const self = this; + self.lines = self.lines || []; + self.lines.level = self.lines.level || []; + + // Save the line so I can do magic later + if (cmd) { + const len = self.lines.level.length ? self.lines.level.length - 1 : 0; + ArrayPrototypePush(self.lines, StringPrototypeRepeat(' ', len) + cmd); + } else { + // I don't want to not change the format too much... + ArrayPrototypePush(self.lines, ''); + } + + if (!cmd) { + self.lines.level = []; + return; + } + + // I need to know "depth." + // Because I can not tell the difference between a } that + // closes an object literal and a } that closes a function + const countMatches = (regex, str) => { + let count = 0; + while (RegExpPrototypeExec(regex, str) !== null) count++; + return count; + }; + + // Going down is { and ( e.g. function() { + // going up is } and ) + const dw = countMatches(/[{(]/g, cmd); + const up = countMatches(/[})]/g, cmd); + let depth = dw.length - up.length; + + if (depth) { + (function workIt() { + if (depth > 0) { + // Going... down. + // Push the line#, depth count, and if the line is a function. + // Since JS only has functional scope I only need to remove + // "function() {" lines, clearly this will not work for + // "function() + // {" but nothing should break, only tab completion for local + // scope will not work for this function. + ArrayPrototypePush(self.lines.level, { + line: self.lines.length - 1, + depth: depth, + }); + } else if (depth < 0) { + // Going... up. + const curr = ArrayPrototypePop(self.lines.level); + if (curr) { + const tmp = curr.depth + depth; + if (tmp < 0) { + // More to go, recurse + depth += curr.depth; + workIt(); + } else if (tmp > 0) { + // Remove and push back + curr.depth += depth; + ArrayPrototypePush(self.lines.level, curr); + } + } + } + }()); + } +} + +function addCommonWords(completionGroups) { + // Only words which do not yet exist as global property should be added to + // this list. + ArrayPrototypePush(completionGroups, [ + 'async', 'await', 'break', 'case', 'catch', 'const', 'continue', + 'debugger', 'default', 'delete', 'do', 'else', 'export', 'false', + 'finally', 'for', 'function', 'if', 'import', 'in', 'instanceof', 'let', + 'new', 'null', 'return', 'switch', 'this', 'throw', 'true', 'try', + 'typeof', 'var', 'void', 'while', 'with', 'yield', + ]); +} + +function _turnOnEditorMode(repl) { + repl.editorMode = true; + ReflectApply(Interface.prototype.setPrompt, repl, ['']); +} + +function _turnOffEditorMode(repl) { + repl.editorMode = false; + repl.setPrompt(repl._initialPrompt); +} + +function defineDefaultCommands(repl) { + repl.defineCommand('break', { + help: 'Sometimes you get stuck, this gets you out', + action: function() { + this.clearBufferedCommand(); + this.displayPrompt(); + }, + }); + + let clearMessage; + if (repl.useGlobal) { + clearMessage = 'Alias for .break'; + } else { + clearMessage = 'Break, and also clear the local context'; + } + repl.defineCommand('clear', { + help: clearMessage, + action: function() { + this.clearBufferedCommand(); + if (!this.useGlobal) { + this.output.write('Clearing context...\n'); + this.resetContext(); + } + this.displayPrompt(); + }, + }); + + repl.defineCommand('exit', { + help: 'Exit the REPL', + action: function() { + this.close(); + }, + }); + + repl.defineCommand('help', { + help: 'Print this help message', + action: function() { + const names = ArrayPrototypeSort(ObjectKeys(this.commands)); + const longestNameLength = MathMaxApply( + ArrayPrototypeMap(names, (name) => name.length), + ); + ArrayPrototypeForEach(names, (name) => { + const cmd = this.commands[name]; + const spaces = + StringPrototypeRepeat(' ', longestNameLength - name.length + 3); + const line = `.${name}${cmd.help ? spaces + cmd.help : ''}\n`; + this.output.write(line); + }); + this.output.write('\nPress Ctrl+C to abort current expression, ' + + 'Ctrl+D to exit the REPL\n'); + this.displayPrompt(); + }, + }); + + repl.defineCommand('save', { + help: 'Save all evaluated commands in this REPL session to a file', + action: function(file) { + try { + if (file === '') { + throw new ERR_MISSING_ARGS('file'); + } + fs.writeFileSync(file, ArrayPrototypeJoin(this.lines, '\n')); + this.output.write(`Session saved to: ${file}\n`); + } catch (error) { + if (error instanceof ERR_MISSING_ARGS) { + this.output.write(`${error.message}\n`); + } else { + this.output.write(`Failed to save: ${file}\n`); + } + } + this.displayPrompt(); + }, + }); + + repl.defineCommand('load', { + help: 'Load JS from a file into the REPL session', + action: function(file) { + try { + if (file === '') { + throw new ERR_MISSING_ARGS('file'); + } + const stats = fs.statSync(file); + if (stats && stats.isFile()) { + _turnOnEditorMode(this); + this[kLoadingSymbol] = true; + const data = fs.readFileSync(file, 'utf8'); + this.write(data); + this[kLoadingSymbol] = false; + _turnOffEditorMode(this); + this.write('\n'); + } else { + this.output.write( + `Failed to load: ${file} is not a valid file\n`, + ); + } + } catch (error) { + if (error instanceof ERR_MISSING_ARGS) { + this.output.write(`${error.message}\n`); + } else { + this.output.write(`Failed to load: ${file}\n`); + } + } + this.displayPrompt(); + }, + }); + if (repl.terminal) { + repl.defineCommand('editor', { + help: 'Enter editor mode', + action() { + _turnOnEditorMode(this); + this.output.write( + '// Entering editor mode (Ctrl+D to finish, Ctrl+C to cancel)\n'); + }, + }); + } +} + +function Recoverable(err) { + this.err = err; +} +ObjectSetPrototypeOf(Recoverable.prototype, SyntaxErrorPrototype); +ObjectSetPrototypeOf(Recoverable, SyntaxError); + +module.exports = { + start, + writer, + REPLServer, + REPL_MODE_SLOPPY, + REPL_MODE_STRICT, + Recoverable, +}; + +ObjectDefineProperty(module.exports, 'builtinModules', { + __proto__: null, + get: () => _builtinLibs, + set: (val) => _builtinLibs = val, + enumerable: true, + configurable: true, +}); + +ObjectDefineProperty(module.exports, '_builtinLibs', { + __proto__: null, + get: pendingDeprecation ? deprecate( + () => _builtinLibs, + 'repl._builtinLibs is deprecated. Check module.builtinModules instead', + 'DEP0142', + ) : () => _builtinLibs, + set: pendingDeprecation ? deprecate( + (val) => _builtinLibs = val, + 'repl._builtinLibs is deprecated. Check module.builtinModules instead', + 'DEP0142', + ) : (val) => _builtinLibs = val, + enumerable: false, + configurable: true, +}); diff --git a/lib/internal/repl/utils.js b/lib/internal/repl/stable/utils.js similarity index 100% rename from lib/internal/repl/utils.js rename to lib/internal/repl/stable/utils.js diff --git a/lib/repl.js b/lib/repl.js index a06ca0dd990f29..a28636f19c2cd9 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -1,1888 +1,11 @@ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -/* A REPL library that you can include in your own code to get a runtime - * interface to your program. - * - * const repl = require("repl"); - * // start repl on stdin - * repl.start("prompt> "); - * - * // listen for unix socket connections and start repl on them - * net.createServer(function(socket) { - * repl.start("node via Unix socket> ", socket); - * }).listen("/tmp/node-repl-sock"); - * - * // listen for TCP socket connections and start repl on them - * net.createServer(function(socket) { - * repl.start("node via TCP socket> ", socket); - * }).listen(5001); - * - * // expose foo to repl context - * repl.start("node > ").context.foo = "stdin is fun"; - */ - 'use strict'; +const StableREPL = require('internal/repl/stable/index'); +const ExperimentalREPL = require('internal/repl/experimental/index'); const { - ArrayPrototypeAt, - ArrayPrototypeFilter, - ArrayPrototypeFindLastIndex, - ArrayPrototypeForEach, - ArrayPrototypeIncludes, - ArrayPrototypeJoin, - ArrayPrototypeMap, - ArrayPrototypePop, - ArrayPrototypePush, - ArrayPrototypePushApply, - ArrayPrototypeShift, - ArrayPrototypeSlice, - ArrayPrototypeSome, - ArrayPrototypeSort, - ArrayPrototypeUnshift, - Boolean, - Error: MainContextError, - FunctionPrototypeBind, - JSONStringify, - MathMaxApply, - NumberIsNaN, - NumberParseFloat, - ObjectAssign, - ObjectDefineProperty, - ObjectGetOwnPropertyDescriptor, - ObjectGetOwnPropertyNames, - ObjectGetPrototypeOf, - ObjectKeys, - ObjectSetPrototypeOf, - Promise, - ReflectApply, - RegExp, - RegExpPrototypeExec, - SafePromiseRace, - SafeSet, - SafeWeakSet, - StringPrototypeCharAt, - StringPrototypeCodePointAt, - StringPrototypeEndsWith, - StringPrototypeIncludes, - StringPrototypeRepeat, - StringPrototypeSlice, - StringPrototypeSplit, - StringPrototypeStartsWith, - StringPrototypeTrim, - StringPrototypeTrimStart, - StringPrototypeToLocaleLowerCase, - Symbol, - SyntaxError, - SyntaxErrorPrototype, - globalThis, -} = primordials; - -const { BuiltinModule } = require('internal/bootstrap/realm'); -const { - makeRequireFunction, - addBuiltinLibsToObject, -} = require('internal/modules/helpers'); -const { - isIdentifierStart, - isIdentifierChar, - parse: acornParse, -} = require('internal/deps/acorn/acorn/dist/acorn'); -const acornWalk = require('internal/deps/acorn/acorn-walk/dist/walk'); -const { - decorateErrorStack, - isError, - deprecate, - SideEffectFreeRegExpPrototypeSymbolReplace, - SideEffectFreeRegExpPrototypeSymbolSplit, -} = require('internal/util'); -const { inspect } = require('internal/util/inspect'); -const vm = require('vm'); - -const { runInThisContext, runInContext } = vm.Script.prototype; - -const path = require('path'); -const fs = require('fs'); -const { Interface } = require('readline'); -const { - commonPrefix, -} = require('internal/readline/utils'); -const { Console } = require('console'); -const { shouldColorize } = require('internal/util/colors'); -const CJSModule = require('internal/modules/cjs/loader').Module; -let _builtinLibs = ArrayPrototypeFilter( - CJSModule.builtinModules, - (e) => !StringPrototypeStartsWith(e, '_'), -); -const nodeSchemeBuiltinLibs = ArrayPrototypeMap( - _builtinLibs, (lib) => `node:${lib}`); -ArrayPrototypeForEach( - BuiltinModule.getSchemeOnlyModuleNames(), - (lib) => ArrayPrototypePush(nodeSchemeBuiltinLibs, `node:${lib}`), -); -const domain = require('domain'); -let debug = require('internal/util/debuglog').debuglog('repl', (fn) => { - debug = fn; -}); -const { - codes: { - ERR_CANNOT_WATCH_SIGINT, - ERR_INVALID_REPL_EVAL_CONFIG, - ERR_INVALID_REPL_INPUT, - ERR_MISSING_ARGS, - ERR_SCRIPT_EXECUTION_INTERRUPTED, - }, - isErrorStackTraceLimitWritable, - overrideStackTrace, - ErrorPrepareStackTrace, -} = require('internal/errors'); -const { sendInspectorCommand } = require('internal/util/inspector'); -const { getOptionValue } = require('internal/options'); -const { - validateFunction, - validateObject, -} = require('internal/validators'); -const experimentalREPLAwait = getOptionValue( - '--experimental-repl-await', -); -const pendingDeprecation = getOptionValue('--pending-deprecation'); -const { - REPL_MODE_SLOPPY, - REPL_MODE_STRICT, - isRecoverableError, - kStandaloneREPL, - setupPreview, - setupReverseSearch, -} = require('internal/repl/utils'); -const { - constants: { - ALL_PROPERTIES, - SKIP_SYMBOLS, - }, - getOwnNonIndexProperties, -} = internalBinding('util'); -const { - startSigintWatchdog, - stopSigintWatchdog, -} = internalBinding('contextify'); - -const history = require('internal/repl/history'); -const { - extensionFormatMap, -} = require('internal/modules/esm/formats'); -const { - makeContextifyScript, -} = require('internal/vm'); -let nextREPLResourceNumber = 1; -// This prevents v8 code cache from getting confused and using a different -// cache from a resource of the same name -function getREPLResourceName() { - return `REPL${nextREPLResourceNumber++}`; -} - -// Lazy-loaded. -let processTopLevelAwait; - -const globalBuiltins = - new SafeSet(vm.runInNewContext('Object.getOwnPropertyNames(globalThis)')); - -const parentModule = module; -const domainSet = new SafeWeakSet(); - -const kBufferedCommandSymbol = Symbol('bufferedCommand'); -const kContextId = Symbol('contextId'); -const kLoadingSymbol = Symbol('loading'); - -let addedNewListener = false; - -try { - // Hack for require.resolve("./relative") to work properly. - module.filename = path.resolve('repl'); -} catch { - // 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 = CJSModule._nodeModulePaths(module.filename); - -// This is the default "writer" value, if none is passed in the REPL options, -// and it can be overridden by custom print functions, such as `probe` or -// `eyes.js`. -const writer = (obj) => inspect(obj, writer.options); -writer.options = { ...inspect.defaultOptions, showProxy: true }; - -// Converts static import statement to dynamic import statement -const toDynamicImport = (codeLine) => { - let dynamicImportStatement = ''; - const ast = acornParse(codeLine, { __proto__: null, sourceType: 'module', ecmaVersion: 'latest' }); - acornWalk.ancestor(ast, { - ImportDeclaration(node) { - const awaitDynamicImport = `await import(${JSONStringify(node.source.value)});`; - if (node.specifiers.length === 0) { - dynamicImportStatement += awaitDynamicImport; - } else if (node.specifiers.length === 1 && node.specifiers[0].type === 'ImportNamespaceSpecifier') { - dynamicImportStatement += `const ${node.specifiers[0].local.name} = ${awaitDynamicImport}`; - } else { - const importNames = ArrayPrototypeJoin(ArrayPrototypeMap(node.specifiers, ({ local, imported }) => - (local.name === imported?.name ? local.name : `${imported?.name ?? 'default'}: ${local.name}`), - ), ', '); - dynamicImportStatement += `const { ${importNames} } = ${awaitDynamicImport}`; - } - }, - }); - return dynamicImportStatement; -}; - -function REPLServer(prompt, - stream, - eval_, - useGlobal, - ignoreUndefined, - replMode) { - if (!(this instanceof REPLServer)) { - return new REPLServer(prompt, - stream, - eval_, - useGlobal, - ignoreUndefined, - replMode); - } - - let options; - if (prompt !== null && typeof prompt === 'object') { - // An options object was given. - options = { ...prompt }; - stream = options.stream || options.socket; - eval_ = options.eval; - useGlobal = options.useGlobal; - ignoreUndefined = options.ignoreUndefined; - prompt = options.prompt; - replMode = options.replMode; - } else { - options = {}; - } - - if (!options.input && !options.output) { - // Legacy API, passing a 'stream'/'socket' option. - if (!stream) { - // Use stdin and stdout as the default streams if none were given. - stream = process; - } - // We're given a duplex readable/writable Stream, like a `net.Socket` - // or a custom object with 2 streams, or the `process` object. - options.input = stream.stdin || stream; - options.output = stream.stdout || stream; - } - - if (options.terminal === undefined) { - options.terminal = options.output.isTTY; - } - options.terminal = !!options.terminal; - - if (options.terminal && options.useColors === undefined) { - // If possible, check if stdout supports colors or not. - options.useColors = shouldColorize(options.output); - } - - // TODO(devsnek): Add a test case for custom eval functions. - const preview = options.terminal && - (options.preview !== undefined ? !!options.preview : !eval_); - - ObjectDefineProperty(this, 'inputStream', { - __proto__: null, - get: pendingDeprecation ? - deprecate(() => this.input, - 'repl.inputStream and repl.outputStream are deprecated. ' + - 'Use repl.input and repl.output instead', - 'DEP0141') : - () => this.input, - set: pendingDeprecation ? - deprecate((val) => this.input = val, - 'repl.inputStream and repl.outputStream are deprecated. ' + - 'Use repl.input and repl.output instead', - 'DEP0141') : - (val) => this.input = val, - enumerable: false, - configurable: true, - }); - ObjectDefineProperty(this, 'outputStream', { - __proto__: null, - get: pendingDeprecation ? - deprecate(() => this.output, - 'repl.inputStream and repl.outputStream are deprecated. ' + - 'Use repl.input and repl.output instead', - 'DEP0141') : - () => this.output, - set: pendingDeprecation ? - deprecate((val) => this.output = val, - 'repl.inputStream and repl.outputStream are deprecated. ' + - 'Use repl.input and repl.output instead', - 'DEP0141') : - (val) => this.output = val, - enumerable: false, - configurable: true, - }); - - this.allowBlockingCompletions = !!options.allowBlockingCompletions; - this.useColors = !!options.useColors; - this._domain = options.domain || domain.create(); - this.useGlobal = !!useGlobal; - this.ignoreUndefined = !!ignoreUndefined; - this.replMode = replMode || module.exports.REPL_MODE_SLOPPY; - this.underscoreAssigned = false; - this.last = undefined; - this.underscoreErrAssigned = false; - this.lastError = undefined; - this.breakEvalOnSigint = !!options.breakEvalOnSigint; - this.editorMode = false; - // Context id for use with the inspector protocol. - this[kContextId] = undefined; - - if (this.breakEvalOnSigint && eval_) { - // Allowing this would not reflect user expectations. - // breakEvalOnSigint affects only the behavior of the default eval(). - throw new ERR_INVALID_REPL_EVAL_CONFIG(); - } - - if (options[kStandaloneREPL]) { - // It is possible to introspect the running REPL accessing this variable - // from inside the REPL. This is useful for anyone working on the REPL. - module.exports.repl = this; - } else if (!addedNewListener) { - // Add this listener only once and use a WeakSet that contains the REPLs - // domains. Otherwise we'd have to add a single listener to each REPL - // instance and that could trigger the `MaxListenersExceededWarning`. - process.prependListener('newListener', (event, listener) => { - if (event === 'uncaughtException' && - process.domain && - listener.name !== 'domainUncaughtExceptionClear' && - domainSet.has(process.domain)) { - // Throw an error so that the event will not be added and the current - // domain takes over. That way the user is notified about the error - // and the current code evaluation is stopped, just as any other code - // that contains an error. - throw new ERR_INVALID_REPL_INPUT( - 'Listeners for `uncaughtException` cannot be used in the REPL'); - } - }); - addedNewListener = true; - } - - domainSet.add(this._domain); - - const savedRegExMatches = ['', '', '', '', '', '', '', '', '', '']; - const sep = '\u0000\u0000\u0000'; - const regExMatcher = new RegExp(`^${sep}(.*)${sep}(.*)${sep}(.*)${sep}(.*)` + - `${sep}(.*)${sep}(.*)${sep}(.*)${sep}(.*)` + - `${sep}(.*)$`); - - eval_ = eval_ || defaultEval; - - const self = this; - - // Pause taking in new input, and store the keys in a buffer. - const pausedBuffer = []; - let paused = false; - function pause() { - paused = true; - } - - function unpause() { - if (!paused) return; - paused = false; - let entry; - const tmpCompletionEnabled = self.isCompletionEnabled; - while ((entry = ArrayPrototypeShift(pausedBuffer)) !== undefined) { - const { 0: type, 1: payload, 2: isCompletionEnabled } = entry; - switch (type) { - case 'key': { - const { 0: d, 1: key } = payload; - self.isCompletionEnabled = isCompletionEnabled; - self._ttyWrite(d, key); - break; - } - case 'close': - self.emit('exit'); - break; - } - if (paused) { - break; - } - } - self.isCompletionEnabled = tmpCompletionEnabled; - } - - function defaultEval(code, context, file, cb) { - let result, script, wrappedErr; - let err = null; - let wrappedCmd = false; - let awaitPromise = false; - const input = code; - - // It's confusing for `{ a : 1 }` to be interpreted as a block - // statement rather than an object literal. So, we first try - // to wrap it in parentheses, so that it will be interpreted as - // an expression. Note that if the above condition changes, - // lib/internal/repl/utils.js needs to be changed to match. - if (RegExpPrototypeExec(/^\s*{/, code) !== null && - RegExpPrototypeExec(/;\s*$/, code) === null) { - code = `(${StringPrototypeTrim(code)})\n`; - wrappedCmd = true; - } - - const hostDefinedOptionId = Symbol(`eval:${file}`); - let parentURL; - try { - const { pathToFileURL } = require('internal/url'); - // Adding `/repl` prevents dynamic imports from loading relative - // to the parent of `process.cwd()`. - parentURL = pathToFileURL(path.join(process.cwd(), 'repl')).href; - } catch { - // Continue regardless of error. - } - async function importModuleDynamically(specifier, _, importAttributes) { - const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader(); - return cascadedLoader.import(specifier, parentURL, importAttributes); - } - // `experimentalREPLAwait` is set to true by default. - // Shall be false in case `--no-experimental-repl-await` flag is used. - if (experimentalREPLAwait && StringPrototypeIncludes(code, 'await')) { - if (processTopLevelAwait === undefined) { - ({ processTopLevelAwait } = require('internal/repl/await')); - } - - try { - const potentialWrappedCode = processTopLevelAwait(code); - if (potentialWrappedCode !== null) { - code = potentialWrappedCode; - wrappedCmd = true; - awaitPromise = true; - } - } catch (e) { - let recoverableError = false; - if (e.name === 'SyntaxError') { - // Remove all "await"s and attempt running the script - // in order to detect if error is truly non recoverable - const fallbackCode = SideEffectFreeRegExpPrototypeSymbolReplace(/\bawait\b/g, code, ''); - try { - makeContextifyScript( - fallbackCode, // code - file, // filename, - 0, // lineOffset - 0, // columnOffset, - undefined, // cachedData - false, // produceCachedData - undefined, // parsingContext - hostDefinedOptionId, // hostDefinedOptionId - importModuleDynamically, // importModuleDynamically - ); - } catch (fallbackError) { - if (isRecoverableError(fallbackError, fallbackCode)) { - recoverableError = true; - err = new Recoverable(e); - } - } - } - if (!recoverableError) { - decorateErrorStack(e); - err = e; - } - } - } - - // First, create the Script object to check the syntax - if (code === '\n') - return cb(null); - - if (err === null) { - while (true) { - try { - if (self.replMode === module.exports.REPL_MODE_STRICT && - RegExpPrototypeExec(/^\s*$/, code) === null) { - // "void 0" keeps the repl from returning "use strict" as the result - // value for statements and declarations that don't return a value. - code = `'use strict'; void 0;\n${code}`; - } - script = makeContextifyScript( - code, // code - file, // filename, - 0, // lineOffset - 0, // columnOffset, - undefined, // cachedData - false, // produceCachedData - undefined, // parsingContext - hostDefinedOptionId, // hostDefinedOptionId - importModuleDynamically, // importModuleDynamically - ); - } catch (e) { - debug('parse error %j', code, e); - if (wrappedCmd) { - // Unwrap and try again - wrappedCmd = false; - awaitPromise = false; - code = input; - wrappedErr = e; - continue; - } - // Preserve original error for wrapped command - const error = wrappedErr || e; - if (isRecoverableError(error, code)) - err = new Recoverable(error); - else - err = error; - } - break; - } - } - - // This will set the values from `savedRegExMatches` to corresponding - // predefined RegExp properties `RegExp.$1`, `RegExp.$2` ... `RegExp.$9` - RegExpPrototypeExec(regExMatcher, - ArrayPrototypeJoin(savedRegExMatches, sep)); - - let finished = false; - function finishExecution(err, result) { - if (finished) return; - finished = true; - - // After executing the current expression, store the values of RegExp - // predefined properties back in `savedRegExMatches` - for (let idx = 1; idx < savedRegExMatches.length; idx += 1) { - savedRegExMatches[idx] = RegExp[`$${idx}`]; - } - - cb(err, result); - } - - if (!err) { - // Unset raw mode during evaluation so that Ctrl+C raises a signal. - let previouslyInRawMode; - if (self.breakEvalOnSigint) { - // Start the SIGINT watchdog before entering raw mode so that a very - // quick Ctrl+C doesn't lead to aborting the process completely. - if (!startSigintWatchdog()) - throw new ERR_CANNOT_WATCH_SIGINT(); - previouslyInRawMode = self._setRawMode(false); - } - - try { - try { - const scriptOptions = { - displayErrors: false, - breakOnSigint: self.breakEvalOnSigint, - }; - - if (self.useGlobal) { - result = ReflectApply(runInThisContext, script, [scriptOptions]); - } else { - result = ReflectApply(runInContext, script, [context, scriptOptions]); - } - } finally { - if (self.breakEvalOnSigint) { - // Reset terminal mode to its previous value. - self._setRawMode(previouslyInRawMode); - - // Returns true if there were pending SIGINTs *after* the script - // has terminated without being interrupted itself. - if (stopSigintWatchdog()) { - self.emit('SIGINT'); - } - } - } - } catch (e) { - err = e; - - if (process.domain) { - debug('not recoverable, send to domain'); - process.domain.emit('error', err); - process.domain.exit(); - return; - } - } - - if (awaitPromise && !err) { - let sigintListener; - pause(); - let promise = result; - if (self.breakEvalOnSigint) { - const interrupt = new Promise((resolve, reject) => { - sigintListener = () => { - const tmp = MainContextError.stackTraceLimit; - if (isErrorStackTraceLimitWritable()) MainContextError.stackTraceLimit = 0; - const err = new ERR_SCRIPT_EXECUTION_INTERRUPTED(); - if (isErrorStackTraceLimitWritable()) MainContextError.stackTraceLimit = tmp; - reject(err); - }; - prioritizedSigintQueue.add(sigintListener); - }); - promise = SafePromiseRace([promise, interrupt]); - } - - (async () => { - try { - const result = (await promise)?.value; - finishExecution(null, result); - } catch (err) { - if (err && process.domain) { - debug('not recoverable, send to domain'); - process.domain.emit('error', err); - process.domain.exit(); - return; - } - finishExecution(err); - } finally { - // Remove prioritized SIGINT listener if it was not called. - prioritizedSigintQueue.delete(sigintListener); - unpause(); - } - })(); - } - } - - if (!awaitPromise || err) { - finishExecution(err, result); - } - } - - self.eval = self._domain.bind(eval_); - - self._domain.on('error', function debugDomainError(e) { - debug('domain error'); - let errStack = ''; - - if (typeof e === 'object' && e !== null) { - overrideStackTrace.set(e, (error, stackFrames) => { - let frames; - if (typeof stackFrames === 'object') { - // Search from the bottom of the call stack to - // find the first frame with a null function name - const idx = ArrayPrototypeFindLastIndex( - stackFrames, - (frame) => frame.getFunctionName() === null, - ); - // If found, get rid of it and everything below it - frames = ArrayPrototypeSlice(stackFrames, 0, idx); - } else { - frames = stackFrames; - } - // FIXME(devsnek): this is inconsistent with the checks - // that the real prepareStackTrace dispatch uses in - // lib/internal/errors.js. - if (typeof MainContextError.prepareStackTrace === 'function') { - return MainContextError.prepareStackTrace(error, frames); - } - return ErrorPrepareStackTrace(error, frames); - }); - decorateErrorStack(e); - - if (e.domainThrown) { - delete e.domain; - delete e.domainThrown; - } - - if (isError(e)) { - if (e.stack) { - if (e.name === 'SyntaxError') { - // Remove stack trace. - e.stack = SideEffectFreeRegExpPrototypeSymbolReplace( - /^\s+at\s.*\n?/gm, - SideEffectFreeRegExpPrototypeSymbolReplace(/^REPL\d+:\d+\r?\n/, e.stack, ''), - ''); - const importErrorStr = 'Cannot use import statement outside a ' + - 'module'; - if (StringPrototypeIncludes(e.message, importErrorStr)) { - e.message = 'Cannot use import statement inside the Node.js ' + - 'REPL, alternatively use dynamic import: ' + toDynamicImport(ArrayPrototypeAt(self.lines, -1)); - e.stack = SideEffectFreeRegExpPrototypeSymbolReplace( - /SyntaxError:.*\n/, - e.stack, - `SyntaxError: ${e.message}\n`); - } - } else if (self.replMode === module.exports.REPL_MODE_STRICT) { - e.stack = SideEffectFreeRegExpPrototypeSymbolReplace( - /(\s+at\s+REPL\d+:)(\d+)/, - e.stack, - (_, pre, line) => pre + (line - 1), - ); - } - } - errStack = self.writer(e); - - // Remove one line error braces to keep the old style in place. - if (errStack[0] === '[' && errStack[errStack.length - 1] === ']') { - errStack = StringPrototypeSlice(errStack, 1, -1); - } - } - } - - if (!self.underscoreErrAssigned) { - self.lastError = e; - } - - if (options[kStandaloneREPL] && - process.listenerCount('uncaughtException') !== 0) { - process.nextTick(() => { - process.emit('uncaughtException', e); - self.clearBufferedCommand(); - self.lines.level = []; - self.displayPrompt(); - }); - } else { - if (errStack === '') { - errStack = self.writer(e); - } - const lines = SideEffectFreeRegExpPrototypeSymbolSplit(/(?<=\n)/, errStack); - let matched = false; - - errStack = ''; - ArrayPrototypeForEach(lines, (line) => { - if (!matched && - RegExpPrototypeExec(/^\[?([A-Z][a-z0-9_]*)*Error/, line) !== null) { - errStack += writer.options.breakLength >= line.length ? - `Uncaught ${line}` : - `Uncaught:\n${line}`; - matched = true; - } else { - errStack += line; - } - }); - if (!matched) { - const ln = lines.length === 1 ? ' ' : ':\n'; - errStack = `Uncaught${ln}${errStack}`; - } - // Normalize line endings. - errStack += StringPrototypeEndsWith(errStack, '\n') ? '' : '\n'; - self.output.write(errStack); - self.clearBufferedCommand(); - self.lines.level = []; - self.displayPrompt(); - } - }); - - self.clearBufferedCommand(); - - function completer(text, cb) { - ReflectApply(complete, self, - [text, self.editorMode ? self.completeOnEditorMode(cb) : cb]); - } - - ReflectApply(Interface, this, [{ - input: options.input, - output: options.output, - completer: options.completer || completer, - terminal: options.terminal, - historySize: options.historySize, - prompt, - }]); - - self.resetContext(); - - this.commands = { __proto__: null }; - defineDefaultCommands(this); - - // Figure out which "writer" function to use - self.writer = options.writer || module.exports.writer; - - if (self.writer === writer) { - // Conditionally turn on ANSI coloring. - writer.options.colors = self.useColors; - - if (options[kStandaloneREPL]) { - ObjectDefineProperty(inspect, 'replDefaults', { - __proto__: null, - get() { - return writer.options; - }, - set(options) { - validateObject(options, 'options'); - return ObjectAssign(writer.options, options); - }, - enumerable: true, - configurable: true, - }); - } - } - - function _parseREPLKeyword(keyword, rest) { - const cmd = this.commands[keyword]; - if (cmd) { - ReflectApply(cmd.action, this, [rest]); - return true; - } - return false; - } - - self.on('close', function emitExit() { - if (paused) { - ArrayPrototypePush(pausedBuffer, ['close']); - return; - } - self.emit('exit'); - }); - - let sawSIGINT = false; - let sawCtrlD = false; - const prioritizedSigintQueue = new SafeSet(); - self.on('SIGINT', function onSigInt() { - if (prioritizedSigintQueue.size > 0) { - for (const task of prioritizedSigintQueue) { - task(); - } - return; - } - - const empty = self.line.length === 0; - self.clearLine(); - _turnOffEditorMode(self); - - const cmd = self[kBufferedCommandSymbol]; - if (!(cmd && cmd.length > 0) && empty) { - if (sawSIGINT) { - self.close(); - sawSIGINT = false; - return; - } - self.output.write( - '(To exit, press Ctrl+C again or Ctrl+D or type .exit)\n', - ); - sawSIGINT = true; - } else { - sawSIGINT = false; - } - - self.clearBufferedCommand(); - self.lines.level = []; - self.displayPrompt(); - }); - - self.on('line', function onLine(cmd) { - debug('line %j', cmd); - cmd = cmd || ''; - sawSIGINT = false; - - if (self.editorMode) { - self[kBufferedCommandSymbol] += cmd + '\n'; - - // code alignment - const matches = self._sawKeyPress && !self[kLoadingSymbol] ? - RegExpPrototypeExec(/^\s+/, cmd) : null; - if (matches) { - const prefix = matches[0]; - self.write(prefix); - self.line = prefix; - self.cursor = prefix.length; - } - ReflectApply(_memory, self, [cmd]); - return; - } - - // Check REPL keywords and empty lines against a trimmed line input. - const trimmedCmd = StringPrototypeTrim(cmd); - - // Check to see if a REPL keyword was used. If it returns true, - // display next prompt and return. - if (trimmedCmd) { - if (StringPrototypeCharAt(trimmedCmd, 0) === '.' && - StringPrototypeCharAt(trimmedCmd, 1) !== '.' && - NumberIsNaN(NumberParseFloat(trimmedCmd))) { - const matches = RegExpPrototypeExec(/^\.([^\s]+)\s*(.*)$/, trimmedCmd); - const keyword = matches && matches[1]; - const rest = matches && matches[2]; - if (ReflectApply(_parseREPLKeyword, self, [keyword, rest]) === true) { - return; - } - if (!self[kBufferedCommandSymbol]) { - self.output.write('Invalid REPL keyword\n'); - finish(null); - return; - } - } - } - - const evalCmd = self[kBufferedCommandSymbol] + cmd + '\n'; - - debug('eval %j', evalCmd); - self.eval(evalCmd, self.context, getREPLResourceName(), finish); - - function finish(e, ret) { - debug('finish', e, ret); - ReflectApply(_memory, self, [cmd]); - - if (e && !self[kBufferedCommandSymbol] && - StringPrototypeStartsWith(StringPrototypeTrim(cmd), 'npm ')) { - self.output.write('npm should be run outside of the ' + - 'Node.js REPL, in your normal shell.\n' + - '(Press Ctrl+D to exit.)\n'); - self.displayPrompt(); - return; - } - - // If error was SyntaxError and not JSON.parse error - if (e) { - if (e instanceof Recoverable && !sawCtrlD) { - // Start buffering data like that: - // { - // ... x: 1 - // ... } - self[kBufferedCommandSymbol] += cmd + '\n'; - self.displayPrompt(); - return; - } - self._domain.emit('error', e.err || e); - } - - // Clear buffer if no SyntaxErrors - self.clearBufferedCommand(); - sawCtrlD = false; - - // If we got any output - print it (if no error) - if (!e && - // When an invalid REPL command is used, error message is printed - // immediately. We don't have to print anything else. So, only when - // the second argument to this function is there, print it. - arguments.length === 2 && - (!self.ignoreUndefined || ret !== undefined)) { - if (!self.underscoreAssigned) { - self.last = ret; - } - self.output.write(self.writer(ret) + '\n'); - } - - // Display prompt again (unless we already did by emitting the 'error' - // event on the domain instance). - if (!e) { - self.displayPrompt(); - } - } - }); - - self.on('SIGCONT', function onSigCont() { - if (self.editorMode) { - self.output.write(`${self._initialPrompt}.editor\n`); - self.output.write( - '// Entering editor mode (Ctrl+D to finish, Ctrl+C to cancel)\n'); - self.output.write(`${self[kBufferedCommandSymbol]}\n`); - self.prompt(true); - } else { - self.displayPrompt(true); - } - }); - - const { reverseSearch } = setupReverseSearch(this); - - const { - clearPreview, - showPreview, - } = setupPreview( - this, - kContextId, - kBufferedCommandSymbol, - preview, - ); - - // Wrap readline tty to enable editor mode and pausing. - const ttyWrite = FunctionPrototypeBind(self._ttyWrite, self); - self._ttyWrite = (d, key) => { - key = key || {}; - if (paused && !(self.breakEvalOnSigint && key.ctrl && key.name === 'c')) { - ArrayPrototypePush(pausedBuffer, - ['key', [d, key], self.isCompletionEnabled]); - return; - } - if (!self.editorMode || !self.terminal) { - // Before exiting, make sure to clear the line. - if (key.ctrl && key.name === 'd' && - self.cursor === 0 && self.line.length === 0) { - self.clearLine(); - } - clearPreview(key); - if (!reverseSearch(d, key)) { - ttyWrite(d, key); - const showCompletionPreview = key.name !== 'escape'; - showPreview(showCompletionPreview); - } - return; - } - - // Editor mode - if (key.ctrl && !key.shift) { - switch (key.name) { - // TODO(BridgeAR): There should not be a special mode necessary for full - // multiline support. - case 'd': // End editor mode - _turnOffEditorMode(self); - sawCtrlD = true; - ttyWrite(d, { name: 'return' }); - break; - case 'n': // Override next history item - case 'p': // Override previous history item - break; - default: - ttyWrite(d, key); - } - } else { - switch (key.name) { - case 'up': // Override previous history item - case 'down': // Override next history item - break; - case 'tab': - // Prevent double tab behavior - self._previousKey = null; - ttyWrite(d, key); - break; - default: - ttyWrite(d, key); - } - } - }; - - self.displayPrompt(); -} -ObjectSetPrototypeOf(REPLServer.prototype, Interface.prototype); -ObjectSetPrototypeOf(REPLServer, Interface); - -// Prompt is a string to print on each line for the prompt, -// source is a stream to use for I/O, defaulting to stdin/stdout. -function start(prompt, source, eval_, useGlobal, ignoreUndefined, replMode) { - return new REPLServer( - prompt, source, eval_, useGlobal, ignoreUndefined, replMode); -} - -REPLServer.prototype.setupHistory = function setupHistory(historyFile, cb) { - history(this, historyFile, cb); -}; - -REPLServer.prototype.clearBufferedCommand = function clearBufferedCommand() { - this[kBufferedCommandSymbol] = ''; -}; - -REPLServer.prototype.close = function close() { - if (this.terminal && this._flushing && !this._closingOnFlush) { - this._closingOnFlush = true; - this.once('flushHistory', () => - ReflectApply(Interface.prototype.close, this, []), - ); - - return; - } - process.nextTick(() => - ReflectApply(Interface.prototype.close, this, []), - ); -}; - -REPLServer.prototype.createContext = function() { - let context; - if (this.useGlobal) { - context = globalThis; - } else { - sendInspectorCommand((session) => { - session.post('Runtime.enable'); - session.once('Runtime.executionContextCreated', ({ params }) => { - this[kContextId] = params.context.id; - }); - context = vm.createContext(); - session.post('Runtime.disable'); - }, () => { - context = vm.createContext(); - }); - ArrayPrototypeForEach(ObjectGetOwnPropertyNames(globalThis), (name) => { - // Only set properties that do not already exist as a global builtin. - if (!globalBuiltins.has(name)) { - ObjectDefineProperty(context, name, - { - __proto__: null, - ...ObjectGetOwnPropertyDescriptor(globalThis, name), - }); - } - }); - context.global = context; - const _console = new Console(this.output); - ObjectDefineProperty(context, 'console', { - __proto__: null, - configurable: true, - writable: true, - value: _console, - }); - } - - const replModule = new CJSModule(''); - replModule.paths = CJSModule._resolveLookupPaths('', parentModule); - - ObjectDefineProperty(context, 'module', { - __proto__: null, - configurable: true, - writable: true, - value: replModule, - }); - ObjectDefineProperty(context, 'require', { - __proto__: null, - configurable: true, - writable: true, - value: makeRequireFunction(replModule), - }); - - addBuiltinLibsToObject(context, ''); - - return context; -}; - -REPLServer.prototype.resetContext = function() { - this.context = this.createContext(); - this.underscoreAssigned = false; - this.underscoreErrAssigned = false; - // TODO(BridgeAR): Deprecate the lines. - this.lines = []; - this.lines.level = []; - - ObjectDefineProperty(this.context, '_', { - __proto__: null, - configurable: true, - get: () => this.last, - set: (value) => { - this.last = value; - if (!this.underscoreAssigned) { - this.underscoreAssigned = true; - this.output.write('Expression assignment to _ now disabled.\n'); - } - }, - }); - - ObjectDefineProperty(this.context, '_error', { - __proto__: null, - configurable: true, - get: () => this.lastError, - set: (value) => { - this.lastError = value; - if (!this.underscoreErrAssigned) { - this.underscoreErrAssigned = true; - this.output.write( - 'Expression assignment to _error now disabled.\n'); - } - }, - }); - - // Allow REPL extensions to extend the new context - this.emit('reset', this.context); -}; - -REPLServer.prototype.displayPrompt = function(preserveCursor) { - let prompt = this._initialPrompt; - if (this[kBufferedCommandSymbol].length) { - prompt = '...'; - const len = this.lines.level.length ? this.lines.level.length - 1 : 0; - const levelInd = StringPrototypeRepeat('..', len); - prompt += levelInd + ' '; - } - - // Do not overwrite `_initialPrompt` here - ReflectApply(Interface.prototype.setPrompt, this, [prompt]); - this.prompt(preserveCursor); -}; - -// When invoked as an API method, overwrite _initialPrompt -REPLServer.prototype.setPrompt = function setPrompt(prompt) { - this._initialPrompt = prompt; - ReflectApply(Interface.prototype.setPrompt, this, [prompt]); -}; - -const importRE = /\bimport\s*\(\s*['"`](([\w@./:-]+\/)?(?:[\w@./:-]*))(?![^'"`])$/; -const requireRE = /\brequire\s*\(\s*['"`](([\w@./:-]+\/)?(?:[\w@./:-]*))(?![^'"`])$/; -const fsAutoCompleteRE = /fs(?:\.promises)?\.\s*[a-z][a-zA-Z]+\(\s*["'](.*)/; -const simpleExpressionRE = - /(?:[\w$'"`[{(](?:\w|\$|['"`\]})])*\??\.)*[a-zA-Z_$](?:\w|\$)*\??\.?$/; -const versionedFileNamesRe = /-\d+\.\d+/; - -function isIdentifier(str) { - if (str === '') { - return false; - } - const first = StringPrototypeCodePointAt(str, 0); - if (!isIdentifierStart(first)) { - return false; - } - const firstLen = first > 0xffff ? 2 : 1; - for (let i = firstLen; i < str.length; i += 1) { - const cp = StringPrototypeCodePointAt(str, i); - if (!isIdentifierChar(cp)) { - return false; - } - if (cp > 0xffff) { - i += 1; - } - } - return true; -} - -function isNotLegacyObjectPrototypeMethod(str) { - return isIdentifier(str) && - str !== '__defineGetter__' && - str !== '__defineSetter__' && - str !== '__lookupGetter__' && - str !== '__lookupSetter__'; -} - -function filteredOwnPropertyNames(obj) { - if (!obj) return []; - // `Object.prototype` is the only non-contrived object that fulfills - // `Object.getPrototypeOf(X) === null && - // Object.getPrototypeOf(Object.getPrototypeOf(X.constructor)) === X`. - let isObjectPrototype = false; - if (ObjectGetPrototypeOf(obj) === null) { - const ctorDescriptor = ObjectGetOwnPropertyDescriptor(obj, 'constructor'); - if (ctorDescriptor && ctorDescriptor.value) { - const ctorProto = ObjectGetPrototypeOf(ctorDescriptor.value); - isObjectPrototype = ctorProto && ObjectGetPrototypeOf(ctorProto) === obj; - } - } - const filter = ALL_PROPERTIES | SKIP_SYMBOLS; - return ArrayPrototypeFilter( - getOwnNonIndexProperties(obj, filter), - isObjectPrototype ? isNotLegacyObjectPrototypeMethod : isIdentifier); -} - -function getGlobalLexicalScopeNames(contextId) { - return sendInspectorCommand((session) => { - let names = []; - session.post('Runtime.globalLexicalScopeNames', { - executionContextId: contextId, - }, (error, result) => { - if (!error) names = result.names; - }); - return names; - }, () => []); -} - -REPLServer.prototype.complete = function() { - ReflectApply(this.completer, this, arguments); -}; - -function gracefulReaddir(...args) { - try { - return ReflectApply(fs.readdirSync, null, args); - } catch { - // Continue regardless of error. - } -} - -function completeFSFunctions(match) { - let baseName = ''; - let filePath = match[1]; - let fileList = gracefulReaddir(filePath, { withFileTypes: true }); - - if (!fileList) { - baseName = path.basename(filePath); - filePath = path.dirname(filePath); - fileList = gracefulReaddir(filePath, { withFileTypes: true }) || []; - } - - const completions = ArrayPrototypeMap( - ArrayPrototypeFilter( - fileList, - (dirent) => StringPrototypeStartsWith(dirent.name, baseName), - ), - (d) => d.name, - ); - - return [[completions], baseName]; -} - -// Provide a list of completions for the given leading text. This is -// given to the readline interface for handling tab completion. -// -// Example: -// complete('let foo = util.') -// -> [['util.print', 'util.debug', 'util.log', 'util.inspect'], -// 'util.' ] -// -// Warning: This eval's code like "foo.bar.baz", so it will run property -// getter code. -function complete(line, callback) { - // List of completion lists, one for each inheritance "level" - let completionGroups = []; - let completeOn, group; - - // Ignore right whitespace. It could change the outcome. - line = StringPrototypeTrimStart(line); - - let filter = ''; - - let match; - // REPL commands (e.g. ".break"). - if ((match = RegExpPrototypeExec(/^\s*\.(\w*)$/, line)) !== null) { - ArrayPrototypePush(completionGroups, ObjectKeys(this.commands)); - completeOn = match[1]; - if (completeOn.length) { - filter = completeOn; - } - } else if ((match = RegExpPrototypeExec(requireRE, line)) !== null) { - // require('...') - completeOn = match[1]; - filter = completeOn; - if (this.allowBlockingCompletions) { - const subdir = match[2] || ''; - const extensions = ObjectKeys(this.context.require.extensions); - const indexes = ArrayPrototypeMap(extensions, - (extension) => `index${extension}`); - ArrayPrototypePush(indexes, 'package.json', 'index'); - - group = []; - let paths = []; - - if (completeOn === '.') { - group = ['./', '../']; - } else if (completeOn === '..') { - group = ['../']; - } else if (RegExpPrototypeExec(/^\.\.?\//, completeOn) !== null) { - paths = [process.cwd()]; - } else { - paths = []; - ArrayPrototypePushApply(paths, module.paths); - ArrayPrototypePushApply(paths, CJSModule.globalPaths); - } - - ArrayPrototypeForEach(paths, (dir) => { - dir = path.resolve(dir, subdir); - const dirents = gracefulReaddir(dir, { withFileTypes: true }) || []; - ArrayPrototypeForEach(dirents, (dirent) => { - if (RegExpPrototypeExec(versionedFileNamesRe, dirent.name) !== null || - dirent.name === '.npm') { - // Exclude versioned names that 'npm' installs. - return; - } - const extension = path.extname(dirent.name); - const base = StringPrototypeSlice(dirent.name, 0, -extension.length); - if (!dirent.isDirectory()) { - if (StringPrototypeIncludes(extensions, extension) && - (!subdir || base !== 'index')) { - ArrayPrototypePush(group, `${subdir}${base}`); - } - return; - } - ArrayPrototypePush(group, `${subdir}${dirent.name}/`); - const absolute = path.resolve(dir, dirent.name); - if (ArrayPrototypeSome( - gracefulReaddir(absolute) || [], - (subfile) => ArrayPrototypeIncludes(indexes, subfile), - )) { - ArrayPrototypePush(group, `${subdir}${dirent.name}`); - } - }); - }); - if (group.length) { - ArrayPrototypePush(completionGroups, group); - } - } - - ArrayPrototypePush(completionGroups, _builtinLibs, nodeSchemeBuiltinLibs); - } else if ((match = RegExpPrototypeExec(importRE, line)) !== null) { - // import('...') - completeOn = match[1]; - filter = completeOn; - if (this.allowBlockingCompletions) { - const subdir = match[2] || ''; - // File extensions that can be imported: - const extensions = ObjectKeys(extensionFormatMap); - - // Only used when loading bare module specifiers from `node_modules`: - const indexes = ArrayPrototypeMap(extensions, (ext) => `index${ext}`); - ArrayPrototypePush(indexes, 'package.json'); - - group = []; - let paths = []; - if (completeOn === '.') { - group = ['./', '../']; - } else if (completeOn === '..') { - group = ['../']; - } else if (RegExpPrototypeExec(/^\.\.?\//, completeOn) !== null) { - paths = [process.cwd()]; - } else { - paths = ArrayPrototypeSlice(module.paths); - } - - ArrayPrototypeForEach(paths, (dir) => { - dir = path.resolve(dir, subdir); - const isInNodeModules = path.basename(dir) === 'node_modules'; - const dirents = gracefulReaddir(dir, { withFileTypes: true }) || []; - ArrayPrototypeForEach(dirents, (dirent) => { - const { name } = dirent; - if (RegExpPrototypeExec(versionedFileNamesRe, name) !== null || - name === '.npm') { - // Exclude versioned names that 'npm' installs. - return; - } - - if (!dirent.isDirectory()) { - const extension = path.extname(name); - if (StringPrototypeIncludes(extensions, extension)) { - ArrayPrototypePush(group, `${subdir}${name}`); - } - return; - } - - ArrayPrototypePush(group, `${subdir}${name}/`); - if (!subdir && isInNodeModules) { - const absolute = path.resolve(dir, name); - const subfiles = gracefulReaddir(absolute) || []; - if (ArrayPrototypeSome(subfiles, (subfile) => { - return ArrayPrototypeIncludes(indexes, subfile); - })) { - ArrayPrototypePush(group, `${subdir}${name}`); - } - } - }); - }); - - if (group.length) { - ArrayPrototypePush(completionGroups, group); - } - } - - ArrayPrototypePush(completionGroups, _builtinLibs, nodeSchemeBuiltinLibs); - } else if ((match = RegExpPrototypeExec(fsAutoCompleteRE, line)) !== null && - this.allowBlockingCompletions) { - ({ 0: completionGroups, 1: completeOn } = completeFSFunctions(match)); - // Handle variable member lookup. - // We support simple chained expressions like the following (no function - // calls, etc.). That is for simplicity and also because we *eval* that - // leading expression so for safety (see WARNING above) don't want to - // eval function calls. - // - // foo.bar<|> # completions for 'foo' with filter 'bar' - // spam.eggs.<|> # completions for 'spam.eggs' with filter '' - // foo<|> # all scope vars with filter 'foo' - // foo.<|> # completions for 'foo' with filter '' - } else if (line.length === 0 || - RegExpPrototypeExec(/\w|\.|\$/, line[line.length - 1]) !== null) { - const { 0: match } = RegExpPrototypeExec(simpleExpressionRE, line) || ['']; - if (line.length !== 0 && !match) { - completionGroupsLoaded(); - return; - } - let expr = ''; - completeOn = match; - if (StringPrototypeEndsWith(line, '.')) { - expr = StringPrototypeSlice(match, 0, -1); - } else if (line.length !== 0) { - const bits = StringPrototypeSplit(match, '.'); - filter = ArrayPrototypePop(bits); - expr = ArrayPrototypeJoin(bits, '.'); - } - - // Resolve expr and get its completions. - if (!expr) { - // Get global vars synchronously - ArrayPrototypePush(completionGroups, - getGlobalLexicalScopeNames(this[kContextId])); - let contextProto = this.context; - while ((contextProto = ObjectGetPrototypeOf(contextProto)) !== null) { - ArrayPrototypePush(completionGroups, - filteredOwnPropertyNames(contextProto)); - } - const contextOwnNames = filteredOwnPropertyNames(this.context); - if (!this.useGlobal) { - // When the context is not `global`, builtins are not own - // properties of it. - // `globalBuiltins` is a `SafeSet`, not an Array-like. - ArrayPrototypePush(contextOwnNames, ...globalBuiltins); - } - ArrayPrototypePush(completionGroups, contextOwnNames); - if (filter !== '') addCommonWords(completionGroups); - completionGroupsLoaded(); - return; - } - - let chaining = '.'; - if (StringPrototypeEndsWith(expr, '?')) { - expr = StringPrototypeSlice(expr, 0, -1); - chaining = '?.'; - } - - const memberGroups = []; - const evalExpr = `try { ${expr} } catch {}`; - this.eval(evalExpr, this.context, getREPLResourceName(), (e, obj) => { - try { - let p; - if ((typeof obj === 'object' && obj !== null) || - typeof obj === 'function') { - memberGroups.push(filteredOwnPropertyNames(obj)); - p = ObjectGetPrototypeOf(obj); - } else { - p = obj.constructor ? obj.constructor.prototype : null; - } - // Circular refs possible? Let's guard against that. - let sentinel = 5; - while (p !== null && sentinel-- !== 0) { - memberGroups.push(filteredOwnPropertyNames(p)); - p = ObjectGetPrototypeOf(p); - } - } catch { - // Maybe a Proxy object without `getOwnPropertyNames` trap. - // We simply ignore it here, as we don't want to break the - // autocompletion. Fixes the bug - // https://github.com/nodejs/node/issues/2119 - } - - if (memberGroups.length) { - expr += chaining; - ArrayPrototypeForEach(memberGroups, (group) => { - ArrayPrototypePush(completionGroups, - ArrayPrototypeMap(group, - (member) => `${expr}${member}`)); - }); - if (filter) { - filter = `${expr}${filter}`; - } - } - - completionGroupsLoaded(); - }); - return; - } - - return completionGroupsLoaded(); - - // Will be called when all completionGroups are in place - // Useful for async autocompletion - function completionGroupsLoaded() { - // Filter, sort (within each group), uniq and merge the completion groups. - if (completionGroups.length && filter) { - const newCompletionGroups = []; - const lowerCaseFilter = StringPrototypeToLocaleLowerCase(filter); - ArrayPrototypeForEach(completionGroups, (group) => { - const filteredGroup = ArrayPrototypeFilter(group, (str) => { - // Filter is always case-insensitive following chromium autocomplete - // behavior. - return StringPrototypeStartsWith( - StringPrototypeToLocaleLowerCase(str), - lowerCaseFilter, - ); - }); - if (filteredGroup.length) { - ArrayPrototypePush(newCompletionGroups, filteredGroup); - } - }); - completionGroups = newCompletionGroups; - } - - const completions = []; - // Unique completions across all groups. - const uniqueSet = new SafeSet(); - uniqueSet.add(''); - // Completion group 0 is the "closest" (least far up the inheritance - // chain) so we put its completions last: to be closest in the REPL. - ArrayPrototypeForEach(completionGroups, (group) => { - ArrayPrototypeSort(group, (a, b) => (b > a ? 1 : -1)); - const setSize = uniqueSet.size; - ArrayPrototypeForEach(group, (entry) => { - if (!uniqueSet.has(entry)) { - ArrayPrototypeUnshift(completions, entry); - uniqueSet.add(entry); - } - }); - // Add a separator between groups. - if (uniqueSet.size !== setSize) { - ArrayPrototypeUnshift(completions, ''); - } - }); - - // Remove obsolete group entry, if present. - if (completions[0] === '') { - ArrayPrototypeShift(completions); - } - - callback(null, [completions, completeOn]); - } -} - -REPLServer.prototype.completeOnEditorMode = (callback) => (err, results) => { - if (err) return callback(err); - - const { 0: completions, 1: completeOn = '' } = results; - let result = ArrayPrototypeFilter(completions, Boolean); - - if (completeOn && result.length !== 0) { - result = [commonPrefix(result)]; - } - - callback(null, [result, completeOn]); -}; - -REPLServer.prototype.defineCommand = function(keyword, cmd) { - if (typeof cmd === 'function') { - cmd = { action: cmd }; - } else { - validateFunction(cmd.action, 'cmd.action'); - } - this.commands[keyword] = cmd; -}; - -// TODO(BridgeAR): This should be replaced with acorn to build an AST. The -// language became more complex and using a simple approach like this is not -// sufficient anymore. -function _memory(cmd) { - const self = this; - self.lines = self.lines || []; - self.lines.level = self.lines.level || []; - - // Save the line so I can do magic later - if (cmd) { - const len = self.lines.level.length ? self.lines.level.length - 1 : 0; - ArrayPrototypePush(self.lines, StringPrototypeRepeat(' ', len) + cmd); - } else { - // I don't want to not change the format too much... - ArrayPrototypePush(self.lines, ''); - } - - if (!cmd) { - self.lines.level = []; - return; - } - - // I need to know "depth." - // Because I can not tell the difference between a } that - // closes an object literal and a } that closes a function - const countMatches = (regex, str) => { - let count = 0; - while (RegExpPrototypeExec(regex, str) !== null) count++; - return count; - }; - - // Going down is { and ( e.g. function() { - // going up is } and ) - const dw = countMatches(/[{(]/g, cmd); - const up = countMatches(/[})]/g, cmd); - let depth = dw.length - up.length; - - if (depth) { - (function workIt() { - if (depth > 0) { - // Going... down. - // Push the line#, depth count, and if the line is a function. - // Since JS only has functional scope I only need to remove - // "function() {" lines, clearly this will not work for - // "function() - // {" but nothing should break, only tab completion for local - // scope will not work for this function. - ArrayPrototypePush(self.lines.level, { - line: self.lines.length - 1, - depth: depth, - }); - } else if (depth < 0) { - // Going... up. - const curr = ArrayPrototypePop(self.lines.level); - if (curr) { - const tmp = curr.depth + depth; - if (tmp < 0) { - // More to go, recurse - depth += curr.depth; - workIt(); - } else if (tmp > 0) { - // Remove and push back - curr.depth += depth; - ArrayPrototypePush(self.lines.level, curr); - } - } - } - }()); - } -} - -function addCommonWords(completionGroups) { - // Only words which do not yet exist as global property should be added to - // this list. - ArrayPrototypePush(completionGroups, [ - 'async', 'await', 'break', 'case', 'catch', 'const', 'continue', - 'debugger', 'default', 'delete', 'do', 'else', 'export', 'false', - 'finally', 'for', 'function', 'if', 'import', 'in', 'instanceof', 'let', - 'new', 'null', 'return', 'switch', 'this', 'throw', 'true', 'try', - 'typeof', 'var', 'void', 'while', 'with', 'yield', - ]); -} - -function _turnOnEditorMode(repl) { - repl.editorMode = true; - ReflectApply(Interface.prototype.setPrompt, repl, ['']); -} - -function _turnOffEditorMode(repl) { - repl.editorMode = false; - repl.setPrompt(repl._initialPrompt); -} - -function defineDefaultCommands(repl) { - repl.defineCommand('break', { - help: 'Sometimes you get stuck, this gets you out', - action: function() { - this.clearBufferedCommand(); - this.displayPrompt(); - }, - }); - - let clearMessage; - if (repl.useGlobal) { - clearMessage = 'Alias for .break'; - } else { - clearMessage = 'Break, and also clear the local context'; - } - repl.defineCommand('clear', { - help: clearMessage, - action: function() { - this.clearBufferedCommand(); - if (!this.useGlobal) { - this.output.write('Clearing context...\n'); - this.resetContext(); - } - this.displayPrompt(); - }, - }); - - repl.defineCommand('exit', { - help: 'Exit the REPL', - action: function() { - this.close(); - }, - }); - - repl.defineCommand('help', { - help: 'Print this help message', - action: function() { - const names = ArrayPrototypeSort(ObjectKeys(this.commands)); - const longestNameLength = MathMaxApply( - ArrayPrototypeMap(names, (name) => name.length), - ); - ArrayPrototypeForEach(names, (name) => { - const cmd = this.commands[name]; - const spaces = - StringPrototypeRepeat(' ', longestNameLength - name.length + 3); - const line = `.${name}${cmd.help ? spaces + cmd.help : ''}\n`; - this.output.write(line); - }); - this.output.write('\nPress Ctrl+C to abort current expression, ' + - 'Ctrl+D to exit the REPL\n'); - this.displayPrompt(); - }, - }); - - repl.defineCommand('save', { - help: 'Save all evaluated commands in this REPL session to a file', - action: function(file) { - try { - if (file === '') { - throw new ERR_MISSING_ARGS('file'); - } - fs.writeFileSync(file, ArrayPrototypeJoin(this.lines, '\n')); - this.output.write(`Session saved to: ${file}\n`); - } catch (error) { - if (error instanceof ERR_MISSING_ARGS) { - this.output.write(`${error.message}\n`); - } else { - this.output.write(`Failed to save: ${file}\n`); - } - } - this.displayPrompt(); - }, - }); - - repl.defineCommand('load', { - help: 'Load JS from a file into the REPL session', - action: function(file) { - try { - if (file === '') { - throw new ERR_MISSING_ARGS('file'); - } - const stats = fs.statSync(file); - if (stats && stats.isFile()) { - _turnOnEditorMode(this); - this[kLoadingSymbol] = true; - const data = fs.readFileSync(file, 'utf8'); - this.write(data); - this[kLoadingSymbol] = false; - _turnOffEditorMode(this); - this.write('\n'); - } else { - this.output.write( - `Failed to load: ${file} is not a valid file\n`, - ); - } - } catch (error) { - if (error instanceof ERR_MISSING_ARGS) { - this.output.write(`${error.message}\n`); - } else { - this.output.write(`Failed to load: ${file}\n`); - } - } - this.displayPrompt(); - }, - }); - if (repl.terminal) { - repl.defineCommand('editor', { - help: 'Enter editor mode', - action() { - _turnOnEditorMode(this); - this.output.write( - '// Entering editor mode (Ctrl+D to finish, Ctrl+C to cancel)\n'); - }, - }); - } -} - -function Recoverable(err) { - this.err = err; -} -ObjectSetPrototypeOf(Recoverable.prototype, SyntaxErrorPrototype); -ObjectSetPrototypeOf(Recoverable, SyntaxError); - -module.exports = { - start, - writer, - REPLServer, - REPL_MODE_SLOPPY, - REPL_MODE_STRICT, - Recoverable, -}; + getOptionValue, +} = require('internal/options'); -ObjectDefineProperty(module.exports, 'builtinModules', { - __proto__: null, - get: () => _builtinLibs, - set: (val) => _builtinLibs = val, - enumerable: true, - configurable: true, -}); +const useExperimentalRepl = getOptionValue('--experimental-repl') || false; -ObjectDefineProperty(module.exports, '_builtinLibs', { - __proto__: null, - get: pendingDeprecation ? deprecate( - () => _builtinLibs, - 'repl._builtinLibs is deprecated. Check module.builtinModules instead', - 'DEP0142', - ) : () => _builtinLibs, - set: pendingDeprecation ? deprecate( - (val) => _builtinLibs = val, - 'repl._builtinLibs is deprecated. Check module.builtinModules instead', - 'DEP0142', - ) : (val) => _builtinLibs = val, - enumerable: false, - configurable: true, -}); +module.exports = useExperimentalRepl ? ExperimentalREPL : StableREPL; diff --git a/src/node_options.cc b/src/node_options.cc index 556f9633b656ca..d68b27293dfda6 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -362,6 +362,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { "top-level await in the graph", &EnvironmentOptions::print_required_tla, kAllowedInEnvvar); + AddOption("--experimental-repl", + "enable experimental REPL support", + &EnvironmentOptions::experimental_repl, + kAllowedInEnvvar); AddOption("--experimental-require-module", "Allow loading explicit ES Modules in require().", &EnvironmentOptions::require_module, diff --git a/src/node_options.h b/src/node_options.h index 94b99806c1778f..c52da8a38705d6 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -109,6 +109,7 @@ class EnvironmentOptions : public Options { bool require_module = false; std::string dns_result_order; bool enable_source_maps = false; + bool experimental_repl = false; bool experimental_fetch = true; bool experimental_websocket = true; bool experimental_global_customevent = true; diff --git a/test/parallel/test-experimental-repl.js b/test/parallel/test-experimental-repl.js new file mode 100644 index 00000000000000..bc474ebf6122f8 --- /dev/null +++ b/test/parallel/test-experimental-repl.js @@ -0,0 +1,15 @@ +'use strict'; + +// Flags: --experimental-repl + +require('../common'); +const assert = require('assert'); + +const repl = require('repl'); +const experimentalREPL = new repl(); +experimentalREPL.start(); + +assert.strictEqual(experimentalREPL.evaluate('1 + 1'), 2); +assert.strictEqual(experimentalREPL.evaluate('globalThis.module'), experimentalREPL.context.module); + +process.exit(0);