import * as ts from "typescript";
import * as fs from "fs";
import * as path from "path";
import { glob } from "glob";
import { TypeDefinition, PropertyDefinition, MethodDefinition, ParameterDefinition, ProjectConfig, PackageInfo } from "./types.js";
export class TypeIndexer {
private program: ts.Program | null = null;
private typeChecker: ts.TypeChecker | null = null;
private sourceFiles: Map<string, ts.SourceFile> = new Map();
private typeCache: Map<string, TypeDefinition[]> = new Map();
private config: ProjectConfig;
constructor(config: ProjectConfig) {
this.config = config;
}
async initialize(): Promise<void> {
const compilerOptions = this.getCompilerOptions();
const rootFiles = await this.findTypeScriptFiles();
this.program = ts.createProgram(rootFiles, compilerOptions);
this.typeChecker = this.program.getTypeChecker();
// Cache all source files
for (const sourceFile of this.program.getSourceFiles()) {
if (!sourceFile.isDeclarationFile && rootFiles.includes(sourceFile.fileName)) {
this.sourceFiles.set(sourceFile.fileName, sourceFile);
}
}
}
private getCompilerOptions(): ts.CompilerOptions {
if (this.config.tsconfigPath && fs.existsSync(this.config.tsconfigPath)) {
const configFile = ts.readConfigFile(this.config.tsconfigPath, ts.sys.readFile);
if (configFile.config) {
const parsed = ts.parseJsonConfigFileContent(
configFile.config,
ts.sys,
path.dirname(this.config.tsconfigPath)
);
return parsed.options;
}
}
return {
target: ts.ScriptTarget.ES2020,
module: ts.ModuleKind.CommonJS,
lib: ["ES2020", "DOM"],
declaration: true,
esModuleInterop: true,
skipLibCheck: true,
strict: true,
moduleResolution: ts.ModuleResolutionKind.NodeJs,
allowSyntheticDefaultImports: true,
resolveJsonModule: true
};
}
private async findTypeScriptFiles(): Promise<string[]> {
const patterns = [
path.join(this.config.rootPath, "**/*.ts"),
path.join(this.config.rootPath, "**/*.tsx"),
path.join(this.config.rootPath, "**/*.d.ts")
];
if (this.config.includeNodeModules) {
patterns.push(
path.join(this.config.rootPath, "node_modules/**/*.d.ts"),
path.join(this.config.rootPath, "node_modules/@types/**/*.d.ts")
);
}
const allFiles: string[] = [];
for (const pattern of patterns) {
const files = await glob(pattern, {
ignore: this.config.excludePatterns,
absolute: true
});
allFiles.push(...files);
}
return [...new Set(allFiles)]; // Remove duplicates
}
async findType(typeName: string, packageName?: string): Promise<TypeDefinition[]> {
if (!this.program || !this.typeChecker) {
throw new Error("TypeIndexer not initialized. Call initialize() first.");
}
const cacheKey = `${typeName}:${packageName || ""}`;
if (this.typeCache.has(cacheKey)) {
const cached = this.typeCache.get(cacheKey)!;
// Check cache age
const now = Date.now();
if (now - this.getCacheTimestamp(cacheKey) < this.config.maxCacheAge) {
return cached;
}
}
const results: TypeDefinition[] = [];
for (const sourceFile of this.program.getSourceFiles()) {
if (packageName && !this.isFromPackage(sourceFile.fileName, packageName)) {
continue;
}
const definitions = this.extractTypeDefinitions(sourceFile, typeName);
results.push(...definitions);
}
this.typeCache.set(cacheKey, results);
this.setCacheTimestamp(cacheKey, Date.now());
return results;
}
private isFromPackage(fileName: string, packageName: string): boolean {
return fileName.includes(`node_modules/${packageName}`) ||
fileName.includes(`node_modules/@types/${packageName.replace("@", "").replace("/", "__")}`);
}
private extractTypeDefinitions(sourceFile: ts.SourceFile, typeName?: string): TypeDefinition[] {
const definitions: TypeDefinition[] = [];
const visit = (node: ts.Node) => {
if (typeName && !this.nodeMatchesTypeName(node, typeName)) {
ts.forEachChild(node, visit);
return;
}
const definition = this.processNodeByKind(node, sourceFile);
if (definition) {
if (Array.isArray(definition)) {
definitions.push(...definition);
} else {
definitions.push(definition);
}
}
ts.forEachChild(node, visit);
};
visit(sourceFile);
return definitions;
}
private nodeMatchesTypeName(node: ts.Node, typeName: string): boolean {
if (ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) ||
ts.isClassDeclaration(node) || ts.isEnumDeclaration(node)) {
return node.name?.getText() === typeName;
}
if (ts.isFunctionDeclaration(node)) {
return node.name?.getText() === typeName;
}
return true; // For other nodes, let the specific processors decide
}
// Process nodes by kind using a cleaner pattern
private processNodeByKind(node: ts.Node, sourceFile: ts.SourceFile): TypeDefinition | TypeDefinition[] | null {
if (ts.isInterfaceDeclaration(node)) {
return this.processInterface(node, sourceFile);
}
if (ts.isTypeAliasDeclaration(node)) {
return this.processTypeAlias(node, sourceFile);
}
if (ts.isClassDeclaration(node)) {
return this.processClass(node, sourceFile);
}
if (ts.isEnumDeclaration(node)) {
return this.processEnum(node, sourceFile);
}
if (ts.isFunctionDeclaration(node)) {
return this.processFunction(node, sourceFile);
}
if (ts.isVariableStatement(node)) {
return this.processVariableStatement(node, sourceFile);
}
return null;
}
private processInterface(node: ts.InterfaceDeclaration, sourceFile: ts.SourceFile): TypeDefinition {
const name = node.name.getText();
const properties: PropertyDefinition[] = [];
for (const member of node.members) {
if (ts.isPropertySignature(member)) {
properties.push(this.processPropertySignature(member));
}
}
return {
name,
kind: "interface",
file: sourceFile.fileName,
line: sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1,
definition: node.getText(),
properties,
extends: node.heritageClauses?.find(h => h.token === ts.SyntaxKind.ExtendsKeyword)
?.types.map(t => t.getText()) || [],
generics: node.typeParameters?.map(t => t.getText()) || [],
jsDoc: this.getJsDoc(node),
packageName: this.getPackageName(sourceFile.fileName)
};
}
private processTypeAlias(node: ts.TypeAliasDeclaration, sourceFile: ts.SourceFile): TypeDefinition {
return {
name: node.name.getText(),
kind: "type",
file: sourceFile.fileName,
line: sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1,
definition: node.getText(),
generics: node.typeParameters?.map(t => t.getText()) || [],
jsDoc: this.getJsDoc(node),
packageName: this.getPackageName(sourceFile.fileName)
};
}
private processClass(node: ts.ClassDeclaration, sourceFile: ts.SourceFile): TypeDefinition {
const name = node.name?.getText() || "anonymous";
const properties: PropertyDefinition[] = [];
const methods: MethodDefinition[] = [];
for (const member of node.members) {
if (ts.isPropertyDeclaration(member)) {
properties.push(this.processPropertyDeclaration(member));
} else if (ts.isMethodDeclaration(member)) {
methods.push(this.processMethodDeclaration(member));
}
}
return {
name,
kind: "class",
file: sourceFile.fileName,
line: sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1,
definition: node.getText(),
properties,
methods,
extends: node.heritageClauses?.find(h => h.token === ts.SyntaxKind.ExtendsKeyword)
?.types.map(t => t.getText()) || [],
implements: node.heritageClauses?.find(h => h.token === ts.SyntaxKind.ImplementsKeyword)
?.types.map(t => t.getText()) || [],
generics: node.typeParameters?.map(t => t.getText()) || [],
jsDoc: this.getJsDoc(node),
packageName: this.getPackageName(sourceFile.fileName)
};
}
private processEnum(node: ts.EnumDeclaration, sourceFile: ts.SourceFile): TypeDefinition {
return {
name: node.name.getText(),
kind: "enum",
file: sourceFile.fileName,
line: sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1,
definition: node.getText(),
jsDoc: this.getJsDoc(node),
packageName: this.getPackageName(sourceFile.fileName)
};
}
private processFunction(node: ts.FunctionDeclaration, sourceFile: ts.SourceFile): TypeDefinition {
const name = node.name?.getText() || "anonymous";
return {
name,
kind: "function",
file: sourceFile.fileName,
line: sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1,
definition: node.getText(),
jsDoc: this.getJsDoc(node),
packageName: this.getPackageName(sourceFile.fileName)
};
}
private processVariableStatement(node: ts.VariableStatement, sourceFile: ts.SourceFile): TypeDefinition[] {
const definitions: TypeDefinition[] = [];
for (const declaration of node.declarationList.declarations) {
if (declaration.name && ts.isIdentifier(declaration.name)) {
definitions.push({
name: declaration.name.getText(),
kind: "variable",
file: sourceFile.fileName,
line: sourceFile.getLineAndCharacterOfPosition(declaration.getStart()).line + 1,
definition: node.getText(),
jsDoc: this.getJsDoc(node),
packageName: this.getPackageName(sourceFile.fileName)
});
}
}
return definitions;
}
private processPropertySignature(member: ts.PropertySignature): PropertyDefinition {
const name = member.name?.getText() || "";
const type = member.type?.getText() || "any";
const optional = !!member.questionToken;
const readonly = member.modifiers?.some(m => m.kind === ts.SyntaxKind.ReadonlyKeyword) || false;
return {
name,
type,
optional,
readonly,
jsDoc: this.getJsDoc(member)
};
}
private processPropertyDeclaration(member: ts.PropertyDeclaration): PropertyDefinition {
const name = member.name?.getText() || "";
const type = member.type?.getText() || "any";
const optional = !!member.questionToken;
const readonly = member.modifiers?.some(m => m.kind === ts.SyntaxKind.ReadonlyKeyword) || false;
return {
name,
type,
optional,
readonly,
jsDoc: this.getJsDoc(member)
};
}
private processMethodDeclaration(member: ts.MethodDeclaration): MethodDefinition {
const name = member.name?.getText() || "";
const optional = !!member.questionToken;
const returnType = member.type?.getText() || "void";
const parameters: ParameterDefinition[] = [];
for (const param of member.parameters) {
if (ts.isParameter(param) && param.name && ts.isIdentifier(param.name)) {
parameters.push({
name: param.name.getText(),
type: param.type?.getText() || "any",
optional: !!param.questionToken,
defaultValue: param.initializer?.getText()
});
}
}
return {
name,
parameters,
returnType,
optional,
jsDoc: this.getJsDoc(member)
};
}
private getJsDoc(node: ts.Node): string | undefined {
const jsDoc = ts.getJSDocCommentsAndTags(node);
if (jsDoc.length > 0) {
return jsDoc.map(j => j.getText()).join("\n");
}
return undefined;
}
private getPackageName(fileName: string): string | undefined {
const nodeModulesMatch = fileName.match(/node_modules\/(@?[^\/]+(?:\/[^\/]+)?)/);
if (nodeModulesMatch) {
return nodeModulesMatch[1];
}
return undefined;
}
// Cache timestamp storage
private cacheTimestamps = new Map<string, number>();
private getCacheTimestamp(key: string): number {
return this.cacheTimestamps.get(key) ?? 0;
}
private setCacheTimestamp(key: string, timestamp: number): void {
this.cacheTimestamps.set(key, timestamp);
}
async findInterfaces(pattern: string): Promise<TypeDefinition[]> {
if (!this.program) {
throw new Error("TypeIndexer not initialized. Call initialize() first.");
}
const results: TypeDefinition[] = [];
const regex = new RegExp(pattern.replace(/\*/g, ".*"));
for (const sourceFile of this.program.getSourceFiles()) {
const definitions = this.extractTypeDefinitions(sourceFile);
const matching = definitions.filter(def =>
def.kind === "interface" && regex.test(def.name)
);
results.push(...matching);
}
return results;
}
async getPackageTypes(packageName: string): Promise<TypeDefinition[]> {
if (!this.program) {
throw new Error("TypeIndexer not initialized. Call initialize() first.");
}
const results: TypeDefinition[] = [];
for (const sourceFile of this.program.getSourceFiles()) {
if (this.isFromPackage(sourceFile.fileName, packageName)) {
const definitions = this.extractTypeDefinitions(sourceFile);
results.push(...definitions);
}
}
return results;
}
}