@@ -231,7 +231,9 @@ export class Application extends AbstractComponent<
231
231
readers . forEach ( ( r ) => app . options . addReader ( r ) ) ;
232
232
app . options . reset ( ) ;
233
233
app . setOptions ( options , /* reportErrors */ false ) ;
234
- await app . options . read ( new Logger ( ) ) ;
234
+ await app . options . read ( new Logger ( ) , undefined , ( path ) =>
235
+ app . watchConfigFile ( path ) ,
236
+ ) ;
235
237
app . logger . level = app . options . getValue ( "logLevel" ) ;
236
238
237
239
await loadPlugins ( app , app . options . getValue ( "plugin" ) ) ;
@@ -265,7 +267,9 @@ export class Application extends AbstractComponent<
265
267
private async _bootstrap ( options : Partial < TypeDocOptions > ) {
266
268
this . options . reset ( ) ;
267
269
this . setOptions ( options , /* reportErrors */ false ) ;
268
- await this . options . read ( this . logger ) ;
270
+ await this . options . read ( this . logger , undefined , ( path ) =>
271
+ this . watchConfigFile ( path ) ,
272
+ ) ;
269
273
this . setOptions ( options ) ;
270
274
this . logger . level = this . options . getValue ( "logLevel" ) ;
271
275
for ( const [ lang , locales ] of Object . entries (
@@ -425,9 +429,49 @@ export class Application extends AbstractComponent<
425
429
return project ;
426
430
}
427
431
428
- public convertAndWatch (
432
+ private watchers = new Map < string , ts . FileWatcher > ( ) ;
433
+ private _watchFile ?: ( path : string , shouldRestart ?: boolean ) => void ;
434
+ private criticalFiles = new Set < string > ( ) ;
435
+
436
+ private clearWatches ( ) {
437
+ this . watchers . forEach ( ( w ) => w . close ( ) ) ;
438
+ this . watchers . clear ( ) ;
439
+ }
440
+
441
+ private watchConfigFile ( path : string ) {
442
+ this . criticalFiles . add ( path ) ;
443
+ }
444
+
445
+ /**
446
+ * Register that the current build depends on a file, so that in watch mode
447
+ * the build will be repeated. Has no effect if a watch build is not
448
+ * running, or if the file has already been registered.
449
+ *
450
+ * @param path The file to watch. It does not need to exist, and you should
451
+ * in fact register files you look for, but which do not exist, so that if
452
+ * they are created the build will re-run. (e.g. if you look through a list
453
+ * of 5 possibilities and find the third, you should register the first 3.)
454
+ *
455
+ * @param shouldRestart Should the build be completely restarted? (This is
456
+ * normally only used for configuration files -- i.e. files whose contents
457
+ * determine how conversion, rendering, or compiling will be done, as
458
+ * opposed to files that are only read *during* the conversion or
459
+ * rendering.)
460
+ */
461
+ public watchFile ( path : string , shouldRestart = false ) {
462
+ this . _watchFile ?.( path , shouldRestart ) ;
463
+ }
464
+
465
+ /**
466
+ * Run a convert / watch process.
467
+ *
468
+ * @param success Callback to run after each convert, receiving the project
469
+ * @returns True if the watch process should be restarted due to a
470
+ * configuration change, false for an options error
471
+ */
472
+ public async convertAndWatch (
429
473
success : ( project : ProjectReflection ) => Promise < void > ,
430
- ) : void {
474
+ ) : Promise < boolean > {
431
475
if (
432
476
! this . options . getValue ( "preserveWatchOutput" ) &&
433
477
this . logger instanceof ConsoleLogger
@@ -459,7 +503,7 @@ export class Application extends AbstractComponent<
459
503
// have reported in the first time... just error out for now. I'm not convinced anyone will actually notice.
460
504
if ( this . options . getFileNames ( ) . length === 0 ) {
461
505
this . logger . error ( this . i18n . solution_not_supported_in_watch_mode ( ) ) ;
462
- return ;
506
+ return false ;
463
507
}
464
508
465
509
// Support for packages mode is currently unimplemented
@@ -468,7 +512,7 @@ export class Application extends AbstractComponent<
468
512
this . entryPointStrategy !== EntryPointStrategy . Expand
469
513
) {
470
514
this . logger . error ( this . i18n . strategy_not_supported_in_watch_mode ( ) ) ;
471
- return ;
515
+ return false ;
472
516
}
473
517
474
518
const tsconfigFile =
@@ -506,16 +550,69 @@ export class Application extends AbstractComponent<
506
550
507
551
let successFinished = true ;
508
552
let currentProgram : ts . Program | undefined ;
553
+ let lastProgram = currentProgram ;
554
+ let restarting = false ;
555
+
556
+ this . _watchFile = ( path : string , shouldRestart = false ) => {
557
+ this . logger . verbose (
558
+ `Watching ${ nicePath ( path ) } , shouldRestart=${ shouldRestart } ` ,
559
+ ) ;
560
+ if ( this . watchers . has ( path ) ) return ;
561
+ this . watchers . set (
562
+ path ,
563
+ host . watchFile (
564
+ path ,
565
+ ( file ) => {
566
+ if ( shouldRestart ) {
567
+ restartMain ( file ) ;
568
+ } else if ( ! currentProgram ) {
569
+ currentProgram = lastProgram ;
570
+ this . logger . info (
571
+ this . i18n . file_0_changed_rebuilding (
572
+ nicePath ( file ) ,
573
+ ) ,
574
+ ) ;
575
+ }
576
+ if ( successFinished ) runSuccess ( ) ;
577
+ } ,
578
+ 2000 ,
579
+ ) ,
580
+ ) ;
581
+ } ;
582
+
583
+ /** resolver for the returned promise */
584
+ let exitWatch : ( restart : boolean ) => unknown ;
585
+ const restartMain = ( file : string ) => {
586
+ if ( restarting ) return ;
587
+ this . logger . info (
588
+ this . i18n . file_0_changed_restarting ( nicePath ( file ) ) ,
589
+ ) ;
590
+ restarting = true ;
591
+ currentProgram = undefined ;
592
+ this . clearWatches ( ) ;
593
+ tsWatcher . close ( ) ;
594
+ } ;
509
595
510
596
const runSuccess = ( ) => {
597
+ if ( restarting && successFinished ) {
598
+ successFinished = false ;
599
+ exitWatch ( true ) ;
600
+ return ;
601
+ }
602
+
511
603
if ( ! currentProgram ) {
512
604
return ;
513
605
}
514
606
515
607
if ( successFinished ) {
516
- if ( this . options . getValue ( "emit" ) === "both" ) {
608
+ if (
609
+ this . options . getValue ( "emit" ) === "both" &&
610
+ currentProgram !== lastProgram
611
+ ) {
517
612
currentProgram . emit ( ) ;
518
613
}
614
+ // Save for possible re-run due to non-.ts file change
615
+ lastProgram = currentProgram ;
519
616
520
617
this . logger . resetErrors ( ) ;
521
618
this . logger . resetWarnings ( ) ;
@@ -527,6 +624,10 @@ export class Application extends AbstractComponent<
527
624
if ( ! entryPoints ) {
528
625
return ;
529
626
}
627
+ this . clearWatches ( ) ;
628
+ this . criticalFiles . forEach ( ( path ) =>
629
+ this . watchFile ( path , true ) ,
630
+ ) ;
530
631
const project = this . converter . convert ( entryPoints ) ;
531
632
currentProgram = undefined ;
532
633
successFinished = false ;
@@ -563,14 +664,22 @@ export class Application extends AbstractComponent<
563
664
564
665
const origAfterProgramCreate = host . afterProgramCreate ;
565
666
host . afterProgramCreate = ( program ) => {
566
- if ( ts . getPreEmitDiagnostics ( program . getProgram ( ) ) . length === 0 ) {
667
+ if (
668
+ ! restarting &&
669
+ ts . getPreEmitDiagnostics ( program . getProgram ( ) ) . length === 0
670
+ ) {
567
671
currentProgram = program . getProgram ( ) ;
568
672
runSuccess ( ) ;
569
673
}
570
674
origAfterProgramCreate ?.( program ) ;
571
675
} ;
572
676
573
- ts . createWatchProgram ( host ) ;
677
+ const tsWatcher = ts . createWatchProgram ( host ) ;
678
+
679
+ // Don't return to caller until the watch needs to restart
680
+ return await new Promise ( ( res ) => {
681
+ exitWatch = res ;
682
+ } ) ;
574
683
}
575
684
576
685
validate ( project : ProjectReflection ) {
0 commit comments