PageRenderTime 34ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

/src/coffee-script.coffee

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