• 跳到… +
    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:多行 hack。

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

    過濾掉僅用於保留註解而產生的符號。

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

    產生符號的 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: ''
  • §

    代理節點行偵聽器

      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、列印、迴圈

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

    XXX:多行 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
  • §

    從檔案儲存和載入命令記錄

    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.lang.tw/#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
  • §

    調整從節點 REPL 繼承的說明

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