Skip to content

Commit aeb77e9

Browse files
module: support eval with ts syntax detection
1 parent b171afe commit aeb77e9

File tree

8 files changed

+243
-35
lines changed

8 files changed

+243
-35
lines changed

doc/api/cli.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1370,8 +1370,15 @@ added: v12.0.0
13701370
-->
13711371

13721372
This configures Node.js to interpret `--eval` or `STDIN` input as CommonJS or
1373-
as an ES module. Valid values are `"commonjs"` or `"module"`. The default is
1374-
`"commonjs"`.
1373+
as an ES module. Valid values are `"commonjs"`, `"module"`, `"module-typescript"` and `"commonjs-typescript"`.
1374+
The `"-typescript"` values are available only in combination with the flag `--experimental-strip-types`.
1375+
The default is `"commonjs"`.
1376+
1377+
If `--experimental-strip-types` is enabled and `--input-type`is not provided,
1378+
Node.js will first try to run the code as JavaScript,
1379+
then it will try to run the code as TypeScript.
1380+
When using TypeScript, the `--input-type=type` flag should always be used,
1381+
to reduce the overhead of multiple syntax detections.
13751382

13761383
The REPL does not support this option. Usage of `--input-type=module` with
13771384
[`--print`][] will throw an error, as `--print` does not support ES module

lib/internal/main/eval_string.js

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,34 @@ const {
1313
prepareMainThreadExecution,
1414
markBootstrapComplete,
1515
} = require('internal/process/pre_execution');
16-
const { evalModuleEntryPoint, evalScript } = require('internal/process/execution');
16+
const {
17+
evalModuleEntryPoint,
18+
evalTypeScript,
19+
parseAndEvalCommonjsTypeScript,
20+
parseAndEvalModuleTypeScript,
21+
evalScript,
22+
} = require('internal/process/execution');
1723
const { addBuiltinLibsToObject } = require('internal/modules/helpers');
18-
const { stripTypeScriptModuleTypes } = require('internal/modules/typescript');
1924
const { getOptionValue } = require('internal/options');
2025

2126
prepareMainThreadExecution();
2227
addBuiltinLibsToObject(globalThis, '<eval>');
2328
markBootstrapComplete();
2429

2530
const code = getOptionValue('--eval');
26-
const source = getOptionValue('--experimental-strip-types') ?
27-
stripTypeScriptModuleTypes(code) :
28-
code;
2931

3032
const print = getOptionValue('--print');
3133
const shouldLoadESM = getOptionValue('--import').length > 0 || getOptionValue('--experimental-loader').length > 0;
32-
if (getOptionValue('--input-type') === 'module') {
33-
evalModuleEntryPoint(source, print);
34+
const inputType = getOptionValue('--input-type');
35+
const tsEnabled = getOptionValue('--experimental-strip-types');
36+
if (inputType === 'module') {
37+
evalModuleEntryPoint(code, print);
38+
} else if (inputType === 'module-typescript' && tsEnabled) {
39+
parseAndEvalModuleTypeScript(code, print);
3440
} else {
3541
// For backward compatibility, we want the identifier crypto to be the
3642
// `node:crypto` module rather than WebCrypto.
37-
const isUsingCryptoIdentifier = RegExpPrototypeExec(/\bcrypto\b/, source) !== null;
43+
const isUsingCryptoIdentifier = RegExpPrototypeExec(/\bcrypto\b/, code) !== null;
3844
const shouldDefineCrypto = isUsingCryptoIdentifier && internalBinding('config').hasOpenSSL;
3945

4046
if (isUsingCryptoIdentifier && !shouldDefineCrypto) {
@@ -49,11 +55,24 @@ if (getOptionValue('--input-type') === 'module') {
4955
};
5056
ObjectDefineProperty(object, name, { __proto__: null, set: setReal });
5157
}
52-
evalScript('[eval]',
53-
shouldDefineCrypto ? (
54-
print ? `let crypto=require("node:crypto");{${source}}` : `(crypto=>{{${source}}})(require('node:crypto'))`
55-
) : source,
56-
getOptionValue('--inspect-brk'),
57-
print,
58-
shouldLoadESM);
58+
59+
let evalFunction;
60+
if (inputType === 'commonjs') {
61+
evalFunction = evalScript;
62+
} else if (inputType === 'commonjs-typescript' && tsEnabled) {
63+
evalFunction = parseAndEvalCommonjsTypeScript;
64+
} else if (tsEnabled) {
65+
evalFunction = evalTypeScript;
66+
} else {
67+
// Default to commonjs.
68+
evalFunction = evalScript;
69+
}
70+
71+
evalFunction('[eval]',
72+
shouldDefineCrypto ? (
73+
print ? `let crypto=require("node:crypto");{${code}}` : `(crypto=>{{${code}}})(require('node:crypto'))`
74+
) : code,
75+
getOptionValue('--inspect-brk'),
76+
print,
77+
shouldLoadESM);
5978
}

lib/internal/modules/cjs/loader.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,6 @@ function initializeCJS() {
449449

450450
const tsEnabled = getOptionValue('--experimental-strip-types');
451451
if (tsEnabled) {
452-
emitExperimentalWarning('Type Stripping');
453452
Module._extensions['.cts'] = loadCTS;
454453
Module._extensions['.ts'] = loadTS;
455454
}

lib/internal/modules/esm/translators.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,6 @@ translators.set('require-commonjs', (url, source, isMain) => {
250250
// Handle CommonJS modules referenced by `require` calls.
251251
// This translator function must be sync, as `require` is sync.
252252
translators.set('require-commonjs-typescript', (url, source, isMain) => {
253-
emitExperimentalWarning('Type Stripping');
254253
assert(cjsParse);
255254
const code = stripTypeScriptModuleTypes(stringify(source), url);
256255
return createCJSModuleWrap(url, code, isMain, 'commonjs-typescript');
@@ -464,7 +463,6 @@ translators.set('wasm', async function(url, source) {
464463

465464
// Strategy for loading a commonjs TypeScript module
466465
translators.set('commonjs-typescript', function(url, source) {
467-
emitExperimentalWarning('Type Stripping');
468466
assertBufferSource(source, true, 'load');
469467
const code = stripTypeScriptModuleTypes(stringify(source), url);
470468
debug(`Translating TypeScript ${url}`);
@@ -473,7 +471,6 @@ translators.set('commonjs-typescript', function(url, source) {
473471

474472
// Strategy for loading an esm TypeScript module
475473
translators.set('module-typescript', function(url, source) {
476-
emitExperimentalWarning('Type Stripping');
477474
assertBufferSource(source, true, 'load');
478475
const code = stripTypeScriptModuleTypes(stringify(source), url);
479476
debug(`Translating TypeScript ${url}`);

lib/internal/modules/typescript.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,13 @@ function processTypeScriptCode(code, options) {
112112
* It is used by internal loaders.
113113
* @param {string} source TypeScript code to parse.
114114
* @param {string} filename The filename of the source code.
115+
* @param {boolean} emitWarning Whether to emit a warning.
115116
* @returns {TransformOutput} The stripped TypeScript code.
116117
*/
117-
function stripTypeScriptModuleTypes(source, filename) {
118+
function stripTypeScriptModuleTypes(source, filename, emitWarning = true) {
119+
if (emitWarning) {
120+
emitExperimentalWarning('Type Stripping');
121+
}
118122
assert(typeof source === 'string');
119123
if (isUnderNodeModules(filename)) {
120124
throw new ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING(filename);

lib/internal/process/execution.js

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
'use strict';
22

33
const {
4+
ArrayPrototypeJoin,
5+
ArrayPrototypeSplice,
46
RegExpPrototypeExec,
7+
StringPrototypeSplit,
58
Symbol,
69
globalThis,
710
} = primordials;
@@ -17,6 +20,7 @@ const {
1720
} = require('internal/errors');
1821
const { pathToFileURL } = require('internal/url');
1922
const { exitCodes: { kGenericUserError } } = internalBinding('errors');
23+
const { stripTypeScriptModuleTypes } = require('internal/modules/typescript');
2024

2125
const {
2226
executionAsyncId,
@@ -32,6 +36,7 @@ const { getOptionValue } = require('internal/options');
3236
const {
3337
makeContextifyScript, runScriptInThisContext,
3438
} = require('internal/vm');
39+
const { emitExperimentalWarning } = require('internal/util');
3540
// shouldAbortOnUncaughtToggle is a typed array for faster
3641
// communication with JS.
3742
const { shouldAbortOnUncaughtToggle } = internalBinding('util');
@@ -84,6 +89,9 @@ function evalScript(name, body, breakFirstLine, print, shouldLoadESM = false) {
8489
if (getOptionValue('--experimental-detect-module') &&
8590
getOptionValue('--input-type') === '' &&
8691
containsModuleSyntax(body, name, null, 'no CJS variables')) {
92+
if (getOptionValue('--experimental-strip-types')) {
93+
return evalTypeScriptModuleEntryPoint(body, print);
94+
}
8795
return evalModuleEntryPoint(body, print);
8896
}
8997

@@ -238,10 +246,120 @@ function readStdin(callback) {
238246
});
239247
}
240248

249+
/**
250+
*
251+
* Adds the TS message to the error stack
252+
* At the 3rd line of the stack, the message is added
253+
* @param {Error} originalStack the stack to decorate
254+
* @param {string} newMessage the message to add to the error stack
255+
* @returns {void}
256+
*/
257+
function decorateCJSErrorWithTSMessage(originalStack, newMessage) {
258+
const lines = StringPrototypeSplit(originalStack, '\n');
259+
ArrayPrototypeSplice(lines, 3, 0, newMessage);
260+
return ArrayPrototypeJoin(lines, '\n');
261+
}
262+
263+
/**
264+
*
265+
* Wrapper of evalScript
266+
*
267+
* This function wraps the evaluation of the source code in a try-catch block.
268+
* If the source code fails to be evaluated, it will retry evaluating the source code
269+
* with the TypeScript parser.
270+
*
271+
* If the source code fails to be evaluated with the TypeScript parser,
272+
* it will rethrow the original error, adding the TypeScript error message to the stack.
273+
*
274+
* This way we don't change the behavior of the code, but we provide a better error message
275+
* in case of a typescript error.
276+
*/
277+
function evalTypeScript(name, source, breakFirstLine, print, shouldLoadESM = false) {
278+
try {
279+
evalScript(name, source, breakFirstLine, print, shouldLoadESM);
280+
} catch (originalError) {
281+
try {
282+
const strippedSource = stripTypeScriptModuleTypes(source, name, false);
283+
evalScript(name, strippedSource, breakFirstLine, print, shouldLoadESM);
284+
// Emit the experimental warning after the code was successfully evaluated.
285+
emitExperimentalWarning('Type Stripping');
286+
} catch (tsError) {
287+
if (tsError.code === 'ERR_INVALID_TYPESCRIPT_SYNTAX') {
288+
originalError.stack = decorateCJSErrorWithTSMessage(originalError.stack, tsError.message);
289+
}
290+
throw originalError;
291+
}
292+
}
293+
}
294+
295+
/**
296+
* Wrapper of evalModuleEntryPoint
297+
*
298+
* This function wraps the evaluation of the source code in a try-catch block.
299+
* If the source code fails to be evaluated, it will retry evaluating the source code
300+
* with the TypeScript parser.
301+
*
302+
*/
303+
function evalTypeScriptModuleEntryPoint(source, print) {
304+
if (print) {
305+
throw new ERR_EVAL_ESM_CANNOT_PRINT();
306+
}
307+
308+
RegExpPrototypeExec(/^/, ''); // Necessary to reset RegExp statics before user code runs.
309+
310+
return require('internal/modules/run_main').runEntryPointWithESMLoader(
311+
async (loader) => {
312+
try {
313+
// Await here to catch the error and rethrow it with the typescript error message.
314+
return await loader.eval(source, getEvalModuleUrl(), true);
315+
} catch (originalError) {
316+
try {
317+
const url = getEvalModuleUrl();
318+
const strippedSource = stripTypeScriptModuleTypes(source, url, false);
319+
const result = await loader.eval(strippedSource, url, true);
320+
// Emit the experimental warning after the code was successfully evaluated.
321+
emitExperimentalWarning('Type Stripping');
322+
return result;
323+
} catch (tsError) {
324+
if (tsError.code === 'ERR_INVALID_TYPESCRIPT_SYNTAX') {
325+
originalError.stack = `${tsError.message}\n\n${originalError.stack}`;
326+
}
327+
throw originalError;
328+
}
329+
}
330+
},
331+
);
332+
};
333+
334+
/**
335+
*
336+
* Function used to shortcut when `--input-type=module-typescript` is set.
337+
* @param {string} source
338+
* @param {boolean} print
339+
*/
340+
function parseAndEvalModuleTypeScript(source, print) {
341+
// We know its a TypeScript module, we can safely emit the experimental warning.
342+
const strippedSource = stripTypeScriptModuleTypes(source, getEvalModuleUrl());
343+
evalModuleEntryPoint(strippedSource, print);
344+
}
345+
346+
/**
347+
* Function used to shortcut when `--input-type=commonjs-typescript` is set
348+
* See evalScript signature
349+
*/
350+
function parseAndEvalCommonjsTypeScript(name, source, breakFirstLine, print, shouldLoadESM = false) {
351+
// We know its a TypeScript module, we can safely emit the experimental warning.
352+
const strippedSource = stripTypeScriptModuleTypes(source, getEvalModuleUrl());
353+
evalScript(name, strippedSource, breakFirstLine, print, shouldLoadESM);
354+
}
355+
241356
module.exports = {
357+
parseAndEvalCommonjsTypeScript,
358+
parseAndEvalModuleTypeScript,
242359
readStdin,
243360
tryGetCwd,
244361
evalModuleEntryPoint,
362+
evalTypeScript,
245363
evalScript,
246364
onGlobalUncaughtException: createOnGlobalUncaughtException(),
247365
setUncaughtExceptionCaptureCallback,

src/node_options.cc

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,12 @@ void PerIsolateOptions::CheckOptions(std::vector<std::string>* errors,
108108
void EnvironmentOptions::CheckOptions(std::vector<std::string>* errors,
109109
std::vector<std::string>* argv) {
110110
if (!input_type.empty()) {
111-
if (input_type != "commonjs" && input_type != "module") {
112-
errors->push_back("--input-type must be \"module\" or \"commonjs\"");
111+
if (input_type != "commonjs" && input_type != "module" &&
112+
input_type != "commonjs-typescript" &&
113+
input_type != "module-typescript") {
114+
errors->push_back(
115+
"--input-type must be \"module\","
116+
"\"commonjs\", \"module-typescript\" or \"commonjs-typescript\"");
113117
}
114118
}
115119

0 commit comments

Comments
 (0)