UNPKG

typedoc-plugin-missing-exports

Version:
191 lines 8.86 kB
import { relative } from "path"; import { Context, Converter, JSX, ParameterType, ReflectionKind, 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(); function discoverMissingExports(owningModule, context, program) { // An export is missing if if was referenced // Is not contained in the documented 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; } // Monkey patch the constructor for references so that we can get every const origCreateSymbolReference = Context.prototype.createSymbolReference; Context.prototype.createSymbolReference = function (symbol, context, name) { const owningModule = getOwningModule(context); const set = referencedSymbols.get(context.program); symbolToOwningModule.set(symbol, owningModule); if (set) { set.add(symbol); } else { referencedSymbols.set(context.program, new Set([symbol])); } return origCreateSymbolReference.call(this, symbol, context, name); }; 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.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.`); } }); 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()); } }); 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); } for (const mod of modules) { const program = knownPrograms.get(mod); if (!program) continue; let missing = discoverMissingExports(mod, context, program); if (!missing.size) continue; // Nasty hack here that will almost certainly break in future TypeDoc versions. context.setActiveProgram(program); 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(); }, 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 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; } // 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