优化
This commit is contained in:
1
client/packages/plugin-declaration-generator/.gitignore
vendored
Normal file
1
client/packages/plugin-declaration-generator/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
lib
|
||||
1
client/packages/plugin-declaration-generator/README.md
Normal file
1
client/packages/plugin-declaration-generator/README.md
Normal file
@@ -0,0 +1 @@
|
||||
WIP
|
||||
40
client/packages/plugin-declaration-generator/package.json
Normal file
40
client/packages/plugin-declaration-generator/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
90
client/packages/plugin-declaration-generator/src/index.ts
Normal file
90
client/packages/plugin-declaration-generator/src/index.ts
Normal 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');
|
||||
}
|
||||
112
client/packages/plugin-declaration-generator/src/parser.ts
Normal file
112
client/packages/plugin-declaration-generator/src/parser.ts
Normal 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();
|
||||
}
|
||||
191
client/packages/plugin-declaration-generator/src/tsgenerator.ts
Normal file
191
client/packages/plugin-declaration-generator/src/tsgenerator.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,9 @@
|
||||
export { foo, fooVar } from '@/foo';
|
||||
export { bar, complexBar } from '@/bar';
|
||||
|
||||
/**
|
||||
* Root export
|
||||
*/
|
||||
export function main() {
|
||||
console.log('main');
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./*"],
|
||||
}
|
||||
}
|
||||
}
|
||||
4
client/packages/plugin-declaration-generator/test/index.d.ts
vendored
Normal file
4
client/packages/plugin-declaration-generator/test/index.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module '@capital/foo' {
|
||||
export const a: any;
|
||||
export const b: string;
|
||||
}
|
||||
20
client/packages/plugin-declaration-generator/test/index.ts
Normal file
20
client/packages/plugin-declaration-generator/test/index.ts
Normal 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);
|
||||
14
client/packages/plugin-declaration-generator/test/parser.ts
Normal file
14
client/packages/plugin-declaration-generator/test/parser.ts
Normal 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())
|
||||
);
|
||||
15
client/packages/plugin-declaration-generator/tsconfig.json
Normal file
15
client/packages/plugin-declaration-generator/tsconfig.json
Normal 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/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user