@@ -231,7 +231,9 @@ export class Application extends AbstractComponent<
231231 readers . forEach ( ( r ) => app . options . addReader ( r ) ) ;
232232 app . options . reset ( ) ;
233233 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+ ) ;
235237 app . logger . level = app . options . getValue ( "logLevel" ) ;
236238
237239 await loadPlugins ( app , app . options . getValue ( "plugin" ) ) ;
@@ -265,7 +267,9 @@ export class Application extends AbstractComponent<
265267 private async _bootstrap ( options : Partial < TypeDocOptions > ) {
266268 this . options . reset ( ) ;
267269 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+ ) ;
269273 this . setOptions ( options ) ;
270274 this . logger . level = this . options . getValue ( "logLevel" ) ;
271275 for ( const [ lang , locales ] of Object . entries (
@@ -425,9 +429,49 @@ export class Application extends AbstractComponent<
425429 return project ;
426430 }
427431
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 (
429473 success : ( project : ProjectReflection ) => Promise < void > ,
430- ) : void {
474+ ) : Promise < boolean > {
431475 if (
432476 ! this . options . getValue ( "preserveWatchOutput" ) &&
433477 this . logger instanceof ConsoleLogger
@@ -459,7 +503,7 @@ export class Application extends AbstractComponent<
459503 // have reported in the first time... just error out for now. I'm not convinced anyone will actually notice.
460504 if ( this . options . getFileNames ( ) . length === 0 ) {
461505 this . logger . error ( this . i18n . solution_not_supported_in_watch_mode ( ) ) ;
462- return ;
506+ return false ;
463507 }
464508
465509 // Support for packages mode is currently unimplemented
@@ -468,7 +512,7 @@ export class Application extends AbstractComponent<
468512 this . entryPointStrategy !== EntryPointStrategy . Expand
469513 ) {
470514 this . logger . error ( this . i18n . strategy_not_supported_in_watch_mode ( ) ) ;
471- return ;
515+ return false ;
472516 }
473517
474518 const tsconfigFile =
@@ -506,16 +550,69 @@ export class Application extends AbstractComponent<
506550
507551 let successFinished = true ;
508552 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+ } ;
509595
510596 const runSuccess = ( ) => {
597+ if ( restarting && successFinished ) {
598+ successFinished = false ;
599+ exitWatch ( true ) ;
600+ return ;
601+ }
602+
511603 if ( ! currentProgram ) {
512604 return ;
513605 }
514606
515607 if ( successFinished ) {
516- if ( this . options . getValue ( "emit" ) === "both" ) {
608+ if (
609+ this . options . getValue ( "emit" ) === "both" &&
610+ currentProgram !== lastProgram
611+ ) {
517612 currentProgram . emit ( ) ;
518613 }
614+ // Save for possible re-run due to non-.ts file change
615+ lastProgram = currentProgram ;
519616
520617 this . logger . resetErrors ( ) ;
521618 this . logger . resetWarnings ( ) ;
@@ -527,6 +624,10 @@ export class Application extends AbstractComponent<
527624 if ( ! entryPoints ) {
528625 return ;
529626 }
627+ this . clearWatches ( ) ;
628+ this . criticalFiles . forEach ( ( path ) =>
629+ this . watchFile ( path , true ) ,
630+ ) ;
530631 const project = this . converter . convert ( entryPoints ) ;
531632 currentProgram = undefined ;
532633 successFinished = false ;
@@ -563,14 +664,22 @@ export class Application extends AbstractComponent<
563664
564665 const origAfterProgramCreate = host . afterProgramCreate ;
565666 host . afterProgramCreate = ( program ) => {
566- if ( ts . getPreEmitDiagnostics ( program . getProgram ( ) ) . length === 0 ) {
667+ if (
668+ ! restarting &&
669+ ts . getPreEmitDiagnostics ( program . getProgram ( ) ) . length === 0
670+ ) {
567671 currentProgram = program . getProgram ( ) ;
568672 runSuccess ( ) ;
569673 }
570674 origAfterProgramCreate ?.( program ) ;
571675 } ;
572676
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+ } ) ;
574683 }
575684
576685 validate ( project : ProjectReflection ) {
0 commit comments