Skip to content

Commit ec519b5

Browse files
fixup! split CJS compile - execute
1 parent d82658c commit ec519b5

File tree

3 files changed

+150
-42
lines changed

3 files changed

+150
-42
lines changed

lib/internal/process/execution.js

Lines changed: 132 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -74,24 +74,14 @@ function evalModuleEntryPoint(source, print) {
7474
}
7575

7676
function evalScript(name, body, breakFirstLine, print, shouldLoadESM = false) {
77-
const CJSModule = require('internal/modules/cjs/loader').Module;
78-
79-
const cwd = tryGetCwd();
8077
const origModule = globalThis.module; // Set e.g. when called from the REPL.
81-
82-
const module = new CJSModule(name);
83-
module.filename = path.join(cwd, name);
84-
module.paths = CJSModule._nodeModulePaths(cwd);
85-
78+
const module = createModule(name);
8679
const baseUrl = pathToFileURL(module.filename).href;
8780

88-
if (getOptionValue('--experimental-detect-module') &&
89-
getOptionValue('--input-type') === '' &&
90-
containsModuleSyntax(body, name, null, 'no CJS variables')) {
91-
if (getOptionValue('--experimental-strip-types')) {
92-
return evalTypeScriptModuleEntryPoint(body, print);
93-
}
94-
return evalModuleEntryPoint(body, print);
81+
if (shouldUseModuleEntryPoint(name, body)) {
82+
return getOptionValue('--experimental-strip-types') ?
83+
evalTypeScriptModuleEntryPoint(body, print) :
84+
evalModuleEntryPoint(body, print);
9585
}
9686

9787
const runScript = () => {
@@ -106,23 +96,8 @@ function evalScript(name, body, breakFirstLine, print, shouldLoadESM = false) {
10696
globalThis.__filename = name;
10797
RegExpPrototypeExec(/^/, ''); // Necessary to reset RegExp statics before user code runs.
10898
const result = module._compile(script, `${name}-wrapper`)(() => {
109-
const hostDefinedOptionId = Symbol(name);
110-
async function importModuleDynamically(specifier, _, importAttributes) {
111-
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
112-
return cascadedLoader.import(specifier, baseUrl, importAttributes);
113-
}
114-
const script = makeContextifyScript(
115-
body, // code
116-
name, // filename,
117-
0, // lineOffset
118-
0, // columnOffset,
119-
undefined, // cachedData
120-
false, // produceCachedData
121-
undefined, // parsingContext
122-
hostDefinedOptionId, // hostDefinedOptionId
123-
importModuleDynamically, // importModuleDynamically
124-
);
125-
return runScriptInThisContext(script, true, !!breakFirstLine);
99+
const compiledScript = compileScript(name, body, baseUrl);
100+
return runScriptInThisContext(compiledScript, true, !!breakFirstLine);
126101
});
127102
if (print) {
128103
const { log } = require('internal/console/global');
@@ -284,16 +259,33 @@ function decorateCJSErrorWithTSMessage(originalStack, newMessage) {
284259
* @returns {void}
285260
*/
286261
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;
287273
try {
288-
evalScript(name, source, breakFirstLine, print, shouldLoadESM);
274+
compiledScript = compileScript(name, source, baseUrl);
289275
} catch (originalError) {
290276
// If it's not a SyntaxError, rethrow it.
291277
if (!isError(originalError) || originalError.name !== 'SyntaxError') {
292278
throw originalError;
293279
}
294280
try {
295-
const strippedSource = stripTypeScriptModuleTypes(source, name, false);
296-
evalScript(name, strippedSource, breakFirstLine, print, shouldLoadESM);
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);
297289
// Emit the experimental warning after the code was successfully evaluated.
298290
emitExperimentalWarning('Type Stripping');
299291
} catch (tsError) {
@@ -308,6 +300,20 @@ function evalTypeScript(name, source, breakFirstLine, print, shouldLoadESM = fal
308300
throw originalError;
309301
}
310302
}
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);
311317
}
312318

313319
/**
@@ -393,6 +399,97 @@ function parseAndEvalCommonjsTypeScript(name, source, breakFirstLine, print, sho
393399
evalScript(name, strippedSource, breakFirstLine, print, shouldLoadESM);
394400
}
395401

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') &&
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+
396493
module.exports = {
397494
parseAndEvalCommonjsTypeScript,
398495
parseAndEvalModuleTypeScript,

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

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -181,16 +181,12 @@ test('check warning is emitted when eval TypeScript CommonJS syntax', async () =
181181
strictEqual(result.code, 0);
182182
});
183183

184-
test('code is throwing a non Error', async () => {
184+
test('code is throwing a non Error is rethrown', async () => {
185185
const result = await spawnPromisified(process.execPath, [
186186
'--experimental-strip-types',
187187
'--eval',
188188
`throw null;`]);
189-
// TODO(marco-ippolito) fix the stack trace of non errors
190-
// being re-thrown
191-
// the stack trace is wrong because it is rethrown
192-
// but it's not an Error object
193-
match(result.stderr, /node:internal\/process\/execution/);
189+
doesNotMatch(result.stderr, /node:internal\/process\/execution/);
194190
strictEqual(result.stdout, '');
195191
strictEqual(result.code, 1);
196192
});
@@ -223,7 +219,21 @@ test('typescript ESM code is throwing a syntax error at runtime', async () => {
223219
'--experimental-strip-types',
224220
'--eval',
225221
'import util from "node:util"; function foo(){}; throw new SyntaxError(foo<Number>(1));']);
226-
// Trick by passing ambiguous syntax to trigger to see if evaluated in TypeScript or JavaScript
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
227237
// If evaluated in JavaScript `foo<Number>(1)` is evaluated as `foo < Number > (1)`
228238
// result in false
229239
// If evaluated in TypeScript `foo<Number>(1)` is evaluated as `foo(1)`

test/fixtures/eval/eval_messages.snapshot

Lines changed: 1 addition & 0 deletions
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)