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