This commit is contained in:
2026-04-25 16:36:34 +08:00
commit db90e7579b
1876 changed files with 189777 additions and 0 deletions

View File

@@ -0,0 +1 @@
lib

View File

@@ -0,0 +1 @@
WIP

View File

@@ -0,0 +1,40 @@
{
"name": "tailchat-plugin-declaration-generator",
"private": true,
"version": "1.0.0",
"description": "",
"main": "lib/index.js",
"scripts": {
"build": "tsc",
"watch": "tsc --watch",
"prepare": "tsc",
"test": "ts-node ./test/index.ts",
"test:parser": "ts-node ./test/parser.ts",
"test:parser:debug": "node -r ts-node/register --inspect-brk ./test/parser.ts"
},
"author": "",
"license": "MIT",
"dependencies": {
"@babel/generator": "^7.20.5",
"@babel/parser": "^7.20.5",
"@babel/template": "^7.18.10",
"@babel/traverse": "^7.20.5",
"fs-extra": "^10.1.0",
"glob": "^7.2.3",
"lodash": "^4.17.21",
"mkdirp": "^1.0.4",
"ts-morph": "^16.0.0",
"typescript": "^4.9.4"
},
"devDependencies": {
"@babel/types": "^7.20.5",
"@types/babel__generator": "^7.6.4",
"@types/babel__template": "^7.4.1",
"@types/babel__traverse": "^7.18.3",
"@types/fs-extra": "^9.0.13",
"@types/lodash": "^4.14.191",
"@types/mkdirp": "^1.0.2",
"@types/node": "^18.11.16",
"ts-node": "^10.9.1"
}
}

View File

@@ -0,0 +1,90 @@
import { parse, ParserPlugin } from '@babel/parser';
import traverse from '@babel/traverse';
import generate from '@babel/generator';
import template from '@babel/template';
import type { Comment } from '@babel/types';
import { program, isFunctionDeclaration } from '@babel/types';
import fs from 'fs-extra';
import _ from 'lodash';
export * from './tsgenerator';
export * from './parser';
const babelPlugins: ParserPlugin[] = ['jsx', 'typescript'];
const buildNamedExport = template('export function %%name%%(): any', {
plugins: babelPlugins,
});
interface Options {
entryPath: string;
// targetPath: string; // TODO
}
export async function generateFunctionDeclare(options: Options) {
const sourcecode = await fs.readFile(options.entryPath, 'utf8');
const exported = getSourceCodeExportedFunction(sourcecode);
const astList = exported.map((e) => {
return buildNamedExport({
name: e.name,
});
});
const code = generate(program(_.flatten(astList))).code;
return code;
}
interface ExportedItem {
name: string;
comments?: string;
}
function getSourceCodeExportedFunction(sourcecode: string): ExportedItem[] {
const ast = parse(sourcecode, {
sourceType: 'module',
plugins: babelPlugins,
});
const exported: ExportedItem[] = [];
traverse(ast, {
ExportNamedDeclaration({ node }) {
if (node.declaration) {
if (isFunctionDeclaration(node.declaration)) {
const name = node.declaration.id?.name;
if (typeof name === 'string') {
exported.push({
name,
comments: getCommentStr(node.leadingComments),
});
}
}
} else {
const names = node.specifiers.map((s) => {
const exported = s.exported;
if (exported.type === 'Identifier') {
return {
name: exported.name,
comments: getCommentStr(node.leadingComments),
};
} else {
return null;
}
});
exported.push(...names.filter((n): n is any => !!n));
}
},
});
return exported;
}
function getCommentStr(
comments: Comment[] | null | undefined
): string | undefined {
if (!comments) {
return undefined;
}
return comments.map((c) => c.value).join('\n');
}

View File

@@ -0,0 +1,112 @@
import {
Project,
ProjectOptions,
Symbol,
SyntaxKind,
ts,
Type,
} from 'ts-morph';
interface Options {
entryPath: string;
project?: ProjectOptions;
hardcodeExportType?: Record<string, string>;
}
export function parseDeclarationEntry(options: Options) {
const project = new Project(options.project);
const sourceFile = project.getSourceFileOrThrow(options.entryPath);
const hardcodeExportType = options.hardcodeExportType ?? {};
const exportDefs: { name: string; type: string }[] = [];
for (const [name, declarations] of sourceFile.getExportedDeclarations()) {
if (hardcodeExportType[name]) {
exportDefs.push({
name,
type: hardcodeExportType[name],
});
continue;
}
console.log('parsing:', name);
const typeDef = declarations
.map((d) => {
if (d.isKind(SyntaxKind.FunctionDeclaration)) {
// 如果是方法导出
return d
.getType()
.getCallSignatures()
.map((s) => {
let fnText = '';
const typeParameters = s.getTypeParameters();
if (typeParameters.length > 0) {
fnText += `<${typeParameters
.map((tp) => tp.getText())
.join(', ')}>`;
}
fnText += `(${s
.getParameters()
.map((p) => {
return parseFunctionParameter(p);
})
.join(', ')}) => ${s.getReturnType().getText()}`;
return fnText;
})
.join(' | ');
} else {
// 其他
return d.getType().getText();
}
})
.join(' | ');
exportDefs.push({
name,
type: typeDef,
});
}
return { exportDefs, project };
}
/**
* 解析函数参数为字符串
*/
function parseFunctionParameter(parameter: Symbol): string {
const name = parameter.getName();
const isOptional = parameter.isOptional();
const type = parseType(parameter.getDeclarations()[0].getType());
if (isOptional) {
return `${name}?: ${type}`;
} else {
return `${name}: ${type}`;
}
}
/**
* 解析函数
*/
function parseType(type: Type<ts.Type>): string {
if (type.isAnonymous()) {
return type.getText();
}
if (type.isObject()) {
const properties = type.getApparentProperties();
debugger;
return `{ ${properties
.map((p) => {
const t = p.getDeclarations()[0].getType();
const text = parseType(t);
return `${p.getName()}: ${text}`;
})
.join(', ')} }`;
}
return type.getText();
}

View File

@@ -0,0 +1,191 @@
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);
}
}

View File

@@ -0,0 +1,26 @@
/**
* This is bar
*/
export function bar() {
console.log('Anything else');
}
interface E {
f: symbol;
}
interface Options {
a: number;
b: string;
c: {
d: string;
e: E;
};
}
/**
* This is bar with complex input
*/
export function complexBar(input: Options) {
console.log('Anything else', input);
}

View File

@@ -0,0 +1,13 @@
import * as mkdirp from 'mkdirp';
/**
* This is foo
*/
export function foo(input: string) {
console.log('Anything', input);
mkdirp('./foo/foo/foo/foo/foo/foo/foo');
return input + 1;
}
export const fooVar = 'fooVar' as string;

View File

@@ -0,0 +1,9 @@
export { foo, fooVar } from '@/foo';
export { bar, complexBar } from '@/bar';
/**
* Root export
*/
export function main() {
console.log('main');
}

View File

@@ -0,0 +1,7 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./*"],
}
}
}

View File

@@ -0,0 +1,4 @@
declare module '@capital/foo' {
export const a: any;
export const b: string;
}

View File

@@ -0,0 +1,20 @@
import { parseExports, parseModuleDeclaration } from '../src/tsgenerator';
import path from 'path';
const { exportModules } = parseExports(
path.resolve(__dirname, './demo/index.ts'),
{
paths: { '@/*': ['./*'] },
}
);
console.log('exportModules', exportModules);
const { modules } = parseModuleDeclaration(
path.resolve(__dirname, './index.d.ts'),
{
paths: { '@/*': ['./*'] },
}
);
console.log('modules', modules);

View File

@@ -0,0 +1,14 @@
import { parseDeclarationEntry } from '../src/parser';
import path from 'path';
const project = parseDeclarationEntry({
entryPath: path.resolve(__dirname, './demo/index.ts'),
project: {
tsConfigFilePath: path.resolve(__dirname, './demo/tsconfig.json'),
},
});
console.log(
'sourceFile',
project.getSourceFiles().map((item) => item.getFilePath())
);

View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "esnext",
"lib": ["ESNext"],
"outDir": "lib",
"declaration": true,
"esModuleInterop": true,
"isolatedModules": true,
"module": "CommonJS",
"moduleResolution": "node",
"strict": true,
"importsNotUsedAsValues": "error",
},
"include": ["./src/*"]
}