UNPKG

typedoc

Version:

Create api documentation for TypeScript projects.

265 lines (264 loc) 9.51 kB
import { resolve } from "path"; import { ParameterType } from "./declaration.js"; import { insertOrderSorted, unique } from "../array.js"; import { convert, getDefaultValue, } from "./declaration.js"; import { addTypeDocOptions } from "./sources/index.js"; import { getOptionsHelp } from "./help.js"; import { getSimilarValues } from "../general.js"; const optionSnapshots = new WeakMap(); /** * Maintains a collection of option declarations split into TypeDoc options * and TypeScript options. Ensures options are of the correct type for calling * code. * * ### Option Discovery * * Since plugins commonly add custom options, and TypeDoc does not permit options which have * not been declared to be set, options must be read twice. The first time options are read, * a noop logger is passed so that any errors are ignored. Then, after loading plugins, options * are read again, this time with the logger specified by the application. * * Options are read in a specific order. * 1. argv (0) - Must be read first since it should change the files read when * passing --options or --tsconfig. * 2. typedoc-json (100) - Read next so that it can specify the tsconfig.json file to read. * 3. tsconfig-json (200) - Last config file reader, cannot specify the typedoc.json file to read. * 4. argv (300) - Read argv again since any options set there should override those set in config * files. * * @group Common * @summary Contains all of TypeDoc's option declarations & values */ export class Options { _readers = []; _declarations = new Map(); _values = {}; _setOptions = new Set(); _compilerOptions = {}; _fileNames = []; _projectReferences = []; _i18n; /** * In packages mode, the directory of the package being converted. */ packageDir; constructor(i18n) { this._i18n = i18n; addTypeDocOptions(this); } /** * Clones the options, intended for use in packages mode. */ copyForPackage(packageDir) { const options = new Options(this._i18n); options.packageDir = packageDir; options._readers = this._readers.filter((reader) => reader.supportsPackages); options._declarations = new Map(this._declarations); options.reset(); for (const [key, val] of Object.entries(this.getValue("packageOptions"))) { options.setValue(key, val, packageDir); } return options; } /** * Take a snapshot of option values now, used in tests only. * @internal */ snapshot() { const key = {}; optionSnapshots.set(key, { values: { ...this._values }, set: new Set(this._setOptions), }); return key; } /** * Take a snapshot of option values now, used in tests only. * @internal */ restore(snapshot) { const data = optionSnapshots.get(snapshot); this._values = { ...data.values }; this._setOptions = new Set(data.set); } reset(name) { if (name != null) { const declaration = this.getDeclaration(name); if (!declaration) { throw new Error(`Cannot reset an option (${name}) which has not been declared.`); } this._values[declaration.name] = getDefaultValue(declaration); this._setOptions.delete(declaration.name); } else { for (const declaration of this.getDeclarations()) { this._values[declaration.name] = getDefaultValue(declaration); } this._setOptions.clear(); this._compilerOptions = {}; this._fileNames = []; } } /** * Adds an option reader that will be used to read configuration values * from the command line, configuration files, or other locations. * @param reader */ addReader(reader) { insertOrderSorted(this._readers, reader); } async read(logger, cwd = process.cwd()) { for (const reader of this._readers) { await reader.read(this, logger, cwd); } } addDeclaration(declaration) { const decl = this.getDeclaration(declaration.name); if (decl) { throw new Error(`The option ${declaration.name} has already been registered`); } else { this._declarations.set(declaration.name, declaration); } this._values[declaration.name] = getDefaultValue(declaration); } /** * Gets a declaration by one of its names. * @param name */ getDeclaration(name) { return this._declarations.get(name); } /** * Gets all declared options. */ getDeclarations() { return unique(this._declarations.values()); } isSet(name) { if (!this._declarations.has(name)) { throw new Error(`Tried to check if an undefined option (${name}) was set`); } return this._setOptions.has(name); } /** * Gets all of the TypeDoc option values defined in this option container. */ getRawValues() { return this._values; } getValue(name) { const declaration = this.getDeclaration(name); if (!declaration) { const nearNames = this.getSimilarOptions(name); throw new Error(this._i18n.unknown_option_0_you_may_have_meant_1(name, nearNames.join("\n\t"))); } return this._values[declaration.name]; } setValue(name, value, configPath) { const declaration = this.getDeclaration(name); if (!declaration) { const nearNames = this.getSimilarOptions(name); throw new Error(this._i18n.unknown_option_0_you_may_have_meant_1(name, nearNames.join("\n\t"))); } let oldValue = this._values[declaration.name]; if (typeof oldValue === "undefined") { oldValue = getDefaultValue(declaration); } const converted = convert(value, declaration, this._i18n, configPath ?? process.cwd(), oldValue); if (declaration.type === ParameterType.Flags) { this._values[declaration.name] = Object.assign({}, this._values[declaration.name], converted); } else if (declaration.name === "outputs") { // This is very unfortunate... there's probably some smarter way to define options // so that this can be done intelligently via the convert function. this._values[declaration.name] = converted.map((c) => { return { ...c, path: resolve(configPath ?? process.cwd(), c.path), }; }); } else { this._values[declaration.name] = converted; } this._setOptions.add(name); } /** * Gets the set compiler options. */ getCompilerOptions() { return this.fixCompilerOptions(this._compilerOptions); } /** @internal */ fixCompilerOptions(options) { const overrides = this.getValue("compilerOptions"); const result = { ...options }; if (overrides) { Object.assign(result, overrides); } if (this.getValue("emit") !== "both") { result.noEmit = true; delete result.emitDeclarationOnly; } return result; } /** * Gets the file names discovered through reading a tsconfig file. */ getFileNames() { return this._fileNames; } /** * Gets the project references - used in solution style tsconfig setups. */ getProjectReferences() { return this._projectReferences; } /** * Sets the compiler options that will be used to get a TS program. */ setCompilerOptions(fileNames, options, projectReferences) { // We do this here instead of in the tsconfig reader so that API consumers which // supply a program to `Converter.convert` instead of letting TypeDoc create one // can just set the compiler options, and not need to know about this mapping. // It feels a bit like a hack... but it's better to have it here than to put it // in Application or Converter. if (options.stripInternal && !this.isSet("excludeInternal")) { this.setValue("excludeInternal", true); } this._fileNames = fileNames; this._compilerOptions = { ...options }; this._projectReferences = projectReferences ?? []; } /** * Discover similar option names to the given name, for use in error reporting. */ getSimilarOptions(missingName) { return getSimilarValues(this._declarations.keys(), missingName); } /** * Get the help message to be displayed to the user if `--help` is passed. */ getHelp(i18n) { return getOptionsHelp(this, i18n); } } /** * Binds an option to an accessor. Does not register the option. * * Note: This is a standard ES decorator. It will not work with pre-TS 5.0 experimental decorators enabled. */ export function Option(name) { return (_, _context) => { return { get() { const options = "options" in this ? this.options : this.application.options; return options.getValue(name); }, set(_value) { throw new Error(`Options may not be set via the Option decorator when setting ${name}`); }, }; }; }