105 lines
2.9 KiB
TypeScript
105 lines
2.9 KiB
TypeScript
// 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<ExportInfo[]> {
|
|
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 <path>")
|
|
process.exit(1)
|
|
}
|
|
console.log(await allExports(path))
|
|
} |