• 跳至… +
    browser.coffee cake.coffee coffee-script.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
  • coffee-script.coffee

  • ¶

    CoffeeScript 可用於伺服器上,作為基於 Node.js/V8 的指令碼編譯器,或直接在瀏覽器中執行 CoffeeScript。此模組包含將來源 CoffeeScript 代碼化為 JavaScript 的主要輸入函數,用於進行標記化、解析和編譯。

    fs            = require 'fs'
    vm            = require 'vm'
    path          = require 'path'
    {Lexer}       = require './lexer'
    {parser}      = require './parser'
    helpers       = require './helpers'
    SourceMap     = require './sourcemap'
  • ¶

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

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

    目前的 CoffeeScript 版本號碼。

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

    公開輔助程式以進行測試。

    exports.helpers = helpers
  • ¶

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

    base64encode = (src) -> switch
      when typeof Buffer is 'function'
        new Buffer(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.')
  • ¶

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

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

    對於每個編譯檔案,將其原始碼儲存在記憶體中,以防我們需要稍後重新編譯它。如果第一次編譯沒有建立原始碼對應(較快),但發生錯誤,我們需要堆疊追蹤,我們可能需要重新編譯。假設大部分時間,程式碼不會擲回例外,在我們需要堆疊追蹤時才編譯兩次,可能比在不太可能使用時總是產生原始碼對應更有效率。以 檔名:(原始碼) 的形式儲存

    sources = {}
  • ¶

    如果產生,也以 檔名:(原始碼對應) 的形式儲存原始碼對應。

    sourceMaps = {}
  • ¶

    使用 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) ->
      {merge, extend} = helpers
      options = extend {}, options
  • ¶

    如果沒有傳入檔名,請務必產生原始碼對應,因為沒有檔名,我們無法在稍後需要重新編譯它以取得 prepareStackTrace 的原始碼對應時,擷取此原始碼。

      generateSourceMap = options.sourceMap or options.inlineMap or not options.filename?
      filename = options.filename or '<anonymous>'
    
      sources[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;如果找到,強制使用 bare 模式。

      unless options.bare? and options.bare is yes
        for token in tokens
          if token[0] in ['IMPORT', 'EXPORT']
            options.bare = yes
            break
    
      fragments = parser.parse(tokens).compileToFragments options
    
      currentLine = 0
      currentLine += 1 if options.header
      currentLine += 1 if options.shiftLine
      currentColumn = 0
      js = ""
      for fragment in fragments
  • ¶

    使用每個片段的資料更新原始碼對應。

        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)
        sourceMaps[filename] = map
    
      if options.inlineMap
        encoded = base64encode JSON.stringify v3SourceMap
        sourceMapDataURI = "//# sourceMappingURL=data:application/json;base64,#{encoded}"
        sourceURL = "//# sourceURL=#{options.filename ? 'coffeescript'}"
        js = "#{js}\n#{sourceMapDataURI}\n#{sourceURL}"
    
      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) ->
      if typeof source is 'string'
        parser.parse lexer.tokenize source, options
      else
        parser.parse source
  • ¶

    編譯並執行 CoffeeScript 字串(在伺服器上),正確設定 __filename、__dirname 和相對的 require()。

    exports.run = (code, options = {}) ->
      mainModule = require.main
  • ¶

    設定檔案名稱。

      mainModule.filename = process.argv[1] =
        if options.filename then fs.realpathSync(options.filename) else '<anonymous>'
  • ¶

    清除模組快取。

      mainModule.moduleCache and= {}
  • ¶

    指定 node_modules 載入路徑

      dir = if options.filename?
        path.dirname fs.realpathSync options.filename
      else
        fs.realpathSync '.'
      mainModule.paths = require('module')._nodeModulePaths dir
  • ¶

    編譯。

      if not helpers.isCoffee(mainModule.filename) or require.extensions
        answer = compile code, options
        code = answer.js ? answer
    
      mainModule._compile code, mainModule.filename
  • ¶

    編譯並評估 CoffeeScript 字串(在類 Node.js 環境中)。CoffeeScript REPL 使用此功能來執行輸入。

    exports.eval = (code, options = {}) ->
      return unless code = code.trim()
      createContext = vm.Script.createContext ? vm.createContext
    
      isContext = vm.isContext ? (ctx) ->
        options.sandbox instanceof createContext().constructor
    
      if createContext
        if options.sandbox?
          if isContext options.sandbox
            sandbox = options.sandbox
          else
            sandbox = createContext()
            sandbox[k] = v for own k, v of options.sandbox
          sandbox.global = sandbox.root = sandbox.GLOBAL = sandbox
        else
          sandbox = global
        sandbox.__filename = options.filename || 'eval'
        sandbox.__dirname  = path.dirname sandbox.__filename
  • ¶

    僅在使用者選擇不指定自己的情況下定義模組/require

        unless sandbox isnt global or sandbox.module or sandbox.require
          Module = require 'module'
          sandbox.module  = _module  = new Module(options.modulename || 'eval')
          sandbox.require = _require = (path) ->  Module._load path, _module, true
          _module.filename = sandbox.__filename
          for r in Object.getOwnPropertyNames require when r not in ['paths', 'arguments', 'caller']
            _require[r] = require[r]
  • ¶

    使用目前 node 在其自己的 REPL 中使用的相同技巧

          _require.paths = _module.paths = Module._nodeModulePaths process.cwd()
          _require.resolve = (request) -> Module._resolveFilename request, _module
      o = {}
      o[k] = v for own k, v of options
      o.bare = on # ensure return value
      js = compile code, o
      if sandbox is global
        vm.runInThisContext js
      else
        vm.runInContext js, sandbox
    
    exports.register = -> require './register'
  • ¶

    在依賴隱式 require.extensions 註冊時擲回錯誤,並顯示不建議使用的警告

    if require.extensions
      for ext in @FILE_EXTENSIONS then do (ext) ->
        require.extensions[ext] ?= ->
          throw new Error """
          Use CoffeeScript.register() or require the coffee-script/register module to require #{ext} files.
          """
    
    exports._compileFile = (filename, sourceMap = no, inlineMap = no) ->
      raw = fs.readFileSync filename, 'utf8'
  • ¶

    移除 Unicode 位元組順序標記(如果此檔案以該標記開頭)。

      stripped = if raw.charCodeAt(0) is 0xFEFF then raw.substring 1 else raw
    
      try
        answer = compile stripped, {
          filename, sourceMap, inlineMap
          sourceFiles: [filename]
          literate: helpers.isLiterate filename
        }
      catch err
  • ¶

    由於動態載入檔案的名稱和程式碼會與使用 CoffeeScript.run 編譯的原始檔案不同,因此將該資訊加入錯誤中,以便稍後可以漂亮列印。

        throw helpers.updateSyntaxError err, stripped, filename
    
      answer
  • ¶

    在這裡為我們自己建立一個詞法分析器。

    lexer = new Lexer
  • ¶

    真正的詞法分析器會產生一般化的代碼串流。此物件提供一個薄的包裝器,與 Jison API 相容。然後我們可以直接將它傳遞為「Jison 詞法分析器」。

    parser.lexer =
      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 的訊息,它包含了多餘的行號資訊。略過令牌,我們直接從詞法分析器取得其值,以防錯誤是由可能參考其來源的已產生令牌所造成。

      {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 屬性,它應該有這個令牌的位置資料。不幸的是,Jison 似乎傳送了一個過期的 loc(來自上一個令牌),所以我們直接從詞法分析器取得位置資訊。

      helpers.throwSyntaxError "unexpected #{errorText}", errorLoc
  • ¶

    根據 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
    
    getSourceMap = (filename) ->
      if sourceMaps[filename]?
        sourceMaps[filename]
  • ¶

    在瀏覽器中編譯的 CoffeeScript 可能會使用 <anonymous> 的 options.filename 編譯,但瀏覽器可能會使用腳本檔案的檔名來要求堆疊追蹤。

      else if sourceMaps['<anonymous>']?
        sourceMaps['<anonymous>']
      else if sources[filename]?
        answer = compile sources[filename],
          filename: filename
          sourceMap: yes
          literate: helpers.isLiterate filename
        answer.sourceMap
      else
        null
  • ¶

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

    Error.prepareStackTrace = (err, stack) ->
      getSourceMapping = (filename, line, column) ->
        sourceMap = getSourceMap filename
        answer = sourceMap.sourceLocation [line - 1, column - 1] if sourceMap?
        if answer? then [answer[0] + 1, answer[1] + 1] else null
    
      frames = for frame in stack
        break if frame.getFunction() is exports.run
        "    at #{formatSourcePosition frame, getSourceMapping}"
    
      "#{err.toString()}\n#{frames.join '\n'}\n"