{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.')
函式包裝器,用於將來源檔案資訊新增至詞法分析器/剖析器/編譯器引發的 SyntaxError。
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'
)
檢查 import 或 export;如果找到,強制裸模式。
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
使用每個片段的資料更新 sourcemap。
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。
lexer = new Lexer
真正的 Lexer 會產生一個通用的 token 串流。此物件提供一個薄的包裝器,與 Jison API 相容。然後,我們可以直接將它傳遞為「Jison lexer」。
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 的訊息,它包含重複的行號資訊。略過 token,我們直接從 lexer 取得其值,以防錯誤是由產生的 token 造成的,而該 token 可能參考其來源。
{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
屬性,該屬性應具有此 token 的位置資料。很不幸地,Jison 似乎傳送了一個過期的 loc
(來自前一個 token),所以我們直接從 lexer 取得位置資訊。
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 轉換堆疊追蹤中的位置,所以我們必須使用 monkey-patch 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}"