UNPKG

typedoc

Version:

Create api documentation for TypeScript projects.

614 lines (613 loc) 30.9 kB
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 };