sniff sniff
This commit is contained in:
parent
7385900445
commit
7a2af832f4
105
app/src/sniff.ts
Normal file
105
app/src/sniff.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
// 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))
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user