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

@@ -49,23 +49,28 @@ describe('E2E Generation', () => {
FIXTURES.VALID,
'--output',
outputPath,
'--name',
'TestApi',
'--mode',
'split',
]);
expect(exitCode).toBe(0);
// 2. Проверка создания файла
const generatedFile = join(outputPath, 'TestApi.ts');
const generatedFile = join(outputPath, 'index.ts');
const exists = await fileExists(generatedFile);
expect(exists).toBe(true);
// 3. Проверка импорта (компиляция TypeScript)
const testFile = join(tempDir, 'test-import.ts');
const testCode = `
import { Api } from '${generatedFile}';
const api = new Api();
import { createApiClient, HttpClient } from '${generatedFile}';
import { getAll } from '${join(outputPath, 'operations', 'get-all.ts')}';
const api = createApiClient(new HttpClient(), {
users: {
getAll,
},
});
console.log('Import successful');
`;
@@ -97,24 +102,7 @@ describe('E2E Generation', () => {
test('повторная генерация (перезапись файлов)', async () => {
const outputPath = join(tempDir, 'output');
const fileName = 'TestApi';
// Первая генерация
await execa('bun', [
'run',
CLI_PATH,
'--input',
FIXTURES.MINIMAL,
'--output',
outputPath,
'--name',
fileName,
]);
const generatedFile = join(outputPath, `${fileName}.ts`);
const firstContent = await readTextFile(generatedFile);
// Вторая генерация (перезапись)
// Первая генерация с endpoints
await execa('bun', [
'run',
CLI_PATH,
@@ -122,8 +110,25 @@ describe('E2E Generation', () => {
FIXTURES.VALID,
'--output',
outputPath,
'--name',
fileName,
'--mode',
'split',
]);
const generatedFile = join(outputPath, 'operations', 'index.ts');
const staleOperationFile = join(outputPath, 'operations', 'get-all.ts');
const firstContent = await readTextFile(generatedFile);
expect(await fileExists(staleOperationFile)).toBe(true);
// Вторая генерация (перезапись меньшей схемой)
await execa('bun', [
'run',
CLI_PATH,
'--input',
FIXTURES.MINIMAL,
'--output',
outputPath,
'--mode',
'split',
]);
const secondContent = await readTextFile(generatedFile);
@@ -134,6 +139,7 @@ describe('E2E Generation', () => {
// Файл должен существовать
const exists = await fileExists(generatedFile);
expect(exists).toBe(true);
expect(await fileExists(staleOperationFile)).toBe(false);
}, 60000);
test('генерация из HTTP URL', async () => {
@@ -147,50 +153,41 @@ describe('E2E Generation', () => {
'https://petstore3.swagger.io/api/v3/openapi.json',
'--output',
outputPath,
'--name',
'PetStore',
'--mode',
'split',
]);
expect(exitCode).toBe(0);
const generatedFile = join(outputPath, 'PetStore.ts');
const generatedFile = join(outputPath, 'index.ts');
const exists = await fileExists(generatedFile);
expect(exists).toBe(true);
// Проверяем что файл не пустой
const content = await readTextFile(generatedFile);
const content = await readTextFile(join(outputPath, 'http-client.ts'));
expect(content.length).toBeGreaterThan(1000);
}, 60000);
test('генерация с флагом --swr', async () => {
test('флаг --swr больше не поддерживается', async () => {
const outputPath = join(tempDir, 'output');
const { exitCode } = await execa('bun', [
'run',
CLI_PATH,
'--input',
FIXTURES.VALID,
'--output',
outputPath,
'--name',
'SwrApi',
'--swr',
]);
expect(exitCode).toBe(0);
const generatedFile = join(outputPath, 'SwrApi.ts');
const content = await readTextFile(generatedFile);
// Проверяем наличие импорта useSWR
expect(content).toContain('import useSWR from "swr"');
// Проверяем наличие use* хуков для GET запросов
expect(content).toContain('useGetAll');
expect(content).toContain('useGetById');
try {
await execa('bun', [
'run',
CLI_PATH,
'--input',
FIXTURES.VALID,
'--output',
outputPath,
'--swr',
]);
throw new Error('Should have thrown');
} catch (error: any) {
expect(error.exitCode).not.toBe(0);
}
}, 30000);
test('генерация без флага --swr не содержит хуки', async () => {
test('REST генерация не содержит SWR хуки', async () => {
const outputPath = join(tempDir, 'output');
const { exitCode } = await execa('bun', [
@@ -200,13 +197,13 @@ describe('E2E Generation', () => {
FIXTURES.VALID,
'--output',
outputPath,
'--name',
'NoSwrApi',
'--mode',
'split',
]);
expect(exitCode).toBe(0);
const generatedFile = join(outputPath, 'NoSwrApi.ts');
const generatedFile = join(outputPath, 'http-client.ts');
const content = await readTextFile(generatedFile);
// Проверяем отсутствие импорта useSWR
@@ -229,16 +226,21 @@ describe('E2E Generation', () => {
FIXTURES.VALID,
'--output',
outputPath,
'--name',
'TestApi',
'--mode',
'split',
]);
// Динамически импортируем сгенерированный API
const generatedFile = join(outputPath, 'TestApi.ts');
const { Api } = await import(generatedFile);
const api = new Api();
const result = await api.users.getAll();
const generatedFile = join(outputPath, 'index.ts');
const { createApiClient, HttpClient } = await import(generatedFile);
const { getAll } = await import(join(outputPath, 'operations', 'get-all.ts'));
const api = createApiClient(new HttpClient(), {
users: {
getAll,
},
});
const result = await api.users.getAll({});
expect(Array.isArray(result)).toBe(true);
expect(result.length).toBe(2);
@@ -254,15 +256,20 @@ describe('E2E Generation', () => {
FIXTURES.VALID,
'--output',
outputPath,
'--name',
'TestApi',
'--mode',
'split',
]);
// Динамически импортируем сгенерированный API
const generatedFile = join(outputPath, 'TestApi.ts');
const { Api } = await import(generatedFile);
const api = new Api();
const generatedFile = join(outputPath, 'index.ts');
const { createApiClient, HttpClient } = await import(generatedFile);
const { create } = await import(join(outputPath, 'operations', 'create.ts'));
const api = createApiClient(new HttpClient(), {
users: {
create,
},
});
const result = await api.users.create({
email: 'new@example.com',
password: 'password123'
@@ -282,17 +289,22 @@ describe('E2E Generation', () => {
FIXTURES.VALID,
'--output',
outputPath,
'--name',
'TestApi',
'--mode',
'split',
]);
const testFile = join(tempDir, 'test-404.ts');
const testCode = `
import { Api } from '${join(outputPath, 'TestApi.ts')}';
const api = new Api();
import { createApiClient, HttpClient } from '${join(outputPath, 'index.ts')}';
import { getById } from '${join(outputPath, 'operations', 'get-by-id.ts')}';
const api = createApiClient(new HttpClient(), {
users: {
getById,
},
});
try {
await api.users.getById('999');
await api.users.getById({ id: '999' });
} catch (error) {
console.log('error');
}
@@ -314,14 +326,16 @@ describe('E2E Generation', () => {
FIXTURES.WITH_AUTH,
'--output',
outputPath,
'--name',
'AuthApi',
'--mode',
'split',
]);
// Динамически импортируем сгенерированный API
const generatedFile = join(outputPath, 'AuthApi.ts');
const { Api, HttpClient } = await import(generatedFile);
const generatedFile = join(outputPath, 'index.ts');
const { createApiClient, HttpClient } = await import(generatedFile);
const { login } = await import(join(outputPath, 'operations', 'login.ts'));
const { get } = await import(join(outputPath, 'operations', 'get.ts'));
// Создаем HttpClient с securityWorker для добавления Bearer токена
const httpClient = new HttpClient({
securityWorker: (securityData: string | null) => {
@@ -334,8 +348,15 @@ describe('E2E Generation', () => {
}
}
});
const api = new Api(httpClient);
const api = createApiClient(httpClient, {
auth: {
login,
},
profile: {
get,
},
});
// Логин
const loginResult = await api.auth.login({
@@ -352,4 +373,4 @@ describe('E2E Generation', () => {
expect(profile.email).toBe('test@example.com');
}, 60000);
});
});
});