From d62db81d5690a07270fe6e8e33d5d7900881c68d Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Fri, 10 Jun 2016 22:32:50 -0700 Subject: [PATCH] Enable node flags pass-through --- src/_bin.ts | 376 ++++++++++++++++++++++++++++++++++++++++++++++ src/bin.ts | 408 ++++++-------------------------------------------- tsconfig.json | 1 + 3 files changed, 422 insertions(+), 363 deletions(-) create mode 100644 src/_bin.ts diff --git a/src/_bin.ts b/src/_bin.ts new file mode 100644 index 000000000..e4eb3e980 --- /dev/null +++ b/src/_bin.ts @@ -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(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) +} diff --git a/src/bin.ts b/src/bin.ts index 43871e381..9862a3122 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -1,378 +1,60 @@ #!/usr/bin/env node -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' +import { spawn } from 'child_process' +import { join } from 'path' -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] +const args = [join(__dirname, '_bin.js')] - 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 + const flag = arg.split('=')[0] + + switch (flag) { + case '-d': + args.unshift('--debug') + break + case 'debug': + case '--debug': + case '--debug-brk': + args.unshift(arg) + break + case '-gc': + case '--expose-gc': + args.unshift('--expose-gc') + break + case '--gc-global': + case '--es_staging': + case '--no-deprecation': + case '--prof': + case '--log-timer-events': + case '--throw-deprecation': + case '--trace-deprecation': + case '--use_strict': + case '--allow-natives-syntax': + case '--perf-basic-prof': + args.unshift(arg) + break + default: + if (/^--(?:harmony|trace|icu-data-dir|max-old-space-size)/.test(arg)) { + args.unshift(arg) + } + break } - if (/^-/.test(arg)) { - // Skip next argument. - if (!isFlagOnly(arg) && !/^-/.test(next)) { - i++ - } - - continue + // Stop on first non-argument because it's the script name. + if (/^[^-]/.test(arg)) { + break } - - stop = i - break -} - -const argv = minimist(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: +const proc = spawn(process.execPath, args.concat(process.argv.slice(2)), { stdio: 'inherit' }) - -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() +proc.on('exit', function (code: number, signal: string) { + process.on('exit', function () { + if (signal) { + process.kill(process.pid, signal) } 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 + process.exit(code) } }) -} - -/** - * 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) -} +}) diff --git a/tsconfig.json b/tsconfig.json index a1ecf12c4..64f69a2a6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,7 @@ "sourceMap": true }, "files": [ + "src/_bin.ts", "src/bin.ts", "src/index.ts", "src/index.spec.ts",