Skip to content

Commit 7229405

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

File tree

8 files changed

+220
-36
lines changed

8 files changed

+220
-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: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,10 @@ function processTypeScriptCode(code, options) {
114114
* @param {string} filename The filename of the source code.
115115
* @returns {TransformOutput} The stripped TypeScript code.
116116
*/
117-
function stripTypeScriptModuleTypes(source, filename) {
117+
function stripTypeScriptModuleTypes(source, filename, emitWarning = true) {
118+
if (emitWarning) {
119+
emitExperimentalWarning('Type Stripping');
120+
}
118121
assert(typeof source === 'string');
119122
if (isUnderNodeModules(filename)) {
120123
throw new ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING(filename);

lib/internal/process/execution.js

Lines changed: 109 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,
@@ -84,7 +88,7 @@ function evalScript(name, body, breakFirstLine, print, shouldLoadESM = false) {
8488
if (getOptionValue('--experimental-detect-module') &&
8589
getOptionValue('--input-type') === '' &&
8690
containsModuleSyntax(body, name, null, 'no CJS variables')) {
87-
return evalModuleEntryPoint(body, print);
91+
return evalTypeScriptModuleEntryPoint(body, print);
8892
}
8993

9094
const runScript = () => {
@@ -238,10 +242,114 @@ function readStdin(callback) {
238242
});
239243
}
240244

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

src/node_options.cc

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,11 @@ 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("--input-type must be \"module\", \"commonjs\", \"module-typescript\" or \"commonjs-typescript\"");
113116
}
114117
}
115118

0 commit comments

Comments
 (0)