@@ -17,7 +17,7 @@ import {
1717 context ,
1818} from 'esbuild' ;
1919import assert from 'node:assert' ;
20- import { basename , extname , isAbsolute , join , normalize , relative } from 'node:path' ;
20+ import { basename , dirname , extname , isAbsolute , join , normalize , relative } from 'node:path' ;
2121import { SERVER_GENERATED_EXTERNALS } from '../../utils/server-rendering/manifest' ;
2222import {
2323 type BuildOutputFile ,
@@ -81,6 +81,7 @@ export class BundlerContext {
8181 #optionsFactory: BundlerOptionsFactory < BuildOptions & { metafile : true ; write : false } > ;
8282 #shouldCacheResult: boolean ;
8383 #loadCache?: LoadResultCache ;
84+ readonly #loadCacheKeys = new Set < string > ( ) ;
8485 #invalidationEpoch = 0 ;
8586 readonly watchFiles = new Set < string > ( ) ;
8687
@@ -93,8 +94,9 @@ export class BundlerContext {
9394 sharedLoadCache ?: LoadResultCache ,
9495 ) {
9596 this . #loadCache = sharedLoadCache ;
96- // To cache the results an option factory is needed to capture the full set of dependencies
97- this . #shouldCacheResult = incremental && typeof options === 'function' ;
97+ // In incremental mode, an option factory is needed to capture the full set of dependencies.
98+ // In non-incremental mode, results can always be cached for the duration of the build.
99+ this . #shouldCacheResult = ! incremental || typeof options === 'function' ;
98100 this . #optionsFactory = ( ...args ) => {
99101 const baseOptions = typeof options === 'function' ? options ( ...args ) : options ;
100102
@@ -227,11 +229,24 @@ export class BundlerContext {
227229 if ( this . incremental && ! this . #loadCache) {
228230 this . #loadCache = new MemoryLoadResultCache ( ) ;
229231 }
230- this . #esbuildOptions = this . #optionsFactory( this . #loadCache) ;
231- }
232-
233- if ( this . incremental ) {
234- this . watchFiles . clear ( ) ;
232+ const loadCache = this . #loadCache;
233+ const scopedLoadCache : LoadResultCache | undefined = loadCache && {
234+ get : ( path ) => {
235+ this . #loadCacheKeys. add ( path ) ;
236+
237+ return loadCache . get ( path ) ;
238+ } ,
239+ put : ( path , result ) => {
240+ this . #loadCacheKeys. add ( path ) ;
241+
242+ return loadCache . put ( path , result ) ;
243+ } ,
244+ invalidate : ( path ) => loadCache . invalidate ( path ) ,
245+ get watchFiles ( ) {
246+ return loadCache . watchFiles ;
247+ } ,
248+ } ;
249+ this . #esbuildOptions = this . #optionsFactory( scopedLoadCache ) ;
235250 }
236251
237252 let result : BuildResult < { metafile : true ; write : false } > ;
@@ -263,7 +278,7 @@ export class BundlerContext {
263278 // Build failures will throw an exception which contains errors/warnings
264279 if ( isEsBuildFailure ( failure ) ) {
265280 this . #addErrorsToWatch( failure ) ;
266- this . #addLoadCacheFilesToWatch( ) ;
281+ await this . #addLoadCacheFilesToWatch( ) ;
267282
268283 return failure ;
269284 } else {
@@ -275,6 +290,8 @@ export class BundlerContext {
275290 // While this should technically not be linked to incremental mode, incremental is only
276291 // currently enabled with watch mode where watch files are needed.
277292 if ( this . incremental ) {
293+ this . watchFiles . clear ( ) ;
294+
278295 // Add input files except virtual angular files which do not exist on disk
279296 for ( const input of Object . keys ( result . metafile . inputs ) ) {
280297 const isInternal = isInternalAngularFile ( input ) || isInternalBundlerFile ( input ) ;
@@ -309,7 +326,7 @@ export class BundlerContext {
309326 // Return if the build encountered any errors
310327 if ( result . errors . length ) {
311328 this . #addErrorsToWatch( result ) ;
312- this . #addLoadCacheFilesToWatch( ) ;
329+ await this . #addLoadCacheFilesToWatch( ) ;
313330
314331 return {
315332 errors : result . errors ,
@@ -425,12 +442,31 @@ export class BundlerContext {
425442 }
426443
427444 #addErrorsToWatch( result : BuildFailure | BuildResult ) : void {
445+ const entryPoints = this . #esbuildOptions?. entryPoints ;
446+ if ( entryPoints ) {
447+ const entries = Array . isArray ( entryPoints )
448+ ? entryPoints . map ( ( entry ) => ( typeof entry === 'string' ? entry : entry . in ) )
449+ : Object . values ( entryPoints ) ;
450+ for ( const entry of entries ) {
451+ if ( ! isInternalAngularFile ( entry ) && ! isInternalBundlerFile ( entry ) ) {
452+ this . watchFiles . add (
453+ isAbsolute ( entry ) ? normalize ( entry ) : join ( this . workspaceRoot , entry ) ,
454+ ) ;
455+ }
456+ }
457+ }
458+
428459 for ( const error of result . errors ) {
429460 let file = error . location ?. file ;
430461 if ( file && ! isInternalAngularFile ( file ) ) {
431- this . watchFiles . add ( isAbsolute ( file ) ? normalize ( file ) : join ( this . workspaceRoot , file ) ) ;
462+ const errorFile = isAbsolute ( file ) ? normalize ( file ) : join ( this . workspaceRoot , file ) ;
463+ this . watchFiles . add ( errorFile ) ;
464+ const match = / ^ C o u l d n o t r e s o l v e " ( \. \. ? \/ [ ^ " ] + ) " / . exec ( error . text ) ;
465+ if ( match ) {
466+ this . watchFiles . add ( join ( dirname ( errorFile ) , match [ 1 ] ) ) ;
467+ }
432468 }
433- for ( const note of error . notes ) {
469+ for ( const note of error . notes ?? [ ] ) {
434470 file = note . location ?. file ;
435471 if ( file && ! isInternalAngularFile ( file ) ) {
436472 this . watchFiles . add ( isAbsolute ( file ) ? normalize ( file ) : join ( this . workspaceRoot , file ) ) ;
@@ -439,11 +475,18 @@ export class BundlerContext {
439475 }
440476 }
441477
442- #addLoadCacheFilesToWatch( ) : void {
478+ async #addLoadCacheFilesToWatch( ) : Promise < void > {
443479 if ( this . incremental && this . #loadCache) {
444- for ( const file of this . #loadCache. watchFiles ) {
445- if ( ! isInternalAngularFile ( file ) ) {
446- this . watchFiles . add ( isAbsolute ( file ) ? normalize ( file ) : join ( this . workspaceRoot , file ) ) ;
480+ for ( const key of this . #loadCacheKeys) {
481+ const cachedResult = await this . #loadCache. get ( key ) ;
482+ if ( cachedResult ?. watchFiles ) {
483+ for ( const file of cachedResult . watchFiles ) {
484+ if ( ! isInternalAngularFile ( file ) ) {
485+ this . watchFiles . add (
486+ isAbsolute ( file ) ? normalize ( file ) : join ( this . workspaceRoot , file ) ,
487+ ) ;
488+ }
489+ }
447490 }
448491 }
449492 }
@@ -499,27 +542,25 @@ export class BundlerContext {
499542 candidateFiles = normalizedFiles ;
500543 }
501544
502- let invalid = false ;
503- for ( const file of candidateFiles ) {
504- if ( this . #loadCache?. invalidate ( file ) ) {
505- invalid = true ;
545+ if ( this . #loadCache) {
546+ for ( const file of candidateFiles ) {
547+ this . #loadCache. invalidate ( file ) ;
506548 }
507549 }
508550
509- if ( ! invalid ) {
510- if ( this . watchFiles . size < candidateFiles . size ) {
511- for ( const file of this . watchFiles ) {
512- if ( candidateFiles . has ( file ) ) {
513- invalid = true ;
514- break ;
515- }
551+ let invalid = false ;
552+ if ( this . watchFiles . size < candidateFiles . size ) {
553+ for ( const file of this . watchFiles ) {
554+ if ( candidateFiles . has ( file ) ) {
555+ invalid = true ;
556+ break ;
516557 }
517- } else {
518- for ( const file of candidateFiles ) {
519- if ( this . watchFiles . has ( file ) ) {
520- invalid = true ;
521- break ;
522- }
558+ }
559+ } else {
560+ for ( const file of candidateFiles ) {
561+ if ( this . watchFiles . has ( file ) ) {
562+ invalid = true ;
563+ break ;
523564 }
524565 }
525566 }
@@ -544,6 +585,7 @@ export class BundlerContext {
544585 this . #esbuildResult = undefined ;
545586 this . #activeBundlePromise = undefined ;
546587 this . #loadCache = undefined ;
588+ this . #loadCacheKeys. clear ( ) ;
547589 await this . #esbuildContext?. dispose ( ) ;
548590 } finally {
549591 this . #esbuildContext = undefined ;
0 commit comments