{Lexer} = require './lexer'
{parser} = require './parser'
helpers = require './helpers'
SourceMap = require './sourcemap'
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.
{Lexer} = require './lexer'
{parser} = require './parser'
helpers = require './helpers'
SourceMap = require './sourcemap'
Require package.json
, which is two levels above this file, as this file is
evaluated from lib/coffeescript
.
packageJson = require '../../package.json'
The current CoffeeScript version number.
exports.VERSION = packageJson.version
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'
Buffer.from(src).toString('base64')
when typeof btoa is 'function'
The contents of a <script>
block are encoded via UTF-16, so if any extended
characters are used in the block, btoa will fail as it maxes out at UTF-8.
See https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_Unicode_Problem
for the gory details, and for the solution implemented here.
btoa encodeURIComponent(src).replace /%([0-9A-F]{2})/g, (match, p1) ->
String.fromCharCode '0x' + p1
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
For each compiled file, save its source in memory in case we need to
recompile it later. We might need to recompile if the first compilation
didn’t create a source map (faster) but something went wrong and we need
a stack trace. Assuming that most of the time, code isn’t throwing
exceptions, it’s probably more efficient to compile twice only when we
need a stack trace, rather than always generating a source map even when
it’s not likely to be used. Save in form of filename
: (source)
sources = {}
Also save source maps if generated, in form of filename
: (source map)
.
sourceMaps = {}
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 programmatic lookups.
exports.compile = compile = withPrettyErrors (code, options) ->
{merge, extend} = helpers
options = extend {}, options
Always generate a source map if no filename is passed in, since without a
a filename we have no way to retrieve this source later in the event that
we need to recompile it to get a source map for prepareStackTrace
.
generateSourceMap = options.sourceMap or options.inlineMap or not options.filename?
filename = options.filename or '<anonymous>'
sources[filename] = code
map = new SourceMap if generateSourceMap
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'
)
Check for import or export; if found, force bare mode.
unless options.bare? and options.bare is yes
for token in tokens
if token[0] in ['IMPORT', 'EXPORT']
options.bare = yes
break
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)
sourceMaps[filename] = map
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
This file used to export these methods; leave stubs that throw warnings
instead. These methods have been moved into index.coffee
to provide
separate entrypoints for Node and non-Node environments, so that static
analysis tools don’t choke on Node packages when compiling for a non-Node
environment.
exports.run = exports.eval = exports.register = ->
throw new Error 'require index.coffee, not this file'
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
getSourceMap = (filename) ->
if sourceMaps[filename]?
sourceMaps[filename]
CoffeeScript compiled in a browser may get compiled with options.filename
of <anonymous>
, but the browser may request the stack trace with the
filename of the script file.
else if sourceMaps['<anonymous>']?
sourceMaps['<anonymous>']
else if sources[filename]?
answer = compile sources[filename],
filename: filename
sourceMap: yes
literate: helpers.isLiterate filename
answer.sourceMap
else
null
Based on michaelficarra/CoffeeScriptRedux 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"