Skip to content

Commit 0badde7

Browse files
committed
repl: add experimental TypeScript support
Signed-off-by: avivkeller <me@aviv.sh>
1 parent 3d3c7ea commit 0badde7

7 files changed

Lines changed: 81 additions & 23 deletions

File tree

doc/api/cli.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1362,6 +1362,16 @@ added:
13621362
13631363
Enable experimental support for the QUIC protocol.
13641364

1365+
### `--experimental-repl-typescript`
1366+
1367+
<!--
1368+
added: REPLACEME
1369+
-->
1370+
1371+
> Stability: 1.2 - Release candidate
1372+
1373+
Enable experimental support for TypeScript stripping in the REPL.
1374+
13651375
### `--experimental-sea-config`
13661376

13671377
<!-- YAML

lib/internal/main/check_syntax.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ function loadESMIfNeeded(cb) {
5858

5959
async function checkSyntax(source, filename) {
6060
let format;
61-
if (filename === '[stdin]' || filename === '[eval]') {
61+
if (filename === '[stdin]' || filename === '[eval]' || filename === '[repl]') {
6262
format = (getOptionValue('--input-type') === 'module') ? 'module' : 'commonjs';
6363
} else {
6464
const { defaultResolve } = require('internal/modules/esm/resolve');
@@ -75,3 +75,7 @@ async function checkSyntax(source, filename) {
7575

7676
wrapSafe(filename, source, undefined, format);
7777
}
78+
79+
module.exports = {
80+
checkSyntax,
81+
};

lib/internal/modules/typescript.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,4 +185,5 @@ module.exports = {
185185
stripTypeScriptModuleTypes,
186186
stripTypeScriptTypes,
187187
stripTypeScriptTypesForCoverage,
188+
processTypeScriptCode,
188189
};

lib/repl.js

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ const {
9797
decorateErrorStack,
9898
isError,
9999
deprecate,
100+
getLazy,
100101
SideEffectFreeRegExpPrototypeSymbolReplace,
101102
SideEffectFreeRegExpPrototypeSymbolSplit,
102103
} = require('internal/util');
@@ -141,6 +142,9 @@ const {
141142
const experimentalREPLAwait = getOptionValue(
142143
'--experimental-repl-await',
143144
);
145+
const experimentalREPLTypeScript = getOptionValue(
146+
'--experimental-repl-typescript'
147+
)
144148
const pendingDeprecation = getOptionValue('--pending-deprecation');
145149
const {
146150
REPL_MODE_SLOPPY,
@@ -177,8 +181,12 @@ const {
177181

178182
// Lazy-loaded.
179183
let processTopLevelAwait;
184+
const processTypeScriptCode = getLazy(() => require('internal/modules/typescript').processTypeScriptCode);
185+
const checkSyntax = getLazy(() => require('internal/main/check_syntax').checkSyntax);
180186

181187
const parentModule = module;
188+
const kREPLTag = '[repl]';
189+
const kTypeScriptOptions = { __proto__: null, mode: 'strip-only', filename: kREPLTag };
182190

183191
// AsyncLocalStorage to track which REPL instance owns the current async context
184192
// This replaces the domain-based tracking for error handling
@@ -477,6 +485,41 @@ class REPLServer extends Interface {
477485
phase === 'evaluation' ? cascadedLoader.kEvaluationPhase :
478486
cascadedLoader.kSourcePhase);
479487
}
488+
function runScript(scriptCode) {
489+
if (experimentalREPLTypeScript) {
490+
try {
491+
// Try parsing as pure JS
492+
checkSyntax()(scriptCode, kREPLTag);
493+
} catch (originalError) {
494+
try {
495+
// That failed, so try stripping TypeScript types
496+
scriptCode = processTypeScriptCode()(scriptCode, kTypeScriptOptions);
497+
} catch (tsError) {
498+
// If it's invalid or unsupported TypeScript syntax, rethrow the original error
499+
// with the TypeScript error message added to the stack.
500+
if (tsError.code === 'ERR_INVALID_TYPESCRIPT_SYNTAX' ||
501+
tsError.code === 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX') {
502+
originalError.stack = `${tsError.message}\n\n${originalError.stack}`;
503+
throw originalError;
504+
}
505+
506+
throw tsError;
507+
}
508+
}
509+
}
510+
511+
return makeContextifyScript(
512+
scriptCode, // code
513+
file, // filename,
514+
0, // lineOffset
515+
0, // columnOffset,
516+
undefined, // cachedData
517+
false, // produceCachedData
518+
undefined, // parsingContext
519+
hostDefinedOptionId, // hostDefinedOptionId
520+
importModuleDynamically, // importModuleDynamically
521+
);
522+
}
480523
// `experimentalREPLAwait` is set to true by default.
481524
// Shall be false in case `--no-experimental-repl-await` flag is used.
482525
if (experimentalREPLAwait && StringPrototypeIncludes(code, 'await')) {
@@ -498,17 +541,7 @@ class REPLServer extends Interface {
498541
// in order to detect if error is truly non recoverable
499542
const fallbackCode = SideEffectFreeRegExpPrototypeSymbolReplace(/\bawait\b/g, code, '');
500543
try {
501-
makeContextifyScript(
502-
fallbackCode, // code
503-
file, // filename,
504-
0, // lineOffset
505-
0, // columnOffset,
506-
undefined, // cachedData
507-
false, // produceCachedData
508-
undefined, // parsingContext
509-
hostDefinedOptionId, // hostDefinedOptionId
510-
importModuleDynamically, // importModuleDynamically
511-
);
544+
runScript(fallbackCode);
512545
} catch (fallbackError) {
513546
if (isRecoverableError(fallbackError, fallbackCode)) {
514547
recoverableError = true;
@@ -536,17 +569,7 @@ class REPLServer extends Interface {
536569
// value for statements and declarations that don't return a value.
537570
code = `'use strict'; void 0;\n${code}`;
538571
}
539-
script = makeContextifyScript(
540-
code, // code
541-
file, // filename,
542-
0, // lineOffset
543-
0, // columnOffset,
544-
undefined, // cachedData
545-
false, // produceCachedData
546-
undefined, // parsingContext
547-
hostDefinedOptionId, // hostDefinedOptionId
548-
importModuleDynamically, // importModuleDynamically
549-
);
572+
script = runScript(code);
550573
} catch (e) {
551574
debug('parse error %j', code, e);
552575
if (wrappedCmd) {

src/node_options.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
726726
&EnvironmentOptions::experimental_repl_await,
727727
kAllowedInEnvvar,
728728
true);
729+
AddOption("--experimental-repl-typescript",
730+
"experimental TypeScript support in REPL",
731+
&EnvironmentOptions::experimental_repl_typescript,
732+
kAllowedInEnvvar);
729733
AddOption("--experimental-vm-modules",
730734
"experimental ES Module support in vm module",
731735
&EnvironmentOptions::experimental_vm_modules,

src/node_options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ class EnvironmentOptions : public Options {
154154
bool allow_ffi = false;
155155
bool allow_worker_threads = false;
156156
bool experimental_repl_await = true;
157+
bool experimental_repl_typescript = false;
157158
bool experimental_vm_modules = EXPERIMENTALS_DEFAULT_VALUE;
158159
bool async_context_frame = true;
159160
bool expose_internals = false;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Flags: --experimental-repl-typescript
2+
'use strict';
3+
require('../common');
4+
const assert = require('assert');
5+
6+
const { startNewREPLServer } = require('../common/repl');
7+
8+
const { input, output } = startNewREPLServer({ terminal: false, prompt: '> ' });
9+
10+
input.emit('data', 'let x: number = 3\n');
11+
assert.match(output.accumulator, /undefined\n> /);
12+
output.accumulator = '';
13+
14+
input.emit('data', 'x\n');
15+
assert.match(output.accumulator, /3\n> /);

0 commit comments

Comments
 (0)