@@ -22,11 +22,18 @@ const mime = require('mime-types');
2222const path = require ( 'path' ) ;
2323const url = require ( 'url' ) ;
2424
25- function debounce ( fn , delay ) {
26- var timeout ;
27- return ( ) => {
25+ const debug = require ( 'debug' ) ( 'ReactNativePackager:Server' ) ;
26+
27+ function debounceAndBatch ( fn , delay ) {
28+ let timeout , args = [ ] ;
29+ return ( value ) => {
30+ args . push ( value ) ;
2831 clearTimeout ( timeout ) ;
29- timeout = setTimeout ( fn , delay ) ;
32+ timeout = setTimeout ( ( ) => {
33+ const a = args ;
34+ args = [ ] ;
35+ fn ( a ) ;
36+ } , delay ) ;
3037 } ;
3138}
3239
@@ -139,7 +146,10 @@ const bundleOpts = declareOpts({
139146 isolateModuleIDs : {
140147 type : 'boolean' ,
141148 default : false
142- }
149+ } ,
150+ resolutionResponse : {
151+ type : 'object' ,
152+ } ,
143153} ) ;
144154
145155const dependencyOpts = declareOpts ( {
@@ -163,8 +173,14 @@ const dependencyOpts = declareOpts({
163173 type : 'boolean' ,
164174 default : false ,
165175 } ,
176+ minify : {
177+ type : 'boolean' ,
178+ default : undefined ,
179+ } ,
166180} ) ;
167181
182+ const bundleDeps = new WeakMap ( ) ;
183+
168184class Server {
169185 constructor ( options ) {
170186 const opts = validateOpts ( options ) ;
@@ -209,12 +225,27 @@ class Server {
209225 const bundlerOpts = Object . create ( opts ) ;
210226 bundlerOpts . fileWatcher = this . _fileWatcher ;
211227 bundlerOpts . assetServer = this . _assetServer ;
228+ bundlerOpts . allowBundleUpdates = ! options . nonPersistent ;
212229 this . _bundler = new Bundler ( bundlerOpts ) ;
213230
214231 this . _fileWatcher . on ( 'all' , this . _onFileChange . bind ( this ) ) ;
215232
216- this . _debouncedFileChangeHandler = debounce ( filePath => {
217- this . _clearBundles ( ) ;
233+ this . _debouncedFileChangeHandler = debounceAndBatch ( filePaths => {
234+ // only clear bundles for non-JS changes
235+ if ( filePaths . every ( RegExp . prototype . test , / \. j s (?: o n ) ? $ / i) ) {
236+ for ( const key in this . _bundles ) {
237+ this . _bundles [ key ] . then ( bundle => {
238+ const deps = bundleDeps . get ( bundle ) ;
239+ filePaths . forEach ( filePath => {
240+ if ( deps . files . has ( filePath ) ) {
241+ deps . outdated . add ( filePath ) ;
242+ }
243+ } ) ;
244+ } ) ;
245+ }
246+ } else {
247+ this . _clearBundles ( ) ;
248+ }
218249 this . _informChangeWatchers ( ) ;
219250 } , 50 ) ;
220251 }
@@ -243,7 +274,25 @@ class Server {
243274 }
244275
245276 const opts = bundleOpts ( options ) ;
246- return this . _bundler . bundle ( opts ) ;
277+ const building = this . _bundler . bundle ( opts ) ;
278+ building . then ( bundle => {
279+ const modules = bundle . getModules ( ) . filter ( m => ! m . virtual ) ;
280+ bundleDeps . set ( bundle , {
281+ files : new Map (
282+ modules
283+ . map ( ( { sourcePath, meta = { dependencies : [ ] } } ) =>
284+ [ sourcePath , meta . dependencies ] )
285+ ) ,
286+ idToIndex : new Map ( modules . map ( ( { id} , i ) => [ id , i ] ) ) ,
287+ dependencyPairs : new Map (
288+ modules
289+ . filter ( ( { meta} ) => meta && meta . dependencyPairs )
290+ . map ( m => [ m . sourcePath , m . meta . dependencyPairs ] )
291+ ) ,
292+ outdated : new Set ( ) ,
293+ } ) ;
294+ } ) ;
295+ return building ;
247296 } ) ;
248297 }
249298
@@ -428,6 +477,91 @@ class Server {
428477 ) . done ( ( ) => Activity . endEvent ( assetEvent ) ) ;
429478 }
430479
480+ _useCachedOrUpdateOrCreateBundle ( options ) {
481+ const optionsJson = JSON . stringify ( options ) ;
482+ const bundleFromScratch = ( ) => {
483+ const building = this . buildBundle ( options ) ;
484+ this . _bundles [ optionsJson ] = building ;
485+ return building ;
486+ } ;
487+
488+ if ( optionsJson in this . _bundles ) {
489+ return this . _bundles [ optionsJson ] . then ( bundle => {
490+ const deps = bundleDeps . get ( bundle ) ;
491+ const { dependencyPairs, files, idToIndex, outdated} = deps ;
492+ if ( outdated . size ) {
493+ debug ( 'Attempt to update existing bundle' ) ;
494+ const changedModules =
495+ Array . from ( outdated , this . getModuleForPath , this ) ;
496+ deps . outdated = new Set ( ) ;
497+
498+ const opts = bundleOpts ( options ) ;
499+ const { platform, dev, minify, hot} = opts ;
500+
501+ // Need to create a resolution response to pass to the bundler
502+ // to process requires after transform. By providing a
503+ // specific response we can compute a non recursive one which
504+ // is the least we need and improve performance.
505+ const bundlePromise = this . _bundles [ optionsJson ] =
506+ this . getDependencies ( {
507+ platform, dev, hot, minify,
508+ entryFile : options . entryFile ,
509+ recursive : false ,
510+ } ) . then ( response => {
511+ debug ( 'Update bundle: rebuild shallow bundle' ) ;
512+
513+ changedModules . forEach ( m => {
514+ response . setResolvedDependencyPairs (
515+ m ,
516+ dependencyPairs . get ( m . path ) ,
517+ { ignoreFinalized : true } ,
518+ ) ;
519+ } ) ;
520+
521+ return this . buildBundle ( {
522+ ...options ,
523+ resolutionResponse : response . copy ( {
524+ dependencies : changedModules ,
525+ } )
526+ } ) . then ( updateBundle => {
527+ const oldModules = bundle . getModules ( ) ;
528+ const newModules = updateBundle . getModules ( ) ;
529+ for ( let i = 0 , n = newModules . length ; i < n ; i ++ ) {
530+ const moduleTransport = newModules [ i ] ;
531+ const { meta, sourcePath} = moduleTransport ;
532+ if ( outdated . has ( sourcePath ) ) {
533+ if ( ! contentsEqual ( meta . dependencies , new Set ( files . get ( sourcePath ) ) ) ) {
534+ // bail out if any dependencies changed
535+ return Promise . reject ( Error (
536+ `Dependencies of ${ sourcePath } changed from [${
537+ files . get ( sourcePath ) . join ( ', ' )
538+ } ] to [${ meta . dependencies . join ( ', ' ) } ]`
539+ ) ) ;
540+ }
541+
542+ oldModules [ idToIndex . get ( moduleTransport . id ) ] = moduleTransport ;
543+ }
544+ }
545+
546+ bundle . invalidateSource ( ) ;
547+ debug ( 'Successfully updated existing bundle' ) ;
548+ return bundle ;
549+ } ) ;
550+ } ) . catch ( e => {
551+ debug ( 'Failed to update existing bundle, rebuilding...' , e . stack || e . message ) ;
552+ return bundleFromScratch ( ) ;
553+ } ) ;
554+ return bundlePromise ;
555+ } else {
556+ debug ( 'Using cached bundle' ) ;
557+ return bundle ;
558+ }
559+ } ) ;
560+ }
561+
562+ return bundleFromScratch ( ) ;
563+ }
564+
431565 processRequest ( req , res , next ) {
432566 const urlObj = url . parse ( req . url , true ) ;
433567 const pathname = urlObj . pathname ;
@@ -458,26 +592,29 @@ class Server {
458592
459593 const startReqEventId = Activity . startEvent ( 'request:' + req . url ) ;
460594 const options = this . _getOptionsFromUrl ( req . url ) ;
461- const optionsJson = JSON . stringify ( options ) ;
462- const building = this . _bundles [ optionsJson ] || this . buildBundle ( options ) ;
463-
464- this . _bundles [ optionsJson ] = building ;
595+ debug ( 'Getting bundle for request' ) ;
596+ const building = this . _useCachedOrUpdateOrCreateBundle ( options ) ;
465597 building . then (
466598 p => {
467599 if ( requestType === 'bundle' ) {
600+ debug ( 'Generating source code' ) ;
468601 const bundleSource = p . getSource ( {
469602 inlineSourceMap : options . inlineSourceMap ,
470603 minify : options . minify ,
471604 dev : options . dev ,
472605 } ) ;
606+ debug ( 'Writing response headers' ) ;
473607 res . setHeader ( 'Content-Type' , 'application/javascript' ) ;
474608 res . setHeader ( 'ETag' , p . getEtag ( ) ) ;
475609 if ( req . headers [ 'if-none-match' ] === res . getHeader ( 'ETag' ) ) {
610+ debug ( 'Responding with 304' ) ;
476611 res . statusCode = 304 ;
477612 res . end ( ) ;
478613 } else {
614+ debug ( 'Writing request body' ) ;
479615 res . end ( bundleSource ) ;
480616 }
617+ debug ( 'Finished response' ) ;
481618 Activity . endEvent ( startReqEventId ) ;
482619 } else if ( requestType === 'map' ) {
483620 let sourceMap = p . getSourceMap ( {
@@ -499,7 +636,7 @@ class Server {
499636 Activity . endEvent ( startReqEventId ) ;
500637 }
501638 } ,
502- this . _handleError . bind ( this , res , optionsJson )
639+ error => this . _handleError ( res , JSON . stringify ( options ) , error )
503640 ) . done ( ) ;
504641 }
505642
@@ -564,9 +701,7 @@ class Server {
564701
565702 _sourceMapForURL ( reqUrl ) {
566703 const options = this . _getOptionsFromUrl ( reqUrl ) ;
567- const optionsJson = JSON . stringify ( options ) ;
568- const building = this . _bundles [ optionsJson ] || this . buildBundle ( options ) ;
569- this . _bundles [ optionsJson ] = building ;
704+ const building = this . _useCachedOrUpdateOrCreateBundle ( options ) ;
570705 return building . then ( p => {
571706 const sourceMap = p . getSourceMap ( {
572707 minify : options . minify ,
@@ -659,4 +794,8 @@ class Server {
659794 }
660795}
661796
797+ function contentsEqual ( array , set ) {
798+ return array . length === set . size && array . every ( set . has , set ) ;
799+ }
800+
662801module . exports = Server ;
0 commit comments