mirror of
https://github.com/sharkdp/bat
synced 2024-12-26 20:23:06 +00:00
392 lines
16 KiB
CoffeeScript
392 lines
16 KiB
CoffeeScript
|
# 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 = 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 `(source)`: [`(source map)`].
|
|||
|
sourceMaps = {}
|
|||
|
|
|||
|
# This is exported to enable an external module to implement caching of
|
|||
|
# compilation results. When the compiled js source is loaded from cache, the
|
|||
|
# original coffee code should be added with this method in order to enable the
|
|||
|
# Error.prepareStackTrace below to correctly adjust the stack trace for the
|
|||
|
# corresponding file (the source map will be generated on demand).
|
|||
|
exports.registerCompiled = registerCompiled = (filename, source, sourcemap) ->
|
|||
|
|
|||
|
sources[filename] ?= []
|
|||
|
sources[filename].push source
|
|||
|
|
|||
|
if sourcemap?
|
|||
|
sourceMaps[filename] ?= []
|
|||
|
sourceMaps[filename].push sourcemap
|
|||
|
|
|||
|
# 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 = {}) ->
|
|||
|
# Clone `options`, to avoid mutating the `options` object passed in.
|
|||
|
options = Object.assign {}, 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>'
|
|||
|
|
|||
|
checkShebangLine 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
|
|||
|
|
|||
|
nodes = parser.parse tokens
|
|||
|
# If all that was requested was a POJO representation of the nodes, e.g.
|
|||
|
# the abstract syntax tree (AST), we can stop now and just return that
|
|||
|
# (after fixing the location data for the root/`File`»`Program` node,
|
|||
|
# which might’ve gotten misaligned from the original source due to the
|
|||
|
# `clean` function in the lexer).
|
|||
|
if options.ast
|
|||
|
nodes.allCommentTokens = helpers.extractAllCommentTokens tokens
|
|||
|
sourceCodeNumberOfLines = (code.match(/\r?\n/g) or '').length + 1
|
|||
|
sourceCodeLastLine = /.*$/.exec(code)[0] # `.*` matches all but line break characters.
|
|||
|
ast = nodes.ast options
|
|||
|
range = [0, code.length]
|
|||
|
ast.start = ast.program.start = range[0]
|
|||
|
ast.end = ast.program.end = range[1]
|
|||
|
ast.range = ast.program.range = range
|
|||
|
ast.loc.start = ast.program.loc.start = {line: 1, column: 0}
|
|||
|
ast.loc.end.line = ast.program.loc.end.line = sourceCodeNumberOfLines
|
|||
|
ast.loc.end.column = ast.program.loc.end.column = sourceCodeLastLine.length
|
|||
|
ast.tokens = tokens
|
|||
|
return ast
|
|||
|
|
|||
|
fragments = nodes.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
|
|||
|
|
|||
|
if options.transpile
|
|||
|
if typeof options.transpile isnt 'object'
|
|||
|
# This only happens if run via the Node API and `transpile` is set to
|
|||
|
# something other than an object.
|
|||
|
throw new Error 'The transpile option must be given an object with options to pass to Babel'
|
|||
|
|
|||
|
# Get the reference to Babel that we have been passed if this compiler
|
|||
|
# is run via the CLI or Node API.
|
|||
|
transpiler = options.transpile.transpile
|
|||
|
delete options.transpile.transpile
|
|||
|
|
|||
|
transpilerOptions = Object.assign {}, options.transpile
|
|||
|
|
|||
|
# See https://github.com/babel/babel/issues/827#issuecomment-77573107:
|
|||
|
# Babel can take a v3 source map object as input in `inputSourceMap`
|
|||
|
# and it will return an *updated* v3 source map object in its output.
|
|||
|
if v3SourceMap and not transpilerOptions.inputSourceMap?
|
|||
|
transpilerOptions.inputSourceMap = v3SourceMap
|
|||
|
transpilerOutput = transpiler js, transpilerOptions
|
|||
|
js = transpilerOutput.code
|
|||
|
if v3SourceMap and transpilerOutput.map
|
|||
|
v3SourceMap = transpilerOutput.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}"
|
|||
|
|
|||
|
registerCompiled filename, code, map
|
|||
|
|
|||
|
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) ->
|
|||
|
source = lexer.tokenize source, options if typeof source is 'string'
|
|||
|
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 =
|
|||
|
yylloc:
|
|||
|
range: []
|
|||
|
options:
|
|||
|
ranges: yes
|
|||
|
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, line, column) ->
|
|||
|
# Skip files that we didn’t compile, like Node system files that appear in
|
|||
|
# the stack trace, as they never have source maps.
|
|||
|
return null unless filename is '<anonymous>' or filename.slice(filename.lastIndexOf('.')) in FILE_EXTENSIONS
|
|||
|
|
|||
|
if filename isnt '<anonymous>' and sourceMaps[filename]?
|
|||
|
return sourceMaps[filename][sourceMaps[filename].length - 1]
|
|||
|
# CoffeeScript compiled in a browser or via `CoffeeScript.compile` or `.run`
|
|||
|
# may get compiled with `options.filename` that’s missing, which becomes
|
|||
|
# `<anonymous>`; but the runtime might request the stack trace with the
|
|||
|
# filename of the script file. See if we have a source map cached under
|
|||
|
# `<anonymous>` that matches the error.
|
|||
|
else if sourceMaps['<anonymous>']?
|
|||
|
# Work backwards from the most recent anonymous source maps, until we find
|
|||
|
# one that works. This isn’t foolproof; there is a chance that multiple
|
|||
|
# source maps will have line/column pairs that match. But we have no other
|
|||
|
# way to match them. `frame.getFunction().toString()` doesn’t always work,
|
|||
|
# and it’s not foolproof either.
|
|||
|
for map in sourceMaps['<anonymous>'] by -1
|
|||
|
sourceLocation = map.sourceLocation [line - 1, column - 1]
|
|||
|
return map if sourceLocation?[0]? and sourceLocation[1]?
|
|||
|
|
|||
|
# If all else fails, recompile this source to get a source map. We need the
|
|||
|
# previous section (for `<anonymous>`) despite this option, because after it
|
|||
|
# gets compiled we will still need to look it up from
|
|||
|
# `sourceMaps['<anonymous>']` in order to find and return it. That’s why we
|
|||
|
# start searching from the end in the previous block, because most of the
|
|||
|
# time the source map we want is the last one.
|
|||
|
if sources[filename]?
|
|||
|
answer = compile sources[filename][sources[filename].length - 1],
|
|||
|
filename: filename
|
|||
|
sourceMap: yes
|
|||
|
literate: helpers.isLiterate filename
|
|||
|
answer.sourceMap
|
|||
|
else
|
|||
|
null
|
|||
|
|
|||
|
# Based on [michaelficarra/CoffeeScriptRedux](http://goo.gl/ZTx1p)
|
|||
|
# 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, line, column
|
|||
|
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"
|
|||
|
|
|||
|
checkShebangLine = (file, input) ->
|
|||
|
firstLine = input.split(/$/m)[0]
|
|||
|
rest = firstLine?.match(/^#!\s*([^\s]+\s*)(.*)/)
|
|||
|
args = rest?[2]?.split(/\s/).filter (s) -> s isnt ''
|
|||
|
if args?.length > 1
|
|||
|
console.error '''
|
|||
|
The script to be run begins with a shebang line with more than one
|
|||
|
argument. This script will fail on platforms such as Linux which only
|
|||
|
allow a single argument.
|
|||
|
'''
|
|||
|
console.error "The shebang line was: '#{firstLine}' in file '#{file}'"
|
|||
|
console.error "The arguments were: #{JSON.stringify args}"
|