408 lines
11 KiB
TypeScript
408 lines
11 KiB
TypeScript
/*
|
|
* Copyright 2025 coze-dev Authors
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
import path from 'path';
|
|
|
|
import prettier, { type Options } from 'prettier';
|
|
import fs from 'fs-extra';
|
|
import { SyntaxType } from '@coze-arch/idl-parser';
|
|
import { type Program, type File, type Node } from '@babel/types';
|
|
import * as t from '@babel/types';
|
|
import template from '@babel/template';
|
|
import { type ParserPlugin, parse } from '@babel/parser';
|
|
|
|
import * as h from './types';
|
|
import { ReservedKeyWord } from './constant';
|
|
export const plugins: ParserPlugin[] = [
|
|
'typescript',
|
|
'decorators-legacy',
|
|
'classProperties',
|
|
'doExpressions',
|
|
];
|
|
export const createFile = (source: string) => {
|
|
const program: Program = template.program(source, {
|
|
plugins,
|
|
})();
|
|
const file: File = {
|
|
type: 'File',
|
|
loc: program.loc,
|
|
start: program.start,
|
|
end: program.end,
|
|
program,
|
|
comments: [],
|
|
tokens: null,
|
|
leadingComments: [],
|
|
trailingComments: [],
|
|
innerComments: [],
|
|
};
|
|
return file;
|
|
};
|
|
|
|
export function createIdWithTypeAnnotation(exp: string) {
|
|
const dec = template.ast(`let ${exp}`, { plugins }) as t.VariableDeclaration;
|
|
return dec.declarations[0].id as t.Identifier;
|
|
}
|
|
|
|
export const parseFile = (fileName: string) =>
|
|
parse(fs.readFileSync(fileName, 'utf8'), {
|
|
sourceType: 'module',
|
|
plugins,
|
|
});
|
|
|
|
export function formatCode(code: string, root = '.') {
|
|
const defaultConfig: Options = {
|
|
tabWidth: 2,
|
|
printWidth: 120,
|
|
singleQuote: true,
|
|
};
|
|
const file = path.resolve(process.cwd(), root, './for-prettier-bug'); // 这里一定要加多一级目录
|
|
const config = prettier.resolveConfig(file, { editorconfig: true });
|
|
return prettier.format(code, {
|
|
...(config || defaultConfig),
|
|
parser: 'typescript',
|
|
});
|
|
}
|
|
|
|
export function disableLint<T extends Node>(node: T, isTs = true) {
|
|
return t.addComment<T>(
|
|
node,
|
|
'leading',
|
|
isTs ? ' tslint:disable ' : ' eslint-disable ',
|
|
);
|
|
}
|
|
|
|
export async function safeWriteFile(fileName: string, content: string) {
|
|
await fs.ensureDirSync(path.dirname(fileName));
|
|
await fs.writeFile(fileName, content, 'utf8');
|
|
}
|
|
|
|
export function addComment<T extends t.Node = t.Node>(
|
|
node: T,
|
|
comments: h.Comment[],
|
|
position?: t.CommentTypeShorthand,
|
|
): T {
|
|
const [content, multi] = convertVComments(comments);
|
|
if (content) {
|
|
return t.addComment(
|
|
node,
|
|
position || multi ? 'leading' : 'trailing',
|
|
content,
|
|
!multi,
|
|
);
|
|
} else {
|
|
return node;
|
|
}
|
|
}
|
|
|
|
export function convertVComments(comments: h.Comment[]): [string, boolean] {
|
|
if (!comments) {
|
|
return ['', false];
|
|
}
|
|
let res = [] as string[];
|
|
for (const comment of comments) {
|
|
const { value } = comment;
|
|
if (Array.isArray(value)) {
|
|
res = [...res, ...value];
|
|
} else {
|
|
res = [...res, value];
|
|
}
|
|
}
|
|
if (res.length > 1) {
|
|
return [
|
|
`*\n * ${res.map(i => i.replace(/\*\//g, '/')).join('\n * ')}\n`,
|
|
true,
|
|
];
|
|
}
|
|
if (res.length > 0) {
|
|
return [`* ${res[0]?.replace(/\*\//g, '/')} `, true];
|
|
}
|
|
|
|
return ['', false];
|
|
}
|
|
|
|
export function getRelativePath(from: string, to: string) {
|
|
let relative = path.relative(path.parse(from).dir, to);
|
|
relative = !relative.startsWith('.') ? `./${relative}` : relative;
|
|
return removeFileExt(relative);
|
|
}
|
|
|
|
export function removeFileExt(fileName: string) {
|
|
const [_, ...target] = fileName.split('.').reverse();
|
|
const res = target.reverse().join('.');
|
|
return res.startsWith('./') || res.startsWith('/') ? res : `./${res}`;
|
|
}
|
|
|
|
export function parseIdFiledType(fieldType: h.Identifier) {
|
|
const { value } = fieldType;
|
|
const namespaceArr = value.split('.');
|
|
const [refName, ...namespace] = namespaceArr.reverse();
|
|
const namespaceName = namespace.reverse().join('.');
|
|
return {
|
|
refName,
|
|
namespace: namespaceName,
|
|
};
|
|
}
|
|
|
|
export function parseId(id: string) {
|
|
const res = id.split('.');
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
const name = res.pop()!;
|
|
const namespace = uniformNs(res.join('_'));
|
|
return namespace ? `${namespace}.${name}` : name;
|
|
}
|
|
|
|
export function uniformNs(ns: string) {
|
|
if (ReservedKeyWord.includes(ns)) {
|
|
// 命中保留字,处理为下划线开头
|
|
return `_${ns}`;
|
|
}
|
|
return ns.replace(/\./g, '_');
|
|
}
|
|
|
|
export function getValuesFromEnum(params: h.EnumDefinition) {
|
|
const { members } = params;
|
|
let currentVal = 0;
|
|
const enumArr = [] as number[];
|
|
for (const member of members) {
|
|
const { initializer } = member;
|
|
if (initializer) {
|
|
if (h.isIntegerLiteral(initializer.value)) {
|
|
currentVal = Number(initializer.value.value);
|
|
} else if (h.isHexLiteral(initializer.value)) {
|
|
// 16进制
|
|
currentVal = Number(initializer.value.value);
|
|
}
|
|
enumArr.push(currentVal);
|
|
} else {
|
|
enumArr.push(currentVal);
|
|
}
|
|
currentVal = currentVal + 1;
|
|
}
|
|
return enumArr;
|
|
}
|
|
|
|
export function parseFiledName(filed: h.FieldDefinition) {
|
|
const { name, extensionConfig } = filed;
|
|
if (extensionConfig?.key === '.') {
|
|
return name.value;
|
|
}
|
|
return extensionConfig?.key || name.value;
|
|
}
|
|
|
|
export function getAnnotation(
|
|
annotations: h.Annotations | undefined,
|
|
name: string,
|
|
) {
|
|
if (!annotations) {
|
|
return undefined;
|
|
}
|
|
for (const ele of annotations.annotations) {
|
|
if (ele.name.value === name) {
|
|
return ele.value && ele.value.value;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
// export function parseAnnotations(anno: h.Annotations) {
|
|
// const result = {} as { [key: string]: string };
|
|
// anno.annotations.forEach((i) => {
|
|
// const { name, value } = i;
|
|
// result[name.value] = value ? value.value : "";
|
|
// });
|
|
// return result;
|
|
// }
|
|
|
|
export function getNamespaceByPath(idlPath: string) {
|
|
return (idlPath.split('/').pop() as string).replace(/\.(thrift|proto)$/, '');
|
|
}
|
|
|
|
export function genAst<T = unknown>(code: string): T {
|
|
return template.ast(code, {
|
|
plugins,
|
|
preserveComments: true,
|
|
startLine: 2,
|
|
}) as any;
|
|
}
|
|
|
|
export function getStatementById(
|
|
id: h.Identifier,
|
|
current: h.IParseResultItem,
|
|
) {
|
|
const { namespace, refName } = parseIdFiledType(id);
|
|
let statement: h.UnifyStatement | undefined;
|
|
if (namespace) {
|
|
const ast = getAstFromNamespace(namespace, current);
|
|
statement = h.findDefinition(ast, refName);
|
|
} else {
|
|
statement = h.findDefinition(current.statements, id.value);
|
|
}
|
|
|
|
if (!statement) {
|
|
throw new Error(`can not find Struct: ${id.value} `);
|
|
}
|
|
return statement;
|
|
}
|
|
|
|
export function getAstFromNamespace(
|
|
namespace: string,
|
|
current: h.IParseResultItem,
|
|
) {
|
|
const item = getParseResultFromNamespace(namespace, current);
|
|
|
|
return item.statements;
|
|
}
|
|
|
|
export function getParseResultFromNamespace(
|
|
namespace: string,
|
|
current: h.IParseResultItem,
|
|
) {
|
|
let item = null as h.IParseResultItem | null;
|
|
|
|
for (const file in current.deps) {
|
|
// eslint-disable-next-line no-prototype-builtins
|
|
if (current.deps.hasOwnProperty(file)) {
|
|
const element = current.deps[file];
|
|
const ns = getNamespaceByPath(file);
|
|
if (ns === namespace) {
|
|
item = element;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!item) {
|
|
throw new Error(`can not find ast by namespace : ${namespace}`);
|
|
}
|
|
return item;
|
|
}
|
|
|
|
export function ignoreField(f: h.FieldDefinition) {
|
|
if (['KContext', 'Base', 'BaseResp'].includes(f.name.value)) {
|
|
return false;
|
|
}
|
|
if (h.isIdentifier(f.fieldType)) {
|
|
return !['base.Base', 'base.BaseResp'].includes(f.fieldType.value);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export function isFullBody(f: h.FieldDefinition) {
|
|
return (
|
|
(f.extensionConfig?.position === 'body' && f.extensionConfig.key === '.') ||
|
|
getAnnotation(f.annotations, 'api.full_body') !== undefined
|
|
);
|
|
}
|
|
|
|
export function hasDynamicJsonAnnotation(annotations?: h.Annotations) {
|
|
if (!annotations) {
|
|
return false;
|
|
}
|
|
return annotations.annotations.find(i =>
|
|
[
|
|
'kgw.json',
|
|
'kgw.json.req',
|
|
'kgw.json.resp',
|
|
'api.request.converter',
|
|
'api.response.converter',
|
|
].some(k => k === i.name.value),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 从 api.(request|response).converter 中解析出前端与网关之间的真实类型,
|
|
* 能搞出这两个注解来,这个协议着实恶心😭
|
|
* @param annotations
|
|
* @returns
|
|
*/
|
|
export function getTypeFromDynamicJsonAnnotation(
|
|
annotations?: h.Annotations,
|
|
): undefined | 'string' | 'Object' | 'unknown' {
|
|
if (!annotations) {
|
|
return undefined;
|
|
}
|
|
const requestVal = annotations.annotations.find(
|
|
i => i.name.value === 'api.request.converter',
|
|
)?.value?.value;
|
|
const responseVal = annotations.annotations.find(
|
|
i => i.name.value === 'api.response.converter',
|
|
)?.value?.value;
|
|
const typeValue = [
|
|
[undefined, 'string', 'Object'],
|
|
['Object', 'unknown', 'string'],
|
|
['Object', 'Object', 'unknown'],
|
|
] as const;
|
|
const index = (value: undefined | 'encode' | 'decode' | string) =>
|
|
value === 'encode' ? 2 : value === 'decode' ? 1 : 0;
|
|
return typeValue[index(responseVal)][index(requestVal)];
|
|
}
|
|
|
|
export function getFieldsAlias(i: h.FieldDefinition) {
|
|
return parseFiledName(i);
|
|
}
|
|
|
|
export function withExportDeclaration(
|
|
node: t.Declaration,
|
|
comments?: h.Comment[],
|
|
) {
|
|
const declaration = t.exportNamedDeclaration(node);
|
|
if (!comments) {
|
|
return declaration;
|
|
}
|
|
return addComment(declaration, comments, 'leading');
|
|
}
|
|
|
|
export function getSchemaRootByPath(absFile: string, idlRoot: string) {
|
|
const pathName = path
|
|
.relative(idlRoot, removeFileExt(absFile))
|
|
.replace(/\//g, '_');
|
|
|
|
return `api://schemas/${pathName}`;
|
|
}
|
|
|
|
export function getOutputName(params: {
|
|
source: string;
|
|
idlRoot: string;
|
|
outputDir: string;
|
|
}) {
|
|
const relativeName = path.relative(params.idlRoot, params.source);
|
|
return path.resolve(params.outputDir, relativeName);
|
|
}
|
|
|
|
export function transformFieldId(fieldName: string) {
|
|
return fieldName.includes('-')
|
|
? t.stringLiteral(fieldName)
|
|
: t.identifier(fieldName);
|
|
}
|
|
|
|
export function getBaseTypeConverts(i64Type: string) {
|
|
let newType = 'number';
|
|
if (i64Type === 'string') {
|
|
newType = 'string';
|
|
}
|
|
|
|
return {
|
|
[SyntaxType.ByteKeyword]: 'number',
|
|
[SyntaxType.I8Keyword]: 'number',
|
|
[SyntaxType.I16Keyword]: 'number',
|
|
[SyntaxType.I32Keyword]: 'number',
|
|
[SyntaxType.I64Keyword]: newType,
|
|
[SyntaxType.DoubleKeyword]: 'number',
|
|
[SyntaxType.BinaryKeyword]: 'object',
|
|
[SyntaxType.StringKeyword]: 'string',
|
|
[SyntaxType.BoolKeyword]: 'boolean',
|
|
};
|
|
}
|