feat: добавить split-режим генерации REST-клиента

- добавлен режим генерации single, split и both
- добавлены отдельные operation-файлы и createApiClient
- удалена генерация SWR-хуков и зависимости React/SWR
- обновлены CLI, шаблоны, примеры, документация и тесты
- версия пакета повышена до 3.0.0
This commit is contained in:
2026-06-30 07:59:52 +03:00
parent 961c7f0ec1
commit bf340b3dbe
21 changed files with 1029 additions and 732 deletions

View File

@@ -47,7 +47,7 @@ describe('CLI', () => {
expect(exists).toBe(true);
}, 30000);
test('должен генерировать с кастомным именем файла', async () => {
test('должен генерировать монолит с кастомным именем файла', async () => {
const outputPath = join(tempDir, 'output');
const customName = 'CustomApi';
@@ -69,6 +69,48 @@ describe('CLI', () => {
expect(exists).toBe(true);
}, 30000);
test('должен генерировать split режим', async () => {
const outputPath = join(tempDir, 'output');
const { exitCode } = await execa('bun', [
'run',
CLI_PATH,
'--input',
FIXTURES.MINIMAL,
'--output',
outputPath,
'--mode',
'split',
]);
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', 'index.ts'))).toBe(true);
}, 30000);
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);
}, 30000);
test('должен отображать версию с --version', async () => {
const { stdout } = await execa('bun', ['run', CLI_PATH, '--version']);
@@ -78,7 +120,7 @@ describe('CLI', () => {
test('должен отображать help с --help', async () => {
const { stdout } = await execa('bun', ['run', CLI_PATH, '--help']);
expect(stdout).toContain('Generate TypeScript API client');
expect(stdout).toContain('Генерация TypeScript API клиента');
expect(stdout).toContain('--input');
expect(stdout).toContain('--output');
});
@@ -141,5 +183,26 @@ describe('CLI', () => {
expect(error.exitCode).not.toBe(0);
}
}, 30000);
test('должен показывать русскую ошибку для недоступного URL', async () => {
const outputPath = join(tempDir, 'output');
try {
await execa('bun', [
'run',
CLI_PATH,
'--input',
'https://127.0.0.1:1/swagger.json',
'--output',
outputPath,
]);
throw new Error('Should have thrown');
} catch (error: any) {
expect(error.exitCode).not.toBe(0);
expect(error.stderr).toContain('Не удалось подключиться к OpenAPI спецификации');
expect(error.stderr).toContain('Проверьте доступность URL и сетевое подключение');
expect(error.stderr).not.toContain('Unable to connect');
}
}, 30000);
});
});
});

View File

@@ -29,7 +29,7 @@ describe('config', () => {
outputPath: './output',
};
expect(() => validateConfig(config)).toThrow('Input path is required');
expect(() => validateConfig(config)).toThrow('Не указан путь к OpenAPI спецификации');
});
test('должен выбросить ошибку без outputPath', () => {
@@ -37,13 +37,13 @@ describe('config', () => {
inputPath: './openapi.json',
};
expect(() => validateConfig(config)).toThrow('Output path is required');
expect(() => validateConfig(config)).toThrow('Не указана директория для генерации');
});
test('должен выбросить ошибку без обоих обязательных полей', () => {
const config: Partial<GeneratorConfig> = {};
expect(() => validateConfig(config)).toThrow('Configuration validation failed');
expect(() => validateConfig(config)).toThrow('Ошибка конфигурации');
});
});
});
});

View File

@@ -36,7 +36,7 @@ describe('Generator', () => {
expect(exists).toBe(true);
}, 30000);
test('должен генерировать корректную структуру кода', async () => {
test('должен добавлять русскую подпись в generated файлы', async () => {
const outputPath = join(tempDir, 'output');
const config: GeneratorConfig = {
inputPath: FIXTURES.MINIMAL,
@@ -49,9 +49,54 @@ describe('Generator', () => {
const generatedFile = join(outputPath, 'TestApi.ts');
const content = await readTextFile(generatedFile);
expect(content.startsWith('/* eslint-disable */\n/* tslint:disable */\n// @ts-nocheck\n\n/*')).toBe(true);
expect(content).toContain('АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ');
expect(content).toContain('Не редактируйте вручную: изменения будут перезаписаны.');
expect(content).toContain('Генератор: @gromlab/api-codegen');
expect(content).toContain('Репозиторий: https://gromlab.ru/gromov/api-codegen');
expect(content).not.toContain('THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API');
}, 30000);
test('должен добавлять русскую подпись в split режиме', async () => {
const outputPath = join(tempDir, 'output');
const config: GeneratorConfig = {
inputPath: FIXTURES.MINIMAL,
outputPath,
fileName: 'TestApi',
mode: 'split',
};
await generate(config);
const generatedFile = join(outputPath, 'index.ts');
const content = await readTextFile(generatedFile);
expect(content.startsWith('/* eslint-disable */\n/* tslint:disable */\n// @ts-nocheck\n\n/*')).toBe(true);
expect(content).toContain('АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ');
expect(content).toContain('Не редактируйте вручную: изменения будут перезаписаны.');
expect(content).toContain('Генератор: @gromlab/api-codegen');
expect(content).not.toContain('THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API');
}, 30000);
test('должен генерировать корректную структуру кода', async () => {
const outputPath = join(tempDir, 'output');
const config: GeneratorConfig = {
inputPath: FIXTURES.MINIMAL,
outputPath,
fileName: 'TestApi',
mode: 'split',
};
await generate(config);
const indexFile = join(outputPath, 'index.ts');
const httpClientFile = join(outputPath, 'http-client.ts');
const indexContent = await readTextFile(indexFile);
const httpClientContent = await readTextFile(httpClientFile);
// Проверяем наличие основных элементов
expect(content).toContain('export class');
expect(content).toContain('HttpClient');
expect(indexContent).toContain('createApiClient');
expect(httpClientContent).toContain('export class HttpClient');
}, 30000);
test('должен обработать все HTTP методы', async () => {
@@ -60,12 +105,13 @@ describe('Generator', () => {
inputPath: FIXTURES.VALID,
outputPath,
fileName: 'TestApi',
mode: 'split',
};
await generate(config);
const generatedFile = join(outputPath, 'TestApi.ts');
const content = await readTextFile(generatedFile);
const operationsIndex = join(outputPath, 'operations', 'index.ts');
const content = await readTextFile(operationsIndex);
// Проверяем что методы UserController переименованы
// UserController_getAll -> getAll
@@ -74,7 +120,7 @@ describe('Generator', () => {
expect(content).toContain('create');
expect(content).toContain('getById');
expect(content).toContain('update');
expect(content).toContain('delete');
expect(content).toContain('deleteUsersId');
}, 30000);
test('должен генерировать типы для request и response', async () => {
@@ -83,11 +129,12 @@ describe('Generator', () => {
inputPath: FIXTURES.VALID,
outputPath,
fileName: 'TestApi',
mode: 'split',
};
await generate(config);
const generatedFile = join(outputPath, 'TestApi.ts');
const generatedFile = join(outputPath, 'data-contracts.ts');
const content = await readTextFile(generatedFile);
// Проверяем наличие типов
@@ -102,15 +149,16 @@ describe('Generator', () => {
inputPath: FIXTURES.VALID,
outputPath,
fileName: 'TestApi',
mode: 'split',
};
await generate(config);
const generatedFile = join(outputPath, 'TestApi.ts');
const generatedFile = join(outputPath, 'data-contracts.ts');
const content = await readTextFile(generatedFile);
// Проверяем наличие enum
expect(content).toContain('UserRole');
expect(content).toContain('type UserRole');
expect(content).toContain('admin');
expect(content).toContain('user');
expect(content).toContain('guest');
@@ -122,11 +170,12 @@ describe('Generator', () => {
inputPath: FIXTURES.WITH_AUTH,
outputPath,
fileName: 'TestApi',
mode: 'split',
};
await generate(config);
const generatedFile = join(outputPath, 'TestApi.ts');
const generatedFile = join(outputPath, 'http-client.ts');
const content = await readTextFile(generatedFile);
// Проверяем наличие методов для работы с токеном
@@ -139,11 +188,12 @@ describe('Generator', () => {
inputPath: FIXTURES.VALID,
outputPath,
fileName: 'TestApi',
mode: 'split',
};
await generate(config);
const generatedFile = join(outputPath, 'TestApi.ts');
const generatedFile = join(outputPath, 'http-client.ts');
const content = await readTextFile(generatedFile);
// Проверяем что baseUrl установлен
@@ -156,11 +206,12 @@ describe('Generator', () => {
inputPath: FIXTURES.VALID,
outputPath,
fileName: 'TestApi',
mode: 'split',
};
await generate(config);
const generatedFile = join(outputPath, 'TestApi.ts');
const generatedFile = join(outputPath, 'operations', 'index.ts');
const content = await readTextFile(generatedFile);
// Проверяем что "Controller" удален из имен методов
@@ -210,11 +261,12 @@ describe('Generator', () => {
inputPath: FIXTURES.COMPLEX,
outputPath,
fileName: 'TestApi',
mode: 'split',
};
await generate(config);
const generatedFile = join(outputPath, 'TestApi.ts');
const generatedFile = join(outputPath, 'operations', 'index.ts');
const content = await readTextFile(generatedFile);
// Проверяем что все контроллеры присутствуют
@@ -238,4 +290,4 @@ describe('Generator', () => {
expect(exists).toBe(true);
}, 30000);
});
});
});