UNPKG

@compodoc/compodoc

Version:

The missing documentation tool for your Angular application

808 lines (734 loc) 31.3 kB
import * as _ from '../../utils/collection.util'; import { MiscellaneousData } from '../interfaces/miscellaneous-data.interface'; import { ParsedData } from '../interfaces/parsed-data.interface'; import { RouteInterface } from '../interfaces/routes.interface'; import AngularApiUtil from '../../utils/angular-api.util'; import { IApiSourceResult } from '../../utils/api-source-result.interface'; import { getNamesCompareFn } from '../../utils/utils'; import { IEnumDecDep, IFunctionDecDep, IGuardDep, IInjectableDep, IInterceptorDep, IInterfaceDep, IPipeDep, ITypeAliasDecDep } from '../compiler/angular/dependencies.interfaces'; import { IComponentDep } from '../compiler/angular/deps/component-dep.factory'; import { IControllerDep } from '../compiler/angular/deps/controller-dep.factory'; import { IDirectiveDep } from '../compiler/angular/deps/directive-dep.factory'; import { IModuleDep } from '../compiler/angular/deps/module-dep.factory'; import { InterfaceDeclarationMerger } from './dependencies/interface-declaration-merger'; const traverse = require('neotraverse/legacy'); export class DependenciesEngine { private static readonly angularApiAliases: { [name: string]: string[] } = { NgFor: ['NgForOf'] }; public rawData: ParsedData; public modules: Object[]; public rawModules: Object[]; public rawModulesForOverview: Object[]; public components: Object[]; public controllers: Object[]; public entities: Object[]; public directives: Object[]; public injectables: Object[]; public interceptors: Object[]; public guards: Object[]; public interfaces: IInterfaceDep[]; public routes: RouteInterface; public pipes: Object[]; public classes: Object[]; public miscellaneous: MiscellaneousData = { variables: [], functions: [], typealiases: [], enumerations: [], groupedVariables: [], groupedFunctions: [], groupedEnumerations: [], groupedTypeAliases: [] }; private relationshipsCache: { [name: string]: { incoming: any[]; outgoing: any[] } } = {}; private interfaceDeclarationMerger = new InterfaceDeclarationMerger(); private static instance: DependenciesEngine; private constructor() {} public static getInstance() { if (!DependenciesEngine.instance) { DependenciesEngine.instance = new DependenciesEngine(); } return DependenciesEngine.instance; } private updateModulesDeclarationsExportsTypes() { const mergeTypes = entry => { const directive = this.findInCompodocDependencies( entry.name, this.directives, entry.file ); if (typeof directive.data !== 'undefined') { entry.type = 'directive'; entry.id = directive.data.id; } const component = this.findInCompodocDependencies( entry.name, this.components, entry.file ); if (typeof component.data !== 'undefined') { entry.type = 'component'; entry.id = component.data.id; } const pipe = this.findInCompodocDependencies(entry.name, this.pipes, entry.file); if (typeof pipe.data !== 'undefined') { entry.type = 'pipe'; entry.id = pipe.data.id; } }; this.modules.forEach((module: any) => { module.declarations.forEach(declaration => { mergeTypes(declaration); }); module.exports.forEach(expt => { mergeTypes(expt); }); module.entryComponents.forEach(ent => { mergeTypes(ent); }); }); } public init(data: ParsedData) { traverse(data).forEach(function (node) { if (node) { if (node.parent) { delete node.parent; } if (node.initializer) { delete node.initializer; } } }); this.rawData = data; this.modules = _.sortBy(this.rawData.modules, [el => el.name.toLowerCase()]); this.rawModulesForOverview = _.sortBy(data.modulesForGraph, [el => el.name.toLowerCase()]); this.rawModules = _.sortBy(data.modulesForGraph, [el => el.name.toLowerCase()]); this.components = _.sortBy(this.rawData.components, [el => el.name.toLowerCase()]); this.controllers = _.sortBy(this.rawData.controllers, [el => el.name.toLowerCase()]); this.entities = _.sortBy(this.rawData.entities, [el => el.name.toLowerCase()]); this.directives = _.sortBy(this.rawData.directives, [el => el.name.toLowerCase()]); this.injectables = _.sortBy(this.rawData.injectables, [el => el.name.toLowerCase()]); this.interceptors = _.sortBy(this.rawData.interceptors, [el => el.name.toLowerCase()]); this.guards = _.sortBy(this.rawData.guards, [el => el.name.toLowerCase()]); this.interfaces = this.interfaceDeclarationMerger.merge(this.rawData.interfaces); this.interfaces = _.sortBy(this.interfaces, [el => el.name.toLowerCase()]); this.pipes = _.sortBy(this.rawData.pipes, [el => el.name.toLowerCase()]); this.classes = _.sortBy(this.rawData.classes, [el => el.name.toLowerCase()]); this.miscellaneous = this.rawData.miscellaneous; this.prepareMiscellaneous(); this.updateModulesDeclarationsExportsTypes(); this.routes = this.rawData.routesTree; this.manageDuplicatesName(); this.cleanRawModulesNames(); this.relationshipsCache = {}; } private cleanRawModulesNames() { this.rawModulesForOverview = this.rawModulesForOverview.map((module: any) => { module.name = module.name.replace('$', ''); return module; }); } private findInCompodocDependencies(name, data, file?): IApiSourceResult<any> { let _result = { source: 'internal', data: undefined, score: 0 }; let nameFoundCounter = 0; if (data && data.length > 0) { for (let i = 0; i < data.length; i++) { if (typeof name !== 'undefined') { if (typeof file !== 'undefined') { if ( name === data[i].name && file.replace(/\\/g, '/').indexOf(data[i].file) !== -1 ) { nameFoundCounter += 1; _result.data = data[i]; _result.score = 2; } else if ( name.indexOf(data[i].name) !== -1 && file.replace(/\\/g, '/').indexOf(data[i].file) !== -1 ) { nameFoundCounter += 1; _result.data = data[i]; _result.score = 1; } } else { if (name === data[i].name) { nameFoundCounter += 1; _result.data = data[i]; _result.score = 2; } else if (name.indexOf(data[i].name) !== -1) { nameFoundCounter += 1; _result.data = data[i]; _result.score = 1; } } } } // Prevent wrong matching like MultiSelectOptionDirective with SelectOptionDirective, or QueryParamGroupService with QueryParamGroup if (nameFoundCounter > 1) { let found = false; for (let i = 0; i < data.length; i++) { if (typeof name !== 'undefined') { if (typeof file !== 'undefined') { if (name === data[i].name) { found = true; _result.data = data[i]; _result.score = 2; } } else { if (name === data[i].name) { found = true; _result.data = data[i]; _result.score = 2; } } } } if (!found) { _result = { source: 'internal', data: undefined, score: 0 }; } } } return _result; } private manageDuplicatesName() { const processDuplicates = (element, index, array) => { const elementsWithSameName = _.filter(array, { name: element.name }); if (elementsWithSameName.length > 1) { // First element is the reference for duplicates for (let i = 1; i < elementsWithSameName.length; i++) { let elementToEdit = elementsWithSameName[i]; if (typeof elementToEdit.isDuplicate === 'undefined') { elementToEdit.isDuplicate = true; elementToEdit.duplicateId = i; elementToEdit.duplicateName = elementToEdit.name + '-' + elementToEdit.duplicateId; elementToEdit.id = elementToEdit.id + '-' + elementToEdit.duplicateId; } } } return element; }; this.classes = this.classes.map(processDuplicates); this.interfaces = this.interfaces.map(processDuplicates); this.injectables = this.injectables.map(processDuplicates); this.pipes = this.pipes.map(processDuplicates); this.interceptors = this.interceptors.map(processDuplicates); this.guards = this.guards.map(processDuplicates); this.modules = this.modules.map(processDuplicates); this.components = this.components.map(processDuplicates); this.controllers = this.controllers.map(processDuplicates); this.entities = this.entities.map(processDuplicates); this.directives = this.directives.map(processDuplicates); } public find(name: string): IApiSourceResult<any> | undefined { const normalizedName = typeof name === 'string' ? name.trim() : name; const namesToResolve = [normalizedName]; if (DependenciesEngine.angularApiAliases[normalizedName]) { namesToResolve.push(...DependenciesEngine.angularApiAliases[normalizedName]); } let bestScore = 0; let bestResult = undefined; for (const resolvedName of namesToResolve) { const searchFunctions: Array<() => IApiSourceResult<any>> = [ () => this.findInCompodocDependencies(resolvedName, this.modules), () => this.findInCompodocDependencies(resolvedName, this.injectables), () => this.findInCompodocDependencies(resolvedName, this.interceptors), () => this.findInCompodocDependencies(resolvedName, this.guards), () => this.findInCompodocDependencies(resolvedName, this.interfaces), () => this.findInCompodocDependencies(resolvedName, this.classes), () => this.findInCompodocDependencies(resolvedName, this.components), () => this.findInCompodocDependencies(resolvedName, this.controllers), () => this.findInCompodocDependencies(resolvedName, this.entities), () => this.findInCompodocDependencies(resolvedName, this.directives), () => this.findInCompodocDependencies(resolvedName, this.miscellaneous.variables), () => this.findInCompodocDependencies(resolvedName, this.miscellaneous.functions), () => this.findInCompodocDependencies(resolvedName, this.miscellaneous.typealiases), () => this.findInCompodocDependencies(resolvedName, this.miscellaneous.enumerations), () => AngularApiUtil.findApi(resolvedName) ]; for (let searchFunction of searchFunctions) { const result = searchFunction(); if (result.data && result.score > bestScore) { bestScore = result.score; bestResult = result; } } } return bestResult; } public update(updatedData): void { if (updatedData.modules.length > 0) { _.forEach(updatedData.modules, (module: IModuleDep) => { const _index = _.findIndex(this.modules, { name: module.name } as any); this.modules[_index] = module; }); } if (updatedData.components.length > 0) { _.forEach(updatedData.components, (component: IComponentDep) => { const _index = _.findIndex(this.components, { name: component.name } as any); this.components[_index] = component; }); } if (updatedData.controllers.length > 0) { _.forEach(updatedData.controllers, (controller: IControllerDep) => { const _index = _.findIndex(this.controllers, { name: controller.name } as any); this.controllers[_index] = controller; }); } if (updatedData.entities.length > 0) { _.forEach(updatedData.entities, (entity: IControllerDep) => { const _index = _.findIndex(this.entities, { name: entity.name } as any); this.entities[_index] = entity; }); } if (updatedData.directives.length > 0) { _.forEach(updatedData.directives, (directive: IDirectiveDep) => { const _index = _.findIndex(this.directives, { name: directive.name } as any); this.directives[_index] = directive; }); } if (updatedData.injectables.length > 0) { _.forEach(updatedData.injectables, (injectable: IInjectableDep) => { const _index = _.findIndex(this.injectables, { name: injectable.name } as any); this.injectables[_index] = injectable; }); } if (updatedData.interceptors.length > 0) { _.forEach(updatedData.interceptors, (interceptor: IInterceptorDep) => { const _index = _.findIndex(this.interceptors, { name: interceptor.name } as any); this.interceptors[_index] = interceptor; }); } if (updatedData.guards.length > 0) { _.forEach(updatedData.guards, (guard: IGuardDep) => { const _index = _.findIndex(this.guards, { name: guard.name } as any); this.guards[_index] = guard; }); } if (updatedData.interfaces.length > 0) { _.forEach(updatedData.interfaces, (int: IInterfaceDep) => { const _index = _.findIndex(this.interfaces, { name: int.name } as any); this.interfaces[_index] = int; }); } if (updatedData.pipes.length > 0) { _.forEach(updatedData.pipes, (pipe: IPipeDep) => { const _index = _.findIndex(this.pipes, { name: pipe.name } as any); this.pipes[_index] = pipe; }); } if (updatedData.classes.length > 0) { _.forEach(updatedData.classes, (classe: any) => { const _index = _.findIndex(this.classes, { name: classe.name } as any); this.classes[_index] = classe; }); } /** * Miscellaneous update */ if (updatedData.miscellaneous.variables.length > 0) { _.forEach(updatedData.miscellaneous.variables, (variable: any) => { const _index = _.findIndex(this.miscellaneous.variables, { name: variable.name, file: variable.file }); this.miscellaneous.variables[_index] = variable; }); } if (updatedData.miscellaneous.functions.length > 0) { _.forEach(updatedData.miscellaneous.functions, (func: IFunctionDecDep) => { const _index = _.findIndex(this.miscellaneous.functions, { name: func.name, file: func.file }); this.miscellaneous.functions[_index] = func; }); } if (updatedData.miscellaneous.typealiases.length > 0) { _.forEach(updatedData.miscellaneous.typealiases, (typealias: ITypeAliasDecDep) => { const _index = _.findIndex(this.miscellaneous.typealiases, { name: typealias.name, file: typealias.file }); this.miscellaneous.typealiases[_index] = typealias; }); } if (updatedData.miscellaneous.enumerations.length > 0) { _.forEach(updatedData.miscellaneous.enumerations, (enumeration: IEnumDecDep) => { const _index = _.findIndex(this.miscellaneous.enumerations, { name: enumeration.name, file: enumeration.file }); this.miscellaneous.enumerations[_index] = enumeration; }); } this.prepareMiscellaneous(); this.relationshipsCache = {}; } public findInCompodoc(name: string) { const mergedData = _.concat( [], this.modules, this.components, this.controllers, this.entities, this.directives, this.injectables, this.interceptors, this.guards, this.interfaces, this.pipes, this.classes, this.miscellaneous.enumerations, this.miscellaneous.typealiases, this.miscellaneous.variables, this.miscellaneous.functions ); const result = _.find(mergedData, { name: name } as any); return result || false; } /** * Returns entity relationships split in two lists: * - incoming: entities that reference/use the current entity * - outgoing: entities referenced by the current entity */ public getRelationships(entityRef: any): { incoming: Array<{ name: string; type: string; description?: string }>; outgoing: Array<{ name: string; type: string; description?: string }>; } { const target = this.resolveEntityFromReference(entityRef); const entityName = typeof entityRef === 'string' ? entityRef : entityRef?.name; if (!entityName) { return { incoming: [], outgoing: [] }; } const cacheKey = target?.id || entityName; if (this.relationshipsCache[cacheKey]) { return this.relationshipsCache[cacheKey]; } const MAX_ITEMS = 80; const incoming: Array<{ name: string; type: string; description?: string }> = []; const outgoing: Array<{ name: string; type: string; description?: string }> = []; const incomingSet = new Set<string>(); const outgoingSet = new Set<string>(); const addIncoming = (dep: any) => { if (!dep?.name || dep.name === entityName || incoming.length >= MAX_ITEMS) { return; } const key = `${dep.type || 'unknown'}::${dep.name}`; if (incomingSet.has(key)) { return; } incomingSet.add(key); incoming.push({ name: dep.name, type: dep.type || 'unknown', description: this.extractShortDescription(dep) }); }; const addOutgoing = (item: any, fallbackType = 'dependency') => { if (!item) { return; } const name = typeof item === 'string' ? item : item.name; if (!name || name === entityName || outgoing.length >= MAX_ITEMS) { return; } const resolved = this.resolveEntityByName(name); const type = resolved?.type || item.type || fallbackType; const key = `${type}::${name}`; if (outgoingSet.has(key)) { return; } outgoingSet.add(key); outgoing.push({ name, type, description: this.extractShortDescription(resolved || item) }); }; const includeIfReferences = (dep: any, references?: Array<any>) => { if (!references || references.length === 0) { return; } if ( references.some((ref) => this.referencePointsToEntity(ref, target, entityName, dep) ) ) { addIncoming(dep); } }; const allInternalEntities: any[] = this.getAllInternalEntities(); allInternalEntities.forEach((dep) => { includeIfReferences(dep, dep.imports); includeIfReferences(dep, dep.exports); includeIfReferences(dep, dep.declarations); includeIfReferences(dep, dep.controllers); includeIfReferences(dep, dep.providers); includeIfReferences(dep, dep.viewProviders); includeIfReferences(dep, dep.entryComponents); includeIfReferences(dep, dep.bootstrap); includeIfReferences(dep, dep.hostDirectives); if (dep.extends) { const extendsList = Array.isArray(dep.extends) ? dep.extends : [dep.extends]; if (extendsList.some((item) => item === entityName || item?.name === entityName)) { addIncoming(dep); } } if (dep.implements) { const implList = Array.isArray(dep.implements) ? dep.implements : [dep.implements]; if (implList.some((item) => item === entityName || item?.name === entityName)) { addIncoming(dep); } } }); if (target) { (target.imports || []).forEach((item) => addOutgoing(item, 'module')); (target.exports || []).forEach((item) => addOutgoing(item, 'module')); (target.declarations || []).forEach((item) => addOutgoing(item)); (target.controllers || []).forEach((item) => addOutgoing(item, 'controller')); (target.providers || []).forEach((item) => addOutgoing(item, 'injectable')); (target.viewProviders || []).forEach((item) => addOutgoing(item, 'injectable')); (target.entryComponents || []).forEach((item) => addOutgoing(item, 'component')); (target.bootstrap || []).forEach((item) => addOutgoing(item, 'component')); (target.hostDirectives || []).forEach((item) => addOutgoing(item, 'directive')); if (target.extends) { const extendsList = Array.isArray(target.extends) ? target.extends : [target.extends]; extendsList.forEach((item) => addOutgoing(item, 'class')); } if (target.implements) { const implList = Array.isArray(target.implements) ? target.implements : [target.implements]; implList.forEach((item) => addOutgoing(item, 'interface')); } } const relationships = { incoming, outgoing }; this.relationshipsCache[cacheKey] = relationships; return relationships; } private referencePointsToEntity( reference: any, targetEntity: any, fallbackName: string, originEntity?: any ): boolean { if (!reference || !fallbackName) { return false; } if (targetEntity?.id && typeof reference === 'object' && reference.id) { return reference.id === targetEntity.id; } const resolved = this.resolveEntityFromReference(reference, originEntity); if (resolved && targetEntity) { return resolved.id === targetEntity.id; } if (typeof reference === 'string') { return reference === fallbackName; } if (reference?.name !== fallbackName) { return false; } const refType = this.normalizeEntityType(reference?.type); const targetType = this.normalizeEntityType(targetEntity?.type); if (refType && targetType && refType !== targetType) { return false; } return true; } private normalizeEntityType(type?: string): string | undefined { if (!type) { return undefined; } return type === 'classe' ? 'class' : type; } private normalizeFilePath(filePath?: string): string | undefined { return filePath ? String(filePath).replace(/\\/g, '/') : undefined; } private getAllInternalEntities(): any[] { return _.concat( [], this.modules, this.components, this.controllers, this.entities, this.directives, this.injectables, this.interceptors, this.guards, this.interfaces, this.pipes, this.classes ); } private resolveEntityByName(name: string): any { if (!name) { return undefined; } return this.resolveEntityFromReference({ name }); } private resolveEntityFromReference(reference: any, originEntity?: any): any { if (!reference) { return undefined; } const entities = this.getAllInternalEntities(); if (typeof reference === 'object' && reference.id) { const byId = _.find(entities, { id: reference.id } as any); if (byId) { return byId; } } const refName = typeof reference === 'string' ? reference : reference.name; if (!refName) { return undefined; } let candidates = entities.filter( (entity: any) => entity.name === refName || entity.duplicateName === refName ); if (candidates.length === 0) { return undefined; } if (candidates.length === 1) { return candidates[0]; } const refType = this.normalizeEntityType( typeof reference === 'object' ? reference.type : undefined ); if (refType) { const typeCandidates = candidates.filter( (entity: any) => this.normalizeEntityType(entity.type) === refType ); if (typeCandidates.length === 1) { return typeCandidates[0]; } if (typeCandidates.length > 0) { candidates = typeCandidates; } } const refFile = this.normalizeFilePath( typeof reference === 'object' ? reference.file : undefined ); if (refFile) { const fileCandidates = candidates.filter((entity: any) => { const entityFile = this.normalizeFilePath(entity.file); return !!entityFile && (entityFile === refFile || refFile.indexOf(entityFile) !== -1); }); if (fileCandidates.length === 1) { return fileCandidates[0]; } if (fileCandidates.length > 0) { candidates = fileCandidates; } } const originFile = this.normalizeFilePath(originEntity?.file); if (originFile) { const sameFileCandidates = candidates.filter((entity: any) => { const entityFile = this.normalizeFilePath(entity.file); return !!entityFile && entityFile === originFile; }); if (sameFileCandidates.length === 1) { return sameFileCandidates[0]; } } const nonDuplicateCandidates = candidates.filter((entity: any) => !entity.isDuplicate); if (nonDuplicateCandidates.length === 1) { return nonDuplicateCandidates[0]; } // Ambiguous homonym: avoid linking to the wrong entity. return undefined; } private extractShortDescription(entity: any): string | undefined { const raw = entity?.rawdescription || entity?.description || ''; if (!raw) { return undefined; } const cleaned = String(raw) .replace(/<[^>]+>/g, ' ') .replace(/\s+/g, ' ') .trim(); if (!cleaned) { return undefined; } return cleaned.length > 140 ? `${cleaned.slice(0, 137).trim()}...` : cleaned; } private prepareMiscellaneous() { this.miscellaneous.variables.sort(getNamesCompareFn()); this.miscellaneous.functions.sort(getNamesCompareFn()); this.miscellaneous.enumerations.sort(getNamesCompareFn()); this.miscellaneous.typealiases.sort(getNamesCompareFn()); // group each subgoup by file this.miscellaneous.groupedVariables = _.groupBy(this.miscellaneous.variables, 'file'); this.miscellaneous.groupedFunctions = _.groupBy(this.miscellaneous.functions, 'file'); this.miscellaneous.groupedEnumerations = _.groupBy(this.miscellaneous.enumerations, 'file'); this.miscellaneous.groupedTypeAliases = _.groupBy(this.miscellaneous.typealiases, 'file'); } public getModule(name: string) { return _.find(this.modules, ['name', name]); } public getRawModule(name: string): any { return _.find(this.rawModules, ['name', name]); } public getModules() { return this.modules; } public getComponents() { return this.components; } public getControllers() { return this.controllers; } public getEntities() { return this.entities; } public getDirectives() { return this.directives; } public getInjectables() { return this.injectables; } public getInterceptors() { return this.interceptors; } public getGuards() { return this.guards; } public getInterfaces() { return this.interfaces; } public getRoutes() { return this.routes; } public getPipes() { return this.pipes; } public getClasses() { return this.classes; } public getMiscellaneous() { return this.miscellaneous; } } export default DependenciesEngine.getInstance();