PageRenderTime 73ms CodeModel.GetById 14ms app.highlight 52ms RepoModel.GetById 1ms app.codeStats 0ms

/src/command.coffee

http://github.com/jashkenas/coffee-script
CoffeeScript | 551 lines | 451 code | 43 blank | 57 comment | 68 complexity | f81cfad89e9a6b45b930fdf06ff4b729 MD5 | raw file
  1# The `coffee` utility. Handles command-line compilation of CoffeeScript
  2# into various forms: saved into `.js` files or printed to stdout
  3# or recompiled every time the source is saved,
  4# printed as a token stream or as the syntax tree, or launch an
  5# interactive REPL.
  6
  7# External dependencies.
  8fs             = require 'fs'
  9path           = require 'path'
 10helpers        = require './helpers'
 11optparse       = require './optparse'
 12CoffeeScript   = require './'
 13{spawn, exec}  = require 'child_process'
 14{EventEmitter} = require 'events'
 15
 16useWinPathSep  = path.sep is '\\'
 17
 18# Allow CoffeeScript to emit Node.js events.
 19helpers.extend CoffeeScript, new EventEmitter
 20
 21printLine = (line) -> process.stdout.write line + '\n'
 22printWarn = (line) -> process.stderr.write line + '\n'
 23
 24hidden = (file) -> /^\.|~$/.test file
 25
 26# The help banner that is printed in conjunction with `-h`/`--help`.
 27BANNER = '''
 28  Usage: coffee [options] path/to/script.coffee [args]
 29
 30  If called without options, `coffee` will run your script.
 31'''
 32
 33# The list of all the valid option flags that `coffee` knows how to handle.
 34SWITCHES = [
 35  [      '--ast',               'generate an abstract syntax tree of nodes']
 36  ['-b', '--bare',              'compile without a top-level function wrapper']
 37  ['-c', '--compile',           'compile to JavaScript and save as .js files']
 38  ['-e', '--eval',              'pass a string from the command line as input']
 39  ['-h', '--help',              'display this help message']
 40  ['-i', '--interactive',       'run an interactive CoffeeScript REPL']
 41  ['-j', '--join [FILE]',       'concatenate the source CoffeeScript before compiling']
 42  ['-l', '--literate',          'treat stdio as literate style coffeescript']
 43  ['-m', '--map',               'generate source map and save as .js.map files']
 44  ['-M', '--inline-map',        'generate source map and include it directly in output']
 45  ['-n', '--nodes',             'print out the parse tree that the parser produces']
 46  [      '--nodejs [ARGS]',     'pass options directly to the "node" binary']
 47  [      '--no-header',         'suppress the "Generated by" header']
 48  ['-o', '--output [PATH]',     'set the output path or path/filename for compiled JavaScript']
 49  ['-p', '--print',             'print out the compiled JavaScript']
 50  ['-r', '--require [MODULE*]', 'require the given module before eval or REPL']
 51  ['-s', '--stdio',             'listen for and compile scripts over stdio']
 52  ['-t', '--transpile',         'pipe generated JavaScript through Babel']
 53  [      '--tokens',            'print out the tokens that the lexer/rewriter produce']
 54  ['-v', '--version',           'display the version number']
 55  ['-w', '--watch',             'watch scripts for changes and rerun commands']
 56]
 57
 58# Top-level objects shared by all the functions.
 59opts         = {}
 60sources      = []
 61sourceCode   = []
 62notSources   = {}
 63watchedDirs  = {}
 64optionParser = null
 65
 66exports.buildCSOptionParser = buildCSOptionParser = ->
 67  new optparse.OptionParser SWITCHES, BANNER
 68
 69# Run `coffee` by parsing passed options and determining what action to take.
 70# Many flags cause us to divert before compiling anything. Flags passed after
 71# `--` will be passed verbatim to your script as arguments in `process.argv`
 72exports.run = ->
 73  optionParser = buildCSOptionParser()
 74  try parseOptions()
 75  catch err
 76    console.error "option parsing error: #{err.message}"
 77    process.exit 1
 78
 79  if (not opts.doubleDashed) and (opts.arguments[1] is '--')
 80    printWarn '''
 81      coffee was invoked with '--' as the second positional argument, which is
 82      now deprecated. To pass '--' as an argument to a script to run, put an
 83      additional '--' before the path to your script.
 84
 85      '--' will be removed from the argument list.
 86    '''
 87    printWarn "The positional arguments were: #{JSON.stringify opts.arguments}"
 88    opts.arguments = [opts.arguments[0]].concat opts.arguments[2..]
 89
 90  # Make the REPL *CLI* use the global context so as to (a) be consistent with the
 91  # `node` REPL CLI and, therefore, (b) make packages that modify native prototypes
 92  # (such as 'colors' and 'sugar') work as expected.
 93  replCliOpts = useGlobal: yes
 94  opts.prelude = makePrelude opts.require       if opts.require
 95  replCliOpts.prelude = opts.prelude
 96  replCliOpts.transpile = opts.transpile
 97  return forkNode()                             if opts.nodejs
 98  return usage()                                if opts.help
 99  return version()                              if opts.version
100  return require('./repl').start(replCliOpts)   if opts.interactive
101  return compileStdio()                         if opts.stdio
102  return compileScript null, opts.arguments[0]  if opts.eval
103  return require('./repl').start(replCliOpts)   unless opts.arguments.length
104  literals = if opts.run then opts.arguments.splice 1 else []
105  process.argv = process.argv[0..1].concat literals
106  process.argv[0] = 'coffee'
107
108  if opts.output
109    outputBasename = path.basename opts.output
110    if '.' in outputBasename and
111       outputBasename not in ['.', '..'] and
112       not helpers.ends(opts.output, path.sep)
113      # An output filename was specified, e.g. `/dist/scripts.js`.
114      opts.outputFilename = outputBasename
115      opts.outputPath = path.resolve path.dirname opts.output
116    else
117      # An output path was specified, e.g. `/dist`.
118      opts.outputFilename = null
119      opts.outputPath = path.resolve opts.output
120
121  if opts.join
122    opts.join = path.resolve opts.join
123    console.error '''
124
125    The --join option is deprecated and will be removed in a future version.
126
127    If for some reason it's necessary to share local variables between files,
128    replace...
129
130        $ coffee --compile --join bundle.js -- a.coffee b.coffee c.coffee
131
132    with...
133
134        $ cat a.coffee b.coffee c.coffee | coffee --compile --stdio > bundle.js
135
136    '''
137  for source in opts.arguments
138    source = path.resolve source
139    compilePath source, yes, source
140
141makePrelude = (requires) ->
142  requires.map (module) ->
143    [full, name, module] = match if match = module.match(/^(.*)=(.*)$/)
144    name or= helpers.baseFileName module, yes, useWinPathSep
145    "global['#{name}'] = require('#{module}')"
146  .join ';'
147
148# Compile a path, which could be a script or a directory. If a directory
149# is passed, recursively compile all '.coffee', '.litcoffee', and '.coffee.md'
150# extension source files in it and all subdirectories.
151compilePath = (source, topLevel, base) ->
152  return if source in sources   or
153            watchedDirs[source] or
154            not topLevel and (notSources[source] or hidden source)
155  try
156    stats = fs.statSync source
157  catch err
158    if err.code is 'ENOENT'
159      console.error "File not found: #{source}"
160      process.exit 1
161    throw err
162  if stats.isDirectory()
163    if path.basename(source) is 'node_modules'
164      notSources[source] = yes
165      return
166    if opts.run
167      compilePath findDirectoryIndex(source), topLevel, base
168      return
169    watchDir source, base if opts.watch
170    try
171      files = fs.readdirSync source
172    catch err
173      if err.code is 'ENOENT' then return else throw err
174    for file in files
175      compilePath (path.join source, file), no, base
176  else if topLevel or helpers.isCoffee source
177    sources.push source
178    sourceCode.push null
179    delete notSources[source]
180    watch source, base if opts.watch
181    try
182      code = fs.readFileSync source
183    catch err
184      if err.code is 'ENOENT' then return else throw err
185    compileScript source, code.toString(), base
186  else
187    notSources[source] = yes
188
189findDirectoryIndex = (source) ->
190  for ext in CoffeeScript.FILE_EXTENSIONS
191    index = path.join source, "index#{ext}"
192    try
193      return index if (fs.statSync index).isFile()
194    catch err
195      throw err unless err.code is 'ENOENT'
196  console.error "Missing index.coffee or index.litcoffee in #{source}"
197  process.exit 1
198
199# Compile a single source script, containing the given code, according to the
200# requested options. If evaluating the script directly, set `__filename`,
201# `__dirname` and `module.filename` to be correct relative to the script's path.
202compileScript = (file, input, base = null) ->
203  options = compileOptions file, base
204  try
205    task = {file, input, options}
206    CoffeeScript.emit 'compile', task
207    if opts.tokens
208      printTokens CoffeeScript.tokens task.input, task.options
209    else if opts.nodes
210      printLine CoffeeScript.nodes(task.input, task.options).toString().trim()
211    else if opts.ast
212      compiled = CoffeeScript.compile task.input, task.options
213      printLine JSON.stringify(compiled, null, 2)
214    else if opts.run
215      CoffeeScript.register()
216      CoffeeScript.eval opts.prelude, task.options if opts.prelude
217      CoffeeScript.run task.input, task.options
218    else if opts.join and task.file isnt opts.join
219      task.input = helpers.invertLiterate task.input if helpers.isLiterate file
220      sourceCode[sources.indexOf(task.file)] = task.input
221      compileJoin()
222    else
223      compiled = CoffeeScript.compile task.input, task.options
224      task.output = compiled
225      if opts.map
226        task.output = compiled.js
227        task.sourceMap = compiled.v3SourceMap
228
229      CoffeeScript.emit 'success', task
230      if opts.print
231        printLine task.output.trim()
232      else if opts.compile or opts.map
233        saveTo = if opts.outputFilename and sources.length is 1
234          path.join opts.outputPath, opts.outputFilename
235        else
236          options.jsPath
237        writeJs base, task.file, task.output, saveTo, task.sourceMap
238  catch err
239    CoffeeScript.emit 'failure', err, task
240    return if CoffeeScript.listeners('failure').length
241    message = err?.stack or "#{err}"
242    if opts.watch
243      printLine message + '\x07'
244    else
245      printWarn message
246      process.exit 1
247
248# Attach the appropriate listeners to compile scripts incoming over **stdin**,
249# and write them back to **stdout**.
250compileStdio = ->
251  if opts.map
252    console.error '--stdio and --map cannot be used together'
253    process.exit 1
254  buffers = []
255  stdin = process.openStdin()
256  stdin.on 'data', (buffer) ->
257    buffers.push buffer if buffer
258  stdin.on 'end', ->
259    compileScript null, Buffer.concat(buffers).toString()
260
261# If all of the source files are done being read, concatenate and compile
262# them together.
263joinTimeout = null
264compileJoin = ->
265  return unless opts.join
266  unless sourceCode.some((code) -> code is null)
267    clearTimeout joinTimeout
268    joinTimeout = wait 100, ->
269      compileScript opts.join, sourceCode.join('\n'), opts.join
270
271# Watch a source CoffeeScript file using `fs.watch`, recompiling it every
272# time the file is updated. May be used in combination with other options,
273# such as `--print`.
274watch = (source, base) ->
275  watcher        = null
276  prevStats      = null
277  compileTimeout = null
278
279  watchErr = (err) ->
280    throw err unless err.code is 'ENOENT'
281    return unless source in sources
282    try
283      rewatch()
284      compile()
285    catch
286      removeSource source, base
287      compileJoin()
288
289  compile = ->
290    clearTimeout compileTimeout
291    compileTimeout = wait 25, ->
292      fs.stat source, (err, stats) ->
293        return watchErr err if err
294        return rewatch() if prevStats and
295                            stats.size is prevStats.size and
296                            stats.mtime.getTime() is prevStats.mtime.getTime()
297        prevStats = stats
298        fs.readFile source, (err, code) ->
299          return watchErr err if err
300          compileScript(source, code.toString(), base)
301          rewatch()
302
303  startWatcher = ->
304    watcher = fs.watch source
305    .on 'change', compile
306    .on 'error', (err) ->
307      throw err unless err.code is 'EPERM'
308      removeSource source, base
309
310  rewatch = ->
311    watcher?.close()
312    startWatcher()
313
314  try
315    startWatcher()
316  catch err
317    watchErr err
318
319# Watch a directory of files for new additions.
320watchDir = (source, base) ->
321  watcher        = null
322  readdirTimeout = null
323
324  startWatcher = ->
325    watcher = fs.watch source
326    .on 'error', (err) ->
327      throw err unless err.code is 'EPERM'
328      stopWatcher()
329    .on 'change', ->
330      clearTimeout readdirTimeout
331      readdirTimeout = wait 25, ->
332        try
333          files = fs.readdirSync source
334        catch err
335          throw err unless err.code is 'ENOENT'
336          return stopWatcher()
337        for file in files
338          compilePath (path.join source, file), no, base
339
340  stopWatcher = ->
341    watcher.close()
342    removeSourceDir source, base
343
344  watchedDirs[source] = yes
345  try
346    startWatcher()
347  catch err
348    throw err unless err.code is 'ENOENT'
349
350removeSourceDir = (source, base) ->
351  delete watchedDirs[source]
352  sourcesChanged = no
353  for file in sources when source is path.dirname file
354    removeSource file, base
355    sourcesChanged = yes
356  compileJoin() if sourcesChanged
357
358# Remove a file from our source list, and source code cache. Optionally remove
359# the compiled JS version as well.
360removeSource = (source, base) ->
361  index = sources.indexOf source
362  sources.splice index, 1
363  sourceCode.splice index, 1
364  unless opts.join
365    silentUnlink outputPath source, base
366    silentUnlink outputPath source, base, '.js.map'
367    timeLog "removed #{source}"
368
369silentUnlink = (path) ->
370  try
371    fs.unlinkSync path
372  catch err
373    throw err unless err.code in ['ENOENT', 'EPERM']
374
375# Get the corresponding output JavaScript path for a source file.
376outputPath = (source, base, extension=".js") ->
377  basename  = helpers.baseFileName source, yes, useWinPathSep
378  srcDir    = path.dirname source
379  dir = unless opts.outputPath
380    srcDir
381  else if source is base
382    opts.outputPath
383  else
384    path.join opts.outputPath, path.relative base, srcDir
385  path.join dir, basename + extension
386
387# Recursively mkdir, like `mkdir -p`.
388mkdirp = (dir, fn) ->
389  mode = 0o777 & ~process.umask()
390
391  do mkdirs = (p = dir, fn) ->
392    fs.exists p, (exists) ->
393      if exists
394        fn()
395      else
396        mkdirs path.dirname(p), ->
397          fs.mkdir p, mode, (err) ->
398            return fn err if err
399            fn()
400
401# Write out a JavaScript source file with the compiled code. By default, files
402# are written out in `cwd` as `.js` files with the same name, but the output
403# directory can be customized with `--output`.
404#
405# If `generatedSourceMap` is provided, this will write a `.js.map` file into the
406# same directory as the `.js` file.
407writeJs = (base, sourcePath, js, jsPath, generatedSourceMap = null) ->
408  sourceMapPath = "#{jsPath}.map"
409  jsDir  = path.dirname jsPath
410  compile = ->
411    if opts.compile
412      js = ' ' if js.length <= 0
413      if generatedSourceMap then js = "#{js}\n//# sourceMappingURL=#{helpers.baseFileName sourceMapPath, no, useWinPathSep}\n"
414      fs.writeFile jsPath, js, (err) ->
415        if err
416          printLine err.message
417          process.exit 1
418        else if opts.compile and opts.watch
419          timeLog "compiled #{sourcePath}"
420    if generatedSourceMap
421      fs.writeFile sourceMapPath, generatedSourceMap, (err) ->
422        if err
423          printLine "Could not write source map: #{err.message}"
424          process.exit 1
425  fs.exists jsDir, (itExists) ->
426    if itExists then compile() else mkdirp jsDir, compile
427
428# Convenience for cleaner setTimeouts.
429wait = (milliseconds, func) -> setTimeout func, milliseconds
430
431# When watching scripts, it's useful to log changes with the timestamp.
432timeLog = (message) ->
433  console.log "#{(new Date).toLocaleTimeString()} - #{message}"
434
435# Pretty-print a stream of tokens, sans location data.
436printTokens = (tokens) ->
437  strings = for token in tokens
438    tag = token[0]
439    value = token[1].toString().replace(/\n/, '\\n')
440    "[#{tag} #{value}]"
441  printLine strings.join(' ')
442
443# Use the [OptionParser module](optparse.html) to extract all options from
444# `process.argv` that are specified in `SWITCHES`.
445parseOptions = ->
446  o = opts      = optionParser.parse process.argv[2..]
447  o.compile     or=  !!o.output
448  o.run         = not (o.compile or o.print or o.map)
449  o.print       = !!  (o.print or (o.eval or o.stdio and o.compile))
450
451# The compile-time options to pass to the CoffeeScript compiler.
452compileOptions = (filename, base) ->
453  if opts.transpile
454    # The user has requested that the CoffeeScript compiler also transpile
455    # via Babel. We don’t include Babel as a dependency because we want to
456    # avoid dependencies in general, and most users probably won’t be relying
457    # on us to transpile for them; we assume most users will probably either
458    # run CoffeeScript’s output without transpilation (modern Node or evergreen
459    # browsers) or use a proper build chain like Gulp or Webpack.
460    try
461      require '@babel/core'
462    catch
463      try
464        require 'babel-core'
465      catch
466        # Give appropriate instructions depending on whether `coffee` was run
467        # locally or globally.
468        if require.resolve('.').indexOf(process.cwd()) is 0
469          console.error '''
470            To use --transpile, you must have @babel/core installed:
471              npm install --save-dev @babel/core
472            And you must save options to configure Babel in one of the places it looks to find its options.
473            See https://coffeescript.org/#transpilation
474          '''
475        else
476          console.error '''
477            To use --transpile with globally-installed CoffeeScript, you must have @babel/core installed globally:
478              npm install --global @babel/core
479            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.
480            See https://coffeescript.org/#transpilation
481          '''
482        process.exit 1
483
484    opts.transpile = {} unless typeof opts.transpile is 'object'
485
486    # Pass a reference to Babel into the compiler, so that the transpile option
487    # is available for the CLI. We need to do this so that tools like Webpack
488    # can `require('coffeescript')` and build correctly, without trying to
489    # require Babel.
490    opts.transpile.transpile = CoffeeScript.transpile
491
492    # Babel searches for its options (a `.babelrc` file, a `.babelrc.js` file,
493    # a `package.json` file with a `babel` key, etc.) relative to the path
494    # given to it in its `filename` option. Make sure we have a path to pass
495    # along.
496    unless opts.transpile.filename
497      opts.transpile.filename = filename or path.resolve(base or process.cwd(), '<anonymous>')
498  else
499    opts.transpile = no
500
501  answer =
502    filename: filename
503    literate: opts.literate or helpers.isLiterate(filename)
504    bare: opts.bare
505    header: opts.compile and not opts['no-header']
506    transpile: opts.transpile
507    sourceMap: opts.map
508    inlineMap: opts['inline-map']
509    ast: opts.ast
510
511  if filename
512    if base
513      cwd = process.cwd()
514      jsPath = outputPath filename, base
515      jsDir = path.dirname jsPath
516      answer = helpers.merge answer, {
517        jsPath
518        sourceRoot: path.relative jsDir, cwd
519        sourceFiles: [path.relative cwd, filename]
520        generatedFile: helpers.baseFileName(jsPath, no, useWinPathSep)
521      }
522    else
523      answer = helpers.merge answer,
524        sourceRoot: ""
525        sourceFiles: [helpers.baseFileName filename, no, useWinPathSep]
526        generatedFile: helpers.baseFileName(filename, yes, useWinPathSep) + ".js"
527  answer
528
529# Start up a new Node.js instance with the arguments in `--nodejs` passed to
530# the `node` binary, preserving the other options.
531forkNode = ->
532  nodeArgs = opts.nodejs.split /\s+/
533  args     = process.argv[1..]
534  args.splice args.indexOf('--nodejs'), 2
535  p = spawn process.execPath, nodeArgs.concat(args),
536    cwd:        process.cwd()
537    env:        process.env
538    stdio:      [0, 1, 2]
539  for signal in ['SIGINT', 'SIGTERM']
540    process.on signal, do (signal) ->
541      -> p.kill signal
542  p.on 'exit', (code) -> process.exit code
543
544# Print the `--help` usage message and exit. Deprecated switches are not
545# shown.
546usage = ->
547  printLine optionParser.help()
548
549# Print the `--version` message and exit.
550version = ->
551  printLine "CoffeeScript version #{CoffeeScript.VERSION}"