/src/repl.coffee

http://github.com/jashkenas/coffee-script · CoffeeScript · 220 lines · 163 code · 18 blank · 39 comment · 20 complexity · 67ab6fbde7039e119bfaff57c3d1cecb MD5 · raw file

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