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

@@ -63,7 +63,7 @@ describe('E2E Generation', () => {
// 3. Проверка импорта (компиляция TypeScript)
const testFile = join(tempDir, 'test-import.ts');
const testCode = `
import { createApiClient, HttpClient } from '${generatedFile}';
import { createApiClient, HttpClient, operationsTree } from '${generatedFile}';
import { getAll } from '${join(outputPath, 'operations', 'get-all.ts')}';
const api = createApiClient(new HttpClient(), {
@@ -71,6 +71,8 @@ describe('E2E Generation', () => {
getAll,
},
});
const fullApi = createApiClient(new HttpClient(), operationsTree);
fullApi.getAll;
console.log('Import successful');
`;
@@ -95,9 +97,10 @@ describe('E2E Generation', () => {
expect(exitCode).toBe(0);
const generatedFile = join(outputPath, 'MinimalAPI.ts');
const exists = await fileExists(generatedFile);
expect(exists).toBe(true);
expect(await fileExists(join(outputPath, 'index.ts'))).toBe(true);
expect(await fileExists(join(outputPath, 'http-client.ts'))).toBe(true);
expect(await fileExists(join(outputPath, 'operations-tree.ts'))).toBe(true);
expect(await fileExists(join(outputPath, 'operations', 'index.ts'))).toBe(true);
}, 30000);
test('повторная генерация (перезапись файлов)', async () => {

View File

@@ -42,9 +42,10 @@ describe('CLI', () => {
expect(exitCode).toBe(0);
const generatedFile = join(outputPath, 'MinimalAPI.ts');
const exists = await fileExists(generatedFile);
expect(exists).toBe(true);
expect(await fileExists(join(outputPath, 'index.ts'))).toBe(true);
expect(await fileExists(join(outputPath, 'http-client.ts'))).toBe(true);
expect(await fileExists(join(outputPath, 'operations-tree.ts'))).toBe(true);
expect(await fileExists(join(outputPath, 'operations', 'index.ts'))).toBe(true);
}, 30000);
test('должен генерировать монолит с кастомным именем файла', async () => {
@@ -60,6 +61,8 @@ describe('CLI', () => {
outputPath,
'--name',
customName,
'--mode',
'single',
]);
expect(exitCode).toBe(0);
@@ -86,29 +89,30 @@ describe('CLI', () => {
expect(exitCode).toBe(0);
expect(await fileExists(join(outputPath, 'index.ts'))).toBe(true);
expect(await fileExists(join(outputPath, 'http-client.ts'))).toBe(true);
expect(await fileExists(join(outputPath, 'operations-tree.ts'))).toBe(true);
expect(await fileExists(join(outputPath, 'operations', 'index.ts'))).toBe(true);
}, 30000);
test('должен генерировать both режим', async () => {
test('должен отклонять удаленный both режим', async () => {
const outputPath = join(tempDir, 'output');
const { exitCode } = await execa('bun', [
'run',
CLI_PATH,
'--input',
FIXTURES.MINIMAL,
'--output',
outputPath,
'--name',
'CustomApi',
'--mode',
'both',
]);
expect(exitCode).toBe(0);
expect(await fileExists(join(outputPath, 'CustomApi.ts'))).toBe(true);
expect(await fileExists(join(outputPath, 'index.ts'))).toBe(true);
expect(await fileExists(join(outputPath, 'operations', 'index.ts'))).toBe(true);
try {
await execa('bun', [
'run',
CLI_PATH,
'--input',
FIXTURES.MINIMAL,
'--output',
outputPath,
'--mode',
'both',
]);
throw new Error('Should have thrown');
} catch (error: any) {
expect(error.exitCode).not.toBe(0);
expect(error.stderr).toContain('Некорректный режим генерации');
expect(error.stderr).toContain('split, single');
}
}, 30000);
test('должен отображать версию с --version', async () => {

View File

@@ -45,5 +45,15 @@ describe('config', () => {
expect(() => validateConfig(config)).toThrow('Ошибка конфигурации');
});
test('должен выбросить ошибку для удаленного both режима', () => {
const config = {
inputPath: './openapi.json',
outputPath: './output',
mode: 'both',
};
expect(() => validateConfig(config as Partial<GeneratorConfig>)).toThrow('Доступные значения: split, single');
});
});
});

View File

@@ -21,7 +21,7 @@ describe('Generator', () => {
});
describe('корректная генерация', () => {
test('должен создать выходной файл', async () => {
test('должен по умолчанию создать split структуру', async () => {
const outputPath = join(tempDir, 'output');
const config: GeneratorConfig = {
inputPath: FIXTURES.MINIMAL,
@@ -31,9 +31,10 @@ describe('Generator', () => {
await generate(config);
const generatedFile = join(outputPath, 'TestApi.ts');
const exists = await fileExists(generatedFile);
expect(exists).toBe(true);
expect(await fileExists(join(outputPath, 'index.ts'))).toBe(true);
expect(await fileExists(join(outputPath, 'http-client.ts'))).toBe(true);
expect(await fileExists(join(outputPath, 'operations-tree.ts'))).toBe(true);
expect(await fileExists(join(outputPath, 'operations', 'index.ts'))).toBe(true);
}, 30000);
test('должен добавлять русскую подпись в generated файлы', async () => {
@@ -42,6 +43,7 @@ describe('Generator', () => {
inputPath: FIXTURES.MINIMAL,
outputPath,
fileName: 'TestApi',
mode: 'single',
};
await generate(config);
@@ -91,12 +93,77 @@ describe('Generator', () => {
const indexFile = join(outputPath, 'index.ts');
const httpClientFile = join(outputPath, 'http-client.ts');
const operationsTreeFile = join(outputPath, 'operations-tree.ts');
const indexContent = await readTextFile(indexFile);
const httpClientContent = await readTextFile(httpClientFile);
const operationsTreeContent = await readTextFile(operationsTreeFile);
// Проверяем наличие основных элементов
expect(indexContent).toContain('createApiClient');
expect(indexContent).toContain('export { operationsTree } from "./operations-tree"');
expect(indexContent).toContain('export type { OperationsTree } from "./operations-tree"');
expect(indexContent).toContain('export * as operations from "./operations"');
expect(httpClientContent).toContain('export class HttpClient');
expect(operationsTreeContent).toContain('export const operationsTree');
expect(operationsTreeContent).toContain('export type OperationsTree');
}, 30000);
test('должен генерировать operationsTree с группировкой по тегам', async () => {
const inputPath = join(tempDir, 'tagged.json');
const outputPath = join(tempDir, 'output');
await Bun.write(inputPath, JSON.stringify({
openapi: '3.0.0',
info: {
title: 'Tagged API',
version: '1.0.0',
},
paths: {
'/users': {
get: {
tags: ['users'],
operationId: 'UserController_getAll',
responses: {
200: { description: 'OK' },
},
},
post: {
tags: ['users'],
operationId: 'UserController_create',
responses: {
201: { description: 'Created' },
},
},
},
'/auth/login': {
post: {
tags: ['auth'],
operationId: 'AuthController_login',
responses: {
200: { description: 'OK' },
},
},
},
},
}));
await generate({
inputPath,
outputPath,
fileName: 'TaggedApi',
mode: 'split',
});
const content = await readTextFile(join(outputPath, 'operations-tree.ts'));
expect(content).toContain('import { create } from "./operations/create"');
expect(content).toContain('import { getAll } from "./operations/get-all"');
expect(content).toContain('import { login } from "./operations/login"');
expect(content).toContain('users: {');
expect(content).toContain('getAll: getAll');
expect(content).toContain('create: create');
expect(content).toContain('auth: {');
expect(content).toContain('login: login');
}, 30000);
test('должен обработать все HTTP методы', async () => {
@@ -123,6 +190,26 @@ describe('Generator', () => {
expect(content).toContain('deleteUsersId');
}, 30000);
test('должен генерировать индексный реэкспорт всех operations', async () => {
const outputPath = join(tempDir, 'output');
const config: GeneratorConfig = {
inputPath: FIXTURES.VALID,
outputPath,
fileName: 'TestApi',
mode: 'split',
};
await generate(config);
const content = await readTextFile(join(outputPath, 'operations', 'index.ts'));
expect(content).toContain('export { create } from "./create";');
expect(content).toContain('export { deleteUsersId } from "./delete-users-id";');
expect(content).toContain('export { getAll } from "./get-all";');
expect(content).toContain('export { getById } from "./get-by-id";');
expect(content).toContain('export { update } from "./update";');
}, 30000);
test('должен генерировать типы для request и response', async () => {
const outputPath = join(tempDir, 'output');
const config: GeneratorConfig = {
@@ -235,7 +322,7 @@ describe('Generator', () => {
// Генерация должна пройти успешно
await generate(config);
const generatedFile = join(outputPath, 'TestApi.ts');
const generatedFile = join(outputPath, 'index.ts');
const exists = await fileExists(generatedFile);
expect(exists).toBe(true);
}, 30000);
@@ -250,7 +337,7 @@ describe('Generator', () => {
await generate(config);
const generatedFile = join(outputPath, 'TestApi.ts');
const generatedFile = join(outputPath, 'index.ts');
const exists = await fileExists(generatedFile);
expect(exists).toBe(true);
}, 30000);
@@ -285,7 +372,7 @@ describe('Generator', () => {
// Генерация должна пройти успешно даже с Unicode
await generate(config);
const generatedFile = join(outputPath, 'TestApi.ts');
const generatedFile = join(outputPath, 'index.ts');
const exists = await fileExists(generatedFile);
expect(exists).toBe(true);
}, 30000);