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.
fs = require 'fs' vm = require 'vm' path = require 'path' child_process = require 'child_process' {Lexer} = require './lexer' {parser} = require './parser' helpers = require './helpers' {SourceMap} = require './sourcemap' iced = require './iced'
The current CoffeeScript version number.
exports.VERSION = '1.6.3-f' fileExtensions = ['.coffee', '.litcoffee', '.coffee.md', '.iced']
Expose helpers for testing.
exports.helpers = helpers
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 programatic
lookups.
exports.compile = compile = (code, options = {}) -> {merge} = helpers if options.sourceMap map = new SourceMap fragments = (iced.transform(parser.parse(lexer.tokenize code, options))).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 options.sourceMap if fragment.locationData map.add( [fragment.locationData.first_line, fragment.locationData.first_column] [currentLine, currentColumn] {noReplace: true}) newLines = helpers.count fragment.code, "\n" currentLine += newLines currentColumn = fragment.code.length - (if newLines then fragment.code.lastIndexOf "\n" else 0)
Copy the code from each fragment into the final JavaScript.
js += fragment.code if options.header header = "Generated by IcedCoffeeScript #{@VERSION}" js = "// #{header}\n#{js}" if options.sourceMap answer = {js} answer.sourceMap = map answer.v3SourceMap = map.generate(options, code) answer else js
Tokenize a string of CoffeeScript code, and return the array of tokens.
exports.tokens = (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 = (source, options) -> if typeof source is 'string' iced.transform(parser.parse(lexer.tokenize(source, options)), options) else iced.transform(parser.parse(source),options)
Compile and execute a string of CoffeeScript (on the server), correctly
setting __filename
, __dirname
, and relative require()
.
exports.run = (code, options = {}) ->
mainModule = require.main
Set the filename.
mainModule.filename = process.argv[1] = if options.filename then fs.realpathSync(options.filename) else '.'
Clear the module cache.
mainModule.moduleCache and= {}
Assign paths for node_modules loading
mainModule.paths = require('module')._nodeModulePaths path.dirname fs.realpathSync options.filename or '.'
Compile.
if not helpers.isCoffee(mainModule.filename) or require.extensions answer = compile code, options code = answer.js ? answer mainModule._compile code, mainModule.filename
Compile and evaluate a string of CoffeeScript (in a Node.js-like environment). The CoffeeScript REPL uses this to run the input.
exports.eval = (code, options = {}) -> return unless code = code.trim() Script = vm.Script if Script if options.sandbox? if options.sandbox instanceof Script.createContext().constructor sandbox = options.sandbox else sandbox = Script.createContext() sandbox[k] = v for own k, v of options.sandbox sandbox.global = sandbox.root = sandbox.GLOBAL = sandbox else sandbox = global sandbox.__filename = options.filename || 'eval' sandbox.__dirname = path.dirname sandbox.__filename
define module/require only if they chose not to specify their own
unless sandbox isnt global or sandbox.module or sandbox.require Module = require 'module' sandbox.module = _module = new Module(options.modulename || 'eval') sandbox.require = _require = (path) -> Module._load path, _module, true _module.filename = sandbox.__filename _require[r] = require[r] for r in Object.getOwnPropertyNames require when r isnt 'paths'
use the same hack node currently uses for their own REPL
_require.paths = _module.paths = Module._nodeModulePaths process.cwd() _require.resolve = (request) -> Module._resolveFilename request, _module o = {} o[k] = v for own k, v of options o.bare = on # ensure return value js = compile code, o if sandbox is global vm.runInThisContext js else vm.runInContext js, sandbox compileFile = (filename, sourceMap) -> raw = fs.readFileSync filename, 'utf8' stripped = if raw.charCodeAt(0) is 0xFEFF then raw.substring 1 else raw try answer = compile(stripped, {filename, sourceMap, literate: helpers.isLiterate filename}) catch err
As the filename and code of a dynamically loaded file will be different from the original file compiled with CoffeeScript.run, add that information to error so it can be pretty-printed later.
err.filename = filename
err.code = stripped
throw err
answer
Load and run a CoffeeScript file for Node, stripping any BOM
s.
loadFile = (module, filename) -> answer = compileFile filename, false module._compile answer, filename
If the installed version of Node supports require.extensions
, register
CoffeeScript as an extension.
if require.extensions for ext in fileExtensions require.extensions[ext] = loadFile
Patch Node's module loader to be able to handle mult-dot extensions. This is a horrible thing that should not be required. Perhaps, one day, when a truly benevolent dictator comes to rule over the Republik of Node, it won't be.
Module = require 'module' findExtension = (filename) -> extensions = path.basename(filename).split '.'
Remove the initial dot from dotfiles.
extensions.shift() if extensions[0] is ''
Start with the longest possible extension and work our way shortwards.
while extensions.shift() curExtension = '.' + extensions.join '.' return curExtension if Module._extensions[curExtension] '.js' Module::load = (filename) -> @filename = filename @paths = Module._nodeModulePaths path.dirname filename extension = findExtension filename Module._extensions[extension](this, filename) @loaded = true
If we're on Node, patch child_process.fork
so that Coffee scripts are able
to fork both CoffeeScript files, and JavaScript files, directly.
if child_process {fork} = child_process child_process.fork = (path, args = [], options = {}) -> execPath = if helpers.isCoffee(path) then 'coffee' else null if not Array.isArray args args = [] options = args or {} options.execPath or= execPath fork path, args, options
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 = @tokens[@pos++] if token [tag, @yytext, @yylloc] = token @yylineno = @yylloc.first_line else tag = '' tag setInput: (@tokens) -> @pos = 0 upcomingInput: -> ""
Make all the AST nodes visible to the parser.
parser.yy = require './nodes'
Export the iced runtime as 'iced'
exports.iced = iced.runtime
Override Jison's default error handling function.
parser.yy.parseError = (message, {token}) ->
Disregard Jison's message, it contains redundant line numer information.
message = "unexpected #{if token is 1 then 'end of input' else token}"
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 message, parser.lexer.yylloc
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
Map of filenames -> sourceMap object.
sourceMaps = {}
Generates the source map for a coffee file and stores it in the local cache variable.
getSourceMap = (filename) -> return sourceMaps[filename] if sourceMaps[filename] return unless path?.extname(filename) in fileExtensions answer = compileFile filename, true sourceMaps[filename] = answer.sourceMap
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.name}: #{err.message ? ''}\n#{frames.join '\n'}\n"