-
Notifications
You must be signed in to change notification settings - Fork 30.7k
/
Copy pathexecution.js
475 lines (433 loc) Β· 16.9 KB
/
execution.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
'use strict';
const {
RegExpPrototypeExec,
StringPrototypeIndexOf,
StringPrototypeSlice,
Symbol,
globalThis,
} = primordials;
const path = require('path');
const {
codes: {
ERR_EVAL_ESM_CANNOT_PRINT,
ERR_INVALID_ARG_TYPE,
ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET,
},
} = require('internal/errors');
const { pathToFileURL } = require('internal/url');
const { exitCodes: { kGenericUserError } } = internalBinding('errors');
const { stripTypeScriptModuleTypes } = require('internal/modules/typescript');
const {
executionAsyncId,
clearDefaultTriggerAsyncId,
clearAsyncIdStack,
hasAsyncIdStack,
afterHooksExist,
emitAfter,
popAsyncContext,
} = require('internal/async_hooks');
const { containsModuleSyntax } = internalBinding('contextify');
const { getOptionValue } = require('internal/options');
const {
makeContextifyScript, runScriptInThisContext,
} = require('internal/vm');
const { emitExperimentalWarning } = require('internal/util');
// shouldAbortOnUncaughtToggle is a typed array for faster
// communication with JS.
const { shouldAbortOnUncaughtToggle } = internalBinding('util');
function tryGetCwd() {
try {
return process.cwd();
} catch {
// getcwd(3) can fail if the current working directory has been deleted.
// Fall back to the directory name of the (absolute) executable path.
// It's not really correct but what are the alternatives?
return path.dirname(process.execPath);
}
}
let evalIndex = 0;
function getEvalModuleUrl() {
return `${pathToFileURL(process.cwd())}/[eval${++evalIndex}]`;
}
/**
* Evaluate an ESM entry point and return the promise that gets fulfilled after
* it finishes evaluation.
* @param {string} source Source code the ESM
* @param {boolean} print Whether the result should be printed.
* @returns {Promise}
*/
function evalModuleEntryPoint(source, print) {
if (print) {
throw new ERR_EVAL_ESM_CANNOT_PRINT();
}
RegExpPrototypeExec(/^/, ''); // Necessary to reset RegExp statics before user code runs.
return require('internal/modules/run_main').runEntryPointWithESMLoader(
(loader) => loader.eval(source, getEvalModuleUrl(), true),
);
}
function evalScript(name, body, breakFirstLine, print, shouldLoadESM = false) {
const origModule = globalThis.module; // Set e.g. when called from the REPL.
const module = createModule(name);
const baseUrl = pathToFileURL(module.filename).href;
if (shouldUseModuleEntryPoint(name, body)) {
return getOptionValue('--experimental-strip-types') ?
evalTypeScriptModuleEntryPoint(body, print) :
evalModuleEntryPoint(body, print);
}
const evalFunction = () => runScriptInContext(name,
body,
breakFirstLine,
print,
module,
baseUrl,
undefined,
origModule);
if (shouldLoadESM) {
return require('internal/modules/run_main').runEntryPointWithESMLoader(evalFunction);
}
evalFunction();
}
const exceptionHandlerState = {
captureFn: null,
reportFlag: false,
};
function setUncaughtExceptionCaptureCallback(fn) {
if (fn === null) {
exceptionHandlerState.captureFn = fn;
shouldAbortOnUncaughtToggle[0] = 1;
process.report.reportOnUncaughtException = exceptionHandlerState.reportFlag;
return;
}
if (typeof fn !== 'function') {
throw new ERR_INVALID_ARG_TYPE('fn', ['Function', 'null'], fn);
}
if (exceptionHandlerState.captureFn !== null) {
throw new ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET();
}
exceptionHandlerState.captureFn = fn;
shouldAbortOnUncaughtToggle[0] = 0;
exceptionHandlerState.reportFlag =
process.report.reportOnUncaughtException === true;
process.report.reportOnUncaughtException = false;
}
function hasUncaughtExceptionCaptureCallback() {
return exceptionHandlerState.captureFn !== null;
}
function noop() {}
// XXX(joyeecheung): for some reason this cannot be defined at the top-level
// and exported to be written to process._fatalException, it has to be
// returned as an *anonymous function* wrapped inside a factory function,
// otherwise it breaks the test-timers.setInterval async hooks test -
// this may indicate that node::errors::TriggerUncaughtException() should
// fix up the callback scope before calling into process._fatalException,
// or this function should take extra care of the async hooks before it
// schedules a setImmediate.
function createOnGlobalUncaughtException() {
// The C++ land node::errors::TriggerUncaughtException() will
// exit the process if it returns false, and continue execution if it
// returns true (which indicates that the exception is handled by the user).
return (er, fromPromise) => {
// It's possible that defaultTriggerAsyncId was set for a constructor
// call that threw and was never cleared. So clear it now.
clearDefaultTriggerAsyncId();
const type = fromPromise ? 'unhandledRejection' : 'uncaughtException';
process.emit('uncaughtExceptionMonitor', er, type);
if (exceptionHandlerState.captureFn !== null) {
exceptionHandlerState.captureFn(er);
} else if (!process.emit('uncaughtException', er, type)) {
// If someone handled it, then great. Otherwise, die in C++ land
// since that means that we'll exit the process, emit the 'exit' event.
try {
if (!process._exiting) {
process._exiting = true;
process.exitCode = kGenericUserError;
process.emit('exit', kGenericUserError);
}
} catch {
// Nothing to be done about it at this point.
}
return false;
}
// If we handled an error, then make sure any ticks get processed
// by ensuring that the next Immediate cycle isn't empty.
require('timers').setImmediate(noop);
// Emit the after() hooks now that the exception has been handled.
if (afterHooksExist()) {
do {
const asyncId = executionAsyncId();
if (asyncId === 0)
popAsyncContext(0);
else
emitAfter(asyncId);
} while (hasAsyncIdStack());
}
// And completely empty the id stack, including anything that may be
// cached on the native side.
clearAsyncIdStack();
return true;
};
}
function readStdin(callback) {
process.stdin.setEncoding('utf8');
let code = '';
process.stdin.on('data', (d) => {
code += d;
});
process.stdin.on('end', () => {
callback(code);
});
}
/**
* Adds the TS message to the error stack.
*
* At the 3rd line of the stack, the message is added.
* @param {string} originalStack The stack to decorate
* @param {string} newMessage the message to add to the error stack
* @returns {void}
*/
function decorateCJSErrorWithTSMessage(originalStack, newMessage) {
let index;
for (let i = 0; i < 3; i++) {
index = StringPrototypeIndexOf(originalStack, '\n', index + 1);
}
return StringPrototypeSlice(originalStack, 0, index) +
'\n' + newMessage +
StringPrototypeSlice(originalStack, index);
}
/**
*
* Wrapper of evalScript
*
* This function wraps the evaluation of the source code in a try-catch block.
* If the source code fails to be evaluated, it will retry evaluating the source code
* with the TypeScript parser.
*
* If the source code fails to be evaluated with the TypeScript parser,
* it will rethrow the original error, adding the TypeScript error message to the stack.
*
* This way we don't change the behavior of the code, but we provide a better error message
* in case of a typescript error.
* @param {string} name The name of the file
* @param {string} source The source code to evaluate
* @param {boolean} breakFirstLine Whether to break on the first line
* @param {boolean} print If the result should be printed
* @param {boolean} shouldLoadESM If the code should be loaded as an ESM module
* @returns {void}
*/
function evalTypeScript(name, source, breakFirstLine, print, shouldLoadESM = false) {
const origModule = globalThis.module; // Set e.g. when called from the REPL.
const module = createModule(name);
const baseUrl = pathToFileURL(module.filename).href;
if (shouldUseModuleEntryPoint(name, source)) {
return evalTypeScriptModuleEntryPoint(source, print);
}
let compiledScript;
// This variable can be modified if the source code is stripped.
let sourceToRun = source;
try {
compiledScript = compileScript(name, source, baseUrl);
} catch (originalError) {
try {
sourceToRun = stripTypeScriptModuleTypes(source, name, false);
// Retry the CJS/ESM syntax detection after stripping the types.
if (shouldUseModuleEntryPoint(name, sourceToRun)) {
return evalTypeScriptModuleEntryPoint(source, print);
}
// If the ContextifiedScript was successfully created, execute it.
// outside the try-catch block to avoid catching runtime errors.
compiledScript = compileScript(name, sourceToRun, baseUrl);
// Emit the experimental warning after the code was successfully evaluated.
emitExperimentalWarning('Type Stripping');
} catch (tsError) {
// If it's invalid or unsupported TypeScript syntax, rethrow the original error
// with the TypeScript error message added to the stack.
if (tsError.code === 'ERR_INVALID_TYPESCRIPT_SYNTAX' || tsError.code === 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX') {
originalError.stack = decorateCJSErrorWithTSMessage(originalError.stack, tsError.message);
throw originalError;
}
throw tsError;
}
}
const evalFunction = () => runScriptInContext(name,
sourceToRun,
breakFirstLine,
print,
module,
baseUrl,
compiledScript,
origModule);
if (shouldLoadESM) {
return require('internal/modules/run_main').runEntryPointWithESMLoader(evalFunction);
}
evalFunction();
}
/**
* Wrapper of evalModuleEntryPoint
*
* This function wraps the compilation of the source code in a try-catch block.
* If the source code fails to be compiled, it will retry transpiling the source code
* with the TypeScript parser.
* @param {string} source The source code to evaluate
* @param {boolean} print If the result should be printed
* @returns {Promise} The module evaluation promise
*/
function evalTypeScriptModuleEntryPoint(source, print) {
if (print) {
throw new ERR_EVAL_ESM_CANNOT_PRINT();
}
RegExpPrototypeExec(/^/, ''); // Necessary to reset RegExp statics before user code runs.
return require('internal/modules/run_main').runEntryPointWithESMLoader(
async (loader) => {
const url = getEvalModuleUrl();
let moduleWrap;
try {
// Compile the module to check for syntax errors.
moduleWrap = loader.createModuleWrap(source, url);
} catch (originalError) {
try {
const strippedSource = stripTypeScriptModuleTypes(source, url, false);
// If the moduleWrap was successfully created, execute the module job.
// outside the try-catch block to avoid catching runtime errors.
moduleWrap = loader.createModuleWrap(strippedSource, url);
// Emit the experimental warning after the code was successfully compiled.
emitExperimentalWarning('Type Stripping');
} catch (tsError) {
// If it's invalid or unsupported TypeScript syntax, rethrow the original error
// with the TypeScript error message added to the stack.
if (tsError.code === 'ERR_INVALID_TYPESCRIPT_SYNTAX' ||
tsError.code === 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX') {
originalError.stack = `${tsError.message}\n\n${originalError.stack}`;
throw originalError;
}
throw tsError;
}
}
// If the moduleWrap was successfully created either with by just compiling
// or after transpilation, execute the module job.
return loader.executeModuleJob(url, moduleWrap, true);
},
);
};
/**
*
* Function used to shortcut when `--input-type=module-typescript` is set.
* @param {string} source
* @param {boolean} print
*/
function parseAndEvalModuleTypeScript(source, print) {
// We know its a TypeScript module, we can safely emit the experimental warning.
const strippedSource = stripTypeScriptModuleTypes(source, getEvalModuleUrl());
evalModuleEntryPoint(strippedSource, print);
}
/**
* Function used to shortcut when `--input-type=commonjs-typescript` is set
* @param {string} name The name of the file
* @param {string} source The source code to evaluate
* @param {boolean} breakFirstLine Whether to break on the first line
* @param {boolean} print If the result should be printed
* @param {boolean} shouldLoadESM If the code should be loaded as an ESM module
* @returns {void}
*/
function parseAndEvalCommonjsTypeScript(name, source, breakFirstLine, print, shouldLoadESM = false) {
// We know its a TypeScript module, we can safely emit the experimental warning.
const strippedSource = stripTypeScriptModuleTypes(source, getEvalModuleUrl());
evalScript(name, strippedSource, breakFirstLine, print, shouldLoadESM);
}
/**
*
* @param {string} name - The filename of the script.
* @param {string} body - The code of the script.
* @param {string} baseUrl Path of the parent importing the module.
* @returns {ContextifyScript} The created contextify script.
*/
function compileScript(name, body, baseUrl) {
const hostDefinedOptionId = Symbol(name);
async function importModuleDynamically(specifier, _, importAttributes) {
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
return cascadedLoader.import(specifier, baseUrl, importAttributes);
}
return makeContextifyScript(
body, // code
name, // filename,
0, // lineOffset
0, // columnOffset,
undefined, // cachedData
false, // produceCachedData
undefined, // parsingContext
hostDefinedOptionId, // hostDefinedOptionId
importModuleDynamically, // importModuleDynamically
);
}
/**
* @param {string} name - The filename of the script.
* @param {string} body - The code of the script.
* @returns {boolean} Whether the module entry point should be evaluated as a module.
*/
function shouldUseModuleEntryPoint(name, body) {
return getOptionValue('--experimental-detect-module') &&
getOptionValue('--input-type') === '' &&
containsModuleSyntax(body, name, null, 'no CJS variables');
}
/**
*
* @param {string} name - The filename of the script.
* @returns {import('internal/modules/esm/loader').CJSModule} The created module.
*/
function createModule(name) {
const CJSModule = require('internal/modules/cjs/loader').Module;
const cwd = tryGetCwd();
const module = new CJSModule(name);
module.filename = path.join(cwd, name);
module.paths = CJSModule._nodeModulePaths(cwd);
return module;
}
/**
*
* @param {string} name - The filename of the script.
* @param {string} body - The code of the script.
* @param {boolean} breakFirstLine Whether to break on the first line
* @param {boolean} print If the result should be printed
* @param {import('internal/modules/esm/loader').CJSModule} module The module
* @param {string} baseUrl Path of the parent importing the module.
* @param {object} compiledScript The compiled script.
* @param {any} origModule The original module.
* @returns {void}
*/
function runScriptInContext(name, body, breakFirstLine, print, module, baseUrl, compiledScript, origModule) {
// Create wrapper for cache entry
const script = `
globalThis.module = module;
globalThis.exports = exports;
globalThis.__dirname = __dirname;
globalThis.require = require;
return (main) => main();
`;
globalThis.__filename = name;
RegExpPrototypeExec(/^/, ''); // Necessary to reset RegExp statics before user code runs.
const result = module._compile(script, `${name}-wrapper`)(() => {
// If the script was already compiled, use it.
return runScriptInThisContext(
compiledScript ?? compileScript(name, body, baseUrl),
true, !!breakFirstLine);
});
if (print) {
const { log } = require('internal/console/global');
process.on('exit', () => {
log(result);
});
}
if (origModule !== undefined)
globalThis.module = origModule;
}
module.exports = {
parseAndEvalCommonjsTypeScript,
parseAndEvalModuleTypeScript,
readStdin,
tryGetCwd,
evalModuleEntryPoint,
evalTypeScript,
evalScript,
onGlobalUncaughtException: createOnGlobalUncaughtException(),
setUncaughtExceptionCaptureCallback,
hasUncaughtExceptionCaptureCallback,
};