fs = require 'fs'
vm = require 'vm'
path = require 'path'
{Lexer} = require './lexer'
{parser} = require './parser'
helpers = require './helpers'
SourceMap = require './sourcemap'
CoffeeScript 可以在服务器端使用,作为基于 Node.js/V8 的命令行编译器,也可以直接在浏览器中运行 CoffeeScript。此模块包含用于将源 CoffeeScript 代码标记化、解析和编译为 JavaScript 的主要入口函数。
fs = require 'fs'
vm = require 'vm'
path = require 'path'
{Lexer} = require './lexer'
{parser} = require './parser'
helpers = require './helpers'
SourceMap = require './sourcemap'
需要 package.json
,它位于此文件上方的两个级别,因为此文件是从 lib/coffee-script
评估的。
packageJson = require '../../package.json'
当前 CoffeeScript 版本号。
exports.VERSION = packageJson.version
exports.FILE_EXTENSIONS = ['.coffee', '.litcoffee', '.coffee.md']
公开用于测试的帮助程序。
exports.helpers = helpers
允许在 nodejs 和浏览器中使用 btoa 的函数。
base64encode = (src) -> switch
when typeof Buffer is 'function'
new Buffer(src).toString('base64')
when typeof btoa is 'function'
<script>
块的内容通过 UTF-16 编码,因此如果块中使用任何扩展字符,btoa 将失败,因为它在 UTF-8 上达到最大值。有关详细信息以及此处实现的解决方案,请参阅 https://mdn.org.cn/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_Unicode_Problem。
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.')
函数包装器,用于将源文件信息添加到由词法分析器/解析器/编译器抛出的 SyntaxErrors。
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
对于每个编译的文件,将其源代码保存在内存中,以防我们以后需要重新编译它。如果第一次编译没有创建源映射(更快),但出现了一些错误,我们需要堆栈跟踪,我们可能需要重新编译。假设大多数情况下代码不会抛出异常,那么只有在需要堆栈跟踪时才进行两次编译,而不是始终生成源映射,即使它不太可能被使用,这可能更有效。以 filename
: (source)
的形式保存。
sources = {}
如果生成了源映射,也以 filename
: (source map)
的形式保存。
sourceMaps = {}
使用 Coffee/Jison 编译器将 CoffeeScript 代码编译为 JavaScript。
如果指定了 options.sourceMap
,则还必须指定 options.filename
。所有可以传递给 SourceMap#generate
的选项也可以在这里传递。
这将返回一个 javascript 字符串,除非传递了 options.sourceMap
,在这种情况下,它将返回一个 {js, v3SourceMap, sourceMap}
对象,其中 sourceMap 是一个 sourcemap.coffee#SourceMap 对象,便于进行编程查找。
exports.compile = compile = withPrettyErrors (code, options) ->
{merge, extend} = helpers
options = extend {}, options
如果未传递文件名,则始终生成源映射,因为没有文件名,我们就无法在需要重新编译以获取 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
传递一个引用的变量列表,以便生成的变量不会获得相同的名称。
options.referencedVars = (
token[1] for token in tokens when token[0] is 'IDENTIFIER'
)
检查导入或导出;如果找到,强制使用裸模式。
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
使用每个片段的数据更新源映射。
if generateSourceMap
不要包含空、空格或仅分号的片段。
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
将每个片段中的代码复制到最终的 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
标记化 CoffeeScript 代码字符串,并返回标记数组。
exports.tokens = withPrettyErrors (code, options) ->
lexer.tokenize code, options
解析 CoffeeScript 代码字符串或词法分析的标记数组,并返回 AST。然后,您可以通过对根调用 .compile()
来编译它,或者通过使用 .traverseChildren()
和回调来遍历它。
exports.nodes = withPrettyErrors (source, options) ->
if typeof source is 'string'
parser.parse lexer.tokenize source, options
else
parser.parse source
编译并执行 CoffeeScript 字符串(在服务器上),正确设置 __filename
、__dirname
和相对 require()
。
exports.run = (code, options = {}) ->
mainModule = require.main
设置文件名。
mainModule.filename = process.argv[1] =
if options.filename then fs.realpathSync(options.filename) else '<anonymous>'
清除模块缓存。
mainModule.moduleCache and= {}
为 node_modules 加载分配路径
dir = if options.filename?
path.dirname fs.realpathSync options.filename
else
fs.realpathSync '.'
mainModule.paths = require('module')._nodeModulePaths dir
编译。
if not helpers.isCoffee(mainModule.filename) or require.extensions
answer = compile code, options
code = answer.js ? answer
mainModule._compile code, mainModule.filename
编译并评估 CoffeeScript 字符串(在类似 Node.js 的环境中)。CoffeeScript REPL 使用此方法运行输入。
exports.eval = (code, options = {}) ->
return unless code = code.trim()
createContext = vm.Script.createContext ? vm.createContext
isContext = vm.isContext ? (ctx) ->
options.sandbox instanceof createContext().constructor
if createContext
if options.sandbox?
if isContext options.sandbox
sandbox = options.sandbox
else
sandbox = 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
仅当他们选择不指定自己的模块/require 时才定义它们
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
for r in Object.getOwnPropertyNames require when r not in ['paths', 'arguments', 'caller']
_require[r] = require[r]
使用 node 当前用于其自身 REPL 的相同 hack
_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'
在依赖于隐式 require.extensions
注册时,抛出带有弃用警告的错误
if require.extensions
for ext in @FILE_EXTENSIONS then do (ext) ->
require.extensions[ext] ?= ->
throw new Error """
Use CoffeeScript.register() or require the coffee-script/register module to require #{ext} files.
"""
exports._compileFile = (filename, sourceMap = no, inlineMap = no) ->
raw = fs.readFileSync filename, 'utf8'
如果此文件以 Unicode 字节顺序标记开头,则将其剥离。
stripped = if raw.charCodeAt(0) is 0xFEFF then raw.substring 1 else raw
try
answer = compile stripped, {
filename, sourceMap, inlineMap
sourceFiles: [filename]
literate: helpers.isLiterate filename
}
catch err
由于动态加载文件的名称和代码将不同于使用 CoffeeScript.run 编译的原始文件,因此将该信息添加到错误中,以便以后可以对其进行美化。
throw helpers.updateSyntaxError err, stripped, filename
answer
为我们在这里使用实例化一个词法分析器。
lexer = new Lexer
真正的词法分析器会生成通用的标记流。此对象在其周围提供了一个薄包装器,与 Jison API 兼容。然后,我们可以直接将其作为“Jison 词法分析器”传递。
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: ->
""
使所有 AST 节点对解析器可见。
parser.yy = require './nodes'
覆盖 Jison 的默认错误处理函数。
parser.yy.parseError = (message, {token}) ->
忽略 Jison 的消息,它包含冗余的行号信息。忽略标记,我们直接从词法分析器获取其值,以防错误是由生成的标记引起的,该标记可能引用其来源。
{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
第二个参数具有 loc
属性,该属性应具有此标记的位置数据。不幸的是,Jison 似乎发送了一个过时的 loc
(来自上一个标记),因此我们直接从词法分析器获取位置信息。
helpers.throwSyntaxError "unexpected #{errorText}", errorLoc
基于 http://v8.googlecode.com/svn/branches/bleeding_edge/src/messages.js 修改为处理 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()
检查 sourceMap 位置
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 可能会使用 options.filename
的 <anonymous>
进行编译,但浏览器可能会使用脚本文件的名称请求堆栈跟踪。
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
基于 michaelficarra/CoffeeScriptRedux NodeJS / V8 不支持使用 sourceMap 转换堆栈跟踪中的位置,因此我们必须修补 Error 以显示 CoffeeScript 源位置。
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"