on 448bfaf help sign in
391 lines 16 kB history raw
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.
6{Lexer} = require './lexer'
7{parser} = require './parser'
8helpers = require './helpers'
9SourceMap = require './sourcemap'
10# Require `package.json`, which is two levels above this file, as this file is
11# evaluated from `lib/coffeescript`.
12packageJson = require '../../package.json'
14# The current CoffeeScript version number.
15exports.VERSION = packageJson.version
17exports.FILE_EXTENSIONS = FILE_EXTENSIONS = ['.coffee', '.litcoffee', '.coffee.md']
19# Expose helpers for testing.
20exports.helpers = helpers
22# Function that allows for btoa in both nodejs and the browser.
23base64encode = (src) -> switch
24 when typeof Buffer is 'function'
25 Buffer.from(src).toString('base64')
26 when typeof btoa is 'function'
27 # The contents of a `<script>` block are encoded via UTF-16, so if any extended
28 # characters are used in the block, btoa will fail as it maxes out at UTF-8.
29 # See https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_Unicode_Problem
30 # for the gory details, and for the solution implemented here.
31 btoa encodeURIComponent(src).replace /%([0-9A-F]{2})/g, (match, p1) ->
32 String.fromCharCode '0x' + p1
33 else
34 throw new Error('Unable to base64 encode inline sourcemap.')
36# Function wrapper to add source file information to SyntaxErrors thrown by the
37# lexer/parser/compiler.
38withPrettyErrors = (fn) ->
39 (code, options = {}) ->
40 try
41 fn.call @, code, options
42 catch err
43 throw err if typeof code isnt 'string' # Support `CoffeeScript.nodes(tokens)`.
44 throw helpers.updateSyntaxError err, code, options.filename
46# For each compiled file, save its source in memory in case we need to
47# recompile it later. We might need to recompile if the first compilation
48# didn’t create a source map (faster) but something went wrong and we need
49# a stack trace. Assuming that most of the time, code isn’t throwing
50# exceptions, it’s probably more efficient to compile twice only when we
51# need a stack trace, rather than always generating a source map even when
52# it’s not likely to be used. Save in form of `filename`: [`(source)`]
53sources = {}
54# Also save source maps if generated, in form of `(source)`: [`(source map)`].
55sourceMaps = {}
57# This is exported to enable an external module to implement caching of
58# compilation results. When the compiled js source is loaded from cache, the
59# original coffee code should be added with this method in order to enable the
60# Error.prepareStackTrace below to correctly adjust the stack trace for the
61# corresponding file (the source map will be generated on demand).
62exports.registerCompiled = registerCompiled = (filename, source, sourcemap) ->
64 sources[filename] ?= []
65 sources[filename].push source
67 if sourcemap?
68 sourceMaps[filename] ?= []
69 sourceMaps[filename].push sourcemap
71# Compile CoffeeScript code to JavaScript, using the Coffee/Jison compiler.
73# If `options.sourceMap` is specified, then `options.filename` must also be
74# specified. All options that can be passed to `SourceMap#generate` may also
75# be passed here.
77# This returns a javascript string, unless `options.sourceMap` is passed,
78# in which case this returns a `{js, v3SourceMap, sourceMap}`
79# object, where sourceMap is a sourcemap.coffee#SourceMap object, handy for
80# doing programmatic lookups.
81exports.compile = compile = withPrettyErrors (code, options = {}) ->
82 # Clone `options`, to avoid mutating the `options` object passed in.
83 options = Object.assign {}, options
84 # Always generate a source map if no filename is passed in, since without a
85 # a filename we have no way to retrieve this source later in the event that
86 # we need to recompile it to get a source map for `prepareStackTrace`.
87 generateSourceMap = options.sourceMap or options.inlineMap or not options.filename?
88 filename = options.filename or '<anonymous>'
90 checkShebangLine filename, code
92 map = new SourceMap if generateSourceMap
94 tokens = lexer.tokenize code, options
96 # Pass a list of referenced variables, so that generated variables won’t get
97 # the same name.
98 options.referencedVars = (
99 token[1] for token in tokens when token[0] is 'IDENTIFIER'
100 )
102 # Check for import or export; if found, force bare mode.
103 unless options.bare? and options.bare is yes
104 for token in tokens
105 if token[0] in ['IMPORT', 'EXPORT']
106 options.bare = yes
107 break
109 nodes = parser.parse tokens
110 # If all that was requested was a POJO representation of the nodes, e.g.
111 # the abstract syntax tree (AST), we can stop now and just return that
112 # (after fixing the location data for the root/`File`»`Program` node,
113 # which might’ve gotten misaligned from the original source due to the
114 # `clean` function in the lexer).
115 if options.ast
116 nodes.allCommentTokens = helpers.extractAllCommentTokens tokens
117 sourceCodeNumberOfLines = (code.match(/\r?\n/g) or '').length + 1
118 sourceCodeLastLine = /.*$/.exec(code)[0] # `.*` matches all but line break characters.
119 ast = nodes.ast options
120 range = [0, code.length]
121 ast.start = ast.program.start = range[0]
122 ast.end = ast.program.end = range[1]
123 ast.range = ast.program.range = range
124 ast.loc.start = ast.program.loc.start = {line: 1, column: 0}
125 ast.loc.end.line = ast.program.loc.end.line = sourceCodeNumberOfLines
126 ast.loc.end.column = ast.program.loc.end.column = sourceCodeLastLine.length
127 ast.tokens = tokens
128 return ast
130 fragments = nodes.compileToFragments options
132 currentLine = 0
133 currentLine += 1 if options.header
134 currentLine += 1 if options.shiftLine
135 currentColumn = 0
136 js = ""
137 for fragment in fragments
138 # Update the sourcemap with data from each fragment.
139 if generateSourceMap
140 # Do not include empty, whitespace, or semicolon-only fragments.
141 if fragment.locationData and not /^[;\s]*$/.test fragment.code
142 map.add(
143 [fragment.locationData.first_line, fragment.locationData.first_column]
144 [currentLine, currentColumn]
145 {noReplace: true})
146 newLines = helpers.count fragment.code, "\n"
147 currentLine += newLines
148 if newLines
149 currentColumn = fragment.code.length - (fragment.code.lastIndexOf("\n") + 1)
150 else
151 currentColumn += fragment.code.length
153 # Copy the code from each fragment into the final JavaScript.
154 js += fragment.code
156 if options.header
157 header = "Generated by CoffeeScript #{@VERSION}"
158 js = "// #{header}\n#{js}"
160 if generateSourceMap
161 v3SourceMap = map.generate options, code
163 if options.transpile
164 if typeof options.transpile isnt 'object'
165 # This only happens if run via the Node API and `transpile` is set to
166 # something other than an object.
167 throw new Error 'The transpile option must be given an object with options to pass to Babel'
169 # Get the reference to Babel that we have been passed if this compiler
170 # is run via the CLI or Node API.
171 transpiler = options.transpile.transpile
172 delete options.transpile.transpile
174 transpilerOptions = Object.assign {}, options.transpile
176 # See https://github.com/babel/babel/issues/827#issuecomment-77573107:
177 # Babel can take a v3 source map object as input in `inputSourceMap`
178 # and it will return an *updated* v3 source map object in its output.
179 if v3SourceMap and not transpilerOptions.inputSourceMap?
180 transpilerOptions.inputSourceMap = v3SourceMap
181 transpilerOutput = transpiler js, transpilerOptions
182 js = transpilerOutput.code
183 if v3SourceMap and transpilerOutput.map
184 v3SourceMap = transpilerOutput.map
186 if options.inlineMap
187 encoded = base64encode JSON.stringify v3SourceMap
188 sourceMapDataURI = "//# sourceMappingURL=data:application/json;base64,#{encoded}"
189 sourceURL = "//# sourceURL=#{options.filename ? 'coffeescript'}"
190 js = "#{js}\n#{sourceMapDataURI}\n#{sourceURL}"
192 registerCompiled filename, code, map
194 if options.sourceMap
195 {
196 js
197 sourceMap: map
198 v3SourceMap: JSON.stringify v3SourceMap, null, 2
199 }
200 else
201 js
203# Tokenize a string of CoffeeScript code, and return the array of tokens.
204exports.tokens = withPrettyErrors (code, options) ->
205 lexer.tokenize code, options
207# Parse a string of CoffeeScript code or an array of lexed tokens, and
208# return the AST. You can then compile it by calling `.compile()` on the root,
209# or traverse it by using `.traverseChildren()` with a callback.
210exports.nodes = withPrettyErrors (source, options) ->
211 source = lexer.tokenize source, options if typeof source is 'string'
212 parser.parse source
214# This file used to export these methods; leave stubs that throw warnings
215# instead. These methods have been moved into `index.coffee` to provide
216# separate entrypoints for Node and non-Node environments, so that static
217# analysis tools don’t choke on Node packages when compiling for a non-Node
218# environment.
219exports.run = exports.eval = exports.register = ->
220 throw new Error 'require index.coffee, not this file'
222# Instantiate a Lexer for our use here.
223lexer = new Lexer
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 yylloc:
230 range: []
231 options:
232 ranges: yes
233 lex: ->
234 token = parser.tokens[@pos++]
235 if token
236 [tag, @yytext, @yylloc] = token
237 parser.errorToken = token.origin or token
238 @yylineno = @yylloc.first_line
239 else
240 tag = ''
241 tag
242 setInput: (tokens) ->
243 parser.tokens = tokens
244 @pos = 0
245 upcomingInput: -> ''
247# Make all the AST nodes visible to the parser.
248parser.yy = require './nodes'
250# Override Jison's default error handling function.
251parser.yy.parseError = (message, {token}) ->
252 # Disregard Jison's message, it contains redundant line number information.
253 # Disregard the token, we take its value directly from the lexer in case
254 # the error is caused by a generated token which might refer to its origin.
255 {errorToken, tokens} = parser
256 [errorTag, errorText, errorLoc] = errorToken
258 errorText = switch
259 when errorToken is tokens[tokens.length - 1]
260 'end of input'
261 when errorTag in ['INDENT', 'OUTDENT']
262 'indentation'
263 when errorTag in ['IDENTIFIER', 'NUMBER', 'INFINITY', 'STRING', 'STRING_START', 'REGEX', 'REGEX_START']
264 errorTag.replace(/_START$/, '').toLowerCase()
265 else
266 helpers.nameWhitespaceCharacter errorText
268 # The second argument has a `loc` property, which should have the location
269 # data for this token. Unfortunately, Jison seems to send an outdated `loc`
270 # (from the previous token), so we take the location information directly
271 # from the lexer.
272 helpers.throwSyntaxError "unexpected #{errorText}", errorLoc
274# Based on http://v8.googlecode.com/svn/branches/bleeding_edge/src/messages.js
275# Modified to handle sourceMap
276formatSourcePosition = (frame, getSourceMapping) ->
277 filename = undefined
278 fileLocation = ''
280 if frame.isNative()
281 fileLocation = "native"
282 else
283 if frame.isEval()
284 filename = frame.getScriptNameOrSourceURL()
285 fileLocation = "#{frame.getEvalOrigin()}, " unless filename
286 else
287 filename = frame.getFileName()
289 filename or= "<anonymous>"
291 line = frame.getLineNumber()
292 column = frame.getColumnNumber()
294 # Check for a sourceMap position
295 source = getSourceMapping filename, line, column
296 fileLocation =
297 if source
298 "#{filename}:#{source[0]}:#{source[1]}"
299 else
300 "#{filename}:#{line}:#{column}"
302 functionName = frame.getFunctionName()
303 isConstructor = frame.isConstructor()
304 isMethodCall = not (frame.isToplevel() or isConstructor)
306 if isMethodCall
307 methodName = frame.getMethodName()
308 typeName = frame.getTypeName()
310 if functionName
311 tp = as = ''
312 if typeName and functionName.indexOf typeName
313 tp = "#{typeName}."
314 if methodName and functionName.indexOf(".#{methodName}") isnt functionName.length - methodName.length - 1
315 as = " [as #{methodName}]"
317 "#{tp}#{functionName}#{as} (#{fileLocation})"
318 else
319 "#{typeName}.#{methodName or '<anonymous>'} (#{fileLocation})"
320 else if isConstructor
321 "new #{functionName or '<anonymous>'} (#{fileLocation})"
322 else if functionName
323 "#{functionName} (#{fileLocation})"
324 else
325 fileLocation
327getSourceMap = (filename, line, column) ->
328 # Skip files that we didn’t compile, like Node system files that appear in
329 # the stack trace, as they never have source maps.
330 return null unless filename is '<anonymous>' or filename.slice(filename.lastIndexOf('.')) in FILE_EXTENSIONS
332 if filename isnt '<anonymous>' and sourceMaps[filename]?
333 return sourceMaps[filename][sourceMaps[filename].length - 1]
334 # CoffeeScript compiled in a browser or via `CoffeeScript.compile` or `.run`
335 # may get compiled with `options.filename` that’s missing, which becomes
336 # `<anonymous>`; but the runtime might request the stack trace with the
337 # filename of the script file. See if we have a source map cached under
338 # `<anonymous>` that matches the error.
339 else if sourceMaps['<anonymous>']?
340 # Work backwards from the most recent anonymous source maps, until we find
341 # one that works. This isn’t foolproof; there is a chance that multiple
342 # source maps will have line/column pairs that match. But we have no other
343 # way to match them. `frame.getFunction().toString()` doesn’t always work,
344 # and it’s not foolproof either.
345 for map in sourceMaps['<anonymous>'] by -1
346 sourceLocation = map.sourceLocation [line - 1, column - 1]
347 return map if sourceLocation?[0]? and sourceLocation[1]?
349 # If all else fails, recompile this source to get a source map. We need the
350 # previous section (for `<anonymous>`) despite this option, because after it
351 # gets compiled we will still need to look it up from
352 # `sourceMaps['<anonymous>']` in order to find and return it. That’s why we
353 # start searching from the end in the previous block, because most of the
354 # time the source map we want is the last one.
355 if sources[filename]?
356 answer = compile sources[filename][sources[filename].length - 1],
357 filename: filename
358 sourceMap: yes
359 literate: helpers.isLiterate filename
360 answer.sourceMap
361 else
362 null
364# Based on [michaelficarra/CoffeeScriptRedux](http://goo.gl/ZTx1p)
365# NodeJS / V8 have no support for transforming positions in stack traces using
366# sourceMap, so we must monkey-patch Error to display CoffeeScript source
367# positions.
368Error.prepareStackTrace = (err, stack) ->
369 getSourceMapping = (filename, line, column) ->
370 sourceMap = getSourceMap filename, line, column
371 answer = sourceMap.sourceLocation [line - 1, column - 1] if sourceMap?
372 if answer? then [answer[0] + 1, answer[1] + 1] else null
374 frames = for frame in stack
375 break if frame.getFunction() is exports.run
376 " at #{formatSourcePosition frame, getSourceMapping}"
378 "#{err.toString()}\n#{frames.join '\n'}\n"
380checkShebangLine = (file, input) ->
381 firstLine = input.split(/$/m)[0]
382 rest = firstLine?.match(/^#!\s*([^\s]+\s*)(.*)/)
383 args = rest?[2]?.split(/\s/).filter (s) -> s isnt ''
384 if args?.length > 1
385 console.error '''
386 The script to be run begins with a shebang line with more than one
387 argument. This script will fail on platforms such as Linux which only
388 allow a single argument.
389 '''
390 console.error "The shebang line was: '#{firstLine}' in file '#{file}'"
391 console.error "The arguments were: #{JSON.stringify args}"