feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,213 @@
/*
* 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 fs from 'fs-extra';
import {
type IPlugin,
type Program,
before,
after,
} from '@coze-arch/idl2ts-plugin';
import {
type IParseEntryCtx,
isStructDefinition,
type FunctionType,
isSetType,
isListType,
SyntaxType,
isMapType,
type FieldType,
createFile,
getAnnotation,
getOutputName,
getTypeFromDynamicJsonAnnotation,
removeFileExt,
parseFile,
genAst,
} from '@coze-arch/idl2ts-helper';
import * as t from '@babel/types';
import { type Options } from '../types';
import { type Contexts, type ProcessIdlCtxWithSchema, HOOK } from '../context';
function isInt(fieldType: FieldType | FunctionType) {
return [
SyntaxType.I8Keyword,
SyntaxType.I16Keyword,
SyntaxType.I32Keyword,
SyntaxType.I64Keyword,
].some(i => i === fieldType.type);
}
export class AdapterPlugin implements IPlugin {
private patchTypes = new Map<string, Record<string, string[]>>();
private options: Options;
constructor(options: Options) {
this.options = options;
}
apply(program: Program<Contexts>): void {
program.register(
before(HOOK.PROCESS_IDL_NODE),
this.adaptStruct.bind(this),
);
program.register(after(HOOK.GEN_FILE_AST), this.genPatchFiles.bind(this));
}
private genPatchFiles(ctx: IParseEntryCtx) {
for (const [idlPath, res] of this.patchTypes.entries()) {
const targetFile = getOutputName({
source: `${removeFileExt(idlPath)}.ts`,
idlRoot: this.options.idlRoot,
outputDir:
this.options.patchTypesOutput ||
path.join(this.options.outputDir, '../patch-types'),
});
let file: t.File;
if (!fs.existsSync(targetFile)) {
file = createFile('');
} else {
file = parseFile(targetFile);
}
Object.keys(res).forEach(structName => {
let target = file.program.body.find(
i =>
t.isExportNamedDeclaration(i) &&
// @ts-expect-error fixme
i.declaration?.id.name === structName,
) as t.ExportNamedDeclaration;
if (!target) {
target = genAst(
`export namespace ${structName} {}`,
) as t.ExportNamedDeclaration;
file.program.body.push(target);
}
const declaration = target.declaration as t.TSModuleDeclaration;
for (const fieldName of res[structName]) {
if (t.isTSModuleBlock(declaration.body)) {
if (
!declaration.body.body.some(i => {
if (t.isExportNamedDeclaration(i)) {
if (
t.isTSTypeAliasDeclaration(i.declaration) ||
t.isInterfaceDeclaration(i.declaration)
) {
return i.declaration.id.name === fieldName;
}
}
return false;
})
) {
declaration.body.body.push(
genAst(
`export type ${fieldName}= unknown`,
) as t.TSTypeAliasDeclaration,
);
}
}
}
});
ctx.files.set(targetFile, { content: file, type: 'babel' });
}
return ctx;
}
private adaptStruct(ctx: ProcessIdlCtxWithSchema) {
const { node, ast } = ctx;
if (!node) {
return ctx;
}
if (isStructDefinition(node)) {
const decodeEncodeFields = [] as string[];
// eslint-disable-next-line complexity
node.fields = node.fields.map(f => {
// req
if (
getAnnotation(f.annotations, 'api.converter') === 'atoi_comp_empty'
) {
if (isInt(f.fieldType)) {
// 类型转换为 string
f.fieldType.type = SyntaxType.StringKeyword;
}
}
// api.converter 对 int 以及 map 类型生效
if (getAnnotation(f.annotations, 'api.converter') === 'itoa') {
if (isInt(f.fieldType)) {
// 类型转换为 string
f.fieldType.type = SyntaxType.StringKeyword;
}
if (isMapType(f.fieldType)) {
const { valueType } = f.fieldType;
if (isInt(valueType)) {
f.fieldType.valueType.type = SyntaxType.StringKeyword;
}
}
}
// item_converter 对 list 类型生效
if (
['atoi_comp_empty', 'itoa'].includes(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
getAnnotation(f.annotations, 'api.item_converter')!,
)
) {
if (isSetType(f.fieldType) || isListType(f.fieldType)) {
f.fieldType.valueType.type = SyntaxType.StringKeyword;
}
}
// 收集 decode encode 注解处理
if (getTypeFromDynamicJsonAnnotation(f.annotations)) {
decodeEncodeFields.push(f.name.value);
}
// api.json 注解处理
const jsonAnnotation = getAnnotation(f.annotations, 'api.json');
if (jsonAnnotation) {
f.extensionConfig = f.extensionConfig || {};
f.extensionConfig.key = jsonAnnotation;
}
// api.json_string 注解处理
const jsonStrAnnotation = getAnnotation(
f.annotations,
'api.json_string',
);
if (jsonStrAnnotation) {
if (isInt(f.fieldType)) {
// 类型转换为 string
f.fieldType.type = SyntaxType.StringKeyword;
f.extensionConfig = f.extensionConfig || {};
f.extensionConfig.key = jsonStrAnnotation;
} else {
throw new Error(
'api.json_string is expected an annotation int type',
);
}
}
return f;
});
if (decodeEncodeFields.length > 0) {
const currentAstRes = this.patchTypes.get(ast.idlPath);
if (!currentAstRes) {
this.patchTypes.set(ast.idlPath, {
[node.name.value]: decodeEncodeFields,
});
} else {
currentAstRes[node.name.value] = decodeEncodeFields;
}
}
}
return ctx;
}
}

View File

@@ -0,0 +1,45 @@
/*
* 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 { type Program, after, type IPlugin } from '@coze-arch/idl2ts-plugin';
import { type IParseEntryCtx, isPbFile } from '@coze-arch/idl2ts-helper';
import { HOOK } from '../context';
export class AutoFixDuplicateIncludesPlugin implements IPlugin {
apply(p: Program<{ PARSE_ENTRY: any }>) {
p.register(after(HOOK.PARSE_ENTRY), (ctx: IParseEntryCtx) => {
if (isPbFile(ctx.entries[0])) {
return ctx;
}
ctx.ast = ctx.ast.map(i => {
const res: string[] = [];
for (const include of i.includes) {
if (res.includes(include)) {
console.error(
`[${include}]` + `has be includes duplicate in file:${i.idlPath}`,
);
} else {
res.push(include);
}
}
i.includes = res;
return i;
});
return ctx;
});
}
}

View File

@@ -0,0 +1,41 @@
/*
* 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 { isAbsolute } from 'path';
import { type Program, after, type IPlugin } from '@coze-arch/idl2ts-plugin';
function ensureRelative(idlPath: string) {
if (isAbsolute(idlPath)) {
return idlPath;
}
if (!idlPath.startsWith('.')) {
return `./${idlPath}`;
}
return idlPath;
}
export class AutoFixPathPlugin implements IPlugin {
apply(p: Program<{ PARSE_ENTRY: any }>) {
p.register(after('PARSE_ENTRY'), ctx => {
ctx.ast = ctx.ast.map(i => {
i.includes = i.includes.map(ensureRelative);
return i;
});
return ctx;
});
}
}

View File

@@ -0,0 +1,494 @@
/*
* 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 {
type IPlugin,
type Program,
on,
after,
} from '@coze-arch/idl2ts-plugin';
import {
type EnumDefinition,
isStructDefinition,
isIdentifier,
type StructDefinition,
isBaseType,
type FieldType,
type FunctionType,
isEnumDefinition,
isListType,
isStringLiteral,
type TypedefDefinition,
type ConstDefinition,
type ConstValue,
isSetType,
isMapType,
isBooleanLiteral,
isIntConstant,
isDoubleConstant,
isConstList,
isConstMap,
type BaseType,
isConstDefinition,
type ProcessIdlCtx,
isTypedefDefinition,
isServiceDefinition,
type IParseResultItem,
type ServiceDefinition,
type IGenTemplateCtx,
addComment,
getAnnotation,
parseFiledName,
parseIdFiledType,
getTypeFromDynamicJsonAnnotation,
withExportDeclaration,
uniformNs,
removeFileExt,
genAst,
getOutputName,
transformFieldId,
getRelativePath,
getFieldsAlias,
SyntaxType,
type UnifyStatement,
} from '@coze-arch/idl2ts-helper';
import * as t from '@babel/types';
import { type Options } from '../types';
import { TypeMapper } from '../type-mapper';
import { genFunc, genPublic } from '../template';
import { type Contexts, HOOK } from '../context';
const hasEnumAnnotation = (statement: UnifyStatement) =>
statement.annotations?.annotations.some(
i => i.name.value === 'ts.enum' && i.value?.value === 'true',
);
const findEnumItemIndex = (enumName: string, statements: UnifyStatement[]) => {
const result: number[] = [];
for (let i = 0; i < statements.length; i++) {
const statement = statements[i];
if (
statement.type === SyntaxType.ConstDefinition &&
statement.fieldType.type === SyntaxType.Identifier &&
statement.fieldType.value === enumName
) {
result.push(i);
}
}
return result;
};
export class ClientPlugin implements IPlugin {
private options: Options;
private program!: Program<Contexts>;
private needPatchTypeFile = new Set<string>();
constructor(options: Options) {
this.options = options;
}
apply(program: Program<Contexts>): void {
this.program = program;
program.register(
on(HOOK.PROCESS_IDL_NODE),
ctx => {
const { node, dts, ast } = ctx;
if (!node) {
throw new Error('node is undefined');
}
if (isStructDefinition(node)) {
const { nested, struct } = this.processStructNode(node, ctx);
dts.program.body.push(struct);
if (nested) {
dts.program.body.push(nested);
}
} else if (isEnumDefinition(node)) {
dts.program.body.push(this.processEnumNode(node));
} else if (isTypedefDefinition(node) && hasEnumAnnotation(node)) {
dts.program.body.push(this.processTypedefEnumNode(node, ctx));
} else if (isConstDefinition(node)) {
dts.program.body.push(this.processConstNode(node));
} else if (isTypedefDefinition(node)) {
dts.program.body.push(this.processTypeDefNode(node));
} else if (isServiceDefinition(node)) {
dts.program.body = [
...dts.program.body,
...this.processServiceDefinition(node, ast, ctx),
];
}
return ctx;
},
0,
);
program.register(after(HOOK.PROCESS_IDL_AST), ctx => {
const { ast, dts } = ctx;
if (ast.isEntry) {
dts.program.body = [genPublic(ctx, this.options), ...dts.program.body];
}
if (ast.includes) {
Object.keys(ast.includeMap).forEach(key => {
dts.program.body = [
...this.processIncludes(key, ast),
...dts.program.body,
];
});
}
const outputFile = getOutputName({
source: `${removeFileExt(ast.idlPath)}.ts`,
outputDir: this.options.outputDir,
idlRoot: this.options.idlRoot,
});
if (this.needPatchTypeFile.has(ast.idlPath)) {
let pathName = '';
if (this.options.patchTypesAliasOutput) {
pathName = path.join(
this.options.patchTypesAliasOutput,
path.relative(
this.options.idlRoot,
ast.idlPath.replace('.thrift', ''),
),
);
} else {
const patchTypeFile = path.join(
this.options.patchTypesOutput ||
path.join(this.options.outputDir, '../patch-types'),
path.relative(this.options.idlRoot, ast.idlPath),
);
pathName = getRelativePath(outputFile, patchTypeFile);
}
const code = `import type * as Patch from '${pathName}'`;
dts.program.body.unshift(genAst<t.ImportDeclaration>(code));
}
ctx.output.set(outputFile, { type: 'babel', content: dts });
return ctx;
});
this.program.register(
on(HOOK.GEN_FUN_TEMPLATE),
(ctx: IGenTemplateCtx) => {
ctx.template = genFunc(ctx);
return ctx;
},
0,
);
}
private processEnumNode(node: EnumDefinition) {
const { members, name, comments } = node;
const enumArr = members.map(i => {
const { name, comments, initializer } = i;
return addComment(
t.tsEnumMember(
t.identifier(name.value),
initializer ? this.getExpFromConstValue(initializer) : undefined,
),
comments,
);
});
const enumAst = t.tsEnumDeclaration(t.identifier(name.value), enumArr);
return withExportDeclaration(enumAst, comments);
}
private processStructNode(node: StructDefinition, ctx: ProcessIdlCtx) {
const { fields, name, comments, nested } = node;
const typeProps: t.ObjectTypeProperty[] = [];
const processedFiledName = {} as Record<string, string>;
fields.forEach(i => {
const fieldName = parseFiledName(i);
if (processedFiledName[fieldName]) {
return;
}
const { fieldType, requiredness, comments, annotations } = i;
const isAnyType = getAnnotation(annotations, 'api.value_type') === 'any';
const dynamicType = getTypeFromDynamicJsonAnnotation(annotations);
let valueType: t.FlowType = this.processFiledType(fieldType);
if (isAnyType) {
valueType = t.anyTypeAnnotation();
} else if (dynamicType) {
valueType = t.genericTypeAnnotation(
t.qualifiedTypeIdentifier(
t.identifier(i.name.value),
t.qualifiedTypeIdentifier(
t.identifier(name.value),
t.identifier('Patch'),
),
),
);
this.needPatchTypeFile.add(ctx.ast.idlPath);
}
const prop = t.objectTypeProperty(transformFieldId(fieldName), valueType);
if (requiredness === 'optional') {
prop.optional = true;
if (this.options.allowNullForOptional) {
prop.value = t.unionTypeAnnotation([
prop.value,
t.nullLiteralTypeAnnotation(),
]);
}
}
processedFiledName[fieldName] = getFieldsAlias(i);
typeProps.push(addComment(prop, comments));
});
const ast = t.interfaceDeclaration(
t.identifier(name.value),
null,
[],
t.objectTypeAnnotation(typeProps),
);
return {
struct: withExportDeclaration(ast, comments),
nested: this.processNested(nested, node, ctx),
};
}
private processFiledType(fieldType: FieldType | FunctionType) {
if (isBaseType(fieldType)) {
return this.getTsTypeFromThriftBaseType(fieldType);
} else if (isListType(fieldType) || isSetType(fieldType)) {
const { valueType } = fieldType;
return t.arrayTypeAnnotation(this.processFiledType(valueType));
} else if (isMapType(fieldType)) {
const { valueType } = fieldType;
const valueFiledType = this.processFiledType(valueType);
return t.objectTypeAnnotation(
[],
[
t.objectTypeIndexer(
t.identifier('key'),
t.unionTypeAnnotation([
t.stringTypeAnnotation(),
t.numberTypeAnnotation(),
]),
valueFiledType,
),
],
);
} else if (isIdentifier(fieldType)) {
const { namespace, refName } = parseIdFiledType(fieldType);
if (namespace) {
return t.genericTypeAnnotation(
t.qualifiedTypeIdentifier(
t.identifier(refName),
t.identifier(
fieldType.namespaceValue?.startsWith('root')
? namespace
: uniformNs(namespace.replace('.', '_')),
),
),
);
}
return t.genericTypeAnnotation(t.identifier(refName));
}
throw new Error(`unknown type:${fieldType.type}`);
}
private processNested(
nested: Record<string, StructDefinition | EnumDefinition> | undefined,
parent: StructDefinition,
ctx: ProcessIdlCtx,
) {
if (!nested) {
return undefined;
}
const block = t.tsModuleBlock([]);
Object.keys(nested).forEach(key => {
const node = nested[key];
if (isStructDefinition(node)) {
const { nested, struct } = this.processStructNode(node, ctx);
block.body.push(struct);
if (nested) {
block.body.push(nested);
}
} else if (isEnumDefinition(node)) {
block.body.push(this.processEnumNode(node));
}
});
const namespaceModule = t.tsModuleDeclaration(
t.identifier(parent.name.value),
block,
);
return withExportDeclaration(namespaceModule);
}
private processConstNode(node: ConstDefinition) {
const { name, comments, initializer } = node;
const exp = this.getExpFromConstValue(initializer);
const declarator = t.variableDeclarator(t.identifier(name.value), exp);
const ast = t.variableDeclaration('const', [declarator]);
return withExportDeclaration(ast, comments);
}
private getExpFromConstValue(initializer: ConstValue): t.Expression {
let exp = null as t.Expression | null;
if (isStringLiteral(initializer)) {
exp = t.stringLiteral(initializer.value);
} else if (isBooleanLiteral(initializer)) {
exp = t.booleanLiteral(initializer.value);
} else if (isIntConstant(initializer) || isDoubleConstant(initializer)) {
exp = t.numericLiteral(Number(initializer.value.value));
} else if (isConstList(initializer)) {
const { elements } = initializer;
exp = t.arrayExpression(elements.map(i => this.getExpFromConstValue(i)));
} else if (isConstMap(initializer)) {
exp = t.objectExpression(
initializer.properties.map(i =>
t.objectProperty(
this.getExpFromConstValue(i.name),
this.getExpFromConstValue(i.initializer),
isIdentifier(i.name),
),
),
);
} else if (isIdentifier(initializer)) {
exp = t.identifier(initializer.value);
}
if (!exp) {
throw new Error(`Not support const type yet : ${initializer.type}`);
}
return exp;
}
private processTypeDefNode(node: TypedefDefinition) {
const { definitionType, name, comments } = node;
// @ts-expect-error no fix
if (node.definitionType?.value?.split('.').length > 2) {
// @ts-expect-error no fix
node.definitionType.value = node.definitionType.namespaceValue;
}
const ast = t.tsTypeAliasDeclaration(
t.identifier(name.value),
null,
this.getTsTypeFromFiledType(definitionType),
);
const res = withExportDeclaration(ast, comments);
return res;
}
private processTypedefEnumNode(node: TypedefDefinition, ctx: ProcessIdlCtx) {
const { name, comments } = node;
const enumName = name.value;
const { statements } = ctx.ast;
const enumItemIndexArray = findEnumItemIndex(enumName, statements);
const enumArr = enumItemIndexArray
.map(i => statements[i])
.map(i => {
const { name, comments, initializer } = i as ConstDefinition;
return addComment(
t.tsEnumMember(
t.identifier(name.value.replace(new RegExp(`^${enumName}_`), '')),
initializer ? this.getExpFromConstValue(initializer) : initializer,
),
comments,
);
});
const enumAst = t.tsEnumDeclaration(t.identifier(name.value), enumArr);
// 从后向前删除枚举项,避免索引变化影响
enumItemIndexArray
.sort((a, b) => b - a)
.forEach(index => {
statements.splice(index, 1);
});
return withExportDeclaration(enumAst, comments);
}
private getTsTypeFromFiledType(fieldType: FieldType) {
if (isBaseType(fieldType)) {
return this.getTsTypeFromThriftBaseType(fieldType, true);
} else if (isListType(fieldType) || isSetType(fieldType)) {
const { valueType } = fieldType;
return t.tsArrayType(this.getTsTypeFromFiledType(valueType));
} else if (isMapType(fieldType)) {
const { keyType, valueType } = fieldType;
return t.tsTypeReference(
t.identifier('Record'),
t.tsTypeParameterInstantiation([
this.getTsTypeFromFiledType(keyType),
this.getTsTypeFromFiledType(valueType),
]),
);
} else if (isIdentifier(fieldType)) {
return t.tsTypeReference(t.identifier(fieldType.value));
}
}
private getTsTypeFromThriftBaseType(fieldType: BaseType, isTsType = false) {
const typeStr = TypeMapper.map(fieldType.type as any);
if (typeStr === 'number') {
return !isTsType ? t.numberTypeAnnotation() : t.tsNumberKeyword();
} else if (typeStr === 'string') {
return !isTsType ? t.stringTypeAnnotation() : t.tsStringKeyword();
} else if (typeStr === 'object') {
const id = t.identifier('Blob');
return !isTsType ? t.genericTypeAnnotation(id) : t.tsTypeReference(id);
}
if (typeStr === 'boolean') {
return !isTsType ? t.booleanTypeAnnotation() : t.tsBooleanKeyword();
}
throw new Error(`not support :${typeStr}`);
}
private processIncludes(include: string, ast: IParseResultItem) {
const includePath = getRelativePath(ast.idlPath, ast.includeMap[include]);
const name = ast.includeRefer[include];
let code = `import * as ${name} from '${includePath}';\n`;
code += `export { ${name} };\n`;
const res = genAst<t.ImportDeclaration[]>(code);
// const res = template.ast(code, { plugins }) as ;
if (!Array.isArray(res)) {
return [res];
}
return res;
}
private processServiceDefinition(
node: ServiceDefinition,
ast: IParseResultItem,
ctx: ProcessIdlCtx,
): t.ExportNamedDeclaration[] {
const { functions } = node;
if (!ast.isEntry) {
return [];
}
const result = [] as t.ExportNamedDeclaration[];
functions.forEach(i => {
const { comments, extensionConfig } = i;
if (!extensionConfig?.method) {
return;
}
const metaCtx = {
ast,
meta: ctx.meta.find(m => m.name === i.name.value),
service: node,
method: i,
template: '',
} as IGenTemplateCtx;
// this.program.trigger(HOOK.PARSE_FUN_META, ctx);
this.program.trigger(HOOK.GEN_FUN_TEMPLATE, metaCtx);
result.push(withExportDeclaration(genAst(metaCtx.template), comments));
});
return result;
}
}

View File

@@ -0,0 +1,66 @@
/*
* 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 { type Program, after, before } from '@coze-arch/idl2ts-plugin';
import { isStructDefinition } from '@coze-arch/idl2ts-helper';
import { type Contexts, HOOK } from '../context';
const MAGIC_COMMENT_KEY = '\n*@magic-comment';
// 忽略 struct 中的字段
export class CommentFormatPlugin {
apply(p: Program<Contexts>) {
p.register(after('PARSE_ENTRY'), ctx => {
const result = ctx.ast;
for (const item of result) {
item.statements.forEach(i => {
if (isStructDefinition(i)) {
const { fields } = i;
i.fields = fields.map(i => {
const comments = i.comments || [];
let value = '';
if (comments.length === 1) {
if (Array.isArray(comments[0].value)) {
if (comments[0].value.length > 1) {
return i;
}
value = comments[0].value[0];
} else {
value = comments[0].value;
}
comments[0].value = MAGIC_COMMENT_KEY + value;
}
return { ...i, comments };
});
}
});
}
ctx.ast = result;
return ctx;
});
p.register(before(HOOK.WRITE_FILE), ctx => {
ctx.content = ctx.content.replaceAll(
`
*@magic-comment`,
'',
);
return ctx;
});
}
}

View File

@@ -0,0 +1,50 @@
/*
* 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 { type Program, after } from '@coze-arch/idl2ts-plugin';
import {
isStructDefinition,
type FieldDefinition,
} from '@coze-arch/idl2ts-helper';
type Filter = (f: FieldDefinition) => boolean;
interface IPops {
filter: Filter;
}
// 忽略 struct 中的字段
export class IgnoreStructFiledPlugin {
private filter: Filter;
constructor({ filter }: IPops) {
this.filter = filter;
}
apply(p: Program<{ PARSE_ENTRY: { ast: any } }>) {
p.register(after('PARSE_ENTRY'), ctx => {
const result = ctx.ast;
for (const item of result) {
item.statements.forEach(i => {
if (isStructDefinition(i)) {
const { fields } = i;
i.fields = fields.filter(f => this.filter(f));
}
});
}
ctx.ast = result;
return ctx;
});
}
}

View File

@@ -0,0 +1,26 @@
/*
* 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.
*/
export * from './adapter-plugin';
export * from './client-plugin';
export * from './meta-plugin';
export * from './mock-transformer';
export * from './schema-plugin';
export * from './pkg-entry-plugin';
export * from './auto-fix-path-plugin';
export * from './ignore-struct-field';
export * from './auto-fix-duplicate-plugin';
export * from './comment-format-plugin';

View File

@@ -0,0 +1,219 @@
/*
* 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 { type IPlugin, type Program, on } from '@coze-arch/idl2ts-plugin';
import {
type Identifier,
type FunctionDefinition,
type FieldDefinition,
type IParseResultItem,
isStructDefinition,
isIdentifier,
type FunctionType,
type IMeta,
type IHttpRpcMapping,
findDefinition,
type Position,
type ServiceDefinition,
getStatementById,
parseIdFiledType,
getFieldsAlias,
parseId,
isFullBody,
getAstFromNamespace,
getSchemaRootByPath,
getAnnotation,
} from '@coze-arch/idl2ts-helper';
import { type Contexts, HOOK, type IProcessMetaItemCtx } from '../context';
interface IOptions {
outputDir: string;
idlRoot: string;
}
export class MetaPlugin implements IPlugin {
options: IOptions;
constructor(options: IOptions) {
this.options = options;
}
apply(program: Program<Contexts>): void {
program.register(
on(HOOK.PARSE_FUN_META),
ctx => {
const node = ctx.node as ServiceDefinition;
node.functions.forEach(fun => {
// 过滤非泛化接口
if (!fun.extensionConfig?.method) {
return;
}
const { meta } = program.trigger<IProcessMetaItemCtx>(
HOOK.PARSE_FUN_META_ITEM,
{
ast: ctx.ast,
service: node,
method: fun,
} as IProcessMetaItemCtx,
);
ctx.meta.push(meta);
});
return ctx;
},
0,
);
program.register(on(HOOK.PARSE_FUN_META_ITEM), ctx => {
const { ast, service, method } = ctx;
const item = this.parseFunAnnotation(method, ast, service.name.value);
ctx.meta = item;
return ctx;
});
}
parseFunAnnotation(
params: FunctionDefinition,
ast: IParseResultItem,
service: string,
) {
const { name, returnType, fields, extensionConfig } = params;
const reqType = fields[0].fieldType as any;
const reqMapping = this.processPayloadFields(
reqType,
extensionConfig?.method === 'GET' ? 'query' : 'body',
ast,
);
const res = {
url: extensionConfig?.uri,
method: extensionConfig?.method ?? 'POST',
name: name.value,
reqType: parseId(reqType.value),
reqMapping,
resType: parseId(this.processReqResPramsType(returnType, ast)),
schemaRoot: getSchemaRootByPath(ast.idlPath, this.options.idlRoot),
service,
} as IMeta;
// 不是 json 时,需要加上 serializer 标识
if (extensionConfig?.serializer && extensionConfig?.serializer !== 'json') {
res.serializer = extensionConfig?.serializer;
}
return res;
}
private processReqResPramsType(id: FunctionType, ast: IParseResultItem) {
if (isIdentifier(id)) {
const statement = getStatementById(id, ast);
if (isStructDefinition(statement)) {
const wholeBody = statement.fields.find(isFullBody);
if (wholeBody) {
// 处理 api.body="." 以及 api.full_body=''
return `${id.value}['${getFieldsAlias(wholeBody)}']`;
} else {
return id.value;
}
}
throw new Error('params must be identifier');
} else {
return 'void';
}
}
private processPayloadFields(
id: Identifier,
defaultPosition: 'query' | 'body',
entry: IParseResultItem,
): IHttpRpcMapping {
const { namespace, refName } = parseIdFiledType(id);
if (namespace) {
const ast = getAstFromNamespace(namespace, entry);
const struct = findDefinition(ast, refName);
if (!struct || !isStructDefinition(struct)) {
throw new Error(`can not find Struct: ${refName} `);
}
return this.createMapping(struct.fields, defaultPosition);
}
const struct = findDefinition(entry.statements, id.value);
if (!struct || !isStructDefinition(struct)) {
throw new Error(`can not find Struct: ${id.value} `);
}
return this.createMapping(struct.fields, defaultPosition);
}
private createMapping(
fields: FieldDefinition[],
defaultPosition: 'query' | 'body',
): IHttpRpcMapping {
const specificPositionFiled = new Set<string>();
const mapping = {} as IHttpRpcMapping;
fields.forEach(filed => {
const jsonAnnotation = getAnnotation(filed.annotations, 'api.json');
if (jsonAnnotation) {
filed.extensionConfig = filed.extensionConfig || {};
filed.extensionConfig.key = jsonAnnotation;
}
const { extensionConfig } = filed;
const alias = getFieldsAlias(filed);
if (extensionConfig) {
if (isFullBody(filed)) {
mapping.entire_body = [alias];
return;
}
Object.keys(extensionConfig).forEach(key => {
if (key === 'position' && extensionConfig.position) {
const filedMapping = this.processMapping(
mapping,
extensionConfig.position,
alias,
);
mapping[extensionConfig.position] = filedMapping;
specificPositionFiled.add(alias);
}
});
}
// 如果没有指定根据method默认指定为query 或者 body
if (!specificPositionFiled.has(alias)) {
const filedMapping = mapping[defaultPosition];
mapping[defaultPosition] = filedMapping
? [...filedMapping, alias]
: [alias];
}
});
return mapping;
}
private processMapping(
mapping: IHttpRpcMapping,
position: Position,
filedName: string,
): string[] {
const mappingKeys = [
'path',
'query',
'status_code',
'header',
'cookie',
'entire_body',
'body',
];
if (mappingKeys.find(i => i === position)) {
const data = mapping[position];
return data ? [...data, filedName] : [filedName];
} else {
return mapping[position] || [];
}
}
}

View File

@@ -0,0 +1,469 @@
/*
* 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 { faker } from '@faker-js/faker';
import {
type IPlugin,
type Program,
on,
after,
} from '@coze-arch/idl2ts-plugin';
import {
type IParseResultItem,
type EnumDefinition,
isServiceDefinition,
isStructDefinition,
isIdentifier,
type StructDefinition,
isBaseType,
type FieldType,
type FunctionType,
isEnumDefinition,
isListType,
isStringLiteral,
type TypedefDefinition,
type ConstDefinition,
type ConstValue,
isSetType,
isMapType,
isIntConstant,
isConstDefinition,
type ProcessIdlCtx,
isTypedefDefinition,
type UnifyStatement,
SyntaxType,
type ServiceDefinition,
getStatementById,
parseIdFiledType,
uniformNs,
hasDynamicJsonAnnotation,
getValuesFromEnum,
isFullBody,
removeFileExt,
parseId,
getOutputName,
getFieldsAlias,
getBaseTypeConverts,
getRelativePath,
} from '@coze-arch/idl2ts-helper';
import * as t from '@babel/types';
import template from '@babel/template';
import { type Options } from '../types';
import { genMockPublic } from '../template';
import { type Contexts, type GenMockFieldCtx, HOOK } from '../context';
interface ProcessIdlCtxWithMock extends ProcessIdlCtx {
mockStatements: t.Statement[];
}
export class MockTransformerPlugin implements IPlugin {
private options: Options;
private program!: Program<Contexts>;
constructor(options: Options) {
this.options = options;
}
apply(program: Program<Contexts>): void {
this.program = program;
program.register(
on(HOOK.PROCESS_IDL_NODE),
ctx => {
const { node } = ctx;
ctx.mockStatements = ctx.mockStatements || [];
if (!node) {
throw new Error('node is undefined');
}
const statement = this.processIdlNode(node, ctx);
if (statement) {
ctx.mockStatements.push(statement);
}
return ctx;
},
0,
);
program.register(on(HOOK.GEN_MOCK_FILED), ctx => this.genMockField(ctx));
program.register(after(HOOK.PROCESS_IDL_AST), ctx => {
const { ast, mock, mockStatements } = ctx;
const exportId = [] as string[];
const mockVarOrder = this.getMockVarOrder(mock);
const nextOrder = {} as Record<string, number>;
mockStatements.forEach((i, index) => {
if (t.isVariableDeclaration(i)) {
const { name } = i.declarations[0].id as t.Identifier;
exportId.push(name);
nextOrder[name] = index;
}
});
// 按照 mock 文件中的顺序优先排序
const getOrder = (name: string) =>
typeof mockVarOrder[name] !== 'undefined'
? mockVarOrder[name]
: nextOrder[name];
const body = mockStatements.sort((a, b) => {
if (t.isVariableDeclaration(a) && t.isVariableDeclaration(b)) {
const { name: nameA } = a.declarations[0].id as t.Identifier;
const { name: nameB } = b.declarations[0].id as t.Identifier;
const result = getOrder(nameA) - getOrder(nameB);
return result;
}
return 0;
});
if (ast.includes) {
Object.keys(ast.includeMap).forEach(i => {
body.unshift(this.processIncludes(i, ast));
});
}
const temp = template.ast(
`module.exports = {${exportId.join(',')}}`,
) as t.Statement;
body.push(temp);
mock.program.body = [genMockPublic(ctx, this.options), ...body];
ctx.output.set(
getOutputName({
source: `${removeFileExt(ast.idlPath)}.mock.js`,
outputDir: this.options.outputDir,
idlRoot: this.options.idlRoot,
}),
{ type: 'babel', content: mock },
);
return ctx;
});
}
private getMockVarOrder(file: t.File) {
const orders = {} as Record<string, number>;
file.program.body.map((i, index) => {
if (t.isVariableDeclaration(i)) {
const identifier = i.declarations[0].id as t.Identifier;
orders[identifier.name] = index;
}
});
return orders;
}
private processIdlNode(
statement: UnifyStatement,
ctx: ProcessIdlCtxWithMock,
) {
if (isStructDefinition(statement)) {
return this.processStructNode(statement, ctx);
} else if (isTypedefDefinition(statement)) {
return this.processTypeDefNode(statement, ctx);
} else if (isEnumDefinition(statement)) {
return this.processEnumDefNode(statement, ctx);
} else if (isConstDefinition(statement)) {
return this.processConstDefNode(statement, ctx);
} else if (isServiceDefinition(statement)) {
return this.processServiceDefinition(statement, ctx);
}
throw new Error(`can not process Node from statement type: ${statement}`);
}
private processIncludes(include: string, ast: IParseResultItem) {
const includePath = getRelativePath(ast.idlPath, ast.includeMap[include]);
const name = ast.includeRefer[include];
const temp = template.ast(
`const ${name} = require('${`${includePath}.mock.js`}')`,
) as t.ImportDeclaration;
return temp;
}
private processServiceDefinition(
srtuct: ServiceDefinition,
ctx: ProcessIdlCtxWithMock,
) {
const { name, functions } = srtuct;
if (this.findTarget(name.value, ctx)) {
return;
}
const variableDeclaration = template.ast(
`var ${name.value} = {${functions.map(f => {
const { name, returnType, fields } = f;
const reqType = fields[0].fieldType as any;
const resType = this.processReqResPramsType(returnType, ctx.ast);
return `${name.value}:{req:${parseId(reqType.value)},res:${parseId(resType)}}`;
})}}`,
) as t.ExpressionStatement;
return variableDeclaration;
}
private processReqResPramsType(
fieldType: FunctionType,
ast: IParseResultItem,
) {
if (isIdentifier(fieldType)) {
const statement = getStatementById(fieldType, ast);
if (isStructDefinition(statement)) {
const wholeBody = statement.fields.find(isFullBody);
if (wholeBody) {
// 处理 api.body="."
const { annotations } = wholeBody;
if (hasDynamicJsonAnnotation(annotations)) {
return '{}';
}
return `${fieldType.value}['${getFieldsAlias(wholeBody)}']`;
} else {
return fieldType.value;
}
}
throw new Error('params must be identifier');
} else {
return 'void';
}
}
private processStructNode(
struct: StructDefinition,
ctx: ProcessIdlCtxWithMock,
) {
const { name, fields } = struct;
if (this.findTarget(name.value, ctx)) {
return;
}
const oldOne = this.getVariableDeclarationById(name.value, ctx);
const variableDeclaration =
oldOne ||
(template.ast(
`var ${name.value} = createStruct(()=>{ return {} })`,
) as t.VariableDeclaration);
const init = (variableDeclaration.declarations[0].init as t.CallExpression)
.arguments[0] as t.ArrowFunctionExpression;
const returnObj = (
(init.body as t.BlockStatement).body.find(i =>
t.isReturnStatement(i),
) as t.ReturnStatement
).argument as t.ObjectExpression;
if (!returnObj) {
throw new Error('struct mock must return obj');
}
const fieldNames = new Set(fields.map(i => getFieldsAlias(i)));
const newPros = [] as t.ObjectProperty[];
const includeFieldName = (pro: t.ObjectProperty) =>
(t.isStringLiteral(pro.key) && fieldNames.has(pro.key.value)) ||
(t.isIdentifier(pro.key) && fieldNames.has(pro.key.name));
returnObj.properties.forEach(i => {
if (t.isObjectProperty(i)) {
if (includeFieldName(i)) {
const key = t.isStringLiteral(i.key)
? i.key.value
: t.isIdentifier(i.key)
? i.key.name
: '';
fieldNames.delete(key);
}
newPros.push(i);
}
});
fields.forEach(f => {
const { fieldType, defaultValue } = f;
const fieldName = getFieldsAlias(f);
if (!fieldNames.has(fieldName)) {
return;
}
// 没有的,需要重新生成
newPros.push(
t.objectProperty(
fieldName.includes('-')
? t.stringLiteral(fieldName)
: t.identifier(fieldName),
this.processValue(fieldType, defaultValue || undefined, {
fieldDefinition: f,
struct,
ast: ctx.ast,
}),
),
);
});
returnObj.properties = newPros;
// this.processNodes.set(name.value, variableDeclaration);
return variableDeclaration;
}
private processValue(
fieldType: FieldType | FunctionType,
defaultValue?: ConstValue,
context?: GenMockFieldCtx['context'],
) {
const ctx = this.program.trigger(HOOK.GEN_MOCK_FILED, {
fieldType,
defaultValue,
context,
} as GenMockFieldCtx);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return ctx.output!;
}
// eslint-disable-next-line complexity
private genMockField(ctx: GenMockFieldCtx) {
let { output } = ctx;
const { defaultValue, fieldType } = ctx;
if (output) {
return ctx;
}
if (isBaseType(fieldType)) {
const type = getBaseTypeConverts('number')[fieldType.type];
if (type === 'string') {
let value = faker.word.words();
if (defaultValue && defaultValue.type === SyntaxType.StringLiteral) {
value = defaultValue.value;
}
output = t.stringLiteral(value);
} else if (type === 'number') {
let value = faker.number.int();
if (defaultValue && defaultValue.type === SyntaxType.IntConstant) {
value = Number(defaultValue.value.value);
}
output = t.numericLiteral(value);
} else if (type === 'boolean') {
let value = faker.datatype.boolean();
if (defaultValue && defaultValue.type === SyntaxType.BooleanLiteral) {
value = defaultValue.value;
}
output = t.booleanLiteral(value);
} else if (type === 'object') {
// binary
output = t.callExpression(
t.memberExpression(t.identifier('Buffer'), t.identifier('from')),
[t.stringLiteral(faker.word.words())],
);
}
} else if (isMapType(fieldType)) {
const { valueType } = fieldType;
output = t.objectExpression([
t.objectProperty(
t.identifier(faker.word.words()),
this.processValue(valueType),
),
]);
} else if (isListType(fieldType)) {
const { valueType } = fieldType;
output = t.arrayExpression([this.processValue(valueType)]);
} else if (isSetType(fieldType)) {
// set 处理成array校验
const { valueType } = fieldType;
output = t.arrayExpression([this.processValue(valueType)]);
} else if (isIdentifier(fieldType)) {
// 引用类型
const { refName, namespace } = parseIdFiledType(fieldType);
if (!namespace) {
output = t.callExpression(t.identifier(refName), []);
} else {
output = t.callExpression(
t.memberExpression(
t.identifier(uniformNs(namespace)),
t.identifier(refName),
),
[],
);
}
}
if (output) {
return { fieldType, defaultValue, output };
}
throw new Error(`can not process fieldType : ${fieldType.type}`);
}
private processConst(constVal: ConstValue) {
// 暂时统一处理成0
if (isStringLiteral(constVal)) {
return t.stringLiteral(constVal.value);
}
if (isIntConstant(constVal)) {
return t.stringLiteral(constVal.value.value);
}
return t.numericLiteral(0);
}
private processTypeDefNode(
typeDef: TypedefDefinition,
ctx: ProcessIdlCtxWithMock,
) {
const { definitionType, name } = typeDef;
if (this.findTarget(name.value, ctx)) {
return;
}
const builder = template(`var ${name.value}= () => %%value%% `);
const variableDeclaration = builder({
value: this.processValue(definitionType),
}) as t.VariableDeclaration;
return variableDeclaration;
}
private processEnumDefNode(def: EnumDefinition, ctx: ProcessIdlCtxWithMock) {
const { name, members } = def;
const values = getValuesFromEnum(def);
const commentValues = values.map((value, index) => {
const { name } = members[index];
return ` ${name.value}: ${value}`;
});
const comment = { type: 'CommentLine', value: commentValues } as any;
const target = this.findTarget(name.value, ctx);
if (target) {
// 需要更新注释
// target.trailingComments = [comment];
return;
}
// 枚举类型统一处理成常量
const builder = template(`var ${name.value}= () => %%value%% `);
const node = builder({
value: t.numericLiteral(values[0] || 0),
}) as t.VariableDeclaration;
const variableDeclaration = t.addComments(node, 'trailing', [comment]);
return variableDeclaration;
}
private processConstDefNode(
constDef: ConstDefinition,
ctx: ProcessIdlCtxWithMock,
) {
const { name, initializer } = constDef;
if (this.findTarget(name.value, ctx)) {
return;
}
const builder = template(`var ${name.value}= () => %%value%% `);
const node = builder({
value: this.processConst(initializer),
}) as t.VariableDeclaration;
// const variableDeclaration = t.addComment(
// ,
// 'leading',
// '暂时对const默认处理为0如有需要请自行重新赋值'
// );
return node;
}
private getVariableDeclarationById(id: string, ctx: ProcessIdlCtxWithMock) {
return ctx.mock.program.body.find(i => {
if (t.isVariableDeclaration(i)) {
const identifier = i.declarations[0].id as t.Identifier;
if (identifier.name === id) {
return true;
}
}
return false;
}) as t.VariableDeclaration | undefined;
}
private findTarget(id: string, ctx: ProcessIdlCtxWithMock) {
return ctx.mockStatements.find(i => this.getIdName(i) === id);
}
private getIdName(statement: t.Statement): string {
if (
t.isVariableDeclaration(statement) &&
t.isVariableDeclarator(statement.declarations[0])
) {
if (t.isIdentifier(statement.declarations[0].id)) {
return statement.declarations[0].id.name;
}
}
return '';
}
}

View File

@@ -0,0 +1,136 @@
/*
* 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 { EOL } from 'os';
import { type IPlugin, type Program, after } from '@coze-arch/idl2ts-plugin';
import {
type ProcessIdlCtx,
type IParseEntryCtx,
getRelativePath,
createFile,
getOutputName,
} from '@coze-arch/idl2ts-helper';
import { type Options } from '../types';
import { type Contexts, HOOK } from '../context';
/**
* 提供统一 api 入口
*/
export class PkgEntryPlugin implements IPlugin {
private options: Options;
private funcs: Map<string, string> = new Map();
private entryName: string;
constructor(options: Options) {
this.options = options;
this.entryName = `/${this.options.entryName || 'api'}.ts`;
}
apply(program: Program<Contexts>): void {
program.register(after(HOOK.PARSE_FUN_META), this.collectFuncs.bind(this));
program.register(after(HOOK.GEN_FILE_AST), this.genPkgEntry.bind(this));
program.register(after(HOOK.GEN_FILE_AST), this.genValidateFile.bind(this));
}
private collectFuncs(ctx: ProcessIdlCtx) {
const { ast, meta } = ctx;
const relativePath = getRelativePath(
this.options.idlRoot + this.entryName,
ast.idlPath,
);
this.funcs.set(
relativePath,
// 只支持单 service
meta[0].service,
);
return ctx;
}
private genPkgEntry = (ctx: IParseEntryCtx) => {
const targetFile = this.options.outputDir + this.entryName;
let source = '';
for (const [path, service] of this.funcs.entries()) {
source += `export * as ${service} from '${path}';${EOL}`;
}
const content = createFile(source);
ctx.files.set(targetFile, { content, type: 'babel' });
return ctx;
};
private genValidateFile = (ctx: IParseEntryCtx) => {
const { ast } = ctx;
const targetFile = `${this.options.outputDir}/_schemas.js`;
const targetFileTypes = `${this.options.outputDir}/_schemas.d.ts`;
const schemaFiles = this.options.genSchema
? ast.map(
i =>
`${getRelativePath(
targetFile,
getOutputName({
source: i.idlPath,
...this.options,
}),
)}.schema.json`,
)
: [];
const code = schemaFiles.map(i => ` require("${i}")`).join(`,${EOL}`);
ctx.files.set(targetFile, {
content: `module.exports = [${EOL + code + EOL}] `,
type: 'text',
});
ctx.files.set(targetFileTypes, {
content: `declare const _schemas: any[];
export default _schemas;
`,
type: 'text',
});
ctx.files.set(`${this.options.outputDir}/_mock_utils.js`, {
content: getMockUtils(),
type: 'text',
});
return ctx;
};
}
function getMockUtils() {
return `function rawParse(str) {
const lines = (str || '').split('\\n');
const entries = lines.map((line) => {
line = line.trim();
const res = line.match(/at (.+) \\((.+)\\)/)||[]
return {
beforeParse: line,
callee: res[1]
};
});
return entries.filter((x) => x.callee !== undefined);
}
function createStruct(fn) {
const structFactory = () => {
const error = new Error();
const items = rawParse(error.stack).filter((i) => i.callee === 'structFactory').map((i) => i.beforeParse);
const isCircle = items.length > Array.from(new Set(items)).length;
if (isCircle) {
return {};
}
const res = fn();
return res;
};
return structFactory;
}
module.exports={ createStruct }
`;
}

View File

@@ -0,0 +1,322 @@
/*
* 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 {
type IPlugin,
type Program,
on,
before,
after,
} from '@coze-arch/idl2ts-plugin';
import {
type EnumDefinition,
isServiceDefinition,
isStructDefinition,
isIdentifier,
type StructDefinition,
isBaseType,
type FieldType,
type FunctionType,
isEnumDefinition,
isListType,
isStringLiteral,
type TypedefDefinition,
type ConstDefinition,
type ConstValue,
isSetType,
isMapType,
isIntConstant,
isConstDefinition,
isTypedefDefinition,
type UnifyStatement,
SyntaxType,
type ServiceDefinition,
parseIdFiledType,
getValuesFromEnum,
removeFileExt,
parseFiledName,
getAnnotation,
getTypeFromDynamicJsonAnnotation,
getSchemaRootByPath,
getOutputName,
} from '@coze-arch/idl2ts-helper';
import {
type Contexts,
HOOK,
type ProcessIdlCtxWithSchema,
type AjvType,
type ListType,
type RefType,
type StringType,
type NumberType,
type BoolType,
type EnumType,
type ConstType,
type StructType,
} from '../context';
interface IOptions {
outputDir: string;
idlRoot: string;
}
export class SchemaPlugin implements IPlugin {
private options: IOptions;
constructor(options: IOptions) {
this.options = options;
}
apply(program: Program<Contexts>): void {
program.register(
on(HOOK.PROCESS_IDL_NODE),
ctx => {
const { schema, node } = ctx;
// if (!node) {
// return ctx;
// }
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const schemas = this.processIdlNode(node!, ctx);
Object.keys(schemas).forEach(id => {
schema.definitions[id] = schemas[id];
});
return ctx;
},
0,
);
program.register(before(HOOK.PROCESS_IDL_AST), ctx => {
ctx.schema = {
$id: getSchemaRootByPath(ctx.ast.idlPath, this.options.idlRoot),
definitions: {},
};
return ctx;
});
program.register(after(HOOK.PROCESS_IDL_AST), ctx => {
const { ast, schema } = ctx;
ctx.output.set(
getOutputName({
source: `${removeFileExt(ast.idlPath)}.schema.json`,
outputDir: this.options.outputDir,
idlRoot: this.options.idlRoot,
}),
{ type: 'json', content: schema },
);
return ctx;
});
}
private processIdlNode(
statement: UnifyStatement,
ctx: ProcessIdlCtxWithSchema,
): { [key: string]: AjvType } {
if (isStructDefinition(statement)) {
return this.processStructNode(statement, ctx);
} else if (isTypedefDefinition(statement)) {
return this.processTypeDefNode(statement, ctx);
} else if (isEnumDefinition(statement)) {
return this.processEnumDefNode(statement);
} else if (isConstDefinition(statement)) {
return this.processConstDefNode(statement);
} else if (isServiceDefinition(statement)) {
return this.processServiceDefinition(statement, ctx);
}
throw new Error(`canot process Node from statement type: ${statement}`);
}
private processServiceDefinition(
struct: ServiceDefinition,
ctx: ProcessIdlCtxWithSchema,
) {
const { functions } = struct;
const schemas = {} as { [key: string]: AjvType };
functions.map(f => {
const { name, returnType, fields } = f;
const reqSchema = this.processValue(fields[0].fieldType, ctx);
const resSchema = this.processValue(returnType, ctx);
schemas[`${name.value}.req`] = reqSchema;
schemas[`${name.value}.res`] = resSchema;
});
return schemas;
}
private processStructNode(
srtuct: StructDefinition,
ctx: ProcessIdlCtxWithSchema,
) {
const { name, fields } = srtuct;
const schema = {
properties: {},
required: [],
type: 'object',
} as StructType;
fields.forEach(i => {
const fieldName = parseFiledName(i);
if (schema.properties[fieldName]) {
return;
}
const { requiredness, annotations, fieldType } = i;
const isAnyType = getAnnotation(annotations, 'api.value_type') === 'any';
const dynamicType = getTypeFromDynamicJsonAnnotation(annotations);
const required = requiredness !== 'optional';
let valueType: AjvType = this.processValue(fieldType, ctx);
if (isAnyType || dynamicType) {
valueType = {};
}
if (required) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
schema.required!.push(fieldName);
}
schema.properties[fieldName] = valueType;
});
return { [name.value]: schema };
}
private processValue(
fieldType: FieldType | FunctionType,
ctx: ProcessIdlCtxWithSchema,
) {
if (isBaseType(fieldType)) {
const type = getBaseTypeConverts('number')[fieldType.type];
if (fieldType.type === SyntaxType.I64Keyword) {
// i64
const schema: NumberType = { type: 'integer' };
return schema;
}
if (type === 'string') {
const schema: StringType = { type: 'string' };
return schema;
} else if (type === 'number') {
const schema: NumberType = { type: 'integer' };
return schema;
} else if (type === 'boolean') {
const schema: BoolType = { type: 'boolean' };
return schema;
} else if (type === 'object') {
// todo check binary
const schema: StructType = { type: 'object', properties: {} };
return schema;
}
} else if (isMapType(fieldType)) {
const { valueType } = fieldType;
const schema: StructType = {
type: 'object',
additionalProperties: this.processValue(valueType, ctx),
};
return schema;
} else if (isListType(fieldType)) {
const { valueType } = fieldType;
const schema: ListType = {
type: 'array',
items: this.processValue(valueType, ctx),
};
return schema;
} else if (isSetType(fieldType)) {
// set 处理成array校验
const { valueType } = fieldType;
const schema: ListType = {
type: 'array',
items: this.processValue(valueType, ctx),
};
return schema;
} else if (isIdentifier(fieldType)) {
// 引用类型
const { refName, namespace } = parseIdFiledType(fieldType);
if (!namespace) {
const schema: RefType = { $ref: `#/definitions/${refName}` };
return schema;
} else {
const schema: RefType = {
$ref: `${this.getSchemeRootByNamespace(
namespace,
ctx,
)}#/definitions/${refName}`,
};
return schema;
}
}
throw new Error(`can not process fieldType : ${fieldType.type}`);
}
private processConst(constVal: ConstValue) {
// 暂时统一处理成0
const schema = {} as ConstType;
if (isStringLiteral(constVal)) {
schema.const = constVal.value;
}
if (isIntConstant(constVal)) {
schema.const = constVal.value.value;
}
return schema;
}
private processTypeDefNode(
typeDef: TypedefDefinition,
ctx: ProcessIdlCtxWithSchema,
) {
const { definitionType, name } = typeDef;
const def = { [name.value]: this.processValue(definitionType, ctx) };
return def;
}
private processEnumDefNode(def: EnumDefinition) {
const { name } = def;
const values = getValuesFromEnum(def);
const schema = { enum: values.length > 0 ? values : [0] } as EnumType;
return { [name.value]: schema };
}
private processConstDefNode(constDef: ConstDefinition) {
const { name, initializer } = constDef;
return { [name.value]: this.processConst(initializer) };
}
/**
* namespace -> relative-path
* @param namespace
* @param ctx
*/
private getSchemeRootByNamespace(
namespace: string,
ctx: ProcessIdlCtxWithSchema,
) {
const { ast } = ctx;
let target = '';
Object.keys(ast.deps).forEach(i => {
// const pathName = getSchemaRootByPath(i, this.options.idlRoot);
if (i.endsWith(`/${namespace}.thrift`)) {
target = getSchemaRootByPath(i, this.options.idlRoot);
}
});
return target;
}
}
// import { SyntaxType } from "@coze-arch/idl-parser";
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',
};
}