Skip to content

Commit 12b60ca

Browse files
committed
Merge remote-tracking branch 'origin/dev'
2 parents c81edb4 + 4e1266f commit 12b60ca

32 files changed

+510
-287
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ title: Changelog
99
- `@includeCode` and `@inline` can now inject parts of files using region
1010
names or line numbers, #2816.
1111
- Introduced `ja` translation options, deprecated `jp` in favor of `ja`, #2843.
12+
- Improved TypeDoc's `--watch` option to support watching files not caught by
13+
TypeScript's watch mode, #2675.
1214
- The `@inline` tag now works in more places for generic types.
1315
- Visibility filters now consider individual signatures, #2846.
1416

@@ -19,9 +21,12 @@ title: Changelog
1921
- Fixed an issue with `@class` incorrectly handling mapped types, #2842.
2022
- TypeDoc will now consider symbols to be external only if all of their declarations are external
2123
so that declaration merged members with global symbols can be documented, #2844.
24+
- Fixed an issue where TypeDoc would constantly rebuild, #2844.
25+
- Fixed an issue where the dropdown arrow in the index group would not respect the state of the dropdown, #2845.
2226

2327
### Thanks!
2428

29+
- @pjeby
2530
- @shawninder
2631
- @tats-u
2732
- @XeroAlpha

bin/typedoc

+21-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
11
#!/usr/bin/env node
22
//@ts-check
33

4-
/* eslint-disable @typescript-eslint/no-var-requires */
5-
import("../dist/lib/cli.js");
4+
const { fork } = require("child_process");
5+
6+
function main() {
7+
fork(__dirname + "/../dist/lib/cli.js", process.argv.slice(2), {
8+
stdio: "inherit",
9+
}).on("exit", (code) => {
10+
// Watch restart required? Fork a new child
11+
if (code === 7) {
12+
// Set an environment variable to ensure we continue watching
13+
// Otherwise, the watch might stop unexpectedly if the watch
14+
// option was set in a config file originally, and change to false
15+
// later, causing a restart
16+
process.env["TYPEDOC_FORCE_WATCH"] = "1";
17+
main();
18+
} else {
19+
process.exit(code || 0);
20+
}
21+
});
22+
}
23+
24+
main();

site/options/other.md

+6-3
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,12 @@ $ typedoc --watch
1313
Use TypeScript's incremental compiler to watch source files for changes and
1414
build the docs on change. May be combined with `--emit`.
1515

16-
> [!note] This mode will only detect changes to files watched by the TypeScript
17-
> compiler. Changes to other files (`README.md`, imported files with `@include` or
18-
> `@includeCode`) will not cause a rebuild.
16+
This mode detects changes to project documents, readme, custom JS/CSS,
17+
configuration files, files imported by `@include`/`@includeCode`, and any
18+
files explicitly registered by plugins as needing to be watched, as well
19+
as all your TypeScript source files.
20+
21+
Watch mode is not supported with `entryPointStrategy` set to `packages` or `merge`.
1922

2023
## preserveWatchOutput
2124

site/tags/include.md

+7
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ For example:
4444
{@includeCode ../../example/src/enums.ts#simpleEnum}
4545
```
4646

47+
Multiple regions may be specified, separated by commas. If multiple regions are
48+
specified, TypeDoc will combine them into a single code block.
49+
50+
```md
51+
{@includeCode file.ts#region1,region2}
52+
```
53+
4754
Regions are specified in the files themselves via comments.
4855

4956
In TypeScript for example, the following would be a valid region:

src/lib/application.ts

+126-36
Original file line numberDiff line numberDiff line change
@@ -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) {

src/lib/cli.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ async function main() {
3232
if (exitCode !== ExitCodes.Watching) {
3333
app.logger.verbose(`Full run took ${Date.now() - start}ms`);
3434
logRunSummary(app.logger);
35-
process.exit(exitCode);
3635
}
36+
process.exit(exitCode);
3737
} catch (error) {
3838
console.error("TypeDoc exiting with unexpected error:");
3939
console.error(error);
@@ -72,12 +72,13 @@ async function run(app: td.Application) {
7272
return ExitCodes.OptionError;
7373
}
7474

75-
if (app.options.getValue("watch")) {
76-
app.convertAndWatch(async (project) => {
75+
if (app.options.getValue("watch") || process.env["TYPEDOC_FORCE_WATCH"]) {
76+
const continueWatching = await app.convertAndWatch(async (project) => {
7777
app.validate(project);
7878
await app.generateOutputs(project);
7979
});
80-
return ExitCodes.Watching;
80+
81+
return continueWatching ? ExitCodes.Watching : ExitCodes.OptionError;
8182
}
8283

8384
const project = await app.convert();

src/lib/converter/converter.ts

+1
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,7 @@ export class Converter extends AbstractComponent<Application, ConverterEvents> {
688688
frontmatter,
689689
);
690690

691+
this.application.watchFile(file.fileName);
691692
parent.addChild(docRefl);
692693
parent.project.registerReflection(docRefl, undefined, file.fileName);
693694
this.trigger(ConverterEvents.CREATE_DOCUMENT, undefined, docRefl);

0 commit comments

Comments
 (0)