{Lexer} = require './lexer'
{parser} = require './parser'
helpers = require './helpers'
SourceMap = require './sourcemap'
CoffeeScript 可以在服务器端使用,作为基于 Node.js/V8 的命令行编译器,也可以直接在浏览器中运行 CoffeeScript。此模块包含用于将源 CoffeeScript 代码标记化、解析和编译为 JavaScript 的主要入口函数。
{Lexer} = require './lexer'
{parser} = require './parser'
helpers = require './helpers'
SourceMap = require './sourcemap'
需要 package.json
,它位于此文件上方的两个级别,因为此文件是从 lib/coffeescript
评估的。
packageJson = require '../../package.json'
当前 CoffeeScript 版本号。
exports.VERSION = packageJson.version
exports.FILE_EXTENSIONS = FILE_EXTENSIONS = ['.coffee', '.litcoffee', '.coffee.md']
公开用于测试的帮助程序。
exports.helpers = helpers
{getSourceMap, registerCompiled} = SourceMap
这被导出以允许外部模块实现源映射的缓存。这仅在调用 patchStackTrace
以调整具有缓存源映射的文件的堆栈跟踪时使用。
exports.registerCompiled = registerCompiled
允许在 nodejs 和浏览器中使用 btoa 的函数。
base64encode = (src) -> switch
when typeof Buffer is 'function'
Buffer.from(src).toString('base64')
when typeof btoa is 'function'
<script>
块的内容通过 UTF-16 编码,因此如果块中使用任何扩展字符,btoa 将失败,因为它在 UTF-8 上达到最大值。有关详细信息以及此处实现的解决方案,请参阅 https://developer.mozilla.org/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
使用 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 = {}) ->
克隆 options
,以避免更改传入的 options
对象。
options = Object.assign {}, options
generateSourceMap = options.sourceMap or options.inlineMap or not options.filename?
filename = options.filename or helpers.anonymousFileName()
checkShebangLine 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
nodes = parser.parse tokens
如果所有请求的只是节点的 POJO 表示,例如抽象语法树 (AST),我们现在就可以停止并只返回它(在修复根/File
»Program
节点的定位数据之后,该数据可能由于词法分析器中的 clean
函数而与原始源代码错位)。
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
使用来自每个片段的数据更新源映射。
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
if options.transpile
if typeof options.transpile isnt 'object'
这仅在通过 Node API 运行且 transpile
设置为除对象以外的其他内容时发生。
throw new Error 'The transpile option must be given an object with options to pass to Babel'
如果此编译器通过 CLI 或 Node API 运行,则获取传递给我们的 Babel 的引用。
transpiler = options.transpile.transpile
delete options.transpile.transpile
transpilerOptions = Object.assign {}, options.transpile
参见 https://github.com/babel/babel/issues/827#issuecomment-77573107:Babel 可以将 v3 源映射对象作为输入在 inputSourceMap
中使用,它将在其输出中返回一个更新的 v3 源映射对象。
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=#{filename}"
js = "#{js}\n#{sourceMapDataURI}\n#{sourceURL}"
registerCompiled filename, code, map
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) ->
source = lexer.tokenize source, options if typeof source is 'string'
parser.parse source
此文件以前导出这些方法;留下抛出警告的存根。这些方法已移至 index.coffee
以提供 Node 和非 Node 环境的单独入口点,以便静态分析工具在为非 Node 环境编译时不会阻塞 Node 包。
exports.run = exports.eval = exports.register = ->
throw new Error 'require index.coffee, not this file'
为我们在此处使用实例化词法分析器。
lexer = new Lexer
真正的词法分析器会生成通用的标记流。此对象在其周围提供了一个薄包装器,与 Jison API 兼容。然后,我们可以将其直接作为“Jison 词法分析器”传递。
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: -> ''
使所有 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
exports.patchStackTrace = ->
基于 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
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
基于 michaelficarra/CoffeeScriptRedux NodeJS / V8 不支持使用 sourceMap 转换堆栈跟踪中的位置,因此我们必须修补 Error 以显示 CoffeeScript 源位置。
Error.prepareStackTrace = (err, stack) ->
frames = for frame in stack
不要显示比 CoffeeScript.run
更深的堆栈帧。
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, 1)[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}"