UNPKG

10.5 kBtext/coffeescriptView Raw
1_ = require 'underscore'
2logging = require './logging'
3pathLib = require 'path'
4
5String::isHTTP = ->
6 @substring(0, 7) == 'http://' or @substring(0, 8) == 'https://'
7
8String::isScript = ->
9 this.substring(this.length-2) is 'js' or this.substring(this.length-6) is 'coffee'
10
11###
12Load any methods or libraries that interact with the outside
13 system. In the tests, these should all be mocked out via stubble
14###
15coffee = require 'coffee-script'
16exec = require('child_process').exec
17flow = require 'flow'
18fs = require 'fs'
19restler = require 'restler'
20readDir = require './readdir'
21uglify = require 'uglify-js'
22yaml = require 'pyyaml'
23
24
25# Default configs values
26defaults =
27 depends: []
28 test_depends: []
29 sources: []
30 minify: false
31 include_depends: false
32 depends_folder: 'requires'
33 test_build_source_file: 'auto-source.js'
34 test_build_test_file: 'auto-test.js'
35 spec_folder: 'specs'
36 source_folder: 'src'
37 build_output: 'output.js'
38
39###
40@description Represents an instance of a Package, which is the basic
41 unit in Jspackle. All tasks are executed against a single package.
42 Provides public methods to execute the basic tasks that Jspackle
43 allows on itself.
44
45@name Package
46@class
47###
48class Package
49
50 exitCode: 0
51
52 exit: (code)->
53 @exitCode = code
54
55 ###
56 ###
57 complete: ->
58 process.nextTick =>
59 process.exit @exitCode
60
61 ###
62 @description Use the settings provided in ``opts`` (command line options
63 for the entire Jspackle process) as well as the settings in ``cmd``
64 (command line options specific for the sub-command that is going to be
65 executed to define the ``@opts`` for this Package instance.
66
67 Extend objects in reverse precedence: overwrite previously set
68 properties, giving the last item the highest precedence.
69
70 1. Default settings (Lowest precedence)
71 2. Config file settings
72 3. Commandline options
73 4. Command specific command-line options (Highest precedence)
74
75 @param {Object} opts Commandline options for jspackle
76 @param {Object} cmd Commandline options specific to jspackle subcommand
77 @returns {void}
78
79 @public
80 @function
81 @constructor
82 @memberOf Package.prototype
83 ###
84 constructor: (opts={}, cmd={})->
85 configs = @loadConfigs opts
86 @opts = []
87 _.extend @opts, defaults, configs, opts, cmd
88
89
90 ###
91 @description Using the base options provided to Jspackle, load the
92 Jspackle config file that defines this package.
93
94 @param {Object} opts Commandline options for jspackle
95 @returns {Object} Config files parsed from the JSON config-file
96
97 @public
98 @function
99 @memberOf Package.prototype
100 ###
101 loadConfigs: (opts)->
102 path = opts.root+opts.path
103 logging.debug "Parsing jspackle file: #{path}"
104 try
105 JSON.parse fs.readFileSync path
106 catch e
107 @error "ERROR opening config file '#{path}'"
108
109 ###
110 @description Defines the standard behavior of when an error is
111 encountered. Logs the error and ends the process with an error
112 code.
113
114 @param {mixed} e Error. Regardless of type, it is passed to the
115 logging module where it co-erced to a string.
116 @param {integer} code Error code to exit this process with. Defaults
117 to 1 (standard error).
118 @returns {void}
119
120 @public
121 @function
122 @memberOf Package.prototype
123 ###
124 error: (e, code=1)->
125 logging.warn e
126 @exit code
127
128 build: ->
129 _this = this
130 sources = []
131
132 # Asyncronously read sources into memory and cache them
133 # the sources object. Register it as an async multi-step
134 # flow command
135 loadSources = ->
136 flow = this
137 sources = []
138 if _this.opts.include_depends
139 sources = sources.concat _this.depends
140 sources = sources.concat _this.sources
141 for index, src of sources
142
143 # Execute in a closure so that i is local to this
144 # loop, so that it doesn't change by the time our
145 # callback is executed.
146 do ->
147 i = index
148 registered = flow.MULTI()
149
150 if src.isHTTP()
151 _this.httpGet src, (script)->
152 sources[i] = script
153 registered()
154
155 else
156
157 # Read the file, cache the source, and mark this portion of
158 # the multi-step as complete
159 fs.readFile _this.opts.root+src, (err, script)->
160 return _this.error err if err
161 sources[i] = script
162 registered()
163
164
165 # Once all our registered multi-steps have completed, join
166 # the ordered sources with new lines and write the output
167 # to our build file.
168 processSources = ->
169 outputFile = _this._generateOutputPath()
170 logging.info "Found #{sources.length} source file"
171 logging.info "Writing processed sources to: '#{outputFile}'"
172 output = sources.join "\n"
173 if _this.opts.minify
174 output = _this.minify output
175 fs.writeFile outputFile, output, this
176
177 # End the program, returning the correct error code based on if
178 # writing finished or not.
179 finish = (err)->
180 _this.exit if err then 1 else 0
181
182 complete = ->
183 _this.complete()
184
185 flow.exec loadSources, processSources, finish, complete
186
187 ###
188 @description The ``test`` task. Create test config file, execute
189 tests, and clean up after itself.
190
191 @returns {void}
192
193 @public
194 @function
195 @memberOf Package.prototype
196 ###
197 test: ->
198 cancel = false
199 _this = this
200
201 # Create the test driver conf file
202 createFile = ->
203 _this._createJsTestDriverFile this
204
205 # Execute tests
206 execute = (err)->
207 cancel = err
208 if cancel
209 _this.exit 1
210 return this()
211 _this._executeTests this
212
213 # Clean up files that were created along the way
214 clean = (err)->
215 flow = this
216 cancel = cancel or err
217 _this.clean flow
218 _this.exit if cancel then (parseInt(cancel, 10) or 1) else 0
219
220 complete = ->
221 _this.complete()
222
223 flow.exec createFile, execute, clean, complete
224
225 ###
226 @description Cleans up after a task. Unlinks any temporary files that
227 were created as part of its process.
228
229 @returns {void}
230
231 @public
232 @function
233 @memberOf Package.prototype
234 ###
235 clean: (flow)->
236 logging.info "Cleaning up after jspackle run..."
237 fs.unlink "#{@opts.root}JsTestDriver.conf", flow.MULTI()
238 fs.unlink @opts.test_build_source_file, flow.MULTI()
239 fs.unlink @opts.test_build_test_file, flow.MULTI()
240
241 ###
242 @description Minifies the source provided to it.
243
244 @params {String} source JavaScript source to be minified
245 @returns {String} Minified JavaScript source
246
247 @public
248 @function
249 @memberOf Package.prototype
250 ###
251 minify: (source)->
252 logging.info "Minifying JavaScript source..."
253 tokens = uglify.parser.parse source
254 tokens = uglify.uglify.ast_mangle tokens
255 tokens = uglify.uglify.ast_squeeze tokens
256 uglify.uglify.gen_code tokens
257
258 ###
259 @description Gets the given URL, and passes the response to
260 the callback function. If an error occurs, the process exits
261 with code 1
262
263 @params {String} url URL of the get request
264 @params {Function} callback Callback function to be executed on
265 complete
266
267 @public
268 @function
269 @memberOf Package.prototype
270 ###
271 httpGet: (url, callback)->
272 resp = restler.get url
273 resp.on 'complete', callback
274 resp.on 'error', => @error "ERROR: Cannot get #{url}"
275
276 ### ------ Private Methods ------- ###
277
278 _executeTests: (callback)->
279 logging.debug "Executing tests: #{@testCmd}"
280 exec @testCmd, (err, stdout, stderr)->
281 msg = """
282
283Output:
284
285#{stdout}
286"""
287 if err
288 logging.warn msg
289 code = err.code
290 else
291 logging.info msg
292 code = 0
293 callback code
294
295 _createJsTestDriverFile: (callback)->
296 configs =
297 server: @opts.test_server
298 timeout: @opts.test_timeout
299
300 configs.load = @depends.concat(@testDepends).concat(@sources)
301 configs.test = @tests
302
303 logging.info "Executing #{configs.test.length} specs"
304
305 path = "#{@opts.root}JsTestDriver.conf"
306 logging.debug "Dumping configs to: #{path}"
307 yaml.dump configs, path, callback
308
309 _coffeeCompile: (sources, path)->
310 logging.info "Compiling coffee-script to '#{path}'"
311 compiled = []
312 paths = []
313 for src in sources
314 if src.isHTTP()
315 paths.push src
316 else
317 try
318 compiled.push coffee.compile fs.readFileSync(src).toString()
319 catch e
320 logging.critical "Cannot pase #{src} as valid CoffeeScript!"
321 logging.critical e
322 throw e
323 fs.writeFileSync path, compiled.join "\n"
324 paths.push path
325 return paths
326
327 _findTests: ->
328 found = readDir @opts.root+@opts.spec_folder
329 tests = []
330 for file in found.files
331 if file.isScript()
332 logging.debug "Discovered test: '#{file}'"
333 tests.push(file.replace(@opts.root+@opts.spec_folder+'/', ''))
334 tests
335
336 ###
337 Generate the output path from our options, and template variables
338 ###
339 _generateOutputPath: ->
340 filePath = pathLib.join @opts.root, @opts.build_output
341 #console.log @opts
342 for variable in ['name', 'version']
343 re = new RegExp "{{#{variable}}}", 'g'
344 filePath = filePath.replace re, @opts[variable]
345 return filePath
346
347 _process: (option, folder, compile=false)->
348 root = folder+'/'
349 sources = []
350 if typeof option == 'string'
351 paths = @opts[option]
352 else
353 paths = option
354
355 for path in paths
356 if path.isHTTP()
357 sources.push path
358 else
359 sources.push root+path
360 return sources if not (compile and @opts.coffee)
361 @_coffeeCompile sources, compile
362
363###
364Read only properties
365--------------------
366
367Defines read-only properties on the Package object using v8's
368``__defineGetter__`` syntax. These properties expand arguments that
369are provided as part of the configuration into fleshed out properties.
370###
371Package.prototype.__defineGetter__ 'sources', ->
372 @_process 'sources', @opts.source_folder, @opts.test_build_source_file
373
374Package.prototype.__defineGetter__ 'depends', ->
375 @_process 'depends', @opts.depends_folder
376
377Package.prototype.__defineGetter__ 'testDepends', ->
378 @_process 'test_depends', @opts.depends_folder
379
380Package.prototype.__defineGetter__ 'tests', ->
381 @_process @_findTests(), @opts.spec_folder, @opts.test_build_test_file
382
383Package.prototype.__defineGetter__ 'testCmd', ->
384 "js-test-driver --config ./JsTestDriver.conf --tests all --reset #{@opts.test_args}"
385
386module.exports = Package