1 | fs = require 'fs'
|
2 | path = 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'
|
8 | CoffeeScript = require './module'
|
9 | Repl = require './repl'
|
10 | cscodegen = try require 'cscodegen'
|
11 | escodegen = try require 'escodegen'
|
12 | esmangle = try require 'esmangle'
|
13 |
|
14 | inspect = (o) -> (require 'util').inspect o, no, 9e9, yes
|
15 |
|
16 |
|
17 | args = process.argv[1 + (process.argv[0] is 'node') ..]
|
18 |
|
19 |
|
20 | additionalArgs = []
|
21 | if '--' in args then additionalArgs = (args.splice (args.indexOf '--'), 9e9)[1..]
|
22 |
|
23 |
|
24 |
|
25 | options = {}
|
26 | optionMap = {}
|
27 |
|
28 | optionArguments = [
|
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 |
|
38 | parameterArguments = [
|
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 |
|
46 | if 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 |
|
61 | if cscodegen?
|
62 | optionArguments.push [['cscodegen', 'f'], off, 'output cscodegen-generated CoffeeScript code']
|
63 |
|
64 |
|
65 | shortOptionArguments = []
|
66 | longOptionArguments = []
|
67 | for 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 |
|
74 | shortParameterArguments = []
|
75 | longParameterArguments = []
|
76 | for 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 |
|
84 | reShortOptions = ///^ - (#{shortOptionArguments.join '|'})+ $///
|
85 | reLongOption = ///^ -- (no-)? (#{longOptionArguments.join '|'}) $///
|
86 | reShortParameter = ///^ - (#{shortParameterArguments.join '|'}) $///
|
87 | reLongParameter = ///^ -- (#{longParameterArguments.join '|'}) $///
|
88 | reShortOptionsShortParameter = ///
|
89 | ^ - (#{shortOptionArguments.join '|'})+
|
90 | (#{shortParameterArguments.join '|'}) $
|
91 | ///
|
92 |
|
93 |
|
94 |
|
95 | positionalArgs = []
|
96 | while 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 |
|
115 |
|
116 | positionalArgs = positionalArgs.concat additionalArgs
|
117 | unless 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 |
|
128 |
|
129 | if 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 |
|
134 | if 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 |
|
139 |
|
140 | if options.require? and not options.eval
|
141 | console.error 'Error: --require (-I) depends on --eval (-e)'
|
142 | process.exit 1
|
143 |
|
144 |
|
145 | if 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 |
|
150 | if 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 |
|
155 | if options['source-map-file'] and not options.js
|
156 | console.error 'Error: --source-map-file depends on --js'
|
157 | process.exit 1
|
158 |
|
159 |
|
160 | if 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 |
|
165 | if options.cscodegen and not cscodegen?
|
166 | console.error 'Error: cscodegen must be installed to use --cscodegen'
|
167 | process.exit 1
|
168 |
|
169 |
|
170 | output = (out) ->
|
171 |
|
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 |
|
180 | if 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 |
|
237 | else if options.version
|
238 | pkg = require './../../package.json'
|
239 | console.log "CoffeeScript version #{pkg.version}"
|
240 |
|
241 | else if options.repl
|
242 | do Repl.start
|
243 |
|
244 | else
|
245 |
|
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 |
|
260 | if 0xFEFF is input.charCodeAt 0 then input = input[1..]
|
261 |
|
262 |
|
263 | if options.debug
|
264 | try
|
265 | console.error '### PREPROCESSED CS ###'
|
266 | console.error numberLines humanReadable Preprocessor.processSync input
|
267 |
|
268 |
|
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 |
|
282 | if options.optimise and result?
|
283 | result = Optimiser.optimise result
|
284 |
|
285 |
|
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 |
|
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 |
|
310 | jsAST = CoffeeScript.compile result, bare: options.bare
|
311 |
|
312 |
|
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 |
|
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 |
|
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 |
|
339 | if sourceMap?
|
340 | output "#{sourceMap}"
|
341 | return
|
342 | else
|
343 | process.exit 1
|
344 |
|
345 |
|
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 |
|
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 |
|
367 | if options.eval
|
368 | runMain input, js, jsAST, inputSource
|
369 | return
|
370 |
|
371 |
|
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
|
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
|