@@ -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 (
@@ -286,17 +290,22 @@ export class Application extends AbstractComponent<
286
290
if ( ! this . internationalization . hasTranslations ( this . lang ) ) {
287
291
// Not internationalized as by definition we don't know what to include here.
288
292
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 ,
290
294
) ;
291
295
this . logger . info (
292
- ( "The supported languages are:\n\t" +
296
+ ( "The languages that translations are available for are:\n\t" +
293
297
this . internationalization
294
298
. getSupportedLanguages ( )
295
299
. join ( "\n\t" ) ) as TranslatedString ,
296
300
) ;
297
301
this . logger . info (
298
302
"You can define/override local locales with the `locales` option, or contribute them to TypeDoc!" as TranslatedString ,
299
303
) ;
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
+ ) ;
300
309
}
301
310
302
311
if (
@@ -425,9 +434,49 @@ export class Application extends AbstractComponent<
425
434
return project ;
426
435
}
427
436
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 (
429
478
success : ( project : ProjectReflection ) => Promise < void > ,
430
- ) : void {
479
+ ) : Promise < boolean > {
431
480
if (
432
481
! this . options . getValue ( "preserveWatchOutput" ) &&
433
482
this . logger instanceof ConsoleLogger
@@ -459,7 +508,7 @@ export class Application extends AbstractComponent<
459
508
// have reported in the first time... just error out for now. I'm not convinced anyone will actually notice.
460
509
if ( this . options . getFileNames ( ) . length === 0 ) {
461
510
this . logger . error ( this . i18n . solution_not_supported_in_watch_mode ( ) ) ;
462
- return ;
511
+ return false ;
463
512
}
464
513
465
514
// Support for packages mode is currently unimplemented
@@ -468,7 +517,7 @@ export class Application extends AbstractComponent<
468
517
this . entryPointStrategy !== EntryPointStrategy . Expand
469
518
) {
470
519
this . logger . error ( this . i18n . strategy_not_supported_in_watch_mode ( ) ) ;
471
- return ;
520
+ return false ;
472
521
}
473
522
474
523
const tsconfigFile =
@@ -481,7 +530,7 @@ export class Application extends AbstractComponent<
481
530
482
531
const host = ts . createWatchCompilerHost (
483
532
tsconfigFile ,
484
- { } ,
533
+ this . options . fixCompilerOptions ( { } ) ,
485
534
ts . sys ,
486
535
ts . createEmitAndSemanticDiagnosticsBuilderProgram ,
487
536
( diagnostic ) => this . logger . diagnostic ( diagnostic ) ,
@@ -506,16 +555,69 @@ export class Application extends AbstractComponent<
506
555
507
556
let successFinished = true ;
508
557
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
+ } ;
509
600
510
601
const runSuccess = ( ) => {
602
+ if ( restarting && successFinished ) {
603
+ successFinished = false ;
604
+ exitWatch ( true ) ;
605
+ return ;
606
+ }
607
+
511
608
if ( ! currentProgram ) {
512
609
return ;
513
610
}
514
611
515
612
if ( successFinished ) {
516
- if ( this . options . getValue ( "emit" ) === "both" ) {
613
+ if (
614
+ this . options . getValue ( "emit" ) === "both" &&
615
+ currentProgram !== lastProgram
616
+ ) {
517
617
currentProgram . emit ( ) ;
518
618
}
619
+ // Save for possible re-run due to non-.ts file change
620
+ lastProgram = currentProgram ;
519
621
520
622
this . logger . resetErrors ( ) ;
521
623
this . logger . resetWarnings ( ) ;
@@ -527,6 +629,10 @@ export class Application extends AbstractComponent<
527
629
if ( ! entryPoints ) {
528
630
return ;
529
631
}
632
+ this . clearWatches ( ) ;
633
+ this . criticalFiles . forEach ( ( path ) =>
634
+ this . watchFile ( path , true ) ,
635
+ ) ;
530
636
const project = this . converter . convert ( entryPoints ) ;
531
637
currentProgram = undefined ;
532
638
successFinished = false ;
@@ -537,40 +643,24 @@ export class Application extends AbstractComponent<
537
643
}
538
644
} ;
539
645
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
-
564
646
const origAfterProgramCreate = host . afterProgramCreate ;
565
647
host . afterProgramCreate = ( program ) => {
566
- if ( ts . getPreEmitDiagnostics ( program . getProgram ( ) ) . length === 0 ) {
648
+ if (
649
+ ! restarting &&
650
+ ts . getPreEmitDiagnostics ( program . getProgram ( ) ) . length === 0
651
+ ) {
567
652
currentProgram = program . getProgram ( ) ;
568
653
runSuccess ( ) ;
569
654
}
570
655
origAfterProgramCreate ?.( program ) ;
571
656
} ;
572
657
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
+ } ) ;
574
664
}
575
665
576
666
validate ( project : ProjectReflection ) {
0 commit comments