/src/coffee-script.coffee
CoffeeScript | 348 lines | 232 code | 53 blank | 63 comment | 49 complexity | bfd6368c5e5d634e5ac05a5ab10d4fba MD5 | raw file
- # 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.10.0'
- 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'
- btoa(src)
- 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'
- )
- 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]
- return unless path?.extname(filename) in exports.FILE_EXTENSIONS
- answer = exports._compileFile filename, true
- sourceMaps[filename] = answer.sourceMap
- # Based on [michaelficarra/CoffeeScriptRedux](http://goo.gl/ZTx1p)
- # 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"