/src/coffee-script.coffee
CoffeeScript | 348 lines | 232 code | 53 blank | 63 comment | 49 complexity | bfd6368c5e5d634e5ac05a5ab10d4fba MD5 | raw file
1# CoffeeScript can be used both on the server, as a command-line compiler based 2# on Node.js/V8, or to run CoffeeScript directly in the browser. This module 3# contains the main entry functions for tokenizing, parsing, and compiling 4# source CoffeeScript into JavaScript. 5 6fs = require 'fs' 7vm = require 'vm' 8path = require 'path' 9{Lexer} = require './lexer' 10{parser} = require './parser' 11helpers = require './helpers' 12SourceMap = require './sourcemap' 13 14# The current CoffeeScript version number. 15exports.VERSION = '1.10.0' 16 17exports.FILE_EXTENSIONS = ['.coffee', '.litcoffee', '.coffee.md'] 18 19# Expose helpers for testing. 20exports.helpers = helpers 21 22# Function that allows for btoa in both nodejs and the browser. 23base64encode = (src) -> switch 24 when typeof Buffer is 'function' 25 new Buffer(src).toString('base64') 26 when typeof btoa is 'function' 27 btoa(src) 28 else 29 throw new Error('Unable to base64 encode inline sourcemap.') 30 31# Function wrapper to add source file information to SyntaxErrors thrown by the 32# lexer/parser/compiler. 33withPrettyErrors = (fn) -> 34 (code, options = {}) -> 35 try 36 fn.call @, code, options 37 catch err 38 throw err if typeof code isnt 'string' # Support `CoffeeScript.nodes(tokens)`. 39 throw helpers.updateSyntaxError err, code, options.filename 40 41# Compile CoffeeScript code to JavaScript, using the Coffee/Jison compiler. 42# 43# If `options.sourceMap` is specified, then `options.filename` must also be specified. All 44# options that can be passed to `SourceMap#generate` may also be passed here. 45# 46# This returns a javascript string, unless `options.sourceMap` is passed, 47# in which case this returns a `{js, v3SourceMap, sourceMap}` 48# object, where sourceMap is a sourcemap.coffee#SourceMap object, handy for doing programatic 49# lookups. 50exports.compile = compile = withPrettyErrors (code, options) -> 51 {merge, extend} = helpers 52 options = extend {}, options 53 generateSourceMap = options.sourceMap or options.inlineMap 54 55 if generateSourceMap 56 map = new SourceMap 57 58 tokens = lexer.tokenize code, options 59 60 # Pass a list of referenced variables, so that generated variables won't get 61 # the same name. 62 options.referencedVars = ( 63 token[1] for token in tokens when token[0] is 'IDENTIFIER' 64 ) 65 66 fragments = parser.parse(tokens).compileToFragments options 67 68 currentLine = 0 69 currentLine += 1 if options.header 70 currentLine += 1 if options.shiftLine 71 currentColumn = 0 72 js = "" 73 for fragment in fragments 74 # Update the sourcemap with data from each fragment 75 if generateSourceMap 76 # Do not include empty, whitespace, or semicolon-only fragments. 77 if fragment.locationData and not /^[;\s]*$/.test fragment.code 78 map.add( 79 [fragment.locationData.first_line, fragment.locationData.first_column] 80 [currentLine, currentColumn] 81 {noReplace: true}) 82 newLines = helpers.count fragment.code, "\n" 83 currentLine += newLines 84 if newLines 85 currentColumn = fragment.code.length - (fragment.code.lastIndexOf("\n") + 1) 86 else 87 currentColumn += fragment.code.length 88 89 # Copy the code from each fragment into the final JavaScript. 90 js += fragment.code 91 92 if options.header 93 header = "Generated by CoffeeScript #{@VERSION}" 94 js = "// #{header}\n#{js}" 95 96 if generateSourceMap 97 v3SourceMap = map.generate(options, code) 98 99 if options.inlineMap 100 encoded = base64encode JSON.stringify v3SourceMap 101 sourceMapDataURI = "//# sourceMappingURL=data:application/json;base64,#{encoded}" 102 sourceURL = "//# sourceURL=#{options.filename ? 'coffeescript'}" 103 js = "#{js}\n#{sourceMapDataURI}\n#{sourceURL}" 104 105 if options.sourceMap 106 { 107 js 108 sourceMap: map 109 v3SourceMap: JSON.stringify v3SourceMap, null, 2 110 } 111 else 112 js 113 114# Tokenize a string of CoffeeScript code, and return the array of tokens. 115exports.tokens = withPrettyErrors (code, options) -> 116 lexer.tokenize code, options 117 118# Parse a string of CoffeeScript code or an array of lexed tokens, and 119# return the AST. You can then compile it by calling `.compile()` on the root, 120# or traverse it by using `.traverseChildren()` with a callback. 121exports.nodes = withPrettyErrors (source, options) -> 122 if typeof source is 'string' 123 parser.parse lexer.tokenize source, options 124 else 125 parser.parse source 126 127# Compile and execute a string of CoffeeScript (on the server), correctly 128# setting `__filename`, `__dirname`, and relative `require()`. 129exports.run = (code, options = {}) -> 130 mainModule = require.main 131 132 # Set the filename. 133 mainModule.filename = process.argv[1] = 134 if options.filename then fs.realpathSync(options.filename) else '.' 135 136 # Clear the module cache. 137 mainModule.moduleCache and= {} 138 139 # Assign paths for node_modules loading 140 dir = if options.filename 141 path.dirname fs.realpathSync options.filename 142 else 143 fs.realpathSync '.' 144 mainModule.paths = require('module')._nodeModulePaths dir 145 146 # Compile. 147 if not helpers.isCoffee(mainModule.filename) or require.extensions 148 answer = compile code, options 149 code = answer.js ? answer 150 151 mainModule._compile code, mainModule.filename 152 153# Compile and evaluate a string of CoffeeScript (in a Node.js-like environment). 154# The CoffeeScript REPL uses this to run the input. 155exports.eval = (code, options = {}) -> 156 return unless code = code.trim() 157 createContext = vm.Script.createContext ? vm.createContext 158 159 isContext = vm.isContext ? (ctx) -> 160 options.sandbox instanceof createContext().constructor 161 162 if createContext 163 if options.sandbox? 164 if isContext options.sandbox 165 sandbox = options.sandbox 166 else 167 sandbox = createContext() 168 sandbox[k] = v for own k, v of options.sandbox 169 sandbox.global = sandbox.root = sandbox.GLOBAL = sandbox 170 else 171 sandbox = global 172 sandbox.__filename = options.filename || 'eval' 173 sandbox.__dirname = path.dirname sandbox.__filename 174 # define module/require only if they chose not to specify their own 175 unless sandbox isnt global or sandbox.module or sandbox.require 176 Module = require 'module' 177 sandbox.module = _module = new Module(options.modulename || 'eval') 178 sandbox.require = _require = (path) -> Module._load path, _module, true 179 _module.filename = sandbox.__filename 180 for r in Object.getOwnPropertyNames require when r not in ['paths', 'arguments', 'caller'] 181 _require[r] = require[r] 182 # use the same hack node currently uses for their own REPL 183 _require.paths = _module.paths = Module._nodeModulePaths process.cwd() 184 _require.resolve = (request) -> Module._resolveFilename request, _module 185 o = {} 186 o[k] = v for own k, v of options 187 o.bare = on # ensure return value 188 js = compile code, o 189 if sandbox is global 190 vm.runInThisContext js 191 else 192 vm.runInContext js, sandbox 193 194exports.register = -> require './register' 195 196# Throw error with deprecation warning when depending upon implicit `require.extensions` registration 197if require.extensions 198 for ext in @FILE_EXTENSIONS then do (ext) -> 199 require.extensions[ext] ?= -> 200 throw new Error """ 201 Use CoffeeScript.register() or require the coffee-script/register module to require #{ext} files. 202 """ 203 204exports._compileFile = (filename, sourceMap = no, inlineMap = no) -> 205 raw = fs.readFileSync filename, 'utf8' 206 stripped = if raw.charCodeAt(0) is 0xFEFF then raw.substring 1 else raw 207 208 try 209 answer = compile stripped, { 210 filename, sourceMap, inlineMap 211 sourceFiles: [filename] 212 literate: helpers.isLiterate filename 213 } 214 catch err 215 # As the filename and code of a dynamically loaded file will be different 216 # from the original file compiled with CoffeeScript.run, add that 217 # information to error so it can be pretty-printed later. 218 throw helpers.updateSyntaxError err, stripped, filename 219 220 answer 221 222# Instantiate a Lexer for our use here. 223lexer = new Lexer 224 225# The real Lexer produces a generic stream of tokens. This object provides a 226# thin wrapper around it, compatible with the Jison API. We can then pass it 227# directly as a "Jison lexer". 228parser.lexer = 229 lex: -> 230 token = parser.tokens[@pos++] 231 if token 232 [tag, @yytext, @yylloc] = token 233 parser.errorToken = token.origin or token 234 @yylineno = @yylloc.first_line 235 else 236 tag = '' 237 238 tag 239 setInput: (tokens) -> 240 parser.tokens = tokens 241 @pos = 0 242 upcomingInput: -> 243 "" 244# Make all the AST nodes visible to the parser. 245parser.yy = require './nodes' 246 247# Override Jison's default error handling function. 248parser.yy.parseError = (message, {token}) -> 249 # Disregard Jison's message, it contains redundant line number information. 250 # Disregard the token, we take its value directly from the lexer in case 251 # the error is caused by a generated token which might refer to its origin. 252 {errorToken, tokens} = parser 253 [errorTag, errorText, errorLoc] = errorToken 254 255 errorText = switch 256 when errorToken is tokens[tokens.length - 1] 257 'end of input' 258 when errorTag in ['INDENT', 'OUTDENT'] 259 'indentation' 260 when errorTag in ['IDENTIFIER', 'NUMBER', 'INFINITY', 'STRING', 'STRING_START', 'REGEX', 'REGEX_START'] 261 errorTag.replace(/_START$/, '').toLowerCase() 262 else 263 helpers.nameWhitespaceCharacter errorText 264 265 # The second argument has a `loc` property, which should have the location 266 # data for this token. Unfortunately, Jison seems to send an outdated `loc` 267 # (from the previous token), so we take the location information directly 268 # from the lexer. 269 helpers.throwSyntaxError "unexpected #{errorText}", errorLoc 270 271# Based on http://v8.googlecode.com/svn/branches/bleeding_edge/src/messages.js 272# Modified to handle sourceMap 273formatSourcePosition = (frame, getSourceMapping) -> 274 fileName = undefined 275 fileLocation = '' 276 277 if frame.isNative() 278 fileLocation = "native" 279 else 280 if frame.isEval() 281 fileName = frame.getScriptNameOrSourceURL() 282 fileLocation = "#{frame.getEvalOrigin()}, " unless fileName 283 else 284 fileName = frame.getFileName() 285 286 fileName or= "<anonymous>" 287 288 line = frame.getLineNumber() 289 column = frame.getColumnNumber() 290 291 # Check for a sourceMap position 292 source = getSourceMapping fileName, line, column 293 fileLocation = 294 if source 295 "#{fileName}:#{source[0]}:#{source[1]}" 296 else 297 "#{fileName}:#{line}:#{column}" 298 299 functionName = frame.getFunctionName() 300 isConstructor = frame.isConstructor() 301 isMethodCall = not (frame.isToplevel() or isConstructor) 302 303 if isMethodCall 304 methodName = frame.getMethodName() 305 typeName = frame.getTypeName() 306 307 if functionName 308 tp = as = '' 309 if typeName and functionName.indexOf typeName 310 tp = "#{typeName}." 311 if methodName and functionName.indexOf(".#{methodName}") isnt functionName.length - methodName.length - 1 312 as = " [as #{methodName}]" 313 314 "#{tp}#{functionName}#{as} (#{fileLocation})" 315 else 316 "#{typeName}.#{methodName or '<anonymous>'} (#{fileLocation})" 317 else if isConstructor 318 "new #{functionName or '<anonymous>'} (#{fileLocation})" 319 else if functionName 320 "#{functionName} (#{fileLocation})" 321 else 322 fileLocation 323 324# Map of filenames -> sourceMap object. 325sourceMaps = {} 326 327# Generates the source map for a coffee file and stores it in the local cache variable. 328getSourceMap = (filename) -> 329 return sourceMaps[filename] if sourceMaps[filename] 330 return unless path?.extname(filename) in exports.FILE_EXTENSIONS 331 answer = exports._compileFile filename, true 332 sourceMaps[filename] = answer.sourceMap 333 334# Based on [michaelficarra/CoffeeScriptRedux](http://goo.gl/ZTx1p) 335# NodeJS / V8 have no support for transforming positions in stack traces using 336# sourceMap, so we must monkey-patch Error to display CoffeeScript source 337# positions. 338Error.prepareStackTrace = (err, stack) -> 339 getSourceMapping = (filename, line, column) -> 340 sourceMap = getSourceMap filename 341 answer = sourceMap.sourceLocation [line - 1, column - 1] if sourceMap 342 if answer then [answer[0] + 1, answer[1] + 1] else null 343 344 frames = for frame in stack 345 break if frame.getFunction() is exports.run 346 " at #{formatSourcePosition frame, getSourceMapping}" 347 348 "#{err.toString()}\n#{frames.join '\n'}\n"