Skip to content

Commit

Permalink
Improve handling of the REPL for ASI and modules
Browse files Browse the repository at this point in the history
Closes #243
  • Loading branch information
blakeembrey committed Dec 9, 2016
1 parent dd15f7f commit e760502
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 46 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"dependencies": {
"arrify": "^1.0.0",
"chalk": "^1.1.1",
"diff": "^3.1.0",
"make-error": "^1.1.1",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
Expand Down
128 changes: 83 additions & 45 deletions src/_bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import arrify = require('arrify')
import Module = require('module')
import minimist = require('minimist')
import chalk = require('chalk')
import { diffLines } from 'diff'
import { createScript } from 'vm'
import { register, VERSION, getFile, fileExists, TSError, parse } from './index'

Expand Down Expand Up @@ -159,17 +160,18 @@ const service = register({
fileExists: isEval ? fileExistsEval : fileExists
})

// Increment the `eval` id to keep track of execution.
let evalId = 0

// Note: TypeScript files must always end with `.ts`.
const EVAL_PATHS: { [path: string]: string } = {}

// Require specified modules before start-up.
for (const id of arrify(argv.require)) {
Module._load(id)
}

/**
* Eval helpers.
*/
const EVAL_FILENAME = `[eval].ts`
const EVAL_PATH = join(cwd, EVAL_FILENAME)
const EVAL_INSTANCE = { input: '', output: '', version: 0, lines: 0 }

// Execute the main contents (either eval, script or piped).
if (isEvalScript) {
evalAndExit(code as string, isPrinted)
Expand All @@ -196,13 +198,11 @@ if (isEvalScript) {
* Evaluate a script.
*/
function evalAndExit (code: string, isPrinted: boolean) {
const filename = getEvalFileName(evalId)

const module = new Module(filename)
module.filename = filename
const module = new Module(EVAL_FILENAME)
module.filename = EVAL_FILENAME
module.paths = Module._nodeModulePaths(cwd)

;(global as any).__filename = filename
;(global as any).__filename = EVAL_FILENAME
;(global as any).__dirname = cwd
;(global as any).exports = module.exports
;(global as any).module = module
Expand Down Expand Up @@ -241,18 +241,36 @@ function print (error: TSError) {
* Evaluate the code snippet.
*/
function _eval (input: string, context: any) {
const lines = EVAL_INSTANCE.lines
const isCompletion = !/\n$/.test(input)
const path = join(cwd, getEvalFileName(evalId++))
const { code, lineOffset } = getEvalContent(input)
const filename = basename(path)
const undo = appendEval(input)
let output: string

const output = service().compile(code, path, lineOffset)
try {
output = service().compile(EVAL_INSTANCE.input, EVAL_PATH, -lines)
} catch (err) {
undo()

const script = createScript(output, supportsScriptOptions ? { filename, lineOffset } : filename)
const result = script.runInNewContext(context)
throw err
}

if (!isCompletion) {
EVAL_PATHS[path] = code
// Use `diff` to check for new JavaScript to execute.
const changes = diffLines(EVAL_INSTANCE.output, output)

if (isCompletion) {
undo()
} else {
EVAL_INSTANCE.output = output
}

let result: any

for (const change of changes) {
if (change.added) {
const script = createScript(change.value, EVAL_FILENAME)

result = script.runInNewContext(context)
}
}

return result
Expand All @@ -270,6 +288,10 @@ function startRepl () {
useGlobal: false
})

const undo = appendEval('')

repl.on('reset', () => undo())

repl.defineCommand('type', {
help: 'Check the type of a TypeScript identifier',
action: function (identifier: string) {
Expand All @@ -278,16 +300,10 @@ function startRepl () {
return
}

const path = join(cwd, getEvalFileName(evalId++))
const { code, lineOffset } = getEvalContent(identifier)

// Cache the file for language services lookup.
EVAL_PATHS[path] = code
const undo = appendEval(identifier)
const { name, comment } = service().getTypeInfo(EVAL_PATH, EVAL_INSTANCE.input.length)

const { name, comment } = service().getTypeInfo(path, code.length)

// Delete the file from the cache after used for lookup.
delete EVAL_PATHS[path]
undo()

repl.outputStream.write(`${chalk.bold(name)}\n${comment ? `${comment}\n` : ''}`)
repl.displayPrompt()
Expand Down Expand Up @@ -327,36 +343,58 @@ function replEval (code: string, context: any, filename: string, callback: (err?
}

/**
* Get the file text, checking for eval first.
* Append to the eval instance and return an undo function.
*/
function getFileEval (path: string) {
return EVAL_PATHS.hasOwnProperty(path) ? EVAL_PATHS[path] : getFile(path)
function appendEval (input: string) {
const undoInput = EVAL_INSTANCE.input
const undoVersion = EVAL_INSTANCE.version
const undoOutput = EVAL_INSTANCE.output
const undoLines = EVAL_INSTANCE.lines

// Handle ASI issues with TypeScript re-evaluation.
if (undoInput.charAt(undoInput.length - 1) === '\n' && /^\s*[\[\(\`]/.test(input) && !/;\s*$/.test(undoInput)) {
EVAL_INSTANCE.input = `${EVAL_INSTANCE.input.slice(0, -1)};\n`
}

EVAL_INSTANCE.input += input
EVAL_INSTANCE.lines += lineCount(input)
EVAL_INSTANCE.version++

return function () {
EVAL_INSTANCE.input = undoInput
EVAL_INSTANCE.output = undoOutput
EVAL_INSTANCE.version = undoVersion
EVAL_INSTANCE.lines = undoLines
}
}

/**
* Get whether the file exists.
* Count the number of lines.
*/
function fileExistsEval (path: string) {
return EVAL_PATHS.hasOwnProperty(path) || fileExists(path)
function lineCount (value: string) {
let count = 0

for (const char of value) {
if (char === '\n') {
count++
}
}

return count
}

/**
* Create an file for evaluation.
* Get the file text, checking for eval first.
*/
function getEvalContent (input: string) {
const refs = Object.keys(EVAL_PATHS).map(x => `/// <reference path="${x}" />\n`)

return {
lineOffset: -refs.length,
code: refs.join('') + input
}
function getFileEval (path: string) {
return path === EVAL_PATH ? EVAL_INSTANCE.input : getFile(path)
}

/**
* Retrieve the eval filename.
* Get whether the file exists.
*/
function getEvalFileName (index: number) {
return `[eval ${index}].ts`
function fileExistsEval (path: string) {
return path === EVAL_PATH || fileExists(path)
}

const RECOVERY_CODES: number[] = [
Expand Down
2 changes: 1 addition & 1 deletion src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ describe('ts-node', function () {
exec(`${BIN_EXEC} -e "import * as m from './tests/module';console.log(m.example(123))"`, function (err) {
expect(err.message).to.match(new RegExp(
// Node 0.10 can not override the `lineOffset` option.
'\\[eval [01]\\]\\.ts \\(1,59\\): Argument of type \'(?:number|123)\' ' +
'\\[eval\\]\\.ts \\(1,59\\): Argument of type \'(?:number|123)\' ' +
'is not assignable to parameter of type \'string\'\\. \\(2345\\)'
))

Expand Down
1 change: 1 addition & 0 deletions typings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"dependencies": {
"arrify": "registry:npm/arrify#1.0.0+20160723033700",
"chalk": "registry:npm/chalk#1.0.0+20160211003958",
"diff": "registry:npm/diff#2.0.0+20160723033700",
"make-error": "registry:npm/make-error#1.0.0+20160211003958",
"minimist": "registry:npm/minimist#1.0.0+20160229232932",
"mkdirp": "registry:npm/mkdirp#0.5.0+20160222053049",
Expand Down

0 comments on commit e760502

Please sign in to comment.