diff --git a/bin/typedoc b/bin/typedoc index 4a26d5636..dad5e7ed1 100755 --- a/bin/typedoc +++ b/bin/typedoc @@ -58,6 +58,24 @@ async function run(app) { return ExitCodes.NoEntryPoints; } + if (app.options.getValue("watch")) { + app.convertAndWatch(async (project) => { + const out = app.options.getValue("out"); + if (out) { + await app.generateDocs(project, out); + } + const json = app.options.getValue("json"); + if (json) { + await app.generateJson(project, json); + } + + if (!out && !json) { + await app.generateDocs(project, "./docs"); + } + }); + return ExitCodes.Ok; + } + const project = app.convert(); if (!project) { return ExitCodes.CompileError; diff --git a/src/lib/application.ts b/src/lib/application.ts index 437aa052c..5f6493154 100644 --- a/src/lib/application.ts +++ b/src/lib/application.ts @@ -24,6 +24,7 @@ import { import { Options, BindOption } from "./utils"; import { TypeDocOptions } from "./utils/options/declaration"; import { flatMap } from "./utils/array"; +import { basename } from "path"; // eslint-disable-next-line @typescript-eslint/no-var-requires const packageInfo = require("../../package.json") as { @@ -238,12 +239,133 @@ export class Application extends ChildableComponent< return; } + if (this.application.options.getValue("emit")) { + for (const program of programs) { + program.emit(); + } + } + return this.converter.convert( this.expandInputFiles(this.entryPoints), programs ); } + public convertAndWatch( + success: (project: ProjectReflection) => Promise + ): void { + if ( + !this.options.getValue("preserveWatchOutput") && + this.logger instanceof ConsoleLogger + ) { + ts.sys.clearScreen?.(); + } + + this.logger.verbose( + "Using TypeScript %s from %s", + this.getTypeScriptVersion(), + this.getTypeScriptPath() + ); + + if ( + !supportedVersionMajorMinor.some( + (version) => version == ts.versionMajorMinor + ) + ) { + this.logger.warn( + `You are running with an unsupported TypeScript version! TypeDoc supports ${supportedVersionMajorMinor.join( + ", " + )}` + ); + } + + if (Object.keys(this.options.getCompilerOptions()).length === 0) { + this.logger.warn( + `No compiler options set. This likely means that TypeDoc did not find your tsconfig.json. Generated documentation will probably be empty.` + ); + } + + // Doing this is considerably more complicated, we'd need to manage an array of programs, not convert until all programs + // have reported in the first time... just error out for now. I'm not convinced anyone will actually notice. + if (this.application.options.getFileNames().length === 0) { + this.logger.error( + "The provided tsconfig file looks like a solution style tsconfig, which is not supported in watch mode." + ); + return; + } + + // Matches the behavior of the tsconfig option reader. + let tsconfigFile = this.options.getValue("tsconfig"); + tsconfigFile = + ts.findConfigFile( + tsconfigFile, + ts.sys.fileExists, + tsconfigFile.toLowerCase().endsWith(".json") + ? basename(tsconfigFile) + : undefined + ) ?? "tsconfig.json"; + + // We don't want to do it the first time to preserve initial debug status messages. They'll be lost + // after the user saves a file, but better than nothing... + let firstStatusReport = true; + + const host = ts.createWatchCompilerHost( + tsconfigFile, + { noEmit: !this.application.options.getValue("emit") }, + ts.sys, + ts.createEmitAndSemanticDiagnosticsBuilderProgram, + (diagnostic) => this.logger.diagnostic(diagnostic), + (status, newLine, _options, errorCount) => { + if ( + !firstStatusReport && + errorCount === void 0 && + !this.options.getValue("preserveWatchOutput") && + this.logger instanceof ConsoleLogger + ) { + ts.sys.clearScreen?.(); + } + firstStatusReport = false; + this.logger.write( + ts.flattenDiagnosticMessageText(status.messageText, newLine) + ); + } + ); + + let successFinished = true; + let currentProgram: ts.Program | undefined; + + const runSuccess = () => { + if (!currentProgram) { + return; + } + + if (successFinished) { + this.logger.resetErrors(); + const project = this.converter.convert( + this.expandInputFiles(this.entryPoints), + currentProgram + ); + currentProgram = undefined; + successFinished = false; + success(project).then(() => { + successFinished = true; + runSuccess(); + }); + } + }; + + const origAfterProgramCreate = host.afterProgramCreate; + host.afterProgramCreate = (program) => { + if (ts.getPreEmitDiagnostics(program.getProgram()).length === 0) { + currentProgram = program.getProgram(); + runSuccess(); + } + origAfterProgramCreate?.(program); + }; + + ts.createWatchProgram(host); + } + /** * Render HTML for the given project */ diff --git a/src/lib/converter/converter.ts b/src/lib/converter/converter.ts index 9eaf97525..7ab0d5025 100644 --- a/src/lib/converter/converter.ts +++ b/src/lib/converter/converter.ts @@ -139,7 +139,7 @@ export class Converter extends ChildableComponent< convert( entryPoints: readonly string[], programs: ts.Program | readonly ts.Program[] - ): ProjectReflection | undefined { + ): ProjectReflection { programs = programs instanceof Array ? programs : [programs]; this.externalPatternCache = void 0; diff --git a/src/lib/utils/options/declaration.ts b/src/lib/utils/options/declaration.ts index c182cdcd9..671ea192b 100644 --- a/src/lib/utils/options/declaration.ts +++ b/src/lib/utils/options/declaration.ts @@ -48,6 +48,10 @@ export interface TypeDocOptionMap { includes: string; media: string; + emit: boolean; + watch: boolean; + preserveWatchOutput: boolean; + out: string; json: string; diff --git a/src/lib/utils/options/readers/arguments.ts b/src/lib/utils/options/readers/arguments.ts index 52e1548e0..75845f5a8 100644 --- a/src/lib/utils/options/readers/arguments.ts +++ b/src/lib/utils/options/readers/arguments.ts @@ -16,10 +16,6 @@ export class ArgumentsReader implements OptionsReader { } read(container: Options, logger: Logger): void { - logger.verbose( - `Arguments reader reading with: ${JSON.stringify(this.args)}` - ); - // Make container's type more lax, we do the appropriate checks manually. const options = container as Options & { setValue(name: string, value: unknown): void; diff --git a/src/lib/utils/options/sources/typedoc.ts b/src/lib/utils/options/sources/typedoc.ts index 68ea095fd..cd848660b 100644 --- a/src/lib/utils/options/sources/typedoc.ts +++ b/src/lib/utils/options/sources/typedoc.ts @@ -84,6 +84,23 @@ export function addTypeDocOptions(options: Pick) { hint: ParameterHint.Directory, }); + options.addDeclaration({ + name: "watch", + help: "Watch files for changes and rebuild docs on change.", + type: ParameterType.Boolean, + }); + options.addDeclaration({ + name: "preserveWatchOutput", + help: + "If set, TypeDoc will not clear the screen between compilation runs.", + type: ParameterType.Boolean, + }); + options.addDeclaration({ + name: "emit", + help: "If set, TypeDoc will emit the TypeScript compilation result", + type: ParameterType.Boolean, + }); + options.addDeclaration({ name: "out", help: "Specifies the location the documentation should be written to.",