mirror of
https://github.com/jashkenas/coffeescript.git
synced 2026-05-03 03:00:14 -04:00
This patch fixes issue #3349, by prints deprecation warning for users who depends on old(<1.7.0) implicit module registration.
307 lines
10 KiB
CoffeeScript
307 lines
10 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.
|
|
|
|
fs = require 'fs'
|
|
vm = require 'vm'
|
|
path = require 'path'
|
|
{Lexer} = require './lexer'
|
|
{parser} = require './parser'
|
|
helpers = require './helpers'
|
|
SourceMap = require './sourcemap'
|
|
|
|
# The current CoffeeScript version number.
|
|
exports.VERSION = '1.7.1'
|
|
|
|
exports.FILE_EXTENSIONS = ['.coffee', '.litcoffee', '.coffee.md']
|
|
|
|
# Expose helpers for testing.
|
|
exports.helpers = helpers
|
|
|
|
# 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 helpers.updateSyntaxError err, code, options.filename
|
|
|
|
# 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 = withPrettyErrors (code, options) ->
|
|
{merge, extend} = helpers
|
|
options = extend {}, options
|
|
|
|
if options.sourceMap
|
|
map = new SourceMap
|
|
|
|
fragments = 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
|
|
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 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 = 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
|
|
|
|
# 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
|
|
dir = if options.filename
|
|
path.dirname fs.realpathSync options.filename
|
|
else
|
|
fs.realpathSync '.'
|
|
mainModule.paths = require('module')._nodeModulePaths dir
|
|
|
|
# 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
|
|
|
|
exports.register = -> require './register'
|
|
|
|
# Throw Error to register extension explicitly.
|
|
if require.extensions
|
|
for ext in @FILE_EXTENSIONS
|
|
unless require.extensions[ext]?
|
|
require.extensions[ext] = ->
|
|
throw new Error('''Use CoffeeScript.register() or the coffee-script/register module \
|
|
to dynamically load CoffeeScript files''')
|
|
|
|
exports._compileFile = (filename, sourceMap = no) ->
|
|
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.
|
|
throw helpers.updateSyntaxError err, stripped, filename
|
|
|
|
answer
|
|
|
|
# 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
|
|
@errorToken = token.origin or 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'
|
|
|
|
# Override Jison's default error handling function.
|
|
parser.yy.parseError = (message, {token}) ->
|
|
# Disregard Jison's message, it contains redundant line numer 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.lexer
|
|
[errorTag, errorText, errorLoc] = errorToken
|
|
|
|
errorText = if errorToken is tokens[tokens.length - 1]
|
|
'end of input'
|
|
else if errorTag in ['INDENT', 'OUTDENT']
|
|
'indentation'
|
|
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
|
|
|
|
# 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 exports.FILE_EXTENSIONS
|
|
answer = exports._compileFile filename, true
|
|
sourceMaps[filename] = answer.sourceMap
|
|
|
|
# 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
|
|
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"
|
|
|