You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tailchat/client/packages/plugin-declaration-generator/src/tsgenerator.ts

192 lines
5.1 KiB
TypeScript

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<string, DeclarationModuleItem[]> = {};
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);
}
}