@compodoc/compodoc
Version:
The missing documentation tool for your Angular application
485 lines (416 loc) • 16.3 kB
text/typescript
import * as semver from 'semver';
import { SyntaxKind, ts } from 'ts-morph';
import Configuration from '../../../../configuration';
import { detectIndent } from '../../../../../utils';
import { ClassHelper } from './class-helper';
import { SignalConfigParser } from './signal-config.parser';
import { IParseDeepIdentifierResult, SymbolHelper } from './symbol-helper';
export class ComponentHelper {
constructor(
private classHelper: ClassHelper,
private symbolHelper: SymbolHelper = new SymbolHelper(),
private signalConfigParser: SignalConfigParser = new SignalConfigParser()
) {}
public getComponentChangeDetection(
props: ReadonlyArray<ts.ObjectLiteralElementLike>,
srcFile: ts.SourceFile
): string {
const result = this.symbolHelper.getSymbolDeps(props, 'changeDetection', srcFile).pop();
if (typeof result === 'undefined') {
const angularVersion = Configuration.mainData.angularVersion;
const coerced = angularVersion ? semver.coerce(angularVersion) : null;
if (coerced && semver.valid(coerced) && semver.gte(coerced.version, '22.0.0')) {
return 'ChangeDetectionStrategy.OnPush';
}
}
return result;
}
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) {
return this.signalConfigParser.getSignalConfig(type, defaultValue);
}
public parseSignalType(type: string) {
return this.signalConfigParser.parseSignalType(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]);
}
if (result === null) {
const angularVersion = Configuration.mainData.angularVersion;
const coerced = angularVersion ? semver.coerce(angularVersion) : null;
if (coerced && semver.valid(coerced) && semver.gte(coerced.version, '19.0.0')) {
result = true;
}
}
return result;
}
public getComponentTemplate(
props: ReadonlyArray<ts.ObjectLiteralElementLike>,
srcFile: ts.SourceFile
): string {
let t = this.symbolHelper.getSymbolDeps(props, 'template', srcFile, true).pop();
if (t && typeof t === 'string') {
t = detectIndent(t, 0);
t = t.replace(/\n/, '');
t = t.replace(/ +$/gm, '');
}
return t;
}
public getComponentTemplateVariables(template: string): Array<IComponentTemplateVariable> {
return ComponentHelper.getTemplateVariablesFromTemplate(template);
}
public static getTemplateVariablesFromTemplate(
template: string
): Array<IComponentTemplateVariable> {
if (!template || typeof template !== 'string') {
return [];
}
const variables: Array<IComponentTemplateVariable> = [];
const regex = /\s+([A-Za-z_$][\w$]*)\s*=\s*([^;]+);/g;
let match: RegExpExecArray | null;
while ((match = regex.exec(template))) {
variables.push({
name: match[1],
defaultValue: match[2].trim()
});
}
return variables;
}
public getComponentStyleUrls(
props: ReadonlyArray<ts.ObjectLiteralElementLike>,
srcFile: ts.SourceFile
): string[] {
const plural = this.symbolHelper.getSymbolDeps(props, 'styleUrls', srcFile);
const singular = this.symbolHelper.getSymbolDeps(props, 'styleUrl', srcFile);
return [...new Set([...plural, ...singular])];
}
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 interface IComponentTemplateVariable {
name: string;
defaultValue: string;
}
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);
}
}