PageRenderTime 43ms CodeModel.GetById 30ms app.highlight 9ms RepoModel.GetById 1ms app.codeStats 0ms

/src/repl.coffee

http://github.com/jashkenas/coffee-script
CoffeeScript | 220 lines | 163 code | 18 blank | 39 comment | 20 complexity | 67ab6fbde7039e119bfaff57c3d1cecb MD5 | raw file
  1fs = require 'fs'
  2path = require 'path'
  3vm = require 'vm'
  4nodeREPL = require 'repl'
  5CoffeeScript = require './'
  6{merge, updateSyntaxError} = require './helpers'
  7
  8sawSIGINT = no
  9transpile = no
 10
 11replDefaults =
 12  prompt: 'coffee> ',
 13  historyFile: do ->
 14    historyPath = process.env.XDG_CACHE_HOME or process.env.HOME
 15    path.join historyPath, '.coffee_history' if historyPath
 16  historyMaxInputSize: 10240
 17  eval: (input, context, filename, cb) ->
 18    # XXX: multiline hack.
 19    input = input.replace /\uFF00/g, '\n'
 20    # Node's REPL sends the input ending with a newline and then wrapped in
 21    # parens. Unwrap all that.
 22    input = input.replace /^\(([\s\S]*)\n\)$/m, '$1'
 23    # Node's REPL v6.9.1+ sends the input wrapped in a try/catch statement.
 24    # Unwrap that too.
 25    input = input.replace /^\s*try\s*{([\s\S]*)}\s*catch.*$/m, '$1'
 26
 27    # Require AST nodes to do some AST manipulation.
 28    {Block, Assign, Value, Literal, Call, Code, Root} = require './nodes'
 29
 30    try
 31      # Tokenize the clean input.
 32      tokens = CoffeeScript.tokens input
 33      # Filter out tokens generated just to hold comments.
 34      if tokens.length >= 2 and tokens[0].generated and
 35         tokens[0].comments?.length isnt 0 and "#{tokens[0][1]}" is '' and
 36         tokens[1][0] is 'TERMINATOR'
 37        tokens = tokens[2...]
 38      if tokens.length >= 1 and tokens[tokens.length - 1].generated and
 39         tokens[tokens.length - 1].comments?.length isnt 0 and "#{tokens[tokens.length - 1][1]}" is ''
 40        tokens.pop()
 41      # Collect referenced variable names just like in `CoffeeScript.compile`.
 42      referencedVars = (token[1] for token in tokens when token[0] is 'IDENTIFIER')
 43      # Generate the AST of the tokens.
 44      ast = CoffeeScript.nodes(tokens).body
 45      # Add assignment to `__` variable to force the input to be an expression.
 46      ast = new Block [new Assign (new Value new Literal '__'), ast, '=']
 47      # Wrap the expression in a closure to support top-level `await`.
 48      ast     = new Code [], ast
 49      isAsync = ast.isAsync
 50      # Invoke the wrapping closure.
 51      ast    = new Root new Block [new Call ast]
 52      js     = ast.compile {bare: yes, locals: Object.keys(context), referencedVars, sharedScope: yes}
 53      if transpile
 54        js = transpile.transpile(js, transpile.options).code
 55        # Strip `"use strict"`, to avoid an exception on assigning to
 56        # undeclared variable `__`.
 57        js = js.replace /^"use strict"|^'use strict'/, ''
 58      result = runInContext js, context, filename
 59      # Await an async result, if necessary.
 60      if isAsync
 61        result.then (resolvedResult) ->
 62          cb null, resolvedResult unless sawSIGINT
 63        sawSIGINT = no
 64      else
 65        cb null, result
 66    catch err
 67      # AST's `compile` does not add source code information to syntax errors.
 68      updateSyntaxError err, input
 69      cb err
 70
 71runInContext = (js, context, filename) ->
 72  if context is global
 73    vm.runInThisContext js, filename
 74  else
 75    vm.runInContext js, context, filename
 76
 77addMultilineHandler = (repl) ->
 78  {inputStream, outputStream} = repl
 79  # Node 0.11.12 changed API, prompt is now _prompt.
 80  origPrompt = repl._prompt ? repl.prompt
 81
 82  multiline =
 83    enabled: off
 84    initialPrompt: origPrompt.replace /^[^> ]*/, (x) -> x.replace /./g, '-'
 85    prompt: origPrompt.replace /^[^> ]*>?/, (x) -> x.replace /./g, '.'
 86    buffer: ''
 87
 88  # Proxy node's line listener
 89  nodeLineListener = repl.listeners('line')[0]
 90  repl.removeListener 'line', nodeLineListener
 91  repl.on 'line', (cmd) ->
 92    if multiline.enabled
 93      multiline.buffer += "#{cmd}\n"
 94      repl.setPrompt multiline.prompt
 95      repl.prompt true
 96    else
 97      repl.setPrompt origPrompt
 98      nodeLineListener cmd
 99    return
100
101  # Handle Ctrl-v
102  inputStream.on 'keypress', (char, key) ->
103    return unless key and key.ctrl and not key.meta and not key.shift and key.name is 'v'
104    if multiline.enabled
105      # allow arbitrarily switching between modes any time before multiple lines are entered
106      unless multiline.buffer.match /\n/
107        multiline.enabled = not multiline.enabled
108        repl.setPrompt origPrompt
109        repl.prompt true
110        return
111      # no-op unless the current line is empty
112      return if repl.line? and not repl.line.match /^\s*$/
113      # eval, print, loop
114      multiline.enabled = not multiline.enabled
115      repl.line = ''
116      repl.cursor = 0
117      repl.output.cursorTo 0
118      repl.output.clearLine 1
119      # XXX: multiline hack
120      multiline.buffer = multiline.buffer.replace /\n/g, '\uFF00'
121      repl.emit 'line', multiline.buffer
122      multiline.buffer = ''
123    else
124      multiline.enabled = not multiline.enabled
125      repl.setPrompt multiline.initialPrompt
126      repl.prompt true
127    return
128
129# Store and load command history from a file
130addHistory = (repl, filename, maxSize) ->
131  lastLine = null
132  try
133    # Get file info and at most maxSize of command history
134    stat = fs.statSync filename
135    size = Math.min maxSize, stat.size
136    # Read last `size` bytes from the file
137    readFd = fs.openSync filename, 'r'
138    buffer = Buffer.alloc size
139    fs.readSync readFd, buffer, 0, size, stat.size - size
140    fs.closeSync readFd
141    # Set the history on the interpreter
142    repl.history = buffer.toString().split('\n').reverse()
143    # If the history file was truncated we should pop off a potential partial line
144    repl.history.pop() if stat.size > maxSize
145    # Shift off the final blank newline
146    repl.history.shift() if repl.history[0] is ''
147    repl.historyIndex = -1
148    lastLine = repl.history[0]
149
150  fd = fs.openSync filename, 'a'
151
152  repl.addListener 'line', (code) ->
153    if code and code.length and code isnt '.history' and code isnt '.exit' and lastLine isnt code
154      # Save the latest command in the file
155      fs.writeSync fd, "#{code}\n"
156      lastLine = code
157
158  # XXX: The SIGINT event from REPLServer is undocumented, so this is a bit fragile
159  repl.on 'SIGINT', -> sawSIGINT = yes
160  repl.on 'exit', -> fs.closeSync fd
161
162  # Add a command to show the history stack
163  repl.commands[getCommandId(repl, 'history')] =
164    help: 'Show command history'
165    action: ->
166      repl.outputStream.write "#{repl.history[..].reverse().join '\n'}\n"
167      repl.displayPrompt()
168
169getCommandId = (repl, commandName) ->
170  # Node 0.11 changed API, a command such as '.help' is now stored as 'help'
171  commandsHaveLeadingDot = repl.commands['.help']?
172  if commandsHaveLeadingDot then ".#{commandName}" else commandName
173
174module.exports =
175  start: (opts = {}) ->
176    [major, minor, build] = process.versions.node.split('.').map (n) -> parseInt(n, 10)
177
178    if major < 6
179      console.warn "Node 6+ required for CoffeeScript REPL"
180      process.exit 1
181
182    CoffeeScript.register()
183    process.argv = ['coffee'].concat process.argv[2..]
184    if opts.transpile
185      transpile = {}
186      try
187        transpile.transpile = require('@babel/core').transform
188      catch
189        try
190          transpile.transpile = require('babel-core').transform
191        catch
192          console.error '''
193            To use --transpile with an interactive REPL, @babel/core must be installed either in the current folder or globally:
194              npm install --save-dev @babel/core
195            or
196              npm install --global @babel/core
197            And you must save options to configure Babel in one of the places it looks to find its options.
198            See https://coffeescript.org/#transpilation
199          '''
200          process.exit 1
201      transpile.options =
202        filename: path.resolve process.cwd(), '<repl>'
203      # Since the REPL compilation path is unique (in `eval` above), we need
204      # another way to get the `options` object attached to a module so that
205      # it knows later on whether it needs to be transpiled. In the case of
206      # the REPL, the only applicable option is `transpile`.
207      Module = require 'module'
208      originalModuleLoad = Module::load
209      Module::load = (filename) ->
210        @options = transpile: transpile.options
211        originalModuleLoad.call @, filename
212    opts = merge replDefaults, opts
213    repl = nodeREPL.start opts
214    runInContext opts.prelude, repl.context, 'prelude' if opts.prelude
215    repl.on 'exit', -> repl.outputStream.write '\n' if not repl.closed
216    addMultilineHandler repl
217    addHistory repl, opts.historyFile, opts.historyMaxInputSize if opts.historyFile
218    # Adapt help inherited from the node REPL
219    repl.commands[getCommandId(repl, 'load')].help = 'Load code from a file into this REPL session'
220    repl