import ts, { isVariableStatement } from 'typescript'; import fs from 'fs-extra'; /** * Tools: https://ts-ast-viewer.com/ */ export interface ExportModuleItem { name: string; comment?: string; pos: number; } export interface DeclarationModuleItem { name: string; text: string; comment?: string; pos?: number; } export function parseModuleDeclaration( filePath: string, options: ts.CompilerOptions ) { const { program } = parseFile(filePath, options); const modules: Record = {}; const sourceFile = program?.getSourceFile(filePath); sourceFile?.forEachChild((node) => { if ( ts.isModuleDeclaration(node) && node.body && ts.isModuleBlock(node.body) ) { const moduleName = node.name.text; if (!modules[moduleName]) { modules[moduleName] = []; } node.body.forEachChild((item) => { if (ts.isVariableStatement(item)) { let comment: string | undefined = undefined; const commentRange = ts.getLeadingCommentRanges( sourceFile.getFullText(), item.pos ); if (Array.isArray(commentRange) && commentRange.length > 0) { comment = ''; commentRange.map(({ pos, end }) => { comment += sourceFile.text.substring(pos, end); }); } item.declarationList.declarations.forEach((declaration) => { const name = declaration.name.getText(); const pos = declaration.pos; modules[moduleName].push({ name, text: declaration.getText(), comment, pos, }); }); } }); } }); return { modules }; } /** * 解析导出文件 */ export function parseExports(filePath: string, options: ts.CompilerOptions) { const { program, service } = parseFile(filePath, options); const exportModules: ExportModuleItem[] = []; const sourceFile = program?.getSourceFile(filePath); sourceFile?.forEachChild((node) => { if (ts.isExportDeclaration(node)) { // 如果为导出声明: export { foo } from 'foo' node.exportClause?.forEachChild((exportSpec) => { if (ts.isExportSpecifier(exportSpec)) { exportModules.push({ name: exportSpec.name.text, // comment: pos: exportSpec.pos, }); } }); } else if (isExportFunc(node)) { // 如果是方法导出: export function foo() {} if (node.name) { exportModules.push({ name: node.name.text, comment: getNodeComments(node), pos: node.pos, }); } } else if ( isVariableStatement(node) && node.modifiers?.some((v) => v.kind === ts.SyntaxKind.ExportKeyword) ) { // 如果为导出变量 // export const foo = '' node.declarationList.declarations.forEach((d) => { if (ts.isIdentifier(d.name)) { exportModules.push({ name: d.name.getText(), pos: d.pos, }); } else { d.name.elements.forEach((n) => { exportModules.push({ name: n.getText(), pos: d.pos, }); }); } }); } }); return { exportModules }; } /** * 解析文件 */ export function parseFile(filePath: string, options: ts.CompilerOptions) { const host = new FileServiceHost(filePath, options); const service = ts.createLanguageService(host, ts.createDocumentRegistry()); const program = service.getProgram(); return { service, program }; } function isExportFunc(node: ts.Node): node is ts.FunctionDeclaration { if (ts.isFunctionDeclaration(node)) { if (node.modifiers) { return node.modifiers.some((m) => m.kind === ts.SyntaxKind.ExportKeyword); } } return false; } function getNodeComments(node: ts.Node): string | undefined { const comments = ts.getSyntheticLeadingComments(node); if (!comments) { return undefined; } return comments.map((c) => c.text).join('\n'); } class FileServiceHost implements ts.LanguageServiceHost { constructor(public filePath: string, private options: ts.CompilerOptions) {} getCompilationSettings = () => this.options; getScriptFileNames = () => [ this.filePath, // For test '/Users/moonrailgun/inventory/tailchat/packages/plugin-declaration-generator/test/demo/foo.ts', '/Users/moonrailgun/inventory/tailchat/packages/plugin-declaration-generator/test/demo/bar.ts', ]; getScriptVersion = () => '1'; getScriptSnapshot = (fileName: string) => { if (!fs.existsSync(fileName)) { return undefined; } return ts.ScriptSnapshot.fromString(fs.readFileSync(fileName).toString()); }; // getCurrentDirectory = () => process.cwd(); getCurrentDirectory = () => // For test '/Users/moonrailgun/inventory/tailchat/packages/plugin-declaration-generator/test/demo/'; getDefaultLibFileName = (options: ts.CompilerOptions) => ts.getDefaultLibFilePath(options); readFile(path: string): string | undefined { return fs.readFileSync(path).toString(); } fileExists(path: string): boolean { return fs.existsSync(path); } }