Skip to content

Commit 174d88e

Browse files
marco-ippolitoaduh95
authored andcommittedFeb 6, 2025
module: support eval with ts syntax detection
PR-URL: #56285 Backport-PR-URL: #56912 Refs: nodejs/typescript#17 Reviewed-By: Pietro Marchini <pietro.marchini94@gmail.com> Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
1 parent 8cbb7cc commit 174d88e

File tree

10 files changed

+511
-68
lines changed

10 files changed

+511
-68
lines changed
 

‎doc/api/cli.md

+17-2
Original file line numberDiff line numberDiff line change
@@ -1414,8 +1414,22 @@ added: v12.0.0
14141414
-->
14151415

14161416
This configures Node.js to interpret `--eval` or `STDIN` input as CommonJS or
1417-
as an ES module. Valid values are `"commonjs"` or `"module"`. The default is
1418-
`"commonjs"` unless [`--experimental-default-type=module`][] is used.
1417+
as an ES module. Valid values are `"commonjs"`, `"module"`, `"module-typescript"` and `"commonjs-typescript"`.
1418+
The `"-typescript"` values are available only in combination with the flag `--experimental-strip-types`.
1419+
The default is `"commonjs"` unless [`--experimental-default-type=module`][] is used.
1420+
If `--experimental-strip-types` is enabled and `--input-type` is not provided,
1421+
Node.js will try to detect the syntax with the following steps:
1422+
1423+
1. Run the input as CommonJS.
1424+
2. If step 1 fails, run the input as an ES module.
1425+
3. If step 2 fails with a SyntaxError, strip the types.
1426+
4. If step 3 fails with an error code [`ERR_INVALID_TYPESCRIPT_SYNTAX`][],
1427+
throw the error from step 2, including the TypeScript error in the message,
1428+
else run as CommonJS.
1429+
5. If step 4 fails, run the input as an ES module.
1430+
1431+
To avoid the delay of multiple syntax detection passes, the `--input-type=type` flag can be used to specify
1432+
how the `--eval` input should be interpreted.
14191433

14201434
The REPL does not support this option. Usage of `--input-type=module` with
14211435
[`--print`][] will throw an error, as `--print` does not support ES module
@@ -3712,6 +3726,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
37123726
[`Atomics.wait()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/wait
37133727
[`Buffer`]: buffer.md#class-buffer
37143728
[`CRYPTO_secure_malloc_init`]: https://www.openssl.org/docs/man3.0/man3/CRYPTO_secure_malloc_init.html
3729+
[`ERR_INVALID_TYPESCRIPT_SYNTAX`]: errors.md#err_invalid_typescript_syntax
37153730
[`NODE_OPTIONS`]: #node_optionsoptions
37163731
[`NO_COLOR`]: https://no-color.org
37173732
[`SlowBuffer`]: buffer.md#class-slowbuffer

‎lib/internal/main/eval_string.js

+36-17
Original file line numberDiff line numberDiff line change
@@ -13,31 +13,37 @@ 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-
(getOptionValue('--experimental-default-type') === 'module' && getOptionValue('--input-type') !== 'commonjs')) {
34-
evalModuleEntryPoint(source, print);
34+
const inputType = getOptionValue('--input-type');
35+
const tsEnabled = getOptionValue('--experimental-strip-types');
36+
if (inputType === 'module' ||
37+
(getOptionValue('--experimental-default-type') === 'module' && inputType !== 'commonjs')) {
38+
evalModuleEntryPoint(code, print);
39+
} else if (inputType === 'module-typescript' && tsEnabled) {
40+
parseAndEvalModuleTypeScript(code, print);
3541
} else {
3642
// For backward compatibility, we want the identifier crypto to be the
3743
// `node:crypto` module rather than WebCrypto.
3844
const isUsingCryptoIdentifier =
39-
getOptionValue('--experimental-global-webcrypto') &&
40-
RegExpPrototypeExec(/\bcrypto\b/, source) !== null;
45+
getOptionValue('--experimental-global-webcrypto') &&
46+
RegExpPrototypeExec(/\bcrypto\b/, code) !== null;
4147
const shouldDefineCrypto = isUsingCryptoIdentifier && internalBinding('config').hasOpenSSL;
4248

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

‎lib/internal/modules/cjs/loader.js

-1
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,6 @@ function initializeCJS() {
435435

436436
const tsEnabled = getOptionValue('--experimental-strip-types');
437437
if (tsEnabled) {
438-
emitExperimentalWarning('Type Stripping');
439438
Module._extensions['.cts'] = loadCTS;
440439
Module._extensions['.ts'] = loadTS;
441440
}

‎lib/internal/modules/esm/loader.js

+30-2
Original file line numberDiff line numberDiff line change
@@ -210,9 +210,25 @@ class ModuleLoader {
210210
}
211211
}
212212

213-
async eval(source, url, isEntryPoint = false) {
213+
/**
214+
*
215+
* @param {string} source Source code of the module.
216+
* @param {string} url URL of the module.
217+
* @returns {object} The module wrap object.
218+
*/
219+
createModuleWrap(source, url) {
220+
return compileSourceTextModule(url, source, this);
221+
}
222+
223+
/**
224+
*
225+
* @param {string} url URL of the module.
226+
* @param {object} wrap Module wrap object.
227+
* @param {boolean} isEntryPoint Whether the module is the entry point.
228+
* @returns {Promise<object>} The module object.
229+
*/
230+
async executeModuleJob(url, wrap, isEntryPoint = false) {
214231
const { ModuleJob } = require('internal/modules/esm/module_job');
215-
const wrap = compileSourceTextModule(url, source, this);
216232
const module = await onImport.tracePromise(async () => {
217233
const job = new ModuleJob(
218234
this, url, undefined, wrap, false, false);
@@ -232,6 +248,18 @@ class ModuleLoader {
232248
};
233249
}
234250

251+
/**
252+
*
253+
* @param {string} source Source code of the module.
254+
* @param {string} url URL of the module.
255+
* @param {boolean} isEntryPoint Whether the module is the entry point.
256+
* @returns {Promise<object>} The module object.
257+
*/
258+
eval(source, url, isEntryPoint = false) {
259+
const wrap = this.createModuleWrap(source, url);
260+
return this.executeModuleJob(url, wrap, isEntryPoint);
261+
}
262+
235263
/**
236264
* Get a (possibly not yet fully linked) module job from the cache, or create one and return its Promise.
237265
* @param {string} specifier The module request of the module to be resolved. Typically, what's

‎lib/internal/modules/esm/translators.js

-3
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,6 @@ translators.set('require-commonjs', (url, source, isMain) => {
242242
// Handle CommonJS modules referenced by `require` calls.
243243
// This translator function must be sync, as `require` is sync.
244244
translators.set('require-commonjs-typescript', (url, source, isMain) => {
245-
emitExperimentalWarning('Type Stripping');
246245
assert(cjsParse);
247246
const code = stripTypeScriptModuleTypes(stringify(source), url);
248247
return createCJSModuleWrap(url, code);
@@ -457,7 +456,6 @@ translators.set('wasm', async function(url, source) {
457456

458457
// Strategy for loading a commonjs TypeScript module
459458
translators.set('commonjs-typescript', function(url, source) {
460-
emitExperimentalWarning('Type Stripping');
461459
assertBufferSource(source, true, 'load');
462460
const code = stripTypeScriptModuleTypes(stringify(source), url);
463461
debug(`Translating TypeScript ${url}`);
@@ -466,7 +464,6 @@ translators.set('commonjs-typescript', function(url, source) {
466464

467465
// Strategy for loading an esm TypeScript module
468466
translators.set('module-typescript', function(url, source) {
469-
emitExperimentalWarning('Type Stripping');
470467
assertBufferSource(source, true, 'load');
471468
const code = stripTypeScriptModuleTypes(stringify(source), url);
472469
debug(`Translating TypeScript ${url}`);

‎lib/internal/modules/typescript.js

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

‎lib/internal/process/execution.js

+284-29
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
const {
44
RegExpPrototypeExec,
5+
StringPrototypeIndexOf,
6+
StringPrototypeSlice,
57
Symbol,
68
globalThis,
79
} = primordials;
@@ -17,6 +19,7 @@ const {
1719
} = require('internal/errors');
1820
const { pathToFileURL } = require('internal/url');
1921
const { exitCodes: { kGenericUserError } } = internalBinding('errors');
22+
const { stripTypeScriptModuleTypes } = require('internal/modules/typescript');
2023

2124
const {
2225
executionAsyncId,
@@ -32,6 +35,7 @@ const { getOptionValue } = require('internal/options');
3235
const {
3336
makeContextifyScript, runScriptInThisContext,
3437
} = require('internal/vm');
38+
const { emitExperimentalWarning, isError } = require('internal/util');
3539
// shouldAbortOnUncaughtToggle is a typed array for faster
3640
// communication with JS.
3741
const { shouldAbortOnUncaughtToggle } = internalBinding('util');
@@ -70,21 +74,14 @@ function evalModuleEntryPoint(source, print) {
7074
}
7175

7276
function evalScript(name, body, breakFirstLine, print, shouldLoadESM = false) {
73-
const CJSModule = require('internal/modules/cjs/loader').Module;
74-
75-
const cwd = tryGetCwd();
7677
const origModule = globalThis.module; // Set e.g. when called from the REPL.
77-
78-
const module = new CJSModule(name);
79-
module.filename = path.join(cwd, name);
80-
module.paths = CJSModule._nodeModulePaths(cwd);
81-
78+
const module = createModule(name);
8279
const baseUrl = pathToFileURL(module.filename).href;
8380

84-
if (getOptionValue('--experimental-detect-module') &&
85-
getOptionValue('--input-type') === '' && getOptionValue('--experimental-default-type') === '' &&
86-
containsModuleSyntax(body, name, null, 'no CJS variables')) {
87-
return evalModuleEntryPoint(body, print);
81+
if (shouldUseModuleEntryPoint(name, body)) {
82+
return getOptionValue('--experimental-strip-types') ?
83+
evalTypeScriptModuleEntryPoint(body, print) :
84+
evalModuleEntryPoint(body, print);
8885
}
8986

9087
const runScript = () => {
@@ -99,23 +96,8 @@ function evalScript(name, body, breakFirstLine, print, shouldLoadESM = false) {
9996
globalThis.__filename = name;
10097
RegExpPrototypeExec(/^/, ''); // Necessary to reset RegExp statics before user code runs.
10198
const result = module._compile(script, `${name}-wrapper`)(() => {
102-
const hostDefinedOptionId = Symbol(name);
103-
async function importModuleDynamically(specifier, _, importAttributes) {
104-
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
105-
return cascadedLoader.import(specifier, baseUrl, importAttributes);
106-
}
107-
const script = makeContextifyScript(
108-
body, // code
109-
name, // filename,
110-
0, // lineOffset
111-
0, // columnOffset,
112-
undefined, // cachedData
113-
false, // produceCachedData
114-
undefined, // parsingContext
115-
hostDefinedOptionId, // hostDefinedOptionId
116-
importModuleDynamically, // importModuleDynamically
117-
);
118-
return runScriptInThisContext(script, true, !!breakFirstLine);
99+
const compiledScript = compileScript(name, body, baseUrl);
100+
return runScriptInThisContext(compiledScript, true, !!breakFirstLine);
119101
});
120102
if (print) {
121103
const { log } = require('internal/console/global');
@@ -238,10 +220,283 @@ function readStdin(callback) {
238220
});
239221
}
240222

223+
/**
224+
* Adds the TS message to the error stack.
225+
*
226+
* At the 3rd line of the stack, the message is added.
227+
* @param {string} originalStack The stack to decorate
228+
* @param {string} newMessage the message to add to the error stack
229+
* @returns {void}
230+
*/
231+
function decorateCJSErrorWithTSMessage(originalStack, newMessage) {
232+
let index;
233+
for (let i = 0; i < 3; i++) {
234+
index = StringPrototypeIndexOf(originalStack, '\n', index + 1);
235+
}
236+
return StringPrototypeSlice(originalStack, 0, index) +
237+
'\n' + newMessage +
238+
StringPrototypeSlice(originalStack, index);
239+
}
240+
241+
/**
242+
*
243+
* Wrapper of evalScript
244+
*
245+
* This function wraps the evaluation of the source code in a try-catch block.
246+
* If the source code fails to be evaluated, it will retry evaluating the source code
247+
* with the TypeScript parser.
248+
*
249+
* If the source code fails to be evaluated with the TypeScript parser,
250+
* it will rethrow the original error, adding the TypeScript error message to the stack.
251+
*
252+
* This way we don't change the behavior of the code, but we provide a better error message
253+
* in case of a typescript error.
254+
* @param {string} name The name of the file
255+
* @param {string} source The source code to evaluate
256+
* @param {boolean} breakFirstLine Whether to break on the first line
257+
* @param {boolean} print If the result should be printed
258+
* @param {boolean} shouldLoadESM If the code should be loaded as an ESM module
259+
* @returns {void}
260+
*/
261+
function evalTypeScript(name, source, breakFirstLine, print, shouldLoadESM = false) {
262+
const origModule = globalThis.module; // Set e.g. when called from the REPL.
263+
const module = createModule(name);
264+
const baseUrl = pathToFileURL(module.filename).href;
265+
266+
if (shouldUseModuleEntryPoint(name, source)) {
267+
return evalTypeScriptModuleEntryPoint(source, print);
268+
}
269+
270+
let compiledScript;
271+
// This variable can be modified if the source code is stripped.
272+
let sourceToRun = source;
273+
try {
274+
compiledScript = compileScript(name, source, baseUrl);
275+
} catch (originalError) {
276+
// If it's not a SyntaxError, rethrow it.
277+
if (!isError(originalError) || originalError.name !== 'SyntaxError') {
278+
throw originalError;
279+
}
280+
try {
281+
sourceToRun = stripTypeScriptModuleTypes(source, name, false);
282+
// Retry the CJS/ESM syntax detection after stripping the types.
283+
if (shouldUseModuleEntryPoint(name, sourceToRun)) {
284+
return evalTypeScriptModuleEntryPoint(source, print);
285+
}
286+
// If the ContextifiedScript was successfully created, execute it.
287+
// outside the try-catch block to avoid catching runtime errors.
288+
compiledScript = compileScript(name, sourceToRun, baseUrl);
289+
// Emit the experimental warning after the code was successfully evaluated.
290+
emitExperimentalWarning('Type Stripping');
291+
} catch (tsError) {
292+
// If its not an error, or it's not an invalid typescript syntax error, rethrow it.
293+
if (!isError(tsError) || tsError?.code !== 'ERR_INVALID_TYPESCRIPT_SYNTAX') {
294+
throw tsError;
295+
}
296+
297+
try {
298+
originalError.stack = decorateCJSErrorWithTSMessage(originalError.stack, tsError.message);
299+
} catch { /* Ignore potential errors coming from `stack` getter/setter */ }
300+
throw originalError;
301+
}
302+
}
303+
304+
if (shouldLoadESM) {
305+
return require('internal/modules/run_main').runEntryPointWithESMLoader(
306+
() => runScriptInContext(name,
307+
sourceToRun,
308+
breakFirstLine,
309+
print,
310+
module,
311+
baseUrl,
312+
compiledScript,
313+
origModule));
314+
}
315+
316+
runScriptInContext(name, sourceToRun, breakFirstLine, print, module, baseUrl, compiledScript, origModule);
317+
}
318+
319+
/**
320+
* Wrapper of evalModuleEntryPoint
321+
*
322+
* This function wraps the compilation of the source code in a try-catch block.
323+
* If the source code fails to be compiled, it will retry transpiling the source code
324+
* with the TypeScript parser.
325+
* @param {string} source The source code to evaluate
326+
* @param {boolean} print If the result should be printed
327+
* @returns {Promise} The module evaluation promise
328+
*/
329+
function evalTypeScriptModuleEntryPoint(source, print) {
330+
if (print) {
331+
throw new ERR_EVAL_ESM_CANNOT_PRINT();
332+
}
333+
334+
RegExpPrototypeExec(/^/, ''); // Necessary to reset RegExp statics before user code runs.
335+
336+
return require('internal/modules/run_main').runEntryPointWithESMLoader(
337+
async (loader) => {
338+
const url = getEvalModuleUrl();
339+
let moduleWrap;
340+
try {
341+
// Compile the module to check for syntax errors.
342+
moduleWrap = loader.createModuleWrap(source, url);
343+
} catch (originalError) {
344+
// If it's not a SyntaxError, rethrow it.
345+
if (!isError(originalError) || originalError.name !== 'SyntaxError') {
346+
throw originalError;
347+
}
348+
let strippedSource;
349+
try {
350+
strippedSource = stripTypeScriptModuleTypes(source, url, false);
351+
// If the moduleWrap was successfully created, execute the module job.
352+
// outside the try-catch block to avoid catching runtime errors.
353+
moduleWrap = loader.createModuleWrap(strippedSource, url);
354+
// Emit the experimental warning after the code was successfully compiled.
355+
emitExperimentalWarning('Type Stripping');
356+
} catch (tsError) {
357+
// If its not an error, or it's not an invalid typescript syntax error, rethrow it.
358+
if (!isError(tsError) || tsError?.code !== 'ERR_INVALID_TYPESCRIPT_SYNTAX') {
359+
throw tsError;
360+
}
361+
try {
362+
originalError.stack = `${tsError.message}\n\n${originalError.stack}`;
363+
} catch { /* Ignore potential errors coming from `stack` getter/setter */ }
364+
365+
throw originalError;
366+
}
367+
}
368+
// If the moduleWrap was successfully created either with by just compiling
369+
// or after transpilation, execute the module job.
370+
return loader.executeModuleJob(url, moduleWrap, true);
371+
},
372+
);
373+
};
374+
375+
/**
376+
*
377+
* Function used to shortcut when `--input-type=module-typescript` is set.
378+
* @param {string} source
379+
* @param {boolean} print
380+
*/
381+
function parseAndEvalModuleTypeScript(source, print) {
382+
// We know its a TypeScript module, we can safely emit the experimental warning.
383+
const strippedSource = stripTypeScriptModuleTypes(source, getEvalModuleUrl());
384+
evalModuleEntryPoint(strippedSource, print);
385+
}
386+
387+
/**
388+
* Function used to shortcut when `--input-type=commonjs-typescript` is set
389+
* @param {string} name The name of the file
390+
* @param {string} source The source code to evaluate
391+
* @param {boolean} breakFirstLine Whether to break on the first line
392+
* @param {boolean} print If the result should be printed
393+
* @param {boolean} shouldLoadESM If the code should be loaded as an ESM module
394+
* @returns {void}
395+
*/
396+
function parseAndEvalCommonjsTypeScript(name, source, breakFirstLine, print, shouldLoadESM = false) {
397+
// We know its a TypeScript module, we can safely emit the experimental warning.
398+
const strippedSource = stripTypeScriptModuleTypes(source, getEvalModuleUrl());
399+
evalScript(name, strippedSource, breakFirstLine, print, shouldLoadESM);
400+
}
401+
402+
/**
403+
*
404+
* @param {string} name - The filename of the script.
405+
* @param {string} body - The code of the script.
406+
* @param {string} baseUrl Path of the parent importing the module.
407+
* @returns {ContextifyScript} The created contextify script.
408+
*/
409+
function compileScript(name, body, baseUrl) {
410+
const hostDefinedOptionId = Symbol(name);
411+
async function importModuleDynamically(specifier, _, importAttributes) {
412+
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
413+
return cascadedLoader.import(specifier, baseUrl, importAttributes);
414+
}
415+
return makeContextifyScript(
416+
body, // code
417+
name, // filename,
418+
0, // lineOffset
419+
0, // columnOffset,
420+
undefined, // cachedData
421+
false, // produceCachedData
422+
undefined, // parsingContext
423+
hostDefinedOptionId, // hostDefinedOptionId
424+
importModuleDynamically, // importModuleDynamically
425+
);
426+
}
427+
428+
/**
429+
* @param {string} name - The filename of the script.
430+
* @param {string} body - The code of the script.
431+
* @returns {boolean} Whether the module entry point should be evaluated as a module.
432+
*/
433+
function shouldUseModuleEntryPoint(name, body) {
434+
return getOptionValue('--experimental-detect-module') && getOptionValue('--experimental-default-type') === '' &&
435+
getOptionValue('--input-type') === '' &&
436+
containsModuleSyntax(body, name, null, 'no CJS variables');
437+
}
438+
439+
/**
440+
*
441+
* @param {string} name - The filename of the script.
442+
* @returns {import('internal/modules/esm/loader').CJSModule} The created module.
443+
*/
444+
function createModule(name) {
445+
const CJSModule = require('internal/modules/cjs/loader').Module;
446+
const cwd = tryGetCwd();
447+
const module = new CJSModule(name);
448+
module.filename = path.join(cwd, name);
449+
module.paths = CJSModule._nodeModulePaths(cwd);
450+
return module;
451+
}
452+
453+
/**
454+
*
455+
* @param {string} name - The filename of the script.
456+
* @param {string} body - The code of the script.
457+
* @param {boolean} breakFirstLine Whether to break on the first line
458+
* @param {boolean} print If the result should be printed
459+
* @param {import('internal/modules/esm/loader').CJSModule} module The module
460+
* @param {string} baseUrl Path of the parent importing the module.
461+
* @param {object} compiledScript The compiled script.
462+
* @param {any} origModule The original module.
463+
* @returns {void}
464+
*/
465+
function runScriptInContext(name, body, breakFirstLine, print, module, baseUrl, compiledScript, origModule) {
466+
// Create wrapper for cache entry
467+
const script = `
468+
globalThis.module = module;
469+
globalThis.exports = exports;
470+
globalThis.__dirname = __dirname;
471+
globalThis.require = require;
472+
return (main) => main();
473+
`;
474+
globalThis.__filename = name;
475+
RegExpPrototypeExec(/^/, ''); // Necessary to reset RegExp statics before user code runs.
476+
const result = module._compile(script, `${name}-wrapper`)(() => {
477+
// If the script was already compiled, use it.
478+
return runScriptInThisContext(
479+
compiledScript,
480+
true, !!breakFirstLine);
481+
});
482+
if (print) {
483+
const { log } = require('internal/console/global');
484+
485+
process.on('exit', () => {
486+
log(result);
487+
});
488+
}
489+
if (origModule !== undefined)
490+
globalThis.module = origModule;
491+
}
492+
241493
module.exports = {
494+
parseAndEvalCommonjsTypeScript,
495+
parseAndEvalModuleTypeScript,
242496
readStdin,
243497
tryGetCwd,
244498
evalModuleEntryPoint,
499+
evalTypeScript,
245500
evalScript,
246501
onGlobalUncaughtException: createOnGlobalUncaughtException(),
247502
setUncaughtExceptionCaptureCallback,

‎src/node_options.cc

+6-2
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,12 @@ void PerIsolateOptions::CheckOptions(std::vector<std::string>* errors,
109109
void EnvironmentOptions::CheckOptions(std::vector<std::string>* errors,
110110
std::vector<std::string>* argv) {
111111
if (!input_type.empty()) {
112-
if (input_type != "commonjs" && input_type != "module") {
113-
errors->push_back("--input-type must be \"module\" or \"commonjs\"");
112+
if (input_type != "commonjs" && 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\"");
114118
}
115119
}
116120

‎test/es-module/test-typescript-eval.mjs

+132-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { skip, spawnPromisified } from '../common/index.mjs';
2-
import { match, strictEqual } from 'node:assert';
2+
import { doesNotMatch, match, strictEqual } from 'node:assert';
33
import { test } from 'node:test';
44

55
if (!process.config.variables.node_use_amaro) skip('Requires Amaro');
@@ -20,7 +20,7 @@ test('eval TypeScript ESM syntax', async () => {
2020
test('eval TypeScript ESM syntax with input-type module', async () => {
2121
const result = await spawnPromisified(process.execPath, [
2222
'--experimental-strip-types',
23-
'--input-type=module',
23+
'--input-type=module-typescript',
2424
'--eval',
2525
`import util from 'node:util'
2626
const text: string = 'Hello, TypeScript!'
@@ -37,17 +37,16 @@ test('eval TypeScript CommonJS syntax', async () => {
3737
'--eval',
3838
`const util = require('node:util');
3939
const text: string = 'Hello, TypeScript!'
40-
console.log(util.styleText('red', text));`,
41-
'--no-warnings']);
40+
console.log(util.styleText('red', text));`]);
4241
match(result.stdout, /Hello, TypeScript!/);
43-
strictEqual(result.stderr, '');
42+
match(result.stderr, /ExperimentalWarning: Type Stripping is an experimental/);
4443
strictEqual(result.code, 0);
4544
});
4645

47-
test('eval TypeScript CommonJS syntax with input-type commonjs', async () => {
46+
test('eval TypeScript CommonJS syntax with input-type commonjs-typescript', async () => {
4847
const result = await spawnPromisified(process.execPath, [
4948
'--experimental-strip-types',
50-
'--input-type=commonjs',
49+
'--input-type=commonjs-typescript',
5150
'--eval',
5251
`const util = require('node:util');
5352
const text: string = 'Hello, TypeScript!'
@@ -84,10 +83,10 @@ test('TypeScript ESM syntax not specified', async () => {
8483
strictEqual(result.code, 0);
8584
});
8685

87-
test('expect fail eval TypeScript CommonJS syntax with input-type module', async () => {
86+
test('expect fail eval TypeScript CommonJS syntax with input-type module-typescript', async () => {
8887
const result = await spawnPromisified(process.execPath, [
8988
'--experimental-strip-types',
90-
'--input-type=module',
89+
'--input-type=module-typescript',
9190
'--eval',
9291
`const util = require('node:util');
9392
const text: string = 'Hello, TypeScript!'
@@ -98,10 +97,10 @@ test('expect fail eval TypeScript CommonJS syntax with input-type module', async
9897
strictEqual(result.code, 1);
9998
});
10099

101-
test('expect fail eval TypeScript ESM syntax with input-type commonjs', async () => {
100+
test('expect fail eval TypeScript ESM syntax with input-type commonjs-typescript', async () => {
102101
const result = await spawnPromisified(process.execPath, [
103102
'--experimental-strip-types',
104-
'--input-type=commonjs',
103+
'--input-type=commonjs-typescript',
105104
'--eval',
106105
`import util from 'node:util'
107106
const text: string = 'Hello, TypeScript!'
@@ -117,6 +116,128 @@ test('check syntax error is thrown when passing invalid syntax', async () => {
117116
'--eval',
118117
'enum Foo { A, B, C }']);
119118
strictEqual(result.stdout, '');
119+
match(result.stderr, /SyntaxError/);
120+
doesNotMatch(result.stderr, /ERR_INVALID_TYPESCRIPT_SYNTAX/);
121+
strictEqual(result.code, 1);
122+
});
123+
124+
test('check syntax error is thrown when passing invalid syntax with --input-type=module-typescript', async () => {
125+
const result = await spawnPromisified(process.execPath, [
126+
'--experimental-strip-types',
127+
'--input-type=module-typescript',
128+
'--eval',
129+
'enum Foo { A, B, C }']);
130+
strictEqual(result.stdout, '');
131+
match(result.stderr, /ERR_INVALID_TYPESCRIPT_SYNTAX/);
132+
strictEqual(result.code, 1);
133+
});
134+
135+
test('check syntax error is thrown when passing invalid syntax with --input-type=commonjs-typescript', async () => {
136+
const result = await spawnPromisified(process.execPath, [
137+
'--experimental-strip-types',
138+
'--input-type=commonjs-typescript',
139+
'--eval',
140+
'enum Foo { A, B, C }']);
141+
strictEqual(result.stdout, '');
120142
match(result.stderr, /ERR_INVALID_TYPESCRIPT_SYNTAX/);
121143
strictEqual(result.code, 1);
122144
});
145+
146+
test('should not parse TypeScript with --type-module=commonjs', async () => {
147+
const result = await spawnPromisified(process.execPath, [
148+
'--experimental-strip-types',
149+
'--input-type=commonjs',
150+
'--eval',
151+
`enum Foo {}`]);
152+
153+
strictEqual(result.stdout, '');
154+
match(result.stderr, /SyntaxError/);
155+
doesNotMatch(result.stderr, /ERR_INVALID_TYPESCRIPT_SYNTAX/);
156+
strictEqual(result.code, 1);
157+
});
158+
159+
test('should not parse TypeScript with --type-module=module', async () => {
160+
const result = await spawnPromisified(process.execPath, [
161+
'--experimental-strip-types',
162+
'--input-type=module',
163+
'--eval',
164+
`enum Foo {}`]);
165+
166+
strictEqual(result.stdout, '');
167+
match(result.stderr, /SyntaxError/);
168+
doesNotMatch(result.stderr, /ERR_INVALID_TYPESCRIPT_SYNTAX/);
169+
strictEqual(result.code, 1);
170+
});
171+
172+
test('check warning is emitted when eval TypeScript CommonJS syntax', async () => {
173+
const result = await spawnPromisified(process.execPath, [
174+
'--experimental-strip-types',
175+
'--eval',
176+
`const util = require('node:util');
177+
const text: string = 'Hello, TypeScript!'
178+
console.log(util.styleText('red', text));`]);
179+
match(result.stderr, /ExperimentalWarning: Type Stripping is an experimental/);
180+
match(result.stdout, /Hello, TypeScript!/);
181+
strictEqual(result.code, 0);
182+
});
183+
184+
test('code is throwing a non Error is rethrown', async () => {
185+
const result = await spawnPromisified(process.execPath, [
186+
'--experimental-strip-types',
187+
'--eval',
188+
`throw null;`]);
189+
doesNotMatch(result.stderr, /node:internal\/process\/execution/);
190+
strictEqual(result.stdout, '');
191+
strictEqual(result.code, 1);
192+
});
193+
194+
test('code is throwing an error with customized accessors', async () => {
195+
const result = await spawnPromisified(process.execPath, [
196+
'--experimental-strip-types',
197+
'--eval',
198+
`throw Object.defineProperty(new Error, "stack", { set() {throw this} });`]);
199+
200+
match(result.stderr, /Error/);
201+
match(result.stderr, /at \[eval\]:1:29/);
202+
strictEqual(result.stdout, '');
203+
strictEqual(result.code, 1);
204+
});
205+
206+
test('typescript code is throwing an error', async () => {
207+
const result = await spawnPromisified(process.execPath, [
208+
'--experimental-strip-types',
209+
'--eval',
210+
`const foo: string = 'Hello, TypeScript!'; throw new Error(foo);`]);
211+
212+
match(result.stderr, /Hello, TypeScript!/);
213+
strictEqual(result.stdout, '');
214+
strictEqual(result.code, 1);
215+
});
216+
217+
test('typescript ESM code is throwing a syntax error at runtime', async () => {
218+
const result = await spawnPromisified(process.execPath, [
219+
'--experimental-strip-types',
220+
'--eval',
221+
'import util from "node:util"; function foo(){}; throw new SyntaxError(foo<Number>(1));']);
222+
// Trick by passing ambiguous syntax to see if evaluated in TypeScript or JavaScript
223+
// If evaluated in JavaScript `foo<Number>(1)` is evaluated as `foo < Number > (1)`
224+
// result in false
225+
// If evaluated in TypeScript `foo<Number>(1)` is evaluated as `foo(1)`
226+
match(result.stderr, /SyntaxError: false/);
227+
strictEqual(result.stdout, '');
228+
strictEqual(result.code, 1);
229+
});
230+
231+
test('typescript CJS code is throwing a syntax error at runtime', async () => {
232+
const result = await spawnPromisified(process.execPath, [
233+
'--experimental-strip-types',
234+
'--eval',
235+
'const util = require("node:util"); function foo(){}; throw new SyntaxError(foo<Number>(1));']);
236+
// Trick by passing ambiguous syntax to see if evaluated in TypeScript or JavaScript
237+
// If evaluated in JavaScript `foo<Number>(1)` is evaluated as `foo < Number > (1)`
238+
// result in false
239+
// If evaluated in TypeScript `foo<Number>(1)` is evaluated as `foo(1)`
240+
match(result.stderr, /SyntaxError: false/);
241+
strictEqual(result.stdout, '');
242+
strictEqual(result.code, 1);
243+
});

‎test/fixtures/eval/eval_messages.snapshot

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ SyntaxError: Strict mode code may not include a with statement
1111

1212

1313

14+
1415
Node.js *
1516
42
1617
42

0 commit comments

Comments
 (0)
Please sign in to comment.