UNPKG

12.6 kBtext/coffeescriptView Raw
1fs = require 'fs'
2path = require 'path'
3{concat, foldl} = require './functional-helpers'
4{numberLines, humanReadable} = require './helpers'
5{Preprocessor} = require './preprocessor'
6{Optimiser} = require './optimiser'
7{runMain} = require './run'
8CoffeeScript = require './module'
9Repl = require './repl'
10cscodegen = try require 'cscodegen'
11escodegen = try require 'escodegen'
12esmangle = try require 'esmangle'
13
14inspect = (o) -> (require 'util').inspect o, no, 9e9, yes
15
16# clone args
17args = process.argv[1 + (process.argv[0] is 'node') ..]
18
19# ignore args after --
20additionalArgs = []
21if '--' in args then additionalArgs = (args.splice (args.indexOf '--'), 9e9)[1..]
22
23
24# initialise options
25options = {}
26optionMap = {}
27
28optionArguments = [
29 [['parse', 'p'], off, 'output a JSON-serialised AST representation of the input']
30 [['compile', 'c'], off, 'output a JSON-serialised AST representation of the output']
31 [['optimise' ], on, 'enable optimisations (default: on)']
32 [['debug' ], off, 'output intermediate representations on stderr for debug']
33 [['raw' ], off, 'preserve source position and raw parse information']
34 [['version', 'v'], off, 'display the version number']
35 [['help' ], off, 'display this help message']
36]
37
38parameterArguments = [
39 [['cli' ], 'INPUT', 'pass a string from the command line as input']
40 [['input', 'i'], 'FILE' , 'file to be used as input instead of STDIN']
41 [['nodejs' ], 'OPTS' , 'pass options through to the node binary']
42 [['output', 'o'], 'FILE' , 'file to be used as output instead of STDIN']
43 [['watch', 'w'], 'FILE' , 'watch the given file/directory for changes']
44]
45
46if escodegen?
47 [].push.apply optionArguments, [
48 [['bare', 'b'], off, 'omit the top-level function wrapper']
49 [['js', 'j'], off, 'generate JavaScript output']
50 [['source-map' ], off, 'generate source map']
51 [['eval', 'e'], off, 'evaluate compiled JavaScript']
52 [['repl' ], off, 'run an interactive CoffeeScript REPL']
53 ]
54 [].push.apply parameterArguments, [
55 [['source-map-file'], 'FILE' , 'file used as output for source map when using --js']
56 [['require', 'I'], 'FILE' , 'require a library before a script is executed']
57 ]
58 if esmangle?
59 optionArguments.push [['minify', 'm'], off, 'run compiled javascript output through a JS minifier']
60
61if cscodegen?
62 optionArguments.push [['cscodegen', 'f'], off, 'output cscodegen-generated CoffeeScript code']
63
64
65shortOptionArguments = []
66longOptionArguments = []
67for opts in optionArguments
68 options[opts[0][0]] = opts[1]
69 for o in opts[0]
70 optionMap[o] = opts[0][0]
71 if o.length is 1 then shortOptionArguments.push o
72 else if o.length > 1 then longOptionArguments.push o
73
74shortParameterArguments = []
75longParameterArguments = []
76for opts in parameterArguments
77 for o in opts[0]
78 optionMap[o] = opts[0][0]
79 if o.length is 1 then shortParameterArguments.push o
80 else if o.length > 1 then longParameterArguments.push o
81
82
83# define some regexps that match our arguments
84reShortOptions = ///^ - (#{shortOptionArguments.join '|'})+ $///
85reLongOption = ///^ -- (no-)? (#{longOptionArguments.join '|'}) $///
86reShortParameter = ///^ - (#{shortParameterArguments.join '|'}) $///
87reLongParameter = ///^ -- (#{longParameterArguments.join '|'}) $///
88reShortOptionsShortParameter = ///
89 ^ - (#{shortOptionArguments.join '|'})+
90 (#{shortParameterArguments.join '|'}) $
91///
92
93
94# parse arguments
95positionalArgs = []
96while args.length
97 arg = args.shift()
98 if reShortOptionsShortParameter.exec arg
99 args.unshift "-#{arg[1...-1]}", "-#{arg[-1..]}"
100 else if reShortOptions.exec arg
101 for o in arg[1..].split ''
102 options[optionMap[o]] = on
103 else if match = reLongOption.exec arg
104 options[optionMap[match[2]]] = if match[1]? then off else on
105 else if match = (reShortParameter.exec arg) ? reLongParameter.exec arg
106 options[optionMap[match[1]]] = args.shift()
107 else if match = /^(-.|--.*)$/.exec arg
108 console.error "Unrecognised option '#{match[0].replace /'/g, '\\\''}'"
109 process.exit 1
110 else
111 positionalArgs.push arg
112
113
114# input validation
115
116positionalArgs = positionalArgs.concat additionalArgs
117unless options.compile or options.js or options['source-map'] or options.parse or options.eval or options.cscodegen
118 if not escodegen?
119 options.compile = on
120 else if positionalArgs.length
121 options.eval = on
122 options.input = positionalArgs.shift()
123 additionalArgs = positionalArgs
124 else
125 options.repl = on
126
127# mutual exclusions
128# - p (parse), c (compile), j (js), source-map, e (eval), cscodegen, repl
129if 1 isnt options.parse + options.compile + (options.js ? 0) + (options['source-map'] ? 0) + (options.eval ? 0) + (options.cscodegen ? 0) + (options.repl ? 0)
130 console.error "Error: At most one of --parse (-p), --compile (-c), --js (-j), --source-map, --eval (-e), --cscodegen, or --repl may be used."
131 process.exit 1
132
133# - i (input), w (watch), cli
134if 1 < options.input? + options.watch? + options.cli?
135 console.error 'Error: At most one of --input (-i), --watch (-w), or --cli may be used.'
136 process.exit 1
137
138# dependencies
139# - I (require) depends on e (eval)
140if options.require? and not options.eval
141 console.error 'Error: --require (-I) depends on --eval (-e)'
142 process.exit 1
143
144# - m (minify) depends on escodegen and esmangle and (c (compile) or e (eval))
145if options.minify and not (options.js or options.eval)
146 console.error 'Error: --minify does not make sense without --js or --eval'
147 process.exit 1
148
149# - b (bare) depends on escodegen and (c (compile) or e (eval)
150if options.bare and not (options.compile or options.js or options['source-map'] or options.eval)
151 console.error 'Error: --bare does not make sense without --compile, --js, --source-map, or --eval'
152 process.exit 1
153
154# - source-map-file depends on j (js)
155if options['source-map-file'] and not options.js
156 console.error 'Error: --source-map-file depends on --js'
157 process.exit 1
158
159# - i (input) depends on o (output) when input is a directory
160if options.input? and (fs.statSync options.input).isDirectory() and (not options.output? or (fs.statSync options.output)?.isFile())
161 console.error 'Error: when --input is a directory, --output must be provided, and --output must not reference a file'
162 process.exit 1
163
164# - cscodegen depends on cscodegen
165if options.cscodegen and not cscodegen?
166 console.error 'Error: cscodegen must be installed to use --cscodegen'
167 process.exit 1
168
169
170output = (out) ->
171 # --output
172 if options.output
173 fs.writeFile options.output, "#{out}\n", (err) ->
174 throw err if err?
175 else
176 process.stdout.write "#{out}\n"
177
178
179# start processing options
180if options.help
181 $0 = if process.argv[0] is 'node' then process.argv[1] else process.argv[0]
182 $0 = path.basename $0
183 maxWidth = 85
184
185 wrap = (lhsWidth, input) ->
186 rhsWidth = maxWidth - lhsWidth
187 pad = (Array lhsWidth + 4 + 1).join ' '
188 rows = while input.length
189 row = input[...rhsWidth]
190 input = input[rhsWidth..]
191 row
192 rows.join "\n#{pad}"
193
194 formatOptions = (opts) ->
195 opts = for opt in opts when opt.length
196 if opt.length is 1 then "-#{opt}" else "--#{opt}"
197 opts.sort (a, b) -> a.length - b.length
198 opts.join ', '
199
200 console.log "
201 Usage: (OPT is interpreted by #{$0}, ARG is passed to FILE)
202
203 #{$0} OPT* -{p,c,j,f} OPT*
204 example: #{$0} --js --no-optimise <input.coffee >output.js
205 #{$0} [-e] FILE {OPT,ARG}* [-- ARG*]
206 example: #{$0} myfile.coffee arg0 arg1
207 #{$0} OPT* [--repl] OPT*
208 example: #{$0}
209 "
210
211 optionRows = for opt in optionArguments
212 [(formatOptions opt[0]), opt[2]]
213 parameterRows = for opt in parameterArguments
214 ["#{formatOptions opt[0]} #{opt[1]}", opt[2]]
215 leftColumnWidth = foldl 0, [optionRows..., parameterRows...], (memo, opt) ->
216 Math.max memo, opt[0].length
217
218 rows = [optionRows..., parameterRows...]
219 rows.sort (a, b) ->
220 a = a[0]; b = b[0]
221 if a[0..1] is '--' and b[0..1] isnt '--' then return 1
222 if b[0..1] is '--' and a[0..1] isnt '--' then return -1
223 if a.toLowerCase() < b.toLowerCase() then -1 else 1
224 for row in rows
225 console.log " #{row[0]}#{(Array leftColumnWidth - row[0].length + 1).join ' '} #{wrap leftColumnWidth, row[1]}"
226
227 console.log "
228 Unless given --input or --cli flags, `#{$0}` will operate on stdin/stdout.
229 When none of --{parse,compile,js,source-map,eval,cscodegen,repl} are given,
230 If positional arguments were given
231 * --eval is implied
232 * the first positional argument is used as an input filename
233 * additional positional arguments are passed as arguments to the script
234 Else --repl is implied
235 "
236
237else if options.version
238 pkg = require './../../package.json'
239 console.log "CoffeeScript version #{pkg.version}"
240
241else if options.repl
242 do Repl.start
243
244else
245 # normal workflow
246
247 input = ''
248 inputName = options.input ? (options.cli and 'cli' or 'stdin')
249 inputSource =
250 if options.input? then fs.realpathSync options.input
251 else options.cli and '(cli)' or '(stdin)'
252
253 processInput = (err) ->
254
255 throw err if err?
256 result = null
257
258 input = input.toString()
259 # strip UTF BOM
260 if 0xFEFF is input.charCodeAt 0 then input = input[1..]
261
262 # preprocess
263 if options.debug
264 try
265 console.error '### PREPROCESSED CS ###'
266 console.error numberLines humanReadable Preprocessor.processSync input
267
268 # parse
269 try
270 result = CoffeeScript.parse input,
271 optimise: no
272 raw: options.raw or options['source-map'] or options['source-map-file'] or options.eval
273 inputSource: inputSource
274 catch e
275 console.error e.message
276 process.exit 1
277 if options.debug and options.optimise and result?
278 console.error '### PARSED CS-AST ###'
279 console.error inspect result.toJSON()
280
281 # optimise
282 if options.optimise and result?
283 result = Optimiser.optimise result
284
285 # --parse
286 if options.parse
287 if result?
288 output inspect result.toJSON()
289 return
290 else
291 process.exit 1
292
293 if options.debug and result?
294 console.error "### #{if options.optimise then 'OPTIMISED' else 'PARSED'} CS-AST ###"
295 console.error inspect result.toJSON()
296
297 # cs code gen
298 if options.cscodegen
299 try result = cscodegen.generate result
300 catch e
301 console.error (e.stack or e.message)
302 process.exit 1
303 if result?
304 output result
305 return
306 else
307 process.exit 1
308
309 # compile
310 jsAST = CoffeeScript.compile result, bare: options.bare
311
312 # --compile
313 if options.compile
314 if jsAST?
315 output inspect jsAST.toJSON()
316 return
317 else
318 process.exit 1
319
320 if options.debug and jsAST?
321 console.error "### COMPILED JS-AST ###"
322 console.error inspect jsAST.toJSON()
323
324 # minification
325 if options.minify
326 try
327 jsAST = esmangle.mangle (esmangle.optimize jsAST.toJSON()), destructive: yes
328 catch e
329 console.error (e.stack or e.message)
330 process.exit 1
331
332 if options['source-map']
333 # source map generation
334 try sourceMap = CoffeeScript.sourceMap jsAST, inputName, compact: options.minify
335 catch e
336 console.error (e.stack or e.message)
337 process.exit 1
338 # --source-map
339 if sourceMap?
340 output "#{sourceMap}"
341 return
342 else
343 process.exit 1
344
345 # js code gen
346 try
347 {code: js, map: sourceMap} = CoffeeScript.jsWithSourceMap jsAST, inputName, compact: options.minify
348 catch e
349 console.error (e.stack or e.message)
350 process.exit 1
351
352 # --js
353 if options.js
354 if options['source-map-file']
355 fs.writeFileSync options['source-map-file'], "#{sourceMap}"
356 js = """
357 #{js}
358
359 /*
360 //@ sourceMappingURL=#{options['source-map-file']}
361 */
362 """
363 output js
364 return
365
366 # --eval
367 if options.eval
368 runMain input, js, jsAST, inputSource
369 return
370
371 # choose input source
372
373 if options.input?
374 fs.stat options.input, (err, stats) ->
375 throw err if err?
376 if stats.isDirectory()
377 options.input = path.join options.input, 'index.coffee'
378 fs.readFile options.input, (err, contents) ->
379 throw err if err?
380 input = contents
381 do processInput
382 else if options.watch?
383 options.watch # TODO: watch
384 else if options.cli?
385 input = options.cli
386 do processInput
387 else
388 process.stdin.on 'data', (data) -> input += data
389 process.stdin.on 'end', processInput
390 process.stdin.setEncoding 'utf8'
391 do process.stdin.resume