UNPKG

@compodoc/compodoc

Version:

The missing documentation tool for your Angular application

495 lines (421 loc) 16.2 kB
import { SyntaxKind, ts } from 'ts-morph'; import { detectIndent } from '../../../../../utils'; import { ClassHelper } from './class-helper'; import { IParseDeepIdentifierResult, SymbolHelper } from './symbol-helper'; export class ComponentHelper { constructor( private classHelper: ClassHelper, private symbolHelper: SymbolHelper = new SymbolHelper() ) {} public getComponentChangeDetection( props: ReadonlyArray<ts.ObjectLiteralElementLike>, srcFile: ts.SourceFile ): string { return this.symbolHelper.getSymbolDeps(props, 'changeDetection', srcFile).pop(); } public getComponentEncapsulation( props: ReadonlyArray<ts.ObjectLiteralElementLike>, srcFile: ts.SourceFile ): Array<string> { return this.symbolHelper.getSymbolDeps(props, 'encapsulation', srcFile); } public getComponentPure( props: ReadonlyArray<ts.ObjectLiteralElementLike>, srcFile: ts.SourceFile ): string { return this.symbolHelper.getSymbolDeps(props, 'pure', srcFile).pop(); } public getComponentName( props: ReadonlyArray<ts.ObjectLiteralElementLike>, srcFile: ts.SourceFile ): string { return this.symbolHelper.getSymbolDeps(props, 'name', srcFile).pop(); } public getComponentExportAs( props: ReadonlyArray<ts.ObjectLiteralElementLike>, srcFile: ts.SourceFile ): string { return this.symbolHelper.getSymbolDeps(props, 'exportAs', srcFile).pop(); } public getComponentHostDirectives( props: ReadonlyArray<ts.ObjectLiteralElementLike> ): Array<any> { const hostDirectiveSymbolParsed = this.symbolHelper.getSymbolDepsRaw( props, 'hostDirectives' ); let hostDirectiveSymbol = null; if (hostDirectiveSymbolParsed.length > 0) { hostDirectiveSymbol = hostDirectiveSymbolParsed.pop(); } const result = []; if ( hostDirectiveSymbol && hostDirectiveSymbol.initializer && hostDirectiveSymbol.initializer.elements && hostDirectiveSymbol.initializer.elements.length > 0 ) { hostDirectiveSymbol.initializer.elements.forEach(element => { if (element.kind === SyntaxKind.Identifier) { result.push({ name: element.escapedText }); } else if ( element.kind === SyntaxKind.ObjectLiteralExpression && element.properties && element.properties.length > 0 ) { const parsedDirective: any = { name: '', inputs: [], outputs: [] }; element.properties.forEach(property => { if (property.name.escapedText === 'directive') { parsedDirective.name = property.initializer.escapedText; } else if (property.name.escapedText === 'inputs') { if ( property.initializer && property.initializer.elements && property.initializer.elements.length > 0 ) { property.initializer.elements.forEach(propertyElement => { parsedDirective.inputs.push(propertyElement.text); }); } } else if (property.name.escapedText === 'outputs') { if ( property.initializer && property.initializer.elements && property.initializer.elements.length > 0 ) { property.initializer.elements.forEach(propertyElement => { parsedDirective.outputs.push(propertyElement.text); }); } } }); result.push(parsedDirective); } }); } return result; } public getComponentHost( props: ReadonlyArray<ts.ObjectLiteralElementLike> ): Map<string, string> { return this.getSymbolDepsObject(props, 'host'); } public getComponentTag( props: ReadonlyArray<ts.ObjectLiteralElementLike>, srcFile: ts.SourceFile ): string { return this.symbolHelper.getSymbolDeps(props, 'tag', srcFile).pop(); } public getComponentInputsMetadata( props: ReadonlyArray<ts.ObjectLiteralElementLike>, srcFile: ts.SourceFile ): Array<string> { return this.symbolHelper.getSymbolDeps(props, 'inputs', srcFile); } public getInputOutputSignals(props) { const inputSignals = []; const outputSignals = []; const properties = []; props.forEach(prop => { const inputSignal = this.getInputSignal(prop); if (inputSignal) { inputSignals.push(inputSignal) } const outputSignal = this.getOutputSignal(prop); if (outputSignal) { outputSignals.push(outputSignal) } if (!inputSignal && !outputSignal) { properties.push(prop) } }); return {inputSignals, outputSignals, properties} } public getInputSignal(prop) { const config = this.getSignalConfig('input', prop.defaultValue) ?? this.getSignalConfig('model', prop.defaultValue); if (config) { return { ...prop, ...config }; } return undefined; } public getOutputSignal(prop) { const config = this.getSignalConfig('output', prop.defaultValue) ?? this.getSignalConfig('model', prop.defaultValue); if (config) { return { ...prop, ...config }; } return undefined; } private getSignalConfig(type: 'input' | 'output' | 'model', defaultValue: string) { // Matches a quote mark const quotePattern = `['"\`]`; // Matches a value for the input const valuePattern = (capture = true) => `(${capture ? '' : '?:'}[^()]*(?:\\([^()]*\\)[^()]*)*)`; // Matches an optional space const spacePattern = `(?: )*`; // Matches the input's type const typesPattern = `(?:<((?:${valuePattern(false)}(?:${spacePattern}\\|${spacePattern})?)+)>)?`; // Matches the alias provided in the options const aliasRegExp = new RegExp(`alias:${spacePattern}${quotePattern}(\\w+)${quotePattern}`); // Matches a signal of the provided type const signalRegExp = new RegExp( `${type}(.required)?${typesPattern}\\(${valuePattern()}?(?:,${spacePattern}({.+}))?\\)` ); const matches = signalRegExp.exec(defaultValue?.replace(/\n/g, '')); if (matches) { const [_match, required, type, defaultValue, options] = matches; const name = options?.match(aliasRegExp)?.[1]; const result = { required: !!required, type: this.parseSignalType(type), defaultValue }; if (name) { return { ...result, name }; } return result; } } public parseSignalType(type: string) { if (!type) { return type; } // adjust union string expression like: 'foo' | 'bar' | 'test' // which should be outputed as: "foo" | "bar" | "test" const unionTypeRegex = /^'([\w-]+)'\s?\|\s?('([\w-]+)'|.*)$/ let typeRest = type; let newType = ""; let typeMatch: RegExpMatchArray; while ((typeMatch = typeRest.match(unionTypeRegex))) { const [, first, rest, second] = typeMatch; if (second) { newType += `"${first}" | "${second}"`; type = newType; break; } newType += `"${first}" | `; typeRest = rest; } return type; } public getComponentStandalone( props: ReadonlyArray<ts.ObjectLiteralElementLike>, srcFile: ts.SourceFile ): boolean { let result = null; const parsedData = this.symbolHelper.getSymbolDeps(props, 'standalone', srcFile); if (parsedData.length === 1) { result = JSON.parse(parsedData[0]); } return result; } public getComponentTemplate( props: ReadonlyArray<ts.ObjectLiteralElementLike>, srcFile: ts.SourceFile ): string { let t = this.symbolHelper.getSymbolDeps(props, 'template', srcFile, true).pop(); if (t) { t = detectIndent(t, 0); t = t.replace(/\n/, ''); t = t.replace(/ +$/gm, ''); } return t; } public getComponentStyleUrls( props: ReadonlyArray<ts.ObjectLiteralElementLike>, srcFile: ts.SourceFile ): string[] { return this.symbolHelper.getSymbolDeps(props, 'styleUrls', srcFile); } public getComponentStyleUrl( props: ReadonlyArray<ts.ObjectLiteralElementLike>, srcFile: ts.SourceFile ): string { return this.symbolHelper.getSymbolDeps(props, 'styleUrl', srcFile).pop(); } public getComponentShadow( props: ReadonlyArray<ts.ObjectLiteralElementLike>, srcFile: ts.SourceFile ): string { return this.symbolHelper.getSymbolDeps(props, 'shadow', srcFile).pop(); } public getComponentScoped( props: ReadonlyArray<ts.ObjectLiteralElementLike>, srcFile: ts.SourceFile ): string { return this.symbolHelper.getSymbolDeps(props, 'scoped', srcFile).pop(); } public getComponentAssetsDir( props: ReadonlyArray<ts.ObjectLiteralElementLike>, srcFile: ts.SourceFile ): string { return this.symbolHelper.getSymbolDeps(props, 'assetsDir', srcFile).pop(); } public getComponentAssetsDirs( props: ReadonlyArray<ts.ObjectLiteralElementLike>, srcFile: ts.SourceFile ): string[] { return this.sanitizeUrls(this.symbolHelper.getSymbolDeps(props, 'assetsDir', srcFile)); } public getComponentStyles( props: ReadonlyArray<ts.ObjectLiteralElementLike>, srcFile: ts.SourceFile ): string[] { return this.symbolHelper.getSymbolDeps(props, 'styles', srcFile); } public getComponentModuleId( props: ReadonlyArray<ts.ObjectLiteralElementLike>, srcFile: ts.SourceFile ): string { return this.symbolHelper.getSymbolDeps(props, 'moduleId', srcFile).pop(); } public getComponentOutputs( props: ReadonlyArray<ts.ObjectLiteralElementLike>, srcFile: ts.SourceFile ): string[] { return this.symbolHelper.getSymbolDeps(props, 'outputs', srcFile); } public getComponentProviders( props: ReadonlyArray<ts.ObjectLiteralElementLike>, srcFile: ts.SourceFile ): Array<IParseDeepIdentifierResult> { return this.symbolHelper .getSymbolDeps(props, 'providers', srcFile) .map(name => this.symbolHelper.parseDeepIndentifier(name)); } public getComponentImports( props: ReadonlyArray<ts.ObjectLiteralElementLike>, srcFile: ts.SourceFile ): Array<IParseDeepIdentifierResult> { return this.symbolHelper .getSymbolDeps(props, 'imports', srcFile) .map(name => this.symbolHelper.parseDeepIndentifier(name)); } public getComponentEntryComponents( props: ReadonlyArray<ts.ObjectLiteralElementLike>, srcFile: ts.SourceFile ): Array<IParseDeepIdentifierResult> { return this.symbolHelper .getSymbolDeps(props, 'entryComponents', srcFile) .map(name => this.symbolHelper.parseDeepIndentifier(name)); } public getComponentViewProviders( props: ReadonlyArray<ts.ObjectLiteralElementLike>, srcFile: ts.SourceFile ): Array<IParseDeepIdentifierResult> { return this.symbolHelper .getSymbolDeps(props, 'viewProviders', srcFile) .map(name => this.symbolHelper.parseDeepIndentifier(name)); } public getComponentTemplateUrl( props: ReadonlyArray<ts.ObjectLiteralElementLike>, srcFile: ts.SourceFile ): Array<string> { return this.symbolHelper.getSymbolDeps(props, 'templateUrl', srcFile); } public getComponentExampleUrls(text: string): Array<string> | undefined { let exampleUrlsMatches = text.match(/<example-url>(.*?)<\/example-url>/g); let exampleUrls = undefined; if (exampleUrlsMatches && exampleUrlsMatches.length) { exampleUrls = exampleUrlsMatches.map(function (val) { return val.replace(/<\/?example-url>/g, ''); }); } return exampleUrls; } public getComponentPreserveWhitespaces( props: ReadonlyArray<ts.ObjectLiteralElementLike>, srcFile: ts.SourceFile ): string { return this.symbolHelper.getSymbolDeps(props, 'preserveWhitespaces', srcFile).pop(); } public getComponentSelector( props: ReadonlyArray<ts.ObjectLiteralElementLike>, srcFile: ts.SourceFile ): string { return this.symbolHelper.getSymbolDeps(props, 'selector', srcFile).pop(); } private parseProperties(node: ts.ObjectLiteralElementLike): Map<string, string> { let obj = new Map<string, string>(); const element = node as any; let properties = element.initializer?.properties || []; properties.forEach((prop: any) => { obj.set(prop.name?.text, prop.initializer?.text); }); return obj; } public getSymbolDepsObject( props: ReadonlyArray<ts.ObjectLiteralElementLike>, type: string, multiLine?: boolean ): Map<string, string> { let i = 0, len = props.length, filteredProps = []; for (i; i < len; i++) { if (props[i].name && (props[i].name as any).text === type) { filteredProps.push(props[i]); } } return filteredProps.map(x => this.parseProperties(x)).pop(); } public getComponentIO( filename: string, sourceFile: ts.SourceFile, node: ts.Node, fileBody, astFile: ts.SourceFile ): any { /** * Copyright https://github.com/ng-bootstrap/ng-bootstrap */ let reducedSource = fileBody ? fileBody.statements : sourceFile.statements; let res = reducedSource.reduce((directive, statement) => { if (ts.isClassDeclaration(statement)) { if (statement.pos === node.pos && statement.end === node.end) { return directive.concat( this.classHelper.visitClassDeclaration( filename, statement, sourceFile, astFile ) ); } } return directive; }, []); return res[0] || {}; } private sanitizeUrls(urls: Array<string>): Array<string> { return urls.map(url => url.replace('./', '')); } } export class ComponentCache { private cache: Map<string, any> = new Map(); public get(key: string): any { return this.cache.get(key); } public set(key: string, value: any): void { this.cache.set(key, value); } }