ts-node-lite
Version:
TypeScript execution environment for node.js, with source map support
433 lines (427 loc) • 19.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.bootstrap = exports.main = void 0;
const path_1 = require("path");
require("util");
const Module = require("module");
let arg;
const util_1 = require("./util");
const index_1 = require("./index");
require("../dist-raw/node-internal-modules-cjs-helpers");
const spawn_child_1 = require("./child/spawn-child");
const configuration_1 = require("./configuration");
/**
* Main `bin` functionality.
*
* This file is split into a chain of functions (phases), each one adding to a shared state object.
* This is done so that the next function can either be invoked in-process or, if necessary, invoked in a child process.
*
* The functions are intentionally given uncreative names and left in the same order as the original code, to make a
* smaller git diff.
*
* @internal
*/
function main(argv = process.argv.slice(2), entrypointArgs = {}) {
const args = parseArgv(argv, entrypointArgs);
const state = {
shouldUseChildProcess: false,
isInChildProcess: false,
isCli: true,
tsNodeScript: __filename,
parseArgvResult: args,
};
return bootstrap(state);
}
exports.main = main;
/** @internal */
function bootstrap(state) {
if (!state.phase2Result) {
state.phase2Result = phase2(state);
if (state.shouldUseChildProcess && !state.isInChildProcess) {
// Note: When transitioning into the child-process after `phase2`,
// the updated working directory needs to be preserved.
return (0, spawn_child_1.callInChild)(state);
}
}
if (!state.phase3Result) {
state.phase3Result = phase3(state);
if (state.shouldUseChildProcess && !state.isInChildProcess) {
// Note: When transitioning into the child-process after `phase2`,
// the updated working directory needs to be preserved.
return (0, spawn_child_1.callInChild)(state);
}
}
return phase4(state);
}
exports.bootstrap = bootstrap;
function parseArgv(argv, entrypointArgs) {
arg ?? (arg = require('arg'));
const args = {
...entrypointArgs,
...arg({
// Node.js-like options.
'--require': [String],
// CLI options.
'--help': Boolean,
'--cwdMode': Boolean,
'--scriptMode': Boolean,
'--version': arg.COUNT,
'--showConfig': Boolean,
'--esm': Boolean,
// Project options.
'--cwd': String,
'--files': Boolean,
'--compiler': String,
'--compilerOptions': util_1.parse,
'--project': String,
'--ignoreDiagnostics': [String],
'--ignore': [String],
'--transpileOnly': Boolean,
'--typeCheck': Boolean,
'--compilerHost': Boolean,
'--pretty': Boolean,
'--skipProject': Boolean,
'--skipIgnore': Boolean,
'--preferTsExts': Boolean,
'--logError': Boolean,
'--emit': Boolean,
'--scope': Boolean,
'--scopeDir': String,
'--experimentalSpecifierResolution': String,
// Aliases.
'-r': '--require',
'-h': '--help',
'-s': '--script-mode',
'-v': '--version',
'-T': '--transpileOnly',
'-H': '--compilerHost',
'-I': '--ignore',
'-P': '--project',
'-C': '--compiler',
'-D': '--ignoreDiagnostics',
'-O': '--compilerOptions',
'--dir': '--cwd',
// Support both tsc-style camelCase and node-style hypen-case for *all* flags
'--cwd-mode': '--cwdMode',
'--script-mode': '--scriptMode',
'--show-config': '--showConfig',
'--compiler-options': '--compilerOptions',
'--ignore-diagnostics': '--ignoreDiagnostics',
'--transpile-only': '--transpileOnly',
'--type-check': '--typeCheck',
'--compiler-host': '--compilerHost',
'--skip-project': '--skipProject',
'--skip-ignore': '--skipIgnore',
'--prefer-ts-exts': '--preferTsExts',
'--log-error': '--logError',
'--scope-dir': '--scopeDir',
'--experimental-specifier-resolution': '--experimentalSpecifierResolution',
}, {
argv,
stopAtPositional: true,
}),
};
// Only setting defaults for CLI-specific flags
// Anything passed to `register()` can be `undefined`; `create()` will apply
// defaults.
const { '--cwd': cwdArg, '--help': help = false, '--scriptMode': scriptMode, '--cwdMode': cwdMode, '--version': version = 0, '--showConfig': showConfig, '--require': argsRequire = [], '--files': files, '--compiler': compiler, '--compilerOptions': compilerOptions, '--project': project, '--ignoreDiagnostics': ignoreDiagnostics, '--ignore': ignore, '--transpileOnly': transpileOnly, '--typeCheck': typeCheck, '--compilerHost': compilerHost, '--pretty': pretty, '--skipProject': skipProject, '--skipIgnore': skipIgnore, '--preferTsExts': preferTsExts, '--logError': logError, '--emit': emit, '--scope': scope = undefined, '--scopeDir': scopeDir = undefined, '--experimentalSpecifierResolution': experimentalSpecifierResolution, '--esm': esm, _: restArgs, } = args;
return {
// Note: argv and restArgs may be overwritten by child process
argv: process.argv,
restArgs,
cwdArg,
help,
scriptMode,
cwdMode,
version,
showConfig,
argsRequire,
files,
compiler,
compilerOptions,
project,
ignoreDiagnostics,
ignore,
transpileOnly,
typeCheck,
compilerHost,
pretty,
skipProject,
skipIgnore,
preferTsExts,
logError,
emit,
scope,
scopeDir,
experimentalSpecifierResolution,
esm,
};
}
function phase2(payload) {
const { help, version, cwdArg, esm } = payload.parseArgvResult;
if (help) {
console.log(`
Usage: ts-node [options] [ script.ts ] [arguments]
Options:
-r, --require [path] Require a node module before execution
--esm Bootstrap with the ESM loader, enabling full ESM support
-h, --help Print CLI usage
-v, --version Print module version information. -vvv to print additional information
--showConfig Print resolved configuration and exit
-T, --transpileOnly Use TypeScript's faster \`transpileModule\`
-H, --compilerHost Use TypeScript's compiler host API
-I, --ignore [pattern] Override the path patterns to skip compilation
-P, --project [path] Path to TypeScript JSON project file
-C, --compiler [name] Specify a custom TypeScript compiler
-D, --ignoreDiagnostics [code] Ignore TypeScript warnings by diagnostic code
-O, --compilerOptions [opts] JSON object to merge with compiler options
--cwd Behave as if invoked within this working directory.
--files Load \`files\`, \`include\` and \`exclude\` from \`tsconfig.json\` on startup
--pretty Use pretty diagnostic formatter (usually enabled by default)
--cwdMode Use current directory instead of <script.ts> for config resolution
--skipProject Skip reading \`tsconfig.json\`
--skipIgnore Skip \`--ignore\` checks
--emit Emit output files into \`.ts-node\` directory
--scope Scope compiler to files within \`scopeDir\`. Anything outside this directory is ignored.
--scopeDir Directory for \`--scope\`
--preferTsExts Prefer importing TypeScript files over JavaScript files
--logError Logs TypeScript errors to stderr instead of throwing exceptions
--experimentalSpecifierResolution [node|explicit]
Equivalent to node's --experimental-specifier-resolution
`);
process.exit(0);
}
// Output project information.
if (version === 1) {
console.log(`v${index_1.VERSION}`);
process.exit(0);
}
const cwd = cwdArg ? (0, path_1.resolve)(cwdArg) : process.cwd();
// If ESM is explicitly enabled through the flag, stage3 should be run in a child process
// with the ESM loaders configured.
if (esm)
payload.shouldUseChildProcess = true;
return {
cwd,
};
}
function phase3(payload) {
const { emit, files, pretty, transpileOnly, typeCheck, compilerHost, ignore, preferTsExts, logError, scriptMode, cwdMode, project, skipProject, skipIgnore, compiler, ignoreDiagnostics, compilerOptions, argsRequire, scope, scopeDir, esm, experimentalSpecifierResolution, } = payload.parseArgvResult;
const { cwd } = payload.phase2Result;
// NOTE: When we transition to a child process for ESM, the entry-point script determined
// here might not be the one used later in `phase4`. This can happen when we execute the
// original entry-point but then the process forks itself using e.g. `child_process.fork`.
// We will always use the original TS project in forked processes anyway, so it is
// expected and acceptable to retrieve the entry-point information here in `phase2`.
// See: https://github.com/TypeStrong/ts-node/issues/1812.
const { entryPointPath } = getEntryPointInfo(payload);
const preloadedConfig = (0, configuration_1.findAndReadConfig)({
cwd,
emit,
files,
pretty,
transpileOnly: transpileOnly ?? undefined,
typeCheck,
compilerHost,
ignore,
logError,
projectSearchDir: getProjectSearchDir(cwd, scriptMode, cwdMode, entryPointPath),
project,
skipProject,
skipIgnore,
compiler,
ignoreDiagnostics,
compilerOptions,
require: argsRequire,
scope,
scopeDir,
preferTsExts,
esm,
experimentalSpecifierResolution: experimentalSpecifierResolution,
});
// If ESM is enabled through the parsed tsconfig, stage4 should be run in a child
// process with the ESM loaders configured.
if (preloadedConfig.options.esm)
payload.shouldUseChildProcess = true;
return { preloadedConfig };
}
/**
* Determines the entry-point information from the argv and phase2 result. This
* method will be invoked in two places:
*
* 1. In phase 3 to be able to find a project from the potential entry-point script.
* 2. In phase 4 to determine the actual entry-point script.
*
* Note that we need to explicitly re-resolve the entry-point information in the final
* stage because the previous stage information could be modified when the bootstrap
* invocation transitioned into a child process for ESM.
*
* Stages before (phase 4) can and will be cached by the child process through the Brotli
* configuration and entry-point information is only reliable in the final phase. More
* details can be found in here: https://github.com/TypeStrong/ts-node/issues/1812.
*/
function getEntryPointInfo(state) {
const { restArgs } = state.parseArgvResult;
const { cwd } = state.phase2Result;
const { isCli } = state;
const executeEntrypoint = restArgs.length > 0;
/**
* Unresolved. May point to a symlink, not realpath. May be missing file extension
* NOTE: resolution relative to cwd option (not `process.cwd()`) is legacy backwards-compat; should be changed in next major: https://github.com/TypeStrong/ts-node/issues/1834
*/
const entryPointPath = executeEntrypoint ? (isCli ? (0, path_1.resolve)(cwd, restArgs[0]) : (0, path_1.resolve)(restArgs[0])) : undefined;
return {
executeEntrypoint,
entryPointPath,
};
}
function phase4(payload) {
const { isInChildProcess, tsNodeScript } = payload;
const { version, showConfig, restArgs, argv } = payload.parseArgvResult;
const { cwd } = payload.phase2Result;
const { preloadedConfig } = payload.phase3Result;
const { entryPointPath, executeEntrypoint } = getEntryPointInfo(payload);
// Register the TypeScript compiler instance.
const service = (0, index_1.createFromPreloadedConfig)({
// Since this struct may have been marshalled across thread or process boundaries, we must restore
// un-marshall-able values.
...preloadedConfig,
options: {
...preloadedConfig.options,
readFile: undefined,
fileExists: undefined,
tsTrace: index_1.DEFAULTS.tsTrace,
},
});
(0, index_1.register)(service);
if (isInChildProcess)
require('./child/child-loader').lateBindHooks((0, index_1.createEsmHooks)(service));
// Output project information.
if (version === 2) {
console.log(`ts-node v${index_1.VERSION}`);
console.log(`node ${process.version}`);
console.log(`compiler v${service.ts.version}`);
process.exit(0);
}
if (version >= 3) {
console.log(`ts-node v${index_1.VERSION} ${(0, path_1.dirname)(__dirname)}`);
console.log(`node ${process.version}`);
console.log(`compiler v${service.ts.version} ${service.compilerPath ?? ''}`);
process.exit(0);
}
if (showConfig) {
const ts = service.ts;
if (typeof ts.convertToTSConfig !== 'function') {
console.error('Error: --showConfig requires a typescript versions >=3.2 that support --showConfig');
process.exit(1);
}
let moduleTypes = undefined;
if (service.options.moduleTypes) {
// Assumption: this codepath requires CLI invocation, so moduleTypes must have come from a tsconfig, not API.
const showRelativeTo = (0, path_1.dirname)(service.configFilePath);
moduleTypes = {};
for (const [key, value] of Object.entries(service.options.moduleTypes)) {
moduleTypes[(0, path_1.relative)(showRelativeTo, (0, path_1.resolve)(service.options.optionBasePaths?.moduleTypes, key))] = value;
}
}
const json = {
['ts-node']: {
...service.options,
require: service.options.require?.length ? service.options.require : undefined,
moduleTypes,
optionBasePaths: undefined,
compilerOptions: undefined,
project: service.configFilePath ?? service.options.project,
},
...ts.convertToTSConfig(service.config, service.configFilePath ?? (0, path_1.join)(cwd, 'ts-node-implicit-tsconfig.json'), service.ts.sys),
};
console.log(
// Assumes that all configuration options which can possibly be specified via the CLI are JSON-compatible.
// If, in the future, we must log functions, for example readFile and fileExists, then we can implement a JSON
// replacer function.
JSON.stringify(json, null, 2));
process.exit(0);
}
// Prepend `ts-node` arguments to CLI for child processes.
process.execArgv.push(tsNodeScript, ...argv.slice(2, argv.length - restArgs.length));
// TODO this comes from BootstrapState
process.argv = [process.argv[1]]
.concat(executeEntrypoint ? [entryPointPath] : [])
.concat(restArgs.slice(executeEntrypoint ? 1 : 0));
// Execute the main contents (either eval, script or piped).
if (executeEntrypoint) {
if (payload.isInChildProcess && (0, util_1.versionGteLt)(process.versions.node, '18.6.0', '18.7.0')) {
// HACK workaround node regression
require('../dist-raw/runmain-hack.js').run(entryPointPath);
}
else {
Module.runMain();
}
}
else {
}
}
/**
* Get project search path from args.
*/
function getProjectSearchDir(cwd, scriptMode, cwdMode, scriptPath) {
// Validate `--script-mode` / `--cwd-mode` / `--cwd` usage is correct.
if (scriptMode && cwdMode) {
throw new TypeError('--cwd-mode cannot be combined with --script-mode');
}
if (scriptMode && !scriptPath) {
throw new TypeError('--script-mode must be used with a script name, e.g. `ts-node --script-mode <script.ts>`');
}
const doScriptMode = scriptMode === true ? true : cwdMode === true ? false : !!scriptPath;
if (doScriptMode) {
// Use node's own resolution behavior to ensure we follow symlinks.
// scriptPath may omit file extension or point to a directory with or without package.json.
// This happens before we are registered, so we tell node's resolver to consider ts, tsx, and jsx files.
// In extremely rare cases, is is technically possible to resolve the wrong directory,
// because we do not yet know preferTsExts, jsx, nor allowJs.
// See also, justification why this will not happen in real-world situations:
// https://github.com/TypeStrong/ts-node/pull/1009#issuecomment-613017081
const exts = ['.js', '.jsx', '.ts', '.tsx'];
const extsTemporarilyInstalled = [];
for (const ext of exts) {
if (!(0, util_1.hasOwnProperty)(require.extensions, ext)) {
extsTemporarilyInstalled.push(ext);
require.extensions[ext] = function () { };
}
}
try {
return (0, path_1.dirname)(requireResolveNonCached(scriptPath));
}
finally {
for (const ext of extsTemporarilyInstalled) {
delete require.extensions[ext];
}
}
}
return cwd;
}
const guaranteedNonexistentDirectoryPrefix = (0, path_1.resolve)(__dirname, 'doesnotexist');
let guaranteedNonexistentDirectorySuffix = 0;
/**
* require.resolve an absolute path, tricking node into *not* caching the results.
* Necessary so that we do not pollute require.resolve cache prior to installing require.extensions
*
* Is a terrible hack, because node does not expose the necessary cache invalidation APIs
* https://stackoverflow.com/questions/59865584/how-to-invalidate-cached-require-resolve-results
*/
function requireResolveNonCached(absoluteModuleSpecifier) {
const { dir, base } = (0, path_1.parse)(absoluteModuleSpecifier);
const relativeModuleSpecifier = `./${base}`;
const req = Module.createRequire((0, path_1.join)(dir, 'imaginaryUncacheableRequireResolveScript'));
return req.resolve(relativeModuleSpecifier, {
paths: [
`${guaranteedNonexistentDirectoryPrefix}${guaranteedNonexistentDirectorySuffix++}`,
...(req.resolve.paths(relativeModuleSpecifier) || []),
],
});
}
if (require.main === module) {
main();
}
//# sourceMappingURL=bin.js.map