mirror of
https://github.com/jashkenas/coffeescript.git
synced 2022-11-09 12:23:24 -05:00

* Get `coffee` command working again in Node 6, by converting the ‘await’ wrapper in the REPL to use a Promise instead of the ‘await’ keyword; add tests for REPL ‘await’ wrapper, including test to skip async tests if the runtime doesn’t support them * Fix async tests: now if a test function is a Promise (which an `await` function is), we add it to an array of async test functions and wait for them all to resolve before finishing the test run, so that if any async tests fail we see those failures in the output * Code review * Unnecessary * Let's support Node 6+ if we can * Simplify the returned promise * Simplify async check
500 lines
18 KiB
CoffeeScript
500 lines
18 KiB
CoffeeScript
fs = require 'fs'
|
||
os = require 'os'
|
||
path = require 'path'
|
||
_ = require 'underscore'
|
||
{ spawn, exec, execSync } = require 'child_process'
|
||
CoffeeScript = require './lib/coffeescript'
|
||
helpers = require './lib/coffeescript/helpers'
|
||
|
||
# ANSI Terminal Colors.
|
||
bold = red = green = yellow = reset = ''
|
||
unless process.env.NODE_DISABLE_COLORS
|
||
bold = '\x1B[0;1m'
|
||
red = '\x1B[0;31m'
|
||
green = '\x1B[0;32m'
|
||
yellow = '\x1B[0;33m'
|
||
reset = '\x1B[0m'
|
||
|
||
# Built file header.
|
||
header = """
|
||
/**
|
||
* CoffeeScript Compiler v#{CoffeeScript.VERSION}
|
||
* http://coffeescript.org
|
||
*
|
||
* Copyright 2011, Jeremy Ashkenas
|
||
* Released under the MIT License
|
||
*/
|
||
"""
|
||
|
||
# Used in folder names like `docs/v1`.
|
||
majorVersion = parseInt CoffeeScript.VERSION.split('.')[0], 10
|
||
|
||
|
||
# Log a message with a color.
|
||
log = (message, color, explanation) ->
|
||
console.log color + message + reset + ' ' + (explanation or '')
|
||
|
||
|
||
spawnNodeProcess = (args, output = 'stderr', callback) ->
|
||
relayOutput = (buffer) -> console.log buffer.toString()
|
||
proc = spawn 'node', args
|
||
proc.stdout.on 'data', relayOutput if output is 'both' or output is 'stdout'
|
||
proc.stderr.on 'data', relayOutput if output is 'both' or output is 'stderr'
|
||
proc.on 'exit', (status) -> callback(status) if typeof callback is 'function'
|
||
|
||
# Run a CoffeeScript through our node/coffee interpreter.
|
||
run = (args, callback) ->
|
||
spawnNodeProcess ['bin/coffee'].concat(args), 'stderr', (status) ->
|
||
process.exit(1) if status isnt 0
|
||
callback() if typeof callback is 'function'
|
||
|
||
|
||
# Build the CoffeeScript language from source.
|
||
buildParser = ->
|
||
helpers.extend global, require 'util'
|
||
require 'jison'
|
||
# We don't need `moduleMain`, since the parser is unlikely to be run standalone.
|
||
parser = require('./lib/coffeescript/grammar').parser.generate(moduleMain: ->)
|
||
fs.writeFileSync 'lib/coffeescript/parser.js', parser
|
||
|
||
buildExceptParser = (callback) ->
|
||
files = fs.readdirSync 'src'
|
||
files = ('src/' + file for file in files when file.match(/\.(lit)?coffee$/))
|
||
run ['-c', '-o', 'lib/coffeescript'].concat(files), callback
|
||
|
||
build = (callback) ->
|
||
buildParser()
|
||
buildExceptParser callback
|
||
|
||
testBuiltCode = (watch = no) ->
|
||
csPath = './lib/coffeescript'
|
||
csDir = path.dirname require.resolve csPath
|
||
|
||
for mod of require.cache when csDir is mod[0 ... csDir.length]
|
||
delete require.cache[mod]
|
||
|
||
testResults = runTests require csPath
|
||
unless watch
|
||
process.exit 1 unless testResults
|
||
|
||
buildAndTest = (includingParser = yes, harmony = no) ->
|
||
process.stdout.write '\x1Bc' # Clear terminal screen.
|
||
execSync 'git checkout lib/*', stdio: [0,1,2] # Reset the generated compiler.
|
||
|
||
buildArgs = ['bin/cake']
|
||
buildArgs.push if includingParser then 'build' else 'build:except-parser'
|
||
log "building#{if includingParser then ', including parser' else ''}...", green
|
||
spawnNodeProcess buildArgs, 'both', ->
|
||
log 'testing...', green
|
||
testArgs = if harmony then ['--harmony'] else []
|
||
testArgs = testArgs.concat ['bin/cake', 'test']
|
||
spawnNodeProcess testArgs, 'both'
|
||
|
||
watchAndBuildAndTest = (harmony = no) ->
|
||
buildAndTest yes, harmony
|
||
fs.watch 'src/', interval: 200, (eventType, filename) ->
|
||
if eventType is 'change'
|
||
log "src/#{filename} changed, rebuilding..."
|
||
buildAndTest (filename is 'grammar.coffee'), harmony
|
||
fs.watch 'test/', {interval: 200, recursive: yes}, (eventType, filename) ->
|
||
if eventType is 'change'
|
||
log "test/#{filename} changed, rebuilding..."
|
||
buildAndTest no, harmony
|
||
|
||
|
||
task 'build', 'build the CoffeeScript compiler from source', build
|
||
|
||
task 'build:parser', 'build the Jison parser only', buildParser
|
||
|
||
task 'build:except-parser', 'build the CoffeeScript compiler, except for the Jison parser', buildExceptParser
|
||
|
||
task 'build:full', 'build the CoffeeScript compiler from source twice, and run the tests', ->
|
||
build ->
|
||
build testBuiltCode
|
||
|
||
task 'build:browser', 'merge the built scripts into a single file for use in a browser', ->
|
||
code = """
|
||
require['../../package.json'] = (function() {
|
||
return #{fs.readFileSync "./package.json"};
|
||
})();
|
||
"""
|
||
for name in ['helpers', 'rewriter', 'lexer', 'parser', 'scope', 'nodes', 'sourcemap', 'coffeescript', 'browser']
|
||
code += """
|
||
require['./#{name}'] = (function() {
|
||
var exports = {}, module = {exports: exports};
|
||
#{fs.readFileSync "lib/coffeescript/#{name}.js"}
|
||
return module.exports;
|
||
})();
|
||
"""
|
||
code = """
|
||
(function(root) {
|
||
var CoffeeScript = function() {
|
||
function require(path){ return require[path]; }
|
||
#{code}
|
||
return require['./browser'];
|
||
}();
|
||
|
||
if (typeof define === 'function' && define.amd) {
|
||
define(function() { return CoffeeScript; });
|
||
} else {
|
||
root.CoffeeScript = CoffeeScript;
|
||
}
|
||
}(this));
|
||
"""
|
||
babel = require 'babel-core'
|
||
presets = []
|
||
# Exclude the `modules` plugin in order to not break the `}(this));`
|
||
# at the end of the above code block.
|
||
presets.push ['env', {modules: no}] unless process.env.TRANSFORM is 'false'
|
||
babelOptions =
|
||
presets: presets
|
||
sourceType: 'script'
|
||
{ code } = babel.transform code, babelOptions unless presets.length is 0
|
||
# Running Babel twice due to https://github.com/babel/babili/issues/614.
|
||
# Once that issue is fixed, move the `babili` preset back up into the
|
||
# `presets` array and run Babel once with both presets together.
|
||
presets = if process.env.MINIFY is 'false' then [] else ['babili']
|
||
babelOptions =
|
||
compact: process.env.MINIFY isnt 'false'
|
||
presets: presets
|
||
sourceType: 'script'
|
||
{ code } = babel.transform code, babelOptions unless presets.length is 0
|
||
outputFolder = "docs/v#{majorVersion}/browser-compiler"
|
||
fs.mkdirSync outputFolder unless fs.existsSync outputFolder
|
||
fs.writeFileSync "#{outputFolder}/coffeescript.js", header + '\n' + code
|
||
|
||
task 'build:browser:full', 'merge the built scripts into a single file for use in a browser, and test it', ->
|
||
invoke 'build:browser'
|
||
console.log "built ... running browser tests:"
|
||
invoke 'test:browser'
|
||
|
||
task 'build:watch', 'watch and continually rebuild the CoffeeScript compiler, running tests on each build', ->
|
||
watchAndBuildAndTest()
|
||
|
||
task 'build:watch:harmony', 'watch and continually rebuild the CoffeeScript compiler, running harmony tests on each build', ->
|
||
watchAndBuildAndTest yes
|
||
|
||
|
||
buildDocs = (watch = no) ->
|
||
# Constants
|
||
indexFile = 'documentation/index.html'
|
||
versionedSourceFolder = "documentation/v#{majorVersion}"
|
||
sectionsSourceFolder = 'documentation/sections'
|
||
examplesSourceFolder = 'documentation/examples'
|
||
outputFolder = "docs/v#{majorVersion}"
|
||
|
||
# Helpers
|
||
releaseHeader = (date, version, prevVersion) ->
|
||
monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
|
||
|
||
formatDate = (date) ->
|
||
date.replace /^(\d\d\d\d)-(\d\d)-(\d\d)$/, (match, $1, $2, $3) ->
|
||
"#{monthNames[$2 - 1]} #{+$3}, #{$1}"
|
||
|
||
"""
|
||
<div class="anchor" id="#{version}"></div>
|
||
<h2 class="header">
|
||
#{prevVersion and "<a href=\"https://github.com/jashkenas/coffeescript/compare/#{prevVersion}...#{version}\">#{version}</a>" or version}
|
||
<span class="timestamp"> — <time datetime="#{date}">#{formatDate date}</time></span>
|
||
</h2>
|
||
"""
|
||
|
||
codeFor = require "./documentation/v#{majorVersion}/code.coffee"
|
||
|
||
htmlFor = ->
|
||
hljs = require 'highlight.js'
|
||
hljs.configure classPrefix: ''
|
||
markdownRenderer = require('markdown-it')
|
||
html: yes
|
||
typographer: yes
|
||
highlight: (str, lang) ->
|
||
# From https://github.com/markdown-it/markdown-it#syntax-highlighting
|
||
if lang and hljs.getLanguage(lang)
|
||
try
|
||
return hljs.highlight(lang, str).value
|
||
catch ex
|
||
return '' # No syntax highlighting
|
||
|
||
|
||
# Add some custom overrides to Markdown-It’s rendering, per
|
||
# https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md#renderer
|
||
defaultFence = markdownRenderer.renderer.rules.fence
|
||
markdownRenderer.renderer.rules.fence = (tokens, idx, options, env, slf) ->
|
||
code = tokens[idx].content
|
||
if code.indexOf('codeFor(') is 0 or code.indexOf('releaseHeader(') is 0
|
||
"<%= #{code} %>"
|
||
else
|
||
"<blockquote class=\"uneditable-code-block\">#{defaultFence.apply @, arguments}</blockquote>"
|
||
|
||
(file, bookmark) ->
|
||
md = fs.readFileSync "#{sectionsSourceFolder}/#{file}.md", 'utf-8'
|
||
md = md.replace /<%= releaseHeader %>/g, releaseHeader
|
||
md = md.replace /<%= majorVersion %>/g, majorVersion
|
||
md = md.replace /<%= fullVersion %>/g, CoffeeScript.VERSION
|
||
html = markdownRenderer.render md
|
||
html = _.template(html)
|
||
codeFor: codeFor()
|
||
releaseHeader: releaseHeader
|
||
|
||
include = ->
|
||
(file) ->
|
||
file = "#{versionedSourceFolder}/#{file}" if file.indexOf('/') is -1
|
||
output = fs.readFileSync file, 'utf-8'
|
||
if /\.html$/.test(file)
|
||
render = _.template output
|
||
output = render
|
||
releaseHeader: releaseHeader
|
||
majorVersion: majorVersion
|
||
fullVersion: CoffeeScript.VERSION
|
||
htmlFor: htmlFor()
|
||
codeFor: codeFor()
|
||
include: include()
|
||
output
|
||
|
||
# Task
|
||
do renderIndex = ->
|
||
render = _.template fs.readFileSync(indexFile, 'utf-8')
|
||
output = render
|
||
include: include()
|
||
fs.writeFileSync "#{outputFolder}/index.html", output
|
||
log 'compiled', green, "#{indexFile} → #{outputFolder}/index.html"
|
||
try
|
||
fs.symlinkSync "v#{majorVersion}/index.html", 'docs/index.html'
|
||
catch exception
|
||
|
||
if watch
|
||
for target in [indexFile, versionedSourceFolder, examplesSourceFolder, sectionsSourceFolder]
|
||
fs.watch target, interval: 200, renderIndex
|
||
log 'watching...', green
|
||
|
||
task 'doc:site', 'build the documentation for the website', ->
|
||
buildDocs()
|
||
|
||
task 'doc:site:watch', 'watch and continually rebuild the documentation for the website', ->
|
||
buildDocs yes
|
||
|
||
|
||
buildDocTests = (watch = no) ->
|
||
# Constants
|
||
testFile = 'documentation/test.html'
|
||
testsSourceFolder = 'test'
|
||
outputFolder = "docs/v#{majorVersion}"
|
||
|
||
# Included in test.html
|
||
testHelpers = fs.readFileSync('test/support/helpers.coffee', 'utf-8').replace /exports\./g, '@'
|
||
|
||
# Helpers
|
||
testsInScriptBlocks = ->
|
||
output = ''
|
||
for filename in fs.readdirSync testsSourceFolder
|
||
if filename.indexOf('.coffee') isnt -1
|
||
type = 'coffeescript'
|
||
else if filename.indexOf('.litcoffee') isnt -1
|
||
type = 'literate-coffeescript'
|
||
else
|
||
continue
|
||
|
||
# Set the type to text/x-coffeescript or text/x-literate-coffeescript
|
||
# to prevent the browser compiler from automatically running the script
|
||
output += """
|
||
<script type="text/x-#{type}" class="test" id="#{filename.split('.')[0]}">
|
||
#{fs.readFileSync "test/#{filename}", 'utf-8'}
|
||
</script>\n
|
||
"""
|
||
output
|
||
|
||
# Task
|
||
do renderTest = ->
|
||
render = _.template fs.readFileSync(testFile, 'utf-8')
|
||
output = render
|
||
testHelpers: testHelpers
|
||
tests: testsInScriptBlocks()
|
||
fs.writeFileSync "#{outputFolder}/test.html", output
|
||
log 'compiled', green, "#{testFile} → #{outputFolder}/test.html"
|
||
|
||
if watch
|
||
for target in [testFile, testsSourceFolder]
|
||
fs.watch target, interval: 200, renderTest
|
||
log 'watching...', green
|
||
|
||
task 'doc:test', 'build the browser-based tests', ->
|
||
buildDocTests()
|
||
|
||
task 'doc:test:watch', 'watch and continually rebuild the browser-based tests', ->
|
||
buildDocTests yes
|
||
|
||
|
||
buildAnnotatedSource = (watch = no) ->
|
||
do generateAnnotatedSource = ->
|
||
exec "node_modules/docco/bin/docco src/*.*coffee --output docs/v#{majorVersion}/annotated-source", (err) -> throw err if err
|
||
log 'generated', green, "annotated source in docs/v#{majorVersion}/annotated-source/"
|
||
|
||
if watch
|
||
fs.watch 'src/', interval: 200, generateAnnotatedSource
|
||
log 'watching...', green
|
||
|
||
task 'doc:source', 'build the annotated source documentation', ->
|
||
buildAnnotatedSource()
|
||
|
||
task 'doc:source:watch', 'watch and continually rebuild the annotated source documentation', ->
|
||
buildAnnotatedSource yes
|
||
|
||
|
||
task 'release', 'build and test the CoffeeScript source, and build the documentation', ->
|
||
execSync '''
|
||
cake build:full
|
||
cake build:browser
|
||
cake test:browser
|
||
cake test:integrations
|
||
cake doc:site
|
||
cake doc:test
|
||
cake doc:source''', stdio: 'inherit'
|
||
|
||
|
||
task 'bench', 'quick benchmark of compilation time', ->
|
||
{Rewriter} = require './lib/coffeescript/rewriter'
|
||
sources = ['coffeescript', 'grammar', 'helpers', 'lexer', 'nodes', 'rewriter']
|
||
coffee = sources.map((name) -> fs.readFileSync "src/#{name}.coffee").join '\n'
|
||
litcoffee = fs.readFileSync("src/scope.litcoffee").toString()
|
||
fmt = (ms) -> " #{bold}#{ " #{ms}".slice -4 }#{reset} ms"
|
||
total = 0
|
||
now = Date.now()
|
||
time = -> total += ms = -(now - now = Date.now()); fmt ms
|
||
tokens = CoffeeScript.tokens coffee, rewrite: no
|
||
littokens = CoffeeScript.tokens litcoffee, rewrite: no, literate: yes
|
||
tokens = tokens.concat(littokens)
|
||
console.log "Lex #{time()} (#{tokens.length} tokens)"
|
||
tokens = new Rewriter().rewrite tokens
|
||
console.log "Rewrite#{time()} (#{tokens.length} tokens)"
|
||
nodes = CoffeeScript.nodes tokens
|
||
console.log "Parse #{time()}"
|
||
js = nodes.compile bare: yes
|
||
console.log "Compile#{time()} (#{js.length} chars)"
|
||
console.log "total #{ fmt total }"
|
||
|
||
|
||
# Run the CoffeeScript test suite.
|
||
runTests = (CoffeeScript) ->
|
||
CoffeeScript.register() unless global.testingBrowser
|
||
|
||
# These are attached to `global` so that they’re accessible from within
|
||
# `test/async.coffee`, which has an async-capable version of
|
||
# `global.test`.
|
||
global.currentFile = null
|
||
global.passedTests = 0
|
||
global.failures = []
|
||
|
||
global[name] = func for name, func of require 'assert'
|
||
|
||
# Convenience aliases.
|
||
global.CoffeeScript = CoffeeScript
|
||
global.Repl = require './lib/coffeescript/repl'
|
||
global.bold = bold
|
||
global.red = red
|
||
global.green = green
|
||
global.yellow = yellow
|
||
global.reset = reset
|
||
|
||
asyncTests = []
|
||
onFail = (description, fn, err) ->
|
||
failures.push
|
||
filename: global.currentFile
|
||
error: err
|
||
description: description
|
||
source: fn.toString() if fn.toString?
|
||
|
||
# Our test helper function for delimiting different test cases.
|
||
global.test = (description, fn) ->
|
||
try
|
||
fn.test = {description, currentFile}
|
||
result = fn.call(fn)
|
||
if result instanceof Promise # An async test.
|
||
asyncTests.push result
|
||
result.then ->
|
||
passedTests++
|
||
.catch (err) ->
|
||
onFail description, fn, err
|
||
else
|
||
passedTests++
|
||
catch err
|
||
onFail description, fn, err
|
||
|
||
global.supportsAsync = try
|
||
new Function('async () => {}')()
|
||
yes
|
||
catch
|
||
no
|
||
|
||
helpers.extend global, require './test/support/helpers'
|
||
|
||
# When all the tests have run, collect and print errors.
|
||
# If a stacktrace is available, output the compiled function source.
|
||
process.on 'exit', ->
|
||
time = ((Date.now() - startTime) / 1000).toFixed(2)
|
||
message = "passed #{passedTests} tests in #{time} seconds#{reset}"
|
||
return log(message, green) unless failures.length
|
||
log "failed #{failures.length} and #{message}", red
|
||
for fail in failures
|
||
{error, filename, description, source} = fail
|
||
console.log ''
|
||
log " #{description}", red if description
|
||
log " #{error.stack}", red
|
||
console.log " #{source}" if source
|
||
return
|
||
|
||
# Run every test in the `test` folder, recording failures.
|
||
files = fs.readdirSync 'test'
|
||
unless global.supportsAsync # Except for async tests, if async isn’t supported.
|
||
files = files.filter (filename) -> filename isnt 'async.coffee'
|
||
|
||
startTime = Date.now()
|
||
for file in files when helpers.isCoffee file
|
||
literate = helpers.isLiterate file
|
||
currentFile = filename = path.join 'test', file
|
||
code = fs.readFileSync filename
|
||
try
|
||
CoffeeScript.run code.toString(), {filename, literate}
|
||
catch error
|
||
failures.push {filename, error}
|
||
|
||
Promise.all(asyncTests).then ->
|
||
Promise.reject() if failures.length isnt 0
|
||
|
||
|
||
task 'test', 'run the CoffeeScript language test suite', ->
|
||
runTests(CoffeeScript).catch -> process.exit 1
|
||
|
||
|
||
task 'test:browser', 'run the test suite against the merged browser script', ->
|
||
source = fs.readFileSync "docs/v#{majorVersion}/browser-compiler/coffeescript.js", 'utf-8'
|
||
result = {}
|
||
global.testingBrowser = yes
|
||
(-> eval source).call result
|
||
runTests(CoffeeScript).catch -> process.exit 1
|
||
|
||
|
||
task 'test:integrations', 'test the module integrated with other libraries and environments', ->
|
||
# Tools like Webpack and Browserify generate builds intended for a browser
|
||
# environment where Node modules are not available. We want to ensure that
|
||
# the CoffeeScript module as presented by the `browser` key in `package.json`
|
||
# can be built by such tools; if such a build succeeds, it verifies that no
|
||
# Node modules are required as part of the compiler (as opposed to the tests)
|
||
# and that therefore the compiler will run in a browser environment.
|
||
tmpdir = os.tmpdir()
|
||
try
|
||
buildLog = execSync "./node_modules/webpack/bin/webpack.js
|
||
--entry=./
|
||
--output-library=CoffeeScript
|
||
--output-library-target=commonjs2
|
||
--output-path=#{tmpdir}
|
||
--output-filename=coffeescript.js"
|
||
catch exception
|
||
console.error buildLog.toString()
|
||
throw exception
|
||
|
||
builtCompiler = path.join tmpdir, 'coffeescript.js'
|
||
CoffeeScript = require builtCompiler
|
||
global.testingBrowser = yes
|
||
testResults = runTests CoffeeScript
|
||
fs.unlinkSync builtCompiler
|
||
process.exit 1 unless testResults
|