• Jump To … +
    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 can be used both on the server, as a command-line compiler based on Node.js/V8, or to run CoffeeScript directly in the browser. This module contains the main entry functions for tokenizing, parsing, and compiling source CoffeeScript into JavaScript.

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

    The current CoffeeScript version number.

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

    Expose helpers for testing.

    exports.helpers = helpers
  • ¶

    Function that allows for btoa in both nodejs and the browser.

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

    The contents of a <script> block are encoded via UTF-16, so if any extended characters are used in the block, btoa will fail as it maxes out at UTF-8. See https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_Unicode_Problem for the gory details, and for the solution implemented here.

        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.')
  • ¶

    Function wrapper to add source file information to SyntaxErrors thrown by the lexer/parser/compiler.

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

    Compile CoffeeScript code to JavaScript, using the Coffee/Jison compiler.

    If options.sourceMap is specified, then options.filename must also be specified. All options that can be passed to SourceMap#generate may also be passed here.

    This returns a javascript string, unless options.sourceMap is passed, in which case this returns a {js, v3SourceMap, sourceMap} object, where sourceMap is a sourcemap.coffee#SourceMap object, handy for doing programatic lookups.

    exports.compile = compile = withPrettyErrors (code, options) ->
      {merge, extend} = helpers
      options = extend {}, options
      generateSourceMap = options.sourceMap or options.inlineMap
    
      if generateSourceMap
        map = new SourceMap
    
      tokens = lexer.tokenize code, options
  • ¶

    Pass a list of referenced variables, so that generated variables won’t get the same name.

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

    Check for import or export; if found, force bare mode

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

    Update the sourcemap with data from each fragment

        if generateSourceMap
  • ¶

    Do not include empty, whitespace, or semicolon-only fragments.

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

    Copy the code from each fragment into the final 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.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
  • ¶

    Tokenize a string of CoffeeScript code, and return the array of tokens.

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

    Parse a string of CoffeeScript code or an array of lexed tokens, and return the AST. You can then compile it by calling .compile() on the root, or traverse it by using .traverseChildren() with a callback.

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

    Compile and execute a string of CoffeeScript (on the server), correctly setting __filename, __dirname, and relative require().

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

    Set the filename.

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

    Clear the module cache.

      mainModule.moduleCache and= {}
  • ¶

    Assign paths for node_modules loading

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

    Compile.

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

    Compile and evaluate a string of CoffeeScript (in a Node.js-like environment). The CoffeeScript REPL uses this to run the input.

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

    define module/require only if they chose not to specify their own

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

    use the same hack node currently uses for their own 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'
  • ¶

    Throw error with deprecation warning when depending upon implicit require.extensions registration

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

    As the filename and code of a dynamically loaded file will be different from the original file compiled with CoffeeScript.run, add that information to error so it can be pretty-printed later.

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

    Instantiate a Lexer for our use here.

    lexer = new Lexer
  • ¶

    The real Lexer produces a generic stream of tokens. This object provides a thin wrapper around it, compatible with the Jison API. We can then pass it directly as a “Jison lexer”.

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

    Make all the AST nodes visible to the parser.

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

    Override Jison’s default error handling function.

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

    Disregard Jison’s message, it contains redundant line number information. Disregard the token, we take its value directly from the lexer in case the error is caused by a generated token which might refer to its origin.

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

    The second argument has a loc property, which should have the location data for this token. Unfortunately, Jison seems to send an outdated loc (from the previous token), so we take the location information directly from the lexer.

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

    Based on http://v8.googlecode.com/svn/branches/bleeding_edge/src/messages.js Modified to handle 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()
  • ¶

    Check for a sourceMap position

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

    Map of filenames -> sourceMap object.

    sourceMaps = {}
  • ¶

    Generates the source map for a coffee file and stores it in the local cache variable.

    getSourceMap = (filename) ->
      return sourceMaps[filename] if sourceMaps[filename]
      for ext in exports.FILE_EXTENSIONS
        if helpers.ends filename, ext
          answer = exports._compileFile filename, true
          return sourceMaps[filename] = answer.sourceMap
      return null
  • ¶

    Based on michaelficarra/CoffeeScriptRedux NodeJS / V8 have no support for transforming positions in stack traces using sourceMap, so we must monkey-patch Error to display CoffeeScript source positions.

    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"