typedoc
Version:
Create api documentation for TypeScript projects.
614 lines (613 loc) • 30.9 kB
JavaScript
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
var _, done = false;
for (var i = decorators.length - 1; i >= 0; i--) {
var context = {};
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
if (kind === "accessor") {
if (result === void 0) continue;
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
if (_ = accept(result.get)) descriptor.get = _;
if (_ = accept(result.set)) descriptor.set = _;
if (_ = accept(result.init)) initializers.unshift(_);
}
else if (_ = accept(result)) {
if (kind === "field") initializers.unshift(_);
else descriptor[key] = _;
}
}
if (target) Object.defineProperty(target, contextIn.name, descriptor);
done = true;
};
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
var useValue = arguments.length > 2;
for (var i = 0; i < initializers.length; i++) {
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
}
return useValue ? value : void 0;
};
import * as Path from "path";
import ts from "typescript";
import { Deserializer, Serializer, } from "./serialization/index.js";
import { Converter } from "./converter/index.js";
import { Renderer } from "./output/renderer.js";
import { Logger, ConsoleLogger, loadPlugins, writeFile, TSConfigReader, TypeDocReader, PackageJsonReader, AbstractComponent, } from "./utils/index.js";
import { Options, Option } from "./utils/index.js";
import { unique } from "./utils/array.js";
import { ok } from "assert";
import { EntryPointStrategy, getEntryPoints, getPackageDirectories, getWatchEntryPoints, inferEntryPoints, } from "./utils/entry-point.js";
import { nicePath } from "./utils/paths.js";
import { getLoadedPaths, hasBeenLoadedMultipleTimes } from "./utils/general.js";
import { validateExports } from "./validation/exports.js";
import { validateDocumentation } from "./validation/documentation.js";
import { validateLinks } from "./validation/links.js";
import { ApplicationEvents } from "./application-events.js";
import { findTsConfigFile } from "./utils/tsconfig.js";
import { deriveRootDir, glob, readFile } from "./utils/fs.js";
import { addInferredDeclarationMapPaths } from "./models/reflections/ReflectionSymbolId.js";
import { Internationalization, } from "./internationalization/internationalization.js";
import { ValidatingFileRegistry, FileRegistry } from "./models/FileRegistry.js";
import { readFileSync } from "fs";
import { fileURLToPath } from "url";
import { createRequire } from "module";
import { Outputs } from "./output/output.js";
import { validateMergeModuleWith } from "./validation/unusedMergeModuleWith.js";
const packageInfo = JSON.parse(readFileSync(Path.join(fileURLToPath(import.meta.url), "../../../package.json"), "utf8"));
const supportedVersionMajorMinor = packageInfo.peerDependencies.typescript
.split("||")
.map((version) => version.replace(/^\s*|\.x\s*$/g, ""));
const DETECTOR = Symbol();
export function createAppForTesting() {
// @ts-expect-error private constructor
const app = new Application(DETECTOR);
app.files = new FileRegistry();
return app;
}
const DEFAULT_READERS = [
new TypeDocReader(),
new PackageJsonReader(),
new TSConfigReader(),
];
/**
* The default TypeDoc main application class.
*
* This class holds the two main components of TypeDoc, the {@link Converter} and
* the {@link Renderer}. When running TypeDoc, first the {@link Converter} is invoked which
* generates a {@link ProjectReflection} from the passed in source files. The
* {@link ProjectReflection} is a hierarchical model representation of the TypeScript
* project. Afterwards the model is passed to the {@link Renderer} which uses an instance
* of {@link Theme} to generate the final documentation.
*
* Both the {@link Converter} and the {@link Renderer} emit a series of events while processing the project.
* Subscribe to these Events to control the application flow or alter the output.
*
* @remarks
*
* Access to an Application instance can be retrieved with {@link Application.bootstrap} or
* {@link Application.bootstrapWithPlugins}. It can not be constructed manually.
*
* @group Common
* @summary Root level class which contains most useful behavior.
*/
let Application = (() => {
let _classSuper = AbstractComponent;
let _lang_decorators;
let _lang_initializers = [];
let _lang_extraInitializers = [];
let _skipErrorChecking_decorators;
let _skipErrorChecking_initializers = [];
let _skipErrorChecking_extraInitializers = [];
let _entryPointStrategy_decorators;
let _entryPointStrategy_initializers = [];
let _entryPointStrategy_extraInitializers = [];
let _entryPoints_decorators;
let _entryPoints_initializers = [];
let _entryPoints_extraInitializers = [];
return class Application extends _classSuper {
static {
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
_lang_decorators = [Option("lang")];
_skipErrorChecking_decorators = [Option("skipErrorChecking")];
_entryPointStrategy_decorators = [Option("entryPointStrategy")];
_entryPoints_decorators = [Option("entryPoints")];
__esDecorate(this, null, _lang_decorators, { kind: "accessor", name: "lang", static: false, private: false, access: { has: obj => "lang" in obj, get: obj => obj.lang, set: (obj, value) => { obj.lang = value; } }, metadata: _metadata }, _lang_initializers, _lang_extraInitializers);
__esDecorate(this, null, _skipErrorChecking_decorators, { kind: "accessor", name: "skipErrorChecking", static: false, private: false, access: { has: obj => "skipErrorChecking" in obj, get: obj => obj.skipErrorChecking, set: (obj, value) => { obj.skipErrorChecking = value; } }, metadata: _metadata }, _skipErrorChecking_initializers, _skipErrorChecking_extraInitializers);
__esDecorate(this, null, _entryPointStrategy_decorators, { kind: "accessor", name: "entryPointStrategy", static: false, private: false, access: { has: obj => "entryPointStrategy" in obj, get: obj => obj.entryPointStrategy, set: (obj, value) => { obj.entryPointStrategy = value; } }, metadata: _metadata }, _entryPointStrategy_initializers, _entryPointStrategy_extraInitializers);
__esDecorate(this, null, _entryPoints_decorators, { kind: "accessor", name: "entryPoints", static: false, private: false, access: { has: obj => "entryPoints" in obj, get: obj => obj.entryPoints, set: (obj, value) => { obj.entryPoints = value; } }, metadata: _metadata }, _entryPoints_initializers, _entryPoints_extraInitializers);
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
}
/**
* The converter used to create the declaration reflections.
*/
converter;
outputs = new Outputs(this);
/**
* The renderer used to generate the HTML documentation output.
*/
renderer;
/**
* The serializer used to generate JSON output.
*/
serializer = new Serializer();
/**
* The deserializer used to restore previously serialized JSON output.
*/
deserializer = new Deserializer(this);
/**
* The logger that should be used to output messages.
*/
logger = new ConsoleLogger();
/**
* Internationalization module which supports translating according to
* the `lang` option.
*/
internationalization = new Internationalization(this);
/**
* Proxy based shortcuts for internationalization keys.
*/
i18n = this.internationalization.proxy;
options = new Options(this.i18n);
files = new ValidatingFileRegistry();
#lang_accessor_storage = __runInitializers(this, _lang_initializers, void 0);
/** @internal */
get lang() { return this.#lang_accessor_storage; }
set lang(value) { this.#lang_accessor_storage = value; }
#skipErrorChecking_accessor_storage = (__runInitializers(this, _lang_extraInitializers), __runInitializers(this, _skipErrorChecking_initializers, void 0));
/** @internal */
get skipErrorChecking() { return this.#skipErrorChecking_accessor_storage; }
set skipErrorChecking(value) { this.#skipErrorChecking_accessor_storage = value; }
#entryPointStrategy_accessor_storage = (__runInitializers(this, _skipErrorChecking_extraInitializers), __runInitializers(this, _entryPointStrategy_initializers, void 0));
/** @internal */
get entryPointStrategy() { return this.#entryPointStrategy_accessor_storage; }
set entryPointStrategy(value) { this.#entryPointStrategy_accessor_storage = value; }
#entryPoints_accessor_storage = (__runInitializers(this, _entryPointStrategy_extraInitializers), __runInitializers(this, _entryPoints_initializers, void 0));
/** @internal */
get entryPoints() { return this.#entryPoints_accessor_storage; }
set entryPoints(value) { this.#entryPoints_accessor_storage = value; }
/**
* The version number of TypeDoc.
*/
static VERSION = packageInfo.version;
/**
* Emitted after plugins have been loaded and options have been read, but before they have been frozen.
* The listener will be given an instance of {@link Application}.
*/
static EVENT_BOOTSTRAP_END = ApplicationEvents.BOOTSTRAP_END;
/**
* Emitted after a project has been deserialized from JSON.
* The listener will be given an instance of {@link ProjectReflection}.
*/
static EVENT_PROJECT_REVIVE = ApplicationEvents.REVIVE;
/**
* Emitted when validation is being run.
* The listener will be given an instance of {@link ProjectReflection}.
*/
static EVENT_VALIDATE_PROJECT = ApplicationEvents.VALIDATE_PROJECT;
/**
* Create a new TypeDoc application instance.
*/
constructor(detector) {
if (detector !== DETECTOR) {
throw new Error("An application handle must be retrieved with Application.bootstrap or Application.bootstrapWithPlugins");
}
super(null); // We own ourselves
__runInitializers(this, _entryPoints_extraInitializers);
this.converter = new Converter(this);
this.renderer = new Renderer(this);
this.logger.i18n = this.i18n;
this.outputs.addOutput("json", async (out, project) => {
const ser = this.serializer.projectToObject(project, process.cwd());
const space = this.options.getValue("pretty") ? "\t" : "";
await writeFile(out, JSON.stringify(ser, null, space) + "\n");
});
this.outputs.addOutput("html", async (out, project) => {
await this.renderer.render(project, out);
});
}
/**
* Initialize TypeDoc, loading plugins if applicable.
*/
static async bootstrapWithPlugins(options = {}, readers = DEFAULT_READERS) {
const app = new Application(DETECTOR);
readers.forEach((r) => app.options.addReader(r));
app.options.reset();
app.setOptions(options, /* reportErrors */ false);
await app.options.read(new Logger());
app.logger.level = app.options.getValue("logLevel");
await loadPlugins(app, app.options.getValue("plugin"));
await app._bootstrap(options);
return app;
}
/**
* Initialize TypeDoc without loading plugins.
*
* @example
* Initialize the application with pretty-printing output disabled.
* ```ts
* const app = Application.bootstrap({ pretty: false });
* ```
*
* @param options Options to set during initialization
* @param readers Option readers to use to discover options from config files.
*/
static async bootstrap(options = {}, readers = DEFAULT_READERS) {
const app = new Application(DETECTOR);
readers.forEach((r) => app.options.addReader(r));
await app._bootstrap(options);
return app;
}
async _bootstrap(options) {
this.options.reset();
this.setOptions(options, /* reportErrors */ false);
await this.options.read(this.logger);
this.setOptions(options);
this.logger.level = this.options.getValue("logLevel");
for (const [lang, locales] of Object.entries(this.options.getValue("locales"))) {
this.internationalization.addTranslations(lang, locales);
}
if (hasBeenLoadedMultipleTimes()) {
this.logger.warn(this.i18n.loaded_multiple_times_0(getLoadedPaths().join("\n\t")));
}
this.trigger(ApplicationEvents.BOOTSTRAP_END, this);
if (!this.internationalization.hasTranslations(this.lang)) {
// Not internationalized as by definition we don't know what to include here.
this.logger.warn(`Options specified "${this.lang}" as the language to use, but TypeDoc does not support it.`);
this.logger.info(("The supported languages are:\n\t" +
this.internationalization
.getSupportedLanguages()
.join("\n\t")));
this.logger.info("You can define/override local locales with the `locales` option, or contribute them to TypeDoc!");
}
if (this.options.getValue("useHostedBaseUrlForAbsoluteLinks") &&
!this.options.getValue("hostedBaseUrl")) {
this.logger.warn(this.i18n.useHostedBaseUrlForAbsoluteLinks_requires_hostedBaseUrl());
this.options.setValue("useHostedBaseUrlForAbsoluteLinks", false);
}
}
/** @internal */
setOptions(options, reportErrors = true) {
let success = true;
for (const [key, val] of Object.entries(options)) {
try {
this.options.setValue(key, val);
}
catch (error) {
success = false;
ok(error instanceof Error);
if (reportErrors) {
this.logger.error(error.message);
}
}
}
return success;
}
/**
* Return the path to the TypeScript compiler.
*/
getTypeScriptPath() {
const req = createRequire(import.meta.url);
return nicePath(Path.dirname(req.resolve("typescript")));
}
getTypeScriptVersion() {
return ts.version;
}
getEntryPoints() {
if (this.options.isSet("entryPoints")) {
return this.getDefinedEntryPoints();
}
return inferEntryPoints(this.logger, this.options);
}
/**
* Gets the entry points to be documented according to the current `entryPoints` and `entryPointStrategy` options.
* May return undefined if entry points fail to be expanded.
*/
getDefinedEntryPoints() {
return getEntryPoints(this.logger, this.options);
}
/**
* Run the converter for the given set of files and return the generated reflections.
*
* @returns An instance of ProjectReflection on success, undefined otherwise.
*/
async convert() {
const start = Date.now();
this.logger.verbose(`Using TypeScript ${this.getTypeScriptVersion()} from ${this.getTypeScriptPath()}`);
if (this.entryPointStrategy === EntryPointStrategy.Merge) {
return this._merge();
}
if (this.entryPointStrategy === EntryPointStrategy.Packages) {
return this._convertPackages();
}
if (!supportedVersionMajorMinor.some((version) => version == ts.versionMajorMinor)) {
this.logger.warn(this.i18n.unsupported_ts_version_0(supportedVersionMajorMinor.join(", ")));
}
const entryPoints = this.getEntryPoints();
if (!entryPoints) {
// Fatal error already reported.
return;
}
const programs = unique(entryPoints.map((e) => e.program));
this.logger.verbose(`Converting with ${programs.length} programs ${entryPoints.length} entry points`);
if (this.skipErrorChecking === false) {
const errors = programs.flatMap((program) => ts.getPreEmitDiagnostics(program));
if (errors.length) {
this.logger.diagnostics(errors);
return;
}
}
if (this.options.getValue("emit") === "both") {
for (const program of programs) {
program.emit();
}
}
const startConversion = Date.now();
this.logger.verbose(`Finished getting entry points in ${Date.now() - start}ms`);
const project = this.converter.convert(entryPoints);
this.logger.verbose(`Finished conversion in ${Date.now() - startConversion}ms`);
return project;
}
convertAndWatch(success) {
if (!this.options.getValue("preserveWatchOutput") &&
this.logger instanceof ConsoleLogger) {
ts.sys.clearScreen?.();
}
this.logger.verbose(`Using TypeScript ${this.getTypeScriptVersion()} from ${this.getTypeScriptPath()}`);
if (!supportedVersionMajorMinor.some((version) => version == ts.versionMajorMinor)) {
this.logger.warn(this.i18n.unsupported_ts_version_0(supportedVersionMajorMinor.join(", ")));
}
if (Object.keys(this.options.getCompilerOptions()).length === 0) {
this.logger.warn(this.i18n.no_compiler_options_set());
}
// 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.options.getFileNames().length === 0) {
this.logger.error(this.i18n.solution_not_supported_in_watch_mode());
return;
}
// Support for packages mode is currently unimplemented
if (this.entryPointStrategy !== EntryPointStrategy.Resolve &&
this.entryPointStrategy !== EntryPointStrategy.Expand) {
this.logger.error(this.i18n.strategy_not_supported_in_watch_mode());
return;
}
const tsconfigFile = findTsConfigFile(this.options.getValue("tsconfig")) ??
"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, {}, 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.info(ts.flattenDiagnosticMessageText(status.messageText, newLine));
});
let successFinished = true;
let currentProgram;
const runSuccess = () => {
if (!currentProgram) {
return;
}
if (successFinished) {
if (this.options.getValue("emit") === "both") {
currentProgram.emit();
}
this.logger.resetErrors();
this.logger.resetWarnings();
const entryPoints = getWatchEntryPoints(this.logger, this.options, currentProgram);
if (!entryPoints) {
return;
}
const project = this.converter.convert(entryPoints);
currentProgram = undefined;
successFinished = false;
void success(project).then(() => {
successFinished = true;
runSuccess();
});
}
};
const origCreateProgram = host.createProgram;
host.createProgram = (rootNames, options, host, oldProgram, configDiagnostics, references) => {
// If we always do this, we'll get a crash the second time a program is created.
if (rootNames !== undefined) {
options = this.options.fixCompilerOptions(options || {});
}
return origCreateProgram(rootNames, options, host, oldProgram, configDiagnostics, references);
};
const origAfterProgramCreate = host.afterProgramCreate;
host.afterProgramCreate = (program) => {
if (ts.getPreEmitDiagnostics(program.getProgram()).length === 0) {
currentProgram = program.getProgram();
runSuccess();
}
origAfterProgramCreate?.(program);
};
ts.createWatchProgram(host);
}
validate(project) {
const checks = this.options.getValue("validation");
const start = Date.now();
// No point in validating exports when merging. Warnings will have already been emitted when
// creating the project jsons that this run merges together.
if (checks.notExported &&
this.entryPointStrategy !== EntryPointStrategy.Merge) {
validateExports(project, this.logger, this.options.getValue("intentionallyNotExported"));
}
if (checks.notDocumented) {
validateDocumentation(project, this.logger, this.options.getValue("requiredToBeDocumented"));
}
if (checks.invalidLink) {
validateLinks(project, this.logger);
}
if (checks.unusedMergeModuleWith) {
validateMergeModuleWith(project, this.logger);
}
this.trigger(Application.EVENT_VALIDATE_PROJECT, project);
this.logger.verbose(`Validation took ${Date.now() - start}ms`);
}
/**
* Render outputs selected with options for the specified project
*/
async generateOutputs(project) {
await this.outputs.writeOutputs(project);
}
/**
* Render HTML for the given project
*/
async generateDocs(project, out) {
await this.outputs.writeOutput({
name: "html",
path: out,
}, project);
}
/**
* Write the reflections to a json file.
*
* @param out The path and file name of the target file.
* @returns Whether the JSON file could be written successfully.
*/
async generateJson(project, out) {
await this.outputs.writeOutput({
name: "json",
path: out,
}, project);
}
/**
* Print the version number.
*/
toString() {
return [
"",
`TypeDoc ${Application.VERSION}`,
`Using TypeScript ${this.getTypeScriptVersion()} from ${this.getTypeScriptPath()}`,
"",
].join("\n");
}
async _convertPackages() {
if (!this.options.isSet("entryPoints")) {
this.logger.error(this.i18n.no_entry_points_for_packages());
return;
}
const packageDirs = getPackageDirectories(this.logger, this.options, this.options.getValue("entryPoints"));
if (packageDirs.length === 0) {
this.logger.error(this.i18n.failed_to_find_packages());
return;
}
const origFiles = this.files;
const origOptions = this.options;
const projects = [];
const projectsToConvert = [];
// Generate a json file for each package
for (const dir of packageDirs) {
this.logger.verbose(`Reading project at ${nicePath(dir)}`);
let opts;
try {
opts = origOptions.copyForPackage(dir);
}
catch (error) {
ok(error instanceof Error);
this.logger.error(error.message);
this.logger.info(this.i18n.previous_error_occurred_when_reading_options_for_0(nicePath(dir)));
continue;
}
await opts.read(this.logger, dir);
// Invalid links should only be reported after everything has been merged.
// Same goes for @mergeModuleWith, should only be validated after merging
// everything together.
opts.setValue("validation", {
invalidLink: false,
unusedMergeModuleWith: false,
});
if (opts.getValue("entryPointStrategy") ===
EntryPointStrategy.Packages) {
this.logger.error(this.i18n.nested_packages_unsupported_0(nicePath(dir)));
continue;
}
addInferredDeclarationMapPaths(opts.getCompilerOptions(), opts.getFileNames());
projectsToConvert.push({ dir, options: opts });
}
for (const { dir, options } of projectsToConvert) {
this.logger.info(this.i18n.converting_project_at_0(nicePath(dir)));
this.options = options;
this.files = new ValidatingFileRegistry();
let project = await this.convert();
if (project) {
this.validate(project);
const serialized = this.serializer.projectToObject(project, process.cwd());
projects.push(serialized);
}
// When debugging memory issues, it's useful to set these
// here so that a breakpoint on the continue statement below
// gets the memory as it ought to be with all TS objects released.
project = undefined;
this.files = undefined;
// global.gc!();
continue;
}
this.options = origOptions;
this.files = origFiles;
if (projects.length !== packageDirs.length) {
this.logger.error(this.i18n.failed_to_convert_packages());
return;
}
this.logger.info(this.i18n.merging_converted_projects());
const result = this.deserializer.reviveProjects(this.options.getValue("name") || "Documentation", projects, {
projectRoot: process.cwd(),
registry: this.files,
addProjectDocuments: true,
});
this.trigger(ApplicationEvents.REVIVE, result);
return result;
}
_merge() {
const start = Date.now();
if (!this.options.isSet("entryPoints")) {
this.logger.error(this.i18n.no_entry_points_to_merge());
return;
}
const rootDir = deriveRootDir(this.entryPoints);
const entryPoints = this.entryPoints.flatMap((entry) => {
const result = glob(entry, rootDir);
if (result.length === 0) {
this.logger.warn(this.i18n.entrypoint_did_not_match_files_0(nicePath(entry)));
}
else if (result.length !== 1) {
this.logger.verbose(`Expanded ${nicePath(entry)} to:\n\t${result
.map(nicePath)
.join("\n\t")}`);
}
return result;
});
const jsonProjects = entryPoints.map((path) => {
try {
return JSON.parse(readFile(path));
}
catch {
this.logger.error(this.i18n.failed_to_parse_json_0(nicePath(path)));
return null;
}
});
if (this.logger.hasErrors())
return;
const result = this.deserializer.reviveProjects(this.options.getValue("name"), jsonProjects, {
projectRoot: process.cwd(),
registry: this.files,
addProjectDocuments: true,
});
this.logger.verbose(`Reviving projects took ${Date.now() - start}ms`);
this.trigger(ApplicationEvents.REVIVE, result);
return result;
}
};
})();
export { Application };