typedoc
Version:
Create api documentation for TypeScript projects.
265 lines (264 loc) • 9.51 kB
JavaScript
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}`);
},
};
};
}