@@ -19,11 +19,12 @@ var through = require('through2'),
1919 */
2020function init ( ) {
2121
22- // share a common cache by default
22+ // share a common cache
2323 var caches = {
2424 cache : { } ,
2525 packageCache : { }
2626 } ;
27+ var internalCache = { } ;
2728
2829 // complete
2930 return {
@@ -37,15 +38,14 @@ function init() {
3738 */
3839 function create ( opt ) {
3940
40- // ensure options
41- // browserify must be in debug mode
41+ // user options override defaults but we always use debug mode and our own cache
4242 var options = merge ( {
4343 anonymous : false ,
4444 bowerRelative : false ,
4545 projectRelative : false ,
4646 transforms : [ ] ,
4747 sourceMapBase : null
48- } , caches , opt , {
48+ } , opt , caches , {
4949 debug : true
5050 } ) ;
5151
@@ -127,158 +127,217 @@ function init() {
127127 }
128128 }
129129 }
130- }
131-
132- module . exports = init ;
133130
134- /**
135- * Create an instance of the multiton that closes a fixed set of files
136- * @param files The composition roots for the bundler
137- * @param options A hash of options for both browserify and internal settings
138- * @returns {function } a bundle method
139- */
140- function createInstance ( files , options ) {
131+ /**
132+ * Create an instance of the multiton that closes a fixed set of files
133+ * @param files The composition roots for the bundler
134+ * @param options A hash of options for both browserify and internal settings
135+ * @returns {function } a bundle method
136+ */
137+ function createInstance ( files , options ) {
138+
139+ // browserify must be debug mode
140+ var browserifyOpts = merge ( { } , options ) ;
141+
142+ // create bundler
143+ var bundler = browserify ( browserifyOpts ) ;
144+
145+ // must setup pipeline on every reset
146+ bundler . on ( 'reset' , setupPipeline ) ;
147+ setupPipeline ( ) ;
148+
149+ // transforms with possible options
150+ // transform, [opt], transform, [opt], ...
151+ [ ] . concat ( options . transforms )
152+ . concat ( requireTransform ( options . bowerRelative , options . projectRelative ) )
153+ . forEach ( function eachItem ( item , i , list ) {
154+ if ( typeof item === 'function' ) {
155+ var opts = ( typeof list [ i + 1 ] === 'object' ) ? merge ( { global : true } , list [ i + 1 ] ) : { global : true } ;
156+ bundler . transform ( item , opts ) ;
157+ }
158+ } ) ;
141159
142- // browserify must be debug mode
143- var browserifyOpts = merge ( { } , options ) ;
160+ // require statements
161+ [ ] . concat ( files )
162+ . forEach ( function eachItem ( item ) {
163+ bundler . require ( item , { entry : true } ) ;
164+ } ) ;
144165
145- // create bundler
146- var bundler = browserify ( browserifyOpts ) ;
166+ // create instance
167+ return {
168+ bundle : bundle
169+ } ;
147170
148- // ensure anonymous module paths when we are minifying
149- if ( options . anonymous ) {
150- bundler . pipeline
151- . get ( 'label' )
152- . push ( anonymousLabeler ( ) ) ;
153- }
171+ /**
172+ * Setup the browserify pipeline
173+ */
174+ function setupPipeline ( ) {
154175
155- // transforms with possible options
156- // transform, [opt], transform, [opt], ...
157- [ ] . concat ( options . transforms )
158- . concat ( requireTransform ( options . bowerRelative , options . projectRelative ) )
159- . forEach ( function eachItem ( item , i , list ) {
160- if ( typeof item === 'function' ) {
161- var opts = ( typeof list [ i + 1 ] === 'object' ) ? merge ( { global : true } , list [ i + 1 ] ) : { global : true } ;
162- bundler . transform ( item , opts ) ;
176+ // ensure anonymous module paths when we are minifying
177+ if ( options . anonymous ) {
178+ bundler . pipeline
179+ . get ( 'label' )
180+ . push ( anonymousLabeler ( ) ) ;
163181 }
164- } ) ;
165182
166- // require statements
167- [ ] . concat ( files )
168- . forEach ( function eachItem ( item ) {
169- bundler . require ( item , { entry : true } ) ;
170- } ) ;
183+ // populate the module-deps cache to achieve incremental compile
184+ var deps = bundler . pipeline . get ( 'deps' ) ;
185+ deps . push ( populateCache ( deps . _streams [ 0 ] . cache ) ) ;
186+ // TODO this cache is inexplicably different from options.cache
187+ }
171188
172- // create instance
173- return {
174- bundle : bundle
175- } ;
189+ /**
190+ * Compile any number of files into a bundle
191+ * @param {stream.Through } stream A stream to push files to
192+ * @param {Array.<string>|string } files Any number of files to bundle
193+ * @param {string } bundleName The name for the output file
194+ * @param {function } done Callback for completion
195+ */
196+ function bundle ( stream , bundleName ) {
176197
177- /**
178- * Compile any number of files into a bundle
179- * @param {stream.Through } stream A stream to push files to
180- * @param {Array.<string>|string } files Any number of files to bundle
181- * @param {string } bundleName The name for the output file
182- * @param {function } done Callback for completion
183- */
184- function bundle ( stream , bundleName ) {
198+ // create a promise
199+ var deferred = q . defer ( ) ;
185200
186- // create a promise
187- var deferred = q . defer ( ) ;
201+ // setup
202+ var outPath = path . resolve ( bundleName ) ;
203+ var mapPath = path . basename ( bundleName ) + '.map' ;
204+ var errors = [ ] ;
205+ var timeout ;
188206
189- // setup
190- var outPath = path . resolve ( bundleName ) ;
191- var mapPath = path . basename ( bundleName ) + '.map' ;
192- var errors = [ ] ;
193- var timeout ;
207+ // compile
208+ bundler . bundle ( )
209+ . on ( 'error' , errorHandler )
210+ . pipe ( outputStream ( ) ) ;
194211
195- // compile
196- bundler . bundle ( )
197- . on ( 'error' , errorHandler )
198- . pipe ( outputStream ( ) ) ;
212+ // return the promise
213+ return deferred . promise ;
199214
200- // return the promise
201- return deferred . promise ;
215+ // handle an error in the context of the timeout
216+ function errorHandler ( error ) {
202217
203- // handle an error in the context of the timeout
204- function errorHandler ( error ) {
218+ // parse the error text
219+ var message = parseError ( error . toString ( ) ) ;
205220
206- // parse the error text
207- var message = parseError ( error . toString ( ) ) ;
221+ // add unique
222+ if ( errors . indexOf ( message ) < 0 ) {
223+ errors . push ( message ) ;
224+ }
208225
209- // add unique
210- if ( errors . indexOf ( message ) < 0 ) {
211- errors . push ( message ) ;
226+ // complete overall only once there are no further errors
227+ // ensure idempotent in the case there are some late errors
228+ clearTimeout ( timeout ) ;
229+ timeout = setTimeout ( onTimeout , 100 ) ;
212230 }
213231
214- // complete overall only once there are no further errors
215- // ensure idempotent in the case there are some late errors
216- clearTimeout ( timeout ) ;
217- timeout = setTimeout ( onTimeout , 100 ) ;
218- }
232+ // error has occurred and a delay has passed with no further errors
233+ function onTimeout ( ) {
234+ deferred . reject ( errors ) ; // complete overall
235+ }
219236
220- // error has occurred and a delay has passed with no further errors
221- function onTimeout ( ) {
222- deferred . reject ( errors ) ; // complete overall
223- }
237+ // handle the output of the bundler (as a stream)
238+ function outputStream ( ) {
239+ var code = '' ;
224240
225- // handle the output of the bundler (as a stream)
226- function outputStream ( ) {
227- var code = '' ;
241+ function transform ( buffer , encoding , done ) {
242+ code += buffer . toString ( ) ; // accumulate code
243+ done ( ) ;
244+ }
228245
229- function transform ( buffer , encoding , done ) {
230- code += buffer . toString ( ) ; // accumulate code
231- done ( ) ;
232- }
246+ function flush ( done ) {
247+ var sourceMap = convert . fromComment ( code ) . toObject ( ) ;
248+ var external = code . replace ( convert . commentRegex , '//# sourceMappingURL=' + mapPath ) ;
249+ delete sourceMap . file ;
250+ delete sourceMap . sourceRoot ;
251+ delete sourceMap . sourcesContent ;
252+ sourceMap . sources
253+ . forEach ( rootRelative ) ;
254+ pushFileToStream ( outPath + '.map' , JSON . stringify ( sourceMap , null , 2 ) ) ;
255+ pushFileToStream ( outPath , external ) ;
256+ done ( ) ;
257+ deferred . resolve ( ) ; // complete overall
258+ }
233259
234- function flush ( done ) {
235- var sourceMap = convert . fromComment ( code ) . toObject ( ) ;
236- var external = code . replace ( convert . commentRegex , '//# sourceMappingURL=' + mapPath ) ;
237- delete sourceMap . file ;
238- delete sourceMap . sourceRoot ;
239- delete sourceMap . sourcesContent ;
240- sourceMap . sources
241- . forEach ( rootRelative ) ;
242- pushFileToStream ( outPath + '.map' , JSON . stringify ( sourceMap , null , 2 ) ) ;
243- pushFileToStream ( outPath , external ) ;
244- done ( ) ;
245- deferred . resolve ( ) ; // complete overall
260+ return through . obj ( transform , flush ) ;
246261 }
247262
248- return through . obj ( transform , flush ) ;
263+ // stream output
264+ function pushFileToStream ( path , text ) {
265+ stream . push ( new gutil . File ( {
266+ path : path ,
267+ contents : new Buffer ( text )
268+ } ) ) ;
269+ }
249270 }
250271
251- // stream output
252- function pushFileToStream ( path , text ) {
253- stream . push ( new gutil . File ( {
254- path : path ,
255- contents : new Buffer ( text )
256- } ) ) ;
272+ /**
273+ * Determine the root relative form of the given file path.
274+ * If the file path is outside the project directory then just return its name.
275+ * @param {string } filePath The input path string
276+ * @param {number } An index for <code>Array.map()</code> type operations
277+ * @param {object } The array for <code>Array.map()</code> type operations
278+ * @return {string } The transformed file path
279+ */
280+ function rootRelative ( filePath , i , array ) {
281+ var rootRelPath = slash ( path . relative ( process . cwd ( ) , path . resolve ( filePath ) ) ) ; // resolve relative references
282+ var isProject = ( rootRelPath . slice ( 0 , 2 ) !== '..' ) ;
283+ var result = [
284+ options . sourceMapBase || '' ,
285+ isProject ? rootRelPath : path . basename ( rootRelPath )
286+ ] . join ( '/' ) ;
287+ if ( ( typeof i === 'number' ) && ( typeof array === 'object' ) ) {
288+ array [ i ] = result ;
289+ }
290+ return result ;
257291 }
258- }
259292
260- /**
261- * Determine the root relative form of the given file path.
262- * If the file path is outside the project directory then just return its name.
263- * @param {string } filePath The input path string
264- * @param {number } An index for <code>Array.map()</code> type operations
265- * @param {object } The array for <code>Array.map()</code> type operations
266- * @return {string } The transformed file path
267- */
268- function rootRelative ( filePath , i , array ) {
269- var rootRelPath = slash ( path . relative ( process . cwd ( ) , path . resolve ( filePath ) ) ) ; // resolve relative references
270- var isProject = ( rootRelPath . slice ( 0 , 2 ) !== '..' ) ;
271- var result = [
272- options . sourceMapBase || '' ,
273- isProject ? rootRelPath : path . basename ( rootRelPath )
274- ] . join ( '/' ) ;
275- if ( ( typeof i === 'number' ) && ( typeof array === 'object' ) ) {
276- array [ i ] = result ;
293+ /**
294+ * A pipeline 'deps' stage that populates cache for incremental compile.
295+ * Called on fully transformed row but only when there is no cache hit.
296+ * @param cache The cache used by module-deps
297+ * @returns {stream.Through } a through stream
298+ */
299+ function populateCache ( cache ) {
300+ function transform ( row , encoding , done ) {
301+ /* jshint validthis:true */
302+ var filename = row . file ;
303+
304+ // set the new transformed row output
305+ internalCache [ filename ] = {
306+ input : fs . readFileSync ( filename ) . toString ( ) ,
307+ output : {
308+ id : filename ,
309+ source : row . source ,
310+ deps : merge ( { } , row . deps ) ,
311+ file : filename
312+ }
313+ } ;
314+
315+ // we need to use a getter as it is the only hook at which we can perform comparison
316+ // getters cannot be redefined so we create on first access and retain, hence the need
317+ // for the internal cache to store the value above
318+ if ( ! cache . hasOwnProperty ( filename ) ) {
319+ Object . defineProperty ( cache , filename , {
320+ get : function ( ) {
321+ // file read and comparison is in the order of 100us
322+ var cached = internalCache [ filename ] ;
323+ var input = fs . readFileSync ( filename ) . toString ( ) ;
324+ var isMatch = ( cached . input === input ) ;
325+ return isMatch ? cached . output : undefined ;
326+ }
327+ } ) ;
328+ }
329+
330+ // complete
331+ this . push ( row ) ;
332+ done ( ) ;
333+ }
334+ return through . obj ( transform ) ;
277335 }
278- return result ;
279336 }
280337}
281338
339+ module . exports = init ;
340+
282341/**
283342 * A pipeline labeler that ensures that final file names are anonymousd in the final output
284343 * @returns {stream.Through } A through stream for the labelling stage
0 commit comments