PageRenderTime 53ms CodeModel.GetById 34ms app.highlight 15ms 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
  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"