• 跳转到… +
    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: 多行代码的处理。

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

    Node 的 REPL 会将以换行符结尾的输入用括号包裹。需要将这些括号去除。

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

    Node 的 REPL v6.9.1+ 会将输入用 try/catch 语句包裹。也需要去除这些语句。

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

    需要使用 AST 节点进行一些 AST 操作。

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

    将清理后的输入进行词法分析。

          tokens = CoffeeScript.tokens input
  • §

    过滤掉仅用于保存注释的 token。

          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()
  • §

    像 CoffeeScript.compile 一样收集引用的变量名。

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

    根据 token 生成 AST。

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

    添加对 __ 变量的赋值,以强制输入为表达式。

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

    将表达式包裹在闭包中,以支持顶层 await。

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

    调用包裹闭包。

          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
  • §

    去除 "use strict",以避免在给未声明的变量 __ 赋值时出现异常。

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

    如果需要,等待异步结果。

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

    AST 的 compile 方法不会在语法错误中添加源代码信息。

          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 改变了 API,提示符现在是 _prompt。

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

    代理 node 的行监听器。

      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
  • §

    处理 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
  • §

    允许在输入多行之前随时任意切换模式。

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

    除非当前行为空,否则不执行任何操作。

          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.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
  • §

    从文件存储和加载命令历史记录。

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

    获取文件信息和最多 maxSize 的命令历史记录。

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

    从文件中读取最后 size 个字节。

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

    在解释器上设置历史记录。

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

    如果历史记录文件被截断,我们应该弹出潜在的未完成行。

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

    移出最后的空换行符。

        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
  • §

    将最新的命令保存到文件中。

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

    XXX: 来自 REPLServer 的 SIGINT 事件是未记录的,所以这有点脆弱。

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

    添加一个命令来显示历史记录栈。

      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 改变了 API,例如 ‘.help’ 这样的命令现在存储为 ‘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.node.org.cn/#transpilation
              '''
              process.exit 1
          transpile.options =
            filename: path.resolve process.cwd(), '<repl>'
  • §

    由于 REPL 编译路径是唯一的(在上面的 eval 中),我们需要另一种方法来获取附加到模块的 options 对象,以便它稍后知道是否需要进行转译。在 REPL 的情况下,唯一适用的选项是 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
  • §

    调整从 node REPL 继承的帮助信息。

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