mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2026-06-12 02:12:28 +00:00
614 lines
16 KiB
TypeScript
614 lines
16 KiB
TypeScript
import * as path from 'path';
|
|
import { Parser, ComponentDoc } from 'react-docgen-typescript';
|
|
import ts, { SymbolFlags, TypeFlags, SyntaxKind } from 'typescript';
|
|
import { isEmpty, isEqual } from 'lodash';
|
|
import { existsSync, readFileSync } from 'fs-extra';
|
|
import findConfig from 'find-config';
|
|
import { debug } from '../../core';
|
|
import { Json } from '../../types';
|
|
import { transformItem } from '../transform';
|
|
import generateDTS from './generateDTS';
|
|
import { IParseArgs } from '../index';
|
|
|
|
const log = debug.extend('parse:ts');
|
|
|
|
type ExtendedType = ts.Type & {
|
|
id: string;
|
|
typeArguments: any[];
|
|
};
|
|
|
|
function getNextParentIds(parentIds: number[], type: ts.Type) {
|
|
// @ts-ignore
|
|
const id = type?.symbol?.id;
|
|
if (id) {
|
|
return [...parentIds, id];
|
|
}
|
|
return parentIds;
|
|
}
|
|
|
|
function getSymbolName(symbol: ts.Symbol) {
|
|
// @ts-ignore
|
|
const prefix: string = symbol?.parent && getSymbolName(symbol.parent);
|
|
const name = symbol.getName();
|
|
if (prefix && prefix.length <= 20) {
|
|
return `${prefix}.${name}`;
|
|
}
|
|
return name;
|
|
}
|
|
|
|
function getFunctionParams(parameters: any[] = [], checker, parentIds, type) {
|
|
return parameters.map((node) => {
|
|
const typeObject = checker.getTypeOfSymbolAtLocation(node.symbol, node.symbol.valueDeclaration);
|
|
const v = getDocgenTypeHelper(checker, typeObject, false, getNextParentIds(parentIds, type));
|
|
const name = node.symbol.escapedName;
|
|
return {
|
|
name,
|
|
propType: v,
|
|
};
|
|
});
|
|
}
|
|
|
|
function getFunctionReturns(node: any, checker, parentIds, type) {
|
|
if (!node) return {};
|
|
const propType = getDocgenTypeHelper(
|
|
checker,
|
|
node.type,
|
|
false,
|
|
getNextParentIds(parentIds, type),
|
|
);
|
|
return {
|
|
propType,
|
|
};
|
|
}
|
|
|
|
const blacklistNames = [
|
|
'prototype',
|
|
'getDerivedStateFromProps',
|
|
'propTypes',
|
|
'defaultProps',
|
|
'contextTypes',
|
|
'displayName',
|
|
'contextType',
|
|
'Provider',
|
|
'Consumer',
|
|
];
|
|
|
|
const blacklistPatterns = [
|
|
/^HTML/,
|
|
/^React\./,
|
|
/^Object$/,
|
|
/^Date$/,
|
|
/^Promise$/,
|
|
/^XML/,
|
|
/^Function$/,
|
|
];
|
|
|
|
// function hasTooManyTypes(type) {
|
|
// return type?.types?.length >= 20;
|
|
// }
|
|
|
|
function isComplexType(type) {
|
|
let isAliasSymbol = false;
|
|
let symbol = type?.symbol;
|
|
if (!symbol) {
|
|
symbol = type?.aliasSymbol;
|
|
isAliasSymbol = true;
|
|
}
|
|
if (!symbol) return false;
|
|
if (isAliasSymbol) {
|
|
return false;
|
|
}
|
|
const name = getSymbolName(symbol);
|
|
if (blacklistPatterns.some((patt) => patt.test(name))) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function getDocgenTypeHelper(
|
|
checker: ts.TypeChecker,
|
|
type: ts.Type,
|
|
skipRequired = false,
|
|
parentIds: number[] = [],
|
|
isRequired = false,
|
|
): any {
|
|
function isTuple(_type: ts.Type) {
|
|
// @ts-ignore use internal methods
|
|
return checker.isArrayLikeType(_type) && !checker.isArrayType(_type);
|
|
}
|
|
let required: boolean;
|
|
if (isRequired !== undefined) {
|
|
required = isRequired;
|
|
} else {
|
|
required = !(type.flags & SymbolFlags.Optional) || isRequired;
|
|
}
|
|
|
|
function makeResult(typeInfo: Json) {
|
|
if (skipRequired) {
|
|
return {
|
|
raw: checker.typeToString(type),
|
|
...typeInfo,
|
|
};
|
|
} else {
|
|
return {
|
|
required,
|
|
raw: checker.typeToString(type),
|
|
...typeInfo,
|
|
};
|
|
}
|
|
}
|
|
|
|
function getShapeFromArray(symbolArr: ts.Symbol[], _type: ts.Type) {
|
|
const shape: Array<{
|
|
key:
|
|
| {
|
|
name: string;
|
|
}
|
|
| string;
|
|
value: any;
|
|
}> = symbolArr.map((prop) => {
|
|
const propType = checker.getTypeOfSymbolAtLocation(
|
|
prop,
|
|
// @ts-ignore
|
|
prop.valueDeclaration || (prop.declarations && prop.declarations[0]) || {},
|
|
);
|
|
return {
|
|
key: prop.getName(),
|
|
|
|
value: getDocgenTypeHelper(
|
|
checker,
|
|
propType,
|
|
false,
|
|
// @ts-ignore
|
|
getNextParentIds(parentIds, _type),
|
|
// @ts-ignore
|
|
!prop?.valueDeclaration?.questionToken,
|
|
),
|
|
};
|
|
});
|
|
// @ts-ignore use internal methods
|
|
if (checker.isArrayLikeType(_type)) {
|
|
return shape;
|
|
}
|
|
if (_type.getStringIndexType()) {
|
|
// @ts-ignore use internal methods
|
|
if (!_type.stringIndexInfo) {
|
|
return shape;
|
|
}
|
|
shape.push({
|
|
key: {
|
|
name: 'string',
|
|
},
|
|
value: getDocgenTypeHelper(
|
|
checker,
|
|
// @ts-ignore use internal methods
|
|
_type.stringIndexInfo.type,
|
|
false,
|
|
getNextParentIds(parentIds, _type),
|
|
),
|
|
});
|
|
} else if (_type.getNumberIndexType()) {
|
|
// @ts-ignore use internal methods
|
|
if (!_type.numberIndexInfo) {
|
|
return shape;
|
|
}
|
|
shape.push({
|
|
key: {
|
|
name: 'number',
|
|
},
|
|
|
|
value: getDocgenTypeHelper(
|
|
checker,
|
|
// @ts-ignore use internal methods
|
|
_type.numberIndexInfo.type,
|
|
false,
|
|
getNextParentIds(parentIds, _type),
|
|
),
|
|
});
|
|
}
|
|
return shape;
|
|
}
|
|
|
|
function getShape(_type: ts.Type) {
|
|
const { symbol } = _type;
|
|
if (symbol && symbol.members) {
|
|
// @ts-ignore
|
|
const props: ts.Symbol[] = Array.from(symbol.members.values());
|
|
// if (props.length >= 20) {
|
|
// throw new Error('too many props');
|
|
// }
|
|
return getShapeFromArray(
|
|
props.filter((prop) => prop.getName() !== '__index'),
|
|
_type,
|
|
);
|
|
} else {
|
|
// @ts-ignore
|
|
const args = _type.resolvedTypeArguments || [];
|
|
const props = checker.getPropertiesOfType(_type);
|
|
// if (props.length >= 20) {
|
|
// throw new Error('too many props');
|
|
// }
|
|
const shape = getShapeFromArray(props.slice(0, args.length), _type);
|
|
return shape;
|
|
}
|
|
}
|
|
|
|
// @ts-ignore
|
|
if (type?.kind === SyntaxKind.VoidExpression) {
|
|
return makeResult({
|
|
name: 'void',
|
|
raw: 'void',
|
|
});
|
|
}
|
|
|
|
const pattern = /^__global\.(.+)$/;
|
|
// @ts-ignore
|
|
if (parentIds.includes(type?.symbol?.id)) {
|
|
return makeResult({
|
|
name: 'object', // checker.typeToString(type),
|
|
});
|
|
}
|
|
if (type.symbol) {
|
|
const symbolName = getSymbolName(type.symbol);
|
|
if (symbolName) {
|
|
const matches = pattern.exec(symbolName);
|
|
if (matches) {
|
|
return makeResult({
|
|
name: matches[1],
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (type.flags & TypeFlags.Number) {
|
|
return makeResult({
|
|
name: 'number',
|
|
});
|
|
} else if (type.flags & TypeFlags.String) {
|
|
return makeResult({
|
|
name: 'string',
|
|
});
|
|
} else if (type.flags & TypeFlags.NumberLiteral) {
|
|
return makeResult({
|
|
name: 'literal',
|
|
// @ts-ignore
|
|
value: type.value,
|
|
});
|
|
} else if (type.flags & TypeFlags.Literal) {
|
|
return makeResult({
|
|
name: 'literal',
|
|
value: checker.typeToString(type),
|
|
});
|
|
} else if (type.symbol?.flags & SymbolFlags.Enum) {
|
|
return makeResult({
|
|
name: 'union',
|
|
// @ts-ignore
|
|
value: type.types.map((t) => t.value),
|
|
});
|
|
// @ts-ignore
|
|
} else if (type.flags & TypeFlags.DisjointDomains) {
|
|
return makeResult({
|
|
name: checker.typeToString(type),
|
|
});
|
|
} else if (type.flags & TypeFlags.Any) {
|
|
return makeResult({
|
|
name: 'any',
|
|
});
|
|
} else if (type.flags & TypeFlags.Undefined) {
|
|
return makeResult({
|
|
name: 'undefined',
|
|
});
|
|
} else if (type.flags & TypeFlags.Union && !isComplexType(type)) {
|
|
return makeResult({
|
|
name: 'union',
|
|
// @ts-ignore
|
|
value: type.types.map((t) =>
|
|
getDocgenTypeHelper(checker, t, true, getNextParentIds(parentIds, type))),
|
|
});
|
|
} else if (isComplexType(type)) {
|
|
return makeResult({
|
|
name: getSymbolName(type?.symbol || type?.aliasSymbol),
|
|
});
|
|
} else if (type.flags & (TypeFlags.Object | TypeFlags.Intersection)) {
|
|
if (isTuple(type)) {
|
|
try {
|
|
const props = getShape(type);
|
|
return makeResult({
|
|
name: 'tuple',
|
|
value: props.map((p) => p.value),
|
|
});
|
|
} catch (e) {
|
|
return makeResult({
|
|
name: 'object',
|
|
});
|
|
}
|
|
|
|
// @ts-ignore
|
|
} else if (checker.isArrayType(type)) {
|
|
return makeResult({
|
|
name: 'Array',
|
|
// @ts-ignore
|
|
elements: [
|
|
getDocgenTypeHelper(
|
|
checker,
|
|
(type as ExtendedType).typeArguments[0],
|
|
false,
|
|
getNextParentIds(parentIds, type),
|
|
),
|
|
],
|
|
});
|
|
// @ts-ignore
|
|
} else if (type?.symbol?.valueDeclaration?.parameters?.length) {
|
|
return makeResult({
|
|
name: 'func',
|
|
params: getFunctionParams(
|
|
// @ts-ignore
|
|
type?.symbol?.valueDeclaration?.parameters,
|
|
checker,
|
|
parentIds,
|
|
type,
|
|
),
|
|
returns: getFunctionReturns(
|
|
checker.typeToTypeNode(type, type?.symbol?.valueDeclaration),
|
|
checker,
|
|
parentIds,
|
|
type,
|
|
),
|
|
});
|
|
} else if (
|
|
// @ts-ignore
|
|
type?.members?.get('__call')?.declarations[0]?.symbol?.declarations[0]?.parameters?.length
|
|
) {
|
|
return makeResult({
|
|
name: 'func',
|
|
params: getFunctionParams(
|
|
// @ts-ignore
|
|
type?.members?.get('__call')?.declarations[0]?.symbol?.declarations[0]?.parameters,
|
|
checker,
|
|
parentIds,
|
|
type,
|
|
),
|
|
});
|
|
} else {
|
|
try {
|
|
const props = getShape(type);
|
|
return makeResult({
|
|
name: 'signature',
|
|
type: {
|
|
signature: {
|
|
properties: props,
|
|
},
|
|
},
|
|
});
|
|
} catch (e) {
|
|
return makeResult({
|
|
name: 'object',
|
|
});
|
|
}
|
|
}
|
|
} else {
|
|
return makeResult({
|
|
name: 'object',
|
|
});
|
|
}
|
|
}
|
|
class MyParser extends Parser {
|
|
getDocgenType(propType: ts.Type): any {
|
|
const parentIds = [];
|
|
// @ts-ignore
|
|
const parentId = propType?.symbol?.parent?.id;
|
|
if (parentId) {
|
|
parentIds.push(parentId);
|
|
}
|
|
// @ts-ignore
|
|
const result = getDocgenTypeHelper(this.checker, propType, true, parentIds);
|
|
return result;
|
|
}
|
|
|
|
// override the builtin method, to avoid the false positive
|
|
public extractPropsFromTypeIfStatelessComponent(type: ts.Type): ts.Symbol | null {
|
|
const callSignatures = type.getCallSignatures();
|
|
|
|
if (callSignatures.length) {
|
|
// Could be a stateless component. Is a function, so the props object we're interested
|
|
// in is the (only) parameter.
|
|
|
|
for (const sig of callSignatures) {
|
|
const params = sig.getParameters();
|
|
if (params.length === 0) {
|
|
continue;
|
|
}
|
|
|
|
// @ts-ignore
|
|
const returnSymbol = this.checker.getReturnTypeOfSignature(sig);
|
|
if (!returnSymbol) continue;
|
|
const symbol = returnSymbol?.symbol;
|
|
if (!symbol) continue;
|
|
// @ts-ignore
|
|
const typeString = this.checker.symbolToString(symbol);
|
|
if (
|
|
typeString.startsWith('ReactElement') ||
|
|
typeString.startsWith('Element') ||
|
|
typeString.startsWith('RaxElement')
|
|
) {
|
|
const propsParam = params[0];
|
|
if (propsParam) {
|
|
return propsParam;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
const getCompilerOptions = (reactTypePath, originalReactTypePath) => {
|
|
const options: any = {
|
|
jsx: ts.JsxEmit.React,
|
|
module: ts.ModuleKind.CommonJS,
|
|
target: ts.ScriptTarget.Latest,
|
|
allowSyntheticDefaultImports: true,
|
|
};
|
|
// if (reactTypePath) {
|
|
// options.paths = {
|
|
// react: [reactTypePath],
|
|
// };
|
|
// options.exclude = [path.dirname(originalReactTypePath)];
|
|
// options.types = [];
|
|
// options.skipLibCheck = true;
|
|
// }
|
|
return options;
|
|
};
|
|
|
|
interface SymbolWithMeta extends ts.Symbol {
|
|
meta?: {
|
|
exportName: string;
|
|
subName?: string;
|
|
};
|
|
}
|
|
|
|
function getComponentName(exportName, displayName) {
|
|
if (displayName) {
|
|
const firstCharCode = displayName.charCodeAt(0);
|
|
if (firstCharCode >= 65 && firstCharCode <= 90) {
|
|
return displayName || exportName;
|
|
}
|
|
}
|
|
return exportName;
|
|
}
|
|
|
|
const defaultTsConfigPath = path.resolve(__dirname, './tsconfig.json');
|
|
|
|
export default function parseTS(filePath: string, args: IParseArgs): ComponentDoc[] {
|
|
if (!filePath) return [];
|
|
|
|
let basePath = args.moduleDir || args.workDir || path.dirname(filePath);
|
|
let tsConfigPath = findConfig('tsconfig.json', { cwd: basePath }); // path.resolve(basePath, 'tsconfig.json')
|
|
if (
|
|
!tsConfigPath ||
|
|
!existsSync(tsConfigPath) ||
|
|
(args.accesser === 'online' && tsConfigPath === 'tsconfig.json')
|
|
) {
|
|
tsConfigPath = defaultTsConfigPath;
|
|
} else {
|
|
basePath = path.dirname(tsConfigPath);
|
|
}
|
|
|
|
log('ts config path is', tsConfigPath);
|
|
const { config, error } = ts.readConfigFile(tsConfigPath, (filename) =>
|
|
readFileSync(filename, 'utf8'));
|
|
|
|
if (error !== undefined) {
|
|
const errorText = `Cannot load custom tsconfig.json from provided path: ${tsConfigPath}, with error code: ${error.code}, message: ${error.messageText}`;
|
|
throw new Error(errorText);
|
|
}
|
|
|
|
const { options, errors } = ts.parseJsonConfigFileContent(
|
|
config,
|
|
ts.sys,
|
|
basePath,
|
|
{},
|
|
tsConfigPath,
|
|
);
|
|
|
|
if (errors && errors.length) {
|
|
throw errors[0];
|
|
}
|
|
log('ts config is', options);
|
|
// const filePaths = Array.isArray(filePathOrPaths) ? filePathOrPaths : [filePathOrPaths];
|
|
generateDTS(args);
|
|
const program = ts.createProgram([filePath], options);
|
|
|
|
const parser = new MyParser(program, {});
|
|
|
|
const checker = program.getTypeChecker();
|
|
|
|
const result = [filePath]
|
|
.map((fPath) => program.getSourceFile(fPath))
|
|
.filter((sourceFile) => typeof sourceFile !== 'undefined')
|
|
.reduce((docs: any[], sourceFile) => {
|
|
const moduleSymbol = checker.getSymbolAtLocation(sourceFile as ts.Node);
|
|
|
|
if (!moduleSymbol) {
|
|
return docs;
|
|
}
|
|
|
|
const exportSymbols = checker.getExportsOfModule(moduleSymbol);
|
|
|
|
for (let index = 0; index < exportSymbols.length; index++) {
|
|
const sym: SymbolWithMeta = exportSymbols[index];
|
|
const name = sym.getName();
|
|
if (blacklistNames.includes(name)) {
|
|
continue;
|
|
}
|
|
|
|
// polyfill valueDeclaration
|
|
sym.valueDeclaration =
|
|
sym.valueDeclaration || (Array.isArray(sym.declarations) && sym.declarations[0]);
|
|
|
|
if (!sym.valueDeclaration) {
|
|
continue;
|
|
}
|
|
const info = parser.getComponentInfo(sym, sourceFile);
|
|
if (info === null) {
|
|
continue;
|
|
}
|
|
const exportName = sym.meta && sym.meta.exportName;
|
|
const meta = {
|
|
subName: exportName ? name : '',
|
|
exportName: exportName || name,
|
|
};
|
|
if (docs.find((x) => isEqual(x.meta, meta))) {
|
|
continue;
|
|
}
|
|
docs.push({
|
|
...info,
|
|
meta,
|
|
});
|
|
// find sub components
|
|
if (!!sym.declarations && sym.declarations.length === 0) {
|
|
continue;
|
|
}
|
|
|
|
const type = checker.getTypeOfSymbolAtLocation(
|
|
sym,
|
|
sym.valueDeclaration || sym.declarations[0],
|
|
);
|
|
Array.prototype.push.apply(
|
|
exportSymbols,
|
|
type.getProperties().map((x: SymbolWithMeta) => {
|
|
x.meta = { exportName: name };
|
|
return x;
|
|
}),
|
|
);
|
|
}
|
|
|
|
return docs;
|
|
}, []);
|
|
const coms = result.reduce((res: any[], info: any) => {
|
|
if (!info || !info.props || isEmpty(info.props)) return res;
|
|
const props = Object.keys(info.props).reduce((acc: any[], name) => {
|
|
// omit aria related properties temporarily
|
|
if (name.startsWith('aria-')) {
|
|
return acc;
|
|
}
|
|
try {
|
|
const item: any = transformItem(name, info.props[name]);
|
|
acc.push(item);
|
|
} catch (e) {
|
|
log(e);
|
|
}
|
|
return acc;
|
|
}, []);
|
|
const exportName = info?.meta?.exportName;
|
|
res.push({
|
|
componentName: getComponentName(exportName, info.displayName),
|
|
props,
|
|
meta: info.meta || {},
|
|
});
|
|
return res;
|
|
}, []);
|
|
return coms;
|
|
}
|