Skip to content

Commit

Permalink
Expand ReplService API; use new API in bin.ts, which further decouple…
Browse files Browse the repository at this point in the history
…s bin.ts from repl.ts
  • Loading branch information
cspotcode committed Nov 20, 2020
1 parent 02c174a commit 95375d5
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 71 deletions.
42 changes: 14 additions & 28 deletions src/bin.ts
Expand Up @@ -13,10 +13,10 @@ import {
VERSION
} from './index'
import {
_eval,
EVAL_FILENAME,
EvalState,
startRepl
createReplService,
ReplService
} from './repl'

/**
Expand Down Expand Up @@ -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({
Expand All @@ -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}`)
Expand All @@ -213,19 +199,19 @@ 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()
} else {
// 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))
}
}
}
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
145 changes: 102 additions & 43 deletions src/repl.ts
Expand Up @@ -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)
}
}

Expand All @@ -63,13 +99,36 @@ export class EvalState {
version = 0
lines = 0

constructor (public path: string) {}
constructor (public path: string) { }
}

export type EvalStateAwareHostFunctions = Pick<CreateOptions, 'readFile' | 'fileExists'>

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)
Expand Down Expand Up @@ -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()
Expand All @@ -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)
Expand All @@ -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
})

Expand Down

0 comments on commit 95375d5

Please sign in to comment.