Skip to content

Commit 86c5d6e

Browse files
authored
Declare types for node builtin modules in REPL so you do not need to import them (#1500)
* Declare types for node builtin modules in REPL so you do not need to import them * fix * fix
1 parent a979dd6 commit 86c5d6e

File tree

7 files changed

+63
-17
lines changed

7 files changed

+63
-17
lines changed

src/index.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,15 @@ export function create(rawOptions: CreateOptions = {}): Service {
685685
});
686686
}
687687

688+
/**
689+
* True if require() hooks should interop with experimental ESM loader.
690+
* Enabled explicitly via a flag since it is a breaking change.
691+
*/
692+
let experimentalEsmLoader = false;
693+
function enableExperimentalEsmLoaderInterop() {
694+
experimentalEsmLoader = true;
695+
}
696+
688697
// Install source map support and read from memory cache.
689698
installSourceMapSupport();
690699
function installSourceMapSupport() {
@@ -1267,15 +1276,6 @@ export function create(rawOptions: CreateOptions = {}): Service {
12671276
});
12681277
}
12691278

1270-
/**
1271-
* True if require() hooks should interop with experimental ESM loader.
1272-
* Enabled explicitly via a flag since it is a breaking change.
1273-
*/
1274-
let experimentalEsmLoader = false;
1275-
function enableExperimentalEsmLoaderInterop() {
1276-
experimentalEsmLoader = true;
1277-
}
1278-
12791279
return {
12801280
[TS_NODE_SERVICE_BRAND]: true,
12811281
ts,

src/repl.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { Console } from 'console';
1414
import * as assert from 'assert';
1515
import type * as tty from 'tty';
1616
import type * as Module from 'module';
17+
import { builtinModules } from 'module';
1718

1819
// Lazy-loaded.
1920
let _processTopLevelAwait: (src: string) => string | null;
@@ -356,6 +357,22 @@ export function createRepl(options: CreateReplOptions = {}) {
356357
if (forceToBeModule) {
357358
state.input += 'export {};void 0;\n';
358359
}
360+
361+
// Declare node builtins.
362+
// Skip the same builtins as `addBuiltinLibsToObject`:
363+
// those starting with _
364+
// those containing /
365+
// those that already exist as globals
366+
// Intentionally suppress type errors in case @types/node does not declare any of them.
367+
state.input += `// @ts-ignore\n${builtinModules
368+
.filter(
369+
(name) =>
370+
!name.startsWith('_') &&
371+
!name.includes('/') &&
372+
!['console', 'module', 'process'].includes(name)
373+
)
374+
.map((name) => `declare import ${name} = require('${name}')`)
375+
.join(';')}\n`;
359376
}
360377

361378
reset();

src/test/helpers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ export const CMD_ESM_LOADER_WITHOUT_PROJECT = `node ${EXPERIMENTAL_MODULES_FLAG}
4040
// `createRequire` does not exist on older node versions
4141
export const testsDirRequire = createRequire(join(TEST_DIR, 'index.js'));
4242

43+
export const ts = testsDirRequire('typescript');
44+
4345
export const xfs = new NodeFS(fs);
4446

4547
/** Pass to `test.context()` to get access to the ts-node API under test */

src/test/index.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as expect from 'expect';
33
import { join, resolve, sep as pathSep } from 'path';
44
import { tmpdir } from 'os';
55
import semver = require('semver');
6-
import ts = require('typescript');
6+
import { ts } from './helpers';
77
import { lstatSync, mkdtempSync } from 'fs';
88
import { npath } from '@yarnpkg/fslib';
99
import type _createRequire from 'create-require';

src/test/repl/node-repl-tla.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { expect } from 'chai';
22
import type { Key } from 'readline';
33
import { Stream } from 'stream';
44
import semver = require('semver');
5-
import ts = require('typescript');
5+
import { ts } from '../helpers';
66
import type { ContextWithTsNodeUnderTest } from './helpers';
77

88
interface SharedObjects extends ContextWithTsNodeUnderTest {

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ test.suite(
9494
}
9595
);
9696

97-
const declareGlobals = `declare var replReport: any, stdinReport: any, evalReport: any, restReport: any, global: any, __filename: any, __dirname: any, module: any, exports: any, fs: any;`;
97+
const declareGlobals = `declare var replReport: any, stdinReport: any, evalReport: any, restReport: any, global: any, __filename: any, __dirname: any, module: any, exports: any;`;
9898
function setReportGlobal(type: 'repl' | 'stdin' | 'eval') {
9999
return `
100100
${declareGlobals}
@@ -107,7 +107,7 @@ test.suite(
107107
modulePaths: typeof module !== 'undefined' && [...module.paths],
108108
exportsTest: typeof exports !== 'undefined' ? module.exports === exports : null,
109109
stackTest: new Error().stack!.split('\\n')[1],
110-
moduleAccessorsTest: typeof fs === 'undefined' ? null : fs === require('fs'),
110+
moduleAccessorsTest: eval('typeof fs') === 'undefined' ? null : eval('fs') === require('fs'),
111111
argv: [...process.argv]
112112
};
113113
`.replace(/\n/g, '');
@@ -203,7 +203,7 @@ test.suite(
203203
exportsTest: true,
204204
// Note: vanilla node uses different name. See #1360
205205
stackTest: expect.stringContaining(
206-
` at ${join(TEST_DIR, '<repl>.ts')}:2:`
206+
` at ${join(TEST_DIR, '<repl>.ts')}:4:`
207207
),
208208
moduleAccessorsTest: true,
209209
argv: [tsNodeExe],
@@ -356,7 +356,7 @@ test.suite(
356356
exportsTest: true,
357357
// Note: vanilla node uses different name. See #1360
358358
stackTest: expect.stringContaining(
359-
` at ${join(TEST_DIR, '<repl>.ts')}:2:`
359+
` at ${join(TEST_DIR, '<repl>.ts')}:4:`
360360
),
361361
moduleAccessorsTest: true,
362362
argv: [tsNodeExe],

src/test/repl/repl.spec.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import ts = require('typescript');
1+
import { ts } from '../helpers';
22
import semver = require('semver');
33
import * as expect from 'expect';
44
import {
@@ -212,7 +212,7 @@ test.suite('top level await', (_test) => {
212212

213213
expect(stdout).toBe('> > ');
214214
expect(stderr.replace(/\r\n/g, '\n')).toBe(
215-
'<repl>.ts(2,7): error TS2322: ' +
215+
'<repl>.ts(4,7): error TS2322: ' +
216216
(semver.gte(ts.version, '4.0.0')
217217
? `Type 'number' is not assignable to type 'string'.\n`
218218
: `Type '1' is not assignable to type 'string'.\n`) +
@@ -411,3 +411,30 @@ test.suite(
411411
);
412412
}
413413
);
414+
415+
test.serial('REPL declares types for node built-ins within REPL', async (t) => {
416+
const { stdout, stderr } = await t.context.executeInRepl(
417+
`util.promisify(setTimeout)("should not be a string" as string)
418+
type Duplex = stream.Duplex
419+
const s = stream
420+
'done'`,
421+
{
422+
registerHooks: true,
423+
waitPattern: `done`,
424+
startInternalOptions: {
425+
useGlobal: false,
426+
},
427+
}
428+
);
429+
430+
// Assert that we receive a typechecking error about improperly using
431+
// `util.promisify` but *not* an error about the absence of `util`
432+
expect(stderr).not.toMatch("Cannot find name 'util'");
433+
expect(stderr).toMatch(
434+
"Argument of type 'string' is not assignable to parameter of type 'number'"
435+
);
436+
// Assert that both types and values can be used without error
437+
expect(stderr).not.toMatch("Cannot find namespace 'stream'");
438+
expect(stderr).not.toMatch("Cannot find name 'stream'");
439+
expect(stdout).toMatch(`done`);
440+
});

0 commit comments

Comments
 (0)