Skip to content

Commit 9e25557

Browse files
Match ts-node REPL behavior of object literals with Node REPL (#1699)
* Implement unparenthesized REPL object literals * Fix property access error inconsistency * Run prettier on src/repl.ts * Add cross-env as dev dependency * Fix issue preventing tests from running on envs with NODE_PATH set * Silence deprecation warning spam on tests * Single quotes caused some cli bugs * Add REPL object literal tests * remove cross-env * Add NODE_PATH check and warning to tests runner See: #1697 (comment) * Run prettier on repl.spec.ts * node nightly tests fail because of unexpected custom ESM loaders warnings so fix that i guess? * fix tests on TS 2.7 * node nightly tests still failed, fix attempt 2 * append instead of overriding NODE_OPTIONS * accept warning-only stderr on nightly test * if check didnt fire * maybe the regex is broken * am i even editing the right lines * make test-cov match test-spec * try checking for nightly again... * tests work! clean them up now, please don't break * Remove node nightly tests warning checks (superseded by #1701) * simplify NODE_PATH check * attempt at running new repl tests in-process for speed * switch tests to run in-process using existing macros * finish changes to run tests in-process Co-authored-by: Andrew Bradley <[email protected]>
1 parent d7670e9 commit 9e25557

File tree

5 files changed

+182
-13
lines changed

5 files changed

+182
-13
lines changed

ava.config.cjs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ module.exports = {
1111
// This avoids passing it to spawned processes under test, which would negatively affect
1212
// their behavior.
1313
FORCE_COLOR: '3',
14+
NODE_PATH: ''
1415
},
1516
require: ['./src/test/remove-env-var-force-color.js'],
1617
timeout: '300s',
@@ -21,7 +22,12 @@ module.exports = {
2122
/*
2223
* Tests *must* install and use our most recent ts-node tarball.
2324
* We must prevent them from accidentally require-ing a different version of
24-
* ts-node, from either node_modules or tests/node_modules
25+
* ts-node, from either node_modules or tests/node_modules.
26+
*
27+
* Another possibility of interference is NODE_PATH environment variable being set,
28+
* and ts-node being installed in any of the paths listed on NODE_PATH, to fix this,
29+
* the NODE_PATH variable must be removed from the environment *BEFORE* running ava.
30+
* An error will be thrown when trying to run tests with NODE_PATH set to paths with ts-node installed.
2531
*/
2632

2733
const { existsSync, rmSync } = require('fs');
@@ -32,7 +38,20 @@ module.exports = {
3238
remove(resolve(__dirname, 'tests/node_modules/ts-node'));
3339

3440
// Prove that we did it correctly
35-
expect(() => {createRequire(resolve(__dirname, 'tests/foo.js')).resolve('ts-node')}).toThrow();
41+
(() => {
42+
let resolved;
43+
try {
44+
resolved = createRequire(resolve(__dirname, 'tests/foo.js')).resolve('ts-node');
45+
} catch(err) {return}
46+
47+
// require.resolve() found ts-node; this should not happen.
48+
let errorMessage = `require.resolve('ts-node') unexpectedly resolved to ${resolved}\n`;
49+
// Check for NODE_PATH interference. See comments above.
50+
if(process.env.NODE_PATH) {
51+
errorMessage += `NODE_PATH environment variable is set. This test suite does not support running with NODE_PATH. Unset it before running the tests.`;
52+
}
53+
throw new Error(errorMessage);
54+
})();
3655

3756
function remove(p) {
3857
// Avoid node deprecation warning triggered by rimraf

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,9 @@
171171
"v8-compile-cache-lib": "^3.0.1",
172172
"yn": "3.1.1"
173173
},
174+
"prettier": {
175+
"singleQuote": true
176+
},
174177
"volta": {
175178
"node": "17.5.0",
176179
"npm": "6.14.15"

src/repl.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -509,17 +509,30 @@ function appendCompileAndEvalInput(options: {
509509
service: Service;
510510
state: EvalState;
511511
input: string;
512+
wrappedErr?: unknown;
512513
/** Enable top-level await but only if the TSNode service allows it. */
513514
enableTopLevelAwait?: boolean;
514515
context: Context | undefined;
515516
}): AppendCompileAndEvalInputResult {
516517
const {
517518
service,
518519
state,
519-
input,
520+
wrappedErr,
520521
enableTopLevelAwait = false,
521522
context,
522523
} = options;
524+
let { input } = options;
525+
526+
// It's confusing for `{ a: 1 }` to be interpreted as a block statement
527+
// rather than an object literal. So, we first try to wrap it in
528+
// parentheses, so that it will be interpreted as an expression.
529+
// Based on https://github.com/nodejs/node/blob/c2e6822153bad023ab7ebd30a6117dcc049e475c/lib/repl.js#L413-L422
530+
let wrappedCmd = false;
531+
if (!wrappedErr && /^\s*{/.test(input) && !/;\s*$/.test(input)) {
532+
input = `(${input.trim()})\n`;
533+
wrappedCmd = true;
534+
}
535+
523536
const lines = state.lines;
524537
const isCompletion = !/\n$/.test(input);
525538
const undo = appendToEvalState(state, input);
@@ -536,6 +549,20 @@ function appendCompileAndEvalInput(options: {
536549
output = service.compile(state.input, state.path, -lines);
537550
} catch (err) {
538551
undo();
552+
553+
if (wrappedCmd) {
554+
if (err instanceof TSError && err.diagnosticCodes[0] === 2339) {
555+
// Ensure consistent and more sane behavior between { a: 1 }['b'] and ({ a: 1 }['b'])
556+
throw err;
557+
}
558+
// Unwrap and try again
559+
return appendCompileAndEvalInput({
560+
...options,
561+
wrappedErr: err,
562+
});
563+
}
564+
565+
if (wrappedErr) throw wrappedErr;
539566
throw err;
540567
}
541568

@@ -689,6 +716,10 @@ const RECOVERY_CODES: Map<number, Set<number> | null> = new Map([
689716
[1005, null], // "')' expected.", "'}' expected."
690717
[1109, null], // "Expression expected."
691718
[1126, null], // "Unexpected end of text."
719+
[
720+
1136, // "Property assignment expected."
721+
new Set([1005]), // happens when typing out an object literal or block scope across multiple lines: '{ foo: 123,'
722+
],
692723
[1160, null], // "Unterminated template literal."
693724
[1161, null], // "Unterminated regular expression literal."
694725
[2355, null], // "A function whose declared type is neither 'void' nor 'any' must return a value."

src/test/helpers.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,8 @@ export function getStream(stream: Readable, waitForPattern?: string | RegExp) {
202202
//#region Reset node environment
203203

204204
const defaultRequireExtensions = captureObjectState(require.extensions);
205-
const defaultProcess = captureObjectState(process);
205+
// Avoid node deprecation warning for accessing _channel
206+
const defaultProcess = captureObjectState(process, ['_channel']);
206207
const defaultModule = captureObjectState(require('module'));
207208
const defaultError = captureObjectState(Error);
208209
const defaultGlobal = captureObjectState(global);
@@ -214,15 +215,20 @@ const defaultGlobal = captureObjectState(global);
214215
* Must also play nice with `nyc`'s environmental mutations.
215216
*/
216217
export function resetNodeEnvironment() {
218+
const sms =
219+
require('@cspotcode/source-map-support') as typeof import('@cspotcode/source-map-support');
217220
// We must uninstall so that it resets its internal state; otherwise it won't know it needs to reinstall in the next test.
218-
require('@cspotcode/source-map-support').uninstall();
221+
sms.uninstall();
222+
// Must remove handlers to avoid a memory leak
223+
sms.resetRetrieveHandlers();
219224

220225
// Modified by ts-node hooks
221226
resetObject(require.extensions, defaultRequireExtensions);
222227

223228
// ts-node attaches a property when it registers an instance
224229
// source-map-support monkey-patches the emit function
225-
resetObject(process, defaultProcess);
230+
// Avoid node deprecation warnings for setting process.config or accessing _channel
231+
resetObject(process, defaultProcess, undefined, ['_channel'], ['config']);
226232

227233
// source-map-support swaps out the prepareStackTrace function
228234
resetObject(Error, defaultError);
@@ -235,11 +241,10 @@ export function resetNodeEnvironment() {
235241
resetObject(global, defaultGlobal, ['__coverage__']);
236242
}
237243

238-
function captureObjectState(object: any) {
244+
function captureObjectState(object: any, avoidGetters: string[] = []) {
239245
const descriptors = Object.getOwnPropertyDescriptors(object);
240246
const values = mapValues(descriptors, (_d, key) => {
241-
// Avoid node deprecation warning for accessing _channel
242-
if (object === process && key === '_channel') return descriptors[key].value;
247+
if (avoidGetters.includes(key)) return descriptors[key].value;
243248
return object[key];
244249
});
245250
return {
@@ -251,7 +256,9 @@ function captureObjectState(object: any) {
251256
function resetObject(
252257
object: any,
253258
state: ReturnType<typeof captureObjectState>,
254-
doNotDeleteTheseKeys: string[] = []
259+
doNotDeleteTheseKeys: string[] = [],
260+
doNotSetTheseKeys: string[] = [],
261+
avoidSetterIfUnchanged: string[] = []
255262
) {
256263
const currentDescriptors = Object.getOwnPropertyDescriptors(object);
257264
for (const key of Object.keys(currentDescriptors)) {
@@ -262,9 +269,8 @@ function resetObject(
262269
// Trigger nyc's setter functions
263270
for (const [key, value] of Object.entries(state.values)) {
264271
try {
265-
// Avoid node deprecation warnings for setting process.config or accessing _channel
266-
if (object === process && key === '_channel') continue;
267-
if (object === process && key === 'config' && object[key] === value)
272+
if (doNotSetTheseKeys.includes(key)) continue;
273+
if (avoidSetterIfUnchanged.includes(key) && object[key] === value)
268274
continue;
269275
object[key] = value;
270276
} catch {}

src/test/repl/repl.spec.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@ import {
1616
} from './helpers';
1717

1818
const test = context(ctxTsNode).context(ctxRepl);
19+
test.runSerially();
1920
test.beforeEach(async (t) => {
2021
t.teardown(() => {
2122
resetNodeEnvironment();
23+
// Useful for debugging memory leaks. Leaving in case I need it again.
24+
// global.gc(); // Requires adding nodeArguments: ['--expose-gc'] to ava config
25+
// console.dir(process.memoryUsage().heapUsed / 1000 / 1000);
2226
});
2327
});
2428

@@ -540,3 +544,109 @@ test.suite('REPL declares types for node built-ins within REPL', (test) => {
540544
expect(stderr).toBe('');
541545
});
542546
});
547+
548+
test.suite('REPL treats object literals and block scopes correctly', (test) => {
549+
test(
550+
'repl should treat { key: 123 } as object literal',
551+
macroReplNoErrorsAndStdoutContains,
552+
'{ key: 123 }',
553+
'{ key: 123 }'
554+
);
555+
test(
556+
'repl should treat ({ key: 123 }) as object literal',
557+
macroReplNoErrorsAndStdoutContains,
558+
'({ key: 123 })',
559+
'{ key: 123 }'
560+
);
561+
test(
562+
'repl should treat ({ let v = 0; v; }) as object literal and error',
563+
macroReplStderrContains,
564+
'({ let v = 0; v; })',
565+
semver.satisfies(ts.version, '2.7')
566+
? 'error TS2304'
567+
: 'No value exists in scope for the shorthand property'
568+
);
569+
test(
570+
'repl should treat { let v = 0; v; } as block scope',
571+
macroReplNoErrorsAndStdoutContains,
572+
'{ let v = 0; v; }',
573+
'0'
574+
);
575+
test.suite('extra', (test) => {
576+
test.skipIf(semver.satisfies(ts.version, '2.7'));
577+
test(
578+
'repl should treat { key: 123 }; as block scope',
579+
macroReplNoErrorsAndStdoutContains,
580+
'{ key: 123 };',
581+
'123'
582+
);
583+
test(
584+
'repl should treat {\\nkey: 123\\n}; as block scope',
585+
macroReplNoErrorsAndStdoutContains,
586+
'{\nkey: 123\n};',
587+
'123'
588+
);
589+
test(
590+
'repl should treat { key: 123 }[] as block scope (edge case)',
591+
macroReplNoErrorsAndStdoutContains,
592+
'{ key: 123 }[]',
593+
'[]'
594+
);
595+
});
596+
test.suite('multiline', (test) => {
597+
test(
598+
'repl should treat {\\nkey: 123\\n} as object literal',
599+
macroReplNoErrorsAndStdoutContains,
600+
'{\nkey: 123\n}',
601+
'{ key: 123 }'
602+
);
603+
test(
604+
'repl should treat ({\\nkey: 123\\n}) as object literal',
605+
macroReplNoErrorsAndStdoutContains,
606+
'({\nkey: 123\n})',
607+
'{ key: 123 }'
608+
);
609+
test(
610+
'repl should treat ({\\nlet v = 0;\\nv;\\n}) as object literal and error',
611+
macroReplStderrContains,
612+
'({\nlet v = 0;\nv;\n})',
613+
semver.satisfies(ts.version, '2.7')
614+
? 'error TS2304'
615+
: 'No value exists in scope for the shorthand property'
616+
);
617+
test(
618+
'repl should treat {\\nlet v = 0;\\nv;\\n} as block scope',
619+
macroReplNoErrorsAndStdoutContains,
620+
'{\nlet v = 0;\nv;\n}',
621+
'0'
622+
);
623+
});
624+
test.suite('property access', (test) => {
625+
test(
626+
'repl should treat { key: 123 }.key as object literal property access',
627+
macroReplNoErrorsAndStdoutContains,
628+
'{ key: 123 }.key',
629+
'123'
630+
);
631+
test(
632+
'repl should treat { key: 123 }["key"] as object literal indexed access',
633+
macroReplNoErrorsAndStdoutContains,
634+
'{ key: 123 }["key"]',
635+
'123'
636+
);
637+
test(
638+
'repl should treat { key: 123 }.foo as object literal non-existent property access',
639+
macroReplStderrContains,
640+
'{ key: 123 }.foo',
641+
"Property 'foo' does not exist on type"
642+
);
643+
test(
644+
'repl should treat { key: 123 }["foo"] as object literal non-existent indexed access',
645+
macroReplStderrContains,
646+
'{ key: 123 }["foo"]',
647+
semver.satisfies(ts.version, '2.7')
648+
? 'error TS7017'
649+
: "Property 'foo' does not exist on type"
650+
);
651+
});
652+
});

0 commit comments

Comments
 (0)