• 跳至… +
    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
  • coffeescript.coffee

  • §

    CoffeeScript 可以在伺服器上使用,作為基於 Node.js/V8 的命令列編譯器,或直接在瀏覽器中執行 CoffeeScript。此模組包含用於將來源 CoffeeScript 代碼轉換為 JavaScript 的主要輸入函式、剖析和編譯。

    {Lexer}       = require './lexer'
    {parser}      = require './parser'
    helpers       = require './helpers'
    SourceMap     = require './sourcemap'
  • §

    需要 package.json,它比此檔案高兩層,因為此檔案是從 lib/coffeescript 評估的。

    packageJson   = require '../../package.json'
  • §

    目前的 CoffeeScript 版本號碼。

    exports.VERSION = packageJson.version
    
    exports.FILE_EXTENSIONS = FILE_EXTENSIONS = ['.coffee', '.litcoffee', '.coffee.md']
  • §

    揭露用於測試的輔助程式。

    exports.helpers = helpers
    
    {getSourceMap, registerCompiled} = SourceMap
  • §

    這會匯出以讓外部模組實作來源地圖快取。這僅在呼叫 patchStackTrace 以調整具有快取來源地圖的檔案的堆疊追蹤時使用。

    exports.registerCompiled = registerCompiled
  • §

    在 nodejs 和瀏覽器中允許使用 btoa 的函式。

    base64encode = (src) -> switch
      when typeof Buffer is 'function'
        Buffer.from(src).toString('base64')
      when typeof btoa is 'function'
  • §

    <script> 區塊的內容會透過 UTF-16 編碼,因此如果在區塊中使用任何延伸字元,btoa 會失敗,因為它的最大值為 UTF-8。請參閱 https://mdn.club.tw/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_Unicode_Problem 以取得血腥的詳細資料,以及在此實作的解決方案。

        btoa encodeURIComponent(src).replace /%([0-9A-F]{2})/g, (match, p1) ->
          String.fromCharCode '0x' + p1
      else
        throw new Error('Unable to base64 encode inline sourcemap.')
  • §

    函式包裝器,用於將來源檔案資訊新增至詞法分析器/剖析器/編譯器引發的 SyntaxError。

    withPrettyErrors = (fn) ->
      (code, options = {}) ->
        try
          fn.call @, code, options
        catch err
          throw err if typeof code isnt 'string' # Support `CoffeeScript.nodes(tokens)`.
          throw helpers.updateSyntaxError err, code, options.filename
  • §

    使用 Coffee/Jison 編譯器將 CoffeeScript 代碼編譯為 JavaScript。

    如果指定 options.sourceMap,則也必須指定 options.filename。所有可以傳遞至 SourceMap#generate 的選項也可以在此傳遞。

    這會傳回一個 javascript 字串,除非傳遞了 options.sourceMap,如果是這樣,這會傳回一個 {js, v3SourceMap, sourceMap} 物件,其中 sourceMap 是 sourcemap.coffee#SourceMap 物件,可輕鬆進行程式化查詢。

    exports.compile = compile = withPrettyErrors (code, options = {}) ->
  • §

    複製 options,以避免變異傳入的 options 物件。

      options = Object.assign {}, options
    
      generateSourceMap = options.sourceMap or options.inlineMap or not options.filename?
      filename = options.filename or helpers.anonymousFileName()
    
      checkShebangLine filename, code
    
      map = new SourceMap if generateSourceMap
    
      tokens = lexer.tokenize code, options
  • §

    傳遞一個參照變數清單,這樣產生的變數就不會得到相同的名稱。

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

    檢查 import 或 export;如果找到,強制裸模式。

      unless options.bare? and options.bare is yes
        for token in tokens
          if token[0] in ['IMPORT', 'EXPORT']
            options.bare = yes
            break
    
      nodes = parser.parse tokens
  • §

    如果所有要求的只是節點的 POJO 表示,例如抽象語法樹 (AST),我們現在可以停止並只傳回它(在修正根/File»Program 節點的位置資料後,由於詞法分析器中的 clean 函式,它可能會與原始來源錯位)。

      if options.ast
        nodes.allCommentTokens = helpers.extractAllCommentTokens tokens
        sourceCodeNumberOfLines = (code.match(/\r?\n/g) or '').length + 1
        sourceCodeLastLine = /.*$/.exec(code)[0] # `.*` matches all but line break characters.
        ast = nodes.ast options
        range = [0, code.length]
        ast.start = ast.program.start = range[0]
        ast.end = ast.program.end = range[1]
        ast.range = ast.program.range = range
        ast.loc.start = ast.program.loc.start = {line: 1, column: 0}
        ast.loc.end.line = ast.program.loc.end.line = sourceCodeNumberOfLines
        ast.loc.end.column = ast.program.loc.end.column = sourceCodeLastLine.length
        ast.tokens = tokens
        return ast
    
      fragments = nodes.compileToFragments options
    
      currentLine = 0
      currentLine += 1 if options.header
      currentLine += 1 if options.shiftLine
      currentColumn = 0
      js = ""
      for fragment in fragments
  • §

    使用每個片段的資料更新 sourcemap。

        if generateSourceMap
  • §

    不包含空白、空白或僅分號的片段。

          if fragment.locationData and not /^[;\s]*$/.test fragment.code
            map.add(
              [fragment.locationData.first_line, fragment.locationData.first_column]
              [currentLine, currentColumn]
              {noReplace: true})
          newLines = helpers.count fragment.code, "\n"
          currentLine += newLines
          if newLines
            currentColumn = fragment.code.length - (fragment.code.lastIndexOf("\n") + 1)
          else
            currentColumn += fragment.code.length
  • §

    將每個片段的程式碼複製到最終的 JavaScript 中。

        js += fragment.code
    
      if options.header
        header = "Generated by CoffeeScript #{@VERSION}"
        js = "// #{header}\n#{js}"
    
      if generateSourceMap
        v3SourceMap = map.generate options, code
    
      if options.transpile
        if typeof options.transpile isnt 'object'
  • §

    這只會在透過 Node API 執行且 transpile 設定為非物件時發生。

          throw new Error 'The transpile option must be given an object with options to pass to Babel'
  • §

    如果透過 CLI 或 Node API 執行此編譯器,取得我們已傳遞的 Babel 參照。

        transpiler = options.transpile.transpile
        delete options.transpile.transpile
    
        transpilerOptions = Object.assign {}, options.transpile
  • §

    請參閱 https://github.com/babel/babel/issues/827#issuecomment-77573107:Babel 可以將 v3 來源地圖物件作為 inputSourceMap 中的輸入,它會在輸出中傳回一個已更新的 v3 來源地圖物件。

        if v3SourceMap and not transpilerOptions.inputSourceMap?
          transpilerOptions.inputSourceMap = v3SourceMap
        transpilerOutput = transpiler js, transpilerOptions
        js = transpilerOutput.code
        if v3SourceMap and transpilerOutput.map
          v3SourceMap = transpilerOutput.map
    
      if options.inlineMap
        encoded = base64encode JSON.stringify v3SourceMap
        sourceMapDataURI = "//# sourceMappingURL=data:application/json;base64,#{encoded}"
        sourceURL = "//# sourceURL=#{filename}"
        js = "#{js}\n#{sourceMapDataURI}\n#{sourceURL}"
    
      registerCompiled filename, code, map
    
      if options.sourceMap
        {
          js
          sourceMap: map
          v3SourceMap: JSON.stringify v3SourceMap, null, 2
        }
      else
        js
  • §

    將 CoffeeScript 程式碼字串進行分詞,並傳回分詞陣列。

    exports.tokens = withPrettyErrors (code, options) ->
      lexer.tokenize code, options
  • §

    剖析 CoffeeScript 程式碼字串或分詞陣列,並傳回 AST。然後,你可以透過在根上呼叫 .compile() 來編譯它,或使用 .traverseChildren() 和回呼函式來遍歷它。

    exports.nodes = withPrettyErrors (source, options) ->
      source = lexer.tokenize source, options if typeof source is 'string'
      parser.parse source
  • §

    此檔案用於匯出這些方法;保留會擲回警告的存根。這些方法已移至 index.coffee 以為 Node 和非 Node 環境提供個別的進入點,如此一來,靜態分析工具在為非 Node 環境編譯時,才不會在 Node 套件上發生問題。

    exports.run = exports.eval = exports.register = ->
      throw new Error 'require index.coffee, not this file'
  • §

    在此為我們的用途建立一個 Lexer。

    lexer = new Lexer
  • §

    真正的 Lexer 會產生一個通用的 token 串流。此物件提供一個薄的包裝器,與 Jison API 相容。然後,我們可以直接將它傳遞為「Jison lexer」。

    parser.lexer =
      yylloc:
        range: []
      options:
        ranges: yes
      lex: ->
        token = parser.tokens[@pos++]
        if token
          [tag, @yytext, @yylloc] = token
          parser.errorToken = token.origin or token
          @yylineno = @yylloc.first_line
        else
          tag = ''
        tag
      setInput: (tokens) ->
        parser.tokens = tokens
        @pos = 0
      upcomingInput: -> ''
  • §

    讓解析器可以看到所有 AST 節點。

    parser.yy = require './nodes'
  • §

    覆寫 Jison 的預設錯誤處理函式。

    parser.yy.parseError = (message, {token}) ->
  • §

    略過 Jison 的訊息,它包含重複的行號資訊。略過 token,我們直接從 lexer 取得其值,以防錯誤是由產生的 token 造成的,而該 token 可能參考其來源。

      {errorToken, tokens} = parser
      [errorTag, errorText, errorLoc] = errorToken
    
      errorText = switch
        when errorToken is tokens[tokens.length - 1]
          'end of input'
        when errorTag in ['INDENT', 'OUTDENT']
          'indentation'
        when errorTag in ['IDENTIFIER', 'NUMBER', 'INFINITY', 'STRING', 'STRING_START', 'REGEX', 'REGEX_START']
          errorTag.replace(/_START$/, '').toLowerCase()
        else
          helpers.nameWhitespaceCharacter errorText
  • §

    第二個引數有一個 loc 屬性,該屬性應具有此 token 的位置資料。很不幸地,Jison 似乎傳送了一個過期的 loc(來自前一個 token),所以我們直接從 lexer 取得位置資訊。

      helpers.throwSyntaxError "unexpected #{errorText}", errorLoc
    
    exports.patchStackTrace = ->
  • §

    根據 http://v8.googlecode.com/svn/branches/bleeding_edge/src/messages.js 修改以處理 sourceMap

      formatSourcePosition = (frame, getSourceMapping) ->
        filename = undefined
        fileLocation = ''
    
        if frame.isNative()
          fileLocation = "native"
        else
          if frame.isEval()
            filename = frame.getScriptNameOrSourceURL()
            fileLocation = "#{frame.getEvalOrigin()}, " unless filename
          else
            filename = frame.getFileName()
    
          filename or= "<anonymous>"
    
          line = frame.getLineNumber()
          column = frame.getColumnNumber()
  • §

    檢查 sourceMap 位置

          source = getSourceMapping filename, line, column
          fileLocation =
            if source
              "#{filename}:#{source[0]}:#{source[1]}"
            else
              "#{filename}:#{line}:#{column}"
    
        functionName = frame.getFunctionName()
        isConstructor = frame.isConstructor()
        isMethodCall = not (frame.isToplevel() or isConstructor)
    
        if isMethodCall
          methodName = frame.getMethodName()
          typeName = frame.getTypeName()
    
          if functionName
            tp = as = ''
            if typeName and functionName.indexOf typeName
              tp = "#{typeName}."
            if methodName and functionName.indexOf(".#{methodName}") isnt functionName.length - methodName.length - 1
              as = " [as #{methodName}]"
    
            "#{tp}#{functionName}#{as} (#{fileLocation})"
          else
            "#{typeName}.#{methodName or '<anonymous>'} (#{fileLocation})"
        else if isConstructor
          "new #{functionName or '<anonymous>'} (#{fileLocation})"
        else if functionName
          "#{functionName} (#{fileLocation})"
        else
          fileLocation
    
      getSourceMapping = (filename, line, column) ->
        sourceMap = getSourceMap filename, line, column
    
        answer = sourceMap.sourceLocation [line - 1, column - 1] if sourceMap?
        if answer? then [answer[0] + 1, answer[1] + 1] else null
  • §

    根據 michaelficarra/CoffeeScriptRedux NodeJS/V8 不支援使用 sourceMap 轉換堆疊追蹤中的位置,所以我們必須使用 monkey-patch Error 來顯示 CoffeeScript 來源位置。

      Error.prepareStackTrace = (err, stack) ->
        frames = for frame in stack
  • §

    不要顯示比 CoffeeScript.run 更深的堆疊框架。

          break if frame.getFunction() is exports.run
          "    at #{formatSourcePosition frame, getSourceMapping}"
    
        "#{err.toString()}\n#{frames.join '\n'}\n"
    
    checkShebangLine = (file, input) ->
      firstLine = input.split(/$/m, 1)[0]
      rest = firstLine?.match(/^#!\s*([^\s]+\s*)(.*)/)
      args = rest?[2]?.split(/\s/).filter (s) -> s isnt ''
      if args?.length > 1
        console.error '''
          The script to be run begins with a shebang line with more than one
          argument. This script will fail on platforms such as Linux which only
          allow a single argument.
        '''
        console.error "The shebang line was: '#{firstLine}' in file '#{file}'"
        console.error "The arguments were: #{JSON.stringify args}"