parent
23d99d6a41
commit
c37b172991
@ -0,0 +1,156 @@ |
||||
import { readFileSync, writeFileSync } from "fs"; |
||||
import { resolve, dirname, relative } from "path"; |
||||
import ts from "typescript"; |
||||
|
||||
const ENTRY = resolve("src/index.ts"); |
||||
const OUT = resolve("dist/index.d.ts"); |
||||
|
||||
const configFile = ts.findConfigFile("./", ts.sys.fileExists, "tsconfig.json"); |
||||
const configHost: ts.ParseConfigFileHost = { |
||||
...ts.sys, |
||||
onUnRecoverableConfigFileDiagnostic: (d) => { |
||||
throw new Error(ts.flattenDiagnosticMessageText(d.messageText, "\n")); |
||||
}, |
||||
}; |
||||
|
||||
const parsed = ts.getParsedCommandLineOfConfigFile( |
||||
configFile!, |
||||
{ emitDeclarationOnly: true, declaration: true, noEmit: false }, |
||||
configHost |
||||
)!; |
||||
|
||||
const program = ts.createProgram([ENTRY], parsed.options); |
||||
const checker = program.getTypeChecker(); |
||||
|
||||
const emitted = new Set<string>(); // dedupe across files
|
||||
const lines: string[] = ["// Auto-generated by build-dts.ts", ""]; |
||||
|
||||
function relativePath(fullfilename: string): string { |
||||
return relative(process.cwd(), fullfilename); |
||||
} |
||||
|
||||
function fileText(sf: ts.SourceFile, node: ts.Node): string { |
||||
return sf.text.slice(node.getFullStart(), node.getEnd()).trim(); |
||||
} |
||||
|
||||
function tryResolveType(sf: ts.SourceFile, node: ts.TypeAliasDeclaration): string | null { |
||||
try { |
||||
const type = checker.getTypeAtLocation(node.name); |
||||
const resolved = checker.typeToString( |
||||
type, |
||||
undefined, |
||||
ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.UseFullyQualifiedType |
||||
); |
||||
if (resolved === node.name.text) return null; |
||||
return `export type ${node.name.text} = ${resolved};`; |
||||
} catch { |
||||
return null; |
||||
} |
||||
} |
||||
|
||||
function visitFile(sf: ts.SourceFile) { |
||||
function visit(node: ts.Node) { |
||||
const isExported = (n: ts.Node) => |
||||
(n as any).modifiers?.some((m: ts.Modifier) => m.kind === ts.SyntaxKind.ExportKeyword); |
||||
|
||||
// export * from "./x" or export type * from "./x" — follow and inline
|
||||
if (ts.isExportDeclaration(node)) { |
||||
const modSpec = node.moduleSpecifier; |
||||
if (modSpec && ts.isStringLiteral(modSpec)) { |
||||
const resolved = ts.resolveModuleName( |
||||
modSpec.text, |
||||
sf.fileName, |
||||
parsed.options, |
||||
ts.sys |
||||
).resolvedModule; |
||||
|
||||
if (resolved && !resolved.isExternalLibraryImport) { |
||||
const targetSf = program.getSourceFile(resolved.resolvedFileName); |
||||
if (targetSf) { |
||||
visitFile(targetSf); // recurse into the re-exported file
|
||||
return; |
||||
} |
||||
} |
||||
} |
||||
// external or unresolved — copy verbatim
|
||||
const text = fileText(sf, node); |
||||
if (!emitted.has(text)) { |
||||
emitted.add(text); |
||||
lines.push(`// ${relativePath(sf.fileName)}\n`); |
||||
lines.push(text); |
||||
} |
||||
return; |
||||
} |
||||
|
||||
// export type Foo = ...
|
||||
if (ts.isTypeAliasDeclaration(node) && isExported(node)) { |
||||
const resolved = tryResolveType(sf, node); |
||||
const text = resolved ?? fileText(sf, node); |
||||
if (!emitted.has(node.name.text)) { |
||||
emitted.add(node.name.text); |
||||
lines.push(`// ${relativePath(sf.fileName)}\n`); |
||||
lines.push(text); |
||||
} |
||||
return; |
||||
} |
||||
|
||||
// export interface, enum, const enum
|
||||
if ( |
||||
(ts.isInterfaceDeclaration(node) || ts.isEnumDeclaration(node)) && |
||||
isExported(node) |
||||
) { |
||||
const text = fileText(sf, node); |
||||
if (!emitted.has((node as any).name.text)) { |
||||
emitted.add((node as any).name.text); |
||||
lines.push(`// ${relativePath(sf.fileName)}\n`); |
||||
lines.push(text); |
||||
} |
||||
return; |
||||
} |
||||
|
||||
// export class
|
||||
if (ts.isClassDeclaration(node) && isExported(node) && node.name) { |
||||
const text = fileText(sf, node); |
||||
if (!emitted.has(node.name.text)) { |
||||
emitted.add(node.name.text); |
||||
lines.push(`// ${relativePath(sf.fileName)}\n`); |
||||
lines.push(text); |
||||
} |
||||
return; |
||||
} |
||||
|
||||
// export function / export const
|
||||
if ( |
||||
(ts.isFunctionDeclaration(node) || ts.isVariableStatement(node)) && |
||||
isExported(node) |
||||
) { |
||||
try { |
||||
if (ts.isFunctionDeclaration(node) && node.name) { |
||||
const sym = checker.getSymbolAtLocation(node.name); |
||||
if (sym && !emitted.has(sym.name)) { |
||||
emitted.add(sym.name); |
||||
const type = checker.getTypeOfSymbolAtLocation(sym, node); |
||||
for (const sig of type.getCallSignatures()) { |
||||
lines.push(`export declare function ${sym.name}${checker.signatureToString(sig)};`); |
||||
} |
||||
return; |
||||
} |
||||
} |
||||
} catch { } |
||||
const text = fileText(sf, node); |
||||
if (!emitted.has(text)) { emitted.add(text); lines.push(text); } |
||||
return; |
||||
} |
||||
|
||||
ts.forEachChild(node, visit); |
||||
} |
||||
|
||||
ts.forEachChild(sf, visit); |
||||
} |
||||
|
||||
const entryFile = program.getSourceFile(ENTRY)!; |
||||
visitFile(entryFile); |
||||
lines.push(""); |
||||
|
||||
writeFileSync(OUT, lines.join("\n")); |
||||
// console.log(`✓ wrote ${OUT}`);
|
||||
Loading…
Reference in new issue