/src/command.coffee

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