typedoc
Version:
Create api documentation for TypeScript projects.
173 lines (172 loc) • 6.8 kB
JavaScript
import { resolve, join, dirname } from "path";
import ts from "typescript";
import { isFile } from "../../fs.js";
import { ok } from "assert";
import { additionalProperties, isTagString, optional, validate, } from "../../validation.js";
import { nicePath, normalizePath } from "../../paths.js";
import { createRequire } from "module";
import { tsdocBlockTags, tsdocInlineTags, tsdocModifierTags, } from "../tsdoc-defaults.js";
import { unique } from "../../array.js";
import { findTsConfigFile, getTypeDocOptionsFromTsConfig, readTsConfig, } from "../../tsconfig.js";
function isSupportForTags(obj) {
return (validate({}, obj) &&
Object.entries(obj).every(([key, val]) => {
return (/^@[a-zA-Z][a-zA-Z0-9]*$/.test(key) && typeof val === "boolean");
}));
}
const tsDocSchema = {
$schema: optional(String),
extends: optional([Array, String]),
noStandardTags: optional(Boolean),
tagDefinitions: optional([
Array,
{
tagName: isTagString,
syntaxKind: ["inline", "block", "modifier"],
allowMultiple: optional(Boolean),
[additionalProperties]: false,
},
]),
supportForTags: optional(isSupportForTags),
// The official parser has code to support for these two, but
// the schema doesn't allow them... just silently ignore them for now.
supportedHtmlElements: optional({}),
reportUnsupportedHtmlElements: optional(Boolean),
[additionalProperties]: false,
};
export class TSConfigReader {
/**
* Note: Runs after the {@link TypeDocReader}.
*/
order = 200;
name = "tsconfig-json";
supportsPackages = true;
seenTsdocPaths = new Set();
read(container, logger, cwd) {
const file = container.getValue("tsconfig") || cwd;
let fileToRead = findTsConfigFile(file);
if (!fileToRead) {
// If the user didn't give us this option, we shouldn't complain about not being able to find it.
if (container.isSet("tsconfig")) {
logger.error(logger.i18n.tsconfig_file_0_does_not_exist(nicePath(file)));
}
return;
}
fileToRead = normalizePath(resolve(fileToRead));
logger.verbose(`Reading tsconfig at ${nicePath(fileToRead)}`);
this.addTagsFromTsdocJson(container, logger, resolve(fileToRead));
const parsed = readTsConfig(fileToRead, logger);
if (!parsed) {
return;
}
logger.diagnostics(parsed.errors);
if (parsed.errors.length) {
return;
}
const typedocOptions = getTypeDocOptionsFromTsConfig(fileToRead);
if (typedocOptions.options) {
logger.error(logger.i18n.tsconfig_file_specifies_options_file());
delete typedocOptions.options;
}
if (typedocOptions.tsconfig) {
logger.error(logger.i18n.tsconfig_file_specifies_tsconfig_file());
delete typedocOptions.tsconfig;
}
container.setCompilerOptions(parsed.fileNames, parsed.options, parsed.projectReferences);
for (const [key, val] of Object.entries(typedocOptions || {})) {
try {
// We catch the error, so can ignore the strict type checks
container.setValue(key, val, join(fileToRead, ".."));
}
catch (error) {
ok(error instanceof Error);
logger.error(error.message);
}
}
}
addTagsFromTsdocJson(container, logger, tsconfig) {
this.seenTsdocPaths.clear();
const tsdoc = join(dirname(tsconfig), "tsdoc.json");
if (!isFile(tsdoc)) {
return;
}
const overwritten = ["blockTags", "inlineTags", "modifierTags"].filter((opt) => container.isSet(opt));
if (overwritten.length) {
logger.warn(logger.i18n.tags_0_defined_in_typedoc_json_overwritten_by_tsdoc_json(overwritten.join(", ")));
}
const config = this.readTsDoc(logger, tsdoc);
if (!config)
return;
const supported = (tag) => {
return config.supportForTags
? !!config.supportForTags[tag.tagName]
: true;
};
const blockTags = [];
const inlineTags = [];
const modifierTags = [];
if (!config.noStandardTags) {
blockTags.push(...tsdocBlockTags);
inlineTags.push(...tsdocInlineTags);
modifierTags.push(...tsdocModifierTags);
}
for (const { tagName, syntaxKind } of config.tagDefinitions?.filter(supported) || []) {
const arr = {
block: blockTags,
inline: inlineTags,
modifier: modifierTags,
}[syntaxKind];
arr.push(tagName);
}
container.setValue("blockTags", unique(blockTags));
container.setValue("inlineTags", unique(inlineTags));
container.setValue("modifierTags", unique(modifierTags));
}
readTsDoc(logger, path) {
if (this.seenTsdocPaths.has(path)) {
logger.error(logger.i18n.circular_reference_extends_0(nicePath(path)));
return;
}
this.seenTsdocPaths.add(path);
const { config, error } = ts.readConfigFile(normalizePath(path), ts.sys.readFile);
if (error) {
logger.error(logger.i18n.failed_read_tsdoc_json_0(nicePath(path)));
return;
}
if (!validate(tsDocSchema, config)) {
logger.error(logger.i18n.invalid_tsdoc_json_0(nicePath(path)));
return;
}
const workingConfig = {};
if (config.extends) {
const resolver = createRequire(path);
for (const extendedPath of config.extends) {
let resolvedPath;
try {
resolvedPath = resolver.resolve(extendedPath);
}
catch {
logger.error(logger.i18n.failed_resolve_0_to_file_in_1(extendedPath, nicePath(path)));
return;
}
const parentConfig = this.readTsDoc(logger, resolvedPath);
if (!parentConfig)
return;
mergeConfigs(parentConfig, workingConfig);
}
}
mergeConfigs(config, workingConfig);
return workingConfig;
}
}
function mergeConfigs(from, into) {
if (from.supportForTags) {
into.supportForTags ||= {};
Object.assign(into.supportForTags, from.supportForTags);
}
if (from.tagDefinitions) {
into.tagDefinitions ||= [];
into.tagDefinitions.push(...from.tagDefinitions);
}
into.noStandardTags = from.noStandardTags ?? into.noStandardTags;
}