The coffee
utility. Handles command-line compilation of CoffeeScript
into various forms: saved into .js
files or printed to stdout
or recompiled every time the source is saved,
printed as a token stream or as the syntax tree, or launch an
interactive REPL.
External dependencies.
fs = require 'fs' path = require 'path' helpers = require './helpers' optparse = require './optparse' CoffeeScript = require './' {spawn, exec} = require 'child_process' {EventEmitter} = require 'events' useWinPathSep = path.sep is '\\'
Allow CoffeeScript to emit Node.js events.
helpers.extend CoffeeScript, new EventEmitter printLine = (line) -> process.stdout.write line + '\n' printWarn = (line) -> process.stderr.write line + '\n' hidden = (file) -> /^\.|~$/.test file
The help banner that is printed in conjunction with -h
/--help
.
BANNER = '''
Usage: coffee [options] path/to/script.coffee [args]
If called without options, `coffee` will run your script.
'''
The list of all the valid option flags that coffee
knows how to handle.
SWITCHES = [ [ '--ast', 'generate an abstract syntax tree of nodes'] ['-b', '--bare', 'compile without a top-level function wrapper'] ['-c', '--compile', 'compile to JavaScript and save as .js files'] ['-e', '--eval', 'pass a string from the command line as input'] ['-h', '--help', 'display this help message'] ['-i', '--interactive', 'run an interactive CoffeeScript REPL'] ['-j', '--join [FILE]', 'concatenate the source CoffeeScript before compiling'] ['-l', '--literate', 'treat stdio as literate style coffeescript'] ['-m', '--map', 'generate source map and save as .js.map files'] ['-M', '--inline-map', 'generate source map and include it directly in output'] ['-n', '--nodes', 'print out the parse tree that the parser produces'] [ '--nodejs [ARGS]', 'pass options directly to the "node" binary'] [ '--no-header', 'suppress the "Generated by" header'] ['-o', '--output [PATH]', 'set the output path or path/filename for compiled JavaScript'] ['-p', '--print', 'print out the compiled JavaScript'] ['-r', '--require [MODULE*]', 'require the given module before eval or REPL'] ['-s', '--stdio', 'listen for and compile scripts over stdio'] ['-t', '--transpile', 'pipe generated JavaScript through Babel'] [ '--tokens', 'print out the tokens that the lexer/rewriter produce'] ['-v', '--version', 'display the version number'] ['-w', '--watch', 'watch scripts for changes and rerun commands'] ]
Top-level objects shared by all the functions.
opts = {} sources = [] sourceCode = [] notSources = {} watchedDirs = {} optionParser = null exports.buildCSOptionParser = buildCSOptionParser = -> new optparse.OptionParser SWITCHES, BANNER
Run coffee
by parsing passed options and determining what action to take.
Many flags cause us to divert before compiling anything. Flags passed after
--
will be passed verbatim to your script as arguments in process.argv
exports.run = -> optionParser = buildCSOptionParser() try parseOptions() catch err console.error "option parsing error: #{err.message}" process.exit 1 if (not opts.doubleDashed) and (opts.arguments[1] is '--') printWarn ''' coffee was invoked with '--' as the second positional argument, which is now deprecated. To pass '--' as an argument to a script to run, put an additional '--' before the path to your script. '--' will be removed from the argument list. ''' printWarn "The positional arguments were: #{JSON.stringify opts.arguments}" opts.arguments = [opts.arguments[0]].concat opts.arguments[2..]
Make the REPL CLI use the global context so as to (a) be consistent with the
node
REPL CLI and, therefore, (b) make packages that modify native prototypes
(such as ‘colors’ and ‘sugar’) work as expected.
replCliOpts = useGlobal: yes opts.prelude = makePrelude opts.require if opts.require replCliOpts.prelude = opts.prelude replCliOpts.transpile = opts.transpile return forkNode() if opts.nodejs return usage() if opts.help return version() if opts.version return require('./repl').start(replCliOpts) if opts.interactive return compileStdio() if opts.stdio return compileScript null, opts.arguments[0] if opts.eval return require('./repl').start(replCliOpts) unless opts.arguments.length literals = if opts.run then opts.arguments.splice 1 else [] process.argv = process.argv[0..1].concat literals process.argv[0] = 'coffee' if opts.output outputBasename = path.basename opts.output if '.' in outputBasename and outputBasename not in ['.', '..'] and not helpers.ends(opts.output, path.sep)
An output filename was specified, e.g. /dist/scripts.js
.
opts.outputFilename = outputBasename
opts.outputPath = path.resolve path.dirname opts.output
else
An output path was specified, e.g. /dist
.
opts.outputFilename = null opts.outputPath = path.resolve opts.output if opts.join opts.join = path.resolve opts.join console.error ''' The --join option is deprecated and will be removed in a future version. If for some reason it's necessary to share local variables between files, replace... $ coffee --compile --join bundle.js -- a.coffee b.coffee c.coffee with... $ cat a.coffee b.coffee c.coffee | coffee --compile --stdio > bundle.js ''' for source in opts.arguments source = path.resolve source compilePath source, yes, source makePrelude = (requires) -> requires.map (module) -> [full, name, module] = match if match = module.match(/^(.*)=(.*)$/) name or= helpers.baseFileName module, yes, useWinPathSep "global['#{name}'] = require('#{module}')" .join ';'
Compile a path, which could be a script or a directory. If a directory is passed, recursively compile all ‘.coffee’, ‘.litcoffee’, and ‘.coffee.md’ extension source files in it and all subdirectories.
compilePath = (source, topLevel, base) -> return if source in sources or watchedDirs[source] or not topLevel and (notSources[source] or hidden source) try stats = fs.statSync source catch err if err.code is 'ENOENT' console.error "File not found: #{source}" process.exit 1 throw err if stats.isDirectory() if path.basename(source) is 'node_modules' notSources[source] = yes return if opts.run compilePath findDirectoryIndex(source), topLevel, base return watchDir source, base if opts.watch try files = fs.readdirSync source catch err if err.code is 'ENOENT' then return else throw err for file in files compilePath (path.join source, file), no, base else if topLevel or helpers.isCoffee source sources.push source sourceCode.push null delete notSources[source] watch source, base if opts.watch try code = fs.readFileSync source catch err if err.code is 'ENOENT' then return else throw err compileScript source, code.toString(), base else notSources[source] = yes findDirectoryIndex = (source) -> for ext in CoffeeScript.FILE_EXTENSIONS index = path.join source, "index#{ext}" try return index if (fs.statSync index).isFile() catch err throw err unless err.code is 'ENOENT' console.error "Missing index.coffee or index.litcoffee in #{source}" process.exit 1
Compile a single source script, containing the given code, according to the
requested options. If evaluating the script directly, set __filename
,
__dirname
and module.filename
to be correct relative to the script’s path.
compileScript = (file, input, base = null) -> options = compileOptions file, base try task = {file, input, options} CoffeeScript.emit 'compile', task if opts.tokens printTokens CoffeeScript.tokens task.input, task.options else if opts.nodes printLine CoffeeScript.nodes(task.input, task.options).toString().trim() else if opts.ast compiled = CoffeeScript.compile task.input, task.options printLine JSON.stringify(compiled, null, 2) else if opts.run CoffeeScript.register() CoffeeScript.eval opts.prelude, task.options if opts.prelude CoffeeScript.run task.input, task.options else if opts.join and task.file isnt opts.join task.input = helpers.invertLiterate task.input if helpers.isLiterate file sourceCode[sources.indexOf(task.file)] = task.input compileJoin() else compiled = CoffeeScript.compile task.input, task.options task.output = compiled if opts.map task.output = compiled.js task.sourceMap = compiled.v3SourceMap CoffeeScript.emit 'success', task if opts.print printLine task.output.trim() else if opts.compile or opts.map saveTo = if opts.outputFilename and sources.length is 1 path.join opts.outputPath, opts.outputFilename else options.jsPath writeJs base, task.file, task.output, saveTo, task.sourceMap catch err CoffeeScript.emit 'failure', err, task return if CoffeeScript.listeners('failure').length message = err?.stack or "#{err}" if opts.watch printLine message + '\x07' else printWarn message process.exit 1
Attach the appropriate listeners to compile scripts incoming over stdin, and write them back to stdout.
compileStdio = -> if opts.map console.error '--stdio and --map cannot be used together' process.exit 1 buffers = [] stdin = process.openStdin() stdin.on 'data', (buffer) -> buffers.push buffer if buffer stdin.on 'end', -> compileScript null, Buffer.concat(buffers).toString()
If all of the source files are done being read, concatenate and compile them together.
joinTimeout = null compileJoin = -> return unless opts.join unless sourceCode.some((code) -> code is null) clearTimeout joinTimeout joinTimeout = wait 100, -> compileScript opts.join, sourceCode.join('\n'), opts.join
Watch a source CoffeeScript file using fs.watch
, recompiling it every
time the file is updated. May be used in combination with other options,
such as --print
.
watch = (source, base) -> watcher = null prevStats = null compileTimeout = null watchErr = (err) -> throw err unless err.code is 'ENOENT' return unless source in sources try rewatch() compile() catch removeSource source, base compileJoin() compile = -> clearTimeout compileTimeout compileTimeout = wait 25, -> fs.stat source, (err, stats) -> return watchErr err if err return rewatch() if prevStats and stats.size is prevStats.size and stats.mtime.getTime() is prevStats.mtime.getTime() prevStats = stats fs.readFile source, (err, code) -> return watchErr err if err compileScript(source, code.toString(), base) rewatch() startWatcher = -> watcher = fs.watch source .on 'change', compile .on 'error', (err) -> throw err unless err.code is 'EPERM' removeSource source, base rewatch = -> watcher?.close() startWatcher() try startWatcher() catch err watchErr err
Watch a directory of files for new additions.
watchDir = (source, base) -> watcher = null readdirTimeout = null startWatcher = -> watcher = fs.watch source .on 'error', (err) -> throw err unless err.code is 'EPERM' stopWatcher() .on 'change', -> clearTimeout readdirTimeout readdirTimeout = wait 25, -> try files = fs.readdirSync source catch err throw err unless err.code is 'ENOENT' return stopWatcher() for file in files compilePath (path.join source, file), no, base stopWatcher = -> watcher.close() removeSourceDir source, base watchedDirs[source] = yes try startWatcher() catch err throw err unless err.code is 'ENOENT' removeSourceDir = (source, base) -> delete watchedDirs[source] sourcesChanged = no for file in sources when source is path.dirname file removeSource file, base sourcesChanged = yes compileJoin() if sourcesChanged
Remove a file from our source list, and source code cache. Optionally remove the compiled JS version as well.
removeSource = (source, base) -> index = sources.indexOf source sources.splice index, 1 sourceCode.splice index, 1 unless opts.join silentUnlink outputPath source, base silentUnlink outputPath source, base, '.js.map' timeLog "removed #{source}" silentUnlink = (path) -> try fs.unlinkSync path catch err throw err unless err.code in ['ENOENT', 'EPERM']
Get the corresponding output JavaScript path for a source file.
outputPath = (source, base, extension=".js") -> basename = helpers.baseFileName source, yes, useWinPathSep srcDir = path.dirname source dir = unless opts.outputPath srcDir else if source is base opts.outputPath else path.join opts.outputPath, path.relative base, srcDir path.join dir, basename + extension
Recursively mkdir, like mkdir -p
.
mkdirp = (dir, fn) -> mode = 0o777 & ~process.umask() do mkdirs = (p = dir, fn) -> fs.exists p, (exists) -> if exists fn() else mkdirs path.dirname(p), -> fs.mkdir p, mode, (err) -> return fn err if err fn()
Write out a JavaScript source file with the compiled code. By default, files
are written out in cwd
as .js
files with the same name, but the output
directory can be customized with --output
.
If generatedSourceMap
is provided, this will write a .js.map
file into the
same directory as the .js
file.
writeJs = (base, sourcePath, js, jsPath, generatedSourceMap = null) -> sourceMapPath = "#{jsPath}.map" jsDir = path.dirname jsPath compile = -> if opts.compile js = ' ' if js.length <= 0 if generatedSourceMap then js = "#{js}\n//# sourceMappingURL=#{helpers.baseFileName sourceMapPath, no, useWinPathSep}\n" fs.writeFile jsPath, js, (err) -> if err printLine err.message process.exit 1 else if opts.compile and opts.watch timeLog "compiled #{sourcePath}" if generatedSourceMap fs.writeFile sourceMapPath, generatedSourceMap, (err) -> if err printLine "Could not write source map: #{err.message}" process.exit 1 fs.exists jsDir, (itExists) -> if itExists then compile() else mkdirp jsDir, compile
Convenience for cleaner setTimeouts.
wait = (milliseconds, func) -> setTimeout func, milliseconds
When watching scripts, it’s useful to log changes with the timestamp.
timeLog = (message) -> console.log "#{(new Date).toLocaleTimeString()} - #{message}"
Pretty-print a stream of tokens, sans location data.
printTokens = (tokens) -> strings = for token in tokens tag = token[0] value = token[1].toString().replace(/\n/, '\\n') "[#{tag} #{value}]" printLine strings.join(' ')
Use the OptionParser module to extract all options from
process.argv
that are specified in SWITCHES
.
parseOptions = -> o = opts = optionParser.parse process.argv[2..] o.compile or= !!o.output o.run = not (o.compile or o.print or o.map) o.print = !! (o.print or (o.eval or o.stdio and o.compile))
The compile-time options to pass to the CoffeeScript compiler.
compileOptions = (filename, base) -> if opts.transpile
The user has requested that the CoffeeScript compiler also transpile via Babel. We don’t include Babel as a dependency because we want to avoid dependencies in general, and most users probably won’t be relying on us to transpile for them; we assume most users will probably either run CoffeeScript’s output without transpilation (modern Node or evergreen browsers) or use a proper build chain like Gulp or Webpack.
try require '@babel/core' catch try require 'babel-core' catch
Give appropriate instructions depending on whether coffee
was run
locally or globally.
if require.resolve('.').indexOf(process.cwd()) is 0 console.error ''' To use --transpile, you must have @babel/core installed: npm install --save-dev @babel/core And you must save options to configure Babel in one of the places it looks to find its options. See https://coffeescript.org/#transpilation ''' else console.error ''' To use --transpile with globally-installed CoffeeScript, you must have @babel/core installed globally: npm install --global @babel/core And you must save options to configure Babel in one of the places it looks to find its options, relative to the file being compiled or to the current folder. See https://coffeescript.org/#transpilation ''' process.exit 1 opts.transpile = {} unless typeof opts.transpile is 'object'
Pass a reference to Babel into the compiler, so that the transpile option
is available for the CLI. We need to do this so that tools like Webpack
can require('coffeescript')
and build correctly, without trying to
require Babel.
opts.transpile.transpile = CoffeeScript.transpile
Babel searches for its options (a .babelrc
file, a .babelrc.js
file,
a package.json
file with a babel
key, etc.) relative to the path
given to it in its filename
option. Make sure we have a path to pass
along.
unless opts.transpile.filename opts.transpile.filename = filename or path.resolve(base or process.cwd(), '<anonymous>') else opts.transpile = no answer = filename: filename literate: opts.literate or helpers.isLiterate(filename) bare: opts.bare header: opts.compile and not opts['no-header'] transpile: opts.transpile sourceMap: opts.map inlineMap: opts['inline-map'] ast: opts.ast if filename if base cwd = process.cwd() jsPath = outputPath filename, base jsDir = path.dirname jsPath answer = helpers.merge answer, { jsPath sourceRoot: path.relative(jsDir, cwd) + path.sep sourceFiles: [path.relative cwd, filename] generatedFile: helpers.baseFileName(jsPath, no, useWinPathSep) } else answer = helpers.merge answer, sourceRoot: "" sourceFiles: [helpers.baseFileName filename, no, useWinPathSep] generatedFile: helpers.baseFileName(filename, yes, useWinPathSep) + ".js" answer
Start up a new Node.js instance with the arguments in --nodejs
passed to
the node
binary, preserving the other options.
forkNode = -> nodeArgs = opts.nodejs.split /\s+/ args = process.argv[1..] args.splice args.indexOf('--nodejs'), 2 p = spawn process.execPath, nodeArgs.concat(args), cwd: process.cwd() env: process.env stdio: [0, 1, 2] for signal in ['SIGINT', 'SIGTERM'] process.on signal, do (signal) -> -> p.kill signal p.on 'exit', (code) -> process.exit code
Print the --help
usage message and exit. Deprecated switches are not
shown.
usage = ->
printLine optionParser.help()
Print the --version
message and exit.
version = -> printLine "CoffeeScript version #{CoffeeScript.VERSION}"