Skip to content

Commit 2188f86

Browse files
committed
feat: Support for --watch, --preserveWatchOutput, --emit
1 parent c3a59be commit 2188f86

File tree

6 files changed

+162
-5
lines changed

6 files changed

+162
-5
lines changed

bin/typedoc

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,24 @@ async function run(app) {
5858
return ExitCodes.NoEntryPoints;
5959
}
6060

61+
if (app.options.getValue("watch")) {
62+
app.convertAndWatch(async (project) => {
63+
const out = app.options.getValue("out");
64+
if (out) {
65+
await app.generateDocs(project, out);
66+
}
67+
const json = app.options.getValue("json");
68+
if (json) {
69+
await app.generateJson(project, json);
70+
}
71+
72+
if (!out && !json) {
73+
await app.generateDocs(project, "./docs");
74+
}
75+
});
76+
return ExitCodes.Ok;
77+
}
78+
6179
const project = app.convert();
6280
if (!project) {
6381
return ExitCodes.CompileError;

src/lib/application.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
import { Options, BindOption } from "./utils";
2525
import { TypeDocOptions } from "./utils/options/declaration";
2626
import { flatMap } from "./utils/array";
27+
import { basename } from "path";
2728

2829
// eslint-disable-next-line @typescript-eslint/no-var-requires
2930
const packageInfo = require("../../package.json") as {
@@ -238,12 +239,133 @@ export class Application extends ChildableComponent<
238239
return;
239240
}
240241

242+
if (this.application.options.getValue("emit")) {
243+
for (const program of programs) {
244+
program.emit();
245+
}
246+
}
247+
241248
return this.converter.convert(
242249
this.expandInputFiles(this.entryPoints),
243250
programs
244251
);
245252
}
246253

254+
public convertAndWatch(
255+
success: (project: ProjectReflection) => Promise<void>
256+
): void {
257+
if (
258+
!this.options.getValue("preserveWatchOutput") &&
259+
this.logger instanceof ConsoleLogger
260+
) {
261+
ts.sys.clearScreen?.();
262+
}
263+
264+
this.logger.verbose(
265+
"Using TypeScript %s from %s",
266+
this.getTypeScriptVersion(),
267+
this.getTypeScriptPath()
268+
);
269+
270+
if (
271+
!supportedVersionMajorMinor.some(
272+
(version) => version == ts.versionMajorMinor
273+
)
274+
) {
275+
this.logger.warn(
276+
`You are running with an unsupported TypeScript version! TypeDoc supports ${supportedVersionMajorMinor.join(
277+
", "
278+
)}`
279+
);
280+
}
281+
282+
if (Object.keys(this.options.getCompilerOptions()).length === 0) {
283+
this.logger.warn(
284+
`No compiler options set. This likely means that TypeDoc did not find your tsconfig.json. Generated documentation will probably be empty.`
285+
);
286+
}
287+
288+
// Doing this is considerably more complicated, we'd need to manage an array of programs, not convert until all programs
289+
// have reported in the first time... just error out for now. I'm not convinced anyone will actually notice.
290+
if (this.application.options.getFileNames().length === 0) {
291+
this.logger.error(
292+
"The provided tsconfig file looks like a solution style tsconfig, which is not supported in watch mode."
293+
);
294+
return;
295+
}
296+
297+
// Matches the behavior of the tsconfig option reader.
298+
let tsconfigFile = this.options.getValue("tsconfig");
299+
tsconfigFile =
300+
ts.findConfigFile(
301+
tsconfigFile,
302+
ts.sys.fileExists,
303+
tsconfigFile.toLowerCase().endsWith(".json")
304+
? basename(tsconfigFile)
305+
: undefined
306+
) ?? "tsconfig.json";
307+
308+
// We don't want to do it the first time to preserve initial debug status messages. They'll be lost
309+
// after the user saves a file, but better than nothing...
310+
let firstStatusReport = true;
311+
312+
const host = ts.createWatchCompilerHost(
313+
tsconfigFile,
314+
{ noEmit: !this.application.options.getValue("emit") },
315+
ts.sys,
316+
ts.createEmitAndSemanticDiagnosticsBuilderProgram,
317+
(diagnostic) => this.logger.diagnostic(diagnostic),
318+
(status, newLine, _options, errorCount) => {
319+
if (
320+
!firstStatusReport &&
321+
errorCount === void 0 &&
322+
!this.options.getValue("preserveWatchOutput") &&
323+
this.logger instanceof ConsoleLogger
324+
) {
325+
ts.sys.clearScreen?.();
326+
}
327+
firstStatusReport = false;
328+
this.logger.write(
329+
ts.flattenDiagnosticMessageText(status.messageText, newLine)
330+
);
331+
}
332+
);
333+
334+
let successFinished = true;
335+
let currentProgram: ts.Program | undefined;
336+
337+
const runSuccess = () => {
338+
if (!currentProgram) {
339+
return;
340+
}
341+
342+
if (successFinished) {
343+
this.logger.resetErrors();
344+
const project = this.converter.convert(
345+
this.expandInputFiles(this.entryPoints),
346+
currentProgram
347+
);
348+
currentProgram = undefined;
349+
successFinished = false;
350+
success(project).then(() => {
351+
successFinished = true;
352+
runSuccess();
353+
});
354+
}
355+
};
356+
357+
const origAfterProgramCreate = host.afterProgramCreate;
358+
host.afterProgramCreate = (program) => {
359+
if (ts.getPreEmitDiagnostics(program.getProgram()).length === 0) {
360+
currentProgram = program.getProgram();
361+
runSuccess();
362+
}
363+
origAfterProgramCreate?.(program);
364+
};
365+
366+
ts.createWatchProgram(host);
367+
}
368+
247369
/**
248370
* Render HTML for the given project
249371
*/

src/lib/converter/converter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ export class Converter extends ChildableComponent<
139139
convert(
140140
entryPoints: readonly string[],
141141
programs: ts.Program | readonly ts.Program[]
142-
): ProjectReflection | undefined {
142+
): ProjectReflection {
143143
programs = programs instanceof Array ? programs : [programs];
144144
this.externalPatternCache = void 0;
145145

src/lib/utils/options/declaration.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ export interface TypeDocOptionMap {
4848
includes: string;
4949
media: string;
5050

51+
emit: boolean;
52+
watch: boolean;
53+
preserveWatchOutput: boolean;
54+
5155
out: string;
5256
json: string;
5357

src/lib/utils/options/readers/arguments.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,6 @@ export class ArgumentsReader implements OptionsReader {
1616
}
1717

1818
read(container: Options, logger: Logger): void {
19-
logger.verbose(
20-
`Arguments reader reading with: ${JSON.stringify(this.args)}`
21-
);
22-
2319
// Make container's type more lax, we do the appropriate checks manually.
2420
const options = container as Options & {
2521
setValue(name: string, value: unknown): void;

src/lib/utils/options/sources/typedoc.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,23 @@ export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
8484
hint: ParameterHint.Directory,
8585
});
8686

87+
options.addDeclaration({
88+
name: "watch",
89+
help: "Watch files for changes and rebuild docs on change.",
90+
type: ParameterType.Boolean,
91+
});
92+
options.addDeclaration({
93+
name: "preserveWatchOutput",
94+
help:
95+
"If set, TypeDoc will not clear the screen between compilation runs.",
96+
type: ParameterType.Boolean,
97+
});
98+
options.addDeclaration({
99+
name: "emit",
100+
help: "If set, TypeDoc will emit the TypeScript compilation result",
101+
type: ParameterType.Boolean,
102+
});
103+
87104
options.addDeclaration({
88105
name: "out",
89106
help: "Specifies the location the documentation should be written to.",

0 commit comments

Comments
 (0)