Untitled diff

Created Diff never expires
15 removals
362 lines
30 additions
378 lines
# CoffeeScript can be used both on the server, as a command-line compiler based
# 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
# on Node.js/V8, or to run CoffeeScript directly in the browser. This module
# contains the main entry functions for tokenizing, parsing, and compiling
# contains the main entry functions for tokenizing, parsing, and compiling
# source CoffeeScript into JavaScript.
# source CoffeeScript into JavaScript.


fs = require 'fs'
fs = require 'fs'
vm = require 'vm'
vm = require 'vm'
path = require 'path'
path = require 'path'
{Lexer} = require './lexer'
{Lexer} = require './lexer'
{parser} = require './parser'
{parser} = require './parser'
helpers = require './helpers'
helpers = require './helpers'
SourceMap = require './sourcemap'
SourceMap = require './sourcemap'
# Require `package.json`, which is two levels above this file, as this file is
# evaluated from `lib/coffee-script`.
packageJson = require '../../package.json'


# The current CoffeeScript version number.
# The current CoffeeScript version number.
exports.VERSION = '1.12.1'
exports.VERSION = packageJson.version


exports.FILE_EXTENSIONS = ['.coffee', '.litcoffee', '.coffee.md']
exports.FILE_EXTENSIONS = ['.coffee', '.litcoffee', '.coffee.md']


# Expose helpers for testing.
# Expose helpers for testing.
exports.helpers = helpers
exports.helpers = helpers


# Function that allows for btoa in both nodejs and the browser.
# Function that allows for btoa in both nodejs and the browser.
base64encode = (src) -> switch
base64encode = (src) -> switch
when typeof Buffer is 'function'
when typeof Buffer is 'function'
new Buffer(src).toString('base64')
new Buffer(src).toString('base64')
when typeof btoa is 'function'
when typeof btoa is 'function'
# The contents of a `<script>` block are encoded via UTF-16, so if any extended
# 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.
# 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
# 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.
# for the gory details, and for the solution implemented here.
btoa encodeURIComponent(src).replace /%([0-9A-F]{2})/g, (match, p1) ->
btoa encodeURIComponent(src).replace /%([0-9A-F]{2})/g, (match, p1) ->
String.fromCharCode '0x' + p1
String.fromCharCode '0x' + p1
else
else
throw new Error('Unable to base64 encode inline sourcemap.')
throw new Error('Unable to base64 encode inline sourcemap.')


# Function wrapper to add source file information to SyntaxErrors thrown by the
# Function wrapper to add source file information to SyntaxErrors thrown by the
# lexer/parser/compiler.
# lexer/parser/compiler.
withPrettyErrors = (fn) ->
withPrettyErrors = (fn) ->
(code, options = {}) ->
(code, options = {}) ->
try
try
fn.call @, code, options
fn.call @, code, options
catch err
catch err
throw err if typeof code isnt 'string' # Support `CoffeeScript.nodes(tokens)`.
throw err if typeof code isnt 'string' # Support `CoffeeScript.nodes(tokens)`.
throw helpers.updateSyntaxError err, code, options.filename
throw helpers.updateSyntaxError err, code, options.filename


# Compile CoffeeScript code to JavaScript, using the Coffee/Jison compiler.
# Compile CoffeeScript code to JavaScript, using the Coffee/Jison compiler.
#
#
# If `options.sourceMap` is specified, then `options.filename` must also be specified. All
# 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.
# options that can be passed to `SourceMap#generate` may also be passed here.
#
#
# This returns a javascript string, unless `options.sourceMap` is passed,
# This returns a javascript string, unless `options.sourceMap` is passed,
# in which case this returns a `{js, v3SourceMap, sourceMap}`
# in which case this returns a `{js, v3SourceMap, sourceMap}`
# object, where sourceMap is a sourcemap.coffee#SourceMap object, handy for doing programatic
# object, where sourceMap is a sourcemap.coffee#SourceMap object, handy for doing programatic
# lookups.
# lookups.
exports.compile = compile = withPrettyErrors (code, options) ->
exports.compile = compile = withPrettyErrors (code, options) ->
{merge, extend} = helpers
{merge, extend} = helpers
options = extend {}, options
options = extend {}, options
generateSourceMap = options.sourceMap or options.inlineMap
generateSourceMap = options.sourceMap or options.inlineMap


if generateSourceMap
if generateSourceMap
map = new SourceMap
map = new SourceMap


tokens = lexer.tokenize code, options
tokens = lexer.tokenize code, options


# Pass a list of referenced variables, so that generated variables won't get
# Pass a list of referenced variables, so that generated variables won't get
# the same name.
# the same name.
options.referencedVars = (
options.referencedVars = (
token[1] for token in tokens when token[0] is 'IDENTIFIER'
token[1] for token in tokens when token[0] is 'IDENTIFIER'
)
)


# Check for import or export; if found, force bare mode
# Check for import or export; if found, force bare mode
unless options.bare? and options.bare is yes
unless options.bare? and options.bare is yes
for token in tokens
for token in tokens
if token[0] in ['IMPORT', 'EXPORT']
if token[0] in ['IMPORT', 'EXPORT']
options.bare = yes
options.bare = yes
break
break


fragments = parser.parse(tokens).compileToFragments options
fragments = parser.parse(tokens).compileToFragments options


currentLine = 0
currentLine = 0
currentLine += 1 if options.header
currentLine += 1 if options.header
currentLine += 1 if options.shiftLine
currentLine += 1 if options.shiftLine
currentColumn = 0
currentColumn = 0
js = ""
js = ""
for fragment in fragments
for fragment in fragments
# Update the sourcemap with data from each fragment
# Update the sourcemap with data from each fragment
if generateSourceMap
if generateSourceMap
# Do not include empty, whitespace, or semicolon-only fragments.
# Do not include empty, whitespace, or semicolon-only fragments.
if fragment.locationData and not /^[;\s]*$/.test fragment.code
if fragment.locationData and not /^[;\s]*$/.test fragment.code
map.add(
map.add(
[fragment.locationData.first_line, fragment.locationData.first_column]
[fragment.locationData.first_line, fragment.locationData.first_column]
[currentLine, currentColumn]
[currentLine, currentColumn]
{noReplace: true})
{noReplace: true})
newLines = helpers.count fragment.code, "\n"
newLines = helpers.count fragment.code, "\n"
currentLine += newLines
currentLine += newLines
if newLines
if newLines
currentColumn = fragment.code.length - (fragment.code.lastIndexOf("\n") + 1)
currentColumn = fragment.code.length - (fragment.code.lastIndexOf("\n") + 1)
else
else
currentColumn += fragment.code.length
currentColumn += fragment.code.length


# Copy the code from each fragment into the final JavaScript.
# Copy the code from each fragment into the final JavaScript.
js += fragment.code
js += fragment.code


if options.header
if options.header
header = "Generated by CoffeeScript #{@VERSION}"
header = "Generated by CoffeeScript #{@VERSION}"
js = "// #{header}\n#{js}"
js = "// #{header}\n#{js}"


if generateSourceMap
if generateSourceMap
v3SourceMap = map.generate(options, code)
v3SourceMap = map.generate(options, code)


if options.inlineMap
if options.inlineMap
encoded = base64encode JSON.stringify v3SourceMap
encoded = base64encode JSON.stringify v3SourceMap
sourceMapDataURI = "//# sourceMappingURL=data:application/json;base64,#{encoded}"
sourceMapDataURI = "//# sourceMappingURL=data:application/json;base64,#{encoded}"
sourceURL = "//# sourceURL=#{options.filename ? 'coffeescript'}"
sourceURL = "//# sourceURL=#{options.filename ? 'coffeescript'}"
js = "#{js}\n#{sourceMapDataURI}\n#{sourceURL}"
js = "#{js}\n#{sourceMapDataURI}\n#{sourceURL}"


if options.sourceMap
if options.sourceMap
{
{
js
js
sourceMap: map
sourceMap: map
v3SourceMap: JSON.stringify v3SourceMap, null, 2
v3SourceMap: JSON.stringify v3SourceMap, null, 2
}
}
else
else
js
js


# Tokenize a string of CoffeeScript code, and return the array of tokens.
# Tokenize a string of CoffeeScript code, and return the array of tokens.
exports.tokens = withPrettyErrors (code, options) ->
exports.tokens = withPrettyErrors (code, options) ->
lexer.tokenize code, options
lexer.tokenize code, options


# Parse a string of CoffeeScript code or an array of lexed tokens, and
# 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,
# return the AST. You can then compile it by calling `.compile()` on the root,
# or traverse it by using `.traverseChildren()` with a callback.
# or traverse it by using `.traverseChildren()` with a callback.
exports.nodes = withPrettyErrors (source, options) ->
exports.nodes = withPrettyErrors (source, options) ->
if typeof source is 'string'
if typeof source is 'string'
parser.parse lexer.tokenize source, options
parser.parse lexer.tokenize source, options
else
else
parser.parse source
parser.parse source


# Compile and execute a string of CoffeeScript (on the server), correctly
# Compile and execute a string of CoffeeScript (on the server), correctly
# setting `__filename`, `__dirname`, and relative `require()`.
# setting `__filename`, `__dirname`, and relative `require()`.
exports.run = (code, options = {}) ->
exports.run = (code, options = {}) ->
mainModule = require.main
mainModule = require.main


# Set the filename.
# Set the filename.
mainModule.filename = process.argv[1] =
mainModule.filename = process.argv[1] =
if options.filename then fs.realpathSync(options.filename) else '.'
if options.filename then fs.realpathSync(options.filename) else '.'


# Clear the module cache.
# Clear the module cache.
mainModule.moduleCache and= {}
mainModule.moduleCache and= {}


# Assign paths for node_modules loading
# Assign paths for node_modules loading
dir = if options.filename
dir = if options.filename
path.dirname fs.realpathSync options.filename
path.dirname fs.realpathSync options.filename
else
else
fs.realpathSync '.'
fs.realpathSync '.'
mainModule.paths = require('module')._nodeModulePaths dir
mainModule.paths = require('module')._nodeModulePaths dir


# Compile.
# Compile.
if not helpers.isCoffee(mainModule.filename) or require.extensions
if not helpers.isCoffee(mainModule.filename) or require.extensions
answer = compile code, options
answer = compile code, options
code = answer.js ? answer
code = answer.js ? answer


mainModule._compile code, mainModule.filename
mainModule._compile code, mainModule.filename


# Compile and evaluate a string of CoffeeScript (in a Node.js-like environment).
# Compile and evaluate a string of CoffeeScript (in a Node.js-like environment).
# The CoffeeScript REPL uses this to run the input.
# The CoffeeScript REPL uses this to run the input.
exports.eval = (code, options = {}) ->
exports.eval = (code, options = {}) ->
return unless code = code.trim()
return unless code = code.trim()
createContext = vm.Script.createContext ? vm.createContext
createContext = vm.Script.createContext ? vm.createContext


isContext = vm.isContext ? (ctx) ->
isContext = vm.isContext ? (ctx) ->
options.sandbox instanceof createContext().constructor
options.sandbox instanceof createContext().constructor


if createContext
if createContext
if options.sandbox?
if options.sandbox?
if isContext options.sandbox
if isContext options.sandbox
sandbox = options.sandbox
sandbox = options.sandbox
else
else
sandbox = createContext()
sandbox = createContext()
sandbox[k] = v for own k, v of options.sandbox
sandbox[k] = v for own k, v of options.sandbox
sandbox.global = sandbox.root = sandbox.GLOBAL = sandbox
sandbox.global = sandbox.root = sandbox.GLOBAL = sandbox
else
else
sandbox = global
sandbox = global
sandbox.__filename = options.filename || 'eval'
sandbox.__filename = options.filename || 'eval'
sandbox.__dirname = path.dirname sandbox.__filename
sandbox.__dirname = path.dirname sandbox.__filename
# define module/require only if they chose not to specify their own
# define module/require only if they chose not to specify their own
unless sandbox isnt global or sandbox.module or sandbox.require
unless sandbox isnt global or sandbox.module or sandbox.require
Module = require 'module'
Module = require 'module'
sandbox.module = _module = new Module(options.modulename || 'eval')
sandbox.module = _module = new Module(options.modulename || 'eval')
sandbox.require = _require = (path) -> Module._load path, _module, true
sandbox.require = _require = (path) -> Module._load path, _module, true
_module.filename = sandbox.__filename
_module.filename = sandbox.__filename
for r in Object.getOwnPropertyNames require when r not in ['paths', 'arguments', 'caller']
for r in Object.getOwnPropertyNames require when r not in ['paths', 'arguments', 'caller']
_require[r] = require[r]
_require[r] = require[r]
# use the same hack node currently uses for their own REPL
# use the same hack node currently uses for their own REPL
_require.paths = _module.paths = Module._nodeModulePaths process.cwd()
_require.paths = _module.paths = Module._nodeModulePaths process.cwd()
_require.resolve = (request) -> Module._resolveFilename request, _module
_require.resolve = (request) -> Module._resolveFilename request, _module
o = {}
o = {}
o[k] = v for own k, v of options
o[k] = v for own k, v of options
o.bare = on # ensure return value
o.bare = on # ensure return value
js = compile code, o
js = compile code, o
if sandbox is global
if sandbox is global
vm.runInThisContext js
vm.runInThisContext js
else
else
vm.runInContext js, sandbox
vm.runInContext js, sandbox


exports.register = -> require './register'
exports.register = -> require './register'


# Throw error with deprecation warning when depending upon implicit `require.extensions` registration
# Throw error with deprecation warning when depending upon implicit `require.extensions` registration
if require.extensions
if require.extensions
for ext in @FILE_EXTENSIONS then do (ext) ->
for ext in @FILE_EXTENSIONS then do (ext) ->
require.extensions[ext] ?= ->
require.extensions[ext] ?= ->
throw new Error """
throw new Error """
Use CoffeeScript.register() or require the coffee-script/register module to require #{ext} files.
Use CoffeeScript.register() or require the coffee-script/register module to require #{ext} files.
"""
"""


# 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.
compiledFiles = {}

exports._compileFile = (filename, sourceMap = no, inlineMap = no) ->
exports._compileFile = (filename, sourceMap = no, inlineMap = no) ->
raw = fs.readFileSync filename, 'utf8'
raw = fs.readFileSync filename, 'utf8'
# Strip the Unicode byte order mark, if this file begins with one.
stripped = if raw.charCodeAt(0) is 0xFEFF then raw.substring 1 else raw
stripped = if raw.charCodeAt(0) is 0xFEFF then raw.substring 1 else raw
compiledFiles[filename] = stripped
compileCode stripped, filename, sourceMap, inlineMap


compileCode = (code, filename, sourceMap = no, inlineMap = no) ->
try
try
answer = compile stripped, {
answer = compile code, {
filename, sourceMap, inlineMap
filename, sourceMap, inlineMap
sourceFiles: [filename]
sourceFiles: [filename]
literate: helpers.isLiterate filename
literate: helpers.isLiterate filename
}
}
catch err
catch err
# As the filename and code of a dynamically loaded file will be different
# As the filename and code of a dynamically loaded file will be different
# from the original file compiled with CoffeeScript.run, add that
# from the original file compiled with CoffeeScript.run, add that
# information to error so it can be pretty-printed later.
# information to error so it can be pretty-printed later.
throw helpers.updateSyntaxError err, stripped, filename
throw helpers.updateSyntaxError err, code, filename


answer
answer


# Instantiate a Lexer for our use here.
# Instantiate a Lexer for our use here.
lexer = new Lexer
lexer = new Lexer


# The real Lexer produces a generic stream of tokens. This object provides a
# 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
# thin wrapper around it, compatible with the Jison API. We can then pass it
# directly as a "Jison lexer".
# directly as a "Jison lexer".
parser.lexer =
parser.lexer =
lex: ->
lex: ->
token = parser.tokens[@pos++]
token = parser.tokens[@pos++]
if token
if token
[tag, @yytext, @yylloc] = token
[tag, @yytext, @yylloc] = token
parser.errorToken = token.origin or token
parser.errorToken = token.origin or token
@yylineno = @yylloc.first_line
@yylineno = @yylloc.first_line
else
else
tag = ''
tag = ''


tag
tag
setInput: (tokens) ->
setInput: (tokens) ->
parser.tokens = tokens
parser.tokens = tokens
@pos = 0
@pos = 0
upcomingInput: ->
upcomingInput: ->
""
""
# Make all the AST nodes visible to the parser.
# Make all the AST nodes visible to the parser.
parser.yy = require './nodes'
parser.yy = require './nodes'


# Override Jison's default error handling function.
# Override Jison's default error handling function.
parser.yy.parseError = (message, {token}) ->
parser.yy.parseError = (message, {token}) ->
# Disregard Jison's message, it contains redundant line number information.
# Disregard Jison's message, it contains redundant line number information.
# Disregard the token, we take its value directly from the lexer in case
# 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.
# the error is caused by a generated token which might refer to its origin.
{errorToken, tokens} = parser
{errorToken, tokens} = parser
[errorTag, errorText, errorLoc] = errorToken
[errorTag, errorText, errorLoc] = errorToken


errorText = switch
errorText = switch
when errorToken is tokens[tokens.length - 1]
when errorToken is tokens[tokens.length - 1]
'end of input'
'end of input'
when errorTag in ['INDENT', 'OUTDENT']
when errorTag in ['INDENT', 'OUTDENT']
'indentation'
'indentation'
when errorTag in ['IDENTIFIER', 'NUMBER', 'INFINITY', 'STRING', 'STRING_START', 'REGEX', 'REGEX_START']
when errorTag in ['IDENTIFIER', 'NUMBER', 'INFINITY', 'STRING', 'STRING_START', 'REGEX', 'REGEX_START']
errorTag.replace(/_START$/, '').toLowerCase()
errorTag.replace(/_START$/, '').toLowerCase()
else
else
helpers.nameWhitespaceCharacter errorText
helpers.nameWhitespaceCharacter errorText


# The second argument has a `loc` property, which should have the location
# The second argument has a `loc` property, which should have the location
# data for this token. Unfortunately, Jison seems to send an outdated `loc`
# 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 previous token), so we take the location information directly
# from the lexer.
# from the lexer.
helpers.throwSyntaxError "unexpected #{errorText}", errorLoc
helpers.throwSyntaxError "unexpected #{errorText}", errorLoc


# Based on http://v8.googlecode.com/svn/branches/bleeding_edge/src/messages.js
# Based on http://v8.googlecode.com/svn/branches/bleeding_edge/src/messages.js
# Modified to handle sourceMap
# Modified to handle sourceMap
formatSourcePosition = (frame, getSourceMapping) ->
formatSourcePosition = (frame, getSourceMapping) ->
fileName = undefined
fileName = undefined
fileLocation = ''
fileLocation = ''


if frame.isNative()
if frame.isNative()
fileLocation = "native"
fileLocation = "native"
else
else
if frame.isEval()
if frame.isEval()
fileName = frame.getScriptNameOrSourceURL()
fileName = frame.getScriptNameOrSourceURL()
fileLocation = "#{frame.getEvalOrigin()}, " unless fileName
fileLocation = "#{frame.getEvalOrigin()}, " unless fileName
else
else
fileName = frame.getFileName()
fileName = frame.getFileName()


fileName or= "<anonymous>"
fileName or= "<anonymous>"


line = frame.getLineNumber()
line = frame.getLineNumber()
column = frame.getColumnNumber()
column = frame.getColumnNumber()


# Check for a sourceMap position
# Check for a sourceMap position
source = getSourceMapping fileName, line, column
source = getSourceMapping fileName, line, column
fileLocation =
fileLocation =
if source
if source
"#{fileName}:#{source[0]}:#{source[1]}"
"#{fileName}:#{source[0]}:#{source[1]}"
else
else
"#{fileName}:#{line}:#{column}"
"#{fileName}:#{line}:#{column}"


functionName = frame.getFunctionName()
functionName = frame.getFunctionName()
isConstructor = frame.isConstructor()
isConstructor = frame.isConstructor()
isMethodCall = not (frame.isToplevel() or isConstructor)
isMethodCall = not (frame.isToplevel() or isConstructor)


if isMethodCall
if isMethodCall
methodName = frame.getMethodName()
methodName = frame.getMethodName()
typeName = frame.getTypeName()
typeName = frame.getTypeName()


if functionName
if functionName
tp = as = ''
tp = as = ''
if typeName and functionName.indexOf typeName
if typeName and functionName.indexOf typeName
tp = "#{typeName}."
tp = "#{typeName}."
if methodName and functionName.indexOf(".#{methodName}") isnt functionName.length - methodName.length - 1
if methodName and functionName.indexOf(".#{methodName}") isnt functionName.length - methodName.length - 1
as = " [as #{methodName}]"
as = " [as #{methodName}]"


"#{tp}#{functionName}#{as} (#{fileLocation})"
"#{tp}#{functionName}#{as} (#{fileLocation})"
else
else
"#{typeName}.#{methodName or '<anonymous>'} (#{fileLocation})"
"#{typeName}.#{methodName or '<anonymous>'} (#{fileLocation})"
else if isConstructor
else if isConstructor
"new #{functionName or '<anonymous>'} (#{fileLocation})"
"new #{functionName or '<anonymous>'} (#{fileLocation})"
else if functionName
else if functionName
"#{functionName} (#{fileLocation})"
"#{functionName} (#{fileLocation})"
else
else
fileLocation
fileLocation


# Map of filenames -> sourceMap object.
# Map of filenames: sourceMap objects.
sourceMaps = {}
sourceMaps = {}

# Generates the source map for a coffee file and stores it in the local cache variable.
# Generates the source map for a coffee file and stores it in the local cache variable.
getSourceMap = (filename) ->
getSourceMap = (filename) ->
return sourceMaps[filename] if sourceMaps[filename]
if sourceMaps[filename]?
for ext in exports.FILE_EXTENSIONS
sourceMaps[filename]
if helpers.ends filename, ext
else if compiledFiles[filename]?
answer = exports._compileFile filename, true
answer = compileCode compiledFiles[filename], filename, yes, no
return sourceMaps[filename] = answer.sourceMap
sourceMaps[filename] = answer.sourceMap
return null
else
null


# Based on [michaelficarra/CoffeeScriptRedux](http://goo.gl/ZTx1p)
# Based on [michaelficarra/CoffeeScriptRedux](http://goo.gl/ZTx1p)
# NodeJS / V8 have no support for transforming positions in stack traces using
# NodeJS / V8 have no support for transforming positions in stack traces using
# sourceMap, so we must monkey-patch Error to display CoffeeScript source
# sourceMap, so we must monkey-patch Error to display CoffeeScript source
# positions.
# positions.
Error.prepareStackTrace = (err, stack) ->
Error.prepareStackTrace = (err, stack) ->
getSourceMapping = (filename, line, column) ->
getSourceMapping = (filename, line, column) ->
sourceMap = getSourceMap filename
sourceMap = getSourceMap filename
answer = sourceMap.sourceLocation [line - 1, column - 1] if sourceMap
answer = sourceMap.sourceLocation [line - 1, column - 1] if sourceMap
if answer then [answer[0] + 1, answer[1] + 1] else null
if answer then [answer[0] + 1, answer[1] + 1] else null


frames = for frame in stack
frames = for frame in stack
break if frame.getFunction() is exports.run
break if frame.getFunction() is exports.run
" at #{formatSourcePosition frame, getSourceMapping}"
" at #{formatSourcePosition frame, getSourceMapping}"


"#{err.toString()}\n#{frames.join '\n'}\n"
"#{err.toString()}\n#{frames.join '\n'}\n"