uui-build/ts/tasks/docsGen/converters/converterForProps.ts (176 lines of code) (raw):

import { Node, Symbol, SyntaxKind, Type } from 'ts-morph'; import { IConverterContext, TConvertable, TTypePropsConverted } from '../types/types'; import { SymbolUtils } from './converterUtils/symbolUtils'; import { NodeUtils } from './converterUtils/nodeUtils'; import { TypeUtils } from './converterUtils/typeUtils'; import { TTypeProp, TTypeRef } from '../types/sharedTypes'; import { getTypeRefFromTypeSummary } from './converterUtils/converterUtils'; import { ConvertableUtils } from './converterUtils/convertableUtils'; function isPropsSupported(node: Node) { const type = node.getType(); const isExternalType = NodeUtils.isExternalNode(node); return TypeUtils.isPropsSupportedByType({ type, isExternalType }); } export function convertTypeProps(nodeOrSymbol: TConvertable, context: IConverterContext): TTypePropsConverted | undefined { const typeNode = ConvertableUtils.getNode(nodeOrSymbol); if (!isPropsSupported(typeNode)) { return; } const type = NodeUtils.getTypeFromNode(typeNode); const idGen = new SimpleIdGen(); if (type.isUnion()) { const unionTypes = type.getUnionTypes(); const props = extractPropsFromNonUnionTypeArr(unionTypes, typeNode, context, idGen); if (props) { return { props, propsFromUnion: true, }; } } else { const props = extractPropsFromNonUnionType({ parentNode: typeNode, type, context, idGen }); if (props) { return { props, propsFromUnion: false, }; } } } function extractPropsFromNonUnionTypeArr(typeArr: Type[], parentNode: Node, context: IConverterContext, idGen: SimpleIdGen) { const allSupportProps = typeArr.every((singleType) => { const typeNode = TypeUtils.getNodeFromType(singleType); /* * If node isn't available for this type, then we treat it as internal. * I hope it's OK - in the worst case, the end user will see a bunch of props from the external type. */ const isExternalType = typeNode ? NodeUtils.isExternalNode(typeNode) : false; return TypeUtils.isPropsSupportedByType({ type: singleType, isExternalType }); }); if (allSupportProps) { const allPropsSets = typeArr.reduce<PropsSet[]>((acc, singleType) => { const utProps = extractPropsFromNonUnionType({ parentNode, type: singleType, context, idGen }); if (utProps) { acc.push(PropsSet.fromArray(utProps)); } return acc; }, []); return PropsSet.concat(allPropsSets); } } function extractPropsFromNonUnionType(params: { parentNode: Node, type: Type, context: IConverterContext, idGen: SimpleIdGen }): TTypeProp[] | undefined { const { parentNode, type, context, idGen } = params; const propsOnly = type.getProperties(); const indexSigns = TypeUtils.getIndexSignature(type); const props = indexSigns.concat(propsOnly); if (props.length > 0) { return props.reduce<TTypeProp[]>((acc, propertySymbol) => { const mapped = mapSingleMember({ parentNode, propertySymbol, context, idGen }); if (mapped) { acc.push(mapped); } return acc; }, []); } } function mapSingleMember(params: { parentNode?: Node, propertySymbol: Symbol, context: IConverterContext, idGen: SimpleIdGen }): TTypeProp | undefined { const { parentNode, propertySymbol, context, idGen } = params; let prop: TTypeProp | undefined = undefined; const propertyNode = SymbolUtils.getNodeFromSymbol(propertySymbol); const nKind = propertyNode.getKind(); const isSupported = [ SyntaxKind.PropertySignature, SyntaxKind.MethodSignature, SyntaxKind.GetAccessor, SyntaxKind.SetAccessor, SyntaxKind.MethodDeclaration, SyntaxKind.PropertyDeclaration, SyntaxKind.IndexSignature, ].indexOf(nKind) !== -1; if (isSupported) { const tv = NodeUtils.getPropertySymbolTypeValue(propertySymbol, context); if (!tv) { return; } const comment = NodeUtils.getCommentFromNode(propertyNode); let fromRef; const propParent = NodeUtils.getPropertyNodeParent(propertyNode, parentNode); if (propParent) { const fromSummary = context.convertTypeSummary({ convertable: propParent }); fromRef = getTypeRefFromTypeSummary(fromSummary); } const name = NodeUtils.getPropertyNodeName(propertyNode); const required = NodeUtils.isPropertyNodeRequired(propertyNode) && !propertySymbol.isOptional(); const uid = idGen.getNextId(name); prop = { // "uid" property is needed because we may have unions where there are props with same name but different type uid, name, comment, typeValue: tv, typeValueRef: getPropertyTypeValueRef({ propertySymbol, context }), editor: context.convertPropEditor({ convertable: propertySymbol }), from: fromRef, required, }; } else { console.error(`New SyntaxKind was found: ${nKind}. Please add it to the list and check that it's processed correctly.`); } return prop; } function getPropertyTypeValueRef(params: { propertySymbol: Symbol, context: IConverterContext }): TTypeRef | undefined { const { propertySymbol, context } = params; const valueDeclNode = propertySymbol.getValueDeclaration(); if (valueDeclNode) { const aliasSymbol = valueDeclNode.getType().getAliasSymbol(); if (aliasSymbol) { // I.e. the property refers some type which is declared in another place (I.e. it's not inline type declaration). const valueTypeSummary = context.convertTypeSummary({ convertable: aliasSymbol }); return `${valueTypeSummary.module}:${valueTypeSummary.typeName.name}`; } } } class PropsSet { private _propsMap = new Map<string, TTypeProp>(); add(p: TTypeProp) { const id = PropsSet.buildId(p); if (!this._propsMap.has(id)) { // we want to keep the first unique type rather than the last one, // because "uid" in unions are less likely contain index in such case - as a result the output JSON is cleaner. this._propsMap.set(id, p); } } addAll(pa: TTypeProp[]) { pa.forEach((p) => { this.add(p); }); } has(p: TTypeProp): boolean { const id = PropsSet.buildId(p); return this._propsMap.has(id); } toArray(): TTypeProp[] { return [...this._propsMap.values()]; } static concat(psa: PropsSet[]): TTypeProp[] { const tempPs = psa.reduce<PropsSet>((acc, ps) => { acc.addAll(ps.toArray()); return acc; }, new PropsSet()); return tempPs.toArray(); } static fromArray(pa: TTypeProp[]): PropsSet { const ps = new PropsSet(); pa.forEach((p) => { ps.add(p); }); return ps; } static buildId(p: TTypeProp) { // we don't use uniqueId property because we need to eliminate duplicates which come from same type (it's possible in union types) return `${p.from}:${p.name}:${p.typeValue.raw.replace(/[\n\s]/g, '')}`; } } class SimpleIdGen { private _usedIds = new Map<string, number>(); getNextId = (name: string) => { const prevIndex = this._usedIds.get(name); if (prevIndex === undefined) { this._usedIds.set(name, 1); return name; } this._usedIds.set(name, prevIndex + 1); return `${name}_${prevIndex + 1}`; }; }