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,63 @@
# @coze-arch/idl2ts-cli
@coze-arch/idl2ts-cli
## Overview
This package is part of the Coze Studio monorepo and provides architecture functionality. It serves as a core component in the Coze ecosystem.
## Getting Started
### Installation
Add this package to your `package.json`:
```json
{
"dependencies": {
"@coze-arch/idl2ts-cli": "workspace:*"
}
}
```
Then run:
```bash
rush update
```
### Usage
```typescript
import { /* exported functions/components */ } from '@coze-arch/idl2ts-cli';
// Example usage
// TODO: Add specific usage examples
```
## Features
- Core functionality for Coze Studio
- TypeScript support
- Modern ES modules
## API Reference
Please refer to the TypeScript definitions for detailed API documentation.
## Development
This package is built with:
- TypeScript
- Modern JavaScript
- Vitest for testing
- ESLint for code quality
## Contributing
This package is part of the Coze Studio monorepo. Please follow the monorepo contribution guidelines.
## License
Apache-2.0

View File

@@ -0,0 +1,12 @@
{
"operationSettings": [
{
"operationName": "test:cov",
"outputFolderNames": ["coverage"]
},
{
"operationName": "ts-check",
"outputFolderNames": ["dist"]
}
]
}

View File

@@ -0,0 +1,15 @@
const { defineConfig } = require('@coze-arch/eslint-config');
module.exports = defineConfig({
packageRoot: __dirname,
preset: 'node',
rules: {
'@typescript-eslint/naming-convention': 'off',
'unicorn/filename-case': 'off',
'max-statements-per-line': 'off',
'max-lines': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@coze-arch/max-line-per-function': 'off',
'@typescript-eslint/consistent-type-assertions': 'off',
},
});

View File

@@ -0,0 +1,38 @@
{
"name": "@coze-arch/idl2ts-cli",
"version": "0.1.7",
"description": "@coze-arch/idl2ts-cli",
"homepage": "",
"license": "Apache-2.0",
"author": "fanwenjie.fe@bytedance.com",
"bin": {
"idl2ts": "./src/cli.js"
},
"scripts": {
"build": "exit 0",
"lint": "eslint ./ --cache",
"test": "vitest --run --passWithNoTests",
"test:cov": "npm run test -- --coverage"
},
"dependencies": {
"@babel/types": "^7.20.7",
"@coze-arch/idl2ts-generator": "workspace:*",
"@coze-arch/idl2ts-helper": "workspace:*",
"@coze-arch/idl2ts-plugin": "workspace:*",
"@faker-js/faker": "~9.3.0",
"commander": "^12.0.0",
"dayjs": "^1.11.7",
"ora": "^5.3.1",
"prettier": "~3.3.3"
},
"devDependencies": {
"@coze-arch/eslint-config": "workspace:*",
"@coze-arch/ts-config": "workspace:*",
"@coze-arch/vitest-config": "workspace:*",
"@types/node": "^18",
"@vitest/coverage-v8": "~3.0.5",
"sucrase": "^3.32.0",
"tsx": "^4.19.2",
"vitest": "~3.0.5"
}
}

View File

@@ -0,0 +1,139 @@
/*
* 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 { genClient } from '@coze-arch/idl2ts-generator';
import { lookupConfig } from './utils';
import { type ApiConfig, type ApiTypeConfig } from './types';
import { MockPlugin } from './plugins/mock-plugin';
import { LocalConfigPlugin } from './plugins/local-config';
import { FormatPlugin } from './plugins/formatter';
import { FilterTypesPlugin } from './plugins/filter-types-plugin';
import { AliasPlugin } from './plugins/alias';
interface GenOptions {
formatConfig?: string;
}
export const gen = (projectRoot: string, options: GenOptions) => {
const configs = lookupConfig(projectRoot);
function genSingle(config: ApiConfig) {
const {
entries,
plugins = [],
commonCodePath,
aggregationExport,
formatter,
} = config;
const aliasMap = new Map();
const idlRoot = path.resolve(projectRoot, config.idlRoot);
const output = path.resolve(projectRoot, config.output);
const realEntries = [] as string[];
Object.keys(entries).forEach(i => {
aliasMap.set(path.resolve(idlRoot, entries[i]), i);
realEntries.push(entries[i]);
});
genClient({
entries: realEntries,
idlRoot: path.resolve(projectRoot, idlRoot),
genSchema: false,
genClient: true,
genMock: false,
plugins: [
new MockPlugin(),
new AliasPlugin(aliasMap),
new FormatPlugin({
path: path.resolve(
process.cwd(),
options.formatConfig || '.prettierrc',
),
formatter,
}),
new LocalConfigPlugin({ outputDir: output, projectRoot, idlRoot }),
...plugins,
],
entryName: aggregationExport || 'index',
outputDir: output,
commonCodePath,
});
}
configs.forEach(c => {
genSingle(c);
});
};
export const genTypes = (projectRoot: string, options: GenOptions) => {
const configs = lookupConfig<ApiTypeConfig>(projectRoot, 'api.filter.js');
function genSingle(config: ApiTypeConfig) {
const {
entries,
plugins = [],
commonCodePath,
aggregationExport,
formatter,
filters,
} = config;
const aliasMap = new Map();
const idlRoot = path.resolve(projectRoot, config.idlRoot);
const output = path.resolve(projectRoot, config.output);
const realEntries = [] as string[];
Object.keys(entries).forEach(i => {
aliasMap.set(path.resolve(idlRoot, entries[i]), i);
realEntries.push(entries[i]);
});
genClient({
entries: realEntries,
idlRoot: path.resolve(projectRoot, idlRoot),
genSchema: false,
genClient: true,
genMock: false,
plugins: [
new MockPlugin(),
new AliasPlugin(aliasMap),
new FormatPlugin({
path: path.resolve(
process.cwd(),
options.formatConfig || '.prettierrc',
),
formatter,
}),
new LocalConfigPlugin({ outputDir: output, projectRoot, idlRoot }),
new FilterTypesPlugin(filters, output),
...plugins,
],
entryName: aggregationExport || 'index',
outputDir: output,
commonCodePath,
});
}
configs.forEach(c => {
genSingle(c);
});
};
export function defineConfig(c: ApiConfig[]) {
return c;
}
export function defineApiTpeConfig(c: ApiTypeConfig[]) {
return c;
}

View File

@@ -0,0 +1,3 @@
require('sucrase/register/ts');
require('./cli.ts');

View File

@@ -0,0 +1,79 @@
/*
* 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 ora from 'ora';
import { Command } from 'commander';
import { gen, genTypes } from './actions';
const main = () => {
const program = new Command();
program
.command('gen')
.description('gen api code by thrift or pb')
.argument('<projectRoot>', 'project root')
.option(
'-f --format-config <formatConfig>',
'prettier config file',
'.prettierrc',
)
.action(
(projectRoot, options: { genMock: boolean; formatConfig: string }) => {
const spinner = ora(
'Generating api. It may take a few seconds',
).start();
try {
gen(projectRoot, {
formatConfig: options.formatConfig,
});
spinner.succeed('Generate api successfully');
} catch (error) {
spinner.fail('Generate api fail');
console.error(error);
process.exit(1);
}
},
);
program
.command('filter')
.description('filter api types')
.argument('<projectRoot>', 'project root')
.option(
'-f --format-config <formatConfig>',
'prettier config file',
'.prettierrc',
)
.action((projectRoot, options: { formatConfig: string }) => {
const spinner = ora(
'Generating filtered types. It may take a few seconds',
).start();
try {
genTypes(projectRoot, options);
spinner.succeed('Generate filtered types successfully');
} catch (error) {
spinner.fail('Generate filtered types fail');
console.error(error);
process.exit(1);
}
});
program.parse(process.argv);
};
main();

View File

@@ -0,0 +1,105 @@
/*
* 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 ApiConfig } from './types';
let hasShowHint = false;
function requiredWithoutCache(src, onError?) {
let data;
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { Module } = require('module');
try {
// disable 了 require 的缓存,这样可以改变了 mock 数据后,无需重启服务。
const originCache = Module._cache;
Module._cache = {};
// eslint-disable-next-line security/detect-non-literal-require, @typescript-eslint/no-require-imports
data = require(src);
Module._cache = originCache;
} catch (error) {
console.error(error);
if (onError) {
onError(error);
} else {
console.error(error);
}
}
return data;
}
export function createProxy({
root,
handleResponseData,
}: {
root: string;
handleResponseData?: (service: string, method: string, data: any) => any;
}) {
// eslint-disable-next-line security/detect-non-literal-require, @typescript-eslint/no-require-imports
const apiConfig = require(path.resolve(root, 'api.config.js')) as ApiConfig[];
// eslint-disable-next-line max-params
return async function proxyResWithMock(_, __, req, resp) {
if (!req.headers['x-svc-method']) {
return Promise.resolve();
}
const config = requiredWithoutCache(
path.resolve(root, './api.dev.local.js'),
() => {
if (!hasShowHint) {
console.warn(
'can not find mock config, please run "gen-api" command if you want to mock',
);
hasShowHint = true;
}
},
);
if (
config &&
config.mock.includes(req.headers['x-svc-method'].split('_').join('.'))
) {
const [svc, method] = req.headers['x-svc-method'].split('_');
const target = apiConfig.find(i => i.entries[svc].length > 0);
if (!target) {
return Promise.resolve();
}
const src = path.resolve(
root,
target.output,
target.entries[svc].replace(/\.(thrift|proto)$/, '.mock.js'),
);
const data = requiredWithoutCache(src);
if (data) {
try {
if (resp) {
resp.statusCode = 200;
resp.setHeader('Content-Type', 'application/json');
} else {
console.warn('resp is not defined');
}
const res = await data[svc][method].res(req);
if (handleResponseData) {
return await handleResponseData(svc, method, res);
}
return res;
} catch (error) {
return error;
}
}
}
return Promise.resolve();
};
}

View File

@@ -0,0 +1,59 @@
/*
* 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, after } from '@coze-arch/idl2ts-plugin';
import {
type IParseEntryCtx,
isServiceDefinition,
} from '@coze-arch/idl2ts-helper';
import { HOOK } from '@coze-arch/idl2ts-generator';
interface IOptions {
patch: {
[service: string]: {
prefix?: string;
method?: { [name: string]: 'GET' | 'POST' };
};
};
}
export class PatchPlugin implements IPlugin {
private options: IOptions;
constructor(options: IOptions) {
this.options = options;
}
apply(p: Program) {
p.register(after(HOOK.PARSE_ENTRY), (ctx: IParseEntryCtx) => {
ctx.ast = ctx.ast.map(i => {
i.statements.map(s => {
if (isServiceDefinition(s) && this.options.patch[s.name.value]) {
const { prefix = '/', method = {} } =
this.options.patch[s.name.value];
s.functions.forEach(f => {
f.extensionConfig = {
uri: `${prefix}/${f.name.value}`,
method: method[f.name.value] || 'POST',
};
});
}
return s;
});
return i;
});
return ctx;
});
}
}

View File

@@ -0,0 +1,17 @@
/*
* 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 { PatchPlugin } from './forward';

View File

@@ -0,0 +1,47 @@
/*
* 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, on } from '@coze-arch/idl2ts-plugin';
import {
type IParseEntryCtx,
isServiceDefinition,
} from '@coze-arch/idl2ts-helper';
import { HOOK } from '@coze-arch/idl2ts-generator';
export class AliasPlugin {
alias = new Map();
constructor(alias: Map<string, string>) {
this.alias = alias;
}
apply(program: Program) {
program.register(on(HOOK.PARSE_ENTRY), this.setAlias.bind(this));
}
setAlias(ctx: IParseEntryCtx) {
ctx.ast.forEach(i => {
if (i.isEntry) {
i.statements.forEach(s => {
if (isServiceDefinition(s) && this.alias.has(i.idlPath)) {
s.name.value = this.alias.get(i.idlPath);
}
});
}
});
return ctx;
}
}

View File

@@ -0,0 +1,72 @@
/*
* 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, after } from '@coze-arch/idl2ts-plugin';
import { type IParseEntryCtx } from '@coze-arch/idl2ts-helper';
import { HOOK } from '@coze-arch/idl2ts-generator';
/**
*
* @param {string} content
* @param {"CommentBlock" | "CommentLine"} type
* @returns {{
* value: string;
* type: "CommentBlock" | "CommentLine";
* }}
*/
function createComment(content, type = 'CommentBlock') {
return {
value: content,
type,
};
}
export class CommentPlugin implements IPlugin {
config: { comments: string[] };
comments: any[] = [];
/**
* @param {{comments: string[]}} config
*/
constructor(config) {
this.config = config;
}
apply(program: Program) {
program.register(after(HOOK.GEN_FILE_AST), this.addComment.bind(this));
}
addComment(ctx: IParseEntryCtx) {
const { files } = ctx;
for (const [file, res] of files.entries()) {
if (
res.type === 'babel' &&
file.includes('/auto-gen/') &&
file.endsWith('.ts')
) {
res.content.leadingComments = this.getComments();
}
}
return ctx;
}
getComments() {
if (this.comments) {
return this.comments;
}
this.comments = this.config.comments.map(i => createComment(i));
return this.comments;
}
}

View File

@@ -0,0 +1,230 @@
/*
* 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 Program, after } from '@coze-arch/idl2ts-plugin';
import {
type Comment,
type EnumDefinition,
type FieldDefinition,
type FieldType,
type FunctionType,
type IParseEntryCtx,
type IParseResultItem,
type Identifier,
type ProcessIdlCtx,
type StructDefinition,
SyntaxType,
type UnifyStatement,
createFile,
findDefinition,
getParseResultFromNamespace,
getStatementById,
getValuesFromEnum,
isBaseType,
isEnumDefinition,
isIdentifier,
isListType,
isMapType,
isServiceDefinition,
isSetType,
isStructDefinition,
parseIdFiledType,
withExportDeclaration,
} from '@coze-arch/idl2ts-helper';
import { HOOK } from '@coze-arch/idl2ts-generator';
// eslint-disable-next-line @coze-arch/no-batch-import-or-export
import * as t from '@babel/types';
export class FilterTypesPlugin {
methods: Record<string, string[]>;
statements: Record<string, UnifyStatement[]> = {};
enums: Record<string, EnumDefinition[]> = {};
output: string;
constructor(methods: Record<string, string[]>, output: string) {
this.methods = methods;
this.output = output;
}
apply(program: Program) {
program.register(after(HOOK.PARSE_ENTRY), this.filterTypes.bind(this));
program.register(
after(HOOK.PROCESS_IDL_AST),
this.genEnumsFiles.bind(this),
);
}
genEnumsFiles(ctx: ProcessIdlCtx) {
const file = createFile('');
Object.keys(this.enums).forEach(key => {
const defs = this.enums[key];
const block = t.tsModuleBlock([]);
defs.forEach(d => {
const values = getValuesFromEnum(d);
const objExps = d.members.map((m, index) => {
const valueProps = t.objectProperty(
t.identifier('value'),
t.numericLiteral(values[index]),
);
t.addComment(valueProps, 'trailing', 'm.name.value');
return t.objectExpression([
valueProps,
t.objectProperty(
t.identifier('label'),
t.stringLiteral(m.name.value),
),
]);
});
const constNode = t.variableDeclaration('const', [
t.variableDeclarator(
t.identifier(d.name.value),
t.arrayExpression(objExps),
),
]);
block.body.push(withExportDeclaration(constNode));
});
const exportNode = withExportDeclaration(
t.tsModuleDeclaration(t.identifier(key), block),
);
file.program.body.push(exportNode);
});
ctx.output.set(path.resolve(this.output, 'enums.ts'), {
type: 'babel',
content: file,
});
return ctx;
}
filterTypes(ctx: IParseEntryCtx) {
ctx.ast.forEach(i => {
if (i.isEntry) {
i.statements.forEach(s => {
if (isServiceDefinition(s) && this.methods[s.name.value]) {
for (const f of s.functions) {
if (this.methods[s.name.value].includes(f.name.value)) {
if (isIdentifier(f.returnType)) {
this.lookupTypes(f.returnType, i);
}
const fieldType = f.fields[0]?.fieldType;
if (isIdentifier(fieldType)) {
this.lookupTypes(fieldType, i);
}
}
}
}
});
}
});
ctx.ast = ctx.ast
.filter(i => this.statements[i.idlPath])
.map(i => ({ ...i, statements: this.statements[i.idlPath] }));
return ctx;
}
private lookupTypes(id: Identifier, current: IParseResultItem) {
const { namespace, refName } = parseIdFiledType(id);
if (namespace) {
const next = getParseResultFromNamespace(namespace, current);
const nextID = findDefinition(next.statements, refName);
if (nextID) {
this.lookupTypes(nextID.name, next);
}
} else {
const statement = getStatementById(id, current);
if (statement) {
if (this.statements[current.idlPath]) {
if (!this.statements[current.idlPath].includes(statement)) {
this.statements[current.idlPath].push(statement);
}
} else {
this.statements[current.idlPath] = [statement];
}
if (isStructDefinition(statement)) {
this.lookupStructTypes(statement, current);
}
}
}
}
private lookupStructTypes(
statement: StructDefinition,
current: IParseResultItem,
) {
for (const field of statement.fields) {
const { fieldType } = field;
this.processFiledType(fieldType, current, field);
}
}
private processFiledType(
fieldType: FieldType | FunctionType,
current: IParseResultItem,
field: FieldDefinition,
) {
if (isBaseType(fieldType)) {
return;
} else if (isListType(fieldType) || isSetType(fieldType)) {
const { valueType } = fieldType;
return this.processFiledType(valueType, current, field);
} else if (isMapType(fieldType)) {
const { valueType } = fieldType;
return this.processFiledType(valueType, current, field);
} else if (isIdentifier(fieldType)) {
const statement = getStatementById(fieldType, current);
if (isEnumDefinition(statement)) {
// 强制转位 number
// @ts-expect-error fixme late
fieldType.type = SyntaxType.I32Keyword;
let namespace = current.unifyNamespace;
const parsedFieldType = parseIdFiledType(fieldType);
if (parsedFieldType.namespace) {
const next = getParseResultFromNamespace(
parsedFieldType.namespace,
current,
);
namespace = next.unifyNamespace;
}
const extraComment = {
type: SyntaxType.CommentLine,
value: `@see${fieldType.value}`,
} as Comment;
if (field.comments) {
field.comments.push(extraComment);
} else {
field.comments = [extraComment];
}
if (this.enums[namespace]) {
if (
this.enums[namespace].some(
i => i.name.value === statement.name.value,
)
) {
return;
}
this.enums[namespace].push(statement);
} else {
this.enums[namespace] = [statement];
}
return;
} else {
return this.lookupTypes(fieldType, current);
}
}
throw new Error(`unknown type:${fieldType.type}`);
}
}

View File

@@ -0,0 +1,83 @@
/*
* 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 fs from 'fs';
import { format } from 'prettier';
import { type Program, on } from '@coze-arch/idl2ts-plugin';
import { HOOK, type WriteFileCtx } from '@coze-arch/idl2ts-generator';
interface IOption {
path: string;
formatter?: (content: string, filename: string) => string;
}
function isPromise(p: any) {
return (
p.then &&
typeof p.then === 'function' &&
typeof p.catch === 'function' &&
typeof p.finally === 'function'
);
}
function readConfig(file: string) {
let config = {};
try {
// eslint-disable-next-line security/detect-non-literal-require, @typescript-eslint/no-require-imports
config = require(file);
if (!config) {
const content = fs.readFileSync(file, { encoding: 'utf8' });
config = JSON.parse(content);
}
// eslint-disable-next-line @coze-arch/no-empty-catch
} catch (error) {
// just
}
return config;
}
export class FormatPlugin {
private config: any;
private formatter?: (content: string, filename: string) => string;
constructor(op: IOption) {
this.config = readConfig(op.path);
this.formatter = op.formatter;
}
apply(program: Program) {
program.register(on(HOOK.WRITE_FILE), this.format.bind(this));
}
format(ctx: WriteFileCtx) {
if (this.formatter) {
ctx.content = this.formatter(ctx.content, ctx.filename);
return ctx;
}
if (ctx.filename.endsWith('ts')) {
try {
const content = format(ctx.content, {
...this.config,
parser: 'typescript',
});
if (!isPromise(content)) {
// @ts-expect-error fixme late
ctx.content = content;
}
} catch (error) {
console.warn(error);
}
}
return ctx;
}
}

View File

@@ -0,0 +1,61 @@
/*
* 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, after } from '@coze-arch/idl2ts-plugin';
import { type IParseEntryCtx } from '@coze-arch/idl2ts-helper';
import { HOOK } from '@coze-arch/idl2ts-generator';
interface Config {
idlRoot: string;
outputDir: string;
projectRoot: string;
}
export class LocalConfigPlugin implements IPlugin {
config: Config;
/**
* @param {} config
*/
constructor(config: Config) {
this.config = config;
}
apply(program: Program) {
program.register(after(HOOK.GEN_FILE_AST), this.genLocalConfig.bind(this));
}
genLocalConfig(ctx: IParseEntryCtx) {
const mockFile = { mock: [] };
const target = path.resolve(this.config.projectRoot, './api.dev.local.js');
try {
// eslint-disable-next-line security/detect-non-literal-require, @typescript-eslint/no-require-imports
const local_config = require(target);
mockFile.mock = local_config.mock || [];
// eslint-disable-next-line @coze-arch/no-empty-catch, no-empty
} catch (error) {}
const content = `
module.exports = {
mock:[${mockFile.mock.map(i => `"${i}"`).join(', ')}],
}
`;
ctx.files.set(target, { type: 'text', content });
return ctx;
}
}

View File

@@ -0,0 +1,101 @@
/*
* 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 dayjs from 'dayjs';
import { faker } from '@faker-js/faker';
import { type IPlugin, type Program, before } from '@coze-arch/idl2ts-plugin';
import {
type IntConstant,
isBaseType,
SyntaxType,
getBaseTypeConverts,
} from '@coze-arch/idl2ts-helper';
import { type GenMockFieldCtx, HOOK } from '@coze-arch/idl2ts-generator';
// eslint-disable-next-line @coze-arch/no-batch-import-or-export
import * as t from '@babel/types';
const NumMapper = {
total: 1,
code: 0,
};
const StrMapper = {
name: faker.person.lastName(),
};
export class MockPlugin implements IPlugin {
apply(program: Program) {
program.register(before(HOOK.GEN_MOCK_FILED), this.genMockValue.bind(this));
}
// eslint-disable-next-line complexity
genMockValue = (ctx: GenMockFieldCtx) => {
const { context, fieldType, defaultValue } = 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 as any).value;
}
if (context) {
const { fieldDefinition } = context;
const fieldName = fieldDefinition.name.value;
// 各类 ID
if (fieldName.toLocaleUpperCase().endsWith('ID')) {
value = String(faker.number.int());
}
// email 处理
if (fieldName.includes('Email')) {
value = `${faker.person.lastName()}@foo.com`;
}
// 直接映射值
value = StrMapper[fieldName] || value;
}
ctx.output = t.stringLiteral(value);
} else if (type === 'number') {
let value = faker.number.int({ min: 0, max: 10000 });
if (defaultValue && defaultValue.type === SyntaxType.IntConstant) {
value = Number((defaultValue as IntConstant).value.value);
}
if (context) {
const { fieldDefinition } = context;
const fieldName = fieldDefinition.name.value;
const formatName = fieldName.toLocaleUpperCase();
// 各类 ID
if (formatName.endsWith('ID')) {
value = faker.number.int();
}
// 时间戳
if (formatName.endsWith('TIME') || formatName.includes('TIMESTAMP')) {
value = dayjs(faker.date.anytime()).valueOf();
}
// 类型状态
if (formatName.endsWith('STATUS') || formatName.includes('TYPE')) {
value = faker.number.int({ min: 0, max: 1 });
}
// 直接映射值
const mapVal = NumMapper[fieldName];
value = typeof mapVal !== 'undefined' ? mapVal : value;
}
ctx.output = t.numericLiteral(value);
}
}
return ctx;
};
}

View File

@@ -0,0 +1,53 @@
/*
* 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 } from '@coze-arch/idl2ts-generator';
export interface ApiConfig {
// idl 入口
entries: Record<string, string>;
// idl 根目录
idlRoot: string;
// 服务别名
// 自定义 api 方法
commonCodePath: string;
// api 产物目录
output: string;
// 仓库信息设置
repository?: {
// 仓库地址
url: string;
// clone 到本地的位置
dest: string;
};
// 插件
plugins?: IPlugin[];
// 聚合导出的文件名
aggregationExport?: string;
// 格式化文件
formatter: (name: string, content: string) => string;
idlFetchConfig?: {
source: string;
branch?: string;
commit?: string;
rootDir?: string;
};
}
export interface ApiTypeConfig extends ApiConfig {
// 需要过滤的方法
filters: Record<string, string[]>;
}

View File

@@ -0,0 +1,33 @@
/*
* 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 ApiConfig } from './types';
export function lookupConfig<T = ApiConfig>(
projectRoot: string,
configName = 'api.config',
) {
const apiConfigPath = path.resolve(process.cwd(), projectRoot, configName);
try {
require.resolve(apiConfigPath);
} catch (error) {
throw Error(`Can not find api config in path ${process.cwd()}`);
}
// eslint-disable-next-line security/detect-non-literal-require, @typescript-eslint/no-require-imports
return require(apiConfigPath) as T[];
}

View File

@@ -0,0 +1,34 @@
{
"extends": "@coze-arch/ts-config/tsconfig.node.json",
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"module": "CommonJS",
"target": "ES2020",
"moduleResolution": "node",
"tsBuildInfoFile": "dist/tsconfig.build.tsbuildinfo"
},
"include": ["src"],
"exclude": ["node_modules", "dist"],
"references": [
{
"path": "../../../config/eslint-config/tsconfig.build.json"
},
{
"path": "../../../config/ts-config/tsconfig.build.json"
},
{
"path": "../../../config/vitest-config/tsconfig.build.json"
},
{
"path": "../idl2ts-generator/tsconfig.build.json"
},
{
"path": "../idl2ts-helper/tsconfig.build.json"
},
{
"path": "../idl2ts-plugin/tsconfig.build.json"
}
]
}

View File

@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"exclude": ["**/*"],
"compilerOptions": {
"composite": true
},
"references": [
{
"path": "./tsconfig.build.json"
},
{
"path": "./tsconfig.misc.json"
}
]
}

View File

@@ -0,0 +1,18 @@
{
"extends": "@coze-arch/ts-config/tsconfig.node.json",
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"rootDir": "./",
"outDir": "./dist",
"module": "CommonJS",
"target": "ES2020",
"moduleResolution": "node"
},
"include": ["__tests__", "vitest.config.ts"],
"exclude": ["./dist"],
"references": [
{
"path": "./tsconfig.build.json"
}
]
}

View File

@@ -0,0 +1,22 @@
/*
* 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 { defineConfig } from '@coze-arch/vitest-config';
export default defineConfig({
dirname: __dirname,
preset: 'node',
});