-
-
Notifications
You must be signed in to change notification settings - Fork 532
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
efcd2fd
commit d62db81
Showing
3 changed files
with
422 additions
and
363 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,376 @@ | ||
import { join, resolve } from 'path' | ||
import { start } from 'repl' | ||
import { inspect } from 'util' | ||
import Module = require('module') | ||
import minimist = require('minimist') | ||
import chalk = require('chalk') | ||
import { diffLines } from 'diff' | ||
import { createScript } from 'vm' | ||
import { register, VERSION, getFile, getVersion, getFileExists, TSError } from './index' | ||
|
||
interface Argv { | ||
eval?: string | ||
print?: string | ||
fast?: boolean | ||
version?: boolean | ||
help?: boolean | ||
compiler?: string | ||
project?: string | ||
ignoreWarnings?: string | string[] | ||
disableWarnings?: boolean | ||
noProject?: boolean | ||
compilerOptions?: any | ||
_: string[] | ||
} | ||
|
||
const strings = ['eval', 'print', 'compiler', 'project', 'ignoreWarnings'] | ||
const booleans = ['help', 'fast', 'version', 'disableWarnings', 'noProject'] | ||
|
||
const aliases: { [key: string]: string[] } = { | ||
help: ['h'], | ||
fast: ['f'], | ||
version: ['v'], | ||
eval: ['e'], | ||
print: ['p'], | ||
project: ['P'], | ||
compiler: ['c'], | ||
ignoreWarnings: ['i', 'ignore-warnings'], | ||
disableWarnings: ['d', 'disable-warnings'], | ||
noProject: ['n', 'no-project'], | ||
compilerOptions: ['o', 'compiler-options'] | ||
} | ||
|
||
let stop = process.argv.length | ||
|
||
function isFlagOnly (arg: string) { | ||
const name = arg.replace(/^--?/, '') | ||
|
||
// The value is part of this argument. | ||
if (/=/.test(name)) { | ||
return true | ||
} | ||
|
||
for (const bool of booleans) { | ||
if (name === bool) { | ||
return true | ||
} | ||
|
||
const alias = aliases[name] | ||
|
||
if (alias) { | ||
for (const other of alias) { | ||
if (other === name) { | ||
return true | ||
} | ||
} | ||
} | ||
} | ||
|
||
return false | ||
} | ||
|
||
// Hack around known subarg issue with `stopEarly`. | ||
for (let i = 2; i < process.argv.length; i++) { | ||
const arg = process.argv[i] | ||
const next = process.argv[i + 1] | ||
|
||
if (/^\[/.test(arg) || /\]$/.test(arg)) { | ||
continue | ||
} | ||
|
||
if (/^-/.test(arg)) { | ||
// Skip next argument. | ||
if (!isFlagOnly(arg) && !/^-/.test(next)) { | ||
i++ | ||
} | ||
|
||
continue | ||
} | ||
|
||
stop = i | ||
break | ||
} | ||
|
||
const argv = minimist<Argv>(process.argv.slice(2, stop), { | ||
string: strings, | ||
boolean: booleans, | ||
alias: aliases | ||
}) | ||
|
||
if (argv.version) { | ||
console.log(VERSION) | ||
process.exit(0) | ||
} | ||
|
||
if (argv.help) { | ||
console.log(` | ||
Usage: ts-node [options] [ -e script | script.ts ] [arguments] | ||
Options: | ||
-e, --eval [code] Evaluate code | ||
-p, --print [code] Evaluate code and print result | ||
-c, --compiler [name] Specify a custom TypeScript compiler | ||
-i, --ignoreWarnings [codes] Ignore TypeScript warnings by diagnostic code | ||
-d, --disableWarnings Ignore every TypeScript warning | ||
-n, --noProject Ignore the "tsconfig.json" project file | ||
-P, --project [path] Specify the path to the TypeScript project | ||
`) | ||
|
||
process.exit(0) | ||
} | ||
|
||
/** | ||
* Override `process.emit` for clearer compiler errors. | ||
*/ | ||
const _emit = process.emit | ||
|
||
process.emit = function (type, error): boolean { | ||
// Print the error message when no other listeners are present. | ||
if (type === 'uncaughtException' && error instanceof TSError && process.listeners(type).length === 0) { | ||
printAndExit(error) | ||
} | ||
|
||
return _emit.apply(this, arguments) | ||
} | ||
|
||
const cwd = process.cwd() | ||
const code = argv.eval == null ? argv.print : argv.eval | ||
const isEvalScript = typeof argv.eval === 'string' || !!argv.print // Minimist struggles with empty strings. | ||
const isEval = isEvalScript || stop === process.argv.length | ||
const isPrinted = argv.print != null | ||
|
||
// Register the TypeScript compiler instance. | ||
const service = register({ | ||
getFile: isEval ? getFileEval : getFile, | ||
getVersion: isEval ? getVersionEval : getVersion, | ||
getFileExists: isEval ? getFileExistsEval : getFileExists, | ||
fast: argv.fast, | ||
compiler: argv.compiler, | ||
ignoreWarnings: list(argv.ignoreWarnings), | ||
project: argv.project, | ||
disableWarnings: argv.disableWarnings, | ||
noProject: argv.noProject, | ||
compilerOptions: argv.compilerOptions | ||
}) | ||
|
||
// TypeScript files must always end with `.ts`. | ||
const EVAL_FILENAME = '[eval].ts' | ||
const EVAL_PATH = join(cwd, EVAL_FILENAME) | ||
|
||
// Store eval contents for in-memory lookups. | ||
const evalFile = { input: '', output: '', version: 0 } | ||
|
||
if (isEvalScript) { | ||
evalAndExit(code, isPrinted) | ||
} else { | ||
if (stop < process.argv.length) { | ||
const args = process.argv.slice(stop) | ||
args[0] = resolve(cwd, args[0]) | ||
process.argv = ['node'].concat(args) | ||
process.execArgv.unshift(__filename) | ||
Module.runMain() | ||
} else { | ||
// Piping of execution _only_ occurs when no other script is specified. | ||
if ((process.stdin as any).isTTY) { | ||
startRepl() | ||
} else { | ||
let code = '' | ||
process.stdin.on('data', (chunk: Buffer) => code += chunk) | ||
process.stdin.on('end', () => evalAndExit(code, isPrinted)) | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Evaluate a script. | ||
*/ | ||
function evalAndExit (code: string, isPrinted: boolean) { | ||
global.__filename = EVAL_FILENAME | ||
global.__dirname = cwd | ||
|
||
const module = new Module(global.__filename) | ||
module.filename = global.__filename | ||
module.paths = Module._nodeModulePaths(global.__dirname) | ||
|
||
global.exports = module.exports | ||
global.module = module | ||
global.require = module.require.bind(module) | ||
|
||
let result: any | ||
|
||
try { | ||
result = _eval(code, global) | ||
} catch (error) { | ||
if (error instanceof TSError) { | ||
printAndExit(error) | ||
} | ||
|
||
throw error | ||
} | ||
|
||
if (isPrinted) { | ||
console.log(typeof result === 'string' ? result : inspect(result)) | ||
} | ||
|
||
process.exit(0) | ||
} | ||
|
||
/** | ||
* Stringify the `TSError` instance. | ||
*/ | ||
function print (error: TSError) { | ||
return chalk.bold(`${chalk.red('⨯')} Unable to compile TypeScript`) + `\n${error.diagnostics.join('\n')}` | ||
} | ||
|
||
/** | ||
* Print the error and exit. | ||
*/ | ||
function printAndExit (error: TSError) { | ||
console.error(print(error)) | ||
process.exit(1) | ||
} | ||
|
||
/** | ||
* Evaluate the code snippet. | ||
*/ | ||
function _eval (code: string, context: any) { | ||
const undo = evalFile.input | ||
const isCompletion = !/\n$/.test(code) | ||
|
||
// Increment eval constants for the compiler to pick up changes. | ||
evalFile.input += code | ||
evalFile.version++ | ||
|
||
let output: string | ||
|
||
// Undo on TypeScript compilation errors. | ||
try { | ||
output = service.compile(EVAL_PATH) | ||
} catch (error) { | ||
evalFile.input = undo | ||
|
||
throw error | ||
} | ||
|
||
// Use `diff` to check for new JavaScript to execute. | ||
const changes = diffLines(evalFile.output, output) | ||
|
||
// Revert the code if running in "completion" environment. Updated the output | ||
// to diff against future executions when evaling code. | ||
if (isCompletion) { | ||
evalFile.input = undo | ||
} else { | ||
evalFile.output = output | ||
} | ||
|
||
let result: any | ||
|
||
// Iterate over the diff and evaluate `added` lines. The only removed lines | ||
// should be the source map and lines that stay the same are ignored. | ||
for (const change of changes) { | ||
if (change.added) { | ||
const script = createScript(change.value, EVAL_FILENAME) | ||
|
||
result = script.runInNewContext(context) | ||
} | ||
} | ||
|
||
return result | ||
} | ||
|
||
/** | ||
* Start a CLI REPL. | ||
*/ | ||
function startRepl () { | ||
const repl = start({ | ||
prompt: '> ', | ||
input: process.stdin, | ||
output: process.stdout, | ||
eval: replEval, | ||
useGlobal: false | ||
}) | ||
|
||
// Reset eval file information when repl is reset. | ||
repl.on('reset', () => { | ||
evalFile.input = '' | ||
evalFile.output = '' | ||
evalFile.version = 0 | ||
}) | ||
|
||
;(repl as any).defineCommand('type', { | ||
help: 'Check the type of a TypeScript identifier', | ||
action: function (identifier: string) { | ||
if (!identifier) { | ||
;(repl as any).displayPrompt() | ||
return | ||
} | ||
|
||
const undo = evalFile.input | ||
|
||
evalFile.input += identifier | ||
evalFile.version++ | ||
|
||
const { name, comment } = service.getTypeInfo(EVAL_PATH, evalFile.input.length) | ||
|
||
;(repl as any).outputStream.write(`${chalk.bold(name)}\n${comment ? `${comment}\n` : ''}`) | ||
;(repl as any).displayPrompt() | ||
|
||
evalFile.input = undo | ||
} | ||
}) | ||
} | ||
|
||
/** | ||
* Eval code from the REPL. | ||
*/ | ||
function replEval (code: string, context: any, filename: string, callback: (err?: Error, result?: any) => any) { | ||
let err: any | ||
let result: any | ||
|
||
// TODO: Figure out how to handle completion here. | ||
if (code === '.scope') { | ||
callback() | ||
return | ||
} | ||
|
||
try { | ||
result = _eval(code, context) | ||
} catch (error) { | ||
if (error instanceof TSError) { | ||
err = print(error) | ||
} else { | ||
err = error | ||
} | ||
} | ||
|
||
callback(err, result) | ||
} | ||
|
||
/** | ||
* Split a string of values into an array. | ||
*/ | ||
function list (value: string | string[]) { | ||
return String(value).split(/ *, */) | ||
} | ||
|
||
/** | ||
* Get the file text, checking for eval first. | ||
*/ | ||
function getFileEval (fileName: string) { | ||
return fileName === EVAL_PATH ? evalFile.input : getFile(fileName) | ||
} | ||
|
||
/** | ||
* Get the file version, checking for eval first. | ||
*/ | ||
function getVersionEval (fileName: string) { | ||
return fileName === EVAL_PATH ? String(evalFile.version) : getVersion(fileName) | ||
} | ||
|
||
/** | ||
* Get whether the file exists. | ||
*/ | ||
function getFileExistsEval (fileName: string) { | ||
return fileName === EVAL_PATH ? true : getFileExists(fileName) | ||
} |
Oops, something went wrong.