diff --git a/src/bin.ts b/src/bin.ts index bb8e45b7c..1fd8df65e 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -13,10 +13,10 @@ import { VERSION } from './index' import { - _eval, EVAL_FILENAME, EvalState, - startRepl + createReplService, + ReplService } from './repl' /** @@ -151,6 +151,8 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re /** Unresolved. May point to a symlink, not realpath. May be missing file extension */ const scriptPath = args._.length ? resolve(cwd, args._[0]) : undefined const state = new EvalState(scriptPath || join(cwd, EVAL_FILENAME)) + const replService = createReplService({ state }) + const { evalStateAwareHostFunctions } = replService // Register the TypeScript compiler instance. const service = register({ @@ -171,29 +173,13 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re ignoreDiagnostics, compilerOptions, require: argsRequire, - readFile: code !== undefined - ? (path: string) => { - if (path === state.path) return state.input - - try { - return readFileSync(path, 'utf8') - } catch (err) {/* Ignore. */} - } - : undefined, - fileExists: code !== undefined - ? (path: string) => { - if (path === state.path) return true - - try { - const stats = statSync(path) - return stats.isFile() || stats.isFIFO() - } catch (err) { - return false - } - } - : undefined + readFile: code !== undefined ? evalStateAwareHostFunctions.readFile : undefined, + fileExists: code !== undefined ? evalStateAwareHostFunctions.fileExists : undefined }) + // Bind REPL service to ts-node compiler service (chicken-and-egg problem) + replService.setService(service) + // Output project information. if (version >= 2) { console.log(`ts-node v${VERSION}`) @@ -213,7 +199,7 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re // Execute the main contents (either eval, script or piped). if (code !== undefined && !interactive) { - evalAndExit(service, state, module, code, print) + evalAndExit(replService, module, code, print) } else { if (args._.length) { Module.runMain() @@ -221,11 +207,11 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re // Piping of execution _only_ occurs when no other script is specified. // --interactive flag forces REPL if (interactive || process.stdin.isTTY) { - startRepl(service, state, code) + replService.start(code) } else { let buffer = code || '' process.stdin.on('data', (chunk: Buffer) => buffer += chunk) - process.stdin.on('end', () => evalAndExit(service, state, module, buffer, print)) + process.stdin.on('end', () => evalAndExit(replService, module, buffer, print)) } } } @@ -275,7 +261,7 @@ function getCwd (dir?: string, scriptMode?: boolean, scriptPath?: string) { /** * Evaluate a script. */ -function evalAndExit (service: Register, state: EvalState, module: Module, code: string, isPrinted: boolean) { +function evalAndExit (replService: ReplService, module: Module, code: string, isPrinted: boolean) { let result: any ;(global as any).__filename = module.filename @@ -285,7 +271,7 @@ function evalAndExit (service: Register, state: EvalState, module: Module, code: ;(global as any).require = module.require.bind(module) try { - result = _eval(service, state, code) + result = replService.evalCode(code) } catch (error) { if (error instanceof TSError) { console.error(error) diff --git a/src/repl.ts b/src/repl.ts index c9c6f6127..54689b5ba 100644 --- a/src/repl.ts +++ b/src/repl.ts @@ -3,54 +3,90 @@ import { homedir } from 'os' import { join } from 'path' import { Recoverable, start } from 'repl' import { Script } from 'vm' -import { Register, TSError } from './index' +import { Register, CreateOptions, TSError } from './index' +import { readFileSync, statSync } from 'fs' /** * Eval filename for REPL/debug. + * @internal */ export const EVAL_FILENAME = `[eval].ts` -/** - * @param service - TypeScript compiler instance - * @param state - Eval state management - * - * @returns an evaluator for the node REPL - */ -export function createReplService ( - service: Register, - state: EvalState = new EvalState(join(process.cwd(), EVAL_FILENAME)) -) { - return { - /** - * Eval code from the REPL. - */ - eval: function (code: string, _context: any, _filename: string, callback: (err: Error | null, result?: any) => any) { - let err: Error | null = null - let result: any - - // TODO: Figure out how to handle completion here. - if (code === '.scope') { - callback(err) - return - } +export interface ReplService { + readonly state: EvalState + setService (service: Register): void + evalCode (code: string): void + /** + * eval implementation compatible with node's REPL API + */ + nodeReplEval (code: string, _context: any, _filename: string, callback: (err: Error | null, result?: any) => any): void + evalStateAwareHostFunctions: EvalStateAwareHostFunctions + /** Start a node REPL */ + start (code?: string): void +} + +export interface CreateReplServiceOptions { + service?: Register + state?: EvalState +} + +export function createReplService (options: CreateReplServiceOptions = {}) { + let service = options.service + const state = options.state ?? new EvalState(join(process.cwd(), EVAL_FILENAME)) + const evalStateAwareHostFunctions = createEvalStateAwareHostFunctions(state) + + const replService: ReplService = { + state: options.state ?? new EvalState(join(process.cwd(), EVAL_FILENAME)), + setService, + evalCode, + nodeReplEval, + evalStateAwareHostFunctions, + start + } + return replService + + function setService (_service: Register) { + service = _service + } + + function evalCode (code: string) { + return _eval(service!, state, code) + } + + /** + * Eval code from the REPL. + */ + function nodeReplEval (code: string, _context: any, _filename: string, callback: (err: Error | null, result?: any) => any) { + let err: Error | null = null + let result: any + + // TODO: Figure out how to handle completion here. + if (code === '.scope') { + callback(err) + return + } - try { - result = _eval(service, state, code) - } catch (error) { - if (error instanceof TSError) { - // Support recoverable compilations using >= node 6. - if (Recoverable && isRecoverable(error)) { - err = new Recoverable(error) - } else { - console.error(error) - } + try { + result = evalCode(code) + } catch (error) { + if (error instanceof TSError) { + // Support recoverable compilations using >= node 6. + if (Recoverable && isRecoverable(error)) { + err = new Recoverable(error) } else { - err = error + console.error(error) } + } else { + err = error } - - return callback(err, result) } + + return callback(err, result) + } + + function start (code?: string) { + // TODO assert that service is set; remove all ! postfixes + return startRepl(replService, service!, state, code) } } @@ -63,13 +99,36 @@ export class EvalState { version = 0 lines = 0 - constructor (public path: string) {} + constructor (public path: string) { } +} + +export type EvalStateAwareHostFunctions = Pick + +export function createEvalStateAwareHostFunctions (state: EvalState): EvalStateAwareHostFunctions { + function readFile (path: string) { + if (path === state.path) return state.input + + try { + return readFileSync(path, 'utf8') + } catch (err) {/* Ignore. */} + } + function fileExists (path: string) { + if (path === state.path) return true + + try { + const stats = statSync(path) + return stats.isFile() || stats.isFIFO() + } catch (err) { + return false + } + } + return { readFile, fileExists } } /** * Evaluate the code snippet. */ -export function _eval (service: Register, state: EvalState, input: string) { +function _eval (service: Register, state: EvalState, input: string) { const lines = state.lines const isCompletion = !/\n$/.test(input) const undo = appendEval(state, input) @@ -99,7 +158,7 @@ export function _eval (service: Register, state: EvalState, input: string) { /** * Execute some code. */ -export function exec (code: string, filename: string) { +function exec (code: string, filename: string) { const script = new Script(code, { filename: filename }) return script.runInThisContext() @@ -108,11 +167,11 @@ export function exec (code: string, filename: string) { /** * Start a CLI REPL. */ -export function startRepl (service: Register, state: EvalState, code?: string) { +function startRepl (replService: ReplService, service: Register, state: EvalState, code?: string) { // Eval incoming code before the REPL starts. if (code) { try { - _eval(service, state, `${code}\n`) + replService.evalCode(`${code}\n`) } catch (err) { console.error(err) process.exit(1) @@ -125,7 +184,7 @@ export function startRepl (service: Register, state: EvalState, code?: string) { output: process.stdout, // Mimicking node's REPL implementation: https://github.com/nodejs/node/blob/168b22ba073ee1cbf8d0bcb4ded7ff3099335d04/lib/internal/repl.js#L28-L30 terminal: process.stdout.isTTY && !parseInt(process.env.NODE_NO_READLINE!, 10), - eval: createReplService(service, state).eval, + eval: replService.nodeReplEval, useGlobal: true })