feat: сделать split-клиент режимом по умолчанию

- добавлен operationsTree для сборки полного клиента
- удален режим генерации both
- обновлена документация под npm SDK workflow
- поднята версия пакета до 4.0.0
This commit is contained in:
2026-06-30 10:46:15 +03:00
parent bf340b3dbe
commit fe5d3ae091
9 changed files with 445 additions and 109 deletions

View File

@@ -33,7 +33,7 @@ program
.option('-i, --input <path>', 'Путь к OpenAPI спецификации (JSON/YAML файл или URL)')
.option('-o, --output <path>', 'Директория для сохранения сгенерированных файлов')
.option('-n, --name <name>', 'Имя монолитного клиента без расширения .ts')
.option('--mode <mode>', 'Режим генерации: single, split, both', 'single')
.option('--mode <mode>', 'Режим генерации: split, single', 'split')
.option('--single-file', 'Устаревший алиас для --mode single')
.action(async (options) => {
try {

View File

@@ -1,4 +1,4 @@
export type GeneratorMode = 'single' | 'split' | 'both';
export type GeneratorMode = 'single' | 'split';
/**
* Конфигурация генератора API
@@ -8,7 +8,7 @@ export interface GeneratorConfig {
inputPath: string;
/** Путь для сохранения сгенерированных файлов */
outputPath: string;
/** Имя сгенерированного файла (без расширения), используется в single/both режиме */
/** Имя сгенерированного файла (без расширения), используется в single режиме */
fileName?: string;
/** Режим генерации */
mode?: GeneratorMode;
@@ -28,8 +28,8 @@ export function validateConfig(config: Partial<GeneratorConfig>): config is Gene
errors.push('Не указана директория для генерации (--output)');
}
if (config.mode && !['single', 'split', 'both'].includes(config.mode)) {
errors.push('Некорректный режим генерации (--mode). Доступные значения: single, split, both');
if (config.mode && !['single', 'split'].includes(config.mode)) {
errors.push('Некорректный режим генерации (--mode). Доступные значения: split, single');
}
if (errors.length > 0) {

View File

@@ -38,6 +38,11 @@ type OperationFileInfo = {
fileName: string;
};
type OperationTreeGroup = {
moduleName: string;
operations: OperationFileInfo[];
};
const RESERVED_IDENTIFIERS = new Set([
'break',
'case',
@@ -204,6 +209,53 @@ function createHttpClientImport(content: string): string {
return imports.join('\n');
}
function createObjectPropertyKey(name: string): string {
return JSON.stringify(name);
}
function createOperationTreePropertyName(operationFile: OperationFileInfo): string {
return operationFile.route.routeName.usage || operationFile.operationName;
}
function createOperationTreeContent(configuration: GenerateApiConfiguration, operationFiles: OperationFileInfo[]): string {
const operationByRoute = new Map(operationFiles.map((operationFile) => [operationFile.route, operationFile]));
const imports = operationFiles.map(
(operationFile) => `import { ${operationFile.operationName} } from "./operations/${operationFile.fileName}";`,
);
const outOfModuleOperations = (configuration.routes.outOfModule || [])
.map((route) => operationByRoute.get(route))
.filter((operationFile): operationFile is OperationFileInfo => Boolean(operationFile));
const operationTreeGroups: OperationTreeGroup[] = (configuration.routes.combined || []).map(({ moduleName, routes }) => ({
moduleName,
operations: (routes || [])
.map((route) => operationByRoute.get(route))
.filter((operationFile): operationFile is OperationFileInfo => Boolean(operationFile)),
}));
const lines = [
...imports,
'',
'export const operationsTree = {',
];
for (const operationFile of outOfModuleOperations) {
lines.push(` ${createObjectPropertyKey(createOperationTreePropertyName(operationFile))}: ${operationFile.operationName},`);
}
for (const { moduleName, operations } of operationTreeGroups) {
lines.push(` ${createObjectPropertyKey(moduleName)}: {`);
for (const operationFile of operations) {
lines.push(` ${createObjectPropertyKey(createOperationTreePropertyName(operationFile))}: ${operationFile.operationName},`);
}
lines.push(' },');
}
lines.push('} as const;', '', 'export type OperationsTree = typeof operationsTree;');
return lines.join('\n');
}
async function renderTemplateFile(
generatorOutput: GenerateApiOutput,
templatePath: string,
@@ -229,6 +281,7 @@ async function cleanTreeOutput(outputDir: string): Promise<void> {
rm(join(outputDir, 'http-client.ts'), { force: true }),
rm(join(outputDir, 'data-contracts.ts'), { force: true }),
rm(join(outputDir, 'create-api-client.ts'), { force: true }),
rm(join(outputDir, 'operations-tree.ts'), { force: true }),
]);
}
@@ -319,9 +372,9 @@ export async function generate(config: GeneratorConfig): Promise<void> {
const outputDir = resolve(config.outputPath);
const outputFileName = `${fileName}.ts`;
const mode = config.mode || 'single';
const shouldGenerateSingle = mode === 'single' || mode === 'both';
const shouldGenerateSplit = mode === 'split' || mode === 'both';
const mode = config.mode || 'split';
const shouldGenerateSingle = mode === 'single';
const shouldGenerateSplit = mode === 'split';
const baseGenerateOptions = {
...(isUrl ? { url } : { input: inputPath }),
@@ -477,6 +530,12 @@ export async function generate(config: GeneratorConfig): Promise<void> {
operationExports.length ? operationExports.join('\n') : 'export {};',
);
await writeFormattedFile(
generatorOutput,
join(outputDir, 'operations-tree.ts'),
createOperationTreeContent(generatorOutput.configuration, operationFiles),
);
await writeFormattedFile(
generatorOutput,
join(outputDir, 'index.ts'),
@@ -486,6 +545,9 @@ export async function generate(config: GeneratorConfig): Promise<void> {
'export { ContentType, HttpClient } from "./http-client";',
'export type { ApiConfig, ApiRequestClient, FullRequestParams, HttpResponse, QueryParamsType, RequestParams, ResponseFormat } from "./http-client";',
'export type * from "./data-contracts";',
'export { operationsTree } from "./operations-tree";',
'export type { OperationsTree } from "./operations-tree";',
'export * as operations from "./operations";',
'export * from "./operations";',
].join('\n'),
);