@irwinproject/storybook-addon-tsdoc
Version:
Generate mdx documentation from your typescript!
292 lines (291 loc) • 13.7 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.declarationOfType = exports.isStatic = exports.renderCode = exports.getExample = exports.isPrimitive = exports.isMethodLike = exports.getDocPath = exports.getFName = exports.getNearestName = exports.getTypeNode = exports.isAsync = exports.getFamilyName = exports.getComments = exports.parseDoc = exports.parseTags = exports.OMITTED_TAGS = exports.getJsDocs = exports.isKeyword = exports.getSignatureName = exports.getFullName = exports.getName = exports.isPrivate = void 0;
const ts_morph_1 = require("ts-morph");
const TS_1 = __importDefault(require("./TS"));
const SyntaxKindDelegator_1 = require("./SyntaxKindDelegator");
const SyntaxKindDelegator_types_1 = __importDefault(require("./SyntaxKindDelegator.types"));
const console_log_colors_1 = require("console-log-colors");
const typescript_1 = require("typescript");
const utils_1 = require("./utils");
const decorators_1 = require("./decorators");
const node_signature_1 = require("./node-signature");
/**
* Because there isnt a standard way to mark something private.
* @param node
* @returns
*/
const isPrivate = (node) => !node
|| (ts_morph_1.Node.isModifierable(node) && node.hasModifier(SyntaxKindDelegator_types_1.default.PrivateKeyword))
|| (ts_morph_1.Node.isJSDocable(node) && !!node.getJsDocs().find(d => d.getTags().some(t => t.getTagName().toLowerCase() === 'private')))
|| (ts_morph_1.Node.isVariableDeclaration(node) && (0, exports.isPrivate)(node.getVariableStatement()));
exports.isPrivate = isPrivate;
/**
* Check if a node has getName method
* @param node
* @returns
*/
const hasName = (node) => 'getName' in node
&& typeof node.getName === 'function';
/**
* Get a nodes name if one is available
* @param node
* @returns
*/
const getName = (node) => (node && hasName(node)) ? (0, utils_1.escape)(node.getName())
: '';
exports.getName = getName;
/**
* Gets a / delimited list of names from source to current node
* @param node
* @returns
*/
const getFullName = (node, delim = ".") => {
const family = (0, exports.getFamilyName)(node, delim);
return [family, (ts_morph_1.Node.isStaticable(node) && node.isStatic() ? 'static' : ''), (0, exports.getName)(node)].filter(a => a).join(delim);
};
exports.getFullName = getFullName;
/**
* Gets the signature of a declaration.
* @param node
* @param delim
* @returns
*/
const getSignatureName = (node, delim = ".") => {
const family = (0, exports.getFamilyName)(node, delim);
return `${[family, (0, exports.getName)(node)].filter(a => a).join(delim)}`;
};
exports.getSignatureName = getSignatureName;
const isKeyword = (node) => ts_morph_1.Node.isAnyKeyword(node)
|| ts_morph_1.Node.isInferKeyword(node)
|| ts_morph_1.Node.isNeverKeyword(node)
|| ts_morph_1.Node.isNumberKeyword(node)
|| ts_morph_1.Node.isObjectKeyword(node) //this should probably be handled differently.
|| ts_morph_1.Node.isStringKeyword(node)
|| ts_morph_1.Node.isSymbolKeyword(node)
|| ts_morph_1.Node.isBooleanKeyword(node)
|| ts_morph_1.Node.isUndefinedKeyword(node);
exports.isKeyword = isKeyword;
/**
* Get the JSDocs if available.
* to avoid potential duplicate documentation explicit corner cases should be used.
*
* - variableDeclarations. (the declarations JSDoc should be derived from the statement)
* @param node
* @returns
*/
const getJsDocs = (node) => (ts_morph_1.Node.isJSDocable(node) && node.getJsDocs())
|| (ts_morph_1.Node.isVariableDeclaration(node) && (0, exports.getJsDocs)(node.getVariableStatement()))
|| [];
exports.getJsDocs = getJsDocs;
/**
* Instead it seems better to just support JSDoc separately from the built in typing. As I integrate properties into the signature process I can omit them from here.
*/
exports.OMITTED_TAGS = new Set([
"example",
"param",
"returns"
]);
/**
* This method is to simplify some of the tag parsing into one place. however with some tags being designed to affect the actual typing in the linter and editor I need to experiment with how these tags effect typescript in different environments.
*
* To be clear this will only parse tags that just need to be displayed but do not effect the typing of the object.
* @param doc
*/
const parseTags = (doc) => doc.getTags().filter(t => !exports.OMITTED_TAGS.has(t.getTagName())).map(t => { var _a; return (0, decorators_1.$t)(6) `${(0, decorators_1.$kd) `@${t.getTagName()}`} ${ts_morph_1.Node.isJSDocTypeTag(t) ? ' : ' + (0, node_signature_1.getSignature)((_a = t.getTypeExpression()) === null || _a === void 0 ? void 0 : _a.getTypeNode()) : ''} ${t.getCommentText()}`; }).join('\n');
exports.parseTags = parseTags;
const parseDoc = (doc) => {
var _a;
const tags = (0, exports.parseTags)(doc);
return ((_a = doc.getComment()) !== null && _a !== void 0 ? _a : "") + (tags ? '\n\n' + tags + '\n---\n' : '');
};
exports.parseDoc = parseDoc;
const getParameters = (node) => ts_morph_1.Node.isParametered(node) ? node.getParameters() : [];
const getTags = (node, typeFilter) => (0, exports.getJsDocs)(node).flatMap(d => {
const tags = d.getTags();
if (typeFilter)
return typeFilter instanceof RegExp ? tags.filter(t => typeFilter.test(t.getTagName())) : tags.filter(t => typeFilter === t.getTagName());
return tags;
});
const getJSDocParameters = (node) => {
if (!node)
return [];
return node ? getTags(node, 'param') : [];
};
const getParameterComment = (node) => {
var _a;
const parent = node.getParent();
const i = getParameters(parent).findIndex(el => el === node);
const param = getJSDocParameters(ts_morph_1.Node.isExpression(parent) ? parent.getParent() : parent)[i];
return (_a = param === null || param === void 0 ? void 0 : param.getCommentText()) !== null && _a !== void 0 ? _a : ""; //no parameter index found
};
const getComments = (node) => (ts_morph_1.Node.isParameterDeclaration(node) ? getParameterComment(node)
: ts_morph_1.Node.isVariableDeclaration(node) ? (0, exports.getComments)(node.getVariableStatement())
: (ts_morph_1.Node.isJSDocable(node) ? node.getJsDocs() : []).map(exports.parseDoc).join('\n') + '\n').wrap('', '<br/>', false);
exports.getComments = getComments;
/**
* Converts the ancestors into a family name.
* @param node
* @returns
*/
const getFamilyName = (node, delim = ".") => node.getAncestors().map(a => (0, exports.getName)(a)).filter(a => a).reverse().join(delim);
exports.getFamilyName = getFamilyName;
const ModMap = {
[SyntaxKindDelegator_types_1.default.TypeAliasDeclaration]: (node) => [node.getName(), '', []],
[SyntaxKindDelegator_types_1.default.PropertySignature]: (node, df) => {
const [pre, post] = (0, SyntaxKindDelegator_1.bySyntax)(node.getParent(), ModMap, df);
return [pre + '.' + node.getName(), post, []];
},
[SyntaxKindDelegator_types_1.default.TypeLiteral]: (node, df) => {
const parent = node.getParent();
const [pre, post, children] = (0, SyntaxKindDelegator_1.bySyntax)(parent, ModMap, df);
return [pre + (children.length > 1 ? '.' + children.findIndex(c => c === node) : ''), post, []];
},
[SyntaxKindDelegator_types_1.default.UnionType]: (node, df) => {
const [pre, post] = (0, SyntaxKindDelegator_1.bySyntax)(node.getParent(), ModMap, df);
return [pre, post, node.getTypeNodes()];
},
[SyntaxKindDelegator_types_1.default.TypeParameter]: (node, df) => {
const [pre, post] = (0, SyntaxKindDelegator_1.bySyntax)(node.getParent(), ModMap, df);
return [pre + '.' + node.getName(), post, []];
},
[SyntaxKindDelegator_types_1.default.Parameter]: (node, df) => {
const [pre, post] = (0, SyntaxKindDelegator_1.bySyntax)(node.getParent(), ModMap, df);
return [pre + '.' + node.getName(), post, []];
},
[SyntaxKindDelegator_types_1.default.FunctionType]: (node, df) => {
const [pre, post] = (0, SyntaxKindDelegator_1.bySyntax)(node.getParent(), ModMap, df);
return [pre, post, []];
}
};
const isAsync = (node) => {
if (!('isAsync' in node) || typeof node.isAsync !== 'function')
return false;
return node.isAsync();
};
exports.isAsync = isAsync;
const getTypeNode = (node) => (ts_morph_1.Node.isTyped(node) && node.getTypeNode())
|| ((ts_morph_1.Node.isInitializerExpressionGetable(node) || ts_morph_1.Node.isInitializerExpressionable(node)) ? node.getInitializer()
: undefined);
exports.getTypeNode = getTypeNode;
/**
* In some cases the named node is the parent node of the evaluated node this just climbs the node tree until it finds a name
* @param node
*/
const getNearestName = (node) => {
let name = (0, exports.getName)(node);
while (node && !name) {
node = node.getParent();
name = (0, exports.getName)(node);
}
return name;
};
exports.getNearestName = getNearestName;
const getFName = (node) => {
const [pre, post] = (0, SyntaxKindDelegator_1.bySyntax)(node, ModMap, n => {
if (!n)
return ['', '', []];
TS_1.default.warn((0, console_log_colors_1.cyan)(n.getKindName()));
return ['', '', []];
});
const nm = pre + post;
if (nm) {
TS_1.default.success("FName", (0, console_log_colors_1.green)(nm));
return nm;
}
return (0, exports.getFullName)(node);
};
exports.getFName = getFName;
/**
* Gets the source of the node and returns a storybook formatted link to the documentation of said node if the node exists within the scope provided by the entry point.
* @param node
*/
const getDocPath = (node) => {
const src = node.getSourceFile().getFilePath();
const ref = TS_1.default.resolveUrl(src);
if (!ref)
return;
const fn = (0, exports.getFullName)(node, TS_1.default.documentStyle === 'file' ? '' : '/');
if (TS_1.default.documentStyle === "declaration")
return TS_1.default.resolveDocPath(ref + '/' + fn).toLowerCase(); //no need for deeplinking but more explicit naming
return TS_1.default.resolveDocPath(ref) + (fn ? '#' + fn.toLowerCase() : '').toLowerCase();
};
exports.getDocPath = getDocPath;
/**
* With there being 4 to 5 different method like declarations it seems like a good way to reduce redundant code.
* @param node
* @returns
*/
const isMethodLike = (node) => {
if (!node)
return false;
const k = node.getKind();
return new Set([
SyntaxKindDelegator_types_1.default.MethodSignature,
SyntaxKindDelegator_types_1.default.MethodDeclaration,
SyntaxKindDelegator_types_1.default.FunctionType
]).has(k);
};
exports.isMethodLike = isMethodLike;
/**
* Checks to see if the Node is primitive
*
* update: using Kind value is simpler response as I am not confirming a specific type.
* @param node
* @returns
*/
const isPrimitive = (node) => {
if (!node)
return false;
const k = node.getKind();
return new Set([
SyntaxKindDelegator_types_1.default.AnyKeyword,
SyntaxKindDelegator_types_1.default.StringKeyword,
SyntaxKindDelegator_types_1.default.StringLiteral,
SyntaxKindDelegator_types_1.default.NumberKeyword,
SyntaxKindDelegator_types_1.default.NumericLiteral,
SyntaxKindDelegator_types_1.default.BooleanKeyword,
SyntaxKindDelegator_types_1.default.TrueKeyword,
SyntaxKindDelegator_types_1.default.FalseKeyword,
SyntaxKindDelegator_types_1.default.BigIntKeyword,
SyntaxKindDelegator_types_1.default.BigIntLiteral,
SyntaxKindDelegator_types_1.default.LiteralType,
SyntaxKindDelegator_types_1.default.NullKeyword,
SyntaxKindDelegator_types_1.default.NeverKeyword,
SyntaxKindDelegator_types_1.default.VoidKeyword,
SyntaxKindDelegator_types_1.default.UndefinedKeyword,
SyntaxKindDelegator_types_1.default.UnknownKeyword,
SyntaxKindDelegator_types_1.default.ExportAssignment, //Just to ignore the warning it wont be used at this stage.
SyntaxKindDelegator_types_1.default.ImportDeclaration
]).has(k);
};
exports.isPrimitive = isPrimitive;
const getExample = (node) => {
const examples = (0, exports.getJsDocs)(node).flatMap(d => d.getTags().filter(t => t.getTagName() === "example").map(t => t.getComment()));
return examples.map(exports.renderCode).join('\n');
};
exports.getExample = getExample;
const renderCode = (code) => code ? `\`\`\`ts\n${(0, typescript_1.createPrinter)({ removeComments: false }).printFile((0, typescript_1.createSourceFile)("t.ts", code, typescript_1.ScriptTarget.Latest, false, typescript_1.ScriptKind.TS))}\n\`\`\`` : '';
exports.renderCode = renderCode;
const isStatic = (node) => {
if (!ts_morph_1.Node.isStaticable(node))
return false;
return node.isStatic();
};
exports.isStatic = isStatic;
/**
* attempt to get a Node from the type declaration
* @param type
*/
const declarationOfType = (type, onlyAnonymous = false) => {
var _a, _b, _c, _d;
if (onlyAnonymous && !type.isAnonymous())
return;
const [symbolDec] = (_b = (_a = type.getSymbol()) === null || _a === void 0 ? void 0 : _a.getDeclarations()) !== null && _b !== void 0 ? _b : [];
const [aliasDec] = (_d = (_c = type.getAliasSymbol()) === null || _c === void 0 ? void 0 : _c.getDeclarations()) !== null && _d !== void 0 ? _d : [];
return symbolDec !== null && symbolDec !== void 0 ? symbolDec : aliasDec;
};
exports.declarationOfType = declarationOfType;