@@ -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 (
@@ -286,17 +290,22 @@ export class Application extends AbstractComponent<
286290 if ( ! this . internationalization . hasTranslations ( this . lang ) ) {
287291 // Not internationalized as by definition we don't know what to include here.
288292 this . logger . warn (
289- `Options specified "${ this . lang } " as the language to use, but TypeDoc does not support it.` as TranslatedString ,
293+ `Options specified "${ this . lang } " as the language to use, but TypeDoc cannot provide translations for it.` as TranslatedString ,
290294 ) ;
291295 this . logger . info (
292- ( "The supported languages are:\n\t" +
296+ ( "The languages that translations are available for are:\n\t" +
293297 this . internationalization
294298 . getSupportedLanguages ( )
295299 . join ( "\n\t" ) ) as TranslatedString ,
296300 ) ;
297301 this . logger . info (
298302 "You can define/override local locales with the `locales` option, or contribute them to TypeDoc!" as TranslatedString ,
299303 ) ;
304+ } else if ( this . lang === "jp" ) {
305+ this . logger . warn (
306+ // Only Japanese see this. Meaning: "jp" is going to be removed in the future. Please designate "ja" instead.
307+ "「jp」は将来削除されます。代わりに「ja」を指定してください。" as TranslatedString ,
308+ ) ;
300309 }
301310
302311 if (
@@ -425,9 +434,49 @@ export class Application extends AbstractComponent<
425434 return project ;
426435 }
427436
428- public convertAndWatch (
437+ private watchers = new Map < string , ts . FileWatcher > ( ) ;
438+ private _watchFile ?: ( path : string , shouldRestart ?: boolean ) => void ;
439+ private criticalFiles = new Set < string > ( ) ;
440+
441+ private clearWatches ( ) {
442+ this . watchers . forEach ( ( w ) => w . close ( ) ) ;
443+ this . watchers . clear ( ) ;
444+ }
445+
446+ private watchConfigFile ( path : string ) {
447+ this . criticalFiles . add ( path ) ;
448+ }
449+
450+ /**
451+ * Register that the current build depends on a file, so that in watch mode
452+ * the build will be repeated. Has no effect if a watch build is not
453+ * running, or if the file has already been registered.
454+ *
455+ * @param path The file to watch. It does not need to exist, and you should
456+ * in fact register files you look for, but which do not exist, so that if
457+ * they are created the build will re-run. (e.g. if you look through a list
458+ * of 5 possibilities and find the third, you should register the first 3.)
459+ *
460+ * @param shouldRestart Should the build be completely restarted? (This is
461+ * normally only used for configuration files -- i.e. files whose contents
462+ * determine how conversion, rendering, or compiling will be done, as
463+ * opposed to files that are only read *during* the conversion or
464+ * rendering.)
465+ */
466+ public watchFile ( path : string , shouldRestart = false ) {
467+ this . _watchFile ?.( path , shouldRestart ) ;
468+ }
469+
470+ /**
471+ * Run a convert / watch process.
472+ *
473+ * @param success Callback to run after each convert, receiving the project
474+ * @returns True if the watch process should be restarted due to a
475+ * configuration change, false for an options error
476+ */
477+ public async convertAndWatch (
429478 success : ( project : ProjectReflection ) => Promise < void > ,
430- ) : void {
479+ ) : Promise < boolean > {
431480 if (
432481 ! this . options . getValue ( "preserveWatchOutput" ) &&
433482 this . logger instanceof ConsoleLogger
@@ -459,7 +508,7 @@ export class Application extends AbstractComponent<
459508 // have reported in the first time... just error out for now. I'm not convinced anyone will actually notice.
460509 if ( this . options . getFileNames ( ) . length === 0 ) {
461510 this . logger . error ( this . i18n . solution_not_supported_in_watch_mode ( ) ) ;
462- return ;
511+ return false ;
463512 }
464513
465514 // Support for packages mode is currently unimplemented
@@ -468,7 +517,7 @@ export class Application extends AbstractComponent<
468517 this . entryPointStrategy !== EntryPointStrategy . Expand
469518 ) {
470519 this . logger . error ( this . i18n . strategy_not_supported_in_watch_mode ( ) ) ;
471- return ;
520+ return false ;
472521 }
473522
474523 const tsconfigFile =
@@ -481,7 +530,7 @@ export class Application extends AbstractComponent<
481530
482531 const host = ts . createWatchCompilerHost (
483532 tsconfigFile ,
484- { } ,
533+ this . options . fixCompilerOptions ( { } ) ,
485534 ts . sys ,
486535 ts . createEmitAndSemanticDiagnosticsBuilderProgram ,
487536 ( diagnostic ) => this . logger . diagnostic ( diagnostic ) ,
@@ -506,16 +555,69 @@ export class Application extends AbstractComponent<
506555
507556 let successFinished = true ;
508557 let currentProgram : ts . Program | undefined ;
558+ let lastProgram = currentProgram ;
559+ let restarting = false ;
560+
561+ this . _watchFile = ( path : string , shouldRestart = false ) => {
562+ this . logger . verbose (
563+ `Watching ${ nicePath ( path ) } , shouldRestart=${ shouldRestart } ` ,
564+ ) ;
565+ if ( this . watchers . has ( path ) ) return ;
566+ this . watchers . set (
567+ path ,
568+ host . watchFile (
569+ path ,
570+ ( file ) => {
571+ if ( shouldRestart ) {
572+ restartMain ( file ) ;
573+ } else if ( ! currentProgram ) {
574+ currentProgram = lastProgram ;
575+ this . logger . info (
576+ this . i18n . file_0_changed_rebuilding (
577+ nicePath ( file ) ,
578+ ) ,
579+ ) ;
580+ }
581+ if ( successFinished ) runSuccess ( ) ;
582+ } ,
583+ 2000 ,
584+ ) ,
585+ ) ;
586+ } ;
587+
588+ /** resolver for the returned promise */
589+ let exitWatch : ( restart : boolean ) => unknown ;
590+ const restartMain = ( file : string ) => {
591+ if ( restarting ) return ;
592+ this . logger . info (
593+ this . i18n . file_0_changed_restarting ( nicePath ( file ) ) ,
594+ ) ;
595+ restarting = true ;
596+ currentProgram = undefined ;
597+ this . clearWatches ( ) ;
598+ tsWatcher . close ( ) ;
599+ } ;
509600
510601 const runSuccess = ( ) => {
602+ if ( restarting && successFinished ) {
603+ successFinished = false ;
604+ exitWatch ( true ) ;
605+ return ;
606+ }
607+
511608 if ( ! currentProgram ) {
512609 return ;
513610 }
514611
515612 if ( successFinished ) {
516- if ( this . options . getValue ( "emit" ) === "both" ) {
613+ if (
614+ this . options . getValue ( "emit" ) === "both" &&
615+ currentProgram !== lastProgram
616+ ) {
517617 currentProgram . emit ( ) ;
518618 }
619+ // Save for possible re-run due to non-.ts file change
620+ lastProgram = currentProgram ;
519621
520622 this . logger . resetErrors ( ) ;
521623 this . logger . resetWarnings ( ) ;
@@ -527,6 +629,10 @@ export class Application extends AbstractComponent<
527629 if ( ! entryPoints ) {
528630 return ;
529631 }
632+ this . clearWatches ( ) ;
633+ this . criticalFiles . forEach ( ( path ) =>
634+ this . watchFile ( path , true ) ,
635+ ) ;
530636 const project = this . converter . convert ( entryPoints ) ;
531637 currentProgram = undefined ;
532638 successFinished = false ;
@@ -537,40 +643,24 @@ export class Application extends AbstractComponent<
537643 }
538644 } ;
539645
540- const origCreateProgram = host . createProgram ;
541- host . createProgram = (
542- rootNames ,
543- options ,
544- host ,
545- oldProgram ,
546- configDiagnostics ,
547- references ,
548- ) => {
549- // If we always do this, we'll get a crash the second time a program is created.
550- if ( rootNames !== undefined ) {
551- options = this . options . fixCompilerOptions ( options || { } ) ;
552- }
553-
554- return origCreateProgram (
555- rootNames ,
556- options ,
557- host ,
558- oldProgram ,
559- configDiagnostics ,
560- references ,
561- ) ;
562- } ;
563-
564646 const origAfterProgramCreate = host . afterProgramCreate ;
565647 host . afterProgramCreate = ( program ) => {
566- if ( ts . getPreEmitDiagnostics ( program . getProgram ( ) ) . length === 0 ) {
648+ if (
649+ ! restarting &&
650+ ts . getPreEmitDiagnostics ( program . getProgram ( ) ) . length === 0
651+ ) {
567652 currentProgram = program . getProgram ( ) ;
568653 runSuccess ( ) ;
569654 }
570655 origAfterProgramCreate ?.( program ) ;
571656 } ;
572657
573- ts . createWatchProgram ( host ) ;
658+ const tsWatcher = ts . createWatchProgram ( host ) ;
659+
660+ // Don't return to caller until the watch needs to restart
661+ return await new Promise ( ( res ) => {
662+ exitWatch = res ;
663+ } ) ;
574664 }
575665
576666 validate ( project : ProjectReflection ) {
0 commit comments