on master help sign in
Recreate nodejs REPL logic (#78)

* Redo nodejs REPL logic * cleanup a lil' * fix tests(ish) * get everything working + wait to create repl until ready * fixed issue with local variables changing * rm vendor * rm change to makefile * uncomment logic to run go tests * cleanup a lil' * rm debug code * fix nix build hash * run prettier * rm unused import * add DISABLE_GO_TESTS for nix * correct comment * rm gnugrep dep

commit02c7adf7b1b2a32864146a0f338d0a123baed25e authorAllAwesome497 <47748690+AllAwesome497@users.noreply.github.com> dateThu 18 Nov 2021, 18:33 (5 years ago) parentf9bc171
12 files changed+1202−197
build-prybar.nix+3−2 go.mod+2−1 go.sum+4−2 languages/nodejs/repl.js+120−185 prybar_assets/nodejs/module-context-hook.js+446 prybar_assets/nodejs/runCode.js+4 run_tests+6−1 run_tests_language+7−2 tests/nodejs/err.exp+1−1 tests/nodejs/main_module_scoped_to_global.exp+1−2 tests/nodejs/repl_test.go+608 tests/nodejs/run_file.exp−1
build-prybar.nix+3−2
@@ -15,7 +15,7 @@ buildGoModule {
1515
1616 subPackages = [ "languages/${language}" ];
1717
18− vendorSha256 = "094js6lf6xpfn1i3418ifbsab5plci4z243spzryy1aah0yymd70";
18+ vendorSha256 = "1nrycpyjmmhs8cl6bmrajfwczxsv8z20y7a9k7js8p7z60fz7pya";
1919
2020 # This prebiuild hook will setup the compiler flags on demand based on the package
2121 # If a language requires this, it MUST expost a ${pkgName}.pc file in its `lib/pkg-config`
@@ -56,7 +56,8 @@ buildGoModule {
5656
5757 patchShebangs run_no_pty ./tests/${language}/*.exp
5858
59− "${bash}/bin/bash" "./run_tests_language" "${language}" "${expect}/bin"
59+ # Currently the go tests won't work in nix because of something w/ nix's sandboxing.
60+ DISABLE_GO_TESTS=1 "${bash}/bin/bash" "./run_tests_language" "${language}" "${expect}/bin"
6061
6162 runHook postCheck
6263 '';
go.mod+2−1
@@ -6,5 +6,6 @@ require (
66 github.com/chzyer/logex v1.1.10 // indirect
77 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
88 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
9− golang.org/x/sys v0.0.0-20210412220455-f1c623a9e750 // indirect
9+ github.com/creack/pty v1.1.17
10+ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1
1011)
go.sum+4−2
@@ -4,5 +4,7 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5O
44github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
55github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
66github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
7−golang.org/x/sys v0.0.0-20210412220455-f1c623a9e750 h1:ZBu6861dZq7xBnG1bn5SRU0vA8nx42at4+kP07FMTog=
8−golang.org/x/sys v0.0.0-20210412220455-f1c623a9e750/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
7+github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
8+github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
9+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
10+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
languages/nodejs/repl.js+120−185
@@ -1,13 +1,67 @@
1−const util = require('util');
21const repl = require('repl');
32const path = require('path');
4−const fs = require('fs');
5−const vm = require('vm');
63const { isatty } = require('tty');
74const assets_dir =
85 process.env.PRYBAR_ASSETS_DIR || path.join(process.cwd(), 'prybar_assets');
6+/**
7+ * @type {import("../../prybar_assets/nodejs/input-sync")}
8+ */
99const rl = require(path.join(assets_dir, 'nodejs', 'input-sync.js'));
10−const Module = require('module');
10+/**
11+ * @type {import("../../prybar_assets/nodejs/module-context-hook")}
12+ */
13+const { runCode, runModule, getRepl } = require(path.join(
14+ assets_dir,
15+ 'nodejs',
16+ 'module-context-hook.js',
17+));
18+
19+// imports to builtin modules don't get added to require.cache.
20+const prybarFilenames = Object.keys(require.cache);
21+
22+for (const filename of prybarFilenames) {
23+ delete require.cache[filename];
24+}
25+
26+Error.prepareStackTrace = function prepareStackTrace(error, callSites) {
27+ // this logic is sourced from the internal repl error handler
28+
29+ // Search from the bottom of the call stack to
30+ // find the first frame with a null function name
31+ callSites.reverse();
32+
33+ const idx = callSites.findIndex((frame) => frame.getFunctionName() === null);
34+ const domainIndex = callSites.findIndex(
35+ (site) => site.getFileName() === 'domain.js',
36+ );
37+
38+ if (domainIndex !== -1 && domainIndex < idx) {
39+ // If found, get rid of it and everything below it
40+ callSites = callSites.slice(idx);
41+ }
42+
43+ callSites.reverse();
44+
45+ const lowestPrybarFileIndex = callSites.findIndex((site) =>
46+ prybarFilenames.includes(site.getFileName()),
47+ );
48+
49+ callSites = callSites.slice(0, lowestPrybarFileIndex);
50+
51+ if (!(error instanceof Error)) {
52+ const tmp = new Error();
53+
54+ tmp.message = error.message;
55+ tmp.name = error.name;
56+ error = tmp;
57+ }
58+
59+ return callSites.length > 0
60+ ? `${error.toString()}\n at ${callSites.join('\n at ')}`
61+ : error.toString();
62+};
63+
64+const isInterractive = Boolean(process.env.PRYBAR_INTERACTIVE);
1165
1266let r;
1367if (!process.env.PRYBAR_QUIET) {
@@ -23,6 +77,10 @@ function logError(msg) {
2377 } else {
2478 process.stdout.write(msg);
2579 }
80+
81+ if (!msg.endsWith('\n')) {
82+ process.stdout.write('\n');
83+ }
2684}
2785
2886// The nodejs repl operates in raw mode and does some funky stuff to
@@ -61,7 +119,7 @@ function handleError(e) {
61119 logError(e.stack);
62120 } else {
63121 // For some reason needs a newline to flush.
64− logError('Thrown: ' + r.writer(e) + '\n');
122+ logError('Thrown: ' + r.writer(e));
65123 }
66124
67125 if (r) {
@@ -71,207 +129,84 @@ function handleError(e) {
71129 }
72130}
73131
74−const sandbox = {
75− // The console in the context doesn't bind to our stdout
76− console,
77− // Browser stdio polyfills
78− alert: console.log,
79− prompt: (p) => {
80− pauseRepl();
81− clearLine();
132+function start() {
133+ /** @type { repl.REPLServer} */
134+ r =
135+ getRepl() ||
136+ repl.start({
137+ useGlobal: true,
138+ prompt: process.env.PRYBAR_PS1,
139+ });
82140
83− let ret = rl.question(`${p}> `);
141+ // remove the internal error and ours for red etc.
142+ r._domain.removeListener('error', r._domain.listeners('error')[0]);
84143
85− resumeRepl();
144+ r._domain.on('error', handleError);
145+ process.on('uncaughtException', handleError);
146+ return false;
147+}
86148
87− // Display prompt on the next turn.
88− if (r) setImmediate(() => r.displayPrompt());
149+global.alert = console.log;
150+global.prompt = (p) => {
151+ pauseRepl();
152+ clearLine();
89153
90− return ret;
91− },
92− confirm: (q) => {
93− pauseRepl();
94− clearLine();
154+ let ret = rl.question(`${p}> `);
95155
96− const ret = rl.keyInYNStrict(q);
156+ resumeRepl();
97157
98− resumeRepl();
158+ // Display prompt on the next turn.
159+ if (r) setImmediate(() => r.displayPrompt());
99160
100− // Display prompt on the next turn.
101− if (r) setImmediate(() => r.displayPrompt());
102− return ret;
103− },
161+ return ret;
104162};
105163
106−const mainPath = process.env.PRYBAR_FILE
107− ? path.resolve(process.env.PRYBAR_FILE)
108− : null;
164+global.confirm = (q) => {
165+ pauseRepl();
166+ clearLine();
109167
110−if (mainPath) {
111− // Make module resolution stuff work as if we're running
112− // `node mainPath`
113− const module = new Module(mainPath, null);
114− module.id = '.';
115− module.filename = mainPath;
116− module.paths = Module._nodeModulePaths(path.dirname(mainPath));
168+ const ret = rl.keyInYNStrict(q);
117169
118− process.mainModule = module;
170+ resumeRepl();
119171
120− sandbox.module = module;
121− sandbox.require = module.require.bind(module);
122− sandbox.__dirname = path.dirname(mainPath);
123− sandbox.__filename = mainPath;
124−}
125−
126−const context = vm.createContext(sandbox);
127−// VM context doesn't have a built-in "global", so we
128−// have to tell it to point to itself
129−Object.defineProperties(context, {
130− global: {
131− ...Object.getOwnPropertyDescriptor(global, 'global'),
132− value: context,
133− },
134− globalThis: {
135− ...Object.getOwnPropertyDescriptor(global, 'globalThis'),
136− value: context,
137− },
138− GLOBAL: {
139− ...Object.getOwnPropertyDescriptor(global, 'GLOBAL'),
140− get: util.deprecate(
141− () => context,
142− `'GLOBAL' is deprecated, use 'global'`,
143− 'DEP0016',
144− ),
145− set: util.deprecate(
146− (value) => {
147− Object.defineProperty(context, 'GLOBAL', {
148− configurable: true,
149− writable: true,
150− enumerable: true,
151− value: value,
152− });
153− },
154− `'GLOBAL' is deprecated, use 'global'`,
155− 'DEP0016',
156− ),
157− },
158− root: {
159− ...Object.getOwnPropertyDescriptor(global, 'root'),
160− get: util.deprecate(
161− () => context,
162− `'root' is deprecated, use 'global'`,
163− 'DEP0016',
164− ),
165− set: util.deprecate(
166− (value) => {
167− Object.defineProperty(context, 'root', {
168− configurable: true,
169− writable: true,
170− enumerable: true,
171− value: value,
172− });
173− },
174− `'root' is deprecated, use 'global'`,
175− 'DEP0016',
176− ),
177− },
178−});
179−
180−// Fill in all the missing globals in the context.
181−// We have to activate the runtime to actually get
182−// a list of globals available on this context
183−const contextGlobals = vm.runInContext(
184− 'Object.getOwnPropertyNames(global)',
185− context,
186−);
187−const localGlobals = Object.getOwnPropertyNames(global);
188−for (let prop of localGlobals) {
189− if (contextGlobals.includes(prop)) {
190− continue;
191− }
192−
193− Object.defineProperty(
194− context,
195− prop,
196− Object.getOwnPropertyDescriptor(global, prop),
197− );
198−}
199−
200−function startRepl() {
201− r = repl.start({
202− prompt: process.env.PRYBAR_PS1,
203− });
204−
205− const replContext = r.context;
206− r.context = context;
207−
208− if (!mainPath) {
209− // Since we don't have a mainpath, we will use
210− // module resolution that's built into the repl
211− r.context.require = replContext.require;
212− r.context.module = replContext.module;
213− }
214−
215− // remove the internal error and ours for red etc.
216− r._domain.removeListener('error', r._domain.listeners('error')[0]);
217− r._domain.on('error', handleError);
218− process.on('uncaughtException', handleError);
219−}
220−
221−if (mainPath) {
222− const main = fs.readFileSync(mainPath, 'utf-8');
172+ // Display prompt on the next turn.
173+ if (r) setImmediate(() => r.displayPrompt());
174+ return ret;
175+};
223176
224− let script;
177+if (process.env.PRYBAR_CODE) {
225178 try {
226− script = new vm.Script(main, {
227− filename: mainPath,
228− displayErrors: false,
229− });
230− } catch (e) {
231− handleError(e);
179+ runCode(process.env.PRYBAR_CODE, isInterractive);
180+ } catch (err) {
181+ handleError(err);
232182 }
233183
234− if (script) {
235− let res;
236− try {
237− res = script.runInContext(context, {
238− displayErrors: false,
239− });
240− } catch (e) {
241− handleError(e);
242− }
243−
244− module.loaded = true;
245−
246− if (typeof res !== 'undefined') {
247− console.log(util.inspect(res, { colors: true }));
248− }
184+ if (isInterractive) {
185+ process.once('beforeExit', start);
249186 }
250−
251− if (isTTY && process.env.PRYBAR_INTERACTIVE) {
252− console.log(
253− '\u001b[0m\u001b[90mHint: hit control+c anytime to enter REPL.\u001b[0m',
254− );
187+} else if (process.env.PRYBAR_EXP) {
188+ try {
189+ console.log(runCode(process.env.PRYBAR_EXP, false));
190+ } catch (err) {
191+ handleError(err);
255192 }
256−
257− if (process.env.PRYBAR_INTERACTIVE) {
258− process.once('SIGINT', () => startRepl());
259− process.once('beforeExit', () => startRepl());
193+} else if (process.env.PRYBAR_FILE) {
194+ try {
195+ runModule(process.env.PRYBAR_FILE, isInterractive);
196+ } catch (err) {
197+ handleError(err);
260198 }
261−} else {
262− const code = process.env.PRYBAR_CODE || process.env.PRYBAR_EXP;
263199
264− if (code) {
265− const result = vm.runInContext(code, context, {
266− breakOnSigint: isTTY || process.env.PRYBAR_INTERACTIVE,
267− });
268−
269− if (process.env.PRYBAR_EXP) {
270− console.log(result);
200+ if (isInterractive) {
201+ if (isTTY) {
202+ console.log(
203+ '\u001b[0m\u001b[90mHint: hit control+c anytime to enter REPL.\u001b[0m',
204+ );
271205 }
272− }
273206
274− if (process.env.PRYBAR_INTERACTIVE) {
275− startRepl();
207+ process.once('beforeExit', start);
208+ process.once('SIGINT', start);
276209 }
210+} else if (isInterractive) {
211+ start();
277212}
prybar_assets/nodejs/module-context-hook.jsnew file+446
@@ -0,0 +1,446 @@
1+const path = require('path');
2+const BuiltinModule = require('module');
3+const vm = require('vm');
4+const { Session } = require('inspector');
5+const Module =
6+ module.constructor.length > 1 ? module.constructor : BuiltinModule;
7+const nodeRepl = require('repl');
8+
9+const kReplitPrybarInit = Symbol.for('replit.prybar.init');
10+const kReplitPrybarEvalCode = Symbol.for('replit.prybar.eval.code');
11+const kReplitPrybarEvalFile = Symbol.for('replit.prybar.eval.file');
12+
13+/**
14+ * The footer we append at the end of files to run the code we want in the scope of the module its appended to.
15+ *
16+ * The code calls global[kReplitPrybarInit] with a callback to evaluate the code at `global`
17+ */
18+const interactiveFooterSnippet = `
19+global[Symbol.for('${kReplitPrybarInit.description}')](() =>
20+ eval(global[Symbol.for('${kReplitPrybarEvalCode.description}')])
21+);
22+`;
23+
24+const moduleVariables = [
25+ // these are all added as arguments of the function modules are wrapped in.
26+ // function (exports, require, module, __filename, __dirname)
27+ 'exports',
28+ 'require',
29+ 'module',
30+ '__filename',
31+ '__dirname',
32+];
33+
34+/**
35+ * Regex to get anything that looks like it could be a top-level variable.
36+ * we use this on each evaluated line strings to test words / strings that could be variables
37+ * and see if they're in the local scope. This is needed for the preview which `repl.js` shows & tab auto-complete.
38+ *
39+ * This will not match properties of values.
40+ */
41+const variableLikeRegex = /(?<=^|[^a-z_0-9$.])[a-z_$][a-z_$0-9]*/gi;
42+
43+/**
44+ * Gets a wrapped version of the eval function which takes a string argument (rather than using a symbol on global)
45+ * and
46+ *
47+ * This is intended to removve any confusion from callsites which aren't referenced in the code of the file we're
48+ * running; calls to eval from lines which don't exist.
49+ *
50+ * @param {(code: string) => any} evalFn
51+ * @returns A a wrapper of the eval function which omits callsites from our proxy logic from stack traces.
52+ */
53+function wrapEval(evalFn) {
54+ return (str) => {
55+ global[kReplitPrybarEvalCode] = str;
56+
57+ return evalFn();
58+ };
59+}
60+
61+/**
62+ * Wraps a function so that none of the callsites under the function's caller are rendered in the error's stack.
63+ * @template T
64+ * @param {T} fn
65+ * @returns {T} A wrapped
66+ */
67+function withoutStack(fn) {
68+ const wrapper = function (...args) {
69+ const oldPrepareStackTrace = Error.prepareStackTrace;
70+
71+ Error.prepareStackTrace = (error) => {
72+ return oldPrepareStackTrace(error, []);
73+ };
74+
75+ let results;
76+ try {
77+ results = fn.apply(this, args);
78+ } catch (err) {
79+ // there's some getter magic on Error.prototype.stack which prevents it from being rendered until
80+ // its value is requested.
81+ err.stack;
82+
83+ throw err;
84+ } finally {
85+ Error.prepareStackTrace = oldPrepareStackTrace;
86+ }
87+
88+ return results;
89+ };
90+
91+ return wrapper;
92+}
93+
94+function hasOwnProperty(obj, prop) {
95+ return Object.hasOwnProperty.call(obj, prop);
96+}
97+
98+let isPreview = false;
99+
100+/**
101+ * The source of the file which require appended the init hook too, if any.
102+ * @type {string | null}
103+ */
104+let hookedFileSource = null;
105+
106+/**
107+ * The function used to evalutate code in the context of the hooked module.
108+ *
109+ * @type {(code: string)=>any}
110+ */
111+let doEval = null;
112+
113+/**
114+ * Registers a function to gbe called once as `Module._compile`.
115+ * @param {(
116+ * this: typeof module,
117+ * code: string,
118+ * fileName: string,
119+ * builtin: ((code: string, fileName: string) => any),
120+ * mod: any
121+ * ) => any} fn The compile function to be used instead of _compile on its next call.
122+ */
123+function mockCompileOnce(fn) {
124+ const defaultCompiler = Module._extensions['.js'];
125+
126+ Module._extensions['.js'] = (mod, fileName) => {
127+ // Only the first import should have interp appended to it.
128+ Module._extensions['.js'] = defaultCompiler;
129+
130+ const oldCompile = mod._compile;
131+ mod.id = '.';
132+ mod.parent = null;
133+
134+ mod._compile = (code) => {
135+ mod._compile = oldCompile;
136+
137+ return fn(code, fileName, oldCompile.bind(mod), mod);
138+ };
139+
140+ return defaultCompiler(mod, fileName);
141+ };
142+}
143+
144+function initRepl(evalFn) {
145+ // wrap the eval fn so that if it errors, it won't include anything from interp in the shell.
146+ doEval = wrapEval(evalFn);
147+}
148+
149+global[kReplitPrybarInit] = initRepl;
150+
151+/**
152+ * Runs a module; adds a hook for an interactive repl if isInteractive is true.
153+ *
154+ * @param {string} moduleName The name / path of the target module
155+ * @param {boolean} isInteractive Whether the target file should init a repl or not.
156+ */
157+function runModule(moduleName, isInteractive) {
158+ // this will throw if the module doesn't exist.
159+ const absPath = require.resolve(path.resolve(process.cwd(), moduleName));
160+ // the the target module has already been `require`d this will drop it from the require cache;
161+ // otherwise this will do nothing.
162+ delete require.cache[absPath];
163+
164+ if (isInteractive) {
165+ mockCompileOnce((code, fileName, compile) => {
166+ let compiled;
167+
168+ try {
169+ hookedFileSource = code;
170+ compiled = compile(code + interactiveFooterSnippet, fileName);
171+ } catch (err) {
172+ // this error is probably unrelated to our added code
173+ // and should fail to compile again.
174+ // It's important that if this is the case, we didn't include our added code
175+ // otherwise users may see an error on a line of code that doesn't exist in the source.
176+ // (for example, an extra { which wasn't closed will show up on the last line of the file)
177+ compiled = compile(code, fileName);
178+
179+ // If we've gotten here something's up w/ interp, not the module itself.
180+ console.warn(
181+ 'running without interp',
182+ err,
183+ 'please submit a bug report.',
184+ );
185+ }
186+
187+ return compiled;
188+ });
189+ }
190+
191+ require(absPath);
192+}
193+
194+/**
195+ * Executes the specified code in a fake module and return the results.
196+ *
197+ * @param {string} code The code to be executed
198+ * @param {boolean} isInterractive Whether a repl should be started in the same scope as the code or not.
199+ * @returns {any} The results of the evalutated code.
200+ */
201+function runCode(code, isInterractive) {
202+ mockCompileOnce((fileSource, __, compile) => {
203+ const fakeFileName = path.join(process.cwd(), '__replit_exec.js');
204+ global[kReplitPrybarEvalFile] = fakeFileName;
205+
206+ hookedFileSource = code;
207+ delete require.cache[fakeFileName];
208+ // we need to `eval` here rather than directly input the code for
209+ global[kReplitPrybarEvalCode] = isInterractive
210+ ? code + interactiveFooterSnippet
211+ : code;
212+
213+ return compile(
214+ fileSource,
215+ // lie to the `compile` function about the file we're compiling
216+ // so calls to `require` will be resolved from the current directory.
217+ fakeFileName,
218+ );
219+ });
220+
221+ delete require.cache[require.resolve('./runCode')];
222+ return require('./runCode');
223+}
224+
225+/**
226+ * If available, returns a repl in the context of executed code or module.
227+ *
228+ * @returns {nodeRepl.REPLServer | null} A vm context if the repl init function was called; otherwise null.
229+ */
230+function getRepl() {
231+ if (doEval === null) {
232+ return null;
233+ }
234+
235+ /**
236+ * A list of variables referenced from the hooked module. This should include all locals, and
237+ * will also end up including any referenced globals.
238+ */
239+ let locals = {};
240+
241+ const variableMatches = hookedFileSource.match(variableLikeRegex);
242+ if (variableMatches) {
243+ for (const maybeVariable of variableMatches) {
244+ let value;
245+
246+ try {
247+ value = doEval(maybeVariable);
248+
249+ try {
250+ // asserts that the variable isn't a constant
251+ doEval(`${maybeVariable}=${maybeVariable}`);
252+
253+ Reflect.defineProperty(locals, maybeVariable, {
254+ value,
255+ writable: true,
256+ enumerable: true,
257+ configurable: false,
258+ });
259+ } catch (err) {
260+ // can't write to constants
261+ if (err instanceof TypeError) {
262+ Reflect.defineProperty(locals, maybeVariable, {
263+ value,
264+ writeable: false,
265+ enumerable: true,
266+ configurable: false,
267+ });
268+ continue;
269+ }
270+
271+ throw err;
272+ }
273+ } catch (err) {
274+ if (err instanceof ReferenceError || err instanceof SyntaxError) {
275+ continue;
276+ }
277+
278+ throw err;
279+ }
280+ }
281+ }
282+
283+ for (const moduleVariable of moduleVariables) {
284+ if (!hasOwnProperty(locals, moduleVariable)) {
285+ Object.defineProperty(locals, moduleVariable, {
286+ writable: true,
287+ configurable: true,
288+ value: doEval(moduleVariable),
289+ });
290+ }
291+ }
292+
293+ repl = nodeRepl.start({
294+ prompt: process.env.PRYBAR_PS1,
295+ });
296+ let isPreview = false;
297+
298+ const replContext = new Proxy(repl.context, {
299+ // Returns a list of all properties of the proxy (enumerable or not).
300+ // https://tc39.es/ecma262/multipage/ordinary-and-exotic-objects-behaviours.html#sec-proxy-object-internal-methods-and-internal-slots-ownpropertykeys
301+ ownKeys(ctx) {
302+ return Array.from(
303+ new Set(
304+ Reflect.ownKeys(global).concat(
305+ Reflect.ownKeys(locals),
306+ Reflect.ownKeys(ctx),
307+ ),
308+ ),
309+ );
310+ },
311+ // Trap for the [[GetOwnProperty]] (used by the Object.getOwnPropertyDescriptor) function
312+ // Returns the descriptor for any variable.
313+ // https://tc39.es/ecma262/multipage/ordinary-and-exotic-objects-behaviours.html#sec-proxy-object-internal-methods-and-internal-slots-getownproperty-p
314+ getOwnPropertyDescriptor(ctx, variable) {
315+ if (hasOwnProperty(ctx, variable)) {
316+ return Reflect.getOwnPropertyDescriptor(ctx, variable);
317+ }
318+
319+ // If the variable exists locally, prioritize it over global.
320+ if (hasOwnProperty(locals, variable)) {
321+ return {
322+ enumerable: true,
323+ configurable: true,
324+ get() {
325+ return locals[variable];
326+ },
327+ set(value) {
328+ // if the variable is a constant, this will throw
329+ replContext[variable] = value;
330+ },
331+ };
332+ }
333+
334+ if (hasOwnProperty(global, variable)) {
335+ const descriptor = Reflect.getOwnPropertyDescriptor(global);
336+
337+ return {
338+ ...descriptor,
339+ configurable: true,
340+ };
341+ }
342+
343+ return undefined;
344+ },
345+
346+ // Trap for determining if the proxy has a key or not.
347+ // https://tc39.es/ecma262/multipage/ordinary-and-exotic-objects-behaviours.html#sec-proxy-object-internal-methods-and-internal-slots-hasproperty-p
348+ has(ctx, variable) {
349+ return (
350+ hasOwnProperty(locals, variable) ||
351+ hasOwnProperty(global, variable) ||
352+ hasOwnProperty(ctx, variable)
353+ );
354+ },
355+
356+ // Trap for getting a property from the proxy.
357+ // https://tc39.es/ecma262/multipage/ordinary-and-exotic-objects-behaviours.html#sec-proxy-object-internal-methods-and-internal-slots-get-p-receiver
358+ get(ctx, variable) {
359+ if (hasOwnProperty(ctx, variable)) {
360+ return ctx[variable];
361+ }
362+
363+ if (hasOwnProperty(locals, variable)) {
364+ // the inspector treats all calls to ``eval` as having side effects,
365+ // so we can't directly call `eval` if we want preview to work.
366+ if (!isPreview) {
367+ // locals may change since we last checked their values.
368+ // we should make sure to grab the most recent value if we're not getting the preview.
369+ locals[variable] = doEval(variable);
370+ }
371+
372+ return locals[variable];
373+ }
374+
375+ if (hasOwnProperty(global, variable)) {
376+ return global[variable];
377+ }
378+
379+ throw new ReferenceError(`${variable} is not defined`);
380+ },
381+
382+ // trap for setting a value in the proxy.
383+ // https://tc39.es/ecma262/multipage/ordinary-and-exotic-objects-behaviours.html#sec-proxy-object-internal-methods-and-internal-slots-set-p-v-receiver
384+ set: withoutStack((ctx, variable, value) => {
385+ if (hasOwnProperty(locals, variable)) {
386+ // To avoid running into potential name clashes of module variables and
387+ // variables owned by this REPL logic, we'll evaluate a callback which sets the variable
388+ // to its first argument, with an argument that has a different name.
389+
390+ doEval(`_${variable}=>${variable}=_${variable}`)(value);
391+
392+ locals[variable] = value;
393+ } else if (hasOwnProperty(global, variable)) {
394+ global[variable] = value;
395+ } else {
396+ ctx[variable] = value;
397+ }
398+
399+ return true;
400+ }),
401+ });
402+
403+ repl.context = vm.createContext(replContext);
404+
405+ for (const moduleVariable of moduleVariables) {
406+ delete repl.context[moduleVariable];
407+ }
408+
409+ const kContextId = Object.getOwnPropertySymbols(repl).find(
410+ (v) => v.description === 'contextId',
411+ );
412+
413+ repl.context.repl = repl;
414+
415+ // this should be 100% seafe since JS is synchronous.
416+ // we aren't using the repl's original context, but we create the new context directly after
417+ // repl.start creates one, so the ID of our context is the ID of the repl's context id + 1.
418+ const replContextId = ++repl[kContextId];
419+
420+ // The nodejs inspector doesn't properly handle the proxy & scope magic,
421+ // so we need to mock its eval function if we want commodities such as preview and
422+ // tab-completion to work as intended.
423+
424+ const post = Session.prototype.post;
425+ Session.prototype.post = function (method, params, callback) {
426+ if (method !== 'Runtime.evaluate' || params.contextId !== replContextId) {
427+ return post.apply(this, arguments);
428+ }
429+
430+ isPreview = true;
431+ // this is synchronous, despite the callback.
432+ return post.call(this, method, params, (err, preview) => {
433+ isPreview = false;
434+
435+ return callback(err, preview);
436+ });
437+ };
438+
439+ return repl;
440+}
441+
442+module.exports = {
443+ runCode,
444+ runModule,
445+ getRepl,
446+};
prybar_assets/nodejs/runCode.jsnew file+4
@@ -0,0 +1,4 @@
1+require = require("module").createRequire(
2+ global[Symbol.for("replit.prybar.eval.file")]
3+);
4+module.exports = eval(global[Symbol.for("replit.prybar.eval.code")]);
run_tests+6−1
@@ -3,11 +3,16 @@
33stty cols 80
44stty rows 20
55
6−tests=$(find ./tests -printf '%P\n')
6+tests=$(find ./tests ! -name '*.go' -printf '%P\n')
77
88for t in ${tests}; do
99 if [[ $t != *.exp ]]; then
1010 echo "$t:"
11+
12+ if [[ $t == 'nodejs' ]]; then
13+ go test ./tests/nodejs
14+ fi
15+
1116 continue
1217 fi
1318
run_tests_language+7−2
@@ -5,7 +5,7 @@ stty rows 20
55
66language=$1
77expect_path=$2
8−tests=$(find "./tests/${language}" -printf '%P\n')
8+tests=$(find "./tests/${language}" ! -name '*.go' -printf '%P\n')
99
1010for t in ${tests}; do
1111 if [[ $t != *.exp ]]; then
@@ -22,9 +22,14 @@ for t in ${tests}; do
2222 if [[ $? != 0 ]]; then
2323 echo -e "\n========== \"$t\" failed =========="
2424 echo "$test_out"
25− exit 1;
25+ exit 1
2626 fi
2727
2828 echo -ne '\r'
2929 echo -e " \e[32m✓\e[0m $name"
3030done
31+
32+if [[ $language == 'nodejs' && $DISABLE_GO_TESTS != 1 ]]; then
33+ echo running go tests
34+ go test ./tests/nodejs
35+fi
tests/nodejs/err.exp+1−1
@@ -5,6 +5,6 @@ spawn ./prybar-nodejs -i -q
55match_max 100000
66expect -exact "\[1G\[0J--> \[5G"
77send -- "x\r"
8−expect -re "ReferenceError: x is not defined\r"
8+expect -e "\[31mReferenceError: x is not defined\[0m\r"
99send -- ""
1010expect eof
tests/nodejs/main_module_scoped_to_global.exp+1−2
@@ -4,8 +4,7 @@ set timeout -1
44spawn ./prybar-nodejs -i ./test_files/main_module_scoped_to_global.js
55match_max 100000
66expect -re {Node v\d+\.\d+.\d+ on (linux|darwin)}
7−expect -exact "{}\r
8−\[0m\[90mHint: hit control+c anytime to enter REPL.\[0m\r
7+expect -exact "\[0m\[90mHint: hit control+c anytime to enter REPL.\[0m\r
98\[1G\[0J--> \[5G"
109send -- ""
1110expect eof
tests/nodejs/repl_test.gonew file+608
@@ -0,0 +1,608 @@
1+package nodejs
2+
3+import (
4+ "bytes"
5+ "fmt"
6+ "io"
7+ "os"
8+ "os/exec"
9+ "path"
10+ "strings"
11+ "sync"
12+ "testing"
13+ "time"
14+
15+ "github.com/creack/pty"
16+ "golang.org/x/sys/unix"
17+)
18+
19+// There's a good chance parts of this break if we upgrade node.
20+// This is probably just a slight change to escape codes which human eyes won't see.
21+// If it does happen, all you need to do to fix it is:
22+//
23+// 1. open a repl the same way that the test would.
24+// 2. simulate the test. Make sure everything looks like it's suppposed to.
25+// 3. If everything looks OK, it's probably fine to just copy + paste the output from "Received: "
26+// directly into the test's expected output.
27+//
28+// Some of the particular things to check are:
29+// - prompt / tab suggestions (though this isn't currently tested, it hopefully will be in the future)
30+// - preview (should evaluate any expression which can't possibly have any side effects and display a truncated version of it below the prompt)
31+// - results of expressions - every line should be evaulated, with the result of that line being printed to stdout
32+// - any errors should be displayed in red.
33+//
34+// If you're adding new tests, the process is pretty similar:
35+// 1. Verfiy that the test case you have works as expected outside of tests.
36+// 2. Create the test entry, you can leave the output as `tbd` or similar
37+// 3. Double check that the test case works normally, run tests, then copy the received output as the expected output.
38+// 4. Write some comments above that test describing what the expected value looks like (see existing tests for examples)
39+
40+var prybarAssetsPath string
41+
42+func init() {
43+ cwd, err := os.Getwd()
44+ if err != nil {
45+ panic(err)
46+ }
47+
48+ prybarAssetsPath = path.Join(cwd, "../../prybar_assets/")
49+
50+ fmt.Println(prybarAssetsPath)
51+}
52+
53+func getBasicArgs(isQuiet, isInteractive bool) []string {
54+ if isQuiet && isInteractive {
55+ return []string{"-q", "-i"}
56+ } else if isQuiet {
57+ return []string{"-q"}
58+ } else if isInteractive {
59+ return []string{"-i"}
60+ }
61+
62+ return []string{}
63+}
64+
65+func getCodeArgs(
66+ isQuiet,
67+ isInteractive bool,
68+ code string,
69+) []string {
70+ return append(getBasicArgs(isQuiet, isInteractive), "-c", code)
71+}
72+
73+func getFileArgs(t *testing.T, isQuiet, isInteractive bool, fileContent string) []string {
74+ file, err := os.CreateTemp("", "prybar_test_*.js")
75+
76+ if err != nil {
77+ panic(err)
78+ }
79+
80+ t.Cleanup(func() {
81+ os.Remove(file.Name())
82+ })
83+
84+ file.WriteString(fileContent)
85+
86+ return append(getBasicArgs(isQuiet, isInteractive), file.Name())
87+
88+}
89+
90+type testCase struct {
91+ name string
92+ input string
93+ expected string
94+}
95+
96+type testInfo struct {
97+ name string
98+ prefix string
99+ args []string
100+}
101+
102+func trimNewlines(str string) string {
103+
104+ return str[1 : len(str)-1]
105+}
106+
107+// used to format a string in the format of:
108+// ```go
109+//`
110+//<input>
111+//`
112+// into a string with '\\x1b' translated to '\x1b' and without the leading + trailing newlines.
113+// only needed for multiline strings.
114+func unescapeExpectedOutput(str string) string {
115+ str = trimNewlines(str)
116+ str = strings.ReplaceAll(str, "\\r", "\r")
117+ return strings.ReplaceAll(str, "\\x1b", "\x1b")
118+}
119+
120+func escape(str string) string {
121+ return strings.ReplaceAll(str, "\x1b", "\\x1b")
122+}
123+
124+type delayedReader struct {
125+ reader io.Reader
126+ once sync.Once
127+}
128+
129+func (r *delayedReader) Read(buf []byte) (int, error) {
130+ r.once.Do(func() {
131+ time.Sleep(50 * time.Millisecond)
132+ })
133+
134+ return r.reader.Read(buf)
135+}
136+
137+func (tc testCase) run(t *testing.T, info testInfo) {
138+ t.Parallel()
139+ input := bytes.NewBufferString(tc.input)
140+ output := &bytes.Buffer{}
141+
142+ cmd := exec.Command("../../prybar-nodejs", info.args...)
143+
144+ cmd.Env = append(os.Environ(), "PRYBAR_ASSETS_DIR="+prybarAssetsPath)
145+
146+ ppty, tty, err := pty.Open()
147+
148+ if err != nil {
149+ t.Fatal(err)
150+ }
151+
152+ defer tty.Close()
153+ defer ppty.Close()
154+
155+ // ensure that we get a constant size for the pty.
156+ pty.Setsize(ppty, &pty.Winsize{
157+ Cols: 100,
158+ Rows: 100,
159+ })
160+
161+ cmd.Stdin = tty
162+ cmd.Stdout = tty
163+ cmd.Stderr = tty
164+
165+ term, err := unix.IoctlGetTermios(int(tty.Fd()), unix.TCGETS)
166+ if err != nil {
167+ t.Fatal(err)
168+ }
169+
170+ // disable local echo, the repl does this manually and we don't want to echo input twice.
171+ // Without this, the input shows up twice, once when we send it (before the proc starts)
172+ // and again later when the proc reads it from buffered input.
173+ term.Lflag &^= unix.ECHO
174+
175+ if err := unix.IoctlSetTermios(int(tty.Fd()), unix.TCSETS, term); err != nil {
176+ t.Fatal(err)
177+ }
178+
179+ if _, err := io.Copy(ppty, input); err != nil {
180+ panic(err)
181+ }
182+ go io.Copy(output, ppty)
183+
184+ var killLock sync.Mutex
185+ didKill := false
186+
187+ time.AfterFunc(time.Second, func() {
188+ killLock.Lock()
189+ defer killLock.Unlock()
190+ didKill = true
191+ cmd.Process.Kill()
192+ })
193+
194+ if err := cmd.Run(); err != nil {
195+
196+ // the cmd failed to start
197+ if cmd.ProcessState == nil {
198+ t.Fatal(err)
199+ }
200+
201+ exitCode := cmd.ProcessState.ExitCode()
202+
203+ killLock.Lock()
204+ killed := didKill
205+ killLock.Unlock()
206+
207+ // we didn't exit with 0 (ok) or 137 (killed with SIGKILL)
208+ if exitCode != 0 && !killed {
209+ t.Fatal(err)
210+ }
211+ }
212+
213+ // ignore carraige returns
214+ outStr := strings.ReplaceAll(output.String(), "\r", "")
215+ outStr = strings.TrimPrefix(outStr, info.prefix)
216+
217+ if outStr != tc.expected {
218+ t.Log("Received:", escape(outStr))
219+ t.Log("Expected:", escape(tc.expected))
220+
221+ t.Fail()
222+ }
223+}
224+
225+func testPreview(t *testing.T, info testInfo) {
226+ t.Parallel()
227+
228+ for _, tc := range [...]testCase{
229+ {
230+ name: "number",
231+ input: "3",
232+ // (prompt) `3`
233+ expected: "\x1b[1G\x1b[0J--> \x1b[5G3",
234+ },
235+ {
236+ name: "number + number",
237+ input: "5 + 10",
238+ // (prompt) `5 + 10`
239+ // (preview) `15`
240+ expected: unescapeExpectedOutput(`
241+\x1b[1G\x1b[0J--> \x1b[5G5 + 10
242+\x1b[90m15\x1b[39m\x1b[11G\x1b[1A
243+`),
244+ },
245+ {
246+ name: "module variable + number",
247+ input: "a + 3",
248+ // (prompt) `a + 3`
249+ // (preview) `8`
250+ expected: unescapeExpectedOutput(`
251+\x1b[1G\x1b[0J--> \x1b[5Ga + 3
252+\x1b[90m8\x1b[39m\x1b[10G\x1b[1A
253+`),
254+ },
255+ {
256+ name: "non-existant variable",
257+ input: "notAVariable",
258+ // (prompt) `notAVariable`
259+ expected: "\x1b[1G\x1b[0J--> \x1b[5GnotAVariable",
260+ },
261+ {
262+ name: "repl-scoped variable",
263+ input: trimNewlines(`
264+const replScopedVariable = 'Bob'
265+replScopedVariable
266+`),
267+ // (prompt) `const replScopedVariable = 'Bob'`
268+ // (result) `undefined`
269+ // (prompt) `replScopedVariable`
270+ // (preview) `'Bob'`
271+ expected: unescapeExpectedOutput(`
272+\x1b[1G\x1b[0J--> \x1b[5Gconst replScopedVariable = 'Bob'
273+\x1b[90mundefined\x1b[39m
274+\x1b[1G\x1b[0J--> \x1b[5GreplScopedVariable
275+\x1b[90m'Bob'\x1b[39m\x1b[23G\x1b[1A
276+`),
277+ },
278+ {
279+ name: "global variable",
280+ input: "obj",
281+ // (prompt) `obj`
282+ // (preview) `{ abc: 'def' }`
283+ expected: unescapeExpectedOutput(`
284+\x1b[1G\x1b[0J--> \x1b[5Gobj
285+\x1b[90m{ abc: 'def' }\x1b[39m\x1b[8G\x1b[1A
286+`),
287+ },
288+ {
289+ name: "new *",
290+ input: "new MyClass()",
291+ // (prompt) `new MyClass()`
292+ // (preview) `MyClass {}`
293+ expected: unescapeExpectedOutput(`
294+\x1b[1G\x1b[0J--> \x1b[5Gnew MyClass()
295+\x1b[90mMyClass {}\x1b[39m\x1b[18G\x1b[1A
296+`),
297+ },
298+ {
299+ name: "global variable + module variable + repl variable",
300+ input: trimNewlines(`
301+const c = 23
302+a + b + c
303+`),
304+ // (prompt) `const c = 23;`
305+ // (result) `undefined`
306+ // (prompt) `a + b + c`
307+ // (preview) `60`
308+ expected: unescapeExpectedOutput(`
309+\x1b[1G\x1b[0J--> \x1b[5Gconst c = 23
310+\x1b[90mundefined\x1b[39m
311+\x1b[1G\x1b[0J--> \x1b[5Ga + b + c
312+\x1b[90m60\x1b[39m\x1b[14G\x1b[1A
313+`),
314+ },
315+ {
316+ name: "property of global object",
317+ input: "obj.abc",
318+ // (prompt) `obj.abc`
319+ // (preview) `'def'`
320+ expected: unescapeExpectedOutput(`
321+\x1b[1G\x1b[0J--> \x1b[5Gobj.abc
322+\x1b[90m'def'\x1b[39m\x1b[12G\x1b[1A
323+`),
324+ },
325+ {
326+ name: "assignment to global variable",
327+ input: "b = 3",
328+
329+ // (prompt) `b = 3`
330+ expected: "\x1b[1G\x1b[0J--> \x1b[5Gb = 3",
331+ },
332+ {
333+ name: "assignment to module variable",
334+ input: "obj = 23",
335+
336+ // (prompt) `obj = 23`
337+ expected: "\x1b[1G\x1b[0J--> \x1b[5Gobj = 23",
338+ },
339+ {
340+ name: "assignment to constant module variable",
341+ input: "obj = 23",
342+
343+ // (prompt) `obj = 23`
344+ expected: "\x1b[1G\x1b[0J--> \x1b[5Gobj = 23",
345+ },
346+ {
347+ name: "displays the cached value of a local variable",
348+ input: trimNewlines(`
349+setValue(2);
350+value
351+`),
352+ // (prompt) `setValue(2);`
353+ // (result) `undefined`
354+ // (prompt) `value`
355+ // (preview) `1`
356+ expected: unescapeExpectedOutput(`
357+\x1b[1G\x1b[0J--> \x1b[5GsetValue(2);
358+\x1b[90mundefined\x1b[39m
359+\x1b[1G\x1b[0J--> \x1b[5Gvalue
360+\x1b[90m1\x1b[39m\x1b[10G\x1b[1A
361+`),
362+ },
363+ } {
364+ t.Run(tc.name, func(t *testing.T) {
365+ tc.run(t, info)
366+ })
367+ }
368+}
369+
370+func testEvalResults(t *testing.T, info testInfo) {
371+ t.Parallel()
372+
373+ for _, tc := range [...]testCase{
374+ {
375+ name: "number",
376+ input: "3\n",
377+ // (prompt) `3`
378+ // (result) `3`
379+ // (prompt)
380+ expected: unescapeExpectedOutput(`
381+\x1b[1G\x1b[0J--> \x1b[5G3
382+\x1b[33m3\x1b[39m
383+\x1b[1G\x1b[0J--> \x1b[5G
384+`),
385+ },
386+ {
387+ name: "number + number",
388+ input: "5 + 10\n",
389+ // (prompt) `5 + 10`
390+ // (result) `15`
391+ // (prompt)
392+ expected: unescapeExpectedOutput(`
393+\x1b[1G\x1b[0J--> \x1b[5G5 + 10
394+\x1b[33m15\x1b[39m
395+\x1b[1G\x1b[0J--> \x1b[5G
396+`),
397+ },
398+ {
399+ name: "module variable + number",
400+ input: "a + 3\n",
401+ // (prompt) `a + 3`
402+ // (result) `8`
403+ // (prompt)
404+ expected: unescapeExpectedOutput(`
405+\x1b[1G\x1b[0J--> \x1b[5Ga + 3
406+\x1b[33m8\x1b[39m
407+\x1b[1G\x1b[0J--> \x1b[5G
408+`),
409+ },
410+ {
411+ name: "non-existant variable",
412+ input: "notAVariable\n",
413+ // (prompt) `notAVariable`
414+ // (result) (error)
415+ // (prompt)
416+ expected: unescapeExpectedOutput(`
417+\x1b[1G\x1b[0J--> \x1b[5GnotAVariable
418+\x1b[0m\x1b[31mReferenceError: notAVariable is not defined\x1b[0m
419+\x1b[1G\x1b[0J--> \x1b[5G
420+`),
421+ },
422+ {
423+ name: "repl-scoped variable",
424+ input: trimNewlines(`
425+const replScopedVariable = 'Bob'
426+replScopedVariable
427+
428+`),
429+ // (prompt) `const replScopedVariable = 'Bob'`
430+ // (result) `undefined`
431+ // (prompt) `replScopedVariable`
432+ // (result) `'Bob'`
433+ expected: unescapeExpectedOutput(`
434+\x1b[1G\x1b[0J--> \x1b[5Gconst replScopedVariable = 'Bob'
435+\x1b[90mundefined\x1b[39m
436+\x1b[1G\x1b[0J--> \x1b[5GreplScopedVariable
437+\x1b[32m'Bob'\x1b[39m
438+\x1b[1G\x1b[0J--> \x1b[5G
439+`),
440+ },
441+ {
442+ name: "global variable",
443+ input: "obj\n",
444+ // (prompt) `obj`
445+ // (result) `{ abc: 'def' }`
446+ expected: unescapeExpectedOutput(`
447+\x1b[1G\x1b[0J--> \x1b[5Gobj
448+{ abc: \x1b[32m'def'\x1b[39m }
449+\x1b[1G\x1b[0J--> \x1b[5G
450+`),
451+ },
452+ {
453+ name: "new *",
454+ input: "new MyClass()\n",
455+ // (prompt) `new MyClass()`
456+ // (result) `MyClass {}`
457+ expected: unescapeExpectedOutput(`
458+\x1b[1G\x1b[0J--> \x1b[5Gnew MyClass()
459+MyClass {}
460+\x1b[1G\x1b[0J--> \x1b[5G
461+`),
462+ },
463+ {
464+ name: "global variable + module variable + repl variable",
465+ input: trimNewlines(`
466+const c = 23
467+a + b + c
468+
469+`),
470+ // (prompt) `const c = 23;`
471+ // (result) `undefined`
472+ // (prompt) `a + b + c`
473+ // (result) `60`
474+ expected: unescapeExpectedOutput(`
475+\x1b[1G\x1b[0J--> \x1b[5Gconst c = 23
476+\x1b[90mundefined\x1b[39m
477+\x1b[1G\x1b[0J--> \x1b[5Ga + b + c
478+\x1b[33m60\x1b[39m
479+\x1b[1G\x1b[0J--> \x1b[5G
480+`),
481+ },
482+ {
483+ name: "property of global object",
484+ input: "obj.abc\n",
485+ // (prompt) `obj.abc`
486+ // (result) `'def'`
487+ // (prompt)
488+ expected: unescapeExpectedOutput(`
489+\x1b[1G\x1b[0J--> \x1b[5Gobj.abc
490+\x1b[32m'def'\x1b[39m
491+\x1b[1G\x1b[0J--> \x1b[5G
492+`),
493+ },
494+ {
495+ name: "assignment to a local variable",
496+ input: "b = 3\n",
497+ // (prompt) `b = 3`
498+ // (result) `3`
499+ // (prompt)
500+ expected: unescapeExpectedOutput(`
501+\x1b[1G\x1b[0J--> \x1b[5Gb = 3
502+\x1b[33m3\x1b[39m
503+\x1b[1G\x1b[0J--> \x1b[5G
504+`),
505+ },
506+ {
507+ name: "assignment to module variable",
508+ input: "obj = 23\n",
509+
510+ // (prompt) `obj = 23`
511+ // (result) `23`
512+ // (prompt)
513+ expected: unescapeExpectedOutput(`
514+\x1b[1G\x1b[0J--> \x1b[5Gobj = 23
515+\x1b[33m23\x1b[39m
516+\x1b[1G\x1b[0J--> \x1b[5G
517+`),
518+ },
519+ {
520+ name: "direct console.trace",
521+ input: "console.trace('Hello')\n",
522+
523+ // (prompt) `obj = 23`
524+ // (trace) `Trace: Hello`
525+ // (result) `23`
526+ // (prompt)
527+ expected: unescapeExpectedOutput(`
528+\x1b[1G\x1b[0J--> \x1b[5Gconsole.trace('Hello')
529+Trace: Hello
530+\x1b[90mundefined\x1b[39m
531+\x1b[1G\x1b[0J--> \x1b[5G
532+`),
533+ },
534+
535+ {
536+ name: "gets the latest value of a variable",
537+ input: trimNewlines(`
538+setValue(2);
539+value
540+
541+`),
542+ // (prompt) `setValue(2);`
543+ // (result) `undefined`
544+ // (prompt) `value`
545+ // (result) `2`
546+ // (prompt)
547+ expected: unescapeExpectedOutput(`
548+\x1b[1G\x1b[0J--> \x1b[5GsetValue(2);
549+\x1b[90mundefined\x1b[39m
550+\x1b[1G\x1b[0J--> \x1b[5Gvalue
551+\x1b[33m2\x1b[39m
552+\x1b[1G\x1b[0J--> \x1b[5G
553+`),
554+ },
555+ } {
556+ t.Run(tc.name, func(t *testing.T) {
557+ tc.run(t, info)
558+ })
559+
560+ }
561+}
562+
563+func TestREPL(t *testing.T) {
564+ const code = `
565+const str = 'Hello, World!';
566+const a = 5;
567+global.b = 32;
568+let obj = { abc: 'def' };
569+class MyClass {}
570+
571+let value = 1;
572+
573+function setValue(newValue) {
574+ value = 2;
575+}
576+`
577+
578+ for _, info := range [...]testInfo{
579+ {
580+ name: "string",
581+ args: getCodeArgs(true, true, code),
582+ },
583+ {
584+ name: "file",
585+ args: getFileArgs(t, true, true, code),
586+ prefix: "\x1b[0m\x1b[90mHint: hit control+c anytime to enter REPL.\x1b[0m\n",
587+ },
588+ } {
589+ for _, test := range [...]struct {
590+ run func(t *testing.T, info testInfo)
591+ name string
592+ }{
593+ {
594+ name: "result preview",
595+ run: testPreview,
596+ },
597+ {
598+ name: "line result",
599+ run: testEvalResults,
600+ },
601+ } {
602+ t.Run(test.name+"/"+info.name, func(t *testing.T) {
603+ test.run(t, info)
604+ })
605+ }
606+ }
607+
608+}
tests/nodejs/run_file.exp−1
@@ -5,7 +5,6 @@ spawn ./prybar-nodejs -i ./test_files/hello.js
55match_max 100000
66expect -re {Node v\d+\.\d+.\d+ on (linux|darwin)}
77expect -exact "hello\r
8−\[32m'hello'\[39m\r
98\[0m\[90mHint: hit control+c anytime to enter REPL.\[0m\r
109\[1G\[0J--> \[5G"
1110send -- ""