typedoc-plugin-missing-exports
Version:
Include non-exported types in TypeDoc documentation
289 lines • 13.7 kB
JavaScript
import { relative } from "path";
import { Application, Context, Converter, JSX, ParameterType, ReflectionKind, ReflectionSymbolId, Renderer, TypeScript as ts, } from "typedoc";
let hasMonkeyPatched = false;
const ModuleLike = ReflectionKind.Project | ReflectionKind.Module;
const InternalModule = Symbol();
const HOOK_JS = `
<script>for (let k in localStorage) if (k.includes("tsd-accordion-") && k.includes(NAME)) localStorage.setItem(k, "false");</script>
`.trim();
export function load(app) {
if (hasMonkeyPatched) {
throw new Error("typedoc-plugin-missing-exports cannot be loaded multiple times");
}
hasMonkeyPatched = true;
const referencedSymbols = new Map();
const symbolToOwningModule = new Map();
const knownPrograms = new Map();
const symbolIdKeyToSymbol = new Map();
/**
* Extracts the set of referenced but export-missing symbols owned by a particular module.
*
* > [!CAUTION]
* > This function is not pure. The returned symbols are removed from the "referenced" set and will not be returned again.
*/
function discoverMissingExports(owningModule, context, program) {
// An export is missing if it was referenced and
// is not contained in the documentation
const referenced = referencedSymbols.get(program) || new Set();
const ownedByOther = new Set();
referencedSymbols.set(program, ownedByOther);
for (const s of [...referenced]) {
if (context.getReflectionFromSymbol(s)) {
referenced.delete(s);
}
else if (symbolToOwningModule.get(s) !== owningModule) {
referenced.delete(s);
ownedByOther.add(s);
}
}
return referenced;
}
/**
* Adds a symbol to the set of referenced (but not necessarily unexported) symbols in the provided {@link Context}.
*/
function markSymbolReferenced(symbol, owningModule, program) {
const set = referencedSymbols.get(program);
symbolToOwningModule.set(symbol, owningModule);
if (set) {
set.add(symbol);
}
else {
referencedSymbols.set(program, new Set([symbol]));
}
}
// Monkey patch the constructor for references so that we can inspect them.
const origCreateSymbolReference = Context.prototype.createSymbolReference;
Context.prototype.createSymbolReference = function (symbol, context, name) {
const owningModule = getOwningModule(context);
markSymbolReferenced(symbol, owningModule, context.program);
return origCreateSymbolReference.call(this, symbol, context, name);
};
const origCreateSymbolId = Context.prototype.createSymbolId;
Context.prototype.createSymbolId = function (symbol, declaration) {
// We don't immediately mark the symbol as referenced here because we
// don't want to include referenced symbols from comments which will
// be excluded by the excludeTags option.
const id = origCreateSymbolId.call(this, symbol, declaration);
symbolIdKeyToSymbol.set(id.getStableKey(), symbol);
return id;
};
app.options.addDeclaration({
name: "internalModule",
help: "[typedoc-plugin-missing-exports] Define the name of the module that internal symbols which are not exported should be placed into.",
defaultValue: "<internal>",
});
app.options.addDeclaration({
name: "collapseInternalModule",
help: "[typedoc-plugin-missing-exports] Include JS in the page to collapse all <internal> entries in the navigation on page load.",
defaultValue: false,
type: ParameterType.Boolean,
});
app.options.addDeclaration({
name: "placeInternalsInOwningModule",
help: "[typedoc-plugin-missing-exports] If set internal symbols will not be placed into an internals module, but directly into the module which references them.",
defaultValue: false,
type: ParameterType.Boolean,
});
app.options.addDeclaration({
name: "includeDocCommentReferences",
help: "[typedoc-plugin-missing-exports] If set, also export private symbols only referenced via documentation comments using the @link family of tags.",
defaultValue: false,
type: ParameterType.Boolean,
});
app.converter.on(Converter.EVENT_BEGIN, () => {
if (app.options.getValue("placeInternalsInOwningModule")
&& app.options.isSet("internalModule")) {
app.logger.warn(`[typedoc-plugin-missing-exports] Both placeInternalsInOwningModule and internalModule are set, the internalModule option will be ignored.`);
}
if (app.options.getValue("includeDocCommentReferences") && Number(Application.VERSION.slice(5)) < 9) {
app.logger.warn(`[typedoc-plugin-missing-exports] The includeDocCommentReferences option requires TypeDoc 0.28.9 or later.`);
}
});
function addCommentReferencedSymbols(context, refl) {
if (app.options.getValue("includeDocCommentReferences")) {
visitReflectionLinkTags(refl, app.options.getValue("excludeTags"), part => {
const symbol = part.target instanceof ReflectionSymbolId && symbolIdKeyToSymbol.get(part.target.getStableKey());
if (symbol) {
markSymbolReferenced(symbol, getOwningModule(context), context.program);
}
});
}
}
app.converter.on(Converter.EVENT_CREATE_DECLARATION, (context, refl) => {
// TypeDoc 0.26+ doesn't fire EVENT_CREATE_DECLARATION for project
// We need to ensure the project has a program attached to it, so
// do that when the first declaration is created.
if (knownPrograms.size === 0) {
knownPrograms.set(refl.project, context.program);
}
if (refl.kindOf(ModuleLike)) {
knownPrograms.set(refl, context.program);
}
// #12 - This plugin might cause TypeDoc to convert some module without
// an export symbol to give it a name other than the full absolute
// path to the symbol. Detect this and rename it to a relative path
// based on base path if specified or CWD.
const symbol = context.getSymbolFromReflection(refl);
const file = symbol?.declarations?.find(ts.isSourceFile);
if (file && /^".*"$/.test(refl.name)) {
refl.name = getModuleName(file.fileName, app.options.getValue("basePath") || process.cwd());
}
addCommentReferencedSymbols(context, refl);
});
app.converter.on(Converter.EVENT_CREATE_SIGNATURE, (context, sig) => {
addCommentReferencedSymbols(context, sig);
});
app.converter.on(Converter.EVENT_RESOLVE_BEGIN, function onResolveBegin(context) {
const modules = context.project.getChildrenByKind(ReflectionKind.Module);
if (modules.length === 0) {
// Single entry point, just target the project.
modules.push(context.project);
}
let first = true;
for (const mod of modules) {
const program = knownPrograms.get(mod);
if (!program)
continue;
// Nasty hack here that will almost certainly break in future TypeDoc versions.
// Do this before discovering missing exports as discovering missing exports will
// create symbol IDs, which requires a program due to our override of the method
// on TypeDoc's Context.
context.setActiveProgram(program);
if (first) {
// We have to do this here because we rely on there being an active program,
// but there isn't any active program during the CREATE_PROJECT event.
addCommentReferencedSymbols(context, context.project);
first = false;
}
let missing = discoverMissingExports(mod, context, program);
if (!missing.size)
continue;
let internalContext;
if (app.options.getValue("placeInternalsInOwningModule")) {
internalContext = context.withScope(mod);
}
else {
const internalNs = context
.withScope(mod)
.createDeclarationReflection(ReflectionKind.Module, void 0, void 0, app.options.getValue("internalModule"));
internalNs[InternalModule] = true;
context.finalizeDeclarationReflection(internalNs);
internalContext = context.withScope(mod).withScope(internalNs);
}
// Keep track of which symbols we've tried to convert. If they don't get converted
// when calling convertSymbol, then the user has excluded them somehow, don't go into
// an infinite loop when converting.
const tried = new Set();
// Any re-exports will be deferred, so we need to allow deferred conversion here
// and finalize it after the loop.
app.converter.permitDeferredConversion();
do {
for (const s of missing) {
if (shouldConvertSymbol(s, context.checker)) {
internalContext.converter.convertSymbol(internalContext, s);
}
tried.add(s);
}
missing = discoverMissingExports(mod, context, program);
for (const s of tried) {
missing.delete(s);
}
} while (missing.size > 0);
app.converter.finalizeDeferredConversion();
// If we added a module and all the missing symbols were excluded, get rid of our namespace.
if (internalContext.scope[InternalModule]
&& !internalContext.scope.children?.length) {
context.project.removeReflection(internalContext.scope);
}
context.setActiveProgram(void 0);
}
knownPrograms.clear();
referencedSymbols.clear();
symbolToOwningModule.clear();
symbolIdKeyToSymbol.clear();
}, 1e9);
app.renderer.on(Renderer.EVENT_BEGIN, () => {
if (app.options.getValue("collapseInternalModule")) {
app.renderer.hooks.on("head.end", () => JSX.createElement(JSX.Raw, {
html: HOOK_JS.replace("NAME", JSON.stringify(app.options.getValue("internalModule"))),
}));
}
});
}
function visitReflectionLinkTags(reflection, excludeTags, visitor) {
if (reflection.isProject() || reflection.isDeclaration()) {
visitLinkTags(reflection.readme || [], visitor);
}
if (reflection.isDocument()) {
visitLinkTags(reflection.content || [], visitor);
}
if (reflection.comment) {
visitLinkTags(reflection.comment.summary, visitor);
for (const tag of reflection.comment.blockTags) {
if (!excludeTags.includes(tag.tag)) {
visitLinkTags(tag.content, visitor);
}
}
}
if (reflection.isDeclaration()
&& reflection.kindOf(ReflectionKind.TypeAlias)
&& reflection.type?.type === "union"
&& reflection.type.elementSummaries) {
for (const parts of reflection.type.elementSummaries) {
visitLinkTags(parts, visitor);
}
}
}
function visitLinkTags(parts, visitor) {
const linkTags = ["@link", "@linkcode", "@linkplain"];
for (const part of parts) {
if (part.kind === "inline-tag" && linkTags.includes(part.tag)) {
visitor(part);
}
}
}
function getOwningModule(context) {
let refl = context.scope;
// Go up the reflection hierarchy until we get to a module
while (!refl.kindOf(ModuleLike)) {
refl = refl.parent;
}
// The <internal> module cannot be an owning module.
if (refl[InternalModule]) {
return refl.parent;
}
return refl;
}
function shouldConvertSymbol(symbol, checker) {
while (symbol.flags & ts.SymbolFlags.Alias) {
symbol = checker.getAliasedSymbol(symbol);
}
// We're looking at an unknown symbol which is declared in some package without
// type declarations. We know nothing about it, so don't convert it.
if (symbol.flags & ts.SymbolFlags.Transient) {
return false;
}
// #35 - Since TypeDoc#2978, TypeDoc will create symbol references which point to
// class methods. This previously wasn't an issue since references were set up directly
// upon resolution, but #2978 needs to use symbols to detect methods through inheritance.
// This somewhat breaks this plugin since we expect to be able to go through all created
// references and for them to be real missed values. It wasn't intentional that this
// plugin would lift class/interface symbols up out of the included classes; we'll just
// entirely ignore symbol references to methods here.
if (symbol.flags & ts.SymbolFlags.Method) {
return false;
}
// This is something inside the special Node `Globals` interface. Don't convert it
// because TypeDoc will reasonably assert that "Property" means that a symbol should be
// inside something that can have properties.
if (symbol.flags & ts.SymbolFlags.Property && symbol.name !== "default") {
return false;
}
return true;
}
function getModuleName(fileName, baseDir) {
return relative(baseDir, fileName)
.replace(/\\/g, "/")
.replace(/(\/index)?(\.d)?\.([cm]?[tj]s|[tj]sx?)$/, "");
}
//# sourceMappingURL=index.js.map