coffeescript.coffee

- Jump To … +
  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 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.

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

- §

  Require `package.json`, which is two levels above this file, as this file is evaluated from `lib/coffeescript`.

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

- §

  The current CoffeeScript version number.

      exports.VERSION = packageJson.version

      exports.FILE_EXTENSIONS = FILE_EXTENSIONS = ['.coffee', '.litcoffee', '.coffee.md']

- §

  Expose helpers for testing.

      exports.helpers = helpers

      {getSourceMap, registerCompiled} = SourceMap

- §

  This is exported to enable an external module to implement caching of sourcemaps. This is used only when `patchStackTrace` has been called to adjust stack traces for files with cached source maps.

      exports.registerCompiled = registerCompiled

- §

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

      base64encode = (src) -> switch
        when typeof Buffer is 'function'
          Buffer.from(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 programmatic lookups.

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

- §

  Clone `options`, to avoid mutating the `options` object passed in.

        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

- §

  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

        nodes = parser.parse tokens

- §

  If all that was requested was a POJO representation of the nodes, e.g. the abstract syntax tree (AST), we can stop now and just return that (after fixing the location data for the root/`File`»`Program` node, which might’ve gotten misaligned from the original source due to the `clean` function in the lexer).

        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

- §

  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.transpile
          if typeof options.transpile isnt 'object'

- §

  This only happens if run via the Node API and `transpile` is set to something other than an object.

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

- §

  Get the reference to Babel that we have been passed if this compiler is run via the CLI or Node API.

          transpiler = options.transpile.transpile
          delete options.transpile.transpile

          transpilerOptions = Object.assign {}, options.transpile

- §

  See https://github.com/babel/babel/issues/827#issuecomment-77573107: Babel can take a v3 source map object as input in `inputSourceMap` and it will return an updated v3 source map object in its output.

          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

- §

  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) ->
        source = lexer.tokenize source, options if typeof source is 'string'
        parser.parse source

- §

  This file used to export these methods; leave stubs that throw warnings instead. These methods have been moved into `index.coffee` to provide separate entrypoints for Node and non-Node environments, so that static analysis tools don’t choke on Node packages when compiling for a non-Node environment.

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

- §

  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 =
        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: -> ''

- §

  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

      exports.patchStackTrace = ->

- §

  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

        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

- §

  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) ->
          frames = for frame in stack

- §

  Don’t display stack frames deeper than `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)[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}"