• ジャンプ先 … +
    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
  • §

    コメントを表示するだけのトークンをフィルタリングします。

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

    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*$/
  • §

    評価、印刷、ループ

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

    ファイル情報と最大サイズのコマンド履歴を取得します

        stat = fs.statSync filename
        size = Math.min maxSize, stat.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.dokyumento.jp/#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