typedoc-plugin-external-module-name
Version:
Specify the Typedoc Module of a file using @module annotation
242 lines (241 loc) • 12.5 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
(function (factory) {
if (typeof module === "object" && typeof module.exports === "object") {
var v = factory(require, exports);
if (v !== undefined) module.exports = v;
}
else if (typeof define === "function" && define.amd) {
define(["require", "exports", "path", "fs", "typedoc/dist/lib/converter/components", "typedoc/dist/lib/converter/converter", "./typedocVersionCompatibility", "./getRawComment"], factory);
}
})(function (require, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ExternalModuleNamePlugin = void 0;
const path = require("path");
const fs = require("fs");
const components_1 = require("typedoc/dist/lib/converter/components");
const converter_1 = require("typedoc/dist/lib/converter/converter");
const typedocVersionCompatibility_1 = require("./typedocVersionCompatibility");
const getRawComment_1 = require("./getRawComment");
const PLUGIN = 'typedoc-plugin-external-module-name';
const CUSTOM_SCRIPT_FILENAMES = [`.${PLUGIN}.js`, `.${PLUGIN}.cjs`, `.${PLUGIN}.mjs`];
/**
* This plugin allows an ES6 module to specify its TypeDoc name.
* It also allows multiple ES6 modules to be merged together into a single TypeDoc module.
*
* @usage
* At the top of an ES6 module, add a "dynamic module comment". Insert "@module typedocModuleName" to
* specify that this ES6 module should be merged with module: "typedocModuleName".
*
* Similar to the [[DynamicModulePlugin]], ensure that there is a comment tag (even blank) for the
* first symbol in the file.
*
* @example
* ```
*
* /**
* * @module newModuleName
* */
* /** for typedoc /
* import {foo} from "../foo";
* export let bar = "bar";
* ```
*
* Also similar to [[DynamicModulePlugin]], if @preferred is found in a dynamic module comment, the comment
* will be used as the module comment, and documentation will be generated from it (note: this plugin does not
* attempt to count lengths of merged module comments in order to guess the best one)
*/
let ExternalModuleNamePlugin = class ExternalModuleNamePlugin extends components_1.ConverterComponent {
constructor() {
super(...arguments);
/** List of module reflections which are models to rename */
this.moduleRenames = [];
this.baseDir = '';
this.defaultGetModuleNameFn = (match, guess) => match || guess;
this.disableAutoModuleName = false;
}
initialize() {
this.listenTo(this.owner, {
[converter_1.Converter.EVENT_BEGIN]: this.onBegin,
[converter_1.Converter.EVENT_CREATE_DECLARATION]: this.onDeclaration,
[converter_1.Converter.EVENT_RESOLVE_BEGIN]: this.onBeginResolve,
});
for (const filename of CUSTOM_SCRIPT_FILENAMES) {
const pathToScript = path.join(process.cwd(), filename);
try {
if (fs.existsSync(pathToScript)) {
const relativePath = path.relative(__dirname, pathToScript);
this.customGetModuleNameFn = require(relativePath);
console.log(`${PLUGIN}: Using custom module name mapping function from ${pathToScript}`);
return;
}
}
catch (error) {
console.error(`${PLUGIN}: Failed to load custom module name mapping function from ${pathToScript}`);
throw error;
}
}
}
onBegin(context) {
var _a, _b;
/** Get the program entry points */
const dir = context.program.getCurrentDirectory();
const rootFileNames = (_a = context.program.getRootFileNames()) !== null && _a !== void 0 ? _a : [];
const options = context.getCompilerOptions();
function commonPrefix(string1, string2) {
let idx = 0;
while (idx < string1.length && string1[idx] === string2[idx]) {
idx++;
}
return string1.substr(0, idx);
}
const commonParent = rootFileNames.reduce((acc, entry) => commonPrefix(acc, path.dirname(path.resolve(dir, entry))), path.resolve((_b = rootFileNames[0]) !== null && _b !== void 0 ? _b : dir));
this.baseDir = options.rootDir || options.baseUrl || commonParent;
/** Process options */
const option = this.application.options.getValue('disableAutoModuleName');
let disableSources;
try {
disableSources = this.application.options.getValue('disableSources');
}
catch (ignored) { }
this.disableAutoModuleName = option === 'true' || option === true || disableSources === true;
}
/**
* Gets the module name for a reflection
*
* Order of precedence:
* 1) custom function found in .typedoc-plugin-external-module-name.js
* 2) explicit @module tag
* 3) auto-create a module name based on the directory
*/
getModuleName(context, reflection, node) {
const comment = getRawComment_1.getRawComment(node);
const preferred = /@preferred/.exec(comment) !== null;
// Look for @module
const [, match] = /@module\s+([\w\u4e00-\u9fa5\.\-_/@"]+)/.exec(comment) || [];
let guess;
let filename;
if (!this.disableAutoModuleName) {
// Make a guess based on enclosing directory structure
filename = reflection.sources[0].file.fullFileName;
guess = this.disableAutoModuleName ? undefined : path.dirname(path.relative(this.baseDir, filename));
if (guess === '.') {
guess = 'root';
}
}
// Try the custom function
const mapper = this.customGetModuleNameFn || this.defaultGetModuleNameFn;
const moduleName = mapper(match, guess, filename, reflection, context);
return [moduleName, preferred];
}
/**
* Process a reflection.
* Determine the module name and add it to a list of renames
*/
onDeclaration(context, reflection, node) {
if (typedocVersionCompatibility_1.isModuleOrNamespace(reflection)) {
const [moduleName, preferred] = this.getModuleName(context, reflection, node);
if (moduleName) {
// Set up a list of renames operations to perform when the resolve phase starts
this.moduleRenames.push({
renameTo: moduleName,
preferred: preferred,
symbol: node.symbol,
reflection: reflection,
});
}
}
// Remove the tags
if (reflection.comment) {
typedocVersionCompatibility_1.removeTags(reflection.comment, 'module');
typedocVersionCompatibility_1.removeTags(reflection.comment, 'preferred');
if (isEmptyComment(reflection.comment)) {
delete reflection.comment;
}
}
}
/**
* OK, we saw all the reflections.
* Now process the renames
*/
onBeginResolve(context) {
let projRefs = context.project.reflections;
let refsArray = Object.values(projRefs);
// Process each rename
this.moduleRenames.forEach((item) => {
let renaming = item.reflection;
// Find or create the module tree until the child's parent (each level is separated by .)
let nameParts = item.renameTo.split('.');
let parent = context.project;
for (let i = 0; i < nameParts.length - 1; ++i) {
let child = parent.children.filter((ref) => ref.name === nameParts[i])[0];
if (!child) {
child = typedocVersionCompatibility_1.createChildReflection(parent, nameParts[i]);
child.parent = parent;
child.children = [];
context.project.reflections[child.id] = child;
parent.children.push(child);
}
parent = child;
}
// Find an existing module with the child's name in the last parent. Use it as the merge target.
let mergeTarget = parent.children.filter((ref) => ref.kind === renaming.kind && ref.name === nameParts[nameParts.length - 1])[0];
// If there wasn't a merge target, change the name of the current module, connect it to the right parent and exit.
if (!mergeTarget) {
renaming.name = nameParts[nameParts.length - 1];
let oldParent = renaming.parent;
for (let i = 0; i < oldParent.children.length; ++i) {
if (oldParent.children[i] === renaming) {
oldParent.children.splice(i, 1);
break;
}
}
item.reflection.parent = parent;
parent.children.push(renaming);
typedocVersionCompatibility_1.updateSymbolMapping(context, item.symbol, parent);
return;
}
typedocVersionCompatibility_1.updateSymbolMapping(context, item.symbol, mergeTarget);
if (!mergeTarget.children) {
mergeTarget.children = [];
}
// Since there is a merge target, relocate all the renaming module's children to the mergeTarget.
let childrenOfRenamed = refsArray.filter((ref) => ref.parent === renaming);
childrenOfRenamed.forEach((ref) => {
// update links in both directions
ref.parent = mergeTarget;
mergeTarget.children.push(ref);
});
// If @preferred was found on the current item, update the mergeTarget's comment
// with comment from the renaming module
if (item.preferred)
mergeTarget.comment = renaming.comment;
// Now that all the children have been relocated to the mergeTarget, delete the empty module
// Make sure the module being renamed doesn't have children, or they will be deleted
if (renaming.children)
renaming.children.length = 0;
typedocVersionCompatibility_1.removeReflection(context.project, renaming);
// Remove @module and @preferred from the comment, if found.
if (mergeTarget.comment) {
typedocVersionCompatibility_1.removeTags(mergeTarget.comment, 'module');
typedocVersionCompatibility_1.removeTags(mergeTarget.comment, 'preferred');
}
if (isEmptyComment(mergeTarget.comment)) {
delete mergeTarget.comment;
}
});
}
};
ExternalModuleNamePlugin = __decorate([
components_1.Component({ name: 'external-module-name' })
], ExternalModuleNamePlugin);
exports.ExternalModuleNamePlugin = ExternalModuleNamePlugin;
function isEmptyComment(comment) {
return !comment || (!comment.text && !comment.shortText && (!comment.tags || comment.tags.length === 0));
}
});