Skip to content

Commit 95375d5

Browse files
committed
Expand ReplService API; use new API in bin.ts, which further decouples bin.ts from repl.ts
1 parent 02c174a commit 95375d5

File tree

2 files changed

+116
-71
lines changed

2 files changed

+116
-71
lines changed

src/bin.ts

+14-28
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ import {
1313
VERSION
1414
} from './index'
1515
import {
16-
_eval,
1716
EVAL_FILENAME,
1817
EvalState,
19-
startRepl
18+
createReplService,
19+
ReplService
2020
} from './repl'
2121

2222
/**
@@ -151,6 +151,8 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re
151151
/** Unresolved. May point to a symlink, not realpath. May be missing file extension */
152152
const scriptPath = args._.length ? resolve(cwd, args._[0]) : undefined
153153
const state = new EvalState(scriptPath || join(cwd, EVAL_FILENAME))
154+
const replService = createReplService({ state })
155+
const { evalStateAwareHostFunctions } = replService
154156

155157
// Register the TypeScript compiler instance.
156158
const service = register({
@@ -171,29 +173,13 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re
171173
ignoreDiagnostics,
172174
compilerOptions,
173175
require: argsRequire,
174-
readFile: code !== undefined
175-
? (path: string) => {
176-
if (path === state.path) return state.input
177-
178-
try {
179-
return readFileSync(path, 'utf8')
180-
} catch (err) {/* Ignore. */}
181-
}
182-
: undefined,
183-
fileExists: code !== undefined
184-
? (path: string) => {
185-
if (path === state.path) return true
186-
187-
try {
188-
const stats = statSync(path)
189-
return stats.isFile() || stats.isFIFO()
190-
} catch (err) {
191-
return false
192-
}
193-
}
194-
: undefined
176+
readFile: code !== undefined ? evalStateAwareHostFunctions.readFile : undefined,
177+
fileExists: code !== undefined ? evalStateAwareHostFunctions.fileExists : undefined
195178
})
196179

180+
// Bind REPL service to ts-node compiler service (chicken-and-egg problem)
181+
replService.setService(service)
182+
197183
// Output project information.
198184
if (version >= 2) {
199185
console.log(`ts-node v${VERSION}`)
@@ -213,19 +199,19 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re
213199

214200
// Execute the main contents (either eval, script or piped).
215201
if (code !== undefined && !interactive) {
216-
evalAndExit(service, state, module, code, print)
202+
evalAndExit(replService, module, code, print)
217203
} else {
218204
if (args._.length) {
219205
Module.runMain()
220206
} else {
221207
// Piping of execution _only_ occurs when no other script is specified.
222208
// --interactive flag forces REPL
223209
if (interactive || process.stdin.isTTY) {
224-
startRepl(service, state, code)
210+
replService.start(code)
225211
} else {
226212
let buffer = code || ''
227213
process.stdin.on('data', (chunk: Buffer) => buffer += chunk)
228-
process.stdin.on('end', () => evalAndExit(service, state, module, buffer, print))
214+
process.stdin.on('end', () => evalAndExit(replService, module, buffer, print))
229215
}
230216
}
231217
}
@@ -275,7 +261,7 @@ function getCwd (dir?: string, scriptMode?: boolean, scriptPath?: string) {
275261
/**
276262
* Evaluate a script.
277263
*/
278-
function evalAndExit (service: Register, state: EvalState, module: Module, code: string, isPrinted: boolean) {
264+
function evalAndExit (replService: ReplService, module: Module, code: string, isPrinted: boolean) {
279265
let result: any
280266

281267
;(global as any).__filename = module.filename
@@ -285,7 +271,7 @@ function evalAndExit (service: Register, state: EvalState, module: Module, code:
285271
;(global as any).require = module.require.bind(module)
286272

287273
try {
288-
result = _eval(service, state, code)
274+
result = replService.evalCode(code)
289275
} catch (error) {
290276
if (error instanceof TSError) {
291277
console.error(error)

src/repl.ts

+102-43
Original file line numberDiff line numberDiff line change
@@ -3,54 +3,90 @@ import { homedir } from 'os'
33
import { join } from 'path'
44
import { Recoverable, start } from 'repl'
55
import { Script } from 'vm'
6-
import { Register, TSError } from './index'
6+
import { Register, CreateOptions, TSError } from './index'
7+
import { readFileSync, statSync } from 'fs'
78

89
/**
910
* Eval filename for REPL/debug.
11+
* @internal
1012
*/
1113
export const EVAL_FILENAME = `[eval].ts`
1214

13-
/**
14-
* @param service - TypeScript compiler instance
15-
* @param state - Eval state management
16-
*
17-
* @returns an evaluator for the node REPL
18-
*/
19-
export function createReplService (
20-
service: Register,
21-
state: EvalState = new EvalState(join(process.cwd(), EVAL_FILENAME))
22-
) {
23-
return {
24-
/**
25-
* Eval code from the REPL.
26-
*/
27-
eval: function (code: string, _context: any, _filename: string, callback: (err: Error | null, result?: any) => any) {
28-
let err: Error | null = null
29-
let result: any
30-
31-
// TODO: Figure out how to handle completion here.
32-
if (code === '.scope') {
33-
callback(err)
34-
return
35-
}
15+
export interface ReplService {
16+
readonly state: EvalState
17+
setService (service: Register): void
18+
evalCode (code: string): void
19+
/**
20+
* eval implementation compatible with node's REPL API
21+
*/
22+
nodeReplEval (code: string, _context: any, _filename: string, callback: (err: Error | null, result?: any) => any): void
23+
evalStateAwareHostFunctions: EvalStateAwareHostFunctions
24+
/** Start a node REPL */
25+
start (code?: string): void
26+
}
27+
28+
export interface CreateReplServiceOptions {
29+
service?: Register
30+
state?: EvalState
31+
}
32+
33+
export function createReplService (options: CreateReplServiceOptions = {}) {
34+
let service = options.service
35+
const state = options.state ?? new EvalState(join(process.cwd(), EVAL_FILENAME))
36+
const evalStateAwareHostFunctions = createEvalStateAwareHostFunctions(state)
37+
38+
const replService: ReplService = {
39+
state: options.state ?? new EvalState(join(process.cwd(), EVAL_FILENAME)),
40+
setService,
41+
evalCode,
42+
nodeReplEval,
43+
evalStateAwareHostFunctions,
44+
start
45+
}
46+
return replService
47+
48+
function setService (_service: Register) {
49+
service = _service
50+
}
51+
52+
function evalCode (code: string) {
53+
return _eval(service!, state, code)
54+
}
55+
56+
/**
57+
* Eval code from the REPL.
58+
*/
59+
function nodeReplEval (code: string, _context: any, _filename: string, callback: (err: Error | null, result?: any) => any) {
60+
let err: Error | null = null
61+
let result: any
62+
63+
// TODO: Figure out how to handle completion here.
64+
if (code === '.scope') {
65+
callback(err)
66+
return
67+
}
3668

37-
try {
38-
result = _eval(service, state, code)
39-
} catch (error) {
40-
if (error instanceof TSError) {
41-
// Support recoverable compilations using >= node 6.
42-
if (Recoverable && isRecoverable(error)) {
43-
err = new Recoverable(error)
44-
} else {
45-
console.error(error)
46-
}
69+
try {
70+
result = evalCode(code)
71+
} catch (error) {
72+
if (error instanceof TSError) {
73+
// Support recoverable compilations using >= node 6.
74+
if (Recoverable && isRecoverable(error)) {
75+
err = new Recoverable(error)
4776
} else {
48-
err = error
77+
console.error(error)
4978
}
79+
} else {
80+
err = error
5081
}
51-
52-
return callback(err, result)
5382
}
83+
84+
return callback(err, result)
85+
}
86+
87+
function start (code?: string) {
88+
// TODO assert that service is set; remove all ! postfixes
89+
return startRepl(replService, service!, state, code)
5490
}
5591
}
5692

@@ -63,13 +99,36 @@ export class EvalState {
6399
version = 0
64100
lines = 0
65101

66-
constructor (public path: string) {}
102+
constructor (public path: string) { }
103+
}
104+
105+
export type EvalStateAwareHostFunctions = Pick<CreateOptions, 'readFile' | 'fileExists'>
106+
107+
export function createEvalStateAwareHostFunctions (state: EvalState): EvalStateAwareHostFunctions {
108+
function readFile (path: string) {
109+
if (path === state.path) return state.input
110+
111+
try {
112+
return readFileSync(path, 'utf8')
113+
} catch (err) {/* Ignore. */}
114+
}
115+
function fileExists (path: string) {
116+
if (path === state.path) return true
117+
118+
try {
119+
const stats = statSync(path)
120+
return stats.isFile() || stats.isFIFO()
121+
} catch (err) {
122+
return false
123+
}
124+
}
125+
return { readFile, fileExists }
67126
}
68127

69128
/**
70129
* Evaluate the code snippet.
71130
*/
72-
export function _eval (service: Register, state: EvalState, input: string) {
131+
function _eval (service: Register, state: EvalState, input: string) {
73132
const lines = state.lines
74133
const isCompletion = !/\n$/.test(input)
75134
const undo = appendEval(state, input)
@@ -99,7 +158,7 @@ export function _eval (service: Register, state: EvalState, input: string) {
99158
/**
100159
* Execute some code.
101160
*/
102-
export function exec (code: string, filename: string) {
161+
function exec (code: string, filename: string) {
103162
const script = new Script(code, { filename: filename })
104163

105164
return script.runInThisContext()
@@ -108,11 +167,11 @@ export function exec (code: string, filename: string) {
108167
/**
109168
* Start a CLI REPL.
110169
*/
111-
export function startRepl (service: Register, state: EvalState, code?: string) {
170+
function startRepl (replService: ReplService, service: Register, state: EvalState, code?: string) {
112171
// Eval incoming code before the REPL starts.
113172
if (code) {
114173
try {
115-
_eval(service, state, `${code}\n`)
174+
replService.evalCode(`${code}\n`)
116175
} catch (err) {
117176
console.error(err)
118177
process.exit(1)
@@ -125,7 +184,7 @@ export function startRepl (service: Register, state: EvalState, code?: string) {
125184
output: process.stdout,
126185
// Mimicking node's REPL implementation: https://github.com/nodejs/node/blob/168b22ba073ee1cbf8d0bcb4ded7ff3099335d04/lib/internal/repl.js#L28-L30
127186
terminal: process.stdout.isTTY && !parseInt(process.env.NODE_NO_READLINE!, 10),
128-
eval: createReplService(service, state).eval,
187+
eval: replService.nodeReplEval,
129188
useGlobal: true
130189
})
131190

0 commit comments

Comments
 (0)