// sniffs the types and params of a file's exports, including const values import ts from "typescript" export type Signature = { type: string, returnType: string | null, params: Param[] } export type Param = { name: string, type: string, optional: boolean, rest: boolean, default: string | null } export class SniffError extends Error { constructor(message: string) { super(message) this.name = "TypeError" Object.setPrototypeOf(this, SniffError.prototype) } } export type ExportInfo = | { kind: "function", name: string, signatures: Signature[] } | { kind: "value", name: string, type: string, value: string | null } let prevProgram: ts.Program | undefined export async function allExports(file: string): Promise { const program = ts.createProgram([file], { target: ts.ScriptTarget.ESNext, module: ts.ModuleKind.ESNext, moduleResolution: ts.ModuleResolutionKind.NodeNext, noLib: true, types: [], skipDefaultLibCheck: true, skipLibCheck: true }, undefined, prevProgram) prevProgram = program const checker = program.getTypeChecker() const sf = program.getSourceFile(file) if (!sf) throw new SniffError(`File not found: ${file}`) const moduleSym = (sf as any).symbol as ts.Symbol | undefined if (!moduleSym) return [] const exportSymbols = checker.getExportsOfModule(moduleSym) const result: ExportInfo[] = [] for (const sym of exportSymbols) { const decl = sym.valueDeclaration ?? sym.declarations?.[0] ?? sf const type = checker.getTypeOfSymbolAtLocation(sym, decl) const sigs = checker.getSignaturesOfType(type, ts.SignatureKind.Call) if (sigs.length > 0) { result.push({ kind: "function", name: sym.getName(), signatures: sigs.map(sig => ({ type: checker.typeToString(type, decl), returnType: checker.typeToString(checker.getReturnTypeOfSignature(sig), decl), params: sig.getParameters().map(p => { const pd: any = p.getDeclarations()?.[0] ?? decl const pt = checker.getTypeOfSymbolAtLocation(p, pd) return { name: p.getName(), type: checker.typeToString(pt, pd), optional: !!pd.questionToken || !!pd.initializer, rest: !!pd.dotDotDotToken, default: pd.initializer ? pd.initializer.getText() : null } }) })) }) } else { let value: string | null = null if (ts.isVariableDeclaration(decl) && decl.initializer) { value = decl.initializer.getText() } result.push({ kind: "value", name: sym.getName(), type: checker.typeToString(type, decl), value }) } } return result } if (import.meta.main) { const path = Bun.argv[2] if (!path) { console.error("usage: sniff ") process.exit(1) } console.log(await allExports(path)) }