• Jump To … +
    browser.coffee cake.coffee coffeescript.coffee command.coffee grammar.coffee helpers.coffee index.coffee lexer.coffee nodes.coffee optparse.coffee register.coffee repl.coffee rewriter.coffee scope.litcoffee sourcemap.litcoffee
  • repl.coffee

  • §
    fs = require 'fs'
    path = require 'path'
    vm = require 'vm'
    nodeREPL = require 'repl'
    CoffeeScript = require './'
    {merge, updateSyntaxError} = require './helpers'
    
    sawSIGINT = no
    transpile = no
    
    replDefaults =
      prompt: 'coffee> ',
      historyFile: do ->
        historyPath = process.env.XDG_CACHE_HOME or process.env.HOME
        path.join historyPath, '.coffee_history' if historyPath
      historyMaxInputSize: 10240
      eval: (input, context, filename, cb) ->
  • §

    XXX: multiline hack.

        input = input.replace /\uFF00/g, '\n'
  • §

    Node’s REPL sends the input ending with a newline and then wrapped in parens. Unwrap all that.

        input = input.replace /^\(([\s\S]*)\n\)$/m, '$1'
  • §

    Node’s REPL v6.9.1+ sends the input wrapped in a try/catch statement. Unwrap that too.

        input = input.replace /^\s*try\s*{([\s\S]*)}\s*catch.*$/m, '$1'
  • §

    Require AST nodes to do some AST manipulation.

        {Block, Assign, Value, Literal, Call, Code, Root} = require './nodes'
    
        try
  • §

    Tokenize the clean input.

          tokens = CoffeeScript.tokens input
  • §

    Filter out tokens generated just to hold comments.

          if tokens.length >= 2 and tokens[0].generated and
             tokens[0].comments?.length isnt 0 and "#{tokens[0][1]}" is '' and
             tokens[1][0] is 'TERMINATOR'
            tokens = tokens[2...]
          if tokens.length >= 1 and tokens[tokens.length - 1].generated and
             tokens[tokens.length - 1].comments?.length isnt 0 and "#{tokens[tokens.length - 1][1]}" is ''
            tokens.pop()
  • §

    Collect referenced variable names just like in CoffeeScript.compile.

          referencedVars = (token[1] for token in tokens when token[0] is 'IDENTIFIER')
  • §

    Generate the AST of the tokens.

          ast = CoffeeScript.nodes(tokens).body
  • §

    Add assignment to __ variable to force the input to be an expression.

          ast = new Block [new Assign (new Value new Literal '__'), ast, '=']
  • §

    Wrap the expression in a closure to support top-level await.

          ast     = new Code [], ast
          isAsync = ast.isAsync
  • §

    Invoke the wrapping closure.

          ast    = new Root new Block [new Call ast]
          js     = ast.compile {bare: yes, locals: Object.keys(context), referencedVars, sharedScope: yes}
          if transpile
            js = transpile.transpile(js, transpile.options).code
  • §

    Strip "use strict", to avoid an exception on assigning to undeclared variable __.

            js = js.replace /^"use strict"|^'use strict'/, ''
          result = runInContext js, context, filename
  • §

    Await an async result, if necessary.

          if isAsync
            result.then (resolvedResult) ->
              cb null, resolvedResult unless sawSIGINT
            sawSIGINT = no
          else
            cb null, result
        catch err
  • §

    AST’s compile does not add source code information to syntax errors.

          updateSyntaxError err, input
          cb err
    
    runInContext = (js, context, filename) ->
      if context is global
        vm.runInThisContext js, filename
      else
        vm.runInContext js, context, filename
    
    addMultilineHandler = (repl) ->
      {inputStream, outputStream} = repl
  • §

    Node 0.11.12 changed API, prompt is now _prompt.

      origPrompt = repl._prompt ? repl.prompt
    
      multiline =
        enabled: off
        initialPrompt: origPrompt.replace /^[^> ]*/, (x) -> x.replace /./g, '-'
        prompt: origPrompt.replace /^[^> ]*>?/, (x) -> x.replace /./g, '.'
        buffer: ''
  • §

    Proxy node’s line listener

      nodeLineListener = repl.listeners('line')[0]
      repl.removeListener 'line', nodeLineListener
      repl.on 'line', (cmd) ->
        if multiline.enabled
          multiline.buffer += "#{cmd}\n"
          repl.setPrompt multiline.prompt
          repl.prompt true
        else
          repl.setPrompt origPrompt
          nodeLineListener cmd
        return
  • §

    Handle Ctrl-v

      inputStream.on 'keypress', (char, key) ->
        return unless key and key.ctrl and not key.meta and not key.shift and key.name is 'v'
        if multiline.enabled
  • §

    allow arbitrarily switching between modes any time before multiple lines are entered

          unless multiline.buffer.match /\n/
            multiline.enabled = not multiline.enabled
            repl.setPrompt origPrompt
            repl.prompt true
            return
  • §

    no-op unless the current line is empty

          return if repl.line? and not repl.line.match /^\s*$/
  • §

    eval, print, loop

          multiline.enabled = not multiline.enabled
          repl.line = ''
          repl.cursor = 0
          repl.output.cursorTo 0
          repl.output.clearLine 1
  • §

    XXX: multiline hack

          multiline.buffer = multiline.buffer.replace /\n/g, '\uFF00'
          repl.emit 'line', multiline.buffer
          multiline.buffer = ''
        else
          multiline.enabled = not multiline.enabled
          repl.setPrompt multiline.initialPrompt
          repl.prompt true
        return
  • §

    Store and load command history from a file

    addHistory = (repl, filename, maxSize) ->
      lastLine = null
      try
  • §

    Get file info and at most maxSize of command history

        stat = fs.statSync filename
        size = Math.min maxSize, stat.size
  • §

    Read last size bytes from the file

        readFd = fs.openSync filename, 'r'
        buffer = Buffer.alloc size
        fs.readSync readFd, buffer, 0, size, stat.size - size
        fs.closeSync readFd
  • §

    Set the history on the interpreter

        repl.history = buffer.toString().split('\n').reverse()
  • §

    If the history file was truncated we should pop off a potential partial line

        repl.history.pop() if stat.size > maxSize
  • §

    Shift off the final blank newline

        repl.history.shift() if repl.history[0] is ''
        repl.historyIndex = -1
        lastLine = repl.history[0]
    
      fd = fs.openSync filename, 'a'
    
      repl.addListener 'line', (code) ->
        if code and code.length and code isnt '.history' and code isnt '.exit' and lastLine isnt code
  • §

    Save the latest command in the file

          fs.writeSync fd, "#{code}\n"
          lastLine = code
  • §

    XXX: The SIGINT event from REPLServer is undocumented, so this is a bit fragile

      repl.on 'SIGINT', -> sawSIGINT = yes
      repl.on 'exit', -> fs.closeSync fd
  • §

    Add a command to show the history stack

      repl.commands[getCommandId(repl, 'history')] =
        help: 'Show command history'
        action: ->
          repl.outputStream.write "#{repl.history[..].reverse().join '\n'}\n"
          repl.displayPrompt()
    
    getCommandId = (repl, commandName) ->
  • §

    Node 0.11 changed API, a command such as ‘.help’ is now stored as ‘help’

      commandsHaveLeadingDot = repl.commands['.help']?
      if commandsHaveLeadingDot then ".#{commandName}" else commandName
    
    module.exports =
      start: (opts = {}) ->
        [major, minor, build] = process.versions.node.split('.').map (n) -> parseInt(n, 10)
    
        if major < 6
          console.warn "Node 6+ required for CoffeeScript REPL"
          process.exit 1
    
        CoffeeScript.register()
        process.argv = ['coffee'].concat process.argv[2..]
        if opts.transpile
          transpile = {}
          try
            transpile.transpile = require('@babel/core').transform
          catch
            try
              transpile.transpile = require('babel-core').transform
            catch
              console.error '''
                To use --transpile with an interactive REPL, @babel/core must be installed either in the current folder or globally:
                  npm install --save-dev @babel/core
                or
                  npm install --global @babel/core
                And you must save options to configure Babel in one of the places it looks to find its options.
                See https://coffeescript.org/#transpilation
              '''
              process.exit 1
          transpile.options =
            filename: path.resolve process.cwd(), '<repl>'
  • §

    Since the REPL compilation path is unique (in eval above), we need another way to get the options object attached to a module so that it knows later on whether it needs to be transpiled. In the case of the REPL, the only applicable option is transpile.

          Module = require 'module'
          originalModuleLoad = Module::load
          Module::load = (filename) ->
            @options = transpile: transpile.options
            originalModuleLoad.call @, filename
        opts = merge replDefaults, opts
        repl = nodeREPL.start opts
        runInContext opts.prelude, repl.context, 'prelude' if opts.prelude
        repl.on 'exit', -> repl.outputStream.write '\n' if not repl.closed
        addMultilineHandler repl
        addHistory repl, opts.historyFile, opts.historyMaxInputSize if opts.historyFile
  • §

    Adapt help inherited from the node REPL

        repl.commands[getCommandId(repl, 'load')].help = 'Load code from a file into this REPL session'
        repl