Skip to content

Commit 6b1fd40

Browse files
authored
Fix bug where REPL inputs were compiled as ESM in ESM projects (#1959)
* change repl virtual filename to .cts extension so it compiles as cjs even in ESM packages with module:nodenext * fix test * Fix on TS < 4.5 * fix failing tests hopefully Also split test/repl/helpers into multiple files
1 parent ece352f commit 6b1fd40

File tree

11 files changed

+152
-72
lines changed

11 files changed

+152
-72
lines changed

src/bin.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,8 @@ function phase4(payload: BootstrapState) {
517517
module.paths = (Module as any)._nodeModulePaths(cwd);
518518
}
519519
if (executeRepl) {
520-
const state = new EvalState(join(cwd, REPL_FILENAME));
520+
// correct path is set later
521+
const state = new EvalState('');
521522
replStuff = {
522523
state,
523524
repl: createRepl({
@@ -541,6 +542,10 @@ function phase4(payload: BootstrapState) {
541542
},
542543
});
543544
register(service);
545+
546+
if (replStuff)
547+
replStuff.state.path = join(cwd, REPL_FILENAME(service.ts.version));
548+
544549
if (isInChildProcess)
545550
(
546551
require('./child/child-loader') as typeof import('./child/child-loader')

src/file-extensions.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ const nodeDoesNotUnderstand: readonly string[] = [
5050
'.mts',
5151
];
5252

53+
export function tsSupportsMtsCtsExts(tsVersion: string) {
54+
return versionGteLt(tsVersion, '4.5.0');
55+
}
56+
5357
/**
5458
* [MUST_UPDATE_FOR_NEW_FILE_EXTENSIONS]
5559
* @internal
@@ -60,7 +64,7 @@ export function getExtensions(
6064
tsVersion: string
6165
) {
6266
// TS 4.5 is first version to understand .cts, .mts, .cjs, and .mjs extensions
63-
const tsSupportsMtsCtsExts = versionGteLt(tsVersion, '4.5.0');
67+
const supportMtsCtsExts = tsSupportsMtsCtsExts(tsVersion);
6468

6569
const requiresHigherTypescriptVersion: string[] = [];
6670
if (!tsSupportsMtsCtsExts)
@@ -78,11 +82,11 @@ export function getExtensions(
7882
const compiledJsxUnsorted: string[] = [];
7983

8084
if (config.options.jsx) compiledJsxUnsorted.push('.tsx');
81-
if (tsSupportsMtsCtsExts) compiledJsUnsorted.push('.mts', '.cts');
85+
if (supportMtsCtsExts) compiledJsUnsorted.push('.mts', '.cts');
8286
if (config.options.allowJs) {
8387
compiledJsUnsorted.push('.js');
8488
if (config.options.jsx) compiledJsxUnsorted.push('.jsx');
85-
if (tsSupportsMtsCtsExts) compiledJsUnsorted.push('.mjs', '.cjs');
89+
if (supportMtsCtsExts) compiledJsUnsorted.push('.mjs', '.cjs');
8690
}
8791

8892
const compiledUnsorted = [...compiledJsUnsorted, ...compiledJsxUnsorted];

src/repl.ts

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import * as assert from 'assert';
1515
import type * as tty from 'tty';
1616
import type * as Module from 'module';
1717
import { builtinModules } from 'module';
18+
import { tsSupportsMtsCtsExts } from './file-extensions';
1819

1920
// Lazy-loaded.
2021
let _processTopLevelAwait: (src: string) => string | null;
@@ -43,7 +44,9 @@ export const STDIN_FILENAME = `[stdin].ts`;
4344
/** @internal */
4445
export const STDIN_NAME = `[stdin]`;
4546
/** @internal */
46-
export const REPL_FILENAME = '<repl>.ts';
47+
export function REPL_FILENAME(tsVersion: string) {
48+
return tsSupportsMtsCtsExts(tsVersion) ? '<repl>.cts' : '<repl>.ts';
49+
}
4750
/** @internal */
4851
export const REPL_NAME = '<repl>';
4952

@@ -130,6 +133,11 @@ export interface CreateReplOptions {
130133
ignoreDiagnosticsThatAreAnnoyingInInteractiveRepl?: boolean;
131134
}
132135

136+
interface StartReplInternalOptions extends ReplOptions {
137+
code?: string;
138+
forceToBeModule?: boolean;
139+
}
140+
133141
/**
134142
* Create a ts-node REPL instance.
135143
*
@@ -148,13 +156,21 @@ export interface CreateReplOptions {
148156
*/
149157
export function createRepl(options: CreateReplOptions = {}) {
150158
const { ignoreDiagnosticsThatAreAnnoyingInInteractiveRepl = true } = options;
151-
let service = options.service;
152159
let nodeReplServer: REPLServer;
153160
// If `useGlobal` is not true, then REPL creates a context when started.
154161
// This stores a reference to it or to `global`, whichever is used, after REPL has started.
155162
let context: Context | undefined;
156-
const state =
157-
options.state ?? new EvalState(join(process.cwd(), REPL_FILENAME));
163+
let state: EvalState;
164+
let mustSetStatePath = false;
165+
if (options.state) {
166+
state = options.state;
167+
} else {
168+
// Correct path set later
169+
state = new EvalState('');
170+
mustSetStatePath = true;
171+
}
172+
let service: Service;
173+
if (options.service) setService(options.service);
158174
const evalAwarePartialHost = createEvalAwarePartialHost(
159175
state,
160176
options.composeWithEvalAwarePartialHost
@@ -167,6 +183,8 @@ export function createRepl(options: CreateReplOptions = {}) {
167183
? console
168184
: new Console(stdout, stderr);
169185

186+
const declaredExports = new Set();
187+
170188
const replService: ReplService = {
171189
state: options.state ?? new EvalState(join(process.cwd(), EVAL_FILENAME)),
172190
setService,
@@ -186,6 +204,8 @@ export function createRepl(options: CreateReplOptions = {}) {
186204

187205
function setService(_service: Service) {
188206
service = _service;
207+
if (mustSetStatePath)
208+
state.path = join(process.cwd(), REPL_FILENAME(service.ts.version));
189209
if (ignoreDiagnosticsThatAreAnnoyingInInteractiveRepl) {
190210
service.addDiagnosticFilter({
191211
appliesToAllFiles: false,
@@ -200,7 +220,19 @@ export function createRepl(options: CreateReplOptions = {}) {
200220
}
201221
}
202222

223+
// Hard fix for TypeScript forcing `Object.defineProperty(exports, ...)`.
224+
function declareExports() {
225+
if (declaredExports.has(context)) return;
226+
runInContext(
227+
'exports = typeof module === "undefined" ? {} : module.exports;',
228+
state.path,
229+
context
230+
);
231+
declaredExports.add(context);
232+
}
233+
203234
function evalCode(code: string) {
235+
declareExports();
204236
const result = appendCompileAndEvalInput({
205237
service: service!,
206238
state,
@@ -218,6 +250,7 @@ export function createRepl(options: CreateReplOptions = {}) {
218250
context: Context;
219251
}) {
220252
const { code, enableTopLevelAwait, context } = options;
253+
declareExports();
221254
return appendCompileAndEvalInput({
222255
service: service!,
223256
state,
@@ -321,9 +354,7 @@ export function createRepl(options: CreateReplOptions = {}) {
321354
}
322355

323356
// Note: `code` argument is deprecated
324-
function startInternal(
325-
options?: ReplOptions & { code?: string; forceToBeModule?: boolean }
326-
) {
357+
function startInternal(options?: StartReplInternalOptions) {
327358
const { code, forceToBeModule = true, ...optionsOverride } = options ?? {};
328359
// TODO assert that `service` is set; remove all `service!` non-null assertions
329360

@@ -362,12 +393,11 @@ export function createRepl(options: CreateReplOptions = {}) {
362393

363394
// Bookmark the point where we should reset the REPL state.
364395
const resetEval = appendToEvalState(state, '');
365-
366396
function reset() {
367397
resetEval();
368398

369-
// Hard fix for TypeScript forcing `Object.defineProperty(exports, ...)`.
370-
runInContext('exports = module.exports', state.path, context);
399+
declareExports();
400+
371401
if (forceToBeModule) {
372402
state.input += 'export {};void 0;\n';
373403
}

src/test/helpers/ctx-ts-node.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export async function installTsNode() {
3939
while (true) {
4040
try {
4141
rimrafSync(join(TEST_DIR, '.yarn/cache/ts-node-file-*'));
42+
writeFileSync(join(TEST_DIR, 'yarn.lock'), '');
4243
const result = await promisify(childProcessExec)(
4344
`yarn --no-immutable`,
4445
{

src/test/repl/helpers.ts renamed to src/test/repl/helpers/ctx-repl.ts

Lines changed: 5 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { PassThrough } from 'stream';
2-
import { delay, TEST_DIR, tsNodeTypes, ctxTsNode } from '../helpers';
3-
import type { ExecutionContext } from 'ava';
4-
import { test, expect } from '../testlib';
1+
import type { ExecutionContext } from '@cspotcode/ava-lib';
52
import { expectStream } from '@cspotcode/expect-stream';
3+
import { PassThrough } from 'stream';
4+
import type { ctxTsNode } from '../../helpers/ctx-ts-node';
5+
import { delay, tsNodeTypes } from '../../helpers/misc';
6+
import { TEST_DIR } from '../../helpers/paths';
67

78
export interface CreateReplViaApiOptions {
89
registerHooks: boolean;
@@ -97,46 +98,3 @@ export async function ctxRepl(t: ctxTsNode.T) {
9798
};
9899
}
99100
}
100-
101-
export const macroReplNoErrorsAndStdoutContains = test.macro(
102-
(script: string, contains: string, options?: Partial<ExecuteInReplOptions>) =>
103-
async (t: ctxRepl.T) => {
104-
macroReplInternal(t, script, contains, undefined, contains, options);
105-
}
106-
);
107-
export const macroReplStderrContains = test.macro(
108-
(
109-
script: string,
110-
errorContains: string,
111-
options?: Partial<ExecuteInReplOptions>
112-
) =>
113-
async (t: ctxRepl.T) => {
114-
macroReplInternal(
115-
t,
116-
script,
117-
undefined,
118-
errorContains,
119-
errorContains,
120-
options
121-
);
122-
}
123-
);
124-
125-
async function macroReplInternal(
126-
t: ctxRepl.T,
127-
script: string,
128-
stdoutContains: string | undefined,
129-
stderrContains: string | undefined,
130-
waitPattern: string,
131-
options?: Partial<ExecuteInReplOptions>
132-
) {
133-
const r = await t.context.executeInRepl(script, {
134-
registerHooks: true,
135-
startInternalOptions: { useGlobal: false },
136-
waitPattern,
137-
...options,
138-
});
139-
if (stderrContains) expect(r.stderr).toContain(stderrContains);
140-
else expect(r.stderr).toBe('');
141-
if (stdoutContains) expect(r.stdout).toContain(stdoutContains);
142-
}

src/test/repl/helpers/macros.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { ctxRepl, ExecuteInReplOptions } from './ctx-repl';
2+
import { expect, test } from '../../testlib';
3+
4+
export const macroReplNoErrorsAndStdoutContains = test.macro(
5+
(script: string, contains: string, options?: Partial<ExecuteInReplOptions>) =>
6+
async (t: ctxRepl.T) => {
7+
macroReplInternal(t, script, contains, undefined, contains, options);
8+
}
9+
);
10+
export const macroReplStderrContains = test.macro(
11+
(
12+
script: string,
13+
errorContains: string,
14+
options?: Partial<ExecuteInReplOptions>
15+
) =>
16+
async (t: ctxRepl.T) => {
17+
macroReplInternal(
18+
t,
19+
script,
20+
undefined,
21+
errorContains,
22+
errorContains,
23+
options
24+
);
25+
}
26+
);
27+
28+
async function macroReplInternal(
29+
t: ctxRepl.T,
30+
script: string,
31+
stdoutContains: string | undefined,
32+
stderrContains: string | undefined,
33+
waitPattern: string,
34+
options?: Partial<ExecuteInReplOptions>
35+
) {
36+
const r = await t.context.executeInRepl(script, {
37+
registerHooks: true,
38+
startInternalOptions: { useGlobal: false },
39+
waitPattern,
40+
...options,
41+
});
42+
if (stderrContains) expect(r.stderr).toContain(stderrContains);
43+
else expect(r.stderr).toBe('');
44+
if (stdoutContains) expect(r.stdout).toContain(stdoutContains);
45+
}

src/test/repl/helpers/misc.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { tsNodeTypes, tsSupportsMtsCtsExtensions } from '../../helpers';
2+
3+
export const replFile = tsSupportsMtsCtsExtensions ? '<repl>.cts' : '<repl>.ts';

src/test/repl/repl-environment.spec.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import {
1414
import { dirname, join } from 'path';
1515
import { createExec, createExecTester } from '../exec-helpers';
1616
import { homedir } from 'os';
17-
import { ctxRepl } from './helpers';
17+
import { replFile } from './helpers/misc';
18+
import { ctxRepl } from './helpers/ctx-repl';
1819

1920
const test = context(ctxTsNode).contextEach(ctxRepl);
2021

@@ -100,7 +101,7 @@ test.suite(
100101
modulePath: typeof module !== 'undefined' && module.path,
101102
moduleFilename: typeof module !== 'undefined' && module.filename,
102103
modulePaths: typeof module !== 'undefined' && [...module.paths],
103-
exportsTest: typeof exports !== 'undefined' ? module.exports === exports : null,
104+
exportsTest: typeof exports !== 'undefined' && typeof module !== 'undefined' ? module.exports === exports : null,
104105
stackTest: new Error().stack!.split('\\n')[1],
105106
moduleAccessorsTest: eval('typeof fs') === 'undefined' ? null : eval('fs') === require('fs'),
106107
argv: [...process.argv]
@@ -197,7 +198,7 @@ test.suite(
197198
exportsTest: true,
198199
// Note: vanilla node uses different name. See #1360
199200
stackTest: expect.stringContaining(
200-
` at ${join(TEST_DIR, '<repl>.ts')}:4:`
201+
` at ${join(TEST_DIR, replFile)}:4:`
201202
),
202203
moduleAccessorsTest: true,
203204
argv: [tsNodeExe],
@@ -350,7 +351,7 @@ test.suite(
350351
exportsTest: true,
351352
// Note: vanilla node uses different name. See #1360
352353
stackTest: expect.stringContaining(
353-
` at ${join(TEST_DIR, '<repl>.ts')}:4:`
354+
` at ${join(TEST_DIR, replFile)}:4:`
354355
),
355356
moduleAccessorsTest: true,
356357
argv: [tsNodeExe],
@@ -428,7 +429,7 @@ test.suite(
428429

429430
// Note: vanilla node uses different name. See #1360
430431
stackTest: expect.stringContaining(
431-
` at ${join(TEST_DIR, '<repl>.ts')}:1:`
432+
` at ${join(TEST_DIR, replFile)}:1:`
432433
),
433434
},
434435
});
@@ -459,7 +460,7 @@ test.suite(
459460
exportsTest: true,
460461
// Note: vanilla node uses different name. See #1360
461462
stackTest: expect.stringContaining(
462-
` at ${join(TEST_DIR, '<repl>.ts')}:1:`
463+
` at ${join(TEST_DIR, replFile)}:1:`
463464
),
464465
moduleAccessorsTest: true,
465466
},

0 commit comments

Comments
 (0)