Skip to content

Commit 728a1fd

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

File tree

8 files changed

+242
-36
lines changed

8 files changed

+242
-36
lines changed

doc/api/cli.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1370,8 +1370,14 @@ 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 `--input-type`is not provided, Node.js will try to run the code as JavaScript,
1378+
then it will try to run the code as TypeScript.
1379+
When using TypeScript, the `--input-type=type` flag should be used,
1380+
to reduce the overhead of multiple syntax detection.
13751381

13761382
The REPL does not support this option. Usage of `--input-type=module` with
13771383
[`--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: 117 additions & 1 deletion
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,7 +89,7 @@ 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')) {
87-
return evalModuleEntryPoint(body, print);
92+
return evalTypeScriptModuleEntryPoint(body, print);
8893
}
8994

9095
const runScript = () => {
@@ -238,10 +243,121 @@ function readStdin(callback) {
238243
});
239244
}
240245

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

src/node_options.cc

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,13 @@ 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" &&
112+
input_type != "module" &&
113+
input_type != "commonjs-typescript" &&
114+
input_type != "module-typescript") {
115+
errors->push_back(
116+
"--input-type must be \"module\","
117+
"\"commonjs\", \"module-typescript\" or \"commonjs-typescript\"");
113118
}
114119
}
115120

0 commit comments

Comments
 (0)